整理docs目录并补充文档规范
This commit is contained in:
@@ -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`
|
||||
@@ -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 组件库文档
|
||||
- 原型图设计稿
|
||||
|
||||
---
|
||||
|
||||
**文档状态:** ✅ 已批准,准备实施
|
||||
@@ -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。
|
||||
578
docs/plans/fullstack/2026-02-27-project-status-counts-fix.md
Normal file
578
docs/plans/fullstack/2026-02-27-project-status-counts-fix.md
Normal 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/
|
||||
1248
docs/plans/fullstack/2026-02-28-database-migration.md
Normal file
1248
docs/plans/fullstack/2026-02-28-database-migration.md
Normal file
File diff suppressed because it is too large
Load Diff
1075
docs/plans/fullstack/2026-03-02-lsfx-integration.md
Normal file
1075
docs/plans/fullstack/2026-03-02-lsfx-integration.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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 个请求模型:
|
||||
- GetTokenRequest(10+ 字段,可选字段有默认值)
|
||||
- UploadFileRequest(通过 Form 数据接收)
|
||||
- FetchInnerFlowRequest(7 个必填字段)
|
||||
- CheckParseStatusRequest(2 个字段)
|
||||
- DeleteFilesRequest(3 个字段)
|
||||
- GetBankStatementRequest(4 个字段)
|
||||
- 所有字段添加 Field 描述(用于 Swagger)
|
||||
- 可选字段使用 `Optional[Type] = default_value`
|
||||
|
||||
3. 创建 `models/response.py`:
|
||||
- 定义嵌套数据模型:
|
||||
- TokenData(5 个字段)
|
||||
- UploadLogItem(15+ 字段)
|
||||
- BankStatementItem(30+ 字段)
|
||||
- PendingItem(15+ 字段)
|
||||
- 定义 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 小时
|
||||
**建议开发模式**: 按顺序执行,每完成一个任务立即测试验证
|
||||
1051
docs/plans/fullstack/2026-03-02-lsfx-update-plan.md
Normal file
1051
docs/plans/fullstack/2026-03-02-lsfx-update-plan.md
Normal file
File diff suppressed because it is too large
Load Diff
745
docs/plans/fullstack/2026-03-04-bank-statement-implementation.md
Normal file
745
docs/plans/fullstack/2026-03-04-bank-statement-implementation.md
Normal 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
|
||||
786
docs/plans/fullstack/2026-03-04-create-project-integrate-lsfx.md
Normal file
786
docs/plans/fullstack/2026-03-04-create-project-integrate-lsfx.md
Normal 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`
|
||||
|
||||
---
|
||||
|
||||
**计划创建完成!**
|
||||
1713
docs/plans/fullstack/2026-03-04-lsfx-interface-update-plan.md
Normal file
1713
docs/plans/fullstack/2026-03-04-lsfx-interface-update-plan.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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#动态组件
|
||||
@@ -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`: 创建者用户ID(Long 类型)
|
||||
- `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 节
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
/** 项目ID(0表示全局配置,>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 项目ID(0表示全局配置)
|
||||
* @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. 验证响应:状态码 200,msg 为 "保存成功"
|
||||
|
||||
**步骤 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 - 项目ID(0表示全局配置)
|
||||
*/
|
||||
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: 暂不提交,继续下一步**
|
||||
@@ -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("项目配置类型已更新为 custom,projectId={}", 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. **前端验证优先** - 参数值验证在前端完成
|
||||
304
docs/plans/fullstack/2026-03-06-theme-light-default.md
Normal file
304
docs/plans/fullstack/2026-03-06-theme-light-default.md
Normal 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` - 设计文档
|
||||
@@ -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"
|
||||
```
|
||||
157
docs/plans/fullstack/2026-03-09-csv-pdf-upload-support.md
Normal file
157
docs/plans/fullstack/2026-03-09-csv-pdf-upload-support.md
Normal 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. 选择 projectId(如:1)
|
||||
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文件上传功能保持不变
|
||||
@@ -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` 相关测试通过
|
||||
@@ -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 分钟
|
||||
**难度等级:** 简单
|
||||
@@ -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
|
||||
```
|
||||
|
||||
预期:
|
||||
- 构建成功
|
||||
- 无模板编译错误
|
||||
@@ -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: 补充流水标签详细日志"
|
||||
```
|
||||
Reference in New Issue
Block a user