diff --git a/空气质量预测/源码/docs/superpowers/specs/2026-07-09-second-independent-deployment-design.md b/空气质量预测/源码/docs/superpowers/specs/2026-07-09-second-independent-deployment-design.md new file mode 100644 index 0000000..a16f7f6 --- /dev/null +++ b/空气质量预测/源码/docs/superpowers/specs/2026-07-09-second-independent-deployment-design.md @@ -0,0 +1,209 @@ +# 第二套独立部署 —— 设计文档 + +日期:2026-07-09 +目标服务器:阿里云 Windows Server 2012 R2,公网 IP `47.95.40.218` + +## 1. 背景与目标 + +服务器上已经运行着一套 IAPIP 生产系统(下称**原系统**),对外域名 `https://indoorhealthair.com/`,自 2025-08-18 起持续在线。 + +现在要在同一台服务器上部署**第二套完全独立的系统**(下称**新系统**),要求: + +- 新系统与原系统互不影响,数据、进程、配置、静态文件全部隔离 +- **绝对不能覆盖、修改或重启原系统的任何部分** +- 新系统部署 `feature/source-charts` 分支的最新代码(含用户端与管理端) +- 新系统的数据库是原系统数据库的一份副本,此后各自演化 + +成功标准:新系统可从 `http://47.95.40.218:8081/iapip-web/` 正常登录并完成一次预测;同时原系统 `https://indoorhealthair.com/iapip-web/` 无任何异常,其 PM2 进程 `iapip-svr` 的 `restart_time` 保持为 0。 + +## 2. 原系统现状(2026-07-09 实地探测) + +| 项 | 值 | +|---|---| +| 后端代码 | `C:\API\iapip-svr`(git `HarveyQi0425/iapip-svr`,分支 `deploy1`,rev `44e1aab`) | +| 后端进程 | PM2 app `iapip-svr`,cluster 模式,`dist/index.js`,online,uptime 自 2025-08-18 | +| 后端端口 | 6060 | +| PM2_HOME | `C:\Users\Administrator\.pm2`(默认) | +| 数据库 | `mysql://root:harvey0425@localhost:3306/iapips` | +| MySQL | 5.7.44,`C:\MySQL\mysql-5.7.44-winx64\bin` | +| IIS 站点 | `iapips`,绑定 `http/*:80:indoorhealthair.com` + `https:443`,应用池 `iapips` | +| IIS 物理路径 | `C:\iapips`,下挂应用 `/iapip-web`、`/iapip-ms` | +| `/api` 反代 | `C:\iapips\web.config` 的 URL Rewrite 规则 → `http://localhost:6060/api/{R:1}` | +| 上传目录 | `C:\API\`(后端 `path.join(__dirname, '../../')` 的结果) | +| 开机自启 | **无**。没有服务、计划任务或启动项;只有 `pm2 save` 留下的 `dump.pm2` | + +服务器已装:Node v18.18.0、npm 9.8.1、git 2.43、pm2 5.4.3、Python 3.12、chocolatey。 +IIS 已装 **URL Rewrite** 与 **ARR**(`system.webServer/proxy` 的 `enabled="true"`)。 +端口 6061 与 8081 均空闲。C 盘剩余约 71 GB。 + +## 3. 关键代码事实 + +这些事实决定了新系统需要改动哪些文件,均已核对源码: + +1. 后端**运行时不加载 dotenv**。`src/index.ts:24` 只读 `process.env.IAPIP_SVR_PORT`,实际值由 PM2 从 `ecosystem.config.js` 的 `env` 注入。`.env` 仅被 `package.json` 中的 `dotenv -e .env -- npx prisma ...` 用于 prisma CLI 命令。 + → 新系统只需改 `ecosystem.config.js` 与 `.env` 两个文件。 + +2. 前端调用的是**同源相对路径** `/api/*`(如 `src/services/api/user.ts:16`),依赖 IIS 在同一 origin 上把 `/api` 反代到本机后端端口。 + → 两套系统必须各有独立 origin,否则 `/api` 冲突。新系统用独立端口 8081 实现。 + +3. 前端 `publicPath` 为 `/iapip-web/`(管理端 `/iapip-ms/`)。新站点是独立 origin 且应用路径结构与原站相同。 + → **前端构建产物无需任何配置改动**,直接 build、直接拷贝。 + (`.umirc.ts` 里 `proxy.target: http://localhost:6060` 只作用于 `pnpm dev`,不影响 `pnpm build` 产物。) + +4. 后端上传目录为 `path.join(__dirname, '../../')`,即代码目录的父目录。新系统代码放在 `C:\API2\iapip-svr`,上传自然落到 `C:\API2\`,与原系统的 `C:\API\` 天然隔离。 + +5. 新代码的 Prisma schema 比原系统数据库多三张表:`ForumThread`、`ForumReply`、`ForumThreadLike`。 + → 从原库复制数据后,必须对新库补跑 `prisma db push`。 + +6. `prisma/seed.ts` 使用固定 id(`U000000` / `A000000`)且不清表,**不可重复执行**。新系统数据来自原库副本,因此**不跑 seed**。 + +## 4. 隔离方案 + +采用**独立 PM2_HOME** 方案。 + +理由:这套部署里唯一带全局副作用的工具是 PM2 —— `pm2 restart all`、`pm2 kill`、`pm2 resurrect`、`pm2 update` 都作用于整个守护进程下的所有应用。若两套系统共用默认 PM2_HOME,隔离就只能靠"操作时小心"来维持。给新系统单独指定 `PM2_HOME=C:\API2\.pm2` 后,它会启动一个独立的 PM2 守护进程,两边的 `pm2 list` 互相不可见,任何 pm2 命令在物理上都够不到对方的进程。 + +IIS 与 MySQL 不需要额外隔离手段:IIS 的站点/应用池、MySQL 的库,本身就是按名字划分的强边界,应用层不存在跨库读写的可能。为此再装一个 MySQL 实例(监听 3307)代价高而收益低,不采用。 + +### 资源分配 + +| 资源 | 原系统(保持不动) | 新系统 | +|---|---|---| +| 后端代码 | `C:\API\iapip-svr` | `C:\API2\iapip-svr` | +| 后端端口 | 6060 | **6061** | +| PM2 应用名 | `iapip-svr` | `iapip-svr2` | +| **PM2_HOME** | `C:\Users\Administrator\.pm2` | **`C:\API2\.pm2`** | +| 数据库 | `iapips` | `iapips2`(同一 MySQL 实例,3306) | +| JWT 密钥 | `PROD-IAPIPS-JWT-SECRET-KEY` | 新生成的随机值 | +| 前端静态文件 | `C:\iapips\{iapip-web,iapip-ms}` | `C:\iapips2\{iapip-web,iapip-ms}` | +| IIS 站点 | `iapips`(80/443 + 域名) | `iapips2`(绑定 `http/*:8081:`) | +| IIS 应用池 | `iapips` | `iapips2` | +| 上传目录 | `C:\API\` | `C:\API2\` | + +共享的仅三样:Windows 操作系统、MySQL 服务进程(库分离)、IIS 服务进程(站点与应用池分离)。 + +新系统的所有 pm2 操作通过封装脚本 `C:\API2\pm2.bat` 执行,该脚本先 `set PM2_HOME=C:\API2\.pm2` 再转发参数给 pm2。 + +## 5. 构建与部署流程 + +### 5.1 前端(本地构建) + +服务器不安装 pnpm,也不接触前端工具链。 + +- 本地 `源码/用户端/iapip-web` 执行 `pnpm build`,产物 `dist/` 传至 `C:\iapips2\iapip-web\` +- 本地 `源码/管理端/iapip-ms` 执行 `pnpm build`,产物 `dist/` 传至 `C:\iapips2\iapip-ms\` +- 比照原站结构,把 PDF 中文字体目录从 `C:\iapips\fonts`、`C:\iapips\fts` **只读复制**到 `C:\iapips2\` 下(各应用目录内亦有同名子目录,一并照抄) + +注记:本地 `pnpm install` 需要 `pnpm-workspace.yaml` 的 `allowBuilds` 放行 `core-js` / `core-js-pure` / `es5-ext` / `esbuild`,否则 pnpm v11 会以 `ERR_PNPM_IGNORED_BUILDS` 退出。已知的 umi tailwindcss 插件补丁只影响 `pnpm dev` 的 watch 模式,与 `pnpm build` 无关。 + +### 5.2 后端(传源码,服务器上编译) + +`@prisma/client` 与 `esbuild` 含平台相关的原生二进制,本地 `node_modules` 不可直接拷贝。 + +1. 传输 `src/`、`prisma/`、`fonts/`、`package.json`、`tsconfig.json`、`opft.xlsx` 至 `C:\API2\iapip-svr\` +2. 服务器上执行 `npm install`(走项目 `.npmrc` 的 npmmirror 源,偶发 ECONNRESET,需重试) +3. `npx prisma generate` +4. `npm run build`(`tsc` 输出 `dist/`) + +新建两个文件: + +JWT 密钥用 `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"` 生成一个 64 位十六进制串,同时写入下面两个文件(两处必须一致)。 + +`C:\API2\iapip-svr\.env`(供 prisma CLI): +``` +DATABASE_URL="mysql://root:harvey0425@localhost:3306/iapips2" +JWT_SECRET_KEY="<上一步生成的 64 位十六进制串>" +IAPIP_SVR_PORT=6061 +NODE_ENV=production +``` + +`C:\API2\iapip-svr\ecosystem.config.js`:`name: "iapip-svr2"`,`env` 中 `DATABASE_URL` 指向 `iapips2`、`IAPIP_SVR_PORT: 6061`、`JWT_SECRET_KEY` 为新值。 + +传输方式优先 `scp`;若服务器 sshd 未启用 sftp 子系统,退回 `tar | ssh` 管道。 + +### 5.3 数据库复制 + +全程只读原库: + +1. `mysqldump --single-transaction iapips > C:\API2\iapips_backup.sql` + (`--single-transaction` 对 InnoDB 表不加锁,不影响线上读写) +2. `CREATE DATABASE iapips2 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;` 然后导入该 dump +3. 对 `iapips2` 执行 `prisma db push`,补上 `ForumThread` / `ForumReply` / `ForumThreadLike` 三张表及列差异 + +第 3 步是唯一有数据风险的操作。若 `db push` 判定某些变更会丢数据而要求 `--accept-data-loss`,**必须先查看它打算执行的变更并交由用户确认**,不得直接加该 flag。该步骤的连接串指向 `iapips2`,原库 `iapips` 不出现在任何写操作中。 + +不执行 `prisma/seed.ts`(见第 3 节第 6 条)。 + +### 5.4 IIS 配置 + +1. 新建应用池 `iapips2` +2. 新建站点 `iapips2`,物理路径 `C:\iapips2`,绑定 `http/*:8081:`,应用池 `iapips2` +3. 站点下新建应用 `/iapip-web`(`C:\iapips2\iapip-web`)与 `/iapip-ms`(`C:\iapips2\iapip-ms`) +4. `C:\iapips2\web.config` 照抄原站结构,仅把反代目标端口改为 6061: + +```xml + + + + + + + + + + + + + +``` + +前端用的是 hash 路由,因此不需要 SPA 回退规则(原站也没有)。 + +所有 IIS 操作只新增对象,不修改站点 `iapips`、应用池 `iapips` 或 `Default Web Site`。 + +### 5.5 网络放行 + +- 服务器本机防火墙:新增入站规则放行 TCP 8081 +- **阿里云安全组:需用户在控制台放行入方向 TCP 8081。此步骤无法由 Claude 代劳。** + +## 6. 启动 + +``` +C:\API2\pm2.bat start ecosystem.config.js --env production +C:\API2\pm2.bat save +``` + +与原系统一致,新系统同样**不配置开机自启**(原系统也没有;引入自启机制会改变服务器现状,超出本次范围)。若日后需要,应为两个 PM2_HOME 分别配置,不可用一条 `pm2 resurrect` 覆盖。 + +## 7. 验收 + +按顺序执行,每步必须通过: + +1. 服务器本机 `curl http://localhost:6061/api/test/ping` 返回 `OK` +2. `C:\API2\pm2.bat list` 只显示 `iapip-svr2`;不带 PM2_HOME 的 `pm2 list` 只显示 `iapip-svr` —— 证明两个守护进程互不可见 +3. 浏览器访问 `http://47.95.40.218:8081/iapip-web/`,用原系统账号登录,完成一次预测,确认 `/source` 页的柱状图与贡献饼图正常渲染 +4. 浏览器访问 `http://47.95.40.218:8081/iapip-ms/`,管理端可登录 +5. **原系统体检**:`https://indoorhealthair.com/iapip-web/` 正常访问;`pm2 list` 中 `iapip-svr` 状态 `online` 且 `restart_time` 仍为 0 + +## 8. 回滚 + +新系统的全部资源都在独立命名空间内,回滚即删除: + +1. `C:\API2\pm2.bat delete iapip-svr2` 并 `C:\API2\pm2.bat kill` +2. 删除 IIS 站点 `iapips2` 与应用池 `iapips2` +3. `DROP DATABASE iapips2;` +4. 删除 `C:\API2\` 与 `C:\iapips2\` +5. 移除防火墙 8081 规则 + +原系统全程零改动,无需回滚。 + +## 9. 风险清单 + +| 风险 | 缓解 | +|---|---| +| pm2 全局命令误伤原系统 | 独立 PM2_HOME;新系统 pm2 一律走 `C:\API2\pm2.bat` | +| `prisma db push` 丢数据 | 仅作用于 `iapips2`;需 `--accept-data-loss` 时先呈报 diff 给用户确认 | +| `mysqldump` 影响线上性能 | `--single-transaction`,InnoDB 不加锁;建议低峰期执行 | +| npmmirror 源不稳定 | `npm install` 失败重试 2–3 次(缓存累积) | +| 8081 未放行导致外部不可达 | 部署前先请用户在阿里云安全组放行;本机 curl 可先行验证后端 | +| 误操作写到 `C:\API` / `C:\iapips` / `iapips` 库 | 所有路径与库名带 `2` 后缀;执行前逐条核对目标路径 |