feat: 补齐流水标签规则分析SQL

This commit is contained in:
wkc
2026-03-16 18:23:45 +08:00
parent 1a49b6b7e1
commit b948c846b1
5 changed files with 546 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
/**
* 对象级标签命中结果
*/
@Data
public class BankTagObjectHitVO {
/** 对象类型 */
private String objectType;
/** 对象主键 */
private String objectKey;
/** 异常原因摘要 */
private String reasonDetail;
}

View File

@@ -0,0 +1,22 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
/**
* 流水级标签命中结果
*/
@Data
public class BankTagStatementHitVO {
/** 流水ID */
private Long bankStatementId;
/** 项目分组ID */
private Integer groupId;
/** 上传日志ID */
private Integer logId;
/** 异常原因摘要 */
private String reasonDetail;
}

View File

@@ -0,0 +1,92 @@
package com.ruoyi.ccdi.project.mapper;
import com.ruoyi.ccdi.project.domain.vo.BankTagStatementHitVO;
import com.ruoyi.ccdi.project.domain.vo.BankTagObjectHitVO;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
/**
* 流水标签分析 Mapper
*/
public interface CcdiBankTagAnalysisMapper {
/**
* 房车消费支出交易
*
* @param projectId 项目ID
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectHouseOrCarExpenseStatements(@Param("projectId") Long projectId);
/**
* 税务支出交易
*
* @param projectId 项目ID
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectTaxExpenseStatements(@Param("projectId") Long projectId);
/**
* 大额单笔收入
*
* @param projectId 项目ID
* @param threshold 单笔阈值
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectSingleLargeIncomeStatements(@Param("projectId") Long projectId,
@Param("threshold") BigDecimal threshold);
/**
* 累计收入超限对象
*
* @param projectId 项目ID
* @param threshold 累计阈值
* @return 对象命中结果
*/
List<BankTagObjectHitVO> selectCumulativeIncomeObjects(@Param("projectId") Long projectId,
@Param("threshold") BigDecimal threshold);
/**
* 年流水交易额超限对象
*
* @param projectId 项目ID
* @param threshold 年交易额阈值
* @return 对象命中结果
*/
List<BankTagObjectHitVO> selectAnnualTurnoverObjects(@Param("projectId") Long projectId,
@Param("threshold") BigDecimal threshold);
/**
* 大额存现交易
*
* @param projectId 项目ID
* @param threshold 存现阈值
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectLargeCashDepositStatements(@Param("projectId") Long projectId,
@Param("threshold") BigDecimal threshold);
/**
* 短时间多次存现对象
*
* @param projectId 项目ID
* @param amountThreshold 单笔存现阈值
* @param frequencyThreshold 频次阈值
* @return 对象命中结果
*/
List<BankTagObjectHitVO> selectFrequentCashDepositObjects(@Param("projectId") Long projectId,
@Param("amountThreshold") BigDecimal amountThreshold,
@Param("frequencyThreshold") Integer frequencyThreshold);
/**
* 大额转账交易
*
* @param projectId 项目ID
* @param threshold 转账阈值
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectLargeTransferStatements(@Param("projectId") Long projectId,
@Param("threshold") BigDecimal threshold);
}

View File

@@ -0,0 +1,366 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.ccdi.project.mapper.CcdiBankTagAnalysisMapper">
<resultMap id="BankTagStatementHitResultMap" type="com.ruoyi.ccdi.project.domain.vo.BankTagStatementHitVO">
<id property="bankStatementId" column="bankStatementId"/>
<result property="groupId" column="groupId"/>
<result property="logId" column="logId"/>
<result property="reasonDetail" column="reasonDetail"/>
</resultMap>
<resultMap id="BankTagObjectHitResultMap" type="com.ruoyi.ccdi.project.domain.vo.BankTagObjectHitVO">
<result property="objectType" column="objectType"/>
<result property="objectKey" column="objectKey"/>
<result property="reasonDetail" column="reasonDetail"/>
</resultMap>
<sql id="statementHitColumns">
NULL AS bankStatementId,
NULL AS groupId,
NULL AS logId,
NULL AS reasonDetail
</sql>
<sql id="objectHitColumns">
NULL AS objectType,
NULL AS objectKey,
NULL AS reasonDetail
</sql>
<sql id="cashDepositPredicate">
(
(
(
(
IFNULL(bs.USER_MEMO, '') LIKE '%现金%'
and IFNULL(bs.USER_MEMO, '') NOT LIKE '%金管理%'
and IFNULL(bs.USER_MEMO, '') NOT LIKE '%金添利%'
and IFNULL(bs.USER_MEMO, '') NOT LIKE '%现金利%'
and IFNULL(bs.USER_MEMO, '') NOT LIKE '%现金宝%'
and IFNULL(bs.USER_MEMO, '') NOT LIKE '%金分析%'
)
or IFNULL(bs.USER_MEMO, '') LIKE '%存现%'
or IFNULL(bs.USER_MEMO, '') LIKE '%现存%'
or IFNULL(bs.CASH_TYPE, '') LIKE '%现金%'
or IFNULL(bs.CASH_TYPE, '') LIKE '%存现%'
or IFNULL(bs.CASH_TYPE, '') LIKE '%现存%'
or IFNULL(bs.CASH_TYPE, '') LIKE '%金存入%'
or IFNULL(bs.USER_MEMO, '') LIKE '%金存入%'
or (
IFNULL(bs.USER_MEMO, '') LIKE '%ATM%'
and (
IFNULL(bs.USER_MEMO, '') LIKE '%存款%'
or IFNULL(bs.USER_MEMO, '') LIKE '%转入%'
)
)
or (
IFNULL(bs.CASH_TYPE, '') LIKE '%ATM%'
and (
IFNULL(bs.CASH_TYPE, '') LIKE '%存款%'
or IFNULL(bs.CASH_TYPE, '') LIKE '%转入%'
)
)
)
and (
IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') = ''
or bs.CUSTOMER_ACCOUNT_NAME = '无'
or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') LIKE '%存现%'
)
)
or IFNULL(bs.USER_MEMO, '') LIKE '%DEPOSIT%'
or (
bs.CUSTOMER_ACCOUNT_NAME = '库存现金'
or (
(
IFNULL(bs.USER_MEMO, '') LIKE '%现金存款%'
or IFNULL(bs.USER_MEMO, '') LIKE '%自助存款%'
or IFNULL(bs.USER_MEMO, '') LIKE '%CRS存款%'
or IFNULL(bs.CASH_TYPE, '') LIKE '%现金存款%'
or IFNULL(bs.CASH_TYPE, '') LIKE '%自助存款%'
or IFNULL(bs.CASH_TYPE, '') LIKE '%本行CRS存款%'
or IFNULL(bs.CASH_TYPE, '') LIKE '%柜面%'
or IFNULL(bs.USER_MEMO, '') LIKE '%柜面%'
)
and IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') = ''
)
or (bs.CUSTOMER_ACCOUNT_NAME = '现金' and IFNULL(bs.USER_MEMO, '') NOT LIKE '%借款%')
or IFNULL(bs.USER_MEMO, '') LIKE '%本行ATM%'
)
)
</sql>
<sql id="salaryExclusionPredicate">
not (
bs.CUSTOMER_ACCOUNT_NAME = '浙江兰溪农村商业银行股份有限公司'
and (
IFNULL(bs.USER_MEMO, '') LIKE '%代发%'
or IFNULL(bs.USER_MEMO, '') LIKE '%工资%'
or IFNULL(bs.USER_MEMO, '') LIKE '%奖金%'
or IFNULL(bs.USER_MEMO, '') LIKE '%薪酬%'
or IFNULL(bs.USER_MEMO, '') LIKE '%薪金%'
or IFNULL(bs.USER_MEMO, '') LIKE '%补贴%'
or IFNULL(bs.USER_MEMO, '') LIKE '%薪%'
or IFNULL(bs.USER_MEMO, '') LIKE '%年终奖%'
or IFNULL(bs.USER_MEMO, '') LIKE '%年金%'
or IFNULL(bs.USER_MEMO, '') LIKE '%加班费%'
or IFNULL(bs.USER_MEMO, '') LIKE '%劳务费%'
or IFNULL(bs.USER_MEMO, '') LIKE '%劳务外包%'
or IFNULL(bs.USER_MEMO, '') LIKE '%提成%'
or IFNULL(bs.USER_MEMO, '') LIKE '%劳务派遣%'
or IFNULL(bs.USER_MEMO, '') LIKE '%绩效%'
or IFNULL(bs.USER_MEMO, '') LIKE '%酬劳%'
or IFNULL(bs.USER_MEMO, '') LIKE '%PAYROLL%'
or IFNULL(bs.USER_MEMO, '') LIKE '%SALA%'
or IFNULL(bs.USER_MEMO, '') LIKE '%CPF%'
or IFNULL(bs.USER_MEMO, '') LIKE '%directors%fees%'
or IFNULL(bs.USER_MEMO, '') LIKE '%批量代付%'
or IFNULL(bs.CASH_TYPE, '') LIKE '%代发%'
or IFNULL(bs.CASH_TYPE, '') LIKE '%工资%'
or IFNULL(bs.CASH_TYPE, '') LIKE '%劳务费%'
)
)
</sql>
<select id="selectHouseOrCarExpenseStatements" resultMap="BankTagStatementHitResultMap">
select
bs.bank_statement_id AS bankStatementId,
bs.group_id AS groupId,
bs.batch_id AS logId,
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 bs.project_id = #{projectId}
and IFNULL(bs.AMOUNT_DR, 0) > 0
and (
IFNULL(bs.USER_MEMO, '') REGEXP '(购|买).*房|(购|买).*车|车款|房款|首付|(房|车).*贷'
or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '汽车销售|汽车金融|4S店|汽贸|车行|房地产|置业|置地|地产|房产|不动产|链家|贝壳|我爱我家|房管局'
)
and (
exists (
select 1
from ccdi_base_staff staff
where staff.id_card = bs.cret_no
)
or exists (
select 1
from ccdi_staff_fmy_relation relation
where relation.relation_cert_no = bs.cret_no
and relation.status = 1
)
)
</select>
<select id="selectTaxExpenseStatements" resultMap="BankTagStatementHitResultMap">
select
bs.bank_statement_id AS bankStatementId,
bs.group_id AS groupId,
bs.batch_id AS logId,
CONCAT(
'摘要命中税务关键词,对手方“', IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''),
'”,支出金额 ', CAST(IFNULL(bs.AMOUNT_DR, 0) AS CHAR), ' 元'
) AS reasonDetail
from ccdi_bank_statement bs
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 '税务|税务局|国库|国家金库|财政'
)
and (
exists (
select 1
from ccdi_base_staff staff
where staff.id_card = bs.cret_no
)
or exists (
select 1
from ccdi_staff_fmy_relation relation
where relation.relation_cert_no = bs.cret_no
and relation.status = 1
)
)
</select>
<select id="selectSingleLargeIncomeStatements" resultMap="BankTagStatementHitResultMap">
select
bs.bank_statement_id AS bankStatementId,
bs.group_id AS groupId,
bs.batch_id AS logId,
CONCAT(
'同一交易对手“', IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''),
'”单笔流入 ', CAST(IFNULL(bs.AMOUNT_CR, 0) AS CHAR),
' 元,超过阈值 ', CAST(#{threshold} AS CHAR), ' 元'
) AS reasonDetail
from ccdi_bank_statement bs
inner join ccdi_base_staff staff on staff.id_card = bs.cret_no
left join ccdi_staff_fmy_relation relation
on relation.person_id = staff.id_card
and relation.relation_name = bs.CUSTOMER_ACCOUNT_NAME
and relation.status = 1
where bs.project_id = #{projectId}
and IFNULL(bs.AMOUNT_CR, 0) > #{threshold}
and IFNULL(bs.AMOUNT_CR, 0) > 0
and IFNULL(bs.LE_ACCOUNT_NAME, '') <> IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '')
and relation.person_id is null
and <include refid="salaryExclusionPredicate"/>
</select>
<select id="selectCumulativeIncomeObjects" resultMap="BankTagObjectHitResultMap">
select
'STAFF_ID_CARD' AS objectType,
t.idCard AS objectKey,
CONCAT(
'同一交易对手累计流入 ', CAST(t.totalAmount AS CHAR),
' 元,超过阈值 ', CAST(#{threshold} AS CHAR),
' 元,对手方:', IFNULL(t.customerAccountName, '')
) AS reasonDetail
from (
select
staff.id_card AS idCard,
bs.CUSTOMER_ACCOUNT_NAME AS customerAccountName,
SUM(IFNULL(bs.AMOUNT_CR, 0)) AS totalAmount
from ccdi_bank_statement bs
inner join ccdi_base_staff staff on staff.id_card = bs.cret_no
left join ccdi_staff_fmy_relation relation
on relation.person_id = staff.id_card
and relation.relation_name = bs.CUSTOMER_ACCOUNT_NAME
and relation.status = 1
where bs.project_id = #{projectId}
and IFNULL(bs.AMOUNT_CR, 0) > 0
and IFNULL(bs.LE_ACCOUNT_NAME, '') <> IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '')
and relation.person_id is null
and <include refid="salaryExclusionPredicate"/>
group by staff.id_card, bs.CUSTOMER_ACCOUNT_NAME
having SUM(IFNULL(bs.AMOUNT_CR, 0)) > #{threshold}
) t
</select>
<select id="selectAnnualTurnoverObjects" resultMap="BankTagObjectHitResultMap">
select
'STAFF_ID_CARD' AS objectType,
t.idCard AS objectKey,
CONCAT(
'近一年交易额 ', CAST(t.annualAmount AS CHAR),
' 元,超过阈值 ', CAST(#{threshold} AS CHAR), ' 元'
) AS reasonDetail
from (
select
staff.id_card AS idCard,
SUM(IFNULL(bs.AMOUNT_DR, 0) + IFNULL(bs.AMOUNT_CR, 0)) AS annualAmount
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.LE_ACCOUNT_NAME, '') <> IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '')
and STR_TO_DATE(LEFT(TRIM(bs.TRX_DATE), 10), '%Y-%m-%d') >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)
group by staff.id_card
having SUM(IFNULL(bs.AMOUNT_DR, 0) + IFNULL(bs.AMOUNT_CR, 0)) > #{threshold}
) t
</select>
<select id="selectLargeCashDepositStatements" resultMap="BankTagStatementHitResultMap">
select
bs.bank_statement_id AS bankStatementId,
bs.group_id AS groupId,
bs.batch_id AS logId,
CONCAT(
'现金存入金额 ', CAST(IFNULL(bs.AMOUNT_CR, 0) AS CHAR),
' 元,超过阈值 ', CAST(#{threshold} AS CHAR), ' 元'
) AS reasonDetail
from ccdi_bank_statement bs
where bs.project_id = #{projectId}
and IFNULL(bs.AMOUNT_CR, 0) > #{threshold}
and <include refid="cashDepositPredicate"/>
and (
exists (
select 1
from ccdi_base_staff staff
where staff.id_card = bs.cret_no
)
or exists (
select 1
from ccdi_staff_fmy_relation relation
where relation.relation_cert_no = bs.cret_no
and relation.status = 1
)
)
</select>
<select id="selectFrequentCashDepositObjects" resultMap="BankTagObjectHitResultMap">
select
'STAFF_ID_CARD' AS objectType,
t.objectKey AS objectKey,
CONCAT(
'单日存现次数 ', CAST(t.cashCount AS CHAR),
' 次,超过阈值 ', CAST(#{frequencyThreshold} AS CHAR),
' 次,交易日:', t.cashDate
) AS reasonDetail
from (
select
source.object_key AS objectKey,
source.cash_date AS cashDate,
COUNT(1) AS cashCount
from (
select
staff.id_card AS object_key,
LEFT(TRIM(bs.TRX_DATE), 10) AS cash_date
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) > #{amountThreshold}
and <include refid="cashDepositPredicate"/>
union all
select
relation.person_id AS object_key,
LEFT(TRIM(bs.TRX_DATE), 10) AS cash_date
from ccdi_bank_statement bs
inner join ccdi_staff_fmy_relation relation on relation.relation_cert_no = bs.cret_no
where bs.project_id = #{projectId}
and relation.status = 1
and IFNULL(bs.AMOUNT_CR, 0) > #{amountThreshold}
and <include refid="cashDepositPredicate"/>
) source
group by source.object_key, source.cash_date
having COUNT(1) > #{frequencyThreshold}
) t
</select>
<select id="selectLargeTransferStatements" resultMap="BankTagStatementHitResultMap">
select
bs.bank_statement_id AS bankStatementId,
bs.group_id AS groupId,
bs.batch_id AS logId,
CONCAT(
'大额转账支出 ', CAST(IFNULL(bs.AMOUNT_DR, 0) AS CHAR),
' 元,超过阈值 ', CAST(#{threshold} AS CHAR), ' 元'
) AS reasonDetail
from ccdi_bank_statement bs
where bs.project_id = #{projectId}
and IFNULL(bs.AMOUNT_DR, 0) > #{threshold}
and (
IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '转账'
or IFNULL(bs.USER_MEMO, '') REGEXP '转帐|转账|汇入|转存|红包|汇款|网转|转入'
or IFNULL(bs.CASH_TYPE, '') REGEXP '转帐|转账|汇入|转存|红包|汇款|网转|转入'
)
and IFNULL(bs.USER_MEMO, '') NOT LIKE '%款%'
and IFNULL(bs.LE_ACCOUNT_NAME, '') <> IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '')
and (
exists (
select 1
from ccdi_base_staff staff
where staff.id_card = bs.cret_no
)
or exists (
select 1
from ccdi_staff_fmy_relation relation
where relation.relation_cert_no = bs.cret_no
and relation.status = 1
)
)
</select>
</mapper>

View File

@@ -0,0 +1,47 @@
package com.ruoyi.ccdi.project.mapper;
import org.junit.jupiter.api.Test;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiBankTagAnalysisMapperXmlTest {
private static final String RESOURCE = "mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml";
@Test
void statementRuleSql_shouldSelectGroupIdAndLogId() throws Exception {
String xml = readXml(RESOURCE);
assertTrue(xml.contains("AS groupId"));
assertTrue(xml.contains("AS logId"));
}
@Test
void houseOrCarExpenseRule_shouldJoinBankStatementAndReturnStatementHitFields() throws Exception {
String xml = readXml(RESOURCE);
assertTrue(xml.contains("selectHouseOrCarExpenseStatements"));
assertTrue(xml.contains("bs.bank_statement_id AS bankStatementId"));
assertTrue(xml.contains("bs.group_id AS groupId"));
assertTrue(xml.contains("bs.batch_id AS logId"));
}
@Test
void allLargeTransactionRules_shouldExistInAnalysisMapperXml() throws Exception {
String xml = readXml(RESOURCE);
assertTrue(xml.contains("selectTaxExpenseStatements"));
assertTrue(xml.contains("selectSingleLargeIncomeStatements"));
assertTrue(xml.contains("selectCumulativeIncomeObjects"));
assertTrue(xml.contains("selectAnnualTurnoverObjects"));
assertTrue(xml.contains("selectLargeCashDepositStatements"));
assertTrue(xml.contains("selectFrequentCashDepositObjects"));
assertTrue(xml.contains("selectLargeTransferStatements"));
}
private String readXml(String resource) throws Exception {
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resource)) {
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
}
}
}