完成银行流水打标规则大写编码与后端落地

This commit is contained in:
wkc
2026-03-18 13:44:15 +08:00
parent 9791dab67e
commit b07b725057
18 changed files with 1241 additions and 25 deletions

View File

@@ -89,4 +89,204 @@ public interface CcdiBankTagAnalysisMapper {
*/
List<BankTagStatementHitVO> selectLargeTransferStatements(@Param("projectId") Long projectId,
@Param("threshold") BigDecimal threshold);
/**
* 与客户之间非正常资金往来
*
* @param projectId 项目ID
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectAbnormalCustomerTransactionStatements(@Param("projectId") Long projectId);
/**
* 低收入亲属大额交易
*
* @param projectId 项目ID
* @return 对象命中结果
*/
List<BankTagObjectHitVO> selectLowIncomeRelativeLargeTransactionObjects(@Param("projectId") Long projectId);
/**
* 疑似赌博交易
*
* @param projectId 项目ID
* @return 对象命中结果
*/
List<BankTagObjectHitVO> selectMultiPartyGamblingTransferObjects(@Param("projectId") Long projectId);
/**
* 疑似敏感交易
*
* @param projectId 项目ID
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectGamblingSensitiveKeywordStatements(@Param("projectId") Long projectId);
/**
* 特殊金额交易
*
* @param projectId 项目ID
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectSpecialAmountTransactionStatements(@Param("projectId") Long projectId);
/**
* 月度固定收入疑似兼职
*
* @param projectId 项目ID
* @return 对象命中结果
*/
List<BankTagObjectHitVO> selectMonthlyFixedIncomeObjects(@Param("projectId") Long projectId);
/**
* 固定交易对手转入疑似兼职
*
* @param projectId 项目ID
* @return 对象命中结果
*/
List<BankTagObjectHitVO> selectFixedCounterpartyTransferObjects(@Param("projectId") Long projectId);
/**
* 摘要收入疑似兼职
*
* @param projectId 项目ID
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectSuspiciousIncomeKeywordStatements(@Param("projectId") Long projectId);
/**
* 购房交易与房产登记不匹配
*
* @param projectId 项目ID
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectHouseRegistrationMismatchStatements(@Param("projectId") Long projectId);
/**
* 物业缴费与房产登记不匹配
*
* @param projectId 项目ID
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectPropertyFeeRegistrationMismatchStatements(@Param("projectId") Long projectId);
/**
* 大额纳税与资产登记不匹配
*
* @param projectId 项目ID
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectTaxAssetRegistrationMismatchStatements(@Param("projectId") Long projectId);
/**
* 收入资产不符
*
* @param projectId 项目ID
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectIncomeAssetMismatchStatements(@Param("projectId") Long projectId);
/**
* 单笔购汇金额超限
*
* @param projectId 项目ID
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectForexBuyAmtStatements(@Param("projectId") Long projectId);
/**
* 单笔结汇金额超限
*
* @param projectId 项目ID
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectForexSellAmtStatements(@Param("projectId") Long projectId);
/**
* 单笔跨境汇款金额超限
*
* @param projectId 项目ID
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectCrossBorderAmtStatements(@Param("projectId") Long projectId);
/**
* 可疑付息
*
* @param projectId 项目ID
* @return 对象命中结果
*/
List<BankTagObjectHitVO> selectInterestPaymentByOthersObjects(@Param("projectId") Long projectId);
/**
* 单笔采购金额超过10万元
*
* @param projectId 项目ID
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectLargePurchaseTransactionStatements(@Param("projectId") Long projectId);
/**
* 供应商集中采购
*
* @param projectId 项目ID
* @return 对象命中结果
*/
List<BankTagObjectHitVO> selectSupplierConcentrationObjects(@Param("projectId") Long projectId);
/**
* 可疑银证大额转账
*
* @param projectId 项目ID
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectStockTfrLargeStatements(@Param("projectId") Long projectId);
/**
* 微信支付宝频繁提现
*
* @param projectId 项目ID
* @return 对象命中结果
*/
List<BankTagObjectHitVO> selectWithdrawCntObjects(@Param("projectId") Long projectId);
/**
* 微信支付宝提现超额
*
* @param projectId 项目ID
* @return 对象命中结果
*/
List<BankTagObjectHitVO> selectWithdrawAmtObjects(@Param("projectId") Long projectId);
/**
* 工资快速转出
*
* @param projectId 项目ID
* @return 对象命中结果
*/
List<BankTagObjectHitVO> selectSalaryQuickTransferObjects(@Param("projectId") Long projectId);
/**
* 工资无使用记录
*
* @param projectId 项目ID
* @return 对象命中结果
*/
List<BankTagObjectHitVO> selectSalaryUnusedObjects(@Param("projectId") Long projectId);
/**
* 大额炒股
*
* @param projectId 项目ID
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectLargeStockTradingStatements(@Param("projectId") Long projectId);
/**
* 疑似代理他人账户
*
* @param projectId 项目ID
* @return 对象命中结果
*/
List<BankTagObjectHitVO> selectProxyAccountOperationObjects(@Param("projectId") Long projectId);
}

