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

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 }) @RequestMapping(value = "/report/export", method = { RequestMethod.GET, RequestMethod.POST })
@Operation(summary = "一键导出结果总览报告") @Operation(summary = "导出结果总览报告")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')") @PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public void exportOverviewReport(HttpServletResponse response, Long projectId) { public void exportOverviewReport(HttpServletResponse response, Long projectId) {
overviewService.exportOverviewReport(response, projectId); overviewService.exportOverviewReport(response, projectId);

View File

@@ -33,4 +33,6 @@ public class CcdiProjectSuspiciousTransactionItemVO {
private Boolean hasModelRuleHit; private Boolean hasModelRuleHit;
private Boolean hasNameListHit; 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 SUBSECTION_FONT_SIZE = 12F;
private static final float LINE_HEIGHT = 12F; private static final float LINE_HEIGHT = 12F;
private static final float CELL_PADDING = 5F; private static final float CELL_PADDING = 5F;
private static final float TABLE_AFTER_GAP = 32F;
private final PDDocument document; private final PDDocument document;
private final PDType0Font font; private final PDType0Font font;
@@ -496,7 +497,7 @@ public class CcdiProjectOverviewReportPdfExporter {
} }
void title(String text) throws IOException { 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 { void text(String text, float fontSize, Color color) throws IOException {
@@ -505,12 +506,12 @@ public class CcdiProjectOverviewReportPdfExporter {
void section(String text) throws IOException { void section(String text) throws IOException {
ensureSpace(32F); 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 { void subsection(String text) throws IOException {
ensureSpace(26F); 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 { void separator() throws IOException {
@@ -557,7 +558,7 @@ public class CcdiProjectOverviewReportPdfExporter {
for (List<String> row : safeRows) { for (List<String> row : safeRows) {
drawRow(row, widths, false); drawRow(row, widths, false);
} }
y -= 8F; y -= TABLE_AFTER_GAP;
} }
private float[] calculateWidths(float[] ratios) { 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 { 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); ensureSpace(advance);
content.beginText(); content.beginText();
content.setNonStrokingColor(color); content.setNonStrokingColor(color);
@@ -639,6 +651,14 @@ public class CcdiProjectOverviewReportPdfExporter {
content.newLineAtOffset(MARGIN + indent, y); content.newLineAtOffset(MARGIN + indent, y);
content.showText(text); content.showText(text);
content.endText(); 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; y -= advance;
} }

View File

@@ -105,6 +105,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<sql id="salaryExclusionPredicate"> <sql id="salaryExclusionPredicate">
not ( not (
(
bs.CUSTOMER_ACCOUNT_NAME = '浙江兰溪农村商业银行股份有限公司' bs.CUSTOMER_ACCOUNT_NAME = '浙江兰溪农村商业银行股份有限公司'
and ( and (
IFNULL(bs.USER_MEMO, '') LIKE '%代发%' IFNULL(bs.USER_MEMO, '') LIKE '%代发%'
@@ -133,6 +134,38 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
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>
<sql id="salaryIncomePredicate"> <sql id="salaryIncomePredicate">
@@ -391,13 +424,131 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select> </select>
<select id="selectAbnormalCustomerTransactionStatements" resultMap="BankTagStatementHitResultMap"> <select id="selectAbnormalCustomerTransactionStatements" resultMap="BankTagStatementHitResultMap">
select
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 select
bs.bank_statement_id AS bankStatementId, bs.bank_statement_id AS bankStatementId,
bs.group_id AS groupId, bs.group_id AS groupId,
bs.batch_id AS logId, bs.batch_id AS logId,
'占位SQL待补充真实规则' AS reasonDetail 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 from ccdi_bank_statement bs
where 1 = 0 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>
<select id="selectLowIncomeRelativeLargeTransactionObjects" resultMap="BankTagObjectHitResultMap"> <select id="selectLowIncomeRelativeLargeTransactionObjects" resultMap="BankTagObjectHitResultMap">
@@ -416,9 +567,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
from ccdi_staff_fmy_relation relation from ccdi_staff_fmy_relation relation
inner join ccdi_bank_statement bs on relation.relation_cert_no = bs.cret_no inner join ccdi_bank_statement bs on relation.relation_cert_no = bs.cret_no
where relation.status = 1 where relation.status = 1
and relation.annual_income is not null
and ( and (
relation.annual_income is null relation.annual_income = 0
or relation.annual_income = 0
or relation.annual_income / 12 &lt; 3000 or relation.annual_income / 12 &lt; 3000
) )
and bs.project_id = #{projectId} and bs.project_id = #{projectId}
@@ -658,6 +809,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where bs.project_id = #{projectId} where bs.project_id = #{projectId}
and IFNULL(bs.AMOUNT_CR, 0) > 0 and IFNULL(bs.AMOUNT_CR, 0) > 0
and IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') &lt;&gt; '浙江兰溪农村商业银行股份有限公司' 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 ( and (
IFNULL(bs.USER_MEMO, '') REGEXP '代发|工资|分红|红利|奖金|薪酬|薪金|补贴|薪|年终奖|年金|加班费|劳务费|劳务外包|提成|劳务派遣|绩效|酬劳|批量代付|PAYROLL|SALA|CPF|directors.*fees' IFNULL(bs.USER_MEMO, '') REGEXP '代发|工资|分红|红利|奖金|薪酬|薪金|补贴|薪|年终奖|年金|加班费|劳务费|劳务外包|提成|劳务派遣|绩效|酬劳|批量代付|PAYROLL|SALA|CPF|directors.*fees'
or IFNULL(bs.CASH_TYPE, '') REGEXP '代发|工资|劳务费' or IFNULL(bs.CASH_TYPE, '') REGEXP '代发|工资|劳务费'

View File

@@ -46,6 +46,7 @@
<result property="displayAmount" column="displayAmount"/> <result property="displayAmount" column="displayAmount"/>
<result property="hasModelRuleHit" column="hasModelRuleHit"/> <result property="hasModelRuleHit" column="hasModelRuleHit"/>
<result property="hasNameListHit" column="hasNameListHit"/> <result property="hasNameListHit" column="hasNameListHit"/>
<result property="nameListHitType" column="nameListHitType"/>
</resultMap> </resultMap>
<resultMap id="AbnormalAccountItemResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountItemVO"> <resultMap id="AbnormalAccountItemResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountItemVO">
@@ -530,57 +531,74 @@
from ccdi_bank_statement_tag_result tr from ccdi_bank_statement_tag_result tr
where tr.project_id = #{query.projectId} where tr.project_id = #{query.projectId}
and tr.bank_statement_id is not null 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>
<sql id="suspiciousTransactionNameHitSql"> <sql id="suspiciousTransactionNameHitSql">
select select
hits.bankStatementId, hits.bankStatementId,
hits.suspiciousPersonName, hits.suspiciousPersonName,
hits.matchPriority hits.matchPriority,
hits.nameListHitType
from ( from (
select select
bs.bank_statement_id as bankStatementId, bs.bank_statement_id as bankStatementId,
intermediary.name as suspiciousPersonName, coalesce(credit_customer.name, account.account_name, '信贷客户账号') as suspiciousPersonName,
1 as matchPriority 1 as matchPriority,
'信贷客户' as nameListHitType
from ccdi_bank_statement bs from ccdi_bank_statement bs
inner join ccdi_biz_intermediary intermediary inner join ccdi_account_info account
on trim(bs.customer_cert_no) != '' on trim(bs.customer_account_no) != ''
and intermediary.person_id = bs.customer_cert_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} where bs.project_id = #{query.projectId}
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000
union all union all
select select
bs.bank_statement_id as bankStatementId, bs.bank_statement_id as bankStatementId,
enterprise.enterprise_name as suspiciousPersonName, coalesce(intermediary.name, enterprise.enterprise_name, account.account_name, '中介账号') as suspiciousPersonName,
2 as matchPriority 2 as matchPriority,
'中介' as nameListHitType
from ccdi_bank_statement bs from ccdi_bank_statement bs
inner join ccdi_enterprise_base_info enterprise inner join ccdi_account_info account
on trim(bs.customer_social_credit_code) != '' on trim(bs.customer_account_no) != ''
and enterprise.social_credit_code = bs.customer_social_credit_code and account.owner_type = 'INTERMEDIARY'
and enterprise.risk_level = '1' and account.account_no = bs.customer_account_no
and enterprise.ent_source = 'INTERMEDIARY' 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} where bs.project_id = #{query.projectId}
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000
union all union all
select select
bs.bank_statement_id as bankStatementId, bs.bank_statement_id as bankStatementId,
intermediary.name as suspiciousPersonName, intermediary.name as suspiciousPersonName,
3 as matchPriority 3 as matchPriority,
'中介' as nameListHitType
from ccdi_bank_statement bs from ccdi_bank_statement bs
inner join ccdi_biz_intermediary intermediary inner join ccdi_biz_intermediary intermediary
on trim(bs.CUSTOMER_ACCOUNT_NAME) != '' on trim(bs.CUSTOMER_ACCOUNT_NAME) != ''
and intermediary.name = bs.CUSTOMER_ACCOUNT_NAME and intermediary.name = bs.CUSTOMER_ACCOUNT_NAME
where bs.project_id = #{query.projectId} where bs.project_id = #{query.projectId}
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000
union all union all
select select
bs.bank_statement_id as bankStatementId, bs.bank_statement_id as bankStatementId,
enterprise.enterprise_name as suspiciousPersonName, enterprise.enterprise_name as suspiciousPersonName,
3 as matchPriority 4 as matchPriority,
'中介' as nameListHitType
from ccdi_bank_statement bs from ccdi_bank_statement bs
inner join ccdi_enterprise_base_info enterprise inner join ccdi_enterprise_base_info enterprise
on trim(bs.CUSTOMER_ACCOUNT_NAME) != '' on trim(bs.CUSTOMER_ACCOUNT_NAME) != ''
@@ -588,6 +606,7 @@
and enterprise.risk_level = '1' and enterprise.risk_level = '1'
and enterprise.ent_source = 'INTERMEDIARY' and enterprise.ent_source = 'INTERMEDIARY'
where bs.project_id = #{query.projectId} where bs.project_id = #{query.projectId}
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 1000
) hits ) hits
</sql> </sql>
@@ -605,7 +624,8 @@
1 as hasModelRuleHit, 1 as hasModelRuleHit,
0 as hasNameListHit, 0 as hasNameListHit,
null as suspiciousPersonName, null as suspiciousPersonName,
null as matchPriority null as matchPriority,
null as nameListHitType
from ( from (
<include refid="suspiciousTransactionBaseSql"/> <include refid="suspiciousTransactionBaseSql"/>
) base ) base
@@ -628,7 +648,8 @@
0 as hasModelRuleHit, 0 as hasModelRuleHit,
1 as hasNameListHit, 1 as hasNameListHit,
name_hits.suspiciousPersonName, name_hits.suspiciousPersonName,
name_hits.matchPriority name_hits.matchPriority,
name_hits.nameListHitType
from ( from (
<include refid="suspiciousTransactionBaseSql"/> <include refid="suspiciousTransactionBaseSql"/>
) base ) base
@@ -663,7 +684,18 @@
max(merged.cashType) as cashType, max(merged.cashType) as cashType,
max(merged.displayAmount) as displayAmount, max(merged.displayAmount) as displayAmount,
max(merged.hasModelRuleHit) as hasModelRuleHit, 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 ( from (
<include refid="suspiciousTransactionMergedSql"/> <include refid="suspiciousTransactionMergedSql"/>
) merged ) merged
@@ -701,7 +733,8 @@
final_result.cashType, final_result.cashType,
final_result.displayAmount, final_result.displayAmount,
final_result.hasModelRuleHit, final_result.hasModelRuleHit,
final_result.hasNameListHit final_result.hasNameListHit,
final_result.nameListHitType
from ( from (
<include refid="suspiciousTransactionAggregatedSql"/> <include refid="suspiciousTransactionAggregatedSql"/>
) final_result ) final_result
@@ -722,7 +755,8 @@
final_result.cashType, final_result.cashType,
final_result.displayAmount, final_result.displayAmount,
final_result.hasModelRuleHit, final_result.hasModelRuleHit,
final_result.hasNameListHit final_result.hasNameListHit,
final_result.nameListHitType
from ( from (
<include refid="suspiciousTransactionAggregatedSql"/> <include refid="suspiciousTransactionAggregatedSql"/>
) final_result ) final_result
@@ -742,7 +776,21 @@
final_result.relatedStaffCode, final_result.relatedStaffCode,
final_result.userMemo, final_result.userMemo,
final_result.cashType, 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 final_result.displayAmount
from ( from (
<include refid="suspiciousTransactionAggregatedSql"/> <include refid="suspiciousTransactionAggregatedSql"/>

View File

@@ -100,7 +100,7 @@ class CcdiBankTagAnalysisMapperXmlTest {
void placeholderRules_shouldUseEmptyResultSqlTemplate() throws Exception { void placeholderRules_shouldUseEmptyResultSqlTemplate() throws Exception {
String xml = readXml(RESOURCE); String xml = readXml(RESOURCE);
assertTrue(xml.contains("占位SQL待补充真实规则")); assertTrue(xml.contains("占位SQL待补充真实规则"));
assertEquals(6, countMatches(xml, "where 1 = 0")); assertEquals(5, countMatches(xml, "where 1 = 0"));
} }
@Test @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 @Test
void withdrawCntObjectRule_shouldUseRealSqlAndKeepObjectHitFields() throws Exception { void withdrawCntObjectRule_shouldUseRealSqlAndKeepObjectHitFields() throws Exception {
String xml = readXml(RESOURCE); String xml = readXml(RESOURCE);

View File

@@ -97,6 +97,37 @@ class CcdiProjectOverviewMapperSqlTest {
assertTrue(suspiciousSql.contains("group by merged.bankStatementId"), suspiciousSql); assertTrue(suspiciousSql.contains("group by merged.bankStatementId"), suspiciousSql);
assertTrue(suspiciousSql.contains("hasModelRuleHit"), suspiciousSql); assertTrue(suspiciousSql.contains("hasModelRuleHit"), suspiciousSql);
assertTrue(suspiciousSql.contains("hasNameListHit"), 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 @Test
@@ -159,4 +190,13 @@ class CcdiProjectOverviewMapperSqlTest {
assertTrue(endIndex >= 0, "missing closing select tag: " + selectId); assertTrue(endIndex >= 0, "missing closing select tag: " + selectId);
return xml.substring(startIndex, endIndex); 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)); 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() { private String resolveTestFontPath() {
List<String> candidates = List.of( List<String> candidates = List.of(
"/System/Library/Fonts/STHeiti Medium.ttc", "/System/Library/Fonts/STHeiti Medium.ttc",
@@ -91,6 +102,12 @@ class CcdiProjectOverviewReportPdfExporterTest {
return report; 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() { private CcdiProjectOverviewDashboardVO buildDashboard() {
CcdiProjectOverviewDashboardVO dashboard = new CcdiProjectOverviewDashboardVO(); CcdiProjectOverviewDashboardVO dashboard = new CcdiProjectOverviewDashboardVO();
dashboard.setStats(List.of( 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" :disabled="!projectId"
@click="handleOverviewReportExport" @click="handleOverviewReportExport"
> >
一键导出 导出报告
</el-button> </el-button>
</div> </div>
<overview-stats :summary="currentData.summary" /> <overview-stats :summary="currentData.summary" />

View File

@@ -353,7 +353,61 @@ const SUSPICIOUS_TYPE_OPTIONS = [
{ value: "MODEL_RULE", label: "模型规则命中" }, { 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 = () => ({ const createEmptyDetailData = () => ({
bankStatementId: "", bankStatementId: "",
@@ -443,7 +497,7 @@ export default {
deep: true, deep: true,
handler(value) { handler(value) {
this.projectId = value && value.projectId ? value.projectId : null; 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.suspiciousPageNum = 1;
this.suspiciousPageSize = 5; this.suspiciousPageSize = 5;
this.suspiciousTotal = Number(value && value.total) || 0; this.suspiciousTotal = Number(value && value.total) || 0;
@@ -603,7 +657,7 @@ export default {
return { return {
...detail, ...detail,
...row, ...row,
hitTags: normalizeHitTags(detail.hitTags), hitTags: normalizeHitTags(detail.hitTags, row),
}; };
} catch (error) { } catch (error) {
return { return {
@@ -647,7 +701,14 @@ export default {
this.detailVisible = true; this.detailVisible = true;
this.detailLoading = true; this.detailLoading = true;
try { 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) { } catch (error) {
this.detailData = createEmptyDetailData(); this.detailData = createEmptyDetailData();
this.$message.error("加载流水详情失败"); this.$message.error("加载流水详情失败");

View File

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