diff --git a/.gitignore b/.gitignore index 9e35c58..ac6b3ec 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,8 @@ deploy/0.2.1/.env /deploy/prod/.env /deploy/prod/data/ + +# ±¾µØÁÙʱ½ű¾ (º¬¿ÚÁî, ²»Èë¿â) +scripts/_*.mjs +.dsh-ops/ + diff --git a/deploy/prod/sql/uuid-triggers.sql b/deploy/prod/sql/uuid-triggers.sql new file mode 100644 index 0000000..79b3fbe --- /dev/null +++ b/deploy/prod/sql/uuid-triggers.sql @@ -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(); diff --git a/docs/021任务.md b/docs/021任务.md index 92ec42d..2c10b69 100644 --- a/docs/021任务.md +++ b/docs/021任务.md @@ -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:5432/iboard" \ + -e SRC_DATABASE_URL="mysql://root:@: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`,可择机删除。