b7cfcf7495
## 需求 - 保留现有后台手动对账逻辑不变 - 每天北京时间 05:00 自动刷新 CPS 对账 - 当前仅处理美团和京东 - 每次按更新时间回拉近 3 天,覆盖延迟更新和订单状态变化 ## 实现 - 新增进程内 CPS 自动对账 worker,并接入应用生命周期 - 美团使用更新时间查询类型 2,京东使用更新时间查询类型 3 - 复用现有仓储层对账及 order_id 幂等更新逻辑 - 美团和京东独立会话、独立异常处理,单个平台失败不阻塞另一平台 - 增加单实例锁、开关、执行小时、回拉天数和轮询间隔配置 - 服务在 05:00 后重启时会补跑当天任务 ## 验证 - `pytest tests/test_cps_reconcile_worker.py tests/test_cps_admin.py tests/test_admin_read.py -q`:27 passed - 相关文件 Ruff 检查通过 - `git diff --check` 通过 --------- Co-authored-by: unknown <798648091@qq.com> Reviewed-on: #162 Co-authored-by: linkeyu <linkeyu@wonderable.ai> Co-committed-by: linkeyu <linkeyu@wonderable.ai>
85 lines
3.2 KiB
Python
85 lines
3.2 KiB
Python
"""美团、京东 CPS 每日自动对账 worker。"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, datetime, timedelta
|
|
|
|
from app.core import cps_reconcile_worker as worker
|
|
from app.core.config import settings
|
|
from app.core.rewards import CN_TZ
|
|
from app.integrations.meituan import MeituanCpsError
|
|
|
|
|
|
def _configure_platforms(monkeypatch) -> None:
|
|
monkeypatch.setattr(settings, "MT_CPS_APP_KEY", "mt-key")
|
|
monkeypatch.setattr(settings, "MT_CPS_APP_SECRET", "mt-secret")
|
|
monkeypatch.setattr(settings, "JD_UNION_APP_KEY", "jd-key")
|
|
monkeypatch.setattr(settings, "JD_UNION_APP_SECRET", "jd-secret")
|
|
monkeypatch.setattr(settings, "CPS_AUTO_RECONCILE_LOOKBACK_DAYS", 3)
|
|
|
|
|
|
def test_reconcile_once_pulls_meituan_and_jd_by_update_time(monkeypatch) -> None:
|
|
_configure_platforms(monkeypatch)
|
|
calls: dict[str, dict] = {}
|
|
|
|
def fake_meituan(db, **kwargs):
|
|
calls["meituan"] = kwargs
|
|
return {"fetched": 2, "inserted": 1, "updated": 1, "pages": 1}
|
|
|
|
def fake_jd(db, **kwargs):
|
|
calls["jd"] = kwargs
|
|
return {"fetched": 3, "inserted": 2, "updated": 1, "pages": 2}
|
|
|
|
monkeypatch.setattr(worker.cps_repo, "reconcile_orders", fake_meituan)
|
|
monkeypatch.setattr(worker.cps_repo, "reconcile_jd_orders", fake_jd)
|
|
monkeypatch.setattr(worker, "_touch_lock", lambda: None)
|
|
now = datetime(2026, 7, 22, 5, 0, tzinfo=CN_TZ)
|
|
|
|
result = worker._reconcile_once(now)
|
|
|
|
assert result["errors"] == {}
|
|
assert result["meituan"]["fetched"] == 2
|
|
assert result["jd"]["fetched"] == 3
|
|
assert calls["meituan"]["query_time_type"] == 2
|
|
assert calls["jd"]["query_time_type"] == 3
|
|
assert calls["meituan"]["end_time"] == int(now.timestamp())
|
|
assert calls["meituan"]["start_time"] == int((now - timedelta(days=3)).timestamp())
|
|
assert calls["jd"]["start_time"] == now - timedelta(days=3)
|
|
assert calls["jd"]["end_time"] == now
|
|
|
|
|
|
def test_meituan_failure_does_not_block_jd(monkeypatch) -> None:
|
|
_configure_platforms(monkeypatch)
|
|
jd_called = False
|
|
|
|
def fail_meituan(db, **kwargs):
|
|
raise MeituanCpsError("temporary failure")
|
|
|
|
def fake_jd(db, **kwargs):
|
|
nonlocal jd_called
|
|
jd_called = True
|
|
return {"fetched": 1, "inserted": 1, "updated": 0, "pages": 1}
|
|
|
|
monkeypatch.setattr(worker.cps_repo, "reconcile_orders", fail_meituan)
|
|
monkeypatch.setattr(worker.cps_repo, "reconcile_jd_orders", fake_jd)
|
|
monkeypatch.setattr(worker, "_touch_lock", lambda: None)
|
|
|
|
result = worker._reconcile_once(datetime(2026, 7, 22, 5, 0, tzinfo=CN_TZ))
|
|
|
|
assert result["errors"]["meituan"] == "temporary failure"
|
|
assert jd_called is True
|
|
assert result["jd"]["fetched"] == 1
|
|
|
|
|
|
def test_should_run_once_after_five_am() -> None:
|
|
before = datetime(2026, 7, 22, 4, 59, tzinfo=CN_TZ)
|
|
at_five = datetime(2026, 7, 22, 5, 0, tzinfo=CN_TZ)
|
|
|
|
assert worker._should_run(None, before, 5) is False
|
|
assert worker._should_run(None, at_five, 5) is True
|
|
assert worker._should_run(date(2026, 7, 22), at_five, 5) is False
|
|
|
|
|
|
def test_start_worker_respects_auto_switch(monkeypatch) -> None:
|
|
monkeypatch.setattr(settings, "CPS_AUTO_RECONCILE_ENABLED", False)
|
|
assert worker.start_cps_reconcile_worker() is None
|