Files
wxserver/MAINTENANCE.md
yuming 756d57d818
部署到群晖 / deploy (push) Successful in 44s
简化纪念日类型:去掉"农历生日"类型,公历/农历改由 isLunar 字段决定
- typeList 从 5 项简化到 4 项:生日 / 结婚纪念日 / 订婚纪念日 / 其他纪念日
- TYPE_NAMES / TYPE_ICONS 中 lunar_birthday 保留兼容映射(也映射到「生日」+ 🎂),
  让线上历史数据自然回显,无需数据库迁移(方案 A)
- getTypeIndex('lunar_birthday') = 0,老数据编辑时正确回显「生日」
- index.js 列表筛选和 wxml 图标判断本来已含 lunar_birthday 兼容,无需改

老数据自然淘汰:用户重新保存时新数据写 type='birthday',老数据 type 保留
直到下次编辑保存才升级。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 07:30:37 +08:00

14 KiB
Raw Permalink Blame History

name, description, metadata
name description metadata
生日提醒小程序自建后端的完整运维 playbook:维护场景操作步骤、故障排查表、关键资源清单和踩坑记录
node_type type originSessionId
memory reference b9f9c436-1656-4989-aa1d-6b9f009fd1f2

项目速查表

资源
小程序前端 /Users/gaotu/WebstormProjects/myself/生日提醒小程序
后端代码 同上目录下 server/
Gitea 仓库 https://git.ymxixi.space/adminym/wxserver.git
对外 HTTPS https://wxserver.ymxixi.space
群晖内网 http://192.168.1.66:15002
小程序 AppID wxe72882e2072d141a
数据库文件 群晖 /volume1/docker/apps/birthday-server/data/birthday.db
Docker 容器名 birthday-server
FRP 公网入口 47.238.152.174(阿里云香港)
frpc 子域名 wxservertype=http,软路由 192.168.1.119
NPM 反代配置 NPM 容器 /data/nginx/proxy_host/33.conf
订阅消息模板 ID 6J7Stt-lu7DKU6jblJ0nZGq_D81z5glnksf7qWfy5Yw

链路图(一图流)

微信小程序 (envVersion 切换 BASE_URL)
  ↓ HTTPS 443
wxserver.ymxixi.space (DNS A 记录 → 47.238.152.174)
  ↓
Nginx Proxy Manager (Docker 容器 nginx-proxy-manager-zh-app-1,监听 80/443/81)
  ↓ proxy_pass http://127.0.0.1:8080Host 透传
