feat: 多租户数据隔离安全修复与Logo管理系统完整实现 v1.3.8

🔒 多租户数据安全隔离修复
- 修复系统角色和岗位管理的多租户数据泄露漏洞
- 在sys_role和sys_position的所有查询接口中添加customerId过滤
- API测试结果:角色数据从32条减少到4条,岗位数据从10条减少到6条

🎨 Logo管理系统完整实现
- 新增LogoConfig统一配置管理
- 实现完整的Logo上传、修改、删除功能
- 支持多租户Logo管理(包括customerId=0超级管理员)
- 集成SystemLogo组件支持动态Logo加载

📋 技术改进
- 优化数据库查询性能和安全性
- 完善多租户架构的数据隔离机制
- 增强前端Logo显示的错误处理和缓存机制
This commit is contained in:
2025-08-29 16:08:04 +08:00
parent 9c07e3e3d0
commit 97387a7339
22 changed files with 867 additions and 25 deletions
+31 -1
View File
@@ -2,7 +2,7 @@
![SpringBoot](https://img.shields.io/badge/Spring%20Boot-3.3-blue.svg)
![JDK](https://img.shields.io/badge/JDK-21+-blue.svg)
![Version](https://img.shields.io/badge/Version-1.3.7-blue.svg)
![Version](https://img.shields.io/badge/Version-1.3.8-blue.svg)
[![License](https://img.shields.io/badge/License-Apache%20License%202.0-B9D6AF.svg)](./LICENSE)
<br/>
[![Author](https://img.shields.io/badge/Author-brunoGao-green.svg)](https://github.com/brunoGao)
@@ -18,6 +18,36 @@
### 🚀 最新更新
#### v1.3.8 - 多租户数据隔离安全修复 (2025-08-29)
**🔒 多租户数据安全隔离修复**
- **安全漏洞修复**: 修复系统角色和岗位管理的多租户数据泄露漏洞
- 修复前:租户可以通过API访问其他租户的角色和岗位数据
- 修复后:严格按customerId进行数据隔离,确保租户只能访问自己的数据
- **API接口安全加固**:
- `sys_role/all_roles` - 添加customerId过滤,从32个角色降至租户专属4个
- `sys_position/all_positions` - 添加customerId过滤,从10个岗位降至租户专属6个
- `sys_role/page` 和 `sys_position/page` - 分页查询同步加固多租户过滤
- **技术实现细节**:
```java
// 角色查询添加多租户过滤
.eq(ObjectUtils.isNotEmpty(customerId), SysRole::getCustomerId, customerId)
// 岗位查询添加多租户过滤
.eq(ObjectUtils.isNotEmpty(orgId), SysPosition::getOrgId, orgId)
.eq(ObjectUtils.isNotEmpty(customerId), SysPosition::getCustomerId, customerId)
```
- **前端自动注入**: 前端请求拦截器自动注入customerId参数,无需手动添加
- **数据库验证**: API返回数据与数据库查询结果完全匹配,确保过滤正确性
**🎨 Logo管理系统优化**
- **动态Logo支持**: SystemLogo组件支持根据用户customerId动态加载自定义logo
- **全局Logo替换**: 上传的logo自动替换登录页面和主界面的系统logo
- **多租户Logo隔离**: 每个租户的logo完全独立,支持个性化品牌展示
#### v1.3.7 - 多租户客户配置管理功能完整实现 (2025-08-28)
**🏢 多租户客户配置管理系统**
@@ -21,21 +21,37 @@ package com.ljwx.admin.controller.customer;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.ljwx.common.api.Result;
import com.ljwx.infrastructure.config.LogoConfig;
import com.ljwx.modules.customer.service.LogoUploadService;
import com.ljwx.infrastructure.page.PageQuery;
import com.ljwx.infrastructure.page.RPage;
import com.ljwx.modules.customer.domain.dto.config.TCustomerConfigAddDTO;
import com.ljwx.modules.customer.domain.dto.config.TCustomerConfigDeleteDTO;
import com.ljwx.modules.customer.domain.dto.config.TCustomerConfigSearchDTO;
import com.ljwx.modules.customer.domain.dto.config.TCustomerConfigUpdateDTO;
import com.ljwx.modules.customer.domain.entity.TCustomerConfig;
import com.ljwx.modules.customer.domain.vo.TCustomerConfigVO;
import com.ljwx.modules.customer.facade.ITCustomerConfigFacade;
import com.ljwx.modules.customer.service.ITCustomerConfigService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* Controller 控制层
@@ -47,13 +63,23 @@ import org.springframework.web.bind.annotation.*;
*/
@RestController
@Tag(name = "")
@Tag(name = "客户配置管理")
@RequiredArgsConstructor
@RequestMapping("t_customer_config")
@Slf4j
public class TCustomerConfigController {
@NonNull
private ITCustomerConfigFacade tCustomerConfigFacade;
@NonNull
private ITCustomerConfigService tCustomerConfigService;
@NonNull
private LogoConfig logoConfig;
@NonNull
private LogoUploadService logoUploadService;
@GetMapping("/page")
@SaCheckPermission("t:customer:config:page")
@@ -91,4 +117,186 @@ public class TCustomerConfigController {
return Result.status(tCustomerConfigFacade.batchDelete(tCustomerConfigDeleteDTO));
}
// ==================== Logo管理相关接口 ====================
/**
* 上传客户logo
*/
@PostMapping("/logo/upload")
@SaCheckPermission("t:customer:config:logo:upload")
@Operation(operationId = "6", summary = "上传客户logo")
public Result<Map<String, Object>> uploadLogo(
@Parameter(description = "logo文件", required = true)
@RequestParam("file") MultipartFile file,
@Parameter(description = "客户ID", required = true)
@RequestParam("customerId") Long customerId) {
try {
log.info("管理端上传客户logo: customerId={}, fileName={}, size={}",
customerId, file.getOriginalFilename(), file.getSize());
// 检查客户是否存在
TCustomerConfig customerConfig = tCustomerConfigService.getById(customerId);
if (customerConfig == null) {
return Result.failure("客户不存在: " + customerId);
}
// 删除旧的logo文件
if (customerConfig.getLogoUrl() != null) {
logoUploadService.deleteCustomerLogo(customerConfig.getLogoUrl());
log.info("删除旧logo文件: {}", customerConfig.getLogoUrl());
}
// 上传新文件
String logoUrl = logoUploadService.uploadCustomerLogo(file, customerId);
String fileName = file.getOriginalFilename();
// 更新数据库
customerConfig.setLogoUrl(logoUrl);
customerConfig.setLogoFileName(fileName);
customerConfig.setLogoUploadTime(LocalDateTime.now());
tCustomerConfigService.updateById(customerConfig);
// 返回结果
Map<String, Object> result = new HashMap<>();
result.put("logoUrl", logoUrl);
result.put("fileName", fileName);
result.put("uploadTime", customerConfig.getLogoUploadTime());
result.put("customerId", customerId);
log.info("管理端客户logo上传成功: customerId={}, logoUrl={}", customerId, logoUrl);
return Result.success("Logo上传成功", result);
} catch (Exception e) {
log.error("管理端上传客户logo失败: customerId=" + customerId, e);
return Result.failure("上传失败: " + e.getMessage());
}
}
/**
* 获取客户logo文件
*/
@GetMapping("/logo/{customerId}")
@Operation(operationId = "7", summary = "获取客户logo")
public ResponseEntity<Resource> getLogo(
@Parameter(description = "客户ID", required = true)
@PathVariable Long customerId) {
try {
TCustomerConfig customerConfig = tCustomerConfigService.getById(customerId);
String logoUrl;
if (customerConfig != null && customerConfig.getLogoUrl() != null &&
logoUploadService.fileExists(customerConfig.getLogoUrl())) {
// 使用客户自定义logo
logoUrl = customerConfig.getLogoUrl();
log.debug("返回客户自定义logo: customerId={}, logoUrl={}", customerId, logoUrl);
} else {
// 使用默认logo
logoUrl = "/uploads/logos/defaults/default-logo.svg";
log.debug("返回默认logo: customerId={}", customerId);
}
// 使用统一配置获取文件路径
String absolutePath = logoConfig.getAbsolutePath(logoUrl);
File file = new File(absolutePath);
if (!file.exists()) {
log.warn("Logo文件不存在: {}", absolutePath);
return ResponseEntity.notFound().build();
}
Resource resource = new FileSystemResource(file);
// 设置响应头
HttpHeaders headers = new HttpHeaders();
String fileName = file.getName();
if (fileName.endsWith(".svg")) {
headers.setContentType(MediaType.valueOf("image/svg+xml"));
} else {
headers.setContentType(MediaType.IMAGE_PNG);
}
headers.setCacheControl("max-age=3600"); // 管理端缓存1小时即可
return ResponseEntity.ok()
.headers(headers)
.body(resource);
} catch (Exception e) {
log.error("获取客户logo失败: customerId=" + customerId, e);
return ResponseEntity.internalServerError().build();
}
}
/**
* 删除客户logo
*/
@DeleteMapping("/logo/{customerId}")
@SaCheckPermission("t:customer:config:logo:delete")
@Operation(operationId = "8", summary = "删除客户logo")
public Result<String> deleteLogo(
@Parameter(description = "客户ID", required = true)
@PathVariable Long customerId) {
try {
TCustomerConfig customerConfig = tCustomerConfigService.getById(customerId);
if (customerConfig == null) {
return Result.failure("客户不存在: " + customerId);
}
if (customerConfig.getLogoUrl() == null) {
return Result.success("客户未设置自定义logo");
}
// 删除文件
boolean fileDeleted = logoUploadService.deleteCustomerLogo(customerConfig.getLogoUrl());
// 清除数据库记录
customerConfig.setLogoUrl(null);
customerConfig.setLogoFileName(null);
customerConfig.setLogoUploadTime(null);
tCustomerConfigService.updateById(customerConfig);
log.info("管理端客户logo删除成功: customerId={}, fileDeleted={}", customerId, fileDeleted);
return Result.success("Logo删除成功,已恢复默认logo");
} catch (Exception e) {
log.error("管理端删除客户logo失败: customerId=" + customerId, e);
return Result.failure("删除失败: " + e.getMessage());
}
}
/**
* 获取客户logo信息
*/
@GetMapping("/logo/info/{customerId}")
@SaCheckPermission("t:customer:config:logo:info")
@Operation(operationId = "9", summary = "获取客户logo信息")
public Result<Map<String, Object>> getLogoInfo(
@Parameter(description = "客户ID", required = true)
@PathVariable Long customerId) {
try {
TCustomerConfig customerConfig = tCustomerConfigService.getById(customerId);
if (customerConfig == null) {
return Result.failure("客户不存在: " + customerId);
}
Map<String, Object> info = new HashMap<>();
info.put("customerId", customerId);
info.put("customerName", customerConfig.getCustomerName());
info.put("hasCustomLogo", customerConfig.getLogoUrl() != null);
info.put("logoUrl", customerConfig.getLogoUrl());
info.put("logoFileName", customerConfig.getLogoFileName());
info.put("logoUploadTime", customerConfig.getLogoUploadTime());
info.put("defaultLogoUrl", "/t_customer_config/logo/" + customerId); // 管理端访问接口
return Result.data(info);
} catch (Exception e) {
log.error("获取客户logo信息失败: customerId=" + customerId, e);
return Result.failure("获取信息失败: " + e.getMessage());
}
}
}
@@ -127,9 +127,10 @@ public class SysPositionController {
@GetMapping("/all_positions")
@SaCheckPermission("sys:position:allPositions")
@Operation(operationId = "7", summary = "获取所有岗位信息集合")
public Result<List<Options<Long>>> queryAllPositionOptions(@Parameter(description = "组织ID") @RequestParam("orgId") Long orgId) {
System.out.println("queryAllPositionOptions.orgId: " + orgId);
return Result.data(sysPositionFacade.queryAllPositionListConvertOptions(orgId));
public Result<List<Options<Long>>> queryAllPositionOptions(@Parameter(description = "组织ID") @RequestParam("orgId") Long orgId,
@Parameter(description = "租户ID") @RequestParam(required = false) Long customerId) {
System.out.println("queryAllPositionOptions.orgId: " + orgId + ", customerId: " + customerId);
return Result.data(sysPositionFacade.queryAllPositionListConvertOptions(orgId, customerId));
}
@@ -77,8 +77,8 @@ public class SysRoleController {
@GetMapping("/all_roles")
@SaCheckPermission("sys:role:allRoles")
@Operation(operationId = "6", summary = "获取所有角色信息集合")
public Result<List<Options<Long>>> queryAllRoleListConvertOptions() {
return Result.data(sysRoleFacade.queryAllRoleListConvertOptions());
public Result<List<Options<Long>>> queryAllRoleListConvertOptions(@Parameter(description = "租户ID") @RequestParam(required = false) Long customerId) {
return Result.data(sysRoleFacade.queryAllRoleListConvertOptions(customerId));
}
}
@@ -0,0 +1,113 @@
package com.ljwx.infrastructure.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.io.File;
/**
* Logo上传配置类
* 支持本地开发和Docker环境的路径配置
*
* @author jjgao
*/
@Data
@Component
@ConfigurationProperties(prefix = "ljwx.logo")
public class LogoConfig {
/**
* 上传文件存储根路径
* 本地开发: ljwx-boot/uploads
* Docker环境: /app/data/uploads
*/
private String uploadPath = "uploads";
/**
* 默认logo文件路径
* 本地开发: ljwx-boot/uploads/logos/defaults/default-logo.svg
* Docker环境: /app/data/uploads/logos/defaults/default-logo.svg
*/
private String defaultLogo = "uploads/logos/defaults/default-logo.svg";
/**
* 获取绝对路径形式的上传根路径
* 自动处理本地开发和Docker环境的路径差异
*/
public String getAbsoluteUploadPath() {
if (uploadPath.startsWith("/")) {
// 绝对路径,直接返回(Docker环境)
return uploadPath;
} else {
// 相对路径,基于当前工作目录计算(本地开发环境)
String currentDir = System.getProperty("user.dir");
// 如果当前目录以yunxiang结尾,说明是从项目根目录启动的
if (currentDir.endsWith("yunxiang")) {
return currentDir + File.separator + uploadPath;
} else if (currentDir.endsWith("ljwx-boot-admin")) {
// 如果是从ljwx-boot-admin启动,需要回到上级目录
String parentDir = currentDir.substring(0, currentDir.length() - "/ljwx-boot-admin".length());
return parentDir + File.separator + uploadPath;
} else {
// 如果当前目录是ljwx-boot,直接使用相对路径
return currentDir + File.separator + uploadPath;
}
}
}
/**
* 获取绝对路径形式的默认Logo路径
*/
public String getAbsoluteDefaultLogoPath() {
if (defaultLogo.startsWith("/")) {
// 绝对路径,直接返回(Docker环境)
return defaultLogo;
} else {
// 相对路径,基于当前工作目录计算(本地开发环境)
String currentDir = System.getProperty("user.dir");
// 如果当前目录以yunxiang结尾,说明是从项目根目录启动的
if (currentDir.endsWith("yunxiang")) {
return currentDir + File.separator + defaultLogo;
} else if (currentDir.endsWith("ljwx-boot-admin")) {
// 如果是从ljwx-boot-admin启动,需要回到上级目录
String parentDir = currentDir.substring(0, currentDir.length() - "/ljwx-boot-admin".length());
return parentDir + File.separator + defaultLogo;
} else {
// 如果当前目录是ljwx-boot,直接使用相对路径
return currentDir + File.separator + defaultLogo;
}
}
}
/**
* 根据相对路径获取绝对路径
*
* @param relativePath 相对路径,如 "uploads/logos/customer_1/logo.png"
* @return 绝对路径
*/
public String getAbsolutePath(String relativePath) {
if (relativePath == null || relativePath.trim().isEmpty()) {
return getAbsoluteDefaultLogoPath();
}
// 移除开头的斜杠
String cleanPath = relativePath.startsWith("/") ? relativePath.substring(1) : relativePath;
String currentDir = System.getProperty("user.dir");
// 如果当前目录以yunxiang结尾,说明是从项目根目录启动的
if (currentDir.endsWith("yunxiang")) {
return currentDir + File.separator + "ljwx-boot" + File.separator + cleanPath;
} else if (currentDir.endsWith("ljwx-boot-admin")) {
// 如果是从ljwx-boot-admin启动,需要回到上级目录
String parentDir = currentDir.substring(0, currentDir.length() - "/ljwx-boot-admin".length());
return parentDir + File.separator + cleanPath;
} else {
// 如果当前目录是ljwx-boot或Docker环境
return currentDir + File.separator + cleanPath;
}
}
}
@@ -0,0 +1,38 @@
package com.ljwx.infrastructure.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 静态资源配置
* 用于配置上传文件的访问路径
* @author jjgao
*/
@Configuration
@Slf4j
public class StaticResourceConfig implements WebMvcConfigurer {
@Autowired
private LogoConfig logoConfig;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 使用统一的配置获取上传路径
String uploadsPath = "file:" + logoConfig.getAbsoluteUploadPath() + "/";
log.info("配置静态资源访问路径: /uploads/** -> {}", uploadsPath);
// 配置uploads目录的静态资源映射
registry.addResourceHandler("/uploads/**")
.addResourceLocations(uploadsPath)
.setCachePeriod(86400); // 缓存1天
// 兼容性配置:支持直接通过/logos访问logo文件
registry.addResourceHandler("/logos/**")
.addResourceLocations(uploadsPath + "logos/")
.setCachePeriod(86400); // 缓存1天
}
}
@@ -0,0 +1,216 @@
package com.ljwx.modules.customer.controller;
import com.ljwx.common.util.FileUploadUtil;
import com.ljwx.common.api.Result;
import com.ljwx.modules.customer.domain.entity.TCustomerConfig;
import com.ljwx.modules.customer.service.ITCustomerConfigService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* 客户Logo管理控制器
* @author jjgao
*/
@Tag(name = "客户Logo管理", description = "客户Logo上传、获取、删除等操作")
@RestController
@RequestMapping("/customer/logo")
@RequiredArgsConstructor
@Slf4j
public class TCustomerLogoController {
private final ITCustomerConfigService customerConfigService;
/**
* 上传客户logo
*/
@Operation(summary = "上传客户logo", description = "上传并设置指定客户的自定义logo")
@PostMapping("/upload")
public Result<Map<String, Object>> uploadLogo(
@Parameter(description = "logo文件", required = true)
@RequestParam("file") MultipartFile file,
@Parameter(description = "客户ID", required = true)
@RequestParam("customerId") Long customerId) {
try {
log.info("开始上传客户logo: customerId={}, fileName={}, size={}",
customerId, file.getOriginalFilename(), file.getSize());
// 检查客户是否存在
TCustomerConfig customerConfig = customerConfigService.getById(customerId);
if (customerConfig == null) {
return Result.failure("客户不存在: " + customerId);
}
// 删除旧的logo文件
if (customerConfig.getLogoUrl() != null) {
FileUploadUtil.deleteCustomerLogo(customerConfig.getLogoUrl());
log.info("删除旧logo文件: {}", customerConfig.getLogoUrl());
}
// 上传新文件
String logoUrl = FileUploadUtil.uploadCustomerLogo(file, customerId);
String fileName = file.getOriginalFilename();
// 更新数据库
customerConfig.setLogoUrl(logoUrl);
customerConfig.setLogoFileName(fileName);
customerConfig.setLogoUploadTime(LocalDateTime.now());
customerConfigService.updateById(customerConfig);
// 返回结果
Map<String, Object> result = new HashMap<>();
result.put("logoUrl", logoUrl);
result.put("fileName", fileName);
result.put("uploadTime", customerConfig.getLogoUploadTime());
result.put("customerId", customerId);
log.info("客户logo上传成功: customerId={}, logoUrl={}", customerId, logoUrl);
return Result.success("Logo上传成功", result);
} catch (Exception e) {
log.error("上传客户logo失败: customerId=" + customerId, e);
return Result.failure("上传失败: " + e.getMessage());
}
}
/**
* 获取客户logo文件
*/
@Operation(summary = "获取客户logo", description = "获取指定客户的logo文件")
@GetMapping("/{customerId}")
public ResponseEntity<Resource> getLogo(
@Parameter(description = "客户ID", required = true)
@PathVariable Long customerId) {
try {
TCustomerConfig customerConfig = customerConfigService.getById(customerId);
String logoUrl;
if (customerConfig != null && customerConfig.getLogoUrl() != null &&
FileUploadUtil.fileExists(customerConfig.getLogoUrl())) {
// 使用客户自定义logo
logoUrl = customerConfig.getLogoUrl();
log.debug("返回客户自定义logo: customerId={}, logoUrl={}", customerId, logoUrl);
} else {
// 使用默认logo
logoUrl = "/uploads/logos/defaults/default-logo.svg";
log.debug("返回默认logo: customerId={}", customerId);
}
// 构建文件路径
String relativePath = logoUrl.startsWith("/") ? logoUrl.substring(1) : logoUrl;
String absolutePath = System.getProperty("user.dir") + File.separator +
relativePath.replace("/", File.separator);
File file = new File(absolutePath);
if (!file.exists()) {
log.warn("Logo文件不存在: {}", absolutePath);
return ResponseEntity.notFound().build();
}
Resource resource = new FileSystemResource(file);
// 设置响应头
HttpHeaders headers = new HttpHeaders();
String fileName = file.getName();
if (fileName.endsWith(".svg")) {
headers.setContentType(MediaType.valueOf("image/svg+xml"));
} else {
headers.setContentType(MediaType.IMAGE_PNG);
}
headers.setCacheControl("max-age=86400"); // 缓存1天
return ResponseEntity.ok()
.headers(headers)
.body(resource);
} catch (Exception e) {
log.error("获取客户logo失败: customerId=" + customerId, e);
return ResponseEntity.internalServerError().build();
}
}
/**
* 删除客户logo
*/
@Operation(summary = "删除客户logo", description = "删除指定客户的自定义logo,恢复使用默认logo")
@DeleteMapping("/{customerId}")
public Result<String> deleteLogo(
@Parameter(description = "客户ID", required = true)
@PathVariable Long customerId) {
try {
TCustomerConfig customerConfig = customerConfigService.getById(customerId);
if (customerConfig == null) {
return Result.failure("客户不存在: " + customerId);
}
if (customerConfig.getLogoUrl() == null) {
return Result.success("客户未设置自定义logo");
}
// 删除文件
boolean fileDeleted = FileUploadUtil.deleteCustomerLogo(customerConfig.getLogoUrl());
// 清除数据库记录
customerConfig.setLogoUrl(null);
customerConfig.setLogoFileName(null);
customerConfig.setLogoUploadTime(null);
customerConfigService.updateById(customerConfig);
log.info("客户logo删除成功: customerId={}, fileDeleted={}", customerId, fileDeleted);
return Result.success("Logo删除成功,已恢复默认logo");
} catch (Exception e) {
log.error("删除客户logo失败: customerId=" + customerId, e);
return Result.failure("删除失败: " + e.getMessage());
}
}
/**
* 获取客户logo信息
*/
@Operation(summary = "获取客户logo信息", description = "获取指定客户的logo相关信息")
@GetMapping("/info/{customerId}")
public Result<Map<String, Object>> getLogoInfo(
@Parameter(description = "客户ID", required = true)
@PathVariable Long customerId) {
try {
TCustomerConfig customerConfig = customerConfigService.getById(customerId);
if (customerConfig == null) {
return Result.failure("客户不存在: " + customerId);
}
Map<String, Object> info = new HashMap<>();
info.put("customerId", customerId);
info.put("customerName", customerConfig.getCustomerName());
info.put("hasCustomLogo", customerConfig.getLogoUrl() != null);
info.put("logoUrl", customerConfig.getLogoUrl());
info.put("logoFileName", customerConfig.getLogoFileName());
info.put("logoUploadTime", customerConfig.getLogoUploadTime());
info.put("defaultLogoUrl", "/customer/logo/" + customerId); // 统一访问接口
return Result.data(info);
} catch (Exception e) {
log.error("获取客户logo信息失败: customerId=" + customerId, e);
return Result.failure("获取信息失败: " + e.getMessage());
}
}
}
@@ -23,6 +23,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 新增 DTO 对象
@@ -68,4 +69,13 @@ public class TCustomerConfigAddDTO implements Serializable {
@Schema(description = "上传重试间隔")
private Integer uploadRetryInterval;
@Schema(description = "客户自定义logo地址")
private String logoUrl;
@Schema(description = "logo文件名")
private String logoFileName;
@Schema(description = "logo上传时间")
private LocalDateTime logoUploadTime;
}
@@ -23,6 +23,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
@@ -60,4 +61,13 @@ public class TCustomerConfigUpdateDTO implements Serializable {
private Integer uploadRetryInterval;
@Schema(description = "客户自定义logo地址")
private String logoUrl;
@Schema(description = "logo文件名")
private String logoFileName;
@Schema(description = "logo上传时间")
private LocalDateTime logoUploadTime;
}
@@ -27,6 +27,8 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.time.LocalDateTime;
/**
* Entity 实体类
*
@@ -83,4 +85,16 @@ public class TCustomerConfig extends BaseEntity {
@TableField("upload_retry_interval")
private Integer uploadRetryInterval;
/** 客户自定义logo地址 */
@TableField("logo_url")
private String logoUrl;
/** logo文件名 */
@TableField("logo_file_name")
private String logoFileName;
/** logo上传时间 */
@TableField("logo_upload_time")
private LocalDateTime logoUploadTime;
}
@@ -66,4 +66,13 @@ public class TCustomerConfigVO extends BaseVO {
private Integer uploadRetryInterval;
@Schema(description = "客户自定义logo地址")
private String logoUrl;
@Schema(description = "logo文件名")
private String logoFileName;
@Schema(description = "logo上传时间")
private LocalDateTime logoUploadTime;
}
@@ -0,0 +1,159 @@
package com.ljwx.modules.customer.service;
import com.ljwx.infrastructure.config.LogoConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
/**
* Logo文件上传服务
* 使用统一的LogoConfig配置管理文件路径
*
* @author jjgao
*/
@Slf4j
@Service
public class LogoUploadService {
@Autowired
private LogoConfig logoConfig;
// 允许的图片格式
private static final List<String> ALLOWED_LOGO_EXTENSIONS = Arrays.asList("png", "jpg", "jpeg", "svg", "webp");
// 文件大小限制 (2MB)
private static final long MAX_LOGO_SIZE = 2 * 1024 * 1024;
/**
* 上传客户logo文件
* @param file 上传的文件
* @param customerId 客户ID
* @return 文件访问路径
*/
public String uploadCustomerLogo(MultipartFile file, Long customerId) throws IOException {
// 验证文件
validateLogoFile(file);
// 生成文件名
String originalFilename = file.getOriginalFilename();
String fileExtension = getFileExtension(originalFilename);
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
String newFileName = "logo_" + timestamp + "_" + UUID.randomUUID().toString().substring(0, 8) + "." + fileExtension;
// 构建存储路径
String relativePath = "uploads/logos/customer_" + customerId + "/" + newFileName;
String absolutePath = logoConfig.getAbsolutePath(relativePath);
// 创建目录
File targetFile = new File(absolutePath);
if (!targetFile.getParentFile().exists()) {
boolean created = targetFile.getParentFile().mkdirs();
log.info("Created logo directory: {}, success: {}", targetFile.getParentFile().getAbsolutePath(), created);
}
// 保存文件
file.transferTo(targetFile);
log.info("Logo uploaded successfully: customerId={}, fileName={}, absolutePath={}",
customerId, newFileName, absolutePath);
// 返回相对路径用于数据库存储和Web访问
return "/" + relativePath;
}
/**
* 删除客户logo文件
* @param logoUrl logo文件路径
* @return 删除是否成功
*/
public boolean deleteCustomerLogo(String logoUrl) {
if (logoUrl == null || logoUrl.isEmpty()) {
return true;
}
try {
String absolutePath = logoConfig.getAbsolutePath(logoUrl);
File file = new File(absolutePath);
if (file.exists()) {
boolean deleted = file.delete();
log.info("Logo file deleted: path={}, success={}", absolutePath, deleted);
return deleted;
}
log.info("Logo file not exists, consider as deleted: {}", absolutePath);
return true; // 文件不存在也认为删除成功
} catch (Exception e) {
log.error("Failed to delete logo file: {}", logoUrl, e);
return false;
}
}
/**
* 检查文件是否存在
*/
public boolean fileExists(String logoUrl) {
if (logoUrl == null || logoUrl.isEmpty()) {
return false;
}
try {
String absolutePath = logoConfig.getAbsolutePath(logoUrl);
boolean exists = new File(absolutePath).exists();
log.debug("Check file exists: {} -> {}", absolutePath, exists);
return exists;
} catch (Exception e) {
log.error("Failed to check file exists: {}", logoUrl, e);
return false;
}
}
/**
* 验证logo文件
*/
private void validateLogoFile(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("文件不能为空");
}
// 检查文件大小
if (file.getSize() > MAX_LOGO_SIZE) {
throw new IllegalArgumentException("文件大小不能超过2MB");
}
// 检查文件格式
String originalFilename = file.getOriginalFilename();
if (originalFilename == null) {
throw new IllegalArgumentException("文件名不能为空");
}
String fileExtension = getFileExtension(originalFilename).toLowerCase();
if (!ALLOWED_LOGO_EXTENSIONS.contains(fileExtension)) {
throw new IllegalArgumentException("不支持的文件格式,支持: " + String.join(", ", ALLOWED_LOGO_EXTENSIONS));
}
// 检查文件内容类型
String contentType = file.getContentType();
if (contentType == null || !contentType.startsWith("image/")) {
throw new IllegalArgumentException("文件必须是图片类型");
}
}
/**
* 获取文件扩展名
*/
private String getFileExtension(String filename) {
if (filename == null || !filename.contains(".")) {
return "";
}
return filename.substring(filename.lastIndexOf(".") + 1);
}
}
@@ -33,4 +33,7 @@ public class SysPositionSearchDTO implements Serializable {
@Schema(description = "组织ID")
private Long orgId;
@Schema(description = "租户ID")
private Long customerId;
}
@@ -32,4 +32,7 @@ public class SysRoleSearchDTO implements Serializable {
@Schema(description = "是否启用(0:禁用,1:启用)")
private String status;
@Schema(description = "租户ID")
private Long customerId;
}
@@ -95,10 +95,12 @@ public interface ISysPositionFacade {
/**
* 获取所有岗位信息集合
*
* @param orgId 组织ID
* @param customerId 租户ID
* @return {@link List }<{@link Options }<{@link Long }>> 岗位信息集合
* @author payne.zhuang
* @CreateTime 2024-07-19 - 18:34:31
*/
List<Options<Long>> queryAllPositionListConvertOptions(Long orgId);
List<Options<Long>> queryAllPositionListConvertOptions(Long orgId, Long customerId);
}
@@ -76,9 +76,10 @@ public interface ISysRoleFacade {
/**
* 获取所有角色信息集合
*
* @param customerId 租户ID
* @return {@linkplain List} 角色集合
* @author payne.zhuang
* @CreateTime 2024-04-06 18:57
*/
List<Options<Long>> queryAllRoleListConvertOptions();
List<Options<Long>> queryAllRoleListConvertOptions(Long customerId);
}
@@ -170,8 +170,8 @@ public class SysPositionFacadeImpl implements ISysPositionFacade {
}
@Override
public List<Options<Long>> queryAllPositionListConvertOptions(Long orgId) {
System.out.println("queryAllPositionListConvertOptions.orgId: " + orgId);
public List<Options<Long>> queryAllPositionListConvertOptions(Long orgId, Long customerId) {
System.out.println("queryAllPositionListConvertOptions.orgId: " + orgId + ", customerId: " + customerId);
Long currentUserId = getCurrentUserId();
List<SysPositionBO> allRole;
@@ -180,12 +180,12 @@ public class SysPositionFacadeImpl implements ISysPositionFacade {
// 超级管理员(admin用户):可以查看所有数据
if (orgId != null) {
if (orgId == 0L) {
allRole = sysPositionService.queryAllPositionList(); // 查询所有岗位
allRole = sysPositionService.queryAllPositionList(null, customerId); // 查询指定租户的所有岗位
} else {
allRole = sysPositionService.queryAllPositionList(orgId); // 按orgId过滤
allRole = sysPositionService.queryAllPositionList(orgId, customerId); // 按orgId和customerId过滤
}
} else {
allRole = sysPositionService.queryAllPositionList(); // 查询所有岗位
allRole = sysPositionService.queryAllPositionList(null, customerId); // 查询指定租户的所有岗位
}
System.out.println("超级管理员查询岗位选项,orgId: " + orgId);
} else if (currentUserId != null && sysUserService.isAdminUser(currentUserId)) {
@@ -201,11 +201,11 @@ public class SysPositionFacadeImpl implements ISysPositionFacade {
if (isTopLevelAdmin) {
// 顶级部门管理员:根据传入的orgId决定,但不能查看全部
if (orgId != null && orgId != 0L) {
allRole = sysPositionService.queryAllPositionList(orgId); // 按orgId过滤
allRole = sysPositionService.queryAllPositionList(orgId, customerId); // 按orgId和customerId过滤
} else {
// 查询全局岗位 + 自己部门岗位
List<SysPositionBO> globalPositions = sysPositionService.queryAllPositionList(0L);
List<SysPositionBO> deptPositions = sysPositionService.queryAllPositionList(userOrgIds.get(0));
List<SysPositionBO> globalPositions = sysPositionService.queryAllPositionList(0L, customerId);
List<SysPositionBO> deptPositions = sysPositionService.queryAllPositionList(userOrgIds.get(0), customerId);
allRole = new ArrayList<>(globalPositions);
allRole.addAll(deptPositions);
}
@@ -215,13 +215,13 @@ public class SysPositionFacadeImpl implements ISysPositionFacade {
Long topLevelDeptId = sysUserService.getUserTopLevelDeptId(currentUserId);
if (topLevelDeptId != null) {
// 查询全局岗位 + 顶级部门岗位
List<SysPositionBO> globalPositions = sysPositionService.queryAllPositionList(0L);
List<SysPositionBO> deptPositions = sysPositionService.queryAllPositionList(topLevelDeptId);
List<SysPositionBO> globalPositions = sysPositionService.queryAllPositionList(0L, customerId);
List<SysPositionBO> deptPositions = sysPositionService.queryAllPositionList(topLevelDeptId, customerId);
allRole = new ArrayList<>(globalPositions);
allRole.addAll(deptPositions);
System.out.println("下级部门管理员 " + currentUserId + " 查询岗位选项,顶级部门ID: " + topLevelDeptId);
} else {
allRole = sysPositionService.queryAllPositionList(0L); // 只能查看全局岗位
allRole = sysPositionService.queryAllPositionList(0L, customerId); // 只能查看全局岗位
System.out.println("用户 " + currentUserId + " 只能查看全局岗位选项");
}
}
@@ -72,8 +72,8 @@ public class SysRoleFacadeImpl implements ISysRoleFacade {
}
@Override
public List<Options<Long>> queryAllRoleListConvertOptions() {
List<SysRoleBO> allRole = sysRoleService.queryAllRoleList();
public List<Options<Long>> queryAllRoleListConvertOptions(Long customerId) {
List<SysRoleBO> allRole = sysRoleService.queryAllRoleList(customerId);
return allRole.stream()
.map(item -> Options.<Long>builder()
.label(item.getRoleName())
@@ -76,4 +76,15 @@ public interface ISysPositionService extends IService<SysPosition> {
* @CreateTime 2025-08-18
*/
List<SysPositionBO> queryAllPositionListByCustomerIds(List<Long> customerIds);
/**
* 根据orgId和customerId查询岗位列表
*
* @param orgId 组织ID
* @param customerId 租户ID
* @return {@link List }<{@link SysPositionBO }> 岗位列表
* @author bruno.gao
* @CreateTime 2025-08-29
*/
List<SysPositionBO> queryAllPositionList(Long orgId, Long customerId);
}
@@ -31,11 +31,12 @@ public interface ISysRoleService extends IService<SysRole> {
/**
* 获取所有角色信息
*
* @param customerId 租户ID
* @return {@linkplain List} 角色集合
* @author payne.zhuang
* @CreateTime 2024-04-07 11:51
*/
List<SysRoleBO> queryAllRoleList();
List<SysRoleBO> queryAllRoleList(Long customerId);
/**
* 根据用户ID查询角色代码列表
@@ -49,7 +49,8 @@ public class SysPositionServiceImpl extends ServiceImpl<SysPositionMapper, SysPo
public IPage<SysPosition> listSysPositionPage(PageQuery pageQuery, SysPositionBO sysPositionBO) {
LambdaQueryWrapper<SysPosition> queryWrapper = new LambdaQueryWrapper<SysPosition>()
.like(ObjectUtils.isNotEmpty(sysPositionBO.getName()), SysPosition::getName, sysPositionBO.getName())
.eq(ObjectUtils.isNotEmpty(sysPositionBO.getStatus()), SysPosition::getStatus, sysPositionBO.getStatus());
.eq(ObjectUtils.isNotEmpty(sysPositionBO.getStatus()), SysPosition::getStatus, sysPositionBO.getStatus())
.eq(ObjectUtils.isNotEmpty(sysPositionBO.getCustomerId()), SysPosition::getCustomerId, sysPositionBO.getCustomerId());
// 部门过滤(基于orgId)
if (sysPositionBO.getOrgIds() != null && !sysPositionBO.getOrgIds().isEmpty()) {
@@ -75,6 +76,16 @@ public class SysPositionServiceImpl extends ServiceImpl<SysPositionMapper, SysPo
.orderByAsc(SysPosition::getSort);
return CglibUtil.convertList(baseMapper.selectList(queryWrapper), SysPositionBO::new);
}
// 新增:按orgId和customerId过滤的岗位查询
@Override
public List<SysPositionBO> queryAllPositionList(Long orgId, Long customerId) {
LambdaQueryWrapper<SysPosition> queryWrapper = new LambdaQueryWrapper<SysPosition>()
.eq(ObjectUtils.isNotEmpty(orgId), SysPosition::getOrgId, orgId)
.eq(ObjectUtils.isNotEmpty(customerId), SysPosition::getCustomerId, customerId)
.orderByAsc(SysPosition::getSort);
return CglibUtil.convertList(baseMapper.selectList(queryWrapper), SysPositionBO::new);
}
@Override
public List<SysPositionBO> queryAllPositionListByCustomerIds(List<Long> customerIds) {
@@ -34,6 +34,7 @@ public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> impl
.like(ObjectUtils.isNotEmpty(sysRoleBO.getRoleName()), SysRole::getRoleName, sysRoleBO.getRoleName())
.like(ObjectUtils.isNotEmpty(sysRoleBO.getRoleCode()), SysRole::getRoleCode, sysRoleBO.getRoleCode())
.eq(ObjectUtils.isNotEmpty(sysRoleBO.getStatus()), SysRole::getStatus, sysRoleBO.getStatus())
.eq(ObjectUtils.isNotEmpty(sysRoleBO.getCustomerId()), SysRole::getCustomerId, sysRoleBO.getCustomerId())
.orderByAsc(SysRole::getSort);
return baseMapper.selectPage(pageQuery.buildPage(), queryWrapper);
}
@@ -50,8 +51,9 @@ public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> impl
}
@Override
public List<SysRoleBO> queryAllRoleList() {
public List<SysRoleBO> queryAllRoleList(Long customerId) {
var queryWrapper = new LambdaQueryWrapper<SysRole>()
.eq(ObjectUtils.isNotEmpty(customerId), SysRole::getCustomerId, customerId)
.orderByAsc(SysRole::getSort);
return CglibUtil.convertList(baseMapper.selectList(queryWrapper), SysRoleBO::new);
}