View File

@@ -27,7 +27,7 @@ public class BankTagRuleConfigResolver {
private static final Map<String, Set<String>> 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"),
"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")

View File

@@ -211,6 +211,20 @@ public class CcdiBankTagServiceImpl implements ICcdiBankTagService {
case "LARGE_TRANSFER" -> analysisMapper.selectLargeTransferStatements(
projectId, toBigDecimal(config.getThresholdValue("FREQUENT_TRANSFER"))
);
case "ABNORMAL_CUSTOMER_TRANSACTION" -> analysisMapper.selectAbnormalCustomerTransactionStatements(projectId);
case "GAMBLING_SENSITIVE_KEYWORD" -> analysisMapper.selectGamblingSensitiveKeywordStatements(projectId);
case "SPECIAL_AMOUNT_TRANSACTION" -> analysisMapper.selectSpecialAmountTransactionStatements(projectId);
case "SUSPICIOUS_INCOME_KEYWORD" -> analysisMapper.selectSuspiciousIncomeKeywordStatements(projectId);
case "HOUSE_REGISTRATION_MISMATCH" -> analysisMapper.selectHouseRegistrationMismatchStatements(projectId);
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 "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);
default -> List.of();
};
}
@@ -223,13 +237,24 @@ public class CcdiBankTagServiceImpl implements ICcdiBankTagService {
projectId, toBigDecimal(config.getThresholdValue("CUMULATIVE_TRANSACTION_AMOUNT"))
);
case "ANNUAL_TURNOVER" -> analysisMapper.selectAnnualTurnoverObjects(
projectId, toBigDecimal(config.getThresholdValue("annual_turnover"))
projectId, toBigDecimal(config.getThresholdValue("ANNUAL_TURNOVER"))
);
case "FREQUENT_CASH_DEPOSIT" -> analysisMapper.selectFrequentCashDepositObjects(
projectId,
toBigDecimal(config.getThresholdValue("LARGE_CASH_DEPOSIT")),
toInteger(config.getThresholdValue("FREQUENT_CASH_DEPOSIT"))
);
case "LOW_INCOME_RELATIVE_LARGE_TRANSACTION" -> analysisMapper.selectLowIncomeRelativeLargeTransactionObjects(projectId);
case "MULTI_PARTY_GAMBLING_TRANSFER" -> analysisMapper.selectMultiPartyGamblingTransferObjects(projectId);
case "MONTHLY_FIXED_INCOME" -> analysisMapper.selectMonthlyFixedIncomeObjects(projectId);
case "FIXED_COUNTERPARTY_TRANSFER" -> analysisMapper.selectFixedCounterpartyTransferObjects(projectId);
case "INTEREST_PAYMENT_BY_OTHERS" -> analysisMapper.selectInterestPaymentByOthersObjects(projectId);
case "SUPPLIER_CONCENTRATION" -> analysisMapper.selectSupplierConcentrationObjects(projectId);
case "WITHDRAW_CNT" -> analysisMapper.selectWithdrawCntObjects(projectId);
case "WITHDRAW_AMT" -> analysisMapper.selectWithdrawAmtObjects(projectId);
case "SALARY_QUICK_TRANSFER" -> analysisMapper.selectSalaryQuickTransferObjects(projectId);
case "SALARY_UNUSED" -> analysisMapper.selectSalaryUnusedObjects(projectId);
case "PROXY_ACCOUNT_OPERATION" -> analysisMapper.selectProxyAccountOperationObjects(projectId);
default -> List.of();
};
}

