CPT交付实施手册
展开步骤目录

网信办食安迁移实施手册

standard_wxb · 麒麟新服务器 · 两套食安 · MySQL 8 迁达梦 · dist 生产包

更新日期:2026-10-20。本文仅覆盖食安项目,供现场实施人员按顺序操作。源库为 foodsafe、foodsafe2;新服务器已安装达梦。软件依赖为 nvm、Node.js 20、pnpm、PM2、Mosquitto,另使用 Nginx 托管前端和反代后端。应用放 /data,包和历史备份按空间规划放 /workspace。

生产发布以 standard_wxb 为唯一代码来源。前端使用四个应用的 dist,后端使用 TypeScript 编译后的完整 dist 和运行依赖;PM2只管理后端,Nginx管理前端。

本次已读取并核对远端 standard_wxb@52c3d9dfad3235b51c113cbeb739f0cff23b74dd(2026-10-19提交)。这只是本手册的代码核对基线,未来发版从该分支选择验收通过的提交,并重新填写发布清单,不能在停机窗口临时取分支最新代码。本文未执行客户服务器安装、迁库或业务测试。

先看迁移安排

阶段 步骤 目的
出发前准备 01—04 冻结版本、盘点、备份、在同版麒麟制备离线运行环境与dist包
新机环境 05—09 安装nvm、Node20、pnpm、PM2、Nginx和Mosquitto
演练迁移及部署 10—17 恢复MySQL副本、升级、迁达梦、部署dist、验证两套业务
正式切换 18—20 停写、最终快照、重迁和切地址
交接维护 21—22 开机恢复、备份、观察及回退

命令采用 Bash。标记“管理员”的命令使用 root/sudo;“foodsafe用户”命令先 sudo -iu foodsafe,完成后 exit。CHANGE_ME 必须替换;NODE_VERSION 等精确版本从经过验收的制品清单填写。文档命令不能整页复制执行,数据库DDL和切换只在确认窗口按步骤执行。

配套文件:现场登记表、运行版本模板、双实例PM2模板、双实例Nginx模板、达梦只读探针。配置模板均不含真实口令。

01|锁定分支、版本和交付边界

位置:管理机/制包机。先完成本步再制包。

cd /实际食安仓库
git status --short --branch
git remote -v
git fetch origin standard_wxb
git log -1 --format='%H %cs %s' origin/standard_wxb

核对origin确为团队食安仓库,分支为standard_wxb。有本地改动先交负责人处理,不覆盖。建议从冻结提交建独立制包工作区:

RELEASE_SHA=CHANGE_ME
[ "$RELEASE_SHA" != CHANGE_ME ] || exit 1
git merge-base --is-ancestor "$RELEASE_SHA" origin/standard_wxb || exit 1
git worktree add --detach /受控制包目录/foodsafe-release-src "$RELEASE_SHA"

在登记表记录SHA、Node20补丁版、nvm/pnpm/PM2/Mosquitto版本、DM版本、架构、包SHA、两套入口及数据库映射。当前分支dmdb和pnpm锁文件均固定为1.0.52452;后端启动脚本为:

node dist/services/gateway-service/src/main.js

本次代码核对发现的发布前检查项:

  • 根目录build只是提示文本,不会生成全部dist;必须逐应用构建。
  • pnpm-workspace.yaml目前有allowBuilds待选择值。先由发布负责人确认匹配的pnpm版本和构建脚本许可,冻结安装必须成功,不能用取消锁文件校验绕过错误。
  • 学校端PublicConfig.vue仍使用固定4211/5209,第二套二维码和跳转存在串到第一套的风险;必须在制包阶段修正/通过测试后再发布第二套。
  • VITE_DEWO_MQTT_HOST若未设置,会回退http://localhost:3300;这个localhost是用户电脑,发布时必须检查相关功能实际使用的可达地址。
  • 该远端核对基线原无db/upgrade/zywxb.sql。2026-10-20已按用户要求从standard@b5d9334084e0c04770174ee2ae558c066b09405b原样复制到本机standard_wxb同路径,当前为未提交文件。它针对foodsafe2,不能仅凭文件已复制就用于第一套或达梦。

完成标准:发布SHA与五项软件的精确版本已冻结;上述影响实际启用功能的检查项已关闭或明确为未交付范围。本文不把分支存在等同于已可上线。

02|盘点旧服务器,固定两套目录与端口

位置:旧机,只读盘点。

cat /etc/os-release
uname -m
node -v
command -v node pnpm pm2 mosquitto nginx
pm2 list
ss -lntp
systemctl list-timers --all
crontab -l
# 在原来运行PM2的用户下查看;root与其他用户的PM2列表互不相同
模块 第一套 第二套 新机运行方式
学校端 4201 4202 Nginx → web-school/dist
监管端 4301 4302 Nginx → web-regulator/dist
大屏 5209 5210 Nginx → screen/dist
移动端 4211 4212 Nginx → mobile-device/dist
网关 3300 3301 PM2 → 编译后的main.js
数据源 foodsafe foodsafe2 分别迁到独立DM用户/同名模式

上述是客户历史端口,现场如已调整,以登记的实际值为准。其他移动端仅在客户实际使用时额外构建和登记,不能因为仓库有目录就默认新增服务。

分别登记旧PM2的进程名、实际cwd、Node路径、环境来源、启动命令;用pm2 describe 进程名在受控终端查看,不公开打印完整环境。记录watchdog/cron、MQTT订阅、设备回调、视频服务、OSS和本地文件、对外URL。MQTT还要记录端口、Client ID/Topic规则、ACL、持久化目录和WebSocket是否使用。

cwd特别重要:当前代码的本地上传写到process.cwd()/data/uploads,AI盒子文件写到process.cwd()/aiboxfile。本手册新PM2固定cwd为各项目根目录,例如/data/apps/foodsafe1;因此迁移后主文件位置应为该根目录下的data/、aiboxfile/。旧进程cwd若为服务子目录,必须按真实旧路径迁移,不能只按仓库目录名猜。

完成标准:两套应用、两个源库、文件、设备和全部自动写入任务有明确对应关系。

03|旧机做演练备份

位置:旧机;正式停机后还会重做最终备份。

创建私有备份目录,备份配置中的口令只保存在客户批准的受控存储:

umask 077
install -d -m 700 /backup/foodsafe-migration/rehearsal
vi /root/foodsafe-mysql8.cnf
chmod 600 /root/foodsafe-mysql8.cnf

连接文件格式如下,填入两个源库所在的真实MySQL8实例:

[client]
host=CHANGE_ME
port=3306
user=CHANGE_ME
password=CHANGE_ME

MySQL8备份命令;若两库不在一个实例,分别备份并在正式停写时保证统一截止:

MYSQLDUMP=/实际MySQL8客户端/bin/mysqldump
BACKUP=/backup/foodsafe-migration/rehearsal
if "$MYSQLDUMP" --defaults-extra-file=/root/foodsafe-mysql8.cnf \
  --single-transaction --quick --routines --triggers --events --hex-blob \
  --default-character-set=utf8mb4 --set-gtid-purged=OFF \
  --databases foodsafe foodsafe2 \
  > "$BACKUP/foodsafe-two.sql.part" 2> "$BACKUP/dump.err"; then
  mv "$BACKUP/foodsafe-two.sql.part" "$BACKUP/foodsafe-two.sql"
else
  echo '备份失败,检查dump.err,不能使用part文件'
  exit 1
fi
sha256sum "$BACKUP/foodsafe-two.sql" > "$BACKUP/dump.sha256"

检查err中的警告;--single-transaction只保证事务表,非事务表需停写或明确锁表方案。备份期间禁止DDL。

程序、配置与文件备份示例,实际目录按第02步:

tar --acls --xattrs -czpf "$BACKUP/apps-and-files.tar.gz" \
  -C /旧应用父目录 food_safety food_safety2
tar -tzf "$BACKUP/apps-and-files.tar.gz" >/dev/null
crontab -l > "$BACKUP/crontab.txt"

还需分别备份:项目根和服务目录.env*、两个cwd下data/和aiboxfile/、其他上传/挂载目录、Nginx、PM2配置/dump、watchdog、MQTT配置/口令文件/ACL/持久状态、证书。采用OSS时核对对象和访问配置,不把数据库中的文件URL误当成本地文件。

完成标准:备份有另一份受控副本,SQL已在隔离MySQL8恢复成功;文件数量和总量可核对。

04|在同版麒麟制包机准备 nvm、Node20、pnpm、PM2

位置:联网制包机;机器的OS、架构、glibc与客户新机匹配。

版本在runtime-versions.env填写。不要只填20或latest,填写v20.x.y等精确值。nvm按用户和shell加载;本次约定运行用户为foodsafe、家目录/var/lib/foodsafe,其他已用账号可替换,但安装、PM2和systemd必须统一。

联网制包机安装步骤:

source /受控制品目录/runtime-versions.env
: "${NVM_VERSION:?填写nvm标签,例如经验证的v0.x.y}"
: "${NODE_VERSION:?填写Node精确版本v20.x.y}"
: "${PNPM_VERSION:?填写pnpm精确版本}"
: "${PM2_VERSION:?填写PM2精确版本}"
export NVM_DIR="$HOME/.nvm"
# 仅全新制包用户,目录不存在时克隆;已有nvm先核验再复用
git clone --branch "$NVM_VERSION" --depth 1 https://github.com/nvm-sh/nvm.git "$NVM_DIR"
. "$NVM_DIR/nvm.sh"
nvm install "$NODE_VERSION"
nvm use "$NODE_VERSION"
nvm alias default "$NODE_VERSION"
npm install -g "pnpm@$PNPM_VERSION" "pm2@$PM2_VERSION"
node -v
pnpm -v
pm2 -v

若制包用户已存在nvm则不重复clone;用实际nvm加载并核对版本。下载源码/二进制必须来自官方或受控镜像,并核对校验和。Node20是本项目兼容基线,升级其他主版本另做适配。

将nvm核心和包含pnpm/PM2的Node安装目录分别打包,避免打进用户家目录、PM2运行日志或凭据:

install -d /受控制品目录/runtime
tar -czf /受控制品目录/runtime/nvm-core.tgz \
  -C "$NVM_DIR" nvm.sh nvm-exec bash_completion
tar -czf /受控制品目录/runtime/node20-with-tools.tgz \
  -C "$NVM_DIR/versions/node/$NODE_VERSION" .

另备Nginx、Mosquitto及其依赖的同版麒麟RPM或审核过源码包、MySQL8客户端、DM管理/迁移工具。客户正式机不因迁移安装MySQL业务服务;临时恢复用独立迁移机。

完成标准:断网同版麒麟可恢复上述环境并执行node、pnpm、pm2;所有软件记录精确版本和SHA。不复制Mac原生依赖到麒麟。

05|新机核对麒麟、达梦与目录

位置:新机管理员;达梦不重装。

cat /etc/os-release
uname -m
ldd --version | head -1
command -v dnf yum apt-get
findmnt -T /data
findmnt -T /workspace
df -h /data /workspace
free -h
timedatectl
systemctl list-unit-files | grep -Ei 'DmService|nginx|mosquitto|pm2'

按实机选择包管理器,不能仅凭“麒麟”套用apt。厂家报告历史配置为32K页、UTF-8、不敏感大小写、MySQL兼容模式4;现场与厂家核对精确build、监听端口、字符长度语义、授权、备份、归档、空间和资源配额。报告中的旧测试授权到期时间不代表现在仍未更新,必须查看当前授权。

现有达梦实例服务共用也可承载食安两模式。模式名称采用厂家正式登记值;前次建议的FOODSAFE_APP/FOODSAFE2_APP是新建时的示例,历史测试环境使用WWWWEB/WWWWEB2,两者不能混用。不要为了名称整齐重建已有业务模式。

id foodsafe || useradd -m -d /var/lib/foodsafe -s /bin/bash foodsafe
install -d -m 755 /data/apps/foodsafe1 /data/apps/foodsafe2
install -d -m 750 -o foodsafe -g foodsafe /data/app-logs/foodsafe1 /data/app-logs/foodsafe2
install -d -m 700 /data/foodsafe-migration
install -d -m 700 /workspace/foodsafe-packages

应用、在线文件和日志优先/data,程序不要放进达梦数据文件目录。两块挂载的实际独立性与剩余容量需核对;旧报告同步写速度不能代替当前实际负载测试。对日志和上传设置容量管理,给数据库增长留余量。

