Files
shaguabijia-app-server/app/models/savings.py
T
xianze ee6c2e4710 feat(记账): 比价省钱记账后端 + 此前未提交的归因订单基础
本次(M1 记账): savings_record 升级为记账唯一真相表(新增 源平台原价/比价价/支付渠道/平台包名/源平台名/源链接/幂等键/device_id, 加 user_id+client_event_id 唯一约束); /api/v1/order/report 改为写 savings_record(source=compare), 省额=源平台原价 减 实付, 按 client_event_id 幂等; 统计与明细改为 有真实记录用真实、否则 demo 兜底; 修复 dishes 列 JSONB 改 with_variant 兼容 SQLite 测试库; 新增 tests/test_order_savings.py, 全量 pytest 54 passed

一并纳入(此前工作区已有、未提交, 非本次改动): order_record 归因订单 模型/repo/api/迁移, 及 main.py、models/__init__ 注册等改动

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 21:30:30 +08:00

67 lines
3.5 KiB
Python
Raw 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.
"""省钱记录表。
是 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}>"