实现结果总览员工结果聚合构建

This commit is contained in:
wkc
2026-03-22 11:45:26 +08:00
parent ec006f202b
commit 0a58ac3251
7 changed files with 421 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
/**
* 结果总览员工命中明细行
*/
@Data
public class CcdiProjectOverviewEmployeeHitRowVO {
private Long projectId;
private String staffIdCard;
private String staffName;
private String staffCode;
private Long deptId;
private String deptName;
private String modelCode;
private String modelName;
private String ruleCode;
private String ruleName;
private String riskLevel;
}

View File

@@ -0,0 +1,16 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
/**
* 结果总览员工模型汇总
*/
@Data
public class CcdiProjectOverviewEmployeeModelSummaryVO {
private String modelCode;
private String modelName;
private Integer warningCount;
}

View File

@@ -0,0 +1,18 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
/**
* 结果总览员工规则汇总
*/
@Data
public class CcdiProjectOverviewEmployeeRuleSummaryVO {
private String ruleCode;
private String ruleName;
private String riskLevel;
private Integer warningCount;
}

View File

@@ -2,6 +2,7 @@ package com.ruoyi.ccdi.project.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.ccdi.project.domain.entity.CcdiProjectOverviewEmployeeResult;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewEmployeeHitRowVO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@@ -34,4 +35,12 @@ public interface CcdiProjectOverviewEmployeeResultMapper extends BaseMapper<Ccdi
* @return 结果列表
*/
List<CcdiProjectOverviewEmployeeResult> selectByProjectId(@Param("projectId") Long projectId);
/**
* 按项目查询员工归并命中明细
*
* @param projectId 项目ID
* @return 命中明细
*/
List<CcdiProjectOverviewEmployeeHitRowVO> selectEmployeeHitRowsByProjectId(@Param("projectId") Long projectId);
}

View File

