当前位置:首页>python>架构演进:从Python到Node.js——公众号中转站的技术选型之路

架构演进:从Python到Node.js——公众号中转站的技术选型之路

  • 2026-10-11 05:44:49
架构演进:从Python到Node.js——公众号中转站的技术选型之路

架构演进:从Python到Node.js——公众号中转站的技术选型之路

工程人的智库 · 技术实战系列(二)

回顾:第一版Python中转站能做什么

上一期我们讲了为什么需要"中转站"。简单回顾:本地电脑IP不在微信公众号白名单里,需要通过一台固定IP的服务器做代理转发。

第一版我用Python写的 wx_server.py 实现了基本功能:

  • 监听8899端口
  • 自动获取和缓存access_token
  • 转发JSON格式的API请求

能正常工作的接口:

  • 获取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版的处理方式是先把整个请求体读进内存,解析出各个字段,再重新组装转发。这种方式有两个问题:

  1. 内存占用大:一张5MB的图片就要占5MB内存
  2. 容易出错: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路径区分:

  • /wxapi/* → 转发微信API
  • / → 返回Web页面
  • /api/publish → 发布文章

这种设计在功能简单时没问题,但功能多了以后路由逻辑越来越复杂,很容易出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发布脚本)调用的,要求高性能、零延迟
  • 8899是给人用的Web界面,要求易用、直观

分开以后,两边可以独立升级,互不影响。

Token管理的改进

Python版:

  • 使用 GET /cgi-bin/token 获取token
  • token缓存在内存变量里
  • 服务器重启后token丢失,需要重新获取

Node.js版:

  • 使用 POST /cgi-bin/stable_token 获取token(这个改进是第三期踩坑的核心,先卖个关子)
  • token持久化到 tokens.json 文件
  • 服务器重启后直接从文件读取缓存token
  • 提前3分钟(200秒)预刷新,避免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错误排查全记录 待发布
四 运维实战:自动化发布完整方案 待发布

最新文章

随机文章