新增结果总览未提交账户展示

This commit is contained in:
wjj
2026-07-20 11:50:03 +08:00
parent 1561df924f
commit dd424cca50
23 changed files with 1051 additions and 5 deletions

View File

@@ -9,10 +9,12 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskExclusionSaveDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectUnsubmittedAccountQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectExternalPersonWarningExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskModelPeopleExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskPeopleOverviewExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectUnsubmittedAccountFlowExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountPageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativePageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExternalRiskSummaryVO;
@@ -24,6 +26,7 @@ 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.domain.vo.CcdiProjectUnsubmittedAccountVO;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService;
import com.ruoyi.common.utils.poi.ExcelUtil;
@@ -226,6 +229,18 @@ public class CcdiProjectOverviewController extends BaseController {
return AjaxResult.success(pageVO);
}
/**
* 查询未提交账户
*/
@GetMapping("/unsubmitted-accounts")
@Operation(summary = "查询未提交账户")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getUnsubmittedAccounts(CcdiProjectUnsubmittedAccountQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectUnsubmittedAccountVO result = overviewService.getUnsubmittedAccounts(queryDTO);
return AjaxResult.success(result);
}
/**
* 导出涉疑交易明细
*/
@@ -302,6 +317,20 @@ public class CcdiProjectOverviewController extends BaseController {
util.exportExcel(response, rows, "外部人员风险模型命中人员");
}
/**
* 导出未提交账户流水
*/
@PostMapping("/unsubmitted-accounts/export")
@Operation(summary = "导出未提交账户流水")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public void exportUnsubmittedAccountFlows(HttpServletResponse response, Long projectId) {
projectAccessService.assertCanRead(projectId);
List<CcdiProjectUnsubmittedAccountFlowExcel> rows = overviewService.exportUnsubmittedAccountFlows(projectId);
ExcelUtil<CcdiProjectUnsubmittedAccountFlowExcel> util =
new ExcelUtil<>(CcdiProjectUnsubmittedAccountFlowExcel.class);
util.exportExcel(response, rows, "未提交账户流水");
}
/**
* 导出风险明细
*/

View File

@@ -0,0 +1,15 @@
package com.ruoyi.ccdi.project.domain.dto;
import lombok.Data;
/**
* 未提交账户查询参数
*/
@Data
public class CcdiProjectUnsubmittedAccountQueryDTO {
/**
* 项目ID
*/
private Long projectId;
}

View File

@@ -0,0 +1,45 @@
package com.ruoyi.ccdi.project.domain.excel;
import com.ruoyi.common.annotation.Excel;
import java.math.BigDecimal;
import lombok.Data;
/**
* 未提交账户流水导出对象
*/
@Data
public class CcdiProjectUnsubmittedAccountFlowExcel {
@Excel(name = "主体名称")
private String subjectName;
@Excel(name = "未提交账号")
private String accountNo;
@Excel(name = "交易时间")
private String trxDate;
@Excel(name = "本方账号")
private String leAccountNo;
@Excel(name = "本方主体")
private String leAccountName;
@Excel(name = "对方名称")
private String customerAccountName;
@Excel(name = "对方账号")
private String customerAccountNo;
@Excel(name = "摘要")
private String userMemo;
@Excel(name = "交易类型")
private String cashType;
@Excel(name = "流入金额")
private BigDecimal amountCr;
@Excel(name = "流出金额")
private BigDecimal amountDr;
}

View File

@@ -4,6 +4,7 @@ import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectExternalPersonWarningExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectUnsubmittedAccountFlowExcel;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
@@ -37,4 +38,6 @@ public class CcdiProjectOverviewReportVO {
private List<CcdiProjectEmployeeCreditNegativeExcel> illegalPeople = new ArrayList<>();
private List<CcdiProjectAbnormalAccountExcel> abnormalAccounts = new ArrayList<>();
private List<CcdiProjectUnsubmittedAccountFlowExcel> unsubmittedAccountFlows = new ArrayList<>();
}

View File

@@ -0,0 +1,33 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.math.BigDecimal;
import lombok.Data;
/**
* 未提交账户流水行
*/
@Data
public class CcdiProjectUnsubmittedAccountFlowVO {
private String subjectName;
private String accountNo;
private String trxDate;
private String leAccountNo;
private String leAccountName;
private String customerAccountName;
private String customerAccountNo;
private String userMemo;
private String cashType;
private BigDecimal amountCr;
private BigDecimal amountDr;
}

View File

@@ -0,0 +1,33 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.math.BigDecimal;
import lombok.Data;
/**
* 未提交账户账号项
*/
@Data
public class CcdiProjectUnsubmittedAccountItemVO {
private String subjectName;
private String accountNo;
private BigDecimal totalAmountCr;
private BigDecimal totalAmountDr;
private BigDecimal netAmount;
private BigDecimal inflowRatio;
private BigDecimal outflowRatio;
private Boolean highRatio;
private Integer transactionCount;
private String startDate;
private String endDate;
}

View File

@@ -0,0 +1,25 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
/**
* 未提交账户主体分组
*/
@Data
public class CcdiProjectUnsubmittedAccountSubjectVO {
private String subjectName;
private Integer accountCount = 0;
private BigDecimal totalAmountCr = BigDecimal.ZERO;
private BigDecimal totalAmountDr = BigDecimal.ZERO;
private BigDecimal netAmount = BigDecimal.ZERO;
private List<CcdiProjectUnsubmittedAccountItemVO> accounts = new ArrayList<>();
}

View File

@@ -0,0 +1,23 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
/**
* 未提交账户查询结果
*/
@Data
public class CcdiProjectUnsubmittedAccountVO {
private Integer accountCount = 0;
private BigDecimal totalAmountCr = BigDecimal.ZERO;
private BigDecimal totalAmountDr = BigDecimal.ZERO;
private BigDecimal netAmount = BigDecimal.ZERO;
private List<CcdiProjectUnsubmittedAccountSubjectVO> subjects = new ArrayList<>();
}

View File

