airpredict/空气质量预测/源码/docs/superpowers/specs/2026-07-09-second-independe...

210 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 第二套独立部署 —— 设计文档
日期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`onlineuptime 自 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
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<rewrite>
<rules>
<rule name="api">
<match url="^api/(.*)$" />
<action type="Rewrite" url="http://localhost:6061/api/{R:1}" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>
```
前端用的是 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` 失败重试 23 次(缓存累积) |
| 8081 未放行导致外部不可达 | 部署前先请用户在阿里云安全组放行;本机 curl 可先行验证后端 |
| 误操作写到 `C:\API` / `C:\iapips` / `iapips` 库 | 所有路径与库名带 `2` 后缀;执行前逐条核对目标路径 |