27 KiB
第二套独立部署 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_HOMEC:\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 变量不跨任务保留):
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)
多个任务结束时要求执行此检查。三项全部通过才算该任务完成:
$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
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 文件:
cat "$SRC/用户端/iapip-web/pnpm-workspace.yaml"
cat "$SRC/管理端/iapip-ms/pnpm-workspace.yaml"
若 allowBuilds 不存在或是占位文字,改成:
allowBuilds:
core-js: true
core-js-pure: true
es5-ext: true
esbuild: true
- Step 3: 构建用户端
cd "$SRC/用户端/iapip-web" && pnpm install && pnpm build
预期:最后打印 Build success(或 umi 的成功摘要),dist/ 目录生成。
npmmirror 源偶发 ECONNRESET,pnpm install 失败就重试,缓存会累积,一般 2–3 次装全。
- Step 4: 验证用户端产物的 publicPath
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: 构建管理端
cd "$SRC/管理端/iapip-ms" && pnpm install && pnpm build
预期:构建成功,dist/ 生成。
- Step 6: 验证管理端产物的 publicPath
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: 记录产物大小,供上传后比对
du -sb "$SRC/用户端/iapip-web/dist" "$SRC/管理端/iapip-ms/dist"
把两个字节数记下来,Task 6 上传后要核对。
(本任务无 commit:dist/ 是构建产物;若 pnpm-workspace.yaml 被改动,在 Task 1 末尾单独提交:)
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: 抓取原系统基线
$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 库不存在
$SSH "netstat -ano | findstr /R \":6061 :8081\""
预期:无输出(退出码非 0 也正常,findstr 无匹配即返回 1)。有输出则端口被占,停止并报告。
$SSH "$MYSQL -e \"show databases;\"" 2>&1 | grep -v Warning
预期输出中没有 iapips2。若已存在 → 停止并报告(可能是上次失败的残留,需先确认能否 DROP)。
- Step 3: 创建目录骨架
$SSH "mkdir C:\API2 & mkdir C:\API2\_deploy & mkdir C:\API2\iapip-svr & mkdir C:\iapips2"
- Step 4: 验证目录已建且原目录未被触碰
$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: 记录原库表数量与关键表行数,供导入后比对
$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: 导出原库
$SSH "$MYSQLDUMP --single-transaction --default-character-set=utf8mb4 --routines --events iapips > C:\API2\iapips_backup.sql"
- Step 3: 验证 dump 文件非空且以正常结尾
$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: 建空库并导入
$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: 比对新库与原库的表数量和行数
$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 是只读的)
$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)
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: 验证上传完整
$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: 安装依赖
$SSH "cd /d C:\API2\iapip-svr && npm install"
预期:以 added N packages 收尾。
npmmirror 偶发 ECONNRESET / CONNECT_TIMEOUT,失败就重跑同一条命令,缓存会累积,一般 2–3 次装全。
- Step 4: 生成 Prisma Client
$SSH "cd /d C:\API2\iapip-svr && npx prisma generate"
预期:Generated Prisma Client (v5.x.x) to .\node_modules\@prisma\client
- Step 5: 编译 TypeScript
$SSH "cd /d C:\API2\iapip-svr && npm run build"
预期:tsc 无输出即成功(有报错则打印错误)。
- Step 6: 验证编译产物
$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_HOMEC:\API2\.pm2 -
Step 1: 本地生成 JWT 密钥并写好两个配置文件
在本地 scratchpad 里生成,再上传——避免远程 shell 的引号转义问题。($WORK 已在"通用变量"中定义。)
JWT=$(node -e "console.log(require('crypto').randomBytes(32).toString('hex'))")
echo "生成的 JWT 密钥: $JWT"
cat > "$WORK/.env" <<EOF
DATABASE_URL="mysql://root:harvey0425@localhost:3306/iapips2"
JWT_SECRET_KEY="$JWT"
IAPIP_SVR_PORT=6061
NODE_ENV=production
EOF
cat > "$WORK/ecosystem.config.js" <<EOF
module.exports = {
apps: [
{
name: "iapip-svr2",
script: "dist/index.js",
exec_mode: "cluster",
instance_var: "INSTANCE_ID",
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: "3G",
log_date_format: "YYYY-MM-DD HH:mm",
env: {
NODE_ENV: "production",
DATABASE_URL: "mysql://root:harvey0425@localhost:3306/iapips2",
JWT_SECRET_KEY: "$JWT",
IAPIP_SVR_PORT: 6061
},
},
],
};
EOF
cat > "$WORK/pm2.bat" <<'EOF'
@echo off
set PM2_HOME=C:\API2\.pm2
pm2 %*
EOF
- Step 2: 上传三个文件
$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(只读,不改库)
$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
$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: 验证三张新表已建,且原库未被波及
$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 启动新后端
$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 上活着
$SSH "powershell -NoProfile -Command \"(Invoke-WebRequest -Uri http://localhost:6061/api/test/ping -UseBasicParsing).Content\""
预期输出:OK
- Step 9: 验证两个 PM2 守护进程互不可见(隔离的核心证据)
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: 上传两个前端产物
$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: 验证上传完整(比对字节数)
$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 中文字体目录
$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)
cat > "$WORK/web.config" <<'EOF'
<?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>
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 应用池读取权限
$SSH "icacls C:\iapips2 /grant \"IIS_IUSRS:(OI)(CI)RX\" /T"
预期:Successfully processed N files
- Step 6: 建应用池与站点
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: 建两个子应用
$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 结构,且原站点未被改动
$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
$SSH "netsh advfirewall firewall add rule name=\"IAPIP2 HTTP 8081\" dir=in action=allow protocol=TCP localport=8081"
预期:确定。 或 Ok.
- Step 10: 服务器本机验证站点与反代
$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 应当失败:
$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: 从本地验证外网可达
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/:
- 用原系统的账号登录(数据是从原库复制的,账号密码一致)
- 新建或打开一个项目,配置空间与材料,执行一次预测
- 进入
/source页,确认分房间柱状图与材料贡献饼图两个新图表正常渲染 - 确认能下载 PDF 报告(验证
fonts/拷贝正确,中文不乱码)
- Step 4: 管理端验收(人工,浏览器)
打开 http://47.95.40.218:8081/iapip-ms/,用管理员账号登录,确认材料库与审核列表能正常加载。
- Step 5: 数据隔离验收
在新系统里改一条数据(例如新建一个项目),然后确认原库不受影响:
$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: 原系统最终体检
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"这条操作纪律。
回滚
任何阶段失败都可以安全回滚,原系统零改动,无需恢复。
$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 确认原系统健在。