View File

@@ -363,4 +363,243 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
)
</select>
<select id="selectAbnormalCustomerTransactionStatements" resultMap="BankTagStatementHitResultMap">
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
</select>
<select id="selectLowIncomeRelativeLargeTransactionObjects" resultMap="BankTagObjectHitResultMap">
select
'STAFF_ID_CARD' AS objectType,
'' AS objectKey,
'占位SQL待补充真实规则' AS reasonDetail
from ccdi_bank_statement bs
where 1 = 0
</select>
<select id="selectMultiPartyGamblingTransferObjects" resultMap="BankTagObjectHitResultMap">
select
'STAFF_ID_CARD' AS objectType,
'' AS objectKey,
'占位SQL待补充真实规则' AS reasonDetail
from ccdi_bank_statement bs
where 1 = 0
</select>
<select id="selectGamblingSensitiveKeywordStatements" resultMap="BankTagStatementHitResultMap">
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
</select>
<select id="selectSpecialAmountTransactionStatements" resultMap="BankTagStatementHitResultMap">
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
</select>
<select id="selectMonthlyFixedIncomeObjects" resultMap="BankTagObjectHitResultMap">
select
'STAFF_ID_CARD' AS objectType,
'' AS objectKey,
'占位SQL待补充真实规则' AS reasonDetail
from ccdi_bank_statement bs
where 1 = 0
</select>
<select id="selectFixedCounterpartyTransferObjects" resultMap="BankTagObjectHitResultMap">
select
'STAFF_ID_CARD' AS objectType,
'' AS objectKey,
'占位SQL待补充真实规则' AS reasonDetail
from ccdi_bank_statement bs
where 1 = 0
</select>
<select id="selectSuspiciousIncomeKeywordStatements" resultMap="BankTagStatementHitResultMap">
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
</select>
<select id="selectHouseRegistrationMismatchStatements" resultMap="BankTagStatementHitResultMap">
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
</select>
<select id="selectPropertyFeeRegistrationMismatchStatements" resultMap="BankTagStatementHitResultMap">
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
</select>
<select id="selectTaxAssetRegistrationMismatchStatements" resultMap="BankTagStatementHitResultMap">
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
</select>
<select id="selectIncomeAssetMismatchStatements" resultMap="BankTagStatementHitResultMap">
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
</select>
<select id="selectForexBuyAmtStatements" resultMap="BankTagStatementHitResultMap">
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
</select>
<select id="selectForexSellAmtStatements" resultMap="BankTagStatementHitResultMap">
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
</select>
<select id="selectCrossBorderAmtStatements" resultMap="BankTagStatementHitResultMap">
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
</select>
<select id="selectInterestPaymentByOthersObjects" resultMap="BankTagObjectHitResultMap">
select
'STAFF_ID_CARD' AS objectType,
'' AS objectKey,
'占位SQL待补充真实规则' AS reasonDetail
from ccdi_bank_statement bs
where 1 = 0
</select>
<select id="selectLargePurchaseTransactionStatements" resultMap="BankTagStatementHitResultMap">
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
</select>
<select id="selectSupplierConcentrationObjects" resultMap="BankTagObjectHitResultMap">
select
'STAFF_ID_CARD' AS objectType,
'' AS objectKey,
'占位SQL待补充真实规则' AS reasonDetail
from ccdi_bank_statement bs
where 1 = 0
</select>
<select id="selectStockTfrLargeStatements" resultMap="BankTagStatementHitResultMap">
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
</select>
<select id="selectWithdrawCntObjects" resultMap="BankTagObjectHitResultMap">
select
'STAFF_ID_CARD' AS objectType,
'' AS objectKey,
'占位SQL待补充真实规则' AS reasonDetail
from ccdi_bank_statement bs
where 1 = 0
</select>
<select id="selectWithdrawAmtObjects" resultMap="BankTagObjectHitResultMap">
select
'STAFF_ID_CARD' AS objectType,
'' AS objectKey,
'占位SQL待补充真实规则' AS reasonDetail
from ccdi_bank_statement bs
where 1 = 0
</select>
<select id="selectSalaryQuickTransferObjects" resultMap="BankTagObjectHitResultMap">
select
'STAFF_ID_CARD' AS objectType,
'' AS objectKey,
'占位SQL待补充真实规则' AS reasonDetail
from ccdi_bank_statement bs
where 1 = 0
</select>
<select id="selectSalaryUnusedObjects" resultMap="BankTagObjectHitResultMap">
select
'STAFF_ID_CARD' AS objectType,
'' AS objectKey,
'占位SQL待补充真实规则' AS reasonDetail
from ccdi_bank_statement bs
where 1 = 0
</select>
<select id="selectLargeStockTradingStatements" resultMap="BankTagStatementHitResultMap">
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
</select>
<select id="selectProxyAccountOperationObjects" resultMap="BankTagObjectHitResultMap">
select
'STAFF_ID_CARD' AS objectType,
'' AS objectKey,
'占位SQL待补充真实规则' AS reasonDetail
from ccdi_bank_statement bs
where 1 = 0
</select>
</mapper>

