feat: 实现微信公众号新闻和视频同步服务

- 使用 draft API 同步文章(适配个人订阅号)
- 使用 material API 同步视频(含详情获取)
- 自动建表(videos)、UPSERT 已有 articles 表
- 同步删除:微信端删除的素材自动从数据库移除
- APScheduler 定时调度,支持 --once 手动触发
- Docker + docker-compose 部署配置

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Asukadaisiki
2026-04-11 13:08:49 +08:00
parent 68724843e5
commit 34a6c6d67e
11 changed files with 833 additions and 1 deletions

175
README.md
View File

@@ -1,3 +1,176 @@
# UpdateDB
定时爬取公众号的视频与文章数据,更新到数据库
定时从微信公众号平台获取视频与文章数据,同步到远端 PostgreSQL 数据库
## 技术栈
- Python 3.11
- httpx (HTTP 客户端)
- psycopg2-binary (PostgreSQL 驱动)
- APScheduler (定时调度)
- Docker 部署
## 项目结构
```
UpdateDB/
main.py # 入口:启动调度器,支持 --once 手动触发
config.py # 加载 .env 配置
wechat.py # 微信 API 客户端token 管理、素材获取)
db.py # 数据库连接、建表、UPSERT
sync.py # 同步编排逻辑
Dockerfile
docker-compose.yml
.env_example # 环境变量模板
api_docs/ # 微信 API 文档
```
## 数据库变更总结
### 新建表:`videos`
程序启动时自动创建(`CREATE TABLE IF NOT EXISTS`**不会影响已有表**。
```sql
CREATE TABLE IF NOT EXISTS videos (
id SERIAL PRIMARY KEY, -- 自增主键
media_id VARCHAR(128) UNIQUE, -- 微信素材 ID唯一约束
name TEXT, -- 视频文件名(来自列表接口)
url TEXT, -- 视频播放 URL来自列表接口
title TEXT, -- 视频标题(来自详情接口)
description TEXT, -- 视频描述(来自详情接口)
down_url TEXT, -- 视频下载地址(来自详情接口,可能过期)
wechat_update_time TIMESTAMP, -- 微信端更新时间
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
```
**后端读取时注意**
- `url``down_url` 都可能过期失效,建议每次使用前检查可用性,或定期触发同步刷新
- `title``description` 可能为 NULL首次同步时详情接口调用失败
- `url` 是播放地址,`down_url` 是下载地址,两者不同
### 未修改的表
#### `articles`(原有表,未改结构)
程序往这些**已有字段**写入数据,未新增/修改任何列:
| 数据库字段 | 写入内容 | 微信 API 来源 | 注意事项 |
|---|---|---|---|
| `title` | 文章标题 | `news_item.title` | 截断到 200 字符 |
| `content` | 文章 HTML 内容 | `news_item.content` | 原始 HTML微信已去除 JS |
| `cover_url` | 封面图 URL | `news_item.thumb_url` | 截断到 500 字符,可能为 NULL |
| `author` | 作者 | `news_item.author` | 截断到 100 字符,可能为 NULL |
| `publish_date` | 发布日期 | `item.update_time` 转 date | 仅日期,不含时分秒 |
| `source_url` | 文章原文链接 | `news_item.url` | 截断到 1000 字符,可能为 NULL |
| `wechat_article_id` | 去重标识 | `{media_id}_{index}` | 格式见下方说明 |
**未写入的字段**(保持原有值/NULL`category``tags`(微信 API 无此数据)
#### `sync_states`(原有表,未改结构)
使用已有的 key-value 结构记录同步状态:
| key | value 示例 |
|---|---|
| `wechat_news_sync` | `{"status":"idle","count":42,"last_sync":"2026-04-09T..."}` |
| `wechat_video_sync` | `{"status":"idle","count":5,"last_sync":"2026-04-09T..."}` |
value 是 JSON 字符串,后端可用 `json.loads()` 解析。
### 同步删除机制
每次同步完成后,程序会对比微信端和数据库的 `media_id`
- 数据库中有、微信端没有的记录 → **自动删除**
- 涉及 `articles``videos` 两张表
- articles 的删除依据:`wechat_article_id` 中提取的 `media_id`(按最后一个 `_` 分割)
### `wechat_article_id` 格式说明
```
格式:{media_id}_{article_index}
示例abc123def456_0 (单图文)
abc123def456_1 (多图文中的第 2 篇)
```
- `media_id`:微信素材 ID一个 media_id 对应一条图文素材
- `article_index`:图文内的文章序号(从 0 开始),多图文时会有 0、1、2...
- 后端查询某篇具体文章:`WHERE wechat_article_id = '{media_id}_{idx}'`
- 后端查询某条图文素材的所有文章:`WHERE wechat_article_id LIKE '{media_id}_%'`
### 数据写入策略
- **articles**`SELECT` 检查 `wechat_article_id` 是否存在 → 存在则 `UPDATE`,不存在则 `INSERT`
- **videos**`INSERT ... ON CONFLICT (media_id) DO UPDATE`,仅在 `wechat_update_time` 更新时覆盖
- **sync_states**`INSERT ... ON CONFLICT (key) DO UPDATE`,每次同步更新状态
## 快速开始
### 本地开发
1. 安装依赖:
```bash
pip install -e .
```
2. 复制配置文件并填写:
```bash
cp .env_example .env
# 编辑 .env填入 app_secret 和数据库信息
```
3. 运行:
```bash
# 启动定时同步(默认每 48 小时)
python main.py
# 只运行一次
python main.py --once
```
### Docker 部署
1. 在 VPS 上克隆仓库,创建 `.env` 文件:
```bash
cp .env_example .env
# 编辑 .env
```
2. 构建并启动:
```bash
docker compose up -d --build
```
3. 查看日志:
```bash
docker compose logs -f
```
## 环境变量
| 变量 | 必填 | 说明 |
|------|------|------|
| `app_id` | 是 | 微信公众号 AppID |
| `app_secret` | 是 | 微信公众号 AppSecret |
| `db_host` | 是 | PostgreSQL 主机 |
| `db_port` | 是 | PostgreSQL 端口 |
| `db_name` | 是 | 数据库名 |
| `db_user` | 是 | 数据库用户 |
| `db_pw` | 是 | 数据库密码 |
| `sync_interval_hours` | 否 | 同步间隔(小时),默认 48 |
## 同步逻辑
1. 调用微信稳定版 Token 接口获取 access_token自动缓存和刷新
2. 分页获取所有 news图文和 video视频素材
3. 对视频素材,额外调用详情接口获取下载地址
4. 使用 UPSERT 写入数据库INSERT ON CONFLICT DO UPDATE
5. news 和 video 独立同步,一个失败不影响另一个