98 Commits

Author SHA1 Message Date
wkc
ffaa2fb2a0 test 2026-07-21 09:01:45 +08:00
wkc
dc8055ce7a 删除征信解析后的本地文件 2026-07-20 15:55:28 +08:00
wkc
90f483f427 更新agents 2026-07-20 15:47:06 +08:00
wkc
2d9828ea5f 新增业务外部接口日志管理 2026-07-20 15:47:06 +08:00
wjj
0a0313af7c Merge remote-tracking branch 'origin/dev-ui' into dev-ui 2026-07-20 14:37:45 +08:00
wjj
175a50bf96 新增流水明细对手方汇总查询 2026-07-20 14:29:23 +08:00
wjj
7fec8e21be 新增未提交账户风险明细 2026-07-20 14:26:29 +08:00
wjj
75d094dad4 支持拉取行内和金综流水 2026-07-20 13:08:33 +08:00
wjj
17628486ae 支持拉取行内和金综流水 2026-07-20 13:07:49 +08:00
wjj
dd424cca50 新增结果总览未提交账户展示 2026-07-20 11:50:45 +08:00
wjj
1561df924f 修复流水明细导出权限校验 2026-07-20 11:26:58 +08:00
wjj
657977dfb7 完善异常账户预警口径和账户查询 2026-07-20 11:13:54 +08:00
wjj
4a75d68bf6 修复流水上传身份证号回填 2026-07-20 11:03:05 +08:00
wjj
b70f967153 调整涉疑交易默认每页十条 2026-07-16 19:05:34 +08:00
wjj
6492fca2dd 设计涉疑交易默认分页调整 2026-07-16 18:52:47 +08:00
wjj
e0188b5e5b 修复涉疑交易筛选与标签错乱 2026-07-16 18:46:51 +08:00
wjj
c3b56bdf2a 设计涉疑交易筛选一致性优化 2026-07-16 18:01:14 +08:00
wjj
6d90bb4e58 优化无外部人员涉疑交易查询 2026-07-16 15:26:14 +08:00
wjj
4a7cfde4af 调整专项排查资产收入匹配口径 2026-07-16 14:47:04 +08:00
wjj
15aa9117c3 调整员工信息维护列表和个人资料校验 2026-07-16 11:28:55 +08:00
wjj
ded07a2c33 优化前端页面布局细节 2026-07-13 16:46:37 +08:00
wjj
7a317204f7 调整流水打标规则口径 2026-07-13 13:05:35 +08:00
wjj
2c6c982ed3 账户库中介风险等级固定为高 2026-07-10 17:46:51 +08:00
wjj
486a499af7 优化流水账号入库和明细筛选 2026-07-10 17:33:16 +08:00
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
343 changed files with 20034 additions and 1780 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

@@ -1,44 +1,5 @@
# AGENTS.md - AI Coding Assistant Guide
## 全局执行规则
### Git
- Git 提交时使用中文添加描述
- 无视 `.DS_Store`
### AGENT
- `using-superpowers` 只有在用户明确声明调用时才允许启用;不要因为“会话开始”“任务较复杂”或“可能适用”而自动调用
- 当用户没有明确声明 `using-superpowers` 时,按普通流程直接处理需求
- 默认不开启 subagent
- 如用户明确要求启用 subagent所有 subagent 必须使用 `gpt-5.5`;默认推理强度为高,审查类 subagent 的推理强度为超高,测试类 subagent 的推理强度为中
### 文档
- 写完的设计文档必须要由审查类子代理进行内容审查,确保方案与实施方法符合实际需求,并检查是否还有需要明确的功能点
- 当功能设计涉及到前端和后端都有改动时,输出两份执行文档,一份为后端的实施计划,一份为前端的实施计划。如果不是前后端架构的项目不需要输出两份执行文档
- 当功能修改只涉及到前端或只涉及到后端,只需要输出对应的实施计划
- 每一次改动都需要留下实施文档,记录修改的内容
- 每次写设计文档的时候,都要检查一下保存路径是否正确
### 测试
- 测试结束后,自动结束测试时开启的前后端进程
- 前端 Node 需要使用 nvm 进行控制版本
- 在完成页面功能开发后,必须使用 `browser-use` 技能打开浏览器进行实际页面测试,禁止打开 prototype 原型页面进行测试
- 所有生成的测试文件不需要上传到 Git
### 方案规范
当需要给出方案时必须符合以下规范:
- 不允许给出兼容性或补丁性的方案,不允许过度设计,保持最短路径实现且不能违反第一条要求
- 不允许自行给出用户提供的需求以外的方案,例如一些兜底和降级方案,这可能导致业务逻辑偏移问题
- 必须确保方案的逻辑正确,必须经过全链路的逻辑验证
---
## 项目概述
本仓库是纪检初核系统主仓库,基于若依 `v3.9.1`,当前技术栈以 `Java 21 + Spring Boot 3 + Vue 2` 为主,并包含独立的流水分析 Mock 服务、Docker 部署文件、SQL 脚本、实施文档与测试文档。
@@ -54,52 +15,26 @@
---
## 高优先级规则
- 使用简体中文进行思考和对话
- Git 提交说明必须使用中文
- 忽略 `.DS_Store` 文件,不将其视为本次任务需要处理或提交的有效变更
- 仅当用户明确声明调用 `using-superpowers` 时才允许启用;未明确声明时按普通流程直接处理需求
- Git 提交前必须检查暂存区,仅允许包含本次任务相关文件;若存在无关文件,必须先移出暂存或与用户确认
- 每一次改动都需要留下实施文档,记录修改内容、影响范围与验证情况
- 功能设计同时涉及前端和后端改动时,必须分别输出后端与前端两份实施计划;若仅涉及单侧,则只输出对应实施计划
- 新增或修改设计文档、实施计划、实施记录前,必须先确认保存路径是否正确
- 前端相关安装、构建、调试、测试命令执行前,必须先通过 `nvm` 切换并确认 Node 版本
- 测试结束后,自动关闭测试过程中启动的前后端进程
- 重启后端时,必须优先使用 `bin/restart_java_backend.sh`
- 禁止在前端源码、配置、示例数据或页面默认值中硬编码或预填真实账号密码;登录页不得将密码保存到 Cookie、localStorage 或 sessionStorage
---
## 协作约定
### 基础协作
- 使用简体中文进行思考和对话
- 前端开发直接在当前分支进行,不需要额外创建 git worktree
- 给出方案时,必须保持最短路径实现,不允许提供兼容性、补丁性或过度设计的方案
- 不允许自行扩展出用户需求之外的兜底、降级或变体方案,避免业务逻辑偏移
- 输出方案前必须完成全链路逻辑校验,确保方案逻辑正确、链路闭环
### Git 与变更管理
- Git 提交前必须检查暂存区,仅保留本次任务相关文件
- 若暂存区存在无关文件,必须先移出暂存或与用户确认,禁止顺带提交
- `.DS_Store` 默认忽略,不纳入任务变更范围
- Git 提交前必须检查暂存区,仅保留本次任务相关文件;若存在无关文件,必须先移出暂存或与用户确认,禁止顺带提交
### 文档产出
- 若需求来自设计文档,默认同时沉淀后端与前端两份实施计划
- 功能设计同时涉及前端和后端改动时,实施计划分别放在 `docs/plans/backend/``docs/plans/frontend/`
- 功能修改只涉及前端或只涉及后端时,只输出对应的实施计划
- 非前后端架构项目不强制拆分两份实施计划
- 每一次改动都需要留下实施文档,实施记录优先放在 `docs/reports/implementation/`
- 每次新增或修改设计文档、实施计划、实施记录前,都要先确认保存路径是否正确
- 后端实施计划放在 `docs/plans/backend/`,前端实施计划放在 `docs/plans/frontend/`
- 综合实施计划、联调计划和通用实施文档放在 `docs/plans/fullstack/`
- 每一次改动都需要留下实施文档,记录修改内容、影响范围与验证情况;实施记录优先放在 `docs/reports/implementation/`
### 测试与运行
- 测试结束后,自动关闭测试过程中启动的前后端进程
- 重启后端时,必须优先使用 `bin/restart_java_backend.sh`,不要直接手工执行 `java -jar` 替代正式重启流程
- 前端相关安装、构建、调试、测试命令执行前,必须先通过 `nvm` 切换并确认 Node 版本
### 数据库与编码
@@ -258,7 +193,8 @@ return AjaxResult.success(result);
- 请求统一使用 `@/utils/request`
- 新增页面或功能入口时,同步检查 `sys_menu`、路由、权限标识
- 优先延续现有 `ccdi*` 业务目录与命名方式,不随意新造平行目录
- 登录页只能在用户主动选择时保存用户名,不允许保存密码或预填默认密码
- 禁止在前端源码、配置、示例数据或页面默认值中硬编码或预填真实账号密码
- 登录页只能在用户主动选择时保存用户名,不允许将密码保存到 Cookie、localStorage 或 sessionStorage也不允许预填默认密码
### 导入功能规范
@@ -386,10 +322,6 @@ ccdi/
- 新增文档优先落到 `docs/` 下的规范目录,不要继续堆放到 `docs/plans/` 根目录
- 只有历史资料或外部原始材料才放入 `assets/`
- 如果移动了文档,需同步修正文档内引用路径
- 若需求来自设计文档,默认同时沉淀后端与前端两份实施计划
- 功能设计同时涉及前端和后端改动时,必须分别输出后端与前端两份实施计划;若仅涉及前端或仅涉及后端,则只输出对应实施计划;非前后端架构项目不强制拆分双文档
- 每一次改动都需要留下实施文档,记录本次修改内容、影响范围与验证情况,实施记录优先放在 `docs/reports/implementation/`
- 每次新增或修改设计文档、实施计划、实施记录前,都要先确认保存路径是否正确
---
@@ -399,5 +331,3 @@ ccdi/
- `lsfx-mock-server` 是独立子项目,除非任务涉及流水分析联调,否则不要把其实现规则混入主 Java 工程
- `docker/backend``docker/frontend``docker/mock` 分别对应三类运行时镜像
- `sql/migration/` 用于增量迁移脚本,新增修复脚本优先按日期或功能命名
- 启动前后端或 Mock 服务做验证后,结束测试时要主动停止进程,避免残留占用端口
- 前端相关安装、构建、调试、测试命令执行前,必须先通过 `nvm` 切换并确认 Node 版本

View File

