"""上报更低价记录表(price_report)。 用户在「比价记录」里选一条记录,上报「某平台有比我们算出的最低价更低的价格」,带截图证明, 人工审核通过奖励金币。与 feedback(自由文本反馈)不同:本表结构化——关联具体比价记录、 原最低价 vs 上报价、审核状态、奖励。 - 原最低价快照(original_*)由后端按 comparison_record_id 反查比价记录的 best_* 冗余填入, 避免被关联记录后续删/改后对不上。 - 价格统一存「分」(cents),与 comparison_record / savings_record 一致。 - status: pending(审核中)/ approved(已通过)/ rejected(未通过),对齐前端筛选与状态徽标。 - images: 截图相对 URL 列表(/media/price_report/...,复用 app.core.media)。 - 奖励发放(通过 → 钱包 +reward_coins)是人工审核后台动作,提交时一律 pending,本表只留字段。 """ from __future__ import annotations from datetime import datetime from sqlalchemy import JSON, DateTime, ForeignKey, Integer, String, func from sqlalchemy.orm import Mapped, mapped_column from app.db.base import Base class PriceReport(Base): __tablename__ = "price_report" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) user_id: Mapped[int] = mapped_column( Integer, ForeignKey("user.id"), index=True, nullable=False ) # 选中的那条比价记录(可空:记录被删后仍保留上报历史) comparison_record_id: Mapped[int | None] = mapped_column( Integer, ForeignKey("comparison_record.id"), index=True, nullable=True ) # ===== 选中比价记录的快照(后端反查 comparison_record 填入,冗余存) ===== store_name: Mapped[str | None] = mapped_column(String(128), nullable=True) dish_summary: Mapped[str | None] = mapped_column(String(256), nullable=True) original_platform_id: Mapped[str | None] = mapped_column(String(32), nullable=True) original_platform_name: Mapped[str | None] = mapped_column(String(32), nullable=True) original_price_cents: Mapped[int | None] = mapped_column(Integer, nullable=True) # ===== 用户上报的更低价 ===== reported_platform_id: Mapped[str] = mapped_column(String(32), nullable=False) reported_platform_name: Mapped[str] = mapped_column(String(32), nullable=False) reported_price_cents: Mapped[int] = mapped_column(Integer, nullable=False) # 截图证明 URL 列表(相对路径,如 ["/media/price_report/u1_ab.jpg"]) images: Mapped[list[str]] = mapped_column(JSON, nullable=False, default=list) # ===== 审核(人工后台) ===== status: Mapped[str] = mapped_column( String(16), nullable=False, default="pending", index=True ) reject_reason: Mapped[str | None] = mapped_column(String(256), nullable=True) reward_coins: Mapped[int | None] = mapped_column(Integer, nullable=True) reviewed_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), server_default=func.now(), index=True, nullable=False ) def __repr__(self) -> str: # pragma: no cover return ( f"" )