实现结果总览排除可疑预警

This commit is contained in:
wjj
2026-07-09 14:49:02 +08:00
parent 9c02812675
commit 3f3e9268f2
22 changed files with 1549 additions and 32 deletions

View File

@@ -7,6 +7,7 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExternalRiskModelPeopleQuery
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskExclusionSaveDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectExternalPersonWarningExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskModelPeopleExcel;
@@ -33,8 +34,10 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@@ -67,6 +70,18 @@ public class CcdiProjectOverviewController extends BaseController {
return AjaxResult.success(dashboard);
}
/**
* 排除单条可疑预警
*/
@PostMapping("/risk-exclusions")
@Operation(summary = "排除单条可疑预警")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult excludeRisk(@Validated @RequestBody CcdiProjectRiskExclusionSaveDTO dto) {
projectAccessService.assertCanOperate(dto.getProjectId());
overviewService.excludeRisk(dto);
return AjaxResult.success("排除成功");
}
/**
* 查询风险人员总览
*/

View File

@@ -0,0 +1,36 @@
package com.ruoyi.ccdi.project.domain.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* 项目结果页排除可疑保存DTO
*/
@Data
public class CcdiProjectRiskExclusionSaveDTO {
/** 项目ID */
@NotNull(message = "项目ID不能为空")
private Long projectId;
/** 人员证件号 */
private String staffIdCard;
/** 规则编码 */
@NotBlank(message = "规则编码不能为空")
private String ruleCode;
/** 排除类型STATEMENT/OBJECT */
@NotBlank(message = "排除类型不能为空")
private String exclusionType;
/** 流水ID */
private Long bankStatementId;
/** 排除原因 */
@NotBlank(message = "排除原因不能为空")
@Size(max = 1000, message = "排除原因不能超过1000个字符")
private String excludeReason;
}

View File

@@ -0,0 +1,58 @@
package com.ruoyi.ccdi.project.domain.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 项目结果页排除可疑记录
*/
@Data
@TableName("ccdi_project_risk_exclusion")
public class CcdiProjectRiskExclusion implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 主键ID */
@TableId(type = IdType.AUTO)
private Long id;
/** 项目ID */
private Long projectId;
/** 人员证件号 */
private String staffIdCard;
/** 规则编码 */
private String ruleCode;
/** 排除类型STATEMENT/OBJECT */
private String exclusionType;
/** 流水ID */
private Long bankStatementId;
/** 排除原因 */
private String excludeReason;
/** 创建者 */
private String createBy;
/** 创建时间 */
private Date createTime;
/** 更新者 */
private String updateBy;
/** 更新时间 */
private Date updateTime;
/** 备注 */
private String remark;
}

View File

