74 Commits

Author SHA1 Message Date
wjj
c999497693 优化外部人员风险明细模型筛选 2026-07-09 16:19:08 +08:00
wjj
3f3e9268f2 实现结果总览排除可疑预警 2026-07-09 14:49:02 +08:00
wjj
9c02812675 合并dev最新代码到dev-ui 2026-07-06 09:05:10 +08:00
wkc
b7e9a8da03 校验上传流水文件名身份证信息 2026-07-03 17:38:46 +08:00
wjj
bb41fd7e89 新增新增贷款查询和导出 2026-07-03 10:01:17 +08:00
wjj
8b9226643c 新增涉疑交易明细筛选 2026-07-03 09:15:14 +08:00
wjj
6d2f40843a 调整项目列表入口和操作按钮 2026-07-02 17:48:08 +08:00
wkc
21c4e91f41 修复专项核查图谱项目ID传递 2026-07-02 17:04:22 +08:00
wkc
87fb6443e6 test 2026-07-02 16:48:17 +08:00
wkc
979ed9669f 调整测试目录忽略规则 2026-07-02 16:46:57 +08:00
wkc
57a33098c9 实现流程项目逻辑删除与恢复 2026-07-02 10:54:36 +08:00
wkc
2f53fc4d1e 用户角色选择改为下拉单选 2026-07-02 10:53:01 +08:00
wkc
64ddc362e3 新增项目管理员角色权限控制 2026-07-01 17:02:17 +08:00
wkc
6c70149e0c 项目上传删除暂不调用流水分析平台 2026-07-01 17:02:17 +08:00
wkc
2298f9e589 移除风险模型导出按钮 2026-07-01 17:02:17 +08:00
wkc
e08f437acc 为风险明细列表添加分页选择器 2026-07-01 17:00:11 +08:00
wjj
6134ec06af 放开涉疑交易流水级模型命中 2026-07-01 16:58:16 +08:00
wjj
dfa35ebd85 优化涉疑交易模型口径和报告展示 2026-07-01 16:58:16 +08:00
wkc
1f82c7e16e 优化资金图谱主题节点检索 2026-07-01 16:57:31 +08:00
wkc
6c206deb5f 实现结果总览详情资产和征信页签 2026-07-01 16:57:31 +08:00
wkc
2999d7cf7d 调整项目人数按流水证件号统计 2026-07-01 16:55:09 +08:00
wjj
f42981f153 放开涉疑交易流水级模型命中 2026-07-01 16:53:44 +08:00
wkc
67f206be1b Refactor project pages and update related docs 2026-07-01 16:47:26 +08:00
wjj
66d34c7ceb 优化涉疑交易模型口径和报告展示 2026-07-01 16:46:23 +08:00
wkc
2e30b2a877 修正双员工夫妻家庭专项核查口径 2026-07-01 16:37:00 +08:00
wkc
d2cdb0f3c9 修复风险总览无风险人员负数问题 2026-07-01 16:37:00 +08:00
wkc
aaec746181 优化资产估值万元展示 2026-07-01 16:37:00 +08:00
wkc
dcfd869d84 Implement credit parse result polling and sentinel handling 2026-07-01 16:37:00 +08:00
wkc
d284923c59 调整征信解析返回解析和日志 2026-07-01 16:37:00 +08:00
wkc
84b04e31ed Refactor credit parse to use remote HTML paths 2026-07-01 16:37:00 +08:00
wkc
507bc1c2d7 Remove obsolete code and documentation 2026-07-01 16:37:00 +08:00
wkc
64891a621d 移除前端默认登录凭据 2026-07-01 16:37:00 +08:00
wkc
606b4f836b 生产配置 2026-07-01 16:37:00 +08:00
wkc
33c1f9df90 新增生产统一部署脚本 2026-07-01 16:37:00 +08:00
wkc
bee1e800ce Fix PDF font loading for project overview reports 2026-07-01 16:37:00 +08:00
wkc
a191cc7ca3 新增专项排查图谱展示 2026-07-01 16:33:59 +08:00
wkc
7826645a90 新增专项排查图谱前端实施计划 2026-07-01 16:31:35 +08:00
wkc
9b11426617 新增专项排查图谱嵌入设计 2026-07-01 16:31:35 +08:00
wkc
1494491d89 展示员工亲属实体关联统信码 2026-07-01 16:31:35 +08:00
wkc
5509ece708 调整招聘信息毕业年月选择控件 2026-07-01 16:31:35 +08:00
wkc
02e45dadcc 完善招聘信息主键关联与工作经历维护 2026-07-01 16:31:35 +08:00
wkc
caeac0f115 招聘导入模板增加招聘类型下拉框 2026-07-01 16:31:35 +08:00
wkc
a4005d3e9f 修复导入模板格式和必填标记 2026-07-01 16:31:35 +08:00
wkc
f9e0a079f5 Update import templates and relation query fields 2026-07-01 16:31:35 +08:00
wkc
e00d60f5c1 回测四类导入自动补入实体库 2026-07-01 16:31:35 +08:00
wkc
87da17bccb 修复双Sheet资产单独导入任务ID 2026-07-01 16:31:35 +08:00
wkc
4a9c6ba64b 实现关联业务自动补入实体库 2026-07-01 16:31:35 +08:00
wkc
17eb11b3b8 uat配置更新 2026-07-01 16:31:35 +08:00
wkc
b740d4c100 移除.DS_Store跟踪 2026-07-01 16:31:35 +08:00
wkc
f5a98bbd7a 修订: 中介实体补入无需机构名称 2026-07-01 16:31:35 +08:00
wkc
987afc3863 调整信息维护页面并修复项目概览统计 2026-07-01 16:31:35 +08:00
wkc
051650370e 计划: 员工资产导入与实体库自动补入修复 2026-07-01 16:28:16 +08:00
wkc
c211413bd2 修订: 明确资产Sheet单独导入规则 2026-07-01 16:28:16 +08:00
wkc
bb9994a339 修订: 完善员工资产导入与实体库补入设计 2026-07-01 16:28:16 +08:00
wkc
daf463bbb4 设计: 员工资产导入与实体库自动补入修复 2026-07-01 16:28:16 +08:00
wkc
6f6f2d264c 统一项目分析弹窗圆角样式 2026-07-01 16:28:16 +08:00
wkc
266108ac06 修复流水上传原始文件名保持 2026-07-01 16:16:48 +08:00
wkc
5ff67442bb 修复流水分析上传文件名传递 2026-07-01 16:16:48 +08:00
wkc
b7bb96b43b 设计: 保持上传流水原始文件名 2026-07-01 16:16:48 +08:00
wkc
9902430350 Add import dropdown validation 2026-07-01 16:16:48 +08:00
wkc
89773caa7b 新增导入下拉框校验实施计划 2026-07-01 16:16:48 +08:00
wkc
def656d57b 完善导入下拉框校验设计 2026-07-01 16:16:48 +08:00
wkc
6b6fae4b92 新增导入下拉框校验设计文档 2026-07-01 16:16:48 +08:00
wkc
0dc3b6ee2f uat配置文件 2026-07-01 16:16:47 +08:00
wjj
04ede64767 优化项目分析详情与异常明细展示 2026-07-01 11:26:01 +08:00
wjj
066d850389 清洗流水本方证件号格式 2026-06-30 13:40:24 +08:00
wjj
5e4bfca05b 完善外部人员预警与项目分析上线内容 2026-06-30 13:26:29 +08:00
wjj
4e90e22ee2 优化项目详情资金流向图谱展示 2026-06-04 18:01:55 +08:00
wjj
a5eba12ed5 调整流水模型理财剔除与提现规则 2026-06-04 15:55:25 +08:00
wjj
aef6c43181 放开涉疑交易流水级模型命中 2026-06-01 16:44:51 +08:00
wjj
4c2ea9d8e6 修正短时间多次存现本人口径 2026-06-01 15:50:25 +08:00
wjj
26cd049991 新增图谱功能及验收清单 2026-05-29 18:33:26 +08:00
wjj
180a892275 优化涉疑交易模型口径和报告展示 2026-05-26 16:43:39 +08:00
wjj
ef8147892e 补充项目分析个人详情页正式化样式 2026-05-06 16:18:03 +08:00
204 changed files with 13102 additions and 860 deletions

4
.gitignore vendored
View File

@@ -86,12 +86,8 @@ ruoyi-ui/vue.config.js
ruoyi-ui/dist.zip
*/src/test/
.pytest_cache/
tests/
tongweb_62318.properties
.superpowers/

View File

@@ -0,0 +1,147 @@
package com.ruoyi.info.collection.controller;
import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffAssetInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel;
import com.ruoyi.info.collection.domain.vo.BaseStaffImportSubmitResultVO;
import com.ruoyi.info.collection.service.ICcdiBaseStaffAssetImportService;
import com.ruoyi.info.collection.service.ICcdiBaseStaffImportService;
import com.ruoyi.info.collection.service.ICcdiBaseStaffService;
import com.ruoyi.info.collection.utils.EasyExcelUtil;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockMultipartFile;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CcdiBaseStaffControllerTest {
@InjectMocks
private CcdiBaseStaffController controller;
@Mock
private ICcdiBaseStaffService baseStaffService;
@Mock
private ICcdiBaseStaffImportService importAsyncService;
@Mock
private ICcdiBaseStaffAssetImportService baseStaffAssetImportService;
@Test
void importTemplate_shouldDownloadDualSheetTemplate() {
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
controller.importTemplate(null);
mocked.verify(() -> EasyExcelUtil.importTemplateWithDictDropdown(
null,
CcdiBaseStaffExcel.class,
"员工信息",
CcdiBaseStaffAssetInfoExcel.class,
"员工资产信息",
"员工信息维护导入模板"
));
}
}
@Test
void importData_shouldWarnWhenBothSheetsAreEmpty() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file",
"base-staff-empty.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"empty".getBytes(StandardCharsets.UTF_8)
);
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiBaseStaffExcel.class), eq("员工信息")))
.thenReturn(List.of());
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiBaseStaffAssetInfoExcel.class), eq("员工资产信息")))
.thenReturn(List.of());
AjaxResult result = controller.importData(file);
assertEquals(HttpStatus.ERROR, result.get(AjaxResult.CODE_TAG));
assertEquals("至少需要一条数据", result.get(AjaxResult.MSG_TAG));
verifyNoInteractions(baseStaffService);
}
}
@Test
void importData_shouldSubmitOnlyStaffTaskWhenOnlyStaffSheetHasRows() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file",
"base-staff.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"staff".getBytes(StandardCharsets.UTF_8)
);
CcdiBaseStaffExcel staffExcel = new CcdiBaseStaffExcel();
staffExcel.setStaffId(1001L);
BaseStaffImportSubmitResultVO submitResult = new BaseStaffImportSubmitResultVO();
submitResult.setStaffTaskId("staff-task-1");
when(baseStaffService.importBaseStaffWithAssets(List.of(staffExcel), List.of())).thenReturn(submitResult);
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiBaseStaffExcel.class), eq("员工信息")))
.thenReturn(List.of(staffExcel));
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiBaseStaffAssetInfoExcel.class), eq("员工资产信息")))
.thenReturn(List.of());
AjaxResult result = controller.importData(file);
assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG));
assertEquals("导入任务已提交,正在后台处理", result.get(AjaxResult.MSG_TAG));
Object data = result.get(AjaxResult.DATA_TAG);
assertEquals("staff-task-1", data.getClass().getMethod("getStaffTaskId").invoke(data));
assertNull(data.getClass().getMethod("getAssetTaskId").invoke(data));
}
}
@Test
void importData_shouldSubmitTwoTasksWhenBothSheetsHaveRows() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file",
"base-staff-both.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"both".getBytes(StandardCharsets.UTF_8)
);
CcdiBaseStaffExcel staffExcel = new CcdiBaseStaffExcel();
staffExcel.setStaffId(1002L);
CcdiBaseStaffAssetInfoExcel assetExcel = new CcdiBaseStaffAssetInfoExcel();
assetExcel.setPersonId("320101199001010011");
BaseStaffImportSubmitResultVO submitResult = new BaseStaffImportSubmitResultVO();
submitResult.setStaffTaskId("staff-task-2");
submitResult.setAssetTaskId("asset-task-2");
when(baseStaffService.importBaseStaffWithAssets(List.of(staffExcel), List.of(assetExcel))).thenReturn(submitResult);
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiBaseStaffExcel.class), eq("员工信息")))
.thenReturn(List.of(staffExcel));
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiBaseStaffAssetInfoExcel.class), eq("员工资产信息")))
.thenReturn(List.of(assetExcel));
AjaxResult result = controller.importData(file);
assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG));
Object data = result.get(AjaxResult.DATA_TAG);
assertEquals("staff-task-2", data.getClass().getMethod("getStaffTaskId").invoke(data));
assertEquals("asset-task-2", data.getClass().getMethod("getAssetTaskId").invoke(data));
}
}
}

View File

@@ -0,0 +1,37 @@
package com.ruoyi.info.collection.controller;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.info.collection.domain.vo.EnumOptionVO;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
class CcdiEnumControllerTest {
private final CcdiEnumController controller = new CcdiEnumController();
@Test
void getEnterpriseRiskLevelOptions_shouldReturnConfiguredOptions() {
AjaxResult result = controller.getEnterpriseRiskLevelOptions();
List<?> data = (List<?>) result.get("data");
assertEquals(3, data.size());
EnumOptionVO first = (EnumOptionVO) data.get(0);
assertEquals("1", first.getValue());
assertEquals("高风险", first.getLabel());
}
@Test
void getEnterpriseSourceOptions_shouldReturnConfiguredOptions() {
AjaxResult result = controller.getEnterpriseSourceOptions();
List<?> data = (List<?>) result.get("data");
assertFalse(data.isEmpty());
EnumOptionVO first = (EnumOptionVO) data.get(0);
assertEquals("GENERAL", first.getValue());
assertEquals("一般企业", first.getLabel());
}
}

View File

@@ -0,0 +1,173 @@
package com.ruoyi.info.collection.controller;
import com.ruoyi.common.constant.HttpStatus;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.info.collection.domain.excel.CcdiAssetInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiStaffFmyRelationExcel;
import com.ruoyi.info.collection.domain.vo.StaffFmyRelationImportSubmitResultVO;
import com.ruoyi.info.collection.domain.vo.StaffFmyRelationImportFailureVO;
import com.ruoyi.info.collection.service.ICcdiAssetInfoImportService;
import com.ruoyi.info.collection.service.ICcdiStaffFmyRelationImportService;
import com.ruoyi.info.collection.service.ICcdiStaffFmyRelationService;
import com.ruoyi.info.collection.utils.EasyExcelUtil;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockMultipartFile;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CcdiStaffFmyRelationControllerTest {
@InjectMocks
private CcdiStaffFmyRelationController controller;
@Mock
private ICcdiStaffFmyRelationService relationService;
@Mock
private ICcdiStaffFmyRelationImportService relationImportService;
@Mock
private ICcdiAssetInfoImportService assetInfoImportService;
@Test
void importTemplate_shouldDownloadDualSheetTemplate() {
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
controller.importTemplate(null);
mocked.verify(() -> EasyExcelUtil.importTemplateWithDictDropdown(
null,
CcdiStaffFmyRelationExcel.class,
"员工亲属关系信息",
CcdiAssetInfoExcel.class,
"亲属资产信息",
"员工亲属关系维护导入模板"
));
}
}
@Test
void importData_shouldErrorWhenBothSheetsAreEmpty() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file",
"staff-family-empty.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"empty".getBytes(StandardCharsets.UTF_8)
);
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiStaffFmyRelationExcel.class), eq("员工亲属关系信息")))
.thenReturn(List.of());
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiAssetInfoExcel.class), eq("亲属资产信息")))
.thenReturn(List.of());
AjaxResult result = controller.importData(file);
assertEquals(HttpStatus.ERROR, result.get(AjaxResult.CODE_TAG));
assertEquals("至少需要一条数据", result.get(AjaxResult.MSG_TAG));
verifyNoInteractions(relationService, assetInfoImportService);
}
}
@Test
void importData_shouldSubmitOnlyRelationTaskWhenOnlyRelationSheetHasRows() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file",
"staff-family-relation.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"relation".getBytes(StandardCharsets.UTF_8)
);
CcdiStaffFmyRelationExcel relationExcel = new CcdiStaffFmyRelationExcel();
relationExcel.setPersonId("320101199001010011");
StaffFmyRelationImportSubmitResultVO submitResult = new StaffFmyRelationImportSubmitResultVO();
submitResult.setRelationTaskId("relation-task-1");
when(relationService.importRelationWithAssets(List.of(relationExcel), List.of())).thenReturn(submitResult);
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiStaffFmyRelationExcel.class), eq("员工亲属关系信息")))
.thenReturn(List.of(relationExcel));
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiAssetInfoExcel.class), eq("亲属资产信息")))
.thenReturn(List.of());
AjaxResult result = controller.importData(file);
assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG));
assertEquals("导入任务已提交,正在后台处理", result.get(AjaxResult.MSG_TAG));
Object data = result.get(AjaxResult.DATA_TAG);
assertEquals("relation-task-1", data.getClass().getMethod("getRelationTaskId").invoke(data));
assertNull(data.getClass().getMethod("getAssetTaskId").invoke(data));
}
}
@Test
void importData_shouldSubmitTwoTasksWhenBothSheetsHaveRows() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file",
"staff-family-both.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"both".getBytes(StandardCharsets.UTF_8)
);
CcdiStaffFmyRelationExcel relationExcel = new CcdiStaffFmyRelationExcel();
relationExcel.setPersonId("320101199001010012");
CcdiAssetInfoExcel assetExcel = new CcdiAssetInfoExcel();
assetExcel.setPersonId("320101199001010099");
StaffFmyRelationImportSubmitResultVO submitResult = new StaffFmyRelationImportSubmitResultVO();
submitResult.setRelationTaskId("relation-task-2");
submitResult.setAssetTaskId("asset-task-2");
when(relationService.importRelationWithAssets(List.of(relationExcel), List.of(assetExcel))).thenReturn(submitResult);
try (MockedStatic<EasyExcelUtil> mocked = mockStatic(EasyExcelUtil.class)) {
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiStaffFmyRelationExcel.class), eq("员工亲属关系信息")))
.thenReturn(List.of(relationExcel));
mocked.when(() -> EasyExcelUtil.importExcel(any(InputStream.class), eq(CcdiAssetInfoExcel.class), eq("亲属资产信息")))
.thenReturn(List.of(assetExcel));
AjaxResult result = controller.importData(file);
assertEquals(HttpStatus.SUCCESS, result.get(AjaxResult.CODE_TAG));
Object data = result.get(AjaxResult.DATA_TAG);
assertEquals("relation-task-2", data.getClass().getMethod("getRelationTaskId").invoke(data));
assertEquals("asset-task-2", data.getClass().getMethod("getAssetTaskId").invoke(data));
}
}
@Test
void getImportFailures_shouldReturnPagedRowsWithSheetAndRowInfo() {
StaffFmyRelationImportFailureVO failure1 = new StaffFmyRelationImportFailureVO();
failure1.setSheetName("员工亲属关系信息");
failure1.setRowNum(2);
failure1.setPersonId("A1");
StaffFmyRelationImportFailureVO failure2 = new StaffFmyRelationImportFailureVO();
failure2.setSheetName("员工亲属关系信息");
failure2.setRowNum(3);
failure2.setPersonId("A2");
when(relationImportService.getImportFailures("task-1")).thenReturn(List.of(failure1, failure2));
TableDataInfo result = controller.getImportFailures("task-1", 2, 1);
assertEquals(2, result.getTotal());
assertEquals(1, result.getRows().size());
StaffFmyRelationImportFailureVO row = (StaffFmyRelationImportFailureVO) result.getRows().get(0);
assertEquals("员工亲属关系信息", row.getSheetName());
assertEquals(3, row.getRowNum());
assertEquals("A2", row.getPersonId());
}
}

View File

@@ -0,0 +1,25 @@
package com.ruoyi.info.collection.mapper;
import org.junit.jupiter.api.Test;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiEnterpriseBaseInfoMapperTest {
@Test
void mapperXml_shouldContainPageQueryAndImportColumns() throws Exception {
try (InputStream inputStream = getClass().getClassLoader()
.getResourceAsStream("mapper/info/collection/CcdiEnterpriseBaseInfoMapper.xml")) {
String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
assertTrue(xml.contains("selectEnterpriseBaseInfoPage"), xml);
assertTrue(xml.contains("risk_level"), xml);
assertTrue(xml.contains("ent_source"), xml);
assertTrue(xml.contains("data_source"), xml);
assertTrue(xml.contains("ORDER BY create_time DESC"), xml);
}
}
}

View File

@@ -0,0 +1,27 @@
package com.ruoyi.info.collection.mapper;
import org.junit.jupiter.api.Test;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiIntermediaryMapperTest {
@Test
void mapperXml_shouldContainThreeRecordTypesAndRelatedKeywordQuery() throws Exception {
try (InputStream inputStream = getClass().getClassLoader()
.getResourceAsStream("mapper/info/collection/CcdiIntermediaryMapper.xml")) {
String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
assertTrue(xml.contains("INTERMEDIARY"), xml);
assertTrue(xml.contains("RELATIVE"), xml);
assertTrue(xml.contains("ENTERPRISE_RELATION"), xml);
assertTrue(xml.contains("relatedIntermediaryKeyword"), xml);
assertTrue(xml.contains("related_intermediary_name"), xml);
assertTrue(xml.contains("relation_text"), xml);
assertTrue(xml.contains("CAST('实体'"), xml);
}
}
}

View File

@@ -0,0 +1,163 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.info.collection.domain.CcdiBaseStaff;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel;
import com.ruoyi.info.collection.domain.vo.ImportFailureVO;
import com.ruoyi.info.collection.service.impl.CcdiBaseStaffImportServiceImpl;
import com.ruoyi.system.mapper.SysDeptMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CcdiBaseStaffDualImportServiceTest {
@InjectMocks
private CcdiBaseStaffImportServiceImpl service;
@Mock
private com.ruoyi.info.collection.mapper.CcdiBaseStaffMapper baseStaffMapper;
@Mock
private RedisTemplate<String, Object> redisTemplate;
@Mock
private HashOperations<String, Object, Object> hashOperations;
@Mock
private ValueOperations<String, Object> valueOperations;
@Mock
private SysDeptMapper deptMapper;
@Test
void importBaseStaffAsync_shouldTreatExistingEmployeeAsFailureInsteadOfUpdate() {
CcdiBaseStaffExcel excel = new CcdiBaseStaffExcel();
excel.setStaffId(1001L);
excel.setName("张三");
excel.setDeptId(10L);
excel.setIdCard("11010519491231002X");
excel.setPhone("13812345678");
excel.setStatus("0");
excel.setPartyMember(1);
CcdiBaseStaff existing = new CcdiBaseStaff();
existing.setStaffId(1001L);
existing.setIdCard("11010519491231002X");
when(baseStaffMapper.selectBatchIds(List.of(1001L))).thenReturn(List.of(existing));
when(baseStaffMapper.selectList(any())).thenReturn(List.of(existing));
lenient().when(deptMapper.selectDeptById(10L)).thenReturn(buildDept(10L, "0", "0"));
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
service.importBaseStaffAsync(List.of(excel), "task-existing");
verify(baseStaffMapper, never()).insertBatch(any());
verify(baseStaffMapper, never()).insertOrUpdateBatch(any());
ArgumentCaptor<Object> failureCaptor = ArgumentCaptor.forClass(Object.class);
verify(valueOperations).set(eq("import:baseStaff:task-existing:failures"), failureCaptor.capture(), eq(7L), eq(TimeUnit.DAYS));
ImportFailureVO failure = (ImportFailureVO) ((List<?>) failureCaptor.getValue()).get(0);
assertEquals("员工信息", failure.getSheetName());
assertEquals(2, failure.getRowNum());
assertEquals(1001L, failure.getStaffId());
assertEquals("该员工ID已存在", failure.getErrorMessage());
}
@Test
void validateStaffData_shouldRejectExistingIdCardWhenStaffIdDoesNotExist() {
when(deptMapper.selectDeptById(10L)).thenReturn(buildDept(10L, "0", "0"));
RuntimeException exception = org.junit.jupiter.api.Assertions.assertThrows(
RuntimeException.class,
() -> service.validateStaffData(buildExcelDto(), Set.of(), Set.of("11010519491231002X"))
);
assertEquals("该身份证号已存在", exception.getMessage());
}
@Test
void importBaseStaffAsync_shouldSaveFailureWhenDeptIsInvalid() {
CcdiBaseStaffExcel validExcel = buildExcel(1001L, 10L, "11010519491231002X");
CcdiBaseStaffExcel invalidExcel = buildExcel(1002L, 99L, "320101199001010014");
when(baseStaffMapper.selectBatchIds(List.of(1001L, 1002L))).thenReturn(List.of());
when(baseStaffMapper.selectList(any())).thenReturn(List.of());
when(deptMapper.selectDeptById(10L)).thenReturn(buildDept(10L, "0", "0"));
when(deptMapper.selectDeptById(99L)).thenReturn(null);
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(redisTemplate.opsForHash()).thenReturn(hashOperations);
service.importBaseStaffAsync(List.of(validExcel, invalidExcel), "task-invalid-dept");
verify(baseStaffMapper).insertBatch(any());
ArgumentCaptor<Object> failureCaptor = ArgumentCaptor.forClass(Object.class);
verify(valueOperations).set(eq("import:baseStaff:task-invalid-dept:failures"), failureCaptor.capture(), eq(7L), eq(TimeUnit.DAYS));
ImportFailureVO failure = (ImportFailureVO) ((List<?>) failureCaptor.getValue()).get(0);
assertEquals("员工信息", failure.getSheetName());
assertEquals(3, failure.getRowNum());
assertEquals(1002L, failure.getStaffId());
assertEquals("所属部门ID[99]不存在或已停用/删除,请检查机构号", failure.getErrorMessage());
ArgumentCaptor<Map<String, Object>> statusCaptor = ArgumentCaptor.forClass(Map.class);
verify(hashOperations).putAll(eq("import:baseStaff:task-invalid-dept"), statusCaptor.capture());
assertEquals("PARTIAL_SUCCESS", statusCaptor.getValue().get("status"));
assertEquals(1, statusCaptor.getValue().get("successCount"));
assertEquals(1, statusCaptor.getValue().get("failureCount"));
}
private com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO buildExcelDto() {
com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO dto = new com.ruoyi.info.collection.domain.dto.CcdiBaseStaffAddDTO();
dto.setName("李四");
dto.setStaffId(2001L);
dto.setDeptId(10L);
dto.setIdCard("11010519491231002X");
dto.setPhone("13812345678");
dto.setStatus("0");
dto.setPartyMember(1);
return dto;
}
private CcdiBaseStaffExcel buildExcel(Long staffId, Long deptId, String idCard) {
CcdiBaseStaffExcel excel = new CcdiBaseStaffExcel();
excel.setStaffId(staffId);
excel.setName("张三");
excel.setDeptId(deptId);
excel.setIdCard(idCard);
excel.setPhone("13812345678");
excel.setStatus("0");
excel.setPartyMember(1);
return excel;
}
private SysDept buildDept(Long deptId, String status, String delFlag) {
SysDept dept = new SysDept();
dept.setDeptId(deptId);
dept.setDeptName("测试部门");
dept.setStatus(status);
dept.setDelFlag(delFlag);
return dept;
}
}

View File

@@ -0,0 +1,113 @@
package com.ruoyi.info.collection.service;
import com.ruoyi.info.collection.domain.CcdiBizIntermediary;
import com.ruoyi.info.collection.domain.dto.CcdiIntermediaryRelativeAddDTO;
import com.ruoyi.info.collection.domain.dto.CcdiIntermediaryPersonAddDTO;
import com.ruoyi.info.collection.mapper.CcdiBizIntermediaryMapper;
import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper;
import com.ruoyi.info.collection.mapper.CcdiIntermediaryEnterpriseRelationMapper;
import com.ruoyi.info.collection.mapper.CcdiIntermediaryMapper;
import com.ruoyi.info.collection.service.impl.CcdiIntermediaryServiceImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.core.RedisTemplate;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CcdiIntermediaryServiceImplTest {
@InjectMocks
private CcdiIntermediaryServiceImpl service;
@Mock
private CcdiBizIntermediaryMapper bizIntermediaryMapper;
@Mock
private CcdiEnterpriseBaseInfoMapper enterpriseBaseInfoMapper;
@Mock
private CcdiIntermediaryMapper intermediaryMapper;
@Mock
private CcdiIntermediaryEnterpriseRelationMapper enterpriseRelationMapper;
@Mock
private ICcdiIntermediaryPersonImportService personImportService;
@Mock
private ICcdiIntermediaryEntityImportService entityImportService;
@Mock
private RedisTemplate<String, Object> redisTemplate;
@Test
void insertIntermediaryPerson_shouldForceBenrenAndClearRelatedNumId() {
CcdiIntermediaryPersonAddDTO addDTO = new CcdiIntermediaryPersonAddDTO();
addDTO.setName("测试中介");
addDTO.setPersonId("320101199001010011");
addDTO.setPersonSubType("配偶");
addDTO.setRelatedNumId("parent-id");
when(bizIntermediaryMapper.selectCount(any())).thenReturn(0L);
when(bizIntermediaryMapper.insert(any(CcdiBizIntermediary.class))).thenReturn(1);
int result = service.insertIntermediaryPerson(addDTO);
assertEquals(1, result);
ArgumentCaptor<CcdiBizIntermediary> captor = ArgumentCaptor.forClass(CcdiBizIntermediary.class);
verify(bizIntermediaryMapper).insert(captor.capture());
assertEquals("本人", captor.getValue().getPersonSubType());
assertNull(captor.getValue().getRelatedNumId());
assertEquals("MANUAL", captor.getValue().getDataSource());
}
@Test
void insertIntermediaryRelative_shouldRejectBenrenSubType() {
CcdiBizIntermediary owner = new CcdiBizIntermediary();
owner.setBizId("biz-1");
owner.setPersonSubType("本人");
CcdiIntermediaryRelativeAddDTO addDTO = new CcdiIntermediaryRelativeAddDTO();
addDTO.setName("测试亲属");
addDTO.setPersonId("320101199001010022");
addDTO.setPersonSubType("本人");
when(bizIntermediaryMapper.selectById("biz-1")).thenReturn(owner);
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.insertIntermediaryRelative("biz-1", addDTO));
assertEquals("亲属关系不能为本人", exception.getMessage());
verify(bizIntermediaryMapper, never()).insert(any(CcdiBizIntermediary.class));
}
@Test
void deleteIntermediaryByIds_shouldDeleteRelativesAndEnterpriseRelationsWhenRemovingOwner() {
CcdiBizIntermediary owner = new CcdiBizIntermediary();
owner.setBizId("biz-1");
owner.setPersonSubType("本人");
when(bizIntermediaryMapper.selectById("biz-1")).thenReturn(owner);
when(bizIntermediaryMapper.delete(any())).thenReturn(2);
when(enterpriseRelationMapper.delete(any())).thenReturn(1);
when(bizIntermediaryMapper.deleteById("biz-1")).thenReturn(1);
int result = service.deleteIntermediaryByIds(new String[]{"biz-1"});
assertEquals(1, result);
verify(bizIntermediaryMapper).delete(any());
verify(enterpriseRelationMapper).delete(any());
verify(bizIntermediaryMapper).deleteById("biz-1");
}
}

View File

@@ -0,0 +1,78 @@
package com.ruoyi.info.collection.service;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiPurchaseTransactionFeatureContractTest {
@Test
void shouldExposeSupplierListContractsAcrossPurchaseTransactionModels() throws Exception {
assertHasField(
"com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionAddDTO",
"supplierList"
);
assertHasField(
"com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionEditDTO",
"supplierList"
);
assertHasField(
"com.ruoyi.info.collection.domain.vo.CcdiPurchaseTransactionVO",
"supplierList"
);
assertHasField(
"com.ruoyi.info.collection.domain.vo.CcdiPurchaseTransactionVO",
"supplierCount"
);
assertNotNull(Class.forName("com.ruoyi.info.collection.domain.CcdiPurchaseTransactionSupplier"));
assertNotNull(Class.forName("com.ruoyi.info.collection.domain.dto.CcdiPurchaseTransactionSupplierDTO"));
assertNotNull(Class.forName("com.ruoyi.info.collection.domain.vo.CcdiPurchaseTransactionSupplierVO"));
assertNotNull(Class.forName("com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionSupplierExcel"));
}
@Test
void shouldDefineSupplierSubTableAndBiddingMigrationScripts() throws Exception {
String initSql = Files.readString(repoPath("sql/ccdi_purchase_transaction.sql"));
assertTrue(initSql.contains("CREATE TABLE `ccdi_purchase_transaction_supplier`"));
assertTrue(initSql.contains("`is_bid_winner`"));
assertTrue(initSql.contains("`sort_order`"));
assertTrue(initSql.contains("utf8mb4_general_ci"));
String menuSql = Files.readString(repoPath("sql/ccdi_purchase_transaction_menu.sql"));
assertTrue(menuSql.contains("招投标信息维护"));
Path migrationPath = repoPath("sql/migration/2026-04-22-bidding-info-maintenance-supplier-detail.sql");
assertTrue(Files.exists(migrationPath), "应提供招投标供应商明细迁移脚本");
String migrationSql = Files.readString(migrationPath);
assertTrue(migrationSql.contains("ccdi_purchase_transaction_supplier"));
assertTrue(migrationSql.contains("INSERT INTO ccdi_purchase_transaction_supplier"));
assertTrue(migrationSql.contains("UPDATE sys_menu"));
assertTrue(migrationSql.contains("招投标信息维护"));
}
@Test
void shouldUseTwoSheetTemplateForBiddingImport() throws Exception {
assertNotNull(Class.forName("com.ruoyi.info.collection.domain.excel.CcdiPurchaseTransactionSupplierExcel"));
String controller = Files.readString(
Path.of("src/main/java/com/ruoyi/info/collection/controller/CcdiPurchaseTransactionController.java")
);
assertTrue(controller.contains("招投标主信息"));
assertTrue(controller.contains("供应商明细"));
}
private void assertHasField(String className, String fieldName) throws Exception {
Class<?> clazz = Class.forName(className);
Field field = clazz.getDeclaredField(fieldName);
assertNotNull(field);
}
private Path repoPath(String relativePath) {
return Path.of("..", relativePath);
}
}

View File

