diff --git a/ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/GetBankStatementResponse.java b/ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/GetBankStatementResponse.java index d08cd433..724e6f1b 100644 --- a/ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/GetBankStatementResponse.java +++ b/ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/GetBankStatementResponse.java @@ -110,6 +110,12 @@ public class GetBankStatementResponse { /** 对手方备注 */ private String customerReference; + /** 交易对手方证件号 */ + private String customerCertNo; + + /** 交易对手方统一社会信用代码 */ + private String customerSocialCreditCode; + // ===== 摘要和备注 ===== /** 用户交易摘要 */ diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java index fab5d955..95a0fe1d 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java @@ -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 rows = overviewService.exportSuspiciousTransactions(queryDTO); + ExcelUtil util = + new ExcelUtil<>(CcdiProjectSuspiciousTransactionExcel.class); + util.exportExcel(response, rows, "涉疑交易明细"); + } } diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiProjectSuspiciousTransactionQueryDTO.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiProjectSuspiciousTransactionQueryDTO.java new file mode 100644 index 00000000..2cc9e6e1 --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiProjectSuspiciousTransactionQueryDTO.java @@ -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; +} diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatement.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatement.java index e7e80797..466c80f3 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatement.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/entity/CcdiBankStatement.java @@ -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 diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/excel/CcdiProjectSuspiciousTransactionExcel.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/excel/CcdiProjectSuspiciousTransactionExcel.java new file mode 100644 index 00000000..8a2293a5 --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/excel/CcdiProjectSuspiciousTransactionExcel.java @@ -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; +} diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectSuspiciousTransactionItemVO.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectSuspiciousTransactionItemVO.java new file mode 100644 index 00000000..736afb79 --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectSuspiciousTransactionItemVO.java @@ -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; +} diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectSuspiciousTransactionPageVO.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectSuspiciousTransactionPageVO.java new file mode 100644 index 00000000..1870f49b --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectSuspiciousTransactionPageVO.java @@ -0,0 +1,16 @@ +package com.ruoyi.ccdi.project.domain.vo; + +import lombok.Data; + +import java.util.List; + +/** + * 涉疑交易分页结果 + */ +@Data +public class CcdiProjectSuspiciousTransactionPageVO { + + private List rows; + + private Long total; +} diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapper.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapper.java index cc9ec9ed..dd402041 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapper.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapper.java @@ -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 selectSuspiciousTransactionPage( + Page page, + @Param("query") CcdiProjectSuspiciousTransactionQueryDTO query + ); + + /** + * 查询涉疑交易导出列表 + * + * @param query 查询条件 + * @return 导出列表 + */ + List selectSuspiciousTransactionList( + @Param("query") CcdiProjectSuspiciousTransactionQueryDTO query + ); + /** * 按员工范围查询命中标签 * diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java index 77a9c9fd..d3d6a919 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java @@ -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 exportSuspiciousTransactions( + CcdiProjectSuspiciousTransactionQueryDTO queryDTO + ) { + return List.of(); + } + /** * 重算结果总览员工结果并同步项目风险人数 * diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java index b28c0401..14e235c4 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java @@ -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 page = new Page<>( + defaultPageNum(queryDTO.getPageNum()), + defaultPageSize(queryDTO.getPageSize()) + ); + Page 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 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 statementRows, List objectRows diff --git a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml index f1d8ff69..92df3289 100644 --- a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml +++ b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiBankStatementMapper.xml @@ -25,6 +25,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + @@ -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}, diff --git a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml index ca3fb08c..d127218a 100644 --- a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml +++ b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml @@ -33,6 +33,21 @@ select="selectRiskHitTagsByScope"/> + + + + + + + + + + + + + + + select 0 as digit union all select 1 @@ -338,6 +353,243 @@ order by result.staff_name asc, result.staff_id_card asc + + 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) + + + + 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 '%可疑%' + + + + 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 + + + + 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 ( + + ) base + inner join ( + + ) 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 ( + + ) base + inner join ( + + ) name_hits on name_hits.bankStatementId = base.bankStatementId + + + + 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 ( + + ) merged + group by merged.bankStatementId + + + + + + where final_result.hasNameListHit = 1 + + + where final_result.hasModelRuleHit = 1 + + + where final_result.hasModelRuleHit = 1 or final_result.hasNameListHit = 1 + + + + + + + + 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 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)); + } +} diff --git a/lsfx-mock-server/models/response.py b/lsfx-mock-server/models/response.py index 7b61525f..e9a7b655 100644 --- a/lsfx-mock-server/models/response.py +++ b/lsfx-mock-server/models/response.py @@ -138,6 +138,8 @@ class BankStatementItem(BaseModel): customerId: int = Field(-1, description="客户ID") customerName: str = Field(..., description="客户名称") customerReference: str = Field("", description="客户参考") + customerCertNo: str = Field("", description="客户证件号") + customerSocialCreditCode: str = Field("", description="客户统一社会信用代码") downPaymentFlag: int = Field(0, description="首付标志") drAmount: float = Field(0, description="借方金额") exceptionType: str = Field("", description="异常类型") diff --git a/lsfx-mock-server/services/statement_rule_samples.py b/lsfx-mock-server/services/statement_rule_samples.py index ebcfcc40..67242902 100644 --- a/lsfx-mock-server/services/statement_rule_samples.py +++ b/lsfx-mock-server/services/statement_rule_samples.py @@ -85,6 +85,8 @@ def _build_statement( customer_account_mask_no: str = "9558800000000001", bank_comments: str = "", customer_bank: str = "", + customer_cert_no: str = "", + customer_social_credit_code: str = "", ) -> Dict: trans_amount = round(dr_amount if dr_amount > 0 else cr_amount, 2) balance_amount = round(80000000 + cr_amount - dr_amount, 2) @@ -114,6 +116,8 @@ def _build_statement( "customerId": -1, "customerName": customer_name, "customerReference": "", + "customerCertNo": customer_cert_no, + "customerSocialCreditCode": customer_social_credit_code, "downPaymentFlag": 0, "drAmount": round(dr_amount, 2), "exceptionType": "", diff --git a/lsfx-mock-server/services/statement_service.py b/lsfx-mock-server/services/statement_service.py index a7a04991..8de23532 100644 --- a/lsfx-mock-server/services/statement_service.py +++ b/lsfx-mock-server/services/statement_service.py @@ -109,6 +109,8 @@ class StatementService: "customerId": -1, "customerName": customer_name, "customerReference": "", + "customerCertNo": rng.choice(allowed_identity_cards), + "customerSocialCreditCode": "", "downPaymentFlag": 0, "drAmount": dr_amount, "exceptionType": "", diff --git a/lsfx-mock-server/tests/test_statement_service.py b/lsfx-mock-server/tests/test_statement_service.py index b3c76899..4e9973e1 100644 --- a/lsfx-mock-server/tests/test_statement_service.py +++ b/lsfx-mock-server/tests/test_statement_service.py @@ -258,6 +258,25 @@ def test_generate_statements_should_stay_within_single_employee_scope_per_log_id assert {item["cretNo"] for item in statements}.issubset(allowed_id_cards) +def test_get_bank_statement_should_include_counterparty_identity_fields(): + service = StatementService() + + response = service.get_bank_statement( + { + "groupId": 1000, + "logId": 20001, + "pageNow": 1, + "pageSize": 5, + } + ) + + statements = response["data"]["bankStatementList"] + + assert statements + assert all("customerCertNo" in item for item in statements) + assert all("customerSocialCreditCode" in item for item in statements) + + def test_all_mode_monthly_fixed_income_log_should_keep_monthly_income_stable(monkeypatch): monkeypatch.setattr("services.file_service.settings.RULE_HIT_MODE", "all") file_service = FileService(staff_identity_repository=FakeStaffIdentityRepository()) diff --git a/ruoyi-ui/src/api/ccdi/projectOverview.js b/ruoyi-ui/src/api/ccdi/projectOverview.js index 08ffd8d4..0ca3ce91 100644 --- a/ruoyi-ui/src/api/ccdi/projectOverview.js +++ b/ruoyi-ui/src/api/ccdi/projectOverview.js @@ -50,3 +50,16 @@ export function getOverviewPersonAnalysisDetail(params) { } }) } + +export function getOverviewSuspiciousTransactions(params) { + return request({ + url: '/ccdi/project/overview/suspicious-transactions', + method: 'get', + params: { + projectId: params.projectId, + suspiciousType: params.suspiciousType, + pageNum: params.pageNum, + pageSize: params.pageSize + } + }) +} diff --git a/ruoyi-ui/src/views/ccdiProject/components/detail/preliminaryCheck.mock.js b/ruoyi-ui/src/views/ccdiProject/components/detail/preliminaryCheck.mock.js index 6733edab..9b0536d9 100644 --- a/ruoyi-ui/src/views/ccdiProject/components/detail/preliminaryCheck.mock.js +++ b/ruoyi-ui/src/views/ccdiProject/components/detail/preliminaryCheck.mock.js @@ -87,6 +87,37 @@ export const mockOverviewData = { ], }, riskDetails: { + projectId: 1, + suspiciousType: "ALL", + total: 2, + suspiciousTransactionList: [ + { + bankStatementId: 1, + trxDate: "2024-01-15 10:00:00", + suspiciousPersonName: "孙七", + relatedPersonName: "孙七", + relatedStaffName: "孙七", + relatedStaffCode: "809901", + relationType: "本人", + userMemo: "", + cashType: "转账", + displayAmount: 500000, + actionLabel: "查看详情", + }, + { + bankStatementId: 2, + trxDate: "2024-01-10 09:20:00", + suspiciousPersonName: "王五", + relatedPersonName: "孙七", + relatedStaffName: "孙七", + relatedStaffCode: "809901", + relationType: "配偶", + userMemo: "零钱商户消费", + cashType: "", + displayAmount: -200000, + actionLabel: "查看详情", + }, + ], transactionList: [ { tradeDate: "2024-01-15", @@ -351,7 +382,17 @@ function normalizeRiskModelCards(cardList) { })); } -export function createOverviewLoadedData({ projectId, dashboardData, riskPeopleData, riskModelCardsData } = {}) { +function normalizeSuspiciousTransactions(rows) { + if (!Array.isArray(rows)) { + return []; + } + return rows.map((item) => ({ + ...item, + actionLabel: item.actionLabel || "查看详情", + })); +} + +export function createOverviewLoadedData({ projectId, dashboardData, riskPeopleData, riskModelCardsData, suspiciousData } = {}) { return { ...mockOverviewData, summary: { @@ -373,6 +414,15 @@ export function createOverviewLoadedData({ projectId, dashboardData, riskPeopleD peopleList: [], total: 0, }, + riskDetails: { + ...mockOverviewData.riskDetails, + projectId, + suspiciousTransactionList: normalizeSuspiciousTransactions(suspiciousData && suspiciousData.rows), + suspiciousType: "ALL", + total: suspiciousData && suspiciousData.total ? suspiciousData.total : 0, + transactionList: normalizeSuspiciousTransactions(suspiciousData && suspiciousData.rows), + abnormalAccountList: [], + }, }; } @@ -395,6 +445,9 @@ export const mockOverviewStateData = { peopleList: [], }, riskDetails: { + suspiciousTransactionList: [], + suspiciousType: "ALL", + total: 0, transactionList: [], abnormalAccountList: [], }, diff --git a/ruoyi-ui/tests/unit/preliminary-check-model-and-detail.test.js b/ruoyi-ui/tests/unit/preliminary-check-model-and-detail.test.js index 22c009ec..9bc8cbb4 100644 --- a/ruoyi-ui/tests/unit/preliminary-check-model-and-detail.test.js +++ b/ruoyi-ui/tests/unit/preliminary-check-model-and-detail.test.js @@ -16,6 +16,13 @@ const detail = fs.readFileSync( ), "utf8" ); +const preliminaryCheck = fs.readFileSync( + path.resolve( + __dirname, + "../../src/views/ccdiProject/components/detail/PreliminaryCheck.vue" + ), + "utf8" +); ["风险模型", "命中模型涉及人员", "员工姓名或工号", "异常标签"].forEach((token) => assert(model.includes(token), token) @@ -26,6 +33,9 @@ const detail = fs.readFileSync( ["部门", "请选择部门", "查询", "重置", "selectedModelText"].forEach((token) => assert(model.includes(token), token) ); -["风险明细", "涉险交易明细", "异常账户人员信息", "查看详情"].forEach((token) => +["风险明细", "涉疑交易明细", "异常账户人员信息", "查看详情"].forEach((token) => assert(detail.includes(token), token) ); +["getOverviewSuspiciousTransactions", "riskDetails"].forEach((token) => + assert(preliminaryCheck.includes(token), token) +); diff --git a/ruoyi-ui/tests/unit/project-overview-api.test.js b/ruoyi-ui/tests/unit/project-overview-api.test.js index 52c0535c..ed124781 100644 --- a/ruoyi-ui/tests/unit/project-overview-api.test.js +++ b/ruoyi-ui/tests/unit/project-overview-api.test.js @@ -12,10 +12,12 @@ const source = fs.readFileSync( "getOverviewRiskPeople", "getOverviewRiskModelCards", "getOverviewRiskModelPeople", + "getOverviewSuspiciousTransactions", "/ccdi/project/overview/dashboard", "/ccdi/project/overview/risk-people", "/ccdi/project/overview/risk-models/cards", "/ccdi/project/overview/risk-models/people", + "/ccdi/project/overview/suspicious-transactions", ].forEach((token) => assert(source.includes(token), token)); [ @@ -39,6 +41,19 @@ assert(riskModelPeopleFn, "应保留模型人员接口参数透传逻辑"); "pageSize: params.pageSize", ].forEach((token) => assert(riskModelPeopleFn[0].includes(token), token)); +const suspiciousTransactionsFn = source.match( + /export function getOverviewSuspiciousTransactions\(params\) \{[\s\S]*?params:\s*\{([\s\S]*?)\}\s*\}\s*\)/m +); + +assert(suspiciousTransactionsFn, "应新增涉疑交易接口参数透传逻辑"); + +[ + "projectId: params.projectId", + "suspiciousType: params.suspiciousType", + "pageNum: params.pageNum", + "pageSize: params.pageSize", +].forEach((token) => assert(suspiciousTransactionsFn[0].includes(token), token)); + ["employeeResult", "resultTable", "overview/result"].forEach((token) => assert(!source.includes(token), `前端 API 契约不应感知结果表实现:${token}`) ); diff --git a/sql/migration/2026-03-27-ccdi-bank-statement-counterparty-identity-columns.sql b/sql/migration/2026-03-27-ccdi-bank-statement-counterparty-identity-columns.sql new file mode 100644 index 00000000..fc0e6f27 --- /dev/null +++ b/sql/migration/2026-03-27-ccdi-bank-statement-counterparty-identity-columns.sql @@ -0,0 +1,3 @@ +ALTER TABLE `ccdi_bank_statement` + ADD COLUMN `customer_cert_no` varchar(50) NULL COMMENT '交易对手方证件号' AFTER `customer_reference`, + ADD COLUMN `customer_social_credit_code` varchar(50) NULL COMMENT '交易对手方统一社会信用代码' AFTER `customer_cert_no`;