@@ -12,6 +12,8 @@ public class CcdiProjectPersonAnalysisObjectRecordVO {
private String modelCode;
private String ruleCode;
private String title;
private String subtitle;

View File

@@ -0,0 +1,27 @@
package com.ruoyi.ccdi.project.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.ccdi.project.domain.entity.CcdiProjectRiskExclusion;
import org.apache.ibatis.annotations.Param;
/**
* 项目结果页排除可疑记录 Mapper
*/
public interface CcdiProjectRiskExclusionMapper extends BaseMapper<CcdiProjectRiskExclusion> {
/**
* 幂等写入排除记录
*
* @param exclusion 排除记录
* @return 写入条数
*/
int upsertExclusion(@Param("exclusion") CcdiProjectRiskExclusion exclusion);
/**
* 查询待排除命中是否存在
*
* @param exclusion 排除记录
* @return 命中数量
*/
int countExistingRiskHit(@Param("exclusion") CcdiProjectRiskExclusion exclusion);
}

View File

@@ -7,6 +7,7 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExternalRiskModelPeopleQuery
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskExclusionSaveDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel;
@@ -42,6 +43,13 @@ public interface ICcdiProjectOverviewService {
*/
CcdiProjectOverviewDashboardVO getDashboard(Long projectId);
/**
* 排除单条可疑预警
*
* @param dto 排除参数
*/
void excludeRisk(CcdiProjectRiskExclusionSaveDTO dto);
/**
* 查询风险人员总览
*

View File

@@ -10,6 +10,7 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExternalRiskModelPeopleQuery
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskExclusionSaveDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel;
@@ -18,6 +19,7 @@ import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskModelPeopleExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskPeopleOverviewExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.entity.CcdiProjectOverviewEmployeeResult;
import com.ruoyi.ccdi.project.domain.entity.CcdiProjectRiskExclusion;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountPageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
@@ -57,14 +59,18 @@ import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
import com.ruoyi.ccdi.project.mapper.CcdiBankTagResultMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewEmployeeResultMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectRiskExclusionMapper;
import com.ruoyi.ccdi.project.constants.CcdiProjectStatusConstants;
import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.annotation.Resource;
import java.io.IOException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -80,6 +86,10 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
private static final String ACTION_LABEL = "查看详情";
private static final String EXCLUSION_TYPE_STATEMENT = "STATEMENT";
private static final String EXCLUSION_TYPE_OBJECT = "OBJECT";
@Resource
private CcdiProjectOverviewMapper overviewMapper;
@@ -98,6 +108,9 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
@Resource
private CcdiBankTagResultMapper bankTagResultMapper;
@Resource
private CcdiProjectRiskExclusionMapper riskExclusionMapper;
@Resource
private CcdiProjectOverviewEmployeeResultBuilder overviewEmployeeResultBuilder;
@@ -133,6 +146,24 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
return dashboard;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void excludeRisk(CcdiProjectRiskExclusionSaveDTO dto) {
CcdiProjectRiskExclusion exclusion = buildRiskExclusion(dto);
CcdiProject project = getRequiredProject(exclusion.getProjectId());
if (CcdiProjectStatusConstants.ARCHIVED.equals(project.getStatus()) || Integer.valueOf(1).equals(project.getIsArchived())) {
throw new ServiceException("已归档项目暂不允许排除可疑");
}
if (CcdiProjectStatusConstants.TAGGING.equals(project.getStatus())) {
throw new ServiceException("项目正在打标中,暂不允许排除可疑");
}
if (riskExclusionMapper.countExistingRiskHit(exclusion) <= 0) {
throw new ServiceException("未找到可排除的预警命中");
}
riskExclusionMapper.upsertExclusion(exclusion);
refreshOverviewEmployeeResults(exclusion.getProjectId(), exclusion.getUpdateBy());
}
@Override
public CcdiProjectRiskPeopleOverviewVO getRiskPeopleOverview(CcdiProjectRiskPeopleQueryDTO queryDTO) {
Long projectId = queryDTO.getProjectId();
@@ -188,6 +219,9 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
queryDTO.getStaffIdCard()
));
attachStatementHitTags(statementRows, queryDTO.getProjectId());
statementRows = statementRows.stream()
.filter(row -> row.getHitTags() != null && !row.getHitTags().isEmpty())
.toList();
normalizeObjectRows(objectRows);
CcdiProjectPersonAnalysisDetailVO detail = new CcdiProjectPersonAnalysisDetailVO();
@@ -608,6 +642,60 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
getRequiredProject(projectId);
}
private CcdiProjectRiskExclusion buildRiskExclusion(CcdiProjectRiskExclusionSaveDTO dto) {
if (dto == null) {
throw new ServiceException("排除参数不能为空");
}
String exclusionType = normalizeExclusionType(dto.getExclusionType());
String ruleCode = normalizeRequired(dto.getRuleCode(), "规则编码不能为空").toUpperCase();
String excludeReason = normalizeRequired(dto.getExcludeReason(), "排除原因不能为空");
if (excludeReason.length() > 1000) {
throw new ServiceException("排除原因不能超过1000个字符");
}
CcdiProjectRiskExclusion exclusion = new CcdiProjectRiskExclusion();
exclusion.setProjectId(dto.getProjectId());
exclusion.setRuleCode(ruleCode);
exclusion.setExclusionType(exclusionType);
exclusion.setExcludeReason(excludeReason);
exclusion.setStaffIdCard(normalizeBlankToNull(dto.getStaffIdCard()));
exclusion.setBankStatementId(dto.getBankStatementId());
if (EXCLUSION_TYPE_STATEMENT.equals(exclusionType) && exclusion.getBankStatementId() == null) {
throw new ServiceException("流水型排除必须指定流水ID");
}
if (EXCLUSION_TYPE_OBJECT.equals(exclusionType) && (exclusion.getStaffIdCard() == null || exclusion.getStaffIdCard().isBlank())) {
throw new ServiceException("对象型排除必须指定人员证件号");
}
String operator = SecurityUtils.getUsername();
Date now = new Date();
exclusion.setCreateBy(operator);
exclusion.setCreateTime(now);
exclusion.setUpdateBy(operator);
exclusion.setUpdateTime(now);
return exclusion;
}
private String normalizeExclusionType(String exclusionType) {
String type = normalizeRequired(exclusionType, "排除类型不能为空").toUpperCase();
if (!EXCLUSION_TYPE_STATEMENT.equals(type) && !EXCLUSION_TYPE_OBJECT.equals(type)) {
throw new ServiceException("排除类型不正确");
}
return type;
}
private String normalizeRequired(String value, String message) {
if (value == null || value.isBlank()) {
throw new ServiceException(message);
}
return value.trim();
}
private String normalizeBlankToNull(String value) {
if (value == null || value.isBlank()) {
return null;
}
return value.trim();
}
private void normalizeRiskModelPeopleQuery(CcdiProjectRiskModelPeopleQueryDTO queryDTO) {
if (queryDTO.getMatchMode() == null || queryDTO.getMatchMode().isBlank()) {
queryDTO.setMatchMode("ANY");

View File

@@ -55,6 +55,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
from ccdi_bank_statement_tag_result
where project_id = #{projectId}
and bank_statement_id is not null
and not exists (
select 1
from ccdi_project_risk_exclusion ex
where ex.project_id = ccdi_bank_statement_tag_result.project_id
and ex.rule_code = ccdi_bank_statement_tag_result.rule_code
and ex.exclusion_type = 'STATEMENT'
and ex.bank_statement_id = ccdi_bank_statement_tag_result.bank_statement_id
)
and bank_statement_id IN
<foreach collection="bankStatementIds" item="item" open="(" separator="," close=")">
#{item}

View File

@@ -79,6 +79,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
on dept.dept_id = coalesce(direct_staff.dept_id, statement_staff.dept_id, family_staff.dept_id)
where tr.project_id = #{projectId}
and coalesce(direct_staff.id_card, statement_staff.id_card, family_staff.id_card) is not null
and not exists (
select 1
from ccdi_project_risk_exclusion ex
where ex.project_id = tr.project_id
and ex.rule_code = tr.rule_code
and (
(ex.exclusion_type = 'STATEMENT'
and ex.bank_statement_id = tr.bank_statement_id)
or (ex.exclusion_type = 'OBJECT'
and ex.staff_id_card = coalesce(direct_staff.id_card, statement_staff.id_card, family_staff.id_card))
)
)
</sql>
<delete id="deleteByProjectId">

View File

@@ -187,6 +187,18 @@
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
and not exists (
select 1
from ccdi_project_risk_exclusion ex
where ex.project_id = tr.project_id
and ex.rule_code = tr.rule_code
and (
(ex.exclusion_type = 'STATEMENT'
and ex.bank_statement_id = tr.bank_statement_id)
or (ex.exclusion_type = 'OBJECT'
and ex.staff_id_card = coalesce(direct_staff.id_card, statement_staff.id_card, family_staff.id_card))
)
)
</sql>
<sql id="employeeRiskAggregateSql">
@@ -662,6 +674,14 @@
and trim(bs.CUSTOMER_ACCOUNT_NAME) != ''
and counter_intermediary.name = trim(bs.CUSTOMER_ACCOUNT_NAME)
where trim(ifnull(bs.LE_ACCOUNT_NAME, '')) != trim(ifnull(bs.CUSTOMER_ACCOUNT_NAME, ''))
and not exists (
select 1
from ccdi_project_risk_exclusion ex
where ex.project_id = tr.project_id
and ex.rule_code = tr.rule_code
and ex.exclusion_type = 'STATEMENT'
and ex.bank_statement_id = tr.bank_statement_id
)
union all
@@ -689,6 +709,14 @@
and tr.object_type = 'EXTERNAL_CERT_NO'
and tr.object_key = subject.cert_no
and tr.model_code in <include refid="externalModelCodeFilterSql"/>
where not exists (
select 1
from ccdi_project_risk_exclusion ex
where ex.project_id = tr.project_id
and ex.rule_code = tr.rule_code
and ex.exclusion_type = 'OBJECT'
and ex.staff_id_card = subject.cert_no
)
</sql>
<sql id="externalPersonAggregateSql">
@@ -972,6 +1000,14 @@
from ccdi_bank_statement_tag_result tr
where tr.project_id = #{query.projectId}
and tr.bank_statement_id is not null
and not exists (
select 1
from ccdi_project_risk_exclusion ex
where ex.project_id = tr.project_id
and ex.rule_code = tr.rule_code
and ex.exclusion_type = 'STATEMENT'
and ex.bank_statement_id = tr.bank_statement_id
)
<if test="query.modelCode != null and query.modelCode != ''">
and tr.model_code = #{query.modelCode}
</if>
@@ -1213,6 +1249,14 @@
where model_filter.project_id = #{query.projectId}
and model_filter.bank_statement_id = final_result.bankStatementId
and model_filter.model_code = #{query.modelCode}
and not exists (
select 1
from ccdi_project_risk_exclusion ex
where ex.project_id = model_filter.project_id
and ex.rule_code = model_filter.rule_code
and ex.exclusion_type = 'STATEMENT'
and ex.bank_statement_id = model_filter.bank_statement_id
)
)
</if>
</sql>
@@ -1305,6 +1349,14 @@
from ccdi_bank_statement_tag_result tr
where tr.project_id = #{query.projectId}
and tr.bank_statement_id is not null
and not exists (
select 1
from ccdi_project_risk_exclusion ex
where ex.project_id = tr.project_id
and ex.rule_code = tr.rule_code
and ex.exclusion_type = 'STATEMENT'
and ex.bank_statement_id = tr.bank_statement_id
)
group by tr.bank_statement_id
) tag_result on tag_result.bank_statement_id = final_result.bankStatementId
<include refid="suspiciousTransactionFilterSql"/>
@@ -1534,6 +1586,7 @@
<select id="selectPersonAnalysisObjectRows" resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisObjectRecordVO">
select
max(tr.model_code) as modelCode,
tr.rule_code as ruleCode,
coalesce(max(staff.name), max(relation.relation_name), max(tr.object_key), max(tr.object_type)) as title,
max(case
when tr.object_type = 'STAFF_ID_CARD' then '员工对象'
@@ -1550,6 +1603,14 @@
and tr.object_key = relation.relation_cert_no
where tr.project_id = #{projectId}
and tr.bank_statement_id is null
and not exists (
select 1
from ccdi_project_risk_exclusion ex
where ex.project_id = tr.project_id
and ex.rule_code = tr.rule_code
and ex.exclusion_type = 'OBJECT'
and ex.staff_id_card = #{staffIdCard}
)
and (
tr.object_key = #{staffIdCard}
or exists (

View File

@@ -0,0 +1,79 @@
<?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.CcdiProjectRiskExclusionMapper">
<resultMap id="CcdiProjectRiskExclusionMap"
type="com.ruoyi.ccdi.project.domain.entity.CcdiProjectRiskExclusion">
<id property="id" column="id"/>
<result property="projectId" column="project_id"/>
<result property="staffIdCard" column="staff_id_card"/>
<result property="ruleCode" column="rule_code"/>
<result property="exclusionType" column="exclusion_type"/>
<result property="bankStatementId" column="bank_statement_id"/>
<result property="excludeReason" column="exclude_reason"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
<result property="remark" column="remark"/>
</resultMap>
<insert id="upsertExclusion">
insert into ccdi_project_risk_exclusion (
project_id, staff_id_card, rule_code, exclusion_type, bank_statement_id,
exclude_reason, create_by, create_time, update_by, update_time, remark
) values (
#{exclusion.projectId}, #{exclusion.staffIdCard}, #{exclusion.ruleCode},
#{exclusion.exclusionType}, #{exclusion.bankStatementId}, #{exclusion.excludeReason},
#{exclusion.createBy}, #{exclusion.createTime}, #{exclusion.updateBy},
#{exclusion.updateTime}, #{exclusion.remark}
)
on duplicate key update
exclude_reason = values(exclude_reason),
update_by = values(update_by),
update_time = values(update_time),
remark = values(remark)
</insert>
<select id="countExistingRiskHit" resultType="int">
select count(1)
from ccdi_bank_statement_tag_result tr
where tr.project_id = #{exclusion.projectId}
and tr.rule_code = #{exclusion.ruleCode}
<choose>
<when test="exclusion.exclusionType == 'STATEMENT'">
and tr.bank_statement_id = #{exclusion.bankStatementId}
</when>
<otherwise>
and (
tr.object_key = #{exclusion.staffIdCard}
or exists (
select 1
from ccdi_bank_statement bs
where bs.bank_statement_id = tr.bank_statement_id
and bs.cret_no = #{exclusion.staffIdCard}
)
or exists (
select 1
from ccdi_staff_fmy_relation relation
where relation.status = 1
and relation.person_id = #{exclusion.staffIdCard}
and relation.relation_cert_no = tr.object_key
)
or exists (
select 1
from ccdi_staff_fmy_relation relation
inner join ccdi_bank_statement bs
on bs.cret_no = relation.relation_cert_no
where relation.status = 1
and relation.person_id = #{exclusion.staffIdCard}
and bs.bank_statement_id = tr.bank_statement_id
)
)
</otherwise>
</choose>
</select>
</mapper>