docs: add implementation plans for shelf registration and 409 fix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
266
docs/plans/2026-05-11-fix-409-response.md
Normal file
266
docs/plans/2026-05-11-fix-409-response.md
Normal file
@@ -0,0 +1,266 @@
|
||||
# 修复 409 响应格式 — 第一阶段:FastAPI
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 将 409 重复上架的响应改为统一错误格式 `{"error_code", "message", "location_code", "registered_at"}`。
|
||||
|
||||
**Architecture:** 用自定义异常 `DuplicateLocationError` 替代 `HTTPException`,通过自定义 exception handler 返回扁平的统一错误结构。同时更新 200 成功响应和 Swagger 文档。
|
||||
|
||||
**Tech Stack:** FastAPI, SQLAlchemy, pytest
|
||||
|
||||
---
|
||||
|
||||
## 目标响应格式
|
||||
|
||||
### 200 成功(不变)
|
||||
|
||||
```json
|
||||
{
|
||||
"zongpai_no": "26B1",
|
||||
"location_code": "A01-02-03",
|
||||
"created_at": "2026-05-11T10:30:00"
|
||||
}
|
||||
```
|
||||
|
||||
### 409 重复上架(修改后)
|
||||
|
||||
```json
|
||||
{
|
||||
"error_code": "DUPLICATE_LOCATION",
|
||||
"message": "该总排号已存在货位记录",
|
||||
"location_code": "A01-02-03",
|
||||
"registered_at": "2026-05-11T10:30:00"
|
||||
}
|
||||
```
|
||||
|
||||
### 400 校验失败(修改后)
|
||||
|
||||
```json
|
||||
{
|
||||
"error_code": "INVALID_ZONGPAI",
|
||||
"message": "INVALID_ZONGPAI"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 自定义异常 + handler + 测试更新
|
||||
|
||||
**Files:**
|
||||
- Modify: `apps/fastAPI/app/services/location_service.py`
|
||||
- Modify: `apps/fastAPI/app/main.py`
|
||||
- Modify: `apps/fastAPI/app/api/v1/location.py`
|
||||
- Modify: `apps/fastAPI/app/schemas/common.py`
|
||||
- Modify: `apps/fastAPI/tests/test_location_api.py`
|
||||
|
||||
**Step 1: 修改 `app/services/location_service.py` — 用自定义异常替换 HTTPException**
|
||||
|
||||
```python
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.finished_goods import FinishedGoodsLocation
|
||||
from app.schemas.location import LocationRequest
|
||||
|
||||
|
||||
class DuplicateLocationError(Exception):
|
||||
"""总排号已存在货位记录。"""
|
||||
|
||||
def __init__(self, location_code: str, registered_at: str):
|
||||
self.location_code = location_code
|
||||
self.registered_at = registered_at
|
||||
|
||||
|
||||
def register_location(db: Session, req: LocationRequest) -> FinishedGoodsLocation:
|
||||
existing = (
|
||||
db.query(FinishedGoodsLocation)
|
||||
.filter(FinishedGoodsLocation.zongpai_no == req.zongpai_no)
|
||||
.first()
|
||||
)
|
||||
if existing:
|
||||
raise DuplicateLocationError(
|
||||
location_code=existing.location_code,
|
||||
registered_at=existing.created_at.isoformat(),
|
||||
)
|
||||
|
||||
record = FinishedGoodsLocation(
|
||||
zongpai_no=req.zongpai_no,
|
||||
location_code=req.location_code,
|
||||
)
|
||||
db.add(record)
|
||||
db.commit()
|
||||
db.refresh(record)
|
||||
return record
|
||||
```
|
||||
|
||||
**Step 2: 修改 `app/schemas/common.py` — 更新 ErrorResponse 匹配新格式**
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ErrorResponse(BaseModel):
|
||||
error_code: str
|
||||
message: str
|
||||
```
|
||||
|
||||
**Step 3: 修改 `app/main.py` — 添加 DuplicateLocationError handler,更新 ValueError handler**
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app.api.v1.location import router as location_router
|
||||
from app.services.location_service import DuplicateLocationError
|
||||
|
||||
app = FastAPI(title="CargoTrace API", version="0.1.0")
|
||||
|
||||
app.include_router(location_router, prefix="/CargoTrace")
|
||||
|
||||
|
||||
@app.exception_handler(DuplicateLocationError)
|
||||
async def duplicate_location_handler(request: Request, exc: DuplicateLocationError):
|
||||
return JSONResponse(
|
||||
status_code=409,
|
||||
content={
|
||||
"error_code": "DUPLICATE_LOCATION",
|
||||
"message": "该总排号已存在货位记录",
|
||||
"location_code": exc.location_code,
|
||||
"registered_at": exc.registered_at,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.exception_handler(ValueError)
|
||||
async def value_error_handler(request: Request, exc: ValueError):
|
||||
error_code = str(exc)
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={
|
||||
"error_code": error_code,
|
||||
"message": error_code,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Welcome to CargoTrace API"}
|
||||
```
|
||||
|
||||
**Step 4: 修改 `app/api/v1/location.py` — 更新 Swagger 409 文档**
|
||||
|
||||
```python
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.schemas.common import ErrorResponse
|
||||
from app.schemas.location import LocationRequest, LocationResponse
|
||||
from app.services.location_service import register_location
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/location",
|
||||
response_model=LocationResponse,
|
||||
responses={
|
||||
400: {
|
||||
"description": "总排号或货位号格式不合法",
|
||||
"model": ErrorResponse,
|
||||
},
|
||||
409: {
|
||||
"description": "该总排号已存在货位记录",
|
||||
"model": ErrorResponse,
|
||||
},
|
||||
},
|
||||
)
|
||||
def create_location(req: LocationRequest, db: Session = Depends(get_db)):
|
||||
record = register_location(db, req)
|
||||
return LocationResponse(
|
||||
zongpai_no=record.zongpai_no,
|
||||
location_code=record.location_code,
|
||||
created_at=record.created_at.isoformat(),
|
||||
)
|
||||
```
|
||||
|
||||
**Step 5: 修改 `tests/test_location_api.py` — 更新断言**
|
||||
|
||||
```python
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
def test_register_location_success(client: TestClient):
|
||||
"""正常上架 — 应返回 200"""
|
||||
resp = client.post(
|
||||
"/CargoTrace/location",
|
||||
json={"zongpai_no": "26B999", "location_code": "A01-02-03"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["zongpai_no"] == "26B999"
|
||||
assert data["location_code"] == "A01-02-03"
|
||||
|
||||
|
||||
def test_register_location_duplicate(client: TestClient):
|
||||
"""重复上架 — 应返回 409,包含统一错误格式"""
|
||||
client.post(
|
||||
"/CargoTrace/location",
|
||||
json={"zongpai_no": "26C999", "location_code": "A01-02-03"},
|
||||
)
|
||||
resp = client.post(
|
||||
"/CargoTrace/location",
|
||||
json={"zongpai_no": "26C999", "location_code": "A02-01-01"},
|
||||
)
|
||||
assert resp.status_code == 409
|
||||
data = resp.json()
|
||||
assert data["error_code"] == "DUPLICATE_LOCATION"
|
||||
assert data["location_code"] == "A01-02-03"
|
||||
assert "registered_at" in data
|
||||
|
||||
|
||||
def test_register_location_invalid_zongpai(client: TestClient):
|
||||
"""无效总排号 — 应返回 400"""
|
||||
resp = client.post(
|
||||
"/CargoTrace/location",
|
||||
json={"zongpai_no": "INVALID", "location_code": "A01-02-03"},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.json()["error_code"] == "INVALID_ZONGPAI"
|
||||
|
||||
|
||||
def test_register_location_invalid_location(client: TestClient):
|
||||
"""无效货位号 — 应返回 400"""
|
||||
resp = client.post(
|
||||
"/CargoTrace/location",
|
||||
json={"zongpai_no": "26T999", "location_code": "bad-location"},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.json()["error_code"] == "INVALID_LOCATION"
|
||||
|
||||
|
||||
def test_register_location_trans_location(client: TestClient):
|
||||
"""转运特殊货位 — 应返回 200"""
|
||||
resp = client.post(
|
||||
"/CargoTrace/location",
|
||||
json={"zongpai_no": "26BW0999", "location_code": "TRANS-01"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["location_code"] == "TRANS-01"
|
||||
```
|
||||
|
||||
注意变更:
|
||||
- `INVALID_ZONGPAI` / `INVALID_LOCATION` 现在返回 **400**(不再是 422),由自定义 `ValueError` handler 处理
|
||||
- 409 响应断言改为验证 `error_code`、`location_code`、`registered_at` 三个字段
|
||||
|
||||
**Step 6: 运行测试**
|
||||
|
||||
Run: `cd D:/projects/CargoTrace/apps/fastAPI && .venv/Scripts/python.exe -m pytest tests/ -v`
|
||||
Expected: 5 tests PASSED
|
||||
|
||||
**Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add app/services/location_service.py app/main.py app/api/v1/location.py app/schemas/common.py tests/test_location_api.py
|
||||
git commit -m "fix: unify error response format with error_code, message and domain fields"
|
||||
```
|
||||
Reference in New Issue
Block a user