Files
xsl a1d0171fc5
CI / lint / typecheck / import-linter / tests / secrets (push) Canceled after 0s
feat(uploads): 本地磁盘直传(COS 降级路线)+ 报告照片存在性校验
- POST /uploads/direct:原始字节 body + Content-Type 申报 + scene query(默认 report_photo),场景/类型/大小三重校验复用 storage 既有规则,落盘 var/uploads/<scene>/<user_id>/<uuid>.<ext>(config 新增 upload_dir),key 布局与 COS 同构;不加 python-multipart 依赖
- storage/local.py:save_direct_upload + require_local_upload(resolve 归并 + is_relative_to 归属收敛 + 存在性校验,越界/模拟/不存在一律 404)
- report/service.create_report:资格校验后、配额预扣前插入照片存在性校验(模拟 key 被拦且不消耗额度;regenerate 复用已验 photo_meta 不复核)
- 测试:test_upload_direct 14 单测 + test_upload_direct_flow 4 集成(真 key 全链路/假 key 404 零流水/401/40010)

偏差说明:test_report_flow._create_report 改走 /uploads/direct 拿真 key(存在性校验必然封死假 key),40920 与幂等重放两条断言语义未动。
2026-09-12 00:24:01 +08:00

112 lines
3.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""报告域的请求/响应模型。
Wire 响应统一走 ok() 信封(架构文档 §9.2),这里的模型只描述 ``data`` 部分;
雪花 ID 一律以字符串返回,避免 JS 精度丢失(NFR-MAINT-01 契约先行)。
"""
from __future__ import annotations
from datetime import datetime
from typing import Literal
from pydantic import BaseModel, Field
class ReportCreateRequest(BaseModel):
questionnaire: dict[str, object] = Field(
default_factory=dict, description="问卷答案;字段结构由产品定稿后在路由层收紧"
)
photo_object_key: str = Field(
min_length=1,
max_length=255,
description=(
"已上传照片的对象键(report_photo 场景);本地降级路线下为 "
"POST /uploads/direct 返回的相对路径,建报告时做存在性校验"
),
)
request_key: str | None = Field(
default=None, max_length=128, description="幂等键;相同键返回同一任务且不重复扣配额"
)
class ReportRegenerateRequest(BaseModel):
request_key: str | None = Field(
default=None, max_length=128, description="幂等键;相同键返回同一任务且不重复扣配额"
)
class ReportExportRequest(BaseModel):
request_key: str | None = Field(
default=None, max_length=128, description="幂等键;相同键返回同一导出任务且不重复扣配额"
)
class ReportExportResponse(BaseModel):
task_id: str
status: str = Field(description="tk_task 状态;导出完成后任务中心可取到 asset_key")
class ReportSubmitResponse(BaseModel):
task_id: str
report_id: str
status: str = Field(description="tk_task 状态;报告本体状态与其同步推进")
version: int
class ReportItem(BaseModel):
report_id: str
tier: str
status: str
version: int
result: dict[str, object] = Field(
description="按阶段键组织的产物;failed 报告只透错误摘要(中间态留给流水线复用)"
)
created_at: datetime
completed_at: datetime | None
# ---------------------------------------------------------------------------
# T-P3-21:维度级结论反馈
# ---------------------------------------------------------------------------
FeedbackVerdict = Literal["accurate", "inaccurate"]
class FeedbackCreateRequest(BaseModel):
dimension_key: str = Field(
min_length=1, max_length=64, description="报告 result.diagnosis.dimensions 的 key"
)
verdict: FeedbackVerdict = Field(description="准(accurate)/ 不准(inaccurate)")
correction: str | None = Field(
default=None, max_length=200, description="verdict=inaccurate 时的可选修正,≤200 字"
)
class FeedbackUpsertResponse(BaseModel):
dimension_key: str
verdict: str
updated: bool = Field(description="true=同维度已有反馈被覆盖更新;false=新建")
class FeedbackItem(BaseModel):
dimension_key: str
verdict: str
correction: str | None
created_at: datetime
# ---------------------------------------------------------------------------
# T-P3-22:预约留资(topic 预留通用性,首期 full_report)
# ---------------------------------------------------------------------------
class ReservationCreateRequest(BaseModel):
topic: str = Field(min_length=1, max_length=32)
class ReservationResponse(BaseModel):
topic: str
created: bool = Field(description="false=已存在(幂等命中,HTTP 仍 200)")
created_at: datetime
class ReservationListResponse(BaseModel):
topics: list[str]