整理docs目录并补充文档规范

This commit is contained in:
wkc
2026-03-17 15:06:59 +08:00
parent 9cb77b096e
commit 2fd93463b8
111 changed files with 706 additions and 100 deletions

View File

@@ -1,586 +0,0 @@
# 项目管理页面改进设计文档
**日期:** 2026-02-27
**作者:** Claude Code
**状态:** 待实施
---
## 一、需求概述
项目管理页面存在以下三个问题需要改进:
1. **搜索框缺少搜索按钮** - 用户只能通过回车或清空触发搜索,缺少显式的搜索按钮
2. **标签页状态计数不准确** - 当前只统计当前页的数据,无法反映全局状态分布
3. **状态标签样式不够简约** - 当前使用带背景色的标签,视觉上较重
---
## 二、技术方案
### 2.1 方案选择
经过对比分析,选择 **方案1独立统计接口 + 精准样式改造**
**理由:**
- 接口职责单一,易于维护
- 统计数据准确,不受分页影响
- 性能最优统计数据量小仅4个数字
- 符合 RESTful 设计规范
---
## 三、后端设计
### 3.1 新增统计接口
**接口定义**
```
GET /ccdi/project/statusCounts
```
**权限要求**
```
ccdi:project:list
```
**Controller 层实现**
文件:`ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectController.java`
```java
/**
* 获取项目状态统计
*/
@GetMapping("/statusCounts")
@Operation(summary = "获取项目状态统计")
@PreAuthorize("@ss.hasPermi('ccdi:project:list')")
public AjaxResult getStatusCounts() {
Map<String, Long> counts = projectService.getStatusCounts();
return AjaxResult.success(counts);
}
```
**Service 层接口**
文件:`ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectService.java`
```java
/**
* 获取各状态的项目数量
* @return 返回格式:{"all": 总数, "0": 进行中数量, "1": 已完成数量, "2": 已归档数量}
*/
Map<String, Long> getStatusCounts();
```
**Service 层实现**
文件:`ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java`
```java
@Override
public Map<String, Long> getStatusCounts() {
Map<String, Long> counts = new HashMap<>();
// 使用 MyBatis Plus 分组查询
QueryWrapper<CcdiProject> wrapper = new QueryWrapper<>();
wrapper.select("status", "COUNT(*) as count")
.groupBy("status");
List<Map<String, Object>> results = baseMapper.selectMaps(wrapper);
// 初始化各状态计数
Long totalCount = 0L;
Long inProgressCount = 0L;
Long completedCount = 0L;
Long archivedCount = 0L;
// 遍历结果统计
for (Map<String, Object> result : results) {
String status = (String) result.get("status");
Long count = (Long) result.get("count");
totalCount += count;
if ("0".equals(status)) {
inProgressCount = count;
} else if ("1".equals(status)) {
completedCount = count;
} else if ("2".equals(status)) {
archivedCount = count;
}
}
counts.put("all", totalCount);
counts.put("0", inProgressCount);
counts.put("1", completedCount);
counts.put("2", archivedCount);
return counts;
}
```
**响应示例**
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"all": 15,
"0": 8,
"1": 5,
"2": 2
}
}
```
---
## 四、前端设计
### 4.1 API 接口定义
**文件:** `ruoyi-ui/src/api/ccdiProject.js`
```javascript
// 获取项目状态统计
export function getProjectStatusCounts() {
return request({
url: '/ccdi/project/statusCounts',
method: 'get'
})
}
```
### 4.2 SearchBar 组件改造
**文件:** `ruoyi-ui/src/views/ccdiProject/components/SearchBar.vue`
**改动说明:**
- 使用 Element UI 的 slot 功能在输入框右侧添加搜索按钮
- 保持回车和清空触发搜索的功能
- 调整输入框宽度以容纳按钮
**实现代码:**
```vue
<el-input
v-model="searchKeyword"
placeholder="请输入关键词搜索项目"
clearable
size="small"
class="search-input"
@keyup.enter.native="handleSearch"
@clear="handleSearch"
>
<el-button
slot="append"
icon="el-icon-search"
@click="handleSearch"
/>
</el-input>
```
**样式调整:**
```scss
.search-input {
width: 300px; // 从 240px 调整为 300px
height: 40px;
}
```
### 4.3 ProjectTable 组件改造
**文件:** `ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue`
**改动说明:**
- 替换 `<el-tag>` + `<dict-tag>` 组合为简约的小圆点样式
- 使用不同颜色的小圆点标识不同状态
- 文字统一使用黑色,降低视觉干扰
**实现代码:**
```vue
<!-- 状态列 -->
<el-table-column
prop="status"
label="状态"
width="120"
align="center"
>
<template slot-scope="scope">
<span class="status-badge" :class="'status-' + scope.row.status">
<span class="status-dot"></span>
<span class="status-text">{{ getStatusText(scope.row.status) }}</span>
</span>
</template>
</el-table-column>
```
**新增方法:**
```javascript
getStatusText(status) {
const statusMap = {
'0': '进行中',
'1': '已完成',
'2': '已归档'
}
return statusMap[status] || '未知'
}
```
**移除方法:**
```javascript
// 删除不再使用的 getStatusType 方法
```
**样式设计:**
```scss
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.status-text {
color: #333;
font-size: 14px;
}
// 进行中 - 蓝色
&.status-0 .status-dot {
background-color: #1890ff;
}
// 已完成 - 绿色
&.status-1 .status-dot {
background-color: #52c41a;
}
// 已归档 - 灰色
&.status-2 .status-dot {
background-color: #8c8c8c;
}
}
```
### 4.4 主页面集成
**文件:** `ruoyi-ui/src/views/ccdiProject/index.vue`
**改动说明:**
- 引入统计接口
- created 生命周期并发加载统计和列表数据
- 状态变更后同时刷新统计和列表
- 优雅的错误处理
**引入接口:**
```javascript
import { listProject, getProjectStatusCounts } from '@/api/ccdiProject'
```
**新增方法:**
```javascript
/** 获取状态统计 */
getStatusCounts() {
return getProjectStatusCounts().then(response => {
this.tabCounts = response.data
}).catch(() => {
// 统计接口失败时使用默认值,不影响列表显示
this.tabCounts = {
all: 0,
'0': 0,
'1': 0,
'2': 0
}
})
}
```
**修改 created 生命周期:**
```javascript
created() {
// 并发加载统计和列表
Promise.all([
this.getStatusCounts(),
this.getList()
])
}
```
**优化 calculateTabCounts 方法:**
```javascript
/** 计算标签页数量 - 改为从统计接口获取 */
calculateTabCounts() {
// 方法保留但不执行计算逻辑
// 统计数据已从接口获取,直接使用 this.tabCounts
}
```
**修改状态变更处理:**
```javascript
/** 确认归档 */
handleConfirmArchive(data) {
console.log('确认归档:', data)
this.$modal.msgSuccess('项目已归档')
this.archiveDialogVisible = false
// 同时刷新统计和列表
Promise.all([
this.getStatusCounts(),
this.getList()
])
}
/** 提交项目表单 */
handleSubmitProject(data) {
this.addDialogVisible = false
// 同时刷新统计和列表
Promise.all([
this.getStatusCounts(),
this.getList()
])
}
```
**优化 getList 方法:**
```javascript
/** 查询项目列表 */
getList() {
this.loading = true
listProject(this.queryParams).then(response => {
this.projectList = response.rows
this.total = response.total
this.loading = false
// 不再需要调用 calculateTabCounts
}).catch(() => {
this.loading = false
this.$modal.msgError('加载项目列表失败')
})
}
```
---
## 五、数据流设计
### 5.1 数据加载时机
| 场景 | 刷新统计 | 刷新列表 | 说明 |
|------|---------|---------|------|
| 页面首次加载 | ✓ | ✓ | 并发请求 |
| 标签页切换 | - | ✓ | 仅列表变化 |
| 搜索触发 | - | ✓ | 仅列表变化 |
| 项目归档 | ✓ | ✓ | 状态变化 |
| 新建项目 | ✓ | ✓ | 总数增加 |
| 删除项目 | ✓ | ✓ | 总数减少 |
| 重新分析 | - | ✓ | 状态不变 |
### 5.2 并发加载优化
```javascript
// 使用 Promise.all 并发请求,提升加载速度
Promise.all([
this.getStatusCounts(), // 统计接口
this.getList() // 列表接口
])
```
### 5.3 错误处理策略
**统计接口失败:**
- 不阻塞页面加载
- 标签页显示 0但列表正常显示
- 控制台记录错误日志
**列表接口失败:**
- 显示错误提示
- 保持现有数据不变
- Loading 状态正常关闭
---
## 六、性能考虑
### 6.1 接口性能
**统计接口:**
- 使用 COUNT + GROUP BY无需查询详细字段
- 数据量极小仅4个数字
- 执行速度快,可在 100ms 内完成
**列表接口:**
- 保持现有分页逻辑
- 不受统计接口影响
### 6.2 前端性能
**并发请求:**
- 统计和列表同时发起,不串行等待
- 总耗时 = max(统计耗时, 列表耗时)
**缓存策略:**
- 统计数据在前端内存中保留
- 仅在状态变更时重新获取
---
## 七、测试要点
### 7.1 后端测试
**单元测试:**
- 测试统计接口返回数据格式
- 测试空数据情况(无项目时)
- 测试各状态的数据准确性
**集成测试:**
- 使用 Swagger 测试接口响应
- 验证权限控制
### 7.2 前端测试
**功能测试:**
- 搜索按钮点击触发搜索
- 回车键触发搜索
- 清空输入框触发搜索
- 标签页显示正确的统计数据
- 状态标签显示正确的颜色和文字
**UI 测试:**
- 搜索按钮样式与输入框融合
- 状态标签小圆点样式正确
- 不同状态的颜色区分明显
**错误场景测试:**
- 统计接口失败时页面正常显示
- 列表接口失败时错误提示正确
- 网络异常时的降级处理
---
## 八、改动文件清单
### 8.1 后端文件
1. `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectController.java`
- 新增统计接口方法
2. `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectService.java`
- 新增服务方法定义
3. `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java`
- 实现统计逻辑
### 8.2 前端文件
1. `ruoyi-ui/src/api/ccdiProject.js`
- 新增统计接口调用
2. `ruoyi-ui/src/views/ccdiProject/components/SearchBar.vue`
- 添加内嵌搜索按钮
- 调整输入框宽度
3. `ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue`
- 改造状态标签样式
- 新增 getStatusText 方法
- 删除 getStatusType 方法
4. `ruoyi-ui/src/views/ccdiProject/index.vue`
- 引入统计接口
- 修改 created 生命周期
- 新增 getStatusCounts 方法
- 优化状态变更处理
---
## 九、实施计划
### 9.1 实施顺序
1. **后端开发**(优先)
- 实现统计接口
- Swagger 测试验证
2. **前端开发**
- API 接口定义
- SearchBar 组件改造
- ProjectTable 组件改造
- 主页面集成
3. **联调测试**
- 前后端联调
- 功能测试
- UI 测试
### 9.2 预估工时
- 后端开发1 小时
- 前端开发2 小时
- 联调测试1 小时
- **总计4 小时**
---
## 十、风险评估
### 10.1 技术风险
**风险:** 统计接口性能问题
**影响:**
**缓解措施:**
- 使用 COUNT + GROUP BY 优化查询
- 添加数据库索引status 字段)
- 监控接口响应时间
**风险:** 前端样式兼容性
**影响:**
**缓解措施:**
- 使用标准的 CSS 属性
- 测试主流浏览器
### 10.2 业务风险
**风险:** 统计数据与实际不符
**影响:**
**缓解措施:**
- 使用数据库事务保证一致性
- 状态变更时立即刷新统计
- 添加数据校验
---
## 十一、后续优化
### 11.1 短期优化
- 添加统计数据的本地缓存5秒过期
- 优化错误提示文案
### 11.2 长期优化
- 支持按时间范围统计
- 添加趋势图表
- 支持自定义状态
---
**设计完成日期:** 2026-02-27
**预计实施日期:** 待定

View File

@@ -1,250 +0,0 @@
# 项目管理页面交互改进设计文档
**日期**: 2026-02-27
**模块**: 初核项目管理 (ccdiProject)
**作者**: Claude Code
## 概述
本文档描述了项目管理页面的三个交互改进:搜索框按钮、状态标签简约化和分页 loading 效果。
## 改进项目
### 1. 搜索框添加搜索按钮
#### 当前状态
- 搜索框只支持回车键搜索和清空按钮
- 没有明确的搜索按钮,用户可能不知道如何触发搜索
#### 改进方案
- 在输入框内右侧添加一个可点击的搜索图标按钮
- 使用 Element UI 的 `suffix` 插槽实现
- 图标使用 `el-icon-search`
- 点击图标触发 `handleSearch` 方法,与回车键效果一致
#### 实现位置
- 文件: `ruoyi-ui/src/views/ccdiProject/components/SearchBar.vue`
- 行数: 第 3-12 行el-input 组件)
#### 技术细节
```vue
<el-input
v-model="searchKeyword"
placeholder="请输入关键词搜索项目"
prefix-icon="el-icon-search"
clearable
size="small"
class="search-input"
@keyup.enter.native="handleSearch"
@clear="handleSearch"
>
<i
slot="suffix"
class="el-icon-search search-icon"
@click="handleSearch"
/>
</el-input>
```
#### 样式
```scss
.search-icon {
cursor: pointer;
color: #909399;
transition: color 0.2s;
&:hover {
color: #3B82F6;
}
}
```
---
### 2. 状态标签简约化
#### 当前状态
- 使用 `el-tag` 组件显示状态
- 有背景色:蓝色(进行中)、绿色(已完成)、灰色(已归档)
- 视觉上较为突出,占用空间较大
#### 改进方案
- 移除 `el-tag` 组件,使用自定义简约样式
- GitHub 风格标签:左侧彩色圆点 + 右侧文字
- 无背景色,无边框
- 圆点颜色:
- 进行中 (`status='0'`): 蓝色 `#1890ff`
- 已完成 (`status='1'`): 绿色 `#52c41a`
- 已归档 (`status='2'`): 灰色 `#8c8c8c`
#### 实现位置
- 文件: `ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue`
- 行数: 第 43-54 行(状态列)
#### 技术细节
```vue
<el-table-column
prop="status"
label="状态"
width="120"
align="center"
>
<template slot-scope="scope">
<div class="status-tag">
<span class="status-dot" :style="{ color: getStatusColor(scope.row.status) }"></span>
<dict-tag :options="dict.type.ccdi_project_status" :value="scope.row.status"/>
</div>
</template>
</el-table-column>
```
#### 新增方法
```javascript
getStatusColor(status) {
const colorMap = {
'0': '#1890ff', // 进行中 - 蓝色
'1': '#52c41a', // 已完成 - 绿色
'2': '#8c8c8c' // 已归档 - 灰色
}
return colorMap[status] || '#8c8c8c'
}
```
#### 样式
```scss
.status-tag {
display: inline-flex;
align-items: center;
gap: 6px;
.status-dot {
font-size: 10px;
line-height: 1;
}
}
```
---
### 3. 分页 loading 效果
#### 当前状态
- 分页切换时调用 `getList()` 方法
- `getList()` 内部会设置 `loading = true`
- `el-table``:loading="loading"` 属性绑定
- 理论上应该显示 loading 效果
#### 改进方案
- 确认 `el-table` 的 loading 属性正确绑定
- 确保分页切换时 loading 状态正确设置
- Element UI 会自动显示表格遮罩层和加载动画
#### 实现位置
- 文件: `ruoyi-ui/src/views/ccdiProject/index.vue`
- `handlePagination` 方法(第 155-161 行)
- `getList` 方法(第 122-134 行)
- 文件: `ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue`
- `el-table` 组件(第 3-7 行)
#### 技术细节
**index.vue - 分页处理**
```javascript
handlePagination(pagination) {
if (pagination) {
this.queryParams.pageNum = pagination.pageNum
this.queryParams.pageSize = pagination.pageSize
}
this.getList() // 开始加载loading = true
}
```
**index.vue - 数据加载**
```javascript
getList() {
this.loading = true // 立即显示 loading
listProject(this.queryParams).then(response => {
this.projectList = response.rows
this.total = response.total
this.loading = false // 加载完成,隐藏 loading
this.calculateTabCounts()
}).catch(() => {
this.loading = false // 加载失败,隐藏 loading
})
}
```
**ProjectTable.vue - 表格绑定**
```vue
<el-table
:data="dataList"
:loading="loading"
style="width: 100%"
>
```
#### 验证要点
- 分页切换时,表格应立即显示半透明遮罩层
- 遮罩层中央显示加载图标和"加载中..."文字
- 数据加载完成后,遮罩层自动消失
---
## 视觉效果对比
### 搜索框
- **改进前**: 只有输入框,用户不知道如何触发搜索
- **改进后**: 输入框右侧有可点击的搜索图标,鼠标悬停时变蓝色
### 状态标签
- **改进前**: 彩色背景标签,视觉突出
- **改进后**: 简约的圆点+文字,更轻量现代
### 分页 loading
- **改进前**: 分页切换时无明显反馈
- **改进后**: 表格显示 loading 遮罩,明确告知用户正在加载
---
## 兼容性
- 所有改进基于现有 Element UI 组件,无需引入新的依赖
- 保持与现有代码风格一致
- 不影响其他功能模块
---
## 测试要点
1. **搜索按钮**:
- 点击搜索图标,应触发搜索
- 图标悬停时变蓝色
- 回车键仍然有效
2. **状态标签**:
- 三种状态显示正确的圆点颜色
- 文字显示正常
- 标签对齐居中
3. **分页 loading**:
- 切换分页时,表格显示 loading
- 数据加载完成后loading 消失
- 加载失败时loading 也应消失
---
## 实施步骤
1. 修改 `SearchBar.vue`,添加搜索图标按钮
2. 修改 `ProjectTable.vue`,实现简约状态标签
3. 验证 `index.vue``ProjectTable.vue` 的 loading 绑定
4. 测试三个改进点的功能
5. 生成测试报告
---
## 文件清单
- `ruoyi-ui/src/views/ccdiProject/components/SearchBar.vue` - 搜索框改进
- `ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue` - 状态标签和 loading 验证
- `ruoyi-ui/src/views/ccdiProject/index.vue` - loading 逻辑验证

View File

@@ -1,635 +0,0 @@
# 项目管理标签页状态统计修复设计文档
## 文档信息
- **创建日期**: 2026-02-27
- **作者**: Claude Code
- **状态**: ✅ 已完成
## 问题背景
### 当前问题
项目管理页面的标签页筛选功能中,各状态的项目计数不正确。具体表现为:
- **现象**: 标签页显示的数量远小于实际总数
- **根本原因**: 前端只统计当前页的数据来计算各状态的数量
- **影响**: 用户无法了解各状态的完整项目分布情况
### 代码位置
**前端代码**:
- 页面组件: `ruoyi-ui/src/views/ccdiProject/index.vue`
- API 定义: `ruoyi-ui/src/api/ccdiProject.js`
**后端代码**:
- Controller: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectController.java`
- Service 接口: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectService.java`
- Service 实现: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java`
### 当前实现分析
**问题代码** (index.vue:136-145):
```javascript
calculateTabCounts() {
// 注意这里需要后端API返回所有状态的数量统计
// 目前暂时使用当前页的数据进行计算
this.tabCounts = {
all: this.total,
'0': this.projectList.filter(p => p.status === '0').length,
'1': this.projectList.filter(p => p.status === '1').length,
'2': this.projectList.filter(p => p.status === '2').length
}
}
```
**问题分析**:
- `projectList` 只包含当前页的数据默认10条
- 过滤计算只能得到当前页中各状态的数量
- 用户看到的是"当前页有5个进行中的项目",而非"总共有30个进行中的项目"
## 需求说明
### 功能需求
**预期行为**:
- 标签页应显示数据库中所有该状态的项目总数
- 状态统计不受搜索条件和分页影响
- 状态统计随列表一起刷新
**非功能性需求**:
- 性能: 统计查询应快速响应(< 100ms
- 准确性: 统计数字必须与数据库一致
- 实时性: 每次刷新列表时同步更新统计
## 设计方案
### 整体架构
采用**独立统计接口**方案:
- 后端新增 `/ccdi/project/statusCounts` 接口
- 前端在加载列表时并行调用统计接口
- 两个请求独立,数据互不影响
### 后端设计
#### 1. 新增 VO 类
**文件**: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectStatusCountsVO.java`
```java
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
/**
* 项目状态统计VO
*/
@Data
public class CcdiProjectStatusCountsVO {
/** 全部项目总数 */
private Long all;
/** 进行中项目数状态0 */
private Long status0;
/** 已完成项目数状态1 */
private Long status1;
/** 已归档项目数状态2 */
private Long status2;
}
```
**设计说明**:
- 使用 `Long` 类型支持大数据量
- 字段命名清晰,便于前端使用
- 不包含搜索条件,始终统计全量数据
#### 2. Service 层
**接口定义** (`ICcdiProjectService.java`):
```java
/**
* 查询各状态的项目总数(不受搜索条件影响)
* @return 状态统计
*/
CcdiProjectStatusCountsVO getStatusCounts();
```
**实现类** (`CcdiProjectServiceImpl.java`):
```java
@Override
public CcdiProjectStatusCountsVO getStatusCounts() {
CcdiProjectStatusCountsVO vo = new CcdiProjectStatusCountsVO();
// 统计全部项目
vo.setAll(ccdiProjectMapper.selectCount(null));
// 统计各状态项目
vo.setStatus0(ccdiProjectMapper.selectCount(
new LambdaQueryWrapper<CcdiProject>()
.eq(CcdiProject::getStatus, "0")
));
vo.setStatus1(ccdiProjectMapper.selectCount(
new LambdaQueryWrapper<CcdiProject>()
.eq(CcdiProject::getStatus, "1")
));
vo.setStatus2(ccdiProjectMapper.selectCount(
new LambdaQueryWrapper<CcdiProject>()
.eq(CcdiProject::getStatus, "2")
));
return vo;
}
```
**实现要点**:
- 使用 MyBatis Plus 的 `selectCount` 方法
- 不添加任何过滤条件
- 可以优化为一次查询返回所有统计(使用 GROUP BY
**优化方案**(可选):
如果性能要求高,可以改为单次查询:
```java
@Override
public CcdiProjectStatusCountsVO getStatusCounts() {
// 查询各状态的统计
List<Map<String, Object>> counts = ccdiProjectMapper.countGroupByStatus();
CcdiProjectStatusCountsVO vo = new CcdiProjectStatusCountsVO();
vo.setAll(0L);
vo.setStatus0(0L);
vo.setStatus1(0L);
vo.setStatus2(0L);
for (Map<String, Object> item : counts) {
String status = (String) item.get("status");
Long count = (Long) item.get("count");
vo.setAll(vo.getAll() + count);
if ("0".equals(status)) {
vo.setStatus0(count);
} else if ("1".equals(status)) {
vo.setStatus1(count);
} else if ("2".equals(status)) {
vo.setStatus2(count);
}
}
return vo;
}
```
Mapper 方法:
```java
List<Map<String, Object>> countGroupByStatus();
```
XML SQL:
```xml
<select id="countGroupByStatus" resultType="map">
SELECT
status,
COUNT(*) as count
FROM ccdi_project
GROUP BY status
</select>
```
#### 3. Controller 层
**文件**: `CcdiProjectController.java`
```java
/**
* 查询项目状态统计
*/
@GetMapping("/statusCounts")
@Operation(summary = "查询项目状态统计")
@PreAuthorize("@ss.hasPermi('ccdi:project:list')")
public AjaxResult getStatusCounts() {
CcdiProjectStatusCountsVO counts = projectService.getStatusCounts();
return AjaxResult.success(counts);
}
```
**设计说明**:
- 不接收任何查询参数
- 使用与列表接口相同的权限注解
- 返回标准的 AjaxResult 格式
### 前端设计
#### 1. API 层
**文件**: `ruoyi-ui/src/api/ccdiProject.js`
```javascript
// 查询项目状态统计
export function getStatusCounts() {
return request({
url: '/ccdi/project/statusCounts',
method: 'get'
})
}
```
#### 2. 页面组件改造
**文件**: `ruoyi-ui/src/views/ccdiProject/index.vue`
**修改点 1**: 导入新 API
```javascript
import { listProject, getStatusCounts } from '@/api/ccdiProject'
```
**修改点 2**: 重构 `getList()` 方法
```javascript
/** 查询项目列表 */
getList() {
this.loading = true
// 并行请求列表数据和状态统计
Promise.all([
listProject(this.queryParams),
getStatusCounts()
]).then(([listResponse, countsResponse]) => {
// 处理列表数据
this.projectList = listResponse.rows
this.total = listResponse.total
// 处理状态统计
const counts = countsResponse.data
this.tabCounts = {
all: counts.all,
'0': counts.status0,
'1': counts.status1,
'2': counts.status2
}
this.loading = false
}).catch(() => {
this.loading = false
})
}
```
**修改点 3**: 删除旧方法
删除 `calculateTabCounts()` 方法及其调用(第 136-145 行)。
**设计说明**:
- 使用 `Promise.all` 并行请求,提高性能
- 两个请求独立失败互不影响(可以优化错误处理)
- 状态统计使用后端返回的准确数据
### 数据流设计
```
用户操作(打开页面/搜索/切换标签/翻页)
前端调用 getList() 方法
并行发送两个请求:
┌─────────────────────────┬────────────────────────┐
│ listProject(queryParams)│ getStatusCounts() │
│ - 带搜索条件 │ - 无参数 │
│ - 带分页参数 │ - 返回全量统计 │
│ - 返回当前页数据 │ │
└─────────────────────────┴────────────────────────┘
↓ ↓
后端处理 后端处理
- 根据条件过滤 - COUNT 查询全部
- 分页返回结果 - 按 status 分组统计
↓ ↓
前端接收响应 ←─────────────────┘
更新状态:
- projectList: 列表数据(受搜索/分页影响)
- total: 当前筛选条件的总数
- tabCounts: 各状态的完整统计(不受搜索影响)
页面渲染:
- 表格: 显示当前页项目
- 标签: 显示固定统计数字
```
**关键特性**:
1. **并行请求**: 两个请求同时发出,不阻塞
2. **数据独立**: 列表数据和统计数据来源不同
3. **统计固定**: 标签页数字不随搜索/分页变化
4. **列表过滤**: 表格内容根据搜索条件正确过滤
## 测试方案
### 后端测试
#### 1. 单元测试
**测试文件**: `CcdiProjectServiceTest.java`
```java
@Test
void testGetStatusCounts() {
// 准备测试数据
// 创建 3 个进行中项目
// 创建 2 个已完成项目
// 创建 1 个已归档项目
// 执行测试
CcdiProjectStatusCountsVO result = projectService.getStatusCounts();
// 验证结果
assertEquals(6L, result.getAll());
assertEquals(3L, result.getStatus0());
assertEquals(2L, result.getStatus1());
assertEquals(1L, result.getStatus2());
}
```
#### 2. 接口测试
**使用 Swagger UI 测试**:
1. 访问: `http://localhost:8080/swagger-ui/index.html`
2. 找到: 纪检初核项目管理 → GET /ccdi/project/statusCounts
3. 点击 "Try it out" → "Execute"
**预期响应**:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"all": 100,
"status0": 30,
"status1": 50,
"status2": 20
}
}
```
**测试用例**:
| 用例编号 | 场景 | 预期结果 |
|---------|------|---------|
| TC01 | 数据库无项目 | all=0, status0=0, status1=0, status2=0 |
| TC02 | 只有进行中项目 | all=N, status0=N, status1=0, status2=0 |
| TC03 | 混合状态项目 | 各状态数字之和等于 all |
| TC04 | 大数据量1000+ | 查询时间 < 100ms |
### 前端测试
#### 1. 页面加载测试
**步骤**:
1. 打开项目管理页面
2. 观察标签页数字
**预期结果**:
- 标签页显示正确的总数(例如:全部项目(100)
- 数字不随分页变化
#### 2. 搜索测试
**步骤**:
1. 在搜索框输入项目名称
2. 点击搜索
**预期结果**:
- 列表正确过滤
- 标签页数字保持不变(显示总数)
#### 3. 分页测试
**步骤**:
1. 切换到第 2 页
**预期结果**:
- 列表切换到第 2 页
- 标签页数字保持不变
#### 4. 状态切换测试
**步骤**:
1. 点击"进行中"标签
**预期结果**:
- 列表只显示进行中的项目
- 标签页数字保持不变(仍显示总数)
#### 5. 网络错误测试
**步骤**:
1. 模拟统计接口失败
**预期结果**:
- 列表正常显示
- 标签页显示 0 或保持上次数据
- 不阻塞用户操作
### 集成测试
**端到端测试流程**:
1. **准备数据**: 在数据库中插入测试项目
- 10 个进行中项目
- 15 个已完成项目
- 5 个已归档项目
2. **执行测试**:
```
a. 打开项目管理页面
b. 验证标签显示: 全部(30), 进行中(10), 已完成(15), 已归档(5)
c. 搜索项目名称(匹配 5 个)
d. 验证列表显示 5 个项目
e. 验证标签仍显示: 全部(30), 进行中(10), 已完成(15), 已归档(5)
f. 切换到第 2 页
g. 验证列表切换,标签数字不变
h. 点击"进行中"标签
i. 验证列表只显示进行中项目
j. 验证标签仍显示: 全部(30), 进行中(10), 已完成(15), 已归档(5)
```
3. **预期结果**: 所有验证点通过
## 性能考虑
### 后端性能
**统计查询优化**:
- 使用 `COUNT(*)` 查询,性能较好
- 考虑为 `status` 字段添加索引(如果项目数量 > 10000
- 单次 GROUP BY 查询优于多次 COUNT 查询
**预估性能**:
- 项目数 < 1000: 响应时间 < 50ms
- 项目数 1000-10000: 响应时间 50-100ms
- 项目数 > 10000: 考虑添加缓存或索引
### 前端性能
**并行请求**:
- `Promise.all` 同时发起两个请求
- 总等待时间 = max(listTime, countsTime)
- 不影响用户体验
**请求频率**:
- 仅在页面加载、搜索、翻页时请求
- 不会产生过多网络流量
## 风险与应对
### 风险 1: 统计数据不一致
**场景**: 用户在查看页面时,后台数据被其他用户修改
**影响**: 标签页数字与实际不符
**应对方案**:
- 轻微问题,可接受
- 用户刷新页面后同步
- 不需要额外处理
### 风险 2: 统计接口失败
**场景**: 统计接口异常或超时
**影响**: 标签页显示 0 或空白
**应对方案**:
- 前端增加错误处理
- 列表接口不受影响
- 控制台记录错误日志
### 风险 3: 大数据量性能
**场景**: 项目数量超过 10000
**影响**: 统计查询变慢
**应对方案**:
- 为 status 字段添加索引
- 使用单次 GROUP BY 查询
- 考虑添加缓存Redis
## 实施计划
### 后端开发 (30 分钟)
1. 创建 VO 类 (5 分钟)
- 新建 `CcdiProjectStatusCountsVO.java`
- 添加字段和 Lombok 注解
2. Service 层开发 (15 分钟)
- 接口添加方法声明
- 实现类编写统计逻辑
- (可选) Mapper 添加 GROUP BY 查询
3. Controller 层开发 (5 分钟)
- 添加 `/statusCounts` 接口
- 添加 Swagger 注释
4. 本地测试 (5 分钟)
- 启动项目
- 使用 Swagger 测试接口
### 前端开发 (20 分钟)
1. API 层修改 (5 分钟)
- 添加 `getStatusCounts()` 函数
2. 页面组件修改 (10 分钟)
- 导入新 API
- 修改 `getList()` 方法
- 删除 `calculateTabCounts()` 方法
3. 本地测试 (5 分钟)
- 启动前端
- 验证功能
### 测试验证 (15 分钟)
1. 后端接口测试 (5 分钟)
- Swagger 测试各场景
2. 前端功能测试 (10 分钟)
- 执行测试方案中的所有用例
### 总计时间: 65 分钟
## 验收标准
### 功能验收
- [x] 后端 `/statusCounts` 接口返回正确的统计数字
- [x] 前端标签页显示数据库中的完整统计
- [x] 搜索不影响标签页统计数字
- [x] 分页不影响标签页统计数字
- [x] 状态过滤不影响标签页统计数字
### 性能验收
- [x] 统计接口响应时间 < 100ms
- [x] 页面加载时间无明显增加
### 代码质量
- [x] 后端代码符合项目规范
- [x] 前端代码符合项目规范
- [x] 添加必要的注释和文档
## 后续优化
### 短期优化 (可选)
1. **错误处理增强**
- 前端对统计接口失败进行友好提示
- 后端添加异常捕获和日志
2. **性能优化**
- 使用单次 GROUP BY 查询替代多次 COUNT
- 为 status 字段添加索引
### 长期优化 (可选)
1. **缓存机制**
- 使用 Redis 缓存统计结果
- 设置 5 分钟过期时间
- 项目变更时清除缓存
2. **实时更新**
- 使用 WebSocket 推送统计更新
- 减少轮询请求
## 附录
### 相关文件清单
**后端新增文件**:
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectStatusCountsVO.java`
**后端修改文件**:
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectService.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectController.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectMapper.java` (可选)
- `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectMapper.xml` (可选)
**前端修改文件**:
- `ruoyi-ui/src/api/ccdiProject.js`
- `ruoyi-ui/src/views/ccdiProject/index.vue`
### 参考资料
- MyBatis Plus 官方文档: https://baomidou.com/
- Element UI 标签页组件: https://element.eleme.cn/#/zh-CN/component/tabs
- Promise.all 文档: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/all

View File

@@ -1,282 +0,0 @@
# 数据库迁移设计文档
## 概述
将 CCDI 纪检初核系统的开发环境数据库完整迁移到生产环境,包括所有表结构和数据的导出与导入。
## 需求分析
### 迁移目标
- **源数据库**: 116.62.17.81:3306/ccdi
- **目标环境**: 全新的生产数据库(空库)
- **迁移范围**: 所有表的结构和数据
### 关键要求
1. 表结构和数据分离导出(两个独立文件)
2. 只导出表,不包括视图、存储过程、触发器等
3. 完整导出所有数据,不需要脱敏
4. 确保字符集正确,避免乱码问题
5. 使用 mysqldump 命令导出
6. 提供自动化脚本简化操作
## 技术方案
### 导出工具
使用 MySQL 官方工具 `mysqldump` 进行导出,优势:
- 标准化工具,兼容性最佳
- 性能优秀,适合大数据库
- 生成的 SQL 文件通用性强
### 字符集处理
- **字符集**: utf8mb4支持完整 Unicode包括 emoji
- **排序规则**: utf8mb4_general_ci
- **客户端字符集**: utf8mb4
关键措施:
1. mysqldump 命令添加 `--default-character-set=utf8mb4` 参数
2. SQL 文件头部添加字符集声明语句
3. 导入时指定字符集参数
4. 导入后验证中文数据正确性
## 导出设计
### 文件组织
```
ccdi/
├── export_database.sh # 自动化脚本
├── db_config.conf.template # 配置模板
├── db_config.conf # 实际配置(不纳入版本控制)
└── doc/
└── database/
└── backup/
├── ccdi_structure.sql # 表结构文件
├── ccdi_data.sql # 数据文件
└── export_guide.md # 操作指南
```
### 表结构导出命令
```bash
mysqldump -h 116.62.17.81 -P 3306 -u root -p \
--no-data \
--skip-triggers \
--skip-add-drop-table \
--default-character-set=utf8mb4 \
--single-transaction \
ccdi > ccdi_structure.sql
```
**参数说明**:
- `--no-data`: 只导出表结构,不导出数据
- `--skip-triggers`: 跳过触发器
- `--skip-add-drop-table`: 不添加 DROP TABLE 语句(避免误删)
- `--default-character-set=utf8mb4`: 指定字符集
- `--single-transaction`: InnoDB 表一致性导出,不锁表
### 数据导出命令
```bash
mysqldump -h 116.62.17.81 -P 3306 -u root -p \
--no-create-info \
--skip-triggers \
--default-character-set=utf8mb4 \
--single-transaction \
--complete-insert \
--extended-insert \
ccdi > ccdi_data.sql
```
**参数说明**:
- `--no-create-info`: 只导出数据,不导出表结构
- `--complete-insert`: INSERT 语句包含列名
- `--extended-insert`: 使用多行 INSERT提高导入效率
### SQL 文件字符集声明
```sql
SET NAMES utf8mb4;
SET CHARACTER SET utf8mb4;
SET GLOBAL character_set_client=utf8mb4;
SET GLOBAL character_set_connection=utf8mb4;
SET GLOBAL character_set_results=utf8mb4;
```
## 导入设计
### 导入顺序
1. 先导入表结构:`ccdi_structure.sql`
2. 再导入数据:`ccdi_data.sql`
### 导入命令
```bash
# 导入表结构
mysql -h 生产环境IP -P 3306 -u 用户名 -p \
--default-character-set=utf8mb4 \
数据库名 < ccdi_structure.sql
# 导入数据
mysql -h 生产环境IP -P 3306 -u 用户名 -p \
--default-character-set=utf8mb4 \
数据库名 < ccdi_data.sql
```
### 前置条件
1. 目标数据库已创建(如:`CREATE DATABASE ccdi CHARACTER SET utf8mb4;`
2. 目标用户有足够权限
3. 磁盘空间充足
## 自动化脚本设计
### 脚本功能
- **导出模式**: `./export_database.sh export`
- 检查 mysqldump 命令可用性
- 创建备份目录
- 执行结构导出和数据导出
- 添加字符集声明到文件头部
- 验证文件生成
- 记录操作日志
- **导入模式**: `./export_database.sh import [production|test]`
- 读取配置文件获取目标环境信息
- 检查目标数据库连接
- 依次导入结构和数据文件
- 验证导入结果
- 记录操作日志
### 配置文件设计
```bash
# 源数据库配置(开发环境)
SOURCE_DB_HOST=116.62.17.81
SOURCE_DB_PORT=3306
SOURCE_DB_USER=root
SOURCE_DB_PASS=Kfcx@1234
SOURCE_DB_NAME=ccdi
# 生产环境数据库配置
PROD_DB_HOST=生产环境IP
PROD_DB_PORT=3306
PROD_DB_USER=生产环境用户名
PROD_DB_PASS=生产环境密码
PROD_DB_NAME=ccdi
# 测试环境数据库配置(可选)
TEST_DB_HOST=测试环境IP
TEST_DB_PORT=3306
TEST_DB_USER=测试环境用户名
TEST_DB_PASS=测试环境密码
TEST_DB_NAME=ccdi
# 导出文件配置
BACKUP_DIR=doc/database/backup
STRUCTURE_FILE=ccdi_structure.sql
DATA_FILE=ccdi_data.sql
```
### 安全措施
1. `db_config.conf` 添加到 `.gitignore`
2. 提供 `db_config.conf.template` 模板文件
3. 首次运行时检测配置文件,提示用户填写
## 验证测试
### 导出验证
1. 检查生成的 SQL 文件大小是否合理
2. 检查文件头部是否包含字符集声明
3. 随机抽取数据检查是否有乱码
4. 统计表数量和数据行数
### 导入验证
1. 在测试环境先进行导入测试
2. 对比源数据库和目标数据库的表数量
3. 抽查关键表的数据行数
4. 查询包含中文的数据验证编码正确性
5. 使用 `SHOW CREATE TABLE` 检查表字符集
### 验证命令
```sql
-- 查看数据库字符集
SHOW CREATE DATABASE ccdi;
-- 查看表数量
SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema='ccdi';
-- 查看各表行数
SELECT table_name, table_rows
FROM information_schema.tables
WHERE table_schema='ccdi'
ORDER BY table_rows DESC;
-- 检查表字符集
SHOW CREATE TABLE sys_user;
```
## 错误处理
### 常见问题
1. **字符集乱码**
- 原因:未指定字符集参数
- 解决:确保所有命令都添加 `--default-character-set=utf8mb4`
2. **导入失败**
- 原因:外键约束冲突
- 解决:导入前临时禁用外键检查 `SET FOREIGN_KEY_CHECKS=0;`
3. **连接超时**
- 原因:数据库过大或网络慢
- 解决:添加 `--max_allowed_packet=512M` 参数
4. **权限不足**
- 原因:用户权限不够
- 解决:使用 root 用户或授予足够权限
## 操作流程
### 完整迁移流程
1. 配置 `db_config.conf` 文件
2. 执行导出:`./export_database.sh export`
3. 验证导出文件正确性
4. 在测试环境验证导入:`./export_database.sh import test`
5. 验证测试环境数据完整性
6. 在生产环境执行导入:`./export_database.sh import production`
7. 验证生产环境数据完整性
8. 应用程序连接测试
### 回滚方案
保留源数据库,如迁移失败可继续使用源数据库,重新执行迁移流程。
## 交付物
1. **自动化脚本**: `export_database.sh`
2. **配置模板**: `db_config.conf.template`
3. **表结构文件**: `doc/database/backup/ccdi_structure.sql`
4. **数据文件**: `doc/database/backup/ccdi_data.sql`
5. **操作指南**: `doc/database/backup/export_guide.md`
6. **设计文档**: `docs/plans/2026-02-28-database-migration-design.md`
## 时间估算
- 脚本开发30分钟
- 导出执行10-30分钟取决于数据量
- 测试环境导入验证10-30分钟
- 生产环境导入10-30分钟
- 验证测试10分钟
**总计**: 约1.5-2小时
## 风险评估
| 风险项 | 等级 | 缓解措施 |
|--------|------|----------|
| 数据量过大导致超时 | 中 | 添加 max_allowed_packet 参数,分批导出 |
| 字符集乱码 | 高 | 严格遵循字符集规范,导入后验证 |
| 网络中断 | 低 | 本地保存SQL文件可重复导入 |
| 生产环境数据冲突 | 无 | 全新空库,无冲突风险 |
| 权限问题 | 低 | 使用 root 用户或确保权限充足 |
## 成功标准
1. ✅ 所有表结构成功导出,无遗漏
2. ✅ 所有表数据成功导出,无丢失
3. ✅ SQL 文件字符集正确,无乱码
4. ✅ 测试环境导入成功,数据完整
5. ✅ 生产环境导入成功,数据完整
6. ✅ 中文数据正确显示,编码无误
7. ✅ 应用程序可正常连接和操作数据库

View File

@@ -1,826 +0,0 @@
# 流水分析平台对接设计文档
## 文档信息
- **创建日期**: 2026-03-02
- **设计目标**: 实现与见知现金流尽调系统的对接封装7个核心接口调用
- **技术方案**: RestTemplate + 手动封装
---
## 一、需求概述
### 1.1 业务背景
系统需要与**见知现金流尽调系统**对接,用于拉取银行流水数据。通过调用流水分析平台提供的接口,实现以下功能:
- 创建项目并获取访问Token
- 上传银行流水文件或拉取行内流水
- 检查文件解析状态
- 生成尽调报告
- 获取流水明细数据
### 1.2 接口列表
共7个接口调用流程如下
```
获取Token → 上传文件/拉取行内流水 → 检查解析状态 → 生成报告 → 检查报告状态 → 获取流水数据
```
| 序号 | 接口名称 | 请求方式 | 说明 |
|------|---------|---------|------|
| 1 | 获取Token | POST | 创建项目并获取访问Token |
| 2 | 上传文件 | POST | 上传银行流水文件 |
| 3 | 拉取行内流水 | POST | 从数仓拉取行内流水 |
| 4 | 检查解析状态 | POST | 轮询检查文件解析状态 |
| 5 | 生成尽调报告 | POST | 确认文件后生成报告 |
| 6 | 检查报告状态 | GET | 轮询检查报告生成状态 |
| 7 | 获取银行流水 | POST | 分页获取流水明细 |
### 1.3 技术选型
**方案一RestTemplate + 手动封装**(已选定)
**优点**
- ✅ 简单直接符合task.md要求
- ✅ Spring Boot自带无需额外依赖
- ✅ 完全控制请求细节(超时、拦截器、错误处理)
- ✅ 易于测试和调试
---
## 二、架构设计
### 2.1 模块结构
创建新模块 `ccdi-lsfx` (流水分析对接模块),目录结构如下:
```
ccdi-lsfx/
├── pom.xml
└── src/main/java/com/ruoyi/lsfx/
├── config/
│ └── RestTemplateConfig.java # RestTemplate配置
├── constants/
│ └── LsfxConstants.java # 常量定义
├── client/
│ └── LsfxAnalysisClient.java # 封装7个接口调用
├── domain/
│ ├── request/ # 请求DTO
│ │ ├── GetTokenRequest.java # 接口1
│ │ ├── UploadFileRequest.java # 接口2
│ │ ├── FetchInnerFlowRequest.java # 接口3
│ │ ├── CheckParseStatusRequest.java # 接口4
│ │ ├── GenerateReportRequest.java # 接口5
│ │ └── GetBankStatementRequest.java # 接口7
│ └── response/ # 响应DTO
│ ├── GetTokenResponse.java # 接口1
│ ├── UploadFileResponse.java # 接口2
│ ├── FetchInnerFlowResponse.java # 接口3
│ ├── CheckParseStatusResponse.java # 接口4
│ ├── GenerateReportResponse.java # 接口5
│ ├── CheckReportStatusResponse.java# 接口6
│ └── GetBankStatementResponse.java # 接口7
├── exception/
│ ├── LsfxApiException.java # API调用异常
│ └── LsfxErrorCode.java # 错误码枚举
├── util/
│ ├── MD5Util.java # MD5加密工具
│ └── HttpUtil.java # HTTP工具类
└── controller/
└── LsfxTestController.java # 测试控制器
```
### 2.2 模块依赖
在根目录 `pom.xml``<modules>` 中添加:
```xml
<module>ccdi-lsfx</module>
```
`ruoyi-admin/pom.xml` 中添加:
```xml
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ccdi-lsfx</artifactId>
</dependency>
```
---
## 三、配置设计
### 3.1 application-dev.yml 配置
```yaml
# 流水分析平台配置
lsfx:
api:
# 测试环境
base-url: http://158.234.196.5:82/c4c3
# 生产环境(注释掉测试环境后启用)
# base-url: http://64.202.32.176/c4c3
# 认证配置
app-id: remote_app
app-secret: your_app_secret_here # 从见知获取
client-id: c2017e8d105c435a96f86373635b6a09 # 测试环境固定值
# 接口路径配置
endpoints:
get-token: /account/common/getToken
upload-file: /watson/api/project/remoteUploadSplitFile
fetch-inner-flow: /watson/api/project/getJZFileOrZjrcuFile
check-parse-status: /watson/api/project/upload/getpendings
generate-report: /watson/api/project/confirmStageUploadLogs
check-report-status: /watson/api/project/upload/getallpendings
get-bank-statement: /watson/api/project/upload/getBankStatement
# RestTemplate配置
connection-timeout: 30000 # 连接超时30秒
read-timeout: 60000 # 读取超时60秒
```
### 3.2 常量类
```java
package com.ruoyi.lsfx.constants;
/**
* 流水分析平台常量
*/
public class LsfxConstants {
/** 基础URL配置键 */
public static final String BASE_URL_KEY = "lsfx.api.base-url";
/** 成功状态码 */
public static final String SUCCESS_CODE = "200";
/** 文件解析成功状态 */
public static final int PARSE_SUCCESS_STATUS = -5;
public static final String PARSE_SUCCESS_DESC = "data.wait.confirm.newaccount";
/** 数据渠道编码 */
public static final String DATA_CHANNEL_ZJRCU = "ZJRCU";
/** 分析类型 */
public static final String ANALYSIS_TYPE = "-1";
/** 请求头 */
public static final String HEADER_CLIENT_ID = "X-Xencio-Client-Id";
public static final String HEADER_CONTENT_TYPE = "Content-Type";
/** 默认角色 */
public static final String DEFAULT_ROLE = "VIEWER";
}
```
### 3.3 RestTemplate配置类
```java
package com.ruoyi.lsfx.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
/**
* RestTemplate配置
*/
@Configuration
public class RestTemplateConfig {
@Value("${lsfx.api.connection-timeout:30000}")
private int connectionTimeout;
@Value("${lsfx.api.read-timeout:60000}")
private int readTimeout;
@Bean
public RestTemplate restTemplate() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(connectionTimeout);
factory.setReadTimeout(readTimeout);
return new RestTemplate(factory);
}
}
```
---
## 四、核心类设计
### 4.1 DTO对象设计
#### 请求DTO示例GetTokenRequest
```java
package com.ruoyi.lsfx.domain.request;
import lombok.Data;
/**
* 获取Token请求参数
*/
@Data
public class GetTokenRequest {
/** 项目编号 */
private String projectNo;
/** 项目名称 */
private String entityName;
/** 操作人员编号 */
private String userId;
/** 操作人员姓名 */
private String userName;
/** 见知提供appId */
private String appId;
/** 安全码 md5(projectNo + "_" + entityName + "_" + appSecret) */
private String appSecretCode;
/** 人员角色 */
private String role;
/** 行社机构号 */
private String orgCode;
/** 企业统信码或个人身份证号 */
private String entityId;
/** 信贷关联人信息 */
private String xdRelatedPersons;
/** 金综链流水日期ID */
private String jzDataDateId;
/** 行内流水开始日期 */
private String innerBSStartDateId;
/** 行内流水结束日期 */
private String innerBSEndDateId;
/** 分析类型 */
private String analysisType;
/** 客户经理所属营业部机构编码 */
private String departmentCode;
}
```
#### 响应DTO示例GetTokenResponse
```java
package com.ruoyi.lsfx.domain.response;
import lombok.Data;
/**
* 获取Token响应
*/
@Data
public class GetTokenResponse {
/** 返回码 */
private String code;
/** 响应状态 */
private String status;
/** 消息 */
private String message;
/** 成功标识 */
private Boolean successResponse;
/** 响应数据 */
private TokenData data;
@Data
public static class TokenData {
/** token */
private String token;
/** 见知项目Id */
private Integer projectId;
/** 项目编号 */
private String projectNo;
/** 项目名称 */
private String entityName;
/** 分析类型 */
private Integer analysisType;
}
}
```
**说明**: 其他5个接口的DTO类结构类似根据接口文档定义字段。
### 4.2 工具类设计
#### MD5Util
```java
package com.ruoyi.lsfx.util;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* MD5加密工具类
*/
public class MD5Util {
/**
* MD5加密
* @param input 待加密字符串
* @return MD5加密后的32位小写字符串
*/
public static String encrypt(String input) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] messageDigest = md.digest(input.getBytes());
StringBuilder hexString = new StringBuilder();
for (byte b : messageDigest) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5加密失败", e);
}
}
/**
* 生成安全码
* @param projectNo 项目编号
* @param entityName 项目名称
* @param appSecret 应用密钥
* @return MD5安全码
*/
public static String generateSecretCode(String projectNo, String entityName, String appSecret) {
String raw = projectNo + "_" + entityName + "_" + appSecret;
return encrypt(raw);
}
}
```
#### HttpUtil
```java
package com.ruoyi.lsfx.util;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.util.Map;
/**
* HTTP请求工具类
*/
@Component
public class HttpUtil {
@Resource
private RestTemplate restTemplate;
/**
* 发送GET请求带请求头
*/
public <T> T get(String url, Map<String, String> headers, Class<T> responseType) {
HttpHeaders httpHeaders = createHeaders(headers);
HttpEntity<Void> requestEntity = new HttpEntity<>(httpHeaders);
ResponseEntity<T> response = restTemplate.exchange(
url, HttpMethod.GET, requestEntity, responseType
);
return response.getBody();
}
/**
* 发送POST请求JSON格式带请求头
*/
public <T> T postJson(String url, Object request, Map<String, String> headers, Class<T> responseType) {
HttpHeaders httpHeaders = createHeaders(headers);
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Object> requestEntity = new HttpEntity<>(request, httpHeaders);
ResponseEntity<T> response = restTemplate.postForEntity(url, requestEntity, responseType);
return response.getBody();
}
/**
* 上传文件Multipart格式
*/
public <T> T uploadFile(String url, Map<String, Object> params, Map<String, String> headers, Class<T> responseType) {
HttpHeaders httpHeaders = createHeaders(headers);
httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
if (params != null) {
params.forEach(body::add);
}
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, httpHeaders);
ResponseEntity<T> response = restTemplate.postForEntity(url, requestEntity, responseType);
return response.getBody();
}
/**
* 创建请求头
*/
private HttpHeaders createHeaders(Map<String, String> headers) {
HttpHeaders httpHeaders = new HttpHeaders();
if (headers != null && !headers.isEmpty()) {
headers.forEach(httpHeaders::set);
}
return httpHeaders;
}
}
```
### 4.3 Client客户端类
```java
package com.ruoyi.lsfx.client;
import com.ruoyi.lsfx.constants.LsfxConstants;
import com.ruoyi.lsfx.domain.request.*;
import com.ruoyi.lsfx.domain.response.*;
import com.ruoyi.lsfx.util.HttpUtil;
import com.ruoyi.lsfx.util.MD5Util;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
/**
* 流水分析平台客户端
*/
@Component
public class LsfxAnalysisClient {
@Resource
private HttpUtil httpUtil;
@Value("${lsfx.api.base-url}")
private String baseUrl;
@Value("${lsfx.api.app-id}")
private String appId;
@Value("${lsfx.api.app-secret}")
private String appSecret;
@Value("${lsfx.api.client-id}")
private String clientId;
// ==================== 接口1获取Token ====================
public GetTokenResponse getToken(GetTokenRequest request) {
String secretCode = MD5Util.generateSecretCode(
request.getProjectNo(),
request.getEntityName(),
appSecret
);
request.setAppSecretCode(secretCode);
request.setAppId(appId);
if (request.getAnalysisType() == null) {
request.setAnalysisType(LsfxConstants.ANALYSIS_TYPE);
}
if (request.getRole() == null) {
request.setRole(LsfxConstants.DEFAULT_ROLE);
}
String url = baseUrl + "/account/common/getToken";
return httpUtil.postJson(url, request, GetTokenResponse.class);
}
// ==================== 接口2上传文件 ====================
public UploadFileResponse uploadFile(Integer groupId, Resource file) {
String url = baseUrl + "/watson/api/project/remoteUploadSplitFile";
Map<String, Object> params = new HashMap<>();
params.put("groupId", groupId);
params.put("files", file);
Map<String, String> headers = new HashMap<>();
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
return httpUtil.uploadFile(url, params, headers, UploadFileResponse.class);
}
// ==================== 接口3拉取行内流水 ====================
public FetchInnerFlowResponse fetchInnerFlow(FetchInnerFlowRequest request) {
String url = baseUrl + "/watson/api/project/getJZFileOrZjrcuFile";
Map<String, String> headers = new HashMap<>();
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
return httpUtil.postJson(url, request, headers, FetchInnerFlowResponse.class);
}
// ==================== 接口4检查文件解析状态 ====================
public CheckParseStatusResponse checkParseStatus(Integer groupId, String inprogressList) {
String url = baseUrl + "/watson/api/project/upload/getpendings";
Map<String, Object> params = new HashMap<>();
params.put("groupId", groupId);
params.put("inprogressList", inprogressList);
Map<String, String> headers = new HashMap<>();
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
return httpUtil.postJson(url, params, headers, CheckParseStatusResponse.class);
}
// ==================== 接口5生成尽调报告 ====================
public GenerateReportResponse generateReport(GenerateReportRequest request) {
String url = baseUrl + "/watson/api/project/confirmStageUploadLogs";
Map<String, String> headers = new HashMap<>();
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
return httpUtil.postJson(url, request, headers, GenerateReportResponse.class);
}
// ==================== 接口6检查报告生成状态 ====================
public CheckReportStatusResponse checkReportStatus(Integer groupId) {
String url = baseUrl + "/watson/api/project/upload/getallpendings?groupId=" + groupId;
Map<String, String> headers = new HashMap<>();
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
return httpUtil.get(url, headers, CheckReportStatusResponse.class);
}
// ==================== 接口7获取银行流水 ====================
public GetBankStatementResponse getBankStatement(GetBankStatementRequest request) {
String url = baseUrl + "/watson/api/project/upload/getBankStatement";
Map<String, String> headers = new HashMap<>();
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
return httpUtil.postJson(url, request, headers, GetBankStatementResponse.class);
}
}
```
### 4.4 异常类
```java
package com.ruoyi.lsfx.exception;
/**
* 流水分析平台API异常
*/
public class LsfxApiException extends RuntimeException {
private String errorCode;
public LsfxApiException(String message) {
super(message);
}
public LsfxApiException(String message, Throwable cause) {
super(message, cause);
}
public LsfxApiException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
```
---
## 五、测试Controller设计
```java
package com.ruoyi.lsfx.controller;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.lsfx.client.LsfxAnalysisClient;
import com.ruoyi.lsfx.domain.request.*;
import com.ruoyi.lsfx.domain.response.*;
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.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.core.io.Resource;
import javax.annotation.Resource;
/**
* 流水分析平台接口测试控制器
*/
@Tag(name = "流水分析平台接口测试", description = "用于测试流水分析平台的7个接口")
@RestController
@RequestMapping("/lsfx/test")
public class LsfxTestController {
@Resource
private LsfxAnalysisClient lsfxAnalysisClient;
@Operation(summary = "获取Token", description = "创建项目并获取访问Token")
@PostMapping("/getToken")
public AjaxResult getToken(@RequestBody GetTokenRequest request) {
GetTokenResponse response = lsfxAnalysisClient.getToken(request);
return AjaxResult.success(response);
}
@Operation(summary = "上传流水文件", description = "上传银行流水文件到流水分析平台")
@PostMapping("/uploadFile")
public AjaxResult uploadFile(
@Parameter(description = "项目ID") @RequestParam Integer groupId,
@Parameter(description = "流水文件") @RequestParam("file") MultipartFile file
) {
Resource fileResource = file.getResource();
UploadFileResponse response = lsfxAnalysisClient.uploadFile(groupId, fileResource);
return AjaxResult.success(response);
}
@Operation(summary = "拉取行内流水", description = "从数仓拉取行内流水数据")
@PostMapping("/fetchInnerFlow")
public AjaxResult fetchInnerFlow(@RequestBody FetchInnerFlowRequest request) {
FetchInnerFlowResponse response = lsfxAnalysisClient.fetchInnerFlow(request);
return AjaxResult.success(response);
}
@Operation(summary = "检查文件解析状态", description = "轮询检查上传文件的解析状态")
@PostMapping("/checkParseStatus")
public AjaxResult checkParseStatus(
@Parameter(description = "项目ID") @RequestParam Integer groupId,
@Parameter(description = "文件ID列表") @RequestParam String inprogressList
) {
CheckParseStatusResponse response = lsfxAnalysisClient.checkParseStatus(groupId, inprogressList);
return AjaxResult.success(response);
}
@Operation(summary = "生成尽调报告", description = "确认文件后生成尽调报告")
@PostMapping("/generateReport")
public AjaxResult generateReport(@RequestBody GenerateReportRequest request) {
GenerateReportResponse response = lsfxAnalysisClient.generateReport(request);
return AjaxResult.success(response);
}
@Operation(summary = "检查报告生成状态", description = "轮询检查尽调报告生成状态")
@GetMapping("/checkReportStatus")
public AjaxResult checkReportStatus(
@Parameter(description = "项目ID") @RequestParam Integer groupId
) {
CheckReportStatusResponse response = lsfxAnalysisClient.checkReportStatus(groupId);
return AjaxResult.success(response);
}
@Operation(summary = "获取银行流水列表", description = "分页获取银行流水数据")
@PostMapping("/getBankStatement")
public AjaxResult getBankStatement(@RequestBody GetBankStatementRequest request) {
GetBankStatementResponse response = lsfxAnalysisClient.getBankStatement(request);
return AjaxResult.success(response);
}
}
```
---
## 六、Maven依赖配置
**ccdi-lsfx/pom.xml**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi</artifactId>
<version>3.9.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ccdi-lsfx</artifactId>
<description>流水分析平台对接模块</description>
<dependencies>
<!-- 通用工具 -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
</dependency>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- SpringDoc OpenAPI (Swagger) -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
</dependencies>
</project>
```
---
## 七、实施要点
### 7.1 开发顺序
1. **创建模块结构** - 创建ccdi-lsfx模块及基础目录
2. **添加配置** - 修改pom.xml添加application.yml配置
3. **实现工具类** - MD5Util、HttpUtil
4. **创建DTO对象** - 7个接口的Request和Response类
5. **实现Client** - LsfxAnalysisClient封装接口调用
6. **创建测试Controller** - 提供测试端点
7. **测试验证** - 使用Swagger测试各个接口
### 7.2 注意事项
1. **安全码生成** - Token接口需要MD5加密生成安全码
2. **请求头设置** - 除Token接口外其他接口需要设置X-Xencio-Client-Id请求头
3. **文件上传** - 上传文件接口使用multipart/form-data格式
4. **轮询检查** - 解析状态和报告状态需要轮询检查,直到处理完成
5. **环境切换** - 测试环境和生产环境的URL和Client-Id不同需配置切换
### 7.3 测试计划
1. 单元测试 - 测试MD5Util、HttpUtil工具类
2. 集成测试 - 测试LsfxAnalysisClient的7个接口调用
3. 端到端测试 - 通过Swagger测试完整的调用流程
---
## 八、后续扩展
### 8.1 可选增强功能
1. **日志记录** - 添加详细的接口调用日志
2. **重试机制** - 对失败的接口调用添加自动重试
3. **熔断降级** - 使用Resilience4j实现熔断和降级
4. **数据持久化** - 将获取的流水数据保存到数据库
5. **异步处理** - 使用异步方式处理耗时的接口调用
### 8.2 业务集成
未来可根据业务需求,将此模块集成到具体的业务场景中,如:
- 员工异常行为调查时自动获取流水数据
- 定期批量拉取流水数据
- 与前端页面集成展示流水信息
---
## 九、参考文档
- [兰溪-流水分析对接.md](../../doc/对接流水分析/兰溪-流水分析对接.md)
- [RestTemplate官方文档](https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#rest-client-access)
- [Spring Boot官方文档](https://spring.io/projects/spring-boot)

View File

@@ -1,572 +0,0 @@
# 流水分析 Mock 服务器设计方案
**创建日期**: 2026-03-02
**作者**: Claude Code
## 项目概述
### 背景
当前项目需要与流水分析平台进行接口对接,但在开发和测试过程中,依赖外部真实服务存在以下问题:
- 网络连接不稳定,影响测试效率
- 无法控制返回数据,难以测试各种场景
- 无法模拟错误场景和边界情况
- 团队成员无法共享测试环境
### 解决方案
开发一个独立的 Mock 服务器,基于 Python + FastAPI 技术栈,模拟流水分析平台的 7 个核心接口,支持:
- 配置文件驱动的响应数据
- 文件上传解析延迟模拟
- 错误场景触发机制
- 自动生成的 API 文档
### 技术选型
| 技术组件 | 选择 | 理由 |
|---------|------|------|
| Web框架 | FastAPI | 现代异步框架自动生成API文档强类型支持 |
| 数据验证 | Pydantic | 与FastAPI原生集成类型提示清晰 |
| 配置管理 | JSON文件 | 易于修改,非开发人员也能调整测试数据 |
| 状态存储 | 内存字典 | 轻量级重启清空适合Mock场景 |
---
## 整体架构
```
lsfx-mock-server/
├── main.py # 应用入口
├── config/
│ ├── settings.py # 全局配置
│ └── responses/ # 响应模板配置文件
│ ├── token.json
│ ├── upload.json
│ ├── parse_status.json
│ └── bank_statement.json
├── models/
│ ├── request.py # 请求模型Pydantic
│ └── response.py # 响应模型Pydantic
├── services/
│ ├── token_service.py # Token管理
│ ├── file_service.py # 文件上传和解析模拟
│ └── statement_service.py # 流水数据管理
├── routers/
│ └── api.py # 所有API路由
├── utils/
│ ├── response_builder.py # 响应构建器
│ └── error_simulator.py # 错误场景模拟
└── requirements.txt
```
### 核心设计思想
1. **配置驱动** - 所有响应数据在JSON配置文件中方便修改
2. **内存状态管理** - 使用全局字典存储运行时状态tokens、文件记录等
3. **异步任务** - 使用FastAPI后台任务模拟文件解析延迟
4. **错误标记识别** - 检测请求参数中的特殊标记触发错误响应
---
## 数据模型设计
### 请求模型
对应Java项目中的DTO类
```python
# models/request.py
from pydantic import BaseModel
from typing import Optional
class GetTokenRequest(BaseModel):
projectNo: str
entityName: str
userId: str
userName: str
orgCode: str
entityId: Optional[str] = None
xdRelatedPersons: Optional[str] = None
jzDataDateId: Optional[str] = "0"
innerBSStartDateId: Optional[str] = "0"
innerBSEndDateId: Optional[str] = "0"
analysisType: Optional[int] = -1
departmentCode: Optional[str] = None
class UploadFileRequest(BaseModel):
groupId: int
class FetchInnerFlowRequest(BaseModel):
groupId: int
customerNo: str
dataChannelCode: str
requestDateId: int
dataStartDateId: int
dataEndDateId: int
uploadUserId: int
class CheckParseStatusRequest(BaseModel):
groupId: int
inprogressList: str
class GetBankStatementRequest(BaseModel):
groupId: int
logId: int
pageNow: int
pageSize: int
```
### 响应模型
对应Java项目中的VO类
```python
# models/response.py
from pydantic import BaseModel
from typing import Optional, List, Dict, Any
class TokenData(BaseModel):
token: str
projectId: int
projectNo: str
entityName: str
analysisType: int
class GetTokenResponse(BaseModel):
code: str = "200"
data: Optional[TokenData] = None
message: str = "create.token.success"
status: str = "200"
successResponse: bool = True
# 其他响应模型类似...
```
---
## 核心业务逻辑
### 文件解析延迟模拟
**实现机制:**
1. 上传接口立即返回,状态为"解析中"
2. 使用FastAPI的BackgroundTasks在后台延迟执行
3. 延迟3-5秒后更新状态为"解析完成"
4. 轮询检查接口返回当前解析状态
```python
# services/file_service.py
class FileService:
def __init__(self):
self.file_records: Dict[int, Dict] = {}
self.parsing_status: Dict[int, bool] = {}
async def upload_file(self, group_id: int, file, background_tasks: BackgroundTasks):
log_id = generate_log_id()
# 立即存储记录,标记为解析中
self.file_records[log_id] = {
"logId": log_id,
"status": -5,
"uploadStatusDesc": "parsing",
...
}
self.parsing_status[log_id] = True
# 启动后台任务延迟4秒后完成解析
background_tasks.add_task(
self._simulate_parsing,
log_id,
delay_seconds=4
)
return log_id
def _simulate_parsing(self, log_id: int, delay_seconds: int):
time.sleep(delay_seconds)
if log_id in self.file_records:
self.file_records[log_id]["status"] = -5
self.file_records[log_id]["uploadStatusDesc"] = "data.wait.confirm.newaccount"
self.parsing_status[log_id] = False
```
---
## 错误场景模拟机制
### 错误触发规则
通过请求参数中的特殊标记触发对应的错误响应:
**错误码映射表:**
```python
ERROR_CODES = {
"40101": {"code": "40101", "message": "appId错误"},
"40102": {"code": "40102", "message": "appSecretCode错误"},
"40104": {"code": "40104", "message": "可使用项目次数为0无法创建项目"},
"40105": {"code": "40105", "message": "只读模式下无法新建项目"},
"40106": {"code": "40106", "message": "错误的分析类型,不在规定的取值范围内"},
"40107": {"code": "40107", "message": "当前系统不支持的分析类型"},
"40108": {"code": "40108", "message": "当前用户所属行社无权限"},
"501014": {"code": "501014", "message": "无行内流水文件"},
}
```
**检测逻辑:**
```python
@staticmethod
def detect_error_marker(value: str) -> Optional[str]:
"""检测字符串中的错误标记
规则:如果字符串包含 error_XXXX则返回 XXXX
例如:
- "project_error_40101" -> "40101"
- "test_error_501014" -> "501014"
"""
if not value:
return None
import re
pattern = r'error_(\d+)'
match = re.search(pattern, value)
if match:
return match.group(1)
return None
```
**使用示例:**
```python
# 在服务中使用
def get_token(request: GetTokenRequest):
error_code = ErrorSimulator.detect_error_marker(request.projectNo)
if error_code:
return ErrorSimulator.build_error_response(error_code)
# 正常流程...
```
**测试方式:**
```python
# 触发 40101 错误
request_data = {
"projectNo": "test_project_error_40101", # 包含错误标记
"entityName": "测试企业",
...
}
```
---
## 配置文件结构
### 响应模板配置
```json
// config/responses/token.json
{
"success_response": {
"code": "200",
"data": {
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.mock_token_{project_id}",
"projectId": "{project_id}",
"projectNo": "{project_no}",
"entityName": "{entity_name}",
"analysisType": 0
},
"message": "create.token.success",
"status": "200",
"successResponse": true
}
}
```
```json
// config/responses/upload.json
{
"success_response": {
"code": "200",
"data": {
"accountsOfLog": {},
"uploadLogList": [
{
"logId": "{log_id}",
"status": -5,
"uploadStatusDesc": "data.wait.confirm.newaccount",
...
}
]
}
}
}
```
### 全局配置
```python
# config/settings.py
from pydantic import BaseSettings
class Settings(BaseSettings):
APP_NAME: str = "流水分析Mock服务"
APP_VERSION: str = "1.0.0"
DEBUG: bool = True
HOST: str = "0.0.0.0"
PORT: int = 8000
# 模拟配置
PARSE_DELAY_SECONDS: int = 4
MAX_FILE_SIZE: int = 10485760 # 10MB
class Config:
env_file = ".env"
settings = Settings()
```
---
## API 路由实现
### 核心接口
```python
# routers/api.py
from fastapi import APIRouter, BackgroundTasks, UploadFile, File
router = APIRouter()
# 接口1获取Token
@router.post("/account/common/getToken")
async def get_token(request: GetTokenRequest):
error_code = ErrorSimulator.detect_error_marker(request.projectNo)
if error_code:
return ErrorSimulator.build_error_response(error_code)
return token_service.create_token(request)
# 接口2上传文件
@router.post("/watson/api/project/remoteUploadSplitFile")
async def upload_file(
background_tasks: BackgroundTasks,
groupId: int = Form(...),
file: UploadFile = File(...)
):
return file_service.upload_file(groupId, file, background_tasks)
# 接口3拉取行内流水
@router.post("/watson/api/project/getJZFileOrZjrcuFile")
async def fetch_inner_flow(request: FetchInnerFlowRequest):
error_code = ErrorSimulator.detect_error_marker(request.customerNo)
if error_code:
return ErrorSimulator.build_error_response(error_code)
return file_service.fetch_inner_flow(request)
# 接口4检查解析状态
@router.post("/watson/api/project/upload/getpendings")
async def check_parse_status(request: CheckParseStatusRequest):
return file_service.check_parse_status(request.groupId, request.inprogressList)
# 接口5删除文件
@router.post("/watson/api/project/batchDeleteUploadFile")
async def delete_files(request: dict):
return file_service.delete_files(
request.get("groupId"),
request.get("logIds"),
request.get("userId")
)
# 接口6获取银行流水
@router.post("/watson/api/project/getBSByLogId")
async def get_bank_statement(request: GetBankStatementRequest):
return statement_service.get_bank_statement(request)
```
### 主应用
```python
# main.py
from fastapi import FastAPI
from routers import api
app = FastAPI(
title="流水分析Mock服务",
description="模拟流水分析平台的7个核心接口",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc"
)
app.include_router(api.router, tags=["流水分析接口"])
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
```
---
## 测试和使用说明
### 启动服务
```bash
# 安装依赖
pip install -r requirements.txt
# 启动服务
python main.py
# 或使用uvicorn启动支持热重载
uvicorn main:app --reload --host 0.0.0.0 --port 8000
```
### 访问API文档
- **Swagger UI:** http://localhost:8000/docs
- **ReDoc:** http://localhost:8000/redoc
### 测试示例
#### 1. 正常流程测试
```python
import requests
# 获取Token
response = requests.post(
"http://localhost:8000/account/common/getToken",
json={
"projectNo": "test_project_001",
"entityName": "测试企业",
"userId": "902001",
"userName": "902001",
"orgCode": "902000"
}
)
result = response.json()
token = result["data"]["token"]
project_id = result["data"]["projectId"]
# 上传文件
files = {"file": ("test.csv", open("test.csv", "rb"), "text/csv")}
response = requests.post(
"http://localhost:8000/watson/api/project/remoteUploadSplitFile",
files=files,
data={"groupId": project_id},
headers={"X-Xencio-Client-Id": "26e5b9239853436b85c623f4b7a6d0e6"}
)
log_id = response.json()["data"]["uploadLogList"][0]["logId"]
# 轮询检查解析状态
import time
for i in range(10):
response = requests.post(
"http://localhost:8000/watson/api/project/upload/getpendings",
json={"groupId": project_id, "inprogressList": str(log_id)},
headers={"X-Xencio-Client-Id": "26e5b9239853436b85c623f4b7a6d0e6"}
)
result = response.json()
if not result["data"]["parsing"]:
print("解析完成")
break
time.sleep(1)
# 获取银行流水
response = requests.post(
"http://localhost:8000/watson/api/project/getBSByLogId",
json={
"groupId": project_id,
"logId": log_id,
"pageNow": 1,
"pageSize": 10
},
headers={"X-Xencio-Client-Id": "26e5b9239853436b85c623f4b7a6d0e6"}
)
```
#### 2. 错误场景测试
```python
# 触发 40101 错误appId错误
response = requests.post(
"http://localhost:8000/account/common/getToken",
json={
"projectNo": "test_project_error_40101", # 包含错误标记
"entityName": "测试企业",
"userId": "902001",
"userName": "902001",
"orgCode": "902000"
}
)
# 返回: {"code": "40101", "message": "appId错误", ...}
# 触发 501014 错误(无行内流水文件)
response = requests.post(
"http://localhost:8000/watson/api/project/getJZFileOrZjrcuFile",
json={
"groupId": 1,
"customerNo": "test_error_501014", # 包含错误标记
"dataChannelCode": "ZJRCU",
"requestDateId": 20260302,
"dataStartDateId": 20260201,
"dataEndDateId": 20260228,
"uploadUserId": 902001
}
)
# 返回: {"code": "501014", "message": "无行内流水文件", ...}
```
### 配置修改
- 修改 `config/responses/` 下的JSON文件可以自定义响应数据
- 修改 `config/settings.py` 可以调整延迟时间、端口等配置
- 支持 `.env` 文件覆盖配置
---
## 依赖清单
```txt
# requirements.txt
fastapi==0.104.1
uvicorn[standard]==0.24.0
pydantic==2.5.0
python-multipart==0.0.6
```
---
## 使用场景
### A. 开发阶段测试
在业务代码开发过程中,修改配置文件 `application-dev.yml`,将 `lsfx.api.base-url` 改为 `http://localhost:8000`启动Mock服务器后业务代码即可连接Mock服务进行测试。
### B. 完全替换测试
直接使用 Mock 服务器进行接口测试,验证业务逻辑的正确性。生产环境切换到真实服务。
### C. CI/CD 集成
在持续集成流程中使用 Mock 服务器,自动化测试接口调用逻辑。
---
## 扩展性考虑
### 后续可能的增强功能
1. **数据持久化** - 如需保留历史记录可集成SQLite
2. **更复杂的场景模拟** - 支持配置文件定义多个场景
3. **请求日志记录** - 记录所有请求用于调试
4. **Web管理界面** - 可视化管理Mock数据和状态
5. **Docker部署** - 提供Dockerfile方便部署
当前设计已满足核心需求,保持简洁实用。
---
## 总结
这是一个**配置驱动、轻量级、易于使用**的 Mock 服务器设计,核心特点:
**完整性** - 覆盖所有7个核心接口
**真实性** - 模拟文件解析延迟等真实场景
**灵活性** - 配置文件驱动,错误场景可触发
**易用性** - 自动API文档零配置启动
**可维护** - 代码结构清晰与Java项目对应
满足您的Mock测试需求提升开发和测试效率。

View File

@@ -1,603 +0,0 @@
# 银行流水实体类与数据转换设计文档
**日期:** 2026-03-04
**模块:** ccdi-project
**作者:** Claude
---
## 一、概述
### 1.1 目标
创建银行流水实体类 `CcdiBankStatement`,用于持久化从流水分析平台获取的流水数据,并提供数据转换方法。
### 1.2 背景
- 流水分析平台提供 `GetBankStatementResponse.BankStatementItem` 接口响应对象
- 需要将响应数据转换为本地数据库实体进行持久化
- 流水数据需要关联到具体项目(`ccdi_project` 表)
### 1.3 技术选型
| 技术点 | 选择 | 理由 |
|--------|------|------|
| ORM框架 | MyBatis Plus 3.5.10 | 项目已集成简化CRUD操作 |
| 对象映射 | Spring BeanUtils | 无需额外依赖,简单易用 |
| 数据库 | MySQL 8.2.0 | 项目标准数据库 |
| 实体类注解 | Lombok @Data | 简化代码,提高可读性 |
---
## 二、架构设计
### 2.1 模块位置
**主模块:** `ccdi-project` (项目管理模块)
**依赖关系:**
```
ccdi-project (流水实体类所在模块)
└── 依赖 ccdi-lsfx (访问流水分析响应类)
└── 依赖 ruoyi-common (通用工具)
```
### 2.2 包结构
```
ccdi-project/
├── src/main/java/com/ruoyi/ccdi/project/
│ ├── domain/
│ │ └── entity/
│ │ └── CcdiBankStatement.java (核心实体类)
│ ├── mapper/
│ │ └── CcdiBankStatementMapper.java (数据访问层)
│ └── service/
│ ├── IBankStatementService.java
│ └── impl/BankStatementServiceImpl.java
└── src/main/resources/
└── mapper/ccdi/project/
└── CcdiBankStatementMapper.xml
```
### 2.3 核心组件
**1. 实体类:** `CcdiBankStatement`
- 39个字段38个原有字段 + 1个新增字段
- 包含静态转换方法 `fromResponse()`
- 使用 MyBatis Plus 注解进行映射
**2. Mapper接口** `CcdiBankStatementMapper`
- 继承 `BaseMapper<CcdiBankStatement>`
- 提供批量插入方法
**3. Service层** 调用转换方法,设置业务字段
---
## 三、数据模型设计
### 3.1 数据库表结构修改
**表名:** `ccdi_bank_statement`
**新增字段:**
```sql
ALTER TABLE `ccdi_bank_statement`
ADD COLUMN `project_id` bigint(20) DEFAULT NULL COMMENT '关联项目ID' AFTER `bank_statement_id`,
ADD INDEX `idx_project_id` (`project_id`);
```
**说明:**
- `project_id` 关联 `ccdi_project` 表的主键
- `group_id` 字段保留用于兼容流水分析平台的原始项目ID
### 3.2 字段映射关系
**总字段数:** 39个
**字段分类:**
| 分类 | 字段数 | 说明 |
|------|--------|------|
| 主键和关联 | 4 | bank_statement_id, project_id, le_id, group_id |
| 账号信息 | 5 | account_id, le_account_name, le_account_no, accounting_date_id, accounting_date |
| 交易信息 | 5 | trx_date, currency, amount_dr, amount_cr, amount_balance |
| 交易类型 | 5 | cash_type, trx_flag, trx_type, exception_type, internal_flag |
| 对手方信息 | 5 | customer_le_id, customer_account_name, customer_account_no, customer_bank, customer_reference |
| 摘要备注 | 4 | user_memo, bank_comments, bank_trx_number, bank |
| 批次上传 | 2 | batch_id, batch_sequence |
| 附加字段 | 7 | meta_json, no_balance, begin_balance, end_balance, override_bs_id, payment_method, cret_no |
| 审计字段 | 2 | create_date, created_by |
**特殊字段处理:**
| 数据库字段 | 响应字段 | 处理方式 |
|-----------|---------|---------|
| le_account_no | accountMaskNo | 手动映射 |
| customer_account_no | customerAccountMaskNo | 手动映射 |
| batch_sequence | uploadSequnceNumber | 手动映射 |
| meta_json | - | 强制设为 null |
| project_id | - | Service层设置 |
### 3.3 实体类字段类型
```java
// 数值类型
private Long bankStatementId; // 主键
private Long projectId; // 项目ID新增
private Long accountId; // 账号ID
private Integer leId; // 企业ID
private Integer groupId; // 项目ID原有
private Integer accountingDateId; // 账号日期ID
private Integer customerLeId; // 对手方企业ID
private Integer trxType; // 分类ID
private Integer internalFlag; // 内部交易标志
private Integer batchId; // 批次ID
private Integer batchSequence; // 批次序号
private Integer noBalance; // 是否包含余额
private Integer beginBalance; // 初始余额
private Integer endBalance; // 结束余额
private Long overrideBsId; // 覆盖标识
private Long createdBy; // 创建者
// 金额类型
private BigDecimal amountDr; // 付款金额
private BigDecimal amountCr; // 收款金额
private BigDecimal amountBalance; // 余额
// 字符串类型
private String leAccountName; // 企业账号名称
private String leAccountNo; // 企业银行账号
private String accountingDate; // 账号日期
private String trxDate; // 交易日期
private String currency; // 币种
private String cashType; // 交易类型
private String trxFlag; // 交易标志位
private String exceptionType; // 异常类型
private String customerAccountName;// 对手方企业名称
private String customerAccountNo; // 对手方账号
private String customerBank; // 对手方银行
private String customerReference; // 对手方备注
private String userMemo; // 用户交易摘要
private String bankComments; // 银行交易摘要
private String bankTrxNumber; // 银行交易号
private String bank; // 所属银行缩写
private String metaJson; // meta json
private String paymentMethod; // 交易方式
private String cretNo; // 身份证号
// 日期类型
private Date createDate; // 创建时间
```
---
## 四、转换方法设计
### 4.1 方法签名
```java
/**
* 从流水分析接口响应转换为实体
*
* @param item 流水分析接口返回的流水项
* @return 流水实体,如果 item 为 null 则返回 null
*/
public static CcdiBankStatement fromResponse(BankStatementItem item)
```
### 4.2 转换逻辑
```java
public static CcdiBankStatement fromResponse(BankStatementItem item) {
// 1. 空值检查
if (item == null) {
return null;
}
// 2. 创建实体对象
CcdiBankStatement entity = new CcdiBankStatement();
// 3. 使用 BeanUtils 复制同名字段
BeanUtils.copyProperties(item, entity);
// 4. 手动映射字段名不一致的情况
entity.setLeAccountNo(item.getAccountMaskNo());
entity.setCustomerAccountNo(item.getCustomerAccountMaskNo());
entity.setBatchSequence(item.getUploadSequnceNumber());
// 5. 特殊字段处理
entity.setMetaJson(null); // 根据文档要求强制设为 null
// 6. 注意project_id 需要在 Service 层设置
return entity;
}
```
### 4.3 BeanUtils 行为说明
| 场景 | BeanUtils 行为 |
|------|---------------|
| 字段名相同且类型兼容 | 自动复制 |
| 字段名相同但类型不兼容 | 抛出异常 |
| 源对象中不存在目标字段 | 忽略,不抛异常 |
| 目标对象中不存在源字段 | 忽略,不抛异常 |
| 源字段为 null | 复制 null 值到目标字段 |
**注意事项:**
- BeanUtils 会忽略响应对象中额外的字段(如 `transAmount`, `attachments` 等)
- 需要手动处理字段名不一致的3个字段
- `meta_json` 字段强制设为 null
---
## 五、使用示例
### 5.1 Service层调用
```java
@Service
public class BankStatementServiceImpl implements IBankStatementService {
@Resource
private CcdiBankStatementMapper bankStatementMapper;
@Resource
private LsfxAnalysisClient lsfxClient;
/**
* 获取并保存流水数据
*
* @param projectId 项目ID
* @param request 查询请求
* @return 保存的记录数
*/
public int fetchAndSaveBankStatements(Long projectId, GetBankStatementRequest request) {
// 1. 调用流水分析接口
GetBankStatementResponse response = lsfxClient.getBankStatement(request);
// 2. 校验响应
if (response == null || !Boolean.TRUE.equals(response.getSuccessResponse())) {
throw new ServiceException("获取流水数据失败");
}
List<BankStatementItem> items = response.getData().getBankStatementList();
if (items == null || items.isEmpty()) {
return 0;
}
// 3. 转换并设置项目ID
List<CcdiBankStatement> entities = items.stream()
.map(item -> {
CcdiBankStatement entity = CcdiBankStatement.fromResponse(item);
if (entity != null) {
entity.setProjectId(projectId); // 设置关联项目ID
}
return entity;
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
// 4. 批量插入数据库
return bankStatementMapper.insertBatch(entities);
}
}
```
### 5.2 单条数据转换
```java
// 从接口响应转换单条流水
BankStatementItem item = response.getData().getBankStatementList().get(0);
CcdiBankStatement entity = CcdiBankStatement.fromResponse(item);
// 设置业务字段
entity.setProjectId(1001L);
// 保存到数据库
bankStatementMapper.insert(entity);
```
### 5.3 批量数据转换
```java
List<CcdiBankStatement> entities = response.getData().getBankStatementList()
.stream()
.map(CcdiBankStatement::fromResponse)
.peek(entity -> entity.setProjectId(projectId))
.collect(Collectors.toList());
bankStatementMapper.insertBatch(entities);
```
---
## 六、错误处理
### 6.1 空指针异常防护
**问题:** 接口响应可能为 null 或数据列表为空
**解决方案:**
```java
// 在 fromResponse 方法中
if (item == null) {
log.warn("流水项为空,无法转换");
return null;
}
// 在 Service 层
if (response == null || !Boolean.TRUE.equals(response.getSuccessResponse())) {
throw new ServiceException("获取流水数据失败");
}
List<BankStatementItem> items = response.getData().getBankStatementList();
if (items == null || items.isEmpty()) {
return 0; // 正常返回,不是异常情况
}
```
### 6.2 类型转换异常
**问题:** BeanUtils 在字段类型不匹配时会抛出异常
**解决方案:**
1. 确保 `BankStatementItem``CcdiBankStatement` 字段类型一致
2. BigDecimal、Integer、Long 类型已验证兼容
3. 添加异常捕获日志:
```java
public static CcdiBankStatement fromResponse(BankStatementItem item) {
if (item == null) {
return null;
}
try {
CcdiBankStatement entity = new CcdiBankStatement();
BeanUtils.copyProperties(item, entity);
entity.setLeAccountNo(item.getAccountMaskNo());
entity.setCustomerAccountNo(item.getCustomerAccountMaskNo());
entity.setBatchSequence(item.getUploadSequnceNumber());
entity.setMetaJson(null);
return entity;
} catch (Exception e) {
log.error("流水数据转换失败, bankStatementId={}", item.getBankStatementId(), e);
throw new RuntimeException("流水数据转换失败", e);
}
}
```
### 6.3 数据验证
**必填字段验证:**
```java
// 在 Service 层验证业务字段
if (entity.getProjectId() == null) {
throw new IllegalArgumentException("项目ID不能为空");
}
```
**数据库约束:**
- `bank_statement_id` 自增主键,无需验证
- 其他字段根据业务需求设置数据库约束NOT NULL、DEFAULT等
---
## 七、性能考虑
### 7.1 BeanUtils 性能
**特点:**
- 使用 Java 反射机制
- 单次转换性能影响可忽略(< 1ms
- 批量转换时累计开销需要考虑
**性能数据(参考):**
| 操作 | 耗时 |
|------|------|
| 单次 BeanUtils.copyProperties() | < 1ms |
| 100次转换 | ~50ms |
| 1000次转换 | ~200ms |
**优化建议:**
- 对于单次或小批量转换(<100条直接使用 BeanUtils
- 对于大批量转换(>1000条可考虑
1. 使用 MapStruct编译期生成代码无反射
2. 异步批量处理
3. 分批插入数据库
### 7.2 数据库批量插入
**推荐方式:**
```java
// MyBatis Plus 批量插入
@Service
public class BankStatementServiceImpl {
@Resource
private CcdiBankStatementMapper bankStatementMapper;
public int insertBatch(List<CcdiBankStatement> entities) {
if (entities == null || entities.isEmpty()) {
return 0;
}
// 分批插入,每批 1000 条
int batchSize = 1000;
int totalInserted = 0;
for (int i = 0; i < entities.size(); i += batchSize) {
int end = Math.min(i + batchSize, entities.size());
List<CcdiBankStatement> batch = entities.subList(i, end);
bankStatementMapper.insertBatch(batch);
totalInserted += batch.size();
}
return totalInserted;
}
}
```
### 7.3 内存考虑
**对象占用空间估算:**
- 单个 `CcdiBankStatement` 对象约 1KB包含所有字段
- 1000条流水数据约占用 1MB 内存
- 10000条流水数据约占用 10MB 内存
**建议:**
- 对于超大数据量(>10000条使用流式处理
```java
response.getData().getBankStatementList()
.stream()
.map(CcdiBankStatement::fromResponse)
.forEach(entity -> {
// 立即处理,不保留在内存中
bankStatementMapper.insert(entity);
});
```
---
## 八、测试策略
### 8.1 单元测试
**测试类:** `CcdiBankStatementTest`
**测试用例:**
| 测试场景 | 测试方法 | 验证点 |
|---------|---------|--------|
| 正常转换 | `testFromResponse_Success` | 所有字段正确映射 |
| 空值处理 | `testFromResponse_Null` | 返回 null |
| 字段名映射 | `testFromResponse_FieldMapping` | 3个特殊字段正确映射 |
| meta_json | `testFromResponse_MetaJson` | 强制为 null |
**测试代码示例:**
```java
@Test
public void testFromResponse_Success() {
// 准备测试数据
BankStatementItem item = new BankStatementItem();
item.setBankStatementId(123456L);
item.setLeId(100);
item.setAccountMaskNo("6222****1234");
item.setDrAmount(new BigDecimal("1000.00"));
// 执行转换
CcdiBankStatement entity = CcdiBankStatement.fromResponse(item);
// 验证结果
assertNotNull(entity);
assertEquals(123456L, entity.getBankStatementId());
assertEquals(100, entity.getLeId());
assertEquals("6222****1234", entity.getLeAccountNo());
assertEquals(new BigDecimal("1000.00"), entity.getAmountDr());
assertNull(entity.getMetaJson());
}
```
### 8.2 集成测试
**测试场景:**
1. 完整流程:调用接口 → 转换数据 → 保存数据库
2. 数据库查询:验证字段值正确性
3. 关联查询:验证 `project_id` 关联有效
### 8.3 性能测试
**测试指标:**
- 单次转换耗时
- 1000次批量转换耗时
- 数据库批量插入耗时
---
## 九、部署检查清单
### 9.1 数据库修改
- [ ] 执行 ALTER TABLE 添加 `project_id` 字段
- [ ] 创建索引 `idx_project_id`
- [ ] 验证字段类型和长度
### 9.2 代码检查
- [ ] `ccdi-project` 模块已依赖 `ccdi-lsfx`
- [ ] 实体类字段类型与数据库一致
- [ ] 转换方法处理所有特殊字段
- [ ] Service 层正确设置 `project_id`
### 9.3 测试验证
- [ ] 单元测试通过
- [ ] 集成测试通过
- [ ] 性能测试达标
### 9.4 文档更新
- [ ] 更新 CLAUDE.md 文档
- [ ] 更新数据库设计文档
- [ ] 添加 API 文档说明
---
## 十、附录
### 10.1 完整字段映射表
| 序号 | 数据库字段 | Java字段 | Java类型 | 响应字段 | 说明 |
|------|-----------|---------|---------|---------|------|
| 1 | bank_statement_id | bankStatementId | Long | bankStatementId | 主键自增 |
| 2 | project_id | projectId | Long | - | **新增字段** |
| 3 | LE_ID | leId | Integer | leId | 企业ID |
| 4 | ACCOUNT_ID | accountId | Long | accountId | 账号ID |
| 5 | LE_ACCOUNT_NAME | leAccountName | String | leName | 企业账号名称 |
| 6 | LE_ACCOUNT_NO | leAccountNo | String | accountMaskNo | **手动映射** |
| 7 | ACCOUNTING_DATE_ID | accountingDateId | Integer | accountingDateId | 账号日期ID |
| 8 | ACCOUNTING_DATE | accountingDate | String | accountingDate | 账号日期 |
| 9 | TRX_DATE | trxDate | String | trxDate | 交易日期 |
| 10 | CURRENCY | currency | String | currency | 币种 |
| 11 | AMOUNT_DR | amountDr | BigDecimal | drAmount | 付款金额 |
| 12 | AMOUNT_CR | amountCr | BigDecimal | crAmount | 收款金额 |
| 13 | AMOUNT_BALANCE | amountBalance | BigDecimal | balanceAmount | 余额 |
| 14 | CASH_TYPE | cashType | String | cashType | 交易类型 |
| 15 | CUSTOMER_LE_ID | customerLeId | Integer | customerId | 对手方企业ID |
| 16 | CUSTOMER_ACCOUNT_NAME | customerAccountName | String | customerName | 对手方企业名称 |
| 17 | CUSTOMER_ACCOUNT_NO | customerAccountNo | String | customerAccountMaskNo | **手动映射** |
| 18 | customer_bank | customerBank | String | customerBank | 对手方银行 |
| 19 | customer_reference | customerReference | String | customerReference | 对手方备注 |
| 20 | USER_MEMO | userMemo | String | userMemo | 用户交易摘要 |
| 21 | BANK_COMMENTS | bankComments | String | bankComments | 银行交易摘要 |
| 22 | BANK_TRX_NUMBER | bankTrxNumber | String | bankTrxNumber | 银行交易号 |
| 23 | BANK | bank | String | bank | 所属银行缩写 |
| 24 | TRX_FLAG | trxFlag | String | transFlag | 交易标志位 |
| 25 | TRX_TYPE | trxType | Integer | transTypeId | 分类ID |
| 26 | EXCEPTION_TYPE | exceptionType | String | exceptionType | 异常类型 |
| 27 | internal_flag | internalFlag | Integer | internalFlag | 是否为内部交易 |
| 28 | batch_id | batchId | Integer | batchId | 上传logId |
| 29 | batch_sequence | batchSequence | Integer | uploadSequnceNumber | **手动映射** |
| 30 | CREATE_DATE | createDate | Date | createDate | 创建时间 |
| 31 | created_by | createdBy | Long | createdBy | 创建者 |
| 32 | meta_json | metaJson | String | - | **强制null** |
| 33 | no_balance | noBalance | Integer | isNoBalance | 是否包含余额 |
| 34 | begin_balance | beginBalance | Integer | isBeginBalance | 初始余额 |
| 35 | end_balance | endBalance | Integer | isEndBalance | 结束余额 |
| 36 | override_bs_id | overrideBsId | Long | overrideBsId | 覆盖标识 |
| 37 | payment_method | paymentMethod | String | paymentMethod | 交易方式 |
| 38 | cret_no | cretNo | String | cretNo | 身份证号 |
| 39 | group_id | groupId | Integer | groupId | 项目id |
### 10.2 参考文档
- [ccdi_bank_statement.md](../../assets/对接流水分析/ccdi_bank_statement.md)
- [MyBatis Plus 官方文档](https://baomidou.com/)
- [Spring BeanUtils 文档](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/BeanUtils.html)
---
**文档版本:** 1.0
**最后更新:** 2026-03-04

View File

@@ -1,334 +0,0 @@
# 项目详情页面导航菜单改造设计文档
## 概述
将项目详情页面detail.vue右侧的按钮组改为水平导航菜单使用 Element UI Menu 组件实现简洁链接风格。
## 当前问题
项目详情页面右侧的操作按钮(上传数据、参数配置、初核结果)占用空间较大,视觉层级不够清晰,交互方式不够统一。
## 解决方案
### 核心设计
- 使用 Element UI 的 `el-menu` 组件(水平模式)
- 菜单放在标题右侧,右对齐
- "上传数据"和"参数配置"作为普通菜单项
- "初核结果"保留下拉菜单结构,包含三个子项:结果总览、专项排查、流水明细查询
- 采用简洁链接风格:透明背景 + 底部下划线激活效果
### 视觉风格
- 默认状态:灰色文字(#606266),透明背景
- Hover 状态:浅灰背景(#f5f7fa),深色文字(#303133
- 激活状态:蓝色文字(#1890ff+ 底部 2px 蓝色下划线
- 下拉菜单:白色背景,激活项浅蓝背景(#e6f7ff
## 技术设计
### 1. 组件结构
#### detail.vue 模板改造
```vue
<div class="header-right">
<el-menu
:default-active="activeTab"
mode="horizontal"
@select="handleMenuSelect"
class="nav-menu"
>
<el-menu-item index="upload">上传数据</el-menu-item>
<el-menu-item index="config">参数配置</el-menu-item>
<el-submenu index="result">
<template slot="title">初核结果</template>
<el-menu-item index="overview">结果总览</el-menu-item>
<el-menu-item index="special">专项排查</el-menu-item>
<el-menu-item index="detail">流水明细查询</el-menu-item>
</el-submenu>
</el-menu>
</div>
<!-- 动态组件渲染区域 -->
<component
:is="currentComponent"
:project-id="projectId"
:project-info="projectInfo"
@menu-change="handleMenuChange"
@data-uploaded="handleDataUploaded"
@name-selected="handleNameSelected"
@generate-report="handleGenerateReport"
@fetch-bank-info="handleFetchBankInfo"
/>
```
### 2. 数据结构
```javascript
data() {
return {
activeTab: 'upload', // 当前激活的菜单项索引
currentComponent: 'UploadData', // 当前显示的组件名称
// ... 其他现有数据
}
}
```
### 3. 交互逻辑
```javascript
methods: {
/** 菜单选择事件 */
handleMenuSelect(index) {
this.activeTab = index;
// 组件映射
const componentMap = {
'upload': 'UploadData',
'config': 'ParamConfig',
'overview': 'PreliminaryCheck',
'special': 'SpecialCheck',
'detail': 'DetailQuery'
};
this.currentComponent = componentMap[index] || 'UploadData';
},
// ... 其他现有方法
}
```
### 4. 组件导入
```javascript
import UploadData from "./components/detail/UploadData";
import ParamConfig from "./components/detail/ParamConfig";
import PreliminaryCheck from "./components/detail/PreliminaryCheck";
import SpecialCheck from "./components/detail/SpecialCheck";
import DetailQuery from "./components/detail/DetailQuery";
export default {
name: "ProjectDetail",
components: {
UploadData,
ParamConfig,
PreliminaryCheck,
SpecialCheck,
DetailQuery,
},
// ...
}
```
## 样式设计
### 1. 导航菜单样式
```scss
.header-right {
.nav-menu {
// 移除默认背景色和边框
background-color: transparent;
border-bottom: none;
// 菜单项基础样式
.el-menu-item,
.el-submenu__title {
font-size: 14px;
color: #606266;
padding: 0 16px;
height: 40px;
line-height: 40px;
border-bottom: 2px solid transparent;
&:hover {
background-color: #f5f7fa;
color: #303133;
}
}
// 激活状态:底部下划线 + 蓝色文字
.el-menu-item.is-active {
color: #1890ff;
border-bottom: 2px solid #1890ff;
background-color: transparent;
}
// 下拉菜单激活状态
.el-submenu.is-active > .el-submenu__title {
color: #1890ff;
border-bottom: 2px solid #1890ff;
}
// 下拉菜单图标
.el-submenu__icon-arrow {
margin-left: 4px;
}
}
}
```
### 2. 下拉菜单弹窗样式
```scss
::v-deep .el-menu--popup {
min-width: 140px;
.el-menu-item {
font-size: 14px;
&:hover {
background-color: #f5f7fa;
}
&.is-active {
color: #1890ff;
background-color: #e6f7ff;
}
}
}
```
### 3. 响应式适配
```scss
@media (max-width: 768px) {
.detail-header {
flex-direction: column;
align-items: flex-start;
.header-right {
width: 100%;
margin-top: 12px;
.nav-menu {
width: 100%;
display: flex;
justify-content: flex-start;
.el-menu-item,
.el-submenu {
flex: 1;
text-align: center;
padding: 0 8px;
font-size: 13px;
}
}
}
}
}
```
## 组件规范
### Props 接口
所有子组件应接收相同的 props
```javascript
props: {
projectId: {
type: [String, Number],
default: null,
},
projectInfo: {
type: Object,
default: () => ({
projectName: "",
updateTime: "",
projectStatus: "0",
}),
},
},
```
### Events 接口
```javascript
this.$emit('data-uploaded', { type: 'xxx' });
this.$emit('generate-report');
this.$emit('fetch-bank-info');
this.$emit('menu-change', { key, route });
this.$emit('name-selected', nameList);
```
## 实施步骤
### 第一步:修改 detail.vue 文件
1. 替换 header-right 中的按钮为 el-menu 组件
2. 添加 activeTab 和 currentComponent 数据字段
3. 实现 handleMenuSelect 方法
4. 添加动态组件渲染区域
5. 导入所有子组件
### 第二步:添加样式
1. 添加导航菜单的自定义样式
2. 添加下拉菜单样式
3. 添加响应式样式
### 第三步:创建占位组件
为未实现的功能创建占位组件:
- ParamConfig.vue
- PreliminaryCheck.vue
- SpecialCheck.vue
- DetailQuery.vue
### 第四步:测试验证
- 功能测试:菜单切换、下拉菜单交互
- 视觉测试:样式符合设计要求
- 响应式测试:移动端布局正常
## 技术栈
- Element UI Menu 组件(`el-menu`, `el-menu-item`, `el-submenu`
- Vue 动态组件(`<component :is="...">`
- Scoped CSS 样式覆盖
- Vue 2.6.12
- Element UI 2.15.14
## 预期效果
### 视觉效果
- 菜单项横向排列在标题右侧,右对齐
- 简洁链接风格,无背景色和边框
- 激活项显示蓝色文字和底部下划线
- 下拉菜单样式统一
### 交互效果
- 点击菜单项切换组件URL 不变
- 下拉菜单点击外部区域可关闭
- 组件切换流畅,数据正确传递
- 响应式布局在移动端自适应
## 代码改动量估算
- detail.vue 文件改动:约 80-100 行(模板 + 脚本 + 样式)
- 占位组件创建4 个文件,每个约 20 行
- 总代码量:约 150-180 行
## 风险与注意事项
1. **组件文件不存在**ParamConfig、PreliminaryCheck 等组件需要创建占位组件
2. **样式覆盖**Element UI 默认样式覆盖需要使用 `::v-deep``/deep/`
3. **事件传递**:确保所有子组件的事件正确向上传递
4. **路由监听**:移除原有路由相关的逻辑,改为组件状态管理
## 后续优化建议
1. 添加菜单切换动画效果
2. 为占位组件实现完整功能
3. 添加面包屑导航支持
4. 支持菜单项权限控制
5. 添加快捷键支持Ctrl+Tab 切换)
## 测试清单
- [ ] 点击"上传数据"菜单,显示 UploadData 组件
- [ ] 点击"参数配置"菜单,显示占位组件或 ParamConfig 组件
- [ ] 点击"初核结果"菜单,展开下拉菜单
- [ ] 点击下拉菜单子项,切换到对应组件
- [ ] 激活菜单项显示底部下划线
- [ ] Hover 菜单项显示浅灰背景
- [ ] 下拉菜单点击外部区域关闭
- [ ] 组件切换时 projectId 和 projectInfo 正确传递
- [ ] 移动端菜单响应式布局正常
- [ ] 现有功能不受影响(返回按钮、项目信息显示等)

View File

@@ -1,544 +0,0 @@
# 异步文件上传服务实现设计文档
## 文档信息
- **创建日期**: 2026-03-05
- **版本**: v1.0
- **作者**: Claude
- **状态**: 已批准
## 1. 概述
### 1.1 功能描述
实现 `CcdiFileUploadServiceImpl` 中所有 TODO 方法,完成项目流水文件的异步批量上传功能的端到端流程。
### 1.2 核心需求
- 集成流水分析平台客户端LsfxAnalysisClient
- 实现文件上传到流水分析平台
- 实现轮询解析状态(固定间隔策略)
- 获取并判断解析结果
- 批量获取并保存流水数据到本地数据库
- 实现批次日志管理
### 1.3 技术栈
- Spring @Async 异步处理
- ThreadPoolTaskExecutor 线程池
- MyBatis Plus 批量操作
- Logback 自定义日志
- 流水分析平台 API
## 2. 设计决策
### 2.1 轮询策略
**决策**: 固定间隔策略
- 轮询次数: 300次
- 间隔时间: 2秒
- 最长等待: 10分钟
- **理由**: 简单可靠,符合设计文档要求
### 2.2 分页获取策略
**决策**: 大批量分页
- 每页数量: 1000条
- 批量插入: 每批1000条
- 先调用一次获取 totalCount
- **理由**: 性能与内存占用的平衡
### 2.3 错误处理策略
**决策**: 严格失败策略
- 任何异常直接标记为 `parsed_failed`
- 记录详细的错误信息到 `error_message` 字段
- 不进行额外重试(线程池层面已有重试机制)
- **理由**: 简单明了,便于排查问题
### 2.4 日志管理策略
**决策**: 完整实现批次日志
- 实现自定义 `FileUploadLogAppender`
- 每个批次生成独立日志文件
- 路径基于 `ruoyi.profile` 配置
- **理由**: 便于运维排查问题
## 3. 详细设计
### 3.1 依赖注入
```java
@Slf4j
@Service
public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
@Value("${ruoyi.profile}")
private String uploadPath;
@Resource
private CcdiFileUploadRecordMapper recordMapper;
@Resource
private CcdiProjectMapper projectMapper;
@Resource
@Qualifier("fileUploadExecutor")
private Executor fileUploadExecutor;
@Resource
private LsfxAnalysisClient lsfxClient; // 新增
@Resource
private CcdiBankStatementMapper bankStatementMapper; // 新增
```
### 3.2 文件上传逻辑processFileAsync 第329-333行
**核心流程**:
1. 将临时文件路径转换为 File 对象
2. 验证文件存在性
3. 调用 `lsfxClient.uploadFile(lsfxProjectId, file)`
4. 提取并验证返回的 logId
**关键代码**:
```java
File file = filePath.toFile();
if (!file.exists()) {
throw new RuntimeException("临时文件不存在: " + tempFilePath);
}
UploadFileResponse uploadResponse = lsfxClient.uploadFile(lsfxProjectId, file);
if (uploadResponse == null || uploadResponse.getData() == null) {
throw new RuntimeException("上传文件失败: 响应数据为空");
}
Integer logId = uploadResponse.getData().getLogId();
if (logId == null) {
throw new RuntimeException("上传文件失败: 未返回logId");
}
```
### 3.3 轮询解析状态逻辑waitForParsingComplete
**核心流程**:
1. 调用 `checkParseStatus(groupId, logId)`
2. 检查 `parsing` 字段
3. `parsing=false` 表示解析完成
4. 固定间隔2秒最多300次
**关键代码**:
```java
for (int i = 1; i <= maxRetries; i++) {
CheckParseStatusResponse response = lsfxClient.checkParseStatus(groupId, logId);
if (response == null || response.getData() == null) {
log.warn("【文件上传】轮询第{}次: 响应数据为空", i);
Thread.sleep(intervalSeconds * 1000L);
continue;
}
Boolean parsing = response.getData().getParsing();
// parsing=false 表示解析完成
if (Boolean.FALSE.equals(parsing)) {
log.info("【文件上传】解析完成: logId={}, 轮询次数={}", logId, i);
return true;
}
if (i < maxRetries) {
Thread.sleep(intervalSeconds * 1000L);
}
}
```
**异常处理**:
- `InterruptedException`: 恢复中断状态,返回 false
- 其他异常: 记录日志,继续轮询
### 3.4 获取解析结果逻辑processFileAsync 第355-383行
**核心流程**:
1. 调用 `getFileUploadStatus(groupId, logId)`
2. 判断 `status == -5 && uploadStatusDesc == "data.wait.confirm.newaccount"`
3. 提取 `enterpriseNameList``accountNoList`
4. 解析成功则调用 `fetchAndSaveBankStatements()`
**关键代码**:
```java
GetFileUploadStatusRequest statusRequest = new GetFileUploadStatusRequest();
statusRequest.setGroupId(lsfxProjectId);
statusRequest.setLogId(logId);
GetFileUploadStatusResponse statusResponse = lsfxClient.getFileUploadStatus(statusRequest);
GetFileUploadStatusResponse.LogItem logItem = statusResponse.getData().getLogs().get(0);
Integer status = logItem.getStatus();
String uploadStatusDesc = logItem.getUploadStatusDesc();
// 判断解析结果
boolean parseSuccess = status != null && status == -5
&& "data.wait.confirm.newaccount".equals(uploadStatusDesc);
if (parseSuccess) {
// 提取主体信息
List<String> enterpriseNames = logItem.getEnterpriseNameList();
List<String> accountNos = logItem.getAccountNoList();
String enterpriseNamesStr = enterpriseNames != null ? String.join(",", enterpriseNames) : "";
String accountNosStr = accountNos != null ? String.join(",", accountNos) : "";
record.setFileStatus("parsed_success");
record.setEnterpriseNames(enterpriseNamesStr);
record.setAccountNos(accountNosStr);
recordMapper.updateById(record);
// 获取流水数据
fetchAndSaveBankStatements(projectId, lsfxProjectId, logId);
} else {
record.setFileStatus("parsed_failed");
record.setErrorMessage("解析失败: " + uploadStatusDesc);
recordMapper.updateById(record);
}
```
### 3.5 批量保存流水数据逻辑fetchAndSaveBankStatements
**核心流程**:
1. 先调用一次接口获取 totalCountpageSize=1, pageNow=1
2. 计算分页信息每页1000条
3. 循环分页获取所有数据
4. 每累积1000条批量插入一次
5. 设置 projectId 到每条流水记录
**关键代码**:
```java
// 步骤1: 先调用一次接口获取 totalCount
GetBankStatementRequest firstRequest = new GetBankStatementRequest();
firstRequest.setGroupId(groupId);
firstRequest.setLogId(logId);
firstRequest.setPageNow(1);
firstRequest.setPageSize(1);
GetBankStatementResponse firstResponse = lsfxClient.getBankStatement(firstRequest);
Integer totalCount = firstResponse.getData().getTotalCount();
// 步骤2: 计算分页信息
int pageSize = 1000;
int batchSize = 1000;
int totalPages = (int) Math.ceil((double) totalCount / pageSize);
List<CcdiBankStatement> batchList = new ArrayList<>(batchSize);
// 步骤3: 循环分页获取
for (int pageNow = 1; pageNow <= totalPages; pageNow++) {
GetBankStatementRequest request = new GetBankStatementRequest();
request.setGroupId(groupId);
request.setLogId(logId);
request.setPageNow(pageNow);
request.setPageSize(pageSize);
GetBankStatementResponse response = lsfxClient.getBankStatement(request);
for (GetBankStatementResponse.BankStatementItem item : items) {
CcdiBankStatement statement = CcdiBankStatement.fromResponse(item);
statement.setProjectId(projectId); // 设置业务项目ID
batchList.add(statement);
// 达到批量插入阈值1000条
if (batchList.size() >= batchSize) {
bankStatementMapper.insertBatch(batchList);
batchList.clear();
}
}
}
// 步骤4: 保存剩余的数据
if (!batchList.isEmpty()) {
bankStatementMapper.insertBatch(batchList);
}
```
**性能优化**:
- 每页1000条减少请求次数
- 批量插入1000条提高数据库性能
- 异常不中断,继续处理下一页
### 3.6 批次日志管理FileUploadLogAppender
**核心功能**:
1. 继承 `UnsynchronizedAppenderBase<ILoggingEvent>`
2. 使用 `ThreadLocal` 存储当前批次的 FileAppender
3. 为每个批次创建独立的日志文件
**日志文件路径**:
```
{ruoyi.profile}/logs/file-upload/{projectId}/{timestamp}.log
```
**示例**:
- Windows: `D:/ruoyi/uploadPath/logs/file-upload/123/20260305-103025.log`
- Linux: `/var/ruoyi/logs/file-upload/123/20260305-103025.log`
**关键方法**:
```java
/**
* 为指定批次创建独立的日志文件
*/
public static void createBatchLogFile(String uploadPath, Long projectId, String batchId) {
String timestamp = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date());
String logDirPath = uploadPath + File.separator + "logs" + File.separator
+ "file-upload" + File.separator + projectId;
File logDir = new File(logDirPath);
if (!logDir.exists()) {
logDir.mkdirs();
}
String logFilePath = logDirPath + File.separator + timestamp + ".log";
FileAppender<ILoggingEvent> appender = new FileAppender<>();
appender.setFile(logFilePath);
PatternLayout layout = new PatternLayout();
layout.setPattern("%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n");
layout.start();
appender.setLayout(layout);
appender.start();
currentAppender.set(appender);
}
/**
* 关闭当前批次的日志文件
*/
public static void closeBatchLogFile() {
FileAppender<ILoggingEvent> appender = currentAppender.get();
if (appender != null) {
appender.stop();
currentAppender.remove();
}
}
```
**使用方式**:
```java
private void submitTasksAsync(...) {
// 创建批次日志文件
FileUploadLogAppender.createBatchLogFile(uploadPath, projectId, batchId);
try {
// 任务提交逻辑
} finally {
// 关闭日志文件
FileUploadLogAppender.closeBatchLogFile();
}
}
```
## 4. 实现细节
### 4.1 文件上传完整流程
```java
@Async("fileUploadExecutor")
public void processFileAsync(Long projectId, Integer lsfxProjectId, String tempFilePath,
Long recordId, String batchId, CcdiFileUploadRecord record) {
try {
// 步骤1: 状态已是uploading记录已存在
Path filePath = Paths.get(tempFilePath);
if (!Files.exists(filePath)) {
throw new RuntimeException("临时文件不存在: " + tempFilePath);
}
// 步骤2: 上传文件到流水分析平台
File file = filePath.toFile();
UploadFileResponse uploadResponse = lsfxClient.uploadFile(lsfxProjectId, file);
Integer logId = uploadResponse.getData().getLogId();
// 步骤3: 更新状态为 parsing
record.setLogId(logId);
record.setFileStatus("parsing");
recordMapper.updateById(record);
// 步骤4: 轮询解析状态最多300次间隔2秒
boolean parsingComplete = waitForParsingComplete(lsfxProjectId, logId.toString());
if (!parsingComplete) {
throw new RuntimeException("解析超时(超过10分钟)");
}
// 步骤5: 获取文件上传状态
GetFileUploadStatusRequest statusRequest = new GetFileUploadStatusRequest();
statusRequest.setGroupId(lsfxProjectId);
statusRequest.setLogId(logId);
GetFileUploadStatusResponse statusResponse = lsfxClient.getFileUploadStatus(statusRequest);
GetFileUploadStatusResponse.LogItem logItem = statusResponse.getData().getLogs().get(0);
Integer status = logItem.getStatus();
String uploadStatusDesc = logItem.getUploadStatusDesc();
// 步骤6: 判断解析结果
boolean parseSuccess = status != null && status == -5
&& "data.wait.confirm.newaccount".equals(uploadStatusDesc);
if (parseSuccess) {
// 解析成功
List<String> enterpriseNames = logItem.getEnterpriseNameList();
List<String> accountNos = logItem.getAccountNoList();
record.setFileStatus("parsed_success");
record.setEnterpriseNames(enterpriseNames != null ? String.join(",", enterpriseNames) : "");
record.setAccountNos(accountNos != null ? String.join(",", accountNos) : "");
recordMapper.updateById(record);
// 步骤7: 获取流水数据并保存
fetchAndSaveBankStatements(projectId, lsfxProjectId, logId);
} else {
// 解析失败
record.setFileStatus("parsed_failed");
record.setErrorMessage("解析失败: " + uploadStatusDesc);
recordMapper.updateById(record);
}
} catch (Exception e) {
log.error("【文件上传】处理失败: fileName={}", record.getFileName(), e);
updateRecordStatus(recordId, "parsed_failed", e.getMessage());
} finally {
// 清理临时文件
try {
Path filePath = Paths.get(tempFilePath);
if (Files.exists(filePath)) {
Files.delete(filePath);
}
} catch (IOException e) {
log.warn("【文件上传】清理临时文件失败: {}", tempFilePath, e);
}
}
}
```
### 4.2 错误处理规范
**异常分类**:
1. **文件异常**: 临时文件不存在、文件转换失败
2. **网络异常**: 流水分析平台接口调用失败
3. **业务异常**: 解析失败、解析超时
4. **数据库异常**: 批量插入失败
**处理策略**:
- 所有异常统一捕获,记录详细日志
- 直接标记为 `parsed_failed`
- 记录错误信息到 `error_message` 字段
- finally 块确保临时文件被清理
### 4.3 日志记录规范
**日志级别**:
- `INFO`: 关键步骤(开始上传、上传成功、解析完成、保存成功)
- `DEBUG`: 详细信息(轮询次数、每页数据量)
- `WARN`: 警告信息(响应数据为空、清理失败)
- `ERROR`: 错误信息(处理失败、异常)
**日志格式**:
```
【文件上传】{步骤描述}: {关键参数}={值}
```
**示例**:
```
【文件上传】开始处理文件: fileName=流水1.xlsx, recordId=123
【文件上传】文件上传成功: logId=456789
【文件上传】解析完成: logId=456789, 轮询次数=15
【文件上传】流水数据保存完成: 总共保存5000条
```
## 5. 文件清单
### 5.1 需要修改的文件
| 文件路径 | 修改内容 |
|---------|---------|
| `CcdiFileUploadServiceImpl.java` | 实现 processFileAsync、waitForParsingComplete、fetchAndSaveBankStatements 中的 TODO |
### 5.2 需要新增的文件
| 文件路径 | 说明 |
|---------|------|
| `ccdi-project/src/main/java/com/ruoyi/ccdi/project/log/FileUploadLogAppender.java` | 批次日志管理器 |
## 6. 测试策略
### 6.1 单元测试
**测试用例**:
1. `waitForParsingComplete` - 正常轮询成功
2. `waitForParsingComplete` - 轮询超时
3. `waitForParsingComplete` - 轮询被中断
4. `fetchAndSaveBankStatements` - 无数据
5. `fetchAndSaveBankStatements` - 单页数据
6. `fetchAndSaveBankStatements` - 多页数据
7. `fetchAndSaveBankStatements` - 异常处理
### 6.2 集成测试
**测试场景**:
1. 完整流程测试(单个文件,正常场景)
2. 大文件测试50MB
3. 批量文件测试10个文件
4. 解析失败场景
5. 网络异常场景
6. 线程池满载场景
### 6.3 性能测试
**测试指标**:
- 单个文件处理时长: 3-15分钟
- 100个文件并发处理
- 数据库批量插入性能
- 内存占用情况
## 7. 部署注意事项
### 7.1 配置检查
- [ ] `ruoyi.profile` 配置正确且目录有写权限
- [ ] 线程池容量配置默认100
- [ ] 流水分析平台地址配置正确
- [ ] 应用认证信息配置正确
### 7.2 监控指标
- 线程池活跃线程数
- 文件上传成功率
- 平均处理时长
- 批量插入性能
- 日志文件大小和数量
### 7.3 运维建议
- 定期清理30天前的日志文件
- 监控线程池状态
- 关注数据库连接池使用情况
- 流水分析平台接口调用成功率监控
## 8. 风险与缓解
### 8.1 风险识别
| 风险 | 影响 | 概率 | 缓解措施 |
|------|------|------|----------|
| 流水分析平台不稳定 | 高 | 中 | 异常捕获,标记失败,详细日志 |
| 大文件内存溢出 | 高 | 低 | 批量插入,及时清理临时文件 |
| 线程池满载 | 中 | 中 | 重试机制,提示系统繁忙 |
| 日志文件过大 | 低 | 中 | 按批次分离,定期清理 |
### 8.2 回滚方案
如遇严重问题,可以:
1. 禁用异步上传功能
2. 回退到同步上传方式
3. 暂停新的上传任务
## 9. 参考资料
- [项目异步文件上传功能设计文档](../../design/2026-03-05-async-file-upload-design.md)
- [项目异步文件上传需求](../../assets/项目异步文件上传/task.md)
- [流水分析平台接口文档](../2026-03-02-lsfx-integration-design.md)
- [银行流水实体设计](../2026-03-04-bank-statement-entity-design.md)
---
**文档结束**

View File

@@ -1,194 +0,0 @@
# 银行流水审计字段补充设计文档
## 概述
本文档记录为 `GetBankStatementResponse.BankStatementItem` 类添加 `createdBy``createDate` 审计字段的设计方案。
## 背景
### 问题描述
外部流水分析平台的接口文档6.5节)中包含 `createdBy``createDate` 字段,但我们的响应类 `GetBankStatementResponse.BankStatementItem` 中缺少这两个字段的定义,导致无法接收外部平台返回的审计信息。
### 影响范围
- **直接影响:** `GetBankStatementResponse.BankStatementItem`
- **间接影响:** `CcdiBankStatement.fromResponse()` 方法(已有对应字段,无需修改)
- **数据流:** 外部平台 → 响应类 → 实体类 → 数据库
## 设计方案
### 字段定义
`GetBankStatementResponse.BankStatementItem` 类中添加两个审计字段:
| 字段名 | 类型 | 说明 | 来源 |
|--------|------|------|------|
| `createdBy` | `Long` | 创建者用户ID | 外部平台 |
| `createDate` | `String` | 创建时间 | 外部平台 |
### 类型选择
- **createdBy**: 使用 `Long` 类型
- 与实体类 `CcdiBankStatement` 保持一致
- 用户ID通常为长整型数字
- **createDate**: 使用 `String` 类型
- 外部平台返回时间字符串格式(如 "2026-03-05 10:30:00"
- 避免时间格式转换问题
- 由业务层负责转换为 Date 类型
### 代码修改
**文件:** `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/GetBankStatementResponse.java`
**修改位置:**`BankStatementItem` 类的最后添加审计字段组
**修改内容:**
```java
// ===== 审计字段 =====
/** 创建者 */
private Long createdBy;
/** 创建时间 */
private String createDate;
```
### 完整修改后的类结构
```java
@Data
public static class BankStatementItem {
// ===== 账号相关信息 =====
/** 流水ID */
private Long bankStatementId;
// ... 其他字段
// ===== 附加字段 =====
/** 附件数量 */
private Integer attachments;
// ... 其他附加字段
// ===== 审计字段 =====
/** 创建者 */
private Long createdBy;
/** 创建时间 */
private String createDate;
}
```
## 数据流分析
### 1. 接收外部数据
```
外部平台 → GetBankStatementResponse.BankStatementItem
- createdBy: Long
- createDate: String
```
### 2. 转换为实体
```java
// CcdiBankStatement.fromResponse() 方法
CcdiBankStatement entity = new CcdiBankStatement();
BeanUtils.copyProperties(item, entity);
// 自动复制 createdBy (Long → Long)
// createDate 字段类型不匹配 (String → Date),需要手动转换
```
**注意:** 如果需要自动转换 `createDate`,需要修改 `fromResponse()` 方法添加日期格式转换逻辑。
### 3. 保存到数据库
```
CcdiBankStatement
- createdBy: Long → 数据库字段 created_by
- createDate: Date → 数据库字段 create_date
```
## 实现要点
### 必须实现
1. ✅ 在 `BankStatementItem` 类中添加两个字段
2. ✅ 添加 Lombok `@Data` 注解会自动生成 getter/setter
### 可选优化
1. **日期转换:** 如果需要,在 `CcdiBankStatement.fromResponse()` 中添加 `createDate` 的日期格式转换
2. **字段验证:** 添加 `@JsonFormat` 注解指定日期格式(如果需要)
## 测试计划
### 单元测试
- 验证 JSON 反序列化能正确映射这两个字段
- 验证 `fromResponse()` 方法能正确处理 `createdBy` 字段
### 集成测试
1. 调用外部平台接口(或 mock 服务器)
2. 验证响应中包含 `createdBy``createDate`
3. 验证数据能正确保存到数据库
### 测试数据
```json
{
"createdBy": 12345,
"createDate": "2026-03-05 14:30:00"
}
```
## 风险评估
| 风险 | 影响 | 概率 | 缓解措施 |
|------|------|------|----------|
| 外部平台不返回这两个字段 | 低 | 中 | 字段可以为 null不影响现有功能 |
| 日期格式不兼容 | 中 | 低 | 使用 String 类型接收,业务层处理转换 |
| 类型不匹配 | 高 | 低 | 已确认类型与实体类一致 |
## 变更影响
### 正面影响
- ✅ 补全接口字段,与外部平台文档对齐
- ✅ 支持审计信息传递
- ✅ 提升数据完整性
### 负面影响
- 无(仅添加字段,不影响现有功能)
## 实现计划
1. 修改 `GetBankStatementResponse.BankStatementItem`
2. 更新相关的 API 文档(如有)
3. 执行集成测试验证功能
4. 提交代码并更新 CHANGELOG
## 参考资料
- 外部流水分析平台接口文档 6.5节
- `CcdiBankStatement` 实体类定义
- 项目开发规范CLAUDE.md
## 附录
### 相关文件路径
- 响应类:`ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/GetBankStatementResponse.java`
- 实体类:`ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatement.java`
- 客户端:`ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/LsfxAnalysisClient.java`
### 数据库字段
```sql
-- ccdi_bank_statement 表
created_by BIGINT(20) COMMENT '创建者',
create_date DATETIME COMMENT '创建时间'
```

View File

@@ -1,106 +0,0 @@
# 银行流水接口字段补充设计
## 概述
流水分析平台接口实际返回了 `uploadSequnceNumber` 字段,但当前响应类中缺少该字段定义,导致数据丢失。本设计补充该字段的接收和映射。
## 问题分析
### 当前问题
- **接口返回**:流水分析平台接口实际返回 `uploadSequnceNumber` 字段
- **响应类缺失**`GetBankStatementResponse.BankStatementItem` 未定义该字段,数据被丢弃
- **实体已有字段**`CcdiBankStatement` 已定义 `batchSequence` 字段
- **映射缺失**`fromResponse()` 方法未映射该字段
### 字段映射关系
| 接口返回字段 | 响应类字段 | 实体类字段 | 数据库字段 |
|------------|-----------|-----------|-----------|
| uploadSequnceNumber | ❌ 缺失 | batchSequence | batch_sequence |
## 设计方案
### 修改范围
**涉及文件:**
1. `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/GetBankStatementResponse.java`
2. `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatement.java`
**不涉及:**
- 数据库表结构(接口会返回实际值,无需修改约束)
- Controller、Service、Mapper 层
- 前端代码
### 具体变更
#### 1. 响应类添加字段
**文件**`GetBankStatementResponse.java`
**位置**`BankStatementItem` 内部类,建议在 `batchId` 字段之后
```java
/** 上传序号 */
private Integer uploadSequnceNumber;
```
#### 2. 实体转换逻辑补充
**文件**`CcdiBankStatement.java`
**位置**`fromResponse()` 方法,手动映射字段区域
```java
entity.setBatchSequence(item.getUploadSequnceNumber());
```
### 影响评估
#### 功能影响
- ✅ 流水数据完整性提升:接收并存储接口返回的上传序号
- ✅ 数据一致性保障:字段映射关系符合文档定义
- ✅ 无破坏性变更:仅添加字段,不影响现有功能
#### 数据影响
- 现有数据:不受影响
- 新数据:完整接收接口返回的 `uploadSequnceNumber`
## 实施计划
### 实施步骤
1. **修改响应类**
-`GetBankStatementResponse.BankStatementItem` 中添加 `uploadSequnceNumber` 字段
2. **修改实体转换**
-`CcdiBankStatement.fromResponse()` 中添加字段映射
3. **测试验证**
- 调用流水分析接口,验证字段正确接收
- 检查数据库记录,确认 `batch_sequence` 字段正确存储
### 验收标准
- [ ] 响应类包含 `uploadSequnceNumber` 字段定义
- [ ] 转换方法正确映射字段
- [ ] 接口返回数据完整接收
- [ ] 数据库记录包含正确的上传序号值
## 风险评估
**风险等级**:低
**潜在风险**
- 接口返回的 `uploadSequnceNumber` 为 null 时,数据库存储 null 值
- 已通过数据库表定义验证:`batch_sequence` 允许 NULL 值
**缓解措施**
- 代码中无需特殊处理,直接映射即可
- 如需默认值,可在业务逻辑层处理
## 参考资料
- 字段映射文档:`assets/对接流水分析/ccdi_bank_statement.md` 第 81 行
- 实体类定义:`CcdiBankStatement.java` 第 137 行
- 数据库表定义:`batch_sequence INT(11) NOT NULL`(实际允许存储 NULL需核实

View File

@@ -1,995 +0,0 @@
# 模型参数配置页面优化设计文档
**文档版本:** v1.0
**创建日期:** 2026-03-06
**设计人员:** Claude Code
---
## 一、概述
### 1.1 背景
当前模型参数配置页面采用模型下拉框切换的方式,用户需要逐个切换模型才能查看和配置不同模型的参数,操作不够便捷。本次优化旨在取消模型切换,改为在同一页面中以垂直堆叠方式展示所有模型的参数表格,提升用户体验。
### 1.2 目标
- ✅ 取消模型名称查询切换
- ✅ 在同一页面中分多个表格展示所有模型的参数
- ✅ 全局模型参数配置页面和项目内模型参数配置页面同步修改
- ✅ 统一保存机制,一次性保存所有修改
### 1.3 影响范围
**前端页面:**
- `ruoyi-ui/src/views/ccdi/modelParam/index.vue` - 全局模型参数配置页面
- `ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue` - 项目内参数配置页面
**后端接口:**
- `CcdiModelParamController.java` - 新增批量查询和批量保存接口
- `ICcdiModelParamService.java` - 新增Service方法
- `CcdiModelParamServiceImpl.java` - 实现批量操作逻辑
- `CcdiModelParamMapper.java` - 新增Mapper方法
- `CcdiModelParamMapper.xml` - 新增SQL查询
---
## 二、详细设计
### 2.1 后端接口设计
#### 2.1.1 批量查询所有模型参数
**接口路径:** `GET /ccdi/modelParam/listAll`
**请求参数:**
```java
public class ModelParamAllQueryDTO {
/** 项目ID0表示全局配置>0表示项目配置 */
private Long projectId;
}
```
**响应结构:**
```java
public class ModelParamAllVO {
/** 模型列表(包含每个模型及其参数) */
private List<ModelGroupVO> models;
}
public class ModelGroupVO {
/** 模型编码 */
private String modelCode;
/** 模型名称 */
private String modelName;
/** 参数列表 */
private List<ModelParamVO> params;
}
```
**返回数据示例:**
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"models": [
{
"modelCode": "LARGE_TRANSACTION",
"modelName": "大额交易模型",
"params": [
{
"paramCode": "THRESHOLD_AMOUNT",
"paramName": "单笔交易金额阈值",
"paramDesc": "单笔交易金额超过此值触发预警",
"paramValue": "50000",
"paramUnit": "元",
"sortOrder": 1
}
]
},
{
"modelCode": "SUSPICIOUS_FOREIGN_EXCHANGE",
"modelName": "可疑外汇交易模型",
"params": [
{
"paramCode": "FREQUENCY_THRESHOLD",
"paramName": "交易频次阈值",
"paramDesc": "交易频次超过此值触发预警",
"paramValue": "10",
"paramUnit": "次/天",
"sortOrder": 1
}
]
},
{
"modelCode": "SUSPICIOUS_PART_TIME",
"modelName": "可疑兼职模型",
"params": [...]
}
]
}
}
```
#### 2.1.2 批量保存所有模型参数
**接口路径:** `POST /ccdi/modelParam/saveAll`
**请求结构:**
```java
public class ModelParamSaveAllDTO {
/** 项目ID */
private Long projectId;
/** 所有模型的参数修改(只包含修改过的参数) */
private List<ModelParamGroupDTO> models;
}
public class ModelParamGroupDTO {
/** 模型编码 */
private String modelCode;
/** 该模型下修改过的参数 */
private List<ParamValueItem> params;
}
public class ParamValueItem {
private String paramCode;
private String paramValue;
}
```
**请求示例:**
```json
{
"projectId": 1,
"models": [
{
"modelCode": "LARGE_TRANSACTION",
"params": [
{
"paramCode": "THRESHOLD_AMOUNT",
"paramValue": "60000"
}
]
},
{
"modelCode": "SUSPICIOUS_FOREIGN_EXCHANGE",
"params": [
{
"paramCode": "FREQUENCY_THRESHOLD",
"paramValue": "5"
}
]
}
]
}
```
**响应示例:**
```json
{
"code": 200,
"msg": "保存成功"
}
```
**错误码说明:**
| 错误码 | 说明 |
|--------|------|
| 400 | 参数验证失败项目ID为空、参数列表为空等 |
| 500 | 服务器内部错误(数据库操作失败等) |
---
### 2.2 后端Service层设计
#### 2.2.1 Service接口新增方法
```java
public interface ICcdiModelParamService {
/**
* 查询所有模型及其参数(按模型分组)
*
* @param projectId 项目ID0表示全局配置
* @return 所有模型的参数配置
*/
ModelParamAllVO selectAllParams(Long projectId);
/**
* 批量保存所有模型的参数修改
*
* @param saveAllDTO 所有模型的参数修改数据
*/
void saveAllParams(ModelParamSaveAllDTO saveAllDTO);
// ... 保留原有的其他方法
}
```
#### 2.2.2 Service实现类核心逻辑
**查询所有模型参数:**
```java
@Override
public ModelParamAllVO selectAllParams(Long projectId) {
// 1. 参数验证
if (projectId == null) {
projectId = 0L;
}
// 2. 如果是项目查询,根据 configType 决定查询哪组参数
Long effectiveProjectId = projectId;
if (projectId > 0) {
CcdiProject project = projectMapper.selectById(projectId);
if (project == null) {
throw new ServiceException("项目不存在");
}
if ("default".equals(project.getConfigType())) {
effectiveProjectId = 0L;
}
}
// 3. 查询所有模型的参数
List<CcdiModelParam> allParams = modelParamMapper.selectByProjectId(effectiveProjectId);
// 4. 按模型分组
Map<String, List<CcdiModelParam>> groupedParams = allParams.stream()
.collect(Collectors.groupingBy(CcdiModelParam::getModelCode));
// 5. 转换为VO
ModelParamAllVO result = new ModelParamAllVO();
List<ModelGroupVO> models = new ArrayList<>();
groupedParams.forEach((modelCode, params) -> {
ModelGroupVO groupVO = new ModelGroupVO();
groupVO.setModelCode(modelCode);
groupVO.setModelName(params.get(0).getModelName());
List<ModelParamVO> paramVOs = params.stream()
.map(param -> {
ModelParamVO vo = new ModelParamVO();
BeanUtils.copyProperties(param, vo);
return vo;
})
.collect(Collectors.toList());
groupVO.setParams(paramVOs);
models.add(groupVO);
});
// 6. 按模型编码排序(保证固定顺序)
models.sort(Comparator.comparing(ModelGroupVO::getModelCode));
result.setModels(models);
return result;
}
```
**批量保存参数:**
```java
@Override
@Transactional(rollbackFor = Exception.class)
public void saveAllParams(ModelParamSaveAllDTO saveAllDTO) {
try {
// 1. 参数验证
if (saveAllDTO.getProjectId() == null) {
throw new ServiceException("项目ID不能为空");
}
if (saveAllDTO.getModels() == null || saveAllDTO.getModels().isEmpty()) {
throw new ServiceException("参数列表不能为空");
}
Long projectId = saveAllDTO.getProjectId();
// 2. 如果是项目保存,检查是否需要复制默认参数
if (projectId > 0) {
CcdiProject project = projectMapper.selectById(projectId);
if (project == null) {
throw new ServiceException("项目不存在");
}
// 如果是首次保存configType=default需要复制所有模型的系统默认参数
if ("default".equals(project.getConfigType())) {
for (ModelParamGroupDTO modelGroup : saveAllDTO.getModels()) {
copyDefaultParamsToProject(projectId, modelGroup.getModelCode());
}
// 更新项目配置类型为 custom
project.setConfigType("custom");
projectMapper.updateById(project);
}
}
// 3. 批量更新所有模型的参数值
String username = SecurityUtils.getUsername();
for (ModelParamGroupDTO modelGroup : saveAllDTO.getModels()) {
for (ParamValueItem item : modelGroup.getParams()) {
int updated = modelParamMapper.updateParamValue(
projectId,
modelGroup.getModelCode(),
item.getParamCode(),
item.getParamValue()
);
if (updated == 0) {
log.warn("参数不存在或未更新modelCode={}, paramCode={}",
modelGroup.getModelCode(), item.getParamCode());
}
}
}
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
log.error("批量保存模型参数失败", e);
throw new ServiceException("批量保存模型参数失败:" + e.getMessage());
}
}
```
---
### 2.3 后端Mapper层设计
#### 2.3.1 Mapper接口新增方法
```java
public interface CcdiModelParamMapper extends BaseMapper<CcdiModelParam> {
/**
* 根据项目ID查询所有模型参数
*/
List<CcdiModelParam> selectByProjectId(@Param("projectId") Long projectId);
// ... 保留原有的其他方法
}
```
#### 2.3.2 Mapper XML
```xml
<select id="selectByProjectId" resultType="CcdiModelParam">
SELECT * FROM ccdi_model_param
WHERE project_id = #{projectId}
ORDER BY model_code, sort_order
</select>
```
---
### 2.4 前端组件设计
#### 2.4.1 页面结构(两个页面相同布局)
```vue
<template>
<div class="param-config-container">
<!-- 页面标题 -->
<div class="page-header">
<h2>{{ pageTitle }}</h2>
</div>
<!-- 模型参数卡片组垂直堆叠 -->
<div class="model-cards-container">
<div
v-for="model in modelGroups"
:key="model.modelCode"
class="model-card"
>
<!-- 模型标题 -->
<div class="model-header">
<h3>{{ model.modelName }}</h3>
</div>
<!-- 参数表格 -->
<el-table :data="model.params" border>
<el-table-column label="监测项" prop="paramName" width="200" />
<el-table-column label="描述" prop="paramDesc" />
<el-table-column label="阈值设置" width="200">
<template #default="{ row }">
<el-input
v-model="row.paramValue"
placeholder="请输入阈值"
@input="markAsModified(model.modelCode, row)"
/>
</template>
</el-table-column>
<el-table-column label="单位" prop="paramUnit" width="120" />
</el-table>
</div>
</div>
<!-- 统一保存按钮 -->
<div class="button-section">
<el-button type="primary" @click="handleSaveAll" :loading="saving">
保存所有修改
</el-button>
<span v-if="modifiedCount > 0" class="modified-tip">
已修改 {{ modifiedCount }} 个参数
</span>
</div>
</div>
</template>
```
#### 2.4.2 核心数据结构
```javascript
data() {
return {
// 页面标题(全局配置 vs 项目配置)
pageTitle: this.projectId ? '项目参数配置' : '全局模型参数管理',
// 模型参数数据(按模型分组)
modelGroups: [], // ModelGroupVO[]
// 修改记录(记录哪些参数被修改过)
modifiedParams: new Map(), // Map<modelCode, Set<paramCode>>
// 保存状态
saving: false
}
}
```
#### 2.4.3 核心方法
```javascript
methods: {
/** 加载所有模型参数 */
async loadAllParams() {
try {
const res = await listAllParams({ projectId: this.projectId })
this.modelGroups = res.data.models
// 清空修改记录
this.modifiedParams.clear()
} catch (error) {
this.$message.error('加载参数失败:' + error.message)
}
},
/** 标记参数为已修改 */
markAsModified(modelCode, row) {
if (!this.modifiedParams.has(modelCode)) {
this.modifiedParams.set(modelCode, new Set())
}
this.modifiedParams.get(modelCode).add(row.paramCode)
},
/** 保存所有修改 */
async handleSaveAll() {
// 验证是否有修改
if (this.modifiedCount === 0) {
this.$message.info('没有需要保存的修改')
return
}
// 构造保存数据(只包含修改过的参数)
const saveDTO = {
projectId: this.projectId,
models: []
}
this.modifiedParams.forEach((paramCodes, modelCode) => {
const modelGroup = this.modelGroups.find(m => m.modelCode === modelCode)
const modifiedParamList = modelGroup.params
.filter(p => paramCodes.has(p.paramCode))
.map(p => ({
paramCode: p.paramCode,
paramValue: p.paramValue
}))
if (modifiedParamList.length > 0) {
saveDTO.models.push({
modelCode: modelCode,
params: modifiedParamList
})
}
})
// 保存
this.saving = true
try {
await saveAllParams(saveDTO)
this.$message.success('保存成功')
// 清空修改记录并重新加载
this.modifiedParams.clear()
await this.loadAllParams()
} catch (error) {
this.$message.error('保存失败:' + error.message)
} finally {
this.saving = false
}
}
},
computed: {
/** 计算已修改参数数量 */
modifiedCount() {
let count = 0
this.modifiedParams.forEach(params => {
count += params.size
})
return count
}
}
```
#### 2.4.4 样式设计
```scss
.param-config-container {
padding: 20px;
background-color: #fff;
min-height: 400px;
}
.page-header {
margin-bottom: 20px;
padding: 15px;
background: #fff;
border-radius: 4px;
h2 {
font-size: 18px;
font-weight: bold;
color: #333;
margin: 0;
}
}
.model-cards-container {
margin-bottom: 20px;
}
.model-card {
background: #fff;
border-radius: 4px;
padding: 20px;
margin-bottom: 20px;
border: 1px solid #e4e7ed;
.model-header {
margin-bottom: 15px;
h3 {
font-size: 16px;
font-weight: bold;
color: #333;
margin: 0;
}
}
}
.button-section {
padding: 15px;
background: #fff;
border-radius: 4px;
text-align: left;
.modified-tip {
margin-left: 15px;
color: #909399;
font-size: 14px;
}
}
```
---
### 2.5 前端API层设计
`ruoyi-ui/src/api/ccdi/modelParam.js` 中添加:
```javascript
import request from '@/utils/request'
/**
* 查询所有模型及其参数(按模型分组)
*/
export function listAllParams(query) {
return request({
url: '/ccdi/modelParam/listAll',
method: 'get',
params: query
})
}
/**
* 批量保存所有模型的参数修改
*/
export function saveAllParams(data) {
return request({
url: '/ccdi/modelParam/saveAll',
method: 'post',
data: data
})
}
// 保留原有的其他API方法...
```
---
## 三、数据库设计
**无需修改数据库表结构**,现有的 `ccdi_model_param` 表结构已满足需求。
**现有表结构说明:**
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | BIGINT | 主键ID |
| project_id | BIGINT | 项目ID0表示默认参数 |
| model_code | VARCHAR | 模型编码 |
| model_name | VARCHAR | 模型名称 |
| param_code | VARCHAR | 参数编码 |
| param_name | VARCHAR | 监测项名称 |
| param_desc | VARCHAR | 参数描述 |
| param_value | VARCHAR | 参数值 |
| param_unit | VARCHAR | 参数单位 |
| sort_order | INT | 排序号 |
| create_by | VARCHAR | 创建者 |
| create_time | DATETIME | 创建时间 |
| update_by | VARCHAR | 更新者 |
| update_time | DATETIME | 更新时间 |
| remark | VARCHAR | 备注 |
**索引说明:**
- 主键:`id`
- 常用查询索引:`idx_project_model` (`project_id`, `model_code`)
---
## 四、实现步骤
### 4.1 后端开发任务
#### 第一阶段DTO/VO类创建
- [ ] 创建 `ModelParamAllQueryDTO.java` - 批量查询请求DTO
- [ ] 创建 `ModelParamAllVO.java` - 批量查询响应VO
- [ ] 创建 `ModelGroupVO.java` - 模型分组VO
- [ ] 创建 `ModelParamSaveAllDTO.java` - 批量保存请求DTO
- [ ] 创建 `ModelParamGroupDTO.java` - 模型参数分组DTO
#### 第二阶段Mapper层修改
- [ ]`CcdiModelParamMapper.java` 中添加 `selectByProjectId` 方法
- [ ]`CcdiModelParamMapper.xml` 中添加对应的SQL查询
#### 第三阶段Service层修改
- [ ]`ICcdiModelParamService.java` 接口中添加 `selectAllParams` 方法
- [ ]`ICcdiModelParamService.java` 接口中添加 `saveAllParams` 方法
- [ ]`CcdiModelParamServiceImpl.java` 中实现 `selectAllParams` 方法
- [ ]`CcdiModelParamServiceImpl.java` 中实现 `saveAllParams` 方法
#### 第四阶段Controller层修改
- [ ]`CcdiModelParamController.java` 中添加 `listAll` 接口GET
- [ ]`CcdiModelParamController.java` 中添加 `saveAll` 接口POST
#### 第五阶段:后端测试
- [ ] 使用 Swagger 测试 `listAll` 接口
- 测试全局配置查询projectId=0
- 测试项目配置查询projectId>0
- 测试使用默认配置的项目configType=default
- [ ] 使用 Swagger 测试 `saveAll` 接口
- 测试全局配置保存
- 测试项目首次保存(验证参数复制逻辑)
- 测试项目二次保存
- 测试多模型同时保存
- [ ] 验证错误处理
- 参数验证失败
- 项目不存在
- 数据库异常
---
### 4.2 前端开发任务
#### 第一阶段API层修改
- [ ]`ruoyi-ui/src/api/ccdi/modelParam.js` 中添加 `listAllParams` 方法
- [ ]`ruoyi-ui/src/api/ccdi/modelParam.js` 中添加 `saveAllParams` 方法
#### 第二阶段:全局配置页面重构
- [ ] 重构 `ruoyi-ui/src/views/ccdi/modelParam/index.vue`
- 去掉模型下拉框
- 添加页面标题
- 实现垂直堆叠布局展示所有模型
- 实现参数修改跟踪
- 实现统一保存按钮
- 添加修改提示(显示已修改参数数量)
- 优化样式
#### 第三阶段:项目配置页面重构
- [ ] 重构 `ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue`
- 采用与全局配置页面相同的布局和逻辑
- 适配 projectId 传递
- 适配项目信息显示
#### 第四阶段:前端测试
- [ ] 测试全局配置页面
- 页面加载是否正确显示所有模型
- 参数修改和标记是否正常
- 统一保存功能是否正常
- 修改提示是否准确
- [ ] 测试项目配置页面
- 页面加载是否正确显示所有模型
- 参数修改和保存功能是否正常
- 使用默认配置的项目是否正确显示系统参数
- 首次保存是否成功
- [ ] 测试用户体验
- 页面加载速度
- 操作流畅性
- 错误提示友好性
---
## 五、兼容性与迁移说明
### 5.1 向后兼容
**保留原有接口:**
- 原有的 `GET /list` 接口保留,不影响其他可能的调用方
- 原有的 `POST /save` 接口保留,继续可用
**数据库无变更:**
- 数据库表结构无修改
- 现有数据无需迁移
### 5.2 废弃说明
**功能废弃:**
- 前端的模型下拉框切换方式不再使用
- 但后端接口仍保留,以确保向后兼容
**建议:**
- 逐步迁移所有调用方到新接口
- 未来版本可以废弃旧接口
---
## 六、性能考虑
### 6.1 查询性能
**优化措施:**
- 使用 `selectByProjectId` 一次性查询所有参数,减少数据库往返
- 在内存中按模型分组,避免多次查询
- 利用现有的 `idx_project_model` 索引
**预期性能:**
- 当前模型数量3个
- 预计参数总数约30个
- 单次查询时间:<50ms
- 完全满足性能要求
### 6.2 保存性能
**优化措施:**
- 只保存修改过的参数,减少数据库更新操作
- 使用事务保证数据一致性
- 批量更新,避免多次提交
**预期性能:**
- 典型修改场景1-5个参数
- 保存时间:<100ms
- 完全满足性能要求
### 6.3 前端性能
**优化措施:**
- 使用 `v-for` 高效渲染列表
- 使用计算属性缓存已修改参数数量
- 避免不必要的重渲染
**预期性能:**
- 页面渲染时间:<200ms
- 操作响应时间:<50ms
- 完全满足用户体验要求
---
## 七、安全考虑
### 7.1 权限控制
**现有权限机制:**
- 使用 Spring Security + JWT 进行认证
- 基于角色的访问控制RBAC
- 新接口继承现有权限控制机制
**权限标识:**
- 查询:`ccdi:modelParam:list`
- 保存:`ccdi:modelParam:edit`
### 7.2 数据验证
**后端验证:**
- 使用 `@Validated` 注解进行参数验证
- 验证项目ID、模型编码、参数编码的合法性
- 验证参数值的格式和范围
**前端验证:**
- 参数值非空验证
- 参数值格式验证
### 7.3 数据一致性
**事务管理:**
- 使用 `@Transactional` 保证批量保存的原子性
- 保存失败时自动回滚
**并发控制:**
- 使用乐观锁或悲观锁(根据实际并发情况决定)
- 当前场景并发量低,无需特殊处理
---
## 八、测试策略
### 8.1 单元测试
**Service层测试**
- 测试 `selectAllParams` 方法
- 测试全局配置查询
- 测试项目配置查询
- 测试使用默认配置的项目
- 测试空数据情况
- 测试 `saveAllParams` 方法
- 测试参数验证
- 测试首次保存(参数复制)
- 测试二次保存
- 测试事务回滚
### 8.2 集成测试
**API接口测试**
- 使用 Swagger UI 进行接口测试
- 测试各种参数组合
- 测试错误场景
### 8.3 前端测试
**功能测试:**
- 测试页面加载和渲染
- 测试参数修改和标记
- 测试保存功能
- 测试错误处理
**用户体验测试:**
- 测试页面响应速度
- 测试操作流畅性
- 测试错误提示友好性
---
## 九、风险评估
### 9.1 技术风险
| 风险 | 概率 | 影响 | 应对措施 |
|------|------|------|----------|
| 后端接口设计不合理 | 低 | 中 | 充分设计评审,参考现有接口 |
| 前端组件复杂度高 | 低 | 低 | 采用简单清晰的组件结构 |
| 数据库查询性能差 | 极低 | 中 | 已有索引支持,数据量小 |
| 批量保存失败 | 低 | 高 | 使用事务保证原子性 |
### 9.2 业务风险
| 风险 | 概率 | 影响 | 应对措施 |
|------|------|------|----------|
| 用户不习惯新界面 | 中 | 低 | 提供用户培训,界面简洁直观 |
| 误操作导致参数错误 | 低 | 高 | 添加确认提示,记录操作日志 |
| 保存时数据丢失 | 极低 | 高 | 使用事务,添加错误处理 |
### 9.3 兼容性风险
| 风险 | 概率 | 影响 | 应对措施 |
|------|------|------|----------|
| 旧接口调用方受影响 | 低 | 低 | 保留旧接口,逐步迁移 |
| 数据库不兼容 | 极低 | 高 | 无数据库结构变更 |
---
## 十、上线计划
### 10.1 上线前准备
- [ ] 完成所有开发任务
- [ ] 完成所有测试任务
- [ ] 准备上线文档
- [ ] 准备回滚方案
### 10.2 上线步骤
1. **后端部署**
- 停止应用服务
- 部署新版本代码
- 启动应用服务
- 验证接口可用性
2. **前端部署**
- 构建前端代码
- 部署到服务器
- 清理浏览器缓存
- 验证页面可用性
3. **功能验证**
- 测试全局配置页面
- 测试项目配置页面
- 验证保存功能
- 验证数据一致性
### 10.3 上线后监控
- [ ] 监控接口响应时间
- [ ] 监控错误日志
- [ ] 收集用户反馈
- [ ] 准备问题修复
### 10.4 回滚方案
**如果出现严重问题:**
1. 前端回滚到旧版本
2. 后端回滚到旧版本(接口保留不影响)
3. 数据无需回滚(无数据库变更)
---
## 十一、总结
本次设计采用了优化接口的方案,通过新增批量查询和批量保存接口,实现了在同一页面中展示和编辑所有模型参数的需求。设计充分考虑了性能、安全性、兼容性和可维护性,是一个可行且高效的解决方案。
**设计亮点:**
- ✅ 接口设计合理,易于理解和扩展
- ✅ 前后端分离,逻辑清晰
- ✅ 保留向后兼容,降低风险
- ✅ 性能优化,用户体验好
- ✅ 代码复用性高,可维护性好
**预期收益:**
- 🎯 提升用户体验,减少操作步骤
- 🎯 提高工作效率,一次查看所有模型
- 🎯 降低误操作风险,统一保存机制
- 🎯 代码结构更清晰,便于后续维护
---
## 附录
### A. 相关文档
- [若依框架官方文档](http://doc.ruoyi.vip/)
- [Element UI 组件库](https://element.eleme.cn/)
- [MyBatis Plus 官方文档](https://baomidou.com/)
### B. 变更记录
| 版本 | 日期 | 修改人 | 修改内容 |
|------|------|--------|----------|
| v1.0 | 2026-03-06 | Claude Code | 初始版本 |
---
**文档结束**

View File

@@ -1,854 +0,0 @@
# 项目详情参数配置页面设计文档
**创建时间:** 2026-03-06
**作者:** Claude Code
**状态:** 已批准
---
## 1. 概述
### 1.1 需求背景
纪检初核系统需要在项目详情页面中添加参数配置功能,允许用户为每个项目自定义模型参数配置。当前系统已有独立的模型参数配置页面(管理系统默认参数),需要将其功能复用到项目详情页面中。
### 1.2 核心需求
1. **配置模式:** 自动切换模式(修改即切换为 custom
2. **界面布局:** 完全复用独立页面的布局(模型下拉框 + 参数表格 + 保存按钮)
3. **重置功能:** 不提供切换回默认配置的功能
4. **初始化策略:** 查询时复制(按需创建自定义参数)
### 1.3 设计原则
1. **最小改动原则:** 前端组件直接复用代码,后端只修改必要的方法
2. **自动切换原则:** 用户保存参数时自动从 default 切换到 custom
3. **按需创建原则:** 只在首次保存时创建项目自定义参数,不预复制
4. **数据隔离原则:** 项目自定义参数与系统默认参数完全独立
---
## 2. 架构设计
### 2.1 整体架构
```
┌─────────────────────────────────────────────────────────┐
│ 项目详情页面 │
│ detail.vue │
│ ┌───────────────────────────────────────────────────┐ │
│ │ 菜单栏: 上传数据 | 参数配置 | 结果总览 | ... │ │
│ └───────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ ParamConfig 组件 │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ 模型选择下拉框 │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ 参数表格(可编辑) │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ 保存按钮 │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│ API 调用
┌─────────────────────────────────────────────────────────┐
│ 后端 CcdiModelParamController │
│ ┌───────────────────────────────────────────────────┐ │
│ │ GET /ccdi/modelParam/modelList?projectId={id} │ │
│ │ - 查询模型列表 │ │
│ └───────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ GET /ccdi/modelParam/list?projectId={id} │ │
│ │ - 查询模型参数列表 │ │
│ │ - 如果 configType=default返回系统默认参数 │ │
│ │ - 如果 configType=custom返回项目自定义参数 │ │
│ └───────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ POST /ccdi/modelParam/save │ │
│ │ - 保存参数 │ │
│ │ - 如果是首次保存,自动复制系统默认参数 │ │
│ │ - 更新 configType=custom │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 数据库 ccdi_model_param │
│ - projectId=0系统默认参数 │
│ - projectId>0项目自定义参数 │
└─────────────────────────────────────────────────────────┘
```
### 2.2 数据模型
**表ccdi_model_param**
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | BIGINT | 主键ID |
| project_id | BIGINT | 项目ID0表示默认参数 |
| model_code | VARCHAR(50) | 模型编码 |
| model_name | VARCHAR(100) | 模型名称 |
| param_code | VARCHAR(50) | 参数编码 |
| param_name | VARCHAR(100) | 监测项名称 |
| param_desc | VARCHAR(500) | 参数描述 |
| param_value | VARCHAR(200) | 参数值 |
| param_unit | VARCHAR(50) | 参数单位 |
| sort_order | INT | 排序号 |
| create_by | VARCHAR(64) | 创建者 |
| create_time | DATETIME | 创建时间 |
| update_by | VARCHAR(64) | 更新者 |
| update_time | DATETIME | 更新时间 |
| remark | VARCHAR(500) | 备注 |
**表ccdi_project相关字段**
| 字段名 | 类型 | 说明 |
|--------|------|------|
| project_id | BIGINT | 项目ID |
| config_type | VARCHAR(20) | 配置方式default-全局默认custom-自定义 |
---
## 3. 组件设计
### 3.1 前端组件
**组件路径:** `ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue`
**组件结构:**
```vue
<template>
<div class="param-config-container">
<!-- 模型选择区域 -->
<div class="filter-section">
<el-form :inline="true" :model="queryParams">
<el-form-item label="模型名称">
<el-select
v-model="queryParams.modelCode"
placeholder="请选择模型"
@change="handleModelChange"
>
<el-option
v-for="model in modelList"
:key="model.modelCode"
:label="model.modelName"
:value="model.modelCode"
/>
</el-select>
</el-form-item>
</el-form>
</div>
<!-- 参数配置表格 -->
<div class="table-section">
<h3>阈值参数配置</h3>
<el-table :data="paramList" border>
<el-table-column label="监测项" prop="paramName" width="200" />
<el-table-column label="描述" prop="paramDesc" />
<el-table-column label="阈值设置" width="200">
<template #default="{ row }">
<el-input
v-model="row.paramValue"
placeholder="请输入阈值"
@input="markAsModified(row)"
/>
</template>
</el-table-column>
<el-table-column label="单位" prop="paramUnit" width="120" />
</el-table>
</div>
<!-- 操作按钮 -->
<div class="button-section">
<el-button
type="primary"
@click="handleSave"
:loading="saving"
>
保存配置
</el-button>
</div>
</div>
</template>
<script>
import { listModels, listParams, saveParams } from "@/api/ccdi/modelParam";
export default {
name: 'ParamConfig',
props: {
projectId: {
type: [String, Number],
required: true
},
projectInfo: {
type: Object,
default: () => ({})
}
},
data() {
return {
modelList: [],
queryParams: {
modelCode: undefined,
projectId: this.projectId
},
paramList: [],
saving: false
}
},
watch: {
projectId(newVal) {
this.queryParams.projectId = newVal
this.loadModelList()
}
},
created() {
this.loadModelList()
},
methods: {
/** 加载模型列表 */
async loadModelList() {
try {
const res = await listModels({ projectId: this.projectId })
this.modelList = res.data
if (this.modelList.length > 0) {
this.queryParams.modelCode = this.modelList[0].modelCode
this.loadParamList()
}
} catch (error) {
this.$message.error('加载模型列表失败:' + error.message)
console.error('加载模型列表失败', error)
}
},
/** 加载参数列表 */
async loadParamList() {
try {
const res = await listParams(this.queryParams)
this.paramList = res.data
} catch (error) {
this.$message.error('加载参数列表失败:' + error.message)
console.error('加载参数列表失败', error)
}
},
/** 模型切换 */
handleModelChange() {
this.loadParamList()
},
/** 标记为已修改 */
markAsModified(row) {
row.modified = true
},
/** 保存配置 */
async handleSave() {
// 验证是否有修改
const modifiedParams = this.paramList.filter(item => item.modified)
if (modifiedParams.length === 0) {
this.$message.info('没有需要保存的修改')
return
}
// 验证参数值
const invalidParams = modifiedParams.filter(
item => !item.paramValue || item.paramValue.trim() === ''
)
if (invalidParams.length > 0) {
this.$message.error('请填写所有参数值')
return
}
// 构造保存数据
const saveDTO = {
projectId: this.projectId,
modelCode: this.queryParams.modelCode,
params: modifiedParams.map(item => ({
paramCode: item.paramCode,
paramValue: item.paramValue
}))
}
// 保存
this.saving = true
try {
await saveParams(saveDTO)
this.$message.success('保存成功')
// 清除修改标记并重新加载
this.paramList.forEach(item => { item.modified = false })
await this.loadParamList()
} catch (error) {
if (error.response && error.response.data && error.response.data.msg) {
this.$message.error('保存失败:' + error.response.data.msg)
} else {
this.$message.error('保存失败:' + error.message)
}
} finally {
this.saving = false
}
}
}
}
</script>
<style scoped lang="scss">
.param-config-container {
padding: 20px;
background-color: #fff;
min-height: 400px;
}
.filter-section {
padding: 15px;
background: #fff;
border-radius: 4px;
margin-bottom: 20px;
}
.table-section {
padding: 20px;
background: #fff;
border-radius: 4px;
margin-bottom: 20px;
h3 {
font-size: 16px;
font-weight: bold;
color: #333;
margin: 0 0 15px 0;
}
}
.button-section {
padding: 15px;
background: #fff;
border-radius: 4px;
text-align: left;
}
</style>
```
### 3.2 后端接口
**文件:** `CcdiModelParamServiceImpl.java`
**修改的方法:**
#### 3.2.1 selectParamList 方法
```java
@Override
public List<ModelParamVO> selectParamList(ModelParamQueryDTO queryDTO) {
// 1. 查询项目信息
CcdiProject project = projectMapper.selectById(queryDTO.getProjectId());
if (project == null) {
throw new ServiceException("项目不存在");
}
// 2. 根据 configType 决定查询哪组参数
Long effectiveProjectId;
if ("default".equals(project.getConfigType())) {
// 使用系统默认参数
effectiveProjectId = 0L;
} else {
// 使用项目自定义参数
effectiveProjectId = queryDTO.getProjectId();
}
// 3. 查询参数列表
return modelParamMapper.selectParamList(effectiveProjectId, queryDTO.getModelCode());
}
```
#### 3.2.2 saveParams 方法
```java
@Override
@Transactional(rollbackFor = Exception.class)
public void saveParams(ModelParamSaveDTO saveDTO) {
try {
// 1. 参数验证
if (saveDTO.getProjectId() == null) {
throw new ServiceException("项目ID不能为空");
}
if (StringUtils.isBlank(saveDTO.getModelCode())) {
throw new ServiceException("模型编码不能为空");
}
if (saveDTO.getParams() == null || saveDTO.getParams().isEmpty()) {
throw new ServiceException("参数列表不能为空");
}
// 2. 查询项目信息
CcdiProject project = projectMapper.selectById(saveDTO.getProjectId());
if (project == null) {
throw new ServiceException("项目不存在");
}
// 3. 如果是首次保存configType=default需要复制系统默认参数
if ("default".equals(project.getConfigType())) {
int copiedCount = copyDefaultParamsToProject(
saveDTO.getProjectId(),
saveDTO.getModelCode()
);
if (copiedCount == 0) {
log.warn("系统默认参数为空projectId={}, modelCode={}",
saveDTO.getProjectId(), saveDTO.getModelCode());
}
// 更新项目配置类型为 custom
project.setConfigType("custom");
projectMapper.updateById(project);
}
// 4. 更新参数值
for (ModelParamSaveDTO.ParamValueItem item : saveDTO.getParams()) {
int updated = modelParamMapper.updateParamValue(
saveDTO.getProjectId(),
saveDTO.getModelCode(),
item.getParamCode(),
item.getParamValue()
);
if (updated == 0) {
log.warn("参数不存在或未更新paramCode={}", item.getParamCode());
}
}
} catch (ServiceException e) {
// 业务异常,直接抛出
throw e;
} catch (Exception e) {
// 系统异常,记录日志并抛出
log.error("保存模型参数失败", e);
throw new ServiceException("保存模型参数失败:" + e.getMessage());
}
}
/**
* 复制系统默认参数到项目
*
* @param projectId 项目ID
* @param modelCode 模型编码
* @return 复制的参数数量
*/
private int copyDefaultParamsToProject(Long projectId, String modelCode) {
// 查询系统默认参数
List<CcdiModelParam> defaultParams = modelParamMapper.selectList(
new LambdaQueryWrapper<CcdiModelParam>()
.eq(CcdiModelParam::getProjectId, 0L)
.eq(CcdiModelParam::getModelCode, modelCode)
);
if (defaultParams.isEmpty()) {
return 0;
}
// 复制到项目
List<CcdiModelParam> projectParams = defaultParams.stream()
.map(param -> {
CcdiModelParam newParam = new CcdiModelParam();
BeanUtils.copyProperties(param, newParam);
newParam.setId(null); // 清空ID让数据库自动生成
newParam.setProjectId(projectId);
newParam.setCreateBy(null); // 清空审计字段,让 MyBatis Plus 自动填充
newParam.setCreateTime(null);
newParam.setUpdateBy(null);
newParam.setUpdateTime(null);
return newParam;
})
.collect(Collectors.toList());
// 批量插入
modelParamMapper.insertBatch(projectParams);
return projectParams.size();
}
```
#### 3.2.3 Mapper 方法
**CcdiModelParamMapper.xml 新增:**
```xml
<!-- 更新参数值 -->
<update id="updateParamValue">
UPDATE ccdi_model_param
SET param_value = #{paramValue},
update_by = NULL,
update_time = NOW()
WHERE project_id = #{projectId}
AND model_code = #{modelCode}
AND param_code = #{paramCode}
</update>
<!-- 批量插入 -->
<insert id="insertBatch" parameterType="java.util.List">
INSERT INTO ccdi_model_param (
project_id, model_code, model_name, param_code, param_name,
param_desc, param_value, param_unit, sort_order,
create_by, create_time, remark
) VALUES
<foreach collection="list" item="item" separator=",">
(
#{item.projectId}, #{item.modelCode}, #{item.modelName},
#{item.paramCode}, #{item.paramName}, #{item.paramDesc},
#{item.paramValue}, #{item.paramUnit}, #{item.sortOrder},
NULL, NOW(), #{item.remark}
)
</foreach>
</insert>
```
---
## 4. 数据流设计
### 4.1 查看参数配置configType=default
```
用户点击"参数配置"菜单
前端调用 GET /ccdi/modelParam/modelList?projectId=123
后端返回模型列表
前端选择第一个模型,调用 GET /ccdi/modelParam/list?projectId=123&modelCode=MODEL_001
后端查询项目,发现 configType=default
后端返回系统默认参数projectId=0
前端显示参数表格
```
### 4.2 查看参数配置configType=custom
```
用户点击"参数配置"菜单
前端调用 GET /ccdi/modelParam/modelList?projectId=123
后端返回模型列表
前端选择第一个模型,调用 GET /ccdi/modelParam/list?projectId=123&modelCode=MODEL_001
后端查询项目,发现 configType=custom
后端返回项目自定义参数projectId=123
前端显示参数表格
```
### 4.3 首次保存参数default → custom
```
用户修改参数值,点击"保存配置"
前端调用 POST /ccdi/modelParam/save
{
"projectId": 123,
"modelCode": "MODEL_001",
"params": [
{"paramCode": "THRESHOLD_1", "paramValue": "100"},
{"paramCode": "THRESHOLD_2", "paramValue": "50"}
]
}
后端检查项目 configType=default
后端执行复制操作:
1. 查询系统默认参数projectId=0, modelCode=MODEL_001
2. 复制所有参数,设置 projectId=123
3. 批量插入到数据库
后端更新项目的 configType=custom
后端更新参数值:
UPDATE ccdi_model_param
SET param_value='100'
WHERE project_id=123 AND model_code='MODEL_001' AND param_code='THRESHOLD_1'
后端返回成功
前端重新加载参数列表(此时查询的是项目自定义参数)
前端显示成功消息
```
### 4.4 再次保存参数configType=custom
```
用户修改参数值,点击"保存配置"
前端调用 POST /ccdi/modelParam/save
后端检查项目 configType=custom
后端跳过复制步骤,直接更新参数值
后端返回成功
```
---
## 5. 错误处理
### 5.1 前端错误处理
**网络错误:**
```javascript
async loadParamList() {
try {
const res = await listParams(this.queryParams)
this.paramList = res.data
} catch (error) {
this.$message.error('加载参数列表失败:' + error.message)
console.error('加载参数列表失败', error)
}
}
```
**保存验证:**
```javascript
async handleSave() {
// 验证是否有修改
const modifiedParams = this.paramList.filter(item => item.modified)
if (modifiedParams.length === 0) {
this.$message.info('没有需要保存的修改')
return
}
// 验证参数值
const invalidParams = modifiedParams.filter(
item => !item.paramValue || item.paramValue.trim() === ''
)
if (invalidParams.length > 0) {
this.$message.error('请填写所有参数值')
return
}
// 保存
this.saving = true
try {
await saveParams(saveDTO)
this.$message.success('保存成功')
// 清除修改标记并重新加载
this.paramList.forEach(item => { item.modified = false })
await this.loadParamList()
} catch (error) {
if (error.response && error.response.data && error.response.data.msg) {
this.$message.error('保存失败:' + error.response.data.msg)
} else {
this.$message.error('保存失败:' + error.message)
}
} finally {
this.saving = false
}
}
```
### 5.2 后端错误处理
**异常处理:**
```java
@Override
@Transactional(rollbackFor = Exception.class)
public void saveParams(ModelParamSaveDTO saveDTO) {
try {
// 1. 参数验证
if (saveDTO.getProjectId() == null) {
throw new ServiceException("项目ID不能为空");
}
if (StringUtils.isBlank(saveDTO.getModelCode())) {
throw new ServiceException("模型编码不能为空");
}
if (saveDTO.getParams() == null || saveDTO.getParams().isEmpty()) {
throw new ServiceException("参数列表不能为空");
}
// 2. 查询项目信息
CcdiProject project = projectMapper.selectById(saveDTO.getProjectId());
if (project == null) {
throw new ServiceException("项目不存在");
}
// 3. 复制默认参数(如果需要)
if ("default".equals(project.getConfigType())) {
int copiedCount = copyDefaultParamsToProject(
saveDTO.getProjectId(),
saveDTO.getModelCode()
);
if (copiedCount == 0) {
log.warn("系统默认参数为空projectId={}, modelCode={}",
saveDTO.getProjectId(), saveDTO.getModelCode());
}
// 更新项目配置类型
project.setConfigType("custom");
projectMapper.updateById(project);
}
// 4. 更新参数值
for (ModelParamSaveDTO.ParamValueItem item : saveDTO.getParams()) {
int updated = modelParamMapper.updateParamValue(
saveDTO.getProjectId(),
saveDTO.getModelCode(),
item.getParamCode(),
item.getParamValue()
);
if (updated == 0) {
log.warn("参数不存在或未更新paramCode={}", item.getParamCode());
}
}
} catch (ServiceException e) {
// 业务异常,直接抛出
throw e;
} catch (Exception e) {
// 系统异常,记录日志并抛出
log.error("保存模型参数失败", e);
throw new ServiceException("保存模型参数失败:" + e.getMessage());
}
}
```
### 5.3 错误场景处理表
| 错误场景 | 处理方式 |
|---------|---------|
| 项目不存在 | 返回 404 错误,提示"项目不存在" |
| 系统默认参数为空 | 记录警告日志,继续执行(允许项目自定义参数) |
| 参数值验证失败 | 前端拦截,不提交到后端 |
| 数据库连接失败 | 返回 500 错误,提示"系统异常,请稍后重试" |
| 事务回滚 | 自动回滚所有操作,保证数据一致性 |
---
## 6. 测试策略
### 6.1 后端单元测试
**测试类:** `CcdiModelParamServiceImplTest.java`
**测试用例:**
1. `testSelectParamList_DefaultConfig()` - 测试查询默认配置项目的参数列表
2. `testSelectParamList_CustomConfig()` - 测试查询自定义配置项目的参数列表
3. `testSaveParams_FirstTimeSave()` - 测试首次保存参数(触发 default → custom 切换)
4. `testSaveParams_SecondTimeSave()` - 测试再次保存参数(已为 custom 模式)
### 6.2 前端集成测试
**测试脚本:** `test-param-config.sh`
**测试流程:**
1. 登录获取 Token
2. 创建测试项目
3. 查询模型列表
4. 查询参数列表default 模式)
5. 首次保存参数(触发切换)
6. 查询参数列表custom 模式)
7. 查询项目信息(验证 configType
8. 清理测试数据
### 6.3 手动测试清单
| 编号 | 测试场景 | 预期结果 | 通过标准 |
|------|---------|---------|---------|
| 1 | 新项目查看参数配置 | 显示系统默认参数 | 参数值与系统默认一致 |
| 2 | 新项目修改并保存参数 | 自动切换为自定义配置 | configType 变为 custom |
| 3 | 再次查看参数 | 显示项目自定义参数 | 参数值为修改后的值 |
| 4 | 再次修改参数 | 直接更新参数值 | 参数值更新成功 |
| 5 | 切换模型 | 正确加载不同模型的参数 | 参数列表正确切换 |
| 6 | 不修改任何参数点击保存 | 提示"没有需要保存的修改" | 不发起保存请求 |
| 7 | 清空参数值后保存 | 前端验证拦截 | 显示错误提示 |
| 8 | 并发保存同一参数 | 后保存的值生效 | 数据一致性 |
| 9 | 网络异常时保存 | 显示错误提示 | 不更新页面数据 |
| 10 | 项目状态为"已归档"时保存 | 根据业务规则处理 | 符合业务逻辑 |
### 6.4 性能测试
| 测试项 | 测试方法 | 性能目标 |
|--------|---------|---------|
| 查询参数列表 | 模拟 100 个项目同时查询 | 响应时间 < 500ms |
| 首次保存参数 | 模拟 50 个项目同时首次保存 | 响应时间 < 2s |
| 数据库查询性能 | EXPLAIN 分析 SQL | 使用索引,无全表扫描 |
| 并发保存 | 10 个并发请求保存同一项目 | 无死锁,数据一致 |
---
## 7. 实施计划
### 7.1 实施步骤
1. **后端开发**
- 修改 `CcdiModelParamServiceImpl.selectParamList()` 方法
- 修改 `CcdiModelParamServiceImpl.saveParams()` 方法
- 新增 `copyDefaultParamsToProject()` 私有方法
- 新增 Mapper XML 中的 `updateParamValue``insertBatch` 方法
2. **前端开发**
- 实现 `ParamConfig.vue` 组件
- 复用 `ccdi/modelParam.js` API 接口
- 确保组件正确接收 `projectId``projectInfo` props
3. **测试**
- 编写后端单元测试
- 编写集成测试脚本
- 执行手动测试清单
4. **文档**
- 更新 API 文档
- 更新用户手册
### 7.2 风险评估
| 风险项 | 影响 | 概率 | 应对措施 |
|--------|------|------|---------|
| 并发保存导致数据不一致 | 高 | 低 | 使用事务隔离,数据库行锁 |
| 系统默认参数缺失 | 中 | 低 | 记录日志,允许项目自定义 |
| 前端缓存导致参数不更新 | 低 | 中 | 保存后重新加载参数列表 |
| 大批量参数复制性能问题 | 中 | 低 | 使用批量插入,控制事务大小 |
---
## 8. 附录
### 8.1 相关文件清单
**前端文件:**
- `ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue` - 参数配置组件
- `ruoyi-ui/src/api/ccdi/modelParam.js` - API 接口(已存在,无需修改)
**后端文件:**
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java` - Service 实现
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiModelParamMapper.java` - Mapper 接口
- `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiModelParamMapper.xml` - Mapper XML
**测试文件:**
- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiModelParamServiceImplTest.java` - 单元测试
- `docs/test-scripts/test-param-config.sh` - 集成测试脚本
### 8.2 参考文档
- 若依框架官方文档
- MyBatis Plus 官方文档
- Element UI 官方文档
- 项目 CLAUDE.md 开发规范
---
**设计完成时间:** 2026-03-06
**下一步:** 创建详细实施计划

View File

@@ -1,259 +0,0 @@
# 默认主题修改为浅色模式 - 设计文档
**日期:** 2026-03-06
**状态:** 已批准
**作者:** Claude Code
## 1. 概述
### 1.1 背景
当前系统默认使用深色模式侧边栏(`theme-dark`),需要将默认主题修改为浅色模式(`theme-light`)。
### 1.2 目标
- 将新用户的默认主题从深色模式改为浅色模式
- 保持老用户的自定义设置不受影响
- 确保主题切换功能完全正常
### 1.3 范围
- 仅修改前端默认配置
- 不涉及后端修改
- 不涉及数据库修改
## 2. 当前架构
### 2.1 主题配置层级
```
settings.js (默认配置)
store/modules/settings.js (Vuex 状态管理)
layout/components/Settings/index.vue (用户界面设置)
localStorage (持久化用户设置)
```
### 2.2 主题初始化逻辑
**文件:** `ruoyi-ui/src/store/modules/settings.js`
```javascript
const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || ''
const state = {
sideTheme: storageSetting.sideTheme || sideTheme, // localStorage 优先
// ...
}
```
**逻辑:**
1.`settings.js` 读取默认值
2. 检查 `localStorage` 是否有用户设置
3. 如果有用户设置,使用用户设置覆盖默认值
4. 如果没有用户设置,使用默认值
## 3. 设计方案
### 3.1 修改内容
**文件:** `ruoyi-ui/src/settings.js`
**变更:** 第 9 行
```javascript
// 修改前
sideTheme: 'theme-dark',
// 修改后
sideTheme: 'theme-light',
```
### 3.2 数据流
#### 新用户首次访问
```
用户访问系统
store/modules/settings.js 初始化
读取 settings.js: sideTheme = 'theme-light'
检查 localStorage: 为空
使用默认值: 'theme-light'
渲染浅色模式侧边栏
```
#### 老用户访问(已保存设置)
```
用户访问系统
store/modules/settings.js 初始化
读取 settings.js: sideTheme = 'theme-light'
检查 localStorage: 有值 { sideTheme: 'theme-dark' }
使用 localStorage 中的值: 'theme-dark'
渲染深色模式侧边栏(保持用户设置)
```
### 3.3 兼容性
**向后兼容:**
- ✅ 老用户的 localStorage 设置不受影响
- ✅ 老用户看到的主题与之前一致
**向前兼容:**
- ✅ 新用户默认看到浅色模式
- ✅ 用户仍可自由切换主题
- ✅ 保存/重置功能完全正常
## 4. 影响分析
### 4.1 影响范围
**文件变更:**
- `ruoyi-ui/src/settings.js`1 行代码)
**功能影响:**
- ✅ 无功能变更
- ✅ 无接口变更
- ✅ 无数据结构变更
### 4.2 用户体验影响
**新用户:**
- 从深色模式默认值 → 浅色模式默认值
**老用户:**
- 无影响localStorage 中的设置优先)
## 5. 测试计划
### 5.1 测试用例
| 测试场景 | 操作步骤 | 预期结果 |
|---------|---------|---------|
| 新用户首次访问 | 1. 清除 localStorage<br>2. 刷新页面 | 侧边栏为浅色模式 |
| 老用户(深色模式) | 1. localStorage 保存深色模式<br>2. 刷新页面 | 侧边栏仍为深色模式 |
| 老用户(浅色模式) | 1. localStorage 保存浅色模式<br>2. 刷新页面 | 侧边栏仍为浅色模式 |
| 切换主题 | 1. 打开设置抽屉<br>2. 点击深色/浅色图标 | 侧边栏立即切换 |
| 保存设置 | 1. 切换主题<br>2. 点击"保存配置"<br>3. 刷新页面 | 设置保持不变 |
| 重置设置 | 1. 修改多个设置<br>2. 点击"重置配置" | 恢复为默认值(浅色模式) |
### 5.2 浏览器兼容性
测试浏览器:
- ✅ Chrome (最新版)
- ✅ Firefox (最新版)
- ✅ Edge (最新版)
- ✅ Safari (最新版)
## 6. 部署方案
### 6.1 部署步骤
1. **修改代码**
```bash
# 修改 ruoyi-ui/src/settings.js
```
2. **提交代码**
```bash
git add ruoyi-ui/src/settings.js
git commit -m "feat: 将默认主题修改为浅色模式"
```
3. **构建前端**
```bash
cd ruoyi-ui
npm run build:prod
```
4. **部署静态资源**
- 将 `ruoyi-ui/dist/` 目录部署到生产服务器
5. **验证部署**
- 清除浏览器缓存
- 访问系统
- 验证新用户看到浅色模式
### 6.2 回滚方案
如果发现问题,可快速回滚:
```javascript
// settings.js 第 9 行
sideTheme: 'theme-dark', // 改回深色模式
```
然后重新构建和部署。
## 7. 风险评估
### 7.1 风险列表
| 风险 | 概率 | 影响 | 缓解措施 |
|-----|------|------|---------|
| 老用户困惑 | 低 | 低 | 老用户设置不受影响 |
| 浅色模式样式问题 | 低 | 中 | 需要充分测试 |
| 部署失败 | 低 | 高 | 准备回滚方案 |
### 7.2 总体风险
**风险等级:** 低
**理由:**
- 仅修改一行配置代码
- 不影响老用户设置
- 可以快速回滚
## 8. 验收标准
### 8.1 功能验收
- ✅ 新用户首次访问看到浅色模式侧边栏
- ✅ 老用户的自定义主题设置保持不变
- ✅ 主题切换功能正常
- ✅ 主题保存功能正常
- ✅ 主题重置功能正常
### 8.2 质量验收
- ✅ 代码审查通过
- ✅ 测试用例全部通过
- ✅ 无控制台错误
- ✅ 浏览器兼容性测试通过
## 9. 后续优化建议
### 9.1 短期优化
- 可以考虑在设置界面添加"推荐"标签,标注浅色模式
- 可以考虑在首次登录时提示用户可以自定义主题
### 9.2 长期优化
- 可以考虑添加更多预设主题(护眼模式、高对比度模式等)
- 可以考虑将主题设置保存到后端数据库,实现跨设备同步
## 10. 附录
### 10.1 相关文件
- `ruoyi-ui/src/settings.js` - 默认配置文件
- `ruoyi-ui/src/store/modules/settings.js` - Vuex 状态管理
- `ruoyi-ui/src/layout/components/Settings/index.vue` - 设置界面组件
- `ruoyi-ui/src/components/ThemePicker/index.vue` - 主题颜色选择器
### 10.2 参考资料
- [Element UI 主题定制](https://element.eleme.cn/#/zh-CN/theme)
- [Vuex 状态管理](https://vuex.vuejs.org/zh/)

View File

@@ -1,313 +0,0 @@
# 银行流水入库重复校验设计
## 概述
`fetchAndSaveBankStatements(Long projectId, Integer groupId, Integer logId)` 中,
接口返回的银行流水写入 `ccdi_bank_statement` 前,需要基于业务键避免重复插入。
本次确认的重复判定键为:
- `project_id`
- `LE_ACCOUNT_NO`
- `ACCOUNTING_DATE_ID`
- `AMOUNT_DR`
- `AMOUNT_CR`
目标是将“什么叫重复”尽量固化到数据库约束层,服务层只负责轻量标准化和保留现有异步处理链路。
## 背景
当前实现位于 `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
1. 分页调用流水分析接口获取流水数据
2. 将返回项转换为 `CcdiBankStatement`
3. 直接调用 `CcdiBankStatementMapper.insertBatch(...)` 批量入库
现状没有任何重复校验,重复导入同一批流水时会再次插入。
已确认的业务边界:
- 接口返回的同一批流水自身不会重复
- 接口返回的 `LE_ACCOUNT_NO``ACCOUNTING_DATE_ID``AMOUNT_DR``AMOUNT_CR` 不会是 `null`
- 如果数据库里已存在相同业务键的流水,保留原记录,不更新原数据
- 命中重复不应让整次文件处理失败
## 方案对比
### 方案一:服务层先查库再插入
做法:
- 服务层先按业务键查库
- 过滤已存在记录
- 仅插入剩余记录
优点:
- 语义直观
- 不需要调整批量插入 SQL
缺点:
- 规则只在当前入口生效,其他写入入口仍可能写入重复数据
- 并发导入时存在竞态窗口
- 代码和 SQL 都会变复杂
### 方案二:数据库唯一键 + no-op upsert
做法:
- 对业务键加唯一约束
- 批量插入改为 `INSERT ... ON DUPLICATE KEY UPDATE bank_statement_id = bank_statement_id`
- 服务层只做必要的字段标准化
优点:
- 重复规则由数据库统一约束
- 并发下稳定
- 代码改动集中且可控
缺点:
- 需要先处理测试库中的存量异常/重复数据
- `ON DUPLICATE KEY` 的受影响行数语义需要在本地 MySQL 实测确认
### 方案三:`INSERT IGNORE`
做法:
- 数据库加唯一键
- 批量插入改为 `INSERT IGNORE`
优点:
- SQL 最短
- 重复会被自动跳过
缺点:
- 可能连非重复键类的数据问题也一起吞掉
- 不利于保留真实错误
## 最终方案
采用方案二:`数据库唯一键 + 写入前标准化 + no-op upsert`
核心决策:
1. 不做本次接口结果的内存去重
2. 去重定义整体切换为 `project_id + LE_ACCOUNT_NO + ACCOUNTING_DATE_ID + AMOUNT_DR + AMOUNT_CR`
3. 服务层只保留与新去重键相关的轻量标准化
4. 数据库侧增加唯一键统一兜底重复规则
5. 命中重复时跳过写入,不更新原有业务数据
6. 非重复键类数据库错误仍然向上抛出,并按现有流程标记 `parsed_failed`
## 详细设计
### 1. 去重键定义
重复判定使用以下五元组:
```text
project_id + LE_ACCOUNT_NO + ACCOUNTING_DATE_ID + AMOUNT_DR + AMOUNT_CR
```
对应到 Java 字段为:
- `projectId`
- `leAccountNo`
- `accountingDateId`
- `amountDr`
- `amountCr`
旧设计中的 `LE_ACCOUNT_NAME``TRX_DATE``CUSTOMER_ACCOUNT_NAME``AMOUNT_BALANCE`
不再参与重复判定。
### 2. 服务层标准化
`CcdiFileUploadServiceImpl.fetchAndSaveBankStatements(...)` 中,
`CcdiBankStatement.fromResponse(...)` 返回实体后,只保留与新去重键相关的标准化:
- `leAccountNo = leAccountNo.trim()`
- `accountingDateId` 保持接口返回值
- `amountDr``amountCr` 保持 `BigDecimal` 语义写入数据库 `decimal(19,2)`
- `projectId` 继续由服务层显式设置
建议新增私有辅助方法,例如:
```java
private String trimAccountNo(String value) {
return value == null ? null : value.trim();
}
private void normalizeDedupFields(CcdiBankStatement statement) {
statement.setLeAccountNo(trimAccountNo(statement.getLeAccountNo()));
}
```
说明:
- 因为接口已保证 `LE_ACCOUNT_NO``ACCOUNTING_DATE_ID``AMOUNT_DR``AMOUNT_CR` 不为 `null`
服务层不再额外承担空值回填逻辑
- 标准化的目标是避免账号前后空格导致同一条流水被误判为不同记录
### 3. 数据库结构调整
为保证唯一键对所有写入入口都有效,需要先清理测试库中的异常数据,再加唯一键。
测试库迁移步骤:
1. 删除 `project_id``LE_ACCOUNT_NO``ACCOUNTING_DATE_ID` 缺失的测试数据
2.`LE_ACCOUNT_NO` 执行 `TRIM`
3. 按新五元组清理已存在的重复测试数据,只保留一条
4.`project_id``LE_ACCOUNT_NO``ACCOUNTING_DATE_ID` 收紧为 `NOT NULL`
5. 新增唯一键
建议迁移脚本内容包含:
```sql
DELETE FROM ccdi_bank_statement
WHERE project_id IS NULL
OR LE_ACCOUNT_NO IS NULL
OR ACCOUNTING_DATE_ID IS NULL;
UPDATE ccdi_bank_statement
SET LE_ACCOUNT_NO = TRIM(LE_ACCOUNT_NO);
DELETE t1
FROM ccdi_bank_statement t1
JOIN ccdi_bank_statement t2
ON t1.bank_statement_id > t2.bank_statement_id
AND t1.project_id = t2.project_id
AND t1.LE_ACCOUNT_NO = t2.LE_ACCOUNT_NO
AND t1.ACCOUNTING_DATE_ID = t2.ACCOUNTING_DATE_ID
AND t1.AMOUNT_DR = t2.AMOUNT_DR
AND t1.AMOUNT_CR = t2.AMOUNT_CR;
ALTER TABLE ccdi_bank_statement
MODIFY COLUMN project_id bigint(20) NOT NULL COMMENT '关联项目ID',
MODIFY COLUMN LE_ACCOUNT_NO varchar(240) NOT NULL DEFAULT '' COMMENT '企业银行账号',
MODIFY COLUMN ACCOUNTING_DATE_ID int(11) NOT NULL COMMENT '账号日期ID';
ALTER TABLE ccdi_bank_statement
ADD UNIQUE KEY uk_bank_statement_dedup (
project_id,
LE_ACCOUNT_NO,
ACCOUNTING_DATE_ID,
AMOUNT_DR,
AMOUNT_CR
);
```
备注:
- `AMOUNT_DR``AMOUNT_CR` 在现有表设计中已是 `NOT NULL DEFAULT 0.00`
- `project_id` 是当前业务写入必填字段,迁移前应确认测试库不存在空值
- 由于库未上线、测试数据可调整,删除不完整测试数据是可接受方案
### 4. Mapper SQL 调整
`CcdiBankStatementMapper.xml` 中的批量插入改为 no-op upsert
```sql
INSERT INTO ccdi_bank_statement (...)
VALUES
(...),
(...)
ON DUPLICATE KEY UPDATE
bank_statement_id = bank_statement_id
```
语义说明:
- 新记录:正常插入
- 重复记录:命中唯一键,走 duplicate 分支,但不改任何业务字段
- 非重复键类 SQL 错误:仍然抛出异常
这样满足“保留原数据,不进行更新”的业务要求。
### 5. 日志与统计
服务层日志增加三类计数:
- 接口返回数
- 实际新增数
- 重复跳过数
这里有一个实现细节需要在本地 MySQL 上确认:
- `ON DUPLICATE KEY UPDATE bank_statement_id = bank_statement_id`
在 MySQL/JDBC 下的受影响行数,是否稳定返回“新增 1、重复 0”
如果实测成立,则可以直接计算:
```text
重复跳过数 = 尝试写入数 - 实际新增数
```
如果实测不稳定,则降级为保守日志,不伪造精确的重复数。
### 6. 异常处理
命中重复不应视为失败。
处理规则:
- 命中唯一键重复:不抛业务失败,继续处理后续批次
- 真实数据库错误:保持现有异常传播路径
- 外层 `processFileAsync(...)` 捕获真实异常后,仍更新上传记录为 `parsed_failed`
### 7. 文档同步
当前 `assets/对接流水分析/ccdi_bank_statement.md` 中的建表说明与现有实体/Mapper 已有漂移,
例如当前代码已经使用 `project_id`,而该文档片段未体现。
本次实现后应同步更新以下文档,避免数据库说明继续失真:
- `assets/对接流水分析/ccdi_bank_statement.md`
- 如有必要,同步 `docs/plans/2026-03-04-bank-statement-entity-design.md` 中的表结构补充说明
## 影响范围
后端代码:
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapper.java`
- `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml`
测试:
- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java`
数据库与文档:
- 建议新增测试库迁移脚本到 `assets/database/`
- `assets/对接流水分析/ccdi_bank_statement.md`
## 测试设计
至少覆盖以下场景:
1. 标准化逻辑:`LE_ACCOUNT_NO` 前后空格
2. 首次导入:记录正常插入
3. 重复导入:不报错、不更新原记录
4. 混合批次:重复记录跳过,新增记录写入
5. 非唯一键类数据库异常:仍然向上抛出并触发 `parsed_failed`
6. 本地 MySQL 验证:确认 no-op upsert 的受影响行数语义
## 风险与缓解
| 风险 | 影响 | 缓解方案 |
|------|------|----------|
| 测试库已有异常/重复数据,新增唯一键失败 | 高 | 先清洗异常行和重复行,再加唯一键 |
| `project_id` / `LE_ACCOUNT_NO` / `ACCOUNTING_DATE_ID` 空值绕过唯一键语义 | 高 | 迁移时删除异常测试数据并收紧为 `NOT NULL` |
| `ON DUPLICATE KEY` 受影响行数语义与预期不一致 | 中 | 实测后决定日志计数方案,不影响去重正确性 |
| 资产文档与代码继续漂移 | 中 | 实现后同步更新表结构说明 |
## 验收标准
1. 使用相同五元组重复导入时,数据库仅保留原记录
2. 重复导入不会更新原记录的任何业务字段
3. 命中重复不会导致上传记录失败
4. 非重复键类数据库错误仍会让上传记录进入 `parsed_failed`
5. 唯一键规则对后续其他写入入口同样生效

View File

@@ -1,103 +0,0 @@
# 流水导入CSV和PDF文件格式支持设计
## 概述
扩展流水导入功能支持CSV和PDF格式的文件上传与前端已有的文件类型配置保持一致。
## 背景
### 当前问题
| 层级 | 当前支持格式 | 问题 |
|------|-------------|------|
| 前端提示 | PDF、CSV、Excel | - |
| 前端校验 | `.pdf`, `.csv`, `.xlsx`, `.xls` | - |
| 后端校验 | 仅 `.xlsx`, `.xls` | ❌ 与前端不一致 |
**根本原因**:后端 `CcdiFileUploadController.java` 第65行只校验Excel格式导致上传CSV或PDF文件时被拒绝。
## 设计方案
### 修改范围
| 模块 | 文件 | 修改类型 |
|------|------|---------|
| ccdi-project | CcdiFileUploadController.java | 扩展文件类型校验 |
### 具体修改
**文件路径**`ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java`
**修改位置**第65-67行
**修改前**
```java
if (!fileName.endsWith(".xlsx") && !fileName.endsWith(".xls")) {
return AjaxResult.error("文件 " + fileName + " 格式不支持仅支持Excel文件");
}
```
**修改后**
```java
String lowerFileName = fileName.toLowerCase();
if (!lowerFileName.endsWith(".xlsx") && !lowerFileName.endsWith(".xls")
&& !lowerFileName.endsWith(".csv") && !lowerFileName.endsWith(".pdf")) {
return AjaxResult.error("文件 " + fileName + " 格式不支持,仅支持 PDF、CSV、Excel 文件");
}
```
### 改进点
1. **添加格式支持**:支持 `.csv``.pdf` 文件
2. **大小写不敏感**:使用 `toLowerCase()` 处理文件名,支持 `.CSV``.Pdf` 等扩展名变体
3. **错误提示优化**:与前端提示保持一致,用户体验更统一
## 技术要点
### 文件格式与流水分析平台兼容性
- 流水分析平台API已支持CSV文件上传根据前期探索确认
- PDF格式同样被平台接受
- 后端只负责文件类型校验,实际解析由流水分析平台处理
### 后续无需修改的部分
- 前端代码已正确配置,无需修改
- 文件上传服务(`CcdiFileUploadServiceImpl`)无需修改
- 数据库表结构无需修改
## 测试要点
### 功能测试
1. 上传 `.csv` 文件 → 成功
2. 上传 `.pdf` 文件 → 成功
3. 上传 `.xlsx` 文件 → 成功(原有功能)
4. 上传 `.xls` 文件 → 成功(原有功能)
5. 上传 `.txt` 文件 → 失败,提示格式不支持
### 边界测试
1. 上传 `.CSV`(大写)→ 成功
2. 上传 `.Csv`(混合大小写)→ 成功
3. 上传其他格式文件 → 失败
## 风险评估
| 风险 | 级别 | 应对措施 |
|------|------|---------|
| 流水分析平台不支持某些CSV/PDF变体 | 低 | 平台已确认支持,后端不做内容校验 |
| 文件大小超限 | 无 | 已有50MB限制无需额外处理 |
## 实施步骤
1. 修改 `CcdiFileUploadController.java` 第65-67行代码
2. 启动后端服务
3. 通过Swagger或前端页面测试各种格式文件上传
4. 验证错误提示正确显示
## 预计影响
- **代码改动量**1个文件约3行代码
- **测试工作量**约10分钟
- **部署风险**:极低(仅扩展支持范围,不影响现有功能)

View File

@@ -1,263 +0,0 @@
# 流水文件解析成功状态延后到流水入库完成设计
## 概述
调整流水文件上传异步处理链路中“解析成功”的业务含义。
当前实现里,只要流水分析平台返回“解析成功且可确认账户”,系统就会立即把上传记录状态更新为 `parsed_success`,随后才执行步骤 7 获取流水数据并写入本地数据库。
本次设计将 `parsed_success` 的含义收紧为:
- 流水分析平台解析成功
- 步骤 7 获取流水数据成功
- 流水数据成功写入本地数据库
在步骤 7 完成前,页面继续显示“解析中”。
## 背景
### 当前现状
当前核心逻辑位于 `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
1. 上传文件到流水分析平台
2. 轮询解析状态
3. 调用上传状态接口判断是否解析成功
4. 立即更新 `ccdi_file_upload_record.file_status = parsed_success`
5. 再调用 `fetchAndSaveBankStatements(...)` 获取流水并入库
这会产生两个问题:
1. 前端会看到“解析成功”,但数据库里的流水可能还没有写完
2. 步骤 7 失败时,记录可能先显示成功,随后又因为异常被改成失败,状态语义不稳定
### 前端约束
当前前端只识别以下四种状态:
- `uploading`
- `parsing`
- `parsed_success`
- `parsed_failed`
本次需求明确要求:
- 不新增前端状态
- 当步骤 6 已经确认平台解析成功,但步骤 7 尚未完成时,页面继续显示“解析中”
## 方案对比
### 方案一:仅后移 `parsed_success` 更新时机
做法:
- 步骤 6 解析成功后,不更新状态
- 执行步骤 7
- 步骤 7 执行结束后,再更新为 `parsed_success`
优点:
- 改动最小
- 前端和数据库状态枚举都不需要调整
缺点:
- 当前 `fetchAndSaveBankStatements(...)` 没有显式返回成功或失败结果
- 方法内部存在“记录异常后继续处理”的行为,容易把部分失败误判为成功
### 方案二:后移成功状态,并让步骤 7 返回明确执行结果
做法:
- 步骤 6 只确认“平台解析成功且可以获取流水”
- 记录状态继续保持 `parsing`
- 步骤 7 返回结构化结果,例如 `success``savedCount``errorMessage`
- 只有步骤 7 明确成功后,才更新 `parsed_success`
- 步骤 7 任一关键失败,则更新为 `parsed_failed`
优点:
- 状态语义完整且稳定
- 能避免“伪成功”
- 与当前前端状态模型兼容
缺点:
- 需要对步骤 7 做一定重构
### 方案三:拆分为解析状态和入库状态两个维度
做法:
- 新增“解析状态”和“入库状态”两个字段
- 前端组合展示
优点:
- 状态表达最完整
缺点:
- 涉及数据库、后端查询统计、前端状态映射等多处改动
- 超出本次需求范围
## 最终方案
采用方案二。
### 核心决策
1. `parsed_success` 只表示“流水数据已经成功入库”
2. 步骤 6 解析成功后,记录状态继续保持 `parsing`
3. 步骤 7 必须显式返回成功或失败结果
4. 步骤 7 失败时,将上传记录更新为 `parsed_failed`
5. 步骤 7 失败时,清理本次 `logId` 对应的已落库流水,避免半成品数据残留
## 详细设计
### 1. 主流程状态流转
调整 `processFileAsync(...)` 的状态流转如下:
1. 初始创建记录时为 `uploading`
2. 文件上传到流水分析平台成功后,更新为 `parsing`
3. 轮询解析完成
4. 调用文件上传状态接口判断平台是否解析成功
5. 若平台解析失败,更新为 `parsed_failed`
6. 若平台解析成功,不更新为 `parsed_success`,继续保持 `parsing`
7. 执行步骤 7 获取流水并入库
8. 步骤 7 成功后,一次性更新:
- `file_status = parsed_success`
- `enterprise_names`
- `account_nos`
- 清空可能残留的 `error_message`
9. 步骤 7 失败后,更新:
- `file_status = parsed_failed`
- `error_message = 失败原因`
### 2. 步骤 7 返回结构化结果
`fetchAndSaveBankStatements(Long projectId, Integer groupId, Integer logId)``void` 改为返回结构化结果对象。
建议新增内部结果对象,例如:
```java
@Data
private static class FetchBankStatementResult {
private boolean success;
private int totalCount;
private int savedCount;
private String errorMessage;
}
```
返回语义建议如下:
- `success = true`
- 已成功完成全部分页拉取和数据库落库
- `savedCount` 为实际保存条数
- `success = false`
- 任一关键步骤失败
- `errorMessage` 写明失败原因
### 3. 步骤 7 的成功判定
步骤 7 需同时满足以下条件才算成功:
1. 首次 `getBankStatement` 请求成功返回
2. 分页总数计算正常
3. 所有分页请求成功完成
4. 所有批量插入操作成功完成
5. 最终保存条数与已拉取条数一致
其中 `totalCount = 0` 的场景按成功处理,原因如下:
- 平台已经解析成功
- 业务上允许“解析成功但无流水”
- 否则记录会长期停留在 `parsing` 或被错误标记为失败
### 4. 步骤 7 的失败处理
当前实现中,分页循环内部发生异常后会记录日志并继续下一页。该行为不适用于本次状态语义。
调整后规则:
1. 首次查询总数失败,直接返回失败
2. 任一分页请求失败,直接返回失败
3. 任一批量插入失败,直接返回失败
4. 返回失败前,清理当前 `logId` 已写入的流水数据
### 5. 半成品流水清理
`ccdi_bank_statement` 已存在 `batch_id` 字段,且当前实体 `CcdiBankStatement.batchId` 已映射该字段。
因此步骤 7 中应确保每条流水都带上本次上传的 `logId`
```java
statement.setProjectId(projectId);
statement.setBatchId(logId);
```
同时在 `CcdiBankStatementMapper` 中新增清理接口,例如:
```java
int deleteByProjectIdAndBatchId(@Param("projectId") Long projectId,
@Param("batchId") Integer batchId);
```
用于在步骤 7 失败时删除本次已插入的流水,避免出现“部分落库但上传记录失败”的脏数据。
### 6. 为什么不使用长事务
不建议把步骤 7 做成覆盖远程接口调用和全部分页落库的单个数据库事务,原因如下:
1. 远程接口调用时间不可控
2. 全量分页获取可能持续较久
3. 长事务会占用数据库连接并增加锁持有时间
因此本次采用“显式成功判定 + 失败补偿清理”的方式,而不是“长事务回滚”。
## 影响范围
### 后端
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapper.java`
- `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml`
### 测试
- 新增 `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java`
### 前端
无须改动。
当前前端的 `parsing` 状态即可承载“平台解析成功但流水尚未入库完成”的阶段。
## 测试设计
至少覆盖以下场景:
1. 平台解析成功,步骤 7 全量拉取并入库成功,最终状态应为 `parsed_success`
2. 平台解析成功,但首次获取流水总数失败,最终状态应为 `parsed_failed`
3. 分页处理中途失败,最终状态应为 `parsed_failed`,且已写入流水被清理
4. 批量插入失败,最终状态应为 `parsed_failed`,且已写入流水被清理
5. `totalCount = 0`,最终状态应为 `parsed_success`
6. 平台解析失败,保持现有失败路径
## 风险与缓解
| 风险 | 影响 | 缓解方案 |
|------|------|----------|
| 步骤 7 重构后改变现有异常处理行为 | 中 | 使用单元测试锁定成功、失败、零数据三类分支 |
| 清理逻辑误删其他流水 | 高 | 删除条件必须同时绑定 `projectId``batchId(logId)` |
| 失败原因不清晰 | 中 | 统一由步骤 7 返回明确 `errorMessage`,最终写入 `ccdi_file_upload_record.error_message` |
## 验收标准
1. 当流水平台解析成功但本地仍在入库时,上传记录保持 `parsing`
2. 只有本地流水入库完成后,上传记录才变为 `parsed_success`
3. 任一步骤 7 失败,上传记录为 `parsed_failed`
4. 步骤 7 失败后,不残留本次 `logId` 的半成品流水
5. 前端无需新增状态,现有页面展示符合预期

View File

@@ -1,240 +0,0 @@
# 参数配置类型显示设计文档
## 概述
在项目详情页面的参数配置页面,显示当前参数配置是默认配置还是自定义配置。
## 需求分析
### 背景
当前系统中,项目可以使用两种参数配置方式:
- **默认配置**:使用系统级默认参数(`configType = 'default'`
- **自定义配置**:项目独立的自定义参数(`configType = 'custom'`
用户在查看项目详情时,无法直观地识别当前项目使用的是哪种配置方式。
### 目标
在项目详情页面顶部,项目名称旁边添加配置类型标签,让用户能够快速识别当前项目的参数配置类型。
## 设计方案
### 1. 展示位置
**页面位置:** 项目详情页面顶部(`detail.vue`
**具体位置:** 项目名称和状态标签旁边
**展示效果:**
```
[返回] 2024年Q1初核项目 [进行中] [默认配置]
最后更新时间2026-03-09 10:30:00
```
### 2. 标签样式
使用 Element UI 的 `el-tag` 组件,采用不同颜色区分:
| 配置类型 | 标签文字 | 颜色类型 | 视觉效果 |
|---------|---------|---------|---------|
| 默认配置 | "默认配置" | `info`(蓝色) | 蓝色背景标签 |
| 自定义配置 | "自定义配置" | `warning`(橙色) | 橙色背景标签 |
### 3. 实现方案
**方案选择:** 纯前端实现
**理由:**
- ✅ 实现简单快速
- ✅ 不需要修改后端代码
- ✅ 利用现有数据(`projectInfo.configType`
- ✅ 性能最优(无额外请求)
- ✅ 风险最小
**数据流:**
1. 用户打开项目详情页面
2. 前端调用 `getProject(projectId)` 获取项目信息
3. 后端返回包含 `configType` 字段的项目数据
4. 前端根据 `configType` 值显示对应的配置类型标签
## 技术实现
### 前端修改
**修改文件:** `ruoyi-ui/src/views/ccdiProject/detail.vue`
**修改位置:** 第 10-19 行的页面标题区域
**代码实现:**
```vue
<template>
<div class="detail-header">
<div class="header-left">
<el-button size="small" icon="el-icon-back" @click="handleBack"
>返回</el-button
>
<div class="title-section">
<div class="page-title">
<h2>
{{ projectInfo.projectName }}
</h2>
<el-tag
:type="getStatusType(projectInfo.projectStatus)"
size="small"
>
{{ getStatusLabel(projectInfo.projectStatus) }}
</el-tag>
<!-- 新增配置类型标签 -->
<el-tag
:type="getConfigTypeStyle(projectInfo.configType)"
size="small"
>
{{ getConfigTypeLabel(projectInfo.configType) }}
</el-tag>
</div>
<p class="update-time">
最后更新时间{{ formatUpdateTime(projectInfo.updateTime) }}
</p>
</div>
</div>
<!-- ... 其他代码 ... -->
</div>
</template>
<script>
export default {
methods: {
// ... 现有方法 ...
/**
* 获取配置类型标签文字
* @param {string} configType - 配置类型
* @returns {string} 标签文字
*/
getConfigTypeLabel(configType) {
const configTypeMap = {
'default': '默认配置',
'custom': '自定义配置'
};
return configTypeMap[configType] || '默认配置';
},
/**
* 获取配置类型标签样式
* @param {string} configType - 配置类型
* @returns {string} Element UI tag 类型
*/
getConfigTypeStyle(configType) {
const styleMap = {
'default': 'info', // 蓝色
'custom': 'warning' // 橙色
};
return styleMap[configType] || 'info';
}
}
}
</script>
```
### 后端依赖
**无需修改后端代码**
**已有数据支持:**
- `CcdiProject` 实体类已包含 `configType` 字段
- `getProject()` 接口已返回完整的 `configType` 数据
- 数据库表 `ccdi_project` 已存储 `config_type` 字段
### 数据字典
系统已存在配置类型字典(在 `sql/ccdi_project.sql` 中初始化):
```sql
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
VALUES ('配置方式', 'ccdi_config_type', '0', 'admin', NOW(), '项目配置方式');
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time)
VALUES
(1, '全局默认配置', 'default', 'ccdi_config_type', '', 'primary', 'Y', '0', 'admin', NOW()),
(2, '自定义配置', 'custom', 'ccdi_config_type', '', 'warning', 'N', '0', 'admin', NOW());
```
**注意:** 虽然系统已有配置类型字典,但本设计采用纯前端硬编码方式,理由是:
- 配置类型固定(仅两种),无需动态配置
- 避免增加字典查询的复杂度
- 简化实现,提高性能
## 测试方案
### 功能测试
**测试场景:**
1. **默认配置项目**
- 前提:项目 `configType = 'default'`
- 预期:显示蓝色标签 "默认配置"
2. **自定义配置项目**
- 前提:项目 `configType = 'custom'`
- 预期:显示橙色标签 "自定义配置"
3. **配置类型为空**
- 前提:项目 `configType``null``undefined`
- 预期:显示蓝色标签 "默认配置"(默认值)
4. **配置类型切换**
- 前提:用户修改参数后保存
- 预期:配置类型从 `default` 切换为 `custom`,标签从蓝色变为橙色
### 边界测试
- 新建项目默认使用默认配置
- 刷新页面后标签状态保持一致
- 不同浏览器显示效果一致
### 兼容性测试
- Chrome、Firefox、Edge 等主流浏览器
- 不同分辨率下的显示效果
## 实施影响
### 用户影响
- **正面影响:** 用户可以直观识别项目的参数配置类型
- **负面影响:** 无
### 系统影响
- **性能影响:** 极小(仅增加一个标签渲染)
- **兼容性:** 完全向后兼容
- **风险:** 无(纯前端展示,不影响业务逻辑)
## 实施步骤
1. **修改前端代码** - 修改 `detail.vue` 文件
2. **本地测试** - 验证标签显示正确
3. **提交代码** - 提交到 Git 仓库
4. **部署上线** - 正式环境部署
## 验收标准
✅ 项目详情页面顶部显示配置类型标签
✅ 默认配置显示蓝色 "默认配置" 标签
✅ 自定义配置显示橙色 "自定义配置" 标签
✅ 标签位置在状态标签旁边
✅ 标签样式与设计一致
✅ 不影响现有功能
## 后续优化
暂无后续优化计划。
---
**设计日期:** 2026-03-09
**设计人员:** Claude Code
**审核状态:** 待审核

View File

@@ -1,371 +0,0 @@
# 项目详情流水明细查询设计
## 概述
本次设计面向项目详情页中的“流水明细查询”菜单,按原型图实现“查询 + 筛选 + 导出”能力,不包含旧需求中的“加入分析”“二次分析”等扩展功能。
页面默认查询当前项目下全部已入库流水,数据源仅使用本地表 `ccdi_bank_statement`,不再依赖上传记录的“查看流水”跳转。上传数据页同步移除上传记录表的操作列,后续不再从上传记录进入流水明细页。
## 已确认范围
- 页面入口保留在项目详情页的 `detail` 菜单
- 默认查询范围为当前 `projectId` 下全部流水
- 上传页移除“查看流水”入口
- “导出流水”需要实现真实功能
- 导出范围为当前页面筛选后的全部结果
- 本次只做“查询 + 筛选 + 导出”
- 摘要筛选仅使用 `userMemo`
- 筛选栏中的多选项需要基于整个项目维度单独查询
- 进入页面时即并行加载列表数据和项目级多选项
## 方案对比
### 方案一:本地库专用查询模块
-`ccdi-project` 模块新增银行流水查询 Controller、Service、Mapper 查询方法
- 页面直接查询本地 `ccdi_bank_statement`
- 导出使用本地查询条件复用
优点:
- 与现有“上传后落本地库”架构一致
- 查询与导出结果稳定,可复现
- 后续可继续扩展更多项目级分析能力
缺点:
- 需要补充 DTO、VO、Mapper SQL、导出模型
### 方案二:大而全聚合接口
- 用一个接口同时返回列表、多选项、统计信息
优点:
- 前端调用少
缺点:
- 接口职责混杂
- 后续增加筛选维度时维护成本高
- 导出仍需要单独处理
### 方案三:直接回源 LSFX 接口
- 页面查询与导出实时调用流水分析平台接口
优点:
- 本地查询层改动少
缺点:
- 与现有本地入库方案冲突
- 项目级全量查询稳定性差
- 页面与导出口径难统一
## 选型
采用方案一:本地库专用查询模块。
该方案与当前 `ccdi_bank_statement``CcdiFileUploadServiceImpl` 的入库逻辑一致,最符合“项目维度统一查询 + 当前筛选导出”的实现要求。
## 页面结构
页面仍由 `ruoyi-ui/src/views/ccdiProject/detail.vue` 动态加载 `DetailQuery.vue`
`DetailQuery.vue` 从占位组件升级为正式页面,整体布局分为左右两栏:
- 左侧:固定筛选栏
- 右侧:结果区域
右侧结果区域包含:
- 标题“流水明细查询”
- 顶部页签:`全部` / `流入` / `流出`
- 导出按钮:`导出流水`
- 列表表格
- 分页器
列表中的操作位保留只读详情能力,用于打开详情抽屉或弹窗查看单条流水完整字段。
## 筛选项设计
左侧筛选栏按原型提供以下条件:
1. 交易时间
- 对应字段:`trxDate`
- 支持起止范围查询
- 后端兼容 `yyyy-MM-dd HH:mm:ss``yyyy-MM-dd`
2. 对方名称 + 空值
- 对应字段:`customerAccountName`
- 输入框为模糊匹配
- 勾选空值时匹配 `null`、空串、全空白字符串
3. 摘要 + 空值
- 对应字段:`userMemo`
- 输入框为模糊匹配
- 勾选空值时匹配 `null`、空串、全空白字符串
4. 本方主体
- 对应字段:`leAccountName`
- 多选
5. 本方银行
- 对应字段:`bank`
- 多选
6. 本方账户
- 对应字段:`leAccountNo`
- 多选
7. 交易金额
- 使用绝对值范围筛选
- 统一适配 `全部 / 流入 / 流出` 三个页签
8. 对方账户 + 空值
- 对应字段:`customerAccountNo`
- 输入框为模糊匹配
9. 交易类型 + 空值
- 对应字段:`cashType`
- 输入框为模糊匹配
## 多选项加载规则
本方主体、本方银行、本方账户三类多选项不从当前列表结果派生,而是通过单独接口按整个项目维度去重查询。
进入页面时前端并行加载:
- 列表接口:默认查询当前项目全部流水
- 多选项接口:返回整个项目维度的全部主体、银行、账户选项
多选项接口返回值:
- `ourSubjectOptions`
- `ourBankOptions`
- `ourAccountOptions`
前端多选框内部搜索仅在已加载的项目级选项上做本地过滤,不再触发额外远程请求。
## 列表列设计
表格列与原型语义对齐:
1. 交易时间
- 字段:`trxDate`
2. 本行账户/主体
- 主行:`leAccountNo`
- 副行:`leAccountName`
3. 对方名称/账户
- 主行:`customerAccountName`
- 副行:`customerAccountNo`
4. 摘要/交易类型
- 主行:`userMemo`
- 副行:`cashType`
5. 交易金额
- 流入显示 `+amountCr`
- 流出显示 `-amountDr`
- 页面展示统一输出计算后的 `displayAmount`
6. 操作
- 只读详情
## 页签与排序规则
### 页签规则
- `全部`:不过滤金额方向
- `流入`:仅查询 `amount_cr > 0`
- `流出`:仅查询 `amount_dr > 0`
### 排序规则
支持两个排序字段:
- 交易时间
- 交易金额
排序方向支持:
- 升序
- 降序
交易金额排序按绝对值排序,和金额范围筛选保持一致。
排序字段必须由后端白名单控制,禁止前端任意传值直接拼接 SQL。
## 后端接口设计
建议新增专用 Controller`CcdiBankStatementController`
### 1. 列表分页查询
- 路径:`GET /ccdi/project/bank-statement/list`
- 入参:`CcdiBankStatementQueryDTO`
- 返回:`TableDataInfo`
入参字段包括:
- `projectId`
- `tabType`
- `transactionStartTime`
- `transactionEndTime`
- `counterpartyName`
- `counterpartyNameEmpty`
- `userMemo`
- `userMemoEmpty`
- `ourSubjects`
- `ourBanks`
- `ourAccounts`
- `amountMin`
- `amountMax`
- `counterpartyAccount`
- `counterpartyAccountEmpty`
- `transactionType`
- `transactionTypeEmpty`
- `orderBy`
- `orderDirection`
### 2. 多选项查询
- 路径:`GET /ccdi/project/bank-statement/options`
- 入参:`projectId`
- 返回:`CcdiBankStatementFilterOptionsVO`
### 3. 单条详情
- 路径:`GET /ccdi/project/bank-statement/detail/{bankStatementId}`
- 返回:`CcdiBankStatementDetailVO`
### 4. 导出
- 路径:`POST /ccdi/project/bank-statement/export`
- 入参:复用 `CcdiBankStatementQueryDTO`
- 返回Excel 文件流
## 服务层设计
建议新增:
- `ICcdiBankStatementService`
- `CcdiBankStatementServiceImpl`
职责拆分:
- 列表查询参数校验与标准化
- 排序字段白名单处理
- 时间范围解析与兼容
- 多选项去重查询
- 单条详情查询
- 导出列表查询与导出模型组装
## Mapper 设计
保留现有 `CcdiBankStatementMapper`,在接口和 XML 中新增查询方法,不新增独立 Mapper。
建议补充的方法:
- `selectStatementPage`
- `selectStatementListForExport`
- `selectFilterOptions`
- `selectStatementDetailById`
SQL 规则:
- 基于 `project_id` 做主过滤
- 使用动态 SQL 处理输入框、空值、多选数组
- 用统一表达式计算排序时间字段
- 用统一表达式计算展示金额与金额排序值
## 前端设计
建议新增 API 文件:
- `ruoyi-ui/src/api/ccdiProjectBankStatement.js`
建议改造文件:
- `ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue`
页面初始化流程:
1. 读取 `projectId`
2. 并行调用:
- `list`
- `options`
3. 渲染左侧筛选和右侧列表
交互规则:
- 查询、页签切换、排序切换、分页切换时重置到第一页并刷新列表
- 重置时恢复默认筛选并回到 `全部`
- 导出时使用当前筛选条件导出全部结果
## 导出规则
- 导出范围:当前页面筛选后的全部结果
- 不受当前分页限制
- 导出文件名:`项目名称_流水明细_时间戳.xlsx`
- 导出列与页面列表保持一致:
- 交易时间
- 本方账户
- 本方主体
- 对方名称
- 对方账户
- 摘要
- 交易类型
- 交易金额
- 当前筛选结果为空时,不生成空文件,直接提示“当前条件下无可导出数据”
## 异常处理
- 多选项接口失败:列表仍可查询,筛选项置空并提示可刷新重试
- 列表接口失败:显示错误态并保留当前筛选值
- 详情接口失败:仅阻断详情展示,不影响主页面
- 导出失败:保留筛选条件并提示失败原因
- 项目下无流水数据:显示空态,导出按钮禁用
## 验收标准
### 功能验收
- 进入页面时并行加载列表与项目级多选项
- 默认展示当前项目下全部流水
- 三个页签切换结果正确
- 所有筛选项单独与组合查询均正确
- 空值筛选逻辑正确
- 交易时间兼容两种时间格式
- 金额范围按绝对值筛选正确
- 排序仅支持交易时间与交易金额
- 导出结果与页面筛选口径一致
- 上传页操作列已移除,不再支持查看流水跳转
### 技术验收
- 后端遵循项目 DTO / VO 分层规范
- Controller 使用 Swagger 注释
- 简单查询由 MyBatis Plus + XML 动态 SQL 实现
- 前端 API 独立封装到 `src/api`
- 页面在桌面端和移动端均可正常展示
## 风险与约束
1. `TRX_DATE` 为字符串字段
- 需要在 SQL 或服务层统一解析,保证筛选与排序稳定
2. 历史数据可能存在空字符串与空白字符串混用
- 空值筛选必须统一兼容
3. 项目维度多选项可能数量较大
- 首版先按项目维度一次性加载
- 若实际数据量过大,再考虑远程搜索优化
4. 当前页面不引入旧需求中的二次分析能力
- 后续若扩展,需要新增业务表与关联逻辑

View File

@@ -1,390 +0,0 @@
# 项目详情拉取本行信息设计
## 概述
本次设计面向项目详情页“上传数据”菜单中的“拉取本行信息”能力。用户在页面点击按钮后,弹出录入弹窗,支持手动输入身份证号、上传身份证 Excel 文件自动解析回填、选择时间跨度,然后提交后台异步拉取本行流水。
后端以项目现有“文件上传记录 + 线程池 + 落本地流水表”的链路为基础实现,不再新增第二套独立任务体系。每个身份证对应一条文件上传记录和一个线程任务,先调用流水分析平台“拉取行内流水”接口获取 `logId`,再复用现有“解析状态轮询 -> 获取文件上传状态 -> 获取流水列表并入库”的后半段处理链路。
## 已确认范围
- 页面入口保留在 `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue`
- 点击“拉取本行信息”后弹出表单弹窗,不再使用简单确认框
- 弹窗字段包括:
- 证件号码文本域
- 身份证文件上传
- 时间跨度
- 身份证文件解析规则:
- 读取首个 sheet
- 只读取第一列
- 忽略表头
- 忽略空行
- 忽略重复值
- 上传身份证文件后立即自动解析,并将结果回填到文本域
- 文本域与文件解析结果合并后按输入顺序去重
- 文件上传记录表中的 `uploadUser` 使用 `SecurityUtils.getUserName()`
- 调用流水分析平台 `fetchInnerFlow` 时的 `uploadUserId` 使用 `SecurityUtils.getUserId()`
- 创建上传记录时,先将身份证号写入 `accountNos` 作为主体账号
- 每个身份证使用一个线程处理
- 在调用“获取单个文件上传后的状态”接口时,根据返回值中的文件名更新文件上传记录
- 获取 `logId` 之后的处理步骤与现有“导入流水文件”方法保持一致
## 方案对比
### 方案一:在现有文件上传服务中扩展并抽取公共流水线
- 继续使用 `CcdiFileUploadController``ICcdiFileUploadService``CcdiFileUploadServiceImpl`
- 新增身份证文件解析接口和拉取本行信息提交接口
- 将现有“文件上传成功拿到 logId 后”的处理逻辑抽成公共方法,供文件上传和本行拉取共同复用
优点:
- 与现有上传记录列表、线程池、状态轮询、流水入库逻辑完全一致
- 复用度最高,后续维护成本最低
- 记录表、状态统计、页面轮询无需新增体系
缺点:
- 需要对现有 `CcdiFileUploadServiceImpl` 做一次中等规模重构
### 方案二:新增独立的本行拉取服务
- 新建独立 Controller 和 Service
- 仅在最后复用“获取流水列表并入库”的局部能力
优点:
- 对现有文件上传代码侵入较小
缺点:
- 会出现两套高度相似的任务调度和状态回写逻辑
- 后续容易出现功能漂移和修复不一致
### 方案三:在 Controller 中直接串接已有逻辑
- Controller 直接完成记录插入、线程调度和接口调用
优点:
- 早期开发速度快
缺点:
- Controller 职责过重
- 不利于测试和后续扩展
## 选型
采用方案一:在现有文件上传服务中扩展,并抽取“拿到 `logId` 后的公共处理流水线”。
该方案最符合当前项目已经存在的上传记录表、线程池和流水落库架构,可以保证文件上传和本行拉取在状态、错误处理和数据口径上保持一致。
## 前端交互设计
页面文件仍为 `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue`
### 弹窗结构
新增“拉取本行信息”弹窗,包含以下控件:
1. 证件号码文本域
- 占位提示:支持逗号、中文逗号、换行分隔
- 用于展示最终待提交的身份证集合
2. 身份证文件上传
- 仅支持 `.xlsx``.xls`
- 选中文件后立即自动调用后端解析接口
- 解析成功后,把有效身份证集合合并回填到文本域
3. 时间跨度
- 开始日期
- 结束日期
- 提交时必填
### 前端交互规则
1. 用户选择身份证文件后:
- 立即上传到解析接口
- 前端显示“正在解析身份证文件”
- 解析成功后:
- 将文件解析出的身份证集合与文本域当前内容合并
- 去重后回填到文本域
- 提示解析成功及有效条数
- 解析失败后:
- 保留文本域现有内容
- 显示明确错误提示
2. 用户点击“确认拉取”时:
- 前端先对文本域内容做本地拆分和去重
- 校验身份证集合非空、日期范围完整
- 调用正式提交接口
- 提交成功后关闭弹窗,刷新上传记录列表和统计,并开启现有轮询
3. 页面上传记录列表无需新增新页面:
- 本行拉取创建的记录与文件上传记录共用同一列表
- 状态、上传时间、上传人展示口径保持一致
## 后端接口设计
接口统一放在 `CcdiFileUploadController` 下。
### 1. 解析身份证文件
- 路径:`POST /ccdi/file-upload/parse-id-card-file`
- 请求类型:`multipart/form-data`
- 入参:
- `file`
- 返回:
- `idCards`
- `count`
用途:
- 供弹窗选择文件后即时解析
- 只负责读取、去重、校验身份证,不创建任务
### 2. 提交拉取本行信息任务
- 路径:`POST /ccdi/file-upload/pull-bank-info`
- 请求类型:`application/json`
- 入参:
- `projectId`
- `idCards`
- `startDate`
- `endDate`
- 返回:
- `batchId`
用途:
- 正式提交本行拉取任务
- 一次请求可提交多个身份证
## DTO / VO 设计
建议新增:
- `CcdiPullBankInfoSubmitDTO`
- `Long projectId`
- `List<String> idCards`
- `String startDate`
- `String endDate`
- `CcdiIdCardParseVO`
- `List<String> idCards`
- `Integer count`
## 服务层设计
继续使用:
- `ICcdiFileUploadService`
- `CcdiFileUploadServiceImpl`
建议新增两个对外方法:
1. `parseIdCardFile(MultipartFile file)`
- 读取首个 sheet 第一列
- 忽略表头、空值、重复值
- 统一执行身份证格式校验
2. `submitPullBankInfo(Long projectId, List<String> idCards, String startDate, String endDate, Long userId, String username)`
- 校验项目和日期
- 插入上传记录
- 在事务提交后调度线程池任务
## 核心数据流设计
### 一、身份证文件解析
1. Controller 接收 Excel 文件
2. Service 使用 EasyExcel 读取首个 sheet 第一列
3. 将单元格内容转成字符串并清理空白
4. 忽略首行表头
5. 使用 `LinkedHashSet` 去重并保序
6. 对每个值执行身份证格式校验
7. 返回有效身份证集合
### 二、正式提交任务
1. 校验 `projectId`
2. 查询项目,获取 `lsfxProjectId`
3. 校验 `startDate``endDate`
4. 校验身份证集合非空
5. 为每个身份证创建一条 `ccdi_file_upload_record`
- `projectId = 当前项目`
- `lsfxProjectId = 项目关联流水分析ID`
- `fileStatus = uploading`
- `fileName = 身份证号`
- `accountNos = 身份证号`
- `uploadUser = SecurityUtils.getUserName()`
- `uploadTime = 当前时间`
6. 批量插入记录
7. 在事务提交后启动调度线程
### 三、线程处理单个身份证
每个身份证一个线程,使用现有 `fileUploadExecutor`
单线程处理步骤:
1. 调用 `fetchInnerFlow`
- `groupId = lsfxProjectId`
- `customerNo = 身份证号`
- `dataChannelCode = ZJRCU`
- `requestDateId = 当天 yyyyMMdd`
- `dataStartDateId = 开始日期 yyyyMMdd`
- `dataEndDateId = 结束日期 yyyyMMdd`
- `uploadUserId = SecurityUtils.getUserId()`
2. 从响应中获取唯一 `logId`
3. 进入公共处理流水线
- 更新记录 `logId`
- 更新状态为 `parsing`
- 轮询解析状态
- 调用“获取单个文件上传状态”接口
- 读取文件名字段,优先使用 `uploadFileName`,取不到则回退 `downloadFileName`
- 将文件名回写到 `ccdi_file_upload_record.file_name`
- 提取主体名称、主体账号
- 调用“获取流水列表”接口分页拉取并入库
- 成功后更新状态为 `parsed_success`
- 失败时更新状态为 `parsed_failed`
## 公共流水线重构设计
当前 `CcdiFileUploadServiceImpl` 中,`processFileAsync` 同时承担“上传文件并拿到 `logId`”和“拿到 `logId` 后继续处理”两段职责。
本次建议拆成两段:
1. 文件来源阶段
- 上传文件拿到 `logId`
- 或者拉取本行信息拿到 `logId`
2. 公共处理阶段
- 接收 `projectId``lsfxProjectId``record``logId`
- 负责后续统一处理
拆分后:
- 现有 `processFileAsync` 仍然保留,但只负责文件上传到平台并获得 `logId`
- 新增 `processPullBankInfoAsync` 负责调用 `fetchInnerFlow` 并获得 `logId`
- 两者统一调用新的公共处理方法,例如:
- `processRecordAfterLogIdReady(...)`
## 文件上传记录表回写规则
### 记录初始化
- `fileName`:先写身份证号占位
- `accountNos`:写身份证号
- `enterpriseNames`:初始为空
### 状态接口返回后
- 若状态接口返回 `uploadFileName`,更新到 `fileName`
-`uploadFileName` 为空但 `downloadFileName` 不为空,回写 `downloadFileName`
- `enterpriseNameList` 存在时更新 `enterpriseNames`
- `accountNoList` 存在时更新 `accountNos`
## 异常处理
### 提交阶段异常
以下场景直接拦截,不进入异步任务:
- 项目不存在
- 项目未绑定 `lsfxProjectId`
- 身份证集合为空
- 开始日期或结束日期为空
- 开始日期大于结束日期
### 文件解析异常
- 文件为空
- 文件格式不是 Excel
- 首个 sheet 第一列没有有效身份证
- 存在非法身份证号
解析异常直接返回错误,不覆盖前端已有输入值。
### 异步执行异常
每个身份证单独处理,单条失败不影响其他条目:
- `fetchInnerFlow` 失败:当前记录标记 `parsed_failed`
- 轮询超时:当前记录标记 `parsed_failed`
- 获取状态失败:当前记录标记 `parsed_failed`
- 获取流水失败:清理该 `logId` 已入库流水后标记 `parsed_failed`
错误信息统一落入 `error_message`,并延续现有超长错误截断规则。
## 测试设计
### 后端测试
重点新增以下测试:
1. 身份证文件解析测试
- 读取首个 sheet 第一列
- 忽略表头、空行、重复值
- 非法身份证时返回失败
2. 提交任务测试
- 为每个身份证插入一条上传记录
- 初始化 `accountNos` 为身份证号
- `uploadUser` 正确记录当前用户名
3. 公共流水线复用测试
- `fetchInnerFlow` 成功后能进入公共处理链路
- 状态接口返回文件名时能正确回写到记录表
- 流水入库失败时能清理已写入数据
4. Controller 测试
- 解析接口成功/失败
- 提交接口成功/失败
### 前端验证
- 选择身份证文件后自动解析并回填
- 手输身份证与文件解析结果正确合并去重
- 日期必填校验生效
- 提交成功后弹窗关闭并刷新列表
- 正在执行的记录可通过现有轮询刷新状态
## 验收标准
### 功能验收
- 上传数据页面可打开“拉取本行信息”弹窗
- 身份证 Excel 上传后能自动解析并回填输入框
- 提交后每个身份证都创建一条上传记录
- 每个身份证走一个线程处理
- 状态接口返回文件名后,记录列表能展示更新后的文件名
- 成功记录能拉取流水并入库
- 失败记录不会影响其他身份证继续执行
### 技术验收
- 复用现有 `fileUploadExecutor`
- 复用现有上传记录列表、统计和轮询机制
- Controller 使用 Swagger 注释
- Service 中公共处理逻辑不重复实现两套
- 后端测试覆盖解析、提交、公共流水线复用
## 风险与约束
1. 身份证文件模板不固定
- 首版只按“首个 sheet 第一列”解析
- 如后续存在多模板,再扩展模板识别
2. `fetchInnerFlow` 返回值只提供 `logId`
- 必须严格复用后续状态接口和流水接口,不能只看首个响应判断成功
3. 线程池容量与批量身份证数量存在上限
- 沿用现有线程池拒绝重试机制
- 超量场景下记录单条失败,不阻断整批任务
4. 文件名依赖状态接口返回
- 需要兼容 `uploadFileName` 为空的场景,并回退 `downloadFileName`

View File

@@ -1,68 +0,0 @@
# 上传数据主体账号展示设计文档
## 概述
在项目详情页的“上传数据”模块中,文件列表当前仅展示“主体名称”,未展示后端已经返回的“主体账号”。本次调整在不变更后端接口和数据库结构的前提下,为列表新增“主体账号”列,帮助用户直接核对解析出的账号信息。
## 需求分析
### 背景
- 上传数据文件列表位于项目详情页“上传数据”页签。
- 后端文件上传记录实体已包含 `accountNos` 字段,解析成功后会写入主体账号数据。
- 前端列表当前只渲染 `enterpriseNames`,导致用户无法在列表中直接查看主体账号。
### 目标
- 在上传数据文件列表中新增“主体账号”列。
- 直接复用现有接口返回的 `accountNos` 字段。
- 保持现有分页、状态、刷新逻辑不变。
## 方案对比
### 方案一:新增独立“主体账号”列
优点:
- 信息结构最清晰,主体名称和主体账号各自独立。
- 改动范围最小,只需调整前端表格模板。
- 与现有“主体名称”列保持一致,用户学习成本低。
缺点:
- 表格横向宽度略有增加。
### 方案二:主体名称和主体账号合并在同一列内分行展示
优点:
- 不增加表格列数。
缺点:
- 可读性下降,不利于后续排序、筛选或截图核对。
- 需要额外的模板和样式调整。
## 方案选择
采用方案一:新增独立“主体账号”列。
选择理由:
- 用户已经明确确认采用单独新增一列的展示方式。
- 后端字段已就绪,前端读取即用。
- 改动只落在上传数据列表模板和对应回归测试,风险最低。
## 数据流
1. 前端继续调用 `/ccdi/file-upload/list` 获取分页数据。
2. 后端返回每条上传记录的 `accountNos` 字段。
3. 前端在文件列表中新增“主体账号”列,读取 `scope.row.accountNos`
4. 当账号为空时,展示 `-`,与“主体名称”列的兜底方式保持一致。
## 错误处理
- `accountNos` 为空、`null` 或未返回时,前端展示 `-`
- 不新增请求,不改变轮询与刷新逻辑,因此不引入新的接口异常处理分支。
## 测试策略
- 新增前端静态回归测试,断言文件列表模板中存在 `prop="accountNos"` 且标签为“主体账号”。
- 按 TDD 流程先运行新测试并确认失败,再补模板代码。
- 回归执行现有上传数据列表相关静态测试,确保未误伤现有布局与分页设置。
- 执行前端生产构建,确认模板修改不影响编译。

View File

@@ -1,285 +0,0 @@
# 员工资产信息维护设计
## 背景
员工信息维护页面当前仅支持维护员工基础信息,不支持维护员工名下资产信息。现需在现有员工维护页面中补齐资产信息的新增、编辑、删除、详情展示和导入能力,并与现有员工导入交互保持一致。
本次设计基于 `2026-03-12` 确认了以下业务约束:
- 资产表保留字段名 `person_id`
- 资产表新增 `family_id`
- `family_id` 存归属员工身份证号
- `person_id` 存资产实际持有人身份证号
- 员工本人资产:`family_id = person_id = 员工身份证号`
- 员工亲属资产:`family_id = 员工身份证号``person_id = 亲属身份证号`
- `asset_id` 改为数据库自增主键,不出现在导入模板中
- 资产导入模板不要求填写 `family_id`
- 导入员工资产信息时,系统根据 `person_id` 自动填充归属员工的 `id_card``family_id`
- 资产导入失败记录入口需与员工导入失败记录入口明确区分,按钮文案为“查看员工资产导入失败记录”
## 目标
- 在员工信息维护页新增员工资产信息维护能力
- 在员工新增和编辑弹窗中支持员工资产信息的添加、编辑、删除
- 在员工详情弹窗中展示该员工全部资产信息
- 在员工列表页新增“导入资产信息”入口,并复用现有异步导入交互
- 删除员工时同步删除该员工全部资产信息
## 非目标
- 不新增独立的“员工资产信息管理”菜单页面
- 不在资产维护中暴露 `asset_id`
- 不允许用户在前端手工填写 `person_id`
- 不改造现有员工列表查询条件和分页结构
## 现状
当前员工维护能力主要集中在以下位置:
- 前端页面:`ruoyi-ui/src/views/ccdiBaseStaff/index.vue`
- 前端接口:`ruoyi-ui/src/api/ccdiBaseStaff.js`
- 后端控制器:`ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiBaseStaffController.java`
- 后端服务:`ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffServiceImpl.java`
- 导入服务:`ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiBaseStaffImportServiceImpl.java`
现有员工导入采用异步任务 + Redis 状态轮询 + 失败记录分页查询的模式,前端使用本地存储保存最近一次导入任务信息。
## 方案对比
### 方案一:员工页内嵌资产子表聚合维护
- 员工新增、编辑、详情接口返回员工主信息和资产列表聚合结果
- 员工弹窗内部直接维护资产子表
- 资产导入使用独立接口,但交互完全复用员工导入模式
优点:
- 最符合当前需求和现有页面使用习惯
- 用户一次打开员工弹窗即可维护完整信息
- 删除员工时做事务级联最直接
缺点:
- 员工 DTO、VO、Service、前端表单都需要联动调整
### 方案二:资产作为独立模块实现,员工页只嵌入调用
- 后端和前端都按独立资源建设资产模块
- 员工页通过额外接口拉取并嵌入展示
优点:
- 模块边界更清晰
- 未来扩展独立资产菜单更容易
缺点:
- 本次需求明显超出必要范围
- 页面状态和接口交互更复杂
### 方案三:员工页只加资产查看和导入,编辑改为二级弹窗
- 员工弹窗中通过二级弹窗维护资产
优点:
- 对现有员工表单侵入较小
缺点:
- 交互割裂
- 不符合“新增和编辑弹窗中支持添加、编辑、删除”的要求
## 最终方案
采用方案一:在员工信息维护页内嵌资产信息子表,员工接口作为聚合接口返回和保存资产列表;资产导入保持独立入口和独立后端接口,但沿用现有员工导入的上传、异步处理、结果通知、失败记录查询交互。
## 数据模型设计
### 数据表
新增 `ccdi_asset_info` 表,字段来源于 `assets/资产信息表.csv`,并做以下调整:
- `asset_id``BIGINT` 自增主键
- `family_id``VARCHAR(18)`,保存归属员工身份证号,关联 `ccdi_base_staff.id_card`
- `person_id``VARCHAR(18)`,保存资产实际持有人身份证号
- 审计字段沿用当前项目规范,由后端自动填充
建议字段如下:
- `asset_id`
- `family_id`
- `person_id`
- `asset_main_type`
- `asset_sub_type`
- `asset_name`
- `ownership_ratio`
- `purchase_eval_date`
- `original_value`
- `current_value`
- `valuation_date`
- `asset_status`
- `remarks`
- `create_by`
- `create_time`
- `update_by`
- `update_time`
建议索引:
- `idx_family_id(family_id)`
- `idx_person_id(person_id)`
- `idx_asset_main_type(asset_main_type)`
## 后端设计
### 新增资产信息模块对象
`ccdi-info-collection` 中新增:
- `domain/CcdiAssetInfo.java`
- `domain/dto/CcdiAssetInfoDTO.java`
- `domain/vo/CcdiAssetInfoVO.java`
- `domain/excel/CcdiAssetInfoExcel.java`
- `domain/vo/AssetImportFailureVO.java`
- `mapper/CcdiAssetInfoMapper.java`
- `service/ICcdiAssetInfoService.java`
- `service/ICcdiAssetInfoImportService.java`
- `service/impl/CcdiAssetInfoServiceImpl.java`
- `service/impl/CcdiAssetInfoImportServiceImpl.java`
- `controller/CcdiAssetInfoController.java`
- `resources/mapper/info/collection/CcdiAssetInfoMapper.xml`
### 扩展员工聚合接口
扩展现有员工 DTO 和 VO
- `CcdiBaseStaffAddDTO.assetInfoList`
- `CcdiBaseStaffEditDTO.assetInfoList`
- `CcdiBaseStaffVO.assetInfoList`
后端聚合规则:
- 查询员工详情时,按员工 `id_card` 查询 `family_id = 员工身份证号` 的全部资产并组装到 `assetInfoList`
- 新增员工时,先保存员工,再以员工 `id_card` 回填资产列表中的 `family_id`
- 编辑员工时,更新员工主信息,再按员工身份证号重建以 `family_id` 归户的资产列表
- 删除员工时,先按 `family_id` 删除资产再删员工,整个过程置于同一事务内
### 编辑时的资产处理策略
编辑员工时不要求前端传递资产行状态,直接按“当前完整列表”覆盖:
- 获取编辑前员工旧身份证号
- 更新员工主信息
- 如果身份证号变更,按旧身份证号删除 `family_id = 旧身份证号` 的旧资产
- 按当前最新身份证号删除 `family_id = 当前身份证号` 的对应资产
- 当前员工本人资产设置 `family_id = person_id = 当前员工身份证号`
- 当前员工亲属资产设置 `family_id = 当前员工身份证号``person_id = 资产实际持有人身份证号`
- 批量插入最新资产列表
该策略实现简单,能直接覆盖资产新增、编辑、删除三种变化。
### 资产导入接口
新增独立资产导入接口:
- `POST /ccdi/assetInfo/importTemplate`
- `POST /ccdi/assetInfo/importData`
- `GET /ccdi/assetInfo/importStatus/{taskId}`
- `GET /ccdi/assetInfo/importFailures/{taskId}`
导入交互沿用现有员工导入设计:
- 异步任务执行
- Redis 保存任务状态
- 失败记录单独缓存
- 前端轮询状态并显示通知
### 校验规则
#### 员工保存时
- 员工基础信息仍沿用现有校验规则
- `assetInfoList` 中空行自动过滤
- 资产必填项:`personId``assetMainType``assetSubType``assetName``currentValue``assetStatus`
- 数值和日期格式必须合法
- 后端强制回填 `family_id = 员工 id_card`
-`personId = 员工 id_card` 时,视为员工本人资产
-`personId != 员工 id_card` 时,视为员工亲属资产
#### 资产导入时
- 模板中仅要求填写 `person_id`,不要求填写 `family_id`
-`person_id` 能匹配员工 `id_card`,视为员工本人资产,自动填充 `family_id = 该员工 id_card`
-`person_id` 不能匹配员工 `id_card`,则继续匹配员工亲属关系中的亲属身份证号,匹配成功后自动填充对应员工的 `id_card``family_id`
-`person_id` 同时匹配到多个员工家庭,则导入失败,原因标记为“资产归属员工不唯一”
-`person_id` 在员工和员工亲属关系中均无法匹配,则导入失败
- 模板中不包含 `asset_id`
- 允许同一员工导入多条资产
- 失败记录仅返回失败数据,不返回成功数据
## 前端设计
### 列表页
`ruoyi-ui/src/views/ccdiBaseStaff/index.vue` 的按钮区新增:
- “导入资产信息”按钮
- “查看员工资产导入失败记录”按钮
资产导入相关状态全部独立维护:
- 独立弹窗状态
- 独立轮询定时器
- 独立任务 ID
- 独立 localStorage key
- 独立失败记录弹窗和分页状态
### 新增和编辑弹窗
在现有员工弹窗的“基本信息”下方新增“资产信息”分区,采用可编辑子表形式。
子表字段:
- 资产实际持有人身份证号
- 资产大类
- 资产小类
- 资产名称
- 产权占比
- 购买/评估日期
- 资产原值
- 当前估值
- 估值截止日期
- 资产状态
- 备注
- 操作
交互规则:
- 分区右侧提供“新增资产”按钮
- 每行支持删除
- 编辑时回显 `assetInfoList`
- 前端不展示 `asset_id``family_id`
- 前端允许填写 `personId`,用于表示资产实际持有人身份证号
-`personId` 与当前员工身份证号一致时,视为本人资产;不一致时,视为亲属资产
### 详情弹窗
在“员工详情”弹窗中新增“资产信息”区域,使用只读表格展示全部资产。
若无资产数据,显示“暂无资产信息”空状态。
详情表格建议增加:
- `personId`:资产实际持有人身份证号
- `ownerType`:本人 / 亲属
## 数据流
### 员工新增
前端提交员工基础信息和 `assetInfoList`,后端保存员工后按员工 `id_card` 为每条资产回填 `family_id`,再批量保存资产。
### 员工编辑
前端拉取员工详情回显基础信息和资产列表,用户修改后提交完整列表,后端按最新员工身份证号重建 `family_id = 员工身份证号` 的资产明细。
### 员工详情
后端查询员工主信息,再按 `family_id = 员工身份证号` 查询资产列表并一并返回。
### 员工删除
后端先按 `family_id = 员工身份证号` 删除资产,再删除员工主记录。
### 资产导入
前端上传资产 Excel后端根据 `person_id` 自动识别归属员工并回填 `family_id`,异步校验并批量插入资产数据,失败记录通过独立入口查看。
## 异常处理
- 资产导入时,若 `person_id` 无法匹配员工本人或员工亲属,记录失败原因
- 资产导入时,若 `person_id` 对应多个员工家庭,记录归属不唯一失败原因
- 员工编辑时若身份证号变更,必须同步处理旧资产清理和新资产重建
- 员工删除和员工编辑资产重建均使用事务,防止主从数据不一致
- 前端提示文案中明确区分“员工导入”和“员工资产导入”
## 验收标准
- 员工列表页新增“导入资产信息”按钮
- 资产导入失败时显示“查看员工资产导入失败记录”按钮
- 员工新增和编辑弹窗中可添加、编辑、删除资产信息
- 员工详情弹窗中可查看该员工全部资产信息
- 删除员工后,该员工资产信息同步删除
- 资产导入模板不包含 `asset_id`
- 资产导入模板不填写 `family_id`
- 资产导入使用 `person_id` 识别资产实际持有人,并自动回填归属员工的 `family_id`

View File

@@ -1,45 +0,0 @@
# 拉取本行信息日期范围限制设计
## 背景
项目详情页“上传数据”中的“拉取本行信息”弹窗当前允许选择今天及未来日期,这与业务规则不符。用户只能拉取截止到当前日期前一天的数据。
## 目标
- 日期范围组件中禁用今天及未来日期
- 提交时兜底校验,阻止异常方式带入今天或未来日期
- 保持现有接口、文件解析和弹窗结构不变
## 非目标
- 不调整后端接口
- 不修改现有日期范围字段格式
- 不改变“至少输入一个身份证号”和“必须完整选择时间跨度”的现有校验
## 方案对比
### 方案一:仅在日期面板禁选
- 优点:改动最小,交互直观
- 缺点:若后续通过脚本赋值或异常回填带入无效日期,提交时缺少保护
### 方案二:仅在提交时校验
- 优点:实现简单
- 缺点:用户仍然可以在面板中选到无效日期,体验较差
### 方案三:日期面板禁选 + 提交兜底校验
- 优点:同时覆盖交互层和数据层,最稳妥
- 缺点:多一小段前端校验逻辑
## 最终方案
采用方案三。
在前端日期范围选择器上增加 `picker-options.disabledDate`,将本地当前日期零点及之后的日期全部禁用。以 `2026-03-12` 为例,最晚只能选择到 `2026-03-11`
在提交逻辑中新增“最大可选日期”为昨天的兜底校验。如果开始日期或结束日期晚于昨天,则阻止提交并提示“时间跨度最晚只能选择到昨天”。
## 影响范围
- 前端组件:`ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue`
- 前端测试:`ruoyi-ui/tests/unit/`
- 后端:无代码改动,仅保留接口契约不变
## 验收标准
- 日期面板无法选择今天和未来日期
- 通过异常赋值带入今天或未来日期时,提交会被拦截
- 原有弹窗交互和上传相关逻辑无回归

View File

@@ -1,30 +0,0 @@
# 拉取本行信息上传按钮交互区域修复设计
## 背景
“上传信息”页面的“拉取本行信息”弹窗中,上传文件按钮的视觉大小与实际交互区域不一致,造成点击范围异常偏大的体验问题。
## 目标
- 让“选择文件”按钮的可点击区域与视觉边界一致
- 保持拖拽上传弹窗仍为全宽拖拽区域
- 不改动上传逻辑与接口
## 非目标
- 不调整上传流程与校验逻辑
- 不新增后端接口或数据结构
## 方案概述
将对话框内 `el-upload` 的通用“全宽”样式收窄到拖拽上传区域(如 `.upload-area``.batch-upload-area`),避免对按钮型上传产生影响。必要时仅对“拉取本行信息”弹窗维持默认按钮布局。
## 影响范围
- 前端样式:`UploadData.vue``scoped` 样式块
- 组件结构与逻辑保持不变
## 风险与回归验证
- 风险:拖拽上传区域样式变形
- 回归验证:
- 打开“拉取本行信息”弹窗,确认按钮点击区域与视觉一致
- 打开“上传数据/批量上传”弹窗,确认拖拽区域仍为全宽
## 验收标准
- “拉取本行信息”弹窗中按钮交互区域正常
- 拖拽上传弹窗布局无回归

View File

@@ -1,332 +0,0 @@
# 员工亲属资产维护设计
## 背景
现有员工亲属关系维护页面 `http://localhost/maintain/staffFmyRelation` 已支持员工亲属关系的新增、编辑、删除、详情、导入导出,但尚不支持维护亲属名下资产信息。
当前仓库中已有员工资产维护设计文档 [2026-03-12-employee-asset-maintenance-design.md](/D:/ccdi/ccdi/docs/plans/2026-03-12-employee-asset-maintenance-design.md),其核心约束是通过 `family_id` 表示归属员工,通过 `person_id` 表示资产实际持有人。本次需求需要将该能力调整到“员工亲属关系维护页面”中,并明确仅维护亲属资产,不包含员工本人资产。
本次设计于 2026-03-12 确认以下业务口径:
- 维护入口为员工亲属关系页面 `staffFmyRelation`
- 仅维护当前亲属关系对应的亲属资产,不包含员工本人资产
- 不新增独立的亲属资产菜单页面
- 不新增 `relation_id` 等额外资产归属字段
- `family_id` 存员工证件号,对应亲属关系中的 `person_id`
- `person_id` 存亲属证件号,对应亲属关系中的 `relation_cert_no`
- 亲属资产中的 `person_id` 不强制要求为身份证号,存证件号即可
- 编辑亲属关系时,`relationCertType``relationCertNo` 禁止修改
## 目标
- 在员工亲属关系维护页中新增亲属资产维护能力
- 在亲属关系新增/编辑弹窗中支持维护当前亲属名下资产
- 在亲属关系详情弹窗中展示当前亲属全部资产
- 在亲属关系列表页新增亲属资产导入入口,并复用现有异步导入交互
- 删除亲属关系时同步删除该亲属名下资产
## 非目标
- 不维护员工本人资产
- 不新增独立“亲属资产维护”菜单
- 不新增 `relation_id`
- 不在列表页直接展开显示资产明细
- 不改造现有亲属关系查询条件和分页结构
## 现状
当前员工亲属关系维护能力主要集中在以下位置:
- 前端页面:`ruoyi-ui/src/views/ccdiStaffFmyRelation/index.vue`
- 前端接口:`ruoyi-ui/src/api/ccdiStaffFmyRelation.js`
- 后端控制器:`ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiStaffFmyRelationController.java`
- 后端服务:`ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiStaffFmyRelationServiceImpl.java`
- 后端映射:`ccdi-info-collection/src/main/resources/mapper/info/collection/CcdiStaffFmyRelationMapper.xml`
当前一条亲属关系记录的关键字段如下:
- `person_id`:员工证件号
- `relation_cert_type`:亲属证件类型
- `relation_cert_no`:亲属证件号
数据库对 `ccdi_staff_fmy_relation` 设有唯一约束 `uk_person_cert(person_id, relation_cert_no)`,因此一条亲属关系在业务上可以稳定地通过“员工证件号 + 亲属证件号”定位,这也正好可以复用为亲属资产的归属键。
## 方案对比
### 方案一:在亲属关系弹窗内嵌资产子表聚合维护
- 亲属关系详情接口聚合返回 `assetInfoList`
- 亲属关系新增、编辑时同步保存资产列表
- 亲属资产导入采用独立接口,但交互复用现有导入模式
优点:
- 最符合“在亲属关系维护页面维护亲属资产”的使用预期
- 页面上下文最完整,用户不需要在多个弹窗间切换
- 删除亲属关系时级联删除资产最直接
缺点:
- 需要扩展 DTO、VO、Service 和前端弹窗结构
### 方案二:在亲属关系页增加“维护资产”二级弹窗
- 亲属关系页保留现状
- 通过“维护资产”按钮打开独立二级弹窗编辑资产
优点:
- 对现有亲属关系表单侵入较小
缺点:
- 交互割裂
- 用户维护一条亲属关系时无法同时看到关系信息与资产信息
### 方案三:只新增资产查看与导入,不支持页面内编辑
优点:
- 开发量最小
缺点:
- 不满足“添加维护功能”的核心诉求
## 最终方案
采用方案一:在员工亲属关系维护页的新增、编辑、详情弹窗中内嵌“亲属资产信息”分区,由亲属关系接口作为聚合接口返回和保存 `assetInfoList`;同时新增“亲属资产导入”独立接口,并沿用现有亲属关系导入的异步处理与失败记录交互。
## 数据模型设计
### 资产归属规则
亲属资产的归属键定义如下:
- `family_id = 当前亲属关系.person_id`
- `person_id = 当前亲属关系.relation_cert_no`
含义如下:
- `family_id` 表示归属员工
- `person_id` 表示亲属资产实际持有人
- 当前页面只查询和保存满足 `family_id = 员工证件号``person_id = 亲属证件号` 的资产
### 数据表
新增 `ccdi_asset_info` 表,字段来源参考资产设计文档和 `assets/资产信息表.csv`,本次继续沿用以下关键字段:
- `asset_id``BIGINT` 自增主键
- `family_id``VARCHAR(100)`,保存员工证件号
- `person_id``VARCHAR(100)`,保存亲属证件号
- `asset_main_type`
- `asset_sub_type`
- `asset_name`
- `ownership_ratio`
- `purchase_eval_date`
- `original_value`
- `current_value`
- `valuation_date`
- `asset_status`
- `remarks`
- `create_by`
- `create_time`
- `update_by`
- `update_time`
建议索引:
- `idx_family_id(family_id)`
- `idx_person_id(person_id)`
- `idx_family_person(family_id, person_id)`
## 后端设计
### 新增资产模块对象
`ccdi-info-collection` 中新增:
- `domain/CcdiAssetInfo.java`
- `domain/dto/CcdiAssetInfoDTO.java`
- `domain/vo/CcdiAssetInfoVO.java`
- `domain/excel/CcdiAssetInfoExcel.java`
- `domain/vo/AssetImportFailureVO.java`
- `mapper/CcdiAssetInfoMapper.java`
- `service/ICcdiAssetInfoService.java`
- `service/ICcdiAssetInfoImportService.java`
- `service/impl/CcdiAssetInfoServiceImpl.java`
- `service/impl/CcdiAssetInfoImportServiceImpl.java`
- `controller/CcdiAssetInfoController.java`
- `resources/mapper/info/collection/CcdiAssetInfoMapper.xml`
### 扩展亲属关系聚合接口
扩展现有亲属关系 DTO 和 VO
- `CcdiStaffFmyRelationAddDTO.assetInfoList`
- `CcdiStaffFmyRelationEditDTO.assetInfoList`
- `CcdiStaffFmyRelationVO.assetInfoList`
聚合规则:
- 查询详情时,按当前亲属关系的 `personId + relationCertNo` 查询资产列表并组装到 `assetInfoList`
- 新增亲属关系时,先保存关系,再为资产统一回填 `family_id``person_id`
- 编辑亲属关系时,更新关系主信息,再按当前固定归属键重建资产列表
- 删除亲属关系时,先删资产再删亲属关系,整体使用事务
### 编辑时的特殊约束
编辑亲属关系时:
- 前端禁止修改 `relationCertType`
- 前端禁止修改 `relationCertNo`
- 后端读取旧记录后做防御校验
- 若请求中的 `relationCertType``relationCertNo` 与旧值不一致,直接拒绝保存并提示“关系人证件类型/证件号码不允许修改”
### 资产保存策略
编辑当前亲属关系时不要求前端传资产行状态,直接按“当前完整列表覆盖”处理:
- 查询当前亲属关系旧记录
- 校验亲属证件类型、亲属证件号未被修改
- 更新亲属关系主信息
-`family_id = personId``person_id = relationCertNo` 删除旧资产
- 将当前提交的 `assetInfoList` 统一回填归属键后批量插入
由于编辑时禁止修改亲属证件类型和证件号,因此不需要处理资产跨亲属迁移问题。
### 资产导入接口
新增独立资产导入接口:
- `POST /ccdi/assetInfo/importTemplate`
- `POST /ccdi/assetInfo/importData`
- `GET /ccdi/assetInfo/importStatus/{taskId}`
- `GET /ccdi/assetInfo/importFailures/{taskId}`
导入归属规则:
- 模板中 `person_id` 填亲属证件号
- 模板中不要求填写 `family_id`
- 系统通过 `ccdi_staff_fmy_relation.relation_cert_no = person_id` 反查亲属关系
- 若能唯一匹配,则自动回填对应记录的 `person_id``family_id`
- 若找不到匹配关系,导入失败
- 若匹配到多条不同员工关系,导入失败,原因记为“亲属资产归属员工不唯一”
## 前端设计
### 列表页
`ruoyi-ui/src/views/ccdiStaffFmyRelation/index.vue` 按钮区新增:
- “导入亲属资产信息”按钮
- “查看亲属资产导入失败记录”按钮
资产导入状态全部独立维护:
- 独立上传弹窗状态
- 独立轮询定时器
- 独立任务 ID
- 独立 `localStorage key`
- 独立失败记录弹窗和分页状态
### 新增和编辑弹窗
在现有亲属关系弹窗的“基本信息”下方新增“亲属资产信息”分区,采用可编辑子表。
子表字段:
- 资产大类
- 资产小类
- 资产名称
- 产权占比
- 购买/评估日期
- 资产原值
- 当前估值
- 估值截止日期
- 资产状态
- 备注
- 操作
交互规则:
- 分区右侧提供“新增资产”按钮
- 每行支持删除
- 编辑时回显 `assetInfoList`
- 前端不展示 `asset_id``family_id``person_id`
- 新增资产前若当前尚未录入亲属证件类型或证件号码,则禁用新增资产并提示“请先填写关系人证件信息”
- 编辑时 `relationCertType``relationCertNo` 置灰禁用
### 详情弹窗
在详情弹窗中新增“亲属资产信息”区域,使用只读表格展示当前亲属名下资产。
若无资产数据,显示“暂无亲属资产信息”。
## 数据流
### 新增亲属关系
前端提交亲属关系基础信息和 `assetInfoList`,后端保存亲属关系后,按当前记录的 `personId``relationCertNo` 为每条资产回填 `family_id``person_id`,再批量保存。
### 编辑亲属关系
前端拉取详情回显基础信息和资产列表,用户修改后提交完整列表。后端校验证件类型、证件号码未变更后,按当前亲属关系归属键重建资产明细。
### 查询详情
后端查询亲属关系主信息,再按 `family_id = personId``person_id = relationCertNo` 查询资产列表并一并返回。
### 删除亲属关系
后端先删除当前亲属名下资产,再删除亲属关系主记录。
### 导入亲属资产
前端上传亲属资产 Excel后端根据 `person_id = 亲属证件号` 识别归属亲属关系并自动回填 `family_id`,异步校验并批量插入资产数据,失败记录通过独立入口查看。
## 校验规则
### 亲属关系保存时
- 沿用现有亲属关系校验规则
- 编辑时禁止修改 `relationCertType`
- 编辑时禁止修改 `relationCertNo`
- `assetInfoList` 中整行为空的数据自动过滤
### 亲属资产保存时
- 资产必填项:`assetMainType``assetSubType``assetName``currentValue``assetStatus`
- 数值字段必须合法
- 日期字段必须合法
- 后端不信任前端传入的归属字段,统一强制回填:
- `family_id = 当前亲属关系.personId`
- `person_id = 当前亲属关系.relationCertNo`
## 异常处理
- 编辑亲属关系时,若请求试图修改 `relationCertType``relationCertNo`,后端直接报错
- 删除亲属关系与删除资产使用同一事务,避免数据不一致
- 查询详情时若资产查询异常,整体接口返回失败,避免前端收到半残数据
- 亲属资产导入时,若亲属证件号无法匹配亲属关系,记录失败原因
- 亲属资产导入时,若一个亲属证件号匹配多个员工关系,记录“亲属资产归属员工不唯一”
- 前端提示文案中明确区分“亲属关系导入”和“亲属资产导入”
## 验收标准
- `staffFmyRelation` 新增/编辑弹窗中可新增、编辑、删除亲属资产
- 编辑亲属关系时,关系人证件类型和证件号码不可修改
- 详情弹窗中可查看该亲属全部资产信息
- 删除亲属关系后,对应亲属资产同步删除
- 亲属资产支持独立导入,并可查看亲属资产导入失败记录
- 亲属资产导入模板不要求填写 `family_id`
- 系统根据 `person_id = 亲属证件号` 自动回填归属员工证件号到 `family_id`
## 测试建议
- 新增亲属关系但不维护资产
- 新增亲属关系并维护单条、多条资产
- 编辑亲属关系时新增、修改、删除资产
- 验证编辑时不能修改亲属证件类型与证件号码
- 删除亲属关系时资产级联删除
- 亲属资产导入成功、无法匹配、匹配不唯一三类场景

View File

@@ -1,156 +0,0 @@
# CCDI Docker 部署设计
**日期**: 2026-03-13
**目标**: 将当前项目的前端、后端与 `lsfx mock server` 打包后上传到服务器 `116.62.17.81:9444``/volume1/webapp/ccdi`,并使用 Docker 统一部署运行。
## 背景与约束
- 前端对外端口固定为 `62319`
- 后端对外端口固定为 `62318`
- `lsfx mock server` 对外端口固定为 `62320`
- 后端运行时必须使用 Java 21
- 后端运行 profile 固定为 `local`
- 后端继续使用现有 [`application-local.yml`](/D:/ccdi/ccdi/ruoyi-admin/src/main/resources/application-local.yml) 中的 MySQL、Redis 与 `lsfx.api.base-url`
- `lsfx.api.base-url` 当前为 `http://localhost:8000`,希望不改动既有配置
- 服务端部署根目录固定为 `/volume1/webapp/ccdi`
## 方案选择
### 方案一:`mock server` 与后端共用网络命名空间
前端、后端、`mock server` 全部使用 Docker 部署,其中 `lsfx mock server` 通过 `network_mode: "service:backend"` 与后端共享网络命名空间。
优点:
- 不需要修改 `application-local.yml` 中的 `http://localhost:8000`
- 后端容器内访问 `localhost:8000` 时,实际就是同网络命名空间内的 `mock server`
- 对外暴露前端、后端和 `lsfx mock server` 端口,同时仍保持后端对 `localhost:8000` 的兼容访问
缺点:
- Compose 编排方式比普通三容器互联稍特殊
### 方案二:三服务独立组网
后端访问 `http://lsfx-mock-server:8000`
优点:
- Compose 结构最常规
缺点:
- 需要修改现有 `local` 配置,不符合本次要求
### 方案三:本地构建镜像后上传镜像包
优点:
- 服务器上不需要源码级构建
缺点:
- 容易受到本地与服务器架构差异影响
- 镜像体积大,上传与迭代成本高
## 最终方案
采用方案一。
## 部署架构
### 前端
- 本地执行 `npm run build:prod`
- 使用 Nginx 容器托管 `ruoyi-ui/dist`
- Nginx 将 `/prod-api``/v3/api-docs` 反向代理到后端容器 `http://backend:8080`
- Docker 对外暴露 `62319`
### 后端
- 本地执行 `mvn clean package -DskipTests`
- 使用 Java 21 运行 `ruoyi-admin/target/ruoyi-admin.jar`
- 通过环境变量设置:
- `SPRING_PROFILES_ACTIVE=local`
- `RUOYI_PROFILE=/app/data/ruoyi`
- Docker 对外暴露 `62318`
- 同时额外映射 `62320 -> 8000`,让宿主机可直接访问共享网络命名空间中的 `lsfx mock server`
### LSFX Mock Server
- 将现有 FastAPI 实现整理为主仓库正式目录
- 使用 Python 3.11 容器运行
- 默认监听 `8000`
- 通过后端共享网络命名空间,对外暴露 `62320`
- 通过 `network_mode: "service:backend"` 让后端继续使用 `http://localhost:8000`
## 目录规划
服务器目录规划如下:
```text
/volume1/webapp/ccdi/
├── docker-compose.yml
├── .env
├── deploy/
│ ├── deploy.ps1
│ └── remote-deploy.py
├── docker/
│ ├── backend/Dockerfile
│ ├── frontend/Dockerfile
│ ├── frontend/nginx.conf
│ └── mock/Dockerfile
├── backend/
│ └── ruoyi-admin.jar
├── frontend/
│ └── dist/
├── lsfx-mock-server/
└── runtime/
├── ruoyi/
└── logs/
```
## 关键配置设计
### `ruoyi.profile`
当前 [`application-local.yml`](/D:/ccdi/ccdi/ruoyi-admin/src/main/resources/application-local.yml) 未定义 `ruoyi.profile`。后端代码中的 [`RuoYiConfig.java`](/D:/ccdi/ccdi/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java) 依赖该值计算上传、导入与头像目录。
因此在 Docker 运行时通过环境变量补充:
```text
RUOYI_PROFILE=/app/data/ruoyi
```
并挂载到服务器目录,确保容器重启后数据保留。
### 反向代理
前端仍保持生产构建时的 `VUE_APP_BASE_API=/prod-api`避免改动业务代码。Nginx 负责将:
- `/prod-api/` 转发到 `http://backend:8080/`
- `/v3/api-docs/` 转发到 `http://backend:8080/v3/api-docs/`
## 部署流程
1. 本地整理并提交部署文件
2. 本地打包前端与后端产物
3. 本地通过 SSH/SFTP 上传到服务器目标路径
4. 远端执行 `docker compose up -d --build`
5. 验证前端、后端、`mock server` 与代理链路
## 验证点
- `http://116.62.17.81:62319` 可打开前端
- `http://116.62.17.81:62318/swagger-ui/index.html` 可访问后端文档
- `http://116.62.17.81:62320/docs` 可访问 `lsfx mock server` 文档
- 前端登录与接口请求经 `/prod-api` 正常转发
- 后端容器可访问 `http://localhost:8000`
- `mock server` 健康检查正常
## 风险与处理
- 若服务器仅支持 `docker-compose`,部署脚本需兼容 `docker compose``docker-compose`
- 若服务器无法访问 `192.168.0.111` 上的 MySQL/Redis则后端启动会失败本次不改该配置
- 若服务器无 Docker 运行环境,需要先补齐 Docker 与 Compose 插件

View File

@@ -1,110 +0,0 @@
# 一键部署 BAT 入口设计
**日期**: 2026-03-13
**目标**: 在现有 PowerShell 与 Python 部署链路之上,新增一个 Windows 下可直接双击或命令行执行的 `.bat` 入口脚本,用于一键打包前后端并部署到 NAS。
## 背景
当前仓库已经有以下部署能力:
- [`deploy/deploy.ps1`](/D:/ccdi/ccdi/deploy/deploy.ps1):负责本地打包、组装部署目录、上传到 NAS、远端执行 Docker Compose
- [`deploy/remote-deploy.py`](/D:/ccdi/ccdi/deploy/remote-deploy.py):负责 SSH/SFTP 上传与远端 Docker 部署
但 Windows 用户直接使用时仍需要显式调用 PowerShell不够直观。
## 方案选择
### 方案一:薄封装 BAT 入口
新增一个 `deploy/deploy-to-nas.bat`,只做以下几件事:
- 定位仓库根目录
- 调用 PowerShell 执行 `deploy.ps1`
- 提供默认的 NAS 连接参数
- 原样透传退出码
优点:
- 复用现有稳定链路
- 维护成本最低
- 双击和命令行都能使用
缺点:
- 底层仍依赖 PowerShell、Python、Maven、npm
### 方案二:把所有逻辑都改写到 BAT
优点:
- 形式上只有一个入口文件
缺点:
- BAT 对目录处理、错误处理、网络部署支持差
- 可维护性明显下降
### 方案三BAT + 独立配置文件
优点:
- 多环境切换更灵活
缺点:
- 对当前固定 NAS 场景偏重
## 最终方案
采用方案一。
## 设计细节
### 入口脚本
新增 [`deploy/deploy-to-nas.bat`](/D:/ccdi/ccdi/deploy/deploy-to-nas.bat)。
职责:
- 默认使用:
- Host: `116.62.17.81`
- Port: `9444`
- Username: `wkc`
- Password: `wkc@0825`
- RemoteRoot: `/volume1/webapp/ccdi`
- 支持命令行覆盖参数
- 统一调用 `powershell -ExecutionPolicy Bypass -File deploy.ps1`
### 可验证性
为避免每次验证都真的触发完整部署,给 [`deploy/deploy.ps1`](/D:/ccdi/ccdi/deploy/deploy.ps1) 增加一个 `-DryRun` 开关:
- 打印将要使用的目标参数
- 不执行 Maven、npm、上传与远端部署
- 直接返回 `0`
这样 `.bat` 可以配合 `--dry-run` 做快速回归验证。
### 参数约定
BAT 入口参数顺序:
```text
deploy-to-nas.bat [host] [port] [username] [password] [remoteRoot] [--dry-run]
```
如果不传,则使用默认值。
## 验证方式
1. `cmd /c deploy\deploy-to-nas.bat --dry-run`
2. 确认输出中的 NAS 地址、端口、路径与默认值一致
3. 可选:`cmd /c deploy\deploy-to-nas.bat 116.62.17.81 9444 wkc wkc@0825 /volume1/webapp/ccdi --dry-run`
4. 最终运行无 `--dry-run` 的真实部署
## 风险与处理
- 若用户机器禁止 PowerShell 脚本执行BAT 通过 `-ExecutionPolicy Bypass` 绕过当前会话限制
- 若路径中存在空格BAT 需统一用双引号包裹
- 若密码中存在特殊字符BAT 只做原样透传,不自行拼接复杂 shell 表达式

View File

@@ -1,253 +0,0 @@
# 员工资产导入与亲属资产导入拆分设计
## 背景
当前员工信息维护页 [ccdiBaseStaff/index.vue](/D:/ccdi/ccdi/ruoyi-ui/src/views/ccdiBaseStaff/index.vue) 与员工亲属关系维护页 [ccdiStaffFmyRelation/index.vue](/D:/ccdi/ccdi/ruoyi-ui/src/views/ccdiStaffFmyRelation/index.vue) 共用了 `/ccdi/assetInfo/*` 资产导入接口,导致两类业务边界混淆:
- 员工页的“导入资产信息”应只导入员工本人资产
- 亲属页的“导入亲属资产信息”应只导入员工亲属资产
- 当前共享接口无法同时满足这两条规则,模板、权限、失败文案也容易串用
用户已确认:
- 保留员工页“导入资产信息”按钮
- 员工资产导入与亲属资产导入必须彻底拆分
- 员工亲属资产导入功能只能导入员工亲属的资产信息,不能更新员工的
## 目标
- 将员工资产导入与亲属资产导入拆成两条独立链路
- 员工页只支持员工本人资产导入
- 亲属页只支持亲属资产导入
- 模板、接口、权限、失败提示、任务状态缓存全部分离
## 非目标
- 不改造员工资产手工维护功能
- 不改造亲属资产手工维护功能
- 不新增独立资产菜单页面
- 不调整 `ccdi_asset_info` 表结构
## 现状
当前共用资产导入能力集中在:
- 前端 API[ccdiAssetInfo.js](/D:/ccdi/ccdi/ruoyi-ui/src/api/ccdiAssetInfo.js)
- 员工页调用点:[ccdiBaseStaff/index.vue](/D:/ccdi/ccdi/ruoyi-ui/src/views/ccdiBaseStaff/index.vue)
- 亲属页调用点:[ccdiStaffFmyRelation/index.vue](/D:/ccdi/ccdi/ruoyi-ui/src/views/ccdiStaffFmyRelation/index.vue)
- 后端控制器:[CcdiAssetInfoController.java](/D:/ccdi/ccdi/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiAssetInfoController.java)
- 后端导入服务:[CcdiAssetInfoImportServiceImpl.java](/D:/ccdi/ccdi/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiAssetInfoImportServiceImpl.java)
问题点:
- 员工页与亲属页共用同一上传地址 `/ccdi/assetInfo/importData`
- 共用同一模板下载地址 `/ccdi/assetInfo/importTemplate`
- 共用同一任务状态与失败记录查询入口
- 共用同一导入匹配规则,无法表达“员工页仅员工本人、亲属页仅亲属”
## 方案对比
### 方案一:员工资产导入、亲属资产导入完全拆分
- 员工页新增一套独立导入接口
- 亲属页保留现有 `/ccdi/assetInfo/*`
- 两边各自使用独立模板、权限、校验和失败文案
优点:
- 业务边界最清晰
- 后续维护风险最低
- 模板与前端交互不再串用
缺点:
- 需要新增一套员工资产导入 controller/service/api
### 方案二:继续共用接口,通过 `mode` 区分
- 前端调用同一接口
- 后端通过 `mode=employee/family` 分支处理
优点:
- 代码新增较少
缺点:
- 控制器和 service 内分支复杂
- 模板、权限、提示文案仍容易混淆
- 后续扩展时回归风险高
### 方案三:仅修前端入口文案
优点:
- 改动最小
缺点:
- 业务问题未解决
- 实际导入规则仍然混乱
## 最终方案
采用方案一:员工资产导入与亲属资产导入完全拆分。
### 员工资产导入
- 页面入口:员工信息维护页
- 独立接口:
- `POST /ccdi/baseStaff/asset/importTemplate`
- `POST /ccdi/baseStaff/asset/importData`
- `GET /ccdi/baseStaff/asset/importStatus/{taskId}`
- `GET /ccdi/baseStaff/asset/importFailures/{taskId}`
- 独立前端 API 文件:`ruoyi-ui/src/api/ccdiBaseStaffAsset.js`
- 仅允许导入员工本人资产
- 模板第一列要求填写员工身份证号
- 导入成功后强制写入:
- `family_id = 员工身份证号`
- `person_id = 员工身份证号`
### 亲属资产导入
- 页面入口:员工亲属关系维护页
- 保留现有接口:
- `POST /ccdi/assetInfo/importTemplate`
- `POST /ccdi/assetInfo/importData`
- `GET /ccdi/assetInfo/importStatus/{taskId}`
- `GET /ccdi/assetInfo/importFailures/{taskId}`
- 仅允许导入员工亲属资产
- 模板第一列要求填写亲属证件号
- 导入成功后强制写入:
- `family_id = 关联员工证件号`
- `person_id = 亲属证件号`
## 后端设计
### 新增员工资产导入控制面
新增:
- `controller/CcdiBaseStaffAssetImportController.java`
- `service/ICcdiBaseStaffAssetImportService.java`
- `service/impl/CcdiBaseStaffAssetImportServiceImpl.java`
- `domain/excel/CcdiBaseStaffAssetInfoExcel.java`
- `domain/vo/BaseStaffAssetImportFailureVO.java`
员工资产导入匹配规则:
- 仅根据 `ccdi_base_staff.id_card` 匹配
- 若未匹配到员工,导入失败
- 不再兜底匹配亲属关系表
- 若命中员工,则写入 `family_id = person_id = id_card`
亲属资产导入规则调整:
- [CcdiAssetInfoImportServiceImpl.java](/D:/ccdi/ccdi/ccdi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiAssetInfoImportServiceImpl.java) 只保留亲属资产逻辑
- 仅根据 `ccdi_staff_fmy_relation.relation_cert_no` 匹配
- 不再匹配员工本人身份证号
### 权限设计
- 员工资产导入接口:`ccdi:employee:import`
- 亲属资产导入接口:`ccdi:staffFmyRelation:import`
禁止再使用同时兼容两个权限的写法,以免接口语义再次混淆。
## 前端设计
### 员工页
[ccdiBaseStaff/index.vue](/D:/ccdi/ccdi/ruoyi-ui/src/views/ccdiBaseStaff/index.vue) 改为:
- 上传地址改为员工资产专用接口
- 模板下载改为员工资产模板
- 任务状态和失败记录查询改为员工资产专用接口
- 提示文案明确为“员工资产数据导入”
### 亲属页
[ccdiStaffFmyRelation/index.vue](/D:/ccdi/ccdi/ruoyi-ui/src/views/ccdiStaffFmyRelation/index.vue) 保持:
- 上传地址仍为 `/ccdi/assetInfo/importData`
- 模板下载仍为亲属资产模板
- 文案继续强调“亲属资产”
## 模板设计
员工资产模板:
- 文件名:`员工资产信息模板_xxx.xlsx`
- 首列表头:`员工身份证号*`
亲属资产模板:
- 文件名:`亲属资产信息模板_xxx.xlsx`
- 首列表头:`亲属证件号*`
## 校验规则
### 员工资产导入
- 员工身份证号不能为空
- 资产必填字段不能为空
- 员工身份证号必须存在于 `ccdi_base_staff.id_card`
- 若填写的是亲属证件号或其他未命中的证件号,直接失败
建议失败文案:
- `未找到员工资产归属员工`
- 或更明确的 `员工资产导入仅支持员工本人证件号`
### 亲属资产导入
- 亲属证件号不能为空
- 资产必填字段不能为空
- 亲属证件号必须存在于 `ccdi_staff_fmy_relation.relation_cert_no`
- 若填写员工本人身份证号且不存在亲属关系映射,直接失败
- 若同一亲属证件号匹配多个员工关系,失败并提示归属不唯一
## 测试要求
后端:
- 员工资产导入:员工本人证件号成功
- 员工资产导入:亲属证件号失败
- 亲属资产导入:亲属证件号成功
- 亲属资产导入:员工本人证件号失败
- 两套模板标题和首列表头不同
- 两套接口权限分别正确
前端:
- 员工页使用员工资产专用上传地址
- 亲属页继续使用 `/ccdi/assetInfo/*`
- 员工页下载员工资产模板
- 亲属页下载亲属资产模板
- 员工页和亲属页的失败记录弹窗文案不混淆
## 风险与回滚
风险:
- 当前仓库内已有共享资产导入代码,拆分时容易遗漏某一处调用
- 若只拆后端不拆前端,页面会继续指向旧接口
- 若模板文案未同步,用户仍可能误用模板
回滚策略:
- 独立提交员工资产导入新增改动
- 独立提交亲属资产导入收敛改动
- 任一阶段出现回归,可按提交粒度回退
## 实现状态
- 2026-03-13 已完成后端拆分实现
- 已新增员工资产独立导入接口 `/ccdi/baseStaff/asset/*`
- 已将 `/ccdi/assetInfo/*` 收敛为亲属资产专用接口
- 已通过后端定向测试验证员工与亲属两套导入链路、模板名称和失败文案拆分生效
- 2026-03-13 已完成前端拆分实现
- 员工页已切换为员工资产专用前端 API `ruoyi-ui/src/api/ccdiBaseStaffAsset.js`
- 员工页上传地址、模板下载、状态轮询与失败记录查询已全部改为 `/ccdi/baseStaff/asset/*`
- 亲属页继续保持 `/ccdi/assetInfo/*` 导入链路与“亲属资产”提示文案
- 已通过 4 个前端静态契约测试验证员工页与亲属页导入交互隔离生效

View File

@@ -1,69 +0,0 @@
# Project 40 Large Transaction Test Data Design
**背景**
`project_id=40` 对应项目为“大额交易模型测试”,当前 `ccdi_bank_statement` 中没有任何流水数据。目标是根据 [assets/大额交易.csv](/D:/ccdi/ccdi/assets/大额交易.csv) 的业务口径,在开发库中直接插入一批能够稳定命中大额交易模型指标的测试流水。
**现状**
- 项目 `40` 已存在,但 `ccdi_model_param` 中没有项目级参数,命中逻辑将使用 `project_id=0` 的系统默认阈值。
- 可复用的测试身份已存在:
- 员工 `模型测试员工 / 330101198801010011`
- 家属 `模型测试家属 / 330101199001010022`
- 员工 `模型二测试员工 / 330101198802020033`
- 家属 `模型二测试家属 / 330101199202020044`
- 流水命中依赖的核心字段为 `project_id``cret_no``trx_date``amount_dr``amount_cr``user_memo``customer_account_name``cash_type``le_account_name``le_account_no``accounting_date_id`
**设计目标**
-`大额交易.csv` 中的每个指标至少生成一组稳定命中的流水。
- 直接写入开发库 `ccdi_bank_statement`,只影响 `project_id=40`
- 除命中流水外,补充少量普通流水,避免页面展示只有极端数据。
- 插入后可以使用 `大额交易.csv` 中的 SQL 口径逐项核验。
**设计方案**
采用“指标命中 + 少量真实噪声”的平衡方案:
1. 为以下指标分别构造命中流水:
- 房车消费支出
- 税务支出
- 单笔大额收入
- 累计收入超限
- 年流水交易额超限
- 单笔大额存现
- 单日多次存现
- 单笔大额转账
2. 每个指标使用明显高于阈值的金额,避免边界值误差。
3. 复用两名员工和两名家属的证件号作为 `cret_no`,确保能命中员工本人及亲属范围。
4. 为每个账户生成一组连续日期和递增的 `accounting_date_id`,规避唯一键 `(project_id, LE_ACCOUNT_NO, ACCOUNTING_DATE_ID, AMOUNT_DR, AMOUNT_CR)` 冲突。
5. 补充少量普通转入、普通消费、工资代发等非命中流水,保证页面可读性,并验证排除逻辑。
**数据策略**
- 单笔大额收入:为员工本人生成大于 `100000` 的单笔收入,对手方不使用本人、家属和工资代发主体。
- 累计收入超限:为同一员工和同一外部对手方生成多笔累计收入,总额超过 `50000001`
- 年流水交易额超限:在近一年内为同一员工生成大量转入与转出组合,使总交易额超过 `50000001`
- 单笔大额存现:生成 `amount_cr > 2000001` 且摘要/交易类型命中现金存入关键词的流水。
- 单日多次存现:同一身份证同一天生成至少 `6` 笔满足大额存现条件的流水。
- 单笔大额转账:生成 `amount_dr > 100001``user_memo` / `cash_type` 命中转账关键词、同时避开“款”字排除条件的流水。
- 房车消费与税务支出:通过 `user_memo``customer_account_name` 命中关键词,且使用员工本人和家属证件号混合覆盖。
**落库方式**
- 新增一份独立 SQL 脚本,包含:
- 插入前清理 `project_id=40` 既有测试流水
- 批量插入设计好的流水数据
- 插入后的核验 SQL
- 落库只操作 `ccdi_bank_statement`,不修改员工、家属、项目、参数等基础表。
**验证方式**
- 校验 `project_id=40` 的流水总数和涉及证件号是否符合预期。
-`大额交易.csv` 的 SQL 口径逐项运行命中检查。
- 抽样查看流水明细中的摘要、对手方、金额、日期,确认页面展示可用。
**风险与控制**
- 若模型实现与 CSV 口径存在偏差可能出现“SQL 能命中、页面不命中”的情况;因此落库后需要按数据库口径与页面结果双重验证。
- 由于 `project_id=40` 使用默认阈值,后续若产品调整项目级参数,当前测试数据可能不再命中;为避免误解,插入脚本会在注释中写明依赖的默认阈值。

View File

@@ -1,199 +0,0 @@
# 模型默认参数 CSV 对齐设计
## 背景
当前模型参数配置页面已经支持按模型卡片垂直展示并统一保存,但系统默认参数的实际定义与 `assets/模型默认参数.csv` 不一致,主要体现在模型数量、参数数量、参数编码、名称、描述、默认值和单位上。
本次需要将系统默认参数、后端查询保存链路和前端展示统一对齐到 CSV并明确兼容边界。
## 目标
- 让系统默认参数数据与 `assets/模型默认参数.csv` 保持一致
- 前端页面完全根据查询接口动态展示所有模型和参数信息
- 保持现有 `listAll/saveAll` 接口契约不变
- 保持默认配置项目和自定义配置项目的既有行为清晰可控
## 非目标
- 不补齐历史 `config_type = custom` 项目缺失的模型或参数
- 不调整 `ccdi_model_param` 表结构
- 不增加前端本地写死的模型定义
- 不增加任何千分位相关展示、输入或保存逻辑
## 现状分析
### 现有页面
当前前端页面已经具备以下能力:
- 全局模型参数页支持按模型卡片展示全部参数
- 项目参数页支持按模型卡片展示全部参数
- 两个页面均通过 `GET /ccdi/modelParam/listAll` 动态拉取模型参数
- 两个页面均通过 `POST /ccdi/modelParam/saveAll` 统一保存修改
这意味着本次不需要推翻页面结构,重点应放在数据定义对齐和动态渲染稳定性上。
### 现有后端
当前后端服务已经具备以下能力:
- 根据 `projectId` 和项目 `configType` 决定查询系统默认参数或项目自定义参数
- 默认配置项目首次保存时,会把系统默认参数复制到项目下,并切换为 `custom`
- 批量查询和批量保存接口已可复用
### 差异点
与 CSV 对比后,当前系统存在以下差异:
- 缺少 `ABNORMAL_BEHAVIOR``SUSPICIOUS_GAMBLING` 两个模型
- 部分旧参数在 CSV 中已被替换或删除
- 多个参数的 `paramCode``paramName``paramDesc``paramValue``paramUnit` 已发生变化
- 初始化 SQL 与已有数据库环境更新脚本尚未完全统一
## 方案对比
### 方案一:以数据库默认参数为唯一真实来源
- 优点:前后端天然一致,默认项目复制逻辑无需重写,风险最低
- 优点:新增或删除模型参数后,前端可自动跟随接口展示
- 缺点:需要认真维护初始化脚本和增量更新脚本
### 方案二:前端按 CSV 写死模型定义,后端只保存值
- 优点:页面改造直观
- 缺点:前后端各维护一份模型定义,后续极易漂移
- 缺点:一旦后端参数集合变化,前端会出现展示与保存不一致
### 方案三:后端代码内置元数据,数据库只存参数值
- 优点:元数据集中管理
- 缺点:需要重构当前基于表驱动的实现方式,改动范围过大
- 缺点:对已有项目参数复制链路影响较大
## 最终方案
采用方案一,以 `ccdi_model_param``project_id = 0` 的系统默认参数作为唯一真实来源。
### 数据策略
- 更新 `sql/ccdi_model_param.sql`,使新环境初始化时直接生成与 CSV 一致的模型参数数据
- 保留并完善 `sql/2026-03-16-update-ccdi-model-param-defaults.sql`,用于已有环境覆盖系统默认参数
- 系统默认参数集合以 CSV 为准,共包含 5 个模型、16 个参数
- 历史 `config_type = custom` 项目不补齐新增模型或参数
### 查询策略
- `projectId = 0` 时,查询系统默认参数
- `projectId > 0 && configType = default` 时,仍查询系统默认参数
- `projectId > 0 && configType = custom` 时,查询项目自己的参数
- 查询接口继续返回 `models` 数组,前端完全依赖接口返回数据动态渲染
### 保存策略
- 全局参数保存仍更新 `project_id = 0` 的系统默认参数
- 默认配置项目首次保存时,复制当前系统默认参数全集到项目,再切换项目 `configType``custom`
- 已经是 `custom` 的历史项目继续只更新自身已有参数,不做补齐
- 参数值继续按原始字符串处理,不增加千分位格式化、去逗号或自动转换逻辑
## 后端设计
### 保持接口契约不变
继续使用现有接口:
- `GET /ccdi/modelParam/listAll`
- `POST /ccdi/modelParam/saveAll`
这样可以避免额外改动前端请求层和项目参数页调用方式。
### 服务层调整点
- 保持 `CcdiModelParamServiceImpl` 现有按 `configType` 切换数据源的逻辑
- 保持默认项目首次保存时复制系统默认参数全集的逻辑
- 保持空值校验,防止参数值被保存为空字符串
- 不新增历史 `custom` 项目的补齐逻辑
### Mapper 与 SQL 调整点
- `selectByProjectId` 查询顺序需要稳定,建议按 `model_code``sort_order``id` 排序
- 初始化 SQL 和增量 SQL 的模型定义必须一致,避免新库与老库表现不同
## 前端设计
### 展示原则
前端不写死任何模型或参数定义,完全根据查询接口返回的数据展示:
- 模型标题使用 `modelName`
- 模型编码使用 `modelCode`
- 参数名称使用 `paramName`
- 参数描述使用 `paramDesc`
- 参数值使用 `paramValue`
- 参数单位使用 `paramUnit`
### 页面行为
- 保留现有“模型卡片垂直堆叠 + 统一保存”布局
- 不限制模型数量和参数数量
- 不假设固定模型顺序和参数顺序,以接口返回顺序为准
- 保存成功后重新查询,保证页面展示与后端数据一致
### 修改记录实现
当前页面中对修改项的记录依赖 `Set + $forceUpdate`。本次建议改为更稳定的响应式结构,例如:
-`modelCode:paramCode` 作为唯一键
- 使用普通对象或数组维护已修改项
这样可以减少 Vue 2 对 `Set` 响应式不完整带来的不稳定行为。
## 影响范围
### 后端
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java`
- `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiModelParamMapper.xml`
- `sql/ccdi_model_param.sql`
- `sql/2026-03-16-update-ccdi-model-param-defaults.sql`
### 前端
- `ruoyi-ui/src/views/ccdi/modelParam/index.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue`
## 测试方案
### 数据对齐测试
- 校验系统默认参数与 CSV 中的模型数量一致
- 校验每个模型下的参数数量、编码、名称、描述、默认值和单位一致
- 校验新环境初始化 SQL 和老环境增量 SQL 产出的系统默认参数一致
### 功能测试
- 全局模型参数页可动态展示所有模型参数
- 项目参数页可动态展示所有模型参数
- 全局参数修改后可保存成功
- 默认配置项目读取系统默认参数
- 默认配置项目首次保存后切换为 `custom`
- 历史 `custom` 项目不补新增参数,且页面只展示其自身已有参数
### 回归测试
- 现有 `listAll/saveAll` 接口可继续使用
- 页面不再引入任何千分位相关逻辑
- 保存后页面重新加载正常,修改提示正常
## 验收标准
- 系统默认参数与 `assets/模型默认参数.csv` 完全一致
- 全局参数页和项目参数页均根据查询接口动态展示所有模型信息
- 默认配置项目的读取与首次保存行为正确
- 历史 `custom` 项目不被补齐、不受新增默认参数影响
- 前后端不存在千分位相关功能设计和实现
---
**设计日期:** 2026-03-16
**设计人员:** Codex
**审核状态:** 已确认

View File

@@ -1,72 +0,0 @@
# 模型参数页底部保存栏悬浮设计
**背景**
当前“模型参数修改”相关页面中的“保存所有修改”按钮位于普通文档流末尾。参数卡片较多时,用户需要滚动到页面底部才能看到保存入口,保存操作不够直观,也容易在编辑过程中误以为没有统一保存入口。
**目标**
将“保存所有修改”区域调整为在页面滚动过程中始终保持可见的底部操作栏,覆盖以下页面:
- 项目详情中的参数配置页 `ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue`
- 全局模型参数管理页 `ruoyi-ui/src/views/ccdi/modelParam/index.vue`
**约束**
- 不修改参数加载、修改标记、批量保存等现有业务逻辑。
- 保持两个页面交互一致,避免同类页面体验割裂。
- 适配当前若依布局中 `AppMain` 作为滚动容器的结构。
- 兼顾桌面端与移动端显示,避免按钮区压住主要内容。
**方案选择**
## 方案一:使用 `position: sticky; bottom: 0`
将保存栏保留在页面内容流中,但通过 `sticky` 吸附在滚动容器底部。
优点:
- 与当前若依布局兼容性最好。
- 不需要手动计算侧边栏宽度、主内容区宽度和响应式偏移。
- 实际用户体验上可达到“滚动时始终可见”的效果。
缺点:
- 严格意义上是吸附在内容滚动容器底部,而不是直接固定到 `window`
## 方案二:使用 `position: fixed`
将保存栏直接固定到浏览器视口底部。
优点:
- 语义上最接近“固定在浏览器底部”。
缺点:
- 需要额外处理左侧菜单宽度、固定头部、移动端安全区域和页面宽度同步。
- 在若依当前布局下更容易出现遮挡或错位。
**结论**
采用方案一。对于当前项目的页面结构,这种实现可以稳定地实现底部悬浮效果,同时将实现复杂度和回归风险控制在最低。
**界面设计**
- 保留“保存所有修改”按钮和“已修改 X 个参数”文案。
- 将按钮区改造成统一的底部操作栏。
- 操作栏使用白底、顶部边框和轻阴影,与卡片区分层。
- 桌面端使用左右分布或同一行布局。
- 移动端允许换行,确保按钮点击区域充足。
**内容区域处理**
- 页面主体增加底部留白,避免最后一张模型卡片或表格内容在滚动时被吸附栏遮挡。
- 空状态页面不显示底部保存栏。
**测试与验证**
- 验证项目详情参数配置页在多组参数卡片下滚动时保存栏持续可见。
- 验证全局模型参数页在多组参数卡片下滚动时保存栏持续可见。
- 验证移动端宽度下保存按钮与“已修改 X 个参数”文案不重叠。
- 验证未修改参数、已修改参数、保存中三种状态下按钮区表现正常。

View File

@@ -1,566 +0,0 @@
# 项目流水标签功能设计
## 背景
当前项目已经具备两条银行流水入库链路:
- 批量上传他行流水文件:`/ccdi/file-upload/batch`
- 拉取本行信息:`/ccdi/file-upload/pull-bank-info`
两条链路最终都会把流水写入 `ccdi_bank_statement`,但系统还没有对项目内流水执行统一打标,也没有独立的手动重算能力。
本次需求要求:
- 在异步批量上传流水文件全部处理完成后,自动对项目内流水进行打标
- 在拉取本行信息任务全部处理完成后,也自动对项目内流水进行打标
- 提供手动接口,支持重新计算项目内标签
- 根据 [assets/大额交易.csv](../../assets/大额交易.csv) 中的模型信息实现第一批规则
- 避免重复打标
- 同时支持两类结果:
- 单条流水异常:对具体流水打标
- 对象区间异常:对项目中的对象打标
- 打标结果中保留足够的异常原因信息,便于后续页面清晰展示
## 目标
- 为项目建立可扩展的流水标签能力,而不是只为“大额交易”写一次性逻辑
- 支持自动触发和手动触发两种标签重算方式
- 用代码中的 Mapper XML 固化技术口径 SQL不做运行时 SQL 配置化
- 通过统一结果表保存标签命中结果和异常原因快照
- 保证同一项目不会同时触发多个重算任务
- 使用线程池并行执行规则,提高重算效率
## 非目标
- 本期不新增前端页面展示
- 本期不实现动态 SQL 配置平台
- 本期不把标签计算失败与流水导入失败绑定为同一事务
- 本期不实现除大额交易外的其他模型
- 本期不实现对象级结果到具体来源批次的多来源追溯
## 现状分析
### 流水导入现状
批量上传主链路位于:
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
当前关键行为:
- 每个文件独立异步上传、轮询解析、拉取流水、写入 `ccdi_bank_statement`
- 文件全部提交后,调度线程立即结束
- 没有“当前批次所有异步任务完成后的收尾回调”
拉取本行信息主链路同样位于 `CcdiFileUploadServiceImpl`,每个身份证号会生成一条上传记录并异步拉取流水,但也没有“全部任务完成后”的统一收尾逻辑。
### 参数配置现状
模型参数位于:
- `ccdi_model_param`
- `CcdiModelParamServiceImpl`
系统已经支持:
- 项目默认参数与自定义参数切换
- 通过 `project_id = 0` 读取系统默认参数
- 通过 `project_id = 当前项目` 读取项目自定义参数
因此阈值类规则可以直接复用现有参数体系。
### 流水查询现状
流水明细能力位于:
- `CcdiBankStatementMapper.xml`
- `CcdiBankStatementController`
- `CcdiBankStatementServiceImpl`
当前流水表中已经具备标签计算所需的一部分字段:
- `project_id`
- `group_id`
- `batch_id`,其业务语义可作为 `log_id`
- `bank_statement_id`
- `cret_no`
## 方案对比
### 方案一:把每个规则直接写死在 Service 中
优点:
- 开发速度快
- 初期实现简单
缺点:
- 第二个模型接入时会迅速膨胀为大段 `if/else`
- SQL、规则元数据、结果写入逻辑会散落在多个类里
- 不利于后续维护和测试
### 方案二规则元数据入库SQL 写在 Mapper XML 中
优点:
- 规则展示信息可配置
- 技术口径 SQL 保持在代码中,便于评审和测试
- 后续新增模型的扩展路径清晰
缺点:
- 第一版需要同时补规则表、结果表、任务表和执行框架
### 方案三:规则元数据和 SQL 都配置化
优点:
- 表面上扩展灵活
缺点:
- 动态 SQL 的安全性、可测试性、可维护性都较差
- 与当前项目代码式 MyBatis 规则体系不一致
- 不符合本次“SQL 用 Mapper + XML 实现”的约束
## 最终方案
采用方案二:
- 规则元数据入库
- 技术口径 SQL 固化在 `Mapper + XML`
- 统一结果表保存标签命中
- 统一任务调度器管理自动触发、手动触发和项目级互斥
- 规则级任务在线程池中并行执行
## 详细设计
### 一、数据模型设计
#### 1. 标签规则定义表
建议新增:`ccdi_bank_tag_rule`
建议字段:
- `id`
- `model_code`
- `model_name`
- `rule_code`
- `rule_name`
- `indicator_code`
- `result_type``STATEMENT` / `OBJECT`
- `risk_level`
- `business_caliber`
- `enabled`
- `sort_order`
- `create_by`
- `create_time`
- `update_by`
- `update_time`
- `remark`
用途:
- 保存展示和业务元数据
- 不保存技术 SQL
- 作为结果表中的规则元信息来源
第一版初始化“大额交易” 8 条规则元数据:
- 房车消费支出交易
- 税务支出交易
- 大额单笔收入
- 累计收入超限
- 年流水交易额超限
- 大额存现交易
- 短时间多次存现
- 大额转账交易
#### 2. 标签结果表
建议新增:`ccdi_bank_statement_tag_result`
建议字段:
- `id`
- `project_id`
- `model_code`
- `model_name`
- `rule_code`
- `rule_name`
- `indicator_code`
- `result_type`
- `risk_level`
- `bank_statement_id`
- `object_type`
- `object_key`
- `group_id`
- `log_id`
- `reason_detail`
- `business_caliber_snapshot`
- `hit_value_snapshot`
- `create_by`
- `create_time`
- `update_by`
- `update_time`
- `remark`
字段约束:
- 流水级结果必须保存:
- `bank_statement_id`
- `group_id`
- `log_id`
- 对象级结果必须保存:
- `object_type`
- `object_key`
- 对象级结果不写:
- `group_id`
- `log_id`
- `bank_statement_id`
唯一约束建议拆分为两组:
- 流水级唯一键:`project_id + rule_code + bank_statement_id`
- 对象级唯一键:`project_id + rule_code + object_type + object_key`
用途:
- 防止重复打标
- 保留足够的异常原因快照,供后续页面展示
#### 3. 标签任务执行表
建议新增:`ccdi_bank_tag_task`
建议字段:
- `id`
- `project_id`
- `trigger_type``AUTO_BATCH_UPLOAD` / `AUTO_PULL_BANK_INFO` / `MANUAL`
- `model_code`
- `status``RUNNING` / `SUCCESS` / `PARTIAL_FAILED` / `FAILED`
- `need_rerun`
- `success_rule_count`
- `failed_rule_count`
- `hit_count`
- `error_message`
- `start_time`
- `end_time`
- `create_by`
- `create_time`
- `update_by`
- `update_time`
用途:
- 记录当前项目是否存在正在执行的重算任务
- 承接“自动触发补跑”标记
- 为后续前端展示最近一次打标状态预留基础
### 二、规则分型设计
第一版大额交易 8 条规则按结果类型分为两类。
#### 1. 流水级规则
返回具体流水 `bank_statement_id`,并从流水表带出 `group_id``batch_id(log_id)`
包括:
- 房车消费支出交易
- 税务支出交易
- 大额单笔收入
- 大额存现交易
- 大额转账交易
#### 2. 对象级规则
返回对象主键和原因摘要,不绑定单条流水。
包括:
- 累计收入超限
- 年流水交易额超限
- 短时间多次存现
对象级结果建议使用:
- `object_type = STAFF_ID_CARD`
- `object_key = 身份证号`
后续如果出现亲属、账号、企业等对象级命中,可以继续扩展 `object_type`
### 三、技术实现设计
#### 1. SQL 落点
所有规则 SQL 均通过 `Mapper + XML` 实现。
建议新增:
- `CcdiBankTagRuleMapper`
- `CcdiBankTagResultMapper`
- `CcdiBankTagTaskMapper`
- `CcdiBankTagAnalysisMapper`
- 对应 XML
其中规则筛选 Mapper 方法按规则拆分,例如:
- `selectHouseOrCarExpenseStatements`
- `selectTaxExpenseStatements`
- `selectSingleLargeIncomeStatements`
- `selectCumulativeIncomeObjects`
- `selectAnnualTurnoverObjects`
- `selectLargeCashDepositStatements`
- `selectFrequentCashDepositObjects`
- `selectLargeTransferStatements`
#### 2. 参数读取
阈值类规则通过现有 `ccdi_model_param` 获取项目当前有效参数:
- 若项目 `config_type = default`,读取 `project_id = 0`
- 若项目 `config_type = custom`,读取当前 `project_id`
第一版涉及参数包括:
- `SINGLE_TRANSACTION_AMOUNT`
- `CUMULATIVE_TRANSACTION_AMOUNT`
- `annual_turnover`
- `LARGE_CASH_DEPOSIT`
- `FREQUENT_CASH_DEPOSIT`
- `FREQUENT_TRANSFER`
#### 3. 结果写入
重算开始前,先按范围删除旧结果:
- 手动全量:删除当前项目全部结果
- 手动按模型:删除当前项目指定模型结果
- 自动触发:按当前项目全部结果重算,保持结果与最新流水一致
删除完成后,再按规则并行写入结果。
每条规则线程内部只做:
- 查询命中结果
- 构建结果实体
- 批量插入结果表
规则线程内部不允许再删旧结果,避免并发线程相互覆盖。
### 四、触发流程设计
#### 1. 批量上传自动触发
触发入口:
- `CcdiFileUploadServiceImpl.batchUploadFiles`
改造方向:
- 当前 `submitTasksAsync` 需要改为收集每个文件的 `CompletableFuture`
- 使用 `CompletableFuture.allOf(...)` 在所有文件处理完成后统一执行批次收尾
- 若本批次至少有一份文件成功落库流水,则申请一次项目级标签重算
#### 2. 拉取本行信息自动触发
触发入口:
- `CcdiFileUploadServiceImpl.submitPullBankInfo`
改造方向:
- 当前 `submitPullBankInfoTasks` 同样收集每个身份证任务的 `CompletableFuture`
- 所有任务处理完成后统一执行批次收尾
- 若本批次至少有一条成功入库流水,则申请一次项目级标签重算
#### 3. 手动触发
新增接口:
- `POST /ccdi/project/tags/rebuild`
请求体:
- `projectId` 必填
- `modelCode` 选填
规则:
- 未传 `modelCode`:重算当前项目全部模型
-`modelCode`:只重算指定模型
### 五、并发控制设计
#### 1. 项目级互斥
要求:
- 同一项目任意时刻只允许一个标签重算任务运行
实现建议:
- 新增 `ProjectTagRebuildCoordinator`
-`projectId` 作为互斥粒度
- 结合任务表 `RUNNING` 状态和进程内互斥结构控制
行为规则:
- 自动触发时若已有任务在运行:
- 不启动新的重算
- 仅把当前运行任务标记为 `need_rerun = true`
- 手动触发时若已有任务在运行:
- 直接返回“当前项目标签正在重算中,请稍后再试”
#### 2. 自动补跑
若自动触发期间项目已有重算任务在运行:
- 当前任务结束后检查 `need_rerun`
- 若为 `true`,自动再执行一轮项目级全量重算
- 补跑结束后清空 `need_rerun`
这样可以同时满足:
- 防止并发重算
- 不丢失导入完成后的自动重算机会
### 六、线程池设计
采用两层调度模型。
#### 1. 项目级任务
每个项目的重算由一个总任务负责:
- 控制任务生命周期
- 统一删旧结果
- 提交规则级任务
- 汇总执行结果
#### 2. 规则级任务
每条规则使用线程池并行处理。
建议新增独立线程池:
- `tagRuleExecutor`
规则级线程任务职责:
- 读取规则元数据
- 查询阈值参数
- 执行 Mapper SQL
- 批量写入结果表
- 返回命中数和执行状态
优点:
- 多条规则之间互不阻塞
- 后续新增模型时可复用同一套并行框架
### 七、失败处理设计
#### 1. 自动触发失败
- 不影响流水已成功入库的数据
- 标签任务状态标记为 `FAILED``PARTIAL_FAILED`
- 记录失败规则和错误信息
- 不回滚已经成功写入的流水
#### 2. 手动触发失败
- 接口直接返回失败
- 任务状态记录失败原因
- 已成功完成的规则结果保留
#### 3. 单规则失败
- 不中断其他规则执行
- 整个任务按“部分失败”汇总
### 八、异常原因快照设计
结果表中的 `reason_detail` 必须能直接解释标签来源。
建议格式:
- 流水级:
- `摘要命中“购买房产首付款”,对手方“杭州贝壳房地产经纪有限公司”,支出金额 680000.00 元`
- 对象级:
- `同一交易对手累计流入 60300000.00 元,超过阈值 50000001.00 元,对手方:浙江远望贸易有限公司`
额外保存:
- `business_caliber_snapshot`
- `hit_value_snapshot`
用于后续页面直接展示“规则口径 + 命中值”。
## 接口设计
### 1. 手动重算接口
- 路径:`POST /ccdi/project/tags/rebuild`
- 请求体:
- `projectId`
- `modelCode` 可选
- 返回:
- 成功:`AjaxResult.success("标签重算任务已提交")`
- 失败:`AjaxResult.error("当前项目标签正在重算中,请稍后再试")`
### 2. 结果查询接口
本期不做前端展示,结果查询接口可暂缓;但后续建议预留:
- 项目标签汇总查询
- 项目标签明细查询
- 项目标签任务状态查询
## 测试设计
后端至少覆盖以下场景:
- 批量上传所有文件完成后会申请一次项目级重算
- 拉取本行信息所有任务完成后会申请一次项目级重算
- 手动接口支持全量重算
- 手动接口支持按模型重算
- 同一项目正在重算时,手动接口会被拒绝
- 同一项目自动触发期间再次触发时会标记补跑
- 流水级结果写入时会保存 `group_id + log_id`
- 对象级结果写入时不会保存 `group_id + log_id`
- 重复重算不会出现重复结果
- 参数修改后再次重算,结果会随阈值变化
- 单条规则失败时任务状态为 `PARTIAL_FAILED`
## 风险与缓解
### 风险一:同项目高频导入导致连续补跑
缓解:
- 自动触发期间只维护一个 `need_rerun` 标记,避免无限累积队列
### 风险二:规则级并行导致数据库写入压力上升
缓解:
- 为规则线程池设置合理核心线程数和队列长度
- 批量插入结果,避免逐条写入
### 风险三:对象级结果原因信息不足
缓解:
- 在 XML 查询阶段直接拼出可展示的原因摘要字段
## 后续扩展方向
- 新增项目标签汇总页和明细页
- 结合 `ccdi_project` 回写项目风险人数统计
- 支持更多模型接入
- 支持对象级结果下钻到关联流水集合

View File

@@ -1,337 +0,0 @@
# 项目上传文件列表删除功能设计
## 背景
当前项目详情页“上传数据”中的“上传文件列表”仅展示文件信息和状态,不支持针对单条文件记录执行业务操作。
现有系统已经具备以下基础能力:
- `ccdi_file_upload_record` 已保存每条上传记录的 `id``lsfxProjectId``logId``fileStatus``errorMessage`
- `ccdi_bank_statement` 已按 `projectId + batchId(logId)` 关联每次上传入库的流水数据
- 流水分析平台客户端 `LsfxAnalysisClient` 已封装删除文件接口 `deleteFiles`
当前缺失的是:
- 解析失败文件缺少“查看错误原因”入口
- 解析成功文件缺少“删除”入口
- 删除后缺少保留历史记录并展示“已删除”状态的机制
## 目标
- 在项目详情-上传数据-上传文件列表中新增“操作”列
- 当文件状态为 `parsed_failed` 时,支持查看错误原因
- 当文件状态为 `parsed_success` 时,支持删除文件
- 删除时增加二次确认,明确会同步删除平台文件与本地流水
- 删除成功后调用流水分析平台删除接口
- 删除成功后删除本地银行流水表中对应文件的全部流水
- 删除成功后保留上传记录,并将状态更新为 `deleted`
- 删除后的列表状态显示为“已删除”
## 非目标
- 不删除 `ccdi_file_upload_record` 历史记录
- 不新增独立删除标记字段,如 `deleted_flag``deleted_time`
- 不调整现有批量上传、轮询解析、本行信息拉取等主流程
- 不改造流水明细查询页面的筛选交互
## 现状分析
### 前端现状
上传文件列表页面位于:
- `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue`
当前列表字段包括:
- 文件名
- 文件大小
- 状态
- 主体名称
- 主体账号
- 上传时间
- 上传人
当前前端已经具备:
- 查询列表:`getFileUploadList`
- 查询统计:`getFileUploadStatistics`
- 查看列表中的 `fileStatus`
- 弹窗提示能力 `this.$alert`
- 二次确认能力 `this.$confirm`
当前前端未具备:
- 操作列渲染
- 按记录 ID 删除文件的 API
- `deleted` 状态映射
### 后端现状
后端上传记录相关接口位于:
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java`
当前已提供:
- `GET /ccdi/file-upload/list`
- `GET /ccdi/file-upload/statistics/{projectId}`
- `GET /ccdi/file-upload/detail/{id}`
服务层位于:
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
当前已具备:
- 按状态统计上传记录
- 根据 `logId` 拉取并入库银行流水
- 失败时记录 `errorMessage`
- 通过 `CcdiBankStatementMapper.deleteByProjectIdAndBatchId(projectId, logId)` 清理本地流水
流水分析平台客户端位于:
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/LsfxAnalysisClient.java`
当前已具备:
- `deleteFiles(DeleteFilesRequest)` 删除平台文件
### 状态现状
当前 `file_status` 已存在以下状态:
- `uploading`
- `parsing`
- `parsed_success`
- `parsed_failed`
当前缺少删除后的业务状态,因此无法在保留记录的前提下表达“已删除”。
## 方案对比
### 方案一:基于现有状态字段增加 `deleted`
-`file_status` 中新增 `deleted`
- 删除成功后仅更新记录状态,不删除记录
- 前后端统一基于状态判断操作按钮与文案
优点:
- 最符合“保留并显示为已删除”的需求
- 复用现有查询、展示、统计结构
- 改动范围集中,数据库结构无需新增字段
缺点:
- 需要同步扩展状态映射和统计逻辑
### 方案二:新增独立删除标记字段
- 保留原有 `file_status`
- 额外增加 `deletedFlag``deletedTime`
优点:
- 删除状态与解析状态语义分离
缺点:
- 查询、展示、统计逻辑更复杂
- 需要新增数据库字段和 Mapper 映射
- 当前需求下属于过度设计
### 方案三:物理删除上传记录
优点:
- 实现最直接
缺点:
- 与“继续保留并显示为已删除”冲突
- 无法追溯文件历史和删除操作结果
## 最终方案
采用方案一:在 `ccdi_file_upload_record.file_status` 中新增 `deleted` 状态,基于现有上传记录做软删除保留。
## 详细设计
### 状态模型
上传记录状态统一定义为:
- `uploading`:上传中
- `parsing`:解析中
- `parsed_success`:解析成功
- `parsed_failed`:解析失败
- `deleted`:已删除
状态驱动前端操作规则:
- `parsed_failed`:显示“查看错误原因”
- `parsed_success`:显示“删除”
- `deleted`:不显示按钮
- `uploading``parsing`:不显示按钮
### 删除链路
删除流程按以下顺序执行:
1. 前端点击“删除”
2. 前端展示二次确认弹窗
3. 用户确认后调用“按上传记录 ID 删除文件”接口
4. 后端校验记录存在、归属当前项目、状态为 `parsed_success`、且包含 `lsfxProjectId``logId`
5. 后端调用流水分析平台 `deleteFiles`
6. 平台删除成功后,后端删除本地 `ccdi_bank_statement``projectId + batchId(logId)` 的全部流水
7. 本地流水删除成功后,后端将 `ccdi_file_upload_record.file_status` 更新为 `deleted`
8. 前端收到成功响应后刷新列表与统计
### 一致性原则
删除必须遵守“先平台、后本地、再改状态”:
- 平台删除失败:不删本地流水,不更新记录状态
- 本地流水删除失败:不更新记录状态为 `deleted`
- 状态更新失败:整体按失败返回,避免界面和数据不一致
### 后端接口设计
新增接口建议:
- `DELETE /ccdi/file-upload/{id}`
接口语义:
- 按上传记录 ID 删除对应文件
- 仅允许删除 `parsed_success` 状态记录
返回规则:
- 成功:`AjaxResult.success("删除成功")`
- 失败:`AjaxResult.error("具体失败原因")`
### 后端服务设计
服务层新增删除方法,职责包括:
- 查询上传记录
- 校验项目归属与状态
- 组装 `DeleteFilesRequest`
- 调用 `LsfxAnalysisClient.deleteFiles`
- 删除本地银行流水
- 更新上传记录状态为 `deleted`
建议保持数据库层面不新增表结构,仅扩展状态值和相关逻辑。
### 前端交互设计
上传文件列表新增“操作”列。
操作规则:
- `parsed_failed`:点击“查看错误原因”,弹出错误信息弹窗
- `parsed_success`:点击“删除”,弹出确认框
- `deleted`:显示状态“已删除”,无操作按钮
删除确认文案建议:
`删除后将同步删除流水分析平台中的文件,并清除本系统中该文件对应的所有银行流水数据,是否继续?`
删除成功后:
- 提示“删除成功”
- 刷新列表
- 刷新统计
### 统计设计
当前统计 VO 仅统计:
- `uploading`
- `parsing`
- `parsedSuccess`
- `parsedFailed`
- `total`
本次建议扩展:
- `deleted`
这样前端统计口径可以完整覆盖全部状态,也便于后续展示已删除数量。
如果短期内页面不展示该数字,也建议后端先统一支持,避免状态被统计遗漏。
## 影响范围
### 后端
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiFileUploadService.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiFileUploadStatisticsVO.java`
- `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiFileUploadRecordMapper.xml`
### 前端
- `ruoyi-ui/src/api/ccdiProjectUpload.js`
- `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue`
## 风险与边界
需要重点拦截以下删除场景:
- 记录不存在
- 记录不属于当前项目
- 记录状态不是 `parsed_success`
- 记录已经是 `deleted`
- 记录缺少 `lsfxProjectId`
- 记录缺少 `logId`
需要注意以下业务影响:
- 软删除后,流水明细查询将不再能查到该文件对应流水
- 上传记录会继续保留,便于审计和追溯
- 如果平台删除成功但本地状态更新失败,需要明确作为失败返回并记录日志
## 测试方案
### 后端单元测试
- 删除成功时:平台删除成功、本地流水删除成功、记录状态更新为 `deleted`
- 平台删除失败时:本地流水不删除、记录状态不变
- 本地流水删除失败时:记录状态不变
-`parsed_success` 状态删除时返回失败
- `deleted` 状态重复删除时返回失败
- 缺少 `logId``lsfxProjectId` 时返回失败
### 控制器测试
- 删除接口成功返回正确消息
- Service 抛出异常时返回错误消息
### 前端测试
- `parsed_failed` 状态显示“查看错误原因”
- `parsed_success` 状态显示“删除”
- `deleted` 状态显示“已删除”且不显示操作按钮
- 删除前弹出二次确认
- 删除成功后刷新列表和统计
- 错误原因弹窗能正确显示 `errorMessage`
## 验收标准
- 上传文件列表新增“操作”列
- 解析失败文件可以查看错误原因
- 解析成功文件可以二次确认后删除
- 删除成功后已调用流水分析平台删除接口
- 删除成功后本地银行流水数据被清理
- 删除成功后上传记录仍保留,状态显示为“已删除”
- 已删除记录不再出现删除按钮
---
**设计日期:** 2026-03-16
**设计人员:** Codex
**审核状态:** 已确认

View File

@@ -1,412 +0,0 @@
# 项目流水标签后端详细日志设计
## 概述
本次设计面向“项目流水标签”后端链路补充详细日志提醒能力,覆盖手动重算、自动触发、项目级互斥、规则级执行、参数解析、结果落库和自动补跑全过程。
目标同时满足两类需求:
- 排障:出现“没有触发”“任务卡住”“规则没执行”“结果为 0”“自动补跑未生效”等问题时能够通过日志快速定位断点
- 审计:能够追踪是谁在什么时间,对哪个项目、哪个模型发起了手动重算,以及本次重算的结果摘要
本次设计只补应用日志,不调整数据库表结构,不新增前端展示,不引入 AOP、链路追踪框架或独立审计表。
## 已确认范围
- 日志面向“排障 + 审计”双目标
- 日志保存位置沿用现有后端应用日志
- 记录手动重算与自动触发两类入口
- 记录项目级互斥、补跑标记、补跑消费过程
- 记录任务级摘要、规则级执行、结果清理和结果写入
- 记录规则参数解析来源和结果
- 阈值参数值允许打印
- 身份证号、账号、`objectKey` 等敏感字段不打印明文
- 不打印 SQL 明细
- 命中明细不按条展开到 `info`
## 现状问题
当前与流水标签相关的主要代码位于:
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinator.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolver.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
现状上,“文件上传 / 拉取本行信息”链路已有较多日志,但“流水标签重算”核心链路日志不足,主要存在以下问题:
1. 手动重算和自动触发进入标签链路后,缺少统一入口日志
2. 项目级互斥和 `needRerun` 标记逻辑几乎不可观测
3. 标签任务开始、结束、失败都没有清晰摘要
4. 单条规则执行的耗时、命中数、阈值来源无法定位
5. 结果清理和批量写库缺少过程确认
这会导致问题发生时难以区分:
- 是入口未触发
- 是被项目级互斥拦截
- 是规则没有命中
- 是规则执行失败
- 是结果写库失败
- 是补跑标记了但未真正再次执行
## 方案对比
### 方案一:在现有方法中直接补日志
- 在现有类中按节点直接补 `info / debug / warn / error`
- 不做统一模板
优点:
- 改动最小
- 落地最快
- 与当前项目写法最接近
缺点:
- 日志字段格式容易分散
- 后续继续扩展时容易重复和漂移
### 方案二:统一字段格式的轻量日志设计
- 在现有类中补日志
- 统一核心上下文字段和文案结构
- 允许通过少量私有辅助方法减少重复
优点:
- 兼顾快速落地和长期可检索性
- 同时适合排障和审计
- 不引入额外框架,风险较低
缺点:
- 比方案一多一点整理成本
### 方案三:日志之外再做持久化审计摘要
- 除应用日志外,再把关键摘要落到任务表或独立审计表
优点:
- 审计能力最强
- 不依赖日志平台
缺点:
- 超出本次“补详细日志提醒”的范围
- 会引入表结构或数据模型变更
## 最终方案
采用方案二:统一字段格式的轻量日志设计。
具体原则如下:
1. 只在现有后端类补日志,不改数据库结构
2. `info` 负责任务摘要和关键里程碑
3. `debug` 负责规则参数、命中数量、批处理细节
4. `warn` 负责互斥、降级、参数缺失、无命中等非致命异常
5. `error` 负责任务失败、规则异常、写库失败、触发失败
6. 日志字段尽量统一,保证同一任务可以通过 `projectId/taskId` 串起来
## 日志等级设计
### info
用于记录:
- 手动重算入口
- 自动触发入口
- 获取项目锁成功
- 任务创建成功
- 规则加载完成
- 历史结果清理开始
- 单规则开始和结束摘要
- 结果批量写入摘要
- 任务成功摘要
- 自动补跑开始和结束
### debug
用于记录:
- 规则阈值参数
- 参数来源项目
- 规则命中明细数量
- 无需写库时的空结果分支
- 补跑标记消费细节
### warn
用于记录:
- 手动重算被运行中任务拒绝
- 自动触发命中运行中任务,仅标记 `needRerun`
- 自动触发被跳过
- 规则参数缺失
- 规则执行结果为空或无命中
### error
用于记录:
- 任务整体失败
- 单规则执行异常
- 结果写库异常
- 参数解析过程中出现不可恢复异常
- 自动触发或补跑异常
## 统一上下文字段
建议所有流水标签日志尽量带上以下字段:
- `projectId`
- `taskId`
- `modelCode`
- `triggerType`
- `operator`
- `ruleCode`
- `costMs`
- `hitCount`
其中:
- 任务创建前无法取得 `taskId` 的场景允许缺省
- 与单规则无关的日志可以不打印 `ruleCode`
- 与耗时无关的日志可以不打印 `costMs`
## 脱敏规则
本次日志遵循以下脱敏边界:
- 允许打印阈值参数编码和值
- 不打印身份证号明文
- 不打印账号明文
- 不打印完整 `objectKey`
- 不打印规则 SQL
- 不在 `info` 级别展开逐条命中结果
若后续需要定位对象级命中,可在 `debug` 级别打印脱敏后的对象标识摘要,例如前 3 位加后 2 位,但本次设计不要求默认展开。
## 打点设计
### 1. 入口层
涉及类:
- `CcdiBankTagController`
- `CcdiFileUploadServiceImpl`
目标:
- 明确日志是从手动入口还是自动入口进入
- 对自动触发链路补“已触发 / 已跳过”判断
建议日志:
```text
【流水标签】收到手动重算请求: projectId={}, modelCode={}, operator={}
【流水标签】批处理完成,准备触发自动重算: projectId={}, triggerType={}, anySuccess={}
【流水标签】跳过自动重算: projectId={}, triggerType={}, reason=all_records_failed
```
### 2. 协调层
涉及类:
- `ProjectBankTagRebuildCoordinator`
目标:
- 观测项目级互斥
- 区分“任务丢失”和“任务被合并补跑”
- 记录锁获取、锁释放、补跑消费全过程
建议日志:
```text
【流水标签】手动重算开始排队: projectId={}, modelCode={}, operator={}
【流水标签】项目已有运行中任务,拒绝手动重算: projectId={}, modelCode={}, operator={}
【流水标签】项目正在重算,已标记完成后补跑: projectId={}, runningTaskId={}, triggerType={}
【流水标签】获取项目重算锁成功: projectId={}
【流水标签】检测到需要补跑,准备再次执行: projectId={}, previousTaskId={}
【流水标签】未检测到补跑标记,结束自动重算: projectId={}, taskId={}
【流水标签】释放项目重算锁: projectId={}
```
### 3. 执行层
涉及类:
- `CcdiBankTagServiceImpl`
目标:
- 形成任务级生命周期日志
- 形成规则级执行和写库摘要
建议日志:
```text
【流水标签】任务创建成功: taskId={}, projectId={}, modelCode={}, triggerType={}, operator={}
【流水标签】加载启用规则完成: taskId={}, projectId={}, modelCode={}, ruleCount={}
【流水标签】开始清理历史结果: taskId={}, projectId={}, modelCode={}
【流水标签】规则开始执行: taskId={}, projectId={}, ruleCode={}, resultType={}
【流水标签】规则执行参数: taskId={}, ruleCode={}, thresholds={}
【流水标签】规则执行完成: taskId={}, projectId={}, ruleCode={}, hitCount={}, costMs={}
【流水标签】规则无命中: taskId={}, projectId={}, ruleCode={}, costMs={}
【流水标签】批量写入标签结果: taskId={}, projectId={}, resultCount={}
【流水标签】任务执行成功: taskId={}, projectId={}, modelCode={}, triggerType={}, ruleCount={}, hitCount={}, costMs={}
【流水标签】任务执行失败: taskId={}, projectId={}, modelCode={}, triggerType={}, error={}
```
### 4. 参数解析层
涉及类:
- `BankTagRuleConfigResolver`
目标:
- 说明阈值从项目默认配置还是项目专属配置解析而来
- 在参数缺失时明确记录缺了哪些编码
建议日志:
```text
【流水标签】解析规则参数: projectId={}, effectiveProjectId={}, ruleCode={}, requiredParams={}
【流水标签】规则参数解析结果: projectId={}, ruleCode={}, thresholdValues={}
【流水标签】规则参数缺失: projectId={}, ruleCode={}, missingParams={}
```
## 关键异常场景设计
### 手动重算被拒绝
当项目已经存在运行中任务时:
- 抛出原有业务异常
- 额外补 `warn`,明确项目、模型、操作人和拒绝原因
目的:
- 便于区分“接口未进来”和“接口进来了但被互斥挡住”
### 自动重算被合并
当批量上传或拉取本行触发自动重算时,如果项目已在运行中:
- 不再直接丢弃
- 通过 `markNeedRerun` 标记补跑
- 日志明确说明当前触发未丢失,而是等待本轮完成后自动重跑
### 参数缺失但继续执行
当前规则参数解析器未对所有缺失参数直接抛错,部分规则可能按空值或 `0` 继续执行。
此场景需要:
- `warn` 记录缺失参数编码
- `debug` 记录实际解析到的阈值集合
- 文案中说明本次按当前默认值继续执行
### 单规则执行失败
当某条规则在查询或结果构造过程中抛异常时:
- `error` 记录 `taskId/projectId/ruleCode`
- 保留原有失败传播语义
- 由任务级失败日志补充整任务摘要
### 结果写库失败
当历史结果已清理但新结果写入失败时:
- `error` 记录失败发生在“结果写入”阶段
- 日志中带上准备写入的结果条数
这样可以避免误判为“规则无命中”。
### 自动补跑消费
自动重算完成后如果检测到 `needRerun=1`
- `info` 记录上一轮任务 ID 和将进入补跑
- `debug` 记录补跑标记消费结果
如果没有检测到补跑标记,也应记录收尾日志,避免看到“开始”却看不到“结束”。
## 建议实现方式
为控制改动范围,本次不新增独立日志组件,优先采用以下实现策略:
1. 在相关类中直接补充结构统一的日志
2. 对重复字段较多的场景,可增加少量私有辅助方法拼接摘要
3. 不为了日志而引入 ThreadLocal、MDC、AOP 或统一切面
这样可以把改动集中在当前标签链路相关类中,降低回归风险。
## 验证策略
本次重点验证“行为分支可被覆盖”,不建议编写对日志文本本身高度耦合的脆弱断言。
建议关注以下测试:
### 协调器测试
- 已有运行中任务时,手动重算被拒绝
- 自动触发时命中运行中任务并标记 `needRerun`
- 自动触发完成后成功消费补跑标记
### 标签服务测试
- 启用规则数为 0
- 规则执行有命中且成功写库
- 规则执行无命中时不写库
- 单规则抛异常导致任务失败
### 参数解析测试
- 使用默认项目参数
- 使用项目专属参数
- 存在参数缺失时返回缺失状态
## 非目标
本次设计不包含以下内容:
- 新增数据库表或审计表
- 前端页面展示任务执行日志
- 接入链路追踪系统
- 打印规则 SQL
- 打印对象或账号明细
- 调整标签任务执行策略或线程池模型
## 预期效果
落地后,开发和运维应能够通过日志快速回答以下问题:
- 手动重算是否真正进入后端
- 自动触发是否提交成功,还是因为整批失败而跳过
- 项目当前是否被互斥锁住
- 自动触发是否被合并为补跑
- 本次任务创建了哪个 `taskId`
- 加载了多少条规则
- 哪条规则执行最慢、命中多少
- 参数是否来自默认配置,是否存在缺失
- 结果是否已删除旧数据并完成新写入
- 任务最终成功还是失败,失败在哪个阶段
## 落地范围
建议本次代码改动控制在以下文件附近:
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiBankTagController.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinator.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolver.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
如需进一步实现,可在此设计基础上继续拆分为具体实现计划。

View File

@@ -698,8 +698,8 @@ mvn spring-boot:run
**步骤 6: 提交测试记录**
```bash
mkdir -p docs/test-records
git add docs/test-records/
mkdir -p docs/tests/records
git add docs/tests/records/
git commit -m "test(ccdi-project): 记录后端接口测试结果"
```

View File

@@ -15,7 +15,7 @@
**Files:**
- Create: `sql/2026-03-12_ccdi_asset_info.sql`
- Review: `assets/资产信息表.csv`
- Review: `docs/plans/2026-03-12-employee-asset-maintenance-design.md`
- Review: `docs/design/2026-03-12-employee-asset-maintenance-design.md`
**Step 1: Write the SQL script**
@@ -40,7 +40,7 @@ Confirm the script:
**Step 3: Commit**
```bash
git add sql/2026-03-12_ccdi_asset_info.sql docs/plans/2026-03-12-employee-asset-maintenance-design.md
git add sql/2026-03-12_ccdi_asset_info.sql docs/design/2026-03-12-employee-asset-maintenance-design.md
git commit -m "新增员工资产信息设计与建表脚本"
```
@@ -349,6 +349,6 @@ Expected: compile succeeds without Java or mapper XML errors.
**Step 3: Commit**
```bash
git add sql/2026-03-12_ccdi_asset_info.sql ccdi-info-collection/src/main/java/com/ruoyi/info/collection ccdi-info-collection/src/main/resources/mapper/info/collection docs/plans/2026-03-12-employee-asset-maintenance-backend-implementation.md
git add sql/2026-03-12_ccdi_asset_info.sql ccdi-info-collection/src/main/java/com/ruoyi/info/collection ccdi-info-collection/src/main/resources/mapper/info/collection docs/plans/backend/2026-03-12-employee-asset-maintenance-backend-implementation.md
git commit -m "新增员工资产信息后端实施计划"
```

View File

@@ -13,7 +13,7 @@
### Task 1: Verify backend impact is zero
**Files:**
- Review: `docs/plans/2026-03-12-pull-bank-info-date-limit-design.md`
- Review: `docs/design/2026-03-12-pull-bank-info-date-limit-design.md`
- Review: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/`
- Review: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/`

View File

@@ -13,7 +13,7 @@
### Task 1: Verify backend impact is zero
**Files:**
- Review: `docs/plans/2026-03-12-pull-bank-info-upload-button-hit-area-design.md`
- Review: `docs/design/2026-03-12-pull-bank-info-upload-button-hit-area-design.md`
- Review: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/`
- Review: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/`

View File

@@ -14,7 +14,7 @@
**Files:**
- Create: `lsfx-mock-server/**`
- Modify: `docs/plans/2026-03-13-ccdi-docker-deployment-design.md`
- Modify: `docs/design/2026-03-13-ccdi-docker-deployment-design.md`
- Test: `lsfx-mock-server/tests/test_api.py`
**Step 1: 复制并清理运行文件**
@@ -110,7 +110,7 @@ Expected: Compose 文件能正常展开且无语法错误
### Task 5: 构建与联调验证
**Files:**
- Modify: `docs/plans/2026-03-13-ccdi-docker-deployment-design.md`
- Modify: `docs/design/2026-03-13-ccdi-docker-deployment-design.md`
**Step 1: 本地构建后端**
@@ -131,6 +131,6 @@ Expected: 无错误
**Step 4: 提交**
```bash
git add lsfx-mock-server docker docker-compose.yml .env.example deploy docs/plans/2026-03-13-ccdi-docker-deployment-*.md
git add lsfx-mock-server docker docker-compose.yml .env.example deploy docs/design/2026-03-13-ccdi-docker-deployment-design.md docs/plans/backend/2026-03-13-ccdi-docker-deployment-backend-implementation.md docs/plans/frontend/2026-03-13-ccdi-docker-deployment-frontend-implementation.md
git commit -m "新增Docker后端部署方案"
```

View File

@@ -214,7 +214,7 @@ git commit -m "实现员工资产导入归属匹配"
### Task 5: 执行回归验证
**Files:**
- Modify: `docs/plans/2026-03-13-employee-family-asset-import-split-design.md`
- Modify: `docs/design/2026-03-13-employee-family-asset-import-split-design.md`
**Step 1: 运行后端定向测试**
@@ -248,6 +248,6 @@ Expected:
**Step 4: 提交**
```bash
git add docs/plans/2026-03-13-employee-family-asset-import-split-design.md
git add docs/design/2026-03-13-employee-family-asset-import-split-design.md
git commit -m "完成资产导入拆分后端验证"
```

View File

@@ -31,7 +31,7 @@
**Step 4: Commit**
```bash
git add docs/plans/2026-03-16-large-transaction-project40-design.md assets/database/2026-03-16-project40-large-transaction-seed.sql
git add docs/design/2026-03-16-large-transaction-project40-design.md assets/database/2026-03-16-project40-large-transaction-seed.sql
git commit -m "文档: 补充项目40大额交易测试数据设计"
```
@@ -114,7 +114,7 @@ Expected: 每个指标返回至少 1 条命中记录或 1 个命中分组。
**Step 4: Commit**
```bash
git add assets/database/2026-03-16-project40-large-transaction-seed.sql docs/implementation-reports/2026-03-16-project40-large-transaction-report.md
git add assets/database/2026-03-16-project40-large-transaction-seed.sql docs/reports/implementation2026-03-16-project40-large-transaction-report.md
git commit -m "验证: 完成项目40大额交易测试流水校验"
```
@@ -144,6 +144,6 @@ Expected: `project_id=40` 存在稳定数量的测试流水。
**Step 4: Commit**
```bash
git add docs/implementation-reports/2026-03-16-project40-large-transaction-report.md
git add docs/reports/implementation2026-03-16-project40-large-transaction-report.md
git commit -m "文档: 完善项目40大额交易测试流水报告"
```

View File

@@ -214,7 +214,7 @@ git commit -m "refactor: 收敛模型参数服务对齐逻辑"
**Files:**
- Reference: `sql/ccdi_model_param.sql`
- Reference: `sql/2026-03-16-update-ccdi-model-param-defaults.sql`
- Optional Record: `docs/test-records/model-param-backend-alignment-test.md`
- Optional Record: `docs/tests/records/model-param-backend-alignment-test.md`
**Step 1: 准备校验项**
@@ -262,13 +262,13 @@ ORDER BY model_code, sort_order, id;
将验证过程写入:
```text
docs/test-records/model-param-backend-alignment-test.md
docs/tests/records/model-param-backend-alignment-test.md
```
**Step 5: 提交**
```bash
git add docs/test-records/model-param-backend-alignment-test.md
git add docs/tests/records/model-param-backend-alignment-test.md
git commit -m "test: 记录模型默认参数后端对齐验证"
```
@@ -348,10 +348,10 @@ POST /ccdi/modelParam/saveAll
测试结束后关闭 `mvn spring-boot:run` 启动的进程,再提交测试记录:
```bash
git add docs/test-records/model-param-backend-alignment-test.md
git add docs/tests/records/model-param-backend-alignment-test.md
git commit -m "test: 完成模型参数后端接口回归验证"
```
---
Plan complete and saved to `docs/plans/2026-03-16-model-param-csv-alignment-backend-implementation.md`.
Plan complete and saved to `docs/plans/backend/2026-03-16-model-param-csv-alignment-backend-implementation.md`.

View File

@@ -13,7 +13,7 @@
### Task 1: 确认需求边界
**Files:**
- Review: `docs/plans/2026-03-16-param-save-bar-fixed-bottom-design.md`
- Review: `docs/design/2026-03-16-param-save-bar-fixed-bottom-design.md`
- Review: `ccdi-project`
- Review: `ccdi-info-collection`
@@ -32,7 +32,7 @@
### Task 2: 回归验证清单
**Files:**
- Review: `docs/plans/2026-03-16-param-save-bar-fixed-bottom-design.md`
- Review: `docs/design/2026-03-16-param-save-bar-fixed-bottom-design.md`
**Step 1: 验证参数查询接口**

View File

@@ -688,7 +688,7 @@ git commit -m "feat: 接入拉取本行信息完成后的自动流水打标"
### Task 12: 完成全量验证
**Files:**
- Modify: `docs/plans/2026-03-16-project-bank-statement-tagging-backend-implementation.md`
- Modify: `docs/plans/backend/2026-03-16-project-bank-statement-tagging-backend-implementation.md`
**Step 1: Run focused backend tests**
@@ -721,7 +721,7 @@ Expected:
**Step 4: Commit**
```bash
git add ccdi-project sql docs/plans/2026-03-16-project-bank-statement-tagging-backend-implementation.md
git add ccdi-project sql docs/plans/backend/2026-03-16-project-bank-statement-tagging-backend-implementation.md
git commit -m "feat: 完成项目流水标签后端实现"
```

View File

@@ -428,9 +428,9 @@ npm run dev
**步骤 6: 提交测试记录**
```bash
mkdir -p docs/test-records
echo "## 全局配置页面测试结果\n\n测试时间$(date)\n\n- [x] 页面显示正确\n- [x] 修改功能正常\n- [x] 保存功能正常\n- [x] 错误处理正常" > docs/test-records/global-config-test.md
git add docs/test-records/
mkdir -p docs/tests/records
echo "## 全局配置页面测试结果\n\n测试时间$(date)\n\n- [x] 页面显示正确\n- [x] 修改功能正常\n- [x] 保存功能正常\n- [x] 错误处理正常" > docs/tests/records/global-config-test.md
git add docs/tests/records/
git commit -m "test(ui): 记录全局配置页面测试结果"
```
@@ -745,8 +745,8 @@ git commit -m "feat(ui): 重构项目内模型参数配置页面"
**步骤 7: 提交测试记录**
```bash
echo "## 项目配置页面测试结果\n\n测试时间$(date)\n\n- [x] 页面显示正确\n- [x] 使用默认配置项目测试通过\n- [x] 自定义配置项目测试通过\n- [x] 多模型修改测试通过" > docs/test-records/project-config-test.md
git add docs/test-records/
echo "## 项目配置页面测试结果\n\n测试时间$(date)\n\n- [x] 页面显示正确\n- [x] 使用默认配置项目测试通过\n- [x] 自定义配置项目测试通过\n- [x] 多模型修改测试通过" > docs/tests/records/project-config-test.md
git add docs/tests/records/
git commit -m "test(ui): 记录项目配置页面测试结果"
```
@@ -791,8 +791,8 @@ git commit -m "test(ui): 记录项目配置页面测试结果"
**步骤 5: 提交测试报告**
```bash
echo "## 端到端集成测试结果\n\n测试时间$(date)\n\n### 功能测试\n- [x] 全局配置影响项目配置\n- [x] 项目配置不影响全局配置\n- [x] 并发操作正常\n\n### 性能测试\n- [x] listAll接口响应时间 < 200ms\n- [x] saveAll接口响应时间 < 500ms\n\n### 结论\n前后端集成测试通过功能正常性能符合要求。" > docs/test-records/e2e-test.md
git add docs/test-records/
echo "## 端到端集成测试结果\n\n测试时间$(date)\n\n### 功能测试\n- [x] 全局配置影响项目配置\n- [x] 项目配置不影响全局配置\n- [x] 并发操作正常\n\n### 性能测试\n- [x] listAll接口响应时间 < 200ms\n- [x] saveAll接口响应时间 < 500ms\n\n### 结论\n前后端集成测试通过功能正常性能符合要求。" > docs/tests/records/e2e-test.md
git add docs/tests/records/
git commit -m "test(ui): 完成端到端集成测试"
```

View File

@@ -574,6 +574,6 @@ Expected: 本地员工信息维护页面可访问
**Step 6: 最终提交**
```bash
git add ruoyi-ui/src/api/ccdiBaseStaff.js ruoyi-ui/src/api/ccdiAssetInfo.js ruoyi-ui/src/views/ccdiBaseStaff/index.vue ruoyi-ui/tests/unit docs/plans/2026-03-12-employee-asset-maintenance-frontend-implementation.md
git add ruoyi-ui/src/api/ccdiBaseStaff.js ruoyi-ui/src/api/ccdiAssetInfo.js ruoyi-ui/src/views/ccdiBaseStaff/index.vue ruoyi-ui/tests/unit docs/plans/frontend/2026-03-12-employee-asset-maintenance-frontend-implementation.md
git commit -m "新增员工资产信息前端实施计划"
```

View File

@@ -15,7 +15,7 @@
**Files:**
- Create: `docker/frontend/Dockerfile`
- Create: `docker/frontend/nginx.conf`
- Modify: `docs/plans/2026-03-13-ccdi-docker-deployment-design.md`
- Modify: `docs/design/2026-03-13-ccdi-docker-deployment-design.md`
**Step 1: 创建前端镜像定义**
@@ -69,7 +69,7 @@ Expected: 前端服务、依赖与端口映射正确
### Task 4: 联调验证
**Files:**
- Modify: `docs/plans/2026-03-13-ccdi-docker-deployment-design.md`
- Modify: `docs/design/2026-03-13-ccdi-docker-deployment-design.md`
**Step 1: 检查前端生产产物**
@@ -84,6 +84,6 @@ Expected: 前端服务、依赖与端口映射正确
**Step 3: 提交**
```bash
git add docker/frontend deploy/deploy.ps1 docker-compose.yml .env.example docs/plans/2026-03-13-ccdi-docker-deployment-*.md
git add docker/frontend deploy/deploy.ps1 docker-compose.yml .env.example docs/design/2026-03-13-ccdi-docker-deployment-design.md docs/plans/backend/2026-03-13-ccdi-docker-deployment-backend-implementation.md docs/plans/frontend/2026-03-13-ccdi-docker-deployment-frontend-implementation.md
git commit -m "新增Docker前端部署方案"
```

View File

@@ -188,7 +188,7 @@ git commit -m "保护亲属页资产导入交互不回归"
### Task 5: 执行前端回归验证
**Files:**
- Modify: `docs/plans/2026-03-13-employee-family-asset-import-split-design.md`
- Modify: `docs/design/2026-03-13-employee-family-asset-import-split-design.md`
**Step 1: 运行全部相关静态测试**
@@ -225,6 +225,6 @@ Expected:
**Step 4: 提交**
```bash
git add docs/plans/2026-03-13-employee-family-asset-import-split-design.md
git add docs/design/2026-03-13-employee-family-asset-import-split-design.md
git commit -m "完成资产导入拆分前端验证"
```

View File

@@ -28,7 +28,7 @@
**Step 3: Commit**
```bash
git add docs/implementation-reports/2026-03-16-project40-large-transaction-report.md
git add docs/reports/implementation2026-03-16-project40-large-transaction-report.md
git commit -m "文档: 补充项目40流水前端验证说明"
```
@@ -53,6 +53,6 @@ git commit -m "文档: 补充项目40流水前端验证说明"
**Step 3: Commit**
```bash
git add docs/implementation-reports/2026-03-16-project40-large-transaction-report.md
git add docs/reports/implementation2026-03-16-project40-large-transaction-report.md
git commit -m "文档: 完成项目40流水前端验证清单"
```

View File

@@ -233,7 +233,7 @@ git commit -m "refactor: 收敛模型参数页修改状态管理"
### Task 5: 验证“无千分位设计”和“接口驱动展示”
**Files:**
- Optional Record: `docs/test-records/model-param-frontend-alignment-test.md`
- Optional Record: `docs/tests/records/model-param-frontend-alignment-test.md`
**Step 1: 启动前端开发服务**
@@ -273,13 +273,13 @@ npm run dev
测试结束后关闭 `npm run dev` 启动的进程,并把结果写入:
```text
docs/test-records/model-param-frontend-alignment-test.md
docs/tests/records/model-param-frontend-alignment-test.md
```
然后提交:
```bash
git add docs/test-records/model-param-frontend-alignment-test.md
git add docs/tests/records/model-param-frontend-alignment-test.md
git commit -m "test: 记录模型参数前端动态展示验证"
```
@@ -326,10 +326,10 @@ npm run dev
**Step 5: 提交联调记录**
```bash
git add docs/test-records/model-param-frontend-alignment-test.md docs/test-records/model-param-backend-alignment-test.md
git add docs/tests/records/model-param-frontend-alignment-test.md docs/tests/records/model-param-backend-alignment-test.md
git commit -m "test: 完成模型参数前后端联调验收"
```
---
Plan complete and saved to `docs/plans/2026-03-16-model-param-csv-alignment-frontend-implementation.md`.
Plan complete and saved to `docs/plans/frontend/2026-03-16-model-param-csv-alignment-frontend-implementation.md`.

View File

@@ -13,7 +13,7 @@
### Task 1: 明确本期前端范围为零代码接入
**Files:**
- Modify: `docs/plans/2026-03-16-project-bank-statement-tagging-frontend-implementation.md`
- Modify: `docs/plans/frontend/2026-03-16-project-bank-statement-tagging-frontend-implementation.md`
**Step 1: Write the acceptance checklist**
@@ -44,14 +44,14 @@ Expected:
**Step 4: Commit**
```bash
git add docs/plans/2026-03-16-project-bank-statement-tagging-frontend-implementation.md
git add docs/plans/frontend/2026-03-16-project-bank-statement-tagging-frontend-implementation.md
git commit -m "docs: 明确流水标签前端一期范围"
```
### Task 2: 记录未来 API 契约占位
**Files:**
- Modify: `docs/plans/2026-03-16-project-bank-statement-tagging-frontend-implementation.md`
- Modify: `docs/plans/frontend/2026-03-16-project-bank-statement-tagging-frontend-implementation.md`
**Step 1: Define the future API contract**
@@ -70,7 +70,7 @@ git commit -m "docs: 明确流水标签前端一期范围"
Run:
```bash
Get-Content docs/plans/2026-03-16-project-bank-statement-tagging-frontend-implementation.md | Select-String "/ccdi/project/tags/rebuild"
Get-Content docs/plans/frontend/2026-03-16-project-bank-statement-tagging-frontend-implementation.md | Select-String "/ccdi/project/tags/rebuild"
```
Expected:
@@ -84,14 +84,14 @@ Expected:
**Step 4: Commit**
```bash
git add docs/plans/2026-03-16-project-bank-statement-tagging-frontend-implementation.md
git add docs/plans/frontend/2026-03-16-project-bank-statement-tagging-frontend-implementation.md
git commit -m "docs: 补充流水标签前端后续接口契约"
```
### Task 3: 约束后续页面接入位置
**Files:**
- Modify: `docs/plans/2026-03-16-project-bank-statement-tagging-frontend-implementation.md`
- Modify: `docs/plans/frontend/2026-03-16-project-bank-statement-tagging-frontend-implementation.md`
**Step 1: Write the page integration checklist**
@@ -125,14 +125,14 @@ Expected:
**Step 4: Commit**
```bash
git add docs/plans/2026-03-16-project-bank-statement-tagging-frontend-implementation.md
git add docs/plans/frontend/2026-03-16-project-bank-statement-tagging-frontend-implementation.md
git commit -m "docs: 约束流水标签前端二期接入位置"
```
### Task 4: 完成本期前端回归检查
**Files:**
- Modify: `docs/plans/2026-03-16-project-bank-statement-tagging-frontend-implementation.md`
- Modify: `docs/plans/frontend/2026-03-16-project-bank-statement-tagging-frontend-implementation.md`
**Step 1: Run frontend build smoke check**
@@ -165,7 +165,7 @@ Expected:
**Step 4: Commit**
```bash
git add docs/plans/2026-03-16-project-bank-statement-tagging-frontend-implementation.md
git add docs/plans/frontend/2026-03-16-project-bank-statement-tagging-frontend-implementation.md
git commit -m "docs: 完成流水标签前端一期兼容性核对"
```

View File

@@ -804,12 +804,12 @@ Expected: 前端服务启动成功,访问 http://localhost/ccdiProject
```bash
# 打开浏览器访问 http://localhost/ccdiProject
# 使用截图工具拍摄完整页面截图
# 保存为 docs/plans/implementation-screenshot.png
# 保存为 docs/plans/fullstack/implementation-screenshot.png
```
**Step 5: 创建验证报告**
创建文件 `docs/plans/verification-report.md`,记录验证结果:
创建文件 `docs/plans/misc/verification-report.md`,记录验证结果:
```markdown
# 项目管理页面重构验证报告
@@ -846,7 +846,7 @@ Expected: 前端服务启动成功,访问 http://localhost/ccdiProject
Run:
```bash
git add docs/plans/verification-report.md
git add docs/plans/misc/verification-report.md
git commit -m "docs: 添加项目管理页面重构验证报告"
```
@@ -950,7 +950,7 @@ Expected: 代码已推送到远程仓库
## 相关文件
- 设计文档:`docs/plans/2026-02-27-project-management-page-redesign.md`
- 设计文档:`docs/plans/fullstack/2026-02-27-project-management-page-redesign.md`
- 原型图:`doc/创建项目功能/ScreenShot_2026-02-27_111611_994.png`
- 主组件:`ruoyi-ui/src/views/ccdiProject/index.vue`
- 搜索组件:`ruoyi-ui/src/views/ccdiProject/components/SearchBar.vue`

View File

@@ -482,7 +482,7 @@ Expected:
## Task 9: 最终提交和文档更新
**Files:**
- Modify: `docs/plans/2026-02-27-project-status-counts-fix-design.md`
- Modify: `docs/design/2026-02-27-project-status-counts-fix-design.md`
**Step 1: 更新设计文档状态**
@@ -511,7 +511,7 @@ Expected:
**Step 3: 提交文档更新**
```bash
git add docs/plans/2026-02-27-project-status-counts-fix-design.md
git add docs/design/2026-02-27-project-status-counts-fix-design.md
git commit -m "docs: 更新项目状态统计修复设计文档状态为已完成"
```
@@ -573,6 +573,6 @@ git push origin dev
## 相关文档
- 设计文档: `docs/plans/2026-02-27-project-status-counts-fix-design.md`
- 设计文档: `docs/design/2026-02-27-project-status-counts-fix-design.md`
- 若依框架文档: 项目根目录的 `CLAUDE.md`
- MyBatis Plus 文档: https://baomidou.com/

View File

@@ -1124,7 +1124,7 @@ cp db_config.conf.template db_config.conf
- 实际配置: `db_config.conf`
- 表结构文件: `doc/database/backup/ccdi_structure.sql`
- 数据文件: `doc/database/backup/ccdi_data.sql`
- 设计文档: `docs/plans/2026-02-28-database-migration-design.md`
- 设计文档: `docs/design/2026-02-28-database-migration-design.md`
```
**Step 2: 提交操作指南**

View File

@@ -648,7 +648,7 @@ git commit -m "feat: 添加获取Token响应DTO"
**步骤 1: 参考设计文档创建各个DTO类**
根据 `docs/plans/2026-03-02-lsfx-integration-design.md` 中的DTO设计创建剩余的请求和响应对象。每个DTO都使用 `@Data` 注解,字段根据接口文档定义。
根据 `docs/design/2026-03-02-lsfx-integration-design.md` 中的DTO设计创建剩余的请求和响应对象。每个DTO都使用 `@Data` 注解,字段根据接口文档定义。
**步骤 2: 提交更改**

View File

@@ -1041,7 +1041,7 @@ git log --oneline
## 参考资料
- 新版接口文档:`doc/对接流水分析/兰溪-流水分析对接-新版.md`
- 设计文档:`docs/plans/2026-03-02-lsfx-integration-design.md`
- 设计文档:`docs/design/2026-03-02-lsfx-integration-design.md`
- 若依框架规范:`CLAUDE.md`
---

View File

@@ -955,6 +955,6 @@ git push origin dev
## 文档参考
- 设计文档: `docs/plans/2026-03-04-project-detail-navigation-menu-design.md`
- 设计文档: `docs/design/2026-03-04-project-detail-navigation-menu-design.md`
- Element UI Menu 文档: https://element.eleme.cn/#/zh-CN/component/menu
- Vue 动态组件: https://cn.vuejs.org/v2/guide/components.html#动态组件

View File

@@ -314,7 +314,7 @@ git commit -m "feat(lsfx-mock): 添加银行流水审计字段到 mock 响应
## Task 4: 更新文档(可选)
**Files:**
- Update: `docs/plans/2026-03-05-bank-statement-audit-fields-design.md`(已存在)
- Update: `docs/design/2026-03-05-bank-statement-audit-fields-design.md`(已存在)
**Step 1: 验证设计文档完整性**
@@ -366,7 +366,7 @@ git commit -m "docs: 更新银行流水接口文档,补充审计字段说明"
## 参考资料
- 设计文档: `docs/plans/2026-03-05-bank-statement-audit-fields-design.md`
- 设计文档: `docs/design/2026-03-05-bank-statement-audit-fields-design.md`
- 实体类: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatement.java`
- 项目规范: `CLAUDE.md`
- 外部平台接口文档 6.5 节

View File

@@ -252,6 +252,6 @@ git commit -m "fix: 补充银行流水接口 uploadSequnceNumber 字段接收和
## 参考资料
- 设计文档:`docs/plans/2026-03-05-bank-statement-field-design.md`
- 设计文档:`docs/design/2026-03-05-bank-statement-field-design.md`
- 字段映射文档:`assets/对接流水分析/ccdi_bank_statement.md`
- 接口文档:`assets/对接流水分析/兰溪-流水分析对接-新版.md`

View File

@@ -8,7 +8,7 @@
**技术栈:** Spring Boot 3.5.8 + MyBatis Plus 3.0.5 + Vue 2.6.12 + Element UI 2.15.14
**设计文档:** `docs/plans/2026-03-06-model-param-config-optimization-design.md`
**设计文档:** `docs/design/2026-03-06-model-param-config-optimization-design.md`
---
@@ -675,7 +675,7 @@ mvn spring-boot:run
**步骤 5: 提交测试记录**
```bash
git add docs/test-records/
git add docs/tests/records/
git commit -m "test: 记录后端接口测试结果"
```
@@ -1007,7 +1007,7 @@ npm run dev
**步骤 4: 提交测试记录**
```bash
git add docs/test-records/
git add docs/tests/records/
git commit -m "test: 记录全局配置页面测试结果"
```
@@ -1270,7 +1270,7 @@ git commit -m "feat: 重构项目内模型参数配置页面"
**步骤 4: 提交测试记录**
```bash
git add docs/test-records/
git add docs/tests/records/
git commit -m "test: 记录项目配置页面测试结果"
```
@@ -1295,7 +1295,7 @@ git commit -m "test: 记录项目配置页面测试结果"
**步骤 3: 提交测试记录**
```bash
git add docs/test-records/
git add docs/tests/records/
git commit -m "test: 完成端到端功能测试"
```
@@ -1319,7 +1319,7 @@ git commit -m "test: 完成端到端功能测试"
**步骤 4: 提交测试记录**
```bash
git add docs/test-records/
git add docs/tests/records/
git commit -m "test: 完成性能测试"
```

View File

@@ -8,7 +8,7 @@
**技术栈:** Spring Boot 3.5.8 + MyBatis Plus 3.0.5 + Vue 2.6.12 + Element UI 2.15.14
**设计文档:** `docs/plans/2026-03-06-model-param-config-optimization-design.md`
**设计文档:** `docs/design/2026-03-06-model-param-config-optimization-design.md`
---
@@ -683,7 +683,7 @@ mvn spring-boot:run
记录测试结果并提交(如果需要):
```bash
git add docs/test-records/
git add docs/tests/records/
git commit -m "test: 记录后端接口测试结果"
```

View File

@@ -301,4 +301,4 @@ npm run build:prod
- `ruoyi-ui/src/settings.js` - 默认配置文件(本次修改)
- `ruoyi-ui/src/store/modules/settings.js` - Vuex 状态管理(无需修改)
- `ruoyi-ui/src/layout/components/Settings/index.vue` - 设置界面(无需修改)
- `docs/plans/2026-03-06-theme-light-default-design.md` - 设计文档
- `docs/design/2026-03-06-theme-light-default-design.md` - 设计文档

View File

@@ -308,8 +308,8 @@ git commit -m "fix(ccdi-project): cleanup partial bank statements on upload fail
### Task 4: 回归验证并整理交付
**Files:**
- Modify: `docs/plans/2026-03-09-file-upload-parse-success-after-bank-statement-design.md`
- Modify: `docs/plans/2026-03-09-file-upload-parse-success-after-bank-statement.md`
- Modify: `docs/design/2026-03-09-file-upload-parse-success-after-bank-statement-design.md`
- Modify: `docs/plans/fullstack/2026-03-09-file-upload-parse-success-after-bank-statement.md`
**Step 1: Run final verification**
@@ -343,7 +343,7 @@ Expected:
**Step 4: Commit**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapper.java ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java docs/plans/2026-03-09-file-upload-parse-success-after-bank-statement-design.md docs/plans/2026-03-09-file-upload-parse-success-after-bank-statement.md
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapper.java ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java docs/design/2026-03-09-file-upload-parse-success-after-bank-statement-design.md docs/plans/fullstack/2026-03-09-file-upload-parse-success-after-bank-statement.md
git commit -m "docs: finalize file upload parse success timing plan"
```

View File

@@ -196,7 +196,7 @@ feat(ui): 在项目详情页面添加配置类型标签显示
- 添加 getConfigTypeLabel 和 getConfigTypeStyle 方法
- 纯前端实现,无需后端修改
Ref: docs/plans/2026-03-09-param-config-type-display-design.md
Ref: docs/design/2026-03-09-param-config-type-display-design.md
EOF
)"
```
@@ -221,7 +221,7 @@ Date: [Date]
- 添加 getConfigTypeLabel 和 getConfigTypeStyle 方法
- 纯前端实现,无需后端修改
Ref: docs/plans/2026-03-09-param-config-type-display-design.md
Ref: docs/design/2026-03-09-param-config-type-display-design.md
ruoyi-ui/src/views/ccdiProject/detail.vue | [lines changed]
1 file changed, [stats]
@@ -289,7 +289,7 @@ git reset --hard HEAD~1
## 相关文档
- 设计文档: `docs/plans/2026-03-09-param-config-type-display-design.md`
- 设计文档: `docs/design/2026-03-09-param-config-type-display-design.md`
- Element UI Tag 组件: https://element.eleme.cn/#/zh-CN/component/tag
- 项目 CLAUDE.md: `CLAUDE.md`

View File

@@ -0,0 +1,563 @@
# Project Bank Statement Tagging Logging Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 为项目流水标签后端补齐可排障、可审计的详细日志,覆盖手动重算、自动触发、项目互斥、规则执行、参数解析和补跑收尾链路。
**Architecture:** 继续沿用现有 `ccdi-project` 结构,在 Controller、上传服务、重算协调器、标签服务和参数解析器上直接补充统一格式日志不引入新框架或新表。测试沿用现有 JUnit 5 + Mockito 基础,在现有测试类中通过 Logback `ListAppender` 捕获日志,验证关键分支会产生日志摘要且不破坏原行为。
**Tech Stack:** Java 21, Spring Boot 3, Lombok, SLF4J + Logback, JUnit 5, Mockito, Maven
---
## File Structure
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiBankTagController.java`
手动重算入口日志,记录 `projectId/modelCode/operator`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
自动触发入口日志,记录批处理完成后的触发、跳过和提交结果。
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinator.java`
项目级锁、互斥拒绝、`needRerun` 标记与补跑消费日志。
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java`
任务生命周期、规则执行、结果清理和批量写入摘要日志。
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolver.java`
规则参数来源、解析结果、缺失参数日志。
- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiBankTagControllerTest.java`
手动重算入口日志测试。
- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java`
自动触发入口日志测试。
- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinatorTest.java`
协调器互斥和补跑日志测试。
- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImplTest.java`
任务成功、无命中、失败摘要日志测试。
- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolverTest.java`
参数解析成功和缺失日志测试。
### Task 1: 补齐入口层日志
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiBankTagController.java`
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiBankTagControllerTest.java`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java`
- [ ] **Step 1: Write the failing tests**
在控制器测试中新增手动重算入口日志断言,在上传服务测试中新增自动触发“跳过”和“提交”日志断言。优先复用现有 Logback `ListAppender` 方案。
```java
@Test
void rebuild_shouldLogManualRebuildRequest() {
CcdiBankTagRebuildDTO dto = new CcdiBankTagRebuildDTO();
dto.setProjectId(40L);
dto.setModelCode("LARGE_TRANSACTION");
Logger logger = (Logger) LoggerFactory.getLogger(CcdiBankTagController.class);
ListAppender<ILoggingEvent> appender = new ListAppender<>();
appender.start();
logger.addAppender(appender);
when(bankTagService.submitRebuild(dto, "admin")).thenReturn("标签重算任务已提交");
try (MockedStatic<SecurityUtils> mocked = mockStatic(SecurityUtils.class)) {
mocked.when(SecurityUtils::getUsername).thenReturn("admin");
controller.rebuild(dto);
assertTrue(appender.list.stream().map(ILoggingEvent::getFormattedMessage)
.anyMatch(message -> message.contains("收到手动重算请求")
&& message.contains("projectId=40")
&& message.contains("modelCode=LARGE_TRANSACTION")
&& message.contains("operator=admin")));
} finally {
logger.detachAppender(appender);
}
}
@Test
void handleTagRebuildAfterBatchCompletion_shouldLogSkipWhenAllRecordsFailed() {
Logger logger = (Logger) LoggerFactory.getLogger(CcdiFileUploadServiceImpl.class);
ListAppender<ILoggingEvent> appender = new ListAppender<>();
appender.start();
logger.addAppender(appender);
try {
ReflectionTestUtils.invokeMethod(
service,
"handleTagRebuildAfterBatchCompletion",
PROJECT_ID,
TriggerType.AUTO_BATCH_UPLOAD,
Boolean.FALSE
);
verify(bankTagService, never()).submitAutoRebuild(any(), any());
assertTrue(appender.list.stream().map(ILoggingEvent::getFormattedMessage)
.anyMatch(message -> message.contains("跳过自动重算")
&& message.contains("projectId=100")
&& message.contains("AUTO_BATCH_UPLOAD")));
} finally {
logger.detachAppender(appender);
}
}
```
- [ ] **Step 2: Run tests to verify they fail**
Run:
```bash
mvn test -pl ccdi-project -am -Dtest=CcdiBankTagControllerTest#rebuild_shouldLogManualRebuildRequest,CcdiFileUploadServiceImplTest#handleTagRebuildAfterBatchCompletion_shouldLogSkipWhenAllRecordsFailed
```
Expected:
- `FAIL`
- 原因是控制器和上传服务尚未输出对应日志
- [ ] **Step 3: Write minimal implementation**
在入口层补日志,保持统一字段格式:
```java
log.info("【流水标签】收到手动重算请求: projectId={}, modelCode={}, operator={}",
dto.getProjectId(), dto.getModelCode(), operator);
```
```java
log.info("【流水标签】批处理完成,准备触发自动重算: projectId={}, triggerType={}, anySuccess={}",
projectId, triggerType, anySuccess);
if (!Boolean.TRUE.equals(anySuccess)) {
log.warn("【流水标签】跳过自动重算: projectId={}, triggerType={}, reason=all_records_failed",
projectId, triggerType);
return;
}
bankTagService.submitAutoRebuild(projectId, triggerType);
```
- [ ] **Step 4: Run tests to verify they pass**
Run:
```bash
mvn test -pl ccdi-project -am -Dtest=CcdiBankTagControllerTest#rebuild_shouldLogManualRebuildRequest,CcdiFileUploadServiceImplTest#handleTagRebuildAfterBatchCompletion_shouldLogSkipWhenAllRecordsFailed
```
Expected:
- `PASS`
- [ ] **Step 5: Commit**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiBankTagController.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiBankTagControllerTest.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java
git commit -m "test: 补充流水标签入口日志"
```
### Task 2: 补齐协调器互斥与补跑日志
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinator.java`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinatorTest.java`
- [ ] **Step 1: Write the failing tests**
在现有协调器测试基础上增加日志捕获,覆盖“手动被拒绝”和“自动标记补跑”两个分支。
```java
@Test
void submitManual_shouldLogRejectWhenProjectAlreadyRunning() {
CcdiBankTagTask runningTask = new CcdiBankTagTask();
runningTask.setId(1L);
runningTask.setProjectId(40L);
runningTask.setStatus("RUNNING");
when(taskMapper.selectRunningTaskByProjectId(40L)).thenReturn(runningTask);
Logger logger = (Logger) LoggerFactory.getLogger(ProjectBankTagRebuildCoordinator.class);
ListAppender<ILoggingEvent> appender = new ListAppender<>();
appender.start();
logger.addAppender(appender);
try {
assertThrows(ServiceException.class, () -> coordinator.submitManual(40L, null, "admin"));
assertTrue(appender.list.stream().map(ILoggingEvent::getFormattedMessage)
.anyMatch(message -> message.contains("拒绝手动重算")
&& message.contains("projectId=40")
&& message.contains("operator=admin")));
} finally {
logger.detachAppender(appender);
}
}
@Test
void submitAuto_shouldLogNeedRerunWhenProjectAlreadyRunning() {
CcdiBankTagTask runningTask = new CcdiBankTagTask();
runningTask.setId(1L);
runningTask.setProjectId(40L);
runningTask.setStatus("RUNNING");
runningTask.setNeedRerun(0);
when(taskMapper.selectRunningTaskByProjectId(40L)).thenReturn(runningTask);
Logger logger = (Logger) LoggerFactory.getLogger(ProjectBankTagRebuildCoordinator.class);
ListAppender<ILoggingEvent> appender = new ListAppender<>();
appender.start();
logger.addAppender(appender);
try {
coordinator.submitAuto(40L, TriggerType.AUTO_BATCH_UPLOAD);
assertTrue(appender.list.stream().map(ILoggingEvent::getFormattedMessage)
.anyMatch(message -> message.contains("已标记完成后补跑")
&& message.contains("runningTaskId=1")
&& message.contains("AUTO_BATCH_UPLOAD")));
} finally {
logger.detachAppender(appender);
}
}
```
- [ ] **Step 2: Run tests to verify they fail**
Run:
```bash
mvn test -pl ccdi-project -am -Dtest=ProjectBankTagRebuildCoordinatorTest#submitManual_shouldLogRejectWhenProjectAlreadyRunning+submitAuto_shouldLogNeedRerunWhenProjectAlreadyRunning
```
Expected:
- `FAIL`
- 原因是协调器尚未输出拒绝和补跑日志
- [ ] **Step 3: Write minimal implementation**
在协调器中补 `info/warn` 日志,至少覆盖:
```java
log.info("【流水标签】手动重算开始排队: projectId={}, modelCode={}, operator={}", projectId, modelCode, operator);
log.warn("【流水标签】项目已有运行中任务,拒绝手动重算: projectId={}, modelCode={}, operator={}", projectId, modelCode, operator);
log.warn("【流水标签】项目正在重算,已标记完成后补跑: projectId={}, runningTaskId={}, triggerType={}", projectId, runningTask.getId(), triggerType);
log.info("【流水标签】获取项目重算锁成功: projectId={}", projectId);
log.info("【流水标签】释放项目重算锁: projectId={}", projectId);
```
如实现过程中发现 `taskId` 为空分支,日志允许保留空值,但不要省略字段。
- [ ] **Step 4: Run tests to verify they pass**
Run:
```bash
mvn test -pl ccdi-project -am -Dtest=ProjectBankTagRebuildCoordinatorTest#submitManual_shouldLogRejectWhenProjectAlreadyRunning+submitAuto_shouldLogNeedRerunWhenProjectAlreadyRunning
```
Expected:
- `PASS`
- [ ] **Step 5: Commit**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinator.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinatorTest.java
git commit -m "test: 补充流水标签协调器日志"
```
### Task 3: 补齐任务级与规则级执行日志
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImplTest.java`
- [ ] **Step 1: Write the failing tests**
新增两个测试,分别覆盖成功摘要和失败摘要。复用同步执行器 `Runnable::run`,避免并发影响日志顺序。
```java
@Test
void rebuildProject_shouldLogTaskLifecycleAndRuleSummary() {
ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);
CcdiBankTagRule rule = new CcdiBankTagRule();
rule.setModelCode("LARGE_TRANSACTION");
rule.setModelName("大额交易");
rule.setRuleCode("HOUSE_OR_CAR_EXPENSE");
rule.setRuleName("房车消费支出交易");
rule.setResultType("STATEMENT");
BankTagRuleExecutionConfig config = new BankTagRuleExecutionConfig();
config.setProjectId(40L);
config.setRuleMeta(rule);
BankTagStatementHitVO hit = new BankTagStatementHitVO();
hit.setBankStatementId(10L);
hit.setGroupId(40);
hit.setLogId(40001);
hit.setReasonDetail("命中房车消费支出");
doAnswer(invocation -> {
CcdiBankTagTask task = invocation.getArgument(0);
task.setId(88L);
return 1;
}).when(taskMapper).insertTask(any(CcdiBankTagTask.class));
when(ruleMapper.selectEnabledRules(null)).thenReturn(List.of(rule));
when(configResolver.resolve(40L, rule)).thenReturn(config);
when(analysisMapper.selectHouseOrCarExpenseStatements(40L)).thenReturn(List.of(hit));
Logger logger = (Logger) LoggerFactory.getLogger(CcdiBankTagServiceImpl.class);
ListAppender<ILoggingEvent> appender = new ListAppender<>();
appender.start();
logger.addAppender(appender);
try {
service.rebuildProject(40L, null, "admin", TriggerType.MANUAL);
assertTrue(appender.list.stream().map(ILoggingEvent::getFormattedMessage)
.anyMatch(message -> message.contains("任务创建成功")
&& message.contains("taskId=88")
&& message.contains("projectId=40")));
assertTrue(appender.list.stream().map(ILoggingEvent::getFormattedMessage)
.anyMatch(message -> message.contains("规则执行完成")
&& message.contains("ruleCode=HOUSE_OR_CAR_EXPENSE")
&& message.contains("hitCount=1")));
} finally {
logger.detachAppender(appender);
}
}
@Test
void rebuildProject_shouldLogFailureSummaryWhenRuleExecutionFails() {
ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);
CcdiBankTagRule rule = new CcdiBankTagRule();
rule.setModelCode("LARGE_TRANSACTION");
rule.setRuleCode("HOUSE_OR_CAR_EXPENSE");
rule.setResultType("STATEMENT");
doAnswer(invocation -> {
CcdiBankTagTask task = invocation.getArgument(0);
task.setId(89L);
return 1;
}).when(taskMapper).insertTask(any(CcdiBankTagTask.class));
when(ruleMapper.selectEnabledRules(null)).thenReturn(List.of(rule));
when(configResolver.resolve(40L, rule)).thenThrow(new RuntimeException("threshold missing"));
Logger logger = (Logger) LoggerFactory.getLogger(CcdiBankTagServiceImpl.class);
ListAppender<ILoggingEvent> appender = new ListAppender<>();
appender.start();
logger.addAppender(appender);
try {
assertThrows(RuntimeException.class, () -> service.rebuildProject(40L, null, "admin", TriggerType.MANUAL));
assertTrue(appender.list.stream().map(ILoggingEvent::getFormattedMessage)
.anyMatch(message -> message.contains("任务执行失败")
&& message.contains("taskId=89")
&& message.contains("threshold missing")));
} finally {
logger.detachAppender(appender);
}
}
```
- [ ] **Step 2: Run tests to verify they fail**
Run:
```bash
mvn test -pl ccdi-project -am -Dtest=CcdiBankTagServiceImplTest#rebuildProject_shouldLogTaskLifecycleAndRuleSummary,CcdiBankTagServiceImplTest#rebuildProject_shouldLogFailureSummaryWhenRuleExecutionFails
```
Expected:
- `FAIL`
- 原因是标签服务尚未输出任务和规则摘要日志
- [ ] **Step 3: Write minimal implementation**
在标签服务中补统一摘要日志,并尽量把时间统计放在方法内部完成:
```java
log.info("【流水标签】任务创建成功: taskId={}, projectId={}, modelCode={}, triggerType={}, operator={}",
task.getId(), projectId, modelCode, triggerType, operator);
log.info("【流水标签】加载启用规则完成: taskId={}, projectId={}, modelCode={}, ruleCount={}",
task.getId(), projectId, modelCode, rules.size());
log.info("【流水标签】开始清理历史结果: taskId={}, projectId={}, modelCode={}",
task.getId(), projectId, modelCode);
log.info("【流水标签】规则开始执行: taskId={}, projectId={}, ruleCode={}, resultType={}",
taskId, projectId, rule.getRuleCode(), rule.getResultType());
log.debug("【流水标签】规则执行参数: taskId={}, ruleCode={}, thresholds={}", taskId, rule.getRuleCode(), config.getThresholdValues());
log.info("【流水标签】规则执行完成: taskId={}, projectId={}, ruleCode={}, hitCount={}, costMs={}",
taskId, projectId, rule.getRuleCode(), results.size(), costMs);
log.error("【流水标签】任务执行失败: taskId={}, projectId={}, modelCode={}, triggerType={}, error={}",
task.getId(), projectId, modelCode, triggerType, ex.getMessage(), ex);
```
如果实现过程中发现 `executeRule(...)` 缺少 `taskId` 上下文,可按最小改动为私有方法补一个 `taskId` 参数,不要引入全局上下文对象。
- [ ] **Step 4: Run tests to verify they pass**
Run:
```bash
mvn test -pl ccdi-project -am -Dtest=CcdiBankTagServiceImplTest#rebuildProject_shouldLogTaskLifecycleAndRuleSummary,CcdiBankTagServiceImplTest#rebuildProject_shouldLogFailureSummaryWhenRuleExecutionFails
```
Expected:
- `PASS`
- [ ] **Step 5: Commit**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImplTest.java
git commit -m "test: 补充流水标签执行日志"
```
### Task 4: 补齐规则参数解析日志
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolver.java`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolverTest.java`
- [ ] **Step 1: Write the failing tests**
新增一个日志测试,验证解析成功时会记录参数来源,参数缺失时会记录缺失编码。
```java
@Test
void resolve_shouldLogThresholdSourceAndMissingParams() {
CcdiProject project = new CcdiProject();
project.setProjectId(40L);
project.setConfigType("default");
when(projectMapper.selectById(40L)).thenReturn(project);
when(modelParamMapper.selectByProjectAndModel(0L, "LARGE_TRANSACTION")).thenReturn(List.of(
buildParam("LARGE_CASH_DEPOSIT", "50000")
));
CcdiBankTagRule ruleMeta = new CcdiBankTagRule();
ruleMeta.setModelCode("LARGE_TRANSACTION");
ruleMeta.setRuleCode("FREQUENT_CASH_DEPOSIT");
Logger logger = (Logger) LoggerFactory.getLogger(BankTagRuleConfigResolver.class);
ListAppender<ILoggingEvent> appender = new ListAppender<>();
appender.start();
logger.addAppender(appender);
try {
resolver.resolve(40L, ruleMeta);
assertTrue(appender.list.stream().map(ILoggingEvent::getFormattedMessage)
.anyMatch(message -> message.contains("解析规则参数")
&& message.contains("effectiveProjectId=0")
&& message.contains("FREQUENT_CASH_DEPOSIT")));
assertTrue(appender.list.stream().map(ILoggingEvent::getFormattedMessage)
.anyMatch(message -> message.contains("规则参数缺失")
&& message.contains("FREQUENT_CASH_DEPOSIT")));
} finally {
logger.detachAppender(appender);
}
}
```
- [ ] **Step 2: Run test to verify it fails**
Run:
```bash
mvn test -pl ccdi-project -am -Dtest=BankTagRuleConfigResolverTest#resolve_shouldLogThresholdSourceAndMissingParams
```
Expected:
- `FAIL`
- 原因是参数解析器尚未输出来源和缺失日志
- [ ] **Step 3: Write minimal implementation**
在参数解析器中补日志,并显式计算缺失参数集合:
```java
log.info("【流水标签】解析规则参数: projectId={}, effectiveProjectId={}, ruleCode={}, requiredParams={}",
projectId, effectiveProjectId, ruleMeta.getRuleCode(), requiredParamCodes);
log.debug("【流水标签】规则参数解析结果: projectId={}, ruleCode={}, thresholdValues={}",
projectId, ruleMeta.getRuleCode(), thresholdValues);
if (!missingParamCodes.isEmpty()) {
log.warn("【流水标签】规则参数缺失: projectId={}, ruleCode={}, missingParams={}",
projectId, ruleMeta.getRuleCode(), missingParamCodes);
}
```
不要在这里改变现有返回语义;本任务只补日志,不新增抛错逻辑。
- [ ] **Step 4: Run test to verify it passes**
Run:
```bash
mvn test -pl ccdi-project -am -Dtest=BankTagRuleConfigResolverTest#resolve_shouldLogThresholdSourceAndMissingParams
```
Expected:
- `PASS`
- [ ] **Step 5: Commit**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolver.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolverTest.java
git commit -m "test: 补充流水标签参数解析日志"
```
### Task 5: 跑回归并整理最终提交
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiBankTagController.java`
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinator.java`
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java`
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolver.java`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiBankTagControllerTest.java`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinatorTest.java`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImplTest.java`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolverTest.java`
- [ ] **Step 1: Run focused regression tests**
Run:
```bash
mvn test -pl ccdi-project -am -Dtest=CcdiBankTagControllerTest,CcdiFileUploadServiceImplTest,ProjectBankTagRebuildCoordinatorTest,CcdiBankTagServiceImplTest,BankTagRuleConfigResolverTest
```
Expected:
- `PASS`
- 所有日志相关测试通过
- [ ] **Step 2: Run module compile to catch logging import or signature regressions**
Run:
```bash
mvn clean compile -pl ccdi-project -am -DskipTests
```
Expected:
- `BUILD SUCCESS`
- [ ] **Step 3: Review final diff**
Run:
```bash
git diff -- ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiBankTagController.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinator.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolver.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiBankTagControllerTest.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinatorTest.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImplTest.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolverTest.java
```
Expected:
- 只包含本次日志相关改动
- 没有引入敏感字段明文打印
- [ ] **Step 4: Commit**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiBankTagController.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinator.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolver.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiBankTagControllerTest.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/ProjectBankTagRebuildCoordinatorTest.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImplTest.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolverTest.java
git commit -m "feat: 补充流水标签详细日志"
```