架构演进:从Python到Node.js——公众号中转站的技术选型之路
工程人的智库 · 技术实战系列(二)
回顾:第一版Python中转站能做什么
上一期我们讲了为什么需要"中转站"。简单回顾:本地电脑IP不在微信公众号白名单里,需要通过一台固定IP的服务器做代理转发。
第一版我用Python写的 wx_server.py 实现了基本功能:
能正常工作的接口:
- 获取token(GET
/cgi-bin/token) - 发布草稿(POST
/cgi-bin/draft/add) - 群发(POST
/cgi-bin/freepublish/submit)
看起来似乎够了?但实际使用中很快暴露了问题。
Python版的三大硬伤
硬伤一:文件上传直接报废
微信公众号需要上传封面图、素材图片,这些接口用的是 multipart/form-data 格式:
POST /cgi-bin/media/uploadimg HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
------WebKitFormBoundary
Content-Disposition: form-data; name="media"; filename="cover.jpg"
Content-Type: image/jpeg
(二进制图片数据...)
------WebKitFormBoundary--
Python版的处理方式是先把整个请求体读进内存,解析出各个字段,再重新组装转发。这种方式有两个问题:
- 容易出错:multipart的边界符、编码方式稍有不对就400
硬伤二:请求"全量缓存"再转发
Python版的核心逻辑是:
# 伪代码
body = request.read() # 把整个请求体读完
new_body = inject_token(body) # 注入token
response = requests.post(target_url, data=new_body) # 重新发送
return response.content # 把整个响应体读完再返回
这意味着无论请求多大,都要先完整缓存。如果上传一个视频素材(几十MB),服务器的内存直接飙上去。
硬伤三:单端口混用
API代理和Web管理界面挤在同一个端口(8899),靠URL路径区分:
这种设计在功能简单时没问题,但功能多了以后路由逻辑越来越复杂,很容易出bug。
Node.js方案:流式转发架构
核心理念:零拷贝代理
Node.js方案的核心改进是流式转发(pipe)——不缓存请求体,直接用管道把数据从客户端"流"到目标服务器:
// 伪代码
const proxyReq = https.request(targetOptions);
request.pipe(proxyReq); // 直接管道转发,不缓存
proxyReq.pipe(response); // 响应也管道返回
这样做的好处:
- 内存占用恒定:不管上传多大的文件,内存占用都是固定的一小块缓冲区
- 速度快:不需要等整个请求体收完,收到数据就立即转发
- 天然兼容multipart:不解析请求体,原样透传,multipart格式完全不会出错
双端口分离架构
:8898 (API透明代理) :8899 (Web管理界面)
├── /wxapi/* ├── GET / → Web页面
│ ↓ 自动注入token ├── POST /api/publish → 发布文章
│ ↓ 流式转发到微信API └── 状态查看、日志记录
└── 支持所有HTTP方法
为什么分开?因为两者的职责完全不同:
- 8898是给
publish.py(Python发布脚本)调用的,要求高性能、零延迟
分开以后,两边可以独立升级,互不影响。
Token管理的改进
Python版:
- 使用
GET /cgi-bin/token 获取token
Node.js版:
- 使用
POST /cgi-bin/stable_token 获取token(这个改进是第三期踩坑的核心,先卖个关子)
// Token缓存逻辑
if (entry && entry.expires_at > Date.now() + 200000) {
return entry.token; // 缓存有效,直接返回
}
// 缓存快过期或不存在,重新获取
请求转发的细节
转发微信API请求时,有几个关键细节:
1. Query String保留
原始请求可能带有参数(比如 ?lang=zh_CN),转发时必须保留,同时注入 access_token:
const query = { ...(parsed.query || {}) };
delete query.access_token; // 移除旧的(如果有)
query.access_token = token; // 注入新的
const targetPath = pathname + '?' + new URLSearchParams(query).toString();
2. Headers精确透传
不能把所有headers都转发(有些是代理层面的),需要选择性透传:
const keep = ['content-type', 'user-agent', 'accept', 'accept-encoding'];
// Content-Length必须精确透传,multipart格式依赖它
if (contentLength) {
headers['content-length'] = String(contentLength);
}
3. Token缓存缺失时的处理
如果缓存没有有效token,不能直接pipe(因为pipe是不回头的),需要先把请求体缓存,获取token后再转发:
if (!token) {
const chunks = [];
req.on('data', chunk => chunks.push(chunk));
req.on('end', () => {
fetchAccessToken(key).then(newToken => {
const buffered = Buffer.concat(chunks);
// 有了token,重新构建请求发送
});
});
return; // 等待异步获取token
}
// 有缓存token,直接pipe,零延迟
多账号支持
运营3个公众号,每个有不同的AppID。中转站通过URL中的 account 参数或请求头区分:
const ACCOUNTS = {
default: { name: '工程人的智库', appid: 'wx...', appsecret: '...' },
helper: { name: '工程人的助手', appid: 'wx...', appsecret: '...' },
everyday:{ name: '工程人的每一天', appid: 'wx...', appsecret: '...' },
};
每个账号的token独立缓存、独立刷新,互不干扰。
部署架构
本地电脑 腾讯云VPS
┌───────────────┐ ┌──────────────────────────┐
│ │ │ │
│ publish.py │ HTTP(S) │ wx_proxy.js │
│ (wenyan-cli) │─────────────→ │ ├─ :8898 API代理 │
│ │ │ └─ :8899 Web管理 │
│ 浏览器 │─────────────→ │ │
│ │ │ tokens.json (token缓存) │
└───────────────┘ │ publish_log.json (日志) │
└──────────────────────────┘
│
HTTPS 出口IP
119.29.168.239
(在白名单里)
│
▼
api.weixin.qq.com
技术栈对比
| 对比项 |
Python版 |
Node.js版 |
| 语言 |
Python 3 |
Node.js 20 |
| 代理方式 |
全量缓存再转发 |
流式pipe |
| 内存占用 |
随请求大小增长 |
恒定(几MB) |
| multipart支持 |
需要解析重组 |
原样透传 |
| 端口 |
单端口(8899) |
双端口(8898+8899) |
| Token存储 |
内存变量 |
文件持久化 |
| 获取Token方式 |
GET /cgi-bin/token |
POST /cgi-bin/stable_token |
| 多账号 |
支持 |
支持 |
| Web管理 |
简单页面 |
完整管理界面 |
下一期预告
Node.js版中转站上线后,大部分接口都正常工作了。但有一个诡异的现象:
freepublish接口正常返回,draft/add接口始终报40001。
同一个代理,同一个token,为什么有的接口能用,有的不行?
起初我怀疑是代理转发了query string的问题,做了大量对比测试。但最终真相出人意料——问题根本不在代理上,而在微信API的一个"隐藏规则"。
下一期,我会完整复盘这个排查过程。
本系列持续更新,关注「工程人的智库」获取最新文章。
系列目录
| 篇序 |
标题 |
状态 |
| 一 |
为什么公众号需要"中转站"? |
已发布 |
| 二 |
架构演进:从Python到Node.js |
本篇 |
| 三 |
踩坑实录:40001错误排查全记录 |
待发布 |
| 四 |
运维实战:自动化发布完整方案 |
待发布 |