diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java index d31d11c7..605fe824 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiProjectOverviewController.java @@ -1,5 +1,6 @@ package com.ruoyi.ccdi.project.controller; +import com.ruoyi.ccdi.project.domain.dto.CcdiProjectAbnormalAccountQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectEmployeeCreditNegativeQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO; @@ -7,6 +8,7 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskPeopleOverviewExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel; +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountPageVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativePageVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisDetailVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO; @@ -130,6 +132,17 @@ public class CcdiProjectOverviewController extends BaseController { return AjaxResult.success(pageVO); } + /** + * 查询异常账户人员信息 + */ + @GetMapping("/abnormal-account-people") + @Operation(summary = "查询异常账户人员信息") + @PreAuthorize("@ss.hasPermi('ccdi:project:query')") + public AjaxResult getAbnormalAccountPeople(CcdiProjectAbnormalAccountQueryDTO queryDTO) { + CcdiProjectAbnormalAccountPageVO pageVO = overviewService.getAbnormalAccountPeople(queryDTO); + return AjaxResult.success(pageVO); + } + /** * 导出涉疑交易明细 */ diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiProjectAbnormalAccountQueryDTO.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiProjectAbnormalAccountQueryDTO.java new file mode 100644 index 00000000..02544c72 --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/dto/CcdiProjectAbnormalAccountQueryDTO.java @@ -0,0 +1,19 @@ +package com.ruoyi.ccdi.project.domain.dto; + +import lombok.Data; + +/** + * 异常账户人员信息查询 DTO + */ +@Data +public class CcdiProjectAbnormalAccountQueryDTO { + + /** 项目ID */ + private Long projectId; + + /** 页码 */ + private Integer pageNum; + + /** 每页数量 */ + private Integer pageSize; +} diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/excel/CcdiProjectAbnormalAccountExcel.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/excel/CcdiProjectAbnormalAccountExcel.java new file mode 100644 index 00000000..2d66b217 --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/excel/CcdiProjectAbnormalAccountExcel.java @@ -0,0 +1,29 @@ +package com.ruoyi.ccdi.project.domain.excel; + +import com.ruoyi.common.annotation.Excel; +import lombok.Data; + +/** + * 异常账户人员信息导出对象 + */ +@Data +public class CcdiProjectAbnormalAccountExcel { + + @Excel(name = "账号") + private String accountNo; + + @Excel(name = "开户人") + private String accountName; + + @Excel(name = "银行") + private String bankName; + + @Excel(name = "异常类型") + private String abnormalType; + + @Excel(name = "异常发生时间") + private String abnormalTime; + + @Excel(name = "状态") + private String status; +} diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectAbnormalAccountItemVO.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectAbnormalAccountItemVO.java new file mode 100644 index 00000000..e4ae1f6c --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectAbnormalAccountItemVO.java @@ -0,0 +1,22 @@ +package com.ruoyi.ccdi.project.domain.vo; + +import lombok.Data; + +/** + * 异常账户人员信息行对象 + */ +@Data +public class CcdiProjectAbnormalAccountItemVO { + + private String accountNo; + + private String accountName; + + private String bankName; + + private String abnormalType; + + private String abnormalTime; + + private String status; +} diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectAbnormalAccountPageVO.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectAbnormalAccountPageVO.java new file mode 100644 index 00000000..9ad9720a --- /dev/null +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiProjectAbnormalAccountPageVO.java @@ -0,0 +1,16 @@ +package com.ruoyi.ccdi.project.domain.vo; + +import java.util.ArrayList; +import java.util.List; +import lombok.Data; + +/** + * 异常账户人员信息分页结果 + */ +@Data +public class CcdiProjectAbnormalAccountPageVO { + + private List rows = new ArrayList<>(); + + private Long total = 0L; +} diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapper.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapper.java index aeac8d66..496d145c 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapper.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/mapper/CcdiProjectOverviewMapper.java @@ -2,10 +2,12 @@ package com.ruoyi.ccdi.project.mapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.ccdi.project.domain.CcdiProject; +import com.ruoyi.ccdi.project.domain.dto.CcdiProjectAbnormalAccountQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectEmployeeCreditNegativeQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO; +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountItemVO; import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativeItemVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO; @@ -106,6 +108,26 @@ public interface CcdiProjectOverviewMapper { @Param("query") CcdiProjectEmployeeCreditNegativeQueryDTO query ); + /** + * 分页查询异常账户人员信息 + * + * @param page 分页参数 + * @param query 查询条件 + * @return 分页结果 + */ + Page selectAbnormalAccountPage( + Page page, + @Param("query") CcdiProjectAbnormalAccountQueryDTO query + ); + + /** + * 查询异常账户人员信息导出列表 + * + * @param projectId 项目ID + * @return 导出列表 + */ + List selectAbnormalAccountList(@Param("projectId") Long projectId); + /** * 查询项目员工负面征信导出列表 * diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java index 18fc1ba5..854906b4 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiProjectOverviewService.java @@ -1,13 +1,16 @@ package com.ruoyi.ccdi.project.service; +import com.ruoyi.ccdi.project.domain.dto.CcdiProjectAbnormalAccountQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectEmployeeCreditNegativeQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO; +import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskPeopleOverviewExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel; +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountPageVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativePageVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisDetailVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO; @@ -144,6 +147,28 @@ public interface ICcdiProjectOverviewService { return new CcdiProjectEmployeeCreditNegativePageVO(); } + /** + * 查询异常账户人员信息 + * + * @param queryDTO 查询条件 + * @return 分页结果 + */ + default CcdiProjectAbnormalAccountPageVO getAbnormalAccountPeople( + CcdiProjectAbnormalAccountQueryDTO queryDTO + ) { + return new CcdiProjectAbnormalAccountPageVO(); + } + + /** + * 导出异常账户人员信息 + * + * @param projectId 项目ID + * @return 导出列表 + */ + default List exportAbnormalAccountPeople(Long projectId) { + return List.of(); + } + /** * 重算结果总览员工结果并同步项目风险人数 * diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java index 9e9e10b4..ab6570f8 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImpl.java @@ -2,15 +2,19 @@ package com.ruoyi.ccdi.project.service.impl; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ruoyi.ccdi.project.domain.CcdiProject; +import com.ruoyi.ccdi.project.domain.dto.CcdiProjectAbnormalAccountQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectEmployeeCreditNegativeQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO; +import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskPeopleOverviewExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel; import com.ruoyi.ccdi.project.domain.entity.CcdiProjectOverviewEmployeeResult; +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountItemVO; +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountPageVO; import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO; import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementHitTagVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativeItemVO; @@ -258,6 +262,31 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi return result; } + @Override + public CcdiProjectAbnormalAccountPageVO getAbnormalAccountPeople(CcdiProjectAbnormalAccountQueryDTO queryDTO) { + ensureProjectExists(queryDTO.getProjectId()); + + Page page = new Page<>( + defaultAbnormalAccountPageNum(queryDTO.getPageNum()), + defaultAbnormalAccountPageSize(queryDTO.getPageSize()) + ); + Page resultPage = overviewMapper.selectAbnormalAccountPage(page, queryDTO); + + CcdiProjectAbnormalAccountPageVO result = new CcdiProjectAbnormalAccountPageVO(); + result.setRows(defaultList(resultPage == null ? null : resultPage.getRecords())); + result.setTotal(resultPage == null ? 0L : resultPage.getTotal()); + return result; + } + + @Override + public List exportAbnormalAccountPeople(Long projectId) { + ensureProjectExists(projectId); + + return defaultList(overviewMapper.selectAbnormalAccountList(projectId)).stream() + .map(this::buildAbnormalAccountExcelRow) + .toList(); + } + @Override public void exportRiskDetails(HttpServletResponse response, Long projectId) { CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO(); @@ -266,8 +295,9 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi List suspiciousRows = exportSuspiciousTransactions(queryDTO); List creditRows = exportEmployeeCreditNegative(projectId); + List abnormalRows = exportAbnormalAccountPeople(projectId); try { - workbookExporter.export(response, projectId, suspiciousRows, creditRows); + workbookExporter.export(response, projectId, suspiciousRows, creditRows, abnormalRows); } catch (IOException e) { throw new ServiceException("导出风险明细失败"); } @@ -420,6 +450,14 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi return pageSize == null || pageSize <= 0 ? 5L : pageSize.longValue(); } + private long defaultAbnormalAccountPageNum(Integer pageNum) { + return pageNum == null || pageNum <= 0 ? 1L : pageNum.longValue(); + } + + private long defaultAbnormalAccountPageSize(Integer pageSize) { + return pageSize == null || pageSize <= 0 ? 5L : pageSize.longValue(); + } + private long defaultPageNum(Integer pageNum) { return pageNum == null || pageNum < 1 ? 1L : pageNum.longValue(); } @@ -462,6 +500,17 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi return row; } + private CcdiProjectAbnormalAccountExcel buildAbnormalAccountExcelRow(CcdiProjectAbnormalAccountItemVO item) { + CcdiProjectAbnormalAccountExcel row = new CcdiProjectAbnormalAccountExcel(); + row.setAccountNo(item.getAccountNo()); + row.setAccountName(item.getAccountName()); + row.setBankName(item.getBankName()); + row.setAbnormalType(item.getAbnormalType()); + row.setAbnormalTime(item.getAbnormalTime()); + row.setStatus(item.getStatus()); + return row; + } + private String formatRelatedStaff(String relatedStaffName, String relatedStaffCode) { if (relatedStaffName == null || relatedStaffName.isBlank()) { return null; diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectRiskDetailWorkbookExporter.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectRiskDetailWorkbookExporter.java index 215c722e..4cc08acb 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectRiskDetailWorkbookExporter.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectRiskDetailWorkbookExporter.java @@ -1,5 +1,6 @@ package com.ruoyi.ccdi.project.service.impl; +import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel; import com.ruoyi.common.utils.file.FileUtils; @@ -27,7 +28,8 @@ public class CcdiProjectRiskDetailWorkbookExporter { HttpServletResponse response, Long projectId, List suspiciousRows, - List creditRows + List creditRows, + List abnormalRows ) throws IOException { response.setContentType(CONTENT_TYPE); FileUtils.setAttachmentResponseHeader(response, "风险明细_" + projectId + ".xlsx"); @@ -35,7 +37,7 @@ public class CcdiProjectRiskDetailWorkbookExporter { try (Workbook workbook = new XSSFWorkbook()) { writeSuspiciousSheet(workbook.createSheet("涉疑交易明细"), suspiciousRows); writeCreditSheet(workbook.createSheet("员工负面征信信息"), creditRows); - writeAbnormalAccountSheet(workbook.createSheet("异常账户人员信息")); + writeAbnormalAccountSheet(workbook.createSheet("异常账户人员信息"), abnormalRows); workbook.write(response.getOutputStream()); } } @@ -88,10 +90,21 @@ public class CcdiProjectRiskDetailWorkbookExporter { } } - private void writeAbnormalAccountSheet(Sheet sheet) { + private void writeAbnormalAccountSheet(Sheet sheet, List rows) { Row header = sheet.createRow(0); String[] headers = { "账号", "开户人", "银行", "异常类型", "异常发生时间", "状态" }; writeHeader(header, headers); + + for (int i = 0; i < rows.size(); i++) { + CcdiProjectAbnormalAccountExcel item = rows.get(i); + Row row = sheet.createRow(i + 1); + row.createCell(0).setCellValue(safeText(item.getAccountNo())); + row.createCell(1).setCellValue(safeText(item.getAccountName())); + row.createCell(2).setCellValue(safeText(item.getBankName())); + row.createCell(3).setCellValue(safeText(item.getAbnormalType())); + row.createCell(4).setCellValue(safeText(item.getAbnormalTime())); + row.createCell(5).setCellValue(safeText(item.getStatus())); + } } private void writeHeader(Row row, String[] headers) { diff --git a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml index 0461042b..73ab829c 100644 --- a/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml +++ b/ccdi-project/src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml @@ -48,6 +48,15 @@ + + + + + + + + + select 0 as digit union all select 1 @@ -644,6 +653,92 @@ order by neg.query_date desc, neg.person_id asc + + select + account.account_no as accountNo, + account.account_no as account_no, + coalesce(nullif(account.account_name, ''), staff.name) as accountName, + account.bank as bankName, + tr.rule_name as abnormalType, + tr.rule_code as rule_code, + case + when tr.rule_code = 'SUDDEN_ACCOUNT_CLOSURE' then date_format(account.invalid_date, '%Y-%m-%d') + when tr.rule_code = 'DORMANT_ACCOUNT_LARGE_ACTIVATION' then substring( + substring_index( + substring_index(tr.reason_detail, ',', 2), + '首次交易日期', + -1 + ), + 1, + 10 + ) + else null + end as abnormal_time, + case + when account.status = 1 then '正常' + when account.status = 2 then '已销户' + else cast(account.status as char) + end as status + from ccdi_bank_statement_tag_result tr + inner join ccdi_account_info account + on account.owner_type = 'EMPLOYEE' + and account.owner_id = tr.object_key + and instr(tr.reason_detail, account.account_no) > 0 + left join ccdi_base_staff staff + on staff.id_card = tr.object_key + + + + + + resultPage = new Page<>(1, 5); + resultPage.setRecords(List.of(item)); + resultPage.setTotal(1L); + when(overviewMapper.selectAbnormalAccountPage(any(Page.class), any(CcdiProjectAbnormalAccountQueryDTO.class))) + .thenReturn(resultPage); + + CcdiProjectAbnormalAccountPageVO result = service.getAbnormalAccountPeople(queryDTO); + + assertEquals(1, result.getRows().size()); + assertEquals(1L, result.getTotal()); + assertEquals("6222000000000001", result.getRows().getFirst().getAccountNo()); + assertEquals("突然销户", result.getRows().getFirst().getAbnormalType()); + verify(overviewMapper).selectAbnormalAccountPage( + argThat(page -> page.getCurrent() == 1L && page.getSize() == 5L), + argThat(query -> query.getProjectId().equals(40L)) + ); + } + + @Test + void shouldDefaultAbnormalAccountPageNumAndPageSizeToOneAndFive() { + mockProjectExists(40L); + + Page emptyPage = new Page<>(1, 5); + emptyPage.setRecords(List.of()); + emptyPage.setTotal(0L); + when(overviewMapper.selectAbnormalAccountPage(any(Page.class), any(CcdiProjectAbnormalAccountQueryDTO.class))) + .thenReturn(emptyPage); + + CcdiProjectAbnormalAccountQueryDTO queryDTO = new CcdiProjectAbnormalAccountQueryDTO(); + queryDTO.setProjectId(40L); + service.getAbnormalAccountPeople(queryDTO); + + verify(overviewMapper).selectAbnormalAccountPage( + argThat(page -> page.getCurrent() == 1L && page.getSize() == 5L), + any(CcdiProjectAbnormalAccountQueryDTO.class) + ); + } + + @Test + void shouldExportAbnormalAccountPeopleRows() { + mockProjectExists(40L); + + CcdiProjectAbnormalAccountItemVO item = new CcdiProjectAbnormalAccountItemVO(); + item.setAccountNo("6222000000000002"); + item.setAccountName("王五"); + item.setBankName("中国银行"); + item.setAbnormalType("休眠账户大额启用"); + item.setAbnormalTime("2025-08-01"); + item.setStatus("正常"); + when(overviewMapper.selectAbnormalAccountList(40L)).thenReturn(List.of(item)); + + List rows = service.exportAbnormalAccountPeople(40L); + + assertEquals(1, rows.size()); + assertEquals("6222000000000002", rows.getFirst().getAccountNo()); + assertEquals("王五", rows.getFirst().getAccountName()); + assertEquals("中国银行", rows.getFirst().getBankName()); + assertEquals("休眠账户大额启用", rows.getFirst().getAbnormalType()); + assertEquals("2025-08-01", rows.getFirst().getAbnormalTime()); + assertEquals("正常", rows.getFirst().getStatus()); + verify(overviewMapper).selectAbnormalAccountList(40L); + } + + @Test + void shouldThrowWhenProjectDoesNotExistForAbnormalAccountQueries() { + when(projectMapper.selectById(99L)).thenReturn(null); + + CcdiProjectAbnormalAccountQueryDTO queryDTO = new CcdiProjectAbnormalAccountQueryDTO(); + queryDTO.setProjectId(99L); + + assertThrows(ServiceException.class, () -> service.getAbnormalAccountPeople(queryDTO)); + assertThrows(ServiceException.class, () -> service.exportAbnormalAccountPeople(99L)); + } + + private void mockProjectExists(Long projectId) { + CcdiProject project = new CcdiProject(); + project.setProjectId(projectId); + when(projectMapper.selectById(projectId)).thenReturn(project); + } +} diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java index 774ae742..c1e5d2a7 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectOverviewServiceImplTest.java @@ -5,10 +5,12 @@ import com.ruoyi.ccdi.project.domain.CcdiProject; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectPersonAnalysisDetailQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO; import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO; +import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskPeopleOverviewExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel; import com.ruoyi.ccdi.project.domain.entity.CcdiProjectOverviewEmployeeResult; +import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountItemVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativeItemVO; import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO; import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO; @@ -268,6 +270,15 @@ class CcdiProjectOverviewServiceImplTest { creditItem.setCivilLmt(new BigDecimal("20000.00")); when(overviewMapper.selectEmployeeCreditNegativeList(40L)).thenReturn(List.of(creditItem)); + CcdiProjectAbnormalAccountItemVO abnormalItem = new CcdiProjectAbnormalAccountItemVO(); + abnormalItem.setAccountNo("6222000000000001"); + abnormalItem.setAccountName("李四"); + abnormalItem.setBankName("中国农业银行"); + abnormalItem.setAbnormalType("突然销户"); + abnormalItem.setAbnormalTime("2026-03-20"); + abnormalItem.setStatus("已销户"); + when(overviewMapper.selectAbnormalAccountList(40L)).thenReturn(List.of(abnormalItem)); + MockHttpServletResponse response = new MockHttpServletResponse(); service.exportRiskDetails(response, 40L); @@ -282,6 +293,9 @@ class CcdiProjectOverviewServiceImplTest { ), argThat((List rows) -> rows.size() == 1 && "李四".equals(rows.getFirst().getPersonName()) + ), + argThat((List rows) -> + rows.size() == 1 && "6222000000000001".equals(rows.getFirst().getAccountNo()) ) ); } diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectRiskDetailWorkbookExporterTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectRiskDetailWorkbookExporterTest.java index 7d2da1ef..c65e061b 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectRiskDetailWorkbookExporterTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiProjectRiskDetailWorkbookExporterTest.java @@ -1,5 +1,6 @@ package com.ruoyi.ccdi.project.service.impl; +import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel; import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel; import org.apache.poi.ss.usermodel.WorkbookFactory; @@ -36,7 +37,15 @@ class CcdiProjectRiskDetailWorkbookExporterTest { creditRow.setCivilCnt(1); creditRow.setCivilLmt(new BigDecimal("20000.00")); - exporter.export(response, 40L, List.of(suspiciousRow), List.of(creditRow)); + CcdiProjectAbnormalAccountExcel abnormalRow = new CcdiProjectAbnormalAccountExcel(); + abnormalRow.setAccountNo("6222000000000001"); + abnormalRow.setAccountName("李四"); + abnormalRow.setBankName("中国农业银行"); + abnormalRow.setAbnormalType("突然销户"); + abnormalRow.setAbnormalTime("2026-03-20"); + abnormalRow.setStatus("已销户"); + + exporter.export(response, 40L, List.of(suspiciousRow), List.of(creditRow), List.of(abnormalRow)); assertTrue(response.getContentType().startsWith( "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" @@ -48,8 +57,18 @@ class CcdiProjectRiskDetailWorkbookExporterTest { assertEquals("员工负面征信信息", workbook.getSheetAt(1).getSheetName()); assertEquals("异常账户人员信息", workbook.getSheetAt(2).getSheetName()); assertEquals("账号", workbook.getSheetAt(2).getRow(0).getCell(0).getStringCellValue()); + assertEquals("开户人", workbook.getSheetAt(2).getRow(0).getCell(1).getStringCellValue()); + assertEquals("银行", workbook.getSheetAt(2).getRow(0).getCell(2).getStringCellValue()); + assertEquals("异常类型", workbook.getSheetAt(2).getRow(0).getCell(3).getStringCellValue()); + assertEquals("异常发生时间", workbook.getSheetAt(2).getRow(0).getCell(4).getStringCellValue()); assertEquals("状态", workbook.getSheetAt(2).getRow(0).getCell(5).getStringCellValue()); - assertEquals(1, workbook.getSheetAt(2).getPhysicalNumberOfRows()); + assertEquals("6222000000000001", workbook.getSheetAt(2).getRow(1).getCell(0).getStringCellValue()); + assertEquals("李四", workbook.getSheetAt(2).getRow(1).getCell(1).getStringCellValue()); + assertEquals("中国农业银行", workbook.getSheetAt(2).getRow(1).getCell(2).getStringCellValue()); + assertEquals("突然销户", workbook.getSheetAt(2).getRow(1).getCell(3).getStringCellValue()); + assertEquals("2026-03-20", workbook.getSheetAt(2).getRow(1).getCell(4).getStringCellValue()); + assertEquals("已销户", workbook.getSheetAt(2).getRow(1).getCell(5).getStringCellValue()); + assertEquals(2, workbook.getSheetAt(2).getPhysicalNumberOfRows()); } } } diff --git a/docs/reports/implementation/2026-03-31-project-detail-risk-details-abnormal-account-backend-implementation.md b/docs/reports/implementation/2026-03-31-project-detail-risk-details-abnormal-account-backend-implementation.md new file mode 100644 index 00000000..825b10d9 --- /dev/null +++ b/docs/reports/implementation/2026-03-31-project-detail-risk-details-abnormal-account-backend-implementation.md @@ -0,0 +1,136 @@ +# 项目详情风险明细异常账户人员信息后端实施记录 + +## 1. 实施概述 + +- 实施日期:2026-03-31 +- 实施目标:为项目详情风险明细补齐“异常账户人员信息”的真实后端分页查询与统一导出能力 +- 实施范围:`ccdi-project` 模块结果总览控制器、服务层、Mapper SQL、统一工作簿导出器及对应测试 + +## 2. 新增接口与对象 + +### 2.1 新增接口 + +- `GET /ccdi/project/overview/abnormal-account-people` + - 入参:`projectId`、`pageNum`、`pageSize` + - 返回:`rows`、`total` + - 权限:`ccdi:project:query` + +### 2.2 新增 DTO / VO / Excel 对象 + +- `CcdiProjectAbnormalAccountQueryDTO` + - 承载异常账户分页查询入参 +- `CcdiProjectAbnormalAccountItemVO` + - 承载单条异常账户明细 +- `CcdiProjectAbnormalAccountPageVO` + - 承载分页查询结果 `rows/total` +- `CcdiProjectAbnormalAccountExcel` + - 承载统一导出第 3 个 sheet 的行数据 + +## 3. Mapper SQL 口径 + +异常账户分页与导出统一复用同一套基础查询口径: + +- 仅查询当前项目:`tr.project_id = projectId` +- 仅查询异常账户模型:`tr.model_code = 'ABNORMAL_ACCOUNT'` +- 仅查询对象型结果:`tr.bank_statement_id is null` +- 仅查询员工本人账户:`account.owner_type = 'EMPLOYEE'` 且 `account.owner_id = tr.object_key` +- 仅在 `reason_detail` 中命中具体账号时返回:`instr(tr.reason_detail, account.account_no) > 0` +- 排序统一为:`异常发生时间 desc -> 账号 asc -> 规则编码 asc` + +字段映射如下: + +- `accountNo`:`ccdi_account_info.account_no` +- `accountName`:优先 `ccdi_account_info.account_name`,为空回退 `ccdi_base_staff.name` +- `bankName`:`ccdi_account_info.bank` +- `abnormalType`:`ccdi_bank_statement_tag_result.rule_name` +- `abnormalTime` + - `SUDDEN_ACCOUNT_CLOSURE` 取 `invalid_date` + - `DORMANT_ACCOUNT_LARGE_ACTIVATION` 从 `reason_detail` 提取首次交易日期 +- `status` + - `1 -> 正常` + - `2 -> 已销户` + +## 4. 服务层与统一导出改动 + +### 4.1 服务层 + +- 在 `ICcdiProjectOverviewService` 中新增: + - `getAbnormalAccountPeople(queryDTO)` + - `exportAbnormalAccountPeople(projectId)` +- 在 `CcdiProjectOverviewServiceImpl` 中实现: + - 项目存在性校验 + - 分页默认值 `pageNum=1`、`pageSize=5` + - 分页结果直接映射为 `CcdiProjectAbnormalAccountPageVO` + - 导出结果映射为 `CcdiProjectAbnormalAccountExcel` + +### 4.2 统一导出 + +- `exportRiskDetails(...)` 现在会同时查询: + - 涉疑交易明细 + - 员工负面征信信息 + - 异常账户人员信息 +- `CcdiProjectRiskDetailWorkbookExporter.export(...)` 方法签名扩展为接收异常账户列表 +- 第 3 个 sheet `异常账户人员信息` 从“仅表头”改为“表头 + 真实数据行” +- 第 3 个 sheet 列顺序固定为: + - `账号` + - `开户人` + - `银行` + - `异常类型` + - `异常发生时间` + - `状态` + +## 5. 自动化验证 + +### 5.1 基线验证 + +执行命令: + +```bash +mvn -pl ccdi-project -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiProjectOverviewControllerContractTest,CcdiProjectOverviewControllerTest,CcdiProjectOverviewMapperSqlTest,CcdiProjectOverviewServiceImplTest,CcdiProjectRiskDetailWorkbookExporterTest test +``` + +验证结果: + +- 40 个相关既有测试通过 + +### 5.2 任务内 TDD 验证 + +按计划分别执行并通过: + +```bash +mvn -pl ccdi-project -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiProjectOverviewControllerContractTest,CcdiProjectOverviewControllerTest test +mvn -pl ccdi-project -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiProjectOverviewMapperSqlTest test +mvn -pl ccdi-project -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiProjectOverviewServiceAbnormalAccountTest test +mvn -pl ccdi-project -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiProjectOverviewServiceImplTest,CcdiProjectRiskDetailWorkbookExporterTest test +``` + +### 5.3 最终回归 + +执行命令: + +```bash +mvn -pl ccdi-project -am -Dsurefire.failIfNoSpecifiedTests=false -Dtest=CcdiProjectOverviewControllerContractTest,CcdiProjectOverviewControllerTest,CcdiProjectOverviewMapperSqlTest,CcdiProjectOverviewServiceAbnormalAccountTest,CcdiProjectOverviewServiceImplTest,CcdiProjectRiskDetailWorkbookExporterTest test +``` + +结果: + +- 47 个测试全部通过 +- `BUILD SUCCESS` + +## 6. 手工联调与进程处理 + +- 本次未执行手工联调 +- 未启动新的后端 `java -jar ruoyi-admin.jar` 进程 +- 因未启动额外前后端进程,无额外进程需要关闭 + +## 7. 结果结论 + +- 异常账户人员信息分页接口已具备真实查询能力 +- 页面查询与统一导出第 3 个 sheet 已复用同一套异常账户明细口径 +- 返回字段已覆盖: + - `accountNo` + - `accountName` + - `bankName` + - `abnormalType` + - `abnormalTime` + - `status`