Files
wxserver/MAINTENANCE.md
T
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

244 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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: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 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 → 自动部署 → 开发者工具上传新版 → 设体验版
### 场景 11DB 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=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.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)做端到端验证,不能只测闰月初一。
14. **`lunarToSolar` 的 BASE 时间戳必须用 UTC**。早期版用 `new Date(1900, 0, 31)`(本地时间)作为基准,在 1900 年早期日期上某些 V8 版本有时区偏差,让每个农历月初对应公历都少 1 天(系统性误差,不容易发现,只在测「正常日期」时才暴露——闰月那次只测了初一,恰好正确)。改用 `Date.UTC(1900, 0, 31)` 算时间戳,再用 `getUTCFullYear/Month/Date` 提取后重新构造本地 Date 对象。**测算法务必跑「公历→农历→公历」互逆**,发现不互逆就是有 bug。
## 与其他记忆的关系
- 群晖 CI/CD 通用规范:见 [[reference_synology_cicd_playbook]]
- 软路由(frpc 所在)基本信息:见 [[project_home_router]]
- Docker 镜像拉取代理优先级:见 [[feedback_docker_mirror]]
- 项目当前架构和接口设计:见 [[project_birthday]]