Files
shaguabijia-app-server/scripts/sim_pangle_callback.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

90 lines
3.4 KiB
Python

"""模拟穿山甲激励视频 S2S 发奖回调,本地验证「验签 → 幂等发币 → 余额到账」闭环。
真穿山甲回调需要后端公网可达 + 穿山甲后台配置"奖励回调 URL",本地内网拿不到。本脚本用
FastAPI TestClient 在进程内打**真实** endpoint(`/api/v1/ad/pangle-callback`)、写**真实** DB
(./data/app.db),绕开公网依赖,证明后端这半条链路 OK——发完进 App 收益明细/重进福利页即可看到余额。
用法(在 shaguabijia-app-server 目录下,用本项目 python 跑):
python scripts/sim_pangle_callback.py # 给最近注册用户发一次(随机 trans_id)
python scripts/sim_pangle_callback.py 2 # 指定 user_id=2
python scripts/sim_pangle_callback.py 2 --trans t1 # 指定交易号;同号再跑应 Δ0(验幂等)
"""
from __future__ import annotations
import argparse
import os
import sys
import uuid
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
# 必须在 import app.* 之前开 PANGLE 开关(settings 在 import 时构造并缓存);
# 这里 setdefault 不覆盖 .env / 真实环境已有的值。
os.environ.setdefault("PANGLE_CALLBACK_ENABLED", "true")
os.environ.setdefault("PANGLE_REWARD_SECRET", "dev-local-pangle-secret")
from fastapi.testclient import TestClient # noqa: E402
from sqlalchemy import select # noqa: E402
from app.core import pangle # noqa: E402
from app.core.config import settings # noqa: E402
from app.core.rewards import AD_REWARD_COIN # noqa: E402
from app.crud import wallet as crud_wallet # noqa: E402
from app.db.session import SessionLocal # noqa: E402
from app.main import app # noqa: E402
from app.models.user import User # noqa: E402
def _latest_user_id() -> int | None:
db = SessionLocal()
try:
return db.execute(select(User.id).order_by(User.id.desc())).scalars().first()
finally:
db.close()
def _coin_balance(user_id: int) -> int:
db = SessionLocal()
try:
return crud_wallet.get_or_create_account(db, user_id).coin_balance
finally:
db.close()
def main() -> None:
p = argparse.ArgumentParser(description="模拟穿山甲发奖回调")
p.add_argument("user_id", nargs="?", type=int, default=None, help="目标 user_id(默认最近注册)")
p.add_argument("--trans", default=None, help="交易号(默认随机);同号重跑验幂等")
args = p.parse_args()
if not settings.pangle_callback_configured:
raise SystemExit("PANGLE 未配置(PANGLE_CALLBACK_ENABLED / PANGLE_REWARD_SECRET)")
uid = args.user_id or _latest_user_id()
if uid is None:
raise SystemExit("DB 里没有用户,先在 App 登录一次再跑")
trans_id = args.trans or f"sim_{uuid.uuid4().hex[:12]}"
before = _coin_balance(uid)
params = {
"user_id": str(uid),
"trans_id": trans_id,
"reward_name": "金币",
"reward_amount": "1",
}
params["sign"] = pangle.build_sign(params, settings.PANGLE_REWARD_SECRET)
resp = TestClient(app).get("/api/v1/ad/pangle-callback", params=params)
after = _coin_balance(uid)
print(f"user_id={uid} trans_id={trans_id}")
print(f"HTTP {resp.status_code} {resp.json()}")
print(f"金币: {before} -> {after}{after - before}, 单次应发 {AD_REWARD_COIN})")
print("同 trans_id 再跑应 Δ0(幂等);进 App 收益明细/重进福利页刷新即可看到余额。")
if __name__ == "__main__":
main()