CPT中央网信办食安迁库与新服务器部署总手册

中央网信办食安迁库与新服务器部署总手册

更新:2026-10-20。适用场景:旧服务器上的两套食安系统迁移到已安装达梦的新麒麟服务器;现场使用现成 dist 制品,填写配置后启动。

本手册是客户现场部署的唯一操作入口,已合并之前的安装说明与必要的制品记录。每一步均给出执行位置、命令、成功标准和失败处理;不需要在两份文档之间跳转。

命令只在标明的机器上执行。Mac/管理电脑负责传文件和阅读,安装、chown、systemctl等命令在客户新麒麟服务器执行。编辑文件使用vi时:按i进入编辑,修改后按Esc,输入:wq回车保存;没有改完可按Esc输入:q!退出。

本次程序来源:standard_wxb@c40700715f8c23eb33eb855beec44b28dd19ec5b,包含包内 provenance/ 留存的发布适配补丁。适用运行环境为银河麒麟 V10、x86_64、glibc≥2.28、Node.js 20;制包验证版本为 Node20.19.5。

**现场不执行 pnpm install、不重新 build,也不从旧项目目录整体覆盖程序。dmdb 1.0.52452 已随运行依赖放进制品。**旧数据库、旧图片附件、客户密钥和私有配置单独迁移。

先认清四个位置和两个阶段

文中位置 做什么 使用账号
旧服务器 记录旧配置、备份数据库与文件、最终停写 旧机管理员、数据库备份账号
独立迁移机 恢复临时 MySQL8、审核并执行升级、用 DTS 迁达梦 迁移管理员、迁移数据库账号
新麒麟服务器 安装运行软件、制品、Nginx,启用两套应用 root 管理员;应用由 foodsafe 用户运行
达梦管理工具 创建或核对用户/表空间、导入、对账 厂家数据库管理员;业务连接用各自业务用户

先按第01~17步做演练,旧服务器继续提供业务,新服务器仅允许实施人员访问;第18~21步用于正式停机、切换和回退。第22步只在后续修改地址时用,第23步是排障表。软件已经装好且第03步核验通过,可跳过第04和06中的安装操作;第05步的Nginx实际路径/include核对仍要做。

命令中的 /实际路径、CHANGE_ME、实际服务名等必须按登记表替换。命令使用 Bash。每个代码块在对应位置执行,报错先处理,不连续粘贴全文。口令只填写到受控配置文件或交互输入,不复制到聊天、截图和公开日志。

01|登记两套系统和迁移参数

执行位置/账号:旧机、新机;管理员。

先填写下表。这里的目标名称与端口是建议值;厂家已有且归属明确的业务用户/表空间优先核对复用。

项目 第一套 第二套
旧 MySQL8 库 foodsafe foodsafe2
达梦业务用户/同名模式 FOODSAFE_APP FOODSAFE2_APP
达梦业务表空间 FOODSAFE_TBS FOODSAFE2_TBS
新应用目录 APP_ROOT /data/apps/foodsafe1 /data/apps/foodsafe2
PM2 名称 foodsafe-1 foodsafe-2
后端 GATEWAY_PORT 3300 3301
学校端 SCHOOL_PORT 4201 4202
监管端 REGULATOR_PORT 4301 4302
大屏 SCREEN_PORT 5209 5210
移动端 MOBILE_PORT 4211 4212

另记录:旧新 IP/域名、达梦实例 IP/端口/版本/授权、旧机程序目录和 cwd、普通图片与 AI 盒子文件目录、两套 .env、设备地址、MQTT broker/认证/Topic、视频服务、OSS、Redis、前厅联动、Nginx 与 PM2 路径、证书路径、备份位置和联系人。

厂家现有一个实例可承载这两个模式,共用一个监听端口(报告中为 5236,现场以实际为准)。这不需要再装两个达梦数据库服务。访问隔离靠用户和授权;实例重启会同时影响两套。

**成功标准:**两套来源与目标一一对应,每条设备、文件和外部服务有明确去向。**失败处理:**无法判断目录或模式归属时先查旧配置、厂家记录,不通过猜名字继续导入。

02|核查新机和安装软件所需空间

执行位置/账号:新机 root。此时只创建包目录和迁移暂存目录,不创建最终 APP_ROOT。

cat /etc/os-release
uname -m
getconf GNU_LIBC_VERSION
free -h
findmnt -T /data
findmnt -T /workspace
df -h /data /workspace
df -i /data /workspace
timedatectl
command -v dnf yum apt-get
ss -lntp

确认 x86_64、glibc≥2.28。应用、在线图片和日志优先放 /data,不放在达梦数据库数据文件子目录;安装包、备份暂存按容量放 /workspace。给数据库、图片增长和日志留足余量,建议 /data 规划保留至少 20% 空闲。

id foodsafe || useradd -m -r -d /var/lib/foodsafe -s /bin/bash foodsafe
install -d -m 755 /data/apps
install -d -m 700 /data/migration/foodsafe1 /data/migration/foodsafe2
install -d -m 700 /workspace/foodsafe-packages

**不要执行 mkdir -p /data/apps/foodsafe1 /data/apps/foodsafe2。安装脚本要求最终 APP_ROOT 不存在,安装时会自动创建。**暂存文件放 /data/migration/...。

**成功标准:**架构匹配、时间为北京时间、空间充足、目标端口已登记。**失败处理:**不是 x86_64 或 Node 报 glibc 不兼容,换匹配制品/运行包;不升级客户系统 glibc 来强行启动。

03|核验运行软件,已有合适版本就复用

执行位置/账号:先 root 查系统;再以 foodsafe 查 Node/PM2。

command -v nginx python3 curl rsync mosquitto
nginx -v
python3 --version
rsync --version | head -n 1
systemctl list-unit-files | grep -Ei 'nginx|mosquitto|pm2|DmService'

切换应用用户:

sudo -iu foodsafe
export NVM_DIR="$HOME/.nvm"
[ ! -s "$NVM_DIR/nvm.sh" ] || . "$NVM_DIR/nvm.sh"
command -v nvm node pnpm pm2
node -v
pnpm -v
pm2 -v
command -v node pnpm pm2
exit

nvm 是 shell 函数;当前 shell 没加载时不能据 which nvm 为空就判定未安装。

建议按已验证组合准备:nvm0.40.3、Node20.19.5、pnpm10.21.0、PM2 6.0.13;此前制包环境Mosquitto为2.0.18。Nginx 和 Mosquitto 使用客户系统同版、依赖齐全的安装包;制包环境 Nginx 为 1.24.0。应用包自身不附带整个操作系统软件环境。

Node/PM2 绝对路径必须可被 foodsafe 访问;不能把 root 私有目录下的 nvm 路径直接填给 foodsafe。Nginx、Mosquitto 若已由其他业务共享,复用已有服务并增加食安配置,不启动第二套抢端口。

**成功标准:**软件和精确版本记录完整。**失败处理:**缺哪个按第 04~06 步补哪个;已有软件不重复覆盖安装。

04|缺少 nvm/Node/pnpm/PM2 时的安装方法

执行位置:联网准备机制作纯软件运行包;离线客户新机恢复。准备机需与目标 Linux x86_64/glibc 匹配。

04.1 在联网准备机制作运行包

以下由专用制包用户执行;不是在客户业务项目下执行。

export NVM_DIR="$HOME/.nvm"
# 仅当该目录尚不存在时安装;已有 nvm 先加载并核对版本
if [ ! -e "$NVM_DIR" ]; then
  git clone --branch v0.40.3 --depth 1 https://github.com/nvm-sh/nvm.git "$NVM_DIR"
fi
. "$NVM_DIR/nvm.sh"
nvm --version
nvm install v20.19.5
nvm use v20.19.5
nvm alias default v20.19.5
npm install -g pnpm@10.21.0 pm2@6.0.13
node -v
pnpm -v
pm2 -v
mkdir -p "$HOME/foodsafe-runtime-package"
tar -czf "$HOME/foodsafe-runtime-package/nvm-core-v0.40.3.tgz" \
  -C "$NVM_DIR" nvm.sh nvm-exec bash_completion
tar -czf "$HOME/foodsafe-runtime-package/node-v20.19.5-linux-x64-with-tools.tgz" \
  -C "$NVM_DIR/versions/node/v20.19.5" .
cd "$HOME/foodsafe-runtime-package"
sha256sum nvm-core-v0.40.3.tgz node-v20.19.5-linux-x64-with-tools.tgz > runtime.sha256

若已有 nvm 不是 0.40.3,不覆盖原环境;由专用准备用户/隔离目录准备该版本。只打包 nvm 核心和 Node 运行目录,不打包用户 HOME、.npmrc、.pm2、密钥或运行日志。

04.2 新机恢复纯软件运行包

