Compare commits

..

4 Commits

Author SHA1 Message Date
guke a97e0346d0 chore: CPS 数据表字典 + 按群对账统计 mock 脚本 + CLAUDE.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 16:21:51 +08:00
guke 37619d4ac5 feat(cps): 美团/京东领券前微信头像授权插页
落地页对齐淘宝:微信内已拿 openid 但未授权头像时出单按钮插页,点按钮触发 snsapi_userinfo 授权(交互手势避免快照页),授权回来经 ?authed=1 自动跳券;取消授权由前端 pageshow/visibilitychange 转跳目标券页 + 回调空 code 兜底,绝不阻断领券。受 wx_oauth_active 开关控制。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 16:21:51 +08:00
guke b3c4898c5d fix(alembic): cps_v2_platforms 兼容 SQLite 且可重跑
batch_alter_table 包 ALTER(SQLite 无原生 ALTER COLUMN),server_default 按方言分叉(PG ::jsonb / SQLite 纯文本),add_column 加 _has_column 存在性守卫,让历史半应用的 SQLite 库自愈重跑。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 16:21:50 +08:00
guke b07997cd8d fix(dev): 启动脚本钉定 .venv 解释器 + watchfiles 降噪
run.bat/run.sh 优先用项目 .venv 的 python(回退 PATH),避免 conda base 等环境误用全局不兼容的 FastAPI(<0.115 撞 Pydantic 2.12 导入期崩 FieldInfo.in_)。logging 把 watchfiles logger 降到 WARNING,消除写日志→检测变更→再写日志的 --reload 自喂回环。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 16:21:50 +08:00
3 changed files with 0 additions and 58 deletions
-15
View File
@@ -120,21 +120,6 @@ def pangle_callback(request: Request, db: DbSession) -> PangleCallbackOut:
ad_session_id = extra.get("ad_session_id")
ecpm = params.get("ecpm")
# 环境隔离:激励视频 mediaExtra 里带「这次观看属于哪个后端环境」(srv_env=dev/prod,客户端按
# BuildConfig.DEBUG 决定),穿山甲 S2S 回调原样带回。回调 URL 在穿山甲后台只配一个(指向生产),
# 所以测试包(dev)看广告的 S2S 也会打到生产——若不拦,生产会把奖发给「本库里同 user_id 的另一个
# 真人」(user_id 是跨库不隔离的裸数字 → 跨库串号)。这里只处理「属于本服环境」的回调:环境不符
# 直接受理但不发币、不写任何记录,保证各环境后台广告收益页只含本环境用户。is_verify=true 让穿山甲
# 不再重试(测试用户的币由 localhost 的 test-grant 单独发,不依赖这条 S2S)。
# 兼容:旧客户端不带 srv_env(取不到)→ 视为本环境,照常处理,不误伤存量正式用户。
callback_env = extra.get("srv_env")
if callback_env and callback_env != settings.APP_ENV:
logger.info(
"pangle callback foreign env skip: callback_env=%s self_env=%s user_id=%d trans_id=%s",
callback_env, settings.APP_ENV, user_id, trans_id,
)
return PangleCallbackOut(is_verify=True, reason=REASON_OK)
existing = crud_ad.find_by_trans(db, trans_id)
if existing is not None:
logger.info(
-9
View File
@@ -12,7 +12,6 @@ FastAPI TestClient 在进程内打**真实** endpoint(`/api/v1/ad/pangle-callbac
from __future__ import annotations
import argparse
import json
import os
import sys
import uuid
@@ -57,11 +56,6 @@ 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="交易号(默认随机);同号重跑验幂等")
p.add_argument(
"--srv-env", default=None,
help="模拟客户端 mediaExtra 里的后端环境标记(dev/prod)。与本服 APP_ENV 不符时"
"应被环境隔离拦下(Δ0、不串号);留空=不带该字段(旧客户端,照常发)",
)
args = p.parse_args()
if not settings.pangle_callback_configured:
@@ -81,9 +75,6 @@ def main() -> None:
# 镜像生产:后台代码位"奖励数量"应配成 AD_REWARD_COIN,回调据此发金币
"reward_amount": str(AD_REWARD_COIN),
}
# 镜像穿山甲透传 mediaExtra:把后端环境标记放进 gromoreExtra(服务端据此做环境隔离)
if args.srv_env:
params["gromoreExtra"] = json.dumps({"srv_env": args.srv_env})
params["sign"] = pangle.build_sign(trans_id, settings.PANGLE_REWARD_SECRET)
resp = TestClient(app).get("/api/v1/ad/pangle-callback", params=params)
-34
View File
@@ -5,8 +5,6 @@
"""
from __future__ import annotations
import json
from sqlalchemy import select
from app.core.config import settings
@@ -181,38 +179,6 @@ def test_callback_unknown_user(client) -> None:
assert r.json() == {"is_verify": False, "reason": 2}
def test_callback_foreign_env_skipped(client) -> None:
"""回调 srv_env 与本服 APP_ENV 不符(测试包的 S2S 打到生产)→ 受理但不发币、不串号。
防的是跨库串号:同一 user_id 在测试库 / 正式库是两个人,回调地址只配生产,测试包(dev)看广告
的 S2S 也会打到生产。测试环境 APP_ENV=dev:srv_env=prod 视为「别的环境」→ 跳过,余额不变、
不写记录;srv_env=dev(本环境)照常发。srv_env 走 gromoreExtra 透传(穿山甲带回 mediaExtra 的字段)。
兼容:不带 srv_env 的旧客户端按本环境处理(其余用例已覆盖)。
"""
phone = "13800003501"
token = _login(client, phone)
uid = _user_id(phone)
# 外环境(prod)回调:受理(is_verify=true 防穿山甲重试)但不发币、不留记录
foreign = _signed(uid, "trans_foreign_env", ecpm="200",
gromoreExtra=json.dumps({"srv_env": "prod"}))
r = _callback(client, foreign)
assert r.status_code == 200, r.text
assert r.json() == {"is_verify": True, "reason": 0}
assert _coin_balance(client, token) == 0
st = client.get("/api/v1/ad/reward-status", headers=_auth(token)).json()
assert st["used_today"] == 0
# 本环境(dev)回调:照常发币
same = _signed(uid, "trans_same_env", ecpm="200",
gromoreExtra=json.dumps({"srv_env": "dev"}))
r = _callback(client, same)
assert r.status_code == 200, r.text
assert _coin_balance(client, token) == calculate_ad_reward_coin("200", 1)
st = client.get("/api/v1/ad/reward-status", headers=_auth(token)).json()
assert st["used_today"] == 1
def test_reward_status_requires_auth(client) -> None:
"""客户端进度接口需登录。"""
r = client.get("/api/v1/ad/reward-status")