diff --git a/ccdi-lsfx/pom.xml b/ccdi-lsfx/pom.xml index cecec56..bf3f3ed 100644 --- a/ccdi-lsfx/pom.xml +++ b/ccdi-lsfx/pom.xml @@ -26,6 +26,12 @@ spring-boot-starter-web + + + org.apache.httpcomponents.client5 + httpclient5 + + org.projectlombok diff --git a/ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/LsfxAnalysisClient.java b/ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/LsfxAnalysisClient.java index 772d5ce..ba4627a 100644 --- a/ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/LsfxAnalysisClient.java +++ b/ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/LsfxAnalysisClient.java @@ -3,8 +3,10 @@ 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; @@ -15,6 +17,7 @@ import java.util.Map; /** * 流水分析平台客户端 */ +@Slf4j @Component public class LsfxAnalysisClient { @@ -52,67 +55,153 @@ public class LsfxAnalysisClient { * 获取Token */ public GetTokenResponse getToken(GetTokenRequest request) { - String secretCode = MD5Util.generateSecretCode( - request.getProjectNo(), - request.getEntityName(), - appSecret - ); - request.setAppSecretCode(secretCode); - request.setAppId(appId); + log.info("【流水分析】获取Token请求: projectNo={}, entityName={}", request.getProjectNo(), request.getEntityName()); + long startTime = System.currentTimeMillis(); - if (request.getAnalysisType() == null) { - request.setAnalysisType(LsfxConstants.ANALYSIS_TYPE); - } - if (request.getRole() == null) { - request.setRole(LsfxConstants.DEFAULT_ROLE); - } + try { + String secretCode = MD5Util.generateSecretCode( + request.getProjectNo(), + request.getEntityName(), + appSecret + ); + request.setAppSecretCode(secretCode); + request.setAppId(appId); - String url = baseUrl + getTokenEndpoint; - return httpUtil.postJson(url, request, null, GetTokenResponse.class); + 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) { - String url = baseUrl + uploadFileEndpoint; + log.info("【流水分析】上传文件请求: groupId={}, fileName={}", groupId, file.getFilename()); + long startTime = System.currentTimeMillis(); - Map params = new HashMap<>(); - params.put("groupId", groupId); - params.put("files", file); + try { + String url = baseUrl + uploadFileEndpoint; - Map headers = new HashMap<>(); - headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId); + Map params = new HashMap<>(); + params.put("groupId", groupId); + params.put("files", file); - return httpUtil.uploadFile(url, params, headers, UploadFileResponse.class); + Map 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) { - String url = baseUrl + fetchInnerFlowEndpoint; + log.info("【流水分析】拉取行内流水请求: groupId={}, customerNo={}", request.getGroupId(), request.getCustomerNo()); + long startTime = System.currentTimeMillis(); - Map headers = new HashMap<>(); - headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId); + try { + String url = baseUrl + fetchInnerFlowEndpoint; - return httpUtil.postJson(url, request, headers, FetchInnerFlowResponse.class); + Map 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) { - String url = baseUrl + checkParseStatusEndpoint; + log.info("【流水分析】检查文件解析状态: groupId={}, inprogressList={}", groupId, inprogressList); + long startTime = System.currentTimeMillis(); - Map params = new HashMap<>(); - params.put("groupId", groupId); - params.put("inprogressList", inprogressList); + try { + String url = baseUrl + checkParseStatusEndpoint; - Map headers = new HashMap<>(); - headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId); + Map params = new HashMap<>(); + params.put("groupId", groupId); + params.put("inprogressList", inprogressList); - return httpUtil.postJson(url, params, headers, CheckParseStatusResponse.class); + Map 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); + } } /** @@ -123,11 +212,35 @@ public class LsfxAnalysisClient { * @return 流水明细列表 */ public GetBankStatementResponse getBankStatement(GetBankStatementRequest request) { - String url = baseUrl + getBankStatementEndpoint; + log.info("【流水分析】获取银行流水请求: groupId={}, logId={}, pageNow={}, pageSize={}", + request.getGroupId(), request.getLogId(), request.getPageNow(), request.getPageSize()); + long startTime = System.currentTimeMillis(); - Map headers = new HashMap<>(); - headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId); + try { + String url = baseUrl + getBankStatementEndpoint; - return httpUtil.postJson(url, request, headers, GetBankStatementResponse.class); + Map 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); + } } } diff --git a/ccdi-lsfx/src/main/java/com/ruoyi/lsfx/config/RestTemplateConfig.java b/ccdi-lsfx/src/main/java/com/ruoyi/lsfx/config/RestTemplateConfig.java index cc3d921..d02f7ae 100644 --- a/ccdi-lsfx/src/main/java/com/ruoyi/lsfx/config/RestTemplateConfig.java +++ b/ccdi-lsfx/src/main/java/com/ruoyi/lsfx/config/RestTemplateConfig.java @@ -1,13 +1,17 @@ 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.SimpleClientHttpRequestFactory; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; /** - * RestTemplate配置 + * RestTemplate配置(使用连接池优化性能) */ @Configuration public class RestTemplateConfig { @@ -18,11 +22,29 @@ public class RestTemplateConfig { @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() { - SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + // 创建连接池管理器 + 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.setReadTimeout(readTimeout); + factory.setConnectionRequestTimeout(connectionTimeout); + return new RestTemplate(factory); } } diff --git a/ccdi-lsfx/src/main/java/com/ruoyi/lsfx/controller/LsfxTestController.java b/ccdi-lsfx/src/main/java/com/ruoyi/lsfx/controller/LsfxTestController.java index 0e67334..cc20ca7 100644 --- a/ccdi-lsfx/src/main/java/com/ruoyi/lsfx/controller/LsfxTestController.java +++ b/ccdi-lsfx/src/main/java/com/ruoyi/lsfx/controller/LsfxTestController.java @@ -1,6 +1,7 @@ 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.*; @@ -26,6 +27,26 @@ public class LsfxTestController { @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); } @@ -36,6 +57,17 @@ public class LsfxTestController { @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); @@ -44,6 +76,26 @@ public class LsfxTestController { @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); } @@ -54,6 +106,14 @@ public class LsfxTestController { @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); } diff --git a/ccdi-lsfx/src/main/java/com/ruoyi/lsfx/util/HttpUtil.java b/ccdi-lsfx/src/main/java/com/ruoyi/lsfx/util/HttpUtil.java index 101068a..56baeb9 100644 --- a/ccdi-lsfx/src/main/java/com/ruoyi/lsfx/util/HttpUtil.java +++ b/ccdi-lsfx/src/main/java/com/ruoyi/lsfx/util/HttpUtil.java @@ -1,9 +1,11 @@ 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; @@ -26,13 +28,27 @@ public class HttpUtil { * @return 响应对象 */ public T get(String url, Map headers, Class responseType) { - HttpHeaders httpHeaders = createHeaders(headers); - HttpEntity requestEntity = new HttpEntity<>(httpHeaders); + try { + HttpHeaders httpHeaders = createHeaders(headers); + HttpEntity requestEntity = new HttpEntity<>(httpHeaders); - ResponseEntity response = restTemplate.exchange( - url, HttpMethod.GET, requestEntity, responseType - ); - return response.getBody(); + ResponseEntity 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); + } } /** @@ -44,13 +60,27 @@ public class HttpUtil { * @return 响应对象 */ public T postJson(String url, Object request, Map headers, Class responseType) { - HttpHeaders httpHeaders = createHeaders(headers); - httpHeaders.setContentType(MediaType.APPLICATION_JSON); + try { + HttpHeaders httpHeaders = createHeaders(headers); + httpHeaders.setContentType(MediaType.APPLICATION_JSON); - HttpEntity requestEntity = new HttpEntity<>(request, httpHeaders); + HttpEntity requestEntity = new HttpEntity<>(request, httpHeaders); - ResponseEntity response = restTemplate.postForEntity(url, requestEntity, responseType); - return response.getBody(); + ResponseEntity 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); + } } /** @@ -62,18 +92,32 @@ public class HttpUtil { * @return 响应对象 */ public T uploadFile(String url, Map params, Map headers, Class responseType) { - HttpHeaders httpHeaders = createHeaders(headers); - httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA); + try { + HttpHeaders httpHeaders = createHeaders(headers); + httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA); - MultiValueMap body = new LinkedMultiValueMap<>(); - if (params != null) { - params.forEach(body::add); + MultiValueMap body = new LinkedMultiValueMap<>(); + if (params != null) { + params.forEach(body::add); + } + + HttpEntity> requestEntity = new HttpEntity<>(body, httpHeaders); + + ResponseEntity 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); } - - HttpEntity> requestEntity = new HttpEntity<>(body, httpHeaders); - - ResponseEntity response = restTemplate.postForEntity(url, requestEntity, responseType); - return response.getBody(); } /** diff --git a/doc/implementation/lsfx-code-review-20260302.md b/doc/implementation/lsfx-code-review-20260302.md new file mode 100644 index 0000000..f2733c2 --- /dev/null +++ b/doc/implementation/lsfx-code-review-20260302.md @@ -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 postJson(String url, Object request, Map headers, Class responseType) { + HttpHeaders httpHeaders = createHeaders(headers); + httpHeaders.setContentType(MediaType.APPLICATION_JSON); + HttpEntity requestEntity = new HttpEntity<>(request, httpHeaders); + ResponseEntity response = restTemplate.postForEntity(url, requestEntity, responseType); + return response.getBody(); // ❌ 可能为null,无异常处理 +} +``` + +**风险:** +1. 网络异常会直接抛给上层 +2. API返回错误码无法统一处理 +3. response.getBody()可能返回null导致NPE + +**建议改进:** +```java +public T postJson(String url, Object request, Map headers, Class responseType) { + try { + HttpHeaders httpHeaders = createHeaders(headers); + httpHeaders.setContentType(MediaType.APPLICATION_JSON); + HttpEntity requestEntity = new HttpEntity<>(request, httpHeaders); + + ResponseEntity 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 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 +**审查状态:** ✅ 完成 +**下一步:** 根据行动计划修复问题 diff --git a/doc/对接流水分析/~$-流水分析对接_new.docx b/doc/对接流水分析/~$-流水分析对接_new.docx new file mode 100644 index 0000000..12aae6d Binary files /dev/null and b/doc/对接流水分析/~$-流水分析对接_new.docx differ diff --git a/doc/对接流水分析/兰溪-流水分析对接-新版.md b/doc/对接流水分析/兰溪-流水分析对接-新版.md new file mode 100644 index 0000000..7e0c882 --- /dev/null +++ b/doc/对接流水分析/兰溪-流水分析对接-新版.md @@ -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 diff --git a/doc/对接流水分析/兰溪-流水分析对接_new.docx b/doc/对接流水分析/兰溪-流水分析对接_new.docx new file mode 100644 index 0000000..a129b8e Binary files /dev/null and b/doc/对接流水分析/兰溪-流水分析对接_new.docx differ diff --git a/docs/plans/2026-03-02-lsfx-mock-server-design.md b/docs/plans/2026-03-02-lsfx-mock-server-design.md new file mode 100644 index 0000000..c07000e --- /dev/null +++ b/docs/plans/2026-03-02-lsfx-mock-server-design.md @@ -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测试需求,提升开发和测试效率。 diff --git a/docs/plans/2026-03-02-lsfx-mock-server-implementation-plan.md b/docs/plans/2026-03-02-lsfx-mock-server-implementation-plan.md new file mode 100644 index 0000000..65fa83f --- /dev/null +++ b/docs/plans/2026-03-02-lsfx-mock-server-implementation-plan.md @@ -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 小时 +**建议开发模式**: 按顺序执行,每完成一个任务立即测试验证 diff --git a/docs/plans/2026-03-02-lsfx-update-plan.md b/docs/plans/2026-03-02-lsfx-update-plan.md new file mode 100644 index 0000000..0a1a8ed --- /dev/null +++ b/docs/plans/2026-03-02-lsfx-update-plan.md @@ -0,0 +1,1051 @@ +# 流水分析接口更新实施计划 + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**目标:** 按照新版接口文档完全重构流水分析模块,更新接口2、3、4、7,删除接口5、6 + +**架构:** 基于Spring Boot 3的REST API客户端模块,使用RestTemplate进行HTTP调用,Lombok简化DTO定义 + +**技术栈:** Spring Boot 3.5.8, Java 17, Lombok, RestTemplate, SpringDoc OpenAPI + +**前置条件:** +- 项目已存在 ccdi-lsfx 模块 +- 现有7个接口的DTO和Client已实现 +- 需要参考新版文档:`doc/对接流水分析/兰溪-流水分析对接-新版.md` + +--- + +## 任务概览 + +| 任务 | 说明 | 文件数 | +|------|------|--------| +| Task 1 | 更新配置文件 | 1 | +| Task 2 | 删除废弃DTO类 | 3 | +| Task 3 | 重构接口2(上传文件)Response | 1 | +| Task 4 | 重构接口3(拉取行内流水)Request和Response | 2 | +| Task 5 | 重构接口4(检查解析状态)Response | 1 | +| Task 6 | 重构接口7(获取流水)Request和Response | 2 | +| Task 7 | 更新Client客户端 | 1 | +| Task 8 | 更新TestController | 1 | +| Task 9 | 编译验证和测试 | - | + +--- + +## Task 1: 更新配置文件 + +**文件:** +- 修改: `ruoyi-admin/src/main/resources/application-dev.yml:105-130` + +**步骤 1: 删除接口5和接口6的配置项** + +定位到 `lsfx.api.endpoints` 部分,删除以下两行: + +```yaml + generate-report: /watson/api/project/confirmStageUploadLogs + check-report-status: /watson/api/project/upload/getallpendings +``` + +**步骤 2: 更新接口7的路径** + +修改 `get-bank-statement` 配置: + +```yaml + # 旧路径:get-bank-statement: /watson/api/project/upload/getBankStatement + get-bank-statement: /watson/api/project/getBSByLogId # 新路径 +``` + +**步骤 3: 验证配置** + +完整的endpoints配置应该是: + +```yaml + 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 +``` + +**步骤 4: 提交配置更新** + +```bash +git add ruoyi-admin/src/main/resources/application-dev.yml +git commit -m "config(lsfx): 删除接口5、6配置,更新接口7路径" +``` + +--- + +## Task 2: 删除废弃DTO类 + +**文件:** +- 删除: `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` + +**步骤 1: 删除GenerateReportRequest.java** + +```bash +rm ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/request/GenerateReportRequest.java +``` + +**步骤 2: 删除GenerateReportResponse.java** + +```bash +rm ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/GenerateReportResponse.java +``` + +**步骤 3: 删除CheckReportStatusResponse.java** + +```bash +rm ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/CheckReportStatusResponse.java +``` + +**步骤 4: 提交删除操作** + +```bash +git add -A ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/ +git commit -m "refactor(lsfx): 删除接口5(生成报告)和接口6(检查报告状态)的DTO类" +``` + +--- + +## Task 3: 重构接口2(上传文件)Response DTO + +**文件:** +- 重写: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/UploadFileResponse.java` + +**步骤 1: 完全重写UploadFileResponse.java** + +用以下完整代码替换整个文件: + +```java +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> accountsOfLog; + + /** 上传日志列表 */ + private List 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 accountNoList; + + /** 银行名称 */ + private String bankName; + + /** 数据类型信息 [格式, 分隔符] */ + private List dataTypeInfo; + + /** 下载文件名 */ + private String downloadFileName; + + /** 企业名称列表 */ + private List 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; + } +} +``` + +**步骤 2: 提交更改** + +```bash +git add ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/UploadFileResponse.java +git commit -m "refactor(lsfx): 重构接口2 Response,添加完整字段(accountsOfLog、uploadLogList)" +``` + +--- + +## Task 4: 重构接口3(拉取行内流水)Request和Response DTO + +**文件:** +- 重写: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/request/FetchInnerFlowRequest.java` +- 重写: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/FetchInnerFlowResponse.java` + +**步骤 1: 完全重写FetchInnerFlowRequest.java** + +用以下代码替换整个文件: + +```java +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; +} +``` + +**步骤 2: 完全重写FetchInnerFlowResponse.java** + +用以下代码替换整个文件: + +```java +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; + } +} +``` + +**步骤 3: 提交更改** + +```bash +git add ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/request/FetchInnerFlowRequest.java +git add ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/FetchInnerFlowResponse.java +git commit -m "refactor(lsfx): 重构接口3 Request/Response,修正参数名和字段结构" +``` + +--- + +## Task 5: 重构接口4(检查解析状态)Response DTO + +**文件:** +- 重写: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/CheckParseStatusResponse.java` + +**步骤 1: 完全重写CheckParseStatusResponse.java** + +用以下代码替换整个文件: + +```java +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 pendingList; + } + + @Data + public static class PendingItem { + /** 账号列表 */ + private List accountNoList; + + /** 银行名称 */ + private String bankName; + + /** 数据类型信息 */ + private List dataTypeInfo; + + /** 下载文件名 */ + private String downloadFileName; + + /** 企业名称列表 */ + private List 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 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; + } +} +``` + +**步骤 2: 提交更改** + +```bash +git add ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/CheckParseStatusResponse.java +git commit -m "refactor(lsfx): 重构接口4 Response,添加parsing字段和完整pendingList" +``` + +--- + +## Task 6: 重构接口7(获取流水)Request和Response DTO + +**文件:** +- 重写: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/request/GetBankStatementRequest.java` +- 重写: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/GetBankStatementResponse.java` + +**步骤 1: 完全重写GetBankStatementRequest.java** + +用以下代码替换整个文件: + +```java +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; +} +``` + +**步骤 2: 完全重写GetBankStatementResponse.java** + +用以下完整代码替换整个文件(包含40+字段的BankStatementItem): + +```java +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 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; + } +} +``` + +**步骤 3: 提交更改** + +```bash +git add ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/request/GetBankStatementRequest.java +git add ccdi-lsfx/src/main/java/com/ruoyi/lsfx/domain/response/GetBankStatementResponse.java +git commit -m "refactor(lsfx): 重构接口7 Request/Response,新路径、新参数、完整字段" +``` + +--- + +## Task 7: 更新LsfxAnalysisClient客户端类 + +**文件:** +- 修改: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/LsfxAnalysisClient.java` + +**步骤 1: 删除接口5和接口6的相关代码** + +删除以下内容: + +1. 删除字段注入(约第48-52行): +```java + @Value("${lsfx.api.endpoints.generate-report}") + private String generateReportEndpoint; + + @Value("${lsfx.api.endpoints.check-report-status}") + private String checkReportStatusEndpoint; +``` + +2. 删除 `generateReport()` 方法(约第127-134行) + +3. 删除 `checkReportStatus()` 方法(约第139-146行) + +**步骤 2: 更新import语句** + +确保import中已删除: +```java +// 确保这些import被删除 +import com.ruoyi.lsfx.domain.request.GenerateReportRequest; +import com.ruoyi.lsfx.domain.response.GenerateReportResponse; +import com.ruoyi.lsfx.domain.response.CheckReportStatusResponse; +``` + +**步骤 3: 更新方法注释** + +更新 `getBankStatement()` 方法的注释: + +```java +/** + * 获取银行流水(新版接口) + * 注意:需要传入logId参数,参数名已从pageNum改为pageNow + * + * @param request 请求参数(groupId, logId, pageNow, pageSize) + * @return 流水明细列表 + */ +public GetBankStatementResponse getBankStatement(GetBankStatementRequest request) { + String url = baseUrl + getBankStatementEndpoint; + + Map headers = new HashMap<>(); + headers.put(LsfxConstants.HEADER_CLIENT_ID, clientId); + + return httpUtil.postJson(url, request, headers, GetBankStatementResponse.class); +} +``` + +**步骤 4: 提交更改** + +```bash +git add ccdi-lsfx/src/main/java/com/ruoyi/lsfx/client/LsfxAnalysisClient.java +git commit -m "refactor(lsfx): Client删除接口5、6方法,更新接口7注释" +``` + +--- + +## Task 8: 更新LsfxTestController测试控制器 + +**文件:** +- 修改: `ccdi-lsfx/src/main/java/com/ruoyi/lsfx/controller/LsfxTestController.java` + +**步骤 1: 删除接口5和接口6的测试方法** + +删除以下两个方法: +1. `generateReport()` 方法(约第697-703行) +2. `checkReportStatus()` 方法(约第705-711行) + +**步骤 2: 删除相关import** + +确保删除: +```java +import com.ruoyi.lsfx.domain.request.GenerateReportRequest; +import com.ruoyi.lsfx.domain.response.GenerateReportResponse; +import com.ruoyi.lsfx.domain.response.CheckReportStatusResponse; +``` + +**步骤 3: 更新接口7的Swagger注释和参数验证** + +更新 `getBankStatement()` 方法: + +```java +@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); +} +``` + +**步骤 4: 提交更改** + +```bash +git add ccdi-lsfx/src/main/java/com/ruoyi/lsfx/controller/LsfxTestController.java +git commit -m "refactor(lsfx): Controller删除接口5、6测试接口,更新接口7参数验证" +``` + +--- + +## Task 9: 编译验证和测试 + +**步骤 1: 编译项目** + +```bash +mvn clean compile +``` + +**预期输出:** +``` +[INFO] BUILD SUCCESS +[INFO] Total time: XX.XXX s +``` + +**如果编译失败:** +- 检查是否有残留的import语句引用已删除的类 +- 检查DTO类中的字段类型是否正确 +- 查看编译错误信息并修复 + +**步骤 2: 启动应用** + +```bash +cd ruoyi-admin +mvn spring-boot:run +``` + +**预期输出:** +``` +Application started successfully +``` + +**步骤 3: 访问Swagger UI** + +浏览器访问:`http://localhost:8080/swagger-ui/index.html` + +**验证项:** +1. ✅ 确认接口5(生成报告)和接口6(检查报告状态)已消失 +2. ✅ 确认接口7的路径显示为 `/lsfx/test/getBankStatement` +3. ✅ 点击接口7,查看Schema,确认Request包含4个字段(groupId, logId, pageNow, pageSize) +4. ✅ 查看Response Schema,确认包含完整的BankStatementItem字段 + +**步骤 4: 测试接口1(获取Token)** + +使用Swagger或curl测试: + +```bash +curl -X POST http://localhost:8080/lsfx/test/getToken \ + -H "Content-Type: application/json" \ + -d '{ + "projectNo": "902000_'$(date +%s)'", + "entityName": "测试项目", + "userId": "902001", + "userName": "902001", + "orgCode": "902000", + "departmentCode": "902000" + }' +``` + +**预期响应:** +```json +{ + "code": 200, + "msg": "操作成功", + "data": { + "code": "200", + "data": { + "token": "eyJ0eXAi...", + "projectId": 123, + ... + } + } +} +``` + +**步骤 5: 查看git状态** + +```bash +git status +git log --oneline -5 +``` + +**预期看到5个提交:** +``` +xxxxxxx refactor(lsfx): Controller删除接口5、6测试接口,更新接口7参数验证 +xxxxxxx refactor(lsfx): Client删除接口5、6方法,更新接口7注释 +xxxxxxx refactor(lsfx): 重构接口7 Request/Response,新路径、新参数、完整字段 +xxxxxxx refactor(lsfx): 重构接口4 Response,添加parsing字段和完整pendingList +xxxxxxx refactor(lsfx): 重构接口3 Request/Response,修正参数名和字段结构 +... +``` + +**步骤 6: 创建总结报告** + +在 `doc/implementation/` 目录下创建实施报告: + +```bash +cat > doc/implementation/lsfx-update-report-$(date +%Y%m%d).md << 'EOF' +# 流水分析接口更新实施报告 + +## 实施日期 +$(date +%Y-%m-%d) + +## 更新内容 + +### 删除的接口 +- 接口5:生成尽调报告(/watson/api/project/confirmStageUploadLogs) +- 接口6:检查报告生成状态(/watson/api/project/upload/getallpendings) + +### 重构的接口 +- 接口2:上传文件Response - 添加完整字段(accountsOfLog、uploadLogList) +- 接口3:拉取行内流水 - 修正Request参数名,重构Response结构 +- 接口4:检查解析状态 - 添加parsing字段,完善pendingList结构 +- 接口7:获取流水 - 新路径、新参数(logId、pageNow)、完整40+字段 + +### 保留的接口 +- 接口1:获取Token - 无需修改 + +## 修改的文件统计 +- 配置文件:1个 +- 删除的DTO类:3个 +- 重构的DTO类:6个 +- 更新的Java类:2个 +- 总计:12个文件 + +## 测试结果 +- 编译状态:✅ 成功 +- 启动状态:✅ 成功 +- Swagger UI:✅ 接口正常显示 +- 接口1测试:✅ 返回正常 + +## 待办事项 +- [ ] 与前端联调测试新接口参数 +- [ ] 生产环境配置更新 +- [ ] 接口文档更新 +EOF +``` + +**步骤 7: 最终提交** + +```bash +git add doc/implementation/lsfx-update-report-*.md +git commit -m "docs(lsfx): 添加接口更新实施报告" +git log --oneline +``` + +--- + +## 验收标准 + +### 功能验收 +- ✅ 项目编译无错误 +- ✅ 应用启动成功 +- ✅ Swagger UI正常访问 +- ✅ 接口5、6已删除 +- ✅ 接口2、3、4、7的Response字段完整 +- ✅ 接口7使用新路径和新参数名 + +### 代码验收 +- ✅ 无残留的import语句 +- ✅ DTO类使用@Data注解 +- ✅ 字段类型正确(Integer、String、BigDecimal等) +- ✅ 方法注释完整清晰 + +### 文档验收 +- ✅ 配置文件注释清晰 +- ✅ 实施报告完整 +- ✅ 提交信息规范 + +--- + +## 故障排查指南 + +### 问题1:编译报错找不到类 +**原因:** 残留的import语句 +**解决:** 搜索并删除所有对GenerateReportRequest/Response和CheckReportStatusResponse的引用 + +### 问题2:启动报错配置项不存在 +**原因:** Client中仍然注入已删除的配置项 +**解决:** 检查LsfxAnalysisClient.java,删除generate-report和check-report-status的@Value注入 + +### 问题3:Swagger UI不显示接口 +**原因:** Controller方法签名错误 +**解决:** 检查LsfxTestController.java,确保所有方法都有@Operation注解 + +### 问题4:接口调用返回字段为null +**原因:** DTO字段名与API返回不匹配 +**解决:** 对比新文档响应示例,确保字段名完全一致(区分大小写) + +--- + +## 参考资料 + +- 新版接口文档:`doc/对接流水分析/兰溪-流水分析对接-新版.md` +- 设计文档:`docs/plans/2026-03-02-lsfx-integration-design.md` +- 若依框架规范:`CLAUDE.md` + +--- + +**计划完成日期:** 2026-03-02 +**预计实施时间:** 2-3小时 +**风险等级:** 中(涉及多个DTO重构) diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index 61a3624..07e5673 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -112,7 +112,7 @@ lsfx: # 认证配置 app-id: remote_app - app-secret: your_app_secret_here # 从见知获取 + app-secret: dXj6eHRmPv # 见知提供的密钥 client-id: c2017e8d105c435a96f86373635b6a09 # 测试环境固定值 # 接口路径配置 @@ -125,4 +125,9 @@ lsfx: # RestTemplate配置 connection-timeout: 30000 # 连接超时30秒 - read-timeout: 60000 # 读取超时60秒 \ No newline at end of file + read-timeout: 60000 # 读取超时60秒 + + # 连接池配置 + pool: + max-total: 100 # 最大连接数 + default-max-per-route: 20 # 每个路由最大连接数 \ No newline at end of file