View File

@@ -7,12 +7,41 @@ import javax.xml.parsers.DocumentBuilderFactory;
import java.io.StringReader;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiBankTagAnalysisMapperXmlTest {
private static final String RESOURCE = "mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml";
private static final List<String> PLACEHOLDER_SELECT_IDS = List.of(
"selectAbnormalCustomerTransactionStatements",
"selectLowIncomeRelativeLargeTransactionObjects",
"selectMultiPartyGamblingTransferObjects",
"selectGamblingSensitiveKeywordStatements",
"selectSpecialAmountTransactionStatements",
"selectMonthlyFixedIncomeObjects",
"selectFixedCounterpartyTransferObjects",
"selectSuspiciousIncomeKeywordStatements",
"selectHouseRegistrationMismatchStatements",
"selectPropertyFeeRegistrationMismatchStatements",
"selectTaxAssetRegistrationMismatchStatements",
"selectIncomeAssetMismatchStatements",
"selectForexBuyAmtStatements",
"selectForexSellAmtStatements",
"selectCrossBorderAmtStatements",
"selectInterestPaymentByOthersObjects",
"selectLargePurchaseTransactionStatements",
"selectSupplierConcentrationObjects",
"selectStockTfrLargeStatements",
"selectWithdrawCntObjects",
"selectWithdrawAmtObjects",
"selectSalaryQuickTransferObjects",
"selectSalaryUnusedObjects",
"selectLargeStockTradingStatements",
"selectProxyAccountOperationObjects"
);
@Test
void statementRuleSql_shouldSelectGroupIdAndLogId() throws Exception {
@@ -42,6 +71,21 @@ class CcdiBankTagAnalysisMapperXmlTest {
assertTrue(xml.contains("selectLargeTransferStatements"));
}
@Test
void allPlaceholderRules_shouldExistInAnalysisMapperXml() throws Exception {
String xml = readXml(RESOURCE);
for (String selectId : PLACEHOLDER_SELECT_IDS) {
assertTrue(xml.contains(selectId), () -> "缺少占位规则 SQL: " + selectId);
}
}
@Test
void placeholderRules_shouldUseEmptyResultSqlTemplate() throws Exception {
String xml = readXml(RESOURCE);
assertTrue(xml.contains("占位SQL待补充真实规则"));
assertEquals(25, countMatches(xml, "where 1 = 0"));
}
@Test
void analysisMapperXml_shouldBeWellFormed() throws Exception {
String xml = readXml(RESOURCE);
@@ -56,4 +100,14 @@ class CcdiBankTagAnalysisMapperXmlTest {
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
}
}
private int countMatches(String text, String target) {
int count = 0;
int index = 0;
while ((index = text.indexOf(target, index)) >= 0) {
count++;
index += target.length();
}
return count;
}
}

View File

@@ -90,6 +90,46 @@ class BankTagRuleConfigResolverTest {
}
}
@Test
void resolve_shouldReturnEmptyThresholdsForPlaceholderRulesWithoutIndicatorCode() {
CcdiProject project = new CcdiProject();
project.setProjectId(40L);
project.setConfigType("default");
when(projectMapper.selectById(40L)).thenReturn(project);
when(modelParamMapper.selectByProjectAndModel(0L, "ABNORMAL_TRANSACTION")).thenReturn(List.of(
buildParam("IGNORED_PARAM", "999")
));
CcdiBankTagRule ruleMeta = new CcdiBankTagRule();
ruleMeta.setModelCode("ABNORMAL_TRANSACTION");
ruleMeta.setRuleCode("ABNORMAL_CUSTOMER_TRANSACTION");
ruleMeta.setIndicatorCode(null);
BankTagRuleExecutionConfig config = resolver.resolve(40L, ruleMeta);
assertTrue(config.getThresholdValues().isEmpty());
}
@Test
void resolve_shouldReadUppercaseAnnualTurnoverParamCode() {
CcdiProject project = new CcdiProject();
project.setProjectId(40L);
project.setConfigType("default");
when(projectMapper.selectById(40L)).thenReturn(project);
when(modelParamMapper.selectByProjectAndModel(0L, "LARGE_TRANSACTION")).thenReturn(List.of(
buildParam("ANNUAL_TURNOVER", "8888")
));
CcdiBankTagRule ruleMeta = new CcdiBankTagRule();
ruleMeta.setModelCode("LARGE_TRANSACTION");
ruleMeta.setRuleCode("ANNUAL_TURNOVER");
ruleMeta.setIndicatorCode("ANNUAL_TURNOVER");
BankTagRuleExecutionConfig config = resolver.resolve(40L, ruleMeta);
assertEquals("8888", config.getThresholdValue("ANNUAL_TURNOVER"));
}
private CcdiModelParam buildParam(String paramCode, String paramValue) {
CcdiModelParam param = new CcdiModelParam();
param.setProjectId(0L);

View File

@@ -28,8 +28,10 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -175,4 +177,61 @@ class CcdiBankTagServiceImplTest {
logger.detachAppender(logAppender);
}
}
@Test
void rebuildProject_shouldDispatchPlaceholderStatementRuleAndFinishWithoutResults() {
ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);
CcdiBankTagRule rule = buildRule("ABNORMAL_TRANSACTION", "异常交易",
"ABNORMAL_CUSTOMER_TRANSACTION", "与客户之间非正常资金往来", "STATEMENT");
BankTagRuleExecutionConfig config = buildConfig(40L, rule);
when(ruleMapper.selectEnabledRules("ABNORMAL_TRANSACTION")).thenReturn(List.of(rule));
when(configResolver.resolve(40L, rule)).thenReturn(config);
when(analysisMapper.selectAbnormalCustomerTransactionStatements(40L)).thenReturn(List.of());
service.rebuildProject(40L, "ABNORMAL_TRANSACTION", "admin", TriggerType.MANUAL);
verify(resultMapper).deleteByProjectAndModel(40L, "ABNORMAL_TRANSACTION");
verify(analysisMapper).selectAbnormalCustomerTransactionStatements(40L);
verify(resultMapper, never()).insertBatch(anyList());
verify(taskMapper).updateTask(argThat(task -> "SUCCESS".equals(task.getStatus()) && task.getHitCount() == 0));
}
@Test
void rebuildProject_shouldDispatchPlaceholderObjectRuleAndFinishWithoutResults() {
ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);
CcdiBankTagRule rule = buildRule("SUSPICIOUS_PURCHASE", "可疑采购",
"SUPPLIER_CONCENTRATION", "可疑采购", "OBJECT");
BankTagRuleExecutionConfig config = buildConfig(40L, rule);
when(ruleMapper.selectEnabledRules("SUSPICIOUS_PURCHASE")).thenReturn(List.of(rule));
when(configResolver.resolve(40L, rule)).thenReturn(config);
when(analysisMapper.selectSupplierConcentrationObjects(40L)).thenReturn(List.of());
service.rebuildProject(40L, "SUSPICIOUS_PURCHASE", "admin", TriggerType.MANUAL);
verify(analysisMapper).selectSupplierConcentrationObjects(40L);
verify(resultMapper).deleteByProjectAndModel(40L, "SUSPICIOUS_PURCHASE");
verify(resultMapper, never()).insertBatch(anyList());
verify(taskMapper).updateTask(argThat(task -> "SUCCESS".equals(task.getStatus()) && task.getFailedRuleCount() == 0));
}
private CcdiBankTagRule buildRule(String modelCode, String modelName, String ruleCode, String ruleName, String resultType) {
CcdiBankTagRule rule = new CcdiBankTagRule();
rule.setModelCode(modelCode);
rule.setModelName(modelName);
rule.setRuleCode(ruleCode);
rule.setRuleName(ruleName);
rule.setResultType(resultType);
return rule;
}
private BankTagRuleExecutionConfig buildConfig(Long projectId, CcdiBankTagRule rule) {
BankTagRuleExecutionConfig config = new BankTagRuleExecutionConfig();
config.setProjectId(projectId);
config.setRuleMeta(rule);
return config;
}
}

