优化涉疑交易模型口径和报告展示
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -469,6 +469,7 @@ public class CcdiProjectOverviewReportPdfExporter {
|
||||
private static final float SUBSECTION_FONT_SIZE = 12F;
|
||||
private static final float LINE_HEIGHT = 12F;
|
||||
private static final float CELL_PADDING = 5F;
|
||||
private static final float TABLE_AFTER_GAP = 32F;
|
||||
|
||||
private final PDDocument document;
|
||||
private final PDType0Font font;
|
||||
@@ -496,7 +497,7 @@ public class CcdiProjectOverviewReportPdfExporter {
|
||||
}
|
||||
|
||||
void title(String text) throws IOException {
|
||||
writeLine(text, TITLE_FONT_SIZE, new Color(18, 56, 93), 0F, 28F);
|
||||
writeLine(text, TITLE_FONT_SIZE, new Color(18, 56, 93), 0F, 28F, true);
|
||||
}
|
||||
|
||||
void text(String text, float fontSize, Color color) throws IOException {
|
||||
@@ -505,12 +506,12 @@ public class CcdiProjectOverviewReportPdfExporter {
|
||||
|
||||
void section(String text) throws IOException {
|
||||
ensureSpace(32F);
|
||||
writeLine(text, SECTION_FONT_SIZE, new Color(18, 56, 93), 0F, 26F);
|
||||
writeLine(text, SECTION_FONT_SIZE, new Color(18, 56, 93), 0F, 26F, true);
|
||||
}
|
||||
|
||||
void subsection(String text) throws IOException {
|
||||
ensureSpace(26F);
|
||||
writeLine(text, SUBSECTION_FONT_SIZE, new Color(51, 65, 85), 0F, 22F);
|
||||
writeLine(text, SUBSECTION_FONT_SIZE, new Color(51, 65, 85), 0F, 22F, true);
|
||||
}
|
||||
|
||||
void separator() throws IOException {
|
||||
@@ -557,7 +558,7 @@ public class CcdiProjectOverviewReportPdfExporter {
|
||||
for (List<String> row : safeRows) {
|
||||
drawRow(row, widths, false);
|
||||
}
|
||||
y -= 8F;
|
||||
y -= TABLE_AFTER_GAP;
|
||||
}
|
||||
|
||||
private float[] calculateWidths(float[] ratios) {
|
||||
@@ -632,6 +633,17 @@ public class CcdiProjectOverviewReportPdfExporter {
|
||||
}
|
||||
|
||||
private void writeLine(String text, float fontSize, Color color, float indent, float advance) throws IOException {
|
||||
writeLine(text, fontSize, color, indent, advance, false);
|
||||
}
|
||||
|
||||
private void writeLine(
|
||||
String text,
|
||||
float fontSize,
|
||||
Color color,
|
||||
float indent,
|
||||
float advance,
|
||||
boolean bold
|
||||
) throws IOException {
|
||||
ensureSpace(advance);
|
||||
content.beginText();
|
||||
content.setNonStrokingColor(color);
|
||||
@@ -639,6 +651,14 @@ public class CcdiProjectOverviewReportPdfExporter {
|
||||
content.newLineAtOffset(MARGIN + indent, y);
|
||||
content.showText(text);
|
||||
content.endText();
|
||||
if (bold) {
|
||||
content.beginText();
|
||||
content.setNonStrokingColor(color);
|
||||
content.setFont(font, fontSize);
|
||||
content.newLineAtOffset(MARGIN + indent + 0.25F, y);
|
||||
content.showText(text);
|
||||
content.endText();
|
||||
}
|
||||
y -= advance;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,17 @@ class CcdiProjectOverviewReportPdfExporterTest {
|
||||
assertTrue(exception.getMessage().contains(missingPath));
|
||||
}
|
||||
|
||||
@Test
|
||||
void tableGap_shouldLeaveEnoughSpaceForNextSectionTitle() throws Exception {
|
||||
Class<?> writerClass = Class.forName(
|
||||
"com.ruoyi.ccdi.project.service.impl.CcdiProjectOverviewReportPdfExporter$PdfPageWriter"
|
||||
);
|
||||
float tableAfterGap = readPrivateFloat(writerClass, "TABLE_AFTER_GAP");
|
||||
float sectionFontSize = readPrivateFloat(writerClass, "SECTION_FONT_SIZE");
|
||||
|
||||
assertTrue(tableAfterGap > sectionFontSize);
|
||||
}
|
||||
|
||||
private String resolveTestFontPath() {
|
||||
List<String> candidates = List.of(
|
||||
"/System/Library/Fonts/STHeiti Medium.ttc",
|
||||
@@ -91,6 +102,12 @@ class CcdiProjectOverviewReportPdfExporterTest {
|
||||
return report;
|
||||
}
|
||||
|
||||
private float readPrivateFloat(Class<?> clazz, String fieldName) throws Exception {
|
||||
java.lang.reflect.Field field = clazz.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
return field.getFloat(null);
|
||||
}
|
||||
|
||||
private CcdiProjectOverviewDashboardVO buildDashboard() {
|
||||
CcdiProjectOverviewDashboardVO dashboard = new CcdiProjectOverviewDashboardVO();
|
||||
dashboard.setStats(List.of(
|
||||
|
||||
Reference in New Issue
Block a user