--- name: "" description: 生日提醒小程序自建后端的完整运维 playbook:维护场景操作步骤、故障排查表、关键资源清单和踩坑记录 metadata: node_type: memory type: reference originSessionId: 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 子域名 | `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 install - `server/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:改环境变量 / 凭据 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` 改成 `formal`,re-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 → 自动部署 → 开发者工具上传新版 → 设体验版 ### 场景 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 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` 即可,无需动数据。 ## 已知"踩过的坑"(避免重蹈) 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.args` 传 `HTTP_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)做端到端验证,不能只测闰月初一。 ## 与其他记忆的关系 - 群晖 CI/CD 通用规范:见 [[reference_synology_cicd_playbook]] - 软路由(frpc 所在)基本信息:见 [[project_home_router]] - Docker 镜像拉取代理优先级:见 [[feedback_docker_mirror]] - 项目当前架构和接口设计:见 [[project_birthday]]