4506677483
Co-authored-by: xianze <ze@192.168.0.138> Reviewed-on: #13 Co-authored-by: zhangxianze <zhangxianze@wonderable.ai> Co-committed-by: zhangxianze <zhangxianze@wonderable.ai>
146 lines
5.5 KiB
Python
146 lines
5.5 KiB
Python
"""上报更低价 endpoint。
|
|
|
|
路由前缀 `/api/v1/report`,需 Bearer 鉴权(上报绑登录用户)。
|
|
POST / 提交上报(multipart:comparison_record_id / reported_platform_id /
|
|
reported_price(元) + images 1~4 张)
|
|
GET /records 上报记录列表(?status= pending/approved/rejected 可选筛选)
|
|
|
|
要点:
|
|
- 截图复用 [app.core.media] 落盘到 /media/price_report/。
|
|
- 原最低价由 comparison_record_id 反查比价记录 best_*(不信任客户端传的快照)。
|
|
- 校验(D):上报价必须 < 原最低价,否则 400。
|
|
- 提交一律 status=pending;通过发奖励是人工审核后台动作,不在此。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, File, Form, HTTPException, UploadFile
|
|
|
|
from app.api.deps import CurrentUser, DbSession
|
|
from app.core import media
|
|
from app.models.comparison import ComparisonRecord
|
|
from app.repositories import report as report_repo
|
|
from app.schemas.report import (
|
|
ReportRecordCounts,
|
|
ReportRecordOut,
|
|
ReportRecordsOut,
|
|
ReportSubmitOut,
|
|
)
|
|
|
|
logger = logging.getLogger("shagua.report")
|
|
|
|
router = APIRouter(prefix="/api/v1/report", tags=["report"])
|
|
|
|
_MAX_IMAGES = 4
|
|
_VALID_STATUS = ("pending", "approved", "rejected")
|
|
|
|
# 上报平台(canonical id → 展示名),与原型 fb-platform-chip data-pf 一致
|
|
_PLATFORM_NAMES = {
|
|
"meituan-waimai": "美团外卖",
|
|
"jd-waimai": "京东外卖",
|
|
"taobao-shanguang": "淘宝闪购",
|
|
}
|
|
|
|
|
|
def _dish_summary(items: list | None) -> str | None:
|
|
"""把比价记录 items[{name,qty,...}] 拼成菜品摘要文案。"""
|
|
if not items:
|
|
return None
|
|
names = [str(it.get("name", "")).strip() for it in items if isinstance(it, dict)]
|
|
names = [n for n in names if n]
|
|
return "、".join(names) if names else None
|
|
|
|
|
|
@router.post("", response_model=ReportSubmitOut, summary="提交上报更低价")
|
|
async def submit_report(
|
|
user: CurrentUser,
|
|
db: DbSession,
|
|
comparison_record_id: int = Form(...),
|
|
reported_platform_id: str = Form(...),
|
|
reported_price: str = Form(..., description="用户填的更低价(元)"),
|
|
images: list[UploadFile] = File(default=[]),
|
|
) -> ReportSubmitOut:
|
|
# 平台
|
|
reported_platform_id = reported_platform_id.strip()
|
|
platform_name = _PLATFORM_NAMES.get(reported_platform_id)
|
|
if platform_name is None:
|
|
raise HTTPException(status_code=400, detail="不支持的上报平台")
|
|
|
|
# 价格(元 → 分)
|
|
try:
|
|
price_yuan = float(reported_price)
|
|
except (TypeError, ValueError):
|
|
raise HTTPException(status_code=400, detail="价格格式不正确") from None
|
|
if price_yuan <= 0:
|
|
raise HTTPException(status_code=400, detail="价格必须大于 0")
|
|
reported_price_cents = round(price_yuan * 100)
|
|
|
|
# 反查比价记录(必须属于本人)→ 取原最低价快照
|
|
rec = db.get(ComparisonRecord, comparison_record_id)
|
|
if rec is None or rec.user_id != user.id:
|
|
raise HTTPException(status_code=404, detail="比价记录不存在")
|
|
original_price_cents = rec.best_price_cents
|
|
|
|
# D:上报价必须低于原最低价
|
|
if original_price_cents is not None and reported_price_cents >= original_price_cents:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"上报价需低于原最低价 ¥{original_price_cents / 100:.2f}",
|
|
)
|
|
|
|
# 截图(至少 1 张,最多 4 张)
|
|
files = [f for f in (images or []) if f is not None and f.filename]
|
|
if not files:
|
|
raise HTTPException(status_code=400, detail="请至少上传一张截图证明")
|
|
if len(files) > _MAX_IMAGES:
|
|
raise HTTPException(status_code=400, detail=f"最多上传 {_MAX_IMAGES} 张图片")
|
|
urls: list[str] = []
|
|
for f in files:
|
|
data = await f.read()
|
|
try:
|
|
urls.append(media.save_report_image(user.id, data))
|
|
except media.MediaError as e:
|
|
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
|
|
rep = report_repo.create_report(
|
|
db,
|
|
user_id=user.id,
|
|
comparison_record_id=comparison_record_id,
|
|
store_name=rec.store_name,
|
|
dish_summary=_dish_summary(rec.items),
|
|
original_platform_id=rec.best_platform_id,
|
|
original_platform_name=rec.best_platform_name,
|
|
original_price_cents=original_price_cents,
|
|
reported_platform_id=reported_platform_id,
|
|
reported_platform_name=platform_name,
|
|
reported_price_cents=reported_price_cents,
|
|
images=urls,
|
|
)
|
|
logger.info("price_report id=%d user_id=%d images=%d", rep.id, user.id, len(urls))
|
|
return ReportSubmitOut.model_validate(rep)
|
|
|
|
|
|
@router.get("/records", response_model=ReportRecordsOut, summary="上报记录列表")
|
|
def list_report_records(
|
|
user: CurrentUser,
|
|
db: DbSession,
|
|
status: str | None = None,
|
|
) -> ReportRecordsOut:
|
|
status = (status or "").strip() or None
|
|
if status and status not in _VALID_STATUS:
|
|
raise HTTPException(status_code=400, detail="无效的状态筛选")
|
|
rows = report_repo.list_reports(db, user.id, status)
|
|
# counts 始终基于全量(不受 status 筛选影响),供前端 chip 计数
|
|
all_rows = rows if status is None else report_repo.list_reports(db, user.id, None)
|
|
counts = ReportRecordCounts(
|
|
all=len(all_rows),
|
|
pending=sum(1 for r in all_rows if r.status == "pending"),
|
|
approved=sum(1 for r in all_rows if r.status == "approved"),
|
|
rejected=sum(1 for r in all_rows if r.status == "rejected"),
|
|
)
|
|
return ReportRecordsOut(
|
|
records=[ReportRecordOut.model_validate(r) for r in rows],
|
|
counts=counts,
|
|
)
|