接通第一期对象规则真实分发

This commit is contained in:
wkc
2026-03-20 13:28:43 +08:00
parent 7d943f96cc
commit 2f86472091
5 changed files with 87 additions and 7 deletions

View File

@@ -252,9 +252,11 @@ public interface CcdiBankTagAnalysisMapper {
* 微信支付宝频繁提现
*
* @param projectId 项目ID
* @param frequencyThreshold 提现频次阈值
* @return 对象命中结果
*/
List<BankTagObjectHitVO> selectWithdrawCntObjects(@Param("projectId") Long projectId);
List<BankTagObjectHitVO> selectWithdrawCntObjects(@Param("projectId") Long projectId,
@Param("frequencyThreshold") Integer frequencyThreshold);
/**
* 微信支付宝提现超额

View File

@@ -272,7 +272,9 @@ public class CcdiBankTagServiceImpl implements ICcdiBankTagService {
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_CNT" -> analysisMapper.selectWithdrawCntObjects(
projectId, toInteger(config.getThresholdValue("WITHDRAW_CNT"))
);
case "WITHDRAW_AMT" -> analysisMapper.selectWithdrawAmtObjects(projectId);
case "SALARY_QUICK_TRANSFER" -> analysisMapper.selectSalaryQuickTransferObjects(projectId);
case "SALARY_UNUSED" -> analysisMapper.selectSalaryUnusedObjects(projectId);

View File

@@ -644,10 +644,28 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="selectWithdrawCntObjects" resultMap="BankTagObjectHitResultMap">
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
</select>
<select id="selectWithdrawAmtObjects" resultMap="BankTagObjectHitResultMap">

View File

@@ -90,7 +90,7 @@ class CcdiBankTagAnalysisMapperXmlTest {
void placeholderRules_shouldUseEmptyResultSqlTemplate() throws Exception {
String xml = readXml(RESOURCE);
assertTrue(xml.contains("占位SQL待补充真实规则"));
assertEquals(17, countMatches(xml, "where 1 = 0"));
assertEquals(16, countMatches(xml, "where 1 = 0"));
}
@Test
@@ -106,6 +106,16 @@ class CcdiBankTagAnalysisMapperXmlTest {
}
}
@Test
void withdrawCntObjectRule_shouldUseRealSqlAndKeepObjectHitFields() throws Exception {
String xml = readXml(RESOURCE);
String selectSql = extractSelectSql(xml, "selectWithdrawCntObjects");
assertTrue(selectSql.contains("'STAFF_ID_CARD' AS objectType"));
assertTrue(selectSql.contains("AS objectKey"));
assertTrue(selectSql.contains("reasonDetail"));
assertTrue(!selectSql.contains("where 1 = 0"));
}
@Test
void analysisMapperXml_shouldBeWellFormed() throws Exception {
String xml = readXml(RESOURCE);

View File

@@ -6,6 +6,7 @@ import ch.qos.logback.core.read.ListAppender;
import com.ruoyi.ccdi.project.domain.entity.CcdiBankTagRule;
import com.ruoyi.ccdi.project.domain.entity.CcdiBankTagTask;
import com.ruoyi.ccdi.project.domain.enums.TriggerType;
import com.ruoyi.ccdi.project.domain.vo.BankTagObjectHitVO;
import com.ruoyi.ccdi.project.domain.vo.BankTagRuleExecutionConfig;
import com.ruoyi.ccdi.project.domain.vo.BankTagStatementHitVO;
import com.ruoyi.ccdi.project.mapper.CcdiBankTagAnalysisMapper;
@@ -24,6 +25,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -243,6 +245,52 @@ class CcdiBankTagServiceImplTest {
verify(taskMapper).updateTask(argThat(task -> "SUCCESS".equals(task.getStatus()) && task.getFailedRuleCount() == 0));
}
@Test
void rebuildProject_shouldDispatchWithdrawCntObjectRuleWithResolvedThreshold() {
ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);
CcdiBankTagRule rule = buildRule("ABNORMAL_BEHAVIOR", "异常行为",
"WITHDRAW_CNT", "微信支付宝频繁提现", "OBJECT");
BankTagRuleExecutionConfig config = buildConfig(40L, rule);
config.setThresholdValues(Map.of("WITHDRAW_CNT", "3"));
BankTagObjectHitVO hit = new BankTagObjectHitVO();
hit.setObjectType("STAFF_ID_CARD");
hit.setObjectKey("330101199001011234");
hit.setReasonDetail("单日微信提现 4 次,超过阈值 3 次");
when(ruleMapper.selectEnabledRules("ABNORMAL_BEHAVIOR")).thenReturn(List.of(rule));
when(configResolver.resolve(40L, rule)).thenReturn(config);
when(analysisMapper.selectWithdrawCntObjects(40L, 3)).thenReturn(List.of(hit));
service.rebuildProject(40L, "ABNORMAL_BEHAVIOR", "admin", TriggerType.MANUAL);
verify(analysisMapper).selectWithdrawCntObjects(40L, 3);
verify(resultMapper).insertBatch(argThat(results -> results.size() == 1
&& "STAFF_ID_CARD".equals(results.get(0).getObjectType())
&& "330101199001011234".equals(results.get(0).getObjectKey())));
}
@Test
void rebuildProject_shouldTreatEmptyWithdrawCntHitsAsSuccess() {
ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);
CcdiBankTagRule rule = buildRule("ABNORMAL_BEHAVIOR", "异常行为",
"WITHDRAW_CNT", "微信支付宝频繁提现", "OBJECT");
BankTagRuleExecutionConfig config = buildConfig(40L, rule);
config.setThresholdValues(Map.of("WITHDRAW_CNT", "3"));
when(ruleMapper.selectEnabledRules("ABNORMAL_BEHAVIOR")).thenReturn(List.of(rule));
when(configResolver.resolve(40L, rule)).thenReturn(config);
when(analysisMapper.selectWithdrawCntObjects(40L, 3)).thenReturn(List.of());
service.rebuildProject(40L, "ABNORMAL_BEHAVIOR", "admin", TriggerType.MANUAL);
verify(analysisMapper).selectWithdrawCntObjects(40L, 3);
verify(resultMapper, never()).insertBatch(anyList());
verify(taskMapper).updateTask(argThat(task -> "SUCCESS".equals(task.getStatus()) && task.getHitCount() == 0));
}
@Test
void shouldMarkProjectTaggingBeforeExecutingAndCompletedAfterSuccess() {
ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);