基于 main 接入各厂商直推服务端 (#118)

改动:新增厂商推送配置、设备 push_vendor/push_token 字段、device push-test 接口、心跳超时厂商直推发送逻辑和对应测试。

验证:python -m pytest tests/test_device_push.py tests/test_auth.py tests/test_health.py 通过。

---------

Co-authored-by: guke <guke@wonderable.ai>
Co-authored-by: 左辰勇 <exinglang@gmail.com>
Co-authored-by: lowmaster-chen <1119780489@qq.com>
Reviewed-on: #118
Co-authored-by: Ghost <>
Co-committed-by: Ghost <>
This commit was merged in pull request #118.
This commit is contained in:
Ghost
2026-07-22 15:42:25 +08:00
committed by guke
parent 2eb36b44c8
commit 0717c09721
47 changed files with 5497 additions and 32 deletions
+135
View File
@@ -0,0 +1,135 @@
"""给指定用户(默认 11111111111)造「后台可驱动」的推送联调数据。
覆盖能从**管理后台点一下就触发手机推送**的 3 类事件,每类 10 条待审记录:
事件(PRD #) 后台动作 造的数据
────────────────────────────────────────────────────────────────────
#10 反馈奖励 反馈工单 → 采纳(填回复留言 + 金币) 10 条 pending feedback(标「请采纳」)
#9 官方回复 反馈工单 → 拒绝(填未采纳原因/留言) 10 条 pending feedback(标「请拒绝」)
#11 爆料审核通过 上报更低价 → 通过 10 条 pending price_report
触发链路:admin 审核 → 发金币/改状态 → services/notification_events 落站内消息 + 厂商直推
→ 该用户已注册设备(device_liveness)收到 push。
其余 3 类(#3 提现成功 / #4 提现失败 / #12 好友下单到账)后台无法在本环境驱动
(wxpay 未配 / 提现单唯一约束 / 后台无入口),用 scripts/fire_push_events.py 直接触发。
幂等:每次先删掉本脚本上一轮造的记录(按内容标记 [PUSH测试] 识别,不动用户真实反馈/爆料),再重建。
.venv\\Scripts\\python.exe scripts\\seed_push_admin_test.py
.venv\\Scripts\\python.exe scripts\\seed_push_admin_test.py --phone 11111111111 --count 10
.venv\\Scripts\\python.exe scripts\\seed_push_admin_test.py --clean-only
"""
from __future__ import annotations
import argparse
import logging
import sys
from sqlalchemy import delete, select
from app.db.session import SessionLocal
from app.models.feedback import Feedback
from app.models.price_report import PriceReport
from app.repositories import user as user_repo
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING) # 静音 SQL 回显,输出更干净
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
DEFAULT_PHONE = "11111111111"
MARK = "[PUSH测试]" # 本脚本造的数据统一带此标记,幂等清理按它识别(不误删真实数据)
def clean(db, uid: int) -> tuple[int, int]:
"""删掉本脚本上一轮造的带标记记录(任何状态都删,彻底重置)。返回 (删反馈数, 删爆料数)。"""
fb_ids = list(db.execute(
select(Feedback.id).where(Feedback.user_id == uid, Feedback.content.like(f"{MARK}%"))
).scalars())
rep_ids = list(db.execute(
select(PriceReport.id).where(
PriceReport.user_id == uid, PriceReport.store_name.like(f"{MARK}%")
)
).scalars())
if fb_ids:
db.execute(delete(Feedback).where(Feedback.id.in_(fb_ids)))
if rep_ids:
db.execute(delete(PriceReport).where(PriceReport.id.in_(rep_ids)))
db.commit()
return len(fb_ids), len(rep_ids)
def seed(db, uid: int, count: int) -> None:
# #10 反馈奖励:采纳这些 → 手机收「反馈奖励已到账」。采纳时记得在后台填「给用户的回复留言」
# (PRD 要求发奖必带官方留言),否则通知里不带留言行。
for i in range(1, count + 1):
db.add(Feedback(
user_id=uid,
content=f"{MARK} 请【采纳】我 → 触发 #10 反馈奖励推送。测试反馈内容 {i:02d}:比价页能加个历史记录就好了。",
contact="",
source="profile",
status="pending",
))
# #9 官方回复:拒绝这些 → 手机收「您的反馈有回复啦」。拒绝时填「未采纳原因」+「回复留言」。
for i in range(1, count + 1):
db.add(Feedback(
user_id=uid,
content=f"{MARK} 请【拒绝】我 → 触发 #9 官方回复推送。测试反馈内容 {i:02d}:希望支持某某小众平台比价。",
contact="",
source="comparison",
scene="other",
status="pending",
))
# #11 爆料审核通过:通过这些 → 手机收「爆料审核通过」(发固定金币)。
for i in range(1, count + 1):
db.add(PriceReport(
user_id=uid,
comparison_record_id=None,
store_name=f"{MARK}测试火锅店{i:02d}",
dish_summary="招牌套餐 × 1",
original_platform_id="meituan-waimai",
original_platform_name="美团外卖",
original_price_cents=9900,
reported_platform_id="jd-waimai",
reported_platform_name="京东外卖",
reported_price_cents=8800,
images=[],
status="pending",
))
db.commit()
def main() -> None:
parser = argparse.ArgumentParser(description="造后台可驱动的推送联调数据(反馈×2 + 爆料)")
parser.add_argument("--phone", default=DEFAULT_PHONE, help=f"目标用户手机号(默认 {DEFAULT_PHONE})")
parser.add_argument("--count", type=int, default=10, help="每类造多少条(默认 10)")
parser.add_argument("--clean-only", action="store_true", help="只清理本脚本造的数据,不重建")
args = parser.parse_args()
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
nf, nr = clean(db, uid)
print(f"🧹 已清理上一轮 [PUSH测试] 数据:反馈 {nf} 条、爆料 {nr}")
if args.clean_only:
print("✅ 仅清理,已完成。")
return
seed(db, uid, args.count)
print(f"\n✅ 已为 {args.phone}(id={uid})造好后台联调数据(每类 {args.count} 条):")
print(f" • 反馈工单「请采纳」× {args.count} → 后台【采纳】(填回复留言+金币)→ 手机收 #10 反馈奖励")
print(f" • 反馈工单「请拒绝」× {args.count} → 后台【拒绝】(填未采纳原因/留言)→ 手机收 #9 官方回复")
print(f" • 上报更低价 × {args.count} → 后台【通过】→ 手机收 #11 爆料审核通过")
print("\n👉 打开管理后台(:8771)对应列表即可看到这些待审记录,逐条审核就会推到手机。")
print(" #3/#4/#12 本环境后台驱动不了,用:.venv\\Scripts\\python.exe scripts\\fire_push_events.py")
finally:
db.close()
if __name__ == "__main__":
main()