先由 root 把三份文件放进 /workspace/foodsafe-packages/runtime/,校验并复制到应用用户家目录:

cd /workspace/foodsafe-packages/runtime
sha256sum -c runtime.sha256
APP_HOME=$(getent passwd foodsafe | cut -d: -f6)
install -d -m 750 -o foodsafe -g "$(id -gn foodsafe)" "$APP_HOME/install"
install -m 640 -o foodsafe -g "$(id -gn foodsafe)" \
  nvm-core-v0.40.3.tgz node-v20.19.5-linux-x64-with-tools.tgz "$APP_HOME/install/"
sudo -iu foodsafe

以下由 foodsafe 执行。已有相同版本目录时先核验,不直接解压覆盖:

export NVM_DIR="$HOME/.nvm"
# 以下针对尚未安装该运行时的新用户
mkdir -p "$NVM_DIR/versions/node"
test ! -e "$NVM_DIR/versions/node/v20.19.5" || { echo '该版本目录已存在,先核验'; exit 1; }
mkdir "$NVM_DIR/versions/node/v20.19.5"
tar -xzf "$HOME/install/nvm-core-v0.40.3.tgz" -C "$NVM_DIR"
tar -xzf "$HOME/install/node-v20.19.5-linux-x64-with-tools.tgz" \
  -C "$NVM_DIR/versions/node/v20.19.5"
. "$NVM_DIR/nvm.sh"
nvm use v20.19.5
nvm alias default v20.19.5
node -v
pnpm -v
pm2 -v
command -v node pm2
exit

在 foodsafe 的 .bashrc 添加一次:

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

root 建立固定路径,后续配置和 systemd 都使用它:

APP_HOME=$(getent passwd foodsafe | cut -d: -f6)
test -x "$APP_HOME/.nvm/versions/node/v20.19.5/bin/node"
# 已有此别名时先 ls -ld 核对;不使用 ln -sf 强行覆盖
if [ -e /opt/foodsafe-node ] || [ -L /opt/foodsafe-node ]; then
  echo '已有固定路径,先核对,不覆盖'; exit 1
fi
ln -s "$APP_HOME/.nvm/versions/node/v20.19.5" /opt/foodsafe-node
runuser -u foodsafe -- /opt/foodsafe-node/bin/node -v
runuser -u foodsafe -- env HOME="$APP_HOME" PM2_HOME="$APP_HOME/.pm2" PATH=/opt/foodsafe-node/bin:/usr/bin:/bin /opt/foodsafe-node/bin/pm2 -v

若运行包按正确用户路径恢复后 pnpm/pm2 仍不可用,先检查脚本符号链接、Node PATH 和执行权限。现场应用包不需要靠再次执行 pnpm install 修补依赖。

**成功标准:**普通运行用户能使用上述版本,/opt/foodsafe-node/bin/node 和 pm2 可执行。**失败处理:**软件包缺依赖时在准备机补齐重打,不在客户机临时安装 latest。

05|缺少 Nginx 和基础工具时安装,并找到真实配置位置

执行位置/账号:新机 root。

已有Nginx和基础工具时跳过下面安装框,直接执行路径/include核对。缺少时优先使用客户已配置的软件源或同版麒麟离线 RPM;以下以yum系统为例,包必须含完整依赖:

