feat(invite): 发奖口径改为「比价并下单」+ DEV dev-reset 出厂重置接口
- 发奖触发点从「比价成功上报」(compare/record)挪到「实际下单上报」 (order/report):被邀请人完成比价并真实下单才给邀请人发 2 元邀请奖励金 (冰 2026-07-02 拍板);仅完成比价不再发奖。幂等闸 compare_reward_granted 不变。 - 新增 POST /api/v1/invite/dev-reset:把固定测试号(TEST_ACCOUNT_PHONE)打回 出厂态(清邀请关系/奖励金/savings/流水 + 刷 created_at 骗过 72h 新用户闸), 便于反复当新用户测发奖。双门控:生产 404 + 只认配置的测试号,无鉴权。 - 测试:8 例改为下单驱动 + 新增 2 例覆盖 dev-reset(可反复邀请 / 开关关闭 400)。
This commit is contained in:
@@ -21,16 +21,15 @@ from app.api.deps import CurrentUser, DbSession
|
||||
from app.db.session import SessionLocal
|
||||
from app.models.comparison import ComparisonRecord
|
||||
from app.repositories import comparison as crud_compare
|
||||
from app.repositories import invite as crud_invite
|
||||
from app.services.pricebot_llm_calls import fetch_llm_calls
|
||||
from app.schemas.compare_record import (
|
||||
CompareStatsOut,
|
||||
ComparisonRecordCreatedOut,
|
||||
ComparisonRecordDetailOut,
|
||||
ComparisonRecordIn,
|
||||
ComparisonRecordPage,
|
||||
ComparisonRecordOut,
|
||||
ComparisonRecordPage,
|
||||
)
|
||||
from app.services.pricebot_llm_calls import fetch_llm_calls
|
||||
|
||||
logger = logging.getLogger("shagua.compare_record")
|
||||
|
||||
@@ -53,18 +52,8 @@ def report_record(
|
||||
# 任务做,不阻塞上报响应(顺带给 pricebot 落盘留足余量)。upsert 已 commit,后台用
|
||||
# 独立 session 按 record id 回填 llm_calls + 派生 llm_call_count/retry_count。
|
||||
background_tasks.add_task(_backfill_llm_calls, rec.id, rec.trace_id)
|
||||
# 邀请 v2 发奖:被邀请人完成一次【成功】比价 → 给邀请人发邀请奖励金(幂等,只发一次)。
|
||||
# best-effort:发奖异常不影响比价上报本身(rec 已 commit),只 log;邀请人补偿靠后续对账。
|
||||
if rec.status == "success":
|
||||
try:
|
||||
reward = crud_invite.try_reward_on_compare(db, user.id)
|
||||
if reward.status == "granted":
|
||||
logger.info(
|
||||
"invite compare reward granted inviter=%s invitee=%s cents=%s",
|
||||
reward.inviter_user_id, user.id, reward.reward_cents,
|
||||
)
|
||||
except Exception as e: # noqa: BLE001 best-effort,发奖失败不阻塞上报
|
||||
logger.warning("invite compare reward failed invitee=%s: %s", user.id, e)
|
||||
# 注:邀请发奖已从"比价成功"挪到"实际下单"(见 api/v1/order.py report_order)——
|
||||
# 冰拍板:被邀请人完成比价并实际下单才算邀请成功,仅完成比价不再发奖。
|
||||
logger.info(
|
||||
"compare record user=%s trace=%s biz=%s status=%s saved=%s (llm_calls backfill queued)",
|
||||
user.id,
|
||||
|
||||
@@ -147,6 +147,32 @@ def clear_fake_invitees(user: CurrentUser, db: DbSession) -> dict:
|
||||
return {"status": "ok", "removed": n}
|
||||
|
||||
|
||||
@router.post("/dev-reset", summary="[DEV] 把固定测试号打回出厂态,便于反复当新用户测发奖(仅非生产)")
|
||||
def dev_reset(db: DbSession) -> dict:
|
||||
"""把 TEST_ACCOUNT_PHONE 这个固定测试号清成"从没被邀请/没下过单"的新用户态。
|
||||
|
||||
⚠️ 无需鉴权(它只按配置里的测试号操作,不依赖登录态,方便测试脚本第一步直接调)。
|
||||
双重门控:生产环境(is_prod)404;测试号总开关(TEST_ACCOUNT_PHONE)没配置 → 400。
|
||||
故它永远只能动那一个配置好的测试号,动不了任意真实用户。
|
||||
|
||||
清什么见 repositories/invite.dev_reset_test_account(全清 + 刷 created_at 骗过 72h 闸)。
|
||||
调完该测试号即可再次绑定 + 下单上报,重新触发一次邀请发奖。
|
||||
"""
|
||||
from fastapi import HTTPException
|
||||
|
||||
if settings.is_prod:
|
||||
raise HTTPException(status_code=404, detail="Not Found")
|
||||
phone = settings.test_account_phone
|
||||
if not phone:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="测试号未启用:请在 .env 配 TEST_ACCOUNT_PHONE=11111111111 并重启服务",
|
||||
)
|
||||
cleared = invite_repo.dev_reset_test_account(db, phone)
|
||||
logger.info("invite dev-reset phone=%s cleared=%s", phone, cleared)
|
||||
return {"status": "ok", "phone": phone, "cleared": cleared}
|
||||
|
||||
|
||||
@router.post(
|
||||
"/landing-track",
|
||||
response_model=LandingTrackOut,
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.deps import CurrentUser, DbSession
|
||||
from app.repositories import invite as crud_invite
|
||||
from app.repositories import savings as crud_savings
|
||||
from app.schemas.order import OrderReportOut, OrderReportRequest
|
||||
|
||||
logger = logging.getLogger("shagua.order")
|
||||
|
||||
router = APIRouter(prefix="/api/v1/order", tags=["order"])
|
||||
|
||||
|
||||
@@ -15,6 +20,20 @@ router = APIRouter(prefix="/api/v1/order", tags=["order"])
|
||||
def report_order(req: OrderReportRequest, user: CurrentUser, db: DbSession) -> OrderReportOut:
|
||||
# 记账唯一真相表是 savings_record(source='compare')。
|
||||
rec, duplicated = crud_savings.create_from_report(db, user.id, req)
|
||||
# 邀请 v2 发奖:被邀请人【实际下单】(而非仅完成比价)才给邀请人发邀请奖励金(幂等,只发一次)。
|
||||
# 触发点从"比价成功上报"挪到这里(冰:完成比价并实际下单才算成功)。仅首次真实上报触发,
|
||||
# 重复上报(duplicated)跳过。best-effort:发奖异常不影响订单上报本身(rec 已 commit),只 log;
|
||||
# try_reward_on_compare 自带 compare_reward_granted 幂等闸,漏发靠后续对账补。
|
||||
if not duplicated:
|
||||
try:
|
||||
reward = crud_invite.try_reward_on_compare(db, user.id)
|
||||
if reward.status == "granted":
|
||||
logger.info(
|
||||
"invite order reward granted inviter=%s invitee=%s cents=%s",
|
||||
reward.inviter_user_id, user.id, reward.reward_cents,
|
||||
)
|
||||
except Exception as e: # noqa: BLE001 best-effort,发奖失败不阻塞订单上报
|
||||
logger.warning("invite order reward failed invitee=%s: %s", user.id, e)
|
||||
return OrderReportOut(
|
||||
id=rec.id,
|
||||
platform=rec.platform or req.platform,
|
||||
|
||||
@@ -407,6 +407,83 @@ def clear_fake_invitees(db: Session, inviter_id: int) -> int:
|
||||
return n
|
||||
|
||||
|
||||
def dev_reset_test_account(db: Session, phone: str) -> dict:
|
||||
"""[DEV] 把固定测试号打回"从没被邀请/没下过单"的出厂态,以便反复当新用户测发奖。
|
||||
|
||||
清理范围(全清,冰 2026-07-02 拍板):该 user 的
|
||||
- invite_relation(作为被邀请人的那条 = 解 already_bound;作为邀请人的那些)
|
||||
- invite_cash_transaction / coin_account 三个余额归零(invite_cash / coin / cash)
|
||||
- savings_record(= 解 order/report 的 duplicated 幂等)
|
||||
- invite_fingerprint(作为邀请人落的指纹)
|
||||
- coin_transaction / cash_transaction / withdraw_order(彻底出厂)
|
||||
并把 user.created_at 刷成 now(UTC)—— 骗过 bind 的"新用户 72h 窗口"闸(_is_new_user),
|
||||
否则该号第一次跑之后 created_at 固定、过 72h 即使清了关系也会 not_eligible。
|
||||
|
||||
仅供 dev-reset 端点调用(端点已 is_prod→404 + 只认配置的测试号)。返回各表清理条数。
|
||||
user 不存在(还没登录过)→ 返回全 0,不报错(下一步登录会新建,天然新号)。
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from app.models.invite_fingerprint import InviteFingerprint
|
||||
from app.models.savings import SavingsRecord
|
||||
from app.models.wallet import (
|
||||
CashTransaction,
|
||||
CoinAccount,
|
||||
CoinTransaction,
|
||||
InviteCashTransaction,
|
||||
WithdrawOrder,
|
||||
)
|
||||
|
||||
user = db.execute(select(User).where(User.phone == phone)).scalar_one_or_none()
|
||||
if user is None:
|
||||
return {"user_found": False}
|
||||
uid = user.id
|
||||
|
||||
def _del(model, whereclause) -> int:
|
||||
rows = db.execute(select(model).where(whereclause)).scalars().all()
|
||||
for r in rows:
|
||||
db.delete(r)
|
||||
return len(rows)
|
||||
|
||||
cleared = {
|
||||
"user_found": True,
|
||||
"user_id": uid,
|
||||
"invite_relation_as_invitee": _del(
|
||||
InviteRelation, InviteRelation.invitee_user_id == uid
|
||||
),
|
||||
"invite_relation_as_inviter": _del(
|
||||
InviteRelation, InviteRelation.inviter_user_id == uid
|
||||
),
|
||||
"invite_cash_transaction": _del(
|
||||
InviteCashTransaction, InviteCashTransaction.user_id == uid
|
||||
),
|
||||
"coin_transaction": _del(CoinTransaction, CoinTransaction.user_id == uid),
|
||||
"cash_transaction": _del(CashTransaction, CashTransaction.user_id == uid),
|
||||
"withdraw_order": _del(WithdrawOrder, WithdrawOrder.user_id == uid),
|
||||
"invite_fingerprint": _del(
|
||||
InviteFingerprint, InviteFingerprint.inviter_user_id == uid
|
||||
),
|
||||
"savings_record": _del(SavingsRecord, SavingsRecord.user_id == uid),
|
||||
}
|
||||
|
||||
# 余额账户归零(保留行,只清三个余额;没有账户行则跳过)
|
||||
acc = db.get(CoinAccount, uid)
|
||||
if acc is not None:
|
||||
acc.invite_cash_balance_cents = 0
|
||||
acc.coin_balance = 0
|
||||
acc.cash_balance_cents = 0
|
||||
cleared["coin_account_zeroed"] = True
|
||||
else:
|
||||
cleared["coin_account_zeroed"] = False
|
||||
|
||||
# 刷 created_at 骗过 72h 新用户闸(存 naive UTC,与登录新建 user 的写法一致)
|
||||
user.created_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
cleared["created_at_refreshed"] = True
|
||||
|
||||
db.commit()
|
||||
return cleared
|
||||
|
||||
|
||||
# ===== 指纹归因(剪贴板兜底,见 [[invite-three-tasks]] 任务 3)=====
|
||||
|
||||
def record_fingerprint(
|
||||
|
||||
Reference in New Issue
Block a user