@@ -0,0 +1,248 @@
package com.ruoyi.info.collection.utils;
import com.alibaba.excel.annotation.ExcelProperty;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffAssetInfoExcel;
import com.ruoyi.info.collection.domain.excel.CcdiBaseStaffExcel;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.DataValidation;
import org.apache.poi.ss.usermodel.DataValidationConstraint;
import org.apache.poi.ss.usermodel.DataValidationHelper;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.math.BigDecimal;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class EasyExcelUtilImportDropdownValidationTest {
@Test
void importExcel_shouldPassWhenAllDictDropdownColumnsKeepListValidation() throws Exception {
byte[] bytes = baseStaffWorkbook(true, true, true, 2);
List<CcdiBaseStaffExcel> rows = EasyExcelUtil.importExcel(
new ByteArrayInputStream(bytes),
CcdiBaseStaffExcel.class,
"员工信息"
);
assertEquals(2, rows.size());
}
@Test
void importExcel_shouldFailWhenPartyMemberDropdownIsMissing() throws Exception {
byte[] bytes = baseStaffWorkbook(false, true, true, 2);
ServiceException exception = assertThrows(ServiceException.class, () ->
EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffExcel.class, "员工信息")
);
assertTrue(exception.getMessage().contains("是否党员 列缺少下拉框"));
}
@Test
void importExcel_shouldFailWhenStatusDropdownIsMissing() throws Exception {
byte[] bytes = baseStaffWorkbook(true, false, true, 2);
ServiceException exception = assertThrows(ServiceException.class, () ->
EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffExcel.class, "员工信息")
);
assertEquals("员工信息 Sheet 的 状态 列缺少下拉框,请下载最新导入模板填写后重新导入", exception.getMessage());
}
@Test
void importExcel_shouldReportAllMissingDropdownColumnsInSameSheet() throws Exception {
byte[] bytes = baseStaffWorkbook(false, false, true, 2);
ServiceException exception = assertThrows(ServiceException.class, () ->
EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffExcel.class, "员工信息")
);
assertEquals("员工信息 Sheet 的 是否党员、状态 列缺少下拉框,请下载最新导入模板填写后重新导入", exception.getMessage());
}
@Test
void importExcel_shouldFailWhenValidationIsNotListType() throws Exception {
byte[] bytes = baseStaffWorkbook(true, true, false, 2);
ServiceException exception = assertThrows(ServiceException.class, () ->
EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffExcel.class, "员工信息")
);
assertTrue(exception.getMessage().contains("状态 列缺少下拉框"));
}
@Test
void importExcel_shouldFailWhenListValidationDoesNotCoverEveryActualDataRow() throws Exception {
byte[] bytes = baseStaffWorkbook(true, true, true, 1);
ServiceException exception = assertThrows(ServiceException.class, () ->
EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffExcel.class, "员工信息")
);
assertTrue(exception.getMessage().contains("状态 列缺少下拉框"));
}
@Test
void importExcel_shouldFailWhenSecondSheetDropdownIsMissing() throws Exception {
byte[] bytes = baseStaffDualSheetWorkbookWithMissingAssetStatusDropdown();
ServiceException exception = assertThrows(ServiceException.class, () ->
EasyExcelUtil.importExcel(new ByteArrayInputStream(bytes), CcdiBaseStaffAssetInfoExcel.class, "员工资产信息")
);
assertEquals("员工资产信息 Sheet 的 资产状态 列缺少下拉框,请下载最新导入模板填写后重新导入", exception.getMessage());
}
@Test
void importExcel_shouldSkipDropdownStructureValidationWhenClassHasNoDictDropdownFields() throws Exception {
byte[] bytes = plainWorkbookWithoutDropdown();
List<PlainExcel> rows = EasyExcelUtil.importExcel(
new ByteArrayInputStream(bytes),
PlainExcel.class,
"普通信息"
);
assertEquals(1, rows.size());
}
private byte[] baseStaffWorkbook(boolean partyDropdown, boolean statusDropdown, boolean statusAsList,
int statusLastRow) throws Exception {
try (Workbook workbook = new XSSFWorkbook();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
Sheet sheet = workbook.createSheet("员工信息");
Row header = sheet.createRow(0);
String[] headers = {"姓名", "员工ID", "所属部门ID", "身份证号", "电话", "年收入(元/年)",
"入职时间", "是否党员", "状态"};
for (int i = 0; i < headers.length; i++) {
header.createCell(i).setCellValue(headers[i]);
}
createBaseStaffRow(sheet, 1, "张三", 9020001L, "33010619850202101X", "0", "1");
createBaseStaffRow(sheet, 2, "李四", 9020002L, "330106198603031022", "1", "1");
if (partyDropdown) {
addListValidation(sheet, 7, 1, 2, "0", "1");
}
if (statusDropdown) {
if (statusAsList) {
addListValidation(sheet, 8, 1, statusLastRow, "0", "1");
} else {
addIntegerValidation(sheet, 8, 1, 2);
}
}
workbook.write(outputStream);
return outputStream.toByteArray();
}
}
private byte[] baseStaffDualSheetWorkbookWithMissingAssetStatusDropdown() throws Exception {
try (Workbook workbook = new XSSFWorkbook();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
Sheet staffSheet = workbook.createSheet("员工信息");
Row staffHeader = staffSheet.createRow(0);
String[] staffHeaders = {"姓名", "员工ID", "所属部门ID", "身份证号", "电话", "年收入(元/年)",
"入职时间", "是否党员", "状态"};
for (int i = 0; i < staffHeaders.length; i++) {
staffHeader.createCell(i).setCellValue(staffHeaders[i]);
}
createBaseStaffRow(staffSheet, 1, "张三", 9020001L, "33010619850202101X", "0", "1");
addListValidation(staffSheet, 7, 1, 1, "0", "1");
addListValidation(staffSheet, 8, 1, 1, "0", "1");
Sheet assetSheet = workbook.createSheet("员工资产信息");
Row assetHeader = assetSheet.createRow(0);
String[] assetHeaders = {"员工身份证号*", "资产大类*", "资产小类*", "资产名称*", "产权占比",
"购买/评估日期", "资产原值", "当前估值*", "估值截止日期", "资产状态*", "备注"};
for (int i = 0; i < assetHeaders.length; i++) {
assetHeader.createCell(i).setCellValue(assetHeaders[i]);
}
Row assetRow = assetSheet.createRow(1);
assetRow.createCell(0).setCellValue("33010619850202101X");
assetRow.createCell(1).setCellValue("房产");
assetRow.createCell(2).setCellValue("住宅");
assetRow.createCell(3).setCellValue("测试住宅");
assetRow.createCell(7).setCellValue(1000000D);
assetRow.createCell(9).setCellValue("正常");
workbook.write(outputStream);
return outputStream.toByteArray();
}
}
private void createBaseStaffRow(Sheet sheet, int rowIndex, String name, long staffId, String idCard,
String partyMember, String status) {
Row row = sheet.createRow(rowIndex);
row.createCell(0).setCellValue(name);
row.createCell(1).setCellValue(staffId);
row.createCell(2).setCellValue(103L);
row.createCell(3, CellType.STRING).setCellValue(idCard);
row.createCell(4, CellType.STRING).setCellValue("13370000001");
row.createCell(5).setCellValue(new BigDecimal("180000").doubleValue());
row.createCell(6).setCellValue("2026-04-30");
row.createCell(7, CellType.STRING).setCellValue(partyMember);
row.createCell(8, CellType.STRING).setCellValue(status);
}
private void addListValidation(Sheet sheet, int columnIndex, int firstRow, int lastRow, String... options) {
DataValidationHelper helper = sheet.getDataValidationHelper();
DataValidationConstraint constraint = helper.createExplicitListConstraint(options);
DataValidation validation = helper.createValidation(
constraint,
new CellRangeAddressList(firstRow, lastRow, columnIndex, columnIndex)
);
sheet.addValidationData(validation);
}
private void addIntegerValidation(Sheet sheet, int columnIndex, int firstRow, int lastRow) {
DataValidationHelper helper = sheet.getDataValidationHelper();
DataValidationConstraint constraint = helper.createIntegerConstraint(
DataValidationConstraint.OperatorType.BETWEEN,
"0",
"1"
);
DataValidation validation = helper.createValidation(
constraint,
new CellRangeAddressList(firstRow, lastRow, columnIndex, columnIndex)
);
sheet.addValidationData(validation);
}
private byte[] plainWorkbookWithoutDropdown() throws Exception {
try (Workbook workbook = new XSSFWorkbook();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
Sheet sheet = workbook.createSheet("普通信息");
Row header = sheet.createRow(0);
header.createCell(0).setCellValue("名称");
Row row = sheet.createRow(1);
row.createCell(0).setCellValue("张三");
workbook.write(outputStream);
return outputStream.toByteArray();
}
}
public static class PlainExcel {
@ExcelProperty(value = "名称", index = 0)
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}

View File

@@ -0,0 +1,61 @@
package com.ruoyi.lsfx.client;
import com.ruoyi.lsfx.constants.LsfxConstants;
import com.ruoyi.lsfx.domain.response.UploadFileResponse;
import com.ruoyi.lsfx.util.HttpUtil;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.core.io.Resource;
import org.springframework.test.util.ReflectionTestUtils;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class LsfxAnalysisClientTest {
@Mock
private HttpUtil httpUtil;
@InjectMocks
private LsfxAnalysisClient client;
@TempDir
Path tempDir;
@Test
void uploadFile_shouldPassOriginalFilenameToMultipartResource() throws Exception {
ReflectionTestUtils.setField(client, "baseUrl", "http://lsfx");
ReflectionTestUtils.setField(client, "uploadFileEndpoint", "/upload");
ReflectionTestUtils.setField(client, "clientId", "client-1");
Path tempFile = tempDir.resolve("batch_0_123456.xlsx");
Files.writeString(tempFile, "content");
UploadFileResponse response = new UploadFileResponse();
response.setData(new UploadFileResponse.UploadData());
ArgumentCaptor<Map<String, Object>> paramsCaptor = ArgumentCaptor.forClass(Map.class);
ArgumentCaptor<Map<String, String>> headersCaptor = ArgumentCaptor.forClass(Map.class);
when(httpUtil.uploadFile(eq("http://lsfx/upload"), paramsCaptor.capture(), headersCaptor.capture(), eq(UploadFileResponse.class)))
.thenReturn(response);
client.uploadFile(200, tempFile.toFile(), "银行流水A.xlsx");
assertEquals(200, paramsCaptor.getValue().get("groupId"));
Resource filePart = assertInstanceOf(Resource.class, paramsCaptor.getValue().get("files"));
assertEquals("银行流水A.xlsx", filePart.getFilename());
assertEquals("client-1", headersCaptor.getValue().get(LsfxConstants.HEADER_CLIENT_ID));
}
}

View File

@@ -0,0 +1,59 @@
package com.ruoyi.lsfx.util;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class HttpUtilTest {
@Mock
private RestTemplate restTemplate;
@TempDir
Path tempDir;
@Test
void uploadFile_shouldUseExplicitResourceFilename() throws Exception {
HttpUtil httpUtil = new HttpUtil();
ReflectionTestUtils.setField(httpUtil, "restTemplate", restTemplate);
Path tempFile = tempDir.resolve("batch_0_123456.xlsx");
Files.writeString(tempFile, "content");
ArgumentCaptor<HttpEntity> captor = ArgumentCaptor.forClass(HttpEntity.class);
when(restTemplate.postForEntity(eq("http://lsfx/upload"), captor.capture(), eq(String.class)))
.thenReturn(ResponseEntity.ok("ok"));
Map<String, Object> params = new HashMap<>();
params.put("groupId", 200);
params.put("files", HttpUtil.namedFileResource(tempFile.toFile(), "银行流水A.xlsx"));
String result = httpUtil.uploadFile("http://lsfx/upload", params, null, String.class);
assertEquals("ok", result);
MultiValueMap<String, Object> body = (MultiValueMap<String, Object>) captor.getValue().getBody();
Object filePart = body.getFirst("files");
Resource resource = assertInstanceOf(Resource.class, filePart);
assertEquals("银行流水A.xlsx", resource.getFilename());
}
}

View File

@@ -0,0 +1 @@
mock-maker-subclass

View File