# 离线:目录中放经校验的同版依赖包
# 实机为 dnf 时使用对应的 dnf 安装方式
yum --disablerepo='*' install /workspace/foodsafe-packages/base-rpms/*.rpm
# base-rpms 包含需要的 nginx/python3/curl/rsync/coreutils 及依赖

安装后:

command -v nginx
readlink -f "$(command -v nginx)"
nginx -V
nginx -t
systemctl status nginx --no-pager

已是宝塔/LNMP 的机器使用其原服务管理方式,勿另装一套覆盖。用实际二进制查看配置,以下输出仅存本地且不外传(可能包含客户配置):

nginx -T > /root/foodsafe-nginx-before.txt 2>&1
grep -nE 'configuration file|include |listen |root |user ' /root/foodsafe-nginx-before.txt
chmod 600 /root/foodsafe-nginx-before.txt

两个常见布局:

部署方式 Nginx 二进制 主配置 食安 vhost 目录
发行版安装常见路径 /usr/sbin/nginx /etc/nginx/nginx.conf /etc/nginx/conf.d/,以 include 为准
宝塔常见路径,须实查 /www/server/nginx/sbin/nginx /www/server/nginx/conf/nginx.conf /www/server/panel/vhost/nginx/,以主配置 include 为准

请客户运维按批准的客户端/设备网段放行八个前端端口(4201/4202、4301/4302、5209/5210、4211/4212);后端默认只监听127.0.0.1,不需要向客户端开放3300/3301。设备必须直连后端或MQTT时,额外按登记的内网端口和来源放行。不要通过关闭整机防火墙来处理访问问题。

不同安装方式路径不同。NGINX_VHOST_DIR 必须指向已被实际运行主配置在 http{} 内 include 的目录。生成的 deploy/nginx.conf 是 vhost 文件,不能拿它覆盖主 nginx.conf。

若没有合适 include,只由管理员在现有主配置的 http {} 内增加独立 include,例如 include /etc/nginx/conf.d/*.conf;,先备份再 nginx -t。保留前厅、进销存、证书、Lua 和其他业务配置。

**成功标准:**Nginx 运行正常、-t 通过、真实路径和 vhost include 已确认。**失败处理:**某端口占用时查 ss -lntp 和现有 vhost;不能只改一个 listen 而遗漏前端二维码、PM2和回调。

06|安装或迁移 Mosquitto

执行位置/账号:新机 root。演练先限制真实设备接入。

先确认旧机 broker 是否被食安和其他业务共用,记录实际 TCP/WebSocket/TLS 端口、认证、ACL、持久化目录和客户端 ID。共享服务应整体规划,不能复制配置后让新旧两边同时处理同一正式 Topic。

有受控离线 RPM 时优先安装:

yum --disablerepo='*' install /workspace/foodsafe-packages/mosquitto-rpms/*.rpm
command -v mosquitto mosquitto_pub mosquitto_sub mosquitto_passwd
mosquitto -h
systemctl cat mosquitto

离线包由准备机按新机麒麟版本制备;未确定 Mosquitto 精确版本和依赖前,不把某个源码压缩包当成已能安装的运行包。原有版本满足设备协议就优先复用。

如果必须沿用源码安装路线,在准备机备齐经验证的源码及 OpenSSL、cJSON、libwebsockets、CMake 和编译工具等依赖;变量 MOSQUITTO_VERSION 填已核准的精确版本:

: "${MOSQUITTO_VERSION:?填写已核准精确版本}"
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"
ldconfig

WS 设备使用场景必须确认 libwebsockets 被找到并通过实际握手,不以 CMake 命令退出成功代替此检查。源码安装二进制可能在 /usr/local/sbin/mosquitto,发行版可能在 /usr/sbin/mosquitto,systemd 用实查路径。

复制旧机的业务口令文件、ACL、TLS 证书和必要配置到新机受控目录;这些不放进公共制品。新建账号可交互执行 mosquitto_passwd;-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

root 建目录和权限:

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
chown root:mosquitto /etc/mosquitto/passwd /etc/mosquitto/acl
chmod 640 /etc/mosquitto/passwd /etc/mosquitto/acl

发行版已有 unit 就复用。源码安装且没有 unit 时,核对二进制后创建 /etc/systemd/system/mosquitto.service:

[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
systemctl daemon-reload
systemctl enable --now mosquitto
systemctl status mosquitto --no-pager
journalctl -u mosquitto -n 50 --no-pager
ss -lntp

**成功标准:**测试账号按 ACL 在专用测试 Topic 正常收发,TCP/WebSocket(实际使用时含 TLS)和重启恢复正常。**失败处理:**监听失败查端口、配置、运行目录/文件权限和依赖;不关闭全部防火墙,不启用匿名访问绕过认证。

07|旧机备份两库、程序配置和业务文件

执行位置/账号:旧机管理员与 MySQL8 备份账号。现在做演练快照,正式停机再做最终快照。

07.1 记录实际运行目录与配置

pm2 list
pm2 describe 旧食安后端进程名
ps -ef | grep -E '[n]ode|[n]odemon|[v]ite'
crontab -l
systemctl list-timers --all
nginx -T > /受控备份目录/nginx-before.txt 2>&1

如果由其他用户管理 PM2,用该用户执行 pm2 describe。重点记录 exec cwd,它决定相对文件路径。备份两套私有 .env、设备/存储/MQTT/视频配置、Nginx vhost、PM2配置及证书;备份包权限设为仅管理员可读。

07.2 备份 MySQL8

以下把备份放到旧机实际有足够空间的目录;不要放内存盘 /tmp。示例中 /backup 不存在时先选定磁盘再创建。

umask 077
mkdir -p /backup/foodsafe-migration/rehearsal
mysql --version
mysqldump --version

建立 /root/foodsafe-mysql-backup.cnf,用编辑器填客户实际值,文件权限 600:

[client]
host=实际MySQL地址
port=实际端口
user=有备份权限的账号
password=实际密码
chmod 600 /root/foodsafe-mysql-backup.cnf
if mysqldump --defaults-extra-file=/root/foodsafe-mysql-backup.cnf \
  --single-transaction --quick --routines --triggers --events --hex-blob \
  --default-character-set=utf8mb4 --set-gtid-purged=OFF --no-tablespaces \
  --databases foodsafe foodsafe2 \
  > /backup/foodsafe-migration/rehearsal/foodsafe-two.sql.part \
  2> /backup/foodsafe-migration/rehearsal/dump.err; then
  mv /backup/foodsafe-migration/rehearsal/foodsafe-two.sql.part \
    /backup/foodsafe-migration/rehearsal/foodsafe-two.sql
else
  echo '备份失败,先检查dump.err;不能使用.part文件继续迁移'
  exit 1
fi

使用与源库兼容的 MySQL8 客户端。非事务表(例如 MyISAM)不能依赖 --single-transaction 保证一致性;存在这类表时需约定停写/锁表备份方案。备份窗口内禁止结构变更。

sha256sum /backup/foodsafe-migration/rehearsal/foodsafe-two.sql \
  > /backup/foodsafe-migration/rehearsal/foodsafe-two.sql.sha256

两个库在不同实例时分别建受控连接文件、分别备份,不能以一个成功文件代替另一实例数据。

07.3 确认并备份图片/附件

普通图片可能位于旧项目 data/uploads/ 或 services/gateway-service/data/uploads/;AI 盒子文件可能位于根或服务目录 aiboxfile/。两处都有内容时,对照数据库图片 URL 和进程 cwd 判断,不盲目合并同名文件。

每套先单独备份确认的目录,示例:

# OLD_APP_ROOT必须填这一套的真实旧程序根目录
OLD_APP_ROOT=/实际第一套旧项目根
ls -ld "$OLD_APP_ROOT/data/uploads" "$OLD_APP_ROOT/aiboxfile"
tar -czpf /backup/foodsafe-migration/rehearsal/foodsafe1-files.tgz \
  -C "$OLD_APP_ROOT" data/uploads aiboxfile

若历史文件实际位于服务目录,上面 -C 和目录参数按实查调整。某类文件确实不用可跳过,不能创建空目录掩盖缺失。第二套独立执行,输出 foodsafe2-files.tgz。

复制备份到受控迁移介质/另一存储并验证 SHA 与压缩包可读;实际恢复到隔离 MySQL8 成功后才算数据库备份验证完成。

**成功标准:**数据库、私有配置、文件、Nginx/MQTT等均有备份和归属。**失败处理:**备份命令非 0、磁盘满或恢复失败,先解决并重新备份,不能继续迁库。

08|把旧文件暂存到新机,暂不放最终应用目录

执行位置/账号:新机 root;从受控备份或旧机复制。

统一整理成:

/data/migration/foodsafe1/data/uploads/  第一套普通图片
/data/migration/foodsafe1/aiboxfile/     第一套AI盒子文件
/data/migration/foodsafe2/data/uploads/  第二套普通图片
/data/migration/foodsafe2/aiboxfile/     第二套AI盒子文件

例如备份包已按第 07 步结构制作:

tar -tzf /受控备份目录/foodsafe1-files.tgz | head
tar -xzpf /受控备份目录/foodsafe1-files.tgz -C /data/migration/foodsafe1
tar -xzpf /受控备份目录/foodsafe2-files.tgz -C /data/migration/foodsafe2
ls -ld /data/migration/foodsafe{1,2}/data/uploads /data/migration/foodsafe{1,2}/aiboxfile

先看目录层级再解压,防止出现 data/uploads/uploads 或解压到错误套。不要把 .env 和图片放进公共压缩包,也不要把旧前端 dist 覆盖到本次制品。

**成功标准:**两套文件分开、相对路径完整、可核对数量和样本。**失败处理:**缺目录先查备份来源,不创建空目录当迁移完成。

09|隔离 MySQL8 恢复并审核两套升级 SQL

执行位置/账号:独立迁移机,MySQL 管理员。不要对旧生产库直接执行升级。

安装/准备与旧源兼容的临时 MySQL8,禁止业务应用连接。临时服务的安装和初始化采用该迁移机已验证方式,不在客户新机上为了转换额外启用 MySQL 业务服务。

用受控 stage8.cnf 保存临时实例连接(权限 600,格式同第 07 步)。先连接确认机器、端口、版本:

MYSQL=/实际MySQL8客户端/bin/mysql
"$MYSQL" --defaults-extra-file=/受控路径/stage8.cnf \
  -e 'SELECT @@hostname,@@port,VERSION();'

确认是临时实例后,关闭事件调度,并导入演练快照:

"$MYSQL" --defaults-extra-file=/受控路径/stage8.cnf \
  -e 'SET GLOBAL event_scheduler=OFF;'
"$MYSQL" --defaults-extra-file=/受控路径/stage8.cnf \
  < /受控备份目录/foodsafe-two.sql > /受控记录目录/restore.log 2>&1
echo $?

备份中 CREATE DATABASE/USE 会选择库,不能以为命令末尾加个库名就能把导入改去另一个库。恢复日志有错误时先处理,不加 --force 忽略。

制品 sql-review/zywxb-mysql8-foodsafe2.sql 是从 standard 取得的 MySQL8 foodsafe2 历史升级候选,包含 USE foodsafe2 和库名断言;不是达梦 SQL,也不保证匹配客户当前结构。研发/迁移人员必须比对两个恢复库与本次发布所需结构,分别形成:

foodsafe-upgrade-reviewed.sql
foodsafe2-upgrade-reviewed.sql

某库无需升级,也应记录核验结果。第一套不能只替换 USE 后套用第二套脚本;要同时核对列、索引、约束、存量数据和必要字典/权限/配置数据。

经审核后的第二套执行形式:

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 不能当作普通事务整体回滚,失败后不能整份反复重跑。

**成功标准:**升级后的两个库满足冻结代码所需结构,原数据保留、变化有记录;导出升级后快照、精确逐表行数和结构清单供达梦对账。**失败处理:**无客户实际快照/比对结论时停止这一步,不由通用手册编造客户 SQL。

10|核对达梦实例,建立两组用户/模式/表空间

执行位置/账号:新机达梦管理工具或 disql;数据库管理员/厂家实施人员。

沿用厂家已装实例,不重新初始化。核对版本、UTF-8、大小写、MySQL兼容模式、页大小、端口、授权与备份。厂家历史报告是 32K 页、UTF-8、不区分大小写、COMPATIBLE_MODE=4;需与实机确认,不直接根据报告改参数。

建议映射:

源库 登录用户及自动创建的同名模式 默认业务表空间
foodsafe FOODSAFE_APP FOODSAFE_TBS
foodsafe2 FOODSAFE2_APP FOODSAFE2_TBS

厂家已有用户时先核对对象归属与空闲目标,避免重复创建或覆盖。测试环境的 WWWWEB/WWWWEB2 不等于客户生产账号。

以下仅为建库语法例子,路径、容量、增长上限与密码由厂家按实机审核后填写;不能带 CHANGE_ME 原样执行。SIZE/NEXT/MAXSIZE 的单位按当前版本确认:

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";

第二套另建 FOODSAFE2_TBS/FOODSAFE2_APP,使用独立密码。创建用户会自动生成同名模式,不再重复建同名 Schema。不把业务导进 SYSDBA/MAIN,不授予业务账号 DBA。迁移阶段需要建表权限,正式运行权限按实际调用收敛;先验证再回收,不能把广泛权限当成解决所有问题的方法。

**成功标准:**两组账号分别能登录,默认模式正确,表空间路径/容量受控、无无关跨模式授权。**失败处理:**模式已有未知业务对象时先查归属,不能清空;字符集等实例属性不匹配由厂家判断方案。

11|用 DTS 将两个升级后的库迁到各自达梦模式

执行位置:迁移机上的达梦 DTS/管理客户端;连接隔离 MySQL8 和新机达梦。

DTS在达梦客户端安装目录的tool目录中,Linux图形环境可执行./dts;服务器没有图形界面时,在能连通源MySQL和目标达梦的管理电脑上运行匹配客户端。迁移工具若提示缺MySQL驱动,需要厂家补齐该DTS版本匹配的MySQL8 JDBC驱动后重新测试;它与应用包内的Node dmdb驱动是两件事,不用pnpm install代替。

DTS 图形操作顺序:

  1. 打开 DTS,新建迁移工程,选择“其他数据库迁移到达梦/MySQL 到 DM”(具体文字随客户端版本)。
  2. 源端填临时 MySQL8,先迁 foodsafe;目标用 FOODSAFE_APP,端口为厂家实际值。
  3. 连接测试后逐项核对目标模式、表空间和对象名称。确保不是 SYSDBA,也不是第二套。
  4. 选择需要的表及其他对象,审核类型/默认值/自增/约束映射。
  5. 分阶段迁移:结构 → 数据 → 普通索引 → 外键/注释 → 视图/触发器/过程等特殊对象 → 自增校正。
  6. 查看每对象结果及失败 SQL,导出日志;有错误先修正、再针对失败对象处理,不无条件重复全部导入。
  7. 第二个工程源端选 foodsafe2,目标 FOODSAFE2_APP,再次核对,不沿用第一套目标账号。

MySQL dump 不能直接喂给达梦;MySQL 事件、过程、触发器需要逐个判断是否迁移或转成应用/systemd/作业任务。MySQL 专属语法需要转换。

若采用 Codex 专用迁移程序,应基于客户最终源备份、源版本、升级脚本和目标实例映射形成受控迁移包再演练;当前程序包不含客户数据库转换结果。

11.1 必须完成的对账

对账项 怎么核对
结构 对照升级后 MySQL 的表、列、主键、唯一索引、外键、默认值与达梦目标
行数 各业务表执行精确 COUNT(*),逐表对比;不能用信息模式估算行数
业务 两套人员、健康证、晨检、留样、消毒、农残、库存台账、审计等数量/关键汇总
字段 中文、日期/北京时间、NULL与空串、金额/小数、大整数 ID、JSON/CLOB/BLOB
长文本 最大 UTF-8 字节长度、样本内容/摘要,不能只检查非空
自增 下一次新增不会撞历史 ID,主子表关系正确
隔离 第二套不引用第一套模式/数据;两个业务账号只获必要权限

项目曾发现 dmdb 1.0.52452 批量长文本截断问题;达到 32767 UTF-8 字节的字符串按已经演练的大字段/单行路径处理,并独立复核。大整数不能先转 JS Number 造成精度丢失。

行数核验例子只展示形式,表名必须选客户实际业务表:

-- 在升级后 MySQL 与对应达梦用户中分别查询同一张实际业务表
SELECT COUNT(*) FROM personnel;

同一行数并不保证字段值正确;抽样并汇总核验关键字段。表数 118 是本次测试账号的实测值,不是客户库的硬编码合格线。

**成功标准:**对账差异为零,或差异均由升级脚本解释并签认;恢复/迁移日志可追溯。**失败处理:**缺表、缺列、乱码、截断、自增冲突先解决,不删除应用结构检查来让程序启动。

12|取得现成制品、校验、准备第一套部署配置

执行位置/账号:新机 root。以下均在制品解压目录操作。

本次本地交付目录为 /Users/wiscourper/Downloads/work/团餐项目/食安/中央网信办后期维护/中央网信办迁移1021/离线打包物/,内有新版压缩包、同名SHA256、这份总手册和版本说明。该目录在管理电脑,不是客户服务器路径。

先在Mac/管理电脑使用SFTP或受控U盘,将下面两个文件放到新机/workspace/foodsafe-packages/,两者一起传,不只传tar.gz。

交付文件名以实际 SHA 清单为准,当前命名为:

foodsafe-standard-wxb-c4070071-kylin-x64.tar.gz
foodsafe-standard-wxb-c4070071-kylin-x64.tar.gz.sha256
cd /workspace/foodsafe-packages
sha256sum -c foodsafe-standard-wxb-c4070071-kylin-x64.tar.gz.sha256
tar -tzf foodsafe-standard-wxb-c4070071-kylin-x64.tar.gz >/dev/null
tar -xzf foodsafe-standard-wxb-c4070071-kylin-x64.tar.gz
cd foodsafe-standard-wxb-c4070071-kylin-x64
cp config/deploy.env.example config/deploy.env
vi config/deploy.env

这套 install.sh/activate.sh 用于首次安装,已有运行目录更新不能重复执行。已部署系统须先备份,保留私有配置、上传文件与本套端口,并执行经审核的更新方案。新旧前后端作为同一版更新,后端先提供本次新增的预警 location 字段再开放新版前端。

首次安装参数示例:

INSTANCE=1
APP_ROOT=/data/apps/foodsafe1
APP_USER=foodsafe
PUBLIC_HOST=CHANGE_ME_CUSTOMER_IP_OR_DOMAIN
PUBLIC_SCHEME=http
LISTEN_HOST=0.0.0.0
SCHOOL_PORT=4201
REGULATOR_PORT=4301
SCREEN_PORT=5209
MOBILE_PORT=4211
GATEWAY_PORT=3300
NODE_BIN=/opt/foodsafe-node/bin/node
PM2_BIN=/opt/foodsafe-node/bin/pm2
NGINX_BIN=/usr/sbin/nginx
NGINX_VHOST_DIR=/etc/nginx/conf.d
INTEGRATION_UPSTREAM=
TLS_CERT_PATH=
TLS_KEY_PATH=

必须修改 PUBLIC_HOST 和 Nginx 实际路径。PUBLIC_HOST 仅域名/IPv4,不带 http:// 和端口;上述 CHANGE_ME_... 不能保留。若使用 HTTPS,填 PUBLIC_SCHEME=https、客户证书和私钥绝对路径,需证书实际覆盖该域名。两套端口确保与其他业务不冲突。

INTEGRATION_UPSTREAM 只在启用独立 MEGO 集成服务时填 http(s)://主机:端口;没有服务就留空,相应功能返回明确 503,不再请求使用者电脑 localhost。主食安 API 不依赖这一项。

**成功标准:**SHA 通过、参数全部对应新机、APP_ROOT 尚不存在。**失败处理:**校验失败重新获取包,不跳过校验;存在 APP_ROOT 时先查归属,不删除目录绕过保护。

13|安装第一套程序,再合并私有配置

执行位置/账号:新机 root;制品解压目录。

bash scripts/install.sh config/deploy.env

脚本检查架构/Node/运行用户/包清单,创建 APP_ROOT 并复制程序,生成:

/data/apps/foodsafe1/services/gateway-service/.env  私有配置模板
/data/apps/foodsafe1/deploy/ecosystem.config.cjs   PM2配置
/data/apps/foodsafe1/deploy/nginx.conf             本套四端Nginx配置
/data/apps/foodsafe1/apps/*/dist/runtime-config.js 运行时地址

