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

27 KiB
Raw Blame History

第二套独立部署 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_HOMEC:\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-svrC:\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 变量不跨任务保留):

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 源偶发 ECONNRESETpnpm install 失败就重试,缓存会累积,一般 23 次装全。

  • 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.tspublicPath 被改过,停止并报告。

  • 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 上传后要核对。

(本任务无 commitdist/ 是构建产物;若 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:\API2C:\API2\_deployC:\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 SiteStoppediapipsStarted

  • 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_OKIAPIPS2_OKORIG_SVR_INTACTORIG_WEB_INTACT

  • Step 5: HEALTHCHECK

执行本文档开头定义的 HEALTHCHECK 三项。全部通过才进入 Task 3。


Task 3: 复制数据库到 iapips2

全程只读原库。dump 用 --single-transaction,对 InnoDB 不加锁,不影响线上读写。

Files:

  • Create服务器: C:\API2\iapips_backup.sql
  • CreateMySQL: 数据库 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/clientesbuild 含平台相关原生二进制,本地 node_modules 不可拷贝,必须在服务器上 npm install

Files:

  • Create服务器: C:\API2\iapip-svr\{src,prisma,fonts}package.jsontsconfig.json.npmrcopft.xlsx
  • Create服务器由 npm/tsc 生成): C:\API2\iapip-svr\node_modulesC:\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_OKPRISMA_OKFONTS_OK

  • Step 3: 安装依赖
$SSH "cd /d C:\API2\iapip-svr && npm install"

预期:以 added N packages 收尾。 npmmirror 偶发 ECONNRESET / CONNECT_TIMEOUT,失败就重跑同一条命令,缓存会累积,一般 23 次装全。

  • 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
  • CreateMySQL: iapips2 中新增三张表 ForumThread / ForumReply / ForumThreadLike

Interfaces:

  • Consumes: C:\API2\iapip-svr\dist\index.jsTask 4、数据库 iapips2Task 3

  • Produces: 监听 localhost:6061 的后端进程PM2 应用名 iapip-svr2,位于 PM2_HOME C:\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-svr26061iapips2 的行。 ecosystem.config.js 里出现不带 2iapips 数据库名 → 停止,配置写错了。

  • 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 为主(ForumThreadForumReplyForumThreadLike)。

  • Step 4: 门禁 —— 把 SQL 交给用户确认

审读上一步的 SQL。若其中包含任何 DROP TABLEDROP 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-svr2status 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
  • CreateIIS: 应用池 iapips2、站点 iapips2、应用 /iapip-web/iapip-ms
  • Create防火墙: 入站规则放行 TCP 8081

Interfaces:

  • Consumes: 本地 dist/Task 1、后端 6061Task 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_OKFTS_OKORIG_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-webiapips2/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: 200ms: 200api: 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 16 的全部产出

  • 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/

  1. 用原系统的账号登录(数据是从原库复制的,账号密码一致)
  2. 新建或打开一个项目,配置空间与材料,执行一次预测
  3. 进入 /source 页,确认分房间柱状图材料贡献饼图两个新图表正常渲染
  4. 确认能下载 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 多 1orig 与 Task 3 Step 1 记录的值相同。

  • Step 6: 原系统最终体检
curl -s -o /dev/null -w "%{http_code}\n" https://indoorhealthair.com/iapip-web/

预期:200

再执行完整 HEALTHCHECK。特别确认 iapip-svrrestart_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 确认原系统健在。