From ec006f202b9e8a316ab0a06d5197dfbddafee6ec Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Sun, 22 Mar 2026 11:40:46 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=BB=93=E6=9E=9C?= =?UTF-8?q?=E6=80=BB=E8=A7=88=E5=91=98=E5=B7=A5=E7=BB=93=E6=9E=9C=E8=A1=A8?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CcdiProjectOverviewEmployeeResult.java | 64 +++++++++++ ...diProjectOverviewEmployeeResultMapper.java | 37 +++++++ ...cdiProjectOverviewEmployeeResultMapper.xml | 100 ++++++++++++++++++ ...ojectOverviewEmployeeResultEntityTest.java | 56 ++++++++++ ...ctOverviewEmployeeResultMapperXmlTest.java | 36 +++++++ ...project-overview-employee-result-table.sql | 27 +++++ 6 files changed, 320 insertions(+) create mode 100644 ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiProjectOverviewEmployeeResult.java create mode 100644 ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewEmployeeResultMapper.java create mode 100644 ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewEmployeeResultMapper.xml create mode 100644 ccdi-project/src/test/java/com/ruoyi/ccdi/project/domain/entity/CcdiProjectOverviewEmployeeResultEntityTest.java create mode 100644 ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewEmployeeResultMapperXmlTest.java create mode 100644 sql/migration/2026-03-20-create-project-overview-employee-result-table.sql diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiProjectOverviewEmployeeResult.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiProjectOverviewEmployeeResult.java new file mode 100644 index 00000000..39fb9366 --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiProjectOverviewEmployeeResult.java @@ -0,0 +1,64 @@ +package com.ruoyi.ccdi.project.domain.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Date; + +/** + * 结果总览员工结果实体 + */ +@Data +@TableName("ccdi_project_overview_employee_result") +public class CcdiProjectOverviewEmployeeResult implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Long id; + + private Long projectId; + + private String staffIdCard; + + private String staffCode; + + private String staffName; + + private Long deptId; + + private String deptName; + + private Integer ruleCount; + + private Integer modelCount; + + private Integer hitCount; + + private String riskLevelCode; + + private String riskPoint; + + private String modelCodesCsv; + + private String modelNamesJson; + + private String hitRulesJson; + + private String modelHitSummaryJson; + + private String createBy; + + private Date createTime; + + private String updateBy; + + private Date updateTime; + + private String remark; +} diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewEmployeeResultMapper.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewEmployeeResultMapper.java new file mode 100644 index 00000000..80d3b34c --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewEmployeeResultMapper.java @@ -0,0 +1,37 @@ +package com.ruoyi.ccdi.project.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.ruoyi.ccdi.project.domain.entity.CcdiProjectOverviewEmployeeResult; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * 结果总览员工结果 Mapper + */ +public interface CcdiProjectOverviewEmployeeResultMapper extends BaseMapper { + + /** + * 按项目删除结果 + * + * @param projectId 项目ID + * @return 删除条数 + */ + int deleteByProjectId(@Param("projectId") Long projectId); + + /** + * 批量插入结果 + * + * @param list 结果列表 + * @return 插入条数 + */ + int insertBatch(@Param("list") List list); + + /** + * 按项目查询结果 + * + * @param projectId 项目ID + * @return 结果列表 + */ + List selectByProjectId(@Param("projectId") Long projectId); +} diff --git a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewEmployeeResultMapper.xml b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewEmployeeResultMapper.xml new file mode 100644 index 00000000..8472a483 --- /dev/null +++ b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewEmployeeResultMapper.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + delete from ccdi_project_overview_employee_result + where project_id = #{projectId} + + + + insert into ccdi_project_overview_employee_result ( + project_id, staff_id_card, staff_code, staff_name, dept_id, dept_name, + rule_count, model_count, hit_count, risk_level_code, risk_point, + model_codes_csv, model_names_json, hit_rules_json, model_hit_summary_json, + create_by, create_time, update_by, update_time, remark + ) values + + ( + #{item.projectId}, #{item.staffIdCard}, #{item.staffCode}, #{item.staffName}, + #{item.deptId}, #{item.deptName}, #{item.ruleCount}, #{item.modelCount}, + #{item.hitCount}, #{item.riskLevelCode}, #{item.riskPoint}, + #{item.modelCodesCsv}, #{item.modelNamesJson}, #{item.hitRulesJson}, #{item.modelHitSummaryJson}, + #{item.createBy}, #{item.createTime}, #{item.updateBy}, #{item.updateTime}, #{item.remark} + ) + + on duplicate key update + staff_code = values(staff_code), + staff_name = values(staff_name), + dept_id = values(dept_id), + dept_name = values(dept_name), + rule_count = values(rule_count), + model_count = values(model_count), + hit_count = values(hit_count), + risk_level_code = values(risk_level_code), + risk_point = values(risk_point), + model_codes_csv = values(model_codes_csv), + model_names_json = values(model_names_json), + hit_rules_json = values(hit_rules_json), + model_hit_summary_json = values(model_hit_summary_json), + update_by = values(update_by), + update_time = values(update_time), + remark = values(remark) + + + + + diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/domain/entity/CcdiProjectOverviewEmployeeResultEntityTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/domain/entity/CcdiProjectOverviewEmployeeResultEntityTest.java new file mode 100644 index 00000000..198f0746 --- /dev/null +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/domain/entity/CcdiProjectOverviewEmployeeResultEntityTest.java @@ -0,0 +1,56 @@ +package com.ruoyi.ccdi.project.domain.entity; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CcdiProjectOverviewEmployeeResultEntityTest { + + private static final Path ENTITY_PATH = Path.of( + "src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiProjectOverviewEmployeeResult.java" + ); + @Test + void entityAndSqlShouldDefineProjectOverviewEmployeeResultTable() throws Exception { + String entitySource = Files.readString(ENTITY_PATH); + String sql = Files.readString(resolveSqlPath()); + String normalizedSql = sql.toLowerCase().replace("`", ""); + + assertAll( + () -> assertTrue(entitySource.contains("@TableName(\"ccdi_project_overview_employee_result\")")), + () -> assertTrue(entitySource.contains("private Long projectId;")), + () -> assertTrue(entitySource.contains("private String staffIdCard;")), + () -> assertTrue(entitySource.contains("private Integer ruleCount;")), + () -> assertTrue(entitySource.contains("private Integer modelCount;")), + () -> assertTrue(entitySource.contains("private Integer hitCount;")), + () -> assertTrue(entitySource.contains("private String riskLevelCode;")), + () -> assertTrue(entitySource.contains("private String modelCodesCsv;")), + () -> assertTrue(entitySource.contains("private String modelNamesJson;")), + () -> assertTrue(entitySource.contains("private String hitRulesJson;")), + () -> assertTrue(entitySource.contains("private String modelHitSummaryJson;")), + () -> assertTrue(normalizedSql.contains("create table if not exists ccdi_project_overview_employee_result")), + () -> assertTrue(normalizedSql.contains("unique key")), + () -> assertTrue(normalizedSql.contains("project_id")), + () -> assertTrue(normalizedSql.contains("staff_id_card")), + () -> assertTrue(normalizedSql.contains("rule_count")), + () -> assertTrue(normalizedSql.contains("model_count")), + () -> assertTrue(normalizedSql.contains("hit_count")), + () -> assertTrue(normalizedSql.contains("risk_level_code")), + () -> assertTrue(normalizedSql.contains("model_codes_csv")), + () -> assertTrue(normalizedSql.contains("model_names_json")), + () -> assertTrue(normalizedSql.contains("hit_rules_json")), + () -> assertTrue(normalizedSql.contains("model_hit_summary_json")) + ); + } + + private Path resolveSqlPath() { + Path moduleRelative = Path.of("../sql/migration/2026-03-20-create-project-overview-employee-result-table.sql"); + if (Files.exists(moduleRelative)) { + return moduleRelative; + } + return Path.of("sql/migration/2026-03-20-create-project-overview-employee-result-table.sql"); + } +} diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewEmployeeResultMapperXmlTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewEmployeeResultMapperXmlTest.java new file mode 100644 index 00000000..590e2f56 --- /dev/null +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewEmployeeResultMapperXmlTest.java @@ -0,0 +1,36 @@ +package com.ruoyi.ccdi.project.mapper; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CcdiProjectOverviewEmployeeResultMapperXmlTest { + + private static final Path MAPPER_PATH = Path.of( + "src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewEmployeeResultMapper.java" + ); + private static final Path XML_PATH = Path.of( + "src/main/resources/mapper/ccdi/project/CcdiProjectOverviewEmployeeResultMapper.xml" + ); + + @Test + void mapperAndXmlShouldExposeMinimumCrudStatements() throws Exception { + String mapperSource = Files.readString(MAPPER_PATH); + String xml = Files.readString(XML_PATH); + + assertAll( + () -> assertTrue(mapperSource.contains("interface CcdiProjectOverviewEmployeeResultMapper")), + () -> assertTrue(mapperSource.contains("deleteByProjectId")), + () -> assertTrue(mapperSource.contains("insertBatch")), + () -> assertTrue(mapperSource.contains("selectByProjectId")), + () -> assertTrue(xml.contains("delete id=\"deleteByProjectId\"")), + () -> assertTrue(xml.contains("insert id=\"insertBatch\"")), + () -> assertTrue(xml.contains("select id=\"selectByProjectId\"")), + () -> assertTrue(xml.contains("ccdi_project_overview_employee_result")) + ); + } +} diff --git a/sql/migration/2026-03-20-create-project-overview-employee-result-table.sql b/sql/migration/2026-03-20-create-project-overview-employee-result-table.sql new file mode 100644 index 00000000..ef17d151 --- /dev/null +++ b/sql/migration/2026-03-20-create-project-overview-employee-result-table.sql @@ -0,0 +1,27 @@ +create table if not exists `ccdi_project_overview_employee_result` ( + `id` bigint not null auto_increment comment '主键ID', + `project_id` bigint not null comment '项目ID', + `staff_id_card` varchar(18) not null comment '员工身份证号', + `staff_code` varchar(64) default null comment '员工工号', + `staff_name` varchar(64) default null comment '员工姓名', + `dept_id` bigint default null comment '部门ID', + `dept_name` varchar(128) default null comment '部门名称', + `rule_count` int not null default 0 comment '命中规则数', + `model_count` int not null default 0 comment '命中模型数', + `hit_count` int not null default 0 comment '命中次数', + `risk_level_code` varchar(32) not null comment '风险等级编码', + `risk_point` varchar(1000) default null comment '风险点', + `model_codes_csv` varchar(1000) default null comment '命中模型编码CSV', + `model_names_json` json default null comment '命中模型名称快照', + `hit_rules_json` json default null comment '命中规则快照', + `model_hit_summary_json` json default null comment '模型命中汇总快照', + `create_by` varchar(64) default null comment '创建者', + `create_time` datetime default current_timestamp comment '创建时间', + `update_by` varchar(64) default null comment '更新者', + `update_time` datetime default current_timestamp on update current_timestamp comment '更新时间', + `remark` varchar(500) default null comment '备注', + primary key (`id`), + unique key `uk_ccdi_project_overview_employee_result` (`project_id`, `staff_id_card`), + key `idx_ccdi_project_overview_employee_result_risk_level` (`project_id`, `risk_level_code`), + key `idx_ccdi_project_overview_employee_result_dept` (`project_id`, `dept_id`) +) engine=innodb default charset=utf8mb4 comment='结果总览员工结果表'; From 0a58ac3251f54eb3e1c0f73f8886ef11895c2830 Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Sun, 22 Mar 2026 11:45:26 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E7=BB=93=E6=9E=9C?= =?UTF-8?q?=E6=80=BB=E8=A7=88=E5=91=98=E5=B7=A5=E7=BB=93=E6=9E=9C=E8=81=9A?= =?UTF-8?q?=E5=90=88=E6=9E=84=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CcdiProjectOverviewEmployeeHitRowVO.java | 32 ++++ ...ProjectOverviewEmployeeModelSummaryVO.java | 16 ++ ...iProjectOverviewEmployeeRuleSummaryVO.java | 18 ++ ...diProjectOverviewEmployeeResultMapper.java | 9 + ...iProjectOverviewEmployeeResultBuilder.java | 171 ++++++++++++++++++ ...cdiProjectOverviewEmployeeResultMapper.xml | 57 ++++++ ...jectOverviewEmployeeResultBuilderTest.java | 118 ++++++++++++ 7 files changed, 421 insertions(+) create mode 100644 ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewEmployeeHitRowVO.java create mode 100644 ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewEmployeeModelSummaryVO.java create mode 100644 ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewEmployeeRuleSummaryVO.java create mode 100644 ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewEmployeeResultBuilder.java create mode 100644 ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewEmployeeResultBuilderTest.java diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewEmployeeHitRowVO.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewEmployeeHitRowVO.java new file mode 100644 index 00000000..5f9dd138 --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewEmployeeHitRowVO.java @@ -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; +} diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewEmployeeModelSummaryVO.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewEmployeeModelSummaryVO.java new file mode 100644 index 00000000..c0a2f0eb --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewEmployeeModelSummaryVO.java @@ -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; +} diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewEmployeeRuleSummaryVO.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewEmployeeRuleSummaryVO.java new file mode 100644 index 00000000..a7e00bd6 --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewEmployeeRuleSummaryVO.java @@ -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; +} diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewEmployeeResultMapper.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewEmployeeResultMapper.java index 80d3b34c..573c8bcc 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewEmployeeResultMapper.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewEmployeeResultMapper.java @@ -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 selectByProjectId(@Param("projectId") Long projectId); + + /** + * 按项目查询员工归并命中明细 + * + * @param projectId 项目ID + * @return 命中明细 + */ + List selectEmployeeHitRowsByProjectId(@Param("projectId") Long projectId); } diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewEmployeeResultBuilder.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewEmployeeResultBuilder.java new file mode 100644 index 00000000..a63dc75d --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewEmployeeResultBuilder.java @@ -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 build(Long projectId, + List 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 staffRows, + String operator, + Date now) { + List ruleSummaries = buildRuleSummaries(staffRows); + List 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 buildRuleSummaries( + List 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 buildModelSummaries( + List 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 staffRows, + java.util.function.Function getter) { + return staffRows.stream() + .map(getter) + .filter(this::isNotBlank) + .findFirst() + .orElse(null); + } + + private T firstNonNull(List staffRows, + java.util.function.Function getter) { + return staffRows.stream() + .map(getter) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + private boolean isNotBlank(String value) { + return value != null && !value.isBlank(); + } +} diff --git a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewEmployeeResultMapper.xml b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewEmployeeResultMapper.xml index 8472a483..30fe9af8 100644 --- a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewEmployeeResultMapper.xml +++ b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewEmployeeResultMapper.xml @@ -29,6 +29,58 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + + + + + + + + + + + + + + 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 + + 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 + + diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewEmployeeResultBuilderTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewEmployeeResultBuilderTest.java new file mode 100644 index 00000000..7ea53c87 --- /dev/null +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewEmployeeResultBuilderTest.java @@ -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 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 results = + (List) 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 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); + } +} From f539c4ba27f505f0649f2f5377dc023e18fc6d4a Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Sun, 22 Mar 2026 11:47:37 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E6=8E=A5=E5=85=A5=E7=BB=93=E6=9E=9C?= =?UTF-8?q?=E6=80=BB=E8=A7=88=E5=91=98=E5=B7=A5=E7=BB=93=E6=9E=9C=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E9=87=8D=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ICcdiProjectOverviewService.java | 8 ++++ .../service/impl/CcdiBankTagServiceImpl.java | 2 +- .../impl/CcdiProjectOverviewServiceImpl.java | 38 ++++++++++++++++ ...diProjectOverviewServiceStructureTest.java | 1 + ...cdiBankTagServiceRiskCountRefreshTest.java | 8 ++-- .../CcdiProjectOverviewServiceImplTest.java | 45 +++++++++++++++++++ 6 files changed, 97 insertions(+), 5 deletions(-) diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java index f3bb80c3..98858b17 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java @@ -56,6 +56,14 @@ public interface ICcdiProjectOverviewService { return new CcdiProjectRiskModelPeopleVO(); } + /** + * 重算结果总览员工结果并同步项目风险人数 + * + * @param projectId 项目ID + * @param operator 操作人 + */ + void refreshOverviewEmployeeResults(Long projectId, String operator); + /** * 刷新项目风险人数 * diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java index c5d6cceb..030e826c 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java @@ -129,7 +129,7 @@ public class CcdiBankTagServiceImpl implements ICcdiBankTagService { resultMapper.insertBatch(allResults); } - projectOverviewService.refreshProjectRiskCounts(projectId, operator); + projectOverviewService.refreshOverviewEmployeeResults(projectId, operator); task.setStatus(STATUS_SUCCESS); task.setSuccessRuleCount(rules.size()); 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 0cc566fc..0f6e935e 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 @@ -3,8 +3,10 @@ package com.ruoyi.ccdi.project.service.impl; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.ccdi.project.domain.CcdiProject; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO; +import com.ruoyi.ccdi.project.domain.entity.CcdiProjectOverviewEmployeeResult; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO; +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewEmployeeHitRowVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewStatVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardsVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO; @@ -14,6 +16,7 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleItemVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO; import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper; +import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewEmployeeResultMapper; import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewMapper; import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService; import com.ruoyi.common.exception.ServiceException; @@ -37,6 +40,12 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi @Resource private CcdiProjectMapper projectMapper; + @Resource + private CcdiProjectOverviewEmployeeResultMapper overviewEmployeeResultMapper; + + @Resource + private CcdiProjectOverviewEmployeeResultBuilder overviewEmployeeResultBuilder; + @Override public CcdiProjectOverviewDashboardVO getDashboard(Long projectId) { CcdiProject project = overviewMapper.selectDashboardBaseByProjectId(projectId); @@ -122,6 +131,29 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi return people; } + @Override + @Transactional(rollbackFor = Exception.class) + public void refreshOverviewEmployeeResults(Long projectId, String operator) { + getRequiredProject(projectId); + overviewEmployeeResultMapper.deleteByProjectId(projectId); + List hitRows = + overviewEmployeeResultMapper.selectEmployeeHitRowsByProjectId(projectId); + List results = + overviewEmployeeResultBuilder.build(projectId, hitRows, operator); + + if (!results.isEmpty()) { + overviewEmployeeResultMapper.insertBatch(results); + } + + projectMapper.updateRiskCountsByProjectId( + projectId, + countRiskLevel(results, "HIGH"), + countRiskLevel(results, "MEDIUM"), + countRiskLevel(results, "LOW"), + operator + ); + } + @Override @Transactional(rollbackFor = Exception.class) public void refreshProjectRiskCounts(Long projectId, String operator) { @@ -196,6 +228,12 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi throw new ServiceException("项目风险人数统计结果类型异常"); } + private Integer countRiskLevel(List results, String riskLevelCode) { + return Math.toIntExact(results.stream() + .filter(item -> riskLevelCode.equals(item.getRiskLevelCode())) + .count()); + } + private Integer defaultZero(Integer value) { return value == null ? 0 : value; } diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiProjectOverviewServiceStructureTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiProjectOverviewServiceStructureTest.java index 5c8545eb..fe7afcc9 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiProjectOverviewServiceStructureTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiProjectOverviewServiceStructureTest.java @@ -13,6 +13,7 @@ class CcdiProjectOverviewServiceStructureTest { assertNotNull(clazz.getMethod("getDashboard", Long.class)); assertNotNull(clazz.getMethod("getRiskPeopleOverview", Long.class)); assertNotNull(clazz.getMethod("getTopRiskPeople", Long.class)); + assertNotNull(clazz.getMethod("refreshOverviewEmployeeResults", Long.class, String.class)); assertNotNull(clazz.getMethod("refreshProjectRiskCounts", Long.class, String.class)); } } diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceRiskCountRefreshTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceRiskCountRefreshTest.java index c3a3858e..a22dc898 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceRiskCountRefreshTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceRiskCountRefreshTest.java @@ -62,7 +62,7 @@ class CcdiBankTagServiceRiskCountRefreshTest { private ProjectBankTagRebuildCoordinator coordinator; @Test - void shouldRefreshProjectRiskCountsAfterTagRebuildSuccess() { + void shouldRefreshOverviewEmployeeResultsAfterTagRebuildSuccess() { ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run); CcdiBankTagRule rule = buildRule(); @@ -84,13 +84,13 @@ class CcdiBankTagServiceRiskCountRefreshTest { inOrder.verify(projectService).updateProjectStatus(40L, "3", "tester"); inOrder.verify(resultMapper).deleteByProjectAndModel(40L, null); inOrder.verify(resultMapper).insertBatch(anyList()); - inOrder.verify(projectOverviewService).refreshProjectRiskCounts(40L, "tester"); + inOrder.verify(projectOverviewService).refreshOverviewEmployeeResults(40L, "tester"); inOrder.verify(taskMapper).updateTask(argThat(task -> "SUCCESS".equals(task.getStatus()))); inOrder.verify(projectService).updateProjectStatus(40L, "1", "tester"); } @Test - void shouldFailTaskWhenRiskCountRefreshFails() { + void shouldFailTaskWhenOverviewEmployeeResultRefreshFails() { ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run); CcdiBankTagRule rule = buildRule(); @@ -105,7 +105,7 @@ class CcdiBankTagServiceRiskCountRefreshTest { when(configResolver.resolve(40L, rule)).thenReturn(config); when(analysisMapper.selectHouseOrCarExpenseStatements(40L)).thenReturn(List.of(buildHit())); doThrow(new RuntimeException("refresh failed")) - .when(projectOverviewService).refreshProjectRiskCounts(40L, "tester"); + .when(projectOverviewService).refreshOverviewEmployeeResults(40L, "tester"); assertThrows(RuntimeException.class, () -> service.rebuildProject(40L, null, "tester", TriggerType.MANUAL)); 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 4637842b..7fcffda6 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 @@ -3,8 +3,10 @@ package com.ruoyi.ccdi.project.service.impl; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.ccdi.project.domain.CcdiProject; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO; +import com.ruoyi.ccdi.project.domain.entity.CcdiProjectOverviewEmployeeResult; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO; 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.CcdiProjectRiskModelPeopleItemVO; @@ -12,6 +14,7 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO; import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper; +import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewEmployeeResultMapper; import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewMapper; import com.ruoyi.common.exception.ServiceException; import java.math.BigDecimal; @@ -44,6 +47,12 @@ class CcdiProjectOverviewServiceImplTest { @Mock private CcdiProjectMapper projectMapper; + @Mock + private CcdiProjectOverviewEmployeeResultMapper overviewEmployeeResultMapper; + + @Mock + private CcdiProjectOverviewEmployeeResultBuilder overviewEmployeeResultBuilder; + @Test void shouldBuildDashboardWithNoRiskCount() { CcdiProject project = new CcdiProject(); @@ -182,6 +191,36 @@ class CcdiProjectOverviewServiceImplTest { assertEquals("查看详情", result.getRows().getFirst().getActionLabel()); } + @Test + void shouldRefreshOverviewEmployeeResultsAndSyncRiskCounts() { + CcdiProject project = new CcdiProject(); + project.setProjectId(43L); + when(projectMapper.selectById(43L)).thenReturn(project); + + List hitRows = List.of(new CcdiProjectOverviewEmployeeHitRowVO()); + List results = List.of( + buildEmployeeResult("HIGH"), + buildEmployeeResult("MEDIUM"), + buildEmployeeResult("LOW") + ); + when(overviewEmployeeResultMapper.selectEmployeeHitRowsByProjectId(43L)).thenReturn(hitRows); + when(overviewEmployeeResultBuilder.build(43L, hitRows, "tester")).thenReturn(results); + + service.refreshOverviewEmployeeResults(43L, "tester"); + + org.mockito.InOrder inOrder = org.mockito.Mockito.inOrder( + projectMapper, + overviewEmployeeResultMapper, + overviewEmployeeResultBuilder + ); + inOrder.verify(projectMapper).selectById(43L); + inOrder.verify(overviewEmployeeResultMapper).deleteByProjectId(43L); + 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"); + } + @Test void shouldReturnEmptyCollectionsForRiskModelCardsAndPeople() { CcdiProject project = new CcdiProject(); @@ -234,4 +273,10 @@ class CcdiProjectOverviewServiceImplTest { queryDTO.setPageSize(10); return queryDTO; } + + private CcdiProjectOverviewEmployeeResult buildEmployeeResult(String riskLevelCode) { + CcdiProjectOverviewEmployeeResult result = new CcdiProjectOverviewEmployeeResult(); + result.setRiskLevelCode(riskLevelCode); + return result; + } } From ef106169dc0a1b3376d3f86f318b21f1f524722f Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Sun, 22 Mar 2026 11:52:09 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E5=88=87=E6=8D=A2=E7=BB=93=E6=9E=9C?= =?UTF-8?q?=E6=80=BB=E8=A7=88=E6=9F=A5=E8=AF=A2=E5=88=B0=E5=91=98=E5=B7=A5?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...iProjectOverviewEmployeeRuleSummaryVO.java | 2 + ...iProjectOverviewEmployeeResultBuilder.java | 1 + .../project/CcdiProjectOverviewMapper.xml | 197 +++++++++++++----- ...ojectOverviewMapperRiskModelCardsTest.java | 9 +- ...jectOverviewMapperRiskModelPeopleTest.java | 21 +- .../CcdiProjectOverviewMapperSqlTest.java | 41 +++- 6 files changed, 192 insertions(+), 79 deletions(-) diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewEmployeeRuleSummaryVO.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewEmployeeRuleSummaryVO.java index a7e00bd6..76553653 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewEmployeeRuleSummaryVO.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewEmployeeRuleSummaryVO.java @@ -8,6 +8,8 @@ import lombok.Data; @Data public class CcdiProjectOverviewEmployeeRuleSummaryVO { + private String modelCode; + private String ruleCode; private String ruleName; diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewEmployeeResultBuilder.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewEmployeeResultBuilder.java index a63dc75d..9794f0e8 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewEmployeeResultBuilder.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewEmployeeResultBuilder.java @@ -103,6 +103,7 @@ public class CcdiProjectOverviewEmployeeResultBuilder { .map(rows -> { CcdiProjectOverviewEmployeeRuleSummaryVO summary = new CcdiProjectOverviewEmployeeRuleSummaryVO(); CcdiProjectOverviewEmployeeHitRowVO first = rows.getFirst(); + summary.setModelCode(first.getModelCode()); summary.setRuleCode(first.getRuleCode()); summary.setRuleName(first.getRuleName()); summary.setRiskLevel(first.getRiskLevel()); diff --git a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml index 57eed98b..57b631ee 100644 --- a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml +++ b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml @@ -33,6 +33,29 @@ select="selectRiskHitTagsByScope"/> + + select 0 as digit + union all select 1 + union all select 2 + union all select 3 + union all select 4 + union all select 5 + union all select 6 + union all select 7 + union all select 8 + union all select 9 + + + + select ones.digit + tens.digit * 10 as idx + from ( + + ) ones + cross join ( + + ) tens + + select distinct tr.id, @@ -186,14 +209,60 @@ @@ -213,13 +282,18 @@ ) models left join ( select - base.model_code, - count(1) as warning_count, - count(distinct base.staff_id_card) as people_count - from ( - - ) base - group by base.model_code + json_unquote(json_extract(result.model_hit_summary_json, concat('$[', idx.idx, '].modelCode'))) as model_code, + sum(cast(json_unquote(json_extract( + result.model_hit_summary_json, + concat('$[', idx.idx, '].warningCount') + )) as unsigned)) as warning_count, + count(distinct result.staff_id_card) as people_count + from ccdi_project_overview_employee_result result + join ( + + ) idx on idx.idx < json_length(result.model_hit_summary_json) + where result.project_id = #{projectId} + group by json_unquote(json_extract(result.model_hit_summary_json, concat('$[', idx.idx, '].modelCode'))) ) stats on models.model_code = stats.model_code order by warning_count desc, model_code asc @@ -227,73 +301,88 @@ ")); + assertTrue(xml.contains("model_hit_summary_json")); + assertTrue(xml.contains("json_extract(")); assertTrue(xml.contains("coalesce(stats.warning_count, 0) as warning_count")); assertTrue(xml.contains("coalesce(stats.people_count, 0) as people_count")); - assertTrue(xml.contains("count(1) as warning_count")); + assertTrue(xml.contains(".warningCount")); assertTrue(xml.contains("order by warning_count desc, model_code asc")); } } diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperRiskModelPeopleTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperRiskModelPeopleTest.java index 9e1b6a32..46fec559 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperRiskModelPeopleTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperRiskModelPeopleTest.java @@ -34,18 +34,19 @@ class CcdiProjectOverviewMapperRiskModelPeopleTest { assertTrue(xml.contains("query.modelCodes != null and query.modelCodes.size() > 0")); assertTrue(xml.contains("query.matchMode == 'ALL'")); assertFalse(xml.contains("#{query.modelCodes.size}")); - assertTrue(xml.contains("count(distinct base.model_code) = #{query.modelCodesCount}")); + assertTrue(xml.contains("find_in_set(#{modelCode}, result.model_codes_csv)")); assertTrue(xml.contains("")); - assertTrue(xml.contains("base.staff_name like concat('%', trim(#{query.keyword}), '%')")); - assertTrue(xml.contains("cast(base.staff_code as char) like concat('%', trim(#{query.keyword}), '%')")); - assertTrue(xml.contains("base.dept_id = #{query.deptId}")); + assertTrue(xml.contains("result.staff_name like concat('%', trim(#{query.keyword}), '%')")); + assertTrue(xml.contains("result.staff_code like concat('%', trim(#{query.keyword}), '%')")); + assertTrue(xml.contains("result.dept_id = #{query.deptId}")); assertTrue(xml.contains("select=\"selectRiskModelNamesByScope\"")); assertTrue(xml.contains("select=\"selectRiskHitTagsByScope\"")); - assertTrue(xml.contains("find_in_set(scoped.model_code, #{selectedModelCodes})")); - assertFalse(xml.contains("select distinct scoped.model_name")); - assertTrue(xml.contains("group by scoped.model_code, scoped.model_name")); - assertTrue(xml.contains("order by scoped.model_code asc")); - assertTrue(xml.contains("order by case max(scoped.risk_level)")); - assertTrue(xml.contains("scoped.rule_code asc")); + assertTrue(xml.contains("model_hit_summary_json")); + assertTrue(xml.contains("hit_rules_json")); + assertTrue(xml.contains("json_extract(")); + assertTrue(xml.contains(".modelCode")); + assertTrue(xml.contains(".modelName")); + assertTrue(xml.contains(".ruleCode")); + assertTrue(xml.contains(".riskLevel")); } } diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperSqlTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperSqlTest.java index fcf87d5b..d1adbf44 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperSqlTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperSqlTest.java @@ -11,18 +11,30 @@ import static org.junit.jupiter.api.Assertions.assertTrue; class CcdiProjectOverviewMapperSqlTest { @Test - void shouldContainEmployeeRiskAggregationSql() throws Exception { + void shouldReadOverviewQueriesFromEmployeeResultTable() throws Exception { String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml")); + String riskPeopleSql = extractSelect(xml, "selectRiskPeopleOverviewByProjectId"); + String topRiskPeopleSql = extractSelect(xml, "selectTopRiskPeopleByProjectId"); + String riskModelCardsSql = extractSelect(xml, "selectRiskModelCardsByProjectId"); + String riskModelPeopleSql = extractSelect(xml, "selectRiskModelPeoplePage"); - assertTrue(xml.contains("count(distinct base.rule_code)")); - assertTrue(xml.contains("count(distinct base.model_code)")); - assertTrue(xml.contains("count(1) as hit_count")); - assertTrue(xml.contains("agg.hit_count")); - assertTrue(xml.contains("when agg.rule_count >= 5 then 'HIGH'")); - assertTrue(xml.contains("when agg.rule_count between 2 and 4 then 'MEDIUM'")); - assertTrue(xml.contains("group_concat(")); - assertTrue(xml.contains("as risk_point")); - assertTrue(xml.contains("order by grouped.hit_count desc, grouped.rule_code asc")); + assertTrue(riskPeopleSql.contains("from ccdi_project_overview_employee_result")); + assertTrue(riskPeopleSql.contains("risk_level_code")); + assertTrue(riskPeopleSql.contains("model_count")); + assertTrue(riskPeopleSql.contains("risk_point")); + assertFalse(riskPeopleSql.contains("resolvedEmployeeRiskBaseSql")); + + assertTrue(topRiskPeopleSql.contains("from ccdi_project_overview_employee_result")); + assertTrue(topRiskPeopleSql.contains("risk_level_code in ('HIGH', 'MEDIUM')")); + assertFalse(topRiskPeopleSql.contains("resolvedEmployeeRiskBaseSql")); + + assertTrue(riskModelCardsSql.contains("from ccdi_project_overview_employee_result")); + assertTrue(riskModelCardsSql.contains("model_hit_summary_json")); + assertFalse(riskModelCardsSql.contains("resolvedEmployeeRiskBaseSql")); + + assertTrue(riskModelPeopleSql.contains("from ccdi_project_overview_employee_result")); + assertTrue(riskModelPeopleSql.contains("model_codes_csv")); + assertFalse(riskModelPeopleSql.contains("resolvedEmployeeRiskBaseSql")); } @Test @@ -30,6 +42,13 @@ class CcdiProjectOverviewMapperSqlTest { String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml")); assertFalse(xml.contains("row_number() over"), xml); - assertTrue(xml.contains("not exists"), xml); + assertFalse(xml.contains("json_table("), xml); + } + + private String extractSelect(String xml, String selectId) { + String start = "