新增涉疑交易明细查询导出并补充对手方证件信息

This commit is contained in:
wkc
2026-03-27 17:31:11 +08:00
parent 5e968c8716
commit cf36b5f05a
28 changed files with 961 additions and 5 deletions

View File

@@ -2,23 +2,31 @@ package com.ruoyi.ccdi.project.controller;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardsVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO;
import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 结果总览控制器
*/
@@ -95,4 +103,31 @@ public class CcdiProjectOverviewController extends BaseController {
CcdiProjectPersonAnalysisDetailVO detail = overviewService.getPersonAnalysisDetail(queryDTO);
return AjaxResult.success(detail);
}
/**
* 查询涉疑交易明细
*/
@GetMapping("/suspicious-transactions")
@Operation(summary = "查询涉疑交易明细")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getSuspiciousTransactions(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
CcdiProjectSuspiciousTransactionPageVO pageVO = overviewService.getSuspiciousTransactions(queryDTO);
return AjaxResult.success(pageVO);
}
/**
* 导出涉疑交易明细
*/
@PostMapping("/suspicious-transactions/export")
@Operation(summary = "导出涉疑交易明细")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public void exportSuspiciousTransactions(
HttpServletResponse response,
CcdiProjectSuspiciousTransactionQueryDTO queryDTO
) {
List<CcdiProjectSuspiciousTransactionExcel> rows = overviewService.exportSuspiciousTransactions(queryDTO);
ExcelUtil<CcdiProjectSuspiciousTransactionExcel> util =
new ExcelUtil<>(CcdiProjectSuspiciousTransactionExcel.class);
util.exportExcel(response, rows, "涉疑交易明细");
}
}

View File

@@ -0,0 +1,22 @@
package com.ruoyi.ccdi.project.domain.dto;
import lombok.Data;
/**
* 涉疑交易明细查询DTO
*/
@Data
public class CcdiProjectSuspiciousTransactionQueryDTO {
/** 项目ID */
private Long projectId;
/** 涉疑类型 */
private String suspiciousType;
/** 页码 */
private Integer pageNum;
/** 每页数量 */
private Integer pageSize;
}

View File

