Merge remote-tracking branch 'origin/dev-ui' into dev
This commit is contained in:
@@ -29,6 +29,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
@@ -181,4 +182,14 @@ public class CcdiProjectOverviewController extends BaseController {
|
||||
public void exportRiskDetails(HttpServletResponse response, Long 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.CcdiProjectPersonAnalysisBasicInfoVO;
|
||||
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.CcdiProjectRiskModelCardVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO;
|
||||
@@ -72,6 +75,40 @@ public interface CcdiProjectOverviewMapper {
|
||||
*/
|
||||
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) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 一键导出结果总览报告
|
||||
*
|
||||
* @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.CcdiProjectEmployeeRiskAggregateVO;
|
||||
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.CcdiProjectOverviewStatVO;
|
||||
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.CcdiProjectTopRiskPeopleItemVO;
|
||||
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.CcdiBankTagResultMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewEmployeeResultMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiProjectOverviewMapper;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiModelParamService;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -81,6 +86,12 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
@Resource
|
||||
private CcdiProjectRiskDetailWorkbookExporter workbookExporter;
|
||||
|
||||
@Resource
|
||||
private CcdiProjectOverviewReportPdfExporter reportPdfExporter;
|
||||
|
||||
@Resource
|
||||
private ICcdiModelParamService modelParamService;
|
||||
|
||||
@Override
|
||||
public CcdiProjectOverviewDashboardVO getDashboard(Long 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
|
||||
public List<CcdiProjectEmployeeCreditNegativeExcel> exportEmployeeCreditNegative(Long projectId) {
|
||||
ensureProjectExists(projectId);
|
||||
@@ -511,6 +554,31 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
|
||||
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) {
|
||||
if (relatedStaffName == null || relatedStaffName.isBlank()) {
|
||||
return null;
|
||||
|
||||
@@ -57,6 +57,40 @@
|
||||
<result property="status" column="status"/>
|
||||
</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">
|
||||
select 0 as digit
|
||||
union all select 1
|
||||
@@ -338,6 +372,87 @@
|
||||
order by warning_count desc, model_code asc
|
||||
</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">
|
||||
<bind name="projectId" value="query.projectId"/>
|
||||
select
|
||||
@@ -615,6 +730,38 @@
|
||||
order by final_result.trxDate desc, final_result.bankStatementId desc
|
||||
</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"
|
||||
resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativeItemVO">
|
||||
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.PostMapping;
|
||||
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.assertNotNull;
|
||||
@@ -181,6 +182,27 @@ class CcdiProjectOverviewControllerContractTest {
|
||||
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
|
||||
void shouldExposeSuspiciousTransactionsQueryDtoFields() throws Exception {
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user