修复涉疑交易筛选与标签错乱

This commit is contained in:
wjj
2026-07-16 18:46:51 +08:00
parent c3b56bdf2a
commit e0188b5e5b
24 changed files with 584 additions and 106 deletions

View File

@@ -72,9 +72,13 @@ public class CcdiBankStatementController extends BaseController {
*/
@GetMapping("/detail/{bankStatementId}")
@Operation(summary = "查询流水详情")
public AjaxResult getDetail(@PathVariable Long bankStatementId) {
public AjaxResult getDetail(@PathVariable Long bankStatementId, String modelCode, String suspiciousType) {
projectAccessService.assertCanReadByBankStatementId(bankStatementId);
CcdiBankStatementDetailVO detail = bankStatementService.getStatementDetail(bankStatementId);
CcdiBankStatementDetailVO detail = bankStatementService.getStatementDetail(
bankStatementId,
modelCode,
suspiciousType
);
return AjaxResult.success(detail);
}

View File

@@ -308,9 +308,12 @@ public class CcdiProjectOverviewController extends BaseController {
@PostMapping("/risk-details/export")
@Operation(summary = "导出风险明细")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public void exportRiskDetails(HttpServletResponse response, Long projectId) {
projectAccessService.assertCanRead(projectId);
overviewService.exportRiskDetails(response, projectId);
public void exportRiskDetails(
HttpServletResponse response,
CcdiProjectSuspiciousTransactionQueryDTO queryDTO
) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
overviewService.exportRiskDetails(response, queryDTO);
}
/**

View File

@@ -8,6 +8,9 @@ import lombok.Data;
@Data
public class CcdiBankStatementHitTagVO {
/** 模型编码 */
private String modelCode;
/** 规则编码 */
private String ruleCode;

View File

@@ -3,6 +3,8 @@ package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* 涉疑交易明细行
@@ -14,6 +16,14 @@ public class CcdiProjectSuspiciousTransactionItemVO {
private String trxDate;
private String leAccountNo;
private String leAccountName;
private String customerAccountName;
private String customerAccountNo;
private String suspiciousPersonName;
private String relatedPersonName;
@@ -35,4 +45,6 @@ public class CcdiProjectSuspiciousTransactionItemVO {
private Boolean hasNameListHit;
private String nameListHitType;
private List<CcdiBankStatementHitTagVO> hitTags = new ArrayList<>();
}

View File

@@ -26,11 +26,15 @@ public interface CcdiBankTagResultMapper extends BaseMapper<CcdiBankTagResult> {
*
* @param projectId 项目ID
* @param bankStatementIds 流水ID列表
* @param modelCode 模型编码
* @param suspiciousType 预警类型
* @return 命中的异常标签列表
*/
List<CcdiBankStatementHitTagVO> selectStatementTagsByProjectAndStatementIds(
@Param("projectId") Long projectId,
@Param("bankStatementIds") List<Long> bankStatementIds
@Param("bankStatementIds") List<Long> bankStatementIds,
@Param("modelCode") String modelCode,
@Param("suspiciousType") String suspiciousType
);
/**

View File

@@ -49,4 +49,18 @@ public interface ICcdiBankStatementService {
* @return 详情
*/
CcdiBankStatementDetailVO getStatementDetail(Long bankStatementId);
/**
* 按涉疑交易筛选范围查询流水详情
*
* @param bankStatementId 流水ID
* @param modelCode 模型编码
* @param suspiciousType 预警类型
* @return 详情
*/
CcdiBankStatementDetailVO getStatementDetail(
Long bankStatementId,
String modelCode,
String suspiciousType
);
}

View File

@@ -213,6 +213,18 @@ public interface ICcdiProjectOverviewService {
default void exportRiskDetails(HttpServletResponse response, Long projectId) {
}
/**
* 按涉疑交易筛选范围统一导出风险明细
*
* @param response 响应流
* @param queryDTO 涉疑交易筛选条件
*/
default void exportRiskDetails(
HttpServletResponse response,
CcdiProjectSuspiciousTransactionQueryDTO queryDTO
) {
}
/**
* 一键导出结果总览报告
*

View File

@@ -32,6 +32,8 @@ public class CcdiBankStatementServiceImpl implements ICcdiBankStatementService {
private static final Set<String> ALLOWED_TAB_TYPES = Set.of("all", "in", "out");
private static final Set<String> ALLOWED_ORDER_DIRECTIONS = Set.of("asc", "desc");
private static final Set<String> ALLOWED_SUSPICIOUS_TYPES =
Set.of("ALL", "MODEL_RULE", "EXTERNAL_PERSON", "NAME_LIST");
@Resource
private CcdiBankStatementMapper bankStatementMapper;
@@ -69,13 +71,24 @@ public class CcdiBankStatementServiceImpl implements ICcdiBankStatementService {
@Override
public CcdiBankStatementDetailVO getStatementDetail(Long bankStatementId) {
return getStatementDetail(bankStatementId, null, null);
}
@Override
public CcdiBankStatementDetailVO getStatementDetail(
Long bankStatementId,
String modelCode,
String suspiciousType
) {
CcdiBankStatementDetailVO detail = bankStatementMapper.selectStatementDetailById(bankStatementId);
if (detail == null || detail.getProjectId() == null || detail.getBankStatementId() == null) {
return detail;
}
Map<Long, List<CcdiBankStatementHitTagVO>> hitTagMap = loadHitTagMap(
detail.getProjectId(),
List.of(detail.getBankStatementId())
detail.getProjectId(),
List.of(detail.getBankStatementId()),
normalizeUpperCase(modelCode),
normalizeSuspiciousType(suspiciousType)
);
detail.setHitTags(new ArrayList<>(hitTagMap.getOrDefault(detail.getBankStatementId(), Collections.emptyList())));
return detail;
@@ -93,18 +106,33 @@ public class CcdiBankStatementServiceImpl implements ICcdiBankStatementService {
if (bankStatementIds.isEmpty()) {
return;
}
Map<Long, List<CcdiBankStatementHitTagVO>> hitTagMap = loadHitTagMap(projectId, bankStatementIds);
Map<Long, List<CcdiBankStatementHitTagVO>> hitTagMap = loadHitTagMap(
projectId,
bankStatementIds,
null,
null
);
rows.forEach(row -> row.setHitTags(new ArrayList<>(
hitTagMap.getOrDefault(row.getBankStatementId(), Collections.emptyList())
)));
}
private Map<Long, List<CcdiBankStatementHitTagVO>> loadHitTagMap(Long projectId, List<Long> bankStatementIds) {
private Map<Long, List<CcdiBankStatementHitTagVO>> loadHitTagMap(
Long projectId,
List<Long> bankStatementIds,
String modelCode,
String suspiciousType
) {
if (projectId == null || bankStatementIds == null || bankStatementIds.isEmpty()) {
return Collections.emptyMap();
}
List<CcdiBankStatementHitTagVO> hitTags =
bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(projectId, bankStatementIds);
bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(
projectId,
bankStatementIds,
modelCode,
suspiciousType
);
if (hitTags == null || hitTags.isEmpty()) {
return Collections.emptyMap();
}
@@ -165,6 +193,16 @@ public class CcdiBankStatementServiceImpl implements ICcdiBankStatementService {
return normalized == null ? null : normalized.toLowerCase(Locale.ROOT);
}
private String normalizeUpperCase(String value) {
String normalized = normalizeText(value);
return normalized == null ? null : normalized.toUpperCase(Locale.ROOT);
}
private String normalizeSuspiciousType(String suspiciousType) {
String normalized = normalizeUpperCase(suspiciousType);
return normalized != null && ALLOWED_SUSPICIOUS_TYPES.contains(normalized) ? normalized : null;
}
private String normalizeText(String value) {
if (value == null) {
return null;

View File

@@ -375,9 +375,12 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
) {
ensureProjectExists(queryDTO.getProjectId());
normalizeSuspiciousTransactionQuery(queryDTO);
if (isSuspiciousTransactionScopeMismatch(queryDTO)) {
return emptySuspiciousTransactionPage();
}
prepareSuspiciousTransactionExternalBranch(queryDTO);
if (isExternalPersonOnlyWithoutSubject(queryDTO)) {
if (isExternalScopeWithoutSubject(queryDTO)) {
return emptySuspiciousTransactionPage();
}
@@ -387,9 +390,12 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
);
Page<CcdiProjectSuspiciousTransactionItemVO> resultPage =
overviewMapper.selectSuspiciousTransactionPage(page, queryDTO);
List<CcdiProjectSuspiciousTransactionItemVO> rows =
defaultList(resultPage == null ? null : resultPage.getRecords());
attachSuspiciousTransactionHitTags(rows, queryDTO);
CcdiProjectSuspiciousTransactionPageVO result = new CcdiProjectSuspiciousTransactionPageVO();
result.setRows(defaultList(resultPage == null ? null : resultPage.getRecords()));
result.setRows(rows);
result.setTotal(resultPage == null ? 0L : resultPage.getTotal());
return result;
}
@@ -400,9 +406,12 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
) {
ensureProjectExists(queryDTO.getProjectId());
normalizeSuspiciousTransactionQuery(queryDTO);
if (isSuspiciousTransactionScopeMismatch(queryDTO)) {
return List.of();
}
prepareSuspiciousTransactionExternalBranch(queryDTO);
if (isExternalPersonOnlyWithoutSubject(queryDTO)) {
if (isExternalScopeWithoutSubject(queryDTO)) {
return List.of();
}
@@ -469,9 +478,17 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
public void exportRiskDetails(HttpServletResponse response, Long projectId) {
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
queryDTO.setProjectId(projectId);
queryDTO.setSuspiciousType("ALL");
queryDTO.setSuspiciousType(SUSPICIOUS_TYPE_ALL);
exportRiskDetails(response, queryDTO);
}
@Override
public void exportRiskDetails(
HttpServletResponse response,
CcdiProjectSuspiciousTransactionQueryDTO queryDTO
) {
List<CcdiProjectSuspiciousTransactionExcel> suspiciousRows = exportSuspiciousTransactions(queryDTO);
Long projectId = queryDTO.getProjectId();
List<CcdiProjectEmployeeCreditNegativeExcel> creditRows = exportEmployeeCreditNegative(projectId);
List<CcdiProjectAbnormalAccountExcel> abnormalRows = exportAbnormalAccountPeople(projectId);
try {
@@ -742,28 +759,44 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
}
private void prepareSuspiciousTransactionExternalBranch(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
if (!shouldCheckExternalPersonBranch(queryDTO.getSuspiciousType())) {
if (!shouldCheckExternalPersonBranch(queryDTO)) {
queryDTO.setIncludeExternalPerson(false);
return;
}
queryDTO.setIncludeExternalPerson(hasExternalPersonSubject(queryDTO.getProjectId()));
}
private boolean shouldCheckExternalPersonBranch(String suspiciousType) {
return SUSPICIOUS_TYPE_ALL.equals(suspiciousType)
|| SUSPICIOUS_TYPE_MODEL_RULE.equals(suspiciousType)
|| SUSPICIOUS_TYPE_EXTERNAL_PERSON.equals(suspiciousType);
private boolean shouldCheckExternalPersonBranch(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
if (SUSPICIOUS_TYPE_EXTERNAL_PERSON.equals(queryDTO.getSuspiciousType())) {
return true;
}
return SUSPICIOUS_TYPE_ALL.equals(queryDTO.getSuspiciousType())
&& (queryDTO.getModelCode() == null || isExternalModelCode(queryDTO.getModelCode()));
}
private boolean hasExternalPersonSubject(Long projectId) {
return overviewMapper.selectExternalPersonSubjectExistsByProjectId(projectId) != null;
}
private boolean isExternalPersonOnlyWithoutSubject(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
return SUSPICIOUS_TYPE_EXTERNAL_PERSON.equals(queryDTO.getSuspiciousType())
private boolean isExternalScopeWithoutSubject(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
return (SUSPICIOUS_TYPE_EXTERNAL_PERSON.equals(queryDTO.getSuspiciousType())
|| isExternalModelCode(queryDTO.getModelCode()))
&& !Boolean.TRUE.equals(queryDTO.getIncludeExternalPerson());
}
private boolean isSuspiciousTransactionScopeMismatch(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
if (queryDTO.getModelCode() == null) {
return false;
}
boolean externalModel = isExternalModelCode(queryDTO.getModelCode());
return (SUSPICIOUS_TYPE_MODEL_RULE.equals(queryDTO.getSuspiciousType()) && externalModel)
|| (SUSPICIOUS_TYPE_EXTERNAL_PERSON.equals(queryDTO.getSuspiciousType()) && !externalModel);
}
private boolean isExternalModelCode(String modelCode) {
return modelCode != null && modelCode.startsWith("EXTERNAL_");
}
private CcdiProjectSuspiciousTransactionPageVO emptySuspiciousTransactionPage() {
CcdiProjectSuspiciousTransactionPageVO result = new CcdiProjectSuspiciousTransactionPageVO();
result.setRows(List.of());
@@ -976,7 +1009,12 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
return;
}
Map<Long, List<CcdiBankStatementHitTagVO>> hitTagMap = defaultList(
bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(projectId, bankStatementIds)
bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(
projectId,
bankStatementIds,
null,
null
)
).stream().filter(item -> item.getBankStatementId() != null)
.collect(Collectors.groupingBy(
CcdiBankStatementHitTagVO::getBankStatementId,
@@ -988,6 +1026,39 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
)));
}
private void attachSuspiciousTransactionHitTags(
List<CcdiProjectSuspiciousTransactionItemVO> rows,
CcdiProjectSuspiciousTransactionQueryDTO queryDTO
) {
if (rows.isEmpty()) {
return;
}
List<Long> bankStatementIds = rows.stream()
.map(CcdiProjectSuspiciousTransactionItemVO::getBankStatementId)
.filter(item -> item != null)
.distinct()
.toList();
if (bankStatementIds.isEmpty()) {
return;
}
Map<Long, List<CcdiBankStatementHitTagVO>> hitTagMap = defaultList(
bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(
queryDTO.getProjectId(),
bankStatementIds,
queryDTO.getModelCode(),
queryDTO.getSuspiciousType()
)
).stream().filter(item -> item.getBankStatementId() != null)
.collect(Collectors.groupingBy(
CcdiBankStatementHitTagVO::getBankStatementId,
LinkedHashMap::new,
Collectors.toList()
));
rows.forEach(row -> row.setHitTags(new ArrayList<>(
hitTagMap.getOrDefault(row.getBankStatementId(), Collections.emptyList())
)));
}
private void normalizeObjectRows(List<CcdiProjectPersonAnalysisObjectRecordVO> objectRows) {
objectRows.forEach(row -> {
if (row.getRiskTags() == null) {

View File

@@ -30,6 +30,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap>
<resultMap id="CcdiBankStatementHitTagVOResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementHitTagVO">
<result property="modelCode" column="model_code"/>
<result property="ruleCode" column="rule_code"/>
<result property="ruleName" column="rule_name"/>
<result property="riskLevel" column="risk_level"/>
@@ -47,6 +48,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="selectStatementTagsByProjectAndStatementIds" resultMap="CcdiBankStatementHitTagVOResultMap">
select
model_code,
rule_code,
rule_name,
risk_level,
@@ -67,6 +69,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<foreach collection="bankStatementIds" item="item" open="(" separator="," close=")">
#{item}
</foreach>
<if test="modelCode != null and modelCode != ''">
and model_code = #{modelCode}
</if>
<choose>
<when test="suspiciousType == 'MODEL_RULE'">
and left(model_code, 9) != 'EXTERNAL_'
</when>
<when test="suspiciousType == 'EXTERNAL_PERSON'">
and left(model_code, 9) = 'EXTERNAL_'
</when>
</choose>
order by bank_statement_id asc, id asc
</select>

View File

@@ -68,6 +68,10 @@
<resultMap id="SuspiciousTransactionItemResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO">
<id property="bankStatementId" column="bankStatementId"/>
<result property="trxDate" column="trxDate"/>
<result property="leAccountNo" column="leAccountNo"/>
<result property="leAccountName" column="leAccountName"/>
<result property="customerAccountName" column="customerAccountName"/>
<result property="customerAccountNo" column="customerAccountNo"/>
<result property="suspiciousPersonName" column="suspiciousPersonName"/>
<result property="relatedPersonName" column="relatedPersonName"/>
<result property="relatedStaffName" column="relatedStaffName"/>
@@ -1024,9 +1028,21 @@
and ex.exclusion_type = 'STATEMENT'
and ex.bank_statement_id = tr.bank_statement_id
)
<include refid="suspiciousTransactionModelScopeSql"/>
</sql>
<sql id="suspiciousTransactionModelScopeSql">
<if test="query.modelCode != null and query.modelCode != ''">
and tr.model_code = #{query.modelCode}
and tr.model_code = #{query.modelCode}
</if>
<choose>
<when test="query.suspiciousType == 'MODEL_RULE'">
and left(tr.model_code, 9) != 'EXTERNAL_'
</when>
<when test="query.suspiciousType == 'EXTERNAL_PERSON'">
and left(tr.model_code, 9) = 'EXTERNAL_'
</when>
</choose>
</sql>
<sql id="suspiciousTransactionNameHitSql">
@@ -1287,6 +1303,10 @@
select
final_result.bankStatementId,
final_result.trxDate,
bs.LE_ACCOUNT_NO as leAccountNo,
bs.LE_ACCOUNT_NAME as leAccountName,
bs.CUSTOMER_ACCOUNT_NAME as customerAccountName,
bs.CUSTOMER_ACCOUNT_NO as customerAccountNo,
final_result.suspiciousPersonName,
final_result.relatedPersonName,
final_result.relatedStaffName,
@@ -1301,6 +1321,8 @@
from (
<include refid="suspiciousTransactionAggregatedSql"/>
) final_result
inner join ccdi_bank_statement bs
on bs.bank_statement_id = final_result.bankStatementId
<include refid="suspiciousTransactionFilterSql"/>
order by final_result.trxDate desc, final_result.bankStatementId desc
</select>
@@ -1309,6 +1331,10 @@
select
final_result.bankStatementId,
final_result.trxDate,
bs.LE_ACCOUNT_NO as leAccountNo,
bs.LE_ACCOUNT_NAME as leAccountName,
bs.CUSTOMER_ACCOUNT_NAME as customerAccountName,
bs.CUSTOMER_ACCOUNT_NO as customerAccountNo,
final_result.suspiciousPersonName,
final_result.relatedPersonName,
final_result.relatedStaffName,
@@ -1323,6 +1349,8 @@
from (
<include refid="suspiciousTransactionAggregatedSql"/>
) final_result
inner join ccdi_bank_statement bs
on bs.bank_statement_id = final_result.bankStatementId
<include refid="suspiciousTransactionFilterSql"/>
order by final_result.trxDate desc, final_result.bankStatementId desc
</select>
@@ -1375,6 +1403,7 @@
and ex.exclusion_type = 'STATEMENT'
and ex.bank_statement_id = tr.bank_statement_id
)
<include refid="suspiciousTransactionModelScopeSql"/>
group by tr.bank_statement_id
) tag_result on tag_result.bank_statement_id = final_result.bankStatementId
<include refid="suspiciousTransactionFilterSql"/>

View File

@@ -7,6 +7,7 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementFilterOptionsVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
import com.ruoyi.ccdi.project.service.ICcdiBankStatementService;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import org.junit.jupiter.api.AfterEach;
@@ -39,6 +40,9 @@ class CcdiBankStatementControllerTest {
@Mock
private ICcdiBankStatementService bankStatementService;
@Mock
private CcdiProjectAccessService projectAccessService;
@AfterEach
void tearDown() {
RequestContextHolder.resetRequestAttributes();
@@ -80,12 +84,17 @@ class CcdiBankStatementControllerTest {
void detail_shouldReturnAjaxResultSuccess() {
CcdiBankStatementDetailVO detailVO = new CcdiBankStatementDetailVO();
detailVO.setBankStatementId(1000L);
when(bankStatementService.getStatementDetail(1000L)).thenReturn(detailVO);
when(bankStatementService.getStatementDetail(
1000L, "SUSPICIOUS_GAMBLING", "MODEL_RULE"
)).thenReturn(detailVO);
AjaxResult result = controller.getDetail(1000L);
AjaxResult result = controller.getDetail(1000L, "SUSPICIOUS_GAMBLING", "MODEL_RULE");
assertEquals(200, result.get("code"));
assertEquals(detailVO, result.get("data"));
verify(bankStatementService).getStatementDetail(
1000L, "SUSPICIOUS_GAMBLING", "MODEL_RULE"
);
}
@Test

View File

@@ -172,7 +172,7 @@ class CcdiProjectOverviewControllerContractTest {
Method method = controllerClass.getMethod(
"exportRiskDetails",
HttpServletResponse.class,
Long.class
Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO")
);
PostMapping postMapping = method.getAnnotation(PostMapping.class);
Operation operation = method.getAnnotation(Operation.class);
@@ -198,7 +198,7 @@ class CcdiProjectOverviewControllerContractTest {
assertEquals("/report/export", requestMapping.value()[0]);
assertEquals(List.of(RequestMethod.GET, RequestMethod.POST), Arrays.asList(requestMapping.method()));
assertNotNull(operation);
assertEquals("一键导出结果总览报告", operation.summary());
assertEquals("导出结果总览报告", operation.summary());
assertNotNull(preAuthorize);
assertEquals("@ss.hasPermi('ccdi:project:query')", preAuthorize.value());
}
@@ -211,7 +211,10 @@ class CcdiProjectOverviewControllerContractTest {
.map(Field::getName)
.collect(Collectors.toList());
assertEquals(List.of("projectId", "suspiciousType", "pageNum", "pageSize"), fieldNames);
assertEquals(
List.of("projectId", "modelCode", "suspiciousType", "pageNum", "pageSize", "includeExternalPerson"),
fieldNames
);
}
@Test

View File

@@ -18,6 +18,7 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO;
import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.AjaxResult;
import io.swagger.v3.oas.annotations.Operation;
@@ -52,6 +53,9 @@ class CcdiProjectOverviewControllerTest {
@Mock
private ICcdiProjectOverviewService overviewService;
@Mock
private CcdiProjectAccessService projectAccessService;
@Test
void shouldExposeDashboardEndpoint() throws Exception {
when(overviewService.getDashboard(40L)).thenReturn(new CcdiProjectOverviewDashboardVO());
@@ -308,15 +312,20 @@ class CcdiProjectOverviewControllerTest {
@Test
void shouldExposeRiskDetailsExportEndpoint() throws Exception {
MockHttpServletResponse response = new MockHttpServletResponse();
CcdiProjectSuspiciousTransactionQueryDTO queryDTO =
new CcdiProjectSuspiciousTransactionQueryDTO();
queryDTO.setProjectId(40L);
queryDTO.setModelCode("SUSPICIOUS_GAMBLING");
queryDTO.setSuspiciousType("MODEL_RULE");
controller.exportRiskDetails(response, 40L);
controller.exportRiskDetails(response, queryDTO);
verify(overviewService).exportRiskDetails(same(response), same(40L));
verify(overviewService).exportRiskDetails(same(response), same(queryDTO));
Method method = CcdiProjectOverviewController.class.getMethod(
"exportRiskDetails",
jakarta.servlet.http.HttpServletResponse.class,
Long.class
CcdiProjectSuspiciousTransactionQueryDTO.class
);
PostMapping postMapping = method.getAnnotation(PostMapping.class);
Operation operation = method.getAnnotation(Operation.class);

View File

@@ -32,6 +32,7 @@ class CcdiBankTagResultMapperXmlTest {
.orElse(null);
assertNotNull(method, "应提供按项目和流水ID批量查询异常标签的方法");
assertTrue(method.getParameterCount() == 4, "标签批量查询应接收模型和预警类型范围");
}
@Test
@@ -42,6 +43,9 @@ class CcdiBankTagResultMapperXmlTest {
assertTrue(xml.contains("selectStatementTagsByProjectAndStatementIds"), xml);
assertTrue(xml.contains("bank_statement_id IN"), xml);
assertTrue(xml.contains("project_id = #{projectId}"), xml);
assertTrue(xml.contains("model_code = #{modelCode}"), xml);
assertTrue(xml.contains("left(model_code, 9) != 'EXTERNAL_'"), xml);
assertTrue(xml.contains("left(model_code, 9) = 'EXTERNAL_'"), xml);
}
}
}

View File

@@ -91,6 +91,7 @@ class CcdiProjectOverviewMapperSqlTest {
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml"));
String suspiciousSql = extractSelect(xml, "selectSuspiciousTransactionPage");
String modelHitSql = extractSqlFragment(xml, "suspiciousTransactionModelHitSql");
String modelScopeSql = extractSqlFragment(xml, "suspiciousTransactionModelScopeSql");
String mergedSql = extractSqlFragment(xml, "suspiciousTransactionMergedSql");
String aggregatedSql = extractSqlFragment(xml, "suspiciousTransactionAggregatedSql");
String externalSubjectExistsSql = extractSelect(xml, "selectExternalPersonSubjectExistsByProjectId");
@@ -99,6 +100,10 @@ class CcdiProjectOverviewMapperSqlTest {
assertTrue(modelHitSql.contains("tr.bank_statement_id is not null"), modelHitSql);
assertFalse(modelHitSql.contains("rule_name like '%可疑%'"), modelHitSql);
assertFalse(modelHitSql.contains("ABNORMAL_CUSTOMER_TRANSACTION"), modelHitSql);
assertTrue(modelHitSql.contains("suspiciousTransactionModelScopeSql"), modelHitSql);
assertTrue(modelScopeSql.contains("tr.model_code = #{query.modelCode}"), modelScopeSql);
assertTrue(modelScopeSql.contains("left(tr.model_code, 9) != 'EXTERNAL_'"), modelScopeSql);
assertTrue(modelScopeSql.contains("left(tr.model_code, 9) = 'EXTERNAL_'"), modelScopeSql);
assertTrue(suspiciousSql.contains("ccdi_biz_intermediary"), suspiciousSql);
assertTrue(suspiciousSql.contains("ccdi_enterprise_base_info"), suspiciousSql);
assertTrue(suspiciousSql.contains("group by merged.bankStatementId"), suspiciousSql);
@@ -106,6 +111,9 @@ class CcdiProjectOverviewMapperSqlTest {
assertTrue(suspiciousSql.contains("hasModelRuleHit"), suspiciousSql);
assertTrue(suspiciousSql.contains("hasNameListHit"), suspiciousSql);
assertTrue(suspiciousSql.contains("final_result.nameListHitType"), suspiciousSql);
assertTrue(suspiciousSql.contains("bs.LE_ACCOUNT_NO as leAccountNo"), suspiciousSql);
assertTrue(suspiciousSql.contains("bs.CUSTOMER_ACCOUNT_NO as customerAccountNo"), suspiciousSql);
assertTrue(suspiciousSql.contains("bs.bank_statement_id = final_result.bankStatementId"), suspiciousSql);
assertTrue(mergedSql.contains("<if test=\"query.includeExternalPerson == true\">"), mergedSql);
assertTrue(mergedSql.contains("<include refid=\"externalSuspiciousTransactionSql\"/>"), mergedSql);
assertTrue(externalSubjectExistsSql.contains("limit 1"), externalSubjectExistsSql);
@@ -117,6 +125,7 @@ class CcdiProjectOverviewMapperSqlTest {
assertTrue(reportSuspiciousSql.contains("疑似与中介往来"), reportSuspiciousSql);
assertTrue(reportSuspiciousSql.contains("final_result.nameListHitType = '信贷客户'"), reportSuspiciousSql);
assertTrue(reportSuspiciousSql.contains("与信贷客户之间非正常资金往来"), reportSuspiciousSql);
assertTrue(reportSuspiciousSql.contains("suspiciousTransactionModelScopeSql"), reportSuspiciousSql);
}
@Test

View File

@@ -129,7 +129,9 @@ class CcdiBankStatementServiceImplTest {
CcdiBankStatementHitTagVO hitTag = new CcdiBankStatementHitTagVO();
hitTag.setBankStatementId(51274L);
hitTag.setRuleName("大额存现交易");
when(bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(43L, List.of(51274L)))
when(bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(
43L, List.of(51274L), null, null
))
.thenReturn(List.of(hitTag));
List<CcdiBankStatementExcel> result = service.selectStatementListForExport(queryDTO);
@@ -152,7 +154,9 @@ class CcdiBankStatementServiceImplTest {
hitTag.setBankStatementId(51274L);
hitTag.setRuleCode("LARGE_CASH_DEPOSIT");
hitTag.setRuleName("大额存现交易");
when(bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(43L, List.of(51274L)))
when(bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(
43L, List.of(51274L), null, null
))
.thenReturn(List.of(hitTag));
Page<CcdiBankStatementListVO> result = service.selectStatementPage(page, queryDTO);
@@ -170,7 +174,9 @@ class CcdiBankStatementServiceImplTest {
CcdiBankStatementHitTagVO hitTag = new CcdiBankStatementHitTagVO();
hitTag.setBankStatementId(200L);
hitTag.setRuleName("大额存现交易");
when(bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(43L, List.of(200L)))
when(bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(
43L, List.of(200L), null, null
))
.thenReturn(List.of(hitTag));
CcdiBankStatementDetailVO result = service.getStatementDetail(200L);
@@ -179,4 +185,21 @@ class CcdiBankStatementServiceImplTest {
assertEquals(1, result.getHitTags().size());
assertEquals("大额存现交易", result.getHitTags().get(0).getRuleName());
}
@Test
void getStatementDetail_shouldApplySuspiciousTransactionScope() {
CcdiBankStatementDetailVO detailVO = new CcdiBankStatementDetailVO();
detailVO.setBankStatementId(200L);
detailVO.setProjectId(43L);
when(bankStatementMapper.selectStatementDetailById(200L)).thenReturn(detailVO);
when(bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(
43L, List.of(200L), "SUSPICIOUS_GAMBLING", "MODEL_RULE"
)).thenReturn(List.of());
service.getStatementDetail(200L, "suspicious_gambling", "model_rule");
verify(bankTagResultMapper).selectStatementTagsByProjectAndStatementIds(
43L, List.of(200L), "SUSPICIOUS_GAMBLING", "MODEL_RULE"
);
}
}

View File

@@ -387,7 +387,9 @@ class CcdiProjectOverviewServiceImplTest {
hitTag.setRuleCode("RULE_A");
hitTag.setRuleName("大额转账");
hitTag.setRiskLevel("HIGH");
when(bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(40L, List.of(1L)))
when(bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(
40L, List.of(1L), null, null
))
.thenReturn(List.of(hitTag));
CcdiProjectPersonAnalysisObjectRecordVO objectRow = new CcdiProjectPersonAnalysisObjectRecordVO();

View File

@@ -5,6 +5,7 @@ import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportSuspiciousTransactionVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementHitTagVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO;
import com.ruoyi.ccdi.project.mapper.CcdiBankTagResultMapper;
@@ -120,6 +121,71 @@ class CcdiProjectOverviewServiceSuspiciousTransactionTest {
);
}
@Test
void shouldSkipExternalBranchAndAttachSelectedEmployeeModelTags() {
CcdiProject project = new CcdiProject();
project.setProjectId(40L);
when(projectMapper.selectById(40L)).thenReturn(project);
CcdiProjectSuspiciousTransactionItemVO item = new CcdiProjectSuspiciousTransactionItemVO();
item.setBankStatementId(101L);
Page<CcdiProjectSuspiciousTransactionItemVO> page = new Page<>(1, 10);
page.setRecords(List.of(item));
page.setTotal(1);
when(overviewMapper.selectSuspiciousTransactionPage(
any(Page.class), any(CcdiProjectSuspiciousTransactionQueryDTO.class)
)).thenReturn(page);
CcdiBankStatementHitTagVO tag = new CcdiBankStatementHitTagVO();
tag.setBankStatementId(101L);
tag.setModelCode("SUSPICIOUS_GAMBLING");
tag.setRuleName("疑似赌博");
when(bankTagResultMapper.selectStatementTagsByProjectAndStatementIds(
40L, List.of(101L), "SUSPICIOUS_GAMBLING", "ALL"
)).thenReturn(List.of(tag));
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
queryDTO.setProjectId(40L);
queryDTO.setModelCode("suspicious_gambling");
queryDTO.setSuspiciousType("all");
CcdiProjectSuspiciousTransactionPageVO result = service.getSuspiciousTransactions(queryDTO);
assertEquals("疑似赌博", result.getRows().getFirst().getHitTags().getFirst().getRuleName());
verify(overviewMapper, never()).selectExternalPersonSubjectExistsByProjectId(40L);
verify(overviewMapper).selectSuspiciousTransactionPage(
any(Page.class),
argThat(query -> !Boolean.TRUE.equals(query.getIncludeExternalPerson()))
);
}
@Test
void shouldKeepEmployeeRuleScopeSeparateFromExternalModels() {
CcdiProject project = new CcdiProject();
project.setProjectId(40L);
when(projectMapper.selectById(40L)).thenReturn(project);
Page<CcdiProjectSuspiciousTransactionItemVO> page = new Page<>(1, 10);
page.setRecords(List.of());
page.setTotal(0);
when(overviewMapper.selectSuspiciousTransactionPage(
any(Page.class), any(CcdiProjectSuspiciousTransactionQueryDTO.class)
)).thenReturn(page);
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
queryDTO.setProjectId(40L);
queryDTO.setSuspiciousType("MODEL_RULE");
service.getSuspiciousTransactions(queryDTO);
verify(overviewMapper, never()).selectExternalPersonSubjectExistsByProjectId(40L);
verify(overviewMapper).selectSuspiciousTransactionPage(
any(Page.class),
argThat(query -> "MODEL_RULE".equals(query.getSuspiciousType())
&& !Boolean.TRUE.equals(query.getIncludeExternalPerson()))
);
}
@Test
void shouldReturnEmptyExternalSuspiciousTransactionsWhenExternalSubjectDoesNotExist() {
CcdiProject project = new CcdiProject();