From 034c87f95572ecf995f656326876e72331040ad4 Mon Sep 17 00:00:00 2001 From: "jif2.zhang" Date: Sun, 25 Oct 2026 23:57:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(0.2.1):=20M4-a=20=E9=83=A8=E7=BD=B2?= =?UTF-8?q?=E6=A0=88=20=E2=80=94=E2=80=94=20compose(PG=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E5=A4=8D=E5=88=B6+powersync+app:13001),=20sync=20streams(?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E5=9F=9F6=E8=A1=A8+User),=20initdb,=20DISABL?= =?UTF-8?q?E=5FCRON=20=E5=BC=80=E5=85=B3,=20=E5=81=A5=E5=BA=B7=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E7=AB=AF=E5=8F=A3=E8=B7=9F=E9=9A=8F=20PORT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 1 + .gitignore | 4 ++ Dockerfile | 4 +- deploy/0.2.1/.env.example | 11 ++++ deploy/0.2.1/README.md | 58 +++++++++++++++++ deploy/0.2.1/docker-compose.yml | 70 +++++++++++++++++++++ deploy/0.2.1/initdb/01-powersync-storage.sh | 18 ++++++ deploy/0.2.1/powersync/service.yaml | 39 ++++++++++++ deploy/0.2.1/powersync/sync-config.yaml | 24 +++++++ server/index.js | 10 +-- 10 files changed, 233 insertions(+), 6 deletions(-) create mode 100644 deploy/0.2.1/.env.example create mode 100644 deploy/0.2.1/README.md create mode 100644 deploy/0.2.1/docker-compose.yml create mode 100644 deploy/0.2.1/initdb/01-powersync-storage.sh create mode 100644 deploy/0.2.1/powersync/service.yaml create mode 100644 deploy/0.2.1/powersync/sync-config.yaml diff --git a/.dockerignore b/.dockerignore index fe773e5..7b88c31 100644 --- a/.dockerignore +++ b/.dockerignore @@ -9,3 +9,4 @@ src-tauri docs *.log readme +deploy diff --git a/.gitignore b/.gitignore index bd42204..c7b750c 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,7 @@ server/mihome/.cache/ # 本地 pnpm 安装产物 (项目使用 package-lock.json) /.pnpm-store + +# 0.2.1 验证部署: 服务器本地数据卷与密钥 (绝不入库) +deploy/0.2.1/data/ +deploy/0.2.1/.env diff --git a/Dockerfile b/Dockerfile index ffec25c..06a701f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -72,9 +72,9 @@ ENV OTA_PACKAGE_DIR=/app/ota-package # 生产为 http 明文 (无 https), 客户端 tauri.conf.json 的 hotswap.require_https 已设为 false 放行 http ENV OTA_PUBLIC_BASE=http://bh.vps.honor3.com:3001 -# 健康检查 +# 健康检查 (端口跟随 PORT 环境变量, 0.2.1 部署为 13001) HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD wget -qO- http://127.0.0.1:3001/api/health || exit 1 + CMD wget -qO- http://127.0.0.1:${PORT:-3001}/api/health || exit 1 EXPOSE 3001 diff --git a/deploy/0.2.1/.env.example b/deploy/0.2.1/.env.example new file mode 100644 index 0000000..6a6e0c1 --- /dev/null +++ b/deploy/0.2.1/.env.example @@ -0,0 +1,11 @@ +# 复制为 .env (服务器上 chmod 600, 已 gitignore)。 +# POWERSYNC_JWT_KEY_B64 = base64url(POWERSYNC_JWT_SECRET 的 UTF-8 字节) +# 生成: printf %s "$POWERSYNC_JWT_SECRET" | openssl base64 -A | tr '+/' '-_' | tr -d '=' + +PG_PASSWORD=change-me +PS_STORAGE_PASSWORD=change-me +JWT_SECRET_021=change-me +POWERSYNC_JWT_SECRET=change-me +POWERSYNC_JWT_KEY_B64=change-me +POWERSYNC_URL=http://bh.vps.honor3.com:13002 +OTA_PUBLIC_BASE=http://bh.vps.honor3.com:13001 diff --git a/deploy/0.2.1/README.md b/deploy/0.2.1/README.md new file mode 100644 index 0000000..417be30 --- /dev/null +++ b/deploy/0.2.1/README.md @@ -0,0 +1,58 @@ +# 0.2.1 独立验证部署 (baihu /app/iboard-0.2.1) + +与现网 `/app/iboard`(3001/宿主 MySQL) 完全隔离:本栈使用独立 PG 容器、端口 13001/13002,不写原库。 + +## 栈 + +| 服务 | 说明 | +| --- | --- | +| `pg` | postgres:16-alpine,`wal_level=logical`(供 PowerSync 逻辑复制),数据在 `./data/pg`,**不对外暴露端口** | +| `app` | 本仓库 Dockerfile 构建,13001 对外;启动时先 `prisma migrate deploy`;`DISABLE_CRON=1` 关闭定时任务 | +| `powersync` | journeyapps/powersync-service,13002 对外;源库=pg,bucket storage=同实例 PG 的 powersync schema | + +## 首次部署 + +```bash +# 服务器上 (源码已上传到 /app/iboard-0.2.1) +cd /app/iboard-0.2.1/deploy/0.2.1 + +# 1. 生成 .env (参照 .env.example; POWERSYNC_JWT_KEY_B64 由 secret 派生) +umask 077 +PG_PASSWORD=$(openssl rand -hex 16) +PS_STORAGE_PASSWORD=$(openssl rand -hex 16) +JWT_SECRET_021=$(openssl rand -hex 32) +POWERSYNC_JWT_SECRET=$(openssl rand -hex 32) +POWERSYNC_JWT_KEY_B64=$(printf %s "$POWERSYNC_JWT_SECRET" | openssl base64 -A | tr '+/' '-_' | tr -d '=') +cat > .env <= 14 允许源库与 bucket storage 同实例 (powersync schema 由 service 自建) +set -e +: "${PS_STORAGE_PASSWORD:?PS_STORAGE_PASSWORD missing}" + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + DO \$\$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'powersync_storage') THEN + CREATE USER powersync_storage WITH PASSWORD '$PS_STORAGE_PASSWORD'; + END IF; + END + \$\$; + GRANT CREATE ON DATABASE $POSTGRES_DB TO powersync_storage; +EOSQL + +echo "[initdb] powersync_storage user ready." diff --git a/deploy/0.2.1/powersync/service.yaml b/deploy/0.2.1/powersync/service.yaml new file mode 100644 index 0000000..18d9892 --- /dev/null +++ b/deploy/0.2.1/powersync/service.yaml @@ -0,0 +1,39 @@ +# PowerSync Service 主配置 (0.2.1 验证部署) +# 参考: https://docs.powersync.com/configuration/powersync-service/self-hosted-instances +# !env 仅支持 PS_ 前缀变量, 由 docker-compose environment 注入 + +replication: + connections: + - type: postgresql + # 源库: 本栈 pg 容器 (0.2.1 独立实例), 复制走 iboard021 账号 + uri: postgresql://iboard021:!env PS_PG_PASSWORD@pg:5432/iboard021 + sslmode: disable + +# bucket storage: 同实例 PG (16 >= 14 允许同库不同 schema, service 自建 powersync schema) +storage: + type: postgresql + uri: postgresql://powersync_storage:!env PS_STORAGE_PASSWORD@pg:5432/iboard021 + +port: 8080 + +sync_config: + path: /config/sync-config.yaml + +# 客户端鉴权: app 后端 /api/powersync/token 签发 HS256 JWT (aud=powersync) +# oct 密钥 k = base64url(POWERSYNC_JWT_SECRET) +client_auth: + jwks: + keys: + - kty: oct + k: !env PS_JWT_KEY_B64 + alg: HS256 + audience: + - powersync + +telemetry: + disable_telemetry_sharing: true + +system: + logging: + level: info + format: text diff --git a/deploy/0.2.1/powersync/sync-config.yaml b/deploy/0.2.1/powersync/sync-config.yaml new file mode 100644 index 0000000..b8a588d --- /dev/null +++ b/deploy/0.2.1/powersync/sync-config.yaml @@ -0,0 +1,24 @@ +# Sync Streams (edition 3): 任务模块 6 表 + User (展示用) +# 引号保留 PG PascalCase 标识; 主键要求: 同步表必须输出 text 型 id 列 => CAST("id" AS TEXT) +# 可见性: 用户能看到自己参与的项目 (ProjectMember), 及其下全部任务域数据 +# 见 docs/V0.2/离线编辑模块/0.2.1实施方案.md + +config: + edition: 3 + +streams: + iboard_users: + auto_subscribe: true + query: SELECT CAST("id" AS TEXT) AS "id", "name", "email" FROM "User" + + iboard_task_data: + auto_subscribe: true + with: + my_projects: SELECT "projectId" FROM "ProjectMember" WHERE CAST("userId" AS TEXT) = auth.user_id() + queries: + - SELECT CAST("id" AS TEXT) AS "id", "uuid", "code", "name", "ownerId", "isPersonal", "archived", "notes", "taskKinds", "icon", "createdAt", "updatedAt" FROM "Project" WHERE "id" IN my_projects + - SELECT CAST("id" AS TEXT) AS "id", "uuid", "projectId", "userId", "permission", "createdAt" FROM "ProjectMember" WHERE "projectId" IN my_projects + - SELECT CAST("id" AS TEXT) AS "id", "uuid", "projectId", "name", "sortOrder", "boundStatus", "retention", "createdAt", "updatedAt" FROM "BoardColumn" WHERE "projectId" IN my_projects + - SELECT CAST("id" AS TEXT) AS "id", "uuid", "projectId", "columnId", "parentId", "title", "description", "kind", "markDate", "status", "doneAt", "assigneeId", "creatorId", "extras", "sortOrder", "number", "createdAt", "updatedAt" FROM "Task" WHERE "projectId" IN my_projects + - SELECT CAST("id" AS TEXT) AS "id", "uuid", "taskId", "userId" FROM "TaskMember" WHERE "taskId" IN (SELECT "id" FROM "Task" WHERE "projectId" IN my_projects) + - SELECT CAST("id" AS TEXT) AS "id", "uuid", "taskId", "name" FROM "TaskTag" WHERE "taskId" IN (SELECT "id" FROM "Task" WHERE "projectId" IN my_projects) diff --git a/server/index.js b/server/index.js index 8c3a87c..1d803de 100644 --- a/server/index.js +++ b/server/index.js @@ -69,11 +69,13 @@ app.use((err, _req, res, _next) => { res.status(500).json({ error: err.message || '服务器内部错误' }) }) -// 启动定时任务: 每天凌晨 3 点拉取新闻 -import('./cron.js').catch((e) => console.error('[cron] 加载失败:', e.message)) +// 启动定时任务: 每天凌晨 3 点拉取新闻; 0.2.1 验证部署以 DISABLE_CRON=1 关闭 (避免重复外呼) +if (process.env.DISABLE_CRON !== '1') { + import('./cron.js').catch((e) => console.error('[cron] 加载失败:', e.message)) -// 米家环境监控定时调度 (独立注册, 不与 cron.js 绑定, 见 server/mihome/scheduler.js) -import('./mihome/scheduler.js').catch((e) => console.error('[cron] 米家调度注册失败:', e.message)) + // 米家环境监控定时调度 (独立注册, 不与 cron.js 绑定, 见 server/mihome/scheduler.js) + import('./mihome/scheduler.js').catch((e) => console.error('[cron] 米家调度注册失败:', e.message)) +} app.listen(PORT, () => { console.log(`[iboard-api] listening on http://127.0.0.1:${PORT}`)