From 1c73322f94561428111d10c3854a0753aaffd99a Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Fri, 20 Mar 2026 13:22:26 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E7=AC=AC=E4=B8=80?= =?UTF-8?q?=E6=9C=9F=E6=B5=81=E6=B0=B4=E6=A8=A1=E5=9E=8B=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E6=98=A0=E5=B0=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/BankTagRuleConfigResolver.java | 19 +++-- .../impl/BankTagRuleConfigResolverTest.java | 71 +++++++++++++++++++ 2 files changed, 83 insertions(+), 7 deletions(-) diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolver.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolver.java index 2fdfc90b..0c1edf3b 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolver.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolver.java @@ -24,13 +24,18 @@ import lombok.extern.slf4j.Slf4j; @Component public class BankTagRuleConfigResolver { - private static final Map> RULE_PARAM_MAPPING = Map.of( - "SINGLE_LARGE_INCOME", Set.of("SINGLE_TRANSACTION_AMOUNT"), - "CUMULATIVE_INCOME", Set.of("CUMULATIVE_TRANSACTION_AMOUNT"), - "ANNUAL_TURNOVER", Set.of("ANNUAL_TURNOVER"), - "LARGE_CASH_DEPOSIT", Set.of("LARGE_CASH_DEPOSIT"), - "FREQUENT_CASH_DEPOSIT", Set.of("LARGE_CASH_DEPOSIT", "FREQUENT_CASH_DEPOSIT"), - "LARGE_TRANSFER", Set.of("FREQUENT_TRANSFER") + private static final Map> RULE_PARAM_MAPPING = Map.ofEntries( + Map.entry("SINGLE_LARGE_INCOME", Set.of("SINGLE_TRANSACTION_AMOUNT")), + Map.entry("CUMULATIVE_INCOME", Set.of("CUMULATIVE_TRANSACTION_AMOUNT")), + Map.entry("ANNUAL_TURNOVER", Set.of("ANNUAL_TURNOVER")), + Map.entry("LARGE_CASH_DEPOSIT", Set.of("LARGE_CASH_DEPOSIT")), + Map.entry("FREQUENT_CASH_DEPOSIT", Set.of("LARGE_CASH_DEPOSIT", "FREQUENT_CASH_DEPOSIT")), + Map.entry("LARGE_TRANSFER", Set.of("FREQUENT_TRANSFER")), + Map.entry("FOREX_BUY_AMT", Set.of("SINGLE_PURCHASE_AMOUNT")), + Map.entry("FOREX_SELL_AMT", Set.of("SINGLE_SETTLEMENT_AMOUNT")), + Map.entry("WITHDRAW_CNT", Set.of("WITHDRAW_CNT")), + Map.entry("STOCK_TFR_LARGE", Set.of("STOCK_TFR_LARGE")), + Map.entry("LARGE_STOCK_TRADING", Set.of("STOCK_TFR_LARGE")) ); @Resource diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolverTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolverTest.java index 168d5844..43f20125 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolverTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/BankTagRuleConfigResolverTest.java @@ -17,6 +17,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.slf4j.LoggerFactory; import java.util.List; +import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -130,6 +131,38 @@ class BankTagRuleConfigResolverTest { assertEquals("8888", config.getThresholdValue("ANNUAL_TURNOVER")); } + @Test + void resolve_shouldMapPhaseOneThresholdRulesToUppercaseParamCodes() { + CcdiProject project = new CcdiProject(); + project.setProjectId(40L); + project.setConfigType("default"); + when(projectMapper.selectById(40L)).thenReturn(project); + + assertSingleThresholdRule("SUSPICIOUS_FOREIGN_EXCHANGE", "FOREX_BUY_AMT", + "SINGLE_PURCHASE_AMOUNT", "50000"); + assertSingleThresholdRule("SUSPICIOUS_FOREIGN_EXCHANGE", "FOREX_SELL_AMT", + "SINGLE_SETTLEMENT_AMOUNT", "60000"); + assertSingleThresholdRule("ABNORMAL_BEHAVIOR", "WITHDRAW_CNT", + "WITHDRAW_CNT", "3"); + assertSingleThresholdRule("ABNORMAL_BEHAVIOR", "STOCK_TFR_LARGE", + "STOCK_TFR_LARGE", "1000000"); + assertSingleThresholdRule("ABNORMAL_BEHAVIOR", "LARGE_STOCK_TRADING", + "STOCK_TFR_LARGE", "1000000"); + } + + @Test + void resolve_shouldKeepEmptyThresholdsForPhaseOneRulesWithoutParams() { + CcdiProject project = new CcdiProject(); + project.setProjectId(40L); + project.setConfigType("default"); + when(projectMapper.selectById(40L)).thenReturn(project); + + assertRuleHasNoThresholds("SUSPICIOUS_GAMBLING", "GAMBLING_SENSITIVE_KEYWORD"); + assertRuleHasNoThresholds("SUSPICIOUS_RELATION", "SPECIAL_AMOUNT_TRANSACTION"); + assertRuleHasNoThresholds("SUSPICIOUS_PART_TIME", "SUSPICIOUS_INCOME_KEYWORD"); + assertRuleHasNoThresholds("SUSPICIOUS_PURCHASE", "LARGE_PURCHASE_TRANSACTION"); + } + private CcdiModelParam buildParam(String paramCode, String paramValue) { CcdiModelParam param = new CcdiModelParam(); param.setProjectId(0L); @@ -138,4 +171,42 @@ class BankTagRuleConfigResolverTest { param.setParamValue(paramValue); return param; } + + private void assertSingleThresholdRule(String modelCode, String ruleCode, String paramCode, String paramValue) { + when(modelParamMapper.selectByProjectAndModel(0L, modelCode)).thenReturn(List.of( + buildParam(modelCode, paramCode, paramValue) + )); + + CcdiBankTagRule ruleMeta = new CcdiBankTagRule(); + ruleMeta.setModelCode(modelCode); + ruleMeta.setRuleCode(ruleCode); + ruleMeta.setIndicatorCode(paramCode); + + BankTagRuleExecutionConfig config = resolver.resolve(40L, ruleMeta); + + assertEquals(Map.of(paramCode, paramValue), config.getThresholdValues()); + } + + private void assertRuleHasNoThresholds(String modelCode, String ruleCode) { + when(modelParamMapper.selectByProjectAndModel(0L, modelCode)).thenReturn(List.of( + buildParam(modelCode, "IGNORED_PARAM", "999") + )); + + CcdiBankTagRule ruleMeta = new CcdiBankTagRule(); + ruleMeta.setModelCode(modelCode); + ruleMeta.setRuleCode(ruleCode); + + BankTagRuleExecutionConfig config = resolver.resolve(40L, ruleMeta); + + assertTrue(config.getThresholdValues().isEmpty()); + } + + private CcdiModelParam buildParam(String modelCode, String paramCode, String paramValue) { + CcdiModelParam param = new CcdiModelParam(); + param.setProjectId(0L); + param.setModelCode(modelCode); + param.setParamCode(paramCode); + param.setParamValue(paramValue); + return param; + } } From edf5869ebabc71c22249439afb46d5db15da2d1d Mon Sep 17 00:00:00 2001 From: wkc <978997012@qq.com> Date: Fri, 20 Mar 2026 13:26:41 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E7=AC=AC=E4=B8=80?= =?UTF-8?q?=E6=9C=9F=E6=B5=81=E6=B0=B4=E6=98=8E=E7=BB=86=E8=A7=84=E5=88=99?= =?UTF-8?q?=E7=9C=9F=E5=AE=9ESQL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapper/CcdiBankTagAnalysisMapper.java | 16 +- .../service/impl/CcdiBankTagServiceImpl.java | 16 +- .../project/CcdiBankTagAnalysisMapper.xml | 146 +++++++++++++++--- .../CcdiBankTagAnalysisMapperXmlTest.java | 47 ++++-- 4 files changed, 188 insertions(+), 37 deletions(-) diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagAnalysisMapper.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagAnalysisMapper.java index 5e78ef3a..9e44654f 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagAnalysisMapper.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagAnalysisMapper.java @@ -190,17 +190,21 @@ public interface CcdiBankTagAnalysisMapper { * 单笔购汇金额超限 * * @param projectId 项目ID + * @param threshold 单笔购汇阈值 * @return 流水命中结果 */ - List selectForexBuyAmtStatements(@Param("projectId") Long projectId); + List selectForexBuyAmtStatements(@Param("projectId") Long projectId, + @Param("threshold") BigDecimal threshold); /** * 单笔结汇金额超限 * * @param projectId 项目ID + * @param threshold 单笔结汇阈值 * @return 流水命中结果 */ - List selectForexSellAmtStatements(@Param("projectId") Long projectId); + List selectForexSellAmtStatements(@Param("projectId") Long projectId, + @Param("threshold") BigDecimal threshold); /** * 单笔跨境汇款金额超限 @@ -238,9 +242,11 @@ public interface CcdiBankTagAnalysisMapper { * 可疑银证大额转账 * * @param projectId 项目ID + * @param threshold 银证转账阈值 * @return 流水命中结果 */ - List selectStockTfrLargeStatements(@Param("projectId") Long projectId); + List selectStockTfrLargeStatements(@Param("projectId") Long projectId, + @Param("threshold") BigDecimal threshold); /** * 微信支付宝频繁提现 @@ -278,9 +284,11 @@ public interface CcdiBankTagAnalysisMapper { * 大额炒股 * * @param projectId 项目ID + * @param threshold 三方资管交易阈值 * @return 流水命中结果 */ - List selectLargeStockTradingStatements(@Param("projectId") Long projectId); + List selectLargeStockTradingStatements(@Param("projectId") Long projectId, + @Param("threshold") BigDecimal threshold); /** * 疑似代理他人账户 diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java index f0fae6da..9e266833 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiBankTagServiceImpl.java @@ -233,12 +233,20 @@ public class CcdiBankTagServiceImpl implements ICcdiBankTagService { case "PROPERTY_FEE_REGISTRATION_MISMATCH" -> analysisMapper.selectPropertyFeeRegistrationMismatchStatements(projectId); case "TAX_ASSET_REGISTRATION_MISMATCH" -> analysisMapper.selectTaxAssetRegistrationMismatchStatements(projectId); case "INCOME_ASSET_MISMATCH" -> analysisMapper.selectIncomeAssetMismatchStatements(projectId); - case "FOREX_BUY_AMT" -> analysisMapper.selectForexBuyAmtStatements(projectId); - case "FOREX_SELL_AMT" -> analysisMapper.selectForexSellAmtStatements(projectId); + case "FOREX_BUY_AMT" -> analysisMapper.selectForexBuyAmtStatements( + projectId, toBigDecimal(config.getThresholdValue("SINGLE_PURCHASE_AMOUNT")) + ); + case "FOREX_SELL_AMT" -> analysisMapper.selectForexSellAmtStatements( + projectId, toBigDecimal(config.getThresholdValue("SINGLE_SETTLEMENT_AMOUNT")) + ); case "CROSS_BORDER_AMT" -> analysisMapper.selectCrossBorderAmtStatements(projectId); case "LARGE_PURCHASE_TRANSACTION" -> analysisMapper.selectLargePurchaseTransactionStatements(projectId); - case "STOCK_TFR_LARGE" -> analysisMapper.selectStockTfrLargeStatements(projectId); - case "LARGE_STOCK_TRADING" -> analysisMapper.selectLargeStockTradingStatements(projectId); + case "STOCK_TFR_LARGE" -> analysisMapper.selectStockTfrLargeStatements( + projectId, toBigDecimal(config.getThresholdValue("STOCK_TFR_LARGE")) + ); + case "LARGE_STOCK_TRADING" -> analysisMapper.selectLargeStockTradingStatements( + projectId, toBigDecimal(config.getThresholdValue("STOCK_TFR_LARGE")) + ); default -> List.of(); }; } diff --git a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml index b930686c..dd525fce 100644 --- a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml +++ b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml @@ -396,9 +396,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" bs.bank_statement_id AS bankStatementId, bs.group_id AS groupId, bs.batch_id AS logId, - '占位SQL,待补充真实规则' AS reasonDetail + CONCAT( + '摘要/对手命中赌博敏感词,摘要“', IFNULL(bs.USER_MEMO, ''), + '”,对手方“', IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''), + '”,支出金额 ', CAST(IFNULL(bs.AMOUNT_DR, 0) AS CHAR), ' 元' + ) AS reasonDetail from ccdi_bank_statement bs - where 1 = 0 + inner join ccdi_base_staff staff on staff.id_card = bs.cret_no + where bs.project_id = #{projectId} + and IFNULL(bs.AMOUNT_DR, 0) > 0 + and ( + IFNULL(bs.USER_MEMO, '') REGEXP '游戏|抖币|体彩|福彩|彩票|赌|球|外围|博彩|六合|时时彩|赛车|赌场|筹码|盘口|返水|洗码|庄家|闲家|百家乐|斗牛|炸金花|牌九|麻将|捕鱼|电子游艺|投注' + or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '游戏|抖币|体彩|福彩|彩票|赌|球|外围|博彩|六合|时时彩|赛车|赌场|筹码|盘口|返水|洗码|庄家|闲家|百家乐|斗牛|炸金花|牌九|麻将|捕鱼|电子游艺|投注' + ) select - bs.bank_statement_id AS bankStatementId, - bs.group_id AS groupId, - bs.batch_id AS logId, - '占位SQL,待补充真实规则' AS reasonDetail - from ccdi_bank_statement bs - where 1 = 0 + CAST(NULL AS SIGNED) AS bankStatementId, + CAST(NULL AS SIGNED) AS groupId, + CAST(NULL AS SIGNED) AS logId, + CONCAT( + '采购事项“', IFNULL(t.subjectName, ''), + '”实际采购金额 ', CAST(IFNULL(t.actualAmount, 0) AS CHAR), + ' 元,供应商“', IFNULL(t.supplierName, ''), '”' + ) AS reasonDetail + from ( + select distinct + pt.purchase_id AS purchaseId, + pt.subject_name AS subjectName, + pt.supplier_name AS supplierName, + pt.actual_amount AS actualAmount + from ccdi_purchase_transaction pt + inner join ccdi_base_staff staff + on CAST(staff.staff_id AS CHAR) = pt.applicant_id + where IFNULL(pt.actual_amount, 0) > 100000 + union + select distinct + pt.purchase_id AS purchaseId, + pt.subject_name AS subjectName, + pt.supplier_name AS supplierName, + pt.actual_amount AS actualAmount + from ccdi_purchase_transaction pt + inner join ccdi_base_staff staff + on CAST(staff.staff_id AS CHAR) = pt.purchase_leader_id + where pt.purchase_leader_id is not null + and IFNULL(pt.actual_amount, 0) > 100000 + ) t select 'STAFF_ID_CARD' AS objectType, - '' AS objectKey, - '占位SQL,待补充真实规则' AS reasonDetail - from ccdi_bank_statement bs - where 1 = 0 + t.objectKey AS objectKey, + CONCAT( + '单日微信/支付宝提现 ', CAST(t.withdrawCount AS CHAR), + ' 次,超过阈值 ', CAST(#{frequencyThreshold} AS CHAR), + ' 次,交易日:', t.transDate + ) AS reasonDetail + from ( + select + staff.id_card AS objectKey, + LEFT(TRIM(bs.TRX_DATE), 10) AS transDate, + COUNT(1) AS withdrawCount + from ccdi_bank_statement bs + inner join ccdi_base_staff staff on staff.id_card = bs.cret_no + where bs.project_id = #{projectId} + and IFNULL(bs.AMOUNT_CR, 0) >= 0 + and ( + IFNULL(bs.USER_MEMO, '') REGEXP '财付通|微信零钱|微信|wechat|WeChat|Tenpay|支付宝|Alipay|提现' + or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '财付通|微信零钱|微信|wechat|WeChat|Tenpay|支付宝|Alipay|提现' + ) + group by staff.id_card, LEFT(TRIM(bs.TRX_DATE), 10) + having COUNT(1) > #{frequencyThreshold} + ) t