6f2731ee2d
- 验签: pangle.build_sign/verify_callback_sign 改为 GroMore 官方
sign=SHA256("{m-key}:{trans_id}")(普通 SHA256,只签 trans_id);
回调响应改成 GroMore 要求的 {is_verify, reason}
- 发奖: 按回调 reward_amount 发金币(rewards 带回退/夹紧),ad_reward 幂等 + 每日上限
- 分层: app/core/{pangle,wxpay}.py → app/integrations/(对齐 jiguang/meituan/sms);
更新 ad.py / crud/wallet.py / scripts/sim_pangle_callback.py / 测试的 import
(含 test_withdraw 的 monkeypatch 路径 app.core.wxpay→app.integrations.wxpay)
- 测试: test_ad_reward + test_withdraw 共 17 项通过
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
88 lines
3.6 KiB
Python
88 lines
3.6 KiB
Python
"""福利 / 激励相关的业务常量与换算规则。
|
|
|
|
集中放在这里,业务代码引用,避免数值散落各处。这些是**产品规则**(签到奖励、
|
|
任务奖励、金币汇率)而非部署配置,所以不走环境变量;要调整直接改这里。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, datetime, timedelta, timezone
|
|
|
|
# 业务时区:签到的"今天"按北京时间算,不能用 UTC。
|
|
# 否则 UTC+8 的凌晨 0~8 点会被算成 UTC 的前一天,导致签到日期错位。
|
|
CN_TZ = timezone(timedelta(hours=8))
|
|
|
|
|
|
def cn_today() -> date:
|
|
"""当前北京时间的日期,签到去重 / 连续判断都以此为准。"""
|
|
return datetime.now(CN_TZ).date()
|
|
|
|
|
|
# ===== 签到:7 天循环,断签重置回第 1 天 =====
|
|
# 第 1→7 天的金币奖励,签到逻辑用 cycle_day(1..7)索引。
|
|
SIGNIN_REWARDS: tuple[int, ...] = (10, 20, 30, 50, 80, 120, 200)
|
|
SIGNIN_CYCLE_LEN: int = len(SIGNIN_REWARDS)
|
|
|
|
|
|
def signin_reward(cycle_day: int) -> int:
|
|
"""cycle_day 取值 1..SIGNIN_CYCLE_LEN。"""
|
|
return SIGNIN_REWARDS[cycle_day - 1]
|
|
|
|
|
|
# ===== 金币 → 现金 汇率 =====
|
|
# 10000 金币 = 1 元 = 100 分。
|
|
COIN_PER_YUAN: int = 10000
|
|
CENTS_PER_YUAN: int = 100
|
|
# 1 分对应多少金币 = 100。兑换金币数必须是它的整数倍,否则会出现不足 1 分的零头。
|
|
COIN_PER_CENT: int = COIN_PER_YUAN // CENTS_PER_YUAN
|
|
# 单次兑换最少 1 元(避免大量 1 分级碎兑)
|
|
MIN_EXCHANGE_COIN: int = COIN_PER_YUAN
|
|
|
|
|
|
def coins_to_cents(coin_amount: int) -> int:
|
|
"""金币换算成现金(分)。调用前应已校验 coin_amount 是 COIN_PER_CENT 的整数倍。"""
|
|
return coin_amount // COIN_PER_CENT
|
|
|
|
|
|
# ===== 提现(现金 → 微信零钱)=====
|
|
# 单次提现额度(分)。下限 0.1 元是微信商家转账的最低额,低于它(如 0.01 测试档)真转账会被拒。
|
|
WITHDRAW_MIN_CENTS: int = 10
|
|
WITHDRAW_MAX_CENTS: int = 5_000_000 # 5 万元
|
|
|
|
|
|
# ===== 一次性任务(领一次,user_task 去重)=====
|
|
TASK_ENABLE_NOTIFICATION = "enable_notification"
|
|
|
|
# task_key -> 奖励金币
|
|
TASK_REWARDS: dict[str, int] = {
|
|
TASK_ENABLE_NOTIFICATION: 10000,
|
|
}
|
|
|
|
|
|
# ===== 看激励视频发金币(穿山甲 S2S 服务端回调发奖)=====
|
|
# 看完一个激励视频发的金币(≈¥0.01,汇率 10000 金币=1 元)。
|
|
# 作用:① 回调缺/坏 reward_amount 时的回退值;② 客户端进度接口展示的"单次预告金币"。
|
|
# 真实发放以穿山甲回调带回的 reward_amount 为准(见 resolve_ad_reward_coin),后台应把
|
|
# 代码位"奖励数量"配成与本值一致(如 100),保证"广告内展示 / 进度预告 / 实际到账"三者一致。
|
|
AD_REWARD_COIN: int = 100
|
|
# 单次发奖金币上限:夹紧穿山甲回调里异常的 reward_amount(如后台多打一个 0),防刷爆余额。
|
|
MAX_AD_REWARD_COIN: int = 1000
|
|
# 每用户每日发奖次数上限,防刷 + 控成本。
|
|
# ⚠️ 占位值:正式上线前按产品策略 + 广告 eCPM 实测收益重新核定,改这里即可。
|
|
DAILY_AD_REWARD_LIMIT: int = 10
|
|
|
|
|
|
def resolve_ad_reward_coin(reward_amount: str | int | None) -> int:
|
|
"""把穿山甲回调的 reward_amount(后台代码位配的"奖励数量")解析成本次发放金币。
|
|
|
|
缺失 / 非数字 / ≤0 → 回退 AD_REWARD_COIN;超过 MAX_AD_REWARD_COIN → 夹紧。
|
|
注:reward_amount 不参与穿山甲 sign(只签 trans_id),但伪造回调需先伪造合法 sign
|
|
(要 SecurityKey),故能过验签的 reward_amount 即视为可信,这里只防后台配错。
|
|
"""
|
|
try:
|
|
coin = int(reward_amount) # type: ignore[arg-type]
|
|
except (TypeError, ValueError):
|
|
return AD_REWARD_COIN
|
|
if coin <= 0:
|
|
return AD_REWARD_COIN
|
|
return min(coin, MAX_AD_REWARD_COIN)
|