# 第二套独立部署 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 在阿里云服务器 `47.95.40.218` 上部署第二套完全独立的 IAPIP 系统(`http://47.95.40.218:8081/iapip-web/`),与现有生产系统在进程、数据、文件、IIS 站点四个层面全部隔离,且原系统零改动。 **Architecture:** 新系统的后端代码放 `C:\API2\iapip-svr`(端口 6061),由一个**独立 PM2_HOME**(`C:\API2\.pm2`)下的守护进程管理,应用名 `iapip-svr2`;前端静态文件放 `C:\iapips2`,由新建的 IIS 站点 `iapips2`(绑定 8081)提供,其 `web.config` 把 `/api` 反代到 6061;数据库 `iapips2` 是原库 `iapips` 的一份 dump 副本,再用 `prisma db push` 补齐新代码多出的表。 **Tech Stack:** Node 18 / Koa / Prisma 5 / MySQL 5.7.44 / PM2 5.4.3 / IIS + URL Rewrite + ARR / UmiJS 4(前端本地构建) 参见设计文档:`docs/superpowers/specs/2026-07-09-second-independent-deployment-design.md` ## Global Constraints - **绝对禁止**修改、覆盖、重启原系统的任何部分:`C:\API\iapip-svr`、`C:\iapips\`、IIS 站点 `iapips`、IIS 应用池 `iapips`、数据库 `iapips`、默认 PM2_HOME `C:\Users\Administrator\.pm2` 下的进程 `iapip-svr`。 - 所有 pm2 命令**一律**通过 `C:\API2\pm2.bat` 执行。**永远不要**在本次部署中直接调用裸 `pm2`(哪怕是 `pm2 list`),除非该步骤明确标注为"原系统体检"。 - **永远不要**对 `prisma db push` 加 `--accept-data-loss`,除非先向用户展示 `migrate diff` 的 SQL 并获得确认。 - 数据库连接串只允许出现 `iapips2`。任何写操作的连接串中出现 `iapips`(无 `2` 后缀)即为事故。 - 端口:后端 `6061`,站点 `8081`。数据库名 `iapips2`。PM2 应用名 `iapip-svr2`。 - 服务器不安装 pnpm。前端在本地构建,只上传产物。 - 新系统**不配置开机自启**,**不执行** `prisma/seed.ts`。 - 本地 shell 为 Git Bash;服务器默认 shell 为 cmd。 ### 通用变量(每个任务的命令都假设已定义) **每个任务开始时都要先执行这一段**(子 agent 分任务执行时,shell 变量不跨任务保留): ```bash SSH="ssh -i C:/Users/11825/.ssh/iapip_deploy -o BatchMode=yes administrator@47.95.40.218" SCP="scp -i C:/Users/11825/.ssh/iapip_deploy -o BatchMode=yes" SRC="C:/code/空气质量预测/源码" WORK="C:/Users/11825/AppData/Local/Temp/claude/C--code----------/fb44c3ac-7eaa-49ef-a411-7f834989572f/scratchpad" APPCMD='C:\Windows\System32\inetsrv\appcmd' MYSQL='"C:\MySQL\mysql-5.7.44-winx64\bin\mysql.exe" -uroot -pharvey0425' MYSQLDUMP='"C:\MySQL\mysql-5.7.44-winx64\bin\mysqldump.exe" -uroot -pharvey0425' mkdir -p "$WORK" ``` > **远程 PowerShell 的引号陷阱**:本地是 Git Bash,双引号里的 `$x` 会被 bash 先行展开,导致远程 PowerShell 收到空变量。本计划中所有远程 PS 命令都刻意避免使用 PS 变量。若你需要写带变量的 PS 脚本,改用:把脚本写成本地 `.ps1` 文件 → `scp` 上去 → `$SSH "powershell -NoProfile -ExecutionPolicy Bypass -File C:\API2\_deploy\x.ps1"`。 ### 原系统体检(下称 **HEALTHCHECK**) 多个任务结束时要求执行此检查。三项全部通过才算该任务完成: ```bash $SSH "C:\Windows\System32\inetsrv\appcmd list site iapips" # 预期输出包含: state:Started $SSH "pm2 jlist" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const a=JSON.parse(s).find(x=>x.name==="iapip-svr");console.log("name="+a.name,"status="+a.pm2_env.status,"restarts="+a.pm2_env.restart_time)})' # 预期输出: name=iapip-svr status=online restarts=0 $SSH "powershell -NoProfile -Command \"(Invoke-WebRequest -Uri http://localhost:6060/api/test/ping -UseBasicParsing).StatusCode\"" # 预期输出: 200 ``` 任何一项不符 → **立即停止,报告用户,不要继续**。 --- ## Task 1: 本地构建前端产物 只在本地操作,不碰服务器。产出用户端与管理端的静态文件。 **Files:** - Modify(如需): `源码/用户端/iapip-web/pnpm-workspace.yaml` - Modify(如需): `源码/管理端/iapip-ms/pnpm-workspace.yaml` - Create(构建产物,不入 git): `源码/用户端/iapip-web/dist/` - Create(构建产物,不入 git): `源码/管理端/iapip-ms/dist/` **Interfaces:** - Consumes: 无 - Produces: `dist/` 目录,供 Task 6 上传。`dist/index.html` 中的资源路径前缀必须是 `/iapip-web/`(管理端为 `/iapip-ms/`) - [ ] **Step 1: 确认当前分支是 feature/source-charts** ```bash cd "$SRC" && git rev-parse --abbrev-ref HEAD ``` 预期输出:`feature/source-charts` 不是的话执行 `git checkout feature/source-charts` 再继续。 - [ ] **Step 2: 确认 pnpm 构建脚本放行** pnpm v11 默认拦截 build scripts,会以 `ERR_PNPM_IGNORED_BUILDS` 退出。检查两个 workspace 文件: ```bash cat "$SRC/用户端/iapip-web/pnpm-workspace.yaml" cat "$SRC/管理端/iapip-ms/pnpm-workspace.yaml" ``` 若 `allowBuilds` 不存在或是占位文字,改成: ```yaml allowBuilds: core-js: true core-js-pure: true es5-ext: true esbuild: true ``` - [ ] **Step 3: 构建用户端** ```bash cd "$SRC/用户端/iapip-web" && pnpm install && pnpm build ``` 预期:最后打印 `Build success`(或 umi 的成功摘要),`dist/` 目录生成。 npmmirror 源偶发 `ECONNRESET`,`pnpm install` 失败就重试,缓存会累积,一般 2–3 次装全。 - [ ] **Step 4: 验证用户端产物的 publicPath** ```bash cd "$SRC/用户端/iapip-web" && ls dist/index.html && grep -o '/iapip-web/[a-z0-9_.-]*\.js' dist/index.html | head -3 ``` 预期:`dist/index.html` 存在,且 grep 打印出若干条以 `/iapip-web/` 开头的路径。 若路径不带 `/iapip-web/` 前缀 → `.umirc.ts` 的 `publicPath` 被改过,停止并报告。 - [ ] **Step 5: 构建管理端** ```bash cd "$SRC/管理端/iapip-ms" && pnpm install && pnpm build ``` 预期:构建成功,`dist/` 生成。 - [ ] **Step 6: 验证管理端产物的 publicPath** ```bash cd "$SRC/管理端/iapip-ms" && ls dist/index.html && grep -o '/iapip-ms/[a-z0-9_.-]*\.js' dist/index.html | head -3 ``` 预期:`dist/index.html` 存在,grep 打印出若干条 `/iapip-ms/` 开头的路径。 - [ ] **Step 7: 记录产物大小,供上传后比对** ```bash du -sb "$SRC/用户端/iapip-web/dist" "$SRC/管理端/iapip-ms/dist" ``` 把两个字节数记下来,Task 6 上传后要核对。 (本任务无 commit:`dist/` 是构建产物;若 `pnpm-workspace.yaml` 被改动,在 Task 1 末尾单独提交:) ```bash cd "$SRC" && git add 用户端/iapip-web/pnpm-workspace.yaml 管理端/iapip-ms/pnpm-workspace.yaml && git commit -m "chore: 放行 pnpm 构建脚本以支持生产构建" ``` --- ## Task 2: 服务器基线快照与目录骨架 在动任何东西之前,把原系统的状态记录下来,作为后续每次 HEALTHCHECK 的比对基准。然后创建新系统的空目录。 **Files:** - Create(服务器): `C:\API2\` - Create(服务器): `C:\API2\_deploy\`(存放临时脚本) - Create(服务器): `C:\iapips2\` - Create(本地): 基线记录,保存到 scratchpad **Interfaces:** - Consumes: 无 - Produces: 服务器上存在空目录 `C:\API2`、`C:\API2\_deploy`、`C:\iapips2` - [ ] **Step 1: 抓取原系统基线** ```bash $SSH "pm2 jlist" > /tmp/baseline-pm2.json $SSH "C:\Windows\System32\inetsrv\appcmd list site" > /tmp/baseline-sites.txt $SSH "C:\Windows\System32\inetsrv\appcmd list apppool" > /tmp/baseline-apppools.txt cat /tmp/baseline-sites.txt ``` 预期 `baseline-sites.txt` 含两行:`Default Web Site`(Stopped)与 `iapips`(Started)。 - [ ] **Step 2: 确认 6061 与 8081 仍空闲,且 iapips2 库不存在** ```bash $SSH "netstat -ano | findstr /R \":6061 :8081\"" ``` 预期:**无输出**(退出码非 0 也正常,findstr 无匹配即返回 1)。有输出则端口被占,停止并报告。 ```bash $SSH "$MYSQL -e \"show databases;\"" 2>&1 | grep -v Warning ``` 预期输出中**没有** `iapips2`。若已存在 → 停止并报告(可能是上次失败的残留,需先确认能否 DROP)。 - [ ] **Step 3: 创建目录骨架** ```bash $SSH "mkdir C:\API2 & mkdir C:\API2\_deploy & mkdir C:\API2\iapip-svr & mkdir C:\iapips2" ``` - [ ] **Step 4: 验证目录已建且原目录未被触碰** ```bash $SSH "if exist C:\API2\_deploy (echo API2_OK) & if exist C:\iapips2 (echo IAPIPS2_OK) & if exist C:\API\iapip-svr (echo ORIG_SVR_INTACT) & if exist C:\iapips\web.config (echo ORIG_WEB_INTACT)" ``` 预期输出四行:`API2_OK`、`IAPIPS2_OK`、`ORIG_SVR_INTACT`、`ORIG_WEB_INTACT` - [ ] **Step 5: HEALTHCHECK** 执行本文档开头定义的 HEALTHCHECK 三项。全部通过才进入 Task 3。 --- ## Task 3: 复制数据库到 iapips2 全程只读原库。dump 用 `--single-transaction`,对 InnoDB 不加锁,不影响线上读写。 **Files:** - Create(服务器): `C:\API2\iapips_backup.sql` - Create(MySQL): 数据库 `iapips2` **Interfaces:** - Consumes: `C:\API2\` 目录(Task 2) - Produces: 数据库 `iapips2`,内含原库全部表与数据(schema 尚未补齐,Task 5 处理) - [ ] **Step 1: 记录原库表数量与关键表行数,供导入后比对** ```bash $SSH "$MYSQL -N -e \"select count(*) from information_schema.tables where table_schema='iapips';\"" 2>&1 | grep -v Warning $SSH "$MYSQL -N -e \"select (select count(*) from iapips.user), (select count(*) from iapips.project), (select count(*) from iapips.material);\"" 2>&1 | grep -v Warning ``` 把这四个数字记下来。 - [ ] **Step 2: 导出原库** ```bash $SSH "$MYSQLDUMP --single-transaction --default-character-set=utf8mb4 --routines --events iapips > C:\API2\iapips_backup.sql" ``` - [ ] **Step 3: 验证 dump 文件非空且以正常结尾** ```bash $SSH "powershell -NoProfile -Command \"(Get-Item 'C:\API2\iapips_backup.sql').Length; (Get-Content 'C:\API2\iapips_backup.sql' -Tail 1)\"" ``` 预期:打印出一个远大于 0 的字节数,最后一行形如 `-- Dump completed on ...`。 若最后一行不是 `Dump completed` → dump 不完整,停止并报告。 - [ ] **Step 4: 建空库并导入** ```bash $SSH "$MYSQL -e \"CREATE DATABASE iapips2 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;\"" 2>&1 | grep -v Warning $SSH "$MYSQL --default-character-set=utf8mb4 iapips2 < C:\API2\iapips_backup.sql" 2>&1 | grep -v Warning ``` - [ ] **Step 5: 比对新库与原库的表数量和行数** ```bash $SSH "$MYSQL -N -e \"select count(*) from information_schema.tables where table_schema='iapips2';\"" 2>&1 | grep -v Warning $SSH "$MYSQL -N -e \"select (select count(*) from iapips2.user), (select count(*) from iapips2.project), (select count(*) from iapips2.material);\"" 2>&1 | grep -v Warning ``` 预期:四个数字与 Step 1 记录的**完全一致**。不一致 → 停止并报告。 - [ ] **Step 6: 确认原库行数没变(证明 dump 是只读的)** ```bash $SSH "$MYSQL -N -e \"select (select count(*) from iapips.user), (select count(*) from iapips.project), (select count(*) from iapips.material);\"" 2>&1 | grep -v Warning ``` 预期:与 Step 1 一致。 - [ ] **Step 7: HEALTHCHECK** --- ## Task 4: 上传后端源码并在服务器上编译 `@prisma/client` 与 `esbuild` 含平台相关原生二进制,本地 `node_modules` 不可拷贝,必须在服务器上 `npm install`。 **Files:** - Create(服务器): `C:\API2\iapip-svr\{src,prisma,fonts}`、`package.json`、`tsconfig.json`、`.npmrc`、`opft.xlsx` - Create(服务器,由 npm/tsc 生成): `C:\API2\iapip-svr\node_modules`、`C:\API2\iapip-svr\dist` **Interfaces:** - Consumes: `C:\API2\iapip-svr\` 空目录(Task 2) - Produces: `C:\API2\iapip-svr\dist\index.js` —— PM2 的启动脚本 - [ ] **Step 1: 上传源码(不含 node_modules / dist / logs / .env)** ```bash cd "$SRC/服务端/iapip-svr" $SCP -r src prisma fonts administrator@47.95.40.218:C:/API2/iapip-svr/ $SCP package.json tsconfig.json opft.xlsx administrator@47.95.40.218:C:/API2/iapip-svr/ [ -f .npmrc ] && $SCP .npmrc administrator@47.95.40.218:C:/API2/iapip-svr/ || echo "no .npmrc, will use default registry" ``` - [ ] **Step 2: 验证上传完整** ```bash $SSH "dir C:\API2\iapip-svr & echo --- & if exist C:\API2\iapip-svr\src\index.ts (echo SRC_OK) & if exist C:\API2\iapip-svr\prisma\schema.prisma (echo PRISMA_OK) & if exist C:\API2\iapip-svr\fonts (echo FONTS_OK)" ``` 预期:三行 `SRC_OK`、`PRISMA_OK`、`FONTS_OK` - [ ] **Step 3: 安装依赖** ```bash $SSH "cd /d C:\API2\iapip-svr && npm install" ``` 预期:以 `added N packages` 收尾。 npmmirror 偶发 `ECONNRESET` / `CONNECT_TIMEOUT`,失败就**重跑同一条命令**,缓存会累积,一般 2–3 次装全。 - [ ] **Step 4: 生成 Prisma Client** ```bash $SSH "cd /d C:\API2\iapip-svr && npx prisma generate" ``` 预期:`Generated Prisma Client (v5.x.x) to .\node_modules\@prisma\client` - [ ] **Step 5: 编译 TypeScript** ```bash $SSH "cd /d C:\API2\iapip-svr && npm run build" ``` 预期:`tsc` 无输出即成功(有报错则打印错误)。 - [ ] **Step 6: 验证编译产物** ```bash $SSH "if exist C:\API2\iapip-svr\dist\index.js (echo DIST_OK) else (echo DIST_MISSING)" ``` 预期:`DIST_OK` - [ ] **Step 7: HEALTHCHECK** --- ## Task 5: 后端配置、schema 补齐、独立 PM2 启动 本任务包含**唯一一个需要用户确认的门禁**(Step 4)。 **Files:** - Create(服务器): `C:\API2\iapip-svr\.env` - Create(服务器): `C:\API2\iapip-svr\ecosystem.config.js` - Create(服务器): `C:\API2\pm2.bat` - Create(MySQL): `iapips2` 中新增三张表 `ForumThread` / `ForumReply` / `ForumThreadLike` **Interfaces:** - Consumes: `C:\API2\iapip-svr\dist\index.js`(Task 4)、数据库 `iapips2`(Task 3) - Produces: 监听 `localhost:6061` 的后端进程,PM2 应用名 `iapip-svr2`,位于 PM2_HOME `C:\API2\.pm2` - [ ] **Step 1: 本地生成 JWT 密钥并写好两个配置文件** 在本地 scratchpad 里生成,再上传——避免远程 shell 的引号转义问题。(`$WORK` 已在"通用变量"中定义。) ```bash JWT=$(node -e "console.log(require('crypto').randomBytes(32).toString('hex'))") echo "生成的 JWT 密钥: $JWT" cat > "$WORK/.env" < "$WORK/ecosystem.config.js" < "$WORK/pm2.bat" <<'EOF' @echo off set PM2_HOME=C:\API2\.pm2 pm2 %* EOF ``` - [ ] **Step 2: 上传三个文件** ```bash $SCP "$WORK/.env" "$WORK/ecosystem.config.js" administrator@47.95.40.218:C:/API2/iapip-svr/ $SCP "$WORK/pm2.bat" administrator@47.95.40.218:C:/API2/ $SSH "type C:\API2\pm2.bat & echo --- & findstr /C:\"iapip-svr2\" /C:\"6061\" /C:\"iapips2\" C:\API2\iapip-svr\ecosystem.config.js" ``` 预期:`pm2.bat` 内容正确;findstr 打印出含 `iapip-svr2`、`6061`、`iapips2` 的行。 **若 `ecosystem.config.js` 里出现不带 `2` 的 `iapips` 数据库名 → 停止,配置写错了。** - [ ] **Step 3: 预览 prisma db push 将要执行的 SQL(只读,不改库)** ```bash $SSH "cd /d C:\API2\iapip-svr && npx prisma migrate diff --from-url \"mysql://root:harvey0425@localhost:3306/iapips2\" --to-schema-datamodel prisma/schema.prisma --script" ``` 预期:输出一段 SQL,应当以 `CREATE TABLE` 为主(`ForumThread`、`ForumReply`、`ForumThreadLike`)。 - [ ] **Step 4: 门禁 —— 把 SQL 交给用户确认** 审读上一步的 SQL。若其中包含任何 `DROP TABLE`、`DROP COLUMN`,或会导致数据丢失的 `MODIFY COLUMN`: > **停止。把完整 SQL 呈报用户,说明哪些语句有丢数据风险,等待用户明确同意后再继续。** > 无论如何**都不要**自行加 `--accept-data-loss`。 若 SQL 只有 `CREATE TABLE` / `CREATE INDEX` / `ADD COLUMN`(可空或有默认值),可直接进入 Step 5。 - [ ] **Step 5: 应用 schema 变更到 iapips2** ```bash $SSH "cd /d C:\API2\iapip-svr && npm run dbpush:prod" ``` (该脚本是 `dotenv -e .env -- npx prisma db push`,`.env` 里的 `DATABASE_URL` 指向 `iapips2`。) 预期:`Your database is now in sync with your Prisma schema.` - [ ] **Step 6: 验证三张新表已建,且原库未被波及** ```bash $SSH "$MYSQL -N -e \"select table_name from information_schema.tables where table_schema='iapips2' and table_name in ('ForumThread','ForumReply','ForumThreadLike','forumthread','forumreply','forumthreadlike');\"" 2>&1 | grep -v Warning $SSH "$MYSQL -N -e \"select count(*) from information_schema.tables where table_schema='iapips' and table_name like 'forum%';\"" 2>&1 | grep -v Warning ``` 预期:第一条打印 3 行表名;第二条打印 `0`(原库**没有**被加上 forum 表)。 - [ ] **Step 7: 用独立 PM2_HOME 启动新后端** ```bash $SSH "cd /d C:\API2\iapip-svr && C:\API2\pm2.bat start ecosystem.config.js --env production" $SSH "cd /d C:\API2\iapip-svr && C:\API2\pm2.bat save" ``` 预期:pm2 打印表格,含一行 `iapip-svr2`,status `online`。 - [ ] **Step 8: 验证后端在 6061 上活着** ```bash $SSH "powershell -NoProfile -Command \"(Invoke-WebRequest -Uri http://localhost:6061/api/test/ping -UseBasicParsing).Content\"" ``` 预期输出:`OK` - [ ] **Step 9: 验证两个 PM2 守护进程互不可见(隔离的核心证据)** ```bash echo "=== 新系统的 pm2(应只有 iapip-svr2)===" $SSH "C:\API2\pm2.bat jlist" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.parse(s).map(x=>x.name)))' echo "=== 原系统的 pm2(应只有 iapip-svr)===" $SSH "pm2 jlist" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.parse(s).map(x=>x.name)))' ``` 预期:第一条打印 `['iapip-svr2']`,第二条打印 `['iapip-svr']`。 **若任一列表里同时出现两个名字 → PM2_HOME 隔离失败,停止并排查 `pm2.bat`。** - [ ] **Step 10: HEALTHCHECK** --- ## Task 6: 前端静态文件与 IIS 站点 **Files:** - Create(服务器): `C:\iapips2\iapip-web\`、`C:\iapips2\iapip-ms\`、`C:\iapips2\fonts\`、`C:\iapips2\fts\` - Create(服务器): `C:\iapips2\web.config` - Create(IIS): 应用池 `iapips2`、站点 `iapips2`、应用 `/iapip-web` 与 `/iapip-ms` - Create(防火墙): 入站规则放行 TCP 8081 **Interfaces:** - Consumes: 本地 `dist/`(Task 1)、后端 6061(Task 5) - Produces: `http://localhost:8081/iapip-web/` 可访问,`http://localhost:8081/api/*` 反代到 6061 - [ ] **Step 1: 上传两个前端产物** ```bash $SSH "mkdir C:\iapips2\iapip-web & mkdir C:\iapips2\iapip-ms" $SCP -r "$SRC/用户端/iapip-web/dist/." administrator@47.95.40.218:C:/iapips2/iapip-web/ $SCP -r "$SRC/管理端/iapip-ms/dist/." administrator@47.95.40.218:C:/iapips2/iapip-ms/ ``` - [ ] **Step 2: 验证上传完整(比对字节数)** ```bash $SSH "powershell -NoProfile -Command \"'web: ' + ((Get-ChildItem C:\iapips2\iapip-web -Recurse -File | Measure-Object Length -Sum).Sum); 'ms: ' + ((Get-ChildItem C:\iapips2\iapip-ms -Recurse -File | Measure-Object Length -Sum).Sum)\"" ``` 预期:两个数字与 Task 1 Step 7 记录的字节数一致。 - [ ] **Step 3: 从原站只读复制 PDF 中文字体目录** ```bash $SSH "xcopy C:\iapips\fonts C:\iapips2\fonts\ /E /I /Y & xcopy C:\iapips\fts C:\iapips2\fts\ /E /I /Y" $SSH "if exist C:\iapips2\fonts (echo FONTS_OK) & if exist C:\iapips2\fts (echo FTS_OK) & if exist C:\iapips\fonts (echo ORIG_FONTS_INTACT)" ``` 预期:`FONTS_OK`、`FTS_OK`、`ORIG_FONTS_INTACT` (`xcopy` 的源是原站目录,只读;目标是新目录。方向不要写反。) - [ ] **Step 4: 写入 web.config(反代到 6061)** ```bash cat > "$WORK/web.config" <<'EOF' EOF $SCP "$WORK/web.config" administrator@47.95.40.218:C:/iapips2/web.config $SSH "type C:\iapips2\web.config | findstr 6061" ``` 预期:打印出含 `localhost:6061` 的那行。 (前端是 hash 路由,不需要 SPA 回退规则;原站也没有。) - [ ] **Step 5: 授予 IIS 应用池读取权限** ```bash $SSH "icacls C:\iapips2 /grant \"IIS_IUSRS:(OI)(CI)RX\" /T" ``` 预期:`Successfully processed N files` - [ ] **Step 6: 建应用池与站点** ```bash APPCMD='C:\Windows\System32\inetsrv\appcmd' $SSH "$APPCMD add apppool /name:iapips2" $SSH "$APPCMD add site /name:iapips2 /bindings:http/*:8081: /physicalPath:C:\iapips2" $SSH "$APPCMD set app \"iapips2/\" /applicationPool:iapips2" ``` 预期:三条依次打印 `APPPOOL object "iapips2" added.` / `SITE object "iapips2" added.` / `APP object "iapips2/" changed.` - [ ] **Step 7: 建两个子应用** ```bash $SSH "$APPCMD add app /site.name:iapips2 /path:/iapip-web /physicalPath:C:\iapips2\iapip-web" $SSH "$APPCMD add app /site.name:iapips2 /path:/iapip-ms /physicalPath:C:\iapips2\iapip-ms" $SSH "$APPCMD set app \"iapips2/iapip-web\" /applicationPool:iapips2" $SSH "$APPCMD set app \"iapips2/iapip-ms\" /applicationPool:iapips2" ``` - [ ] **Step 8: 验证 IIS 结构,且原站点未被改动** ```bash $SSH "$APPCMD list site & echo --- & $APPCMD list app" ``` 预期: - `iapips2` 站点存在,`bindings:http/*:8081:`,`state:Started` - `iapips` 站点仍然 `state:Started`,绑定仍含 `indoorhealthair.com` - 应用列表含 `iapips2/`、`iapips2/iapip-web`、`iapips2/iapip-ms`,且原有的 `iapips/*` 三条一字未变 - [ ] **Step 9: 放行本机防火墙 8081** ```bash $SSH "netsh advfirewall firewall add rule name=\"IAPIP2 HTTP 8081\" dir=in action=allow protocol=TCP localport=8081" ``` 预期:`确定。` 或 `Ok.` - [ ] **Step 10: 服务器本机验证站点与反代** ```bash $SSH "powershell -NoProfile -Command \"'web: ' + (Invoke-WebRequest -Uri http://localhost:8081/iapip-web/ -UseBasicParsing).StatusCode\"" $SSH "powershell -NoProfile -Command \"'ms: ' + (Invoke-WebRequest -Uri http://localhost:8081/iapip-ms/ -UseBasicParsing).StatusCode\"" $SSH "powershell -NoProfile -Command \"'api: ' + (Invoke-WebRequest -Uri http://localhost:8081/api/test/ping -UseBasicParsing).Content\"" ``` 预期:`web: 200`、`ms: 200`、`api: OK` 最后一条是关键——它证明 IIS 的 rewrite 规则把 `/api` 打到了 **6061**(新后端),而不是 6060。 - [ ] **Step 11: 证明新站点的 /api 确实走的是新后端** 新旧后端连的是不同的库。往 `iapips2` 里加一条只存在于新库的论坛帖,再通过 8081 读出来——如果读到了,说明链路确实是 `8081 → 6061 → iapips2`。 更简单的等价验证:确认原后端的 6060 上 `/api/test/ping` 与新站点 8081 的 `/api/test/ping` 都返回 OK,但**停掉新后端后 8081 的 /api 应当失败**: ```bash $SSH "C:\API2\pm2.bat stop iapip-svr2" $SSH "powershell -NoProfile -Command \"try { (Invoke-WebRequest -Uri http://localhost:8081/api/test/ping -UseBasicParsing -TimeoutSec 10).StatusCode } catch { 'FAILED_AS_EXPECTED' }\"" $SSH "powershell -NoProfile -Command \"(Invoke-WebRequest -Uri http://localhost:6060/api/test/ping -UseBasicParsing).Content\"" $SSH "C:\API2\pm2.bat start iapip-svr2" ``` 预期:第二条打印 `FAILED_AS_EXPECTED`(新站点的 /api 依赖新后端),第三条仍打印 `OK`(原后端不受影响)。 最后一条把新后端重新拉起。 - [ ] **Step 12: HEALTHCHECK** --- ## Task 7: 端到端验收 **Files:** 无(纯验证) **Interfaces:** - Consumes: Task 1–6 的全部产出 - Produces: 验收结论 - [ ] **Step 1: 请用户在阿里云控制台放行 8081** > **这一步 Claude 无法代劳。** 请用户到阿里云控制台 → ECS 实例 `iZt3gm4avglu0sZ` → 安全组 → 配置规则 → 入方向 → 添加:协议 TCP,端口范围 `8081/8081`,授权对象 `0.0.0.0/0`(或按需收窄)。 - [ ] **Step 2: 从本地验证外网可达** ```bash curl -s -o /dev/null -w "%{http_code}\n" http://47.95.40.218:8081/iapip-web/ curl -s http://47.95.40.218:8081/api/test/ping ``` 预期:`200`,然后 `OK`。 若超时 → 安全组未生效,回到 Step 1。 (注意:在 Git Bash 里 curl 的路径 `/api/...` 可能被 MSYS 改写,若结果异常改用 `curl -s "http://47.95.40.218:8081/api/test/ping"` 加引号,或在 PowerShell 里跑。) - [ ] **Step 3: 用户端功能验收(人工,浏览器)** 打开 `http://47.95.40.218:8081/iapip-web/`: 1. 用原系统的账号登录(数据是从原库复制的,账号密码一致) 2. 新建或打开一个项目,配置空间与材料,执行一次预测 3. 进入 `/source` 页,确认**分房间柱状图**与**材料贡献饼图**两个新图表正常渲染 4. 确认能下载 PDF 报告(验证 `fonts/` 拷贝正确,中文不乱码) - [ ] **Step 4: 管理端验收(人工,浏览器)** 打开 `http://47.95.40.218:8081/iapip-ms/`,用管理员账号登录,确认材料库与审核列表能正常加载。 - [ ] **Step 5: 数据隔离验收** 在新系统里改一条数据(例如新建一个项目),然后确认原库不受影响: ```bash $SSH "$MYSQL -N -e \"select (select count(*) from iapips.project) as orig, (select count(*) from iapips2.project) as new_;\"" 2>&1 | grep -v Warning ``` 预期:`new_` 比 `orig` 多 1,`orig` 与 Task 3 Step 1 记录的值相同。 - [ ] **Step 6: 原系统最终体检** ```bash curl -s -o /dev/null -w "%{http_code}\n" https://indoorhealthair.com/iapip-web/ ``` 预期:`200` 再执行完整 HEALTHCHECK。特别确认 `iapip-svr` 的 `restart_time` **仍然是 0** —— 这证明整个部署过程中原后端一次都没被重启过。 - [ ] **Step 7: 更新部署记忆** 把新系统的坐标写入 `C:\Users\11825\.claude\projects\C--code\memory\iapip-deploy-server.md`:两套系统的端口/库名/PM2_HOME/站点名对照表,以及"新系统 pm2 必须走 `C:\API2\pm2.bat`"这条操作纪律。 --- ## 回滚 任何阶段失败都可以安全回滚,原系统零改动,无需恢复。 ```bash $SSH "C:\API2\pm2.bat delete iapip-svr2" $SSH "C:\API2\pm2.bat kill" $SSH "C:\Windows\System32\inetsrv\appcmd delete site iapips2" $SSH "C:\Windows\System32\inetsrv\appcmd delete apppool iapips2" $SSH "$MYSQL -e \"DROP DATABASE iapips2;\"" $SSH "netsh advfirewall firewall delete rule name=\"IAPIP2 HTTP 8081\"" $SSH "rmdir /S /Q C:\API2 & rmdir /S /Q C:\iapips2" ``` 执行后跑一次 HEALTHCHECK 确认原系统健在。