dev-ui #3
@@ -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);
|
||||
|
||||
@@ -33,4 +33,6 @@ public class CcdiProjectSuspiciousTransactionItemVO {
|
||||
private Boolean hasModelRuleHit;
|
||||
|
||||
private Boolean hasNameListHit;
|
||||
|
||||
private String nameListHitType;
|
||||
}
|
||||
|
||||
@@ -393,6 +393,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;
|
||||
@@ -420,7 +421,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 {
|
||||
@@ -429,12 +430,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 {
|
||||
@@ -481,7 +482,7 @@ public class CcdiProjectOverviewReportPdfExporter {
|
||||
for (List<String> row : safeRows) {
|
||||
drawRow(row, widths, false);
|
||||
}
|
||||
y -= 8F;
|
||||
y -= TABLE_AFTER_GAP;
|
||||
}
|
||||
|
||||
private float[] calculateWidths(float[] ratios) {
|
||||
@@ -556,6 +557,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);
|
||||
@@ -563,6 +575,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 < 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, '') <> '浙江兰溪农村商业银行股份有限公司'
|
||||
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 '代发|工资|劳务费'
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,17 @@ class CcdiProjectOverviewReportPdfExporterTest {
|
||||
assertTrue(response.getContentAsByteArray().length > 1000);
|
||||
}
|
||||
|
||||
@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 CcdiProjectOverviewReportVO buildReport() {
|
||||
CcdiProjectOverviewReportVO report = new CcdiProjectOverviewReportVO();
|
||||
CcdiProject project = new CcdiProject();
|
||||
@@ -53,6 +64,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(
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# 2026-05-06 项目分析个人详情页样式对齐前端实施计划
|
||||
|
||||
## 目标
|
||||
|
||||
- 将项目分析个人详情页样式对齐到用户提供的参考图。
|
||||
- 本次只调整前端样式表现,不改接口、字段、交互逻辑和业务内容。
|
||||
|
||||
## 范围
|
||||
|
||||
- `ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisDialog.vue`
|
||||
- `ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisSidebar.vue`
|
||||
- `ruoyi-ui/src/views/ccdiProject/components/detail/ProjectAnalysisAbnormalTab.vue`
|
||||
|
||||
## 实施要点
|
||||
|
||||
- 调整详情弹窗头部、左右分栏比例、页签尺寸和间距。
|
||||
- 调整左侧人物档案与命中模型摘要区块的标题、信息行、风险徽标和标签样式。
|
||||
- 调整右侧异常明细内容区的区块标题、表格头部、单元格留白、异常对象摘要卡片和快照块样式。
|
||||
- 保持现有数据绑定、页签切换、证据库按钮和分页逻辑不变。
|
||||
|
||||
## 验证
|
||||
|
||||
- 在真实业务页面打开项目总览详情弹窗,检查个人详情页视觉是否与参考图一致。
|
||||
- 确认异常明细、对象摘要、加入证据库按钮和分页仍可正常显示。
|
||||
@@ -0,0 +1,18 @@
|
||||
# 2026-05-06 项目分析个人详情页样式对齐实施记录
|
||||
|
||||
## 本次修改
|
||||
|
||||
- 调整 `ProjectAnalysisDialog.vue`,补齐参考图中的标题区、内容区留白、左右分栏间距和页签样式。
|
||||
- 调整 `ProjectAnalysisSidebar.vue`,补齐人物档案区、风险等级徽标、命中模型摘要和标签的正式化版式。
|
||||
- 调整 `ProjectAnalysisAbnormalTab.vue`,补齐流水异常明细表格、异常对象摘要区、快照块和信息行样式。
|
||||
|
||||
## 未改内容
|
||||
|
||||
- 未改接口请求和数据拼装逻辑。
|
||||
- 未改页签切换、分页、加入证据库、异常分组和字段内容。
|
||||
- 未新增或删除业务区块。
|
||||
|
||||
## 验证方式
|
||||
|
||||
- 使用真实业务页面 `http://localhost/ccdiProject/detail/90337?tab=overview` 打开个人详情弹窗进行样式核对。
|
||||
- 核对左侧人物档案、右侧页签、表格块和异常对象摘要块的正式化效果。
|
||||
@@ -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 报告按名单库类型细分展示异常标签,且不重复展示。
|
||||
@@ -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 个测试。
|
||||
@@ -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 导出包含“疑似与中介往来”和“与信贷客户之间非正常资金往来”。
|
||||
|
||||
@@ -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`,默认日期符合“开始默认一年前、结束默认昨天”。
|
||||
@@ -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 个测试。
|
||||
- 新增测试断言表格后间距必须大于章节标题字号,避免后续回退成过小间距。
|
||||
@@ -27,7 +27,7 @@
|
||||
:disabled="!projectId"
|
||||
@click="handleOverviewReportExport"
|
||||
>
|
||||
一键导出
|
||||
导出报告
|
||||
</el-button>
|
||||
</div>
|
||||
<overview-stats :summary="currentData.summary" />
|
||||
|
||||
@@ -244,28 +244,31 @@ export default {
|
||||
.project-analysis-abnormal-tab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.abnormal-card {
|
||||
padding: 20px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border: 1px solid #d9e1ed;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.abnormal-card__header {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 0;
|
||||
padding: 22px 30px;
|
||||
border-bottom: 1px solid #e3eaf3;
|
||||
}
|
||||
|
||||
.abnormal-card__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.abnormal-card__content {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
gap: 0;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.abnormal-card__subtitle {
|
||||
@@ -274,14 +277,15 @@ export default {
|
||||
}
|
||||
|
||||
.abnormal-table {
|
||||
border-radius: 12px;
|
||||
border-radius: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.abnormal-pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
padding: 18px 24px 20px;
|
||||
border-top: 1px solid #e8eef6;
|
||||
}
|
||||
|
||||
.multi-line-cell {
|
||||
@@ -303,7 +307,7 @@ export default {
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.object-card-grid {
|
||||
@@ -314,8 +318,8 @@ export default {
|
||||
|
||||
.object-card {
|
||||
padding: 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #d9e1ed;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.object-card__header {
|
||||
@@ -364,7 +368,8 @@ export default {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid #dbeafe;
|
||||
background: #eff6ff;
|
||||
border-left: 3px solid #245b8f;
|
||||
background: #f4f8fd;
|
||||
}
|
||||
|
||||
.object-card__snapshot-label {
|
||||
@@ -392,4 +397,36 @@ export default {
|
||||
color: #1e293b;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.abnormal-table ::v-deep(.el-table__header-wrapper th) {
|
||||
height: 58px;
|
||||
padding: 0 22px;
|
||||
border-bottom: 1px solid #d9e1ed;
|
||||
background: #f3f6fa;
|
||||
color: #5f7592;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.abnormal-table ::v-deep(.el-table__body-wrapper td) {
|
||||
padding: 22px;
|
||||
border-bottom: 1px solid #e6edf5;
|
||||
}
|
||||
|
||||
.abnormal-table ::v-deep(.cell) {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.tag-list ::v-deep(.el-tag) {
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #c8d6e8;
|
||||
border-radius: 2px;
|
||||
background: #ffffff;
|
||||
color: #245b8f;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 26px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -223,7 +223,7 @@ export default {
|
||||
}
|
||||
|
||||
.project-analysis-header {
|
||||
padding: 32px 36px 24px;
|
||||
padding: 38px 54px 34px;
|
||||
border-bottom: 1px solid #dde3ec;
|
||||
background: #ffffff;
|
||||
}
|
||||
@@ -241,15 +241,15 @@ export default {
|
||||
|
||||
.project-analysis-header__eyebrow {
|
||||
color: #65758d;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.project-analysis-header__title {
|
||||
margin-top: 18px;
|
||||
margin-top: 24px;
|
||||
color: #101a2b;
|
||||
font-size: 30px;
|
||||
font-size: 34px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -280,10 +280,10 @@ export default {
|
||||
.project-analysis-workspace {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 36px;
|
||||
min-height: 700px;
|
||||
gap: 56px;
|
||||
min-height: 720px;
|
||||
max-height: calc(96vh - 168px);
|
||||
padding: 36px;
|
||||
padding: 28px 54px 42px;
|
||||
overflow: auto;
|
||||
background: #ffffff;
|
||||
}
|
||||
@@ -295,14 +295,15 @@ export default {
|
||||
}
|
||||
|
||||
.project-analysis-layout__sidebar {
|
||||
flex: 0 0 420px;
|
||||
flex: 0 0 38%;
|
||||
max-width: 520px;
|
||||
}
|
||||
|
||||
.project-analysis-layout__main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border-left: 1px solid #dde3ec;
|
||||
padding-left: 36px;
|
||||
padding-left: 56px;
|
||||
}
|
||||
|
||||
.project-analysis-layout__alert {
|
||||
@@ -310,7 +311,7 @@ export default {
|
||||
}
|
||||
|
||||
.project-analysis-tabs {
|
||||
margin-top: -6px;
|
||||
margin-top: -2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -342,26 +343,26 @@ export default {
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
height: 46px;
|
||||
padding: 0 24px !important;
|
||||
height: 58px;
|
||||
padding: 0 32px !important;
|
||||
color: #2a374a;
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
line-height: 46px;
|
||||
line-height: 58px;
|
||||
}
|
||||
|
||||
.el-tabs__item.is-active {
|
||||
color: #245b8f;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.el-tabs__active-bar {
|
||||
height: 3px;
|
||||
height: 4px;
|
||||
background: #245b8f;
|
||||
}
|
||||
|
||||
.el-tabs__content {
|
||||
padding-top: 20px;
|
||||
padding-top: 28px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -91,21 +91,21 @@ export default {
|
||||
}
|
||||
|
||||
.sidebar-profile {
|
||||
padding: 24px 26px 20px;
|
||||
padding: 34px 42px 28px;
|
||||
border-bottom: 1px solid #edf1f5;
|
||||
}
|
||||
|
||||
.sidebar-summary {
|
||||
padding: 22px 26px 26px;
|
||||
padding: 30px 42px 34px;
|
||||
border-top: 1px solid #dde3ec;
|
||||
background: #fcfdfe;
|
||||
}
|
||||
|
||||
.sidebar-profile__identity-label {
|
||||
color: #637187;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.sidebar-profile__name-row {
|
||||
@@ -117,7 +117,7 @@ export default {
|
||||
|
||||
.sidebar-profile__name {
|
||||
color: #111827;
|
||||
font-size: 30px;
|
||||
font-size: 34px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
@@ -126,30 +126,30 @@ export default {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 64px;
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #edcaca;
|
||||
min-width: 102px;
|
||||
height: 44px;
|
||||
padding: 0 16px;
|
||||
border: 1px solid #f0c6c1;
|
||||
border-radius: 2px;
|
||||
background: #fbefef;
|
||||
color: #ad2f2f;
|
||||
font-size: 14px;
|
||||
background: #fff3f2;
|
||||
color: #c43d33;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
line-height: 26px;
|
||||
line-height: 42px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sidebar-profile__meta {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
margin-top: 18px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.sidebar-profile__item {
|
||||
display: grid;
|
||||
grid-template-columns: 88px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
padding: 10px 0;
|
||||
grid-template-columns: 116px minmax(0, 1fr);
|
||||
gap: 16px;
|
||||
padding: 18px 0;
|
||||
border-bottom: 1px solid #edf1f5;
|
||||
}
|
||||
|
||||
@@ -160,46 +160,46 @@ export default {
|
||||
.sidebar-profile__label,
|
||||
.sidebar-summary__count-label {
|
||||
color: #637187;
|
||||
font-size: 15px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.sidebar-profile__value,
|
||||
.sidebar-summary__count-value {
|
||||
color: #172033;
|
||||
font-size: 16px;
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.sidebar-summary__title {
|
||||
margin: 0 0 18px;
|
||||
margin: 0 0 24px;
|
||||
color: #223047;
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sidebar-summary__count {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
gap: 16px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 22px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.sidebar-summary__tags {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
gap: 16px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.sidebar-summary__count-label {
|
||||
font-size: 15px;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.sidebar-summary__count-value {
|
||||
color: #172033;
|
||||
font-size: 22px;
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -212,14 +212,14 @@ export default {
|
||||
}
|
||||
|
||||
.sidebar-tag-list ::v-deep(.el-tag) {
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #bfd0e2;
|
||||
height: 38px;
|
||||
padding: 0 16px;
|
||||
border: 1px solid #cad8eb;
|
||||
border-radius: 2px;
|
||||
background: #ffffff;
|
||||
color: #245b8f;
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 26px;
|
||||
line-height: 36px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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("加载流水详情失败");
|
||||
|
||||
@@ -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="身份证文件">
|
||||
@@ -423,7 +423,7 @@ export default {
|
||||
resetPullBankInfoForm() {
|
||||
this.pullBankInfoForm = {
|
||||
idCardText: "",
|
||||
dateRange: [],
|
||||
dateRange: this.buildDefaultPullBankInfoDateRange(),
|
||||
};
|
||||
this.idCardFileList = [];
|
||||
this.parsingIdCardFile = false;
|
||||
@@ -433,7 +433,7 @@ export default {
|
||||
return Array.from(
|
||||
new Set(
|
||||
(text || "")
|
||||
.split(/[\n,,]+/)
|
||||
.split(/,+/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
)
|
||||
@@ -503,11 +503,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)
|
||||
@@ -519,13 +544,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() {
|
||||
@@ -554,7 +589,7 @@ export default {
|
||||
}
|
||||
|
||||
if (this.hasInvalidPullBankInfoDateRange([startDate, endDate])) {
|
||||
this.$message.warning("时间跨度最晚只能选择到昨天");
|
||||
this.$message.warning("时间跨度仅支持 2025-01-01 至昨天");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user