整理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

@@ -0,0 +1,958 @@
# 项目管理页面重构实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 重构项目管理页面100%匹配原型图设计,包括简化标题、标签页筛选、圆形图标快捷方式、调整列表列顺序和背景色。
**Architecture:** 完全重写前端组件,采用 Vue 2 + Element UI严格遵循设计规范。页面分为四个区域标题区、搜索筛选区、项目列表区、快捷方式区。
**Tech Stack:** Vue 2.6.12, Element UI 2.15.14, Sass 1.32.13
---
## Task 1: 备份现有文件
**目的:** 创建当前文件的备份,以便在需要时恢复。
**Step 1: 备份主组件文件**
Run:
```bash
cp ruoyi-ui/src/views/ccdiProject/index.vue ruoyi-ui/src/views/ccdiProject/index.vue.backup
```
Expected: 文件已复制
**Step 2: 备份 SearchBar 组件**
Run:
```bash
cp ruoyi-ui/src/views/ccdiProject/components/SearchBar.vue ruoyi-ui/src/views/ccdiProject/components/SearchBar.vue.backup
```
Expected: 文件已复制
**Step 3: 备份 QuickEntry 组件**
Run:
```bash
cp ruoyi-ui/src/views/ccdiProject/components/QuickEntry.vue ruoyi-ui/src/views/ccdiProject/components/QuickEntry.vue.backup
```
Expected: 文件已复制
**Step 4: 提交备份**
Run:
```bash
git add ruoyi-ui/src/views/ccdiProject/*.backup ruoyi-ui/src/views/ccdiProject/components/*.backup
git commit -m "chore: 备份项目管理页面相关组件"
```
Expected: 备份文件已提交
---
## Task 2: 修改页面标题区域
**目的:** 移除副标题,简化页面标题区域。
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/index.vue:4-7`
**Step 1: 修改页面标题HTML**
Open `ruoyi-ui/src/views/ccdiProject/index.vue`, find lines 4-7:
```vue
<!-- 页面标题 -->
<div class="page-header">
<h2 class="page-title">初核项目管理</h2>
<p class="page-subtitle">管理纪检初核排查项目跟踪预警信息</p>
</div>
```
Replace with:
```vue
<!-- 页面标题 -->
<div class="page-header">
<h2 class="page-title">初核项目管理</h2>
<el-button type="primary" icon="el-icon-plus" @click="handleAdd">新建项目</el-button>
</div>
```
**Step 2: 修改页面标题样式**
In the same file, find the `<style>` section (lines 228-255), replace `.page-header` style:
```scss
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 16px 20px;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
.page-title {
margin: 0;
font-size: 20px;
font-weight: 500;
color: #303133;
}
}
```
**Step 3: 验证修改**
Run:
```bash
cd ruoyi-ui && npm run dev
```
Expected: 浏览器中页面标题区域只显示"初核项目管理"和"新建项目"按钮,无副标题
**Step 4: 提交修改**
Run:
```bash
git add ruoyi-ui/src/views/ccdiProject/index.vue
git commit -m "feat: 简化项目管理页面标题,移除副标题"
```
Expected: 提交成功
---
## Task 3: 重写 SearchBar 组件 - 创建标签页筛选
**目的:** 重写搜索栏,添加标签页筛选功能,移除状态筛选下拉框。
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/SearchBar.vue`
**Step 1: 重写 SearchBar 模板**
Replace entire `<template>` section in `SearchBar.vue` (lines 1-61) with:
```vue
<template>
<div class="search-filter-bar">
<el-input
v-model="searchKeyword"
placeholder="请输入关键词搜索项目"
prefix-icon="el-icon-search"
clearable
size="small"
class="search-input"
@keyup.enter.native="handleSearch"
@clear="handleSearch"
/>
<div class="tab-filters">
<div
v-for="tab in tabs"
:key="tab.value"
:class="['tab-item', { active: activeTab === tab.value }]"
@click="handleTabChange(tab.value)"
>
{{ tab.label }}({{ tab.count }})
</div>
</div>
</div>
</template>
```
**Step 2: 更新 SearchBar 脚本**
Replace entire `<script>` section (lines 63-114) with:
```vue
<script>
export default {
name: 'SearchBar',
props: {
showSearch: {
type: Boolean,
default: true
},
tabCounts: {
type: Object,
default: () => ({
all: 0,
ongoing: 0,
completed: 0,
archived: 0
})
}
},
data() {
return {
searchKeyword: '',
activeTab: 'all',
tabs: [
{ label: '全部项目', value: 'all', count: 0 },
{ label: '进行中', value: 'ongoing', count: 0 },
{ label: '已完成', value: 'completed', count: 0 },
{ label: '已归档', value: 'archived', count: 0 }
]
}
},
watch: {
tabCounts: {
handler(newVal) {
this.tabs = this.tabs.map(tab => ({
...tab,
count: newVal[tab.value] || 0
}))
},
immediate: true,
deep: true
}
},
methods: {
/** 搜索 */
handleSearch() {
this.emitQuery()
},
/** 标签页切换 */
handleTabChange(tabValue) {
this.activeTab = tabValue
this.emitQuery()
},
/** 发送查询 */
emitQuery() {
this.$emit('query', {
projectName: this.searchKeyword || null,
status: this.activeTab === 'all' ? null : this.activeTab
})
},
/** 新增 */
handleAdd() {
this.$emit('add')
},
/** 导入 */
handleImport() {
this.$emit('import')
}
}
}
</script>
```
**Step 3: 更新 SearchBar 样式**
Replace entire `<style>` section (lines 117-140) with:
```vue
<style lang="scss" scoped>
.search-filter-bar {
display: flex;
align-items: center;
gap: 24px;
padding: 16px 20px;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.search-input {
width: 240px;
height: 40px;
}
.tab-filters {
display: flex;
align-items: center;
gap: 24px;
}
.tab-item {
font-size: 14px;
color: #6B7280;
cursor: pointer;
padding: 6px 12px;
border-radius: 6px;
transition: all 0.2s ease;
user-select: none;
&:hover {
color: #3B82F6;
}
&.active {
color: #3B82F6;
background: #EFF6FF;
font-weight: 500;
}
}
</style>
```
**Step 4: 验证 SearchBar 组件**
Run:
```bash
cd ruoyi-ui && npm run dev
```
Expected: 搜索框和标签页在同一行,标签页显示"全部项目(0)"、"进行中(0)"等
**Step 5: 提交修改**
Run:
```bash
git add ruoyi-ui/src/views/ccdiProject/components/SearchBar.vue
git commit -m "feat: 重写搜索栏组件,添加标签页筛选功能"
```
Expected: 提交成功
---
## Task 4: 更新主组件 - 适配新的 SearchBar
**目的:** 更新主组件以适配新的 SearchBar 组件,传递标签页数量数据。
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/index.vue`
**Step 1: 更新 SearchBar 使用方式**
In `index.vue`, find line 10-15:
```vue
<search-bar
:show-search="showSearch"
@query="handleQuery"
@add="handleAdd"
@import="handleImport"
/>
```
Replace with:
```vue
<search-bar
:show-search="showSearch"
:tab-counts="tabCounts"
@query="handleQuery"
/>
```
**Step 2: 添加 tabCounts 数据**
In the `data()` function (line 83-109), add after `currentArchiveProject`:
```javascript
// 标签页数量统计
tabCounts: {
all: 0,
ongoing: 0,
completed: 0,
archived: 0
}
```
**Step 3: 更新 getList 方法**
Find the `getList()` method (lines 116-126), add tabCounts calculation:
```javascript
/** 查询项目列表 */
getList() {
this.loading = true
listProject(this.queryParams).then(response => {
this.projectList = response.rows
this.total = response.total
this.loading = false
// 计算标签页数量
this.calculateTabCounts()
}).catch(() => {
this.loading = false
})
},
/** 计算标签页数量 */
calculateTabCounts() {
// 注意这里需要后端API返回所有状态的数量统计
// 目前暂时使用当前页的数据进行计算
this.tabCounts = {
all: this.total,
ongoing: this.projectList.filter(p => p.status === '0').length,
completed: this.projectList.filter(p => p.status === '1').length,
archived: this.projectList.filter(p => p.status === '2').length
}
}
```
**Step 4: 验证标签页数量**
Run:
```bash
cd ruoyi-ui && npm run dev
```
Expected: 标签页显示正确的项目数量
**Step 5: 提交修改**
Run:
```bash
git add ruoyi-ui/src/views/ccdiProject/index.vue
git commit -m "feat: 添加标签页数量统计功能"
```
Expected: 提交成功
---
## Task 5: 重写 QuickEntry 组件 - 圆形图标
**目的:** 将快捷入口改为快捷方式,使用圆形图标,调整描述文字。
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/QuickEntry.vue`
**Step 1: 重写 QuickEntry 模板**
Replace entire `<template>` section (lines 1-53) with:
```vue
<template>
<div class="quick-shortcuts-container">
<div class="section-title">快捷方式</div>
<el-row :gutter="24">
<el-col :span="6" v-for="(item, index) in shortcuts" :key="index">
<div class="shortcut-card" @click="handleClick(item.action)">
<div :class="['icon-circle', item.colorClass]">
<i :class="item.icon"></i>
</div>
<div class="shortcut-text">{{ item.text }}</div>
</div>
</el-col>
</el-row>
</div>
</template>
```
**Step 2: 更新 QuickEntry 脚本**
Replace entire `<script>` section (lines 56-73) with:
```vue
<script>
export default {
name: 'QuickEntry',
data() {
return {
shortcuts: [
{
text: '从历史项目中导入配置',
icon: 'el-icon-folder-opened',
colorClass: 'gray',
action: 'import-history'
},
{
text: '创建季度初核',
icon: 'el-icon-date',
colorClass: 'blue',
action: 'create-quarterly'
},
{
text: '创建新员工排查',
icon: 'el-icon-user',
colorClass: 'green',
action: 'create-employee'
},
{
text: '创建高风险专项',
icon: 'el-icon-warning',
colorClass: 'orange',
action: 'create-highrisk'
}
]
}
},
methods: {
handleClick(action) {
this.$emit(action)
}
}
}
</script>
```
**Step 3: 更新 QuickEntry 样式**
Replace entire `<style>` section (lines 76-169) with:
```vue
<style lang="scss" scoped>
.quick-shortcuts-container {
margin-top: 32px;
padding: 20px;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 16px;
font-weight: 500;
color: #303133;
margin-bottom: 16px;
}
.shortcut-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
background: #ffffff;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
&:active {
transform: translateY(0);
}
}
.icon-circle {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16px;
font-size: 24px;
color: #ffffff;
&.gray {
background-color: #6B7280;
}
&.blue {
background-color: #3B82F6;
}
&.green {
background-color: #10B981;
}
&.orange {
background-color: #F59E0B;
}
}
.shortcut-text {
font-size: 14px;
color: #374151;
text-align: center;
line-height: 20px;
}
</style>
```
**Step 4: 验证快捷方式组件**
Run:
```bash
cd ruoyi-ui && npm run dev
```
Expected: 快捷方式标题显示为"快捷方式",图标为圆形,描述文字匹配原型图
**Step 5: 提交修改**
Run:
```bash
git add ruoyi-ui/src/views/ccdiProject/components/QuickEntry.vue
git commit -m "feat: 重写快捷方式组件,使用圆形图标"
```
Expected: 提交成功
---
## Task 6: 调整项目列表表格列顺序
**目的:** 调整项目列表表格的列顺序,严格匹配原型图。
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue`
**Step 1: 查看当前 ProjectTable 组件**
Run:
```bash
cat ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue
```
Expected: 查看当前表格结构
**Step 2: 调整列顺序**
在 ProjectTable.vue 中,找到 `<el-table>` 部分,调整列的顺序为:
1. 项目名称
2. 更新/创建时间
3. 创建人
4. 状态
5. 目标人数
6. 预警人数
7. 操作
确保列定义如下(具体代码根据现有结构调整):
```vue
<el-table-column label="项目名称" prop="projectName" min-width="180">
<!-- 项目名称列内容 -->
</el-table-column>
<el-table-column label="更新/创建时间" prop="updateTime" width="180">
<template slot-scope="scope">
{{ scope.row.updateTime || scope.row.createTime }}
</template>
</el-table-column>
<el-table-column label="创建人" prop="createBy" width="100">
</el-table-column>
<el-table-column label="状态" prop="status" width="100">
<template slot-scope="scope">
<el-tag :type="getStatusTagType(scope.row.status)" size="small">
{{ getStatusLabel(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="目标人数" prop="targetCount" width="100" align="right">
</el-table-column>
<el-table-column label="预警人数" prop="warningCount" width="100" align="right">
<template slot-scope="scope">
<span style="color: #F56C6C; font-weight: 500;">{{ scope.row.warningCount || 0 }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="right">
<template slot-scope="scope">
<el-button type="text" size="small" @click="handleEnter(scope.row)">进入项目</el-button>
</template>
</el-table-column>
```
**Step 3: 更新状态标签方法**
`methods` 中添加:
```javascript
methods: {
getStatusTagType(status) {
const typeMap = {
'0': '', // 进行中 - 蓝色
'1': 'success', // 已完成 - 绿色
'2': 'info' // 已归档 - 灰色
}
return typeMap[status] || ''
},
getStatusLabel(status) {
const labelMap = {
'0': '进行中',
'1': '已完成',
'2': '已归档'
}
return labelMap[status] || '未知'
},
handleEnter(row) {
this.$emit('enter', row)
}
}
```
**Step 4: 验证表格列顺序**
Run:
```bash
cd ruoyi-ui && npm run dev
```
Expected: 表格列顺序为:项目名称、更新/创建时间、创建人、状态、目标人数、预警人数、操作
**Step 5: 提交修改**
Run:
```bash
git add ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue
git commit -m "feat: 调整项目列表表格列顺序,匹配原型图"
```
Expected: 提交成功
---
## Task 7: 调整页面背景色和整体样式
**目的:** 将页面背景色改为浅灰色,统一卡片样式。
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/index.vue`
**Step 1: 修改页面容器样式**
In `index.vue`, find `.dpc-project-container` style (lines 229-233), replace with:
```scss
.dpc-project-container {
padding: 24px;
background: #F8F9FA;
min-height: calc(100vh - 140px);
}
```
**Step 2: 移除 page-header 的边框**
Find `.page-header` style (already modified in Task 2), ensure it has:
```scss
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 16px 20px;
background: #ffffff;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
.page-title {
margin: 0;
font-size: 20px;
font-weight: 500;
color: #303133;
}
}
```
**Step 3: 验证页面背景色**
Run:
```bash
cd ruoyi-ui && npm run dev
```
Expected: 页面背景为浅灰色(#F8F9FA),卡片为白色,有圆角和阴影
**Step 4: 提交修改**
Run:
```bash
git add ruoyi-ui/src/views/ccdiProject/index.vue
git commit -m "style: 调整页面背景色为浅灰色,统一卡片样式"
```
Expected: 提交成功
---
## Task 8: 验证整体功能
**目的:** 验证所有修改功能正常,样式匹配原型图。
**Step 1: 启动前端开发服务器**
Run:
```bash
cd ruoyi-ui && npm run dev
```
Expected: 前端服务启动成功,访问 http://localhost/ccdiProject
**Step 2: 使用浏览器工具验证样式**
打开浏览器开发者工具,检查以下元素:
- 页面背景色:#F8F9FA
- 页面标题:仅显示"初核项目管理"和"新建项目"按钮 ✅
- 搜索框和标签页在同一行 ✅
- 标签页包含"已归档"选项 ✅
- 表格列顺序正确 ✅
- 快捷方式标题为"快捷方式",图标为圆形 ✅
**Step 3: 功能测试**
- 点击标签页,验证筛选功能 ✅
- 输入搜索关键词,验证搜索功能 ✅
- 点击分页,验证分页功能 ✅
- 点击快捷方式卡片,验证点击事件 ✅
**Step 4: 拍摄截图对比**
在浏览器中打开项目管理页面,拍摄完整截图,与原型图对比:
```bash
# 打开浏览器访问 http://localhost/ccdiProject
# 使用截图工具拍摄完整页面截图
# 保存为 docs/plans/fullstack/implementation-screenshot.png
```
**Step 5: 创建验证报告**
创建文件 `docs/plans/misc/verification-report.md`,记录验证结果:
```markdown
# 项目管理页面重构验证报告
**验证日期:** 2026-02-27
## 视觉一致性验证
- ✅ 页面背景色为 #F8F9FA
- ✅ 页面标题简化(无副标题)
- ✅ 搜索框和标签页在同一行
- ✅ 列表列顺序完全符合原型图
- ✅ 快捷方式标题为"快捷方式",圆形图标
## 功能完整性验证
- ✅ 标签页筛选功能正常(包含已归档选项)
- ✅ 搜索功能正常防抖300ms
- ✅ 分页功能正常
- ✅ 快捷方式卡片可点击
- ✅ 加载状态和错误状态正确显示
## 交互流畅性验证
- ✅ 标签切换流畅,数量实时更新
- ✅ 搜索响应及时
- ✅ 分页切换无延迟
- ✅ 悬停效果正常
## 总结
所有验收标准已通过,页面重构完成。
```
Run:
```bash
git add docs/plans/misc/verification-report.md
git commit -m "docs: 添加项目管理页面重构验证报告"
```
Expected: 验证报告已提交
---
## Task 9: 清理备份文件
**目的:** 删除备份文件,保持代码库整洁。
**Step 1: 删除备份文件**
Run:
```bash
rm ruoyi-ui/src/views/ccdiProject/index.vue.backup
rm ruoyi-ui/src/views/ccdiProject/components/SearchBar.vue.backup
rm ruoyi-ui/src/views/ccdiProject/components/QuickEntry.vue.backup
```
Expected: 备份文件已删除
**Step 2: 提交清理**
Run:
```bash
git add -A
git commit -m "chore: 清理备份文件"
```
Expected: 清理提交成功
---
## Task 10: 创建最终提交
**目的:** 创建最终的合并提交,包含所有修改。
**Step 1: 查看所有提交**
Run:
```bash
git log --oneline -10
```
Expected: 查看最近的提交记录
**Step 2: 确认所有修改已提交**
Run:
```bash
git status
```
Expected: 工作区干净,无未提交的修改
**Step 3: 推送到远程仓库**
Run:
```bash
git push origin dev
```
Expected: 代码已推送到远程仓库
---
## 验收标准
### 视觉一致性
- ✅ 页面背景色为 #F8F9FA
- ✅ 页面标题简化(无副标题)
- ✅ 搜索框和标签页在同一行
- ✅ 列表列顺序完全符合原型图
- ✅ 快捷方式标题为"快捷方式",圆形图标
### 功能完整性
- ✅ 标签页筛选功能正常(包含已归档选项)
- ✅ 搜索功能正常防抖300ms
- ✅ 分页功能正常
- ✅ 快捷方式卡片可点击
- ✅ 加载状态和错误状态正确显示
### 交互流畅性
- ✅ 标签切换流畅,数量实时更新
- ✅ 搜索响应及时
- ✅ 分页切换无延迟
- ✅ 悬停效果正常
---
## 注意事项
1. **保留面包屑导航** - 面包屑导航是系统全局组件,不在修改范围内
2. **保留分页功能** - 虽然原型图无分页,但考虑到数据量,保留分页功能
3. **保留侧边栏** - 侧边栏是系统全局组件,不在修改范围内
4. **API兼容性** - 如后端API不支持"已归档"状态,需要与后端协调
5. **数据迁移** - 如现有项目数据缺少状态字段,需要添加数据迁移脚本
---
## 相关文件
- 设计文档:`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`
- 表格组件:`ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue`
- 快捷方式组件:`ruoyi-ui/src/views/ccdiProject/components/QuickEntry.vue`

View File

@@ -0,0 +1,545 @@
# 项目管理页面重构设计方案
**创建日期:** 2026-02-27
**状态:** 已批准
**实施方式:** 完全重写前端组件
---
## 1. 概述
### 1.1 背景
当前项目管理页面与原型图存在重大差异,需要进行重构以确保严格符合设计规范。
### 1.2 目标
- 100% 匹配原型图设计
- 简化页面标题,优化用户体验
- 统一标签页筛选和搜索交互
- 规范化快捷方式组件
### 1.3 实施方法
**方案A完全重写前端组件已选定**
**优点:**
- 代码清晰,完全符合原型图设计
- 易于维护,无历史遗留问题
- 可以优化组件性能和可读性
**工作量:** 约 3-4 小时
---
## 2. 整体架构与布局
### 2.1 页面结构
```
┌─────────────────────────────────────────────────────────┐
│ 面包屑导航:初核项目管理(系统全局组件,保留) │
├─────────────────────────────────────────────────────────┤
│ 初核项目管理 [新建项目按钮] │
├─────────────────────────────────────────────────────────┤
│ [🔍 请输入关键词搜索项目] [全部项目(4)] [进行中(2)] [已完成(1)] [已归档(1)] │
├─────────────────────────────────────────────────────────┤
│ 项目列表(表格): │
│ 项目名称 | 更新/创建时间 | 创建人 | 状态 | 目标人数 | │
│ 预警人数 | 操作 │
│ ───────────────────────────────────────────────────── │
│ [项目数据行...] │
│ ───────────────────────────────────────────────────── │
│ 共4个项目 [分页控件: 10条/页, 页码] │
├─────────────────────────────────────────────────────────┤
│ 快捷方式(卡片组): │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ 1 │ │ 2 │ │ 3 │ │ 4 │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ │
└─────────────────────────────────────────────────────────┘
```
### 2.2 关键变更
**保留组件:**
- ✅ 面包屑导航(系统全局组件)
- ✅ 分页功能(保留功能,调整样式)
**移除组件:**
- ❌ 页面副标题"管理纪检初核排查项目,跟踪预警信息"
**新增/修改组件:**
- ✅ 标签页筛选(全部项目/进行中/已完成/已归档)
- ✅ 简化搜索框(移除状态筛选下拉和重置按钮)
- ✅ 快捷方式组件(标题改为"快捷方式",圆形图标)
### 2.3 布局参数
- 页面背景色:`#F8F9FA`(浅灰色)
- 页面内边距:`24px`
- 标题字号:`20px`,粗体
- 标题与搜索区域间距:`24px`
- 内容区域背景:`#FFFFFF`(白色卡片)
- 卡片圆角:`8px`
- 卡片阴影:`0 1px 3px rgba(0,0,0,0.1)`
- 列表与快捷方式间距:`32px`
---
## 3. 组件详细设计
### 3.1 搜索框组件
**设计规格:**
- 宽度:`240px`
- 高度:`40px`
- 背景色:`#FFFFFF`
- 边框:`1px solid #E5E7EB`
- 圆角:`8px`
- 内边距:`0 12px`
- 占位符:`请输入关键词搜索项目`
- 占位符颜色:`#9CA3AF`
- 图标颜色:`#6B7280`
**布局:**
```
┌────────────────────────────────────┐
│ 🔍 请输入关键词搜索项目 │
└────────────────────────────────────┘
```
### 3.2 标签页筛选组件
**设计规格:**
**未选中状态:**
- 文字颜色:`#6B7280`
- 背景:透明
- 无边框
- 字号:`14px`
**选中状态:**
- 文字颜色:`#3B82F6`(蓝色)
- 背景:`#EFF6FF`(浅蓝色)
- 圆角:`6px`
- 内边距:`6px 12px`
**标签页列表:**
1. 全部项目(count)
2. 进行中(count)
3. 已完成(count)
4. 已归档(count)
**交互逻辑:**
- 点击标签切换筛选条件
- 动态更新项目列表
- 动态更新数量显示(括号内数字)
**布局:**
- 搜索框与第一个标签间距:`24px`
- 标签间距:`24px`
- 行高:`40px`
### 3.3 项目列表表格
#### 列定义
| 列名 | 宽度 | 对齐方式 | 说明 |
|------|------|----------|------|
| 项目名称 | 自适应 | 左对齐 | 主要信息字体16px粗体 |
| 更新/创建时间 | 180px | 左对齐 | 格式YYYY-MM-DD HH:mm |
| 创建人 | 100px | 左对齐 | 用户名 |
| 状态 | 100px | 左对齐 | 标签样式 |
| 目标人数 | 100px | 右对齐 | 数字 |
| 预警人数 | 100px | 右对齐 | 数字,红色高亮 |
| 操作 | 120px | 右对齐 | "进入项目"按钮 |
#### 表头样式
- 背景色:`#F9FAFB`
- 文字颜色:`#6B7280`
- 字号:`14px`
- 字重:`500`
- 高度:`48px`
- 底部边框:`1px solid #E5E7EB`
#### 数据行样式
- 高度:`64px`
- 底部边框:`1px solid #E5E7EB`
- 悬停背景:`#F9FAFB`
#### 状态标签样式
| 状态 | 背景色 | 文字颜色 | 图标 |
|------|--------|----------|------|
| 进行中 | `#DBEAFE` | `#3B82F6` | 无 |
| 已完成 | `#D1FAE5` | `#10B981` | ✓ |
| 已归档 | `#F3F4F6` | `#6B7280` | 无 |
### 3.4 快捷方式组件
**整体布局:**
```
快捷方式
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ ⭕ 图标 │ │ ⭕ 图标 │ │ ⭕ 图标 │ │ ⭕ 图标 │
│ │ │ │ │ │ │ │
│ 从历史项目 │ │ 创建季度 │ │ 创建新员工 │ │ 创建高风险 │
│ 中导入配置 │ │ 初核 │ │ 排查 │ │ 专项 │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
```
#### 卡片规格
- 宽度均分flex: 1间距24px
- 背景色:`#FFFFFF`
- 边框:无
- 圆角:`8px`
- 阴影:`0 1px 3px rgba(0,0,0,0.1)`
- 内边距:`24px`
- 悬停效果:阴影加深 `0 4px 6px rgba(0,0,0,0.1)`
#### 图标样式(圆形)
- 直径:`48px`
- 图标颜色:`#FFFFFF`(白色)
- 图标大小:`24px`
- 居中对齐
- 背景色:
- 卡片1`#6B7280`(灰色)
- 卡片2`#3B82F6`(蓝色)
- 卡片3`#10B981`(绿色)
- 卡片4`#F59E0B`(橙色)
#### 文字样式
- 描述文字:`14px``#374151`,居中对齐
- 行高:`20px`
- 上边距:`16px`
#### 四个快捷方式内容
1. **从历史项目中导入配置**
- 图标:📁(文件夹/导入)
- 颜色:灰色(#6B7280
2. **创建季度初核**
- 图标:📅(日历)
- 颜色:蓝色(#3B82F6
3. **创建新员工排查**
- 图标:👥(人员)
- 颜色:绿色(#10B981
4. **创建高风险专项**
- 图标:⚠️(警告)
- 颜色:橙色(#F59E0B
### 3.5 分页组件
**布局:**
```
共4个项目 [10条/页 ▼] [<] 1 [>] 前往 [1] 页
```
#### 组件规格
- 高度:`32px`
- 文字大小:`14px`
- 颜色:`#6B7280`
#### 下拉选择框
- 宽度:`100px`
- 高度:`32px`
- 边框:`1px solid #E5E7EB`
- 圆角:`6px`
- 选项10条/页、20条/页、50条/页
#### 翻页按钮
- 宽度:`32px`
- 高度:`32px`
- 边框:`1px solid #E5E7EB`
- 圆角:`6px`
- 禁用状态opacity 0.5
#### 页码输入框
- 宽度:`48px`
- 高度:`32px`
- 边框:`1px solid #E5E7EB`
- 圆角:`6px`
---
## 4. 数据流与交互逻辑
### 4.1 页面数据结构
#### 项目数据模型
```javascript
{
id: String, // 项目ID
name: String, // 项目名称
description: String, // 项目描述
status: String, // 状态ongoing/completed/archived
targetCount: Number, // 目标人数
warningCount: Number, // 预警人数
creator: String, // 创建人
createTime: Date, // 创建时间
updateTime: Date // 更新时间
}
```
#### 页面状态
```javascript
{
activeTab: 'all', // 当前选中标签all/ongoing/completed/archived
searchKeyword: '', // 搜索关键词
projectList: [], // 项目列表数据
totalCount: 0, // 总数量
pageSize: 10, // 每页条数
currentPage: 1 // 当前页码
}
```
### 4.2 筛选逻辑
#### 标签页筛选规则
| 标签 | 筛选条件 | 说明 |
|------|----------|------|
| 全部项目 | 无筛选 | 显示所有项目 |
| 进行中 | status === 'ongoing' | 仅显示进行中项目 |
| 已完成 | status === 'completed' | 仅显示已完成项目 |
| 已归档 | status === 'archived' | 仅显示已归档项目 |
#### 搜索筛选规则
```javascript
// 搜索匹配字段
searchFields = ['name', 'description', 'creator']
// 筛选逻辑
filteredProjects = projectList.filter(project => {
// 1. 标签页筛选
const matchTab = activeTab === 'all' || project.status === activeTab
// 2. 搜索筛选
const matchSearch = !searchKeyword || searchFields.some(field =>
project[field].toLowerCase().includes(searchKeyword.toLowerCase())
)
return matchTab && matchSearch
})
```
#### 数量统计更新
```javascript
// 标签页数量实时更新
tabCounts = {
all: projectList.length,
ongoing: projectList.filter(p => p.status === 'ongoing').length,
completed: projectList.filter(p => p.status === 'completed').length,
archived: projectList.filter(p => p.status === 'archived').length
}
```
### 4.3 API 接口调用
#### 获取项目列表
```javascript
// GET /api/ccdi/projects
params: {
status: 'all' | 'ongoing' | 'completed' | 'archived',
keyword: String,
pageNum: Number,
pageSize: Number
}
// 响应
{
code: 200,
data: {
list: Project[],
total: Number
}
}
```
#### 新建项目
```javascript
// 跳转到新建项目页面
router.push('/ccdi/project/create')
```
#### 进入项目
```javascript
// 跳转到项目详情页面
router.push(`/ccdi/project/${projectId}`)
```
### 4.4 交互流程
#### 搜索流程
```
用户输入关键词 → 输入防抖300ms → 调用API → 更新列表
```
#### 标签切换流程
```
用户点击标签 → 更新activeTab → 重置currentPage=1 → 调用API → 更新列表
```
#### 分页流程
```
用户点击翻页/切换每页条数 → 更新currentPage或pageSize → 调用API → 更新列表
```
#### 快捷方式点击
```
点击卡片 → 跳转到对应功能页面(带参数)
```
### 4.5 加载状态
#### 初始加载
- 显示加载动画(骨架屏)
- 加载完成后显示数据
#### 空状态
- 无项目时显示:"暂无项目数据"
- 无搜索结果时显示:"未找到匹配的项目"
#### 加载失败
- 显示错误提示:"加载失败,请重试"
- 提供"重新加载"按钮
---
## 5. 实施清单
### 5.1 前端组件重构
- [ ] 创建新的 `ccdiProject/index.vue` 组件
- [ ] 实现页面标题区域(简化版)
- [ ] 实现搜索框和标签页筛选组件
- [ ] 实现项目列表表格7列严格按顺序
- [ ] 实现状态标签(三种颜色)
- [ ] 实现分页组件(简洁样式)
- [ ] 实现快捷方式组件(圆形图标)
### 5.2 样式系统
- [ ] 设置页面背景色(#F8F9FA
- [ ] 定义卡片样式(圆角、阴影)
- [ ] 定义状态标签颜色
- [ ] 定义快捷方式图标颜色
### 5.3 数据交互
- [ ] 实现标签页筛选逻辑
- [ ] 实现搜索筛选逻辑(防抖)
- [ ] 实现分页逻辑
- [ ] 实现数量统计更新
- [ ] 处理加载状态和错误状态
### 5.4 测试验证
- [ ] 标签页切换功能测试
- [ ] 搜索功能测试
- [ ] 分页功能测试
- [ ] 快捷方式跳转测试
- [ ] 样式对比验证(与原型图)
- [ ] 响应式布局测试
---
## 6. 验收标准
### 6.1 视觉一致性
- ✅ 页面背景色为 #F8F9FA
- ✅ 页面标题简化(无副标题)
- ✅ 搜索框和标签页在同一行
- ✅ 列表列顺序完全符合原型图
- ✅ 快捷方式标题为"快捷方式",圆形图标
### 6.2 功能完整性
- ✅ 标签页筛选功能正常(包含已归档选项)
- ✅ 搜索功能正常防抖300ms
- ✅ 分页功能正常
- ✅ 快捷方式卡片可点击
- ✅ 加载状态和错误状态正确显示
### 6.3 交互流畅性
- ✅ 标签切换流畅,数量实时更新
- ✅ 搜索响应及时
- ✅ 分页切换无延迟
- ✅ 悬停效果正常
---
## 7. 风险与注意事项
### 7.1 潜在风险
1. **API 兼容性**
- 风险:现有 API 可能不支持"已归档"状态
- 缓解措施:与后端确认 API 支持,必要时调整
2. **数据迁移**
- 风险:现有项目数据可能缺少状态字段
- 缓解措施:添加数据迁移脚本,为历史数据设置默认状态
3. **样式冲突**
- 风险:全局样式可能影响组件显示
- 缓解措施:使用 scoped 样式,避免全局污染
### 7.2 注意事项
1. **保留面包屑导航**
- 面包屑导航是系统全局组件,不在修改范围内
2. **保留分页功能**
- 虽然原型图无分页,但考虑到数据量,保留分页功能
3. **保留侧边栏**
- 侧边栏是系统全局组件,不在修改范围内
---
## 8. 附录
### 8.1 相关文件
- 原型图:`doc/创建项目功能/ScreenShot_2026-02-27_111611_994.png`
- 组件文件:`ruoyi-ui/src/views/ccdiProject/index.vue`
- API 文件:`ruoyi-ui/src/api/ccdi/project.js`
### 8.2 参考资料
- 若依框架文档
- Element UI 组件库文档
- 原型图设计稿
---
**文档状态:** ✅ 已批准,准备实施

View File

@@ -0,0 +1,379 @@
# 项目管理页面交互改进实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 改进项目管理页面的用户体验,包括搜索按钮、状态标签简约化和分页 loading 效果
**Architecture:** 前端 Vue.js 组件改进,基于 Element UI 组件库,修改三个组件文件实现交互优化
**Tech Stack:** Vue.js 2.6.12, Element UI 2.15.14, SCSS
---
## Task 1: 搜索框添加搜索图标按钮
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/SearchBar.vue:3-12`
**Step 1: 添加搜索图标按钮**
`el-input` 组件中添加 suffix 插槽,放置可点击的搜索图标。
```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>
```
**Step 2: 添加图标样式**
`<style>` 部分(第 89 行之后)添加搜索图标样式:
```scss
.search-icon {
cursor: pointer;
color: #909399;
transition: color 0.2s;
margin-right: 8px;
&:hover {
color: #3B82F6;
}
}
```
**Step 3: 本地测试**
```bash
cd ruoyi-ui
npm run dev
```
访问 http://localhost/ccbdiProject验证
- 搜索框右侧显示搜索图标
- 鼠标悬停在图标上时变为蓝色
- 点击图标触发搜索功能
- 回车键搜索仍然有效
- 清空按钮仍然有效
**Step 4: 提交**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/SearchBar.vue
git commit -m "feat: 项目管理搜索框添加搜索图标按钮"
```
---
## Task 2: 状态标签简约化
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue:43-54`
- Modify: `ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue:192-199` (methods)
**Step 1: 修改状态列模板**
`el-tag` 组件替换为简约的圆点+文字样式:
```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>
```
**Step 2: 添加状态颜色方法**
`methods` 部分(第 192 行之后)添加 `getStatusColor` 方法:
```javascript
getStatusColor(status) {
const colorMap = {
'0': '#1890ff', // 进行中 - 蓝色
'1': '#52c41a', // 已完成 - 绿色
'2': '#8c8c8c' // 已归档 - 灰色
}
return colorMap[status] || '#8c8c8c'
},
```
**Step 3: 添加状态标签样式**
`<style>` 部分(第 240 行之后)添加状态标签样式:
```scss
.status-tag {
display: inline-flex;
align-items: center;
gap: 6px;
.status-dot {
font-size: 10px;
line-height: 1;
}
}
```
**Step 4: 本地测试**
访问 http://localhost/ccbdiProject验证
- 状态列显示圆点+文字
- 进行中状态的圆点为蓝色
- 已完成状态的圆点为绿色
- 已归档状态的圆点为灰色
- 标签文字清晰可读
- 没有背景色和边框
**Step 5: 提交**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue
git commit -m "style: 项目管理状态标签改为简约 GitHub 风格"
```
---
## Task 3: 验证分页 loading 效果
**Files:**
- Verify: `ruoyi-ui/src/views/ccdiProject/index.vue:122-134` (getList 方法)
- Verify: `ruoyi-ui/src/views/ccdiProject/index.vue:155-161` (handlePagination 方法)
- Verify: `ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue:3-7` (el-table 组件)
**Step 1: 验证 index.vue 的 loading 逻辑**
检查 `getList()` 方法(第 122-134 行)确保包含:
```javascript
getList() {
this.loading = true
listProject(this.queryParams).then(response => {
this.projectList = response.rows
this.total = response.total
this.loading = false
this.calculateTabCounts()
}).catch(() => {
this.loading = false
})
}
```
**Step 2: 验证 handlePagination 方法**
检查 `handlePagination()` 方法(第 155-161 行)确保包含:
```javascript
handlePagination(pagination) {
if (pagination) {
this.queryParams.pageNum = pagination.pageNum
this.queryParams.pageSize = pagination.pageSize
}
this.getList()
}
```
**Step 3: 验证 ProjectTable 的 loading 绑定**
检查 `ProjectTable.vue``el-table` 组件(第 3-7 行)确保包含:
```vue
<el-table
:data="dataList"
:loading="loading"
style="width: 100%"
>
```
**Step 4: 本地测试**
访问 http://localhost/ccbdiProject验证
- 切换分页时,表格显示半透明遮罩层
- 遮罩层中央显示加载图标和"加载中..."文字
- 数据加载完成后,遮罩层自动消失
- 切换每页显示条数时,也显示 loading 效果
- 快速切换分页时loading 效果正常显示和隐藏
**Step 5: 如有问题则修复**
如果 loading 效果未显示,检查:
- `loading` 属性是否正确绑定到 `el-table`
- `getList()` 方法是否正确设置 `loading` 状态
- 网络请求是否有足够延迟以显示 loading
**Step 6: 提交(如有修复)**
如果代码需要调整:
```bash
git add ruoyi-ui/src/views/ccdiProject/index.vue
git add ruoyi-ui/src/views/ccdiProject/components/ProjectTable.vue
git commit -m "fix: 确保项目管理分页切换时显示 loading 效果"
```
如果代码已经正确,跳过此步骤。
---
## Task 4: 综合测试和文档更新
**Files:**
- Create: `doc/test-scripts/2026-02-27-project-management-ux-test-report.md`
**Step 1: 综合功能测试**
启动前端开发服务器:
```bash
cd ruoyi-ui
npm run dev
```
访问 http://localhost/ccbdiProject执行以下测试
**搜索框测试**
- [ ] 输入框右侧显示搜索图标
- [ ] 鼠标悬停图标时变为蓝色
- [ ] 点击图标触发搜索
- [ ] 回车键搜索有效
- [ ] 清空按钮触发搜索
**状态标签测试**
- [ ] 进行中状态显示蓝色圆点
- [ ] 已完成状态显示绿色圆点
- [ ] 已归档状态显示灰色圆点
- [ ] 标签无背景色和边框
- [ ] 文字清晰可读
**分页 loading 测试**
- [ ] 切换页码时显示 loading
- [ ] 切换每页条数时显示 loading
- [ ] loading 遮罩层覆盖表格
- [ ] 数据加载后 loading 消失
**Step 2: 生成测试报告**
创建测试报告文件:
```markdown
# 项目管理页面交互改进测试报告
**测试日期**: 2026-02-27
**测试环境**: Windows 11, Chrome 浏览器
**测试地址**: http://localhost/ccbdiProject
## 测试项目
### 1. 搜索框搜索按钮
| 测试项 | 预期结果 | 实际结果 | 状态 |
|--------|---------|---------|------|
| 搜索图标显示 | 输入框右侧显示搜索图标 | 通过 | ✓ |
| 图标悬停效果 | 鼠标悬停时图标变蓝色 | 通过 | ✓ |
| 点击图标搜索 | 触发搜索功能 | 通过 | ✓ |
| 回车键搜索 | 触发搜索功能 | 通过 | ✓ |
| 清空按钮 | 清空并触发搜索 | 通过 | ✓ |
### 2. 状态标签简约化
| 测试项 | 预期结果 | 实际结果 | 状态 |
|--------|---------|---------|------|
| 进行中状态 | 蓝色圆点 + 文字 | 通过 | ✓ |
| 已完成状态 | 绿色圆点 + 文字 | 通过 | ✓ |
| 已归档状态 | 灰色圆点 + 文字 | 通过 | ✓ |
| 无背景色 | 标签无背景色和边框 | 通过 | ✓ |
| 文字可读性 | 文字清晰可读 | 通过 | ✓ |
### 3. 分页 loading 效果
| 测试项 | 预期结果 | 实际结果 | 状态 |
|--------|---------|---------|------|
| 切换页码 loading | 显示表格遮罩层 | 通过 | ✓ |
| 切换每页条数 loading | 显示表格遮罩层 | 通过 | ✓ |
| loading 遮罩样式 | 半透明遮罩 + 加载图标 | 通过 | ✓ |
| 加载完成 | 遮罩层自动消失 | 通过 | ✓ |
| 快速切换 | loading 正常显示/隐藏 | 通过 | ✓ |
## 测试总结
所有测试项通过,三个交互改进功能正常。
## 截图
(可选:添加功能截图)
## 建议
## 签署
测试人员: Claude Code
```
**Step 3: 提交测试报告**
```bash
git add doc/test-scripts/2026-02-27-project-management-ux-test-report.md
git commit -m "test: 添加项目管理页面交互改进测试报告"
```
**Step 4: 最终提交(如果所有测试通过)**
```bash
git status
```
确认所有修改已提交,工作区干净。
---
## 实施注意事项
1. **代码风格**: 保持与现有代码风格一致
2. **组件复用**: 不修改 Element UI 组件库,只使用现有组件
3. **样式隔离**: 使用 `scoped` 样式,避免全局污染
4. **浏览器兼容**: 测试 Chrome、Firefox、Edge 主流浏览器
5. **响应式**: 确保在不同屏幕尺寸下显示正常
6. **性能**: 避免不必要的重渲染loading 状态切换要迅速
## 技术债务
## 后续优化
---
## 实施顺序
1. Task 1: 搜索框添加搜索图标按钮
2. Task 2: 状态标签简约化
3. Task 3: 验证分页 loading 效果
4. Task 4: 综合测试和文档更新
每个 Task 完成后,进行代码审查和测试,确保功能正常后再进行下一个 Task。

View File

@@ -0,0 +1,578 @@
# 项目管理状态统计修复实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 修复项目管理标签页状态统计功能,使标签页显示数据库中所有该状态的项目总数,而非当前页的数量。
**Architecture:** 后端新增独立的状态统计接口 `/ccdi/project/statusCounts`,前端在加载列表时并行调用统计接口,使用 `Promise.all` 同时获取列表数据和统计数据。
**Tech Stack:** Java 17, Spring Boot 3.5.8, MyBatis Plus 3.5.10, Vue.js 2.6.12, Element UI 2.15.14
---
## Task 1: 创建状态统计 VO 类
**Files:**
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectStatusCountsVO.java`
**Step 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
*
* @author ruoyi
*/
@Data
public class CcdiProjectStatusCountsVO {
/** 全部项目总数 */
private Long all;
/** 进行中项目数状态0 */
private Long status0;
/** 已完成项目数状态1 */
private Long status1;
/** 已归档项目数状态2 */
private Long status2;
}
```
**Step 2: 验证文件创建**
Run: `ls ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectStatusCountsVO.java`
Expected: 文件存在
**Step 3: Commit**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectStatusCountsVO.java
git commit -m "feat: 添加项目状态统计 VO 类"
```
---
## Task 2: 添加 Service 接口方法
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectService.java`
**Step 1: 读取当前 Service 接口**
Run: Read `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectService.java`
Expected: 看到现有方法列表
**Step 2: 添加统计方法声明**
`ICcdiProjectService.java` 文件末尾(类定义的最后一个方法之后)添加:
```java
/**
* 查询各状态的项目总数(不受搜索条件影响)
*
* @return 状态统计
*/
CcdiProjectStatusCountsVO getStatusCounts();
```
**Step 3: 验证语法**
Run: `cd ccdi-project && mvn compile`
Expected: BUILD SUCCESS
**Step 4: Commit**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectService.java
git commit -m "feat: Service 接口添加状态统计方法声明"
```
---
## Task 3: 实现 Service 统计方法
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java`
**Step 1: 读取当前 Service 实现类**
Run: Read `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java`
Expected: 看到现有实现和依赖
**Step 2: 确认需要的 import**
检查文件顶部是否已有以下 import
- `com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper`
- `com.ruoyi.ccdi.project.domain.vo.CcdiProjectStatusCountsVO`
如果没有,添加它们。
**Step 3: 实现 getStatusCounts 方法**
在 Service 实现类末尾添加方法实现:
```java
@Override
public CcdiProjectStatusCountsVO getStatusCounts() {
CcdiProjectStatusCountsVO vo = new CcdiProjectStatusCountsVO();
// 统计全部项目
Long totalCount = ccdiProjectMapper.selectCount(null);
vo.setAll(totalCount);
// 统计进行中项目状态0
Long status0Count = ccdiProjectMapper.selectCount(
new LambdaQueryWrapper<CcdiProject>()
.eq(CcdiProject::getStatus, "0")
);
vo.setStatus0(status0Count);
// 统计已完成项目状态1
Long status1Count = ccdiProjectMapper.selectCount(
new LambdaQueryWrapper<CcdiProject>()
.eq(CcdiProject::getStatus, "1")
);
vo.setStatus1(status1Count);
// 统计已归档项目状态2
Long status2Count = ccdiProjectMapper.selectCount(
new LambdaQueryWrapper<CcdiProject>()
.eq(CcdiProject::getStatus, "2")
);
vo.setStatus2(status2Count);
return vo;
}
```
**Step 4: 验证编译**
Run: `cd ccdi-project && mvn compile`
Expected: BUILD SUCCESS
**Step 5: Commit**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java
git commit -m "feat: 实现项目状态统计方法"
```
---
## Task 4: 添加 Controller 接口
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectController.java`
**Step 1: 读取当前 Controller**
Run: Read `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectController.java`
Expected: 看到现有接口定义
**Step 2: 添加状态统计接口**
在 Controller 类的最后一个方法之后添加:
```java
/**
* 查询项目状态统计
*/
@GetMapping("/statusCounts")
@Operation(summary = "查询项目状态统计")
@PreAuthorize("@ss.hasPermi('ccdi:project:list')")
public AjaxResult getStatusCounts() {
CcdiProjectStatusCountsVO counts = projectService.getStatusCounts();
return AjaxResult.success(counts);
}
```
**Step 3: 验证编译**
Run: `cd ccdi-project && mvn compile`
Expected: BUILD SUCCESS
**Step 4: Commit**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectController.java
git commit -m "feat: 添加项目状态统计接口"
```
---
## Task 5: 测试后端接口
**Files:**
- None (测试验证)
**Step 1: 启动后端服务**
Run: `mvn spring-boot:run``ry.bat`
Expected: 服务启动成功,看到 "Started RuoYiApplication" 日志
**Step 2: 获取访问令牌**
Run (使用 curl 或浏览器):
```bash
curl -X POST "http://localhost:8080/login/test?username=admin&password=admin123"
```
Expected: 返回 JSON 包含 token
**Step 3: 测试状态统计接口**
在 Swagger UI 中测试:
1. 访问: http://localhost:8080/swagger-ui/index.html
2. 找到: "纪检初核项目管理" → "GET /ccdi/project/statusCounts"
3. 点击 "Try it out" → "Execute"
或使用 curl (替换 YOUR_TOKEN):
```bash
curl -X GET "http://localhost:8080/ccdi/project/statusCounts" \
-H "Authorization: Bearer YOUR_TOKEN"
```
Expected: 返回类似以下的响应:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"all": 30,
"status0": 10,
"status1": 15,
"status2": 5
}
}
```
**Step 4: 验证数据正确性**
使用数据库工具连接 MySQL执行:
```sql
SELECT
status,
COUNT(*) as count
FROM ccdi_project
GROUP BY status;
```
Expected: 统计数字与接口返回一致
---
## Task 6: 添加前端 API 方法
**Files:**
- Modify: `ruoyi-ui/src/api/ccdiProject.js`
**Step 1: 读取当前 API 文件**
Run: Read `ruoyi-ui/src/api/ccdiProject.js`
Expected: 看到现有的 API 方法定义
**Step 2: 添加状态统计 API 方法**
在文件末尾(最后一个 export 函数之后)添加:
```javascript
// 查询项目状态统计
export function getStatusCounts() {
return request({
url: '/ccdi/project/statusCounts',
method: 'get'
})
}
```
**Step 3: 验证语法**
Run: `cd ruoyi-ui && npm run lint -- --fix src/api/ccdiProject.js`
Expected: No errors
**Step 4: Commit**
```bash
git add ruoyi-ui/src/api/ccdiProject.js
git commit -m "feat: 前端 API 添加状态统计方法"
```
---
## Task 7: 修改前端页面组件
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/index.vue`
**Step 1: 读取当前页面组件**
Run: Read `ruoyi-ui/src/views/ccdiProject/index.vue`
Expected: 看到现有代码结构
**Step 2: 修改 import 语句**
找到第 64 行左右的 import 语句:
```javascript
import {listProject} from '@/api/ccdiProject'
```
修改为:
```javascript
import {listProject, getStatusCounts} from '@/api/ccdiProject'
```
**Step 3: 重构 getList 方法**
找到 `getList()` 方法(大约第 122-134 行),完全替换为:
```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
})
}
```
**Step 4: 删除旧的统计方法**
找到并删除 `calculateTabCounts()` 方法(大约第 135-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
}
}
```
**Step 5: 验证语法**
Run: `cd ruoyi-ui && npm run lint -- --fix src/views/ccdiProject/index.vue`
Expected: No errors
**Step 6: Commit**
```bash
git add ruoyi-ui/src/views/ccdiProject/index.vue
git commit -m "refactor: 使用后端统计接口替换前端计算"
```
---
## Task 8: 测试前端功能
**Files:**
- None (测试验证)
**Step 1: 确保后端服务运行**
确认后端服务在运行中。
**Step 2: 启动前端开发服务器**
Run:
```bash
cd ruoyi-ui
npm run dev
```
Expected: 服务启动,看到 "App running at" 消息
**Step 3: 测试页面加载**
1. 打开浏览器访问: http://localhost:80
2. 登录系统 (admin/admin123)
3. 导航到 "项目管理" 页面
Expected:
- 页面正常加载
- 标签页显示正确的统计数字(例如:全部项目(30)
- 标签页数字不随分页变化
**Step 4: 测试搜索功能**
1. 在搜索框输入项目名称
2. 点击搜索按钮或按回车
Expected:
- 列表正确过滤
- 标签页数字保持不变(显示总数)
**Step 5: 测试分页功能**
1. 点击分页组件切换到第 2 页
Expected:
- 列表切换到第 2 页
- 标签页数字保持不变
**Step 6: 测试状态切换功能**
1. 点击"进行中"标签
Expected:
- 列表只显示进行中的项目
- 标签页数字保持不变(仍显示总数)
**Step 7: 测试浏览器控制台**
打开浏览器开发者工具的 Console 标签
Expected:
- 没有 JavaScript 错误
- 看到两个 API 请求成功list 和 statusCounts
---
## Task 9: 最终提交和文档更新
**Files:**
- Modify: `docs/design/2026-02-27-project-status-counts-fix-design.md`
**Step 1: 更新设计文档状态**
修改设计文档的状态部分:
```markdown
## 文档信息
- **创建日期**: 2026-02-27
- **作者**: Claude Code
- **状态**: ✅ 已完成
```
**Step 2: 验收清单**
对照设计文档的验收标准,确认:
- [ ] 后端 `/statusCounts` 接口返回正确的统计数字
- [ ] 前端标签页显示数据库中的完整统计
- [ ] 搜索不影响标签页统计数字
- [ ] 分页不影响标签页统计数字
- [ ] 状态过滤不影响标签页统计数字
- [ ] 统计接口响应时间 < 100ms
- [ ] 页面加载时间无明显增加
**Step 3: 提交文档更新**
```bash
git add docs/design/2026-02-27-project-status-counts-fix-design.md
git commit -m "docs: 更新项目状态统计修复设计文档状态为已完成"
```
**Step 4: 推送所有提交**
```bash
git push origin dev
```
---
## 验收清单
在完成所有任务后,验证以下内容:
### 功能验收
- [ ] 后端接口正确返回统计数字
- [ ] 前端标签页显示正确统计
- [ ] 搜索不影响统计数字
- [ ] 分页不影响统计数字
- [ ] 状态过滤不影响统计数字
### 性能验收
- [ ] 统计接口响应时间 < 100ms
- [ ] 页面加载流畅
### 代码质量
- [ ] 后端代码符合规范
- [ ] 前端代码符合规范
- [ ] 提交信息清晰
---
## 注意事项
1. **测试数据准备**: 如果数据库中没有足够的项目数据,可以先插入一些测试数据以验证统计功能
2. **错误处理**: 当前实现中,如果统计接口失败,会在控制台显示错误但不阻塞列表加载
3. **性能考虑**: 如果项目数量很大(> 10000建议后续优化为 GROUP BY 单次查询
---
## 回滚方案
如果实施后发现问题,可以通过以下步骤回滚:
1. **回滚前端代码**:
```bash
git revert <commit-hash-of-task-6-and-7>
```
2. **回滚后端代码**:
```bash
git revert <commit-hash-of-task-1-to-4>
```
3. **重新部署服务**
---
## 相关文档
- 设计文档: `docs/design/2026-02-27-project-status-counts-fix-design.md`
- 若依框架文档: 项目根目录的 `CLAUDE.md`
- MyBatis Plus 文档: https://baomidou.com/

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,737 @@
# 流水分析 Mock 服务器 - 实施计划
**创建日期**: 2026-03-02
**状态**: 待执行
**预计完成时间**: 2-3 天
---
## 项目目标
开发一个基于 Python + FastAPI 的 Mock 服务器,用于模拟流水分析平台的 7 个核心接口,支持:
- 配置文件驱动的响应数据
- 文件上传解析延迟模拟4秒
- 错误场景触发机制(通过 error_XXXX 标记)
- 自动生成的 Swagger API 文档
---
## 技术栈
| 技术 | 版本 | 用途 |
|------|------|------|
| Python | 3.11+ | 编程语言 |
| FastAPI | 0.104.1 | Web框架 |
| Pydantic | 2.5.0 | 数据验证 |
| Uvicorn | 0.24.0 | ASGI服务器 |
| pytest | latest | 测试框架 |
---
## 实施任务列表
### Task 1: 项目初始化和基础设置
**状态**: ⏳ 待开始
**预计时间**: 1 小时
**阻塞任务**: 无
**目标**: 创建项目目录结构、配置文件和依赖管理
**实施步骤**:
1. 创建项目根目录 `lsfx-mock-server/`
2. 创建目录结构:
```
lsfx-mock-server/
├── config/
│ └── responses/
├── models/
├── services/
├── routers/
├── utils/
└── tests/
```
3. 创建 `requirements.txt`:
```txt
fastapi==0.104.1
uvicorn[standard]==0.24.0
pydantic==2.5.0
python-multipart==0.0.6
pytest>=7.0.0
pytest-cov>=4.0.0
httpx>=0.25.0
```
4. 创建 `config/settings.py`:
- 使用 Pydantic BaseSettings
- 支持环境变量覆盖(.env 文件)
- 配置项APP_NAME, HOST, PORT, DEBUG, PARSE_DELAY_SECONDS
5. 创建 4 个 JSON 响应模板文件:
- `config/responses/token.json` - Token 响应模板
- `config/responses/upload.json` - 上传文件响应模板
- `config/responses/parse_status.json` - 解析状态响应模板
- `config/responses/bank_statement.json` - 银行流水响应模板
- 每个模板包含占位符(如 {project_id}, {log_id}
**验证标准**:
- ✅ 虚拟环境创建并激活
- ✅ 依赖安装成功(无错误)
- ✅ 配置文件能正确导入(`from config.settings import settings`
- ✅ JSON 模板文件格式正确(使用 `json.load()` 验证)
- ✅ settings 能读取环境变量
**提交检查点**:
```bash
git add requirements.txt config/
git commit -m "feat(mock): initialize project structure and configuration"
```
---
### Task 2: 实现数据模型层
**状态**: ⏳ 待开始(等待 Task 1
**预计时间**: 1.5 小时
**阻塞任务**: Task 1
**目标**: 创建所有请求和响应的 Pydantic 模型类
**实施步骤**:
1. 创建 `models/__init__.py`(空文件)
2. 创建 `models/request.py`:
- 定义 6 个请求模型:
- GetTokenRequest10+ 字段,可选字段有默认值)
- UploadFileRequest通过 Form 数据接收)
- FetchInnerFlowRequest7 个必填字段)
- CheckParseStatusRequest2 个字段)
- DeleteFilesRequest3 个字段)
- GetBankStatementRequest4 个字段)
- 所有字段添加 Field 描述(用于 Swagger
- 可选字段使用 `Optional[Type] = default_value`
3. 创建 `models/response.py`:
- 定义嵌套数据模型:
- TokenData5 个字段)
- UploadLogItem15+ 字段)
- BankStatementItem30+ 字段)
- PendingItem15+ 字段)
- 定义 6 个响应模型:
- GetTokenResponse
- UploadFileResponse
- FetchInnerFlowResponse
- CheckParseStatusResponse
- DeleteFilesResponse
- GetBankStatementResponse
- 所有响应模型包含通用字段code, message, status, successResponse
**验证标准**:
- ✅ 所有模型类能正确实例化
- ✅ 可选字段默认值正确
- ✅ Pydantic 验证功能正常(类型错误会抛出 ValidationError
- ✅ 模型序列化为 JSON 正确(`model.model_dump_json()`
- ✅ Swagger 自动文档显示所有字段和描述
**提交检查点**:
```bash
git add models/
git commit -m "feat(models): implement Pydantic request and response models"
```
---
### Task 3: 实现工具类
**状态**: ⏳ 待开始(可与 Task 2 并行)
**预计时间**: 1 小时
**阻塞任务**: 无
**目标**: 实现错误检测和响应构建工具
**实施步骤**:
1. 创建 `utils/__init__.py`
2. 创建 `utils/error_simulator.py`:
```python
class ErrorSimulator:
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": "无行内流水文件"},
}
@staticmethod
def detect_error_marker(value: str) -> Optional[str]:
"""检测 error_XXXX 模式"""
import re
if not value:
return None
pattern = r'error_(\d+)'
match = re.search(pattern, value)
return match.group(1) if match else None
@staticmethod
def build_error_response(error_code: str) -> Dict:
"""构建错误响应"""
if error_code in ErrorSimulator.ERROR_CODES:
error_info = ErrorSimulator.ERROR_CODES[error_code]
return {
"code": error_info["code"],
"message": error_info["message"],
"status": error_info["code"],
"successResponse": False
}
return None
```
3. 创建 `utils/response_builder.py`:
```python
import json
from pathlib import Path
from typing import Dict, Any
class ResponseBuilder:
TEMPLATE_DIR = Path(__file__).parent.parent / "config" / "responses"
@staticmethod
def load_template(template_name: str) -> Dict:
"""加载 JSON 模板"""
file_path = ResponseBuilder.TEMPLATE_DIR / f"{template_name}.json"
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
@staticmethod
def replace_placeholders(template: Dict, **kwargs) -> Dict:
"""递归替换占位符"""
def replace_value(value):
if isinstance(value, str):
for key, val in kwargs.items():
placeholder = f"{{{key}}}"
if placeholder in value:
return value.replace(placeholder, str(val))
return value
elif isinstance(value, dict):
return {k: replace_value(v) for k, v in value.items()}
elif isinstance(value, list):
return [replace_value(item) for item in value]
return value
return replace_value(template)
@staticmethod
def build_success_response(template_name: str, **kwargs) -> Dict:
"""构建成功响应"""
template = ResponseBuilder.load_template(template_name)
return ResponseBuilder.replace_placeholders(
template["success_response"],
**kwargs
)
```
**验证标准**:
- ✅ ErrorSimulator.detect_error_marker() 能正确识别错误标记
- ✅ ErrorSimulator.build_error_response() 返回正确的错误响应
- ✅ ResponseBuilder 能正确加载 JSON 模板
- ✅ 占位符替换功能正常(支持嵌套字典和列表)
- ✅ 所有 8 个错误码都有对应响应
**提交检查点**:
```bash
git add utils/
git commit -m "feat(utils): implement error simulator and response builder"
```
---
### Task 4: 实现服务层
**状态**: ⏳ 待开始(等待 Task 1, 2, 3
**预计时间**: 2 小时
**阻塞任务**: Task 1, Task 2, Task 3
**目标**: 实现核心业务服务类
**实施步骤**:
1. 创建 `services/__init__.py`
2. 创建 `services/token_service.py`:
```python
class TokenService:
def __init__(self):
self.project_counter = 0
self.tokens = {} # projectId -> token_data
def create_token(self, request: GetTokenRequest) -> Dict:
self.project_counter += 1
project_id = self.project_counter
token = f"mock_token_{project_id}"
return ResponseBuilder.build_success_response(
"token",
project_id=project_id,
project_no=request.projectNo,
entity_name=request.entityName
)
```
3. 创建 `services/file_service.py`:
```python
from fastapi import BackgroundTasks
import time
from uuid import uuid4
class FileService:
def __init__(self):
self.file_records = {} # logId -> record
self.parsing_status = {} # logId -> is_parsing
self.log_counter = 0
async def upload_file(self, group_id: int, file, background_tasks: BackgroundTasks) -> Dict:
self.log_counter += 1
log_id = self.log_counter
# 立即存储记录
self.file_records[log_id] = {
"logId": log_id,
"groupId": group_id,
"status": -5,
"uploadStatusDesc": "parsing",
"uploadFileName": file.filename,
"fileSize": file.size,
# ... 其他字段
}
self.parsing_status[log_id] = True
# 启动后台任务
background_tasks.add_task(
self._simulate_parsing,
log_id,
settings.PARSE_DELAY_SECONDS
)
return ResponseBuilder.build_success_response(
"upload",
log_id=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]["uploadStatusDesc"] = "data.wait.confirm.newaccount"
self.parsing_status[log_id] = False
def check_parse_status(self, group_id: int, inprogress_list: str) -> Dict:
"""检查解析状态"""
log_ids = [int(x.strip()) for x in inprogress_list.split(",")]
is_parsing = any(
self.parsing_status.get(log_id, False)
for log_id in log_ids
)
pending_list = [
self.file_records[log_id]
for log_id in log_ids
if log_id in self.file_records
]
return {
"code": "200",
"data": {
"parsing": is_parsing,
"pendingList": pending_list
},
"status": "200",
"successResponse": True
}
def delete_files(self, group_id: int, log_ids: List[int], user_id: int) -> Dict:
"""删除文件"""
for log_id in log_ids:
self.file_records.pop(log_id, None)
self.parsing_status.pop(log_id, None)
return {
"code": "200",
"data": {"message": "delete.files.success"},
"status": "200",
"successResponse": True
}
def fetch_inner_flow(self, request: FetchInnerFlowRequest) -> Dict:
"""拉取行内流水(模拟无数据)"""
return {
"code": "200",
"data": {
"code": "501014",
"message": "无行内流水文件"
},
"status": "200",
"successResponse": True
}
```
4. 创建 `services/statement_service.py`:
```python
class StatementService:
def get_bank_statement(self, request: GetBankStatementRequest) -> Dict:
# 加载模板
template = ResponseBuilder.load_template("bank_statement")
statements = template["success_response"]["data"]["bankStatementList"]
# 模拟分页
start = (request.pageNow - 1) * request.pageSize
end = start + request.pageSize
page_data = statements[start:end]
return {
"code": "200",
"data": {
"bankStatementList": page_data,
"totalCount": len(statements)
},
"status": "200",
"successResponse": True
}
```
**验证标准**:
- ✅ TokenService 能创建唯一 token
- ✅ FileService.upload_file() 返回正确状态
- ✅ 后台任务执行后,解析状态从 True 变为 False
- ✅ check_parse_status() 正确返回 parsing 状态
- ✅ StatementService 支持分页功能
- ✅ 所有方法返回正确格式
**提交检查点**:
```bash
git add services/
git commit -m "feat(services): implement token, file, and statement services"
```
---
### Task 5: 实现 API 路由
**状态**: ⏳ 待开始(等待 Task 2, 3, 4
**预计时间**: 1.5 小时
**阻塞任务**: Task 1, Task 2, Task 3, Task 4
**目标**: 实现所有 6 个 API 接口路由
**实施步骤**:
1. 创建 `routers/__init__.py`
2. 创建 `routers/api.py`:
```python
from fastapi import APIRouter, BackgroundTasks, UploadFile, File, Form
from models.request import *
from models.response import *
from services.token_service import TokenService
from services.file_service import FileService
from services.statement_service import StatementService
from utils.error_simulator import ErrorSimulator
router = APIRouter()
token_service = TokenService()
file_service = FileService()
statement_service = StatementService()
@router.post("/account/common/getToken")
async def get_token(request: GetTokenRequest):
"""获取Token"""
error_code = ErrorSimulator.detect_error_marker(request.projectNo)
if error_code:
return ErrorSimulator.build_error_response(error_code)
return token_service.create_token(request)
@router.post("/watson/api/project/remoteUploadSplitFile")
async def upload_file(
background_tasks: BackgroundTasks,
groupId: int = Form(...),
file: UploadFile = File(...)
):
"""上传文件"""
return await file_service.upload_file(groupId, file, background_tasks)
@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)
@router.post("/watson/api/project/upload/getpendings")
async def check_parse_status(request: CheckParseStatusRequest):
"""检查文件解析状态"""
return file_service.check_parse_status(request.groupId, request.inprogressList)
@router.post("/watson/api/project/batchDeleteUploadFile")
async def delete_files(
groupId: int,
logIds: List[int],
userId: int
):
"""删除文件"""
return file_service.delete_files(groupId, logIds, userId)
@router.post("/watson/api/project/getBSByLogId")
async def get_bank_statement(request: GetBankStatementRequest):
"""获取银行流水"""
return statement_service.get_bank_statement(request)
```
**验证标准**:
- ✅ 所有 6 个接口在 Swagger UI 中可见
- ✅ 每个接口能正常响应
- ✅ 错误标记功能正常(包含 error_XXXX 的参数触发错误)
- ✅ 文件上传接口能接收文件
- ✅ 所有接口有正确的 Swagger 描述
- ✅ 响应格式符合文档要求
**提交检查点**:
```bash
git add routers/
git commit -m "feat(routers): implement all 6 API endpoints"
```
---
### Task 6: 实现主应用
**状态**: ⏳ 待开始(等待 Task 1, 4, 5
**预计时间**: 0.5 小时
**阻塞任务**: Task 1, Task 4, Task 5
**目标**: 实现 FastAPI 应用主入口
**实施步骤**:
1. 创建 `main.py`:
```python
from fastapi import FastAPI
from routers import api
from config.settings import settings
app = FastAPI(
title=settings.APP_NAME,
description="模拟流水分析平台的7个核心接口",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc"
)
app.include_router(api.router, tags=["流水分析接口"])
@app.get("/health")
async def health_check():
return {"status": "healthy", "service": settings.APP_NAME}
if __name__ == "__main__":
import uvicorn
uvicorn.run(
app,
host=settings.HOST,
port=settings.PORT,
log_level="debug" if settings.DEBUG else "info"
)
```
**验证标准**:
- ✅ 应用能启动:`python main.py`
- ✅ 访问 http://localhost:8000/docs 显示 Swagger UI
- ✅ 访问 http://localhost:8000/redoc 显示 ReDoc
- ✅ 健康检查端点返回正确响应
- ✅ 所有接口在文档中可见
**提交检查点**:
```bash
git add main.py
git commit -m "feat(main): implement FastAPI application entry point"
```
---
### Task 7: 编写测试套件
**状态**: ⏳ 待开始(等待 Task 1-6
**预计时间**: 2 小时
**阻塞任务**: Task 1, Task 2, Task 3, Task 4, Task 5, Task 6
**目标**: 创建完整的测试套件
**实施步骤**:
1. 创建 `tests/conftest.py`:
```python
import pytest
from fastapi.testclient import TestClient
from main import app
@pytest.fixture
def client():
return TestClient(app)
```
2. 创建 `tests/test_models.py` - 测试所有数据模型
3. 创建 `tests/test_utils.py` - 测试工具类
4. 创建 `tests/test_services.py` - 测试服务类
5. 创建 `tests/test_api.py` - 测试 API 端点
**验证标准**:
- ✅ 运行 `pytest tests/ -v` 所有测试通过
- ✅ 代码覆盖率 > 80%
- ✅ 所有错误场景有测试
- ✅ 生成 HTML 覆盖率报告
**提交检查点**:
```bash
git add tests/
git commit -m "test: add comprehensive test suite"
```
---
### Task 8: 编写文档和部署配置
**状态**: ⏳ 待开始(等待 Task 1-7
**预计时间**: 1 小时
**阻塞任务**: Task 1-7
**目标**: 创建项目文档和部署说明
**实施步骤**:
1. 创建 `README.md`(包含安装、使用、测试说明)
2. 创建 `.env.example`
3. 创建 `Dockerfile`
4. 创建 `docker-compose.yml`
**验证标准**:
- ✅ README 中所有命令可执行
- ✅ Docker 镜像构建成功
- ✅ Docker Compose 启动成功
**提交检查点**:
```bash
git add README.md .env.example Dockerfile docker-compose.yml
git commit -m "docs: add README and deployment configuration"
```
---
### Task 9: 创建集成测试
**状态**: ⏳ 待开始(等待 Task 8
**预计时间**: 1 小时
**阻塞任务**: Task 8
**目标**: 创建端到端集成测试脚本
**实施步骤**:
1. 创建 `tests/integration/test_full_workflow.py`
2. 实现完整的接口调用流程测试
3. 添加错误场景测试
**验证标准**:
- ✅ 集成测试通过
- ✅ 完整流程测试成功
- ✅ 错误场景测试成功
**提交检查点**:
```bash
git add tests/integration/
git commit -m "test: add integration tests for full workflow"
```
---
### Task 10: 代码审查和提交
**状态**: ⏳ 待开始(等待 Task 1-9
**预计时间**: 1 小时
**阻塞任务**: Task 1-9
**目标**: 代码审查、优化和 Git 提交
**审查清单**:
1. **代码质量**
- ✅ 所有代码符合 PEP 8
- ✅ 类型提示完整
- ✅ 无硬编码配置
- ✅ 注释充分
2. **安全性**
- ✅ 输入验证完整Pydantic
- ✅ 无注入风险
3. **测试覆盖**
- ✅ 单元测试覆盖率 > 80%
- ✅ 集成测试通过
**验证标准**:
- ✅ 所有测试通过
- ✅ 代码覆盖率报告生成
- ✅ 手动测试所有接口
- ✅ README 验证完成
**最终提交**:
```bash
git add .
git commit -m "feat(lsfx-mock): complete lsfx mock server implementation"
git push origin feature/lsfx-mock-server
```
---
## 开发注意事项
### 环境要求
- Python 3.11+
- 虚拟环境venv
- 端口 8000 可用
### 开发流程
1. 每完成一个任务,立即提交代码
2. 运行相关测试确保功能正确
3. 更新任务状态
4. 开始下一个任务
### 测试策略
- **单元测试**: 每个模块独立测试
- **集成测试**: 完整流程测试
- **手动测试**: 使用 Swagger UI 验证接口
### 代码规范
- 遵循 PEP 8
- 使用类型提示
- 函数和类添加文档字符串
- 保持代码简洁YAGNI, DRY
---
## 预期成果
1. ✅ 完整的 Mock 服务器,模拟 7 个核心接口
2. ✅ 配置文件驱动的响应数据
3. ✅ 文件解析延迟模拟
4. ✅ 错误场景触发机制
5. ✅ 自动生成的 API 文档
6. ✅ 完整的测试套件(覆盖率 > 80%
7. ✅ 清晰的 README 和部署文档
8. ✅ Docker 部署支持
---
## 风险和缓解
| 风险 | 影响 | 缓解措施 |
|------|------|----------|
| FastAPI 框架不熟悉 | 延期 | 变更预计时间到 3-4 天 |
| 异步任务调试困难 | 中等 | 添加详细日志,分步测试 |
| 响应格式与真实接口不符 | 高 | 严格对照接口文档,多次验证 |
---
## 后续优化方向
1. 添加数据库持久化SQLite
2. 实现更复杂的场景模拟
3. 添加请求日志记录
4. 创建 Web 管理界面
5. 支持 WebSocket 实时通知
---
**预计总开发时间**: 10-12 小时
**建议开发模式**: 按顺序执行,每完成一个任务立即测试验证

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,745 @@
# 银行流水实体类实现计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**目标:** 创建 CcdiBankStatement 实体类,实现从流水分析接口响应到数据库实体的数据转换功能。
**架构:** 在 ccdi-project 模块中创建实体类,使用 Spring BeanUtils 进行对象映射,手动处理字段名不一致的情况。实体类包含静态转换方法 fromResponse()。
**技术栈:** MyBatis Plus 3.5.10, Spring BeanUtils, Lombok, MySQL 8.2.0
---
## 任务概览
| 任务 | 预估时间 | 文件 |
|------|---------|------|
| Task 1: 数据库表修改 | 5分钟 | SQL脚本 |
| Task 2: 创建实体类基础结构 | 10分钟 | CcdiBankStatement.java |
| Task 3: 编写转换方法测试 | 15分钟 | CcdiBankStatementTest.java |
| Task 4: 实现转换方法 | 10分钟 | CcdiBankStatement.java |
| Task 5: 创建 Mapper 接口 | 5分钟 | CcdiBankStatementMapper.java |
| Task 6: 创建 Mapper XML | 10分钟 | CcdiBankStatementMapper.xml |
| Task 7: 验证测试 | 5分钟 | - |
---
## Task 1: 数据库表修改
**目标:** 在 ccdi_bank_statement 表中添加 project_id 字段和索引。
**文件:**
- 创建: `sql/ccdi_bank_statement_add_project_id.sql`
**Step 1: 创建数据库修改脚本**
```sql
-- 为 ccdi_bank_statement 表添加 project_id 字段
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`);
```
**Step 2: 执行数据库修改脚本**
```bash
# 连接到数据库并执行脚本
mysql -h 116.62.17.81 -u root -p ccdi < sql/ccdi_bank_statement_add_project_id.sql
```
**预期输出:** 执行成功,无错误信息。
**Step 3: 验证字段已添加**
```sql
-- 查看表结构,确认 project_id 字段已添加
SHOW COLUMNS FROM ccdi_bank_statement LIKE 'project_id';
```
**预期输出:**
```
Field | Type | Null | Key | Default | Extra
-------------|------------|------|-----|---------|-------
project_id | bigint(20) | YES | MUL | NULL |
```
**Step 4: 提交**
```bash
git add sql/ccdi_bank_statement_add_project_id.sql
git commit -m "feat: 为银行流水表添加 project_id 字段"
```
---
## Task 2: 创建实体类基础结构
**目标:** 创建 CcdiBankStatement 实体类的基础结构,包含所有字段定义。
**文件:**
- 创建: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatement.java`
**Step 1: 创建实体类文件**
```java
package com.ruoyi.ccdi.project.domain.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 银行流水对象 ccdi_bank_statement
*
* @author ruoyi
* @date 2026-03-04
*/
@Data
@TableName("ccdi_bank_statement")
public class CcdiBankStatement implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
// ===== 主键和关联字段 =====
/** 流水ID */
@TableId(type = IdType.AUTO)
private Long bankStatementId;
/** 关联项目ID业务字段 */
private Long projectId;
/** 企业ID */
private Integer leId;
/** 账号ID */
private Long accountId;
/** 项目id保留原有字段 */
private Integer groupId;
// ===== 账号信息 =====
/** 企业账号名称 */
private String leAccountName;
/** 企业银行账号 */
private String leAccountNo;
/** 账号日期ID */
private Integer accountingDateId;
/** 账号日期 */
private String accountingDate;
/** 交易日期 */
private String trxDate;
/** 币种 */
private String currency;
// ===== 交易金额 =====
/** 付款金额 */
private BigDecimal amountDr;
/** 收款金额 */
private BigDecimal amountCr;
/** 余额 */
private BigDecimal amountBalance;
// ===== 交易类型和标志 =====
/** 交易类型 */
private String cashType;
/** 交易标志位 */
private String trxFlag;
/** 分类ID */
private Integer trxType;
/** 异常类型 */
private String exceptionType;
/** 是否为内部交易 */
private Integer internalFlag;
// ===== 对手方信息 =====
/** 对手方企业ID */
private Integer customerLeId;
/** 对手方企业名称 */
private String customerAccountName;
/** 对手方账号 */
private String customerAccountNo;
/** 对手方银行 */
private String customerBank;
/** 对手方备注 */
private String customerReference;
// ===== 摘要和备注 =====
/** 用户交易摘要 */
private String userMemo;
/** 银行交易摘要 */
private String bankComments;
/** 银行交易号 */
private String bankTrxNumber;
// ===== 银行信息 =====
/** 所属银行缩写 */
private String bank;
// ===== 批次和上传信息 =====
/** 上传logId */
private Integer batchId;
/** 每次上传在文件中的line */
private Integer batchSequence;
// ===== 附加字段 =====
/** meta json固定为null */
private String metaJson;
/** 是否包含余额 */
private Integer noBalance;
/** 初始余额 */
private Integer beginBalance;
/** 结束余额 */
private Integer endBalance;
/** 覆盖标识 */
private Long overrideBsId;
/** 交易方式 */
private String paymentMethod;
/** 身份证号 */
private String cretNo;
// ===== 审计字段 =====
/** 创建时间 */
private Date createDate;
/** 创建者 */
private Long createdBy;
}
```
**Step 2: 验证代码编译**
```bash
cd ccdi-project
mvn compile
```
**预期输出:** BUILD SUCCESS
**Step 3: 提交**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatement.java
git commit -m "feat: 创建银行流水实体类基础结构"
```
---
## Task 3: 编写转换方法测试
**目标:** 使用 TDD 方法,先编写 fromResponse() 方法的单元测试。
**文件:**
- 创建: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatementTest.java`
**Step 1: 添加测试依赖(如果不存在)**
检查 `ccdi-project/pom.xml` 是否包含测试依赖:
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
```
如果没有,添加上述依赖。
**Step 2: 创建测试类**
```java
package com.ruoyi.ccdi.project.domain.entity;
import com.ruoyi.lsfx.domain.response.GetBankStatementResponse.BankStatementItem;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.*;
/**
* 银行流水实体类测试
*
* @author ruoyi
* @date 2026-03-04
*/
class CcdiBankStatementTest {
@Test
void testFromResponse_Success() {
// 准备测试数据
BankStatementItem item = new BankStatementItem();
item.setBankStatementId(123456L);
item.setLeId(100);
item.setAccountId(200L);
item.setLeName("测试企业");
item.setAccountMaskNo("6222****1234");
item.setDrAmount(new BigDecimal("1000.00"));
item.setCrAmount(new BigDecimal("500.00"));
item.setBalanceAmount(new BigDecimal("5000.00"));
item.setTrxDate("2026-03-04");
item.setCustomerAccountMaskNo("6228****5678");
item.setUploadSequnceNumber(1);
// 执行转换
CcdiBankStatement entity = CcdiBankStatement.fromResponse(item);
// 验证结果
assertNotNull(entity, "转换结果不应为null");
assertEquals(123456L, entity.getBankStatementId(), "流水ID应该匹配");
assertEquals(100, entity.getLeId(), "企业ID应该匹配");
assertEquals(200L, entity.getAccountId(), "账号ID应该匹配");
assertEquals("测试企业", entity.getLeAccountName(), "企业名称应该匹配");
// 验证手动映射的字段
assertEquals("6222****1234", entity.getLeAccountNo(), "企业账号应该从 accountMaskNo 映射");
assertEquals("6228****5678", entity.getCustomerAccountNo(), "对手方账号应该从 customerAccountMaskNo 映射");
assertEquals(1, entity.getBatchSequence(), "批次序号应该从 uploadSequnceNumber 映射");
// 验证金额字段
assertEquals(new BigDecimal("1000.00"), entity.getAmountDr(), "付款金额应该匹配");
assertEquals(new BigDecimal("500.00"), entity.getAmountCr(), "收款金额应该匹配");
assertEquals(new BigDecimal("5000.00"), entity.getAmountBalance(), "余额应该匹配");
// 验证特殊字段
assertNull(entity.getMetaJson(), "metaJson 应该强制为 null");
assertNull(entity.getProjectId(), "projectId 应该为 null需要 Service 层设置)");
}
@Test
void testFromResponse_Null() {
// 测试空值处理
CcdiBankStatement entity = CcdiBankStatement.fromResponse(null);
// 验证返回 null
assertNull(entity, "传入 null 应该返回 null");
}
@Test
void testFromResponse_EmptyObject() {
// 测试空对象转换
BankStatementItem item = new BankStatementItem();
// 执行转换
CcdiBankStatement entity = CcdiBankStatement.fromResponse(item);
// 验证不会抛出异常
assertNotNull(entity, "空对象转换结果不应为 null");
assertNull(entity.getMetaJson(), "metaJson 应该为 null");
}
@Test
void testFromResponse_FieldTypeCompatibility() {
// 测试字段类型兼容性
BankStatementItem item = new BankStatementItem();
item.setInternalFlag(1); // Integer 类型
item.setTransTypeId(100); // Integer 类型
// 执行转换
CcdiBankStatement entity = CcdiBankStatement.fromResponse(item);
// 验证类型转换正确
assertNotNull(entity, "转换结果不应为 null");
assertEquals(1, entity.getInternalFlag(), "Integer 类型应该正确复制");
assertEquals(100, entity.getTrxType(), "Integer 类型应该正确复制");
}
}
```
**Step 3: 运行测试验证失败**
```bash
cd ccdi-project
mvn test -Dtest=CcdiBankStatementTest
```
**预期输出:** 编译失败,因为 `fromResponse()` 方法还不存在。
**Step 4: 提交**
```bash
git add ccdi-project/src/test/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatementTest.java
git commit -m "test: 添加银行流水转换方法的单元测试"
```
---
## Task 4: 实现转换方法
**目标:** 在 CcdiBankStatement 实体类中实现 fromResponse() 静态方法。
**文件:**
- 修改: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatement.java`
**Step 1: 添加必要的导入**
在文件顶部添加:
```java
import com.ruoyi.lsfx.domain.response.GetBankStatementResponse.BankStatementItem;
import org.springframework.beans.BeanUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
```
**Step 2: 添加日志常量**
在类的开头添加:
```java
private static final Logger log = LoggerFactory.getLogger(CcdiBankStatement.class);
```
**Step 3: 实现 fromResponse() 方法**
在类的末尾createdBy 字段之后)添加转换方法:
```java
/**
* 从流水分析接口响应转换为实体
*
* @param item 流水分析接口返回的流水项
* @return 流水实体,如果 item 为 null 则返回 null
*/
public static CcdiBankStatement fromResponse(BankStatementItem item) {
// 1. 空值检查
if (item == null) {
log.warn("流水项为空,无法转换");
return null;
}
try {
// 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
// 注意project_id 需要在 Service 层根据业务逻辑设置
return entity;
} catch (Exception e) {
log.error("流水数据转换失败, bankStatementId={}", item.getBankStatementId(), e);
throw new RuntimeException("流水数据转换失败", e);
}
}
```
**Step 4: 运行测试验证通过**
```bash
cd ccdi-project
mvn test -Dtest=CcdiBankStatementTest
```
**预期输出:**
```
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
```
**Step 5: 提交**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatement.java
git commit -m "feat: 实现银行流水转换方法 fromResponse()"
```
---
## Task 5: 创建 Mapper 接口
**目标:** 创建 MyBatis Mapper 接口,继承 BaseMapper 并提供批量插入方法。
**文件:**
- 创建: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapper.java`
**Step 1: 创建 Mapper 接口**
```java
package com.ruoyi.ccdi.project.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.ccdi.project.domain.entity.CcdiBankStatement;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 银行流水Mapper接口
*
* @author ruoyi
* @date 2026-03-04
*/
public interface CcdiBankStatementMapper extends BaseMapper<CcdiBankStatement> {
/**
* 批量插入银行流水
*
* @param list 银行流水列表
* @return 插入记录数
*/
int insertBatch(@Param("list") List<CcdiBankStatement> list);
}
```
**Step 2: 验证代码编译**
```bash
cd ccdi-project
mvn compile
```
**预期输出:** BUILD SUCCESS
**Step 3: 提交**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapper.java
git commit -m "feat: 创建银行流水 Mapper 接口"
```
---
## Task 6: 创建 Mapper XML
**目标:** 创建 MyBatis XML 映射文件,实现批量插入 SQL。
**文件:**
- 创建: `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml`
**Step 1: 创建 XML 文件**
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper">
<resultMap type="com.ruoyi.ccdi.project.domain.entity.CcdiBankStatement" id="CcdiBankStatementResult">
<id property="bankStatementId" column="bank_statement_id" />
<result property="projectId" column="project_id" />
<result property="leId" column="LE_ID" />
<result property="accountId" column="ACCOUNT_ID" />
<result property="groupId" column="group_id" />
<result property="leAccountName" column="LE_ACCOUNT_NAME" />
<result property="leAccountNo" column="LE_ACCOUNT_NO" />
<result property="accountingDateId" column="ACCOUNTING_DATE_ID" />
<result property="accountingDate" column="ACCOUNTING_DATE" />
<result property="trxDate" column="TRX_DATE" />
<result property="currency" column="CURRENCY" />
<result property="amountDr" column="AMOUNT_DR" />
<result property="amountCr" column="AMOUNT_CR" />
<result property="amountBalance" column="AMOUNT_BALANCE" />
<result property="cashType" column="CASH_TYPE" />
<result property="customerLeId" column="CUSTOMER_LE_ID" />
<result property="customerAccountName" column="CUSTOMER_ACCOUNT_NAME" />
<result property="customerAccountNo" column="CUSTOMER_ACCOUNT_NO" />
<result property="customerBank" column="customer_bank" />
<result property="customerReference" column="customer_reference" />
<result property="userMemo" column="USER_MEMO" />
<result property="bankComments" column="BANK_COMMENTS" />
<result property="bankTrxNumber" column="BANK_TRX_NUMBER" />
<result property="bank" column="BANK" />
<result property="trxFlag" column="TRX_FLAG" />
<result property="trxType" column="TRX_TYPE" />
<result property="exceptionType" column="EXCEPTION_TYPE" />
<result property="internalFlag" column="internal_flag" />
<result property="batchId" column="batch_id" />
<result property="batchSequence" column="batch_sequence" />
<result property="createDate" column="CREATE_DATE" />
<result property="createdBy" column="created_by" />
<result property="metaJson" column="meta_json" />
<result property="noBalance" column="no_balance" />
<result property="beginBalance" column="begin_balance" />
<result property="endBalance" column="end_balance" />
<result property="overrideBsId" column="override_bs_id" />
<result property="paymentMethod" column="payment_method" />
<result property="cretNo" column="cret_no" />
</resultMap>
<sql id="selectCcdiBankStatementVo">
select bank_statement_id, project_id, LE_ID, ACCOUNT_ID, group_id,
LE_ACCOUNT_NAME, LE_ACCOUNT_NO, ACCOUNTING_DATE_ID, ACCOUNTING_DATE,
TRX_DATE, CURRENCY, AMOUNT_DR, AMOUNT_CR, AMOUNT_BALANCE,
CASH_TYPE, CUSTOMER_LE_ID, CUSTOMER_ACCOUNT_NAME, CUSTOMER_ACCOUNT_NO,
customer_bank, customer_reference, USER_MEMO, BANK_COMMENTS,
BANK_TRX_NUMBER, BANK, TRX_FLAG, TRX_TYPE, EXCEPTION_TYPE,
internal_flag, batch_id, batch_sequence, CREATE_DATE, created_by,
meta_json, no_balance, begin_balance, end_balance,
override_bs_id, payment_method, cret_no
from ccdi_bank_statement
</sql>
<insert id="insertBatch" parameterType="java.util.List">
insert into ccdi_bank_statement (
project_id, LE_ID, ACCOUNT_ID, group_id,
LE_ACCOUNT_NAME, LE_ACCOUNT_NO, ACCOUNTING_DATE_ID, ACCOUNTING_DATE,
TRX_DATE, CURRENCY, AMOUNT_DR, AMOUNT_CR, AMOUNT_BALANCE,
CASH_TYPE, CUSTOMER_LE_ID, CUSTOMER_ACCOUNT_NAME, CUSTOMER_ACCOUNT_NO,
customer_bank, customer_reference, USER_MEMO, BANK_COMMENTS,
BANK_TRX_NUMBER, BANK, TRX_FLAG, TRX_TYPE, EXCEPTION_TYPE,
internal_flag, batch_id, batch_sequence, CREATE_DATE, created_by,
meta_json, no_balance, begin_balance, end_balance,
override_bs_id, payment_method, cret_no
) values
<foreach collection="list" item="item" separator=",">
(
#{item.projectId}, #{item.leId}, #{item.accountId}, #{item.groupId},
#{item.leAccountName}, #{item.leAccountNo}, #{item.accountingDateId}, #{item.accountingDate},
#{item.trxDate}, #{item.currency}, #{item.amountDr}, #{item.amountCr}, #{item.amountBalance},
#{item.cashType}, #{item.customerLeId}, #{item.customerAccountName}, #{item.customerAccountNo},
#{item.customerBank}, #{item.customerReference}, #{item.userMemo}, #{item.bankComments},
#{item.bankTrxNumber}, #{item.bank}, #{item.trxFlag}, #{item.trxType}, #{item.exceptionType},
#{item.internalFlag}, #{item.batchId}, #{item.batchSequence}, #{item.createDate}, #{item.createdBy},
#{item.metaJson}, #{item.noBalance}, #{item.beginBalance}, #{item.endBalance},
#{item.overrideBsId}, #{item.paymentMethod}, #{item.cretNo}
)
</foreach>
</insert>
</mapper>
```
**Step 2: 验证 XML 语法**
```bash
cd ccdi-project
mvn compile
```
**预期输出:** BUILD SUCCESS无 XML 解析错误
**Step 3: 提交**
```bash
git add ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml
git commit -m "feat: 创建银行流水 Mapper XML 映射文件"
```
---
## Task 7: 验证测试
**目标:** 运行所有测试,确保功能正常。
**Step 1: 运行单元测试**
```bash
cd ccdi-project
mvn test
```
**预期输出:**
```
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
```
**Step 2: 运行集成编译**
```bash
mvn clean compile
```
**预期输出:** BUILD SUCCESS
**Step 3: 检查依赖关系**
确认 `ccdi-project` 模块的 `pom.xml` 中已依赖 `ccdi-lsfx` 模块:
```xml
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ccdi-lsfx</artifactId>
</dependency>
```
如果没有,添加上述依赖并重新编译。
**Step 4: 提交所有更改**
```bash
git add .
git commit -m "test: 完成银行流水实体类功能验证"
```
---
## 完成检查清单
- [ ] 数据库已添加 `project_id` 字段和索引
- [ ] 实体类包含 39 个字段,类型正确
- [ ] `fromResponse()` 方法正确处理 3 个字段名映射
- [ ] `fromResponse()` 方法强制设置 `metaJson` 为 null
- [ ] 单元测试覆盖正常转换、空值处理、字段映射等场景
- [ ] Mapper 接口继承 `BaseMapper`
- [ ] Mapper XML 包含批量插入 SQL
- [ ] 所有测试通过
---
## 后续工作
本实施计划完成后,可以进行以下扩展:
1. **创建 Service 层** - 实现 `IBankStatementService` 接口和实现类
2. **创建 Controller 层** - 提供 REST API 接口
3. **编写集成测试** - 测试完整的数据库插入流程
4. **添加业务逻辑** - 在 Service 层设置 `projectId` 等业务字段
5. **性能优化** - 根据实际数据量调整批量插入大小
---
**计划版本:** 1.0
**创建日期:** 2026-03-04

View File

@@ -0,0 +1,786 @@
# 创建项目集成流水分析平台实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 在创建项目时同步调用流水分析平台获取projectId并保存到数据库
**Architecture:** Service层集成LsfxAnalysisClient同步调用getToken接口使用Spring事务管理确保数据一致性
**Tech Stack:** Spring Boot 3.5.8, MyBatis Plus 3.0.5, Lombok, JUnit 5
---
## 前置条件
- [ ] 流水分析平台 Mock Server 可用http://localhost:8000
- [ ] 数据库连接正常
- [ ] 后端项目可正常启动
---
## Task 1: 执行数据库变更
**Files:**
- Execute: `doc/design/2026-03-04-add-lsfx-project-id.sql`
**Step 1: 连接数据库执行SQL脚本**
使用MCP工具连接数据库并执行SQL
```bash
# 或者使用MySQL客户端
mysql -h 116.62.17.81 -u root -p ccdi < doc/design/2026-03-04-add-lsfx-project-id.sql
```
**Step 2: 验证字段已添加**
```sql
SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_COMMENT
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = 'ccdi'
AND TABLE_NAME = 'ccdi_project'
AND COLUMN_NAME = 'lsfx_project_id';
```
预期结果:应返回字段信息,类型为 `INT(11)`,允许为空
**Step 3: 提交数据库变更**
```bash
git add doc/design/2026-03-04-add-lsfx-project-id.sql
git commit -m "chore: 添加流水分析平台项目ID字段到ccdi_project表"
```
---
## Task 2: 修改CcdiProject实体类
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/CcdiProject.java`
**Step 1: 打开实体类文件**
定位到 `lowRiskCount` 字段的位置约第50行
**Step 2: 添加新字段**
`lowRiskCount` 字段之后,`delFlag` 字段之前添加:
```java
/** 低风险人数 */
private Integer lowRiskCount;
/** 流水分析平台项目ID */
private Integer lsfxProjectId;
/** 删除标志0-存在2-删除 */
@TableLogic
private String delFlag;
```
**Step 3: 验证代码**
确保:
- Lombok 的 `@Data` 注解存在(会自动生成 getter/setter
- 字段顺序与数据库表结构一致
- 注释清晰
**Step 4: 提交变更**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/CcdiProject.java
git commit -m "feat: CcdiProject实体类添加lsfxProjectId字段"
```
---
## Task 3: 修改CcdiProjectVO视图对象
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectVO.java`
**Step 1: 打开VO文件**
定位到 `lowRiskCount` 字段的位置
**Step 2: 添加新字段**
`lowRiskCount` 字段之后添加:
```java
/** 低风险人数 */
private Integer lowRiskCount;
/** 流水分析平台项目ID */
private Integer lsfxProjectId;
/** 创建时间 */
private Date createTime;
```
**Step 3: 验证代码**
确保字段顺序与实体类一致
**Step 4: 提交变更**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectVO.java
git commit -m "feat: CcdiProjectVO添加lsfxProjectId字段"
```
---
## Task 4: 准备测试环境
**Files:**
- Create: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiProjectServiceImplTest.java`
**Step 1: 创建测试目录(如果不存在)**
```bash
mkdir -p ccdi-project/src/test/java/com/ruoyi/ccdi/project/service
```
**Step 2: 创建测试类**
```java
package com.ruoyi.ccdi.project.service;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSaveDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectVO;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.lsfx.client.LsfxAnalysisClient;
import com.ruoyi.lsfx.domain.request.GetTokenRequest;
import com.ruoyi.lsfx.domain.response.GetTokenResponse;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import static org.junit.jupiter.api.Assertions.*;
/**
* 项目Service测试类
*/
@SpringBootTest
@Transactional
public class CcdiProjectServiceImplTest {
@Resource
private ICcdiProjectService projectService;
@Resource
private LsfxAnalysisClient lsfxAnalysisClient;
@Test
public void testCreateProject_Success() {
// 准备数据
CcdiProjectSaveDTO dto = new CcdiProjectSaveDTO();
dto.setProjectName("测试项目");
dto.setDescription("测试描述");
dto.setConfigType("default");
// 执行
CcdiProjectVO result = projectService.createProject(dto);
// 验证
assertNotNull(result);
assertNotNull(result.getProjectId());
assertNotNull(result.getLsfxProjectId(), "流水分析平台项目ID不应为空");
assertEquals("测试项目", result.getProjectName());
}
}
```
**Step 3: 运行测试(预期失败)**
```bash
cd ccdi-project
mvn test -Dtest=CcdiProjectServiceImplTest#testCreateProject_Success
```
预期结果:测试失败,因为 `CcdiProjectServiceImpl` 尚未注入 `LsfxAnalysisClient`
**Step 4: 提交测试代码**
```bash
git add ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiProjectServiceImplTest.java
git commit -m "test: 添加项目创建集成流水分析平台的测试用例"
```
---
## Task 5: 修改CcdiProjectServiceImpl - 注入依赖
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java`
**Step 1: 打开Service实现类**
找到依赖注入区域约第25行
**Step 2: 添加LsfxAnalysisClient依赖**
```java
@Service
public class CcdiProjectServiceImpl implements ICcdiProjectService {
@Resource
private CcdiProjectMapper projectMapper;
@Resource
private LsfxAnalysisClient lsfxAnalysisClient; // 新增依赖注入
// ... 方法实现 ...
}
```
**Step 3: 添加必要的import**
```java
import com.ruoyi.lsfx.client.LsfxAnalysisClient;
import com.ruoyi.lsfx.domain.request.GetTokenRequest;
import com.ruoyi.lsfx.domain.response.GetTokenResponse;
```
**Step 4: 验证编译**
```bash
cd ccdi-project
mvn compile
```
预期结果:编译成功
**Step 5: 提交变更**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java
git commit -m "feat: CcdiProjectServiceImpl注入LsfxAnalysisClient依赖"
```
---
## Task 6: 实现callLsfxPlatform私有方法
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java`
**Step 1: 在类末尾添加私有方法**
`getStatusCounts()` 方法之后添加:
```java
/**
* 调用流水分析平台获取projectId
*
* @param projectName 项目名称
* @return 流水分析平台项目ID
* @throws ServiceException 调用失败或响应无效时抛出
*/
private Integer callLsfxPlatform(String projectName) {
// 构建请求参数
GetTokenRequest request = new GetTokenRequest();
request.setProjectNo("902000_" + System.currentTimeMillis());
request.setEntityName(projectName);
request.setUserId("902001");
request.setUserName("902001");
request.setRole("VIEWER");
request.setOrgCode("902000");
request.setAnalysisType("-1");
request.setDepartmentCode("902000");
// 调用流水分析平台(异常处理和日志已在 LsfxAnalysisClient 中完成)
GetTokenResponse response = lsfxAnalysisClient.getToken(request);
// 业务层校验:确保响应有效
if (response == null || response.getData() == null) {
throw new ServiceException("流水分析平台响应数据为空");
}
if (response.getData().getProjectId() == null) {
throw new ServiceException("流水分析平台返回的projectId为空");
}
// 校验返回码
if (!"200".equals(response.getCode())) {
throw new ServiceException("流水分析平台返回错误: " + response.getMessage());
}
return response.getData().getProjectId();
}
```
**Step 2: 验证编译**
```bash
cd ccdi-project
mvn compile
```
预期结果:编译成功
**Step 3: 提交变更**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java
git commit -m "feat: 实现callLsfxPlatform方法调用流水分析平台"
```
---
## Task 7: 修改createProject方法集成流水分析
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java`
**Step 1: 定位createProject方法**
找到 `createProject` 方法约第29行
**Step 2: 修改方法实现**
将现有实现修改为:
```java
@Override
public CcdiProjectVO createProject(CcdiProjectSaveDTO dto) {
// 1. 调用流水分析平台获取projectId
Integer lsfxProjectId = callLsfxPlatform(dto.getProjectName());
// 2. 创建项目实体
CcdiProject project = new CcdiProject();
BeanUtils.copyProperties(dto, project);
// 3. 设置默认值和流水分析平台ID
project.setStatus("0"); // 进行中
project.setIsArchived(0); // 未归档
project.setTargetCount(0);
project.setHighRiskCount(0);
project.setMediumRiskCount(0);
project.setLowRiskCount(0);
project.setLsfxProjectId(lsfxProjectId); // 设置流水分析平台ID
// 4. 保存到数据库
projectMapper.insert(project);
// 5. 返回VO
CcdiProjectVO vo = new CcdiProjectVO();
BeanUtils.copyProperties(project, vo);
return vo;
}
```
**Step 3: 确认事务注解存在**
确保方法上有 `@Transactional` 注解(如果没有则添加):
```java
@Override
@Transactional(rollbackFor = Exception.class)
public CcdiProjectVO createProject(CcdiProjectSaveDTO dto) {
// ...
}
```
需要添加import
```java
import org.springframework.transaction.annotation.Transactional;
```
**Step 4: 验证编译**
```bash
cd ccdi-project
mvn compile
```
预期结果:编译成功
**Step 5: 提交变更**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectServiceImpl.java
git commit -m "feat: createProject方法集成流水分析平台调用"
```
---
## Task 8: 运行单元测试验证功能
**Files:**
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiProjectServiceImplTest.java`
**Step 1: 确保Mock Server运行**
```bash
cd lsfx-mock-server
python app.py
```
确认输出:`Running on http://localhost:8000`
**Step 2: 运行测试**
```bash
cd ccdi-project
mvn test -Dtest=CcdiProjectServiceImplTest#testCreateProject_Success
```
预期结果:
```
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
```
**Step 3: 如果测试失败,检查日志**
常见问题:
- Mock Server 未启动:启动 Mock Server
- 数据库连接失败:检查数据库配置
- 字段映射错误:检查 BeanUtils.copyProperties 是否正常
**Step 4: 提交测试结果**
如果测试通过,无需额外提交(测试代码已提交)
---
## Task 9: 添加异常测试用例
**Files:**
- Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiProjectServiceImplTest.java`
**Step 1: 添加测试方法**
在测试类中添加:
```java
@Test
public void testCreateProject_WithNullProjectName() {
// 准备数据 - 项目名称为null
CcdiProjectSaveDTO dto = new CcdiProjectSaveDTO();
dto.setProjectName(null);
dto.setDescription("测试描述");
dto.setConfigType("default");
// 执行并验证异常
assertThrows(ServiceException.class, () -> {
projectService.createProject(dto);
});
}
```
**Step 2: 运行新测试**
```bash
cd ccdi-project
mvn test -Dtest=CcdiProjectServiceImplTest#testCreateProject_WithNullProjectName
```
预期结果:测试可能失败(因为没有验证项目名称),这是正常的
**Step 3: 提交测试代码**
```bash
git add ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiProjectServiceImplTest.java
git commit -m "test: 添加项目名称为null的异常测试用例"
```
---
## Task 10: 使用Swagger进行集成测试
**Files:**
- 无需修改文件
**Step 1: 启动后端应用**
```bash
cd ruoyi-admin
mvn spring-boot:run
```
等待启动完成,确认无错误
**Step 2: 访问Swagger UI**
浏览器打开:`http://localhost:8080/swagger-ui/index.html`
**Step 3: 找到创建项目接口**
导航到:`纪检初核项目管理``POST /ccdi/project`
**Step 4: 准备测试数据**
点击 "Try it out",输入请求体:
```json
{
"projectName": "集成测试项目001",
"description": "测试集成流水分析平台",
"configType": "default"
}
```
**Step 5: 执行请求**
点击 "Execute"
**Step 6: 验证响应**
检查响应体:
```json
{
"code": 200,
"msg": "项目创建成功",
"data": {
"projectId": 1,
"projectName": "集成测试项目001",
"lsfxProjectId": 77, // 必须有值
"description": "测试集成流水分析平台",
"configType": "default",
"status": "0",
...
}
}
```
**Step 7: 验证数据库**
使用MCP工具或MySQL客户端查询
```sql
SELECT project_id, project_name, lsfx_project_id, create_time
FROM ccdi_project
WHERE project_name = '集成测试项目001';
```
预期结果:`lsfx_project_id` 字段有值如77
**Step 8: 记录测试结果**
无代码提交,手动记录测试通过
---
## Task 11: 测试异常场景
**Files:**
- 无需修改文件
**Step 1: 停止Mock Server**
在 Mock Server 运行的终端按 `Ctrl+C` 停止
**Step 2: 尝试创建项目**
使用 Swagger 或 curl
```bash
curl -X POST http://localhost:8080/ccdi/project \
-H "Content-Type: application/json" \
-d '{
"projectName": "异常测试项目",
"description": "测试流水分析平台不可用",
"configType": "default"
}'
```
**Step 3: 验证响应**
预期结果:
```json
{
"code": 500,
"msg": "调用流水分析平台失败: ..." // 包含错误信息
}
```
**Step 4: 验证数据库没有脏数据**
```sql
SELECT COUNT(*) FROM ccdi_project WHERE project_name = '异常测试项目';
```
预期结果:`0`(事务已回滚)
**Step 5: 重启Mock Server**
```bash
cd lsfx-mock-server
python app.py
```
**Step 6: 验证功能恢复**
再次创建项目,确认功能正常
---
## Task 12: 清理和文档更新
**Files:**
- Modify: `doc/design/2026-03-04-create-project-integrate-lsfx-design.md`
**Step 1: 更新设计文档状态**
修改设计文档开头:
```markdown
**状态**: 已实施 ✅
```
**Step 2: 更新变更清单**
将变更清单中的状态更新为"已完成"
```markdown
| 类型 | 文件 | 变更内容 | 状态 |
|-----|------|---------|------|
| 数据库 | `ccdi_project` 表 | 新增 `lsfx_project_id` 字段 | ✅ 已完成 |
| SQL | `2026-03-04-add-lsfx-project-id.sql` | 数据库迁移脚本 | ✅ 已执行 |
| 实体类 | `CcdiProject.java` | 新增 `lsfxProjectId` 属性 | ✅ 已修改 |
| VO | `CcdiProjectVO.java` | 新增 `lsfxProjectId` 属性 | ✅ 已修改 |
| Service | `CcdiProjectServiceImpl.java` | 注入 `LsfxAnalysisClient`,添加调用逻辑 | ✅ 已修改 |
```
**Step 3: 提交文档更新**
```bash
git add doc/design/2026-03-04-create-project-integrate-lsfx-design.md
git commit -m "docs: 更新设计文档状态为已实施"
```
**Step 4: 创建实施总结**
```bash
git log --oneline --graph > doc/design/2026-03-04-implementation-summary.txt
```
提交总结:
```bash
git add doc/design/2026-03-04-implementation-summary.txt
git commit -m "docs: 添加实施总结"
```
---
## Task 13: 最终验收
**Files:**
- 无需修改文件
**Step 1: 运行所有测试**
```bash
cd ccdi-project
mvn test
```
预期结果:所有测试通过
**Step 2: 检查代码质量**
```bash
mvn checkstyle:check
```
如果失败,根据提示修复代码风格问题
**Step 3: 验证数据库字段**
```sql
DESC ccdi_project;
```
确认 `lsfx_project_id` 字段存在
**Step 4: 端到端测试**
通过前端或 Swagger 完整测试创建项目流程:
1. 创建项目成功
2. 查询项目列表,确认 `lsfxProjectId` 显示
3. 查询项目详情,确认 `lsfxProjectId` 正确
**Step 5: 记录验收结果**
在项目文档中记录:
- 功能验收通过 ✅
- 测试覆盖率100%
- 性能测试创建项目耗时约1-2秒取决于网络
- 异常处理:已验证
---
## 后续任务(可选)
### Task 14: 添加前端展示(可选)
如果需要在项目列表页面展示 `lsfxProjectId`
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/index.vue`
- Modify: `ruoyi-ui/src/api/ccdi/project.js`
**Step 1: 修改表格列定义**
`index.vue` 的表格列中添加:
```javascript
{
label: '流水分析项目ID',
prop: 'lsfxProjectId',
width: '120'
}
```
**Step 2: 提交前端变更**
```bash
git add ruoyi-ui/src/views/ccdiProject/index.vue
git commit -m "feat: 前端项目列表展示流水分析平台项目ID"
```
---
## 实施注意事项
### 关键点
1. **事务一致性**:确保 `@Transactional` 注解存在,任何异常都会回滚
2. **参数正确性**:严格按照《兰溪-流水分析对接-新版.md》配置固定参数
3. **错误提示**:给用户清晰的错误提示,便于排查问题
4. **测试覆盖**:必须测试成功和失败两种场景
### 常见问题
**Q1: 测试时提示找不到 LsfxAnalysisClient**
- 检查 ccdi-project 模块是否依赖 ccdi-lsfx 模块
- 检查 Spring 组件扫描是否包含 `com.ruoyi.lsfx`
**Q2: 数据库字段添加失败**
- 确认数据库连接正常
- 检查是否有权限修改表结构
**Q3: Mock Server 无法启动**
- 检查 8000 端口是否被占用
- 确认 Python 环境正常
**Q4: 事务回滚不生效**
- 确认方法上有 `@Transactional` 注解
- 检查异常是否被捕获后未重新抛出
---
## 参考资料
- 设计文档: `doc/design/2026-03-04-create-project-integrate-lsfx-design.md`
- 流水分析对接文档: `assets/对接流水分析/兰溪-流水分析对接-新版.md`
- 项目规范: `CLAUDE.md`
---
**计划创建完成!**

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,960 @@
# 项目详情页面导航菜单改造实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 将项目详情页面右侧的按钮组改为水平导航菜单,使用 Element UI Menu 组件实现简洁链接风格,支持菜单切换组件内容。
**Architecture:** 使用 Element UI 的 `el-menu` 组件(水平模式)替换现有的按钮组,通过 Vue 动态组件(`<component :is="...">`)实现内容切换。菜单项包括"上传数据"、"参数配置"和"初核结果"下拉菜单(含三个子项)。采用简洁链接风格,激活状态显示底部下划线。
**Tech Stack:** Vue 2.6.12, Element UI 2.15.14, Scoped CSS
---
## 前置检查
**验证当前项目状态:**
```bash
cd D:/ccdi/ccdi
git status
```
预期:工作目录干净,或只有 CLAUDE.md 修改
**验证文件存在:**
```bash
ls ruoyi-ui/src/views/ccdiProject/detail.vue
ls ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue
```
预期:两个文件都存在
---
## Task 1: 创建占位组件 ParamConfig
**Files:**
- Create: `ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue`
**Step 1: 创建 ParamConfig 占位组件**
创建文件 `ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue`:
```vue
<template>
<div class="param-config-container">
<div class="placeholder-content">
<i class="el-icon-setting"></i>
<p>参数配置功能开发中...</p>
</div>
</div>
</template>
<script>
export default {
name: "ParamConfig",
props: {
projectId: {
type: [String, Number],
default: null,
},
projectInfo: {
type: Object,
default: () => ({
projectName: "",
updateTime: "",
projectStatus: "0",
}),
},
},
};
</script>
<style lang="scss" scoped>
.param-config-container {
padding: 40px 20px;
background: #fff;
min-height: 400px;
}
.placeholder-content {
text-align: center;
color: #909399;
i {
font-size: 48px;
margin-bottom: 16px;
}
p {
font-size: 14px;
margin: 0;
}
}
</style>
```
**Step 2: 提交 ParamConfig 组件**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue
git commit -m "feat(ccdiProject): 添加参数配置占位组件"
```
---
## Task 2: 创建占位组件 PreliminaryCheck
**Files:**
- Create: `ruoyi-ui/src/views/ccdiProject/components/detail/PreliminaryCheck.vue`
**Step 1: 创建 PreliminaryCheck 占位组件**
创建文件 `ruoyi-ui/src/views/ccdiProject/components/detail/PreliminaryCheck.vue`:
```vue
<template>
<div class="preliminary-check-container">
<div class="placeholder-content">
<i class="el-icon-data-analysis"></i>
<p>结果总览功能开发中...</p>
</div>
</div>
</template>
<script>
export default {
name: "PreliminaryCheck",
props: {
projectId: {
type: [String, Number],
default: null,
},
projectInfo: {
type: Object,
default: () => ({
projectName: "",
updateTime: "",
projectStatus: "0",
}),
},
},
};
</script>
<style lang="scss" scoped>
.preliminary-check-container {
padding: 40px 20px;
background: #fff;
min-height: 400px;
}
.placeholder-content {
text-align: center;
color: #909399;
i {
font-size: 48px;
margin-bottom: 16px;
}
p {
font-size: 14px;
margin: 0;
}
}
</style>
```
**Step 2: 提交 PreliminaryCheck 组件**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/PreliminaryCheck.vue
git commit -m "feat(ccdiProject): 添加结果总览占位组件"
```
---
## Task 3: 创建占位组件 SpecialCheck
**Files:**
- Create: `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue`
**Step 1: 创建 SpecialCheck 占位组件**
创建文件 `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue`:
```vue
<template>
<div class="special-check-container">
<div class="placeholder-content">
<i class="el-icon-search"></i>
<p>专项排查功能开发中...</p>
</div>
</div>
</template>
<script>
export default {
name: "SpecialCheck",
props: {
projectId: {
type: [String, Number],
default: null,
},
projectInfo: {
type: Object,
default: () => ({
projectName: "",
updateTime: "",
projectStatus: "0",
}),
},
},
};
</script>
<style lang="scss" scoped>
.special-check-container {
padding: 40px 20px;
background: #fff;
min-height: 400px;
}
.placeholder-content {
text-align: center;
color: #909399;
i {
font-size: 48px;
margin-bottom: 16px;
}
p {
font-size: 14px;
margin: 0;
}
}
</style>
```
**Step 2: 提交 SpecialCheck 组件**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue
git commit -m "feat(ccdiProject): 添加专项排查占位组件"
```
---
## Task 4: 创建占位组件 DetailQuery
**Files:**
- Create: `ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue`
**Step 1: 创建 DetailQuery 占位组件**
创建文件 `ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue`:
```vue
<template>
<div class="detail-query-container">
<div class="placeholder-content">
<i class="el-icon-document"></i>
<p>流水明细查询功能开发中...</p>
</div>
</div>
</template>
<script>
export default {
name: "DetailQuery",
props: {
projectId: {
type: [String, Number],
default: null,
},
projectInfo: {
type: Object,
default: () => ({
projectName: "",
updateTime: "",
projectStatus: "0",
}),
},
},
};
</script>
<style lang="scss" scoped>
.detail-query-container {
padding: 40px 20px;
background: #fff;
min-height: 400px;
}
.placeholder-content {
text-align: center;
color: #909399;
i {
font-size: 48px;
margin-bottom: 16px;
}
p {
font-size: 14px;
margin: 0;
}
}
</style>
```
**Step 2: 提交 DetailQuery 组件**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue
git commit -m "feat(ccdiProject): 添加流水明细查询占位组件"
```
---
## Task 5: 修改 detail.vue - 添加数据字段和导入组件
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/detail.vue`
**Step 1: 添加组件导入**
`detail.vue``<script>` 部分,找到 import 语句(第 72 行附近),替换为:
```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";
```
**Step 2: 注册组件**
`components` 对象中(第 81-88 行),替换为:
```javascript
components: {
UploadData,
ParamConfig,
PreliminaryCheck,
SpecialCheck,
DetailQuery,
},
```
**Step 3: 添加数据字段**
`data()` 函数中(第 89-110 行),在 `activeTab: "data"` 之后添加:
```javascript
data() {
return {
// 当前激活的菜单项索引
activeTab: "upload",
// 当前显示的组件名称
currentComponent: "UploadData",
// 项目ID
projectId: this.$route.params.projectId,
// ... 其他现有数据保持不变
projectInfo: {
projectId: this.$route.params.projectId,
projectName: "",
projectDesc: "",
createTime: "",
updateTime: "",
startDate: "",
endDate: "",
targetCount: 0,
warningCount: 0,
warningThreshold: 60,
projectStatus: "0",
},
};
},
```
**Step 4: 验证文件语法正确**
```bash
cd ruoyi-ui
npm run lint -- --fix src/views/ccdiProject/detail.vue
```
预期:无语法错误
**Step 5: 提交组件导入修改**
```bash
git add ruoyi-ui/src/views/ccdiProject/detail.vue
git commit -m "feat(ccdiProject): 导入子组件并添加菜单状态数据"
```
---
## Task 6: 修改 detail.vue - 替换模板中的按钮为菜单
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/detail.vue`
**Step 1: 替换 header-right 中的按钮组**
找到 `<div class="header-right">` 部分(第 27-55 行),替换为:
```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>
```
**Step 2: 替换 UploadData 为动态组件**
找到 `<UploadData>` 组件(第 59-67 行),替换为:
```vue
<!-- 动态组件渲染区域 -->
<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"
/>
```
**Step 3: 验证模板语法**
```bash
cd ruoyi-ui
npm run lint -- --fix src/views/ccdiProject/detail.vue
```
预期:无语法错误
**Step 4: 提交模板修改**
```bash
git add ruoyi-ui/src/views/ccdiProject/detail.vue
git commit -m "feat(ccdiProject): 替换按钮组为导航菜单并使用动态组件"
```
---
## Task 7: 修改 detail.vue - 添加菜单选择处理方法
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/detail.vue`
**Step 1: 添加菜单选择处理方法**
`methods` 对象中(第 124 行),在 `handleBack()` 方法之后添加新方法:
```javascript
/** 菜单选择事件 */
handleMenuSelect(index) {
console.log("菜单选择:", index);
this.activeTab = index;
// 组件映射
const componentMap = {
upload: "UploadData",
config: "ParamConfig",
overview: "PreliminaryCheck",
special: "SpecialCheck",
detail: "DetailQuery",
};
this.currentComponent = componentMap[index] || "UploadData";
},
```
**Step 2: 删除废弃的方法**
删除以下不再使用的方法(第 226-251 行):
- `handleUploadData()`
- `handleParamConfig()`
- `handleCheckResultCommand()`
**Step 3: 更新 handleMenuChange 方法**
修改 `handleMenuChange` 方法(第 185-205 行),简化为:
```javascript
/** UploadData 组件:菜单切换 */
handleMenuChange({ key, route }) {
console.log("切换到菜单:", key, route);
// 直接触发菜单选择
this.handleMenuSelect(route);
},
```
**Step 4: 验证方法逻辑**
```bash
cd ruoyi-ui
npm run lint -- --fix src/views/ccdiProject/detail.vue
```
预期:无语法错误,未使用的方法已删除
**Step 5: 提交方法修改**
```bash
git add ruoyi-ui/src/views/ccdiProject/detail.vue
git commit -m "feat(ccdiProject): 添加菜单选择处理方法并清理废弃代码"
```
---
## Task 8: 修改 detail.vue - 添加导航菜单样式
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/detail.vue`
**Step 1: 添加导航菜单样式**
`<style lang="scss" scoped>` 部分(第 306 行之后),在 `.header-right` 样式块内部添加:
```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;
}
}
}
```
**Step 2: 添加下拉菜单弹窗样式**
在样式末尾(第 496 行之后),添加深度选择器样式:
```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;
}
}
}
```
**Step 3: 验证样式语法**
```bash
cd ruoyi-ui
npm run lint -- --fix src/views/ccdiProject/detail.vue
```
预期:无语法错误
**Step 4: 提交导航菜单样式**
```bash
git add ruoyi-ui/src/views/ccdiProject/detail.vue
git commit -m "style(ccdiProject): 添加导航菜单简洁链接风格样式"
```
---
## Task 9: 修改 detail.vue - 添加响应式样式
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/detail.vue`
**Step 1: 添加响应式样式**
在现有的 `@media (max-width: 768px)` 媒体查询中(第 464 行),找到 `.detail-header` 样式块,添加响应式菜单样式:
```scss
@media (max-width: 768px) {
.dpc-detail-container {
padding: 8px;
}
.detail-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
.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;
}
}
}
}
// ... 其他现有响应式样式保持不变
}
```
**Step 2: 验证响应式样式**
```bash
cd ruoyi-ui
npm run lint -- --fix src/views/ccdiProject/detail.vue
```
预期:无语法错误
**Step 3: 提交响应式样式**
```bash
git add ruoyi-ui/src/views/ccdiProject/detail.vue
git commit -m "style(ccdiProject): 添加导航菜单响应式布局支持"
```
---
## Task 10: 手动测试验证
**Files:**
- Test: `ruoyi-ui/src/views/ccdiProject/detail.vue`
**Step 1: 启动前端开发服务器**
```bash
cd ruoyi-ui
npm run dev
```
预期:服务启动成功,监听 http://localhost:80
**Step 2: 访问项目详情页面**
在浏览器中访问:`http://localhost/ccdiProject`,点击任意项目进入详情页面
预期:页面正常加载,右侧显示导航菜单
**Step 3: 测试菜单切换功能**
测试步骤:
1. 点击"上传数据"菜单项
- 预期:激活状态显示蓝色文字和底部下划线,显示 UploadData 组件
2. 点击"参数配置"菜单项
- 预期:激活状态切换,显示 ParamConfig 占位组件("参数配置功能开发中..."
3. 点击"初核结果"菜单,展开下拉菜单
- 预期:下拉菜单展开,显示三个子项
4. 点击"结果总览"子菜单项
- 预期:激活状态切换,显示 PreliminaryCheck 占位组件
5. 点击"专项排查"子菜单项
- 预期:显示 SpecialCheck 占位组件
6. 点击"流水明细查询"子菜单项
- 预期:显示 DetailQuery 占位组件
**Step 4: 测试样式效果**
测试步骤:
1. Hover 菜单项
- 预期:显示浅灰背景(#f5f7fa
2. 检查激活菜单项
- 预期:蓝色文字(#1890ff+ 底部 2px 蓝色下划线
3. 点击下拉菜单外部区域
- 预期:下拉菜单关闭
4. 调整浏览器窗口宽度至 768px 以下
- 预期:菜单项平均分配宽度,布局自适应
**Step 5: 测试数据传递**
测试步骤:
1. 切换到"参数配置"组件
2. 在浏览器控制台检查组件 props
- 预期projectId 和 projectInfo 正确传递
3. 点击"上传数据"中的功能按钮
- 预期:事件正常触发,原有功能不受影响
**Step 6: 记录测试结果**
创建测试报告文件 `docs/test-reports/2026-03-04-navigation-menu-test.md`:
```markdown
# 项目详情页面导航菜单改造测试报告
## 测试环境
- 浏览器: [记录浏览器名称和版本]
- 测试时间: 2026-03-04
- 测试人员: [你的名字]
## 功能测试
### 菜单切换
- [x] 点击"上传数据",显示 UploadData 组件
- [x] 点击"参数配置",显示 ParamConfig 占位组件
- [x] 点击"初核结果",下拉菜单展开
- [x] 点击"结果总览",显示 PreliminaryCheck 占位组件
- [x] 点击"专项排查",显示 SpecialCheck 占位组件
- [x] 点击"流水明细查询",显示 DetailQuery 占位组件
### 样式测试
- [x] 默认状态:灰色文字,透明背景
- [x] Hover 状态:浅灰背景,深色文字
- [x] 激活状态:蓝色文字 + 底部下划线
- [x] 下拉菜单样式统一
### 交互测试
- [x] 下拉菜单点击外部区域关闭
- [x] 菜单切换流畅无闪烁
- [x] 组件切换数据正确传递
### 响应式测试
- [x] 移动端菜单布局正常
- [x] 菜单项平均分配宽度
## 问题记录
[记录发现的任何问题]
## 测试结论
[通过/需要修复]
```
**Step 7: 提交测试报告**
```bash
git add docs/test-reports/2026-03-04-navigation-menu-test.md
git commit -m "test(ccdiProject): 添加导航菜单改造测试报告"
```
---
## Task 11: 清理和最终提交
**Step 1: 检查所有修改文件**
```bash
git status
```
预期:所有修改已提交
**Step 2: 查看提交历史**
```bash
git log --oneline -10
```
预期:看到 10 个新提交:
1. feat(ccdiProject): 添加参数配置占位组件
2. feat(ccdiProject): 添加结果总览占位组件
3. feat(ccdiProject): 添加专项排查占位组件
4. feat(ccdiProject): 添加流水明细查询占位组件
5. feat(ccdiProject): 导入子组件并添加菜单状态数据
6. feat(ccdiProject): 替换按钮组为导航菜单并使用动态组件
7. feat(ccdiProject): 添加菜单选择处理方法并清理废弃代码
8. style(ccdiProject): 添加导航菜单简洁链接风格样式
9. style(ccdiProject): 添加导航菜单响应式布局支持
10. test(ccdiProject): 添加导航菜单改造测试报告
**Step 3: 验证无遗留问题**
```bash
cd ruoyi-ui
npm run lint
```
预期:无 lint 错误
**Step 4: 推送到远程分支**
```bash
git push origin dev
```
预期:推送成功
---
## 实施后检查清单
- [ ] 所有 4 个占位组件已创建
- [ ] detail.vue 已修改完成(模板、脚本、样式)
- [ ] 导航菜单样式符合简洁链接风格
- [ ] 菜单切换功能正常
- [ ] 下拉菜单交互正常
- [ ] 响应式布局正常
- [ ] 所有修改已提交到 git
- [ ] 测试报告已完成
- [ ] 代码已推送到远程仓库
---
## 预期成果
### 文件创建
- `ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/PreliminaryCheck.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/SpecialCheck.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue`
- `docs/test-reports/2026-03-04-navigation-menu-test.md`
### 文件修改
- `ruoyi-ui/src/views/ccdiProject/detail.vue`(模板、脚本、样式)
### Git 提交
- 10 个功能清晰的提交记录
### 功能实现
- ✅ 水平导航菜单替代按钮组
- ✅ 简洁链接风格样式
- ✅ 菜单切换组件内容
- ✅ 下拉菜单支持
- ✅ 响应式布局
- ✅ 原有功能不受影响
---
## 潜在问题和解决方案
### 问题 1: 组件切换时状态丢失
**解决方案:** 使用 `<keep-alive>` 包裹动态组件(可选优化)
```vue
<keep-alive>
<component :is="currentComponent" ... />
</keep-alive>
```
### 问题 2: 下拉菜单样式不生效
**解决方案:** 检查 `::v-deep` 是否被正确编译,可能需要使用 `/deep/``>>>`
### 问题 3: 移动端菜单换行
**解决方案:** 调整响应式断点或使用折叠菜单el-menu 的 collapse 模式)
### 问题 4: 原有事件未触发
**解决方案:** 检查动态组件的事件绑定是否完整,确保所有事件都有对应的处理方法
---
## 后续优化建议
1. **添加组件切换动画**
```vue
<transition name="fade" mode="out-in">
<component :is="currentComponent" ... />
</transition>
```
2. **实现占位组件的完整功能**
- ParamConfig: 模型参数配置界面
- PreliminaryCheck: 结果总览数据展示
- SpecialCheck: 专项排查功能
- DetailQuery: 流水明细查询和筛选
3. **添加菜单权限控制**
- 根据用户权限显示/隐藏菜单项
- 使用 `v-if` 或动态生成菜单配置
4. **添加面包屑导航**
- 在页面顶部显示当前位置
- 支持快速返回上级页面
5. **添加快捷键支持**
- Ctrl+Tab: 切换到下一个菜单
- Ctrl+Shift+Tab: 切换到上一个菜单
---
## 相关技能参考
- @superpowers:brainstorming - 需求分析和设计
- @superpowers:test-driven-development - TDD 开发流程
- @superpowers:verification-before-completion - 完成前验证
- @superpowers:requesting-code-review - 代码审查
---
## 文档参考
- 设计文档: `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

@@ -0,0 +1,372 @@
# 银行流水审计字段补充实现计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 为 GetBankStatementResponse.BankStatementItem 类添加 createdBy 和 createDate 两个审计字段,使其能够接收外部流水分析平台返回的审计信息。
**Architecture:** 在响应类的 BankStatementItem 内部类中添加两个审计字段Lombok @Data 注解会自动生成 getter/setter无需手动编写。字段类型为 Long 和 String与外部平台接口文档对齐。
**Tech Stack:** Java 21, Lombok, Jackson (JSON 序列化/反序列化)
---
## Task 1: 添加审计字段到响应类
**Files:**
- Modify: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/GetBankStatementResponse.java:189-190`
**Step 1: 打开响应类文件**
在编辑器中打开文件:
```
ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/GetBankStatementResponse.java
```
定位到 `BankStatementItem` 内部类的最后,找到第 189 行附近(在 `trxBalance` 字段之后)。
**Step 2: 添加审计字段**
在第 189 行之后添加以下代码:
```java
/** 交易余额 */
private BigDecimal trxBalance;
// ===== 审计字段 =====
/** 创建者 */
private Long createdBy;
/** 创建时间 */
private String createDate;
}
```
**完整修改后的类尾部:**
```java
/** 转换余额 */
private BigDecimal transfromBalanceAmount;
/** 交易余额 */
private BigDecimal trxBalance;
// ===== 审计字段 =====
/** 创建者 */
private Long createdBy;
/** 创建时间 */
private String createDate;
}
```
**Step 3: 验证代码编译**
运行以下命令验证代码编译通过:
```bash
cd D:/ccdi/ccdi
mvn clean compile -pl ccdi-lsfx -am
```
Expected: `BUILD SUCCESS`
**Step 4: 提交代码**
```bash
git add ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/GetBankStatementResponse.java
git commit -m "feat(ccdi-lsfx): 添加银行流水审计字段 createdBy 和 createDate
- 在 GetBankStatementResponse.BankStatementItem 中添加 createdBy 字段Long 类型)
- 在 GetBankStatementResponse.BankStatementItem 中添加 createDate 字段String 类型)
- 补充外部流水分析平台接口文档 6.5 节中定义的审计字段
- 支持接收外部平台返回的创建者和创建时间信息"
```
---
## Task 2: 验证 JSON 反序列化(可选但推荐)
**Files:**
- Create: `ccdi-lsfx/src/test/java/com/ruoyi/lsfx/domain/response/GetBankStatementResponseTest.java`
**Step 1: 创建测试类**
创建测试文件:
```
ccdi-lsfx/src/test/java/com/ruoyi/lsfx/domain/response/GetBankStatementResponseTest.java
```
**Step 2: 编写测试代码**
```java
package com.ruoyi.lsfx.domain.response;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.*;
/**
* GetBankStatementResponse 单元测试
*/
class GetBankStatementResponseTest {
private final ObjectMapper objectMapper = new ObjectMapper();
@Test
void testDeserializeBankStatementItem() throws Exception {
// 准备测试数据(包含审计字段)
String json = """
{
"code": "0",
"status": "success",
"successResponse": true,
"data": {
"bankStatementList": [
{
"bankStatementId": 123456,
"leId": 100,
"accountId": 200,
"leName": "测试企业",
"accountMaskNo": "6222****1234",
"trxDate": "2026-03-05",
"currency": "CNY",
"drAmount": 1000.00,
"crAmount": 0,
"balanceAmount": 5000.00,
"createdBy": 12345,
"createDate": "2026-03-05 14:30:00"
}
],
"totalCount": 1
}
}
""";
// 反序列化
GetBankStatementResponse response = objectMapper.readValue(json, GetBankStatementResponse.class);
// 验证基本字段
assertNotNull(response);
assertEquals("0", response.getCode());
assertEquals("success", response.getStatus());
assertTrue(response.getSuccessResponse());
// 验证数据列表
assertNotNull(response.getData());
assertNotNull(response.getData().getBankStatementList());
assertEquals(1, response.getData().getTotalCount());
// 验证流水项
GetBankStatementResponse.BankStatementItem item = response.getData().getBankStatementList().get(0);
assertNotNull(item);
assertEquals(123456L, item.getBankStatementId());
assertEquals(100, item.getLeId());
assertEquals("测试企业", item.getLeName());
// 验证审计字段
assertEquals(12345L, item.getCreatedBy());
assertEquals("2026-03-05 14:30:00", item.getCreateDate());
}
@Test
void testDeserializeWithNullAuditFields() throws Exception {
// 测试审计字段为 null 的情况
String json = """
{
"code": "0",
"data": {
"bankStatementList": [
{
"bankStatementId": 123456
}
],
"totalCount": 1
}
}
""";
GetBankStatementResponse response = objectMapper.readValue(json, GetBankStatementResponse.class);
GetBankStatementResponse.BankStatementItem item = response.getData().getBankStatementList().get(0);
// 审计字段应该为 null
assertNull(item.getCreatedBy());
assertNull(item.getCreateDate());
}
}
```
**Step 3: 运行测试**
```bash
cd D:/ccdi/ccdi
mvn test -Dtest=GetBankStatementResponseTest -pl ccdi-lsfx
```
Expected: `Tests run: 2, Failures: 0, Errors: 0, Skipped: 0`
**Step 4: 提交测试代码**
```bash
git add ccdi-lsfx/src/test/java/com/ruoyi/lsfx/domain/response/GetBankStatementResponseTest.java
git commit -m "test(ccdi-lsfx): 添加银行流水响应类单元测试
- 测试 JSON 反序列化能正确映射 createdBy 和 createDate 字段
- 测试审计字段为 null 时的处理
- 验证字段类型和值的正确性"
```
---
## Task 3: 集成测试验证
**Files:**
- Modify: `lsfx-mock-server/app.py` (如果需要更新 mock 服务器)
- Test: 使用 Swagger UI 或 curl 测试接口
**Step 1: 检查 mock 服务器是否返回审计字段**
检查 `lsfx-mock-server/app.py` 文件,确认银行流水接口返回的数据中包含 `createdBy``createDate` 字段。
如果 mock 服务器未返回这两个字段,添加以下内容到响应中:
```python
# 在 bank_statement_data 字典中添加
'createdBy': 12345,
'createDate': '2026-03-05 14:30:00',
```
**Step 2: 启动后端服务**
提示用户手动启动后端服务:
```bash
# 在项目根目录执行
mvn spring-boot:run
# 或者运行启动脚本
ry.bat
```
**Step 3: 启动 mock 服务器(新终端)**
```bash
cd lsfx-mock-server
python app.py
```
Expected: Mock 服务器在 http://localhost:8000 启动
**Step 4: 使用 Swagger UI 测试接口**
1. 打开浏览器访问: http://localhost:8080/swagger-ui/index.html
2. 找到 "流水分析平台接口测试" 分组
3. 点击 "POST /lsfx/test/getBankStatement" 接口
4. 点击 "Try it out"
5. 输入测试参数:
```json
{
"groupId": 1,
"logId": 1,
"pageNow": 1,
"pageSize": 10
}
```
6. 点击 "Execute"
7. 查看响应,验证 `createdBy``createDate` 字段存在
Expected: 响应中的 `bankStatementList` 包含 `createdBy``createDate` 字段
**Step 5: 使用 curl 测试(可选)**
```bash
curl -X POST "http://localhost:8080/lsfx/test/getBankStatement" \
-H "Content-Type: application/json" \
-d '{
"groupId": 1,
"logId": 1,
"pageNow": 1,
"pageSize": 10
}'
```
Expected: JSON 响应中包含 `createdBy``createDate` 字段
**Step 6: 提交 mock 服务器更新(如果有修改)**
```bash
git add lsfx-mock-server/app.py
git commit -m "feat(lsfx-mock): 添加银行流水审计字段到 mock 响应
- 添加 createdBy 字段用户ID
- 添加 createDate 字段(创建时间)
- 与外部平台接口文档 6.5 节对齐"
```
---
## Task 4: 更新文档(可选)
**Files:**
- Update: `docs/design/2026-03-05-bank-statement-audit-fields-design.md`(已存在)
**Step 1: 验证设计文档完整性**
确认设计文档包含以下内容:
- ✅ 问题描述
- ✅ 字段定义
- ✅ 代码修改
- ✅ 测试计划
- ✅ 风险评估
**Step 2: 更新 API 文档(如果有)**
如果项目中有 API 文档文件,更新银行流水接口的响应字段说明,添加:
- `createdBy`: 创建者用户IDLong 类型)
- `createDate`: 创建时间String 类型)
**Step 3: 提交文档更新**
```bash
git add docs/
git commit -m "docs: 更新银行流水接口文档,补充审计字段说明"
```
---
## 完成清单
- [ ] Task 1: 添加审计字段到响应类
- [ ] Task 2: 验证 JSON 反序列化(可选但推荐)
- [ ] Task 3: 集成测试验证
- [ ] Task 4: 更新文档(可选)
## 验收标准
1.`GetBankStatementResponse.BankStatementItem` 类包含 `createdBy``createDate` 字段
2. ✅ 字段类型正确:`createdBy` 为 Long`createDate` 为 String
3. ✅ 代码编译通过
4. ✅ 单元测试通过(如果编写)
5. ✅ 集成测试通过,能正确接收外部平台的审计字段
6. ✅ 代码已提交到 git
## 风险与缓解
| 风险 | 缓解措施 |
|------|----------|
| 外部平台不返回审计字段 | 字段可以为 null不影响现有功能 |
| 日期格式不一致 | 使用 String 类型接收,业务层处理转换 |
| JSON 反序列化失败 | 编写单元测试验证,使用 Jackson 注解处理格式 |
## 参考资料
- 设计文档: `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

@@ -0,0 +1,257 @@
# 银行流水接口字段补充实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 补充 `uploadSequnceNumber` 字段的接收和映射,确保流水分析接口返回的上传序号正确存储到数据库。
**Architecture:** 在响应类中添加字段定义接收接口返回值,在实体转换方法中映射到 `batchSequence` 字段,通过 MyBatis Plus 自动持久化到数据库的 `batch_sequence` 列。
**Tech Stack:** Java 21, Lombok, Spring Boot 3.5.8, MyBatis Plus
---
## Task 1: 响应类添加字段
**Files:**
- Modify: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/GetBankStatementResponse.java:132`
**Step 1: 在 BankStatementItem 内部类中添加字段**
`batchId` 字段(第 132 行)之后添加:
```java
/** 上传序号 */
private Integer uploadSequnceNumber;
```
完整上下文:
```java
/** 上传logId */
private Integer batchId;
/** 上传序号 */
private Integer uploadSequnceNumber;
/** 项目id */
private Integer groupId;
```
**Step 2: 验证 Lombok 注解生效**
确认 `@Data` 注解在 `BankStatementItem` 类上Lombok 会自动生成 getter/setter
```java
@Data
public static class BankStatementItem {
// ... 其他字段
private Integer batchId;
private Integer uploadSequnceNumber; // 新增字段
// ... 其他字段
}
```
---
## Task 2: 实体转换方法添加映射
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatement.java:201`
**Step 1: 在 fromResponse() 方法中添加字段映射**
在第 201 行(`entity.setCustomerAccountName(item.getCustomerName());` 之后)添加:
```java
entity.setBatchSequence(item.getUploadSequnceNumber());
```
完整上下文:
```java
// 4. 手动映射字段名不一致的情况
entity.setLeAccountNo(item.getAccountMaskNo());
entity.setCustomerAccountNo(item.getCustomerAccountMaskNo());
entity.setLeAccountName(item.getLeName());
entity.setAmountDr(item.getDrAmount());
entity.setAmountCr(item.getCrAmount());
entity.setAmountBalance(item.getBalanceAmount());
entity.setTrxFlag(item.getTransFlag());
entity.setTrxType(item.getTransTypeId());
entity.setCustomerLeId(item.getCustomerId());
entity.setCustomerAccountName(item.getCustomerName());
entity.setBatchSequence(item.getUploadSequnceNumber()); // 新增映射
// 5. 特殊字段处理
entity.setMetaJson(null); // 根据文档要求强制设为 null
```
**Step 2: 验证映射逻辑**
确认:
- 源字段:`item.getUploadSequnceNumber()` 返回 `Integer`
- 目标字段:`entity.setBatchSequence()` 接受 `Integer`
- 类型匹配,无需类型转换
---
## Task 3: 编译验证
**Files:**
- 无文件修改
**Step 1: 编译项目**
在项目根目录执行:
```bash
mvn clean compile
```
**预期输出:**
```
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: X.XXX s
[INFO] Finished at: 2026-03-05T...
[INFO] ------------------------------------------------------------------------
```
**Step 2: 检查编译错误(如果有)**
如果出现编译错误,检查:
- 字段名拼写是否正确:`uploadSequnceNumber`注意Sequence 不是 Sequence
- Lombok 注解处理器是否正确配置
- 导入语句是否需要补充(通常 Lombok 不需要额外导入)
---
## Task 4: 代码审查
**Files:**
- 无文件修改
**Step 1: 检查字段命名一致性**
对比文档 `assets/对接流水分析/ccdi_bank_statement.md:81`
```
| 28 | batch_sequence | uploadSequnceNumber |
```
确认:
- 响应类字段名:`uploadSequnceNumber`(与文档一致)
- 实体类字段名:`batchSequence`(与数据库列名 `batch_sequence` 对应)
**Step 2: 检查空值处理**
确认 `Integer` 类型允许 null 值:
- 接口返回 null 时,`item.getUploadSequnceNumber()` 返回 null
- `entity.setBatchSequence(null)` 设置 null 值
- MyBatis Plus 将 null 写入数据库
**Step 3: 检查 BeanUtils.copyProperties 行为**
确认 `BeanUtils.copyProperties(item, entity)` 不会自动映射该字段:
- 源字段名:`uploadSequnceNumber`
- 目标字段名:`batchSequence`
- 字段名不一致BeanUtils 不会自动复制
- 必须手动映射(已在 Task 2 添加)
---
## Task 5: 提交代码
**Files:**
- 无文件修改
**Step 1: 查看修改内容**
```bash
git diff
```
**预期输出:**
```diff
diff --git a/ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/GetBankStatementResponse.java b/ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/GetBankStatementResponse.java
index ...
--- a/ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/GetBankStatementResponse.java
+++ b/ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/GetBankStatementResponse.java
@@ -132,6 +132,9 @@ public class GetBankStatementResponse {
/** 上传logId */
private Integer batchId;
+ /** 上传序号 */
+ private Integer uploadSequnceNumber;
+
/** 项目id */
private Integer groupId;
diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatement.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatement.java
index ...
--- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatement.java
+++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatement.java
@@ -199,6 +199,7 @@ public class CcdiBankStatement implements Serializable {
entity.setTrxType(item.getTransTypeId());
entity.setCustomerLeId(item.getCustomerId());
entity.setCustomerAccountName(item.getCustomerName());
+ entity.setBatchSequence(item.getUploadSequnceNumber());
// 5. 特殊字段处理
entity.setMetaJson(null); // 根据文档要求强制设为 null
```
**Step 2: 添加到暂存区**
```bash
git add ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/GetBankStatementResponse.java
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatement.java
```
**Step 3: 提交更改**
```bash
git commit -m "fix: 补充银行流水接口 uploadSequnceNumber 字段接收和映射
- 在 GetBankStatementResponse.BankStatementItem 中添加 uploadSequnceNumber 字段
- 在 CcdiBankStatement.fromResponse() 中添加字段映射到 batchSequence
- 修复流水分析接口返回的上传序号数据丢失问题"
```
**预期输出:**
```
[dev abc1234] fix: 补充银行流水接口 uploadSequnceNumber 字段接收和映射
2 files changed, 2 insertions(+)
```
---
## 验收清单
- [ ] 响应类 `GetBankStatementResponse.BankStatementItem` 包含 `uploadSequnceNumber` 字段
- [ ] Lombok `@Data` 注解为该字段生成 getter/setter
- [ ] 实体转换方法 `fromResponse()` 包含 `batchSequence` 字段映射
- [ ] 项目编译成功(`mvn clean compile`
- [ ] 字段命名与文档 `assets/对接流水分析/ccdi_bank_statement.md` 一致
- [ ] 代码已提交到 git
---
## 后续验证(可选)
如需进一步验证功能,可以:
1. **接口测试**:调用流水分析接口,检查响应数据是否包含 `uploadSequnceNumber` 字段
2. **数据验证**:查询数据库 `ccdi_bank_statement` 表,检查 `batch_sequence` 列是否有正确的值
3. **日志检查**:在转换方法中添加日志,确认字段值正确传递
---
## 参考资料
- 设计文档:`docs/design/2026-03-05-bank-statement-field-design.md`
- 字段映射文档:`assets/对接流水分析/ccdi_bank_statement.md`
- 接口文档:`assets/对接流水分析/兰溪-流水分析对接-新版.md`

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,821 @@
# 模型参数配置页面优化实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**目标:** 优化模型参数配置页面,取消模型下拉切换,改为垂直堆叠展示所有模型参数,并实现统一保存
**架构:** 采用前后端分离架构,后端新增批量查询和批量保存接口,前端重构两个配置页面使用统一布局
**技术栈:** Spring Boot 3.5.8 + MyBatis Plus 3.0.5 + Vue 2.6.12 + Element UI 2.15.14
**设计文档:** `docs/design/2026-03-06-model-param-config-optimization-design.md`
---
## 后端开发任务
### Task 1: 创建批量查询请求DTO
**文件:**
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/ModelParamAllQueryDTO.java`
**步骤 1: 创建 ModelParamAllQueryDTO 类**
```java
package com.ruoyi.ccdi.project.domain.dto;
import lombok.Data;
/**
* 批量查询所有模型参数DTO
*/
@Data
public class ModelParamAllQueryDTO {
/** 项目ID0表示全局配置>0表示项目配置 */
private Long projectId;
}
```
**步骤 2: 提交代码**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/ModelParamAllQueryDTO.java
git commit -m "feat: 添加批量查询所有模型参数DTO"
```
---
### Task 2: 创建模型分组VO
**文件:**
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/ModelGroupVO.java`
**步骤 1: 创建 ModelGroupVO 类**
```java
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
import java.util.List;
/**
* 模型分组VO用于按模型分组展示参数
*/
@Data
public class ModelGroupVO {
/** 模型编码 */
private String modelCode;
/** 模型名称 */
private String modelName;
/** 参数列表 */
private List<ModelParamVO> params;
}
```
**步骤 2: 提交代码**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/ModelGroupVO.java
git commit -m "feat: 添加模型分组VO"
```
---
### Task 3: 创建批量查询响应VO
**文件:**
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/ModelParamAllVO.java`
**步骤 1: 创建 ModelParamAllVO 类**
```java
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
import java.util.List;
/**
* 批量查询所有模型参数响应VO
*/
@Data
public class ModelParamAllVO {
/** 模型列表(包含每个模型及其参数) */
private List<ModelGroupVO> models;
}
```
**步骤 2: 提交代码**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/ModelParamAllVO.java
git commit -m "feat: 添加批量查询所有模型参数响应VO"
```
---
### Task 4: 创建批量保存参数项DTO
**文件:**
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/ParamValueItem.java`
**步骤 1: 创建 ParamValueItem 类**
```java
package com.ruoyi.ccdi.project.domain.dto;
import lombok.Data;
/**
* 参数值项DTO
*/
@Data
public class ParamValueItem {
/** 参数编码 */
private String paramCode;
/** 参数值 */
private String paramValue;
}
```
**步骤 2: 提交代码**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/ParamValueItem.java
git commit -m "feat: 添加参数值项DTO"
```
---
### Task 5: 创建批量保存模型参数组DTO
**文件:**
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/ModelParamGroupDTO.java`
**步骤 1: 创建 ModelParamGroupDTO 类**
```java
package com.ruoyi.ccdi.project.domain.dto;
import lombok.Data;
import java.util.List;
/**
* 模型参数分组DTO用于批量保存
*/
@Data
public class ModelParamGroupDTO {
/** 模型编码 */
private String modelCode;
/** 该模型下修改过的参数 */
private List<ParamValueItem> params;
}
```
**步骤 2: 提交代码**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/ModelParamGroupDTO.java
git commit -m "feat: 添加模型参数分组DTO"
```
---
### Task 6: 创建批量保存请求DTO
**文件:**
- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/ModelParamSaveAllDTO.java`
**步骤 1: 创建 ModelParamSaveAllDTO 类**
```java
package com.ruoyi.ccdi.project.domain.dto;
import lombok.Data;
import java.util.List;
/**
* 批量保存所有模型参数DTO
*/
@Data
public class ModelParamSaveAllDTO {
/** 项目ID */
private Long projectId;
/** 所有模型的参数修改(只包含修改过的参数) */
private List<ModelParamGroupDTO> models;
}
```
**步骤 2: 提交代码**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/ModelParamSaveAllDTO.java
git commit -m "feat: 添加批量保存所有模型参数DTO"
```
---
### Task 7: 在Mapper接口中添加批量查询方法
**文件:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiModelParamMapper.java`
**步骤 1: 添加 selectByProjectId 方法**
打开 `CcdiModelParamMapper.java` 文件,在接口中添加新方法:
```java
/**
* 根据项目ID查询所有模型参数
*
* @param projectId 项目ID
* @return 参数列表
*/
List<CcdiModelParam> selectByProjectId(@Param("projectId") Long projectId);
```
**步骤 2: 检查导入语句**
确保文件顶部包含必要的导入:
```java
import org.apache.ibatis.annotations.Param;
import java.util.List;
```
**步骤 3: 提交代码**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiModelParamMapper.java
git commit -m "feat: 在Mapper接口中添加批量查询方法"
```
---
### Task 8: 在Mapper XML中添加SQL查询
**文件:**
- Modify: `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiModelParamMapper.xml`
**步骤 1: 添加 selectByProjectId SQL**
打开 `CcdiModelParamMapper.xml` 文件,在 `<mapper>` 标签内添加:
```xml
<!-- 根据项目ID查询所有模型参数 -->
<select id="selectByProjectId" resultType="CcdiModelParam">
SELECT * FROM ccdi_model_param
WHERE project_id = #{projectId}
ORDER BY model_code, sort_order
</select>
```
**步骤 2: 提交代码**
```bash
git add ccdi-project/src/main/resources/mapper/ccdi/project/CcdiModelParamMapper.xml
git commit -m "feat: 在Mapper XML中添加批量查询SQL"
```
---
### Task 9: 在Service接口中添加批量查询方法
**文件:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiModelParamService.java`
**步骤 1: 添加 selectAllParams 方法签名**
打开 `ICcdiModelParamService.java` 文件,在接口中添加:
```java
/**
* 查询所有模型及其参数(按模型分组)
*
* @param projectId 项目ID0表示全局配置
* @return 所有模型的参数配置
*/
ModelParamAllVO selectAllParams(Long projectId);
```
**步骤 2: 添加导入语句**
在文件顶部添加:
```java
import com.ruoyi.ccdi.project.domain.vo.ModelParamAllVO;
```
**步骤 3: 提交代码**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiModelParamService.java
git commit -m "feat: 在Service接口中添加批量查询方法"
```
---
### Task 10: 在Service接口中添加批量保存方法
**文件:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiModelParamService.java`
**步骤 1: 添加 saveAllParams 方法签名**
打开 `ICcdiModelParamService.java` 文件,在接口中添加:
```java
/**
* 批量保存所有模型的参数修改
*
* @param saveAllDTO 所有模型的参数修改数据
*/
void saveAllParams(ModelParamSaveAllDTO saveAllDTO);
```
**步骤 2: 添加导入语句**
在文件顶部添加:
```java
import com.ruoyi.ccdi.project.domain.dto.ModelParamSaveAllDTO;
```
**步骤 3: 提交代码**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiModelParamService.java
git commit -m "feat: 在Service接口中添加批量保存方法"
```
---
### Task 11: 实现批量查询方法(第一部分)
**文件:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java`
**步骤 1: 添加必要的导入语句**
在文件顶部的导入区域添加:
```java
import com.ruoyi.ccdi.project.domain.dto.ModelParamSaveAllDTO;
import com.ruoyi.ccdi.project.domain.dto.ModelParamGroupDTO;
import com.ruoyi.ccdi.project.domain.dto.ParamValueItem;
import com.ruoyi.ccdi.project.domain.vo.ModelParamAllVO;
import com.ruoyi.ccdi.project.domain.vo.ModelGroupVO;
import java.util.Comparator;
```
**步骤 2: 提交代码**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java
git commit -m "feat: 添加批量操作所需的导入语句"
```
---
### Task 12: 实现批量查询方法(第二部分)
**文件:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java`
**步骤 1: 实现 selectAllParams 方法**
`CcdiModelParamServiceImpl` 类中添加方法实现:
```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;
}
```
**步骤 2: 提交代码**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java
git commit -m "feat: 实现批量查询所有模型参数方法"
```
---
### Task 13: 实现批量保存方法
**文件:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java`
**步骤 1: 实现 saveAllParams 方法**
`CcdiModelParamServiceImpl` 类中添加方法实现:
```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. 批量更新所有模型的参数值
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: 提交代码**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java
git commit -m "feat: 实现批量保存所有模型参数方法"
```
---
### Task 14: 在Controller中添加批量查询接口
**文件:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiModelParamController.java`
**步骤 1: 添加必要的导入语句**
在文件顶部添加:
```java
import com.ruoyi.ccdi.project.domain.dto.ModelParamAllQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.ModelParamSaveAllDTO;
import com.ruoyi.ccdi.project.domain.vo.ModelParamAllVO;
```
**步骤 2: 添加 listAll 接口方法**
`CcdiModelParamController` 类中添加:
```java
/**
* 查询所有模型及其参数(按模型分组)
*/
@Operation(summary = "查询所有模型及其参数")
@GetMapping("/listAll")
public AjaxResult listAll(@Validated ModelParamAllQueryDTO queryDTO) {
ModelParamAllVO result = modelParamService.selectAllParams(queryDTO.getProjectId());
return success(result);
}
```
**步骤 3: 提交代码**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiModelParamController.java
git commit -m "feat: 在Controller中添加批量查询接口"
```
---
### Task 15: 在Controller中添加批量保存接口
**文件:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiModelParamController.java`
**步骤 1: 添加 saveAll 接口方法**
`CcdiModelParamController` 类中添加:
```java
/**
* 批量保存所有模型的参数修改
*/
@Operation(summary = "批量保存所有模型参数")
@Log(title = "模型参数配置", businessType = BusinessType.UPDATE)
@PostMapping("/saveAll")
public AjaxResult saveAll(@Validated @RequestBody ModelParamSaveAllDTO saveAllDTO) {
modelParamService.saveAllParams(saveAllDTO);
return success("保存成功");
}
```
**步骤 2: 提交代码**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiModelParamController.java
git commit -m "feat: 在Controller中添加批量保存接口"
```
---
### Task 16: 使用Swagger测试后端接口
**步骤 1: 启动后端应用**
提示用户手动启动后端应用:
```bash
# 在项目根目录执行
mvn spring-boot:run
```
**步骤 2: 访问Swagger UI**
打开浏览器访问:`http://localhost:8080/swagger-ui/index.html`
**步骤 3: 测试批量查询接口**
1. 找到"模型参数配置"分组
2. 找到 `GET /ccdi/modelParam/listAll` 接口
3. 点击 "Try it out"
4. 输入参数:
- `projectId`: 0 (测试全局配置)
5. 点击 "Execute"
6. 验证响应:
- 状态码200
- 返回数据包含 `models` 数组
- 每个模型包含 `modelCode`, `modelName`, `params`
**预期结果:**
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"models": [
{
"modelCode": "LARGE_TRANSACTION",
"modelName": "大额交易模型",
"params": [...]
},
...
]
}
}
```
**步骤 4: 测试批量保存接口**
1. 找到 `POST /ccdi/modelParam/saveAll` 接口
2. 点击 "Try it out"
3. 输入请求体:
```json
{
"projectId": 0,
"models": [
{
"modelCode": "LARGE_TRANSACTION",
"params": [
{
"paramCode": "THRESHOLD_AMOUNT",
"paramValue": "60000"
}
]
}
]
}
```
4. 点击 "Execute"
5. 验证响应:状态码 200msg 为 "保存成功"
**步骤 5: 提交测试记录**
记录测试结果并提交(如果需要):
```bash
git add docs/tests/records/
git commit -m "test: 记录后端接口测试结果"
```
---
## 前端开发任务
### Task 17: 在API层添加批量查询方法
**文件:**
- Modify: `ruoyi-ui/src/api/ccdi/modelParam.js`
**步骤 1: 添加 listAllParams 方法**
打开 `modelParam.js` 文件,添加:
```javascript
/**
* 查询所有模型及其参数(按模型分组)
* @param {Object} query - 查询参数
* @param {Number} query.projectId - 项目ID0表示全局配置
*/
export function listAllParams(query) {
return request({
url: '/ccdi/modelParam/listAll',
method: 'get',
params: query
})
}
```
**步骤 2: 提交代码**
```bash
git add ruoyi-ui/src/api/ccdi/modelParam.js
git commit -m "feat: 在API层添加批量查询方法"
```
---
### Task 18: 在API层添加批量保存方法
**文件:**
- Modify: `ruoyi-ui/src/api/ccdi/modelParam.js`
**步骤 1: 添加 saveAllParams 方法**
打开 `modelParam.js` 文件,添加:
```javascript
/**
* 批量保存所有模型的参数修改
* @param {Object} data - 保存数据
* @param {Number} data.projectId - 项目ID
* @param {Array} data.models - 模型参数列表
*/
export function saveAllParams(data) {
return request({
url: '/ccdi/modelParam/saveAll',
method: 'post',
data: data
})
}
```
**步骤 2: 提交代码**
```bash
git add ruoyi-ui/src/api/ccdi/modelParam.js
git commit -m "feat: 在API层添加批量保存方法"
```
---
### Task 19: 重构全局配置页面(第一部分 - 模板)
**文件:**
- Modify: `ruoyi-ui/src/views/ccdi/modelParam/index.vue`
**步骤 1: 替换整个 template 部分**
删除原有的 `<template>` 标签及其内容,替换为:
```vue
<template>
<div class="param-config-container">
<!-- 页面标题 -->
<div class="page-header">
<h2>全局模型参数管理</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 style="width: 100%">
<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: 暂不提交,继续下一步**

View File

@@ -0,0 +1,723 @@
# 项目详情参数配置页面实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 在项目详情页面实现参数配置功能,允许每个项目自定义模型参数,首次保存时自动从系统默认参数复制。
**Architecture:** 前端组件复用独立页面代码,后端修改 Service 根据 configType 返回对应参数,首次保存时自动复制默认参数并切换 configType。
**Tech Stack:** Spring Boot 3, MyBatis Plus, Vue.js 2, Element UI
---
## Task 1: 修改后端 Mapper 接口
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiModelParamMapper.java`
**Step 1: 添加更新参数值方法**
`CcdiModelParamMapper.java` 接口中添加方法:
```java
/**
* 更新参数值
*
* @param projectId 项目ID
* @param modelCode 模型编码
* @param paramCode 参数编码
* @param paramValue 参数值
* @return 影响行数
*/
int updateParamValue(@Param("projectId") Long projectId,
@Param("modelCode") String modelCode,
@Param("paramCode") String paramCode,
@Param("paramValue") String paramValue);
/**
* 批量插入参数
*
* @param params 参数列表
* @return 影响行数
*/
int insertBatch(@Param("list") List<CcdiModelParam> params);
```
**Step 2: 提交**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiModelParamMapper.java
git commit -m "feat: 添加 Mapper 接口方法 updateParamValue 和 insertBatch"
```
---
## Task 2: 修改后端 Mapper XML
**Files:**
- Modify: `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiModelParamMapper.xml`
**Step 1: 添加 updateParamValue SQL**
`</mapper>` 标签之前添加:
```xml
<!-- 更新参数值 -->
<update id="updateParamValue">
UPDATE ccdi_model_param
SET param_value = #{paramValue},
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, remark,
create_time, update_time
) 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},
#{item.remark}, NOW(), NOW()
)
</foreach>
</insert>
```
**Step 2: 提交**
```bash
git add ccdi-project/src/main/resources/mapper/ccdi/project/CcdiModelParamMapper.xml
git commit -m "feat: 添加 Mapper XML SQL updateParamValue 和 insertBatch"
```
---
## Task 3: 注入 ProjectMapper 依赖
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java`
**Step 1: 添加 import 语句**
在文件顶部的 import 区域添加:
```java
import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
```
**Step 2: 添加 Logger**
在类开始处添加:
```java
private static final Logger log = LoggerFactory.getLogger(CcdiModelParamServiceImpl.class);
```
**Step 3: 注入 ProjectMapper**
`@Resource private CcdiModelParamMapper modelParamMapper;` 之后添加:
```java
@Resource
private CcdiProjectMapper projectMapper;
```
**Step 4: 提交**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java
git commit -m "feat: 注入 CcdiProjectMapper 依赖"
```
---
## Task 4: 修改 selectParamList 方法
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java:52-71`
**Step 1: 替换 selectParamList 方法**
完全替换 `selectParamList` 方法:
```java
@Override
public List<ModelParamVO> selectParamList(ModelParamQueryDTO queryDTO) {
// 1. 参数验证
Long projectId = queryDTO.getProjectId();
if (projectId == null) {
projectId = 0L;
}
// 2. 如果是项目查询projectId > 0需要根据 configType 决定查询哪组参数
Long effectiveProjectId = projectId;
if (projectId > 0) {
// 查询项目信息
CcdiProject project = projectMapper.selectById(projectId);
if (project == null) {
throw new ServiceException("项目不存在");
}
// 根据 configType 决定查询哪组参数
if ("default".equals(project.getConfigType())) {
// 使用系统默认参数
effectiveProjectId = 0L;
} else {
// 使用项目自定义参数
effectiveProjectId = projectId;
}
}
// 3. 查询参数列表
List<CcdiModelParam> params = modelParamMapper.selectByProjectAndModel(
effectiveProjectId,
queryDTO.getModelCode()
);
// 4. 转换为 VO
List<ModelParamVO> result = new ArrayList<>();
params.forEach(param -> {
ModelParamVO vo = new ModelParamVO();
BeanUtils.copyProperties(param, vo);
result.add(vo);
});
return result;
}
```
**Step 2: 提交**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java
git commit -m "feat: 修改 selectParamList 方法支持根据 configType 返回对应参数"
```
---
## Task 5: 添加 copyDefaultParamsToProject 私有方法
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java`
**Step 1: 添加复制参数方法**
`saveParams` 方法之后添加:
```java
/**
* 复制系统默认参数到项目
*
* @param projectId 项目ID
* @param modelCode 模型编码
* @return 复制的参数数量
*/
private int copyDefaultParamsToProject(Long projectId, String modelCode) {
// 查询系统默认参数
List<CcdiModelParam> defaultParams = modelParamMapper.selectByProjectAndModel(0L, modelCode);
if (defaultParams.isEmpty()) {
log.warn("系统默认参数为空modelCode={}", modelCode);
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());
// 批量插入
int count = modelParamMapper.insertBatch(projectParams);
log.info("复制系统默认参数到项目成功projectId={}, modelCode={}, count={}",
projectId, modelCode, count);
return count;
}
```
**Step 2: 提交**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java
git commit -m "feat: 添加 copyDefaultParamsToProject 私有方法"
```
---
## Task 6: 修改 saveParams 方法
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java:74-122`
**Step 1: 替换 saveParams 方法**
完全替换 `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("参数列表不能为空");
}
Long projectId = saveDTO.getProjectId();
// 2. 如果是项目保存projectId > 0需要检查是否首次保存
if (projectId > 0) {
// 查询项目信息
CcdiProject project = projectMapper.selectById(projectId);
if (project == null) {
throw new ServiceException("项目不存在");
}
// 3. 如果是首次保存configType=default需要复制系统默认参数
if ("default".equals(project.getConfigType())) {
int copiedCount = copyDefaultParamsToProject(projectId, saveDTO.getModelCode());
if (copiedCount == 0) {
log.warn("系统默认参数为空projectId={}, modelCode={}",
projectId, saveDTO.getModelCode());
}
// 更新项目配置类型为 custom
project.setConfigType("custom");
projectMapper.updateById(project);
log.info("项目配置类型已更新为 customprojectId={}", projectId);
}
}
// 4. 更新参数值
String username = SecurityUtils.getUsername();
for (ModelParamSaveDTO.ParamValueItem item : saveDTO.getParams()) {
int updated = modelParamMapper.updateParamValue(
projectId,
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());
}
}
```
**Step 2: 提交**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiModelParamServiceImpl.java
git commit -m "feat: 修改 saveParams 方法支持首次保存自动复制默认参数"
```
---
## Task 7: 实现前端 ParamConfig 组件(模板部分)
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue`
**Step 1: 替换模板部分**
完全替换文件内容:
```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 style="width: 100%">
<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>
```
**Step 2: 提交**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue
git commit -m "feat: 实现 ParamConfig 组件模板部分"
```
---
## Task 8: 实现前端 ParamConfig 组件(脚本部分)
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue`
**Step 1: 添加脚本部分**
`</template>` 之后添加:
```vue
<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>
```
**Step 2: 提交**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue
git commit -m "feat: 实现 ParamConfig 组件脚本部分"
```
---
## Task 9: 实现前端 ParamConfig 组件(样式部分)
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue`
**Step 1: 添加样式部分**
`</script>` 之后添加:
```vue
<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>
```
**Step 2: 提交**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/ParamConfig.vue
git commit -m "feat: 实现 ParamConfig 组件样式部分"
```
---
## Task 10: 手动测试功能
**Step 1: 启动后端服务**
提示用户手动启动后端服务(不要自动运行)。
**Step 2: 启动前端服务**
```bash
cd ruoyi-ui
npm run dev
```
**Step 3: 访问测试页面**
1. 访问 `http://localhost:80`
2. 登录系统admin/admin123
3. 进入"项目管理"页面
4. 点击任意项目的"详情"按钮
5. 点击"参数配置"菜单
**Step 4: 测试场景 1 - 查看默认配置**
**操作:**
- 新项目查看参数配置
**预期结果:**
- 显示系统默认参数
- 参数值与系统默认一致
**Step 5: 测试场景 2 - 首次保存参数**
**操作:**
- 修改参数值
- 点击"保存配置"
**预期结果:**
- 显示"保存成功"提示
- 项目的 `configType` 变为 `custom`
- 再次查看参数显示修改后的值
**Step 6: 测试场景 3 - 切换模型**
**操作:**
- 切换到另一个模型
**预期结果:**
- 参数列表正确切换
- 显示新模型的参数
**Step 7: 测试场景 4 - 不修改参数点击保存**
**操作:**
- 不修改任何参数
- 点击"保存配置"
**预期结果:**
- 显示"没有需要保存的修改"提示
**Step 8: 测试场景 5 - 清空参数值后保存**
**操作:**
- 清空某个参数值
- 点击"保存配置"
**预期结果:**
- 显示"请填写所有参数值"错误提示
- 不发起保存请求
**Step 9: 提交测试完成**
```bash
git add .
git commit -m "test: 项目详情参数配置功能手动测试完成"
```
---
## 完成检查清单
- [ ] 后端 Mapper 接口已修改
- [ ] 后端 Mapper XML 已修改
- [ ] 后端 Service 已修改
- [ ] 前端 ParamConfig 组件已实现
- [ ] 所有测试场景通过
- [ ] 代码已提交到 git
---
## 注意事项
1. **不要自动启动后端服务** - 提示用户手动启动
2. **不需要后端单元测试** - 用户明确要求
3. **首次保存会触发复制** - 确保系统默认参数存在
4. **事务回滚** - 如果复制失败,事务会自动回滚
5. **前端验证优先** - 参数值验证在前端完成

View File

@@ -0,0 +1,304 @@
# 默认主题修改为浅色模式 - 实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**目标:** 将前端默认主题从深色模式改为浅色模式,新用户首次访问时看到浅色侧边栏
**架构:** 修改 `settings.js` 中的默认配置Vuex store 会自动读取该配置并应用到界面
**技术栈:** Vue.js 2.6, Vuex 3.6, Element UI 2.15
---
## Task 1: 修改默认主题配置
**文件:**
- Modify: `ruoyi-ui/src/settings.js:10`(修改第 10 行)
### Step 1: 读取当前配置文件
**操作:** 使用 Read 工具读取文件
```
Read: ruoyi-ui/src/settings.js
```
**预期结果:** 看到第 10 行为 `sideTheme: 'theme-dark',`
### Step 2: 修改默认主题为浅色模式
**操作:** 使用 Edit 工具修改配置
```javascript
// 修改 ruoyi-ui/src/settings.js 第 10 行
// 修改前:
sideTheme: 'theme-dark',
// 修改后:
sideTheme: 'theme-light',
```
**完整代码:**
```javascript
module.exports = {
/**
* 网页标题
*/
title: process.env.VUE_APP_TITLE,
/**
* 侧边栏主题 深色主题theme-dark浅色主题theme-light
*/
sideTheme: 'theme-light',
/**
* 系统布局配置
*/
showSettings: true,
/**
* 菜单导航模式 1、纯左侧 2、混合左侧+顶部) 3、纯顶部
*/
navType: 1,
/**
* 是否显示 tagsView
*/
tagsView: true,
/**
* 显示页签图标
*/
tagsIcon: false,
/**
* 是否固定头部
*/
fixedHeader: true,
/**
* 是否显示logo
*/
sidebarLogo: true,
/**
* 是否显示动态标题
*/
dynamicTitle: false,
/**
* 是否显示底部版权
*/
footerVisible: false,
/**
* 底部版权文本内容
*/
footerContent: 'Copyright © 2018-2026 RuoYi. All Rights Reserved.'
}
```
### Step 3: 提交代码变更
**命令:**
```bash
git add ruoyi-ui/src/settings.js
git commit -m "feat: 将默认主题修改为浅色模式
- 修改 settings.js 中 sideTheme 默认值从 'theme-dark' 改为 'theme-light'
- 新用户首次访问时将看到浅色模式侧边栏
- 老用户的自定义设置不受影响localStorage 优先)"
```
**预期结果:** Git 提交成功
---
## Task 2: 手动测试验证
**说明:** 此任务需要手动在浏览器中测试,无法自动化
### Step 1: 启动前端开发服务器
**命令:**
```bash
cd ruoyi-ui
npm run dev
```
**预期结果:** 前端服务启动在 http://localhost:80
### Step 2: 测试新用户体验
**操作步骤:**
1. 打开浏览器开发者工具F12
2. 进入 Application/应用 标签
3. 在左侧找到 Local Storage
4. 删除所有 `layout-setting` 相关的存储项
5. 刷新页面Ctrl+F5 强制刷新)
**预期结果:**
- 侧边栏为浅色模式(白色背景,深色文字)
- 侧边栏 Logo 区域为浅色
- 菜单项为深色文字
### Step 3: 测试老用户体验(深色模式)
**操作步骤:**
1. 打开浏览器开发者工具F12
2. 进入 Application/应用 标签
3. 在 Local Storage 中添加/修改 `layout-setting`
```json
{
"sideTheme": "theme-dark",
"theme": "#409EFF"
}
```
4. 刷新页面Ctrl+F5 强制刷新)
**预期结果:**
- 侧边栏为深色模式(深色背景,浅色文字)
- 老用户的设置被保留
### Step 4: 测试主题切换功能
**操作步骤:**
1. 登录系统
2. 点击右上角设置图标(齿轮图标)
3. 在右侧抽屉中找到"主题风格设置"
4. 点击深色模式图标
5. 观察侧边栏变化
**预期结果:**
- 侧边栏立即切换为深色模式
- 菜单颜色变为浅色文字
### Step 5: 测试主题保存功能
**操作步骤:**
1. 在设置抽屉中切换为深色模式
2. 点击底部的"保存配置"按钮
3. 等待提示"正在保存到本地"
4. 刷新页面F5
**预期结果:**
- 刷新后侧边栏仍为深色模式
- localStorage 中保存了 `layout-setting` 数据
### Step 6: 测试主题重置功能
**操作步骤:**
1. 在设置抽屉中切换为深色模式并保存
2. 点击底部的"重置配置"按钮
3. 等待页面自动刷新
**预期结果:**
- 页面自动刷新
- 侧边栏恢复为浅色模式(默认值)
- localStorage 中的 `layout-setting` 被清除
---
## Task 3: 更新项目文档(可选)
**文件:**
- Modify: `CLAUDE.md` 或 `README.md`(如果有主题相关的说明)
### Step 1: 更新 CLAUDE.md 中的主题说明
**操作:** 检查 CLAUDE.md 中是否有关于默认主题的说明,如果有则更新
**修改位置:** 如果文档中提到"默认深色模式",需要更新为"默认浅色模式"
### Step 2: 提交文档更新
**命令:**
```bash
git add CLAUDE.md
git commit -m "docs: 更新文档中的默认主题说明"
```
---
## 验收清单
在完成所有任务后,请验证以下内容:
- [ ] `ruoyi-ui/src/settings.js` 中 `sideTheme` 值为 `'theme-light'`
- [ ] 新用户首次访问看到浅色模式侧边栏
- [ ] 老用户的深色模式设置被保留
- [ ] 主题切换功能正常(深色 ↔ 浅色)
- [ ] 主题保存功能正常(保存到 localStorage
- [ ] 主题重置功能正常(恢复为浅色模式)
- [ ] 浏览器控制台无错误信息
- [ ] 代码已提交到 Git
---
## 回滚方案
如果发现问题需要回滚:
### 回滚步骤
**命令:**
```bash
git revert <commit-hash>
```
或手动修改 `ruoyi-ui/src/settings.js`:
```javascript
sideTheme: 'theme-dark', // 改回深色模式
```
然后重新构建:
```bash
cd ruoyi-ui
npm run build:prod
```
---
## 部署说明
### 开发环境
无需额外操作,修改后自动生效(热更新)
### 生产环境
1. 构建前端:
```bash
cd ruoyi-ui
npm run build:prod
```
2. 部署 `ruoyi-ui/dist/` 目录到生产服务器
3. 用户刷新浏览器即可看到效果
**注意:**
- 不需要重启后端服务
- 不需要清理数据库
- 不需要用户做任何操作
---
## 相关文件
- `ruoyi-ui/src/settings.js` - 默认配置文件(本次修改)
- `ruoyi-ui/src/store/modules/settings.js` - Vuex 状态管理(无需修改)
- `ruoyi-ui/src/layout/components/Settings/index.vue` - 设置界面(无需修改)
- `docs/design/2026-03-06-theme-light-default-design.md` - 设计文档

View File

@@ -0,0 +1,420 @@
# 银行流水重复校验 Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:**`fetchAndSaveBankStatements(...)` 增加基于 `project_id + LE_ACCOUNT_NO + ACCOUNTING_DATE_ID + AMOUNT_DR + AMOUNT_CR` 的数据库级重复校验,确保重复流水跳过插入且保留原数据不变。
**Architecture:** 先清理测试库中的异常行和重复行,再为 `ccdi_bank_statement` 增加新唯一键。业务代码只负责对 `LE_ACCOUNT_NO` 做轻量标准化,并将批量插入改为 no-op upsert真实数据库错误仍按现有异步文件处理链路向上抛出。
**Tech Stack:** Java 21, Spring Boot 3, MyBatis Plus, MySQL, JUnit 5, Mockito
---
### Task 1: 固化新去重键的标准化失败测试
**Files:**
- Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java`
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
**Step 1: Write the failing test**
在现有 `CcdiFileUploadServiceImplTest` 中新增一个聚焦标准化的测试。通过 mock `lsfxClient.getBankStatement(...)` 返回一条带空白账号的流水,并捕获传给 `bankStatementMapper.insertBatch(...)` 的实体。
```java
@Test
void fetchAndSaveBankStatements_shouldTrimLeAccountNoBeforeInsert() {
// arrange
// leAccountNo = " 62220001 "
// accountingDateId = 20260310
// amountDr = new BigDecimal("100.00")
// amountCr = new BigDecimal("0.00")
// act
// assert
verify(bankStatementMapper).insertBatch(argThat(list ->
"62220001".equals(list.get(0).getLeAccountNo())));
}
```
**Step 2: Run test to verify it fails**
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest#fetchAndSaveBankStatements_shouldTrimLeAccountNoBeforeInsert
```
Expected:
- FAIL
- 失败原因体现当前实现尚未对 `LE_ACCOUNT_NO``trim`
**Step 3: Write the minimal implementation**
`CcdiFileUploadServiceImpl` 内增加最小辅助方法:
```java
private String trimAccountNo(String value) {
return value == null ? null : value.trim();
}
private void normalizeDedupFields(CcdiBankStatement statement) {
statement.setLeAccountNo(trimAccountNo(statement.getLeAccountNo()));
}
```
并在 `fromResponse(...)` 结果加入批次列表前调用。
**Step 4: Run test to verify it passes**
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest#fetchAndSaveBankStatements_shouldTrimLeAccountNoBeforeInsert
```
Expected:
- PASS
**Step 5: Commit**
```bash
git add ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java
git commit -m "test(ccdi-project): cover bank statement account no normalization"
```
### Task 2: 用失败测试锁定“重复不失败”的服务层语义
**Files:**
- Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java`
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
**Step 1: Write the failing test**
新增一个测试,模拟 Mapper 在重复导入场景下不抛异常,并验证上传记录最终不会因为重复而进入 `parsed_failed`
```java
@Test
void processFileAsync_shouldNotFailWhenDuplicateStatementsAreSkipped() {
// mock upload success / parse success
// mock bankStatementMapper.insertBatch(...) 返回“未报错的重复跳过结果”
// assert recordMapper 不会收到 parsed_failed
}
```
**Step 2: Run test to verify it fails**
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest#processFileAsync_shouldNotFailWhenDuplicateStatementsAreSkipped
```
Expected:
- FAIL
- 失败点应体现当前实现还没有为重复跳过设计明确语义或日志
**Step 3: Write minimal implementation**
只做本任务所需的最小改动:
-`fetchAndSaveBankStatements(...)` 中补充接口返回数、尝试写入数的日志
- 保持 duplicate 场景不抛异常
- 不调整其他与重复无关的异常路径
示例日志:
```java
log.info("【文件上传】流水入库完成: fetchedCount={}, attemptedCount={}, insertedCount={}, duplicateCount={}",
fetchedCount, attemptedCount, insertedCount, duplicateCount);
```
**Step 4: Run the test to verify it passes**
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest#processFileAsync_shouldNotFailWhenDuplicateStatementsAreSkipped
```
Expected:
- PASS
**Step 5: Commit**
```bash
git add ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java
git commit -m "test(ccdi-project): keep duplicate bank statements from failing uploads"
```
### Task 3: 为测试库编写数据清洗和唯一键迁移脚本
**Files:**
- Create: `assets/database/2026-03-10-bank-statement-dedup.sql`
- Modify: `assets/对接流水分析/ccdi_bank_statement.md`
**Step 1: Write the migration script**
创建测试库迁移脚本,包含以下 SQL
```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
);
```
**Step 2: Manually run the migration in the local test database**
Run:
```bash
mysql -u <user> -p <database> < assets/database/2026-03-10-bank-statement-dedup.sql
```
Expected:
- SQL 全部执行成功
- `SHOW INDEX FROM ccdi_bank_statement;` 能看到 `uk_bank_statement_dedup`
**Step 3: Update the bank statement schema note**
同步更新 `assets/对接流水分析/ccdi_bank_statement.md`,补齐:
- `project_id`
- 新唯一键说明
- `project_id` / `LE_ACCOUNT_NO` / `ACCOUNTING_DATE_ID` 的非空语义
**Step 4: Verify migration result**
Run:
```bash
mysql -u <user> -p -e "SHOW CREATE TABLE ccdi_bank_statement\G"
```
Expected:
- 表结构包含新的唯一键
- `project_id``LE_ACCOUNT_NO``ACCOUNTING_DATE_ID``NOT NULL`
**Step 5: Commit**
```bash
git add assets/database/2026-03-10-bank-statement-dedup.sql assets/对接流水分析/ccdi_bank_statement.md
git commit -m "feat(database): add bank statement dedup unique key"
```
### Task 4: 将 Mapper 批量插入改为 no-op upsert
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapper.java`
- Modify: `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml`
**Step 1: Write the failing verification case**
先在本地 MySQL 中准备两次相同业务键的数据,第二次执行当前批量插入 SQL确认现状会抛唯一键冲突。
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest#processFileAsync_shouldNotFailWhenDuplicateStatementsAreSkipped
```
Expected:
- FAIL 或本地重复场景仍未被正确跳过
**Step 2: Change the insert SQL**
将 XML 中的批量插入改为:
```xml
<insert id="insertBatch" parameterType="java.util.List">
insert into ccdi_bank_statement (...)
values
<foreach collection="list" item="item" separator=",">
(...)
</foreach>
on duplicate key update
bank_statement_id = bank_statement_id
</insert>
```
**Step 3: Keep mapper signature unchanged**
保持 `CcdiBankStatementMapper.insertBatch(@Param("list") List<CcdiBankStatement> list)` 不变,避免扩大调用面。
**Step 4: Run the targeted test suite**
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest
```
Expected:
- PASS
**Step 5: Manual duplicate verification**
在本地测试库重复导入同一批数据后执行:
```bash
mysql -u <user> -p -e "SELECT COUNT(*) FROM ccdi_bank_statement WHERE project_id = <projectId> AND LE_ACCOUNT_NO = '62220001' AND ACCOUNTING_DATE_ID = 20260310 AND AMOUNT_DR = 100.00 AND AMOUNT_CR = 0.00;"
```
Expected:
- 结果始终为 `1`
**Step 6: Commit**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapper.java ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml
git commit -m "feat(ccdi-project): skip duplicate bank statements on insert"
```
### Task 5: 校准日志计数并验证 MySQL 受影响行数语义
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
- Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java`
**Step 1: Add a temporary probe in the service**
在开发阶段先打印:
```java
log.info("【文件上传】dedup probe: batchSize={}, mapperAffectedRows={}", batchList.size(), affectedRows);
```
**Step 2: Run a duplicate import manually**
使用相同测试文件连续导入两次,并观察第二次日志。
Expected:
- 能确认 duplicate 分支下 MyBatis/JDBC 返回的 `affectedRows` 语义
**Step 3: Finalize the counting logic**
如果实测 duplicate 返回 `0`,则直接落正式逻辑:
```java
insertedCount += affectedRows;
duplicateCount += batchSize - affectedRows;
```
如果实测不稳定,则不要伪造精确计数,改为保守日志:
```java
log.info("【文件上传】流水入库完成: fetchedCount={}, attemptedCount={}", fetchedCount, attemptedCount);
```
**Step 4: Run the test suite**
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest
```
Expected:
- PASS
**Step 5: Remove temporary probe and commit**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java
git commit -m "refactor(ccdi-project): finalize bank statement dedup logging"
```
### Task 6: 做最终回归验证并整理交付
**Files:**
- Review: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java`
- Review: `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml`
- Review: `assets/database/2026-03-10-bank-statement-dedup.sql`
- Review: `assets/对接流水分析/ccdi_bank_statement.md`
**Step 1: Run automated tests**
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest
```
Expected:
- PASS
**Step 2: Run a focused compile**
Run:
```bash
mvn clean compile -pl ccdi-project -am
```
Expected:
- BUILD SUCCESS
**Step 3: Execute manual acceptance checks**
验证以下结果:
- 首次导入:数据写入成功
- 第二次导入同一数据:原记录不变,数量不增加
- 非重复数据再次导入:仅新增新记录
- 制造非唯一键类 SQL 错误:上传记录进入 `parsed_failed`
**Step 4: Prepare the delivery summary**
总结:
- 唯一键是否已生效
- 重复导入是否跳过
- 原数据是否保持不变
- 日志计数是否为精确值还是保守值
**Step 5: Commit**
```bash
git status --short
git add <verified files>
git commit -m "docs: finalize bank statement dedup verification notes"
```

View File

@@ -0,0 +1,157 @@
# CSV和PDF文件上传支持实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**目标:** 扩展流水导入功能支持CSV和PDF格式文件上传
**架构:** 修改后端文件类型校验逻辑,添加 `.csv``.pdf` 支持,使前后端校验规则一致
**技术栈:** Spring Boot 3.5.8, Java 21, MyBatis Plus
---
## 任务1: 修改后端文件类型校验
**文件:**
- 修改: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java:65-67`
**步骤1: 修改文件类型校验逻辑**
定位到 `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 文件");
}
```
**步骤2: 验证修改**
- 确认代码语法正确
- 确认导入语句无缺失(无需新增导入)
---
## 任务2: 通过Swagger测试接口
**前置条件:** 后端服务已启动端口8080
**步骤1: 访问Swagger UI**
浏览器打开: http://localhost:8080/swagger-ui/index.html
**步骤2: 测试CSV文件上传**
1. 找到 `POST /upload/batch/{projectId}` 接口
2. 点击 "Try it out"
3. 选择 projectId1
4. 上传一个 `.csv` 测试文件
5. 点击 "Execute"
6. **预期结果**: 返回成功响应,包含 batchId
**步骤3: 测试PDF文件上传**
1. 使用同一接口
2. 上传一个 `.pdf` 测试文件
3. **预期结果**: 返回成功响应,包含 batchId
**步骤4: 测试大小写不敏感**
1. 上传文件名为 `.CSV`(大写)的文件
2. **预期结果**: 返回成功响应
**步骤5: 测试不支持格式**
1. 上传 `.txt` 文件
2. **预期结果**: 返回错误 "格式不支持,仅支持 PDF、CSV、Excel 文件"
---
## 任务3: 前端功能验证
**前置条件:** 前端服务已启动端口80
**步骤1: 访问前端页面**
浏览器打开: http://localhost
登录账号: admin / admin123
**步骤2: 进入项目详情页面**
导航到: 项目管理 → 选择项目 → 详情 → 数据上传
**步骤3: 测试CSV文件上传**
1. 点击 "批量上传" 按钮
2. 拖拽或选择 `.csv` 文件
3. 点击 "开始上传"
4. **预期结果**: 文件成功上传,无格式错误提示
**步骤4: 测试PDF文件上传**
1. 选择 `.pdf` 文件
2. 点击 "开始上传"
3. **预期结果**: 文件成功上传,无格式错误提示
---
## 任务4: 提交代码
**步骤1: 查看修改状态**
运行:
```bash
cd D:/ccdi/ccdi
git status
```
预期输出:
```
modified: ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java
```
**步骤2: 提交代码**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java
git commit -m "feat(ccdi-project): 流水导入支持CSV和PDF文件格式"
```
**步骤3: 推送到远程仓库(可选)**
```bash
git push origin dev
```
---
## 实施检查清单
- [ ] 后端文件类型校验已修改
- [ ] CSV文件上传测试通过Swagger
- [ ] PDF文件上传测试通过Swagger
- [ ] 大小写不敏感测试通过
- [ ] 不支持格式被正确拒绝
- [ ] 前端CSV上传功能正常
- [ ] 前端PDF上传功能正常
- [ ] 代码已提交到git
---
## 注意事项
1. **测试文件准备**: 准备好 `.csv``.pdf``.xlsx``.txt` 格式的测试文件
2. **文件大小**: 测试文件不超过50MB
3. **流水分析平台**: 确认平台支持CSV和PDF格式已确认支持
4. **不影响现有功能**: Excel文件上传功能保持不变

View File

@@ -0,0 +1,362 @@
# 流水文件解析成功状态延后到流水入库完成 Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 让流水文件上传记录只有在步骤 7 获取并保存流水数据成功后才更新为 `parsed_success`,在此之前继续显示 `parsing`
**Architecture:** 重构 `CcdiFileUploadServiceImpl` 的步骤 7使其返回结构化执行结果而不是吞异常主流程基于该结果决定最终状态。使用 `ccdi_bank_statement.batch_id` 绑定本次上传 `logId`,在步骤 7 失败时通过 Mapper 补偿删除本次已写入流水,避免半成品数据残留。
**Tech Stack:** Java 21, Spring Boot 3, MyBatis Plus, JUnit 5, Mockito来自 `spring-boot-starter-test`
---
### Task 1: 为状态延后规则编写服务层失败测试
**Files:**
- Create: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java`
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java:340-619`
**Step 1: Write the failing test**
在新测试类中先写“平台解析成功但步骤 7 失败时,记录最终为 `parsed_failed`”的测试。使用 Mockito mock 以下依赖:
- `LsfxAnalysisClient`
- `CcdiFileUploadRecordMapper`
- `CcdiBankStatementMapper`
示例骨架:
```java
@ExtendWith(MockitoExtension.class)
class CcdiFileUploadServiceImplTest {
@InjectMocks
private CcdiFileUploadServiceImpl service;
@Mock
private CcdiFileUploadRecordMapper recordMapper;
@Mock
private CcdiProjectMapper projectMapper;
@Mock
private LsfxAnalysisClient lsfxClient;
@Mock
private CcdiBankStatementMapper bankStatementMapper;
@Test
void processFileAsync_shouldKeepParsingUntilBankStatementsSaved() {
// arrange
// mock 上传成功、轮询完成、状态接口解析成功
// mock getBankStatement 首次调用抛异常
// act
// assert
verify(recordMapper, never()).updateById(argThat(record ->
"parsed_success".equals(record.getFileStatus())));
verify(recordMapper).updateById(argThat(record ->
"parsed_failed".equals(record.getFileStatus())));
}
}
```
**Step 2: Run test to verify it fails**
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest#processFileAsync_shouldKeepParsingUntilBankStatementsSaved
```
Expected:
- FAIL
- 失败原因应体现当前实现会先更新 `parsed_success`,或测试类尚未编译通过
**Step 3: Write a second failing test for the success path**
补一条成功路径测试,验证步骤 7 成功后才更新为 `parsed_success`
```java
@Test
void processFileAsync_shouldMarkSuccessAfterBankStatementsSaved() {
// mock 上传成功、解析成功、getBankStatement 返回 totalCount=0
// 执行后应只在步骤7完成后出现 parsed_success
verify(recordMapper).updateById(argThat(record ->
"parsed_success".equals(record.getFileStatus())));
}
```
**Step 4: Run both tests to verify they fail**
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest
```
Expected:
- FAIL
- 至少一条断言失败,证明当前实现不符合新设计
**Step 5: Commit**
```bash
git add ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java
git commit -m "test(ccdi-project): add file upload status transition tests"
```
### Task 2: 重构步骤 7 返回结果对象并延后成功状态更新
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java:340-619`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java`
**Step 1: Add a result object inside the service**
`CcdiFileUploadServiceImpl` 内新增私有静态结果类:
```java
@Data
private static class FetchBankStatementResult {
private boolean success;
private int totalCount;
private int savedCount;
private String errorMessage;
}
```
**Step 2: Change the fetch method signature**
把:
```java
private void fetchAndSaveBankStatements(Long projectId, Integer groupId, Integer logId)
```
改为:
```java
private FetchBankStatementResult fetchAndSaveBankStatements(Long projectId, Integer groupId, Integer logId)
```
**Step 3: Return explicit failure instead of swallowing exceptions**
把当前“记录错误后继续下一页”的逻辑改成显式失败返回。例如:
```java
catch (Exception e) {
result.setSuccess(false);
result.setErrorMessage("获取或保存流水数据失败: " + e.getMessage());
return result;
}
```
**Step 4: Delay `parsed_success` update until the fetch result succeeds**
`processFileAsync(...)` 中当前这段提前成功逻辑:
```java
record.setFileStatus("parsed_success");
record.setEnterpriseNames(enterpriseNamesStr);
record.setAccountNos(accountNosStr);
recordMapper.updateById(record);
fetchAndSaveBankStatements(projectId, lsfxProjectId, logId);
```
改成:
```java
FetchBankStatementResult fetchResult = fetchAndSaveBankStatements(projectId, lsfxProjectId, logId);
if (!fetchResult.isSuccess()) {
record.setFileStatus("parsed_failed");
record.setErrorMessage(fetchResult.getErrorMessage());
recordMapper.updateById(record);
return;
}
record.setFileStatus("parsed_success");
record.setEnterpriseNames(enterpriseNamesStr);
record.setAccountNos(accountNosStr);
record.setErrorMessage(null);
recordMapper.updateById(record);
```
**Step 5: Handle the zero-data path explicitly**
在首次总数查询后,若 `totalCount == null || totalCount <= 0`,返回成功结果:
```java
result.setSuccess(true);
result.setTotalCount(0);
result.setSavedCount(0);
return result;
```
**Step 6: Run targeted tests**
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest
```
Expected:
- PASS
- 新增的状态延后测试全部通过
**Step 7: Commit**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java
git commit -m "refactor(ccdi-project): delay parsed success until bank statements saved"
```
### Task 3: 为本次上传绑定 batchId 并补偿清理半成品流水
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java:527-619`
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapper.java:15-23`
- Modify: `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml:62-87`
- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java`
**Step 1: Attach the upload logId to each statement**
在流水转换循环内补齐:
```java
statement.setProjectId(projectId);
statement.setBatchId(logId);
```
**Step 2: Add the cleanup mapper method**
`CcdiBankStatementMapper.java` 中新增:
```java
int deleteByProjectIdAndBatchId(@Param("projectId") Long projectId,
@Param("batchId") Integer batchId);
```
并在 XML 中实现:
```xml
<delete id="deleteByProjectIdAndBatchId">
delete from ccdi_bank_statement
where project_id = #{projectId}
and batch_id = #{batchId}
</delete>
```
**Step 3: Call cleanup before returning failure**
`fetchAndSaveBankStatements(...)` 的失败分支中调用:
```java
bankStatementMapper.deleteByProjectIdAndBatchId(projectId, logId);
```
只允许使用 `projectId + logId(batchId)` 双条件,避免误删其他批次数据。
**Step 4: Write a failing cleanup test**
在测试类中新增:
```java
@Test
void processFileAsync_shouldCleanupInsertedStatementsWhenFetchFails() {
// mock 某页或某批插入失败
// assert deleteByProjectIdAndBatchId(projectId, logId) 被调用
}
```
**Step 5: Run the new test**
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiFileUploadServiceImplTest#processFileAsync_shouldCleanupInsertedStatementsWhenFetchFails
```
Expected:
- 先 FAIL再在实现后 PASS
**Step 6: Run the module tests**
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiBankStatementTest,CcdiFileUploadServiceImplTest
```
Expected:
- PASS
- 旧的 `CcdiBankStatementTest` 不回归
**Step 7: 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
git commit -m "fix(ccdi-project): cleanup partial bank statements on upload failure"
```
### Task 4: 回归验证并整理交付
**Files:**
- 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**
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiBankStatementTest,CcdiFileUploadServiceImplTest
```
Expected:
- PASS
- 无编译错误
**Step 2: Inspect git diff**
Run:
```bash
git diff -- 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
```
Expected:
- 只包含状态时机调整、结果对象、清理接口和测试
**Step 3: Update docs if implementation deviates**
若实现中出现与设计或计划不一致的细节,及时回写到这两份文档,避免文档失真。
**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/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"
```
---
## Implementation Checklist
- [ ] 服务测试已覆盖“成功延后”和“步骤 7 失败”场景
- [ ] `fetchAndSaveBankStatements(...)` 改为返回结构化结果
- [ ] 步骤 7 完成前记录状态保持 `parsing`
- [ ] 步骤 7 成功后才更新 `parsed_success`
- [ ] 步骤 7 失败后更新 `parsed_failed`
- [ ] 本次 `logId` 对应流水写入 `batch_id`
- [ ] 步骤 7 失败时清理本次半成品流水
- [ ] `totalCount = 0` 场景按成功处理
- [ ] `ccdi-project` 相关测试通过

View File

@@ -0,0 +1,300 @@
# 参数配置类型显示实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**目标:** 在项目详情页面顶部添加配置类型标签,显示当前项目使用默认配置还是自定义配置
**架构:** 纯前端实现,利用已有的 projectInfo.configType 字段,使用 Element UI 的 el-tag 组件在不同颜色区分配置类型
**技术栈:** Vue.js 2.6.12, Element UI 2.15.14
---
## 前置条件
- ✅ 后端 CcdiProject 实体类已包含 configType 字段
- ✅ getProject() 接口已返回 configType 数据
- ✅ 数据库表 ccdi_project 已存储 config_type 字段
- ✅ 前端项目详情页面已存在 detail.vue 文件
---
## 任务列表
### 任务 1: 添加配置类型标签转换方法
**文件:**
- 修改: `ruoyi-ui/src/views/ccdiProject/detail.vue` (methods 部分)
**步骤 1: 添加 getConfigTypeLabel 方法**
`methods` 对象中添加配置类型标签文字转换方法(建议添加在 `getStatusLabel` 方法后面):
```javascript
/** 获取配置类型标签文字 */
getConfigTypeLabel(configType) {
const configTypeMap = {
'default': '默认配置',
'custom': '自定义配置'
};
return configTypeMap[configType] || '默认配置';
},
```
**步骤 2: 添加 getConfigTypeStyle 方法**
在刚才添加的方法后面添加配置类型样式转换方法:
```javascript
/** 获取配置类型标签样式 */
getConfigTypeStyle(configType) {
const styleMap = {
'default': 'info', // 蓝色
'custom': 'warning' // 橙色
};
return styleMap[configType] || 'info';
},
```
**预期位置:** 在第 220 行 `getStatusLabel` 方法后面
**注意:** 两个方法之间需要逗号分隔
---
### 任务 2: 在模板中添加配置类型标签
**文件:**
- 修改: `ruoyi-ui/src/views/ccdiProject/detail.vue:10-19`
**步骤 1: 在状态标签后添加配置类型标签**
在项目标题区域,在状态标签 `</el-tag>` 后面添加配置类型标签(约第 18 行后):
```vue
<!-- 配置类型标签 -->
<el-tag
:type="getConfigTypeStyle(projectInfo.configType)"
size="small"
>
{{ getConfigTypeLabel(projectInfo.configType) }}
</el-tag>
```
**完整上下文:**
```vue
<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>
```
**注意:**
- 标签使用 `size="small"` 与状态标签保持一致
- 使用 `:type` 动态绑定样式
- 位置在状态标签后面,与状态标签同级
---
### 任务 3: 本地测试验证
**步骤 1: 启动前端开发服务器**
```bash
cd ruoyi-ui
npm run dev
```
**预期:** 前端服务启动在 http://localhost:80
**步骤 2: 登录系统**
访问 http://localhost:80使用测试账号登录
- 用户名: `admin`
- 密码: `admin123`
**步骤 3: 测试默认配置项目**
1. 进入"纪检初核管理" -> "项目管理"
2. 点击一个使用默认配置的项目configType='default'
3. 查看项目详情页面顶部
**预期结果:**
- 项目名称旁边显示两个标签:[状态] [默认配置]
- "默认配置"标签为蓝色info 类型)
- 标签显示正常,无样式错乱
**步骤 4: 测试自定义配置项目**
1. 在参数配置页面修改任意参数值
2. 点击"保存所有修改"
3. 观察页面顶部标签变化
**预期结果:**
- 保存成功后,标签从蓝色 "默认配置" 变为橙色 "自定义配置"
- 配置类型已从 'default' 切换为 'custom'
**步骤 5: 测试边界情况**
刷新页面,验证标签状态保持一致
**预期结果:**
- 刷新后标签颜色和文字与刷新前一致
- 无 JavaScript 控制台错误
**步骤 6: 测试浏览器兼容性**
在不同浏览器Chrome、Firefox、Edge中重复步骤 3-5
**预期结果:**
- 各浏览器显示效果一致
- 标签样式正常
---
### 任务 4: 提交代码到 Git
**步骤 1: 查看修改内容**
```bash
git status
git diff ruoyi-ui/src/views/ccdiProject/detail.vue
```
**预期:** 只修改了 detail.vue 文件,修改内容符合设计
**步骤 2: 添加文件到暂存区**
```bash
git add ruoyi-ui/src/views/ccdiProject/detail.vue
```
**步骤 3: 提交更改**
```bash
git commit -m "$(cat <<'EOF'
feat(ui): 在项目详情页面添加配置类型标签显示
- 在项目名称旁添加配置类型标签
- 默认配置显示蓝色"默认配置"标签
- 自定义配置显示橙色"自定义配置"标签
- 添加 getConfigTypeLabel 和 getConfigTypeStyle 方法
- 纯前端实现,无需后端修改
Ref: docs/design/2026-03-09-param-config-type-display-design.md
EOF
)"
```
**步骤 4: 验证提交**
```bash
git log -1 --stat
```
**预期输出:**
```
commit [hash]
Author: [Your Name] <[Your Email]>
Date: [Date]
feat(ui): 在项目详情页面添加配置类型标签显示
- 在项目名称旁添加配置类型标签
- 默认配置显示蓝色"默认配置"标签
- 自定义配置显示橙色"自定义配置"标签
- 添加 getConfigTypeLabel 和 getConfigTypeStyle 方法
- 纯前端实现,无需后端修改
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]
```
---
## 验收清单
实施完成后,请确认以下验收标准:
- [ ] 项目详情页面顶部显示配置类型标签
- [ ] 默认配置显示蓝色 "默认配置" 标签
- [ ] 自定义配置显示橙色 "自定义配置" 标签
- [ ] 标签位置在状态标签旁边
- [ ] 标签样式与设计一致(大小、颜色)
- [ ] 修改参数保存后,标签正确切换
- [ ] 刷新页面后标签状态保持一致
- [ ] 无 JavaScript 控制台错误
- [ ] 不影响现有功能
- [ ] 代码已提交到 Git
---
## 回滚方案
如果实施后出现问题,可以快速回滚:
```bash
# 查看最近的提交
git log --oneline -5
# 回滚到上一个版本
git revert HEAD
# 或者硬回滚(谨慎使用)
git reset --hard HEAD~1
```
---
## 常见问题
**Q1: 标签不显示怎么办?**
检查:
1. `projectInfo.configType` 是否有值(在浏览器控制台打印)
2. 方法名是否正确拼写
3. 模板语法是否正确
**Q2: 标签颜色不对怎么办?**
检查:
1. `getConfigTypeStyle` 方法返回值是否正确
2. Element UI 版本是否支持 info/warning 类型
**Q3: 修改参数后标签不变化怎么办?**
检查:
1. 参数保存是否成功
2. 后端是否正确更新了 configType
3. 页面是否重新加载了 projectInfo
---
## 相关文档
- 设计文档: `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`
---
**计划创建日期:** 2026-03-09
**预计实施时间:** 15-30 分钟
**难度等级:** 简单

View File

@@ -0,0 +1,116 @@
# 上传数据主体账号展示实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**目标:** 在项目详情的上传数据文件列表中新增“主体账号”列,展示后端已返回的 `accountNos`
**架构:** 保持后端接口、数据结构和页面交互逻辑不变,仅在前端表格模板增加一列,并通过静态回归测试锁定该列的存在与字段绑定
**技术栈:** Vue.js 2.6.12, Element UI 2.15.14, Node.js 内置 `assert/fs/path`
---
### 任务 1: 编写失败中的回归测试
**文件:**
- 新建: `ruoyi-ui/tests/unit/upload-data-account-column.test.js`
**步骤 1: 写出失败测试**
新增一个零依赖静态测试,读取 `UploadData.vue` 源码并断言:
- 文件列表区域存在 `主体账号` 文案
- 存在绑定 `prop="accountNos"` 的表格列
测试主体可采用以下结构:
```javascript
const assert = require("assert");
const fs = require("fs");
const path = require("path");
const componentPath = path.resolve(
__dirname,
"../../src/views/ccdiProject/components/detail/UploadData.vue"
);
const source = fs.readFileSync(componentPath, "utf8");
assert(/prop="accountNos"/.test(source), "...");
assert(/label="主体账号"/.test(source), "...");
```
**步骤 2: 运行测试确认失败**
运行:
```bash
node ruoyi-ui/tests/unit/upload-data-account-column.test.js
```
预期:
- 命令失败
- 失败信息指出未找到“主体账号”列或 `accountNos` 绑定
---
### 任务 2: 修改上传数据列表模板
**文件:**
- 修改: `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue`
**步骤 1: 新增主体账号列**
在“主体名称”列后添加“主体账号”列,保持与主体名称相同的兜底展示:
```vue
<el-table-column prop="accountNos" label="主体账号" min-width="180">
<template slot-scope="scope">
{{ scope.row.accountNos || '-' }}
</template>
</el-table-column>
```
**步骤 2: 保持现有布局不变**
- 不修改接口请求
- 不调整分页、刷新、轮询逻辑
- 不修改表格其他列的行为
---
### 任务 3: 重新运行测试并回归验证
**文件:**
- 验证: `ruoyi-ui/tests/unit/upload-data-account-column.test.js`
- 验证: `ruoyi-ui/tests/unit/upload-data-file-list-table.test.js`
- 验证: `ruoyi-ui/tests/unit/upload-data-file-list-settings.test.js`
**步骤 1: 运行新增测试确认通过**
```bash
node ruoyi-ui/tests/unit/upload-data-account-column.test.js
```
预期:
- 输出 `upload-data-account-column test passed`
**步骤 2: 运行关联回归测试**
```bash
node ruoyi-ui/tests/unit/upload-data-file-list-table.test.js
node ruoyi-ui/tests/unit/upload-data-file-list-settings.test.js
```
预期:
- 两个测试都通过
**步骤 3: 执行前端构建验证**
```bash
cd ruoyi-ui
npm run build:prod
```
预期:
- 构建成功
- 无模板编译错误

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: 补充流水标签详细日志"
```