Files
ccdi/docs/plans/2026-03-02-lsfx-integration-design.md
wkc 9f70795911 docs: 添加流水分析平台对接设计文档
- 定义ccdi-lsfx模块架构
- 设计7个接口的调用封装
- 采用RestTemplate + HttpUtil技术方案
- 包含完整配置、工具类、Client和测试Controller设计
2026-03-02 09:40:10 +08:00

827 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 流水分析平台对接设计文档
## 文档信息
- **创建日期**: 2026-03-02
- **设计目标**: 实现与见知现金流尽调系统的对接封装7个核心接口调用
- **技术方案**: RestTemplate + 手动封装
---
## 一、需求概述
### 1.1 业务背景
系统需要与**见知现金流尽调系统**对接,用于拉取银行流水数据。通过调用流水分析平台提供的接口,实现以下功能:
- 创建项目并获取访问Token
- 上传银行流水文件或拉取行内流水
- 检查文件解析状态
- 生成尽调报告
- 获取流水明细数据
### 1.2 接口列表
共7个接口调用流程如下
```
获取Token → 上传文件/拉取行内流水 → 检查解析状态 → 生成报告 → 检查报告状态 → 获取流水数据
```
| 序号 | 接口名称 | 请求方式 | 说明 |
|------|---------|---------|------|
| 1 | 获取Token | POST | 创建项目并获取访问Token |
| 2 | 上传文件 | POST | 上传银行流水文件 |
| 3 | 拉取行内流水 | POST | 从数仓拉取行内流水 |
| 4 | 检查解析状态 | POST | 轮询检查文件解析状态 |
| 5 | 生成尽调报告 | POST | 确认文件后生成报告 |
| 6 | 检查报告状态 | GET | 轮询检查报告生成状态 |
| 7 | 获取银行流水 | POST | 分页获取流水明细 |
### 1.3 技术选型
**方案一RestTemplate + 手动封装**(已选定)
**优点**
- ✅ 简单直接符合task.md要求
- ✅ Spring Boot自带无需额外依赖
- ✅ 完全控制请求细节(超时、拦截器、错误处理)
- ✅ 易于测试和调试
---
## 二、架构设计
### 2.1 模块结构
创建新模块 `ccdi-lsfx` (流水分析对接模块),目录结构如下:
```
ccdi-lsfx/
├── pom.xml
└── src/main/java/com/ruoyi/lsfx/
├── config/
│ └── RestTemplateConfig.java # RestTemplate配置
├── constants/
│ └── LsfxConstants.java # 常量定义
├── client/
│ └── LsfxAnalysisClient.java # 封装7个接口调用
├── domain/
│ ├── request/ # 请求DTO
│ │ ├── GetTokenRequest.java # 接口1
│ │ ├── UploadFileRequest.java # 接口2
│ │ ├── FetchInnerFlowRequest.java # 接口3
│ │ ├── CheckParseStatusRequest.java # 接口4
│ │ ├── GenerateReportRequest.java # 接口5
│ │ └── GetBankStatementRequest.java # 接口7
│ └── response/ # 响应DTO
│ ├── GetTokenResponse.java # 接口1
│ ├── UploadFileResponse.java # 接口2
│ ├── FetchInnerFlowResponse.java # 接口3
│ ├── CheckParseStatusResponse.java # 接口4
│ ├── GenerateReportResponse.java # 接口5
│ ├── CheckReportStatusResponse.java# 接口6
│ └── GetBankStatementResponse.java # 接口7
├── exception/
│ ├── LsfxApiException.java # API调用异常
│ └── LsfxErrorCode.java # 错误码枚举
├── util/
│ ├── MD5Util.java # MD5加密工具
│ └── HttpUtil.java # HTTP工具类
└── controller/
└── LsfxTestController.java # 测试控制器
```
### 2.2 模块依赖
在根目录 `pom.xml``<modules>` 中添加:
```xml
<module>ccdi-lsfx</module>
```
`ruoyi-admin/pom.xml` 中添加:
```xml
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ccdi-lsfx</artifactId>
</dependency>
```
---
## 三、配置设计
### 3.1 application-dev.yml 配置
```yaml
# 流水分析平台配置
lsfx:
api:
# 测试环境
base-url: http://158.234.196.5:82/c4c3
# 生产环境(注释掉测试环境后启用)
# base-url: http://64.202.32.176/c4c3
# 认证配置
app-id: remote_app
app-secret: your_app_secret_here # 从见知获取
client-id: c2017e8d105c435a96f86373635b6a09 # 测试环境固定值
# 接口路径配置
endpoints:
get-token: /account/common/getToken
upload-file: /watson/api/project/remoteUploadSplitFile
fetch-inner-flow: /watson/api/project/getJZFileOrZjrcuFile
check-parse-status: /watson/api/project/upload/getpendings
generate-report: /watson/api/project/confirmStageUploadLogs
check-report-status: /watson/api/project/upload/getallpendings
get-bank-statement: /watson/api/project/upload/getBankStatement
# RestTemplate配置
connection-timeout: 30000 # 连接超时30秒
read-timeout: 60000 # 读取超时60秒
```
### 3.2 常量类
```java
package com.ruoyi.lsfx.constants;
/**
* 流水分析平台常量
*/
public class LsfxConstants {
/** 基础URL配置键 */
public static final String BASE_URL_KEY = "lsfx.api.base-url";
/** 成功状态码 */
public static final String SUCCESS_CODE = "200";
/** 文件解析成功状态 */
public static final int PARSE_SUCCESS_STATUS = -5;
public static final String PARSE_SUCCESS_DESC = "data.wait.confirm.newaccount";
/** 数据渠道编码 */
public static final String DATA_CHANNEL_ZJRCU = "ZJRCU";
/** 分析类型 */
public static final String ANALYSIS_TYPE = "-1";
/** 请求头 */
public static final String HEADER_CLIENT_ID = "X-Xencio-Client-Id";
public static final String HEADER_CONTENT_TYPE = "Content-Type";
/** 默认角色 */
public static final String DEFAULT_ROLE = "VIEWER";
}
```
### 3.3 RestTemplate配置类
```java
package com.ruoyi.lsfx.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
/**
* RestTemplate配置
*/
@Configuration
public class RestTemplateConfig {
@Value("${lsfx.api.connection-timeout:30000}")
private int connectionTimeout;
@Value("${lsfx.api.read-timeout:60000}")
private int readTimeout;
@Bean
public RestTemplate restTemplate() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(connectionTimeout);
factory.setReadTimeout(readTimeout);
return new RestTemplate(factory);
}
}
```
---
## 四、核心类设计
### 4.1 DTO对象设计
#### 请求DTO示例GetTokenRequest
```java
package com.ruoyi.lsfx.domain.request;
import lombok.Data;
/**
* 获取Token请求参数
*/
@Data
public class GetTokenRequest {
/** 项目编号 */
private String projectNo;
/** 项目名称 */
private String entityName;
/** 操作人员编号 */
private String userId;
/** 操作人员姓名 */
private String userName;
/** 见知提供appId */
private String appId;
/** 安全码 md5(projectNo + "_" + entityName + "_" + appSecret) */
private String appSecretCode;
/** 人员角色 */
private String role;
/** 行社机构号 */
private String orgCode;
/** 企业统信码或个人身份证号 */
private String entityId;
/** 信贷关联人信息 */
private String xdRelatedPersons;
/** 金综链流水日期ID */
private String jzDataDateId;
/** 行内流水开始日期 */
private String innerBSStartDateId;
/** 行内流水结束日期 */
private String innerBSEndDateId;
/** 分析类型 */
private String analysisType;
/** 客户经理所属营业部机构编码 */
private String departmentCode;
}
```
#### 响应DTO示例GetTokenResponse
```java
package com.ruoyi.lsfx.domain.response;
import lombok.Data;
/**
* 获取Token响应
*/
@Data
public class GetTokenResponse {
/** 返回码 */
private String code;
/** 响应状态 */
private String status;
/** 消息 */
private String message;
/** 成功标识 */
private Boolean successResponse;
/** 响应数据 */
private TokenData data;
@Data
public static class TokenData {
/** token */
private String token;
/** 见知项目Id */
private Integer projectId;
/** 项目编号 */
private String projectNo;
/** 项目名称 */
private String entityName;
/** 分析类型 */
private Integer analysisType;
}
}
```
**说明**: 其他5个接口的DTO类结构类似根据接口文档定义字段。
### 4.2 工具类设计
#### MD5Util
```java
package com.ruoyi.lsfx.util;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* MD5加密工具类
*/
public class MD5Util {
/**
* MD5加密
* @param input 待加密字符串
* @return MD5加密后的32位小写字符串
*/
public static String encrypt(String input) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] messageDigest = md.digest(input.getBytes());
StringBuilder hexString = new StringBuilder();
for (byte b : messageDigest) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5加密失败", e);
}
}
/**
* 生成安全码
* @param projectNo 项目编号
* @param entityName 项目名称
* @param appSecret 应用密钥
* @return MD5安全码
*/
public static String generateSecretCode(String projectNo, String entityName, String appSecret) {
String raw = projectNo + "_" + entityName + "_" + appSecret;
return encrypt(raw);
}
}
```
#### HttpUtil
```java
package com.ruoyi.lsfx.util;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.util.Map;
/**
* HTTP请求工具类
*/
@Component
public class HttpUtil {
@Resource
private RestTemplate restTemplate;
/**
* 发送GET请求带请求头
*/
public <T> T get(String url, Map<String, String> headers, Class<T> responseType) {
HttpHeaders httpHeaders = createHeaders(headers);
HttpEntity<Void> requestEntity = new HttpEntity<>(httpHeaders);
ResponseEntity<T> response = restTemplate.exchange(
url, HttpMethod.GET, requestEntity, responseType
);
return response.getBody();
}
/**
* 发送POST请求JSON格式带请求头
*/
public <T> T postJson(String url, Object request, Map<String, String> headers, Class<T> responseType) {
HttpHeaders httpHeaders = createHeaders(headers);
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Object> requestEntity = new HttpEntity<>(request, httpHeaders);
ResponseEntity<T> response = restTemplate.postForEntity(url, requestEntity, responseType);
return response.getBody();
}
/**
* 上传文件Multipart格式
*/
public <T> T uploadFile(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);
}
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, httpHeaders);
ResponseEntity<T> response = restTemplate.postForEntity(url, requestEntity, responseType);
return response.getBody();
}
/**
* 创建请求头
*/
private HttpHeaders createHeaders(Map<String, String> headers) {
HttpHeaders httpHeaders = new HttpHeaders();
if (headers != null && !headers.isEmpty()) {
headers.forEach(httpHeaders::set);
}
return httpHeaders;
}
}
```
### 4.3 Client客户端类
```java
package com.ruoyi.lsfx.client;
import com.ruoyi.lsfx.constants.LsfxConstants;
import com.ruoyi.lsfx.domain.request.*;
import com.ruoyi.lsfx.domain.response.*;
import com.ruoyi.lsfx.util.HttpUtil;
import com.ruoyi.lsfx.util.MD5Util;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
/**
* 流水分析平台客户端
*/
@Component
public class LsfxAnalysisClient {
@Resource
private HttpUtil httpUtil;
@Value("${lsfx.api.base-url}")
private String baseUrl;
@Value("${lsfx.api.app-id}")
private String appId;
@Value("${lsfx.api.app-secret}")
private String appSecret;
@Value("${lsfx.api.client-id}")
private String clientId;
// ==================== 接口1获取Token ====================
public GetTokenResponse getToken(GetTokenRequest request) {
String secretCode = MD5Util.generateSecretCode(
request.getProjectNo(),
request.getEntityName(),
appSecret
);
request.setAppSecretCode(secretCode);
request.setAppId(appId);
if (request.getAnalysisType() == null) {
request.setAnalysisType(LsfxConstants.ANALYSIS_TYPE);
}
if (request.getRole() == null) {
request.setRole(LsfxConstants.DEFAULT_ROLE);
}
String url = baseUrl + "/account/common/getToken";
return httpUtil.postJson(url, request, GetTokenResponse.class);
}
// ==================== 接口2上传文件 ====================
public UploadFileResponse uploadFile(Integer groupId, Resource file) {
String url = baseUrl + "/watson/api/project/remoteUploadSplitFile";
Map<String, Object> params = new HashMap<>();
params.put("groupId", groupId);
params.put("files", file);
Map<String, String> headers = new HashMap<>();
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
return httpUtil.uploadFile(url, params, headers, UploadFileResponse.class);
}
// ==================== 接口3拉取行内流水 ====================
public FetchInnerFlowResponse fetchInnerFlow(FetchInnerFlowRequest request) {
String url = baseUrl + "/watson/api/project/getJZFileOrZjrcuFile";
Map<String, String> headers = new HashMap<>();
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
return httpUtil.postJson(url, request, headers, FetchInnerFlowResponse.class);
}
// ==================== 接口4检查文件解析状态 ====================
public CheckParseStatusResponse checkParseStatus(Integer groupId, String inprogressList) {
String url = baseUrl + "/watson/api/project/upload/getpendings";
Map<String, Object> params = new HashMap<>();
params.put("groupId", groupId);
params.put("inprogressList", inprogressList);
Map<String, String> headers = new HashMap<>();
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
return httpUtil.postJson(url, params, headers, CheckParseStatusResponse.class);
}
// ==================== 接口5生成尽调报告 ====================
public GenerateReportResponse generateReport(GenerateReportRequest request) {
String url = baseUrl + "/watson/api/project/confirmStageUploadLogs";
Map<String, String> headers = new HashMap<>();
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
return httpUtil.postJson(url, request, headers, GenerateReportResponse.class);
}
// ==================== 接口6检查报告生成状态 ====================
public CheckReportStatusResponse checkReportStatus(Integer groupId) {
String url = baseUrl + "/watson/api/project/upload/getallpendings?groupId=" + groupId;
Map<String, String> headers = new HashMap<>();
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
return httpUtil.get(url, headers, CheckReportStatusResponse.class);
}
// ==================== 接口7获取银行流水 ====================
public GetBankStatementResponse getBankStatement(GetBankStatementRequest request) {
String url = baseUrl + "/watson/api/project/upload/getBankStatement";
Map<String, String> headers = new HashMap<>();
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
return httpUtil.postJson(url, request, headers, GetBankStatementResponse.class);
}
}
```
### 4.4 异常类
```java
package com.ruoyi.lsfx.exception;
/**
* 流水分析平台API异常
*/
public class LsfxApiException extends RuntimeException {
private String errorCode;
public LsfxApiException(String message) {
super(message);
}
public LsfxApiException(String message, Throwable cause) {
super(message, cause);
}
public LsfxApiException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
```
---
## 五、测试Controller设计
```java
package com.ruoyi.lsfx.controller;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.lsfx.client.LsfxAnalysisClient;
import com.ruoyi.lsfx.domain.request.*;
import com.ruoyi.lsfx.domain.response.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.core.io.Resource;
import javax.annotation.Resource;
/**
* 流水分析平台接口测试控制器
*/
@Tag(name = "流水分析平台接口测试", description = "用于测试流水分析平台的7个接口")
@RestController
@RequestMapping("/lsfx/test")
public class LsfxTestController {
@Resource
private LsfxAnalysisClient lsfxAnalysisClient;
@Operation(summary = "获取Token", description = "创建项目并获取访问Token")
@PostMapping("/getToken")
public AjaxResult getToken(@RequestBody GetTokenRequest request) {
GetTokenResponse response = lsfxAnalysisClient.getToken(request);
return AjaxResult.success(response);
}
@Operation(summary = "上传流水文件", description = "上传银行流水文件到流水分析平台")
@PostMapping("/uploadFile")
public AjaxResult uploadFile(
@Parameter(description = "项目ID") @RequestParam Integer groupId,
@Parameter(description = "流水文件") @RequestParam("file") MultipartFile file
) {
Resource fileResource = file.getResource();
UploadFileResponse response = lsfxAnalysisClient.uploadFile(groupId, fileResource);
return AjaxResult.success(response);
}
@Operation(summary = "拉取行内流水", description = "从数仓拉取行内流水数据")
@PostMapping("/fetchInnerFlow")
public AjaxResult fetchInnerFlow(@RequestBody FetchInnerFlowRequest request) {
FetchInnerFlowResponse response = lsfxAnalysisClient.fetchInnerFlow(request);
return AjaxResult.success(response);
}
@Operation(summary = "检查文件解析状态", description = "轮询检查上传文件的解析状态")
@PostMapping("/checkParseStatus")
public AjaxResult checkParseStatus(
@Parameter(description = "项目ID") @RequestParam Integer groupId,
@Parameter(description = "文件ID列表") @RequestParam String inprogressList
) {
CheckParseStatusResponse response = lsfxAnalysisClient.checkParseStatus(groupId, inprogressList);
return AjaxResult.success(response);
}
@Operation(summary = "生成尽调报告", description = "确认文件后生成尽调报告")
@PostMapping("/generateReport")
public AjaxResult generateReport(@RequestBody GenerateReportRequest request) {
GenerateReportResponse response = lsfxAnalysisClient.generateReport(request);
return AjaxResult.success(response);
}
@Operation(summary = "检查报告生成状态", description = "轮询检查尽调报告生成状态")
@GetMapping("/checkReportStatus")
public AjaxResult checkReportStatus(
@Parameter(description = "项目ID") @RequestParam Integer groupId
) {
CheckReportStatusResponse response = lsfxAnalysisClient.checkReportStatus(groupId);
return AjaxResult.success(response);
}
@Operation(summary = "获取银行流水列表", description = "分页获取银行流水数据")
@PostMapping("/getBankStatement")
public AjaxResult getBankStatement(@RequestBody GetBankStatementRequest request) {
GetBankStatementResponse response = lsfxAnalysisClient.getBankStatement(request);
return AjaxResult.success(response);
}
}
```
---
## 六、Maven依赖配置
**ccdi-lsfx/pom.xml**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi</artifactId>
<version>3.9.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ccdi-lsfx</artifactId>
<description>流水分析平台对接模块</description>
<dependencies>
<!-- 通用工具 -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common</artifactId>
</dependency>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- SpringDoc OpenAPI (Swagger) -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
</dependencies>
</project>
```
---
## 七、实施要点
### 7.1 开发顺序
1. **创建模块结构** - 创建ccdi-lsfx模块及基础目录
2. **添加配置** - 修改pom.xml添加application.yml配置
3. **实现工具类** - MD5Util、HttpUtil
4. **创建DTO对象** - 7个接口的Request和Response类
5. **实现Client** - LsfxAnalysisClient封装接口调用
6. **创建测试Controller** - 提供测试端点
7. **测试验证** - 使用Swagger测试各个接口
### 7.2 注意事项
1. **安全码生成** - Token接口需要MD5加密生成安全码
2. **请求头设置** - 除Token接口外其他接口需要设置X-Xencio-Client-Id请求头
3. **文件上传** - 上传文件接口使用multipart/form-data格式
4. **轮询检查** - 解析状态和报告状态需要轮询检查,直到处理完成
5. **环境切换** - 测试环境和生产环境的URL和Client-Id不同需配置切换
### 7.3 测试计划
1. 单元测试 - 测试MD5Util、HttpUtil工具类
2. 集成测试 - 测试LsfxAnalysisClient的7个接口调用
3. 端到端测试 - 通过Swagger测试完整的调用流程
---
## 八、后续扩展
### 8.1 可选增强功能
1. **日志记录** - 添加详细的接口调用日志
2. **重试机制** - 对失败的接口调用添加自动重试
3. **熔断降级** - 使用Resilience4j实现熔断和降级
4. **数据持久化** - 将获取的流水数据保存到数据库
5. **异步处理** - 使用异步方式处理耗时的接口调用
### 8.2 业务集成
未来可根据业务需求,将此模块集成到具体的业务场景中,如:
- 员工异常行为调查时自动获取流水数据
- 定期批量拉取流水数据
- 与前端页面集成展示流水信息
---
## 九、参考文档
- [兰溪-流水分析对接.md](../../doc/对接流水分析/兰溪-流水分析对接.md)
- [RestTemplate官方文档](https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#rest-client-access)
- [Spring Boot官方文档](https://spring.io/projects/spring-boot)