"""福利模块(钱包 / 签到 / 任务)请求 / 响应 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);为空表示到底")