"""福利模块(钱包 / 签到 / 任务)请求 / 响应 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="当前现金余额(分)") invite_cash_balance_cents: int = Field(0, 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(可能为空)") transfer_auth_enabled: bool = Field( False, description="是否已开启免确认到账(开启后提现免跳微信确认,直接到账)" ) # ===== 免确认收款授权(用户授权免确认模式)===== class TransferAuthResultOut(BaseModel): already_active: bool = Field(False, description="是否已是开启状态(无需再授权)") package_info: str | None = Field(None, description="拉起微信授权页的 package(已开启时为空)") mch_id: str | None = None app_id: str | None = None class TransferAuthStatusOut(BaseModel): state: str = Field(..., description="none(未开启)/pending(待确认)/active(已开启)/closed(已关闭)") enabled: bool = Field(..., description="是否已开启免确认到账") 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 UnbindWechatRequest(BaseModel): # 有进行中提现单时,首次解绑会被拦(needs_confirm),用户确认后带 force=true 再调一次才真解绑。 force: bool = Field(False, description="跳过「进行中提现」二次确认,强制解绑") class UnbindWechatResultOut(BaseModel): bound: bool = False # 有进行中提现单且未 force → True:本次未解绑(bound 仍 True),客户端弹确认弹窗。 needs_confirm: bool = False message: str | None = None class WithdrawRequest(BaseModel): amount_cents: int = Field(..., gt=0, description="提现金额(分)") source: str = Field("coin_cash", description="提现账户:coin_cash(金币现金) / invite_cash(邀请奖励金)") user_name: str | None = Field(None, description="实名(达额时微信要求,可空)") out_bill_no: str | None = Field( None, description="客户端幂等键(商户单号):同号重试不重复转账。不传则服务端生成" ) skip_review: bool = Field( False, description=( "调试直发:跳过人工审核立即打款。仅非生产(APP_ENV!=prod)生效," "客户端仅 debug 包下发(开发设置开关)。生产恒走人工审核。" ), ) class WithdrawResultOut(BaseModel): out_bill_no: str = Field(..., description="商户提现单号(查单用)") status: str = Field(..., description="reviewing(待审核) / pending / success / failed / rejected") 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="reviewing(待审核) / pending / success / failed / rejected") wechat_state: str | None = None amount_cents: int fail_reason: str | None = Field(None, description="failed/rejected 时的原因(用户可读)") # 审核通过进入打款、且微信要求用户确认(WAIT_USER_CONFIRM)时,带回这三项供 App 拉起微信确认页 package_info: str | None = None mch_id: str | None = None app_id: str | None = None class WithdrawOrderOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int out_bill_no: str amount_cents: int source: str = Field("coin_cash", description="提现账户:coin_cash / invite_cash") status: str = Field(..., description="reviewing(待审核) / pending / success / failed / rejected") 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..14") 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 SigninBoostRequest(BaseModel): ad_ref_id: str | None = Field(None, description="广告会话/交易号。当前开发期可空,后续接 S2S 时回填") class SigninBoostResultOut(BaseModel): coin_awarded: int = Field(..., description="本次膨胀补发金币") coin_balance: int = Field(..., description="膨胀补发后金币余额") signin_date: str = Field(..., description="被膨胀的签到日期 YYYY-MM-DD") # ===== 任务 ===== 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="连续省钱天数") compare_count: int = Field(..., description="累计完成比价次数(= 比价上报记录数)") class SavingsRecordOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int order_amount_cents: int # 实付金额(分) saved_amount_cents: int # 省下(分) original_price_cents: int | None = None # 源平台原价(分);demo 行为空 platform: str | None = None title: str | None = None shop_name: str | None = None dishes: list[str] = [] pay_channel: str | None = None # wechat/alipay;demo 行为空 source_platform_name: str | None = None # 源平台名,如 美团 created_at: datetime class SavingsRecordPage(BaseModel): items: list[SavingsRecordOut] next_cursor: int | None = Field(None, description="下一页游标(末条 id);为空表示到底")