Compare commits

...

2 Commits

24 changed files with 661 additions and 41 deletions

BIN
.DS_Store vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -8,6 +8,10 @@ import lombok.Data;
@Data
public class CcdiProjectRiskHitTagVO {
private String modelCode;
private String modelName;
private String ruleCode;
private String ruleName;

View File

@@ -1,5 +1,6 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.util.List;
import lombok.Data;
/**
@@ -24,5 +25,7 @@ public class CcdiProjectRiskPeopleOverviewItemVO {
private String riskPoint;
private List<CcdiProjectRiskHitTagVO> riskPointTagList;
private String actionLabel;
}

View File

@@ -5,6 +5,7 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO;
import java.util.List;
import java.util.Map;
@@ -61,6 +62,20 @@ public interface CcdiProjectOverviewMapper {
@Param("query") CcdiProjectRiskModelPeopleQueryDTO query
);
/**
* 按员工范围查询命中标签
*
* @param projectId 项目ID
* @param staffIdCard 员工身份证号
* @param selectedModelCodes 已选模型编码CSV可为空
* @return 命中标签列表
*/
List<CcdiProjectRiskHitTagVO> selectRiskHitTagsByScope(
@Param("projectId") Long projectId,
@Param("staffIdCard") String staffIdCard,
@Param("selectedModelCodes") String selectedModelCodes
);
/**
* 查询项目风险人数汇总
*

View File

@@ -78,7 +78,7 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
List<CcdiProjectRiskPeopleOverviewItemVO> overviewList = overviewMapper.selectRiskPeopleOverviewByProjectId(projectId)
.stream()
.map(this::buildRiskPeopleItem)
.map(aggregate -> buildRiskPeopleItem(projectId, aggregate))
.toList();
CcdiProjectRiskPeopleOverviewVO overview = new CcdiProjectRiskPeopleOverviewVO();
@@ -168,7 +168,7 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
);
}
private CcdiProjectRiskPeopleOverviewItemVO buildRiskPeopleItem(CcdiProjectEmployeeRiskAggregateVO aggregate) {
private CcdiProjectRiskPeopleOverviewItemVO buildRiskPeopleItem(Long projectId, CcdiProjectEmployeeRiskAggregateVO aggregate) {
CcdiProjectRiskPeopleOverviewItemVO item = new CcdiProjectRiskPeopleOverviewItemVO();
item.setName(aggregate.getStaffName());
item.setIdNo(aggregate.getStaffIdCard());
@@ -178,6 +178,9 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
item.setRiskLevelType(resolveRiskLevelType(aggregate.getRiskLevelCode()));
item.setModelCount(defaultZero(aggregate.getModelCount()));
item.setRiskPoint(aggregate.getRiskPoint());
item.setRiskPointTagList(defaultList(
overviewMapper.selectRiskHitTagsByScope(projectId, aggregate.getStaffIdCard(), null)
));
item.setActionLabel(ACTION_LABEL);
return item;
}

View File

@@ -347,12 +347,6 @@
) idx on idx.idx &lt; json_length(result.model_hit_summary_json)
where result.project_id = #{projectId}
and result.staff_id_card = #{staffIdCard}
<if test="selectedModelCodes != null and selectedModelCodes != ''">
and find_in_set(
json_unquote(json_extract(result.model_hit_summary_json, concat('$[', idx.idx, '].modelCode'))),
#{selectedModelCodes}
)
</if>
group by
json_unquote(json_extract(result.model_hit_summary_json, concat('$[', idx.idx, '].modelCode'))),
json_unquote(json_extract(result.model_hit_summary_json, concat('$[', idx.idx, '].modelName')))
@@ -361,6 +355,8 @@
<select id="selectRiskHitTagsByScope" resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO">
select
json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].modelCode'))) as model_code,
max(json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].modelName')))) as model_name,
json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].ruleCode'))) as rule_code,
max(json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].ruleName')))) as rule_name,
max(json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].riskLevel')))) as risk_level
@@ -370,18 +366,14 @@
) idx on idx.idx &lt; json_length(result.hit_rules_json)
where result.project_id = #{projectId}
and result.staff_id_card = #{staffIdCard}
<if test="selectedModelCodes != null and selectedModelCodes != ''">
and find_in_set(
json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].modelCode'))),
#{selectedModelCodes}
)
</if>
group by json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].ruleCode')))
group by json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].modelCode'))),
json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].ruleCode')))
order by case max(json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].riskLevel'))))
when 'HIGH' then 1
when 'MEDIUM' then 2
else 3
end,
json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].modelCode'))) asc,
json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].ruleCode'))) asc
</select>

View File

@@ -1,6 +1,7 @@
package com.ruoyi.ccdi.project.controller;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO;
@@ -62,6 +63,11 @@ class CcdiProjectOverviewControllerTest {
item.setRiskLevel("中风险");
item.setRiskLevelType("warning");
item.setModelCount(4);
CcdiProjectRiskHitTagVO riskPointTag = new CcdiProjectRiskHitTagVO();
riskPointTag.setModelCode("SALARY");
riskPointTag.setModelName("产薪异常模型");
riskPointTag.setRuleName("多工资转入");
item.setRiskPointTagList(List.of(riskPointTag));
CcdiProjectRiskPeopleOverviewVO overview = new CcdiProjectRiskPeopleOverviewVO();
overview.setOverviewList(List.of(item));
when(overviewService.getRiskPeopleOverview(40L)).thenReturn(overview);
@@ -73,6 +79,7 @@ class CcdiProjectOverviewControllerTest {
assertEquals("中风险", data.getOverviewList().getFirst().getRiskLevel());
assertEquals("warning", data.getOverviewList().getFirst().getRiskLevelType());
assertEquals(4, data.getOverviewList().getFirst().getModelCount());
assertEquals("SALARY", data.getOverviewList().getFirst().getRiskPointTagList().getFirst().getModelCode());
verify(overviewService).getRiskPeopleOverview(40L);
Method method = CcdiProjectOverviewController.class.getMethod("getRiskPeople", Long.class);

View File

@@ -48,5 +48,8 @@ class CcdiProjectOverviewMapperRiskModelPeopleTest {
assertTrue(xml.contains(".modelName"));
assertTrue(xml.contains(".ruleCode"));
assertTrue(xml.contains(".riskLevel"));
assertTrue(xml.contains("as model_code"));
assertTrue(xml.contains("as model_name"));
assertTrue(xml.contains("group by json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].modelCode')))"));
}
}

View File

@@ -9,6 +9,7 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewEmployeeHitRowVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardsVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO;
@@ -86,6 +87,10 @@ class CcdiProjectOverviewServiceImplTest {
aggregate.setModelCount(3);
aggregate.setRiskPoint("大额单笔收入、疑似兼职");
when(overviewMapper.selectRiskPeopleOverviewByProjectId(40L)).thenReturn(List.of(aggregate));
when(overviewMapper.selectRiskHitTagsByScope(40L, "330000000000000001", null)).thenReturn(List.of(
buildHitTag("LARGE_TRANSACTION", "大额交易模型", "RULE_A", "大额单笔收入", "HIGH"),
buildHitTag("PART_TIME", "兼职取酬模型", "RULE_B", "疑似兼职", "MEDIUM")
));
CcdiProjectRiskPeopleOverviewVO overview = service.getRiskPeopleOverview(40L);
@@ -94,6 +99,9 @@ class CcdiProjectOverviewServiceImplTest {
assertEquals("高风险", overview.getOverviewList().getFirst().getRiskLevel());
assertEquals("danger", overview.getOverviewList().getFirst().getRiskLevelType());
assertEquals(3, overview.getOverviewList().getFirst().getModelCount());
assertEquals(2, overview.getOverviewList().getFirst().getRiskPointTagList().size());
assertEquals("LARGE_TRANSACTION", overview.getOverviewList().getFirst().getRiskPointTagList().getFirst().getModelCode());
assertEquals("大额交易模型", overview.getOverviewList().getFirst().getRiskPointTagList().getFirst().getModelName());
assertEquals("大额单笔收入、疑似兼职", overview.getOverviewList().getFirst().getRiskPoint());
assertEquals("查看详情", overview.getOverviewList().getFirst().getActionLabel());
}
@@ -279,4 +287,20 @@ class CcdiProjectOverviewServiceImplTest {
result.setRiskLevelCode(riskLevelCode);
return result;
}
private CcdiProjectRiskHitTagVO buildHitTag(
String modelCode,
String modelName,
String ruleCode,
String ruleName,
String riskLevel
) {
CcdiProjectRiskHitTagVO hitTag = new CcdiProjectRiskHitTagVO();
hitTag.setModelCode(modelCode);
hitTag.setModelName(modelName);
hitTag.setRuleCode(ruleCode);
hitTag.setRuleName(ruleName);
hitTag.setRiskLevel(riskLevel);
return hitTag;
}
}

View File

@@ -0,0 +1,124 @@
# Results Overview Hit Tag Model Color 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:** 为项目总览页“核心异常点”和“异常标签”补充标签所属模型字段,支撑前端按选中模型高亮标签颜色。
**Architecture:** 保持现有结果总览接口与查询入口不变,仅在标签 VO、Mapper XML 和服务映射链路中补充 `modelCode``modelName` 字段。风险人员总览、命中模型涉及人员、流水详情统一复用同一模型归属信息,不新增兼容接口、不改动统计口径。
**Tech Stack:** Java 21, Spring Boot 3, MyBatis XML, Maven, JUnit 5, Mockito
---
### Task 1: 先锁定后端标签模型字段契约
**Files:**
- Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java`
- Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerTest.java`
- Modify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperRiskModelPeopleTest.java`
- [ ] **Step 1: Write the failing test**
补充断言,锁定以下预期:
- 风险模型人员列表返回的 `hitTagList` 项包含 `modelCode`
- 风险人员总览返回的核心异常点标签项包含模型字段
- Mapper XML 的标签查询明确从 JSON 中提取 `modelCode``modelName`
- [ ] **Step 2: Run test to verify it fails**
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiProjectOverviewServiceImplTest,CcdiProjectOverviewControllerTest,CcdiProjectOverviewMapperRiskModelPeopleTest
```
Expected:
- `FAIL`
- 原因是当前标签 VO 与 SQL 尚未暴露模型字段
- [ ] **Step 3: Commit the test expectation update**
```bash
git add ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerTest.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperRiskModelPeopleTest.java
git commit -m "锁定结果总览标签模型字段测试"
```
### Task 2: 扩展标签 VO 与 Mapper 提取字段
**Files:**
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectRiskHitTagVO.java`
- Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiBankStatementHitTagVO.java`
- Modify: `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml`
- [ ] **Step 1: Extend the tag VOs**
为两个标签 VO 均新增:
- `private String modelCode;`
- `private String modelName;`
不要新增无关字段,也不要调整现有字段命名。
- [ ] **Step 2: Update tag SQL extraction**
`CcdiProjectOverviewMapper.xml` 中:
- `selectRiskHitTagsByScope` 增加 `model_code``model_name`
- 风险人员总览核心异常点对应查询同步返回模型字段
- 保持现有按 `selectedModelCodes` 过滤逻辑不变
- [ ] **Step 3: Run focused backend tests**
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiProjectOverviewServiceImplTest,CcdiProjectOverviewMapperRiskModelPeopleTest
```
Expected:
- `PASS`
- [ ] **Step 4: Review interface boundary**
人工确认:
- 未新增接口路径
- 未修改查询入参
- 未变更风险等级与人数统计逻辑
- [ ] **Step 5: Commit**
```bash
git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectRiskHitTagVO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiBankStatementHitTagVO.java ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml
git commit -m "补充结果总览标签模型字段"
```
### Task 3: 回归验证后端结果总览链路
**Files:**
- Verify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerTest.java`
- Verify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperSqlTest.java`
- Verify: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiProjectOverviewServiceStructureTest.java`
- [ ] **Step 1: Run backend regression checks**
Run:
```bash
mvn test -pl ccdi-project -Dtest=CcdiProjectOverviewControllerTest,CcdiProjectOverviewMapperSqlTest,CcdiProjectOverviewServiceStructureTest
```
Expected:
- `PASS`
- 证明结果总览接口边界、SQL 结构和服务接口未被破坏
- [ ] **Step 2: Commit**
```bash
git add ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerTest.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperSqlTest.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiProjectOverviewServiceStructureTest.java
git commit -m "回归验证结果总览标签模型字段后端改动"
```

View File

@@ -0,0 +1,132 @@
# Results Overview Hit Tag Model Color 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:** 在项目总览页中将已选模型对应的异常标签显示为红色,未选中的标签保持无色,并让核心异常点与异常标签列遵循同一颜色规则。
**Architecture:** 继续沿用结果总览现有组件拆分,不新增页面层级。前端只基于后端返回的 `modelCode/modelName` 和当前 `selectedModelCodes` 计算标签视觉状态;当没有选中模型时,标签默认无色,避免误导。风险人员总览、命中模型涉及人员、流水详情共用同一颜色映射工具方法。
**Tech Stack:** Vue 2, Element UI, Node.js
---
### Task 1: 先锁定前端颜色规则测试
**Files:**
- Modify: `ruoyi-ui/tests/unit/preliminary-check-risk-people-hit-tags.test.js`
- Modify: `ruoyi-ui/tests/unit/preliminary-check-model-table-columns.test.js`
- Modify: `ruoyi-ui/tests/unit/detail-query-hit-tags-list.test.js`
- [ ] **Step 1: Write the failing test**
补充静态断言,锁定以下预期:
- 标签渲染逻辑读取 `tag.modelCode`
- 命中模型涉及人员标签根据选中模型切换 `danger` / `info`
- 流水详情和列表异常标签共用同一颜色判断方法
- [ ] **Step 2: Run test to verify it fails**
Run:
```bash
cd ruoyi-ui
node tests/unit/preliminary-check-risk-people-hit-tags.test.js
node tests/unit/preliminary-check-model-table-columns.test.js
node tests/unit/detail-query-hit-tags-list.test.js
```
Expected:
- `FAIL`
- 原因是当前前端仍按 `riskLevel` 映射颜色
- [ ] **Step 3: Commit the test expectation update**
```bash
git add ruoyi-ui/tests/unit/preliminary-check-risk-people-hit-tags.test.js ruoyi-ui/tests/unit/preliminary-check-model-table-columns.test.js ruoyi-ui/tests/unit/detail-query-hit-tags-list.test.js
git commit -m "锁定结果总览标签模型颜色规则"
```
### Task 2: 实现标签模型颜色映射
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/RiskPeopleSection.vue`
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/RiskModelSection.vue`
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue`
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/preliminaryCheck.mock.js`
- [ ] **Step 1: Normalize incoming tag model fields**
在风险人员总览、命中模型人员、流水详情中统一兼容:
- `tag.modelCode`
- `tag.modelName`
mock 数据同步补齐模型字段,避免本地演示与真实接口脱节。
- [ ] **Step 2: Implement color rule**
颜色规则固定为:
- 当前标签 `modelCode` 在已选模型集合中时,标签 `type="danger"`
- 当前标签不在已选模型集合中时,标签 `type="info"`
- 风险人员总览没有模型筛选器时,默认按“未选中”展示为无色
不要继续使用 `riskLevel` 做颜色分流。
- [ ] **Step 3: Run focused frontend tests**
Run:
```bash
cd ruoyi-ui
node tests/unit/preliminary-check-risk-people-hit-tags.test.js
node tests/unit/preliminary-check-model-table-columns.test.js
node tests/unit/detail-query-hit-tags-list.test.js
node tests/unit/preliminary-check-summary-and-people.test.js
```
Expected:
- `PASS`
- [ ] **Step 4: Commit**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/RiskPeopleSection.vue ruoyi-ui/src/views/ccdiProject/components/detail/RiskModelSection.vue ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue ruoyi-ui/src/views/ccdiProject/components/detail/preliminaryCheck.mock.js
git commit -m "调整结果总览标签模型颜色展示"
```
### Task 3: 做结果总览前端回归检查
**Files:**
- Verify: `ruoyi-ui/tests/unit/preliminary-check-model-linkage-flow.test.js`
- Verify: `ruoyi-ui/tests/unit/preliminary-check-summary-and-people.test.js`
- Verify: `ruoyi-ui/tests/unit/project-overview-api.test.js`
- [ ] **Step 1: Run final regression checks**
Run:
```bash
cd ruoyi-ui
node tests/unit/preliminary-check-model-linkage-flow.test.js
node tests/unit/preliminary-check-summary-and-people.test.js
node tests/unit/preliminary-check-risk-people-hit-tags.test.js
node tests/unit/preliminary-check-model-table-columns.test.js
node tests/unit/detail-query-hit-tags-list.test.js
node tests/unit/project-overview-api.test.js
```
Expected:
- `PASS`
- 证明标签联动、页面静态结构和 API 调用都保持稳定
- [ ] **Step 2: Commit**
```bash
git add ruoyi-ui/tests/unit/preliminary-check-model-linkage-flow.test.js ruoyi-ui/tests/unit/preliminary-check-summary-and-people.test.js ruoyi-ui/tests/unit/project-overview-api.test.js
git commit -m "回归验证结果总览标签颜色前端改动"
```

View File

@@ -0,0 +1,137 @@
# Risk People Core Tag Fixed Palette 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:** 将项目总览页“风险人员总览”中的“核心异常点”改为按当前 10 个固定模型编码绑定颜色展示。
**Architecture:** 仅修改 `RiskPeopleSection.vue` 的核心异常点标签渲染逻辑,不改风险模型区、项目详情页或后端接口。前端基于后端已有的 `riskPointTagList[].modelCode` 维护一份固定的 10 模型色板映射;未映射到颜色的模型回退为无色,避免错绑。
**Tech Stack:** Vue 2, Element UI, Node.js
---
### Task 1: 先锁定固定 10 模型色板测试
**Files:**
- Modify: `ruoyi-ui/tests/unit/preliminary-check-risk-people-hit-tags.test.js`
- [ ] **Step 1: Write the failing test**
补充静态断言,锁定以下预期:
- `RiskPeopleSection.vue` 内存在固定模型颜色映射常量
- 色板至少覆盖当前 10 个固定模型编码
- 核心异常点标签按 `modelCode` 读取固定颜色,不再依赖 `selectedModelCodes`
- [ ] **Step 2: Run test to verify it fails**
Run:
```bash
cd ruoyi-ui
node tests/unit/preliminary-check-risk-people-hit-tags.test.js
```
Expected:
- `FAIL`
- 原因是当前实现仍依赖 `selectedModelCodes` 做颜色判断
- [ ] **Step 3: Commit the failing test**
```bash
git add ruoyi-ui/tests/unit/preliminary-check-risk-people-hit-tags.test.js
git commit -m "锁定风险人员核心异常点固定色板测试"
```
### Task 2: 实现风险人员总览固定模型色板
**Files:**
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/RiskPeopleSection.vue`
- Modify: `ruoyi-ui/src/views/ccdiProject/components/detail/preliminaryCheck.mock.js`
- [ ] **Step 1: Add fixed palette mapping**
`RiskPeopleSection.vue` 中新增固定 `modelCode -> color` 映射,覆盖以下 10 个模型编码:
- `LARGE_TRANSACTION`
- `ABNORMAL_TRANSACTION`
- `SUSPICIOUS_GAMBLING`
- `SUSPICIOUS_RELATION`
- `SUSPICIOUS_PART_TIME`
- `SUSPICIOUS_PROPERTY`
- `SUSPICIOUS_FOREIGN_EXCHANGE`
- `SUSPICIOUS_INTEREST_PAYMENT`
- `SUSPICIOUS_PURCHASE`
- `ABNORMAL_BEHAVIOR`
- [ ] **Step 2: Render tags by fixed palette**
核心异常点标签改为根据 `tag.modelCode` 直接命中固定色板,移除对 `selectedModelCodes` 的依赖。优先使用 Element 内置 `type`,必要时用样式类补足 10 色。
- [ ] **Step 3: Align mock data**
更新 `preliminaryCheck.mock.js` 中的示例 `modelCode`,与真实固定模型编码保持一致,避免 mock 和生产映射脱节。
- [ ] **Step 4: Run focused frontend tests**
Run:
```bash
cd ruoyi-ui
node tests/unit/preliminary-check-risk-people-hit-tags.test.js
node tests/unit/preliminary-check-risk-people-binding.test.js
node tests/unit/preliminary-check-summary-and-people.test.js
```
Expected:
- `PASS`
- [ ] **Step 5: Commit**
```bash
git add ruoyi-ui/src/views/ccdiProject/components/detail/RiskPeopleSection.vue ruoyi-ui/src/views/ccdiProject/components/detail/preliminaryCheck.mock.js
git commit -m "调整风险人员核心异常点固定模型色板"
```
### Task 3: 做结果总览相关前端回归并补文档
**Files:**
- Create: `docs/reports/implementation/2026-03-23-risk-people-core-tag-fixed-palette-implementation.md`
- Verify: `ruoyi-ui/tests/unit/project-overview-api.test.js`
- Verify: `ruoyi-ui/tests/unit/preliminary-check-api-integration.test.js`
- [ ] **Step 1: Run final regression checks**
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-states.test.js
node tests/unit/preliminary-check-summary-and-people.test.js
node tests/unit/preliminary-check-risk-people-binding.test.js
node tests/unit/preliminary-check-risk-people-hit-tags.test.js
```
Expected:
- `PASS`
- 证明结果总览入口和风险人员总览展示边界仍稳定
- [ ] **Step 2: Write implementation record**
记录:
- 仅改风险人员总览核心异常点
- 10 模型固定色板写死在前端
- 风险模型区和详情页不在本次范围
- [ ] **Step 3: Commit**
```bash
git add docs/reports/implementation/2026-03-23-risk-people-core-tag-fixed-palette-implementation.md
git commit -m "补充风险人员核心异常点固定色板实施记录"
```

View File

@@ -0,0 +1,44 @@
# 项目总览标签按模型着色实施记录
## 本次改动
- 后端扩展结果总览标签对象 [`CcdiProjectRiskHitTagVO`](/Users/wkc/Desktop/ccdi/ccdi/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectRiskHitTagVO),新增 `modelCode``modelName` 字段,供前端按模型识别标签归属。
- 后端扩展风险人员总览项 [`CcdiProjectRiskPeopleOverviewItemVO`](/Users/wkc/Desktop/ccdi/ccdi/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectRiskPeopleOverviewItemVO),新增 `riskPointTagList`,让“核心异常点”不再只依赖纯文本拆分。
- 调整结果总览 Mapper 与服务:
- [`CcdiProjectOverviewMapper.xml`](/Users/wkc/Desktop/ccdi/ccdi/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml) 的标签查询改为返回 `model_code``model_name`,并按 `modelCode + ruleCode` 分组,避免跨模型标签被合并。
- [`CcdiProjectOverviewMapper.java`](/Users/wkc/Desktop/ccdi/ccdi/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapper.java) 新增 `selectRiskHitTagsByScope` 方法。
- [`CcdiProjectOverviewServiceImpl.java`](/Users/wkc/Desktop/ccdi/ccdi/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java) 在风险人员总览映射时补充 `riskPointTagList`
- 调整结果总览前端联动:
- [`PreliminaryCheck.vue`](/Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui/src/views/ccdiProject/components/detail/PreliminaryCheck.vue) 维护共享的 `selectedModelCodes`,并把模型区选中状态同步给风险人员区。
- [`RiskModelSection.vue`](/Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui/src/views/ccdiProject/components/detail/RiskModelSection.vue) 的“异常标签”列改为按模型判断颜色:已选模型标签为红色,未选中标签保持无色。
- [`RiskPeopleSection.vue`](/Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui/src/views/ccdiProject/components/detail/RiskPeopleSection.vue) 的“核心异常点”标签改为按共享选中模型着色,未选中时保持无色。
- [`preliminaryCheck.mock.js`](/Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui/src/views/ccdiProject/components/detail/preliminaryCheck.mock.js) 补齐标签示例数据中的 `modelCode``modelName`
## 实现说明
- 选中模型只影响标签颜色,不再裁掉同一人员的其他模型标签,保证“选中红色、未选中无色”的对比能真实展示。
- 风险模型人员列表的行筛选逻辑保持不变,仍由现有 `selectedModelCodes + matchMode` 控制。
- 当项目切换为空、重新加载或加载失败时,前端会主动清空共享的模型选中状态,避免旧状态污染新页面。
## 验证情况
### 后端
- `mvn test -pl ccdi-project -Dtest=CcdiProjectOverviewServiceImplTest,CcdiProjectOverviewControllerTest,CcdiProjectOverviewMapperRiskModelPeopleTest`
### 前端
- `node ruoyi-ui/tests/unit/project-overview-api.test.js`
- `node ruoyi-ui/tests/unit/preliminary-check-api-integration.test.js`
- `node ruoyi-ui/tests/unit/preliminary-check-states.test.js`
- `node ruoyi-ui/tests/unit/preliminary-check-summary-and-people.test.js`
- `node ruoyi-ui/tests/unit/preliminary-check-risk-people-binding.test.js`
- `node ruoyi-ui/tests/unit/preliminary-check-risk-people-hit-tags.test.js`
- `node ruoyi-ui/tests/unit/preliminary-check-model-linkage-flow.test.js`
- `node ruoyi-ui/tests/unit/preliminary-check-model-multiselect.test.js`
- `node ruoyi-ui/tests/unit/preliminary-check-model-table-columns.test.js`
## 未包含内容
- 未改动项目详情页 [`DetailQuery.vue`](/Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui/src/views/ccdiProject/components/detail/DetailQuery.vue) 的异常标签展示规则。
- 未新增新的模型颜色体系,当前仅按“已选模型红色、未选模型无色”执行。

View File

@@ -0,0 +1,37 @@
# 风险人员总览核心异常点固定模型色板实施记录
## 本次改动
- 调整 [`RiskPeopleSection.vue`](/Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui/src/views/ccdiProject/components/detail/RiskPeopleSection.vue),将“核心异常点”标签从“按已选模型高亮”改为“按固定 10 模型编码绑定颜色”。
- 在组件内新增固定色板 `CORE_TAG_PALETTE`,覆盖以下 10 个模型编码:
- `LARGE_TRANSACTION`
- `ABNORMAL_TRANSACTION`
- `SUSPICIOUS_GAMBLING`
- `SUSPICIOUS_RELATION`
- `SUSPICIOUS_PART_TIME`
- `SUSPICIOUS_PROPERTY`
- `SUSPICIOUS_FOREIGN_EXCHANGE`
- `SUSPICIOUS_INTEREST_PAYMENT`
- `SUSPICIOUS_PURCHASE`
- `ABNORMAL_BEHAVIOR`
- 新增 `core-tag--*` 样式类,按你确认的 `A` 方案落为商务稳重风格。
- 调整 [`preliminaryCheck.mock.js`](/Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui/src/views/ccdiProject/components/detail/preliminaryCheck.mock.js),将示例标签的 `modelCode/modelName` 改成真实模型编码口径。
- 更新 [`preliminary-check-risk-people-hit-tags.test.js`](/Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui/tests/unit/preliminary-check-risk-people-hit-tags.test.js),锁定固定 10 模型色板实现,不再允许该组件依赖 `selectedModelCodes`
## 实现说明
- 本次只改“风险人员总览”的“核心异常点”,不改“命中模型涉及人员”的异常标签,也不改项目详情页。
- 标签颜色直接由 `riskPointTagList[].modelCode` 命中固定色板决定,不再依赖模型区卡片是否被选中。
- 若后续标签返回的 `modelCode` 不在固定映射内,组件会回退为无色,避免错误着色。
## 验证情况
- `node ruoyi-ui/tests/unit/preliminary-check-risk-people-hit-tags.test.js`
- `node ruoyi-ui/tests/unit/preliminary-check-risk-people-binding.test.js`
- `node ruoyi-ui/tests/unit/preliminary-check-summary-and-people.test.js`
## 未包含内容
- 未调整 [`RiskModelSection.vue`](/Users/wkc/Desktop/ccdi/ccdi/ruoyi-ui/src/views/ccdiProject/components/detail/RiskModelSection.vue) 的标签颜色规则
- 未新增自动分配颜色逻辑
- 未改动后端接口或模型卡片展示逻辑

View File

@@ -14,8 +14,14 @@
<div v-else class="preliminary-check-page">
<overview-stats :summary="currentData.summary" />
<risk-people-section :section-data="currentData.riskPeople" />
<risk-model-section :section-data="currentData.riskModels" />
<risk-people-section
:section-data="currentData.riskPeople"
:selected-model-codes="selectedModelCodes"
/>
<risk-model-section
:section-data="currentData.riskModels"
@selection-change="handleRiskModelSelectionChange"
/>
<risk-detail-section :section-data="currentData.riskDetails" />
</div>
</div>
@@ -62,6 +68,7 @@ export default {
data() {
return {
pageState: "loading",
selectedModelCodes: [],
mockData: mockOverviewData,
stateDataMap: mockOverviewStateData,
realData: mockOverviewData,
@@ -83,6 +90,7 @@ export default {
}
this.realData = this.stateDataMap.empty;
this.pageState = "empty";
this.selectedModelCodes = [];
},
},
created() {
@@ -94,14 +102,19 @@ export default {
this.pageState = "empty";
},
methods: {
handleRiskModelSelectionChange(modelCodes) {
this.selectedModelCodes = Array.isArray(modelCodes) ? [...modelCodes] : [];
},
async loadOverviewData() {
if (!this.projectId) {
this.realData = this.stateDataMap.empty;
this.pageState = "empty";
this.selectedModelCodes = [];
return;
}
this.pageState = "loading";
this.selectedModelCodes = [];
try {
const [dashboardRes, riskPeopleRes, riskModelCardsRes] = await Promise.all([
getOverviewDashboard(this.projectId),
@@ -129,6 +142,7 @@ export default {
} catch (error) {
this.realData = this.stateDataMap.empty;
this.pageState = "empty";
this.selectedModelCodes = [];
console.error("加载结果总览失败", error);
}
},

View File

@@ -113,7 +113,7 @@
:key="`${scope.row.staffCode || scope.row.idNo || index}-tag-${index}`"
size="mini"
effect="plain"
:type="mapRiskLevelToTagType(tag.riskLevel)"
:type="resolveModelTagType(tag)"
>
{{ tag.ruleName }}
</el-tag>
@@ -232,6 +232,8 @@ export default {
projectId: {
immediate: true,
handler() {
this.selectedModelCodes = [];
this.$emit("selection-change", this.selectedModelCodes);
this.pageNum = 1;
this.fetchPeopleList();
},
@@ -250,6 +252,7 @@ export default {
} else {
this.selectedModelCodes = [...this.selectedModelCodes, modelCode];
}
this.$emit("selection-change", this.selectedModelCodes);
this.pageNum = 1;
this.fetchPeopleList({ syncCardLoading: true });
},
@@ -263,6 +266,7 @@ export default {
},
resetQuery() {
this.selectedModelCodes = [];
this.$emit("selection-change", this.selectedModelCodes);
this.matchMode = "ANY";
this.keyword = "";
this.deptId = undefined;
@@ -290,15 +294,14 @@ export default {
}
return modelNames.join("、");
},
mapRiskLevelToTagType(riskLevel) {
const level = String(riskLevel || "").toUpperCase();
if (level === "HIGH") {
resolveModelTagType(tag) {
if (!this.selectedModelCodes.length) {
return "";
}
if (this.selectedModelCodes.includes(tag.modelCode)) {
return "danger";
}
if (level === "MEDIUM") {
return "warning";
}
return "info";
return "";
},
async loadDeptOptions() {
this.deptLoading = true;

View File

@@ -33,9 +33,10 @@
<el-tag
v-for="(tag, index) in scope.row.riskPointTagList"
:key="`${scope.row.idNo || scope.row.name || index}-risk-point-${index}`"
class="core-risk-tag"
:style="resolveModelTagStyle(tag)"
size="mini"
effect="plain"
:type="mapRiskLevelToTagType(tag.riskLevel)"
>
{{ tag.ruleName }}
</el-tag>
@@ -55,6 +56,59 @@
</template>
<script>
const CORE_TAG_PALETTE = {
LARGE_TRANSACTION: {
color: "#b91c1c",
borderColor: "#fca5a5",
backgroundColor: "#fff1f2",
},
ABNORMAL_TRANSACTION: {
color: "#c2410c",
borderColor: "#fdba74",
backgroundColor: "#fff7ed",
},
SUSPICIOUS_GAMBLING: {
color: "#a16207",
borderColor: "#fcd34d",
backgroundColor: "#fefce8",
},
SUSPICIOUS_RELATION: {
color: "#166534",
borderColor: "#86efac",
backgroundColor: "#f0fdf4",
},
SUSPICIOUS_PART_TIME: {
color: "#0f766e",
borderColor: "#6ee7b7",
backgroundColor: "#ecfeff",
},
SUSPICIOUS_PROPERTY: {
color: "#1d4ed8",
borderColor: "#93c5fd",
backgroundColor: "#eff6ff",
},
SUSPICIOUS_FOREIGN_EXCHANGE: {
color: "#4338ca",
borderColor: "#a5b4fc",
backgroundColor: "#eef2ff",
},
SUSPICIOUS_INTEREST_PAYMENT: {
color: "#7e22ce",
borderColor: "#d8b4fe",
backgroundColor: "#faf5ff",
},
SUSPICIOUS_PURCHASE: {
color: "#be185d",
borderColor: "#f9a8d4",
backgroundColor: "#fdf2f8",
},
ABNORMAL_BEHAVIOR: {
color: "#334155",
borderColor: "#cbd5e1",
backgroundColor: "#f8fafc",
},
};
function normalizeRiskPointTags(tags, riskPoint, riskLevel) {
if (Array.isArray(tags) && tags.length) {
return tags
@@ -69,6 +123,8 @@ function normalizeRiskPointTags(tags, riskPoint, riskLevel) {
return {
ruleName: item.ruleName || item.label || item.name || "",
riskLevel: item.riskLevel || riskLevel,
modelCode: item.modelCode || "",
modelName: item.modelName || "",
};
}
return null;
@@ -115,15 +171,8 @@ export default {
},
},
methods: {
mapRiskLevelToTagType(riskLevel) {
const level = String(riskLevel || "").toUpperCase();
if (level === "HIGH" || level === "DANGER") {
return "danger";
}
if (level === "MEDIUM" || level === "WARNING") {
return "warning";
}
return "info";
resolveModelTagStyle(tag) {
return CORE_TAG_PALETTE[tag.modelCode] || {};
},
},
};
@@ -176,6 +225,11 @@ export default {
gap: 6px;
}
:deep(.core-risk-tag) {
border-radius: 999px;
font-weight: 500;
}
.empty-text {
color: #94a3b8;
}

View File

@@ -23,8 +23,8 @@ export const mockOverviewData = {
modelCount: 3,
riskPoint: "跨地域转账频繁交易",
riskPointTagList: [
{ ruleName: "跨地域转账", riskLevel: "HIGH" },
{ ruleName: "频繁交易", riskLevel: "HIGH" },
{ modelCode: "LARGE_TRANSACTION", modelName: "大额交易", ruleName: "跨地域转账", riskLevel: "HIGH" },
{ modelCode: "ABNORMAL_TRANSACTION", modelName: "异常交易", ruleName: "频繁交易", riskLevel: "HIGH" },
],
actionLabel: "查看详情",
},
@@ -38,8 +38,8 @@ export const mockOverviewData = {
modelCount: 2,
riskPoint: "多工资转入频繁交易",
riskPointTagList: [
{ ruleName: "多工资转入", riskLevel: "MEDIUM" },
{ ruleName: "频繁交易", riskLevel: "MEDIUM" },
{ modelCode: "SUSPICIOUS_PART_TIME", modelName: "可疑兼职", ruleName: "多工资转入", riskLevel: "MEDIUM" },
{ modelCode: "ABNORMAL_BEHAVIOR", modelName: "异常行为", ruleName: "频繁交易", riskLevel: "MEDIUM" },
],
actionLabel: "查看详情",
},
@@ -53,7 +53,7 @@ export const mockOverviewData = {
modelCount: 1,
riskPoint: "频繁小额转账",
riskPointTagList: [
{ ruleName: "频繁小额转账", riskLevel: "LOW" },
{ modelCode: "SUSPICIOUS_GAMBLING", modelName: "疑似赌博", ruleName: "频繁小额转账", riskLevel: "LOW" },
],
actionLabel: "查看详情",
},

View File

@@ -16,6 +16,9 @@ const source = fs.readFileSync(
"异常标签",
"hitTagList",
"ruleName",
"tag.modelCode",
'this.$emit("selection-change", this.selectedModelCodes)',
':type="resolveModelTagType(tag)"',
].forEach((token) => assert(source.includes(token), token));
[

View File

@@ -11,7 +11,23 @@ const source = fs.readFileSync(
"risk-point-tag-list",
"scope.row.riskPointTagList",
"normalizeRiskPointTags",
":type=\"mapRiskLevelToTagType(tag.riskLevel)\"",
"tag.modelCode",
"CORE_TAG_PALETTE",
"backgroundColor",
"borderColor",
"LARGE_TRANSACTION",
"ABNORMAL_TRANSACTION",
"SUSPICIOUS_GAMBLING",
"SUSPICIOUS_RELATION",
"SUSPICIOUS_PART_TIME",
"SUSPICIOUS_PROPERTY",
"SUSPICIOUS_FOREIGN_EXCHANGE",
"SUSPICIOUS_INTEREST_PAYMENT",
"SUSPICIOUS_PURCHASE",
"ABNORMAL_BEHAVIOR",
":style=\"resolveModelTagStyle(tag)\"",
].forEach((token) => assert(source.includes(token), token));
assert(!source.includes("selectedModelCodes"), "核心异常点颜色不应再依赖已选模型");
assert(!source.includes(":class=\"resolveModelTagClass(tag)\""), "不应继续依赖 class 覆盖颜色");
assert(!source.includes('<el-table-column prop="riskPoint" label="核心异常点" min-width="220" />'));

View File

@@ -38,6 +38,11 @@ const mockSource = fs.readFileSync(
["currentData.summary", "currentData.riskPeople"].forEach((token) =>
assert(entry.includes(token), token)
);
[
':selected-model-codes="selectedModelCodes"',
'@selection-change="handleRiskModelSelectionChange"',
"selectedModelCodes: []",
].forEach((token) => assert(entry.includes(token), token));
["风险人员总览", "风险等级", "命中模型数", "查看详情"].forEach((token) =>
assert(people.includes(token), token)
);