此步不启动后端、不导入数据库。真实文件名为 ecosystem.config.cjs,不要改成旧文档中的其他名称。

接着编辑新 .env,将旧客户有效配置逐项合并;不直接用旧整份 .env 覆盖模板:

vi /data/apps/foodsafe1/services/gateway-service/.env
chmod 600 /data/apps/foodsafe1/services/gateway-service/.env

私有.env是dotenv配置,不要用source当Shell脚本执行。密码包含#或空格时用引号包住,例如DM_PASSWORD="厂家实际密码",避免被当成注释截断。

至少核对:

NODE_ENV=production
HOST=127.0.0.1
PORT=3300
DB_KIND=dm
DM_HOST=客户达梦实际地址
DM_PORT=实际端口
DM_USER=第一套业务用户
DM_PASSWORD=实际密码
JWT_SECRET=保留客户有效原密钥或按方案生成强随机密钥
DB_AUTO_MIGRATE=0
DB_SYNC_COMMENTS=0
TZ=Asia/Shanghai
DEMO_SEED=false
DEMO_AI_JOB_ENABLED=0
AIBOX_SCHEDULER_ENABLED=0
DEWO_CALLBACK_SUBSCRIBE_ENABLED=0
YLT_TOKEN_REFRESH_ENABLED=0
AUDIT_ENABLED=true
USE_REDIS_CACHE=false
OSS_SWITCH=false
BRIGHT_KITCHEN_MODE=2
PUBLIC_BASE_URL=http://客户实际IP或域名:4201
DEWO_MQTT_URIS=
DEWO_MQTT_USERNAME=
DEWO_MQTT_PASSWORD=
FOODSAFE_MOBILE_BASE_URL=http://客户实际IP或域名:4211
ZHCT_BASE_URL=

达梦模式由登录用户的默认同名模式决定,没有 DM_SCHEMA 配置项。两套分别用自己的账号,不能把测试账号写到客户配置。保留客户原 JWT、设备签名、OSS/MQTT/视频及其他业务参数,旧 IP、开发服务器 URL 必须逐项核对;正式JWT密钥不能使用源码内的开发默认值。