完成标准:厂家实例正常、目标账号映射明确、软件包匹配新机、目录权限可控。

06|在新机安装 nvm 和 Node20

位置:新机管理员传包,然后切换foodsafe用户安装。

将已校验运行包传到/workspace/foodsafe-packages;因其为私有包目录,管理员先把纯软件运行包复制到该用户可读的目录:

install -d -m 750 -o foodsafe -g foodsafe /var/lib/foodsafe/install
install -m 640 -o foodsafe -g foodsafe /workspace/foodsafe-packages/runtime/nvm-core.tgz /var/lib/foodsafe/install/
install -m 640 -o foodsafe -g foodsafe /workspace/foodsafe-packages/runtime/node20-with-tools.tgz /var/lib/foodsafe/install/
install -m 640 -o foodsafe -g foodsafe /workspace/foodsafe-packages/runtime-versions.env /var/lib/foodsafe/install/
sudo -iu foodsafe

以下是foodsafe用户命令:

source "$HOME/install/runtime-versions.env"
[[ "$NODE_VERSION" =~ ^v20\.[0-9]+\.[0-9]+$ ]] || exit 1
export NVM_DIR="$HOME/.nvm"
# 全新安装位置;已有文件先核对,不覆盖别的项目运行环境
mkdir -p "$NVM_DIR/versions/node/$NODE_VERSION"
tar -xzf "$HOME/install/nvm-core.tgz" -C "$NVM_DIR"
tar -xzf "$HOME/install/node20-with-tools.tgz" -C "$NVM_DIR/versions/node/$NODE_VERSION"
. "$NVM_DIR/nvm.sh"
nvm use "$NODE_VERSION"
nvm alias default "$NODE_VERSION"
node -v
node -p 'process.platform + " " + process.arch'

用编辑器在该用户.bashrc添加一次:

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"

用command -v nvm核验,nvm是shell函数,不用which nvm判定是否安装。若Node报glibc版本不符,回到同版麒麟制包机修正运行包,不在客户机替换系统glibc。

完成标准:新登录会话可执行nvm并使用指定Node20;记录command -v node完整路径。

07|核验 pnpm/PM2,并固定生产启动路径

位置:foodsafe用户。

export NVM_DIR="$HOME/.nvm"
. "$NVM_DIR/nvm.sh"
source "$HOME/install/runtime-versions.env"
nvm use "$NODE_VERSION"
pnpm --version
pm2 --version
command -v node pnpm pm2
npm list -g --depth=0

pnpm/PM2已由第04步装进固定Node运行目录,离线新机不必执行在线npm安装。若缺失,退回制包检查;不要现场安装latest掩盖不完整运行包。pnpm用于依赖管理和制包检查,dist已带齐依赖时客户现场无需重新构建前端。

生产systemd不能假定自动加载.bashrc。管理员创建一个固定别名指向该Node版本(先确认目标不存在):

# 回到管理员会话,NODE_VERSION由运行清单设置
source /workspace/foodsafe-packages/runtime-versions.env
test -x "/var/lib/foodsafe/.nvm/versions/node/$NODE_VERSION/bin/node" || exit 1
test ! -e /opt/foodsafe-node || { echo '已有别名,先核对'; exit 1; }
ln -s "/var/lib/foodsafe/.nvm/versions/node/$NODE_VERSION" /opt/foodsafe-node
/opt/foodsafe-node/bin/node -v

PM2本体路径为/opt/foodsafe-node/lib/node_modules/pm2/bin/pm2,食安后端解释器固定/opt/foodsafe-node/bin/node。以后更换Node需重新核验启动服务和依赖,不能只改nvm默认版本。

完成标准:nvm、Node20、pnpm、PM2都存在,版本和运行用户与清单一致;尚未启动正式业务。

08|安装或复用 Nginx

位置:新机管理员。

若前厅已在新机装好Nginx,食安增加vhost即可,不安装第二个服务抢端口。先检查:

command -v nginx
ps -ef | grep '[n]ginx'
ss -lntp

无Nginx时使用客户麒麟源或同版离线RPM;例如yum系列离线安装形态:

