From cb8e144564b43aa4c6cae935599e1b3e4c0eb0c9 Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Thu, 19 Mar 2026 15:23:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E7=BB=93=E6=9E=9C=E6=80=BB?= =?UTF-8?q?=E8=A7=88=E9=A3=8E=E9=99=A9=E6=8E=A5=E5=8F=A3=E5=B9=B6=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E5=9B=9E=E5=86=99=E8=81=94=E8=B0=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CcdiProjectOverviewController.java | 60 ++++++ .../CcdiProjectEmployeeRiskAggregateVO.java | 32 ++++ .../vo/CcdiProjectOverviewDashboardVO.java | 17 ++ .../domain/vo/CcdiProjectOverviewStatVO.java | 16 ++ .../CcdiProjectRiskPeopleOverviewItemVO.java | 22 +++ .../vo/CcdiProjectRiskPeopleOverviewVO.java | 13 ++ .../vo/CcdiProjectTopRiskPeopleItemVO.java | 24 +++ .../domain/vo/CcdiProjectTopRiskPeopleVO.java | 13 ++ .../project/mapper/CcdiProjectMapper.java | 16 ++ .../mapper/CcdiProjectOverviewMapper.java | 47 +++++ .../service/ICcdiProjectOverviewService.java | 43 +++++ .../service/impl/CcdiBankTagServiceImpl.java | 6 + .../impl/CcdiProjectOverviewServiceImpl.java | 179 ++++++++++++++++++ .../mapper/ccdi/project/CcdiProjectMapper.xml | 11 ++ .../project/CcdiProjectOverviewMapper.xml | 175 +++++++++++++++++ .../CcdiProjectOverviewControllerTest.java | 94 +++++++++ .../CcdiProjectOverviewMapperSqlTest.java | 30 +++ ...diProjectOverviewServiceStructureTest.java | 18 ++ .../impl/CcdiBankTagServiceImplTest.java | 4 + ...cdiBankTagServiceRiskCountRefreshTest.java | 143 ++++++++++++++ .../CcdiProjectOverviewServiceImplTest.java | 124 ++++++++++++ ...verview-risk-api-backend-implementation.md | 31 +++ ...-overview-risk-api-backend-verification.md | 54 ++++++ 23 files changed, 1172 insertions(+) create mode 100644 ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java create mode 100644 ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectEmployeeRiskAggregateVO.java create mode 100644 ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewDashboardVO.java create mode 100644 ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewStatVO.java create mode 100644 ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectRiskPeopleOverviewItemVO.java create mode 100644 ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectRiskPeopleOverviewVO.java create mode 100644 ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectTopRiskPeopleItemVO.java create mode 100644 ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectTopRiskPeopleVO.java create mode 100644 ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapper.java create mode 100644 ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java create mode 100644 ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java create mode 100644 ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml create mode 100644 ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerTest.java create mode 100644 ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperSqlTest.java create mode 100644 ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiProjectOverviewServiceStructureTest.java create mode 100644 ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceRiskCountRefreshTest.java create mode 100644 ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java create mode 100644 docs/reports/implementation/2026-03-19-results-overview-risk-api-backend-implementation.md create mode 100644 docs/tests/records/2026-03-19-results-overview-risk-api-backend-verification.md diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java new file mode 100644 index 00000000..910e2548 --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java @@ -0,0 +1,60 @@ +package com.ruoyi.ccdi.project.controller; + +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO; +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO; +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO; +import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 结果总览控制器 + */ +@RestController +@RequestMapping("/ccdi/project/overview") +@Tag(name = "项目结果总览") +public class CcdiProjectOverviewController extends BaseController { + + @Resource + private ICcdiProjectOverviewService overviewService; + + /** + * 查询风险仪表盘 + */ + @GetMapping("/dashboard") + @Operation(summary = "查询风险仪表盘") + @PreAuthorize("@ss.hasPermi('ccdi:project:query')") + public AjaxResult getDashboard(Long projectId) { + CcdiProjectOverviewDashboardVO dashboard = overviewService.getDashboard(projectId); + return AjaxResult.success(dashboard); + } + + /** + * 查询风险人员总览 + */ + @GetMapping("/risk-people") + @Operation(summary = "查询风险人员总览") + @PreAuthorize("@ss.hasPermi('ccdi:project:query')") + public AjaxResult getRiskPeople(Long projectId) { + CcdiProjectRiskPeopleOverviewVO overview = overviewService.getRiskPeopleOverview(projectId); + return AjaxResult.success(overview); + } + + /** + * 查询中高风险人员TOP10 + */ + @GetMapping("/top-risk-people") + @Operation(summary = "查询中高风险人员TOP10") + @PreAuthorize("@ss.hasPermi('ccdi:project:query')") + public AjaxResult getTopRiskPeople(Long projectId) { + CcdiProjectTopRiskPeopleVO topRiskPeople = overviewService.getTopRiskPeople(projectId); + return AjaxResult.success(topRiskPeople); + } +} diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectEmployeeRiskAggregateVO.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectEmployeeRiskAggregateVO.java new file mode 100644 index 00000000..f70c2baf --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectEmployeeRiskAggregateVO.java @@ -0,0 +1,32 @@ +package com.ruoyi.ccdi.project.domain.vo; + +import lombok.Data; + +/** + * 员工风险聚合结果 + */ +@Data +public class CcdiProjectEmployeeRiskAggregateVO { + + private String staffIdCard; + + private String staffName; + + private Long deptId; + + private String deptName; + + private Integer ruleCount; + + private Integer modelCount; + + private String topRuleCode; + + private String topRuleName; + + private String riskLevelCode; + + private String riskLevelName; + + private Integer riskLevelSort; +} diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewDashboardVO.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewDashboardVO.java new file mode 100644 index 00000000..7980860a --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewDashboardVO.java @@ -0,0 +1,17 @@ +package com.ruoyi.ccdi.project.domain.vo; + +import java.util.List; +import lombok.Data; + +/** + * 结果总览风险仪表盘 + */ +@Data +public class CcdiProjectOverviewDashboardVO { + + private String title; + + private String subtitle; + + private List stats; +} diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewStatVO.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewStatVO.java new file mode 100644 index 00000000..b698ed9c --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectOverviewStatVO.java @@ -0,0 +1,16 @@ +package com.ruoyi.ccdi.project.domain.vo; + +import lombok.Data; + +/** + * 结果总览统计项 + */ +@Data +public class CcdiProjectOverviewStatVO { + + private String key; + + private String label; + + private Integer value; +} diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectRiskPeopleOverviewItemVO.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectRiskPeopleOverviewItemVO.java new file mode 100644 index 00000000..cb28c890 --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectRiskPeopleOverviewItemVO.java @@ -0,0 +1,22 @@ +package com.ruoyi.ccdi.project.domain.vo; + +import lombok.Data; + +/** + * 风险人员总览项 + */ +@Data +public class CcdiProjectRiskPeopleOverviewItemVO { + + private String name; + + private String idNo; + + private String department; + + private Integer riskCount; + + private String riskPoint; + + private String actionLabel; +} diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectRiskPeopleOverviewVO.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectRiskPeopleOverviewVO.java new file mode 100644 index 00000000..46f0ce80 --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectRiskPeopleOverviewVO.java @@ -0,0 +1,13 @@ +package com.ruoyi.ccdi.project.domain.vo; + +import java.util.List; +import lombok.Data; + +/** + * 风险人员总览 + */ +@Data +public class CcdiProjectRiskPeopleOverviewVO { + + private List overviewList; +} diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectTopRiskPeopleItemVO.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectTopRiskPeopleItemVO.java new file mode 100644 index 00000000..4c8736b4 --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectTopRiskPeopleItemVO.java @@ -0,0 +1,24 @@ +package com.ruoyi.ccdi.project.domain.vo; + +import lombok.Data; + +/** + * 中高风险人员项 + */ +@Data +public class CcdiProjectTopRiskPeopleItemVO { + + private String name; + + private String idNo; + + private String department; + + private String riskLevel; + + private String riskLevelType; + + private Integer modelCount; + + private String actionLabel; +} diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectTopRiskPeopleVO.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectTopRiskPeopleVO.java new file mode 100644 index 00000000..5a03a10f --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectTopRiskPeopleVO.java @@ -0,0 +1,13 @@ +package com.ruoyi.ccdi.project.domain.vo; + +import java.util.List; +import lombok.Data; + +/** + * 中高风险人员TOP10 + */ +@Data +public class CcdiProjectTopRiskPeopleVO { + + private List topRiskList; +} 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 5e86c806..b1011473 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 @@ -23,4 +23,20 @@ public interface CcdiProjectMapper extends BaseMapper { * @return 分页结果 */ Page selectProjectPage(Page page, @Param("queryDTO") CcdiProjectQueryDTO queryDTO); + + /** + * 更新项目风险人数 + * + * @param projectId 项目ID + * @param highRiskCount 高风险人数 + * @param mediumRiskCount 中风险人数 + * @param lowRiskCount 低风险人数 + * @param updateBy 更新人 + * @return 更新行数 + */ + int updateRiskCountsByProjectId(@Param("projectId") Long projectId, + @Param("highRiskCount") Integer highRiskCount, + @Param("mediumRiskCount") Integer mediumRiskCount, + @Param("lowRiskCount") Integer lowRiskCount, + @Param("updateBy") String updateBy); } diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapper.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapper.java new file mode 100644 index 00000000..df151904 --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapper.java @@ -0,0 +1,47 @@ +package com.ruoyi.ccdi.project.mapper; + +import com.ruoyi.ccdi.project.domain.CcdiProject; +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO; +import java.util.List; +import java.util.Map; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +/** + * 结果总览Mapper + */ +@Mapper +public interface CcdiProjectOverviewMapper { + + /** + * 查询仪表盘基础数据 + * + * @param projectId 项目ID + * @return 项目基础数据 + */ + CcdiProject selectDashboardBaseByProjectId(@Param("projectId") Long projectId); + + /** + * 查询风险人员总览 + * + * @param projectId 项目ID + * @return 风险人员聚合列表 + */ + List selectRiskPeopleOverviewByProjectId(@Param("projectId") Long projectId); + + /** + * 查询中高风险TOP10 + * + * @param projectId 项目ID + * @return 中高风险人员列表 + */ + List selectTopRiskPeopleByProjectId(@Param("projectId") Long projectId); + + /** + * 查询项目风险人数汇总 + * + * @param projectId 项目ID + * @return 风险人数汇总 + */ + Map selectRiskCountSummaryByProjectId(@Param("projectId") Long projectId); +} 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 new file mode 100644 index 00000000..a60f802f --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java @@ -0,0 +1,43 @@ +package com.ruoyi.ccdi.project.service; + +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO; +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO; +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO; + +/** + * 结果总览服务接口 + */ +public interface ICcdiProjectOverviewService { + + /** + * 查询风险仪表盘 + * + * @param projectId 项目ID + * @return 风险仪表盘 + */ + CcdiProjectOverviewDashboardVO getDashboard(Long projectId); + + /** + * 查询风险人员总览 + * + * @param projectId 项目ID + * @return 风险人员总览 + */ + CcdiProjectRiskPeopleOverviewVO getRiskPeopleOverview(Long projectId); + + /** + * 查询中高风险人员TOP10 + * + * @param projectId 项目ID + * @return 中高风险人员TOP10 + */ + CcdiProjectTopRiskPeopleVO getTopRiskPeople(Long projectId); + + /** + * 刷新项目风险人数 + * + * @param projectId 项目ID + * @param operator 操作人 + */ + void refreshProjectRiskCounts(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 d6535f5c..f0fae6da 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 @@ -14,6 +14,7 @@ import com.ruoyi.ccdi.project.mapper.CcdiBankTagResultMapper; import com.ruoyi.ccdi.project.mapper.CcdiBankTagRuleMapper; import com.ruoyi.ccdi.project.mapper.CcdiBankTagTaskMapper; import com.ruoyi.ccdi.project.service.ICcdiBankTagService; +import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService; import com.ruoyi.ccdi.project.service.ICcdiProjectService; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Lazy; @@ -59,6 +60,9 @@ public class CcdiBankTagServiceImpl implements ICcdiBankTagService { @Resource private ICcdiProjectService projectService; + @Resource + private ICcdiProjectOverviewService projectOverviewService; + @Resource @Qualifier("tagRuleExecutor") private Executor tagRuleExecutor; @@ -125,6 +129,8 @@ public class CcdiBankTagServiceImpl implements ICcdiBankTagService { resultMapper.insertBatch(allResults); } + projectOverviewService.refreshProjectRiskCounts(projectId, operator); + task.setStatus(STATUS_SUCCESS); task.setSuccessRuleCount(rules.size()); task.setFailedRuleCount(0); 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 new file mode 100644 index 00000000..665d4585 --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java @@ -0,0 +1,179 @@ +package com.ruoyi.ccdi.project.service.impl; + +import com.ruoyi.ccdi.project.domain.CcdiProject; +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO; +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO; +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewStatVO; +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewItemVO; +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.CcdiProjectOverviewMapper; +import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService; +import com.ruoyi.common.exception.ServiceException; +import jakarta.annotation.Resource; +import java.util.List; +import java.util.Map; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * 结果总览服务实现 + */ +@Service +public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewService { + + private static final String ACTION_LABEL = "查看详情"; + + @Resource + private CcdiProjectOverviewMapper overviewMapper; + + @Resource + private CcdiProjectMapper projectMapper; + + @Override + public CcdiProjectOverviewDashboardVO getDashboard(Long projectId) { + CcdiProject project = overviewMapper.selectDashboardBaseByProjectId(projectId); + if (project == null) { + throw new ServiceException("项目不存在"); + } + + int targetCount = defaultZero(project.getTargetCount()); + int highRiskCount = defaultZero(project.getHighRiskCount()); + int mediumRiskCount = defaultZero(project.getMediumRiskCount()); + int lowRiskCount = defaultZero(project.getLowRiskCount()); + int noRiskCount = targetCount - highRiskCount - mediumRiskCount - lowRiskCount; + + CcdiProjectOverviewDashboardVO dashboard = new CcdiProjectOverviewDashboardVO(); + dashboard.setTitle("风险仪表盘"); + dashboard.setSubtitle("风险仪表盘数据概览"); + dashboard.setStats(List.of( + buildStat("people", "总人数", targetCount), + buildStat("riskPeople", "高风险", highRiskCount), + buildStat("medium", "中风险", mediumRiskCount), + buildStat("low", "低风险", lowRiskCount), + buildStat("count", "无风险人员", noRiskCount) + )); + return dashboard; + } + + @Override + public CcdiProjectRiskPeopleOverviewVO getRiskPeopleOverview(Long projectId) { + ensureProjectExists(projectId); + + List overviewList = overviewMapper.selectRiskPeopleOverviewByProjectId(projectId) + .stream() + .map(this::buildRiskPeopleItem) + .toList(); + + CcdiProjectRiskPeopleOverviewVO overview = new CcdiProjectRiskPeopleOverviewVO(); + overview.setOverviewList(overviewList); + return overview; + } + + @Override + public CcdiProjectTopRiskPeopleVO getTopRiskPeople(Long projectId) { + ensureProjectExists(projectId); + + List topRiskList = overviewMapper.selectTopRiskPeopleByProjectId(projectId) + .stream() + .map(this::buildTopRiskPeopleItem) + .toList(); + + CcdiProjectTopRiskPeopleVO topRiskPeople = new CcdiProjectTopRiskPeopleVO(); + topRiskPeople.setTopRiskList(topRiskList); + return topRiskPeople; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void refreshProjectRiskCounts(Long projectId, String operator) { + getRequiredProject(projectId); + Map summary = overviewMapper.selectRiskCountSummaryByProjectId(projectId); + projectMapper.updateRiskCountsByProjectId( + projectId, + readCount(summary, "highRiskCount"), + readCount(summary, "mediumRiskCount"), + readCount(summary, "lowRiskCount"), + operator + ); + } + + private CcdiProjectRiskPeopleOverviewItemVO buildRiskPeopleItem(CcdiProjectEmployeeRiskAggregateVO aggregate) { + CcdiProjectRiskPeopleOverviewItemVO item = new CcdiProjectRiskPeopleOverviewItemVO(); + item.setName(aggregate.getStaffName()); + item.setIdNo(aggregate.getStaffIdCard()); + item.setDepartment(aggregate.getDeptName()); + item.setRiskCount(defaultZero(aggregate.getRuleCount())); + item.setRiskPoint(aggregate.getTopRuleName()); + item.setActionLabel(ACTION_LABEL); + return item; + } + + private CcdiProjectTopRiskPeopleItemVO buildTopRiskPeopleItem(CcdiProjectEmployeeRiskAggregateVO aggregate) { + CcdiProjectTopRiskPeopleItemVO item = new CcdiProjectTopRiskPeopleItemVO(); + item.setName(aggregate.getStaffName()); + item.setIdNo(aggregate.getStaffIdCard()); + item.setDepartment(aggregate.getDeptName()); + item.setRiskLevel(resolveRiskLevelName(aggregate.getRiskLevelCode())); + item.setRiskLevelType(resolveRiskLevelType(aggregate.getRiskLevelCode())); + item.setModelCount(defaultZero(aggregate.getModelCount())); + item.setActionLabel(ACTION_LABEL); + return item; + } + + private void ensureProjectExists(Long projectId) { + getRequiredProject(projectId); + } + + private CcdiProjectOverviewStatVO buildStat(String key, String label, Integer value) { + CcdiProjectOverviewStatVO stat = new CcdiProjectOverviewStatVO(); + stat.setKey(key); + stat.setLabel(label); + stat.setValue(value); + return stat; + } + + private Integer readCount(Map summary, String key) { + if (summary == null) { + return 0; + } + Object value = summary.get(key); + if (value == null) { + return 0; + } + if (value instanceof Number number) { + return number.intValue(); + } + throw new ServiceException("项目风险人数统计结果类型异常"); + } + + private Integer defaultZero(Integer value) { + return value == null ? 0 : value; + } + + private CcdiProject getRequiredProject(Long projectId) { + CcdiProject project = projectMapper.selectById(projectId); + if (project == null) { + throw new ServiceException("项目不存在"); + } + return project; + } + + private String resolveRiskLevelName(String riskLevelCode) { + return switch (riskLevelCode) { + case "HIGH" -> "高风险"; + case "MEDIUM" -> "中风险"; + default -> "低风险"; + }; + } + + private String resolveRiskLevelType(String riskLevelCode) { + return switch (riskLevelCode) { + case "HIGH" -> "danger"; + case "MEDIUM" -> "warning"; + default -> "info"; + }; + } +} 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 0506c078..b8131b59 100644 --- a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectMapper.xml +++ b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectMapper.xml @@ -40,4 +40,15 @@ ORDER BY p.update_time DESC + + + update ccdi_project + set high_risk_count = #{highRiskCount}, + medium_risk_count = #{mediumRiskCount}, + low_risk_count = #{lowRiskCount}, + update_by = #{updateBy}, + update_time = now() + where project_id = #{projectId} + and del_flag = '0' + diff --git a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml new file mode 100644 index 00000000..a9f60c20 --- /dev/null +++ b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + select distinct + tr.id, + 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, + coalesce(direct_staff.dept_id, statement_staff.dept_id, family_staff.dept_id) as dept_id, + tr.rule_code, + tr.rule_name, + tr.model_code + 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 + where tr.project_id = #{projectId} + and coalesce(direct_staff.id_card, statement_staff.id_card, family_staff.id_card) is not null + + + + select + agg.staff_id_card, + agg.staff_name, + agg.dept_id, + dept.dept_name, + agg.rule_count, + agg.model_count, + rule_pick.rule_code as top_rule_code, + rule_pick.rule_name as top_rule_name, + case + when agg.rule_count >= 5 then 'HIGH' + when agg.rule_count between 2 and 4 then 'MEDIUM' + else 'LOW' + end as risk_level_code, + case + when agg.rule_count >= 5 then '高风险' + when agg.rule_count between 2 and 4 then '中风险' + else '低风险' + end as risk_level_name, + case + when agg.rule_count >= 5 then 1 + when agg.rule_count between 2 and 4 then 2 + else 3 + end as risk_level_sort + from ( + select + base.staff_id_card, + max(base.staff_name) as staff_name, + max(base.dept_id) as dept_id, + count(distinct base.rule_code) as rule_count, + count(distinct base.model_code) as model_count + from ( + + ) base + group by base.staff_id_card + ) agg + left join sys_dept dept on agg.dept_id = dept.dept_id + left join ( + select + chosen.staff_id_card, + chosen.rule_code, + chosen.rule_name + from ( + select + grouped.staff_id_card, + grouped.rule_code, + grouped.rule_name, + grouped.hit_count + from ( + select + base.staff_id_card, + base.rule_code, + max(base.rule_name) as rule_name, + count(1) as hit_count + from ( + + ) base + group by base.staff_id_card, base.rule_code + ) grouped + where not exists ( + select 1 + from ( + select + base.staff_id_card, + base.rule_code, + max(base.rule_name) as rule_name, + count(1) as hit_count + from ( + + ) base + group by base.staff_id_card, base.rule_code + ) challenger + where challenger.staff_id_card = grouped.staff_id_card + and ( + challenger.hit_count > grouped.hit_count + or (challenger.hit_count = grouped.hit_count + and challenger.rule_code < grouped.rule_code) + ) + ) + ) chosen + ) rule_pick on rule_pick.staff_id_card = agg.staff_id_card + + + + + + + + + + diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerTest.java new file mode 100644 index 00000000..bd7fbd62 --- /dev/null +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerTest.java @@ -0,0 +1,94 @@ +package com.ruoyi.ccdi.project.controller; + +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO; +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO; +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO; +import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService; +import com.ruoyi.common.core.domain.AjaxResult; +import io.swagger.v3.oas.annotations.Operation; +import java.lang.reflect.Method; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CcdiProjectOverviewControllerTest { + + @InjectMocks + private CcdiProjectOverviewController controller; + + @Mock + private ICcdiProjectOverviewService overviewService; + + @Test + void shouldExposeDashboardEndpoint() throws Exception { + when(overviewService.getDashboard(40L)).thenReturn(new CcdiProjectOverviewDashboardVO()); + + AjaxResult result = controller.getDashboard(40L); + + assertEquals(200, result.get("code")); + verify(overviewService).getDashboard(40L); + + RequestMapping mapping = CcdiProjectOverviewController.class.getAnnotation(RequestMapping.class); + Method method = CcdiProjectOverviewController.class.getMethod("getDashboard", Long.class); + GetMapping getMapping = method.getAnnotation(GetMapping.class); + PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class); + Operation operation = method.getAnnotation(Operation.class); + + assertNotNull(mapping); + assertEquals("/ccdi/project/overview", mapping.value()[0]); + assertNotNull(getMapping); + assertEquals("/dashboard", getMapping.value()[0]); + assertNotNull(preAuthorize); + assertEquals("@ss.hasPermi('ccdi:project:query')", preAuthorize.value()); + assertNotNull(operation); + } + + @Test + void shouldExposeRiskPeopleEndpoint() throws Exception { + when(overviewService.getRiskPeopleOverview(40L)).thenReturn(new CcdiProjectRiskPeopleOverviewVO()); + + AjaxResult result = controller.getRiskPeople(40L); + + assertEquals(200, result.get("code")); + verify(overviewService).getRiskPeopleOverview(40L); + + Method method = CcdiProjectOverviewController.class.getMethod("getRiskPeople", Long.class); + GetMapping getMapping = method.getAnnotation(GetMapping.class); + PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class); + + assertNotNull(getMapping); + assertEquals("/risk-people", getMapping.value()[0]); + assertNotNull(preAuthorize); + assertEquals("@ss.hasPermi('ccdi:project:query')", preAuthorize.value()); + } + + @Test + void shouldExposeTopRiskPeopleEndpoint() throws Exception { + when(overviewService.getTopRiskPeople(40L)).thenReturn(new CcdiProjectTopRiskPeopleVO()); + + AjaxResult result = controller.getTopRiskPeople(40L); + + assertEquals(200, result.get("code")); + verify(overviewService).getTopRiskPeople(40L); + + Method method = CcdiProjectOverviewController.class.getMethod("getTopRiskPeople", Long.class); + GetMapping getMapping = method.getAnnotation(GetMapping.class); + PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class); + + assertNotNull(getMapping); + assertEquals("/top-risk-people", getMapping.value()[0]); + assertNotNull(preAuthorize); + assertEquals("@ss.hasPermi('ccdi:project:query')", preAuthorize.value()); + } +} 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 new file mode 100644 index 00000000..e9ed05d7 --- /dev/null +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperSqlTest.java @@ -0,0 +1,30 @@ +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.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CcdiProjectOverviewMapperSqlTest { + + @Test + void shouldContainEmployeeRiskAggregationSql() throws Exception { + String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml")); + + assertTrue(xml.contains("count(distinct base.rule_code)")); + assertTrue(xml.contains("count(distinct base.model_code)")); + assertTrue(xml.contains("when agg.rule_count >= 5 then 'HIGH'")); + assertTrue(xml.contains("when agg.rule_count between 2 and 4 then 'MEDIUM'")); + } + + @Test + void shouldAvoidWindowFunctionsForMysql57Compatibility() throws Exception { + 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); + } +} 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 new file mode 100644 index 00000000..5c8545eb --- /dev/null +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/CcdiProjectOverviewServiceStructureTest.java @@ -0,0 +1,18 @@ +package com.ruoyi.ccdi.project.service; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class CcdiProjectOverviewServiceStructureTest { + + @Test + void shouldExposeOverviewServiceMethods() throws Exception { + Class clazz = Class.forName("com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService"); + + assertNotNull(clazz.getMethod("getDashboard", Long.class)); + assertNotNull(clazz.getMethod("getRiskPeopleOverview", Long.class)); + assertNotNull(clazz.getMethod("getTopRiskPeople", Long.class)); + assertNotNull(clazz.getMethod("refreshProjectRiskCounts", Long.class, String.class)); + } +} diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImplTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImplTest.java index 28e7e204..feec55f3 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImplTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImplTest.java @@ -12,6 +12,7 @@ import com.ruoyi.ccdi.project.mapper.CcdiBankTagAnalysisMapper; import com.ruoyi.ccdi.project.mapper.CcdiBankTagResultMapper; import com.ruoyi.ccdi.project.mapper.CcdiBankTagRuleMapper; import com.ruoyi.ccdi.project.mapper.CcdiBankTagTaskMapper; +import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService; import com.ruoyi.ccdi.project.service.ICcdiProjectService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -60,6 +61,9 @@ class CcdiBankTagServiceImplTest { @Mock private ICcdiProjectService projectService; + @Mock + private ICcdiProjectOverviewService projectOverviewService; + @Mock private ProjectBankTagRebuildCoordinator coordinator; 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 new file mode 100644 index 00000000..c3a3858e --- /dev/null +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceRiskCountRefreshTest.java @@ -0,0 +1,143 @@ +package com.ruoyi.ccdi.project.service.impl; + +import com.ruoyi.ccdi.project.domain.entity.CcdiBankTagRule; +import com.ruoyi.ccdi.project.domain.entity.CcdiBankTagTask; +import com.ruoyi.ccdi.project.domain.enums.TriggerType; +import com.ruoyi.ccdi.project.domain.vo.BankTagRuleExecutionConfig; +import com.ruoyi.ccdi.project.domain.vo.BankTagStatementHitVO; +import com.ruoyi.ccdi.project.mapper.CcdiBankTagAnalysisMapper; +import com.ruoyi.ccdi.project.mapper.CcdiBankTagResultMapper; +import com.ruoyi.ccdi.project.mapper.CcdiBankTagRuleMapper; +import com.ruoyi.ccdi.project.mapper.CcdiBankTagTaskMapper; +import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService; +import com.ruoyi.ccdi.project.service.ICcdiProjectService; +import java.util.List; +import java.util.concurrent.Executor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InOrder; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CcdiBankTagServiceRiskCountRefreshTest { + + @InjectMocks + private CcdiBankTagServiceImpl service; + + @Mock + private CcdiBankTagRuleMapper ruleMapper; + + @Mock + private CcdiBankTagResultMapper resultMapper; + + @Mock + private CcdiBankTagTaskMapper taskMapper; + + @Mock + private CcdiBankTagAnalysisMapper analysisMapper; + + @Mock + private BankTagRuleConfigResolver configResolver; + + @Mock + private ICcdiProjectService projectService; + + @Mock + private ICcdiProjectOverviewService projectOverviewService; + + @Mock + private ProjectBankTagRebuildCoordinator coordinator; + + @Test + void shouldRefreshProjectRiskCountsAfterTagRebuildSuccess() { + ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run); + + CcdiBankTagRule rule = buildRule(); + BankTagRuleExecutionConfig config = buildConfig(rule); + BankTagStatementHitVO hit = buildHit(); + + doAnswer(invocation -> { + CcdiBankTagTask task = invocation.getArgument(0); + task.setId(101L); + return 1; + }).when(taskMapper).insertTask(any(CcdiBankTagTask.class)); + when(ruleMapper.selectEnabledRules(null)).thenReturn(List.of(rule)); + when(configResolver.resolve(40L, rule)).thenReturn(config); + when(analysisMapper.selectHouseOrCarExpenseStatements(40L)).thenReturn(List.of(hit)); + + service.rebuildProject(40L, null, "tester", TriggerType.MANUAL); + + InOrder inOrder = inOrder(projectService, resultMapper, projectOverviewService, taskMapper); + 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(taskMapper).updateTask(argThat(task -> "SUCCESS".equals(task.getStatus()))); + inOrder.verify(projectService).updateProjectStatus(40L, "1", "tester"); + } + + @Test + void shouldFailTaskWhenRiskCountRefreshFails() { + ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run); + + CcdiBankTagRule rule = buildRule(); + BankTagRuleExecutionConfig config = buildConfig(rule); + + doAnswer(invocation -> { + CcdiBankTagTask task = invocation.getArgument(0); + task.setId(102L); + return 1; + }).when(taskMapper).insertTask(any(CcdiBankTagTask.class)); + when(ruleMapper.selectEnabledRules(null)).thenReturn(List.of(rule)); + 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"); + + assertThrows(RuntimeException.class, + () -> service.rebuildProject(40L, null, "tester", TriggerType.MANUAL)); + + verify(taskMapper).updateTask(argThat(task -> "FAILED".equals(task.getStatus()) + && "refresh failed".equals(task.getErrorMessage()))); + verify(projectService).updateProjectStatus(40L, "0", "tester"); + } + + private CcdiBankTagRule buildRule() { + CcdiBankTagRule rule = new CcdiBankTagRule(); + rule.setModelCode("LARGE_TRANSACTION"); + rule.setModelName("大额交易"); + rule.setRuleCode("HOUSE_OR_CAR_EXPENSE"); + rule.setRuleName("房车消费支出交易"); + rule.setResultType("STATEMENT"); + return rule; + } + + private BankTagRuleExecutionConfig buildConfig(CcdiBankTagRule rule) { + BankTagRuleExecutionConfig config = new BankTagRuleExecutionConfig(); + config.setProjectId(40L); + config.setRuleMeta(rule); + return config; + } + + private BankTagStatementHitVO buildHit() { + BankTagStatementHitVO hit = new BankTagStatementHitVO(); + hit.setBankStatementId(10L); + hit.setGroupId(40); + hit.setLogId(40001); + hit.setReasonDetail("命中房车消费支出"); + return hit; + } +} 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 new file mode 100644 index 00000000..fbfc8f5f --- /dev/null +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java @@ -0,0 +1,124 @@ +package com.ruoyi.ccdi.project.service.impl; + +import com.ruoyi.ccdi.project.domain.CcdiProject; +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO; +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO; +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.CcdiProjectOverviewMapper; +import com.ruoyi.common.exception.ServiceException; +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CcdiProjectOverviewServiceImplTest { + + @InjectMocks + private CcdiProjectOverviewServiceImpl service; + + @Mock + private CcdiProjectOverviewMapper overviewMapper; + + @Mock + private CcdiProjectMapper projectMapper; + + @Test + void shouldBuildDashboardWithNoRiskCount() { + CcdiProject project = new CcdiProject(); + project.setProjectId(40L); + project.setTargetCount(100); + project.setHighRiskCount(5); + project.setMediumRiskCount(10); + project.setLowRiskCount(15); + + when(overviewMapper.selectDashboardBaseByProjectId(40L)).thenReturn(project); + + CcdiProjectOverviewDashboardVO dashboard = service.getDashboard(40L); + + assertEquals("风险仪表盘", dashboard.getTitle()); + assertEquals(70, dashboard.getStats().get(4).getValue()); + } + + @Test + void shouldMapRiskPeopleOverviewRows() { + CcdiProject project = new CcdiProject(); + project.setProjectId(40L); + when(projectMapper.selectById(40L)).thenReturn(project); + + CcdiProjectEmployeeRiskAggregateVO aggregate = new CcdiProjectEmployeeRiskAggregateVO(); + aggregate.setStaffName("李四"); + aggregate.setStaffIdCard("330000000000000001"); + aggregate.setDeptName("信息二部"); + aggregate.setRuleCount(5); + aggregate.setTopRuleName("大额单笔收入"); + when(overviewMapper.selectRiskPeopleOverviewByProjectId(40L)).thenReturn(List.of(aggregate)); + + CcdiProjectRiskPeopleOverviewVO overview = service.getRiskPeopleOverview(40L); + + assertEquals(1, overview.getOverviewList().size()); + assertEquals(5, overview.getOverviewList().getFirst().getRiskCount()); + assertEquals("大额单笔收入", overview.getOverviewList().getFirst().getRiskPoint()); + assertEquals("查看详情", overview.getOverviewList().getFirst().getActionLabel()); + } + + @Test + void shouldMapTopRiskPeopleRows() { + CcdiProject project = new CcdiProject(); + project.setProjectId(40L); + when(projectMapper.selectById(40L)).thenReturn(project); + + CcdiProjectEmployeeRiskAggregateVO aggregate = new CcdiProjectEmployeeRiskAggregateVO(); + aggregate.setStaffName("张三"); + aggregate.setStaffIdCard("330000000000000002"); + aggregate.setDeptName("信贷部"); + aggregate.setRiskLevelCode("HIGH"); + aggregate.setModelCount(8); + when(overviewMapper.selectTopRiskPeopleByProjectId(40L)).thenReturn(List.of(aggregate)); + + CcdiProjectTopRiskPeopleVO topRiskPeople = service.getTopRiskPeople(40L); + + assertEquals(1, topRiskPeople.getTopRiskList().size()); + assertEquals("高风险", topRiskPeople.getTopRiskList().getFirst().getRiskLevel()); + assertEquals("danger", topRiskPeople.getTopRiskList().getFirst().getRiskLevelType()); + assertEquals("查看详情", topRiskPeople.getTopRiskList().getFirst().getActionLabel()); + } + + @Test + void shouldThrowWhenProjectDoesNotExist() { + when(projectMapper.selectById(99L)).thenReturn(null); + + assertThrows(ServiceException.class, () -> service.getRiskPeopleOverview(99L)); + assertThrows(ServiceException.class, () -> service.getTopRiskPeople(99L)); + } + + @Test + @SuppressWarnings({"rawtypes", "unchecked"}) + void shouldConvertBigDecimalRiskCountsWhenRefreshingProjectRiskCounts() { + CcdiProject project = new CcdiProject(); + project.setProjectId(43L); + when(projectMapper.selectById(43L)).thenReturn(project); + + HashMap summary = new HashMap(); + summary.put("highRiskCount", new BigDecimal("2")); + summary.put("mediumRiskCount", new BigDecimal("1")); + summary.put("lowRiskCount", new BigDecimal("3")); + when(overviewMapper.selectRiskCountSummaryByProjectId(43L)).thenReturn(summary); + + service.refreshProjectRiskCounts(43L, "tester"); + + verify(projectMapper).updateRiskCountsByProjectId(eq(43L), eq(2), eq(1), eq(3), eq("tester")); + } +} diff --git a/docs/reports/implementation/2026-03-19-results-overview-risk-api-backend-implementation.md b/docs/reports/implementation/2026-03-19-results-overview-risk-api-backend-implementation.md new file mode 100644 index 00000000..43e66d9a --- /dev/null +++ b/docs/reports/implementation/2026-03-19-results-overview-risk-api-backend-implementation.md @@ -0,0 +1,31 @@ +# 结果总览风险接口后端实施记录 + +## 本次改动 + +- 新增结果总览专用 Controller、Service、Mapper 与 VO +- 新增风险仪表盘、风险人员总览、中高风险人员 TOP10 三个后端接口 +- 新增员工维度风险聚合 SQL,按命中去重规则数划分高、中、低风险 +- 在项目流水标签重算成功后刷新并回写项目高、中、低风险人数 +- 同步补充结构测试、SQL 结构测试、服务层测试、控制器测试以及打标回写测试 +- 联调阶段根据真实环境反馈,将代表性规则选择 SQL 从窗口函数改为 MySQL 5.7 兼容的 `not exists` 实现 +- 联调阶段根据真实重算反馈,将风险人数汇总结果读取逻辑从 `Integer` 强转改为 `Number.intValue()`,兼容 MySQL 聚合返回 `BigDecimal` + +## 未包含内容 + +- 未扩展风险模型区接口 +- 未扩展风险明细区接口 +- 未增加设计范围外的导出、降级或补丁逻辑 + +## 涉及模块 + +- `ccdi-project` +- `docs/tests/records` +- `docs/reports/implementation` + +## 验证情况 + +- 计划内核心验证 11 个测试全部通过 +- 受影响回归用例 8 个测试全部通过 +- 真实后端联调已确认 3 个结果总览接口可访问,其中风险人员总览与 TOP10 可返回真实数据 +- 真实重算 `projectId=43` 已确认任务成功并写回高风险人数 +- 修复 MySQL 5.7 兼容问题与 `BigDecimal` 取值问题后,完整验证总计 21 个测试通过,详见 `docs/tests/records/2026-03-19-results-overview-risk-api-backend-verification.md` diff --git a/docs/tests/records/2026-03-19-results-overview-risk-api-backend-verification.md b/docs/tests/records/2026-03-19-results-overview-risk-api-backend-verification.md new file mode 100644 index 00000000..8bb3cb8a --- /dev/null +++ b/docs/tests/records/2026-03-19-results-overview-risk-api-backend-verification.md @@ -0,0 +1,54 @@ +# 结果总览风险接口后端验证记录 + +## 验证范围 + +- 风险仪表盘接口 +- 风险人员总览接口 +- 中高风险人员 TOP10 接口 +- 打标完成后项目风险人数回写 + +## 验证命令 + +```bash +mvn test -pl ccdi-project -Dtest=CcdiProjectOverviewServiceStructureTest,CcdiProjectOverviewMapperSqlTest,CcdiProjectOverviewServiceImplTest,CcdiProjectOverviewControllerTest,CcdiBankTagServiceRiskCountRefreshTest +``` + +## 验证结果 + +- 2026-03-19 14:54:00 执行计划内核心验证命令,`CcdiProjectOverviewServiceStructureTest`、`CcdiProjectOverviewMapperSqlTest`、`CcdiProjectOverviewServiceImplTest`、`CcdiProjectOverviewControllerTest`、`CcdiBankTagServiceRiskCountRefreshTest` 共 11 个测试全部通过 +- 2026-03-19 14:55:24 补充执行受影响旧用例 `CcdiBankTagServiceImplTest`,8 个测试全部通过 +- 2026-03-19 15:02 在真实联调环境发现 `risk-people` 与 `top-risk-people` 接口报错,根因是数据库版本为 MySQL 5.7.44,不支持 `row_number() over (...)` +- 2026-03-19 15:03 新增 MySQL 5.7 兼容性回归测试,改为 `not exists` 方式选择代表性规则后,`CcdiProjectOverviewMapperSqlTest` 重新通过 +- 2026-03-19 15:04 重新执行结果总览相关测试组,10 个测试全部通过 +- 2026-03-19 15:14 在真实重算 `projectId=43` 时发现回写链路报错,根因是风险人数汇总 map 中的聚合值被映射为 `BigDecimal` +- 2026-03-19 15:15 新增 `BigDecimal` 场景回归测试,服务层改为按 `Number.intValue()` 读取风险人数,相关测试重新通过 +- 合并验证命令如下: + +```bash +mvn test -pl ccdi-project -Dtest=CcdiProjectOverviewServiceStructureTest,CcdiProjectOverviewMapperSqlTest,CcdiProjectOverviewServiceImplTest,CcdiProjectOverviewControllerTest,CcdiBankTagServiceRiskCountRefreshTest,CcdiBankTagServiceImplTest +``` + +- 2026-03-19 15:16:54 在修复 `BigDecimal` 取值问题后重新执行完整验证,`CcdiProjectOverviewServiceStructureTest`、`CcdiProjectOverviewMapperSqlTest`、`CcdiProjectOverviewServiceImplTest`、`CcdiProjectOverviewControllerTest`、`CcdiBankTagServiceRiskCountRefreshTest`、`CcdiBankTagServiceImplTest` 共 21 个测试全部通过 +- 合并验证结果:21 个测试全部通过,0 failure,0 error + +## 真实接口验证 + +- 登录接口:`POST /login/test`,返回 `200`,成功拿到 token +- 仪表盘接口:`GET /ccdi/project/overview/dashboard?projectId=42`,返回 `200` +- 风险人员总览接口:`GET /ccdi/project/overview/risk-people?projectId=42`,返回 `200`,当前返回 1 条员工风险数据 +- 中高风险 TOP10 接口:`GET /ccdi/project/overview/top-risk-people?projectId=42`,返回 `200`,当前返回 1 条中风险员工数据 +- 2026-03-19 15:15:40 触发 `POST /ccdi/project/tags/rebuild` 对 `projectId=43` 的真实手动重算,任务 `id=22` 最终 `SUCCESS` +- 重算后 `ccdi_project.project_id=43` 更新为:`high_risk_count=2`、`medium_risk_count=0`、`low_risk_count=0` +- 重算后接口验证: + - `GET /ccdi/project/overview/risk-people?projectId=43` 返回 `200`,共 2 条高风险员工数据 + - `GET /ccdi/project/overview/top-risk-people?projectId=43` 返回 `200`,共 2 条高风险 TOP 数据 + - `GET /ccdi/project/overview/dashboard?projectId=43` 返回 `200`,高风险人数更新为 `2` +- 说明:`projectId=43` 的 `target_count` 当前仍为 `0`,因此仪表盘中的“无风险人员”计算结果为 `-2`;这反映的是项目基础人数未维护,与本次风险人数回写链路无关 + +## 结论 + +- 结果总览后端接口、员工风险聚合 SQL、打标后风险人数回写链路已按实施计划完成 +- 已额外修复 MySQL 5.7 环境下的窗口函数兼容问题 +- 已额外修复真实重算场景下的 `BigDecimal` 风险人数取值问题 +- 只读查询接口与风险人数回写链路均已通过真实后端联调验证 +- 当前剩余问题是部分项目的 `target_count` 基础数据为 0,会导致仪表盘“无风险人员”出现负数