Compare commits
45 Commits
aa34361bf3
...
0a815be4bd
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a815be4bd | |||
| b022ec75b8 | |||
| a1f062d09d | |||
| 1983d93a5d | |||
| 651e4540af | |||
| 661fa88839 | |||
| 1bc65f9830 | |||
| 0d4fcd089b | |||
| e6bc2d64dd | |||
| aa17a14c4e | |||
| 921c15ffad | |||
| 72bab28b5d | |||
| ac4ebd1d22 | |||
| b2471c3cc7 | |||
| fe7f7eafce | |||
| 731f078348 | |||
| b89584a3dc | |||
| c272ee79d8 | |||
| 27b58d20d1 | |||
| d122e52c82 | |||
| c1099ddce7 | |||
| f21da8b1e9 | |||
| 7cc0dd30f1 | |||
| 6d101a018f | |||
| 3039300518 | |||
| 049b6dcbd5 | |||
| e9d6b0245a | |||
| 97927b40eb | |||
| aeab0d83ae | |||
| d2645a9cbb | |||
| 51f5bc58c7 | |||
| a6b36241aa | |||
| 2a9bb7f2b6 | |||
| 0c20a18a9a | |||
| 04afa03d0d | |||
| d20ba860ba | |||
| 51918d25e9 | |||
| 8a75a34242 | |||
| a32af2fc37 | |||
| 4d94a3cd9d | |||
| 9f70795911 | |||
| 46dd386919 | |||
| 79f00f30d8 | |||
| 4d4076227f | |||
| 690c2aa267 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -56,3 +56,7 @@ test/
|
||||
######################################################################
|
||||
# Excel Temporary Files
|
||||
doc/test-data/**/~$*
|
||||
|
||||
######################################################################
|
||||
# Database Configuration
|
||||
db_config.conf
|
||||
|
||||
48
ccdi-lsfx/pom.xml
Normal file
48
ccdi-lsfx/pom.xml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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>
|
||||
|
||||
<!-- Apache HttpClient (用于连接池) -->
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||
<artifactId>httpclient5</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>
|
||||
@@ -0,0 +1,246 @@
|
||||
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.exception.LsfxApiException;
|
||||
import com.ruoyi.lsfx.util.HttpUtil;
|
||||
import com.ruoyi.lsfx.util.MD5Util;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 流水分析平台客户端
|
||||
*/
|
||||
@Slf4j
|
||||
@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;
|
||||
|
||||
@Value("${lsfx.api.endpoints.get-token}")
|
||||
private String getTokenEndpoint;
|
||||
|
||||
@Value("${lsfx.api.endpoints.upload-file}")
|
||||
private String uploadFileEndpoint;
|
||||
|
||||
@Value("${lsfx.api.endpoints.fetch-inner-flow}")
|
||||
private String fetchInnerFlowEndpoint;
|
||||
|
||||
@Value("${lsfx.api.endpoints.check-parse-status}")
|
||||
private String checkParseStatusEndpoint;
|
||||
|
||||
@Value("${lsfx.api.endpoints.get-bank-statement}")
|
||||
private String getBankStatementEndpoint;
|
||||
|
||||
/**
|
||||
* 获取Token
|
||||
*/
|
||||
public GetTokenResponse getToken(GetTokenRequest request) {
|
||||
log.info("【流水分析】获取Token请求: projectNo={}, entityName={}", request.getProjectNo(), request.getEntityName());
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
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 + getTokenEndpoint;
|
||||
GetTokenResponse response = httpUtil.postJson(url, request, null, GetTokenResponse.class);
|
||||
|
||||
long elapsed = System.currentTimeMillis() - startTime;
|
||||
if (response != null && response.getData() != null) {
|
||||
log.info("【流水分析】获取Token成功: projectId={}, 耗时={}ms",
|
||||
response.getData().getProjectId(), elapsed);
|
||||
} else {
|
||||
log.warn("【流水分析】获取Token响应异常: 耗时={}ms", elapsed);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (LsfxApiException e) {
|
||||
log.error("【流水分析】获取Token失败: projectNo={}, error={}", request.getProjectNo(), e.getMessage(), e);
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("【流水分析】获取Token未知异常: projectNo={}", request.getProjectNo(), e);
|
||||
throw new LsfxApiException("获取Token失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*/
|
||||
public UploadFileResponse uploadFile(Integer groupId, org.springframework.core.io.Resource file) {
|
||||
log.info("【流水分析】上传文件请求: groupId={}, fileName={}", groupId, file.getFilename());
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
String url = baseUrl + uploadFileEndpoint;
|
||||
|
||||
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);
|
||||
|
||||
UploadFileResponse response = httpUtil.uploadFile(url, params, headers, UploadFileResponse.class);
|
||||
|
||||
long elapsed = System.currentTimeMillis() - startTime;
|
||||
if (response != null && response.getData() != null) {
|
||||
log.info("【流水分析】上传文件成功: uploadStatus={}, 耗时={}ms",
|
||||
response.getData().getUploadStatus(), elapsed);
|
||||
} else {
|
||||
log.warn("【流水分析】上传文件响应异常: 耗时={}ms", elapsed);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (LsfxApiException e) {
|
||||
log.error("【流水分析】上传文件失败: groupId={}, error={}", groupId, e.getMessage(), e);
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("【流水分析】上传文件未知异常: groupId={}", groupId, e);
|
||||
throw new LsfxApiException("上传文件失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取行内流水
|
||||
*/
|
||||
public FetchInnerFlowResponse fetchInnerFlow(FetchInnerFlowRequest request) {
|
||||
log.info("【流水分析】拉取行内流水请求: groupId={}, customerNo={}", request.getGroupId(), request.getCustomerNo());
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
String url = baseUrl + fetchInnerFlowEndpoint;
|
||||
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
|
||||
|
||||
FetchInnerFlowResponse response = httpUtil.postJson(url, request, headers, FetchInnerFlowResponse.class);
|
||||
|
||||
long elapsed = System.currentTimeMillis() - startTime;
|
||||
if (response != null && response.getData() != null) {
|
||||
log.info("【流水分析】拉取行内流水完成: code={}, message={}, 耗时={}ms",
|
||||
response.getData().getCode(), response.getData().getMessage(), elapsed);
|
||||
} else {
|
||||
log.warn("【流水分析】拉取行内流水响应异常: 耗时={}ms", elapsed);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (LsfxApiException e) {
|
||||
log.error("【流水分析】拉取行内流水失败: groupId={}, error={}", request.getGroupId(), e.getMessage(), e);
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("【流水分析】拉取行内流水未知异常: groupId={}", request.getGroupId(), e);
|
||||
throw new LsfxApiException("拉取行内流水失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件解析状态
|
||||
*/
|
||||
public CheckParseStatusResponse checkParseStatus(Integer groupId, String inprogressList) {
|
||||
log.info("【流水分析】检查文件解析状态: groupId={}, inprogressList={}", groupId, inprogressList);
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
String url = baseUrl + checkParseStatusEndpoint;
|
||||
|
||||
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);
|
||||
|
||||
CheckParseStatusResponse response = httpUtil.postJson(url, params, headers, CheckParseStatusResponse.class);
|
||||
|
||||
long elapsed = System.currentTimeMillis() - startTime;
|
||||
if (response != null && response.getData() != null) {
|
||||
log.info("【流水分析】检查解析状态完成: parsing={}, pendingList.size={}, 耗时={}ms",
|
||||
response.getData().getParsing(),
|
||||
response.getData().getPendingList() != null ? response.getData().getPendingList().size() : 0,
|
||||
elapsed);
|
||||
} else {
|
||||
log.warn("【流水分析】检查解析状态响应异常: 耗时={}ms", elapsed);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (LsfxApiException e) {
|
||||
log.error("【流水分析】检查解析状态失败: groupId={}, error={}", groupId, e.getMessage(), e);
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("【流水分析】检查解析状态未知异常: groupId={}", groupId, e);
|
||||
throw new LsfxApiException("检查解析状态失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取银行流水(新版接口)
|
||||
* 注意: 需要传入logId参数,参数名已从pageNum改为pageNow
|
||||
*
|
||||
* @param request 请求参数(groupId, logId, pageNow, pageSize)
|
||||
* @return 流水明细列表
|
||||
*/
|
||||
public GetBankStatementResponse getBankStatement(GetBankStatementRequest request) {
|
||||
log.info("【流水分析】获取银行流水请求: groupId={}, logId={}, pageNow={}, pageSize={}",
|
||||
request.getGroupId(), request.getLogId(), request.getPageNow(), request.getPageSize());
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
String url = baseUrl + getBankStatementEndpoint;
|
||||
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId);
|
||||
|
||||
GetBankStatementResponse response = httpUtil.postJson(url, request, headers, GetBankStatementResponse.class);
|
||||
|
||||
long elapsed = System.currentTimeMillis() - startTime;
|
||||
if (response != null && response.getData() != null) {
|
||||
log.info("【流水分析】获取银行流水成功: totalCount={}, 耗时={}ms",
|
||||
response.getData().getTotalCount(), elapsed);
|
||||
} else {
|
||||
log.warn("【流水分析】获取银行流水响应异常: 耗时={}ms", elapsed);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (LsfxApiException e) {
|
||||
log.error("【流水分析】获取银行流水失败: groupId={}, logId={}, error={}",
|
||||
request.getGroupId(), request.getLogId(), e.getMessage(), e);
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("【流水分析】获取银行流水未知异常: groupId={}, logId={}",
|
||||
request.getGroupId(), request.getLogId(), e);
|
||||
throw new LsfxApiException("获取银行流水失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.ruoyi.lsfx.config;
|
||||
|
||||
import org.apache.hc.client5.http.classic.HttpClient;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClients;
|
||||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
|
||||
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
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;
|
||||
|
||||
@Value("${lsfx.api.pool.max-total:100}")
|
||||
private int maxTotal;
|
||||
|
||||
@Value("${lsfx.api.pool.default-max-per-route:20}")
|
||||
private int defaultMaxPerRoute;
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
// 创建连接池管理器
|
||||
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
|
||||
connectionManager.setMaxTotal(maxTotal); // 最大连接数
|
||||
connectionManager.setDefaultMaxPerRoute(defaultMaxPerRoute); // 每个路由的最大连接数
|
||||
|
||||
// 创建HttpClient并设置连接池
|
||||
HttpClient httpClient = HttpClients.custom()
|
||||
.setConnectionManager(connectionManager)
|
||||
.build();
|
||||
|
||||
// 创建HttpComponentsClientHttpRequestFactory
|
||||
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
|
||||
factory.setConnectTimeout(connectionTimeout);
|
||||
factory.setConnectionRequestTimeout(connectionTimeout);
|
||||
|
||||
return new RestTemplate(factory);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
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";
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package com.ruoyi.lsfx.controller;
|
||||
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
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 jakarta.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) {
|
||||
// 参数校验
|
||||
if (StringUtils.isBlank(request.getProjectNo())) {
|
||||
return AjaxResult.error("参数不完整:projectNo为必填");
|
||||
}
|
||||
if (StringUtils.isBlank(request.getEntityName())) {
|
||||
return AjaxResult.error("参数不完整:entityName为必填");
|
||||
}
|
||||
if (StringUtils.isBlank(request.getUserId())) {
|
||||
return AjaxResult.error("参数不完整:userId为必填");
|
||||
}
|
||||
if (StringUtils.isBlank(request.getUserName())) {
|
||||
return AjaxResult.error("参数不完整:userName为必填");
|
||||
}
|
||||
if (StringUtils.isBlank(request.getOrgCode())) {
|
||||
return AjaxResult.error("参数不完整:orgCode为必填");
|
||||
}
|
||||
if (StringUtils.isBlank(request.getDepartmentCode())) {
|
||||
return AjaxResult.error("参数不完整:departmentCode为必填");
|
||||
}
|
||||
|
||||
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
|
||||
) {
|
||||
// 参数校验
|
||||
if (groupId == null || groupId <= 0) {
|
||||
return AjaxResult.error("参数不完整:groupId为必填且大于0");
|
||||
}
|
||||
if (file == null || file.isEmpty()) {
|
||||
return AjaxResult.error("参数不完整:文件不能为空");
|
||||
}
|
||||
if (file.getSize() > 10 * 1024 * 1024) { // 10MB限制
|
||||
return AjaxResult.error("文件大小超过限制:最大10MB");
|
||||
}
|
||||
|
||||
org.springframework.core.io.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) {
|
||||
// 参数校验
|
||||
if (request.getGroupId() == null || request.getGroupId() <= 0) {
|
||||
return AjaxResult.error("参数不完整:groupId为必填且大于0");
|
||||
}
|
||||
if (StringUtils.isEmpty(request.getCustomerNo())) {
|
||||
return AjaxResult.error("参数不完整:customerNo为必填");
|
||||
}
|
||||
if (request.getRequestDateId() == null) {
|
||||
return AjaxResult.error("参数不完整:requestDateId为必填");
|
||||
}
|
||||
if (request.getDataStartDateId() == null) {
|
||||
return AjaxResult.error("参数不完整:dataStartDateId为必填");
|
||||
}
|
||||
if (request.getDataEndDateId() == null) {
|
||||
return AjaxResult.error("参数不完整:dataEndDateId为必填");
|
||||
}
|
||||
if (request.getDataStartDateId() > request.getDataEndDateId()) {
|
||||
return AjaxResult.error("参数错误:开始日期不能大于结束日期");
|
||||
}
|
||||
|
||||
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
|
||||
) {
|
||||
// 参数校验
|
||||
if (groupId == null || groupId <= 0) {
|
||||
return AjaxResult.error("参数不完整:groupId为必填且大于0");
|
||||
}
|
||||
if (StringUtils.isEmpty(inprogressList)) {
|
||||
return AjaxResult.error("参数不完整:inprogressList为必填");
|
||||
}
|
||||
|
||||
CheckParseStatusResponse response = lsfxAnalysisClient.checkParseStatus(groupId, inprogressList);
|
||||
return AjaxResult.success(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取银行流水列表(新版)",
|
||||
description = "分页获取指定文件的银行流水数据,需要提供logId参数")
|
||||
@PostMapping("/getBankStatement")
|
||||
public AjaxResult getBankStatement(@RequestBody GetBankStatementRequest request) {
|
||||
// 参数校验
|
||||
if (request.getGroupId() == null) {
|
||||
return AjaxResult.error("参数不完整:groupId为必填");
|
||||
}
|
||||
if (request.getLogId() == null) {
|
||||
return AjaxResult.error("参数不完整:logId为必填(文件ID)");
|
||||
}
|
||||
if (request.getPageNow() == null || request.getPageNow() < 1) {
|
||||
return AjaxResult.error("参数不完整:pageNow为必填且大于0");
|
||||
}
|
||||
if (request.getPageSize() == null || request.getPageSize() < 1) {
|
||||
return AjaxResult.error("参数不完整:pageSize为必填且大于0");
|
||||
}
|
||||
|
||||
GetBankStatementResponse response = lsfxAnalysisClient.getBankStatement(request);
|
||||
return AjaxResult.success(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.ruoyi.lsfx.domain.request;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 拉取行内流水请求参数(匹配新文档3.2节)
|
||||
*/
|
||||
@Data
|
||||
public class FetchInnerFlowRequest {
|
||||
|
||||
/** 项目ID */
|
||||
private Integer groupId;
|
||||
|
||||
/** 客户身份证号 */
|
||||
private String customerNo;
|
||||
|
||||
/** 数据渠道编码(固定值:ZJRCU) */
|
||||
private String dataChannelCode;
|
||||
|
||||
/** 发起请求的时间(格式:yyyyMMdd) */
|
||||
private Integer requestDateId;
|
||||
|
||||
/** 拉取开始日期(格式:yyyyMMdd) */
|
||||
private Integer dataStartDateId;
|
||||
|
||||
/** 拉取结束日期(格式:yyyyMMdd) */
|
||||
private Integer dataEndDateId;
|
||||
|
||||
/** 柜员号 */
|
||||
private Integer uploadUserId;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.ruoyi.lsfx.domain.request;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 获取银行流水请求参数(匹配新文档6.2节)
|
||||
*/
|
||||
@Data
|
||||
public class GetBankStatementRequest {
|
||||
|
||||
/** 项目ID */
|
||||
private Integer groupId;
|
||||
|
||||
/** 文件ID(新增必填参数) */
|
||||
private Integer logId;
|
||||
|
||||
/** 当前页码(原pageNum) */
|
||||
private Integer pageNow;
|
||||
|
||||
/** 每页数量 */
|
||||
private Integer pageSize;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.ruoyi.lsfx.domain.response;
|
||||
|
||||
import lombok.Data;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 检查文件解析状态响应(匹配新文档4.5节)
|
||||
*/
|
||||
@Data
|
||||
public class CheckParseStatusResponse {
|
||||
|
||||
/** 返回码 */
|
||||
private String code;
|
||||
|
||||
/** 状态 */
|
||||
private String status;
|
||||
|
||||
/** 成功标识 */
|
||||
private Boolean successResponse;
|
||||
|
||||
/** 响应数据 */
|
||||
private ParseStatusData data;
|
||||
|
||||
@Data
|
||||
public static class ParseStatusData {
|
||||
/** 是否正在解析(true=解析中, false=解析结束) - 关键字段 */
|
||||
private Boolean parsing;
|
||||
|
||||
/** 待处理文件列表 */
|
||||
private List<PendingItem> pendingList;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class PendingItem {
|
||||
/** 账号列表 */
|
||||
private List<String> accountNoList;
|
||||
|
||||
/** 银行名称 */
|
||||
private String bankName;
|
||||
|
||||
/** 数据类型信息 */
|
||||
private List<String> dataTypeInfo;
|
||||
|
||||
/** 下载文件名 */
|
||||
private String downloadFileName;
|
||||
|
||||
/** 企业名称列表 */
|
||||
private List<String> enterpriseNameList;
|
||||
|
||||
/** 文件包ID */
|
||||
private String filePackageId;
|
||||
|
||||
/** 文件大小(字节) */
|
||||
private Long fileSize;
|
||||
|
||||
/** 上传用户ID */
|
||||
private Integer fileUploadBy;
|
||||
|
||||
/** 上传用户名 */
|
||||
private String fileUploadByUserName;
|
||||
|
||||
/** 上传时间 */
|
||||
private String fileUploadTime;
|
||||
|
||||
/** 是否拆分 */
|
||||
private Integer isSplit;
|
||||
|
||||
/** 企业ID */
|
||||
private Integer leId;
|
||||
|
||||
/** 文件ID(重要) */
|
||||
private Integer logId;
|
||||
|
||||
/** 日志元数据 */
|
||||
private String logMeta;
|
||||
|
||||
/** 日志类型 */
|
||||
private String logType;
|
||||
|
||||
/** 登录企业ID */
|
||||
private Integer loginLeId;
|
||||
|
||||
/** 丢失的表头 */
|
||||
private List<String> lostHeader;
|
||||
|
||||
/** 真实银行名称 */
|
||||
private String realBankName;
|
||||
|
||||
/** 行数 */
|
||||
private Integer rows;
|
||||
|
||||
/** 来源 */
|
||||
private String source;
|
||||
|
||||
/** 状态(-5表示成功) */
|
||||
private Integer status;
|
||||
|
||||
/** 模板名称 */
|
||||
private String templateName;
|
||||
|
||||
/** 总记录数 */
|
||||
private Integer totalRecords;
|
||||
|
||||
/** 交易结束日期ID */
|
||||
private Integer trxDateEndId;
|
||||
|
||||
/** 交易开始日期ID */
|
||||
private Integer trxDateStartId;
|
||||
|
||||
/** 上传文件名 */
|
||||
private String uploadFileName;
|
||||
|
||||
/** 上传状态描述(data.wait.confirm.newaccount表示成功) */
|
||||
private String uploadStatusDesc;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.ruoyi.lsfx.domain.response;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 拉取行内流水响应(匹配新文档3.5节)
|
||||
*/
|
||||
@Data
|
||||
public class FetchInnerFlowResponse {
|
||||
|
||||
/** 返回码 */
|
||||
private String code;
|
||||
|
||||
/** 状态 */
|
||||
private String status;
|
||||
|
||||
/** 成功标识 */
|
||||
private Boolean successResponse;
|
||||
|
||||
/** 响应数据 */
|
||||
private FetchData data;
|
||||
|
||||
@Data
|
||||
public static class FetchData {
|
||||
/** 状态码(如:501014表示无行内流水文件) */
|
||||
private String code;
|
||||
|
||||
/** 消息(如:无行内流水文件) */
|
||||
private String message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package com.ruoyi.lsfx.domain.response;
|
||||
|
||||
import lombok.Data;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 获取银行流水响应(匹配新文档6.5节)
|
||||
*/
|
||||
@Data
|
||||
public class GetBankStatementResponse {
|
||||
|
||||
/** 返回码 */
|
||||
private String code;
|
||||
|
||||
/** 状态 */
|
||||
private String status;
|
||||
|
||||
/** 成功标识 */
|
||||
private Boolean successResponse;
|
||||
|
||||
/** 响应数据 */
|
||||
private BankStatementData data;
|
||||
|
||||
@Data
|
||||
public static class BankStatementData {
|
||||
/** 流水列表 */
|
||||
private List<BankStatementItem> bankStatementList;
|
||||
|
||||
/** 总条数 */
|
||||
private Integer totalCount;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class BankStatementItem {
|
||||
// ===== 账号相关信息 =====
|
||||
|
||||
/** 流水ID */
|
||||
private Long bankStatementId;
|
||||
|
||||
/** 企业ID */
|
||||
private Integer leId;
|
||||
|
||||
/** 账号ID */
|
||||
private Long accountId;
|
||||
|
||||
/** 企业账号名称 */
|
||||
private String leName;
|
||||
|
||||
/** 企业银行账号 */
|
||||
private String accountMaskNo;
|
||||
|
||||
/** 账号日期ID */
|
||||
private Integer accountingDateId;
|
||||
|
||||
/** 账号日期 */
|
||||
private String accountingDate;
|
||||
|
||||
/** 交易日期 */
|
||||
private String trxDate;
|
||||
|
||||
/** 币种 */
|
||||
private String currency;
|
||||
|
||||
// ===== 交易金额 =====
|
||||
|
||||
/** 付款金额 */
|
||||
private BigDecimal drAmount;
|
||||
|
||||
/** 收款金额 */
|
||||
private BigDecimal crAmount;
|
||||
|
||||
/** 余额 */
|
||||
private BigDecimal balanceAmount;
|
||||
|
||||
/** 交易金额 */
|
||||
private BigDecimal transAmount;
|
||||
|
||||
// ===== 交易类型和标志 =====
|
||||
|
||||
/** 交易类型 */
|
||||
private String cashType;
|
||||
|
||||
/** 交易标志位 */
|
||||
private String transFlag;
|
||||
|
||||
/** 分类ID */
|
||||
private Integer transTypeId;
|
||||
|
||||
/** 异常类型 */
|
||||
private String exceptionType;
|
||||
|
||||
// ===== 对手方信息 =====
|
||||
|
||||
/** 对手方企业ID */
|
||||
private Integer customerId;
|
||||
|
||||
/** 对手方企业名称 */
|
||||
private String customerName;
|
||||
|
||||
/** 对手方账号 */
|
||||
private String customerAccountMaskNo;
|
||||
|
||||
/** 对手方银行 */
|
||||
private String customerBank;
|
||||
|
||||
/** 对手方备注 */
|
||||
private String customerReference;
|
||||
|
||||
// ===== 摘要和备注 =====
|
||||
|
||||
/** 用户交易摘要 */
|
||||
private String userMemo;
|
||||
|
||||
/** 银行交易摘要 */
|
||||
private String bankComments;
|
||||
|
||||
/** 银行交易号 */
|
||||
private String bankTrxNumber;
|
||||
|
||||
// ===== 银行信息 =====
|
||||
|
||||
/** 所属银行缩写 */
|
||||
private String bank;
|
||||
|
||||
// ===== 其他字段 =====
|
||||
|
||||
/** 是否为内部交易 */
|
||||
private Integer internalFlag;
|
||||
|
||||
/** 上传logId */
|
||||
private Integer batchId;
|
||||
|
||||
/** 项目id */
|
||||
private Integer groupId;
|
||||
|
||||
/** 覆盖标识 */
|
||||
private Long overrideBsId;
|
||||
|
||||
/** 交易方式 */
|
||||
private String paymentMethod;
|
||||
|
||||
/** 客户账号掩码号 */
|
||||
private String cretNo;
|
||||
|
||||
// ===== 附加字段 =====
|
||||
|
||||
/** 附件数量 */
|
||||
private Integer attachments;
|
||||
|
||||
/** 评论数 */
|
||||
private Integer commentsNum;
|
||||
|
||||
/** 归档标志 */
|
||||
private Integer archivingFlag;
|
||||
|
||||
/** 下付款标志 */
|
||||
private Integer downPaymentFlag;
|
||||
|
||||
/** 源目录ID */
|
||||
private Integer sourceCatalogId;
|
||||
|
||||
/** 拆分标志 */
|
||||
private Integer split;
|
||||
|
||||
/** 子流水ID */
|
||||
private Long subBankstatementId;
|
||||
|
||||
/** 待办标志 */
|
||||
private Integer toDoFlag;
|
||||
|
||||
/** 转换金额 */
|
||||
private BigDecimal transformAmount;
|
||||
|
||||
/** 转换收款金额 */
|
||||
private BigDecimal transformCrAmount;
|
||||
|
||||
/** 转换付款金额 */
|
||||
private BigDecimal transformDrAmount;
|
||||
|
||||
/** 转换余额 */
|
||||
private BigDecimal transfromBalanceAmount;
|
||||
|
||||
/** 交易余额 */
|
||||
private BigDecimal trxBalance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.ruoyi.lsfx.domain.response;
|
||||
|
||||
import lombok.Data;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 上传文件响应(完整版,匹配新文档2.5节)
|
||||
*/
|
||||
@Data
|
||||
public class UploadFileResponse {
|
||||
|
||||
/** 返回码 */
|
||||
private String code;
|
||||
|
||||
/** 状态 */
|
||||
private String status;
|
||||
|
||||
/** 成功标识 */
|
||||
private Boolean successResponse;
|
||||
|
||||
/** 响应数据 */
|
||||
private UploadData data;
|
||||
|
||||
@Data
|
||||
public static class UploadData {
|
||||
/** 账号映射信息(key为logId) */
|
||||
private Map<String, List<AccountInfo>> accountsOfLog;
|
||||
|
||||
/** 上传日志列表 */
|
||||
private List<UploadLogItem> uploadLogList;
|
||||
|
||||
/** 上传状态 */
|
||||
private Integer uploadStatus;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class AccountInfo {
|
||||
/** 所属银行 */
|
||||
private String bank;
|
||||
|
||||
/** 账号名称 */
|
||||
private String accountName;
|
||||
|
||||
/** 账号 */
|
||||
private String accountNo;
|
||||
|
||||
/** 币种 */
|
||||
private String currency;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class UploadLogItem {
|
||||
/** 账号列表 */
|
||||
private List<String> accountNoList;
|
||||
|
||||
/** 银行名称 */
|
||||
private String bankName;
|
||||
|
||||
/** 数据类型信息 [格式, 分隔符] */
|
||||
private List<String> dataTypeInfo;
|
||||
|
||||
/** 下载文件名 */
|
||||
private String downloadFileName;
|
||||
|
||||
/** 企业名称列表 */
|
||||
private List<String> enterpriseNameList;
|
||||
|
||||
/** 文件包ID */
|
||||
private String filePackageId;
|
||||
|
||||
/** 文件大小(字节) */
|
||||
private Long fileSize;
|
||||
|
||||
/** 上传用户ID */
|
||||
private Integer fileUploadBy;
|
||||
|
||||
/** 上传用户名 */
|
||||
private String fileUploadByUserName;
|
||||
|
||||
/** 上传时间 */
|
||||
private String fileUploadTime;
|
||||
|
||||
/** 企业ID */
|
||||
private Integer leId;
|
||||
|
||||
/** 文件ID(重要) */
|
||||
private Integer logId;
|
||||
|
||||
/** 日志元数据 */
|
||||
private String logMeta;
|
||||
|
||||
/** 日志类型 */
|
||||
private String logType;
|
||||
|
||||
/** 登录企业ID */
|
||||
private Integer loginLeId;
|
||||
|
||||
/** 真实银行名称 */
|
||||
private String realBankName;
|
||||
|
||||
/** 行数 */
|
||||
private Integer rows;
|
||||
|
||||
/** 来源 */
|
||||
private String source;
|
||||
|
||||
/** 状态(-5表示成功) */
|
||||
private Integer status;
|
||||
|
||||
/** 模板名称 */
|
||||
private String templateName;
|
||||
|
||||
/** 总记录数 */
|
||||
private Integer totalRecords;
|
||||
|
||||
/** 交易结束日期ID */
|
||||
private Integer trxDateEndId;
|
||||
|
||||
/** 交易开始日期ID */
|
||||
private Integer trxDateStartId;
|
||||
|
||||
/** 上传文件名 */
|
||||
private String uploadFileName;
|
||||
|
||||
/** 上传状态描述 */
|
||||
private String uploadStatusDesc;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
135
ccdi-lsfx/src/main/java/com/ruoyi/lsfx/util/HttpUtil.java
Normal file
135
ccdi-lsfx/src/main/java/com/ruoyi/lsfx/util/HttpUtil.java
Normal file
@@ -0,0 +1,135 @@
|
||||
package com.ruoyi.lsfx.util;
|
||||
|
||||
import com.ruoyi.lsfx.exception.LsfxApiException;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* HTTP请求工具类
|
||||
*/
|
||||
@Component
|
||||
public class HttpUtil {
|
||||
|
||||
@Resource
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
/**
|
||||
* 发送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
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件(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(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("文件上传失败,HTTP状态码: " + response.getStatusCode());
|
||||
}
|
||||
|
||||
T responseBody = response.getBody();
|
||||
if (responseBody == null) {
|
||||
throw new LsfxApiException("文件上传返回数据为空");
|
||||
}
|
||||
|
||||
return responseBody;
|
||||
} catch (RestClientException e) {
|
||||
throw new LsfxApiException("文件上传请求失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建请求头
|
||||
* @param headers 请求头Map
|
||||
* @return HttpHeaders对象
|
||||
*/
|
||||
private HttpHeaders createHeaders(Map<String, String> headers) {
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
if (headers != null && !headers.isEmpty()) {
|
||||
headers.forEach(httpHeaders::set);
|
||||
}
|
||||
return httpHeaders;
|
||||
}
|
||||
}
|
||||
45
ccdi-lsfx/src/main/java/com/ruoyi/lsfx/util/MD5Util.java
Normal file
45
ccdi-lsfx/src/main/java/com/ruoyi/lsfx/util/MD5Util.java
Normal file
@@ -0,0 +1,45 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
76
doc/database/alter_collation_to_general_ci.sql
Normal file
76
doc/database/alter_collation_to_general_ci.sql
Normal file
@@ -0,0 +1,76 @@
|
||||
-- =====================================================
|
||||
-- 修改数据库字段排序规则脚本
|
||||
-- 从 utf8mb4_unicode_ci 改为 utf8mb4_general_ci
|
||||
-- 目标表:3 个表,45 个字段
|
||||
-- 执行时间:2026-02-28
|
||||
-- =====================================================
|
||||
|
||||
USE ccdi;
|
||||
|
||||
-- =====================================================
|
||||
-- 1. 修改 ccdi_base_staff 表(5 个字段)
|
||||
-- =====================================================
|
||||
ALTER TABLE ccdi_base_staff MODIFY COLUMN name varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '姓名';
|
||||
ALTER TABLE ccdi_base_staff MODIFY COLUMN phone varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '电话';
|
||||
ALTER TABLE ccdi_base_staff MODIFY COLUMN status char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '状态(0在职 1离职)';
|
||||
ALTER TABLE ccdi_base_staff MODIFY COLUMN create_by varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '创建者';
|
||||
ALTER TABLE ccdi_base_staff MODIFY COLUMN update_by varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '更新者';
|
||||
|
||||
-- =====================================================
|
||||
-- 2. 修改 ccdi_biz_intermediary 表(20 个字段)
|
||||
-- =====================================================
|
||||
ALTER TABLE ccdi_biz_intermediary MODIFY COLUMN biz_id varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '人员ID';
|
||||
ALTER TABLE ccdi_biz_intermediary MODIFY COLUMN person_type varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '人员类型';
|
||||
ALTER TABLE ccdi_biz_intermediary MODIFY COLUMN person_sub_type varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '人员子类型';
|
||||
ALTER TABLE ccdi_biz_intermediary MODIFY COLUMN name varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '姓名';
|
||||
ALTER TABLE ccdi_biz_intermediary MODIFY COLUMN gender char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '性别';
|
||||
ALTER TABLE ccdi_biz_intermediary MODIFY COLUMN id_type varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '身份证' COMMENT '证件类型';
|
||||
ALTER TABLE ccdi_biz_intermediary MODIFY COLUMN person_id varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '证件号码';
|
||||
ALTER TABLE ccdi_biz_intermediary MODIFY COLUMN mobile varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '手机号码';
|
||||
ALTER TABLE ccdi_biz_intermediary MODIFY COLUMN wechat_no varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '微信号';
|
||||
ALTER TABLE ccdi_biz_intermediary MODIFY COLUMN contact_address varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '联系地址';
|
||||
ALTER TABLE ccdi_biz_intermediary MODIFY COLUMN company varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '所在公司';
|
||||
ALTER TABLE ccdi_biz_intermediary MODIFY COLUMN social_credit_code varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '企业统一信用码';
|
||||
ALTER TABLE ccdi_biz_intermediary MODIFY COLUMN position varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '职位';
|
||||
ALTER TABLE ccdi_biz_intermediary MODIFY COLUMN related_num_id varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '关联人员ID';
|
||||
ALTER TABLE ccdi_biz_intermediary MODIFY COLUMN relation_type varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '关联关系';
|
||||
ALTER TABLE ccdi_biz_intermediary MODIFY COLUMN data_source varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT 'MANUAL' COMMENT '数据来源';
|
||||
ALTER TABLE ccdi_biz_intermediary MODIFY COLUMN remark varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注信息';
|
||||
ALTER TABLE ccdi_biz_intermediary MODIFY COLUMN created_by varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '记录创建人';
|
||||
ALTER TABLE ccdi_biz_intermediary MODIFY COLUMN updated_by varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '记录更新人';
|
||||
|
||||
-- =====================================================
|
||||
-- 3. 修改 ccdi_enterprise_base_info 表(20 个字段)
|
||||
-- =====================================================
|
||||
ALTER TABLE ccdi_enterprise_base_info MODIFY COLUMN social_credit_code varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '统一社会信用代码';
|
||||
ALTER TABLE ccdi_enterprise_base_info MODIFY COLUMN enterprise_name varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '企业名称';
|
||||
ALTER TABLE ccdi_enterprise_base_info MODIFY COLUMN enterprise_type varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '企业类型';
|
||||
ALTER TABLE ccdi_enterprise_base_info MODIFY COLUMN enterprise_nature varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '企业性质';
|
||||
ALTER TABLE ccdi_enterprise_base_info MODIFY COLUMN industry_class varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '行业分类';
|
||||
ALTER TABLE ccdi_enterprise_base_info MODIFY COLUMN industry_name varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '所属行业';
|
||||
ALTER TABLE ccdi_enterprise_base_info MODIFY COLUMN register_address varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '注册地址';
|
||||
ALTER TABLE ccdi_enterprise_base_info MODIFY COLUMN legal_representative varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '法定代表人';
|
||||
ALTER TABLE ccdi_enterprise_base_info MODIFY COLUMN legal_cert_type varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '法定代表人证件类型';
|
||||
ALTER TABLE ccdi_enterprise_base_info MODIFY COLUMN legal_cert_no varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '法定代表人证件号码';
|
||||
ALTER TABLE ccdi_enterprise_base_info MODIFY COLUMN shareholder1 varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '股东1';
|
||||
ALTER TABLE ccdi_enterprise_base_info MODIFY COLUMN shareholder2 varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '股东2';
|
||||
ALTER TABLE ccdi_enterprise_base_info MODIFY COLUMN shareholder3 varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '股东3';
|
||||
ALTER TABLE ccdi_enterprise_base_info MODIFY COLUMN shareholder4 varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '股东4';
|
||||
ALTER TABLE ccdi_enterprise_base_info MODIFY COLUMN shareholder5 varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '股东5';
|
||||
ALTER TABLE ccdi_enterprise_base_info MODIFY COLUMN status varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '经营状态';
|
||||
ALTER TABLE ccdi_enterprise_base_info MODIFY COLUMN risk_level varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '3' COMMENT '风险等级:1-高风险, 2-中风险, 3-低风险';
|
||||
ALTER TABLE ccdi_enterprise_base_info MODIFY COLUMN ent_source varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT 'GENERAL' COMMENT '企业来源:GENERAL-一般企业, EMP_RELATION-员工关系人, CREDIT_CUSTOMER-信贷客户, INTERMEDIARY-中介, BOTH-兼有';
|
||||
ALTER TABLE ccdi_enterprise_base_info MODIFY COLUMN data_source varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT 'MANUAL' COMMENT '数据来源';
|
||||
ALTER TABLE ccdi_enterprise_base_info MODIFY COLUMN created_by varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '创建人';
|
||||
ALTER TABLE ccdi_enterprise_base_info MODIFY COLUMN updated_by varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '更新人';
|
||||
|
||||
-- =====================================================
|
||||
-- 验证修改结果
|
||||
-- =====================================================
|
||||
SELECT
|
||||
COUNT(*) as remaining_unicode_ci_columns
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'ccdi'
|
||||
AND COLLATION_NAME = 'utf8mb4_unicode_ci';
|
||||
|
||||
-- 应该返回 0
|
||||
0
doc/database/backup/.gitkeep
Normal file
0
doc/database/backup/.gitkeep
Normal file
450
doc/database/backup/ccdi_data.sql
Normal file
450
doc/database/backup/ccdi_data.sql
Normal file
File diff suppressed because one or more lines are too long
1095
doc/database/backup/ccdi_structure.sql
Normal file
1095
doc/database/backup/ccdi_structure.sql
Normal file
File diff suppressed because it is too large
Load Diff
468
doc/database/数据库迁移操作指南.md
Normal file
468
doc/database/数据库迁移操作指南.md
Normal file
@@ -0,0 +1,468 @@
|
||||
# CCDI 数据库迁移操作指南
|
||||
|
||||
## 概述
|
||||
|
||||
本文档提供 CCDI 纪检初核系统数据库迁移的详细操作步骤,包括从开发环境导出数据库和导入到生产/测试环境。
|
||||
|
||||
## 脚本说明
|
||||
|
||||
项目提供两个独立的脚本:
|
||||
|
||||
1. **export_database.sh** - 数据库导出脚本
|
||||
- 从开发环境导出数据库
|
||||
- 生成表结构和数据文件到 `doc/database/backup/` 文件夹
|
||||
- 配置已内置在脚本顶部
|
||||
|
||||
2. **import_database.sh** - 数据库导入脚本
|
||||
- 从 `doc/database/backup/` 文件夹读取备份文件
|
||||
- 导入到指定的目标环境(dev/test/prod)
|
||||
- 配置已内置在脚本顶部
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
项目根目录/
|
||||
├── export_database.sh # 导出脚本(配置已内置)
|
||||
├── import_database.sh # 导入脚本(配置已内置)
|
||||
└── doc/
|
||||
└── database/
|
||||
├── 数据库迁移操作指南.md # 本文档
|
||||
├── alter_collation_to_general_ci.sql # 排序规则修改脚本
|
||||
└── backup/ # 备份文件夹
|
||||
├── .gitkeep
|
||||
├── ccdi_structure.sql # 表结构(~60KB)
|
||||
└── ccdi_data.sql # 数据文件(~5.7MB)
|
||||
```
|
||||
|
||||
**注意:** 数据库配置已直接内置在脚本中,无需额外的配置文件。
|
||||
|
||||
## 前置条件
|
||||
|
||||
### 必需工具
|
||||
- MySQL 客户端工具(包含 mysqldump 和 mysql 命令)
|
||||
- Bash shell 环境(Windows 用户可使用 Git Bash)
|
||||
- 网络访问权限(能连接源数据库和目标数据库)
|
||||
|
||||
### 检查工具是否安装
|
||||
```bash
|
||||
mysqldump --version
|
||||
mysql --version
|
||||
```
|
||||
|
||||
如果未安装,请根据操作系统安装 MySQL 客户端:
|
||||
- **Windows**: 安装 MySQL Community Server
|
||||
- **Linux (Ubuntu/Debian)**: `sudo apt-get install mysql-client`
|
||||
- **Linux (CentOS/RHEL)**: `sudo yum install mysql`
|
||||
- **macOS**: `brew install mysql-client`
|
||||
|
||||
## 配置步骤
|
||||
|
||||
### 1. 修改导出脚本配置
|
||||
|
||||
编辑 `export_database.sh` 脚本顶部配置:
|
||||
|
||||
```bash
|
||||
# 源数据库配置(开发环境)
|
||||
DB_HOST="116.62.17.81" # 数据库地址
|
||||
DB_PORT="3306" # 数据库端口
|
||||
DB_USER="root" # 数据库用户名
|
||||
DB_PASS="Kfcx@1234" # 数据库密码
|
||||
DB_NAME="ccdi" # 数据库名称
|
||||
```
|
||||
|
||||
### 2. 修改导入脚本配置
|
||||
|
||||
编辑 `import_database.sh` 脚本顶部配置:
|
||||
|
||||
**开发环境:**
|
||||
```bash
|
||||
DEV_DB_HOST="116.62.17.81" # 开发环境数据库地址
|
||||
DEV_DB_PORT="3306" # 数据库端口
|
||||
DEV_DB_USER="root" # 数据库用户名
|
||||
DEV_DB_PASS="Kfcx@1234" # 数据库密码
|
||||
DEV_DB_NAME="ccdi" # 数据库名称
|
||||
```
|
||||
|
||||
**测试环境:**
|
||||
```bash
|
||||
TEST_DB_HOST="your_test_host" # 测试环境数据库地址
|
||||
TEST_DB_PORT="3306" # 数据库端口
|
||||
TEST_DB_USER="your_test_user" # 数据库用户名
|
||||
TEST_DB_PASS="your_test_password" # 数据库密码
|
||||
TEST_DB_NAME="ccdi" # 数据库名称
|
||||
```
|
||||
|
||||
**生产环境:**
|
||||
```bash
|
||||
PROD_DB_HOST="your_prod_host" # 生产环境数据库地址
|
||||
PROD_DB_PORT="3306" # 数据库端口
|
||||
PROD_DB_USER="your_prod_user" # 数据库用户名
|
||||
PROD_DB_PASS="your_prod_password" # 数据库密码
|
||||
PROD_DB_NAME="ccdi" # 数据库名称
|
||||
```
|
||||
|
||||
### 3. 验证配置
|
||||
|
||||
查看配置是否正确:
|
||||
```bash
|
||||
# 查看导出脚本配置
|
||||
head -20 export_database.sh
|
||||
|
||||
# 查看导入脚本配置
|
||||
head -30 import_database.sh
|
||||
```
|
||||
|
||||
## 数据库导出
|
||||
|
||||
### 执行导出
|
||||
|
||||
```bash
|
||||
# 方式1: 使用默认命令
|
||||
./export_database.sh
|
||||
|
||||
# 方式2: 显式指定命令
|
||||
./export_database.sh export
|
||||
```
|
||||
|
||||
### 预期输出
|
||||
|
||||
```
|
||||
[INFO] ========== 开始导出数据库 ==========
|
||||
[INFO] 配置文件加载成功
|
||||
[INFO] mysqldump 命令检查通过
|
||||
[INFO] 开始导出表结构...
|
||||
[INFO] 表结构导出成功: doc/database/backup/ccdi_structure.sql
|
||||
[INFO] 文件大小: 60K
|
||||
[INFO] 开始导出数据...
|
||||
[INFO] 数据导出成功: doc/database/backup/ccdi_data.sql
|
||||
[INFO] 文件大小: 5.7M
|
||||
[INFO] 验证导出文件...
|
||||
[INFO] 导出文件验证通过
|
||||
[INFO] 表结构文件: doc/database/backup/ccdi_structure.sql (60K)
|
||||
[INFO] 数据文件: doc/database/backup/ccdi_data.sql (5.7M)
|
||||
[INFO] ========== 数据库导出完成 ==========
|
||||
[INFO] 使用 ./import_database.sh <env> 导入到目标环境
|
||||
```
|
||||
|
||||
### 验证导出文件
|
||||
|
||||
**1. 检查文件是否存在**
|
||||
```bash
|
||||
ls -lh doc/database/backup/
|
||||
```
|
||||
|
||||
应该看到:
|
||||
- `ccdi_structure.sql` - 表结构文件(~60KB)
|
||||
- `ccdi_data.sql` - 数据文件(~5.7MB)
|
||||
|
||||
**2. 检查字符集声明**
|
||||
```bash
|
||||
head -20 doc/database/backup/ccdi_structure.sql
|
||||
```
|
||||
|
||||
应该包含:
|
||||
```sql
|
||||
SET NAMES utf8mb4;
|
||||
SET CHARACTER SET utf8mb4;
|
||||
```
|
||||
|
||||
**3. 检查文件内容**
|
||||
```bash
|
||||
# 查看表数量
|
||||
grep "CREATE TABLE" doc/database/backup/ccdi_structure.sql | wc -l
|
||||
|
||||
# 查看数据量(INSERT 语句数量)
|
||||
grep "INSERT" doc/database/backup/ccdi_data.sql | wc -l
|
||||
```
|
||||
|
||||
## 数据库导入
|
||||
|
||||
### 准备工作
|
||||
|
||||
**1. 确认目标数据库已创建**
|
||||
|
||||
连接到目标数据库服务器:
|
||||
```bash
|
||||
mysql -h 目标IP -P 3306 -u 用户名 -p
|
||||
```
|
||||
|
||||
创建数据库(如果不存在):
|
||||
```sql
|
||||
CREATE DATABASE ccdi CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
```
|
||||
|
||||
**2. 确认用户权限**
|
||||
|
||||
目标数据库用户需要以下权限:
|
||||
- CREATE、ALTER、DROP(创建和修改表)
|
||||
- INSERT、UPDATE、DELETE(数据操作)
|
||||
- INDEX(创建索引)
|
||||
- REFERENCES(外键约束)
|
||||
|
||||
### 导入到测试环境
|
||||
|
||||
```bash
|
||||
./import_database.sh test
|
||||
```
|
||||
|
||||
### 导入到生产环境
|
||||
|
||||
```bash
|
||||
./import_database.sh production
|
||||
```
|
||||
|
||||
或简写:
|
||||
```bash
|
||||
./import_database.sh prod
|
||||
```
|
||||
|
||||
### 导入到开发环境
|
||||
|
||||
```bash
|
||||
./import_database.sh dev
|
||||
```
|
||||
|
||||
### 预期输出
|
||||
|
||||
```
|
||||
[INFO] ========== 开始导入数据库到 test 环境 ==========
|
||||
[INFO] 配置文件加载成功
|
||||
[INFO] mysql 命令检查通过
|
||||
[INFO] 检查备份文件...
|
||||
[INFO] 备份文件检查通过
|
||||
[INFO] 表结构文件: doc/database/backup/ccdi_structure.sql (60K)
|
||||
[INFO] 数据文件: doc/database/backup/ccdi_data.sql (5.7M)
|
||||
[INFO] 导入表结构到 test 环境: XXX:3306/ccdi
|
||||
[INFO] 表结构导入成功
|
||||
[INFO] 导入数据到 test 环境: XXX:3306/ccdi
|
||||
[INFO] 数据导入成功
|
||||
[INFO] 验证导入结果...
|
||||
[INFO] 目标数据库表数量: 42
|
||||
[INFO] sys_user 表数据行数: XX
|
||||
[INFO] 数据库字符集: utf8mb4
|
||||
[INFO] ========== 数据库导入完成 ==========
|
||||
```
|
||||
|
||||
## 导入后验证
|
||||
|
||||
### 1. 验证表数量
|
||||
|
||||
连接到目标数据库:
|
||||
```bash
|
||||
mysql -h 目标IP -P 3306 -u 用户名 -p ccdi
|
||||
```
|
||||
|
||||
查询表数量:
|
||||
```sql
|
||||
SELECT COUNT(*) FROM information_schema.tables
|
||||
WHERE table_schema='ccdi';
|
||||
```
|
||||
|
||||
对比源数据库和目标数据库的表数量是否一致。
|
||||
|
||||
### 2. 验证数据行数
|
||||
|
||||
查询各表数据行数:
|
||||
```sql
|
||||
SELECT table_name, table_rows
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema='ccdi'
|
||||
ORDER BY table_rows DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
对比源数据库和目标数据库的关键表行数。
|
||||
|
||||
### 3. 验证字符集
|
||||
|
||||
检查数据库字符集:
|
||||
```sql
|
||||
SHOW CREATE DATABASE ccdi;
|
||||
```
|
||||
|
||||
应该显示:`DEFAULT CHARACTER SET utf8mb4`
|
||||
|
||||
检查表字符集:
|
||||
```sql
|
||||
SHOW CREATE TABLE sys_user;
|
||||
```
|
||||
|
||||
应该显示:`ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`
|
||||
|
||||
### 4. 验证中文数据
|
||||
|
||||
查询包含中文的数据:
|
||||
```sql
|
||||
-- 查询用户表
|
||||
SELECT user_name, nick_name FROM sys_user LIMIT 10;
|
||||
|
||||
-- 查询字典数据
|
||||
SELECT dict_label, dict_value FROM sys_dict_data LIMIT 10;
|
||||
|
||||
-- 查询业务表
|
||||
SELECT name, person_type FROM ccdi_biz_intermediary LIMIT 10;
|
||||
```
|
||||
|
||||
确保中文字符显示正常,无乱码。
|
||||
|
||||
### 5. 应用程序连接测试
|
||||
|
||||
修改应用程序配置文件连接到目标数据库,启动应用程序进行功能测试。
|
||||
|
||||
## 完整迁移流程示例
|
||||
|
||||
### 场景:从开发环境迁移到生产环境
|
||||
|
||||
**1. 配置数据库连接**
|
||||
```bash
|
||||
# 编辑导出脚本配置(开发环境)
|
||||
nano export_database.sh
|
||||
# 修改脚本顶部的 DB_HOST, DB_USER, DB_PASS 等配置
|
||||
|
||||
# 编辑导入脚本配置(生产环境)
|
||||
nano import_database.sh
|
||||
# 修改脚本顶部的 PROD_DB_HOST, PROD_DB_USER, PROD_DB_PASS 等配置
|
||||
```
|
||||
|
||||
**2. 导出数据库**
|
||||
```bash
|
||||
./export_database.sh
|
||||
```
|
||||
|
||||
**3. 验证导出文件**
|
||||
```bash
|
||||
ls -lh doc/database/backup/
|
||||
head -20 doc/database/backup/ccdi_structure.sql
|
||||
```
|
||||
|
||||
**4. 先在测试环境验证**
|
||||
```bash
|
||||
# 确保已在 import_database.sh 中配置测试环境
|
||||
./import_database.sh test
|
||||
```
|
||||
|
||||
**5. 验证测试环境**
|
||||
- 连接测试数据库验证数据
|
||||
- 应用程序连接测试环境进行功能测试
|
||||
|
||||
**6. 导入到生产环境**
|
||||
```bash
|
||||
./import_database.sh prod
|
||||
```
|
||||
|
||||
**7. 验证生产环境**
|
||||
- 连接生产数据库验证数据
|
||||
- 应用程序连接生产环境进行功能测试
|
||||
|
||||
**8. 完成迁移**
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. mysqldump: command not found
|
||||
|
||||
**原因**: MySQL 客户端未安装或未添加到 PATH
|
||||
|
||||
**解决**:
|
||||
- 安装 MySQL 客户端工具
|
||||
- 或使用完整路径:`/usr/bin/mysqldump`
|
||||
|
||||
### 2. 数据库连接失败
|
||||
|
||||
**错误信息**: 连接被拒绝或认证失败
|
||||
|
||||
**解决**:
|
||||
- 检查脚本顶部的数据库配置是否正确
|
||||
- 使用 mysql 命令手动测试连接
|
||||
- 检查防火墙规则
|
||||
|
||||
### 3. 导入时字符集乱码
|
||||
|
||||
**原因**: 未正确指定字符集
|
||||
|
||||
**解决**:
|
||||
- 确保导出文件包含字符集声明
|
||||
- 导入命令添加 `--default-character-set=utf8mb4` 参数
|
||||
- 脚本已自动处理,如仍有问题请检查数据库默认字符集
|
||||
|
||||
### 5. 外键约束失败
|
||||
|
||||
**错误信息**: `ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails`
|
||||
|
||||
**解决**:
|
||||
- 脚本已自动添加 `SET FOREIGN_KEY_CHECKS=0;` 和 `SET FOREIGN_KEY_CHECKS=1;`
|
||||
- 如仍有问题,请检查数据完整性
|
||||
|
||||
### 6. 数据包过大
|
||||
|
||||
**错误信息**: `ERROR 1153 (08S01): Got a packet bigger than 'max_allowed_packet' bytes`
|
||||
|
||||
**解决**:
|
||||
- 配置文件中的 `MAX_ALLOWED_PACKET=512M` 已处理此问题
|
||||
- 如数据量特别大,可增大此值
|
||||
|
||||
### 7. 权限不足
|
||||
|
||||
**错误信息**: `ERROR 1044 (42000): Access denied for user`
|
||||
|
||||
**解决**:
|
||||
- 使用具有足够权限的用户(如 root)
|
||||
- 或授予用户必要权限
|
||||
|
||||
### 8. 备份文件不存在
|
||||
|
||||
**错误信息**: `表结构文件不存在: doc/database/backup/ccdi_structure.sql`
|
||||
|
||||
**解决**:
|
||||
- 先执行导出:`./export_database.sh`
|
||||
- 检查 backup 文件夹中是否有 SQL 文件
|
||||
|
||||
## 回滚方案
|
||||
|
||||
如果迁移失败或出现问题:
|
||||
|
||||
1. **保留源数据库**: 不要删除开发环境数据库
|
||||
2. **重新迁移**: 修复问题后重新执行迁移流程
|
||||
3. **从备份恢复**: 如生产环境有备份,可从备份恢复
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **安全性**:
|
||||
- 数据库配置已内置在脚本中,包含敏感信息
|
||||
- 不要将脚本提交到公开的版本控制系统
|
||||
- 迁移完成后建议删除脚本中的密码或使用占位符
|
||||
|
||||
2. **性能**:
|
||||
- 大数据库导出/导入可能需要较长时间
|
||||
- 建议在低峰期执行迁移
|
||||
- 确保有足够的磁盘空间
|
||||
|
||||
3. **数据一致性**:
|
||||
- 导出期间源数据库应避免写入操作
|
||||
- 或使用 `--single-transaction` 参数(已包含)
|
||||
|
||||
4. **字符集**:
|
||||
- 确保所有步骤都使用 utf8mb4 字符集
|
||||
- 验证阶段重点检查中文数据
|
||||
- 表结构文件不再包含显式的 COLLATE 配置(使用默认 utf8mb4_general_ci)
|
||||
|
||||
5. **脚本配置**:
|
||||
- 首次使用前必须在脚本顶部配置数据库信息
|
||||
- 三个环境的配置是独立的,可以只配置需要的环境
|
||||
- 修改配置后无需其他操作即可使用
|
||||
|
||||
## 技术支持
|
||||
|
||||
如遇到问题:
|
||||
1. 检查本文档的常见问题部分
|
||||
2. 查看脚本执行的错误信息
|
||||
3. 检查数据库连接和权限
|
||||
4. 查看数据库日志
|
||||
|
||||
## 相关文件
|
||||
|
||||
- 导出脚本: `export_database.sh`(配置已内置)
|
||||
- 导入脚本: `import_database.sh`(配置已内置)
|
||||
- 表结构文件: `doc/database/backup/ccdi_structure.sql`
|
||||
- 数据文件: `doc/database/backup/ccdi_data.sql`
|
||||
- 排序规则修改脚本: `doc/database/alter_collation_to_general_ci.sql`
|
||||
- 设计文档: `docs/plans/2026-02-28-database-migration-design.md`
|
||||
705
doc/implementation/lsfx-code-review-20260302.md
Normal file
705
doc/implementation/lsfx-code-review-20260302.md
Normal file
@@ -0,0 +1,705 @@
|
||||
# 流水分析对接代码审查报告
|
||||
|
||||
**审查日期:** 2026-03-02
|
||||
**审查范围:** ccdi-lsfx 模块
|
||||
**参考文档:** `doc/对接流水分析/兰溪-流水分析对接-新版.md`
|
||||
|
||||
---
|
||||
|
||||
## 📊 审查总结
|
||||
|
||||
### 整体评估
|
||||
|
||||
| 项目 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| 接口覆盖率 | 85.7% | 6/7个接口已实现 |
|
||||
| 字段完整性 | 100% | 已实现的接口字段完整 |
|
||||
| 代码规范 | ✅ 优秀 | 符合项目规范 |
|
||||
| 错误处理 | ❌ 缺失 | 需要改进 |
|
||||
| 日志记录 | ❌ 缺失 | 需要改进 |
|
||||
| 参数校验 | ⚠️ 部分 | 需要加强 |
|
||||
|
||||
### 关键发现
|
||||
|
||||
**✅ 做得好的地方:**
|
||||
1. DTO类设计完整,字段与文档完全匹配
|
||||
2. 使用Lombok简化代码
|
||||
3. 配置外部化,便于环境切换
|
||||
4. Swagger文档完整
|
||||
5. 代码结构清晰,模块化良好
|
||||
|
||||
**❌ 需要改进的地方:**
|
||||
1. **接口5未实现** - 删除主体功能缺失
|
||||
2. **缺少异常处理** - 可能导致运行时崩溃
|
||||
3. **缺少日志记录** - 难以排查问题
|
||||
4. **配置值未更新** - app-secret使用占位符
|
||||
|
||||
---
|
||||
|
||||
## 📋 接口审查详情
|
||||
|
||||
### 接口1:获取Token ✅
|
||||
|
||||
**文档路径:** `/account/common/getToken`
|
||||
|
||||
**实现位置:**
|
||||
- Request: `GetTokenRequest.java`
|
||||
- Response: `GetTokenResponse.java`
|
||||
- Client: `LsfxAnalysisClient.getToken()`
|
||||
- Controller: `LsfxTestController.getToken()`
|
||||
|
||||
**字段对比:**
|
||||
|
||||
| 文档字段 | 代码字段 | 必填 | 状态 |
|
||||
|---------|---------|------|------|
|
||||
| projectNo | ✅ projectNo | 是 | ✅ 匹配 |
|
||||
| entityName | ✅ entityName | 是 | ✅ 匹配 |
|
||||
| userId | ✅ userId | 是 | ✅ 匹配 |
|
||||
| userName | ✅ userName | 是 | ✅ 匹配 |
|
||||
| appId | ✅ appId | 是 | ✅ 匹配 |
|
||||
| appSecretCode | ✅ appSecretCode | 是 | ✅ 匹配 |
|
||||
| role | ✅ role | 是 | ✅ 匹配 |
|
||||
| orgCode | ✅ orgCode | 是 | ✅ 匹配 |
|
||||
| entityId | ✅ entityId | 否 | ✅ 匹配 |
|
||||
| xdRelatedPersons | ✅ xdRelatedPersons | 否 | ✅ 匹配 |
|
||||
| jzDataDateId | ✅ jzDataDateId | 否 | ✅ 匹配 |
|
||||
| innerBSStartDateId | ✅ innerBSStartDateId | 否 | ✅ 匹配 |
|
||||
| innerBSEndDateId | ✅ innerBSEndDateId | 否 | ✅ 匹配 |
|
||||
| analysisType | ✅ analysisType | 是 | ✅ 匹配 |
|
||||
| departmentCode | ✅ departmentCode | 是 | ✅ 匹配 |
|
||||
|
||||
**实现验证:**
|
||||
- ✅ MD5安全码生成正确(`MD5Util.generateSecretCode()`)
|
||||
- ✅ 默认值设置正确(analysisType="-1", role="VIEWER")
|
||||
- ⚠️ 配置文件中 `app-secret: your_app_secret_here` 需要替换为 `dXj6eHRmPv`
|
||||
|
||||
**问题:**
|
||||
```yaml
|
||||
# application-dev.yml:115
|
||||
app-secret: your_app_secret_here # ❌ 占位符,需要替换
|
||||
# 应该改为:
|
||||
app-secret: dXj6eHRmPv # ✅ 正确的密钥
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 接口2:上传文件 ✅
|
||||
|
||||
**文档路径:** `/watson/api/project/remoteUploadSplitFile`
|
||||
|
||||
**实现位置:**
|
||||
- Request: 参数直接传递(groupId, files)
|
||||
- Response: `UploadFileResponse.java`
|
||||
- Client: `LsfxAnalysisClient.uploadFile()`
|
||||
- Controller: `LsfxTestController.uploadFile()`
|
||||
|
||||
**字段对比:**
|
||||
|
||||
| 文档字段 | 代码字段 | 必填 | 状态 |
|
||||
|---------|---------|------|------|
|
||||
| groupId | ✅ groupId | 是 | ✅ 匹配 |
|
||||
| files | ✅ files | 是 | ✅ 匹配 |
|
||||
|
||||
**Header验证:**
|
||||
- ✅ X-Xencio-Client-Id 已设置
|
||||
|
||||
**Response字段对比:**
|
||||
|
||||
| 文档字段 | 代码字段 | 状态 |
|
||||
|---------|---------|------|
|
||||
| code | ✅ code | ✅ 匹配 |
|
||||
| data | ✅ data | ✅ 匹配 |
|
||||
| data.accountsOfLog | ✅ accountsOfLog | ✅ 匹配 |
|
||||
| data.uploadLogList | ✅ uploadLogList | ✅ 匹配 |
|
||||
| data.uploadStatus | ✅ uploadStatus | ✅ 匹配 |
|
||||
|
||||
**UploadLogItem字段 (27个):**
|
||||
- ✅ 所有字段完整匹配文档2.5节
|
||||
- ✅ 包含关键字段:logId, status, uploadStatusDesc
|
||||
|
||||
**状态码验证:**
|
||||
- ✅ 成功状态:status = -5, uploadStatusDesc = "data.wait.confirm.newaccount"
|
||||
|
||||
---
|
||||
|
||||
### 接口3:拉取行内流水 ✅
|
||||
|
||||
**文档路径:** `/watson/api/project/getJZFileOrZjrcuFile`
|
||||
|
||||
**实现位置:**
|
||||
- Request: `FetchInnerFlowRequest.java`
|
||||
- Response: `FetchInnerFlowResponse.java`
|
||||
- Client: `LsfxAnalysisClient.fetchInnerFlow()`
|
||||
- Controller: `LsfxTestController.fetchInnerFlow()`
|
||||
|
||||
**字段对比:**
|
||||
|
||||
| 文档字段 | 代码字段 | 必填 | 状态 |
|
||||
|---------|---------|------|------|
|
||||
| groupId | ✅ groupId | 是 | ✅ 匹配 |
|
||||
| customerNo | ✅ customerNo | 是 | ✅ 匹配 |
|
||||
| dataChannelCode | ✅ dataChannelCode | 是 | ✅ 匹配 |
|
||||
| requestDateId | ✅ requestDateId | 是 | ✅ 匹配 |
|
||||
| dataStartDateId | ✅ dataStartDateId | 是 | ✅ 匹配 |
|
||||
| dataEndDateId | ✅ dataEndDateId | 是 | ✅ 匹配 |
|
||||
| uploadUserId | ✅ uploadUserId | 是 | ✅ 匹配 |
|
||||
|
||||
**Header验证:**
|
||||
- ✅ X-Xencio-Client-Id 已设置
|
||||
|
||||
**Response字段对比:**
|
||||
- ✅ data.code (如:"501014" 表示无行内流水)
|
||||
- ✅ data.message (如:"无行内流水文件")
|
||||
|
||||
---
|
||||
|
||||
### 接口4:检查文件解析状态 ✅
|
||||
|
||||
**文档路径:** `/watson/api/project/upload/getpendings`
|
||||
|
||||
**实现位置:**
|
||||
- Request: 参数直接传递(groupId, inprogressList)
|
||||
- Response: `CheckParseStatusResponse.java`
|
||||
- Client: `LsfxAnalysisClient.checkParseStatus()`
|
||||
- Controller: `LsfxTestController.checkParseStatus()`
|
||||
|
||||
**字段对比:**
|
||||
|
||||
| 文档字段 | 代码字段 | 必填 | 状态 |
|
||||
|---------|---------|------|------|
|
||||
| groupId | ✅ groupId | 是 | ✅ 匹配 |
|
||||
| inprogressList | ✅ inprogressList | 是 | ✅ 匹配 |
|
||||
|
||||
**Header验证:**
|
||||
- ✅ X-Xencio-Client-Id 已设置(值:c2017e8d105c435a96f86373635b6a09)
|
||||
|
||||
**Response关键字段:**
|
||||
- ✅ **parsing** (Boolean) - 核心字段,true=解析中,false=解析结束
|
||||
- ✅ **pendingList** - 包含完整的文件信息
|
||||
|
||||
**PendingItem字段 (26个):**
|
||||
- ✅ 所有字段完整匹配文档4.5节
|
||||
- ✅ 包含关键字段:logId, status, parsing, uploadStatusDesc
|
||||
- ✅ 成功状态:status = -5, uploadStatusDesc = "data.wait.confirm.newaccount"
|
||||
|
||||
---
|
||||
|
||||
### 接口5:删除主体 ❌
|
||||
|
||||
**文档路径:** `/watson/api/project/batchDeleteUploadFile`
|
||||
|
||||
**状态:** **❌ 未实现**
|
||||
|
||||
**文档要求:**
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| groupId | Int | 是 | 项目ID |
|
||||
| logIds | Array | 是 | 文件ID数组 |
|
||||
| userId | int | 是 | 用户柜员号 |
|
||||
|
||||
**预期Response:**
|
||||
```json
|
||||
{
|
||||
"code": "200 OK",
|
||||
"data": {
|
||||
"message": "delete.files.success"
|
||||
},
|
||||
"status": "200",
|
||||
"successResponse": true
|
||||
}
|
||||
```
|
||||
|
||||
**影响:**
|
||||
- 流水文件解析失败后无法删除重新上传
|
||||
- 可能导致项目下积累无效的失败文件
|
||||
|
||||
**建议实现:**
|
||||
1. 创建 `DeleteUploadFileRequest.java`
|
||||
2. 创建 `DeleteUploadFileResponse.java`
|
||||
3. 在 `LsfxAnalysisClient` 中添加 `deleteUploadFile()` 方法
|
||||
4. 在 `LsfxTestController` 中添加测试接口
|
||||
|
||||
---
|
||||
|
||||
### 接口6:生成报告 ✅
|
||||
|
||||
**状态:** ✅ 已按计划删除
|
||||
|
||||
**说明:**
|
||||
- 旧版接口,新版文档中不再需要
|
||||
- 已从代码中完全移除(Request/Response/Client/Controller)
|
||||
|
||||
---
|
||||
|
||||
### 接口7:获取银行流水列表 ✅
|
||||
|
||||
**文档路径:** `/watson/api/project/getBSByLogId` (新路径)
|
||||
|
||||
**实现位置:**
|
||||
- Request: `GetBankStatementRequest.java`
|
||||
- Response: `GetBankStatementResponse.java`
|
||||
- Client: `LsfxAnalysisClient.getBankStatement()`
|
||||
- Controller: `LsfxTestController.getBankStatement()`
|
||||
|
||||
**字段对比:**
|
||||
|
||||
| 文档字段 | 代码字段 | 必填 | 状态 |
|
||||
|---------|---------|------|------|
|
||||
| groupId | ✅ groupId | 是 | ✅ 匹配 |
|
||||
| logId | ✅ logId | 是 | ✅ 匹配 |
|
||||
| pageNow | ✅ pageNow | 是 | ✅ 匹配 |
|
||||
| pageSize | ✅ pageSize | 是 | ✅ 匹配 |
|
||||
|
||||
**Header验证:**
|
||||
- ✅ X-Xencio-Client-Id 已设置
|
||||
|
||||
**Response字段:**
|
||||
- ✅ **bankStatementList** - 流水列表
|
||||
- ✅ **totalCount** - 总条数
|
||||
|
||||
**BankStatementItem字段 (40+个字段):**
|
||||
- ✅ 所有字段完整匹配文档6.5节
|
||||
- ✅ 包含关键信息:
|
||||
- 账号信息:accountMaskNo, leName, accountingDate
|
||||
- 交易金额:drAmount, crAmount, balanceAmount
|
||||
- 对手方信息:customerName, customerAccountMaskNo
|
||||
- 交易信息:trxDate, cashType, transFlag
|
||||
|
||||
**参数校验:**
|
||||
- ✅ Controller中有完整的参数校验
|
||||
```java
|
||||
if (request.getGroupId() == null) {
|
||||
return AjaxResult.error("参数不完整:groupId为必填");
|
||||
}
|
||||
if (request.getLogId() == null) {
|
||||
return AjaxResult.error("参数不完整:logId为必填(文件ID)");
|
||||
}
|
||||
if (request.getPageNow() == null || request.getPageNow() < 1) {
|
||||
return AjaxResult.error("参数不完整:pageNow为必填且大于0");
|
||||
}
|
||||
if (request.getPageSize() == null || request.getPageSize() < 1) {
|
||||
return AjaxResult.error("参数不完整:pageSize为必填且大于0");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 代码质量审查
|
||||
|
||||
### 1. 错误处理 ❌
|
||||
|
||||
**问题:** 整个模块缺少异常处理机制
|
||||
|
||||
**当前代码:**
|
||||
```java
|
||||
// HttpUtil.java
|
||||
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(); // ❌ 可能为null,无异常处理
|
||||
}
|
||||
```
|
||||
|
||||
**风险:**
|
||||
1. 网络异常会直接抛给上层
|
||||
2. API返回错误码无法统一处理
|
||||
3. response.getBody()可能返回null导致NPE
|
||||
|
||||
**建议改进:**
|
||||
```java
|
||||
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调用失败: " + response.getStatusCode());
|
||||
}
|
||||
|
||||
T body = response.getBody();
|
||||
if (body == null) {
|
||||
throw new LsfxApiException("API返回数据为空");
|
||||
}
|
||||
|
||||
return body;
|
||||
} catch (RestClientException e) {
|
||||
throw new LsfxApiException("网络请求失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 日志记录 ❌
|
||||
|
||||
**问题:** 整个模块没有任何日志记录
|
||||
|
||||
**影响:**
|
||||
- 无法追踪API调用情况
|
||||
- 无法排查生产环境问题
|
||||
- 无法监控性能
|
||||
|
||||
**建议添加日志:**
|
||||
|
||||
**LsfxAnalysisClient.java:**
|
||||
```java
|
||||
@Slf4j
|
||||
@Component
|
||||
public class LsfxAnalysisClient {
|
||||
|
||||
public GetTokenResponse getToken(GetTokenRequest request) {
|
||||
log.info("获取Token请求: projectNo={}, entityName={}", request.getProjectNo(), request.getEntityName());
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
// ... 现有代码 ...
|
||||
GetTokenResponse response = httpUtil.postJson(url, request, null, GetTokenResponse.class);
|
||||
|
||||
long elapsed = System.currentTimeMillis() - startTime;
|
||||
log.info("获取Token成功: projectId={}, 耗时={}ms", response.getData().getProjectId(), elapsed);
|
||||
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
log.error("获取Token失败: projectNo={}, error={}", request.getProjectNo(), e.getMessage(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 参数校验 ⚠️
|
||||
|
||||
**问题:** 只有接口7有参数校验,其他接口缺少校验
|
||||
|
||||
**已有校验(接口7):**
|
||||
- ✅ groupId非空校验
|
||||
- ✅ logId非空校验
|
||||
- ✅ pageNow范围校验
|
||||
- ✅ pageSize范围校验
|
||||
|
||||
**缺少校验的接口:**
|
||||
- ❌ 接口1(获取Token):projectNo格式校验
|
||||
- ❌ 接口2(上传文件):文件大小、格式校验
|
||||
- ❌ 接口3(拉取行内流水):日期范围校验
|
||||
- ❌ 接口4(检查解析状态):inprogressList格式校验
|
||||
|
||||
**建议添加校验:**
|
||||
|
||||
**接口1示例:**
|
||||
```java
|
||||
@PostMapping("/getToken")
|
||||
public AjaxResult getToken(@RequestBody GetTokenRequest request) {
|
||||
// 参数校验
|
||||
if (StringUtils.isBlank(request.getProjectNo())) {
|
||||
return AjaxResult.error("参数不完整:projectNo为必填");
|
||||
}
|
||||
if (!request.getProjectNo().matches("^902000_\\d+$")) {
|
||||
return AjaxResult.error("参数格式错误:projectNo格式应为902000_当前时间戳");
|
||||
}
|
||||
if (StringUtils.isBlank(request.getEntityName())) {
|
||||
return AjaxResult.error("参数不完整:entityName为必填");
|
||||
}
|
||||
// ... 其他字段校验 ...
|
||||
|
||||
GetTokenResponse response = lsfxAnalysisClient.getToken(request);
|
||||
return AjaxResult.success(response);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 性能优化 ⚠️
|
||||
|
||||
**问题:** RestTemplate未使用连接池
|
||||
|
||||
**当前配置:**
|
||||
```java
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
factory.setConnectTimeout(connectionTimeout);
|
||||
factory.setReadTimeout(readTimeout);
|
||||
return new RestTemplate(factory); // ❌ 每次请求可能创建新连接
|
||||
}
|
||||
```
|
||||
|
||||
**建议改进(使用连接池):**
|
||||
```java
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
PoolingHttpClientConnectionManager connectionManager =
|
||||
new PoolingHttpClientConnectionManager();
|
||||
connectionManager.setMaxTotal(100); // 最大连接数
|
||||
connectionManager.setDefaultMaxPerRoute(20); // 每个路由最大连接数
|
||||
|
||||
CloseableHttpClient httpClient = HttpClientBuilder.create()
|
||||
.setConnectionManager(connectionManager)
|
||||
.build();
|
||||
|
||||
HttpComponentsClientHttpRequestFactory factory =
|
||||
new HttpComponentsClientHttpRequestFactory(httpClient);
|
||||
factory.setConnectTimeout(connectionTimeout);
|
||||
factory.setReadTimeout(readTimeout);
|
||||
|
||||
return new RestTemplate(factory);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. 配置管理 ⚠️
|
||||
|
||||
**问题:** app-secret使用占位符
|
||||
|
||||
**当前配置:**
|
||||
```yaml
|
||||
lsfx:
|
||||
api:
|
||||
app-secret: your_app_secret_here # ❌ 占位符
|
||||
```
|
||||
|
||||
**正确配置:**
|
||||
```yaml
|
||||
lsfx:
|
||||
api:
|
||||
app-secret: dXj6eHRmPv # ✅ 正确的密钥(来自文档)
|
||||
```
|
||||
|
||||
**建议:**
|
||||
1. 立即更新配置文件
|
||||
2. 使用配置中心或环境变量管理敏感信息
|
||||
3. 添加配置验证
|
||||
|
||||
---
|
||||
|
||||
### 6. 代码规范 ✅
|
||||
|
||||
**符合规范:**
|
||||
- ✅ 使用 `@Data` 注解简化代码
|
||||
- ✅ 使用 `@Resource` 注入依赖
|
||||
- ✅ 实体类不继承 BaseEntity
|
||||
- ✅ 使用 MyBatis Plus(虽然此模块无数据库操作)
|
||||
- ✅ Swagger 文档完整
|
||||
- ✅ 注释清晰
|
||||
|
||||
---
|
||||
|
||||
## 📝 代码规范符合性检查
|
||||
|
||||
### Java代码风格 ✅
|
||||
|
||||
| 规范项 | 状态 | 说明 |
|
||||
|--------|------|------|
|
||||
| 使用@Data注解 | ✅ | 所有DTO类使用Lombok |
|
||||
| 使用@Resource | ✅ | 依赖注入使用@Resource |
|
||||
| 禁止全限定类名 | ✅ | 所有类都使用import |
|
||||
| 禁止extends ServiceImpl | ✅ | 无ServiceImpl继承 |
|
||||
| DTO/VO分离 | ✅ | Request/Response独立 |
|
||||
| 审计字段 | N/A | 此模块无数据库操作 |
|
||||
|
||||
---
|
||||
|
||||
## 🐛 发现的Bug
|
||||
|
||||
### Bug 1: 响应体可能为null
|
||||
|
||||
**位置:** `HttpUtil.java:52`
|
||||
|
||||
**问题:**
|
||||
```java
|
||||
ResponseEntity<T> response = restTemplate.postForEntity(url, requestEntity, responseType);
|
||||
return response.getBody(); // ❌ 可能为null
|
||||
```
|
||||
|
||||
**影响:** NullPointerException
|
||||
|
||||
**修复方案:**
|
||||
```java
|
||||
T body = response.getBody();
|
||||
if (body == null) {
|
||||
throw new LsfxApiException("API响应体为空");
|
||||
}
|
||||
return body;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Bug 2: 异常类未使用
|
||||
|
||||
**位置:** `LsfxApiException.java`
|
||||
|
||||
**问题:** 定义了自定义异常类,但从未在代码中使用
|
||||
|
||||
**建议:**
|
||||
- 要么使用它进行异常处理
|
||||
- 要么删除这个类
|
||||
|
||||
---
|
||||
|
||||
## 📊 测试建议
|
||||
|
||||
### 单元测试
|
||||
|
||||
**建议为以下类添加单元测试:**
|
||||
1. `MD5Util` - 测试MD5加密
|
||||
2. `LsfxAnalysisClient` - Mock RestTemplate测试各接口
|
||||
3. `HttpUtil` - 测试HTTP工具方法
|
||||
|
||||
**示例测试:**
|
||||
```java
|
||||
@Test
|
||||
public void testGenerateSecretCode() {
|
||||
String projectNo = "902000_123456";
|
||||
String entityName = "测试项目";
|
||||
String appSecret = "dXj6eHRmPv";
|
||||
|
||||
String secretCode = MD5Util.generateSecretCode(projectNo, entityName, appSecret);
|
||||
|
||||
assertNotNull(secretCode);
|
||||
assertEquals(32, secretCode.length()); // MD5长度为32
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 集成测试
|
||||
|
||||
**建议测试场景:**
|
||||
1. 完整流程测试:getToken → uploadFile → checkParseStatus → getBankStatement
|
||||
2. 异常场景测试:网络超时、API返回错误码
|
||||
3. 并发测试:多线程调用API
|
||||
|
||||
---
|
||||
|
||||
## 🔒 安全性审查
|
||||
|
||||
### 安全问题
|
||||
|
||||
| 项目 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| 密钥管理 | ⚠️ | app-secret硬编码在配置文件中 |
|
||||
| MD5加密 | ⚠️ | MD5已不安全,但这是接口要求 |
|
||||
| HTTPS | ✅ | 生产环境使用HTTPS |
|
||||
| 输入验证 | ⚠️ | 缺少完整的参数校验 |
|
||||
|
||||
---
|
||||
|
||||
## 📈 性能评估
|
||||
|
||||
### 当前性能瓶颈
|
||||
|
||||
1. **无连接池** - 每次请求可能创建新连接
|
||||
2. **无缓存** - Token未缓存,每次都重新获取
|
||||
3. **无异步处理** - 所有操作都是同步的
|
||||
|
||||
### 优化建议
|
||||
|
||||
1. **添加连接池** - 使用Apache HttpClient连接池
|
||||
2. **Token缓存** - Token一次获取后可缓存30分钟
|
||||
3. **批量操作** - 对于大量流水数据,支持批量获取
|
||||
|
||||
---
|
||||
|
||||
## ✅ 行动计划
|
||||
|
||||
### 高优先级(立即修复)
|
||||
|
||||
| 任务 | 文件 | 预计时间 |
|
||||
|------|------|----------|
|
||||
| 修复app-secret配置 | application-dev.yml | 5分钟 |
|
||||
| 实现接口5(删除主体) | 新增3个文件 | 1小时 |
|
||||
| 添加异常处理 | HttpUtil.java, Client | 2小时 |
|
||||
| 添加日志记录 | 所有类 | 2小时 |
|
||||
|
||||
### 中优先级(本周完成)
|
||||
|
||||
| 任务 | 文件 | 预计时间 |
|
||||
|------|------|----------|
|
||||
| 添加参数校验 | Controller | 2小时 |
|
||||
| 添加连接池 | RestTemplateConfig.java | 1小时 |
|
||||
| 添加单元测试 | test/ | 3小时 |
|
||||
|
||||
### 低优先级(后续优化)
|
||||
|
||||
| 任务 | 文件 | 预计时间 |
|
||||
|------|------|----------|
|
||||
| Token缓存 | Client | 1小时 |
|
||||
| 性能优化 | - | 2小时 |
|
||||
| 文档完善 | - | 1小时 |
|
||||
|
||||
---
|
||||
|
||||
## 📋 检查清单
|
||||
|
||||
### 功能完整性
|
||||
- ✅ 接口1:获取Token
|
||||
- ✅ 接口2:上传文件
|
||||
- ✅ 接口3:拉取行内流水
|
||||
- ✅ 接口4:检查解析状态
|
||||
- ❌ 接口5:删除主体(**未实现**)
|
||||
- ✅ 接口7:获取流水列表
|
||||
|
||||
### 代码质量
|
||||
- ✅ 代码结构清晰
|
||||
- ✅ 命名规范
|
||||
- ✅ 注释完整
|
||||
- ❌ 异常处理缺失
|
||||
- ❌ 日志记录缺失
|
||||
- ⚠️ 参数校验不完整
|
||||
|
||||
### 测试覆盖
|
||||
- ❌ 无单元测试
|
||||
- ❌ 无集成测试
|
||||
- ❌ 无性能测试
|
||||
|
||||
---
|
||||
|
||||
## 🎯 总结
|
||||
|
||||
### 优点
|
||||
1. ✅ **架构设计良好** - 模块化、分层清晰
|
||||
2. ✅ **字段映射准确** - DTO与文档完全匹配
|
||||
3. ✅ **代码规范** - 符合项目编码规范
|
||||
4. ✅ **配置灵活** - 支持多环境配置
|
||||
|
||||
### 缺点
|
||||
1. ❌ **接口5未实现** - 功能不完整
|
||||
2. ❌ **缺少异常处理** - 稳定性风险
|
||||
3. ❌ **缺少日志记录** - 可维护性差
|
||||
4. ⚠️ **配置值未更新** - 可能导致调用失败
|
||||
|
||||
### 风险评估
|
||||
|
||||
| 风险 | 等级 | 说明 |
|
||||
|------|------|------|
|
||||
| 接口调用失败 | 🔴 高 | app-secret配置错误 |
|
||||
| 运行时异常 | 🟡 中 | 缺少异常处理 |
|
||||
| 性能问题 | 🟡 中 | 无连接池 |
|
||||
| 功能缺失 | 🟡 中 | 接口5未实现 |
|
||||
| 难以排查问题 | 🟡 中 | 缺少日志 |
|
||||
|
||||
### 建议
|
||||
|
||||
**立即行动:**
|
||||
1. 修复 `app-secret` 配置
|
||||
2. 实现接口5(删除主体)
|
||||
3. 添加异常处理和日志
|
||||
|
||||
**后续优化:**
|
||||
1. 添加单元测试
|
||||
2. 优化性能(连接池、缓存)
|
||||
3. 完善参数校验
|
||||
|
||||
---
|
||||
|
||||
**审查人:** Claude Code
|
||||
**审查状态:** ✅ 完成
|
||||
**下一步:** 根据行动计划修复问题
|
||||
255
doc/implementation/lsfx-update-report-20260302.md
Normal file
255
doc/implementation/lsfx-update-report-20260302.md
Normal file
@@ -0,0 +1,255 @@
|
||||
# 流水分析接口更新实施报告
|
||||
|
||||
## 实施日期
|
||||
2026-03-02
|
||||
|
||||
## 更新内容概览
|
||||
|
||||
### 删除的接口
|
||||
- **接口5**: 生成尽调报告 (`/watson/api/project/confirmStageUploadLogs`)
|
||||
- 删除 DTO: `GenerateReportRequest.java`, `GenerateReportResponse.java`
|
||||
|
||||
- **接口6**: 检查报告生成状态 (`/watson/api/project/upload/getallpendings`)
|
||||
- 删除 DTO: `CheckReportStatusResponse.java`
|
||||
|
||||
### 重构的接口
|
||||
- **接口2**: 上传文件 Response
|
||||
- 新增字段: `accountsOfLog` (账号映射信息)
|
||||
- 新增字段: `uploadLogList` (上传日志列表,含30+字段)
|
||||
- 新增内部类: `AccountInfo`, `UploadLogItem`
|
||||
|
||||
- **接口3**: 拉取行内流水 Request/Response
|
||||
- 修正参数名: `customerNo`, `dataChannelCode`, `requestDateId` 等
|
||||
- 重构 Response: 简化为 `code` 和 `message` 字段
|
||||
|
||||
- **接口4**: 检查解析状态 Response
|
||||
- 新增关键字段: `parsing` (是否正在解析)
|
||||
- 完善字段: `pendingList` (待处理文件列表,含30+字段)
|
||||
|
||||
- **接口7**: 获取银行流水 Request/Response
|
||||
- 更新路径: `/watson/api/project/getBSByLogId`
|
||||
- 新增参数: `logId` (文件ID,必填)
|
||||
- 参数重命名: `pageNum` → `pageNow`
|
||||
- 完整字段: `BankStatementItem` 包含40+个字段
|
||||
|
||||
### 保留的接口
|
||||
- **接口1**: 获取Token - 无需修改
|
||||
|
||||
---
|
||||
|
||||
## 修改的文件统计
|
||||
|
||||
### 配置文件 (1个)
|
||||
- `ruoyi-admin/src/main/resources/application-dev.yml`
|
||||
- 删除 `generate-report`, `check-report-status` 配置项
|
||||
- 更新 `get-bank-statement` 路径
|
||||
|
||||
### DTO类文件 (9个)
|
||||
|
||||
#### 删除的文件 (3个)
|
||||
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/request/GenerateReportRequest.java`
|
||||
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/GenerateReportResponse.java`
|
||||
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/CheckReportStatusResponse.java`
|
||||
|
||||
#### 重构的文件 (6个)
|
||||
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/request/FetchInnerFlowRequest.java`
|
||||
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/FetchInnerFlowResponse.java`
|
||||
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/UploadFileResponse.java`
|
||||
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/CheckParseStatusResponse.java`
|
||||
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/request/GetBankStatementRequest.java`
|
||||
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/GetBankStatementResponse.java`
|
||||
|
||||
### 业务逻辑文件 (2个)
|
||||
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/LsfxAnalysisClient.java`
|
||||
- 删除 `generateReport()`, `checkReportStatus()` 方法
|
||||
- 更新 `getBankStatement()` 方法注释
|
||||
|
||||
- `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/controller/LsfxTestController.java`
|
||||
- 删除接口5、6的测试方法
|
||||
- 更新接口7的Swagger注释和参数验证
|
||||
|
||||
**总计**: 12个文件
|
||||
|
||||
---
|
||||
|
||||
## Git 提交记录
|
||||
|
||||
```
|
||||
72bab28 refactor(lsfx): Controller删除接口5、6测试接口,更新接口7参数验证
|
||||
ac4ebd1 refactor(lsfx): Client删除接口5、6方法,更新接口7注释
|
||||
b2471c3 refactor(lsfx): 重构接口7 Request/Response,新路径、新参数、完整字段
|
||||
fe7f7ea refactor(lsfx): 重构接口4 Response,添加parsing字段和完整pendingList
|
||||
731f078 refactor(lsfx): 重构接口3 Request/Response,修正参数名和字段结构
|
||||
b89584a refactor(lsfx): 重构接口2 Response,添加完整字段(accountsOfLog、uploadLogList)
|
||||
c272ee7 refactor(lsfx): 删除接口5(生成报告)和接口6(检查报告状态)的DTO类
|
||||
d122e52 config(lsfx): 删除接口5、6配置,更新接口7路径
|
||||
```
|
||||
|
||||
**提交次数**: 8次
|
||||
**提交信息规范**: 符合 Conventional Commits 规范
|
||||
|
||||
---
|
||||
|
||||
## 编译验证结果
|
||||
|
||||
### 编译状态
|
||||
```
|
||||
[INFO] BUILD SUCCESS
|
||||
[INFO] Total time: 15.950 s
|
||||
[INFO] Finished at: 2026-03-02T22:10:37+08:00
|
||||
```
|
||||
|
||||
**结果**: ✅ 编译成功,无错误
|
||||
|
||||
### 编译的模块
|
||||
- ruoyi-common ✅
|
||||
- ruoyi-system ✅
|
||||
- ruoyi-framework ✅
|
||||
- ruoyi-quartz ✅
|
||||
- ruoyi-generator ✅
|
||||
- ccdi-info-collection ✅
|
||||
- ccdi-project ✅
|
||||
- **ccdi-lsfx** ✅ (本次更新核心模块)
|
||||
- ruoyi-admin ✅
|
||||
|
||||
---
|
||||
|
||||
## 验收检查清单
|
||||
|
||||
### 功能验收
|
||||
- ✅ 项目编译无错误
|
||||
- ✅ 无残留的import语句
|
||||
- ✅ DTO类使用 `@Data` 注解
|
||||
- ✅ 字段类型正确 (Integer, String, BigDecimal等)
|
||||
- ✅ 配置文件已更新
|
||||
|
||||
### 代码验收
|
||||
- ✅ 接口5、6相关代码已完全删除
|
||||
- ✅ 接口2、3、4、7的Response字段完整
|
||||
- ✅ 接口7使用新路径 `/watson/api/project/getBSByLogId`
|
||||
- ✅ 接口7参数包含 `logId`, `pageNow`, `pageSize`
|
||||
- ✅ Client方法注释清晰
|
||||
- ✅ Controller参数验证完整
|
||||
|
||||
### 提交信息验收
|
||||
- ✅ 提交信息格式规范
|
||||
- ✅ 每个功能点独立提交
|
||||
- ✅ 提交信息清晰描述变更内容
|
||||
|
||||
---
|
||||
|
||||
## 接口字段对比表
|
||||
|
||||
### 接口2: 上传文件 Response
|
||||
|
||||
| 新增字段 | 类型 | 说明 |
|
||||
|---------|------|------|
|
||||
| `data.accountsOfLog` | Map<String, List<AccountInfo>> | 账号映射信息(key为logId) |
|
||||
| `data.uploadLogList` | List<UploadLogItem> | 上传日志列表 |
|
||||
|
||||
**UploadLogItem 新增关键字段**:
|
||||
- `logId` (文件ID,重要)
|
||||
- `status` (状态,-5表示成功)
|
||||
- `uploadStatusDesc` (状态描述)
|
||||
- `totalRecords` (总记录数)
|
||||
- `trxDateStartId`, `trxDateEndId` (交易日期范围)
|
||||
|
||||
### 接口3: 拉取行内流水 Request
|
||||
|
||||
| 旧参数名 | 新参数名 | 类型 | 说明 |
|
||||
|---------|---------|------|------|
|
||||
| `dataChannel` | `dataChannelCode` | String | 数据渠道编码(固定值:ZJRCU) |
|
||||
| `jzDataDateId` | `requestDateId` | Integer | 发起请求的时间(格式:yyyyMMdd) |
|
||||
| `innerBSStartDateId` | `dataStartDateId` | Integer | 拉取开始日期(格式:yyyyMMdd) |
|
||||
| `innerBSEndDateId` | `dataEndDateId` | Integer | 拉取结束日期(格式:yyyyMMdd) |
|
||||
| - | `customerNo` | String | 客户身份证号(新增) |
|
||||
| - | `uploadUserId` | Integer | 柜员号(新增) |
|
||||
|
||||
### 接口4: 检查解析状态 Response
|
||||
|
||||
| 新增字段 | 类型 | 说明 |
|
||||
|---------|------|------|
|
||||
| `data.parsing` | Boolean | 是否正在解析(**关键字段**) |
|
||||
| `data.pendingList` | List<PendingItem> | 待处理文件列表(完整结构) |
|
||||
|
||||
**PendingItem 关键字段**:
|
||||
- `logId` (文件ID)
|
||||
- `status` (-5表示成功)
|
||||
- `uploadStatusDesc` (`data.wait.confirm.newaccount`表示成功)
|
||||
- `lostHeader` (丢失的表头)
|
||||
|
||||
### 接口7: 获取流水 Request
|
||||
|
||||
| 旧参数名 | 新参数名 | 类型 | 必填 | 说明 |
|
||||
|---------|---------|------|------|------|
|
||||
| `groupId` | `groupId` | Integer | 是 | 项目ID |
|
||||
| - | `logId` | Integer | **是** | 文件ID(**新增必填**) |
|
||||
| `pageNum` | `pageNow` | Integer | 是 | 当前页码(重命名) |
|
||||
| `pageSize` | `pageSize` | Integer | 是 | 每页数量 |
|
||||
|
||||
### 接口7: 获取流水 Response
|
||||
|
||||
**BankStatementItem 新增的主要字段** (40+字段):
|
||||
|
||||
| 字段分类 | 主要字段 |
|
||||
|---------|---------|
|
||||
| **账号信息** | `bankStatementId`, `leId`, `accountId`, `leName`, `accountMaskNo` |
|
||||
| **交易金额** | `drAmount`, `crAmount`, `balanceAmount`, `transAmount` (均为BigDecimal) |
|
||||
| **交易类型** | `cashType`, `transFlag`, `transTypeId`, `exceptionType` |
|
||||
| **对手方** | `customerId`, `customerName`, `customerAccountMaskNo`, `customerBank` |
|
||||
| **摘要备注** | `userMemo`, `bankComments`, `bankTrxNumber` |
|
||||
| **银行信息** | `bank` |
|
||||
| **其他** | `internalFlag`, `batchId`, `groupId`, `paymentMethod`, `cretNo` |
|
||||
| **转换金额** | `transformAmount`, `transformCrAmount`, `transformDrAmount`, `transfromBalanceAmount` |
|
||||
|
||||
---
|
||||
|
||||
## 待办事项
|
||||
|
||||
### 测试相关
|
||||
- [ ] 启动应用,访问 Swagger UI 验证接口显示
|
||||
- [ ] 使用 Swagger 测试接口1(获取Token)
|
||||
- [ ] 与前端联调测试新接口参数
|
||||
- [ ] 测试接口7的分页查询功能
|
||||
|
||||
### 部署相关
|
||||
- [ ] 更新生产环境配置文件 (`application-prod.yml`)
|
||||
- [ ] 确认生产环境接口路径
|
||||
- [ ] 准备上线发布说明
|
||||
|
||||
### 文档相关
|
||||
- [ ] 更新接口文档
|
||||
- [ ] 更新 API 使用示例
|
||||
- [ ] 通知前端开发人员接口变更
|
||||
|
||||
---
|
||||
|
||||
## 风险评估
|
||||
|
||||
### 影响范围
|
||||
- **前端调用**: 接口5、6已删除,前端需移除相关调用
|
||||
- **接口7参数**: 新增必填参数 `logId`,前端需调整
|
||||
- **接口3参数**: 多个参数重命名,前端需同步修改
|
||||
|
||||
### 风险等级
|
||||
**中等风险** - 涉及多个DTO重构和接口参数变更
|
||||
|
||||
### 建议措施
|
||||
1. 与前端团队充分沟通接口变更
|
||||
2. 在测试环境完整测试所有接口
|
||||
3. 保留旧版本文档作为参考
|
||||
4. 采用灰度发布方式逐步上线
|
||||
|
||||
---
|
||||
|
||||
## 参考资料
|
||||
|
||||
- **新版接口文档**: `doc/对接流水分析/兰溪-流水分析对接-新版.md`
|
||||
- **实施计划**: `docs/plans/2026-03-02-lsfx-update-plan.md`
|
||||
- **项目规范**: `CLAUDE.md`
|
||||
|
||||
---
|
||||
|
||||
**报告生成时间**: 2026-03-02 22:10
|
||||
**报告生成工具**: Claude Code
|
||||
**实施人员**: Claude Code AI Assistant
|
||||
308
doc/test-scripts/2026-02-28-frontend-test-report.md
Normal file
308
doc/test-scripts/2026-02-28-frontend-test-report.md
Normal file
@@ -0,0 +1,308 @@
|
||||
# 前端功能测试报告
|
||||
|
||||
## 测试概述
|
||||
|
||||
**测试日期**: 2026-02-28
|
||||
**测试人员**: Claude Code
|
||||
**测试环境**:
|
||||
- 后端: http://localhost:8080
|
||||
- 前端: http://localhost:84
|
||||
- 浏览器: Chrome 145.0.0.0
|
||||
- 测试账号: admin/admin123
|
||||
|
||||
## 测试目标
|
||||
|
||||
验证项目管理页面状态统计数字显示正确,并在用户交互(搜索、分页、状态切换)过程中保持稳定。
|
||||
|
||||
## 测试场景
|
||||
|
||||
### ✅ 场景 1: 页面初始加载
|
||||
|
||||
**操作步骤**:
|
||||
1. 访问前端应用 http://localhost:84
|
||||
2. 使用 admin/admin123 登录系统
|
||||
3. 导航到"初核项目管理"页面
|
||||
|
||||
**预期结果**:
|
||||
- 页面正常加载
|
||||
- 标签页显示正确的统计数字
|
||||
- 两个 API 请求成功(list 和 statusCounts)
|
||||
|
||||
**实际结果**: ✅ **通过**
|
||||
|
||||
**验证数据**:
|
||||
- 标签页统计:
|
||||
- 全部项目(29) ✅
|
||||
- 进行中(27) ✅
|
||||
- 已完成(1) ✅
|
||||
- 已归档(1) ✅
|
||||
- 列表显示: 共 29 条 ✅
|
||||
- API 请求:
|
||||
- `/ccdi/project/list?pageNum=1&pageSize=10` → 200 OK
|
||||
- `/ccdi/project/statusCounts` → 200 OK
|
||||
|
||||
**响应数据验证**:
|
||||
```json
|
||||
// list 接口响应
|
||||
{
|
||||
"total": 29,
|
||||
"rows": [ ...10条记录... ],
|
||||
"code": 200,
|
||||
"msg": "查询成功"
|
||||
}
|
||||
|
||||
// statusCounts 接口响应
|
||||
{
|
||||
"msg": "操作成功",
|
||||
"code": 200,
|
||||
"data": {
|
||||
"all": 29,
|
||||
"status0": 27,
|
||||
"status1": 1,
|
||||
"status2": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ✅ 场景 2: 搜索功能
|
||||
|
||||
**操作步骤**:
|
||||
1. 在搜索框输入 "测试4"
|
||||
2. 按回车键触发搜索
|
||||
|
||||
**预期结果**:
|
||||
- 列表只显示匹配的项目
|
||||
- 标签页数字保持不变(显示总数)
|
||||
|
||||
**实际结果**: ✅ **通过**
|
||||
|
||||
**验证数据**:
|
||||
- 搜索结果: 1 条(测试4) ✅
|
||||
- 标签页统计(保持不变):
|
||||
- 全部项目(29) ✅
|
||||
- 进行中(27) ✅
|
||||
- 已完成(1) ✅
|
||||
- 已归档(1) ✅
|
||||
- 分页显示: 共 1 条 ✅
|
||||
|
||||
**API 请求验证**:
|
||||
```
|
||||
GET /ccdi/project/list?pageNum=1&pageSize=10&projectName=测试4 → 200 OK
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ✅ 场景 3: 分页功能
|
||||
|
||||
**操作步骤**:
|
||||
1. 清空搜索框,刷新页面恢复初始状态
|
||||
2. 点击分页组件的"2"按钮,切换到第 2 页
|
||||
|
||||
**预期结果**:
|
||||
- 列表切换到第 2 页数据
|
||||
- 标签页数字保持不变
|
||||
|
||||
**实际结果**: ✅ **通过**
|
||||
|
||||
**验证数据**:
|
||||
- 当前页码: 第 2 页 ✅
|
||||
- 标签页统计(保持不变):
|
||||
- 全部项目(29) ✅
|
||||
- 进行中(27) ✅
|
||||
- 已完成(1) ✅
|
||||
- 已归档(1) ✅
|
||||
- 分页显示: 共 29 条 ✅
|
||||
|
||||
**API 请求验证**:
|
||||
```
|
||||
GET /ccdi/project/list?pageNum=2&pageSize=10 → 200 OK
|
||||
GET /ccdi/project/statusCounts → 200 OK
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ✅ 场景 4: 状态切换功能
|
||||
|
||||
**操作步骤**:
|
||||
1. 点击"进行中"标签
|
||||
|
||||
**预期结果**:
|
||||
- 列表只显示"进行中"状态的项目
|
||||
- 标签页数字保持不变(仍显示总数)
|
||||
|
||||
**实际结果**: ✅ **通过**
|
||||
|
||||
**验证数据**:
|
||||
- 列表过滤: 所有项目状态都是"进行中" ✅
|
||||
- 标签页统计(保持不变):
|
||||
- 全部项目(29) ✅
|
||||
- 进行中(27) ✅
|
||||
- 已完成(1) ✅
|
||||
- 已归档(1) ✅
|
||||
- 分页显示: 共 27 条(正确反映当前状态的项目数) ✅
|
||||
|
||||
**API 请求验证**:
|
||||
```
|
||||
GET /ccdi/project/list?pageNum=1&pageSize=10&status=0 → 200 OK
|
||||
GET /ccdi/project/statusCounts → 200 OK
|
||||
```
|
||||
|
||||
**响应数据验证**:
|
||||
```json
|
||||
// status=0 过滤后的列表
|
||||
{
|
||||
"total": 27,
|
||||
"rows": [
|
||||
{"projectId": 31, "projectName": "测试123", "status": "0", ...},
|
||||
{"projectId": 23, "projectName": "测试23", "status": "0", ...},
|
||||
...
|
||||
],
|
||||
"code": 200,
|
||||
"msg": "查询成功"
|
||||
}
|
||||
|
||||
// 状态统计(始终返回总数)
|
||||
{
|
||||
"msg": "操作成功",
|
||||
"code": 200,
|
||||
"data": {
|
||||
"all": 29,
|
||||
"status0": 27,
|
||||
"status1": 1,
|
||||
"status2": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ✅ 场景 5: 浏览器控制台检查
|
||||
|
||||
**操作步骤**:
|
||||
1. 打开浏览器开发者工具的 Console 标签
|
||||
|
||||
**预期结果**:
|
||||
- 没有 JavaScript 错误
|
||||
- 看到两个 API 请求成功
|
||||
|
||||
**实际结果**: ✅ **通过**
|
||||
|
||||
**控制台消息**:
|
||||
- ✅ 没有 JavaScript 错误
|
||||
- ⚠️ 1 个警告: "A form field element should have an id or name attribute" (不影响功能)
|
||||
|
||||
---
|
||||
|
||||
## 网络请求统计
|
||||
|
||||
**总请求数**: 40 个
|
||||
|
||||
**关键 API 请求**:
|
||||
1. 初始加载:
|
||||
- `/ccdi/project/list?pageNum=1&pageSize=10` → 200 OK
|
||||
- `/ccdi/project/statusCounts` → 200 OK
|
||||
|
||||
2. 搜索功能:
|
||||
- `/ccdi/project/list?pageNum=1&pageSize=10&projectName=测试4` → 200 OK
|
||||
|
||||
3. 分页功能:
|
||||
- `/ccdi/project/list?pageNum=2&pageSize=10` → 200 OK
|
||||
- `/ccdi/project/statusCounts` → 200 OK
|
||||
|
||||
4. 状态切换:
|
||||
- `/ccdi/project/list?pageNum=1&pageSize=10&status=0` → 200 OK
|
||||
- `/ccdi/project/statusCounts` → 200 OK
|
||||
|
||||
**所有请求状态**: ✅ 全部成功(200 OK)
|
||||
|
||||
---
|
||||
|
||||
## 核心修复验证
|
||||
|
||||
### 问题回顾
|
||||
|
||||
**原始问题**: 标签页数字随分页变化,不稳定
|
||||
|
||||
**根本原因**: 前端使用列表响应的 total 字段来更新标签页数字,导致搜索/分页/过滤时数字会变化
|
||||
|
||||
**解决方案**:
|
||||
1. 后端新增 `/statusCounts` 接口,始终返回所有状态的总数
|
||||
2. 前端在每次加载时并行请求 list 和 statusCounts
|
||||
3. 标签页数字只使用 statusCounts 的数据,不受列表过滤影响
|
||||
|
||||
### 修复效果验证
|
||||
|
||||
✅ **搜索时**: 标签页数字保持 29/27/1/1 不变
|
||||
✅ **分页时**: 标签页数字保持 29/27/1/1 不变
|
||||
✅ **状态切换时**: 标签页数字保持 29/27/1/1 不变
|
||||
|
||||
---
|
||||
|
||||
## 测试结论
|
||||
|
||||
### ✅ 所有测试场景通过
|
||||
|
||||
| 测试场景 | 状态 | 备注 |
|
||||
|---------|------|------|
|
||||
| 页面初始加载 | ✅ 通过 | 标签页数字正确显示 |
|
||||
| 搜索功能 | ✅ 通过 | 数字保持稳定 |
|
||||
| 分页功能 | ✅ 通过 | 数字保持稳定 |
|
||||
| 状态切换功能 | ✅ 通过 | 数字保持稳定 |
|
||||
| 浏览器控制台 | ✅ 通过 | 无 JavaScript 错误 |
|
||||
|
||||
### 关键指标
|
||||
|
||||
- ✅ **功能正确性**: 100% 通过
|
||||
- ✅ **数据一致性**: 标签页数字在所有操作中保持稳定
|
||||
- ✅ **用户体验**: 符合预期,数字显示直观清晰
|
||||
- ✅ **性能**: API 请求并行执行,响应迅速
|
||||
- ✅ **代码质量**: 无 JavaScript 错误,警告不影响功能
|
||||
|
||||
### 建议
|
||||
|
||||
1. ✅ **功能完善**: 建议将此修复方案应用到其他类似的列表页面
|
||||
2. ⚠️ **警告处理**: 建议为搜索框添加 id 或 name 属性以消除控制台警告
|
||||
3. ✅ **文档更新**: 更新用户手册,说明标签页数字表示总数而非当前过滤结果
|
||||
|
||||
---
|
||||
|
||||
## 附录
|
||||
|
||||
### 测试环境信息
|
||||
|
||||
```
|
||||
操作系统: Windows 11 Pro 10.0.26200
|
||||
浏览器: Chrome 145.0.0.0
|
||||
后端服务: http://localhost:8080
|
||||
前端服务: http://localhost:84
|
||||
数据库: MySQL 8.2.0
|
||||
Java 版本: 17
|
||||
Spring Boot 版本: 3.5.8
|
||||
Vue.js 版本: 2.6.12
|
||||
```
|
||||
|
||||
### 相关文件
|
||||
|
||||
**后端**:
|
||||
- `D:/ccdi/ccdi/ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/domain/vo/ProjectStatusCountsVO.java`
|
||||
- `D:/ccdi/ccdi/ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/service/ICcdiProjectService.java`
|
||||
- `D:/ccdi/ccdi/ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/service/impl/CcdiProjectServiceImpl.java`
|
||||
- `D:/ccdi/ccdi/ruoyi-info-collection/src/main/java/com/ruoyi/info/collection/controller/CcdiProjectController.java`
|
||||
|
||||
**前端**:
|
||||
- `D:/ccdi/ccdi/ruoyi-ui/src/api/ccdiProject.js`
|
||||
- `D:/ccdi/ccdi/ruoyi-ui/src/views/ccdiProject/index.vue`
|
||||
- `D:/ccdi/ccdi/ruoyi-ui/src/views/ccdiProject/components/SearchBar.vue`
|
||||
|
||||
### 测试执行时间
|
||||
|
||||
- 开始时间: 2026-02-28 09:46:48
|
||||
- 结束时间: 2026-02-28 09:50:00
|
||||
- 总耗时: 约 3 分钟
|
||||
|
||||
---
|
||||
|
||||
**测试人员签名**: Claude Code
|
||||
**测试日期**: 2026-02-28
|
||||
7
doc/对接流水分析/task.md
Normal file
7
doc/对接流水分析/task.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# 系统需要对接流水分析平台,调用流水分析平台的接口
|
||||
- 新建一个模块ccdi_lsfx,保存所有调用流水分析平台的代码
|
||||
- 创建一个用于发起http请求的工具类,使用RestTemplate
|
||||
- 读取接口文档 D:\ccdi\ccdi\doc\对接流水分析\兰溪-流水分析对接.docx,分析每个接口的入参和返回值格式,封装入参和出参的对象
|
||||
- 在配置文档中添加每个接口对应的url配置
|
||||
- 在后端服务层中实现这些接口的调用,获取返回值
|
||||
- 创建一个控制层,可以调用这些接口,用于测试
|
||||
BIN
doc/对接流水分析/~$-流水分析对接_new.docx
Normal file
BIN
doc/对接流水分析/~$-流水分析对接_new.docx
Normal file
Binary file not shown.
548
doc/对接流水分析/兰溪-流水分析对接-新版.md
Normal file
548
doc/对接流水分析/兰溪-流水分析对接-新版.md
Normal file
@@ -0,0 +1,548 @@
|
||||
# 兰溪-流水分析对接文档
|
||||
|
||||
## 接口说明
|
||||
|
||||
**生产环境IP**: `64.202.32.176`
|
||||
|
||||
### 接口调用流程
|
||||
|
||||
1. 初始化调用 `/account/common/getToken` 接口创建项目(必填参数按要求输入,选填参数可忽略)
|
||||
2. 调用 `/watson/api/project/remoteUploadSplitFile` 接口上传文件,或者拉取行内流水 `/watson/api/project/getJZFileOrZjrcuFile`
|
||||
3. 调用 `/watson/api/project/upload/getpendings` 获取文件解析的状态
|
||||
- 文件上传后有个解析过程,需要观察该接口返回的 `parsing` 是否为 `false`
|
||||
- 如果为 `true`,可间隔1s轮询调用此接口,直到 `parsing` 为 `false`
|
||||
- 获取 `status` 的值,如果不为 `-5`,提示用户解析失败
|
||||
4. 如果流水文件解析失败,可以调用 `/watson/api/project/batchDeleteUploadFile` 接口删除流水文件
|
||||
5. 流水解析成功后,调用 `/watson/api/project/upload/getBankStatement` 接口将对应的流水明细存储到兰溪本地
|
||||
|
||||
---
|
||||
|
||||
## 1. 新建项目并获取token
|
||||
|
||||
### 1.1 接口请求地址
|
||||
|
||||
- **测试环境**: `http://158.234.196.5:82/c4c3/account/common/getToken`
|
||||
- **请求方法**: `POST`
|
||||
|
||||
### 1.2 请求参数说明
|
||||
|
||||
**接口备注**: 第三方系统中,点击需要查看的项目向见知现金流尽调系统请求访问token,每个项目的token不同。现金流尽调系统根据 ProjectNo 为唯一标识查找项目,如果对应的项目不存在则自动创建项目。注意token使用一次后即失效,再次访问项目需要重新申请。(支持拉取金综和行内流水)
|
||||
|
||||
**请求体参数说明**:
|
||||
|
||||
| 参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 |
|
||||
|--------|--------|----------|----------|----------|
|
||||
| projectNo | 902000_当前时间戳 | String | 是 | 项目编号,格式:902000_当前时间戳 |
|
||||
| entityName | 902000_202603021400 | String | 是 | 项目名称 |
|
||||
| userId | 902001 | String | 是 | 操作人员编号,固定值 |
|
||||
| userName | 902001 | String | 是 | 操作人员姓名,固定值 |
|
||||
| appId | remote_app | String | 是 | 固定值 |
|
||||
| appSecretCode | 6ee87a361f29234ad25d7893da9975a9 | String | 是 | 安全码 md5(projectNo + "_" + entityName + "_" + dXj6eHRmPv) |
|
||||
| role | VIEWER | String | 是 | 固定值 |
|
||||
| orgCode | 902000 | String | 是 | 行社机构号,固定值 |
|
||||
| entityId | 123456 | String | 否 | 企业统信码或个人身份证号 |
|
||||
| xdRelatedPersons | [{"relatedPerson":"上海上水纯净水有限公司","relation":"董事长"}, {"relatedPerson":"于小雪","relation":"股东"}, {"relatedPerson":"深圳市云顶信息技术有限公司","relation":"父子"}] | String | 否 | 信贷关联人信息 |
|
||||
| jzDataDateId | 0 | String | 否 | 拉取指定日期推送过来的金综链流水,为0时标识不需要拉取金综链流水 |
|
||||
| innerBSStartDateId | 0 | String | 否 | 拉取行内流水开始日期,0:不需要拉取行内流水。流水分析系统根据entityId到数仓中查询行内流水 |
|
||||
| innerBSEndDateId | 0 | String | 否 | 拉取行内流水结束日期,0:不需要拉取行内流水。流水分析系统根据entityId到数仓中查询行内流水 |
|
||||
| analysisType | -1 | String | 是 | 固定值 |
|
||||
| departmentCode | 902000 | String | 是 | 客户经理所属营业部/分理处的机构编码,固定值 |
|
||||
|
||||
### 1.3 返回参数说明
|
||||
|
||||
**成功响应 (200)**:
|
||||
|
||||
| 参数名 | 示例值 | 参数类型 | 参数描述 |
|
||||
|--------|--------|----------|----------|
|
||||
| code | 200 | String | 返回码: 200 请求成功; 请求失败: 40100 未知异常, 40101 appId错误, 40102 appSecretCode错误, 40104 可使用项目次数为0无法创建项目, 40105 只读模式下无法新建项目, 40106 错误的分析类型不在规定的取值范围内, 40107 当前系统不支持的分析类型, 40108 当前用户所属行社无权限 |
|
||||
| data | - | Object | 返回数据 |
|
||||
| data.token | eyJ0eXAi... | String | token |
|
||||
| data.projectId | 77 | Integer | 见知项目Id |
|
||||
| data.projectNo | test-zjnx-1204 | String | 项目编号 |
|
||||
| data.entityName | 浙江农信test1204 | String | 项目名称 |
|
||||
| data.analysisType | 0 | Integer | 分析类型 |
|
||||
| message | create.token.success | String | 返回消息 |
|
||||
| status | 200 | String | 状态 |
|
||||
| successResponse | true | Boolean | 是否成功响应 |
|
||||
|
||||
### 1.4 返回示例
|
||||
|
||||
**成功响应 (200)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "200",
|
||||
"data": {
|
||||
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwcm9qZWN0Tm8iOiJ0ZXN0LXpqbngtMTIwNCIsInJvbGUiOiJWSUVXRVIiLCJlbnRpdHlOYW1lIjoi5rWZ5rGf5Yac5L-hdGVzdDEyMDQiLCJ1c2VyTmFtZSI6Iua1i-ivlTAwMSIsImV4cCI6MTcwMTY3ODEyMSwicHJvamVjdElkIjo3NywidXNlcklkIjoidGVzdDAwMSJ9.UMloP6vB1dayQglVdVcpC9w01kv8kyodKDYfPOC7Hac",
|
||||
"projectId": 77,
|
||||
"projectNo": "test-zjnx-1204",
|
||||
"entityName": "浙江农信test1204",
|
||||
"analysisType": 0
|
||||
},
|
||||
"message": "create.token.success",
|
||||
"status": "200",
|
||||
"successResponse": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 上传文件接口
|
||||
|
||||
### 2.1 接口请求地址
|
||||
|
||||
- **测试环境**: `158.234.196.5:82/c4c3/watson/api/project/remoteUploadSplitFile`
|
||||
- **请求头**: `X-Xencio-Client-Id: 26e5b9239853436b85c623f4b7a6d0e6`
|
||||
- **请求方法**: `POST`
|
||||
|
||||
### 2.2 请求参数说明
|
||||
|
||||
| 参数 | 类型 | 参数名称 | 是否必填 | 说明 |
|
||||
|------|------|----------|----------|------|
|
||||
| groupId | Int | 项目id | 是 | - |
|
||||
| files | File | 上传的文件 | 是 | - |
|
||||
|
||||
### 2.3 响应结果信息
|
||||
|
||||
**注意**: `status` 等于 `-5` 且 `uploadStatusDesc` 等于 `data.wait.confirm.newaccount` 表示当前流水文件上传后解析成功。反之则没有成功。
|
||||
|
||||
| 序号 | 字段 | 类型 | 备注 |
|
||||
|------|------|------|------|
|
||||
| - | code | String | 200成功 其他状态码失败 |
|
||||
| - | data | Object | 列表 |
|
||||
| - | accountName | - | 主体名称 |
|
||||
| - | accountNo | - | 账号 |
|
||||
| - | uploadFileName | - | 文件名称 |
|
||||
| - | fileSize | - | 文件大小,单位Byte |
|
||||
| - | status | - | 状态值 |
|
||||
| - | uploadStatusDesc | - | 文件状态描述 |
|
||||
| - | bank | - | 所属银行 |
|
||||
| - | currency | - | 币种 |
|
||||
| - | accountId | - | 账号id |
|
||||
| - | logId | - | 文件id |
|
||||
|
||||
### 2.4 参数请求样例
|
||||
|
||||
*暂未提供*
|
||||
|
||||
### 2.5 结果集合样例
|
||||
|
||||
**注意**: 结果集合样例不为测试案例结果,具体测试案例结果由具体的参数案例返回为具体值。
|
||||
|
||||
**成功响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "200",
|
||||
"data": {
|
||||
"accountsOfLog": {
|
||||
"13976": [
|
||||
{
|
||||
"bank": "BSX",
|
||||
"accountName": "",
|
||||
"accountNo": "虞海良绍兴银行流水",
|
||||
"currency": "CNY"
|
||||
}
|
||||
]
|
||||
},
|
||||
"uploadLogList": [
|
||||
{
|
||||
"accountNoList": [],
|
||||
"bankName": "BSX",
|
||||
"dataTypeInfo": [
|
||||
"CSV",
|
||||
","
|
||||
],
|
||||
"downloadFileName": "虞海良绍兴银行流水.csv",
|
||||
"enterpriseNameList": [],
|
||||
"filePackageId": "14b13103010e4d32b5406c764cfe3644",
|
||||
"fileSize": 46724,
|
||||
"fileUploadBy": 448,
|
||||
"fileUploadByUserName": "admin@support.com",
|
||||
"fileUploadTime": "2025-03-12 18:53:29",
|
||||
"leId": 10724,
|
||||
"logId": 13976,
|
||||
"logMeta": "{\"lostHeader\":[],\"balanceAmount\":true}",
|
||||
"logType": "bankstatement",
|
||||
"loginLeId": 10724,
|
||||
"realBankName": "BSX",
|
||||
"rows": 0,
|
||||
"source": "http",
|
||||
"status": -5,
|
||||
"templateName": "BSX_T240925",
|
||||
"totalRecords": 280,
|
||||
"trxDateEndId": 20240905,
|
||||
"trxDateStartId": 20230914,
|
||||
"uploadFileName": "虞海良绍兴银行流水.csv",
|
||||
"uploadStatusDesc": "data.wait.confirm.newaccount"
|
||||
}
|
||||
],
|
||||
"uploadStatus": 1
|
||||
},
|
||||
"status": "200",
|
||||
"successResponse": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 拉取行内流水的接口
|
||||
|
||||
### 3.1 接口请求地址
|
||||
|
||||
- **测试环境**: `158.234.196.5:82/c4c3/watson/api/project/getJZFileOrZjrcuFile`
|
||||
- **请求头**: `X-Xencio-Client-Id: 26e5b9239853436b85c623f4b7a6d0e6`
|
||||
- **请求方法**: `POST`
|
||||
|
||||
### 3.2 请求参数说明
|
||||
|
||||
| 参数 | 类型 | 参数名称 | 是否必填 | 说明 |
|
||||
|------|------|----------|----------|------|
|
||||
| groupId | Int | 项目id | 是 | - |
|
||||
| customerNo | String | 客户身份证号 | 是 | - |
|
||||
| dataChannelCode | String | 校验码 | 是 | ZJRCU |
|
||||
| requestDateId | Int | 发起请求的时间 | 是 | 当天请求时间 |
|
||||
| dataStartDateId | Int | 拉取开始日期 | 是 | - |
|
||||
| dataEndDateId | Int | 拉取结束日期 | 是 | - |
|
||||
| uploadUserId | int | 柜员号 | 是 | - |
|
||||
|
||||
### 3.3 响应结果信息
|
||||
|
||||
| 序号 | 字段 | 类型 | 备注 |
|
||||
|------|------|------|------|
|
||||
| 1 | code | String | 200成功 其他状态码失败 |
|
||||
| 2 | data | Object | 列表 |
|
||||
|
||||
### 3.4 参数请求样例
|
||||
|
||||
拉取行内流水
|
||||
|
||||
*暂未提供*
|
||||
|
||||
### 3.5 结果集合样例
|
||||
|
||||
**注意**: 结果集合样例不为测试案例结果,具体测试案例结果由具体的参数案例返回为具体值。
|
||||
|
||||
**行内流水失败**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "200",
|
||||
"data": {
|
||||
"code": "501014",
|
||||
"message": "无行内流水文件"
|
||||
},
|
||||
"status": "200",
|
||||
"successResponse": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 判断文件是否解析结束
|
||||
|
||||
### 4.1 接口请求地址
|
||||
|
||||
- **测试环境**: `http://158.234.196.5:82/c4c3/watson/api/project/upload/getpendings`
|
||||
- **请求头**: `X-Xencio-Client-Id: c2017e8d105c435a96f86373635b6a09`
|
||||
- **请求方法**: `POST`
|
||||
|
||||
### 4.2 请求参数说明
|
||||
|
||||
| 参数 | 类型 | 参数名称 | 是否必填 | 说明 |
|
||||
|------|------|----------|----------|------|
|
||||
| groupId | Int | 项目id | 是 | - |
|
||||
| inprogressList | String | 文件id | 是 | - |
|
||||
|
||||
### 4.3 响应结果信息
|
||||
|
||||
**注意**: 文件解析有个处理过程,`parsing` 为 `false` 表示解析结束,可以轮询调用此接口。`status` 等于 `-5` 且 `uploadStatusDesc` 等于 `data.wait.confirm.newaccount` 表示文件解析成功。反之则没有成功。
|
||||
|
||||
| 序号 | 字段 | 类型 | 备注 |
|
||||
|------|------|------|------|
|
||||
| 1 | code | String | 200成功 其他状态码失败 |
|
||||
| 2 | data | Object | 列表 |
|
||||
| 3 | uploadFileName | - | 上传文件名称 |
|
||||
| 4 | status | - | 文件解析后状态值 |
|
||||
| 5 | uploadStatusDesc | - | 文件解析后状态描述 |
|
||||
| 6 | parsing | - | 文件解析状态,true表示解析中,false表示解析结束 |
|
||||
|
||||
### 4.4 参数请求样例
|
||||
|
||||
*暂未提供*
|
||||
|
||||
### 4.5 结果集合样例
|
||||
|
||||
**注意**: 结果集合样例不为测试案例结果,具体测试案例结果由具体的参数案例返回为具体值。
|
||||
|
||||
**成功响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "200",
|
||||
"data": {
|
||||
"parsing": false,
|
||||
"pendingList": [
|
||||
{
|
||||
"accountNoList": [],
|
||||
"bankName": "ZJRCU",
|
||||
"dataTypeInfo": [
|
||||
"CSV",
|
||||
","
|
||||
],
|
||||
"downloadFileName": "230902199012261247_20260201_20260201_1772096608615.csv",
|
||||
"enterpriseNameList": [],
|
||||
"filePackageId": "cde6c7cf5cab48e8892f0c1c36b2aa7d",
|
||||
"fileSize": 53101,
|
||||
"fileUploadBy": 448,
|
||||
"fileUploadByUserName": "admin@support.com",
|
||||
"fileUploadTime": "2026-02-27 09:50:18",
|
||||
"isSplit": 0,
|
||||
"leId": 16210,
|
||||
"logId": 19116,
|
||||
"logMeta": "{\"lostHeader\":[],\"balanceAmount\":true}",
|
||||
"logType": "bankstatement",
|
||||
"loginLeId": 16210,
|
||||
"lostHeader": [],
|
||||
"realBankName": "ZJRCU",
|
||||
"rows": 0,
|
||||
"source": "http",
|
||||
"status": -5,
|
||||
"templateName": "ZJRCU_T251114",
|
||||
"totalRecords": 131,
|
||||
"trxDateEndId": 20240228,
|
||||
"trxDateStartId": 20240201,
|
||||
"uploadFileName": "230902199012261247_20260201_20260201_1772096608615.csv",
|
||||
"uploadStatusDesc": "data.wait.confirm.newaccount"
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "200",
|
||||
"successResponse": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 获取流水列表并存储到兰溪本地
|
||||
|
||||
### 6.1 接口请求地址
|
||||
|
||||
- **测试环境**: `158.234.196.5:82/c4c3/watson/api/project/getBSByLogId`
|
||||
- **请求头**: `X-Xencio-Client-Id: 26e5b9239853436b85c623f4b7a6d0e6`
|
||||
- **请求方法**: `POST`
|
||||
|
||||
### 6.2 请求参数说明
|
||||
|
||||
| 参数 | 类型 | 参数名称 | 是否必填 | 说明 |
|
||||
|------|------|----------|----------|------|
|
||||
| groupId | Int | 项目id | 是 | - |
|
||||
| logId | Int | 文件id | 是 | - |
|
||||
| pageNow | Int | 当前页码 | 是 | - |
|
||||
| pageSize | Int | 查询条数 | 是 | - |
|
||||
|
||||
### 6.3 响应结果信息
|
||||
|
||||
| 序号 | 字段 | 类型 | 备注 |
|
||||
|------|------|------|------|
|
||||
| 1 | code | String | 200成功 其他状态码失败 |
|
||||
| 2 | data | Object | 列表 |
|
||||
| 3 | bankStatementList | - | 流水列表 |
|
||||
| 4 | totalCount | - | 总条数 |
|
||||
|
||||
### 6.4 参数请求样例
|
||||
|
||||
*暂未提供*
|
||||
|
||||
### 6.5 结果集合样例
|
||||
|
||||
**注意**: 结果集合样例不为测试案例结果,具体测试案例结果由具体的参数案例返回为具体值。
|
||||
|
||||
**成功响应**:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "200",
|
||||
"data": {
|
||||
"bankStatementList": [
|
||||
{
|
||||
"accountId": 0,
|
||||
"accountMaskNo": "101015251071645",
|
||||
"accountingDate": "2024-02-01",
|
||||
"accountingDateId": 20240201,
|
||||
"archivingFlag": 0,
|
||||
"attachments": 0,
|
||||
"balanceAmount": 4814.82,
|
||||
"bank": "ZJRCU",
|
||||
"bankComments": "",
|
||||
"bankStatementId": 12847662,
|
||||
"bankTrxNumber": "1a10458dd5c3366d7272285812d434fc",
|
||||
"batchId": 19135,
|
||||
"cashType": "1",
|
||||
"commentsNum": 0,
|
||||
"crAmount": 0,
|
||||
"cretNo": "230902199012261247",
|
||||
"currency": "CNY",
|
||||
"customerAccountMaskNo": "597671502",
|
||||
"customerBank": "",
|
||||
"customerId": -1,
|
||||
"customerName": "小店",
|
||||
"customerReference": "",
|
||||
"downPaymentFlag": 0,
|
||||
"drAmount": 245.8,
|
||||
"exceptionType": "",
|
||||
"groupId": 16238,
|
||||
"internalFlag": 0,
|
||||
"leId": 16308,
|
||||
"leName": "张传伟",
|
||||
"overrideBsId": 0,
|
||||
"paymentMethod": "",
|
||||
"sourceCatalogId": 0,
|
||||
"split": 0,
|
||||
"subBankstatementId": 0,
|
||||
"toDoFlag": 0,
|
||||
"transAmount": 245.8,
|
||||
"transFlag": "P",
|
||||
"transTypeId": 0,
|
||||
"transformAmount": 0,
|
||||
"transformCrAmount": 0,
|
||||
"transformDrAmount": 0,
|
||||
"transfromBalanceAmount": 0,
|
||||
"trxBalance": 0,
|
||||
"trxDate": "2024-02-01 10:33:44",
|
||||
"userMemo": "财付通消费_小店"
|
||||
}
|
||||
],
|
||||
"totalCount": 131
|
||||
},
|
||||
"status": "200",
|
||||
"successResponse": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 兰溪存储的流水表表结构
|
||||
|
||||
### 7.1 表结构定义
|
||||
|
||||
```sql
|
||||
CREATE TABLE `c4c_bank_statement_stg` (
|
||||
`bank_statement_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`LE_ID` int(10) unsigned DEFAULT '0' COMMENT '企业ID',
|
||||
`ACCOUNT_ID` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '账号ID',
|
||||
`LE_ACCOUNT_NAME` varchar(240) DEFAULT 'NONE' COMMENT '企业账号名称',
|
||||
`LE_ACCOUNT_NO` varchar(240) DEFAULT NULL COMMENT '企业银行账号',
|
||||
`ACCOUNTING_DATE_ID` int(11) DEFAULT NULL COMMENT '账号日期ID',
|
||||
`ACCOUNTING_DATE` varchar(10) DEFAULT '0000-00-00' COMMENT '账号日期',
|
||||
`TRX_DATE` varchar(20) NOT NULL COMMENT '交易日期',
|
||||
`CURRENCY` varchar(10) DEFAULT NULL COMMENT '币种',
|
||||
`AMOUNT_DR` decimal(19,2) NOT NULL DEFAULT '0.00' COMMENT '付款金额',
|
||||
`AMOUNT_CR` decimal(19,2) NOT NULL DEFAULT '0.00' COMMENT '收款金额',
|
||||
`AMOUNT_BALANCE` decimal(19,2) NOT NULL COMMENT '余额',
|
||||
`CASH_TYPE` varchar(500) DEFAULT NULL COMMENT '交易类型',
|
||||
`CUSTOMER_LE_ID` int(11) DEFAULT '-1' COMMENT '对手方企业ID',
|
||||
`CUSTOMER_ACCOUNT_NAME` varchar(240) DEFAULT NULL COMMENT '对手方企业名称',
|
||||
`CUSTOMER_ACCOUNT_NO` varchar(240) DEFAULT NULL COMMENT '对手方账号',
|
||||
`customer_bank` varchar(300) DEFAULT NULL COMMENT '对手方银行',
|
||||
`customer_reference` varchar(500) DEFAULT NULL COMMENT '对手方备注',
|
||||
`USER_MEMO` varchar(1000) DEFAULT NULL COMMENT '用户交易摘要',
|
||||
`BANK_COMMENTS` varchar(240) DEFAULT NULL COMMENT '银行交易摘要',
|
||||
`BANK_TRX_NUMBER` varchar(240) DEFAULT NULL COMMENT '银行交易号',
|
||||
`BANK` varchar(250) NOT NULL DEFAULT '' COMMENT '所属银行缩写',
|
||||
`TRX_FLAG` varchar(2) DEFAULT '0' COMMENT '交易标志位',
|
||||
`TRX_TYPE` int(11) NOT NULL DEFAULT '0' COMMENT '分类ID',
|
||||
`EXCEPTION_TYPE` varchar(50) NOT NULL DEFAULT '' COMMENT '异常类型',
|
||||
`internal_flag` tinyint(1) DEFAULT '0' COMMENT '是否为内部交易1 是 0 否',
|
||||
`batch_id` int(11) NOT NULL DEFAULT '0' COMMENT '上传logId对应upload_log',
|
||||
`batch_sequence` int(11) NOT NULL COMMENT '每次上传在文件中的line',
|
||||
`CREATE_DATE` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`created_by` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '创建者',
|
||||
`meta_json` text COMMENT 'meta json',
|
||||
`no_balance` tinyint(1) DEFAULT '0' COMMENT '是否包含余额',
|
||||
`begin_balance` tinyint(1) DEFAULT '0' COMMENT '初始余额',
|
||||
`end_balance` tinyint(1) DEFAULT '0' COMMENT '结束余额',
|
||||
`group_id` int(11) DEFAULT '0' COMMENT '项目id',
|
||||
`override_bs_id` bigint(20) DEFAULT '0' COMMENT '=0表示该数据未覆盖主表,>0表示覆盖主表,<0表示被主表覆盖',
|
||||
`payment_method` varchar(500) DEFAULT NULL COMMENT '微信、支付宝流水字段,交易方式',
|
||||
PRIMARY KEY (`bank_statement_id`),
|
||||
KEY `idx_batch_id_account` (`batch_id`,`LE_ACCOUNT_NO`,`ACCOUNTING_DATE_ID`),
|
||||
KEY `GROUP_ID` (`group_id`),
|
||||
KEY `c4c_bank_statement_stg_batch_id_IDX` (`batch_id`,`LE_ACCOUNT_NO`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='银行流水的中间处理表';
|
||||
```
|
||||
|
||||
### 7.2 字段映射关系
|
||||
|
||||
| 接口返回字段 | 数据库字段 | 说明 |
|
||||
|-------------|-----------|------|
|
||||
| bankStatementId | bank_statement_id | 流水ID |
|
||||
| leId | LE_ID | 企业ID |
|
||||
| accountId | ACCOUNT_ID | 账号ID |
|
||||
| leName | LE_ACCOUNT_NAME | 企业账号名称 |
|
||||
| accountMaskNo | LE_ACCOUNT_NO | 企业银行账号 |
|
||||
| accountingDateId | ACCOUNTING_DATE_ID | 账号日期ID |
|
||||
| accountingDate | ACCOUNTING_DATE | 账号日期 |
|
||||
| trxDate | TRX_DATE | 交易日期 |
|
||||
| currency | CURRENCY | 币种 |
|
||||
| drAmount | AMOUNT_DR | 付款金额 |
|
||||
| crAmount | AMOUNT_CR | 收款金额 |
|
||||
| balanceAmount | AMOUNT_BALANCE | 余额 |
|
||||
| cashType | CASH_TYPE | 交易类型 |
|
||||
| customerId | CUSTOMER_LE_ID | 对手方企业ID |
|
||||
| customerName | CUSTOMER_ACCOUNT_NAME | 对手方企业名称 |
|
||||
| customerAccountMaskNo | CUSTOMER_ACCOUNT_NO | 对手方账号 |
|
||||
| customerBank | customer_bank | 对手方银行 |
|
||||
| customerReference | customer_reference | 对手方备注 |
|
||||
| userMemo | USER_MEMO | 用户交易摘要 |
|
||||
| bankComments | BANK_COMMENTS | 银行交易摘要 |
|
||||
| bankTrxNumber | BANK_TRX_NUMBER | 银行交易号 |
|
||||
| bank | BANK | 所属银行缩写 |
|
||||
| transFlag | TRX_FLAG | 交易标志位 |
|
||||
| transTypeId | TRX_TYPE | 分类ID |
|
||||
| exceptionType | EXCEPTION_TYPE | 异常类型 |
|
||||
| internalFlag | internal_flag | 是否为内部交易 |
|
||||
| batchId | batch_id | 上传logId |
|
||||
| - | batch_sequence | 每次上传在文件中的line |
|
||||
| - | CREATE_DATE | 创建时间 |
|
||||
| - | created_by | 创建者 |
|
||||
| - | meta_json | meta json |
|
||||
| - | no_balance | 是否包含余额 |
|
||||
| - | begin_balance | 初始余额 |
|
||||
| - | end_balance | 结束余额 |
|
||||
| groupId | group_id | 项目id |
|
||||
| overrideBsId | override_bs_id | 覆盖标识 |
|
||||
| paymentMethod | payment_method | 交易方式 |
|
||||
|
||||
---
|
||||
|
||||
## 附录
|
||||
|
||||
### 常见错误码
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| 200 | 请求成功 |
|
||||
| 40100 | 未知异常 |
|
||||
| 40101 | appId错误 |
|
||||
| 40102 | appSecretCode错误 |
|
||||
| 40104 | 可使用项目次数为0,无法创建项目 |
|
||||
| 40105 | 只读模式下无法新建项目 |
|
||||
| 40106 | 错误的分析类型,不在规定的取值范围内 |
|
||||
| 40107 | 当前系统不支持的分析类型 |
|
||||
| 40108 | 当前用户所属行社无权限 |
|
||||
| 501014 | 无行内流水文件 |
|
||||
|
||||
### 文件解析状态说明
|
||||
|
||||
| 字段 | 值 | 说明 |
|
||||
|------|-----|------|
|
||||
| status | -5 | 文件解析成功 |
|
||||
| uploadStatusDesc | data.wait.confirm.newaccount | 等待确认新账户 |
|
||||
| parsing | true | 文件解析中 |
|
||||
| parsing | false | 文件解析结束 |
|
||||
|
||||
---
|
||||
|
||||
**文档生成时间**: 2026-03-02
|
||||
**文档来源**: 兰溪-流水分析对接_new.docx
|
||||
BIN
doc/对接流水分析/兰溪-流水分析对接.docx
Normal file
BIN
doc/对接流水分析/兰溪-流水分析对接.docx
Normal file
Binary file not shown.
561
doc/对接流水分析/兰溪-流水分析对接.md
Normal file
561
doc/对接流水分析/兰溪-流水分析对接.md
Normal file
@@ -0,0 +1,561 @@
|
||||
# 兰溪-流水分析对接文档
|
||||
|
||||
## 概述
|
||||
|
||||
本文档描述与**见知现金流尽调系统**的对接接口,用于拉取银行流水数据。
|
||||
|
||||
---
|
||||
|
||||
## 1. 新建项目并获取Token
|
||||
|
||||
### 1.1 接口请求地址
|
||||
|
||||
- **测试环境**: `http://158.234.196.5:82/c4c3/account/common/getToken`
|
||||
- **请求方法**: POST
|
||||
|
||||
### 1.2 请求参数说明
|
||||
|
||||
> 接口备注:第三方系统中,点击需要查看的项目向见知现金流尽调系统请求访问token,每个项目的token不同。现金流尽调系统根据 ProjectNo为唯一标识查找项目,如果对应的项目不存在则自动创建项目。注意token使用一次后即失效,再次访问项目需要重新申请。(支持拉取金综和行内流水)
|
||||
|
||||
| 参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 |
|
||||
|--------|--------|----------|----------|----------|
|
||||
| projectNo | test-zjnx-1204 | String | 是 | 项目编号 |
|
||||
| entityName | 浙江农信test1204 | String | 是 | 项目名称 |
|
||||
| userId | test001 | String | 是 | 操作人员编号 |
|
||||
| userName | 测试001 | String | 是 | 操作人员姓名 |
|
||||
| appId | remote_app | String | 是 | 见知提供appId |
|
||||
| appSecretCode | 6ee87a361f29234ad25d7893da9975a9 | String | 是 | 安全码 md5(projectNo + "_" + entityName + "_" + appSecret) |
|
||||
| role | VIEWER | String | 否 | 人员角色(VIEWER:普通用户,READER:只读用户)默认值:VIEWER |
|
||||
| orgCode | 800000 | String | 是 | 行社机构号 |
|
||||
| entityId | ZJNX1234567890 | String | 否 | 企业统信码或个人身份证号 |
|
||||
| xdRelatedPersons | [{"relatedPerson":"上海上水纯净水有限公司","relation":"董事长"}] | String | 否 | 信贷关联人信息 |
|
||||
| jzDataDateId | 0 | String | 否 | 拉取指定日期推送过来的金综链流水,为0时标识不需要拉取金综链流水 |
|
||||
| innerBSStartDateId | 0 | String | 否 | 拉取行内流水开始日期,0:不需要拉取行内流水。流水分析系统根据entityId到数仓中查询行内流水 |
|
||||
| innerBSEndDateId | 0 | String | 否 | 拉取行内流水结束日期,0:不需要拉取行内流水。流水分析系统根据entityId到数仓中查询行内流水 |
|
||||
| analysisType | -1 | String | 是 | 固定值 |
|
||||
| departmentCode | 800111 | String | 是 | 客户经理所属营业部/分理处的机构编码 |
|
||||
|
||||
### 1.3 返回参数说明
|
||||
|
||||
#### 成功响应 (200)
|
||||
|
||||
| 参数名 | 示例值 | 参数类型 | 参数描述 |
|
||||
|--------|--------|----------|----------|
|
||||
| code | 200 | String | 返回码 |
|
||||
| data.token | eyJ0eXAi... | String | token |
|
||||
| data.projectId | 77 | Integer | 见知项目Id |
|
||||
| data.projectNo | test-zjnx-1204 | String | 项目编号 |
|
||||
| data.entityName | 浙江农信test1204 | String | 项目名称 |
|
||||
| data.analysisType | 0 | Integer | 分析类型 |
|
||||
| message | create.token.success | String | 消息 |
|
||||
| status | 200 | String | 状态 |
|
||||
| successResponse | true | Boolean | 成功标识 |
|
||||
|
||||
#### 错误码说明
|
||||
|
||||
| 错误码 | 说明 |
|
||||
|--------|------|
|
||||
| 200 | 请求成功 |
|
||||
| 40100 | 未知异常 |
|
||||
| 40101 | appId错误 |
|
||||
| 40102 | appSecretCode错误 |
|
||||
| 40104 | 可使用项目次数为0,无法创建项目 |
|
||||
| 40105 | 只读模式下无法新建项目 |
|
||||
| 40106 | 错误的分析类型,不在规定的取值范围内 |
|
||||
| 40107 | 当前系统不支持的分析类型 |
|
||||
| 40108 | 当前用户所属行社无权限 |
|
||||
|
||||
### 1.4 返回示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "200",
|
||||
"data": {
|
||||
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
|
||||
"projectId": 77,
|
||||
"projectNo": "test-zjnx-1204",
|
||||
"entityName": "浙江农信test1204",
|
||||
"analysisType": 0
|
||||
},
|
||||
"message": "create.token.success",
|
||||
"status": "200",
|
||||
"successResponse": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 上传文件接口
|
||||
|
||||
### 2.1 接口请求地址
|
||||
|
||||
- **测试环境**: `158.234.196.5:82/c4c3/watson/api/project/remoteUploadSplitFile`
|
||||
- **请求方法**: POST
|
||||
- **请求头**: `X-Xencio-Client-Id: c2017e8d105c435a96f86373635b6a09`
|
||||
|
||||
### 2.2 请求参数说明
|
||||
|
||||
| 参数 | 类型 | 参数名称 | 是否必填 | 说明 |
|
||||
|------|------|----------|----------|------|
|
||||
| groupId | Int | 项目id | 是 | - |
|
||||
| files | File | 上传的文件 | 是 | - |
|
||||
|
||||
### 2.3 响应结果信息
|
||||
|
||||
| 字段 | 类型 | 备注 |
|
||||
|------|------|------|
|
||||
| code | String | 200成功,其他状态码失败 |
|
||||
| data | Object | 列表 |
|
||||
| accountName | - | 主体名称 |
|
||||
| accountNo | - | 账号 |
|
||||
| uploadFileName | - | 文件名称 |
|
||||
| fileSize | - | 文件大小,单位Byte |
|
||||
| status | - | 状态值 |
|
||||
| uploadStatusDesc | - | 文件状态描述 |
|
||||
| bank | - | 所属银行 |
|
||||
| currency | - | 币种 |
|
||||
| accountId | - | 账号id |
|
||||
| logId | - | 文件id |
|
||||
|
||||
> **注意**: `status`等于-5且`uploadStatusDesc`等于`data.wait.confirm.newaccount`表示当前流水文件上传后解析成功。反之则没有成功。
|
||||
|
||||
### 2.4 返回示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "200",
|
||||
"data": {
|
||||
"accountsOfLog": {
|
||||
"13976": [
|
||||
{
|
||||
"bank": "BSX",
|
||||
"accountName": "",
|
||||
"accountNo": "虞海良绍兴银行流水",
|
||||
"currency": "CNY"
|
||||
}
|
||||
]
|
||||
},
|
||||
"uploadLogList": [
|
||||
{
|
||||
"accountNoList": [],
|
||||
"bankName": "BSX",
|
||||
"dataTypeInfo": ["CSV", ","],
|
||||
"downloadFileName": "虞海良绍兴银行流水.csv",
|
||||
"enterpriseNameList": [],
|
||||
"filePackageId": "14b13103010e4d32b5406c764cfe3644",
|
||||
"fileSize": 46724,
|
||||
"fileUploadBy": 448,
|
||||
"fileUploadByUserName": "admin@support.com",
|
||||
"fileUploadTime": "2025-03-12 18:53:29",
|
||||
"leId": 10724,
|
||||
"logId": 13976,
|
||||
"logMeta": "{\"lostHeader\":[],\"balanceAmount\":true}",
|
||||
"logType": "bankstatement",
|
||||
"loginLeId": 10724,
|
||||
"realBankName": "BSX",
|
||||
"rows": 0,
|
||||
"source": "http",
|
||||
"status": -5,
|
||||
"templateName": "BSX_T240925",
|
||||
"totalRecords": 280,
|
||||
"trxDateEndId": 20240905,
|
||||
"trxDateStartId": 20230914,
|
||||
"uploadFileName": "虞海良绍兴银行流水.csv",
|
||||
"uploadStatusDesc": "data.wait.confirm.newaccount"
|
||||
}
|
||||
],
|
||||
"uploadStatus": 1
|
||||
},
|
||||
"status": "200",
|
||||
"successResponse": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 拉取行内流水接口
|
||||
|
||||
### 3.1 接口请求地址
|
||||
|
||||
- **测试环境**: `158.234.196.5:82/c4c3/watson/api/project/getJZFileOrZjrcuFile`
|
||||
- **请求方法**: POST
|
||||
- **请求头**: `X-Xencio-Client-Id: c2017e8d105c435a96f86373635b6a09`
|
||||
|
||||
### 3.2 请求参数说明
|
||||
|
||||
| 参数 | 类型 | 参数名称 | 是否必填 | 说明 |
|
||||
|------|------|----------|----------|------|
|
||||
| groupId | Int | 项目id | 是 | - |
|
||||
| customerNo | String | 客户身份证号 | 是 | - |
|
||||
| dataChannelCode | String | 校验码 | 是 | 固定值: ZJRCU |
|
||||
| requestDateId | Int | 发起请求的时间 | 是 | 当天请求时间 |
|
||||
| dataStartDateId | Int | 拉取开始日期 | 是 | - |
|
||||
| dataEndDateId | Int | 拉取结束日期 | 是 | - |
|
||||
| uploadUserId | int | 柜员号 | 是 | - |
|
||||
|
||||
### 3.3 响应结果信息
|
||||
|
||||
| 序号 | 字段 | 类型 | 备注 |
|
||||
|------|------|------|------|
|
||||
| 1 | code | String | 200成功,其他状态码失败 |
|
||||
| 2 | data | Object | 列表 |
|
||||
|
||||
### 3.4 返回示例(无行内流水)
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "200",
|
||||
"data": {
|
||||
"code": "501014",
|
||||
"message": "无行内流水文件"
|
||||
},
|
||||
"status": "200",
|
||||
"successResponse": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 判断文件是否解析结束
|
||||
|
||||
### 4.1 接口请求地址
|
||||
|
||||
- **测试环境**: `http://158.234.196.5:82/c4c3/watson/api/project/upload/getpendings`
|
||||
- **请求方法**: POST
|
||||
- **请求头**: `X-Xencio-Client-Id: c2017e8d105c435a96f86373635b6a09`
|
||||
|
||||
### 4.2 请求参数说明
|
||||
|
||||
| 参数 | 类型 | 参数名称 | 是否必填 | 说明 |
|
||||
|------|------|----------|----------|------|
|
||||
| groupId | Int | 项目id | 是 | - |
|
||||
| inprogressList | String | 文件id | 是 | - |
|
||||
|
||||
### 4.3 响应结果信息
|
||||
|
||||
| 序号 | 字段 | 类型 | 备注 |
|
||||
|------|------|------|------|
|
||||
| 1 | code | String | 200成功,其他状态码失败 |
|
||||
| 2 | data | Object | 列表 |
|
||||
| 3 | uploadFileName | - | 上传文件名称 |
|
||||
| 4 | status | - | 文件解析后状态值 |
|
||||
| 5 | uploadStatusDesc | - | 文件解析后状态描述 |
|
||||
| 6 | parsing | - | 文件解析状态,true表示解析中,false表示解析结束 |
|
||||
|
||||
> **注意**: 文件解析有个处理过程,`parsing`为false表示解析结束,可以轮询调用此接口,`status`等于-5且`uploadStatusDesc`等于`data.wait.confirm.newaccount`表示文件解析成功。
|
||||
|
||||
### 4.4 返回示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "200",
|
||||
"data": {
|
||||
"parsing": false,
|
||||
"pendingList": [
|
||||
{
|
||||
"accountNoList": [],
|
||||
"bankName": "ZJRCU",
|
||||
"dataTypeInfo": ["CSV", ","],
|
||||
"downloadFileName": "230902199012261247_20260201_20260201_1772096608615.csv",
|
||||
"enterpriseNameList": [],
|
||||
"filePackageId": "cde6c7cf5cab48e8892f0c1c36b2aa7d",
|
||||
"fileSize": 53101,
|
||||
"fileUploadBy": 448,
|
||||
"fileUploadByUserName": "admin@support.com",
|
||||
"fileUploadTime": "2026-02-27 09:50:18",
|
||||
"isSplit": 0,
|
||||
"leId": 16210,
|
||||
"logId": 19116,
|
||||
"logMeta": "{\"lostHeader\":[],\"balanceAmount\":true}",
|
||||
"logType": "bankstatement",
|
||||
"loginLeId": 16210,
|
||||
"lostHeader": [],
|
||||
"realBankName": "ZJRCU",
|
||||
"rows": 0,
|
||||
"source": "http",
|
||||
"status": -5,
|
||||
"templateName": "ZJRCU_T251114",
|
||||
"totalRecords": 131,
|
||||
"trxDateEndId": 20240228,
|
||||
"trxDateStartId": 20240201,
|
||||
"uploadFileName": "230902199012261247_20260201_20260201_1772096608615.csv",
|
||||
"uploadStatusDesc": "data.wait.confirm.newaccount"
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "200",
|
||||
"successResponse": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 生成尽调报告接口
|
||||
|
||||
### 5.1 接口请求地址
|
||||
|
||||
- **测试环境**: `158.234.196.5:82/c4c3/watson/api/project/confirmStageUploadLogs`
|
||||
- **请求方法**: POST
|
||||
- **请求头**: `X-Xencio-Client-Id: c2017e8d105c435a96f86373635b6a09`
|
||||
|
||||
### 5.2 请求参数说明
|
||||
|
||||
| 参数 | 类型 | 参数名称 | 是否必填 | 说明 |
|
||||
|------|------|----------|----------|------|
|
||||
| groupId | Int | 项目id | 是 | - |
|
||||
| logIds | Array | 文件id数组 | 是 | 上传几个文件就用几个 |
|
||||
| userLogin | Int | 登录柜员号 | 是 | - |
|
||||
|
||||
### 5.3 响应结果信息
|
||||
|
||||
| 序号 | 字段 | 类型 | 备注 |
|
||||
|------|------|------|------|
|
||||
| 1 | Code | String | 200成功,其他状态码失败 |
|
||||
| 2 | Data | Object | 列表 |
|
||||
|
||||
### 5.4 返回示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "200",
|
||||
"data": {
|
||||
"message": "upload.confirm.ok"
|
||||
},
|
||||
"status": "200",
|
||||
"successResponse": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 判断尽调报告是否生成
|
||||
|
||||
### 6.1 接口请求地址
|
||||
|
||||
- **测试环境**: `158.234.196.5:82/c4c3/watson/api/project/upload/getallpendings?groupId=#{groupId}`
|
||||
- **请求方法**: GET
|
||||
- **请求头**: `X-Xencio-Client-Id: c2017e8d105c435a96f86373635b6a09`
|
||||
|
||||
### 6.2 请求参数说明
|
||||
|
||||
| 参数 | 类型 | 参数名称 | 是否必填 | 说明 |
|
||||
|------|------|----------|----------|------|
|
||||
| groupId | Int | 项目id | 是 | - |
|
||||
|
||||
### 6.3 响应结果信息
|
||||
|
||||
| 序号 | 字段 | 类型 | 备注 |
|
||||
|------|------|------|------|
|
||||
| 1 | code | String | 200成功,其他状态码失败 |
|
||||
| 2 | data | Object | 列表 |
|
||||
|
||||
> **注意**: 生成尽调报告有个处理过程,`pendingList`为[]表示处理结束,可以轮询调用此接口。
|
||||
|
||||
### 6.4 返回示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "200",
|
||||
"data": {
|
||||
"pendingList": []
|
||||
},
|
||||
"status": "200",
|
||||
"successResponse": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 获取流水列表并存储到兰溪本地
|
||||
|
||||
### 7.1 接口请求地址
|
||||
|
||||
- **测试环境**: `158.234.196.5:82/c4c3/watson/api/project/upload/getBankStatement`
|
||||
- **请求方法**: POST
|
||||
- **请求头**: `X-Xencio-Client-Id: c2017e8d105c435a96f86373635b6a09`
|
||||
|
||||
### 7.2 请求参数说明
|
||||
|
||||
| 参数 | 类型 | 参数名称 | 是否必填 | 说明 |
|
||||
|------|------|----------|----------|------|
|
||||
| groupId | Int | 项目id | 是 | - |
|
||||
| logId | Int | 文件id | 是 | - |
|
||||
| pageNow | Int | 当前页码 | 是 | - |
|
||||
| pageSize | Int | 查询条数 | 是 | - |
|
||||
|
||||
### 7.3 响应结果信息
|
||||
|
||||
| 序号 | 字段 | 类型 | 备注 |
|
||||
|------|------|------|------|
|
||||
| 1 | code | String | 200成功,其他状态码失败 |
|
||||
| 2 | data | Object | 列表 |
|
||||
| 3 | bankStatementList | - | 流水列表 |
|
||||
| 4 | pageable | - | 分页参数 |
|
||||
| 5 | searchable | - | 查询参数 |
|
||||
|
||||
### 7.4 流水字段说明
|
||||
|
||||
| 字段名 | 说明 | 示例值 |
|
||||
|--------|------|--------|
|
||||
| accountId | 账户ID | 0 |
|
||||
| accountMaskNo | 账号 | 6228580199062321798 |
|
||||
| accountingDate | 记账日期 | 2025-02-03 |
|
||||
| accountingDateId | 记账日期ID | 20250203 |
|
||||
| balanceAmount | 余额 | 85688.37 |
|
||||
| bank | 银行代码 | AI |
|
||||
| bankComments | 银行备注 | - |
|
||||
| bankStatementId | 流水ID | 4585279 |
|
||||
| catalogName | 交易名称 | 收单收入 |
|
||||
| crAmount | 贷方金额 | 290 |
|
||||
| currency | 币种 | CNY |
|
||||
| customerAccountMaskNo | 客户账号 | 80100001471621000100 |
|
||||
| customerAccountName | 客户账户名 | 系统内清算资金往来-全渠道收单平台 |
|
||||
| customerName | 客户名称 | 系统内清算资金往来-全渠道收单平台 |
|
||||
| drAmount | 借方金额 | 0 |
|
||||
| leName | 企业名称 | 徐设华 |
|
||||
| transAmount | 交易金额 | 290 |
|
||||
| transFlag | 交易标志 | R |
|
||||
| transTypeName | 交易名称 | 收单收入 |
|
||||
| trxDate | 交易日期 | 2025-02-03 00:00:00 |
|
||||
| userMemo | 用户备注 | 收单 |
|
||||
|
||||
### 7.5 返回示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "200",
|
||||
"data": {
|
||||
"bankStatementList": [
|
||||
{
|
||||
"accountId": 0,
|
||||
"accountMaskNo": "6228580199062321798",
|
||||
"accountingDate": "2025-02-03",
|
||||
"accountingDateId": 20250203,
|
||||
"archivingFlag": 0,
|
||||
"attachments": 0,
|
||||
"balanceAmount": 85688.37,
|
||||
"bank": "AI",
|
||||
"bankComments": "",
|
||||
"bankStatementId": 4585279,
|
||||
"bankTrxNumber": "",
|
||||
"cashType": "",
|
||||
"catalogName": "收单收入",
|
||||
"commentsNum": 0,
|
||||
"crAmount": 290,
|
||||
"currency": "CNY",
|
||||
"customNoteCount": 0,
|
||||
"customerAccountMaskNo": "80100001471621000100",
|
||||
"customerAccountName": "系统内清算资金往来-全渠道收单平台",
|
||||
"customerId": 0,
|
||||
"customerName": "系统内清算资金往来-全渠道收单平台",
|
||||
"customerReference": "",
|
||||
"downPaymentFlag": 0,
|
||||
"drAmount": 0,
|
||||
"hasCustomNote": 0,
|
||||
"internalFlag": 0,
|
||||
"isMarked": 0,
|
||||
"leId": 16260,
|
||||
"leName": "徐设华",
|
||||
"sourceCatalogId": 405625,
|
||||
"split": 0,
|
||||
"subBankstatementId": 0,
|
||||
"toDoFlag": 0,
|
||||
"transAmount": 290,
|
||||
"transFlag": "R",
|
||||
"transTypeId": 405625,
|
||||
"transTypeName": "收单收入",
|
||||
"transformAmount": 290,
|
||||
"transformCrAmount": 290,
|
||||
"transformDrAmount": 0,
|
||||
"transfromBalanceAmount": 85688.37,
|
||||
"trxBalance": 0,
|
||||
"trxDate": "2025-02-03 00:00:00",
|
||||
"trxFlag": "R",
|
||||
"userMemo": "收单"
|
||||
}
|
||||
],
|
||||
"pageable": {
|
||||
"hasNext": true,
|
||||
"hasPre": false,
|
||||
"isFirst": true,
|
||||
"isLast": false,
|
||||
"pageNow": 1,
|
||||
"pageSize": 1,
|
||||
"startPos": 0,
|
||||
"totalCount": 3392,
|
||||
"totalPageCount": 3392
|
||||
},
|
||||
"searchable": {
|
||||
"appInput": 0,
|
||||
"dayFromId": 0,
|
||||
"dayToId": 0,
|
||||
"endDateId": 0,
|
||||
"enterpriseId": 0,
|
||||
"groupTypeId": 0,
|
||||
"logId": 19060,
|
||||
"pageNow": 1,
|
||||
"pageSize": 1,
|
||||
"showDownPayment": 0,
|
||||
"startDateId": 0,
|
||||
"trxAmount": 0,
|
||||
"trxTypeId": 0,
|
||||
"uploadFromDateId": 0,
|
||||
"uploadToDateId": 0,
|
||||
"useForBsSearch": 0,
|
||||
"useNameExactMatching": 0,
|
||||
"withOrderBy": true
|
||||
}
|
||||
},
|
||||
"status": "200",
|
||||
"successResponse": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 接口调用流程
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 1. 初始化调用 /account/common/getToken 接口创建项目 │
|
||||
│ ↓ │
|
||||
│ 2. 调用 /remoteUploadSplitFile 接口上传文件 │
|
||||
│ 或调用 /getJZFileOrZjrcuFile 拉取行内流水 │
|
||||
│ ↓ │
|
||||
│ 3. 调用 /getpendings 获取文件解析状态 │
|
||||
│ - parsing=true 时,间隔1s轮询 │
|
||||
│ - parsing=false 且 status=-5 表示解析成功 │
|
||||
│ ↓ │
|
||||
│ 4. 调用 /confirmStageUploadLogs 接口生成尽调报告 │
|
||||
│ ↓ │
|
||||
│ 5. 调用 /getallpending 检查尽调报告生成状态 │
|
||||
│ - pendingList 不为空时,间隔1s轮询 │
|
||||
│ - pendingList=[] 表示生成完成 │
|
||||
│ ↓ │
|
||||
│ 6. 调用 /getBankStatement 接口获取流水数据存储到兰溪本地 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 生产环境配置
|
||||
|
||||
| 配置项 | 值 |
|
||||
|--------|-----|
|
||||
| 生产IP | 64.202.32.176 |
|
||||
| 生产X-Xencio-Client-Id | 通过接口获取 |
|
||||
|
||||
### 获取生产环境 Client-Id
|
||||
|
||||
```
|
||||
GET http://64.202.32.176/c4c3/watson/api/common/GenerateAccessKey?userLogin={流水分析平台登录柜员号}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附录:公共请求头
|
||||
|
||||
| 请求头 | 值 | 说明 |
|
||||
|--------|-----|------|
|
||||
| X-Xencio-Client-Id | c2017e8d105c435a96f86373635b6a09 | 测试环境固定值 |
|
||||
| Content-Type | application/json | POST请求 |
|
||||
BIN
doc/对接流水分析/兰溪-流水分析对接_new.docx
Normal file
BIN
doc/对接流水分析/兰溪-流水分析对接_new.docx
Normal file
Binary file not shown.
282
docs/plans/2026-02-28-database-migration-design.md
Normal file
282
docs/plans/2026-02-28-database-migration-design.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# 数据库迁移设计文档
|
||||
|
||||
## 概述
|
||||
|
||||
将 CCDI 纪检初核系统的开发环境数据库完整迁移到生产环境,包括所有表结构和数据的导出与导入。
|
||||
|
||||
## 需求分析
|
||||
|
||||
### 迁移目标
|
||||
- **源数据库**: 116.62.17.81:3306/ccdi
|
||||
- **目标环境**: 全新的生产数据库(空库)
|
||||
- **迁移范围**: 所有表的结构和数据
|
||||
|
||||
### 关键要求
|
||||
1. 表结构和数据分离导出(两个独立文件)
|
||||
2. 只导出表,不包括视图、存储过程、触发器等
|
||||
3. 完整导出所有数据,不需要脱敏
|
||||
4. 确保字符集正确,避免乱码问题
|
||||
5. 使用 mysqldump 命令导出
|
||||
6. 提供自动化脚本简化操作
|
||||
|
||||
## 技术方案
|
||||
|
||||
### 导出工具
|
||||
使用 MySQL 官方工具 `mysqldump` 进行导出,优势:
|
||||
- 标准化工具,兼容性最佳
|
||||
- 性能优秀,适合大数据库
|
||||
- 生成的 SQL 文件通用性强
|
||||
|
||||
### 字符集处理
|
||||
- **字符集**: utf8mb4(支持完整 Unicode,包括 emoji)
|
||||
- **排序规则**: utf8mb4_general_ci
|
||||
- **客户端字符集**: utf8mb4
|
||||
|
||||
关键措施:
|
||||
1. mysqldump 命令添加 `--default-character-set=utf8mb4` 参数
|
||||
2. SQL 文件头部添加字符集声明语句
|
||||
3. 导入时指定字符集参数
|
||||
4. 导入后验证中文数据正确性
|
||||
|
||||
## 导出设计
|
||||
|
||||
### 文件组织
|
||||
```
|
||||
ccdi/
|
||||
├── export_database.sh # 自动化脚本
|
||||
├── db_config.conf.template # 配置模板
|
||||
├── db_config.conf # 实际配置(不纳入版本控制)
|
||||
└── doc/
|
||||
└── database/
|
||||
└── backup/
|
||||
├── ccdi_structure.sql # 表结构文件
|
||||
├── ccdi_data.sql # 数据文件
|
||||
└── export_guide.md # 操作指南
|
||||
```
|
||||
|
||||
### 表结构导出命令
|
||||
```bash
|
||||
mysqldump -h 116.62.17.81 -P 3306 -u root -p \
|
||||
--no-data \
|
||||
--skip-triggers \
|
||||
--skip-add-drop-table \
|
||||
--default-character-set=utf8mb4 \
|
||||
--single-transaction \
|
||||
ccdi > ccdi_structure.sql
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
- `--no-data`: 只导出表结构,不导出数据
|
||||
- `--skip-triggers`: 跳过触发器
|
||||
- `--skip-add-drop-table`: 不添加 DROP TABLE 语句(避免误删)
|
||||
- `--default-character-set=utf8mb4`: 指定字符集
|
||||
- `--single-transaction`: InnoDB 表一致性导出,不锁表
|
||||
|
||||
### 数据导出命令
|
||||
```bash
|
||||
mysqldump -h 116.62.17.81 -P 3306 -u root -p \
|
||||
--no-create-info \
|
||||
--skip-triggers \
|
||||
--default-character-set=utf8mb4 \
|
||||
--single-transaction \
|
||||
--complete-insert \
|
||||
--extended-insert \
|
||||
ccdi > ccdi_data.sql
|
||||
```
|
||||
|
||||
**参数说明**:
|
||||
- `--no-create-info`: 只导出数据,不导出表结构
|
||||
- `--complete-insert`: INSERT 语句包含列名
|
||||
- `--extended-insert`: 使用多行 INSERT,提高导入效率
|
||||
|
||||
### SQL 文件字符集声明
|
||||
```sql
|
||||
SET NAMES utf8mb4;
|
||||
SET CHARACTER SET utf8mb4;
|
||||
SET GLOBAL character_set_client=utf8mb4;
|
||||
SET GLOBAL character_set_connection=utf8mb4;
|
||||
SET GLOBAL character_set_results=utf8mb4;
|
||||
```
|
||||
|
||||
## 导入设计
|
||||
|
||||
### 导入顺序
|
||||
1. 先导入表结构:`ccdi_structure.sql`
|
||||
2. 再导入数据:`ccdi_data.sql`
|
||||
|
||||
### 导入命令
|
||||
```bash
|
||||
# 导入表结构
|
||||
mysql -h 生产环境IP -P 3306 -u 用户名 -p \
|
||||
--default-character-set=utf8mb4 \
|
||||
数据库名 < ccdi_structure.sql
|
||||
|
||||
# 导入数据
|
||||
mysql -h 生产环境IP -P 3306 -u 用户名 -p \
|
||||
--default-character-set=utf8mb4 \
|
||||
数据库名 < ccdi_data.sql
|
||||
```
|
||||
|
||||
### 前置条件
|
||||
1. 目标数据库已创建(如:`CREATE DATABASE ccdi CHARACTER SET utf8mb4;`)
|
||||
2. 目标用户有足够权限
|
||||
3. 磁盘空间充足
|
||||
|
||||
## 自动化脚本设计
|
||||
|
||||
### 脚本功能
|
||||
- **导出模式**: `./export_database.sh export`
|
||||
- 检查 mysqldump 命令可用性
|
||||
- 创建备份目录
|
||||
- 执行结构导出和数据导出
|
||||
- 添加字符集声明到文件头部
|
||||
- 验证文件生成
|
||||
- 记录操作日志
|
||||
|
||||
- **导入模式**: `./export_database.sh import [production|test]`
|
||||
- 读取配置文件获取目标环境信息
|
||||
- 检查目标数据库连接
|
||||
- 依次导入结构和数据文件
|
||||
- 验证导入结果
|
||||
- 记录操作日志
|
||||
|
||||
### 配置文件设计
|
||||
```bash
|
||||
# 源数据库配置(开发环境)
|
||||
SOURCE_DB_HOST=116.62.17.81
|
||||
SOURCE_DB_PORT=3306
|
||||
SOURCE_DB_USER=root
|
||||
SOURCE_DB_PASS=Kfcx@1234
|
||||
SOURCE_DB_NAME=ccdi
|
||||
|
||||
# 生产环境数据库配置
|
||||
PROD_DB_HOST=生产环境IP
|
||||
PROD_DB_PORT=3306
|
||||
PROD_DB_USER=生产环境用户名
|
||||
PROD_DB_PASS=生产环境密码
|
||||
PROD_DB_NAME=ccdi
|
||||
|
||||
# 测试环境数据库配置(可选)
|
||||
TEST_DB_HOST=测试环境IP
|
||||
TEST_DB_PORT=3306
|
||||
TEST_DB_USER=测试环境用户名
|
||||
TEST_DB_PASS=测试环境密码
|
||||
TEST_DB_NAME=ccdi
|
||||
|
||||
# 导出文件配置
|
||||
BACKUP_DIR=doc/database/backup
|
||||
STRUCTURE_FILE=ccdi_structure.sql
|
||||
DATA_FILE=ccdi_data.sql
|
||||
```
|
||||
|
||||
### 安全措施
|
||||
1. `db_config.conf` 添加到 `.gitignore`
|
||||
2. 提供 `db_config.conf.template` 模板文件
|
||||
3. 首次运行时检测配置文件,提示用户填写
|
||||
|
||||
## 验证测试
|
||||
|
||||
### 导出验证
|
||||
1. 检查生成的 SQL 文件大小是否合理
|
||||
2. 检查文件头部是否包含字符集声明
|
||||
3. 随机抽取数据检查是否有乱码
|
||||
4. 统计表数量和数据行数
|
||||
|
||||
### 导入验证
|
||||
1. 在测试环境先进行导入测试
|
||||
2. 对比源数据库和目标数据库的表数量
|
||||
3. 抽查关键表的数据行数
|
||||
4. 查询包含中文的数据验证编码正确性
|
||||
5. 使用 `SHOW CREATE TABLE` 检查表字符集
|
||||
|
||||
### 验证命令
|
||||
```sql
|
||||
-- 查看数据库字符集
|
||||
SHOW CREATE DATABASE ccdi;
|
||||
|
||||
-- 查看表数量
|
||||
SELECT COUNT(*) FROM information_schema.tables
|
||||
WHERE table_schema='ccdi';
|
||||
|
||||
-- 查看各表行数
|
||||
SELECT table_name, table_rows
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema='ccdi'
|
||||
ORDER BY table_rows DESC;
|
||||
|
||||
-- 检查表字符集
|
||||
SHOW CREATE TABLE sys_user;
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 常见问题
|
||||
1. **字符集乱码**
|
||||
- 原因:未指定字符集参数
|
||||
- 解决:确保所有命令都添加 `--default-character-set=utf8mb4`
|
||||
|
||||
2. **导入失败**
|
||||
- 原因:外键约束冲突
|
||||
- 解决:导入前临时禁用外键检查 `SET FOREIGN_KEY_CHECKS=0;`
|
||||
|
||||
3. **连接超时**
|
||||
- 原因:数据库过大或网络慢
|
||||
- 解决:添加 `--max_allowed_packet=512M` 参数
|
||||
|
||||
4. **权限不足**
|
||||
- 原因:用户权限不够
|
||||
- 解决:使用 root 用户或授予足够权限
|
||||
|
||||
## 操作流程
|
||||
|
||||
### 完整迁移流程
|
||||
1. 配置 `db_config.conf` 文件
|
||||
2. 执行导出:`./export_database.sh export`
|
||||
3. 验证导出文件正确性
|
||||
4. 在测试环境验证导入:`./export_database.sh import test`
|
||||
5. 验证测试环境数据完整性
|
||||
6. 在生产环境执行导入:`./export_database.sh import production`
|
||||
7. 验证生产环境数据完整性
|
||||
8. 应用程序连接测试
|
||||
|
||||
### 回滚方案
|
||||
保留源数据库,如迁移失败可继续使用源数据库,重新执行迁移流程。
|
||||
|
||||
## 交付物
|
||||
|
||||
1. **自动化脚本**: `export_database.sh`
|
||||
2. **配置模板**: `db_config.conf.template`
|
||||
3. **表结构文件**: `doc/database/backup/ccdi_structure.sql`
|
||||
4. **数据文件**: `doc/database/backup/ccdi_data.sql`
|
||||
5. **操作指南**: `doc/database/backup/export_guide.md`
|
||||
6. **设计文档**: `docs/plans/2026-02-28-database-migration-design.md`
|
||||
|
||||
## 时间估算
|
||||
|
||||
- 脚本开发:30分钟
|
||||
- 导出执行:10-30分钟(取决于数据量)
|
||||
- 测试环境导入验证:10-30分钟
|
||||
- 生产环境导入:10-30分钟
|
||||
- 验证测试:10分钟
|
||||
|
||||
**总计**: 约1.5-2小时
|
||||
|
||||
## 风险评估
|
||||
|
||||
| 风险项 | 等级 | 缓解措施 |
|
||||
|--------|------|----------|
|
||||
| 数据量过大导致超时 | 中 | 添加 max_allowed_packet 参数,分批导出 |
|
||||
| 字符集乱码 | 高 | 严格遵循字符集规范,导入后验证 |
|
||||
| 网络中断 | 低 | 本地保存SQL文件,可重复导入 |
|
||||
| 生产环境数据冲突 | 无 | 全新空库,无冲突风险 |
|
||||
| 权限问题 | 低 | 使用 root 用户或确保权限充足 |
|
||||
|
||||
## 成功标准
|
||||
|
||||
1. ✅ 所有表结构成功导出,无遗漏
|
||||
2. ✅ 所有表数据成功导出,无丢失
|
||||
3. ✅ SQL 文件字符集正确,无乱码
|
||||
4. ✅ 测试环境导入成功,数据完整
|
||||
5. ✅ 生产环境导入成功,数据完整
|
||||
6. ✅ 中文数据正确显示,编码无误
|
||||
7. ✅ 应用程序可正常连接和操作数据库
|
||||
1248
docs/plans/2026-02-28-database-migration.md
Normal file
1248
docs/plans/2026-02-28-database-migration.md
Normal file
File diff suppressed because it is too large
Load Diff
826
docs/plans/2026-03-02-lsfx-integration-design.md
Normal file
826
docs/plans/2026-03-02-lsfx-integration-design.md
Normal file
@@ -0,0 +1,826 @@
|
||||
# 流水分析平台对接设计文档
|
||||
|
||||
## 文档信息
|
||||
|
||||
- **创建日期**: 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)
|
||||
1075
docs/plans/2026-03-02-lsfx-integration.md
Normal file
1075
docs/plans/2026-03-02-lsfx-integration.md
Normal file
File diff suppressed because it is too large
Load Diff
572
docs/plans/2026-03-02-lsfx-mock-server-design.md
Normal file
572
docs/plans/2026-03-02-lsfx-mock-server-design.md
Normal file
@@ -0,0 +1,572 @@
|
||||
# 流水分析 Mock 服务器设计方案
|
||||
|
||||
**创建日期**: 2026-03-02
|
||||
**作者**: Claude Code
|
||||
|
||||
## 项目概述
|
||||
|
||||
### 背景
|
||||
当前项目需要与流水分析平台进行接口对接,但在开发和测试过程中,依赖外部真实服务存在以下问题:
|
||||
- 网络连接不稳定,影响测试效率
|
||||
- 无法控制返回数据,难以测试各种场景
|
||||
- 无法模拟错误场景和边界情况
|
||||
- 团队成员无法共享测试环境
|
||||
|
||||
### 解决方案
|
||||
开发一个独立的 Mock 服务器,基于 Python + FastAPI 技术栈,模拟流水分析平台的 7 个核心接口,支持:
|
||||
- 配置文件驱动的响应数据
|
||||
- 文件上传解析延迟模拟
|
||||
- 错误场景触发机制
|
||||
- 自动生成的 API 文档
|
||||
|
||||
### 技术选型
|
||||
|
||||
| 技术组件 | 选择 | 理由 |
|
||||
|---------|------|------|
|
||||
| Web框架 | FastAPI | 现代异步框架,自动生成API文档,强类型支持 |
|
||||
| 数据验证 | Pydantic | 与FastAPI原生集成,类型提示清晰 |
|
||||
| 配置管理 | JSON文件 | 易于修改,非开发人员也能调整测试数据 |
|
||||
| 状态存储 | 内存字典 | 轻量级,重启清空,适合Mock场景 |
|
||||
|
||||
---
|
||||
|
||||
## 整体架构
|
||||
|
||||
```
|
||||
lsfx-mock-server/
|
||||
├── main.py # 应用入口
|
||||
├── config/
|
||||
│ ├── settings.py # 全局配置
|
||||
│ └── responses/ # 响应模板配置文件
|
||||
│ ├── token.json
|
||||
│ ├── upload.json
|
||||
│ ├── parse_status.json
|
||||
│ └── bank_statement.json
|
||||
├── models/
|
||||
│ ├── request.py # 请求模型(Pydantic)
|
||||
│ └── response.py # 响应模型(Pydantic)
|
||||
├── services/
|
||||
│ ├── token_service.py # Token管理
|
||||
│ ├── file_service.py # 文件上传和解析模拟
|
||||
│ └── statement_service.py # 流水数据管理
|
||||
├── routers/
|
||||
│ └── api.py # 所有API路由
|
||||
├── utils/
|
||||
│ ├── response_builder.py # 响应构建器
|
||||
│ └── error_simulator.py # 错误场景模拟
|
||||
└── requirements.txt
|
||||
```
|
||||
|
||||
### 核心设计思想
|
||||
1. **配置驱动** - 所有响应数据在JSON配置文件中,方便修改
|
||||
2. **内存状态管理** - 使用全局字典存储运行时状态(tokens、文件记录等)
|
||||
3. **异步任务** - 使用FastAPI后台任务模拟文件解析延迟
|
||||
4. **错误标记识别** - 检测请求参数中的特殊标记触发错误响应
|
||||
|
||||
---
|
||||
|
||||
## 数据模型设计
|
||||
|
||||
### 请求模型
|
||||
|
||||
对应Java项目中的DTO类:
|
||||
|
||||
```python
|
||||
# models/request.py
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
class GetTokenRequest(BaseModel):
|
||||
projectNo: str
|
||||
entityName: str
|
||||
userId: str
|
||||
userName: str
|
||||
orgCode: str
|
||||
entityId: Optional[str] = None
|
||||
xdRelatedPersons: Optional[str] = None
|
||||
jzDataDateId: Optional[str] = "0"
|
||||
innerBSStartDateId: Optional[str] = "0"
|
||||
innerBSEndDateId: Optional[str] = "0"
|
||||
analysisType: Optional[int] = -1
|
||||
departmentCode: Optional[str] = None
|
||||
|
||||
class UploadFileRequest(BaseModel):
|
||||
groupId: int
|
||||
|
||||
class FetchInnerFlowRequest(BaseModel):
|
||||
groupId: int
|
||||
customerNo: str
|
||||
dataChannelCode: str
|
||||
requestDateId: int
|
||||
dataStartDateId: int
|
||||
dataEndDateId: int
|
||||
uploadUserId: int
|
||||
|
||||
class CheckParseStatusRequest(BaseModel):
|
||||
groupId: int
|
||||
inprogressList: str
|
||||
|
||||
class GetBankStatementRequest(BaseModel):
|
||||
groupId: int
|
||||
logId: int
|
||||
pageNow: int
|
||||
pageSize: int
|
||||
```
|
||||
|
||||
### 响应模型
|
||||
|
||||
对应Java项目中的VO类:
|
||||
|
||||
```python
|
||||
# models/response.py
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
class TokenData(BaseModel):
|
||||
token: str
|
||||
projectId: int
|
||||
projectNo: str
|
||||
entityName: str
|
||||
analysisType: int
|
||||
|
||||
class GetTokenResponse(BaseModel):
|
||||
code: str = "200"
|
||||
data: Optional[TokenData] = None
|
||||
message: str = "create.token.success"
|
||||
status: str = "200"
|
||||
successResponse: bool = True
|
||||
|
||||
# 其他响应模型类似...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 核心业务逻辑
|
||||
|
||||
### 文件解析延迟模拟
|
||||
|
||||
**实现机制:**
|
||||
1. 上传接口立即返回,状态为"解析中"
|
||||
2. 使用FastAPI的BackgroundTasks在后台延迟执行
|
||||
3. 延迟3-5秒后更新状态为"解析完成"
|
||||
4. 轮询检查接口返回当前解析状态
|
||||
|
||||
```python
|
||||
# services/file_service.py
|
||||
class FileService:
|
||||
def __init__(self):
|
||||
self.file_records: Dict[int, Dict] = {}
|
||||
self.parsing_status: Dict[int, bool] = {}
|
||||
|
||||
async def upload_file(self, group_id: int, file, background_tasks: BackgroundTasks):
|
||||
log_id = generate_log_id()
|
||||
|
||||
# 立即存储记录,标记为解析中
|
||||
self.file_records[log_id] = {
|
||||
"logId": log_id,
|
||||
"status": -5,
|
||||
"uploadStatusDesc": "parsing",
|
||||
...
|
||||
}
|
||||
self.parsing_status[log_id] = True
|
||||
|
||||
# 启动后台任务,延迟4秒后完成解析
|
||||
background_tasks.add_task(
|
||||
self._simulate_parsing,
|
||||
log_id,
|
||||
delay_seconds=4
|
||||
)
|
||||
|
||||
return log_id
|
||||
|
||||
def _simulate_parsing(self, log_id: int, delay_seconds: int):
|
||||
time.sleep(delay_seconds)
|
||||
if log_id in self.file_records:
|
||||
self.file_records[log_id]["status"] = -5
|
||||
self.file_records[log_id]["uploadStatusDesc"] = "data.wait.confirm.newaccount"
|
||||
self.parsing_status[log_id] = False
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误场景模拟机制
|
||||
|
||||
### 错误触发规则
|
||||
|
||||
通过请求参数中的特殊标记触发对应的错误响应:
|
||||
|
||||
**错误码映射表:**
|
||||
```python
|
||||
ERROR_CODES = {
|
||||
"40101": {"code": "40101", "message": "appId错误"},
|
||||
"40102": {"code": "40102", "message": "appSecretCode错误"},
|
||||
"40104": {"code": "40104", "message": "可使用项目次数为0,无法创建项目"},
|
||||
"40105": {"code": "40105", "message": "只读模式下无法新建项目"},
|
||||
"40106": {"code": "40106", "message": "错误的分析类型,不在规定的取值范围内"},
|
||||
"40107": {"code": "40107", "message": "当前系统不支持的分析类型"},
|
||||
"40108": {"code": "40108", "message": "当前用户所属行社无权限"},
|
||||
"501014": {"code": "501014", "message": "无行内流水文件"},
|
||||
}
|
||||
```
|
||||
|
||||
**检测逻辑:**
|
||||
```python
|
||||
@staticmethod
|
||||
def detect_error_marker(value: str) -> Optional[str]:
|
||||
"""检测字符串中的错误标记
|
||||
|
||||
规则:如果字符串包含 error_XXXX,则返回 XXXX
|
||||
例如:
|
||||
- "project_error_40101" -> "40101"
|
||||
- "test_error_501014" -> "501014"
|
||||
"""
|
||||
if not value:
|
||||
return None
|
||||
|
||||
import re
|
||||
pattern = r'error_(\d+)'
|
||||
match = re.search(pattern, value)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
```
|
||||
|
||||
**使用示例:**
|
||||
```python
|
||||
# 在服务中使用
|
||||
def get_token(request: GetTokenRequest):
|
||||
error_code = ErrorSimulator.detect_error_marker(request.projectNo)
|
||||
if error_code:
|
||||
return ErrorSimulator.build_error_response(error_code)
|
||||
|
||||
# 正常流程...
|
||||
```
|
||||
|
||||
**测试方式:**
|
||||
```python
|
||||
# 触发 40101 错误
|
||||
request_data = {
|
||||
"projectNo": "test_project_error_40101", # 包含错误标记
|
||||
"entityName": "测试企业",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置文件结构
|
||||
|
||||
### 响应模板配置
|
||||
|
||||
```json
|
||||
// config/responses/token.json
|
||||
{
|
||||
"success_response": {
|
||||
"code": "200",
|
||||
"data": {
|
||||
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.mock_token_{project_id}",
|
||||
"projectId": "{project_id}",
|
||||
"projectNo": "{project_no}",
|
||||
"entityName": "{entity_name}",
|
||||
"analysisType": 0
|
||||
},
|
||||
"message": "create.token.success",
|
||||
"status": "200",
|
||||
"successResponse": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// config/responses/upload.json
|
||||
{
|
||||
"success_response": {
|
||||
"code": "200",
|
||||
"data": {
|
||||
"accountsOfLog": {},
|
||||
"uploadLogList": [
|
||||
{
|
||||
"logId": "{log_id}",
|
||||
"status": -5,
|
||||
"uploadStatusDesc": "data.wait.confirm.newaccount",
|
||||
...
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 全局配置
|
||||
|
||||
```python
|
||||
# config/settings.py
|
||||
from pydantic import BaseSettings
|
||||
|
||||
class Settings(BaseSettings):
|
||||
APP_NAME: str = "流水分析Mock服务"
|
||||
APP_VERSION: str = "1.0.0"
|
||||
DEBUG: bool = True
|
||||
HOST: str = "0.0.0.0"
|
||||
PORT: int = 8000
|
||||
|
||||
# 模拟配置
|
||||
PARSE_DELAY_SECONDS: int = 4
|
||||
MAX_FILE_SIZE: int = 10485760 # 10MB
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
||||
settings = Settings()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 路由实现
|
||||
|
||||
### 核心接口
|
||||
|
||||
```python
|
||||
# routers/api.py
|
||||
from fastapi import APIRouter, BackgroundTasks, UploadFile, File
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# 接口1:获取Token
|
||||
@router.post("/account/common/getToken")
|
||||
async def get_token(request: GetTokenRequest):
|
||||
error_code = ErrorSimulator.detect_error_marker(request.projectNo)
|
||||
if error_code:
|
||||
return ErrorSimulator.build_error_response(error_code)
|
||||
return token_service.create_token(request)
|
||||
|
||||
# 接口2:上传文件
|
||||
@router.post("/watson/api/project/remoteUploadSplitFile")
|
||||
async def upload_file(
|
||||
background_tasks: BackgroundTasks,
|
||||
groupId: int = Form(...),
|
||||
file: UploadFile = File(...)
|
||||
):
|
||||
return file_service.upload_file(groupId, file, background_tasks)
|
||||
|
||||
# 接口3:拉取行内流水
|
||||
@router.post("/watson/api/project/getJZFileOrZjrcuFile")
|
||||
async def fetch_inner_flow(request: FetchInnerFlowRequest):
|
||||
error_code = ErrorSimulator.detect_error_marker(request.customerNo)
|
||||
if error_code:
|
||||
return ErrorSimulator.build_error_response(error_code)
|
||||
return file_service.fetch_inner_flow(request)
|
||||
|
||||
# 接口4:检查解析状态
|
||||
@router.post("/watson/api/project/upload/getpendings")
|
||||
async def check_parse_status(request: CheckParseStatusRequest):
|
||||
return file_service.check_parse_status(request.groupId, request.inprogressList)
|
||||
|
||||
# 接口5:删除文件
|
||||
@router.post("/watson/api/project/batchDeleteUploadFile")
|
||||
async def delete_files(request: dict):
|
||||
return file_service.delete_files(
|
||||
request.get("groupId"),
|
||||
request.get("logIds"),
|
||||
request.get("userId")
|
||||
)
|
||||
|
||||
# 接口6:获取银行流水
|
||||
@router.post("/watson/api/project/getBSByLogId")
|
||||
async def get_bank_statement(request: GetBankStatementRequest):
|
||||
return statement_service.get_bank_statement(request)
|
||||
```
|
||||
|
||||
### 主应用
|
||||
|
||||
```python
|
||||
# main.py
|
||||
from fastapi import FastAPI
|
||||
from routers import api
|
||||
|
||||
app = FastAPI(
|
||||
title="流水分析Mock服务",
|
||||
description="模拟流水分析平台的7个核心接口",
|
||||
version="1.0.0",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc"
|
||||
)
|
||||
|
||||
app.include_router(api.router, tags=["流水分析接口"])
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试和使用说明
|
||||
|
||||
### 启动服务
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 启动服务
|
||||
python main.py
|
||||
|
||||
# 或使用uvicorn启动(支持热重载)
|
||||
uvicorn main:app --reload --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
### 访问API文档
|
||||
|
||||
- **Swagger UI:** http://localhost:8000/docs
|
||||
- **ReDoc:** http://localhost:8000/redoc
|
||||
|
||||
### 测试示例
|
||||
|
||||
#### 1. 正常流程测试
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
# 获取Token
|
||||
response = requests.post(
|
||||
"http://localhost:8000/account/common/getToken",
|
||||
json={
|
||||
"projectNo": "test_project_001",
|
||||
"entityName": "测试企业",
|
||||
"userId": "902001",
|
||||
"userName": "902001",
|
||||
"orgCode": "902000"
|
||||
}
|
||||
)
|
||||
result = response.json()
|
||||
token = result["data"]["token"]
|
||||
project_id = result["data"]["projectId"]
|
||||
|
||||
# 上传文件
|
||||
files = {"file": ("test.csv", open("test.csv", "rb"), "text/csv")}
|
||||
response = requests.post(
|
||||
"http://localhost:8000/watson/api/project/remoteUploadSplitFile",
|
||||
files=files,
|
||||
data={"groupId": project_id},
|
||||
headers={"X-Xencio-Client-Id": "26e5b9239853436b85c623f4b7a6d0e6"}
|
||||
)
|
||||
log_id = response.json()["data"]["uploadLogList"][0]["logId"]
|
||||
|
||||
# 轮询检查解析状态
|
||||
import time
|
||||
for i in range(10):
|
||||
response = requests.post(
|
||||
"http://localhost:8000/watson/api/project/upload/getpendings",
|
||||
json={"groupId": project_id, "inprogressList": str(log_id)},
|
||||
headers={"X-Xencio-Client-Id": "26e5b9239853436b85c623f4b7a6d0e6"}
|
||||
)
|
||||
result = response.json()
|
||||
if not result["data"]["parsing"]:
|
||||
print("解析完成")
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
# 获取银行流水
|
||||
response = requests.post(
|
||||
"http://localhost:8000/watson/api/project/getBSByLogId",
|
||||
json={
|
||||
"groupId": project_id,
|
||||
"logId": log_id,
|
||||
"pageNow": 1,
|
||||
"pageSize": 10
|
||||
},
|
||||
headers={"X-Xencio-Client-Id": "26e5b9239853436b85c623f4b7a6d0e6"}
|
||||
)
|
||||
```
|
||||
|
||||
#### 2. 错误场景测试
|
||||
|
||||
```python
|
||||
# 触发 40101 错误(appId错误)
|
||||
response = requests.post(
|
||||
"http://localhost:8000/account/common/getToken",
|
||||
json={
|
||||
"projectNo": "test_project_error_40101", # 包含错误标记
|
||||
"entityName": "测试企业",
|
||||
"userId": "902001",
|
||||
"userName": "902001",
|
||||
"orgCode": "902000"
|
||||
}
|
||||
)
|
||||
# 返回: {"code": "40101", "message": "appId错误", ...}
|
||||
|
||||
# 触发 501014 错误(无行内流水文件)
|
||||
response = requests.post(
|
||||
"http://localhost:8000/watson/api/project/getJZFileOrZjrcuFile",
|
||||
json={
|
||||
"groupId": 1,
|
||||
"customerNo": "test_error_501014", # 包含错误标记
|
||||
"dataChannelCode": "ZJRCU",
|
||||
"requestDateId": 20260302,
|
||||
"dataStartDateId": 20260201,
|
||||
"dataEndDateId": 20260228,
|
||||
"uploadUserId": 902001
|
||||
}
|
||||
)
|
||||
# 返回: {"code": "501014", "message": "无行内流水文件", ...}
|
||||
```
|
||||
|
||||
### 配置修改
|
||||
|
||||
- 修改 `config/responses/` 下的JSON文件可以自定义响应数据
|
||||
- 修改 `config/settings.py` 可以调整延迟时间、端口等配置
|
||||
- 支持 `.env` 文件覆盖配置
|
||||
|
||||
---
|
||||
|
||||
## 依赖清单
|
||||
|
||||
```txt
|
||||
# requirements.txt
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
pydantic==2.5.0
|
||||
python-multipart==0.0.6
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用场景
|
||||
|
||||
### A. 开发阶段测试
|
||||
在业务代码开发过程中,修改配置文件 `application-dev.yml`,将 `lsfx.api.base-url` 改为 `http://localhost:8000`,启动Mock服务器后,业务代码即可连接Mock服务进行测试。
|
||||
|
||||
### B. 完全替换测试
|
||||
直接使用 Mock 服务器进行接口测试,验证业务逻辑的正确性。生产环境切换到真实服务。
|
||||
|
||||
### C. CI/CD 集成
|
||||
在持续集成流程中使用 Mock 服务器,自动化测试接口调用逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 扩展性考虑
|
||||
|
||||
### 后续可能的增强功能
|
||||
|
||||
1. **数据持久化** - 如需保留历史记录,可集成SQLite
|
||||
2. **更复杂的场景模拟** - 支持配置文件定义多个场景
|
||||
3. **请求日志记录** - 记录所有请求用于调试
|
||||
4. **Web管理界面** - 可视化管理Mock数据和状态
|
||||
5. **Docker部署** - 提供Dockerfile方便部署
|
||||
|
||||
当前设计已满足核心需求,保持简洁实用。
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
这是一个**配置驱动、轻量级、易于使用**的 Mock 服务器设计,核心特点:
|
||||
|
||||
✅ **完整性** - 覆盖所有7个核心接口
|
||||
✅ **真实性** - 模拟文件解析延迟等真实场景
|
||||
✅ **灵活性** - 配置文件驱动,错误场景可触发
|
||||
✅ **易用性** - 自动API文档,零配置启动
|
||||
✅ **可维护** - 代码结构清晰,与Java项目对应
|
||||
|
||||
满足您的Mock测试需求,提升开发和测试效率。
|
||||
737
docs/plans/2026-03-02-lsfx-mock-server-implementation-plan.md
Normal file
737
docs/plans/2026-03-02-lsfx-mock-server-implementation-plan.md
Normal file
@@ -0,0 +1,737 @@
|
||||
# 流水分析 Mock 服务器 - 实施计划
|
||||
|
||||
**创建日期**: 2026-03-02
|
||||
**状态**: 待执行
|
||||
**预计完成时间**: 2-3 天
|
||||
|
||||
---
|
||||
|
||||
## 项目目标
|
||||
|
||||
开发一个基于 Python + FastAPI 的 Mock 服务器,用于模拟流水分析平台的 7 个核心接口,支持:
|
||||
- 配置文件驱动的响应数据
|
||||
- 文件上传解析延迟模拟(4秒)
|
||||
- 错误场景触发机制(通过 error_XXXX 标记)
|
||||
- 自动生成的 Swagger API 文档
|
||||
|
||||
---
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 技术 | 版本 | 用途 |
|
||||
|------|------|------|
|
||||
| Python | 3.11+ | 编程语言 |
|
||||
| FastAPI | 0.104.1 | Web框架 |
|
||||
| Pydantic | 2.5.0 | 数据验证 |
|
||||
| Uvicorn | 0.24.0 | ASGI服务器 |
|
||||
| pytest | latest | 测试框架 |
|
||||
|
||||
---
|
||||
|
||||
## 实施任务列表
|
||||
|
||||
### Task 1: 项目初始化和基础设置
|
||||
**状态**: ⏳ 待开始
|
||||
**预计时间**: 1 小时
|
||||
**阻塞任务**: 无
|
||||
|
||||
**目标**: 创建项目目录结构、配置文件和依赖管理
|
||||
|
||||
**实施步骤**:
|
||||
1. 创建项目根目录 `lsfx-mock-server/`
|
||||
2. 创建目录结构:
|
||||
```
|
||||
lsfx-mock-server/
|
||||
├── config/
|
||||
│ └── responses/
|
||||
├── models/
|
||||
├── services/
|
||||
├── routers/
|
||||
├── utils/
|
||||
└── tests/
|
||||
```
|
||||
3. 创建 `requirements.txt`:
|
||||
```txt
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
pydantic==2.5.0
|
||||
python-multipart==0.0.6
|
||||
pytest>=7.0.0
|
||||
pytest-cov>=4.0.0
|
||||
httpx>=0.25.0
|
||||
```
|
||||
|
||||
4. 创建 `config/settings.py`:
|
||||
- 使用 Pydantic BaseSettings
|
||||
- 支持环境变量覆盖(.env 文件)
|
||||
- 配置项:APP_NAME, HOST, PORT, DEBUG, PARSE_DELAY_SECONDS
|
||||
|
||||
5. 创建 4 个 JSON 响应模板文件:
|
||||
- `config/responses/token.json` - Token 响应模板
|
||||
- `config/responses/upload.json` - 上传文件响应模板
|
||||
- `config/responses/parse_status.json` - 解析状态响应模板
|
||||
- `config/responses/bank_statement.json` - 银行流水响应模板
|
||||
- 每个模板包含占位符(如 {project_id}, {log_id})
|
||||
|
||||
**验证标准**:
|
||||
- ✅ 虚拟环境创建并激活
|
||||
- ✅ 依赖安装成功(无错误)
|
||||
- ✅ 配置文件能正确导入(`from config.settings import settings`)
|
||||
- ✅ JSON 模板文件格式正确(使用 `json.load()` 验证)
|
||||
- ✅ settings 能读取环境变量
|
||||
|
||||
**提交检查点**:
|
||||
```bash
|
||||
git add requirements.txt config/
|
||||
git commit -m "feat(mock): initialize project structure and configuration"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 实现数据模型层
|
||||
**状态**: ⏳ 待开始(等待 Task 1)
|
||||
**预计时间**: 1.5 小时
|
||||
**阻塞任务**: Task 1
|
||||
|
||||
**目标**: 创建所有请求和响应的 Pydantic 模型类
|
||||
|
||||
**实施步骤**:
|
||||
1. 创建 `models/__init__.py`(空文件)
|
||||
2. 创建 `models/request.py`:
|
||||
- 定义 6 个请求模型:
|
||||
- GetTokenRequest(10+ 字段,可选字段有默认值)
|
||||
- UploadFileRequest(通过 Form 数据接收)
|
||||
- FetchInnerFlowRequest(7 个必填字段)
|
||||
- CheckParseStatusRequest(2 个字段)
|
||||
- DeleteFilesRequest(3 个字段)
|
||||
- GetBankStatementRequest(4 个字段)
|
||||
- 所有字段添加 Field 描述(用于 Swagger)
|
||||
- 可选字段使用 `Optional[Type] = default_value`
|
||||
|
||||
3. 创建 `models/response.py`:
|
||||
- 定义嵌套数据模型:
|
||||
- TokenData(5 个字段)
|
||||
- UploadLogItem(15+ 字段)
|
||||
- BankStatementItem(30+ 字段)
|
||||
- PendingItem(15+ 字段)
|
||||
- 定义 6 个响应模型:
|
||||
- GetTokenResponse
|
||||
- UploadFileResponse
|
||||
- FetchInnerFlowResponse
|
||||
- CheckParseStatusResponse
|
||||
- DeleteFilesResponse
|
||||
- GetBankStatementResponse
|
||||
- 所有响应模型包含通用字段:code, message, status, successResponse
|
||||
|
||||
**验证标准**:
|
||||
- ✅ 所有模型类能正确实例化
|
||||
- ✅ 可选字段默认值正确
|
||||
- ✅ Pydantic 验证功能正常(类型错误会抛出 ValidationError)
|
||||
- ✅ 模型序列化为 JSON 正确(`model.model_dump_json()`)
|
||||
- ✅ Swagger 自动文档显示所有字段和描述
|
||||
|
||||
**提交检查点**:
|
||||
```bash
|
||||
git add models/
|
||||
git commit -m "feat(models): implement Pydantic request and response models"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 实现工具类
|
||||
**状态**: ⏳ 待开始(可与 Task 2 并行)
|
||||
**预计时间**: 1 小时
|
||||
**阻塞任务**: 无
|
||||
|
||||
**目标**: 实现错误检测和响应构建工具
|
||||
|
||||
**实施步骤**:
|
||||
1. 创建 `utils/__init__.py`
|
||||
2. 创建 `utils/error_simulator.py`:
|
||||
```python
|
||||
class ErrorSimulator:
|
||||
ERROR_CODES = {
|
||||
"40101": {"code": "40101", "message": "appId错误"},
|
||||
"40102": {"code": "40102", "message": "appSecretCode错误"},
|
||||
"40104": {"code": "40104", "message": "可使用项目次数为0"},
|
||||
"40105": {"code": "40105", "message": "只读模式下无法新建项目"},
|
||||
"40106": {"code": "40106", "message": "错误的分析类型"},
|
||||
"40107": {"code": "40107", "message": "当前系统不支持的分析类型"},
|
||||
"40108": {"code": "40108", "message": "当前用户所属行社无权限"},
|
||||
"501014": {"code": "501014", "message": "无行内流水文件"},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def detect_error_marker(value: str) -> Optional[str]:
|
||||
"""检测 error_XXXX 模式"""
|
||||
import re
|
||||
if not value:
|
||||
return None
|
||||
pattern = r'error_(\d+)'
|
||||
match = re.search(pattern, value)
|
||||
return match.group(1) if match else None
|
||||
|
||||
@staticmethod
|
||||
def build_error_response(error_code: str) -> Dict:
|
||||
"""构建错误响应"""
|
||||
if error_code in ErrorSimulator.ERROR_CODES:
|
||||
error_info = ErrorSimulator.ERROR_CODES[error_code]
|
||||
return {
|
||||
"code": error_info["code"],
|
||||
"message": error_info["message"],
|
||||
"status": error_info["code"],
|
||||
"successResponse": False
|
||||
}
|
||||
return None
|
||||
```
|
||||
|
||||
3. 创建 `utils/response_builder.py`:
|
||||
```python
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
|
||||
class ResponseBuilder:
|
||||
TEMPLATE_DIR = Path(__file__).parent.parent / "config" / "responses"
|
||||
|
||||
@staticmethod
|
||||
def load_template(template_name: str) -> Dict:
|
||||
"""加载 JSON 模板"""
|
||||
file_path = ResponseBuilder.TEMPLATE_DIR / f"{template_name}.json"
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
@staticmethod
|
||||
def replace_placeholders(template: Dict, **kwargs) -> Dict:
|
||||
"""递归替换占位符"""
|
||||
def replace_value(value):
|
||||
if isinstance(value, str):
|
||||
for key, val in kwargs.items():
|
||||
placeholder = f"{{{key}}}"
|
||||
if placeholder in value:
|
||||
return value.replace(placeholder, str(val))
|
||||
return value
|
||||
elif isinstance(value, dict):
|
||||
return {k: replace_value(v) for k, v in value.items()}
|
||||
elif isinstance(value, list):
|
||||
return [replace_value(item) for item in value]
|
||||
return value
|
||||
|
||||
return replace_value(template)
|
||||
|
||||
@staticmethod
|
||||
def build_success_response(template_name: str, **kwargs) -> Dict:
|
||||
"""构建成功响应"""
|
||||
template = ResponseBuilder.load_template(template_name)
|
||||
return ResponseBuilder.replace_placeholders(
|
||||
template["success_response"],
|
||||
**kwargs
|
||||
)
|
||||
```
|
||||
|
||||
**验证标准**:
|
||||
- ✅ ErrorSimulator.detect_error_marker() 能正确识别错误标记
|
||||
- ✅ ErrorSimulator.build_error_response() 返回正确的错误响应
|
||||
- ✅ ResponseBuilder 能正确加载 JSON 模板
|
||||
- ✅ 占位符替换功能正常(支持嵌套字典和列表)
|
||||
- ✅ 所有 8 个错误码都有对应响应
|
||||
|
||||
**提交检查点**:
|
||||
```bash
|
||||
git add utils/
|
||||
git commit -m "feat(utils): implement error simulator and response builder"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 实现服务层
|
||||
**状态**: ⏳ 待开始(等待 Task 1, 2, 3)
|
||||
**预计时间**: 2 小时
|
||||
**阻塞任务**: Task 1, Task 2, Task 3
|
||||
|
||||
**目标**: 实现核心业务服务类
|
||||
|
||||
**实施步骤**:
|
||||
1. 创建 `services/__init__.py`
|
||||
2. 创建 `services/token_service.py`:
|
||||
```python
|
||||
class TokenService:
|
||||
def __init__(self):
|
||||
self.project_counter = 0
|
||||
self.tokens = {} # projectId -> token_data
|
||||
|
||||
def create_token(self, request: GetTokenRequest) -> Dict:
|
||||
self.project_counter += 1
|
||||
project_id = self.project_counter
|
||||
token = f"mock_token_{project_id}"
|
||||
|
||||
return ResponseBuilder.build_success_response(
|
||||
"token",
|
||||
project_id=project_id,
|
||||
project_no=request.projectNo,
|
||||
entity_name=request.entityName
|
||||
)
|
||||
```
|
||||
|
||||
3. 创建 `services/file_service.py`:
|
||||
```python
|
||||
from fastapi import BackgroundTasks
|
||||
import time
|
||||
from uuid import uuid4
|
||||
|
||||
class FileService:
|
||||
def __init__(self):
|
||||
self.file_records = {} # logId -> record
|
||||
self.parsing_status = {} # logId -> is_parsing
|
||||
self.log_counter = 0
|
||||
|
||||
async def upload_file(self, group_id: int, file, background_tasks: BackgroundTasks) -> Dict:
|
||||
self.log_counter += 1
|
||||
log_id = self.log_counter
|
||||
|
||||
# 立即存储记录
|
||||
self.file_records[log_id] = {
|
||||
"logId": log_id,
|
||||
"groupId": group_id,
|
||||
"status": -5,
|
||||
"uploadStatusDesc": "parsing",
|
||||
"uploadFileName": file.filename,
|
||||
"fileSize": file.size,
|
||||
# ... 其他字段
|
||||
}
|
||||
self.parsing_status[log_id] = True
|
||||
|
||||
# 启动后台任务
|
||||
background_tasks.add_task(
|
||||
self._simulate_parsing,
|
||||
log_id,
|
||||
settings.PARSE_DELAY_SECONDS
|
||||
)
|
||||
|
||||
return ResponseBuilder.build_success_response(
|
||||
"upload",
|
||||
log_id=log_id
|
||||
)
|
||||
|
||||
def _simulate_parsing(self, log_id: int, delay_seconds: int):
|
||||
"""后台任务:模拟解析"""
|
||||
time.sleep(delay_seconds)
|
||||
if log_id in self.file_records:
|
||||
self.file_records[log_id]["uploadStatusDesc"] = "data.wait.confirm.newaccount"
|
||||
self.parsing_status[log_id] = False
|
||||
|
||||
def check_parse_status(self, group_id: int, inprogress_list: str) -> Dict:
|
||||
"""检查解析状态"""
|
||||
log_ids = [int(x.strip()) for x in inprogress_list.split(",")]
|
||||
is_parsing = any(
|
||||
self.parsing_status.get(log_id, False)
|
||||
for log_id in log_ids
|
||||
)
|
||||
|
||||
pending_list = [
|
||||
self.file_records[log_id]
|
||||
for log_id in log_ids
|
||||
if log_id in self.file_records
|
||||
]
|
||||
|
||||
return {
|
||||
"code": "200",
|
||||
"data": {
|
||||
"parsing": is_parsing,
|
||||
"pendingList": pending_list
|
||||
},
|
||||
"status": "200",
|
||||
"successResponse": True
|
||||
}
|
||||
|
||||
def delete_files(self, group_id: int, log_ids: List[int], user_id: int) -> Dict:
|
||||
"""删除文件"""
|
||||
for log_id in log_ids:
|
||||
self.file_records.pop(log_id, None)
|
||||
self.parsing_status.pop(log_id, None)
|
||||
|
||||
return {
|
||||
"code": "200",
|
||||
"data": {"message": "delete.files.success"},
|
||||
"status": "200",
|
||||
"successResponse": True
|
||||
}
|
||||
|
||||
def fetch_inner_flow(self, request: FetchInnerFlowRequest) -> Dict:
|
||||
"""拉取行内流水(模拟无数据)"""
|
||||
return {
|
||||
"code": "200",
|
||||
"data": {
|
||||
"code": "501014",
|
||||
"message": "无行内流水文件"
|
||||
},
|
||||
"status": "200",
|
||||
"successResponse": True
|
||||
}
|
||||
```
|
||||
|
||||
4. 创建 `services/statement_service.py`:
|
||||
```python
|
||||
class StatementService:
|
||||
def get_bank_statement(self, request: GetBankStatementRequest) -> Dict:
|
||||
# 加载模板
|
||||
template = ResponseBuilder.load_template("bank_statement")
|
||||
statements = template["success_response"]["data"]["bankStatementList"]
|
||||
|
||||
# 模拟分页
|
||||
start = (request.pageNow - 1) * request.pageSize
|
||||
end = start + request.pageSize
|
||||
page_data = statements[start:end]
|
||||
|
||||
return {
|
||||
"code": "200",
|
||||
"data": {
|
||||
"bankStatementList": page_data,
|
||||
"totalCount": len(statements)
|
||||
},
|
||||
"status": "200",
|
||||
"successResponse": True
|
||||
}
|
||||
```
|
||||
|
||||
**验证标准**:
|
||||
- ✅ TokenService 能创建唯一 token
|
||||
- ✅ FileService.upload_file() 返回正确状态
|
||||
- ✅ 后台任务执行后,解析状态从 True 变为 False
|
||||
- ✅ check_parse_status() 正确返回 parsing 状态
|
||||
- ✅ StatementService 支持分页功能
|
||||
- ✅ 所有方法返回正确格式
|
||||
|
||||
**提交检查点**:
|
||||
```bash
|
||||
git add services/
|
||||
git commit -m "feat(services): implement token, file, and statement services"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 实现 API 路由
|
||||
**状态**: ⏳ 待开始(等待 Task 2, 3, 4)
|
||||
**预计时间**: 1.5 小时
|
||||
**阻塞任务**: Task 1, Task 2, Task 3, Task 4
|
||||
|
||||
**目标**: 实现所有 6 个 API 接口路由
|
||||
|
||||
**实施步骤**:
|
||||
1. 创建 `routers/__init__.py`
|
||||
2. 创建 `routers/api.py`:
|
||||
```python
|
||||
from fastapi import APIRouter, BackgroundTasks, UploadFile, File, Form
|
||||
from models.request import *
|
||||
from models.response import *
|
||||
from services.token_service import TokenService
|
||||
from services.file_service import FileService
|
||||
from services.statement_service import StatementService
|
||||
from utils.error_simulator import ErrorSimulator
|
||||
|
||||
router = APIRouter()
|
||||
token_service = TokenService()
|
||||
file_service = FileService()
|
||||
statement_service = StatementService()
|
||||
|
||||
@router.post("/account/common/getToken")
|
||||
async def get_token(request: GetTokenRequest):
|
||||
"""获取Token"""
|
||||
error_code = ErrorSimulator.detect_error_marker(request.projectNo)
|
||||
if error_code:
|
||||
return ErrorSimulator.build_error_response(error_code)
|
||||
return token_service.create_token(request)
|
||||
|
||||
@router.post("/watson/api/project/remoteUploadSplitFile")
|
||||
async def upload_file(
|
||||
background_tasks: BackgroundTasks,
|
||||
groupId: int = Form(...),
|
||||
file: UploadFile = File(...)
|
||||
):
|
||||
"""上传文件"""
|
||||
return await file_service.upload_file(groupId, file, background_tasks)
|
||||
|
||||
@router.post("/watson/api/project/getJZFileOrZjrcuFile")
|
||||
async def fetch_inner_flow(request: FetchInnerFlowRequest):
|
||||
"""拉取行内流水"""
|
||||
error_code = ErrorSimulator.detect_error_marker(request.customerNo)
|
||||
if error_code:
|
||||
return ErrorSimulator.build_error_response(error_code)
|
||||
return file_service.fetch_inner_flow(request)
|
||||
|
||||
@router.post("/watson/api/project/upload/getpendings")
|
||||
async def check_parse_status(request: CheckParseStatusRequest):
|
||||
"""检查文件解析状态"""
|
||||
return file_service.check_parse_status(request.groupId, request.inprogressList)
|
||||
|
||||
@router.post("/watson/api/project/batchDeleteUploadFile")
|
||||
async def delete_files(
|
||||
groupId: int,
|
||||
logIds: List[int],
|
||||
userId: int
|
||||
):
|
||||
"""删除文件"""
|
||||
return file_service.delete_files(groupId, logIds, userId)
|
||||
|
||||
@router.post("/watson/api/project/getBSByLogId")
|
||||
async def get_bank_statement(request: GetBankStatementRequest):
|
||||
"""获取银行流水"""
|
||||
return statement_service.get_bank_statement(request)
|
||||
```
|
||||
|
||||
**验证标准**:
|
||||
- ✅ 所有 6 个接口在 Swagger UI 中可见
|
||||
- ✅ 每个接口能正常响应
|
||||
- ✅ 错误标记功能正常(包含 error_XXXX 的参数触发错误)
|
||||
- ✅ 文件上传接口能接收文件
|
||||
- ✅ 所有接口有正确的 Swagger 描述
|
||||
- ✅ 响应格式符合文档要求
|
||||
|
||||
**提交检查点**:
|
||||
```bash
|
||||
git add routers/
|
||||
git commit -m "feat(routers): implement all 6 API endpoints"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 实现主应用
|
||||
**状态**: ⏳ 待开始(等待 Task 1, 4, 5)
|
||||
**预计时间**: 0.5 小时
|
||||
**阻塞任务**: Task 1, Task 4, Task 5
|
||||
|
||||
**目标**: 实现 FastAPI 应用主入口
|
||||
|
||||
**实施步骤**:
|
||||
1. 创建 `main.py`:
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
from routers import api
|
||||
from config.settings import settings
|
||||
|
||||
app = FastAPI(
|
||||
title=settings.APP_NAME,
|
||||
description="模拟流水分析平台的7个核心接口",
|
||||
version="1.0.0",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc"
|
||||
)
|
||||
|
||||
app.include_router(api.router, tags=["流水分析接口"])
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
return {"status": "healthy", "service": settings.APP_NAME}
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=settings.HOST,
|
||||
port=settings.PORT,
|
||||
log_level="debug" if settings.DEBUG else "info"
|
||||
)
|
||||
```
|
||||
|
||||
**验证标准**:
|
||||
- ✅ 应用能启动:`python main.py`
|
||||
- ✅ 访问 http://localhost:8000/docs 显示 Swagger UI
|
||||
- ✅ 访问 http://localhost:8000/redoc 显示 ReDoc
|
||||
- ✅ 健康检查端点返回正确响应
|
||||
- ✅ 所有接口在文档中可见
|
||||
|
||||
**提交检查点**:
|
||||
```bash
|
||||
git add main.py
|
||||
git commit -m "feat(main): implement FastAPI application entry point"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 编写测试套件
|
||||
**状态**: ⏳ 待开始(等待 Task 1-6)
|
||||
**预计时间**: 2 小时
|
||||
**阻塞任务**: Task 1, Task 2, Task 3, Task 4, Task 5, Task 6
|
||||
|
||||
**目标**: 创建完整的测试套件
|
||||
|
||||
**实施步骤**:
|
||||
1. 创建 `tests/conftest.py`:
|
||||
```python
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from main import app
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
return TestClient(app)
|
||||
```
|
||||
|
||||
2. 创建 `tests/test_models.py` - 测试所有数据模型
|
||||
3. 创建 `tests/test_utils.py` - 测试工具类
|
||||
4. 创建 `tests/test_services.py` - 测试服务类
|
||||
5. 创建 `tests/test_api.py` - 测试 API 端点
|
||||
|
||||
**验证标准**:
|
||||
- ✅ 运行 `pytest tests/ -v` 所有测试通过
|
||||
- ✅ 代码覆盖率 > 80%
|
||||
- ✅ 所有错误场景有测试
|
||||
- ✅ 生成 HTML 覆盖率报告
|
||||
|
||||
**提交检查点**:
|
||||
```bash
|
||||
git add tests/
|
||||
git commit -m "test: add comprehensive test suite"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: 编写文档和部署配置
|
||||
**状态**: ⏳ 待开始(等待 Task 1-7)
|
||||
**预计时间**: 1 小时
|
||||
**阻塞任务**: Task 1-7
|
||||
|
||||
**目标**: 创建项目文档和部署说明
|
||||
|
||||
**实施步骤**:
|
||||
1. 创建 `README.md`(包含安装、使用、测试说明)
|
||||
2. 创建 `.env.example`
|
||||
3. 创建 `Dockerfile`
|
||||
4. 创建 `docker-compose.yml`
|
||||
|
||||
**验证标准**:
|
||||
- ✅ README 中所有命令可执行
|
||||
- ✅ Docker 镜像构建成功
|
||||
- ✅ Docker Compose 启动成功
|
||||
|
||||
**提交检查点**:
|
||||
```bash
|
||||
git add README.md .env.example Dockerfile docker-compose.yml
|
||||
git commit -m "docs: add README and deployment configuration"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: 创建集成测试
|
||||
**状态**: ⏳ 待开始(等待 Task 8)
|
||||
**预计时间**: 1 小时
|
||||
**阻塞任务**: Task 8
|
||||
|
||||
**目标**: 创建端到端集成测试脚本
|
||||
|
||||
**实施步骤**:
|
||||
1. 创建 `tests/integration/test_full_workflow.py`
|
||||
2. 实现完整的接口调用流程测试
|
||||
3. 添加错误场景测试
|
||||
|
||||
**验证标准**:
|
||||
- ✅ 集成测试通过
|
||||
- ✅ 完整流程测试成功
|
||||
- ✅ 错误场景测试成功
|
||||
|
||||
**提交检查点**:
|
||||
```bash
|
||||
git add tests/integration/
|
||||
git commit -m "test: add integration tests for full workflow"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 10: 代码审查和提交
|
||||
**状态**: ⏳ 待开始(等待 Task 1-9)
|
||||
**预计时间**: 1 小时
|
||||
**阻塞任务**: Task 1-9
|
||||
|
||||
**目标**: 代码审查、优化和 Git 提交
|
||||
|
||||
**审查清单**:
|
||||
1. **代码质量**
|
||||
- ✅ 所有代码符合 PEP 8
|
||||
- ✅ 类型提示完整
|
||||
- ✅ 无硬编码配置
|
||||
- ✅ 注释充分
|
||||
|
||||
2. **安全性**
|
||||
- ✅ 输入验证完整(Pydantic)
|
||||
- ✅ 无注入风险
|
||||
|
||||
3. **测试覆盖**
|
||||
- ✅ 单元测试覆盖率 > 80%
|
||||
- ✅ 集成测试通过
|
||||
|
||||
**验证标准**:
|
||||
- ✅ 所有测试通过
|
||||
- ✅ 代码覆盖率报告生成
|
||||
- ✅ 手动测试所有接口
|
||||
- ✅ README 验证完成
|
||||
|
||||
**最终提交**:
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "feat(lsfx-mock): complete lsfx mock server implementation"
|
||||
git push origin feature/lsfx-mock-server
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 开发注意事项
|
||||
|
||||
### 环境要求
|
||||
- Python 3.11+
|
||||
- 虚拟环境(venv)
|
||||
- 端口 8000 可用
|
||||
|
||||
### 开发流程
|
||||
1. 每完成一个任务,立即提交代码
|
||||
2. 运行相关测试确保功能正确
|
||||
3. 更新任务状态
|
||||
4. 开始下一个任务
|
||||
|
||||
### 测试策略
|
||||
- **单元测试**: 每个模块独立测试
|
||||
- **集成测试**: 完整流程测试
|
||||
- **手动测试**: 使用 Swagger UI 验证接口
|
||||
|
||||
### 代码规范
|
||||
- 遵循 PEP 8
|
||||
- 使用类型提示
|
||||
- 函数和类添加文档字符串
|
||||
- 保持代码简洁(YAGNI, DRY)
|
||||
|
||||
---
|
||||
|
||||
## 预期成果
|
||||
|
||||
1. ✅ 完整的 Mock 服务器,模拟 7 个核心接口
|
||||
2. ✅ 配置文件驱动的响应数据
|
||||
3. ✅ 文件解析延迟模拟
|
||||
4. ✅ 错误场景触发机制
|
||||
5. ✅ 自动生成的 API 文档
|
||||
6. ✅ 完整的测试套件(覆盖率 > 80%)
|
||||
7. ✅ 清晰的 README 和部署文档
|
||||
8. ✅ Docker 部署支持
|
||||
|
||||
---
|
||||
|
||||
## 风险和缓解
|
||||
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
|------|------|----------|
|
||||
| FastAPI 框架不熟悉 | 延期 | 变更预计时间到 3-4 天 |
|
||||
| 异步任务调试困难 | 中等 | 添加详细日志,分步测试 |
|
||||
| 响应格式与真实接口不符 | 高 | 严格对照接口文档,多次验证 |
|
||||
|
||||
---
|
||||
|
||||
## 后续优化方向
|
||||
|
||||
1. 添加数据库持久化(SQLite)
|
||||
2. 实现更复杂的场景模拟
|
||||
3. 添加请求日志记录
|
||||
4. 创建 Web 管理界面
|
||||
5. 支持 WebSocket 实时通知
|
||||
|
||||
---
|
||||
|
||||
**预计总开发时间**: 10-12 小时
|
||||
**建议开发模式**: 按顺序执行,每完成一个任务立即测试验证
|
||||
1051
docs/plans/2026-03-02-lsfx-update-plan.md
Normal file
1051
docs/plans/2026-03-02-lsfx-update-plan.md
Normal file
File diff suppressed because it is too large
Load Diff
16
lsfx-mock-server/.env.example
Normal file
16
lsfx-mock-server/.env.example
Normal file
@@ -0,0 +1,16 @@
|
||||
# 应用配置
|
||||
APP_NAME=流水分析Mock服务
|
||||
APP_VERSION=1.0.0
|
||||
DEBUG=true
|
||||
|
||||
# 服务器配置
|
||||
HOST=0.0.0.0
|
||||
PORT=8000
|
||||
|
||||
# 模拟配置
|
||||
PARSE_DELAY_SECONDS=4
|
||||
MAX_FILE_SIZE=10485760
|
||||
|
||||
# 初始ID配置
|
||||
INITIAL_PROJECT_ID=1000
|
||||
INITIAL_LOG_ID=10000
|
||||
45
lsfx-mock-server/.gitignore
vendored
Normal file
45
lsfx-mock-server/.gitignore
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual Environment
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Environment
|
||||
.env
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
19
lsfx-mock-server/Dockerfile
Normal file
19
lsfx-mock-server/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制依赖文件
|
||||
COPY requirements.txt .
|
||||
|
||||
# 安装依赖
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 复制项目文件
|
||||
COPY . .
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8000
|
||||
|
||||
# 启动命令
|
||||
CMD ["python", "main.py"]
|
||||
236
lsfx-mock-server/README.md
Normal file
236
lsfx-mock-server/README.md
Normal file
@@ -0,0 +1,236 @@
|
||||
# 流水分析 Mock 服务器
|
||||
|
||||
基于 Python + FastAPI 的独立 Mock 服务器,用于模拟流水分析平台的 7 个核心接口。
|
||||
|
||||
## ✨ 特性
|
||||
|
||||
- ✅ **完整的接口模拟** - 实现所有 7 个核心接口
|
||||
- ✅ **文件解析延迟** - 使用 FastAPI 后台任务模拟 4 秒解析延迟
|
||||
- ✅ **错误场景触发** - 通过 `error_XXXX` 标记触发所有 8 个错误码
|
||||
- ✅ **自动 API 文档** - Swagger UI 和 ReDoc 自动生成
|
||||
- ✅ **配置驱动** - JSON 模板文件,易于修改响应数据
|
||||
- ✅ **零配置启动** - 开箱即用,无需数据库
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2. 启动服务
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
或使用 uvicorn(支持热重载):
|
||||
|
||||
```bash
|
||||
uvicorn main:app --reload --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
### 3. 访问 API 文档
|
||||
|
||||
- **Swagger UI**: http://localhost:8000/docs
|
||||
- **ReDoc**: http://localhost:8000/redoc
|
||||
|
||||
## 📖 使用示例
|
||||
|
||||
### 正常流程
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
# 1. 获取 Token
|
||||
response = requests.post(
|
||||
"http://localhost:8000/account/common/getToken",
|
||||
json={
|
||||
"projectNo": "test_project_001",
|
||||
"entityName": "测试企业",
|
||||
"userId": "902001",
|
||||
"userName": "902001",
|
||||
"orgCode": "902000"
|
||||
}
|
||||
)
|
||||
token_data = response.json()
|
||||
project_id = token_data["data"]["projectId"]
|
||||
|
||||
# 2. 上传文件
|
||||
files = {"file": ("test.csv", open("test.csv", "rb"), "text/csv")}
|
||||
response = requests.post(
|
||||
"http://localhost:8000/watson/api/project/remoteUploadSplitFile",
|
||||
files=files,
|
||||
data={"groupId": project_id}
|
||||
)
|
||||
log_id = response.json()["data"]["uploadLogList"][0]["logId"]
|
||||
|
||||
# 3. 轮询检查解析状态
|
||||
import time
|
||||
for i in range(10):
|
||||
response = requests.post(
|
||||
"http://localhost:8000/watson/api/project/upload/getpendings",
|
||||
json={"groupId": project_id, "inprogressList": str(log_id)}
|
||||
)
|
||||
result = response.json()
|
||||
if not result["data"]["parsing"]:
|
||||
print("解析完成")
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
# 4. 获取银行流水
|
||||
response = requests.post(
|
||||
"http://localhost:8000/watson/api/project/getBSByLogId",
|
||||
json={
|
||||
"groupId": project_id,
|
||||
"logId": log_id,
|
||||
"pageNow": 1,
|
||||
"pageSize": 10
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### 错误场景测试
|
||||
|
||||
```python
|
||||
# 触发 40101 错误(appId错误)
|
||||
response = requests.post(
|
||||
"http://localhost:8000/account/common/getToken",
|
||||
json={
|
||||
"projectNo": "test_error_40101", # 包含错误标记
|
||||
"entityName": "测试企业",
|
||||
"userId": "902001",
|
||||
"userName": "902001",
|
||||
"orgCode": "902000"
|
||||
}
|
||||
)
|
||||
# 返回: {"code": "40101", "message": "appId错误", ...}
|
||||
```
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### 环境变量
|
||||
|
||||
创建 `.env` 文件(参考 `.env.example`):
|
||||
|
||||
```bash
|
||||
# 应用配置
|
||||
APP_NAME=流水分析Mock服务
|
||||
APP_VERSION=1.0.0
|
||||
DEBUG=true
|
||||
|
||||
# 服务器配置
|
||||
HOST=0.0.0.0
|
||||
PORT=8000
|
||||
|
||||
# 模拟配置
|
||||
PARSE_DELAY_SECONDS=4
|
||||
MAX_FILE_SIZE=10485760
|
||||
```
|
||||
|
||||
### 响应模板
|
||||
|
||||
修改 `config/responses/` 下的 JSON 文件可以自定义响应数据:
|
||||
|
||||
- `token.json` - Token 响应模板
|
||||
- `upload.json` - 上传文件响应模板
|
||||
- `parse_status.json` - 解析状态响应模板
|
||||
- `bank_statement.json` - 银行流水响应模板
|
||||
|
||||
## 🐳 Docker 部署
|
||||
|
||||
### 使用 Docker
|
||||
|
||||
```bash
|
||||
# 构建镜像
|
||||
docker build -t lsfx-mock-server .
|
||||
|
||||
# 运行容器
|
||||
docker run -d -p 8000:8000 --name lsfx-mock lsfx-mock-server
|
||||
```
|
||||
|
||||
### 使用 Docker Compose
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
lsfx-mock-server/
|
||||
├── main.py # 应用入口
|
||||
├── config/
|
||||
│ ├── settings.py # 全局配置
|
||||
│ └── responses/ # 响应模板
|
||||
├── models/
|
||||
│ ├── request.py # 请求模型
|
||||
│ └── response.py # 响应模型
|
||||
├── services/
|
||||
│ ├── token_service.py # Token 管理
|
||||
│ ├── file_service.py # 文件上传和解析
|
||||
│ └── statement_service.py # 流水数据管理
|
||||
├── routers/
|
||||
│ └── api.py # API 路由
|
||||
├── utils/
|
||||
│ ├── error_simulator.py # 错误模拟
|
||||
│ └── response_builder.py # 响应构建器
|
||||
└── tests/ # 测试套件
|
||||
```
|
||||
|
||||
## 🧪 运行测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
pytest tests/ -v
|
||||
|
||||
# 生成覆盖率报告
|
||||
pytest tests/ -v --cov=. --cov-report=html
|
||||
```
|
||||
|
||||
## 🔌 API 接口列表
|
||||
|
||||
| 接口 | 方法 | 路径 | 描述 |
|
||||
|------|------|------|------|
|
||||
| 1 | POST | `/account/common/getToken` | 获取 Token |
|
||||
| 2 | POST | `/watson/api/project/remoteUploadSplitFile` | 上传文件 |
|
||||
| 3 | POST | `/watson/api/project/getJZFileOrZjrcuFile` | 拉取行内流水 |
|
||||
| 4 | POST | `/watson/api/project/upload/getpendings` | 检查解析状态 |
|
||||
| 5 | POST | `/watson/api/project/batchDeleteUploadFile` | 删除文件 |
|
||||
| 6 | POST | `/watson/api/project/getBSByLogId` | 获取银行流水 |
|
||||
|
||||
## ⚠️ 错误码列表
|
||||
|
||||
| 错误码 | 描述 |
|
||||
|--------|------|
|
||||
| 40101 | appId错误 |
|
||||
| 40102 | appSecretCode错误 |
|
||||
| 40104 | 可使用项目次数为0,无法创建项目 |
|
||||
| 40105 | 只读模式下无法新建项目 |
|
||||
| 40106 | 错误的分析类型,不在规定的取值范围内 |
|
||||
| 40107 | 当前系统不支持的分析类型 |
|
||||
| 40108 | 当前用户所属行社无权限 |
|
||||
| 501014 | 无行内流水文件 |
|
||||
|
||||
## 🛠️ 开发指南
|
||||
|
||||
### 添加新接口
|
||||
|
||||
1. 在 `models/request.py` 和 `models/response.py` 中添加模型
|
||||
2. 在 `services/` 中添加服务类
|
||||
3. 在 `routers/api.py` 中添加路由
|
||||
4. 在 `config/responses/` 中添加响应模板
|
||||
5. 编写测试
|
||||
|
||||
### 修改响应数据
|
||||
|
||||
直接编辑 `config/responses/` 下的 JSON 文件,重启服务即可生效。
|
||||
|
||||
## 📝 License
|
||||
|
||||
MIT
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
BIN
lsfx-mock-server/config/__pycache__/settings.cpython-313.pyc
Normal file
BIN
lsfx-mock-server/config/__pycache__/settings.cpython-313.pyc
Normal file
Binary file not shown.
106
lsfx-mock-server/config/responses/bank_statement.json
Normal file
106
lsfx-mock-server/config/responses/bank_statement.json
Normal file
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"success_response": {
|
||||
"code": "200",
|
||||
"data": {
|
||||
"bankStatementList": [
|
||||
{
|
||||
"accountId": 0,
|
||||
"accountMaskNo": "101015251071645",
|
||||
"accountingDate": "2024-02-01",
|
||||
"accountingDateId": 20240201,
|
||||
"archivingFlag": 0,
|
||||
"attachments": 0,
|
||||
"balanceAmount": 4814.82,
|
||||
"bank": "ZJRCU",
|
||||
"bankComments": "",
|
||||
"bankStatementId": 12847662,
|
||||
"bankTrxNumber": "1a10458dd5c3366d7272285812d434fc",
|
||||
"batchId": 19135,
|
||||
"cashType": "1",
|
||||
"commentsNum": 0,
|
||||
"crAmount": 0,
|
||||
"cretNo": "230902199012261247",
|
||||
"currency": "CNY",
|
||||
"customerAccountMaskNo": "597671502",
|
||||
"customerBank": "",
|
||||
"customerId": -1,
|
||||
"customerName": "小店",
|
||||
"customerReference": "",
|
||||
"downPaymentFlag": 0,
|
||||
"drAmount": 245.8,
|
||||
"exceptionType": "",
|
||||
"groupId": 16238,
|
||||
"internalFlag": 0,
|
||||
"leId": 16308,
|
||||
"leName": "张传伟",
|
||||
"overrideBsId": 0,
|
||||
"paymentMethod": "",
|
||||
"sourceCatalogId": 0,
|
||||
"split": 0,
|
||||
"subBankstatementId": 0,
|
||||
"toDoFlag": 0,
|
||||
"transAmount": 245.8,
|
||||
"transFlag": "P",
|
||||
"transTypeId": 0,
|
||||
"transformAmount": 0,
|
||||
"transformCrAmount": 0,
|
||||
"transformDrAmount": 0,
|
||||
"transfromBalanceAmount": 0,
|
||||
"trxBalance": 0,
|
||||
"trxDate": "2024-02-01 10:33:44",
|
||||
"userMemo": "财付通消费_小店"
|
||||
},
|
||||
{
|
||||
"accountId": 0,
|
||||
"accountMaskNo": "101015251071645",
|
||||
"accountingDate": "2024-02-02",
|
||||
"accountingDateId": 20240202,
|
||||
"archivingFlag": 0,
|
||||
"attachments": 0,
|
||||
"balanceAmount": 5000.00,
|
||||
"bank": "ZJRCU",
|
||||
"bankComments": "",
|
||||
"bankStatementId": 12847663,
|
||||
"bankTrxNumber": "2b20568ee6d4477e8383396923e545gd",
|
||||
"batchId": 19135,
|
||||
"cashType": "1",
|
||||
"commentsNum": 0,
|
||||
"crAmount": 185.18,
|
||||
"cretNo": "230902199012261247",
|
||||
"currency": "CNY",
|
||||
"customerAccountMaskNo": "123456789",
|
||||
"customerBank": "",
|
||||
"customerId": -1,
|
||||
"customerName": "支付宝",
|
||||
"customerReference": "",
|
||||
"downPaymentFlag": 0,
|
||||
"drAmount": 0,
|
||||
"exceptionType": "",
|
||||
"groupId": 16238,
|
||||
"internalFlag": 0,
|
||||
"leId": 16308,
|
||||
"leName": "张传伟",
|
||||
"overrideBsId": 0,
|
||||
"paymentMethod": "",
|
||||
"sourceCatalogId": 0,
|
||||
"split": 0,
|
||||
"subBankstatementId": 0,
|
||||
"toDoFlag": 0,
|
||||
"transAmount": 185.18,
|
||||
"transFlag": "R",
|
||||
"transTypeId": 0,
|
||||
"transformAmount": 0,
|
||||
"transformCrAmount": 0,
|
||||
"transformDrAmount": 0,
|
||||
"transfromBalanceAmount": 0,
|
||||
"trxBalance": 0,
|
||||
"trxDate": "2024-02-02 14:22:18",
|
||||
"userMemo": "支付宝转账_支付宝"
|
||||
}
|
||||
],
|
||||
"totalCount": 131
|
||||
},
|
||||
"status": "200",
|
||||
"successResponse": true
|
||||
}
|
||||
}
|
||||
41
lsfx-mock-server/config/responses/parse_status.json
Normal file
41
lsfx-mock-server/config/responses/parse_status.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"success_response": {
|
||||
"code": "200",
|
||||
"data": {
|
||||
"parsing": false,
|
||||
"pendingList": [
|
||||
{
|
||||
"accountNoList": [],
|
||||
"bankName": "ZJRCU",
|
||||
"dataTypeInfo": ["CSV", ","],
|
||||
"downloadFileName": "230902199012261247_20260201_20260201_1772096608615.csv",
|
||||
"enterpriseNameList": [],
|
||||
"filePackageId": "cde6c7cf5cab48e8892f0c1c36b2aa7d",
|
||||
"fileSize": 53101,
|
||||
"fileUploadBy": 448,
|
||||
"fileUploadByUserName": "admin@support.com",
|
||||
"fileUploadTime": "2026-02-27 09:50:18",
|
||||
"isSplit": 0,
|
||||
"leId": 16210,
|
||||
"logId": "{log_id}",
|
||||
"logMeta": "{\"lostHeader\":[],\"balanceAmount\":true}",
|
||||
"logType": "bankstatement",
|
||||
"loginLeId": 16210,
|
||||
"lostHeader": [],
|
||||
"realBankName": "ZJRCU",
|
||||
"rows": 0,
|
||||
"source": "http",
|
||||
"status": -5,
|
||||
"templateName": "ZJRCU_T251114",
|
||||
"totalRecords": 131,
|
||||
"trxDateEndId": 20240228,
|
||||
"trxDateStartId": 20240201,
|
||||
"uploadFileName": "230902199012261247_20260201_20260201_1772096608615.csv",
|
||||
"uploadStatusDesc": "data.wait.confirm.newaccount"
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "200",
|
||||
"successResponse": true
|
||||
}
|
||||
}
|
||||
15
lsfx-mock-server/config/responses/token.json
Normal file
15
lsfx-mock-server/config/responses/token.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"success_response": {
|
||||
"code": "200",
|
||||
"data": {
|
||||
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.mock_token_{project_id}",
|
||||
"projectId": "{project_id}",
|
||||
"projectNo": "{project_no}",
|
||||
"entityName": "{entity_name}",
|
||||
"analysisType": 0
|
||||
},
|
||||
"message": "create.token.success",
|
||||
"status": "200",
|
||||
"successResponse": true
|
||||
}
|
||||
}
|
||||
49
lsfx-mock-server/config/responses/upload.json
Normal file
49
lsfx-mock-server/config/responses/upload.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"success_response": {
|
||||
"code": "200",
|
||||
"data": {
|
||||
"accountsOfLog": {
|
||||
"{log_id}": [
|
||||
{
|
||||
"bank": "BSX",
|
||||
"accountName": "测试账户",
|
||||
"accountNo": "6222021234567890",
|
||||
"currency": "CNY"
|
||||
}
|
||||
]
|
||||
},
|
||||
"uploadLogList": [
|
||||
{
|
||||
"accountNoList": [],
|
||||
"bankName": "BSX",
|
||||
"dataTypeInfo": ["CSV", ","],
|
||||
"downloadFileName": "测试流水.csv",
|
||||
"enterpriseNameList": [],
|
||||
"filePackageId": "14b13103010e4d32b5406c764cfe3644",
|
||||
"fileSize": 46724,
|
||||
"fileUploadBy": 448,
|
||||
"fileUploadByUserName": "admin@support.com",
|
||||
"fileUploadTime": "{upload_time}",
|
||||
"leId": 10724,
|
||||
"logId": "{log_id}",
|
||||
"logMeta": "{\"lostHeader\":[],\"balanceAmount\":true}",
|
||||
"logType": "bankstatement",
|
||||
"loginLeId": 10724,
|
||||
"realBankName": "BSX",
|
||||
"rows": 0,
|
||||
"source": "http",
|
||||
"status": -5,
|
||||
"templateName": "BSX_T240925",
|
||||
"totalRecords": 280,
|
||||
"trxDateEndId": 20240905,
|
||||
"trxDateStartId": 20230914,
|
||||
"uploadFileName": "测试流水.csv",
|
||||
"uploadStatusDesc": "data.wait.confirm.newaccount"
|
||||
}
|
||||
],
|
||||
"uploadStatus": 1
|
||||
},
|
||||
"status": "200",
|
||||
"successResponse": true
|
||||
}
|
||||
}
|
||||
30
lsfx-mock-server/config/settings.py
Normal file
30
lsfx-mock-server/config/settings.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""全局配置类"""
|
||||
|
||||
# 应用配置
|
||||
APP_NAME: str = "流水分析Mock服务"
|
||||
APP_VERSION: str = "1.0.0"
|
||||
DEBUG: bool = True
|
||||
|
||||
# 服务器配置
|
||||
HOST: str = "0.0.0.0"
|
||||
PORT: int = 8000
|
||||
|
||||
# 模拟配置
|
||||
PARSE_DELAY_SECONDS: int = 4 # 文件解析延迟秒数
|
||||
MAX_FILE_SIZE: int = 10485760 # 10MB
|
||||
|
||||
# 测试数据配置
|
||||
INITIAL_PROJECT_ID: int = 1000
|
||||
INITIAL_LOG_ID: int = 10000
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
env_file_encoding = "utf-8"
|
||||
|
||||
|
||||
settings = Settings()
|
||||
17
lsfx-mock-server/docker-compose.yml
Normal file
17
lsfx-mock-server/docker-compose.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
lsfx-mock-server:
|
||||
build: .
|
||||
container_name: lsfx-mock-server
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
- APP_NAME=流水分析Mock服务
|
||||
- APP_VERSION=1.0.0
|
||||
- DEBUG=true
|
||||
- HOST=0.0.0.0
|
||||
- PORT=8000
|
||||
- PARSE_DELAY_SECONDS=4
|
||||
- MAX_FILE_SIZE=10485760
|
||||
restart: unless-stopped
|
||||
80
lsfx-mock-server/main.py
Normal file
80
lsfx-mock-server/main.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""
|
||||
流水分析Mock服务器 - 主应用入口
|
||||
|
||||
基于 FastAPI 实现的 Mock 服务器,用于模拟流水分析平台的 7 个核心接口
|
||||
"""
|
||||
from fastapi import FastAPI
|
||||
from routers import api
|
||||
from config.settings import settings
|
||||
|
||||
# 创建 FastAPI 应用实例
|
||||
app = FastAPI(
|
||||
title=settings.APP_NAME,
|
||||
description="""
|
||||
## 流水分析 Mock 服务器
|
||||
|
||||
模拟流水分析平台的 7 个核心接口,用于开发和测试。
|
||||
|
||||
### 主要功能
|
||||
|
||||
- **Token管理** - 创建项目并获取访问Token
|
||||
- **文件上传** - 上传流水文件,支持异步解析(4秒延迟)
|
||||
- **行内流水** - 拉取行内流水数据
|
||||
- **解析状态** - 轮询检查文件解析状态
|
||||
- **文件删除** - 批量删除上传的文件
|
||||
- **流水查询** - 分页获取银行流水数据
|
||||
|
||||
### 错误模拟
|
||||
|
||||
在请求参数中包含 `error_XXXX` 标记可触发对应的错误响应。
|
||||
|
||||
例如:`projectNo: "test_error_40101"` 将返回 40101 错误。
|
||||
|
||||
### 使用方式
|
||||
|
||||
1. 获取Token: POST /account/common/getToken
|
||||
2. 上传文件: POST /watson/api/project/remoteUploadSplitFile
|
||||
3. 轮询解析状态: POST /watson/api/project/upload/getpendings
|
||||
4. 获取流水: POST /watson/api/project/getBSByLogId
|
||||
""",
|
||||
version=settings.APP_VERSION,
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
)
|
||||
|
||||
# 包含 API 路由
|
||||
app.include_router(api.router, tags=["流水分析接口"])
|
||||
|
||||
|
||||
@app.get("/", summary="服务根路径")
|
||||
async def root():
|
||||
"""服务根路径,返回基本信息"""
|
||||
return {
|
||||
"service": settings.APP_NAME,
|
||||
"version": settings.APP_VERSION,
|
||||
"swagger_docs": "/docs",
|
||||
"redoc": "/redoc",
|
||||
"status": "running",
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health", summary="健康检查")
|
||||
async def health_check():
|
||||
"""健康检查端点"""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": settings.APP_NAME,
|
||||
"version": settings.APP_VERSION,
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
# 启动服务器
|
||||
uvicorn.run(
|
||||
app,
|
||||
host=settings.HOST,
|
||||
port=settings.PORT,
|
||||
log_level="debug" if settings.DEBUG else "info",
|
||||
)
|
||||
1
lsfx-mock-server/models/__init__.py
Normal file
1
lsfx-mock-server/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Models package
|
||||
50
lsfx-mock-server/models/request.py
Normal file
50
lsfx-mock-server/models/request.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
class GetTokenRequest(BaseModel):
|
||||
"""获取Token请求模型"""
|
||||
projectNo: str = Field(..., description="项目编号,格式:902000_当前时间戳")
|
||||
entityName: str = Field(..., description="项目名称")
|
||||
userId: str = Field(..., description="操作人员编号,固定值")
|
||||
userName: str = Field(..., description="操作人员姓名,固定值")
|
||||
orgCode: str = Field(..., description="行社机构号,固定值")
|
||||
entityId: Optional[str] = Field(None, description="企业统信码或个人身份证号")
|
||||
xdRelatedPersons: Optional[str] = Field(None, description="信贷关联人信息")
|
||||
jzDataDateId: Optional[str] = Field("0", description="拉取指定日期推送过来的金综链流水")
|
||||
innerBSStartDateId: Optional[str] = Field("0", description="拉取行内流水开始日期")
|
||||
innerBSEndDateId: Optional[str] = Field("0", description="拉取行内流水结束日期")
|
||||
analysisType: Optional[int] = Field(-1, description="分析类型")
|
||||
departmentCode: Optional[str] = Field(None, description="客户经理所属营业部/分理处的机构编码")
|
||||
|
||||
|
||||
class FetchInnerFlowRequest(BaseModel):
|
||||
"""拉取行内流水请求模型"""
|
||||
groupId: int = Field(..., description="项目id")
|
||||
customerNo: str = Field(..., description="客户身份证号")
|
||||
dataChannelCode: str = Field(..., description="校验码")
|
||||
requestDateId: int = Field(..., description="发起请求的时间")
|
||||
dataStartDateId: int = Field(..., description="拉取开始日期")
|
||||
dataEndDateId: int = Field(..., description="拉取结束日期")
|
||||
uploadUserId: int = Field(..., description="柜员号")
|
||||
|
||||
|
||||
class CheckParseStatusRequest(BaseModel):
|
||||
"""检查文件解析状态请求模型"""
|
||||
groupId: int = Field(..., description="项目id")
|
||||
inprogressList: str = Field(..., description="文件id列表,逗号分隔")
|
||||
|
||||
|
||||
class GetBankStatementRequest(BaseModel):
|
||||
"""获取银行流水请求模型"""
|
||||
groupId: int = Field(..., description="项目id")
|
||||
logId: int = Field(..., description="文件id")
|
||||
pageNow: int = Field(..., description="当前页码")
|
||||
pageSize: int = Field(..., description="查询条数")
|
||||
|
||||
|
||||
class DeleteFilesRequest(BaseModel):
|
||||
"""删除文件请求模型"""
|
||||
groupId: int = Field(..., description="项目id")
|
||||
logIds: List[int] = Field(..., description="文件id数组")
|
||||
userId: int = Field(..., description="用户柜员号")
|
||||
187
lsfx-mock-server/models/response.py
Normal file
187
lsfx-mock-server/models/response.py
Normal file
@@ -0,0 +1,187 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
|
||||
# ==================== Token相关模型 ====================
|
||||
|
||||
class TokenData(BaseModel):
|
||||
"""Token数据"""
|
||||
token: str = Field(..., description="token")
|
||||
projectId: int = Field(..., description="见知项目Id")
|
||||
projectNo: str = Field(..., description="项目编号")
|
||||
entityName: str = Field(..., description="项目名称")
|
||||
analysisType: int = Field(0, description="分析类型")
|
||||
|
||||
|
||||
class GetTokenResponse(BaseModel):
|
||||
"""获取Token响应"""
|
||||
code: str = Field("200", description="返回码")
|
||||
data: Optional[TokenData] = Field(None, description="返回数据")
|
||||
message: str = Field("create.token.success", description="返回消息")
|
||||
status: str = Field("200", description="状态")
|
||||
successResponse: bool = Field(True, description="是否成功响应")
|
||||
|
||||
|
||||
# ==================== 文件上传相关模型 ====================
|
||||
|
||||
class AccountInfo(BaseModel):
|
||||
"""账户信息"""
|
||||
bank: str = Field(..., description="银行")
|
||||
accountName: str = Field(..., description="账户名称")
|
||||
accountNo: str = Field(..., description="账号")
|
||||
currency: str = Field(..., description="币种")
|
||||
|
||||
|
||||
class UploadLogItem(BaseModel):
|
||||
"""上传日志项"""
|
||||
accountNoList: List[str] = Field(default=[], description="账号列表")
|
||||
bankName: str = Field(..., description="银行名称")
|
||||
dataTypeInfo: List[str] = Field(default=[], description="数据类型信息")
|
||||
downloadFileName: str = Field(..., description="下载文件名")
|
||||
enterpriseNameList: List[str] = Field(default=[], description="企业名称列表")
|
||||
filePackageId: str = Field(..., description="文件包ID")
|
||||
fileSize: int = Field(..., description="文件大小")
|
||||
fileUploadBy: int = Field(..., description="上传者ID")
|
||||
fileUploadByUserName: str = Field(..., description="上传者用户名")
|
||||
fileUploadTime: str = Field(..., description="上传时间")
|
||||
leId: int = Field(..., description="企业ID")
|
||||
logId: int = Field(..., description="日志ID")
|
||||
logMeta: str = Field(..., description="日志元数据")
|
||||
logType: str = Field(..., description="日志类型")
|
||||
loginLeId: int = Field(..., description="登录企业ID")
|
||||
realBankName: str = Field(..., description="真实银行名称")
|
||||
rows: int = Field(0, description="行数")
|
||||
source: str = Field(..., description="来源")
|
||||
status: int = Field(-5, description="状态值")
|
||||
templateName: str = Field(..., description="模板名称")
|
||||
totalRecords: int = Field(0, description="总记录数")
|
||||
trxDateEndId: int = Field(..., description="交易结束日期ID")
|
||||
trxDateStartId: int = Field(..., description="交易开始日期ID")
|
||||
uploadFileName: str = Field(..., description="上传文件名")
|
||||
uploadStatusDesc: str = Field(..., description="上传状态描述")
|
||||
|
||||
|
||||
class UploadFileResponse(BaseModel):
|
||||
"""上传文件响应"""
|
||||
code: str = Field("200", description="返回码")
|
||||
data: Optional[Dict[str, Any]] = Field(None, description="返回数据")
|
||||
status: str = Field("200", description="状态")
|
||||
successResponse: bool = Field(True, description="是否成功响应")
|
||||
|
||||
|
||||
# ==================== 检查解析状态相关模型 ====================
|
||||
|
||||
class PendingItem(BaseModel):
|
||||
"""待处理项"""
|
||||
accountNoList: List[str] = Field(default=[], description="账号列表")
|
||||
bankName: str = Field(..., description="银行名称")
|
||||
dataTypeInfo: List[str] = Field(default=[], description="数据类型信息")
|
||||
downloadFileName: str = Field(..., description="下载文件名")
|
||||
enterpriseNameList: List[str] = Field(default=[], description="企业名称列表")
|
||||
filePackageId: str = Field(..., description="文件包ID")
|
||||
fileSize: int = Field(..., description="文件大小")
|
||||
fileUploadBy: int = Field(..., description="上传者ID")
|
||||
fileUploadByUserName: str = Field(..., description="上传者用户名")
|
||||
fileUploadTime: str = Field(..., description="上传时间")
|
||||
isSplit: int = Field(0, description="是否分割")
|
||||
leId: int = Field(..., description="企业ID")
|
||||
logId: int = Field(..., description="日志ID")
|
||||
logMeta: str = Field(..., description="日志元数据")
|
||||
logType: str = Field(..., description="日志类型")
|
||||
loginLeId: int = Field(..., description="登录企业ID")
|
||||
lostHeader: List[str] = Field(default=[], description="丢失的头部")
|
||||
realBankName: str = Field(..., description="真实银行名称")
|
||||
rows: int = Field(0, description="行数")
|
||||
source: str = Field(..., description="来源")
|
||||
status: int = Field(-5, description="状态值")
|
||||
templateName: str = Field(..., description="模板名称")
|
||||
totalRecords: int = Field(0, description="总记录数")
|
||||
trxDateEndId: int = Field(..., description="交易结束日期ID")
|
||||
trxDateStartId: int = Field(..., description="交易开始日期ID")
|
||||
uploadFileName: str = Field(..., description="上传文件名")
|
||||
uploadStatusDesc: str = Field(..., description="上传状态描述")
|
||||
|
||||
|
||||
class CheckParseStatusResponse(BaseModel):
|
||||
"""检查解析状态响应"""
|
||||
code: str = Field("200", description="返回码")
|
||||
data: Optional[Dict[str, Any]] = Field(None, description="返回数据,包含parsing和pendingList")
|
||||
status: str = Field("200", description="状态")
|
||||
successResponse: bool = Field(True, description="是否成功响应")
|
||||
|
||||
|
||||
# ==================== 银行流水相关模型 ====================
|
||||
|
||||
class BankStatementItem(BaseModel):
|
||||
"""银行流水项"""
|
||||
accountId: int = Field(0, description="账号ID")
|
||||
accountMaskNo: str = Field(..., description="账号")
|
||||
accountingDate: str = Field(..., description="记账日期")
|
||||
accountingDateId: int = Field(..., description="记账日期ID")
|
||||
archivingFlag: int = Field(0, description="归档标志")
|
||||
attachments: int = Field(0, description="附件数")
|
||||
balanceAmount: float = Field(..., description="余额")
|
||||
bank: str = Field(..., description="银行")
|
||||
bankComments: str = Field("", description="银行注释")
|
||||
bankStatementId: int = Field(..., description="流水ID")
|
||||
bankTrxNumber: str = Field(..., description="银行交易号")
|
||||
batchId: int = Field(..., description="批次ID")
|
||||
cashType: str = Field("1", description="现金类型")
|
||||
commentsNum: int = Field(0, description="评论数")
|
||||
crAmount: float = Field(0, description="贷方金额")
|
||||
cretNo: str = Field(..., description="证件号")
|
||||
currency: str = Field("CNY", description="币种")
|
||||
customerAccountMaskNo: str = Field(..., description="客户账号")
|
||||
customerBank: str = Field("", description="客户银行")
|
||||
customerId: int = Field(-1, description="客户ID")
|
||||
customerName: str = Field(..., description="客户名称")
|
||||
customerReference: str = Field("", description="客户参考")
|
||||
downPaymentFlag: int = Field(0, description="首付标志")
|
||||
drAmount: float = Field(0, description="借方金额")
|
||||
exceptionType: str = Field("", description="异常类型")
|
||||
groupId: int = Field(0, description="项目ID")
|
||||
internalFlag: int = Field(0, description="内部标志")
|
||||
leId: int = Field(..., description="企业ID")
|
||||
leName: str = Field(..., description="企业名称")
|
||||
overrideBsId: int = Field(0, description="覆盖流水ID")
|
||||
paymentMethod: str = Field("", description="支付方式")
|
||||
sourceCatalogId: int = Field(0, description="来源目录ID")
|
||||
split: int = Field(0, description="分割")
|
||||
subBankstatementId: int = Field(0, description="子流水ID")
|
||||
toDoFlag: int = Field(0, description="待办标志")
|
||||
transAmount: float = Field(..., description="交易金额")
|
||||
transFlag: str = Field("P", description="交易标志")
|
||||
transTypeId: int = Field(0, description="交易类型ID")
|
||||
transformAmount: int = Field(0, description="转换金额")
|
||||
transformCrAmount: int = Field(0, description="转换贷方金额")
|
||||
transformDrAmount: int = Field(0, description="转换借方金额")
|
||||
transfromBalanceAmount: int = Field(0, description="转换余额")
|
||||
trxBalance: int = Field(0, description="交易余额")
|
||||
trxDate: str = Field(..., description="交易日期")
|
||||
userMemo: str = Field(..., description="用户备注")
|
||||
|
||||
|
||||
class GetBankStatementResponse(BaseModel):
|
||||
"""获取银行流水响应"""
|
||||
code: str = Field("200", description="返回码")
|
||||
data: Optional[Dict[str, Any]] = Field(None, description="返回数据,包含bankStatementList和totalCount")
|
||||
status: str = Field("200", description="状态")
|
||||
successResponse: bool = Field(True, description="是否成功响应")
|
||||
|
||||
|
||||
# ==================== 其他响应模型 ====================
|
||||
|
||||
class FetchInnerFlowResponse(BaseModel):
|
||||
"""拉取行内流水响应"""
|
||||
code: str = Field("200", description="返回码")
|
||||
data: Optional[Dict[str, Any]] = Field(None, description="返回数据")
|
||||
status: str = Field("200", description="状态")
|
||||
successResponse: bool = Field(True, description="是否成功响应")
|
||||
|
||||
|
||||
class DeleteFilesResponse(BaseModel):
|
||||
"""删除文件响应"""
|
||||
code: str = Field("200", description="返回码")
|
||||
data: Optional[Dict[str, str]] = Field(None, description="返回数据")
|
||||
status: str = Field("200", description="状态")
|
||||
successResponse: bool = Field(True, description="是否成功响应")
|
||||
8
lsfx-mock-server/requirements.txt
Normal file
8
lsfx-mock-server/requirements.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
pydantic==2.5.0
|
||||
pydantic-settings==2.1.0
|
||||
python-multipart==0.0.6
|
||||
pytest>=7.0.0
|
||||
pytest-cov>=4.0.0
|
||||
httpx>=0.25.0
|
||||
1
lsfx-mock-server/routers/__init__.py
Normal file
1
lsfx-mock-server/routers/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Routers package
|
||||
BIN
lsfx-mock-server/routers/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
lsfx-mock-server/routers/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
lsfx-mock-server/routers/__pycache__/api.cpython-313.pyc
Normal file
BIN
lsfx-mock-server/routers/__pycache__/api.cpython-313.pyc
Normal file
Binary file not shown.
99
lsfx-mock-server/routers/api.py
Normal file
99
lsfx-mock-server/routers/api.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from fastapi import APIRouter, BackgroundTasks, UploadFile, File, Form
|
||||
from models.request import (
|
||||
GetTokenRequest,
|
||||
FetchInnerFlowRequest,
|
||||
CheckParseStatusRequest,
|
||||
GetBankStatementRequest,
|
||||
DeleteFilesRequest,
|
||||
)
|
||||
from services.token_service import TokenService
|
||||
from services.file_service import FileService
|
||||
from services.statement_service import StatementService
|
||||
from utils.error_simulator import ErrorSimulator
|
||||
from typing import List
|
||||
|
||||
# 创建路由器
|
||||
router = APIRouter()
|
||||
|
||||
# 初始化服务实例
|
||||
token_service = TokenService()
|
||||
file_service = FileService()
|
||||
statement_service = StatementService()
|
||||
|
||||
|
||||
# ==================== 接口1:获取Token ====================
|
||||
@router.post("/account/common/getToken")
|
||||
async def get_token(request: GetTokenRequest):
|
||||
"""创建项目并获取访问Token
|
||||
|
||||
如果 projectNo 包含 error_XXXX 标记,将返回对应的错误响应
|
||||
"""
|
||||
# 检测错误标记
|
||||
error_code = ErrorSimulator.detect_error_marker(request.projectNo)
|
||||
if error_code:
|
||||
return ErrorSimulator.build_error_response(error_code)
|
||||
|
||||
# 正常流程
|
||||
return token_service.create_token(request)
|
||||
|
||||
|
||||
# ==================== 接口2:上传文件 ====================
|
||||
@router.post("/watson/api/project/remoteUploadSplitFile")
|
||||
async def upload_file(
|
||||
background_tasks: BackgroundTasks,
|
||||
groupId: int = Form(..., description="项目ID"),
|
||||
file: UploadFile = File(..., description="流水文件"),
|
||||
):
|
||||
"""上传流水文件
|
||||
|
||||
文件将立即返回,并在后台延迟4秒完成解析
|
||||
"""
|
||||
return await file_service.upload_file(groupId, file, background_tasks)
|
||||
|
||||
|
||||
# ==================== 接口3:拉取行内流水 ====================
|
||||
@router.post("/watson/api/project/getJZFileOrZjrcuFile")
|
||||
async def fetch_inner_flow(request: FetchInnerFlowRequest):
|
||||
"""拉取行内流水
|
||||
|
||||
如果 customerNo 包含 error_XXXX 标记,将返回对应的错误响应
|
||||
"""
|
||||
# 检测错误标记
|
||||
error_code = ErrorSimulator.detect_error_marker(request.customerNo)
|
||||
if error_code:
|
||||
return ErrorSimulator.build_error_response(error_code)
|
||||
|
||||
# 正常流程
|
||||
return file_service.fetch_inner_flow(request)
|
||||
|
||||
|
||||
# ==================== 接口4:检查文件解析状态 ====================
|
||||
@router.post("/watson/api/project/upload/getpendings")
|
||||
async def check_parse_status(request: CheckParseStatusRequest):
|
||||
"""检查文件解析状态
|
||||
|
||||
返回文件是否还在解析中(parsing字段)
|
||||
"""
|
||||
return file_service.check_parse_status(
|
||||
request.groupId, request.inprogressList
|
||||
)
|
||||
|
||||
|
||||
# ==================== 接口5:删除文件 ====================
|
||||
@router.post("/watson/api/project/batchDeleteUploadFile")
|
||||
async def delete_files(request: DeleteFilesRequest):
|
||||
"""批量删除上传的文件
|
||||
|
||||
根据logIds列表删除对应的文件记录
|
||||
"""
|
||||
return file_service.delete_files(request.groupId, request.logIds, request.userId)
|
||||
|
||||
|
||||
# ==================== 接口6:获取银行流水 ====================
|
||||
@router.post("/watson/api/project/getBSByLogId")
|
||||
async def get_bank_statement(request: GetBankStatementRequest):
|
||||
"""获取银行流水列表
|
||||
|
||||
支持分页查询(pageNow, pageSize)
|
||||
"""
|
||||
return statement_service.get_bank_statement(request)
|
||||
1
lsfx-mock-server/services/__init__.py
Normal file
1
lsfx-mock-server/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Services package
|
||||
BIN
lsfx-mock-server/services/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
lsfx-mock-server/services/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
151
lsfx-mock-server/services/file_service.py
Normal file
151
lsfx-mock-server/services/file_service.py
Normal file
@@ -0,0 +1,151 @@
|
||||
from fastapi import BackgroundTasks, UploadFile
|
||||
from models.request import FetchInnerFlowRequest
|
||||
from utils.response_builder import ResponseBuilder
|
||||
from config.settings import settings
|
||||
from typing import Dict, List
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class FileService:
|
||||
"""文件上传和解析服务"""
|
||||
|
||||
def __init__(self):
|
||||
self.file_records = {} # logId -> record
|
||||
self.parsing_status = {} # logId -> is_parsing
|
||||
self.log_counter = settings.INITIAL_LOG_ID
|
||||
|
||||
async def upload_file(
|
||||
self, group_id: int, file: UploadFile, background_tasks: BackgroundTasks
|
||||
) -> Dict:
|
||||
"""上传文件并启动后台解析任务
|
||||
|
||||
Args:
|
||||
group_id: 项目ID
|
||||
file: 上传的文件
|
||||
background_tasks: FastAPI后台任务
|
||||
|
||||
Returns:
|
||||
上传响应字典
|
||||
"""
|
||||
# 生成唯一logId
|
||||
self.log_counter += 1
|
||||
log_id = self.log_counter
|
||||
|
||||
# 获取当前时间
|
||||
upload_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# 立即存储文件记录(初始状态:解析中)
|
||||
self.file_records[log_id] = {
|
||||
"logId": log_id,
|
||||
"groupId": group_id,
|
||||
"status": -5,
|
||||
"uploadStatusDesc": "parsing",
|
||||
"uploadFileName": file.filename,
|
||||
"fileSize": 0, # 简化处理
|
||||
"bankName": "MOCK",
|
||||
"uploadTime": upload_time,
|
||||
}
|
||||
|
||||
# 标记为解析中
|
||||
self.parsing_status[log_id] = True
|
||||
|
||||
# 启动后台任务,延迟解析
|
||||
background_tasks.add_task(
|
||||
self._simulate_parsing, log_id, settings.PARSE_DELAY_SECONDS
|
||||
)
|
||||
|
||||
# 构建响应
|
||||
response = ResponseBuilder.build_success_response(
|
||||
"upload", log_id=log_id, upload_time=upload_time
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def _simulate_parsing(self, log_id: int, delay_seconds: int):
|
||||
"""后台任务:模拟文件解析过程
|
||||
|
||||
Args:
|
||||
log_id: 日志ID
|
||||
delay_seconds: 延迟秒数
|
||||
"""
|
||||
time.sleep(delay_seconds)
|
||||
|
||||
# 解析完成,更新状态
|
||||
if log_id in self.file_records:
|
||||
self.file_records[log_id]["uploadStatusDesc"] = (
|
||||
"data.wait.confirm.newaccount"
|
||||
)
|
||||
self.parsing_status[log_id] = False
|
||||
|
||||
def check_parse_status(self, group_id: int, inprogress_list: str) -> Dict:
|
||||
"""检查文件解析状态
|
||||
|
||||
Args:
|
||||
group_id: 项目ID
|
||||
inprogress_list: 文件ID列表(逗号分隔)
|
||||
|
||||
Returns:
|
||||
解析状态响应字典
|
||||
"""
|
||||
# 解析logId列表
|
||||
log_ids = [int(x.strip()) for x in inprogress_list.split(",") if x.strip()]
|
||||
|
||||
# 检查是否还在解析中
|
||||
is_parsing = any(
|
||||
self.parsing_status.get(log_id, False) for log_id in log_ids
|
||||
)
|
||||
|
||||
# 获取待处理列表
|
||||
pending_list = [
|
||||
self.file_records[log_id]
|
||||
for log_id in log_ids
|
||||
if log_id in self.file_records
|
||||
]
|
||||
|
||||
return {
|
||||
"code": "200",
|
||||
"data": {"parsing": is_parsing, "pendingList": pending_list},
|
||||
"status": "200",
|
||||
"successResponse": True,
|
||||
}
|
||||
|
||||
def delete_files(self, group_id: int, log_ids: List[int], user_id: int) -> Dict:
|
||||
"""删除文件
|
||||
|
||||
Args:
|
||||
group_id: 项目ID
|
||||
log_ids: 文件ID列表
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
删除响应字典
|
||||
"""
|
||||
# 删除文件记录
|
||||
for log_id in log_ids:
|
||||
self.file_records.pop(log_id, None)
|
||||
self.parsing_status.pop(log_id, None)
|
||||
|
||||
return {
|
||||
"code": "200",
|
||||
"data": {"message": "delete.files.success"},
|
||||
"status": "200",
|
||||
"successResponse": True,
|
||||
}
|
||||
|
||||
def fetch_inner_flow(self, request: FetchInnerFlowRequest) -> Dict:
|
||||
"""拉取行内流水(模拟无数据场景)
|
||||
|
||||
Args:
|
||||
request: 拉取流水请求
|
||||
|
||||
Returns:
|
||||
流水响应字典
|
||||
"""
|
||||
# 模拟无行内流水文件场景
|
||||
return {
|
||||
"code": "200",
|
||||
"data": {"code": "501014", "message": "无行内流水文件"},
|
||||
"status": "200",
|
||||
"successResponse": True,
|
||||
}
|
||||
33
lsfx-mock-server/services/statement_service.py
Normal file
33
lsfx-mock-server/services/statement_service.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from models.request import GetBankStatementRequest
|
||||
from utils.response_builder import ResponseBuilder
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class StatementService:
|
||||
"""流水数据服务"""
|
||||
|
||||
def get_bank_statement(self, request: GetBankStatementRequest) -> Dict:
|
||||
"""获取银行流水列表
|
||||
|
||||
Args:
|
||||
request: 获取银行流水请求
|
||||
|
||||
Returns:
|
||||
银行流水响应字典
|
||||
"""
|
||||
# 加载模板
|
||||
template = ResponseBuilder.load_template("bank_statement")
|
||||
statements = template["success_response"]["data"]["bankStatementList"]
|
||||
total_count = len(statements)
|
||||
|
||||
# 模拟分页
|
||||
start = (request.pageNow - 1) * request.pageSize
|
||||
end = start + request.pageSize
|
||||
page_data = statements[start:end]
|
||||
|
||||
return {
|
||||
"code": "200",
|
||||
"data": {"bankStatementList": page_data, "totalCount": total_count},
|
||||
"status": "200",
|
||||
"successResponse": True,
|
||||
}
|
||||
49
lsfx-mock-server/services/token_service.py
Normal file
49
lsfx-mock-server/services/token_service.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from models.request import GetTokenRequest
|
||||
from utils.response_builder import ResponseBuilder
|
||||
from config.settings import settings
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class TokenService:
|
||||
"""Token管理服务"""
|
||||
|
||||
def __init__(self):
|
||||
self.project_counter = settings.INITIAL_PROJECT_ID
|
||||
self.tokens = {} # projectId -> token_data
|
||||
|
||||
def create_token(self, request: GetTokenRequest) -> Dict:
|
||||
"""创建Token
|
||||
|
||||
Args:
|
||||
request: 获取Token请求
|
||||
|
||||
Returns:
|
||||
Token响应字典
|
||||
"""
|
||||
# 生成唯一项目ID
|
||||
self.project_counter += 1
|
||||
project_id = self.project_counter
|
||||
|
||||
# 构建响应
|
||||
response = ResponseBuilder.build_success_response(
|
||||
"token",
|
||||
project_id=project_id,
|
||||
project_no=request.projectNo,
|
||||
entity_name=request.entityName
|
||||
)
|
||||
|
||||
# 存储token信息
|
||||
self.tokens[project_id] = response.get("data")
|
||||
|
||||
return response
|
||||
|
||||
def get_project(self, project_id: int) -> Dict:
|
||||
"""获取项目信息
|
||||
|
||||
Args:
|
||||
project_id: 项目ID
|
||||
|
||||
Returns:
|
||||
项目信息字典
|
||||
"""
|
||||
return self.tokens.get(project_id)
|
||||
1
lsfx-mock-server/tests/__init__.py
Normal file
1
lsfx-mock-server/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Tests package
|
||||
30
lsfx-mock-server/tests/conftest.py
Normal file
30
lsfx-mock-server/tests/conftest.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Pytest 配置和共享 fixtures
|
||||
"""
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加项目根目录到 sys.path
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from main import app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
"""创建测试客户端"""
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_token_request():
|
||||
"""示例 Token 请求"""
|
||||
return {
|
||||
"projectNo": "test_project_001",
|
||||
"entityName": "测试企业",
|
||||
"userId": "902001",
|
||||
"userName": "902001",
|
||||
"orgCode": "902000",
|
||||
}
|
||||
1
lsfx-mock-server/tests/integration/__init__.py
Normal file
1
lsfx-mock-server/tests/integration/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Integration tests package
|
||||
113
lsfx-mock-server/tests/integration/test_full_workflow.py
Normal file
113
lsfx-mock-server/tests/integration/test_full_workflow.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""
|
||||
集成测试 - 完整的接口调用流程测试
|
||||
"""
|
||||
import pytest
|
||||
import time
|
||||
|
||||
|
||||
def test_complete_workflow(client):
|
||||
"""测试完整的接口调用流程"""
|
||||
# 1. 获取 Token
|
||||
response = client.post(
|
||||
"/account/common/getToken",
|
||||
json={
|
||||
"projectNo": "integration_test_001",
|
||||
"entityName": "集成测试企业",
|
||||
"userId": "902001",
|
||||
"userName": "902001",
|
||||
"orgCode": "902000",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
token_data = response.json()
|
||||
assert token_data["code"] == "200"
|
||||
project_id = token_data["data"]["projectId"]
|
||||
token = token_data["data"]["token"]
|
||||
assert token is not None
|
||||
|
||||
# 2. 上传文件(模拟)
|
||||
# 注意:在测试环境中,我们跳过实际的文件上传,直接测试其他接口
|
||||
|
||||
# 3. 检查解析状态
|
||||
response = client.post(
|
||||
"/watson/api/project/upload/getpendings",
|
||||
json={"groupId": project_id, "inprogressList": "10001"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
status_data = response.json()
|
||||
assert "parsing" in status_data["data"]
|
||||
|
||||
# 4. 获取银行流水
|
||||
response = client.post(
|
||||
"/watson/api/project/getBSByLogId",
|
||||
json={
|
||||
"groupId": project_id,
|
||||
"logId": 10001,
|
||||
"pageNow": 1,
|
||||
"pageSize": 10,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
statement_data = response.json()
|
||||
assert statement_data["code"] == "200"
|
||||
assert "bankStatementList" in statement_data["data"]
|
||||
assert "totalCount" in statement_data["data"]
|
||||
|
||||
|
||||
def test_all_error_codes(client):
|
||||
"""测试所有错误码"""
|
||||
error_codes = ["40101", "40102", "40104", "40105", "40106", "40107", "40108"]
|
||||
|
||||
for error_code in error_codes:
|
||||
response = client.post(
|
||||
"/account/common/getToken",
|
||||
json={
|
||||
"projectNo": f"test_error_{error_code}",
|
||||
"entityName": "测试企业",
|
||||
"userId": "902001",
|
||||
"userName": "902001",
|
||||
"orgCode": "902000",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == error_code, f"错误码 {error_code} 未正确触发"
|
||||
assert data["successResponse"] == False
|
||||
|
||||
|
||||
def test_pagination(client):
|
||||
"""测试分页功能"""
|
||||
# 获取 Token
|
||||
response = client.post(
|
||||
"/account/common/getToken",
|
||||
json={
|
||||
"projectNo": "pagination_test",
|
||||
"entityName": "分页测试",
|
||||
"userId": "902001",
|
||||
"userName": "902001",
|
||||
"orgCode": "902000",
|
||||
},
|
||||
)
|
||||
project_id = response.json()["data"]["projectId"]
|
||||
|
||||
# 测试第一页
|
||||
response = client.post(
|
||||
"/watson/api/project/getBSByLogId",
|
||||
json={"groupId": project_id, "logId": 10001, "pageNow": 1, "pageSize": 1},
|
||||
)
|
||||
page1 = response.json()
|
||||
|
||||
# 测试第二页
|
||||
response = client.post(
|
||||
"/watson/api/project/getBSByLogId",
|
||||
json={"groupId": project_id, "logId": 10001, "pageNow": 2, "pageSize": 1},
|
||||
)
|
||||
page2 = response.json()
|
||||
|
||||
# 验证总记录数相同
|
||||
assert page1["data"]["totalCount"] == page2["data"]["totalCount"]
|
||||
|
||||
# 验证页码不同
|
||||
if page1["data"]["totalCount"] > 1:
|
||||
assert len(page1["data"]["bankStatementList"]) == 1
|
||||
assert len(page2["data"]["bankStatementList"]) >= 0
|
||||
46
lsfx-mock-server/tests/test_api.py
Normal file
46
lsfx-mock-server/tests/test_api.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
API 端点测试
|
||||
"""
|
||||
|
||||
|
||||
def test_root_endpoint(client):
|
||||
"""测试根路径"""
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "running"
|
||||
assert "swagger_docs" in data
|
||||
|
||||
|
||||
def test_health_check(client):
|
||||
"""测试健康检查端点"""
|
||||
response = client.get("/health")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "healthy"
|
||||
|
||||
|
||||
def test_get_token_success(client, sample_token_request):
|
||||
"""测试获取 Token - 成功场景"""
|
||||
response = client.post("/account/common/getToken", json=sample_token_request)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == "200"
|
||||
assert "token" in data["data"]
|
||||
assert "projectId" in data["data"]
|
||||
|
||||
|
||||
def test_get_token_error_40101(client):
|
||||
"""测试获取 Token - 错误场景 40101"""
|
||||
request_data = {
|
||||
"projectNo": "test_error_40101",
|
||||
"entityName": "测试企业",
|
||||
"userId": "902001",
|
||||
"userName": "902001",
|
||||
"orgCode": "902000",
|
||||
}
|
||||
response = client.post("/account/common/getToken", json=request_data)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == "40101"
|
||||
assert data["successResponse"] == False
|
||||
1
lsfx-mock-server/utils/__init__.py
Normal file
1
lsfx-mock-server/utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Utils package
|
||||
BIN
lsfx-mock-server/utils/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
lsfx-mock-server/utils/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
49
lsfx-mock-server/utils/error_simulator.py
Normal file
49
lsfx-mock-server/utils/error_simulator.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from typing import Dict, Optional
|
||||
import re
|
||||
|
||||
|
||||
class ErrorSimulator:
|
||||
"""错误场景模拟器"""
|
||||
|
||||
# 错误码映射表
|
||||
ERROR_CODES = {
|
||||
"40101": {"code": "40101", "message": "appId错误"},
|
||||
"40102": {"code": "40102", "message": "appSecretCode错误"},
|
||||
"40104": {"code": "40104", "message": "可使用项目次数为0,无法创建项目"},
|
||||
"40105": {"code": "40105", "message": "只读模式下无法新建项目"},
|
||||
"40106": {"code": "40106", "message": "错误的分析类型,不在规定的取值范围内"},
|
||||
"40107": {"code": "40107", "message": "当前系统不支持的分析类型"},
|
||||
"40108": {"code": "40108", "message": "当前用户所属行社无权限"},
|
||||
"501014": {"code": "501014", "message": "无行内流水文件"},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def detect_error_marker(value: str) -> Optional[str]:
|
||||
"""检测字符串中的错误标记
|
||||
|
||||
规则:如果字符串包含 error_XXXX,则返回 XXXX
|
||||
例如:
|
||||
- "project_error_40101" -> "40101"
|
||||
- "test_error_501014" -> "501014"
|
||||
"""
|
||||
if not value:
|
||||
return None
|
||||
|
||||
pattern = r'error_(\d+)'
|
||||
match = re.search(pattern, value)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def build_error_response(error_code: str) -> Optional[Dict]:
|
||||
"""构建错误响应"""
|
||||
if error_code in ErrorSimulator.ERROR_CODES:
|
||||
error_info = ErrorSimulator.ERROR_CODES[error_code]
|
||||
return {
|
||||
"code": error_info["code"],
|
||||
"message": error_info["message"],
|
||||
"status": error_info["code"],
|
||||
"successResponse": False
|
||||
}
|
||||
return None
|
||||
69
lsfx-mock-server/utils/response_builder.py
Normal file
69
lsfx-mock-server/utils/response_builder.py
Normal file
@@ -0,0 +1,69 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
import copy
|
||||
|
||||
|
||||
class ResponseBuilder:
|
||||
"""响应构建器"""
|
||||
|
||||
TEMPLATE_DIR = Path(__file__).parent.parent / "config" / "responses"
|
||||
|
||||
@staticmethod
|
||||
def load_template(template_name: str) -> Dict:
|
||||
"""加载 JSON 模板
|
||||
|
||||
Args:
|
||||
template_name: 模板名称(不含.json扩展名)
|
||||
|
||||
Returns:
|
||||
模板字典
|
||||
"""
|
||||
file_path = ResponseBuilder.TEMPLATE_DIR / f"{template_name}.json"
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
@staticmethod
|
||||
def replace_placeholders(template: Dict, **kwargs) -> Dict:
|
||||
"""递归替换占位符
|
||||
|
||||
Args:
|
||||
template: 模板字典
|
||||
**kwargs: 占位符键值对
|
||||
|
||||
Returns:
|
||||
替换后的字典
|
||||
"""
|
||||
def replace_value(value):
|
||||
if isinstance(value, str):
|
||||
result = value
|
||||
for key, val in kwargs.items():
|
||||
placeholder = f"{{{key}}}"
|
||||
if placeholder in result:
|
||||
result = result.replace(placeholder, str(val))
|
||||
return result
|
||||
elif isinstance(value, dict):
|
||||
return {k: replace_value(v) for k, v in value.items()}
|
||||
elif isinstance(value, list):
|
||||
return [replace_value(item) for item in value]
|
||||
return value
|
||||
|
||||
# 深拷贝模板,避免修改原始数据
|
||||
return replace_value(copy.deepcopy(template))
|
||||
|
||||
@staticmethod
|
||||
def build_success_response(template_name: str, **kwargs) -> Dict:
|
||||
"""构建成功响应
|
||||
|
||||
Args:
|
||||
template_name: 模板名称
|
||||
**kwargs: 占位符键值对
|
||||
|
||||
Returns:
|
||||
响应字典
|
||||
"""
|
||||
template = ResponseBuilder.load_template(template_name)
|
||||
return ResponseBuilder.replace_placeholders(
|
||||
template["success_response"],
|
||||
**kwargs
|
||||
)
|
||||
1
pom.xml
1
pom.xml
@@ -227,6 +227,7 @@
|
||||
<module>ruoyi-common</module>
|
||||
<module>ccdi-info-collection</module>
|
||||
<module>ccdi-project</module>
|
||||
<module>ccdi-lsfx</module>
|
||||
</modules>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
|
||||
@@ -66,6 +66,13 @@
|
||||
<artifactId>ccdi-project</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 流水分析平台对接-->
|
||||
<dependency>
|
||||
<groupId>com.ruoyi</groupId>
|
||||
<artifactId>ccdi-lsfx</artifactId>
|
||||
<version>3.9.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -100,4 +100,34 @@ spring:
|
||||
# 连接池的最大数据库连接数
|
||||
max-active: 8
|
||||
# #连接池最大阻塞等待时间(使用负值表示没有限制)
|
||||
max-wait: -1ms
|
||||
max-wait: -1ms
|
||||
|
||||
# 流水分析平台配置
|
||||
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: dXj6eHRmPv # 见知提供的密钥
|
||||
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
|
||||
get-bank-statement: /watson/api/project/getBSByLogId
|
||||
|
||||
# RestTemplate配置
|
||||
connection-timeout: 30000 # 连接超时30秒
|
||||
read-timeout: 60000 # 读取超时60秒
|
||||
|
||||
# 连接池配置
|
||||
pool:
|
||||
max-total: 100 # 最大连接数
|
||||
default-max-per-route: 20 # 每个路由最大连接数
|
||||
Reference in New Issue
Block a user