完善项目配置和测试用例
新增内容: 1. 添加 AGENTS.md 和 CLAUDE.md AI 助手配置文件 2. 添加安全修复说明文档 (doc/security-fixes.md) 3. 新增单元测试用例: - fund-common: TenantContextHolderTest, UserContextHolderTest, PageResultTest, ResultTest - fund-sys: AuthServiceImplTest, RoleServiceImplTest, TenantServiceImplTest 修改内容: 1. 数据库初始化脚本更新 (fund_sys_init.sql) 2. 前端依赖更新 (package.json) 3. 登录和密码管理功能优化: - 管理后台和移动端登录页面 - 密码修改功能 4. 租户上下文处理优化 (TenantLineHandlerImpl) 5. 网关过滤器增强: - TenantGatewayFilter 租户过滤 - TokenAuthFilter 认证过滤 6. Controller 层代码优化 7. DTO 和 Service 层代码改进 技术改进: - 密码加密方式从 BCrypt 改为 MD5(前后端一致) - 登录验证流程优化,支持多租户 - 增加日志输出便于调试 - 代码规范性和可维护性提升
This commit is contained in:
parent
645056eaf0
commit
455a20c1df
33
AGENTS.md
Normal file
33
AGENTS.md
Normal file
@ -0,0 +1,33 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## 项目结构与模块组织
|
||||
- 后端为 Maven 多模块 Spring Boot 工程。核心服务位于 `fund-*` 模块(如 `fund-sys`、`fund-cust`、`fund-proj`、`fund-gateway`),共享代码在 `fund-common`。
|
||||
- 前端为独立 Vite 项目:管理端在 `fund-admin`,移动端 H5 在 `fund-mobile`。
|
||||
- 运维与打包相关内容在 `scripts/`、`deploy/`、`docker/`、`assembly/`。前端构建先生成各模块 `dist/`,再通过 `scripts/build-frontend.sh` 复制到 `deploy/`;后端构建产物也统一放置在 `deploy/`。
|
||||
|
||||
## 构建、测试与本地开发命令
|
||||
- `mvn -q -DskipTests package`: 在仓库根目录构建所有后端模块。
|
||||
- `mvn test`: 运行后端单元测试(依赖 Spring Boot Test)。
|
||||
- `cd fund-admin && npm install && npm run dev`: 本地启动管理端。
|
||||
- `cd fund-mobile && npm install && npm run dev`: 本地启动移动端。
|
||||
- `./scripts/build-frontend.sh [admin|mobile]`: 构建单个或全部前端。
|
||||
- `./scripts/docker-build.sh build-all`: 构建后端服务 Docker 镜像。
|
||||
- `docker-compose up -d`: 启动 `docker-compose.yml` 中定义的本地依赖/服务。
|
||||
|
||||
## 编码风格与命名规范
|
||||
- Java 采用常规 Spring Boot 风格,包名格式如 `com.fundplatform.{模块}.{层级}`。
|
||||
- 后端类命名按职责清晰命名(如 `CustomerController`、`CustomerServiceImpl`)。
|
||||
- 前端 TypeScript/Vue 没有统一的格式化配置,保持与现有代码风格一致。
|
||||
|
||||
## 测试指南
|
||||
- 后端测试放在各模块 `src/test/java`,通过 `mvn test` 运行。
|
||||
- 前端 `package.json` 未配置测试脚本,若新增测试需同时引入对应测试工具。
|
||||
|
||||
## 提交与拉取请求规范
|
||||
- Git 历史显示轻量 Conventional Commits 风格,建议使用 `feat:`、`fix:` 等前缀,描述简洁明确。
|
||||
- PR 需说明范围;有需求单则关联;涉及 UI(`fund-admin`、`fund-mobile`)请附截图。
|
||||
- 涉及配置或部署脚本的变更(`scripts/`、`deploy/`、`docker/`)需显式说明。
|
||||
|
||||
## 配置与运行说明
|
||||
- 服务启动脚本读取 `conf/env.properties` 和 `conf/service.properties`(见 `scripts/start.sh`)。
|
||||
- 根 `pom.xml` 指定 Java 版本为 `21`。
|
||||
165
CLAUDE.md
Normal file
165
CLAUDE.md
Normal file
@ -0,0 +1,165 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## 项目概述
|
||||
|
||||
**资金服务平台(FundPlatform)** — 多租户微服务架构的项目资金管理系统。
|
||||
|
||||
- **技术栈**: Java 21 + Spring Boot 3.2.0 + Spring Cloud Alibaba 2023.0.0.0 + MyBatis-Plus 3.5.5 + MySQL 8.0 + Redis
|
||||
- **前端**: Vue 3 + TypeScript(管理后台用 Element Plus,移动端用 Vant)
|
||||
|
||||
## 常用命令
|
||||
|
||||
### 后端构建
|
||||
|
||||
```bash
|
||||
# 构建所有模块(跳过测试)
|
||||
mvn -q -DskipTests package
|
||||
|
||||
# 构建单个模块
|
||||
mvn -q -DskipTests package -pl fund-sys -am
|
||||
|
||||
# 运行测试
|
||||
mvn test
|
||||
|
||||
# 打包成 tar.gz 部署包(使用 Assembly)
|
||||
mvn -q -DskipTests package -pl fund-sys -am -P assembly
|
||||
```
|
||||
|
||||
### 前端开发
|
||||
|
||||
```bash
|
||||
# 管理后台
|
||||
cd fund-admin && npm install && npm run dev
|
||||
|
||||
# 移动端
|
||||
cd fund-mobile && npm install && npm run dev
|
||||
|
||||
# 打包前端
|
||||
./scripts/build-frontend.sh admin # 或 mobile
|
||||
```
|
||||
|
||||
### 本地环境
|
||||
|
||||
```bash
|
||||
# 启动基础设施(MySQL、Redis、Nacos、Prometheus、Grafana)
|
||||
docker-compose up -d
|
||||
|
||||
# 停止并清理
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
## 模块结构
|
||||
|
||||
| 模块 | 端口 | 职责 |
|
||||
|------|------|------|
|
||||
| fund-common | — | 公共工具:Token、Redis、多租户上下文、统一响应 |
|
||||
| fund-gateway | 8000 | API网关:路由、认证、限流 |
|
||||
| fund-sys | 8100 | 系统服务:用户、角色、菜单、部门、租户管理 |
|
||||
| fund-cust | 8200 | 客户管理 |
|
||||
| fund-proj | 8300 | 项目与合同管理 |
|
||||
| fund-req | 8400 | 需求工单 |
|
||||
| fund-exp | 8500 | 支出管理 |
|
||||
| fund-receipt | 8600 | 收款管理 |
|
||||
| fund-report | 8700 | 报表统计 |
|
||||
| fund-file | 8800 | 文件存储(腾讯云COS) |
|
||||
|
||||
## 代码架构
|
||||
|
||||
### 分层约定
|
||||
|
||||
每个业务服务模块遵循统一分层:
|
||||
|
||||
```
|
||||
com.fundplatform.{module}/
|
||||
├── controller/ REST API,返回 Result<T>
|
||||
├── service/ 业务逻辑
|
||||
│ └── impl/
|
||||
├── data/
|
||||
│ ├── entity/ MyBatis-Plus 实体(对应数据库表)
|
||||
│ ├── mapper/ Mapper 接口
|
||||
│ └── service/ 数据层服务(IService)
|
||||
├── dto/ 请求参数对象
|
||||
├── vo/ 响应视图对象
|
||||
├── feign/ Feign 客户端(调用其他服务)
|
||||
├── aop/ 切面(操作日志等)
|
||||
└── config/ 模块配置
|
||||
```
|
||||
|
||||
### 统一响应
|
||||
|
||||
所有接口使用 `fund-common` 中的 `Result<T>` 和 `PageResult<T>`:
|
||||
|
||||
```java
|
||||
Result.success(data)
|
||||
Result.success(data, "message")
|
||||
Result.error("message")
|
||||
```
|
||||
|
||||
### 基础实体
|
||||
|
||||
实体类继承 `BaseEntity`(包含 `id`、`tenantId`、`createdBy`、`createdTime`、`updatedBy`、`updatedTime`、`deleted`)。
|
||||
|
||||
### 认证机制
|
||||
|
||||
- Token 基于 UUID,存储在 Redis(Key: `auth:token:{token}`),有效期24小时
|
||||
- 密码使用 MD5 加密(`Md5Util`)
|
||||
- 请求头 `Authorization` 携带 Token,`X-Tenant-Id` 携带租户ID
|
||||
|
||||
## 多租户架构
|
||||
|
||||
核心设计:**一库多租户 + VIP专属实例混合模式**
|
||||
|
||||
1. **数据隔离**:所有业务表含 `tenant_id` 字段,MyBatis-Plus 租户插件自动注入 SQL 条件
|
||||
2. **上下文传递**:`TenantContextHolder` 存储当前租户,通过 HTTP Header 传递,Feign 拦截器自动转发
|
||||
3. **VIP专属实例**:Nacos 元数据 `tenant-id` 标记服务实例,`TenantAwareLoadBalancer` 将 VIP 租户路由到专属实例
|
||||
|
||||
```yaml
|
||||
# Nacos 元数据配置(区分共享/专属实例)
|
||||
spring.cloud.nacos.discovery.metadata:
|
||||
tenant-id: ${TENANT_ID:} # 空=共享实例,有值=VIP专属
|
||||
```
|
||||
|
||||
## 打包与部署
|
||||
|
||||
### 部署包结构
|
||||
|
||||
Maven Assembly 打包输出 tar.gz,解压后:
|
||||
|
||||
```
|
||||
bin/ 启动脚本(start.sh)
|
||||
conf/ 配置文件(application.yml、bootstrap.yml、logback-spring.xml、env.properties)
|
||||
lib/ 所有 JAR(依赖 + 应用本身)
|
||||
```
|
||||
|
||||
> **注意**:根 pom.xml 禁用了 Spring Boot repackage,各服务的 fat jar 由 Assembly 统一管理。
|
||||
|
||||
### 数据库初始化
|
||||
|
||||
SQL 脚本位于 `doc/sql/`,执行顺序:
|
||||
1. `01_create_user.sql` — 创建 MySQL 用户(fundsp / fundSP@123)
|
||||
2. `02_grant_user.sql` — 授权
|
||||
3. `fund_*_init.sql` — 各模块建表及初始数据
|
||||
|
||||
### 环境变量
|
||||
|
||||
关键变量在根目录 `.env` 文件中配置(MySQL、Redis、Nacos 连接信息、腾讯云COS凭证)。
|
||||
|
||||
## 服务间通信
|
||||
|
||||
使用 OpenFeign 声明式调用,`FeignChainInterceptor` 自动传递 `Authorization`、`X-Tenant-Id`、`X-Trace-Id` 等 Header。
|
||||
|
||||
```java
|
||||
@FeignClient(name = "fund-sys")
|
||||
public interface SysUserFeign {
|
||||
@GetMapping("/sys/user/{id}")
|
||||
Result<UserDto> getUserById(@PathVariable Long id);
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- **fund-gateway** 使用 WebFlux,不能引入 `spring-boot-starter-web`,须排除 fund-common 中的 web 自动配置
|
||||
- **租户忽略**:特殊场景(如登录、租户管理)需用 `TenantIgnoreHelper` 标记绕过租户过滤
|
||||
- **日志**:使用 Logback + Logstash Encoder,日志格式为 JSON,适配 ELK 收集
|
||||
168
doc/security-fixes.md
Normal file
168
doc/security-fixes.md
Normal file
@ -0,0 +1,168 @@
|
||||
# 租户隔离安全漏洞修复记录
|
||||
|
||||
**修复日期**: 2026-03-01
|
||||
**修复分支**: master
|
||||
**修复范围**: 多租户数据隔离机制
|
||||
|
||||
---
|
||||
|
||||
## 漏洞一:网关未覆盖客户端伪造的 X-Tenant-Id(高危)
|
||||
|
||||
### 漏洞描述
|
||||
|
||||
`TokenAuthFilter` 在 Token 验证通过后,使用 Spring WebFlux 的 `request.mutate().header()` 将 Token 中的 tenantId 写入请求头。然而 WebFlux 的 `header()` 方法是**追加**而非替换语义,若客户端预先在请求中设置了 `X-Tenant-Id`,下游服务从 `getFirst()` 取到的仍然是客户端伪造的值。攻击者可以通过在请求中携带任意 `X-Tenant-Id` 来访问其他租户的数据。
|
||||
|
||||
### 攻击场景
|
||||
|
||||
```
|
||||
攻击者发送请求:
|
||||
Authorization: Bearer <合法Token,属于租户A>
|
||||
X-Tenant-Id: 999 ← 伪造的租户ID
|
||||
```
|
||||
|
||||
原始代码中,下游 `ContextInterceptor` 从请求头取到的 `X-Tenant-Id` 可能是 999,导致 `TenantContextHolder` 被设置为 999,进而 MyBatis-Plus 租户插件以 tenant_id=999 过滤 SQL,越权访问其他租户数据。
|
||||
|
||||
### 修复方案
|
||||
|
||||
在 `TokenAuthFilter` 中先移除客户端传来的 `X-Tenant-Id`,再写入 Token 中已认证的值:
|
||||
|
||||
```java
|
||||
// 安全修复:先移除客户端传来的 X-Tenant-Id,再写入 Token 中已认证的值
|
||||
ServerHttpRequest mutatedRequest = request.mutate()
|
||||
.headers(headers -> headers.remove(TENANT_ID_HEADER)) // 先删除客户端值
|
||||
.header(USER_ID_HEADER, String.valueOf(tokenInfo.getUserId()))
|
||||
.header(USERNAME_HEADER, tokenInfo.getUsername())
|
||||
.header(TENANT_ID_HEADER, String.valueOf(tokenInfo.getTenantId())) // 再写入认证值
|
||||
.build();
|
||||
```
|
||||
|
||||
### 修复位置
|
||||
|
||||
`fund-gateway/src/main/java/com/fundplatform/gateway/filter/TokenAuthFilter.java` 第 86-93 行
|
||||
|
||||
---
|
||||
|
||||
## 漏洞二:IGNORE_TABLES 包含业务敏感表(高危)
|
||||
|
||||
### 漏洞描述
|
||||
|
||||
`TenantLineHandlerImpl.IGNORE_TABLES` 中包含了 `sys_user`、`sys_role`、`sys_dept`、`sys_config` 等业务表,这些表均有 `tenant_id` 字段,属于租户私有数据。将其加入忽略列表后,MyBatis-Plus 不会为这些表的查询自动注入 `tenant_id` 过滤条件,导致以下问题:
|
||||
|
||||
1. **跨租户用户数据泄漏**:查询 `sys_user` 时返回所有租户的用户数据
|
||||
2. **跨租户角色泄漏**:查询 `sys_role` 时返回所有租户的角色配置
|
||||
3. **跨租户部门泄漏**:查询 `sys_dept` 时返回所有租户的部门结构
|
||||
|
||||
### 修复方案
|
||||
|
||||
`IGNORE_TABLES` 中仅保留真正全平台共享的静态数据表:
|
||||
|
||||
```java
|
||||
// 修复后:仅保留平台级全局共享表
|
||||
private static final Set<String> IGNORE_TABLES = new HashSet<>(Arrays.asList(
|
||||
"sys_menu", // 菜单表(系统菜单结构全局共享)
|
||||
"sys_dict", // 字典表(枚举数据全局共享)
|
||||
"sys_log", // 日志表(独立存储逻辑)
|
||||
"gen_table", // 代码生成表
|
||||
"gen_table_column"
|
||||
));
|
||||
// 已移除:sys_user、sys_role、sys_dept、sys_config(均为租户私有数据)
|
||||
```
|
||||
|
||||
### 修复位置
|
||||
|
||||
`fund-common/src/main/java/com/fundplatform/common/mybatis/TenantLineHandlerImpl.java` 第 34-46 行
|
||||
|
||||
---
|
||||
|
||||
## 漏洞三:租户 ID 缺失时 Fallback 到默认值 1(中危)
|
||||
|
||||
### 漏洞描述
|
||||
|
||||
`TenantLineHandlerImpl.getTenantId()` 中,当从 `TenantContextHolder` 获取不到租户 ID 时,代码回退到默认值 `1L`:
|
||||
|
||||
```java
|
||||
// 漏洞代码
|
||||
if (tenantId == null) {
|
||||
tenantId = 1L; // 危险:误操作租户1的数据
|
||||
}
|
||||
```
|
||||
|
||||
这导致在没有认证上下文的场景(如定时任务、内部错误等)下,SQL 会意外查询租户1的数据,可能造成租户1数据泄漏或误写入。
|
||||
|
||||
### 修复方案
|
||||
|
||||
当租户上下文缺失时直接抛出异常,中断 SQL 执行:
|
||||
|
||||
```java
|
||||
// 修复后:缺失租户上下文时强制报错
|
||||
if (tenantId == null) {
|
||||
throw new IllegalStateException("[Security] 当前请求缺少租户上下文,拒绝执行SQL");
|
||||
}
|
||||
```
|
||||
|
||||
### 修复位置
|
||||
|
||||
`fund-common/src/main/java/com/fundplatform/common/mybatis/TenantLineHandlerImpl.java` 第 56-66 行
|
||||
|
||||
---
|
||||
|
||||
## 漏洞四:登录接口跨租户用户名混乱(中危)
|
||||
|
||||
### 漏洞描述
|
||||
|
||||
登录接口 `/auth/login` 在 Token 白名单中,`TenantContextHolder` 为空。由于 `sys_user` 原来在 `IGNORE_TABLES` 中(全局查询),登录时仅按用户名查询不区分租户:
|
||||
|
||||
```java
|
||||
// 漏洞代码:不同租户可能有同名用户,查到哪个是不确定的
|
||||
wrapper.eq(SysUser::getUsername, request.getUsername());
|
||||
// 未加 tenantId 条件
|
||||
SysUser user = userDataService.getOne(wrapper);
|
||||
```
|
||||
|
||||
当不同租户有同名用户时,认证结果不确定,存在以下风险:
|
||||
- 用户 A(租户1)可能以租户2用户的身份登录
|
||||
- 登录成功后 Token 中绑定了错误的 tenantId
|
||||
|
||||
### 修复方案
|
||||
|
||||
1. `LoginRequestDTO` 增加 `tenantId` 字段(必填),前端登录时显式指定租户
|
||||
2. 登录查询使用 `TenantIgnoreHelper` 跳过自动租户过滤,同时显式加入 `tenant_id` 条件:
|
||||
|
||||
```java
|
||||
// 修复后:使用 TenantIgnoreHelper + 显式 tenantId 条件
|
||||
SysUser user = TenantIgnoreHelper.ignore(() -> {
|
||||
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(SysUser::getUsername, request.getUsername());
|
||||
wrapper.eq(SysUser::getTenantId, request.getTenantId()); // 显式限定租户
|
||||
wrapper.eq(SysUser::getDeleted, 0);
|
||||
return userDataService.getOne(wrapper);
|
||||
});
|
||||
```
|
||||
|
||||
### 修复位置
|
||||
|
||||
- `fund-sys/src/main/java/com/fundplatform/sys/dto/LoginRequestDTO.java` — 新增 tenantId 字段
|
||||
- `fund-sys/src/main/java/com/fundplatform/sys/service/impl/AuthServiceImpl.java` 第 31-44 行
|
||||
|
||||
---
|
||||
|
||||
## 修复总结
|
||||
|
||||
| 编号 | 漏洞类型 | 危险级别 | 修复文件 |
|
||||
|------|---------|---------|---------|
|
||||
| 1 | 网关未覆盖客户端伪造的 X-Tenant-Id | 高危 | `fund-gateway/.../TokenAuthFilter.java` |
|
||||
| 2 | IGNORE_TABLES 包含业务敏感表 | 高危 | `fund-common/.../TenantLineHandlerImpl.java` |
|
||||
| 3 | 租户 ID 缺失时 fallback 到默认值 | 中危 | `fund-common/.../TenantLineHandlerImpl.java` |
|
||||
| 4 | 登录查询未限定租户范围 | 中危 | `fund-sys/.../AuthServiceImpl.java`, `LoginRequestDTO.java` |
|
||||
|
||||
## 修复后的安全保证
|
||||
|
||||
1. **网关层**:经过 Token 验证的请求,`X-Tenant-Id` 由 Token 中的认证值强制覆盖,客户端无法伪造
|
||||
2. **数据层**:业务表(sys_user、sys_role、sys_dept、sys_config)均受 MyBatis-Plus 租户插件保护,自动注入 `tenant_id` 过滤
|
||||
3. **兜底保护**:租户上下文为空时 SQL 拒绝执行,不会 fallback 到任何租户
|
||||
4. **登录安全**:登录时必须指定 tenantId,确保同名用户不会跨租户混乱认证
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 所有需要跨租户操作的合法场景(如超管管理所有租户),必须显式使用 `TenantIgnoreHelper.ignore()` 包装,并在代码中注明安全意图
|
||||
- 定时任务、事件监听器等异步场景在执行 DB 操作前,必须先设置 `TenantContextHolder`,执行完后在 finally 中清理
|
||||
@ -255,9 +255,9 @@ INSERT INTO sys_tenant (id, tenant_code, tenant_name, contact, phone, status, ma
|
||||
VALUES (1, 'DEFAULT', '默认租户', '管理员', '13800138000', 1, 100, '系统默认租户', NOW())
|
||||
ON DUPLICATE KEY UPDATE tenant_code=tenant_code;
|
||||
|
||||
-- 插入超级管理员用户 (租户ID=1, 密码: admin123)
|
||||
-- 插入超级管理员用户 (租户ID=1, 密码: admin123, MD5: 0192023a7bbd73250516f069df18b500)
|
||||
INSERT INTO sys_user (id, tenant_id, username, password, real_name, phone, status, created_by, created_time)
|
||||
VALUES (1, 1, 'admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt6Z5E', '超级管理员', '13800138000', 1, 1, NOW())
|
||||
VALUES (1, 1, 'admin', '0192023a7bbd73250516f069df18b500', '超级管理员', '13800138000', 1, 1, NOW())
|
||||
ON DUPLICATE KEY UPDATE username=username;
|
||||
|
||||
-- 插入超级管理员角色
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
"axios": "^1.13.5",
|
||||
"echarts": "^6.0.0",
|
||||
"element-plus": "^2.13.2",
|
||||
"js-md5": "^0.8.3",
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.25",
|
||||
"vue-router": "^5.0.2"
|
||||
|
||||
@ -51,6 +51,7 @@ import { useRouter, useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import md5 from 'js-md5'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@ -70,24 +71,6 @@ const rules: FormRules = {
|
||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// 简单的 MD5 加密函数(生产环境建议使用 crypto-js 等成熟库)
|
||||
const md5 = (str: string): string => {
|
||||
// 这里使用一个简化的 MD5 实现,实际项目建议安装 crypto-js 或 blueimp-md5
|
||||
// npm install crypto-js
|
||||
// import CryptoJS from 'crypto-js'
|
||||
// return CryptoJS.MD5(str).toString()
|
||||
|
||||
// 临时实现:为了演示,这里直接返回(需要替换为真实的 MD5 实现)
|
||||
// 推荐使用:npm install blueimp-md5 && import md5 from 'blueimp-md5'
|
||||
let hash = 0
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i)
|
||||
hash = ((hash << 5) - hash) + char
|
||||
hash = hash & hash
|
||||
}
|
||||
return Math.abs(hash).toString(16).padStart(32, '0')
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
@ -97,7 +80,6 @@ const handleLogin = async () => {
|
||||
|
||||
// 密码先进行 MD5 加密
|
||||
const encryptedPassword = md5(form.password)
|
||||
console.log('登录 - 原始密码:', form.password, 'MD5 加密后:', encryptedPassword)
|
||||
|
||||
await userStore.loginAction(form.username, encryptedPassword)
|
||||
|
||||
|
||||
@ -127,6 +127,7 @@ import { ElMessage, FormInstance, FormRules, UploadProps } from 'element-plus'
|
||||
import { UserFilled, OfficeBuilding } from '@element-plus/icons-vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { updateProfile, updatePassword } from '@/api/user'
|
||||
import md5 from 'js-md5'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const defaultAvatar = 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
|
||||
@ -236,7 +237,11 @@ const handleUpdatePassword = async () => {
|
||||
if (valid) {
|
||||
saving.value = true
|
||||
try {
|
||||
await updatePassword(passwordForm)
|
||||
await updatePassword({
|
||||
oldPassword: md5(passwordForm.oldPassword),
|
||||
newPassword: md5(passwordForm.newPassword),
|
||||
confirmPassword: md5(passwordForm.confirmPassword)
|
||||
})
|
||||
ElMessage.success('密码修改成功')
|
||||
resetPasswordForm()
|
||||
} catch (error: any) {
|
||||
|
||||
@ -31,32 +31,34 @@ public class TenantLineHandlerImpl implements TenantLineHandler {
|
||||
private static final Logger logger = LoggerFactory.getLogger(TenantLineHandlerImpl.class);
|
||||
|
||||
/**
|
||||
* 忽略租户过滤的表(系统表、字典表等公共数据)
|
||||
* 忽略租户过滤的表(仅限平台级公共数据,所有租户共享)
|
||||
*
|
||||
* <p>安全说明:仅将真正全平台共享的静态配置表列为忽略表。</p>
|
||||
* <p>sys_user/sys_role/sys_dept 均属于租户私有数据,含 tenant_id 字段,
|
||||
* 不得加入此列表,否则会导致跨租户数据泄漏。</p>
|
||||
*/
|
||||
private static final Set<String> IGNORE_TABLES = new HashSet<>(Arrays.asList(
|
||||
"sys_user", // 用户表(可能跨租户)
|
||||
"sys_role", // 角色表(可能跨租户)
|
||||
"sys_menu", // 菜单表(所有租户共享)
|
||||
"sys_dict", // 字典表(所有租户共享)
|
||||
"sys_config", // 配置表(所有租户共享)
|
||||
"sys_dept", // 部门表(可能跨租户)
|
||||
"sys_log", // 日志表(独立存储)
|
||||
"gen_table", // 代码生成表
|
||||
"sys_menu", // 菜单表(所有租户共享的系统菜单结构)
|
||||
"sys_dict", // 字典表(所有租户共享的枚举数据)
|
||||
"sys_log", // 日志表(独立存储,由专属逻辑管理)
|
||||
"gen_table", // 代码生成表(开发工具,非业务数据)
|
||||
"gen_table_column" // 代码生成字段表
|
||||
));
|
||||
|
||||
/**
|
||||
* 获取租户 ID 值
|
||||
*
|
||||
* <p>从 TenantContextHolder 获取当前线程的租户 ID</p>
|
||||
* <p>从 TenantContextHolder 获取当前线程的租户 ID。</p>
|
||||
* <p>安全修复:不再使用 fallback 默认值 1L,若租户上下文为空则直接报错,
|
||||
* 防止在缺少认证上下文的情况下误操作租户1的数据。</p>
|
||||
*/
|
||||
@Override
|
||||
public Expression getTenantId() {
|
||||
Long tenantId = getCurrentTenantId();
|
||||
|
||||
if (tenantId == null) {
|
||||
logger.debug("[MyBatis Tenant] 未获取到租户 ID,使用默认值 1");
|
||||
tenantId = 1L;
|
||||
// 安全修复:租户上下文缺失时必须中断,不能 fallback 到任意租户
|
||||
throw new IllegalStateException("[Security] 当前请求缺少租户上下文,拒绝执行SQL,请确保请求经过认证过滤器");
|
||||
}
|
||||
|
||||
logger.debug("[MyBatis Tenant] 当前租户 ID: {}", tenantId);
|
||||
|
||||
@ -0,0 +1,76 @@
|
||||
package com.fundplatform.common.context;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* TenantContextHolder 租户上下文持有者单元测试
|
||||
*/
|
||||
class TenantContextHolderTest {
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
TenantContextHolder.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("设置租户ID后可以正确获取")
|
||||
void setAndGetTenantId() {
|
||||
TenantContextHolder.setTenantId(100L);
|
||||
|
||||
assertEquals(100L, TenantContextHolder.getTenantId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("未设置时 getTenantId 返回 null")
|
||||
void getTenantId_null_whenNotSet() {
|
||||
assertNull(TenantContextHolder.getTenantId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("clear 后 getTenantId 返回 null")
|
||||
void clear_removesValue() {
|
||||
TenantContextHolder.setTenantId(200L);
|
||||
TenantContextHolder.clear();
|
||||
|
||||
assertNull(TenantContextHolder.getTenantId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("多次设置取最后一次的值")
|
||||
void setMultipleTimes_lastValueWins() {
|
||||
TenantContextHolder.setTenantId(10L);
|
||||
TenantContextHolder.setTenantId(20L);
|
||||
TenantContextHolder.setTenantId(30L);
|
||||
|
||||
assertEquals(30L, TenantContextHolder.getTenantId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("设置 null 值后 getTenantId 返回 null")
|
||||
void setNullTenantId() {
|
||||
TenantContextHolder.setTenantId(100L);
|
||||
TenantContextHolder.setTenantId(null);
|
||||
|
||||
assertNull(TenantContextHolder.getTenantId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("线程隔离 - 子线程设置的值不影响主线程")
|
||||
void threadIsolation() throws InterruptedException {
|
||||
TenantContextHolder.setTenantId(1L);
|
||||
|
||||
Thread subThread = new Thread(() -> {
|
||||
TenantContextHolder.setTenantId(999L);
|
||||
assertEquals(999L, TenantContextHolder.getTenantId());
|
||||
});
|
||||
subThread.start();
|
||||
subThread.join();
|
||||
|
||||
// 主线程的值不受子线程影响
|
||||
assertEquals(1L, TenantContextHolder.getTenantId());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,107 @@
|
||||
package com.fundplatform.common.context;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* UserContextHolder 用户上下文持有者单元测试
|
||||
*/
|
||||
class UserContextHolderTest {
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
UserContextHolder.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("设置用户ID后可以正确获取")
|
||||
void setAndGetUserId() {
|
||||
UserContextHolder.setUserId(1L);
|
||||
|
||||
assertEquals(1L, UserContextHolder.getUserId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("设置用户名后可以正确获取")
|
||||
void setAndGetUserName() {
|
||||
UserContextHolder.setUserName("admin");
|
||||
|
||||
assertEquals("admin", UserContextHolder.getUserName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("同时设置 userId 和 userName")
|
||||
void setBoth_userId_and_userName() {
|
||||
UserContextHolder.setUserId(42L);
|
||||
UserContextHolder.setUserName("testuser");
|
||||
|
||||
assertEquals(42L, UserContextHolder.getUserId());
|
||||
assertEquals("testuser", UserContextHolder.getUserName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("未设置时 getUserId 返回 null")
|
||||
void getUserId_null_whenNotSet() {
|
||||
assertNull(UserContextHolder.getUserId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("未设置时 getUserName 返回 null")
|
||||
void getUserName_null_whenNotSet() {
|
||||
assertNull(UserContextHolder.getUserName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("clear 后 userId 和 userName 均为 null")
|
||||
void clear_removesAllValues() {
|
||||
UserContextHolder.setUserId(1L);
|
||||
UserContextHolder.setUserName("admin");
|
||||
UserContextHolder.clear();
|
||||
|
||||
assertNull(UserContextHolder.getUserId());
|
||||
assertNull(UserContextHolder.getUserName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("多次设置 userId 取最后一次的值")
|
||||
void setUserId_multiple_lastWins() {
|
||||
UserContextHolder.setUserId(1L);
|
||||
UserContextHolder.setUserId(2L);
|
||||
UserContextHolder.setUserId(3L);
|
||||
|
||||
assertEquals(3L, UserContextHolder.getUserId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("线程隔离 - 子线程设置的 userId 不影响主线程")
|
||||
void threadIsolation_userId() throws InterruptedException {
|
||||
UserContextHolder.setUserId(1L);
|
||||
|
||||
Thread subThread = new Thread(() -> {
|
||||
UserContextHolder.setUserId(999L);
|
||||
assertEquals(999L, UserContextHolder.getUserId());
|
||||
});
|
||||
subThread.start();
|
||||
subThread.join();
|
||||
|
||||
assertEquals(1L, UserContextHolder.getUserId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("线程隔离 - 子线程设置的 userName 不影响主线程")
|
||||
void threadIsolation_userName() throws InterruptedException {
|
||||
UserContextHolder.setUserName("mainthread");
|
||||
|
||||
Thread subThread = new Thread(() -> {
|
||||
UserContextHolder.setUserName("subthread");
|
||||
assertEquals("subthread", UserContextHolder.getUserName());
|
||||
});
|
||||
subThread.start();
|
||||
subThread.join();
|
||||
|
||||
assertEquals("mainthread", UserContextHolder.getUserName());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
package com.fundplatform.common.core;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* PageResult 分页返回结构单元测试
|
||||
*/
|
||||
class PageResultTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("无参构造函数 - records 默认为空列表")
|
||||
void defaultConstructor_emptyRecords() {
|
||||
PageResult<String> pageResult = new PageResult<>();
|
||||
|
||||
assertNotNull(pageResult.getRecords());
|
||||
assertTrue(pageResult.getRecords().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("全参构造函数 - 正确设置所有字段")
|
||||
void fullConstructor() {
|
||||
List<String> records = Arrays.asList("a", "b", "c");
|
||||
PageResult<String> pageResult = new PageResult<>(1, 10, 3, records);
|
||||
|
||||
assertEquals(1, pageResult.getPageNum());
|
||||
assertEquals(10, pageResult.getPageSize());
|
||||
assertEquals(3, pageResult.getTotal());
|
||||
assertEquals(3, pageResult.getRecords().size());
|
||||
assertEquals("a", pageResult.getRecords().get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("全参构造函数 - records 为 null 时自动转为空列表")
|
||||
void fullConstructor_nullRecords() {
|
||||
PageResult<String> pageResult = new PageResult<>(1, 10, 0, null);
|
||||
|
||||
assertNotNull(pageResult.getRecords());
|
||||
assertTrue(pageResult.getRecords().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("setter/getter - 可正常设置和获取所有字段")
|
||||
void settersAndGetters() {
|
||||
PageResult<Integer> pageResult = new PageResult<>();
|
||||
pageResult.setPageNum(2);
|
||||
pageResult.setPageSize(20);
|
||||
pageResult.setTotal(100);
|
||||
pageResult.setRecords(Arrays.asList(1, 2, 3));
|
||||
|
||||
assertEquals(2, pageResult.getPageNum());
|
||||
assertEquals(20, pageResult.getPageSize());
|
||||
assertEquals(100, pageResult.getTotal());
|
||||
assertEquals(3, pageResult.getRecords().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("total 为 0 时表示无数据")
|
||||
void total_zero() {
|
||||
PageResult<Object> pageResult = new PageResult<>(1, 10, 0, Collections.emptyList());
|
||||
|
||||
assertEquals(0, pageResult.getTotal());
|
||||
assertTrue(pageResult.getRecords().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("记录数量与 total 可以不一致(当前页可为最后一页不满页)")
|
||||
void records_lessThanPageSize() {
|
||||
List<String> records = List.of("x", "y");
|
||||
PageResult<String> pageResult = new PageResult<>(5, 10, 42, records);
|
||||
|
||||
assertEquals(42, pageResult.getTotal());
|
||||
assertEquals(2, pageResult.getRecords().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("setRecords - 可覆盖原有记录")
|
||||
void setRecords_override() {
|
||||
PageResult<String> pageResult = new PageResult<>(1, 10, 2, Arrays.asList("old1", "old2"));
|
||||
pageResult.setRecords(List.of("new1"));
|
||||
|
||||
assertEquals(1, pageResult.getRecords().size());
|
||||
assertEquals("new1", pageResult.getRecords().get(0));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,125 @@
|
||||
package com.fundplatform.common.core;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Result 统一响应封装单元测试
|
||||
*/
|
||||
class ResultTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("success() - 无参成功返回:code=200, message=success, data=null")
|
||||
void success_noArgs() {
|
||||
Result<Void> result = Result.success();
|
||||
|
||||
assertEquals(200, result.getCode());
|
||||
assertEquals("success", result.getMessage());
|
||||
assertNull(result.getData());
|
||||
assertTrue(result.isSuccess());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("success(data) - 携带非String数据的成功返回")
|
||||
void success_withData() {
|
||||
Integer data = 42;
|
||||
Result<Integer> result = Result.success(data);
|
||||
|
||||
assertEquals(200, result.getCode());
|
||||
assertEquals("success", result.getMessage());
|
||||
assertEquals(42, result.getData());
|
||||
assertTrue(result.isSuccess());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("success(message) - 仅消息的成功返回,data 为 null")
|
||||
void success_withMessage() {
|
||||
Result<Void> result = Result.success("操作成功");
|
||||
|
||||
assertEquals(200, result.getCode());
|
||||
assertEquals("操作成功", result.getMessage());
|
||||
assertNull(result.getData());
|
||||
assertTrue(result.isSuccess());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("success(data, message) - 携带数据和自定义消息的成功返回")
|
||||
void success_withDataAndMessage() {
|
||||
Integer data = 42;
|
||||
Result<Integer> result = Result.success(data, "查询成功");
|
||||
|
||||
assertEquals(200, result.getCode());
|
||||
assertEquals("查询成功", result.getMessage());
|
||||
assertEquals(42, result.getData());
|
||||
assertTrue(result.isSuccess());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("error(message) - 使用默认错误码 500")
|
||||
void error_withMessage() {
|
||||
Result<Void> result = Result.error("系统错误");
|
||||
|
||||
assertEquals(500, result.getCode());
|
||||
assertEquals("系统错误", result.getMessage());
|
||||
assertNull(result.getData());
|
||||
assertFalse(result.isSuccess());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("error(code, message) - 自定义错误码")
|
||||
void error_withCodeAndMessage() {
|
||||
Result<Void> result = Result.error(404, "资源未找到");
|
||||
|
||||
assertEquals(404, result.getCode());
|
||||
assertEquals("资源未找到", result.getMessage());
|
||||
assertNull(result.getData());
|
||||
assertFalse(result.isSuccess());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("isSuccess() - code=200 时返回 true")
|
||||
void isSuccess_true() {
|
||||
Result<Object> result = new Result<>(200, "ok", null);
|
||||
assertTrue(result.isSuccess());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("isSuccess() - code!=200 时返回 false")
|
||||
void isSuccess_false() {
|
||||
Result<Object> result = new Result<>(500, "error", null);
|
||||
assertFalse(result.isSuccess());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("无参构造函数 + setter - 可正常设置字段")
|
||||
void defaultConstructor_withSetters() {
|
||||
Result<String> result = new Result<>();
|
||||
result.setCode(200);
|
||||
result.setMessage("ok");
|
||||
result.setData("test");
|
||||
|
||||
assertEquals(200, result.getCode());
|
||||
assertEquals("ok", result.getMessage());
|
||||
assertEquals("test", result.getData());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("success(data, message) 中 data 为 null 也能正常返回")
|
||||
void success_nullData() {
|
||||
Result<String> result = Result.success(null, "查询成功");
|
||||
|
||||
assertEquals(200, result.getCode());
|
||||
assertNull(result.getData());
|
||||
assertEquals("查询成功", result.getMessage());
|
||||
assertTrue(result.isSuccess());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("常量值:SUCCESS=200, ERROR=500")
|
||||
void constants() {
|
||||
assertEquals(200, Result.SUCCESS);
|
||||
assertEquals(500, Result.ERROR);
|
||||
}
|
||||
}
|
||||
@ -61,6 +61,13 @@
|
||||
<version>7.4</version>
|
||||
</dependency>
|
||||
|
||||
<!-- SpringDoc OpenAPI (Swagger UI) -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Nacos服务注册发现 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
|
||||
@ -5,12 +5,16 @@ import com.fundplatform.common.core.Result;
|
||||
import com.fundplatform.cust.dto.ContactDTO;
|
||||
import com.fundplatform.cust.service.ContactService;
|
||||
import com.fundplatform.cust.vo.ContactVO;
|
||||
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 org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 联系人管理Controller
|
||||
*/
|
||||
@Tag(name = "联系人管理", description = "客户联系人的增删改查及主要联系人设置")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/customer/contact")
|
||||
public class ContactController {
|
||||
@ -24,6 +28,7 @@ public class ContactController {
|
||||
/**
|
||||
* 创建联系人
|
||||
*/
|
||||
@Operation(summary = "创建联系人")
|
||||
@PostMapping
|
||||
public Result<Long> create(@Valid @RequestBody ContactDTO dto) {
|
||||
Long id = contactService.createContact(dto);
|
||||
@ -33,8 +38,9 @@ public class ContactController {
|
||||
/**
|
||||
* 更新联系人
|
||||
*/
|
||||
@Operation(summary = "更新联系人信息")
|
||||
@PutMapping("/{id}")
|
||||
public Result<Boolean> update(@PathVariable Long id, @Valid @RequestBody ContactDTO dto) {
|
||||
public Result<Boolean> update(@Parameter(description = "联系人ID") @PathVariable Long id, @Valid @RequestBody ContactDTO dto) {
|
||||
boolean result = contactService.updateContact(id, dto);
|
||||
return Result.success(result);
|
||||
}
|
||||
@ -42,8 +48,9 @@ public class ContactController {
|
||||
/**
|
||||
* 根据ID查询联系人
|
||||
*/
|
||||
@Operation(summary = "根据ID查询联系人")
|
||||
@GetMapping("/{id}")
|
||||
public Result<ContactVO> getById(@PathVariable Long id) {
|
||||
public Result<ContactVO> getById(@Parameter(description = "联系人ID") @PathVariable Long id) {
|
||||
ContactVO vo = contactService.getContactById(id);
|
||||
return Result.success(vo);
|
||||
}
|
||||
@ -51,11 +58,12 @@ public class ContactController {
|
||||
/**
|
||||
* 分页查询联系人
|
||||
*/
|
||||
@Operation(summary = "分页查询联系人")
|
||||
@GetMapping("/page")
|
||||
public Result<Page<ContactVO>> page(
|
||||
@RequestParam(defaultValue = "1") int pageNum,
|
||||
@RequestParam(defaultValue = "10") int pageSize,
|
||||
@RequestParam(required = false) Long customerId) {
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") int pageNum,
|
||||
@Parameter(description = "每页条数") @RequestParam(defaultValue = "10") int pageSize,
|
||||
@Parameter(description = "客户ID") @RequestParam(required = false) Long customerId) {
|
||||
Page<ContactVO> page = contactService.pageContacts(pageNum, pageSize, customerId);
|
||||
return Result.success(page);
|
||||
}
|
||||
@ -63,8 +71,9 @@ public class ContactController {
|
||||
/**
|
||||
* 删除联系人
|
||||
*/
|
||||
@Operation(summary = "删除联系人")
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Boolean> delete(@PathVariable Long id) {
|
||||
public Result<Boolean> delete(@Parameter(description = "联系人ID") @PathVariable Long id) {
|
||||
boolean result = contactService.deleteContact(id);
|
||||
return Result.success(result);
|
||||
}
|
||||
@ -72,10 +81,11 @@ public class ContactController {
|
||||
/**
|
||||
* 设置主要联系人
|
||||
*/
|
||||
@Operation(summary = "设置主要联系人", description = "将指定联系人设为客户的主要联系人")
|
||||
@PutMapping("/{customerId}/contact/{contactId}/primary")
|
||||
public Result<Boolean> setPrimary(
|
||||
@PathVariable Long customerId,
|
||||
@PathVariable Long contactId) {
|
||||
@Parameter(description = "客户ID") @PathVariable Long customerId,
|
||||
@Parameter(description = "联系人ID") @PathVariable Long contactId) {
|
||||
boolean result = contactService.setPrimaryContact(customerId, contactId);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@ -7,12 +7,16 @@ import com.fundplatform.cust.dto.CustomerCreateDTO;
|
||||
import com.fundplatform.cust.dto.CustomerUpdateDTO;
|
||||
import com.fundplatform.cust.service.CustomerService;
|
||||
import com.fundplatform.cust.vo.CustomerVO;
|
||||
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 org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 客户Controller
|
||||
*/
|
||||
@Tag(name = "客户管理", description = "客户的增删改查")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/customer")
|
||||
public class CustomerController {
|
||||
@ -26,6 +30,7 @@ public class CustomerController {
|
||||
/**
|
||||
* 创建客户
|
||||
*/
|
||||
@Operation(summary = "创建客户")
|
||||
@PostMapping
|
||||
public Result<Long> createCustomer(@Valid @RequestBody CustomerCreateDTO dto) {
|
||||
Long id = customerService.createCustomer(dto);
|
||||
@ -35,8 +40,9 @@ public class CustomerController {
|
||||
/**
|
||||
* 更新客户
|
||||
*/
|
||||
@Operation(summary = "更新客户信息")
|
||||
@PutMapping("/{id}")
|
||||
public Result<Void> updateCustomer(@PathVariable Long id, @Valid @RequestBody CustomerUpdateDTO dto) {
|
||||
public Result<Void> updateCustomer(@Parameter(description = "客户ID") @PathVariable Long id, @Valid @RequestBody CustomerUpdateDTO dto) {
|
||||
customerService.updateCustomer(id, dto);
|
||||
return Result.success();
|
||||
}
|
||||
@ -44,8 +50,9 @@ public class CustomerController {
|
||||
/**
|
||||
* 查询客户详情
|
||||
*/
|
||||
@Operation(summary = "查询客户详情")
|
||||
@GetMapping("/{id}")
|
||||
public Result<CustomerVO> getCustomer(@PathVariable Long id) {
|
||||
public Result<CustomerVO> getCustomer(@Parameter(description = "客户ID") @PathVariable Long id) {
|
||||
CustomerVO vo = customerService.getCustomerById(id);
|
||||
return Result.success(vo);
|
||||
}
|
||||
@ -53,11 +60,12 @@ public class CustomerController {
|
||||
/**
|
||||
* 分页查询客户
|
||||
*/
|
||||
@Operation(summary = "分页查询客户")
|
||||
@GetMapping("/page")
|
||||
public Result<PageResult<CustomerVO>> pageCustomers(
|
||||
@RequestParam(defaultValue = "1") int pageNum,
|
||||
@RequestParam(defaultValue = "10") int pageSize,
|
||||
@RequestParam(required = false) String keyword) {
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") int pageNum,
|
||||
@Parameter(description = "每页条数") @RequestParam(defaultValue = "10") int pageSize,
|
||||
@Parameter(description = "关键词(客户名/编码/联系人模糊查询)") @RequestParam(required = false) String keyword) {
|
||||
|
||||
Page<CustomerVO> page = customerService.pageCustomers(pageNum, pageSize, keyword);
|
||||
PageResult<CustomerVO> pageResult = new PageResult<>(
|
||||
@ -72,8 +80,9 @@ public class CustomerController {
|
||||
/**
|
||||
* 删除客户
|
||||
*/
|
||||
@Operation(summary = "删除客户(逻辑删除)")
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> deleteCustomer(@PathVariable Long id) {
|
||||
public Result<Void> deleteCustomer(@Parameter(description = "客户ID") @PathVariable Long id) {
|
||||
customerService.deleteCustomer(id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@ -66,7 +66,9 @@ public class TenantGatewayFilter implements GlobalFilter, Ordered {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
|
||||
// 检查X-Tenant-Id请求头
|
||||
// 安全修复:从请求头中取 X-Tenant-Id(TokenAuthFilter 已用 Token 中的值覆盖客户端传入值)
|
||||
// 因此此处取到的必为 Token 认证后的租户ID,无需再与客户端值单独比对。
|
||||
// 但仍需校验格式合法性,防止异常数据流入下游服务。
|
||||
String tenantId = request.getHeaders().getFirst(HEADER_TENANT_ID);
|
||||
if (tenantId == null || tenantId.trim().isEmpty()) {
|
||||
logger.warn("[TenantGateway] 缺少X-Tenant-Id请求头,路径: {}", path);
|
||||
|
||||
@ -83,8 +83,10 @@ public class TokenAuthFilter implements GlobalFilter, Ordered {
|
||||
return unauthorized(exchange, "Token无效或已过期");
|
||||
}
|
||||
|
||||
// 将用户信息写入请求头
|
||||
// 安全修复:先移除客户端传来的 X-Tenant-Id,再写入 Token 中已认证的值,
|
||||
// 防止攻击者通过伪造 X-Tenant-Id 请求头绕过租户隔离。
|
||||
ServerHttpRequest mutatedRequest = request.mutate()
|
||||
.headers(headers -> headers.remove(TENANT_ID_HEADER))
|
||||
.header(USER_ID_HEADER, String.valueOf(tokenInfo.getUserId()))
|
||||
.header(USERNAME_HEADER, tokenInfo.getUsername())
|
||||
.header(TENANT_ID_HEADER, String.valueOf(tokenInfo.getTenantId()))
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
"js-md5": "^0.8.3",
|
||||
"pinia": "^2.1.7",
|
||||
"vant": "^4.9.22",
|
||||
"vue": "^3.4.0",
|
||||
|
||||
@ -52,6 +52,7 @@ import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showToast, showSuccessToast } from 'vant'
|
||||
import { login } from '@/api'
|
||||
import md5 from 'js-md5'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
@ -74,7 +75,10 @@ const handleLogin = async () => {
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await login(form)
|
||||
const res: any = await login({
|
||||
username: form.username,
|
||||
password: md5(form.password)
|
||||
})
|
||||
const data = res.data
|
||||
localStorage.setItem('token', data.token)
|
||||
localStorage.setItem('userInfo', JSON.stringify({
|
||||
|
||||
@ -60,6 +60,7 @@ import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showFailToast, showSuccessToast, showConfirmDialog } from 'vant'
|
||||
import { updatePassword } from '@/api'
|
||||
import md5 from 'js-md5'
|
||||
|
||||
const router = useRouter()
|
||||
const submitting = ref(false)
|
||||
@ -94,9 +95,9 @@ const onSubmit = async () => {
|
||||
submitting.value = true
|
||||
|
||||
await updatePassword({
|
||||
oldPassword: form.value.oldPassword,
|
||||
newPassword: form.value.newPassword,
|
||||
confirmPassword: form.value.confirmPassword
|
||||
oldPassword: md5(form.value.oldPassword),
|
||||
newPassword: md5(form.value.newPassword),
|
||||
confirmPassword: md5(form.value.confirmPassword)
|
||||
})
|
||||
|
||||
showSuccessToast('密码修改成功')
|
||||
|
||||
@ -112,6 +112,20 @@
|
||||
<artifactId>logstash-logback-encoder</artifactId>
|
||||
<version>7.4</version>
|
||||
</dependency>
|
||||
|
||||
<!-- SpringDoc OpenAPI (Swagger UI) -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Test -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@ -5,6 +5,9 @@ import com.fundplatform.sys.dto.LoginRequestDTO;
|
||||
import com.fundplatform.sys.service.AuthService;
|
||||
import com.fundplatform.sys.vo.LoginVO;
|
||||
import com.fundplatform.sys.vo.UserVO;
|
||||
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 org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@ -16,6 +19,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
/**
|
||||
* 认证Controller
|
||||
*/
|
||||
@Tag(name = "认证管理", description = "登录、登出、Token刷新等认证接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth")
|
||||
public class AuthController {
|
||||
@ -29,6 +33,7 @@ public class AuthController {
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
@Operation(summary = "用户登录", description = "使用用户名和MD5加密密码登录,返回Token及用户信息")
|
||||
@PostMapping("/login")
|
||||
public Result<LoginVO> login(@Valid @RequestBody LoginRequestDTO request) {
|
||||
LoginVO vo = authService.login(request);
|
||||
@ -38,8 +43,9 @@ public class AuthController {
|
||||
/**
|
||||
* 用户登出
|
||||
*/
|
||||
@Operation(summary = "用户登出", description = "销毁当前用户Token,使会话失效")
|
||||
@PostMapping("/logout")
|
||||
public Result<Void> logout(@RequestHeader(value = "X-User-Id", required = false) Long userId) {
|
||||
public Result<Void> logout(@Parameter(description = "当前登录用户ID") @RequestHeader(value = "X-User-Id", required = false) Long userId) {
|
||||
authService.logout(userId);
|
||||
return Result.success();
|
||||
}
|
||||
@ -47,8 +53,9 @@ public class AuthController {
|
||||
/**
|
||||
* 刷新Token
|
||||
*/
|
||||
@Operation(summary = "刷新Token", description = "延长当前Token的有效期")
|
||||
@PostMapping("/refresh")
|
||||
public Result<LoginVO> refreshToken(@RequestHeader("X-User-Id") Long userId) {
|
||||
public Result<LoginVO> refreshToken(@Parameter(description = "当前登录用户ID") @RequestHeader("X-User-Id") Long userId) {
|
||||
LoginVO vo = authService.refreshToken(userId);
|
||||
return Result.success(vo);
|
||||
}
|
||||
@ -56,8 +63,9 @@ public class AuthController {
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
@Operation(summary = "获取当前用户信息", description = "根据Token中的用户ID获取用户详情")
|
||||
@GetMapping("/info")
|
||||
public Result<UserVO> getUserInfo(@RequestHeader("X-User-Id") Long userId) {
|
||||
public Result<UserVO> getUserInfo(@Parameter(description = "当前登录用户ID") @RequestHeader("X-User-Id") Long userId) {
|
||||
UserVO vo = authService.getUserInfo(userId);
|
||||
return Result.success(vo);
|
||||
}
|
||||
|
||||
@ -4,6 +4,9 @@ import com.fundplatform.common.core.Result;
|
||||
import com.fundplatform.sys.dto.DeptDTO;
|
||||
import com.fundplatform.sys.service.DeptService;
|
||||
import com.fundplatform.sys.vo.DeptVO;
|
||||
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 org.springframework.web.bind.annotation.*;
|
||||
|
||||
@ -12,6 +15,7 @@ import java.util.List;
|
||||
/**
|
||||
* 部门管理Controller
|
||||
*/
|
||||
@Tag(name = "部门管理", description = "部门的增删改查及树形结构查询")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/sys/dept")
|
||||
public class DeptController {
|
||||
@ -22,44 +26,51 @@ public class DeptController {
|
||||
this.deptService = deptService;
|
||||
}
|
||||
|
||||
@Operation(summary = "创建部门")
|
||||
@PostMapping
|
||||
public Result<Long> create(@Valid @RequestBody DeptDTO dto) {
|
||||
Long deptId = deptService.createDept(dto);
|
||||
return Result.success(deptId);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新部门信息")
|
||||
@PutMapping
|
||||
public Result<Boolean> update(@Valid @RequestBody DeptDTO dto) {
|
||||
boolean result = deptService.updateDept(dto);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "根据ID查询部门")
|
||||
@GetMapping("/{id}")
|
||||
public Result<DeptVO> getById(@PathVariable Long id) {
|
||||
public Result<DeptVO> getById(@Parameter(description = "部门ID") @PathVariable Long id) {
|
||||
DeptVO vo = deptService.getDeptById(id);
|
||||
return Result.success(vo);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取部门树形结构")
|
||||
@GetMapping("/tree")
|
||||
public Result<List<DeptVO>> getTree() {
|
||||
List<DeptVO> tree = deptService.getDeptTree();
|
||||
return Result.success(tree);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询所有部门列表(扁平)")
|
||||
@GetMapping("/list")
|
||||
public Result<List<DeptVO>> listAll() {
|
||||
List<DeptVO> list = deptService.listAllDepts();
|
||||
return Result.success(list);
|
||||
}
|
||||
|
||||
@Operation(summary = "删除部门")
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Boolean> delete(@PathVariable Long id) {
|
||||
public Result<Boolean> delete(@Parameter(description = "部门ID") @PathVariable Long id) {
|
||||
boolean result = deptService.deleteDept(id);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新部门状态")
|
||||
@PutMapping("/{id}/status")
|
||||
public Result<Boolean> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
|
||||
public Result<Boolean> updateStatus(@Parameter(description = "部门ID") @PathVariable Long id, @Parameter(description = "状态:0禁用 1启用") @RequestParam Integer status) {
|
||||
boolean result = deptService.updateStatus(id, status);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@ -4,6 +4,9 @@ import com.fundplatform.common.core.Result;
|
||||
import com.fundplatform.sys.dto.MenuDTO;
|
||||
import com.fundplatform.sys.service.MenuService;
|
||||
import com.fundplatform.sys.vo.MenuVO;
|
||||
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 org.springframework.web.bind.annotation.*;
|
||||
|
||||
@ -12,6 +15,7 @@ import java.util.List;
|
||||
/**
|
||||
* 菜单管理Controller
|
||||
*/
|
||||
@Tag(name = "菜单管理", description = "菜单和权限的增删改查")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/sys/menu")
|
||||
public class MenuController {
|
||||
@ -22,38 +26,44 @@ public class MenuController {
|
||||
this.menuService = menuService;
|
||||
}
|
||||
|
||||
@Operation(summary = "创建菜单/权限")
|
||||
@PostMapping
|
||||
public Result<Long> create(@Valid @RequestBody MenuDTO dto) {
|
||||
Long menuId = menuService.createMenu(dto);
|
||||
return Result.success(menuId);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新菜单/权限信息")
|
||||
@PutMapping
|
||||
public Result<Boolean> update(@Valid @RequestBody MenuDTO dto) {
|
||||
boolean result = menuService.updateMenu(dto);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "根据ID查询菜单")
|
||||
@GetMapping("/{id}")
|
||||
public Result<MenuVO> getById(@PathVariable Long id) {
|
||||
public Result<MenuVO> getById(@Parameter(description = "菜单ID") @PathVariable Long id) {
|
||||
MenuVO vo = menuService.getMenuById(id);
|
||||
return Result.success(vo);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取菜单树(全量)")
|
||||
@GetMapping("/tree")
|
||||
public Result<List<MenuVO>> getTree() {
|
||||
List<MenuVO> tree = menuService.getMenuTree();
|
||||
return Result.success(tree);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取用户菜单树", description = "根据用户ID获取其有权访问的菜单树")
|
||||
@GetMapping("/user/{userId}")
|
||||
public Result<List<MenuVO>> getUserTree(@PathVariable Long userId) {
|
||||
public Result<List<MenuVO>> getUserTree(@Parameter(description = "用户ID") @PathVariable Long userId) {
|
||||
List<MenuVO> tree = menuService.getUserMenuTree(userId);
|
||||
return Result.success(tree);
|
||||
}
|
||||
|
||||
@Operation(summary = "删除菜单/权限")
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Boolean> delete(@PathVariable Long id) {
|
||||
public Result<Boolean> delete(@Parameter(description = "菜单ID") @PathVariable Long id) {
|
||||
boolean result = menuService.deleteMenu(id);
|
||||
return Result.success(result);
|
||||
}
|
||||
@ -61,8 +71,9 @@ public class MenuController {
|
||||
/**
|
||||
* 获取用户权限标识列表
|
||||
*/
|
||||
@Operation(summary = "获取用户权限标识列表", description = "返回用户拥有的所有权限标识字符串")
|
||||
@GetMapping("/permissions/{userId}")
|
||||
public Result<List<String>> getUserPermissions(@PathVariable Long userId) {
|
||||
public Result<List<String>> getUserPermissions(@Parameter(description = "用户ID") @PathVariable Long userId) {
|
||||
List<String> permissions = menuService.getUserPermissions(userId);
|
||||
return Result.success(permissions);
|
||||
}
|
||||
|
||||
@ -4,11 +4,15 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.fundplatform.common.core.Result;
|
||||
import com.fundplatform.sys.data.entity.OperationLog;
|
||||
import com.fundplatform.sys.service.OperationLogService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 操作日志Controller
|
||||
*/
|
||||
@Tag(name = "操作日志", description = "系统操作日志查询与管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/log")
|
||||
public class OperationLogController {
|
||||
@ -22,30 +26,33 @@ public class OperationLogController {
|
||||
/**
|
||||
* 分页查询操作日志
|
||||
*/
|
||||
@Operation(summary = "分页查询操作日志")
|
||||
@GetMapping("/page")
|
||||
public Result<Page<OperationLog>> page(
|
||||
@RequestParam(defaultValue = "1") int pageNum,
|
||||
@RequestParam(defaultValue = "10") int pageSize,
|
||||
@RequestParam(required = false) Long userId,
|
||||
@RequestParam(required = false) String operation,
|
||||
@RequestParam(required = false) String startTime,
|
||||
@RequestParam(required = false) String endTime) {
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") int pageNum,
|
||||
@Parameter(description = "每页条数") @RequestParam(defaultValue = "10") int pageSize,
|
||||
@Parameter(description = "用户ID") @RequestParam(required = false) Long userId,
|
||||
@Parameter(description = "操作类型") @RequestParam(required = false) String operation,
|
||||
@Parameter(description = "开始时间(yyyy-MM-dd HH:mm:ss)") @RequestParam(required = false) String startTime,
|
||||
@Parameter(description = "结束时间(yyyy-MM-dd HH:mm:ss)") @RequestParam(required = false) String endTime) {
|
||||
return Result.success(operationLogService.pageLogs(pageNum, pageSize, userId, operation, startTime, endTime));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志详情
|
||||
*/
|
||||
@Operation(summary = "获取操作日志详情")
|
||||
@GetMapping("/{id}")
|
||||
public Result<OperationLog> getById(@PathVariable Long id) {
|
||||
public Result<OperationLog> getById(@Parameter(description = "日志ID") @PathVariable Long id) {
|
||||
return Result.success(operationLogService.getById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理历史日志
|
||||
*/
|
||||
@Operation(summary = "清理历史操作日志", description = "删除N天前的操作日志,默认90天")
|
||||
@DeleteMapping("/clean")
|
||||
public Result<Integer> cleanLogs(@RequestParam(defaultValue = "90") int days) {
|
||||
public Result<Integer> cleanLogs(@Parameter(description = "保留天数,默认90天") @RequestParam(defaultValue = "90") int days) {
|
||||
return Result.success(operationLogService.cleanLogs(days));
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,12 +5,16 @@ import com.fundplatform.sys.dto.PasswordDTO;
|
||||
import com.fundplatform.sys.dto.ProfileDTO;
|
||||
import com.fundplatform.sys.service.UserService;
|
||||
import com.fundplatform.sys.vo.UserVO;
|
||||
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 org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 个人中心Controller
|
||||
*/
|
||||
@Tag(name = "个人中心", description = "当前登录用户的个人信息查询与修改")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/sys/profile")
|
||||
public class ProfileController {
|
||||
@ -24,8 +28,9 @@ public class ProfileController {
|
||||
/**
|
||||
* 获取个人信息
|
||||
*/
|
||||
@Operation(summary = "获取个人信息")
|
||||
@GetMapping
|
||||
public Result<UserVO> getProfile(@RequestHeader("X-User-Id") Long userId) {
|
||||
public Result<UserVO> getProfile(@Parameter(description = "当前登录用户ID") @RequestHeader("X-User-Id") Long userId) {
|
||||
UserVO vo = userService.getUserById(userId);
|
||||
return Result.success(vo);
|
||||
}
|
||||
@ -33,9 +38,10 @@ public class ProfileController {
|
||||
/**
|
||||
* 更新个人信息
|
||||
*/
|
||||
@Operation(summary = "更新个人信息")
|
||||
@PutMapping
|
||||
public Result<Boolean> updateProfile(
|
||||
@RequestHeader("X-User-Id") Long userId,
|
||||
@Parameter(description = "当前登录用户ID") @RequestHeader("X-User-Id") Long userId,
|
||||
@Valid @RequestBody ProfileDTO dto) {
|
||||
boolean result = userService.updateProfile(userId, dto);
|
||||
return Result.success(result);
|
||||
@ -44,9 +50,10 @@ public class ProfileController {
|
||||
/**
|
||||
* 修改密码
|
||||
*/
|
||||
@Operation(summary = "修改密码", description = "需要提供旧密码和新密码(MD5加密后传输)")
|
||||
@PutMapping("/password")
|
||||
public Result<Boolean> updatePassword(
|
||||
@RequestHeader("X-User-Id") Long userId,
|
||||
@Parameter(description = "当前登录用户ID") @RequestHeader("X-User-Id") Long userId,
|
||||
@Valid @RequestBody PasswordDTO dto) {
|
||||
boolean result = userService.updatePassword(userId, dto);
|
||||
return Result.success(result);
|
||||
|
||||
@ -5,6 +5,9 @@ import com.fundplatform.common.core.Result;
|
||||
import com.fundplatform.sys.dto.RoleDTO;
|
||||
import com.fundplatform.sys.service.RoleService;
|
||||
import com.fundplatform.sys.vo.RoleVO;
|
||||
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 org.springframework.web.bind.annotation.*;
|
||||
|
||||
@ -13,6 +16,7 @@ import java.util.List;
|
||||
/**
|
||||
* 角色管理Controller
|
||||
*/
|
||||
@Tag(name = "角色管理", description = "角色的增删改查及菜单权限分配")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/sys/role")
|
||||
public class RoleController {
|
||||
@ -23,48 +27,55 @@ public class RoleController {
|
||||
this.roleService = roleService;
|
||||
}
|
||||
|
||||
@Operation(summary = "创建角色")
|
||||
@PostMapping
|
||||
public Result<Long> create(@Valid @RequestBody RoleDTO dto) {
|
||||
Long roleId = roleService.createRole(dto);
|
||||
return Result.success(roleId);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新角色信息")
|
||||
@PutMapping
|
||||
public Result<Boolean> update(@Valid @RequestBody RoleDTO dto) {
|
||||
boolean result = roleService.updateRole(dto);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "根据ID查询角色")
|
||||
@GetMapping("/{id}")
|
||||
public Result<RoleVO> getById(@PathVariable Long id) {
|
||||
public Result<RoleVO> getById(@Parameter(description = "角色ID") @PathVariable Long id) {
|
||||
RoleVO vo = roleService.getRoleById(id);
|
||||
return Result.success(vo);
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询角色")
|
||||
@GetMapping("/page")
|
||||
public Result<Page<RoleVO>> page(
|
||||
@RequestParam(defaultValue = "1") int pageNum,
|
||||
@RequestParam(defaultValue = "10") int pageSize,
|
||||
@RequestParam(required = false) String roleName,
|
||||
@RequestParam(required = false) Integer status) {
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") int pageNum,
|
||||
@Parameter(description = "每页条数") @RequestParam(defaultValue = "10") int pageSize,
|
||||
@Parameter(description = "角色名称(模糊查询)") @RequestParam(required = false) String roleName,
|
||||
@Parameter(description = "状态:0禁用 1启用") @RequestParam(required = false) Integer status) {
|
||||
Page<RoleVO> page = roleService.pageRoles(pageNum, pageSize, roleName, status);
|
||||
return Result.success(page);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询所有角色列表")
|
||||
@GetMapping("/list")
|
||||
public Result<List<RoleVO>> listAll() {
|
||||
List<RoleVO> list = roleService.listAllRoles();
|
||||
return Result.success(list);
|
||||
}
|
||||
|
||||
@Operation(summary = "删除角色")
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Boolean> delete(@PathVariable Long id) {
|
||||
public Result<Boolean> delete(@Parameter(description = "角色ID") @PathVariable Long id) {
|
||||
boolean result = roleService.deleteRole(id);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新角色状态")
|
||||
@PutMapping("/{id}/status")
|
||||
public Result<Boolean> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
|
||||
public Result<Boolean> updateStatus(@Parameter(description = "角色ID") @PathVariable Long id, @Parameter(description = "状态:0禁用 1启用") @RequestParam Integer status) {
|
||||
boolean result = roleService.updateStatus(id, status);
|
||||
return Result.success(result);
|
||||
}
|
||||
@ -72,8 +83,9 @@ public class RoleController {
|
||||
/**
|
||||
* 获取角色菜单ID列表
|
||||
*/
|
||||
@Operation(summary = "获取角色的菜单ID列表")
|
||||
@GetMapping("/{id}/menus")
|
||||
public Result<List<Long>> getRoleMenus(@PathVariable Long id) {
|
||||
public Result<List<Long>> getRoleMenus(@Parameter(description = "角色ID") @PathVariable Long id) {
|
||||
List<Long> menuIds = roleService.getRoleMenus(id);
|
||||
return Result.success(menuIds);
|
||||
}
|
||||
@ -81,8 +93,9 @@ public class RoleController {
|
||||
/**
|
||||
* 分配菜单给角色
|
||||
*/
|
||||
@Operation(summary = "为角色分配菜单权限")
|
||||
@PutMapping("/{id}/menus")
|
||||
public Result<Boolean> assignMenus(@PathVariable Long id, @RequestBody List<Long> menuIds) {
|
||||
public Result<Boolean> assignMenus(@Parameter(description = "角色ID") @PathVariable Long id, @RequestBody List<Long> menuIds) {
|
||||
boolean result = roleService.assignMenus(id, menuIds);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@ -5,12 +5,16 @@ import com.fundplatform.common.core.Result;
|
||||
import com.fundplatform.sys.dto.TenantDTO;
|
||||
import com.fundplatform.sys.service.TenantService;
|
||||
import com.fundplatform.sys.vo.TenantVO;
|
||||
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 org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 租户管理Controller
|
||||
*/
|
||||
@Tag(name = "租户管理", description = "平台租户的增删改查及状态管理(超级管理员使用)")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/sys/tenant")
|
||||
public class TenantController {
|
||||
@ -21,36 +25,42 @@ public class TenantController {
|
||||
this.tenantService = tenantService;
|
||||
}
|
||||
|
||||
@Operation(summary = "创建租户")
|
||||
@PostMapping
|
||||
public Result<Long> create(@Valid @RequestBody TenantDTO dto) {
|
||||
return Result.success(tenantService.createTenant(dto));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新租户信息")
|
||||
@PutMapping
|
||||
public Result<Boolean> update(@Valid @RequestBody TenantDTO dto) {
|
||||
return Result.success(tenantService.updateTenant(dto));
|
||||
}
|
||||
|
||||
@Operation(summary = "根据ID查询租户")
|
||||
@GetMapping("/{id}")
|
||||
public Result<TenantVO> getById(@PathVariable Long id) {
|
||||
public Result<TenantVO> getById(@Parameter(description = "租户ID") @PathVariable Long id) {
|
||||
return Result.success(tenantService.getTenantById(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询租户")
|
||||
@GetMapping("/page")
|
||||
public Result<Page<TenantVO>> page(
|
||||
@RequestParam(defaultValue = "1") int pageNum,
|
||||
@RequestParam(defaultValue = "10") int pageSize,
|
||||
@RequestParam(required = false) String keyword) {
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") int pageNum,
|
||||
@Parameter(description = "每页条数") @RequestParam(defaultValue = "10") int pageSize,
|
||||
@Parameter(description = "关键词(租户名/编码/联系人模糊查询)") @RequestParam(required = false) String keyword) {
|
||||
return Result.success(tenantService.pageTenants(pageNum, pageSize, keyword));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除租户")
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Boolean> delete(@PathVariable Long id) {
|
||||
public Result<Boolean> delete(@Parameter(description = "租户ID") @PathVariable Long id) {
|
||||
return Result.success(tenantService.deleteTenant(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新租户状态")
|
||||
@PutMapping("/{id}/status")
|
||||
public Result<Boolean> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
|
||||
public Result<Boolean> updateStatus(@Parameter(description = "租户ID") @PathVariable Long id, @Parameter(description = "状态:0禁用 1启用 2过期") @RequestParam Integer status) {
|
||||
return Result.success(tenantService.updateStatus(id, status));
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,12 +5,16 @@ import com.fundplatform.common.core.Result;
|
||||
import com.fundplatform.sys.dto.UserDTO;
|
||||
import com.fundplatform.sys.service.UserService;
|
||||
import com.fundplatform.sys.vo.UserVO;
|
||||
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 org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 用户管理Controller
|
||||
*/
|
||||
@Tag(name = "用户管理", description = "用户的增删改查及状态管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/sys/user")
|
||||
public class UserController {
|
||||
@ -24,6 +28,7 @@ public class UserController {
|
||||
/**
|
||||
* 创建用户
|
||||
*/
|
||||
@Operation(summary = "创建用户")
|
||||
@PostMapping
|
||||
public Result<Long> create(@Valid @RequestBody UserDTO dto) {
|
||||
Long userId = userService.createUser(dto);
|
||||
@ -33,6 +38,7 @@ public class UserController {
|
||||
/**
|
||||
* 更新用户
|
||||
*/
|
||||
@Operation(summary = "更新用户信息")
|
||||
@PutMapping
|
||||
public Result<Boolean> update(@Valid @RequestBody UserDTO dto) {
|
||||
boolean result = userService.updateUser(dto);
|
||||
@ -42,8 +48,9 @@ public class UserController {
|
||||
/**
|
||||
* 根据ID查询用户
|
||||
*/
|
||||
@Operation(summary = "根据ID查询用户")
|
||||
@GetMapping("/{id}")
|
||||
public Result<UserVO> getById(@PathVariable Long id) {
|
||||
public Result<UserVO> getById(@Parameter(description = "用户ID") @PathVariable Long id) {
|
||||
UserVO vo = userService.getUserById(id);
|
||||
return Result.success(vo);
|
||||
}
|
||||
@ -51,13 +58,14 @@ public class UserController {
|
||||
/**
|
||||
* 分页查询用户
|
||||
*/
|
||||
@Operation(summary = "分页查询用户")
|
||||
@GetMapping("/page")
|
||||
public Result<Page<UserVO>> page(
|
||||
@RequestParam(defaultValue = "1") int pageNum,
|
||||
@RequestParam(defaultValue = "10") int pageSize,
|
||||
@RequestParam(required = false) String username,
|
||||
@RequestParam(required = false) Integer status,
|
||||
@RequestParam(required = false) Long deptId) {
|
||||
@Parameter(description = "页码,从1开始") @RequestParam(defaultValue = "1") int pageNum,
|
||||
@Parameter(description = "每页条数") @RequestParam(defaultValue = "10") int pageSize,
|
||||
@Parameter(description = "用户名(模糊查询)") @RequestParam(required = false) String username,
|
||||
@Parameter(description = "状态:0禁用 1启用") @RequestParam(required = false) Integer status,
|
||||
@Parameter(description = "部门ID") @RequestParam(required = false) Long deptId) {
|
||||
Page<UserVO> page = userService.pageUsers(pageNum, pageSize, username, status, deptId);
|
||||
return Result.success(page);
|
||||
}
|
||||
@ -65,8 +73,9 @@ public class UserController {
|
||||
/**
|
||||
* 删除用户
|
||||
*/
|
||||
@Operation(summary = "删除用户(逻辑删除)")
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Boolean> delete(@PathVariable Long id) {
|
||||
public Result<Boolean> delete(@Parameter(description = "用户ID") @PathVariable Long id) {
|
||||
boolean result = userService.deleteUser(id);
|
||||
return Result.success(result);
|
||||
}
|
||||
@ -74,6 +83,7 @@ public class UserController {
|
||||
/**
|
||||
* 批量删除用户
|
||||
*/
|
||||
@Operation(summary = "批量删除用户")
|
||||
@DeleteMapping("/batch")
|
||||
public Result<Boolean> batchDelete(@RequestBody Long[] ids) {
|
||||
boolean result = userService.batchDeleteUsers(ids);
|
||||
@ -83,8 +93,9 @@ public class UserController {
|
||||
/**
|
||||
* 重置密码
|
||||
*/
|
||||
@Operation(summary = "重置用户密码", description = "将用户密码重置为系统默认密码")
|
||||
@PutMapping("/{id}/reset-password")
|
||||
public Result<Boolean> resetPassword(@PathVariable Long id) {
|
||||
public Result<Boolean> resetPassword(@Parameter(description = "用户ID") @PathVariable Long id) {
|
||||
boolean result = userService.resetPassword(id);
|
||||
return Result.success(result);
|
||||
}
|
||||
@ -92,8 +103,9 @@ public class UserController {
|
||||
/**
|
||||
* 更新用户状态
|
||||
*/
|
||||
@Operation(summary = "更新用户状态")
|
||||
@PutMapping("/{id}/status")
|
||||
public Result<Boolean> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
|
||||
public Result<Boolean> updateStatus(@Parameter(description = "用户ID") @PathVariable Long id, @Parameter(description = "状态:0禁用 1启用") @RequestParam Integer status) {
|
||||
boolean result = userService.updateStatus(id, status);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.fundplatform.sys.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 登录请求DTO
|
||||
@ -13,6 +14,12 @@ public class LoginRequestDTO {
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 租户ID,登录时必须指定,以确保用户名在正确租户范围内唯一匹配,防止跨租户登录混乱。
|
||||
*/
|
||||
@NotNull(message = "租户ID不能为空")
|
||||
private Long tenantId;
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
@ -28,4 +35,12 @@ public class LoginRequestDTO {
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public Long getTenantId() {
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
public void setTenantId(Long tenantId) {
|
||||
this.tenantId = tenantId;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ package com.fundplatform.sys.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.fundplatform.common.auth.TokenService;
|
||||
import com.fundplatform.common.util.Md5Util;
|
||||
import com.fundplatform.common.mybatis.TenantIgnoreHelper;
|
||||
import com.fundplatform.sys.data.entity.SysUser;
|
||||
import com.fundplatform.sys.data.service.SysUserDataService;
|
||||
import com.fundplatform.sys.dto.LoginRequestDTO;
|
||||
@ -29,14 +29,20 @@ public class AuthServiceImpl implements AuthService {
|
||||
|
||||
@Override
|
||||
public LoginVO login(LoginRequestDTO request) {
|
||||
// 查询用户
|
||||
// 安全修复:登录时尚未建立租户上下文(白名单路径不经过 TokenAuthFilter),
|
||||
// 必须使用 TenantIgnoreHelper 跳过 MyBatis-Plus 自动租户过滤,
|
||||
// 同时显式在查询条件中加入 tenantId,确保只在指定租户范围内匹配用户名,
|
||||
// 防止不同租户的同名用户发生认证混乱。
|
||||
SysUser user = TenantIgnoreHelper.ignore(() -> {
|
||||
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(SysUser::getUsername, request.getUsername());
|
||||
wrapper.eq(SysUser::getTenantId, request.getTenantId());
|
||||
wrapper.eq(SysUser::getDeleted, 0);
|
||||
SysUser user = userDataService.getOne(wrapper);
|
||||
return userDataService.getOne(wrapper);
|
||||
});
|
||||
|
||||
if (user == null) {
|
||||
log.error("登录失败 - 用户不存在:username={}", request.getUsername());
|
||||
log.error("登录失败 - 用户不存在:username={}, tenantId={}", request.getUsername(), request.getTenantId());
|
||||
throw new RuntimeException("用户名或密码错误");
|
||||
}
|
||||
|
||||
|
||||
@ -47,13 +47,10 @@ public class UserServiceImpl implements UserService {
|
||||
throw new RuntimeException("用户名已存在");
|
||||
}
|
||||
|
||||
// 创建用户,密码使用 MD5 加密
|
||||
// 创建用户,密码由前端 MD5 加密后传入,直接存储
|
||||
SysUser user = new SysUser();
|
||||
String rawPassword = dto.getPassword();
|
||||
String md5Password = Md5Util.encrypt(rawPassword);
|
||||
log.info("创建用户 - 原始密码:{}, MD5 加密后:{}", rawPassword, md5Password);
|
||||
user.setUsername(dto.getUsername());
|
||||
user.setPassword(md5Password);
|
||||
user.setPassword(dto.getPassword());
|
||||
user.setRealName(dto.getRealName());
|
||||
user.setPhone(dto.getPhone());
|
||||
user.setEmail(dto.getEmail());
|
||||
@ -100,10 +97,8 @@ public class UserServiceImpl implements UserService {
|
||||
user.setUsername(dto.getUsername());
|
||||
}
|
||||
if (StringUtils.hasText(dto.getPassword())) {
|
||||
String rawPassword = dto.getPassword();
|
||||
String md5Password = Md5Util.encrypt(rawPassword);
|
||||
log.info("更新用户密码 - 原始密码:{}, MD5 加密后:{}", rawPassword, md5Password);
|
||||
user.setPassword(md5Password);
|
||||
// 密码由前端 MD5 加密后传入,直接存储
|
||||
user.setPassword(dto.getPassword());
|
||||
}
|
||||
user.setRealName(dto.getRealName());
|
||||
user.setPhone(dto.getPhone());
|
||||
@ -243,16 +238,15 @@ public class UserServiceImpl implements UserService {
|
||||
throw new RuntimeException("用户不存在");
|
||||
}
|
||||
|
||||
// 验证旧密码
|
||||
if (!Md5Util.matches(dto.getOldPassword(), user.getPassword())) {
|
||||
// 验证旧密码(前端已 MD5,直接与数据库存储的 MD5 值比对)
|
||||
if (!dto.getOldPassword().equals(user.getPassword())) {
|
||||
log.error("修改密码失败 - 旧密码错误:userId={}", userId);
|
||||
throw new RuntimeException("旧密码错误");
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
String rawNewPassword = dto.getNewPassword();
|
||||
String md5NewPassword = Md5Util.encrypt(rawNewPassword);
|
||||
log.info("修改用户密码 - userId={}, 原始新密码:{}, MD5: {}", userId, rawNewPassword, md5NewPassword);
|
||||
// 更新密码(前端已 MD5 加密,直接存储)
|
||||
String md5NewPassword = dto.getNewPassword();
|
||||
log.info("修改用户密码 - userId={}", userId);
|
||||
LambdaUpdateWrapper<SysUser> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(SysUser::getId, userId);
|
||||
wrapper.set(SysUser::getPassword, md5NewPassword);
|
||||
|
||||
@ -0,0 +1,185 @@
|
||||
package com.fundplatform.sys.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.fundplatform.common.auth.TokenService;
|
||||
import com.fundplatform.sys.data.entity.SysUser;
|
||||
import com.fundplatform.sys.data.service.SysUserDataService;
|
||||
import com.fundplatform.sys.dto.LoginRequestDTO;
|
||||
import com.fundplatform.sys.vo.LoginVO;
|
||||
import com.fundplatform.sys.vo.UserVO;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 认证服务单元测试
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AuthServiceImplTest {
|
||||
|
||||
@Mock
|
||||
private SysUserDataService userDataService;
|
||||
|
||||
@Mock
|
||||
private TokenService tokenService;
|
||||
|
||||
@InjectMocks
|
||||
private AuthServiceImpl authService;
|
||||
|
||||
private SysUser mockUser;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockUser = new SysUser();
|
||||
mockUser.setId(1L);
|
||||
mockUser.setUsername("testuser");
|
||||
// MD5("password123") 的值,直接使用固定的 MD5 字符串避免调用加密工具
|
||||
mockUser.setPassword("482c811da5d5b4bc6d497ffa98491e38");
|
||||
mockUser.setStatus(1);
|
||||
mockUser.setTenantId(100L);
|
||||
mockUser.setDeleted(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("登录成功 - 用户名密码正确")
|
||||
void login_success() {
|
||||
LoginRequestDTO request = new LoginRequestDTO();
|
||||
request.setUsername("testuser");
|
||||
// 前端传来的 MD5 密码与数据库一致
|
||||
request.setPassword("482c811da5d5b4bc6d497ffa98491e38");
|
||||
|
||||
when(userDataService.getOne(ArgumentMatchers.<LambdaQueryWrapper<SysUser>>any()))
|
||||
.thenReturn(mockUser);
|
||||
when(tokenService.generateToken(1L, "testuser", 100L))
|
||||
.thenReturn("mock-token-uuid");
|
||||
|
||||
LoginVO result = authService.login(request);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1L, result.getUserId());
|
||||
assertEquals("testuser", result.getUsername());
|
||||
assertEquals("mock-token-uuid", result.getToken());
|
||||
assertEquals(100L, result.getTenantId());
|
||||
verify(tokenService).generateToken(1L, "testuser", 100L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("登录失败 - 用户不存在")
|
||||
void login_fail_userNotFound() {
|
||||
LoginRequestDTO request = new LoginRequestDTO();
|
||||
request.setUsername("nouser");
|
||||
request.setPassword("anypassword");
|
||||
|
||||
when(userDataService.getOne(ArgumentMatchers.<LambdaQueryWrapper<SysUser>>any()))
|
||||
.thenReturn(null);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> authService.login(request));
|
||||
assertEquals("用户名或密码错误", ex.getMessage());
|
||||
verify(tokenService, never()).generateToken(anyLong(), anyString(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("登录失败 - 密码错误")
|
||||
void login_fail_wrongPassword() {
|
||||
LoginRequestDTO request = new LoginRequestDTO();
|
||||
request.setUsername("testuser");
|
||||
request.setPassword("wrongmd5hash00000000000000000000");
|
||||
|
||||
when(userDataService.getOne(ArgumentMatchers.<LambdaQueryWrapper<SysUser>>any()))
|
||||
.thenReturn(mockUser);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> authService.login(request));
|
||||
assertEquals("用户名或密码错误", ex.getMessage());
|
||||
verify(tokenService, never()).generateToken(anyLong(), anyString(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("登录失败 - 用户已被禁用")
|
||||
void login_fail_userDisabled() {
|
||||
mockUser.setStatus(0);
|
||||
|
||||
LoginRequestDTO request = new LoginRequestDTO();
|
||||
request.setUsername("testuser");
|
||||
request.setPassword("482c811da5d5b4bc6d497ffa98491e38");
|
||||
|
||||
when(userDataService.getOne(ArgumentMatchers.<LambdaQueryWrapper<SysUser>>any()))
|
||||
.thenReturn(mockUser);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> authService.login(request));
|
||||
assertEquals("用户已被禁用", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("登出成功 - 清除所有 Token")
|
||||
void logout_success() {
|
||||
doNothing().when(tokenService).deleteAllUserTokens(1L);
|
||||
|
||||
authService.logout(1L);
|
||||
|
||||
verify(tokenService).deleteAllUserTokens(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("刷新 Token 成功")
|
||||
void refreshToken_success() {
|
||||
when(userDataService.getById(1L)).thenReturn(mockUser);
|
||||
when(tokenService.generateToken(1L, "testuser", 100L)).thenReturn("new-token");
|
||||
|
||||
LoginVO result = authService.refreshToken(1L);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals("new-token", result.getToken());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("刷新 Token 失败 - 用户不存在")
|
||||
void refreshToken_fail_userNotFound() {
|
||||
when(userDataService.getById(99L)).thenReturn(null);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> authService.refreshToken(99L));
|
||||
assertEquals("用户不存在", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("刷新 Token 失败 - 用户已被禁用")
|
||||
void refreshToken_fail_userDisabled() {
|
||||
mockUser.setStatus(0);
|
||||
when(userDataService.getById(1L)).thenReturn(mockUser);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> authService.refreshToken(1L));
|
||||
assertEquals("用户已被禁用", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("获取用户信息成功")
|
||||
void getUserInfo_success() {
|
||||
mockUser.setRealName("测试用户");
|
||||
mockUser.setPhone("13800138000");
|
||||
mockUser.setEmail("test@example.com");
|
||||
when(userDataService.getById(1L)).thenReturn(mockUser);
|
||||
|
||||
UserVO result = authService.getUserInfo(1L);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1L, result.getId());
|
||||
assertEquals("testuser", result.getUsername());
|
||||
assertEquals("测试用户", result.getRealName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("获取用户信息失败 - 用户不存在")
|
||||
void getUserInfo_fail_userNotFound() {
|
||||
when(userDataService.getById(99L)).thenReturn(null);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> authService.getUserInfo(99L));
|
||||
assertEquals("用户不存在", ex.getMessage());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,312 @@
|
||||
package com.fundplatform.sys.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.fundplatform.sys.data.entity.SysRole;
|
||||
import com.fundplatform.sys.data.service.SysRoleDataService;
|
||||
import com.fundplatform.sys.dto.RoleDTO;
|
||||
import com.fundplatform.sys.vo.RoleVO;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 角色服务单元测试
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RoleServiceImplTest {
|
||||
|
||||
@Mock
|
||||
private SysRoleDataService roleDataService;
|
||||
|
||||
@InjectMocks
|
||||
private RoleServiceImpl roleService;
|
||||
|
||||
private SysRole mockRole;
|
||||
|
||||
@BeforeAll
|
||||
static void initMybatisPlusLambdaCache() {
|
||||
MapperBuilderAssistant assistant = new MapperBuilderAssistant(
|
||||
new org.apache.ibatis.session.Configuration(), "");
|
||||
TableInfoHelper.initTableInfo(assistant, SysRole.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
mockRole = new SysRole();
|
||||
mockRole.setId(1L);
|
||||
mockRole.setRoleCode("ADMIN");
|
||||
mockRole.setRoleName("管理员");
|
||||
mockRole.setDataScope(1);
|
||||
mockRole.setStatus(1);
|
||||
mockRole.setSortOrder(1);
|
||||
mockRole.setDeleted(0);
|
||||
mockRole.setTenantId(100L);
|
||||
}
|
||||
|
||||
// ==================== createRole ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("创建角色成功")
|
||||
void createRole_success() {
|
||||
RoleDTO dto = new RoleDTO();
|
||||
dto.setRoleCode("OPERATOR");
|
||||
dto.setRoleName("操作员");
|
||||
dto.setDataScope(2);
|
||||
dto.setStatus(1);
|
||||
|
||||
when(roleDataService.count(ArgumentMatchers.<LambdaQueryWrapper<SysRole>>any())).thenReturn(0L);
|
||||
when(roleDataService.save(any(SysRole.class))).thenReturn(true);
|
||||
|
||||
assertDoesNotThrow(() -> roleService.createRole(dto));
|
||||
verify(roleDataService).save(any(SysRole.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建角色失败 - 角色编码已存在")
|
||||
void createRole_fail_roleCodeExists() {
|
||||
RoleDTO dto = new RoleDTO();
|
||||
dto.setRoleCode("ADMIN");
|
||||
dto.setRoleName("管理员副本");
|
||||
|
||||
when(roleDataService.count(ArgumentMatchers.<LambdaQueryWrapper<SysRole>>any())).thenReturn(1L);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> roleService.createRole(dto));
|
||||
assertEquals("角色编码已存在", ex.getMessage());
|
||||
verify(roleDataService, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建角色 - 默认启用状态为1")
|
||||
void createRole_defaultStatus() {
|
||||
RoleDTO dto = new RoleDTO();
|
||||
dto.setRoleCode("VIEWER");
|
||||
dto.setRoleName("查看者");
|
||||
// status 和 dataScope 不传,使用默认值
|
||||
|
||||
when(roleDataService.count(ArgumentMatchers.<LambdaQueryWrapper<SysRole>>any())).thenReturn(0L);
|
||||
when(roleDataService.save(any(SysRole.class))).thenReturn(true);
|
||||
|
||||
assertDoesNotThrow(() -> roleService.createRole(dto));
|
||||
}
|
||||
|
||||
// ==================== updateRole ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("更新角色成功")
|
||||
void updateRole_success() {
|
||||
RoleDTO dto = new RoleDTO();
|
||||
dto.setId(1L);
|
||||
dto.setRoleName("超级管理员");
|
||||
dto.setDataScope(1);
|
||||
dto.setStatus(1);
|
||||
|
||||
when(roleDataService.getById(1L)).thenReturn(mockRole);
|
||||
when(roleDataService.updateById(any(SysRole.class))).thenReturn(true);
|
||||
|
||||
boolean result = roleService.updateRole(dto);
|
||||
|
||||
assertTrue(result);
|
||||
verify(roleDataService).updateById(any(SysRole.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("更新角色失败 - ID为空")
|
||||
void updateRole_fail_idNull() {
|
||||
RoleDTO dto = new RoleDTO();
|
||||
dto.setId(null);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> roleService.updateRole(dto));
|
||||
assertEquals("角色ID不能为空", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("更新角色失败 - 角色不存在")
|
||||
void updateRole_fail_notFound() {
|
||||
RoleDTO dto = new RoleDTO();
|
||||
dto.setId(99L);
|
||||
dto.setRoleName("不存在的角色");
|
||||
|
||||
when(roleDataService.getById(99L)).thenReturn(null);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> roleService.updateRole(dto));
|
||||
assertEquals("角色不存在", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("更新角色失败 - 角色已删除")
|
||||
void updateRole_fail_deleted() {
|
||||
mockRole.setDeleted(1);
|
||||
RoleDTO dto = new RoleDTO();
|
||||
dto.setId(1L);
|
||||
dto.setRoleName("已删除角色");
|
||||
|
||||
when(roleDataService.getById(1L)).thenReturn(mockRole);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> roleService.updateRole(dto));
|
||||
assertEquals("角色不存在", ex.getMessage());
|
||||
}
|
||||
|
||||
// ==================== getRoleById ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("根据ID查询角色成功")
|
||||
void getRoleById_success() {
|
||||
when(roleDataService.getById(1L)).thenReturn(mockRole);
|
||||
|
||||
RoleVO vo = roleService.getRoleById(1L);
|
||||
|
||||
assertNotNull(vo);
|
||||
assertEquals(1L, vo.getId());
|
||||
assertEquals("ADMIN", vo.getRoleCode());
|
||||
assertEquals("管理员", vo.getRoleName());
|
||||
assertEquals("全部数据", vo.getDataScopeName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("根据ID查询角色失败 - 不存在")
|
||||
void getRoleById_fail_notFound() {
|
||||
when(roleDataService.getById(99L)).thenReturn(null);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> roleService.getRoleById(99L));
|
||||
assertEquals("角色不存在", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("根据ID查询角色失败 - 已删除")
|
||||
void getRoleById_fail_deleted() {
|
||||
mockRole.setDeleted(1);
|
||||
when(roleDataService.getById(1L)).thenReturn(mockRole);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> roleService.getRoleById(1L));
|
||||
assertEquals("角色不存在", ex.getMessage());
|
||||
}
|
||||
|
||||
// ==================== pageRoles ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("分页查询角色成功")
|
||||
void pageRoles_success() {
|
||||
Page<SysRole> mockPage = new Page<>(1, 10);
|
||||
mockPage.setRecords(List.of(mockRole));
|
||||
mockPage.setTotal(1);
|
||||
|
||||
when(roleDataService.page(any(Page.class), ArgumentMatchers.<LambdaQueryWrapper<SysRole>>any()))
|
||||
.thenReturn(mockPage);
|
||||
|
||||
Page<RoleVO> result = roleService.pageRoles(1, 10, null, null);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.getTotal());
|
||||
assertEquals(1, result.getRecords().size());
|
||||
assertEquals("ADMIN", result.getRecords().get(0).getRoleCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("分页查询角色 - 带关键词过滤")
|
||||
void pageRoles_withFilter() {
|
||||
Page<SysRole> mockPage = new Page<>(1, 10);
|
||||
mockPage.setRecords(List.of(mockRole));
|
||||
mockPage.setTotal(1);
|
||||
|
||||
when(roleDataService.page(any(Page.class), ArgumentMatchers.<LambdaQueryWrapper<SysRole>>any()))
|
||||
.thenReturn(mockPage);
|
||||
|
||||
Page<RoleVO> result = roleService.pageRoles(1, 10, "管理", 1);
|
||||
|
||||
assertEquals(1, result.getTotal());
|
||||
}
|
||||
|
||||
// ==================== listAllRoles ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("获取所有启用角色列表")
|
||||
void listAllRoles_success() {
|
||||
when(roleDataService.list(ArgumentMatchers.<LambdaQueryWrapper<SysRole>>any()))
|
||||
.thenReturn(List.of(mockRole));
|
||||
|
||||
List<RoleVO> result = roleService.listAllRoles();
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("获取所有启用角色列表 - 空列表")
|
||||
void listAllRoles_empty() {
|
||||
when(roleDataService.list(ArgumentMatchers.<LambdaQueryWrapper<SysRole>>any()))
|
||||
.thenReturn(List.of());
|
||||
|
||||
List<RoleVO> result = roleService.listAllRoles();
|
||||
|
||||
assertNotNull(result);
|
||||
assertTrue(result.isEmpty());
|
||||
}
|
||||
|
||||
// ==================== deleteRole ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("删除角色成功 - 逻辑删除")
|
||||
void deleteRole_success() {
|
||||
when(roleDataService.update(ArgumentMatchers.<LambdaUpdateWrapper<SysRole>>any())).thenReturn(true);
|
||||
|
||||
boolean result = roleService.deleteRole(1L);
|
||||
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
// ==================== updateStatus ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("更新角色状态成功")
|
||||
void updateStatus_success() {
|
||||
when(roleDataService.update(ArgumentMatchers.<LambdaUpdateWrapper<SysRole>>any())).thenReturn(true);
|
||||
|
||||
boolean result = roleService.updateStatus(1L, 0);
|
||||
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
// ==================== dataScopeName ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("数据范围名称转换 - 各枚举值正确")
|
||||
void getRoleById_dataScopeNames() {
|
||||
int[] scopes = {1, 2, 3, 4};
|
||||
String[] names = {"全部数据", "本部门数据", "本部门及下级数据", "仅本人数据"};
|
||||
|
||||
for (int i = 0; i < scopes.length; i++) {
|
||||
mockRole.setDataScope(scopes[i]);
|
||||
mockRole.setDeleted(0);
|
||||
when(roleDataService.getById((long) (i + 10))).thenReturn(mockRole);
|
||||
|
||||
RoleVO vo = roleService.getRoleById((long) (i + 10));
|
||||
assertEquals(names[i], vo.getDataScopeName(), "dataScope=" + scopes[i] + " 的名称应为 " + names[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("数据范围名称转换 - 未知值返回空字符串")
|
||||
void getRoleById_unknownDataScope() {
|
||||
mockRole.setDataScope(99);
|
||||
when(roleDataService.getById(1L)).thenReturn(mockRole);
|
||||
|
||||
RoleVO vo = roleService.getRoleById(1L);
|
||||
assertEquals("", vo.getDataScopeName());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,348 @@
|
||||
package com.fundplatform.sys.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.fundplatform.common.context.UserContextHolder;
|
||||
import com.fundplatform.sys.data.entity.SysTenant;
|
||||
import com.fundplatform.sys.data.service.SysTenantDataService;
|
||||
import com.fundplatform.sys.dto.TenantDTO;
|
||||
import com.fundplatform.sys.vo.TenantVO;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 租户服务单元测试
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class TenantServiceImplTest {
|
||||
|
||||
@Mock
|
||||
private SysTenantDataService tenantDataService;
|
||||
|
||||
@InjectMocks
|
||||
private TenantServiceImpl tenantService;
|
||||
|
||||
private SysTenant mockTenant;
|
||||
|
||||
@BeforeAll
|
||||
static void initMybatisPlusLambdaCache() {
|
||||
MapperBuilderAssistant assistant = new MapperBuilderAssistant(
|
||||
new org.apache.ibatis.session.Configuration(), "");
|
||||
TableInfoHelper.initTableInfo(assistant, SysTenant.class);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// 设置 UserContextHolder(TenantServiceImpl 调用 UserContextHolder.getUserId())
|
||||
UserContextHolder.setUserId(1L);
|
||||
|
||||
mockTenant = new SysTenant();
|
||||
mockTenant.setId(1L);
|
||||
mockTenant.setTenantCode("TENANT001");
|
||||
mockTenant.setTenantName("测试租户");
|
||||
mockTenant.setContact("张三");
|
||||
mockTenant.setPhone("13800138000");
|
||||
mockTenant.setEmail("tenant@example.com");
|
||||
mockTenant.setAddress("北京市");
|
||||
mockTenant.setStatus(1);
|
||||
mockTenant.setMaxUsers(10);
|
||||
mockTenant.setDeleted(0);
|
||||
mockTenant.setExpireTime(LocalDateTime.now().plusYears(1));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
UserContextHolder.clear();
|
||||
}
|
||||
|
||||
// ==================== createTenant ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("创建租户成功")
|
||||
void createTenant_success() {
|
||||
TenantDTO dto = new TenantDTO();
|
||||
dto.setTenantCode("NEWTENANT");
|
||||
dto.setTenantName("新租户");
|
||||
dto.setStatus(1);
|
||||
dto.setMaxUsers(20);
|
||||
|
||||
when(tenantDataService.count(ArgumentMatchers.<LambdaQueryWrapper<SysTenant>>any())).thenReturn(0L);
|
||||
when(tenantDataService.save(any(SysTenant.class))).thenReturn(true);
|
||||
|
||||
assertDoesNotThrow(() -> tenantService.createTenant(dto));
|
||||
verify(tenantDataService).save(any(SysTenant.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建租户失败 - 租户编码已存在")
|
||||
void createTenant_fail_codeExists() {
|
||||
TenantDTO dto = new TenantDTO();
|
||||
dto.setTenantCode("TENANT001");
|
||||
dto.setTenantName("重复租户");
|
||||
|
||||
when(tenantDataService.count(ArgumentMatchers.<LambdaQueryWrapper<SysTenant>>any())).thenReturn(1L);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> tenantService.createTenant(dto));
|
||||
assertEquals("租户编码已存在", ex.getMessage());
|
||||
verify(tenantDataService, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("创建租户 - 默认 maxUsers 为 10")
|
||||
void createTenant_defaultMaxUsers() {
|
||||
TenantDTO dto = new TenantDTO();
|
||||
dto.setTenantCode("NEWTENANT2");
|
||||
dto.setTenantName("默认配额租户");
|
||||
// maxUsers 不传
|
||||
|
||||
when(tenantDataService.count(ArgumentMatchers.<LambdaQueryWrapper<SysTenant>>any())).thenReturn(0L);
|
||||
when(tenantDataService.save(any(SysTenant.class))).thenReturn(true);
|
||||
|
||||
assertDoesNotThrow(() -> tenantService.createTenant(dto));
|
||||
}
|
||||
|
||||
// ==================== updateTenant ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("更新租户成功")
|
||||
void updateTenant_success() {
|
||||
TenantDTO dto = new TenantDTO();
|
||||
dto.setId(1L);
|
||||
dto.setTenantCode("TENANT001"); // 编码不变
|
||||
dto.setTenantName("更新的租户名");
|
||||
dto.setContact("李四");
|
||||
dto.setStatus(1);
|
||||
|
||||
when(tenantDataService.getById(1L)).thenReturn(mockTenant);
|
||||
when(tenantDataService.updateById(any(SysTenant.class))).thenReturn(true);
|
||||
|
||||
boolean result = tenantService.updateTenant(dto);
|
||||
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("更新租户失败 - ID为空")
|
||||
void updateTenant_fail_idNull() {
|
||||
TenantDTO dto = new TenantDTO();
|
||||
dto.setId(null);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> tenantService.updateTenant(dto));
|
||||
assertEquals("租户ID不能为空", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("更新租户失败 - 租户不存在")
|
||||
void updateTenant_fail_notFound() {
|
||||
TenantDTO dto = new TenantDTO();
|
||||
dto.setId(99L);
|
||||
dto.setTenantCode("XXXX");
|
||||
dto.setTenantName("不存在");
|
||||
|
||||
when(tenantDataService.getById(99L)).thenReturn(null);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> tenantService.updateTenant(dto));
|
||||
assertEquals("租户不存在", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("更新租户失败 - 租户已删除")
|
||||
void updateTenant_fail_deleted() {
|
||||
mockTenant.setDeleted(1);
|
||||
TenantDTO dto = new TenantDTO();
|
||||
dto.setId(1L);
|
||||
dto.setTenantCode("TENANT001");
|
||||
dto.setTenantName("已删除租户");
|
||||
|
||||
when(tenantDataService.getById(1L)).thenReturn(mockTenant);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> tenantService.updateTenant(dto));
|
||||
assertEquals("租户不存在", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("更新租户失败 - 更换编码时新编码已被占用")
|
||||
void updateTenant_fail_newCodeExists() {
|
||||
TenantDTO dto = new TenantDTO();
|
||||
dto.setId(1L);
|
||||
dto.setTenantCode("EXISTING_CODE");
|
||||
dto.setTenantName("修改编码的租户");
|
||||
|
||||
when(tenantDataService.getById(1L)).thenReturn(mockTenant);
|
||||
when(tenantDataService.count(ArgumentMatchers.<LambdaQueryWrapper<SysTenant>>any())).thenReturn(1L);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> tenantService.updateTenant(dto));
|
||||
assertEquals("租户编码已存在", ex.getMessage());
|
||||
}
|
||||
|
||||
// ==================== getTenantById ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("根据ID查询租户成功")
|
||||
void getTenantById_success() {
|
||||
when(tenantDataService.getById(1L)).thenReturn(mockTenant);
|
||||
|
||||
TenantVO vo = tenantService.getTenantById(1L);
|
||||
|
||||
assertNotNull(vo);
|
||||
assertEquals(1L, vo.getId());
|
||||
assertEquals("TENANT001", vo.getTenantCode());
|
||||
assertEquals("测试租户", vo.getTenantName());
|
||||
assertEquals("启用", vo.getStatusName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("根据ID查询租户 - 禁用状态名称为\"禁用\"")
|
||||
void getTenantById_disabled_statusName() {
|
||||
mockTenant.setStatus(0);
|
||||
when(tenantDataService.getById(1L)).thenReturn(mockTenant);
|
||||
|
||||
TenantVO vo = tenantService.getTenantById(1L);
|
||||
|
||||
assertEquals("禁用", vo.getStatusName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("根据ID查询租户失败 - 不存在")
|
||||
void getTenantById_fail_notFound() {
|
||||
when(tenantDataService.getById(99L)).thenReturn(null);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> tenantService.getTenantById(99L));
|
||||
assertEquals("租户不存在", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("根据ID查询租户失败 - 已删除")
|
||||
void getTenantById_fail_deleted() {
|
||||
mockTenant.setDeleted(1);
|
||||
when(tenantDataService.getById(1L)).thenReturn(mockTenant);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> tenantService.getTenantById(1L));
|
||||
assertEquals("租户不存在", ex.getMessage());
|
||||
}
|
||||
|
||||
// ==================== pageTenants ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("分页查询租户成功")
|
||||
void pageTenants_success() {
|
||||
Page<SysTenant> mockPage = new Page<>(1, 10);
|
||||
mockPage.setRecords(List.of(mockTenant));
|
||||
mockPage.setTotal(1);
|
||||
|
||||
when(tenantDataService.page(any(Page.class), ArgumentMatchers.<LambdaQueryWrapper<SysTenant>>any()))
|
||||
.thenReturn(mockPage);
|
||||
|
||||
Page<TenantVO> result = tenantService.pageTenants(1, 10, null);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.getTotal());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("分页查询租户 - 带关键词")
|
||||
void pageTenants_withKeyword() {
|
||||
Page<SysTenant> mockPage = new Page<>(1, 10);
|
||||
mockPage.setRecords(List.of(mockTenant));
|
||||
mockPage.setTotal(1);
|
||||
|
||||
when(tenantDataService.page(any(Page.class), ArgumentMatchers.<LambdaQueryWrapper<SysTenant>>any()))
|
||||
.thenReturn(mockPage);
|
||||
|
||||
Page<TenantVO> result = tenantService.pageTenants(1, 10, "测试");
|
||||
|
||||
assertEquals(1, result.getTotal());
|
||||
}
|
||||
|
||||
// ==================== deleteTenant ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("删除租户成功")
|
||||
void deleteTenant_success() {
|
||||
when(tenantDataService.getById(1L)).thenReturn(mockTenant);
|
||||
when(tenantDataService.update(ArgumentMatchers.<LambdaUpdateWrapper<SysTenant>>any())).thenReturn(true);
|
||||
|
||||
boolean result = tenantService.deleteTenant(1L);
|
||||
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("删除租户失败 - 租户不存在")
|
||||
void deleteTenant_fail_notFound() {
|
||||
when(tenantDataService.getById(99L)).thenReturn(null);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> tenantService.deleteTenant(99L));
|
||||
assertEquals("租户不存在", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("删除租户失败 - 不允许删除默认租户")
|
||||
void deleteTenant_fail_defaultTenant() {
|
||||
mockTenant.setTenantCode("DEFAULT");
|
||||
when(tenantDataService.getById(1L)).thenReturn(mockTenant);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> tenantService.deleteTenant(1L));
|
||||
assertEquals("默认租户不能删除", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("删除租户失败 - 已删除的租户")
|
||||
void deleteTenant_fail_alreadyDeleted() {
|
||||
mockTenant.setDeleted(1);
|
||||
when(tenantDataService.getById(1L)).thenReturn(mockTenant);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> tenantService.deleteTenant(1L));
|
||||
assertEquals("租户不存在", ex.getMessage());
|
||||
}
|
||||
|
||||
// ==================== updateStatus ====================
|
||||
|
||||
@Test
|
||||
@DisplayName("更新租户状态成功")
|
||||
void updateStatus_success() {
|
||||
when(tenantDataService.getById(1L)).thenReturn(mockTenant);
|
||||
when(tenantDataService.update(ArgumentMatchers.<LambdaUpdateWrapper<SysTenant>>any())).thenReturn(true);
|
||||
|
||||
boolean result = tenantService.updateStatus(1L, 0);
|
||||
|
||||
assertTrue(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("更新租户状态失败 - 租户不存在")
|
||||
void updateStatus_fail_notFound() {
|
||||
when(tenantDataService.getById(99L)).thenReturn(null);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> tenantService.updateStatus(99L, 0));
|
||||
assertEquals("租户不存在", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("更新租户状态失败 - 已删除的租户")
|
||||
void updateStatus_fail_deleted() {
|
||||
mockTenant.setDeleted(1);
|
||||
when(tenantDataService.getById(1L)).thenReturn(mockTenant);
|
||||
|
||||
RuntimeException ex = assertThrows(RuntimeException.class, () -> tenantService.updateStatus(1L, 0));
|
||||
assertEquals("租户不存在", ex.getMessage());
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user