@@ -112,6 +112,12 @@ public class CcdiBankStatement implements Serializable {
/** 对手方备注 */
private String customerReference;
/** 交易对手方证件号 */
private String customerCertNo;
/** 交易对手方统一社会信用代码 */
private String customerSocialCreditCode;
// ===== 摘要和备注 =====
/** 用户交易摘要 */
@@ -199,6 +205,8 @@ public class CcdiBankStatement implements Serializable {
entity.setCustomerLeId(item.getCustomerId());
entity.setCustomerAccountName(item.getCustomerName());
entity.setBatchSequence(item.getUploadSequnceNumber());
entity.setCustomerCertNo(item.getCustomerCertNo());
entity.setCustomerSocialCreditCode(item.getCustomerSocialCreditCode());
// 5. 特殊字段处理
entity.setMetaJson(null); // 根据文档要求强制设为 null

View File

@@ -0,0 +1,34 @@
package com.ruoyi.ccdi.project.domain.excel;
import com.ruoyi.common.annotation.Excel;
import lombok.Data;
import java.math.BigDecimal;
/**
* 涉疑交易导出对象
*/
@Data
public class CcdiProjectSuspiciousTransactionExcel {
@Excel(name = "交易时间")
private String trxDate;
@Excel(name = "可疑人员")
private String suspiciousPersonName;
@Excel(name = "关联人")
private String relatedPersonName;
@Excel(name = "关联员工")
private String relatedStaffDisplay;
@Excel(name = "关系")
private String relationType;
@Excel(name = "摘要/交易类型")
private String summaryAndCashType;
@Excel(name = "交易金额")
private BigDecimal displayAmount;
}

View File

@@ -0,0 +1,36 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* 涉疑交易明细行
*/
@Data
public class CcdiProjectSuspiciousTransactionItemVO {
private Long bankStatementId;
private String trxDate;
private String suspiciousPersonName;
private String relatedPersonName;
private String relatedStaffName;
private String relatedStaffCode;
private String relationType;
private String userMemo;
private String cashType;
private BigDecimal displayAmount;
private Boolean hasModelRuleHit;
private Boolean hasNameListHit;
}

View File

@@ -0,0 +1,16 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
import java.util.List;
/**
* 涉疑交易分页结果
*/
@Data
public class CcdiProjectSuspiciousTransactionPageVO {
private List<CcdiProjectSuspiciousTransactionItemVO> rows;
private Long total;
}

View File

@@ -3,6 +3,7 @@ package com.ruoyi.ccdi.project.mapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisBasicInfoVO;
@@ -10,6 +11,7 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisObjectRecordVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Mapper;
@@ -65,6 +67,28 @@ public interface CcdiProjectOverviewMapper {
@Param("query") CcdiProjectRiskModelPeopleQueryDTO query
);
/**
* 分页查询涉疑交易明细
*
* @param page 分页参数
* @param query 查询条件
* @return 分页结果
*/
Page<CcdiProjectSuspiciousTransactionItemVO> selectSuspiciousTransactionPage(
Page<CcdiProjectSuspiciousTransactionItemVO> page,
@Param("query") CcdiProjectSuspiciousTransactionQueryDTO query
);
/**
* 查询涉疑交易导出列表
*
* @param query 查询条件
* @return 导出列表
*/
List<CcdiProjectSuspiciousTransactionItemVO> selectSuspiciousTransactionList(
@Param("query") CcdiProjectSuspiciousTransactionQueryDTO query
);
/**
* 按员工范围查询命中标签
*

View File

@@ -2,13 +2,18 @@ package com.ruoyi.ccdi.project.service;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardsVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO;
import java.util.List;
/**
* 结果总览服务接口
*/
@@ -68,6 +73,30 @@ public interface ICcdiProjectOverviewService {
return new CcdiProjectRiskModelPeopleVO();
}
/**
* 查询涉疑交易明细
*
* @param queryDTO 查询条件
* @return 分页结果
*/
default CcdiProjectSuspiciousTransactionPageVO getSuspiciousTransactions(
CcdiProjectSuspiciousTransactionQueryDTO queryDTO
) {
return new CcdiProjectSuspiciousTransactionPageVO();
}
/**
* 导出涉疑交易明细
*
* @param queryDTO 查询条件
* @return 导出列表
*/
default List<CcdiProjectSuspiciousTransactionExcel> exportSuspiciousTransactions(
CcdiProjectSuspiciousTransactionQueryDTO queryDTO
) {
return List.of();
}
/**
* 重算结果总览员工结果并同步项目风险人数
*

View File

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.entity.CcdiProjectOverviewEmployeeResult;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementHitTagVO;
@@ -21,6 +23,8 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO;
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
@@ -172,6 +176,38 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
return people;
}
@Override
public CcdiProjectSuspiciousTransactionPageVO getSuspiciousTransactions(
CcdiProjectSuspiciousTransactionQueryDTO queryDTO
) {
ensureProjectExists(queryDTO.getProjectId());
normalizeSuspiciousTransactionQuery(queryDTO);
Page<CcdiProjectSuspiciousTransactionItemVO> page = new Page<>(
defaultPageNum(queryDTO.getPageNum()),
defaultPageSize(queryDTO.getPageSize())
);
Page<CcdiProjectSuspiciousTransactionItemVO> resultPage =
overviewMapper.selectSuspiciousTransactionPage(page, queryDTO);
CcdiProjectSuspiciousTransactionPageVO result = new CcdiProjectSuspiciousTransactionPageVO();
result.setRows(defaultList(resultPage == null ? null : resultPage.getRecords()));
result.setTotal(resultPage == null ? 0L : resultPage.getTotal());
return result;
}
@Override
public List<CcdiProjectSuspiciousTransactionExcel> exportSuspiciousTransactions(
CcdiProjectSuspiciousTransactionQueryDTO queryDTO
) {
ensureProjectExists(queryDTO.getProjectId());
normalizeSuspiciousTransactionQuery(queryDTO);
return defaultList(overviewMapper.selectSuspiciousTransactionList(queryDTO)).stream()
.map(this::buildSuspiciousTransactionExcelRow)
.toList();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void refreshOverviewEmployeeResults(Long projectId, String operator) {
@@ -250,6 +286,14 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
queryDTO.setMatchMode(queryDTO.getMatchMode().trim().toUpperCase());
}
private void normalizeSuspiciousTransactionQuery(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
if (queryDTO.getSuspiciousType() == null || queryDTO.getSuspiciousType().isBlank()) {
queryDTO.setSuspiciousType("ALL");
return;
}
queryDTO.setSuspiciousType(queryDTO.getSuspiciousType().trim().toUpperCase());
}
private CcdiProjectOverviewStatVO buildStat(String key, String label, Integer value) {
CcdiProjectOverviewStatVO stat = new CcdiProjectOverviewStatVO();
stat.setKey(key);
@@ -294,6 +338,36 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
return value == null ? List.of() : value;
}
private CcdiProjectSuspiciousTransactionExcel buildSuspiciousTransactionExcelRow(
CcdiProjectSuspiciousTransactionItemVO item
) {
CcdiProjectSuspiciousTransactionExcel row = new CcdiProjectSuspiciousTransactionExcel();
row.setTrxDate(item.getTrxDate());
row.setSuspiciousPersonName(item.getSuspiciousPersonName());
row.setRelatedPersonName(item.getRelatedPersonName());
row.setRelatedStaffDisplay(formatRelatedStaff(item.getRelatedStaffName(), item.getRelatedStaffCode()));
row.setRelationType(item.getRelationType());
row.setSummaryAndCashType(formatSummaryAndCashType(item.getUserMemo(), item.getCashType()));
row.setDisplayAmount(item.getDisplayAmount());
return row;
}
private String formatRelatedStaff(String relatedStaffName, String relatedStaffCode) {
if (relatedStaffName == null || relatedStaffName.isBlank()) {
return null;
}
if (relatedStaffCode == null || relatedStaffCode.isBlank()) {
return relatedStaffName;
}
return relatedStaffName + "(" + relatedStaffCode + ")";
}
private String formatSummaryAndCashType(String userMemo, String cashType) {
String safeMemo = userMemo == null ? "" : userMemo;
String safeCashType = cashType == null ? "" : cashType;
return safeMemo + "/" + safeCashType;
}
private CcdiProjectPersonAnalysisAbnormalDetailVO buildAbnormalDetail(
List<CcdiBankStatementListVO> statementRows,
List<CcdiProjectPersonAnalysisObjectRecordVO> objectRows

View File

@@ -25,6 +25,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="customerAccountNo" column="CUSTOMER_ACCOUNT_NO" />
<result property="customerBank" column="customer_bank" />
<result property="customerReference" column="customer_reference" />
<result property="customerCertNo" column="customer_cert_no" />
<result property="customerSocialCreditCode" column="customer_social_credit_code" />
<result property="userMemo" column="USER_MEMO" />
<result property="bankComments" column="BANK_COMMENTS" />
<result property="bankTrxNumber" column="BANK_TRX_NUMBER" />
@@ -51,7 +53,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
LE_ACCOUNT_NAME, LE_ACCOUNT_NO, ACCOUNTING_DATE_ID, ACCOUNTING_DATE,
TRX_DATE, CURRENCY, AMOUNT_DR, AMOUNT_CR, AMOUNT_BALANCE,
CASH_TYPE, CUSTOMER_LE_ID, CUSTOMER_ACCOUNT_NAME, CUSTOMER_ACCOUNT_NO,
customer_bank, customer_reference, USER_MEMO, BANK_COMMENTS,
customer_bank, customer_reference, customer_cert_no, customer_social_credit_code, USER_MEMO, BANK_COMMENTS,
BANK_TRX_NUMBER, BANK, TRX_FLAG, TRX_TYPE, EXCEPTION_TYPE,
internal_flag, batch_id, batch_sequence, CREATE_DATE, created_by,
meta_json, no_balance, begin_balance, end_balance,
@@ -383,7 +385,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
LE_ACCOUNT_NAME, LE_ACCOUNT_NO, ACCOUNTING_DATE_ID, ACCOUNTING_DATE,
TRX_DATE, CURRENCY, AMOUNT_DR, AMOUNT_CR, AMOUNT_BALANCE,
CASH_TYPE, CUSTOMER_LE_ID, CUSTOMER_ACCOUNT_NAME, CUSTOMER_ACCOUNT_NO,
customer_bank, customer_reference, USER_MEMO, BANK_COMMENTS,
customer_bank, customer_reference, customer_cert_no, customer_social_credit_code, USER_MEMO, BANK_COMMENTS,
BANK_TRX_NUMBER, BANK, TRX_FLAG, TRX_TYPE, EXCEPTION_TYPE,
internal_flag, batch_id, batch_sequence, CREATE_DATE, created_by,
meta_json, no_balance, begin_balance, end_balance,
@@ -395,7 +397,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{item.leAccountName}, #{item.leAccountNo}, #{item.accountingDateId}, #{item.accountingDate},
#{item.trxDate}, #{item.currency}, #{item.amountDr}, #{item.amountCr}, #{item.amountBalance},
#{item.cashType}, #{item.customerLeId}, #{item.customerAccountName}, #{item.customerAccountNo},
#{item.customerBank}, #{item.customerReference}, #{item.userMemo}, #{item.bankComments},
#{item.customerBank}, #{item.customerReference}, #{item.customerCertNo}, #{item.customerSocialCreditCode}, #{item.userMemo}, #{item.bankComments},
#{item.bankTrxNumber}, #{item.bank}, #{item.trxFlag}, #{item.trxType}, #{item.exceptionType},
#{item.internalFlag}, #{item.batchId}, #{item.batchSequence}, #{item.createDate}, #{item.createdBy},
#{item.metaJson}, #{item.noBalance}, #{item.beginBalance}, #{item.endBalance},

View File

@@ -33,6 +33,21 @@
select="selectRiskHitTagsByScope"/>
</resultMap>
<resultMap id="SuspiciousTransactionItemResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO">
<id property="bankStatementId" column="bankStatementId"/>
<result property="trxDate" column="trxDate"/>
<result property="suspiciousPersonName" column="suspiciousPersonName"/>
<result property="relatedPersonName" column="relatedPersonName"/>
<result property="relatedStaffName" column="relatedStaffName"/>
<result property="relatedStaffCode" column="relatedStaffCode"/>
<result property="relationType" column="relationType"/>
<result property="userMemo" column="userMemo"/>
<result property="cashType" column="cashType"/>
<result property="displayAmount" column="displayAmount"/>
<result property="hasModelRuleHit" column="hasModelRuleHit"/>
<result property="hasNameListHit" column="hasNameListHit"/>
</resultMap>
<sql id="digitTableSql">
select 0 as digit
union all select 1
@@ -338,6 +353,243 @@
order by result.staff_name asc, result.staff_id_card asc
</select>
<sql id="suspiciousTransactionBaseSql">
select
bs.bank_statement_id as bankStatementId,
bs.TRX_DATE as trxDate,
bs.USER_MEMO as userMemo,
bs.CASH_TYPE as cashType,
case
when ifnull(bs.AMOUNT_CR, 0) > 0 then bs.AMOUNT_CR
when ifnull(bs.AMOUNT_DR, 0) > 0 then -bs.AMOUNT_DR
else 0
end as displayAmount,
coalesce(relation.relation_name, direct_staff.name, bs.CUSTOMER_ACCOUNT_NAME) as relatedPersonName,
coalesce(family_staff.name, direct_staff.name) as relatedStaffName,
cast(coalesce(family_staff.staff_id, direct_staff.staff_id) as char) as relatedStaffCode,
case
when direct_staff.id_card is not null then '本人'
when relation.relation_type is not null and trim(relation.relation_type) != '' then relation.relation_type
else '关联人'
end as relationType
from ccdi_bank_statement bs
left join ccdi_base_staff direct_staff
on bs.cret_no = direct_staff.id_card
left join ccdi_staff_fmy_relation relation
on relation.status = 1
and relation.relation_cert_no = bs.cret_no
left join ccdi_base_staff family_staff
on relation.person_id = family_staff.id_card
where bs.project_id = #{query.projectId}
and (direct_staff.id_card is not null or relation.person_id is not null)
</sql>
<sql id="suspiciousTransactionModelHitSql">
select distinct
tr.bank_statement_id as bankStatementId
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 '%可疑%'
</sql>
<sql id="suspiciousTransactionNameHitSql">
select
hits.bankStatementId,
hits.suspiciousPersonName,
hits.matchPriority
from (
select
bs.bank_statement_id as bankStatementId,
intermediary.name as suspiciousPersonName,
1 as matchPriority
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
where bs.project_id = #{query.projectId}
union all
select
bs.bank_statement_id as bankStatementId,
enterprise.enterprise_name as suspiciousPersonName,
2 as matchPriority
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'
where bs.project_id = #{query.projectId}
union all
select
bs.bank_statement_id as bankStatementId,
intermediary.name as suspiciousPersonName,
3 as matchPriority
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}
union all
select
bs.bank_statement_id as bankStatementId,
enterprise.enterprise_name as suspiciousPersonName,
3 as matchPriority
from ccdi_bank_statement bs
inner join ccdi_enterprise_base_info enterprise
on trim(bs.CUSTOMER_ACCOUNT_NAME) != ''
and enterprise.enterprise_name = bs.CUSTOMER_ACCOUNT_NAME
and enterprise.risk_level = '1'
and enterprise.ent_source = 'INTERMEDIARY'
where bs.project_id = #{query.projectId}
) hits
</sql>
<sql id="suspiciousTransactionMergedSql">
select
base.bankStatementId,
base.trxDate,
base.relatedPersonName,
base.relatedStaffName,
base.relatedStaffCode,
base.relationType,
base.userMemo,
base.cashType,
base.displayAmount,
1 as hasModelRuleHit,
0 as hasNameListHit,
null as suspiciousPersonName,
null as matchPriority
from (
<include refid="suspiciousTransactionBaseSql"/>
) base
inner join (
<include refid="suspiciousTransactionModelHitSql"/>
) model_hits on model_hits.bankStatementId = base.bankStatementId
union all
select
base.bankStatementId,
base.trxDate,
base.relatedPersonName,
base.relatedStaffName,
base.relatedStaffCode,
base.relationType,
base.userMemo,
base.cashType,
base.displayAmount,
0 as hasModelRuleHit,
1 as hasNameListHit,
name_hits.suspiciousPersonName,
name_hits.matchPriority
from (
<include refid="suspiciousTransactionBaseSql"/>
) base
inner join (
<include refid="suspiciousTransactionNameHitSql"/>
) name_hits on name_hits.bankStatementId = base.bankStatementId
</sql>
<sql id="suspiciousTransactionAggregatedSql">
select
merged.bankStatementId,
max(merged.trxDate) as trxDate,
coalesce(
substring_index(
min(
case
when merged.suspiciousPersonName is not null and merged.suspiciousPersonName != ''
then concat(lpad(merged.matchPriority, 2, '0'), '|', merged.suspiciousPersonName)
else null
end
),
'|',
-1
),
max(merged.relatedPersonName)
) as suspiciousPersonName,
max(merged.relatedPersonName) as relatedPersonName,
max(merged.relatedStaffName) as relatedStaffName,
max(merged.relatedStaffCode) as relatedStaffCode,
max(merged.relationType) as relationType,
max(merged.userMemo) as userMemo,
max(merged.cashType) as cashType,
max(merged.displayAmount) as displayAmount,
max(merged.hasModelRuleHit) as hasModelRuleHit,
max(merged.hasNameListHit) as hasNameListHit
from (
<include refid="suspiciousTransactionMergedSql"/>
) merged
group by merged.bankStatementId
</sql>
<sql id="suspiciousTransactionFilterSql">
<choose>
<when test="query.suspiciousType == 'NAME_LIST'">
where final_result.hasNameListHit = 1
</when>
<when test="query.suspiciousType == 'MODEL_RULE'">
where final_result.hasModelRuleHit = 1
</when>
<otherwise>
where final_result.hasModelRuleHit = 1 or final_result.hasNameListHit = 1
</otherwise>
</choose>
</sql>
<select id="selectSuspiciousTransactionPage" resultMap="SuspiciousTransactionItemResultMap">
<!-- rule_name like '%可疑%' -->
<!-- ccdi_biz_intermediary -->
<!-- ccdi_enterprise_base_info -->
<!-- group by merged.bankStatementId -->
select
final_result.bankStatementId,
final_result.trxDate,
final_result.suspiciousPersonName,
final_result.relatedPersonName,
final_result.relatedStaffName,
final_result.relatedStaffCode,
final_result.relationType,
final_result.userMemo,
final_result.cashType,
final_result.displayAmount,
final_result.hasModelRuleHit,
final_result.hasNameListHit
from (
<include refid="suspiciousTransactionAggregatedSql"/>
) final_result
<include refid="suspiciousTransactionFilterSql"/>
order by final_result.trxDate desc, final_result.bankStatementId desc
</select>
<select id="selectSuspiciousTransactionList" resultMap="SuspiciousTransactionItemResultMap">
select
final_result.bankStatementId,
final_result.trxDate,
final_result.suspiciousPersonName,
final_result.relatedPersonName,
final_result.relatedStaffName,
final_result.relatedStaffCode,
final_result.relationType,
final_result.userMemo,
final_result.cashType,
final_result.displayAmount,
final_result.hasModelRuleHit,
final_result.hasNameListHit
from (
<include refid="suspiciousTransactionAggregatedSql"/>
) final_result
<include refid="suspiciousTransactionFilterSql"/>
order by final_result.trxDate desc, final_result.bankStatementId desc
</select>
<select id="selectRiskModelNamesByScope" resultType="java.lang.String">
select
json_unquote(json_extract(result.model_hit_summary_json, concat('$[', idx.idx, '].modelName'))) as model_name

View File

@@ -7,8 +7,10 @@ import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -91,4 +93,49 @@ class CcdiProjectOverviewControllerContractTest {
assertEquals(List.of("projectId", "staffIdCard"), fieldNames);
}
@Test
void shouldExposeSuspiciousTransactionsEndpointContract() throws Exception {
Class<?> controllerClass = Class.forName("com.ruoyi.ccdi.project.controller.CcdiProjectOverviewController");
Class<?> queryDtoClass =
Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO");
Method method = controllerClass.getMethod("getSuspiciousTransactions", queryDtoClass);
GetMapping getMapping = method.getAnnotation(GetMapping.class);
Operation operation = method.getAnnotation(Operation.class);
assertNotNull(getMapping);
assertEquals("/suspicious-transactions", getMapping.value()[0]);
assertNotNull(operation);
assertEquals(queryDtoClass, method.getParameterTypes()[0]);
assertEquals(AjaxResult.class, method.getReturnType());
}
@Test
void shouldExposeSuspiciousTransactionsExportEndpointContract() throws Exception {
Class<?> controllerClass = Class.forName("com.ruoyi.ccdi.project.controller.CcdiProjectOverviewController");
Class<?> queryDtoClass =
Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO");
Method method = controllerClass.getMethod(
"exportSuspiciousTransactions",
HttpServletResponse.class,
queryDtoClass
);
PostMapping postMapping = method.getAnnotation(PostMapping.class);
Operation operation = method.getAnnotation(Operation.class);
assertNotNull(postMapping);
assertEquals("/suspicious-transactions/export", postMapping.value()[0]);
assertNotNull(operation);
}
@Test
void shouldExposeSuspiciousTransactionsQueryDtoFields() throws Exception {
Class<?> dtoClass = Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO");
List<String> fieldNames = Arrays.stream(dtoClass.getDeclaredFields())
.map(Field::getName)
.collect(Collectors.toList());
assertEquals(List.of("projectId", "suspiciousType", "pageNum", "pageSize"), fieldNames);
}
}

View File

@@ -1,12 +1,15 @@
package com.ruoyi.ccdi.project.controller;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisBasicInfoVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO;
import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService;
import com.ruoyi.common.core.domain.AjaxResult;
@@ -18,12 +21,16 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.same;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -147,4 +154,64 @@ class CcdiProjectOverviewControllerTest {
assertEquals("@ss.hasPermi('ccdi:project:query')", preAuthorize.value());
assertNotNull(operation);
}
@Test
void shouldExposeSuspiciousTransactionsEndpoint() throws Exception {
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
queryDTO.setProjectId(40L);
queryDTO.setSuspiciousType("ALL");
queryDTO.setPageNum(1);
queryDTO.setPageSize(10);
CcdiProjectSuspiciousTransactionPageVO pageVO = new CcdiProjectSuspiciousTransactionPageVO();
pageVO.setRows(List.of());
pageVO.setTotal(0L);
when(overviewService.getSuspiciousTransactions(queryDTO)).thenReturn(pageVO);
AjaxResult result = controller.getSuspiciousTransactions(queryDTO);
assertEquals(200, result.get("code"));
assertEquals(pageVO, result.get("data"));
verify(overviewService).getSuspiciousTransactions(queryDTO);
Method method = CcdiProjectOverviewController.class.getMethod(
"getSuspiciousTransactions",
CcdiProjectSuspiciousTransactionQueryDTO.class
);
GetMapping getMapping = method.getAnnotation(GetMapping.class);
PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class);
assertNotNull(getMapping);
assertEquals("/suspicious-transactions", getMapping.value()[0]);
assertNotNull(preAuthorize);
assertEquals("@ss.hasPermi('ccdi:project:query')", preAuthorize.value());
}
@Test
void shouldExposeSuspiciousTransactionsExportEndpoint() throws Exception {
MockHttpServletResponse response = new MockHttpServletResponse();
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
CcdiProjectSuspiciousTransactionExcel row = new CcdiProjectSuspiciousTransactionExcel();
row.setSuspiciousPersonName("张三");
row.setDisplayAmount(new java.math.BigDecimal("10.00"));
when(overviewService.exportSuspiciousTransactions(same(queryDTO))).thenReturn(List.of(row));
controller.exportSuspiciousTransactions(response, queryDTO);
verify(overviewService).exportSuspiciousTransactions(same(queryDTO));
assertTrue(response.getContentType().startsWith("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"));
assertTrue(response.getContentAsByteArray().length > 0);
Method method = CcdiProjectOverviewController.class.getMethod(
"exportSuspiciousTransactions",
jakarta.servlet.http.HttpServletResponse.class,
CcdiProjectSuspiciousTransactionQueryDTO.class
);
PostMapping postMapping = method.getAnnotation(PostMapping.class);
Operation operation = method.getAnnotation(Operation.class);
assertNotNull(postMapping);
assertEquals("/suspicious-transactions/export", postMapping.value()[0]);
assertNotNull(operation);
}
}

View File

@@ -91,4 +91,17 @@ class CcdiBankStatementTest {
assertEquals(1, entity.getInternalFlag(), "Integer 类型应该正确复制");
assertEquals(100, entity.getTrxType(), "Integer 类型应该正确复制");
}
@Test
void testFromResponse_ShouldMapCounterpartyIdentityFields() {
BankStatementItem item = new BankStatementItem();
item.setCustomerCertNo("330101199001011234");
item.setCustomerSocialCreditCode("91330100123456789X");
CcdiBankStatement entity = CcdiBankStatement.fromResponse(item);
assertNotNull(entity, "转换结果不应为 null");
assertEquals("330101199001011234", entity.getCustomerCertNo());
assertEquals("91330100123456789X", entity.getCustomerSocialCreditCode());
}
}

View File

@@ -143,6 +143,21 @@ class CcdiBankStatementMapperXmlTest {
}
}
@Test
void mapperXml_shouldContainCounterpartyIdentityColumns() throws Exception {
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
assertTrue(xml.contains("<result property=\"customerCertNo\" column=\"customer_cert_no\" />"), xml);
assertTrue(
xml.contains("<result property=\"customerSocialCreditCode\" column=\"customer_social_credit_code\" />"),
xml
);
assertTrue(xml.contains("customer_bank, customer_reference, customer_cert_no, customer_social_credit_code,"), xml);
assertTrue(xml.contains("#{item.customerBank}, #{item.customerReference}, #{item.customerCertNo}, #{item.customerSocialCreditCode},"), xml);
}
}
private MappedStatement loadMappedStatement(String statementId) throws Exception {
Configuration configuration = new Configuration();
configuration.setEnvironment(new Environment("test", new JdbcTransactionFactory(), new NoOpDataSource()));

View File

@@ -71,6 +71,19 @@ class CcdiProjectOverviewMapperSqlTest {
assertTrue(objectRowsSql.contains("tr.staff_id_card = #{staffIdCard}") || objectRowsSql.contains("#{staffIdCard}"), objectRowsSql);
}
@Test
void shouldExposeSuspiciousTransactionAggregationQuery() throws Exception {
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml"));
String suspiciousSql = extractSelect(xml, "selectSuspiciousTransactionPage");
assertTrue(suspiciousSql.contains("rule_name like '%可疑%'"), suspiciousSql);
assertTrue(suspiciousSql.contains("ccdi_biz_intermediary"), suspiciousSql);
assertTrue(suspiciousSql.contains("ccdi_enterprise_base_info"), suspiciousSql);
assertTrue(suspiciousSql.contains("group by merged.bankStatementId"), suspiciousSql);
assertTrue(suspiciousSql.contains("hasModelRuleHit"), suspiciousSql);
assertTrue(suspiciousSql.contains("hasNameListHit"), suspiciousSql);
}
private String extractSelect(String xml, String selectId) {
String start = "<select id=\"" + selectId + "\"";
int startIndex = xml.indexOf(start);

View File

@@ -17,6 +17,14 @@ class CcdiProjectOverviewServiceStructureTest {
"getPersonAnalysisDetail",
Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO")
));
assertNotNull(clazz.getMethod(
"getSuspiciousTransactions",
Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO")
));
assertNotNull(clazz.getMethod(
"exportSuspiciousTransactions",
Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO")
));
assertNotNull(clazz.getMethod("refreshOverviewEmployeeResults", Long.class, String.class));
assertNotNull(clazz.getMethod("refreshProjectRiskCounts", Long.class, String.class));
}

View File

@@ -0,0 +1,134 @@
package com.ruoyi.ccdi.project.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO;
import com.ruoyi.ccdi.project.mapper.CcdiBankTagResultMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewEmployeeResultMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewMapper;
import com.ruoyi.common.exception.ServiceException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CcdiProjectOverviewServiceSuspiciousTransactionTest {
@InjectMocks
private CcdiProjectOverviewServiceImpl service;
@Mock
private CcdiProjectOverviewMapper overviewMapper;
@Mock
private CcdiProjectMapper projectMapper;
@Mock
private CcdiProjectOverviewEmployeeResultMapper overviewEmployeeResultMapper;
@Mock
private CcdiBankTagResultMapper bankTagResultMapper;
@Mock
private CcdiProjectOverviewEmployeeResultBuilder overviewEmployeeResultBuilder;
@Test
void shouldReturnDeduplicatedSuspiciousTransactions() {
CcdiProject project = new CcdiProject();
project.setProjectId(40L);
when(projectMapper.selectById(40L)).thenReturn(project);
CcdiProjectSuspiciousTransactionItemVO item = new CcdiProjectSuspiciousTransactionItemVO();
item.setBankStatementId(101L);
item.setSuspiciousPersonName("王五");
item.setRelatedPersonName("孙七");
item.setRelatedStaffName("孙七");
item.setRelatedStaffCode("809901");
item.setRelationType("配偶");
item.setUserMemo("零钱商户消费");
item.setCashType("转账");
item.setDisplayAmount(new BigDecimal("200000.00"));
item.setHasModelRuleHit(true);
item.setHasNameListHit(true);
Page<CcdiProjectSuspiciousTransactionItemVO> page = new Page<>(1, 10);
page.setRecords(List.of(item));
page.setTotal(1);
when(overviewMapper.selectSuspiciousTransactionPage(any(Page.class), any(CcdiProjectSuspiciousTransactionQueryDTO.class)))
.thenReturn(page);
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
queryDTO.setProjectId(40L);
queryDTO.setSuspiciousType("name_list");
queryDTO.setPageNum(1);
queryDTO.setPageSize(10);
CcdiProjectSuspiciousTransactionPageVO result = service.getSuspiciousTransactions(queryDTO);
assertEquals(1, result.getRows().size());
assertEquals(1L, result.getTotal());
assertTrue(result.getRows().getFirst().getHasModelRuleHit());
assertTrue(result.getRows().getFirst().getHasNameListHit());
verify(overviewMapper).selectSuspiciousTransactionPage(
any(Page.class),
argThat(query -> "NAME_LIST".equals(query.getSuspiciousType()))
);
}
@Test
void shouldExportSuspiciousTransactionsWithCurrentFilter() {
CcdiProject project = new CcdiProject();
project.setProjectId(40L);
when(projectMapper.selectById(40L)).thenReturn(project);
CcdiProjectSuspiciousTransactionItemVO item = new CcdiProjectSuspiciousTransactionItemVO();
item.setTrxDate("2024-01-15 10:00:00");
item.setSuspiciousPersonName("孙七");
item.setRelatedPersonName("孙七");
item.setRelatedStaffName("孙七");
item.setRelatedStaffCode("809901");
item.setRelationType("本人");
item.setUserMemo("");
item.setCashType("转账");
item.setDisplayAmount(new BigDecimal("500000.00"));
when(overviewMapper.selectSuspiciousTransactionList(any(CcdiProjectSuspiciousTransactionQueryDTO.class)))
.thenReturn(List.of(item));
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
queryDTO.setProjectId(40L);
List<CcdiProjectSuspiciousTransactionExcel> rows = service.exportSuspiciousTransactions(queryDTO);
assertEquals(1, rows.size());
assertEquals("孙七(809901)", rows.getFirst().getRelatedStaffDisplay());
assertEquals("/转账", rows.getFirst().getSummaryAndCashType());
}
@Test
void shouldThrowWhenSuspiciousTransactionProjectDoesNotExist() {
when(projectMapper.selectById(99L)).thenReturn(null);
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
queryDTO.setProjectId(99L);
assertThrows(ServiceException.class, () -> service.getSuspiciousTransactions(queryDTO));
assertThrows(ServiceException.class, () -> service.exportSuspiciousTransactions(queryDTO));
}
}