实现结果总览风险接口并完成回写联调
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<CcdiProjectOverviewStatVO> stats;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.ruoyi.ccdi.project.domain.vo;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 风险人员总览
|
||||
*/
|
||||
@Data
|
||||
public class CcdiProjectRiskPeopleOverviewVO {
|
||||
|
||||
private List<CcdiProjectRiskPeopleOverviewItemVO> overviewList;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<CcdiProjectTopRiskPeopleItemVO> topRiskList;
|
||||
}
|
||||
@@ -23,4 +23,20 @@ public interface CcdiProjectMapper extends BaseMapper<CcdiProject> {
|
||||
* @return 分页结果
|
||||
*/
|
||||
Page<CcdiProjectVO> selectProjectPage(Page<CcdiProjectVO> 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);
|
||||
}
|
||||
|
||||
@@ -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<CcdiProjectEmployeeRiskAggregateVO> selectRiskPeopleOverviewByProjectId(@Param("projectId") Long projectId);
|
||||
|
||||
/**
|
||||
* 查询中高风险TOP10
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @return 中高风险人员列表
|
||||
*/
|
||||
List<CcdiProjectEmployeeRiskAggregateVO> selectTopRiskPeopleByProjectId(@Param("projectId") Long projectId);
|
||||
|
||||
/**
|
||||
* 查询项目风险人数汇总
|
||||
*
|
||||
* @param projectId 项目ID
|
||||
* @return 风险人数汇总
|
||||
*/
|
||||
Map<String, Object> selectRiskCountSummaryByProjectId(@Param("projectId") Long projectId);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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<CcdiProjectRiskPeopleOverviewItemVO> 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<CcdiProjectTopRiskPeopleItemVO> 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<String, Object> 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<String, Object> 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";
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -40,4 +40,15 @@
|
||||
</where>
|
||||
ORDER BY p.update_time DESC
|
||||
</select>
|
||||
|
||||
<update id="updateRiskCountsByProjectId">
|
||||
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'
|
||||
</update>
|
||||
</mapper>
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewMapper">
|
||||
|
||||
<resultMap id="EmployeeRiskAggregateResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO">
|
||||
<result property="staffIdCard" column="staff_id_card"/>
|
||||
<result property="staffName" column="staff_name"/>
|
||||
<result property="deptId" column="dept_id"/>
|
||||
<result property="deptName" column="dept_name"/>
|
||||
<result property="ruleCount" column="rule_count"/>
|
||||
<result property="modelCount" column="model_count"/>
|
||||
<result property="topRuleCode" column="top_rule_code"/>
|
||||
<result property="topRuleName" column="top_rule_name"/>
|
||||
<result property="riskLevelCode" column="risk_level_code"/>
|
||||
<result property="riskLevelName" column="risk_level_name"/>
|
||||
<result property="riskLevelSort" column="risk_level_sort"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="resolvedEmployeeRiskBaseSql">
|
||||
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
|
||||
</sql>
|
||||
|
||||
<sql id="employeeRiskAggregateSql">
|
||||
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 (
|
||||
<include refid="resolvedEmployeeRiskBaseSql"/>
|
||||
) 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 (
|
||||
<include refid="resolvedEmployeeRiskBaseSql"/>
|
||||
) 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 (
|
||||
<include refid="resolvedEmployeeRiskBaseSql"/>
|
||||
) 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
|
||||
</sql>
|
||||
|
||||
<select id="selectDashboardBaseByProjectId" resultType="com.ruoyi.ccdi.project.domain.CcdiProject">
|
||||
select
|
||||
project_id,
|
||||
project_name,
|
||||
target_count,
|
||||
high_risk_count,
|
||||
medium_risk_count,
|
||||
low_risk_count
|
||||
from ccdi_project
|
||||
where project_id = #{projectId}
|
||||
and del_flag = '0'
|
||||
</select>
|
||||
|
||||
<select id="selectRiskPeopleOverviewByProjectId" resultMap="EmployeeRiskAggregateResultMap">
|
||||
<include refid="employeeRiskAggregateSql"/>
|
||||
order by risk_level_sort asc, model_count desc, rule_count desc, staff_id_card asc
|
||||
</select>
|
||||
|
||||
<select id="selectTopRiskPeopleByProjectId" resultMap="EmployeeRiskAggregateResultMap">
|
||||
<include refid="employeeRiskAggregateSql"/>
|
||||
where rule_count >= 2
|
||||
order by risk_level_sort asc, model_count desc, rule_count desc, staff_id_card asc
|
||||
limit 10
|
||||
</select>
|
||||
|
||||
<select id="selectRiskCountSummaryByProjectId" resultType="map">
|
||||
select
|
||||
coalesce(sum(case when agg.rule_count >= 5 then 1 else 0 end), 0) as highRiskCount,
|
||||
coalesce(sum(case when agg.rule_count between 2 and 4 then 1 else 0 end), 0) as mediumRiskCount,
|
||||
coalesce(sum(case when agg.rule_count = 1 then 1 else 0 end), 0) as lowRiskCount
|
||||
from (
|
||||
select
|
||||
base.staff_id_card,
|
||||
count(distinct base.rule_code) as rule_count
|
||||
from (
|
||||
<include refid="resolvedEmployeeRiskBaseSql"/>
|
||||
) base
|
||||
group by base.staff_id_card
|
||||
) agg
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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`
|
||||
@@ -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,会导致仪表盘“无风险人员”出现负数
|
||||
Reference in New Issue
Block a user