@@ -9,6 +9,7 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExternalRiskModelPeopleQuery
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectUnsubmittedAccountQueryDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativeItemVO;
@@ -24,6 +25,8 @@ 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 com.ruoyi.ccdi.project.domain.vo.CcdiProjectUnsubmittedAccountFlowVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectUnsubmittedAccountItemVO;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Mapper;
@@ -259,6 +262,24 @@ public interface CcdiProjectOverviewMapper {
*/
List<CcdiProjectAbnormalAccountItemVO> selectAbnormalAccountList(@Param("projectId") Long projectId);
/**
* 查询未提交账户列表
*
* @param query 查询条件
* @return 未提交账户列表
*/
List<CcdiProjectUnsubmittedAccountItemVO> selectUnsubmittedAccountList(
@Param("query") CcdiProjectUnsubmittedAccountQueryDTO query
);
/**
* 查询未提交账户流水
*
* @param projectId 项目ID
* @return 未提交账户流水
*/
List<CcdiProjectUnsubmittedAccountFlowVO> selectUnsubmittedAccountFlowList(@Param("projectId") Long projectId);
/**
* 查询项目员工负面征信导出列表
*

View File

@@ -9,12 +9,14 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskExclusionSaveDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectUnsubmittedAccountQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectExternalPersonWarningExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskModelPeopleExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskPeopleOverviewExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectUnsubmittedAccountFlowExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountPageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativePageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExternalRiskSummaryVO;
@@ -26,6 +28,7 @@ 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.domain.vo.CcdiProjectUnsubmittedAccountVO;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
@@ -278,6 +281,28 @@ public interface ICcdiProjectOverviewService {
return List.of();
}
/**
* 查询未提交账户
*
* @param queryDTO 查询条件
* @return 未提交账户结果
*/
default CcdiProjectUnsubmittedAccountVO getUnsubmittedAccounts(
CcdiProjectUnsubmittedAccountQueryDTO queryDTO
) {
return new CcdiProjectUnsubmittedAccountVO();
}
/**
* 导出未提交账户流水
*
* @param projectId 项目ID
* @return 导出列表
*/
default List<CcdiProjectUnsubmittedAccountFlowExcel> exportUnsubmittedAccountFlows(Long projectId) {
return List.of();
}
/**
* 重算结果总览员工结果并同步项目风险人数
*

View File

@@ -246,6 +246,25 @@ public class CcdiProjectOverviewReportPdfExporter {
new float[] { 0.12F, 0.1F, 0.14F, 0.14F, 0.11F, 0.08F, 0.22F, 0.09F },
"暂无异常账户信息"
);
writer.subsection("4. 未提交账户流水表(共" + report.getUnsubmittedAccountFlows().size() + "条)");
writer.table(
List.of("主体", "未提交账号", "交易时间", "本方账号", "对方名称", "摘要/交易类型", "流入", "流出"),
report.getUnsubmittedAccountFlows().stream()
.map(item -> List.of(
safeText(item.getSubjectName()),
maskAccount(item.getAccountNo()),
safeText(item.getTrxDate()),
maskAccount(item.getLeAccountNo()),
safeText(item.getCustomerAccountName()),
formatSummaryAndCashType(item.getUserMemo(), item.getCashType()),
formatMoney(item.getAmountCr()),
formatMoney(item.getAmountDr())
))
.collect(Collectors.toList()),
new float[] { 0.14F, 0.13F, 0.12F, 0.13F, 0.14F, 0.17F, 0.08F, 0.09F },
"暂无未提交账户流水"
);
}
private LoadedChineseFont loadChineseFont(PDDocument document) throws IOException {

View File

@@ -12,12 +12,14 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskExclusionSaveDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectUnsubmittedAccountQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectExternalPersonWarningExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskModelPeopleExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskPeopleOverviewExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectUnsubmittedAccountFlowExcel;
import com.ruoyi.ccdi.project.domain.entity.CcdiProjectOverviewEmployeeResult;
import com.ruoyi.ccdi.project.domain.entity.CcdiProjectRiskExclusion;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountItemVO;
@@ -53,6 +55,10 @@ 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.domain.vo.CcdiProjectUnsubmittedAccountFlowVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectUnsubmittedAccountItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectUnsubmittedAccountSubjectVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectUnsubmittedAccountVO;
import com.ruoyi.ccdi.project.mapper.CcdiModelParamMapper;
import com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
@@ -67,6 +73,7 @@ import com.ruoyi.common.utils.SecurityUtils;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.annotation.Resource;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
@@ -474,6 +481,39 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
.toList();
}
@Override
public CcdiProjectUnsubmittedAccountVO getUnsubmittedAccounts(CcdiProjectUnsubmittedAccountQueryDTO queryDTO) {
ensureProjectExists(queryDTO.getProjectId());
List<CcdiProjectUnsubmittedAccountItemVO> rows =
defaultList(overviewMapper.selectUnsubmittedAccountList(queryDTO));
CcdiProjectUnsubmittedAccountVO result = new CcdiProjectUnsubmittedAccountVO();
result.setAccountCount(rows.size());
result.setTotalAmountCr(sumUnsubmittedAmount(rows, true));
result.setTotalAmountDr(sumUnsubmittedAmount(rows, false));
result.setNetAmount(result.getTotalAmountCr().subtract(result.getTotalAmountDr()));
Map<String, List<CcdiProjectUnsubmittedAccountItemVO>> groupedRows = rows.stream()
.collect(Collectors.groupingBy(
item -> item.getSubjectName() == null ? "" : item.getSubjectName(),
LinkedHashMap::new,
Collectors.toList()
));
result.setSubjects(groupedRows.entrySet().stream()
.map(entry -> buildUnsubmittedAccountSubject(entry.getKey(), entry.getValue()))
.toList());
return result;
}
@Override
public List<CcdiProjectUnsubmittedAccountFlowExcel> exportUnsubmittedAccountFlows(Long projectId) {
ensureProjectExists(projectId);
return defaultList(overviewMapper.selectUnsubmittedAccountFlowList(projectId)).stream()
.map(this::buildUnsubmittedAccountFlowExcelRow)
.toList();
}
@Override
public void exportRiskDetails(HttpServletResponse response, Long projectId) {
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
@@ -491,8 +531,9 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
Long projectId = queryDTO.getProjectId();
List<CcdiProjectEmployeeCreditNegativeExcel> creditRows = exportEmployeeCreditNegative(projectId);
List<CcdiProjectAbnormalAccountExcel> abnormalRows = exportAbnormalAccountPeople(projectId);
List<CcdiProjectUnsubmittedAccountFlowExcel> unsubmittedRows = exportUnsubmittedAccountFlows(projectId);
try {
workbookExporter.export(response, projectId, suspiciousRows, creditRows, abnormalRows);
workbookExporter.export(response, projectId, suspiciousRows, creditRows, abnormalRows, unsubmittedRows);
} catch (IOException e) {
throw new ServiceException("导出风险明细失败");
}
@@ -525,6 +566,7 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
));
report.setIllegalPeople(exportEmployeeCreditNegative(projectId));
report.setAbnormalAccounts(exportAbnormalAccountPeople(projectId));
report.setUnsubmittedAccountFlows(exportUnsubmittedAccountFlows(projectId));
try {
reportPdfExporter.export(response, report);
@@ -910,6 +952,45 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
return row;
}
private CcdiProjectUnsubmittedAccountSubjectVO buildUnsubmittedAccountSubject(
String subjectName,
List<CcdiProjectUnsubmittedAccountItemVO> rows
) {
CcdiProjectUnsubmittedAccountSubjectVO subject = new CcdiProjectUnsubmittedAccountSubjectVO();
subject.setSubjectName(subjectName);
subject.setAccountCount(rows.size());
subject.setTotalAmountCr(sumUnsubmittedAmount(rows, true));
subject.setTotalAmountDr(sumUnsubmittedAmount(rows, false));
subject.setNetAmount(subject.getTotalAmountCr().subtract(subject.getTotalAmountDr()));
subject.setAccounts(rows);
return subject;
}
private BigDecimal sumUnsubmittedAmount(List<CcdiProjectUnsubmittedAccountItemVO> rows, boolean inflow) {
return rows.stream()
.map(item -> inflow ? item.getTotalAmountCr() : item.getTotalAmountDr())
.filter(value -> value != null)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
private CcdiProjectUnsubmittedAccountFlowExcel buildUnsubmittedAccountFlowExcelRow(
CcdiProjectUnsubmittedAccountFlowVO item
) {
CcdiProjectUnsubmittedAccountFlowExcel row = new CcdiProjectUnsubmittedAccountFlowExcel();
row.setSubjectName(item.getSubjectName());
row.setAccountNo(item.getAccountNo());
row.setTrxDate(item.getTrxDate());
row.setLeAccountNo(item.getLeAccountNo());
row.setLeAccountName(item.getLeAccountName());
row.setCustomerAccountName(item.getCustomerAccountName());
row.setCustomerAccountNo(item.getCustomerAccountNo());
row.setUserMemo(item.getUserMemo());
row.setCashType(item.getCashType());
row.setAmountCr(item.getAmountCr());
row.setAmountDr(item.getAmountDr());
return row;
}
private List<CcdiProjectOverviewReportParamVO> buildReportParams(CcdiProject project) {
Long effectiveProjectId = "default".equals(project.getConfigType()) ? 0L : project.getProjectId();
return defaultList(modelParamMapper.selectByProjectId(effectiveProjectId)).stream()

View File

@@ -3,6 +3,7 @@ package com.ruoyi.ccdi.project.service.impl;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectUnsubmittedAccountFlowExcel;
import com.ruoyi.common.utils.file.FileUtils;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.Row;
@@ -29,7 +30,8 @@ public class CcdiProjectRiskDetailWorkbookExporter {
Long projectId,
List<CcdiProjectSuspiciousTransactionExcel> suspiciousRows,
List<CcdiProjectEmployeeCreditNegativeExcel> creditRows,
List<CcdiProjectAbnormalAccountExcel> abnormalRows
List<CcdiProjectAbnormalAccountExcel> abnormalRows,
List<CcdiProjectUnsubmittedAccountFlowExcel> unsubmittedRows
) throws IOException {
response.setContentType(CONTENT_TYPE);
FileUtils.setAttachmentResponseHeader(response, "风险明细_" + projectId + ".xlsx");
@@ -38,6 +40,7 @@ public class CcdiProjectRiskDetailWorkbookExporter {
writeSuspiciousSheet(workbook.createSheet("涉疑交易明细"), suspiciousRows);
writeCreditSheet(workbook.createSheet("员工负面征信信息"), creditRows);
writeAbnormalAccountSheet(workbook.createSheet("异常账户人员信息"), abnormalRows);
writeUnsubmittedAccountSheet(workbook.createSheet("未提交账户流水"), unsubmittedRows);
workbook.write(response.getOutputStream());
}
}
@@ -123,6 +126,40 @@ public class CcdiProjectRiskDetailWorkbookExporter {
}
}
private void writeUnsubmittedAccountSheet(Sheet sheet, List<CcdiProjectUnsubmittedAccountFlowExcel> rows) {
Row header = sheet.createRow(0);
String[] headers = {
"主体名称",
"未提交账号",
"交易时间",
"本方账号",
"本方主体",
"对方名称",
"对方账号",
"摘要",
"交易类型",
"流入金额",
"流出金额"
};
writeHeader(header, headers);
for (int i = 0; i < rows.size(); i++) {
CcdiProjectUnsubmittedAccountFlowExcel item = rows.get(i);
Row row = sheet.createRow(i + 1);
row.createCell(0).setCellValue(safeText(item.getSubjectName()));
row.createCell(1).setCellValue(safeText(item.getAccountNo()));
row.createCell(2).setCellValue(safeText(item.getTrxDate()));
row.createCell(3).setCellValue(safeText(item.getLeAccountNo()));
row.createCell(4).setCellValue(safeText(item.getLeAccountName()));
row.createCell(5).setCellValue(safeText(item.getCustomerAccountName()));
row.createCell(6).setCellValue(safeText(item.getCustomerAccountNo()));
row.createCell(7).setCellValue(safeText(item.getUserMemo()));
row.createCell(8).setCellValue(safeText(item.getCashType()));
row.createCell(9).setCellValue(safeNumber(item.getAmountCr()));
row.createCell(10).setCellValue(safeNumber(item.getAmountDr()));
}
}
private void writeHeader(Row row, String[] headers) {
for (int i = 0; i < headers.length; i++) {
row.createCell(i).setCellValue(headers[i]);

View File

@@ -96,6 +96,34 @@
<result property="involvedAmount" column="involved_amount"/>
</resultMap>
<resultMap id="UnsubmittedAccountItemResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectUnsubmittedAccountItemVO">
<result property="subjectName" column="subjectName"/>
<result property="accountNo" column="accountNo"/>
<result property="totalAmountCr" column="totalAmountCr"/>
<result property="totalAmountDr" column="totalAmountDr"/>
<result property="netAmount" column="netAmount"/>
<result property="inflowRatio" column="inflowRatio"/>
<result property="outflowRatio" column="outflowRatio"/>
<result property="highRatio" column="highRatio"/>
<result property="transactionCount" column="transactionCount"/>
<result property="startDate" column="startDate"/>
<result property="endDate" column="endDate"/>
</resultMap>
<resultMap id="UnsubmittedAccountFlowResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectUnsubmittedAccountFlowVO">
<result property="subjectName" column="subjectName"/>
<result property="accountNo" column="accountNo"/>
<result property="trxDate" column="trxDate"/>
<result property="leAccountNo" column="leAccountNo"/>
<result property="leAccountName" column="leAccountName"/>
<result property="customerAccountName" column="customerAccountName"/>
<result property="customerAccountNo" column="customerAccountNo"/>
<result property="userMemo" column="userMemo"/>
<result property="cashType" column="cashType"/>
<result property="amountCr" column="amountCr"/>
<result property="amountDr" column="amountDr"/>
</resultMap>
<resultMap id="ReportUploadSubjectResultMap"
type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportUploadSubjectVO">
<result property="subjectName" column="subjectName"/>
@@ -1545,6 +1573,185 @@
order by abnormal.abnormal_time desc, abnormal.account_no asc, abnormal.rule_code asc
</select>
<sql id="unsubmittedAccountLoanConditionSql">
(
bs.USER_MEMO like '%还贷%'
or bs.USER_MEMO like '%还贷款%'
or bs.USER_MEMO like '%贷款还款%'
or bs.USER_MEMO like '%归还贷款%'
or bs.USER_MEMO like '%贷款归还%'
or bs.USER_MEMO like '%还本%'
or bs.USER_MEMO like '%还息%'
or bs.USER_MEMO like '%付息%'
or bs.USER_MEMO like '%贷息%'
or bs.USER_MEMO like '%贷款利息%'
or bs.USER_MEMO like '%贷款发放%'
or bs.USER_MEMO like '%普贷放款%'
or bs.USER_MEMO like '%放款%'
or bs.USER_MEMO like '%放贷%'
or bs.USER_MEMO like '%银团贷%'
or bs.USER_MEMO like '%流贷%'
or bs.USER_MEMO like '%续贷%资金%'
or bs.CASH_TYPE like '%贷款本息%'
or bs.CASH_TYPE like '%贷款扣款%'
or bs.CASH_TYPE like '%贷款回收%'
or bs.CASH_TYPE like '%还贷款%'
or bs.CASH_TYPE like '%归还本金%'
or bs.CASH_TYPE like '%归还本息%'
or bs.CASH_TYPE like '%归还利息%'
or bs.CASH_TYPE like '%归还逾期利息%'
or bs.CASH_TYPE like '%贷息%'
or bs.CASH_TYPE like '%贷款放%'
or bs.CASH_TYPE like '%放贷%'
or bs.CASH_TYPE like '%信贷系统业务对公资产%'
or bs.CUSTOMER_ACCOUNT_NAME like '%个贷系统平账专户%'
or bs.CUSTOMER_ACCOUNT_NAME like '%企业贷%'
or bs.CUSTOMER_ACCOUNT_NAME like '%微粒贷%'
or bs.CUSTOMER_ACCOUNT_NAME like '%贷款还款户%'
or bs.CUSTOMER_ACCOUNT_NAME like '%贷款归还户%'
or bs.CUSTOMER_ACCOUNT_NAME like '%融资利息收入%'
or bs.CUSTOMER_ACCOUNT_NAME like '%短期贷款%'
or bs.CUSTOMER_ACCOUNT_NAME like '%科创贷%'
or bs.CUSTOMER_ACCOUNT_NAME like '%企金贷%'
or bs.CUSTOMER_ACCOUNT_NAME like '%网贷通%'
or bs.CUSTOMER_ACCOUNT_NAME like '%联合贷%'
or bs.CUSTOMER_ACCOUNT_NAME like '%数字信用凭据融资%'
or bs.CUSTOMER_ACCOUNT_NAME like '%消费金融%'
)
</sql>
<sql id="unsubmittedAccountCandidateRowSql">
select
bs.bank_statement_id as bankStatementId,
trim(bs.LE_ACCOUNT_NAME) as subjectName,
replace(replace(replace(trim(bs.CUSTOMER_ACCOUNT_NO), '-', ''), char(9), ''), ' ', '') as accountNo,
bs.TRX_DATE as trxDate,
bs.LE_ACCOUNT_NO as leAccountNo,
bs.LE_ACCOUNT_NAME as leAccountName,
bs.CUSTOMER_ACCOUNT_NAME as customerAccountName,
bs.CUSTOMER_ACCOUNT_NO as customerAccountNo,
bs.USER_MEMO as userMemo,
bs.CASH_TYPE as cashType,
ifnull(bs.AMOUNT_CR, 0) as amountCr,
ifnull(bs.AMOUNT_DR, 0) as amountDr,
case
when <include refid="unsubmittedAccountLoanConditionSql"/> then 1
else 0
end as loanFlag
from ccdi_bank_statement bs
where bs.project_id = #{unsubmittedProjectId}
and bs.LE_ACCOUNT_NAME is not null
and trim(bs.LE_ACCOUNT_NAME) != ''
and bs.CUSTOMER_ACCOUNT_NAME is not null
and trim(bs.CUSTOMER_ACCOUNT_NAME) != ''
and replace(replace(trim(bs.CUSTOMER_ACCOUNT_NAME), '', '('), '', ')')
= replace(replace(trim(bs.LE_ACCOUNT_NAME), '', '('), '', ')')
and bs.CUSTOMER_ACCOUNT_NO is not null
and replace(replace(replace(trim(bs.CUSTOMER_ACCOUNT_NO), '-', ''), char(9), ''), ' ', '') != ''
and replace(replace(replace(trim(bs.CUSTOMER_ACCOUNT_NO), '-', ''), char(9), ''), ' ', '')
!= replace(replace(replace(trim(ifnull(bs.LE_ACCOUNT_NO, '')), '-', ''), char(9), ''), ' ', '')
and not exists (
select 1
from ccdi_bank_statement submitted
where submitted.project_id = bs.project_id
and submitted.LE_ACCOUNT_NO is not null
and replace(replace(replace(trim(submitted.LE_ACCOUNT_NO), '-', ''), char(9), ''), ' ', '')
= replace(replace(replace(trim(bs.CUSTOMER_ACCOUNT_NO), '-', ''), char(9), ''), ' ', '')
)
</sql>
<sql id="unsubmittedAccountAggregateSql">
select
candidate.subjectName,
candidate.accountNo,
sum(candidate.amountCr) as totalAmountCr,
sum(candidate.amountDr) as totalAmountDr,
sum(candidate.amountCr) - sum(candidate.amountDr) as netAmount,
count(*) as transactionCount,
min(candidate.trxDate) as startDate,
max(candidate.trxDate) as endDate
from (
<include refid="unsubmittedAccountCandidateRowSql"/>
) candidate
group by candidate.subjectName, candidate.accountNo
having sum(candidate.loanFlag) &lt; count(*)
</sql>
<sql id="unsubmittedAccountSubjectTotalSql">
select
trim(bs.LE_ACCOUNT_NAME) as subjectName,
sum(ifnull(bs.AMOUNT_CR, 0)) as subjectAmountCr,
sum(ifnull(bs.AMOUNT_DR, 0)) as subjectAmountDr
from ccdi_bank_statement bs
where bs.project_id = #{unsubmittedProjectId}
and bs.LE_ACCOUNT_NAME is not null
and trim(bs.LE_ACCOUNT_NAME) != ''
group by trim(bs.LE_ACCOUNT_NAME)
</sql>
<select id="selectUnsubmittedAccountList" resultMap="UnsubmittedAccountItemResultMap">
<bind name="unsubmittedProjectId" value="query.projectId"/>
select
agg.subjectName,
agg.accountNo,
agg.totalAmountCr,
agg.totalAmountDr,
agg.netAmount,
case
when ifnull(subject_total.subjectAmountCr, 0) &gt; 0
then round(agg.totalAmountCr / subject_total.subjectAmountCr, 4)
else 0
end as inflowRatio,
case
when ifnull(subject_total.subjectAmountDr, 0) &gt; 0
then round(agg.totalAmountDr / subject_total.subjectAmountDr, 4)
else 0
end as outflowRatio,
case
when ifnull(subject_total.subjectAmountCr, 0) &gt; 0
and agg.totalAmountCr / subject_total.subjectAmountCr &gt;= 0.1
then 1
when ifnull(subject_total.subjectAmountDr, 0) &gt; 0
and agg.totalAmountDr / subject_total.subjectAmountDr &gt;= 0.1
then 1
else 0
end as highRatio,
agg.transactionCount,
agg.startDate,
agg.endDate
from (
<include refid="unsubmittedAccountAggregateSql"/>
) agg
left join (
<include refid="unsubmittedAccountSubjectTotalSql"/>
) subject_total on subject_total.subjectName = agg.subjectName
order by agg.totalAmountCr + agg.totalAmountDr desc, agg.subjectName asc, agg.accountNo asc
</select>
<select id="selectUnsubmittedAccountFlowList" resultMap="UnsubmittedAccountFlowResultMap">
<bind name="unsubmittedProjectId" value="projectId"/>
select
candidate.subjectName,
candidate.accountNo,
candidate.trxDate,
candidate.leAccountNo,
candidate.leAccountName,
candidate.customerAccountName,
candidate.customerAccountNo,
candidate.userMemo,
candidate.cashType,
candidate.amountCr,
candidate.amountDr
from (
<include refid="unsubmittedAccountCandidateRowSql"/>
) candidate
inner join (
<include refid="unsubmittedAccountAggregateSql"/>
) agg on agg.subjectName = candidate.subjectName
and agg.accountNo = candidate.accountNo
order by candidate.subjectName asc, candidate.accountNo asc, candidate.trxDate asc, candidate.bankStatementId asc
</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

@@ -238,6 +238,44 @@ class CcdiProjectOverviewMapperSqlTest {
assertTrue(xml.contains("as involved_amount"), xml);
}
@Test
void shouldExposeUnsubmittedAccountQueriesWithLoanExclusion() throws Exception {
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml"));
String accountSql = extractSelect(xml, "selectUnsubmittedAccountList");
String flowSql = extractSelect(xml, "selectUnsubmittedAccountFlowList");
String candidateSql = extractSqlFragment(xml, "unsubmittedAccountCandidateRowSql");
String loanSql = extractSqlFragment(xml, "unsubmittedAccountLoanConditionSql");
String aggregateSql = extractSqlFragment(xml, "unsubmittedAccountAggregateSql");
assertTrue(accountSql.contains("unsubmittedAccountAggregateSql"), accountSql);
assertTrue(accountSql.contains("unsubmittedAccountSubjectTotalSql"), accountSql);
assertTrue(flowSql.contains("unsubmittedAccountCandidateRowSql"), flowSql);
assertTrue(flowSql.contains("unsubmittedAccountAggregateSql"), flowSql);
assertTrue(candidateSql.contains("bs.project_id = #{unsubmittedProjectId}"), candidateSql);
assertTrue(candidateSql.contains("bs.CUSTOMER_ACCOUNT_NAME"), candidateSql);
assertTrue(candidateSql.contains("bs.LE_ACCOUNT_NAME"), candidateSql);
assertTrue(candidateSql.contains("not exists"), candidateSql);
assertTrue(candidateSql.contains("submitted.LE_ACCOUNT_NO"), candidateSql);
assertFalse(candidateSql.contains("TRX_TYPE"), candidateSql);
assertTrue(loanSql.contains("还贷"), loanSql);
assertTrue(loanSql.contains("贷款还款"), loanSql);
assertTrue(loanSql.contains("归还贷款"), loanSql);
assertTrue(loanSql.contains("贷款发放"), loanSql);
assertTrue(loanSql.contains("普贷放款"), loanSql);
assertTrue(loanSql.contains("信贷系统业务对公资产"), loanSql);
assertTrue(loanSql.contains("个贷系统平账专户"), loanSql);
assertTrue(loanSql.contains("企业贷"), loanSql);
assertTrue(loanSql.contains("消费金融"), loanSql);
assertFalse(loanSql.contains("民间借贷"), loanSql);
assertFalse(loanSql.contains("资金拆借"), loanSql);
assertFalse(loanSql.contains("调拨"), loanSql);
assertFalse(loanSql.contains("划拨"), loanSql);
assertFalse(loanSql.contains("TRX_TYPE"), loanSql);
assertTrue(aggregateSql.contains("having sum(candidate.loanFlag) &lt; count(*)"), aggregateSql);
}
private String extractSelect(String xml, String selectId) {
String start = "<select id=\"" + selectId + "\"";
int startIndex = xml.indexOf(start);

View File

@@ -10,6 +10,7 @@ import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskPeopleOverviewExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectUnsubmittedAccountFlowExcel;
import com.ruoyi.ccdi.project.domain.entity.CcdiProjectOverviewEmployeeResult;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativeItemVO;
@@ -322,6 +323,9 @@ class CcdiProjectOverviewServiceImplTest {
&& "6222000000000001".equals(rows.getFirst().getAccountNo())
&& "账户6222000000000001销户前存在异常交易".equals(rows.getFirst().getReasonDetail())
&& new BigDecimal("120000.00").equals(rows.getFirst().getInvolvedAmount())
),
argThat((List<CcdiProjectUnsubmittedAccountFlowExcel> rows) ->
rows != null && rows.isEmpty()
)
);
}

View File

@@ -3,6 +3,7 @@ package com.ruoyi.ccdi.project.service.impl;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectUnsubmittedAccountFlowExcel;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletResponse;
@@ -17,7 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiProjectRiskDetailWorkbookExporterTest {
@Test
void shouldExportWorkbookWithThreeOrderedSheets() throws Exception {
void shouldExportWorkbookWithFourOrderedSheets() throws Exception {
CcdiProjectRiskDetailWorkbookExporter exporter = new CcdiProjectRiskDetailWorkbookExporter();
MockHttpServletResponse response = new MockHttpServletResponse();
@@ -50,17 +51,38 @@ class CcdiProjectRiskDetailWorkbookExporterTest {
abnormalRow.setReasonDetail("账户6222000000000001销户前存在异常交易");
abnormalRow.setInvolvedAmount(new BigDecimal("120000.00"));
exporter.export(response, 40L, List.of(suspiciousRow), List.of(creditRow), List.of(abnormalRow));
CcdiProjectUnsubmittedAccountFlowExcel unsubmittedRow = new CcdiProjectUnsubmittedAccountFlowExcel();
unsubmittedRow.setSubjectName("测试主体");
unsubmittedRow.setAccountNo("330001000000000001");
unsubmittedRow.setTrxDate("2026-03-21");
unsubmittedRow.setLeAccountNo("330001000000000002");
unsubmittedRow.setLeAccountName("测试主体");
unsubmittedRow.setCustomerAccountName("测试主体");
unsubmittedRow.setCustomerAccountNo("330001000000000001");
unsubmittedRow.setUserMemo("往来款");
unsubmittedRow.setCashType("转账");
unsubmittedRow.setAmountCr(new BigDecimal("30000.00"));
unsubmittedRow.setAmountDr(BigDecimal.ZERO);
exporter.export(
response,
40L,
List.of(suspiciousRow),
List.of(creditRow),
List.of(abnormalRow),
List.of(unsubmittedRow)
);
assertTrue(response.getContentType().startsWith(
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
));
try (var workbook = WorkbookFactory.create(new ByteArrayInputStream(response.getContentAsByteArray()))) {
assertEquals(3, workbook.getNumberOfSheets());
assertEquals(4, workbook.getNumberOfSheets());
assertEquals("涉疑交易明细", workbook.getSheetAt(0).getSheetName());
assertEquals("员工负面征信信息", workbook.getSheetAt(1).getSheetName());
assertEquals("异常账户人员信息", workbook.getSheetAt(2).getSheetName());
assertEquals("未提交账户流水", workbook.getSheetAt(3).getSheetName());
assertEquals("交易时间", workbook.getSheetAt(0).getRow(0).getCell(0).getStringCellValue());
assertEquals("本方账户", workbook.getSheetAt(0).getRow(0).getCell(1).getStringCellValue());
assertEquals("本方主体", workbook.getSheetAt(0).getRow(0).getCell(2).getStringCellValue());
@@ -91,6 +113,13 @@ class CcdiProjectRiskDetailWorkbookExporterTest {
assertEquals("账户6222000000000001销户前存在异常交易", workbook.getSheetAt(2).getRow(1).getCell(6).getStringCellValue());
assertEquals(120000D, workbook.getSheetAt(2).getRow(1).getCell(7).getNumericCellValue());
assertEquals(2, workbook.getSheetAt(2).getPhysicalNumberOfRows());
assertEquals("主体名称", workbook.getSheetAt(3).getRow(0).getCell(0).getStringCellValue());
assertEquals("未提交账号", workbook.getSheetAt(3).getRow(0).getCell(1).getStringCellValue());
assertEquals("交易时间", workbook.getSheetAt(3).getRow(0).getCell(2).getStringCellValue());
assertEquals("流入金额", workbook.getSheetAt(3).getRow(0).getCell(9).getStringCellValue());
assertEquals("测试主体", workbook.getSheetAt(3).getRow(1).getCell(0).getStringCellValue());
assertEquals("330001000000000001", workbook.getSheetAt(3).getRow(1).getCell(1).getStringCellValue());
assertEquals(30000D, workbook.getSheetAt(3).getRow(1).getCell(9).getNumericCellValue());
}
}
}

View File

@@ -0,0 +1,27 @@
# 未提交账户后端实施计划
## 目标
在结果总览风险明细中新增“未提交账户”能力,参考 lsfx 未提交账户口径识别本项目已上传流水中疑似未提交分析的同名账户,并按已确认范围排除银行贷款/信贷类账号。
## 实施范围
- 新增未提交账户查询接口,返回汇总、主体分组和账号卡片数据。
- 新增未提交账户导出接口,导出未提交账户流水。
- 风险明细统一导出增加“未提交账户流水” Sheet。
- 结果总览报告增加未提交账户明细。
- 不新增数据库表,不新增菜单与权限。
## 关键口径
1. 候选账号来自 `ccdi_bank_statement` 当前项目流水。
2. 对手方户名等于本方主体名,对手方账号非空,且清洗后不等于本方账号。
3. 对手方账号不在本项目已提交账号集合中,已提交账号集合来自上传记录保存的 `account_nos`
4. 贷款排除只使用银行贷款/信贷高确定性关键词,不使用 `TRX_TYPE`
5. 只有候选账号关联流水全部命中贷款/信贷排除条件时,才排除该账号。
## 验证
- Mapper XML 断言覆盖未提交账户 SQL、贷款排除关键词与“不使用 TRX_TYPE”。
- 风险明细工作簿测试覆盖新增 Sheet。
- 后端编译或相关测试通过后记录实施结果。

View File

@@ -0,0 +1,26 @@
# 未提交账户前端实施计划
## 目标
在结果总览风险明细页面新增“未提交账户”展示区,形态参考 lsfx 截图:顶部汇总、主体分组、账号卡片和导出流水入口。
## 实施范围
- `projectOverview.js` 增加未提交账户查询 API。
- `RiskDetailSection.vue` 增加未提交账户区块。
- 展示未提交账户数、流入总额、流出总额、流入/流出差额。
- 按主体展示账号卡片,卡片显示账号、流入金额、流出金额和交易占比较高标记。
- 导出按钮调用后端未提交账户流水导出接口。
## 交互规则
- 当前项目为空时显示空态。
- 查询失败时展示错误提示并清空未提交账户数据。
- 账号显示使用中间省略,金额按万元展示。
- 交易占比较高按后端返回的标记展示。
## 验证
- 前端单元静态断言覆盖标题、汇总字段、导出入口和卡片字段。
- 按项目要求使用可用 Node/npm 版本完成构建或测试。
- 页面开发完成后使用浏览器实际验证。

View File

@@ -0,0 +1,20 @@
# 未提交账户实施记录
## 修改内容
- 新增结果总览“未提交账户”后端查询与导出能力。
- 新增前端风险明细未提交账户卡片展示。
- 风险明细统一导出与结果总览报告接入未提交账户流水。
- 贷款排除仅使用银行贷款/信贷高确定性关键词,未启用内部杂项账号排除。
## 影响范围
- 后端:`ccdi-project` 结果总览 Controller、Service、Mapper、VO、Excel 与导出器。
- 前端:`ruoyi-ui` 结果总览风险明细组件与 API。
- 文档:后端计划、前端计划、实施记录。
## 验证情况
- 后端执行 `mvn -pl ccdi-project -am test "-Dtest=CcdiProjectOverviewMapperSqlTest,CcdiProjectRiskDetailWorkbookExporterTest,CcdiProjectOverviewReportPdfExporterTest,CcdiProjectOverviewServiceImplTest" "-Dsurefire.failIfNoSpecifiedTests=false"`32 个测试通过。
- 前端使用 Node 22.22.3 / npm 10.9.8 执行 `node tests\unit\risk-detail-abnormal-account-layout.test.js`,静态断言通过。
- 前端使用 Node 22.22.3 / npm 10.9.8 执行 `npm run build:prod`,构建通过;仍存在项目既有资源体积告警。

View File

@@ -144,6 +144,14 @@ export function getOverviewAbnormalAccountPeople(params) {
})
}
export function getOverviewUnsubmittedAccounts(projectId) {
return request({
url: '/ccdi/project/overview/unsubmitted-accounts',
method: 'get',
params: { projectId }
})
}
export function exportOverviewReport(projectId) {
return request({
url: '/ccdi/project/overview/report/export',

View File

@@ -11,6 +11,93 @@
</el-button>
</div>
<div class="block">
<div class="block-header">
<div>
<div class="block-title">未提交账户</div>
<div class="block-subtitle">被调查主体未提交分析的同名账户已排除银行贷款类账号</div>
</div>
<el-button
size="mini"
type="text"
icon="el-icon-download"
:disabled="!projectId || unsubmittedAccountTotal <= 0"
@click="handleUnsubmittedAccountExport"
>
导出流水
</el-button>
</div>
<div v-loading="unsubmittedAccountLoading" class="unsubmitted-panel">
<template v-if="unsubmittedAccountTotal > 0">
<div class="unsubmitted-metrics">
<div class="unsubmitted-metric">
<div class="metric-label">未提交账户数</div>
<div class="metric-value">{{ unsubmittedAccountTotal }}</div>
</div>
<div class="unsubmitted-metric">
<div class="metric-label">流入总额</div>
<div class="metric-value amount-in">{{ formatWanAmount(unsubmittedAccountSummary.totalAmountCr) }}</div>
</div>
<div class="unsubmitted-metric">
<div class="metric-label">流出总额</div>
<div class="metric-value amount-out">-{{ formatWanAmount(unsubmittedAccountSummary.totalAmountDr) }}</div>
</div>
<div class="unsubmitted-metric">
<div class="metric-label">流入/流出差额</div>
<div
class="metric-value"
:class="Number(unsubmittedAccountSummary.netAmount || 0) >= 0 ? 'amount-in' : 'amount-out'"
>
{{ formatWanAmount(unsubmittedAccountSummary.netAmount) }}
</div>
</div>
</div>
<div
v-for="subject in unsubmittedAccountSubjects"
:key="subject.subjectName"
class="unsubmitted-subject"
>
<div class="unsubmitted-subject-name">{{ formatField(subject.subjectName) }}</div>
<div class="unsubmitted-account-grid">
<div
v-for="account in subject.accounts"
:key="`${subject.subjectName}-${account.accountNo}`"
class="unsubmitted-account-card"
>
<div class="unsubmitted-account-head">
<div class="unsubmitted-account-no">
<i class="el-icon-bank-card"></i>
<span>{{ maskAccountNo(account.accountNo) }}</span>
</div>
<el-tag
v-if="account.highRatio"
size="mini"
type="danger"
effect="plain"
>
交易占比较高
</el-tag>
</div>
<div class="unsubmitted-card-metrics">
<div>
<div class="card-metric-label">流入金额</div>
<div class="card-metric-value amount-in">{{ formatWanAmount(account.totalAmountCr) }}</div>
</div>
<div>
<div class="card-metric-label">流出金额</div>
<div class="card-metric-value amount-out">-{{ formatWanAmount(account.totalAmountDr) }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<el-empty v-else :image-size="96" description="当前项目暂无未提交账户" />
</div>
</div>
<div class="block">
<div class="block-header">
<div>
@@ -369,6 +456,7 @@ import {
getOverviewAbnormalAccountPeople,
getOverviewEmployeeCreditNegative,
getOverviewSuspiciousTransactions,
getOverviewUnsubmittedAccounts,
} from "@/api/ccdi/projectOverview";
import { getBankStatementDetail } from "@/api/ccdiProjectBankStatement";
import { buildFlowEvidenceFingerprint, buildFlowEvidenceSnapshot } from "@/utils/ccdiEvidence";
@@ -559,6 +647,14 @@ export default {
abnormalAccountPageSize: DEFAULT_RISK_DETAIL_PAGE_SIZE,
abnormalAccountTotal: 0,
abnormalAccountList: [],
unsubmittedAccountLoading: false,
unsubmittedAccountSummary: {
totalAmountCr: 0,
totalAmountDr: 0,
netAmount: 0,
},
unsubmittedAccountTotal: 0,
unsubmittedAccountSubjects: [],
projectId: null,
statementDetailCache: {},
suspiciousRequestSeq: 0,
@@ -603,6 +699,8 @@ export default {
this.abnormalAccountPageSize = DEFAULT_RISK_DETAIL_PAGE_SIZE;
this.abnormalAccountTotal = 0;
this.abnormalAccountList = [];
this.unsubmittedAccountTotal = 0;
this.unsubmittedAccountSubjects = [];
this.employeeCreditNegativeList = Array.isArray(value && value.employeeCreditNegativeList)
? value.employeeCreditNegativeList
: [];
@@ -622,6 +720,7 @@ export default {
this.closeDetailDialog();
}
this.loadAbnormalAccountPeople();
this.loadUnsubmittedAccounts();
},
},
},
@@ -791,6 +890,44 @@ export default {
this.abnormalAccountLoading = false;
}
},
async loadUnsubmittedAccounts() {
if (!this.projectId) {
this.unsubmittedAccountSummary = {
totalAmountCr: 0,
totalAmountDr: 0,
netAmount: 0,
};
this.unsubmittedAccountTotal = 0;
this.unsubmittedAccountSubjects = [];
this.unsubmittedAccountLoading = false;
return;
}
this.unsubmittedAccountLoading = true;
try {
const response = await getOverviewUnsubmittedAccounts(this.projectId);
const data = (response && response.data) || {};
this.unsubmittedAccountSummary = {
totalAmountCr: Number(data.totalAmountCr) || 0,
totalAmountDr: Number(data.totalAmountDr) || 0,
netAmount: Number(data.netAmount) || 0,
};
this.unsubmittedAccountTotal = Number(data.accountCount) || 0;
this.unsubmittedAccountSubjects = Array.isArray(data.subjects) ? data.subjects : [];
} catch (error) {
this.unsubmittedAccountSummary = {
totalAmountCr: 0,
totalAmountDr: 0,
netAmount: 0,
};
this.unsubmittedAccountTotal = 0;
this.unsubmittedAccountSubjects = [];
this.$message.error("加载未提交账户失败");
console.error("加载未提交账户失败", error);
} finally {
this.unsubmittedAccountLoading = false;
}
},
async fetchStatementDetail(bankStatementId, silent, scope = {}) {
if (!bankStatementId) {
return createEmptyDetailData();
@@ -899,6 +1036,16 @@ export default {
`风险明细_${this.projectId}_${new Date().getTime()}.xlsx`
);
},
handleUnsubmittedAccountExport() {
if (!this.projectId || this.unsubmittedAccountTotal <= 0) {
return;
}
this.download(
"ccdi/project/overview/unsubmitted-accounts/export",
{ projectId: this.projectId },
`未提交账户流水_${this.projectId}_${new Date().getTime()}.xlsx`
);
},
formatRelatedStaff(row) {
if (!row || !row.relatedStaffName) {
return "-";
@@ -943,6 +1090,29 @@ export default {
maximumFractionDigits: 2,
});
},
formatWanAmount(value) {
if (value === null || value === undefined || value === "") {
return "0.00 万元";
}
const amount = Number(value);
if (Number.isNaN(amount)) {
return "0.00 万元";
}
return `${(amount / 10000).toLocaleString("zh-CN", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})} 万元`;
},
maskAccountNo(value) {
if (value === null || value === undefined || value === "") {
return "-";
}
const text = String(value);
if (text.length <= 10) {
return text;
}
return `${text.slice(0, 8)} ... ${text.slice(-6)}`;
},
formatSignedAmount(value) {
if (value === null || value === undefined || value === "") {
return "-";
@@ -1121,6 +1291,112 @@ export default {
width: 240px;
}
.unsubmitted-panel {
min-height: 120px;
}
.unsubmitted-metrics {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 18px;
margin-bottom: 24px;
}
.unsubmitted-metric {
min-width: 0;
}
.metric-label {
color: #64748b;
font-size: 13px;
line-height: 20px;
}
.metric-value {
margin-top: 6px;
color: #0f172a;
font-size: 24px;
line-height: 32px;
font-weight: 700;
overflow-wrap: anywhere;
}
.unsubmitted-subject + .unsubmitted-subject {
margin-top: 22px;
}
.unsubmitted-subject-name {
margin-bottom: 12px;
color: #111827;
font-size: 15px;
line-height: 22px;
font-weight: 700;
}
.unsubmitted-account-grid {
display: grid;
grid-template-columns: repeat(4, minmax(220px, 1fr));
gap: 16px;
}
.unsubmitted-account-card {
min-width: 0;
min-height: 132px;
padding: 18px 20px;
border: 1px solid #e5e7eb;
background: #fff;
}
.unsubmitted-account-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
min-height: 28px;
}
.unsubmitted-account-no {
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 0;
color: #111827;
font-size: 14px;
line-height: 22px;
font-weight: 600;
}
.unsubmitted-account-no i {
flex: 0 0 auto;
color: #9ca3af;
}
.unsubmitted-account-no span {
min-width: 0;
overflow-wrap: anywhere;
}
.unsubmitted-card-metrics {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
margin-top: 24px;
}
.card-metric-label {
color: #64748b;
font-size: 13px;
line-height: 20px;
}
.card-metric-value {
margin-top: 4px;
font-size: 16px;
line-height: 24px;
font-weight: 700;
overflow-wrap: anywhere;
}
.result-table {
width: 100%;
}
@@ -1355,6 +1631,14 @@ export default {
}
@media (max-width: 992px) {
.unsubmitted-metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.unsubmitted-account-grid {
grid-template-columns: repeat(2, minmax(220px, 1fr));
}
.detail-overview-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 20px 24px;
@@ -1362,6 +1646,12 @@ export default {
}
@media (max-width: 768px) {
.unsubmitted-metrics,
.unsubmitted-account-grid,
.unsubmitted-card-metrics {
grid-template-columns: 1fr;
}
.suspicious-filter-form {
align-items: stretch;
}

View File

@@ -11,6 +11,14 @@ const source = fs.readFileSync(
);
[
"未提交账户",
"未提交账户数",
"流入总额",
"流出总额",
"流入/流出差额",
"交易占比较高",
"导出流水",
"当前项目暂无未提交账户",
"异常账户人员信息",
"账号",
"开户人",