Compare commits

..

20 Commits

Author SHA1 Message Date
zhangjf
fffeaa48a5 refactor(docs):清理旧版文档文件
- 删除过时的中文命名文档文件
-统一使用英文命名的规范文档
-保持文档目录整洁
2026-03-02 07:31:48 +08:00
zhangjf
52dd44a7d5 feat(sys):全面优化系统管理模块
- 完善所有控制器类(Auth、Config、Dept、Menu、Profile、Role、Tenant、User)
- 优化所有实体类(OperationLog、SysDept、SysMenu、SysTenant、SysUser)
- 更新所有DTO和VO数据传输对象
-完善所有Service接口和实现类
-增强系统管理核心功能和业务逻辑
2026-03-02 07:31:36 +08:00
zhangjf
8a3d9017e1 feat(req): 优化请款管理模块实体类
-完善FundRequest请款实体类
- 优化字段定义和业务逻辑
-增强请款管理功能
2026-03-02 07:31:29 +08:00
zhangjf
46b6b2c396 feat(receipt):完善收款管理模块功能
- 优化FundReceipt收款实体类和Receivable应收实体类
- 更新DTO和VO数据传输对象
-增强收款管理业务逻辑
2026-03-02 07:31:21 +08:00
zhangjf
5425271f94 feat(proj): 优化项目管理模块实体类
-完善Project项目实体类和Requirement需求实体类
- 优化字段定义和业务逻辑
-增强项目管理功能
2026-03-02 07:31:14 +08:00
zhangjf
d69c9b060e feat(file): 优化文件管理模块实体类
-完善FileRecord文件记录实体类
- 优化字段定义和业务逻辑
-增强文件管理功能
2026-03-02 07:31:06 +08:00
zhangjf
dfea91308e feat(exp):完善支出管理模块功能
- 优化FundExpense支出实体类和控制器
-完善ExpenseType支出类型相关类
- 更新VO和Service实现类
-增强支出管理业务逻辑
2026-03-02 07:30:59 +08:00
zhangjf
c9ef7d7306 feat(cust): 优化客户管理模块实体类
- 完善CustomerContact客户联系人实体
- 优化实体类字段定义和注解配置
-增强数据校验和业务逻辑
2026-03-02 07:30:51 +08:00
zhangjf
6dfc8ea686 feat(common): 优化基础框架和认证模块
- 完善Token认证服务和用户上下文管理
- 优化BaseEntity基础实体类
- 更新pom.xml依赖配置
-增强通用工具类功能
2026-03-02 07:30:44 +08:00
zhangjf
ab412935e1 feat(管理端): 优化支出类型管理界面
-完善支出类型列表展示
- 优化表单验证逻辑
- 改进用户交互体验
-统一UI组件样式
2026-03-02 07:30:36 +08:00
zhangjf
e93488d3d8 feat(sql): 更新数据库初始化脚本
-完善各模块表结构定义
- 优化索引和约束配置
- 更新初始化数据
-统一SQL脚本格式
2026-03-02 07:30:29 +08:00
zhangjf
83e9b2b658 docs: 更新项目文档和功能清单
- 重新整理项目功能清单文档
- 更新架构设计文档
- 完善需求文档内容
-统一文档命名规范
2026-03-02 07:30:22 +08:00
zhangjf
7ecebc9518 feat(common): 新增雪花算法ID生成器用于分布式唯一ID生成
- 添加SnowflakeIdGenerator工具类
-支持生成19位字符串ID和Long类型ID
- 解决前端JavaScript大数精度丢失问题
- 为MyBatis Plus IdentifierGenerator提供支持
2026-03-02 07:30:14 +08:00
zhangjf
bc56bd672b revert: 恢复 expense API路径与 gateway 配置保持一致
架构说明:
- 前端 baseURL: /fund
- 网关路由:Path=/fund/exp/**
- StripPrefix=1: 去掉/fund 前缀
- PrefixPath=/api/v1: 自动添加/api/v1 前缀
- 后端 Controller: /api/v1/exp/expense-type

请求流程:
前端:GET /fund/exp/expense-type/page
网关:匹配 Path=/fund/exp/** → StripPrefix=1 → PrefixPath=/api/v1
后端:接收 /api/v1/exp/expense-type/page ✓

与其他模块保持一致:
- customer: GET /customer/page → /api/v1/customer/page
- project: GET /project/page → /api/v1/project/page
- expense: GET /exp/expense-type/page → /api/v1/exp/expense-type/page

设计原则:
- 前端不硬编码/api/v1,由网关统一处理
- 符合网关路由与 API路径分离的架构设计
2026-03-01 22:39:02 +08:00
zhangjf
256a592478 fix(管理端): 修复支出类型 API路径缺失/api/v1 前缀
问题现象:
- 管理端支出类型页面无法显示数据库数据
- 后端接口返回 404

根本原因:
- ExpenseTypeController 的路径是 /api/v1/exp/expense-type
- 前端 expense.ts 中所有 API 调用都缺少 /api/v1 前缀

修复内容:
- getExpenseTypeList: /exp/expense-type/page → /api/v1/exp/expense-type/page
- getExpenseTypeTree: /exp/expense-type/tree → /api/v1/exp/expense-type/tree
- createExpenseType: /exp/expense-type → /api/v1/exp/expense-type
- updateExpenseType: /exp/expense-type/{id} → /api/v1/exp/expense-type/{id}
- deleteExpenseType: /exp/expense-type/{id} → /api/v1/exp/expense-type/{id}
- getExpenseList: /exp/expense/page → /api/v1/exp/expense/page
- getExpenseById: /exp/expense/{id} → /api/v1/exp/expense/{id}
- createExpense: /exp/expense → /api/v1/exp/expense
- updateExpense: /exp/expense/{id} → /api/v1/exp/expense/{id}
- deleteExpense: /exp/expense/{id} → /api/v1/exp/expense/{id}
- 所有审批流程 API 同样添加 /api/v1 前缀
- exportExpense 导出 URL 也添加 /api/v1 前缀

技术细节:
- 统一 API路径规范,与后端 Controller 保持一致
- 符合项目 RESTful API 设计标准(/api/v1/模块/资源)
2026-03-01 22:28:14 +08:00
zhangjf
a74875eeda feat(移动端): 新增支出使用 COS 上传附件
1. API 增强 (src/api/index.ts):
   - 新增 uploadFile 函数:支持文件上传到腾讯云 COS
   - 新增 getFileList 函数:获取文件列表
   - 新增 deleteFile 函数:删除文件

2. 新增支出页面优化 (src/views/expense/Add.vue):
   - 修改附件上传逻辑:从 base64 改为 COS 上传
   - onAfterRead: 调用 uploadFile API 上传到 COS
   - 获取 COS 返回的文件路径并存储
   - 提交时将 COS 路径数组转为逗号分隔字符串
   - 图片预览直接使用 COS URL
   - 添加上传进度提示和成功/失败反馈

技术实现:
- 使用 FormData 进行 multipart/form-data 上传
- 业务类型标识为'expense'
- 附件以 COS 完整 URL 形式存储(逗号分隔)
- 支持多图片上传(最多 9 张)
- 每张图片独立上传到 COS,获得永久可访问链接
2026-03-01 22:23:59 +08:00
zhangjf
da4488dccc feat(移动端): 优化支出管理功能
1. 新增支出 (Add.vue):
   - 增加图片附件上传功能(限制为图片类型)
   - 支持最多上传 9 张图片
   - 实现图片预览和删除功能
   - 将图片转 base64 格式提交到后端 attachments 字段

2. 支出列表 (List.vue):
   - 重构卡片布局为 5 行展示:
     * 第一行:标题 + 支出时间(右侧对齐)
     * 第二行:支出类型(左)+ 支出金额(右,红色突出显示)
     * 第三行:收款单位
     * 第四行:支付描述(可选,有内容时显示)
     * 第五行:查看附件按钮(有附件时显示,蓝色可点击)
   - 添加 formatDateTime 函数格式化日期时间
   - 添加 getAttachmentCount 函数计算附件数量
   - 添加 previewAttachments 函数实现图片预览
   - 优化样式:分隔线、图标、标签等细节美化

技术实现:
- 使用 Vant 的 van-uploader 组件上传图片
- 使用 ImagePreview 组件预览图片
- 附件以 base64 逗号分隔字符串形式存储
- 响应式布局适配移动端
2026-03-01 22:17:20 +08:00
zhangjf
6923024650 修复前端登录租户ID 缺失问题 + 新增集成测试
1. 管理后台 (fund-admin):
   - src/api/auth.ts: 登录请求自动添加默认租户ID (tenantId: 1)
   - src/views/login/index.vue: 优化 MD5 加密注释

2. 移动端 (fund-mobile):
   - src/api/index.ts: 登录 API 自动添加默认租户ID (tenantId: 1)

3. 系统服务 (fund-sys):
   - 新增 AuthControllerIntegrationTest.java: 登录接口集成测试
   - 验证登录请求格式和响应格式的正确性
   - 演示完整的登录流程(需要数据库支持)

4. 依赖更新:
   - fund-admin/package-lock.json
   - fund-mobile/package-lock.json

技术细节:
- 解决后端要求 tenantId 必填导致的 400 错误
- 前后端一致的租户ID 默认值处理
- 端到端登录流程验证
2026-03-01 22:03:03 +08:00
zhangjf
455a20c1df 完善项目配置和测试用例
新增内容:
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(前后端一致)
- 登录验证流程优化,支持多租户
- 增加日志输出便于调试
- 代码规范性和可维护性提升
2026-03-01 19:06:42 +08:00
zhangjf
645056eaf0 添加服务启动方式说明文档
更新内容:
1. 在单机部署文档中增加 4.8 节,详细说明服务启动方式
2. 在部署运维文档中增加 1.5 节,说明开发环境和生产环境的启动方式

关键约束:
- 明确说明不能使用 java -jar 方式启动(瘦包打包导致)
- 开发环境:使用 mvn spring-boot:run 或 IDE 直接运行
- 生产环境:使用 bin/start.sh 脚本启动
- 解释技术原因:Maven Assembly Plugin 瘦包打包、Manifest 配置缺失、类加载机制

调试建议:
- 开发环境推荐使用 IDE 或 Maven 插件
- 生产环境始终使用启动脚本
- 提供日志查看、进程检查等调试方法
2026-03-01 19:05:04 +08:00
109 changed files with 3017 additions and 704 deletions

33
AGENTS.md Normal file
View 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
View 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存储在 RedisKey: `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
View 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 中清理

View File

@ -1,8 +1,9 @@
-- =============================================
-- 资金服务平台 - 客户中心数据库初始化脚本
-- Database: fund_cust
-- Version: 1.0
-- Version: 2.0
-- Created: 2026-02-17
-- Updated: 2026-03-02 (主键类型改为VARCHAR雪花ID)
-- =============================================
CREATE DATABASE IF NOT EXISTS fund_cust DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
@ -13,8 +14,8 @@ USE fund_cust;
-- 1. 客户表 (customer)
-- =============================================
CREATE TABLE IF NOT EXISTS customer (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '客户ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
id VARCHAR(32) NOT NULL COMMENT '主键ID雪花算法',
tenant_id VARCHAR(32) NOT NULL COMMENT '租户ID',
customer_code VARCHAR(64) NOT NULL COMMENT '客户编码',
customer_name VARCHAR(128) NOT NULL COMMENT '客户名称',
contact VARCHAR(64) COMMENT '联系人',
@ -23,9 +24,9 @@ CREATE TABLE IF NOT EXISTS customer (
address VARCHAR(255) COMMENT '地址',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态: 0-禁用, 1-启用',
remark VARCHAR(500) COMMENT '备注',
created_by BIGINT COMMENT '创建人',
created_by VARCHAR(32) COMMENT '创建人',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_by BIGINT COMMENT '更新人',
updated_by VARCHAR(32) COMMENT '更新人',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记: 0-未删除, 1-已删除',
PRIMARY KEY (id),
@ -39,18 +40,18 @@ CREATE TABLE IF NOT EXISTS customer (
-- 2. 联系人表 (customer_contact)
-- =============================================
CREATE TABLE IF NOT EXISTS customer_contact (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '联系人ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
customer_id BIGINT NOT NULL COMMENT '客户ID',
id VARCHAR(32) NOT NULL COMMENT '主键ID雪花算法',
tenant_id VARCHAR(32) NOT NULL COMMENT '租户ID',
customer_id VARCHAR(32) NOT NULL COMMENT '客户ID',
contact_name VARCHAR(64) NOT NULL COMMENT '联系人姓名',
phone VARCHAR(20) COMMENT '手机号',
email VARCHAR(128) COMMENT '邮箱',
position VARCHAR(64) COMMENT '职位',
is_primary TINYINT NOT NULL DEFAULT 0 COMMENT '是否主要联系人: 0-否, 1-是',
remark VARCHAR(500) COMMENT '备注',
created_by BIGINT COMMENT '创建人',
created_by VARCHAR(32) COMMENT '创建人',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_by BIGINT COMMENT '更新人',
updated_by VARCHAR(32) COMMENT '更新人',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记: 0-未删除, 1-已删除',
PRIMARY KEY (id),
@ -62,9 +63,9 @@ CREATE TABLE IF NOT EXISTS customer_contact (
-- 初始化测试数据(租户ID=1)
-- =============================================
INSERT INTO customer (id, tenant_id, customer_code, customer_name, contact, phone, status, created_by, created_time)
VALUES (1, 1, 'CUST001', '测试客户A', '张三', '13800138001', 1, 1, NOW())
VALUES ('1', '1', 'CUST001', '测试客户A', '张三', '13800138001', 1, '1', NOW())
ON DUPLICATE KEY UPDATE customer_code=customer_code;
INSERT INTO customer_contact (tenant_id, customer_id, contact_name, phone, position, is_primary, status, created_by, created_time)
VALUES (1, 1, '张三', '13800138001', '总经理', 1, 1, 1, NOW())
VALUES ('1', '1', '张三', '13800138001', '总经理', 1, 1, '1', NOW())
ON DUPLICATE KEY UPDATE contact_name=contact_name;

View File

@ -1,8 +1,9 @@
-- =============================================
-- 资金服务平台 - 支出管理数据库初始化脚本
-- Database: fund_exp
-- Version: 1.0
-- Version: 2.0
-- Created: 2026-02-22
-- Updated: 2026-03-02 (主键类型改为VARCHAR雪花ID)
-- =============================================
CREATE DATABASE IF NOT EXISTS fund_exp DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
@ -13,19 +14,19 @@ USE fund_exp;
-- 1. 支出类型表 (expense_type)
-- =============================================
CREATE TABLE IF NOT EXISTS expense_type (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '支出类型ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
id VARCHAR(32) NOT NULL COMMENT '主键ID雪花算法',
tenant_id VARCHAR(32) NOT NULL COMMENT '租户ID',
type_code VARCHAR(64) COMMENT '支出类型编码',
type_name VARCHAR(128) NOT NULL COMMENT '支出类型名称',
parent_id BIGINT NOT NULL DEFAULT 0 COMMENT '父类型ID, 0表示一级类型',
parent_id VARCHAR(32) NOT NULL DEFAULT '0' COMMENT '父类型ID, 0表示一级类型',
type_level INT NOT NULL DEFAULT 1 COMMENT '类型层级',
sort_order INT DEFAULT 0 COMMENT '排序号',
description VARCHAR(500) COMMENT '类型描述',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态: 0-禁用, 1-启用',
remark VARCHAR(500) COMMENT '备注',
created_by BIGINT COMMENT '创建人',
created_by VARCHAR(32) COMMENT '创建人',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_by BIGINT COMMENT '更新人',
updated_by VARCHAR(32) COMMENT '更新人',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记: 0-未删除, 1-已删除',
PRIMARY KEY (id),
@ -39,34 +40,34 @@ CREATE TABLE IF NOT EXISTS expense_type (
-- 2. 支出表 (fund_expense)
-- =============================================
CREATE TABLE IF NOT EXISTS fund_expense (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '支出ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
id VARCHAR(32) NOT NULL COMMENT '主键ID雪花算法',
tenant_id VARCHAR(32) NOT NULL COMMENT '租户ID',
expense_no VARCHAR(64) NOT NULL COMMENT '支出单号',
title VARCHAR(200) COMMENT '支出标题',
amount DECIMAL(18,2) NOT NULL COMMENT '支出金额',
currency VARCHAR(16) DEFAULT 'CNY' COMMENT '币种',
expense_type BIGINT COMMENT '支出类型ID',
expense_type VARCHAR(32) COMMENT '支出类型ID',
payee_name VARCHAR(128) COMMENT '收款单位',
payee_bank VARCHAR(128) COMMENT '收款银行',
payee_account VARCHAR(64) COMMENT '收款账号',
expense_date DATETIME COMMENT '支出日期',
purpose VARCHAR(500) COMMENT '用途说明',
request_id BIGINT COMMENT '关联用款申请ID',
project_id BIGINT COMMENT '所属项目ID',
customer_id BIGINT COMMENT '客户ID',
request_id VARCHAR(32) COMMENT '关联用款申请ID',
project_id VARCHAR(32) COMMENT '所属项目ID',
customer_id VARCHAR(32) COMMENT '客户ID',
pay_status INT DEFAULT 0 COMMENT '支付状态: 0-待支付, 1-已支付, 2-支付失败',
pay_time DATETIME COMMENT '支付时间',
pay_channel VARCHAR(32) COMMENT '支付渠道',
pay_voucher VARCHAR(255) COMMENT '支付凭证',
approval_status INT DEFAULT 0 COMMENT '审批状态',
approver_id BIGINT COMMENT '审批人ID',
approver_id VARCHAR(32) COMMENT '审批人ID',
approval_time DATETIME COMMENT '审批时间',
approval_comment VARCHAR(500) COMMENT '审批意见',
attachments VARCHAR(1000) COMMENT '附件URL',
remark VARCHAR(500) COMMENT '备注',
created_by BIGINT COMMENT '创建人',
created_by VARCHAR(32) COMMENT '创建人',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_by BIGINT COMMENT '更新人',
updated_by VARCHAR(32) COMMENT '更新人',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记: 0-未删除, 1-已删除',
PRIMARY KEY (id),
@ -85,28 +86,28 @@ CREATE TABLE IF NOT EXISTS fund_expense (
-- 一级支出类型
INSERT INTO expense_type (id, tenant_id, type_code, type_name, parent_id, type_level, sort_order, status, created_by, created_time)
VALUES
(1, 1, 'LABOR', '人力成本', 0, 1, 1, 1, 1, NOW()),
(2, 1, 'OFFICE', '办公费用', 0, 1, 2, 1, 1, NOW()),
(3, 1, 'TRAVEL', '差旅费用', 0, 1, 3, 1, 1, NOW()),
(4, 1, 'PURCHASE', '采购费用', 0, 1, 4, 1, 1, NOW()),
(5, 1, 'OTHER', '其他费用', 0, 1, 5, 1, 1, NOW())
('1', '1', 'LABOR', '人力成本', '0', 1, 1, 1, '1', NOW()),
('2', '1', 'OFFICE', '办公费用', '0', 1, 2, 1, '1', NOW()),
('3', '1', 'TRAVEL', '差旅费用', '0', 1, 3, 1, '1', NOW()),
('4', '1', 'PURCHASE', '采购费用', '0', 1, 4, 1, '1', NOW()),
('5', '1', 'OTHER', '其他费用', '0', 1, 5, 1, '1', NOW())
ON DUPLICATE KEY UPDATE type_code=type_code;
-- 二级支出类型
INSERT INTO expense_type (id, tenant_id, type_code, type_name, parent_id, type_level, sort_order, status, created_by, created_time)
VALUES
(11, 1, 'SALARY', '工资', 1, 2, 1, 1, 1, NOW()),
(12, 1, 'BONUS', '奖金', 1, 2, 2, 1, 1, NOW()),
(13, 1, 'SOCIAL_INSURANCE', '社保', 1, 2, 3, 1, 1, NOW()),
(21, 1, 'RENT', '房租', 2, 2, 1, 1, 1, NOW()),
(22, 1, 'UTILITIES', '水电费', 2, 2, 2, 1, 1, NOW()),
(23, 1, 'SUPPLIES', '办公用品', 2, 2, 3, 1, 1, NOW()),
(31, 1, 'TRANSPORT', '交通费', 3, 2, 1, 1, 1, NOW()),
(32, 1, 'ACCOMMODATION', '住宿费', 3, 2, 2, 1, 1, NOW()),
(33, 1, 'MEALS', '餐饮费', 3, 2, 3, 1, 1, NOW()),
(41, 1, 'EQUIPMENT', '设备采购', 4, 2, 1, 1, 1, NOW()),
(42, 1, 'SOFTWARE', '软件采购', 4, 2, 2, 1, 1, NOW()),
(43, 1, 'SERVICE', '服务采购', 4, 2, 3, 1, 1, NOW())
('11', '1', 'SALARY', '工资', '1', 2, 1, 1, '1', NOW()),
('12', '1', 'BONUS', '奖金', '1', 2, 2, 1, '1', NOW()),
('13', '1', 'SOCIAL_INSURANCE', '社保', '1', 2, 3, 1, '1', NOW()),
('21', '1', 'RENT', '房租', '2', 2, 1, 1, '1', NOW()),
('22', '1', 'UTILITIES', '水电费', '2', 2, 2, 1, '1', NOW()),
('23', '1', 'SUPPLIES', '办公用品', '2', 2, 3, 1, '1', NOW()),
('31', '1', 'TRANSPORT', '交通费', '3', 2, 1, 1, '1', NOW()),
('32', '1', 'ACCOMMODATION', '住宿费', '3', 2, 2, 1, '1', NOW()),
('33', '1', 'MEALS', '餐饮费', '3', 2, 3, 1, '1', NOW()),
('41', '1', 'EQUIPMENT', '设备采购', '4', 2, 1, 1, '1', NOW()),
('42', '1', 'SOFTWARE', '软件采购', '4', 2, 2, 1, '1', NOW()),
('43', '1', 'SERVICE', '服务采购', '4', 2, 3, 1, '1', NOW())
ON DUPLICATE KEY UPDATE type_code=type_code;
-- =============================================

View File

@ -1,8 +1,9 @@
-- =============================================
-- 资金服务平台 - 文件管理数据库初始化脚本
-- Database: fund_file
-- Version: 1.0
-- Version: 2.0
-- Created: 2026-02-22
-- Updated: 2026-03-02 (主键类型改为VARCHAR雪花ID)
-- =============================================
CREATE DATABASE IF NOT EXISTS fund_file DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
@ -13,8 +14,8 @@ USE fund_file;
-- 文件记录表 (file_record)
-- =============================================
CREATE TABLE IF NOT EXISTS `file_record` (
`file_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '文件ID',
`tenant_id` BIGINT NOT NULL COMMENT '租户ID',
`file_id` VARCHAR(32) NOT NULL COMMENT '主键ID雪花算法',
`tenant_id` VARCHAR(32) NOT NULL COMMENT '租户ID',
`file_name` VARCHAR(200) NOT NULL COMMENT '原始文件名',
`file_path` VARCHAR(500) NOT NULL COMMENT '文件存储路径',
`file_url` VARCHAR(500) COMMENT '文件访问URL',
@ -24,13 +25,13 @@ CREATE TABLE IF NOT EXISTS `file_record` (
`content_type` VARCHAR(100) COMMENT 'MIME类型',
`md5` VARCHAR(32) COMMENT '文件MD5',
`business_type` VARCHAR(50) COMMENT '业务类型(contract/receipt/expense/other)',
`business_id` BIGINT COMMENT '关联业务ID',
`business_id` VARCHAR(32) COMMENT '关联业务ID',
`description` VARCHAR(500) COMMENT '文件描述',
`download_count` INT NOT NULL DEFAULT 0 COMMENT '下载次数',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态0-禁用1-启用',
`created_by` BIGINT COMMENT '上传人ID',
`created_by` VARCHAR(32) COMMENT '上传人ID',
`created_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_by` BIGINT COMMENT '更新人ID',
`updated_by` VARCHAR(32) COMMENT '更新人ID',
`updated_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除0-未删除1-已删除',
PRIMARY KEY (`file_id`),

View File

@ -1,9 +1,9 @@
-- =============================================
-- 资金服务平台 - 项目管理数据库初始化脚本
-- Database: fund_proj
-- Version: 1.1
-- Version: 2.0
-- Created: 2026-02-17
-- Updated: 2026-02-22 (添加requirement表)
-- Updated: 2026-03-02 (主键类型改为VARCHAR雪花ID)
-- =============================================
CREATE DATABASE IF NOT EXISTS fund_proj DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
@ -14,11 +14,11 @@ USE fund_proj;
-- 1. 项目表 (project)
-- =============================================
CREATE TABLE IF NOT EXISTS project (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '项目ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
id VARCHAR(32) NOT NULL COMMENT '主键ID雪花算法',
tenant_id VARCHAR(32) NOT NULL COMMENT '租户ID',
project_code VARCHAR(64) NOT NULL COMMENT '项目编码',
project_name VARCHAR(128) NOT NULL COMMENT '项目名称',
customer_id BIGINT NOT NULL COMMENT '客户ID',
customer_id VARCHAR(32) NOT NULL COMMENT '客户ID',
project_type VARCHAR(32) NOT NULL COMMENT '项目类型',
budget_amount DECIMAL(18,2) COMMENT '预算金额',
start_date DATE COMMENT '开始日期',
@ -26,9 +26,9 @@ CREATE TABLE IF NOT EXISTS project (
project_manager VARCHAR(64) COMMENT '项目经理',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态: 0-已关闭, 1-进行中, 2-已完成',
remark VARCHAR(500) COMMENT '备注',
created_by BIGINT COMMENT '创建人',
created_by VARCHAR(32) COMMENT '创建人',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_by BIGINT COMMENT '更新人',
updated_by VARCHAR(32) COMMENT '更新人',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记: 0-未删除, 1-已删除',
PRIMARY KEY (id),
@ -43,13 +43,13 @@ CREATE TABLE IF NOT EXISTS project (
-- 2. 需求工单表 (requirement)
-- =============================================
CREATE TABLE IF NOT EXISTS requirement (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键,需求ID',
tenant_id BIGINT NOT NULL COMMENT '租户ID',
id VARCHAR(32) NOT NULL COMMENT '主键ID(雪花算法)',
tenant_id VARCHAR(32) NOT NULL COMMENT '租户ID',
requirement_code VARCHAR(50) NOT NULL COMMENT '需求编号',
requirement_name VARCHAR(200) NOT NULL COMMENT '需求名称',
description TEXT COMMENT '需求描述',
project_id BIGINT NOT NULL COMMENT '项目ID',
customer_id BIGINT NOT NULL COMMENT '客户ID',
project_id VARCHAR(32) NOT NULL COMMENT '项目ID',
customer_id VARCHAR(32) NOT NULL COMMENT '客户ID',
priority VARCHAR(20) DEFAULT 'normal' COMMENT '优先级high-高normal-中low-低',
estimated_hours DECIMAL(8,2) DEFAULT 0.00 COMMENT '预估开发工时(小时)',
actual_hours DECIMAL(8,2) DEFAULT 0.00 COMMENT '实际开发工时(小时)',
@ -64,9 +64,9 @@ CREATE TABLE IF NOT EXISTS requirement (
progress INT DEFAULT 0 COMMENT '开发进度0-100',
remark VARCHAR(500) COMMENT '备注',
attachment_url VARCHAR(500) COMMENT '附件URL',
created_by BIGINT COMMENT '创建人ID',
created_by VARCHAR(32) COMMENT '创建人ID',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_by BIGINT COMMENT '更新人ID',
updated_by VARCHAR(32) COMMENT '更新人ID',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT DEFAULT 0 COMMENT '逻辑删除0-未删除1-已删除',
PRIMARY KEY (id),
@ -81,7 +81,7 @@ CREATE TABLE IF NOT EXISTS requirement (
-- 初始化测试数据(租户ID=1)
-- =============================================
INSERT INTO project (id, tenant_id, project_code, project_name, customer_id, project_type, budget_amount, start_date, status, created_by, created_time)
VALUES (1, 1, 'PROJ001', '测试项目A', 1, '开发项目', 1000000.00, '2026-01-01', 1, 1, NOW())
VALUES ('1', '1', 'PROJ001', '测试项目A', '1', '开发项目', 1000000.00, '2026-01-01', 1, '1', NOW())
ON DUPLICATE KEY UPDATE project_code=project_code;
-- =============================================

View File

@ -1,8 +1,9 @@
-- =============================================
-- 资金服务平台 - 收款管理数据库初始化脚本
-- Database: fund_receipt
-- Version: 1.0
-- Version: 2.0
-- Created: 2026-02-22
-- Updated: 2026-03-02 (主键类型改为VARCHAR雪花ID)
-- =============================================
CREATE DATABASE IF NOT EXISTS fund_receipt DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
@ -13,12 +14,12 @@ USE fund_receipt;
-- 1. 应收款表 (receivable)
-- =============================================
CREATE TABLE IF NOT EXISTS receivable (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '应收款ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
id VARCHAR(32) NOT NULL COMMENT '主键ID雪花算法',
tenant_id VARCHAR(32) NOT NULL COMMENT '租户ID',
receivable_code VARCHAR(64) NOT NULL COMMENT '应收款编号',
requirement_id BIGINT COMMENT '需求ID',
project_id BIGINT NOT NULL COMMENT '项目ID',
customer_id BIGINT NOT NULL COMMENT '客户ID',
requirement_id VARCHAR(32) COMMENT '需求ID',
project_id VARCHAR(32) NOT NULL COMMENT '项目ID',
customer_id VARCHAR(32) NOT NULL COMMENT '客户ID',
receivable_amount DECIMAL(18,2) NOT NULL COMMENT '应收款金额',
received_amount DECIMAL(18,2) DEFAULT 0.00 COMMENT '已收款金额',
unpaid_amount DECIMAL(18,2) DEFAULT 0.00 COMMENT '未收款金额',
@ -30,11 +31,11 @@ CREATE TABLE IF NOT EXISTS receivable (
overdue_days INT DEFAULT 0 COMMENT '逾期天数',
confirm_status INT DEFAULT 0 COMMENT '确认状态: 0-待确认, 1-已确认',
confirm_time DATETIME COMMENT '确认时间',
confirm_by BIGINT COMMENT '确认人ID',
confirm_by VARCHAR(32) COMMENT '确认人ID',
remark VARCHAR(500) COMMENT '备注',
created_by BIGINT COMMENT '创建人',
created_by VARCHAR(32) COMMENT '创建人',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_by BIGINT COMMENT '更新人',
updated_by VARCHAR(32) COMMENT '更新人',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记: 0-未删除, 1-已删除',
PRIMARY KEY (id),
@ -51,8 +52,8 @@ CREATE TABLE IF NOT EXISTS receivable (
-- 2. 收款记录表 (fund_receipt)
-- =============================================
CREATE TABLE IF NOT EXISTS fund_receipt (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '收款记录ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
id VARCHAR(32) NOT NULL COMMENT '主键ID雪花算法',
tenant_id VARCHAR(32) NOT NULL COMMENT '租户ID',
receipt_no VARCHAR(64) NOT NULL COMMENT '收款单号',
title VARCHAR(200) COMMENT '收款标题',
amount DECIMAL(18,2) NOT NULL COMMENT '收款金额',
@ -63,21 +64,21 @@ CREATE TABLE IF NOT EXISTS fund_receipt (
payer_account VARCHAR(64) COMMENT '付款账号',
receipt_date DATETIME COMMENT '收款日期',
purpose VARCHAR(500) COMMENT '用途说明',
project_id BIGINT COMMENT '项目ID',
customer_id BIGINT COMMENT '客户ID',
receivable_id BIGINT COMMENT '应收款ID',
project_id VARCHAR(32) COMMENT '项目ID',
customer_id VARCHAR(32) COMMENT '客户ID',
receivable_id VARCHAR(32) COMMENT '应收款ID',
receipt_status INT DEFAULT 0 COMMENT '收款状态: 0-待确认, 1-已确认, 2-已核销',
confirm_time DATETIME COMMENT '确认时间',
confirm_by BIGINT COMMENT '确认人ID',
confirm_by VARCHAR(32) COMMENT '确认人ID',
write_off_time DATETIME COMMENT '核销时间',
write_off_by BIGINT COMMENT '核销人ID',
write_off_by VARCHAR(32) COMMENT '核销人ID',
voucher VARCHAR(255) COMMENT '收款凭证',
invoice_no VARCHAR(64) COMMENT '发票号',
attachments VARCHAR(1000) COMMENT '附件URL',
remark VARCHAR(500) COMMENT '备注',
created_by BIGINT COMMENT '创建人',
created_by VARCHAR(32) COMMENT '创建人',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_by BIGINT COMMENT '更新人',
updated_by VARCHAR(32) COMMENT '更新人',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记: 0-未删除, 1-已删除',
PRIMARY KEY (id),
@ -93,7 +94,7 @@ CREATE TABLE IF NOT EXISTS fund_receipt (
-- 初始化测试数据(租户ID=1)
-- =============================================
INSERT INTO receivable (id, tenant_id, receivable_code, project_id, customer_id, receivable_amount, received_amount, unpaid_amount, receivable_date, payment_due_date, status, created_by, created_time)
VALUES (1, 1, 'REC20260101001', 1, 1, 50000.00, 0.00, 50000.00, '2026-01-15', '2026-02-15', 'pending', 1, NOW())
VALUES ('1', '1', 'REC20260101001', '1', '1', 50000.00, 0.00, 50000.00, '2026-01-15', '2026-02-15', 'pending', '1', NOW())
ON DUPLICATE KEY UPDATE receivable_code=receivable_code;
-- =============================================

View File

@ -1,8 +1,9 @@
-- =============================================
-- 资金服务平台 - 用款申请数据库初始化脚本
-- Database: fund_req
-- Version: 1.0
-- Version: 2.0
-- Created: 2026-02-17
-- Updated: 2026-03-02 (主键类型改为VARCHAR雪花ID)
-- =============================================
CREATE DATABASE IF NOT EXISTS fund_req DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
@ -13,8 +14,8 @@ USE fund_req;
-- 用款申请表 (fund_request)
-- =============================================
CREATE TABLE IF NOT EXISTS fund_request (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '申请ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
id VARCHAR(32) NOT NULL COMMENT '主键ID雪花算法',
tenant_id VARCHAR(32) NOT NULL COMMENT '租户ID',
request_no VARCHAR(64) NOT NULL COMMENT '申请单号',
title VARCHAR(200) COMMENT '申请标题',
amount DECIMAL(18,2) NOT NULL COMMENT '申请金额',
@ -24,20 +25,20 @@ CREATE TABLE IF NOT EXISTS fund_request (
payee_bank VARCHAR(128) COMMENT '收款银行',
payee_account VARCHAR(64) COMMENT '收款账号',
purpose VARCHAR(500) COMMENT '用途说明',
project_id BIGINT COMMENT '项目ID',
customer_id BIGINT COMMENT '客户ID',
project_id VARCHAR(32) COMMENT '项目ID',
customer_id VARCHAR(32) COMMENT '客户ID',
request_date DATETIME COMMENT '申请日期',
expected_pay_date DATETIME COMMENT '期望付款日期',
approval_status INT DEFAULT 0 COMMENT '审批状态: 0-待审批, 1-审批中, 2-审批通过, 3-审批拒绝, 4-已撤回',
current_node INT COMMENT '当前审批节点',
approver_id BIGINT COMMENT '审批人ID',
approver_id VARCHAR(32) COMMENT '审批人ID',
approval_time DATETIME COMMENT '审批时间',
approval_comment VARCHAR(500) COMMENT '审批意见',
attachments VARCHAR(1000) COMMENT '附件URL',
remark VARCHAR(500) COMMENT '备注',
created_by BIGINT COMMENT '创建人',
created_by VARCHAR(32) COMMENT '创建人',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_by BIGINT COMMENT '更新人',
updated_by VARCHAR(32) COMMENT '更新人',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记: 0-未删除, 1-已删除',
PRIMARY KEY (id),
@ -49,7 +50,7 @@ CREATE TABLE IF NOT EXISTS fund_request (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用款申请表';
INSERT INTO fund_request (id, tenant_id, request_no, title, amount, request_type, purpose, project_id, approval_status, created_by, created_time)
VALUES (1, 1, 'REQ20260101001', '测试用款申请', 50000.00, 2, '测试用款申请', 1, 0, 1, NOW())
VALUES ('1', '1', 'REQ20260101001', '测试用款申请', 50000.00, 2, '测试用款申请', '1', 0, '1', NOW())
ON DUPLICATE KEY UPDATE request_no=request_no;
-- =============================================

View File

@ -1,9 +1,10 @@
-- =============================================
-- 资金服务平台 - 系统服务数据库初始化脚本
-- Database: fund_sys
-- Version: 1.0
-- Version: 2.0
-- Author: fundplatform team
-- Created: 2026-02-17
-- Updated: 2026-03-02 (主键类型改为VARCHAR雪花ID)
-- =============================================
-- 创建数据库
@ -15,20 +16,20 @@ USE fund_sys;
-- 1. 用户表 (sys_user)
-- =============================================
CREATE TABLE IF NOT EXISTS sys_user (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
id VARCHAR(32) NOT NULL COMMENT '主键ID雪花算法',
tenant_id VARCHAR(32) NOT NULL COMMENT '租户ID',
username VARCHAR(64) NOT NULL COMMENT '用户名',
password VARCHAR(128) NOT NULL COMMENT '密码 (MD5)',
real_name VARCHAR(64) COMMENT '真实姓名',
phone VARCHAR(20) COMMENT '手机号',
email VARCHAR(128) COMMENT '邮箱',
dept_id BIGINT COMMENT '部门ID',
dept_id VARCHAR(32) COMMENT '部门ID',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态: 0-禁用, 1-启用',
avatar VARCHAR(255) COMMENT '头像URL',
remark VARCHAR(500) COMMENT '备注',
created_by BIGINT COMMENT '创建人',
created_by VARCHAR(32) COMMENT '创建人',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_by BIGINT COMMENT '更新人',
updated_by VARCHAR(32) COMMENT '更新人',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记: 0-未删除, 1-已删除',
PRIMARY KEY (id),
@ -42,17 +43,17 @@ CREATE TABLE IF NOT EXISTS sys_user (
-- 2. 角色表 (sys_role)
-- =============================================
CREATE TABLE IF NOT EXISTS sys_role (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '角色ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
id VARCHAR(32) NOT NULL COMMENT '主键ID雪花算法',
tenant_id VARCHAR(32) NOT NULL COMMENT '租户ID',
role_code VARCHAR(64) NOT NULL COMMENT '角色编码',
role_name VARCHAR(128) NOT NULL COMMENT '角色名称',
data_scope TINYINT NOT NULL DEFAULT 1 COMMENT '数据权限: 1-全部, 2-本部门及子部门, 3-仅本部门, 4-仅本人',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态: 0-禁用, 1-启用',
sort_order INT DEFAULT 0 COMMENT '排序号',
remark VARCHAR(500) COMMENT '备注',
created_by BIGINT COMMENT '创建人',
created_by VARCHAR(32) COMMENT '创建人',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_by BIGINT COMMENT '更新人',
updated_by VARCHAR(32) COMMENT '更新人',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记: 0-未删除, 1-已删除',
PRIMARY KEY (id),
@ -65,11 +66,11 @@ CREATE TABLE IF NOT EXISTS sys_role (
-- 3. 用户角色关联表 (sys_user_role)
-- =============================================
CREATE TABLE IF NOT EXISTS sys_user_role (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
user_id BIGINT NOT NULL COMMENT '用户ID',
role_id BIGINT NOT NULL COMMENT '角色ID',
created_by BIGINT COMMENT '创建人',
id VARCHAR(32) NOT NULL COMMENT '主键ID(雪花算法)',
tenant_id VARCHAR(32) NOT NULL COMMENT '租户ID',
user_id VARCHAR(32) NOT NULL COMMENT '用户ID',
role_id VARCHAR(32) NOT NULL COMMENT '角色ID',
created_by VARCHAR(32) COMMENT '创建人',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (id),
UNIQUE KEY uk_tenant_user_role (tenant_id, user_id, role_id),
@ -81,9 +82,9 @@ CREATE TABLE IF NOT EXISTS sys_user_role (
-- 4. 菜单表 (sys_menu)
-- =============================================
CREATE TABLE IF NOT EXISTS sys_menu (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
parent_id BIGINT NOT NULL DEFAULT 0 COMMENT '父菜单ID, 0表示根菜单',
id VARCHAR(32) NOT NULL COMMENT '主键ID雪花算法',
tenant_id VARCHAR(32) NOT NULL COMMENT '租户ID',
parent_id VARCHAR(32) NOT NULL DEFAULT '0' COMMENT '父菜单ID, 0表示根菜单',
menu_name VARCHAR(128) NOT NULL COMMENT '菜单名称',
menu_type TINYINT NOT NULL DEFAULT 1 COMMENT '菜单类型: 1-目录, 2-菜单, 3-按钮',
menu_path VARCHAR(255) COMMENT '路由路径',
@ -94,9 +95,9 @@ CREATE TABLE IF NOT EXISTS sys_menu (
visible TINYINT NOT NULL DEFAULT 1 COMMENT '是否可见: 0-隐藏, 1-显示',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态: 0-禁用, 1-启用',
remark VARCHAR(500) COMMENT '备注',
created_by BIGINT COMMENT '创建人',
created_by VARCHAR(32) COMMENT '创建人',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_by BIGINT COMMENT '更新人',
updated_by VARCHAR(32) COMMENT '更新人',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记: 0-未删除, 1-已删除',
PRIMARY KEY (id),
@ -109,11 +110,11 @@ CREATE TABLE IF NOT EXISTS sys_menu (
-- 5. 角色菜单关联表 (sys_role_menu)
-- =============================================
CREATE TABLE IF NOT EXISTS sys_role_menu (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
role_id BIGINT NOT NULL COMMENT '角色ID',
menu_id BIGINT NOT NULL COMMENT '菜单ID',
created_by BIGINT COMMENT '创建人',
id VARCHAR(32) NOT NULL COMMENT '主键ID(雪花算法)',
tenant_id VARCHAR(32) NOT NULL COMMENT '租户ID',
role_id VARCHAR(32) NOT NULL COMMENT '角色ID',
menu_id VARCHAR(32) NOT NULL COMMENT '菜单ID',
created_by VARCHAR(32) COMMENT '创建人',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (id),
UNIQUE KEY uk_tenant_role_menu (tenant_id, role_id, menu_id),
@ -125,9 +126,9 @@ CREATE TABLE IF NOT EXISTS sys_role_menu (
-- 6. 部门表 (sys_dept)
-- =============================================
CREATE TABLE IF NOT EXISTS sys_dept (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '部门ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
parent_id BIGINT NOT NULL DEFAULT 0 COMMENT '父部门ID, 0表示根部门',
id VARCHAR(32) NOT NULL COMMENT '主键ID雪花算法',
tenant_id VARCHAR(32) NOT NULL COMMENT '租户ID',
parent_id VARCHAR(32) NOT NULL DEFAULT '0' COMMENT '父部门ID, 0表示根部门',
dept_code VARCHAR(64) NOT NULL COMMENT '部门编码',
dept_name VARCHAR(128) NOT NULL COMMENT '部门名称',
dept_leader VARCHAR(64) COMMENT '部门负责人',
@ -136,9 +137,9 @@ CREATE TABLE IF NOT EXISTS sys_dept (
sort_order INT DEFAULT 0 COMMENT '排序号',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态: 0-禁用, 1-启用',
remark VARCHAR(500) COMMENT '备注',
created_by BIGINT COMMENT '创建人',
created_by VARCHAR(32) COMMENT '创建人',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_by BIGINT COMMENT '更新人',
updated_by VARCHAR(32) COMMENT '更新人',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记: 0-未删除, 1-已删除',
PRIMARY KEY (id),
@ -152,17 +153,17 @@ CREATE TABLE IF NOT EXISTS sys_dept (
-- 7. 数据字典表 (sys_dict)
-- =============================================
CREATE TABLE IF NOT EXISTS sys_dict (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '字典ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
id VARCHAR(32) NOT NULL COMMENT '主键ID雪花算法',
tenant_id VARCHAR(32) NOT NULL COMMENT '租户ID',
dict_type VARCHAR(64) NOT NULL COMMENT '字典类型',
dict_label VARCHAR(128) NOT NULL COMMENT '字典标签',
dict_value VARCHAR(128) NOT NULL COMMENT '字典值',
sort_order INT DEFAULT 0 COMMENT '排序号',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态: 0-禁用, 1-启用',
remark VARCHAR(500) COMMENT '备注',
created_by BIGINT COMMENT '创建人',
created_by VARCHAR(32) COMMENT '创建人',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_by BIGINT COMMENT '更新人',
updated_by VARCHAR(32) COMMENT '更新人',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记: 0-未删除, 1-已删除',
PRIMARY KEY (id),
@ -176,8 +177,8 @@ CREATE TABLE IF NOT EXISTS sys_dict (
-- 8. 系统配置表 (sys_config)
-- =============================================
CREATE TABLE IF NOT EXISTS sys_config (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '配置ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
id VARCHAR(32) NOT NULL COMMENT '主键ID雪花算法',
tenant_id VARCHAR(32) NOT NULL COMMENT '租户ID',
config_key VARCHAR(128) NOT NULL COMMENT '配置键',
config_value TEXT COMMENT '配置值',
config_type VARCHAR(64) DEFAULT 'string' COMMENT '配置类型: string/number/boolean/json',
@ -188,9 +189,9 @@ CREATE TABLE IF NOT EXISTS sys_config (
group_name VARCHAR(128) COMMENT '分组名称',
sort_order INT DEFAULT 0 COMMENT '排序号',
remark VARCHAR(500) COMMENT '备注',
created_by BIGINT COMMENT '创建人',
created_by VARCHAR(32) COMMENT '创建人',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_by BIGINT COMMENT '更新人',
updated_by VARCHAR(32) COMMENT '更新人',
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记: 0-未删除, 1-已删除',
PRIMARY KEY (id),
@ -203,9 +204,9 @@ CREATE TABLE IF NOT EXISTS sys_config (
-- 9. 操作日志表 (sys_log)
-- =============================================
CREATE TABLE IF NOT EXISTS sys_log (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '日志ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
user_id BIGINT COMMENT '操作用户ID',
id VARCHAR(32) NOT NULL COMMENT '主键ID雪花算法',
tenant_id VARCHAR(32) NOT NULL COMMENT '租户ID',
user_id VARCHAR(32) COMMENT '操作用户ID',
username VARCHAR(64) COMMENT '操作用户名',
operation VARCHAR(128) COMMENT '操作描述',
method VARCHAR(255) COMMENT '请求方法',
@ -225,12 +226,10 @@ CREATE TABLE IF NOT EXISTS sys_log (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统操作日志表';
-- =============================================
-- 初始化数据
-- 10. 租户表 (sys_tenant)
-- =============================================
-- 创建租户表 (sys_tenant)
CREATE TABLE IF NOT EXISTS sys_tenant (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '租户ID',
id VARCHAR(32) NOT NULL COMMENT '租户ID雪花算法',
tenant_code VARCHAR(50) NOT NULL COMMENT '租户编码',
tenant_name VARCHAR(100) NOT NULL COMMENT '租户名称',
contact VARCHAR(50) COMMENT '联系人',
@ -242,37 +241,41 @@ CREATE TABLE IF NOT EXISTS sys_tenant (
max_users INT NOT NULL DEFAULT 10 COMMENT '最大用户数',
remark VARCHAR(500) COMMENT '备注',
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '删除标记: 0-未删除, 1-已删除',
created_by BIGINT COMMENT '创建人',
created_by VARCHAR(32) COMMENT '创建人',
created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_by BIGINT COMMENT '更新人',
updated_by VARCHAR(32) COMMENT '更新人',
updated_time DATETIME ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_tenant_code (tenant_code, deleted)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='租户表';
-- =============================================
-- 初始化数据 (使用雪花ID)
-- =============================================
-- 插入默认租户
INSERT INTO sys_tenant (id, tenant_code, tenant_name, contact, phone, status, max_users, remark, created_time)
VALUES (1, 'DEFAULT', '默认租户', '管理员', '13800138000', 1, 100, '系统默认租户', NOW())
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;
-- 插入超级管理员角色
INSERT INTO sys_role (id, tenant_id, role_code, role_name, data_scope, status, created_by, created_time)
VALUES (1, 1, 'admin', '超级管理员', 1, 1, 1, NOW())
VALUES ('1', '1', 'admin', '超级管理员', 1, 1, '1', NOW())
ON DUPLICATE KEY UPDATE role_code=role_code;
-- 关联超级管理员用户和角色
INSERT INTO sys_user_role (tenant_id, user_id, role_id, created_by, created_time)
VALUES (1, 1, 1, 1, NOW())
INSERT INTO sys_user_role (id, tenant_id, user_id, role_id, created_by, created_time)
VALUES ('1', '1', '1', '1', '1', NOW())
ON DUPLICATE KEY UPDATE user_id=user_id;
-- 插入根部门
INSERT INTO sys_dept (id, tenant_id, parent_id, dept_code, dept_name, status, created_by, created_time)
VALUES (1, 1, 0, 'ROOT', '根部门', 1, 1, NOW())
VALUES ('1', '1', '0', 'ROOT', '根部门', 1, '1', NOW())
ON DUPLICATE KEY UPDATE dept_code=dept_code;
-- =============================================

View File

@ -622,6 +622,69 @@ spring:
/opt/fundplatform/deploy/status.sh
```
### 4.8 服务启动方式说明(重要)
**⚠️ 重要提示:** 为了打包发布的需要,每个服务 JAR 都采用瘦包方式打包(依赖分离),因此**不能**使用 `java -jar fund*.jar` 方式执行。
#### 4.8.1 正确的启动方式
**方式一:使用启动脚本(推荐)**
```bash
# 使用服务自带的启动脚本
cd /opt/fundplatform/deploy/fund-sys
./bin/start.sh
# 或使用一键启动脚本
cd /opt/fundplatform/deploy
./start-all.sh
```
**方式二:使用 Maven Spring Boot 插件**
```bash
# 开发环境下,可以使用 Maven 插件启动
cd fund-sys
mvn spring-boot:run
```
**方式三:手动指定类路径(不推荐)**
```bash
# 如果必须手动启动,需要指定完整的类路径
cd /opt/fundplatform/deploy/fund-sys
java -cp "lib/*:conf/" com.fundplatform.sys.SysApplication
```
#### 4.8.2 错误的启动方式
**❌ 错误示例:**
```bash
# 以下方式是错误的,会导致 ClassNotFoundException
cd /opt/fundplatform/deploy/fund-sys/lib
java -jar fund-sys.jar
# 或在开发环境
cd fund-sys/target
java -jar fund-sys-0.0.1-SNAPSHOT.jar
```
#### 4.8.3 为什么不能使用 java -jar
1. **瘦包打包方式**:项目使用 Maven Assembly Plugin 将依赖 JAR 分离到 `lib/` 目录
2. **Manifest 配置**:主 JAR 包的 MANIFEST.MF 中没有 Class-Path 属性
3. **类加载机制**JVM 无法自动找到 `lib/` 目录下的依赖
#### 4.8.4 调试建议
**开发环境调试:**
- 使用 IDE 直接运行 Application 类
- 或使用 `mvn spring-boot:run` 命令
- 避免使用 `java -jar` 命令
**生产环境调试:**
- 始终使用 `bin/start.sh` 脚本启动
- 查看日志:`tail -f /datacfs/applogs/fund-sys/info.log`
- 检查进程:`ps aux | grep fund-sys`
- 查看端口:`netstat -tlnp | grep 8100`
## 五、部署操作指南
### 5.1 首次部署

View File

@ -161,10 +161,77 @@ npm run dev
# 移动端H5http://localhost:8080
# 网关地址http://localhost:8000
# Nacos 控制台http://localhost:8048/nacos
# Grafana 监控http://localhost:3000 (Docker环境) 或 http://localhost:3001 (本地开发)
# Grafana 监控http://localhost:3000 (Docker 环境) 或 http://localhost:3001 (本地开发)
# Prometheushttp://localhost:9090
```
### 1.5 服务启动方式说明(重要)
**⚠️ 重要提示:** 为了打包发布的需要,每个服务 JAR 都采用瘦包方式打包(依赖分离),因此**不能**使用 `java -jar` 方式执行。
#### 1.5.1 开发环境启动方式
**✅ 正确的方式:**
1. **使用 Maven 插件(推荐)**
```bash
cd fund-sys
mvn spring-boot:run
```
2. **使用 IDE 直接运行**
- 找到对应的 Application 类(如 `SysApplication.java`
- 右键 -> Run 运行
**❌ 错误的方式:**
```bash
# 以下方式会导致 ClassNotFoundException
cd fund-sys/target
java -jar fund-sys-0.0.1-SNAPSHOT.jar
```
#### 1.5.2 生产环境启动方式
**✅ 正确的方式:**
1. **使用启动脚本(推荐)**
```bash
cd /opt/fundplatform/deploy/fund-sys
./bin/start.sh
```
2. **手动指定类路径**
```bash
cd /opt/fundplatform/deploy/fund-sys
java -cp "lib/*:conf/" com.fundplatform.sys.SysApplication
```
**❌ 错误的方式:**
```bash
# 以下方式会导致 ClassNotFoundException
cd /opt/fundplatform/deploy/fund-sys/lib
java -jar fund-sys.jar
```
#### 1.5.3 为什么不能使用 java -jar
1. **瘦包打包方式**:项目使用 Maven Assembly Plugin 将依赖 JAR 分离到 `lib/` 目录
2. **Manifest 配置**:主 JAR 包的 MANIFEST.MF 中没有 Class-Path 属性
3. **类加载机制**JVM 无法自动找到 `lib/` 目录下的依赖
#### 1.5.4 调试建议
**开发环境:**
- ✅ 使用 IDE 直接运行 Application 类
- ✅ 使用 `mvn spring-boot:run` 命令
- ❌ 避免使用 `java -jar` 命令
**生产环境:**
- ✅ 始终使用 `bin/start.sh` 脚本启动
- ✅ 查看日志:`tail -f /datacfs/applogs/fund-sys/info.log`
- ✅ 检查进程:`ps aux | grep fund-sys`
- ✅ 查看端口:`netstat -tlnp | grep 8100`
---
## 二、Docker Compose 部署

View File

@ -12,6 +12,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"
@ -2484,6 +2485,12 @@
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/js-md5": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.8.3.tgz",
"integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==",
"license": "MIT"
},
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",

View File

@ -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"

View File

@ -1,8 +1,13 @@
import { request } from './request'
// 登录
export function login(data: { username: string; password: string }) {
return request.post('/auth/login', data)
export function login(data: { username: string; password: string, tenantId?: number }) {
// 如果没有传递 tenantId使用默认值 1
const requestData = {
...data,
tenantId: data.tenantId || 1
}
return request.post('/auth/login', requestData)
}
// 登出

View File

@ -24,7 +24,7 @@
</div>
<el-table :data="tableData" v-loading="loading" border stripe>
<el-table-column prop="typeId" label="类型ID" width="80" />
<el-table-column prop="id" label="类型 ID" width="80" />
<el-table-column prop="typeCode" label="类型编码" width="140" />
<el-table-column prop="typeName" label="类型名称" min-width="150" />
<el-table-column prop="parentName" label="上级类型" width="140" />
@ -40,7 +40,7 @@
</template>
</el-table-column>
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column prop="createdTime" label="创建时间" width="160" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button link type="primary" :icon="Edit" @click="handleEdit(row)">编辑</el-button>
@ -81,7 +81,7 @@
<el-tree-select
v-model="form.parentId"
:data="typeTreeData"
:props="{ label: 'typeName', value: 'typeId', children: 'children' }"
:props="{ label: 'typeName', value: 'id', children: 'children' }"
placeholder="请选择上级类型(不选则为顶级类型)"
check-strictly
clearable
@ -137,7 +137,7 @@ const dialogTitle = ref('新增类型')
const formRef = ref<FormInstance>()
const form = reactive({
typeId: null as number | null,
id: null as number | null,
typeCode: '',
typeName: '',
parentId: null as number | null,
@ -154,11 +154,40 @@ const rules = reactive<FormRules>({
const fetchData = async () => {
loading.value = true
try {
const res: any = await getExpenseTypeList(queryParams)
tableData.value = res.data?.records || []
// ENABLED/DISABLED 1/0
const apiParams: any = {
pageNum: queryParams.pageNum,
pageSize: queryParams.pageSize,
typeName: queryParams.typeName
}
// ENABLED -> 1, DISABLED -> 0
if (queryParams.status === 'ENABLED') {
apiParams.status = 1
} else if (queryParams.status === 'DISABLED') {
apiParams.status = 0
}
console.log('请求参数:', apiParams)
const res: any = await getExpenseTypeList(apiParams)
console.log('API 响应:', res)
console.log('res.data:', res.data)
// records list
const rawData = res.data?.records || res.data?.list || []
console.log('原始数据:', rawData)
tableData.value = rawData.map((item: any) => ({
...item,
status: item.status === 1 ? 'ENABLED' : 'DISABLED'
}))
console.log('处理后的表格数据:', tableData.value)
total.value = res.data?.total || 0
console.log('总数:', total.value)
} catch (e) {
console.error(e)
console.error('获取数据失败:', e)
ElMessage.error('获取数据失败')
} finally {
loading.value = false
}
@ -199,11 +228,15 @@ const handleEdit = (row: any) => {
const handleStatusChange = async (row: any) => {
try {
await updateExpenseType(row.typeId, { status: row.status })
//
const statusValue = row.status === 'ENABLED' ? 1 : 0
await updateExpenseType(row.id, { status: statusValue })
ElMessage.success('状态更新成功')
} catch (e) {
console.error(e)
//
row.status = row.status === 'ENABLED' ? 'DISABLED' : 'ENABLED'
ElMessage.error('状态更新失败')
}
}
@ -214,7 +247,7 @@ const handleDelete = (row: any) => {
type: 'warning'
}).then(async () => {
try {
await deleteExpenseType(row.typeId)
await deleteExpenseType(row.id)
ElMessage.success('删除成功')
fetchData()
fetchTypeTree()
@ -231,18 +264,32 @@ const handleSubmit = async () => {
submitLoading.value = true
try {
if (form.typeId) {
await updateExpenseType(form.typeId, form)
//
const submitData: any = { ...form }
submitData.status = form.status === 'ENABLED' ? 1 : 0
// parentId null 0
if (submitData.parentId === null || submitData.parentId === undefined) {
submitData.parentId = 0
}
console.log('提交数据:', submitData)
if (form.id) {
console.log('更新类型 ID:', form.id)
await updateExpenseType(form.id, submitData)
ElMessage.success('更新成功')
} else {
await createExpenseType(form)
console.log('创建新类型')
await createExpenseType(submitData)
ElMessage.success('创建成功')
}
dialogVisible.value = false
fetchData()
fetchTypeTree()
} catch (e) {
console.error(e)
console.error('提交失败:', e)
ElMessage.error(e instanceof Error ? e.message : '操作失败')
} finally {
submitLoading.value = false
}
@ -250,7 +297,7 @@ const handleSubmit = async () => {
}
const resetForm = () => {
form.typeId = null
form.id = null
form.typeCode = ''
form.typeName = ''
form.parentId = null

View File

@ -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
@ -95,9 +78,8 @@ const handleLogin = async () => {
await formRef.value.validate()
loading.value = true
// MD5
// MD5 使
const encryptedPassword = md5(form.password)
console.log('登录 - 原始密码:', form.password, 'MD5 加密后:', encryptedPassword)
await userStore.loginAction(form.username, encryptedPassword)

View File

@ -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) {

View File

@ -78,6 +78,13 @@
<scope>provided</scope>
</dependency>
<!-- Hutool (雪花ID生成器) -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.8.25</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@ -17,7 +17,7 @@ public class TokenInfo implements Serializable {
/**
* 用户ID
*/
private Long userId;
private String userId;
/**
* 用户名
@ -27,7 +27,7 @@ public class TokenInfo implements Serializable {
/**
* 租户ID
*/
private Long tenantId;
private String tenantId;
/**
* 登录时间戳
@ -42,7 +42,7 @@ public class TokenInfo implements Serializable {
public TokenInfo() {
}
public TokenInfo(Long userId, String username, Long tenantId, Long expireTime) {
public TokenInfo(String userId, String username, String tenantId, Long expireTime) {
this.userId = userId;
this.username = username;
this.tenantId = tenantId;
@ -50,11 +50,11 @@ public class TokenInfo implements Serializable {
this.expireTime = expireTime;
}
public Long getUserId() {
public String getUserId() {
return userId;
}
public void setUserId(Long userId) {
public void setUserId(String userId) {
this.userId = userId;
}
@ -66,11 +66,11 @@ public class TokenInfo implements Serializable {
this.username = username;
}
public Long getTenantId() {
public String getTenantId() {
return tenantId;
}
public void setTenantId(Long tenantId) {
public void setTenantId(String tenantId) {
this.tenantId = tenantId;
}

View File

@ -42,7 +42,7 @@ public class TokenService {
* @param tenantId 租户ID
* @return Token字符串
*/
public String generateToken(Long userId, String username, Long tenantId) {
public String generateToken(String userId, String username, String tenantId) {
return generateToken(userId, username, tenantId, DEFAULT_EXPIRE_SECONDS);
}
@ -55,7 +55,7 @@ public class TokenService {
* @param expireSeconds 过期时间
* @return Token字符串
*/
public String generateToken(Long userId, String username, Long tenantId, long expireSeconds) {
public String generateToken(String userId, String username, String tenantId, long expireSeconds) {
// 生成UUID作为Token
String token = UUID.randomUUID().toString().replace("-", "");
@ -173,7 +173,7 @@ public class TokenService {
*
* @param userId 用户ID
*/
public void deleteAllUserTokens(Long userId) {
public void deleteAllUserTokens(String userId) {
String userTokensKey = getUserTokensKey(userId);
java.util.Map<Object, Object> tokens = redisService.hGetAll(userTokensKey);
@ -225,7 +225,7 @@ public class TokenService {
/**
* 构建用户Token列表Key
*/
private String getUserTokensKey(Long userId) {
private String getUserTokensKey(String userId) {
return USER_TOKENS_PREFIX + userId;
}
}

View File

@ -5,17 +5,17 @@ package com.fundplatform.common.context;
*/
public final class UserContextHolder {
private static final ThreadLocal<Long> USER_ID_HOLDER = new ThreadLocal<>();
private static final ThreadLocal<String> USER_ID_HOLDER = new ThreadLocal<>();
private static final ThreadLocal<String> USER_NAME_HOLDER = new ThreadLocal<>();
private UserContextHolder() {
}
public static void setUserId(Long userId) {
public static void setUserId(String userId) {
USER_ID_HOLDER.set(userId);
}
public static Long getUserId() {
public static String getUserId() {
return USER_ID_HOLDER.get();
}

View File

@ -8,25 +8,27 @@ import java.time.LocalDateTime;
*
* <p>注意此类不绑定具体 ORM 框架注解 JPAMyBatis-Plus
* 仅作为字段规范的统一来源具体映射由各模块自行扩展</p>
*
* <p>主键采用字符串类型雪花算法生成解决前端JavaScript大数精度丢失问题</p>
*/
public abstract class BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
/** 主键ID */
private Long id;
/** 主键ID雪花算法生成的19位字符串 */
private String id;
/** 租户ID多租户隔离 */
private Long tenantId;
private String tenantId;
/** 创建人 */
private Long createdBy;
private String createdBy;
/** 创建时间 */
private LocalDateTime createdTime;
/** 更新人 */
private Long updatedBy;
private String updatedBy;
/** 更新时间 */
private LocalDateTime updatedTime;
@ -37,27 +39,27 @@ public abstract class BaseEntity implements Serializable {
/** 备注 */
private String remark;
public Long getId() {
public String getId() {
return id;
}
public void setId(Long id) {
public void setId(String id) {
this.id = id;
}
public Long getTenantId() {
public String getTenantId() {
return tenantId;
}
public void setTenantId(Long tenantId) {
public void setTenantId(String tenantId) {
this.tenantId = tenantId;
}
public Long getCreatedBy() {
public String getCreatedBy() {
return createdBy;
}
public void setCreatedBy(Long createdBy) {
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
@ -69,11 +71,11 @@ public abstract class BaseEntity implements Serializable {
this.createdTime = createdTime;
}
public Long getUpdatedBy() {
public String getUpdatedBy() {
return updatedBy;
}
public void setUpdatedBy(Long updatedBy) {
public void setUpdatedBy(String updatedBy) {
this.updatedBy = updatedBy;
}

View File

@ -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);

View File

@ -0,0 +1,54 @@
package com.fundplatform.common.util;
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
/**
* 雪花算法ID生成器
* 生成字符串类型的分布式唯一ID
*
* <p>雪花ID特点</p>
* <ul>
* <li>19位数字字符串</li>
* <li>趋势递增有利于数据库索引</li>
* <li>分布式环境下唯一</li>
* <li>解决前端JavaScript大数精度丢失问题</li>
* </ul>
*
* @author fundplatform team
*/
public class SnowflakeIdGenerator {
/**
* 雪花算法实例
* workerId: 工作机器ID (0-31)
* datacenterId: 数据中心ID (0-31)
* 生产环境应根据实际部署情况配置
*/
private static final Snowflake SNOWFLAKE = IdUtil.getSnowflake(1, 1);
/**
* 私有构造函数防止实例化
*/
private SnowflakeIdGenerator() {
}
/**
* 生成下一个字符串类型的雪花ID
*
* @return 19位数字字符串
*/
public static String nextId() {
return String.valueOf(SNOWFLAKE.nextId());
}
/**
* 生成下一个Long类型的雪花ID
* 供MyBatis Plus IdentifierGenerator使用
*
* @return Long类型ID
*/
public static Long nextIdAsLong() {
return SNOWFLAKE.nextId();
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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);
}

View File

@ -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();
}

View File

@ -14,7 +14,7 @@ public class CustomerContact extends BaseEntity {
/**
* 客户ID
*/
private Long customerId;
private String customerId;
/**
* 联系人姓名
@ -51,11 +51,11 @@ public class CustomerContact extends BaseEntity {
*/
private String remark;
public Long getCustomerId() {
public String getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
public void setCustomerId(String customerId) {
this.customerId = customerId;
}

View File

@ -1,6 +1,7 @@
package com.fundplatform.exp.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fundplatform.common.core.PageResult;
import com.fundplatform.common.core.Result;
import com.fundplatform.common.util.ExcelUtil;
import com.fundplatform.exp.dto.ExpenseExcel;
@ -62,14 +63,21 @@ public class FundExpenseController {
* 分页查询支出列表
*/
@GetMapping("/page")
public Result<Page<FundExpenseVO>> page(
public Result<PageResult<FundExpenseVO>> page(
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize,
@RequestParam(required = false) String title,
@RequestParam(required = false) Long expenseType,
@RequestParam(required = false) Integer payStatus,
@RequestParam(required = false) Integer approvalStatus) {
return Result.success(expenseService.pageExpenses(pageNum, pageSize, title, expenseType, payStatus, approvalStatus));
Page<FundExpenseVO> page = expenseService.pageExpenses(pageNum, pageSize, title, expenseType, payStatus, approvalStatus);
PageResult<FundExpenseVO> pageResult = new PageResult<>(
page.getCurrent(),
page.getSize(),
page.getTotal(),
page.getRecords()
);
return Result.success(pageResult);
}
/**

View File

@ -16,7 +16,7 @@ public class ExpenseType extends BaseEntity {
private String typeName;
/** 父类型ID0表示一级类型 */
private Long parentId;
private String parentId;
/** 类型层级 */
private Integer typeLevel;
@ -46,11 +46,11 @@ public class ExpenseType extends BaseEntity {
this.typeName = typeName;
}
public Long getParentId() {
public String getParentId() {
return parentId;
}
public void setParentId(Long parentId) {
public void setParentId(String parentId) {
this.parentId = parentId;
}

View File

@ -25,7 +25,7 @@ public class FundExpense extends BaseEntity {
private String currency;
/** 支出类型(1-日常支出 2-项目支出 3-工资发放 4-其他) */
private Long expenseType;
private String expenseType;
/** 收款单位 */
private String payeeName;
@ -43,13 +43,13 @@ public class FundExpense extends BaseEntity {
private String purpose;
/** 关联用款申请ID */
private Long requestId;
private String requestId;
/** 项目ID */
private Long projectId;
private String projectId;
/** 客户ID */
private Long customerId;
private String customerId;
/** 支付状态(0-待支付 1-已支付 2-支付失败) */
private Integer payStatus;
@ -67,7 +67,7 @@ public class FundExpense extends BaseEntity {
private Integer approvalStatus;
/** 审批人ID */
private Long approverId;
private String approverId;
/** 审批时间 */
private LocalDateTime approvalTime;
@ -110,11 +110,11 @@ public class FundExpense extends BaseEntity {
this.currency = currency;
}
public Long getExpenseType() {
public String getExpenseType() {
return expenseType;
}
public void setExpenseType(Long expenseType) {
public void setExpenseType(String expenseType) {
this.expenseType = expenseType;
}
@ -158,27 +158,27 @@ public class FundExpense extends BaseEntity {
this.purpose = purpose;
}
public Long getRequestId() {
public String getRequestId() {
return requestId;
}
public void setRequestId(Long requestId) {
public void setRequestId(String requestId) {
this.requestId = requestId;
}
public Long getProjectId() {
public String getProjectId() {
return projectId;
}
public void setProjectId(Long projectId) {
public void setProjectId(String projectId) {
this.projectId = projectId;
}
public Long getCustomerId() {
public String getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
public void setCustomerId(String customerId) {
this.customerId = customerId;
}
@ -222,11 +222,11 @@ public class FundExpense extends BaseEntity {
this.approvalStatus = approvalStatus;
}
public Long getApproverId() {
public String getApproverId() {
return approverId;
}
public void setApproverId(Long approverId) {
public void setApproverId(String approverId) {
this.approverId = approverId;
}

View File

@ -46,7 +46,7 @@ public class ExpenseTypeServiceImpl implements ExpenseTypeService {
Page<ExpenseType> page = typeDataService.page(new Page<>(pageNum, pageSize), wrapper);
Page<ExpenseTypeVO> voPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
voPage.setRecords(page.getRecords().stream().map(this::convertToVO).collect(Collectors.toList()));
voPage.setRecords(page.getRecords().stream().map(this::convertToVOWithParentName).collect(Collectors.toList()));
return voPage;
}
@ -86,8 +86,10 @@ public class ExpenseTypeServiceImpl implements ExpenseTypeService {
type.setId(dto.getId());
type.setTypeCode(dto.getTypeCode());
type.setTypeName(dto.getTypeName());
type.setParentId(dto.getParentId() != null ? dto.getParentId() : 0L);
type.setSortOrder(dto.getSortOrder());
type.setDescription(dto.getDescription());
type.setStatus(dto.getStatus());
type.setUpdatedTime(LocalDateTime.now());
type.setUpdatedBy(UserContextHolder.getUserId());
@ -195,6 +197,20 @@ public class ExpenseTypeServiceImpl implements ExpenseTypeService {
vo.setCreatedTime(type.getCreatedTime());
vo.setUpdatedTime(type.getUpdatedTime());
vo.setRemark(type.getRemark());
// 查询父类型名称
if (type.getParentId() != null && type.getParentId() > 0) {
ExpenseType parentType = typeDataService.getById(type.getParentId());
if (parentType != null) {
vo.setParentName(parentType.getTypeName());
}
}
return vo;
}
private ExpenseTypeVO convertToVOWithParentName(ExpenseType type) {
ExpenseTypeVO vo = convertToVO(type);
return vo;
}
}

View File

@ -1,20 +1,27 @@
package com.fundplatform.exp.vo;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import java.time.LocalDateTime;
import java.util.List;
public class ExpenseTypeVO {
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
private String typeCode;
private String typeName;
@JsonSerialize(using = ToStringSerializer.class)
private Long parentId;
private String parentName;
private Integer typeLevel;
private Integer sortOrder;
private String description;
private Integer status;
@JsonSerialize(using = ToStringSerializer.class)
private Long tenantId;
@JsonSerialize(using = ToStringSerializer.class)
private Long createdBy;
private LocalDateTime createdTime;
private LocalDateTime updatedTime;

View File

@ -1,15 +1,20 @@
package com.fundplatform.exp.vo;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import java.math.BigDecimal;
import java.time.LocalDateTime;
public class FundExpenseVO {
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
private String expenseNo;
private String title;
private BigDecimal amount;
private String currency;
@JsonSerialize(using = ToStringSerializer.class)
private Long expenseType;
private String expenseTypeName;
private String payeeName;
@ -17,8 +22,11 @@ public class FundExpenseVO {
private String payeeAccount;
private LocalDateTime expenseDate;
private String purpose;
@JsonSerialize(using = ToStringSerializer.class)
private Long requestId;
@JsonSerialize(using = ToStringSerializer.class)
private Long projectId;
@JsonSerialize(using = ToStringSerializer.class)
private Long customerId;
private Integer payStatus;
private String payStatusName;
@ -27,11 +35,14 @@ public class FundExpenseVO {
private String payVoucher;
private Integer approvalStatus;
private String approvalStatusName;
@JsonSerialize(using = ToStringSerializer.class)
private Long approverId;
private LocalDateTime approvalTime;
private String approvalComment;
private String attachments;
@JsonSerialize(using = ToStringSerializer.class)
private Long tenantId;
@JsonSerialize(using = ToStringSerializer.class)
private Long createdBy;
private LocalDateTime createdTime;

View File

@ -1,6 +1,5 @@
package com.fundplatform.file.data.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
@ -13,10 +12,10 @@ import java.time.LocalDateTime;
@TableName("file_record")
public class FileRecord {
@TableId(type = IdType.AUTO)
private Long fileId;
@TableId
private String fileId;
private Long tenantId;
private String tenantId;
private String fileName;
@ -36,7 +35,7 @@ public class FileRecord {
private String businessType;
private Long businessId;
private String businessId;
private String description;
@ -44,11 +43,11 @@ public class FileRecord {
private Integer status;
private Long createdBy;
private String createdBy;
private LocalDateTime createdTime;
private Long updatedBy;
private String updatedBy;
private LocalDateTime updatedTime;
@ -56,19 +55,19 @@ public class FileRecord {
private Integer deleted;
// Getters and Setters
public Long getFileId() {
public String getFileId() {
return fileId;
}
public void setFileId(Long fileId) {
public void setFileId(String fileId) {
this.fileId = fileId;
}
public Long getTenantId() {
public String getTenantId() {
return tenantId;
}
public void setTenantId(Long tenantId) {
public void setTenantId(String tenantId) {
this.tenantId = tenantId;
}
@ -144,11 +143,11 @@ public class FileRecord {
this.businessType = businessType;
}
public Long getBusinessId() {
public String getBusinessId() {
return businessId;
}
public void setBusinessId(Long businessId) {
public void setBusinessId(String businessId) {
this.businessId = businessId;
}
@ -176,11 +175,11 @@ public class FileRecord {
this.status = status;
}
public Long getCreatedBy() {
public String getCreatedBy() {
return createdBy;
}
public void setCreatedBy(Long createdBy) {
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
@ -192,11 +191,11 @@ public class FileRecord {
this.createdTime = createdTime;
}
public Long getUpdatedBy() {
public String getUpdatedBy() {
return updatedBy;
}
public void setUpdatedBy(Long updatedBy) {
public void setUpdatedBy(String updatedBy) {
this.updatedBy = updatedBy;
}

View File

@ -66,7 +66,9 @@ public class TenantGatewayFilter implements GlobalFilter, Ordered {
return chain.filter(exchange);
}
// 检查X-Tenant-Id请求头
// 安全修复从请求头中取 X-Tenant-IdTokenAuthFilter 已用 Token 中的值覆盖客户端传入值
// 因此此处取到的必为 Token 认证后的租户ID无需再与客户端值单独比对
// 但仍需校验格式合法性防止异常数据流入下游服务
String tenantId = request.getHeaders().getFirst(HEADER_TENANT_ID);
if (tenantId == null || tenantId.trim().isEmpty()) {
logger.warn("[TenantGateway] 缺少X-Tenant-Id请求头路径: {}", path);

View File

@ -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()))

View File

@ -24,5 +24,6 @@ declare module 'vue' {
VanPullRefresh: typeof import('vant/es')['PullRefresh']
VanSearch: typeof import('vant/es')['Search']
VanTag: typeof import('vant/es')['Tag']
VanUploader: typeof import('vant/es')['Uploader']
}
}

View File

@ -9,6 +9,7 @@
"version": "0.0.1",
"dependencies": {
"axios": "^1.6.0",
"js-md5": "^0.8.3",
"pinia": "^2.1.7",
"vant": "^4.9.22",
"vue": "^3.4.0",
@ -1690,6 +1691,12 @@
"node": ">=0.12.0"
}
},
"node_modules/js-md5": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.8.3.tgz",
"integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==",
"license": "MIT"
},
"node_modules/local-pkg": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz",

View File

@ -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",

View File

@ -2,8 +2,13 @@ import request from './request'
// ===================== 用户认证 =====================
export function login(data: { username: string; password: string }) {
return request.post('/auth/login', data)
export function login(data: { username: string; password: string, tenantId?: number }) {
// 如果没有传递 tenantId使用默认值 1
const requestData = {
...data,
tenantId: data.tenantId || 1
}
return request.post('/auth/login', requestData)
}
export function getUserInfo() {
@ -18,6 +23,30 @@ export function updatePassword(data: { oldPassword: string; newPassword: string;
return request.put('/sys/profile/password', data)
}
// ===================== 文件管理 =====================
export function uploadFile(file: File, businessType?: string, businessId?: number, description?: string) {
const formData = new FormData()
formData.append('file', file)
if (businessType) formData.append('businessType', businessType)
if (businessId) formData.append('businessId', String(businessId))
if (description) formData.append('description', description)
return request.post('/file/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
export function getFileList(params?: { pageNum: number; pageSize: number; businessType?: string; businessId?: number; fileType?: string }) {
return request.get('/file/page', { params })
}
export function deleteFile(id: number) {
return request.delete(`/file/${id}`)
}
// ===================== 项目管理 =====================
export function getProjectList(params?: { pageNum: number; pageSize: number; keyword?: string }) {

View File

@ -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({

View File

@ -51,6 +51,18 @@
<label>支出描述</label>
<textarea v-model="form.description" placeholder="输入描述" rows="3" class="mac-textarea"></textarea>
</div>
<div class="form-group">
<label>附件上传图片</label>
<van-uploader
v-model="fileList"
:accept="'image/*'"
:max-count="9"
:after-read="onAfterRead"
:before-delete="onBeforeDelete"
multiple
/>
</div>
</div>
<div class="submit-btn">
@ -74,13 +86,15 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { showToast, showSuccessToast, showFailToast } from 'vant'
import { createExpense, getExpenseTypeTree } from '@/api'
import { showToast, showSuccessToast, showFailToast, ImagePreview } from 'vant'
import { createExpense, getExpenseTypeTree, uploadFile } from '@/api'
const router = useRouter()
const loading = ref(false)
const showTypePicker = ref(false)
const showDatePicker = ref(false)
const fileList = ref<any[]>([])
const uploadedAttachments = ref<string[]>([]) // COS
const form = reactive({
title: '',
@ -106,6 +120,48 @@ const onDateConfirm = ({ selectedValues }: any) => {
showDatePicker.value = false
}
// - 使 COS
const onAfterRead = async (file: any) => {
if (!file.file) return
try {
//
showToast('上传中...')
// uploadFile API COS
const res: any = await uploadFile(file.file, 'expense', undefined, '支出附件')
//
const filePath = res.data?.filePath || res.data?.url
if (filePath) {
uploadedAttachments.value.push(filePath)
console.log('文件上传成功:', filePath)
showSuccessToast('上传成功')
} else {
showFailToast('上传失败:未获取文件路径')
}
} catch (error: any) {
console.error('上传失败:', error)
showFailToast(error.message || '上传失败')
}
}
const onBeforeDelete = (file: any, detail: any) => {
//
uploadedAttachments.value.splice(detail.index, 1)
return true
}
// URL
const onPreviewImage = (index: number) => {
//
// 使 URL
ImagePreview.show({
images: uploadedAttachments.value,
startPosition: index,
})
}
const handleSubmit = async () => {
if (!form.title) {
showFailToast('请输入支出标题')
@ -126,15 +182,22 @@ const handleSubmit = async () => {
loading.value = true
try {
// LocalDateTime
// LocalDateTime
const expenseDateTime = form.expenseDate ? `${form.expenseDate}T12:00:00` : null
// COS
const attachmentsStr = uploadedAttachments.value.length > 0
? uploadedAttachments.value.join(',')
: null
const requestData = {
title: form.title,
expenseType: form.expenseTypeId,
amount: parseFloat(form.amount),
expenseDate: expenseDateTime,
purpose: form.description,
payeeName: form.payeeName
payeeName: form.payeeName,
attachments: attachmentsStr
}
console.log('提交支出数据:', requestData)
await createExpense(requestData)

View File

@ -16,29 +16,42 @@
@load="onLoad"
>
<div class="expense-card" v-for="item in list" :key="item.expenseId">
<div class="expense-header">
<span class="expense-title">{{ item.title }}</span>
<van-tag :type="getStatusType(item.status)">{{ getStatusText(item.status) }}</van-tag>
<!-- 第一行标题 + 支出时间 -->
<div class="card-row title-row">
<span class="card-title">{{ item.title }}</span>
<span class="card-time">{{ formatDateTime(item.expenseDate) }}</span>
</div>
<div class="expense-info">
<div class="info-item">
<van-icon name="apps-o" />
<!-- 第二行支出类型 + 支出金额 -->
<div class="card-row info-row">
<div class="info-left">
<van-icon name="apps-o" class="row-icon" />
<span class="info-label">支出类型</span>
<span>{{ item.typeName || '-' }}</span>
</div>
<div class="info-item">
<van-icon name="clock-o" />
<span>{{ item.expenseDate }}</span>
</div>
<span class="card-amount">¥{{ item.amount?.toLocaleString() }}</span>
</div>
<div class="expense-amount">
<div class="amount-item">
<span class="label">支出金额</span>
<span class="value expense-value">¥{{ item.amount?.toLocaleString() }}</span>
</div>
<div class="amount-item" v-if="item.paidAmount">
<span class="label">已支付</span>
<span class="value">¥{{ item.paidAmount?.toLocaleString() }}</span>
</div>
<!-- 第三行收款单位 -->
<div class="card-row payee-row">
<van-icon name="shop-o" class="row-icon" />
<span class="info-label">收款单位</span>
<span>{{ item.payeeName || '-' }}</span>
</div>
<!-- 第四行支付描述 -->
<div class="card-row desc-row" v-if="item.purpose">
<van-icon name="description-o" class="row-icon" />
<span class="info-label">支付描述</span>
<span class="desc-text">{{ item.purpose }}</span>
</div>
<!-- 第五行查看附件 -->
<div class="card-row attachment-row" v-if="item.attachments">
<van-icon name="photo-o" class="row-icon" />
<span class="attachment-btn" @click="previewAttachments(item)">
查看附件{{ getAttachmentCount(item.attachments) }}
</span>
</div>
</div>
</van-list>
@ -54,6 +67,7 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { getExpenseList } from '@/api'
import { ImagePreview } from 'vant'
const searchText = ref('')
const loading = ref(false)
@ -63,6 +77,43 @@ const list = ref<any[]>([])
const pageNum = ref(1)
const pageSize = 10
//
const formatDateTime = (dateTime: string) => {
if (!dateTime) return ''
try {
// LocalDateTime 2024-01-15T10:30:00
const date = new Date(dateTime)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
} catch (e) {
return dateTime
}
}
//
const getAttachmentCount = (attachments: string) => {
if (!attachments) return 0
return attachments.split(',').filter(s => s.trim()).length
}
//
const previewAttachments = (item: any) => {
if (!item.attachments) return
// base64 URL
const attachmentList = item.attachments.split(',')
const imageUrls = attachmentList.map((b64: string) => `data:image/jpeg;base64,${b64}`)
ImagePreview.show({
images: imageUrls,
startPosition: 0,
})
}
const getStatusType = (status: string): 'primary' | 'success' | 'warning' | 'danger' | 'default' => {
const map: Record<string, 'primary' | 'success' | 'warning' | 'danger' | 'default'> = {
'pending': 'warning',
@ -152,59 +203,99 @@ onMounted(() => {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.expense-header {
.card-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
margin-bottom: 10px;
}
.expense-title {
.card-row:last-child {
margin-bottom: 0;
}
.title-row {
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
padding-bottom: 10px;
border-bottom: 1px solid #f0f0f0;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #333;
flex: 1;
margin-right: 12px;
}
.expense-info {
display: flex;
gap: 16px;
margin-bottom: 12px;
.card-time {
font-size: 12px;
color: #999;
white-space: nowrap;
}
.info-item {
.info-row {
justify-content: space-between;
margin-bottom: 10px;
}
.info-left {
display: flex;
align-items: center;
gap: 4px;
flex: 1;
font-size: 13px;
color: #666;
}
.expense-amount {
display: flex;
gap: 24px;
padding-top: 12px;
.row-icon {
font-size: 14px;
color: #999;
margin-right: 4px;
}
.info-label {
color: #999;
margin-right: 4px;
}
.card-amount {
font-size: 18px;
font-weight: 600;
color: #FF3B30;
white-space: nowrap;
}
.payee-row,
.desc-row {
font-size: 13px;
color: #666;
margin-bottom: 8px;
}
.desc-text {
flex: 1;
line-height: 1.5;
}
.attachment-row {
padding-top: 10px;
margin-top: 10px;
border-top: 1px solid #f0f0f0;
}
.amount-item {
display: flex;
flex-direction: column;
.attachment-btn {
color: #007AFF;
font-size: 13px;
cursor: pointer;
padding: 4px 8px;
background: rgba(0, 122, 255, 0.08);
border-radius: 4px;
transition: all 0.2s ease;
}
.amount-item .label {
font-size: 12px;
color: #999;
}
.amount-item .value {
font-size: 15px;
font-weight: 600;
color: #333;
margin-top: 4px;
}
.expense-value {
color: #FF3B30 !important;
.attachment-btn:active {
background: rgba(0, 122, 255, 0.15);
}
.add-btn {

View File

@ -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('密码修改成功')

View File

@ -14,7 +14,7 @@ public class Project extends BaseEntity {
private String projectCode;
private String projectName;
private Long customerId;
private String customerId;
private String customerName;
private String projectType;
private BigDecimal budgetAmount;
@ -39,11 +39,11 @@ public class Project extends BaseEntity {
this.projectName = projectName;
}
public Long getCustomerId() {
public String getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
public void setCustomerId(String customerId) {
this.customerId = customerId;
}

View File

@ -23,10 +23,10 @@ public class Requirement extends BaseEntity {
private String description;
/** 项目ID */
private Long projectId;
private String projectId;
/** 客户ID */
private Long customerId;
private String customerId;
/** 优先级(high-高normal-中low-低) */
private String priority;
@ -91,19 +91,19 @@ public class Requirement extends BaseEntity {
this.description = description;
}
public Long getProjectId() {
public String getProjectId() {
return projectId;
}
public void setProjectId(Long projectId) {
public void setProjectId(String projectId) {
this.projectId = projectId;
}
public Long getCustomerId() {
public String getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
public void setCustomerId(String customerId) {
this.customerId = customerId;
}

View File

@ -43,13 +43,13 @@ public class FundReceipt extends BaseEntity {
private String purpose;
/** 关联项目ID */
private Long projectId;
private String projectId;
/** 关联客户ID */
private Long customerId;
private String customerId;
/** 关联应收款ID */
private Long receivableId;
private String receivableId;
/** 收款状态(0-待确认 1-已确认 2-已核销) */
private Integer receiptStatus;
@ -58,13 +58,13 @@ public class FundReceipt extends BaseEntity {
private LocalDateTime confirmTime;
/** 确认人ID */
private Long confirmBy;
private String confirmBy;
/** 核销时间 */
private LocalDateTime writeOffTime;
/** 核销人ID */
private Long writeOffBy;
private String writeOffBy;
/** 收款凭证 */
private String voucher;
@ -95,22 +95,22 @@ public class FundReceipt extends BaseEntity {
public void setReceiptDate(LocalDateTime receiptDate) { this.receiptDate = receiptDate; }
public String getPurpose() { return purpose; }
public void setPurpose(String purpose) { this.purpose = purpose; }
public Long getProjectId() { return projectId; }
public void setProjectId(Long projectId) { this.projectId = projectId; }
public Long getCustomerId() { return customerId; }
public void setCustomerId(Long customerId) { this.customerId = customerId; }
public Long getReceivableId() { return receivableId; }
public void setReceivableId(Long receivableId) { this.receivableId = receivableId; }
public String getProjectId() { return projectId; }
public void setProjectId(String projectId) { this.projectId = projectId; }
public String getCustomerId() { return customerId; }
public void setCustomerId(String customerId) { this.customerId = customerId; }
public String getReceivableId() { return receivableId; }
public void setReceivableId(String receivableId) { this.receivableId = receivableId; }
public Integer getReceiptStatus() { return receiptStatus; }
public void setReceiptStatus(Integer receiptStatus) { this.receiptStatus = receiptStatus; }
public LocalDateTime getConfirmTime() { return confirmTime; }
public void setConfirmTime(LocalDateTime confirmTime) { this.confirmTime = confirmTime; }
public Long getConfirmBy() { return confirmBy; }
public void setConfirmBy(Long confirmBy) { this.confirmBy = confirmBy; }
public String getConfirmBy() { return confirmBy; }
public void setConfirmBy(String confirmBy) { this.confirmBy = confirmBy; }
public LocalDateTime getWriteOffTime() { return writeOffTime; }
public void setWriteOffTime(LocalDateTime writeOffTime) { this.writeOffTime = writeOffTime; }
public Long getWriteOffBy() { return writeOffBy; }
public void setWriteOffBy(Long writeOffBy) { this.writeOffBy = writeOffBy; }
public String getWriteOffBy() { return writeOffBy; }
public void setWriteOffBy(String writeOffBy) { this.writeOffBy = writeOffBy; }
public String getVoucher() { return voucher; }
public void setVoucher(String voucher) { this.voucher = voucher; }
public String getInvoiceNo() { return invoiceNo; }

View File

@ -17,13 +17,13 @@ public class Receivable extends BaseEntity {
private String receivableCode;
/** 关联需求ID */
private Long requirementId;
private String requirementId;
/** 关联项目ID */
private Long projectId;
private String projectId;
/** 关联客户ID */
private Long customerId;
private String customerId;
/** 应收款金额 */
private BigDecimal receivableAmount;
@ -59,7 +59,7 @@ public class Receivable extends BaseEntity {
private LocalDateTime confirmTime;
/** 确认人ID */
private Long confirmBy;
private String confirmBy;
/** 备注 */
private String remark;
@ -72,27 +72,27 @@ public class Receivable extends BaseEntity {
this.receivableCode = receivableCode;
}
public Long getRequirementId() {
public String getRequirementId() {
return requirementId;
}
public void setRequirementId(Long requirementId) {
public void setRequirementId(String requirementId) {
this.requirementId = requirementId;
}
public Long getProjectId() {
public String getProjectId() {
return projectId;
}
public void setProjectId(Long projectId) {
public void setProjectId(String projectId) {
this.projectId = projectId;
}
public Long getCustomerId() {
public String getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
public void setCustomerId(String customerId) {
this.customerId = customerId;
}
@ -184,11 +184,11 @@ public class Receivable extends BaseEntity {
this.confirmTime = confirmTime;
}
public Long getConfirmBy() {
public String getConfirmBy() {
return confirmBy;
}
public void setConfirmBy(Long confirmBy) {
public void setConfirmBy(String confirmBy) {
this.confirmBy = confirmBy;
}

View File

@ -8,7 +8,7 @@ import java.time.LocalDateTime;
public class FundReceiptDTO {
private Long id;
private String id;
@NotBlank(message = "收款标题不能为空")
private String title;
@ -29,15 +29,15 @@ public class FundReceiptDTO {
private String payerAccount;
private LocalDateTime receiptDate;
private String purpose;
private Long projectId;
private Long customerId;
private String projectId;
private String customerId;
private String invoiceNo;
private String voucher;
private String attachments;
private String remark;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public BigDecimal getAmount() { return amount; }
@ -56,10 +56,10 @@ public class FundReceiptDTO {
public void setReceiptDate(LocalDateTime receiptDate) { this.receiptDate = receiptDate; }
public String getPurpose() { return purpose; }
public void setPurpose(String purpose) { this.purpose = purpose; }
public Long getProjectId() { return projectId; }
public void setProjectId(Long projectId) { this.projectId = projectId; }
public Long getCustomerId() { return customerId; }
public void setCustomerId(Long customerId) { this.customerId = customerId; }
public String getProjectId() { return projectId; }
public void setProjectId(String projectId) { this.projectId = projectId; }
public String getCustomerId() { return customerId; }
public void setCustomerId(String customerId) { this.customerId = customerId; }
public String getInvoiceNo() { return invoiceNo; }
public void setInvoiceNo(String invoiceNo) { this.invoiceNo = invoiceNo; }
public String getVoucher() { return voucher; }

View File

@ -12,15 +12,15 @@ import java.time.LocalDate;
*/
public class ReceivableDTO {
private Long id;
private String id;
private Long requirementId;
private String requirementId;
@NotNull(message = "项目ID不能为空")
private Long projectId;
private String projectId;
@NotNull(message = "客户ID不能为空")
private Long customerId;
private String customerId;
@NotNull(message = "应收款金额不能为空")
@Positive(message = "应收款金额必须大于0")
@ -37,35 +37,35 @@ public class ReceivableDTO {
private String remark;
public Long getId() {
public String getId() {
return id;
}
public void setId(Long id) {
public void setId(String id) {
this.id = id;
}
public Long getRequirementId() {
public String getRequirementId() {
return requirementId;
}
public void setRequirementId(Long requirementId) {
public void setRequirementId(String requirementId) {
this.requirementId = requirementId;
}
public Long getProjectId() {
public String getProjectId() {
return projectId;
}
public void setProjectId(Long projectId) {
public void setProjectId(String projectId) {
this.projectId = projectId;
}
public Long getCustomerId() {
public String getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
public void setCustomerId(String customerId) {
this.customerId = customerId;
}

View File

@ -5,7 +5,7 @@ import java.time.LocalDateTime;
public class FundReceiptVO {
private Long id;
private String id;
private String receiptNo;
private String title;
private BigDecimal amount;
@ -17,24 +17,24 @@ public class FundReceiptVO {
private String payerAccount;
private LocalDateTime receiptDate;
private String purpose;
private Long projectId;
private Long customerId;
private Long receivableId;
private String projectId;
private String customerId;
private String receivableId;
private Integer receiptStatus;
private String receiptStatusName;
private LocalDateTime confirmTime;
private Long confirmBy;
private String confirmBy;
private LocalDateTime writeOffTime;
private Long writeOffBy;
private String writeOffBy;
private String voucher;
private String invoiceNo;
private String attachments;
private Long tenantId;
private Long createdBy;
private String tenantId;
private String createdBy;
private LocalDateTime createdTime;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getReceiptNo() { return receiptNo; }
public void setReceiptNo(String receiptNo) { this.receiptNo = receiptNo; }
public String getTitle() { return title; }
@ -57,34 +57,34 @@ public class FundReceiptVO {
public void setReceiptDate(LocalDateTime receiptDate) { this.receiptDate = receiptDate; }
public String getPurpose() { return purpose; }
public void setPurpose(String purpose) { this.purpose = purpose; }
public Long getProjectId() { return projectId; }
public void setProjectId(Long projectId) { this.projectId = projectId; }
public Long getCustomerId() { return customerId; }
public void setCustomerId(Long customerId) { this.customerId = customerId; }
public Long getReceivableId() { return receivableId; }
public void setReceivableId(Long receivableId) { this.receivableId = receivableId; }
public String getProjectId() { return projectId; }
public void setProjectId(String projectId) { this.projectId = projectId; }
public String getCustomerId() { return customerId; }
public void setCustomerId(String customerId) { this.customerId = customerId; }
public String getReceivableId() { return receivableId; }
public void setReceivableId(String receivableId) { this.receivableId = receivableId; }
public Integer getReceiptStatus() { return receiptStatus; }
public void setReceiptStatus(Integer receiptStatus) { this.receiptStatus = receiptStatus; }
public String getReceiptStatusName() { return receiptStatusName; }
public void setReceiptStatusName(String receiptStatusName) { this.receiptStatusName = receiptStatusName; }
public LocalDateTime getConfirmTime() { return confirmTime; }
public void setConfirmTime(LocalDateTime confirmTime) { this.confirmTime = confirmTime; }
public Long getConfirmBy() { return confirmBy; }
public void setConfirmBy(Long confirmBy) { this.confirmBy = confirmBy; }
public String getConfirmBy() { return confirmBy; }
public void setConfirmBy(String confirmBy) { this.confirmBy = confirmBy; }
public LocalDateTime getWriteOffTime() { return writeOffTime; }
public void setWriteOffTime(LocalDateTime writeOffTime) { this.writeOffTime = writeOffTime; }
public Long getWriteOffBy() { return writeOffBy; }
public void setWriteOffBy(Long writeOffBy) { this.writeOffBy = writeOffBy; }
public String getWriteOffBy() { return writeOffBy; }
public void setWriteOffBy(String writeOffBy) { this.writeOffBy = writeOffBy; }
public String getVoucher() { return voucher; }
public void setVoucher(String voucher) { this.voucher = voucher; }
public String getInvoiceNo() { return invoiceNo; }
public void setInvoiceNo(String invoiceNo) { this.invoiceNo = invoiceNo; }
public String getAttachments() { return attachments; }
public void setAttachments(String attachments) { this.attachments = attachments; }
public Long getTenantId() { return tenantId; }
public void setTenantId(Long tenantId) { this.tenantId = tenantId; }
public Long getCreatedBy() { return createdBy; }
public void setCreatedBy(Long createdBy) { this.createdBy = createdBy; }
public String getTenantId() { return tenantId; }
public void setTenantId(String tenantId) { this.tenantId = tenantId; }
public String getCreatedBy() { return createdBy; }
public void setCreatedBy(String createdBy) { this.createdBy = createdBy; }
public LocalDateTime getCreatedTime() { return createdTime; }
public void setCreatedTime(LocalDateTime createdTime) { this.createdTime = createdTime; }
}

View File

@ -40,10 +40,10 @@ public class FundRequest extends BaseEntity {
private String purpose;
/** 项目ID */
private Long projectId;
private String projectId;
/** 客户ID */
private Long customerId;
private String customerId;
/** 申请日期 */
private LocalDateTime requestDate;
@ -58,7 +58,7 @@ public class FundRequest extends BaseEntity {
private Integer currentNode;
/** 审批人ID */
private Long approverId;
private String approverId;
/** 审批时间 */
private LocalDateTime approvalTime;
@ -141,19 +141,19 @@ public class FundRequest extends BaseEntity {
this.purpose = purpose;
}
public Long getProjectId() {
public String getProjectId() {
return projectId;
}
public void setProjectId(Long projectId) {
public void setProjectId(String projectId) {
this.projectId = projectId;
}
public Long getCustomerId() {
public String getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
public void setCustomerId(String customerId) {
this.customerId = customerId;
}
@ -189,11 +189,11 @@ public class FundRequest extends BaseEntity {
this.currentNode = currentNode;
}
public Long getApproverId() {
public String getApproverId() {
return approverId;
}
public void setApproverId(Long approverId) {
public void setApproverId(String approverId) {
this.approverId = approverId;
}

View File

@ -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>

View File

@ -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) String 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") String 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") String userId) {
UserVO vo = authService.getUserInfo(userId);
return Result.success(vo);
}

View File

@ -28,8 +28,8 @@ public class ConfigController {
* 创建参数
*/
@PostMapping
public Result<Long> create(@Valid @RequestBody ConfigDTO dto) {
Long id = configService.createConfig(dto);
public Result<String> create(@Valid @RequestBody ConfigDTO dto) {
String id = configService.createConfig(dto);
return Result.success(id);
}
@ -46,7 +46,7 @@ public class ConfigController {
* 根据ID查询参数
*/
@GetMapping("/{id}")
public Result<ConfigVO> getById(@PathVariable Long id) {
public Result<ConfigVO> getById(@PathVariable String id) {
ConfigVO vo = configService.getConfigById(id);
return Result.success(vo);
}
@ -94,7 +94,7 @@ public class ConfigController {
* 删除参数
*/
@DeleteMapping("/{id}")
public Result<Boolean> delete(@PathVariable Long id) {
public Result<Boolean> delete(@PathVariable String id) {
boolean result = configService.deleteConfig(id);
return Result.success(result);
}

View File

@ -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);
public Result<String> create(@Valid @RequestBody DeptDTO dto) {
String 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 String 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 String 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 String id, @Parameter(description = "状态0禁用 1启用") @RequestParam Integer status) {
boolean result = deptService.updateStatus(id, status);
return Result.success(result);
}

View File

@ -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);
public Result<String> create(@Valid @RequestBody MenuDTO dto) {
String 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 String 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 String 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 String 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 String userId) {
List<String> permissions = menuService.getUserPermissions(userId);
return Result.success(permissions);
}

View File

@ -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));
}
}

View File

@ -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") String 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") String 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") String userId,
@Valid @RequestBody PasswordDTO dto) {
boolean result = userService.updatePassword(userId, dto);
return Result.success(result);

View File

@ -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);
public Result<String> create(@Valid @RequestBody RoleDTO dto) {
String 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 String 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 String 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 String id, @Parameter(description = "状态0禁用 1启用") @RequestParam Integer status) {
boolean result = roleService.updateStatus(id, status);
return Result.success(result);
}
@ -72,17 +83,19 @@ public class RoleController {
/**
* 获取角色菜单ID列表
*/
@Operation(summary = "获取角色的菜单ID列表")
@GetMapping("/{id}/menus")
public Result<List<Long>> getRoleMenus(@PathVariable Long id) {
List<Long> menuIds = roleService.getRoleMenus(id);
public Result<List<String>> getRoleMenus(@Parameter(description = "角色ID") @PathVariable String id) {
List<String> menuIds = roleService.getRoleMenus(id);
return Result.success(menuIds);
}
/**
* 分配菜单给角色
*/
@Operation(summary = "为角色分配菜单权限")
@PutMapping("/{id}/menus")
public Result<Boolean> assignMenus(@PathVariable Long id, @RequestBody List<Long> menuIds) {
public Result<Boolean> assignMenus(@Parameter(description = "角色ID") @PathVariable String id, @RequestBody List<String> menuIds) {
boolean result = roleService.assignMenus(id, menuIds);
return Result.success(result);
}

View File

@ -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) {
public Result<String> 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 String 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 String 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 String id, @Parameter(description = "状态0禁用 1启用 2过期") @RequestParam Integer status) {
return Result.success(tenantService.updateStatus(id, status));
}
}

View File

@ -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,15 +28,17 @@ public class UserController {
/**
* 创建用户
*/
@Operation(summary = "创建用户")
@PostMapping
public Result<Long> create(@Valid @RequestBody UserDTO dto) {
Long userId = userService.createUser(dto);
public Result<String> create(@Valid @RequestBody UserDTO dto) {
String userId = userService.createUser(dto);
return Result.success(userId);
}
/**
* 更新用户
*/
@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 String 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) String 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 String id) {
boolean result = userService.deleteUser(id);
return Result.success(result);
}
@ -74,8 +83,9 @@ public class UserController {
/**
* 批量删除用户
*/
@Operation(summary = "批量删除用户")
@DeleteMapping("/batch")
public Result<Boolean> batchDelete(@RequestBody Long[] ids) {
public Result<Boolean> batchDelete(@RequestBody String[] ids) {
boolean result = userService.batchDeleteUsers(ids);
return Result.success(result);
}
@ -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 String 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 String id, @Parameter(description = "状态0禁用 1启用") @RequestParam Integer status) {
boolean result = userService.updateStatus(id, status);
return Result.success(result);
}

View File

@ -1,6 +1,5 @@
package com.fundplatform.sys.data.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@ -12,10 +11,10 @@ import java.time.LocalDateTime;
@TableName("sys_operation_log")
public class OperationLog {
@TableId(type = IdType.AUTO)
private Long logId;
@TableId
private String logId;
private Long userId;
private String userId;
private String username;
@ -37,19 +36,19 @@ public class OperationLog {
private String errorMsg;
public Long getLogId() {
public String getLogId() {
return logId;
}
public void setLogId(Long logId) {
public void setLogId(String logId) {
this.logId = logId;
}
public Long getUserId() {
public String getUserId() {
return userId;
}
public void setUserId(Long userId) {
public void setUserId(String userId) {
this.userId = userId;
}

View File

@ -9,7 +9,7 @@ import com.fundplatform.common.core.BaseEntity;
@TableName("sys_dept")
public class SysDept extends BaseEntity {
private Long parentId;
private String parentId;
private String deptCode;
private String deptName;
private String deptLeader;
@ -18,11 +18,11 @@ public class SysDept extends BaseEntity {
private Integer sortOrder;
private Integer status;
public Long getParentId() {
public String getParentId() {
return parentId;
}
public void setParentId(Long parentId) {
public void setParentId(String parentId) {
this.parentId = parentId;
}

View File

@ -9,7 +9,7 @@ import com.fundplatform.common.core.BaseEntity;
@TableName("sys_menu")
public class SysMenu extends BaseEntity {
private Long parentId;
private String parentId;
private String menuName;
private Integer menuType;
private String menuPath;
@ -20,11 +20,11 @@ public class SysMenu extends BaseEntity {
private Integer visible;
private Integer status;
public Long getParentId() {
public String getParentId() {
return parentId;
}
public void setParentId(Long parentId) {
public void setParentId(String parentId) {
this.parentId = parentId;
}

View File

@ -14,13 +14,13 @@ public class SysTenant extends BaseEntity {
/** 租户表不需要tenant_id字段 */
@TableField(exist = false)
private Long tenantId;
private String tenantId;
public Long getTenantId() {
public String getTenantId() {
return tenantId;
}
public void setTenantId(Long tenantId) {
public void setTenantId(String tenantId) {
this.tenantId = tenantId;
}

View File

@ -14,7 +14,7 @@ public class SysUser extends BaseEntity {
private String realName;
private String phone;
private String email;
private Long deptId;
private String deptId;
private Integer status;
private String avatar;
@ -59,11 +59,11 @@ public class SysUser extends BaseEntity {
this.email = email;
}
public Long getDeptId() {
public String getDeptId() {
return deptId;
}
public void setDeptId(Long deptId) {
public void setDeptId(String deptId) {
this.deptId = deptId;
}

View File

@ -7,7 +7,7 @@ import jakarta.validation.constraints.NotBlank;
*/
public class ConfigDTO {
private Long id;
private String id;
@NotBlank(message = "参数键不能为空")
private String configKey;
@ -28,11 +28,11 @@ public class ConfigDTO {
private Integer sortOrder;
public Long getId() {
public String getId() {
return id;
}
public void setId(Long id) {
public void setId(String id) {
this.id = id;
}

View File

@ -8,9 +8,9 @@ import jakarta.validation.constraints.Size;
*/
public class DeptDTO {
private Long id;
private String id;
private Long parentId;
private String parentId;
@NotBlank(message = "部门编码不能为空")
@Size(max = 50, message = "部门编码不能超过50个字符")
@ -33,19 +33,19 @@ public class DeptDTO {
private String remark;
public Long getId() {
public String getId() {
return id;
}
public void setId(Long id) {
public void setId(String id) {
this.id = id;
}
public Long getParentId() {
public String getParentId() {
return parentId;
}
public void setParentId(Long parentId) {
public void setParentId(String parentId) {
this.parentId = parentId;
}

View File

@ -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;
}
}

View File

@ -8,9 +8,9 @@ import jakarta.validation.constraints.Size;
*/
public class MenuDTO {
private Long id;
private String id;
private Long parentId;
private String parentId;
@NotBlank(message = "菜单名称不能为空")
@Size(max = 50, message = "菜单名称不能超过50个字符")
@ -38,19 +38,19 @@ public class MenuDTO {
private String remark;
public Long getId() {
public String getId() {
return id;
}
public void setId(Long id) {
public void setId(String id) {
this.id = id;
}
public Long getParentId() {
public String getParentId() {
return parentId;
}
public void setParentId(Long parentId) {
public void setParentId(String parentId) {
this.parentId = parentId;
}

View File

@ -8,7 +8,7 @@ import jakarta.validation.constraints.Size;
*/
public class RoleDTO {
private Long id;
private String id;
@NotBlank(message = "角色编码不能为空")
@Size(max = 50, message = "角色编码不能超过50个字符")
@ -26,11 +26,11 @@ public class RoleDTO {
private String remark;
public Long getId() {
public String getId() {
return id;
}
public void setId(Long id) {
public void setId(String id) {
this.id = id;
}

View File

@ -10,7 +10,7 @@ import java.time.LocalDateTime;
*/
public class TenantDTO {
private Long id;
private String id;
@NotBlank(message = "租户编码不能为空")
@Size(max = 50, message = "租户编码长度不能超过50")
@ -41,11 +41,11 @@ public class TenantDTO {
@Size(max = 500, message = "备注长度不能超过500")
private String remark;
public Long getId() {
public String getId() {
return id;
}
public void setId(Long id) {
public void setId(String id) {
this.id = id;
}

View File

@ -9,7 +9,7 @@ import jakarta.validation.constraints.Size;
*/
public class UserDTO {
private Long id;
private String id;
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 50, message = "用户名长度必须在3-50个字符之间")
@ -28,7 +28,7 @@ public class UserDTO {
@Pattern(regexp = "^$|^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", message = "邮箱格式不正确")
private String email;
private Long deptId;
private String deptId;
private Integer status;
@ -36,11 +36,11 @@ public class UserDTO {
private String remark;
public Long getId() {
public String getId() {
return id;
}
public void setId(Long id) {
public void setId(String id) {
this.id = id;
}
@ -84,11 +84,11 @@ public class UserDTO {
this.email = email;
}
public Long getDeptId() {
public String getDeptId() {
return deptId;
}
public void setDeptId(Long deptId) {
public void setDeptId(String deptId) {
this.deptId = deptId;
}

View File

@ -17,15 +17,15 @@ public interface AuthService {
/**
* 用户登出
*/
void logout(Long userId);
void logout(String userId);
/**
* 刷新Token
*/
LoginVO refreshToken(Long userId);
LoginVO refreshToken(String userId);
/**
* 获取当前用户信息
*/
UserVO getUserInfo(Long userId);
UserVO getUserInfo(String userId);
}

View File

@ -15,7 +15,7 @@ public interface ConfigService {
/**
* 创建参数
*/
Long createConfig(ConfigDTO dto);
String createConfig(ConfigDTO dto);
/**
* 更新参数
@ -25,7 +25,7 @@ public interface ConfigService {
/**
* 根据ID查询参数
*/
ConfigVO getConfigById(Long id);
ConfigVO getConfigById(String id);
/**
* 根据参数键查询值
@ -50,7 +50,7 @@ public interface ConfigService {
/**
* 删除参数
*/
boolean deleteConfig(Long id);
boolean deleteConfig(String id);
/**
* 批量更新参数值

View File

@ -10,17 +10,17 @@ import java.util.List;
*/
public interface DeptService {
Long createDept(DeptDTO dto);
String createDept(DeptDTO dto);
boolean updateDept(DeptDTO dto);
DeptVO getDeptById(Long id);
DeptVO getDeptById(String id);
List<DeptVO> getDeptTree();
List<DeptVO> listAllDepts();
boolean deleteDept(Long id);
boolean deleteDept(String id);
boolean updateStatus(Long id, Integer status);
boolean updateStatus(String id, Integer status);
}

View File

@ -13,7 +13,7 @@ public interface MenuService {
/**
* 创建菜单
*/
Long createMenu(MenuDTO dto);
String createMenu(MenuDTO dto);
/**
* 更新菜单
@ -23,7 +23,7 @@ public interface MenuService {
/**
* 根据ID查询菜单
*/
MenuVO getMenuById(Long id);
MenuVO getMenuById(String id);
/**
* 查询菜单树
@ -33,15 +33,15 @@ public interface MenuService {
/**
* 查询用户菜单树
*/
List<MenuVO> getUserMenuTree(Long userId);
List<MenuVO> getUserMenuTree(String userId);
/**
* 删除菜单
*/
boolean deleteMenu(Long id);
boolean deleteMenu(String id);
/**
* 获取用户权限标识列表
*/
List<String> getUserPermissions(Long userId);
List<String> getUserPermissions(String userId);
}

View File

@ -14,7 +14,7 @@ public interface RoleService {
/**
* 创建角色
*/
Long createRole(RoleDTO dto);
String createRole(RoleDTO dto);
/**
* 更新角色
@ -24,7 +24,7 @@ public interface RoleService {
/**
* 根据ID查询角色
*/
RoleVO getRoleById(Long id);
RoleVO getRoleById(String id);
/**
* 分页查询角色
@ -39,20 +39,20 @@ public interface RoleService {
/**
* 删除角色
*/
boolean deleteRole(Long id);
boolean deleteRole(String id);
/**
* 更新角色状态
*/
boolean updateStatus(Long id, Integer status);
boolean updateStatus(String id, Integer status);
/**
* 获取角色菜单ID列表
*/
List<Long> getRoleMenus(Long roleId);
List<String> getRoleMenus(String roleId);
/**
* 分配菜单权限
*/
boolean assignMenus(Long roleId, List<Long> menuIds);
boolean assignMenus(String roleId, List<String> menuIds);
}

View File

@ -9,15 +9,15 @@ import com.fundplatform.sys.vo.TenantVO;
*/
public interface TenantService {
Long createTenant(TenantDTO dto);
String createTenant(TenantDTO dto);
boolean updateTenant(TenantDTO dto);
TenantVO getTenantById(Long id);
TenantVO getTenantById(String id);
Page<TenantVO> pageTenants(int pageNum, int pageSize, String keyword);
boolean deleteTenant(Long id);
boolean deleteTenant(String id);
boolean updateStatus(Long id, Integer status);
boolean updateStatus(String id, Integer status);
}

View File

@ -17,7 +17,7 @@ public interface UserService {
* @param dto 用户DTO
* @return 用户ID
*/
Long createUser(UserDTO dto);
String createUser(UserDTO dto);
/**
* 更新用户
@ -33,7 +33,7 @@ public interface UserService {
* @param id 用户ID
* @return 用户VO
*/
UserVO getUserById(Long id);
UserVO getUserById(String id);
/**
* 分页查询用户
@ -45,7 +45,7 @@ public interface UserService {
* @param deptId 部门ID
* @return 分页数据
*/
Page<UserVO> pageUsers(int pageNum, int pageSize, String username, Integer status, Long deptId);
Page<UserVO> pageUsers(int pageNum, int pageSize, String username, Integer status, String deptId);
/**
* 删除用户
@ -53,7 +53,7 @@ public interface UserService {
* @param id 用户ID
* @return 是否成功
*/
boolean deleteUser(Long id);
boolean deleteUser(String id);
/**
* 批量删除用户
@ -61,7 +61,7 @@ public interface UserService {
* @param ids 用户ID列表
* @return 是否成功
*/
boolean batchDeleteUsers(Long[] ids);
boolean batchDeleteUsers(String[] ids);
/**
* 重置密码
@ -69,7 +69,7 @@ public interface UserService {
* @param id 用户ID
* @return 是否成功
*/
boolean resetPassword(Long id);
boolean resetPassword(String id);
/**
* 更新用户状态
@ -78,7 +78,7 @@ public interface UserService {
* @param status 状态
* @return 是否成功
*/
boolean updateStatus(Long id, Integer status);
boolean updateStatus(String id, Integer status);
/**
* 更新个人信息
@ -87,7 +87,7 @@ public interface UserService {
* @param dto 个人信息DTO
* @return 是否成功
*/
boolean updateProfile(Long userId, ProfileDTO dto);
boolean updateProfile(String userId, ProfileDTO dto);
/**
* 修改密码
@ -96,5 +96,5 @@ public interface UserService {
* @param dto 密码DTO
* @return 是否成功
*/
boolean updatePassword(Long userId, PasswordDTO dto);
boolean updatePassword(String userId, PasswordDTO dto);
}

View File

@ -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) {
// 查询用户
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysUser::getUsername, request.getUsername());
wrapper.eq(SysUser::getDeleted, 0);
SysUser user = userDataService.getOne(wrapper);
// 安全修复登录时尚未建立租户上下文白名单路径不经过 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);
return userDataService.getOne(wrapper);
});
if (user == null) {
log.error("登录失败 - 用户不存在username={}", request.getUsername());
log.error("登录失败 - 用户不存在username={}, tenantId={}", request.getUsername(), request.getTenantId());
throw new RuntimeException("用户名或密码错误");
}
@ -64,14 +70,14 @@ public class AuthServiceImpl implements AuthService {
}
@Override
public void logout(Long userId) {
public void logout(String userId) {
// 清除用户所有Token强制登出所有设备
// 如果只需要登出当前设备需要从前端传递token
tokenService.deleteAllUserTokens(userId);
}
@Override
public LoginVO refreshToken(Long userId) {
public LoginVO refreshToken(String userId) {
SysUser user = userDataService.getById(userId);
if (user == null) {
throw new RuntimeException("用户不存在");
@ -89,7 +95,7 @@ public class AuthServiceImpl implements AuthService {
}
@Override
public UserVO getUserInfo(Long userId) {
public UserVO getUserInfo(String userId) {
SysUser user = userDataService.getById(userId);
if (user == null) {
throw new RuntimeException("用户不存在");

View File

@ -33,7 +33,7 @@ public class ConfigServiceImpl implements ConfigService {
@Override
@Transactional(rollbackFor = Exception.class)
public Long createConfig(ConfigDTO dto) {
public String createConfig(ConfigDTO dto) {
// 检查key是否已存在
LambdaQueryWrapper<SysConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysConfig::getConfigKey, dto.getConfigKey());
@ -87,7 +87,7 @@ public class ConfigServiceImpl implements ConfigService {
}
@Override
public ConfigVO getConfigById(Long id) {
public ConfigVO getConfigById(String id) {
SysConfig config = configDataService.getById(id);
if (config == null || config.getDeleted() == 1) {
throw new RuntimeException("参数不存在");
@ -157,7 +157,7 @@ public class ConfigServiceImpl implements ConfigService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteConfig(Long id) {
public boolean deleteConfig(String id) {
SysConfig existing = configDataService.getById(id);
if (existing == null || existing.getDeleted() == 1) {
throw new RuntimeException("参数不存在");

View File

@ -34,7 +34,7 @@ public class DeptServiceImpl implements DeptService {
@Override
@Transactional(rollbackFor = Exception.class)
public Long createDept(DeptDTO dto) {
public String createDept(DeptDTO dto) {
LambdaQueryWrapper<SysDept> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysDept::getDeptCode, dto.getDeptCode());
wrapper.eq(SysDept::getDeleted, 0);
@ -43,7 +43,7 @@ public class DeptServiceImpl implements DeptService {
}
SysDept dept = new SysDept();
dept.setParentId(dto.getParentId() != null ? dto.getParentId() : 0L);
dept.setParentId(dto.getParentId() != null ? dto.getParentId() : "0");
dept.setDeptCode(dto.getDeptCode());
dept.setDeptName(dto.getDeptName());
dept.setDeptLeader(dto.getDeptLeader());
@ -88,7 +88,7 @@ public class DeptServiceImpl implements DeptService {
}
@Override
public DeptVO getDeptById(Long id) {
public DeptVO getDeptById(String id) {
SysDept dept = deptDataService.getById(id);
if (dept == null || dept.getDeleted() == 1) {
throw new RuntimeException("部门不存在");
@ -104,7 +104,7 @@ public class DeptServiceImpl implements DeptService {
wrapper.orderByAsc(SysDept::getSortOrder);
List<SysDept> depts = deptDataService.list(wrapper);
return buildDeptTree(depts, 0L);
return buildDeptTree(depts, "0");
}
@Override
@ -117,7 +117,7 @@ public class DeptServiceImpl implements DeptService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteDept(Long id) {
public boolean deleteDept(String id) {
LambdaQueryWrapper<SysDept> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysDept::getParentId, id);
wrapper.eq(SysDept::getDeleted, 0);
@ -136,7 +136,7 @@ public class DeptServiceImpl implements DeptService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateStatus(Long id, Integer status) {
public boolean updateStatus(String id, Integer status) {
LambdaUpdateWrapper<SysDept> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(SysDept::getId, id);
wrapper.set(SysDept::getStatus, status);
@ -146,10 +146,10 @@ public class DeptServiceImpl implements DeptService {
return result;
}
private List<DeptVO> buildDeptTree(List<SysDept> depts, Long parentId) {
private List<DeptVO> buildDeptTree(List<SysDept> depts, String parentId) {
List<DeptVO> tree = new ArrayList<>();
Map<Long, List<SysDept>> deptMap = depts.stream()
Map<String, List<SysDept>> deptMap = depts.stream()
.collect(Collectors.groupingBy(SysDept::getParentId));
List<SysDept> rootDepts = deptMap.getOrDefault(parentId, new ArrayList<>());

View File

@ -34,9 +34,9 @@ public class MenuServiceImpl implements MenuService {
@Override
@Transactional(rollbackFor = Exception.class)
public Long createMenu(MenuDTO dto) {
public String createMenu(MenuDTO dto) {
SysMenu menu = new SysMenu();
menu.setParentId(dto.getParentId() != null ? dto.getParentId() : 0L);
menu.setParentId(dto.getParentId() != null ? dto.getParentId() : "0");
menu.setMenuName(dto.getMenuName());
menu.setMenuType(dto.getMenuType() != null ? dto.getMenuType() : 1);
menu.setMenuPath(dto.getMenuPath());
@ -86,7 +86,7 @@ public class MenuServiceImpl implements MenuService {
}
@Override
public MenuVO getMenuById(Long id) {
public MenuVO getMenuById(String id) {
SysMenu menu = menuDataService.getById(id);
if (menu == null || menu.getDeleted() == 1) {
throw new RuntimeException("菜单不存在");
@ -102,18 +102,18 @@ public class MenuServiceImpl implements MenuService {
wrapper.orderByAsc(SysMenu::getSortOrder);
List<SysMenu> menus = menuDataService.list(wrapper);
return buildMenuTree(menus, 0L);
return buildMenuTree(menus, "0");
}
@Override
public List<MenuVO> getUserMenuTree(Long userId) {
public List<MenuVO> getUserMenuTree(String userId) {
// TODO: 根据用户角色查询菜单
return getMenuTree();
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteMenu(Long id) {
public boolean deleteMenu(String id) {
// 检查是否有子菜单
LambdaQueryWrapper<SysMenu> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysMenu::getParentId, id);
@ -132,7 +132,7 @@ public class MenuServiceImpl implements MenuService {
}
@Override
public List<String> getUserPermissions(Long userId) {
public List<String> getUserPermissions(String userId) {
// 查询所有启用的菜单/按钮
LambdaQueryWrapper<SysMenu> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysMenu::getDeleted, 0);
@ -150,10 +150,10 @@ public class MenuServiceImpl implements MenuService {
.collect(Collectors.toList());
}
private List<MenuVO> buildMenuTree(List<SysMenu> menus, Long parentId) {
private List<MenuVO> buildMenuTree(List<SysMenu> menus, String parentId) {
List<MenuVO> tree = new ArrayList<>();
Map<Long, List<SysMenu>> menuMap = menus.stream()
Map<String, List<SysMenu>> menuMap = menus.stream()
.collect(Collectors.groupingBy(SysMenu::getParentId));
List<SysMenu> rootMenus = menuMap.getOrDefault(parentId, new ArrayList<>());

View File

@ -33,7 +33,7 @@ public class RoleServiceImpl implements RoleService {
@Override
@Transactional(rollbackFor = Exception.class)
public Long createRole(RoleDTO dto) {
public String createRole(RoleDTO dto) {
// 检查角色编码是否存在
LambdaQueryWrapper<SysRole> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysRole::getRoleCode, dto.getRoleCode());
@ -82,7 +82,7 @@ public class RoleServiceImpl implements RoleService {
}
@Override
public RoleVO getRoleById(Long id) {
public RoleVO getRoleById(String id) {
SysRole role = roleDataService.getById(id);
if (role == null || role.getDeleted() == 1) {
throw new RuntimeException("角色不存在");
@ -122,7 +122,7 @@ public class RoleServiceImpl implements RoleService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteRole(Long id) {
public boolean deleteRole(String id) {
LambdaUpdateWrapper<SysRole> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(SysRole::getId, id);
wrapper.set(SysRole::getDeleted, 1);
@ -134,7 +134,7 @@ public class RoleServiceImpl implements RoleService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateStatus(Long id, Integer status) {
public boolean updateStatus(String id, Integer status) {
LambdaUpdateWrapper<SysRole> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(SysRole::getId, id);
wrapper.set(SysRole::getStatus, status);
@ -145,7 +145,7 @@ public class RoleServiceImpl implements RoleService {
}
@Override
public List<Long> getRoleMenus(Long roleId) {
public List<String> getRoleMenus(String roleId) {
// TODO: 从sys_role_menu表查询角色关联的菜单ID
// 目前返回空列表
log.info("获取角色菜单: roleId={}", roleId);
@ -154,7 +154,7 @@ public class RoleServiceImpl implements RoleService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean assignMenus(Long roleId, List<Long> menuIds) {
public boolean assignMenus(String roleId, List<String> menuIds) {
// TODO: 实现角色菜单关联
log.info("分配角色菜单: roleId={}, menuIds={}", roleId, menuIds);
return true;

View File

@ -30,7 +30,7 @@ public class TenantServiceImpl implements TenantService {
@Override
@Transactional(rollbackFor = Exception.class)
public Long createTenant(TenantDTO dto) {
public String createTenant(TenantDTO dto) {
// 检查编码是否重复
LambdaQueryWrapper<SysTenant> checkWrapper = new LambdaQueryWrapper<>();
checkWrapper.eq(SysTenant::getTenantCode, dto.getTenantCode());
@ -101,7 +101,7 @@ public class TenantServiceImpl implements TenantService {
}
@Override
public TenantVO getTenantById(Long id) {
public TenantVO getTenantById(String id) {
SysTenant tenant = tenantDataService.getById(id);
if (tenant == null || tenant.getDeleted() == 1) {
throw new RuntimeException("租户不存在");
@ -134,7 +134,7 @@ public class TenantServiceImpl implements TenantService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteTenant(Long id) {
public boolean deleteTenant(String id) {
SysTenant existing = tenantDataService.getById(id);
if (existing == null || existing.getDeleted() == 1) {
throw new RuntimeException("租户不存在");
@ -156,7 +156,7 @@ public class TenantServiceImpl implements TenantService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateStatus(Long id, Integer status) {
public boolean updateStatus(String id, Integer status) {
SysTenant existing = tenantDataService.getById(id);
if (existing == null || existing.getDeleted() == 1) {
throw new RuntimeException("租户不存在");

View File

@ -38,7 +38,7 @@ public class UserServiceImpl implements UserService {
@Override
@Transactional(rollbackFor = Exception.class)
public Long createUser(UserDTO dto) {
public String createUser(UserDTO dto) {
// 检查用户名是否存在
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysUser::getUsername, dto.getUsername());
@ -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());
@ -120,7 +115,7 @@ public class UserServiceImpl implements UserService {
}
@Override
public UserVO getUserById(Long id) {
public UserVO getUserById(String id) {
SysUser user = userDataService.getById(id);
if (user == null || user.getDeleted() == 1) {
throw new RuntimeException("用户不存在");
@ -129,7 +124,7 @@ public class UserServiceImpl implements UserService {
}
@Override
public Page<UserVO> pageUsers(int pageNum, int pageSize, String username, Integer status, Long deptId) {
public Page<UserVO> pageUsers(int pageNum, int pageSize, String username, Integer status, String deptId) {
Page<SysUser> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysUser::getDeleted, 0);
@ -155,7 +150,7 @@ public class UserServiceImpl implements UserService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteUser(Long id) {
public boolean deleteUser(String id) {
LambdaUpdateWrapper<SysUser> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(SysUser::getId, id);
wrapper.set(SysUser::getDeleted, 1);
@ -167,7 +162,7 @@ public class UserServiceImpl implements UserService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean batchDeleteUsers(Long[] ids) {
public boolean batchDeleteUsers(String[] ids) {
if (ids == null || ids.length == 0) {
return false;
}
@ -182,7 +177,7 @@ public class UserServiceImpl implements UserService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean resetPassword(Long id) {
public boolean resetPassword(String id) {
String defaultPassword = "123456";
String md5Password = Md5Util.encrypt(defaultPassword);
log.info("重置用户密码 - userId={}, 原始密码:{}, MD5: {}", id, defaultPassword, md5Password);
@ -197,7 +192,7 @@ public class UserServiceImpl implements UserService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateStatus(Long id, Integer status) {
public boolean updateStatus(String id, Integer status) {
LambdaUpdateWrapper<SysUser> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(SysUser::getId, id);
wrapper.set(SysUser::getStatus, status);
@ -209,7 +204,7 @@ public class UserServiceImpl implements UserService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateProfile(Long userId, ProfileDTO dto) {
public boolean updateProfile(String userId, ProfileDTO dto) {
SysUser user = userDataService.getById(userId);
if (user == null || user.getDeleted() == 1) {
throw new RuntimeException("用户不存在");
@ -232,7 +227,7 @@ public class UserServiceImpl implements UserService {
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updatePassword(Long userId, PasswordDTO dto) {
public boolean updatePassword(String userId, PasswordDTO dto) {
// 验证新密码和确认密码一致
if (!dto.getNewPassword().equals(dto.getConfirmPassword())) {
throw new RuntimeException("新密码和确认密码不一致");
@ -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);

View File

@ -7,7 +7,7 @@ import java.time.LocalDateTime;
*/
public class ConfigVO {
private Long id;
private String id;
private String configKey;
private String configValue;
private String configType;
@ -20,11 +20,11 @@ public class ConfigVO {
private LocalDateTime createdTime;
private LocalDateTime updatedTime;
public Long getId() {
public String getId() {
return id;
}
public void setId(Long id) {
public void setId(String id) {
this.id = id;
}

View File

@ -8,8 +8,8 @@ import java.util.List;
*/
public class DeptVO {
private Long id;
private Long parentId;
private String id;
private String parentId;
private String parentName;
private String deptCode;
private String deptName;
@ -18,26 +18,26 @@ public class DeptVO {
private String email;
private Integer sortOrder;
private Integer status;
private Long tenantId;
private String tenantId;
private LocalDateTime createdTime;
private LocalDateTime updatedTime;
// 子部门
private List<DeptVO> children;
public Long getId() {
public String getId() {
return id;
}
public void setId(Long id) {
public void setId(String id) {
this.id = id;
}
public Long getParentId() {
public String getParentId() {
return parentId;
}
public void setParentId(Long parentId) {
public void setParentId(String parentId) {
this.parentId = parentId;
}
@ -105,11 +105,11 @@ public class DeptVO {
this.status = status;
}
public Long getTenantId() {
public String getTenantId() {
return tenantId;
}
public void setTenantId(Long tenantId) {
public void setTenantId(String tenantId) {
this.tenantId = tenantId;
}

Some files were not shown because too many files have changed in this diff Show More