修复风险总览无风险人员负数问题
This commit is contained in:
@@ -36,9 +36,10 @@ public interface CcdiProjectMapper extends BaseMapper<CcdiProject> {
|
||||
List<CcdiProjectHistoryListItemVO> 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<CcdiProject> {
|
||||
* @return 更新行数
|
||||
*/
|
||||
int updateRiskCountsByProjectId(@Param("projectId") Long projectId,
|
||||
@Param("targetCount") Integer targetCount,
|
||||
@Param("highRiskCount") Integer highRiskCount,
|
||||
@Param("mediumRiskCount") Integer mediumRiskCount,
|
||||
@Param("lowRiskCount") Integer lowRiskCount,
|
||||
|
||||
@@ -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<String, Object> 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());
|
||||
|
||||
@@ -117,12 +117,37 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</resultMap>
|
||||
|
||||
<select id="countMatchedStaffCountByProjectId" resultType="java.lang.Integer">
|
||||
select count(distinct trim(bs.cret_no))
|
||||
from ccdi_bank_statement bs
|
||||
inner join ccdi_base_staff staff on staff.id_card = bs.cret_no
|
||||
where bs.project_id = #{projectId}
|
||||
and bs.cret_no is not null
|
||||
and trim(bs.cret_no) != ''
|
||||
select count(distinct scope_staff.id_card)
|
||||
from (
|
||||
select staff.id_card
|
||||
from ccdi_bank_statement bs
|
||||
inner join ccdi_base_staff staff on staff.id_card = trim(bs.cret_no)
|
||||
where bs.project_id = #{projectId}
|
||||
and bs.cret_no is not null
|
||||
and trim(bs.cret_no) != ''
|
||||
union
|
||||
select 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.project_id = #{projectId}
|
||||
and bs.cret_no is not null
|
||||
and trim(bs.cret_no) != ''
|
||||
union
|
||||
select 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.project_id = #{projectId}
|
||||
and bs.LE_ACCOUNT_NO is not null
|
||||
and trim(bs.LE_ACCOUNT_NO) != ''
|
||||
) scope_staff
|
||||
</select>
|
||||
|
||||
<sql id="parsedTrxDateExpr">
|
||||
|
||||
@@ -70,7 +70,8 @@
|
||||
|
||||
<update id="updateRiskCountsByProjectId">
|
||||
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},
|
||||
|
||||
@@ -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 = "<select id=\"" + selectId + "\"";
|
||||
int startIndex = xml.indexOf(start);
|
||||
assertTrue(startIndex >= 0, "missing select: " + selectId);
|
||||
int endIndex = xml.indexOf("</select>", startIndex);
|
||||
assertTrue(endIndex >= 0, "missing closing select tag: " + selectId);
|
||||
return xml.substring(startIndex, endIndex);
|
||||
}
|
||||
|
||||
private void registerTypeAliases(TypeAliasRegistry typeAliasRegistry) {
|
||||
typeAliasRegistry.registerAlias("map", Map.class);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`,约束刷新风险人数时同步写回总人数。
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user