@@ -0,0 +1,171 @@
package com.ruoyi.ccdi.project.service.impl;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.ccdi.project.domain.entity.CcdiProjectOverviewEmployeeResult;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewEmployeeHitRowVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewEmployeeModelSummaryVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewEmployeeRuleSummaryVO;
import org.springframework.stereotype.Component;
import java.util.Comparator;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 结果总览员工结果构建器
*/
@Component
public class CcdiProjectOverviewEmployeeResultBuilder {
/**
* 按员工归并命中明细并构建结果表实体
*
* @param projectId 项目ID
* @param hitRows 命中明细
* @param operator 操作人
* @return 结果表实体列表
*/
public List<CcdiProjectOverviewEmployeeResult> build(Long projectId,
List<CcdiProjectOverviewEmployeeHitRowVO> hitRows,
String operator) {
if (hitRows == null || hitRows.isEmpty()) {
return List.of();
}
Date now = new Date();
return hitRows.stream()
.filter(item -> isNotBlank(item.getStaffIdCard()))
.collect(Collectors.groupingBy(
CcdiProjectOverviewEmployeeHitRowVO::getStaffIdCard,
LinkedHashMap::new,
Collectors.toList()
))
.entrySet()
.stream()
.sorted(Map.Entry.comparingByKey())
.map(entry -> buildSingleResult(projectId, entry.getKey(), entry.getValue(), operator, now))
.toList();
}
private CcdiProjectOverviewEmployeeResult buildSingleResult(Long projectId,
String staffIdCard,
List<CcdiProjectOverviewEmployeeHitRowVO> staffRows,
String operator,
Date now) {
List<CcdiProjectOverviewEmployeeRuleSummaryVO> ruleSummaries = buildRuleSummaries(staffRows);
List<CcdiProjectOverviewEmployeeModelSummaryVO> modelSummaries = buildModelSummaries(staffRows);
CcdiProjectOverviewEmployeeResult result = new CcdiProjectOverviewEmployeeResult();
result.setProjectId(projectId);
result.setStaffIdCard(staffIdCard);
result.setStaffName(firstNonBlank(staffRows, CcdiProjectOverviewEmployeeHitRowVO::getStaffName));
result.setStaffCode(firstNonBlank(staffRows, CcdiProjectOverviewEmployeeHitRowVO::getStaffCode));
result.setDeptId(firstNonNull(staffRows, CcdiProjectOverviewEmployeeHitRowVO::getDeptId));
result.setDeptName(firstNonBlank(staffRows, CcdiProjectOverviewEmployeeHitRowVO::getDeptName));
result.setRuleCount(ruleSummaries.size());
result.setModelCount(modelSummaries.size());
result.setHitCount(staffRows.size());
result.setRiskLevelCode(resolveRiskLevelCode(ruleSummaries.size()));
result.setRiskPoint(ruleSummaries.stream()
.map(CcdiProjectOverviewEmployeeRuleSummaryVO::getRuleName)
.filter(this::isNotBlank)
.collect(Collectors.joining("")));
result.setModelCodesCsv(modelSummaries.stream()
.map(CcdiProjectOverviewEmployeeModelSummaryVO::getModelCode)
.collect(Collectors.joining(",")));
result.setModelNamesJson(JSON.toJSONString(modelSummaries.stream()
.map(CcdiProjectOverviewEmployeeModelSummaryVO::getModelName)
.toList()));
result.setHitRulesJson(JSON.toJSONString(ruleSummaries));
result.setModelHitSummaryJson(JSON.toJSONString(modelSummaries));
result.setCreateBy(operator);
result.setCreateTime(now);
result.setUpdateBy(operator);
result.setUpdateTime(now);
return result;
}
private List<CcdiProjectOverviewEmployeeRuleSummaryVO> buildRuleSummaries(
List<CcdiProjectOverviewEmployeeHitRowVO> staffRows
) {
return staffRows.stream()
.collect(Collectors.groupingBy(
CcdiProjectOverviewEmployeeHitRowVO::getRuleCode,
LinkedHashMap::new,
Collectors.toList()
))
.values()
.stream()
.map(rows -> {
CcdiProjectOverviewEmployeeRuleSummaryVO summary = new CcdiProjectOverviewEmployeeRuleSummaryVO();
CcdiProjectOverviewEmployeeHitRowVO first = rows.getFirst();
summary.setRuleCode(first.getRuleCode());
summary.setRuleName(first.getRuleName());
summary.setRiskLevel(first.getRiskLevel());
summary.setWarningCount(rows.size());
return summary;
})
.sorted(Comparator.comparing(CcdiProjectOverviewEmployeeRuleSummaryVO::getWarningCount).reversed()
.thenComparing(CcdiProjectOverviewEmployeeRuleSummaryVO::getRuleCode))
.toList();
}
private List<CcdiProjectOverviewEmployeeModelSummaryVO> buildModelSummaries(
List<CcdiProjectOverviewEmployeeHitRowVO> staffRows
) {
return staffRows.stream()
.collect(Collectors.groupingBy(
CcdiProjectOverviewEmployeeHitRowVO::getModelCode,
LinkedHashMap::new,
Collectors.toList()
))
.values()
.stream()
.map(rows -> {
CcdiProjectOverviewEmployeeModelSummaryVO summary = new CcdiProjectOverviewEmployeeModelSummaryVO();
CcdiProjectOverviewEmployeeHitRowVO first = rows.getFirst();
summary.setModelCode(first.getModelCode());
summary.setModelName(first.getModelName());
summary.setWarningCount(rows.size());
return summary;
})
.sorted(Comparator.comparing(CcdiProjectOverviewEmployeeModelSummaryVO::getModelCode))
.toList();
}
private String resolveRiskLevelCode(int ruleCount) {
if (ruleCount >= 5) {
return "HIGH";
}
if (ruleCount >= 2) {
return "MEDIUM";
}
return "LOW";
}
private String firstNonBlank(List<CcdiProjectOverviewEmployeeHitRowVO> staffRows,
java.util.function.Function<CcdiProjectOverviewEmployeeHitRowVO, String> getter) {
return staffRows.stream()
.map(getter)
.filter(this::isNotBlank)
.findFirst()
.orElse(null);
}
private <T> T firstNonNull(List<CcdiProjectOverviewEmployeeHitRowVO> staffRows,
java.util.function.Function<CcdiProjectOverviewEmployeeHitRowVO, T> getter) {
return staffRows.stream()
.map(getter)
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}
private boolean isNotBlank(String value) {
return value != null && !value.isBlank();
}
}

View File