@@ -11,6 +11,8 @@ import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.info.collection.domain.dto.CcdiCreditInfoQueryDTO;
import com.ruoyi.info.collection.domain.vo.CreditInfoListVO;
import com.ruoyi.info.collection.service.ICcdiCreditInfoService;
import com.ruoyi.lsfx.domain.CallerContext;
import com.ruoyi.common.utils.SecurityUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
@@ -42,7 +44,8 @@ public class CcdiCreditInfoController extends BaseController {
@Log(title = "征信维护", businessType = BusinessType.IMPORT)
@PostMapping("/upload")
public AjaxResult upload(@RequestParam("files") MultipartFile[] files) {
return AjaxResult.success("上传成功", creditInfoService.upload(Arrays.asList(files)));
CallerContext caller = CallerContext.from(SecurityUtils.getLoginUser());
return AjaxResult.success("上传成功", creditInfoService.upload(Arrays.asList(files), caller));
}
@Operation(summary = "查询征信维护列表")

View File

@@ -39,6 +39,10 @@ public class CcdiAccountInfoQueryDTO implements Serializable {
@Schema(description = "账户姓名")
private String accountName;
/** 账户号码 */
@Schema(description = "账户号码")
private String accountNo;
/** 账户类型 */
@Schema(description = "账户类型")
private String accountType;

View File

@@ -28,6 +28,14 @@ public interface CcdiBaseStaffMapper extends BaseMapper<CcdiBaseStaff> {
Page<CcdiBaseStaffVO> selectBaseStaffPageWithDept(@Param("page") Page<CcdiBaseStaffVO> page,
@Param("query") CcdiBaseStaffQueryDTO queryDTO);
/**
* 查询员工详情(包含部门名称)
*
* @param staffId 员工ID
* @return 员工详情
*/
CcdiBaseStaffVO selectBaseStaffByIdWithDept(@Param("staffId") Long staffId);
int insertOrUpdateBatch(@Param("list") List<CcdiBaseStaff> list);
/**

View File

@@ -7,6 +7,7 @@ import com.ruoyi.info.collection.domain.dto.CcdiCreditInfoQueryDTO;
import com.ruoyi.info.collection.domain.vo.CreditInfoDetailVO;
import com.ruoyi.info.collection.domain.vo.CreditInfoListVO;
import com.ruoyi.info.collection.domain.vo.CreditInfoUploadResultVO;
import com.ruoyi.lsfx.domain.CallerContext;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@@ -16,7 +17,7 @@ import java.util.List;
*/
public interface ICcdiCreditInfoService {
CreditInfoUploadResultVO upload(List<MultipartFile> files);
CreditInfoUploadResultVO upload(List<MultipartFile> files, CallerContext caller);
Page<CreditInfoListVO> selectCreditInfoPage(Page<CreditInfoListVO> page, CcdiCreditInfoQueryDTO queryDTO);

View File

@@ -49,6 +49,10 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
private static final Set<String> ACCOUNT_TYPES = Set.of("BANK", "SECURITIES", "PAYMENT", "OTHER");
private static final Set<String> BANK_SCOPES = Set.of("INTERNAL", "EXTERNAL");
private static final Set<String> LEVELS = Set.of("LOW", "MEDIUM", "HIGH");
private static final String OWNER_TYPE_INTERMEDIARY = "INTERMEDIARY";
private static final String BANK_SCOPE_EXTERNAL = "EXTERNAL";
private static final String RISK_LEVEL_LOW = "LOW";
private static final String RISK_LEVEL_HIGH = "HIGH";
private final List<AccountInfoImportFailureVO> latestImportFailures = new CopyOnWriteArrayList<>();
@Resource
@@ -195,12 +199,12 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
private void validateOwner(String ownerType, String ownerId) {
if (StringUtils.isEmpty(ownerId)) {
if ("EXTERNAL".equals(ownerType) || "INTERMEDIARY".equals(ownerType)) {
if ("EXTERNAL".equals(ownerType) || OWNER_TYPE_INTERMEDIARY.equals(ownerType)) {
throw new RuntimeException("证件号不能为空");
}
throw new RuntimeException("所属人不能为空");
}
if ("EXTERNAL".equals(ownerType) || "INTERMEDIARY".equals(ownerType)) {
if ("EXTERNAL".equals(ownerType) || OWNER_TYPE_INTERMEDIARY.equals(ownerType)) {
return;
}
if ("EMPLOYEE".equals(ownerType)) {
@@ -232,8 +236,9 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
}
private void prepareAnalysisFields(CcdiAccountInfo accountInfo) {
if (!"EXTERNAL".equals(accountInfo.getBankScope())) {
if (!BANK_SCOPE_EXTERNAL.equals(accountInfo.getBankScope())) {
clearAnalysisFields(accountInfo);
applyIntermediaryRiskLevel(accountInfo);
return;
}
if (accountInfo.getIsActualControl() == null) {
@@ -249,8 +254,9 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
accountInfo.setTxnFrequencyLevel("MEDIUM");
}
if (StringUtils.isEmpty(accountInfo.getTxnRiskLevel())) {
accountInfo.setTxnRiskLevel("LOW");
accountInfo.setTxnRiskLevel(RISK_LEVEL_LOW);
}
applyIntermediaryRiskLevel(accountInfo);
}
private void clearAnalysisFields(CcdiAccountInfo accountInfo) {
@@ -265,6 +271,12 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
accountInfo.setTxnRiskLevel(null);
}
private void applyIntermediaryRiskLevel(CcdiAccountInfo accountInfo) {
if (OWNER_TYPE_INTERMEDIARY.equals(accountInfo.getOwnerType())) {
accountInfo.setTxnRiskLevel(RISK_LEVEL_HIGH);
}
}
private void validateAmount(BigDecimal amount, String fieldLabel) {
if (amount == null) {
return;
@@ -285,6 +297,7 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
addDTO.setTxnFrequencyLevel(toUpper(addDTO.getTxnFrequencyLevel()));
addDTO.setTxnRiskLevel(toUpper(addDTO.getTxnRiskLevel()));
addDTO.setOwnerId(normalizeOwnerId(addDTO.getOwnerId()));
applyIntermediaryRiskLevel(addDTO);
}
private void normalizeEditDto(CcdiAccountInfoEditDTO editDTO) {
@@ -295,6 +308,19 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
editDTO.setTxnFrequencyLevel(toUpper(editDTO.getTxnFrequencyLevel()));
editDTO.setTxnRiskLevel(toUpper(editDTO.getTxnRiskLevel()));
editDTO.setOwnerId(normalizeOwnerId(editDTO.getOwnerId()));
applyIntermediaryRiskLevel(editDTO);
}
private void applyIntermediaryRiskLevel(CcdiAccountInfoAddDTO addDTO) {
if (OWNER_TYPE_INTERMEDIARY.equals(addDTO.getOwnerType())) {
addDTO.setTxnRiskLevel(RISK_LEVEL_HIGH);
}
}
private void applyIntermediaryRiskLevel(CcdiAccountInfoEditDTO editDTO) {
if (OWNER_TYPE_INTERMEDIARY.equals(editDTO.getOwnerType())) {
editDTO.setTxnRiskLevel(RISK_LEVEL_HIGH);
}
}
private String normalizeCurrency(String currency) {
@@ -362,7 +388,8 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
private CcdiAccountInfoAddDTO toAddDto(CcdiAccountInfoExcel excel) {
CcdiAccountInfoAddDTO dto = new CcdiAccountInfoAddDTO();
dto.setOwnerType(parseOwnerType(excel.getOwnerType()));
String ownerType = parseOwnerType(excel.getOwnerType());
dto.setOwnerType(ownerType);
dto.setOwnerId(normalizeOwnerId(excel.getOwnerId()));
dto.setAccountName(trimToNull(excel.getAccountName()));
dto.setAccountNo(trimToNull(excel.getAccountNo()));
@@ -382,7 +409,9 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
dto.setCreditSingleMaxAmount(parseDecimal(excel.getCreditSingleMaxAmount()));
dto.setDebitDailyMaxAmount(parseDecimal(excel.getDebitDailyMaxAmount()));
dto.setCreditDailyMaxAmount(parseDecimal(excel.getCreditDailyMaxAmount()));
dto.setTxnRiskLevel(parseLevel(excel.getTxnRiskLevel(), "风险等级"));
dto.setTxnRiskLevel(OWNER_TYPE_INTERMEDIARY.equals(ownerType)
? RISK_LEVEL_HIGH
: parseLevel(excel.getTxnRiskLevel(), "风险等级"));
return dto;
}
@@ -418,7 +447,7 @@ public class CcdiAccountInfoServiceImpl implements ICcdiAccountInfoService {
return "RELATION";
}
if ("中介".equals(value)) {
return "INTERMEDIARY";
return OWNER_TYPE_INTERMEDIARY;
}
if ("外部人员".equals(value)) {
return "EXTERNAL";

View File

@@ -119,10 +119,10 @@ public class CcdiBaseStaffServiceImpl implements ICcdiBaseStaffService {
*/
@Override
public CcdiBaseStaffVO selectBaseStaffById(Long staffId) {
CcdiBaseStaff staff = baseStaffMapper.selectById(staffId);
CcdiBaseStaffVO vo = convertToVO(staff);
if (staff != null) {
vo.setAssetInfoList(assetInfoService.selectByFamilyIdAndPersonId(staff.getIdCard(), staff.getIdCard()).stream().map(asset -> {
CcdiBaseStaffVO vo = baseStaffMapper.selectBaseStaffByIdWithDept(staffId);
if (vo != null) {
vo.setStatusDesc(EmployeeStatus.getDescByCode(vo.getStatus()));
vo.setAssetInfoList(assetInfoService.selectByFamilyIdAndPersonId(vo.getIdCard(), vo.getIdCard()).stream().map(asset -> {
CcdiAssetInfoVO assetInfoVO = new CcdiAssetInfoVO();
BeanUtils.copyProperties(asset, assetInfoVO);
return assetInfoVO;

View File

@@ -1,7 +1,6 @@
package com.ruoyi.info.collection.service.impl;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.info.collection.domain.CcdiCreditNegativeInfo;
import com.ruoyi.info.collection.domain.CcdiDebtsInfo;
import com.ruoyi.info.collection.domain.dto.CcdiCreditInfoQueryDTO;
@@ -17,10 +16,12 @@ import com.ruoyi.info.collection.service.ICcdiCreditInfoService;
import com.ruoyi.info.collection.service.support.CreditHtmlStorageService;
import com.ruoyi.info.collection.service.support.CreditInfoPayloadAssembler;
import com.ruoyi.lsfx.client.CreditParseClient;
import com.ruoyi.lsfx.domain.CallerContext;
import com.ruoyi.lsfx.domain.response.CreditParseInvokeResponse;
import com.ruoyi.lsfx.domain.response.CreditParsePayload;
import com.ruoyi.lsfx.domain.response.CreditParseResponse;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
@@ -33,6 +34,7 @@ import java.util.Map;
/**
* 征信维护服务实现
*/
@Slf4j
@Service
public class CcdiCreditInfoServiceImpl implements ICcdiCreditInfoService {
@@ -59,12 +61,12 @@ public class CcdiCreditInfoServiceImpl implements ICcdiCreditInfoService {
private CcdiCreditInfoQueryMapper queryMapper;
@Override
public CreditInfoUploadResultVO upload(List<MultipartFile> files) {
public CreditInfoUploadResultVO upload(List<MultipartFile> files, CallerContext caller) {
CreditInfoUploadResultVO result = new CreditInfoUploadResultVO();
List<CreditInfoUploadFailureVO> failures = new ArrayList<>();
int totalCount = files == null ? 0 : files.size();
int successCount = 0;
String userName = currentUserName();
String userName = caller.username();
if (files == null || files.isEmpty()) {
result.setTotalCount(0);
@@ -77,7 +79,7 @@ public class CcdiCreditInfoServiceImpl implements ICcdiCreditInfoService {
for (MultipartFile file : files) {
try {
validateHtmlFile(file);
handleSingleFile(file, userName);
handleSingleFile(file, userName, caller);
successCount++;
} catch (Exception e) {
failures.add(buildFailure(file, null, null, e.getMessage()));
@@ -148,9 +150,9 @@ public class CcdiCreditInfoServiceImpl implements ICcdiCreditInfoService {
}
}
private void handleSingleFile(MultipartFile multipartFile, String userName) throws Exception {
private void handleSingleFile(MultipartFile multipartFile, String userName, CallerContext caller) throws Exception {
CreditHtmlStorageService.StoredCreditHtml storedHtml = creditHtmlStorageService.save(multipartFile);
CreditParseInvokeResponse response = creditParseClient.parse(storedHtml.remotePath());
CreditParseInvokeResponse response = parseAndDeleteLocalFile(caller, storedHtml);
CreditParsePayload payload = requireResponse(response).getPayload();
Map<String, Object> header = requireHeader(payload);
String personId = stringValue(header.get("query_cert_no"));
@@ -164,6 +166,25 @@ public class CcdiCreditInfoServiceImpl implements ICcdiCreditInfoService {
replaceEmployeeCredit(personId, debts, negative, userName);
}
private CreditParseInvokeResponse parseAndDeleteLocalFile(
CallerContext caller, CreditHtmlStorageService.StoredCreditHtml storedHtml) throws Exception {
CreditParseInvokeResponse response;
try {
response = creditParseClient.parse(caller, storedHtml.remotePath());
} catch (Exception parseException) {
try {
creditHtmlStorageService.delete(storedHtml);
} catch (Exception deleteException) {
parseException.addSuppressed(deleteException);
log.error("征信解析失败后清理本地文件失败", deleteException);
}
throw parseException;
}
creditHtmlStorageService.delete(storedHtml);
return response;
}
private void validateHtmlFile(MultipartFile file) {
String originalFilename = file == null ? null : file.getOriginalFilename();
if (originalFilename == null) {
@@ -286,11 +307,4 @@ public class CcdiCreditInfoServiceImpl implements ICcdiCreditInfoService {
return negativeVO;
}
private String currentUserName() {
try {
return SecurityUtils.getUsername();
} catch (Exception e) {
return "system";
}
}
}

View File

@@ -8,6 +8,10 @@ import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* 征信 HTML 服务器落盘与远程访问地址生成。
@@ -16,6 +20,7 @@ import java.io.File;
public class CreditHtmlStorageService {
private static final String CREDIT_HTML_DIR = "credit-html";
private static final String CREDIT_HTML_PROFILE_PREFIX = "/profile/" + CREDIT_HTML_DIR + "/";
private static final String[] HTML_EXTENSIONS = {"html", "htm"};
@Value("${credit-parse.api.file-public-base-url}")
@@ -26,6 +31,25 @@ public class CreditHtmlStorageService {
return new StoredCreditHtml(profilePath, buildRemotePath(profilePath));
}
public void delete(StoredCreditHtml storedHtml) throws IOException {
String profilePath = storedHtml == null ? null : storedHtml.profilePath();
if (StringUtils.isBlank(profilePath) || !profilePath.startsWith(CREDIT_HTML_PROFILE_PREFIX)) {
throw new IllegalArgumentException("征信HTML本地文件路径非法");
}
String relativePath = profilePath.substring(CREDIT_HTML_PROFILE_PREFIX.length());
if (StringUtils.isBlank(relativePath)) {
throw new IllegalArgumentException("征信HTML本地文件路径非法");
}
Path baseDir = Paths.get(getCreditHtmlBaseDir()).toAbsolutePath().normalize();
Path targetFile = baseDir.resolve(relativePath).normalize();
if (!targetFile.startsWith(baseDir)) {
throw new IllegalArgumentException("征信HTML本地文件路径越界");
}
Files.deleteIfExists(targetFile);
}
private String getCreditHtmlBaseDir() {
return RuoYiConfig.getProfile() + File.separator + CREDIT_HTML_DIR;
}

View File

@@ -104,6 +104,9 @@
<if test="query.accountName != null and query.accountName != ''">
AND ai.account_name LIKE CONCAT('%', #{query.accountName}, '%')
</if>
<if test="query.accountNo != null and query.accountNo != ''">
AND ai.account_no LIKE CONCAT('%', #{query.accountNo}, '%')
</if>
<if test="query.accountType != null and query.accountType != ''">
AND ai.account_type = #{query.accountType}
</if>

View File

@@ -42,7 +42,16 @@
AND e.status = #{query.status}
</if>
</where>
ORDER BY e.create_time DESC, e.staff_id DESC
ORDER BY e.hire_date DESC, e.staff_id DESC
</select>
<select id="selectBaseStaffByIdWithDept" resultMap="CcdiBaseStaffVOResult">
SELECT
e.staff_id, e.name, e.dept_id, e.id_card, e.phone, e.annual_income, e.hire_date, e.is_party_member, e.status, e.create_time,
d.dept_name
FROM ccdi_base_staff e
LEFT JOIN sys_dept d ON e.dept_id = d.dept_id
WHERE e.staff_id = #{staffId}
</select>
<!-- 批量插入或更新员工信息只更新非null字段 -->

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

@@ -38,6 +38,18 @@ class CcdiAccountInfoMapperTest {
assertTrue(sql.contains("ai.owner_type <> 'credit_customer'"), sql);
}
@Test
void selectAccountInfoPage_shouldFilterByAccountNo() throws Exception {
MappedStatement mappedStatement = loadMappedStatement(
"com.ruoyi.info.collection.mapper.CcdiAccountInfoMapper.selectAccountInfoPage");
CcdiAccountInfoQueryDTO queryDTO = new CcdiAccountInfoQueryDTO();
queryDTO.setAccountNo("6222");
String sql = renderSql(mappedStatement, Map.of("query", queryDTO)).toLowerCase();
assertTrue(sql.contains("ai.account_no like concat('%', ?, '%')"), sql);
}
private MappedStatement loadMappedStatement(String statementId) throws Exception {
Configuration configuration = new Configuration();
configuration.setEnvironment(new Environment("test", new JdbcTransactionFactory(), new NoOpDataSource()));

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

@@ -3,6 +3,7 @@ package com.ruoyi.info.collection.service;
import com.ruoyi.info.collection.domain.CcdiAccountInfo;
import com.ruoyi.info.collection.domain.CcdiBaseStaff;
import com.ruoyi.info.collection.domain.dto.CcdiAccountInfoAddDTO;
import com.ruoyi.info.collection.domain.excel.CcdiAccountInfoExcel;
import com.ruoyi.info.collection.mapper.CcdiAccountInfoMapper;
import com.ruoyi.info.collection.mapper.CcdiBaseStaffMapper;
import com.ruoyi.info.collection.mapper.CcdiStaffFmyRelationMapper;
@@ -16,6 +17,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.BeanWrapperImpl;
import java.math.BigDecimal;
import java.util.List;
import java.util.Date;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -109,6 +111,64 @@ class CcdiAccountInfoServiceImplTest {
assertNull(wrapper.getPropertyValue("txnRiskLevel"));
}
@Test
void insertInternalIntermediaryAccount_shouldForceHighRiskLevelOnAccountInfo() {
CcdiAccountInfoAddDTO dto = buildBaseAddDto();
dto.setOwnerType("INTERMEDIARY");
dto.setOwnerId("91330100MA00000001");
dto.setBankScope("INTERNAL");
dto.setIsActualControl(1);
dto.setAvgMonthTxnCount(8);
dto.setAvgMonthTxnAmount(new BigDecimal("9988.66"));
dto.setTxnFrequencyLevel("HIGH");
dto.setDebitSingleMaxAmount(new BigDecimal("111.11"));
dto.setCreditSingleMaxAmount(new BigDecimal("222.22"));
dto.setDebitDailyMaxAmount(new BigDecimal("333.33"));
dto.setCreditDailyMaxAmount(new BigDecimal("444.44"));
dto.setTxnRiskLevel("LOW");
when(accountInfoMapper.selectCount(any())).thenReturn(0L);
when(accountInfoMapper.insert(any(CcdiAccountInfo.class))).thenReturn(1);
service.insertAccountInfo(dto);
ArgumentCaptor<CcdiAccountInfo> captor = ArgumentCaptor.forClass(CcdiAccountInfo.class);
verify(accountInfoMapper).insert(captor.capture());
BeanWrapperImpl wrapper = new BeanWrapperImpl(captor.getValue());
assertNull(wrapper.getPropertyValue("isActualControl"));
assertNull(wrapper.getPropertyValue("avgMonthTxnCount"));
assertNull(wrapper.getPropertyValue("avgMonthTxnAmount"));
assertNull(wrapper.getPropertyValue("txnFrequencyLevel"));
assertEquals("HIGH", wrapper.getPropertyValue("txnRiskLevel"));
}
@Test
void importExternalIntermediaryAccount_shouldForceHighRiskLevelOnAccountInfo() {
CcdiAccountInfoExcel excel = new CcdiAccountInfoExcel();
excel.setOwnerType("中介");
excel.setOwnerId("91330100MA00000002");
excel.setAccountName("测试中介");
excel.setAccountNo("6222024000000099");
excel.setAccountType("银行账户");
excel.setBankScope("行外");
excel.setOpenBank("中国银行");
excel.setBankCode("BOC");
excel.setCurrency("CNY");
excel.setStatus("正常");
excel.setEffectiveDate("2026-07-10");
excel.setTxnRiskLevel("LOW");
when(accountInfoMapper.selectOne(any())).thenReturn(null);
when(accountInfoMapper.selectCount(any())).thenReturn(0L);
when(accountInfoMapper.insert(any(CcdiAccountInfo.class))).thenReturn(1);
service.importAccountInfo(List.of(excel), false);
ArgumentCaptor<CcdiAccountInfo> captor = ArgumentCaptor.forClass(CcdiAccountInfo.class);
verify(accountInfoMapper).insert(captor.capture());
assertEquals("HIGH", captor.getValue().getTxnRiskLevel());
}
private CcdiAccountInfoAddDTO buildBaseAddDto() {
CcdiAccountInfoAddDTO dto = new CcdiAccountInfoAddDTO();
dto.setAccountNo("6222024000000001");

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

@@ -11,6 +11,7 @@ import com.ruoyi.info.collection.service.impl.CcdiCreditInfoServiceImpl;
import com.ruoyi.info.collection.service.support.CreditHtmlStorageService;
import com.ruoyi.info.collection.service.support.CreditInfoPayloadAssembler;
import com.ruoyi.lsfx.client.CreditParseClient;
import com.ruoyi.lsfx.domain.CallerContext;
import com.ruoyi.lsfx.domain.response.CreditParseInvokeData;
import com.ruoyi.lsfx.domain.response.CreditParseInvokeResponse;
import com.ruoyi.lsfx.domain.response.CreditParsePayload;
@@ -18,6 +19,7 @@ import com.ruoyi.lsfx.domain.response.CreditParseResponse;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.InOrder;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -34,15 +36,26 @@ import java.util.List;
import java.util.Map;
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;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CcdiCreditInfoServiceImplTest {
private static final CallerContext CALLER = CallerContext.of(7L, "tester");
private static final CreditHtmlStorageService.StoredCreditHtml STORED_HTML =
new CreditHtmlStorageService.StoredCreditHtml(
"/profile/credit-html/2026/05/12/a_1.html",
"http://127.0.0.1:62318/profile/credit-html/2026/05/12/a_1.html");
@InjectMocks
private CcdiCreditInfoServiceImpl service;
@@ -73,19 +86,23 @@ class CcdiCreditInfoServiceImplTest {
.thenReturn(new CreditHtmlStorageService.StoredCreditHtml(
"/profile/credit-html/2026/05/12/family_1.html",
"http://127.0.0.1:62318/profile/credit-html/2026/05/12/family_1.html"));
when(creditParseClient.parse(anyString()))
when(creditParseClient.parse(any(CallerContext.class), anyString()))
.thenReturn(successResponse("330101199202020022", "李四", "2026-03-24"));
when(assembler.buildDebts(anyString(), anyString(), any(LocalDate.class), any(CreditParsePayload.class)))
.thenReturn(List.of(buildDebt("330101199202020022")));
when(assembler.buildNegative(anyString(), anyString(), any(LocalDate.class), any(CreditParsePayload.class)))
.thenReturn(buildNegative("330101199202020022"));
CreditInfoUploadResultVO result = service.upload(List.of(file));
CreditInfoUploadResultVO result = service.upload(List.of(file), CALLER);
assertEquals(1, result.getSuccessCount());
assertEquals(0, result.getFailureCount());
verify(creditParseClient).parse("http://127.0.0.1:62318/profile/credit-html/2026/05/12/family_1.html");
verify(debtsInfoMapper).deleteByPersonId("330101199202020022");
InOrder processingOrder = inOrder(creditParseClient, creditHtmlStorageService, debtsInfoMapper);
processingOrder.verify(creditParseClient)
.parse(CALLER, "http://127.0.0.1:62318/profile/credit-html/2026/05/12/family_1.html");
processingOrder.verify(creditHtmlStorageService)
.delete(any(CreditHtmlStorageService.StoredCreditHtml.class));
processingOrder.verify(debtsInfoMapper).deleteByPersonId("330101199202020022");
verify(negativeInfoMapper).deleteByPersonId("330101199202020022");
}
@@ -97,15 +114,16 @@ class CcdiCreditInfoServiceImplTest {
.thenReturn(new CreditHtmlStorageService.StoredCreditHtml(
"/profile/credit-html/2026/05/12/a_1.html",
"http://127.0.0.1:62318/profile/credit-html/2026/05/12/a_1.html"));
when(creditParseClient.parse(anyString()))
when(creditParseClient.parse(any(CallerContext.class), anyString()))
.thenReturn(successResponse("330101199001010011", "张三", "2026-03-03"));
when(queryMapper.selectLatestQueryDate("330101199001010011"))
.thenReturn(LocalDate.parse("2026-03-05"));
CreditInfoUploadResultVO result = service.upload(List.of(file));
CreditInfoUploadResultVO result = service.upload(List.of(file), CALLER);
assertEquals(0, result.getSuccessCount());
assertEquals("上传征信日期早于当前已维护最新记录", result.getFailures().get(0).getReason());
verify(creditHtmlStorageService).delete(STORED_HTML);
}
@Test
@@ -118,12 +136,13 @@ class CcdiCreditInfoServiceImplTest {
"http://127.0.0.1:62318/profile/credit-html/2026/05/12/a_1.html"));
CreditParseInvokeResponse response = successResponse("330101199001010011", "张三", "2026-03-03");
response.setCode(99999);
when(creditParseClient.parse(anyString())).thenReturn(response);
when(creditParseClient.parse(any(CallerContext.class), anyString())).thenReturn(response);
CreditInfoUploadResultVO result = service.upload(List.of(file));
CreditInfoUploadResultVO result = service.upload(List.of(file), CALLER);
assertEquals(0, result.getSuccessCount());
assertEquals("征信解析平台状态码异常: 99999", result.getFailures().get(0).getReason());
verify(creditHtmlStorageService).delete(STORED_HTML);
}
@Test
@@ -138,17 +157,83 @@ class CcdiCreditInfoServiceImplTest {
response.getData().setStatus(0);
response.getData().setReasonCode(500);
response.getData().setReasonMessage("结果解析失败");
when(creditParseClient.parse(anyString())).thenReturn(response);
when(creditParseClient.parse(any(CallerContext.class), anyString())).thenReturn(response);
CreditInfoUploadResultVO result = service.upload(List.of(file));
CreditInfoUploadResultVO result = service.upload(List.of(file), CALLER);
assertEquals(0, result.getSuccessCount());
assertEquals("结果解析失败", result.getFailures().get(0).getReason());
verify(creditHtmlStorageService).delete(STORED_HTML);
}
@Test
void uploadHtmlFiles_shouldDeleteBeforeDatabaseFailure() throws Exception {
MockMultipartFile file = htmlFile("a.html");
when(creditHtmlStorageService.save(any())).thenReturn(STORED_HTML);
when(creditParseClient.parse(any(CallerContext.class), anyString()))
.thenReturn(successResponse("330101199001010011", "张三", "2026-03-03"));
when(assembler.buildDebts(anyString(), anyString(), any(LocalDate.class), any(CreditParsePayload.class)))
.thenReturn(List.of(buildDebt("330101199001010011")));
when(assembler.buildNegative(anyString(), anyString(), any(LocalDate.class), any(CreditParsePayload.class)))
.thenReturn(buildNegative("330101199001010011"));
doThrow(new RuntimeException("入库失败"))
.when(debtsInfoMapper).deleteByPersonId("330101199001010011");
CreditInfoUploadResultVO result = service.upload(List.of(file), CALLER);
assertEquals(0, result.getSuccessCount());
assertEquals("入库失败", result.getFailures().get(0).getReason());
InOrder processingOrder = inOrder(creditHtmlStorageService, debtsInfoMapper);
processingOrder.verify(creditHtmlStorageService).delete(STORED_HTML);
processingOrder.verify(debtsInfoMapper).deleteByPersonId("330101199001010011");
}
@Test
void uploadHtmlFiles_shouldFailBeforeValidationWhenDeleteFails() throws Exception {
MockMultipartFile file = htmlFile("a.html");
when(creditHtmlStorageService.save(any())).thenReturn(STORED_HTML);
when(creditParseClient.parse(any(CallerContext.class), anyString()))
.thenReturn(successResponse("330101199001010011", "张三", "2026-03-03"));
doThrow(new java.io.IOException("本地文件删除失败"))
.when(creditHtmlStorageService).delete(STORED_HTML);
CreditInfoUploadResultVO result = service.upload(List.of(file), CALLER);
assertEquals(0, result.getSuccessCount());
assertEquals("本地文件删除失败", result.getFailures().get(0).getReason());
verifyNoInteractions(assembler, debtsInfoMapper, negativeInfoMapper, queryMapper);
}
@Test
void uploadHtmlFiles_shouldPreserveParseErrorAndContinueWhenDeleteAlsoFails() throws Exception {
MockMultipartFile failedFile = htmlFile("failed.html");
MockMultipartFile successFile = htmlFile("success.html");
CreditHtmlStorageService.StoredCreditHtml failedStoredHtml = storedHtml("failed_1.html");
CreditHtmlStorageService.StoredCreditHtml successStoredHtml = storedHtml("success_1.html");
when(creditHtmlStorageService.save(any())).thenReturn(failedStoredHtml, successStoredHtml);
when(creditParseClient.parse(any(CallerContext.class), anyString()))
.thenThrow(new RuntimeException("解析接口异常"))
.thenReturn(successResponse("330101199001010011", "张三", "2026-03-03"));
doThrow(new java.io.IOException("本地文件删除失败"))
.doNothing()
.when(creditHtmlStorageService).delete(any(CreditHtmlStorageService.StoredCreditHtml.class));
when(assembler.buildDebts(anyString(), anyString(), any(LocalDate.class), any(CreditParsePayload.class)))
.thenReturn(List.of());
when(assembler.buildNegative(anyString(), anyString(), any(LocalDate.class), any(CreditParsePayload.class)))
.thenReturn(null);
CreditInfoUploadResultVO result = service.upload(List.of(failedFile, successFile), CALLER);
assertEquals(1, result.getSuccessCount());
assertEquals(1, result.getFailureCount());
assertEquals("解析接口异常", result.getFailures().get(0).getReason());
verify(creditHtmlStorageService).delete(failedStoredHtml);
verify(creditHtmlStorageService).delete(successStoredHtml);
}
@Test
void creditHtmlStorage_shouldStoreHtmlUnderProfileAndBuildRemotePath(@TempDir Path profileDir) throws Exception {
void creditHtmlStorage_shouldStoreAndDeleteHtmlWithinProfile(@TempDir Path profileDir) throws Exception {
String oldProfile = RuoYiConfig.getProfile();
new RuoYiConfig().setProfile(profileDir.toString());
try {
@@ -164,11 +249,42 @@ class CcdiCreditInfoServiceImplTest {
assertEquals("http://127.0.0.1:62318" + storedHtml.profilePath(), storedHtml.remotePath());
Path savedFile = profileDir.resolve(storedHtml.profilePath().substring("/profile/".length()));
assertTrue(Files.exists(savedFile));
storageService.delete(storedHtml);
assertFalse(Files.exists(savedFile));
storageService.delete(storedHtml);
} finally {
new RuoYiConfig().setProfile(oldProfile);
}
}
@Test
void creditHtmlStorage_shouldRejectPathOutsideCreditHtmlDirectory(@TempDir Path profileDir) throws Exception {
String oldProfile = RuoYiConfig.getProfile();
new RuoYiConfig().setProfile(profileDir.toString());
try {
CreditHtmlStorageService storageService = new CreditHtmlStorageService();
Path outsideFile = Files.writeString(profileDir.resolve("outside.html"), "outside");
CreditHtmlStorageService.StoredCreditHtml storedHtml = new CreditHtmlStorageService.StoredCreditHtml(
"/profile/credit-html/../outside.html", "http://127.0.0.1/profile/credit-html/../outside.html");
assertThrows(IllegalArgumentException.class, () -> storageService.delete(storedHtml));
assertTrue(Files.exists(outsideFile));
} finally {
new RuoYiConfig().setProfile(oldProfile);
}
}
private MockMultipartFile htmlFile(String fileName) {
return new MockMultipartFile(
"files", fileName, "text/html", "<html>ok</html>".getBytes(StandardCharsets.UTF_8));
}
private CreditHtmlStorageService.StoredCreditHtml storedHtml(String fileName) {
return new CreditHtmlStorageService.StoredCreditHtml(
"/profile/credit-html/2026/05/12/" + fileName,
"http://127.0.0.1:62318/profile/credit-html/2026/05/12/" + fileName);
}
private CreditParseInvokeResponse successResponse(String personId, String personName, String reportTime) {
CreditParsePayload payload = new CreditParsePayload();
Map<String, Object> header = new HashMap<>();

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

@@ -20,6 +20,12 @@
<artifactId>ruoyi-common</artifactId>
</dependency>
<!-- 系统日志持久化 -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-system</artifactId>
</dependency>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@@ -3,6 +3,7 @@ package com.ruoyi.lsfx.client;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.uuid.IdUtils;
import com.ruoyi.lsfx.domain.CallerContext;
import com.ruoyi.lsfx.domain.response.CreditParseInvokeResponse;
import com.ruoyi.lsfx.exception.LsfxApiException;
import com.ruoyi.lsfx.util.HttpUtil;
@@ -45,20 +46,20 @@ public class CreditParseClient {
@Value("${credit-parse.api.model:LXCUSTALL}")
private String defaultModel;
public CreditParseInvokeResponse parse(String remotePath) {
return parse(defaultModel, remotePath);
public CreditParseInvokeResponse parse(CallerContext caller, String remotePath) {
return parse(caller, defaultModel, remotePath);
}
public CreditParseInvokeResponse parse(String model, String remotePath) {
public CreditParseInvokeResponse parse(CallerContext caller, String model, String remotePath) {
long startTime = System.currentTimeMillis();
String actualModel = StringUtils.isBlank(model) ? defaultModel : model;
String serialNum = buildSerialNum();
try {
Map<String, Object> initiateParams = buildInitiateParams(serialNum, actualModel, remotePath);
CreditParseInvokeResponse initiateResponse = request(creditParseUrl, initiateParams, "发起接口");
CreditParseInvokeResponse initiateResponse = request(caller, creditParseUrl, initiateParams, "发起接口");
requireSuccessfulInitiateResponse(initiateResponse, "征信解析发起接口");
CreditParseInvokeResponse response = queryResult(serialNum);
CreditParseInvokeResponse response = queryResult(caller, serialNum);
long elapsed = System.currentTimeMillis() - startTime;
log.info("【征信解析】调用完成: success={}, code={}, businessStatusCode={}, cost={}ms",
@@ -94,10 +95,10 @@ public class CreditParseClient {
return params;
}
private CreditParseInvokeResponse queryResult(String serialNum) {
private CreditParseInvokeResponse queryResult(CallerContext caller, String serialNum) {
Map<String, Object> params = buildBaseParams(serialNum);
for (int attempt = 1; attempt <= RESULT_QUERY_MAX_ATTEMPTS; attempt++) {
CreditParseInvokeResponse response = request(creditParseResultUrl, params,
CreditParseInvokeResponse response = request(caller, creditParseResultUrl, params,
"结果接口第" + attempt + "次查询");
requireSuccessfulServiceResponse(response, "征信解析结果接口");
if (response.getData() == null || response.getData().getMappingOutputFields() == null) {
@@ -130,10 +131,10 @@ public class CreditParseClient {
Thread.sleep(intervalMillis);
}
private CreditParseInvokeResponse request(String url, Map<String, Object> params, String stage) {
private CreditParseInvokeResponse request(CallerContext caller, String url, Map<String, Object> params, String stage) {
try {
log.info("【征信解析】{}请求: url={}, params={}", stage, url, toJson(params));
String responseJson = httpUtil.postUrlEncodedFormForString(url, params, null);
String responseJson = httpUtil.postUrlEncodedFormForString(caller, url, params, null);
log.info("【征信解析】{}返回JSON: {}", stage, responseJson);
return objectMapper.readValue(responseJson, CreditParseInvokeResponse.class);
} catch (LsfxApiException e) {

View File

@@ -1,6 +1,7 @@
package com.ruoyi.lsfx.client;
import com.ruoyi.lsfx.constants.LsfxConstants;
import com.ruoyi.lsfx.domain.CallerContext;
import com.ruoyi.lsfx.domain.request.DeleteFilesRequest;
import com.ruoyi.lsfx.domain.request.FetchInnerFlowRequest;
import com.ruoyi.lsfx.domain.request.GetBankStatementRequest;
@@ -68,7 +69,7 @@ public class LsfxAnalysisClient {
/**
* 获取Token
*/
public GetTokenResponse getToken(GetTokenRequest request) {
public GetTokenResponse getToken(CallerContext caller, GetTokenRequest request) {
log.info("【流水分析】获取Token请求: projectNo={}, entityName={}", request.getProjectNo(), request.getEntityName());
long startTime = System.currentTimeMillis();
@@ -88,7 +89,7 @@ public class LsfxAnalysisClient {
params.put("analysisType", request.getAnalysisType() != null ? request.getAnalysisType() : LsfxConstants.ANALYSIS_TYPE);
String url = baseUrl + getTokenEndpoint;
GetTokenResponse response = httpUtil.postFormData(url, params, null, GetTokenResponse.class);
GetTokenResponse response = httpUtil.postFormData(caller, url, params, null, GetTokenResponse.class);
long elapsed = System.currentTimeMillis() - startTime;
if (response != null && response.getData() != null) {
@@ -110,14 +111,14 @@ public class LsfxAnalysisClient {
/**
* 上传文件
*/
public UploadFileResponse uploadFile(Integer groupId, File file) {
return uploadFile(groupId, file, file.getName());
public UploadFileResponse uploadFile(CallerContext caller, Integer groupId, File file) {
return uploadFile(caller, groupId, file, file.getName());
}
/**
* 上传文件
*/
public UploadFileResponse uploadFile(Integer groupId, File file, String uploadFileName) {
public UploadFileResponse uploadFile(CallerContext caller, Integer groupId, File file, String uploadFileName) {
String multipartFileName = StringUtils.hasText(uploadFileName) ? uploadFileName : file.getName();
log.info("【流水分析】上传文件请求: groupId={}, fileName={}", groupId, multipartFileName);
long startTime = System.currentTimeMillis();
@@ -132,7 +133,7 @@ public class LsfxAnalysisClient {
Map<String, String> headers = new HashMap<>();
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
UploadFileResponse response = httpUtil.uploadFile(url, params, headers, UploadFileResponse.class);
UploadFileResponse response = httpUtil.uploadFile(caller, url, params, headers, UploadFileResponse.class);
long elapsed = System.currentTimeMillis() - startTime;
if (response != null && response.getData() != null) {
@@ -155,7 +156,7 @@ public class LsfxAnalysisClient {
/**
* 拉取行内流水
*/
public FetchInnerFlowResponse fetchInnerFlow(FetchInnerFlowRequest request) {
public FetchInnerFlowResponse fetchInnerFlow(CallerContext caller, FetchInnerFlowRequest request) {
log.info("【流水分析】拉取行内流水请求: groupId={}, customerNo={}", request.getGroupId(), request.getCustomerNo());
long startTime = System.currentTimeMillis();
@@ -168,7 +169,7 @@ public class LsfxAnalysisClient {
Map<String, String> headers = new HashMap<>();
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
FetchInnerFlowResponse response = httpUtil.postFormData(url, params, headers, FetchInnerFlowResponse.class);
FetchInnerFlowResponse response = httpUtil.postFormData(caller, url, params, headers, FetchInnerFlowResponse.class);
long elapsed = System.currentTimeMillis() - startTime;
if (response != null && response.getData() != null) {
@@ -190,7 +191,7 @@ public class LsfxAnalysisClient {
/**
* 检查文件解析状态
*/
public CheckParseStatusResponse checkParseStatus(Integer groupId, String inprogressList) {
public CheckParseStatusResponse checkParseStatus(CallerContext caller, Integer groupId, String inprogressList) {
log.info("【流水分析】检查文件解析状态: groupId={}, inprogressList={}", groupId, inprogressList);
long startTime = System.currentTimeMillis();
@@ -205,7 +206,7 @@ public class LsfxAnalysisClient {
Map<String, String> headers = new HashMap<>();
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
CheckParseStatusResponse response = httpUtil.postFormData(url, params, headers, CheckParseStatusResponse.class);
CheckParseStatusResponse response = httpUtil.postFormData(caller, url, params, headers, CheckParseStatusResponse.class);
long elapsed = System.currentTimeMillis() - startTime;
if (response != null && response.getData() != null) {
@@ -234,7 +235,7 @@ public class LsfxAnalysisClient {
* @param request 请求参数(groupId, logId, pageNow, pageSize)
* @return 流水明细列表
*/
public GetBankStatementResponse getBankStatement(GetBankStatementRequest request) {
public GetBankStatementResponse getBankStatement(CallerContext caller, GetBankStatementRequest request) {
log.info("【流水分析】获取银行流水请求: groupId={}, logId={}, pageNow={}, pageSize={}",
request.getGroupId(), request.getLogId(), request.getPageNow(), request.getPageSize());
long startTime = System.currentTimeMillis();
@@ -248,7 +249,7 @@ public class LsfxAnalysisClient {
Map<String, String> headers = new HashMap<>();
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
GetBankStatementResponse response = httpUtil.postFormData(url, params, headers, GetBankStatementResponse.class);
GetBankStatementResponse response = httpUtil.postFormData(caller, url, params, headers, GetBankStatementResponse.class);
long elapsed = System.currentTimeMillis() - startTime;
if (response != null && response.getData() != null) {
@@ -281,7 +282,7 @@ public class LsfxAnalysisClient {
* @param request 请求参数(groupId必填, logId可选)
* @return 文件上传状态信息
*/
public GetFileUploadStatusResponse getFileUploadStatus(GetFileUploadStatusRequest request) {
public GetFileUploadStatusResponse getFileUploadStatus(CallerContext caller, GetFileUploadStatusRequest request) {
log.info("【流水分析】获取文件上传状态: groupId={}, logId={}",
request.getGroupId(), request.getLogId());
long startTime = System.currentTimeMillis();
@@ -299,7 +300,7 @@ public class LsfxAnalysisClient {
Map<String, String> headers = new HashMap<>();
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
GetFileUploadStatusResponse response = httpUtil.get(url, params, headers,
GetFileUploadStatusResponse response = httpUtil.get(caller, url, params, headers,
GetFileUploadStatusResponse.class);
long elapsed = System.currentTimeMillis() - startTime;
@@ -334,7 +335,7 @@ public class LsfxAnalysisClient {
* @param request 请求参数(groupId, logIds, userId必填)
* @return 删除结果
*/
public DeleteFilesResponse deleteFiles(DeleteFilesRequest request) {
public DeleteFilesResponse deleteFiles(CallerContext caller, DeleteFilesRequest request) {
log.info("【流水分析】删除文件请求: groupId={}, logIds={}, userId={}",
request.getGroupId(), Arrays.toString(request.getLogIds()), request.getUserId());
long startTime = System.currentTimeMillis();
@@ -351,7 +352,7 @@ public class LsfxAnalysisClient {
Map<String, String> headers = new HashMap<>();
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
DeleteFilesResponse response = httpUtil.postFormData(url, params, headers,
DeleteFilesResponse response = httpUtil.postFormData(caller, url, params, headers,
DeleteFilesResponse.class);
long elapsed = System.currentTimeMillis() - startTime;

View File

@@ -17,6 +17,7 @@ public class LsfxConstants {
/** 数据渠道编码 */
public static final String DATA_CHANNEL_ZJRCU = "ZJRCU";
public static final String DATA_CHANNEL_JZL = "JZL";
/** 分析类型 */
public static final String ANALYSIS_TYPE = "-1";

View File

@@ -1,22 +1,24 @@
package com.ruoyi.lsfx.controller;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.lsfx.client.CreditParseClient;
import com.ruoyi.lsfx.domain.CallerContext;
import com.ruoyi.lsfx.domain.response.CreditParseInvokeResponse;
import com.ruoyi.lsfx.exception.LsfxApiException;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "征信解析接口测试", description = "用于测试征信解析接口")
@Anonymous
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
@RestController
@RequestMapping("/lsfx/credit")
public class CreditParseController {
@@ -37,7 +39,8 @@ public class CreditParseController {
String actualModel = StringUtils.isBlank(model) ? DEFAULT_MODEL : model;
try {
CreditParseInvokeResponse response = creditParseClient.parse(actualModel, remotePath);
CreditParseInvokeResponse response = creditParseClient.parse(
CallerContext.from(SecurityUtils.getLoginUser()), actualModel, remotePath);
return AjaxResult.success(response);
} catch (LsfxApiException e) {
return AjaxResult.error(e.getMessage());

View File

@@ -1,10 +1,11 @@
package com.ruoyi.lsfx.controller;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.lsfx.client.LsfxAnalysisClient;
import com.ruoyi.lsfx.constants.LsfxConstants;
import com.ruoyi.lsfx.domain.CallerContext;
import com.ruoyi.lsfx.domain.request.DeleteFilesRequest;
import com.ruoyi.lsfx.domain.request.FetchInnerFlowRequest;
import com.ruoyi.lsfx.domain.request.GetBankStatementRequest;
@@ -15,6 +16,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@@ -28,7 +30,7 @@ import java.nio.file.StandardCopyOption;
* 流水分析平台接口测试控制器
*/
@Tag(name = "流水分析平台接口测试", description = "用于测试流水分析平台的7个接口")
@Anonymous
@PreAuthorize("@ss.hasPermi('ccdi:project:edit')")
@RestController
@RequestMapping("/lsfx/test")
public class LsfxTestController {
@@ -61,7 +63,7 @@ public class LsfxTestController {
request.setDepartmentCode(LsfxConstants.DEFAULT_DEPARTMENT_CODE);
}
GetTokenResponse response = lsfxAnalysisClient.getToken(request);
GetTokenResponse response = lsfxAnalysisClient.getToken(currentCaller(), request);
return AjaxResult.success(response);
}
@@ -90,7 +92,7 @@ public class LsfxTestController {
Files.copy(file.getInputStream(), tempFile, StandardCopyOption.REPLACE_EXISTING);
File convertedFile = tempFile.toFile();
UploadFileResponse response = lsfxAnalysisClient.uploadFile(groupId, convertedFile);
UploadFileResponse response = lsfxAnalysisClient.uploadFile(currentCaller(), groupId, convertedFile);
return AjaxResult.success(response);
} catch (IOException e) {
return AjaxResult.error("文件转换失败:" + e.getMessage());
@@ -134,7 +136,7 @@ public class LsfxTestController {
request.setDataChannelCode(LsfxConstants.DEFAULT_DATA_CHANNEL_CODE);
}
FetchInnerFlowResponse response = lsfxAnalysisClient.fetchInnerFlow(request);
FetchInnerFlowResponse response = lsfxAnalysisClient.fetchInnerFlow(currentCaller(), request);
return AjaxResult.success(response);
}
@@ -152,7 +154,7 @@ public class LsfxTestController {
return AjaxResult.error("参数不完整inprogressList为必填");
}
CheckParseStatusResponse response = lsfxAnalysisClient.checkParseStatus(groupId, inprogressList);
CheckParseStatusResponse response = lsfxAnalysisClient.checkParseStatus(currentCaller(), groupId, inprogressList);
return AjaxResult.success(response);
}
@@ -174,7 +176,7 @@ public class LsfxTestController {
return AjaxResult.error("参数不完整pageSize为必填且大于0");
}
GetBankStatementResponse response = lsfxAnalysisClient.getBankStatement(request);
GetBankStatementResponse response = lsfxAnalysisClient.getBankStatement(currentCaller(), request);
return AjaxResult.success(response);
}
@@ -194,7 +196,7 @@ public class LsfxTestController {
request.setGroupId(groupId);
request.setLogId(logId);
GetFileUploadStatusResponse response = lsfxAnalysisClient.getFileUploadStatus(request);
GetFileUploadStatusResponse response = lsfxAnalysisClient.getFileUploadStatus(currentCaller(), request);
return AjaxResult.success(response);
}
@@ -213,7 +215,11 @@ public class LsfxTestController {
return AjaxResult.error("参数不完整userId为必填");
}
DeleteFilesResponse response = lsfxAnalysisClient.deleteFiles(request);
DeleteFilesResponse response = lsfxAnalysisClient.deleteFiles(currentCaller(), request);
return AjaxResult.success(response);
}
private CallerContext currentCaller() {
return CallerContext.from(SecurityUtils.getLoginUser());
}
}

View File

@@ -0,0 +1,31 @@
package com.ruoyi.lsfx.domain;
import com.ruoyi.common.core.domain.model.LoginUser;
import org.springframework.util.StringUtils;
/**
* 业务外部接口调用发起人快照。
*/
public record CallerContext(Long userId, String username) {
public CallerContext {
if (!StringUtils.hasText(username)) {
throw new IllegalArgumentException("接口调用账号不能为空");
}
}
public static CallerContext from(LoginUser loginUser) {
if (loginUser == null) {
throw new IllegalArgumentException("登录用户不能为空");
}
return new CallerContext(loginUser.getUserId(), loginUser.getUsername());
}
public static CallerContext of(Long userId, String username) {
return new CallerContext(userId, username);
}
public static CallerContext system() {
return new CallerContext(null, "system");
}
}

View File

@@ -14,7 +14,7 @@ public class FetchInnerFlowRequest {
/** 客户身份证号 */
private String customerNo;
/** 数据渠道编码(固定值:ZJRCU) */
/** 数据渠道编码(ZJRCU-行内JZL-金综) */
private String dataChannelCode;
/** 发起请求的时间(格式:yyyyMMdd) */

View File

@@ -51,7 +51,7 @@ public class GetBankStatementResponse {
private String leName;
/** 企业银行账号 */
private String accountMaskNo;
private String accountNo;
/** 账号日期ID */
private Integer accountingDateId;
@@ -102,7 +102,7 @@ public class GetBankStatementResponse {
private String customerName;
/** 对手方账号 */
private String customerAccountMaskNo;
private String customerAccountNo;
/** 对手方银行 */
private String customerBank;

View File

@@ -1,30 +1,48 @@
package com.ruoyi.lsfx.util;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.lsfx.domain.CallerContext;
import com.ruoyi.lsfx.exception.LsfxApiException;
import com.ruoyi.system.domain.SysApiLog;
import com.ruoyi.system.service.ISysApiLogService;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.*;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* HTTP请求工具
* 业务外部HTTP请求工具,统一保存完整调用日志。
*/
@Component
public class HttpUtil {
private static final Logger log = LoggerFactory.getLogger(HttpUtil.class);
private static final String CALL_SUCCESS = "0";
private static final String CALL_FAILED = "1";
@Resource
private RestTemplate restTemplate;
@@ -32,6 +50,9 @@ public class HttpUtil {
@Resource
private ObjectMapper objectMapper;
@Resource
private ISysApiLogService apiLogService;
public static org.springframework.core.io.Resource namedFileResource(File file, String filename) {
return new NamedFileSystemResource(file, filename);
}
@@ -50,299 +71,245 @@ public class HttpUtil {
}
}
/**
* 发送GET请求带查询参数和请求头
* @param url 请求URL
* @param params 查询参数
* @param headers 请求头
* @param responseType 响应类型
* @return 响应对象
*/
public <T> T get(String url, Map<String, Object> params, Map<String, String> headers, Class<T> responseType) {
public <T> T get(CallerContext caller, String url, Map<String, Object> params,
Map<String, String> headers, Class<T> responseType) {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
if (params != null) {
params.forEach((key, value) -> {
if (value != null) {
builder.queryParam(key, value);
}
});
}
String fullUrl = builder.toUriString();
HttpHeaders httpHeaders = createHeaders(headers);
return execute(caller, fullUrl, HttpMethod.GET, httpHeaders, params,
new HttpEntity<>(httpHeaders), responseType, "API返回数据为空", "GET请求异常");
}
public <T> T get(CallerContext caller, String url, Map<String, String> headers, Class<T> responseType) {
HttpHeaders httpHeaders = createHeaders(headers);
return execute(caller, url, HttpMethod.GET, httpHeaders, null,
new HttpEntity<>(httpHeaders), responseType, "API返回数据为空", "网络请求失败");
}
public <T> T postJson(CallerContext caller, String url, Object request,
Map<String, String> headers, Class<T> responseType) {
HttpHeaders httpHeaders = createHeaders(headers);
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
return execute(caller, url, HttpMethod.POST, httpHeaders, request,
new HttpEntity<>(request, httpHeaders), responseType, "API返回数据为空", "网络请求失败");
}
public <T> T postFormData(CallerContext caller, String url, Map<String, Object> params,
Map<String, String> headers, Class<T> responseType) {
HttpHeaders httpHeaders = createHeaders(headers);
httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
if (params != null) {
params.forEach(body::add);
}
return execute(caller, url, HttpMethod.POST, httpHeaders, params,
new HttpEntity<>(body, httpHeaders), responseType, "API返回数据为空", "网络请求失败");
}
public <T> T postUrlEncodedForm(CallerContext caller, String url, Map<String, Object> params,
Map<String, String> headers, Class<T> responseType) {
HttpHeaders httpHeaders = createHeaders(headers);
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> body = toUrlEncodedBody(params);
return execute(caller, url, HttpMethod.POST, httpHeaders, params,
new HttpEntity<>(body, httpHeaders), responseType, "API返回数据为空", "网络请求失败");
}
public String postUrlEncodedFormForString(CallerContext caller, String url,
Map<String, Object> params, Map<String, String> headers) {
HttpHeaders httpHeaders = createHeaders(headers);
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> body = toUrlEncodedBody(params);
return execute(caller, url, HttpMethod.POST, httpHeaders, params,
new HttpEntity<>(body, httpHeaders), String.class, "API返回数据为空", "网络请求失败");
}
public <T> T uploadFile(CallerContext caller, String url, Map<String, Object> params,
Map<String, String> headers, Class<T> responseType) {
HttpHeaders httpHeaders = createHeaders(headers);
httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
if (params != null) {
params.forEach((key, value) -> {
if (value instanceof File file) {
body.add(key, new FileSystemResource(file));
} else {
body.add(key, value);
}
});
}
return execute(caller, url, HttpMethod.POST, httpHeaders, params,
new HttpEntity<>(body, httpHeaders), responseType, "文件上传返回数据为空", "文件上传请求失败");
}
private <T> T execute(CallerContext caller, String url, HttpMethod method, HttpHeaders requestHeaders,
Object requestParams, HttpEntity<?> requestEntity, Class<T> responseType,
String emptyResponseMessage, String requestFailureMessage) {
if (caller == null) {
throw new IllegalArgumentException("接口调用用户不能为空");
}
long startTime = System.currentTimeMillis();
SysApiLog apiLog = createBaseApiLog(caller, url, method, startTime);
try {
// 构建URL with查询参数
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
if (params != null && !params.isEmpty()) {
params.forEach((key, value) -> {
if (value != null) {
builder.queryParam(key, value);
}
});
}
String fullUrl = builder.toUriString();
log.debug("【HTTP GET】请求URL: {}", fullUrl);
// 创建请求头
HttpHeaders httpHeaders = new HttpHeaders();
if (headers != null) {
headers.forEach(httpHeaders::add);
}
// 构建请求实体
HttpEntity<String> entity = new HttpEntity<>(httpHeaders);
// 执行GET请求
ResponseEntity<String> response = restTemplate.exchange(
fullUrl,
HttpMethod.GET,
entity,
String.class
);
log.debug("【HTTP GET】响应状态: {}", response.getStatusCode());
log.debug("【HTTP GET】响应内容: {}", response.getBody());
// 解析响应
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
return objectMapper.readValue(response.getBody(), responseType);
} else {
throw new LsfxApiException("GET请求失败: " + response.getStatusCode());
}
captureRequest(apiLog, requestHeaders, requestParams);
} catch (Exception e) {
log.error("【HTTP GET】请求异常: url={}, error={}", url, e.getMessage(), e);
throw new LsfxApiException("GET请求异常: " + e.getMessage(), e);
log.error("接口日志请求快照构建失败: method={}, url={}", method, url, e);
}
}
/**
* 发送GET请求带请求头
* @param url 请求URL
* @param headers 请求头
* @param responseType 响应类型
* @return 响应对象
*/
public <T> T get(String url, Map<String, String> headers, Class<T> responseType) {
try {
HttpHeaders httpHeaders = createHeaders(headers);
HttpEntity<Void> requestEntity = new HttpEntity<>(httpHeaders);
ResponseEntity<T> response = restTemplate.exchange(
url, HttpMethod.GET, requestEntity, responseType
);
ResponseEntity<String> response = restTemplate.exchange(url, method, requestEntity, String.class);
captureResponse(apiLog, response);
if (!response.getStatusCode().is2xxSuccessful()) {
throw new LsfxApiException("API调用失败HTTP状态码: " + response.getStatusCode());
throw new LsfxApiException("API调用失败HTTP状态码: " + response.getStatusCode().value());
}
if (!StringUtils.hasText(response.getBody())) {
throw new LsfxApiException(emptyResponseMessage);
}
T body = response.getBody();
if (body == null) {
throw new LsfxApiException("API返回数据为空");
}
return body;
T result = parseResponse(response.getBody(), responseType);
apiLog.setCallStatus(CALL_SUCCESS);
return result;
} catch (RestClientResponseException e) {
apiLog.setResponseStatus(e.getStatusCode().value());
apiLog.setResponseHeaders(safeSerialize(e.getResponseHeaders()));
apiLog.setResponseBody(e.getResponseBodyAsString());
apiLog.setErrorMsg(formatException(e));
throw new LsfxApiException(requestFailureMessage + ": " + e.getMessage(), e);
} catch (LsfxApiException e) {
apiLog.setErrorMsg(formatException(e));
throw e;
} catch (RestClientException e) {
throw new LsfxApiException("网络请求失败: " + e.getMessage(), e);
apiLog.setErrorMsg(formatException(e));
throw new LsfxApiException(requestFailureMessage + ": " + e.getMessage(), e);
} catch (Exception e) {
apiLog.setErrorMsg(formatException(e));
throw new LsfxApiException("响应解析失败: " + e.getMessage(), e);
} finally {
apiLog.setCostTime(System.currentTimeMillis() - startTime);
persistApiLog(apiLog);
}
}
/**
* 发送POST请求JSON格式带请求头
* @param url 请求URL
* @param request 请求对象
* @param headers 请求头
* @param responseType 响应类型
* @return 响应对象
*/
public <T> T postJson(String url, Object request, Map<String, String> headers, Class<T> responseType) {
try {
HttpHeaders httpHeaders = createHeaders(headers);
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Object> requestEntity = new HttpEntity<>(request, httpHeaders);
ResponseEntity<T> response = restTemplate.postForEntity(url, requestEntity, responseType);
if (!response.getStatusCode().is2xxSuccessful()) {
throw new LsfxApiException("API调用失败HTTP状态码: " + response.getStatusCode());
}
T body = response.getBody();
if (body == null) {
throw new LsfxApiException("API返回数据为空");
}
return body;
} catch (RestClientException e) {
throw new LsfxApiException("网络请求失败: " + e.getMessage(), e);
}
private SysApiLog createBaseApiLog(CallerContext caller, String url, HttpMethod method, long startTime) {
SysApiLog apiLog = new SysApiLog();
apiLog.setCallerUserId(caller.userId());
apiLog.setCallerUsername(caller.username());
apiLog.setApiUrl(url);
apiLog.setHttpMethod(method.name());
apiLog.setCallStatus(CALL_FAILED);
apiLog.setCallTime(new Date(startTime));
return apiLog;
}
/**
* 发送POST请求multipart/form-data格式带请求头
* 用于提交表单数据(非文件上传场景)
* @param url 请求URL
* @param params 表单参数
* @param headers 请求头
* @param responseType 响应类型
* @return 响应对象
*/
public <T> T postFormData(String url, Map<String, Object> params, Map<String, String> headers, Class<T> responseType) {
try {
HttpHeaders httpHeaders = createHeaders(headers);
httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
if (params != null) {
params.forEach(body::add);
}
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, httpHeaders);
ResponseEntity<T> response = restTemplate.postForEntity(url, requestEntity, responseType);
if (!response.getStatusCode().is2xxSuccessful()) {
throw new LsfxApiException("API调用失败HTTP状态码: " + response.getStatusCode());
}
T responseBody = response.getBody();
if (responseBody == null) {
throw new LsfxApiException("API返回数据为空");
}
return responseBody;
} catch (RestClientException e) {
throw new LsfxApiException("网络请求失败: " + e.getMessage(), e);
}
private void captureRequest(SysApiLog apiLog, HttpHeaders headers, Object params) {
apiLog.setContentType(headers.getContentType() == null ? null : headers.getContentType().toString());
apiLog.setRequestHeaders(safeSerialize(headers));
apiLog.setRequestParams(safeSerialize(normalizeValue(params)));
}
/**
* 发送POST请求application/x-www-form-urlencoded格式带请求头
* @param url 请求URL
* @param params 表单参数
* @param headers 请求头
* @param responseType 响应类型
* @return 响应对象
*/
public <T> T postUrlEncodedForm(String url, Map<String, Object> params, Map<String, String> headers, Class<T> responseType) {
try {
HttpHeaders httpHeaders = createHeaders(headers);
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
if (params != null) {
params.forEach((key, value) -> {
if (value != null) {
body.add(key, value.toString());
}
});
}
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, httpHeaders);
ResponseEntity<T> response = restTemplate.postForEntity(url, requestEntity, responseType);
if (!response.getStatusCode().is2xxSuccessful()) {
throw new LsfxApiException("API调用失败HTTP状态码: " + response.getStatusCode());
}
T responseBody = response.getBody();
if (responseBody == null) {
throw new LsfxApiException("API返回数据为空");
}
return responseBody;
} catch (RestClientException e) {
throw new LsfxApiException("网络请求失败: " + e.getMessage(), e);
}
private void captureResponse(SysApiLog apiLog, ResponseEntity<String> response) {
apiLog.setResponseStatus(response.getStatusCode().value());
apiLog.setResponseHeaders(safeSerialize(response.getHeaders()));
apiLog.setResponseBody(response.getBody());
}
/**
* 发送POST请求application/x-www-form-urlencoded格式并返回原始JSON字符串
* @param url 请求URL
* @param params 表单参数
* @param headers 请求头
* @return 原始响应内容
*/
public String postUrlEncodedFormForString(String url, Map<String, Object> params, Map<String, String> headers) {
try {
HttpHeaders httpHeaders = createHeaders(headers);
httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
if (params != null) {
params.forEach((key, value) -> {
if (value != null) {
body.add(key, value.toString());
}
});
}
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, httpHeaders);
ResponseEntity<String> response = restTemplate.postForEntity(url, requestEntity, String.class);
if (!response.getStatusCode().is2xxSuccessful()) {
throw new LsfxApiException("API调用失败HTTP状态码: " + response.getStatusCode());
}
String responseBody = response.getBody();
if (responseBody == null) {
throw new LsfxApiException("API返回数据为空");
}
return responseBody;
} catch (RestClientException e) {
throw new LsfxApiException("网络请求失败: " + e.getMessage(), e);
@SuppressWarnings("unchecked")
private <T> T parseResponse(String responseBody, Class<T> responseType) throws Exception {
if (String.class.equals(responseType)) {
return (T) responseBody;
}
return objectMapper.readValue(responseBody, responseType);
}
/**
* 上传文件Multipart格式
* @param url 请求URL
* @param params 参数(包含文件)
* @param headers 请求头
* @param responseType 响应类型
* @return 响应对象
*/
public <T> T uploadFile(String url, Map<String, Object> params, Map<String, String> headers, Class<T> responseType) {
try {
HttpHeaders httpHeaders = createHeaders(headers);
httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
if (params != null) {
params.forEach((key, value) -> {
// 如果是File对象包装为FileSystemResource
if (value instanceof File) {
File file = (File) value;
body.add(key, new FileSystemResource(file));
} else if (value instanceof org.springframework.core.io.Resource) {
body.add(key, value);
} else {
body.add(key, value);
}
});
}
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, httpHeaders);
ResponseEntity<T> response = restTemplate.postForEntity(url, requestEntity, responseType);
if (!response.getStatusCode().is2xxSuccessful()) {
throw new LsfxApiException("文件上传失败HTTP状态码: " + response.getStatusCode());
}
T responseBody = response.getBody();
if (responseBody == null) {
throw new LsfxApiException("文件上传返回数据为空");
}
return responseBody;
} catch (RestClientException e) {
throw new LsfxApiException("文件上传请求失败: " + e.getMessage(), e);
private MultiValueMap<String, String> toUrlEncodedBody(Map<String, Object> params) {
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
if (params != null) {
params.forEach((key, value) -> {
if (value != null) {
body.add(key, value.toString());
}
});
}
return body;
}
/**
* 创建请求头
* @param headers 请求头Map
* @return HttpHeaders对象
*/
private HttpHeaders createHeaders(Map<String, String> headers) {
HttpHeaders httpHeaders = new HttpHeaders();
if (headers != null && !headers.isEmpty()) {
if (headers != null) {
headers.forEach(httpHeaders::set);
}
return httpHeaders;
}
private Object normalizeValue(Object value) {
if (value == null) {
return null;
}
if (value instanceof File file) {
return fileMetadata(file.getName(), file.length());
}
if (value instanceof FileSystemResource resource) {
return fileMetadata(resource.getFilename(), resource.getFile().length());
}
if (value instanceof org.springframework.core.io.Resource resource) {
return fileMetadata(resource.getFilename(), null);
}
if (value instanceof Map<?, ?> map) {
Map<String, Object> normalized = new LinkedHashMap<>();
map.forEach((key, item) -> normalized.put(String.valueOf(key), normalizeValue(item)));
return normalized;
}
if (value instanceof Iterable<?> iterable) {
List<Object> normalized = new ArrayList<>();
iterable.forEach(item -> normalized.add(normalizeValue(item)));
return normalized;
}
if (value.getClass().isArray()) {
List<Object> normalized = new ArrayList<>();
for (int i = 0; i < Array.getLength(value); i++) {
normalized.add(normalizeValue(Array.get(value, i)));
}
return normalized;
}
return value;
}
private Map<String, Object> fileMetadata(String filename, Long size) {
Map<String, Object> metadata = new LinkedHashMap<>();
metadata.put("filename", filename);
metadata.put("size", size);
metadata.put("contentType", MediaTypeFactory.getMediaType(filename == null ? "" : filename)
.map(MediaType::toString).orElse(null));
return metadata;
}
private String safeSerialize(Object value) {
try {
return objectMapper.writeValueAsString(value);
} catch (Exception e) {
log.error("接口日志序列化失败", e);
return String.valueOf(value);
}
}
private String formatException(Exception exception) {
StringWriter writer = new StringWriter();
exception.printStackTrace(new PrintWriter(writer));
return writer.toString();
}
private void persistApiLog(SysApiLog apiLog) {
try {
apiLogService.recordApiLog(apiLog);
} catch (Exception e) {
log.error("接口日志入库失败: method={}, url={}", apiLog.getHttpMethod(), apiLog.getApiUrl(), e);
}
}
}

View File

@@ -0,0 +1,63 @@
package com.ruoyi.lsfx.client;
import com.ruoyi.lsfx.constants.LsfxConstants;
import com.ruoyi.lsfx.domain.CallerContext;
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);
CallerContext caller = CallerContext.of(7L, "tester");
when(httpUtil.uploadFile(eq(caller), eq("http://lsfx/upload"), paramsCaptor.capture(), headersCaptor.capture(), eq(UploadFileResponse.class)))
.thenReturn(response);
client.uploadFile(caller, 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

@@ -2,10 +2,14 @@ package com.ruoyi.lsfx.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.lsfx.client.CreditParseClient;
import com.ruoyi.lsfx.domain.CallerContext;
import com.ruoyi.lsfx.domain.response.CreditParseInvokeResponse;
import com.ruoyi.lsfx.exception.LsfxApiException;
import com.ruoyi.lsfx.util.HttpUtil;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
@@ -13,10 +17,14 @@ import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -24,6 +32,7 @@ import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
@@ -34,12 +43,19 @@ import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CreditParseControllerTest {
private static final CallerContext CALLER = CallerContext.of(7L, "tester");
@Mock
private CreditParseClient client;
@InjectMocks
private CreditParseController controller;
@AfterEach
void clearSecurityContext() {
SecurityContextHolder.clearContext();
}
@Test
void parse_shouldRejectBlankRemotePath() {
AjaxResult result = controller.parse(null, null);
@@ -48,12 +64,13 @@ class CreditParseControllerTest {
@Test
void shouldUseDefaultModelWhenMissing() {
setLoginUser(CALLER.userId(), CALLER.username());
CreditParseInvokeResponse response = new CreditParseInvokeResponse();
response.setSuccess(true);
response.setCode(10000);
String remotePath = "http://127.0.0.1:62318/profile/credit-html/a.html";
when(client.parse(eq("LXCUSTALL"), eq(remotePath))).thenReturn(response);
when(client.parse(eq(CALLER), eq("LXCUSTALL"), eq(remotePath))).thenReturn(response);
AjaxResult result = controller.parse(remotePath, null);
@@ -63,7 +80,8 @@ class CreditParseControllerTest {
@Test
void shouldReturnAjaxErrorWhenClientThrows() {
when(client.parse(anyString(), anyString()))
setLoginUser(CALLER.userId(), CALLER.username());
when(client.parse(any(CallerContext.class), anyString(), anyString()))
.thenThrow(new LsfxApiException("超时"));
AjaxResult result = controller.parse("http://127.0.0.1:62318/profile/credit-html/a.html", null);
@@ -71,6 +89,16 @@ class CreditParseControllerTest {
assertEquals(500, result.get("code"));
}
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);
}
@Test
@SuppressWarnings({"unchecked", "rawtypes"})
void creditParseClient_shouldInitiateAndQueryResultWithSameSerialNum() throws Exception {
@@ -88,18 +116,20 @@ class CreditParseControllerTest {
String payload = "{\"lx_header\":{\"query_cert_no\":\"330101199001010011\",\"query_cust_name\":\"张三\",\"report_time\":\"2026-03-24\"},\"lx_debt\":{\"uncle_bank_house_bal\":\"1\"},\"lx_publictype\":{\"civil_cnt\":1}}";
when(httpUtil.postUrlEncodedFormForString(
eq(CALLER),
eq("http://tz/api/service/interface/invokeService/xfeature"),
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
isNull()
)).thenReturn(initiateSuccessResponse());
when(httpUtil.postUrlEncodedFormForString(
eq(CALLER),
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
isNull()
)).thenReturn(resultSuccessResponse(objectMapper, payload, "ERR_SHOULD_IGNORE"));
String remotePath = "http://127.0.0.1:62318/profile/credit-html/a.html";
CreditParseInvokeResponse actual = parseClient.parse(remotePath);
CreditParseInvokeResponse actual = parseClient.parse(CALLER, remotePath);
assertEquals(true, actual.getSuccess());
assertEquals(10000, actual.getCode());
@@ -107,12 +137,14 @@ class CreditParseControllerTest {
.getPayload().getLxHeader().get("query_cert_no"));
ArgumentCaptor<Map<String, Object>> initiateParamsCaptor = ArgumentCaptor.forClass((Class) Map.class);
verify(httpUtil).postUrlEncodedFormForString(
eq(CALLER),
eq("http://tz/api/service/interface/invokeService/xfeature"),
initiateParamsCaptor.capture(),
isNull()
);
ArgumentCaptor<Map<String, Object>> resultParamsCaptor = ArgumentCaptor.forClass((Class) Map.class);
verify(httpUtil).postUrlEncodedFormForString(
eq(CALLER),
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
resultParamsCaptor.capture(),
isNull()
@@ -152,21 +184,24 @@ class CreditParseControllerTest {
String resultResponse = resultSuccessResponse(objectMapper, payload, "ERR_SHOULD_IGNORE");
when(httpUtil.postUrlEncodedFormForString(
eq(CALLER),
eq("http://tz/api/service/interface/invokeService/xfeature"),
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
isNull()
)).thenReturn(initiateSuccessResponse());
when(httpUtil.postUrlEncodedFormForString(
eq(CALLER),
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
isNull()
)).thenReturn(emptyPayloadResponse, emptyPayloadResponse, emptyPayloadResponse, emptyPayloadResponse, resultResponse);
CreditParseInvokeResponse actual = parseClient.parse("http://127.0.0.1:62318/profile/credit-html/a.html");
CreditParseInvokeResponse actual = parseClient.parse(CALLER, "http://127.0.0.1:62318/profile/credit-html/a.html");
assertEquals("330101199001010011", actual.getData().getMappingOutputFields()
.getPayload().getLxHeader().get("query_cert_no"));
verify(httpUtil, times(5)).postUrlEncodedFormForString(
eq(CALLER),
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
isNull()
@@ -188,18 +223,20 @@ class CreditParseControllerTest {
ReflectionTestUtils.setField(parseClient, "objectMapper", objectMapper);
when(httpUtil.postUrlEncodedFormForString(
eq(CALLER),
eq("http://tz/api/service/interface/invokeService/xfeature"),
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
isNull()
)).thenReturn(initiateSuccessResponse());
when(httpUtil.postUrlEncodedFormForString(
eq(CALLER),
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
isNull()
)).thenReturn("{\"success\":true,\"code\":99999,\"data\":{\"mappingOutputFields\":{\"message\":\"\",\"status_code\":\"0\"}}}");
LsfxApiException exception = assertThrows(LsfxApiException.class,
() -> parseClient.parse("http://127.0.0.1:62318/profile/credit-html/a.html"));
() -> parseClient.parse(CALLER, "http://127.0.0.1:62318/profile/credit-html/a.html"));
assertTrue(exception.getMessage().contains("平台状态码异常"));
}
@@ -218,18 +255,20 @@ class CreditParseControllerTest {
ReflectionTestUtils.setField(parseClient, "objectMapper", objectMapper);
when(httpUtil.postUrlEncodedFormForString(
eq(CALLER),
eq("http://tz/api/service/interface/invokeService/xfeature"),
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
isNull()
)).thenReturn(initiateSuccessResponse());
when(httpUtil.postUrlEncodedFormForString(
eq(CALLER),
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
isNull()
)).thenReturn("{\"success\":true,\"code\":10000,\"data\":{\"reasonMessage\":\"解析失败\",\"reasonCode\":500,\"status\":0,\"mappingOutputFields\":{\"message\":\"结果异常\"}}}");
LsfxApiException exception = assertThrows(LsfxApiException.class,
() -> parseClient.parse("http://127.0.0.1:62318/profile/credit-html/a.html"));
() -> parseClient.parse(CALLER, "http://127.0.0.1:62318/profile/credit-html/a.html"));
assertTrue(exception.getMessage().contains("解析失败"));
}
@@ -248,16 +287,18 @@ class CreditParseControllerTest {
ReflectionTestUtils.setField(parseClient, "objectMapper", objectMapper);
when(httpUtil.postUrlEncodedFormForString(
eq(CALLER),
eq("http://tz/api/service/interface/invokeService/xfeature"),
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
isNull()
)).thenReturn("{\"success\":true,\"code\":10000,\"data\":{\"mappingOutputFields\":{\"message\":\"文件写入失败\"},\"reasonMessage\":\"文件写入失败\",\"reasonCode\":500,\"status\":0}}");
LsfxApiException exception = assertThrows(LsfxApiException.class,
() -> parseClient.parse("http://127.0.0.1:62318/profile/credit-html/a.html"));
() -> parseClient.parse(CALLER, "http://127.0.0.1:62318/profile/credit-html/a.html"));
assertTrue(exception.getMessage().contains("文件写入失败"));
verify(httpUtil, times(0)).postUrlEncodedFormForString(
eq(CALLER),
eq("http://tz/api/service/interface/invokeService/xfeatureResult"),
org.mockito.ArgumentMatchers.<Map<String, Object>>any(),
isNull()

View File

@@ -0,0 +1,271 @@
package com.ruoyi.lsfx.util;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.lsfx.domain.CallerContext;
import com.ruoyi.lsfx.exception.LsfxApiException;
import com.ruoyi.system.domain.SysApiLog;
import com.ruoyi.system.service.ISysApiLogService;
import org.junit.jupiter.api.BeforeEach;
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.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class HttpUtilTest {
private static final CallerContext CALLER = CallerContext.of(7L, "tester");
@Mock
private RestTemplate restTemplate;
@Mock
private ISysApiLogService apiLogService;
@TempDir
Path tempDir;
private HttpUtil httpUtil;
@BeforeEach
void setUp() {
httpUtil = new HttpUtil();
ReflectionTestUtils.setField(httpUtil, "restTemplate", restTemplate);
ReflectionTestUtils.setField(httpUtil, "objectMapper", new ObjectMapper());
ReflectionTestUtils.setField(httpUtil, "apiLogService", apiLogService);
}
@Test
void uploadFile_shouldUseExplicitFilenameAndOnlyPersistFileMetadata() throws Exception {
Path tempFile = tempDir.resolve("batch_0_123456.xlsx");
Files.writeString(tempFile, "binary-content-must-not-enter-log");
ArgumentCaptor<HttpEntity<?>> requestCaptor = httpEntityCaptor();
when(restTemplate.exchange(eq("http://lsfx/upload"), eq(HttpMethod.POST),
requestCaptor.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"));
assertEquals("ok", httpUtil.uploadFile(CALLER, "http://lsfx/upload", params, null, String.class));
MultiValueMap<String, Object> body = (MultiValueMap<String, Object>) requestCaptor.getValue().getBody();
Resource resource = assertInstanceOf(Resource.class, body.getFirst("files"));
assertEquals("银行流水A.xlsx", resource.getFilename());
ArgumentCaptor<SysApiLog> logCaptor = ArgumentCaptor.forClass(SysApiLog.class);
verify(apiLogService).recordApiLog(logCaptor.capture());
SysApiLog saved = logCaptor.getValue();
assertEquals(7L, saved.getCallerUserId());
assertEquals("tester", saved.getCallerUsername());
assertTrue(saved.getRequestParams().contains("银行流水A.xlsx"));
assertTrue(saved.getRequestParams().contains("\"size\":" + Files.size(tempFile)));
assertFalse(saved.getRequestParams().contains("binary-content-must-not-enter-log"));
}
@Test
void non2xx_shouldPersistOriginalStatusHeadersAndBody() {
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.add("X-Trace-Id", "trace-1");
HttpClientErrorException error = HttpClientErrorException.create(
HttpStatus.BAD_REQUEST,
"Bad Request",
responseHeaders,
"{\"error\":\"参数错误\"}".getBytes(StandardCharsets.UTF_8),
StandardCharsets.UTF_8
);
when(restTemplate.exchange(eq("http://lsfx/fail"), eq(HttpMethod.POST),
any(HttpEntity.class), eq(String.class))).thenThrow(error);
assertThrows(LsfxApiException.class, () ->
httpUtil.postJson(CALLER, "http://lsfx/fail", Map.of("token", "raw-token"), null, String.class));
ArgumentCaptor<SysApiLog> logCaptor = ArgumentCaptor.forClass(SysApiLog.class);
verify(apiLogService).recordApiLog(logCaptor.capture());
SysApiLog saved = logCaptor.getValue();
assertEquals(400, saved.getResponseStatus());
assertEquals("{\"error\":\"参数错误\"}", saved.getResponseBody());
assertTrue(saved.getResponseHeaders().contains("trace-1"));
assertTrue(saved.getRequestParams().contains("raw-token"));
assertEquals("1", saved.getCallStatus());
}
@Test
void logPersistenceFailure_shouldNotChangeSuccessfulBusinessResult() {
when(restTemplate.exchange(eq("http://lsfx/success"), eq(HttpMethod.GET),
any(HttpEntity.class), eq(String.class))).thenReturn(ResponseEntity.ok("ok"));
doThrow(new RuntimeException("db unavailable")).when(apiLogService).recordApiLog(any(SysApiLog.class));
assertEquals("ok", httpUtil.get(CALLER, "http://lsfx/success", (Map<String, String>) null, String.class));
}
@Test
void networkFailure_shouldPersistExceptionWithoutResponseStatus() {
when(restTemplate.exchange(eq("http://lsfx/network-error"), eq(HttpMethod.GET),
any(HttpEntity.class), eq(String.class))).thenThrow(new ResourceAccessException("connection refused"));
assertThrows(LsfxApiException.class, () ->
httpUtil.get(CALLER, "http://lsfx/network-error", (Map<String, String>) null, String.class));
ArgumentCaptor<SysApiLog> logCaptor = ArgumentCaptor.forClass(SysApiLog.class);
verify(apiLogService).recordApiLog(logCaptor.capture());
SysApiLog saved = logCaptor.getValue();
assertEquals(null, saved.getResponseStatus());
assertTrue(saved.getErrorMsg().contains("connection refused"));
assertEquals("1", saved.getCallStatus());
}
@Test
void emptyResponse_shouldPersistRawResponseBeforeFailing() {
when(restTemplate.exchange(eq("http://lsfx/empty"), eq(HttpMethod.GET),
any(HttpEntity.class), eq(String.class))).thenReturn(ResponseEntity.ok(" "));
assertThrows(LsfxApiException.class, () ->
httpUtil.get(CALLER, "http://lsfx/empty", (Map<String, String>) null, String.class));
ArgumentCaptor<SysApiLog> logCaptor = ArgumentCaptor.forClass(SysApiLog.class);
verify(apiLogService).recordApiLog(logCaptor.capture());
SysApiLog saved = logCaptor.getValue();
assertEquals(200, saved.getResponseStatus());
assertEquals(" ", saved.getResponseBody());
assertTrue(saved.getErrorMsg().contains("API返回数据为空"));
}
@Test
void parseFailure_shouldPersistOriginalResponseBody() {
String rawBody = "{\"value\":\"not-an-integer\",\"unknown\":true}";
when(restTemplate.exchange(eq("http://lsfx/parse-error"), eq(HttpMethod.GET),
any(HttpEntity.class), eq(String.class))).thenReturn(ResponseEntity.ok(rawBody));
assertThrows(LsfxApiException.class, () ->
httpUtil.get(CALLER, "http://lsfx/parse-error", (Map<String, String>) null, Integer.class));
ArgumentCaptor<SysApiLog> logCaptor = ArgumentCaptor.forClass(SysApiLog.class);
verify(apiLogService).recordApiLog(logCaptor.capture());
SysApiLog saved = logCaptor.getValue();
assertEquals(rawBody, saved.getResponseBody());
assertEquals("1", saved.getCallStatus());
assertTrue(saved.getErrorMsg() != null && !saved.getErrorMsg().isBlank());
}
@Test
void successfulResponse_shouldPersistCompleteRawBodyIncludingUnknownFields() {
String rawBody = "{\"known\":1,\"unknown\":{\"nested\":true}}";
when(restTemplate.exchange(eq("http://lsfx/raw"), eq(HttpMethod.GET),
any(HttpEntity.class), eq(String.class))).thenReturn(ResponseEntity.ok(rawBody));
Map<?, ?> result = httpUtil.get(CALLER, "http://lsfx/raw", (Map<String, String>) null, Map.class);
assertEquals(Boolean.TRUE, ((Map<?, ?>) result.get("unknown")).get("nested"));
ArgumentCaptor<SysApiLog> logCaptor = ArgumentCaptor.forClass(SysApiLog.class);
verify(apiLogService).recordApiLog(logCaptor.capture());
assertEquals(rawBody, logCaptor.getValue().getResponseBody());
assertEquals("0", logCaptor.getValue().getCallStatus());
}
@Test
void requestSnapshotFailure_shouldNotPreventExternalCallOrLogging() {
Iterable<String> brokenSnapshotValue = () -> new Iterator<>() {
@Override
public boolean hasNext() {
throw new IllegalStateException("snapshot failed");
}
@Override
public String next() {
return "unused";
}
};
when(restTemplate.exchange(eq("http://lsfx/snapshot-error"), eq(HttpMethod.POST),
any(HttpEntity.class), eq(String.class))).thenReturn(ResponseEntity.ok("ok"));
assertEquals("ok", httpUtil.postJson(CALLER, "http://lsfx/snapshot-error",
brokenSnapshotValue, null, String.class));
ArgumentCaptor<SysApiLog> logCaptor = ArgumentCaptor.forClass(SysApiLog.class);
verify(apiLogService).recordApiLog(logCaptor.capture());
assertEquals("0", logCaptor.getValue().getCallStatus());
verify(restTemplate).exchange(eq("http://lsfx/snapshot-error"), eq(HttpMethod.POST),
any(HttpEntity.class), eq(String.class));
}
@Test
void allPublicRequestTypes_shouldUseUnifiedLoggingPath() {
when(restTemplate.exchange(any(String.class), any(HttpMethod.class),
any(HttpEntity.class), eq(String.class))).thenReturn(ResponseEntity.ok("ok"));
Map<String, Object> params = Map.of("value", "完整参数");
Map<String, String> headers = Map.of("client-id", "client-1");
assertEquals("ok", httpUtil.get(CALLER, "http://lsfx/get", params, headers, String.class));
assertEquals("ok", httpUtil.get(CALLER, "http://lsfx/get", headers, String.class));
assertEquals("ok", httpUtil.postJson(CALLER, "http://lsfx/json", params, headers, String.class));
assertEquals("ok", httpUtil.postFormData(CALLER, "http://lsfx/form-data", params, headers, String.class));
assertEquals("ok", httpUtil.postUrlEncodedForm(CALLER, "http://lsfx/form", params, headers, String.class));
assertEquals("ok", httpUtil.postUrlEncodedFormForString(CALLER, "http://lsfx/form-string", params, headers));
assertEquals("ok", httpUtil.uploadFile(CALLER, "http://lsfx/upload", params, headers, String.class));
verify(apiLogService, times(7)).recordApiLog(any(SysApiLog.class));
}
@Test
void missingCaller_shouldFailBeforeHttpRequest() {
assertThrows(IllegalArgumentException.class, () ->
httpUtil.postJson(null, "http://lsfx/json", Map.of(), null, String.class));
verify(restTemplate, never()).exchange(any(String.class), any(HttpMethod.class),
any(HttpEntity.class), eq(String.class));
verify(apiLogService, never()).recordApiLog(any(SysApiLog.class));
}
@Test
void systemCaller_shouldPersistNullUserIdAndSystemUsername() {
when(restTemplate.exchange(eq("http://lsfx/system"), eq(HttpMethod.GET),
any(HttpEntity.class), eq(String.class))).thenReturn(ResponseEntity.ok("ok"));
assertEquals("ok", httpUtil.get(CallerContext.system(), "http://lsfx/system",
(Map<String, String>) null, String.class));
ArgumentCaptor<SysApiLog> logCaptor = ArgumentCaptor.forClass(SysApiLog.class);
verify(apiLogService).recordApiLog(logCaptor.capture());
assertEquals(null, logCaptor.getValue().getCallerUserId());
assertEquals("system", logCaptor.getValue().getCallerUsername());
}
@SuppressWarnings({"unchecked", "rawtypes"})
private ArgumentCaptor<HttpEntity<?>> httpEntityCaptor() {
return (ArgumentCaptor) ArgumentCaptor.forClass(HttpEntity.class);
}
}

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

@@ -3,9 +3,11 @@ package com.ruoyi.ccdi.project.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.dto.CcdiBankStatementQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiBankStatementExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementCounterpartyVO;
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;
@@ -17,7 +19,6 @@ 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 org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@@ -39,26 +40,42 @@ 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);
return getDataTable(result.getRecords(), result.getTotal());
}
/**
* 分页查询对手方聚合列表
*/
@GetMapping("/counterparty/list")
@Operation(summary = "分页查询对手方聚合列表")
public TableDataInfo counterpartyList(CcdiBankStatementQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
PageDomain pageDomain = TableSupport.buildPageRequest();
Page<CcdiBankStatementCounterpartyVO> page = new Page<>(pageDomain.getPageNum(), pageDomain.getPageSize());
Page<CcdiBankStatementCounterpartyVO> result = bankStatementService.selectCounterpartyPage(page, queryDTO);
return getDataTable(result.getRecords(), result.getTotal());
}
/**
* 查询项目级筛选项
*/
@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,9 +85,13 @@ public class CcdiBankStatementController extends BaseController {
*/
@GetMapping("/detail/{bankStatementId}")
@Operation(summary = "查询流水详情")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getDetail(@PathVariable Long bankStatementId) {
CcdiBankStatementDetailVO detail = bankStatementService.getStatementDetail(bankStatementId);
public AjaxResult getDetail(@PathVariable Long bankStatementId, String modelCode, String suspiciousType) {
projectAccessService.assertCanReadByBankStatementId(bankStatementId);
CcdiBankStatementDetailVO detail = bankStatementService.getStatementDetail(
bankStatementId,
modelCode,
suspiciousType
);
return AjaxResult.success(detail);
}
@@ -79,8 +100,8 @@ public class CcdiBankStatementController extends BaseController {
*/
@PostMapping("/export")
@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;
@@ -13,10 +14,13 @@ import com.ruoyi.common.core.page.PageDomain;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.page.TableSupport;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.lsfx.constants.LsfxConstants;
import com.ruoyi.lsfx.domain.CallerContext;
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 +44,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 +67,7 @@ public class CcdiFileUploadController extends BaseController {
return AjaxResult.error("单次最多上传100个文件");
}
// 校验文件大小和格式
// 校验文件数量、空文件和大小,文件名业务规则统一在 Service 层处理
for (MultipartFile file : files) {
if (file.isEmpty()) {
return AjaxResult.error("文件不能为空");
@@ -66,20 +75,11 @@ 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 {
String username = SecurityUtils.getUsername();
String batchId = fileUploadService.batchUploadFiles(projectId, files, username);
CallerContext caller = CallerContext.from(SecurityUtils.getLoginUser());
String batchId = fileUploadService.batchUploadFiles(projectId, files, caller);
return AjaxResult.success("上传任务已提交", batchId);
} catch (RejectedExecutionException e) {
log.warn("线程池已满,拒绝上传请求: projectId={}, fileCount={}", projectId, files.length);
@@ -95,6 +95,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("身份证文件不能为空");
@@ -104,30 +105,40 @@ public class CcdiFileUploadController extends BaseController {
}
/**
* 提交拉取本行信息任务
* 提交拉取行内/金综流水任务
*/
@PostMapping("/pull-bank-info")
@Operation(summary = "拉取本行信息", description = "身份证号批量提交拉取本行信息任务")
@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("身份证号不能为空");
}
if (!StringUtils.hasText(dto.getStartDate()) || !StringUtils.hasText(dto.getEndDate())) {
if (!StringUtils.hasText(dto.getDataChannelCode())) {
return AjaxResult.error("流水来源不能为空");
}
String dataChannelCode = dto.getDataChannelCode().trim().toUpperCase();
if (!LsfxConstants.DATA_CHANNEL_ZJRCU.equals(dataChannelCode)
&& !LsfxConstants.DATA_CHANNEL_JZL.equals(dataChannelCode)) {
return AjaxResult.error("流水来源不支持");
}
if (LsfxConstants.DATA_CHANNEL_ZJRCU.equals(dataChannelCode)
&& (!StringUtils.hasText(dto.getStartDate()) || !StringUtils.hasText(dto.getEndDate()))) {
return AjaxResult.error("开始日期和结束日期不能为空");
}
Long userId = SecurityUtils.getUserId();
String username = SecurityUtils.getUsername();
CallerContext caller = CallerContext.from(SecurityUtils.getLoginUser());
String batchId = fileUploadService.submitPullBankInfo(
dto.getProjectId(),
dto.getIdCards(),
dataChannelCode,
dto.getStartDate(),
dto.getEndDate(),
userId,
username
caller
);
return AjaxResult.success("拉取任务已提交", batchId);
}
@@ -138,6 +149,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 +162,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 +173,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 +183,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

@@ -13,6 +13,7 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectHistoryListItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectStatusCountsVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectVO;
import com.ruoyi.ccdi.project.service.ICcdiProjectService;
import com.ruoyi.lsfx.domain.CallerContext;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
@@ -43,7 +44,7 @@ public class CcdiProjectController extends BaseController {
@Operation(summary = "创建项目")
@PreAuthorize("@ss.hasPermi('ccdi:project:add')")
public AjaxResult createProject(@Validated @RequestBody CcdiProjectSaveDTO dto) {
CcdiProjectVO vo = projectService.createProject(dto);
CcdiProjectVO vo = projectService.createProject(dto, CallerContext.from(SecurityUtils.getLoginUser()));
return AjaxResult.success("项目创建成功", vo);
}
@@ -74,18 +75,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 +107,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 +119,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);
@@ -122,7 +131,7 @@ public class CcdiProjectController extends BaseController {
@Operation(summary = "导入历史项目")
@PreAuthorize("@ss.hasPermi('ccdi:project:add')")
public AjaxResult importFromHistory(@Validated @RequestBody CcdiProjectImportHistoryDTO dto) {
CcdiProjectVO vo = projectService.importFromHistory(dto, SecurityUtils.getUsername());
CcdiProjectVO vo = projectService.importFromHistory(dto, CallerContext.from(SecurityUtils.getLoginUser()));
return AjaxResult.success("项目创建成功", vo);
}
@@ -131,7 +140,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,23 @@ 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.dto.CcdiProjectUnsubmittedAccountQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectExternalPersonWarningExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskModelPeopleExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectRiskPeopleOverviewExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectSuspiciousTransactionExcel;
import com.ruoyi.ccdi.project.domain.excel.CcdiProjectUnsubmittedAccountFlowExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountPageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativePageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectExternalRiskSummaryVO;
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 +26,9 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskPeopleOverviewVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionPageVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectTopRiskPeopleVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectUnsubmittedAccountFlowVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectUnsubmittedAccountVO;
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
import com.ruoyi.ccdi.project.service.ICcdiProjectOverviewService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.controller.BaseController;
@@ -26,8 +38,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 +59,9 @@ public class CcdiProjectOverviewController extends BaseController {
@Resource
private ICcdiProjectOverviewService overviewService;
@Resource
private CcdiProjectAccessService projectAccessService;
/**
* 查询风险仪表盘
*/
@@ -52,10 +69,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 +93,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 +129,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 +141,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 +153,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 +189,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 +201,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 +213,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,10 +225,35 @@ 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);
}
/**
* 查询未提交账户
*/
@GetMapping("/unsubmitted-accounts")
@Operation(summary = "查询未提交账户")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getUnsubmittedAccounts(CcdiProjectUnsubmittedAccountQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
CcdiProjectUnsubmittedAccountVO result = overviewService.getUnsubmittedAccounts(queryDTO);
return AjaxResult.success(result);
}
/**
* 查询未提交账户流水
*/
@GetMapping("/unsubmitted-accounts/flows")
@Operation(summary = "查询未提交账户流水")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public AjaxResult getUnsubmittedAccountFlows(CcdiProjectUnsubmittedAccountQueryDTO queryDTO) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
List<CcdiProjectUnsubmittedAccountFlowVO> rows = overviewService.getUnsubmittedAccountFlows(queryDTO);
return AjaxResult.success(rows);
}
/**
* 导出涉疑交易明细
*/
@@ -154,6 +264,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,20 +278,87 @@ 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, "外部人员风险模型命中人员");
}
/**
* 导出未提交账户流水
*/
@PostMapping("/unsubmitted-accounts/export")
@Operation(summary = "导出未提交账户流水")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public void exportUnsubmittedAccountFlows(
HttpServletResponse response,
CcdiProjectUnsubmittedAccountQueryDTO queryDTO
) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
List<CcdiProjectUnsubmittedAccountFlowExcel> rows = overviewService.exportUnsubmittedAccountFlows(queryDTO);
ExcelUtil<CcdiProjectUnsubmittedAccountFlowExcel> util =
new ExcelUtil<>(CcdiProjectUnsubmittedAccountFlowExcel.class);
util.exportExcel(response, rows, "未提交账户流水");
}
/**
* 导出风险明细
*/
@PostMapping("/risk-details/export")
@Operation(summary = "导出风险明细")
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
public void exportRiskDetails(HttpServletResponse response, Long projectId) {
overviewService.exportRiskDetails(response, projectId);
public void exportRiskDetails(
HttpServletResponse response,
CcdiProjectSuspiciousTransactionQueryDTO queryDTO
) {
projectAccessService.assertCanRead(queryDTO.getProjectId());
overviewService.exportRiskDetails(response, queryDTO);
}
/**
@@ -190,6 +368,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;
@@ -19,4 +22,7 @@ public class CcdiProjectSuspiciousTransactionQueryDTO {
/** 每页数量 */
private Integer pageSize;
/** 是否包含外部人员预警分支 */
private Boolean includeExternalPerson;
}

View File

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

View File

@@ -5,7 +5,7 @@ import lombok.Data;
import java.util.List;
/**
* 拉取本行信息提交参数
* 拉取行内/金综流水提交参数
*/
@Data
public class CcdiPullBankInfoSubmitDTO {
@@ -16,6 +16,9 @@ public class CcdiPullBankInfoSubmitDTO {
/** 身份证号列表 */
private List<String> idCards;
/** 数据渠道编码ZJRCU-行内JZL-金综 */
private String dataChannelCode;
/** 开始日期 */
private String startDate;

View File

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

View File

@@ -194,8 +194,8 @@ public class CcdiBankStatement implements Serializable {
BeanUtils.copyProperties(item, entity);
// 4. 手动映射字段名不一致的情况
entity.setLeAccountNo(item.getAccountMaskNo());
entity.setCustomerAccountNo(item.getCustomerAccountMaskNo());
entity.setLeAccountNo(item.getAccountNo());
entity.setCustomerAccountNo(item.getCustomerAccountNo());
entity.setLeAccountName(item.getLeName());
entity.setAmountDr(item.getDrAmount());
entity.setAmountCr(item.getCrAmount());
@@ -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

@@ -1,6 +1,7 @@
package com.ruoyi.ccdi.project.domain.excel;
import com.ruoyi.common.annotation.Excel;
import java.math.BigDecimal;
import lombok.Data;
/**
@@ -26,4 +27,10 @@ public class CcdiProjectAbnormalAccountExcel {
@Excel(name = "状态")
private String status;
@Excel(name = "命中原因")
private String reasonDetail;
@Excel(name = "涉及金额")
private BigDecimal involvedAmount;
}

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,21 +14,30 @@ 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 BigDecimal displayAmount;
@Excel(name = "异常标签")
private String hitTags;
}

View File

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

View File

@@ -0,0 +1,44 @@
package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* 流水对手方聚合VO
*
* @author ruoyi
*/
@Data
public class CcdiBankStatementCounterpartyVO {
/** 对方名称 */
private String customerAccountName;
/** 对方名称是否为空 */
private Boolean nameEmpty;
/** 关联账号数 */
private Integer accountCount;
/** 流入合计 */
private BigDecimal inAmount;
/** 流入笔数 */
private Long inCount;
/** 流出合计 */
private BigDecimal outAmount;
/** 流出笔数 */
private Long outCount;
/** 总交易金额 */
private BigDecimal totalAmount;
/** 总笔数 */
private Long totalCount;
/** 最近交易时间 */
private String latestTrxDate;
}

View File

@@ -8,6 +8,9 @@ import lombok.Data;
@Data
public class CcdiBankStatementHitTagVO {
/** 模型编码 */
private String modelCode;
/** 规则编码 */
private String ruleCode;

View File

@@ -1,5 +1,6 @@
package com.ruoyi.ccdi.project.domain.vo;
import java.math.BigDecimal;
import lombok.Data;
/**
@@ -19,4 +20,8 @@ public class CcdiProjectAbnormalAccountItemVO {
private String abnormalTime;
private String status;
private String reasonDetail;
private BigDecimal involvedAmount;
}

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

@@ -24,6 +24,15 @@ public class CcdiProjectFamilyAssetLiabilityListItemVO {
/** 家庭总年收入 */
private BigDecimal totalIncome;
/** 本人年收入 */
private BigDecimal selfIncome;
/** 配偶年收入 */
private BigDecimal spouseIncome;
/** 本人入职年限 */
private Integer employmentYears;
/** 家庭总资产 */
private BigDecimal totalAsset;
@@ -33,6 +42,12 @@ public class CcdiProjectFamilyAssetLiabilityListItemVO {
/** 收入负债对比金额 */
private BigDecimal comparisonAmount;
/** 可解释收入 */
private BigDecimal explainableIncome;
/** 资产收入倍数 */
private BigDecimal assetIncomeRatio;
/** 风险等级编码 */
private String riskLevelCode;

View File

@@ -15,6 +15,12 @@ public class CcdiProjectFamilyIncomeDetailVO {
/** 配偶年收入 */
private BigDecimal spouseIncome;
/** 本人入职年限 */
private Integer employmentYears;
/** 家庭总年收入 */
private BigDecimal totalIncome;
/** 可解释收入 */
private BigDecimal explainableIncome;
}

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,8 @@ 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 com.ruoyi.ccdi.project.domain.excel.CcdiProjectUnsubmittedAccountFlowExcel;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
@@ -21,13 +23,21 @@ 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<>();
private List<CcdiProjectAbnormalAccountExcel> abnormalAccounts = new ArrayList<>();
private List<CcdiProjectUnsubmittedAccountFlowExcel> unsubmittedAccountFlows = 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

@@ -3,6 +3,8 @@ package com.ruoyi.ccdi.project.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
* 涉疑交易明细行
@@ -14,6 +16,14 @@ public class CcdiProjectSuspiciousTransactionItemVO {
private String trxDate;
private String leAccountNo;
private String leAccountName;
private String customerAccountName;
private String customerAccountNo;
private String suspiciousPersonName;
private String relatedPersonName;
@@ -35,4 +45,6 @@ public class CcdiProjectSuspiciousTransactionItemVO {
private Boolean hasNameListHit;
private String nameListHitType;
private List<CcdiBankStatementHitTagVO> hitTags = new ArrayList<>();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.entity.CcdiBankStatement;
import com.ruoyi.ccdi.project.domain.dto.CcdiBankStatementQueryDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementCounterpartyVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementFilterOptionsVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
@@ -38,6 +39,9 @@ public interface CcdiBankStatementMapper extends BaseMapper<CcdiBankStatement> {
Page<CcdiBankStatementListVO> selectStatementPage(Page<CcdiBankStatementListVO> page,
@Param("query") CcdiBankStatementQueryDTO query);
Page<CcdiBankStatementCounterpartyVO> selectCounterpartyPage(Page<CcdiBankStatementCounterpartyVO> page,
@Param("query") CcdiBankStatementQueryDTO query);
List<CcdiBankStatementListVO> selectStatementListForExport(@Param("query") CcdiBankStatementQueryDTO query);
CcdiBankStatementDetailVO selectStatementDetailById(@Param("bankStatementId") Long bankStatementId);

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

@@ -26,11 +26,15 @@ public interface CcdiBankTagResultMapper extends BaseMapper<CcdiBankTagResult> {
*
* @param projectId 项目ID
* @param bankStatementIds 流水ID列表
* @param modelCode 模型编码
* @param suspiciousType 预警类型
* @return 命中的异常标签列表
*/
List<CcdiBankStatementHitTagVO> selectStatementTagsByProjectAndStatementIds(
@Param("projectId") Long projectId,
@Param("bankStatementIds") List<Long> bankStatementIds
@Param("bankStatementIds") List<Long> bankStatementIds,
@Param("modelCode") String modelCode,
@Param("suspiciousType") String suspiciousType
);
/**

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,13 +4,18 @@ 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;
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectUnsubmittedAccountQueryDTO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectAbnormalAccountItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectEmployeeCreditNegativeItemVO;
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;
@@ -20,6 +25,8 @@ import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskHitTagVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelCardVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectRiskModelPeopleItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectSuspiciousTransactionItemVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectUnsubmittedAccountFlowVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiProjectUnsubmittedAccountItemVO;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Mapper;
@@ -121,6 +128,96 @@ 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 存在时返回1否则返回空
*/
Integer selectExternalPersonSubjectExistsByProjectId(@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
);
/**
* 分页查询涉疑交易明细
*
@@ -165,6 +262,26 @@ public interface CcdiProjectOverviewMapper {
*/
List<CcdiProjectAbnormalAccountItemVO> selectAbnormalAccountList(@Param("projectId") Long projectId);
/**
* 查询未提交账户列表
*
* @param query 查询条件
* @return 未提交账户列表
*/
List<CcdiProjectUnsubmittedAccountItemVO> selectUnsubmittedAccountList(
@Param("query") CcdiProjectUnsubmittedAccountQueryDTO query
);
/**
* 查询未提交账户流水
*
* @param query 查询条件
* @return 未提交账户流水
*/
List<CcdiProjectUnsubmittedAccountFlowVO> selectUnsubmittedAccountFlowList(
@Param("query") CcdiProjectUnsubmittedAccountQueryDTO query
);
/**
* 查询项目员工负面征信导出列表
*
@@ -240,4 +357,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

@@ -3,6 +3,7 @@ package com.ruoyi.ccdi.project.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.ccdi.project.domain.dto.CcdiBankStatementQueryDTO;
import com.ruoyi.ccdi.project.domain.excel.CcdiBankStatementExcel;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementCounterpartyVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementDetailVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementFilterOptionsVO;
import com.ruoyi.ccdi.project.domain.vo.CcdiBankStatementListVO;
@@ -34,6 +35,16 @@ public interface ICcdiBankStatementService {
Page<CcdiBankStatementListVO> selectStatementPage(Page<CcdiBankStatementListVO> page,
CcdiBankStatementQueryDTO queryDTO);
/**
* 分页查询对手方聚合列表
*
* @param page 分页对象
* @param queryDTO 查询条件
* @return 分页结果
*/
Page<CcdiBankStatementCounterpartyVO> selectCounterpartyPage(Page<CcdiBankStatementCounterpartyVO> page,
CcdiBankStatementQueryDTO queryDTO);
/**
* 查询导出列表
*
@@ -49,4 +60,18 @@ public interface ICcdiBankStatementService {
* @return 详情
*/
CcdiBankStatementDetailVO getStatementDetail(Long bankStatementId);
/**
* 按涉疑交易筛选范围查询流水详情
*
* @param bankStatementId 流水ID
* @param modelCode 模型编码
* @param suspiciousType 预警类型
* @return 详情
*/
CcdiBankStatementDetailVO getStatementDetail(
Long bankStatementId,
String modelCode,
String suspiciousType
);
}

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