AUDIT_ENABLED=false 仅关闭部分审计,不能保证应用不写库。严格只读核验仅用独立 SELECT 探针或经过验证的驱动层写入拦截,不以登录/健康检查无报错代替只读证明。完整业务演练应使用隔离恢复库;正式切换前按客户方案恢复审计。其他前三个真实后台任务开关在设备联调/切换时按需恢复,DEMO_AI_JOB_ENABLED=0 正式保持关闭。

发布包默认 HOST=127.0.0.1,浏览器通过 Nginx 访问;设备必须直连后端时经网络登记改 0.0.0.0 并限制设备网段。Redis/OSS 使用时填真实配置,不因示例 false 就关闭客户实际依赖。前厅免登/联动填 ZHCT_BASE_URL。

13.1 MQTT三层配置要一起核对

  1. 先确认实际 Broker 所在服务器、监听地址、密码文件与Topic权限;客户继续用旧外部Broker时沿用其真实参数,不强行改成本机。
  2. 本套后端私有.env按实际配置全局DEWO_MQTT_URIS、DEWO_MQTT_USERNAME、DEWO_MQTT_PASSWORD,口令用引号;空值不是完成配置。
  3. 学校端进入“系统配置→设备配置”,编辑对应设备,核对设备类型、厂商、学校/食堂绑定以及设备级MQTT URI、账号、密码;当前代码可能优先使用设备级配置,不能只补全局三行。
  4. 页面普通保存只保存连接参数,不会在Mosquitto自动创建账号或ACL。 本版新增的MQTT手工配置指南可以在设备配置弹窗中打开查看,不需要安装自动账号管理脚本或给后端sudo权限。
  5. 若需要新增/改密码,在Broker实际所在机器先备份真实password_file,再交互执行(示例路径/账号须替换):
# Broker管理员执行,先核对真实路径,不是默认在应用服务器执行
cp -p /etc/mosquitto/passwd /etc/mosquitto/passwd.bak.$(date +%Y%m%d-%H%M%S)
mosquitto_passwd /etc/mosquitto/passwd foodsafe1
# 只对实际Mosquitto主进程重读认证;服务名以现场为准
systemctl kill -s HUP --kill-who=main mosquitto.service

已有密码文件不能加-c,否则会覆盖其他账号。共享账号改密码会影响其他设备,优先用各项目自己的账号;账号存在也不等于Topic权限已配置。交互输入与页面/设备端一致的密码,不把口令写在公开命令中。

  1. 经批准,用厂商约定的测试Topic验证认证、订阅和下发,再核对设备显示/回执;消息到Broker不代表设备已经处理。
  2. 需要得沃晨检回执订阅时,先确认真实Broker、设备及绑定,再把本套DEWO_CALLBACK_SUBSCRIBE_ENABLED=1并定向重启本套后端。当前服务启动时扫描设备列表,更新连接记录后不能假定自动重新订阅。旧机与新机不能同时消费真实消息。

13.2 前端配置也要按两套分别检查

生成器会写本套runtime-config.js,并且仅在integration.config.json缺失或仍为包内完全一致的演示默认值时,替换为无演示设备编号的最小配置;已有客户独立修改的JSON不会覆盖。

cat /data/apps/foodsafe1/apps/web-school/dist/integration.config.json
vi /data/apps/foodsafe1/apps/web-school/dist/integration.config.json
python3 -m json.tool /data/apps/foodsafe1/apps/web-school/dist/integration.config.json > /dev/null

新生成的最小内容为:

{
  "SCHOOL_INTEGRATION_BASE": "/integration",
  "BRIGHT_KITCHEN_MODE": 2,
  "OSS_SWITCH": false
}

本地明厨模式2和本地存储false仅为当前默认,按客户原有效业务设置确认,前后端应一致。有MEGO服务时,额外填写真实MEGO_CANDIDATES、EQUIPMENT_CODE并验证网络目标;无服务时不使用相关功能,不保留localhost:4001/4003或DEMO-EC-0001。JSON无注释,修改后用上述命令验证语法。

第二套对/data/apps/foodsafe2/...单独检查;运行时移动端4212、大屏5210、PUBLIC_BASE_URL学校端4202。浏览器localStorage可能覆盖部分集成配置,首次用无痕窗口验收,并通过F12网络查看真实请求目标;配置文件正确不等于浏览器已使用正确值。

PUBLIC_BASE_URL用于该版本支持的设备图片完整URL,填写客户设备可达的协议/地址/学校端口,避免Nginx默认Host不带非标准端口造成图片地址错误。它不能代替数据库内旧完整URL的逐类处理,也不能让只支持设备直连地址的业务自动改用Nginx。

配置加载先找项目根下 services/gateway-service/.env,进程环境变量优先。用本包独立 PM2 名称与配置启动,避免旧 PM2 的 DB_KIND/DM_USER 覆盖文件;真实密钥不填在公开 deploy.env。

**成功标准:**安装完成、配置无占位符、用户/模式/端口正确、私有文件仅所需用户可读。**失败处理:**脚本因目录存在停下,查是否部分安装;不要整目录删除或覆盖上传文件。驱动缺失先查包完整性,不现场下载另一个 dmdb 版本。

14|把第一套旧图片与 AI 盒子文件放入已安装项目

执行位置/账号:新机 root;同一 Bash 窗口、制品解压目录。先安装,再复制。

set -e
set -a
source config/deploy.env
set +a
test -f "$APP_ROOT/deploy/ecosystem.config.cjs"
OLD_FILES_ROOT=/data/migration/foodsafe1
ls -ld "$OLD_FILES_ROOT/data/uploads" "$OLD_FILES_ROOT/aiboxfile"
mkdir -p "$APP_ROOT/data/uploads" "$APP_ROOT/aiboxfile"
rsync -a "$OLD_FILES_ROOT/data/uploads/" "$APP_ROOT/data/uploads/"
rsync -a "$OLD_FILES_ROOT/aiboxfile/" "$APP_ROOT/aiboxfile/"
chown -R "$APP_USER:$(id -gn "$APP_USER")" "$APP_ROOT/data" "$APP_ROOT/aiboxfile"
rsync -rcn --itemize-changes "$OLD_FILES_ROOT/data/uploads/" "$APP_ROOT/data/uploads/"
rsync -rcn --itemize-changes "$OLD_FILES_ROOT/aiboxfile/" "$APP_ROOT/aiboxfile/"
runuser -u "$APP_USER" -- test -r "$APP_ROOT/data/uploads"
runuser -u "$APP_USER" -- test -x "$APP_ROOT/data/uploads"
runuser -u "$APP_USER" -- test -w "$APP_ROOT/data/uploads"
runuser -u "$APP_USER" -- test -r "$APP_ROOT/aiboxfile"
runuser -u "$APP_USER" -- test -x "$APP_ROOT/aiboxfile"
runuser -u "$APP_USER" -- test -w "$APP_ROOT/aiboxfile"

set -e 遇错会退出当前 shell;修正后重新打开 Bash 并加载本套参数。确实没有某类旧文件时跳过对应命令并登记,不用空目录冒充。

源目录末尾 / 表示复制其内部内容,不要多套一层 uploads/uploads。不要增加 --delete;新机已经产生业务新图后,旧目录不得通过覆盖或删除选项破坏它。

图片路径对应关系

页面/数据库中的路径 第一套读取的文件位置
/files/view/2026/10/20/abc.jpg /data/apps/foodsafe1/data/uploads/2026/10/20/abc.jpg
/api/files/view/2026/10/20/abc.jpg 同上,Nginx 去掉 /api 后转发
/static/device/abc.txt 通常为 /data/apps/foodsafe1/aiboxfile/device/abc.txt,盒子文本可解码为图片

/static/ 先找程序自带静态资源,找不到才转后端,不能把业务图与 /static/cpt.png 等程序资源混成同一路径。

旧完整 URL 和 OSS 要单独处理

数据库若存 https://旧图片域名/xxx.jpg、旧 IP、旧端口,复制文件不会自动更改这些地址。继续使用原存储时验证客户网络可达;改离线本地时先导出对象,再在迁移副本中按真实字段和路径对应关系转换,或配置经过验证的图片代理。

**不能只改 OSS_SWITCH=false 就认为历史图片已本地化,也不能全库盲目替换域名。**视频、设备、文件 URL 可能使用不同服务,逐类确认。

**成功标准:**rsync 只读内容比较无差异、权限检查通过、图片 URL 与路径有对应。**失败处理:**404 先查路径、cwd、权限和代理,不把图片平铺到 uploads 根目录解决。

15|检查并启用第一套 Nginx/PM2

执行位置/账号:新机 root;制品解压目录。

先核对第 05 步 Nginx 路径和 include,确认服务已经运行;检查端口和冲突站点。若旧的 food_safety.conf 已复制到新机,单独备份并停用其中冲突的食安 vhost;勿移动全部站点。