frps (vhost_http_port: 8080;公开 vhost_https_port: 4430 但本项目不用)
  ↓ frp 隧道(subdomain=wxserver
frpc (软路由 192.168.1.119UCI `/etc/config/frpc`)
  ↓ TCP 转发到 192.168.1.66:15002
群晖 Docker 容器 birthday-server (Node 20 + Express + SQLite + node-cron)
  ↓
/volume1/docker/apps/birthday-server/data/birthday.db

关键文件清单

前端

  • app.js — 启动初始化 + 调 /api/login 拿 openid,加缓存复用
  • utils/api.js — 请求封装;BASE_URL 自动切换develop → localhost:3000trial/release → 线上 HTTPS
  • pages/add-anniversary/add-anniversary.js — 添加纪念日时调 syncToCloud

后端

  • server/src/index.js — Express 入口 + 4 个接口(/api/login、/api/anniversary、/api/reminder/run、/api/health
  • server/src/db.js — SQLite 表结构(anniversaries / remind_logs
  • server/src/wx.js — 微信 APIaccess_token 缓存、code2session、sendSubscribeMessage
  • server/src/reminder.js — node-cron 定时任务 + 推送逻辑

部署

  • .gitea/workflows/deploy.yml — Gitea Actions 自动构建并部署
  • server/Dockerfile — Node 20 + apt 装 python3/make/g++ + npm install
  • server/docker-compose.yml — 本地开发用,生产走 Gitea Actions 直接 docker run

配置

  • server/.env.example — 配置模板(已入库)
  • server/.env — 本地真实配置(已 gitignore)
  • Gitea SecretsWX_APPID / WX_APPSECRET / WX_MINIPROGRAM_STATE(可选 WX_TEMPLATE_ID / REMINDER_CRON

维护场景 Playbook

场景 1:改后端代码(最常见)

# 在本地改 server/ 下文件
git add server/...
git commit -m "..."
git push origin master

Gitea Actions 会自动构建并部署(约 3-8 分钟)。验证:

curl https://wxserver.ymxixi.space/api/health

后端无需审核,push 即生效。关键纪律:新接口字段要保持向后兼容(默认值 / 可选字段),否则线上用户的旧版小程序会崩。

场景 2:迭代小程序前端(页面 / 样式 / 交互改动)

1. 本地改 pages/ utils/ app.* 等
2. 开发者工具点「编译」预览,自测
3. 右上角「上传」→ 版本号递增(如 2.0.1 → 2.0.2)
   - 改 bug:选「修订补丁」
   - 加小功能:选「特性更新」
   - 重大变更:选「版本升级」
4. mp.weixin.qq.com → 管理 → 版本管理 → 找到刚上传的「开发版本」
5. 两条路:
   a) 点「体验版」→ 自己/体验成员扫码先试,没问题再回来走 b
   b) 点「提交审核」→ 审核通过(1-2 天)→ 点「发布」正式上线
6. 用户重新打开小程序自动拉新代码(强制更新可能要 24h)

注意:微信对小程序代码强制审核,没有"热替换跳过审核"的办法。紧急修线上 bug 可用「加急审核」(账号每年 2 次额度)。

场景 3:前端 + 后端一起改

为避免线上旧前端遇到新后端接口出错,严格按这个顺序

① 改后端 → git push → CI 部署 → 验证 /api/health
② 验证后端对旧版本前端仍兼容(关键,否则线上炸锅)
③ 再改前端 → 上传 → 审核 → 发布

后端无审核延迟,可以随时回滚;前端审核+发布有延迟。所以"后端先行 + 向后兼容"是铁律。

场景 4:改环境变量 / 凭据

  1. 浏览器登 https://git.ymxixi.space/adminym/wxserver/settings/actions/secrets
  2. 编辑对应密钥(如 WX_APPSECRET
  3. 进 Actions 页面找到最近一次 workflow → 右上角 Re-run 重跑
  4. 不要靠 push 触发——光改 Secrets 不会触发新流水线

场景 5:从体验版上线到正式版

  1. 开发者工具点上传 → 选「版本升级」(或对应 semver)→ 上传
  2. mp.weixin.qq.com → 管理 → 版本管理 → 提交审核
  3. 审核通过后点「发布」
  4. 关键:去 Gitea Secrets 把 WX_MINIPROGRAM_STATE 改成 formalre-run workflow (不改的话推送跳转的是开发版小程序,体验差)

场景 6:拉新人加体验

  1. mp.weixin.qq.com → 管理 → 成员管理 → 体验成员 → 添加(最多 15 人,填对方微信号)
  2. 把体验版二维码图发给对方,让他扫码

场景 7:备份数据库

ssh 群晖
cp /volume1/docker/apps/birthday-server/data/birthday.db \
   /volume1/backup/birthday-$(date +%F).db

或从 Mac 拉到本地:

scp 群晖:/volume1/docker/apps/birthday-server/data/birthday.db ~/Downloads/

场景 8:看后端日志

ssh 群晖
docker logs -f birthday-server          # 实时
docker logs --tail 200 birthday-server  # 最近 200 行

场景 9:本地起后端调试

cd server
cp .env.example .env
# 编辑 .env 填 AppID/AppSecret
docker compose up -d
docker logs -f birthday-server

开发者工具勾选「不校验合法域名」,BASE_URL 会自动走 localhost:3000。

场景 10:扩展新接口

  1. server/src/index.js 加新的 app.post('/api/...', ...) 路由
  2. 数据库变动改 server/src/db.js(注意 SQLite CREATE TABLE IF NOT EXISTS 不能增列,需手动 ALTER 或重建)
  3. 前端 utils/api.js 加调用方法
  4. push → 自动部署 → 开发者工具上传新版 → 设体验版

场景 11DB Schema 升级

SQLite 重启不会自动改表结构。如果加字段:

  • 临时操作:进容器 docker exec -it birthday-server sqlite3 /app/data/birthday.dbALTER TABLE anniversaries ADD COLUMN xxx
  • 永久方案:在 db.js 启动逻辑里加迁移 SQL,参考 PRAGMA user_version 做版本号管理

场景 12:回滚到旧版本

Gitea Actions 每次 push 都会构建新镜像但 tag 都是 :latest,旧镜像在群晖 Docker 里残留但没标签。最稳的回滚:

git revert <提交哈希>
git push

让 CI 重新构建旧代码版本,比 docker run 旧镜像 ID 干净。

故障排查表

症状 可能原因 排查命令 / 处理
https://wxserver.ymxixi.space/api/health 502 NPM 反代目标配错 进 NPM Web UI 检查:Forward Scheme=httpIP=127.0.0.1Port=8080
502 但 NPM 配置看着对 nginx 没传 Host header / frps 找不到 vhost NPM 默认会传,自建 nginx 要 proxy_set_header Host $host
TLS 报 unrecognized name nginx 上没有该 server_name NPM 里新加 Proxy Host 或修改 server_name
frpc 改了配置但不生效 改的是 /var/etc/frpc.ini(临时) 通过 LuCI / /etc/config/frpc 改 UCI 配置,再 /etc/init.d/frpc restart
Actions 卡「等待中」不跑 Runner 离线 Container Manager 重启 gitea-runner 容器
构建镜像超时 / 拉不下来 docker.io 国际网络慢 重试,或群晖 Container Manager 加镜像加速
改了 .env 但容器没生效 docker compose restart 不重读 .env docker compose up -d --force-recreate 或重建容器
部署成功但 docker run 启动 1s 失败 bind mount 目录不存在 通过 docker run --rm -v /宿主路径:/x <image> mkdir -p /x/... 在宿主上建
微信小程序请求失败「不在合法域名列表」 服务器域名没配 / 改后没生效 mp.weixin.qq.com → 开发管理 → 服务器域名添加并扫码确认
体验版能收推送但点击跳错版本 WX_MINIPROGRAM_STATE 与发布状态不匹配 developer/trial/formal 对应小程序版本,改 Secrets re-run
用户看到旧数据 小程序本地 storage 共享 wx.clearStorageSync() 或重装小程序
code2session 报 invalid appid (40013) AppID/AppSecret 不匹配 或 .env 没生效 容器内 printenv WX_APPID 检查;改 Secrets 需重建容器
订阅消息发送返回 errcode≠0 用户未授权 / 模板 ID 错 / 配额耗尽 查 docker logs;用户每次添加才有一次订阅机会

产品决策记录(避免反复纠结)

闰月生日的提醒策略(采用 A 方案)

农历闰月(如闰二月、闰六月)只在某些年份存在。当用户的农历生日落在闰月(例如「闰二月初一」),遇到当年没有该闰月的年份,按对应普通月份的同日提醒(即「二月初一」)。这是民间最常见的过法。

数据字段anniversaries.isLeapMonth0/1),保存时由 solarToLunar(...).isLeap 决定。 算法实现lunar.lunarToSolar(year, month, day, isLeap) 在 isLeap=true 但当年无对应闰月时,会自动按普通月份返回——所以代码上无需显式 fallback。 UI 提示:用户在添加纪念日页面勾选「农历」+ 选择公历日期后,会实时显示「对应农历:闰X月X日」并标黄警示,说明无闰月年按普通月份提醒。

如未来需要改为 B 方案(按下一个月初过)或 C 方案(跳过不提醒),改 reminder.jsgetThisYearDate 即可,无需动数据。

已知"踩过的坑"(避免重蹈)

  1. docker compose restart 不会重读 .env — 必须 up -d --force-recreate。光 restart 会保留旧环境变量。
  2. Gitea Runner 容器内 mkdir 创建的是 runner 容器内的路径,不是群晖宿主路径。在宿主上建持久化目录要走 docker run -v 宿主路径:/x <镜像> mkdir -p /x/子目录,让 docker daemon 用宿主路径处理 bind mount。
  3. frp type=https 是 TLS 透传,要求本地服务自己监听 HTTPS 并带证书。HTTP 后端用 type=http,配合 frps 的 vhost_http_port,由 frps 按 Host header 路由。
  4. NPM 反代默认配置容易写成「自己的公网 IP + frps 公开端口」(比如 47.238.152.174:4430)。正确写法是 127.0.0.1:8080,让流量在本机经 frps vhost_http_port 进入隧道。
  5. frpc 配置 /var/etc/frpc.ini 是临时文件,是从 /etc/config/frpc(UCI)启动时生成的。直接改临时文件下次重启就丢,必须改 UCI(LuCI 界面或 uci set)。
  6. 微信小程序的「服务器域名」修改有次数限制(个人小程序约 5 次/月),改前最好确认无误。
  7. 微信小程序的开发版/体验版/正式版共享同一份本地 wx.setStorageSync(同一台手机、同一 AppID)。看到"莫名其妙的旧数据"先怀疑这个。
  8. miniprogram_state 三个值developer 只能推到开发版,trial 推体验版,formal 推正式版。配错就收不到。
  9. Docker build 阶段不会自动用宿主代理。代理走 docker-compose build.argsHTTP_PROXY/HTTPS_PROXY(保留 build-arg,不会写入运行时 ENV)。容器内访问宿主用 host.docker.internal
  10. Gitea Actions 的 Secrets 改完不会自动触发流水线——必须手动 re-run,或者推一个新 commit。
  11. 农历 lunarInfo 数据表来源要可信。早期版本数据有错位(多个年份的闰月信息错),导致 solarToLunar 算的春节日期跟实际差 1-2 个月。已替换为权威源 github.com/jjonline/calendar.js 的版本。如未来要扩展年份范围(>2100),务必同样从该源取。
  12. 农历存字段必须含 isLeapMonth。只存 lunarMonth/lunarDay 会丢闰月信息,闰月生日的人每年被错误提醒到普通月份。修复需同时改:数据库列、后端 FIELDS、前端 formData、lunar.lunarToSolar 第 4 参数透传。
  13. solarToLunar 月循环必须保留 temp 变量。早期版本在闰月分支里 offset -= leapDays、非闰分支 offset -= monthDays,循环外的 if (offset < 0) 又根据 isLeap 重新判断要加哪个——闰月期间的"非初一日期"会被错算到下一个普通月(例如「闰二月初二」算成「三月初二」)。权威实现(jjonline/calendar.js)让循环内统一 offset -= temp 并把 temp 保留到循环外用,是更稳的写法。修改算法时一定要用 闰月连续日期(如 2023-03-22 到 2023-04-19)做端到端验证,不能只测闰月初一。
  14. lunarToSolar 的 BASE 时间戳必须用 UTC。早期版用 new Date(1900, 0, 31)(本地时间)作为基准,在 1900 年早期日期上某些 V8 版本有时区偏差,让每个农历月初对应公历都少 1 天(系统性误差,不容易发现,只在测「正常日期」时才暴露——闰月那次只测了初一,恰好正确)。改用 Date.UTC(1900, 0, 31) 算时间戳,再用 getUTCFullYear/Month/Date 提取后重新构造本地 Date 对象。测算法务必跑「公历→农历→公历」互逆,发现不互逆就是有 bug。

与其他记忆的关系