=== 2/8 校验产物与静态检查 === ✅ pms-api (12.1 MB) ✅ pms-device-server (2.8 MB) ✅ web/dist (6 个文件) === 3/8 服务器备份(保留旧二进制/.env/nginx/单元/前端) === [DryRun] ssh: set -e TS=$(date +%Y%m%d-%H%M%S) mkdir -p /data/pms/backup-$TS cp -a /data/pms/api-server/pms-api /data/pms/backup-$TS/ 2>/dev/null || true cp -a /data/pms/device-server/pms-device-server /data/pms/backup-$TS/ 2>/dev/null || true cp -a /data/pms/api-server/.env /data/pms/device-server/.env /data/pms/backup-$TS/ 2>/dev/null || true mkdir -p /data/pms/backup-$TS/web && cp -a /data/pms/web/. /data/pms/backup-$TS/web/ 2>/dev/null || true echo $TS > /tmp/pms_last_backup echo " 备份目录: /data/pms/backup-$TS" === 4/8 上传(二进制传 .new,运行中的二进制不可直接覆盖) === [DryRun] scp: D:\charging-cabinet\software\api-server\target\x86_64-unknown-linux-gnu\release\pms-api -> /data/pms/api-server/pms-api.new [DryRun] scp: D:\charging-cabinet\software\device-server\target\x86_64-unknown-linux-gnu\release\pms-device-server -> /data/pms/device-server/pms-device-server.new [DryRun] scp: C:\Users\12451\AppData\Local\Temp\pms-dist.tgz -> /tmp/pms-dist.tgz === 5/8 原子替换并重启服务 === [DryRun] ssh: set -e chmod +x /data/pms/api-server/pms-api.new /data/pms/device-server/pms-device-server.new systemctl stop pms-api pms-device mv -f /data/pms/api-server/pms-api.new /data/pms/api-server/pms-api mv -f /data/pms/device-server/pms-device-server.new /data/pms/device-server/pms-device-server rm -rf /data/pms/web/* && tar -xzf /tmp/pms-dist.tgz -C /data/pms/web/ && rm -f /tmp/pms-dist.tgz systemctl daemon-reload systemctl start pms-device pms-api sleep 8 nginx -t && systemctl reload nginx === 6/8 验收(服务/端口/域名/日志) === [DryRun] ssh: echo '--- 服务 ---'; systemctl is-active pms-api pms-device nginx | paste -sd' ' echo '--- 端口(应为 9000 / 9001)---'; ss -lntp | grep -E ':9000|:9001' | awk '{print " " $4}' echo '--- 域名 ---' curl -sk -m 8 -o /dev/null -w ' /api/pms/cabinets -> %{http_code}\n' https://app.anzhizhichong.com/api/pms/cabinets curl -sk -m 8 -o /dev/null -w ' /pms/ -> %{http_code}\n' https://app.anzhizhichong.com/pms/ echo '--- 启动日志尾部 ---'; journalctl -u pms-api --no-pager -n 4 | tail -4 echo '--- 新进程 ERROR 数(应为 0)---' MAIN=$(systemctl show -p MainPID --value pms-api) journalctl -u pms-api --since '-3 min' --no-pager | grep -c " ERROR " | sed 's/^/ ERROR 行数: /' === 7/8 上报消费链路探测(用不存在的 IMEI,不碰真实设备数据) === === 8/8 回滚点保留策略(只保留最近 5 次 backup-YYYYMMDD-HHMMSS) === [DryRun] ssh: set -e shopt -s nullglob KEEP=5 # glob 字符类只匹配严格的 backup-YYYYMMDD-HHMMSS;nullglob 让无匹配时展开为空数组。 # glob 结果按字典序排列,对该命名格式而言等价于时间戳升序(最早的在前)。 rollbacks=() for p in /data/pms/backup-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]; do if [ -d "$p" ]; then rollbacks+=( "$p" ) fi done total=${#rollbacks[@]} echo " 当前回滚点 $total 个(deploy 会为每次部署新建一份):" for p in "${rollbacks[@]}"; do echo " $p" done if [ "$total" -gt "$KEEP" ]; then drop=$((total - KEEP)) echo " 将删除最早的 $drop 个(保留最近 $KEEP 个):" i=0 for p in "${rollbacks[@]}"; do if [ "$i" -ge "$drop" ]; then break fi i=$((i + 1)) # 复核:名字必须严格符合 backup-YYYYMMDD-HHMMSS,否则拒绝删除(绝不碰其它目录) case "$(basename "$p")" in backup-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]) echo " 删除 $p" rm -rf -- "$p" ;; *) echo " [skip] 名字不符合回滚点模式,拒绝删除: $p" ;; esac done else echo " 回滚点数量未超过 $KEEP,无需清理" fi kept=() for p in /data/pms/backup-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]; do if [ -d "$p" ]; then kept+=( "$p" ) fi done echo " 清理后保留的回滚点 ${#kept[@]} 个:" for p in "${kept[@]}"; do echo " $p" done echo " /data/pms 占用: $(du -sh /data/pms | cut -f1)" === 部署完成 === 正式环境:https://app.anzhizhichong.com/pms/ API :9000 TCP :9001 回滚方法(如需): plink -batch -pw root@118.31.119.206 "systemctl stop pms-api pms-device && cp /data/pms/backup-/* /data/pms/... && systemctl start pms-api pms-device" 备份目录见 /data/pms/backup-<时间戳>/(脚本已把 .env / 前端 / 单元一并备份;只保留最近 5 次)