# 第二套独立部署 —— 设计文档 日期: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` 后缀;执行前逐条核对目标路径 |