bash scripts/check.sh config/deploy.env
bash scripts/activate.sh config/deploy.env

check.sh 检查运行依赖、四端 dist、配置占位符并执行达梦只读探针;探针只执行 SELECT,不迁库。activate.sh 再检查端口/同名 PM2,安装本套 foodsafe-release-1.conf、nginx -t、用 APP_USER 启动 foodsafe-1;后端健康通过才 reload 并保存 PM2 列表。Nginx语法、后端启动/健康或reload失败时会撤销本次新配置/进程。若仅pm2 save失败,服务仍在运行,脚本会提示“未持久化”;先查该用户PM2权限并补做save,不能误认为已回滚,也不要重复首次启用。

首次启用脚本遇同名站点/进程会停下保护现场,不把它当无条件更新脚本。健康 HTTP200 仅证明服务响应,还需下文业务验收。

本版新增的大屏路径

本版四路视频与轮播页面分别为:

http://客户IP:5209/screen/video-four
http://客户IP:5209/screen/video-carousel
http://客户IP:5210/screen/video-four
http://客户IP:5210/screen/video-carousel

制品站点生成器在大屏vhost为以上两路径及尾斜杠增加精确SPA规则,避免被通用/screen/*后端API规则拦截。屏端源码Vite配置已同步修正为根路径base=/,普通构建即可生成正确资源地址,避免深层入口加载相对assets失败。真实/screen/*接口仍保留代理;不要把整个screen前缀改成SPA。部署后直接访问、刷新和检查静态资源,真实视频流仍要现场验收。

Nginx 文件到底放哪里

脚本生成文件:/data/apps/foodsafe1/deploy/nginx.conf。脚本启用时复制到:

<NGINX_VHOST_DIR>/foodsafe-release-1.conf

第二套是 foodsafe-release-2.conf。发行版常见为 /etc/nginx/conf.d/;宝塔常见为 /www/server/panel/vhost/nginx/。以新机 nginx -T 显示的 include 为准。

完整学校端 vhost 示例(解释用,不需要再手工重复安装)

以下为第一套 HTTP、学校端 4201、后端 3300 的完整等价示例;共用头放在 server 层。正式以脚本生成内容为准,包内实际文件还包含监管/大屏/移动三个 server。此示例放在 http{} 内 include 的 vhost 文件中,不加外层 http{}:

展开:学校端完整 Nginx 示例
upstream foodsafe_release_1 {
    server 127.0.0.1:3300;
    keepalive 32;
}
map $http_upgrade $foodsafe_release_1_upgrade {
    default upgrade;
    '' '';
}
server {
    listen 0.0.0.0:4201;
    server_name _;
    root /data/apps/foodsafe1/apps/web-school/dist;
    index index.html;
    client_max_body_size 50m;

    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $foodsafe_release_1_upgrade;
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
    proxy_buffering off;

    location ~ /\. { deny all; }
    location ^~ /api/ { proxy_pass http://foodsafe_release_1/; }
    location = /api { proxy_pass http://foodsafe_release_1/; }
    location ^~ /websocket/ { proxy_pass http://foodsafe_release_1; }
    location ~ ^/(school|screen|iot|bright|bright-local|reg|gatewayGBS|gateway|trustivs|files|auth|audit|health|openApi|wosapi|deviceIot|cabinet|inspectionRecord|uploadInspectionRecord|sample|common|device|sakura|push|regulator|home|reports|sampling|public|control|dewo)(/|$) {
        proxy_pass http://foodsafe_release_1;
    }
    location = /system/storage/status { proxy_pass http://foodsafe_release_1; }
    location = /system/storage/init { proxy_pass http://foodsafe_release_1; }
    location = /system/group/device/active-group { proxy_pass http://foodsafe_release_1; }
    location ^~ /static/ { try_files $uri @foodsafe_static; }
    location @foodsafe_static { proxy_pass http://foodsafe_release_1; }
    location ^~ /integration/ {
        default_type application/json;
        return 503 '{"code":503,"message":"MEGO integration upstream is not configured"}';
    }
    location = /runtime-config.js { add_header Cache-Control "no-store" always; }
    location = /index.html { add_header Cache-Control "no-cache" always; }
    location / { try_files $uri $uri/ /index.html; }
    location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|otf)$ {
        try_files $uri =404;
        expires 7d;
    }
}

规则:/api/xxx 去 /api 前缀,原生设备与业务路径保留前缀;/system/ 页面走SPA,仅三条真实后端路径精确反代,不能使用笼统system前缀劫持页面;WebSocket 保留 Upgrade,SSE 不缓冲;图片 /static/ 有后端回落。50m 是模板值,需和应用 BODY_LIMIT_MB/UPLOAD_LIMIT_MB 及客户原限制一起审核。使用 HTTPS 由脚本根据证书生成 listen ... ssl。

其余三个入口不是另起开发服务,而是同一 Nginx 中复制 server 规则并改 root/listen,upstream 继续指本套后端;脚本已经自动生成:

第一套入口 listen root
学校 4201 /data/apps/foodsafe1/apps/web-school/dist
监管 4301 /data/apps/foodsafe1/apps/web-regulator/dist
大屏 5209 /data/apps/foodsafe1/apps/screen/dist
移动 4211 /data/apps/foodsafe1/apps/mobile-device/dist

第二套脚本生成 upstream foodsafe_release_2→3301,roots 全部为 /data/apps/foodsafe2/...,listen 为 4202/4302/5210/4212。runtime-config 和二维码使用本套地址,不需要重编译。

启用后的连通性检查(不保证无写入)

curl --fail --silent --show-error http://127.0.0.1:3300/health
curl --fail --silent --show-error http://127.0.0.1:4201/api/health
curl -I http://127.0.0.1:4201/
curl -I http://127.0.0.1:4301/
curl -I http://127.0.0.1:5209/
curl -I http://127.0.0.1:4211/

LISTEN_HOST 若不是 0.0.0.0/127.0.0.1,curl 改实际监听地址。浏览器打开四端,检查页面、JS/CSS、本地播放器、二维码和刷新。普通 HTTP 的 Connection 映射留空是正常配置,不改回不必要的 close。

**成功标准:**脚本完成、后端 online、四端能打开并反代本套。**失败处理:**先看脚本错误、PM2日志和 nginx -t,不重启所有业务。

16|按独立配置安装第二套

执行位置/账号:新机 root;制品解压目录。

cp config/deploy.env config/deploy2.env
vi config/deploy2.env

必须改以下字段,其他路径与私有参数按第二套实际值:

INSTANCE=2
APP_ROOT=/data/apps/foodsafe2
SCHOOL_PORT=4202
REGULATOR_PORT=4302
SCREEN_PORT=5210
MOBILE_PORT=4212
GATEWAY_PORT=3301
bash scripts/install.sh config/deploy2.env
vi /data/apps/foodsafe2/services/gateway-service/.env

合并第二套私有配置:PORT=3301、第二套 DM 用户/密码、第二套设备/存储/MQTT配置、移动 URL 4212;不把第一套密码和目标模式照抄。

在同一 Bash 窗口复制第二套业务文件:

set -e
set -a
source config/deploy2.env
set +a
test -f "$APP_ROOT/deploy/ecosystem.config.cjs"
OLD_FILES_ROOT=/data/migration/foodsafe2
printf '第二套目标:%s\n' "$APP_ROOT"
ls -ld "$OLD_FILES_ROOT/data/uploads" "$OLD_FILES_ROOT/aiboxfile"
mkdir -p "$APP_ROOT/data/uploads" "$APP_ROOT/aiboxfile"
rsync -a "$OLD_FILES_ROOT/data/uploads/" "$APP_ROOT/data/uploads/"
rsync -a "$OLD_FILES_ROOT/aiboxfile/" "$APP_ROOT/aiboxfile/"
chown -R "$APP_USER:$(id -gn "$APP_USER")" "$APP_ROOT/data" "$APP_ROOT/aiboxfile"
rsync -rcn --itemize-changes "$OLD_FILES_ROOT/data/uploads/" "$APP_ROOT/data/uploads/"
rsync -rcn --itemize-changes "$OLD_FILES_ROOT/aiboxfile/" "$APP_ROOT/aiboxfile/"
runuser -u "$APP_USER" -- test -r "$APP_ROOT/data/uploads"
runuser -u "$APP_USER" -- test -x "$APP_ROOT/data/uploads"
runuser -u "$APP_USER" -- test -w "$APP_ROOT/data/uploads"
runuser -u "$APP_USER" -- test -r "$APP_ROOT/aiboxfile"
runuser -u "$APP_USER" -- test -x "$APP_ROOT/aiboxfile"
runuser -u "$APP_USER" -- test -w "$APP_ROOT/aiboxfile"
bash scripts/check.sh config/deploy2.env
bash scripts/activate.sh config/deploy2.env

在第二套四端分别核对 runtime-config.js,扫描二维码进入 4212,不只看二维码出现;第一套仍进入 4211。测试不同账号和数据库标识,证明没有串套。

**成功标准:**两个 PM2、两套配置、八个入口互相对应。**失败处理:**第二套错误不重装第一套;先核对是否 source 错 deploy.env、复制错旧文件目录或混用 DM 用户。

17|演练业务验收、PM2开机恢复和备份

执行位置:新机浏览器、应用用户、管理员;先受限访问。

17.1 两套分别执行并记录结果

检查 合格表现
登录/权限 真实客户测试账号可登录,学校和角色范围正确;无跨套数据
历史资料 人员、健康证、晨检/留样/消毒/农残及库存台账与迁移对账一致
旧普通图片 至少抽查证件、晨检、留样真实旧图,返回图片不是 HTML
AI盒子文件 抽查实际 /static/... 路径,文本解码和图片显示正常
新增/上传 经批准的测试操作可保存,刷新仍存在;新图落本套位置
报表/导出 日期、北京时间、数量和筛选正确,导出可打开
大屏/二维码 第二套二维码进入第二套,离线本地logo/二维码/播放器正常;大屏首页、视频模式、四路和轮播路径直接访问与刷新正常
视频/设备 实际播放、MQTT认证与Topic、设备回调、WS/SSE、重连和消息单次处理
任务 仅新机按需启用真实后台任务,演示AI任务关闭

图片测试用数据库中真实存在的路径,例如把以下示例替换后访问:

http://客户IP:4201/files/view/实际子目录/实际旧图.jpg
http://客户IP:4201/static/实际盒子目录/实际文件.txt

第二套端口改 4202。检查文件内容类型,不把 HTTP200 的 SPA首页当作图片成功。原库存在 OSS/外网业务图片的,确认已迁本地或仍有授权访问,不用程序 logo 正常代替旧图验收。

17.2 PM2与开机恢复

启用脚本明确使用 APP_USER 的 HOME 和 PM2_HOME。新建 foodsafe 默认家目录为 /var/lib/foodsafe,已有账号以 getent passwd 为准;root 也须显式指定这两项,避免接入另一用户或验收用 PM2 daemon。检查同一用户的进程与保存列表:

APP_HOME=$(getent passwd foodsafe | cut -d: -f6)
cd /data/apps/foodsafe1
runuser -u foodsafe -- env HOME="$APP_HOME" PM2_HOME="$APP_HOME/.pm2" PATH=/opt/foodsafe-node/bin:/usr/bin:/bin \
  /opt/foodsafe-node/bin/pm2 list
runuser -u foodsafe -- env HOME="$APP_HOME" PM2_HOME="$APP_HOME/.pm2" PATH=/opt/foodsafe-node/bin:/usr/bin:/bin \
  /opt/foodsafe-node/bin/pm2 save

注册 systemd,APP_HOME 按实际用户目录:

APP_HOME=$(getent passwd foodsafe | cut -d: -f6)
env HOME="$APP_HOME" PM2_HOME="$APP_HOME/.pm2" PATH=/opt/foodsafe-node/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
  /opt/foodsafe-node/bin/pm2 startup systemd -u foodsafe --hp "$APP_HOME"
systemctl cat pm2-foodsafe
systemctl enable pm2-foodsafe

按 PM2 实际输出核对生成服务,确保 Node/PM2 绝对路径仍有效,不能指 root 私有 nvm。已有 pm2-foodsafe 服务先核对,别覆盖其他项目共用列表。pm2 save 仅保存列表,不等于开机恢复已通过。

在演练阶段安排一次短暂停服,让systemd从已保存列表启动。先确认foodsafe是本手册两套食安的专用账号,pm2 list中只有foodsafe-1/foodsafe-2;若还管理其他业务,不执行下面的kill,先由运维安排。

# 前面的pm2 save必须成功;这些命令只操作foodsafe账号自己的daemon
APP_HOME=$(getent passwd foodsafe | cut -d: -f6)
cd /data/apps/foodsafe1
runuser -u foodsafe -- env HOME="$APP_HOME" PM2_HOME="$APP_HOME/.pm2" PATH=/opt/foodsafe-node/bin:/usr/bin:/bin \
  /opt/foodsafe-node/bin/pm2 kill
systemctl reset-failed pm2-foodsafe
systemctl start pm2-foodsafe
systemctl is-active pm2-foodsafe
systemctl is-enabled pm2-foodsafe

期望分别返回active、enabled。等待后端启动后,再检查两套/api/health与登录;失败先看journalctl -u pm2-foodsafe -n 50 --no-pager,核对HOME、PM2_HOME、PID文件及Node路径,不另起root后端抢端口。

在客户批准窗口重启新机(会影响该机前厅/达梦等同机业务,需统一安排),验证达梦、Nginx、Mosquitto和PM2恢复、两套登录、文件读写和设备重连。Nginx若由宝塔管理,按实际服务机制检查,不能只看 systemctl is-enabled nginx。

17.3 数据备份和空间

与厂家确认达梦归档、全备/增备实际作业,检查备份文件与失败告警,至少做一次恢复验证。业务图片文件也要备份,另存独立存储;只在本机同一块盘留副本不能抵御整机故障。给应用日志设轮转、保留期限与容量上限,定期查 df -h。

**成功标准:**演练验收留表、重启恢复验证通过、备份可恢复、记录恢复/迁移/文件同步耗时。**失败处理:**上线前关闭未通过项目,修正后重新验证,不能用构建和单元测试替代客户现场业务验收。

18|正式停机:让旧库与旧文件取得同一截止点

执行位置/账号:旧机管理员;客户约定窗口。

按顺序:

  1. 通知客户停用,记录停机时间和最后业务记录。
  2. 关闭旧食安业务入口或切维护页,阻止客户端继续写入。
  3. 停止旧机这两套的后台、定时任务、设备回调消费和 MQTT 订阅;暂停会自动拉起它们的 watchdog。
  4. 确认设备不会继续往旧后端上传,待在途请求/任务处理完。
  5. 核对业务数据不再增长。
  6. 按第 07 步重新备份两个库,另存 final/,生成 SHA;重新同步文件增量。

只关闭网页不算停写。停止命令由第 01 步登记的真实进程确定;不用 pm2 stop all、停共享 Redis/MQTT或 Nginx 总服务影响其他业务。

本阶段新系统也先受限,不接收正式写入。旧机原程序、MySQL库和文件保持完整供回退。

**成功标准:**两个最终源库和文件都对应同一停写时点,备份和截止记录完整。**失败处理:**发现后台/设备仍写旧库,先处理写入来源,再重做最终快照,不能把不同时点数据拼为最终迁移。

19|用最终快照重迁、对账,再切换入口

执行位置:迁移机、达梦、新机;迁移管理员/数据库管理员。

  1. 停止新机演练实例与任务(仅本套),保留演练记录。
  2. 在干净临时 MySQL8 恢复最终备份。
  3. 执行第 09 步审核通过且 SHA 相同的逐库升级脚本。
  4. 重复第 10~11 步导入核准的正式达梦目标;不能与演练夹具混导。只清理确认归属的演练目标,涉及删除由数据库负责人核对。
  5. 对最终源与目标重新逐表、逐项对账,记录截止时点关键数量。
  6. 同步最终图片/盒子文件到新机,按第 14/16 步只读比较和核对权限。
  7. check.sh 重新核查;已启用的服务用指定 PM2 名称定向重启,不重复执行首次 install/activate。

在第1项停止新机演练后端时,可在新机root执行(两套须已完成第17步):

APP_HOME=$(getent passwd foodsafe | cut -d: -f6)
cd /data/apps/foodsafe1
runuser -u foodsafe -- env HOME="$APP_HOME" PM2_HOME="$APP_HOME/.pm2" PATH=/opt/foodsafe-node/bin:/usr/bin:/bin \
  /opt/foodsafe-node/bin/pm2 stop foodsafe-1 foodsafe-2

完成最终迁库、文件同步及check后,定向重启形式如下,按本套配置和实际运行账号执行:

APP_HOME=$(getent passwd foodsafe | cut -d: -f6)
cd /data/apps/foodsafe1
runuser -u foodsafe -- env HOME="$APP_HOME" PM2_HOME="$APP_HOME/.pm2" PATH=/opt/foodsafe-node/bin:/usr/bin:/bin \
  /opt/foodsafe-node/bin/pm2 restart /data/apps/foodsafe1/deploy/ecosystem.config.cjs --update-env
cd /data/apps/foodsafe2
runuser -u foodsafe -- env HOME="$APP_HOME" PM2_HOME="$APP_HOME/.pm2" PATH=/opt/foodsafe-node/bin:/usr/bin:/bin \
  /opt/foodsafe-node/bin/pm2 restart /data/apps/foodsafe2/deploy/ecosystem.config.cjs --update-env

重启前确认该用户没有遗留 DM_* 等环境覆盖文件;如果曾手工通过 shell 注入,先用不打印敏感值的方式核对实际配置,再处理该实例,不凭 --update-env 推断所有旧变量自动消失。

小范围复验通过后,切换域名/DNS、代理、NAT或设备地址,确认实际请求落新机。新机接管旧 IP 必须先让旧机释放,避免 IP 冲突;保留旧机管理入口。外部回调、白名单、设备网段、视频/MQTT地址一起核对。

真实后台任务按需要恢复:AI盒子调度、得沃回调订阅、YLT token刷新;演示AI关闭。新旧两边不能同时处理同一正式设备消息或同时接收业务写入。

**成功标准:**用户与设备进入新机,两套各自数据正确,切换前后的新增记录衔接完整。**失败处理:**按第 21 步判断是否已经产生新数据,再决定回退。

20|交付验收与观察

执行位置:客户现场,实施人员和客户共同确认。

按第 17 步表格对正式最终数据再验一次,并补上:

旧机等观察期结束、备份可恢复并经客户验收后再安排下线;不在迁移当天顺手清空。

21|失败回退:先判断新系统是否已产生正式数据

情况 处理
新机尚未接收正式写入 停新机两套应用/任务,入口和设备切回旧机,恢复旧机相应进程与任务,核对旧数据截止点后恢复业务
新机已产生人员、晨检、留样、库存等正式记录/上传文件 先停写,备份新达梦数据、图片和日志,形成新旧差异清单;由实施/研发确定补录或同步方案,对账后再切,不能直接丢弃新数据
仅一套故障 确认设备和联动边界后,只回退该套;共享达梦/消息服务的操作须核对另一套影响

回退不等于删新机库,也不通过强行复制旧目录覆盖新文件。保留失败时点证据、原因与恢复时间。

22|后续改IP/端口,不重编译

执行位置/账号:新机 root;制品解压目录。只针对要修改的一套。

先备份该套私有 .env、deploy 和已安装 vhost。编辑 config/deploy.env(第二套为 deploy2.env),然后:

set -a
source config/deploy.env
set +a
python3 scripts/configure.py

它会重生成本套 runtime-config.js、deploy/nginx.conf 和 deploy/ecosystem.config.cjs,不会覆盖现有私有 .env。后端端口或移动地址变更时,手动同步 .env 中的 PORT/FOODSAFE_MOBILE_BASE_URL/PUBLIC_BASE_URL。

审核 deploy/nginx.conf,备份并替换该套已安装的 foodsafe-release-1.conf;执行实际 NGINX_BIN -t,通过后才 reload。按第19步只重启该套 PM2,再验证四端/二维码/回调并 pm2 save。

不要运行 install.sh 覆盖已有 APP_ROOT;不要把首次启用的 activate.sh 当成强制更新命令。

23|常见故障按这个顺序查

表现 优先检查与处理
安装提示 APP_ROOT 已存在 查是否提前创建/部分安装/已有业务;保留原目录,不 rm -rf 绕过。首次安装选确认空闲的新路径
node/pm2 无法执行 APP_USER对Node路径的遍历权限、PATH、架构/glibc;root 的 nvm 私有目录不能给普通用户直接用
Cannot find module dmdb/qrcode 包 SHA、node_modules/.pnpm是否完整、符号链接是否被破坏;恢复完整制品,不在现场装随机版本
达梦连接失败 .env 的 DB_KIND、IP端口、账号/口令、网络、防火墙和用户状态;不把原始连接串贴公开日志
DM schema incomplete 对照迁移日志和必需表/列,修补客户迁移包;不删除启动检查
后端 online但页面502 Nginx实际upstream端口、后端HOST、PM2是否循环重启、错误日志
某端口仍是旧页面/Vite ss -lntp、nginx -T、实际IP与缓存;只停冲突的旧食安服务,保留其他站点
图片200但显示不出 看响应Content-Type/文件内容,可能是SPA HTML;检查/static回落、/files代理与真实相对路径
旧图404、新图正常 源文件遗漏、子目录丢失、旧完整URL/OSS未处理、复制错套;核对文件与数据库URL
第二套跳第一套/二维码地址错 第二套deploy参数/runtime-config、FOODSAFE_MOBILE_BASE_URL、浏览器缓存,不靠改旧dist解决
MEGO返回503 未配置独立INTEGRATION_UPSTREAM;不用该功能可保持,不是主达梦接口故障
页面刷新404 SPA的try_files、root是否正确;程序由Nginx托管,不启动Vite掩盖配置问题
WebSocket/SSE不通 /websocket原生路径、Upgrade/Connection、proxy_buffering off、超时、代理与防火墙
MQTT设备不上报/重复上报 broker地址、TCP/WS/TLS、账号/ACL、Topic/Client ID、是否新旧都订阅,设备是否仍指旧机
重启服务器后服务没了 同一APP_USER的pm2 save、pm2-foodsafe unit、Node绝对路径、Mosquitto运行目录、实际Nginx启动机制
磁盘不断增长 应用日志、图片、临时导出与DM归档/备份分别检查;按保留策略清理,禁止删除未知DM数据/归档

查看本套日志的命令(root):

tail -n 100 /data/apps/foodsafe1/logs/error*.log
tail -n 100 /data/apps/foodsafe1/logs/out*.log
APP_HOME=$(getent passwd foodsafe | cut -d: -f6)
cd /data/apps/foodsafe1
runuser -u foodsafe -- env HOME="$APP_HOME" PM2_HOME="$APP_HOME/.pm2" PATH=/opt/foodsafe-node/bin:/usr/bin:/bin \
  /opt/foodsafe-node/bin/pm2 describe foodsafe-1

PM2可能实际生成error-0.log/out-0.log等带进程编号的文件,因此上面用通配符;也可在pm2 describe中查看真实日志位置。第二套替换目录和名称为foodsafe2/foodsafe-2。日志可能有业务隐私,分享前脱敏,不把真实.env打印到聊天。

附录A|交付版本与已验证范围

本包来源 standard_wxb@c40700715f8c23eb33eb855beec44b28dd19ec5b,含包内 provenance/ 留存的运行时地址、本地二维码/播放器、HOST配置与屏端base根路径修正等发布适配补丁。运行依赖包括 dmdb1.0.52452、qrcode1.5.4;客户现场不重新安装依赖或构建。

交付包含四端 dist、完整后端 dist与依赖闭包、config/、scripts/、RELEASE.json、PAYLOAD.sha256及来源补丁/锁文件。sql-review/ 内是待审核的MySQL8 foodsafe2历史升级候选,不自动执行。少量共享库 .ts 与发布辅助源码仍保留,因此不声称是加密“零源码”包;客户私有配置、数据库、上传文件和证书私钥不在其中。

项目 实际制品验证记录
后端/四端构建 通过
后端测试 本版136套、1049项通过(真实Linux构建目录全量Jest)
干净解压及脚本安装 两套通过;依赖闭包完整
普通账号PM2与Nginx运行 本版两套隔离目录、专用普通账号与临时端口实测
达梦只读查询 连接与用户匹配通过,当时测试账号118表可见
四端×两套浏览器 本版8入口HTTP200、无JS运行异常;两套新增四路/轮播及尾斜杠页面浏览器通过,页面空态用拦截响应验证,非真实视频联调
文件路由/运行时地址 本版/files/view、/static测试图片和两套运行时端口通过
本地二维码 本版两套均使用桩仓库验证端口与PNG,不冒充业务写入验收
Nginx重载失败恢复 本版故障注入验证撤销新增进程/站点、第一套保留及第二套恢复

这些证明程序制品可以安装运行,不等于已迁客户数据库或完成现场验收。本版运行验证使用同一测试DM账号并在QA进程驱动层拒绝非SELECT;独立探针仅执行SELECT。该写入拦截仅用于QA进程,不随交付应用启用。两套目录/端口验证不等于两套数据库权限隔离验收;客户必须改成各自业务用户/模式并迁移对账。客户正向登录、完整读写、历史图片、实际设备/视频、权限隔离、备份恢复及整机重启仍按正文完成。118不是客户库固定表数。

本次standard合入没有新增达梦表/列,db/upgrade/zywxb.sql与旧制品来源相同;已部署达梦的系统不能因为换本版就重复执行该MySQL8脚本。首次迁移仍按第08~11步逐库审核和对账。

本手册中的工具操作已对照达梦MySQL迁移指南、PM2开机恢复、nvm固定版本说明和Mosquitto配置手册核对。官网用于术语和工具操作参考,实际应用路径及配置以本次制品和本手册为准。

附录B|实施签认清单