fix(risk): 列表接口的信封对齐 docs/05 §3.3——游标从 data 挪到 meta
docs/05 §3.3 的列表样例是 data 为**纯数组**、
ext_cursor 与 has_more 放在 meta 里,
并明确「业务接口不得增加其他顶层字段」。而 RiskQueryService._page 返回的
{items, next_cursor, has_more} 被**整体塞进 data** —— 游标因此出现在**业务数据**里,
meta 只剩 trace_id,两处都不符合契约。
改动:
- 新增 _list_envelope(page, context):把 _page 的结构拆成 data = items、
meta = {trace_id, next_cursor, has_more}。**service 侧不动** —— 它继续返回那个内部
结构,只是不再直接当 data 用。
- 3 个列表端点改用它:/alerts、/evidence/{source}、/notifications。
非列表端点(overview、详情、各类写操作)保持原样,不带游标。
**影响调用方**:这是接口形状变更。组员若写了前端读 data.items,需要改成读 data、
并从 meta 取分页元数据。改动依据是 docs/05 这个唯一权威接口文档(§20 也要求实现与
文档同步)。**合并时要提醒组员。**
**实测**(9002 身份):
- /alerts?limit=2 → 顶层键 ['data','meta']、data 是 list(2 条真实数据)、
meta 键 ['has_more','next_cursor','trace_id']
- /evidence/customers 与 /notifications 同形状
- 对照 /overview(非列表)→ meta 只有 trace_id、不带游标
**测试**:3 处断言从 data["items"] 改为 data;/notifications 那处补上对 meta 的断言;
新增 test_list_endpoints_follow_the_documented_envelope,直接断言"data 是纯数组、
meta 恰好三个键、顶层恰好 data/meta",把 §3.3 的契约钉住。
过程里踩了两个自己的坑:① 忘了 rom typing import Any(与 customer_service.py 同一失误,
被 ruff/mypy 当场抓住);② 漏了 /evidence/{source} 这个列表端点,是测试先失败才发现的。
ruff / mypy(136 文件) / 640 unit+contract / 29 integration 全绿。
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import json
|
||||
from collections.abc import AsyncIterator
|
||||
from datetime import datetime, time
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, File, Path, UploadFile
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -55,7 +56,7 @@ async def list_risk_alerts(
|
||||
session: AsyncSession = Depends(get_session), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
data = await RiskQueryService(session).list_alerts(context, query)
|
||||
return _envelope(data, context)
|
||||
return _list_envelope(data, context)
|
||||
|
||||
|
||||
@router.post("/alerts/scan")
|
||||
@@ -159,7 +160,7 @@ async def list_risk_evidence(
|
||||
session: AsyncSession = Depends(get_session), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
data = await RiskQueryService(session).list_evidence(context, source, query)
|
||||
return _envelope(data, context)
|
||||
return _list_envelope(data, context)
|
||||
|
||||
|
||||
@router.get("/notifications")
|
||||
@@ -169,7 +170,7 @@ async def list_risk_notifications(
|
||||
session: AsyncSession = Depends(get_session), # noqa: B008
|
||||
) -> dict[str, object]:
|
||||
data = await RiskNotificationService(session).list_notifications(context, query)
|
||||
return _envelope(data, context)
|
||||
return _list_envelope(data, context)
|
||||
|
||||
|
||||
@router.post("/daily-report")
|
||||
@@ -231,3 +232,23 @@ def _envelope(data: object, context: RequestContext) -> dict[str, object]:
|
||||
"data": data,
|
||||
"meta": {"trace_id": context.trace_id},
|
||||
}
|
||||
|
||||
|
||||
def _list_envelope(page: dict[str, Any], context: RequestContext) -> dict[str, object]:
|
||||
"""列表资源的信封(docs/05 §3.3)。
|
||||
|
||||
§3.3 的列表样例是 `data` 为**纯数组**、游标与 `has_more` 放在 `meta` 里,并且明确
|
||||
「业务接口不得增加其他顶层字段」。而 `RiskQueryService._page` 返回的是
|
||||
`{items, next_cursor, has_more}` —— 整体塞进 `data` 后,游标跑进了**业务数据**里、
|
||||
`meta` 只剩 trace_id,两处都不符合契约。
|
||||
|
||||
这里统一拆包;service 侧不必改(它继续返回那个内部结构,只是不再直接当 `data` 用)。
|
||||
"""
|
||||
return {
|
||||
"data": page.get("items") or [],
|
||||
"meta": {
|
||||
"trace_id": context.trace_id,
|
||||
"next_cursor": page.get("next_cursor"),
|
||||
"has_more": bool(page.get("has_more")),
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user