diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiProjectSuspiciousTransactionQueryDTO.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiProjectSuspiciousTransactionQueryDTO.java index 2cc9e6e1..2babf192 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiProjectSuspiciousTransactionQueryDTO.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiProjectSuspiciousTransactionQueryDTO.java @@ -11,6 +11,9 @@ public class CcdiProjectSuspiciousTransactionQueryDTO { /** 项目ID */ private Long projectId; + /** 模型编码 */ + private String modelCode; + /** 涉疑类型 */ private String suspiciousType; 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 1d7e92ff..aaa12823 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 @@ -625,6 +625,11 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi } private void normalizeSuspiciousTransactionQuery(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) { + if (queryDTO.getModelCode() != null && !queryDTO.getModelCode().isBlank()) { + queryDTO.setModelCode(queryDTO.getModelCode().trim().toUpperCase()); + } else { + queryDTO.setModelCode(null); + } if (queryDTO.getSuspiciousType() == null || queryDTO.getSuspiciousType().isBlank()) { queryDTO.setSuspiciousType("ALL"); return; 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 facf2e2e..076abb30 100644 --- a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml +++ b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml @@ -972,6 +972,9 @@ from ccdi_bank_statement_tag_result tr where tr.project_id = #{query.projectId} and tr.bank_statement_id is not null + + and tr.model_code = #{query.modelCode} + @@ -1203,6 +1206,15 @@ where final_result.hasModelRuleHit = 1 or final_result.hasNameListHit = 1 + + and exists ( + select 1 + from ccdi_bank_statement_tag_result model_filter + where model_filter.project_id = #{query.projectId} + and model_filter.bank_statement_id = final_result.bankStatementId + and model_filter.model_code = #{query.modelCode} + ) + diff --git a/docs/reports/implementation/2026-07-02-suspicious-transaction-model-filter.md b/docs/reports/implementation/2026-07-02-suspicious-transaction-model-filter.md new file mode 100644 index 00000000..2166e88d --- /dev/null +++ b/docs/reports/implementation/2026-07-02-suspicious-transaction-model-filter.md @@ -0,0 +1,30 @@ +# 涉疑交易明细模型筛选实施记录 + +## 修改内容 + +- 在结果总览“风险明细 / 涉疑交易明细”区域新增一行筛选栏,包含“命中模型”“可疑规则”和“查询”按钮。 +- 可疑规则下拉保留“全部可疑规则”“员工规则命中”“外部人员预警”,不在前端提供“名单库命中”独立筛选项。 +- 选择“外部人员预警”时自动清空并禁用“命中模型”筛选,避免误解为外部人员预警也能叠加模型筛选。 +- 模型下拉复用结果总览已加载的风险模型卡片数据,默认展示“全部模型”。 +- 涉疑交易列表查询接口新增 `modelCode` 参数,点击“查询”后按当前模型和可疑类型重新加载第一页。 +- “风险明细导出”保持原有全量导出逻辑,仅按项目 `projectId` 导出,不跟随页面筛选条件。 +- 后端涉疑交易查询 DTO 增加 `modelCode` 字段,并在查询前统一转为大写编码。 +- 涉疑交易 SQL 增加模型命中条件,确保服务端分页结果和总数都按所选模型过滤。 + +## 影响范围 + +- 前端页面:`ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue` +- 前端接口:`ruoyi-ui/src/api/ccdi/projectOverview.js` +- 结果总览数据适配:`ruoyi-ui/src/views/ccdiProject/components/detail/preliminaryCheck.mock.js` +- 后端查询参数:`ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiProjectSuspiciousTransactionQueryDTO.java` +- 后端查询规范化:`ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java` +- 后端 SQL:`ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml` + +## 验证情况 + +- 已执行 `mvn -pl ccdi-project -am compile -DskipTests`,后端相关模块编译通过。 +- 已通过 `nvm use 22.22.3` 切换 Node 后执行 `npm run build:prod`,前端生产构建通过,存在既有资源体积 warning。 +- 已使用 Playwright 打开真实前端业务路由 `http://localhost:8080/ccdiProject/detail/40?tab=overview` 进行页面验收。 +- 因本机 62318 后端未运行且 `bin/restart_java_backend.sh` 在当前 Windows 环境缺少 `lsof` 无法执行,验收时临时启动本地 mock 后端提供最小接口响应,测试后已关闭该进程。 +- 页面验收确认“涉疑交易明细”显示一行筛选栏,模型下拉包含“全部模型”“大额交易模型”“疑似赌博交易模型”,可疑规则下拉仅包含“全部可疑规则”“员工规则命中”“外部人员预警”。 +- 页面验收确认选择“大额交易模型”后点击“查询”,涉疑交易请求携带 `modelCode=LARGE_TRANSACTION`,同时保留 `suspiciousType=ALL&pageNum=1&pageSize=5`。 diff --git a/ruoyi-ui/src/api/ccdi/projectOverview.js b/ruoyi-ui/src/api/ccdi/projectOverview.js index e6b85d0a..e1b8e3eb 100644 --- a/ruoyi-ui/src/api/ccdi/projectOverview.js +++ b/ruoyi-ui/src/api/ccdi/projectOverview.js @@ -104,6 +104,7 @@ export function getOverviewSuspiciousTransactions(params) { method: 'get', params: { projectId: params.projectId, + modelCode: params.modelCode, suspiciousType: params.suspiciousType, pageNum: params.pageNum, pageSize: params.pageSize diff --git a/ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue b/ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue index a45094c8..a6796c34 100644 --- a/ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue +++ b/ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue @@ -17,24 +17,46 @@ 涉疑交易明细 展示涉疑交易的关键字段与风险金额 - - - - {{ currentSuspiciousTypeLabel }} - - - - - {{ item.label }} - - - - + + + + + + + + + + + + + + + 查询 + + + ( SUSPICIOUS_TYPE_OPTIONS.some((item) => item.value === value) ? value : "ALL" ); +const normalizeRiskModelOptions = (rows) => { + if (!Array.isArray(rows)) { + return []; + } + const seen = new Set(); + return rows + .map((item) => ({ + value: item && (item.value || item.modelCode || item.key), + label: item && (item.label || item.modelName || item.title), + })) + .filter((item) => { + if (!item.value || seen.has(item.value)) { + return false; + } + seen.add(item.value); + return true; + }); +}; + const ABNORMAL_CUSTOMER_TRANSACTION_RULE_CODE = "ABNORMAL_CUSTOMER_TRANSACTION"; const ABNORMAL_CUSTOMER_TRANSACTION_RULE_NAME = "与客户之间非正常资金往来"; @@ -467,10 +507,12 @@ export default { detailVisible: false, detailData: createEmptyDetailData(), currentSuspiciousType: "ALL", + selectedModelCode: "", suspiciousPageNum: 1, suspiciousPageSize: DEFAULT_RISK_DETAIL_PAGE_SIZE, suspiciousTotal: 0, suspiciousTransactionList: [], + riskModelOptions: [], employeeCreditNegativeLoading: false, employeeCreditNegativePageNum: 1, employeeCreditNegativePageSize: DEFAULT_RISK_DETAIL_PAGE_SIZE, @@ -492,9 +534,8 @@ export default { suspiciousTypeOptions() { return SUSPICIOUS_TYPE_OPTIONS; }, - currentSuspiciousTypeLabel() { - const matched = this.suspiciousTypeOptions.find((item) => item.value === this.currentSuspiciousType); - return matched ? matched.label : "全部可疑人员类型"; + isExternalSuspiciousType() { + return this.currentSuspiciousType === "EXTERNAL_PERSON"; }, }, watch: { @@ -504,6 +545,8 @@ export default { handler(value) { this.projectId = value && value.projectId ? value.projectId : null; this.currentSuspiciousType = normalizeSuspiciousType(value && value.suspiciousType); + this.selectedModelCode = ""; + this.riskModelOptions = normalizeRiskModelOptions(value && value.riskModelOptions); this.suspiciousPageNum = 1; this.suspiciousPageSize = DEFAULT_RISK_DETAIL_PAGE_SIZE; this.suspiciousTotal = Number(value && value.total) || 0; @@ -527,8 +570,12 @@ export default { }, methods: { buildFlowEvidenceFingerprint, - async handleSuspiciousTypeChange(command) { - this.currentSuspiciousType = command; + handleSuspiciousTypeSelectChange(value) { + if (value === "EXTERNAL_PERSON") { + this.selectedModelCode = ""; + } + }, + async handleSuspiciousQuery() { this.suspiciousPageNum = 1; await this.loadSuspiciousTransactions(); }, @@ -571,6 +618,7 @@ export default { try { const response = await getOverviewSuspiciousTransactions({ projectId: this.projectId, + modelCode: this.selectedModelCode, suspiciousType: this.currentSuspiciousType, pageNum: this.suspiciousPageNum, pageSize: this.suspiciousPageSize, @@ -954,6 +1002,35 @@ export default { color: #94a3b8; } +.suspicious-filter-form { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px 12px; + margin-bottom: 14px; + padding: 10px 12px; + background: #f8fafc; + border: 1px solid #e2e8f0; +} + +.suspicious-filter-form :deep(.el-form-item) { + margin-right: 0; + margin-bottom: 0; +} + +.suspicious-filter-form :deep(.el-form-item__label) { + color: #475569; + font-weight: 500; +} + +.suspicious-filter-select { + width: 220px; +} + +.suspicious-type-select { + width: 160px; +} + .result-table { width: 100%; } @@ -1182,6 +1259,21 @@ export default { } @media (max-width: 768px) { + .suspicious-filter-form { + align-items: stretch; + } + + .suspicious-filter-form :deep(.el-form-item) { + width: 100%; + } + + .suspicious-filter-form :deep(.el-form-item__content), + .suspicious-filter-select, + .suspicious-type-select, + .suspicious-filter-form :deep(.el-button) { + width: 100%; + } + .detail-overview-grid { grid-template-columns: 1fr; gap: 18px; diff --git a/ruoyi-ui/src/views/ccdiProject/components/detail/preliminaryCheck.mock.js b/ruoyi-ui/src/views/ccdiProject/components/detail/preliminaryCheck.mock.js index d880c70f..1fc94794 100644 --- a/ruoyi-ui/src/views/ccdiProject/components/detail/preliminaryCheck.mock.js +++ b/ruoyi-ui/src/views/ccdiProject/components/detail/preliminaryCheck.mock.js @@ -463,6 +463,7 @@ export function createOverviewLoadedData({ riskDetails: { ...mockOverviewData.riskDetails, projectId, + riskModelOptions: normalizeRiskModelCards(riskModelCardsData && riskModelCardsData.cardList), suspiciousTransactionList: normalizeSuspiciousTransactions(suspiciousData && suspiciousData.rows), suspiciousType: "ALL", total: suspiciousData && suspiciousData.total ? suspiciousData.total : 0,