@@ -29,6 +29,58 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="remark" column="remark"/>
</resultMap>
<resultMap id="CcdiProjectOverviewEmployeeHitRowMap"
type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewEmployeeHitRowVO">
<result property="projectId" column="project_id"/>
<result property="staffIdCard" column="staff_id_card"/>
<result property="staffName" column="staff_name"/>
<result property="staffCode" column="staff_code"/>
<result property="deptId" column="dept_id"/>
<result property="deptName" column="dept_name"/>
<result property="modelCode" column="model_code"/>
<result property="modelName" column="model_name"/>
<result property="ruleCode" column="rule_code"/>
<result property="ruleName" column="rule_name"/>
<result property="riskLevel" column="risk_level"/>
</resultMap>
<sql id="resolvedEmployeeHitRowsSql">
select distinct
tr.project_id,
coalesce(direct_staff.id_card, statement_staff.id_card, family_staff.id_card) as staff_id_card,
coalesce(direct_staff.name, statement_staff.name, family_staff.name) as staff_name,
cast(coalesce(direct_staff.staff_id, statement_staff.staff_id, family_staff.staff_id) as char) as staff_code,
coalesce(direct_staff.dept_id, statement_staff.dept_id, family_staff.dept_id) as dept_id,
dept.dept_name,
tr.model_code,
tr.model_name,
tr.rule_code,
tr.rule_name,
tr.risk_level
from ccdi_bank_statement_tag_result tr
left join ccdi_base_staff direct_staff
on tr.object_type = 'STAFF_ID_CARD'
and tr.object_key = direct_staff.id_card
left join ccdi_bank_statement bs
on tr.bank_statement_id = bs.bank_statement_id
left join ccdi_base_staff statement_staff
on (tr.object_key is null or tr.object_key = '')
and bs.cret_no = statement_staff.id_card
left join ccdi_staff_fmy_relation relation
on relation.status = 1
and (
((tr.object_key is null or tr.object_key = '') and bs.cret_no = relation.relation_cert_no)
or ((tr.object_key is not null and tr.object_key != '') and tr.object_type != 'STAFF_ID_CARD'
and tr.object_key = relation.relation_cert_no)
)
left join ccdi_base_staff family_staff
on relation.person_id = family_staff.id_card
left join sys_dept dept
on dept.dept_id = coalesce(direct_staff.dept_id, statement_staff.dept_id, family_staff.dept_id)
where tr.project_id = #{projectId}
and coalesce(direct_staff.id_card, statement_staff.id_card, family_staff.id_card) is not null
</sql>
<delete id="deleteByProjectId">
delete from ccdi_project_overview_employee_result
where project_id = #{projectId}
@@ -97,4 +149,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
order by id asc
</select>
<select id="selectEmployeeHitRowsByProjectId" resultMap="CcdiProjectOverviewEmployeeHitRowMap">
<include refid="resolvedEmployeeHitRowsSql"/>
order by staff_id_card asc, model_code asc, rule_code asc
</select>
</mapper>

View File

