feat(invite): 发奖改「比价并下单」+ 好友 is_compared 状态 + DEV 造数/重置接口 #113
@@ -21,16 +21,15 @@ from app.api.deps import CurrentUser, DbSession
|
|||||||
from app.db.session import SessionLocal
|
from app.db.session import SessionLocal
|
||||||
from app.models.comparison import ComparisonRecord
|
from app.models.comparison import ComparisonRecord
|
||||||
from app.repositories import comparison as crud_compare
|
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 (
|
from app.schemas.compare_record import (
|
||||||
CompareStatsOut,
|
CompareStatsOut,
|
||||||
ComparisonRecordCreatedOut,
|
ComparisonRecordCreatedOut,
|
||||||
ComparisonRecordDetailOut,
|
ComparisonRecordDetailOut,
|
||||||
ComparisonRecordIn,
|
ComparisonRecordIn,
|
||||||
ComparisonRecordPage,
|
|
||||||
ComparisonRecordOut,
|
ComparisonRecordOut,
|
||||||
|
ComparisonRecordPage,
|
||||||
)
|
)
|
||||||
|
from app.services.pricebot_llm_calls import fetch_llm_calls
|
||||||
|
|
||||||
logger = logging.getLogger("shagua.compare_record")
|
logger = logging.getLogger("shagua.compare_record")
|
||||||
|
|
||||||
@@ -53,18 +52,8 @@ def report_record(
|
|||||||
# 任务做,不阻塞上报响应(顺带给 pricebot 落盘留足余量)。upsert 已 commit,后台用
|
# 任务做,不阻塞上报响应(顺带给 pricebot 落盘留足余量)。upsert 已 commit,后台用
|
||||||
# 独立 session 按 record id 回填 llm_calls + 派生 llm_call_count/retry_count。
|
# 独立 session 按 record id 回填 llm_calls + 派生 llm_call_count/retry_count。
|
||||||
background_tasks.add_task(_backfill_llm_calls, rec.id, rec.trace_id)
|
background_tasks.add_task(_backfill_llm_calls, rec.id, rec.trace_id)
|
||||||
# 邀请 v2 发奖:被邀请人完成一次【成功】比价 → 给邀请人发邀请奖励金(幂等,只发一次)。
|
# 注:邀请发奖已从"比价成功"挪到"实际下单"(见 api/v1/order.py report_order)——
|
||||||
# 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)
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"compare record user=%s trace=%s biz=%s status=%s saved=%s (llm_calls backfill queued)",
|
"compare record user=%s trace=%s biz=%s status=%s saved=%s (llm_calls backfill queued)",
|
||||||
user.id,
|
user.id,
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from app.api.deps import CurrentUser, DbSession
|
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.repositories import savings as crud_savings
|
||||||
from app.schemas.order import OrderReportOut, OrderReportRequest
|
from app.schemas.order import OrderReportOut, OrderReportRequest
|
||||||
|
|
||||||
|
logger = logging.getLogger("shagua.order")
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/v1/order", tags=["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:
|
def report_order(req: OrderReportRequest, user: CurrentUser, db: DbSession) -> OrderReportOut:
|
||||||
# 记账唯一真相表是 savings_record(source='compare')。
|
# 记账唯一真相表是 savings_record(source='compare')。
|
||||||
rec, duplicated = crud_savings.create_from_report(db, user.id, req)
|
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(
|
return OrderReportOut(
|
||||||
id=rec.id,
|
id=rec.id,
|
||||||
platform=rec.platform or req.platform,
|
platform=rec.platform or req.platform,
|
||||||
|
|||||||
@@ -304,6 +304,9 @@ def get_invitees(
|
|||||||
"avatar_url": u.avatar_url or u.wechat_avatar_url or None,
|
"avatar_url": u.avatar_url or u.wechat_avatar_url or None,
|
||||||
"coins": rel.inviter_coin, # v3 起恒 0(邀请人收益改走邀请奖励金)
|
"coins": rel.inviter_coin, # v3 起恒 0(邀请人收益改走邀请奖励金)
|
||||||
"invited_at": rel.created_at,
|
"invited_at": rel.created_at,
|
||||||
|
# 是否已完成过一次比价(= 已发过邀请奖励金)。客户端据此:好友列表分"去提醒/邀请成功"、
|
||||||
|
# 在途列表只取未比价(is_compared=False)、算在途好友数与在途收益(未比价数×2元)。
|
||||||
|
"is_compared": bool(rel.compare_reward_granted),
|
||||||
})
|
})
|
||||||
has_more = offset + len(rows) < int(total)
|
has_more = offset + len(rows) < int(total)
|
||||||
return items, int(total), has_more
|
return items, int(total), has_more
|
||||||
|
|||||||
@@ -296,6 +296,9 @@ def daily_auto_exchange(db: Session) -> dict:
|
|||||||
- **逐用户独立事务**:单个用户异常 rollback 不影响其他人;exchange_coins_to_cash 内部按用户 commit。
|
- **逐用户独立事务**:单个用户异常 rollback 不影响其他人;exchange_coins_to_cash 内部按用户 commit。
|
||||||
返回统计 dict(scanned/converted/skipped_done/skipped_dust/failed/total_cents)。
|
返回统计 dict(scanned/converted/skipped_done/skipped_dust/failed/total_cents)。
|
||||||
"""
|
"""
|
||||||
|
# ⚠️「今天」写死北京时(rewards.cn_today() = datetime.now(CN_TZ).date(), CN_TZ=+8),与服务器/用户
|
||||||
|
# 时区无关(用户 2026-07-01 硬约束:兑换一律北京 0 点日切)。**禁止**改成 date.today() / 裸
|
||||||
|
# datetime.now().date()——那会跟随进程本地时区,服务器非 CST 时会在错误的"天"兑/重复兑。
|
||||||
today = rewards.cn_today()
|
today = rewards.cn_today()
|
||||||
stats = {
|
stats = {
|
||||||
"scanned": 0, "converted": 0, "skipped_done": 0,
|
"scanned": 0, "converted": 0, "skipped_done": 0,
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ class InviteeItem(BaseModel):
|
|||||||
avatar_url: str | None = None # 头像 URL;null = 前端画默认色块
|
avatar_url: str | None = None # 头像 URL;null = 前端画默认色块
|
||||||
coins: int # 这次邀请给我(邀请人)发的金币
|
coins: int # 这次邀请给我(邀请人)发的金币
|
||||||
invited_at: datetime # 邀请绑定时间(前端转"今天/3天前")
|
invited_at: datetime # 邀请绑定时间(前端转"今天/3天前")
|
||||||
|
is_compared: bool = False # 该好友是否已完成过一次比价(好友列表分"去提醒/邀请成功";在途列表只取 False)
|
||||||
|
|
||||||
|
|
||||||
class InviteeListOut(BaseModel):
|
class InviteeListOut(BaseModel):
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ Type=oneshot
|
|||||||
User=root
|
User=root
|
||||||
WorkingDirectory=/opt/shaguabijia-app-server
|
WorkingDirectory=/opt/shaguabijia-app-server
|
||||||
Environment="PATH=/opt/shaguabijia-app-server/.venv/bin:/usr/bin:/bin"
|
Environment="PATH=/opt/shaguabijia-app-server/.venv/bin:/usr/bin:/bin"
|
||||||
|
# 写死北京时:兑换的"当天/0 点"一律按北京时,不随服务器 OS 时区漂(用户 2026-07-01 硬约束)。
|
||||||
|
# 脚本内的日期判断本就走 rewards.cn_today()(CN_TZ=+8),这里再把进程 TZ 也钉成北京,双保险。
|
||||||
|
Environment="TZ=Asia/Shanghai"
|
||||||
EnvironmentFile=/opt/shaguabijia-app-server/.env
|
EnvironmentFile=/opt/shaguabijia-app-server/.env
|
||||||
ExecStart=/opt/shaguabijia-app-server/.venv/bin/python -m scripts.daily_auto_exchange --once
|
ExecStart=/opt/shaguabijia-app-server/.venv/bin/python -m scripts.daily_auto_exchange --once
|
||||||
SyslogIdentifier=daily-exchange
|
SyslogIdentifier=daily-exchange
|
||||||
|
|||||||
@@ -4,8 +4,10 @@
|
|||||||
Description=Run daily auto-exchange coins->cash at midnight
|
Description=Run daily auto-exchange coins->cash at midnight
|
||||||
|
|
||||||
[Timer]
|
[Timer]
|
||||||
# 每天 0 点跑。客户端文案已注明「可能存在延迟」,可按需改 00:05 错开整点扎堆。
|
# 每天【北京时】0 点跑,写死时区(用户 2026-07-01 硬约束:兑换一律北京 0 点,不随服务器本地时区漂)。
|
||||||
OnCalendar=*-*-* 00:00:00
|
# systemd OnCalendar 支持尾缀时区;不写时区会按服务器 OS 本地时区触发 → 服务器非 CST 时会在错误时刻兑。
|
||||||
|
# 客户端文案已注明「可能存在延迟」,可按需改 00:05 错开整点扎堆。
|
||||||
|
OnCalendar=*-*-* 00:00:00 Asia/Shanghai
|
||||||
# 服务器宕机/重启后,补跑错过的那一轮(而不是干等次日)。
|
# 服务器宕机/重启后,补跑错过的那一轮(而不是干等次日)。
|
||||||
Persistent=true
|
Persistent=true
|
||||||
AccuracySec=1min
|
AccuracySec=1min
|
||||||
|
|||||||
Reference in New Issue
Block a user