288766443a
从最新 origin/main 重拉的干净分支,只含本人记账改动,不含 marco 的归因订单(order_record)——后者是另一条未合并分支 feat/user-order-records 的功能,此前被误用 git add -A 和记账混进了 feat/savings-accounting-m1。本分支不带它:savings_record 升级为唯一真相表(新增原价/比价价/支付渠道/源平台/幂等键等列,迁移 savings_report_fields 直接挂 f01db5d77dac、不经 order_record_table);/order/report 改写 savings_record(source='compare'),省额=源平台原价−实付;展示 /api/v1/savings/* 真实优先 demo 兜底。无 order_record 的 model/repo/迁移/路由。alembic 单一 head + pytest 54 passed。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: xianze <ze@192.168.0.138> Reviewed-on: #10 Co-authored-by: zhangxianze <zhangxianze@wonderable.ai> Co-committed-by: zhangxianze <zhangxianze@wonderable.ai>
67 lines
3.5 KiB
Python
67 lines
3.5 KiB
Python
"""省钱记录表。
|
||
|
||
是 profile「累计帮你省了」和「省钱战绩」的唯一数据源:每完成一次比价/下单,
|
||
省了多少记一行。本期比价还没接后端,数据由 demo seeder 幂等灌入(source='demo'),
|
||
聚合接口照常做真实 SUM/分组;等比价真接入后,改成上报写入、停掉 seeder 即可。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
|
||
from sqlalchemy import JSON, DateTime, ForeignKey, Integer, String, UniqueConstraint, func
|
||
from sqlalchemy.dialects.postgresql import JSONB
|
||
from sqlalchemy.orm import Mapped, mapped_column
|
||
|
||
from app.db.base import Base
|
||
|
||
|
||
class SavingsRecord(Base):
|
||
__tablename__ = "savings_record"
|
||
__table_args__ = (
|
||
# 真实上报(source='compare')按 (user, client_event_id) 幂等;
|
||
# demo 行 client_event_id 为 NULL,不参与唯一性冲突(SQLite/PG 均允许多 NULL)
|
||
UniqueConstraint("user_id", "client_event_id", name="uq_savings_user_event"),
|
||
)
|
||
|
||
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
|
||
)
|
||
# 订单金额 / 省下金额,单位:分
|
||
order_amount_cents: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
saved_amount_cents: Mapped[int] = mapped_column(Integer, nullable=False)
|
||
platform: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||
title: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||
# 店铺名(订单明细卡标题,如「窑鸡王(王府井店)」)
|
||
shop_name: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||
# 菜品名列表:PG 用 JSONB(可建 GIN 索引),SQLite 用 JSON(TEXT)。前 2 道直接展示,其余收进「还有 N 道菜」。
|
||
dishes: Mapped[list[str]] = mapped_column(
|
||
JSON().with_variant(JSONB(), "postgresql"), nullable=False, default=list
|
||
)
|
||
# 来源:demo(演示) / compare(真实比价上报)
|
||
source: Mapped[str] = mapped_column(String(16), nullable=False, default="compare")
|
||
|
||
# ===== 真实比价上报(source='compare')新增字段;demo 行这些均为 NULL =====
|
||
# 源平台原价(用户原本要付的钱,分);省额 saved_amount_cents = original − order_amount(实付)
|
||
original_price_cents: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||
# 我们当时给出的比价价(分),审计/备用展示
|
||
compared_price_cents: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||
# 支付渠道:wechat / alipay
|
||
pay_channel: Mapped[str | None] = mapped_column(String(16), nullable=True)
|
||
# 实际下单平台包名
|
||
platform_package: Mapped[str | None] = mapped_column(String(128), nullable=True)
|
||
# 源平台展示名(如「美团」),用于「原价 ¥36.8(美团)」展示
|
||
source_platform_name: Mapped[str | None] = mapped_column(String(32), nullable=True)
|
||
# 源平台重进链接(预留:后续「重新进店/重复下单」用,本期只存不展示)
|
||
source_deeplink: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||
# 客户端幂等键(UUID);demo 行为 NULL
|
||
client_event_id: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||
device_id: Mapped[str | None] = mapped_column(String(128), 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"<SavingsRecord id={self.id} user_id={self.user_id} saved={self.saved_amount_cents}>"
|