完善lsfx mock服务上传状态接口与部署文档

This commit is contained in:
wkc
2026-03-13 16:38:07 +08:00
parent bda89202ba
commit 109b5220b2
29 changed files with 4489 additions and 67 deletions

View File

@@ -0,0 +1,221 @@
# 设计文档:修改拉取行内流水接口返回值
**日期:** 2026-03-04
**状态:** 已批准
**作者:** Claude Code
## 1. 概述和目标
### 目标
修改 `/watson/api/project/getJZFileOrZjrcuFile` 接口的返回格式,从当前的错误格式改为返回 logId 数组。
### 当前实现
```json
{
"code": "200",
"data": {"code": "501014", "message": "无行内流水文件"},
"status": "200",
"successResponse": true
}
```
### 修改后实现
**成功场景:**
```json
{
"code": "200",
"data": [19154],
"status": "200",
"successResponse": true
}
```
**错误场景(通过 `error_501014` 标记触发):**
```json
{
"code": "501014",
"message": "无行内流水文件",
"status": "501014",
"successResponse": false
}
```
### 关键特性
- logId 通过随机数生成范围10000-99999
- 独立简化管理,不存储到 `file_records`,不支持后续操作
- 保留错误模拟功能(通过 `error_XXXX` 标记触发)
## 2. 技术实现
### 修改文件
- `services/file_service.py` - 修改 `fetch_inner_flow()` 方法
### 具体实现
`FileService` 类中修改 `fetch_inner_flow()` 方法:
```python
def fetch_inner_flow(self, request: Union[Dict, object]) -> Dict:
"""拉取行内流水返回随机logId
Args:
request: 拉取流水请求(可以是字典或对象)
Returns:
流水响应字典包含随机生成的logId数组
"""
import random
# 随机生成一个logId范围10000-99999
log_id = random.randint(10000, 99999)
# 返回成功的响应包含logId数组
return {
"code": "200",
"data": [log_id],
"status": "200",
"successResponse": True,
}
```
### 关键变化
1. 移除原来的"无行内流水文件"硬编码错误响应
2. 使用 `random.randint(10000, 99999)` 生成随机 logId
3. 返回格式改为 `{"code": "200", "data": [log_id], ...}`
4. `import random` 放在方法内部,避免顶层导入(保持简单)
### 无需修改的部分
- `routers/api.py` - 错误检测逻辑保持不变
- `utils/error_simulator.py` - 错误码定义已包含 501014
- `config/settings.py` - 无需新增配置
## 3. 测试计划
### 测试文件
- `tests/test_api.py`
### 新增测试用例
#### 3.1 测试成功场景
```python
def test_fetch_inner_flow_success(client, sample_inner_flow_request):
"""测试拉取行内流水 - 成功场景"""
response = client.post(
"/watson/api/project/getJZFileOrZjrcuFile",
data=sample_inner_flow_request
)
assert response.status_code == 200
data = response.json()
assert data["code"] == "200"
assert data["successResponse"] == True
assert isinstance(data["data"], list)
assert len(data["data"]) == 1
assert isinstance(data["data"][0], int)
assert 10000 <= data["data"][0] <= 99999
```
#### 3.2 测试错误场景
```python
def test_fetch_inner_flow_error_501014(client):
"""测试拉取行内流水 - 错误场景 501014"""
request_data = {
"groupId": 1001,
"customerNo": "test_error_501014",
"dataChannelCode": "test_code",
"requestDateId": 20240101,
"dataStartDateId": 20240101,
"dataEndDateId": 20240131,
"uploadUserId": 902001,
}
response = client.post(
"/watson/api/project/getJZFileOrZjrcuFile",
data=request_data
)
assert response.status_code == 200
data = response.json()
assert data["code"] == "501014"
assert data["successResponse"] == False
```
### 测试命令
```bash
# 运行所有行内流水相关测试
pytest tests/test_api.py -k "fetch_inner_flow" -v
# 运行单个测试
pytest tests/test_api.py::test_fetch_inner_flow_success -v
pytest tests/test_api.py::test_fetch_inner_flow_error_501014 -v
```
## 4. 文档更新
### 4.1 README.md
更新接口说明部分,将"模拟无数据场景"改为"返回随机logId"。
### 4.2 CLAUDE.md
在架构设计部分补充说明行内流水接口的特殊性:
- 简化管理(不存储到 file_records
- 随机 logId无需持久化
- 无后续操作支持(无需解析状态检查)
## 5. 设计决策
### 为什么选择随机生成 logId
- **简化管理**:行内流水拉取是独立的简化流程,不需要与文件上传共用复杂的状态管理
- **无需持久化**logId 仅用于返回,不需要存储或后续查询
- **测试友好**:每次调用返回不同的值,避免固定值导致的测试假阳性
### 为什么不使用配置文件?
- 响应数据需要运行时动态生成(随机 logId
- 配置文件适合静态或模板化的响应,不适合需要随机值的场景
- 保持代码简单直接,避免过度设计
### 为什么保留错误模拟?
- Mock 服务器的核心功能之一是模拟各种场景
- 501014 错误是真实的业务场景(无行内流水文件)
- 通过 `error_XXXX` 标记触发错误,与项目整体设计一致
## 6. 影响范围
### 直接影响
- `services/file_service.py` - 修改 1 个方法
- `tests/test_api.py` - 新增/修改测试用例
### 间接影响
- API 文档自动更新FastAPI Swagger UI
- README.md 需要更新示例
### 无影响
- 其他 6 个接口的返回格式
- 错误模拟机制
- 前端集成(假设前端已按新格式设计)
## 7. 风险和限制
### 风险
- **logId 冲突**:理论上可能生成重复的 logId但由于不存储不会造成实际问题
- **前端兼容性**:如果前端已按旧格式实现,需要协调更新
### 限制
- 不支持后续的解析状态检查
- 不支持通过 logId 查询流水数据
- 不支持删除操作
这些限制是设计决策的一部分,符合"简化管理"的目标。
## 8. 验收标准
- [ ] 修改后接口返回正确的格式(包含 logId 数组)
- [ ] logId 在指定范围内10000-99999
- [ ] 错误模拟功能正常工作
- [ ] 所有测试用例通过
- [ ] 文档已更新
- [ ] 代码通过 pytest 测试
## 9. 时间线
预计实施时间30 分钟
- 代码修改10 分钟
- 测试编写和验证15 分钟
- 文档更新5 分钟

View File

@@ -0,0 +1,432 @@
# 修改拉取行内流水接口返回值 - 实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 修改拉取行内流水接口的返回格式,从错误格式改为返回随机 logId 数组
**Architecture:** 修改 `FileService.fetch_inner_flow()` 方法,使用随机数生成 logId10000-99999返回包含 logId 数组的成功响应,保留错误模拟功能
**Tech Stack:** Python 3.11, FastAPI, pytest
---
## Task 1: 添加测试夹具
**Files:**
- Modify: `tests/conftest.py:35-35` (在文件末尾添加)
**Step 1: 添加测试夹具**
`tests/conftest.py` 文件末尾添加:
```python
@pytest.fixture
def sample_inner_flow_request():
"""示例拉取行内流水请求"""
return {
"groupId": 1001,
"customerNo": "test_customer_001",
"dataChannelCode": "test_code",
"requestDateId": 20240101,
"dataStartDateId": 20240101,
"dataEndDateId": 20240131,
"uploadUserId": 902001,
}
```
**Step 2: 验证夹具定义正确**
运行: `python -c "from tests.conftest import sample_inner_flow_request; print('OK')"`
预期输出: `OK`
**Step 3: 提交**
```bash
git add tests/conftest.py
git commit -m "test: add sample_inner_flow_request fixture"
```
---
## Task 2: 编写成功场景的失败测试
**Files:**
- Modify: `tests/test_api.py` (在文件末尾添加)
**Step 1: 编写测试用例**
`tests/test_api.py` 文件末尾添加:
```python
def test_fetch_inner_flow_success(client, sample_inner_flow_request):
"""测试拉取行内流水 - 成功场景"""
response = client.post(
"/watson/api/project/getJZFileOrZjrcuFile",
data=sample_inner_flow_request
)
assert response.status_code == 200
data = response.json()
assert data["code"] == "200"
assert data["successResponse"] == True
assert isinstance(data["data"], list)
assert len(data["data"]) == 1
assert isinstance(data["data"][0], int)
assert 10000 <= data["data"][0] <= 99999
```
**Step 2: 运行测试验证失败**
运行: `pytest tests/test_api.py::test_fetch_inner_flow_success -v`
预期输出:
```
FAILED - assert data["successResponse"] == True
```
**Step 3: 暂不提交(等待实现)**
---
## Task 3: 实现 fetch_inner_flow 方法修改
**Files:**
- Modify: `services/file_service.py:135-150` (修改 `fetch_inner_flow` 方法)
**Step 1: 读取当前实现**
运行: `grep -n "def fetch_inner_flow" services/file_service.py`
预期输出: `135: def fetch_inner_flow(self, request: Union[Dict, object]) -> Dict:`
**Step 2: 修改方法实现**
`services/file_service.py` 中的 `fetch_inner_flow` 方法替换为:
```python
def fetch_inner_flow(self, request: Union[Dict, object]) -> Dict:
"""拉取行内流水返回随机logId
Args:
request: 拉取流水请求(可以是字典或对象)
Returns:
流水响应字典包含随机生成的logId数组
"""
import random
# 随机生成一个logId范围10000-99999
log_id = random.randint(10000, 99999)
# 返回成功的响应包含logId数组
return {
"code": "200",
"data": [log_id],
"status": "200",
"successResponse": True,
}
```
**Step 3: 运行测试验证通过**
运行: `pytest tests/test_api.py::test_fetch_inner_flow_success -v`
预期输出:
```
PASSED
```
**Step 4: 提交实现**
```bash
git add services/file_service.py tests/test_api.py
git commit -m "feat: modify fetch_inner_flow to return random logId array"
```
---
## Task 4: 编写错误场景测试
**Files:**
- Modify: `tests/test_api.py` (在 test_fetch_inner_flow_success 后添加)
**Step 1: 编写错误场景测试**
`tests/test_api.py``test_fetch_inner_flow_success` 后添加:
```python
def test_fetch_inner_flow_error_501014(client):
"""测试拉取行内流水 - 错误场景 501014"""
request_data = {
"groupId": 1001,
"customerNo": "test_error_501014",
"dataChannelCode": "test_code",
"requestDateId": 20240101,
"dataStartDateId": 20240101,
"dataEndDateId": 20240131,
"uploadUserId": 902001,
}
response = client.post(
"/watson/api/project/getJZFileOrZjrcuFile",
data=request_data
)
assert response.status_code == 200
data = response.json()
assert data["code"] == "501014"
assert data["successResponse"] == False
```
**Step 2: 运行错误场景测试**
运行: `pytest tests/test_api.py::test_fetch_inner_flow_error_501014 -v`
预期输出:
```
PASSED
```
**Step 3: 提交测试**
```bash
git add tests/test_api.py
git commit -m "test: add error scenario test for fetch_inner_flow"
```
---
## Task 5: 运行完整测试套件
**Files:**
- 无文件修改
**Step 1: 运行所有 fetch_inner_flow 相关测试**
运行: `pytest tests/test_api.py -k "fetch_inner_flow" -v`
预期输出:
```
test_fetch_inner_flow_success PASSED
test_fetch_inner_flow_error_501014 PASSED
```
**Step 2: 运行完整测试套件确保无破坏**
运行: `pytest tests/ -v`
预期输出:
```
所有测试 PASSED
```
**Step 3: 无需提交**
---
## Task 6: 更新 README.md 文档
**Files:**
- Modify: `README.md` (更新行内流水接口说明)
**Step 1: 找到接口说明位置**
运行: `grep -n "拉取行内流水" README.md`
预期输出: 找到行内流水接口的说明位置
**Step 2: 更新接口说明**
在 README.md 中找到行内流水接口的说明,将"模拟无数据场景"相关描述改为:
```markdown
### 3. 拉取行内流水
返回随机生成的 logId 数组范围10000-99999支持通过 `error_XXXX` 标记触发错误场景。
```
同时更新成功响应示例(如果有的话):
```json
{
"code": "200",
"data": [19154],
"status": "200",
"successResponse": true
}
```
**Step 3: 验证文档更新**
运行: `grep -A 5 "拉取行内流水" README.md`
预期输出: 显示更新后的说明
**Step 4: 提交文档更新**
```bash
git add README.md
git commit -m "docs: update fetch_inner_flow interface description"
```
---
## Task 7: 更新 CLAUDE.md 文档
**Files:**
- Modify: `CLAUDE.md` (补充行内流水接口说明)
**Step 1: 找到架构设计部分**
运行: `grep -n "### 服务类职责" CLAUDE.md`
预期输出: 找到服务类职责说明的位置
**Step 2: 更新服务类职责说明**
`CLAUDE.md` 的"服务类职责"部分,找到 `FileService` 的说明,补充:
```markdown
- **FileService**: 管理文件记录、解析状态、支持后台任务
- `fetch_inner_flow()`: 返回随机 logId 数组(简化管理,不存储记录)
```
**Step 3: 添加行内流水接口特殊性说明**
在合适的位置(如"注意事项"部分)添加:
```markdown
- **行内流水接口特殊性**:
- 简化管理:不存储到 file_records
- 随机 logId无需持久化仅用于返回
- 无后续操作:不支持解析状态检查、删除或查询流水
```
**Step 4: 提交文档更新**
```bash
git add CLAUDE.md
git commit -m "docs: update CLAUDE.md with inner flow interface details"
```
---
## Task 8: 验证 Swagger UI 文档
**Files:**
- 无文件修改
**Step 1: 启动服务器**
运行: `python main.py` (后台运行或新终端)
预期输出:
```
INFO: Uvicorn running on http://0.0.0.0:8000
```
**Step 2: 访问 Swagger UI**
打开浏览器访问: `http://localhost:8000/docs`
预期: 看到 `/watson/api/project/getJZFileOrZjrcuFile` 接口
**Step 3: 测试接口**
在 Swagger UI 中:
1. 点击 `/watson/api/project/getJZFileOrZjrcuFile` 接口
2. 点击 "Try it out"
3. 填写测试数据:
- groupId: 1001
- customerNo: test_customer
- dataChannelCode: test_code
- requestDateId: 20240101
- dataStartDateId: 20240101
- dataEndDateId: 20240131
- uploadUserId: 902001
4. 点击 "Execute"
5. 查看响应
预期响应:
```json
{
"code": "200",
"data": [12345],
"status": "200",
"successResponse": true
}
```
**Step 4: 停止服务器**
运行: `Ctrl+C` 或关闭终端
**Step 5: 无需提交**
---
## Task 9: 最终验收
**Files:**
- 无文件修改
**Step 1: 运行完整测试套件**
运行: `pytest tests/ -v --cov=. --cov-report=term`
预期输出:
```
所有测试 PASSED
覆盖率报告显示 file_service.py 覆盖率提升
```
**Step 2: 验证验收标准**
检查以下验收标准是否全部满足:
- [x] 修改后接口返回正确的格式(包含 logId 数组)
- [x] logId 在指定范围内10000-99999
- [x] 错误模拟功能正常工作
- [x] 所有测试用例通过
- [x] 文档已更新
- [x] 代码通过 pytest 测试
**Step 3: 查看提交历史**
运行: `git log --oneline -5`
预期输出:
```
docs: update CLAUDE.md with inner flow interface details
docs: update fetch_inner_flow interface description
test: add error scenario test for fetch_inner_flow
feat: modify fetch_inner_flow to return random logId array
test: add sample_inner_flow_request fixture
```
**Step 4: 完成**
实施完成!代码已通过所有测试,文档已更新。
---
## 总结
**修改文件:**
- `tests/conftest.py` - 添加测试夹具
- `tests/test_api.py` - 添加 2 个测试用例
- `services/file_service.py` - 修改 1 个方法
- `README.md` - 更新接口说明
- `CLAUDE.md` - 补充架构说明
**测试用例:**
- `test_fetch_inner_flow_success` - 验证成功场景
- `test_fetch_inner_flow_error_501014` - 验证错误场景
**提交记录:**
- 5 个清晰的提交,遵循原子提交原则
- 提交信息符合约定式提交规范
**实施时间:** 约 30 分钟

View File

@@ -0,0 +1,309 @@
# 流水分析 Mock 服务器接口完整对齐设计
**日期:** 2026-03-04
**目标:** 根据 `兰溪-流水分析对接3.md` 文档,完整对齐所有接口实现
## 概述
本次更新将 Mock 服务器完全对齐最新的接口文档,包括新增缺失接口、完善响应字段、统一错误处理。采用渐进式更新策略,保持现有功能不受影响。
## 设计目标
1. **新增缺失接口** - 实现文档中的第5个接口获取单个文件上传状态
2. **响应字段完整** - 所有7个接口的响应字段完全对齐文档示例
3. **数据模型增强** - 扩展文件记录模型以支持完整字段
4. **错误码完善** - 补充文档中提到的所有错误码
5. **无测试依赖** - 按用户要求,不涉及测试用例更新
## 架构设计
### 总体架构
保持现有无数据库架构不变,通过内存数据结构增强支持完整字段存储。
```
┌─────────────────────────────────────────┐
│ FastAPI 应用 │
├─────────────────────────────────────────┤
│ routers/api.py │
│ ├─ 7个接口路由新增接口5
│ └─ 错误标记检测 │
├─────────────────────────────────────────┤
│ services/ │
│ ├─ token_service.py │
│ ├─ file_service.py增强
│ │ ├─ FileRecord扩展字段
│ │ ├─ upload_file()(初始化完整字段) │
│ │ ├─ get_upload_status()(新增) │
│ │ └─ delete_files() │
│ └─ statement_service.py │
├─────────────────────────────────────────┤
│ config/responses/ │
│ ├─ token.json更新
│ ├─ upload.json更新
│ ├─ parse_status.json更新
│ ├─ bank_statement.json更新
│ └─ upload_status.json新建
├─────────────────────────────────────────┤
│ utils/ │
│ └─ error_simulator.py补充错误码
└─────────────────────────────────────────┘
```
## 核心设计
### 1. 数据模型扩展
#### FileRecord 扩展字段
`services/file_service.py` 中扩展 `FileRecord` 类:
**现有字段:**
- `log_id`, `group_id`, `file_name`, `status`, `upload_status_desc`, `parsing`
**新增字段(对齐文档):**
- `account_no_list: List[str]` - 账号列表
- `enterprise_name_list: List[str]` - 主体名称列表
- `bank_name: str` - 银行名称(如 "ZJRCU", "ALIPAY", "BSX"
- `real_bank_name: str` - 真实银行名称
- `template_name: str` - 模板名称(如 "ZJRCU_T251114"
- `data_type_info: List[str]` - 数据类型(如 ["CSV", ","]
- `file_size: int` - 文件大小(字节)
- `download_file_name: str` - 下载文件名
- `file_package_id: str` - 文件包IDUUID格式
- `file_upload_by: int` - 上传用户ID
- `file_upload_by_user_name: str` - 上传用户名
- `file_upload_time: str` - 上传时间(如 "2026-02-27 09:50:18"
- `le_id: int` - 法律实体ID
- `login_le_id: int` - 登录法律实体ID
- `log_type: str` - 日志类型(如 "bankstatement"
- `log_meta: str` - 日志元数据JSON字符串
- `lost_header: List[str]` - 丢失的头部信息
- `rows: int` - 行数
- `source: str` - 来源(如 "http"
- `total_records: int` - 总记录数
- `trx_date_start_id: int` - 交易开始日期ID如 20240201
- `trx_date_end_id: int` - 交易结束日期ID如 20240228
- `is_split: int` - 是否分割0或1
#### 字段初始化策略
- `bank_name`: 根据文件名推断(包含"支付宝"→"ALIPAY",默认"ZJRCU"
- `template_name`: 根据 bank_name 生成(如 "ZJRCU_T251114"
- `file_package_id`: 生成随机UUID
- `file_upload_time`: 使用当前服务器时间
- `total_records`: 随机生成100-300
- `trx_date_start_id`/`trx_date_end_id`: 生成合理的日期范围
- 其他字段: 使用文档示例中的典型值
### 2. 新增接口实现
#### 接口5GET `/watson/api/project/bs/upload`
**功能:** 获取单个文件上传后的状态
**请求参数:**
- `groupId` (int, 必填) - 项目ID
- `logId` (int, 可选) - 文件ID
**响应结构:**
```json
{
"code": "200",
"data": {
"logs": [
{
"accountNoList": ["18785967364"],
"bankName": "ALIPAY",
"dataTypeInfo": ["CSV", ","],
"downloadFileName": "支付宝.csv",
"enterpriseNameList": ["曾孝成"],
"fileSize": 16322,
"fileUploadBy": 448,
"fileUploadByUserName": "admin@support.com",
"fileUploadTime": "2025-03-13 08:45:32",
"isSplit": 0,
"leId": 10741,
"logId": 13994,
"logMeta": "{\"lostHeader\":[],\"balanceAmount\":\"-1\"}",
"logType": "bankstatement",
"loginLeId": 10741,
"lostHeader": [],
"realBankName": "ALIPAY",
"rows": 0,
"source": "http",
"status": -5,
"templateName": "ALIPAY_T220708",
"totalRecords": 127,
"trxDateEndId": 20231231,
"trxDateStartId": 20230102,
"uploadFileName": "支付宝.pdf",
"uploadStatusDesc": "data.wait.confirm.newaccount"
}
],
"status": "",
"accountId": 8954,
"currency": "CNY"
},
"status": "200",
"successResponse": true
}
```
**实现逻辑:**
1. 路由:在 `routers/api.py` 添加 GET 路由
2. 服务:在 `file_service.py` 添加 `get_upload_status(groupId, logId)` 方法
3. 逻辑:
- 如果提供 `logId`,返回该特定文件的状态
- 如果不提供 `logId`,返回该项目的所有文件状态
-`file_records` 中查询并构建响应
**特殊处理:**
- `accountId``currency`: 从文件记录中提取或使用默认值8954, "CNY"
- 空主体标识:如果 `enterpriseNameList` 仅包含空字符串,表示流水文件未生成主体
### 3. 现有接口响应字段更新
#### 接口1`/account/common/getToken`
- 确认 `data.analysisType` 类型为 Integer
- 保持其他字段不变
#### 接口2`/watson/api/project/remoteUploadSplitFile`
- 补充 `accountsOfLog` 结构
- 完善 `uploadLogList` 中的所有字段
- 新增 `uploadStatus` 字段(固定值 1
#### 接口3`/watson/api/project/getJZFileOrZjrcuFile`
- 保持现有响应格式
- 返回 `{code, data: [logId数组], status, successResponse}`
#### 接口4`/watson/api/project/upload/getpendings`
- 补充 `data.pendingList` 中的所有字段
- 确保包含 `isSplit`, `lostHeader`, `leId`, `loginLeId`
#### 接口6`/watson/api/project/batchDeleteUploadFile`
- 注意 `code` 字段为 "200 OK" 而非 "200"
- 响应格式:`{code: "200 OK", data: {message: "delete.files.success"}, ...}`
#### 接口7`/watson/api/project/getBSByLogId`
- 补充 `bankStatementList` 中每个对象的所有50+个字段
- 字段包括accountId, accountMaskNo, accountingDate, balanceAmount, bank, bankStatementId, bankTrxNumber, batchId, cashType, crAmount, cretNo, currency, customerAccountMaskNo, customerBank, customerId, customerName, drAmount, groupId, leId, leName, transAmount, transFlag, trxDate, userMemo 等
### 4. 错误码完善
#### 当前错误码(已有)
- 40101: appId错误
- 40102: appSecretCode错误
- 40104: 可使用项目次数为0无法创建项目
- 40105: 只读模式下无法新建项目
- 40106: 错误的分析类型,不在规定的取值范围内
- 40107: 当前系统不支持的分析类型
- 40108: 当前用户所属行社无权限
- 501014: 无行内流水文件
#### 新增错误码
- 40100: 未知异常
#### 错误响应格式
```json
{
"code": "错误码",
"message": "错误描述",
"status": "错误码",
"successResponse": false
}
```
#### 错误触发机制
- 在任意字符串参数中包含 `error_XXXX` 标记
- 例如:`projectNo: "test_error_40100"` 触发 40100 错误
### 5. 请求头处理
#### X-Xencio-Client-Id
- **策略:** 不验证,接受任意值
- **原因:** 简化测试,不需要记住特定的 client-id
- **实现:** FastAPI 不检查该请求头
## 实施计划
### 步骤1数据模型扩展
- **文件:** `services/file_service.py`
- **内容:** 扩展 `FileRecord` 类,添加所有新字段
- **验证:** 启动服务无报错
### 步骤2文件服务增强
- **文件:** `services/file_service.py`
- **内容:**
-`upload_file()` 方法中初始化所有新字段
- 添加 `get_upload_status()` 方法
- 更新 `delete_files()` 方法以处理新增字段
- **验证:** 上传文件后能返回完整字段
### 步骤3新增接口路由
- **文件:** `routers/api.py`
- **内容:** 添加 GET `/watson/api/project/bs/upload` 路由
- **验证:** 访问 `/docs` 能看到新接口
### 步骤4响应模板更新
- **文件:**
- `config/responses/token.json`
- `config/responses/upload.json`
- `config/responses/parse_status.json`
- `config/responses/bank_statement.json`
- 新建 `config/responses/upload_status.json`
- **内容:** 补充所有缺失字段,对齐文档示例
- **验证:** 调用接口返回完整字段
### 步骤5错误码补充
- **文件:** `utils/error_simulator.py`
- **内容:** 添加 40100 错误码
- **验证:** 使用 `error_40100` 能触发对应错误
### 步骤6文档更新
- **文件:**
- `CLAUDE.md`
- `README.md`(如存在)
- **内容:** 添加新接口说明,更新注意事项
## 文件变更清单
```
services/file_service.py [修改] - 数据模型和服务方法
routers/api.py [修改] - 新增接口路由
utils/error_simulator.py [修改] - 新增错误码
config/responses/token.json [修改] - 完善响应字段
config/responses/upload.json [修改] - 完善响应字段
config/responses/parse_status.json [修改] - 完善响应字段
config/responses/bank_statement.json [修改] - 完善响应字段
config/responses/upload_status.json [新建] - 接口5响应模板
CLAUDE.md [修改] - 更新接口说明
README.md [修改] - 更新项目说明(如存在)
```
## 风险评估
### 低风险
- 数据模型扩展:仅添加字段,不影响现有功能
- 响应模板更新:仅添加字段,向后兼容
- 错误码补充:新增错误码,不影响现有错误处理
### 需注意
- 文件上传逻辑:需要确保所有新字段都正确初始化
- 时间格式:确保 `file_upload_time` 使用正确的格式
- 字段类型:确保 Integer 字段不使用字符串
## 成功标准
1. 所有7个接口都能正常调用
2. 每个接口的响应字段完全对齐文档示例
3. 错误标记机制在所有接口中都能正常工作
4. 新增的 40100 错误码能正确触发
5. 服务启动无报错,能正常处理请求
## 后续工作
本次更新完成后Mock 服务器将完全对齐接口文档,可以支持前端开发和集成测试。后续可根据实际使用情况:
- 调整字段生成逻辑(如更真实的数据)
- 添加更多银行的模板支持
- 优化错误场景的模拟

View File

@@ -0,0 +1,717 @@
# 接口完整对齐实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 根据 `兰溪-流水分析对接3.md` 文档,完整对齐 Mock 服务器的所有7个接口实现
**Architecture:** 保持无数据库架构通过扩展内存数据模型支持完整字段新增1个接口完善6个现有接口的响应字段
**Tech Stack:** FastAPI, Python 3.8+, Pydantic
---
## Task 1: 扩展 FileRecord 数据模型
**Files:**
- Modify: `services/file_service.py`
**Step 1: 读取现有 file_service.py 文件**
先查看当前的 FileRecord 实现。
**Step 2: 扩展 FileRecord 类添加所有新字段**
`FileRecord` 类中添加以下字段:
```python
from dataclasses import dataclass, field
from typing import List
import uuid
from datetime import datetime
@dataclass
class FileRecord:
"""文件记录模型(扩展版)"""
# 原有字段
log_id: int
group_id: int
file_name: str
status: int = -5 # -5 表示解析成功
upload_status_desc: str = "data.wait.confirm.newaccount"
parsing: bool = True # True表示正在解析
# 新增字段 - 账号和主体信息
account_no_list: List[str] = field(default_factory=list)
enterprise_name_list: List[str] = field(default_factory=list)
# 新增字段 - 银行和模板信息
bank_name: str = "ZJRCU"
real_bank_name: str = "ZJRCU"
template_name: str = "ZJRCU_T251114"
data_type_info: List[str] = field(default_factory=lambda: ["CSV", ","])
# 新增字段 - 文件元数据
file_size: int = 50000
download_file_name: str = ""
file_package_id: str = field(default_factory=lambda: str(uuid.uuid4()).replace('-', ''))
# 新增字段 - 上传用户信息
file_upload_by: int = 448
file_upload_by_user_name: str = "admin@support.com"
file_upload_time: str = field(default_factory=lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
# 新增字段 - 法律实体信息
le_id: int = 10000
login_le_id: int = 10000
log_type: str = "bankstatement"
log_meta: str = "{\"lostHeader\":[],\"balanceAmount\":true}"
lost_header: List[str] = field(default_factory=list)
# 新增字段 - 记录统计
rows: int = 0
source: str = "http"
total_records: int = 150
is_split: int = 0
# 新增字段 - 交易日期范围
trx_date_start_id: int = 20240101
trx_date_end_id: int = 20241231
```
**Step 3: 验证服务能正常启动**
```bash
python main.py
```
预期:服务启动成功,无报错信息。
---
## Task 2: 更新 upload_file 方法初始化所有字段
**Files:**
- Modify: `services/file_service.py`
**Step 1: 读取 upload_file 方法**
查看当前的 `upload_file` 方法实现。
**Step 2: 根据文件名推断银行名称**
`upload_file` 方法中添加银行名称推断逻辑:
```python
def _infer_bank_name(self, filename: str) -> tuple:
"""根据文件名推断银行名称和模板名称"""
if "支付宝" in filename or "alipay" in filename.lower():
return "ALIPAY", "ALIPAY_T220708"
elif "绍兴银行" in filename or "BSX" in filename:
return "BSX", "BSX_T240925"
else:
return "ZJRCU", "ZJRCU_T251114"
async def upload_file(self, group_id: int, file: UploadFile, background_tasks: BackgroundTasks) -> dict:
"""上传文件并初始化所有字段"""
# 生成新的 log_id
self.current_log_id += 1
log_id = self.current_log_id
# 推断银行信息
bank_name, template_name = self._infer_bank_name(file.filename)
# 生成合理的交易日期范围
import random
from datetime import datetime, timedelta
end_date = datetime.now()
start_date = end_date - timedelta(days=random.randint(90, 365))
trx_date_start_id = int(start_date.strftime("%Y%m%d"))
trx_date_end_id = int(end_date.strftime("%Y%m%d"))
# 生成随机账号和主体
account_no = f"{random.randint(10000000000, 99999999999)}"
enterprise_names = ["测试主体"] if random.random() > 0.3 else [""]
# 创建完整的文件记录
file_record = FileRecord(
log_id=log_id,
group_id=group_id,
file_name=file.filename,
download_file_name=file.filename,
bank_name=bank_name,
real_bank_name=bank_name,
template_name=template_name,
account_no_list=[account_no],
enterprise_name_list=enterprise_names,
le_id=10000 + random.randint(0, 9999),
login_le_id=10000 + random.randint(0, 9999),
file_size=random.randint(10000, 100000),
total_records=random.randint(100, 300),
trx_date_start_id=trx_date_start_id,
trx_date_end_id=trx_date_end_id,
parsing=True,
status=-5
)
# 存储记录
self.file_records[log_id] = file_record
# 添加后台任务(延迟解析)
background_tasks.add_task(self._delayed_parse, log_id)
# 构建响应
return self._build_upload_response(file_record)
```
**Step 3: 实现 _build_upload_response 方法**
```python
def _build_upload_response(self, file_record: FileRecord) -> dict:
"""构建上传接口的完整响应"""
return {
"code": "200",
"data": {
"accountsOfLog": {
str(file_record.log_id): [
{
"bank": file_record.bank_name,
"accountName": file_record.enterprise_name_list[0] if file_record.enterprise_name_list else "",
"accountNo": file_record.account_no_list[0] if file_record.account_no_list else "",
"currency": "CNY"
}
]
},
"uploadLogList": [
{
"accountNoList": file_record.account_no_list,
"bankName": file_record.bank_name,
"dataTypeInfo": file_record.data_type_info,
"downloadFileName": file_record.download_file_name,
"enterpriseNameList": file_record.enterprise_name_list,
"filePackageId": file_record.file_package_id,
"fileSize": file_record.file_size,
"fileUploadBy": file_record.file_upload_by,
"fileUploadByUserName": file_record.file_upload_by_user_name,
"fileUploadTime": file_record.file_upload_time,
"leId": file_record.le_id,
"logId": file_record.log_id,
"logMeta": file_record.log_meta,
"logType": file_record.log_type,
"loginLeId": file_record.login_le_id,
"lostHeader": file_record.lost_header,
"realBankName": file_record.real_bank_name,
"rows": file_record.rows,
"source": file_record.source,
"status": file_record.status,
"templateName": file_record.template_name,
"totalRecords": file_record.total_records,
"trxDateEndId": file_record.trx_date_end_id,
"trxDateStartId": file_record.trx_date_start_id,
"uploadFileName": file_record.file_name,
"uploadStatusDesc": file_record.upload_status_desc
}
],
"uploadStatus": 1
},
"status": "200",
"successResponse": True
}
```
**Step 4: 验证上传接口返回完整字段**
重启服务并调用上传接口,检查响应是否包含所有字段。
---
## Task 3: 添加 get_upload_status 方法
**Files:**
- Modify: `services/file_service.py`
**Step 1: 实现 get_upload_status 方法**
`FileService` 类中添加新方法:
```python
def get_upload_status(self, group_id: int, log_id: int = None) -> dict:
"""获取文件上传状态接口5"""
logs = []
if log_id:
# 返回特定文件的状态
if log_id in self.file_records:
record = self.file_records[log_id]
if record.group_id == group_id:
logs.append(self._build_log_detail(record))
else:
# 返回该项目的所有文件状态
for record in self.file_records.values():
if record.group_id == group_id:
logs.append(self._build_log_detail(record))
# 构建响应
return {
"code": "200",
"data": {
"logs": logs,
"status": "",
"accountId": 8954,
"currency": "CNY"
},
"status": "200",
"successResponse": True
}
def _build_log_detail(self, record: FileRecord) -> dict:
"""构建日志详情对象"""
return {
"accountNoList": record.account_no_list,
"bankName": record.bank_name,
"dataTypeInfo": record.data_type_info,
"downloadFileName": record.download_file_name,
"enterpriseNameList": record.enterprise_name_list,
"fileSize": record.file_size,
"fileUploadBy": record.file_upload_by,
"fileUploadByUserName": record.file_upload_by_user_name,
"fileUploadTime": record.file_upload_time,
"isSplit": record.is_split,
"leId": record.le_id,
"logId": record.log_id,
"logMeta": record.log_meta,
"logType": record.log_type,
"loginLeId": record.login_le_id,
"lostHeader": record.lost_header,
"realBankName": record.real_bank_name,
"rows": record.rows,
"source": record.source,
"status": record.status,
"templateName": record.template_name,
"totalRecords": record.total_records,
"trxDateEndId": record.trx_date_end_id,
"trxDateStartId": record.trx_date_start_id,
"uploadFileName": record.file_name,
"uploadStatusDesc": record.upload_status_desc
}
```
**Step 2: 验证方法能正确查询文件记录**
在代码中确保 `file_records` 字典正确初始化和管理。
---
## Task 4: 在 API 路由中添加新接口
**Files:**
- Modify: `routers/api.py`
**Step 1: 读取现有 api.py 文件**
查看当前的路由定义。
**Step 2: 添加 GET 接口路由**
在接口5的位置check_parse_status 和 delete_files 之间)添加:
```python
# ==================== 接口5获取文件上传状态 ====================
@router.get("/watson/api/project/bs/upload")
async def get_upload_status(
groupId: int = Form(..., description="项目id"),
logId: Optional[int] = Form(None, description="文件id"),
):
"""获取单个文件上传后的状态
如果不提供 logId返回该项目的所有文件状态
"""
return file_service.get_upload_status(groupId, logId)
```
**Step 3: 确认导入了 Optional**
在文件顶部确认:
```python
from typing import List, Optional
```
**Step 4: 验证新接口出现在 Swagger 文档中**
重启服务,访问 http://localhost:8000/docs确认能看到新的 GET 接口。
---
## Task 5: 更新 check_parse_status 响应字段
**Files:**
- Modify: `services/file_service.py`
**Step 1: 修改 check_parse_status 方法**
确保返回的 pendingList 包含所有字段:
```python
def check_parse_status(self, group_id: int, inprogress_list: str) -> dict:
"""检查文件解析状态"""
log_ids = [int(id.strip()) for id in inprogress_list.split(",")]
pending_list = []
all_parsing_complete = True
for log_id in log_ids:
if log_id in self.file_records:
record = self.file_records[log_id]
if record.parsing:
all_parsing_complete = False
pending_list.append(self._build_log_detail(record))
return {
"code": "200",
"data": {
"parsing": not all_parsing_complete,
"pendingList": pending_list
},
"status": "200",
"successResponse": True
}
```
**Step 2: 验证解析状态接口返回完整字段**
调用接口4检查响应中的 pendingList 是否包含所有字段。
---
## Task 6: 更新 delete_files 方法响应格式
**Files:**
- Modify: `services/file_service.py`
**Step 1: 修改 delete_files 方法**
确保响应的 code 字段为 "200 OK"
```python
def delete_files(self, group_id: int, log_ids: List[int], user_id: int) -> dict:
"""批量删除文件"""
deleted_count = 0
for log_id in log_ids:
if log_id in self.file_records:
del self.file_records[log_id]
deleted_count += 1
return {
"code": "200 OK", # 注意:这里是 "200 OK" 不是 "200"
"data": {
"message": "delete.files.success"
},
"message": "delete.files.success",
"status": "200",
"successResponse": True
}
```
**Step 2: 验证删除接口响应格式正确**
调用删除接口,检查响应的 code 字段是否为 "200 OK"。
---
## Task 7: 更新 token.json 响应模板
**Files:**
- Modify: `config/responses/token.json`
**Step 1: 确认 analysisType 为 Integer**
确保 token.json 中的 analysisType 字段类型正确:
```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
}
}
```
确认 `analysisType` 的值是数字 0不是字符串 "0"。
**Step 2: 验证接口1响应正确**
调用 getToken 接口,检查 analysisType 的类型。
---
## Task 8: 创建 upload_status.json 响应模板
**Files:**
- Create: `config/responses/upload_status.json`
**Step 1: 创建新的响应模板文件**
创建文件并添加内容(虽然实际响应在代码中构建,但保留模板作为参考):
```json
{
"success_response": {
"code": "200",
"data": {
"logs": [
{
"accountNoList": ["18785967364"],
"bankName": "ALIPAY",
"dataTypeInfo": ["CSV", ","],
"downloadFileName": "支付宝.csv",
"enterpriseNameList": ["曾孝成"],
"fileSize": 16322,
"fileUploadBy": 448,
"fileUploadByUserName": "admin@support.com",
"fileUploadTime": "2025-03-13 08:45:32",
"isSplit": 0,
"leId": 10741,
"logId": 13994,
"logMeta": "{\"lostHeader\":[],\"balanceAmount\":\"-1\"}",
"logType": "bankstatement",
"loginLeId": 10741,
"lostHeader": [],
"realBankName": "ALIPAY",
"rows": 0,
"source": "http",
"status": -5,
"templateName": "ALIPAY_T220708",
"totalRecords": 127,
"trxDateEndId": 20231231,
"trxDateStartId": 20230102,
"uploadFileName": "支付宝.pdf",
"uploadStatusDesc": "data.wait.confirm.newaccount"
}
],
"status": "",
"accountId": 8954,
"currency": "CNY"
},
"status": "200",
"successResponse": true
}
}
```
---
## Task 9: 更新 bank_statement.json 响应模板
**Files:**
- Modify: `config/responses/bank_statement.json`
**Step 1: 补充流水记录的所有字段**
确保 bank_statement.json 包含所有50+个字段:
```json
{
"success_response": {
"code": "200",
"data": {
"bankStatementList": [
{
"accountId": 0,
"accountMaskNo": "101015251071645",
"accountingDate": "2024-02-01",
"accountingDateId": 20240201,
"archivingFlag": 0,
"attachments": 0,
"balanceAmount": 4814.82,
"bank": "ZJRCU",
"bankComments": "",
"bankStatementId": 12847662,
"bankTrxNumber": "1a10458dd5c3366d7272285812d434fc",
"batchId": 19135,
"cashType": "1",
"commentsNum": 0,
"crAmount": 0,
"cretNo": "230902199012261247",
"currency": "CNY",
"customerAccountMaskNo": "597671502",
"customerBank": "",
"customerId": -1,
"customerName": "小店",
"customerReference": "",
"downPaymentFlag": 0,
"drAmount": 245.8,
"exceptionType": "",
"groupId": 16238,
"internalFlag": 0,
"leId": 16308,
"leName": "张传伟",
"overrideBsId": 0,
"paymentMethod": "",
"sourceCatalogId": 0,
"split": 0,
"subBankstatementId": 0,
"toDoFlag": 0,
"transAmount": 245.8,
"transFlag": "P",
"transTypeId": 0,
"transformAmount": 0,
"transformCrAmount": 0,
"transformDrAmount": 0,
"transfromBalanceAmount": 0,
"trxBalance": 0,
"trxDate": "2024-02-01 10:33:44",
"userMemo": "财付通消费_小店"
}
],
"totalCount": 131
},
"status": "200",
"successResponse": true
}
}
```
**Step 2: 验证流水查询接口返回所有字段**
调用 getBSByLogId 接口,检查响应是否包含所有字段。
---
## Task 10: 添加 40100 错误码
**Files:**
- Modify: `utils/error_simulator.py`
**Step 1: 在 ERROR_CODES 字典中添加新错误码**
```python
ERROR_CODES = {
"40100": {"code": "40100", "message": "未知异常"},
"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": "无行内流水文件"},
}
```
**Step 2: 验证错误码能正确触发**
调用任意接口,在参数中包含 `error_40100`,检查是否返回对应错误。
---
## Task 11: 更新 CLAUDE.md 文档
**Files:**
- Modify: `CLAUDE.md`
**Step 1: 更新接口列表说明**
在 "API 接口说明" 部分更新为:
```markdown
## API 接口说明
7个核心接口
1. `/account/common/getToken` (POST) - 创建项目并获取 Token
2. `/watson/api/project/remoteUploadSplitFile` (POST) - 上传流水文件multipart/form-data
3. `/watson/api/project/getJZFileOrZjrcuFile` (POST) - 拉取行内流水
4. `/watson/api/project/upload/getpendings` (POST) - 检查文件解析状态
5. `/watson/api/project/bs/upload` (GET) - 获取单个文件上传后的状态
6. `/watson/api/project/batchDeleteUploadFile` (POST) - 批量删除文件
7. `/watson/api/project/getBSByLogId` (POST) - 获取银行流水(分页)
详细接口文档请访问 Swagger UI (`/docs`) 或查看 `assets/兰溪-流水分析对接3.md`
```
**Step 2: 更新注意事项**
添加关于响应字段完整性的说明:
```markdown
## 注意事项
- **数据持久化**: 所有数据存储在内存中,服务重启后数据丢失
- **响应字段完整性**: 所有接口响应字段完全对齐接口文档示例
- **并发安全**: 当前实现未考虑多线程安全,生产环境需要加锁
- **文件存储**: 上传的文件不实际保存,仅模拟元数据
- **错误标记**: 错误触发通过字符串匹配实现,确保测试数据唯一性
- **后台任务**: FastAPI BackgroundTasks 在同一进程内执行,不会阻塞响应
- **请求头处理**: X-Xencio-Client-Id 请求头不验证,接受任意值
```
---
## Task 12: 最终验证
**Files:**
- All modified files
**Step 1: 启动服务**
```bash
python main.py
```
预期:服务正常启动,无报错。
**Step 2: 访问 Swagger 文档**
访问 http://localhost:8000/docs
预期能看到所有7个接口包括新增的 GET 接口。
**Step 3: 测试所有7个接口**
使用 Swagger UI 或 curl 测试每个接口,确保:
1. 接口1返回包含 analysisType (Integer) 的响应
2. 接口2返回包含 accountsOfLog 和完整 uploadLogList 的响应
3. 接口3返回 logId 数组
4. 接口4返回包含完整字段的 pendingList
5. 接口5返回包含完整字段的 logs 数组
6. 接口6返回 code 为 "200 OK" 的响应
7. 接口7返回包含所有50+字段的 bankStatementList
**Step 4: 测试错误码**
调用接口1使用参数 `projectNo: "test_error_40100"`
预期:返回 40100 错误。
---
## Success Criteria
- [x] FileRecord 包含所有必需字段
- [x] upload_file 方法正确初始化所有字段
- [x] get_upload_status 方法正确实现
- [x] 新接口出现在 /docs 中
- [x] 所有响应字段完全对齐文档示例
- [x] 40100 错误码能正确触发
- [x] 服务启动无报错
- [x] 所有7个接口都能正常调用
---
## Notes
- 所有代码修改都保持向后兼容
- 无需数据库迁移(使用内存存储)
- 错误处理机制保持不变
- 请求头 X-Xencio-Client-Id 不验证

View File

@@ -0,0 +1,373 @@
# 获取单个文件上传状态接口优化设计
## 文档信息
- **创建日期**: 2026-03-12
- **设计者**: Claude Code
- **状态**: 待实施
## 1. 需求背景
### 1.1 接口信息
- **接口路径**: `/watson/api/project/bs/upload` (GET)
- **接口名称**: 获取单个文件上传后的状态
- **项目背景**: 流水分析 Mock 服务器
### 1.2 当前问题
当前实现存在以下问题:
1. **依赖实际上传记录**: 接口依赖 `self.file_records`上传时存储的记录如果没有上传过文件logs 返回空数组
2. **不符合 Mock 服务器定位**: Mock 服务器应该独立工作,前端测试时不应依赖其他接口
3. **字段值不正确**: `logMeta` 字段中的 `balanceAmount` 值为布尔值 `true`,应该为字符串 `"-1"`
### 1.3 期望行为
根据接口文档(`assets/兰溪-流水分析对接3.md` 第374-516行
1. **带 logId 参数**: 根据 logId 生成固定的文件记录数据(相同 logId 返回相同数据)
2. **不带 logId 参数**: 返回空的 logs 数组
3. **固定成功状态**: status=-5, uploadStatusDesc="data.wait.confirm.newaccount"
4. **独立性**: 不依赖实际上传的文件记录,接口独立工作
## 2. 解决方案
### 2.1 设计原则
1. **确定性随机**: 使用 `random.seed(log_id)` 确保相同 logId 生成相同数据
2. **完全独立**: 不依赖 `self.file_records`,在 `get_upload_status()` 中直接生成数据
3. **文档对齐**: 严格遵循接口文档示例的字段和格式
4. **简单高效**: 代码简洁,易于维护和测试
### 2.2 核心设计
#### 2.2.1 数据生成策略
**基于 logId 的确定性随机生成**
```python
def get_upload_status(self, group_id: int, log_id: int = None) -> dict:
"""
获取文件上传状态
Args:
group_id: 项目ID
log_id: 文件ID可选
Returns:
上传状态响应字典
"""
logs = []
if log_id:
# 使用 logId 作为随机种子,确保相同 logId 返回相同数据
random.seed(log_id)
# 生成确定性的文件记录
record = self._generate_deterministic_record(log_id, group_id)
logs.append(record)
# 返回响应
return {
"code": "200",
"data": {
"logs": logs,
"status": "",
"accountId": 8954,
"currency": "CNY"
},
"status": "200",
"successResponse": True
}
```
#### 2.2.2 字段生成规则
根据文档示例(`assets/兰溪-流水分析对接3.md` 第431-499行logs 数组中的每个对象包含 26 个字段:
| 字段名 | 生成规则 | 示例值 |
|--------|----------|--------|
| accountNoList | 11位随机数字 | ["18785967364"] |
| bankName | 从3种银行中随机选择 | "ALIPAY" |
| dataTypeInfo | 固定值 | ["CSV", ","] |
| downloadFileName | 基于 logId 生成 | "测试文件_13994.csv" |
| enterpriseNameList | 70%概率有主体30%为空 | ["测试主体"] 或 [""] |
| fileSize | 随机范围 10000-100000 | 16322 |
| fileUploadBy | 固定值 | 448 |
| fileUploadByUserName | 固定值 | "admin@support.com" |
| fileUploadTime | 当前时间 | "2025-03-13 08:45:32" |
| isSplit | 固定值 | 0 |
| leId | 10000 + 随机数 | 10741 |
| logId | 参数传入 | 13994 |
| logMeta | **修复为字符串 "-1"** | "{\"lostHeader\":[],\"balanceAmount\":\"-1\"}" |
| logType | 固定值 | "bankstatement" |
| loginLeId | 10000 + 随机数 | 10741 |
| lostHeader | 固定空数组 | [] |
| realBankName | 与 bankName 一致 | "ALIPAY" |
| rows | 固定值 | 0 |
| source | 固定值 | "http" |
| status | 固定成功值 | -5 |
| templateName | 根据银行选择对应模板 | "ALIPAY_T220708" |
| totalRecords | 随机范围 100-300 | 127 |
| trxDateEndId | 当前日期 | 20231231 |
| trxDateStartId | 当前日期 - 随机90-365天 | 20230102 |
| uploadFileName | 基于 logId 生成 | "测试文件_13994.pdf" |
| uploadStatusDesc | 固定成功描述 | "data.wait.confirm.newaccount" |
#### 2.2.3 银行类型映射
| bankName | templateName | realBankName |
|----------|--------------|--------------|
| "ALIPAY" | "ALIPAY_T220708" | "ALIPAY" |
| "BSX" | "BSX_T240925" | "BSX" |
| "ZJRCU" | "ZJRCU_T251114" | "ZJRCU" |
### 2.3 关键修复点
#### 修复1: logMeta 字段
**当前实现**`services/file_service.py:47`:
```python
log_meta: str = "{\"lostHeader\":[],\"balanceAmount\":true}" # ❌ 错误
```
**修复后**:
```python
log_meta: str = "{\"lostHeader\":[],\"balanceAmount\":\"-1\"}" # ✅ 正确
```
#### 修复2: 独立数据生成
**当前实现**: 依赖 `self.file_records`
**修复后**: 在 `get_upload_status()` 中独立生成数据,不依赖上传记录
## 3. 技术设计
### 3.1 修改文件清单
| 文件 | 修改内容 |
|------|----------|
| `services/file_service.py` | 1. 修复 FileRecord.log_meta 默认值<br>2. 重构 get_upload_status() 方法<br>3. 新增 _generate_deterministic_record() 方法 |
### 3.2 核心代码实现
#### 3.2.1 新增方法: _generate_deterministic_record()
```python
def _generate_deterministic_record(self, log_id: int, group_id: int) -> dict:
"""
基于 logId 生成确定性的文件记录
Args:
log_id: 文件ID用作随机种子
group_id: 项目ID
Returns:
文件记录字典26个字段
"""
# 银行类型选项
bank_options = [
("ALIPAY", "ALIPAY_T220708"),
("BSX", "BSX_T240925"),
("ZJRCU", "ZJRCU_T251114")
]
bank_name, template_name = random.choice(bank_options)
# 生成交易日期范围
end_date = datetime.now()
start_date = end_date - timedelta(days=random.randint(90, 365))
# 生成账号和主体
account_no = f"{random.randint(10000000000, 99999999999)}"
enterprise_names = ["测试主体"] if random.random() > 0.3 else [""]
return {
"accountNoList": [account_no],
"bankName": bank_name,
"dataTypeInfo": ["CSV", ","],
"downloadFileName": f"测试文件_{log_id}.csv",
"enterpriseNameList": enterprise_names,
"fileSize": random.randint(10000, 100000),
"fileUploadBy": 448,
"fileUploadByUserName": "admin@support.com",
"fileUploadTime": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"isSplit": 0,
"leId": 10000 + random.randint(0, 9999),
"logId": log_id,
"logMeta": "{\"lostHeader\":[],\"balanceAmount\":\"-1\"}",
"logType": "bankstatement",
"loginLeId": 10000 + random.randint(0, 9999),
"lostHeader": [],
"realBankName": bank_name,
"rows": 0,
"source": "http",
"status": -5,
"templateName": template_name,
"totalRecords": random.randint(100, 300),
"trxDateEndId": int(end_date.strftime("%Y%m%d")),
"trxDateStartId": int(start_date.strftime("%Y%m%d")),
"uploadFileName": f"测试文件_{log_id}.pdf",
"uploadStatusDesc": "data.wait.confirm.newaccount"
}
```
#### 3.2.2 重构方法: get_upload_status()
```python
def get_upload_status(self, group_id: int, log_id: int = None) -> dict:
"""
获取文件上传状态(基于 logId 生成确定性数据)
Args:
group_id: 项目ID
log_id: 文件ID可选
Returns:
上传状态响应字典
"""
logs = []
if log_id:
# 使用 logId 作为随机种子,确保相同 logId 返回相同数据
random.seed(log_id)
# 生成确定性的文件记录
record = self._generate_deterministic_record(log_id, group_id)
logs.append(record)
# 返回响应
return {
"code": "200",
"data": {
"logs": logs,
"status": "",
"accountId": 8954,
"currency": "CNY"
},
"status": "200",
"successResponse": True
}
```
### 3.3 测试设计
#### 3.3.1 测试场景
1. **带 logId 查询**: 验证返回非空 logs 数组
2. **不带 logId 查询**: 验证返回空 logs 数组
3. **确定性测试**: 相同 logId 多次调用返回相同数据
4. **字段完整性**: 验证返回的 26 个字段都存在
5. **字段值正确性**: 验证 status=-5, logMeta 格式正确
6. **银行类型随机性**: 验证不同 logId 生成不同银行类型
#### 3.3.2 测试用例示例
```python
def test_get_upload_status_with_log_id():
"""测试带 logId 参数查询"""
response = client.get("/watson/api/project/bs/upload?groupId=1000&logId=13994")
assert response.status_code == 200
data = response.json()
assert data["code"] == "200"
assert len(data["data"]["logs"]) == 1
assert data["data"]["logs"][0]["logId"] == 13994
assert data["data"]["logs"][0]["status"] == -5
assert data["data"]["logs"][0]["logMeta"] == "{\"lostHeader\":[],\"balanceAmount\":\"-1\"}"
def test_get_upload_status_without_log_id():
"""测试不带 logId 参数查询"""
response = client.get("/watson/api/project/bs/upload?groupId=1000")
assert response.status_code == 200
data = response.json()
assert data["code"] == "200"
assert len(data["data"]["logs"]) == 0
def test_deterministic_data():
"""测试相同 logId 返回相同数据"""
response1 = client.get("/watson/api/project/bs/upload?groupId=1000&logId=13994")
response2 = client.get("/watson/api/project/bs/upload?groupId=1000&logId=13994")
log1 = response1.json()["data"]["logs"][0]
log2 = response2.json()["data"]["logs"][0]
# 验证关键字段相同(除了 fileUploadTime
assert log1["logId"] == log2["logId"]
assert log1["bankName"] == log2["bankName"]
assert log1["accountNoList"] == log2["accountNoList"]
assert log1["enterpriseNameList"] == log2["enterpriseNameList"]
```
## 4. 实施要点
### 4.1 实施步骤
1. **修复 FileRecord 类**:修改 `log_meta` 默认值为正确的字符串格式
2. **重构 get_upload_status() 方法**:移除对 `self.file_records` 的依赖
3. **新增 _generate_deterministic_record() 方法**:实现确定性数据生成
4. **更新单元测试**:添加新的测试用例验证功能
5. **运行测试验证**:确保所有测试通过
### 4.2 注意事项
1. **随机种子**: 必须在生成数据前调用 `random.seed(log_id)`
2. **时间字段**: `fileUploadTime` 使用当前时间,每次调用会不同
3. **兼容性**: 不影响其他接口(上传、解析状态检查等)
4. **性能**: 无需优化,当前方案已足够高效
### 4.3 风险评估
| 风险 | 影响 | 缓解措施 |
|------|------|----------|
| 与上传接口数据不一致 | 低 | Mock 服务器允许独立数据源 |
| 随机种子冲突 | 极低 | logId 范围足够大10000+ |
| 字段缺失 | 中 | 严格按文档生成 26 个字段 |
## 5. 验收标准
### 5.1 功能验收
- [ ] 带 logId 参数查询返回非空 logs 数组
- [ ] 不带 logId 参数查询返回空 logs 数组
- [ ] 相同 logId 多次查询返回相同的核心字段值
- [ ] 返回数据包含完整的 26 个字段
- [ ] status 字段值为 -5
- [ ] logMeta 字段中 balanceAmount 为字符串 "-1"
### 5.2 质量验收
- [ ] 所有单元测试通过
- [ ] 代码符合项目编码规范
- [ ] 无语法错误和运行时错误
- [ ] API 文档Swagger UI正确展示接口
### 5.3 文档验收
- [ ] CLAUDE.md 更新(如有必要)
- [ ] 代码注释完整清晰
- [ ] 测试用例覆盖所有场景
## 6. 后续优化建议
### 6.1 可选增强
1. **缓存机制**: 如需提高性能,可基于 logId 缓存生成结果
2. **更多银行类型**: 扩展银行类型和模板选项
3. **异常场景**: 支持通过特殊 logId 触发错误响应
### 6.2 不建议的优化
1. **关联上传记录**: 会增加复杂度,违背 Mock 服务器独立原则
2. **预生成数据池**: 过度设计,当前场景不需要
## 7. 参考资料
- 接口文档: `assets/兰溪-流水分析对接3.md` 第374-516行
- 当前实现: `services/file_service.py` 第265-300行
- FileRecord 模型: `services/file_service.py` 第12-59行

View File

@@ -0,0 +1,468 @@
# 获取单个文件上传状态接口优化实施计划
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** 优化 `/watson/api/project/bs/upload` 接口,实现基于 logId 的确定性数据生成,不依赖上传记录。
**Architecture:** 使用 `random.seed(log_id)` 确保相同 logId 生成相同数据,完全独立于文件上传记录,符合 Mock 服务器定位。
**Tech Stack:** FastAPI, Python random/datetime, pytest
---
## Task 1: 修复 FileRecord 类的 log_meta 默认值
**Files:**
- Modify: `services/file_service.py:47`
**Step 1: 修改 log_meta 默认值**
`services/file_service.py` 第 47 行,将:
```python
log_meta: str = "{\"lostHeader\":[],\"balanceAmount\":true}"
```
改为:
```python
log_meta: str = "{\"lostHeader\":[],\"balanceAmount\":\"-1\"}"
```
**Step 2: 验证修改**
运行: `python -c "from services.file_service import FileRecord; r = FileRecord(log_id=1, group_id=1, file_name='test.csv'); print(r.log_meta)"`
预期输出:
```
{"lostHeader":[],"balanceAmount":"-1"}
```
**Step 3: 提交修复**
```bash
git add services/file_service.py
git commit -m "fix: 修复 FileRecord.log_meta 中 balanceAmount 值为字符串 '-1'"
```
---
## Task 2: 编写测试 - 带 logId 查询返回数据
**Files:**
- Modify: `tests/test_api.py`
**Step 1: 编写测试用例**
`tests/test_api.py` 文件末尾添加:
```python
def test_get_upload_status_with_log_id():
"""测试带 logId 参数查询返回非空 logs"""
response = client.get("/watson/api/project/bs/upload?groupId=1000&logId=13994")
assert response.status_code == 200
data = response.json()
# 验证基本响应结构
assert data["code"] == "200"
assert data["status"] == "200"
assert data["successResponse"] is True
# 验证 logs 不为空
assert len(data["data"]["logs"]) == 1
# 验证返回的 logId 正确
log = data["data"]["logs"][0]
assert log["logId"] == 13994
# 验证固定成功状态
assert log["status"] == -5
assert log["uploadStatusDesc"] == "data.wait.confirm.newaccount"
# 验证 logMeta 格式正确
assert log["logMeta"] == "{\"lostHeader\":[],\"balanceAmount\":\"-1\"}"
```
**Step 2: 运行测试验证失败**
运行: `pytest tests/test_api.py::test_get_upload_status_with_log_id -v`
预期: FAIL因为还未实现
**Step 3: 提交测试**
```bash
git add tests/test_api.py
git commit -m "test: 添加带 logId 查询的测试用例"
```
---
## Task 3: 编写测试 - 不带 logId 查询返回空数组
**Files:**
- Modify: `tests/test_api.py`
**Step 1: 编写测试用例**
`tests/test_api.py` 文件末尾添加:
```python
def test_get_upload_status_without_log_id():
"""测试不带 logId 参数查询返回空 logs 数组"""
response = client.get("/watson/api/project/bs/upload?groupId=1000")
assert response.status_code == 200
data = response.json()
# 验证基本响应结构
assert data["code"] == "200"
assert data["status"] == "200"
assert data["successResponse"] is True
# 验证 logs 为空
assert len(data["data"]["logs"]) == 0
# 验证其他字段存在
assert data["data"]["status"] == ""
assert data["data"]["accountId"] == 8954
assert data["data"]["currency"] == "CNY"
```
**Step 2: 运行测试验证失败**
运行: `pytest tests/test_api.py::test_get_upload_status_without_log_id -v`
预期: FAIL因为还未实现
**Step 3: 提交测试**
```bash
git add tests/test_api.py
git commit -m "test: 添加不带 logId 查询的测试用例"
```
---
## Task 4: 编写测试 - 确定性数据生成
**Files:**
- Modify: `tests/test_api.py`
**Step 1: 编写测试用例**
`tests/test_api.py` 文件末尾添加:
```python
def test_deterministic_data_generation():
"""测试相同 logId 多次查询返回相同的核心字段值"""
# 第一次查询
response1 = client.get("/watson/api/project/bs/upload?groupId=1000&logId=13994")
log1 = response1.json()["data"]["logs"][0]
# 第二次查询
response2 = client.get("/watson/api/project/bs/upload?groupId=1000&logId=13994")
log2 = response2.json()["data"]["logs"][0]
# 验证关键字段相同
assert log1["logId"] == log2["logId"]
assert log1["bankName"] == log2["bankName"]
assert log1["accountNoList"] == log2["accountNoList"]
assert log1["enterpriseNameList"] == log2["enterpriseNameList"]
assert log1["status"] == log2["status"]
assert log1["logMeta"] == log2["logMeta"]
assert log1["templateName"] == log2["templateName"]
assert log1["trxDateStartId"] == log2["trxDateStartId"]
assert log1["trxDateEndId"] == log2["trxDateEndId"]
def test_field_completeness():
"""测试返回数据包含完整的 26 个字段"""
response = client.get("/watson/api/project/bs/upload?groupId=1000&logId=13994")
log = response.json()["data"]["logs"][0]
# 验证所有必需字段存在
required_fields = [
"accountNoList", "bankName", "dataTypeInfo", "downloadFileName",
"enterpriseNameList", "fileSize", "fileUploadBy", "fileUploadByUserName",
"fileUploadTime", "isSplit", "leId", "logId", "logMeta", "logType",
"loginLeId", "lostHeader", "realBankName", "rows", "source", "status",
"templateName", "totalRecords", "trxDateEndId", "trxDateStartId",
"uploadFileName", "uploadStatusDesc"
]
for field in required_fields:
assert field in log, f"缺少字段: {field}"
```
**Step 2: 运行测试验证失败**
运行: `pytest tests/test_api.py::test_deterministic_data_generation tests/test_api.py::test_field_completeness -v`
预期: FAIL因为还未实现
**Step 3: 提交测试**
```bash
git add tests/test_api.py
git commit -m "test: 添加确定性和字段完整性测试用例"
```
---
## Task 5: 实现 _generate_deterministic_record() 方法
**Files:**
- Modify: `services/file_service.py`
**Step 1: 在 FileService 类中添加新方法**
`services/file_service.py``FileService` 类中,在 `_delayed_parse` 方法之后(约第 200 行)添加:
```python
def _generate_deterministic_record(self, log_id: int, group_id: int) -> dict:
"""
基于 logId 生成确定性的文件记录
Args:
log_id: 文件ID用作随机种子
group_id: 项目ID
Returns:
文件记录字典26个字段
"""
# 银行类型选项
bank_options = [
("ALIPAY", "ALIPAY_T220708"),
("BSX", "BSX_T240925"),
("ZJRCU", "ZJRCU_T251114")
]
bank_name, template_name = random.choice(bank_options)
# 生成交易日期范围
end_date = datetime.now()
start_date = end_date - timedelta(days=random.randint(90, 365))
# 生成账号和主体
account_no = f"{random.randint(10000000000, 99999999999)}"
enterprise_names = ["测试主体"] if random.random() > 0.3 else [""]
return {
"accountNoList": [account_no],
"bankName": bank_name,
"dataTypeInfo": ["CSV", ","],
"downloadFileName": f"测试文件_{log_id}.csv",
"enterpriseNameList": enterprise_names,
"fileSize": random.randint(10000, 100000),
"fileUploadBy": 448,
"fileUploadByUserName": "admin@support.com",
"fileUploadTime": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"isSplit": 0,
"leId": 10000 + random.randint(0, 9999),
"logId": log_id,
"logMeta": "{\"lostHeader\":[],\"balanceAmount\":\"-1\"}",
"logType": "bankstatement",
"loginLeId": 10000 + random.randint(0, 9999),
"lostHeader": [],
"realBankName": bank_name,
"rows": 0,
"source": "http",
"status": -5,
"templateName": template_name,
"totalRecords": random.randint(100, 300),
"trxDateEndId": int(end_date.strftime("%Y%m%d")),
"trxDateStartId": int(start_date.strftime("%Y%m%d")),
"uploadFileName": f"测试文件_{log_id}.pdf",
"uploadStatusDesc": "data.wait.confirm.newaccount"
}
```
**Step 2: 验证语法正确**
运行: `python -m py_compile services/file_service.py`
预期: 无输出(表示语法正确)
**Step 3: 提交代码**
```bash
git add services/file_service.py
git commit -m "feat: 添加 _generate_deterministic_record 方法"
```
---
## Task 6: 重构 get_upload_status() 方法
**Files:**
- Modify: `services/file_service.py:265-300`
**Step 1: 替换整个 get_upload_status() 方法**
`services/file_service.py` 中,找到 `get_upload_status` 方法(约第 265-300 行),完全替换为:
```python
def get_upload_status(self, group_id: int, log_id: int = None) -> dict:
"""
获取文件上传状态(基于 logId 生成确定性数据)
Args:
group_id: 项目ID
log_id: 文件ID可选
Returns:
上传状态响应字典
"""
logs = []
if log_id:
# 使用 logId 作为随机种子,确保相同 logId 返回相同数据
random.seed(log_id)
# 生成确定性的文件记录
record = self._generate_deterministic_record(log_id, group_id)
logs.append(record)
# 返回响应
return {
"code": "200",
"data": {
"logs": logs,
"status": "",
"accountId": 8954,
"currency": "CNY"
},
"status": "200",
"successResponse": True
}
```
**Step 2: 验证语法正确**
运行: `python -m py_compile services/file_service.py`
预期: 无输出(表示语法正确)
**Step 3: 提交重构**
```bash
git add services/file_service.py
git commit -m "refactor: 重构 get_upload_status 方法实现独立数据生成"
```
---
## Task 7: 运行所有测试验证功能
**Files:**
- Test: `tests/test_api.py`
**Step 1: 运行新增的测试用例**
运行: `pytest tests/test_api.py::test_get_upload_status_with_log_id tests/test_api.py::test_get_upload_status_without_log_id tests/test_api.py::test_deterministic_data_generation tests/test_api.py::test_field_completeness -v`
预期: 所有测试 PASS
**Step 2: 运行完整的测试套件**
运行: `pytest tests/ -v`
预期: 所有测试 PASS确保没有破坏其他功能
**Step 3: 手动测试接口**
运行: `python main.py`(在后台启动服务器)
在另一个终端运行:
```bash
curl "http://localhost:8000/watson/api/project/bs/upload?groupId=1000&logId=13994"
```
预期: 返回包含 logId=13994 的 JSON 数据
**Step 4: 提交验证记录**
```bash
git add tests/
git commit -m "test: 验证所有测试通过"
```
---
## Task 8: 更新文档并提交
**Files:**
- Modify: `CLAUDE.md`(可选)
**Step 1: 检查是否需要更新 CLAUDE.md**
查看项目根目录的 `CLAUDE.md` 文件,确认是否需要添加关于接口独立性的说明。如果需要,在适当位置添加:
```markdown
### 接口说明
**获取单个文件上传状态接口 (`/watson/api/project/bs/upload`)**
- 此接口完全独立工作,不依赖文件上传记录
- 基于 logId 参数生成确定性的随机数据
- 相同 logId 每次查询返回相同的核心字段值
```
**Step 2: 提交文档更新(如果有)**
```bash
git add CLAUDE.md
git commit -m "docs: 更新接口独立性说明"
```
**Step 3: 最终提交**
确保所有修改已提交:
```bash
git status
```
预期: 工作目录干净
---
## 验收清单
实施完成后,确认以下验收标准:
### 功能验收
- [x] 带 logId 参数查询返回非空 logs 数组
- [x] 不带 logId 参数查询返回空 logs 数组
- [x] 相同 logId 多次查询返回相同的核心字段值
- [x] 返回数据包含完整的 26 个字段
- [x] status 字段值为 -5
- [x] logMeta 字段中 balanceAmount 为字符串 "-1"
### 质量验收
- [x] 所有单元测试通过
- [x] 代码符合项目编码规范
- [x] 无语法错误和运行时错误
- [x] API 文档Swagger UI正确展示接口
### 文档验收
- [x] 代码注释完整清晰
- [x] 测试用例覆盖所有场景
---
## 实施说明
1. **TDD 流程**: 严格遵循"先写测试 → 运行失败 → 写代码 → 运行通过 → 提交"的流程
2. **频繁提交**: 每个小的步骤都有独立的提交,便于回滚和追踪
3. **独立性**: 此修改不影响其他接口(上传、解析状态检查等)
4. **确定性**: 使用 `random.seed(log_id)` 确保相同 logId 生成相同数据
5. **简单高效**: 代码简洁,无过度设计,符合 YAGNI 原则
---
## 参考资料
- 设计文档: `docs/plans/2026-03-12-upload-status-api-design.md`
- 接口文档: `assets/兰溪-流水分析对接3.md` 第374-516行
- 当前实现: `services/file_service.py` 第265-300行