142 lines
7.1 KiB
Python
142 lines
7.1 KiB
Python
"""直接触发「消息通知中心」真实推送链路,给指定用户(默认 11111111111)的已注册设备发 push。
|
||
|
||
用于**后台无法驱动**的事件联调(本环境:wxpay 未配 → 提现成功打不通、提现单唯一约束 →
|
||
造不了多张待审单、好友下单后台无入口)。本脚本直接调 services/notification_events 的真实
|
||
下发函数,走的就是生产同一条链路:落 notification 表(站内消息) + 厂商直推(honor/huawei/
|
||
xiaomi/oppo/vivo)到该用户 device_liveness 里已注册的 push token。
|
||
|
||
默认只发这 3 类(后台驱动不了的):
|
||
#3 withdraw_success 提现到账
|
||
#4 withdraw_failed 提现失败,款项已退回
|
||
#12 invite_order_reward 好友下单奖励到账
|
||
可用 --types 指定;--types all 追加后台能驱动的 #9/#10/#11(注意:这几类的点击跳转 id 是假的,
|
||
仅验证「推送到达手机」,真实跳转请走后台审核流程)。
|
||
|
||
.venv\\Scripts\\python.exe scripts\\fire_push_events.py # 3 类各 10 条
|
||
.venv\\Scripts\\python.exe scripts\\fire_push_events.py --count 1 # 各 1 条(先小量验证通道)
|
||
.venv\\Scripts\\python.exe scripts\\fire_push_events.py --types withdraw_failed --count 3
|
||
.venv\\Scripts\\python.exe scripts\\fire_push_events.py --types all --count 2
|
||
|
||
推送成败看输出里的 `shagua.vendor_push` 日志(push sent / push failed);2 台设备则每条各推 2 次。
|
||
凭据缺失或 token 失效时 notification_events 只记日志、不抛错(站内消息仍会落库)。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import logging
|
||
import random
|
||
import sys
|
||
import uuid
|
||
|
||
from app.core.rewards import INVITE_COMPARE_REWARD_CENTS, PRICE_REPORT_REWARD_COINS
|
||
from app.db.session import SessionLocal, engine
|
||
from app.models.feedback import Feedback
|
||
from app.models.price_report import PriceReport
|
||
from app.models.wallet import WithdrawOrder
|
||
from app.repositories import device as device_repo
|
||
from app.repositories import user as user_repo
|
||
from app.services import notification_events
|
||
|
||
# SQL 回显静音;shagua.* 开到 INFO,好看到「push sent / push failed」结果
|
||
# dev 下 APP_DEBUG=true → engine echo=True,echo 走 InstanceLogger 直写、无视 logger level,只能关 echo 本身
|
||
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
|
||
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
|
||
engine.echo = False
|
||
|
||
if hasattr(sys.stdout, "reconfigure"):
|
||
sys.stdout.reconfigure(encoding="utf-8")
|
||
|
||
DEFAULT_PHONE = "11111111111"
|
||
DEFAULT_TYPES = ["withdraw_success", "withdraw_failed", "invite_order_reward"]
|
||
ADMIN_DRIVEN = ["feedback_reward", "feedback_reply", "report_approved"] # --types all 追加
|
||
ALL_TYPES = DEFAULT_TYPES + ADMIN_DRIVEN
|
||
|
||
_FAIL_REASONS = [
|
||
"微信零钱未实名,款项已退回",
|
||
"收款账户异常,款项已退回",
|
||
"超出微信零钱收款限额,款项已退回",
|
||
]
|
||
|
||
|
||
def _fire_one(db, uid: int, type_key: str, i: int) -> None:
|
||
"""构造一条该类型的瞬态业务对象(不落业务表,只为给 notify 函数读字段),触发真实推送。"""
|
||
if type_key == "withdraw_success":
|
||
order = WithdrawOrder(user_id=uid, out_bill_no=uuid.uuid4().hex, amount_cents=50, source="coin_cash")
|
||
notification_events.notify_withdraw_success(db, order)
|
||
elif type_key == "withdraw_failed":
|
||
order = WithdrawOrder(
|
||
user_id=uid, out_bill_no=uuid.uuid4().hex, amount_cents=350, source="coin_cash",
|
||
fail_reason=random.choice(_FAIL_REASONS),
|
||
)
|
||
notification_events.notify_withdraw_failed(db, order)
|
||
elif type_key == "invite_order_reward":
|
||
# 假被邀请人 id(> 真实用户范围,避重):昵称回退「好友」。真实昵称请走 API 流程(见文末说明)。
|
||
fake_invitee = random.randint(900000, 999999)
|
||
notification_events.notify_invite_order_reward(
|
||
db, inviter_user_id=uid, invitee_user_id=fake_invitee, cash_cents=INVITE_COMPARE_REWARD_CENTS
|
||
)
|
||
elif type_key == "feedback_reward":
|
||
fb = Feedback(user_id=uid, content="(直发)", contact="", status="adopted",
|
||
reward_coins=300, admin_reply="感谢反馈,您说的问题已修复上线,金币请查收~")
|
||
fb.id = random.randint(900000, 999999)
|
||
notification_events.notify_feedback_reward(db, fb)
|
||
elif type_key == "feedback_reply":
|
||
fb = Feedback(user_id=uid, content="(直发)", contact="", status="rejected",
|
||
admin_reply="您的建议我们记录啦,会在后续版本评估~")
|
||
fb.id = random.randint(900000, 999999)
|
||
notification_events.notify_feedback_reply(db, fb)
|
||
elif type_key == "report_approved":
|
||
rep = PriceReport(
|
||
user_id=uid, reported_platform_id="jd", reported_platform_name="京东外卖",
|
||
reported_price_cents=8800, images=[], status="approved",
|
||
reward_coins=PRICE_REPORT_REWARD_COINS, store_name=f"测试火锅店{i:02d}",
|
||
)
|
||
rep.id = random.randint(900000, 999999)
|
||
notification_events.notify_report_approved(db, rep)
|
||
else:
|
||
raise SystemExit(f"未知类型: {type_key}(可选: {', '.join(ALL_TYPES)})")
|
||
|
||
|
||
def main() -> None:
|
||
parser = argparse.ArgumentParser(description="直接触发消息通知中心真实推送(后台驱动不了的事件用)")
|
||
parser.add_argument("--phone", default=DEFAULT_PHONE, help=f"目标用户手机号(默认 {DEFAULT_PHONE})")
|
||
parser.add_argument("--count", type=int, default=10, help="每类发多少条(默认 10)")
|
||
parser.add_argument(
|
||
"--types", default=",".join(DEFAULT_TYPES),
|
||
help=f"逗号分隔的类型;'all' = {', '.join(ALL_TYPES)}。默认 {', '.join(DEFAULT_TYPES)}",
|
||
)
|
||
args = parser.parse_args()
|
||
|
||
types = ALL_TYPES if args.types.strip() == "all" else [t.strip() for t in args.types.split(",") if t.strip()]
|
||
bad = [t for t in types if t not in ALL_TYPES]
|
||
if bad:
|
||
print(f"❌ 未知类型: {', '.join(bad)}(可选: {', '.join(ALL_TYPES)})")
|
||
return
|
||
|
||
db = SessionLocal()
|
||
try:
|
||
user = user_repo.get_user_by_phone(db, args.phone)
|
||
if user is None:
|
||
print(f"❌ 用户 {args.phone} 不存在。请先用该手机号在 App 登录一次再跑本脚本。")
|
||
return
|
||
uid = user.id
|
||
|
||
targets = device_repo.list_push_targets(db, user_id=uid)
|
||
print(f"目标用户 {args.phone}(id={uid});已注册推送设备 {len(targets)} 台:"
|
||
f"{[t.push_vendor for t in targets] or '无(手机收不到!先在 App 上报 push token)'}")
|
||
print(f"即将触发:{types},每类 {args.count} 条 → 共 {len(types) * args.count} 条\n")
|
||
|
||
for t in types:
|
||
print(f"── {t} ×{args.count} " + "─" * 30)
|
||
for i in range(1, args.count + 1):
|
||
_fire_one(db, uid, t, i)
|
||
|
||
print(f"\n✅ 已触发完。站内消息已落 notification 表(用 {args.phone} 登录 App 可在消息中心看到);"
|
||
"\n 手机推送成败见上方 `shagua.vendor_push` 日志(push sent=成功 / push failed=失败)。")
|
||
finally:
|
||
db.close()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|