View File

@@ -65,7 +65,7 @@ class CcdiModelParamServiceImplTest {
when(projectMapper.selectById(123L)).thenReturn(project);
when(modelParamMapper.selectByProjectId(0L)).thenReturn(List.of(
buildParam(1L, 0L, "LARGE_TRANSACTION", "大额交易模型", "SINGLE_TRANSACTION_AMOUNT", "1111"),
buildParam(2L, 0L, "SUSPICIOUS_GAMBLING", "疑似赌博交易模型", "multi_party_amt_min", "500")
buildParam(2L, 0L, "SUSPICIOUS_GAMBLING", "疑似赌博交易模型", "MULTI_PARTY_AMT_MIN", "500")
));
when(modelParamMapper.insertBatch(anyList())).thenReturn(2);
when(modelParamMapper.updateParamValue(123L, "LARGE_TRANSACTION", "SINGLE_TRANSACTION_AMOUNT", "2222", "admin"))

View File

@@ -22,6 +22,15 @@ class CcdiModelParamSqlDefaultsTest {
assertQuarterlyStableIncomeRangeConfig(updateSql);
}
@Test
void defaultSql_shouldUseUppercaseParamCodesForBankTagModels() throws IOException {
String initSql = readProjectFile("sql", "ccdi_model_param.sql");
String updateSql = readProjectFile("sql", "2026-03-16-update-ccdi-model-param-defaults.sql");
assertUppercaseBankTagParamCodes(initSql);
assertUppercaseBankTagParamCodes(updateSql);
}
private void assertQuarterlyStableIncomeRangeConfig(String sqlContent) {
assertAll(
() -> assertTrue(sqlContent.contains("FIXED_COUNTERPARTY_TRANSFER_MIN"),
@@ -41,6 +50,35 @@ class CcdiModelParamSqlDefaultsTest {
);
}
private void assertUppercaseBankTagParamCodes(String sqlContent) {
assertAll(
() -> assertTrue(sqlContent.contains("ANNUAL_TURNOVER"),
"应包含大写参数编码 ANNUAL_TURNOVER"),
() -> assertTrue(sqlContent.contains("STOCK_TFR_LARGE"),
"应包含大写参数编码 STOCK_TFR_LARGE"),
() -> assertTrue(sqlContent.contains("WITHDRAW_CNT"),
"应包含大写参数编码 WITHDRAW_CNT"),
() -> assertTrue(sqlContent.contains("WITHDRAW_AMT"),
"应包含大写参数编码 WITHDRAW_AMT"),
() -> assertTrue(sqlContent.contains("MULTI_PARTY_AMT_MIN"),
"应包含大写参数编码 MULTI_PARTY_AMT_MIN"),
() -> assertTrue(sqlContent.contains("MULTI_PARTY_AMT_MAX"),
"应包含大写参数编码 MULTI_PARTY_AMT_MAX"),
() -> assertFalse(sqlContent.contains("'annual_turnover'"),
"不应继续保留小写参数编码 annual_turnover"),
() -> assertFalse(sqlContent.contains("'stock_tfr_large'"),
"不应继续保留小写参数编码 stock_tfr_large"),
() -> assertFalse(sqlContent.contains("'withdraw_cnt'"),
"不应继续保留小写参数编码 withdraw_cnt"),
() -> assertFalse(sqlContent.contains("'withdraw_amt'"),
"不应继续保留小写参数编码 withdraw_amt"),
() -> assertFalse(sqlContent.contains("'multi_party_amt_min'"),
"不应继续保留小写参数编码 multi_party_amt_min"),
() -> assertFalse(sqlContent.contains("'multi_party_amt_max'"),
"不应继续保留小写参数编码 multi_party_amt_max")
);
}
private String readProjectFile(String... parts) throws IOException {
Path path = Path.of("..", parts);
return Files.readString(path, StandardCharsets.UTF_8);