From f06ae4a9bf3644cd0b70f4891d2e0247d08140fd Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Thu, 19 Mar 2026 12:12:48 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=E7=BB=93=E6=9E=9C=E6=80=BB?= =?UTF-8?q?=E8=A7=88=E9=A3=8E=E9=99=A9=E6=8E=A5=E5=8F=A3=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=E4=B8=8E=E5=AE=9E=E6=96=BD=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...-03-19-results-overview-risk-api-design.md | 483 +++++++++++++++++ ...verview-risk-api-backend-implementation.md | 487 ++++++++++++++++++ ...erview-risk-api-frontend-implementation.md | 311 +++++++++++ ...9-results-overview-risk-api-plan-record.md | 35 ++ 4 files changed, 1316 insertions(+) create mode 100644 docs/design/2026-03-19-results-overview-risk-api-design.md create mode 100644 docs/plans/backend/2026-03-19-results-overview-risk-api-backend-implementation.md create mode 100644 docs/plans/frontend/2026-03-19-results-overview-risk-api-frontend-implementation.md create mode 100644 docs/reports/implementation/2026-03-19-results-overview-risk-api-plan-record.md diff --git a/docs/design/2026-03-19-results-overview-risk-api-design.md b/docs/design/2026-03-19-results-overview-risk-api-design.md new file mode 100644 index 00000000..c0c7ccb8 --- /dev/null +++ b/docs/design/2026-03-19-results-overview-risk-api-design.md @@ -0,0 +1,483 @@ +# 结果总览风险接口设计文档 + +**日期**: 2026-03-19 +**模块**: 初核项目详情 - 结果总览 +**作者**: Codex +**状态**: 已确认 + +## 一、概述 + +本文档用于沉淀结果总览页面中以下 3 个区块的后端设计方案: + +- 风险仪表盘 +- 风险人员总览 +- 中高风险人员 TOP10 + +本次设计目标是为现有结果总览页面补齐真实后端接口,并把项目级高、中、低风险人数的统计口径收拢到流水标签打标链路中,保证项目列表、结果总览仪表盘、风险人员榜单三处口径一致。 + +## 二、设计范围 + +### 2.1 包含内容 + +- 新增结果总览 3 个独立后端查询接口 +- 新增员工维度风险聚合查询 +- 在项目流水标签打标完成后回写项目风险人数 +- 输出对应后端实施计划与前端实施计划 + +### 2.2 不包含内容 + +- 不改造结果总览中的风险模型区、风险明细区 +- 不新增设计需求之外的导出、弹窗、降级、补丁逻辑 +- 不在查询接口中触发重新打标 +- 不新增客户、中介等非员工维度榜单 + +## 三、当前上下文 + +### 3.1 前端现状 + +结果总览页面已经完成静态结构,相关文件如下: + +- `ruoyi-ui/src/views/ccdiProject/components/detail/PreliminaryCheck.vue` +- `ruoyi-ui/src/views/ccdiProject/components/detail/OverviewStats.vue` +- `ruoyi-ui/src/views/ccdiProject/components/detail/RiskPeopleSection.vue` +- `ruoyi-ui/src/views/ccdiProject/components/detail/preliminaryCheck.mock.js` + +其中当前页面仍由本地 mock 数据驱动,三块目标区块的字段结构已经稳定: + +- 风险仪表盘:`title/subtitle/stats` +- 风险人员总览:`overviewList` +- 中高风险人员 TOP10:`topRiskList` + +### 3.2 后端现状 + +当前仓库中还没有结果总览专用后端接口,但已有以下可复用数据基础: + +1. `ccdi_project` + - 已有 `target_count` + - 已有 `high_risk_count` + - 已有 `medium_risk_count` + - 已有 `low_risk_count` +2. `ccdi_bank_statement_tag_result` + - 已沉淀项目维度的标签命中结果 + - 含 `project_id/model_code/rule_code/risk_level/bank_statement_id/object_type/object_key` +3. `ccdi_base_staff` + - 员工主数据,含 `name/dept_id/id_card` +4. `ccdi_staff_fmy_relation` + - 员工亲属映射,含 `person_id/relation_cert_no/status` +5. `sys_dept` + - 用于补齐所属部门名称 + +### 3.3 已确认业务口径 + +用户已确认以下口径: + +1. 接口粒度 + - 采用 3 个独立接口,而非 1 个汇总接口 +2. 风险仪表盘口径 + - `总人数 = ccdi_project.target_count` + - `高风险/中风险/低风险 = ccdi_project.high_risk_count / medium_risk_count / low_risk_count` + - `无风险人员 = 总人数 - 高风险 - 中风险 - 低风险` +3. 榜单统计对象 + - 统计“员工本人 + 员工亲属” + - 若命中亲属,则归并到所属员工名下 +4. 风险人员总览中的 `疑似违规数` + - 统计员工命中的去重规则数 +5. 员工风险等级口径 + - 命中规则数 `>= 5`:高风险 + - 命中规则数 `2-4`:中风险 + - 命中规则数 `= 1`:低风险 +6. TOP10 排序 + - 按员工风险等级优先级排序 + - 同等级按命中模型数倒序 + - 再按命中规则数倒序 + - 最后按员工身份证号升序 +7. 项目表风险人数维护方式 + - 在项目流水标签打标完成后回写高、中、低风险人数 + +## 四、方案对比与结论 + +### 4.1 方案一:新增结果总览专用控制器与 3 个独立接口 + +优点: + +- 与前端 3 个区块一一对应 +- 查询职责边界清晰 +- 后续风险模型区、风险明细区继续扩展时可沿用同一入口 + +缺点: + +- 需要新增一组 DTO/VO/Mapper 查询 + +### 4.2 方案二:把接口直接追加到 `CcdiProjectController` + +优点: + +- 表面改动点更少 + +缺点: + +- 项目管理与结果总览查询混在同一控制器 +- 后续结果总览继续扩展时边界会变乱 + +### 4.3 方案三:提供 1 个汇总接口 + +优点: + +- 请求数更少 + +缺点: + +- 与已确认的“3 个独立接口”目标不符 +- 单个区块口径变化会牵动整个响应结构 + +### 4.4 最终结论 + +采用方案一:新增结果总览专用控制器,提供 3 个独立接口,并新增员工维度聚合查询与项目风险人数回写逻辑。 + +## 五、接口设计 + +### 5.1 控制器落点 + +建议新增: + +- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java` + +接口统一挂载到: + +- `/ccdi/project/overview` + +### 5.2 接口清单 + +#### 接口一:查询风险仪表盘 + +- 方法:`GET` +- 路径:`/ccdi/project/overview/dashboard` +- 参数:`projectId` +- 返回:`AjaxResult.success(data)` + +返回结构示例: + +```json +{ + "title": "风险仪表盘", + "subtitle": "风险仪表盘数据概览", + "stats": [ + { "key": "people", "label": "总人数", "value": 500 }, + { "key": "riskPeople", "label": "高风险", "value": 10 }, + { "key": "medium", "label": "中风险", "value": 20 }, + { "key": "low", "label": "低风险", "value": 38 }, + { "key": "count", "label": "无风险人员", "value": 432 } + ] +} +``` + +#### 接口二:查询风险人员总览 + +- 方法:`GET` +- 路径:`/ccdi/project/overview/risk-people` +- 参数:`projectId` +- 返回:`AjaxResult.success(data)` + +返回结构示例: + +```json +{ + "overviewList": [ + { + "name": "李四", + "idNo": "330000000000000001", + "department": "信息二部", + "riskCount": 5, + "riskPoint": "大额单笔收入", + "actionLabel": "查看详情" + } + ] +} +``` + +#### 接口三:查询中高风险人员 TOP10 + +- 方法:`GET` +- 路径:`/ccdi/project/overview/top-risk-people` +- 参数:`projectId` +- 返回:`AjaxResult.success(data)` + +返回结构示例: + +```json +{ + "topRiskList": [ + { + "name": "张三", + "idNo": "330000000000000002", + "department": "信贷部", + "riskLevel": "高风险", + "riskLevelType": "danger", + "modelCount": 8, + "actionLabel": "查看详情" + } + ] +} +``` + +## 六、数据模型设计 + +### 6.1 新增 VO + +建议新增以下 VO,避免与现有 `CcdiProjectVO` 混用: + +1. 仪表盘 VO + - `CcdiProjectOverviewDashboardVO` + - `CcdiProjectOverviewStatVO` +2. 风险人员总览 VO + - `CcdiProjectRiskPeopleOverviewVO` + - `CcdiProjectRiskPeopleOverviewItemVO` +3. 中高风险人员 TOP10 VO + - `CcdiProjectTopRiskPeopleVO` + - `CcdiProjectTopRiskPeopleItemVO` +4. 内部聚合中间 VO + - `CcdiProjectEmployeeRiskAggregateVO` + +### 6.2 内部聚合字段 + +员工维度聚合中间结果至少包含: + +- `staffIdCard` +- `staffName` +- `deptId` +- `deptName` +- `ruleCount` +- `modelCount` +- `topRuleCode` +- `topRuleName` +- `riskLevelCode` +- `riskLevelName` +- `riskLevelSort` + +其中: + +- `ruleCount = count(distinct rule_code)` +- `modelCount = count(distinct model_code)` +- `riskLevelCode` 根据 `ruleCount` 映射为 `HIGH/MEDIUM/LOW` +- `riskLevelName` 映射为 `高风险/中风险/低风险` +- `riskLevelSort` 仅供 SQL 排序使用,`HIGH=1`、`MEDIUM=2`、`LOW=3` + +## 七、员工维度归并设计 + +### 7.1 归并目标 + +所有榜单都统一归并到员工维度,页面不直接展示亲属维度行。 + +### 7.2 归并规则 + +标签结果转员工归属时,按以下优先级处理: + +1. 若 `object_type = STAFF_ID_CARD` 且 `object_key` 命中员工身份证 + - 直接归到该员工 +2. 若 `object_key` 为空,但该条结果关联到流水,且流水 `cret_no` 命中员工身份证 + - 归到该员工 +3. 若 `object_key` 为空或为亲属证件号,且能通过 `ccdi_staff_fmy_relation.relation_cert_no` 关联到员工 `person_id` + - 归到该员工 +4. 若无法归并到员工 + - 本次查询中直接丢弃,不进入结果总览榜单 + +### 7.3 归并实现原则 + +- 不改造现有标签结果表结构 +- 不为本次需求新增缓存表或汇总表 +- 统一通过查询层完成员工归并 + +## 八、风险等级与项目人数回写设计 + +### 8.1 员工风险等级计算 + +按员工命中的去重规则数计算: + +- `ruleCount >= 5` -> `HIGH` / `高风险` +- `ruleCount between 2 and 4` -> `MEDIUM` / `中风险` +- `ruleCount = 1` -> `LOW` / `低风险` + +### 8.2 项目表风险人数统计 + +在员工维度聚合结果基础上统计: + +- `highRiskCount = 员工风险等级为 HIGH 的人数` +- `mediumRiskCount = 员工风险等级为 MEDIUM 的人数` +- `lowRiskCount = 员工风险等级为 LOW 的人数` + +### 8.3 回写时机 + +项目流水标签任务成功结束时执行回写,链路放在 `CcdiBankTagServiceImpl.rebuildProject(...)` 内: + +1. 删除旧标签结果 +2. 执行全部规则 +3. 批量写入新标签结果 +4. 重新按员工维度聚合高、中、低风险人数 +5. 更新 `ccdi_project` +6. 更新任务状态为成功 + +这样可保证查询接口读到的项目风险人数与最新标签结果一致。 + +### 8.4 不采用的方案 + +不采用“查询时实时扫标签结果并顺手更新项目表”的做法,因为: + +- 查询副作用过重 +- 口径维护分散 +- 项目列表与结果总览可能出现时间差 + +## 九、SQL 设计 + +### 9.1 Mapper 落点 + +建议新增: + +- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapper.java` +- `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml` + +### 9.2 查询职责拆分 + +Mapper 只负责以下查询: + +1. 查询项目仪表盘基础数据 +2. 查询员工维度风险总览列表 +3. 查询员工维度中高风险 TOP10 +4. 查询项目员工风险等级分布人数 + +### 9.3 员工聚合 SQL 结构 + +建议采用“公共子查询 + 外层聚合”的方式: + +1. 先构造标签结果到员工身份证的归并明细 +2. 再按员工身份证聚合规则数、模型数和代表性规则 +3. 最后外层补部门名称、风险等级和排序字段 + +建议公共子查询输出字段: + +- `project_id` +- `staff_id_card` +- `rule_code` +- `rule_name` +- `model_code` + +### 9.4 代表性异常点选择 + +`风险人员总览.riskPoint` 采用以下稳定选择策略: + +1. 先按员工 + 规则维度统计命中次数 +2. 按命中次数倒序 +3. 再按 `rule_code` 升序 +4. 取第一条 `rule_name` + +这样不依赖数据库非确定性行为,也不会因为同条规则多次命中而随机波动。 + +### 9.5 TOP10 筛选规则 + +TOP10 查询仅保留: + +- `ruleCount >= 2` + +即仅展示中风险和高风险员工。 + +## 十、服务层设计 + +### 10.1 Service 落点 + +建议新增: + +- `ICcdiProjectOverviewService` +- `CcdiProjectOverviewServiceImpl` + +### 10.2 服务职责 + +服务层负责: + +1. 调用 Mapper 查询项目仪表盘基础数据 +2. 组装前端需要的 `title/subtitle/stats` +3. 调用员工聚合查询并映射到列表 VO +4. 在打标完成后调用项目风险人数回写逻辑 +5. 处理空数据场景 + +### 10.3 空数据处理 + +1. 仪表盘 + - 若项目存在但风险人数为空,按 `0` 返回 +2. 风险人员总览 + - 返回 `overviewList: []` +3. TOP10 + - 返回 `topRiskList: []` + +## 十一、控制器与权限设计 + +控制器沿用现有项目模块风格: + +- 使用 `@RestController` +- 使用 `@Tag` +- 使用 `@Operation` +- 使用 `@PreAuthorize("@ss.hasPermi('ccdi:project:query')")` +- 返回 `AjaxResult.success(...)` + +本次不新增新的权限标识,直接复用项目查询权限。 + +## 十二、异常处理设计 + +### 12.1 查询接口 + +- 项目不存在:返回业务异常,提示“项目不存在” +- `projectId` 为空:走参数校验失败 + +### 12.2 回写链路 + +若项目风险人数回写失败: + +- 视为本次打标任务失败 +- 不允许出现“标签写入成功但项目人数未更新且任务仍成功”的状态 + +原因是本次需求要求三处口径一致,项目人数回写属于打标成功链路的一部分,而不是可选附加动作。 + +## 十三、测试设计 + +### 13.1 后端单元/集成测试重点 + +1. 风险仪表盘 + - 校验 `无风险人员` 计算逻辑 +2. 风险人员总览 + - 校验员工本人命中可正常归并 + - 校验亲属命中可正常归并到员工 + - 校验 `riskCount = 去重规则数` + - 校验 `riskPoint` 选择稳定 +3. TOP10 + - 校验仅返回中高风险 + - 校验排序规则正确 +4. 打标回写 + - 校验任务成功后项目表风险人数被更新 + - 校验回写失败时任务整体失败 + +### 13.2 前端联调验证重点 + +1. mock 替换为 3 个真实接口 +2. 字段命名与当前页面结构一致 +3. 空列表时页面能正确展示空表格 + +## 十四、风险与约束 + +### 14.1 已知约束 + +- 当前标签结果表未直接存员工归属字段,需要通过查询归并 +- 部分标签结果可能来自 `bank_statement_id` 关联的流水,而非直接对象命中 + +### 14.2 风险控制 + +- 统一通过员工聚合查询输出榜单,避免前后接口各自实现一套归并逻辑 +- 项目风险人数只在打标成功后回写,避免查询阶段产生副作用 +- 不引入额外缓存与汇总表,保持最短实现路径 + +## 十五、最终结论 + +本方案采用“结果总览专用接口 + 标签完成后回写项目风险人数”的最短路径实现: + +1. 结果总览新增 3 个独立接口 +2. 风险榜单统一按员工维度聚合 +3. 员工风险等级按命中规则条数区间计算 +4. 项目表高、中、低风险人数在打标完成后统一回写 +5. 项目列表、风险仪表盘、风险人员榜单三处口径保持一致 diff --git a/docs/plans/backend/2026-03-19-results-overview-risk-api-backend-implementation.md b/docs/plans/backend/2026-03-19-results-overview-risk-api-backend-implementation.md new file mode 100644 index 00000000..bf1b35b7 --- /dev/null +++ b/docs/plans/backend/2026-03-19-results-overview-risk-api-backend-implementation.md @@ -0,0 +1,487 @@ +# Results Overview Risk API Backend 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:** 为结果总览页面实现风险仪表盘、风险人员总览、中高风险人员 TOP10 三个后端接口,并在项目流水标签打标完成后回写项目高、中、低风险人数。 + +**Architecture:** 在 `ccdi-project` 模块内新增结果总览专用 Controller、Service、Mapper 与 VO,查询统一基于 `ccdi_project` 和 `ccdi_bank_statement_tag_result` 聚合,不新增兼容性补丁链路。员工风险等级按命中去重规则数分级,项目表高、中、低风险人数在标签任务成功结束后同步回写,保证项目列表与结果总览口径一致。 + +**Tech Stack:** Java 21, Spring Boot 3, MyBatis XML, MyBatis Plus, Maven, JUnit + +--- + +### Task 1: 定义结果总览 VO 与服务接口骨架 + +**Files:** +- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewDashboardVO.java` +- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewStatVO.java` +- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectRiskPeopleOverviewVO.java` +- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectRiskPeopleOverviewItemVO.java` +- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectTopRiskPeopleVO.java` +- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectTopRiskPeopleItemVO.java` +- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectEmployeeRiskAggregateVO.java` +- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java` +- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiProjectOverviewServiceStructureTest.java` + +- [ ] **Step 1: Write the failing test** + +新增结构测试,锁定服务接口与 VO 名称: + +```java +@Test +void shouldExposeOverviewServiceMethods() throws Exception { + Class clazz = Class.forName("com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService"); + assertNotNull(clazz.getMethod("getDashboard", Long.class)); + assertNotNull(clazz.getMethod("getRiskPeopleOverview", Long.class)); + assertNotNull(clazz.getMethod("getTopRiskPeople", Long.class)); + assertNotNull(clazz.getMethod("refreshProjectRiskCounts", Long.class, String.class)); +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: + +```bash +mvn test -pl ccdi-project -Dtest=CcdiProjectOverviewServiceStructureTest +``` + +Expected: + +- `FAIL` +- 原因是接口与 VO 尚未创建 + +- [ ] **Step 3: Write minimal implementation** + +最小落地如下: + +1. 为 3 个区块分别创建返回 VO。 +2. 新建员工聚合中间 VO,字段至少包括: + - `staffIdCard` + - `staffName` + - `deptName` + - `ruleCount` + - `modelCount` + - `topRuleName` + - `riskLevelCode` + - `riskLevelName` + - `riskLevelSort` +3. 新建 `ICcdiProjectOverviewService`,声明以下方法: + +```java +CcdiProjectOverviewDashboardVO getDashboard(Long projectId); + +CcdiProjectRiskPeopleOverviewVO getRiskPeopleOverview(Long projectId); + +CcdiProjectTopRiskPeopleVO getTopRiskPeople(Long projectId); + +void refreshProjectRiskCounts(Long projectId, String operator); +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: + +```bash +mvn test -pl ccdi-project -Dtest=CcdiProjectOverviewServiceStructureTest +``` + +Expected: + +- `PASS` + +- [ ] **Step 5: Commit** + +```bash +git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewDashboardVO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewStatVO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectRiskPeopleOverviewVO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectRiskPeopleOverviewItemVO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectTopRiskPeopleVO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectTopRiskPeopleItemVO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectEmployeeRiskAggregateVO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiProjectOverviewServiceStructureTest.java +git commit -m "定义结果总览风险接口基础结构" +``` + +### Task 2: 新增结果总览 Mapper 与员工归并聚合 SQL + +**Files:** +- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapper.java` +- Create: `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml` +- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperSqlTest.java` + +- [ ] **Step 1: Write the failing test** + +新增 SQL 结构测试,锁定 Mapper XML 中必须包含员工归并和风险等级分段逻辑: + +```java +@Test +void shouldContainEmployeeRiskAggregationSql() throws Exception { + String xml = Files.readString(Path.of("ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml")); + assertTrue(xml.contains("count(distinct base.rule_code)")); + assertTrue(xml.contains("count(distinct base.model_code)")); + assertTrue(xml.contains("when agg.rule_count >= 5 then 'HIGH'")); + assertTrue(xml.contains("when agg.rule_count between 2 and 4 then 'MEDIUM'")); +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: + +```bash +mvn test -pl ccdi-project -Dtest=CcdiProjectOverviewMapperSqlTest +``` + +Expected: + +- `FAIL` +- 原因是 Mapper 文件还不存在 + +- [ ] **Step 3: Write minimal implementation** + +创建 `CcdiProjectOverviewMapper` 与 XML,至少提供以下查询: + +1. `selectDashboardBaseByProjectId(Long projectId)` +2. `selectRiskPeopleOverviewByProjectId(Long projectId)` +3. `selectTopRiskPeopleByProjectId(Long projectId)` +4. `selectRiskCountSummaryByProjectId(Long projectId)` + +SQL 设计要求: + +1. 先用公共子查询把标签结果归并到员工身份证: + - 本人命中:`object_type = 'STAFF_ID_CARD'` + - 流水本人命中:`bank_statement_id -> ccdi_bank_statement.cret_no` + - 亲属命中:`relation_cert_no -> person_id` +2. 外层按员工聚合: + +```sql +count(distinct base.rule_code) as rule_count, +count(distinct base.model_code) as model_count +``` + +3. 风险等级按规则数分段: + +```sql +case + when agg.rule_count >= 5 then 'HIGH' + when agg.rule_count between 2 and 4 then 'MEDIUM' + else 'LOW' +end +``` + +4. `riskPoint` 使用“规则命中次数倒序 + rule_code 升序”取第一条规则名。 + +- [ ] **Step 4: Run test to verify it passes** + +Run: + +```bash +mvn test -pl ccdi-project -Dtest=CcdiProjectOverviewMapperSqlTest +``` + +Expected: + +- `PASS` + +- [ ] **Step 5: Commit** + +```bash +git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapper.java ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperSqlTest.java +git commit -m "新增结果总览员工风险聚合查询" +``` + +### Task 3: 实现结果总览 Service + +**Files:** +- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java` +- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiProjectOverviewServiceImplTest.java` + +- [ ] **Step 1: Write the failing test** + +为服务层编写测试,锁定仪表盘与列表组装逻辑: + +```java +@Test +void shouldBuildDashboardWithNoRiskCount() { + // mock project targetCount=100 high=5 medium=10 low=15 + // assert noRiskCount == 70 +} + +@Test +void shouldMapRiskPeopleOverviewRows() { + // mock aggregate row + // assert riskCount/topRuleName/actionLabel mapping +} + +@Test +void shouldMapTopRiskPeopleRows() { + // mock aggregate row with HIGH + // assert riskLevelType == "danger" +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: + +```bash +mvn test -pl ccdi-project -Dtest=CcdiProjectOverviewServiceImplTest +``` + +Expected: + +- `FAIL` + +- [ ] **Step 3: Write minimal implementation** + +在 `CcdiProjectOverviewServiceImpl` 中完成: + +1. `getDashboard` + - 读项目基础数据 + - 计算无风险人数,空值按 0 处理 +2. `getRiskPeopleOverview` + - 调 Mapper 查询员工聚合结果 + - 映射为 `overviewList` + - 固定补 `actionLabel = "查看详情"` +3. `getTopRiskPeople` + - 调 Mapper 查询 TOP10 + - 将 `HIGH/MEDIUM` 映射为 `高风险/中风险` + - 将 `HIGH -> danger`、`MEDIUM -> warning` +4. 若项目不存在,抛出业务异常 + +如需项目存在性校验,直接复用现有项目查询能力,不新增兜底流程。 + +- [ ] **Step 4: Run test to verify it passes** + +Run: + +```bash +mvn test -pl ccdi-project -Dtest=CcdiProjectOverviewServiceImplTest +``` + +Expected: + +- `PASS` + +- [ ] **Step 5: Commit** + +```bash +git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiProjectOverviewServiceImplTest.java +git commit -m "实现结果总览风险接口服务层" +``` + +### Task 4: 新增结果总览 Controller 接口 + +**Files:** +- Create: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java` +- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerTest.java` + +- [ ] **Step 1: Write the failing test** + +为控制器编写测试,锁定 3 个 GET 接口: + +```java +@Test +void shouldExposeDashboardEndpoint() {} + +@Test +void shouldExposeRiskPeopleEndpoint() {} + +@Test +void shouldExposeTopRiskPeopleEndpoint() {} +``` + +校验内容: + +- 路径为 `/ccdi/project/overview/*` +- 权限为 `ccdi:project:query` +- 返回 `AjaxResult.success(...)` + +- [ ] **Step 2: Run test to verify it fails** + +Run: + +```bash +mvn test -pl ccdi-project -Dtest=CcdiProjectOverviewControllerTest +``` + +Expected: + +- `FAIL` + +- [ ] **Step 3: Write minimal implementation** + +控制器实现: + +```java +@GetMapping("/dashboard") +public AjaxResult getDashboard(Long projectId) { ... } + +@GetMapping("/risk-people") +public AjaxResult getRiskPeople(Long projectId) { ... } + +@GetMapping("/top-risk-people") +public AjaxResult getTopRiskPeople(Long projectId) { ... } +``` + +要求: + +- 添加 Swagger 注释 +- 复用 `@PreAuthorize("@ss.hasPermi('ccdi:project:query')")` +- 仅接收 `projectId` + +- [ ] **Step 4: Run test to verify it passes** + +Run: + +```bash +mvn test -pl ccdi-project -Dtest=CcdiProjectOverviewControllerTest +``` + +Expected: + +- `PASS` + +- [ ] **Step 5: Commit** + +```bash +git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerTest.java +git commit -m "新增结果总览风险查询接口" +``` + +### Task 5: 在标签任务成功后回写项目风险人数 + +**Files:** +- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java` +- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectMapper.java` +- Modify: `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectMapper.xml` +- Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiBankTagServiceRiskCountRefreshTest.java` + +- [ ] **Step 1: Write the failing test** + +新增测试,锁定打标成功后必须刷新项目风险人数: + +```java +@Test +void shouldRefreshProjectRiskCountsAfterTagRebuildSuccess() { + // mock successful rule execution + // verify overviewService.refreshProjectRiskCounts(projectId, operator) +} +``` + +并新增一条失败场景: + +```java +@Test +void shouldFailTaskWhenRiskCountRefreshFails() { + // mock refreshProjectRiskCounts throws exception + // assert task status becomes FAILED +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: + +```bash +mvn test -pl ccdi-project -Dtest=CcdiBankTagServiceRiskCountRefreshTest +``` + +Expected: + +- `FAIL` + +- [ ] **Step 3: Write minimal implementation** + +实施要求: + +1. 在 `CcdiBankTagServiceImpl.rebuildProject(...)` 中: + - 标签结果批量写入成功后 + - 任务状态改成功前 + - 调用 `projectOverviewService.refreshProjectRiskCounts(projectId, operator)` +2. 在项目 Mapper 中新增更新人数方法: + +```java +int updateRiskCountsByProjectId(@Param("projectId") Long projectId, + @Param("highRiskCount") Integer highRiskCount, + @Param("mediumRiskCount") Integer mediumRiskCount, + @Param("lowRiskCount") Integer lowRiskCount, + @Param("updateBy") String updateBy); +``` + +3. `refreshProjectRiskCounts` 内部先查员工聚合统计,再更新项目表。 + +- [ ] **Step 4: Run test to verify it passes** + +Run: + +```bash +mvn test -pl ccdi-project -Dtest=CcdiBankTagServiceRiskCountRefreshTest +``` + +Expected: + +- `PASS` + +- [ ] **Step 5: Commit** + +```bash +git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectMapper.java ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectMapper.xml ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiBankTagServiceRiskCountRefreshTest.java +git commit -m "打标完成后回写项目风险人数" +``` + +### Task 6: 补充后端验证记录与实施记录 + +**Files:** +- Create: `docs/tests/records/2026-03-19-results-overview-risk-api-backend-verification.md` +- Create: `docs/reports/implementation/2026-03-19-results-overview-risk-api-backend-implementation.md` + +- [ ] **Step 1: Write the failing record skeleton** + +先创建验证记录模板: + +```md +# 结果总览风险接口后端验证记录 + +## 验证范围 +- 风险仪表盘接口 +- 风险人员总览接口 +- 中高风险人员 TOP10 接口 +- 打标完成后项目风险人数回写 +``` + +- [ ] **Step 2: Run backend verification commands** + +Run: + +```bash +mvn test -pl ccdi-project -Dtest=CcdiProjectOverviewServiceStructureTest,CcdiProjectOverviewMapperSqlTest,CcdiProjectOverviewServiceImplTest,CcdiProjectOverviewControllerTest,CcdiBankTagServiceRiskCountRefreshTest +``` + +Expected: + +- 相关测试全部 `PASS` + +- [ ] **Step 3: Write minimal implementation record** + +在实施记录中说明: + +- 新增 3 个结果总览接口 +- 员工风险等级按规则数分级 +- 标签完成后回写项目风险人数 +- 未扩展风险模型区和风险明细区接口 + +- [ ] **Step 4: Re-run record review** + +Run: + +```bash +sed -n '1,220p' docs/tests/records/2026-03-19-results-overview-risk-api-backend-verification.md +sed -n '1,220p' docs/reports/implementation/2026-03-19-results-overview-risk-api-backend-implementation.md +``` + +Expected: + +- 两份文档都能覆盖本次后端改动和验证结果 + +- [ ] **Step 5: Commit** + +```bash +git add docs/tests/records/2026-03-19-results-overview-risk-api-backend-verification.md docs/reports/implementation/2026-03-19-results-overview-risk-api-backend-implementation.md +git commit -m "补充结果总览风险接口后端记录" +``` diff --git a/docs/plans/frontend/2026-03-19-results-overview-risk-api-frontend-implementation.md b/docs/plans/frontend/2026-03-19-results-overview-risk-api-frontend-implementation.md new file mode 100644 index 00000000..9ac9df01 --- /dev/null +++ b/docs/plans/frontend/2026-03-19-results-overview-risk-api-frontend-implementation.md @@ -0,0 +1,311 @@ +# Results Overview Risk API Frontend 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:** 将结果总览页面中的风险仪表盘、风险人员总览、中高风险人员 TOP10 从本地 mock 数据切换为真实后端接口数据。 + +**Architecture:** 保持 `PreliminaryCheck.vue` 作为结果总览页面入口,不改造当前 4 区块布局,只在前端增加结果总览 API 封装、页面数据拉取与 3 个区块的真实数据映射。风险模型区和风险明细区继续使用现有 mock 或原状,不额外扩展接口范围。 + +**Tech Stack:** Vue 2, Element UI, Axios (`@/utils/request`), Node.js, npm + +--- + +### Task 1: 新增结果总览 API 封装 + +**Files:** +- Create: `ruoyi-ui/src/api/ccdi/projectOverview.js` +- Test: `ruoyi-ui/tests/unit/project-overview-api.test.js` + +- [ ] **Step 1: Write the failing test** + +新增静态断言脚本,锁定 3 个接口方法: + +```js +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const source = fs.readFileSync( + path.resolve(__dirname, "../../src/api/ccdi/projectOverview.js"), + "utf8" +); + +[ + "getOverviewDashboard", + "getOverviewRiskPeople", + "getOverviewTopRiskPeople", + "/ccdi/project/overview/dashboard", + "/ccdi/project/overview/risk-people", + "/ccdi/project/overview/top-risk-people", +].forEach((token) => assert(source.includes(token), token)); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: + +```bash +cd ruoyi-ui +node tests/unit/project-overview-api.test.js +``` + +Expected: + +- `FAIL` + +- [ ] **Step 3: Write minimal implementation** + +创建 API 文件: + +```js +import request from "@/utils/request"; + +export function getOverviewDashboard(projectId) { + return request({ url: "/ccdi/project/overview/dashboard", method: "get", params: { projectId } }); +} +``` + +同理补齐另外两个接口。 + +- [ ] **Step 4: Run test to verify it passes** + +Run: + +```bash +cd ruoyi-ui +node tests/unit/project-overview-api.test.js +``` + +Expected: + +- `PASS` + +- [ ] **Step 5: Commit** + +```bash +git add ruoyi-ui/src/api/ccdi/projectOverview.js ruoyi-ui/tests/unit/project-overview-api.test.js +git commit -m "新增结果总览风险接口前端封装" +``` + +### Task 2: 在 PreliminaryCheck 页面接入 3 个真实接口 + +**Files:** +- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/PreliminaryCheck.vue` +- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/preliminaryCheck.mock.js` +- Test: `ruoyi-ui/tests/unit/preliminary-check-api-integration.test.js` + +- [ ] **Step 1: Write the failing test** + +新增静态断言脚本,锁定页面必须引入真实 API: + +```js +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const source = fs.readFileSync( + path.resolve(__dirname, "../../src/views/ccdiProject/components/detail/PreliminaryCheck.vue"), + "utf8" +); + +[ + "getOverviewDashboard", + "getOverviewRiskPeople", + "getOverviewTopRiskPeople", + "loadOverviewData", + "Promise.all", +].forEach((token) => assert(source.includes(token), token)); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: + +```bash +cd ruoyi-ui +node tests/unit/preliminary-check-api-integration.test.js +``` + +Expected: + +- `FAIL` + +- [ ] **Step 3: Write minimal implementation** + +在 `PreliminaryCheck.vue` 中: + +1. 引入 3 个 API 方法 +2. 新增 `loadOverviewData` +3. 在页面进入或 `projectId` 变化时并发拉取 3 个接口 +4. 将返回结果合并到页面数据: + +```js +this.realData = { + ...this.mockData, + summary: dashboardData, + riskPeople: { + overviewList: riskPeopleData.overviewList || [], + topRiskList: topRiskPeopleData.topRiskList || [], + }, +}; +``` + +5. 保留现有 `loading / empty / loaded` 状态 + +本任务只替换这 3 块数据,不动风险模型区和风险明细区。 + +- [ ] **Step 4: Run test to verify it passes** + +Run: + +```bash +cd ruoyi-ui +node tests/unit/preliminary-check-api-integration.test.js +``` + +Expected: + +- `PASS` + +- [ ] **Step 5: Commit** + +```bash +git add ruoyi-ui/src/views/ccdiProject/components/detail/PreliminaryCheck.vue ruoyi-ui/src/views/ccdiProject/components/detail/preliminaryCheck.mock.js ruoyi-ui/tests/unit/preliminary-check-api-integration.test.js +git commit -m "接入结果总览风险真实接口" +``` + +### Task 3: 校准风险人员区字段映射与空态 + +**Files:** +- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/RiskPeopleSection.vue` +- Test: `ruoyi-ui/tests/unit/preliminary-check-risk-people-binding.test.js` + +- [ ] **Step 1: Write the failing test** + +新增静态断言脚本,锁定字段映射仍与当前页面结构兼容: + +```js +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const source = fs.readFileSync( + path.resolve(__dirname, "../../src/views/ccdiProject/components/detail/RiskPeopleSection.vue"), + "utf8" +); + +[ + "sectionData.overviewList", + "sectionData.topRiskList", + "riskCount", + "riskPoint", + "modelCount", + "riskLevelType", +].forEach((token) => assert(source.includes(token), token)); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: + +```bash +cd ruoyi-ui +node tests/unit/preliminary-check-risk-people-binding.test.js +``` + +Expected: + +- 若字段被改乱或误删则 `FAIL` + +- [ ] **Step 3: Write minimal implementation** + +实施要求: + +1. 保持现有表格列不重排 +2. 若接口返回空数组,表格自然展示 Element 空态 +3. `riskLevelType` 不在组件内重新判断,直接使用后端返回值 +4. `actionLabel` 缺失时仍回退为 `查看详情` + +- [ ] **Step 4: Run test to verify it passes** + +Run: + +```bash +cd ruoyi-ui +node tests/unit/preliminary-check-risk-people-binding.test.js +``` + +Expected: + +- `PASS` + +- [ ] **Step 5: Commit** + +```bash +git add ruoyi-ui/src/views/ccdiProject/components/detail/RiskPeopleSection.vue ruoyi-ui/tests/unit/preliminary-check-risk-people-binding.test.js +git commit -m "校准结果总览风险人员区字段映射" +``` + +### Task 4: 补充前端验证记录与实施记录 + +**Files:** +- Create: `docs/tests/records/2026-03-19-results-overview-risk-api-frontend-verification.md` +- Create: `docs/reports/implementation/2026-03-19-results-overview-risk-api-frontend-implementation.md` + +- [ ] **Step 1: Write the failing record skeleton** + +先创建验证记录模板: + +```md +# 结果总览风险接口前端验证记录 + +## 验证范围 +- API 封装 +- 结果总览页面并发取数 +- 风险人员区字段映射 +``` + +- [ ] **Step 2: Run frontend verification commands** + +Run: + +```bash +cd ruoyi-ui +node tests/unit/project-overview-api.test.js +node tests/unit/preliminary-check-api-integration.test.js +node tests/unit/preliminary-check-risk-people-binding.test.js +npm run build:prod +``` + +Expected: + +- 3 个脚本 `PASS` +- 构建成功 + +- [ ] **Step 3: Write minimal implementation record** + +实施记录至少包含: + +- 新增结果总览 API 封装 +- 页面接入 3 个真实接口 +- 风险模型区和风险明细区本轮保持不变 + +- [ ] **Step 4: Re-run record review** + +Run: + +```bash +sed -n '1,220p' docs/tests/records/2026-03-19-results-overview-risk-api-frontend-verification.md +sed -n '1,220p' docs/reports/implementation/2026-03-19-results-overview-risk-api-frontend-implementation.md +``` + +Expected: + +- 两份文档都能覆盖本次前端接入范围和验证方法 + +- [ ] **Step 5: Commit** + +```bash +git add docs/tests/records/2026-03-19-results-overview-risk-api-frontend-verification.md docs/reports/implementation/2026-03-19-results-overview-risk-api-frontend-implementation.md +git commit -m "补充结果总览风险接口前端记录" +``` diff --git a/docs/reports/implementation/2026-03-19-results-overview-risk-api-plan-record.md b/docs/reports/implementation/2026-03-19-results-overview-risk-api-plan-record.md new file mode 100644 index 00000000..25938521 --- /dev/null +++ b/docs/reports/implementation/2026-03-19-results-overview-risk-api-plan-record.md @@ -0,0 +1,35 @@ +# 结果总览风险接口计划记录 + +## 变更概述 + +- 新增结果总览风险接口设计文档 1 份。 +- 新增结果总览风险接口后端实施计划 1 份。 +- 新增结果总览风险接口前端实施计划 1 份。 +- 本次仅完成设计与计划沉淀,尚未进入代码实现阶段。 + +## 新增文件 + +- `docs/design/2026-03-19-results-overview-risk-api-design.md` +- `docs/plans/backend/2026-03-19-results-overview-risk-api-backend-implementation.md` +- `docs/plans/frontend/2026-03-19-results-overview-risk-api-frontend-implementation.md` + +## 设计结论 + +- 结果总览采用 3 个独立接口:风险仪表盘、风险人员总览、中高风险人员 TOP10。 +- 风险人员榜单统一按员工维度聚合,员工亲属命中归并到所属员工。 +- 员工风险等级按命中去重规则数分级: + - 5 条及以上:高风险 + - 2 到 4 条:中风险 + - 1 条:低风险 +- 项目表高、中、低风险人数在项目流水标签打标成功后统一回写。 + +## 计划结论 + +- 后端计划聚焦结果总览 Controller、Service、Mapper、聚合 SQL 与打标后人数回写。 +- 前端计划聚焦 3 个结果总览风险接口接入,不扩展风险模型区和风险明细区。 +- 后续实施阶段需分别补充前后端验证记录与实施记录。 + +## 说明 + +- 本次按仓库规范将设计文档落在 `docs/design/`,将实施计划分别落在 `docs/plans/backend/` 与 `docs/plans/frontend/`。 +- 由于本轮用户明确要求直接完成设计与计划,不再分段等待确认,故本记录视为本次设计收口材料。