diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiBankStatementController.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiBankStatementController.java index 28aa89b9..4d504285 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiBankStatementController.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiBankStatementController.java @@ -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); } diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java index e7c2d334..406413a4 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java @@ -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); } /** diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiBankStatementHitTagVO.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiBankStatementHitTagVO.java index ad315692..948b0c78 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiBankStatementHitTagVO.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiBankStatementHitTagVO.java @@ -8,6 +8,9 @@ import lombok.Data; @Data public class CcdiBankStatementHitTagVO { + /** 模型编码 */ + private String modelCode; + /** 规则编码 */ private String ruleCode; diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectSuspiciousTransactionItemVO.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectSuspiciousTransactionItemVO.java index ac2a834c..d38e44e0 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectSuspiciousTransactionItemVO.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectSuspiciousTransactionItemVO.java @@ -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 hitTags = new ArrayList<>(); } diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagResultMapper.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagResultMapper.java index 9482af5b..14dd6385 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagResultMapper.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagResultMapper.java @@ -26,11 +26,15 @@ public interface CcdiBankTagResultMapper extends BaseMapper { * * @param projectId 项目ID * @param bankStatementIds 流水ID列表 + * @param modelCode 模型编码 + * @param suspiciousType 预警类型 * @return 命中的异常标签列表 */ List selectStatementTagsByProjectAndStatementIds( @Param("projectId") Long projectId, - @Param("bankStatementIds") List bankStatementIds + @Param("bankStatementIds") List bankStatementIds, + @Param("modelCode") String modelCode, + @Param("suspiciousType") String suspiciousType ); /** diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiBankStatementService.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiBankStatementService.java index 05ad67f0..9abf6079 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiBankStatementService.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiBankStatementService.java @@ -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 + ); } diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java index 3512385a..8adfb15a 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java @@ -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 + ) { + } + /** * 一键导出结果总览报告 * diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankStatementServiceImpl.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankStatementServiceImpl.java index b2c9d798..da644921 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankStatementServiceImpl.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankStatementServiceImpl.java @@ -32,6 +32,8 @@ public class CcdiBankStatementServiceImpl implements ICcdiBankStatementService { private static final Set ALLOWED_TAB_TYPES = Set.of("all", "in", "out"); private static final Set ALLOWED_ORDER_DIRECTIONS = Set.of("asc", "desc"); + private static final Set 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> 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> hitTagMap = loadHitTagMap(projectId, bankStatementIds); + Map> hitTagMap = loadHitTagMap( + projectId, + bankStatementIds, + null, + null + ); rows.forEach(row -> row.setHitTags(new ArrayList<>( hitTagMap.getOrDefault(row.getBankStatementId(), Collections.emptyList()) ))); } - private Map> loadHitTagMap(Long projectId, List bankStatementIds) { + private Map> loadHitTagMap( + Long projectId, + List bankStatementIds, + String modelCode, + String suspiciousType + ) { if (projectId == null || bankStatementIds == null || bankStatementIds.isEmpty()) { return Collections.emptyMap(); } List 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; diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java index 0d73c78a..6e12f7f6 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java @@ -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 resultPage = overviewMapper.selectSuspiciousTransactionPage(page, queryDTO); + List 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 suspiciousRows = exportSuspiciousTransactions(queryDTO); + Long projectId = queryDTO.getProjectId(); List creditRows = exportEmployeeCreditNegative(projectId); List 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> 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 rows, + CcdiProjectSuspiciousTransactionQueryDTO queryDTO + ) { + if (rows.isEmpty()) { + return; + } + List bankStatementIds = rows.stream() + .map(CcdiProjectSuspiciousTransactionItemVO::getBankStatementId) + .filter(item -> item != null) + .distinct() + .toList(); + if (bankStatementIds.isEmpty()) { + return; + } + Map> 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 objectRows) { objectRows.forEach(row -> { if (row.getRiskTags() == null) { diff --git a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankTagResultMapper.xml b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankTagResultMapper.xml index c4a14b81..0cb820b6 100644 --- a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankTagResultMapper.xml +++ b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankTagResultMapper.xml @@ -30,6 +30,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -47,6 +48,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" diff --git a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml index 26f6940e..b4bd2a62 100644 --- a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml +++ b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml @@ -68,6 +68,10 @@ + + + + @@ -1024,9 +1028,21 @@ and ex.exclusion_type = 'STATEMENT' and ex.bank_statement_id = tr.bank_statement_id ) + + + + - and tr.model_code = #{query.modelCode} + and tr.model_code = #{query.modelCode} + + + and left(tr.model_code, 9) != 'EXTERNAL_' + + + and left(tr.model_code, 9) = 'EXTERNAL_' + + @@ -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 ( ) final_result + inner join ccdi_bank_statement bs + on bs.bank_statement_id = final_result.bankStatementId order by final_result.trxDate desc, final_result.bankStatementId desc @@ -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 ( ) final_result + inner join ccdi_bank_statement bs + on bs.bank_statement_id = final_result.bankStatementId order by final_result.trxDate desc, final_result.bankStatementId desc @@ -1375,6 +1403,7 @@ 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 diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiBankStatementControllerTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiBankStatementControllerTest.java index dc38fe19..cac9b8aa 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiBankStatementControllerTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiBankStatementControllerTest.java @@ -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 diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerContractTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerContractTest.java index 952bbfdc..eca160ea 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerContractTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerContractTest.java @@ -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 diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerTest.java index fcfb730f..146104e2 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewControllerTest.java @@ -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); diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagResultMapperXmlTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagResultMapperXmlTest.java index 92f43398..766f9d89 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagResultMapperXmlTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagResultMapperXmlTest.java @@ -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); } } } diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperSqlTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperSqlTest.java index 6fcdb6e1..9cec6229 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperSqlTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperSqlTest.java @@ -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(""), mergedSql); assertTrue(mergedSql.contains(""), 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 diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankStatementServiceImplTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankStatementServiceImplTest.java index a89b3e2f..bf97fa6a 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankStatementServiceImplTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiBankStatementServiceImplTest.java @@ -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 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 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" + ); + } } diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java index 9201f60c..f2ec5881 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java @@ -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(); diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceSuspiciousTransactionTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceSuspiciousTransactionTest.java index ac49f809..5580e492 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceSuspiciousTransactionTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceSuspiciousTransactionTest.java @@ -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 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 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(); diff --git a/docs/reports/implementation/2026-07-16-suspicious-transaction-filter-consistency.md b/docs/reports/implementation/2026-07-16-suspicious-transaction-filter-consistency.md new file mode 100644 index 00000000..c3dd176f --- /dev/null +++ b/docs/reports/implementation/2026-07-16-suspicious-transaction-filter-consistency.md @@ -0,0 +1,81 @@ +# 涉疑交易明细筛选一致性实施记录 + +## 修改内容 + +### 后端 + +- 涉疑交易模型命中 SQL 按预警类型隔离模型范围: + - `MODEL_RULE` 排除 `EXTERNAL_*` 外部模型。 + - `EXTERNAL_PERSON` 仅保留 `EXTERNAL_*` 外部模型。 + - 选择具体 `modelCode` 时精确匹配模型。 +- 优化外部人员分支判断:`MODEL_RULE` 和 `ALL + 员工具体模型` 不再检查或拼接外部人员分支。 +- 涉疑交易分页结果直接返回本方账户、对方账户等列表展示字段。 +- 分页完成后,仅针对当前页流水 ID 批量查询一次筛选范围内的标签,并返回标签 `modelCode`。 +- 流水详情接口支持 `modelCode`、`suspiciousType`,详情标签与列表筛选一致。 +- 风险明细统一导出接口接收当前涉疑筛选条件,涉疑交易工作表与页面口径一致。 +- 涉疑交易导出标签聚合应用相同模型范围。 + +### 前端 + +- 模型或预警类型变化时立即清空旧列表、总数、详情和缓存,但不自动发起查询。 +- 增加涉疑交易请求序号,只有最后一次请求可以更新列表、总数和加载状态。 +- 父组件同项目数据更新时不再重置本地筛选或覆盖当前列表;仅项目切换时重新初始化。 +- 删除涉疑列表当前页逐条请求流水详情的补全逻辑,直接使用列表接口返回字段和标签。 +- 详情缓存键增加预警类型与模型编码,详情请求透传当前筛选条件。 +- 风险明细导出透传当前涉疑交易筛选条件。 + +## 性能变化 + +- 浏览器列表加载由“1 次涉疑列表请求 + 当前页 N 次流水详情请求”降为 1 次涉疑列表请求。 +- 后端每次分页固定执行分页查询和当前页标签批量查询,不按流水逐条查询标签。 +- 员工具体模型查询不再执行外部人员主体判断和外部人员复杂关联分支。 +- 下拉框变化不请求后端,只有点击“查询”才执行一次查询。 + +## 自动化验证 + +### 后端 + +执行: + +```bash +mvn -pl ccdi-project -am "-Dtest=CcdiProjectOverviewServiceSuspiciousTransactionTest,CcdiProjectOverviewMapperSqlTest,CcdiBankStatementServiceImplTest,CcdiBankTagResultMapperXmlTest,CcdiBankStatementControllerTest,CcdiProjectOverviewControllerTest,CcdiProjectOverviewControllerContractTest" "-Dsurefire.failIfNoSpecifiedTests=false" test +``` + +结果:`Tests run: 55, Failures: 0, Errors: 0, Skipped: 0`。 + +同时执行 `mvn -pl ruoyi-admin -am package -DskipTests`,后端打包通过。 + +### 前端 + +项目 `.nvmrc` 对应的 Node 14.21.3 本机安装缺少 npm,因此按仓库规则使用已验证的 Node 22.22.3、npm 10.9.8。 + +执行以下定向脚本,全部通过: + +```bash +node tests/unit/risk-detail-suspicious-filter-consistency.test.js +node tests/unit/risk-detail-suspicious-filter-linkage.test.js +node tests/unit/risk-detail-suspicious-transaction-detail-dialog.test.js +node tests/unit/preliminary-check-suspicious-transaction-load.test.js +node tests/unit/project-overview-api.test.js +``` + +执行 `npm run build:prod`,生产构建通过;仅保留仓库原有包体积警告。 + +## 真实页面验证 + +`browser-use` 所需的内置浏览器执行工具在当前会话不可用,按技能检查流程确认后,使用终端 Playwright 连接真实前端 `http://localhost:9528` 和真实 Java 后端 `http://localhost:62318` 验证,未使用原型或 Mock。 + +- 项目 42: + - 切换模型时旧列表立即清空,未自动发起查询。 + - `ALL + SUSPICIOUS_GAMBLING` 返回空结果,未保留上一轮大额交易列表。 + - `ALL + LARGE_TRANSACTION` 返回标签全部为 `LARGE_TRANSACTION`。 + - 主动打开详情后,详情请求携带 `modelCode=LARGE_TRANSACTION&suspiciousType=ALL`。 +- 项目 90629002: + - `MODEL_RULE + 全部模型` 仅返回 2 条员工流水,标签均为员工模型。 + - `EXTERNAL_PERSON + 全部模型` 仅返回 3 条外部人员流水,标签均为 `EXTERNAL_*`。 + - `EXTERNAL_PERSON + EXTERNAL_LARGE_TRANSACTION` 仅返回外部人员大额交易标签。 + - `EXTERNAL_PERSON + EXTERNAL_SUSPICIOUS_GAMBLING` 仅返回 1 条外部人员可疑赌博流水和对应标签,未混入外部人员大额交易标签。 +- 列表查询期间未产生当前页逐条详情请求;仅主动点击“详情”后发起一条详情请求。 +- 浏览器控制台错误数为 0。 + +测试结束后已关闭本轮启动的前端、后端和浏览器进程,62318、9528 均无监听进程。 diff --git a/ruoyi-ui/src/api/ccdiProjectBankStatement.js b/ruoyi-ui/src/api/ccdiProjectBankStatement.js index 3fd87d7b..1122a4d1 100644 --- a/ruoyi-ui/src/api/ccdiProjectBankStatement.js +++ b/ruoyi-ui/src/api/ccdiProjectBankStatement.js @@ -18,9 +18,10 @@ export function getBankStatementOptions(projectId) { }); } -export function getBankStatementDetail(bankStatementId) { +export function getBankStatementDetail(bankStatementId, params) { return request({ url: `/ccdi/project/bank-statement/detail/${bankStatementId}`, method: "get", + params, }); } diff --git a/ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue b/ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue index 546ad264..e36927c2 100644 --- a/ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue +++ b/ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue @@ -25,6 +25,7 @@ class="suspicious-filter-select" :placeholder="allModelOptionLabel" filterable + @change="handleSuspiciousModelSelectChange" > ({ hitTags: normalizeHitTags(detail && detail.hitTags), }); +const normalizeSuspiciousRows = (rows) => ( + Array.isArray(rows) + ? rows.map((row) => ({ + ...row, + hitTags: normalizeHitTags(row && row.hitTags, row), + })) + : [] +); + export default { name: "RiskDetailSection", props: { @@ -546,6 +556,8 @@ export default { abnormalAccountList: [], projectId: null, statementDetailCache: {}, + suspiciousRequestSeq: 0, + detailRequestSeq: 0, }; }, computed: { @@ -574,15 +586,11 @@ export default { watch: { sectionData: { immediate: true, - deep: true, handler(value) { - this.projectId = value && value.projectId ? value.projectId : null; - this.currentSuspiciousType = normalizeSuspiciousType(value && value.suspiciousType); - this.selectedModelCode = ""; + const nextProjectId = value && value.projectId ? value.projectId : null; + const projectChanged = String(nextProjectId || "") !== String(this.projectId || ""); + this.projectId = nextProjectId; this.riskModelOptions = normalizeRiskModelOptions(value && value.riskModelOptions); - this.suspiciousPageNum = 1; - this.suspiciousPageSize = DEFAULT_RISK_DETAIL_PAGE_SIZE; - this.suspiciousTotal = Number(value && value.total) || 0; this.employeeCreditNegativePageNum = 1; this.employeeCreditNegativePageSize = DEFAULT_RISK_DETAIL_PAGE_SIZE; this.employeeCreditNegativeTotal = Number(value && value.employeeCreditNegativeTotal) || 0; @@ -590,19 +598,33 @@ export default { this.abnormalAccountPageSize = DEFAULT_RISK_DETAIL_PAGE_SIZE; this.abnormalAccountTotal = 0; this.abnormalAccountList = []; - const rows = Array.isArray(value && value.suspiciousTransactionList) - ? value.suspiciousTransactionList - : []; this.employeeCreditNegativeList = Array.isArray(value && value.employeeCreditNegativeList) ? value.employeeCreditNegativeList : []; - this.hydrateSuspiciousRows(rows); + if (projectChanged) { + this.suspiciousRequestSeq += 1; + this.detailRequestSeq += 1; + this.currentSuspiciousType = normalizeSuspiciousType(value && value.suspiciousType); + this.selectedModelCode = ""; + this.suspiciousPageNum = 1; + this.suspiciousPageSize = DEFAULT_RISK_DETAIL_PAGE_SIZE; + this.suspiciousTotal = Number(value && value.total) || 0; + this.suspiciousTransactionList = normalizeSuspiciousRows( + value && value.suspiciousTransactionList + ); + this.suspiciousLoading = false; + this.statementDetailCache = {}; + this.closeDetailDialog(); + } this.loadAbnormalAccountPeople(); }, }, }, methods: { buildFlowEvidenceFingerprint, + handleSuspiciousModelSelectChange() { + this.resetSuspiciousFilterResults(); + }, handleSuspiciousTypeSelectChange(value) { if (value === "EXTERNAL_PERSON") { const hasExternalModel = EXTERNAL_RISK_MODEL_OPTIONS @@ -610,11 +632,20 @@ export default { if (!hasExternalModel) { this.selectedModelCode = ""; } - return; - } - if (value === "MODEL_RULE" && isExternalModelCode(this.selectedModelCode)) { + } else if (value === "MODEL_RULE" && isExternalModelCode(this.selectedModelCode)) { this.selectedModelCode = ""; } + this.resetSuspiciousFilterResults(); + }, + resetSuspiciousFilterResults() { + this.suspiciousRequestSeq += 1; + this.detailRequestSeq += 1; + this.suspiciousPageNum = 1; + this.suspiciousTotal = 0; + this.suspiciousTransactionList = []; + this.suspiciousLoading = false; + this.statementDetailCache = {}; + this.closeDetailDialog(); }, async handleSuspiciousQuery() { this.suspiciousPageNum = 1; @@ -663,6 +694,7 @@ export default { await this.loadAbnormalAccountPeople(); }, async loadSuspiciousTransactions() { + const requestSeq = ++this.suspiciousRequestSeq; if (!this.projectId) { this.suspiciousTransactionList = []; this.suspiciousTotal = 0; @@ -670,24 +702,34 @@ export default { return; } + const query = { + projectId: this.projectId, + modelCode: this.selectedModelCode, + suspiciousType: this.currentSuspiciousType, + pageNum: this.suspiciousPageNum, + pageSize: this.suspiciousPageSize, + }; this.suspiciousLoading = true; try { - const response = await getOverviewSuspiciousTransactions({ - projectId: this.projectId, - modelCode: this.selectedModelCode, - suspiciousType: this.currentSuspiciousType, - pageNum: this.suspiciousPageNum, - pageSize: this.suspiciousPageSize, - }); + const response = await getOverviewSuspiciousTransactions(query); + if (requestSeq !== this.suspiciousRequestSeq) { + return; + } const data = (response && response.data) || {}; this.suspiciousTotal = Number(data.total) || 0; - await this.hydrateSuspiciousRows(Array.isArray(data.rows) ? data.rows : []); + this.suspiciousTransactionList = normalizeSuspiciousRows(data.rows); } catch (error) { + if (requestSeq !== this.suspiciousRequestSeq) { + return; + } this.suspiciousTransactionList = []; this.suspiciousTotal = 0; this.$message.error("加载涉疑交易明细失败"); console.error("加载涉疑交易明细失败", error); - this.suspiciousLoading = false; + } finally { + if (requestSeq === this.suspiciousRequestSeq) { + this.suspiciousLoading = false; + } } }, async loadEmployeeCreditNegative() { @@ -744,57 +786,25 @@ export default { this.abnormalAccountLoading = false; } }, - async hydrateSuspiciousRows(rows) { - const safeRows = Array.isArray(rows) ? rows : []; - if (!safeRows.length) { - this.suspiciousTransactionList = []; - this.suspiciousLoading = false; - return; - } - - this.suspiciousLoading = true; - try { - const enrichedRows = await Promise.all( - safeRows.map(async (row) => { - if (!row || !row.bankStatementId) { - return { - ...row, - hitTags: [], - }; - } - try { - const detail = await this.fetchStatementDetail(row.bankStatementId, true); - return { - ...detail, - ...row, - hitTags: normalizeHitTags(detail.hitTags, row), - }; - } catch (error) { - return { - ...row, - hitTags: [], - }; - } - }) - ); - this.suspiciousTransactionList = enrichedRows; - } finally { - this.suspiciousLoading = false; - } - }, - async fetchStatementDetail(bankStatementId, silent) { + async fetchStatementDetail(bankStatementId, silent, scope = {}) { if (!bankStatementId) { return createEmptyDetailData(); } - if (this.statementDetailCache[bankStatementId]) { - return this.statementDetailCache[bankStatementId]; + const modelCode = scope.modelCode !== undefined ? scope.modelCode : this.selectedModelCode; + const suspiciousType = scope.suspiciousType || this.currentSuspiciousType; + const cacheKey = `${bankStatementId}|${suspiciousType || "ALL"}|${modelCode || "ALL"}`; + if (this.statementDetailCache[cacheKey]) { + return this.statementDetailCache[cacheKey]; } try { - const response = await getBankStatementDetail(bankStatementId); + const response = await getBankStatementDetail(bankStatementId, { + modelCode, + suspiciousType, + }); const detail = normalizeDetailData(response && response.data); - this.$set(this.statementDetailCache, bankStatementId, detail); + this.$set(this.statementDetailCache, cacheKey, detail); return detail; } catch (error) { if (!silent) { @@ -808,10 +818,18 @@ export default { return; } + const requestSeq = ++this.detailRequestSeq; + const scope = { + modelCode: this.selectedModelCode, + suspiciousType: this.currentSuspiciousType, + }; this.detailVisible = true; this.detailLoading = true; try { - const detail = await this.fetchStatementDetail(row.bankStatementId, false); + const detail = await this.fetchStatementDetail(row.bankStatementId, false, scope); + if (requestSeq !== this.detailRequestSeq) { + return; + } this.detailData = { ...detail, ...row, @@ -820,11 +838,16 @@ export default { : normalizeHitTags(detail.hitTags, row), }; } catch (error) { + if (requestSeq !== this.detailRequestSeq) { + return; + } this.detailData = createEmptyDetailData(); this.$message.error("加载流水详情失败"); console.error("加载流水详情失败", error); } finally { - this.detailLoading = false; + if (requestSeq === this.detailRequestSeq) { + this.detailLoading = false; + } } }, closeDetailDialog() { @@ -865,6 +888,8 @@ export default { "ccdi/project/overview/risk-details/export", { projectId: this.projectId, + modelCode: this.selectedModelCode, + suspiciousType: this.currentSuspiciousType, }, `风险明细_${this.projectId}_${new Date().getTime()}.xlsx` ); diff --git a/ruoyi-ui/tests/unit/risk-detail-suspicious-filter-consistency.test.js b/ruoyi-ui/tests/unit/risk-detail-suspicious-filter-consistency.test.js new file mode 100644 index 00000000..e056a4ff --- /dev/null +++ b/ruoyi-ui/tests/unit/risk-detail-suspicious-filter-consistency.test.js @@ -0,0 +1,43 @@ +const assert = require("assert"); +const fs = require("fs"); +const path = require("path"); + +const componentSource = fs.readFileSync( + path.resolve( + __dirname, + "../../src/views/ccdiProject/components/detail/RiskDetailSection.vue" + ), + "utf8" +); +const statementApiSource = fs.readFileSync( + path.resolve(__dirname, "../../src/api/ccdiProjectBankStatement.js"), + "utf8" +); + +[ + '@change="handleSuspiciousModelSelectChange"', + "resetSuspiciousFilterResults()", + "const requestSeq = ++this.suspiciousRequestSeq;", + "if (requestSeq !== this.suspiciousRequestSeq)", + "if (requestSeq === this.suspiciousRequestSeq)", + "this.suspiciousTransactionList = normalizeSuspiciousRows(data.rows);", + "const projectChanged =", + "if (projectChanged) {", + "modelCode: this.selectedModelCode", + "suspiciousType: this.currentSuspiciousType", + "const cacheKey = `${bankStatementId}|${suspiciousType || \"ALL\"}|${modelCode || \"ALL\"}`;", +].forEach((token) => assert(componentSource.includes(token), token)); + +assert( + !componentSource.includes("async hydrateSuspiciousRows(rows)"), + "涉疑列表不得再按行请求详情补标签" +); +assert( + !componentSource.includes("deep: true"), + "父页面同项目数据更新不得通过深度监听重置筛选" +); + +[ + "getBankStatementDetail(bankStatementId, params)", + "params,", +].forEach((token) => assert(statementApiSource.includes(token), token)); diff --git a/ruoyi-ui/tests/unit/risk-detail-suspicious-transaction-detail-dialog.test.js b/ruoyi-ui/tests/unit/risk-detail-suspicious-transaction-detail-dialog.test.js index a5aafbbe..78ca56ae 100644 --- a/ruoyi-ui/tests/unit/risk-detail-suspicious-transaction-detail-dialog.test.js +++ b/ruoyi-ui/tests/unit/risk-detail-suspicious-transaction-detail-dialog.test.js @@ -15,7 +15,7 @@ const source = fs.readFileSync( "detailVisible", "handleViewDetail", "this.download(", - "ccdi/project/overview/suspicious-transactions/export", + "ccdi/project/overview/risk-details/export", "原始文件", "命中异常标签", "formatOriginalFileName",