原算法在月循环外的 if (offset < 0) 分支根据 isLeap 重新判断加哪个月份天数, 但闰月期间的非初一日期会因为变量切换被错算到下一个普通月。 用 jjonline/calendar.js 的权威实现替换:循环内统一 offset -= temp, 退出循环后用保留的 temp 加回,简洁且正确。 修复验证: - 2023-03-22 → 闰二月初一 ✓(之前也对) - 2023-03-23 → 闰二月初二 ✓(之前错为「三月初二」) - 2023-04-19 → 闰二月廿九 ✓(之前错为「三月廿九」) - 2025-08-22 → 闰六月廿九 ✓(之前错为「七月廿九」) 维护手册新增踩坑 #13。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
13 KiB
name, description, metadata
| name | description | metadata | ||||||
|---|---|---|---|---|---|---|---|---|
| 生日提醒小程序自建后端的完整运维 playbook:维护场景操作步骤、故障排查表、关键资源清单和踩坑记录 |
|
项目速查表
| 资源 | 值 |
|---|---|
| 小程序前端 | /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 子域名 | wxserver,type=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:8080,Host 透传
frps (vhost_http_port: 8080;公开 vhost_https_port: 4430 但本项目不用)
↓ frp 隧道(subdomain=wxserver)
frpc (软路由 192.168.1.119,UCI `/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:3000,trial/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— 微信 API(access_token 缓存、code2session、sendSubscribeMessage)server/src/reminder.js— node-cron 定时任务 + 推送逻辑
部署
.gitea/workflows/deploy.yml— Gitea Actions 自动构建并部署server/Dockerfile— Node 20 + apt 装 python3/make/g++ + npm installserver/docker-compose.yml— 本地开发用,生产走 Gitea Actions 直接 docker run
配置
server/.env.example— 配置模板(已入库)server/.env— 本地真实配置(已 gitignore)- Gitea Secrets:
WX_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:改环境变量 / 凭据
- 浏览器登
https://git.ymxixi.space/adminym/wxserver/settings/actions/secrets - 编辑对应密钥(如
WX_APPSECRET) - 进 Actions 页面找到最近一次 workflow → 右上角 Re-run 重跑
- 不要靠 push 触发——光改 Secrets 不会触发新流水线
场景 5:从体验版上线到正式版
- 开发者工具点上传 → 选「版本升级」(或对应 semver)→ 上传
- mp.weixin.qq.com → 管理 → 版本管理 → 提交审核
- 审核通过后点「发布」
- 关键:去 Gitea Secrets 把
WX_MINIPROGRAM_STATE改成formal,re-run workflow (不改的话推送跳转的是开发版小程序,体验差)
场景 6:拉新人加体验
- mp.weixin.qq.com → 管理 → 成员管理 → 体验成员 → 添加(最多 15 人,填对方微信号)
- 把体验版二维码图发给对方,让他扫码
场景 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:扩展新接口
server/src/index.js加新的app.post('/api/...', ...)路由- 数据库变动改
server/src/db.js(注意 SQLiteCREATE TABLE IF NOT EXISTS不能增列,需手动 ALTER 或重建) - 前端
utils/api.js加调用方法 - push → 自动部署 → 开发者工具上传新版 → 设体验版
场景 11:DB Schema 升级
SQLite 重启不会自动改表结构。如果加字段:
- 临时操作:进容器
docker exec -it birthday-server sqlite3 /app/data/birthday.db,ALTER 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=http,IP=127.0.0.1,Port=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.isLeapMonth(0/1),保存时由 solarToLunar(...).isLeap 决定。
算法实现:lunar.lunarToSolar(year, month, day, isLeap) 在 isLeap=true 但当年无对应闰月时,会自动按普通月份返回——所以代码上无需显式 fallback。
UI 提示:用户在添加纪念日页面勾选「农历」+ 选择公历日期后,会实时显示「对应农历:闰X月X日」并标黄警示,说明无闰月年按普通月份提醒。
如未来需要改为 B 方案(按下一个月初过)或 C 方案(跳过不提醒),改 reminder.js 的 getThisYearDate 即可,无需动数据。
已知"踩过的坑"(避免重蹈)
docker compose restart不会重读.env— 必须up -d --force-recreate。光 restart 会保留旧环境变量。- Gitea Runner 容器内
mkdir创建的是 runner 容器内的路径,不是群晖宿主路径。在宿主上建持久化目录要走docker run -v 宿主路径:/x <镜像> mkdir -p /x/子目录,让 docker daemon 用宿主路径处理 bind mount。 - frp
type=https是 TLS 透传,要求本地服务自己监听 HTTPS 并带证书。HTTP 后端用type=http,配合 frps 的vhost_http_port,由 frps 按Hostheader 路由。 - NPM 反代默认配置容易写成「自己的公网 IP + frps 公开端口」(比如 47.238.152.174:4430)。正确写法是
127.0.0.1:8080,让流量在本机经 frps vhost_http_port 进入隧道。 - frpc 配置
/var/etc/frpc.ini是临时文件,是从/etc/config/frpc(UCI)启动时生成的。直接改临时文件下次重启就丢,必须改 UCI(LuCI 界面或uci set)。 - 微信小程序的「服务器域名」修改有次数限制(个人小程序约 5 次/月),改前最好确认无误。
- 微信小程序的开发版/体验版/正式版共享同一份本地
wx.setStorageSync(同一台手机、同一 AppID)。看到"莫名其妙的旧数据"先怀疑这个。 miniprogram_state三个值:developer只能推到开发版,trial推体验版,formal推正式版。配错就收不到。- Docker build 阶段不会自动用宿主代理。代理走 docker-compose
build.args传HTTP_PROXY/HTTPS_PROXY(保留 build-arg,不会写入运行时 ENV)。容器内访问宿主用host.docker.internal。 - Gitea Actions 的 Secrets 改完不会自动触发流水线——必须手动 re-run,或者推一个新 commit。
- 农历
lunarInfo数据表来源要可信。早期版本数据有错位(多个年份的闰月信息错),导致 solarToLunar 算的春节日期跟实际差 1-2 个月。已替换为权威源github.com/jjonline/calendar.js的版本。如未来要扩展年份范围(>2100),务必同样从该源取。 - 农历存字段必须含
isLeapMonth。只存lunarMonth/lunarDay会丢闰月信息,闰月生日的人每年被错误提醒到普通月份。修复需同时改:数据库列、后端 FIELDS、前端 formData、lunar.lunarToSolar第 4 参数透传。 solarToLunar月循环必须保留temp变量。早期版本在闰月分支里offset -= leapDays、非闰分支offset -= monthDays,循环外的if (offset < 0)又根据 isLeap 重新判断要加哪个——闰月期间的"非初一日期"会被错算到下一个普通月(例如「闰二月初二」算成「三月初二」)。权威实现(jjonline/calendar.js)让循环内统一offset -= temp并把temp保留到循环外用,是更稳的写法。修改算法时一定要用 闰月连续日期(如 2023-03-22 到 2023-04-19)做端到端验证,不能只测闰月初一。
与其他记忆的关系
- 群晖 CI/CD 通用规范:见 reference_synology_cicd_playbook
- 软路由(frpc 所在)基本信息:见 project_home_router
- Docker 镜像拉取代理优先级:见 feedback_docker_mirror
- 项目当前架构和接口设计:见 project_birthday