From a39594faf860263bab5ab53a65c274490f16f36f Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Tue, 26 May 2026 16:55:53 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=A3=8E=E9=99=A9=E6=80=BB?= =?UTF-8?q?=E8=A7=88=E6=97=A0=E9=A3=8E=E9=99=A9=E4=BA=BA=E5=91=98=E8=B4=9F?= =?UTF-8?q?=E6=95=B0=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/mapper/CcdiProjectMapper.java | 4 +- .../impl/CcdiProjectOverviewServiceImpl.java | 10 ++++ .../ccdi/project/CcdiBankStatementMapper.xml | 37 +++++++++++--- .../mapper/ccdi/project/CcdiProjectMapper.xml | 3 +- .../CcdiBankStatementMapperXmlTest.java | 25 ++++++++++ .../CcdiProjectOverviewServiceImplTest.java | 10 +++- ...5-22-risk-overview-negative-no-risk-fix.md | 38 ++++++++++++++ ...ix-project-target-count-employee-scope.sql | 50 +++++++++++++++++++ 8 files changed, 167 insertions(+), 10 deletions(-) create mode 100644 docs/reports/implementation/2026-05-22-risk-overview-negative-no-risk-fix.md create mode 100644 sql/migration/2026-05-22-fix-project-target-count-employee-scope.sql diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectMapper.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectMapper.java index 6d35186d..333708ce 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectMapper.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectMapper.java @@ -36,9 +36,10 @@ public interface CcdiProjectMapper extends BaseMapper { List selectHistoryProjects(@Param("queryDTO") CcdiProjectQueryDTO queryDTO); /** - * 更新项目风险人数 + * 更新项目总人数与风险人数 * * @param projectId 项目ID + * @param targetCount 总人数 * @param highRiskCount 高风险人数 * @param mediumRiskCount 中风险人数 * @param lowRiskCount 低风险人数 @@ -46,6 +47,7 @@ public interface CcdiProjectMapper extends BaseMapper { * @return 更新行数 */ int updateRiskCountsByProjectId(@Param("projectId") Long projectId, + @Param("targetCount") Integer targetCount, @Param("highRiskCount") Integer highRiskCount, @Param("mediumRiskCount") Integer mediumRiskCount, @Param("lowRiskCount") Integer lowRiskCount, diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java index cdc1bf2f..fa398c2a 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java @@ -41,6 +41,7 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleItemVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO; import com.ruoyi.ccdi.project.mapper.CcdiModelParamMapper; +import com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper; import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper; import com.ruoyi.ccdi.project.mapper.CcdiBankTagResultMapper; import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewEmployeeResultMapper; @@ -74,6 +75,9 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi @Resource private CcdiProjectMapper projectMapper; + @Resource + private CcdiBankStatementMapper bankStatementMapper; + @Resource private CcdiModelParamMapper modelParamMapper; @@ -371,6 +375,7 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi projectMapper.updateRiskCountsByProjectId( projectId, + countProjectScopeStaff(projectId), countRiskLevel(results, "HIGH"), countRiskLevel(results, "MEDIUM"), countRiskLevel(results, "LOW"), @@ -385,6 +390,7 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi Map summary = overviewMapper.selectRiskCountSummaryByProjectId(projectId); projectMapper.updateRiskCountsByProjectId( projectId, + countProjectScopeStaff(projectId), readCount(summary, "highRiskCount"), readCount(summary, "mediumRiskCount"), readCount(summary, "lowRiskCount"), @@ -392,6 +398,10 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi ); } + private int countProjectScopeStaff(Long projectId) { + return defaultZero(bankStatementMapper.countMatchedStaffCountByProjectId(projectId)); + } + private CcdiProjectRiskPeopleOverviewItemVO buildRiskPeopleItem(Long projectId, CcdiProjectEmployeeRiskAggregateVO aggregate) { CcdiProjectRiskPeopleOverviewItemVO item = new CcdiProjectRiskPeopleOverviewItemVO(); item.setName(aggregate.getStaffName()); diff --git a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml index 2a273c36..da9c5276 100644 --- a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml +++ b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml @@ -117,12 +117,37 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" diff --git a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectMapper.xml b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectMapper.xml index fa2c7d37..d76061d2 100644 --- a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectMapper.xml +++ b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectMapper.xml @@ -70,7 +70,8 @@ update ccdi_project - set high_risk_count = #{highRiskCount}, + set target_count = #{targetCount}, + high_risk_count = #{highRiskCount}, medium_risk_count = #{mediumRiskCount}, low_risk_count = #{lowRiskCount}, update_by = #{updateBy}, diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapperXmlTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapperXmlTest.java index 000babd5..3c64a4ef 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapperXmlTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankStatementMapperXmlTest.java @@ -220,6 +220,22 @@ class CcdiBankStatementMapperXmlTest { } } + @Test + void targetCount_shouldUseResolvedProjectEmployeeScope() throws Exception { + try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) { + String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + String selectSql = extractSelect(xml, "countMatchedStaffCountByProjectId"); + + assertTrue(selectSql.contains("select count(distinct scope_staff.id_card)"), selectSql); + assertTrue(selectSql.contains("inner join ccdi_base_staff staff on staff.id_card = trim(bs.cret_no)"), selectSql); + assertTrue(selectSql.contains("inner join ccdi_staff_fmy_relation relation"), selectSql); + assertTrue(selectSql.contains("family_staff.id_card = relation.person_id"), selectSql); + assertTrue(selectSql.contains("inner join ccdi_account_info account"), selectSql); + assertTrue(selectSql.contains("account.owner_type = 'EMPLOYEE'"), selectSql); + assertTrue(selectSql.contains("account_staff.id_card = account.owner_id"), selectSql); + } + } + private MappedStatement loadMappedStatement(String statementId) throws Exception { Configuration configuration = new Configuration(); configuration.setEnvironment(new Environment("test", new JdbcTransactionFactory(), new NoOpDataSource())); @@ -242,6 +258,15 @@ class CcdiBankStatementMapperXmlTest { return boundSql.getSql().replaceAll("\\s+", " ").trim(); } + private String extractSelect(String xml, String selectId) { + String start = "", startIndex); + assertTrue(endIndex >= 0, "missing closing select tag: " + selectId); + return xml.substring(startIndex, endIndex); + } + private void registerTypeAliases(TypeAliasRegistry typeAliasRegistry) { typeAliasRegistry.registerAlias("map", Map.class); } diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java index 50712242..98f319ef 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java @@ -29,6 +29,7 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO; +import com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper; import com.ruoyi.ccdi.project.mapper.CcdiModelParamMapper; import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper; import com.ruoyi.ccdi.project.mapper.CcdiBankTagResultMapper; @@ -69,6 +70,9 @@ class CcdiProjectOverviewServiceImplTest { @Mock private CcdiProjectMapper projectMapper; + @Mock + private CcdiBankStatementMapper bankStatementMapper; + @Mock private CcdiModelParamMapper modelParamMapper; @@ -427,10 +431,11 @@ class CcdiProjectOverviewServiceImplTest { summary.put("mediumRiskCount", new BigDecimal("1")); summary.put("lowRiskCount", new BigDecimal("3")); when(overviewMapper.selectRiskCountSummaryByProjectId(43L)).thenReturn(summary); + when(bankStatementMapper.countMatchedStaffCountByProjectId(43L)).thenReturn(6); service.refreshProjectRiskCounts(43L, "tester"); - verify(projectMapper).updateRiskCountsByProjectId(eq(43L), eq(2), eq(1), eq(3), eq("tester")); + verify(projectMapper).updateRiskCountsByProjectId(eq(43L), eq(6), eq(2), eq(1), eq(3), eq("tester")); } @Test @@ -490,6 +495,7 @@ class CcdiProjectOverviewServiceImplTest { ); when(overviewEmployeeResultMapper.selectEmployeeHitRowsByProjectId(43L)).thenReturn(hitRows); when(overviewEmployeeResultBuilder.build(43L, hitRows, "tester")).thenReturn(results); + when(bankStatementMapper.countMatchedStaffCountByProjectId(43L)).thenReturn(3); service.refreshOverviewEmployeeResults(43L, "tester"); @@ -503,7 +509,7 @@ class CcdiProjectOverviewServiceImplTest { inOrder.verify(overviewEmployeeResultMapper).selectEmployeeHitRowsByProjectId(43L); inOrder.verify(overviewEmployeeResultBuilder).build(43L, hitRows, "tester"); inOrder.verify(overviewEmployeeResultMapper).insertBatch(results); - inOrder.verify(projectMapper).updateRiskCountsByProjectId(43L, 1, 1, 1, "tester"); + inOrder.verify(projectMapper).updateRiskCountsByProjectId(43L, 3, 1, 1, 1, "tester"); } @Test diff --git a/docs/reports/implementation/2026-05-22-risk-overview-negative-no-risk-fix.md b/docs/reports/implementation/2026-05-22-risk-overview-negative-no-risk-fix.md new file mode 100644 index 00000000..2c2f78b7 --- /dev/null +++ b/docs/reports/implementation/2026-05-22-risk-overview-negative-no-risk-fix.md @@ -0,0 +1,38 @@ +# 风险总览无风险人员负数修复记录 + +## 问题现象 + +- 生产项目结果总览出现“无风险人员”为负数。 +- 页面展示中“高风险 + 中风险 + 低风险”人数大于“总人数”。 + +## 根因 + +- `ccdi_project.target_count` 原口径只统计 `ccdi_bank_statement.cret_no` 直接匹配到员工主数据的去重人数。 +- 风险人员结果表 `ccdi_project_overview_employee_result` 的生成口径会把关系人流水、对象规则命中、账户归属等结果归并回员工。 +- 当项目存在“关系人流水归属员工”或“项目流水账号归属员工”时,风险人数的员工范围大于总人数范围,导致 `target_count - high - medium - low` 出现负数。 + +## 修改内容 + +- 调整 `CcdiBankStatementMapper.countMatchedStaffCountByProjectId`: + - 直接员工:流水 `cret_no` 匹配员工身份证。 + - 关系人归属员工:流水 `cret_no` 匹配员工关系人证件号后归属到员工。 + - 账号归属员工:项目流水账号匹配员工账户信息后归属到员工。 +- 调整 `CcdiProjectOverviewServiceImpl`: + - 标签重算刷新结果总览员工结果后,同步刷新项目总人数和风险人数。 + - 手动刷新项目风险人数时,同步刷新项目总人数。 +- 调整 `CcdiProjectMapper.updateRiskCountsByProjectId`: + - 更新风险人数时同时写回 `target_count`,保证总人数与风险人数使用同一项目员工范围口径。 +- 新增生产回填脚本: + - `sql/migration/2026-05-22-fix-project-target-count-employee-scope.sql` + - 用新口径回填既有项目的 `target_count`。 + +## 影响范围 + +- 后端结果总览统计。 +- 项目列表中的目标人数。 +- 生产既有项目需要执行新增 SQL 回填脚本,后续上传、历史导入、标签重算会自动按新口径刷新。 + +## 验证 + +- 已补充 `CcdiBankStatementMapperXmlTest.targetCount_shouldUseResolvedProjectEmployeeScope`,约束总人数 SQL 必须包含直接员工、关系人归属员工、账号归属员工三类范围。 +- 已更新 `CcdiProjectOverviewServiceImplTest`,约束刷新风险人数时同步写回总人数。 diff --git a/sql/migration/2026-05-22-fix-project-target-count-employee-scope.sql b/sql/migration/2026-05-22-fix-project-target-count-employee-scope.sql new file mode 100644 index 00000000..bf122686 --- /dev/null +++ b/sql/migration/2026-05-22-fix-project-target-count-employee-scope.sql @@ -0,0 +1,50 @@ +-- 修复项目总人数统计口径:直接员工、关系人流水归属员工、项目流水账号归属员工统一纳入项目员工范围。 +UPDATE ccdi_project project +LEFT JOIN ( + SELECT + scope_staff.project_id, + COUNT(DISTINCT scope_staff.id_card) AS target_count + FROM ( + SELECT + bs.project_id, + staff.id_card + FROM ccdi_bank_statement bs + INNER JOIN ccdi_base_staff staff + ON staff.id_card = TRIM(bs.cret_no) + WHERE bs.cret_no IS NOT NULL + AND TRIM(bs.cret_no) != '' + + UNION + + SELECT + bs.project_id, + family_staff.id_card + FROM ccdi_bank_statement bs + INNER JOIN ccdi_staff_fmy_relation relation + ON relation.relation_cert_no = TRIM(bs.cret_no) + AND relation.status = 1 + INNER JOIN ccdi_base_staff family_staff + ON family_staff.id_card = relation.person_id + WHERE bs.cret_no IS NOT NULL + AND TRIM(bs.cret_no) != '' + + UNION + + SELECT + bs.project_id, + account_staff.id_card + FROM ccdi_bank_statement bs + INNER JOIN ccdi_account_info account + ON TRIM(account.account_no) = TRIM(bs.LE_ACCOUNT_NO) + AND account.owner_type = 'EMPLOYEE' + INNER JOIN ccdi_base_staff account_staff + ON account_staff.id_card = account.owner_id + WHERE bs.LE_ACCOUNT_NO IS NOT NULL + AND TRIM(bs.LE_ACCOUNT_NO) != '' + ) scope_staff + GROUP BY scope_staff.project_id +) stats ON stats.project_id = project.project_id +SET project.target_count = COALESCE(stats.target_count, 0), + project.update_by = 'system', + project.update_time = NOW() +WHERE project.del_flag = '0';