@@ -0,0 +1,118 @@
package com.ruoyi.ccdi.project.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.ccdi.project.domain.entity.CcdiProjectOverviewEmployeeResult;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
class CcdiProjectOverviewEmployeeResultBuilderTest {
@Test
@SuppressWarnings("unchecked")
void shouldAggregateHitRowsIntoEmployeeResultSnapshots() throws Exception {
Class<?> rowClass = Class.forName("com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewEmployeeHitRowVO");
Class<?> builderClass = Class.forName("com.ruoyi.ccdi.project.service.impl.CcdiProjectOverviewEmployeeResultBuilder");
Object builder = builderClass.getDeclaredConstructor().newInstance();
Method buildMethod = builderClass.getMethod("build", Long.class, List.class, String.class);
List<Object> hitRows = List.of(
buildHitRow(rowClass, "330000000000000001", "李四", "E1001", 12L, "信息二部",
"LARGE_TRANSACTION", "大额交易", "HOUSE_OR_CAR_EXPENSE", "房车消费支出交易", "HIGH"),
buildHitRow(rowClass, "330000000000000001", "李四", "E1001", 12L, "信息二部",
"LARGE_TRANSACTION", "大额交易", "HOUSE_OR_CAR_EXPENSE", "房车消费支出交易", "HIGH"),
buildHitRow(rowClass, "330000000000000001", "李四", "E1001", 12L, "信息二部",
"LARGE_TRANSACTION", "大额交易", "TAX_EXPENSE", "税务支出交易", "HIGH"),
buildHitRow(rowClass, "330000000000000001", "李四", "E1001", 12L, "信息二部",
"ABNORMAL_TRANSACTION", "异常交易", "ABNORMAL_CUSTOMER_TRANSACTION", "异常客户交易", "HIGH"),
buildHitRow(rowClass, "330000000000000001", "李四", "E1001", 12L, "信息二部",
"ABNORMAL_TRANSACTION", "异常交易", "ABNORMAL_CUSTOMER_TRANSACTION", "异常客户交易", "HIGH"),
buildHitRow(rowClass, "330000000000000001", "李四", "E1001", 12L, "信息二部",
"SUSPICIOUS_PART_TIME", "可疑兼职", "MONTHLY_FIXED_INCOME", "疑似兼职", "MEDIUM"),
buildHitRow(rowClass, "330000000000000001", "李四", "E1001", 12L, "信息二部",
"SUSPICIOUS_PROPERTY", "可疑财产", "HOUSE_REGISTRATION_MISMATCH", "房产登记不匹配", "LOW")
);
List<CcdiProjectOverviewEmployeeResult> results =
(List<CcdiProjectOverviewEmployeeResult>) buildMethod.invoke(builder, 40L, hitRows, "tester");
assertEquals(1, results.size());
CcdiProjectOverviewEmployeeResult result = results.getFirst();
assertEquals(40L, result.getProjectId());
assertEquals("330000000000000001", result.getStaffIdCard());
assertEquals("李四", result.getStaffName());
assertEquals("E1001", result.getStaffCode());
assertEquals(12L, result.getDeptId());
assertEquals("信息二部", result.getDeptName());
assertEquals(5, result.getRuleCount());
assertEquals(4, result.getModelCount());
assertEquals(7, result.getHitCount());
assertEquals("HIGH", result.getRiskLevelCode());
assertEquals("ABNORMAL_TRANSACTION,LARGE_TRANSACTION,SUSPICIOUS_PART_TIME,SUSPICIOUS_PROPERTY",
result.getModelCodesCsv());
assertNotNull(result.getRiskPoint());
JSONArray modelNames = JSON.parseArray(result.getModelNamesJson());
assertEquals(List.of("异常交易", "大额交易", "可疑兼职", "可疑财产"),
modelNames.toList(String.class));
JSONArray hitRules = JSON.parseArray(result.getHitRulesJson());
assertEquals(5, hitRules.size());
JSONObject firstRule = hitRules.getJSONObject(0);
assertEquals("ABNORMAL_CUSTOMER_TRANSACTION", firstRule.getString("ruleCode"));
assertEquals("异常客户交易", firstRule.getString("ruleName"));
assertEquals("HIGH", firstRule.getString("riskLevel"));
assertEquals(2, firstRule.getIntValue("warningCount"));
JSONArray modelSummary = JSON.parseArray(result.getModelHitSummaryJson());
Map<String, Integer> warningCountByModel = modelSummary.toList(JSONObject.class).stream()
.collect(java.util.stream.Collectors.toMap(
item -> item.getString("modelCode"),
item -> item.getIntValue("warningCount")
));
assertEquals(2, warningCountByModel.get("ABNORMAL_TRANSACTION"));
assertEquals(3, warningCountByModel.get("LARGE_TRANSACTION"));
assertEquals(1, warningCountByModel.get("SUSPICIOUS_PROPERTY"));
assertEquals(1, warningCountByModel.get("SUSPICIOUS_PART_TIME"));
}
private Object buildHitRow(Class<?> rowClass,
String staffIdCard,
String staffName,
String staffCode,
Long deptId,
String deptName,
String modelCode,
String modelName,
String ruleCode,
String ruleName,
String riskLevel) throws Exception {
Object row = rowClass.getDeclaredConstructor().newInstance();
setField(rowClass, row, "setProjectId", Long.class, 40L);
setField(rowClass, row, "setStaffIdCard", String.class, staffIdCard);
setField(rowClass, row, "setStaffName", String.class, staffName);
setField(rowClass, row, "setStaffCode", String.class, staffCode);
setField(rowClass, row, "setDeptId", Long.class, deptId);
setField(rowClass, row, "setDeptName", String.class, deptName);
setField(rowClass, row, "setModelCode", String.class, modelCode);
setField(rowClass, row, "setModelName", String.class, modelName);
setField(rowClass, row, "setRuleCode", String.class, ruleCode);
setField(rowClass, row, "setRuleName", String.class, ruleName);
setField(rowClass, row, "setRiskLevel", String.class, riskLevel);
return row;
}
private void setField(Class<?> rowClass, Object row, String methodName, Class<?> parameterType, Object value)
throws Exception {
Method method = rowClass.getMethod(methodName, parameterType);
method.invoke(row, value);
}
}