07c0b7502c
提现人工审核(提现不再即时打款): - models/wallet.py: WithdrawOrder 状态机改为 reviewing →(审核通过)→ pending → success/failed;reviewing →(驳回)→ rejected(已退款), 默认态 pending → reviewing(扣现金建单但不打款);新增 user_name 列(实名, 微信达额转账要求) - admin/routers/withdraw.py + admin/schemas/wallet.py: 运营后台审核接口(通过触发微信转账 / 驳回退款) - api/v1/wallet.py + repositories/wallet.py: 发起提现进 reviewing 态, 驳回退回现金并写 withdraw_refund - schemas/welfare.py: 提现出参增 reviewing/rejected 状态 + fail_reason 看广告每日时长限流(防刷主闸 + 次数兜底): - 新增 models/ad_watch_log.py: 按 (user_id, watch_date) 聚合当日观看秒数 - 新增 repositories/ad_watch.py: watched_seconds_today / add_watch_seconds(夹 [0, MAX_SINGLE_WATCH_SECONDS]) - api/v1/ad.py: 新增 POST /ad/watch-report(JWT 取 user, 落时长返当日累计); /ad/reward-status 增 watched_seconds_today / limit / remaining - schemas/ad.py: WatchReportIn/Out - core/rewards.py: 新增 DAILY_AD_WATCH_SECONDS_LIMIT=50 分钟(主闸)+ MAX_SINGLE_WATCH_SECONDS=120; DAILY_AD_REWARD_LIMIT 20→200 改作次数兜底(防前端少报时长绕过时长闸, 又不误伤看短广告的正常用户) - repositories/ad_reward.py + models/__init__.py: 接入新表与时长闸 alembic: withdraw_review_and_ad_watch 迁移(withdraw_order.user_name 列 + ad_watch_log 表) tests: 补 test_ad_reward / test_withdraw Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: pure <pure@192.168.0.104> Reviewed-on: #15
223 lines
7.2 KiB
Python
223 lines
7.2 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="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
|
|
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..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 # 省下(分)
|
|
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);为空表示到底")
|