Compare commits
6 Commits
0a0313af7c
...
dev-ui
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d004a66e8 | |||
| 2bcba71259 | |||
| ffaa2fb2a0 | |||
| dc8055ce7a | |||
| 90f483f427 | |||
| 2d9828ea5f |
84
AGENTS.md
84
AGENTS.md
@@ -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 版本
|
||||
|
||||
@@ -57,12 +57,6 @@
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ccdi-lsfx</artifactId>
|
||||
<version>3.9.1</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.ruoyi.info.collection.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/** 工商信息同步专用线程池。 */
|
||||
@Configuration
|
||||
public class EnterpriseProfileExecutorConfig {
|
||||
|
||||
@Bean("enterpriseProfileExecutor")
|
||||
public Executor enterpriseProfileExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(3);
|
||||
executor.setMaxPoolSize(3);
|
||||
executor.setQueueCapacity(1000);
|
||||
executor.setThreadNamePrefix("enterprise-profile-");
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
@@ -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 = "查询征信维护列表")
|
||||
|
||||
@@ -8,6 +8,7 @@ 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.enums.BusinessType;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoAddDTO;
|
||||
import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoEditDTO;
|
||||
import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoQueryDTO;
|
||||
@@ -18,6 +19,8 @@ import com.ruoyi.info.collection.domain.vo.ImportResultVO;
|
||||
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
|
||||
import com.ruoyi.info.collection.service.ICcdiEnterpriseBaseInfoImportService;
|
||||
import com.ruoyi.info.collection.service.ICcdiEnterpriseBaseInfoService;
|
||||
import com.ruoyi.info.collection.service.IEnterpriseEntitySyncService;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import com.ruoyi.info.collection.utils.EasyExcelUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -56,6 +59,9 @@ public class CcdiEnterpriseBaseInfoController extends BaseController {
|
||||
@Resource
|
||||
private ICcdiEnterpriseBaseInfoImportService enterpriseBaseInfoImportService;
|
||||
|
||||
@Resource
|
||||
private IEnterpriseEntitySyncService enterpriseEntitySyncService;
|
||||
|
||||
@Operation(summary = "查询实体库列表")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:enterpriseBaseInfo:list')")
|
||||
@GetMapping("/list")
|
||||
@@ -73,12 +79,36 @@ public class CcdiEnterpriseBaseInfoController extends BaseController {
|
||||
return success(enterpriseBaseInfoService.selectEnterpriseBaseInfoById(socialCreditCode));
|
||||
}
|
||||
|
||||
@Operation(summary = "重新查询实体工商信息")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:enterpriseBaseInfo:refresh')")
|
||||
@PostMapping("/enterprise-profile/refresh/{socialCreditCode}")
|
||||
public AjaxResult refreshEnterpriseProfile(@PathVariable String socialCreditCode) {
|
||||
enterpriseEntitySyncService.refresh(CallerContext.from(SecurityUtils.getLoginUser()), socialCreditCode);
|
||||
return AjaxResult.success("工商信息更新成功");
|
||||
}
|
||||
|
||||
@Operation(summary = "启动实体工商信息历史补全")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:enterpriseBaseInfo:backfill')")
|
||||
@PostMapping("/enterprise-profile/backfill")
|
||||
public AjaxResult startEnterpriseProfileBackfill() {
|
||||
String taskId = enterpriseEntitySyncService.startBackfill(CallerContext.from(SecurityUtils.getLoginUser()));
|
||||
return AjaxResult.success("工商信息补全任务已提交", taskId);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询实体工商信息历史补全状态")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:enterpriseBaseInfo:backfill')")
|
||||
@GetMapping("/enterprise-profile/backfill/{taskId}")
|
||||
public AjaxResult getEnterpriseProfileBackfillStatus(@PathVariable String taskId) {
|
||||
return AjaxResult.success(enterpriseEntitySyncService.getBackfillStatus(taskId));
|
||||
}
|
||||
|
||||
@Operation(summary = "新增实体库信息")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:enterpriseBaseInfo:add')")
|
||||
@Log(title = "实体库管理", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public AjaxResult add(@Validated @RequestBody CcdiEnterpriseBaseInfoAddDTO addDTO) {
|
||||
return toAjax(enterpriseBaseInfoService.insertEnterpriseBaseInfo(addDTO));
|
||||
return toAjax(enterpriseBaseInfoService.insertEnterpriseBaseInfo(
|
||||
CallerContext.from(SecurityUtils.getLoginUser()), addDTO));
|
||||
}
|
||||
|
||||
@Operation(summary = "修改实体库信息")
|
||||
@@ -86,7 +116,8 @@ public class CcdiEnterpriseBaseInfoController extends BaseController {
|
||||
@Log(title = "实体库管理", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public AjaxResult edit(@Validated @RequestBody CcdiEnterpriseBaseInfoEditDTO editDTO) {
|
||||
return toAjax(enterpriseBaseInfoService.updateEnterpriseBaseInfo(editDTO));
|
||||
return toAjax(enterpriseBaseInfoService.updateEnterpriseBaseInfo(
|
||||
CallerContext.from(SecurityUtils.getLoginUser()), editDTO));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除实体库信息")
|
||||
@@ -113,7 +144,8 @@ public class CcdiEnterpriseBaseInfoController extends BaseController {
|
||||
return error("至少需要一条数据");
|
||||
}
|
||||
|
||||
String taskId = enterpriseBaseInfoService.importEnterpriseBaseInfo(list);
|
||||
String taskId = enterpriseBaseInfoService.importEnterpriseBaseInfo(
|
||||
CallerContext.from(SecurityUtils.getLoginUser()), list);
|
||||
ImportResultVO result = new ImportResultVO();
|
||||
result.setTaskId(taskId);
|
||||
result.setStatus("PROCESSING");
|
||||
|
||||
@@ -5,6 +5,7 @@ import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
@@ -48,6 +49,30 @@ public class CcdiEnterpriseBaseInfo implements Serializable {
|
||||
/** 法定代表人 */
|
||||
private String legalRepresentative;
|
||||
|
||||
/** 注册资本 */
|
||||
private BigDecimal registeredCapital;
|
||||
|
||||
/** 注册资本单位 */
|
||||
private String registeredCapitalUnit;
|
||||
|
||||
/** 注册日期 */
|
||||
private Date registerDate;
|
||||
|
||||
/** 区域编码 */
|
||||
private String regionCode;
|
||||
|
||||
/** 区域名称 */
|
||||
private String regionName;
|
||||
|
||||
/** 从业人数 */
|
||||
private Integer employeeCount;
|
||||
|
||||
/** 工商缓存ID */
|
||||
private Long cacheInfoId;
|
||||
|
||||
/** 工商同步时间 */
|
||||
private Date enterpriseSyncTime;
|
||||
|
||||
/** 法定代表人证件类型 */
|
||||
private String legalCertType;
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.ruoyi.info.collection.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/** 工商信息原始响应缓存。 */
|
||||
@Data
|
||||
@TableName("ccdi_enterpriseinfo_query_cache")
|
||||
public class CcdiEnterpriseInfoQueryCache {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long infoId;
|
||||
private String queryParam;
|
||||
private String queryResult;
|
||||
private String queryType;
|
||||
private Date createdDate;
|
||||
private Date validDate;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.ruoyi.info.collection.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/** 实体库企业完整股东。 */
|
||||
@Data
|
||||
@TableName("ccdi_enterprise_shareholder")
|
||||
public class CcdiEnterpriseShareholder {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long shareholderId;
|
||||
private String socialCreditCode;
|
||||
private Integer shareholderSeq;
|
||||
private String shareholderName;
|
||||
private String shareholderType;
|
||||
private String shareholderCreditCode;
|
||||
private BigDecimal stockPercent;
|
||||
private BigDecimal subscribedCapital;
|
||||
private String capitalUnit;
|
||||
private Long cacheInfoId;
|
||||
private Date syncTime;
|
||||
private String createBy;
|
||||
private Date createTime;
|
||||
private String updateBy;
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.ruoyi.info.collection.domain.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** 统一工商信息结构。 */
|
||||
@Data
|
||||
public class EnterpriseProfile {
|
||||
private String creditCode;
|
||||
private String enterpriseName;
|
||||
private BigDecimal registeredCapital;
|
||||
private String registeredCapitalUnit;
|
||||
private LocalDate registerDate;
|
||||
private LocalDate establishDate;
|
||||
private String industryCode;
|
||||
private String industryName;
|
||||
private String organizationTypeCode;
|
||||
private String organizationTypeName;
|
||||
private String regionCode;
|
||||
private String regionName;
|
||||
private String registerAddress;
|
||||
private Integer employeeCount;
|
||||
private String legalRepresentative;
|
||||
private List<EnterpriseShareholderProfile> shareholders = new ArrayList<>();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.ruoyi.info.collection.domain.model;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/** 统一工商查询结果。 */
|
||||
public record EnterpriseProfileQueryResult(
|
||||
EnterpriseProfile profile,
|
||||
Long cacheInfoId,
|
||||
Date validDate,
|
||||
boolean fromCache
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ruoyi.info.collection.domain.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/** 工商股东结构。 */
|
||||
@Data
|
||||
public class EnterpriseShareholderProfile {
|
||||
private Integer sequence;
|
||||
private String shareholderName;
|
||||
private BigDecimal stockPercent;
|
||||
private BigDecimal subscribedCapital;
|
||||
private String capitalUnit;
|
||||
}
|
||||
@@ -5,7 +5,10 @@ import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 实体库管理 VO
|
||||
@@ -47,6 +50,39 @@ public class CcdiEnterpriseBaseInfoVO implements Serializable {
|
||||
@Schema(description = "法定代表人")
|
||||
private String legalRepresentative;
|
||||
|
||||
@Schema(description = "注册资本")
|
||||
private BigDecimal registeredCapital;
|
||||
|
||||
@Schema(description = "注册资本单位")
|
||||
private String registeredCapitalUnit;
|
||||
|
||||
@Schema(description = "注册日期")
|
||||
private Date registerDate;
|
||||
|
||||
@Schema(description = "区域编码")
|
||||
private String regionCode;
|
||||
|
||||
@Schema(description = "区域名称")
|
||||
private String regionName;
|
||||
|
||||
@Schema(description = "从业人数")
|
||||
private Integer employeeCount;
|
||||
|
||||
@Schema(description = "工商缓存ID")
|
||||
private Long cacheInfoId;
|
||||
|
||||
@Schema(description = "工商同步时间")
|
||||
private Date enterpriseSyncTime;
|
||||
|
||||
@Schema(description = "缓存有效期")
|
||||
private Date enterpriseCacheValidDate;
|
||||
|
||||
@Schema(description = "是否允许重新查询")
|
||||
private Boolean canRefreshEnterpriseProfile;
|
||||
|
||||
@Schema(description = "完整股东列表")
|
||||
private List<EnterpriseShareholderVO> shareholders = new ArrayList<>();
|
||||
|
||||
@Schema(description = "法定代表人证件类型")
|
||||
private String legalCertType;
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.ruoyi.info.collection.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/** 工商信息历史补全任务状态。 */
|
||||
@Data
|
||||
public class EnterpriseProfileSyncTaskVO {
|
||||
private String taskId;
|
||||
private String status;
|
||||
private Integer totalCount;
|
||||
private Integer completedCount;
|
||||
private Integer successCount;
|
||||
private Integer skippedCount;
|
||||
private Integer failureCount;
|
||||
private String message;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.ruoyi.info.collection.domain.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/** 企业股东明细。 */
|
||||
@Data
|
||||
@Schema(description = "企业股东明细")
|
||||
public class EnterpriseShareholderVO {
|
||||
private Integer shareholderSeq;
|
||||
private String shareholderName;
|
||||
private BigDecimal stockPercent;
|
||||
private BigDecimal subscribedCapital;
|
||||
private String capitalUnit;
|
||||
}
|
||||
@@ -52,4 +52,8 @@ public interface CcdiEnterpriseBaseInfoMapper extends BaseMapper<CcdiEnterpriseB
|
||||
* @return 更新行数
|
||||
*/
|
||||
int updateBatch(List<CcdiEnterpriseBaseInfo> list);
|
||||
|
||||
int updateEnterpriseProfile(CcdiEnterpriseBaseInfo entity);
|
||||
|
||||
List<CcdiEnterpriseBaseInfo> selectEnterpriseProfileCandidates();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.ruoyi.info.collection.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ruoyi.info.collection.domain.CcdiEnterpriseInfoQueryCache;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/** 工商信息缓存 Mapper。 */
|
||||
public interface CcdiEnterpriseInfoQueryCacheMapper extends BaseMapper<CcdiEnterpriseInfoQueryCache> {
|
||||
CcdiEnterpriseInfoQueryCache selectValidCache(@Param("queryParam") String queryParam,
|
||||
@Param("queryType") String queryType,
|
||||
@Param("now") Date now);
|
||||
|
||||
int upsertCache(CcdiEnterpriseInfoQueryCache cache);
|
||||
|
||||
CcdiEnterpriseInfoQueryCache selectCache(@Param("queryParam") String queryParam,
|
||||
@Param("queryType") String queryType);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ruoyi.info.collection.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ruoyi.info.collection.domain.CcdiEnterpriseShareholder;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 实体工商股东 Mapper。 */
|
||||
public interface CcdiEnterpriseShareholderMapper extends BaseMapper<CcdiEnterpriseShareholder> {
|
||||
List<CcdiEnterpriseShareholder> selectByCreditCode(@Param("creditCode") String creditCode);
|
||||
int deleteByCreditCode(@Param("creditCode") String creditCode);
|
||||
int insertBatch(@Param("list") List<CcdiEnterpriseShareholder> list);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo;
|
||||
import com.ruoyi.info.collection.domain.excel.CcdiEnterpriseBaseInfoExcel;
|
||||
import com.ruoyi.info.collection.domain.vo.EnterpriseBaseInfoImportFailureVO;
|
||||
import com.ruoyi.info.collection.domain.vo.ImportStatusVO;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -16,7 +17,7 @@ import java.util.Set;
|
||||
*/
|
||||
public interface ICcdiEnterpriseBaseInfoImportService {
|
||||
|
||||
void importEnterpriseBaseInfoAsync(List<CcdiEnterpriseBaseInfoExcel> excelList, String taskId, String userName);
|
||||
void importEnterpriseBaseInfoAsync(List<CcdiEnterpriseBaseInfoExcel> excelList, String taskId, CallerContext caller);
|
||||
|
||||
ImportStatusVO getImportStatus(String taskId);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoEditDTO;
|
||||
import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoQueryDTO;
|
||||
import com.ruoyi.info.collection.domain.excel.CcdiEnterpriseBaseInfoExcel;
|
||||
import com.ruoyi.info.collection.domain.vo.CcdiEnterpriseBaseInfoVO;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -22,13 +23,13 @@ public interface ICcdiEnterpriseBaseInfoService {
|
||||
|
||||
CcdiEnterpriseBaseInfoVO selectEnterpriseBaseInfoById(String socialCreditCode);
|
||||
|
||||
int insertEnterpriseBaseInfo(CcdiEnterpriseBaseInfoAddDTO addDTO);
|
||||
int insertEnterpriseBaseInfo(CallerContext caller, CcdiEnterpriseBaseInfoAddDTO addDTO);
|
||||
|
||||
int updateEnterpriseBaseInfo(CcdiEnterpriseBaseInfoEditDTO editDTO);
|
||||
int updateEnterpriseBaseInfo(CallerContext caller, CcdiEnterpriseBaseInfoEditDTO editDTO);
|
||||
|
||||
int deleteEnterpriseBaseInfoByIds(String[] socialCreditCodes);
|
||||
|
||||
List<CcdiEnterpriseBaseInfoExcel> selectEnterpriseBaseInfoListForExport(CcdiEnterpriseBaseInfoQueryDTO queryDTO);
|
||||
|
||||
String importEnterpriseBaseInfo(List<CcdiEnterpriseBaseInfoExcel> excelList);
|
||||
String importEnterpriseBaseInfo(CallerContext caller, List<CcdiEnterpriseBaseInfoExcel> excelList);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ruoyi.info.collection.service;
|
||||
|
||||
import com.ruoyi.info.collection.domain.vo.EnterpriseProfileSyncTaskVO;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 实体库工商信息同步服务。 */
|
||||
public interface IEnterpriseEntitySyncService {
|
||||
void submitSync(CallerContext caller, String socialCreditCode);
|
||||
void submitSyncBatch(CallerContext caller, List<String> socialCreditCodes);
|
||||
void refresh(CallerContext caller, String socialCreditCode);
|
||||
String startBackfill(CallerContext caller);
|
||||
EnterpriseProfileSyncTaskVO getBackfillStatus(String taskId);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.ruoyi.info.collection.service;
|
||||
|
||||
import com.ruoyi.info.collection.domain.CcdiEnterpriseInfoQueryCache;
|
||||
import com.ruoyi.info.collection.domain.model.EnterpriseProfileQueryResult;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
|
||||
/** 统一工商信息查询服务。 */
|
||||
public interface IEnterpriseProfileQueryService {
|
||||
EnterpriseProfileQueryResult query(CallerContext caller, String enterpriseName, boolean forceRefresh);
|
||||
CcdiEnterpriseInfoQueryCache findCache(String enterpriseName);
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import com.ruoyi.info.collection.enums.EnterpriseRiskLevel;
|
||||
import com.ruoyi.info.collection.enums.EnterpriseSource;
|
||||
import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper;
|
||||
import com.ruoyi.info.collection.service.ICcdiEnterpriseBaseInfoImportService;
|
||||
import com.ruoyi.info.collection.service.IEnterpriseEntitySyncService;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
@@ -47,9 +49,12 @@ public class CcdiEnterpriseBaseInfoImportServiceImpl implements ICcdiEnterpriseB
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Resource
|
||||
private IEnterpriseEntitySyncService enterpriseEntitySyncService;
|
||||
|
||||
@Override
|
||||
@Async
|
||||
public void importEnterpriseBaseInfoAsync(List<CcdiEnterpriseBaseInfoExcel> excelList, String taskId, String userName) {
|
||||
public void importEnterpriseBaseInfoAsync(List<CcdiEnterpriseBaseInfoExcel> excelList, String taskId, CallerContext caller) {
|
||||
List<CcdiEnterpriseBaseInfo> successRecords = new ArrayList<>();
|
||||
List<EnterpriseBaseInfoImportFailureVO> failures = new ArrayList<>();
|
||||
Set<String> existingCreditCodes = getExistingCreditCodes(excelList);
|
||||
@@ -57,7 +62,7 @@ public class CcdiEnterpriseBaseInfoImportServiceImpl implements ICcdiEnterpriseB
|
||||
|
||||
for (CcdiEnterpriseBaseInfoExcel excel : excelList) {
|
||||
try {
|
||||
CcdiEnterpriseBaseInfo entity = validateAndBuildEntity(excel, existingCreditCodes, processedCreditCodes, userName);
|
||||
CcdiEnterpriseBaseInfo entity = validateAndBuildEntity(excel, existingCreditCodes, processedCreditCodes, caller.username());
|
||||
successRecords.add(entity);
|
||||
processedCreditCodes.add(entity.getSocialCreditCode());
|
||||
} catch (Exception e) {
|
||||
@@ -70,6 +75,9 @@ public class CcdiEnterpriseBaseInfoImportServiceImpl implements ICcdiEnterpriseB
|
||||
|
||||
if (!successRecords.isEmpty()) {
|
||||
saveBatch(successRecords, 500);
|
||||
enterpriseEntitySyncService.submitSyncBatch(caller, successRecords.stream()
|
||||
.map(CcdiEnterpriseBaseInfo::getSocialCreditCode)
|
||||
.toList());
|
||||
}
|
||||
|
||||
if (!failures.isEmpty()) {
|
||||
|
||||
@@ -2,10 +2,10 @@ package com.ruoyi.info.collection.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.info.collection.domain.CcdiCustEnterpriseRelation;
|
||||
import com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo;
|
||||
import com.ruoyi.info.collection.domain.CcdiEnterpriseInfoQueryCache;
|
||||
import com.ruoyi.info.collection.domain.CcdiIntermediaryEnterpriseRelation;
|
||||
import com.ruoyi.info.collection.domain.CcdiStaffEnterpriseRelation;
|
||||
import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoAddDTO;
|
||||
@@ -13,20 +13,27 @@ import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoEditDTO;
|
||||
import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoQueryDTO;
|
||||
import com.ruoyi.info.collection.domain.excel.CcdiEnterpriseBaseInfoExcel;
|
||||
import com.ruoyi.info.collection.domain.vo.CcdiEnterpriseBaseInfoVO;
|
||||
import com.ruoyi.info.collection.domain.vo.EnterpriseShareholderVO;
|
||||
import com.ruoyi.info.collection.enums.DataSource;
|
||||
import com.ruoyi.info.collection.enums.EnterpriseRiskLevel;
|
||||
import com.ruoyi.info.collection.enums.EnterpriseSource;
|
||||
import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper;
|
||||
import com.ruoyi.info.collection.mapper.CcdiEnterpriseShareholderMapper;
|
||||
import com.ruoyi.info.collection.mapper.CcdiCustEnterpriseRelationMapper;
|
||||
import com.ruoyi.info.collection.mapper.CcdiIntermediaryEnterpriseRelationMapper;
|
||||
import com.ruoyi.info.collection.mapper.CcdiStaffEnterpriseRelationMapper;
|
||||
import com.ruoyi.info.collection.service.ICcdiEnterpriseBaseInfoImportService;
|
||||
import com.ruoyi.info.collection.service.ICcdiEnterpriseBaseInfoService;
|
||||
import com.ruoyi.info.collection.service.IEnterpriseEntitySyncService;
|
||||
import com.ruoyi.info.collection.service.IEnterpriseProfileQueryService;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -62,6 +69,15 @@ public class CcdiEnterpriseBaseInfoServiceImpl implements ICcdiEnterpriseBaseInf
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Resource
|
||||
private CcdiEnterpriseShareholderMapper enterpriseShareholderMapper;
|
||||
|
||||
@Resource
|
||||
private IEnterpriseProfileQueryService enterpriseProfileQueryService;
|
||||
|
||||
@Resource
|
||||
private IEnterpriseEntitySyncService enterpriseEntitySyncService;
|
||||
|
||||
@Override
|
||||
public Page<CcdiEnterpriseBaseInfoVO> selectEnterpriseBaseInfoPage(Page<CcdiEnterpriseBaseInfoVO> page,
|
||||
CcdiEnterpriseBaseInfoQueryDTO queryDTO) {
|
||||
@@ -76,12 +92,21 @@ public class CcdiEnterpriseBaseInfoServiceImpl implements ICcdiEnterpriseBaseInf
|
||||
}
|
||||
CcdiEnterpriseBaseInfoVO vo = new CcdiEnterpriseBaseInfoVO();
|
||||
BeanUtils.copyProperties(entity, vo);
|
||||
vo.setShareholders(enterpriseShareholderMapper.selectByCreditCode(socialCreditCode).stream().map(item -> {
|
||||
EnterpriseShareholderVO shareholder = new EnterpriseShareholderVO();
|
||||
BeanUtils.copyProperties(item, shareholder);
|
||||
return shareholder;
|
||||
}).toList());
|
||||
CcdiEnterpriseInfoQueryCache cache = enterpriseProfileQueryService.findCache(entity.getEnterpriseName());
|
||||
vo.setEnterpriseCacheValidDate(cache == null ? null : cache.getValidDate());
|
||||
vo.setCanRefreshEnterpriseProfile(entity.getEnterpriseSyncTime() == null || cache == null
|
||||
|| cache.getValidDate() == null || !cache.getValidDate().after(new java.util.Date()));
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public int insertEnterpriseBaseInfo(CcdiEnterpriseBaseInfoAddDTO addDTO) {
|
||||
public int insertEnterpriseBaseInfo(CallerContext caller, CcdiEnterpriseBaseInfoAddDTO addDTO) {
|
||||
if (enterpriseBaseInfoMapper.selectById(addDTO.getSocialCreditCode()) != null) {
|
||||
throw new RuntimeException("该统一社会信用代码已存在");
|
||||
}
|
||||
@@ -91,12 +116,16 @@ public class CcdiEnterpriseBaseInfoServiceImpl implements ICcdiEnterpriseBaseInf
|
||||
BeanUtils.copyProperties(addDTO, entity);
|
||||
entity.setStatus(trimToNull(addDTO.getStatus()));
|
||||
entity.setDataSource(DataSource.MANUAL.getCode());
|
||||
return enterpriseBaseInfoMapper.insert(entity);
|
||||
int rows = enterpriseBaseInfoMapper.insert(entity);
|
||||
if (rows > 0) {
|
||||
submitSyncAfterCommit(caller, entity.getSocialCreditCode());
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public int updateEnterpriseBaseInfo(CcdiEnterpriseBaseInfoEditDTO editDTO) {
|
||||
public int updateEnterpriseBaseInfo(CallerContext caller, CcdiEnterpriseBaseInfoEditDTO editDTO) {
|
||||
CcdiEnterpriseBaseInfo existing = enterpriseBaseInfoMapper.selectById(editDTO.getSocialCreditCode());
|
||||
if (existing == null) {
|
||||
throw new RuntimeException("实体库记录不存在");
|
||||
@@ -107,7 +136,11 @@ public class CcdiEnterpriseBaseInfoServiceImpl implements ICcdiEnterpriseBaseInf
|
||||
BeanUtils.copyProperties(editDTO, entity);
|
||||
entity.setStatus(trimToNull(editDTO.getStatus()));
|
||||
entity.setDataSource(existing.getDataSource());
|
||||
return enterpriseBaseInfoMapper.updateById(entity);
|
||||
int rows = enterpriseBaseInfoMapper.updateById(entity);
|
||||
if (rows > 0 && !java.util.Objects.equals(trimToNull(existing.getEnterpriseName()), trimToNull(entity.getEnterpriseName()))) {
|
||||
submitSyncAfterCommit(caller, entity.getSocialCreditCode());
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -118,6 +151,7 @@ public class CcdiEnterpriseBaseInfoServiceImpl implements ICcdiEnterpriseBaseInf
|
||||
}
|
||||
for (String socialCreditCode : socialCreditCodes) {
|
||||
validateDeleteRelations(socialCreditCode);
|
||||
enterpriseShareholderMapper.deleteByCreditCode(socialCreditCode);
|
||||
}
|
||||
return enterpriseBaseInfoMapper.deleteBatchIds(List.of(socialCreditCodes));
|
||||
}
|
||||
@@ -134,7 +168,7 @@ public class CcdiEnterpriseBaseInfoServiceImpl implements ICcdiEnterpriseBaseInf
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public String importEnterpriseBaseInfo(List<CcdiEnterpriseBaseInfoExcel> excelList) {
|
||||
public String importEnterpriseBaseInfo(CallerContext caller, List<CcdiEnterpriseBaseInfoExcel> excelList) {
|
||||
String taskId = UUID.randomUUID().toString();
|
||||
String statusKey = "import:enterpriseBaseInfo:" + taskId;
|
||||
|
||||
@@ -151,10 +185,24 @@ public class CcdiEnterpriseBaseInfoServiceImpl implements ICcdiEnterpriseBaseInf
|
||||
redisTemplate.opsForHash().putAll(statusKey, statusData);
|
||||
redisTemplate.expire(statusKey, 7, TimeUnit.DAYS);
|
||||
|
||||
enterpriseBaseInfoImportService.importEnterpriseBaseInfoAsync(excelList, taskId, SecurityUtils.getUsername());
|
||||
enterpriseBaseInfoImportService.importEnterpriseBaseInfoAsync(
|
||||
excelList, taskId, caller);
|
||||
return taskId;
|
||||
}
|
||||
|
||||
private void submitSyncAfterCommit(CallerContext caller, String socialCreditCode) {
|
||||
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
enterpriseEntitySyncService.submitSync(caller, socialCreditCode);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
enterpriseEntitySyncService.submitSync(caller, socialCreditCode);
|
||||
}
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<CcdiEnterpriseBaseInfo> buildQueryWrapper(CcdiEnterpriseBaseInfoQueryDTO queryDTO) {
|
||||
LambdaQueryWrapper<CcdiEnterpriseBaseInfo> wrapper = new LambdaQueryWrapper<>();
|
||||
if (queryDTO == null) {
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
package com.ruoyi.info.collection.service.impl;
|
||||
|
||||
import com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo;
|
||||
import com.ruoyi.info.collection.domain.CcdiEnterpriseInfoQueryCache;
|
||||
import com.ruoyi.info.collection.domain.CcdiEnterpriseShareholder;
|
||||
import com.ruoyi.info.collection.domain.model.EnterpriseProfile;
|
||||
import com.ruoyi.info.collection.domain.model.EnterpriseProfileQueryResult;
|
||||
import com.ruoyi.info.collection.domain.model.EnterpriseShareholderProfile;
|
||||
import com.ruoyi.info.collection.domain.vo.EnterpriseProfileSyncTaskVO;
|
||||
import com.ruoyi.info.collection.enums.DataSource;
|
||||
import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper;
|
||||
import com.ruoyi.info.collection.mapper.CcdiEnterpriseShareholderMapper;
|
||||
import com.ruoyi.info.collection.service.IEnterpriseEntitySyncService;
|
||||
import com.ruoyi.info.collection.service.IEnterpriseProfileQueryService;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** 实体库工商信息同步服务实现。 */
|
||||
@Service
|
||||
public class EnterpriseEntitySyncServiceImpl implements IEnterpriseEntitySyncService {
|
||||
|
||||
private static final String TASK_PREFIX = "enterprise:profile:backfill:";
|
||||
|
||||
@Resource
|
||||
private CcdiEnterpriseBaseInfoMapper enterpriseMapper;
|
||||
|
||||
@Resource
|
||||
private CcdiEnterpriseShareholderMapper shareholderMapper;
|
||||
|
||||
@Resource
|
||||
private IEnterpriseProfileQueryService queryService;
|
||||
|
||||
@Resource
|
||||
private TransactionTemplate transactionTemplate;
|
||||
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Resource
|
||||
@Qualifier("enterpriseProfileExecutor")
|
||||
private Executor enterpriseProfileExecutor;
|
||||
|
||||
@Override
|
||||
public void submitSync(CallerContext caller, String socialCreditCode) {
|
||||
submitSyncBatch(caller, List.of(socialCreditCode));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void submitSyncBatch(CallerContext caller, List<String> socialCreditCodes) {
|
||||
if (socialCreditCodes == null || socialCreditCodes.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<String> normalizedCodes = socialCreditCodes.stream().filter(StringUtils::hasText).distinct().toList();
|
||||
if (normalizedCodes.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<CcdiEnterpriseBaseInfo> entities = enterpriseMapper.selectBatchIds(normalizedCodes);
|
||||
Map<String, List<CcdiEnterpriseBaseInfo>> groups = entities.stream()
|
||||
.filter(item -> StringUtils.hasText(item.getEnterpriseName()))
|
||||
.collect(Collectors.groupingBy(item -> item.getEnterpriseName().trim(), LinkedHashMap::new, Collectors.toList()));
|
||||
groups.forEach((name, sameNameEntities) -> CompletableFuture.runAsync(() -> {
|
||||
EnterpriseProfileQueryResult result = queryService.query(caller, name, false);
|
||||
sameNameEntities.stream()
|
||||
.filter(item -> result.profile().getCreditCode().equals(item.getSocialCreditCode()))
|
||||
.findFirst()
|
||||
.ifPresent(item -> applyProfile(item, result, caller.username()));
|
||||
}, enterpriseProfileExecutor));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refresh(CallerContext caller, String socialCreditCode) {
|
||||
CcdiEnterpriseBaseInfo entity = requireEntity(socialCreditCode);
|
||||
CcdiEnterpriseInfoQueryCache cache = queryService.findCache(entity.getEnterpriseName());
|
||||
boolean cacheValid = cache != null && cache.getValidDate() != null && cache.getValidDate().after(new Date());
|
||||
if (entity.getEnterpriseSyncTime() != null && cacheValid) {
|
||||
throw new IllegalStateException("工商缓存仍在有效期内,无需重新查询");
|
||||
}
|
||||
SyncOutcome outcome = syncByCreditCode(caller, socialCreditCode, !cacheValid);
|
||||
if (outcome != SyncOutcome.SUCCESS) {
|
||||
throw new IllegalStateException("工商信用代码与实体主键不一致");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String startBackfill(CallerContext caller) {
|
||||
String taskId = UUID.randomUUID().toString().replace("-", "");
|
||||
List<CcdiEnterpriseBaseInfo> candidates = enterpriseMapper.selectEnterpriseProfileCandidates();
|
||||
Map<String, List<CcdiEnterpriseBaseInfo>> groups = candidates.stream()
|
||||
.filter(item -> StringUtils.hasText(item.getEnterpriseName()))
|
||||
.collect(Collectors.groupingBy(item -> item.getEnterpriseName().trim(), LinkedHashMap::new, Collectors.toList()));
|
||||
initializeTask(taskId, groups.size());
|
||||
CompletableFuture.runAsync(() -> runBackfill(taskId, caller, groups));
|
||||
return taskId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EnterpriseProfileSyncTaskVO getBackfillStatus(String taskId) {
|
||||
Map<Object, Object> status = redisTemplate.opsForHash().entries(taskKey(taskId));
|
||||
if (status.isEmpty()) {
|
||||
throw new IllegalArgumentException("任务不存在或已过期");
|
||||
}
|
||||
EnterpriseProfileSyncTaskVO vo = new EnterpriseProfileSyncTaskVO();
|
||||
vo.setTaskId(string(status.get("taskId")));
|
||||
vo.setStatus(string(status.get("status")));
|
||||
vo.setTotalCount(number(status.get("totalCount")));
|
||||
vo.setCompletedCount(number(status.get("completedCount")));
|
||||
vo.setSuccessCount(number(status.get("successCount")));
|
||||
vo.setSkippedCount(number(status.get("skippedCount")));
|
||||
vo.setFailureCount(number(status.get("failureCount")));
|
||||
vo.setMessage(string(status.get("message")));
|
||||
return vo;
|
||||
}
|
||||
|
||||
private void runBackfill(String taskId, CallerContext caller,
|
||||
Map<String, List<CcdiEnterpriseBaseInfo>> groups) {
|
||||
AtomicInteger completed = new AtomicInteger();
|
||||
AtomicInteger success = new AtomicInteger();
|
||||
AtomicInteger skipped = new AtomicInteger();
|
||||
AtomicInteger failure = new AtomicInteger();
|
||||
|
||||
List<Map.Entry<String, List<CcdiEnterpriseBaseInfo>>> entries = new ArrayList<>(groups.entrySet());
|
||||
for (int start = 0; start < entries.size(); start += 100) {
|
||||
int end = Math.min(start + 100, entries.size());
|
||||
List<CompletableFuture<Void>> futures = entries.subList(start, end).stream()
|
||||
.map(entry -> CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
EnterpriseProfileQueryResult result = queryService.query(caller, entry.getKey(), false);
|
||||
CcdiEnterpriseBaseInfo match = entry.getValue().stream()
|
||||
.filter(item -> result.profile().getCreditCode().equals(item.getSocialCreditCode()))
|
||||
.findFirst().orElse(null);
|
||||
if (match == null) {
|
||||
skipped.incrementAndGet();
|
||||
} else {
|
||||
applyProfile(match, result, caller.username());
|
||||
success.incrementAndGet();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
failure.incrementAndGet();
|
||||
} finally {
|
||||
completed.incrementAndGet();
|
||||
updateTask(taskId, "PROCESSING", completed.get(), success.get(), skipped.get(), failure.get());
|
||||
}
|
||||
}, enterpriseProfileExecutor)).toList();
|
||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
||||
}
|
||||
updateTask(taskId, "COMPLETED", completed.get(), success.get(), skipped.get(), failure.get());
|
||||
}
|
||||
|
||||
private SyncOutcome syncByCreditCode(CallerContext caller, String socialCreditCode,
|
||||
boolean forceRefresh) {
|
||||
CcdiEnterpriseBaseInfo entity = requireEntity(socialCreditCode);
|
||||
EnterpriseProfileQueryResult result = queryService.query(caller, entity.getEnterpriseName(), forceRefresh);
|
||||
if (!result.profile().getCreditCode().equals(entity.getSocialCreditCode())) {
|
||||
return SyncOutcome.SKIPPED;
|
||||
}
|
||||
applyProfile(entity, result, caller.username());
|
||||
return SyncOutcome.SUCCESS;
|
||||
}
|
||||
|
||||
private void applyProfile(CcdiEnterpriseBaseInfo existing, EnterpriseProfileQueryResult result, String username) {
|
||||
transactionTemplate.executeWithoutResult(status -> {
|
||||
EnterpriseProfile profile = result.profile();
|
||||
Date now = new Date();
|
||||
CcdiEnterpriseBaseInfo update = new CcdiEnterpriseBaseInfo();
|
||||
update.setSocialCreditCode(existing.getSocialCreditCode());
|
||||
update.setEnterpriseName(existing.getEnterpriseName().trim());
|
||||
update.setEnterpriseType(profile.getOrganizationTypeName());
|
||||
update.setIndustryClass(profile.getIndustryCode());
|
||||
update.setIndustryName(profile.getIndustryName());
|
||||
update.setEstablishDate(toDate(profile.getEstablishDate()));
|
||||
update.setRegisterAddress(profile.getRegisterAddress());
|
||||
update.setLegalRepresentative(profile.getLegalRepresentative());
|
||||
update.setRegisteredCapital(profile.getRegisteredCapital());
|
||||
update.setRegisteredCapitalUnit(profile.getRegisteredCapitalUnit());
|
||||
update.setRegisterDate(toDate(profile.getRegisterDate()));
|
||||
update.setRegionCode(profile.getRegionCode());
|
||||
update.setRegionName(profile.getRegionName());
|
||||
update.setEmployeeCount(profile.getEmployeeCount());
|
||||
update.setCacheInfoId(result.cacheInfoId());
|
||||
update.setEnterpriseSyncTime(now);
|
||||
update.setDataSource(DataSource.API.getCode());
|
||||
update.setUpdatedBy(username);
|
||||
setTopFive(update, profile.getShareholders());
|
||||
if (enterpriseMapper.updateEnterpriseProfile(update) != 1) {
|
||||
throw new IllegalStateException("实体工商信息更新失败");
|
||||
}
|
||||
|
||||
shareholderMapper.deleteByCreditCode(existing.getSocialCreditCode());
|
||||
List<CcdiEnterpriseShareholder> shareholders = profile.getShareholders().stream()
|
||||
.map(item -> toEntityShareholder(existing.getSocialCreditCode(), item, result.cacheInfoId(), now, username))
|
||||
.toList();
|
||||
if (!shareholders.isEmpty()) {
|
||||
shareholderMapper.insertBatch(shareholders);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private CcdiEnterpriseShareholder toEntityShareholder(String creditCode,
|
||||
EnterpriseShareholderProfile item,
|
||||
Long cacheInfoId,
|
||||
Date now,
|
||||
String username) {
|
||||
CcdiEnterpriseShareholder shareholder = new CcdiEnterpriseShareholder();
|
||||
shareholder.setSocialCreditCode(creditCode);
|
||||
shareholder.setShareholderSeq(item.getSequence());
|
||||
shareholder.setShareholderName(item.getShareholderName());
|
||||
shareholder.setStockPercent(item.getStockPercent());
|
||||
shareholder.setSubscribedCapital(item.getSubscribedCapital());
|
||||
shareholder.setCapitalUnit(item.getCapitalUnit());
|
||||
shareholder.setCacheInfoId(cacheInfoId);
|
||||
shareholder.setSyncTime(now);
|
||||
shareholder.setCreateBy(username);
|
||||
shareholder.setCreateTime(now);
|
||||
shareholder.setUpdateBy(username);
|
||||
shareholder.setUpdateTime(now);
|
||||
return shareholder;
|
||||
}
|
||||
|
||||
private void setTopFive(CcdiEnterpriseBaseInfo entity, List<EnterpriseShareholderProfile> shareholders) {
|
||||
String[] names = new String[5];
|
||||
for (int i = 0; i < Math.min(names.length, shareholders.size()); i++) {
|
||||
names[i] = shareholders.get(i).getShareholderName();
|
||||
}
|
||||
entity.setShareholder1(names[0]);
|
||||
entity.setShareholder2(names[1]);
|
||||
entity.setShareholder3(names[2]);
|
||||
entity.setShareholder4(names[3]);
|
||||
entity.setShareholder5(names[4]);
|
||||
}
|
||||
|
||||
private CcdiEnterpriseBaseInfo requireEntity(String socialCreditCode) {
|
||||
CcdiEnterpriseBaseInfo entity = enterpriseMapper.selectById(socialCreditCode);
|
||||
if (entity == null) {
|
||||
throw new IllegalArgumentException("实体库记录不存在");
|
||||
}
|
||||
if (!StringUtils.hasText(entity.getEnterpriseName())) {
|
||||
throw new IllegalArgumentException("企业名称不能为空");
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
private Date toDate(java.time.LocalDate value) {
|
||||
return value == null ? null : Date.from(value.atStartOfDay(ZoneId.systemDefault()).toInstant());
|
||||
}
|
||||
|
||||
private void initializeTask(String taskId, int totalCount) {
|
||||
Map<String, Object> values = new LinkedHashMap<>();
|
||||
values.put("taskId", taskId);
|
||||
values.put("status", "PROCESSING");
|
||||
values.put("totalCount", totalCount);
|
||||
values.put("completedCount", 0);
|
||||
values.put("successCount", 0);
|
||||
values.put("skippedCount", 0);
|
||||
values.put("failureCount", 0);
|
||||
values.put("message", "正在补全工商信息");
|
||||
redisTemplate.opsForHash().putAll(taskKey(taskId), values);
|
||||
redisTemplate.expire(taskKey(taskId), 7, TimeUnit.DAYS);
|
||||
}
|
||||
|
||||
private void updateTask(String taskId, String status, int completed, int success, int skipped, int failure) {
|
||||
Map<String, Object> values = new LinkedHashMap<>();
|
||||
values.put("status", status);
|
||||
values.put("completedCount", completed);
|
||||
values.put("successCount", success);
|
||||
values.put("skippedCount", skipped);
|
||||
values.put("failureCount", failure);
|
||||
values.put("message", "COMPLETED".equals(status) ? "工商信息补全完成" : "正在补全工商信息");
|
||||
redisTemplate.opsForHash().putAll(taskKey(taskId), values);
|
||||
}
|
||||
|
||||
private String taskKey(String taskId) {
|
||||
return TASK_PREFIX + taskId;
|
||||
}
|
||||
|
||||
private String string(Object value) {
|
||||
return value == null ? null : value.toString();
|
||||
}
|
||||
|
||||
private Integer number(Object value) {
|
||||
return value == null ? 0 : Integer.valueOf(value.toString());
|
||||
}
|
||||
|
||||
private enum SyncOutcome {
|
||||
SUCCESS, SKIPPED
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.ruoyi.info.collection.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ruoyi.info.collection.domain.CcdiEnterpriseInfoQueryCache;
|
||||
import com.ruoyi.info.collection.domain.model.EnterpriseProfile;
|
||||
import com.ruoyi.info.collection.domain.model.EnterpriseProfileQueryResult;
|
||||
import com.ruoyi.info.collection.mapper.CcdiEnterpriseInfoQueryCacheMapper;
|
||||
import com.ruoyi.info.collection.service.IEnterpriseProfileQueryService;
|
||||
import com.ruoyi.info.collection.service.support.XinhuaEnterpriseProfileParser;
|
||||
import com.ruoyi.lsfx.client.XinhuaEnterpriseClient;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import com.ruoyi.lsfx.domain.response.XinhuaEnterpriseQueryResult;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Date;
|
||||
|
||||
/** 统一工商信息查询服务实现。 */
|
||||
@Service
|
||||
public class EnterpriseProfileQueryServiceImpl implements IEnterpriseProfileQueryService {
|
||||
|
||||
public static final String QUERY_TYPE = "EnterpriseProfile";
|
||||
private static final int CACHE_VALID_DAYS = 180;
|
||||
|
||||
@Resource
|
||||
private CcdiEnterpriseInfoQueryCacheMapper cacheMapper;
|
||||
|
||||
@Resource
|
||||
private XinhuaEnterpriseClient xinhuaEnterpriseClient;
|
||||
|
||||
@Resource
|
||||
private XinhuaEnterpriseProfileParser profileParser;
|
||||
|
||||
@Resource
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
public EnterpriseProfileQueryResult query(CallerContext caller, String enterpriseName, boolean forceRefresh) {
|
||||
String normalizedName = normalize(enterpriseName);
|
||||
if (!StringUtils.hasText(normalizedName)) {
|
||||
throw new IllegalArgumentException("企业名称不能为空");
|
||||
}
|
||||
Date now = new Date();
|
||||
if (!forceRefresh) {
|
||||
CcdiEnterpriseInfoQueryCache cache = cacheMapper.selectValidCache(normalizedName, QUERY_TYPE, now);
|
||||
if (cache != null) {
|
||||
return fromCache(cache);
|
||||
}
|
||||
}
|
||||
|
||||
XinhuaEnterpriseQueryResult external = xinhuaEnterpriseClient.query(caller, normalizedName);
|
||||
EnterpriseProfile profile = profileParser.parse(external.profile());
|
||||
Date successTime = new Date();
|
||||
Date validDate = Date.from(LocalDateTime.ofInstant(successTime.toInstant(), ZoneId.systemDefault())
|
||||
.plusDays(CACHE_VALID_DAYS)
|
||||
.atZone(ZoneId.systemDefault()).toInstant());
|
||||
|
||||
CcdiEnterpriseInfoQueryCache cache = new CcdiEnterpriseInfoQueryCache();
|
||||
cache.setQueryParam(normalizedName);
|
||||
cache.setQueryType(QUERY_TYPE);
|
||||
cache.setQueryResult(external.rawJson());
|
||||
cache.setCreatedDate(successTime);
|
||||
cache.setValidDate(validDate);
|
||||
cacheMapper.upsertCache(cache);
|
||||
CcdiEnterpriseInfoQueryCache saved = cacheMapper.selectCache(normalizedName, QUERY_TYPE);
|
||||
return new EnterpriseProfileQueryResult(profile, saved.getInfoId(), saved.getValidDate(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CcdiEnterpriseInfoQueryCache findCache(String enterpriseName) {
|
||||
String normalizedName = normalize(enterpriseName);
|
||||
return StringUtils.hasText(normalizedName) ? cacheMapper.selectCache(normalizedName, QUERY_TYPE) : null;
|
||||
}
|
||||
|
||||
private EnterpriseProfileQueryResult fromCache(CcdiEnterpriseInfoQueryCache cache) {
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(cache.getQueryResult());
|
||||
EnterpriseProfile profile = profileParser.parse(root.path("data").path("mappingOutputFields"));
|
||||
return new EnterpriseProfileQueryResult(profile, cache.getInfoId(), cache.getValidDate(), true);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("工商缓存内容无法解析: infoId=" + cache.getInfoId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return value == null ? null : value.trim();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -4,10 +4,14 @@ import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo;
|
||||
import com.ruoyi.info.collection.enums.EnterpriseSource;
|
||||
import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper;
|
||||
import com.ruoyi.info.collection.service.IEnterpriseEntitySyncService;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -27,6 +31,9 @@ public class EnterpriseAutoFillService {
|
||||
@Resource
|
||||
private CcdiEnterpriseBaseInfoMapper enterpriseBaseInfoMapper;
|
||||
|
||||
@Resource
|
||||
private IEnterpriseEntitySyncService enterpriseEntitySyncService;
|
||||
|
||||
public record EnterpriseFillItem(
|
||||
String socialCreditCode,
|
||||
String enterpriseName,
|
||||
@@ -63,6 +70,7 @@ public class EnterpriseAutoFillService {
|
||||
return;
|
||||
}
|
||||
insertBatchIgnoreDuplicate(missingEntities);
|
||||
submitSyncAfterCommit(missingEntities);
|
||||
}
|
||||
|
||||
private Map<String, EnterpriseFillItem> normalizeItems(List<EnterpriseFillItem> items) {
|
||||
@@ -121,6 +129,23 @@ public class EnterpriseAutoFillService {
|
||||
}
|
||||
}
|
||||
|
||||
private void submitSyncAfterCommit(List<CcdiEnterpriseBaseInfo> entities) {
|
||||
String username = entities.stream().map(CcdiEnterpriseBaseInfo::getCreatedBy)
|
||||
.filter(StringUtils::isNotEmpty).findFirst().orElse("system");
|
||||
CallerContext caller = CallerContext.of(null, username);
|
||||
List<String> creditCodes = entities.stream().map(CcdiEnterpriseBaseInfo::getSocialCreditCode).toList();
|
||||
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
enterpriseEntitySyncService.submitSyncBatch(caller, creditCodes);
|
||||
return;
|
||||
}
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
enterpriseEntitySyncService.submitSyncBatch(caller, creditCodes);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
if (StringUtils.isEmpty(value)) {
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.ruoyi.info.collection.service.support;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.ruoyi.info.collection.domain.model.EnterpriseProfile;
|
||||
import com.ruoyi.info.collection.domain.model.EnterpriseShareholderProfile;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/** 将新华社 mappingOutputFields 解析为统一工商结构。 */
|
||||
@Component
|
||||
public class XinhuaEnterpriseProfileParser {
|
||||
|
||||
public EnterpriseProfile parse(JsonNode node) {
|
||||
if (node == null || !node.isObject() || node.isEmpty()) {
|
||||
throw new IllegalArgumentException("工商信息对象不能为空");
|
||||
}
|
||||
EnterpriseProfile profile = new EnterpriseProfile();
|
||||
profile.setCreditCode(text(node, "creditCode"));
|
||||
profile.setEnterpriseName(text(node, "enterpriseName"));
|
||||
|
||||
JsonNode capital = node.path("regCapitalAmount");
|
||||
profile.setRegisteredCapital(decimal(text(capital, "regist_capi_value"), false));
|
||||
profile.setRegisteredCapitalUnit(text(capital, "regist_capi_unit"));
|
||||
profile.setRegisterDate(date(text(node, "estiblishTime")));
|
||||
profile.setEstablishDate(date(text(node, "fromTime")));
|
||||
|
||||
JsonNode industry = node.path("industry");
|
||||
profile.setIndustryCode(text(industry, "industry_code"));
|
||||
profile.setIndustryName(text(industry, "industry"));
|
||||
|
||||
JsonNode orgType = node.path("companyOrgType");
|
||||
if (orgType.isObject()) {
|
||||
profile.setOrganizationTypeCode(text(orgType, "econ_kind_code"));
|
||||
profile.setOrganizationTypeName(text(orgType, "econ_kind"));
|
||||
} else if (orgType.isTextual()) {
|
||||
profile.setOrganizationTypeName(normalize(orgType.asText()));
|
||||
}
|
||||
|
||||
JsonNode city = node.path("baseCity");
|
||||
profile.setRegionCode(text(city, "province_code"));
|
||||
profile.setRegionName(regionName(city));
|
||||
profile.setRegisterAddress(text(node, "regLocation"));
|
||||
profile.setEmployeeCount(integer(node.get("employeeCount")));
|
||||
profile.setLegalRepresentative(text(node, "legalPersonName"));
|
||||
profile.setShareholders(shareholders(node.path("holders")));
|
||||
return profile;
|
||||
}
|
||||
|
||||
private List<EnterpriseShareholderProfile> shareholders(JsonNode holders) {
|
||||
List<EnterpriseShareholderProfile> result = new ArrayList<>();
|
||||
if (!holders.isArray()) {
|
||||
return result;
|
||||
}
|
||||
int sequence = 1;
|
||||
for (JsonNode holder : holders) {
|
||||
String name = text(holder, "stock_name");
|
||||
if (!StringUtils.hasText(name)) {
|
||||
continue;
|
||||
}
|
||||
EnterpriseShareholderProfile item = new EnterpriseShareholderProfile();
|
||||
item.setSequence(sequence++);
|
||||
item.setShareholderName(name);
|
||||
item.setStockPercent(decimal(text(holder, "stock_percent"), true));
|
||||
item.setSubscribedCapital(decimal(text(holder, "should_capi"), false));
|
||||
item.setCapitalUnit("万元");
|
||||
result.add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String regionName(JsonNode city) {
|
||||
Set<String> values = new LinkedHashSet<>();
|
||||
add(values, text(city, "province"));
|
||||
add(values, text(city, "city"));
|
||||
add(values, text(city, "county"));
|
||||
return values.isEmpty() ? null : String.join("", values);
|
||||
}
|
||||
|
||||
private void add(Set<String> values, String value) {
|
||||
if (StringUtils.hasText(value)) {
|
||||
values.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
private String text(JsonNode node, String field) {
|
||||
if (node == null || !node.isObject()) {
|
||||
return null;
|
||||
}
|
||||
JsonNode value = node.get(field);
|
||||
return value == null || value.isNull() ? null : normalize(value.asText());
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return value == null ? null : value.trim();
|
||||
}
|
||||
|
||||
private BigDecimal decimal(String value, boolean percentage) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return null;
|
||||
}
|
||||
String normalized = value.replace(",", "").trim();
|
||||
if (percentage && normalized.endsWith("%")) {
|
||||
normalized = normalized.substring(0, normalized.length() - 1).trim();
|
||||
}
|
||||
try {
|
||||
return new BigDecimal(normalized);
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Integer integer(JsonNode value) {
|
||||
if (value == null || value.isNull()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Integer.valueOf(value.asText().trim());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private LocalDate date(String value) {
|
||||
if (!StringUtils.hasText(value) || value.length() < 10) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return LocalDate.parse(value.substring(0, 10));
|
||||
} catch (DateTimeParseException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="establishDate" column="establish_date"/>
|
||||
<result property="registerAddress" column="register_address"/>
|
||||
<result property="legalRepresentative" column="legal_representative"/>
|
||||
<result property="registeredCapital" column="registered_capital"/>
|
||||
<result property="registeredCapitalUnit" column="registered_capital_unit"/>
|
||||
<result property="registerDate" column="register_date"/>
|
||||
<result property="regionCode" column="region_code"/>
|
||||
<result property="regionName" column="region_name"/>
|
||||
<result property="employeeCount" column="employee_count"/>
|
||||
<result property="cacheInfoId" column="cache_info_id"/>
|
||||
<result property="enterpriseSyncTime" column="enterprise_sync_time"/>
|
||||
<result property="legalCertType" column="legal_cert_type"/>
|
||||
<result property="legalCertNo" column="legal_cert_no"/>
|
||||
<result property="shareholder1" column="shareholder1"/>
|
||||
@@ -39,6 +47,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
establish_date,
|
||||
register_address,
|
||||
legal_representative,
|
||||
registered_capital,
|
||||
registered_capital_unit,
|
||||
register_date,
|
||||
region_code,
|
||||
region_name,
|
||||
employee_count,
|
||||
cache_info_id,
|
||||
enterprise_sync_time,
|
||||
legal_cert_type,
|
||||
legal_cert_no,
|
||||
shareholder1,
|
||||
@@ -178,4 +194,39 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</foreach>
|
||||
</update>
|
||||
|
||||
<update id="updateEnterpriseProfile" parameterType="com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo">
|
||||
update ccdi_enterprise_base_info
|
||||
set enterprise_type = #{enterpriseType},
|
||||
industry_class = #{industryClass},
|
||||
industry_name = #{industryName},
|
||||
establish_date = #{establishDate},
|
||||
register_address = #{registerAddress},
|
||||
legal_representative = #{legalRepresentative},
|
||||
registered_capital = #{registeredCapital},
|
||||
registered_capital_unit = #{registeredCapitalUnit},
|
||||
register_date = #{registerDate},
|
||||
region_code = #{regionCode},
|
||||
region_name = #{regionName},
|
||||
employee_count = #{employeeCount},
|
||||
cache_info_id = #{cacheInfoId},
|
||||
enterprise_sync_time = #{enterpriseSyncTime},
|
||||
shareholder1 = #{shareholder1},
|
||||
shareholder2 = #{shareholder2},
|
||||
shareholder3 = #{shareholder3},
|
||||
shareholder4 = #{shareholder4},
|
||||
shareholder5 = #{shareholder5},
|
||||
data_source = #{dataSource},
|
||||
updated_by = #{updatedBy},
|
||||
update_time = now()
|
||||
where social_credit_code = #{socialCreditCode}
|
||||
and trim(enterprise_name) = trim(#{enterpriseName})
|
||||
</update>
|
||||
|
||||
<select id="selectEnterpriseProfileCandidates" resultType="com.ruoyi.info.collection.domain.CcdiEnterpriseBaseInfo">
|
||||
select social_credit_code, enterprise_name, cache_info_id, enterprise_sync_time
|
||||
from ccdi_enterprise_base_info
|
||||
where enterprise_name is not null and trim(enterprise_name) != ''
|
||||
order by enterprise_name, social_credit_code
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.info.collection.mapper.CcdiEnterpriseInfoQueryCacheMapper">
|
||||
<select id="selectValidCache" resultType="com.ruoyi.info.collection.domain.CcdiEnterpriseInfoQueryCache">
|
||||
select info_id, query_param, query_result, query_type, created_date, valid_date
|
||||
from ccdi_enterpriseinfo_query_cache
|
||||
where query_param = #{queryParam}
|
||||
and query_type = #{queryType}
|
||||
and valid_date > #{now}
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectCache" resultType="com.ruoyi.info.collection.domain.CcdiEnterpriseInfoQueryCache">
|
||||
select info_id, query_param, query_result, query_type, created_date, valid_date
|
||||
from ccdi_enterpriseinfo_query_cache
|
||||
where query_param = #{queryParam}
|
||||
and query_type = #{queryType}
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<insert id="upsertCache" parameterType="com.ruoyi.info.collection.domain.CcdiEnterpriseInfoQueryCache">
|
||||
insert into ccdi_enterpriseinfo_query_cache
|
||||
(query_param, query_result, query_type, created_date, valid_date)
|
||||
values
|
||||
(#{queryParam}, #{queryResult}, #{queryType}, #{createdDate}, #{validDate})
|
||||
on duplicate key update
|
||||
query_result = values(query_result),
|
||||
created_date = values(created_date),
|
||||
valid_date = values(valid_date)
|
||||
</insert>
|
||||
</mapper>
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.info.collection.mapper.CcdiEnterpriseShareholderMapper">
|
||||
<select id="selectByCreditCode" resultType="com.ruoyi.info.collection.domain.CcdiEnterpriseShareholder">
|
||||
select shareholder_id, social_credit_code, shareholder_seq, shareholder_name,
|
||||
shareholder_type, shareholder_credit_code, stock_percent, subscribed_capital,
|
||||
capital_unit, cache_info_id, sync_time, create_by, create_time, update_by, update_time
|
||||
from ccdi_enterprise_shareholder
|
||||
where social_credit_code = #{creditCode}
|
||||
order by shareholder_seq
|
||||
</select>
|
||||
|
||||
<delete id="deleteByCreditCode">
|
||||
delete from ccdi_enterprise_shareholder where social_credit_code = #{creditCode}
|
||||
</delete>
|
||||
|
||||
<insert id="insertBatch">
|
||||
insert into ccdi_enterprise_shareholder (
|
||||
social_credit_code, shareholder_seq, shareholder_name, shareholder_type,
|
||||
shareholder_credit_code, stock_percent, subscribed_capital, capital_unit,
|
||||
cache_info_id, sync_time, create_by, create_time, update_by, update_time
|
||||
) values
|
||||
<foreach collection="list" item="item" separator=",">
|
||||
(#{item.socialCreditCode}, #{item.shareholderSeq}, #{item.shareholderName}, #{item.shareholderType},
|
||||
#{item.shareholderCreditCode}, #{item.stockPercent}, #{item.subscribedCapital}, #{item.capitalUnit},
|
||||
#{item.cacheInfoId}, #{item.syncTime}, #{item.createBy}, #{item.createTime}, #{item.updateBy}, #{item.updateTime})
|
||||
</foreach>
|
||||
</insert>
|
||||
</mapper>
|
||||
@@ -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<>();
|
||||
|
||||
@@ -6,9 +6,11 @@ import com.ruoyi.info.collection.domain.dto.CcdiEnterpriseBaseInfoEditDTO;
|
||||
import com.ruoyi.info.collection.domain.vo.CcdiEnterpriseBaseInfoVO;
|
||||
import com.ruoyi.info.collection.mapper.CcdiCustEnterpriseRelationMapper;
|
||||
import com.ruoyi.info.collection.mapper.CcdiEnterpriseBaseInfoMapper;
|
||||
import com.ruoyi.info.collection.mapper.CcdiEnterpriseShareholderMapper;
|
||||
import com.ruoyi.info.collection.mapper.CcdiIntermediaryEnterpriseRelationMapper;
|
||||
import com.ruoyi.info.collection.mapper.CcdiStaffEnterpriseRelationMapper;
|
||||
import com.ruoyi.info.collection.service.impl.CcdiEnterpriseBaseInfoServiceImpl;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
@@ -49,13 +51,22 @@ class CcdiEnterpriseBaseInfoServiceImplTest {
|
||||
@Mock
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Mock
|
||||
private CcdiEnterpriseShareholderMapper enterpriseShareholderMapper;
|
||||
|
||||
@Mock
|
||||
private IEnterpriseProfileQueryService enterpriseProfileQueryService;
|
||||
|
||||
@Mock
|
||||
private IEnterpriseEntitySyncService enterpriseEntitySyncService;
|
||||
|
||||
@Test
|
||||
void insertEnterpriseBaseInfo_shouldPersistWhenSocialCreditCodeIsUnique() {
|
||||
CcdiEnterpriseBaseInfoAddDTO addDTO = buildAddDto();
|
||||
when(enterpriseBaseInfoMapper.selectById(addDTO.getSocialCreditCode())).thenReturn(null);
|
||||
when(enterpriseBaseInfoMapper.insert(any(CcdiEnterpriseBaseInfo.class))).thenReturn(1);
|
||||
|
||||
int result = service.insertEnterpriseBaseInfo(addDTO);
|
||||
int result = service.insertEnterpriseBaseInfo(CallerContext.system(), addDTO);
|
||||
|
||||
assertEquals(1, result);
|
||||
ArgumentCaptor<CcdiEnterpriseBaseInfo> captor = ArgumentCaptor.forClass(CcdiEnterpriseBaseInfo.class);
|
||||
@@ -72,7 +83,7 @@ class CcdiEnterpriseBaseInfoServiceImplTest {
|
||||
when(enterpriseBaseInfoMapper.selectById(addDTO.getSocialCreditCode())).thenReturn(null);
|
||||
when(enterpriseBaseInfoMapper.insert(any(CcdiEnterpriseBaseInfo.class))).thenReturn(1);
|
||||
|
||||
int result = service.insertEnterpriseBaseInfo(addDTO);
|
||||
int result = service.insertEnterpriseBaseInfo(CallerContext.system(), addDTO);
|
||||
|
||||
assertEquals(1, result);
|
||||
ArgumentCaptor<CcdiEnterpriseBaseInfo> captor = ArgumentCaptor.forClass(CcdiEnterpriseBaseInfo.class);
|
||||
@@ -88,7 +99,7 @@ class CcdiEnterpriseBaseInfoServiceImplTest {
|
||||
when(enterpriseBaseInfoMapper.selectById(addDTO.getSocialCreditCode())).thenReturn(null);
|
||||
|
||||
RuntimeException exception = assertThrows(RuntimeException.class,
|
||||
() -> service.insertEnterpriseBaseInfo(addDTO));
|
||||
() -> service.insertEnterpriseBaseInfo(CallerContext.system(), addDTO));
|
||||
|
||||
assertEquals("风险等级不在允许范围内", exception.getMessage());
|
||||
}
|
||||
@@ -99,7 +110,7 @@ class CcdiEnterpriseBaseInfoServiceImplTest {
|
||||
when(enterpriseBaseInfoMapper.selectById(editDTO.getSocialCreditCode())).thenReturn(null);
|
||||
|
||||
RuntimeException exception = assertThrows(RuntimeException.class,
|
||||
() -> service.updateEnterpriseBaseInfo(editDTO));
|
||||
() -> service.updateEnterpriseBaseInfo(CallerContext.system(), editDTO));
|
||||
|
||||
assertEquals("实体库记录不存在", exception.getMessage());
|
||||
}
|
||||
@@ -114,7 +125,7 @@ class CcdiEnterpriseBaseInfoServiceImplTest {
|
||||
when(enterpriseBaseInfoMapper.selectById(editDTO.getSocialCreditCode())).thenReturn(existing);
|
||||
when(enterpriseBaseInfoMapper.updateById(any(CcdiEnterpriseBaseInfo.class))).thenReturn(1);
|
||||
|
||||
int result = service.updateEnterpriseBaseInfo(editDTO);
|
||||
int result = service.updateEnterpriseBaseInfo(CallerContext.system(), editDTO);
|
||||
|
||||
assertEquals(1, result);
|
||||
ArgumentCaptor<CcdiEnterpriseBaseInfo> captor = ArgumentCaptor.forClass(CcdiEnterpriseBaseInfo.class);
|
||||
@@ -130,6 +141,7 @@ class CcdiEnterpriseBaseInfoServiceImplTest {
|
||||
entity.setRiskLevel("1");
|
||||
entity.setEntSource("GENERAL");
|
||||
when(enterpriseBaseInfoMapper.selectById("91310000123456789A")).thenReturn(entity);
|
||||
when(enterpriseShareholderMapper.selectByCreditCode("91310000123456789A")).thenReturn(java.util.List.of());
|
||||
|
||||
CcdiEnterpriseBaseInfoVO vo = service.selectEnterpriseBaseInfoById("91310000123456789A");
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.ruoyi.lsfx.client;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ruoyi.common.utils.uuid.IdUtils;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import com.ruoyi.lsfx.domain.response.XinhuaEnterpriseQueryResult;
|
||||
import com.ruoyi.lsfx.exception.LsfxApiException;
|
||||
import com.ruoyi.lsfx.util.HttpUtil;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/** 新华社工商信息同步接口客户端。 */
|
||||
@Component
|
||||
public class XinhuaEnterpriseClient {
|
||||
|
||||
private static final int PLATFORM_SUCCESS_CODE = 10000;
|
||||
private static final int BUSINESS_SUCCESS_STATUS = 1;
|
||||
private static final int BUSINESS_SUCCESS_REASON_CODE = 200;
|
||||
|
||||
@Resource
|
||||
private HttpUtil httpUtil;
|
||||
|
||||
@Resource
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Value("${xinhua-enterprise.api.url}")
|
||||
private String apiUrl;
|
||||
|
||||
@Value("${xinhua-enterprise.api.org-code:999000}")
|
||||
private String orgCode;
|
||||
|
||||
@Value("${xinhua-enterprise.api.run-type:1}")
|
||||
private String runType;
|
||||
|
||||
public XinhuaEnterpriseQueryResult query(CallerContext caller, String enterpriseName) {
|
||||
String normalizedName = normalize(enterpriseName);
|
||||
if (!StringUtils.hasText(normalizedName)) {
|
||||
throw new IllegalArgumentException("企业名称不能为空");
|
||||
}
|
||||
|
||||
Map<String, Object> params = new LinkedHashMap<>();
|
||||
params.put("entName", normalizedName);
|
||||
params.put("serialNum", buildSerialNum());
|
||||
params.put("runType", runType);
|
||||
params.put("orgCode", orgCode);
|
||||
|
||||
String rawJson = httpUtil.postUrlEncodedFormForString(caller, apiUrl, params, null);
|
||||
JsonNode root = parse(rawJson);
|
||||
JsonNode data = requireSuccess(root);
|
||||
JsonNode profile = data.path("mappingOutputFields");
|
||||
if (!profile.isObject() || profile.isEmpty()) {
|
||||
throw new LsfxApiException("新华社工商接口返回工商信息为空");
|
||||
}
|
||||
|
||||
String responseName = normalize(profile.path("enterpriseName").asText(null));
|
||||
if (!normalizedName.equals(responseName)) {
|
||||
throw new LsfxApiException("新华社工商接口返回企业名称与查询名称不一致");
|
||||
}
|
||||
if (!StringUtils.hasText(normalize(profile.path("creditCode").asText(null)))) {
|
||||
throw new LsfxApiException("新华社工商接口未返回统一社会信用代码");
|
||||
}
|
||||
return new XinhuaEnterpriseQueryResult(rawJson, profile);
|
||||
}
|
||||
|
||||
private JsonNode parse(String rawJson) {
|
||||
try {
|
||||
return objectMapper.readTree(rawJson);
|
||||
} catch (Exception e) {
|
||||
throw new LsfxApiException("新华社工商接口响应不是合法JSON", e);
|
||||
}
|
||||
}
|
||||
|
||||
private JsonNode requireSuccess(JsonNode root) {
|
||||
if (!root.path("success").asBoolean(false)) {
|
||||
throw new LsfxApiException(message(root, "新华社工商接口平台调用失败"));
|
||||
}
|
||||
if (root.path("code").asInt(Integer.MIN_VALUE) != PLATFORM_SUCCESS_CODE) {
|
||||
throw new LsfxApiException("新华社工商接口平台状态码异常: " + root.path("code").asText());
|
||||
}
|
||||
JsonNode data = root.path("data");
|
||||
if (!data.isObject()) {
|
||||
throw new LsfxApiException("新华社工商接口返回data为空");
|
||||
}
|
||||
if (data.path("status").asInt(Integer.MIN_VALUE) != BUSINESS_SUCCESS_STATUS) {
|
||||
throw new LsfxApiException(message(root, "新华社工商接口业务状态异常"));
|
||||
}
|
||||
if (data.path("reasonCode").asInt(Integer.MIN_VALUE) != BUSINESS_SUCCESS_REASON_CODE) {
|
||||
throw new LsfxApiException(message(root, "新华社工商接口业务原因码异常"));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private String message(JsonNode root, String defaultMessage) {
|
||||
String reason = normalize(root.path("data").path("reasonMessage").asText(null));
|
||||
return StringUtils.hasText(reason) ? reason : defaultMessage;
|
||||
}
|
||||
|
||||
private String buildSerialNum() {
|
||||
return "CCDI_GS_" + System.currentTimeMillis() + "_" + IdUtils.fastSimpleUUID();
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
return value == null ? null : value.trim();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.ruoyi.lsfx.domain.response;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
/** 新华社工商信息同步查询结果。 */
|
||||
public record XinhuaEnterpriseQueryResult(String rawJson, JsonNode profile) {
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.response.UploadFileResponse;
|
||||
import com.ruoyi.lsfx.util.HttpUtil;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -48,10 +49,11 @@ class LsfxAnalysisClientTest {
|
||||
|
||||
ArgumentCaptor<Map<String, Object>> paramsCaptor = ArgumentCaptor.forClass(Map.class);
|
||||
ArgumentCaptor<Map<String, String>> headersCaptor = ArgumentCaptor.forClass(Map.class);
|
||||
when(httpUtil.uploadFile(eq("http://lsfx/upload"), paramsCaptor.capture(), headersCaptor.capture(), eq(UploadFileResponse.class)))
|
||||
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(200, tempFile.toFile(), "银行流水A.xlsx");
|
||||
client.uploadFile(caller, 200, tempFile.toFile(), "银行流水A.xlsx");
|
||||
|
||||
assertEquals(200, paramsCaptor.getValue().get("groupId"));
|
||||
Resource filePart = assertInstanceOf(Resource.class, paramsCaptor.getValue().get("files"));
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
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;
|
||||
@@ -8,52 +14,258 @@ 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;
|
||||
|
||||
@Test
|
||||
void uploadFile_shouldUseExplicitResourceFilename() throws Exception {
|
||||
HttpUtil httpUtil = new HttpUtil();
|
||||
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, "content");
|
||||
Files.writeString(tempFile, "binary-content-must-not-enter-log");
|
||||
|
||||
ArgumentCaptor<HttpEntity> captor = ArgumentCaptor.forClass(HttpEntity.class);
|
||||
when(restTemplate.postForEntity(eq("http://lsfx/upload"), captor.capture(), eq(String.class)))
|
||||
.thenReturn(ResponseEntity.ok("ok"));
|
||||
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"));
|
||||
|
||||
String result = httpUtil.uploadFile("http://lsfx/upload", params, null, String.class);
|
||||
assertEquals("ok", httpUtil.uploadFile(CALLER, "http://lsfx/upload", params, null, String.class));
|
||||
|
||||
assertEquals("ok", result);
|
||||
MultiValueMap<String, Object> body = (MultiValueMap<String, Object>) captor.getValue().getBody();
|
||||
Object filePart = body.getFirst("files");
|
||||
Resource resource = assertInstanceOf(Resource.class, filePart);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,13 @@
|
||||
<version>${ruoyi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 企业工商信息统一查询能力 -->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ccdi-info-collection</artifactId>
|
||||
<version>${ruoyi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
|
||||
@@ -15,6 +15,7 @@ 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;
|
||||
@@ -77,8 +78,8 @@ public class CcdiFileUploadController extends BaseController {
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -130,16 +131,14 @@ public class CcdiFileUploadController extends BaseController {
|
||||
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);
|
||||
}
|
||||
@@ -187,8 +186,8 @@ public class CcdiFileUploadController extends BaseController {
|
||||
@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);
|
||||
CallerContext caller = CallerContext.from(SecurityUtils.getLoginUser());
|
||||
String message = fileUploadService.deleteFileUploadRecord(id, caller);
|
||||
return AjaxResult.success(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -130,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.ruoyi.ccdi.project.controller;
|
||||
|
||||
import com.ruoyi.ccdi.project.domain.dto.ProjectCounterpartyRefreshDTO;
|
||||
import com.ruoyi.ccdi.project.service.IProjectCounterpartyEnterpriseService;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
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 jakarta.validation.Valid;
|
||||
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;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/ccdi/project/counterparty-enterprise")
|
||||
@Tag(name = "项目对手方工商信息")
|
||||
public class CcdiProjectCounterpartyEnterpriseController {
|
||||
@Resource
|
||||
private IProjectCounterpartyEnterpriseService service;
|
||||
|
||||
@GetMapping("/detail")
|
||||
@Operation(summary = "查询本地项目对手方工商详情")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:project:query')")
|
||||
public AjaxResult detail(@RequestParam Long projectId, @RequestParam String counterpartyName) {
|
||||
return AjaxResult.success(service.getDetail(projectId, counterpartyName));
|
||||
}
|
||||
|
||||
@PostMapping("/refresh")
|
||||
@Operation(summary = "重新查询单个项目对手方工商信息")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:project:counterpartyEnterprise:refresh')")
|
||||
public AjaxResult refresh(@Valid @RequestBody ProjectCounterpartyRefreshDTO dto) {
|
||||
return AjaxResult.success(service.refresh(CallerContext.from(SecurityUtils.getLoginUser()),
|
||||
dto.getProjectId(), dto.getCounterpartyName()));
|
||||
}
|
||||
|
||||
@PostMapping("/backfill")
|
||||
@Operation(summary = "启动项目对手方工商信息历史补全")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:project:counterpartyEnterprise:backfill')")
|
||||
public AjaxResult backfill() {
|
||||
return AjaxResult.success("任务已提交", service.startBackfill(CallerContext.from(SecurityUtils.getLoginUser())));
|
||||
}
|
||||
|
||||
@GetMapping("/backfill/{taskId}")
|
||||
@Operation(summary = "查询项目对手方工商信息补全状态")
|
||||
@PreAuthorize("@ss.hasPermi('ccdi:project:counterpartyEnterprise:backfill')")
|
||||
public AjaxResult backfillStatus(@PathVariable String taskId) {
|
||||
return AjaxResult.success(service.getBackfillStatus(taskId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ruoyi.ccdi.project.domain.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ProjectCounterpartyRefreshDTO {
|
||||
@NotNull(message = "项目ID不能为空")
|
||||
private Long projectId;
|
||||
|
||||
@NotBlank(message = "对手方名称不能为空")
|
||||
private String counterpartyName;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Date;
|
||||
|
||||
/** 项目对手方工商信息。 */
|
||||
@Data
|
||||
@TableName("ccdi_project_counterparty_enterprise")
|
||||
public class CcdiProjectCounterpartyEnterprise {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long counterpartyEnterpriseId;
|
||||
private Long projectId;
|
||||
private String counterpartyName;
|
||||
private String socialCreditCode;
|
||||
private String enterpriseName;
|
||||
private BigDecimal registeredCapital;
|
||||
private String registeredCapitalUnit;
|
||||
private LocalDate registerDate;
|
||||
private LocalDate establishDate;
|
||||
private String industryCode;
|
||||
private String industryName;
|
||||
private String organizationTypeCode;
|
||||
private String organizationTypeName;
|
||||
private String regionCode;
|
||||
private String regionName;
|
||||
private String registerAddress;
|
||||
private Integer employeeCount;
|
||||
private String legalRepresentative;
|
||||
private Long cacheInfoId;
|
||||
private Date syncTime;
|
||||
private String createBy;
|
||||
private Date createTime;
|
||||
private String updateBy;
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.ruoyi.ccdi.project.domain.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/** 项目对手方股东信息。 */
|
||||
@Data
|
||||
@TableName("ccdi_project_counterparty_shareholder")
|
||||
public class CcdiProjectCounterpartyShareholder {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long shareholderId;
|
||||
private Long counterpartyEnterpriseId;
|
||||
@TableField("shareholder_seq")
|
||||
private Integer sequenceNo;
|
||||
private String shareholderName;
|
||||
private BigDecimal stockPercent;
|
||||
private BigDecimal subscribedCapital;
|
||||
private String capitalUnit;
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.ccdi.project.domain.event;
|
||||
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectImportHistoryDTO;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
|
||||
/**
|
||||
* 历史项目导入提交事件
|
||||
@@ -10,14 +11,14 @@ public class CcdiProjectHistoryImportSubmittedEvent {
|
||||
private final Long targetProjectId;
|
||||
private final Integer targetLsfxProjectId;
|
||||
private final CcdiProjectImportHistoryDTO dto;
|
||||
private final String operator;
|
||||
private final CallerContext caller;
|
||||
|
||||
public CcdiProjectHistoryImportSubmittedEvent(Long targetProjectId, Integer targetLsfxProjectId,
|
||||
CcdiProjectImportHistoryDTO dto, String operator) {
|
||||
CcdiProjectImportHistoryDTO dto, CallerContext caller) {
|
||||
this.targetProjectId = targetProjectId;
|
||||
this.targetLsfxProjectId = targetLsfxProjectId;
|
||||
this.dto = dto;
|
||||
this.operator = operator;
|
||||
this.caller = caller;
|
||||
}
|
||||
|
||||
public Long getTargetProjectId() {
|
||||
@@ -32,7 +33,7 @@ public class CcdiProjectHistoryImportSubmittedEvent {
|
||||
return dto;
|
||||
}
|
||||
|
||||
public String getOperator() {
|
||||
return operator;
|
||||
public CallerContext getCaller() {
|
||||
return caller;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.ruoyi.ccdi.project.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class ProjectCounterpartyEnterpriseVO {
|
||||
private Long projectId;
|
||||
private String counterpartyName;
|
||||
private String status;
|
||||
private Boolean canRefresh;
|
||||
private Date cacheValidDate;
|
||||
private String socialCreditCode;
|
||||
private String enterpriseName;
|
||||
private BigDecimal registeredCapital;
|
||||
private String registeredCapitalUnit;
|
||||
private LocalDate registerDate;
|
||||
private LocalDate establishDate;
|
||||
private String industryCode;
|
||||
private String industryName;
|
||||
private String organizationTypeCode;
|
||||
private String organizationTypeName;
|
||||
private String regionCode;
|
||||
private String regionName;
|
||||
private String registerAddress;
|
||||
private Integer employeeCount;
|
||||
private String legalRepresentative;
|
||||
private Date syncTime;
|
||||
private List<ProjectCounterpartyShareholderVO> shareholders = new ArrayList<>();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ruoyi.ccdi.project.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
public class ProjectCounterpartyShareholderVO {
|
||||
private Integer sequenceNo;
|
||||
private String shareholderName;
|
||||
private BigDecimal stockPercent;
|
||||
private BigDecimal subscribedCapital;
|
||||
private String capitalUnit;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ruoyi.ccdi.project.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ProjectCounterpartySyncTaskVO {
|
||||
private String taskId;
|
||||
private String status;
|
||||
private Integer totalCount;
|
||||
private Integer completedCount;
|
||||
private Integer successCount;
|
||||
private Integer skippedCount;
|
||||
private Integer failureCount;
|
||||
private String message;
|
||||
}
|
||||
@@ -49,4 +49,8 @@ public interface CcdiBankStatementMapper extends BaseMapper<CcdiBankStatement> {
|
||||
CcdiBankStatementFilterOptionsVO selectFilterOptions(@Param("projectId") Long projectId);
|
||||
|
||||
Integer countMatchedStaffCountByProjectId(@Param("projectId") Long projectId);
|
||||
|
||||
List<String> selectDistinctCounterpartyNames(@Param("projectId") Long projectId);
|
||||
|
||||
List<Long> selectDistinctProjectIdsWithCounterparties();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.ruoyi.ccdi.project.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ruoyi.ccdi.project.domain.entity.CcdiProjectCounterpartyEnterprise;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface CcdiProjectCounterpartyEnterpriseMapper extends BaseMapper<CcdiProjectCounterpartyEnterprise> {
|
||||
CcdiProjectCounterpartyEnterprise selectByProjectAndName(@Param("projectId") Long projectId,
|
||||
@Param("counterpartyName") String counterpartyName);
|
||||
|
||||
List<CcdiProjectCounterpartyEnterprise> selectByProjectId(@Param("projectId") Long projectId);
|
||||
|
||||
int deleteByProjectId(@Param("projectId") Long projectId);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ruoyi.ccdi.project.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.ruoyi.ccdi.project.domain.entity.CcdiProjectCounterpartyShareholder;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface CcdiProjectCounterpartyShareholderMapper extends BaseMapper<CcdiProjectCounterpartyShareholder> {
|
||||
List<CcdiProjectCounterpartyShareholder> selectByEnterpriseId(@Param("counterpartyEnterpriseId") Long counterpartyEnterpriseId);
|
||||
|
||||
int deleteByEnterpriseId(@Param("counterpartyEnterpriseId") Long counterpartyEnterpriseId);
|
||||
|
||||
int deleteByProjectId(@Param("projectId") Long projectId);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiFileUploadQueryDTO;
|
||||
import com.ruoyi.ccdi.project.domain.entity.CcdiFileUploadRecord;
|
||||
import com.ruoyi.ccdi.project.domain.vo.CcdiFileUploadStatisticsVO;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
@@ -24,7 +25,7 @@ public interface ICcdiFileUploadService {
|
||||
* @param username 上传人
|
||||
* @return 批次ID
|
||||
*/
|
||||
String batchUploadFiles(Long projectId, MultipartFile[] files, String username);
|
||||
String batchUploadFiles(Long projectId, MultipartFile[] files, CallerContext caller);
|
||||
|
||||
/**
|
||||
* 解析身份证文件
|
||||
@@ -51,17 +52,16 @@ public interface ICcdiFileUploadService {
|
||||
String dataChannelCode,
|
||||
String startDate,
|
||||
String endDate,
|
||||
Long userId,
|
||||
String username);
|
||||
CallerContext caller);
|
||||
|
||||
/**
|
||||
* 删除上传记录并清理关联数据
|
||||
*
|
||||
* @param id 上传记录ID
|
||||
* @param operatorUserId 当前操作用户ID
|
||||
* @param caller 当前操作人上下文
|
||||
* @return 删除结果
|
||||
*/
|
||||
String deleteFileUploadRecord(Long id, Long operatorUserId);
|
||||
String deleteFileUploadRecord(Long id, CallerContext caller);
|
||||
|
||||
/**
|
||||
* 查询上传记录列表
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.ccdi.project.service;
|
||||
|
||||
import com.ruoyi.ccdi.project.domain.dto.CcdiProjectImportHistoryDTO;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
|
||||
/**
|
||||
* 历史项目导入服务
|
||||
@@ -15,8 +16,8 @@ public interface ICcdiProjectHistoryImportService {
|
||||
* @param targetProjectId 目标项目ID
|
||||
* @param targetLsfxProjectId 目标流水分析项目ID
|
||||
* @param dto 导入参数
|
||||
* @param operator 操作人
|
||||
* @param caller 原始操作人上下文
|
||||
*/
|
||||
void submitImport(Long targetProjectId, Integer targetLsfxProjectId,
|
||||
CcdiProjectImportHistoryDTO dto, String operator);
|
||||
CcdiProjectImportHistoryDTO dto, CallerContext caller);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.ruoyi.ccdi.project.domain.dto.CcdiProjectSaveDTO;
|
||||
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.lsfx.domain.CallerContext;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -22,7 +23,7 @@ public interface ICcdiProjectService {
|
||||
* @param dto 项目保存DTO
|
||||
* @return 项目VO
|
||||
*/
|
||||
CcdiProjectVO createProject(CcdiProjectSaveDTO dto);
|
||||
CcdiProjectVO createProject(CcdiProjectSaveDTO dto, CallerContext caller);
|
||||
|
||||
/**
|
||||
* 更新项目
|
||||
@@ -81,7 +82,7 @@ public interface ICcdiProjectService {
|
||||
* @param operator 操作人
|
||||
* @return 新建项目
|
||||
*/
|
||||
CcdiProjectVO importFromHistory(CcdiProjectImportHistoryDTO dto, String operator);
|
||||
CcdiProjectVO importFromHistory(CcdiProjectImportHistoryDTO dto, CallerContext caller);
|
||||
|
||||
/**
|
||||
* 查询各状态的项目总数(不受搜索条件影响)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ruoyi.ccdi.project.service;
|
||||
|
||||
import com.ruoyi.ccdi.project.domain.vo.ProjectCounterpartyEnterpriseVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.ProjectCounterpartySyncTaskVO;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
|
||||
public interface IProjectCounterpartyEnterpriseService {
|
||||
ProjectCounterpartyEnterpriseVO getDetail(Long projectId, String counterpartyName);
|
||||
ProjectCounterpartyEnterpriseVO refresh(CallerContext caller, Long projectId, String counterpartyName);
|
||||
void submitReconcile(CallerContext caller, Long projectId);
|
||||
String startBackfill(CallerContext caller);
|
||||
ProjectCounterpartySyncTaskVO getBackfillStatus(String taskId);
|
||||
void deleteProjectData(Long projectId);
|
||||
}
|
||||
@@ -16,9 +16,11 @@ import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiBankTagService;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiFileUploadService;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiProjectService;
|
||||
import com.ruoyi.ccdi.project.service.IProjectCounterpartyEnterpriseService;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.lsfx.client.LsfxAnalysisClient;
|
||||
import com.ruoyi.lsfx.constants.LsfxConstants;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import com.ruoyi.lsfx.domain.request.FetchInnerFlowRequest;
|
||||
import com.ruoyi.lsfx.domain.request.GetBankStatementRequest;
|
||||
import com.ruoyi.lsfx.domain.request.GetFileUploadStatusRequest;
|
||||
@@ -105,6 +107,9 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
@Resource
|
||||
private ICcdiProjectService projectService;
|
||||
|
||||
@Resource
|
||||
private IProjectCounterpartyEnterpriseService counterpartyEnterpriseService;
|
||||
|
||||
/**
|
||||
* 获取临时文件存储目录
|
||||
*/
|
||||
@@ -157,8 +162,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
String dataChannelCode,
|
||||
String startDate,
|
||||
String endDate,
|
||||
Long userId,
|
||||
String username) {
|
||||
CallerContext caller) {
|
||||
if (projectId == null) {
|
||||
throw new IllegalArgumentException("项目ID不能为空");
|
||||
}
|
||||
@@ -209,7 +213,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
record.setFileStatus("uploading");
|
||||
record.setAccountNos(normalized);
|
||||
record.setUploadTime(now);
|
||||
record.setUploadUser(username);
|
||||
record.setUploadUser(caller.username());
|
||||
records.add(record);
|
||||
}
|
||||
if (records.isEmpty()) {
|
||||
@@ -223,7 +227,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
public void afterCommit() {
|
||||
CompletableFuture.runAsync(() -> submitPullBankInfoTasks(
|
||||
projectId, lsfxProjectId, records, normalizedIdCards,
|
||||
normalizedDataChannelCode, startDate, endDate, batchId
|
||||
normalizedDataChannelCode, startDate, endDate, batchId, caller
|
||||
));
|
||||
}
|
||||
});
|
||||
@@ -243,7 +247,8 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String deleteFileUploadRecord(Long id, Long operatorUserId) {
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String deleteFileUploadRecord(Long id, CallerContext caller) {
|
||||
CcdiFileUploadRecord record = recordMapper.selectById(id);
|
||||
validateDeleteRecord(record);
|
||||
|
||||
@@ -252,7 +257,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
* DeleteFilesRequest request = new DeleteFilesRequest();
|
||||
* request.setGroupId(record.getLsfxProjectId());
|
||||
* request.setLogIds(new Integer[]{record.getLogId()});
|
||||
* request.setUserId(toUploadUserId(operatorUserId));
|
||||
* request.setUserId(toUploadUserId(caller.userId()));
|
||||
*
|
||||
* DeleteFilesResponse response = lsfxClient.deleteFiles(request);
|
||||
* if (response == null || Boolean.FALSE.equals(response.getSuccessResponse())) {
|
||||
@@ -272,9 +277,23 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
}
|
||||
|
||||
bankTagService.submitAutoRebuild(record.getProjectId(), TriggerType.AUTO_FILE_DELETE);
|
||||
submitCounterpartyReconcileAfterCommit(caller, record.getProjectId());
|
||||
return "删除成功,已开始项目重新打标";
|
||||
}
|
||||
|
||||
private void submitCounterpartyReconcileAfterCommit(CallerContext caller, Long projectId) {
|
||||
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
counterpartyEnterpriseService.submitReconcile(caller, projectId);
|
||||
return;
|
||||
}
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
counterpartyEnterpriseService.submitReconcile(caller, projectId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<CcdiFileUploadRecord> selectPage(Page<CcdiFileUploadRecord> page,
|
||||
CcdiFileUploadQueryDTO queryDTO) {
|
||||
@@ -345,9 +364,9 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public String batchUploadFiles(Long projectId, MultipartFile[] files, String username) {
|
||||
public String batchUploadFiles(Long projectId, MultipartFile[] files, CallerContext caller) {
|
||||
log.info("【文件上传】开始批量上传: projectId={}, 文件数量={}, username={}",
|
||||
projectId, files.length, username);
|
||||
projectId, files.length, caller.username());
|
||||
|
||||
projectService.ensureProjectNotArchived(projectId, "已归档项目暂不允许上传或拉取数据");
|
||||
projectService.ensureProjectWritable(projectId, "当前项目正在进行银行流水打标,暂不允许上传或拉取数据");
|
||||
@@ -406,7 +425,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
record.setFileSize(file.getSize());
|
||||
record.setFileStatus("uploading");
|
||||
record.setUploadTime(now);
|
||||
record.setUploadUser(username);
|
||||
record.setUploadUser(caller.username());
|
||||
records.add(record);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
@@ -438,7 +457,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
public void afterCommit() {
|
||||
log.info("【文件上传】事务已提交,启动异步任务");
|
||||
CompletableFuture.runAsync(() -> {
|
||||
submitTasksAsync(projectId, finalLsfxProjectId, tempFilePaths, records, batchId);
|
||||
submitTasksAsync(projectId, finalLsfxProjectId, tempFilePaths, records, batchId, caller);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -496,7 +515,8 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
private void submitTasksAsync(Long projectId, Integer lsfxProjectId,
|
||||
List<String> tempFilePaths,
|
||||
List<CcdiFileUploadRecord> records,
|
||||
String batchId) {
|
||||
String batchId,
|
||||
CallerContext caller) {
|
||||
log.info("【文件上传】调度线程启动: projectId={}, batchId={}", projectId, batchId);
|
||||
|
||||
List<CompletableFuture<Boolean>> futures = new ArrayList<>();
|
||||
@@ -519,7 +539,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
try {
|
||||
// 尝试提交异步任务
|
||||
CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(
|
||||
() -> processFileAsync(projectId, lsfxProjectId, tempFilePath, record.getId(), batchId, record),
|
||||
() -> processFileAsync(projectId, lsfxProjectId, tempFilePath, record.getId(), batchId, record, caller),
|
||||
fileUploadExecutor
|
||||
);
|
||||
futures.add(future);
|
||||
@@ -553,6 +573,9 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
.whenComplete((unused, throwable) -> {
|
||||
boolean anySuccess = futures.stream().anyMatch(future -> Boolean.TRUE.equals(future.getNow(Boolean.FALSE)));
|
||||
handleTagRebuildAfterBatchCompletion(projectId, TriggerType.AUTO_BATCH_UPLOAD, anySuccess);
|
||||
if (anySuccess) {
|
||||
counterpartyEnterpriseService.submitReconcile(caller, projectId);
|
||||
}
|
||||
});
|
||||
|
||||
log.info("【文件上传】调度线程完成: projectId={}, batchId={}", projectId, batchId);
|
||||
@@ -600,7 +623,8 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
String dataChannelCode,
|
||||
String startDate,
|
||||
String endDate,
|
||||
String batchId) {
|
||||
String batchId,
|
||||
CallerContext caller) {
|
||||
log.info("【拉取本行信息】调度线程启动: projectId={}, batchId={}", projectId, batchId);
|
||||
|
||||
List<CompletableFuture<Boolean>> futures = new ArrayList<>();
|
||||
@@ -619,7 +643,8 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
while (!submitted && retryCount < 2) {
|
||||
try {
|
||||
CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(
|
||||
() -> processPullBankInfoAsync(projectId, lsfxProjectId, record, idCard, dataChannelCode, startDate, endDate),
|
||||
() -> processPullBankInfoAsync(projectId, lsfxProjectId, record, idCard,
|
||||
dataChannelCode, startDate, endDate, caller),
|
||||
fileUploadExecutor
|
||||
);
|
||||
futures.add(future);
|
||||
@@ -651,6 +676,9 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
.whenComplete((unused, throwable) -> {
|
||||
boolean anySuccess = futures.stream().anyMatch(future -> Boolean.TRUE.equals(future.getNow(Boolean.FALSE)));
|
||||
handleTagRebuildAfterBatchCompletion(projectId, TriggerType.AUTO_PULL_BANK_INFO, anySuccess);
|
||||
if (anySuccess) {
|
||||
counterpartyEnterpriseService.submitReconcile(caller, projectId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -660,7 +688,8 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
String idCard,
|
||||
String dataChannelCode,
|
||||
String startDate,
|
||||
String endDate ) {
|
||||
String endDate,
|
||||
CallerContext caller) {
|
||||
try {
|
||||
String normalizedDataChannelCode = normalizePullBankInfoDataChannelCode(dataChannelCode);
|
||||
FetchInnerFlowRequest request = new FetchInnerFlowRequest();
|
||||
@@ -677,18 +706,33 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
}
|
||||
request.setUploadUserId(LsfxConstants.DEFAULT_USER_ID);
|
||||
|
||||
FetchInnerFlowResponse response = lsfxClient.fetchInnerFlow(request);
|
||||
FetchInnerFlowResponse response = lsfxClient.fetchInnerFlow(caller, request);
|
||||
if (response == null || response.getData() == null || response.getData().isEmpty()) {
|
||||
throw new RuntimeException("拉取本行信息失败: 未返回logId");
|
||||
}
|
||||
|
||||
Integer logId = response.getData().get(0);
|
||||
if (logId == null) {
|
||||
List<Integer> logIds = response.getData().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
if (logIds.isEmpty()) {
|
||||
throw new RuntimeException("拉取本行信息失败: 未返回logId");
|
||||
}
|
||||
|
||||
processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId);
|
||||
return true;
|
||||
boolean anySuccess = false;
|
||||
for (int i = 0; i < logIds.size(); i++) {
|
||||
Integer logId = logIds.get(i);
|
||||
CcdiFileUploadRecord currentRecord = i == 0
|
||||
? record
|
||||
: createAdditionalPullBankInfoRecord(record, idCard);
|
||||
try {
|
||||
anySuccess |= processRecordAfterLogIdReady(projectId, lsfxProjectId, currentRecord, logId, caller);
|
||||
} catch (Exception logException) {
|
||||
log.error("【拉取本行信息】处理logId失败: idCard={}, logId={}, recordId={}",
|
||||
idCard, logId, currentRecord.getId(), logException);
|
||||
updateFailedRecord(currentRecord, logException.getMessage());
|
||||
}
|
||||
}
|
||||
return anySuccess;
|
||||
} catch (Exception e) {
|
||||
log.error("【拉取本行信息】处理失败: idCard={}, recordId={}", idCard, record.getId(), e);
|
||||
updateFailedRecord(record, e.getMessage());
|
||||
@@ -696,6 +740,24 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
}
|
||||
}
|
||||
|
||||
private CcdiFileUploadRecord createAdditionalPullBankInfoRecord(CcdiFileUploadRecord sourceRecord,
|
||||
String idCard) {
|
||||
CcdiFileUploadRecord record = new CcdiFileUploadRecord();
|
||||
record.setProjectId(sourceRecord.getProjectId());
|
||||
record.setLsfxProjectId(sourceRecord.getLsfxProjectId());
|
||||
record.setFileName(idCard);
|
||||
record.setFileSize(0L);
|
||||
record.setFileStatus("uploading");
|
||||
record.setAccountNos(idCard);
|
||||
record.setUploadTime(new Date());
|
||||
record.setUploadUser(sourceRecord.getUploadUser());
|
||||
recordMapper.insertBatch(List.of(record));
|
||||
if (record.getId() == null) {
|
||||
throw new RuntimeException("创建金综流水上传记录失败: 未生成记录ID");
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步处理单个文件的完整流程
|
||||
* 包含:上传 → 轮询解析状态 → 获取结果 → 保存流水数据
|
||||
@@ -709,7 +771,8 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
*/
|
||||
@Async("fileUploadExecutor")
|
||||
public boolean processFileAsync(Long projectId, Integer lsfxProjectId, String tempFilePath,
|
||||
Long recordId, String batchId, CcdiFileUploadRecord record) {
|
||||
Long recordId, String batchId, CcdiFileUploadRecord record,
|
||||
CallerContext caller) {
|
||||
log.info("【文件上传】开始处理文件: fileName={}, recordId={}, tempPath={}",
|
||||
record.getFileName(), recordId, tempFilePath);
|
||||
|
||||
@@ -730,7 +793,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
throw new RuntimeException("临时文件不存在: " + tempFilePath);
|
||||
}
|
||||
|
||||
UploadFileResponse uploadResponse = lsfxClient.uploadFile(lsfxProjectId, file, record.getFileName());
|
||||
UploadFileResponse uploadResponse = lsfxClient.uploadFile(caller, lsfxProjectId, file, record.getFileName());
|
||||
if (uploadResponse == null || uploadResponse.getData() == null
|
||||
|| uploadResponse.getData().getUploadLogList() == null
|
||||
|| uploadResponse.getData().getUploadLogList().isEmpty()) {
|
||||
@@ -744,7 +807,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
}
|
||||
|
||||
log.info("【文件上传】文件上传成功: logId={}", logId);
|
||||
processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId, true);
|
||||
processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId, true, caller);
|
||||
log.info("【文件上传】处理完成: fileName={}", record.getFileName());
|
||||
return true;
|
||||
|
||||
@@ -777,25 +840,27 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
bankTagService.submitAutoRebuild(projectId, triggerType);
|
||||
}
|
||||
|
||||
private void processRecordAfterLogIdReady(Long projectId,
|
||||
Integer lsfxProjectId,
|
||||
CcdiFileUploadRecord record,
|
||||
Integer logId) {
|
||||
processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId, false);
|
||||
private boolean processRecordAfterLogIdReady(Long projectId,
|
||||
Integer lsfxProjectId,
|
||||
CcdiFileUploadRecord record,
|
||||
Integer logId,
|
||||
CallerContext caller) {
|
||||
return processRecordAfterLogIdReady(projectId, lsfxProjectId, record, logId, false, caller);
|
||||
}
|
||||
|
||||
private void processRecordAfterLogIdReady(Long projectId,
|
||||
Integer lsfxProjectId,
|
||||
CcdiFileUploadRecord record,
|
||||
Integer logId,
|
||||
boolean preserveRecordFileName) {
|
||||
private boolean processRecordAfterLogIdReady(Long projectId,
|
||||
Integer lsfxProjectId,
|
||||
CcdiFileUploadRecord record,
|
||||
Integer logId,
|
||||
boolean preserveRecordFileName,
|
||||
CallerContext caller) {
|
||||
log.info("【文件上传】步骤3: 更新状态为解析中, logId={}", logId);
|
||||
record.setLogId(logId);
|
||||
record.setFileStatus("parsing");
|
||||
recordMapper.updateById(record);
|
||||
|
||||
log.info("【文件上传】步骤4: 开始轮询解析状态");
|
||||
boolean parsingComplete = waitForParsingComplete(lsfxProjectId, logId.toString());
|
||||
boolean parsingComplete = waitForParsingComplete(caller, lsfxProjectId, logId.toString());
|
||||
if (!parsingComplete) {
|
||||
throw new RuntimeException("解析超时(超过10分钟),请检查文件格式是否正确");
|
||||
}
|
||||
@@ -805,7 +870,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
statusRequest.setGroupId(lsfxProjectId);
|
||||
statusRequest.setLogId(logId);
|
||||
|
||||
GetFileUploadStatusResponse statusResponse = lsfxClient.getFileUploadStatus(statusRequest);
|
||||
GetFileUploadStatusResponse statusResponse = lsfxClient.getFileUploadStatus(caller, statusRequest);
|
||||
if (statusResponse == null || statusResponse.getData() == null
|
||||
|| statusResponse.getData().getLogs() == null
|
||||
|| statusResponse.getData().getLogs().isEmpty()) {
|
||||
@@ -833,7 +898,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
if (!parseSuccess) {
|
||||
log.warn("【文件上传】步骤6: 解析失败: status={}, desc={}", status, uploadStatusDesc);
|
||||
updateFailedRecord(record, "解析失败: " + uploadStatusDesc);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
log.info("【文件上传】步骤6: 解析成功,保存主体信息");
|
||||
@@ -846,11 +911,11 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
|
||||
log.info("【文件上传】步骤7: 获取流水数据");
|
||||
String fallbackCretNo = extractIdCardFromFileName(record.getFileName());
|
||||
FetchBankStatementResult fetchResult = fetchAndSaveBankStatements(projectId, lsfxProjectId, logId,
|
||||
fallbackCretNo);
|
||||
FetchBankStatementResult fetchResult = fetchAndSaveBankStatements(caller, projectId, lsfxProjectId,
|
||||
logId, fallbackCretNo);
|
||||
if (!fetchResult.isSuccess()) {
|
||||
updateFailedRecord(record, fetchResult.getErrorMessage());
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
record.setFileStatus("parsed_success");
|
||||
@@ -858,6 +923,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
record.setAccountNos(accountNosStr);
|
||||
record.setErrorMessage(null);
|
||||
recordMapper.updateById(record);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -867,7 +933,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
* @param logId 文件ID
|
||||
* @return true=解析完成,false=超时未完成
|
||||
*/
|
||||
private boolean waitForParsingComplete(Integer groupId, String logId) {
|
||||
private boolean waitForParsingComplete(CallerContext caller, Integer groupId, String logId) {
|
||||
log.info("【文件上传】开始轮询解析状态: groupId={}, logId={}", groupId, logId);
|
||||
|
||||
int maxRetries = 300;
|
||||
@@ -876,7 +942,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
for (int i = 1; i <= maxRetries; i++) {
|
||||
try {
|
||||
// 调用检查解析状态接口
|
||||
CheckParseStatusResponse response = lsfxClient.checkParseStatus(groupId, logId);
|
||||
CheckParseStatusResponse response = lsfxClient.checkParseStatus(caller, groupId, logId);
|
||||
|
||||
if (response == null || response.getData() == null) {
|
||||
log.warn("【文件上传】轮询第{}次: 响应数据为空", i);
|
||||
@@ -919,9 +985,8 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
* @param groupId 流水分析平台项目ID
|
||||
* @param logId 文件ID
|
||||
*/
|
||||
private FetchBankStatementResult fetchAndSaveBankStatements(Long projectId, Integer groupId,
|
||||
Integer logId,
|
||||
String fallbackCretNo) {
|
||||
private FetchBankStatementResult fetchAndSaveBankStatements(CallerContext caller, Long projectId, Integer groupId,
|
||||
Integer logId, String fallbackCretNo) {
|
||||
log.info("【文件上传】开始获取流水数据: projectId={}, groupId={}, logId={}",
|
||||
projectId, groupId, logId);
|
||||
|
||||
@@ -934,7 +999,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
firstRequest.setPageNow(1);
|
||||
firstRequest.setPageSize(1);
|
||||
|
||||
GetBankStatementResponse firstResponse = lsfxClient.getBankStatement(firstRequest);
|
||||
GetBankStatementResponse firstResponse = lsfxClient.getBankStatement(caller, firstRequest);
|
||||
if (firstResponse == null || firstResponse.getData() == null) {
|
||||
result.setSuccess(false);
|
||||
result.setErrorMessage("获取流水数据失败: 响应数据为空");
|
||||
@@ -968,7 +1033,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
|
||||
request.setPageNow(pageNow);
|
||||
request.setPageSize(pageSize);
|
||||
|
||||
GetBankStatementResponse response = lsfxClient.getBankStatement(request);
|
||||
GetBankStatementResponse response = lsfxClient.getBankStatement(caller, request);
|
||||
if (response == null || response.getData() == null
|
||||
|| response.getData().getBankStatementList() == null) {
|
||||
result.setSuccess(false);
|
||||
|
||||
@@ -21,7 +21,7 @@ public class CcdiProjectHistoryImportEventListener {
|
||||
event.getTargetProjectId(),
|
||||
event.getTargetLsfxProjectId(),
|
||||
event.getDto(),
|
||||
event.getOperator()
|
||||
event.getCaller()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import com.ruoyi.ccdi.project.mapper.CcdiFileUploadRecordMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiBankTagService;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiProjectHistoryImportService;
|
||||
import com.ruoyi.ccdi.project.service.IProjectCounterpartyEnterpriseService;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
@@ -52,14 +54,17 @@ public class CcdiProjectHistoryImportServiceImpl implements ICcdiProjectHistoryI
|
||||
@Resource
|
||||
private ICcdiBankTagService bankTagService;
|
||||
|
||||
@Resource
|
||||
private IProjectCounterpartyEnterpriseService counterpartyEnterpriseService;
|
||||
|
||||
@Override
|
||||
public void submitImport(Long targetProjectId, Integer targetLsfxProjectId,
|
||||
CcdiProjectImportHistoryDTO dto, String operator) {
|
||||
fileUploadExecutor.execute(() -> executeImport(targetProjectId, targetLsfxProjectId, dto, operator));
|
||||
CcdiProjectImportHistoryDTO dto, CallerContext caller) {
|
||||
fileUploadExecutor.execute(() -> executeImport(targetProjectId, targetLsfxProjectId, dto, caller));
|
||||
}
|
||||
|
||||
private void executeImport(Long targetProjectId, Integer targetLsfxProjectId,
|
||||
CcdiProjectImportHistoryDTO dto, String operator) {
|
||||
CcdiProjectImportHistoryDTO dto, CallerContext caller) {
|
||||
List<CcdiFileUploadRecord> sourceRecords = recordMapper.selectSuccessfulRecordsByProjectIds(dto.getSourceProjectIds());
|
||||
if (sourceRecords == null || sourceRecords.isEmpty()) {
|
||||
log.info("【项目历史导入】无可复制的来源批次: projectId={}, sourceProjectIds={}",
|
||||
@@ -91,7 +96,7 @@ public class CcdiProjectHistoryImportServiceImpl implements ICcdiProjectHistoryI
|
||||
|
||||
if (statementsToInsert.size() > sizeBefore) {
|
||||
recordsToInsert.add(buildHistoryImportRecord(
|
||||
sourceRecord, targetProjectId, targetLsfxProjectId, newBatchId, resolveSourceProjectName(sourceRecord.getProjectId()), operator
|
||||
sourceRecord, targetProjectId, targetLsfxProjectId, newBatchId, resolveSourceProjectName(sourceRecord.getProjectId()), caller.username()
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -105,6 +110,7 @@ public class CcdiProjectHistoryImportServiceImpl implements ICcdiProjectHistoryI
|
||||
if (!statementsToInsert.isEmpty()) {
|
||||
refreshProjectTargetCount(targetProjectId);
|
||||
bankTagService.submitAutoRebuild(targetProjectId, TriggerType.AUTO_BATCH_UPLOAD);
|
||||
counterpartyEnterpriseService.submitReconcile(caller, targetProjectId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiProjectService;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.lsfx.client.LsfxAnalysisClient;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import com.ruoyi.lsfx.domain.request.GetTokenRequest;
|
||||
import com.ruoyi.lsfx.domain.response.GetTokenResponse;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -61,9 +62,9 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public CcdiProjectVO createProject(CcdiProjectSaveDTO dto) {
|
||||
public CcdiProjectVO createProject(CcdiProjectSaveDTO dto, CallerContext caller) {
|
||||
// 1. 调用流水分析平台获取projectId
|
||||
Integer lsfxProjectId = callLsfxPlatform(dto.getProjectName());
|
||||
Integer lsfxProjectId = callLsfxPlatform(dto.getProjectName(), caller);
|
||||
|
||||
// 2. 创建项目实体
|
||||
CcdiProject project = new CcdiProject();
|
||||
@@ -163,18 +164,18 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public CcdiProjectVO importFromHistory(CcdiProjectImportHistoryDTO dto, String operator) {
|
||||
public CcdiProjectVO importFromHistory(CcdiProjectImportHistoryDTO dto, CallerContext caller) {
|
||||
projectAccessService.assertSourceProjectsReadable(dto.getSourceProjectIds());
|
||||
CcdiProjectSaveDTO saveDTO = new CcdiProjectSaveDTO();
|
||||
saveDTO.setProjectName(dto.getProjectName());
|
||||
saveDTO.setDescription(dto.getDescription());
|
||||
saveDTO.setConfigType("default");
|
||||
CcdiProjectVO project = createProject(saveDTO);
|
||||
CcdiProjectVO project = createProject(saveDTO, caller);
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
applicationEventPublisher.publishEvent(
|
||||
new CcdiProjectHistoryImportSubmittedEvent(project.getProjectId(), project.getLsfxProjectId(), dto, operator)
|
||||
new CcdiProjectHistoryImportSubmittedEvent(project.getProjectId(), project.getLsfxProjectId(), dto, caller)
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -380,7 +381,7 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
||||
* @return 流水分析平台项目ID
|
||||
* @throws ServiceException 调用失败或响应无效时抛出
|
||||
*/
|
||||
private Integer callLsfxPlatform(String projectName) {
|
||||
private Integer callLsfxPlatform(String projectName, CallerContext caller) {
|
||||
// 构建请求参数
|
||||
GetTokenRequest request = new GetTokenRequest();
|
||||
request.setProjectNo("902000_" + System.currentTimeMillis());
|
||||
@@ -393,7 +394,7 @@ public class CcdiProjectServiceImpl implements ICcdiProjectService {
|
||||
request.setDepartmentCode("902000");
|
||||
|
||||
// 调用流水分析平台(异常处理和日志已在 LsfxAnalysisClient 中完成)
|
||||
GetTokenResponse response = lsfxAnalysisClient.getToken(request);
|
||||
GetTokenResponse response = lsfxAnalysisClient.getToken(caller, request);
|
||||
|
||||
// 业务层校验:确保响应有效
|
||||
if (response == null || response.getData() == null) {
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
package com.ruoyi.ccdi.project.service.impl;
|
||||
|
||||
import com.ruoyi.ccdi.project.domain.entity.CcdiProjectCounterpartyEnterprise;
|
||||
import com.ruoyi.ccdi.project.domain.entity.CcdiProjectCounterpartyShareholder;
|
||||
import com.ruoyi.ccdi.project.domain.vo.ProjectCounterpartyEnterpriseVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.ProjectCounterpartyShareholderVO;
|
||||
import com.ruoyi.ccdi.project.domain.vo.ProjectCounterpartySyncTaskVO;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiProjectCounterpartyEnterpriseMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiProjectCounterpartyShareholderMapper;
|
||||
import com.ruoyi.ccdi.project.service.IProjectCounterpartyEnterpriseService;
|
||||
import com.ruoyi.info.collection.domain.CcdiEnterpriseInfoQueryCache;
|
||||
import com.ruoyi.info.collection.domain.model.EnterpriseProfile;
|
||||
import com.ruoyi.info.collection.domain.model.EnterpriseProfileQueryResult;
|
||||
import com.ruoyi.info.collection.domain.model.EnterpriseShareholderProfile;
|
||||
import com.ruoyi.info.collection.service.IEnterpriseProfileQueryService;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@Service
|
||||
public class ProjectCounterpartyEnterpriseServiceImpl implements IProjectCounterpartyEnterpriseService {
|
||||
private static final String TASK_PREFIX = "project:counterparty:profile:backfill:";
|
||||
|
||||
@Resource
|
||||
private CcdiBankStatementMapper bankStatementMapper;
|
||||
@Resource
|
||||
private CcdiProjectCounterpartyEnterpriseMapper enterpriseMapper;
|
||||
@Resource
|
||||
private CcdiProjectCounterpartyShareholderMapper shareholderMapper;
|
||||
@Resource
|
||||
private IEnterpriseProfileQueryService queryService;
|
||||
@Resource
|
||||
private TransactionTemplate transactionTemplate;
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
@Resource
|
||||
@Qualifier("enterpriseProfileExecutor")
|
||||
private Executor enterpriseProfileExecutor;
|
||||
|
||||
@Override
|
||||
public ProjectCounterpartyEnterpriseVO getDetail(Long projectId, String counterpartyName) {
|
||||
String name = normalize(counterpartyName);
|
||||
requireCurrentCounterparty(projectId, name);
|
||||
CcdiProjectCounterpartyEnterprise entity = enterpriseMapper.selectByProjectAndName(projectId, name);
|
||||
CcdiEnterpriseInfoQueryCache cache = queryService.findCache(name);
|
||||
Date now = new Date();
|
||||
if (entity == null) {
|
||||
ProjectCounterpartyEnterpriseVO vo = new ProjectCounterpartyEnterpriseVO();
|
||||
vo.setProjectId(projectId);
|
||||
vo.setCounterpartyName(name);
|
||||
vo.setStatus("NOT_FOUND");
|
||||
vo.setCanRefresh(true);
|
||||
vo.setCacheValidDate(cache == null ? null : cache.getValidDate());
|
||||
return vo;
|
||||
}
|
||||
ProjectCounterpartyEnterpriseVO vo = new ProjectCounterpartyEnterpriseVO();
|
||||
BeanUtils.copyProperties(entity, vo);
|
||||
vo.setStatus(cache != null && cache.getValidDate() != null && cache.getValidDate().after(now) ? "SUCCESS" : "EXPIRED");
|
||||
vo.setCanRefresh(!"SUCCESS".equals(vo.getStatus()));
|
||||
vo.setCacheValidDate(cache == null ? null : cache.getValidDate());
|
||||
vo.setShareholders(shareholderMapper.selectByEnterpriseId(entity.getCounterpartyEnterpriseId()).stream()
|
||||
.map(this::toShareholderVO).toList());
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProjectCounterpartyEnterpriseVO refresh(CallerContext caller, Long projectId, String counterpartyName) {
|
||||
String name = normalize(counterpartyName);
|
||||
requireCurrentCounterparty(projectId, name);
|
||||
CcdiProjectCounterpartyEnterprise existing = enterpriseMapper.selectByProjectAndName(projectId, name);
|
||||
CcdiEnterpriseInfoQueryCache cache = queryService.findCache(name);
|
||||
boolean cacheValid = cache != null && cache.getValidDate() != null && cache.getValidDate().after(new Date());
|
||||
if (existing != null && cacheValid) {
|
||||
throw new IllegalStateException("工商缓存仍在有效期内,无需重新查询");
|
||||
}
|
||||
EnterpriseProfileQueryResult result = queryService.query(caller, name, !cacheValid);
|
||||
upsert(projectId, name, result, caller.username());
|
||||
return getDetail(projectId, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void submitReconcile(CallerContext caller, Long projectId) {
|
||||
CompletableFuture.runAsync(() -> reconcile(caller, projectId, null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String startBackfill(CallerContext caller) {
|
||||
String taskId = UUID.randomUUID().toString().replace("-", "");
|
||||
List<Long> projectIds = bankStatementMapper.selectDistinctProjectIdsWithCounterparties();
|
||||
int total = projectIds.stream().mapToInt(id -> bankStatementMapper.selectDistinctCounterpartyNames(id).size()).sum();
|
||||
initializeTask(taskId, total);
|
||||
CompletableFuture.runAsync(() -> {
|
||||
TaskCounters counters = new TaskCounters();
|
||||
for (Long projectId : projectIds) {
|
||||
reconcile(caller, projectId, new Progress(taskId, counters));
|
||||
}
|
||||
updateTask(taskId, "COMPLETED", counters);
|
||||
});
|
||||
return taskId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProjectCounterpartySyncTaskVO getBackfillStatus(String taskId) {
|
||||
Map<Object, Object> values = redisTemplate.opsForHash().entries(taskKey(taskId));
|
||||
if (values.isEmpty()) {
|
||||
throw new IllegalArgumentException("任务不存在或已过期");
|
||||
}
|
||||
ProjectCounterpartySyncTaskVO vo = new ProjectCounterpartySyncTaskVO();
|
||||
vo.setTaskId(string(values.get("taskId")));
|
||||
vo.setStatus(string(values.get("status")));
|
||||
vo.setTotalCount(number(values.get("totalCount")));
|
||||
vo.setCompletedCount(number(values.get("completedCount")));
|
||||
vo.setSuccessCount(number(values.get("successCount")));
|
||||
vo.setSkippedCount(number(values.get("skippedCount")));
|
||||
vo.setFailureCount(number(values.get("failureCount")));
|
||||
vo.setMessage(string(values.get("message")));
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteProjectData(Long projectId) {
|
||||
shareholderMapper.deleteByProjectId(projectId);
|
||||
enterpriseMapper.deleteByProjectId(projectId);
|
||||
}
|
||||
|
||||
private void reconcile(CallerContext caller, Long projectId, Progress progress) {
|
||||
List<String> names = bankStatementMapper.selectDistinctCounterpartyNames(projectId);
|
||||
if (names.isEmpty()) {
|
||||
transactionTemplate.executeWithoutResult(status -> deleteProjectData(projectId));
|
||||
return;
|
||||
}
|
||||
for (int start = 0; start < names.size(); start += 100) {
|
||||
List<String> batch = names.subList(start, Math.min(start + 100, names.size()));
|
||||
List<CompletableFuture<Void>> futures = batch.stream().map(name -> CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
EnterpriseProfileQueryResult result = queryService.query(caller, name, false);
|
||||
upsert(projectId, name, result, caller.username());
|
||||
if (progress != null) progress.counters.success.incrementAndGet();
|
||||
} catch (Exception e) {
|
||||
if (progress != null) progress.counters.failure.incrementAndGet();
|
||||
} finally {
|
||||
if (progress != null) {
|
||||
progress.counters.completed.incrementAndGet();
|
||||
updateTask(progress.taskId, "PROCESSING", progress.counters);
|
||||
}
|
||||
}
|
||||
}, enterpriseProfileExecutor)).toList();
|
||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
||||
}
|
||||
removeStale(projectId, new HashSet<>(names));
|
||||
}
|
||||
|
||||
private void upsert(Long projectId, String name, EnterpriseProfileQueryResult result, String username) {
|
||||
transactionTemplate.executeWithoutResult(status -> {
|
||||
EnterpriseProfile profile = result.profile();
|
||||
CcdiProjectCounterpartyEnterprise entity = enterpriseMapper.selectByProjectAndName(projectId, name);
|
||||
Date now = new Date();
|
||||
boolean insert = entity == null;
|
||||
if (insert) {
|
||||
entity = new CcdiProjectCounterpartyEnterprise();
|
||||
entity.setProjectId(projectId);
|
||||
entity.setCounterpartyName(name);
|
||||
entity.setCreateBy(username);
|
||||
entity.setCreateTime(now);
|
||||
}
|
||||
entity.setSocialCreditCode(profile.getCreditCode());
|
||||
entity.setEnterpriseName(profile.getEnterpriseName());
|
||||
entity.setRegisteredCapital(profile.getRegisteredCapital());
|
||||
entity.setRegisteredCapitalUnit(profile.getRegisteredCapitalUnit());
|
||||
entity.setRegisterDate(profile.getRegisterDate());
|
||||
entity.setEstablishDate(profile.getEstablishDate());
|
||||
entity.setIndustryCode(profile.getIndustryCode());
|
||||
entity.setIndustryName(profile.getIndustryName());
|
||||
entity.setOrganizationTypeCode(profile.getOrganizationTypeCode());
|
||||
entity.setOrganizationTypeName(profile.getOrganizationTypeName());
|
||||
entity.setRegionCode(profile.getRegionCode());
|
||||
entity.setRegionName(profile.getRegionName());
|
||||
entity.setRegisterAddress(profile.getRegisterAddress());
|
||||
entity.setEmployeeCount(profile.getEmployeeCount());
|
||||
entity.setLegalRepresentative(profile.getLegalRepresentative());
|
||||
entity.setCacheInfoId(result.cacheInfoId());
|
||||
entity.setSyncTime(now);
|
||||
entity.setUpdateBy(username);
|
||||
entity.setUpdateTime(now);
|
||||
if (insert) enterpriseMapper.insert(entity); else enterpriseMapper.updateById(entity);
|
||||
shareholderMapper.deleteByEnterpriseId(entity.getCounterpartyEnterpriseId());
|
||||
for (EnterpriseShareholderProfile item : profile.getShareholders()) {
|
||||
CcdiProjectCounterpartyShareholder shareholder = new CcdiProjectCounterpartyShareholder();
|
||||
shareholder.setCounterpartyEnterpriseId(entity.getCounterpartyEnterpriseId());
|
||||
shareholder.setSequenceNo(item.getSequence());
|
||||
shareholder.setShareholderName(item.getShareholderName());
|
||||
shareholder.setStockPercent(item.getStockPercent());
|
||||
shareholder.setSubscribedCapital(item.getSubscribedCapital());
|
||||
shareholder.setCapitalUnit(item.getCapitalUnit());
|
||||
shareholder.setCreateTime(now);
|
||||
shareholderMapper.insert(shareholder);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void removeStale(Long projectId, Set<String> currentNames) {
|
||||
transactionTemplate.executeWithoutResult(status -> enterpriseMapper.selectByProjectId(projectId).stream()
|
||||
.filter(item -> !currentNames.contains(item.getCounterpartyName()))
|
||||
.forEach(item -> {
|
||||
shareholderMapper.deleteByEnterpriseId(item.getCounterpartyEnterpriseId());
|
||||
enterpriseMapper.deleteById(item.getCounterpartyEnterpriseId());
|
||||
}));
|
||||
}
|
||||
|
||||
private void requireCurrentCounterparty(Long projectId, String name) {
|
||||
if (projectId == null || !StringUtils.hasText(name)
|
||||
|| !bankStatementMapper.selectDistinctCounterpartyNames(projectId).contains(name)) {
|
||||
throw new IllegalArgumentException("该对手方不在项目当前流水中");
|
||||
}
|
||||
}
|
||||
|
||||
private ProjectCounterpartyShareholderVO toShareholderVO(CcdiProjectCounterpartyShareholder entity) {
|
||||
ProjectCounterpartyShareholderVO vo = new ProjectCounterpartyShareholderVO();
|
||||
BeanUtils.copyProperties(entity, vo);
|
||||
return vo;
|
||||
}
|
||||
|
||||
private String normalize(String value) { return value == null ? null : value.trim(); }
|
||||
private String taskKey(String taskId) { return TASK_PREFIX + taskId; }
|
||||
private String string(Object value) { return value == null ? null : value.toString(); }
|
||||
private Integer number(Object value) { return value == null ? 0 : Integer.valueOf(value.toString()); }
|
||||
|
||||
private void initializeTask(String taskId, int total) {
|
||||
Map<String, Object> values = new LinkedHashMap<>();
|
||||
values.put("taskId", taskId);
|
||||
values.put("status", "PROCESSING");
|
||||
values.put("totalCount", total);
|
||||
values.put("completedCount", 0);
|
||||
values.put("successCount", 0);
|
||||
values.put("skippedCount", 0);
|
||||
values.put("failureCount", 0);
|
||||
values.put("message", "正在补全项目对手方工商信息");
|
||||
redisTemplate.opsForHash().putAll(taskKey(taskId), values);
|
||||
redisTemplate.expire(taskKey(taskId), 7, TimeUnit.DAYS);
|
||||
}
|
||||
|
||||
private void updateTask(String taskId, String status, TaskCounters counters) {
|
||||
Map<String, Object> values = new LinkedHashMap<>();
|
||||
values.put("status", status);
|
||||
values.put("completedCount", counters.completed.get());
|
||||
values.put("successCount", counters.success.get());
|
||||
values.put("skippedCount", counters.skipped.get());
|
||||
values.put("failureCount", counters.failure.get());
|
||||
values.put("message", "COMPLETED".equals(status) ? "项目对手方工商信息补全完成" : "正在补全项目对手方工商信息");
|
||||
redisTemplate.opsForHash().putAll(taskKey(taskId), values);
|
||||
}
|
||||
|
||||
private static class TaskCounters {
|
||||
private final AtomicInteger completed = new AtomicInteger();
|
||||
private final AtomicInteger success = new AtomicInteger();
|
||||
private final AtomicInteger skipped = new AtomicInteger();
|
||||
private final AtomicInteger failure = new AtomicInteger();
|
||||
}
|
||||
|
||||
private record Progress(String taskId, TaskCounters counters) { }
|
||||
}
|
||||
@@ -139,6 +139,24 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
and trim(bs.cret_no) != ''
|
||||
</select>
|
||||
|
||||
<select id="selectDistinctCounterpartyNames" resultType="java.lang.String">
|
||||
select distinct trim(CUSTOMER_ACCOUNT_NAME)
|
||||
from ccdi_bank_statement
|
||||
where project_id = #{projectId}
|
||||
and CUSTOMER_ACCOUNT_NAME is not null
|
||||
and trim(CUSTOMER_ACCOUNT_NAME) != ''
|
||||
order by trim(CUSTOMER_ACCOUNT_NAME)
|
||||
</select>
|
||||
|
||||
<select id="selectDistinctProjectIdsWithCounterparties" resultType="java.lang.Long">
|
||||
select distinct project_id
|
||||
from ccdi_bank_statement
|
||||
where project_id is not null
|
||||
and CUSTOMER_ACCOUNT_NAME is not null
|
||||
and trim(CUSTOMER_ACCOUNT_NAME) != ''
|
||||
order by project_id
|
||||
</select>
|
||||
|
||||
<sql id="parsedTrxDateExpr">
|
||||
CASE
|
||||
WHEN bs.TRX_DATE IS NULL OR TRIM(bs.TRX_DATE) = '' THEN NULL
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.ccdi.project.mapper.CcdiProjectCounterpartyEnterpriseMapper">
|
||||
<select id="selectByProjectAndName" resultType="com.ruoyi.ccdi.project.domain.entity.CcdiProjectCounterpartyEnterprise">
|
||||
select * from ccdi_project_counterparty_enterprise
|
||||
where project_id = #{projectId} and counterparty_name = #{counterpartyName}
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectByProjectId" resultType="com.ruoyi.ccdi.project.domain.entity.CcdiProjectCounterpartyEnterprise">
|
||||
select * from ccdi_project_counterparty_enterprise where project_id = #{projectId}
|
||||
</select>
|
||||
|
||||
<delete id="deleteByProjectId">
|
||||
delete from ccdi_project_counterparty_enterprise where project_id = #{projectId}
|
||||
</delete>
|
||||
</mapper>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.ccdi.project.mapper.CcdiProjectCounterpartyShareholderMapper">
|
||||
<select id="selectByEnterpriseId" resultType="com.ruoyi.ccdi.project.domain.entity.CcdiProjectCounterpartyShareholder">
|
||||
select * from ccdi_project_counterparty_shareholder
|
||||
where counterparty_enterprise_id = #{counterpartyEnterpriseId}
|
||||
order by shareholder_seq, shareholder_id
|
||||
</select>
|
||||
|
||||
<delete id="deleteByEnterpriseId">
|
||||
delete from ccdi_project_counterparty_shareholder
|
||||
where counterparty_enterprise_id = #{counterpartyEnterpriseId}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteByProjectId">
|
||||
delete s from ccdi_project_counterparty_shareholder s
|
||||
inner join ccdi_project_counterparty_enterprise e
|
||||
on e.counterparty_enterprise_id = s.counterparty_enterprise_id
|
||||
where e.project_id = #{projectId}
|
||||
</delete>
|
||||
</mapper>
|
||||
@@ -6,6 +6,7 @@ import com.ruoyi.ccdi.project.service.ICcdiFileUploadService;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -30,6 +31,7 @@ import static org.mockito.Mockito.when;
|
||||
class CcdiFileUploadControllerTest {
|
||||
|
||||
private static final Long PROJECT_ID = 100L;
|
||||
private static final CallerContext CALLER = CallerContext.of(9527L, "admin");
|
||||
|
||||
@InjectMocks
|
||||
private CcdiFileUploadController controller;
|
||||
@@ -72,7 +74,7 @@ class CcdiFileUploadControllerTest {
|
||||
};
|
||||
|
||||
setLoginUser(9527L, "admin");
|
||||
when(fileUploadService.batchUploadFiles(PROJECT_ID, files, "admin"))
|
||||
when(fileUploadService.batchUploadFiles(PROJECT_ID, files, CALLER))
|
||||
.thenReturn("batch-1");
|
||||
|
||||
AjaxResult result = controller.batchUpload(PROJECT_ID, files);
|
||||
@@ -80,7 +82,7 @@ class CcdiFileUploadControllerTest {
|
||||
assertEquals(200, result.get("code"));
|
||||
assertEquals("batch-1", result.get("data"));
|
||||
verify(projectAccessService).assertCanOperate(PROJECT_ID);
|
||||
verify(fileUploadService).batchUploadFiles(PROJECT_ID, files, "admin");
|
||||
verify(fileUploadService).batchUploadFiles(PROJECT_ID, files, CALLER);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -93,7 +95,8 @@ class CcdiFileUploadControllerTest {
|
||||
dto.setEndDate("2026-03-10");
|
||||
|
||||
setLoginUser(9527L, "admin");
|
||||
when(fileUploadService.submitPullBankInfo(PROJECT_ID, dto.getIdCards(), "ZJRCU", "2026-03-01", "2026-03-10", 9527L, "admin"))
|
||||
when(fileUploadService.submitPullBankInfo(PROJECT_ID, dto.getIdCards(), "ZJRCU",
|
||||
"2026-03-01", "2026-03-10", CALLER))
|
||||
.thenReturn("batch-1");
|
||||
|
||||
AjaxResult result = controller.pullBankInfo(dto);
|
||||
@@ -109,7 +112,7 @@ class CcdiFileUploadControllerTest {
|
||||
dto.setDataChannelCode("JZL");
|
||||
|
||||
setLoginUser(9527L, "admin");
|
||||
when(fileUploadService.submitPullBankInfo(PROJECT_ID, dto.getIdCards(), "JZL", null, null, 9527L, "admin"))
|
||||
when(fileUploadService.submitPullBankInfo(PROJECT_ID, dto.getIdCards(), "JZL", null, null, CALLER))
|
||||
.thenReturn("batch-1");
|
||||
|
||||
AjaxResult result = controller.pullBankInfo(dto);
|
||||
@@ -131,16 +134,16 @@ class CcdiFileUploadControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteFile_shouldUseCurrentLoginUserId() {
|
||||
void deleteFile_shouldUseCurrentLoginUser() {
|
||||
setLoginUser(9527L, "admin");
|
||||
when(fileUploadService.deleteFileUploadRecord(123L, 9527L))
|
||||
when(fileUploadService.deleteFileUploadRecord(123L, CALLER))
|
||||
.thenReturn("删除成功");
|
||||
|
||||
AjaxResult result = controller.deleteFile(123L);
|
||||
|
||||
assertEquals(200, result.get("code"));
|
||||
assertEquals("删除成功", result.get("msg"));
|
||||
verify(fileUploadService).deleteFileUploadRecord(123L, 9527L);
|
||||
verify(fileUploadService).deleteFileUploadRecord(123L, CALLER);
|
||||
}
|
||||
|
||||
private void setLoginUser(Long userId, String username) {
|
||||
|
||||
@@ -14,9 +14,11 @@ import com.ruoyi.ccdi.project.mapper.CcdiFileUploadRecordMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiBankTagService;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiProjectService;
|
||||
import com.ruoyi.ccdi.project.service.IProjectCounterpartyEnterpriseService;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.lsfx.client.LsfxAnalysisClient;
|
||||
import com.ruoyi.lsfx.constants.LsfxConstants;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import com.ruoyi.lsfx.domain.request.FetchInnerFlowRequest;
|
||||
import com.ruoyi.lsfx.domain.request.GetBankStatementRequest;
|
||||
import com.ruoyi.lsfx.domain.response.CheckParseStatusResponse;
|
||||
@@ -34,6 +36,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@@ -63,6 +66,7 @@ import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CcdiFileUploadServiceImplTest {
|
||||
@@ -71,6 +75,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
private static final Integer LSFX_PROJECT_ID = 200;
|
||||
private static final Long RECORD_ID = 300L;
|
||||
private static final Integer LOG_ID = 400;
|
||||
private static final CallerContext CALLER = CallerContext.of(9527L, "admin");
|
||||
private static final int MAX_ERROR_MESSAGE_LENGTH = 2000;
|
||||
|
||||
@InjectMocks
|
||||
@@ -97,6 +102,9 @@ class CcdiFileUploadServiceImplTest {
|
||||
@Mock
|
||||
private ICcdiProjectService projectService;
|
||||
|
||||
@Mock
|
||||
private IProjectCounterpartyEnterpriseService counterpartyEnterpriseService;
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@@ -149,8 +157,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
LsfxConstants.DATA_CHANNEL_ZJRCU,
|
||||
"2026-03-01",
|
||||
"2026-03-10",
|
||||
9527L,
|
||||
"admin"
|
||||
CALLER
|
||||
);
|
||||
|
||||
assertNotNull(batchId);
|
||||
@@ -177,8 +184,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
LsfxConstants.DATA_CHANNEL_ZJRCU,
|
||||
"2026-01-01",
|
||||
"2026-01-31",
|
||||
1L,
|
||||
"tester"
|
||||
CALLER
|
||||
));
|
||||
}
|
||||
|
||||
@@ -195,7 +201,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
);
|
||||
|
||||
assertThrows(ServiceException.class,
|
||||
() -> service.batchUploadFiles(PROJECT_ID, new MultipartFile[]{file}, "tester"));
|
||||
() -> service.batchUploadFiles(PROJECT_ID, new MultipartFile[]{file}, CALLER));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -222,7 +228,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
|
||||
TransactionSynchronizationManager.initSynchronization();
|
||||
try {
|
||||
String batchId = service.batchUploadFiles(PROJECT_ID, new MultipartFile[]{file}, "tester");
|
||||
String batchId = service.batchUploadFiles(PROJECT_ID, new MultipartFile[]{file}, CALLER);
|
||||
|
||||
assertNotNull(batchId);
|
||||
assertNotNull(inserted.get());
|
||||
@@ -251,12 +257,12 @@ class CcdiFileUploadServiceImplTest {
|
||||
);
|
||||
|
||||
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
|
||||
() -> service.batchUploadFiles(PROJECT_ID, new MultipartFile[]{file}, "tester"));
|
||||
() -> service.batchUploadFiles(PROJECT_ID, new MultipartFile[]{file}, CALLER));
|
||||
|
||||
assertTrue(exception.getMessage().contains("身份证"));
|
||||
assertFalse(Files.exists(tempDir.resolve("temp")));
|
||||
verify(recordMapper, never()).insertBatch(any());
|
||||
verify(lsfxClient, never()).uploadFile(any(), org.mockito.ArgumentMatchers.<java.io.File>any(), any());
|
||||
verify(lsfxClient, never()).uploadFile(any(), any(), org.mockito.ArgumentMatchers.<java.io.File>any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -272,12 +278,12 @@ class CcdiFileUploadServiceImplTest {
|
||||
);
|
||||
|
||||
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
|
||||
() -> service.batchUploadFiles(PROJECT_ID, new MultipartFile[]{file}, "tester"));
|
||||
() -> service.batchUploadFiles(PROJECT_ID, new MultipartFile[]{file}, CALLER));
|
||||
|
||||
assertTrue(exception.getMessage().contains("文件名不能为空"));
|
||||
assertFalse(Files.exists(tempDir.resolve("temp")));
|
||||
verify(recordMapper, never()).insertBatch(any());
|
||||
verify(lsfxClient, never()).uploadFile(any(), org.mockito.ArgumentMatchers.<java.io.File>any(), any());
|
||||
verify(lsfxClient, never()).uploadFile(any(), any(), org.mockito.ArgumentMatchers.<java.io.File>any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -293,6 +299,29 @@ class CcdiFileUploadServiceImplTest {
|
||||
assertFalse(Files.exists(batchLogDir));
|
||||
}
|
||||
|
||||
@Test
|
||||
void submitTasksAsync_shouldKeepEachCapturedCallerAfterSecurityContextIsCleared() throws Exception {
|
||||
setField("fileUploadExecutor", (Executor) Runnable::run);
|
||||
CallerContext callerA = CallerContext.of(101L, "userA");
|
||||
CallerContext callerB = CallerContext.of(202L, "userB");
|
||||
Path fileA = createTempFile();
|
||||
Path fileB = createTempFile();
|
||||
CcdiFileUploadRecord recordA = buildRecord();
|
||||
CcdiFileUploadRecord recordB = buildRecord();
|
||||
recordB.setId(RECORD_ID + 1);
|
||||
when(lsfxClient.uploadFile(any(CallerContext.class), eq(LSFX_PROJECT_ID), any(), any()))
|
||||
.thenThrow(new RuntimeException("stop after caller capture"));
|
||||
|
||||
SecurityContextHolder.clearContext();
|
||||
invokeSubmitTasksAsync(List.of(fileA.toString()), List.of(recordA), "batch-a", callerA);
|
||||
invokeSubmitTasksAsync(List.of(fileB.toString()), List.of(recordB), "batch-b", callerB);
|
||||
|
||||
ArgumentCaptor<CallerContext> callerCaptor = ArgumentCaptor.forClass(CallerContext.class);
|
||||
verify(lsfxClient, org.mockito.Mockito.times(2)).uploadFile(
|
||||
callerCaptor.capture(), eq(LSFX_PROJECT_ID), any(), any());
|
||||
assertEquals(List.of(callerA, callerB), callerCaptor.getAllValues());
|
||||
}
|
||||
|
||||
@Test
|
||||
void handleTagRebuildAfterBatchCompletion_shouldLogSkipWhenAllRecordsFailed() {
|
||||
Logger logger = (Logger) LoggerFactory.getLogger(CcdiFileUploadServiceImpl.class);
|
||||
@@ -352,18 +381,18 @@ class CcdiFileUploadServiceImplTest {
|
||||
AtomicInteger sequence = new AtomicInteger();
|
||||
captureRecordStatus(events, sequence);
|
||||
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
when(lsfxClient.uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenThrow(new RuntimeException("bank statement fetch failed"));
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record, CALLER);
|
||||
|
||||
assertTrue(events.stream().anyMatch(event -> event.endsWith("record:parsed_failed")));
|
||||
assertFalse(events.stream().anyMatch(event -> event.endsWith("record:parsed_success")));
|
||||
@@ -379,12 +408,12 @@ class CcdiFileUploadServiceImplTest {
|
||||
when(projectMapper.selectById(PROJECT_ID)).thenReturn(project);
|
||||
when(bankStatementMapper.countMatchedStaffCountByProjectId(PROJECT_ID)).thenReturn(1);
|
||||
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
when(lsfxClient.uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
events.add(sequence.incrementAndGet() + ":bank-fetch");
|
||||
return buildEmptyBankStatementResponse();
|
||||
@@ -393,7 +422,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record, CALLER);
|
||||
|
||||
int fetchIndex = findEventIndex(events, "bank-fetch");
|
||||
int successIndex = findEventIndex(events, "record:parsed_success");
|
||||
@@ -406,18 +435,18 @@ class CcdiFileUploadServiceImplTest {
|
||||
|
||||
@Test
|
||||
void processFileAsync_shouldCleanupInsertedStatementsWhenFetchFails() throws IOException {
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
when(lsfxClient.uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenThrow(new RuntimeException("bank statement fetch failed"));
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record, CALLER);
|
||||
|
||||
verify(bankStatementMapper).deleteByProjectIdAndBatchId(PROJECT_ID, LOG_ID);
|
||||
}
|
||||
@@ -427,11 +456,11 @@ class CcdiFileUploadServiceImplTest {
|
||||
GetFileUploadStatusResponse statusResponse = buildParsedSuccessStatusResponse("XX身份证.xlsx");
|
||||
statusResponse.getData().getLogs().get(0).setFileSize(2048L);
|
||||
|
||||
when(lsfxClient.fetchInnerFlow(any())).thenReturn(buildFetchInnerFlowResponse(LOG_ID));
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
when(lsfxClient.fetchInnerFlow(eq(CALLER), any())).thenReturn(buildFetchInnerFlowResponse(LOG_ID));
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(statusResponse);
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any())).thenReturn(statusResponse);
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenReturn(buildEmptyBankStatementResponse());
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
@@ -444,7 +473,8 @@ class CcdiFileUploadServiceImplTest {
|
||||
"110101199001018888",
|
||||
LsfxConstants.DATA_CHANNEL_ZJRCU,
|
||||
"2026-03-01",
|
||||
"2026-03-10"
|
||||
"2026-03-10",
|
||||
CALLER
|
||||
);
|
||||
|
||||
verify(recordMapper, org.mockito.Mockito.atLeastOnce()).updateById(
|
||||
@@ -457,11 +487,11 @@ class CcdiFileUploadServiceImplTest {
|
||||
|
||||
@Test
|
||||
void processPullBankInfoAsync_shouldFetchJzlWithZeroDateRange() {
|
||||
when(lsfxClient.fetchInnerFlow(any())).thenReturn(buildFetchInnerFlowResponse(LOG_ID));
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
when(lsfxClient.fetchInnerFlow(eq(CALLER), any())).thenReturn(buildFetchInnerFlowResponse(LOG_ID));
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenReturn(buildEmptyBankStatementResponse());
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
@@ -473,10 +503,11 @@ class CcdiFileUploadServiceImplTest {
|
||||
"110101199001018888",
|
||||
LsfxConstants.DATA_CHANNEL_JZL,
|
||||
null,
|
||||
null
|
||||
null,
|
||||
CALLER
|
||||
);
|
||||
|
||||
verify(lsfxClient).fetchInnerFlow(argThat((FetchInnerFlowRequest request) ->
|
||||
verify(lsfxClient).fetchInnerFlow(eq(CALLER), argThat((FetchInnerFlowRequest request) ->
|
||||
LsfxConstants.DATA_CHANNEL_JZL.equals(request.getDataChannelCode())
|
||||
&& Integer.valueOf(0).equals(request.getDataStartDateId())
|
||||
&& Integer.valueOf(0).equals(request.getDataEndDateId())
|
||||
@@ -484,22 +515,81 @@ class CcdiFileUploadServiceImplTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void processFileAsync_shouldUploadToLsfxWithOriginalRecordFileName() throws IOException {
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), eq("原始流水.xlsx")))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
void processPullBankInfoAsync_shouldProcessAllJzlLogIds() {
|
||||
Integer secondLogId = LOG_ID + 1;
|
||||
Integer thirdLogId = LOG_ID + 2;
|
||||
List<CcdiFileUploadRecord> insertedAdditionalRecords = new ArrayList<>();
|
||||
|
||||
doAnswer(invocation -> {
|
||||
List<CcdiFileUploadRecord> records = invocation.getArgument(0);
|
||||
for (int i = 0; i < records.size(); i++) {
|
||||
records.get(i).setId(RECORD_ID + i + 1);
|
||||
CcdiFileUploadRecord snapshot = new CcdiFileUploadRecord();
|
||||
snapshot.setId(records.get(i).getId());
|
||||
snapshot.setFileName(records.get(i).getFileName());
|
||||
snapshot.setAccountNos(records.get(i).getAccountNos());
|
||||
snapshot.setUploadUser(records.get(i).getUploadUser());
|
||||
snapshot.setFileStatus(records.get(i).getFileStatus());
|
||||
insertedAdditionalRecords.add(snapshot);
|
||||
}
|
||||
return records.size();
|
||||
}).when(recordMapper).insertBatch(any());
|
||||
|
||||
when(lsfxClient.fetchInnerFlow(eq(CALLER), any()))
|
||||
.thenReturn(buildFetchInnerFlowResponse(LOG_ID, secondLogId, thirdLogId));
|
||||
when(lsfxClient.checkParseStatus(eq(CALLER), eq(LSFX_PROJECT_ID), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenReturn(buildEmptyBankStatementResponse());
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
record.setUploadUser("admin");
|
||||
|
||||
boolean success = service.processPullBankInfoAsync(
|
||||
PROJECT_ID,
|
||||
LSFX_PROJECT_ID,
|
||||
record,
|
||||
"110101199001018888",
|
||||
LsfxConstants.DATA_CHANNEL_JZL,
|
||||
null,
|
||||
null,
|
||||
CALLER
|
||||
);
|
||||
|
||||
assertTrue(success);
|
||||
assertEquals(2, insertedAdditionalRecords.size());
|
||||
assertEquals("110101199001018888", insertedAdditionalRecords.get(0).getFileName());
|
||||
assertEquals("110101199001018888", insertedAdditionalRecords.get(0).getAccountNos());
|
||||
assertEquals("admin", insertedAdditionalRecords.get(0).getUploadUser());
|
||||
verify(lsfxClient).checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID));
|
||||
verify(lsfxClient).checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(secondLogId));
|
||||
verify(lsfxClient).checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(thirdLogId));
|
||||
verify(lsfxClient).getBankStatement(eq(CALLER), org.mockito.ArgumentMatchers.<GetBankStatementRequest>argThat(request ->
|
||||
LOG_ID.equals(request.getLogId())));
|
||||
verify(lsfxClient).getBankStatement(eq(CALLER), org.mockito.ArgumentMatchers.<GetBankStatementRequest>argThat(request ->
|
||||
secondLogId.equals(request.getLogId())));
|
||||
verify(lsfxClient).getBankStatement(eq(CALLER), org.mockito.ArgumentMatchers.<GetBankStatementRequest>argThat(request ->
|
||||
thirdLogId.equals(request.getLogId())));
|
||||
}
|
||||
|
||||
@Test
|
||||
void processFileAsync_shouldUploadToLsfxWithOriginalRecordFileName() throws IOException {
|
||||
when(lsfxClient.uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), any(), eq("原始流水.xlsx")))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenReturn(buildEmptyBankStatementResponse());
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
record.setFileName("原始流水.xlsx");
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record, CALLER);
|
||||
|
||||
verify(lsfxClient).uploadFile(eq(LSFX_PROJECT_ID), argThat(file ->
|
||||
verify(lsfxClient).uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), argThat(file ->
|
||||
file.getName().startsWith("upload-") && file.getName().endsWith(".xlsx")
|
||||
), eq("原始流水.xlsx"));
|
||||
}
|
||||
@@ -517,14 +607,15 @@ class CcdiFileUploadServiceImplTest {
|
||||
project.setProjectId(PROJECT_ID);
|
||||
when(projectMapper.selectById(PROJECT_ID)).thenReturn(project);
|
||||
when(bankStatementMapper.countMatchedStaffCountByProjectId(PROJECT_ID)).thenReturn(1);
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), eq("张三_330101199001010011_流水.xlsx")))
|
||||
when(lsfxClient.uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), any(),
|
||||
eq("张三_330101199001010011_流水.xlsx")))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
GetBankStatementRequest request = invocation.getArgument(0);
|
||||
GetBankStatementRequest request = invocation.getArgument(1);
|
||||
if (Integer.valueOf(1).equals(request.getPageSize())) {
|
||||
return buildBankStatementCountResponse(1);
|
||||
}
|
||||
@@ -535,7 +626,8 @@ class CcdiFileUploadServiceImplTest {
|
||||
record.setFileName("张三_330101199001010011_流水.xlsx");
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record,
|
||||
CALLER);
|
||||
|
||||
assertNotNull(insertedStatements.get());
|
||||
assertEquals(1, insertedStatements.get().size());
|
||||
@@ -544,20 +636,20 @@ class CcdiFileUploadServiceImplTest {
|
||||
|
||||
@Test
|
||||
void processFileAsync_shouldKeepOriginalFileNameWhenStatusReturnsDifferentName() throws IOException {
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
when(lsfxClient.uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any()))
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any()))
|
||||
.thenReturn(buildParsedSuccessStatusResponse("平台返回文件名.xlsx"));
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenReturn(buildEmptyBankStatementResponse());
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
record.setFileName("原始流水.xlsx");
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record, CALLER);
|
||||
|
||||
verify(recordMapper, org.mockito.Mockito.atLeastOnce()).updateById(
|
||||
org.mockito.ArgumentMatchers.<CcdiFileUploadRecord>argThat(item ->
|
||||
@@ -573,17 +665,17 @@ class CcdiFileUploadServiceImplTest {
|
||||
logItem.setStatus(-1);
|
||||
logItem.setUploadStatusDesc("parse.failed");
|
||||
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
when(lsfxClient.uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(statusResponse);
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any())).thenReturn(statusResponse);
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
record.setFileName("原始流水.xlsx");
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record, CALLER);
|
||||
|
||||
verify(recordMapper, org.mockito.Mockito.atLeastOnce()).updateById(
|
||||
org.mockito.ArgumentMatchers.<CcdiFileUploadRecord>argThat(item ->
|
||||
@@ -607,15 +699,16 @@ class CcdiFileUploadServiceImplTest {
|
||||
when(bankStatementMapper.countMatchedStaffCountByProjectId(PROJECT_ID)).thenReturn(2);
|
||||
when(recordMapper.updateById(any(CcdiFileUploadRecord.class))).thenReturn(1);
|
||||
|
||||
String result = service.deleteFileUploadRecord(RECORD_ID, 9527L);
|
||||
String result = service.deleteFileUploadRecord(RECORD_ID, CALLER);
|
||||
|
||||
assertEquals("删除成功,已开始项目重新打标", result);
|
||||
verify(lsfxClient, never()).deleteFiles(any());
|
||||
verify(lsfxClient, never()).deleteFiles(any(), any());
|
||||
verify(bankStatementMapper).deleteByProjectIdAndBatchId(PROJECT_ID, LOG_ID);
|
||||
verify(recordMapper).updateById(org.mockito.ArgumentMatchers.<CcdiFileUploadRecord>argThat(item ->
|
||||
RECORD_ID.equals(item.getId()) && "deleted".equals(item.getFileStatus())
|
||||
));
|
||||
verify(bankTagService).submitAutoRebuild(PROJECT_ID, TriggerType.AUTO_FILE_DELETE);
|
||||
verify(counterpartyEnterpriseService).submitReconcile(CALLER, PROJECT_ID);
|
||||
verify(projectMapper).updateById(org.mockito.ArgumentMatchers.<CcdiProject>argThat(item ->
|
||||
PROJECT_ID.equals(item.getProjectId()) && Integer.valueOf(2).equals(item.getTargetCount())
|
||||
));
|
||||
@@ -628,7 +721,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
when(recordMapper.selectById(RECORD_ID)).thenReturn(record);
|
||||
|
||||
RuntimeException exception = assertThrows(RuntimeException.class,
|
||||
() -> service.deleteFileUploadRecord(RECORD_ID, 9527L));
|
||||
() -> service.deleteFileUploadRecord(RECORD_ID, CALLER));
|
||||
|
||||
assertTrue(exception.getMessage().contains("仅支持删除解析成功文件"));
|
||||
}
|
||||
@@ -641,10 +734,10 @@ class CcdiFileUploadServiceImplTest {
|
||||
when(recordMapper.selectById(RECORD_ID)).thenReturn(record);
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> service.deleteFileUploadRecord(RECORD_ID, 9527L));
|
||||
() -> service.deleteFileUploadRecord(RECORD_ID, CALLER));
|
||||
|
||||
assertTrue(exception.getMessage().contains("历史导入文件不支持删除"));
|
||||
verify(lsfxClient, never()).deleteFiles(any());
|
||||
verify(lsfxClient, never()).deleteFiles(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -656,10 +749,10 @@ class CcdiFileUploadServiceImplTest {
|
||||
when(recordMapper.selectById(RECORD_ID)).thenReturn(record);
|
||||
when(recordMapper.updateById(any(CcdiFileUploadRecord.class))).thenReturn(1);
|
||||
|
||||
String result = service.deleteFileUploadRecord(RECORD_ID, 9527L);
|
||||
String result = service.deleteFileUploadRecord(RECORD_ID, CALLER);
|
||||
|
||||
assertEquals("删除成功,已开始项目重新打标", result);
|
||||
verify(lsfxClient, never()).deleteFiles(any());
|
||||
verify(lsfxClient, never()).deleteFiles(any(), any());
|
||||
verify(bankStatementMapper).deleteByProjectIdAndBatchId(PROJECT_ID, LOG_ID);
|
||||
verify(recordMapper).updateById(org.mockito.ArgumentMatchers.<CcdiFileUploadRecord>argThat(item ->
|
||||
"deleted".equals(item.getFileStatus())
|
||||
@@ -669,7 +762,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
|
||||
// @Test
|
||||
// void processPullBankInfoAsync_shouldMarkParsedFailedWhenFetchInnerFlowThrows() {
|
||||
// when(lsfxClient.fetchInnerFlow(any())).thenThrow(new RuntimeException("fetch inner flow failed"));
|
||||
// when(lsfxClient.fetchInnerFlow(eq(CALLER), any())).thenThrow(new RuntimeException("fetch inner flow failed"));
|
||||
//
|
||||
// CcdiFileUploadRecord record = buildRecord();
|
||||
// service.processPullBankInfoAsync(
|
||||
@@ -692,19 +785,19 @@ class CcdiFileUploadServiceImplTest {
|
||||
AtomicInteger sequence = new AtomicInteger();
|
||||
captureRecordStatus(events, sequence);
|
||||
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
when(lsfxClient.uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenReturn(buildBankStatementResponseWithTotalCount(1))
|
||||
.thenThrow(new RuntimeException("paged fetch failed"));
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record, CALLER);
|
||||
|
||||
assertTrue(events.stream().anyMatch(event -> event.endsWith("record:parsed_failed")));
|
||||
assertFalse(events.stream().anyMatch(event -> event.endsWith("record:parsed_success")));
|
||||
@@ -716,18 +809,18 @@ class CcdiFileUploadServiceImplTest {
|
||||
List<CcdiFileUploadRecord> updates = new ArrayList<>();
|
||||
captureUpdatedRecords(updates);
|
||||
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
when(lsfxClient.uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenThrow(new RuntimeException("bank statement fetch failed:" + "x".repeat(3000)));
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record, CALLER);
|
||||
|
||||
CcdiFileUploadRecord failedRecord = findLastUpdatedRecordByStatus(updates, "parsed_failed");
|
||||
assertTrue(failedRecord.getErrorMessage().length() <= MAX_ERROR_MESSAGE_LENGTH);
|
||||
@@ -738,13 +831,13 @@ class CcdiFileUploadServiceImplTest {
|
||||
List<CcdiFileUploadRecord> updates = new ArrayList<>();
|
||||
captureUpdatedRecords(updates);
|
||||
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
when(lsfxClient.uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenThrow(new RuntimeException("upload failed:" + "x".repeat(3000)));
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record, CALLER);
|
||||
|
||||
CcdiFileUploadRecord failedRecord = findLastUpdatedRecordByStatus(updates, "parsed_failed");
|
||||
assertTrue(failedRecord.getErrorMessage().length() <= MAX_ERROR_MESSAGE_LENGTH);
|
||||
@@ -752,19 +845,19 @@ class CcdiFileUploadServiceImplTest {
|
||||
|
||||
@Test
|
||||
void fetchAndSaveBankStatements_shouldTrimLeAccountNoBeforeInsert() throws IOException {
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
when(lsfxClient.uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenReturn(buildBankStatementResponseWithItems(1, List.of(buildBankStatementItem(" 62220001 "))))
|
||||
.thenReturn(buildBankStatementResponseWithItems(1, List.of(buildBankStatementItem(" 62220001 "))));
|
||||
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record, CALLER);
|
||||
|
||||
verify(bankStatementMapper).insertBatch(any());
|
||||
verify(bankStatementMapper).insertBatch(org.mockito.ArgumentMatchers.argThat(list ->
|
||||
@@ -773,7 +866,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
|
||||
@Test
|
||||
void fetchAndSaveBankStatements_shouldLogConservativeCountsWhenAffectedRowsAreAmbiguous() {
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenReturn(buildBankStatementResponseWithItems(1, List.of(buildBankStatementItem("62220001"))))
|
||||
.thenReturn(buildBankStatementResponseWithItems(1, List.of(buildBankStatementItem("62220001"))));
|
||||
when(bankStatementMapper.insertBatch(any())).thenReturn(1);
|
||||
@@ -787,6 +880,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
Object result = ReflectionTestUtils.invokeMethod(
|
||||
service,
|
||||
"fetchAndSaveBankStatements",
|
||||
CALLER,
|
||||
PROJECT_ID,
|
||||
LSFX_PROJECT_ID,
|
||||
LOG_ID,
|
||||
@@ -825,12 +919,12 @@ class CcdiFileUploadServiceImplTest {
|
||||
AtomicInteger sequence = new AtomicInteger();
|
||||
captureRecordStatus(events, sequence);
|
||||
|
||||
when(lsfxClient.uploadFile(eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
when(lsfxClient.uploadFile(eq(CALLER), eq(LSFX_PROJECT_ID), any(), org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(buildUploadResponse());
|
||||
when(lsfxClient.checkParseStatus(LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
when(lsfxClient.checkParseStatus(CALLER, LSFX_PROJECT_ID, String.valueOf(LOG_ID)))
|
||||
.thenReturn(buildCheckParseStatusResponse(false));
|
||||
when(lsfxClient.getFileUploadStatus(any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(any(GetBankStatementRequest.class)))
|
||||
when(lsfxClient.getFileUploadStatus(eq(CALLER), any())).thenReturn(buildParsedSuccessStatusResponse());
|
||||
when(lsfxClient.getBankStatement(eq(CALLER), any(GetBankStatementRequest.class)))
|
||||
.thenReturn(buildBankStatementResponseWithItems(1, List.of(buildBankStatementItem("62220001"))))
|
||||
.thenReturn(buildBankStatementResponseWithItems(1, List.of(buildBankStatementItem("62220001"))));
|
||||
when(bankStatementMapper.insertBatch(any()))
|
||||
@@ -839,7 +933,7 @@ class CcdiFileUploadServiceImplTest {
|
||||
CcdiFileUploadRecord record = buildRecord();
|
||||
Path tempFile = createTempFile();
|
||||
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record);
|
||||
service.processFileAsync(PROJECT_ID, LSFX_PROJECT_ID, tempFile.toString(), RECORD_ID, "batch-1", record, CALLER);
|
||||
|
||||
assertTrue(events.stream().anyMatch(event -> event.endsWith("record:parsed_failed")));
|
||||
assertFalse(events.stream().anyMatch(event -> event.endsWith("record:parsed_success")));
|
||||
@@ -965,9 +1059,9 @@ class CcdiFileUploadServiceImplTest {
|
||||
return response;
|
||||
}
|
||||
|
||||
private FetchInnerFlowResponse buildFetchInnerFlowResponse(Integer logId) {
|
||||
private FetchInnerFlowResponse buildFetchInnerFlowResponse(Integer... logIds) {
|
||||
FetchInnerFlowResponse response = new FetchInnerFlowResponse();
|
||||
response.setData(List.of(logId));
|
||||
response.setData(List.of(logIds));
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -1026,10 +1120,17 @@ class CcdiFileUploadServiceImplTest {
|
||||
private void invokeSubmitTasksAsync(List<String> tempFilePaths,
|
||||
List<CcdiFileUploadRecord> records,
|
||||
String batchId) throws Exception {
|
||||
invokeSubmitTasksAsync(tempFilePaths, records, batchId, CALLER);
|
||||
}
|
||||
|
||||
private void invokeSubmitTasksAsync(List<String> tempFilePaths,
|
||||
List<CcdiFileUploadRecord> records,
|
||||
String batchId,
|
||||
CallerContext caller) throws Exception {
|
||||
Method method = CcdiFileUploadServiceImpl.class.getDeclaredMethod("submitTasksAsync",
|
||||
Long.class, Integer.class, List.class, List.class, String.class);
|
||||
Long.class, Integer.class, List.class, List.class, String.class, CallerContext.class);
|
||||
method.setAccessible(true);
|
||||
method.invoke(service, PROJECT_ID, LSFX_PROJECT_ID, tempFilePaths, records, batchId);
|
||||
method.invoke(service, PROJECT_ID, LSFX_PROJECT_ID, tempFilePaths, records, batchId, caller);
|
||||
}
|
||||
|
||||
private void setField(String fieldName, Object value) throws Exception {
|
||||
|
||||
@@ -9,6 +9,8 @@ import com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiFileUploadRecordMapper;
|
||||
import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
|
||||
import com.ruoyi.ccdi.project.service.ICcdiBankTagService;
|
||||
import com.ruoyi.ccdi.project.service.IProjectCounterpartyEnterpriseService;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
@@ -34,6 +36,8 @@ import static org.mockito.Mockito.when;
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CcdiProjectHistoryImportServiceImplTest {
|
||||
|
||||
private static final CallerContext CALLER = CallerContext.of(9527L, "tester");
|
||||
|
||||
@InjectMocks
|
||||
private CcdiProjectHistoryImportServiceImpl service;
|
||||
|
||||
@@ -52,6 +56,9 @@ class CcdiProjectHistoryImportServiceImplTest {
|
||||
@Mock
|
||||
private ICcdiBankTagService bankTagService;
|
||||
|
||||
@Mock
|
||||
private IProjectCounterpartyEnterpriseService counterpartyEnterpriseService;
|
||||
|
||||
@Test
|
||||
void shouldFilterStatementsByTrxDateAndDeduplicateAcrossSourceProjects() {
|
||||
CcdiProjectImportHistoryDTO dto = buildImportDto();
|
||||
@@ -80,7 +87,7 @@ class CcdiProjectHistoryImportServiceImplTest {
|
||||
when(projectMapper.selectById(11L)).thenReturn(buildProject(11L, "历史项目A"));
|
||||
when(projectMapper.selectById(12L)).thenReturn(buildProject(12L, "历史项目B"));
|
||||
|
||||
service.submitImport(90L, 3001, dto, "tester");
|
||||
service.submitImport(90L, 3001, dto, CALLER);
|
||||
|
||||
assertEquals(2, insertedStatements.get().size());
|
||||
assertTrue(insertedStatements.get().stream().allMatch(item -> Long.valueOf(90L).equals(item.getProjectId())));
|
||||
@@ -120,7 +127,7 @@ class CcdiProjectHistoryImportServiceImplTest {
|
||||
|
||||
when(projectMapper.selectById(11L)).thenReturn(buildProject(11L, "历史项目A"));
|
||||
|
||||
service.submitImport(90L, 3001, dto, "tester");
|
||||
service.submitImport(90L, 3001, dto, CALLER);
|
||||
|
||||
assertEquals(1, insertedStatements.get().size());
|
||||
assertNotEquals(101, insertedStatements.get().get(0).getBatchId());
|
||||
@@ -148,7 +155,7 @@ class CcdiProjectHistoryImportServiceImplTest {
|
||||
when(projectMapper.selectById(90L)).thenReturn(buildProject(90L, "新项目"));
|
||||
when(bankStatementMapper.countMatchedStaffCountByProjectId(90L)).thenReturn(3);
|
||||
|
||||
service.submitImport(90L, 3001, dto, "tester");
|
||||
service.submitImport(90L, 3001, dto, CALLER);
|
||||
|
||||
verify(projectMapper).updateById(org.mockito.ArgumentMatchers.<CcdiProject>argThat(project ->
|
||||
Long.valueOf(90L).equals(project.getProjectId()) && Integer.valueOf(3).equals(project.getTargetCount())
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper;
|
||||
import com.ruoyi.ccdi.project.service.CcdiProjectAccessService;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.lsfx.client.LsfxAnalysisClient;
|
||||
import com.ruoyi.lsfx.domain.CallerContext;
|
||||
import com.ruoyi.lsfx.domain.response.GetTokenResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -48,6 +49,8 @@ import static org.mockito.Mockito.when;
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CcdiProjectServiceImplTest {
|
||||
|
||||
private static final CallerContext CALLER = CallerContext.of(7L, "tester");
|
||||
|
||||
@InjectMocks
|
||||
private CcdiProjectServiceImpl service;
|
||||
|
||||
@@ -282,7 +285,7 @@ class CcdiProjectServiceImplTest {
|
||||
dto.setStartDate("2026-01-01");
|
||||
dto.setEndDate("2026-01-31");
|
||||
|
||||
when(lsfxAnalysisClient.getToken(any())).thenReturn(buildTokenResponse(3001));
|
||||
when(lsfxAnalysisClient.getToken(any(CallerContext.class), any())).thenReturn(buildTokenResponse(3001));
|
||||
doAnswer(invocation -> {
|
||||
CcdiProject project = invocation.getArgument(0);
|
||||
project.setProjectId(90L);
|
||||
@@ -291,7 +294,7 @@ class CcdiProjectServiceImplTest {
|
||||
|
||||
TransactionSynchronizationManager.initSynchronization();
|
||||
try {
|
||||
CcdiProjectVO project = service.importFromHistory(dto, "tester");
|
||||
CcdiProjectVO project = service.importFromHistory(dto, CALLER);
|
||||
|
||||
assertNotNull(project);
|
||||
assertEquals(90L, project.getProjectId());
|
||||
@@ -306,7 +309,7 @@ class CcdiProjectServiceImplTest {
|
||||
(CcdiProjectHistoryImportSubmittedEvent) eventCaptor.getValue();
|
||||
assertEquals(90L, event.getTargetProjectId());
|
||||
assertEquals(3001, event.getTargetLsfxProjectId());
|
||||
assertEquals("tester", event.getOperator());
|
||||
assertEquals(CALLER, event.getCaller());
|
||||
assertEquals(dto, event.getDto());
|
||||
} finally {
|
||||
TransactionSynchronizationManager.clearSynchronization();
|
||||
@@ -320,7 +323,7 @@ class CcdiProjectServiceImplTest {
|
||||
dto.setDescription("测试项目");
|
||||
dto.setConfigType("default");
|
||||
|
||||
when(lsfxAnalysisClient.getToken(any())).thenReturn(buildTokenResponse(2001));
|
||||
when(lsfxAnalysisClient.getToken(any(CallerContext.class), any())).thenReturn(buildTokenResponse(2001));
|
||||
doAnswer(invocation -> {
|
||||
CcdiProject project = invocation.getArgument(0);
|
||||
project.setProjectId(88L);
|
||||
@@ -333,7 +336,7 @@ class CcdiProjectServiceImplTest {
|
||||
logger.addAppender(logAppender);
|
||||
|
||||
try {
|
||||
service.createProject(dto);
|
||||
service.createProject(dto, CALLER);
|
||||
|
||||
assertTrue(logAppender.list.stream().map(ILoggingEvent::getFormattedMessage)
|
||||
.anyMatch(message -> message.contains("项目状态初始化")
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# 征信本地文件清理后端实施计划
|
||||
|
||||
## 目标
|
||||
|
||||
征信 HTML 上传后仅作为外部解析接口的临时输入。解析调用结束后立即删除本地文件,避免成功或失败流程残留敏感征信文件。
|
||||
|
||||
## 实施内容
|
||||
|
||||
1. 在 `CreditHtmlStorageService` 增加受限删除能力,仅允许删除 `ruoyi.profile/credit-html` 目录内的文件。
|
||||
2. 在征信上传单文件流程中,解析成功后先删除本地文件,再校验解析结果并入库。
|
||||
3. 解析失败时仍执行删除;解析与删除同时失败时保留原始解析错误,并记录删除异常。
|
||||
4. 删除失败时终止当前文件后续处理,由现有批量上传逻辑记录失败并继续处理其他文件。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 仅修改征信维护上传的后端处理流程。
|
||||
- 不修改上传接口、返回结构、数据库和前端页面。
|
||||
- `/lsfx/credit/parse` 外部地址测试接口不管理本地上传文件,保持不变。
|
||||
|
||||
## 验证
|
||||
|
||||
- 覆盖成功、解析失败、结果校验失败、入库失败和删除失败场景。
|
||||
- 验证删除路径不能逃逸 `ruoyi.profile/credit-html`。
|
||||
- 运行征信维护定向测试及 `ccdi-info-collection` 模块测试。
|
||||
@@ -0,0 +1,26 @@
|
||||
# 业务外部接口日志后端实施计划
|
||||
|
||||
## 目标
|
||||
|
||||
在不改变业务外部接口调用结果的前提下,统一记录流水分析与征信解析请求的发起用户、完整请求、原始响应、HTTP状态、耗时和异常,并提供接口日志分页查询与详情接口。
|
||||
|
||||
## 实施内容
|
||||
|
||||
1. 新建`sys_api_log`,请求头、请求参数、响应头、响应正文和异常使用`LONGTEXT`,表和字符字段统一使用`utf8mb4_general_ci`。
|
||||
2. 在`ruoyi-system`新增独立Entity、查询DTO、列表VO、详情VO、Mapper和事务Service;列表SQL不读取大字段,详情按日志ID读取完整内容。
|
||||
3. 在`ruoyi-admin`新增`/monitor/apilog/list`与`/monitor/apilog/{logId}`,分别校验`monitor:apilog:list`和`monitor:apilog:query`权限。
|
||||
4. 在`ccdi-lsfx`新增不可变`CallerContext`,替换流水分析、征信解析和`HttpUtil`原有调用签名,所有调用方必须显式传入发起人。
|
||||
5. 批量上传与拉取本行信息在提交异步任务前生成调用用户快照,并贯穿上传、拉取、轮询、状态查询和流水分页查询全链路。
|
||||
6. `HttpUtil`统一获取原始响应文本后再反序列化;非2xx保存状态、响应头和错误正文,网络异常和解析异常同样保存失败日志。
|
||||
7. 认证字段按原文保存;multipart文件仅保存文件名、大小和媒体类型,不读取或保存文件内容。
|
||||
8. 日志写入使用`REQUIRES_NEW`事务;日志序列化或入库失败只记录应用错误,不覆盖原业务结果。
|
||||
9. 通过增量SQL幂等创建表、接口日志菜单和查询权限按钮,不自动写入`sys_role_menu`。
|
||||
10. 流水分析和征信解析测试控制器取消匿名访问,统一校验`ccdi:project:edit`,人工测试调用按当前登录用户记录,不再记为`system`。
|
||||
|
||||
## 验证标准
|
||||
|
||||
- JDK 21下后端主工程编译通过。
|
||||
- 全部`HttpUtil`调用类型均能保存成功或失败日志,原始响应字段不因DTO反序列化丢失。
|
||||
- 异步任务在线程安全上下文清理后仍保存最初发起用户,不同用户连续任务不串号。
|
||||
- 未授权用户不能访问接口日志列表和详情。
|
||||
- 日志入库失败不影响外部接口返回或原始业务异常。
|
||||
@@ -0,0 +1,20 @@
|
||||
# 金综流水多 logId 处理后端实施计划
|
||||
|
||||
## 背景
|
||||
|
||||
金综 `dataChannelCode=JZL` 拉取链路可能在一次请求中返回多个 XML 文件对应的多个 `logId`。接入端如果只处理首个 `logId`,会导致同一证件号下后续金综 XML 文件未进入解析状态轮询、上传状态查询和流水明细落库。
|
||||
|
||||
## 实施内容
|
||||
|
||||
- 将金综拉取响应处理从单个 `logId` 改为遍历 `response.data` 中全部非空 `logId`。
|
||||
- 第一个 `logId` 复用拉取提交时创建的上传记录。
|
||||
- 第二个及后续 `logId` 自动创建独立上传记录,避免多个 XML 的文件名、账号、解析状态相互覆盖。
|
||||
- 每个 `logId` 独立执行解析状态轮询、文件状态查询、流水明细获取和落库。
|
||||
- 继续沿用最新代码中的 `CallerContext`,确保流水平台调用仍写入外部接口日志。
|
||||
- 单个 `logId` 失败只标记对应记录失败,不中断其余 `logId`;本次证件号任务至少一个 `logId` 成功即视为成功。
|
||||
|
||||
## 验证计划
|
||||
|
||||
- 补充单元测试模拟金综返回 3 个 `logId`。
|
||||
- 验证 3 个 `logId` 均调用解析状态轮询和流水明细查询。
|
||||
- 验证后续 `logId` 会新增独立上传记录,并保留证件号、上传人和上传中状态。
|
||||
@@ -0,0 +1,23 @@
|
||||
# 业务外部接口日志前端实施计划
|
||||
|
||||
## 目标
|
||||
|
||||
在“系统管理 > 日志管理”下新增“接口日志”页面,以紧凑列表和独立详情展示业务外部接口调用情况及完整请求响应。
|
||||
|
||||
## 实施内容
|
||||
|
||||
1. 新增`src/api/monitor/apilog.js`,封装分页列表和详情接口。
|
||||
2. 新增`src/views/monitor/apilog/index.vue`,支持接口地址、HTTP方法、HTTP调用状态和调用时间筛选。
|
||||
3. 列表展示日志编号、调用账号、接口地址、HTTP方法、HTTP状态、HTTP调用状态、耗时和调用时间。
|
||||
4. 详情按需二次加载,展示调用账号和用户ID、URL、Content-Type、请求头、请求参数、响应头、原始返回正文和异常。
|
||||
5. 请求响应长文本使用等宽字体和固定最大高度;可解析的JSON基于原始字符串进行无损结构美化并按2空格缩进展示,不将数值反序列化后再输出,普通文本保留原始换行,长字段按容器自动折行;使用文本插值,禁止`v-html`。
|
||||
6. 页面只提供查询和详情,不增加删除、清空或导出操作。
|
||||
7. 详情弹窗使用响应式宽度和桌面最大宽度,长URL和长文本不得撑破页面。
|
||||
|
||||
## 验证标准
|
||||
|
||||
- 使用项目`.nvmrc`切换Node版本后,生产构建通过。
|
||||
- 管理员可从真实系统菜单进入接口日志页,列表、筛选、分页和详情正常。
|
||||
- 成功、失败、空响应和超长响应均能稳定展示,JSON缩进、超大整数原值、中文、原始换行和长字段自动折行正确。
|
||||
- 外部响应中的HTML只作为文本显示,不产生脚本或页面节点。
|
||||
- 未授权用户看不到详情入口且直接请求详情接口被拒绝。
|
||||
@@ -0,0 +1,62 @@
|
||||
# 新华社工商信息同步接入实施计划
|
||||
|
||||
## 1. 实施目标
|
||||
|
||||
接入新华社同步工商接口 `POST /api/service/interface/invokeService/LSFXXHS`,开发环境仅连接本地 FastAPI Mock,生产环境连接真实接口。接口响应直接读取 `data.mappingOutputFields`,统一社会信用代码字段固定为 `creditCode`。
|
||||
|
||||
本次不实现异步结果查询、任务 Key 提取、轮询、结果缓存接口,也不接入 `LSFXXHSstockrelation`。
|
||||
|
||||
## 2. 接口与日志
|
||||
|
||||
- 使用 `application/x-www-form-urlencoded` 提交 `entName`、`serialNum`、`orgCode=999000`、`runType=1`。
|
||||
- `serialNum` 格式为 `CCDI_GS_时间戳_UUID`,仅作为请求流水号。
|
||||
- 依次校验 HTTP、JSON、外层状态、业务状态、非空结果对象、企业名称和 `creditCode`;任一校验失败均不得写缓存及业务数据。
|
||||
- 所有外部 HTTP 调用显式携带不可变 `CallerContext`,异步链路沿用原始发起用户。
|
||||
- 每次实际外呼写入一条 `sys_api_log`;缓存命中不写日志。日志独立事务保存,日志失败不影响业务请求。
|
||||
- 接口日志仅提供列表和详情,不提供删除、清空或导出。
|
||||
|
||||
## 3. 数据与解析
|
||||
|
||||
- 新增工商原始响应缓存、实体完整股东、项目对手方企业、项目对手方股东和接口日志表。
|
||||
- 缓存键为 `TRIM(entName) + EnterpriseProfile`,有效期为成功调用时间后 180 天,`query_result` 保存完整原始 JSON。
|
||||
- 实体表补充注册资本、注册日期、区域、从业人数、缓存关联和工商同步时间。
|
||||
- 映射 `creditCode`、注册资本、日期、机构类型、行业、区域、注册地址、从业人数、法定代表人和股东字段。
|
||||
- `stock_percent` 去除 `%` 后按百分数值保存,`should_capi` 单位固定为“万元”。文档未提供字段不推断。
|
||||
- `holders` 为空时整体清空旧股东和实体表前五股东字段。
|
||||
|
||||
## 4. 实体库同步
|
||||
|
||||
- 新增、导入、关系自动补全和名称变更均在原事务提交后异步执行。
|
||||
- 单批按去除首尾空格后的名称去重,同名只查询一次。
|
||||
- 仅在返回名称与请求名称一致且返回 `creditCode` 与实体主键完全一致时回写。
|
||||
- 同名但信用代码不一致的实体不更新;成功缓存保留,历史任务记录为跳过。
|
||||
- 工商字段、缓存关联、同步时间和 `data_source=API` 在短事务内更新,完整股东整体替换。
|
||||
- 不覆盖风险等级、企业来源、企业性质、经营状态和业务关系。
|
||||
|
||||
## 5. 项目对手方同步
|
||||
|
||||
- 流水上传、平台拉取和历史导入在整个批次完成后各触发一次 reconcile。
|
||||
- 当前项目全部非空对手方名称去重后按每批 100 条处理,外呼共用最大并发数为 3 的专用执行器。
|
||||
- 按“项目 ID + 对手方名称”更新或新增,信用代码变化时更新同一记录,并事务性替换股东。
|
||||
- 单项失败保留旧成功数据;批次结束后删除已不在当前流水集合中的项目工商数据。
|
||||
- 删除流水后基于剩余流水同步;空集合直接清空且不外呼。删除项目时直接删除项目工商数据。
|
||||
- 项目工商信息与实体库相互隔离。
|
||||
|
||||
## 6. API 与页面
|
||||
|
||||
- 实体库提供详情刷新、历史补全启动和 Redis 任务状态查询。
|
||||
- 项目提供对手方详情、单个刷新、历史补全启动和 Redis 任务状态查询。
|
||||
- 任务状态包含总数、完成数、成功数、跳过数、失败数和最终状态,不新增任务表。
|
||||
- 实体详情展示新增工商字段和完整股东;仅未同步或缓存过期时显示重新查询。
|
||||
- 流水对手方名称打开本地工商详情;打开、翻页和普通刷新不得触发外呼。
|
||||
- 接口日志页面为安静的只读运维表格,详情展示请求、响应和异常原文。
|
||||
|
||||
## 7. 实施顺序与验收
|
||||
|
||||
1. 执行 `sql/migration/2026-07-29-xinhua-enterprise-profile.sql`,重复执行验证幂等性。
|
||||
2. 启动 FastAPI Mock、Java 后端和 Vue 前端,开发配置必须指向本地 Mock。
|
||||
3. 运行 Mock、Java 模块测试和前端生产构建,确认源码不存在 `LSFXXHSResult`。
|
||||
4. 构造信用代码匹配、同名信用代码不匹配、空股东和多股东场景,核对缓存、实体、项目和日志。
|
||||
5. 使用应用内浏览器验收实体库、流水明细和接口日志真实页面。
|
||||
6. 清理业务测试数据并关闭本轮启动的前后端及 Mock 进程。
|
||||
7. 生产先对单个已知企业烟测,确认一次外呼、一条日志、原始缓存、`creditCode` 和股东一致后,再启动历史补全;任何环节失败立即停止上线。
|
||||
@@ -0,0 +1,41 @@
|
||||
# 征信本地文件清理后端实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 为征信 HTML 存储服务增加本地文件删除能力,并限制删除范围为 `ruoyi.profile/credit-html`。
|
||||
- 征信上传调用外部解析接口后立即删除本地文件,再继续解析结果校验与入库。
|
||||
- 解析失败时同样清理文件;清理异常不覆盖原始解析错误。
|
||||
- 删除失败时当前文件不再入库,批量上传继续处理后续文件。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 后端模块:`ccdi-info-collection`
|
||||
- 前端、接口协议、数据库结构均无变更。
|
||||
- 外部 `remotePath` 测试接口无变更。
|
||||
|
||||
## 验证情况
|
||||
|
||||
### 定向测试
|
||||
|
||||
执行命令:
|
||||
|
||||
```bash
|
||||
mvn -pl ccdi-info-collection -am -Dtest=CcdiCreditInfoServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false test
|
||||
```
|
||||
|
||||
结果:通过,共执行 9 个用例,失败 0、错误 0。已覆盖解析成功、解析失败、结果校验失败、入库失败、删除失败、批量继续处理、真实文件删除、重复删除和越界路径拒绝。
|
||||
|
||||
### 模块测试
|
||||
|
||||
执行命令:
|
||||
|
||||
```bash
|
||||
mvn -pl ccdi-info-collection -am test
|
||||
```
|
||||
|
||||
结果:共执行 178 个用例,本次修改相关测试全部通过;模块整体存在 1 个失败和 1 个错误:
|
||||
|
||||
- `CcdiBaseStaffMapperTest.mapperXml_shouldUseStableOrderForBaseStaffPagination`:员工 Mapper 排序契约断言失败。
|
||||
- `CcdiBaseStaffServiceImplTest.selectBaseStaffById_shouldReturnSelfOwnedAssetInfoList`:员工详情测试返回空对象并触发空指针。
|
||||
|
||||
上述失败位于未修改的员工信息代码与测试中,与本次征信本地文件清理无关,本次未扩大范围处理。
|
||||
@@ -0,0 +1,43 @@
|
||||
# 业务外部接口日志实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 新增`sys_api_log`表及“日志管理 > 接口日志”菜单增量SQL。
|
||||
- 新增接口日志持久化、列表查询、详情查询及对应权限控制。
|
||||
- 将流水分析和征信解析调用统一接入完整请求响应日志。
|
||||
- 新增显式调用用户快照,并贯穿项目创建、征信解析、批量上传和拉取本行信息异步链路。
|
||||
- 流水分析和征信解析测试控制器取消匿名访问,改为校验`ccdi:project:edit`并记录当前登录用户。
|
||||
- 新增接口日志前端列表、筛选和完整详情页面。
|
||||
- 接口日志详情支持JSON无损结构美化、2空格缩进展示和长文本自动换行;超大整数不经过JavaScript数值再序列化,非JSON内容保持原文。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- `ruoyi-system`:接口日志实体、DTO/VO、Mapper和事务Service。
|
||||
- `ruoyi-admin`:接口日志监控接口。
|
||||
- `ccdi-lsfx`:调用用户上下文、HTTP原始响应处理与日志写入。
|
||||
- `ccdi-project`、`ccdi-info-collection`:调用用户显式透传。
|
||||
- `ruoyi-ui`:接口日志菜单页面和API封装。
|
||||
- `sql/migration`:表结构与菜单权限增量脚本。
|
||||
|
||||
## 验证记录
|
||||
|
||||
- JDK 21主工程编译及`ruoyi-admin`全模块跳过测试打包:已通过。
|
||||
- `ccdi-lsfx`定向测试:20个测试全部通过,覆盖7类HTTP调用、成功原始正文、非2xx、网络异常、空响应、反序列化失败、请求快照失败、multipart文件元数据、日志入库失败、调用用户必填及`NULL/system`系统调用人。
|
||||
- `ccdi-project`定向测试:57个既有回归测试全部通过;新增异步调用人隔离用例后,`CcdiFileUploadServiceImplTest`共33个测试全部通过,验证清理`SecurityContext`后用户A、用户B连续任务不串号。
|
||||
- `ccdi-info-collection`征信调用人透传测试:5个测试全部通过。
|
||||
- 前端生产构建:通过`.nvmrc`切换Node 14.21.3后执行`npm run build:prod`成功,仅保留项目既有资源体积告警。
|
||||
- 数据库迁移:使用`bin/mysql_utf8_exec.sh`连续执行两次均成功;`sys_api_log`表排序规则为`utf8mb4_general_ci`,`caller_username`无默认值,`api_url`为`TEXT`,请求参数和响应正文为`LONGTEXT`;菜单与查询按钮均保持单条。
|
||||
- 真实接口日志:本地Mock下生成6条真实调用日志,覆盖Token、multipart上传、征信URL-encoded发起及结果轮询;日志调用人为`admin`、用户ID为1,multipart仅保存文件名、10423字节大小和媒体类型,详情保留完整原始响应。
|
||||
- 权限验证:未登录请求`/monitor/apilog/list`返回业务码401;管理员列表与详情接口返回正常。
|
||||
- Browser use真实页面验收:应用内浏览器从“系统管理 > 日志管理 > 接口日志”进入成功;列表显示调用账号,按`xfeature`筛选从6条收敛为2条,详情完整展示用户、URL、请求响应头、请求参数、原始正文、HTTP状态及耗时,长文本滚动区域无溢出。
|
||||
- 详情格式化验收:可解析JSON按层级换行缩进展示,`9223372036854775807`等超大整数保持原值,长字段在内容区域内自动折行;异常堆栈等非JSON文本保留原始换行。
|
||||
- 测试清理:Browser use验收完成后删除本轮生成的6条接口日志测试数据,并关闭后端、前端和Mock测试进程。
|
||||
|
||||
## 说明
|
||||
|
||||
- 认证字段按已确认需求原文保存,接口日志访问权限必须严格控制。
|
||||
- `call_status`表示HTTP传输与响应反序列化状态,页面统一标注为“HTTP调用状态”,不表示外部系统业务码结果。
|
||||
- multipart日志仅保存文件元数据,不保存文件字节。
|
||||
- 失败日志的异常字段保存完整异常堆栈,不只保存异常消息。
|
||||
- 日志入库异常不会改变外部接口调用结果。
|
||||
- 临时测试数据、临时测试文件和浏览器验收产物不纳入Git提交范围;既有回归测试因公共方法签名变化产生的适配随业务代码一并交付。
|
||||
23
docs/reports/implementation/2026-07-21-jzl-multiple-logid.md
Normal file
23
docs/reports/implementation/2026-07-21-jzl-multiple-logid.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# 金综流水多 logId 处理实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 修复拉取金综流水时只处理首个 `logId` 的问题。
|
||||
- `FetchInnerFlowResponse.data` 中多个 `logId` 现在会逐个进入解析状态轮询、上传状态查询和流水明细落库。
|
||||
- 第一个 `logId` 复用原上传记录,后续 `logId` 自动新增独立上传记录。
|
||||
- 单个 `logId` 处理失败时只更新对应记录为失败,不影响其余 `logId` 继续处理。
|
||||
- 基于最新 `origin/dev-ui` 实施,保留 `CallerContext` 外部接口日志链路。
|
||||
|
||||
## 影响范围
|
||||
|
||||
- `/ccdi/file-upload/pull-bank-info` 金综 `JZL` 拉取后的异步处理链路。
|
||||
- 项目详情上传数据列表中金综多 XML 文件对应的上传记录展示。
|
||||
- 后续流水明细保存与自动打标触发判断。
|
||||
|
||||
## 验证情况
|
||||
|
||||
- 后端单测通过:
|
||||
- `mvn -pl ccdi-project -am test "-Dtest=CcdiFileUploadServiceImplTest" "-Dsurefire.failIfNoSpecifiedTests=false"`
|
||||
- 结果:36 个测试通过。
|
||||
- 新增用例覆盖金综返回 3 个 `logId` 的场景,确认 3 个 `logId` 均进入解析状态轮询和流水明细查询,且后续 `logId` 会新增独立上传记录。
|
||||
- 验证多 `logId` 处理继续传递同一个 `CallerContext`,兼容最新外部接口日志链路。
|
||||
@@ -0,0 +1,70 @@
|
||||
# 新华社工商信息同步接入实施报告
|
||||
|
||||
## 1. 实施结果
|
||||
|
||||
已按同步接口契约完成新华社工商信息接入。开发环境通过 FastAPI Mock 联调,生产配置保留真实接口地址;代码、配置和 Mock 均未实现 `LSFXXHSResult` 或 `LSFXXHSstockrelation`。
|
||||
|
||||
## 2. 主要改动
|
||||
|
||||
### 外部接口与日志
|
||||
|
||||
- `ccdi-lsfx` 新增新华社同步客户端、同步响应模型和不可变 `CallerContext`。
|
||||
- 重构共享 HTTP 工具,统一记录调用人、URL、方法、请求头和参数、HTTP 状态、响应头、原始响应、异常堆栈和耗时。
|
||||
- `sys_api_log` 使用独立事务写入;HTTP 200 的业务失败仍按传输成功保存,业务失败信息保留在原始响应中。
|
||||
- 恢复只读接口日志列表、详情 API 和页面,无删除、清空、导出能力。
|
||||
|
||||
### 缓存、解析与实体库
|
||||
|
||||
- 新增 180 天成功缓存,缓存保存同步接口完整原始 JSON。
|
||||
- 新增工商字段解析、实体完整股东表和股东整体替换。
|
||||
- 实体新增、导入、关系自动补全及名称变化后,在原事务提交后异步同步。
|
||||
- 单批按企业名称去重,返回 `creditCode` 仅回写主键完全匹配的实体;同名不匹配实体保持不变。
|
||||
- 手工刷新和历史补全使用 Redis 状态,不增加任务表。
|
||||
|
||||
### 项目对手方
|
||||
|
||||
- 新增项目对手方企业、股东、详情、刷新和历史补全能力。
|
||||
- 流水上传、平台拉取及历史导入完成后按项目当前对手方集合 reconcile。
|
||||
- 查询按每批 100 条执行,新华社查询共用最大并发数 3 的执行器。
|
||||
- 项目删除直接清理项目工商数据,流水删除后按剩余流水同步,空集合不外呼。
|
||||
|
||||
### 前端与 Mock
|
||||
|
||||
- 实体详情新增注册资本、注册日期、区域、从业人数、同步时间和完整股东表。
|
||||
- 流水对手方名称可打开本地工商详情,仅无数据或缓存过期时显示重新查询。
|
||||
- Mock 覆盖公司、个体、空股东、多股东、HTTP 失败、超时、空响应、非法 JSON、外层失败、业务失败、结果缺失、名称不一致和信用代码缺失。
|
||||
|
||||
## 3. 数据库实施
|
||||
|
||||
- 新增迁移:`sql/migration/2026-07-29-xinhua-enterprise-profile.sql`。
|
||||
- 已通过 `bin/mysql_utf8_exec.sh` 连续执行两次,均成功。
|
||||
- 已核对 5 张新增或校准表均为 `utf8mb4_general_ci`。
|
||||
- 已核对缓存 `query_result` 为 `LONGTEXT`,缓存唯一键为 `(query_param, query_type)`,项目唯一键为 `(project_id, counterparty_name)`。
|
||||
- 已核对实体工商扩展字段、菜单权限和管理员角色权限落库。
|
||||
|
||||
## 4. 自动化验证
|
||||
|
||||
- `python3 -m pytest tests/test_enterprise_api.py -v`:新华社 Mock 专项 4 项通过。
|
||||
- `mvn -pl ccdi-info-collection -am test`:构建成功,覆盖新华社客户端、HTTP 日志、字段解析和信息采集模块回归。
|
||||
- 项目模块定向测试:53 项通过,覆盖上传、拉取、历史导入、项目删除及调用人传递。
|
||||
- `mvn -pl ccdi-project -am -DskipTests compile`:构建成功。
|
||||
- 前端在 Node 14.21.3 下执行 `npm run build:prod`:构建成功。
|
||||
- `git diff --check`:通过。
|
||||
|
||||
全量项目测试当前仍有 3 个失败和 1 个错误,均位于既有结果总览和银行标签基线断言:按钮文案、规则数量、`modelParamService` 结构断言及对应未配置 Mock。本次新增链路的定向测试均通过,未修改这些无关基线。
|
||||
|
||||
Mock 子项目全量 99 项测试为 75 项通过、24 项失败;失败用例均属于既有流水 Mock 链路,原因是测试尝试使用账号 `znsj` 从当前宿主连接 MySQL 时被拒绝。新华社工商专项 4 项不依赖该连接并全部通过。
|
||||
|
||||
## 5. 应用内浏览器验收
|
||||
|
||||
- 实体库真实页面成功展示接口返回的统一社会信用代码、注册资本、日期、区域、从业人数及 7 条完整股东。
|
||||
- 新增同名但不同信用代码的实体后,确认该实体保持手工数据且无股东回写;缓存命中后新华社日志仍为 1 条。
|
||||
- 补充验证同名未同步实体即使存在有效缓存仍可发起刷新,并明确返回“工商信用代码与实体主键不一致”;项目无本地记录时可复用有效缓存补齐,两种缓存命中均不新增接口日志。
|
||||
- 流水明细真实页面确认对手方名称为本地详情入口,打开弹窗不触发新华社;无数据时显示重新查询。
|
||||
- 对手方手工刷新后展示接口返回的信用代码、结构化工商字段和完整股东。
|
||||
- 接口日志真实页面确认每次缓存未命中对应一条日志,详情包含调用账号、表单参数、`CCDI_GS_时间戳_UUID`、HTTP 状态、响应头和原始 JSON。
|
||||
- 联调业务数据已清理;接口历史日志按设计保留。
|
||||
|
||||
## 6. 上线要求
|
||||
|
||||
生产环境不得配置或恢复异步结果查询。上线先使用一个已知企业完成单次烟测,逐项确认单次外呼、单条日志、原始缓存、`creditCode` 和股东落库一致;确认后才能启动实体和项目历史补全,失败时停止上线。
|
||||
@@ -7,7 +7,7 @@ import argparse
|
||||
import os
|
||||
|
||||
from fastapi import FastAPI
|
||||
from routers import api, credit_api
|
||||
from routers import api, credit_api, enterprise_api
|
||||
from config.settings import settings
|
||||
|
||||
# 创建 FastAPI 应用实例
|
||||
@@ -27,6 +27,7 @@ app = FastAPI(
|
||||
- **文件删除** - 批量删除上传的文件
|
||||
- **流水查询** - 分页获取银行流水数据
|
||||
- **征信解析** - 发起 HTML 远程解析并通过结果接口返回结构化征信 payload
|
||||
- **工商信息** - 按企业名称同步返回新华社工商信息
|
||||
|
||||
### 错误模拟
|
||||
|
||||
@@ -51,6 +52,7 @@ app = FastAPI(
|
||||
# 包含 API 路由
|
||||
app.include_router(api.router, tags=["流水分析接口"])
|
||||
app.include_router(credit_api.router, tags=["征信解析接口"])
|
||||
app.include_router(enterprise_api.router, tags=["新华社工商信息接口"])
|
||||
|
||||
|
||||
@app.get("/", summary="服务根路径")
|
||||
|
||||
115
lsfx-mock-server/routers/enterprise_api.py
Normal file
115
lsfx-mock-server/routers/enterprise_api.py
Normal file
@@ -0,0 +1,115 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Form, Response
|
||||
from fastapi.responses import PlainTextResponse
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/api/service/interface/invokeService/LSFXXHS")
|
||||
async def query_enterprise(
|
||||
entName: Optional[str] = Form(None),
|
||||
serialNum: Optional[str] = Form(None),
|
||||
runType: Optional[str] = Form(None),
|
||||
orgCode: Optional[str] = Form(None),
|
||||
):
|
||||
missing = next((name for name, value in {
|
||||
"entName": entName,
|
||||
"serialNum": serialNum,
|
||||
"runType": runType,
|
||||
"orgCode": orgCode,
|
||||
}.items() if not value), None)
|
||||
if missing:
|
||||
return business_error(f"缺少参数: {missing}")
|
||||
|
||||
name = entName.strip()
|
||||
if name == "MOCK_HTTP_500":
|
||||
return Response(content="mock server error", status_code=500)
|
||||
if name == "MOCK_TIMEOUT":
|
||||
await asyncio.sleep(5)
|
||||
return success_wrapper(build_profile(name))
|
||||
if name == "MOCK_EMPTY_RESPONSE":
|
||||
return Response(content="", media_type="application/json")
|
||||
if name == "MOCK_INVALID_JSON":
|
||||
return PlainTextResponse("not-json")
|
||||
if name == "MOCK_OUTER_FAILURE":
|
||||
return {"success": False, "code": 50000, "data": None}
|
||||
if name == "MOCK_BUSINESS_FAILURE":
|
||||
return business_error("工商查询失败")
|
||||
if name == "MOCK_MAPPING_MISSING":
|
||||
return success_wrapper(None, include_mapping=False)
|
||||
if name == "MOCK_MAPPING_NULL":
|
||||
return success_wrapper(None)
|
||||
if name == "MOCK_MAPPING_EMPTY":
|
||||
return success_wrapper({})
|
||||
|
||||
profile = build_profile(name)
|
||||
if name == "MOCK_NAME_MISMATCH":
|
||||
profile["enterpriseName"] = "其他企业有限公司"
|
||||
if name == "MOCK_CREDIT_CODE_MISSING":
|
||||
profile["creditCode"] = " "
|
||||
return success_wrapper(profile)
|
||||
|
||||
|
||||
def success_wrapper(profile, include_mapping=True):
|
||||
data = {
|
||||
"reasonMessage": "Running successfully",
|
||||
"reasonCode": 200,
|
||||
"status": 1,
|
||||
"extensionMap": {},
|
||||
}
|
||||
if include_mapping:
|
||||
data["mappingOutputFields"] = profile
|
||||
return {"success": True, "code": 10000, "data": data}
|
||||
|
||||
|
||||
def business_error(message):
|
||||
return {
|
||||
"success": True,
|
||||
"code": 10000,
|
||||
"data": {
|
||||
"mappingOutputFields": None,
|
||||
"reasonMessage": message,
|
||||
"reasonCode": 500,
|
||||
"status": 0,
|
||||
"extensionMap": {},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def build_profile(name):
|
||||
digest = hashlib.sha1(name.encode("utf-8")).hexdigest()
|
||||
credit_code = "91" + "".join(str(int(ch, 16) % 10) for ch in digest[:16])
|
||||
holders = [] if "空股东" in name else [
|
||||
{"stock_name": "张三", "stock_percent": "60%", "should_capi": "600"},
|
||||
{"stock_name": "李四", "stock_percent": "40%", "should_capi": "400"},
|
||||
]
|
||||
if "多股东" in name:
|
||||
holders = [
|
||||
{"stock_name": f"股东{i}", "stock_percent": f"{i}%", "should_capi": str(i * 10)}
|
||||
for i in range(1, 8)
|
||||
]
|
||||
|
||||
individual = "个体" in name
|
||||
return {
|
||||
"creditCode": credit_code,
|
||||
"enterpriseName": name,
|
||||
"baseCity": {
|
||||
"province": "浙江省",
|
||||
"province_code": "330000",
|
||||
"city": "杭州市" if individual else None,
|
||||
"county": "西湖区" if individual else None,
|
||||
},
|
||||
"regCapitalAmount": {"regist_capi_value": "1000", "regist_capi_unit": "万元"},
|
||||
"estiblishTime": "2020-01-02",
|
||||
"fromTime": "2020-01-01",
|
||||
"companyOrgType": "个体" if individual else {"econ_kind": "有限责任公司", "econ_kind_code": "1100"},
|
||||
"industry": {"industry_code": "I65", "industry": "软件和信息技术服务业"},
|
||||
"regLocation": "浙江省杭州市西湖区测试路1号",
|
||||
"employeeCount": None if individual else 25,
|
||||
"legalPersonName": "王五",
|
||||
"holders": holders,
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.ruoyi.web.controller.monitor;
|
||||
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.system.domain.dto.SysApiLogQueryDTO;
|
||||
import com.ruoyi.system.domain.vo.SysApiLogListVO;
|
||||
import com.ruoyi.system.service.ISysApiLogService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
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.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 业务外部接口日志。
|
||||
*/
|
||||
@Tag(name = "接口日志")
|
||||
@RestController
|
||||
@RequestMapping("/monitor/apilog")
|
||||
public class SysApiLogController extends BaseController {
|
||||
|
||||
@Resource
|
||||
private ISysApiLogService apiLogService;
|
||||
|
||||
@Operation(summary = "查询接口日志列表")
|
||||
@PreAuthorize("@ss.hasPermi('monitor:apilog:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(SysApiLogQueryDTO queryDTO) {
|
||||
startPage();
|
||||
List<SysApiLogListVO> list = apiLogService.selectApiLogList(queryDTO);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询接口日志详情")
|
||||
@PreAuthorize("@ss.hasPermi('monitor:apilog:query')")
|
||||
@GetMapping("/{logId}")
|
||||
public AjaxResult detail(@PathVariable Long logId) {
|
||||
return success(apiLogService.selectApiLogById(logId));
|
||||
}
|
||||
}
|
||||
@@ -155,3 +155,9 @@ credit-parse:
|
||||
org-code: 999000
|
||||
run-type: 1
|
||||
model: LXCUSTALL
|
||||
|
||||
xinhua-enterprise:
|
||||
api:
|
||||
url: http://localhost:8000/api/service/interface/invokeService/LSFXXHS
|
||||
org-code: 999000
|
||||
run-type: 1
|
||||
|
||||
@@ -155,3 +155,9 @@ credit-parse:
|
||||
org-code: 999000
|
||||
run-type: 1
|
||||
model: LXCUSTALL
|
||||
|
||||
xinhua-enterprise:
|
||||
api:
|
||||
url: http://192.168.0.111:62320/api/service/interface/invokeService/LSFXXHS
|
||||
org-code: 999000
|
||||
run-type: 1
|
||||
|
||||
@@ -150,3 +150,9 @@ credit-parse:
|
||||
org-code: 999000
|
||||
run-type: 1
|
||||
model: LXCUSTALL
|
||||
|
||||
xinhua-enterprise:
|
||||
api:
|
||||
url: http://64.202.32.40:8083/api/service/interface/invokeService/LSFXXHS
|
||||
org-code: 999000
|
||||
run-type: 1
|
||||
|
||||
@@ -153,3 +153,9 @@ credit-parse:
|
||||
org-code: 999000
|
||||
run-type: 1
|
||||
model: LXCUSTALL
|
||||
|
||||
xinhua-enterprise:
|
||||
api:
|
||||
url: http://192.168.0.111:62320/api/service/interface/invokeService/LSFXXHS
|
||||
org-code: 999000
|
||||
run-type: 1
|
||||
|
||||
@@ -23,6 +23,12 @@
|
||||
<artifactId>ruoyi-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.ruoyi.system.domain;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 业务外部接口调用日志。
|
||||
*/
|
||||
@Data
|
||||
public class SysApiLog {
|
||||
|
||||
private Long logId;
|
||||
private Long callerUserId;
|
||||
private String callerUsername;
|
||||
private String apiUrl;
|
||||
private String httpMethod;
|
||||
private String contentType;
|
||||
private String requestHeaders;
|
||||
private String requestParams;
|
||||
private Integer responseStatus;
|
||||
private String responseHeaders;
|
||||
private String responseBody;
|
||||
private String callStatus;
|
||||
private String errorMsg;
|
||||
private Long costTime;
|
||||
private Date callTime;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.ruoyi.system.domain.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 接口日志查询条件。
|
||||
*/
|
||||
@Data
|
||||
public class SysApiLogQueryDTO {
|
||||
|
||||
private String apiUrl;
|
||||
private String httpMethod;
|
||||
private String callStatus;
|
||||
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date beginTime;
|
||||
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date endTime;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.ruoyi.system.domain.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 接口日志完整详情。
|
||||
*/
|
||||
@Data
|
||||
public class SysApiLogDetailVO {
|
||||
|
||||
private Long logId;
|
||||
private Long callerUserId;
|
||||
private String callerUsername;
|
||||
private String apiUrl;
|
||||
private String httpMethod;
|
||||
private String contentType;
|
||||
private String requestHeaders;
|
||||
private String requestParams;
|
||||
private Integer responseStatus;
|
||||
private String responseHeaders;
|
||||
private String responseBody;
|
||||
private String callStatus;
|
||||
private String errorMsg;
|
||||
private Long costTime;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date callTime;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.ruoyi.system.domain.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 接口日志列表项,不包含请求响应大字段。
|
||||
*/
|
||||
@Data
|
||||
public class SysApiLogListVO {
|
||||
|
||||
private Long logId;
|
||||
private Long callerUserId;
|
||||
private String callerUsername;
|
||||
private String apiUrl;
|
||||
private String httpMethod;
|
||||
private Integer responseStatus;
|
||||
private String callStatus;
|
||||
private Long costTime;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date callTime;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.ruoyi.system.mapper;
|
||||
|
||||
import com.ruoyi.system.domain.SysApiLog;
|
||||
import com.ruoyi.system.domain.dto.SysApiLogQueryDTO;
|
||||
import com.ruoyi.system.domain.vo.SysApiLogDetailVO;
|
||||
import com.ruoyi.system.domain.vo.SysApiLogListVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 接口日志数据层。
|
||||
*/
|
||||
public interface SysApiLogMapper {
|
||||
|
||||
int insertApiLog(SysApiLog apiLog);
|
||||
|
||||
List<SysApiLogListVO> selectApiLogList(SysApiLogQueryDTO queryDTO);
|
||||
|
||||
SysApiLogDetailVO selectApiLogById(Long logId);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.ruoyi.system.service;
|
||||
|
||||
import com.ruoyi.system.domain.SysApiLog;
|
||||
import com.ruoyi.system.domain.dto.SysApiLogQueryDTO;
|
||||
import com.ruoyi.system.domain.vo.SysApiLogDetailVO;
|
||||
import com.ruoyi.system.domain.vo.SysApiLogListVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 接口日志服务。
|
||||
*/
|
||||
public interface ISysApiLogService {
|
||||
|
||||
void recordApiLog(SysApiLog apiLog);
|
||||
|
||||
List<SysApiLogListVO> selectApiLogList(SysApiLogQueryDTO queryDTO);
|
||||
|
||||
SysApiLogDetailVO selectApiLogById(Long logId);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user