@@ -10,6 +10,7 @@ public final class CcdiProjectStatusConstants {
public static final String ARCHIVED = "2";
public static final String TAGGING = "3";
public static final String TAG_FAILED = "4";
public static final String DELETED = "5";
private CcdiProjectStatusConstants() {
}

View File

@@ -6,6 +6,7 @@ import com.ruoyi.ccdi.project.domain.excel.CcdiBankStatementExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementFilterOptionsVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.ccdi.project.service.ICcdiBankStatementService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
@@ -39,13 +40,16 @@ public class CcdiBankStatementController extends BaseController {
@Resource
private ICcdiBankStatementService bankStatementService;
@Resource
private CcdiProjectAccessService projectAccessService;
/**
* 分页查询流水明细
*/
@GetMapping("/list")
@Operation(summary = "分页查询流水明细")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public TableDataInfo list(CcdiBankStatementQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
PageDomain pageDomain = TableSupport.buildPageRequest();
Page<CcdiBankStatementListVO> page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize());
Page<CcdiBankStatementListVO> result = bankStatementService.selectStatementPage(page, queryDTO);
@@ -57,8 +61,8 @@ public class CcdiBankStatementController extends BaseController {
*/
@GetMapping("/options")
@Operation(summary = "查询项目级筛选项")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getOptions(Long projectId) {
projectAccessService.assertCanRead(projectId);
CcdiBankStatementFilterOptionsVO options = bankStatementService.getFilterOptions(projectId);
return AjaxResult.success(options);
}
@@ -68,8 +72,8 @@ public class CcdiBankStatementController extends BaseController {
*/
@GetMapping("/detail/{bankStatementId}")
@Operation(summary = "查询流水详情")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getDetail(@PathVariable Long bankStatementId) {
projectAccessService.assertCanReadByBankStatementId(bankStatementId);
CcdiBankStatementDetailVO detail = bankStatementService.getStatementDetail(bankStatementId);
return AjaxResult.success(detail);
}
@@ -81,6 +85,7 @@ public class CcdiBankStatementController extends BaseController {
@Operation(summary = "导出流水明细")
@PreAuthorize("@ss.hasPermi('ccdi:project:export')")
public void export(HttpServletResponse response, CcdiBankStatementQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
List<CcdiBankStatementExcel> list = bankStatementService.selectStatementListForExport(queryDTO);
ExcelUtil<CcdiBankStatementExcel> util = new ExcelUtil<>(CcdiBankStatementExcel.class);
util.exportExcel(response, list, "流水明细");

View File

@@ -1,6 +1,7 @@
package com.ruoyi.ccdi.project.controller;
import com.ruoyi.ccdi.project.domain.dto.CcdiBankTagRebuildDTO;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.ccdi.project.service.ICcdiBankTagService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
@@ -9,6 +10,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -27,12 +29,17 @@ public class CcdiBankTagController extends BaseController {
@Resource
private ICcdiBankTagService bankTagService;
@Resource
private CcdiProjectAccessService projectAccessService;
/**
* 手动提交流水标签重算任务
*/
@Operation(summary = "手动重算项目流水标签")
@PostMapping("/rebuild")
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
public AjaxResult rebuild(@Validated @RequestBody CcdiBankTagRebuildDTO dto) {
projectAccessService.assertCanOperate(dto.getProjectId());
String operator = SecurityUtils.getUsername();
log.info("【流水标签】收到手动重算请求: projectId={}, modelCode={}, operator={}",
dto.getProjectId(), dto.getModelCode(), operator);

View File

@@ -3,6 +3,7 @@ package com.ruoyi.ccdi.project.controller;
import com.ruoyi.ccdi.project.domain.dto.CcdiEvidenceQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiEvidenceSaveDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiEvidenceVO;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.ccdi.project.service.ICcdiEvidenceService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
@@ -34,13 +35,17 @@ public class CcdiEvidenceController extends BaseController {
@Resource
private ICcdiEvidenceService evidenceService;
@Resource
private CcdiProjectAccessService projectAccessService;
/**
* 保存证据
*/
@PostMapping
@Operation(summary = "保存证据")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
public AjaxResult saveEvidence(@Validated @RequestBody CcdiEvidenceSaveDTO dto) {
projectAccessService.assertCanOperate(dto.getProjectId());
CcdiEvidenceVO vo = evidenceService.saveEvidence(dto, SecurityUtils.getUsername());
return AjaxResult.success("证据入库成功", vo);
}
@@ -50,8 +55,8 @@ public class CcdiEvidenceController extends BaseController {
*/
@GetMapping("/list")
@Operation(summary = "查询项目证据列表")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult listEvidence(CcdiEvidenceQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
List<CcdiEvidenceVO> list = evidenceService.listEvidence(queryDTO);
return AjaxResult.success(list);
}
@@ -61,8 +66,8 @@ public class CcdiEvidenceController extends BaseController {
*/
@GetMapping("/{evidenceId}")
@Operation(summary = "查询证据详情")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getEvidence(@PathVariable Long evidenceId) {
projectAccessService.assertCanReadByEvidenceId(evidenceId);
CcdiEvidenceVO vo = evidenceService.getEvidence(evidenceId);
return AjaxResult.success(vo);
}

View File

@@ -6,6 +6,7 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiPullBankInfoSubmitDTO;
import com.ruoyi.ccdi.project.domain.entity.CcdiFileUploadRecord;
import com.ruoyi.ccdi.project.domain.vo.CcdiFileUploadStatisticsVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiIdCardParseVO;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.ccdi.project.service.ICcdiFileUploadService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
@@ -17,6 +18,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
@@ -40,17 +42,22 @@ public class CcdiFileUploadController extends BaseController {
@Resource
private ICcdiFileUploadService fileUploadService;
@Resource
private CcdiProjectAccessService projectAccessService;
/**
* 批量上传文件(异步)
*/
@PostMapping("/batch")
@Operation(summary = "批量上传文件", description = "异步批量上传流水文件")
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
public AjaxResult batchUpload(@RequestParam Long projectId,
@RequestParam MultipartFile[] files) {
// 参数校验
if (projectId == null) {
return AjaxResult.error("项目ID不能为空");
}
projectAccessService.assertCanOperate(projectId);
if (files == null || files.length == 0) {
return AjaxResult.error("请选择要上传的文件");
}
@@ -58,7 +65,7 @@ public class CcdiFileUploadController extends BaseController {
return AjaxResult.error("单次最多上传100个文件");
}
// 校验文件大小和格式
// 校验文件数量、空文件和大小,文件名业务规则统一在 Service 层处理
for (MultipartFile file : files) {
if (file.isEmpty()) {
return AjaxResult.error("文件不能为空");
@@ -66,15 +73,6 @@ public class CcdiFileUploadController extends BaseController {
if (file.getSize() > 50 * 1024 * 1024) {
return AjaxResult.error("文件 " + file.getOriginalFilename() + " 超过50MB限制");
}
String fileName = file.getOriginalFilename();
if (fileName == null || fileName.trim().isEmpty()) {
return AjaxResult.error("文件名不能为空");
}
String lowerFileName = fileName.toLowerCase();
if (!lowerFileName.endsWith(".xlsx") && !lowerFileName.endsWith(".csv")
&& !lowerFileName.endsWith(".pdf")) {
return AjaxResult.error("文件 " + fileName + " 格式不支持, 仅支持 PDF, CSV, XLSX 文件");
}
}
try {
@@ -95,6 +93,7 @@ public class CcdiFileUploadController extends BaseController {
*/
@PostMapping("/parse-id-card-file")
@Operation(summary = "解析身份证文件", description = "解析首个sheet第一列的身份证号")
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
public AjaxResult parseIdCardFile(@RequestParam MultipartFile file) {
if (file == null || file.isEmpty()) {
return AjaxResult.error("身份证文件不能为空");
@@ -108,10 +107,12 @@ public class CcdiFileUploadController extends BaseController {
*/
@PostMapping("/pull-bank-info")
@Operation(summary = "拉取本行信息", description = "按身份证号批量提交拉取本行信息任务")
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
public AjaxResult pullBankInfo(@RequestBody CcdiPullBankInfoSubmitDTO dto) {
if (dto == null || dto.getProjectId() == null) {
return AjaxResult.error("项目ID不能为空");
}
projectAccessService.assertCanOperate(dto.getProjectId());
if (CollectionUtils.isEmpty(dto.getIdCards())) {
return AjaxResult.error("身份证号不能为空");
}
@@ -138,6 +139,7 @@ public class CcdiFileUploadController extends BaseController {
@GetMapping("/list")
@Operation(summary = "查询上传记录列表", description = "分页查询文件上传记录")
public TableDataInfo list(CcdiFileUploadQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
PageDomain pageDomain = TableSupport.buildPageRequest();
Page<CcdiFileUploadRecord> page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize());
Page<CcdiFileUploadRecord> result = fileUploadService.selectPage(page, queryDTO);
@@ -150,6 +152,7 @@ public class CcdiFileUploadController extends BaseController {
@GetMapping("/statistics/{projectId}")
@Operation(summary = "查询上传统计", description = "统计各状态的文件数量")
public AjaxResult getStatistics(@PathVariable Long projectId) {
projectAccessService.assertCanRead(projectId);
CcdiFileUploadStatisticsVO statistics = fileUploadService.countByStatus(projectId);
return AjaxResult.success(statistics);
}
@@ -160,6 +163,7 @@ public class CcdiFileUploadController extends BaseController {
@GetMapping("/detail/{id}")
@Operation(summary = "查询记录详情", description = "根据ID查询文件上传记录详情")
public AjaxResult getDetail(@PathVariable Long id) {
projectAccessService.assertCanReadByFileRecordId(id);
CcdiFileUploadRecord record = fileUploadService.getById(id);
return AjaxResult.success(record);
}
@@ -169,7 +173,9 @@ public class CcdiFileUploadController extends BaseController {
*/
@DeleteMapping("/{id}")
@Operation(summary = "删除上传文件", description = "按上传记录ID删除文件并清理流水")
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
public AjaxResult deleteFile(@PathVariable Long id) {
projectAccessService.assertCanOperateByFileRecordId(id);
Long userId = SecurityUtils.getUserId();
String message = fileUploadService.deleteFileUploadRecord(id, userId);
return AjaxResult.success(message);

View File

@@ -8,6 +8,7 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiFundGraphEdgeVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiFundGraphNodeVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiFundGraphStatementVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiFundGraphVO;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.ccdi.project.service.ICcdiFundGraphService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
@@ -38,10 +39,14 @@ public class CcdiFundGraphController extends BaseController {
@Resource
private ICcdiFundGraphService fundGraphService;
@Resource
private CcdiProjectAccessService projectAccessService;
@GetMapping("/search")
@Operation(summary = "查询资金流图谱主体")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult searchSubjects(CcdiFundGraphQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
List<CcdiFundGraphNodeVO> subjects = fundGraphService.searchSubjects(queryDTO);
return AjaxResult.success(subjects);
}
@@ -50,6 +55,7 @@ public class CcdiFundGraphController extends BaseController {
@Operation(summary = "查询一层资金流图谱")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getGraph(CcdiFundGraphQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiFundGraphVO graph = fundGraphService.getFundGraph(queryDTO);
return AjaxResult.success(graph);
}
@@ -58,6 +64,7 @@ public class CcdiFundGraphController extends BaseController {
@Operation(summary = "查询资金边流水明细")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public TableDataInfo getEdgeDetail(CcdiFundGraphEdgeDetailQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
PageDomain pageDomain = TableSupport.buildPageRequest();
Page<CcdiFundGraphStatementVO> page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize());
Page<CcdiFundGraphStatementVO> result = fundGraphService.getEdgeDetails(page, queryDTO);
@@ -66,9 +73,10 @@ public class CcdiFundGraphController extends BaseController {
@PostMapping("/manual-edge")
@Operation(summary = "新增手工资金流向")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
public AjaxResult saveManualEdge(@RequestBody CcdiFundGraphManualEdgeSaveDTO saveDTO) {
try {
projectAccessService.assertCanOperate(saveDTO == null ? null : saveDTO.getProjectId());
CcdiFundGraphEdgeVO edge = fundGraphService.saveManualEdge(saveDTO, SecurityUtils.getUsername());
return AjaxResult.success(edge);
} catch (IllegalArgumentException e) {

View File

@@ -11,9 +11,11 @@ import com.ruoyi.ccdi.project.domain.dto.ModelParamSaveAllDTO;
import com.ruoyi.ccdi.project.domain.vo.ModelListVO;
import com.ruoyi.ccdi.project.domain.vo.ModelParamVO;
import com.ruoyi.ccdi.project.domain.vo.ModelParamAllVO;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.ccdi.project.service.ICcdiModelParamService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -31,12 +33,17 @@ public class CcdiModelParamController extends BaseController {
@Resource
private ICcdiModelParamService modelParamService;
@Resource
private CcdiProjectAccessService projectAccessService;
/**
* 查询模型列表
*/
@Operation(summary = "查询模型列表")
@GetMapping("/modelList")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult listModels(@RequestParam(required = false) Long projectId) {
assertCanReadProjectParam(projectId);
List<ModelListVO> list = modelParamService.selectModelList(projectId);
return success(list);
}
@@ -46,7 +53,9 @@ public class CcdiModelParamController extends BaseController {
*/
@Operation(summary = "查询模型参数列表")
@GetMapping("/list")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult list(@Validated ModelParamQueryDTO queryDTO) {
assertCanReadProjectParam(queryDTO.getProjectId());
List<ModelParamVO> list = modelParamService.selectParamList(queryDTO);
return success(list);
}
@@ -57,7 +66,9 @@ public class CcdiModelParamController extends BaseController {
@Operation(summary = "保存模型参数")
@Log(title = "模型参数配置", businessType = BusinessType.UPDATE)
@PostMapping("/save")
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
public AjaxResult save(@Validated @RequestBody ModelParamSaveDTO saveDTO) {
assertCanOperateProjectParam(saveDTO.getProjectId());
modelParamService.saveParams(saveDTO);
return success("保存成功");
}
@@ -67,7 +78,9 @@ public class CcdiModelParamController extends BaseController {
*/
@Operation(summary = "查询所有模型及其参数")
@GetMapping("/listAll")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult listAll(@Validated ModelParamAllQueryDTO queryDTO) {
assertCanReadProjectParam(queryDTO.getProjectId());
ModelParamAllVO result = modelParamService.selectAllParams(queryDTO.getProjectId());
return success(result);
}
@@ -78,8 +91,24 @@ public class CcdiModelParamController extends BaseController {
@Operation(summary = "批量保存所有模型参数")
@Log(title = "模型参数配置", businessType = BusinessType.UPDATE)
@PostMapping("/saveAll")
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
public AjaxResult saveAll(@Validated @RequestBody ModelParamSaveAllDTO saveAllDTO) {
assertCanOperateProjectParam(saveAllDTO.getProjectId());
modelParamService.saveAllParams(saveAllDTO);
return success("保存成功");
}
private void assertCanReadProjectParam(Long projectId) {
if (projectId == null || projectId <= 0) {
return;
}
projectAccessService.assertCanRead(projectId);
}
private void assertCanOperateProjectParam(Long projectId) {
if (projectId == null || projectId <= 0) {
return;
}
projectAccessService.assertCanOperate(projectId);
}
}

View File

@@ -74,18 +74,28 @@ public class CcdiProjectController extends BaseController {
*/
@DeleteMapping("/{projectId}")
@Operation(summary = "删除项目")
@PreAuthorize("@ss.hasPermi('ccdi:project:remove')")
@PreAuthorize("@ss.hasPermi('ccdi:project:list')")
public AjaxResult deleteProject(@PathVariable Long projectId) {
boolean success = projectService.deleteProject(projectId);
boolean success = projectService.deleteProject(projectId, SecurityUtils.getUsername());
return success ? AjaxResult.success("项目删除成功") : AjaxResult.error("项目删除失败");
}
/**
* 恢复项目
*/
@PostMapping("/{projectId}/restore")
@Operation(summary = "恢复项目")
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
public AjaxResult restoreProject(@PathVariable Long projectId) {
projectService.restoreProject(projectId, SecurityUtils.getUsername());
return AjaxResult.success("项目恢复成功");
}
/**
* 查询项目详情
*/
@GetMapping("/{projectId}")
@Operation(summary = "查询项目详情")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getProject(@PathVariable Long projectId) {
CcdiProjectVO vo = projectService.getProjectById(projectId);
return AjaxResult.success(vo);
@@ -96,7 +106,6 @@ public class CcdiProjectController extends BaseController {
*/
@GetMapping("/list")
@Operation(summary = "查询项目列表")
@PreAuthorize("@ss.hasPermi('ccdi:project:list')")
public TableDataInfo listProject(CcdiProjectQueryDTO queryDTO) {
PageDomain pageDomain = TableSupport.buildPageRequest();
Page<CcdiProjectVO> page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize());
@@ -109,7 +118,6 @@ public class CcdiProjectController extends BaseController {
*/
@GetMapping("/history")
@Operation(summary = "查询历史项目列表")
@PreAuthorize("@ss.hasPermi('ccdi:project:list')")
public AjaxResult listHistoryProjects(CcdiProjectQueryDTO queryDTO) {
List<CcdiProjectHistoryListItemVO> result = projectService.listHistoryProjects(queryDTO);
return AjaxResult.success(result);
@@ -131,7 +139,6 @@ public class CcdiProjectController extends BaseController {
*/
@GetMapping("/statusCounts")
@Operation(summary = "查询项目状态统计")
@PreAuthorize("@ss.hasPermi('ccdi:project:list')")
public AjaxResult getStatusCounts() {
CcdiProjectStatusCountsVO counts = projectService.getStatusCounts();
return AjaxResult.success(counts);

View File

@@ -2,14 +2,21 @@ 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.CcdiProjectExternalPersonQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExternalRiskModelPeopleQueryDTO;
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.CcdiProjectRiskExclusionSaveDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectExternalPersonWarningExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskModelPeopleExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskPeopleOverviewExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountPageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativePageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExternalRiskSummaryVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExternalPersonWarningVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardsVO;
@@ -17,6 +24,7 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.controller.BaseController;
@@ -26,8 +34,10 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@@ -45,6 +55,9 @@ public class CcdiProjectOverviewController extends BaseController {
@Resource
private ICcdiProjectOverviewService overviewService;
@Resource
private CcdiProjectAccessService projectAccessService;
/**
* 查询风险仪表盘
*/
@@ -52,10 +65,23 @@ public class CcdiProjectOverviewController extends BaseController {
@Operation(summary = "查询风险仪表盘")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getDashboard(Long projectId) {
projectAccessService.assertCanRead(projectId);
CcdiProjectOverviewDashboardVO dashboard = overviewService.getDashboard(projectId);
return AjaxResult.success(dashboard);
}
/**
* 排除单条可疑预警
*/
@PostMapping("/risk-exclusions")
@Operation(summary = "排除单条可疑预警")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult excludeRisk(@Validated @RequestBody CcdiProjectRiskExclusionSaveDTO dto) {
projectAccessService.assertCanOperate(dto.getProjectId());
overviewService.excludeRisk(dto);
return AjaxResult.success("排除成功");
}
/**
* 查询风险人员总览
*/
@@ -63,10 +89,35 @@ public class CcdiProjectOverviewController extends BaseController {
@Operation(summary = "查询风险人员总览")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getRiskPeople(CcdiProjectRiskPeopleQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectRiskPeopleOverviewVO overview = overviewService.getRiskPeopleOverview(queryDTO);
return AjaxResult.success(overview);
}
/**
* 查询外部人员预警
*/
@GetMapping("/external-persons")
@Operation(summary = "查询外部人员预警")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExternalPersons(CcdiProjectExternalPersonQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectExternalPersonWarningVO warnings = overviewService.getExternalPersonWarnings(queryDTO);
return AjaxResult.success(warnings);
}
/**
* 查询外部人员风险汇总
*/
@GetMapping("/external-persons/summary")
@Operation(summary = "查询外部人员风险汇总")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExternalRiskSummary(Long projectId) {
projectAccessService.assertCanRead(projectId);
CcdiProjectExternalRiskSummaryVO summary = overviewService.getExternalRiskSummary(projectId);
return AjaxResult.success(summary);
}
/**
* 查询中高风险人员TOP10
*/
@@ -74,6 +125,7 @@ public class CcdiProjectOverviewController extends BaseController {
@Operation(summary = "查询中高风险人员TOP10")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getTopRiskPeople(Long projectId) {
projectAccessService.assertCanRead(projectId);
CcdiProjectTopRiskPeopleVO topRiskPeople = overviewService.getTopRiskPeople(projectId);
return AjaxResult.success(topRiskPeople);
}
@@ -85,6 +137,7 @@ public class CcdiProjectOverviewController extends BaseController {
@Operation(summary = "查询风险模型卡片")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getRiskModelCards(Long projectId) {
projectAccessService.assertCanRead(projectId);
CcdiProjectRiskModelCardsVO cards = overviewService.getRiskModelCards(projectId);
return AjaxResult.success(cards);
}
@@ -96,10 +149,35 @@ public class CcdiProjectOverviewController extends BaseController {
@Operation(summary = "查询风险模型命中人员")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getRiskModelPeople(CcdiProjectRiskModelPeopleQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectRiskModelPeopleVO people = overviewService.getRiskModelPeople(queryDTO);
return AjaxResult.success(people);
}
/**
* 查询外部人员风险模型卡片
*/
@GetMapping("/external-risk-models/cards")
@Operation(summary = "查询外部人员风险模型卡片")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExternalRiskModelCards(Long projectId) {
projectAccessService.assertCanRead(projectId);
CcdiProjectRiskModelCardsVO cards = overviewService.getExternalRiskModelCards(projectId);
return AjaxResult.success(cards);
}
/**
* 查询外部人员风险模型命中人员
*/
@GetMapping("/external-risk-models/people")
@Operation(summary = "查询外部人员风险模型命中人员")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExternalRiskModelPeople(CcdiProjectExternalRiskModelPeopleQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectRiskModelPeopleVO people = overviewService.getExternalRiskModelPeople(queryDTO);
return AjaxResult.success(people);
}
/**
* 查询项目分析详情
*/
@@ -107,6 +185,7 @@ public class CcdiProjectOverviewController extends BaseController {
@Operation(summary = "查询项目分析详情")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getPersonAnalysisDetail(CcdiProjectPersonAnalysisDetailQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectPersonAnalysisDetailVO detail = overviewService.getPersonAnalysisDetail(queryDTO);
return AjaxResult.success(detail);
}
@@ -118,6 +197,7 @@ public class CcdiProjectOverviewController extends BaseController {
@Operation(summary = "查询涉疑交易明细")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getSuspiciousTransactions(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectSuspiciousTransactionPageVO pageVO = overviewService.getSuspiciousTransactions(queryDTO);
return AjaxResult.success(pageVO);
}
@@ -129,6 +209,7 @@ public class CcdiProjectOverviewController extends BaseController {
@Operation(summary = "查询项目员工负面征信")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getEmployeeCreditNegative(CcdiProjectEmployeeCreditNegativeQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectEmployeeCreditNegativePageVO pageVO = overviewService.getEmployeeCreditNegative(queryDTO);
return AjaxResult.success(pageVO);
}
@@ -140,6 +221,7 @@ public class CcdiProjectOverviewController extends BaseController {
@Operation(summary = "查询异常账户人员信息")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getAbnormalAccountPeople(CcdiProjectAbnormalAccountQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectAbnormalAccountPageVO pageVO = overviewService.getAbnormalAccountPeople(queryDTO);
return AjaxResult.success(pageVO);
}
@@ -154,6 +236,7 @@ public class CcdiProjectOverviewController extends BaseController {
HttpServletResponse response,
CcdiProjectSuspiciousTransactionQueryDTO queryDTO
) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
List<CcdiProjectSuspiciousTransactionExcel> rows = overviewService.exportSuspiciousTransactions(queryDTO);
ExcelUtil<CcdiProjectSuspiciousTransactionExcel> util =
new ExcelUtil<>(CcdiProjectSuspiciousTransactionExcel.class);
@@ -167,12 +250,58 @@ public class CcdiProjectOverviewController extends BaseController {
@Operation(summary = "导出风险人员总览")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public void exportRiskPeople(HttpServletResponse response, Long projectId) {
projectAccessService.assertCanRead(projectId);
List<CcdiProjectRiskPeopleOverviewExcel> rows = overviewService.exportRiskPeopleOverview(projectId);
ExcelUtil<CcdiProjectRiskPeopleOverviewExcel> util =
new ExcelUtil<>(CcdiProjectRiskPeopleOverviewExcel.class);
util.exportExcel(response, rows, "风险人员总览");
}
/**
* 导出外部人员预警
*/
@PostMapping("/external-persons/export")
@Operation(summary = "导出外部人员预警")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public void exportExternalPersons(HttpServletResponse response, Long projectId) {
projectAccessService.assertCanRead(projectId);
List<CcdiProjectExternalPersonWarningExcel> rows = overviewService.exportExternalPersonWarnings(projectId);
ExcelUtil<CcdiProjectExternalPersonWarningExcel> util =
new ExcelUtil<>(CcdiProjectExternalPersonWarningExcel.class);
util.exportExcel(response, rows, "外部人员预警");
}
/**
* 导出风险模型命中人员
*/
@PostMapping("/risk-models/people/export")
@Operation(summary = "导出风险模型命中人员")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public void exportRiskModelPeople(HttpServletResponse response, CcdiProjectRiskModelPeopleQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
List<CcdiProjectRiskModelPeopleExcel> rows = overviewService.exportRiskModelPeople(queryDTO);
ExcelUtil<CcdiProjectRiskModelPeopleExcel> util =
new ExcelUtil<>(CcdiProjectRiskModelPeopleExcel.class);
util.exportExcel(response, rows, "风险模型命中人员");
}
/**
* 导出外部人员风险模型命中人员
*/
@PostMapping("/external-risk-models/people/export")
@Operation(summary = "导出外部人员风险模型命中人员")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public void exportExternalRiskModelPeople(
HttpServletResponse response,
CcdiProjectExternalRiskModelPeopleQueryDTO queryDTO
) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
List<CcdiProjectRiskModelPeopleExcel> rows = overviewService.exportExternalRiskModelPeople(queryDTO);
ExcelUtil<CcdiProjectRiskModelPeopleExcel> util =
new ExcelUtil<>(CcdiProjectRiskModelPeopleExcel.class);
util.exportExcel(response, rows, "外部人员风险模型命中人员");
}
/**
* 导出风险明细
*/
@@ -180,6 +309,7 @@ public class CcdiProjectOverviewController extends BaseController {
@Operation(summary = "导出风险明细")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public void exportRiskDetails(HttpServletResponse response, Long projectId) {
projectAccessService.assertCanRead(projectId);
overviewService.exportRiskDetails(response, projectId);
}
@@ -190,6 +320,7 @@ public class CcdiProjectOverviewController extends BaseController {
@Operation(summary = "导出结果总览报告")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public void exportOverviewReport(HttpServletResponse response, Long projectId) {
projectAccessService.assertCanRead(projectId);
overviewService.exportOverviewReport(response, projectId);
}
}

View File

@@ -8,6 +8,8 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferDetailQueryD
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityListQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectIncreaseLendingQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectIncreaseLendingExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentDetailVO;
@@ -16,15 +18,21 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectIncreaseLendingListVO;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.ccdi.project.service.ICcdiProjectSpecialCheckService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.poi.ExcelUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -39,6 +47,9 @@ public class CcdiProjectSpecialCheckController extends BaseController {
@Resource
private ICcdiProjectSpecialCheckService specialCheckService;
@Resource
private CcdiProjectAccessService projectAccessService;
/**
* 查询员工家庭资产负债列表
*/
@@ -46,6 +57,7 @@ public class CcdiProjectSpecialCheckController extends BaseController {
@Operation(summary = "查询员工家庭资产负债列表")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getFamilyAssetLiabilityList(@Validated CcdiProjectFamilyAssetLiabilityListQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectFamilyAssetLiabilityListVO result = specialCheckService.getFamilyAssetLiabilityList(queryDTO);
return AjaxResult.success(result);
}
@@ -57,6 +69,7 @@ public class CcdiProjectSpecialCheckController extends BaseController {
@Operation(summary = "查询员工家庭资产负债详情")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getFamilyAssetLiabilityDetail(@Validated CcdiProjectFamilyAssetLiabilityDetailQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectFamilyAssetLiabilityDetailVO result = specialCheckService.getFamilyAssetLiabilityDetail(queryDTO);
return AjaxResult.success(result);
}
@@ -68,6 +81,7 @@ public class CcdiProjectSpecialCheckController extends BaseController {
@Operation(summary = "查询采购拓展列表")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExtendedPurchaseList(@Validated CcdiProjectExtendedPurchaseQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectExtendedPurchaseListVO result = specialCheckService.getExtendedPurchaseList(queryDTO);
return AjaxResult.success(result);
}
@@ -79,6 +93,7 @@ public class CcdiProjectSpecialCheckController extends BaseController {
@Operation(summary = "查询采购拓展详情")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExtendedPurchaseDetail(@Validated CcdiProjectExtendedPurchaseDetailQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectExtendedPurchaseDetailVO result = specialCheckService.getExtendedPurchaseDetail(queryDTO);
return AjaxResult.success(result);
}
@@ -90,6 +105,7 @@ public class CcdiProjectSpecialCheckController extends BaseController {
@Operation(summary = "查询招聘拓展列表")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExtendedRecruitmentList(@Validated CcdiProjectExtendedRecruitmentQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectExtendedRecruitmentListVO result = specialCheckService.getExtendedRecruitmentList(queryDTO);
return AjaxResult.success(result);
}
@@ -101,6 +117,7 @@ public class CcdiProjectSpecialCheckController extends BaseController {
@Operation(summary = "查询招聘拓展详情")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExtendedRecruitmentDetail(@Validated CcdiProjectExtendedRecruitmentDetailQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectExtendedRecruitmentDetailVO result = specialCheckService.getExtendedRecruitmentDetail(queryDTO);
return AjaxResult.success(result);
}
@@ -112,6 +129,7 @@ public class CcdiProjectSpecialCheckController extends BaseController {
@Operation(summary = "查询调动拓展列表")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExtendedTransferList(@Validated CcdiProjectExtendedTransferQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectExtendedTransferListVO result = specialCheckService.getExtendedTransferList(queryDTO);
return AjaxResult.success(result);
}
@@ -123,7 +141,33 @@ public class CcdiProjectSpecialCheckController extends BaseController {
@Operation(summary = "查询调动拓展详情")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getExtendedTransferDetail(@Validated CcdiProjectExtendedTransferDetailQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectExtendedTransferDetailVO result = specialCheckService.getExtendedTransferDetail(queryDTO);
return AjaxResult.success(result);
}
/**
* 查询新增贷款列表
*/
@GetMapping("/increase-lending/list")
@Operation(summary = "查询新增贷款列表")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getIncreaseLendingList(@Validated CcdiProjectIncreaseLendingQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectIncreaseLendingListVO result = specialCheckService.getIncreaseLendingList(queryDTO);
return AjaxResult.success(result);
}
/**
* 导出新增贷款列表
*/
@PostMapping("/increase-lending/export")
@Operation(summary = "导出新增贷款列表")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public void exportIncreaseLending(HttpServletResponse response, @Validated CcdiProjectIncreaseLendingQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
List<CcdiProjectIncreaseLendingExcel> rows = specialCheckService.exportIncreaseLendingList(queryDTO);
ExcelUtil<CcdiProjectIncreaseLendingExcel> util = new ExcelUtil<>(CcdiProjectIncreaseLendingExcel.class);
util.exportExcel(response, rows, "新增贷款查询");
}
}

View File

@@ -5,6 +5,7 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiRelationGraphSuspectedEnterpriseQue
import com.ruoyi.ccdi.project.domain.vo.CcdiRelationGraphNodeVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiRelationGraphSuspectedEnterpriseVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiRelationGraphVO;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.ccdi.project.service.ICcdiRelationGraphService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
@@ -29,10 +30,14 @@ public class CcdiRelationGraphController extends BaseController {
@Resource
private ICcdiRelationGraphService relationGraphService;
@Resource
private CcdiProjectAccessService projectAccessService;
@GetMapping("/search")
@Operation(summary = "查询关系图谱主体")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult searchSubjects(CcdiRelationGraphQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
List<CcdiRelationGraphNodeVO> subjects = relationGraphService.searchSubjects(queryDTO);
return AjaxResult.success(subjects);
}
@@ -41,6 +46,7 @@ public class CcdiRelationGraphController extends BaseController {
@Operation(summary = "查询一层关系图谱")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getGraph(CcdiRelationGraphQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiRelationGraphVO graph = relationGraphService.getRelationGraph(queryDTO);
return AjaxResult.success(graph);
}
@@ -49,6 +55,7 @@ public class CcdiRelationGraphController extends BaseController {
@Operation(summary = "查询关系图谱疑似同名企业")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getSuspectedEnterprises(CcdiRelationGraphSuspectedEnterpriseQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiRelationGraphSuspectedEnterpriseVO result = relationGraphService.getSuspectedEnterprises(queryDTO);
return AjaxResult.success(result);
}

View File

@@ -32,7 +32,7 @@ public class CcdiProject implements Serializable {
/** 配置方式default-全局默认custom-自定义 */
private String configType;
/** 项目状态0-进行中1-已完成2-已归档3-打标中4-打标失败 */
/** 项目状态0-进行中1-已完成2-已归档3-打标中4-打标失败5-已删除 */
private String status;
/** 是否归档0-未归档1-已归档 */
@@ -54,7 +54,7 @@ public class CcdiProject implements Serializable {
private Integer lsfxProjectId;
/** 删除标志0-存在2-删除 */
@TableLogic
@TableLogic(value = "0", delval = "2")
private String delFlag;
/** 创建者 */

View File

@@ -0,0 +1,24 @@
package com.ruoyi.ccdi.project.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* 当前登录用户的项目访问范围。
*/
@Data
@AllArgsConstructor
public class ProjectAccessScope {
/** 当前用户名 */
private String username;
/** 是否可查看全部项目 */
private boolean viewAllProjects;
/** 是否超级管理员 */
private boolean superAdmin;
/** 是否项目管理员 */
private boolean projectManager;
}

View File

@@ -40,6 +40,9 @@ public class CcdiBankStatementQueryDTO {
/** 本方主体 */
private List<String> ourSubjects;
/** 本方证件号 */
private List<String> ourCertNos;
/** 本方银行 */
private List<String> ourBanks;

View File

@@ -10,6 +10,9 @@ import java.math.BigDecimal;
@Data
public class CcdiFundGraphManualEdgeSaveDTO {
/** 当前项目ID仅用于写权限校验不参与手工资金流归属过滤 */
private Long projectId;
/** 起点主体object_key为空时默认使用当前查询中心 */
private String fromObjectKey;

View File

@@ -0,0 +1,19 @@
package com.ruoyi.ccdi.project.domain.dto;
import lombok.Data;
/**
* 外部人员预警查询DTO
*/
@Data
public class CcdiProjectExternalPersonQueryDTO {
/** 项目ID */
private Long projectId;
/** 页码 */
private Integer pageNum;
/** 每页数量 */
private Integer pageSize;
}

View File

@@ -0,0 +1,41 @@
package com.ruoyi.ccdi.project.domain.dto;
import java.util.List;
import java.util.stream.Collectors;
import lombok.Data;
/**
* 外部人员模型命中人员查询DTO
*/
@Data
public class CcdiProjectExternalRiskModelPeopleQueryDTO {
/** 项目ID */
private Long projectId;
/** 模型编码 */
private List<String> modelCodes;
/** 匹配方式 */
private String matchMode;
/** 关键字 */
private String keyword;
/** 页码 */
private Integer pageNum;
/** 每页数量 */
private Integer pageSize;
public String getModelCodesCsv() {
if (modelCodes == null || modelCodes.isEmpty()) {
return null;
}
return modelCodes.stream()
.filter(item -> item != null && !item.isBlank())
.map(String::trim)
.distinct()
.collect(Collectors.joining(","));
}
}

View File

@@ -0,0 +1,36 @@
package com.ruoyi.ccdi.project.domain.dto;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 项目新增贷款查询入参
*/
@Data
public class CcdiProjectIncreaseLendingQueryDTO {
/** 项目ID */
@NotNull(message = "项目ID不能为空")
private Long projectId;
/** 柜员号 */
private String staffId;
/** 员工身份证号 */
private String staffIdCard;
/** 审核人柜员号 */
private String approver;
/** 发放日期起 */
private String loanStartDate;
/** 发放日期止 */
private String loanEndDate;
/** 页码 */
private Integer pageNum = 1;
/** 每页条数 */
private Integer pageSize = 10;
}

View File

@@ -14,4 +14,7 @@ public class CcdiProjectQueryDTO {
/** 项目状态 */
private String status;
/** 是否查询已删除项目列表 */
private Boolean includeDeleted;
}

View File

@@ -0,0 +1,36 @@
package com.ruoyi.ccdi.project.domain.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* 项目结果页排除可疑保存DTO
*/
@Data
public class CcdiProjectRiskExclusionSaveDTO {
/** 项目ID */
@NotNull(message = "项目ID不能为空")
private Long projectId;
/** 人员证件号 */
private String staffIdCard;
/** 规则编码 */
@NotBlank(message = "规则编码不能为空")
private String ruleCode;
/** 排除类型STATEMENT/OBJECT */
@NotBlank(message = "排除类型不能为空")
private String exclusionType;
/** 流水ID */
private Long bankStatementId;
/** 排除原因 */
@NotBlank(message = "排除原因不能为空")
@Size(max = 1000, message = "排除原因不能超过1000个字符")
private String excludeReason;
}

View File

@@ -11,6 +11,9 @@ public class CcdiProjectSuspiciousTransactionQueryDTO {
/** 项目ID */
private Long projectId;
/** 模型编码 */
private String modelCode;
/** 涉疑类型 */
private String suspiciousType;

View File

@@ -8,6 +8,9 @@ import lombok.Data;
@Data
public class CcdiRelationGraphSuspectedEnterpriseQueryDTO {
/** 项目ID */
private Long projectId;
/** 姓名 */
private String personName;

View File

@@ -207,6 +207,7 @@ public class CcdiBankStatement implements Serializable {
entity.setBatchSequence(item.getUploadSequnceNumber());
entity.setCustomerCertNo(item.getCustomerCertNo());
entity.setCustomerSocialCreditCode(item.getCustomerSocialCreditCode());
entity.setCretNo(normalizeCertNo(item.getCretNo()));
// 5. 特殊字段处理
entity.setMetaJson(null); // 根据文档要求强制设为 null
@@ -219,4 +220,19 @@ public class CcdiBankStatement implements Serializable {
throw new RuntimeException("流水数据转换失败", e);
}
}
private static String normalizeCertNo(String value) {
if (value == null || value.isBlank()) {
return null;
}
String normalized = value.trim()
.replace('', '-')
.replace('—', '-')
.replace('', '-');
int separatorIndex = normalized.indexOf('-');
if (separatorIndex >= 0) {
normalized = normalized.substring(0, separatorIndex).trim();
}
return normalized.isEmpty() ? null : normalized;
}
}

View File

@@ -0,0 +1,58 @@
package com.ruoyi.ccdi.project.domain.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 项目结果页排除可疑记录
*/
@Data
@TableName("ccdi_project_risk_exclusion")
public class CcdiProjectRiskExclusion implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 主键ID */
@TableId(type = IdType.AUTO)
private Long id;
/** 项目ID */
private Long projectId;
/** 人员证件号 */
private String staffIdCard;
/** 规则编码 */
private String ruleCode;
/** 排除类型STATEMENT/OBJECT */
private String exclusionType;
/** 流水ID */
private Long bankStatementId;
/** 排除原因 */
private String excludeReason;
/** 创建者 */
private String createBy;
/** 创建时间 */
private Date createTime;
/** 更新者 */
private String updateBy;
/** 更新时间 */
private Date updateTime;
/** 备注 */
private String remark;
}

View File

@@ -0,0 +1,35 @@
package com.ruoyi.ccdi.project.domain.excel;
import com.ruoyi.common.annotation.Excel;
import lombok.Data;
/**
* 外部人员预警导出对象
*/
@Data
public class CcdiProjectExternalPersonWarningExcel {
@Excel(name = "姓名")
private String name;
@Excel(name = "证件号")
private String idNo;
@Excel(name = "主体类型")
private String subjectType;
@Excel(name = "风险等级")
private String riskLevel;
@Excel(name = "命中模型数")
private Integer modelCount;
@Excel(name = "核心异常点")
private String riskPoint;
@Excel(name = "涉及对象")
private String relatedObject;
@Excel(name = "最近交易时间")
private String latestTradeTime;
}

View File

@@ -0,0 +1,84 @@
package com.ruoyi.ccdi.project.domain.excel;
import com.ruoyi.common.annotation.Excel;
import java.math.BigDecimal;
import lombok.Data;
/**
* 新增贷款查询导出对象
*/
@Data
public class CcdiProjectIncreaseLendingExcel {
/** 柜员号 */
@Excel(name = "柜员号")
private String staffId;
/** 员工姓名 */
@Excel(name = "员工姓名")
private String staffName;
/** 员工身份证 */
@Excel(name = "员工身份证")
private String staffIdCard;
/** 部门 */
@Excel(name = "部门")
private String deptName;
/** 合同编号 */
@Excel(name = "合同编号")
private String contractNo;
/** 放款机构号 */
@Excel(name = "放款机构号")
private String lendingOrgNo;
/** 借款人 */
@Excel(name = "借款人")
private String borrowerName;
/** 借款人证件号 */
@Excel(name = "借款人证件号")
private String borrowerCertNo;
/** 贷款产品 */
@Excel(name = "贷款产品")
private String loanProduct;
/** 合同金额 */
@Excel(name = "合同金额")
private BigDecimal contractAmount;
/** 当前余额 */
@Excel(name = "当前余额")
private BigDecimal loanBalance;
/** 发放日期 */
@Excel(name = "发放日期")
private String loanStartDate;
/** 到期日期 */
@Excel(name = "到期日期")
private String loanEndDate;
/** 合同状态 */
@Excel(name = "合同状态")
private String status;
/** 五级分类 */
@Excel(name = "五级分类")
private String fiveClassification;
/** 客户经理ID */
@Excel(name = "客户经理ID")
private String customerManagerId;
/** 客户经理 */
@Excel(name = "客户经理")
private String customerManagerName;
/** 审批人 */
@Excel(name = "审批人")
private String approver;
}

View File

@@ -0,0 +1,29 @@
package com.ruoyi.ccdi.project.domain.excel;
import com.ruoyi.common.annotation.Excel;
import lombok.Data;
/**
* 风险模型命中人员导出对象
*/
@Data
public class CcdiProjectRiskModelPeopleExcel {
@Excel(name = "风险主体")
private String personName;
@Excel(name = "主体类型")
private String subjectType;
@Excel(name = "证件号")
private String idNo;
@Excel(name = "部门/涉及对象")
private String scopeName;
@Excel(name = "命中模型")
private String modelNames;
@Excel(name = "异常标签")
private String hitTags;
}

View File

@@ -14,20 +14,29 @@ public class CcdiProjectSuspiciousTransactionExcel {
@Excel(name = "交易时间")
private String trxDate;
@Excel(name = "可疑人员")
private String suspiciousPersonName;
@Excel(name = "本方账户")
private String leAccountNo;
@Excel(name = "关联人")
private String relatedPersonName;
@Excel(name = "本方主体")
private String leAccountName;
@Excel(name = "对方名称")
private String customerAccountName;
@Excel(name = "对方账户")
private String customerAccountNo;
@Excel(name = "关联员工")
private String relatedStaffDisplay;
@Excel(name = "关系")
private String relationType;
@Excel(name = "摘要")
private String userMemo;
@Excel(name = "摘要/交易类型")
private String summaryAndCashType;
@Excel(name = "交易类型")
private String cashType;
@Excel(name = "异常标签")
private String hitTags;
@Excel(name = "交易金额")
private BigDecimal displayAmount;

View File

@@ -0,0 +1,35 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.util.List;
import lombok.Data;
/**
* 外部人员预警项
*/
@Data
public class CcdiProjectExternalPersonWarningItemVO {
private String name;
private String idNo;
private String subjectType;
private String riskLevel;
private String riskLevelType;
private Integer riskCount;
private Integer modelCount;
private String riskPoint;
private String relatedObject;
private String latestTradeTime;
private List<CcdiProjectRiskHitTagVO> riskPointTagList;
private String actionLabel;
}

View File

@@ -0,0 +1,19 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.util.List;
import lombok.Data;
/**
* 外部人员预警分页
*/
@Data
public class CcdiProjectExternalPersonWarningVO {
private List<CcdiProjectExternalPersonWarningItemVO> rows;
private Long total;
private Long pageNum;
private Long pageSize;
}

View File

@@ -0,0 +1,20 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
/**
* 外部人员风险等级汇总
*/
@Data
public class CcdiProjectExternalRiskSummaryVO {
private Integer total;
private Integer high;
private Integer medium;
private Integer low;
private Integer noRisk;
}

View File

@@ -0,0 +1,65 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.math.BigDecimal;
import lombok.Data;
/**
* 项目新增贷款查询列表项
*/
@Data
public class CcdiProjectIncreaseLendingListItemVO {
/** 员工ID/柜员号 */
private String staffId;
/** 员工姓名 */
private String staffName;
/** 员工身份证号 */
private String staffIdCard;
/** 所属部门 */
private String deptName;
/** 合同编号 */
private String contractNo;
/** 放款机构号 */
private String lendingOrgNo;
/** 借款人名称 */
private String borrowerName;
/** 借款人证件号码 */
private String borrowerCertNo;
/** 贷款产品 */
private String loanProduct;
/** 合同金额 */
private BigDecimal contractAmount;
/** 当前贷款余额 */
private BigDecimal loanBalance;
/** 贷款发放日期 */
private String loanStartDate;
/** 贷款到期日期 */
private String loanEndDate;
/** 合同状态 */
private String status;
/** 五级分类 */
private String fiveClassification;
/** 客户经理ID */
private String customerManagerId;
/** 客户经理姓名 */
private String customerManagerName;
/** 审批人 */
private String approver;
}

View File

@@ -0,0 +1,17 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.util.List;
import lombok.Data;
/**
* 项目新增贷款查询列表结果
*/
@Data
public class CcdiProjectIncreaseLendingListVO {
/** 列表数据 */
private List<CcdiProjectIncreaseLendingListItemVO> rows;
/** 总数 */
private Long total;
}

View File

@@ -3,6 +3,7 @@ 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 com.ruoyi.ccdi.project.domain.excel.CcdiProjectExternalPersonWarningExcel;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
@@ -21,10 +22,16 @@ public class CcdiProjectOverviewReportVO {
private CcdiProjectOverviewDashboardVO dashboard = new CcdiProjectOverviewDashboardVO();
private CcdiProjectExternalRiskSummaryVO externalRiskSummary = new CcdiProjectExternalRiskSummaryVO();
private List<CcdiProjectOverviewReportModelSummaryVO> modelSummaries = new ArrayList<>();
private List<CcdiProjectOverviewReportModelSummaryVO> externalModelSummaries = new ArrayList<>();
private List<CcdiProjectRiskModelPeopleItemVO> riskPeople = new ArrayList<>();
private List<CcdiProjectExternalPersonWarningExcel> externalPersonWarnings = new ArrayList<>();
private List<CcdiProjectOverviewReportSuspiciousTransactionVO> suspiciousTransactions = new ArrayList<>();
private List<CcdiProjectEmployeeCreditNegativeExcel> illegalPeople = new ArrayList<>();

View File

@@ -13,4 +13,8 @@ public class CcdiProjectOverviewStatVO {
private String label;
private Integer value;
private Integer employeeValue;
private Integer externalValue;
}

View File

@@ -12,6 +12,8 @@ public class CcdiProjectPersonAnalysisObjectRecordVO {
private String modelCode;
private String ruleCode;
private String title;
private String subtitle;

View File

@@ -17,4 +17,6 @@ public class CcdiProjectRiskHitTagVO {
private String ruleName;
private String riskLevel;
private String reasonDetail;
}

View File

@@ -26,4 +26,7 @@ public class CcdiProjectStatusCountsVO {
/** 打标失败项目数(状态4) */
private Long status4;
/** 已删除项目数(状态5) */
private Long status5;
}

View File

@@ -61,4 +61,13 @@ public class CcdiProjectVO {
/** 创建者姓名(真实姓名) */
private String createByName;
/** 是否当前用户创建 */
private Boolean ownedByCurrentUser;
/** 当前用户是否可操作 */
private Boolean canOperate;
/** 当前用户是否可删除 */
private Boolean canDelete;
}

View File

@@ -90,6 +90,36 @@ public interface CcdiBankTagAnalysisMapper {
List<BankTagStatementHitVO> selectLargeTransferStatements(@Param("projectId") Long projectId,
@Param("threshold") BigDecimal threshold);
/**
* 外部人员单笔大额交易
*
* @param projectId 项目ID
* @param threshold 单笔大额阈值
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectExternalSingleLargeAmountStatements(@Param("projectId") Long projectId,
@Param("threshold") BigDecimal threshold);
/**
* 外部人员累计交易超限
*
* @param projectId 项目ID
* @param threshold 累计交易阈值
* @return 对象命中结果
*/
List<BankTagObjectHitVO> selectExternalCumulativeTransactionAmountObjects(@Param("projectId") Long projectId,
@Param("threshold") BigDecimal threshold);
/**
* 外部人员年流水超限
*
* @param projectId 项目ID
* @param threshold 年流水阈值
* @return 对象命中结果
*/
List<BankTagObjectHitVO> selectExternalAnnualTurnoverObjects(@Param("projectId") Long projectId,
@Param("threshold") BigDecimal threshold);
/**
* 与客户之间非正常资金往来
*
@@ -126,6 +156,26 @@ public interface CcdiBankTagAnalysisMapper {
*/
List<BankTagStatementHitVO> selectGamblingSensitiveKeywordStatements(@Param("projectId") Long projectId);
/**
* 外部人员疑似赌博摘要
*
* @param projectId 项目ID
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectExternalGamblingMemoStatements(@Param("projectId") Long projectId);
/**
* 外部人员同日多对手方疑似赌博交易
*
* @param projectId 项目ID
* @param amountMinThreshold 可疑金额下限
* @param amountMaxThreshold 可疑金额上限
* @return 对象命中结果
*/
List<BankTagObjectHitVO> selectExternalMultiPartyGamblingTransferObjects(@Param("projectId") Long projectId,
@Param("amountMinThreshold") BigDecimal amountMinThreshold,
@Param("amountMaxThreshold") BigDecimal amountMaxThreshold);
/**
* 特殊金额交易
*
@@ -134,6 +184,22 @@ public interface CcdiBankTagAnalysisMapper {
*/
List<BankTagStatementHitVO> selectSpecialAmountTransactionStatements(@Param("projectId") Long projectId);
/**
* 外部人员与员工或员工亲属交易
*
* @param projectId 项目ID
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectExternalToStaffOrFamilyTransactionStatements(@Param("projectId") Long projectId);
/**
* 外部人员夜间交易
*
* @param projectId 项目ID
* @return 流水命中结果
*/
List<BankTagStatementHitVO> selectExternalNightTransactionStatements(@Param("projectId") Long projectId);
/**
* 月度固定收入疑似兼职
*

View File

@@ -3,6 +3,7 @@ package com.ruoyi.ccdi.project.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.ProjectAccessScope;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectQueryDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectHistoryListItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectVO;
@@ -25,7 +26,9 @@ public interface CcdiProjectMapper extends BaseMapper<CcdiProject> {
* @param queryDTO 查询条件
* @return 分页结果
*/
Page<CcdiProjectVO> selectProjectPage(Page<CcdiProjectVO> page, @Param("queryDTO") CcdiProjectQueryDTO queryDTO);
Page<CcdiProjectVO> selectProjectPage(Page<CcdiProjectVO> page,
@Param("queryDTO") CcdiProjectQueryDTO queryDTO,
@Param("scope") ProjectAccessScope scope);
/**
* 查询历史项目列表
@@ -33,7 +36,34 @@ public interface CcdiProjectMapper extends BaseMapper<CcdiProject> {
* @param queryDTO 查询条件
* @return 历史项目列表
*/
List<CcdiProjectHistoryListItemVO> selectHistoryProjects(@Param("queryDTO") CcdiProjectQueryDTO queryDTO);
List<CcdiProjectHistoryListItemVO> selectHistoryProjects(@Param("queryDTO") CcdiProjectQueryDTO queryDTO,
@Param("scope") ProjectAccessScope scope);
/**
* 业务逻辑删除项目,仅更新项目主表状态和删除标记。
*
* @param projectId 项目ID
* @param operator 操作人
* @return 更新行数
*/
int markProjectDeleted(@Param("projectId") Long projectId, @Param("operator") String operator);
/**
* 恢复已删除项目为已完成。
*
* @param projectId 项目ID
* @param operator 操作人
* @return 更新行数
*/
int restoreDeletedProject(@Param("projectId") Long projectId, @Param("operator") String operator);
/**
* 统计已删除项目数量。
*
* @param scope 项目访问范围
* @return 已删除项目数量
*/
Long selectDeletedProjectCount(@Param("scope") ProjectAccessScope scope);
/**
* 更新项目总人数与风险人数

View File

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectAbnormalAccountQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectEmployeeCreditNegativeQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExternalPersonQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExternalRiskModelPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskModelPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectRiskPeopleQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
@@ -11,6 +13,8 @@ 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;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExternalRiskSummaryVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExternalPersonWarningItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisBasicInfoVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisObjectRecordVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportModelSummaryVO;
@@ -121,6 +125,88 @@ public interface CcdiProjectOverviewMapper {
@Param("query") CcdiProjectRiskModelPeopleQueryDTO query
);
/**
* 查询风险模型命中人员导出列表
*
* @param query 查询条件
* @return 命中人员列表
*/
List<CcdiProjectRiskModelPeopleItemVO> selectRiskModelPeopleList(
@Param("query") CcdiProjectRiskModelPeopleQueryDTO query
);
/**
* 分页查询外部人员预警
*
* @param page 分页参数
* @param query 查询条件
* @return 外部人员预警分页
*/
Page<CcdiProjectExternalPersonWarningItemVO> selectExternalPersonWarningPage(
Page<CcdiProjectExternalPersonWarningItemVO> page,
@Param("query") CcdiProjectExternalPersonQueryDTO query
);
/**
* 查询外部人员预警导出列表
*
* @param projectId 项目ID
* @return 外部人员预警列表
*/
List<CcdiProjectExternalPersonWarningItemVO> selectExternalPersonWarningList(@Param("projectId") Long projectId);
/**
* 查询外部人员风险等级汇总
*
* @param projectId 项目ID
* @return 外部人员风险等级汇总
*/
CcdiProjectExternalRiskSummaryVO selectExternalRiskSummaryByProjectId(@Param("projectId") Long projectId);
/**
* 查询外部人员预警模型卡片
*
* @param projectId 项目ID
* @return 模型卡片
*/
List<CcdiProjectRiskModelCardVO> selectExternalRiskModelCardsByProjectId(@Param("projectId") Long projectId);
/**
* 分页查询外部人员模型命中人员
*
* @param page 分页参数
* @param query 查询条件
* @return 命中人员分页
*/
Page<CcdiProjectRiskModelPeopleItemVO> selectExternalRiskModelPeoplePage(
Page<CcdiProjectRiskModelPeopleItemVO> page,
@Param("query") CcdiProjectExternalRiskModelPeopleQueryDTO query
);
/**
* 查询外部人员模型命中人员导出列表
*
* @param query 查询条件
* @return 命中人员列表
*/
List<CcdiProjectRiskModelPeopleItemVO> selectExternalRiskModelPeopleList(
@Param("query") CcdiProjectExternalRiskModelPeopleQueryDTO query
);
/**
* 查询外部人员命中标签
*
* @param projectId 项目ID
* @param certNo 外部人员证件号
* @param selectedModelCodes 已选模型编码CSV可为空
* @return 命中标签列表
*/
List<CcdiProjectRiskHitTagVO> selectExternalRiskHitTagsByScope(
@Param("projectId") Long projectId,
@Param("certNo") String certNo,
@Param("selectedModelCodes") String selectedModelCodes
);
/**
* 分页查询涉疑交易明细
*
@@ -240,4 +326,5 @@ public interface CcdiProjectOverviewMapper {
* @return 风险人数汇总
*/
Map<String, Object> selectRiskCountSummaryByProjectId(@Param("projectId") Long projectId);
}

View File

@@ -0,0 +1,27 @@
package com.ruoyi.ccdi.project.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.ccdi.project.domain.entity.CcdiProjectRiskExclusion;
import org.apache.ibatis.annotations.Param;
/**
* 项目结果页排除可疑记录 Mapper
*/
public interface CcdiProjectRiskExclusionMapper extends BaseMapper<CcdiProjectRiskExclusion> {
/**
* 幂等写入排除记录
*
* @param exclusion 排除记录
* @return 写入条数
*/
int upsertExclusion(@Param("exclusion") CcdiProjectRiskExclusion exclusion);
/**
* 查询待排除命中是否存在
*
* @param exclusion 排除记录
* @return 命中数量
*/
int countExistingRiskHit(@Param("exclusion") CcdiProjectRiskExclusion exclusion);
}

View File

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedPurchaseQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedRecruitmentQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectIncreaseLendingQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectIncreaseLendingExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseListItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseSupplierVO;
@@ -15,6 +17,7 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyDebtItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectIncreaseLendingListItemVO;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -145,6 +148,28 @@ public interface CcdiProjectSpecialCheckMapper {
@Param("query") CcdiProjectExtendedTransferQueryDTO queryDTO
);
/**
* 查询项目新增贷款列表
*
* @param page 分页对象
* @param queryDTO 查询条件
* @return 分页结果
*/
Page<CcdiProjectIncreaseLendingListItemVO> selectIncreaseLendingPage(
@Param("page") Page<CcdiProjectIncreaseLendingListItemVO> page,
@Param("query") CcdiProjectIncreaseLendingQueryDTO queryDTO
);
/**
* 查询项目新增贷款导出列表
*
* @param queryDTO 查询条件
* @return 导出列表
*/
List<CcdiProjectIncreaseLendingExcel> selectIncreaseLendingExportList(
@Param("query") CcdiProjectIncreaseLendingQueryDTO queryDTO
);
/**
* 查询专项核查调动拓展详情
*

View File

@@ -0,0 +1,213 @@
package com.ruoyi.ccdi.project.service;
import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.ProjectAccessScope;
import com.ruoyi.ccdi.project.constants.CcdiProjectStatusConstants;
import com.ruoyi.ccdi.project.domain.entity.CcdiBankStatement;
import com.ruoyi.ccdi.project.domain.entity.CcdiEvidence;
import com.ruoyi.ccdi.project.domain.entity.CcdiFileUploadRecord;
import com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper;
import com.ruoyi.ccdi.project.mapper.CcdiEvidenceMapper;
import com.ruoyi.ccdi.project.mapper.CcdiFileUploadRecordMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Objects;
/**
* 项目访问控制。
*/
@Service
public class CcdiProjectAccessService {
private static final String ROLE_ADMIN = "admin";
private static final String ROLE_MANAGER = "manager";
@Resource
private CcdiProjectMapper projectMapper;
@Resource
private CcdiBankStatementMapper bankStatementMapper;
@Resource
private CcdiFileUploadRecordMapper fileUploadRecordMapper;
@Resource
private CcdiEvidenceMapper evidenceMapper;
public ProjectAccessScope buildCurrentScope() {
LoginUser loginUser = SecurityUtils.getLoginUser();
String username = SecurityUtils.getUsername();
boolean superAdmin = isSuperAdmin(loginUser);
boolean projectManager = hasRole(loginUser, ROLE_MANAGER);
return new ProjectAccessScope(username, superAdmin || projectManager, superAdmin, projectManager);
}
public boolean canOperate(CcdiProject project) {
if (project == null) {
return false;
}
ProjectAccessScope scope = buildCurrentScope();
return scope.isSuperAdmin() || Objects.equals(scope.getUsername(), project.getCreateBy());
}
public boolean canDelete(CcdiProject project) {
if (project == null) {
return false;
}
ProjectAccessScope scope = buildCurrentScope();
return isProjectAdmin(scope) || Objects.equals(scope.getUsername(), project.getCreateBy());
}
public void assertCanRead(Long projectId) {
CcdiProject project = getRequiredProject(projectId);
assertProjectNotDeleted(project);
ProjectAccessScope scope = buildCurrentScope();
if (scope.isViewAllProjects() || Objects.equals(scope.getUsername(), project.getCreateBy())) {
return;
}
throw new ServiceException("无权查看该项目");
}
public void assertCanOperate(Long projectId) {
CcdiProject project = getRequiredProject(projectId);
assertProjectNotDeleted(project);
if (canOperate(project)) {
return;
}
throw new ServiceException("无权操作该项目");
}
public void assertCanDelete(Long projectId) {
CcdiProject project = getRequiredProject(projectId);
assertProjectNotDeleted(project);
if (canDelete(project)) {
return;
}
throw new ServiceException("无权删除该项目");
}
public void assertCanManageDeletedProjects() {
if (isProjectAdmin(buildCurrentScope())) {
return;
}
throw new ServiceException("无权管理已删除项目");
}
public void assertCanReadByBankStatementId(Long bankStatementId) {
CcdiBankStatement statement = getRequiredBankStatement(bankStatementId);
assertCanRead(statement.getProjectId());
}
public void assertCanReadByFileRecordId(Long fileRecordId) {
CcdiFileUploadRecord record = getRequiredFileRecord(fileRecordId);
assertCanRead(record.getProjectId());
}
public void assertCanOperateByFileRecordId(Long fileRecordId) {
CcdiFileUploadRecord record = getRequiredFileRecord(fileRecordId);
assertCanOperate(record.getProjectId());
}
public void assertCanReadByEvidenceId(Long evidenceId) {
CcdiEvidence evidence = getRequiredEvidence(evidenceId);
assertCanRead(evidence.getProjectId());
}
public void assertSourceProjectsReadable(List<Long> sourceProjectIds) {
if (CollectionUtils.isEmpty(sourceProjectIds)) {
return;
}
for (Long sourceProjectId : sourceProjectIds) {
assertCanRead(sourceProjectId);
}
}
private CcdiProject getRequiredProject(Long projectId) {
if (projectId == null) {
throw new ServiceException("项目ID不能为空");
}
CcdiProject project = projectMapper.selectById(projectId);
if (project == null) {
throw new ServiceException("项目不存在");
}
return project;
}
private void assertProjectNotDeleted(CcdiProject project) {
if (project == null) {
return;
}
if (CcdiProjectStatusConstants.DELETED.equals(project.getStatus()) || "2".equals(project.getDelFlag())) {
throw new ServiceException("项目已删除");
}
}
private boolean isProjectAdmin(ProjectAccessScope scope) {
return scope != null && (scope.isSuperAdmin() || scope.isProjectManager());
}
private CcdiBankStatement getRequiredBankStatement(Long bankStatementId) {
if (bankStatementId == null) {
throw new ServiceException("流水ID不能为空");
}
CcdiBankStatement statement = bankStatementMapper.selectById(bankStatementId);
if (statement == null) {
throw new ServiceException("流水记录不存在");
}
return statement;
}
private CcdiFileUploadRecord getRequiredFileRecord(Long fileRecordId) {
if (fileRecordId == null) {
throw new ServiceException("文件记录ID不能为空");
}
CcdiFileUploadRecord record = fileUploadRecordMapper.selectById(fileRecordId);
if (record == null) {
throw new ServiceException("文件记录不存在");
}
return record;
}
private CcdiEvidence getRequiredEvidence(Long evidenceId) {
if (evidenceId == null) {
throw new ServiceException("证据ID不能为空");
}
CcdiEvidence evidence = evidenceMapper.selectById(evidenceId);
if (evidence == null) {
throw new ServiceException("证据不存在");
}
return evidence;
}
private boolean isSuperAdmin(LoginUser loginUser) {
if (loginUser == null) {
return false;
}
if (SecurityUtils.isAdmin(loginUser.getUserId())) {
return true;
}
return hasRole(loginUser, ROLE_ADMIN);
}
private boolean hasRole(LoginUser loginUser, String roleKey) {
if (loginUser == null || loginUser.getUser() == null
|| CollectionUtils.isEmpty(loginUser.getUser().getRoles())) {
return false;
}
for (SysRole role : loginUser.getUser().getRoles()) {
if (roleKey.equals(role.getRoleKey())) {
return true;
}
}
return false;
}
}

View File

@@ -2,16 +2,23 @@ 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.CcdiProjectExternalPersonQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExternalRiskModelPeopleQueryDTO;
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.CcdiProjectRiskExclusionSaveDTO;
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.CcdiProjectExternalPersonWarningExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskModelPeopleExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskPeopleOverviewExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountPageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativePageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExternalRiskSummaryVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExternalPersonWarningVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardsVO;
@@ -36,6 +43,13 @@ public interface ICcdiProjectOverviewService {
*/
CcdiProjectOverviewDashboardVO getDashboard(Long projectId);
/**
* 排除单条可疑预警
*
* @param dto 排除参数
*/
void excludeRisk(CcdiProjectRiskExclusionSaveDTO dto);
/**
* 查询风险人员总览
*
@@ -82,6 +96,80 @@ public interface ICcdiProjectOverviewService {
return new CcdiProjectRiskModelPeopleVO();
}
/**
* 导出风险模型命中人员
*
* @param queryDTO 查询条件
* @return 导出列表
*/
default List<CcdiProjectRiskModelPeopleExcel> exportRiskModelPeople(CcdiProjectRiskModelPeopleQueryDTO queryDTO) {
return List.of();
}
/**
* 查询外部人员预警
*
* @param queryDTO 查询条件
* @return 外部人员预警
*/
default CcdiProjectExternalPersonWarningVO getExternalPersonWarnings(CcdiProjectExternalPersonQueryDTO queryDTO) {
return new CcdiProjectExternalPersonWarningVO();
}
/**
* 查询外部人员风险等级汇总
*
* @param projectId 项目ID
* @return 外部人员风险等级汇总
*/
default CcdiProjectExternalRiskSummaryVO getExternalRiskSummary(Long projectId) {
return new CcdiProjectExternalRiskSummaryVO();
}
/**
* 导出外部人员预警
*
* @param projectId 项目ID
* @return 导出列表
*/
default List<CcdiProjectExternalPersonWarningExcel> exportExternalPersonWarnings(Long projectId) {
return List.of();
}
/**
* 查询外部人员风险模型卡片
*
* @param projectId 项目ID
* @return 风险模型卡片
*/
default CcdiProjectRiskModelCardsVO getExternalRiskModelCards(Long projectId) {
return new CcdiProjectRiskModelCardsVO();
}
/**
* 查询外部人员风险模型命中人员
*
* @param queryDTO 查询条件
* @return 命中人员
*/
default CcdiProjectRiskModelPeopleVO getExternalRiskModelPeople(
CcdiProjectExternalRiskModelPeopleQueryDTO queryDTO
) {
return new CcdiProjectRiskModelPeopleVO();
}
/**
* 导出外部人员风险模型命中人员
*
* @param queryDTO 查询条件
* @return 导出列表
*/
default List<CcdiProjectRiskModelPeopleExcel> exportExternalRiskModelPeople(
CcdiProjectExternalRiskModelPeopleQueryDTO queryDTO
) {
return List.of();
}
/**
* 查询涉疑交易明细
*

View File

@@ -36,9 +36,18 @@ public interface ICcdiProjectService {
* 删除项目
*
* @param projectId 项目ID
* @param operator 操作人
* @return 是否成功
*/
boolean deleteProject(Long projectId);
boolean deleteProject(Long projectId, String operator);
/**
* 恢复已删除项目
*
* @param projectId 项目ID
* @param operator 操作人
*/
void restoreProject(Long projectId, String operator);
/**
* 查询项目详情

View File

@@ -8,6 +8,8 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferDetailQueryD
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityListQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectIncreaseLendingQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectIncreaseLendingExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedRecruitmentDetailVO;
@@ -16,6 +18,8 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedTransferListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectIncreaseLendingListVO;
import java.util.List;
/**
* 项目专项核查服务接口
@@ -91,4 +95,20 @@ public interface ICcdiProjectSpecialCheckService {
* @return 详情结果
*/
CcdiProjectExtendedTransferDetailVO getExtendedTransferDetail(CcdiProjectExtendedTransferDetailQueryDTO queryDTO);
/**
* 查询项目新增贷款列表
*
* @param queryDTO 查询条件
* @return 列表结果
*/
CcdiProjectIncreaseLendingListVO getIncreaseLendingList(CcdiProjectIncreaseLendingQueryDTO queryDTO);
/**
* 导出项目新增贷款列表
*
* @param queryDTO 查询条件
* @return 导出列表
*/
List<CcdiProjectIncreaseLendingExcel> exportIncreaseLendingList(CcdiProjectIncreaseLendingQueryDTO queryDTO);
}

View File

@@ -27,10 +27,13 @@ public class BankTagRuleConfigResolver {
private static final Map<String, Set<String>> RULE_PARAM_MAPPING = Map.ofEntries(
Map.entry("SINGLE_LARGE_INCOME", Set.of("SINGLE_TRANSACTION_AMOUNT")),
Map.entry("CUMULATIVE_INCOME", Set.of("CUMULATIVE_TRANSACTION_AMOUNT")),
Map.entry("EXTERNAL_CUMULATIVE_TRANSACTION_AMOUNT", Set.of("CUMULATIVE_TRANSACTION_AMOUNT")),
Map.entry("ANNUAL_TURNOVER", Set.of("ANNUAL_TURNOVER")),
Map.entry("EXTERNAL_ANNUAL_TURNOVER", Set.of("ANNUAL_TURNOVER")),
Map.entry("LARGE_CASH_DEPOSIT", Set.of("LARGE_CASH_DEPOSIT")),
Map.entry("FREQUENT_CASH_DEPOSIT", Set.of("LARGE_CASH_DEPOSIT", "FREQUENT_CASH_DEPOSIT")),
Map.entry("LARGE_TRANSFER", Set.of("FREQUENT_TRANSFER")),
Map.entry("EXTERNAL_SINGLE_LARGE_AMOUNT", Set.of("FREQUENT_TRANSFER")),
Map.entry("FOREX_BUY_AMT", Set.of("SINGLE_PURCHASE_AMOUNT")),
Map.entry("FOREX_SELL_AMT", Set.of("SINGLE_SETTLEMENT_AMOUNT")),
Map.entry("WITHDRAW_CNT", Set.of("WITHDRAW_CNT")),
@@ -38,10 +41,18 @@ public class BankTagRuleConfigResolver {
Map.entry("STOCK_TFR_LARGE", Set.of("STOCK_TFR_LARGE")),
Map.entry("LARGE_STOCK_TRADING", Set.of("STOCK_TFR_LARGE")),
Map.entry("MULTI_PARTY_GAMBLING_TRANSFER", Set.of("MULTI_PARTY_AMT_MIN", "MULTI_PARTY_AMT_MAX")),
Map.entry("EXTERNAL_MULTI_PARTY_GAMBLING_TRANSFER", Set.of("MULTI_PARTY_AMT_MIN", "MULTI_PARTY_AMT_MAX")),
Map.entry("MONTHLY_FIXED_INCOME", Set.of("MONTHLY_FIXED_INCOME")),
Map.entry("FIXED_COUNTERPARTY_TRANSFER", Set.of("FIXED_COUNTERPARTY_TRANSFER_MIN", "FIXED_COUNTERPARTY_TRANSFER_MAX"))
);
private static final Map<String, String> RULE_PARAM_MODEL_MAPPING = Map.of(
"EXTERNAL_SINGLE_LARGE_AMOUNT", "LARGE_TRANSACTION",
"EXTERNAL_CUMULATIVE_TRANSACTION_AMOUNT", "LARGE_TRANSACTION",
"EXTERNAL_ANNUAL_TURNOVER", "LARGE_TRANSACTION",
"EXTERNAL_MULTI_PARTY_GAMBLING_TRANSFER", "SUSPICIOUS_GAMBLING"
);
@Resource
private CcdiProjectMapper projectMapper;
@@ -69,12 +80,13 @@ public class BankTagRuleConfigResolver {
}
Long effectiveProjectId = "default".equals(project.getConfigType()) ? 0L : projectId;
List<CcdiModelParam> params = modelParamMapper.selectByProjectAndModel(effectiveProjectId, ruleMeta.getModelCode());
String paramModelCode = RULE_PARAM_MODEL_MAPPING.getOrDefault(ruleMeta.getRuleCode(), ruleMeta.getModelCode());
List<CcdiModelParam> params = modelParamMapper.selectByProjectAndModel(effectiveProjectId, paramModelCode);
Map<String, String> thresholdValues = new LinkedHashMap<>();
Set<String> requiredParamCodes = RULE_PARAM_MAPPING.getOrDefault(ruleMeta.getRuleCode(), Set.of());
log.info("【流水标签】解析规则参数: projectId={}, effectiveProjectId={}, ruleCode={}, requiredParams={}",
projectId, effectiveProjectId, ruleMeta.getRuleCode(), requiredParamCodes);
log.info("【流水标签】解析规则参数: projectId={}, effectiveProjectId={}, ruleCode={}, paramModelCode={}, requiredParams={}",
projectId, effectiveProjectId, ruleMeta.getRuleCode(), paramModelCode, requiredParamCodes);
for (CcdiModelParam param : params) {
if (requiredParamCodes.contains(param.getParamCode())) {
thresholdValues.put(param.getParamCode(), param.getParamValue());

View File

@@ -225,9 +225,15 @@ public class CcdiBankTagServiceImpl implements ICcdiBankTagService {
case "LARGE_TRANSFER" -> analysisMapper.selectLargeTransferStatements(
projectId, toBigDecimal(config.getThresholdValue("FREQUENT_TRANSFER"))
);
case "EXTERNAL_SINGLE_LARGE_AMOUNT" -> analysisMapper.selectExternalSingleLargeAmountStatements(
projectId, toBigDecimal(config.getThresholdValue("FREQUENT_TRANSFER"))
);
case "ABNORMAL_CUSTOMER_TRANSACTION" -> analysisMapper.selectAbnormalCustomerTransactionStatements(projectId);
case "EXTERNAL_NIGHT_TRANSACTION" -> analysisMapper.selectExternalNightTransactionStatements(projectId);
case "GAMBLING_SENSITIVE_KEYWORD" -> analysisMapper.selectGamblingSensitiveKeywordStatements(projectId);
case "EXTERNAL_GAMBLING_MEMO" -> analysisMapper.selectExternalGamblingMemoStatements(projectId);
case "SPECIAL_AMOUNT_TRANSACTION" -> analysisMapper.selectSpecialAmountTransactionStatements(projectId);
case "EXTERNAL_TO_STAFF_FAMILY_TRANSACTION" -> analysisMapper.selectExternalToStaffOrFamilyTransactionStatements(projectId);
case "SUSPICIOUS_INCOME_KEYWORD" -> analysisMapper.selectSuspiciousIncomeKeywordStatements(projectId);
case "HOUSE_REGISTRATION_MISMATCH" -> analysisMapper.selectHouseRegistrationMismatchStatements(projectId);
case "PROPERTY_FEE_REGISTRATION_MISMATCH" -> analysisMapper.selectPropertyFeeRegistrationMismatchStatements(projectId);
@@ -258,9 +264,15 @@ public class CcdiBankTagServiceImpl implements ICcdiBankTagService {
case "CUMULATIVE_INCOME" -> analysisMapper.selectCumulativeIncomeObjects(
projectId, toBigDecimal(config.getThresholdValue("CUMULATIVE_TRANSACTION_AMOUNT"))
);
case "EXTERNAL_CUMULATIVE_TRANSACTION_AMOUNT" -> analysisMapper.selectExternalCumulativeTransactionAmountObjects(
projectId, toBigDecimal(config.getThresholdValue("CUMULATIVE_TRANSACTION_AMOUNT"))
);
case "ANNUAL_TURNOVER" -> analysisMapper.selectAnnualTurnoverObjects(
projectId, toBigDecimal(config.getThresholdValue("ANNUAL_TURNOVER"))
);
case "EXTERNAL_ANNUAL_TURNOVER" -> analysisMapper.selectExternalAnnualTurnoverObjects(
projectId, toBigDecimal(config.getThresholdValue("ANNUAL_TURNOVER"))
);
case "FREQUENT_CASH_DEPOSIT" -> analysisMapper.selectFrequentCashDepositObjects(
projectId,
toBigDecimal(config.getThresholdValue("LARGE_CASH_DEPOSIT")),
@@ -272,6 +284,11 @@ public class CcdiBankTagServiceImpl implements ICcdiBankTagService {
toBigDecimal(config.getThresholdValue("MULTI_PARTY_AMT_MIN")),
toBigDecimal(config.getThresholdValue("MULTI_PARTY_AMT_MAX"))
);
case "EXTERNAL_MULTI_PARTY_GAMBLING_TRANSFER" -> analysisMapper.selectExternalMultiPartyGamblingTransferObjects(
projectId,
toBigDecimal(config.getThresholdValue("MULTI_PARTY_AMT_MIN")),
toBigDecimal(config.getThresholdValue("MULTI_PARTY_AMT_MAX"))
);
case "MONTHLY_FIXED_INCOME" -> analysisMapper.selectMonthlyFixedIncomeObjects(
projectId, toBigDecimal(config.getThresholdValue("MONTHLY_FIXED_INCOME"))
);

View File

@@ -64,6 +64,9 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
private static final int MAX_ERROR_MESSAGE_LENGTH = 2000;
private static final Pattern ID_CARD_PATTERN =
Pattern.compile("^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])([0-2]\\d|3[01])\\d{3}[0-9Xx]$");
private static final Pattern UPLOAD_FILE_NAME_ID_CARD_PATTERN =
Pattern.compile("(?<!\\d)\\d{17}[0-9Xx](?!\\d)");
private static final Pattern FILE_NAME_SPACE_PATTERN = Pattern.compile("[\\s\\u3000]+");
@Data
private static class FetchBankStatementResult {
@@ -347,6 +350,8 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
log.info("【文件上传】项目信息验证通过: projectId={}, lsfxProjectId={}", projectId, lsfxProjectId);
List<String> normalizedFileNames = normalizeAndValidateUploadFileNames(files);
// Critical Fix #2 & #4: 保存临时文件和创建记录在同一个循环中,确保一一对应
List<String> tempFilePaths = new ArrayList<>();
List<CcdiFileUploadRecord> records = new ArrayList<>();
@@ -362,10 +367,11 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
// 同一个循环中保存临时文件和创建记录,确保索引一一对应
for (int i = 0; i < files.length; i++) {
MultipartFile file = files[i];
String normalizedFileName = normalizedFileNames.get(i);
// 1. 保存临时文件
String originalFilename = file.getOriginalFilename();
String tempFileName = batchId + "_" + i + "_" + System.currentTimeMillis() + "_" + originalFilename;
String tempFileName = batchId + "_" + i + "_" + System.currentTimeMillis() + "_" + normalizedFileName;
Path tempFilePath = tempDir.resolve(tempFileName);
Files.copy(file.getInputStream(), tempFilePath, StandardCopyOption.REPLACE_EXISTING);
@@ -378,7 +384,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
CcdiFileUploadRecord record = new CcdiFileUploadRecord();
record.setProjectId(projectId);
record.setLsfxProjectId(lsfxProjectId);
record.setFileName(originalFilename);
record.setFileName(normalizedFileName);
record.setFileSize(file.getSize());
record.setFileStatus("uploading");
record.setUploadTime(now);
@@ -423,6 +429,42 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
return batchId;
}
private List<String> normalizeAndValidateUploadFileNames(MultipartFile[] files) {
List<String> normalizedFileNames = new ArrayList<>();
for (MultipartFile file : files) {
String originalFileName = file.getOriginalFilename() == null ? "" : file.getOriginalFilename();
String normalizedFileName = FILE_NAME_SPACE_PATTERN.matcher(originalFileName).replaceAll("");
validateUploadFileName(normalizedFileName);
normalizedFileNames.add(normalizedFileName);
}
return normalizedFileNames;
}
private void validateUploadFileName(String fileName) {
if (!StringUtils.hasText(fileName)) {
throw new IllegalArgumentException("文件名不能为空");
}
String lowerFileName = fileName.toLowerCase(Locale.ROOT);
if (!lowerFileName.endsWith(".xlsx") && !lowerFileName.endsWith(".csv")
&& !lowerFileName.endsWith(".pdf")) {
throw new IllegalArgumentException("文件 " + fileName + " 格式不支持, 仅支持 PDF, CSV, XLSX 文件");
}
String mainFileName = getMainFileName(fileName);
if (!UPLOAD_FILE_NAME_ID_CARD_PATTERN.matcher(mainFileName).find()) {
throw new IllegalArgumentException("文件 " + fileName + " 文件名未包含身份证信息");
}
}
private String getMainFileName(String fileName) {
int extensionIndex = fileName.lastIndexOf('.');
if (extensionIndex <= 0) {
return fileName;
}
return fileName.substring(0, extensionIndex);
}
/**
* 调度线程:循环提交任务到线程池
* 支持等待30秒重试机制

View File

@@ -2,6 +2,7 @@ package com.ruoyi.ccdi.project.service.impl;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectAbnormalAccountExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectEmployeeCreditNegativeExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectExternalPersonWarningExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportModelSummaryVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportParamVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportSuspiciousTransactionVO;
@@ -65,7 +66,7 @@ public class CcdiProjectOverviewReportPdfExporter {
writeCover(writer, report);
writeUploadSubjects(writer, report.getUploadSubjects());
writeParams(writer, report.getParams());
writeRiskModels(writer, report);
writeRiskOverview(writer, report);
writeRiskDetails(writer, report);
writer.close();
document.save(response.getOutputStream());
@@ -118,9 +119,9 @@ public class CcdiProjectOverviewReportPdfExporter {
);
}
private void writeRiskModels(PdfPageWriter writer, CcdiProjectOverviewReportVO report) throws IOException {
writer.section("三、风险模型");
writer.metrics(report.getDashboard().getStats());
private void writeRiskOverview(PdfPageWriter writer, CcdiProjectOverviewReportVO report) throws IOException {
writer.section("三、风险总览");
writer.metrics(buildOverallRiskMetrics(report));
writer.subsection("风险模型汇总");
writer.table(
List.of("模型名称", "预警数量", "涉及人员"),
@@ -151,6 +152,40 @@ public class CcdiProjectOverviewReportPdfExporter {
new float[] { 0.1F, 0.11F, 0.16F, 0.14F, 0.24F, 0.25F },
"暂无风险人员与异常点数据"
);
if (hasExternalRisk(report)) {
writer.subsection("外部人员预警");
writer.metrics(buildExternalMetrics(report));
writer.table(
List.of("外部模型", "预警数量", "涉及人数"),
report.getExternalModelSummaries().stream()
.map(item -> List.of(
safeText(item.getModelName()),
String.valueOf(defaultZero(item.getWarningCount())),
formatCount(item.getPeopleCount(), "")
))
.collect(Collectors.toList()),
new float[] { 0.5F, 0.2F, 0.3F },
"暂无外部人员模型汇总数据"
);
writer.table(
List.of("姓名", "证件号", "主体类型", "风险等级", "命中模型数", "核心异常点", "涉及对象", "最近交易时间"),
report.getExternalPersonWarnings().stream()
.map(item -> List.of(
safeText(item.getName()),
maskIdCard(item.getIdNo()),
safeText(item.getSubjectType()),
safeText(item.getRiskLevel()),
String.valueOf(defaultZero(item.getModelCount())),
safeText(item.getRiskPoint()),
safeText(item.getRelatedObject()),
safeText(item.getLatestTradeTime())
))
.collect(Collectors.toList()),
new float[] { 0.09F, 0.15F, 0.1F, 0.09F, 0.1F, 0.25F, 0.12F, 0.1F },
"暂无外部人员预警数据"
);
}
}
private void writeRiskDetails(PdfPageWriter writer, CcdiProjectOverviewReportVO report) throws IOException {
@@ -328,6 +363,69 @@ public class CcdiProjectOverviewReportPdfExporter {
return result;
}
private List<CcdiProjectOverviewStatVO> buildOverallRiskMetrics(CcdiProjectOverviewReportVO report) {
List<CcdiProjectOverviewStatVO> employeeStats = report.getDashboard().getStats();
if (employeeStats == null || employeeStats.isEmpty()) {
return List.of(
buildMetric("总人数", report.getExternalRiskSummary().getTotal()),
buildMetric("高风险", report.getExternalRiskSummary().getHigh()),
buildMetric("中风险", report.getExternalRiskSummary().getMedium()),
buildMetric("低风险", report.getExternalRiskSummary().getLow()),
buildMetric("无风险", report.getExternalRiskSummary().getNoRisk())
);
}
int employeeTotal = metricValue(employeeStats, "people");
int high = metricValue(employeeStats, "riskPeople");
int medium = metricValue(employeeStats, "medium");
int low = metricValue(employeeStats, "low");
int noRisk = metricValue(employeeStats, "count");
int externalTotal = defaultZero(report.getExternalRiskSummary().getTotal());
int externalHigh = defaultZero(report.getExternalRiskSummary().getHigh());
int externalMedium = defaultZero(report.getExternalRiskSummary().getMedium());
int externalLow = defaultZero(report.getExternalRiskSummary().getLow());
int externalNoRisk = defaultZero(report.getExternalRiskSummary().getNoRisk());
return List.of(
buildMetric("总人数", employeeTotal + externalTotal),
buildMetric("高风险", high + externalHigh),
buildMetric("中风险", medium + externalMedium),
buildMetric("低风险", low + externalLow),
buildMetric("无风险", noRisk + externalNoRisk)
);
}
private Integer metricValue(List<CcdiProjectOverviewStatVO> stats, String key) {
return defaultZero(stats.stream()
.filter(stat -> key.equals(stat.getKey()))
.findFirst()
.map(CcdiProjectOverviewStatVO::getValue)
.orElse(0));
}
private boolean hasExternalRisk(CcdiProjectOverviewReportVO report) {
return defaultZero(report.getExternalRiskSummary().getHigh()) > 0
|| defaultZero(report.getExternalRiskSummary().getMedium()) > 0
|| defaultZero(report.getExternalRiskSummary().getLow()) > 0
|| !report.getExternalModelSummaries().isEmpty()
|| !report.getExternalPersonWarnings().isEmpty();
}
private List<CcdiProjectOverviewStatVO> buildExternalMetrics(CcdiProjectOverviewReportVO report) {
return List.of(
buildMetric("外部人员", report.getExternalRiskSummary().getTotal()),
buildMetric("高风险", report.getExternalRiskSummary().getHigh()),
buildMetric("中风险", report.getExternalRiskSummary().getMedium()),
buildMetric("低风险", report.getExternalRiskSummary().getLow()),
buildMetric("无风险人员", report.getExternalRiskSummary().getNoRisk())
);
}
private CcdiProjectOverviewStatVO buildMetric(String label, Integer value) {
CcdiProjectOverviewStatVO stat = new CcdiProjectOverviewStatVO();
stat.setLabel(label);
stat.setValue(defaultZero(value));
return stat;
}
private String formatPeopleSummary(CcdiProjectOverviewReportModelSummaryVO item) {
String names = safeText(item.getPeopleNames());
if ("-".equals(names)) {

View File

@@ -1,18 +1,25 @@
package com.ruoyi.ccdi.project.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.CcdiModelParam;
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.CcdiProjectExternalPersonQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExternalRiskModelPeopleQueryDTO;
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.CcdiProjectRiskExclusionSaveDTO;
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.CcdiProjectExternalPersonWarningExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskModelPeopleExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskPeopleOverviewExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.entity.CcdiProjectOverviewEmployeeResult;
import com.ruoyi.ccdi.project.domain.entity.CcdiProjectRiskExclusion;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountPageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
@@ -25,40 +32,49 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisAbnormalGroupVO
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.CcdiProjectExternalPersonWarningItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExternalPersonWarningVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExternalRiskSummaryVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportParamVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportSuspiciousTransactionVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewEmployeeHitRowVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportModelSummaryVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewStatVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardsVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO;
import com.ruoyi.ccdi.project.domain.vo.ModelParamAllVO;
import com.ruoyi.ccdi.project.mapper.CcdiModelParamMapper;
import com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper;
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.mapper.CcdiProjectRiskExclusionMapper;
import com.ruoyi.ccdi.project.constants.CcdiProjectStatusConstants;
import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
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.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -70,6 +86,10 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
private static final String ACTION_LABEL = "查看详情";
private static final String EXCLUSION_TYPE_STATEMENT = "STATEMENT";
private static final String EXCLUSION_TYPE_OBJECT = "OBJECT";
@Resource
private CcdiProjectOverviewMapper overviewMapper;
@@ -79,12 +99,18 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
@Resource
private CcdiBankStatementMapper bankStatementMapper;
@Resource
private CcdiModelParamMapper modelParamMapper;
@Resource
private CcdiProjectOverviewEmployeeResultMapper overviewEmployeeResultMapper;
@Resource
private CcdiBankTagResultMapper bankTagResultMapper;
@Resource
private CcdiProjectRiskExclusionMapper riskExclusionMapper;
@Resource
private CcdiProjectOverviewEmployeeResultBuilder overviewEmployeeResultBuilder;
@@ -94,10 +120,6 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
@Resource
private CcdiProjectOverviewReportPdfExporter reportPdfExporter;
@Resource
@Lazy
private ICcdiModelParamService modelParamService;
@Override
public CcdiProjectOverviewDashboardVO getDashboard(Long projectId) {
CcdiProject project = overviewMapper.selectDashboardBaseByProjectId(projectId);
@@ -124,6 +146,24 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
return dashboard;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void excludeRisk(CcdiProjectRiskExclusionSaveDTO dto) {
CcdiProjectRiskExclusion exclusion = buildRiskExclusion(dto);
CcdiProject project = getRequiredProject(exclusion.getProjectId());
if (CcdiProjectStatusConstants.ARCHIVED.equals(project.getStatus()) || Integer.valueOf(1).equals(project.getIsArchived())) {
throw new ServiceException("已归档项目暂不允许排除可疑");
}
if (CcdiProjectStatusConstants.TAGGING.equals(project.getStatus())) {
throw new ServiceException("项目正在打标中,暂不允许排除可疑");
}
if (riskExclusionMapper.countExistingRiskHit(exclusion) <= 0) {
throw new ServiceException("未找到可排除的预警命中");
}
riskExclusionMapper.upsertExclusion(exclusion);
refreshOverviewEmployeeResults(exclusion.getProjectId(), exclusion.getUpdateBy());
}
@Override
public CcdiProjectRiskPeopleOverviewVO getRiskPeopleOverview(CcdiProjectRiskPeopleQueryDTO queryDTO) {
Long projectId = queryDTO.getProjectId();
@@ -179,6 +219,9 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
queryDTO.getStaffIdCard()
));
attachStatementHitTags(statementRows, queryDTO.getProjectId());
statementRows = statementRows.stream()
.filter(row -> row.getHitTags() != null && !row.getHitTags().isEmpty())
.toList();
normalizeObjectRows(objectRows);
CcdiProjectPersonAnalysisDetailVO detail = new CcdiProjectPersonAnalysisDetailVO();
@@ -218,6 +261,108 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
return people;
}
@Override
public List<CcdiProjectRiskModelPeopleExcel> exportRiskModelPeople(CcdiProjectRiskModelPeopleQueryDTO queryDTO) {
ensureProjectExists(queryDTO.getProjectId());
normalizeRiskModelPeopleQuery(queryDTO);
return defaultList(overviewMapper.selectRiskModelPeopleList(queryDTO)).stream()
.map(item -> buildRiskModelPeopleExcelRow(item, "员工"))
.toList();
}
@Override
public CcdiProjectExternalPersonWarningVO getExternalPersonWarnings(CcdiProjectExternalPersonQueryDTO queryDTO) {
ensureProjectExists(queryDTO.getProjectId());
Page<CcdiProjectExternalPersonWarningItemVO> page = new Page<>(
defaultRiskPeoplePageNum(queryDTO.getPageNum()),
defaultRiskPeoplePageSize(queryDTO.getPageSize())
);
Page<CcdiProjectExternalPersonWarningItemVO> resultPage =
overviewMapper.selectExternalPersonWarningPage(page, queryDTO);
List<CcdiProjectExternalPersonWarningItemVO> rows =
defaultList(resultPage == null ? null : resultPage.getRecords()).stream()
.peek(item -> item.setActionLabel(ACTION_LABEL))
.toList();
CcdiProjectExternalPersonWarningVO warnings = new CcdiProjectExternalPersonWarningVO();
warnings.setRows(rows);
warnings.setTotal(resultPage == null ? 0L : resultPage.getTotal());
warnings.setPageNum(page.getCurrent());
warnings.setPageSize(page.getSize());
return warnings;
}
@Override
public CcdiProjectExternalRiskSummaryVO getExternalRiskSummary(Long projectId) {
ensureProjectExists(projectId);
CcdiProjectExternalRiskSummaryVO summary = overviewMapper.selectExternalRiskSummaryByProjectId(projectId);
if (summary == null) {
return new CcdiProjectExternalRiskSummaryVO();
}
summary.setTotal(defaultZero(summary.getTotal()));
summary.setHigh(defaultZero(summary.getHigh()));
summary.setMedium(defaultZero(summary.getMedium()));
summary.setLow(defaultZero(summary.getLow()));
summary.setNoRisk(defaultZero(summary.getNoRisk()));
return summary;
}
@Override
public List<CcdiProjectExternalPersonWarningExcel> exportExternalPersonWarnings(Long projectId) {
ensureProjectExists(projectId);
return defaultList(overviewMapper.selectExternalPersonWarningList(projectId)).stream()
.map(this::buildExternalPersonWarningExcelRow)
.toList();
}
@Override
public CcdiProjectRiskModelCardsVO getExternalRiskModelCards(Long projectId) {
ensureProjectExists(projectId);
CcdiProjectRiskModelCardsVO cards = new CcdiProjectRiskModelCardsVO();
cards.setCardList(defaultList(overviewMapper.selectExternalRiskModelCardsByProjectId(projectId)));
return cards;
}
@Override
public CcdiProjectRiskModelPeopleVO getExternalRiskModelPeople(CcdiProjectExternalRiskModelPeopleQueryDTO queryDTO) {
ensureProjectExists(queryDTO.getProjectId());
normalizeExternalRiskModelPeopleQuery(queryDTO);
Page<CcdiProjectRiskModelPeopleItemVO> page = new Page<>(
defaultPageNum(queryDTO.getPageNum()),
defaultPageSize(queryDTO.getPageSize())
);
Page<CcdiProjectRiskModelPeopleItemVO> resultPage =
overviewMapper.selectExternalRiskModelPeoplePage(page, queryDTO);
List<CcdiProjectRiskModelPeopleItemVO> rows = defaultList(resultPage == null ? null : resultPage.getRecords())
.stream()
.peek(item -> item.setActionLabel(ACTION_LABEL))
.toList();
CcdiProjectRiskModelPeopleVO people = new CcdiProjectRiskModelPeopleVO();
people.setRows(rows);
people.setTotal(resultPage == null ? 0L : resultPage.getTotal());
return people;
}
@Override
public List<CcdiProjectRiskModelPeopleExcel> exportExternalRiskModelPeople(
CcdiProjectExternalRiskModelPeopleQueryDTO queryDTO
) {
ensureProjectExists(queryDTO.getProjectId());
normalizeExternalRiskModelPeopleQuery(queryDTO);
return defaultList(overviewMapper.selectExternalRiskModelPeopleList(queryDTO)).stream()
.map(item -> buildRiskModelPeopleExcelRow(item, item.getStaffCode()))
.toList();
}
@Override
public CcdiProjectSuspiciousTransactionPageVO getSuspiciousTransactions(
CcdiProjectSuspiciousTransactionQueryDTO queryDTO
@@ -245,7 +390,7 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
ensureProjectExists(queryDTO.getProjectId());
normalizeSuspiciousTransactionQuery(queryDTO);
return defaultList(overviewMapper.selectSuspiciousTransactionList(queryDTO)).stream()
return defaultList(overviewMapper.selectReportSuspiciousTransactionList(queryDTO)).stream()
.map(this::buildSuspiciousTransactionExcelRow)
.toList();
}
@@ -334,11 +479,14 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
report.setUploadSubjects(defaultList(overviewMapper.selectReportUploadSubjects(projectId)).stream()
.peek(item -> item.setDataPeriod(formatDataPeriod(item.getMinTrxDate(), item.getMaxTrxDate())))
.toList());
report.setParams(buildReportParams(projectId));
report.setParams(buildReportParams(project));
report.setModelSummaries(defaultList(overviewMapper.selectReportRiskModelSummaries(projectId)));
report.setExternalRiskSummary(getExternalRiskSummary(projectId));
report.setExternalModelSummaries(buildExternalReportModelSummaries(projectId));
report.setRiskPeople(defaultList(overviewMapper.selectReportRiskPeople(projectId)).stream()
.peek(item -> item.setActionLabel(ACTION_LABEL))
.toList());
report.setExternalPersonWarnings(exportExternalPersonWarnings(projectId));
report.setSuspiciousTransactions(defaultList(
overviewMapper.selectReportSuspiciousTransactionList(suspiciousQuery)
));
@@ -445,10 +593,109 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
return row;
}
private CcdiProjectExternalPersonWarningExcel buildExternalPersonWarningExcelRow(
CcdiProjectExternalPersonWarningItemVO item
) {
CcdiProjectExternalPersonWarningExcel row = new CcdiProjectExternalPersonWarningExcel();
row.setName(item.getName());
row.setIdNo(item.getIdNo());
row.setSubjectType(item.getSubjectType());
row.setRiskLevel(item.getRiskLevel());
row.setModelCount(item.getModelCount());
row.setRiskPoint(item.getRiskPoint());
row.setRelatedObject(item.getRelatedObject());
row.setLatestTradeTime(item.getLatestTradeTime());
return row;
}
private List<CcdiProjectOverviewReportModelSummaryVO> buildExternalReportModelSummaries(Long projectId) {
return defaultList(overviewMapper.selectExternalRiskModelCardsByProjectId(projectId)).stream()
.map(this::buildExternalReportModelSummary)
.toList();
}
private CcdiProjectOverviewReportModelSummaryVO buildExternalReportModelSummary(CcdiProjectRiskModelCardVO card) {
CcdiProjectOverviewReportModelSummaryVO row = new CcdiProjectOverviewReportModelSummaryVO();
row.setModelCode(card.getModelCode());
row.setModelName(card.getModelName());
row.setWarningCount(card.getWarningCount());
row.setPeopleCount(card.getPeopleCount());
row.setPeopleNames("-");
return row;
}
private CcdiProjectRiskModelPeopleExcel buildRiskModelPeopleExcelRow(
CcdiProjectRiskModelPeopleItemVO item,
String subjectType
) {
CcdiProjectRiskModelPeopleExcel row = new CcdiProjectRiskModelPeopleExcel();
row.setPersonName(item.getStaffName());
row.setSubjectType(subjectType);
row.setIdNo(item.getIdNo());
row.setScopeName(item.getDepartment());
row.setModelNames(joinModelNames(item.getModelNames()));
row.setHitTags(joinHitTagNames(item.getHitTagList()));
return row;
}
private void ensureProjectExists(Long projectId) {
getRequiredProject(projectId);
}
private CcdiProjectRiskExclusion buildRiskExclusion(CcdiProjectRiskExclusionSaveDTO dto) {
if (dto == null) {
throw new ServiceException("排除参数不能为空");
}
String exclusionType = normalizeExclusionType(dto.getExclusionType());
String ruleCode = normalizeRequired(dto.getRuleCode(), "规则编码不能为空").toUpperCase();
String excludeReason = normalizeRequired(dto.getExcludeReason(), "排除原因不能为空");
if (excludeReason.length() > 1000) {
throw new ServiceException("排除原因不能超过1000个字符");
}
CcdiProjectRiskExclusion exclusion = new CcdiProjectRiskExclusion();
exclusion.setProjectId(dto.getProjectId());
exclusion.setRuleCode(ruleCode);
exclusion.setExclusionType(exclusionType);
exclusion.setExcludeReason(excludeReason);
exclusion.setStaffIdCard(normalizeBlankToNull(dto.getStaffIdCard()));
exclusion.setBankStatementId(dto.getBankStatementId());
if (EXCLUSION_TYPE_STATEMENT.equals(exclusionType) && exclusion.getBankStatementId() == null) {
throw new ServiceException("流水型排除必须指定流水ID");
}
if (EXCLUSION_TYPE_OBJECT.equals(exclusionType) && (exclusion.getStaffIdCard() == null || exclusion.getStaffIdCard().isBlank())) {
throw new ServiceException("对象型排除必须指定人员证件号");
}
String operator = SecurityUtils.getUsername();
Date now = new Date();
exclusion.setCreateBy(operator);
exclusion.setCreateTime(now);
exclusion.setUpdateBy(operator);
exclusion.setUpdateTime(now);
return exclusion;
}
private String normalizeExclusionType(String exclusionType) {
String type = normalizeRequired(exclusionType, "排除类型不能为空").toUpperCase();
if (!EXCLUSION_TYPE_STATEMENT.equals(type) && !EXCLUSION_TYPE_OBJECT.equals(type)) {
throw new ServiceException("排除类型不正确");
}
return type;
}
private String normalizeRequired(String value, String message) {
if (value == null || value.isBlank()) {
throw new ServiceException(message);
}
return value.trim();
}
private String normalizeBlankToNull(String value) {
if (value == null || value.isBlank()) {
return null;
}
return value.trim();
}
private void normalizeRiskModelPeopleQuery(CcdiProjectRiskModelPeopleQueryDTO queryDTO) {
if (queryDTO.getMatchMode() == null || queryDTO.getMatchMode().isBlank()) {
queryDTO.setMatchMode("ANY");
@@ -457,7 +704,20 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
queryDTO.setMatchMode(queryDTO.getMatchMode().trim().toUpperCase());
}
private void normalizeExternalRiskModelPeopleQuery(CcdiProjectExternalRiskModelPeopleQueryDTO queryDTO) {
if (queryDTO.getMatchMode() == null || queryDTO.getMatchMode().isBlank()) {
queryDTO.setMatchMode("ANY");
return;
}
queryDTO.setMatchMode(queryDTO.getMatchMode().trim().toUpperCase());
}
private void normalizeSuspiciousTransactionQuery(CcdiProjectSuspiciousTransactionQueryDTO queryDTO) {
if (queryDTO.getModelCode() != null && !queryDTO.getModelCode().isBlank()) {
queryDTO.setModelCode(queryDTO.getModelCode().trim().toUpperCase());
} else {
queryDTO.setModelCode(null);
}
if (queryDTO.getSuspiciousType() == null || queryDTO.getSuspiciousType().isBlank()) {
queryDTO.setSuspiciousType("ALL");
return;
@@ -526,15 +786,18 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
}
private CcdiProjectSuspiciousTransactionExcel buildSuspiciousTransactionExcelRow(
CcdiProjectSuspiciousTransactionItemVO item
CcdiProjectOverviewReportSuspiciousTransactionVO item
) {
CcdiProjectSuspiciousTransactionExcel row = new CcdiProjectSuspiciousTransactionExcel();
row.setTrxDate(item.getTrxDate());
row.setSuspiciousPersonName(item.getSuspiciousPersonName());
row.setRelatedPersonName(item.getRelatedPersonName());
row.setLeAccountNo(item.getLeAccountNo());
row.setLeAccountName(item.getLeAccountName());
row.setCustomerAccountName(item.getCustomerAccountName());
row.setCustomerAccountNo(item.getCustomerAccountNo());
row.setRelatedStaffDisplay(formatRelatedStaff(item.getRelatedStaffName(), item.getRelatedStaffCode()));
row.setRelationType(item.getRelationType());
row.setSummaryAndCashType(formatSummaryAndCashType(item.getUserMemo(), item.getCashType()));
row.setUserMemo(item.getUserMemo());
row.setCashType(item.getCashType());
row.setHitTags(item.getHitTags());
row.setDisplayAmount(item.getDisplayAmount());
return row;
}
@@ -566,21 +829,23 @@ 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;
}))
private List<CcdiProjectOverviewReportParamVO> buildReportParams(CcdiProject project) {
Long effectiveProjectId = "default".equals(project.getConfigType()) ? 0L : project.getProjectId();
return defaultList(modelParamMapper.selectByProjectId(effectiveProjectId)).stream()
.map(this::buildReportParamRow)
.toList();
}
private CcdiProjectOverviewReportParamVO buildReportParamRow(CcdiModelParam param) {
CcdiProjectOverviewReportParamVO row = new CcdiProjectOverviewReportParamVO();
row.setModelName(param.getModelName());
row.setParamName(param.getParamName());
row.setParamValue(param.getParamValue());
row.setParamUnit(param.getParamUnit());
row.setParamDesc(param.getParamDesc());
return row;
}
private String formatDataPeriod(String minTrxDate, String maxTrxDate) {
if (minTrxDate == null || minTrxDate.isBlank() || maxTrxDate == null || maxTrxDate.isBlank()) {
return "-";
@@ -607,6 +872,21 @@ public class CcdiProjectOverviewServiceImpl implements ICcdiProjectOverviewServi
return safeMemo + "/" + safeCashType;
}
private String joinModelNames(List<String> modelNames) {
return defaultList(modelNames).stream()
.filter(item -> item != null && !item.isBlank())
.distinct()
.collect(Collectors.joining(""));
}
private String joinHitTagNames(List<CcdiProjectRiskHitTagVO> hitTags) {
return defaultList(hitTags).stream()
.map(CcdiProjectRiskHitTagVO::getRuleName)
.filter(item -> item != null && !item.isBlank())
.distinct()
.collect(Collectors.joining(""));
}
private CcdiProjectPersonAnalysisAbnormalDetailVO buildAbnormalDetail(
List<CcdiBankStatementListVO> statementRows,
List<CcdiProjectPersonAnalysisObjectRecordVO> objectRows

View File

@@ -44,19 +44,33 @@ public class CcdiProjectRiskDetailWorkbookExporter {
private void writeSuspiciousSheet(Sheet sheet, List<CcdiProjectSuspiciousTransactionExcel> rows) {
Row header = sheet.createRow(0);
String[] headers = { "交易时间", "可疑人员", "关联人", "关联员工", "关系", "摘要/交易类型", "交易金额" };
String[] headers = {
"交易时间",
"本方账户",
"本方主体",
"对方名称",
"对方账户",
"关联员工",
"摘要",
"交易类型",
"异常标签",
"交易金额"
};
writeHeader(header, headers);
for (int i = 0; i < rows.size(); i++) {
CcdiProjectSuspiciousTransactionExcel item = rows.get(i);
Row row = sheet.createRow(i + 1);
row.createCell(0).setCellValue(safeText(item.getTrxDate()));
row.createCell(1).setCellValue(safeText(item.getSuspiciousPersonName()));
row.createCell(2).setCellValue(safeText(item.getRelatedPersonName()));
row.createCell(3).setCellValue(safeText(item.getRelatedStaffDisplay()));
row.createCell(4).setCellValue(safeText(item.getRelationType()));
row.createCell(5).setCellValue(safeText(item.getSummaryAndCashType()));
row.createCell(6).setCellValue(safeNumber(item.getDisplayAmount()));
row.createCell(1).setCellValue(safeText(item.getLeAccountNo()));
row.createCell(2).setCellValue(safeText(item.getLeAccountName()));
row.createCell(3).setCellValue(safeText(item.getCustomerAccountName()));
row.createCell(4).setCellValue(safeText(item.getCustomerAccountNo()));
row.createCell(5).setCellValue(safeText(item.getRelatedStaffDisplay()));
row.createCell(6).setCellValue(safeText(item.getUserMemo()));
row.createCell(7).setCellValue(safeText(item.getCashType()));
row.createCell(8).setCellValue(safeText(item.getHitTags()));
row.createCell(9).setCellValue(safeNumber(item.getDisplayAmount()));
}
}

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.constants.CcdiProjectStatusConstants;
import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.ProjectAccessScope;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectImportHistoryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSaveDTO;
@@ -14,6 +15,7 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectStatusCountsVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectVO;
import com.ruoyi.ccdi.project.mapper.CcdiBankTagTaskMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.ccdi.project.service.ICcdiProjectService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.lsfx.client.LsfxAnalysisClient;
@@ -54,6 +56,9 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
@Resource
private ApplicationEventPublisher applicationEventPublisher;
@Resource
private CcdiProjectAccessService projectAccessService;
@Override
@Transactional(rollbackFor = Exception.class)
public CcdiProjectVO createProject(CcdiProjectSaveDTO dto) {
@@ -82,7 +87,7 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
// 5. 返回VO
CcdiProjectVO vo = new CcdiProjectVO();
BeanUtils.copyProperties(project, vo);
fillLatestTagFailure(project, vo);
fillProjectExtraFields(project, vo);
return vo;
}
@@ -96,6 +101,7 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
if (existingProject == null) {
throw new ServiceException("项目不存在");
}
projectAccessService.assertCanOperate(dto.getProjectId());
// 只更新允许修改的字段
existingProject.setProjectName(dto.getProjectName());
@@ -106,39 +112,59 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
CcdiProjectVO vo = new CcdiProjectVO();
BeanUtils.copyProperties(existingProject, vo);
fillProjectExtraFields(existingProject, vo);
return vo;
}
@Override
public boolean deleteProject(Long projectId) {
return projectMapper.deleteById(projectId) > 0;
public boolean deleteProject(Long projectId, String operator) {
projectAccessService.assertCanDelete(projectId);
return projectMapper.markProjectDeleted(projectId, resolveOperator(operator)) > 0;
}
@Override
public void restoreProject(Long projectId, String operator) {
projectAccessService.assertCanManageDeletedProjects();
int rows = projectMapper.restoreDeletedProject(projectId, resolveOperator(operator));
if (rows <= 0) {
throw new ServiceException("仅已删除项目允许恢复");
}
log.info("【项目】项目状态变更: projectId={}, oldStatus={}, oldStatusLabel={}, newStatus={}, newStatusLabel={}, operator={}",
projectId, CcdiProjectStatusConstants.DELETED, resolveStatusLabel(CcdiProjectStatusConstants.DELETED),
CcdiProjectStatusConstants.COMPLETED, resolveStatusLabel(CcdiProjectStatusConstants.COMPLETED),
resolveOperator(operator));
}
@Override
public CcdiProjectVO getProjectById(Long projectId) {
projectAccessService.assertCanRead(projectId);
CcdiProject project = projectMapper.selectById(projectId);
if (project == null) {
return null;
}
CcdiProjectVO vo = new CcdiProjectVO();
BeanUtils.copyProperties(project, vo);
fillLatestTagFailure(project, vo);
fillProjectExtraFields(project, vo);
return vo;
}
@Override
public Page<CcdiProjectVO> selectProjectPage(Page<CcdiProjectVO> page, CcdiProjectQueryDTO queryDTO) {
return projectMapper.selectProjectPage(page, queryDTO);
ProjectAccessScope scope = projectAccessService.buildCurrentScope();
Page<CcdiProjectVO> result = projectMapper.selectProjectPage(page, queryDTO, scope);
fillProjectExtraFields(result.getRecords());
return result;
}
@Override
public List<CcdiProjectHistoryListItemVO> listHistoryProjects(CcdiProjectQueryDTO queryDTO) {
return projectMapper.selectHistoryProjects(queryDTO);
return projectMapper.selectHistoryProjects(queryDTO, projectAccessService.buildCurrentScope());
}
@Override
@Transactional(rollbackFor = Exception.class)
public CcdiProjectVO importFromHistory(CcdiProjectImportHistoryDTO dto, String operator) {
projectAccessService.assertSourceProjectsReadable(dto.getSourceProjectIds());
CcdiProjectSaveDTO saveDTO = new CcdiProjectSaveDTO();
saveDTO.setProjectName(dto.getProjectName());
saveDTO.setDescription(dto.getDescription());
@@ -158,50 +184,49 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
@Override
public CcdiProjectStatusCountsVO getStatusCounts() {
CcdiProjectStatusCountsVO vo = new CcdiProjectStatusCountsVO();
ProjectAccessScope scope = projectAccessService.buildCurrentScope();
// 统计全部项目
Long totalCount = projectMapper.selectCount(null);
LambdaQueryWrapper<CcdiProject> baseWrapper = buildScopeWrapper(scope);
Long totalCount = projectMapper.selectCount(baseWrapper);
vo.setAll(totalCount);
// 统计进行中项目状态0
Long status0Count = projectMapper.selectCount(
new LambdaQueryWrapper<CcdiProject>()
.eq(CcdiProject::getStatus, CcdiProjectStatusConstants.PROCESSING)
);
Long status0Count = projectMapper.selectCount(buildScopeWrapper(scope)
.eq(CcdiProject::getStatus, CcdiProjectStatusConstants.PROCESSING));
vo.setStatus0(status0Count);
// 统计已完成项目状态1
Long status1Count = projectMapper.selectCount(
new LambdaQueryWrapper<CcdiProject>()
.eq(CcdiProject::getStatus, CcdiProjectStatusConstants.COMPLETED)
);
Long status1Count = projectMapper.selectCount(buildScopeWrapper(scope)
.eq(CcdiProject::getStatus, CcdiProjectStatusConstants.COMPLETED));
vo.setStatus1(status1Count);
// 统计已归档项目状态2
Long status2Count = projectMapper.selectCount(
new LambdaQueryWrapper<CcdiProject>()
.eq(CcdiProject::getStatus, CcdiProjectStatusConstants.ARCHIVED)
);
Long status2Count = projectMapper.selectCount(buildScopeWrapper(scope)
.eq(CcdiProject::getStatus, CcdiProjectStatusConstants.ARCHIVED));
vo.setStatus2(status2Count);
Long status3Count = projectMapper.selectCount(
new LambdaQueryWrapper<CcdiProject>()
.eq(CcdiProject::getStatus, CcdiProjectStatusConstants.TAGGING)
);
Long status3Count = projectMapper.selectCount(buildScopeWrapper(scope)
.eq(CcdiProject::getStatus, CcdiProjectStatusConstants.TAGGING));
vo.setStatus3(status3Count);
Long status4Count = projectMapper.selectCount(
new LambdaQueryWrapper<CcdiProject>()
.eq(CcdiProject::getStatus, CcdiProjectStatusConstants.TAG_FAILED)
);
Long status4Count = projectMapper.selectCount(buildScopeWrapper(scope)
.eq(CcdiProject::getStatus, CcdiProjectStatusConstants.TAG_FAILED));
vo.setStatus4(status4Count);
Long status5Count = 0L;
if (isProjectAdmin(scope)) {
status5Count = projectMapper.selectDeletedProjectCount(scope);
}
vo.setStatus5(status5Count);
return vo;
}
@Override
public void archiveProject(Long projectId, String operator) {
CcdiProject project = getRequiredProject(projectId);
projectAccessService.assertCanOperate(projectId);
if (CcdiProjectStatusConstants.ARCHIVED.equals(project.getStatus())) {
throw new ServiceException("项目已归档,无需重复操作");
}
@@ -223,6 +248,9 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
public void updateProjectStatus(Long projectId, String status, String operator) {
CcdiProject project = getRequiredProject(projectId);
String oldStatus = project.getStatus();
if (CcdiProjectStatusConstants.DELETED.equals(project.getStatus())) {
throw new ServiceException("已删除项目不允许更新状态");
}
if (CcdiProjectStatusConstants.ARCHIVED.equals(project.getStatus())
&& !CcdiProjectStatusConstants.ARCHIVED.equals(status)) {
throw new ServiceException("已归档项目不允许重新进入打标流程");
@@ -241,6 +269,9 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
@Override
public void ensureProjectCanStartTagging(Long projectId) {
CcdiProject project = getRequiredProject(projectId);
if (CcdiProjectStatusConstants.DELETED.equals(project.getStatus())) {
throw new ServiceException("已删除项目不允许重新进入打标流程");
}
if (CcdiProjectStatusConstants.ARCHIVED.equals(project.getStatus())) {
throw new ServiceException("已归档项目不允许重新进入打标流程");
}
@@ -249,6 +280,9 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
@Override
public void ensureProjectNotArchived(Long projectId, String message) {
CcdiProject project = getRequiredProject(projectId);
if (CcdiProjectStatusConstants.DELETED.equals(project.getStatus())) {
throw new ServiceException("已删除项目暂不允许操作");
}
if (CcdiProjectStatusConstants.ARCHIVED.equals(project.getStatus())) {
throw new ServiceException(message);
}
@@ -257,6 +291,9 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
@Override
public void ensureProjectWritable(Long projectId, String message) {
CcdiProject project = getRequiredProject(projectId);
if (CcdiProjectStatusConstants.DELETED.equals(project.getStatus())) {
throw new ServiceException("已删除项目暂不允许操作");
}
if (CcdiProjectStatusConstants.TAGGING.equals(project.getStatus())) {
throw new ServiceException(message);
}
@@ -277,6 +314,7 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
case CcdiProjectStatusConstants.ARCHIVED -> "已归档";
case CcdiProjectStatusConstants.TAGGING -> "打标中";
case CcdiProjectStatusConstants.TAG_FAILED -> "打标失败";
case CcdiProjectStatusConstants.DELETED -> "已删除";
default -> "未知";
};
}
@@ -293,6 +331,44 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
vo.setLatestTagTaskEndTime(latestFailedTask.getEndTime());
}
private LambdaQueryWrapper<CcdiProject> buildScopeWrapper(ProjectAccessScope scope) {
LambdaQueryWrapper<CcdiProject> wrapper = new LambdaQueryWrapper<>();
wrapper.ne(CcdiProject::getStatus, CcdiProjectStatusConstants.DELETED);
if (scope != null && !scope.isViewAllProjects()) {
wrapper.eq(CcdiProject::getCreateBy, scope.getUsername());
}
return wrapper;
}
private boolean isProjectAdmin(ProjectAccessScope scope) {
return scope != null && (scope.isSuperAdmin() || scope.isProjectManager());
}
private void fillProjectExtraFields(List<CcdiProjectVO> records) {
if (records == null || records.isEmpty()) {
return;
}
ProjectAccessScope scope = projectAccessService.buildCurrentScope();
for (CcdiProjectVO vo : records) {
fillProjectAccessFields(vo, scope);
}
}
private void fillProjectExtraFields(CcdiProject project, CcdiProjectVO vo) {
fillLatestTagFailure(project, vo);
fillProjectAccessFields(vo, projectAccessService.buildCurrentScope());
}
private void fillProjectAccessFields(CcdiProjectVO vo, ProjectAccessScope scope) {
if (vo == null || scope == null) {
return;
}
boolean ownedByCurrentUser = Objects.equals(scope.getUsername(), vo.getCreateBy());
vo.setOwnedByCurrentUser(ownedByCurrentUser);
vo.setCanOperate(scope.isSuperAdmin() || ownedByCurrentUser);
vo.setCanDelete(isProjectAdmin(scope) || ownedByCurrentUser);
}
private String resolveOperator(String operator) {
return StringUtils.hasText(operator) ? operator : "system";
}

View File

@@ -10,6 +10,8 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferDetailQueryD
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectExtendedTransferQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityDetailQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectFamilyAssetLiabilityListQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectIncreaseLendingQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectIncreaseLendingExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseListItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExtendedPurchaseListVO;
@@ -25,10 +27,13 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListItemV
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyDebtDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyIncomeDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectIncreaseLendingListItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectIncreaseLendingListVO;
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectSpecialCheckMapper;
import com.ruoyi.ccdi.project.service.ICcdiProjectSpecialCheckService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import jakarta.annotation.Resource;
import java.math.BigDecimal;
import java.util.List;
@@ -178,6 +183,37 @@ public class CcdiProjectSpecialCheckServiceImpl implements ICcdiProjectSpecialCh
return detail;
}
@Override
public CcdiProjectIncreaseLendingListVO getIncreaseLendingList(CcdiProjectIncreaseLendingQueryDTO queryDTO) {
validateIncreaseLendingQuery(queryDTO);
Page<CcdiProjectIncreaseLendingListItemVO> page = new Page<>(
defaultPageNum(queryDTO.getPageNum()),
defaultPageSize(queryDTO.getPageSize())
);
Page<CcdiProjectIncreaseLendingListItemVO> resultPage = specialCheckMapper.selectIncreaseLendingPage(page, queryDTO);
CcdiProjectIncreaseLendingListVO result = new CcdiProjectIncreaseLendingListVO();
result.setRows(resultPage == null ? List.of() : defaultList(resultPage.getRecords()));
result.setTotal(resultPage == null ? 0L : resultPage.getTotal());
return result;
}
@Override
public List<CcdiProjectIncreaseLendingExcel> exportIncreaseLendingList(CcdiProjectIncreaseLendingQueryDTO queryDTO) {
validateIncreaseLendingQuery(queryDTO);
return defaultList(specialCheckMapper.selectIncreaseLendingExportList(queryDTO));
}
private void validateIncreaseLendingQuery(CcdiProjectIncreaseLendingQueryDTO queryDTO) {
ensureProjectExists(queryDTO.getProjectId());
if (StringUtils.isBlank(queryDTO.getStaffId())
&& StringUtils.isBlank(queryDTO.getStaffIdCard())
&& StringUtils.isBlank(queryDTO.getApprover())) {
throw new ServiceException("柜员号、员工身份证和审核人柜员号至少填写一项");
}
}
private void ensureProjectExists(Long projectId) {
CcdiProject project = projectMapper.selectById(projectId);
if (project == null) {

View File

@@ -215,6 +215,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{item}
</foreach>
</if>
<if test="query.ourCertNos != null and query.ourCertNos.size() > 0">
AND bs.cret_no IN
<foreach collection="query.ourCertNos" item="item" open="(" separator="," close=")">
#{item}
</foreach>
</if>
<if test="query.ourBanks != null and query.ourBanks.size() > 0">
AND bs.BANK IN
<foreach collection="query.ourBanks" item="item" open="(" separator="," close=")">

View File

@@ -41,6 +41,23 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
and trim(bs.cret_no) != ''
</sql>
<sql id="externalPersonPredicateSql">
bs.cret_no is not null
and trim(bs.cret_no) != ''
and not exists (
select 1
from ccdi_base_staff staff
where staff.id_card = bs.cret_no
)
and not exists (
select 1
from ccdi_staff_fmy_relation relation
where relation.status = 1
and relation.relation_cert_no = bs.cret_no
)
and trim(IFNULL(bs.LE_ACCOUNT_NAME, '')) &lt;&gt; trim(IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''))
</sql>
<sql id="cashDepositPredicate">
(
(
@@ -466,6 +483,74 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
)
</select>
<select id="selectExternalSingleLargeAmountStatements" resultMap="BankTagStatementHitResultMap">
select
bs.bank_statement_id AS bankStatementId,
bs.group_id AS groupId,
bs.batch_id AS logId,
CONCAT(
'外部人员“', IFNULL(bs.LE_ACCOUNT_NAME, ''),
'”单笔交易金额 ', CAST(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS CHAR),
' 元,超过阈值 ', CAST(#{threshold} AS CHAR),
' 元,对手方“', IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''), '”'
) AS reasonDetail
from ccdi_bank_statement bs
where bs.project_id = #{projectId}
and <include refid="externalPersonPredicateSql"/>
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > #{threshold}
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 0
and <include refid="financialProductExclusionPredicate"/>
</select>
<select id="selectExternalCumulativeTransactionAmountObjects" resultMap="BankTagObjectHitResultMap">
select
'EXTERNAL_CERT_NO' AS objectType,
t.certNo AS objectKey,
CONCAT(
'外部人员“', IFNULL(t.personName, ''),
'”累计交易金额 ', CAST(t.totalAmount AS CHAR),
' 元,超过阈值 ', CAST(#{threshold} AS CHAR), ' 元'
) AS reasonDetail
from (
select
bs.cret_no AS certNo,
max(IFNULL(bs.LE_ACCOUNT_NAME, '')) AS personName,
ROUND(SUM(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0))), 2) AS totalAmount
from ccdi_bank_statement bs
where bs.project_id = #{projectId}
and <include refid="externalPersonPredicateSql"/>
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 0
and <include refid="financialProductExclusionPredicate"/>
group by bs.cret_no
having ROUND(SUM(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0))), 2) > #{threshold}
) t
</select>
<select id="selectExternalAnnualTurnoverObjects" resultMap="BankTagObjectHitResultMap">
select
'EXTERNAL_CERT_NO' AS objectType,
t.certNo AS objectKey,
CONCAT(
'外部人员“', IFNULL(t.personName, ''),
'”近一年流水交易额 ', CAST(t.annualAmount AS CHAR),
' 元,超过阈值 ', CAST(#{threshold} AS CHAR), ' 元'
) AS reasonDetail
from (
select
bs.cret_no AS certNo,
max(IFNULL(bs.LE_ACCOUNT_NAME, '')) AS personName,
ROUND(SUM(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0))), 2) AS annualAmount
from ccdi_bank_statement bs
where bs.project_id = #{projectId}
and <include refid="externalPersonPredicateSql"/>
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 0
and <include refid="financialProductExclusionPredicate"/>
and STR_TO_DATE(LEFT(TRIM(bs.TRX_DATE), 10), '%Y-%m-%d') >= DATE_SUB(CURDATE(), INTERVAL 12 MONTH)
group by bs.cret_no
having ROUND(SUM(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0))), 2) > #{threshold}
) t
</select>
<select id="selectAbnormalCustomerTransactionStatements" resultMap="BankTagStatementHitResultMap">
select
hit.bankStatementId AS bankStatementId,
@@ -702,6 +787,72 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
)
</select>
<select id="selectExternalGamblingMemoStatements" resultMap="BankTagStatementHitResultMap">
select
bs.bank_statement_id AS bankStatementId,
bs.group_id AS groupId,
bs.batch_id AS logId,
CONCAT(
'外部人员“', IFNULL(bs.LE_ACCOUNT_NAME, ''),
'”摘要/对手方命中疑似赌博关键词,摘要“', IFNULL(bs.USER_MEMO, ''),
'”,对手方“', IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''),
'”,交易金额 ', CAST(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS CHAR), ' 元'
) AS reasonDetail
from ccdi_bank_statement bs
where bs.project_id = #{projectId}
and <include refid="externalPersonPredicateSql"/>
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 0
and (
IFNULL(bs.USER_MEMO, '') REGEXP '游戏|抖币|体彩|福彩|彩票|赌博|赌球|下注|投注|球赛投注|外围|博彩|六合|时时彩|赛车|赌场|筹码|盘口|返水|洗码|庄家|闲家|百家乐|斗牛|炸金花|牌九|麻将|牌局|捕鱼|电子游艺|VIP666|USDT下注'
or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '游戏|抖币|体彩|福彩|彩票|赌博|赌球|下注|投注|球赛投注|外围|博彩|六合|时时彩|赛车|赌场|筹码|盘口|返水|洗码|庄家|闲家|百家乐|斗牛|炸金花|牌九|麻将|牌局|捕鱼|电子游艺|VIP666|USDT下注'
or IFNULL(bs.CASH_TYPE, '') REGEXP '游戏|抖币|体彩|福彩|彩票|赌博|赌球|下注|投注|球赛投注|外围|博彩|六合|时时彩|赛车|赌场|筹码|盘口|返水|洗码|庄家|闲家|百家乐|斗牛|炸金花|牌九|麻将|牌局|捕鱼|电子游艺|VIP666|USDT下注'
)
</select>
<select id="selectExternalMultiPartyGamblingTransferObjects" resultMap="BankTagObjectHitResultMap">
select
'EXTERNAL_CERT_NO' AS objectType,
t.certNo AS objectKey,
CONCAT(
'外部人员“', IFNULL(MAX(t.personName), ''),
'”交易日 ', MAX(t.tradeDate),
' 发生 ', CAST(MAX(t.hitCount) AS CHAR),
' 笔疑似赌博交易,涉及 ', CAST(MAX(t.partyCount) AS CHAR),
' 个对手方,金额合计 ', CAST(MAX(t.totalAmount) AS CHAR), ' 元'
) AS reasonDetail
from (
select
source.certNo AS certNo,
max(source.personName) AS personName,
source.tradeDate AS tradeDate,
COUNT(1) AS hitCount,
COUNT(DISTINCT source.customerAccountName) AS partyCount,
ROUND(SUM(source.tradeAmount), 2) AS totalAmount
from (
select
bs.cret_no AS certNo,
IFNULL(bs.LE_ACCOUNT_NAME, '') AS personName,
LEFT(TRIM(bs.TRX_DATE), 10) AS tradeDate,
bs.CUSTOMER_ACCOUNT_NAME AS customerAccountName,
GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS tradeAmount
from ccdi_bank_statement bs
where bs.project_id = #{projectId}
and <include refid="externalPersonPredicateSql"/>
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) between #{amountMinThreshold} and #{amountMaxThreshold}
and IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') &lt;&gt; ''
and (
IFNULL(bs.USER_MEMO, '') REGEXP '微信|wechat|WeChat|财付通|Tenpay|支付宝|Alipay|转账|红包|牌局|赌'
or IFNULL(bs.CASH_TYPE, '') REGEXP '微信|wechat|WeChat|财付通|Tenpay|支付宝|Alipay|转账|红包|牌局|赌'
or IFNULL(bs.CUSTOMER_ACCOUNT_NAME, '') REGEXP '微信|wechat|WeChat|财付通|Tenpay|支付宝|Alipay'
)
) source
group by source.certNo, source.tradeDate
having COUNT(1) > 2
and COUNT(DISTINCT source.customerAccountName) >= 2
) t
group by t.certNo
</select>
<select id="selectSpecialAmountTransactionStatements" resultMap="BankTagStatementHitResultMap">
select
bs.bank_statement_id AS bankStatementId,
@@ -728,6 +879,74 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
)
</select>
<select id="selectExternalToStaffOrFamilyTransactionStatements" resultMap="BankTagStatementHitResultMap">
select distinct
bs.bank_statement_id AS bankStatementId,
bs.group_id AS groupId,
bs.batch_id AS logId,
CONCAT(
'外部人员“', IFNULL(bs.LE_ACCOUNT_NAME, ''),
'”与', CASE
WHEN counter_account.owner_type = 'EMPLOYEE' THEN '员工'
WHEN counter_account.owner_type = 'RELATION' THEN '员工亲属'
WHEN counter_staff.id_card is not null THEN '员工'
WHEN counter_relation.relation_cert_no is not null THEN '员工亲属'
ELSE '员工/员工亲属'
END,
'“', IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''),
'”发生资金往来,交易金额 ',
CAST(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS CHAR), ' 元'
) AS reasonDetail
from ccdi_bank_statement bs
left join ccdi_account_info counter_account
on trim(bs.CUSTOMER_ACCOUNT_NO) != ''
and counter_account.account_no = trim(bs.CUSTOMER_ACCOUNT_NO)
and counter_account.owner_type in ('EMPLOYEE', 'RELATION', 'INTERMEDIARY', 'CREDIT_CUSTOMER')
left join ccdi_base_staff counter_staff
on counter_account.account_no is null
and trim(bs.CUSTOMER_ACCOUNT_NAME) != ''
and counter_staff.name = trim(bs.CUSTOMER_ACCOUNT_NAME)
left join ccdi_staff_fmy_relation counter_relation
on counter_account.account_no is null
and counter_relation.status = 1
and trim(bs.CUSTOMER_ACCOUNT_NAME) != ''
and counter_relation.relation_name = trim(bs.CUSTOMER_ACCOUNT_NAME)
where bs.project_id = #{projectId}
and <include refid="externalPersonPredicateSql"/>
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 0
and (
counter_account.owner_type in ('EMPLOYEE', 'RELATION')
or (
counter_account.account_no is null
and (
counter_staff.id_card is not null
or counter_relation.relation_cert_no is not null
)
)
)
</select>
<select id="selectExternalNightTransactionStatements" resultMap="BankTagStatementHitResultMap">
select
bs.bank_statement_id AS bankStatementId,
bs.group_id AS groupId,
bs.batch_id AS logId,
CONCAT(
'外部人员“', IFNULL(bs.LE_ACCOUNT_NAME, ''),
'”夜间交易,交易时间 ', IFNULL(bs.TRX_DATE, ''),
',对手方“', IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''),
'”,交易金额 ', CAST(GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) AS CHAR), ' 元'
) AS reasonDetail
from ccdi_bank_statement bs
where bs.project_id = #{projectId}
and <include refid="externalPersonPredicateSql"/>
and GREATEST(IFNULL(bs.AMOUNT_DR, 0), IFNULL(bs.AMOUNT_CR, 0)) > 0
and (
HOUR(STR_TO_DATE(LEFT(TRIM(bs.TRX_DATE), 19), '%Y-%m-%d %H:%i:%s')) >= 22
or HOUR(STR_TO_DATE(LEFT(TRIM(bs.TRX_DATE), 19), '%Y-%m-%d %H:%i:%s')) &lt; 6
)
</select>
<select id="selectMonthlyFixedIncomeObjects" resultMap="BankTagObjectHitResultMap">
select
'STAFF_ID_CARD' AS objectType,

View File

@@ -55,6 +55,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
from ccdi_bank_statement_tag_result
where project_id = #{projectId}
and bank_statement_id is not null
and not exists (
select 1
from ccdi_project_risk_exclusion ex
where ex.project_id = ccdi_bank_statement_tag_result.project_id
and ex.rule_code = ccdi_bank_statement_tag_result.rule_code
and ex.exclusion_type = 'STATEMENT'
and ex.bank_statement_id = ccdi_bank_statement_tag_result.bank_statement_id
)
and bank_statement_id IN
<foreach collection="bankStatementIds" item="item" open="(" separator="," close=")">
#{item}

View File

@@ -40,10 +40,23 @@
FROM ccdi_project p
LEFT JOIN sys_user u ON p.create_by = u.user_name AND u.del_flag = '0'
<where>
<choose>
<when test="queryDTO != null and queryDTO.includeDeleted == true and scope != null and scope.viewAllProjects">
AND p.del_flag = '2'
AND p.status = '5'
</when>
<otherwise>
AND p.del_flag = '0'
AND p.status != '5'
</otherwise>
</choose>
<if test="scope != null and !scope.viewAllProjects">
AND p.create_by = #{scope.username}
</if>
<if test="queryDTO.projectName != null and queryDTO.projectName != ''">
AND p.project_name LIKE CONCAT('%', #{queryDTO.projectName}, '%')
</if>
<if test="queryDTO.status != null and queryDTO.status != ''">
<if test="!(queryDTO != null and queryDTO.includeDeleted == true) and queryDTO.status != null and queryDTO.status != ''">
AND p.status = #{queryDTO.status}
</if>
</where>
@@ -61,6 +74,10 @@
FROM ccdi_project p
<where>
p.status in ('1', '2')
AND p.del_flag = '0'
<if test="scope != null and !scope.viewAllProjects">
AND p.create_by = #{scope.username}
</if>
<if test="queryDTO.projectName != null and queryDTO.projectName != ''">
AND p.project_name LIKE CONCAT('%', #{queryDTO.projectName}, '%')
</if>
@@ -68,6 +85,41 @@
ORDER BY p.update_time DESC
</select>
<update id="markProjectDeleted">
update ccdi_project
set status = '5',
del_flag = '2',
update_by = #{operator},
update_time = now()
where project_id = #{projectId}
and del_flag = '0'
and status != '5'
</update>
<update id="restoreDeletedProject">
update ccdi_project
set status = '1',
del_flag = '0',
is_archived = 0,
update_by = #{operator},
update_time = now()
where project_id = #{projectId}
and del_flag = '2'
and status = '5'
</update>
<select id="selectDeletedProjectCount" resultType="java.lang.Long">
select count(1)
from ccdi_project p
<where>
p.del_flag = '2'
and p.status = '5'
<if test="scope != null and !scope.viewAllProjects">
AND p.create_by = #{scope.username}
</if>
</where>
</select>
<update id="updateRiskCountsByProjectId">
update ccdi_project
set target_count = #{targetCount},

View File

@@ -79,6 +79,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
on dept.dept_id = coalesce(direct_staff.dept_id, statement_staff.dept_id, family_staff.dept_id)
where tr.project_id = #{projectId}
and coalesce(direct_staff.id_card, statement_staff.id_card, family_staff.id_card) is not null
and not exists (
select 1
from ccdi_project_risk_exclusion ex
where ex.project_id = tr.project_id
and ex.rule_code = tr.rule_code
and (
(ex.exclusion_type = 'STATEMENT'
and ex.bank_statement_id = tr.bank_statement_id)
or (ex.exclusion_type = 'OBJECT'
and ex.staff_id_card = coalesce(direct_staff.id_card, statement_staff.id_card, family_staff.id_card))
)
)
</sql>
<delete id="deleteByProjectId">

View File

@@ -33,6 +33,38 @@
select="selectRiskHitTagsByScope"/>
</resultMap>
<resultMap id="ExternalRiskModelPeopleItemResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO">
<id property="idNo" column="cert_no"/>
<result property="staffName" column="person_name"/>
<result property="staffCode" column="subject_type"/>
<result property="department" column="related_object"/>
<collection property="modelNames"
column="{projectId=project_id,certNo=cert_no,selectedModelCodes=selected_model_codes}"
ofType="java.lang.String"
select="selectExternalRiskModelNamesByScope"/>
<collection property="hitTagList"
column="{projectId=project_id,certNo=cert_no,selectedModelCodes=selected_model_codes}"
ofType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO"
select="selectExternalRiskHitTagsByScope"/>
</resultMap>
<resultMap id="ExternalPersonWarningItemResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectExternalPersonWarningItemVO">
<result property="name" column="person_name"/>
<result property="idNo" column="cert_no"/>
<result property="subjectType" column="subject_type"/>
<result property="riskLevel" column="risk_level_name"/>
<result property="riskLevelType" column="risk_level_type"/>
<result property="riskCount" column="risk_count"/>
<result property="modelCount" column="model_count"/>
<result property="riskPoint" column="risk_point"/>
<result property="relatedObject" column="related_object"/>
<result property="latestTradeTime" column="latest_trade_time"/>
<collection property="riskPointTagList"
column="{projectId=project_id,certNo=cert_no,selectedModelCodes=selected_model_codes}"
ofType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO"
select="selectExternalRiskHitTagsByScope"/>
</resultMap>
<resultMap id="SuspiciousTransactionItemResultMap" type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO">
<id property="bankStatementId" column="bankStatementId"/>
<result property="trxDate" column="trxDate"/>
@@ -115,6 +147,13 @@
) tens
</sql>
<sql id="externalModelCodeFilterSql">
('EXTERNAL_LARGE_TRANSACTION',
'EXTERNAL_ABNORMAL_TRANSACTION',
'EXTERNAL_SUSPICIOUS_GAMBLING',
'EXTERNAL_SUSPICIOUS_RELATION')
</sql>
<sql id="resolvedEmployeeRiskBaseSql">
select distinct
tr.id,
@@ -148,6 +187,18 @@
on relation.person_id = family_staff.id_card
where tr.project_id = #{projectId}
and coalesce(direct_staff.id_card, statement_staff.id_card, family_staff.id_card) is not null
and not exists (
select 1
from ccdi_project_risk_exclusion ex
where ex.project_id = tr.project_id
and ex.rule_code = tr.rule_code
and (
(ex.exclusion_type = 'STATEMENT'
and ex.bank_statement_id = tr.bank_statement_id)
or (ex.exclusion_type = 'OBJECT'
and ex.staff_id_card = coalesce(direct_staff.id_card, statement_staff.id_card, family_staff.id_card))
)
)
</sql>
<sql id="employeeRiskAggregateSql">
@@ -494,6 +545,424 @@
order by result.staff_name asc, result.staff_id_card asc
</select>
<select id="selectRiskModelPeopleList" resultMap="RiskModelPeopleItemResultMap">
<bind name="projectId" value="query.projectId"/>
select
result.project_id,
result.staff_id_card,
result.staff_name,
result.staff_code,
result.dept_name as department,
#{query.modelCodesCsv} as selected_model_codes
from ccdi_project_overview_employee_result result
where 1 = 1
and result.project_id = #{query.projectId}
<if test="query.modelCodes != null and query.modelCodes.size() > 0">
<choose>
<when test="query.matchMode == 'ALL'">
<foreach collection="query.modelCodes" item="modelCode">
and find_in_set(#{modelCode}, result.model_codes_csv)
</foreach>
</when>
<otherwise>
and (
<foreach collection="query.modelCodes" item="modelCode" separator=" or ">
find_in_set(#{modelCode}, result.model_codes_csv)
</foreach>
)
</otherwise>
</choose>
</if>
<if test="query.keyword != null and query.keyword != ''">
and (
result.staff_name like concat('%', trim(#{query.keyword}), '%')
or result.staff_code like concat('%', trim(#{query.keyword}), '%')
)
</if>
<if test="query.deptId != null">
and result.dept_id = #{query.deptId}
</if>
order by result.staff_name asc, result.staff_id_card asc
</select>
<sql id="externalPersonSubjectSql">
select
bs.project_id,
bs.cret_no as cert_no,
coalesce(
max(intermediary.name),
max(customer.name),
max(nullif(trim(bs.LE_ACCOUNT_NAME), '')),
'外部人员'
) as person_name,
case
when max(case when intermediary.person_id is not null then 1 else 0 end) > 0 then '中介'
when max(case when customer.person_id is not null then 1 else 0 end) > 0 then '客户'
else '外部人员'
end as subject_type
from ccdi_bank_statement bs
left join ccdi_base_staff staff
on staff.id_card = bs.cret_no
left join ccdi_staff_fmy_relation relation
on relation.status = 1
and relation.relation_cert_no = bs.cret_no
left join ccdi_biz_intermediary intermediary
on intermediary.person_sub_type = '本人'
and intermediary.person_id = bs.cret_no
left join ccdi_credit_customer_base customer
on customer.person_id = bs.cret_no
where bs.project_id = #{externalProjectId}
and bs.cret_no is not null
and trim(bs.cret_no) != ''
and staff.id_card is null
and relation.relation_cert_no is null
group by bs.project_id, bs.cret_no
</sql>
<sql id="externalPersonSourceSql">
select
subject.project_id,
subject.cert_no,
subject.person_name,
subject.subject_type,
bs.bank_statement_id,
bs.TRX_DATE as trx_date,
bs.CUSTOMER_ACCOUNT_NAME as customer_account_name,
bs.customer_cert_no,
case
when counter_account.owner_type = 'EMPLOYEE' then '员工'
when counter_account.owner_type = 'RELATION' then '员工亲属'
when counter_account.owner_type = 'CREDIT_CUSTOMER' then '信贷客户'
when counter_account.owner_type = 'INTERMEDIARY' then '中介库人员'
when counter_staff.id_card is not null then '员工'
when counter_relation.relation_cert_no is not null then '员工亲属'
when counter_intermediary.person_id is not null then '中介库人员'
else null
end as related_object,
tr.model_code,
tr.model_name,
tr.rule_code,
tr.rule_name,
tr.risk_level,
tr.reason_detail
from (
<include refid="externalPersonSubjectSql"/>
) subject
inner join ccdi_bank_statement bs
on bs.project_id = subject.project_id
and bs.cret_no = subject.cert_no
inner join ccdi_bank_statement_tag_result tr
on tr.project_id = bs.project_id
and tr.bank_statement_id = bs.bank_statement_id
and tr.model_code in <include refid="externalModelCodeFilterSql"/>
left join ccdi_account_info counter_account
on trim(bs.CUSTOMER_ACCOUNT_NO) != ''
and counter_account.account_no = trim(bs.CUSTOMER_ACCOUNT_NO)
and counter_account.owner_type in ('EMPLOYEE', 'RELATION', 'INTERMEDIARY', 'CREDIT_CUSTOMER')
left join ccdi_base_staff counter_staff
on counter_account.account_no is null
and trim(bs.CUSTOMER_ACCOUNT_NAME) != ''
and counter_staff.name = trim(bs.CUSTOMER_ACCOUNT_NAME)
left join ccdi_staff_fmy_relation counter_relation
on counter_account.account_no is null
and counter_relation.status = 1
and trim(bs.CUSTOMER_ACCOUNT_NAME) != ''
and counter_relation.relation_name = trim(bs.CUSTOMER_ACCOUNT_NAME)
left join ccdi_biz_intermediary counter_intermediary
on counter_account.account_no is null
and counter_intermediary.person_sub_type = '本人'
and trim(bs.CUSTOMER_ACCOUNT_NAME) != ''
and counter_intermediary.name = trim(bs.CUSTOMER_ACCOUNT_NAME)
where trim(ifnull(bs.LE_ACCOUNT_NAME, '')) != trim(ifnull(bs.CUSTOMER_ACCOUNT_NAME, ''))
and not exists (
select 1
from ccdi_project_risk_exclusion ex
where ex.project_id = tr.project_id
and ex.rule_code = tr.rule_code
and ex.exclusion_type = 'STATEMENT'
and ex.bank_statement_id = tr.bank_statement_id
)
union all
select
subject.project_id,
subject.cert_no,
subject.person_name,
subject.subject_type,
null as bank_statement_id,
null as trx_date,
null as customer_account_name,
null as customer_cert_no,
'资金' as related_object,
tr.model_code,
tr.model_name,
tr.rule_code,
tr.rule_name,
tr.risk_level,
tr.reason_detail
from (
<include refid="externalPersonSubjectSql"/>
) subject
inner join ccdi_bank_statement_tag_result tr
on tr.project_id = subject.project_id
and tr.object_type = 'EXTERNAL_CERT_NO'
and tr.object_key = subject.cert_no
and tr.model_code in <include refid="externalModelCodeFilterSql"/>
where not exists (
select 1
from ccdi_project_risk_exclusion ex
where ex.project_id = tr.project_id
and ex.rule_code = tr.rule_code
and ex.exclusion_type = 'OBJECT'
and ex.staff_id_card = subject.cert_no
)
</sql>
<sql id="externalPersonAggregateSql">
select
source.project_id,
source.cert_no,
max(source.person_name) as person_name,
max(source.subject_type) as subject_type,
count(*) as risk_count,
count(distinct source.model_code) as model_count,
group_concat(distinct source.rule_name order by source.rule_name separator '、') as risk_point,
group_concat(distinct source.related_object order by source.related_object separator '、') as related_object,
max(source.trx_date) as latest_trade_time,
case
when sum(case when source.risk_level = 'HIGH' then 1 else 0 end) > 0 then 'HIGH'
when sum(case when source.risk_level = 'MEDIUM' then 1 else 0 end) > 0 then 'MEDIUM'
else 'LOW'
end as risk_level_code,
null as selected_model_codes
from (
<include refid="externalPersonSourceSql"/>
) source
group by source.project_id, source.cert_no
</sql>
<sql id="externalPersonWarningSelectSql">
select
agg.project_id,
agg.cert_no,
agg.person_name,
agg.subject_type,
agg.risk_count,
agg.model_count,
agg.risk_point,
coalesce(agg.related_object, '-') as related_object,
agg.latest_trade_time,
agg.risk_level_code,
case
when agg.risk_level_code = 'HIGH' then '高风险'
when agg.risk_level_code = 'MEDIUM' then '中风险'
else '低风险'
end as risk_level_name,
case
when agg.risk_level_code = 'HIGH' then 'danger'
when agg.risk_level_code = 'MEDIUM' then 'warning'
else 'info'
end as risk_level_type,
case
when agg.risk_level_code = 'HIGH' then 1
when agg.risk_level_code = 'MEDIUM' then 2
else 3
end as risk_level_sort,
agg.selected_model_codes
from (
<include refid="externalPersonAggregateSql"/>
) agg
</sql>
<select id="selectExternalPersonWarningPage" resultMap="ExternalPersonWarningItemResultMap">
<bind name="externalProjectId" value="query.projectId"/>
select *
from (
<include refid="externalPersonWarningSelectSql"/>
) warning
order by warning.risk_level_sort asc, warning.model_count desc, warning.risk_count desc, warning.latest_trade_time desc
</select>
<select id="selectExternalPersonWarningList" resultMap="ExternalPersonWarningItemResultMap">
<bind name="externalProjectId" value="projectId"/>
select *
from (
<include refid="externalPersonWarningSelectSql"/>
) warning
order by warning.risk_level_sort asc, warning.model_count desc, warning.risk_count desc, warning.latest_trade_time desc
</select>
<select id="selectExternalRiskSummaryByProjectId" resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectExternalRiskSummaryVO">
<bind name="externalProjectId" value="projectId"/>
select
count(*) as total,
coalesce(sum(case when risk.risk_level_code = 'HIGH' then 1 else 0 end), 0) as high,
coalesce(sum(case when risk.risk_level_code = 'MEDIUM' then 1 else 0 end), 0) as medium,
coalesce(sum(case when risk.risk_level_code = 'LOW' then 1 else 0 end), 0) as low,
coalesce(sum(case when risk.risk_level_code is null then 1 else 0 end), 0) as noRisk
from (
<include refid="externalPersonSubjectSql"/>
) subject
left join (
<include refid="externalPersonAggregateSql"/>
) risk
on risk.project_id = subject.project_id
and risk.cert_no = subject.cert_no
</select>
<select id="selectExternalRiskModelCardsByProjectId" resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardVO">
<bind name="externalProjectId" value="projectId"/>
select
model_scope.model_code,
max(model_scope.model_name) as model_name,
count(*) as warning_count,
count(distinct model_scope.cert_no) as people_count
from (
<include refid="externalPersonSourceSql"/>
) model_scope
group by model_scope.model_code
order by warning_count desc, model_scope.model_code asc
</select>
<select id="selectExternalRiskModelPeoplePage" resultMap="ExternalRiskModelPeopleItemResultMap">
<bind name="externalProjectId" value="query.projectId"/>
select
warning.project_id,
warning.cert_no,
warning.person_name,
warning.subject_type,
warning.related_object,
#{query.modelCodesCsv} as selected_model_codes
from (
<include refid="externalPersonWarningSelectSql"/>
) warning
where 1 = 1
<if test="query.modelCodes != null and query.modelCodes.size() > 0">
<choose>
<when test="query.matchMode == 'ALL'">
<foreach collection="query.modelCodes" item="modelCode">
and exists (
select 1
from (
<include refid="externalPersonSourceSql"/>
) source
where source.cert_no = warning.cert_no
and source.model_code = #{modelCode}
)
</foreach>
</when>
<otherwise>
and exists (
select 1
from (
<include refid="externalPersonSourceSql"/>
) source
where source.cert_no = warning.cert_no
and source.model_code in
<foreach collection="query.modelCodes" item="modelCode" open="(" separator="," close=")">
#{modelCode}
</foreach>
)
</otherwise>
</choose>
</if>
<if test="query.keyword != null and query.keyword != ''">
and (
warning.person_name like concat('%', trim(#{query.keyword}), '%')
or warning.cert_no like concat('%', trim(#{query.keyword}), '%')
)
</if>
order by warning.person_name asc, warning.cert_no asc
</select>
<select id="selectExternalRiskModelPeopleList" resultMap="ExternalRiskModelPeopleItemResultMap">
<bind name="externalProjectId" value="query.projectId"/>
select
warning.project_id,
warning.cert_no,
warning.person_name,
warning.subject_type,
warning.related_object,
#{query.modelCodesCsv} as selected_model_codes
from (
<include refid="externalPersonWarningSelectSql"/>
) warning
where 1 = 1
<if test="query.modelCodes != null and query.modelCodes.size() > 0">
<choose>
<when test="query.matchMode == 'ALL'">
<foreach collection="query.modelCodes" item="modelCode">
and exists (
select 1
from (
<include refid="externalPersonSourceSql"/>
) source
where source.cert_no = warning.cert_no
and source.model_code = #{modelCode}
)
</foreach>
</when>
<otherwise>
and exists (
select 1
from (
<include refid="externalPersonSourceSql"/>
) source
where source.cert_no = warning.cert_no
and source.model_code in
<foreach collection="query.modelCodes" item="modelCode" open="(" separator="," close=")">
#{modelCode}
</foreach>
)
</otherwise>
</choose>
</if>
<if test="query.keyword != null and query.keyword != ''">
and (
warning.person_name like concat('%', trim(#{query.keyword}), '%')
or warning.cert_no like concat('%', trim(#{query.keyword}), '%')
)
</if>
order by warning.person_name asc, warning.cert_no asc
</select>
<select id="selectExternalRiskModelNamesByScope" resultType="java.lang.String">
<bind name="externalProjectId" value="projectId"/>
select distinct source.model_name
from (
<include refid="externalPersonSourceSql"/>
) source
where source.cert_no = #{certNo}
<if test="selectedModelCodes != null and selectedModelCodes != ''">
and find_in_set(source.model_code, #{selectedModelCodes})
</if>
order by source.model_name asc
</select>
<select id="selectExternalRiskHitTagsByScope" resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO">
<bind name="externalProjectId" value="projectId"/>
select
source.model_code as modelCode,
max(source.model_name) as modelName,
source.rule_code as ruleCode,
max(source.rule_name) as ruleName,
max(source.risk_level) as riskLevel,
coalesce(
max(case when source.bank_statement_id is null then nullif(source.reason_detail, '') end),
max(nullif(source.reason_detail, ''))
) as reasonDetail
from (
<include refid="externalPersonSourceSql"/>
) source
where source.cert_no = #{certNo}
<if test="selectedModelCodes != null and selectedModelCodes != ''">
and find_in_set(source.model_code, #{selectedModelCodes})
</if>
group by source.model_code, source.rule_code
order by source.model_code asc, source.rule_code asc
</select>
<sql id="suspiciousTransactionBaseSql">
select
bs.bank_statement_id as bankStatementId,
@@ -531,6 +1000,17 @@
from ccdi_bank_statement_tag_result tr
where tr.project_id = #{query.projectId}
and tr.bank_statement_id is not null
and not exists (
select 1
from ccdi_project_risk_exclusion ex
where ex.project_id = tr.project_id
and ex.rule_code = tr.rule_code
and ex.exclusion_type = 'STATEMENT'
and ex.bank_statement_id = tr.bank_statement_id
)
<if test="query.modelCode != null and query.modelCode != ''">
and tr.model_code = #{query.modelCode}
</if>
</sql>
<sql id="suspiciousTransactionNameHitSql">
@@ -606,6 +1086,34 @@
) hits
</sql>
<sql id="externalSuspiciousTransactionSql">
select
bs.bank_statement_id as bankStatementId,
bs.TRX_DATE as trxDate,
source.person_name as relatedPersonName,
null as relatedStaffName,
null as relatedStaffCode,
source.subject_type as relationType,
bs.USER_MEMO as userMemo,
bs.CASH_TYPE as cashType,
case
when ifnull(bs.AMOUNT_CR, 0) > 0 then bs.AMOUNT_CR
when ifnull(bs.AMOUNT_DR, 0) > 0 then -bs.AMOUNT_DR
else 0
end as displayAmount,
1 as hasModelRuleHit,
0 as hasNameListHit,
source.person_name as suspiciousPersonName,
9 as matchPriority,
'外部人员预警' as nameListHitType
from (
<bind name="externalProjectId" value="query.projectId"/>
<include refid="externalPersonSourceSql"/>
) source
inner join ccdi_bank_statement bs
on bs.bank_statement_id = source.bank_statement_id
</sql>
<sql id="suspiciousTransactionMergedSql">
select
base.bankStatementId,
@@ -652,6 +1160,27 @@
inner join (
<include refid="suspiciousTransactionNameHitSql"/>
) name_hits on name_hits.bankStatementId = base.bankStatementId
union all
select
external_hits.bankStatementId,
external_hits.trxDate,
external_hits.relatedPersonName,
external_hits.relatedStaffName,
external_hits.relatedStaffCode,
external_hits.relationType,
external_hits.userMemo,
external_hits.cashType,
external_hits.displayAmount,
external_hits.hasModelRuleHit,
external_hits.hasNameListHit,
external_hits.suspiciousPersonName,
external_hits.matchPriority,
external_hits.nameListHitType
from (
<include refid="externalSuspiciousTransactionSql"/>
) external_hits
</sql>
<sql id="suspiciousTransactionAggregatedSql">
@@ -706,10 +1235,30 @@
<when test="query.suspiciousType == 'MODEL_RULE'">
where final_result.hasModelRuleHit = 1
</when>
<when test="query.suspiciousType == 'EXTERNAL_PERSON'">
where final_result.nameListHitType = '外部人员预警'
</when>
<otherwise>
where final_result.hasModelRuleHit = 1 or final_result.hasNameListHit = 1
</otherwise>
</choose>
<if test="query.modelCode != null and query.modelCode != ''">
and exists (
select 1
from ccdi_bank_statement_tag_result model_filter
where model_filter.project_id = #{query.projectId}
and model_filter.bank_statement_id = final_result.bankStatementId
and model_filter.model_code = #{query.modelCode}
and not exists (
select 1
from ccdi_project_risk_exclusion ex
where ex.project_id = model_filter.project_id
and ex.rule_code = model_filter.rule_code
and ex.exclusion_type = 'STATEMENT'
and ex.bank_statement_id = model_filter.bank_statement_id
)
)
</if>
</sql>
<select id="selectSuspiciousTransactionPage" resultMap="SuspiciousTransactionItemResultMap">
@@ -800,6 +1349,14 @@
from ccdi_bank_statement_tag_result tr
where tr.project_id = #{query.projectId}
and tr.bank_statement_id is not null
and not exists (
select 1
from ccdi_project_risk_exclusion ex
where ex.project_id = tr.project_id
and ex.rule_code = tr.rule_code
and ex.exclusion_type = 'STATEMENT'
and ex.bank_statement_id = tr.bank_statement_id
)
group by tr.bank_statement_id
) tag_result on tag_result.bank_statement_id = final_result.bankStatementId
<include refid="suspiciousTransactionFilterSql"/>
@@ -951,7 +1508,8 @@
max(json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].modelName')))) as model_name,
json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].ruleCode'))) as rule_code,
max(json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].ruleName')))) as rule_name,
max(json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].riskLevel')))) as risk_level
max(json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].riskLevel')))) as risk_level,
max(json_unquote(json_extract(result.hit_rules_json, concat('$[', idx.idx, '].reasonDetail')))) as reason_detail
from ccdi_project_overview_employee_result result
join (
<include refid="jsonArrayIndexSql"/>
@@ -1028,6 +1586,7 @@
<select id="selectPersonAnalysisObjectRows" resultType="com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisObjectRecordVO">
select
max(tr.model_code) as modelCode,
tr.rule_code as ruleCode,
coalesce(max(staff.name), max(relation.relation_name), max(tr.object_key), max(tr.object_type)) as title,
max(case
when tr.object_type = 'STAFF_ID_CARD' then '员工对象'
@@ -1044,6 +1603,14 @@
and tr.object_key = relation.relation_cert_no
where tr.project_id = #{projectId}
and tr.bank_statement_id is null
and not exists (
select 1
from ccdi_project_risk_exclusion ex
where ex.project_id = tr.project_id
and ex.rule_code = tr.rule_code
and ex.exclusion_type = 'OBJECT'
and ex.staff_id_card = #{staffIdCard}
)
and (
tr.object_key = #{staffIdCard}
or exists (
@@ -1073,4 +1640,5 @@
group by base.staff_id_card
) agg
</select>
</mapper>

View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.ccdi.project.mapper.CcdiProjectRiskExclusionMapper">
<resultMap id="CcdiProjectRiskExclusionMap"
type="com.ruoyi.ccdi.project.domain.entity.CcdiProjectRiskExclusion">
<id property="id" column="id"/>
<result property="projectId" column="project_id"/>
<result property="staffIdCard" column="staff_id_card"/>
<result property="ruleCode" column="rule_code"/>
<result property="exclusionType" column="exclusion_type"/>
<result property="bankStatementId" column="bank_statement_id"/>
<result property="excludeReason" column="exclude_reason"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
<result property="remark" column="remark"/>
</resultMap>
<insert id="upsertExclusion">
insert into ccdi_project_risk_exclusion (
project_id, staff_id_card, rule_code, exclusion_type, bank_statement_id,
exclude_reason, create_by, create_time, update_by, update_time, remark
) values (
#{exclusion.projectId}, #{exclusion.staffIdCard}, #{exclusion.ruleCode},
#{exclusion.exclusionType}, #{exclusion.bankStatementId}, #{exclusion.excludeReason},
#{exclusion.createBy}, #{exclusion.createTime}, #{exclusion.updateBy},
#{exclusion.updateTime}, #{exclusion.remark}
)
on duplicate key update
exclude_reason = values(exclude_reason),
update_by = values(update_by),
update_time = values(update_time),
remark = values(remark)
</insert>
<select id="countExistingRiskHit" resultType="int">
select count(1)
from ccdi_bank_statement_tag_result tr
where tr.project_id = #{exclusion.projectId}
and tr.rule_code = #{exclusion.ruleCode}
<choose>
<when test="exclusion.exclusionType == 'STATEMENT'">
and tr.bank_statement_id = #{exclusion.bankStatementId}
</when>
<otherwise>
and (
tr.object_key = #{exclusion.staffIdCard}
or exists (
select 1
from ccdi_bank_statement bs
where bs.bank_statement_id = tr.bank_statement_id
and bs.cret_no = #{exclusion.staffIdCard}
)
or exists (
select 1
from ccdi_staff_fmy_relation relation
where relation.status = 1
and relation.person_id = #{exclusion.staffIdCard}
and relation.relation_cert_no = tr.object_key
)
or exists (
select 1
from ccdi_staff_fmy_relation relation
inner join ccdi_bank_statement bs
on bs.cret_no = relation.relation_cert_no
where relation.status = 1
and relation.person_id = #{exclusion.staffIdCard}
and bs.bank_statement_id = tr.bank_statement_id
)
)
</otherwise>
</choose>
</select>
</mapper>

View File

@@ -67,6 +67,28 @@
<result property="transferDate" column="transfer_date"/>
</resultMap>
<resultMap id="IncreaseLendingListItemResultMap"
type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectIncreaseLendingListItemVO">
<result property="staffId" column="staff_id"/>
<result property="staffName" column="staff_name"/>
<result property="staffIdCard" column="staff_id_card"/>
<result property="deptName" column="dept_name"/>
<result property="contractNo" column="contract_no"/>
<result property="lendingOrgNo" column="lending_org_no"/>
<result property="borrowerName" column="borrower_name"/>
<result property="borrowerCertNo" column="borrower_cert_no"/>
<result property="loanProduct" column="loan_product"/>
<result property="contractAmount" column="contract_amount"/>
<result property="loanBalance" column="loan_balance"/>
<result property="loanStartDate" column="loan_start_date"/>
<result property="loanEndDate" column="loan_end_date"/>
<result property="status" column="status"/>
<result property="fiveClassification" column="five_classification"/>
<result property="customerManagerId" column="customer_manager_id"/>
<result property="customerManagerName" column="customer_manager_name"/>
<result property="approver" column="approver"/>
</resultMap>
<resultMap id="FamilyAssetLiabilityDetailResultMap"
type="com.ruoyi.ccdi.project.domain.vo.CcdiProjectFamilyAssetLiabilityDetailVO">
<association property="incomeDetail"
@@ -114,32 +136,18 @@
<sql id="projectEmployeeScopeSql">
select distinct
coalesce(direct_staff.id_card, statement_staff.id_card, family_staff.id_card) as staff_id_card,
cast(coalesce(direct_staff.staff_id, statement_staff.staff_id, family_staff.staff_id) as char) as staff_code,
coalesce(direct_staff.name, statement_staff.name, family_staff.name) as staff_name,
statement_staff.id_card as staff_id_card,
cast(statement_staff.staff_id as char) as staff_code,
statement_staff.name as staff_name,
dept.dept_name
from ccdi_bank_statement_tag_result tr
left join ccdi_base_staff direct_staff
on tr.object_type = 'STAFF_ID_CARD'
and tr.object_key = direct_staff.id_card
left join ccdi_bank_statement bs
on tr.bank_statement_id = bs.bank_statement_id
left join ccdi_base_staff statement_staff
on (tr.object_key is null or tr.object_key = '')
and bs.cret_no = statement_staff.id_card
left join ccdi_staff_fmy_relation relation
on relation.status = 1
and (
((tr.object_key is null or tr.object_key = '') and bs.cret_no = relation.relation_cert_no)
or ((tr.object_key is not null and tr.object_key != '') and tr.object_type != 'STAFF_ID_CARD'
and tr.object_key = relation.relation_cert_no)
)
left join ccdi_base_staff family_staff
on relation.person_id = family_staff.id_card
from ccdi_bank_statement bs
inner join ccdi_base_staff statement_staff
on statement_staff.id_card = trim(bs.cret_no)
left join sys_dept dept
on dept.dept_id = coalesce(direct_staff.dept_id, statement_staff.dept_id, family_staff.dept_id)
where tr.project_id = #{projectId}
and coalesce(direct_staff.id_card, statement_staff.id_card, family_staff.id_card) is not null
on dept.dept_id = statement_staff.dept_id
where bs.project_id = #{projectId}
and bs.cret_no is not null
and trim(bs.cret_no) != ''
</sql>
<sql id="spouseRelationSql">
@@ -820,4 +828,95 @@
)
</select>
<select id="selectIncreaseLendingPage" resultMap="IncreaseLendingListItemResultMap">
select
trim(loan.customer_manager_id) as staff_id,
coalesce(staff.name, loan.customer_manager_name) as staff_name,
staff.id_card as staff_id_card,
dept.dept_name,
loan.nfaacono as contract_no,
loan.nfaabrno as lending_org_no,
loan.borrower_name,
loan.borrower_cert_no,
loan.loan_product,
loan.contract_amount,
loan.loan_balance,
date_format(loan.loan_start_date, '%Y-%m-%d') as loan_start_date,
date_format(loan.loan_end_date, '%Y-%m-%d') as loan_end_date,
loan.status,
loan.five_classification,
loan.customer_manager_id,
loan.customer_manager_name,
loan.approver
from ccdi_increase_lending loan
left join ccdi_base_staff staff
on trim(loan.customer_manager_id) = cast(staff.staff_id as char)
left join sys_dept dept
on dept.dept_id = staff.dept_id
<where>
<if test="query.staffId != null and query.staffId != ''">
and trim(loan.customer_manager_id) = #{query.staffId}
</if>
<if test="query.staffIdCard != null and query.staffIdCard != ''">
and staff.id_card = #{query.staffIdCard}
</if>
<if test="query.approver != null and query.approver != ''">
and trim(loan.approver) = #{query.approver}
</if>
<if test="query.loanStartDate != null and query.loanStartDate != ''">
and loan.loan_start_date &gt;= #{query.loanStartDate}
</if>
<if test="query.loanEndDate != null and query.loanEndDate != ''">
and loan.loan_start_date &lt;= #{query.loanEndDate}
</if>
</where>
order by loan.loan_start_date desc, loan.nfaacono desc
</select>
<select id="selectIncreaseLendingExportList"
resultType="com.ruoyi.ccdi.project.domain.excel.CcdiProjectIncreaseLendingExcel">
select
trim(loan.customer_manager_id) as staff_id,
coalesce(staff.name, loan.customer_manager_name) as staff_name,
staff.id_card as staff_id_card,
dept.dept_name,
loan.nfaacono as contract_no,
loan.nfaabrno as lending_org_no,
loan.borrower_name,
loan.borrower_cert_no,
loan.loan_product,
loan.contract_amount,
loan.loan_balance,
date_format(loan.loan_start_date, '%Y-%m-%d') as loan_start_date,
date_format(loan.loan_end_date, '%Y-%m-%d') as loan_end_date,
loan.status,
loan.five_classification,
loan.customer_manager_id,
loan.customer_manager_name,
loan.approver
from ccdi_increase_lending loan
left join ccdi_base_staff staff
on trim(loan.customer_manager_id) = cast(staff.staff_id as char)
left join sys_dept dept
on dept.dept_id = staff.dept_id
<where>
<if test="query.staffId != null and query.staffId != ''">
and trim(loan.customer_manager_id) = #{query.staffId}
</if>
<if test="query.staffIdCard != null and query.staffIdCard != ''">
and staff.id_card = #{query.staffIdCard}
</if>
<if test="query.approver != null and query.approver != ''">
and trim(loan.approver) = #{query.approver}
</if>
<if test="query.loanStartDate != null and query.loanStartDate != ''">
and loan.loan_start_date &gt;= #{query.loanStartDate}
</if>
<if test="query.loanEndDate != null and query.loanEndDate != ''">
and loan.loan_start_date &lt;= #{query.loanEndDate}
</if>
</where>
order by loan.loan_start_date desc, loan.nfaacono desc
</select>
</mapper>

View File

@@ -1,23 +1,29 @@
package com.ruoyi.ccdi.project.controller;
import com.ruoyi.ccdi.project.domain.dto.CcdiPullBankInfoSubmitDTO;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.ccdi.project.service.ICcdiFileUploadService;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.SecurityUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@@ -31,6 +37,14 @@ class CcdiFileUploadControllerTest {
@Mock
private ICcdiFileUploadService fileUploadService;
@Mock
private CcdiProjectAccessService projectAccessService;
@AfterEach
void tearDown() {
SecurityContextHolder.clearContext();
}
@Test
void parseIdCardFile_shouldReturnAjaxResultSuccess() {
MockMultipartFile file = new MockMultipartFile(
@@ -46,6 +60,29 @@ class CcdiFileUploadControllerTest {
assertEquals(200, result.get("code"));
}
@Test
void batchUpload_shouldDelegateFilenameValidationToService() {
MultipartFile[] files = new MultipartFile[]{
new MockMultipartFile(
"files",
"330101199001010011 - 流水.xl sx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"content".getBytes(StandardCharsets.UTF_8)
)
};
setLoginUser(9527L, "admin");
when(fileUploadService.batchUploadFiles(PROJECT_ID, files, "admin"))
.thenReturn("batch-1");
AjaxResult result = controller.batchUpload(PROJECT_ID, files);
assertEquals(200, result.get("code"));
assertEquals("batch-1", result.get("data"));
verify(projectAccessService).assertCanOperate(PROJECT_ID);
verify(fileUploadService).batchUploadFiles(PROJECT_ID, files, "admin");
}
@Test
void pullBankInfo_shouldUseCurrentLoginUserInfo() {
CcdiPullBankInfoSubmitDTO dto = new CcdiPullBankInfoSubmitDTO();
@@ -54,30 +91,35 @@ class CcdiFileUploadControllerTest {
dto.setStartDate("2026-03-01");
dto.setEndDate("2026-03-10");
try (MockedStatic<SecurityUtils> mocked = mockStatic(SecurityUtils.class)) {
mocked.when(SecurityUtils::getUserId).thenReturn(9527L);
mocked.when(SecurityUtils::getUsername).thenReturn("admin");
when(fileUploadService.submitPullBankInfo(PROJECT_ID, dto.getIdCards(), "2026-03-01", "2026-03-10", 9527L, "admin"))
.thenReturn("batch-1");
setLoginUser(9527L, "admin");
when(fileUploadService.submitPullBankInfo(PROJECT_ID, dto.getIdCards(), "2026-03-01", "2026-03-10", 9527L, "admin"))
.thenReturn("batch-1");
AjaxResult result = controller.pullBankInfo(dto);
AjaxResult result = controller.pullBankInfo(dto);
assertEquals(200, result.get("code"));
}
assertEquals(200, result.get("code"));
}
@Test
void deleteFile_shouldUseCurrentLoginUserId() {
try (MockedStatic<SecurityUtils> mocked = mockStatic(SecurityUtils.class)) {
mocked.when(SecurityUtils::getUserId).thenReturn(9527L);
when(fileUploadService.deleteFileUploadRecord(123L, 9527L))
.thenReturn("删除成功");
setLoginUser(9527L, "admin");
when(fileUploadService.deleteFileUploadRecord(123L, 9527L))
.thenReturn("删除成功");
AjaxResult result = controller.deleteFile(123L);
AjaxResult result = controller.deleteFile(123L);
assertEquals(200, result.get("code"));
assertEquals("删除成功", result.get("msg"));
verify(fileUploadService).deleteFileUploadRecord(123L, 9527L);
}
assertEquals(200, result.get("code"));
assertEquals("删除成功", result.get("msg"));
verify(fileUploadService).deleteFileUploadRecord(123L, 9527L);
}
private void setLoginUser(Long userId, String username) {
SysUser user = new SysUser();
user.setUserName(username);
LoginUser loginUser = new LoginUser(user, Set.of("*:*:*"));
loginUser.setUserId(userId);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(loginUser, null, Collections.emptyList());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}

View File

@@ -1,28 +1,20 @@
package com.ruoyi.ccdi.project.controller;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectImportHistoryDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectVO;
import com.ruoyi.ccdi.project.service.ICcdiProjectService;
import com.ruoyi.common.core.domain.AjaxResult;
import io.swagger.v3.oas.annotations.Operation;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import java.lang.reflect.Method;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
class CcdiProjectControllerTest {
@@ -35,16 +27,6 @@ class CcdiProjectControllerTest {
@Test
void shouldArchiveProject() throws Exception {
try (MockedStatic<com.ruoyi.common.utils.SecurityUtils> mocked = mockStatic(com.ruoyi.common.utils.SecurityUtils.class)) {
mocked.when(com.ruoyi.common.utils.SecurityUtils::getUsername).thenReturn("tester");
AjaxResult result = controller.archiveProject(40L);
assertEquals(200, result.get("code"));
assertEquals("项目归档成功", result.get("msg"));
verify(projectService).archiveProject(40L, "tester");
}
Method method = CcdiProjectController.class.getMethod("archiveProject", Long.class);
PostMapping postMapping = method.getAnnotation(PostMapping.class);
PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class);
@@ -59,24 +41,32 @@ class CcdiProjectControllerTest {
}
@Test
void shouldImportFromHistoryAndReturnCreatedProject() {
CcdiProjectImportHistoryDTO dto = new CcdiProjectImportHistoryDTO();
dto.setProjectName("新建项目");
void shouldRestoreProject() throws Exception {
Method method = CcdiProjectController.class.getMethod("restoreProject", Long.class);
PostMapping postMapping = method.getAnnotation(PostMapping.class);
PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class);
Operation operation = method.getAnnotation(Operation.class);
CcdiProjectVO project = new CcdiProjectVO();
project.setProjectId(88L);
project.setProjectName("新建项目");
when(projectService.importFromHistory(eq(dto), eq("tester"))).thenReturn(project);
assertNotNull(postMapping);
assertEquals("/{projectId}/restore", postMapping.value()[0]);
assertNotNull(preAuthorize);
assertEquals("@ss.hasPermi('ccdi:project:edit')", preAuthorize.value());
assertNotNull(operation);
assertEquals("恢复项目", operation.summary());
}
try (MockedStatic<com.ruoyi.common.utils.SecurityUtils> mocked = mockStatic(com.ruoyi.common.utils.SecurityUtils.class)) {
mocked.when(com.ruoyi.common.utils.SecurityUtils::getUsername).thenReturn("tester");
@Test
void shouldAllowProjectListUsersToReachBusinessDeleteCheck() throws Exception {
Method method = CcdiProjectController.class.getMethod("deleteProject", Long.class);
DeleteMapping deleteMapping = method.getAnnotation(DeleteMapping.class);
PreAuthorize preAuthorize = method.getAnnotation(PreAuthorize.class);
Operation operation = method.getAnnotation(Operation.class);
AjaxResult result = controller.importFromHistory(dto);
assertEquals(200, result.get("code"));
assertEquals("项目创建成功", result.get("msg"));
assertSame(project, result.get("data"));
verify(projectService).importFromHistory(dto, "tester");
}
assertNotNull(deleteMapping);
assertEquals("/{projectId}", deleteMapping.value()[0]);
assertNotNull(preAuthorize);
assertEquals("@ss.hasPermi('ccdi:project:list')", preAuthorize.value());
assertNotNull(operation);
assertEquals("删除项目", operation.summary());
}
}

View File

@@ -281,7 +281,8 @@ class CcdiProjectOverviewControllerTest {
MockHttpServletResponse response = new MockHttpServletResponse();
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
CcdiProjectSuspiciousTransactionExcel row = new CcdiProjectSuspiciousTransactionExcel();
row.setSuspiciousPersonName("张三");
row.setLeAccountName("张三");
row.setCustomerAccountName("测试对手方");
row.setDisplayAmount(new java.math.BigDecimal("10.00"));
when(overviewService.exportSuspiciousTransactions(same(queryDTO))).thenReturn(List.of(row));

View File

@@ -104,4 +104,26 @@ class CcdiBankStatementTest {
assertEquals("330101199001011234", entity.getCustomerCertNo());
assertEquals("91330100123456789X", entity.getCustomerSocialCreditCode());
}
@Test
void testFromResponse_ShouldNormalizeCretNoBeforeDash() {
BankStatementItem item = new BankStatementItem();
item.setCretNo(" 330100198801010033 - 测试人员 ");
CcdiBankStatement entity = CcdiBankStatement.fromResponse(item);
assertNotNull(entity);
assertEquals("330100198801010033", entity.getCretNo());
}
@Test
void testFromResponse_ShouldNormalizeCretNoWithChineseDash() {
BankStatementItem item = new BankStatementItem();
item.setCretNo("330100198801010033测试人员");
CcdiBankStatement entity = CcdiBankStatement.fromResponse(item);
assertNotNull(entity);
assertEquals("330100198801010033", entity.getCretNo());
}
}

View File

@@ -13,6 +13,7 @@ import java.util.regex.Pattern;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiBankTagAnalysisMapperXmlTest {
@@ -26,11 +27,18 @@ class CcdiBankTagAnalysisMapperXmlTest {
"selectForexSellAmtStatements",
"selectLargePurchaseTransactionStatements",
"selectStockTfrLargeStatements",
"selectLargeStockTradingStatements"
"selectLargeStockTradingStatements",
"selectExternalSingleLargeAmountStatements",
"selectExternalNightTransactionStatements",
"selectExternalGamblingMemoStatements",
"selectExternalToStaffOrFamilyTransactionStatements"
);
private static final List<String> PHASE_TWO_OBJECT_SELECT_IDS = List.of(
"selectLowIncomeRelativeLargeTransactionObjects",
"selectMultiPartyGamblingTransferObjects",
"selectExternalCumulativeTransactionAmountObjects",
"selectExternalAnnualTurnoverObjects",
"selectExternalMultiPartyGamblingTransferObjects",
"selectMonthlyFixedIncomeObjects",
"selectFixedCounterpartyTransferObjects",
"selectSupplierConcentrationObjects",
@@ -100,7 +108,10 @@ class CcdiBankTagAnalysisMapperXmlTest {
void placeholderRules_shouldUseEmptyResultSqlTemplate() throws Exception {
String xml = readXml(RESOURCE);
assertTrue(xml.contains("占位SQL待补充真实规则"));
assertEquals(5, countMatches(xml, "where 1 = 0"));
assertEquals(
countMatches(xml, "占位SQL待补充真实规则"),
countMatches(xml, "where 1 = 0")
);
}
@Test
@@ -156,7 +167,11 @@ class CcdiBankTagAnalysisMapperXmlTest {
String xml = readXml(RESOURCE);
for (String selectId : PHASE_TWO_OBJECT_SELECT_IDS) {
String selectSql = extractSelectSql(xml, selectId);
assertTrue(selectSql.contains("'STAFF_ID_CARD' AS objectType"), () -> selectId + " 缺少 objectType");
assertTrue(
selectSql.contains("'STAFF_ID_CARD' AS objectType")
|| selectSql.contains("'EXTERNAL_CERT_NO' AS objectType"),
() -> selectId + " 缺少 objectType"
);
assertTrue(selectSql.contains("AS objectKey"), () -> selectId + " 缺少 objectKey");
assertTrue(selectSql.contains("reasonDetail"), () -> selectId + " 缺少 reasonDetail");
assertTrue(!selectSql.contains("where 1 = 0"), () -> selectId + " 仍是占位 SQL");
@@ -221,6 +236,31 @@ class CcdiBankTagAnalysisMapperXmlTest {
factory.newDocumentBuilder().parse(new InputSource(new StringReader(xml)));
}
@Test
void externalPersonRules_shouldUseExternalSubjectScopeAndCounterpartyEmployeeMatching() throws Exception {
String xml = readXml(RESOURCE);
String scopeSql = extractSqlFragment(xml, "externalPersonPredicateSql");
String relationSql = extractSelectSql(xml, "selectExternalToStaffOrFamilyTransactionStatements");
String annualSql = extractSelectSql(xml, "selectExternalAnnualTurnoverObjects");
String gamblingSql = extractSelectSql(xml, "selectExternalMultiPartyGamblingTransferObjects");
assertTrue(scopeSql.contains("bs.cret_no is not null"));
assertTrue(scopeSql.contains("staff.id_card = bs.cret_no"));
assertTrue(scopeSql.contains("relation.relation_cert_no = bs.cret_no"));
assertTrue(scopeSql.contains("trim(IFNULL(bs.LE_ACCOUNT_NAME, '')) &lt;&gt; trim(IFNULL(bs.CUSTOMER_ACCOUNT_NAME, ''))"));
assertFalse(scopeSql.contains("LE_ACCOUNT_NO"));
assertTrue(annualSql.contains("'EXTERNAL_CERT_NO' AS objectType"));
assertTrue(annualSql.contains("bs.cret_no AS certNo"));
assertTrue(gamblingSql.contains("'EXTERNAL_CERT_NO' AS objectType"));
assertTrue(gamblingSql.contains("having COUNT(1) > 2"));
assertTrue(relationSql.contains("counter_account.owner_type in ('EMPLOYEE', 'RELATION', 'INTERMEDIARY', 'CREDIT_CUSTOMER')"));
assertTrue(relationSql.contains("on counter_account.account_no is null"));
assertTrue(relationSql.contains("counter_staff.name = trim(bs.CUSTOMER_ACCOUNT_NAME)"));
assertTrue(relationSql.contains("counter_relation.relation_name = trim(bs.CUSTOMER_ACCOUNT_NAME)"));
assertTrue(relationSql.contains("counter_account.owner_type in ('EMPLOYEE', 'RELATION')"));
assertFalse(relationSql.contains("customer_cert_no"));
}
private String readXml(String resource) throws Exception {
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resource)) {
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);

View File

@@ -0,0 +1,40 @@
package com.ruoyi.ccdi.project.mapper;
import org.junit.jupiter.api.Test;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiBankTagTaskMapperXmlTest {
private static final String RESOURCE = "mapper/ccdi/project/CcdiBankTagTaskMapper.xml";
@Test
void mapper_shouldExposeLatestFailedTaskQuery() {
Method method = Arrays.stream(CcdiBankTagTaskMapper.class.getDeclaredMethods())
.filter(item -> "selectLatestFailedTaskByProjectId".equals(item.getName()))
.findFirst()
.orElse(null);
assertNotNull(method, "应提供查询项目最近失败打标任务的方法");
}
@Test
void xml_shouldSelectLatestFailedTaskByProjectId() throws Exception {
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(RESOURCE)) {
String xml = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
assertTrue(xml.contains("selectLatestFailedTaskByProjectId"), xml);
assertTrue(xml.contains("from ccdi_bank_tag_task"), xml);
assertTrue(xml.contains("project_id = #{projectId}"), xml);
assertTrue(xml.contains("status = 'FAILED'"), xml);
assertTrue(xml.contains("order by id desc"), xml);
assertTrue(xml.contains("limit 1"), xml);
}
}
}

View File

@@ -0,0 +1,27 @@
package com.ruoyi.ccdi.project.mapper;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiProjectExtendedPurchaseSupplierContractTest {
@Test
void shouldExposeExtendedPurchaseSupplierDetailQuery() throws Exception {
Class<?> mapperClass = Class.forName("com.ruoyi.ccdi.project.mapper.CcdiProjectSpecialCheckMapper");
Method method = mapperClass.getMethod("selectExtendedPurchaseSuppliers", Long.class, String.class);
assertEquals(List.class, method.getReturnType());
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectSpecialCheckMapper.xml"));
assertTrue(xml.contains("select id=\"selectExtendedPurchaseSuppliers\""));
assertTrue(xml.contains("ccdi_purchase_transaction_supplier"));
assertTrue(xml.contains("is_bid_winner"));
assertTrue(xml.contains("sort_order"));
}
}

View File

@@ -0,0 +1,51 @@
package com.ruoyi.ccdi.project.mapper;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class CcdiProjectMapperXmlTest {
@Test
void projectQueriesShouldApplyProjectVisibilityAndDeletedFilters() throws Exception {
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectMapper.xml"));
assertFalse(xml.contains("params.dataScope"), xml);
assertFalse(xml.contains("${queryDTO.params.dataScope}"), xml);
String listSql = extractSelect(xml, "selectProjectPage");
assertTrue(listSql.contains("LEFT JOIN sys_user u ON p.create_by = u.user_name"), listSql);
assertTrue(listSql.contains("scope != null and !scope.viewAllProjects"), listSql);
assertTrue(listSql.contains("p.create_by = #{scope.username}"), listSql);
assertTrue(listSql.contains("queryDTO.includeDeleted"), listSql);
assertTrue(listSql.contains("p.del_flag = '2'"), listSql);
assertTrue(listSql.contains("p.status = '5'"), listSql);
assertTrue(listSql.contains("p.del_flag = '0'"), listSql);
assertTrue(listSql.contains("p.status != '5'"), listSql);
String historySql = extractSelect(xml, "selectHistoryProjects");
assertTrue(historySql.contains("p.status in ('1', '2')"), historySql);
assertTrue(historySql.contains("p.del_flag = '0'"), historySql);
assertTrue(xml.contains("<update id=\"markProjectDeleted\">"), xml);
assertTrue(xml.contains("status = '5'"), xml);
assertTrue(xml.contains("del_flag = '2'"), xml);
assertTrue(xml.contains("<update id=\"restoreDeletedProject\">"), xml);
assertTrue(xml.contains("status = '1'"), xml);
assertTrue(xml.contains("del_flag = '0'"), xml);
assertTrue(xml.contains("is_archived = 0"), xml);
}
private String extractSelect(String xml, String selectId) {
String start = "<select id=\"" + selectId + "\"";
int startIndex = xml.indexOf(start);
assertTrue(startIndex >= 0, "missing select: " + selectId);
int endIndex = xml.indexOf("</select>", startIndex);
assertTrue(endIndex >= 0, "missing closing select tag: " + selectId);
return xml.substring(startIndex, endIndex);
}
}

View File

@@ -112,6 +112,33 @@ class CcdiProjectOverviewMapperSqlTest {
assertTrue(reportSuspiciousSql.contains("与信贷客户之间非正常资金往来"), reportSuspiciousSql);
}
@Test
void externalPersonSourceSql_shouldIncludeStatementAndObjectHits() throws Exception {
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml"));
String externalSourceSql = extractSqlFragment(xml, "externalPersonSourceSql");
String normalizedExternalSourceSql = externalSourceSql.replace("\r\n", "\n");
assertTrue(externalSourceSql.contains("tr.bank_statement_id = bs.bank_statement_id"), externalSourceSql);
assertTrue(externalSourceSql.contains("union all"), externalSourceSql);
assertTrue(externalSourceSql.contains("tr.object_type = 'EXTERNAL_CERT_NO'"), externalSourceSql);
assertTrue(externalSourceSql.contains("tr.object_key = subject.cert_no"), externalSourceSql);
assertTrue(externalSourceSql.contains("'资金' as related_object"), externalSourceSql);
assertFalse(externalSourceSql.contains("counter_staff.id_card = bs.customer_cert_no"), externalSourceSql);
assertFalse(externalSourceSql.contains("counter_relation.relation_cert_no = bs.customer_cert_no"), externalSourceSql);
assertFalse(externalSourceSql.contains("counter_intermediary.person_id = bs.customer_cert_no"), externalSourceSql);
assertInOrder(
externalSourceSql,
"when counter_account.owner_type = 'EMPLOYEE' then '员工'",
"when counter_account.owner_type = 'RELATION' then '员工亲属'",
"when counter_account.owner_type = 'CREDIT_CUSTOMER' then '信贷客户'",
"when counter_account.owner_type = 'INTERMEDIARY' then '中介库人员'",
"when counter_staff.id_card is not null then '员工'",
"when counter_relation.relation_cert_no is not null then '员工亲属'"
);
assertTrue(normalizedExternalSourceSql.contains("left join ccdi_base_staff counter_staff\n on counter_account.account_no is null"), externalSourceSql);
assertTrue(normalizedExternalSourceSql.contains("left join ccdi_staff_fmy_relation counter_relation\n on counter_account.account_no is null"), externalSourceSql);
}
@Test
void suspiciousTransactionNameListSql_shouldKeepCreditCustomerAndIntermediaryRulesScopedByAmount() throws Exception {
String xml = Files.readString(Path.of("src/main/resources/mapper/ccdi/project/CcdiProjectOverviewMapper.xml"));
@@ -205,4 +232,13 @@ class CcdiProjectOverviewMapperSqlTest {
assertTrue(endIndex >= 0, "missing closing sql tag: " + sqlId);
return xml.substring(startIndex, endIndex);
}
private void assertInOrder(String sql, String... fragments) {
int previousIndex = -1;
for (String fragment : fragments) {
int currentIndex = sql.indexOf(fragment);
assertTrue(currentIndex > previousIndex, () -> "fragment order mismatch: " + fragment + "\n" + sql);
previousIndex = currentIndex;
}
}
}

View File

@@ -15,7 +15,9 @@ class CcdiProjectSpecialCheckMapperListSqlTest {
String listSql = extractSelect(xml, "selectFamilyAssetLiabilityList");
assertTrue(listSql.contains("order by risk_level_sort desc, comparison_amount desc, staff_name asc"));
assertTrue(xml.contains("from ccdi_bank_statement_tag_result"));
assertTrue(xml.contains("from ccdi_bank_statement bs"));
assertTrue(xml.contains("statement_staff.id_card = trim(bs.cret_no)"));
assertTrue(xml.contains("bs.project_id = #{projectId}"));
assertTrue(xml.contains("ccdi_base_staff"));
assertTrue(xml.contains("ccdi_staff_fmy_relation"));
assertTrue(xml.contains("relation_type = '配偶'"));

View File

@@ -132,6 +132,49 @@ class BankTagRuleConfigResolverTest {
assertEquals("8888", config.getThresholdValue("ANNUAL_TURNOVER"));
}
@Test
void resolve_shouldUseLargeTransactionParamsForExternalLargeTransactionRule() {
CcdiProject project = new CcdiProject();
project.setProjectId(40L);
project.setConfigType("default");
when(projectMapper.selectById(40L)).thenReturn(project);
when(modelParamMapper.selectByProjectAndModel(0L, "LARGE_TRANSACTION")).thenReturn(List.of(
buildParam("LARGE_TRANSACTION", "FREQUENT_TRANSFER", "100000")
));
CcdiBankTagRule ruleMeta = new CcdiBankTagRule();
ruleMeta.setModelCode("EXTERNAL_LARGE_TRANSACTION");
ruleMeta.setRuleCode("EXTERNAL_SINGLE_LARGE_AMOUNT");
ruleMeta.setIndicatorCode("FREQUENT_TRANSFER");
BankTagRuleExecutionConfig config = resolver.resolve(40L, ruleMeta);
assertEquals("100000", config.getThresholdValue("FREQUENT_TRANSFER"));
}
@Test
void resolve_shouldUseOriginalModelParamsForExternalObjectRules() {
CcdiProject project = new CcdiProject();
project.setProjectId(40L);
project.setConfigType("default");
when(projectMapper.selectById(40L)).thenReturn(project);
when(modelParamMapper.selectByProjectAndModel(0L, "LARGE_TRANSACTION")).thenReturn(List.of(
buildParam("LARGE_TRANSACTION", "CUMULATIVE_TRANSACTION_AMOUNT", "500000"),
buildParam("LARGE_TRANSACTION", "ANNUAL_TURNOVER", "800000")
));
when(modelParamMapper.selectByProjectAndModel(0L, "SUSPICIOUS_GAMBLING")).thenReturn(List.of(
buildParam("SUSPICIOUS_GAMBLING", "MULTI_PARTY_AMT_MIN", "500"),
buildParam("SUSPICIOUS_GAMBLING", "MULTI_PARTY_AMT_MAX", "5000")
));
assertRuleThresholds("EXTERNAL_LARGE_TRANSACTION", "EXTERNAL_CUMULATIVE_TRANSACTION_AMOUNT",
Map.of("CUMULATIVE_TRANSACTION_AMOUNT", "500000"));
assertRuleThresholds("EXTERNAL_LARGE_TRANSACTION", "EXTERNAL_ANNUAL_TURNOVER",
Map.of("ANNUAL_TURNOVER", "800000"));
assertRuleThresholds("EXTERNAL_SUSPICIOUS_GAMBLING", "EXTERNAL_MULTI_PARTY_GAMBLING_TRANSFER",
Map.of("MULTI_PARTY_AMT_MIN", "500", "MULTI_PARTY_AMT_MAX", "5000"));
}
@Test
void resolve_shouldMapPhaseOneThresholdRulesToUppercaseParamCodes() {
CcdiProject project = new CcdiProject();

View File

@@ -496,6 +496,95 @@ class CcdiBankTagServiceImplTest {
)));
}
@Test
void rebuildProject_shouldDispatchExternalPersonStatementRules() {
ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);
CcdiBankTagRule largeRule = buildRule("EXTERNAL_LARGE_TRANSACTION", "外部人员大额交易",
"EXTERNAL_SINGLE_LARGE_AMOUNT", "外部人员单笔大额交易", "STATEMENT");
CcdiBankTagRule nightRule = buildRule("EXTERNAL_ABNORMAL_TRANSACTION", "外部人员异常交易",
"EXTERNAL_NIGHT_TRANSACTION", "外部人员夜间集中交易", "STATEMENT");
CcdiBankTagRule gamblingRule = buildRule("EXTERNAL_SUSPICIOUS_GAMBLING", "外部人员可疑赌博",
"EXTERNAL_GAMBLING_MEMO", "外部人员疑似赌博摘要", "STATEMENT");
CcdiBankTagRule relationRule = buildRule("EXTERNAL_SUSPICIOUS_RELATION", "外部人员可疑关系",
"EXTERNAL_TO_STAFF_FAMILY_TRANSACTION", "外部人员与员工或员工亲属交易", "STATEMENT");
BankTagRuleExecutionConfig largeConfig = buildConfig(40L, largeRule);
largeConfig.setThresholdValues(Map.of("FREQUENT_TRANSFER", "100000"));
BankTagStatementHitVO hit = new BankTagStatementHitVO();
hit.setBankStatementId(100L);
hit.setGroupId(40);
hit.setLogId(40001);
hit.setReasonDetail("外部人员命中");
when(ruleMapper.selectEnabledRules(null)).thenReturn(List.of(largeRule, nightRule, gamblingRule, relationRule));
when(configResolver.resolve(40L, largeRule)).thenReturn(largeConfig);
when(configResolver.resolve(40L, nightRule)).thenReturn(buildConfig(40L, nightRule));
when(configResolver.resolve(40L, gamblingRule)).thenReturn(buildConfig(40L, gamblingRule));
when(configResolver.resolve(40L, relationRule)).thenReturn(buildConfig(40L, relationRule));
when(analysisMapper.selectExternalSingleLargeAmountStatements(40L, new BigDecimal("100000"))).thenReturn(List.of(hit));
when(analysisMapper.selectExternalNightTransactionStatements(40L)).thenReturn(List.of());
when(analysisMapper.selectExternalGamblingMemoStatements(40L)).thenReturn(List.of());
when(analysisMapper.selectExternalToStaffOrFamilyTransactionStatements(40L)).thenReturn(List.of());
service.rebuildProject(40L, null, "admin", TriggerType.MANUAL);
verify(analysisMapper).selectExternalSingleLargeAmountStatements(40L, new BigDecimal("100000"));
verify(analysisMapper).selectExternalNightTransactionStatements(40L);
verify(analysisMapper).selectExternalGamblingMemoStatements(40L);
verify(analysisMapper).selectExternalToStaffOrFamilyTransactionStatements(40L);
verify(resultMapper).insertBatch(argThat(results -> results.stream().anyMatch(item ->
"EXTERNAL_LARGE_TRANSACTION".equals(item.getModelCode())
&& "EXTERNAL_SINGLE_LARGE_AMOUNT".equals(item.getRuleCode())
&& "STATEMENT".equals(item.getResultType())
&& Long.valueOf(100L).equals(item.getBankStatementId())
)));
}
@Test
void rebuildProject_shouldDispatchExternalPersonObjectRules() {
ReflectionTestUtils.setField(service, "tagRuleExecutor", (Executor) Runnable::run);
CcdiBankTagRule cumulativeRule = buildRule("EXTERNAL_LARGE_TRANSACTION", "外部人员大额交易",
"EXTERNAL_CUMULATIVE_TRANSACTION_AMOUNT", "外部人员累计交易超限", "OBJECT");
CcdiBankTagRule annualRule = buildRule("EXTERNAL_LARGE_TRANSACTION", "外部人员大额交易",
"EXTERNAL_ANNUAL_TURNOVER", "外部人员年流水交易额超限", "OBJECT");
CcdiBankTagRule gamblingRule = buildRule("EXTERNAL_SUSPICIOUS_GAMBLING", "外部人员可疑赌博",
"EXTERNAL_MULTI_PARTY_GAMBLING_TRANSFER", "外部人员同日多对手方疑似赌博交易", "OBJECT");
BankTagRuleExecutionConfig cumulativeConfig = buildConfig(40L, cumulativeRule);
cumulativeConfig.setThresholdValues(Map.of("CUMULATIVE_TRANSACTION_AMOUNT", "500000"));
BankTagRuleExecutionConfig annualConfig = buildConfig(40L, annualRule);
annualConfig.setThresholdValues(Map.of("ANNUAL_TURNOVER", "800000"));
BankTagRuleExecutionConfig gamblingConfig = buildConfig(40L, gamblingRule);
gamblingConfig.setThresholdValues(Map.of("MULTI_PARTY_AMT_MIN", "500", "MULTI_PARTY_AMT_MAX", "5000"));
BankTagObjectHitVO hit = new BankTagObjectHitVO();
hit.setObjectType("EXTERNAL_CERT_NO");
hit.setObjectKey("330100198801010033");
hit.setReasonDetail("外部人员累计交易超限");
when(ruleMapper.selectEnabledRules(null)).thenReturn(List.of(cumulativeRule, annualRule, gamblingRule));
when(configResolver.resolve(40L, cumulativeRule)).thenReturn(cumulativeConfig);
when(configResolver.resolve(40L, annualRule)).thenReturn(annualConfig);
when(configResolver.resolve(40L, gamblingRule)).thenReturn(gamblingConfig);
when(analysisMapper.selectExternalCumulativeTransactionAmountObjects(40L, new BigDecimal("500000"))).thenReturn(List.of(hit));
when(analysisMapper.selectExternalAnnualTurnoverObjects(40L, new BigDecimal("800000"))).thenReturn(List.of());
when(analysisMapper.selectExternalMultiPartyGamblingTransferObjects(40L, new BigDecimal("500"), new BigDecimal("5000"))).thenReturn(List.of());
service.rebuildProject(40L, null, "admin", TriggerType.MANUAL);
verify(analysisMapper).selectExternalCumulativeTransactionAmountObjects(40L, new BigDecimal("500000"));
verify(analysisMapper).selectExternalAnnualTurnoverObjects(40L, new BigDecimal("800000"));
verify(analysisMapper).selectExternalMultiPartyGamblingTransferObjects(40L, new BigDecimal("500"), new BigDecimal("5000"));
verify(resultMapper).insertBatch(argThat(results -> results.stream().anyMatch(item ->
"EXTERNAL_LARGE_TRANSACTION".equals(item.getModelCode())
&& "EXTERNAL_CUMULATIVE_TRANSACTION_AMOUNT".equals(item.getRuleCode())
&& "OBJECT".equals(item.getResultType())
&& "EXTERNAL_CERT_NO".equals(item.getObjectType())
&& "330100198801010033".equals(item.getObjectKey())
)));
}
@Test
void buildSafeTaskErrorMessage_shouldKeepLongMessageForLongTextColumn() throws Exception {
Method method = CcdiBankTagServiceImpl.class.getDeclaredMethod(

View File

@@ -193,6 +193,88 @@ class CcdiFileUploadServiceImplTest {
() -> service.batchUploadFiles(PROJECT_ID, new MultipartFile[]{file}, "tester"));
}
@Test
void batchUploadFiles_shouldNormalizeFilenameBeforeSavingRecordAndTempFile() throws Exception {
setField("uploadPath", tempDir.toString());
mockProjectWithLsfxProjectId();
AtomicReference<List<CcdiFileUploadRecord>> inserted = new AtomicReference<>();
doAnswer(invocation -> {
List<CcdiFileUploadRecord> records = invocation.getArgument(0);
for (int i = 0; i < records.size(); i++) {
records.get(i).setId((long) (i + 1));
}
inserted.set(new ArrayList<>(records));
return records.size();
}).when(recordMapper).insertBatch(any());
MultipartFile file = new MockMultipartFile(
"files",
"330101199001010011 - 流 水 .xl sx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"content".getBytes()
);
TransactionSynchronizationManager.initSynchronization();
try {
String batchId = service.batchUploadFiles(PROJECT_ID, new MultipartFile[]{file}, "tester");
assertNotNull(batchId);
assertNotNull(inserted.get());
assertEquals("330101199001010011-流水.xlsx", inserted.get().get(0).getFileName());
Path tempRoot = tempDir.resolve("temp");
assertTrue(Files.exists(tempRoot));
try (var paths = Files.list(tempRoot)) {
assertTrue(paths.anyMatch(path ->
path.getFileName().toString().endsWith("_330101199001010011-流水.xlsx")));
}
} finally {
TransactionSynchronizationManager.clearSynchronization();
}
}
@Test
void batchUploadFiles_shouldRejectFileNameWithoutIdCardBeforeSavingTempFile() throws Exception {
setField("uploadPath", tempDir.toString());
mockProjectWithLsfxProjectId();
MultipartFile file = new MockMultipartFile(
"files",
"普通流水.xlsx",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"content".getBytes()
);
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> service.batchUploadFiles(PROJECT_ID, new MultipartFile[]{file}, "tester"));
assertTrue(exception.getMessage().contains("身份证"));
assertFalse(Files.exists(tempDir.resolve("temp")));
verify(recordMapper, never()).insertBatch(any());
verify(lsfxClient, never()).uploadFile(any(), org.mockito.ArgumentMatchers.<java.io.File>any(), any());
}
@Test
void batchUploadFiles_shouldRejectBlankNormalizedFilenameBeforeSavingTempFile() throws Exception {
setField("uploadPath", tempDir.toString());
mockProjectWithLsfxProjectId();
MultipartFile file = new MockMultipartFile(
"files",
" \u3000 ",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"content".getBytes()
);
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> service.batchUploadFiles(PROJECT_ID, new MultipartFile[]{file}, "tester"));
assertTrue(exception.getMessage().contains("文件名不能为空"));
assertFalse(Files.exists(tempDir.resolve("temp")));
verify(recordMapper, never()).insertBatch(any());
verify(lsfxClient, never()).uploadFile(any(), org.mockito.ArgumentMatchers.<java.io.File>any(), any());
}
@Test
void submitTasksAsync_shouldNotCreateLocalBatchLogFiles() throws Exception {
setField("uploadPath", tempDir.toString());
@@ -439,7 +521,7 @@ class CcdiFileUploadServiceImplTest {
}
@Test
void deleteFileUploadRecord_shouldDeletePlatformFileBankStatementsAndMarkDeleted() {
void deleteFileUploadRecord_shouldDeleteLocalBankStatementsAndMarkDeleted() {
CcdiFileUploadRecord record = buildRecord();
record.setProjectId(PROJECT_ID);
record.setLsfxProjectId(LSFX_PROJECT_ID);
@@ -449,7 +531,6 @@ class CcdiFileUploadServiceImplTest {
project.setProjectId(PROJECT_ID);
when(recordMapper.selectById(RECORD_ID)).thenReturn(record);
when(lsfxClient.deleteFiles(any())).thenReturn(buildDeleteFilesResponse());
when(projectMapper.selectById(PROJECT_ID)).thenReturn(project);
when(bankStatementMapper.countMatchedStaffCountByProjectId(PROJECT_ID)).thenReturn(2);
when(recordMapper.updateById(any(CcdiFileUploadRecord.class))).thenReturn(1);
@@ -457,12 +538,7 @@ class CcdiFileUploadServiceImplTest {
String result = service.deleteFileUploadRecord(RECORD_ID, 9527L);
assertEquals("删除成功已开始项目重新打标", result);
verify(lsfxClient).deleteFiles(argThat(request ->
request.getGroupId().equals(LSFX_PROJECT_ID)
&& request.getUserId().equals(9527)
&& request.getLogIds().length == 1
&& request.getLogIds()[0].equals(LOG_ID)
));
verify(lsfxClient, never()).deleteFiles(any());
verify(bankStatementMapper).deleteByProjectIdAndBatchId(PROJECT_ID, LOG_ID);
verify(recordMapper).updateById(org.mockito.ArgumentMatchers.<CcdiFileUploadRecord>argThat(item ->
RECORD_ID.equals(item.getId()) && "deleted".equals(item.getFileStatus())
@@ -500,21 +576,23 @@ class CcdiFileUploadServiceImplTest {
}
@Test
void deleteFileUploadRecord_shouldStopWhenLsfxDeleteFails() {
void deleteFileUploadRecord_shouldSkipLsfxDeleteApi() {
CcdiFileUploadRecord record = buildRecord();
record.setFileStatus("parsed_success");
record.setLogId(LOG_ID);
record.setLsfxProjectId(LSFX_PROJECT_ID);
when(recordMapper.selectById(RECORD_ID)).thenReturn(record);
when(lsfxClient.deleteFiles(any())).thenThrow(new RuntimeException("lsfx delete failed"));
when(recordMapper.updateById(any(CcdiFileUploadRecord.class))).thenReturn(1);
assertThrows(RuntimeException.class, () -> service.deleteFileUploadRecord(RECORD_ID, 9527L));
String result = service.deleteFileUploadRecord(RECORD_ID, 9527L);
verify(bankStatementMapper, never()).deleteByProjectIdAndBatchId(any(), any());
verify(bankTagService, never()).submitAutoRebuild(any(), any());
verify(recordMapper, never()).updateById(org.mockito.ArgumentMatchers.<CcdiFileUploadRecord>argThat(item ->
assertEquals("删除成功已开始项目重新打标", result);
verify(lsfxClient, never()).deleteFiles(any());
verify(bankStatementMapper).deleteByProjectIdAndBatchId(PROJECT_ID, LOG_ID);
verify(recordMapper).updateById(org.mockito.ArgumentMatchers.<CcdiFileUploadRecord>argThat(item ->
"deleted".equals(item.getFileStatus())
));
verify(bankTagService).submitAutoRebuild(PROJECT_ID, TriggerType.AUTO_FILE_DELETE);
}
// @Test
@@ -754,6 +832,13 @@ class CcdiFileUploadServiceImplTest {
}).when(recordMapper).updateById(any(CcdiFileUploadRecord.class));
}
private void mockProjectWithLsfxProjectId() {
CcdiProject project = new CcdiProject();
project.setProjectId(PROJECT_ID);
project.setLsfxProjectId(LSFX_PROJECT_ID);
when(projectMapper.selectById(PROJECT_ID)).thenReturn(project);
}
private CcdiFileUploadRecord buildRecord() {
CcdiFileUploadRecord record = new CcdiFileUploadRecord();
record.setId(RECORD_ID);

View File

@@ -3,6 +3,7 @@ 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.excel.CcdiProjectExternalPersonWarningExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportModelSummaryVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportParamVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportSuspiciousTransactionVO;
@@ -10,10 +11,12 @@ 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.CcdiProjectExternalRiskSummaryVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO;
import com.ruoyi.common.exception.ServiceException;
import java.io.File;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.List;
@@ -21,6 +24,7 @@ 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.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -39,6 +43,103 @@ class CcdiProjectOverviewReportPdfExporterTest {
assertTrue(response.getContentAsByteArray().length > 1000);
}
@Test
void shouldUseOverallRiskSummaryInReportMetrics() throws Exception {
CcdiProjectOverviewReportPdfExporter exporter = new CcdiProjectOverviewReportPdfExporter(resolveTestFontPath());
CcdiProjectOverviewReportVO report = buildReport();
report.setExternalRiskSummary(buildExternalSummary());
List<CcdiProjectOverviewStatVO> stats = invokeOverallRiskMetrics(exporter, report);
assertEquals(12, stats.get(0).getValue());
assertEquals(3, stats.get(1).getValue());
assertEquals(4, stats.get(2).getValue());
assertEquals(1, stats.get(3).getValue());
assertEquals("无风险", stats.get(4).getLabel());
assertEquals(4, stats.get(4).getValue());
}
@Test
void shouldHideExternalSectionWhenReportHasOnlyEmployees() throws Exception {
CcdiProjectOverviewReportPdfExporter exporter = new CcdiProjectOverviewReportPdfExporter(resolveTestFontPath());
MockHttpServletResponse response = new MockHttpServletResponse();
CcdiProjectOverviewReportVO report = buildReport();
exporter.export(response, report);
assertEquals("application/pdf", response.getContentType());
assertFalse(invokeHasExternalRisk(exporter, report));
}
@Test
void shouldShowExternalSectionWhenExternalRiskExists() throws Exception {
CcdiProjectOverviewReportPdfExporter exporter = new CcdiProjectOverviewReportPdfExporter(resolveTestFontPath());
MockHttpServletResponse response = new MockHttpServletResponse();
CcdiProjectOverviewReportVO report = buildReport();
report.setExternalRiskSummary(buildExternalSummary());
report.setExternalModelSummaries(List.of(buildExternalModelSummary()));
report.setExternalPersonWarnings(List.of(buildExternalPersonWarning()));
exporter.export(response, report);
List<CcdiProjectOverviewStatVO> externalMetrics = invokeExternalMetrics(exporter, report);
assertEquals("application/pdf", response.getContentType());
assertTrue(invokeHasExternalRisk(exporter, report));
assertEquals("外部人员", externalMetrics.get(0).getLabel());
assertEquals(2, externalMetrics.get(0).getValue());
assertEquals("高风险", externalMetrics.get(1).getLabel());
assertEquals(1, externalMetrics.get(1).getValue());
}
@Test
void tableGap_shouldLeaveEnoughSpaceForNextSectionTitle() throws Exception {
Class<?> writerClass = Class.forName(
"com.ruoyi.ccdi.project.service.impl.CcdiProjectOverviewReportPdfExporter$PdfPageWriter"
);
float tableAfterGap = readPrivateFloat(writerClass, "TABLE_AFTER_GAP");
float sectionFontSize = readPrivateFloat(writerClass, "SECTION_FONT_SIZE");
assertTrue(tableAfterGap > sectionFontSize);
}
@SuppressWarnings("unchecked")
private List<CcdiProjectOverviewStatVO> invokeOverallRiskMetrics(
CcdiProjectOverviewReportPdfExporter exporter,
CcdiProjectOverviewReportVO report
) throws Exception {
Method method = CcdiProjectOverviewReportPdfExporter.class.getDeclaredMethod(
"buildOverallRiskMetrics",
CcdiProjectOverviewReportVO.class
);
method.setAccessible(true);
return (List<CcdiProjectOverviewStatVO>) method.invoke(exporter, report);
}
@SuppressWarnings("unchecked")
private List<CcdiProjectOverviewStatVO> invokeExternalMetrics(
CcdiProjectOverviewReportPdfExporter exporter,
CcdiProjectOverviewReportVO report
) throws Exception {
Method method = CcdiProjectOverviewReportPdfExporter.class.getDeclaredMethod(
"buildExternalMetrics",
CcdiProjectOverviewReportVO.class
);
method.setAccessible(true);
return (List<CcdiProjectOverviewStatVO>) method.invoke(exporter, report);
}
private boolean invokeHasExternalRisk(
CcdiProjectOverviewReportPdfExporter exporter,
CcdiProjectOverviewReportVO report
) throws Exception {
Method method = CcdiProjectOverviewReportPdfExporter.class.getDeclaredMethod(
"hasExternalRisk",
CcdiProjectOverviewReportVO.class
);
method.setAccessible(true);
return (Boolean) method.invoke(exporter, report);
}
@Test
void shouldRejectBlankPdfFontPath() {
CcdiProjectOverviewReportPdfExporter exporter = new CcdiProjectOverviewReportPdfExporter("");
@@ -60,17 +161,6 @@ class CcdiProjectOverviewReportPdfExporterTest {
assertTrue(exception.getMessage().contains(missingPath));
}
@Test
void tableGap_shouldLeaveEnoughSpaceForNextSectionTitle() throws Exception {
Class<?> writerClass = Class.forName(
"com.ruoyi.ccdi.project.service.impl.CcdiProjectOverviewReportPdfExporter$PdfPageWriter"
);
float tableAfterGap = readPrivateFloat(writerClass, "TABLE_AFTER_GAP");
float sectionFontSize = readPrivateFloat(writerClass, "SECTION_FONT_SIZE");
assertTrue(tableAfterGap > sectionFontSize);
}
private String resolveTestFontPath() {
List<String> candidates = List.of(
"/System/Library/Fonts/STHeiti Medium.ttc",
@@ -111,22 +201,33 @@ class CcdiProjectOverviewReportPdfExporterTest {
private CcdiProjectOverviewDashboardVO buildDashboard() {
CcdiProjectOverviewDashboardVO dashboard = new CcdiProjectOverviewDashboardVO();
dashboard.setStats(List.of(
buildStat("总人数", 10),
buildStat("高风险", 2),
buildStat("中风险", 3),
buildStat("低风险", 1),
buildStat("无风险", 4)
buildStat("people", "总人数", 10),
buildStat("riskPeople", "高风险", 2),
buildStat("medium", "中风险", 3),
buildStat("low", "低风险", 1),
buildStat("count", "无风险人员", 4)
));
return dashboard;
}
private CcdiProjectOverviewStatVO buildStat(String label, Integer value) {
private CcdiProjectOverviewStatVO buildStat(String key, String label, Integer value) {
CcdiProjectOverviewStatVO stat = new CcdiProjectOverviewStatVO();
stat.setKey(key);
stat.setLabel(label);
stat.setValue(value);
return stat;
}
private CcdiProjectExternalRiskSummaryVO buildExternalSummary() {
CcdiProjectExternalRiskSummaryVO summary = new CcdiProjectExternalRiskSummaryVO();
summary.setTotal(2);
summary.setHigh(1);
summary.setMedium(1);
summary.setLow(0);
summary.setNoRisk(0);
return summary;
}
private CcdiProjectOverviewReportUploadSubjectVO buildUploadSubject() {
CcdiProjectOverviewReportUploadSubjectVO row = new CcdiProjectOverviewReportUploadSubjectVO();
row.setSubjectName("测试主体");
@@ -155,6 +256,15 @@ class CcdiProjectOverviewReportPdfExporterTest {
return row;
}
private CcdiProjectOverviewReportModelSummaryVO buildExternalModelSummary() {
CcdiProjectOverviewReportModelSummaryVO row = new CcdiProjectOverviewReportModelSummaryVO();
row.setModelName("外部人员异常交易");
row.setWarningCount(2);
row.setPeopleCount(2);
row.setPeopleNames("-");
return row;
}
private CcdiProjectRiskModelPeopleItemVO buildRiskPeople() {
CcdiProjectRiskModelPeopleItemVO row = new CcdiProjectRiskModelPeopleItemVO();
row.setStaffName("张三");
@@ -168,6 +278,19 @@ class CcdiProjectOverviewReportPdfExporterTest {
return row;
}
private CcdiProjectExternalPersonWarningExcel buildExternalPersonWarning() {
CcdiProjectExternalPersonWarningExcel row = new CcdiProjectExternalPersonWarningExcel();
row.setName("外部人员甲");
row.setIdNo("330000000000000003");
row.setSubjectType("外部人员");
row.setRiskLevel("高风险");
row.setModelCount(1);
row.setRiskPoint("疑似异常往来");
row.setRelatedObject("张三");
row.setLatestTradeTime("2026-06-25 10:00:00");
return row;
}
private CcdiProjectOverviewReportSuspiciousTransactionVO buildSuspiciousTransaction() {
CcdiProjectOverviewReportSuspiciousTransactionVO row = new CcdiProjectOverviewReportSuspiciousTransactionVO();
row.setTrxDate("2026-03-20 10:00:00");

View File

@@ -17,6 +17,7 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeRiskAggregateVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewDashboardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewEmployeeHitRowVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportSuspiciousTransactionVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectPersonAnalysisBasicInfoVO;
@@ -264,17 +265,20 @@ class CcdiProjectOverviewServiceImplTest {
project.setProjectId(40L);
when(projectMapper.selectById(40L)).thenReturn(project);
CcdiProjectSuspiciousTransactionItemVO suspiciousItem = new CcdiProjectSuspiciousTransactionItemVO();
CcdiProjectOverviewReportSuspiciousTransactionVO suspiciousItem =
new CcdiProjectOverviewReportSuspiciousTransactionVO();
suspiciousItem.setTrxDate("2026-03-20 10:00:00");
suspiciousItem.setSuspiciousPersonName("张三");
suspiciousItem.setRelatedPersonName("张三");
suspiciousItem.setLeAccountNo("6222000000000000");
suspiciousItem.setLeAccountName("张三");
suspiciousItem.setCustomerAccountName("测试对手方");
suspiciousItem.setCustomerAccountNo("6222000000000001");
suspiciousItem.setRelatedStaffName("张三");
suspiciousItem.setRelatedStaffCode("1001");
suspiciousItem.setRelationType("本人");
suspiciousItem.setUserMemo("转账");
suspiciousItem.setCashType("转账");
suspiciousItem.setHitTags("异常标签");
suspiciousItem.setDisplayAmount(new BigDecimal("100.00"));
when(overviewMapper.selectSuspiciousTransactionList(any())).thenReturn(List.of(suspiciousItem));
when(overviewMapper.selectReportSuspiciousTransactionList(any())).thenReturn(List.of(suspiciousItem));
CcdiProjectEmployeeCreditNegativeItemVO creditItem = new CcdiProjectEmployeeCreditNegativeItemVO();
creditItem.setPersonName("李四");
@@ -296,14 +300,17 @@ class CcdiProjectOverviewServiceImplTest {
MockHttpServletResponse response = new MockHttpServletResponse();
service.exportRiskDetails(response, 40L);
verify(overviewMapper).selectSuspiciousTransactionList(argThat(query ->
verify(overviewMapper).selectReportSuspiciousTransactionList(argThat(query ->
query.getProjectId().equals(40L) && "ALL".equals(query.getSuspiciousType())
));
verify(workbookExporter).export(
eq(response),
eq(40L),
argThat((List<CcdiProjectSuspiciousTransactionExcel> rows) ->
rows.size() == 1 && "张三".equals(rows.getFirst().getSuspiciousPersonName())
rows.size() == 1
&& "张三".equals(rows.getFirst().getLeAccountName())
&& "测试对手方".equals(rows.getFirst().getCustomerAccountName())
&& "异常标签".equals(rows.getFirst().getHitTags())
),
argThat((List<CcdiProjectEmployeeCreditNegativeExcel> rows) ->
rows.size() == 1 && "李四".equals(rows.getFirst().getPersonName())

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSuspiciousTransactionQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectOverviewReportSuspiciousTransactionVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO;
import com.ruoyi.ccdi.project.mapper.CcdiBankTagResultMapper;
@@ -98,17 +99,20 @@ class CcdiProjectOverviewServiceSuspiciousTransactionTest {
project.setProjectId(40L);
when(projectMapper.selectById(40L)).thenReturn(project);
CcdiProjectSuspiciousTransactionItemVO item = new CcdiProjectSuspiciousTransactionItemVO();
CcdiProjectOverviewReportSuspiciousTransactionVO item =
new CcdiProjectOverviewReportSuspiciousTransactionVO();
item.setTrxDate("2024-01-15 10:00:00");
item.setSuspiciousPersonName("孙七");
item.setRelatedPersonName("孙七");
item.setLeAccountNo("6222000000000000");
item.setLeAccountName("孙七");
item.setCustomerAccountName("测试对手方");
item.setCustomerAccountNo("6222000000000001");
item.setRelatedStaffName("孙七");
item.setRelatedStaffCode("809901");
item.setRelationType("本人");
item.setUserMemo("");
item.setCashType("转账");
item.setHitTags("大额交易");
item.setDisplayAmount(new BigDecimal("500000.00"));
when(overviewMapper.selectSuspiciousTransactionList(any(CcdiProjectSuspiciousTransactionQueryDTO.class)))
when(overviewMapper.selectReportSuspiciousTransactionList(any(CcdiProjectSuspiciousTransactionQueryDTO.class)))
.thenReturn(List.of(item));
CcdiProjectSuspiciousTransactionQueryDTO queryDTO = new CcdiProjectSuspiciousTransactionQueryDTO();
@@ -117,8 +121,14 @@ class CcdiProjectOverviewServiceSuspiciousTransactionTest {
List<CcdiProjectSuspiciousTransactionExcel> rows = service.exportSuspiciousTransactions(queryDTO);
assertEquals(1, rows.size());
assertEquals("6222000000000000", rows.getFirst().getLeAccountNo());
assertEquals("孙七", rows.getFirst().getLeAccountName());
assertEquals("测试对手方", rows.getFirst().getCustomerAccountName());
assertEquals("6222000000000001", rows.getFirst().getCustomerAccountNo());
assertEquals("孙七(809901)", rows.getFirst().getRelatedStaffDisplay());
assertEquals("/转账", rows.getFirst().getSummaryAndCashType());
assertEquals("", rows.getFirst().getUserMemo());
assertEquals("转账", rows.getFirst().getCashType());
assertEquals("大额交易", rows.getFirst().getHitTags());
}
@Test

View File

@@ -23,11 +23,14 @@ class CcdiProjectRiskDetailWorkbookExporterTest {
CcdiProjectSuspiciousTransactionExcel suspiciousRow = new CcdiProjectSuspiciousTransactionExcel();
suspiciousRow.setTrxDate("2026-03-20 10:00:00");
suspiciousRow.setSuspiciousPersonName("张三");
suspiciousRow.setRelatedPersonName("张三");
suspiciousRow.setLeAccountNo("6222000000000000");
suspiciousRow.setLeAccountName("张三");
suspiciousRow.setCustomerAccountName("测试对手方");
suspiciousRow.setCustomerAccountNo("6222000000000001");
suspiciousRow.setRelatedStaffDisplay("张三(1001)");
suspiciousRow.setRelationType("本人");
suspiciousRow.setSummaryAndCashType("转账/转账");
suspiciousRow.setUserMemo("转账");
suspiciousRow.setCashType("转账");
suspiciousRow.setHitTags("异常标签");
suspiciousRow.setDisplayAmount(new BigDecimal("100.00"));
CcdiProjectEmployeeCreditNegativeExcel creditRow = new CcdiProjectEmployeeCreditNegativeExcel();
@@ -56,6 +59,19 @@ class CcdiProjectRiskDetailWorkbookExporterTest {
assertEquals("涉疑交易明细", workbook.getSheetAt(0).getSheetName());
assertEquals("员工负面征信信息", workbook.getSheetAt(1).getSheetName());
assertEquals("异常账户人员信息", workbook.getSheetAt(2).getSheetName());
assertEquals("交易时间", workbook.getSheetAt(0).getRow(0).getCell(0).getStringCellValue());
assertEquals("本方账户", workbook.getSheetAt(0).getRow(0).getCell(1).getStringCellValue());
assertEquals("本方主体", workbook.getSheetAt(0).getRow(0).getCell(2).getStringCellValue());
assertEquals("对方名称", workbook.getSheetAt(0).getRow(0).getCell(3).getStringCellValue());
assertEquals("对方账户", workbook.getSheetAt(0).getRow(0).getCell(4).getStringCellValue());
assertEquals("关联员工", workbook.getSheetAt(0).getRow(0).getCell(5).getStringCellValue());
assertEquals("摘要", workbook.getSheetAt(0).getRow(0).getCell(6).getStringCellValue());
assertEquals("交易类型", workbook.getSheetAt(0).getRow(0).getCell(7).getStringCellValue());
assertEquals("异常标签", workbook.getSheetAt(0).getRow(0).getCell(8).getStringCellValue());
assertEquals("交易金额", workbook.getSheetAt(0).getRow(0).getCell(9).getStringCellValue());
assertEquals("测试对手方", workbook.getSheetAt(0).getRow(1).getCell(3).getStringCellValue());
assertEquals("6222000000000001", workbook.getSheetAt(0).getRow(1).getCell(4).getStringCellValue());
assertEquals("异常标签", workbook.getSheetAt(0).getRow(1).getCell(8).getStringCellValue());
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());

View File

@@ -4,6 +4,7 @@ import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import com.ruoyi.ccdi.project.domain.CcdiProject;
import com.ruoyi.ccdi.project.domain.ProjectAccessScope;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectImportHistoryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectQueryDTO;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSaveDTO;
@@ -14,6 +15,7 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectStatusCountsVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectVO;
import com.ruoyi.ccdi.project.mapper.CcdiBankTagTaskMapper;
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.lsfx.client.LsfxAnalysisClient;
import com.ruoyi.lsfx.domain.response.GetTokenResponse;
@@ -52,6 +54,9 @@ class CcdiProjectServiceImplTest {
@Mock
private CcdiProjectMapper projectMapper;
@Mock
private CcdiProjectAccessService projectAccessService;
@Mock
private CcdiBankTagTaskMapper bankTagTaskMapper;
@@ -79,6 +84,48 @@ class CcdiProjectServiceImplTest {
assertEquals(5L, counts.getStatus4());
}
@Test
void shouldCountDeletedProjectsSeparatelyForManagerScope() {
when(projectAccessService.buildCurrentScope()).thenReturn(
new ProjectAccessScope("manager", true, false, true)
);
when(projectMapper.selectCount(any())).thenReturn(10L, 3L, 4L, 2L, 1L, 5L);
when(projectMapper.selectDeletedProjectCount(any())).thenReturn(6L);
CcdiProjectStatusCountsVO counts = service.getStatusCounts();
assertEquals(6L, counts.getStatus5());
}
@Test
void shouldMarkProjectDeletedWithoutCallingBaseDelete() {
when(projectMapper.markProjectDeleted(40L, "tester")).thenReturn(1);
boolean result = service.deleteProject(40L, "tester");
assertTrue(result);
verify(projectAccessService).assertCanDelete(40L);
verify(projectMapper).markProjectDeleted(40L, "tester");
verify(projectMapper, never()).deleteById(40L);
}
@Test
void shouldRestoreDeletedProjectToCompleted() {
when(projectMapper.restoreDeletedProject(40L, "tester")).thenReturn(1);
service.restoreProject(40L, "tester");
verify(projectAccessService).assertCanManageDeletedProjects();
verify(projectMapper).restoreDeletedProject(40L, "tester");
}
@Test
void shouldRejectRestoreWhenProjectIsNotDeleted() {
when(projectMapper.restoreDeletedProject(41L, "tester")).thenReturn(0);
assertThrows(ServiceException.class, () -> service.restoreProject(41L, "tester"));
}
@Test
void shouldReturnLatestFailedTagTaskOnFailedProjectDetail() {
Date endTime = new Date();
@@ -123,6 +170,19 @@ class CcdiProjectServiceImplTest {
() -> service.updateProjectStatus(99L, "3", "system"));
}
@Test
void shouldRejectUpdatingDeletedProjectToTaggingOrCompleted() {
CcdiProject deleted = new CcdiProject();
deleted.setProjectId(99L);
deleted.setStatus("5");
when(projectMapper.selectById(99L)).thenReturn(deleted);
assertThrows(ServiceException.class,
() -> service.updateProjectStatus(99L, "3", "system"));
assertThrows(ServiceException.class,
() -> service.updateProjectStatus(99L, "1", "system"));
}
@Test
void shouldRejectWritingWhenProjectIsTagging() {
CcdiProject tagging = new CcdiProject();
@@ -204,13 +264,13 @@ class CcdiProjectServiceImplTest {
archived.setProjectId(2L);
archived.setStatus("2");
when(projectMapper.selectHistoryProjects(queryDTO)).thenReturn(List.of(completed, archived));
when(projectMapper.selectHistoryProjects(any(), any())).thenReturn(List.of(completed, archived));
List<CcdiProjectHistoryListItemVO> result = service.listHistoryProjects(queryDTO);
assertEquals(2, result.size());
assertEquals(List.of(completed, archived), result);
verify(projectMapper).selectHistoryProjects(queryDTO);
verify(projectMapper).selectHistoryProjects(any(), any());
}
@Test

View File

@@ -18,6 +18,8 @@ class CcdiProjectStatusSqlTest {
String migrationSql = Files.readString(repoRoot.resolve("sql/migration/2026-03-18-add-project-tagging-status.sql"));
String tagFailedMigrationSql =
Files.readString(repoRoot.resolve("sql/migration/2026-05-27-add-project-tag-failed-status.sql"));
String deletedMigrationSql =
Files.readString(repoRoot.resolve("sql/migration/2026-07-02-add-project-deleted-status.sql"));
assertTrue(initSql.contains("打标中"));
assertTrue(initSql.contains("'3'"));
@@ -34,5 +36,14 @@ class CcdiProjectStatusSqlTest {
assertTrue(tagFailedMigrationSql.contains("'4'"));
assertTrue(tagFailedMigrationSql.contains("latest_task.status = 'FAILED'"));
assertTrue(tagFailedMigrationSql.contains("project.status IN ('0', '3')"));
assertTrue(initSql.contains("已删除"));
assertTrue(initSql.contains("'5'"));
assertTrue(prodInitSql.contains("已删除"));
assertTrue(prodInitSql.contains("'5','ccdi_project_status'"));
assertTrue(deletedMigrationSql.contains("ccdi_project_status"));
assertTrue(deletedMigrationSql.contains("已删除"));
assertTrue(deletedMigrationSql.contains("'5'"));
assertTrue(deletedMigrationSql.contains("utf8mb4_general_ci"));
}
}

View File

@@ -0,0 +1 @@
mock-maker-subclass

View File

@@ -0,0 +1,211 @@
# 项目结果总览排除可疑改造设计
## 1. 背景
项目详情的结果总览当前直接消费银行流水打标结果与员工结果快照。用户在核查过程中如果确认某条预警不是可疑问题,只能在认知上忽略,系统没有持久化排除能力。刷新页面、重新进入项目或导出报告时,该预警仍会按有效命中展示。
仓库初始化 SQL 中已存在 `ccdi_project_risk_exclusion` 表,语义是“项目结果页排除可疑记录表”,字段能够承载项目、人员或流水、规则编码、排除类型与排除原因。本次改造使用该表完成排除记录持久化,不删除原始打标结果。
## 2. 目标
1. 用户可在项目分析详情的异常明细中对单条预警执行“排除可疑”。
2. 排除对象精确到单条规则命中,不影响同一人员或同一流水的其他规则。
3. 排除后当前页面立即刷新,风险人员、模型预警次数、命中人数、涉疑交易明细按有效命中重新展示。
4. 浏览器刷新或重新进入项目后,已排除预警仍不作为有效预警展示。
5. 本阶段不做“恢复预警”入口,保持最短闭环。
## 3. 非目标
1. 不删除 `ccdi_bank_statement_tag_result` 原始命中结果。
2. 不使用 `localStorage``sessionStorage` 或 Cookie 保存排除状态。
3. 不做批量排除、恢复排除、排除记录管理页。
4. 不改变打标规则执行逻辑;重新打标后仍生成原始命中,展示查询阶段通过排除表过滤。
## 4. 用户操作链路
### 4.1 流水明细型预警
示例:某笔流水同时命中“大额转账交易”和“疑似敏感交易”。
1. 用户进入项目详情 > 结果总览。
2. 点击风险人员或模型命中人员的“查看项目”。
3. 在项目分析详情中进入“异常明细”。
4. 在“流水异常明细”表格中,异常标签旁展示小号操作“排除可疑”。
5. 用户点击某个标签的“排除可疑”,填写排除原因并确认。
6. 系统只排除该笔流水上的该条规则标签。
排除键:
```text
project_id + bank_statement_id + rule_code + exclusion_type = STATEMENT
```
效果:
1. 被排除的标签不再显示。
2. 同一笔流水的其他标签继续显示。
3. 如果该笔流水没有剩余有效标签,则不再出现在异常流水明细中。
4. 相关模型预警次数减少。
### 4.2 对象型 / 人员型预警
示例:某人命中“年流水交易额超限”,该预警是按人员聚合生成,不对应单笔流水。
1. 用户进入项目详情 > 结果总览。
2. 点击该人员的“查看项目”。
3. 在项目分析详情中进入“异常明细”。
4. 在“对象异常明细”卡片右上角展示小号操作“排除可疑”,样式与“加入证据库”保持一致。
5. 用户点击“排除可疑”,填写排除原因并确认。
6. 系统只排除该人员命中的该条规则。
排除键:
```text
project_id + staff_id_card + rule_code + exclusion_type = OBJECT
```
效果:
1. 该对象型预警卡片不再显示。
2. 该人员其他规则命中继续保留。
3. 如果该人员没有剩余有效规则,则从风险人员列表中移除。
4. 如果该人员仍有其他有效规则,则人员仍展示,但模型数、规则标签和风险等级按剩余有效命中重新计算。
## 5. 数据设计
使用既有表 `ccdi_project_risk_exclusion`
关键字段:
| 字段 | 用途 |
| --- | --- |
| `project_id` | 项目 ID |
| `staff_id_card` | 对象型预警对应人员证件号;外部人员也使用证件号 |
| `rule_code` | 被排除规则编码 |
| `exclusion_type` | `STATEMENT``OBJECT` |
| `bank_statement_id` | 流水型预警对应流水 ID |
| `exclude_reason` | 排除原因 |
唯一约束:
1. 流水型:`project_id + rule_code + exclusion_type + bank_statement_id`
2. 对象型:`project_id + staff_id_card + rule_code + exclusion_type`
如果目标环境缺少该表,实施时补充增量 SQL使用 `utf8mb4``utf8mb4_general_ci`
## 6. 后端设计
### 6.1 新增接口
新增“排除可疑”接口:
```text
POST /ccdi/project/overview/risk-exclusions
```
请求字段:
| 字段 | 必填 | 说明 |
| --- | --- | --- |
| `projectId` | 是 | 项目 ID |
| `exclusionType` | 是 | `STATEMENT``OBJECT` |
| `ruleCode` | 是 | 规则编码 |
| `bankStatementId` | 流水型必填 | 流水 ID |
| `staffIdCard` | 对象型必填 | 人员证件号 |
| `excludeReason` | 是 | 排除原因 |
校验规则:
1. `exclusionType=STATEMENT` 时必须传 `bankStatementId`
2. `exclusionType=OBJECT` 时必须传 `staffIdCard`
3. `excludeReason` 不能为空,长度不超过 1000。
4. 先校验用户对项目有读写权限;归档或只读项目不允许排除。
5. 重复排除视为幂等成功,更新原因和更新时间。
### 6.2 查询过滤
所有结果总览相关查询应统一过滤排除表。
过滤规则:
1. 读取 `ccdi_bank_statement_tag_result` 时左关联排除表。
2. 流水型命中用 `project_id + rule_code + bank_statement_id + STATEMENT` 匹配。
3. 对象型命中用 `project_id + rule_code + staff_id_card/object_key + OBJECT` 匹配。
4. 匹配到排除记录的命中不进入后续聚合、列表、标签组装、导出。
重点影响范围:
1. 风险人员列表。
2. 风险模型卡片。
3. 风险模型命中人员。
4. 人员项目分析详情。
5. 涉疑交易明细。
6. 一键 PDF 报告与 Excel 导出。
7. 外部人员预警及外部人员详情。
### 6.3 员工结果快照
当前风险人员列表读取 `ccdi_project_overview_employee_result` 快照。为了让“排除可疑”后统计即时变化,实施时应在排除成功后触发当前项目员工结果快照重算,或将列表查询切回有效命中实时聚合。
本次推荐:
1. 排除接口写入排除表后,同步触发当前项目结果总览员工快照重算。
2. 重算逻辑只使用未排除命中。
3. 页面刷新后读取更新后的快照。
理由:保留现有列表分页性能与页面结构,改动集中,不引入双口径。
## 7. 前端设计
### 7.1 操作入口
流水异常明细:
1. 异常标签展示为标签 + 小号文字按钮。
2. 按钮文案为“排除可疑”。
3. 按钮只作用于当前标签,不作用于整行。
对象异常明细:
1. 卡片右上角增加小号按钮“排除可疑”。
2. 样式仿照“加入证据库”,保持轻量。
3. 不新增大按钮、不改变卡片主视觉。
### 7.2 确认弹窗
使用 Element UI 弹窗或对话框,要求用户填写排除原因。
确认文案需要明确影响范围:
1. 流水型:仅排除当前流水的当前规则标签。
2. 对象型:仅排除当前人员的当前规则预警。
### 7.3 刷新策略
排除成功后,前端通知父组件刷新:
1. 重新加载项目分析详情。
2. 重新加载风险人员列表。
3. 重新加载风险模型卡片与命中人员。
4. 重新加载涉疑交易明细。
不做局部假刷新,避免页面统计与后端状态不一致。
## 8. 风险与约束
1. 如果只过滤详情、不刷新快照,风险人员列表和模型统计会不一致;必须统一刷新统计口径。
2. 对象型预警没有 `bank_statement_id`,必须用人员证件号和规则编码定位。
3. 外部人员对象型预警同样需要按证件号处理,不能只适配员工。
4. 排除记录不删除原始命中,因此重新打标不会覆盖排除结果。
## 9. 验证场景
1. 流水一条多标签,排除其中一个标签后另一个标签仍显示。
2. 排除流水标签后模型预警次数减少。
3. 排除对象型“年流水交易额超限”后对象卡片消失。
4. 人员仅剩一条对象型预警时,排除后人员不再出现在风险人员列表。
5. 人员有多条预警时,排除一条后人员仍保留,标签和统计减少。
6. 刷新浏览器后排除结果仍生效。
7. 导出报告不包含已排除预警。
8. 归档或只读项目不允许排除。

View File

@@ -0,0 +1,46 @@
# 外部人员预警后端实施计划
## 目标
在不改变结果总览员工主口径的前提下,新增外部人员预警查询与导出能力,使中介、客户、其他外部人员作为本方流水导入后,也能在结果总览中形成预警结果。
## 实施范围
1. 新增外部人员预警分页接口。
2. 新增外部人员预警导出接口。
3. 新增外部人员模型统计接口。
4. 新增外部人员模型命中人员分页接口。
5. 扩展涉疑交易明细筛选,支持外部人员相关流水。
6. 补充外部人员预警测试数据 SQL。
## 业务口径
1. 本方 `cret_no` 命中员工身份证号时,归入员工。
2. 本方 `cret_no` 命中员工亲属证件号时,归入员工亲属。
3. 本方 `cret_no` 未命中员工和员工亲属时,归入外部人员。
4. 外部人员 `cret_no` 命中中介库本人证件号时,主体类型为中介。
5. 外部人员 `cret_no` 命中信贷客户证件号时,主体类型为客户。
6. 其他外部人员统一显示为外部人员。
7. 外部人员只跑交易和关系类模型,不套用员工资产、负面征信、岗位部门类模型。
## 模型范围
1. `EXTERNAL_LARGE_TRANSACTION`:外部人员大额交易。
2. `EXTERNAL_ABNORMAL_TRANSACTION`:外部人员异常交易。
3. `EXTERNAL_SUSPICIOUS_GAMBLING`:外部人员可疑赌博。
4. `EXTERNAL_SUSPICIOUS_RELATION`:外部人员可疑关系。
## 数据实现
本轮采用直接聚合现有流水和标签结果的最短路径,不新增结果快照表:
1. 外部人员主体来自 `ccdi_bank_statement.cret_no`
2. 命中模型来自 `ccdi_bank_statement_tag_result` 中外部人员模型编码。
3. 与员工或员工亲属的关系来自交易对手证件号、交易对手姓名、账户库命中结果。
4. 中介识别优先使用中介库 `person_id`,不使用姓名作为主体识别依据。
## 验证
1. 执行新增测试数据 SQL。
2. 运行 `mvn test -pl ccdi-project` 相关测试或至少编译 `ccdi-project`
3. 启动后端后验证新增接口返回外部人员预警列表、模型统计和导出接口。

View File

@@ -0,0 +1,29 @@
# 专项分析项目员工范围后端实施计划
## 背景
专项分析原先通过项目打标命中结果解析员工范围。未命中风险规则的项目员工不会展示在专项分析中,无法满足“项目内员工即使未命中也进行专项排查”的业务要求。
## 实施范围
- 修改专项分析公共员工范围 SQL。
- 范围口径调整为:项目已入库银行流水 `ccdi_bank_statement``cret_no` 能匹配员工主数据 `ccdi_base_staff.id_card` 的员工。
- 不新增前端入口、不新增人工纳入表、不伪造打标命中结果。
## 实施步骤
1. 调整 `CcdiProjectSpecialCheckMapper.xml``projectEmployeeScopeSql`
2. 使用 `ccdi_bank_statement.project_id``cret_no` 获取项目员工范围。
3. 关联 `ccdi_base_staff` 获取员工姓名、柜员号、部门。
4. 保持资产负债专项核查、采购拓展、招聘拓展、调动拓展共用同一员工范围。
## 影响范围
- 员工家庭资产负债专项核查列表与详情。
- 专项分析下采购拓展、招聘拓展、调动拓展查询。
- 结果总览风险人员统计不在本次调整范围内,仍按打标结果口径。
## 验证计划
- 执行 Maven 编译,确认 Mapper XML 与 Java 工程可编译。
- 检查 SQL 引用字段存在且口径与项目目标人数回填逻辑一致。

Some files were not shown because too many files have changed in this diff Show More