优化涉疑交易模型口径和报告展示

This commit is contained in:
wjj
2026-05-26 16:37:16 +08:00
committed by wkc
parent 9d3e8beceb
commit 000e8698a5
16 changed files with 672 additions and 75 deletions

View File

@@ -184,10 +184,10 @@ public class CcdiProjectOverviewController extends BaseController {
}
/**
* 一键导出结果总览报告
* 导出结果总览报告
*/
@RequestMapping(value = "/report/export", method = { RequestMethod.GET, RequestMethod.POST })
@Operation(summary = "一键导出结果总览报告")
@Operation(summary = "导出结果总览报告")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public void exportOverviewReport(HttpServletResponse response, Long projectId) {
overviewService.exportOverviewReport(response, projectId);

View File

@@ -33,4 +33,6 @@ public class CcdiProjectSuspiciousTransactionItemVO {
private Boolean hasModelRuleHit;
private Boolean hasNameListHit;
private String nameListHitType;
}

View File

@@ -469,6 +469,7 @@ public class CcdiProjectOverviewReportPdfExporter {
private static final float SUBSECTION_FONT_SIZE = 12F;
private static final float LINE_HEIGHT = 12F;
private static final float CELL_PADDING = 5F;
private static final float TABLE_AFTER_GAP = 32F;
private final PDDocument document;
private final PDType0Font font;
@@ -496,7 +497,7 @@ public class CcdiProjectOverviewReportPdfExporter {
}
void title(String text) throws IOException {
writeLine(text, TITLE_FONT_SIZE, new Color(18, 56, 93), 0F, 28F);
writeLine(text, TITLE_FONT_SIZE, new Color(18, 56, 93), 0F, 28F, true);
}
void text(String text, float fontSize, Color color) throws IOException {
@@ -505,12 +506,12 @@ public class CcdiProjectOverviewReportPdfExporter {
void section(String text) throws IOException {
ensureSpace(32F);
writeLine(text, SECTION_FONT_SIZE, new Color(18, 56, 93), 0F, 26F);
writeLine(text, SECTION_FONT_SIZE, new Color(18, 56, 93), 0F, 26F, true);
}
void subsection(String text) throws IOException {
ensureSpace(26F);
writeLine(text, SUBSECTION_FONT_SIZE, new Color(51, 65, 85), 0F, 22F);
writeLine(text, SUBSECTION_FONT_SIZE, new Color(51, 65, 85), 0F, 22F, true);
}
void separator() throws IOException {
@@ -557,7 +558,7 @@ public class CcdiProjectOverviewReportPdfExporter {
for (List<String> row : safeRows) {
drawRow(row, widths, false);
}
y -= 8F;
y -= TABLE_AFTER_GAP;
}
private float[] calculateWidths(float[] ratios) {
@@ -632,6 +633,17 @@ public class CcdiProjectOverviewReportPdfExporter {
}
private void writeLine(String text, float fontSize, Color color, float indent, float advance) throws IOException {
writeLine(text, fontSize, color, indent, advance, false);
}
private void writeLine(
String text,
float fontSize,
Color color,
float indent,
float advance,
boolean bold
) throws IOException {
ensureSpace(advance);
content.beginText();
content.setNonStrokingColor(color);
@@ -639,6 +651,14 @@ public class CcdiProjectOverviewReportPdfExporter {
content.newLineAtOffset(MARGIN + indent, y);
content.showText(text);
content.endText();
if (bold) {
content.beginText();
content.setNonStrokingColor(color);
content.setFont(font, fontSize);
content.newLineAtOffset(MARGIN + indent + 0.25F, y);
content.showText(text);
content.endText();
}
y -= advance;
}

View File

@@ -105,36 +105,69 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<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 '%劳务费%'
(
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 '%劳务费%'
)
)
or (
IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') LIKE '%公积金中心%'
and (
IFNULL(bs.USER_MEMO, '') LIKE '%公积金%'
or IFNULL(bs.USER_MEMO, '') LIKE '%批量代付%'
or IFNULL(bs.CASH_TYPE, '') LIKE '%公积金%'
or IFNULL(bs.CASH_TYPE, '') LIKE '%批量代付%'
)
)
)
</sql>
<sql id="abnormalCustomerTransactionSubjectSql">
select
staff.id_card as subjectCertNo,
staff.name as subjectName,
'本人' as subjectType
from ccdi_base_staff staff
union all
select
relation.relation_cert_no as subjectCertNo,
relation.relation_name as subjectName,
case
when relation.relation_type is not null and trim(relation.relation_type) != '' then relation.relation_type
else '关系人'
end as subjectType
from ccdi_staff_fmy_relation relation
where relation.status = 1
and relation.relation_cert_no is not null
and trim(relation.relation_cert_no) != ''
</sql>
<sql id="salaryIncomePredicate">
bs.CUSTOMER_ACCOUNT_NAME = '浙江兰溪农村商业银行股份有限公司'
and (
@@ -392,12 +425,130 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<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
hit.bankStatementId AS bankStatementId,
max(hit.groupId) AS groupId,
max(hit.logId) AS logId,
substring_index(
min(concat(lpad(hit.matchPriority, 2, '0'), '|', hit.reasonDetail)),
'|',
-1
) AS reasonDetail
from (
select
bs.bank_statement_id AS bankStatementId,
bs.group_id AS groupId,
bs.batch_id AS logId,
1 AS matchPriority,
CONCAT(
subject.subjectType, '“', IFNULL(subject.subjectName, ''), '”与信贷客户账号发生交易,',
'金额 ', CAST(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS CHAR),
' 元,对手方“', IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''), '”'
) AS reasonDetail
from ccdi_bank_statement bs
inner join (
<include refid="abnormalCustomerTransactionSubjectSql"/>
) subject on subject.subjectCertNo = bs.cret_no
inner join ccdi_account_info account
on trim(IFNULL(bs.customer_account_no, '')) != ''
and account.owner_type = 'CREDIT_CUSTOMER'
and account.account_no = bs.customer_account_no
where bs.project_id = #{projectId}
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000
union all
select
bs.bank_statement_id AS bankStatementId,
bs.group_id AS groupId,
bs.batch_id AS logId,
2 AS matchPriority,
CONCAT(
subject.subjectType, '“', IFNULL(subject.subjectName, ''), '”与中介账号发生交易,',
'金额 ', CAST(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS CHAR),
' 元,对手方“', IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''), '”'
) AS reasonDetail
from ccdi_bank_statement bs
inner join (
<include refid="abnormalCustomerTransactionSubjectSql"/>
) subject on subject.subjectCertNo = bs.cret_no
inner join ccdi_account_info account
on trim(IFNULL(bs.customer_account_no, '')) != ''
and account.owner_type = 'INTERMEDIARY'
and account.account_no = bs.customer_account_no
where bs.project_id = #{projectId}
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000
union all
select
bs.bank_statement_id AS bankStatementId,
bs.group_id AS groupId,
bs.batch_id AS logId,
3 AS matchPriority,
CONCAT(
subject.subjectType, '“', IFNULL(subject.subjectName, ''), '”与中介关联企业发生交易,',
'金额 ', CAST(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS CHAR),
' 元,对手方“', IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''), '”'
) AS reasonDetail
from ccdi_bank_statement bs
inner join (
<include refid="abnormalCustomerTransactionSubjectSql"/>
) subject on subject.subjectCertNo = bs.cret_no
inner join ccdi_enterprise_base_info enterprise
on trim(IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '')) != ''
and enterprise.enterprise_name = bs.CUSTOMER_ACCOUNT_NAME
and enterprise.ent_source = 'INTERMEDIARY'
where bs.project_id = #{projectId}
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000
union all
select
bs.bank_statement_id AS bankStatementId,
bs.group_id AS groupId,
bs.batch_id AS logId,
4 AS matchPriority,
CONCAT(
subject.subjectType, '“', IFNULL(subject.subjectName, ''), '”与中介库人员发生微信/支付宝交易,',
'金额 ', CAST(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS CHAR),
' 元,对手方“', IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''), '”'
) AS reasonDetail
from ccdi_bank_statement bs
inner join (
<include refid="abnormalCustomerTransactionSubjectSql"/>
) subject on subject.subjectCertNo = bs.cret_no
inner join ccdi_biz_intermediary intermediary
on trim(IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '')) != ''
and trim(IFNULL(intermediary.name, '')) != ''
and bs.CUSTOMER_ACCOUNT_NAME like concat('%', intermediary.name, '%')
where bs.project_id = #{projectId}
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000
and bs.bank in ('ALIPAY', 'WECHAT')
union all
select
bs.bank_statement_id AS bankStatementId,
bs.group_id AS groupId,
bs.batch_id AS logId,
5 AS matchPriority,
CONCAT(
subject.subjectType, '“', IFNULL(subject.subjectName, ''), '”与中介库人员发生名称精确匹配交易,',
'金额 ', CAST(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS CHAR),
' 元,对手方“', IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''), '”'
) AS reasonDetail
from ccdi_bank_statement bs
inner join (
<include refid="abnormalCustomerTransactionSubjectSql"/>
) subject on subject.subjectCertNo = bs.cret_no
inner join ccdi_biz_intermediary intermediary
on trim(IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '')) != ''
and intermediary.name = bs.CUSTOMER_ACCOUNT_NAME
where bs.project_id = #{projectId}
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000
) hit
group by hit.bankStatementId
</select>
<select id="selectLowIncomeRelativeLargeTransactionObjects" resultMap="BankTagObjectHitResultMap">
@@ -416,9 +567,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
from ccdi_staff_fmy_relation relation
inner join ccdi_bank_statement bs on relation.relation_cert_no = bs.cret_no
where relation.status = 1
and relation.annual_income is not null
and (
relation.annual_income is null
or relation.annual_income = 0
relation.annual_income = 0
or relation.annual_income / 12 &lt; 3000
)
and bs.project_id = #{projectId}
@@ -658,6 +809,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where bs.project_id = #{projectId}
and IFNULL(bs.AMOUNT_CR, 0) > 0
and IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') &lt;&gt; '浙江兰溪农村商业银行股份有限公司'
and not (
IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') LIKE '%公积金中心%'
and (
IFNULL(bs.USER_MEMO, '') LIKE '%公积金%'
or IFNULL(bs.USER_MEMO, '') LIKE '%批量代付%'
or IFNULL(bs.CASH_TYPE, '') LIKE '%公积金%'
or IFNULL(bs.CASH_TYPE, '') LIKE '%批量代付%'
)
)
and (
IFNULL(bs.USER_MEMO, '') REGEXP '代发|工资|分红|红利|奖金|薪酬|薪金|补贴|薪|年终奖|年金|加班费|劳务费|劳务外包|提成|劳务派遣|绩效|酬劳|批量代付|PAYROLL|SALA|CPF|directors.*fees'
or IFNULL(bs.CASH_TYPE, '') REGEXP '代发|工资|劳务费'

View File

@@ -46,6 +46,7 @@
<result property="displayAmount" column="displayAmount"/>
<result property="hasModelRuleHit" column="hasModelRuleHit"/>
<result property="hasNameListHit" column="hasNameListHit"/>
<result property="nameListHitType" column="nameListHitType"/>
</resultMap>
<resultMap id="AbnormalAccountItemResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountItemVO">
@@ -530,57 +531,74 @@
from ccdi_bank_statement_tag_result tr
where tr.project_id = #{query.projectId}
and tr.bank_statement_id is not null
and tr.rule_name like '%可疑%'
and (
tr.rule_name like '%可疑%'
or tr.rule_code = 'ABNORMAL_CUSTOMER_TRANSACTION'
)
</sql>
<sql id="suspiciousTransactionNameHitSql">
select
hits.bankStatementId,
hits.suspiciousPersonName,
hits.matchPriority
hits.matchPriority,
hits.nameListHitType
from (
select
bs.bank_statement_id as bankStatementId,
intermediary.name as suspiciousPersonName,
1 as matchPriority
coalesce(credit_customer.name, account.account_name, '信贷客户账号') as suspiciousPersonName,
1 as matchPriority,
'信贷客户' as nameListHitType
from ccdi_bank_statement bs
inner join ccdi_biz_intermediary intermediary
on trim(bs.customer_cert_no) != ''
and intermediary.person_id = bs.customer_cert_no
inner join ccdi_account_info account
on trim(bs.customer_account_no) != ''
and account.owner_type = 'CREDIT_CUSTOMER'
and account.account_no = bs.customer_account_no
left join ccdi_credit_customer_base credit_customer
on credit_customer.person_id = account.owner_id
where bs.project_id = #{query.projectId}
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000
union all
select
bs.bank_statement_id as bankStatementId,
enterprise.enterprise_name as suspiciousPersonName,
2 as matchPriority
coalesce(intermediary.name, enterprise.enterprise_name, account.account_name, '中介账号') as suspiciousPersonName,
2 as matchPriority,
'中介' as nameListHitType
from ccdi_bank_statement bs
inner join ccdi_enterprise_base_info enterprise
on trim(bs.customer_social_credit_code) != ''
and enterprise.social_credit_code = bs.customer_social_credit_code
and enterprise.risk_level = '1'
and enterprise.ent_source = 'INTERMEDIARY'
inner join ccdi_account_info account
on trim(bs.customer_account_no) != ''
and account.owner_type = 'INTERMEDIARY'
and account.account_no = bs.customer_account_no
left join ccdi_biz_intermediary intermediary
on intermediary.person_id = account.owner_id
left join ccdi_enterprise_base_info enterprise
on enterprise.social_credit_code = account.owner_id
where bs.project_id = #{query.projectId}
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000
union all
select
bs.bank_statement_id as bankStatementId,
intermediary.name as suspiciousPersonName,
3 as matchPriority
3 as matchPriority,
'中介' as nameListHitType
from ccdi_bank_statement bs
inner join ccdi_biz_intermediary intermediary
on trim(bs.CUSTOMER_ACCOUNT_NAME) != ''
and intermediary.name = bs.CUSTOMER_ACCOUNT_NAME
where bs.project_id = #{query.projectId}
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000
union all
select
bs.bank_statement_id as bankStatementId,
enterprise.enterprise_name as suspiciousPersonName,
3 as matchPriority
4 as matchPriority,
'中介' as nameListHitType
from ccdi_bank_statement bs
inner join ccdi_enterprise_base_info enterprise
on trim(bs.CUSTOMER_ACCOUNT_NAME) != ''
@@ -588,6 +606,7 @@
and enterprise.risk_level = '1'
and enterprise.ent_source = 'INTERMEDIARY'
where bs.project_id = #{query.projectId}
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000
) hits
</sql>
@@ -605,7 +624,8 @@
1 as hasModelRuleHit,
0 as hasNameListHit,
null as suspiciousPersonName,
null as matchPriority
null as matchPriority,
null as nameListHitType
from (
<include refid="suspiciousTransactionBaseSql"/>
) base
@@ -628,7 +648,8 @@
0 as hasModelRuleHit,
1 as hasNameListHit,
name_hits.suspiciousPersonName,
name_hits.matchPriority
name_hits.matchPriority,
name_hits.nameListHitType
from (
<include refid="suspiciousTransactionBaseSql"/>
) base
@@ -663,7 +684,18 @@
max(merged.cashType) as cashType,
max(merged.displayAmount) as displayAmount,
max(merged.hasModelRuleHit) as hasModelRuleHit,
max(merged.hasNameListHit) as hasNameListHit
max(merged.hasNameListHit) as hasNameListHit,
substring_index(
min(
case
when merged.nameListHitType is not null and merged.nameListHitType != ''
then concat(lpad(merged.matchPriority, 2, '0'), '|', merged.nameListHitType)
else null
end
),
'|',
-1
) as nameListHitType
from (
<include refid="suspiciousTransactionMergedSql"/>
) merged
@@ -701,7 +733,8 @@
final_result.cashType,
final_result.displayAmount,
final_result.hasModelRuleHit,
final_result.hasNameListHit
final_result.hasNameListHit,
final_result.nameListHitType
from (
<include refid="suspiciousTransactionAggregatedSql"/>
) final_result
@@ -722,7 +755,8 @@
final_result.cashType,
final_result.displayAmount,
final_result.hasModelRuleHit,
final_result.hasNameListHit
final_result.hasNameListHit,
final_result.nameListHitType
from (
<include refid="suspiciousTransactionAggregatedSql"/>
) final_result
@@ -742,7 +776,21 @@
final_result.relatedStaffCode,
final_result.userMemo,
final_result.cashType,
tag_result.hitTags,
case
when final_result.nameListHitType = '中介' then
replace(
replace(ifnull(tag_result.hitTags, ''), '与客户之间非正常资金往来', '疑似与中介往来'),
'异常交易',
'疑似与中介往来'
)
when final_result.nameListHitType = '信贷客户' then
replace(
replace(ifnull(tag_result.hitTags, ''), '与客户之间非正常资金往来', '与信贷客户之间非正常资金往来'),
'异常交易',
'与信贷客户之间非正常资金往来'
)
else tag_result.hitTags
end as hitTags,
final_result.displayAmount
from (
<include refid="suspiciousTransactionAggregatedSql"/>

View File

@@ -100,7 +100,7 @@ class CcdiBankTagAnalysisMapperXmlTest {
void placeholderRules_shouldUseEmptyResultSqlTemplate() throws Exception {
String xml = readXml(RESOURCE);
assertTrue(xml.contains("占位SQL待补充真实规则"));
assertEquals(6, countMatches(xml, "where 1 = 0"));
assertEquals(5, countMatches(xml, "where 1 = 0"));
}
@Test
@@ -116,6 +116,31 @@ class CcdiBankTagAnalysisMapperXmlTest {
}
}
@Test
void lowIncomeRelativeRule_shouldIgnoreNullAnnualIncome() throws Exception {
String xml = readXml(RESOURCE);
String selectSql = extractSelectSql(xml, "selectLowIncomeRelativeLargeTransactionObjects");
assertTrue(selectSql.contains("relation.annual_income is not null"));
assertTrue(!selectSql.contains("relation.annual_income is null"));
}
@Test
void abnormalCustomerTransactionRule_shouldUseCreditCustomerAndIntermediaryAccountRules() throws Exception {
String xml = readXml(RESOURCE);
String selectSql = extractSelectSql(xml, "selectAbnormalCustomerTransactionStatements");
assertTrue(selectSql.contains("account.owner_type = 'CREDIT_CUSTOMER'"));
assertTrue(selectSql.contains("account.owner_type = 'INTERMEDIARY'"));
assertTrue(selectSql.contains("account.account_no = bs.customer_account_no"));
assertTrue(selectSql.contains("enterprise.ent_source = 'INTERMEDIARY'"));
assertTrue(selectSql.contains("intermediary.name = bs.CUSTOMER_ACCOUNT_NAME"));
assertTrue(selectSql.contains("bs.CUSTOMER_ACCOUNT_NAME like concat('%', intermediary.name, '%')"));
assertTrue(selectSql.contains("bs.bank in ('ALIPAY', 'WECHAT')"));
assertEquals(5, countMatches(selectSql, "GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000"));
assertTrue(!selectSql.contains("customer_cert_no"));
assertTrue(!selectSql.contains("social_credit_code = bs"));
}
@Test
void withdrawCntObjectRule_shouldUseRealSqlAndKeepObjectHitFields() throws Exception {
String xml = readXml(RESOURCE);

View File

@@ -97,6 +97,37 @@ class CcdiProjectOverviewMapperSqlTest {
assertTrue(suspiciousSql.contains("group by merged.bankStatementId"), suspiciousSql);
assertTrue(suspiciousSql.contains("hasModelRuleHit"), suspiciousSql);
assertTrue(suspiciousSql.contains("hasNameListHit"), suspiciousSql);
assertTrue(suspiciousSql.contains("final_result.nameListHitType"), suspiciousSql);
String reportSuspiciousSql = extractSelect(xml, "selectReportSuspiciousTransactionList");
assertTrue(reportSuspiciousSql.contains("final_result.nameListHitType = '中介'"), reportSuspiciousSql);
assertTrue(reportSuspiciousSql.contains("疑似与中介往来"), reportSuspiciousSql);
assertTrue(reportSuspiciousSql.contains("final_result.nameListHitType = '信贷客户'"), reportSuspiciousSql);
assertTrue(reportSuspiciousSql.contains("与信贷客户之间非正常资金往来"), reportSuspiciousSql);
}
@Test
void suspiciousTransactionNameListSql_shouldKeepCreditCustomerAndIntermediaryRulesScopedByAmount() throws Exception {
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml"));
String nameHitSql = extractSqlFragment(xml, "suspiciousTransactionNameHitSql");
String aggregatedSql = extractSqlFragment(xml, "suspiciousTransactionAggregatedSql");
assertTrue(nameHitSql.contains("account.owner_type = 'CREDIT_CUSTOMER'"), nameHitSql);
assertTrue(nameHitSql.contains("account.owner_type = 'INTERMEDIARY'"), nameHitSql);
assertTrue(nameHitSql.contains("account.account_no = bs.customer_account_no"), nameHitSql);
assertTrue(nameHitSql.contains("'信贷客户' as nameListHitType"), nameHitSql);
assertTrue(nameHitSql.contains("'中介' as nameListHitType"), nameHitSql);
assertTrue(nameHitSql.contains("intermediary.name = bs.CUSTOMER_ACCOUNT_NAME"), nameHitSql);
assertTrue(nameHitSql.contains("enterprise.ent_source = 'INTERMEDIARY'"), nameHitSql);
assertTrue(
nameHitSql.contains("GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000"),
nameHitSql
);
assertFalse(nameHitSql.contains("customer_cert_no"), nameHitSql);
assertFalse(nameHitSql.contains("social_credit_code = bs"), nameHitSql);
assertTrue(aggregatedSql.contains("group by merged.bankStatementId"), aggregatedSql);
assertTrue(aggregatedSql.contains("max(merged.hasModelRuleHit) as hasModelRuleHit"), aggregatedSql);
assertTrue(aggregatedSql.contains("max(merged.hasNameListHit) as hasNameListHit"), aggregatedSql);
}
@Test
@@ -159,4 +190,13 @@ class CcdiProjectOverviewMapperSqlTest {
assertTrue(endIndex >= 0, "missing closing select tag: " + selectId);
return xml.substring(startIndex, endIndex);
}
private String extractSqlFragment(String xml, String sqlId) {
String start = "<sql id=\"" + sqlId + "\"";
int startIndex = xml.indexOf(start);
assertTrue(startIndex >= 0, "missing sql fragment: " + sqlId);
int endIndex = xml.indexOf("</sql>", startIndex);
assertTrue(endIndex >= 0, "missing closing sql tag: " + sqlId);
return xml.substring(startIndex, endIndex);
}
}

View File

@@ -60,6 +60,17 @@ class CcdiProjectOverviewReportPdfExporterTest {
assertTrue(exception.getMessage().contains(missingPath));
}
@Test
void tableGap_shouldLeaveEnoughSpaceForNextSectionTitle() throws Exception {
Class<?> writerClass = Class.forName(
"com.ruoyi.ccdi.project.service.impl.CcdiProjectOverviewReportPdfExporter$PdfPageWriter"
);
float tableAfterGap = readPrivateFloat(writerClass, "TABLE_AFTER_GAP");
float sectionFontSize = readPrivateFloat(writerClass, "SECTION_FONT_SIZE");
assertTrue(tableAfterGap > sectionFontSize);
}
private String resolveTestFontPath() {
List<String> candidates = List.of(
"/System/Library/Fonts/STHeiti Medium.ttc",
@@ -91,6 +102,12 @@ class CcdiProjectOverviewReportPdfExporterTest {
return report;
}
private float readPrivateFloat(Class<?> clazz, String fieldName) throws Exception {
java.lang.reflect.Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.getFloat(null);
}
private CcdiProjectOverviewDashboardVO buildDashboard() {
CcdiProjectOverviewDashboardVO dashboard = new CcdiProjectOverviewDashboardVO();
dashboard.setStats(List.of(

View File

@@ -0,0 +1,65 @@
# 与客户之间非正常资金往来实施记录
## 本次修改
- 调整 `ABNORMAL_CUSTOMER_TRANSACTION` 模型执行 SQL。
- 调整“涉疑交易明细 -> 名单库命中” SQL。
- 调整涉疑交易明细前端标签展示。
- 补充 SQL 口径测试,不改表结构。
## 模型 SQL 口径
- 位置:`ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml`
- 主体范围:员工本人、`ccdi_staff_fmy_relation.status = 1` 的有效亲属。
- 金额口径:`GREATEST(IFNULL(amount_dr, 0), IFNULL(amount_cr, 0)) > 1000`
- 信贷客户命中:对手方账号命中 `ccdi_account_info.owner_type = 'CREDIT_CUSTOMER'`
- 中介账号命中:对手方账号命中 `ccdi_account_info.owner_type = 'INTERMEDIARY'`
- 中介企业命中:对手方名称精确命中 `ccdi_enterprise_base_info.enterprise_name`,且企业来源为中介。
- 中介人员精确命中:对手方名称精确命中 `ccdi_biz_intermediary.name`
- 中介人员模糊命中:对手方名称包含 `ccdi_biz_intermediary.name`,仅限 `bank in ('ALIPAY', 'WECHAT')`
- 明确不再使用对手方证件号、对手方统一社会信用代码命中。
## 名单库命中口径
- 位置:`ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml`
- 信贷客户账号命中时,`nameListHitType` 返回“信贷客户”。
- 中介账号、人员名称、企业名称命中时,`nameListHitType` 返回“中介”。
- 名单库命中同样要求交易金额大于 1000。
- 涉疑交易明细和 PDF 导出按 `bank_statement_id` 聚合,避免同一流水因“模型规则命中”和“名单库命中”重复展示。
## 前端展示
- 位置:`ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue`
- `ABNORMAL_CUSTOMER_TRANSACTION` 在异常标签中优先展示。
- 如果同一流水名单库类型为“中介”,该标签展示为“疑似与中介往来”。
- 如果同一流水名单库类型为“信贷客户”,该标签展示为“与信贷客户之间非正常资金往来”。
- 如果没有名单库类型,兜底展示为“与客户之间非正常资金往来”。
- 不再把“中介/信贷客户”追加成额外异常标签,避免重复展示。
## 报告展示
- 结果总览 PDF 报告中的涉疑交易明细表同步使用上述标签展示口径。
- 同一条流水只替换 `ABNORMAL_CUSTOMER_TRANSACTION` 的展示文案,不额外追加“中介/信贷客户”标签。
## 影响文件
- `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml`
- `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectSuspiciousTransactionItemVO.java`
- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagAnalysisMapperXmlTest.java`
- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperSqlTest.java`
- `ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue`
- `docs/reports/implementation/2026-05-21-abnormal-customer-transaction-implementation.md`
## 验证情况
- `mvn -pl ccdi-project -am -DskipTests compile` 通过。
- `mvn -pl ccdi-project "-Dtest=CcdiBankTagAnalysisMapperXmlTest,CcdiProjectOverviewMapperSqlTest" test` 通过21 个测试。
- 测试覆盖的伪造场景口径:
- 信贷客户账号命中。
- 中介账号命中。
- 中介人员名称精确命中。
- 中介人员名称模糊命中仅限 `ALIPAY/WECHAT`
- 名单库命中金额门槛大于 1000。
- 涉疑交易明细按 `bank_statement_id` 聚合去重。
- 涉疑交易明细和 PDF 报告按名单库类型细分展示异常标签,且不重复展示。

View File

@@ -0,0 +1,17 @@
# 低收入亲属大额交易空收入口径调整实施记录
## 修改内容
- 调整 `LOW_INCOME_RELATIVE_LARGE_TRANSACTION` 规则 SQL。
- 关系人 `annual_income is null` 时不再进入低收入候选,不触发该规则。
- 仅当 `annual_income` 明确有值,且满足 `annual_income = 0``annual_income / 12 < 3000` 时,才进入低收入候选。
## 影响范围
- 后端银行流水打标规则:低收入亲属大额交易。
- 不涉及前端展示、接口入参、数据库表结构。
## 验证
- `mvn -pl ccdi-project -am -DskipTests compile` 通过。
- `mvn -pl ccdi-project "-Dtest=CcdiBankTagAnalysisMapperXmlTest,CcdiBankTagServiceImplTest,CcdiBankTagRuleSqlMetadataTest,BankTagRuleConfigResolverTest" test` 通过,共 43 个测试。

View File

@@ -0,0 +1,63 @@
# dev-ui 本次提交清单
## 提交信息
- 分支:`dev-ui`
- 提交说明:`优化涉疑交易模型口径和报告展示`
## 功能清单
1. 涉疑交易模型口径调整
- `ABNORMAL_CUSTOMER_TRANSACTION` 补充信贷客户账号命中。
- `ABNORMAL_CUSTOMER_TRANSACTION` 补充中介账号命中、中介名称精确命中、中介人员微信/支付宝流水名称模糊命中。
- 名单库命中保留中介和信贷客户两类。
- 金额门槛统一按单边流水金额大于 1000 元判断。
- 低收入亲属大额交易排除年收入为空的亲属;年收入为空不主动预警。
- 大额单笔收入、疑似兼职相关收入预警排除公积金中心收入。
2. 涉疑交易明细展示与导出
- 涉疑交易明细保留“名单库命中”和“模型规则命中”筛选。
- 同一条流水同时命中名单库和模型规则时,按 `bank_statement_id` 聚合去重,不重复展示。
- 名单库命中类型区分为“中介”和“信贷客户”。
- 前端异常标签按名单类型展示:
- 中介:`疑似与中介往来`
- 信贷客户:`与信贷客户之间非正常资金往来`
- 未命中名单类型时:`与客户之间非正常资金往来`
- PDF 导出复用去重后的明细逻辑。
3. 拉取本行流水弹窗
- 证件号码输入提示改为仅支持英文逗号分隔。
- 日期默认开始时间为昨天往前一年,结束时间为昨天。
- 可选日期范围限制为 2025-01-01 到昨天。
4. 报告导出展示
- “一键导出”按钮文案改为“导出报告”。
- PDF 报告章节标题错行问题修正。
- PDF 报告标题加粗展示。
## 涉及文件
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectSuspiciousTransactionItemVO.java`
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewReportPdfExporter.java`
- `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankTagAnalysisMapper.xml`
- `ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml`
- `ruoyi-ui/src/views/ccdiProject/components/detail/PreliminaryCheck.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/RiskDetailSection.vue`
- `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue`
- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiBankTagAnalysisMapperXmlTest.java`
- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapperSqlTest.java`
- `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewReportPdfExporterTest.java`
## 验证情况
- 已执行后端 Mapper SQL 断言测试。
- 已执行 PDF 导出样式断言测试。
- 已执行前端生产构建。
- 已在本地页面验证涉疑交易明细中“中介/信贷客户”命中展示不重复。
- 已验证 PDF 导出包含“疑似与中介往来”和“与信贷客户之间非正常资金往来”。

View File

@@ -0,0 +1,24 @@
# 拉取本行流水弹窗交互调整实施记录
## 修改内容
- 将证件号码输入提示调整为“仅支持英文逗号分隔”。
- 证件号码解析仅按英文逗号分隔,不再支持中文逗号、顿号、换行等分隔方式。
- 本行存量流水日期默认值调整为:
- 开始日期:昨天往前一年。
- 结束日期:昨天。
- 日期可选范围限制为 `2025-01-01` 到昨天。
## 影响范围
- 前端页面:
- `ruoyi-ui/src/views/ccdiProject/components/detail/UploadData.vue`
- 不涉及后端接口、不改数据库。
## 验证情况
- 已在真实项目详情页打开“拉取本行流水”弹窗验证:
- 提示文案显示为仅支持英文逗号分隔。
- 默认开始日期为 `2025-05-25`
- 默认结束日期为 `2026-05-25`
- 当前系统日期为 `2026-05-26`,默认日期符合“开始默认一年前、结束默认昨天”。

View File

@@ -0,0 +1,20 @@
# 导出报告章节标题错行修复实施记录
## 修改内容
- 修复结果总览 PDF 导出报告中,表格结束后下一节标题可能压到表格底边框的问题。
- 原因是 PDFBox 绘制文本时 `y` 坐标为文字基线,表格后仅保留 `8F` 间距15 号章节标题的字形会向上占用空间,导致视觉上与上一张表格边框重叠。
- 将表格后间距调整为 `32F`,确保下一节标题与上一张表格有明显稳定留白。
- 章节标题、子标题和封面标题改为加粗展示;正文、表格文字、颜色和列宽不调整。
## 影响范围
- 后端 PDF 报告导出:
- `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewReportPdfExporter.java`
- 不涉及前端页面、不改接口、不改数据库。
## 验证情况
- `mvn -pl ccdi-project -am -DskipTests compile` 通过。
- `mvn -pl ccdi-project "-Dtest=CcdiProjectOverviewReportPdfExporterTest" test` 通过2 个测试。
- 新增测试断言表格后间距必须大于章节标题字号,避免后续回退成过小间距。

View File

@@ -27,7 +27,7 @@
:disabled="!projectId"
@click="handleOverviewReportExport"
>
一键导出
导出报告
</el-button>
</div>
<overview-stats :summary="currentData.summary" />

View File

@@ -353,7 +353,61 @@ const SUSPICIOUS_TYPE_OPTIONS = [
{ value: "MODEL_RULE", label: "模型规则命中" },
];
const normalizeHitTags = (hitTags) => (Array.isArray(hitTags) ? hitTags : []);
const normalizeSuspiciousType = (value) => (
SUSPICIOUS_TYPE_OPTIONS.some((item) => item.value === value) ? value : "ALL"
);
const ABNORMAL_CUSTOMER_TRANSACTION_RULE_CODE = "ABNORMAL_CUSTOMER_TRANSACTION";
const ABNORMAL_CUSTOMER_TRANSACTION_RULE_NAME = "与客户之间非正常资金往来";
const resolveAbnormalCustomerTransactionRuleName = (nameListHitType) => {
if (nameListHitType === "中介") {
return "疑似与中介往来";
}
if (nameListHitType === "信贷客户") {
return "与信贷客户之间非正常资金往来";
}
return ABNORMAL_CUSTOMER_TRANSACTION_RULE_NAME;
};
const normalizeRuleName = (tag, context) => {
if (!tag) {
return "";
}
if (
tag.ruleCode === ABNORMAL_CUSTOMER_TRANSACTION_RULE_CODE
|| tag.ruleName === "异常交易"
|| tag.ruleName === ABNORMAL_CUSTOMER_TRANSACTION_RULE_NAME
) {
return resolveAbnormalCustomerTransactionRuleName(context && context.nameListHitType);
}
return tag.ruleName || "";
};
const normalizeHitTags = (hitTags, context = {}) => {
if (!Array.isArray(hitTags)) {
return [];
}
const seen = new Set();
return hitTags
.map((tag) => ({
...tag,
ruleName: normalizeRuleName(tag, context),
}))
.filter((tag) => {
const key = `${tag.ruleCode || ""}|${tag.ruleName || ""}`;
if (seen.has(key)) {
return false;
}
seen.add(key);
return true;
})
.sort((left, right) => {
const leftPriority = left.ruleCode === ABNORMAL_CUSTOMER_TRANSACTION_RULE_CODE ? 0 : 1;
const rightPriority = right.ruleCode === ABNORMAL_CUSTOMER_TRANSACTION_RULE_CODE ? 0 : 1;
return leftPriority - rightPriority;
});
};
const createEmptyDetailData = () => ({
bankStatementId: "",
@@ -443,7 +497,7 @@ export default {
deep: true,
handler(value) {
this.projectId = value && value.projectId ? value.projectId : null;
this.currentSuspiciousType = value && value.suspiciousType ? value.suspiciousType : "ALL";
this.currentSuspiciousType = normalizeSuspiciousType(value && value.suspiciousType);
this.suspiciousPageNum = 1;
this.suspiciousPageSize = 5;
this.suspiciousTotal = Number(value && value.total) || 0;
@@ -603,7 +657,7 @@ export default {
return {
...detail,
...row,
hitTags: normalizeHitTags(detail.hitTags),
hitTags: normalizeHitTags(detail.hitTags, row),
};
} catch (error) {
return {
@@ -647,7 +701,14 @@ export default {
this.detailVisible = true;
this.detailLoading = true;
try {
this.detailData = await this.fetchStatementDetail(row.bankStatementId, false);
const detail = await this.fetchStatementDetail(row.bankStatementId, false);
this.detailData = {
...detail,
...row,
hitTags: row && Array.isArray(row.hitTags) && row.hitTags.length
? normalizeHitTags(row.hitTags, row)
: normalizeHitTags(detail.hitTags, row),
};
} catch (error) {
this.detailData = createEmptyDetailData();
this.$message.error("加载流水详情失败");

View File

@@ -126,10 +126,10 @@
type="textarea"
:rows="5"
:disabled="isProjectTagging || isProjectArchived"
placeholder="支持逗号、中文逗号、换行分隔"
placeholder="支持英文逗号分隔"
/>
<div class="pull-bank-field-tip">
支持逗号中文逗号换行分隔文件解析结果会自动合并并去重
支持英文逗号分隔文件解析结果会自动合并并去重
</div>
</el-form-item>
<el-form-item label="身份证文件">
@@ -424,7 +424,7 @@ export default {
resetPullBankInfoForm() {
this.pullBankInfoForm = {
idCardText: "",
dateRange: [],
dateRange: this.buildDefaultPullBankInfoDateRange(),
};
this.idCardFileList = [];
this.parsingIdCardFile = false;
@@ -434,7 +434,7 @@ export default {
return Array.from(
new Set(
(text || "")
.split(/[\n,]+/)
.split(/,+/)
.map((item) => item.trim())
.filter(Boolean)
)
@@ -504,11 +504,36 @@ export default {
today.setHours(0, 0, 0, 0);
return today;
},
getPullBankInfoMinSelectableDate() {
return new Date(2025, 0, 1);
},
getPullBankInfoMaxSelectableDate() {
const yesterday = this.getPullBankInfoTodayStart();
yesterday.setDate(yesterday.getDate() - 1);
return yesterday;
},
formatPullBankInfoDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
},
buildDefaultPullBankInfoDateRange() {
const minSelectableDate = this.getPullBankInfoMinSelectableDate();
const maxSelectableDate = this.getPullBankInfoMaxSelectableDate();
const defaultStartDate = new Date(maxSelectableDate.getTime());
defaultStartDate.setFullYear(defaultStartDate.getFullYear() - 1);
if (defaultStartDate.getTime() < minSelectableDate.getTime()) {
return [
this.formatPullBankInfoDate(minSelectableDate),
this.formatPullBankInfoDate(maxSelectableDate),
];
}
return [
this.formatPullBankInfoDate(defaultStartDate),
this.formatPullBankInfoDate(maxSelectableDate),
];
},
parsePullBankInfoDate(dateValue) {
if (!dateValue) return null;
const [year, month, day] = String(dateValue)
@@ -520,13 +545,23 @@ export default {
return new Date(year, month - 1, day);
},
isPullBankInfoDateDisabled(time) {
return time.getTime() >= this.getPullBankInfoTodayStart().getTime();
const minSelectableDate = this.getPullBankInfoMinSelectableDate();
const todayStart = this.getPullBankInfoTodayStart();
return (
time.getTime() < minSelectableDate.getTime() ||
time.getTime() >= todayStart.getTime()
);
},
hasInvalidPullBankInfoDateRange(dateRange) {
const minSelectableDate = this.getPullBankInfoMinSelectableDate();
const maxSelectableDate = this.getPullBankInfoMaxSelectableDate();
return (dateRange || []).some((dateValue) => {
const date = this.parsePullBankInfoDate(dateValue);
return !date || date.getTime() > maxSelectableDate.getTime();
return (
!date ||
date.getTime() < minSelectableDate.getTime() ||
date.getTime() > maxSelectableDate.getTime()
);
});
},
buildFinalIdCardList() {
@@ -555,7 +590,7 @@ export default {
}
if (this.hasInvalidPullBankInfoDateRange([startDate, endDate])) {
this.$message.warning("时间跨度最晚只能选择到昨天");
this.$message.warning("时间跨度仅支持 2025-01-01 至昨天");
return;
}