Files
shaguabijia-app-server/app/schemas/welfare.py
T
OuYingJun1024 0722d7b0d5 feat: 福利钱包+签到+省钱战绩+激励广告发奖+微信提现 后端
- 福利钱包: 金币/现金余额、流水、兑换 (api/v1/wallet.py, crud/wallet.py, models/wallet.py, schemas/welfare.py)
- 每日签到: 连续天数 + 档位奖励 (api/v1/signin.py, crud/signin.py, models/signin.py)
- 任务系统: "打开消息提醒"等任务领奖 (api/v1/tasks.py, crud/task.py, models/task.py)
- 省钱战绩: 省钱汇总/战绩/店铺菜品 (api/v1/savings.py, crud/savings.py, models/savings.py)
- 激励广告发奖: 穿山甲服务端回调 + 发奖规则 + 限流 (api/v1/ad.py, core/pangle.py, core/rewards.py, core/ratelimit.py, crud/ad_reward.py, schemas/ad.py, docs/ad_reward_golive_checklist.md)
- 微信提现: 商家转账到零钱 V3 (core/wxpay.py); user 表加微信 openid/nickname/avatar
- DB 迁移: 8 个 alembic (welfare 表/cash_transaction/openid 唯一约束/savings 店铺菜品/savings_record/ad_reward/withdraw_order+openid/user 微信字段)
- 运维脚本: reconcile_withdraws(对账) + reset_signin/reset_welfare + sim_pangle_callback(模拟穿山甲回调)
- 测试: test_welfare / test_withdraw / test_ad_reward
- 配置: .env.example + config.py 新增福利/广告/微信支付项; main.py 挂 5 个新路由

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 17:38:08 +08:00

215 lines
6.4 KiB
Python

"""福利模块(钱包 / 签到 / 任务)请求 / 响应 schemas。
约定同 auth:字段 snake_case、时间 ISO 8601(UTC)、金额存整数。
"""
from __future__ import annotations
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
# ===== 钱包 / 我的资产 =====
class CoinAccountOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
coin_balance: int = Field(..., description="当前金币余额")
cash_balance_cents: int = Field(..., description="当前现金余额(分)")
total_coin_earned: int = Field(..., description="累计赚取金币")
class CoinTransactionOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
amount: int = Field(..., description="正=入账,负=出账")
balance_after: int
biz_type: str
ref_id: str | None = None
remark: str | None = None
created_at: datetime
class CoinTransactionPage(BaseModel):
items: list[CoinTransactionOut]
next_cursor: int | None = Field(None, description="下一页游标(末条 id);为空表示到底")
class CashTransactionOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
amount_cents: int = Field(..., description="正=兑入,负=提现")
balance_after_cents: int
biz_type: str
ref_id: str | None = None
remark: str | None = None
created_at: datetime
class CashTransactionPage(BaseModel):
items: list[CashTransactionOut]
next_cursor: int | None = Field(None, description="下一页游标(末条 id);为空表示到底")
# ===== 金币兑现金 =====
class ExchangeInfoOut(BaseModel):
coin_per_yuan: int = Field(..., description="多少金币兑 1 元")
min_coin: int = Field(..., description="单次兑换最少金币")
step_coin: int = Field(..., description="兑换金币需为该值的整数倍(整分)")
class ExchangeRequest(BaseModel):
coin_amount: int = Field(..., gt=0, description="要兑换的金币数")
class ExchangeResultOut(BaseModel):
coin_amount: int = Field(..., description="本次扣除的金币")
cash_added_cents: int = Field(..., description="本次兑入的现金(分)")
coin_balance: int = Field(..., description="兑换后金币余额")
cash_balance_cents: int = Field(..., description="兑换后现金余额(分)")
# ===== 提现(现金 → 微信零钱) =====
class WithdrawInfoOut(BaseModel):
min_cents: int = Field(..., description="单次最低提现(分)")
max_cents: int = Field(..., description="单次最高提现(分)")
wechat_bound: bool = Field(..., description="当前用户是否已绑定微信")
wechat_nickname: str | None = Field(None, description="微信昵称(可能为空/脱敏)")
wechat_avatar_url: str | None = Field(None, description="微信头像 URL(可能为空)")
class BindWechatRequest(BaseModel):
code: str = Field(..., description="微信授权返回的 code")
class BindWechatResultOut(BaseModel):
bound: bool = True
wechat_nickname: str | None = None
wechat_avatar_url: str | None = None
class UnbindWechatResultOut(BaseModel):
bound: bool = False
class WithdrawRequest(BaseModel):
amount_cents: int = Field(..., gt=0, description="提现金额(分)")
user_name: str | None = Field(None, description="实名(达额时微信要求,可空)")
out_bill_no: str | None = Field(
None, description="客户端幂等键(商户单号):同号重试不重复转账。不传则服务端生成"
)
class WithdrawResultOut(BaseModel):
out_bill_no: str = Field(..., description="商户提现单号(查单用)")
status: str = Field(..., description="pending / success / failed")
wechat_state: str | None = Field(None, description="微信侧原始状态")
amount_cents: int
cash_balance_cents: int = Field(..., description="提现后现金余额(分)")
# 供 App 拉起微信确认页(WXOpenBusinessView requestMerchantTransfer)
package_info: str | None = None
mch_id: str | None = None
app_id: str | None = None
class WithdrawStatusOut(BaseModel):
out_bill_no: str
status: str = Field(..., description="pending / success / failed")
wechat_state: str | None = None
amount_cents: int
class WithdrawOrderOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
out_bill_no: str
amount_cents: int
status: str
wechat_state: str | None = None
fail_reason: str | None = None
created_at: datetime
class WithdrawOrderPage(BaseModel):
items: list[WithdrawOrderOut]
next_cursor: int | None = Field(None, description="下一页游标(末条 id);为空表示到底")
# ===== 签到 =====
class SigninStepOut(BaseModel):
day: int = Field(..., description="循环内第几天 1..7")
coin: int
status: str = Field(..., description="claimed / today / locked")
class SigninStatusOut(BaseModel):
today_signed: bool
consecutive_days: int
today_cycle_day: int
today_coin: int
can_claim: bool
steps: list[SigninStepOut]
class SigninResultOut(BaseModel):
coin_awarded: int
cycle_day: int
streak: int
coin_balance: int = Field(..., description="签到后金币余额")
# ===== 任务 =====
class TaskOut(BaseModel):
task_key: str
coin: int
claimed: bool
class TaskListOut(BaseModel):
items: list[TaskOut]
class TaskClaimResultOut(BaseModel):
task_key: str
coin_awarded: int
coin_balance: int = Field(..., description="领奖后金币余额")
# ===== 省钱(累计帮你省了 / 省钱战绩 / 明细)=====
class SavingsSummaryOut(BaseModel):
total_saved_cents: int = Field(..., description="累计省下(分)")
order_count: int = Field(..., description="累计省钱订单数")
avg_saved_cents: int = Field(..., description="平均每单省(分)")
class SavingsBattleOut(BaseModel):
week_saved_cents: int = Field(..., description="本周已省(分)")
beat_percent: int = Field(..., description="超过百分之多少用户")
streak_days: int = Field(..., description="连续省钱天数")
class SavingsRecordOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
order_amount_cents: int
saved_amount_cents: int
platform: str | None = None
title: str | None = None
shop_name: str | None = None
dishes: list[str] = []
created_at: datetime
class SavingsRecordPage(BaseModel):
items: list[SavingsRecordOut]
next_cursor: int | None = Field(None, description="下一页游标(末条 id);为空表示到底")