feat(0.2.1): 生产切换完成 —— 单端口上线 (3001 + 内网 PG/PowerSync), 迁移 MySQL 全量数据, 补 uuid 镜像触发器
Docker Build and Push / build-image (push) Successful in 2m14s

This commit is contained in:
jif2.zhang
2026-10-27 07:26:47 +08:00
parent 836035f4cd
commit a1c267b0d4
3 changed files with 221 additions and 3 deletions
+5
View File
@@ -67,3 +67,8 @@ deploy/0.2.1/.env
/deploy/prod/.env
/deploy/prod/data/
# 本地临时脚本 (含口令, 不入库)
scripts/_*.mjs
.dsh-ops/
+82
View File
@@ -0,0 +1,82 @@
-- iBoard 0.2.1: 外键 uuid 镜像列触发器
-- 由 0.2.1 验证库导出; 服务端写入时自动维护 projectUuid/columnUuid/parentUuid/taskUuid,
-- 同步流据此把镜像列以 projectId/columnId/parentId/taskId 的别名下发给客户端。
CREATE OR REPLACE FUNCTION public.func_boardcolumn_uuid_map()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
SELECT "uuid" INTO NEW."projectUuid" FROM "Project" WHERE "id" = NEW."projectId";
IF NOT FOUND THEN RAISE EXCEPTION 'Project % 不存在', NEW."projectId"; END IF;
RETURN NEW;
END;
$function$
;
CREATE OR REPLACE FUNCTION public.func_projectmember_uuid_map()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
SELECT "uuid" INTO NEW."projectUuid" FROM "Project" WHERE "id" = NEW."projectId";
IF NOT FOUND THEN RAISE EXCEPTION 'Project % 不存在', NEW."projectId"; END IF;
RETURN NEW;
END;
$function$
;
CREATE OR REPLACE FUNCTION public.func_task_uuid_map()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
SELECT "uuid" INTO NEW."projectUuid" FROM "Project" WHERE "id" = NEW."projectId";
IF NOT FOUND THEN RAISE EXCEPTION 'Project % 不存在', NEW."projectId"; END IF;
SELECT "uuid" INTO NEW."columnUuid" FROM "BoardColumn" WHERE "id" = NEW."columnId";
IF NOT FOUND THEN RAISE EXCEPTION 'BoardColumn % 不存在', NEW."columnId"; END IF;
IF NEW."parentId" IS NULL THEN
NEW."parentUuid" := NULL;
ELSE
SELECT "uuid" INTO NEW."parentUuid" FROM "Task" WHERE "id" = NEW."parentId";
IF NOT FOUND THEN RAISE EXCEPTION 'Task % 不存在', NEW."parentId"; END IF;
END IF;
RETURN NEW;
END;
$function$
;
CREATE OR REPLACE FUNCTION public.func_taskmember_uuid_map()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
SELECT "uuid" INTO NEW."taskUuid" FROM "Task" WHERE "id" = NEW."taskId";
IF NOT FOUND THEN RAISE EXCEPTION 'Task % 不存在', NEW."taskId"; END IF;
RETURN NEW;
END;
$function$
;
CREATE OR REPLACE FUNCTION public.func_tasktag_uuid_map()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
SELECT "uuid" INTO NEW."taskUuid" FROM "Task" WHERE "id" = NEW."taskId";
IF NOT FOUND THEN RAISE EXCEPTION 'Task % 不存在', NEW."taskId"; END IF;
RETURN NEW;
END;
$function$
;
CREATE TRIGGER trg_boardcolumn_uuid_map BEFORE INSERT OR UPDATE ON public."BoardColumn" FOR EACH ROW EXECUTE FUNCTION func_boardcolumn_uuid_map();
CREATE TRIGGER trg_projectmember_uuid_map BEFORE INSERT OR UPDATE ON public."ProjectMember" FOR EACH ROW EXECUTE FUNCTION func_projectmember_uuid_map();
CREATE TRIGGER trg_task_uuid_map BEFORE INSERT OR UPDATE ON public."Task" FOR EACH ROW EXECUTE FUNCTION func_task_uuid_map();
CREATE TRIGGER trg_taskmember_uuid_map BEFORE INSERT OR UPDATE ON public."TaskMember" FOR EACH ROW EXECUTE FUNCTION func_taskmember_uuid_map();
CREATE TRIGGER trg_tasktag_uuid_map BEFORE INSERT OR UPDATE ON public."TaskTag" FOR EACH ROW EXECUTE FUNCTION func_tasktag_uuid_map();
+134 -3
View File
@@ -1,8 +1,29 @@
# 0.2.1 发布任务 — 目标与现状
> 最后更新:2026-10-27 07:05
> 分支:`dev-0.2`(已推送,与 `origin/dev-0.2` 同步于 `b29be62`)
> 状态:**代码与打包全部就绪;生产切换尚未执行(停在"镜像已构建,待起栈+迁移+切域名")**
> 最后更新:2026-10-27 08:05
> 分支:`dev-0.2`(已推送)
> 状态:**生产已切换到 0.2.1 单端口架构并验证通过;剩余 "合 master 发布" 一步**
## 零、执行结果速览(2026-10-27 已完成)
| 项目 | 结果 |
| --- | --- |
| 生产地址 | `https://iboard.honor3.com` → baihu **3001**(NPM + 有效证书) |
| 生产架构 | `iboard-prod-app-1`(3001 对外) + `iboard-prod-pg-1`(内网) + `iboard-prod-powersync-1`(内网) |
| 端口 | **只剩 3001**;13001 / 13002 / 13443 全部下线 |
| 数据迁移 | MySQL → PG 完成:Project 3、Task 25、BoardColumn 12、User 1、CheckIn 17、News 19、IndicatorPoint 245、Mihome 等全部迁入 |
| 旧生产 | 容器 `iboard` 已 **stop(未删除)**,`docker start iboard` 可秒级回滚 |
| 登录态 | `JWT_SECRET` 沿用 `iboard`,现有用户无需重新登录 |
| 米家凭证 | `/app/iboard/cache` → `deploy/prod/data/cache`(`auth.txt` + `apizero/`) |
| E2E 验证 | 在线:项目卡片 + `ADMIN-16`/`ADMIN-18` 编号;断网:徽标出现、任务页数据完整、无 fail fetch;恢复:徽标消失 |
### 迁移过程中踩到并修掉的坑
1. `prisma/migrations/` 是 **MySQL 语法**(反引号),PG 无法 `migrate deploy` → 改用 `prisma db push --accept-data-loss` 按 schema 建表。
2. `db push` 首次因参数不被识别而输出帮助(表结构停在旧 schema,缺 `autoArchive`)→ 去掉错误参数重跑,列已补齐。
3. 新库缺 **uuid 镜像触发器**(`projectUuid`/`columnUuid`/`parentUuid`/`taskUuid`)→ 从 0.2.1 验证库导出 5 个函数 + 5 个触发器,落库并入库 `deploy/prod/sql/uuid-triggers.sql`。
4. bash 把 `$$` 展开成 PID 导致 TRUNCATE 脚本报错 → 改用 SQL 文件传入。
5. baihu 上 `docker build` 缺 buildx → 改用 `docker compose build`。
---
@@ -147,6 +168,116 @@ bash /app/iboard-prod/scripts/deploy/prod.sh
curl -s https://iboard.honor3.com/api/health
```
## 五之二、后续动作执行步骤(已确认方案,按序执行)
### 决策确认(2026-10-27)
| 项 | 决定 |
| --- | --- |
| PG 密钥 | **不进 Gitea Secrets**,直接写在服务器 `/app/iboard-prod/deploy/prod/.env`(600) |
| 迁移时机 | 立即执行 |
| 米家凭证 | 保留,`/app/iboard/cache` 搬到新栈 `deploy/prod/data/cache` |
| `JWT_SECRET` | 沿用现网值(`iboard`),保持现有登录态 |
| 域名 | `iboard.honor3.com` → baihu **3001**(NPM 已配好,SSL 强制) |
### 步骤 A:准备(不改动现网)
```bash
# A1. 沿用现网 JWT_SECRET (其余密钥保持已生成的随机值)
cd /app/iboard-prod/deploy/prod
sed -i 's/^JWT_SECRET=.*/JWT_SECRET=iboard/' .env
# A2. 搬米家缓存 (auth.txt 等)
mkdir -p data/cache
rsync -a /app/iboard/cache/ data/cache/
# A3. 保留旧镜像用于回滚
docker tag iboard:latest iboard:mysql-final # 需在重新构建前执行; 若已覆盖见"回滚预案"
```
### 步骤 B:起灰度栈(不动现网 3001)
```bash
cd /app/iboard-prod/deploy/prod
HOST_PORT=13011 docker compose --project-name iboard-prod \
--env-file .env -f docker-compose.yml up -d pg
# 迁移建表
docker run --rm --network iboard-prod_default \
-e DATABASE_URL="postgresql://iboard:$(grep ^PG_PASSWORD .env | cut -d= -f2)@pg:5432/iboard" \
iboard:latest npx prisma migrate deploy
# 建 publication (必须在建表之后)
docker compose --project-name iboard-prod --env-file .env -f docker-compose.yml exec -T pg \
psql -U iboard -d iboard -v ON_ERROR_STOP=1 -f /dev/stdin < sql/publication.sql
# 起 app + powersync (只有 13011 对外)
HOST_PORT=13011 docker compose --project-name iboard-prod \
--env-file .env -f docker-compose.yml up -d app powersync
# 验收
curl -s http://127.0.0.1:13011/api/health # {"ok":true,...}
ss -tlnp | grep -E ':(13011|5432|8080)\b' # 只应看到 13011
```
### 步骤 C:数据迁移(MySQL → PG)
```bash
docker run --rm --network iboard-prod_default \
-e DATABASE_URL="postgresql://iboard:<PG_PASSWORD>@pg:5432/iboard" \
-e SRC_DATABASE_URL="mysql://root:<MYSQL_PASSWORD>@<host>:3306/iboard" \
iboard:latest node scripts/etl-mariadb-to-pg.mjs
# 迁移后重启同步服务重建快照
docker compose --project-name iboard-prod --env-file .env -f docker-compose.yml restart powersync
```
**验收**:项目 3 个(ADMIN / IBOARD / P3F921)、任务 ~22 条、用户 1 个(admin@iboard.local);
`docker compose logs powersync` 显示 replication 已激活且无报错。
### 步骤 D:功能验证(13011)
- 登录 `admin@iboard.local`
- 任务页:项目卡片、任务编号(如 `ADMIN-16`)
- **离线验证**:断网后列表仍可读、左上角出现「离线」徽标,恢复网络自动同步回流
- 监控 / 宏观 / 打卡页有数据
### 步骤 E:切换生产
```bash
# E1. 停旧容器 (MySQL 版)
docker rm -vf iboard || true
# E2. 新栈改绑 3001
cd /app/iboard-prod/deploy/prod
HOST_PORT=3001 docker compose --project-name iboard-prod \
--env-file .env -f docker-compose.yml up -d app
curl -s https://iboard.honor3.com/api/health # 经 NPM + 证书
# E3. 下线 0.2.1 验证栈 (释放 13001/13002/13443)
cd /app/iboard-0.2.1/deploy/0.2.1 && docker compose down
```
**验收**:`https://iboard.honor3.com` 可登录、任务正常、离线可编辑;
`ss -tlnp` 只剩 3001(外加仅供本机的 3306)。
### 步骤 F:发布
```bash
git checkout master && git merge dev-0.2 --no-edit
git push origin master # 触发 CI: 构建镜像 + OTA
```
CI 未配 PG secrets → 只构建不部署;生产已由步骤 B~E 手工就绪。
### 回滚预案
| 场景 | 动作 |
| --- | --- |
| 新栈起不来 | 旧容器未停,现网不受影响,回步骤 B 排查 |
| 迁移数据不对 | 清空 `deploy/prod/data/pg` 后重跑 ETL |
| 切换后严重故障 | 用 `iboard:mysql-final` 旧镜像按原 MySQL 参数恢复 3001 |
## 六、已知遗留
- `dev-0.2.1` / `dev-windows-0.1` 两个 worktree 分支内容已全部并入 `dev-0.2`,可择机删除。