WeChat's DataCube API (getarticletotaldetail) retains per-article engagement
statistics for approximately 180 days from each article's publish date.
Once an article ages past that window, WeChat purges its metrics — they cannot
be recovered via the API (errcode 61501 is returned for any date query on
expired articles).
Without automated syncing, any article published more than 6 months ago before you first ran a manual sync will have no retrievable stats. The auto-sync solves this by running daily and pulling every article's metrics before they expire.
- At the configured hour each day, a background asyncio task wakes up.
- It computes the sync window:
[today − window_days, today − 2 days].- The 2-day offset accounts for WeChat's 1–2 day processing lag.
- The default 170-day window leaves a 10-day buffer before the 180-day expiry.
- It calls
getarticletotaldetailfor each day in that range across all configured WeChat accounts. - Results are upserted into
media_postsandmedia_post_metrics_daily. Re-running is safe — existing rows are updated with the latest values. - Each run is recorded in
media_sync_runs(status, posts/metrics counts, timestamps, error messages).
All settings are environment variables (also readable from .env):
| Variable | Default | Description |
|---|---|---|
WECHAT_AUTO_SYNC_ENABLED |
false |
Set to true to enable the scheduler |
WECHAT_AUTO_SYNC_WINDOW_DAYS |
170 |
Days of history to cover per run |
WECHAT_AUTO_SYNC_HOUR |
3 |
Hour of day (0–23) to run, in APP_TIMEZONE |
APP_TIMEZONE |
Asia/Shanghai |
IANA timezone for scheduling |
WECHAT_AUTO_SYNC_ENABLED=trueThe scheduler uses the same WECHAT_APP_ID_N / WECHAT_APP_SECRET_N
credentials as the manual sync — see the numbered-account block in
.env.example and Architecture → Configuration reference.
On a freshly deployed server with no historical data, run a one-time backfill via the admin UI (Media → WeChat Sync) with:
- Start date: today − 170 days
- End date: today − 2 days
After that, the daily scheduler maintains coverage going forward.
Missing one daily run is harmless — the next run covers the same date range. Missing several weeks is also recoverable as long as articles are still within the 180-day window. The risk is only if the scheduler is disabled for more than ~10 days (the safety buffer), at which point the oldest articles in the window begin losing data.
Every daily run sends exactly one WeCom notification — success or failure, never both — covering every configured account:
- All accounts synced successfully: a summary message with per-account
posts/metrics counts, unless
WECOM_NOTIFY_SUCCESSis set tofalse(see collector.md, shared with the creator-portal collector). - Any account failed: an alert listing each failure, followed by a section for any accounts that did sync successfully in the same run.
- No accounts configured at all: no notification (nothing to report; this is the existing "skip the run" case, unchanged).
Sent via the same WeCom self-built app used by the collector's alerts (see "Alerting" in collector.md) — no separate credential to manage.
A daily health-watchdog check (also in app/scheduler.py, see "Pipeline
health watchdog" in collector.md) separately catches the
case where this loop stops running altogether, since a per-run notification
only fires when a run actually happens.
Check the media_sync_runs table for recent run history:
SELECT account_id, status, start_date, end_date,
posts_upserted, metrics_upserted, error_message, finished_at
FROM media_sync_runs
ORDER BY finished_at DESC
LIMIT 20;Failed runs appear with status = 'failed' and an error_message. Common
causes:
| Error | Cause | Fix |
|---|---|---|
40001 invalid credential |
Access token expired mid-sync | Credentials rotate automatically; transient — usually resolves on next run |
61501 |
Date is outside the retention window or data not yet available | Normal for very recent dates; the 2-day lag offset should prevent this |
40164 not whitelisted |
Server IP not in WeChat IP whitelist | Add the server's outbound IP to the WeChat Official Account platform |
The scheduler lives in app/scheduler.py and is started as an asyncio task
in the FastAPI app lifespan (app/main.py). It imports the sync logic
directly from app/views/media/routes.py (_sync_one_wechat_account,
_ensure_env_wechat_accounts) rather than going through HTTP, so no
self-calling is needed.
The task is cancelled cleanly when the application shuts down (SIGTERM /
uvicorn reload) because asyncio.CancelledError is propagated rather than
swallowed.