yum --disablerepo='*' install /workspace/foodsafe-packages/nginx-rpms/*.rpm
nginx -v
nginx -t

包内依赖须齐全,不使用--nodeps。如果已是LNMP/宝塔,后续命令用实际/usr/local/nginx/sbin/nginx或/www/server/nginx/sbin/nginx,配置目录也按实际include定位。记录worker用户,保证它能穿越/data/apps目录并读取dist。

完成标准:仅一套Nginx运行、nginx -t通过、现有站点仍正常。食安vhost到第16步安装。

09|安装与迁移 Mosquitto

位置:新机管理员;真实设备接入留到正式切换。

先核对旧机是否已有共享Broker。食安需要MQTT软件环境,但不能因此新建一个Broker替代前厅原共享服务;已有合适Broker就迁配置和验证,避免重复占用1883/9001。以下示例用于新建独立Broker。

优先使用同版受控RPM。若按此前有道笔记采用源码路线,准备冻结的Mosquitto源码及对应编译依赖;版本变量来自运行清单:

source /workspace/foodsafe-packages/runtime-versions.env
yum --disablerepo='*' install /workspace/foodsafe-packages/mqtt-build-rpms/*.rpm
install -d /opt/foodsafe-install
tar -xzf "/workspace/foodsafe-packages/runtime/mosquitto-$MOSQUITTO_VERSION.tar.gz" -C /opt/foodsafe-install
cmake -S "/opt/foodsafe-install/mosquitto-$MOSQUITTO_VERSION" \
  -B "/opt/foodsafe-install/mosquitto-$MOSQUITTO_VERSION/build-foodsafe" \
  -DWITH_WEBSOCKETS=ON
cmake --build "/opt/foodsafe-install/mosquitto-$MOSQUITTO_VERSION/build-foodsafe" -j4
cmake --install "/opt/foodsafe-install/mosquitto-$MOSQUITTO_VERSION/build-foodsafe"

每条编译命令成功才执行下一条,libwebsockets必须实际被找到;仅需要TCP时可按客户确认禁用WebSocket,但客户端用ws/wss就必须验证WebSocket功能。源码库若安装到/usr/local/lib64,登记实际目录:

find /usr/local/lib /usr/local/lib64 -name 'libmosquitto.so*' 2>/dev/null
# 下面以lib64为例,必须与实际结果一致
printf '%s\n' /usr/local/lib64 > /etc/ld.so.conf.d/foodsafe-mosquitto.conf
ldconfig
ldd /usr/local/bin/mosquitto_sub
id mosquitto || useradd -r -s /sbin/nologin mosquitto
install -d -m 750 -o mosquitto -g mosquitto /var/lib/mosquitto /var/log/mosquitto
install -d -m 750 -o root -g mosquitto /etc/mosquitto

不把libmosquitto覆盖到系统lib64。从旧机迁口令文件和ACL,保留现有认证;若从零建账号,mosquitto_passwd -c /etc/mosquitto/passwd 实际业务用户交互输入密码,-c只用于不存在的新文件。已有文件新增用户去掉-c。

配置模板(示例端口按旧系统登记值修改):

# /etc/mosquitto/mosquitto.conf
pid_file /run/mosquitto/mosquitto.pid
persistence true
persistence_location /var/lib/mosquitto/
log_dest file /var/log/mosquitto/mosquitto.log
allow_anonymous false
password_file /etc/mosquitto/passwd
acl_file /etc/mosquitto/acl
listener 1883
protocol mqtt
listener 9001
protocol websockets

保留客户原有TLS监听及证书,不因迁移降级。已有设备可能共用Topic:新机演练不能与旧机同时订阅并处理正式消息;两套食安的Client ID和Topic归属需核对,不能盲目复制相同ID。

源码安装的systemd示例写到/etc/systemd/system/mosquitto.service;发行版已有服务时检查并用drop-in调整,不重复创建:

[Unit]
Description=FoodSafety Mosquitto
After=network.target
[Service]
Type=simple
User=mosquitto
Group=mosquitto
RuntimeDirectory=mosquitto
RuntimeDirectoryMode=0755
ExecStart=/usr/local/sbin/mosquitto -c /etc/mosquitto/mosquitto.conf
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target

口令、ACL文件设root:mosquitto和640;/run/mosquitto由systemd每次启动创建。执行systemctl daemon-reload、systemctl enable --now mosquitto后查状态和日志。验证TCP认证、ACL、专用测试Topic消息收发、WebSocket101握手和重启后重连。口令不放-P参数进入进程列表;用受控客户端配置文件或批准工具完成认证测试。

完成标准:协议、认证、订阅和重启均通过;演练Broker只对测试网段可达。

10|制包机从 standard_wxb 构建全部 dist

位置:第01步冻结的制包工作区,使用匹配麒麟运行环境。

cd /受控制包目录/foodsafe-release-src
export NVM_DIR="$HOME/.nvm"
. "$NVM_DIR/nvm.sh"
source /受控制品目录/runtime-versions.env
nvm use "$NODE_VERSION"
test "$(pnpm --version)" = "$PNPM_VERSION" || exit 1
pnpm install --frozen-lockfile

遇到锁文件不匹配/构建脚本授权失败,回发布负责人修正并提交后重新冻结,不能改用--no-frozen-lockfile或现场自由升级依赖。制包目录不得携带真实.env;当前分支曾跟踪环境文件,只允许制包所需的脱敏构建配置进入制包环境,不能打入客户包。

逐项执行并保留退出码,任一步失败即停止:

pnpm --dir services/gateway-service run build
pnpm --dir services/gateway-service run test
VITE_API_BASE=/api pnpm --dir apps/web-school run build
VITE_API_BASE=/api pnpm --dir apps/web-regulator run build
VITE_API_BASE=/api pnpm --dir apps/screen run build
VITE_API_BASE=/api pnpm --dir apps/mobile-device run build

不能用根目录pnpm build替代这些命令。若前端需要实际VITE_DEWO_MQTT_HOST、视频或其他配置,按客户第一/第二套分别设置,不能让localhost落入浏览器请求;固定端口二维码问题必须在冻结版本中解决并验证。后端可以两套共用相同编译物;前端是否同包取决于是否已消除实例专属地址,不能未经验证宣称一包通用。

产物检查:

test -f services/gateway-service/dist/services/gateway-service/src/main.js || exit 1
for app in web-school web-regulator screen mobile-device; do
  test -s "apps/$app/dist/index.html" || exit 1
done
node -p 'require("./services/gateway-service/package.json").scripts.start'
node -p 'require("./services/gateway-service/package.json").dependencies.dmdb'

固定入口为嵌套dist路径,与当前package.json一致。不接受“tsc报错但有文件就部署”。完整dist保留生成的libs等目录,不能只复制main.js。

完成标准:后端build/test和四前端build均通过,实例专属URL已验,dist加载和关键接口在同版麒麟演练通过。本文没有替代执行该分支全量测试。

11|制作和检查离线发布包

位置:制包机。

客户包应包含:四前端dist、服务完整dist、必要package.json、完整运行依赖、运行时读取的资源/模板/脚本/libs,以及版本清单。不包含.git、任何.env*、客户data/aiboxfile、日志、生产dump、浏览器缓存或凭据。

pnpm工作区依赖通过符号链接连接到根node_modules/.pnpm。最稳妥的本次制包路线是保留同版麒麟已安装依赖的完整链接闭包(根和各工作区node_modules及被链接的libs),不只拷贝gateway子目录。可保留开发依赖以确保首次制品完整,后续优化体积再做独立验证。若用pnpm deploy裁剪,先按锁定pnpm版本和工作区依赖验证,不能假设某个deploy命令永远适用。

在单独脱敏制品目录整理好文件后打包,打包前检查:

cd /受控制品目录/foodsafe-dist
find . -name '.env*' -o -name '*.key' -o -name '*.pem'
find . -xtype l
# 上述输出须逐项审查;find -xtype l出现断链即退回修包
test -s services/gateway-service/dist/services/gateway-service/src/main.js || exit 1

首条可能列出仅含占位符的example模板,需审阅确认;私钥不能入包。不要用忽略.env的方法误删某些依赖自身必需资源,最终清单由制包负责人审核。

在脱敏制品根执行依赖探针:

node - <<'JS'
const {createRequire}=require('module');
const path=require('path');
const req=createRequire(path.resolve('services/gateway-service/package.json'));
for(const m of ['dmdb','dotenv','@nestjs/core','express','mysql2','mqtt']) {
  req(m); console.log(m,'OK');
}
JS

再校验实际运行所需模板、动态导入和图片等文件。打包和传输:

cd /受控制品目录
tar -czf foodsafe-dist-release.tgz -C foodsafe-dist .
sha256sum foodsafe-dist-release.tgz > foodsafe-dist-release.tgz.sha256
tar -tzf foodsafe-dist-release.tgz >/dev/null

两套前端如果有差异,包名和manifest明确标注第一/第二套;不要覆盖同名包。客户收到后校验SHA并在空暂存目录解压,不覆盖正在运行的程序。

完成标准:断网麒麟上无需在线安装即可启动;无缺失模块/断链,无客户密钥混入。

12|隔离恢复 MySQL8,并形成两库升级包

位置:独立迁移机。旧生产库保持原版本。

用与源相匹配的MySQL8恢复第03步dump;禁止业务程序访问该临时实例,管理员关闭事件调度:

SET GLOBAL event_scheduler=OFF;
MYSQL=/实际MySQL8客户端/bin/mysql
"$MYSQL" --defaults-extra-file=/受控路径/stage8.cnf \
  -e 'SELECT @@hostname,@@port,VERSION();'
# 确认为隔离实例后才导入,dump中的USE不能被命令末尾库名覆盖
"$MYSQL" --defaults-extra-file=/受控路径/stage8.cnf \
  < /受控备份目录/foodsafe-two.sql > /受控记录目录/restore.log 2>&1
echo $?

退出码非0停止,不加--force忽略错误。记录两库每表精确行数、列、索引、外键、触发器、事件、例程、字符集、SQL_MODE和实际大字段长度。

按冻结standard_wxb版本比对并生成foodsafe-upgrade-reviewed.sql和foodsafe2-upgrade-reviewed.sql,或记录无需升级。本机已从standard取回db/upgrade/zywxb.sql,637行、SHA256为6e27142d94e002f97a94b0be85468aa1ef4e8b2252ae4e84f3d5314c5dd8e727。该文件当前为本地未提交复制件,按foodsafe2历史结构生成,包含USE foodsafe2与当前库名断言;先在恢复副本核对它与冻结发布版的差异,再作为第二套升级候选。第一套仍需独立比对,不能只改一个库名。入正式发布包前将审核后的SQL与发布清单一起冻结。

sha256sum /受控升级包/foodsafe2-upgrade-reviewed.sql
"$MYSQL" --defaults-extra-file=/受控路径/stage8.cnf \
  --show-warnings --binary-mode=1 foodsafe2 \
  < /受控升级包/foodsafe2-upgrade-reviewed.sql \
  > /受控记录目录/upgrade-foodsafe2.log 2>&1
echo $?

第一套独立执行其脚本,失败即停,按已完成编号续跑;MySQL DDL不能依赖事务回滚。升级包含必要的字典/权限/配置数据,不只新增表字段。客户特有字段、AI记录payload可空等语义以源库和冻结代码核实,不收窄或猜补历史值。

完成标准:两库升级后与发布版所需结构一致,原数据保留,增量变化有记录;导出升级后快照和逐表基线供达梦迁移。

13|准备两组达梦用户、模式与表空间

位置:新机达梦,数据库管理员执行。

沿用厂家已装实例,两个源库各有独立用户/同名模式和业务表空间。例如:

源库 新建时建议用户/模式 建议表空间
foodsafe FOODSAFE_APP FOODSAFE_TBS
foodsafe2 FOODSAFE2_APP FOODSAFE2_TBS

名称以第05步确认值为准,不能把历史测试WWWWEB映射套到客户生产。一个实例共用端口,两模式同名表互不冲突;账号默认模式决定访问哪套表。实例维护和实例级物理恢复仍同时影响两套。

先由管理员查已有对象归属、目标表是否为空、空间和权限。创建语法示例,只有完成路径/容量/密码审核且确认不存在目标后才执行:

-- SIZE等按当前版本单位核对;示例2048MB并非现场容量定值
CREATE TABLESPACE "FOODSAFE_TBS"
DATAFILE '/CHANGE_ME_APPROVED_DIR/FOODSAFE_TBS.DBF'
SIZE 2048 AUTOEXTEND ON NEXT 128 MAXSIZE 32768;
CREATE USER "FOODSAFE_APP" IDENTIFIED BY "CHANGE_ME_PRIVATE_PASSWORD"
DEFAULT TABLESPACE "FOODSAFE_TBS"
DEFAULT INDEX TABLESPACE "FOODSAFE_TBS";
GRANT CREATE SESSION TO "FOODSAFE_APP";
GRANT RESOURCE TO "FOODSAFE_APP";

第二套独立创建对应组。创建用户自动产生同名模式,不重复建模式;RESOURCE用于需要建对象的迁移/运行路径,正式权限按代码实际调用收敛,不授DBA。现有代码仍可能检查/创建特定对象,不能未验证就假定可以去掉所有DDL权限。

完成标准:两账号登录正常、仅访问本组对象,表和索引位于本组业务表空间;不导入SYSDBA或MAIN,不覆盖已有数据。

14|将升级后两库迁至达梦并对账

位置:迁移机连接临时MySQL8和核准DM目标。

采用已演练的达梦DTS或Codex迁移程序。DTS操作:新建工程 → 其他数据库迁移到达梦 → MySQL至DM → 源端指定一个临时库 → 目标端指定对应业务账号 → 核对目的模式/表空间 → 审核类型和对象映射 → 分阶段迁移 → 查看每对象失败明细。第二库另建任务,避免沿用第一套账号。

迁移顺序:表结构 → 数据 → 普通索引 → 外键/注释 → 特殊对象 → 自增/序列核验 → 独立对账。主键/唯一支撑索引单独分类,避免重复建索引;外键不得引用第一套或旧模式。MySQL dump不能直接执行到DM。

Codex迁移需提供最终源dump、SHA、源版本、发布SHA、目标参数和模式映射,再形成客户专属可执行包。本手册不是通用转换器,不能凭开发库快照推算客户表数。services/gateway-service/migrations/dm/001_aibox_aisle.sql等DM专用迁移只在核对“未应用/已应用/部分应用”后按需执行。

逐项核验:

项目 验收要求
表、列、约束 对应升级后基线和批准类型映射,无漏表漏列,必需表和列见dm-required-tables.ts
数据 每表精确COUNT相同,关键人员/记录/台账/库存/审计汇总一致
类型 中文字符容量、unsigned、大ID、DECIMAL、日期与时区、空串/NULL正确
长文本 JSON/CLOB/BLOB空值、最大字节长度和样本摘要匹配
新增 下一次自增不会撞ID,主子表关联和回滚正常
隔离 第二套没有引用第一套数据;账号不获无关模式权限

本分支dmdb固定1.0.52452,项目曾发现该版本批量长文本推断造成截断。迁移导入对达到32767 UTF-8字节的长字符串按已演练的单行/大字段路径处理;仅声明CLOB不足以当作验证。大整数传输不得先转JS Number再“恢复”。

完成标准:独立只读复验全部对账,差异为零或有批准的升级解释;表数或应用启动成功不能替代业务数据验收。

15|新机部署 dist、客户配置及文件

位置:新机管理员。演练只连演练模式。

在空暂存目录校验、解压发布包,检查顶层目录没有重复嵌套:

cd /workspace/foodsafe-packages
sha256sum -c foodsafe-dist-release.tgz.sha256
install -d -m 750 /data/foodsafe-migration/staging
tar -xzf foodsafe-dist-release.tgz -C /data/foodsafe-migration/staging
test -f /data/foodsafe-migration/staging/services/gateway-service/dist/services/gateway-service/src/main.js || exit 1

确认包中无客户配置和运行数据后复制到新机两套目录;第二套前端若专包则用第二套暂存来源:

rsync -a --exclude='.env*' --exclude='/data/' --exclude='/aiboxfile/' --exclude='/logs/' \
  --exclude='/services/gateway-service/data/' --exclude='/services/gateway-service/aiboxfile/' \
  --exclude='/services/gateway-service/logs/' \
  /data/foodsafe-migration/staging/ /data/apps/foodsafe1/
rsync -a --exclude='.env*' --exclude='/data/' --exclude='/aiboxfile/' --exclude='/logs/' \
  --exclude='/services/gateway-service/data/' --exclude='/services/gateway-service/aiboxfile/' \
  --exclude='/services/gateway-service/logs/' \
  /data/foodsafe-migration/staging2/ /data/apps/foodsafe2/

本例以/开头的rsync排除项相对于传输根定位应用数据,不会排除依赖内部同名data目录。暂存第二套若使用相同已验收制品,可将staging2替换为staging;若前端有实例专属差异,必须使用对应专包。不要使用--delete。

当前分支从cwd/services/gateway-service/.env开始查找,存在即停止;否则才读cwd/.env,不自动加载.env.local。本手册固定PM2 cwd为项目根,故只将每套正式配置放在:

/data/apps/foodsafe1/services/gateway-service/.env
/data/apps/foodsafe2/services/gateway-service/.env

从各自旧配置保留JWT/设备签名/存储/视频/公共访问等私有值,修改DB部分:

NODE_ENV=production
PORT=3300
DB_KIND=dm
DM_HOST=CHANGE_ME
DM_PORT=5236
DM_USER=FOODSAFE_APP
DM_PASSWORD=CHANGE_ME
DB_AUTO_MIGRATE=0
DB_SYNC_COMMENTS=0
DEMO_SEED=false
DEMO_AI_JOB_ENABLED=0
AIBOX_SCHEDULER_ENABLED=0
DEWO_CALLBACK_SUBSCRIBE_ENABLED=0
YLT_TOKEN_REFRESH_ENABLED=0

第二套为3301和其独立DM_USER/密码;其他相同字段不是复制第一套私有配置的理由。前四个任务开关在当前代码中已核对;它们不能覆盖所有外呼,演练仍需限制真实设备/第三方出口。演示AI生成任务正式默认关闭,其他任务切换时按实际使用恢复。

DB_KIND=dm时仅读取DM_HOST/DM_PORT/DM_USER/DM_PASSWORD,当前代码没有单独DM_SCHEMA配置;默认模式由登录用户决定。当前DM bootstrap验证必需表列并跳过MySQL迁移和种子;缺表会报错,应补迁移包,不删除检查。显式进程环境优先于文件,PM2不得残留另一套DB_KIND/DM_USER覆盖文件。

迁文件时将旧cwd的data、aiboxfile分别恢复到新项目根同名目录,冲突文件按来源和校验和处理;根目录和服务目录若都有历史文件,先列清单再合并,不盲目覆盖。Nginx/程序中的文件URL、OSS开关和路径也要核对。

权限示例:

for n in 1 2; do
  root="/data/apps/foodsafe$n"
  install -d -m 750 -o foodsafe -g foodsafe "$root/data" "$root/aiboxfile"
  chown foodsafe:foodsafe "$root/services/gateway-service/.env"
  chmod 600 "$root/services/gateway-service/.env"
done

恢复后的数据文件需核对UID/GID并赋予foodsafe访问权;源码和dist由发布账号管理,Nginx只需可读,不全目录777。

完成标准:每套一个生效配置,cwd与文件目录一致,包和文件不串套,未加载旧MySQL连接或演练数据。

16|配置 PM2 与 Nginx生产服务

位置:新机。先限制在演练网络,不启用正式业务订阅。

将配套foodsafe-ecosystem.cjs.example审阅后复制为/data/apps/foodsafe-ecosystem.cjs;核心内容:

module.exports = {
  apps: [1, 2].map(n => ({
    name: `foodsafe-${n}`,
    cwd: `/data/apps/foodsafe${n}`,
    script: 'services/gateway-service/dist/services/gateway-service/src/main.js',
    interpreter: '/opt/foodsafe-node/bin/node',
    env: {NODE_ENV: 'production', PORT: n === 1 ? '3300' : '3301'},
    instances: 1, exec_mode: 'fork', autorestart: true,
    out_file: `/data/app-logs/foodsafe${n}/out.log`,
    error_file: `/data/app-logs/foodsafe${n}/error.log`,
    time: true
  }))
};

在foodsafe用户的新会话加载nvm并启动:

export NVM_DIR="$HOME/.nvm"
. "$NVM_DIR/nvm.sh"
nvm use default
pm2 start /data/apps/foodsafe-ecosystem.cjs
pm2 list
pm2 logs foodsafe-1 --lines 50 --nostream
pm2 logs foodsafe-2 --lines 50 --nostream

如果之前试启同名进程且注入过错误DB环境,先保存私有配置,停掉并按明确清单移除本项目两个PM2进程,再从干净环境重建;不要pm2 delete all,也不要以root起第二份进程。期望日志包含DM schema verified; MySQL bootstrap skipped,但这只代表核心结构门禁通过。

安装Nginx模板到实际http{}下的vhost include目录。模板有两个upstream、8个静态站点;第一套反代3300,第二套3301。旧配置存在时先备份并审查重复端口,不能直接叠加:

NGINX=/实际nginx二进制
"$NGINX" -t
# 例:目录已核对确实被主配置include后安装
install -m 644 /workspace/foodsafe-packages/checks/foodsafe-nginx.conf.example \
  /实际nginx-vhost目录/foodsafe.conf
"$NGINX" -t
# 仅上条成功才重载
"$NGINX" -s reload

/api/按原配置去前缀,/school/等保留前缀,WebSocket保留Upgrade;普通HTTP的Connection映射为'' '',避免旧包'' close产生200/400交替。前端dist只由Nginx托管,不使用Vite dev/preview。请求体限制与BODY_LIMIT_MB/UPLOAD_LIMIT_MB按原客户配置协调,模板50m不是默认放大业务限制。

for port in 4201 4301 5209 4211 4202 4302 5210 4212; do
  curl --fail -sS -o /dev/null -w "$port %{http_code}\n" "http://127.0.0.1:$port/"
done
curl -sS http://127.0.0.1:3300/health
curl -sS http://127.0.0.1:3301/health
ss -lntp | grep -E ':(3300|3301|4201|4202|4301|4302|5209|5210|4211|4212)\b'

当前代码有GET /health,它本身不查询数据库;不能用其ok当作数据库验收,也不假设存在/health/db。使用下一步探针和实际业务查询。

17|验证真实数据库、两套隔离及文件/设备链路

位置:新机演练环境,使用测试账户/设备。

配套只读探针放到管理员批准的位置,foodsafe用户在对应项目根执行;它按当前代码主配置路径加载,不接受.env.local替代:

cd /data/apps/foodsafe1
/opt/foodsafe-node/bin/node /受控工具目录/dm-readonly-probe.cjs
cd /data/apps/foodsafe2
/opt/foodsafe-node/bin/node /受控工具目录/dm-readonly-probe.cjs

探针只执行SELECT 1 FROM DUAL、当前用户和本模式表计数,不输出口令。通过后仍要分别测试:登录/角色/食堂范围,人员健康证,晨检/留样/消毒/农残,台账/库存/报表导出,图片上传与旧图片,AI盒子,视频、大屏及设备回调。

第一套从学校端生成移动端/大屏二维码,确认4211/5209;第二套确认4212/5210。浏览器Network不得向本机localhost3300或另一套发请求。设备上报和文件路径使用新机可达地址,不能把服务端回环地址发给设备。

写测试使用演练模式和登记测试数据;两套分别新增唯一测试记录,核对仅落本模式/本文件目录,账号不能读取无关模式。测试后不得将演练夹具带入正式迁移。DM adapter当前每网关最大池连接10,两套合计按实际连接池和数据库会话能力核验,不照抄全机资源调参。

MQTT验证TCP与WebSocket、认证、Topic权限、重连及消息只处理一次;视频验证实际播放而不是仅有URL。记录MySQL恢复、升级、DM导入、文件传输、对账和验收耗时,再确定停机窗口。

完成标准:两套核心业务和文件/设备链路通过,数据差异已解释,正式授权和可恢复备份具备,才进入停机迁移。

18|正式停机,取得最终两库与文件快照

位置:旧机;客户批准的停机窗口。

  1. 通知用户停止录入,记录截止时间。
  2. 按旧机清单暂停watchdog,再停止本项目后端、定时任务、设备回调写入、MQTT消费者和第三方同步;保留共享服务其他业务。
  3. 等待在途请求完成,确认MySQL两库不再新增;设备离线缓存/补传策略单独记录。
  4. 使用第03步相同命令重新备份两库,目录改为/backup/foodsafe-migration/final,生成新SHA。
  5. 最终同步各自文件、需保留的MQTT持久状态等;新旧设备不能同时处理同一批消息。

文件同步示例,先dry-run再执行:

rsync -an --itemize-changes /旧机实际cwd/data/ 迁移账号@新机:/data/apps/foodsafe1/data/
rsync -a  --itemize-changes /旧机实际cwd/data/ 迁移账号@新机:/data/apps/foodsafe1/data/
rsync -an --itemize-changes /旧机实际cwd/aiboxfile/ 迁移账号@新机:/data/apps/foodsafe1/aiboxfile/
rsync -a  --itemize-changes /旧机实际cwd/aiboxfile/ 迁移账号@新机:/data/apps/foodsafe1/aiboxfile/

第二套使用它自己的旧cwd和新目录。不能只拷一套文件给两套使用;不加--delete删除未知数据。MQTT持久库文件迁移需按Broker停服/一致性方式完成,不在运行时复制后假设完整。

完成标准:最终备份、文件和消息截止一致;旧机未升级结构且持续停写,可以回退。

19|使用最终快照重做迁移并切正式配置

位置:迁移机和新机。

重复第12—14步:干净临时MySQL恢复最终快照 → 相同SHA的逐库升级 → 迁到核准正式DM目标 → 独立逐表和业务对账。不能用上周演练数据直接切换,也不向留有夹具的库盲目追加。

正式目标已有对象时由管理员确认归属和备份后处理;不自动DROP用户。导入失败保留现场,按演练过的分段续跑或重建策略处理。对账通过后更新每套.env到正式DM用户,停止后重启本项目进程,让旧连接池退出。

用第17步探针、真实登录和业务读回确认最终模式。再次核验源升级后基线与目标DM的表、列、行数、附件路径和下一次自增行为。正式写入放行前排除演练数据。

完成标准:两个正式目标与最终源一致,每套进程实际访问正确模式,文件可读,旧机仍停写。

20|切换地址、网络及正式设备任务

位置:新旧机和客户网络管理端。

按预定方式切域名/代理/NAT至新机,或旧机释放IP后新机接管,禁止两机同IP。保留旧机管理通道。按登记表更新设备服务地址、二维码、视频、回调白名单和文件域名。

防火墙按业务网段放行前端、按设备网段放行实际3300/3301和MQTT端口;达梦仅应用/管理来源。当前后端app.listen(port)可能监听所有网卡,必须用ss及来源实测确认边界,不能认为Nginx走127.0.0.1就已经限制后端。

逐项恢复正式需要的后台任务:AI盒子调度、得沃回调订阅、令牌刷新等;示例演示AI任务保持关闭。新任务启用前确认旧机对应项已停;共享MQTT Broker不为食安迁移贸然重启前厅连接。检查补传与重复消息处理,避免重复建档或重复记录。

先小范围用户和测试设备验证,再逐批接入全部设备。两套都验,不用第一套通过代替第二套。API不仅看HTTP200,还核响应业务状态和数据库新增记录。

完成标准:访问和设备流量进入新机、旧机无写入、新记录与附件落在各自模式和目录。

21|保存 PM2、设置开机恢复与备份

位置:先foodsafe用户,再管理员。

# foodsafe用户
export NVM_DIR="$HOME/.nvm"
. "$NVM_DIR/nvm.sh"
nvm use default
pm2 save

管理员注册systemd启动,固定foodsafe家目录和Node路径:

env PATH=/opt/foodsafe-node/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
  /opt/foodsafe-node/bin/node /opt/foodsafe-node/lib/node_modules/pm2/bin/pm2 \
  startup systemd -u foodsafe --hp /var/lib/foodsafe
systemctl cat pm2-foodsafe
systemctl is-enabled pm2-foodsafe
systemctl status pm2-foodsafe --no-pager

检查生成服务确实使用已登记Node路径和用户,若输出额外命令,核对后执行。nvm用户安装不自动保障重启恢复,必须实测。不让root的PM2 dump覆盖foodsafe用户的dump。

Mosquitto启用自启动并核RuntimeDirectory;Nginx服务名以现场为准;watchdog更新为新cwd、两套端口、foodsafe用户和生产启动方式。对PM2、Nginx、MQTT日志设置轮转,保留周期按客户要求,防止日志挤满/data。

厂家现有实例级备份核查实际成功记录,补两模式逻辑导出和异机备份;用隔离恢复演练证明可用。单模式逻辑恢复与全实例物理恢复分开,不能恢复一套时误覆盖另一套。

在约定窗口重启新机,核对达梦、PM2两后端、8个前端入口、Mosquitto TCP/WebSocket、设备重连、中文、登录、旧附件/新上传、任务不重复。观察至少一个业务高峰和完整设备上报周期,记录错误、连接、归档增长和磁盘余量。

完成标准:重启可恢复、无重复任务、备份可恢复、两套数据和文件正常,客户验收。

22|故障回退与交接

先停新机写入与消费者,保全新机数据、文件及消息状态。

情况 处理
尚无正式新增数据 停新机本项目进程/任务;入口和设备切回旧机;恢复旧机MySQL和旧应用任务;验证两套业务
新机已有新增记录/文件 停写并记录差异时间窗,保全DM、附件和消息;先制定补回旧系统和对账方案,再切回,不丢弃新增数据
只需回退新代码 先核旧发布版本是否支持当前DM结构;不能直接拿MySQL旧代码连接DM
单套故障 按模式和应用范围处理,不为回退第二套就恢复整个实例覆盖第一套

故障期间不立即删除DM模式或dump。旧机保留到观察期结束及客户签收后安排下线。

交接包括:standard_wxb发布SHA、五软件和dmdb版本、包SHA、两库映射、私有配置位置、固定cwd、Node路径、PM2用户、Nginx端口、Topic/设备清单、数据/文件对账、备份恢复证据、重启记录及回退联系人。密码和授权通过私有渠道交接。

附录A|现场快速排障

现象 检查顺序
nvm找不到 是否同一foodsafe用户,NVM_DIR和nvm.sh是否加载;nvm是shell函数
Node缺glibc符号 客户与制包机OS/glibc/架构;更换匹配运行包,不替换系统glibc
pnpm冻结安装失败 固定pnpm、锁文件、allowBuilds配置和依赖版本,回制包阶段修复
PM2重启后没进程 foodsafe用户pm2 save、pm2-foodsafe服务、Node绝对路径和家目录
Cannot find module dmdb/tslib 包的完整依赖闭包和pnpm链接目标,不能只带gateway子node_modules
找不到main.js package.json start与嵌套dist入口,不能只拷main.js
启动仍连MySQL/错模式 DB_KIND、DM_USER、生效.env、PM2继承环境;本分支不读.env.local
/health正常但业务失败 /health不查DB;运行只读探针、查DM schema门禁与实际业务表
图片404/新图落错目录 PM2 cwd、旧实际文件目录、data/uploads、aiboxfile、OSS开关和访问权限
第二套二维码进入第一套 固定4211/5209的构建代码、实例专属配置;需制包前修正和验证
浏览器访问localhost3300 VITE_DEWO_MQTT_HOST及运行时回退地址,localhost指用户电脑
Nginx200/400交替 Connection空值映射、keepalive;分别测试直连和反代
MQTT重启失败 RuntimeDirectory、mosquitto用户权限、动态库、配置语法
MQTT端口通但前端连接失败 9001是否WebSocket、ws/wss和证书、Client ID/Topic/ACL

附录B|依据与未执行边界

代码核对基线:standard_wxb@52c3d9dfad3235b51c113cbeb739f0cff23b74dd;已检查package.json、pnpm锁文件、main.ts、db.service.ts、db.bootstrap.ts、dm.adapter.ts、dm-required-tables.ts、health.controller.ts、oss.util.ts、设备文件路径和前端固定端口。文档来源包括项目既有麒麟生产dist部署记录、用户提供厂家报告与达梦手册、此前已读取的有道麒麟笔记。

官方参考:nvm安装与使用、PM2启动恢复、DM Node.js驱动、Mosquitto配置、MySQL至DM迁移。本文不复制资料中的密码、授权或客户私有地址。

本次交付是手册及模板,未制备或声称交付新的客户安装包,未在客户服务器执行安装/切换;软件精确版本和最终客户迁移SQL按第01—04、12步填写和验证。文档中的命令与模板会做语法/一致性检查,实际适配和上线需完成第17—21步。