新增结果总览一键导出报告
This commit is contained in:
@@ -49,6 +49,12 @@
|
|||||||
<artifactId>easyexcel</artifactId>
|
<artifactId>easyexcel</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- pdf导出工具 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.pdfbox</groupId>
|
||||||
|
<artifactId>pdfbox</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- 测试依赖 -->
|
<!-- 测试依赖 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
|||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -181,4 +182,14 @@ public class CcdiProjectOverviewController extends BaseController {
|
|||||||
public void exportRiskDetails(HttpServletResponse response, Long projectId) {
|
public void exportRiskDetails(HttpServletResponse response, Long projectId) {
|
||||||
overviewService.exportRiskDetails(response, projectId);
|
overviewService.exportRiskDetails(response, projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一键导出结果总览报告
|
||||||
|
*/
|
||||||
|
@RequestMapping(value = "/report/export", method = { RequestMethod.GET, RequestMethod.POST })
|
||||||
|
@Operation(summary = "一键导出结果总览报告")
|
||||||
|
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
|
||||||
|
public void exportOverviewReport(HttpServletResponse response, Long projectId) {
|
||||||
|
overviewService.exportOverviewReport(response, projectId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.ruoyi.ccdi.project.domain.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结果总览报告风险模型汇总
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class CcdiProjectOverviewReportModelSummaryVO {
|
||||||
|
|
||||||
|
private String modelCode;
|
||||||
|
|
||||||
|
private String modelName;
|
||||||
|
|
||||||
|
private Integer warningCount;
|
||||||
|
|
||||||
|
private Integer peopleCount;
|
||||||
|
|
||||||
|
private String peopleNames;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.ruoyi.ccdi.project.domain.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结果总览报告参数配置项
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class CcdiProjectOverviewReportParamVO {
|
||||||
|
|
||||||
|
private String modelName;
|
||||||
|
|
||||||
|
private String paramName;
|
||||||
|
|
||||||
|
private String paramValue;
|
||||||
|
|
||||||
|
private String paramUnit;
|
||||||
|
|
||||||
|
private String paramDesc;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.ruoyi.ccdi.project.domain.vo;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结果总览报告涉疑交易明细
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class CcdiProjectOverviewReportSuspiciousTransactionVO {
|
||||||
|
|
||||||
|
private Long bankStatementId;
|
||||||
|
|
||||||
|
private String trxDate;
|
||||||
|
|
||||||
|
private String leAccountNo;
|
||||||
|
|
||||||
|
private String leAccountName;
|
||||||
|
|
||||||
|
private String customerAccountName;
|
||||||
|
|
||||||
|
private String customerAccountNo;
|
||||||
|
|
||||||
|
private String relatedStaffName;
|
||||||
|
|
||||||
|
private String relatedStaffCode;
|
||||||
|
|
||||||
|
private String userMemo;
|
||||||
|
|
||||||
|
private String cashType;
|
||||||
|
|
||||||
|
private String hitTags;
|
||||||
|
|
||||||
|
private BigDecimal displayAmount;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.ruoyi.ccdi.project.domain.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结果总览报告上传主体汇总
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class CcdiProjectOverviewReportUploadSubjectVO {
|
||||||
|
|
||||||
|
private String subjectName;
|
||||||
|
|
||||||
|
private String accountNos;
|
||||||
|
|
||||||
|
private String minTrxDate;
|
||||||
|
|
||||||
|
private String maxTrxDate;
|
||||||
|
|
||||||
|
private Integer fileCount;
|
||||||
|
|
||||||
|
private String dataPeriod;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.ruoyi.ccdi.project.domain.vo;
|
||||||
|
|
||||||
|
import com.ruoyi.ccdi.project.domain.CcdiProject;
|
||||||
|
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel;
|
||||||
|
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结果总览一键导出报告
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class CcdiProjectOverviewReportVO {
|
||||||
|
|
||||||
|
private CcdiProject project;
|
||||||
|
|
||||||
|
private List<CcdiProjectOverviewReportUploadSubjectVO> uploadSubjects = new ArrayList<>();
|
||||||
|
|
||||||
|
private List<CcdiProjectOverviewReportParamVO> params = new ArrayList<>();
|
||||||
|
|
||||||
|
private CcdiProjectOverviewDashboardVO dashboard = new CcdiProjectOverviewDashboardVO();
|
||||||
|
|
||||||
|
private List<CcdiProjectOverviewReportModelSummaryVO> modelSummaries = new ArrayList<>();
|
||||||
|
|
||||||
|
private List<CcdiProjectRiskModelPeopleItemVO> riskPeople = new ArrayList<>();
|
||||||
|
|
||||||
|
private List<CcdiProjectOverviewReportSuspiciousTransactionVO> suspiciousTransactions = new ArrayList<>();
|
||||||
|
|
||||||
|
private List<CcdiProjectEmployeeCreditNegativeExcel> illegalPeople = new ArrayList<>();
|
||||||
|
|
||||||
|
private List<CcdiProjectAbnormalAccountExcel> abnormalAccounts = new ArrayList<>();
|
||||||
|
}
|
||||||
@@ -13,6 +13,9 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativeItemVO;
|
|||||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO;
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO;
|
||||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisBasicInfoVO;
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisBasicInfoVO;
|
||||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisObjectRecordVO;
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisObjectRecordVO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportModelSummaryVO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportSuspiciousTransactionVO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportUploadSubjectVO;
|
||||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO;
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO;
|
||||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardVO;
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardVO;
|
||||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO;
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO;
|
||||||
@@ -72,6 +75,40 @@ public interface CcdiProjectOverviewMapper {
|
|||||||
*/
|
*/
|
||||||
List<CcdiProjectRiskModelCardVO> selectRiskModelCardsByProjectId(@Param("projectId") Long projectId);
|
List<CcdiProjectRiskModelCardVO> selectRiskModelCardsByProjectId(@Param("projectId") Long projectId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询报告上传主体汇总
|
||||||
|
*
|
||||||
|
* @param projectId 项目ID
|
||||||
|
* @return 上传主体汇总
|
||||||
|
*/
|
||||||
|
List<CcdiProjectOverviewReportUploadSubjectVO> selectReportUploadSubjects(@Param("projectId") Long projectId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询报告风险模型汇总
|
||||||
|
*
|
||||||
|
* @param projectId 项目ID
|
||||||
|
* @return 风险模型汇总
|
||||||
|
*/
|
||||||
|
List<CcdiProjectOverviewReportModelSummaryVO> selectReportRiskModelSummaries(@Param("projectId") Long projectId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询报告风险人员与异常点
|
||||||
|
*
|
||||||
|
* @param projectId 项目ID
|
||||||
|
* @return 风险人员与异常点
|
||||||
|
*/
|
||||||
|
List<CcdiProjectRiskModelPeopleItemVO> selectReportRiskPeople(@Param("projectId") Long projectId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询报告涉疑交易明细
|
||||||
|
*
|
||||||
|
* @param query 查询条件
|
||||||
|
* @return 涉疑交易明细
|
||||||
|
*/
|
||||||
|
List<CcdiProjectOverviewReportSuspiciousTransactionVO> selectReportSuspiciousTransactionList(
|
||||||
|
@Param("query") CcdiProjectSuspiciousTransactionQueryDTO query
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询风险模型命中人员
|
* 分页查询风险模型命中人员
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -125,6 +125,15 @@ public interface ICcdiProjectOverviewService {
|
|||||||
default void exportRiskDetails(HttpServletResponse response, Long projectId) {
|
default void exportRiskDetails(HttpServletResponse response, Long projectId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一键导出结果总览报告
|
||||||
|
*
|
||||||
|
* @param response 响应流
|
||||||
|
* @param projectId 项目ID
|
||||||
|
*/
|
||||||
|
default void exportOverviewReport(HttpServletResponse response, Long projectId) {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出项目员工负面征信
|
* 导出项目员工负面征信
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,603 @@
|
|||||||
|
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.vo.CcdiProjectOverviewReportModelSummaryVO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportParamVO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportSuspiciousTransactionVO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportUploadSubjectVO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportVO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewStatVO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO;
|
||||||
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
|
import com.ruoyi.common.utils.file.FileUtils;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.text.DecimalFormat;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.apache.fontbox.ttf.TrueTypeCollection;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.font.PDType0Font;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结果总览PDF报告导出器
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class CcdiProjectOverviewReportPdfExporter {
|
||||||
|
|
||||||
|
private static final String CONTENT_TYPE = "application/pdf";
|
||||||
|
private static final DateTimeFormatter EXPORT_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||||
|
private static final DecimalFormat MONEY_FORMAT = new DecimalFormat("#,##0.00");
|
||||||
|
|
||||||
|
public void export(HttpServletResponse response, CcdiProjectOverviewReportVO report) throws IOException {
|
||||||
|
response.setContentType(CONTENT_TYPE);
|
||||||
|
FileUtils.setAttachmentResponseHeader(
|
||||||
|
response,
|
||||||
|
safeFileName(report.getProject().getProjectName()) + "_初核结果报告.pdf"
|
||||||
|
);
|
||||||
|
|
||||||
|
try (PDDocument document = new PDDocument()) {
|
||||||
|
PDType0Font font = loadChineseFont(document);
|
||||||
|
PdfPageWriter writer = new PdfPageWriter(document, font);
|
||||||
|
writer.newPage();
|
||||||
|
writeCover(writer, report);
|
||||||
|
writeUploadSubjects(writer, report.getUploadSubjects());
|
||||||
|
writeParams(writer, report.getParams());
|
||||||
|
writeRiskModels(writer, report);
|
||||||
|
writeRiskDetails(writer, report);
|
||||||
|
writer.close();
|
||||||
|
document.save(response.getOutputStream());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeCover(PdfPageWriter writer, CcdiProjectOverviewReportVO report) throws IOException {
|
||||||
|
writer.title("初核结果报告");
|
||||||
|
writer.text("项目名称:" + safeText(report.getProject().getProjectName()), 12, Color.GRAY);
|
||||||
|
writer.text("导出时间:" + LocalDateTime.now().format(EXPORT_TIME_FORMATTER), 12, Color.GRAY);
|
||||||
|
writer.separator();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeUploadSubjects(
|
||||||
|
PdfPageWriter writer,
|
||||||
|
List<CcdiProjectOverviewReportUploadSubjectVO> rows
|
||||||
|
) throws IOException {
|
||||||
|
writer.section("一、上传文件");
|
||||||
|
writer.table(
|
||||||
|
List.of("序号", "主体名称", "主体账号", "数据周期", "文件数"),
|
||||||
|
indexedRows(rows).stream()
|
||||||
|
.map(item -> List.of(
|
||||||
|
item.index(),
|
||||||
|
safeText(item.row().getSubjectName()),
|
||||||
|
maskAccountList(item.row().getAccountNos()),
|
||||||
|
safeText(item.row().getDataPeriod()),
|
||||||
|
formatCount(item.row().getFileCount(), "个")
|
||||||
|
))
|
||||||
|
.collect(Collectors.toList()),
|
||||||
|
new float[] { 0.07F, 0.2F, 0.45F, 0.14F, 0.14F },
|
||||||
|
"暂无上传文件数据"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeParams(PdfPageWriter writer, List<CcdiProjectOverviewReportParamVO> rows) throws IOException {
|
||||||
|
writer.section("二、参数配置");
|
||||||
|
writer.table(
|
||||||
|
List.of("模型名称", "监测项", "参数值", "单位", "描述"),
|
||||||
|
rows.stream()
|
||||||
|
.map(item -> List.of(
|
||||||
|
safeText(item.getModelName()),
|
||||||
|
safeText(item.getParamName()),
|
||||||
|
safeText(item.getParamValue()),
|
||||||
|
safeText(item.getParamUnit()),
|
||||||
|
safeText(item.getParamDesc())
|
||||||
|
))
|
||||||
|
.collect(Collectors.toList()),
|
||||||
|
new float[] { 0.18F, 0.26F, 0.14F, 0.12F, 0.3F },
|
||||||
|
"暂无参数配置数据"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeRiskModels(PdfPageWriter writer, CcdiProjectOverviewReportVO report) throws IOException {
|
||||||
|
writer.section("三、风险模型");
|
||||||
|
writer.metrics(report.getDashboard().getStats());
|
||||||
|
writer.subsection("风险模型汇总");
|
||||||
|
writer.table(
|
||||||
|
List.of("模型名称", "预警数量", "涉及人员"),
|
||||||
|
report.getModelSummaries().stream()
|
||||||
|
.map(item -> List.of(
|
||||||
|
safeText(item.getModelName()),
|
||||||
|
String.valueOf(defaultZero(item.getWarningCount())),
|
||||||
|
formatPeopleSummary(item)
|
||||||
|
))
|
||||||
|
.collect(Collectors.toList()),
|
||||||
|
new float[] { 0.26F, 0.14F, 0.6F },
|
||||||
|
"暂无风险模型汇总数据"
|
||||||
|
);
|
||||||
|
|
||||||
|
writer.subsection("风险人员与异常点");
|
||||||
|
writer.table(
|
||||||
|
List.of("姓名", "工号", "身份证号", "所属部门", "命中模型", "异常标签"),
|
||||||
|
report.getRiskPeople().stream()
|
||||||
|
.map(item -> List.of(
|
||||||
|
safeText(item.getStaffName()),
|
||||||
|
safeText(item.getStaffCode()),
|
||||||
|
maskIdCard(item.getIdNo()),
|
||||||
|
safeText(item.getDepartment()),
|
||||||
|
joinText(item.getModelNames()),
|
||||||
|
formatHitTags(item.getHitTagList())
|
||||||
|
))
|
||||||
|
.collect(Collectors.toList()),
|
||||||
|
new float[] { 0.1F, 0.11F, 0.16F, 0.14F, 0.24F, 0.25F },
|
||||||
|
"暂无风险人员与异常点数据"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeRiskDetails(PdfPageWriter writer, CcdiProjectOverviewReportVO report) throws IOException {
|
||||||
|
writer.section("四、风险明细");
|
||||||
|
writer.subsection("1. 涉疑交易明细表(共" + report.getSuspiciousTransactions().size() + "条)");
|
||||||
|
writer.table(
|
||||||
|
List.of("交易时间", "本方账户", "对方账户", "关联员工", "摘要/交易类型", "异常标签", "交易金额"),
|
||||||
|
report.getSuspiciousTransactions().stream()
|
||||||
|
.map(item -> List.of(
|
||||||
|
safeText(item.getTrxDate()),
|
||||||
|
formatAccount(item.getLeAccountNo(), item.getLeAccountName()),
|
||||||
|
formatAccount(item.getCustomerAccountNo(), item.getCustomerAccountName()),
|
||||||
|
formatRelatedStaff(item.getRelatedStaffName(), item.getRelatedStaffCode()),
|
||||||
|
formatSummaryAndCashType(item.getUserMemo(), item.getCashType()),
|
||||||
|
safeText(item.getHitTags()),
|
||||||
|
formatMoney(item.getDisplayAmount())
|
||||||
|
))
|
||||||
|
.collect(Collectors.toList()),
|
||||||
|
new float[] { 0.14F, 0.16F, 0.16F, 0.12F, 0.17F, 0.16F, 0.09F },
|
||||||
|
"暂无涉疑交易明细"
|
||||||
|
);
|
||||||
|
|
||||||
|
writer.subsection("2. 违法信息人员表(共" + report.getIllegalPeople().size() + "人)");
|
||||||
|
writer.table(
|
||||||
|
List.of("姓名", "身份证号", "最近查询日期", "民事案件笔数", "民事案件金额", "强制执行笔数", "强制执行金额", "行政处罚笔数", "行政处罚金额"),
|
||||||
|
report.getIllegalPeople().stream()
|
||||||
|
.map(item -> List.of(
|
||||||
|
safeText(item.getPersonName()),
|
||||||
|
maskIdCard(item.getPersonId()),
|
||||||
|
safeText(item.getQueryDate()),
|
||||||
|
String.valueOf(defaultZero(item.getCivilCnt())),
|
||||||
|
formatMoney(item.getCivilLmt()),
|
||||||
|
String.valueOf(defaultZero(item.getEnforceCnt())),
|
||||||
|
formatMoney(item.getEnforceLmt()),
|
||||||
|
String.valueOf(defaultZero(item.getAdmCnt())),
|
||||||
|
formatMoney(item.getAdmLmt())
|
||||||
|
))
|
||||||
|
.collect(Collectors.toList()),
|
||||||
|
new float[] { 0.09F, 0.15F, 0.12F, 0.1F, 0.11F, 0.1F, 0.11F, 0.1F, 0.12F },
|
||||||
|
"暂无违法信息人员数据"
|
||||||
|
);
|
||||||
|
|
||||||
|
writer.subsection("3. 异常账户信息表(共" + report.getAbnormalAccounts().size() + "条)");
|
||||||
|
writer.table(
|
||||||
|
List.of("账号", "开户人", "银行", "异常类型", "异常发生时间", "状态"),
|
||||||
|
report.getAbnormalAccounts().stream()
|
||||||
|
.map(item -> List.of(
|
||||||
|
maskAccount(item.getAccountNo()),
|
||||||
|
safeText(item.getAccountName()),
|
||||||
|
safeText(item.getBankName()),
|
||||||
|
safeText(item.getAbnormalType()),
|
||||||
|
safeText(item.getAbnormalTime()),
|
||||||
|
safeText(item.getStatus())
|
||||||
|
))
|
||||||
|
.collect(Collectors.toList()),
|
||||||
|
new float[] { 0.18F, 0.13F, 0.2F, 0.23F, 0.14F, 0.12F },
|
||||||
|
"暂无异常账户信息"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PDType0Font loadChineseFont(PDDocument document) throws IOException {
|
||||||
|
List<String> candidates = List.of(
|
||||||
|
"C:/Windows/Fonts/NotoSansSC-VF.ttf",
|
||||||
|
"C:/Windows/Fonts/simhei.ttf",
|
||||||
|
"C:/Windows/Fonts/simsunb.ttf",
|
||||||
|
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttf",
|
||||||
|
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttf",
|
||||||
|
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc"
|
||||||
|
);
|
||||||
|
for (String path : candidates) {
|
||||||
|
File file = new File(path);
|
||||||
|
if (!file.exists() || !file.isFile()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String lowerPath = path.toLowerCase();
|
||||||
|
if (lowerPath.endsWith(".ttf")) {
|
||||||
|
return PDType0Font.load(document, file);
|
||||||
|
}
|
||||||
|
if (lowerPath.endsWith(".ttc")) {
|
||||||
|
PDType0Font font = loadFirstCollectionFont(document, file);
|
||||||
|
if (font != null) {
|
||||||
|
return font;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new ServiceException("未找到可用中文字体,无法导出PDF报告");
|
||||||
|
}
|
||||||
|
|
||||||
|
private PDType0Font loadFirstCollectionFont(PDDocument document, File file) throws IOException {
|
||||||
|
AtomicReference<PDType0Font> font = new AtomicReference<>();
|
||||||
|
try (TrueTypeCollection collection = new TrueTypeCollection(file)) {
|
||||||
|
collection.processAllFonts(typeFont -> {
|
||||||
|
if (font.get() == null) {
|
||||||
|
font.set(PDType0Font.load(document, typeFont, true));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return font.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<IndexedUploadSubject> indexedRows(List<CcdiProjectOverviewReportUploadSubjectVO> rows) {
|
||||||
|
List<IndexedUploadSubject> result = new ArrayList<>();
|
||||||
|
for (int i = 0; i < rows.size(); i++) {
|
||||||
|
result.add(new IndexedUploadSubject(String.valueOf(i + 1), rows.get(i)));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatPeopleSummary(CcdiProjectOverviewReportModelSummaryVO item) {
|
||||||
|
String names = safeText(item.getPeopleNames());
|
||||||
|
if ("-".equals(names)) {
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
List<String> people = Arrays.stream(names.split("、"))
|
||||||
|
.filter(value -> value != null && !value.isBlank())
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
if (people.size() <= 4) {
|
||||||
|
return String.join("、", people);
|
||||||
|
}
|
||||||
|
return String.join("、", people.subList(0, 4)) + "等" + defaultZero(item.getPeopleCount()) + "人";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatHitTags(List<CcdiProjectRiskHitTagVO> tags) {
|
||||||
|
if (tags == null || tags.isEmpty()) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
String text = tags.stream()
|
||||||
|
.map(CcdiProjectRiskHitTagVO::getRuleName)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.filter(value -> !value.isBlank())
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.joining("、"));
|
||||||
|
return text.isBlank() ? "-" : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String joinText(List<String> values) {
|
||||||
|
if (values == null || values.isEmpty()) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
String text = values.stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.filter(value -> !value.isBlank())
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.joining("、"));
|
||||||
|
return text.isBlank() ? "-" : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatRelatedStaff(String name, String code) {
|
||||||
|
if (name == null || name.isBlank()) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
if (code == null || code.isBlank()) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
return name + "(" + code + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatSummaryAndCashType(String summary, String cashType) {
|
||||||
|
return safeText(summary) + "/" + safeText(cashType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatAccount(String accountNo, String accountName) {
|
||||||
|
String masked = maskAccount(accountNo);
|
||||||
|
String name = safeText(accountName);
|
||||||
|
if ("-".equals(name)) {
|
||||||
|
return masked;
|
||||||
|
}
|
||||||
|
return masked + "\n" + name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String maskAccountList(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
return Arrays.stream(value.split("、|,|,"))
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(item -> !item.isBlank())
|
||||||
|
.map(this::maskAccount)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.joining("、"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String maskAccount(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
String text = value.trim().replaceAll("\\s+", "");
|
||||||
|
if (text.length() <= 8) {
|
||||||
|
return text.length() <= 4 ? text : text.substring(0, 2) + "****" + text.substring(text.length() - 2);
|
||||||
|
}
|
||||||
|
return text.substring(0, 4) + "****" + text.substring(text.length() - 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String maskIdCard(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
String text = value.trim();
|
||||||
|
if (text.length() < 10) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
return text.substring(0, 3) + "***********" + text.substring(text.length() - 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatCount(Integer value, String unit) {
|
||||||
|
return defaultZero(value) + unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatMoney(BigDecimal value) {
|
||||||
|
if (value == null) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
return MONEY_FORMAT.format(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer defaultZero(Integer value) {
|
||||||
|
return value == null ? 0 : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String safeText(String value) {
|
||||||
|
return value == null || value.isBlank() ? "-" : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String safeFileName(String value) {
|
||||||
|
String text = safeText(value);
|
||||||
|
return text.replaceAll("[\\\\/:*?\"<>|]", "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
private record IndexedUploadSubject(String index, CcdiProjectOverviewReportUploadSubjectVO row) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PdfPageWriter {
|
||||||
|
|
||||||
|
private static final float MARGIN = 36F;
|
||||||
|
private static final PDRectangle LANDSCAPE_A4 = new PDRectangle(
|
||||||
|
PDRectangle.A4.getHeight(),
|
||||||
|
PDRectangle.A4.getWidth()
|
||||||
|
);
|
||||||
|
private static final float CONTENT_WIDTH = LANDSCAPE_A4.getWidth() - MARGIN * 2;
|
||||||
|
private static final float PAGE_TOP = LANDSCAPE_A4.getHeight() - MARGIN;
|
||||||
|
private static final float PAGE_BOTTOM = MARGIN;
|
||||||
|
private static final float BODY_FONT_SIZE = 9F;
|
||||||
|
private static final float HEADER_FONT_SIZE = 9F;
|
||||||
|
private static final float TITLE_FONT_SIZE = 22F;
|
||||||
|
private static final float SECTION_FONT_SIZE = 15F;
|
||||||
|
private static final float SUBSECTION_FONT_SIZE = 12F;
|
||||||
|
private static final float LINE_HEIGHT = 12F;
|
||||||
|
private static final float CELL_PADDING = 5F;
|
||||||
|
|
||||||
|
private final PDDocument document;
|
||||||
|
private final PDType0Font font;
|
||||||
|
private PDPageContentStream content;
|
||||||
|
private float y;
|
||||||
|
|
||||||
|
PdfPageWriter(PDDocument document, PDType0Font font) {
|
||||||
|
this.document = document;
|
||||||
|
this.font = font;
|
||||||
|
}
|
||||||
|
|
||||||
|
void newPage() throws IOException {
|
||||||
|
close();
|
||||||
|
PDPage page = new PDPage(LANDSCAPE_A4);
|
||||||
|
document.addPage(page);
|
||||||
|
content = new PDPageContentStream(document, page);
|
||||||
|
y = PAGE_TOP;
|
||||||
|
}
|
||||||
|
|
||||||
|
void close() throws IOException {
|
||||||
|
if (content != null) {
|
||||||
|
content.close();
|
||||||
|
content = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void title(String text) throws IOException {
|
||||||
|
writeLine(text, TITLE_FONT_SIZE, new Color(18, 56, 93), 0F, 28F);
|
||||||
|
}
|
||||||
|
|
||||||
|
void text(String text, float fontSize, Color color) throws IOException {
|
||||||
|
writeLine(text, fontSize, color, 0F, 18F);
|
||||||
|
}
|
||||||
|
|
||||||
|
void section(String text) throws IOException {
|
||||||
|
ensureSpace(32F);
|
||||||
|
writeLine(text, SECTION_FONT_SIZE, new Color(18, 56, 93), 0F, 26F);
|
||||||
|
}
|
||||||
|
|
||||||
|
void subsection(String text) throws IOException {
|
||||||
|
ensureSpace(26F);
|
||||||
|
writeLine(text, SUBSECTION_FONT_SIZE, new Color(51, 65, 85), 0F, 22F);
|
||||||
|
}
|
||||||
|
|
||||||
|
void separator() throws IOException {
|
||||||
|
ensureSpace(16F);
|
||||||
|
content.setStrokingColor(new Color(31, 78, 121));
|
||||||
|
content.setLineWidth(1.4F);
|
||||||
|
content.moveTo(MARGIN, y);
|
||||||
|
content.lineTo(MARGIN + CONTENT_WIDTH, y);
|
||||||
|
content.stroke();
|
||||||
|
y -= 22F;
|
||||||
|
}
|
||||||
|
|
||||||
|
void metrics(List<CcdiProjectOverviewStatVO> stats) throws IOException {
|
||||||
|
ensureSpace(58F);
|
||||||
|
float cellWidth = CONTENT_WIDTH / Math.max(stats.size(), 1);
|
||||||
|
float x = MARGIN;
|
||||||
|
float rowHeight = 50F;
|
||||||
|
for (CcdiProjectOverviewStatVO stat : stats) {
|
||||||
|
drawRect(x, y - rowHeight, cellWidth, rowHeight, null);
|
||||||
|
drawCenteredText(String.valueOf(stat.getValue()), x, y - 18F, cellWidth, 18F, new Color(31, 78, 121));
|
||||||
|
drawCenteredText(stat.getLabel(), x, y - 36F, cellWidth, 10F, Color.GRAY);
|
||||||
|
x += cellWidth;
|
||||||
|
}
|
||||||
|
y -= rowHeight + 18F;
|
||||||
|
}
|
||||||
|
|
||||||
|
void table(
|
||||||
|
List<String> headers,
|
||||||
|
List<List<String>> rows,
|
||||||
|
float[] widthRatios,
|
||||||
|
String emptyText
|
||||||
|
) throws IOException {
|
||||||
|
List<List<String>> safeRows = rows.isEmpty()
|
||||||
|
? List.of(List.of(emptyText))
|
||||||
|
: rows;
|
||||||
|
List<String> safeHeaders = rows.isEmpty()
|
||||||
|
? List.of(headers.get(0))
|
||||||
|
: headers;
|
||||||
|
float[] widths = rows.isEmpty()
|
||||||
|
? new float[] { CONTENT_WIDTH }
|
||||||
|
: calculateWidths(widthRatios);
|
||||||
|
|
||||||
|
drawHeader(safeHeaders, widths);
|
||||||
|
for (List<String> row : safeRows) {
|
||||||
|
drawRow(row, widths, false);
|
||||||
|
}
|
||||||
|
y -= 8F;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float[] calculateWidths(float[] ratios) {
|
||||||
|
float[] widths = new float[ratios.length];
|
||||||
|
for (int i = 0; i < ratios.length; i++) {
|
||||||
|
widths[i] = CONTENT_WIDTH * ratios[i];
|
||||||
|
}
|
||||||
|
return widths;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawHeader(List<String> headers, float[] widths) throws IOException {
|
||||||
|
drawRow(headers, widths, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawRow(List<String> cells, float[] widths, boolean header) throws IOException {
|
||||||
|
List<List<String>> wrappedCells = new ArrayList<>();
|
||||||
|
float rowHeight = 0F;
|
||||||
|
for (int i = 0; i < widths.length; i++) {
|
||||||
|
String text = i < cells.size() ? cells.get(i) : "";
|
||||||
|
List<String> lines = wrapText(text, widths[i] - CELL_PADDING * 2, header ? HEADER_FONT_SIZE : BODY_FONT_SIZE);
|
||||||
|
wrappedCells.add(lines);
|
||||||
|
rowHeight = Math.max(rowHeight, lines.size() * LINE_HEIGHT + CELL_PADDING * 2);
|
||||||
|
}
|
||||||
|
rowHeight = Math.max(rowHeight, 24F);
|
||||||
|
ensureSpace(rowHeight + 4F);
|
||||||
|
|
||||||
|
float x = MARGIN;
|
||||||
|
for (int i = 0; i < widths.length; i++) {
|
||||||
|
Color background = header ? new Color(234, 241, 248) : null;
|
||||||
|
drawRect(x, y - rowHeight, widths[i], rowHeight, background);
|
||||||
|
drawCellText(wrappedCells.get(i), x + CELL_PADDING, y - CELL_PADDING - (header ? HEADER_FONT_SIZE : BODY_FONT_SIZE), header);
|
||||||
|
x += widths[i];
|
||||||
|
}
|
||||||
|
y -= rowHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawRect(float x, float bottomY, float width, float height, Color fill) throws IOException {
|
||||||
|
if (fill != null) {
|
||||||
|
content.setNonStrokingColor(fill);
|
||||||
|
content.addRect(x, bottomY, width, height);
|
||||||
|
content.fill();
|
||||||
|
}
|
||||||
|
content.setStrokingColor(new Color(205, 217, 229));
|
||||||
|
content.setLineWidth(0.5F);
|
||||||
|
content.addRect(x, bottomY, width, height);
|
||||||
|
content.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawCellText(List<String> lines, float x, float startY, boolean header) throws IOException {
|
||||||
|
content.beginText();
|
||||||
|
content.setNonStrokingColor(header ? new Color(24, 59, 90) : new Color(31, 41, 55));
|
||||||
|
content.setFont(font, header ? HEADER_FONT_SIZE : BODY_FONT_SIZE);
|
||||||
|
content.newLineAtOffset(x, startY);
|
||||||
|
for (int i = 0; i < lines.size(); i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
content.newLineAtOffset(0, -LINE_HEIGHT);
|
||||||
|
}
|
||||||
|
content.showText(lines.get(i));
|
||||||
|
}
|
||||||
|
content.endText();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawCenteredText(String text, float x, float baselineY, float width, float fontSize, Color color)
|
||||||
|
throws IOException {
|
||||||
|
float textWidth = textWidth(text, fontSize);
|
||||||
|
content.beginText();
|
||||||
|
content.setNonStrokingColor(color);
|
||||||
|
content.setFont(font, fontSize);
|
||||||
|
content.newLineAtOffset(x + (width - textWidth) / 2F, baselineY);
|
||||||
|
content.showText(text);
|
||||||
|
content.endText();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeLine(String text, float fontSize, Color color, float indent, float advance) throws IOException {
|
||||||
|
ensureSpace(advance);
|
||||||
|
content.beginText();
|
||||||
|
content.setNonStrokingColor(color);
|
||||||
|
content.setFont(font, fontSize);
|
||||||
|
content.newLineAtOffset(MARGIN + indent, y);
|
||||||
|
content.showText(text);
|
||||||
|
content.endText();
|
||||||
|
y -= advance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureSpace(float height) throws IOException {
|
||||||
|
if (y - height < PAGE_BOTTOM) {
|
||||||
|
newPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> wrapText(String text, float maxWidth, float fontSize) throws IOException {
|
||||||
|
String safeText = text == null || text.isBlank() ? "-" : text;
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
for (String part : safeText.split("\\n")) {
|
||||||
|
wrapPart(part, maxWidth, fontSize, result);
|
||||||
|
}
|
||||||
|
return result.isEmpty() ? List.of("-") : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void wrapPart(String text, float maxWidth, float fontSize, List<String> result) throws IOException {
|
||||||
|
StringBuilder current = new StringBuilder();
|
||||||
|
for (int i = 0; i < text.length(); i++) {
|
||||||
|
String next = String.valueOf(text.charAt(i));
|
||||||
|
if (textWidth(current + next, fontSize) > maxWidth && current.length() > 0) {
|
||||||
|
result.add(current.toString());
|
||||||
|
current.setLength(0);
|
||||||
|
}
|
||||||
|
current.append(next);
|
||||||
|
}
|
||||||
|
if (current.length() > 0) {
|
||||||
|
result.add(current.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float textWidth(String text, float fontSize) throws IOException {
|
||||||
|
return font.getStringWidth(text) / 1000F * fontSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,8 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisBasicInfoVO;
|
|||||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisObjectRecordVO;
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisObjectRecordVO;
|
||||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO;
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO;
|
||||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO;
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportParamVO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportVO;
|
||||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewEmployeeHitRowVO;
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewEmployeeHitRowVO;
|
||||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewStatVO;
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewStatVO;
|
||||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardsVO;
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardsVO;
|
||||||
@@ -37,15 +39,18 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO;
|
|||||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO;
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO;
|
||||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleItemVO;
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleItemVO;
|
||||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO;
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.vo.ModelParamAllVO;
|
||||||
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
|
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
|
||||||
import com.ruoyi.ccdi.project.mapper.CcdiBankTagResultMapper;
|
import com.ruoyi.ccdi.project.mapper.CcdiBankTagResultMapper;
|
||||||
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewEmployeeResultMapper;
|
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewEmployeeResultMapper;
|
||||||
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewMapper;
|
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewMapper;
|
||||||
|
import com.ruoyi.ccdi.project.service.ICcdiModelParamService;
|
||||||
import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService;
|
import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService;
|
||||||
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
@@ -81,6 +86,12 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
|||||||
@Resource
|
@Resource
|
||||||
private CcdiProjectRiskDetailWorkbookExporter workbookExporter;
|
private CcdiProjectRiskDetailWorkbookExporter workbookExporter;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private CcdiProjectOverviewReportPdfExporter reportPdfExporter;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ICcdiModelParamService modelParamService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CcdiProjectOverviewDashboardVO getDashboard(Long projectId) {
|
public CcdiProjectOverviewDashboardVO getDashboard(Long projectId) {
|
||||||
CcdiProject project = overviewMapper.selectDashboardBaseByProjectId(projectId);
|
CcdiProject project = overviewMapper.selectDashboardBaseByProjectId(projectId);
|
||||||
@@ -303,6 +314,38 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void exportOverviewReport(HttpServletResponse response, Long projectId) {
|
||||||
|
CcdiProject project = getRequiredProject(projectId);
|
||||||
|
|
||||||
|
CcdiProjectSuspiciousTransactionQueryDTO suspiciousQuery = new CcdiProjectSuspiciousTransactionQueryDTO();
|
||||||
|
suspiciousQuery.setProjectId(projectId);
|
||||||
|
suspiciousQuery.setSuspiciousType("ALL");
|
||||||
|
|
||||||
|
CcdiProjectOverviewReportVO report = new CcdiProjectOverviewReportVO();
|
||||||
|
report.setProject(project);
|
||||||
|
report.setDashboard(getDashboard(projectId));
|
||||||
|
report.setUploadSubjects(defaultList(overviewMapper.selectReportUploadSubjects(projectId)).stream()
|
||||||
|
.peek(item -> item.setDataPeriod(formatDataPeriod(item.getMinTrxDate(), item.getMaxTrxDate())))
|
||||||
|
.toList());
|
||||||
|
report.setParams(buildReportParams(projectId));
|
||||||
|
report.setModelSummaries(defaultList(overviewMapper.selectReportRiskModelSummaries(projectId)));
|
||||||
|
report.setRiskPeople(defaultList(overviewMapper.selectReportRiskPeople(projectId)).stream()
|
||||||
|
.peek(item -> item.setActionLabel(ACTION_LABEL))
|
||||||
|
.toList());
|
||||||
|
report.setSuspiciousTransactions(defaultList(
|
||||||
|
overviewMapper.selectReportSuspiciousTransactionList(suspiciousQuery)
|
||||||
|
));
|
||||||
|
report.setIllegalPeople(exportEmployeeCreditNegative(projectId));
|
||||||
|
report.setAbnormalAccounts(exportAbnormalAccountPeople(projectId));
|
||||||
|
|
||||||
|
try {
|
||||||
|
reportPdfExporter.export(response, report);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ServiceException("导出结果总览报告失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<CcdiProjectEmployeeCreditNegativeExcel> exportEmployeeCreditNegative(Long projectId) {
|
public List<CcdiProjectEmployeeCreditNegativeExcel> exportEmployeeCreditNegative(Long projectId) {
|
||||||
ensureProjectExists(projectId);
|
ensureProjectExists(projectId);
|
||||||
@@ -511,6 +554,31 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
|||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<CcdiProjectOverviewReportParamVO> buildReportParams(Long projectId) {
|
||||||
|
ModelParamAllVO response = modelParamService.selectAllParams(projectId);
|
||||||
|
return defaultList(response == null ? null : response.getModels()).stream()
|
||||||
|
.flatMap(model -> defaultList(model.getParams()).stream().map(param -> {
|
||||||
|
CcdiProjectOverviewReportParamVO row = new CcdiProjectOverviewReportParamVO();
|
||||||
|
row.setModelName(model.getModelName());
|
||||||
|
row.setParamName(param.getParamName());
|
||||||
|
row.setParamValue(param.getParamValue());
|
||||||
|
row.setParamUnit(param.getParamUnit());
|
||||||
|
row.setParamDesc(param.getParamDesc());
|
||||||
|
return row;
|
||||||
|
}))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatDataPeriod(String minTrxDate, String maxTrxDate) {
|
||||||
|
if (minTrxDate == null || minTrxDate.isBlank() || maxTrxDate == null || maxTrxDate.isBlank()) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
LocalDate start = LocalDate.parse(minTrxDate);
|
||||||
|
LocalDate end = LocalDate.parse(maxTrxDate);
|
||||||
|
int months = (end.getYear() - start.getYear()) * 12 + end.getMonthValue() - start.getMonthValue() + 1;
|
||||||
|
return Math.max(months, 1) + "个月";
|
||||||
|
}
|
||||||
|
|
||||||
private String formatRelatedStaff(String relatedStaffName, String relatedStaffCode) {
|
private String formatRelatedStaff(String relatedStaffName, String relatedStaffCode) {
|
||||||
if (relatedStaffName == null || relatedStaffName.isBlank()) {
|
if (relatedStaffName == null || relatedStaffName.isBlank()) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -57,6 +57,40 @@
|
|||||||
<result property="status" column="status"/>
|
<result property="status" column="status"/>
|
||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
|
<resultMap id="ReportUploadSubjectResultMap"
|
||||||
|
type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportUploadSubjectVO">
|
||||||
|
<result property="subjectName" column="subjectName"/>
|
||||||
|
<result property="accountNos" column="accountNos"/>
|
||||||
|
<result property="minTrxDate" column="minTrxDate"/>
|
||||||
|
<result property="maxTrxDate" column="maxTrxDate"/>
|
||||||
|
<result property="fileCount" column="fileCount"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<resultMap id="ReportModelSummaryResultMap"
|
||||||
|
type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportModelSummaryVO">
|
||||||
|
<result property="modelCode" column="modelCode"/>
|
||||||
|
<result property="modelName" column="modelName"/>
|
||||||
|
<result property="warningCount" column="warningCount"/>
|
||||||
|
<result property="peopleCount" column="peopleCount"/>
|
||||||
|
<result property="peopleNames" column="peopleNames"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<resultMap id="ReportSuspiciousTransactionResultMap"
|
||||||
|
type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportSuspiciousTransactionVO">
|
||||||
|
<id property="bankStatementId" column="bankStatementId"/>
|
||||||
|
<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="relatedStaffName" column="relatedStaffName"/>
|
||||||
|
<result property="relatedStaffCode" column="relatedStaffCode"/>
|
||||||
|
<result property="userMemo" column="userMemo"/>
|
||||||
|
<result property="cashType" column="cashType"/>
|
||||||
|
<result property="hitTags" column="hitTags"/>
|
||||||
|
<result property="displayAmount" column="displayAmount"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
<sql id="digitTableSql">
|
<sql id="digitTableSql">
|
||||||
select 0 as digit
|
select 0 as digit
|
||||||
union all select 1
|
union all select 1
|
||||||
@@ -338,6 +372,87 @@
|
|||||||
order by warning_count desc, model_code asc
|
order by warning_count desc, model_code asc
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectReportUploadSubjects" resultMap="ReportUploadSubjectResultMap">
|
||||||
|
select
|
||||||
|
trim(bs.LE_ACCOUNT_NAME) as subjectName,
|
||||||
|
group_concat(distinct trim(bs.LE_ACCOUNT_NO) order by trim(bs.LE_ACCOUNT_NO) separator '、') as accountNos,
|
||||||
|
date_format(min(
|
||||||
|
case
|
||||||
|
when bs.TRX_DATE is null or trim(bs.TRX_DATE) = '' then null
|
||||||
|
when length(trim(bs.TRX_DATE)) = 10 then str_to_date(concat(trim(bs.TRX_DATE), ' 00:00:00'), '%Y-%m-%d %H:%i:%s')
|
||||||
|
else str_to_date(trim(bs.TRX_DATE), '%Y-%m-%d %H:%i:%s')
|
||||||
|
end
|
||||||
|
), '%Y-%m-%d') as minTrxDate,
|
||||||
|
date_format(max(
|
||||||
|
case
|
||||||
|
when bs.TRX_DATE is null or trim(bs.TRX_DATE) = '' then null
|
||||||
|
when length(trim(bs.TRX_DATE)) = 10 then str_to_date(concat(trim(bs.TRX_DATE), ' 00:00:00'), '%Y-%m-%d %H:%i:%s')
|
||||||
|
else str_to_date(trim(bs.TRX_DATE), '%Y-%m-%d %H:%i:%s')
|
||||||
|
end
|
||||||
|
), '%Y-%m-%d') as maxTrxDate,
|
||||||
|
case
|
||||||
|
when count(distinct fur.id) > 0 then count(distinct fur.id)
|
||||||
|
else count(distinct bs.batch_id)
|
||||||
|
end as fileCount
|
||||||
|
from ccdi_bank_statement bs
|
||||||
|
left join ccdi_file_upload_record fur
|
||||||
|
on fur.project_id = bs.project_id
|
||||||
|
and fur.log_id = bs.batch_id
|
||||||
|
where bs.project_id = #{projectId}
|
||||||
|
and bs.LE_ACCOUNT_NAME is not null
|
||||||
|
and trim(bs.LE_ACCOUNT_NAME) != ''
|
||||||
|
group by trim(bs.LE_ACCOUNT_NAME)
|
||||||
|
order by trim(bs.LE_ACCOUNT_NAME) asc
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectReportRiskModelSummaries" resultMap="ReportModelSummaryResultMap">
|
||||||
|
select
|
||||||
|
model_scope.modelCode,
|
||||||
|
max(model_scope.modelName) as modelName,
|
||||||
|
sum(model_scope.warningCount) as warningCount,
|
||||||
|
count(distinct model_scope.staffIdCard) as peopleCount,
|
||||||
|
group_concat(distinct model_scope.staffName order by model_scope.staffName separator '、') as peopleNames
|
||||||
|
from (
|
||||||
|
select
|
||||||
|
result.staff_id_card as staffIdCard,
|
||||||
|
result.staff_name as staffName,
|
||||||
|
json_unquote(json_extract(result.model_hit_summary_json, concat('$[', idx.idx, '].modelCode'))) as modelCode,
|
||||||
|
json_unquote(json_extract(result.model_hit_summary_json, concat('$[', idx.idx, '].modelName'))) as modelName,
|
||||||
|
cast(json_unquote(json_extract(
|
||||||
|
result.model_hit_summary_json,
|
||||||
|
concat('$[', idx.idx, '].warningCount')
|
||||||
|
)) as unsigned) as warningCount
|
||||||
|
from ccdi_project_overview_employee_result result
|
||||||
|
join (
|
||||||
|
<include refid="jsonArrayIndexSql"/>
|
||||||
|
) idx on idx.idx < json_length(result.model_hit_summary_json)
|
||||||
|
where result.project_id = #{projectId}
|
||||||
|
) model_scope
|
||||||
|
where model_scope.modelCode is not null
|
||||||
|
group by model_scope.modelCode
|
||||||
|
order by warningCount desc, model_scope.modelCode asc
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectReportRiskPeople" resultMap="RiskModelPeopleItemResultMap">
|
||||||
|
select
|
||||||
|
result.project_id,
|
||||||
|
result.staff_id_card,
|
||||||
|
result.staff_name,
|
||||||
|
result.staff_code,
|
||||||
|
result.dept_name as department,
|
||||||
|
null as selected_model_codes
|
||||||
|
from ccdi_project_overview_employee_result result
|
||||||
|
where result.project_id = #{projectId}
|
||||||
|
order by case result.risk_level_code
|
||||||
|
when 'HIGH' then 1
|
||||||
|
when 'MEDIUM' then 2
|
||||||
|
else 3
|
||||||
|
end,
|
||||||
|
result.model_count desc,
|
||||||
|
result.rule_count desc,
|
||||||
|
result.staff_id_card asc
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="selectRiskModelPeoplePage" resultMap="RiskModelPeopleItemResultMap">
|
<select id="selectRiskModelPeoplePage" resultMap="RiskModelPeopleItemResultMap">
|
||||||
<bind name="projectId" value="query.projectId"/>
|
<bind name="projectId" value="query.projectId"/>
|
||||||
select
|
select
|
||||||
@@ -615,6 +730,38 @@
|
|||||||
order by final_result.trxDate desc, final_result.bankStatementId desc
|
order by final_result.trxDate desc, final_result.bankStatementId desc
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectReportSuspiciousTransactionList" resultMap="ReportSuspiciousTransactionResultMap">
|
||||||
|
select
|
||||||
|
final_result.bankStatementId,
|
||||||
|
final_result.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,
|
||||||
|
final_result.relatedStaffName,
|
||||||
|
final_result.relatedStaffCode,
|
||||||
|
final_result.userMemo,
|
||||||
|
final_result.cashType,
|
||||||
|
tag_result.hitTags,
|
||||||
|
final_result.displayAmount
|
||||||
|
from (
|
||||||
|
<include refid="suspiciousTransactionAggregatedSql"/>
|
||||||
|
) final_result
|
||||||
|
inner join ccdi_bank_statement bs
|
||||||
|
on bs.bank_statement_id = final_result.bankStatementId
|
||||||
|
left join (
|
||||||
|
select
|
||||||
|
tr.bank_statement_id,
|
||||||
|
group_concat(distinct tr.rule_name order by tr.id separator '、') as hitTags
|
||||||
|
from ccdi_bank_statement_tag_result tr
|
||||||
|
where tr.project_id = #{query.projectId}
|
||||||
|
and tr.bank_statement_id is not null
|
||||||
|
group by tr.bank_statement_id
|
||||||
|
) tag_result on tag_result.bank_statement_id = final_result.bankStatementId
|
||||||
|
<include refid="suspiciousTransactionFilterSql"/>
|
||||||
|
order by final_result.trxDate desc, final_result.bankStatementId desc
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="selectEmployeeCreditNegativePage"
|
<select id="selectEmployeeCreditNegativePage"
|
||||||
resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativeItemVO">
|
resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativeItemVO">
|
||||||
select
|
select
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
|||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
@@ -181,6 +182,27 @@ class CcdiProjectOverviewControllerContractTest {
|
|||||||
assertNotNull(operation);
|
assertNotNull(operation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldExposeOverviewReportExportEndpointContract() throws Exception {
|
||||||
|
Class<?> controllerClass = Class.forName("com.ruoyi.ccdi.project.controller.CcdiProjectOverviewController");
|
||||||
|
Method method = controllerClass.getMethod(
|
||||||
|
"exportOverviewReport",
|
||||||
|
HttpServletResponse.class,
|
||||||
|
Long.class
|
||||||
|
);
|
||||||
|
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
|
||||||
|
Operation operation = method.getAnnotation(Operation.class);
|
||||||
|
PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class);
|
||||||
|
|
||||||
|
assertNotNull(requestMapping);
|
||||||
|
assertEquals("/report/export", requestMapping.value()[0]);
|
||||||
|
assertEquals(List.of(RequestMethod.GET, RequestMethod.POST), Arrays.asList(requestMapping.method()));
|
||||||
|
assertNotNull(operation);
|
||||||
|
assertEquals("一键导出结果总览报告", operation.summary());
|
||||||
|
assertNotNull(preAuthorize);
|
||||||
|
assertEquals("@ss.hasPermi('ccdi:project:query')", preAuthorize.value());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldExposeSuspiciousTransactionsQueryDtoFields() throws Exception {
|
void shouldExposeSuspiciousTransactionsQueryDtoFields() throws Exception {
|
||||||
Class<?> dtoClass = Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO");
|
Class<?> dtoClass = Class.forName("com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO");
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package com.ruoyi.ccdi.project.service.impl;
|
||||||
|
|
||||||
|
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.vo.CcdiProjectOverviewReportModelSummaryVO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportParamVO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportSuspiciousTransactionVO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportUploadSubjectVO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportVO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewStatVO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO;
|
||||||
|
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.mock.web.MockHttpServletResponse;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class CcdiProjectOverviewReportPdfExporterTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldExportOverviewReportPdf() throws Exception {
|
||||||
|
CcdiProjectOverviewReportPdfExporter exporter = new CcdiProjectOverviewReportPdfExporter();
|
||||||
|
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||||
|
|
||||||
|
exporter.export(response, buildReport());
|
||||||
|
|
||||||
|
assertEquals("application/pdf", response.getContentType());
|
||||||
|
String header = new String(response.getContentAsByteArray(), 0, 4, StandardCharsets.ISO_8859_1);
|
||||||
|
assertEquals("%PDF", header);
|
||||||
|
assertTrue(response.getContentAsByteArray().length > 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CcdiProjectOverviewReportVO buildReport() {
|
||||||
|
CcdiProjectOverviewReportVO report = new CcdiProjectOverviewReportVO();
|
||||||
|
CcdiProject project = new CcdiProject();
|
||||||
|
project.setProjectId(40L);
|
||||||
|
project.setProjectName("导入测试");
|
||||||
|
report.setProject(project);
|
||||||
|
report.setDashboard(buildDashboard());
|
||||||
|
report.setUploadSubjects(List.of(buildUploadSubject()));
|
||||||
|
report.setParams(List.of(buildParam()));
|
||||||
|
report.setModelSummaries(List.of(buildModelSummary()));
|
||||||
|
report.setRiskPeople(List.of(buildRiskPeople()));
|
||||||
|
report.setSuspiciousTransactions(List.of(buildSuspiciousTransaction()));
|
||||||
|
report.setIllegalPeople(List.of(buildIllegalPerson()));
|
||||||
|
report.setAbnormalAccounts(List.of(buildAbnormalAccount()));
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CcdiProjectOverviewDashboardVO buildDashboard() {
|
||||||
|
CcdiProjectOverviewDashboardVO dashboard = new CcdiProjectOverviewDashboardVO();
|
||||||
|
dashboard.setStats(List.of(
|
||||||
|
buildStat("总人数", 10),
|
||||||
|
buildStat("高风险", 2),
|
||||||
|
buildStat("中风险", 3),
|
||||||
|
buildStat("低风险", 1),
|
||||||
|
buildStat("无风险", 4)
|
||||||
|
));
|
||||||
|
return dashboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CcdiProjectOverviewStatVO buildStat(String label, Integer value) {
|
||||||
|
CcdiProjectOverviewStatVO stat = new CcdiProjectOverviewStatVO();
|
||||||
|
stat.setLabel(label);
|
||||||
|
stat.setValue(value);
|
||||||
|
return stat;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CcdiProjectOverviewReportUploadSubjectVO buildUploadSubject() {
|
||||||
|
CcdiProjectOverviewReportUploadSubjectVO row = new CcdiProjectOverviewReportUploadSubjectVO();
|
||||||
|
row.setSubjectName("测试主体");
|
||||||
|
row.setAccountNos("6222000000000001、6222000000000002");
|
||||||
|
row.setDataPeriod("3个月");
|
||||||
|
row.setFileCount(2);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CcdiProjectOverviewReportParamVO buildParam() {
|
||||||
|
CcdiProjectOverviewReportParamVO row = new CcdiProjectOverviewReportParamVO();
|
||||||
|
row.setModelName("大额交易模型");
|
||||||
|
row.setParamName("单笔大额转账金额");
|
||||||
|
row.setParamValue("5000");
|
||||||
|
row.setParamUnit("元");
|
||||||
|
row.setParamDesc("单笔转账金额超过");
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CcdiProjectOverviewReportModelSummaryVO buildModelSummary() {
|
||||||
|
CcdiProjectOverviewReportModelSummaryVO row = new CcdiProjectOverviewReportModelSummaryVO();
|
||||||
|
row.setModelName("大额交易");
|
||||||
|
row.setWarningCount(6);
|
||||||
|
row.setPeopleCount(2);
|
||||||
|
row.setPeopleNames("张三、李四");
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CcdiProjectRiskModelPeopleItemVO buildRiskPeople() {
|
||||||
|
CcdiProjectRiskModelPeopleItemVO row = new CcdiProjectRiskModelPeopleItemVO();
|
||||||
|
row.setStaffName("张三");
|
||||||
|
row.setStaffCode("1001");
|
||||||
|
row.setIdNo("330000000000000001");
|
||||||
|
row.setDepartment("财务部");
|
||||||
|
row.setModelNames(List.of("大额交易"));
|
||||||
|
CcdiProjectRiskHitTagVO tag = new CcdiProjectRiskHitTagVO();
|
||||||
|
tag.setRuleName("大额转账交易");
|
||||||
|
row.setHitTagList(List.of(tag));
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CcdiProjectOverviewReportSuspiciousTransactionVO buildSuspiciousTransaction() {
|
||||||
|
CcdiProjectOverviewReportSuspiciousTransactionVO row = new CcdiProjectOverviewReportSuspiciousTransactionVO();
|
||||||
|
row.setTrxDate("2026-03-20 10:00:00");
|
||||||
|
row.setLeAccountNo("6222000000000001");
|
||||||
|
row.setLeAccountName("测试主体");
|
||||||
|
row.setCustomerAccountNo("6222000000000002");
|
||||||
|
row.setCustomerAccountName("对方账户");
|
||||||
|
row.setRelatedStaffName("张三");
|
||||||
|
row.setRelatedStaffCode("1001");
|
||||||
|
row.setUserMemo("转账");
|
||||||
|
row.setCashType("支出");
|
||||||
|
row.setHitTags("大额转账交易");
|
||||||
|
row.setDisplayAmount(new BigDecimal("-5000.00"));
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CcdiProjectEmployeeCreditNegativeExcel buildIllegalPerson() {
|
||||||
|
CcdiProjectEmployeeCreditNegativeExcel row = new CcdiProjectEmployeeCreditNegativeExcel();
|
||||||
|
row.setPersonName("李四");
|
||||||
|
row.setPersonId("330000000000000002");
|
||||||
|
row.setQueryDate("2026-03-20");
|
||||||
|
row.setCivilCnt(1);
|
||||||
|
row.setCivilLmt(new BigDecimal("20000.00"));
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CcdiProjectAbnormalAccountExcel buildAbnormalAccount() {
|
||||||
|
CcdiProjectAbnormalAccountExcel row = new CcdiProjectAbnormalAccountExcel();
|
||||||
|
row.setAccountNo("6222000000000003");
|
||||||
|
row.setAccountName("王五");
|
||||||
|
row.setBankName("中国银行");
|
||||||
|
row.setAbnormalType("突然销户");
|
||||||
|
row.setAbnormalTime("2026-03-20");
|
||||||
|
row.setStatus("已销户");
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
pom.xml
8
pom.xml
@@ -28,6 +28,7 @@
|
|||||||
<oshi.version>6.9.1</oshi.version>
|
<oshi.version>6.9.1</oshi.version>
|
||||||
<commons.io.version>2.21.0</commons.io.version>
|
<commons.io.version>2.21.0</commons.io.version>
|
||||||
<poi.version>4.1.2</poi.version>
|
<poi.version>4.1.2</poi.version>
|
||||||
|
<pdfbox.version>2.0.30</pdfbox.version>
|
||||||
<easyexcel.version>3.3.4</easyexcel.version>
|
<easyexcel.version>3.3.4</easyexcel.version>
|
||||||
<velocity.version>2.3</velocity.version>
|
<velocity.version>2.3</velocity.version>
|
||||||
<jwt.version>0.9.1</jwt.version>
|
<jwt.version>0.9.1</jwt.version>
|
||||||
@@ -131,6 +132,13 @@
|
|||||||
<version>${poi.version}</version>
|
<version>${poi.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- pdf导出工具 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.pdfbox</groupId>
|
||||||
|
<artifactId>pdfbox</artifactId>
|
||||||
|
<version>${pdfbox.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- easyexcel工具 -->
|
<!-- easyexcel工具 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.alibaba</groupId>
|
<groupId>com.alibaba</groupId>
|
||||||
|
|||||||
@@ -91,3 +91,12 @@ export function getOverviewAbnormalAccountPeople(params) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function exportOverviewReport(projectId) {
|
||||||
|
return request({
|
||||||
|
url: '/ccdi/project/overview/report/export',
|
||||||
|
method: 'post',
|
||||||
|
responseType: 'blob',
|
||||||
|
params: { projectId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,16 @@
|
|||||||
<div class="section-title">风险总览</div>
|
<div class="section-title">风险总览</div>
|
||||||
<div class="section-subtitle">集中展示项目风险统计与命中人员总览</div>
|
<div class="section-subtitle">集中展示项目风险统计与命中人员总览</div>
|
||||||
</div>
|
</div>
|
||||||
|
<el-button
|
||||||
|
size="mini"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
icon="el-icon-download"
|
||||||
|
:disabled="!projectId"
|
||||||
|
@click="handleOverviewReportExport"
|
||||||
|
>
|
||||||
|
一键导出
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<overview-stats :summary="currentData.summary" />
|
<overview-stats :summary="currentData.summary" />
|
||||||
<risk-people-section
|
<risk-people-section
|
||||||
@@ -191,6 +201,18 @@ export default {
|
|||||||
riskTags,
|
riskTags,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
handleOverviewReportExport() {
|
||||||
|
if (!this.projectId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.download(
|
||||||
|
"ccdi/project/overview/report/export",
|
||||||
|
{
|
||||||
|
projectId: this.projectId,
|
||||||
|
},
|
||||||
|
`初核结果报告_${this.projectId}_${new Date().getTime()}.pdf`
|
||||||
|
);
|
||||||
|
},
|
||||||
async loadOverviewData() {
|
async loadOverviewData() {
|
||||||
if (!this.projectId) {
|
if (!this.projectId) {
|
||||||
this.realData = this.stateDataMap.empty;
|
this.realData = this.stateDataMap.empty;
|
||||||
@@ -294,6 +316,10 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
border-left: 4px solid var(--ccdi-primary);
|
border-left: 4px solid var(--ccdi-primary);
|
||||||
|
|||||||
@@ -16,7 +16,21 @@ const mockSource = fs.readFileSync(
|
|||||||
),
|
),
|
||||||
"utf8"
|
"utf8"
|
||||||
);
|
);
|
||||||
|
const preliminaryCheckSource = fs.readFileSync(
|
||||||
|
path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../../src/views/ccdiProject/components/detail/PreliminaryCheck.vue"
|
||||||
|
),
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
|
|
||||||
assert(!overviewSource.includes("section-actions"), "统计区不应继续保留旧操作区壳层");
|
assert(!overviewSource.includes("section-actions"), "统计区不应继续保留旧操作区壳层");
|
||||||
assert(!mockSource.includes('label: "批量导出"'), "风险总览不应保留批量导出按钮");
|
assert(!mockSource.includes('label: "批量导出"'), "风险总览不应保留批量导出按钮");
|
||||||
assert(!mockSource.includes('label: "切换视图"'), "风险总览不应保留切换视图按钮");
|
assert(!mockSource.includes('label: "切换视图"'), "风险总览不应保留切换视图按钮");
|
||||||
|
|
||||||
|
[
|
||||||
|
"handleOverviewReportExport",
|
||||||
|
"ccdi/project/overview/report/export",
|
||||||
|
"初核结果报告_",
|
||||||
|
"一键导出",
|
||||||
|
].forEach((token) => assert(preliminaryCheckSource.includes(token), token));
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ const source = fs.readFileSync(
|
|||||||
"getOverviewRiskModelPeople",
|
"getOverviewRiskModelPeople",
|
||||||
"getOverviewSuspiciousTransactions",
|
"getOverviewSuspiciousTransactions",
|
||||||
"getOverviewEmployeeCreditNegative",
|
"getOverviewEmployeeCreditNegative",
|
||||||
|
"exportOverviewReport",
|
||||||
"/ccdi/project/overview/dashboard",
|
"/ccdi/project/overview/dashboard",
|
||||||
"/ccdi/project/overview/risk-people",
|
"/ccdi/project/overview/risk-people",
|
||||||
"/ccdi/project/overview/risk-models/cards",
|
"/ccdi/project/overview/risk-models/cards",
|
||||||
"/ccdi/project/overview/risk-models/people",
|
"/ccdi/project/overview/risk-models/people",
|
||||||
"/ccdi/project/overview/suspicious-transactions",
|
"/ccdi/project/overview/suspicious-transactions",
|
||||||
"/ccdi/project/overview/employee-credit-negative",
|
"/ccdi/project/overview/employee-credit-negative",
|
||||||
|
"/ccdi/project/overview/report/export",
|
||||||
].forEach((token) => assert(source.includes(token), token));
|
].forEach((token) => assert(source.includes(token), token));
|
||||||
|
|
||||||
[
|
[
|
||||||
@@ -76,6 +78,18 @@ assert(employeeCreditNegativeFn, "应新增员工负面征信接口参数透传
|
|||||||
"pageSize: params.pageSize",
|
"pageSize: params.pageSize",
|
||||||
].forEach((token) => assert(employeeCreditNegativeFn[0].includes(token), token));
|
].forEach((token) => assert(employeeCreditNegativeFn[0].includes(token), token));
|
||||||
|
|
||||||
|
const overviewReportExportFn = source.match(
|
||||||
|
/export function exportOverviewReport\(projectId\) \{[\s\S]*?params:\s*\{([\s\S]*?)\}\s*\}\s*\)/m
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(overviewReportExportFn, "应新增结果总览PDF导出接口");
|
||||||
|
|
||||||
|
[
|
||||||
|
"method: 'post'",
|
||||||
|
"responseType: 'blob'",
|
||||||
|
"projectId",
|
||||||
|
].forEach((token) => assert(overviewReportExportFn[0].includes(token), token));
|
||||||
|
|
||||||
["employeeResult", "resultTable", "overview/result"].forEach((token) =>
|
["employeeResult", "resultTable", "overview/result"].forEach((token) =>
|
||||||
assert(!source.includes(token), `前端 API 契约不应感知结果表实现:${token}`)
|
assert(!source.includes(token), `前端 API 契约不应感知结果表实现:${token}`)
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user