CPT交付实施手册
展开步骤目录
归档状态:暂停草稿。 当前为此前暂停的在线打包部署草稿,基于bc3267ef旧基线;尚未同步c4070071及最新屏端和Nginx修复,不作为新版现场可执行方案。当前可部署版本请使用离线打包物中的已验收制品和总手册。

食安客户新服务器现场打包与两套系统部署手册

适用:客户新服务器为银河麒麟 V10,达梦已由厂家安装,可访问公网;业务代码采用 standard_wxb。目标是在客户新机上制作完整部署制品,再部署两套食安;以后在该机重复完成版本更新,不依赖临时服务器。

本手册另行新增,原《食安迁移部署总手册》和原制品保留。首次可继续用原制品上线;本手册用于建立现场打包能力,也包含两套系统首次安装、数据与文件迁移、Nginx、更新和回退。配套文件为 foodsafe-onsite-build-tools.tar.gz,必须与本手册一起保存。

先说明验证范围:配套工具经过源码核对、语法及隔离组装验证;本次没有登录客户新机,也没有在客户新机完成全量构建或业务验收。现场必须通过第 11~13、23 步后才能正式使用。后续源码变化可能要求研发更新发布适配工具;工具遇到不匹配会停止,不会自动猜测修改。

01|先看懂执行方式

01.1 分清账号和机器

文中标记 在哪里执行 做什么
管理电脑 你的 Mac / Windows 电脑 阅读、上传源码或文件、用浏览器验收
新机 root SSH 登录客户新麒麟服务器 装软件、创建账号、安装制品、配置 Nginx
新机 foodsafe-build 客户新机专用打包用户 拉取源码、安装开发依赖、build、生成归档
新机 foodsafe 客户新机专用应用用户 由 PM2 运行两套已编译后端
旧机管理员 原食安服务器 备份两个库、图片与配置,正式切换时停写
数据库管理员 厂家或客户 DBA 达梦账号、模式、迁库、备份、对账

foodsafe 和 foodsafe-build 是 Linux 用户,与达梦数据库用户是两回事。应用用户和打包用户分开,避免打包时操作到运行目录。数据库的两套用户也必须分开。

01.2 每个命令块怎么执行

  1. 先看当前步骤的机器和账号,确认后再复制命令。
  2. 命令统一在 Bash 下执行。SSH 进入后输入 bash 即可。
  3. 不复制代码块外面的说明;# 开头是解释,可以一起复制。
  4. 文中的 CHANGE_ME、中文示例 IP、/实际路径 必须先改成真实值。
  5. 一个代码块执行完,检查成功标准,再进行下一块。报错就停在本步。
  6. sudo -iu foodsafe-build 表示切换用户;exit 表示返回上一层,不是停止业务系统。
  7. vi 文件:按 i 编辑;完成后按 Esc,输入 :wq 回车;放弃本次编辑用 :q!。
  8. 数据库口令只填私有 .env 或交互输入;不要放进命令行、截图或公开文档。
  9. 本文给出重启、覆盖程序和迁库操作,实际执行要安排客户确认的维护窗口。写手册不等于这些操作已经执行。

01.3 两种使用路线

  • **新机还未部署:**按 02~24 顺序操作;第 14~17 包含首次迁库和文件准备。
  • **新机已有两套系统:**先完成 02~13 的打包环境与制品验证;以后走第 25~28 的更新流程,不重复首次安装、不重迁整库。

打包时不会启动后端、不会连接达梦、不会写 Nginx,也不会替你执行数据库升级。后端“启动成功”与“数据库数据正确”要分别验收。

02|填写两套系统登记表

执行位置:管理电脑记录,现场逐项核对。

项目 第一套 第二套
原 MySQL8 库 foodsafe foodsafe2
达梦登录用户 / 同名模式(建议名) FOODSAFE_APP FOODSAFE2_APP
达梦表空间(建议名) FOODSAFE_TBS FOODSAFE2_TBS
应用目录 /data/apps/foodsafe1 /data/apps/foodsafe2
PM2 进程 foodsafe-1 foodsafe-2
后端端口 3300 3301
学校端端口 4201 4202
监管端端口 4301 4302
大屏端口 5209 5210
移动端端口 4211 4212
本套旧图暂存目录 /data/migration/foodsafe1 /data/migration/foodsafe2
本套部署配置 /data/config/foodsafe/deploy1.env /data/config/foodsafe/deploy2.env

另外登记:新旧 IP/域名、SSH 端口、达梦版本/实例地址/端口、Nginx 安装与 include 路径、两个旧项目目录、图片真实路径、两份后端环境配置、设备回调地址、MQTT broker 和认证/Topic、视频服务、OSS、前厅联动、备份位置。

厂家一个达梦实例可以承载两个独立业务用户/模式,共用同一个数据库监听端口;不要求为食安另装两个数据库实例。上述命名以厂家实际分配为准,不能因为名字不同就重新建库。

**成功标准:**所有端口、账号和目录能一一对应;没有不知道属于哪套的旧库或文件。

03|检查新服务器和磁盘

执行位置:新机 root。

03.1 检查系统、架构和时间

bash
cat /etc/os-release
uname -m
getconf GNU_LIBC_VERSION
free -h
timedatectl

本配套工具限定 Linux x86_64、Node20.19.5;Node 运行条件需要 glibc≥2.28。达梦是厂家已装好的服务,不重装。若显示 aarch64、其他架构或 glibc 不满足,不套用本工具,交研发准备匹配版本。核对时间为北京时间,校时按客户现有 NTP 管理方式进行。

03.2 检查空间和占用

findmnt -T /data
findmnt -T /workspace
df -h /data /workspace
df -i /data /workspace
ss -lntp

应用、打包临时目录和在线图片优先放 /data;大的归档、备份暂存可以放 /workspace,不要放进达梦数据文件目录。历史报告中 /workspace IO 明显偏低,未复测前不把它作为高频构建目录。

规划 /data 至少能同时容纳源码、完整开发依赖、构建结果、新旧制品及业务文件;先预留约 20GB 打包余量,这只是初始预算,不含图片、数据库和增长空间。首次打包后用 du -sh /data/build/jobs/版本号 实测。大体量图片按实际容量另算,并保留磁盘余量。

**成功标准:**架构匹配,系统时间正确,端口无冲突,磁盘和 inode 都充足。**失败处理:**不在客户现场升级 glibc,也不删除达梦文件腾空间。

04|准备目录、两个 Linux 账号和基础命令

执行位置:新机 root。

04.1 创建不存在的专用账号

id foodsafe || useradd -m -r -d /var/lib/foodsafe -s /bin/bash foodsafe
id foodsafe-build || useradd -m -r -d /var/lib/foodsafe-build -s /bin/bash foodsafe-build
getent passwd foodsafe foodsafe-build

如果账号已存在,以 getent 返回的家目录为准,不修改已有账号归属,不重新创建。

04.2 创建分类目录

install -d -m 755 /data/apps /data/build /data/releases /opt/foodsafe-build-tools
install -d -m 750 -o foodsafe-build -g "$(id -gn foodsafe-build)" \
  /data/build/food_safety /data/build/jobs /data/releases/food_safety
install -d -m 700 /data/config/foodsafe /data/backups/foodsafe \
  /data/migration/foodsafe1 /data/migration/foodsafe2

**现在不要创建 /data/apps/foodsafe1 和 /data/apps/foodsafe2。**首次安装脚本要求目标不存在,防止覆盖数据。

04.3 补基础命令

先检查:

command -v git curl tar gzip python3 rsync make gcc g++
python3 --version

麒麟仓库可用且缺少上述软件时,按实际包管理器选择一条,不重复执行两条:

# 新机使用 dnf 时
sudo dnf install -y git curl tar gzip python3 rsync make gcc gcc-c++
# 新机只有 yum 时
sudo yum install -y git curl tar gzip python3 rsync make gcc gcc-c++

配套 Python 脚本只用标准库,使用 Python3.6 或以上。不要求安装 pip 库。系统软件仓库受客户管理,包缺失时请运维补充同版麒麟仓库或安装包,不混用其他发行版源,不执行全系统升级。

**成功标准:**两个账号存在;打包账号能写三个打包目录;基础命令都有路径。

05|安装 nvm、Node20、pnpm 和 PM2

执行位置:先新机 root 切到 foodsafe-build。已有固定版本可直接核验,不重复覆盖。

05.1 在打包用户下安装 nvm

sudo -iu foodsafe-build
bash
export NVM_DIR="$HOME/.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

预期 0.40.3。如果 GitHub 不通,即使普通网页可打开也不能继续安装;通过管理电脑下载可信版本,离线传入,不粘贴来源不明的一键脚本。现有 nvm 不是该版本也不覆盖,先由运维核对能否加载固定 Node。

05.2 安装本流程固定的软件版本

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
node -p "require(process.env.NVM_DIR+'/versions/node/v20.19.5/lib/node_modules/pm2/package.json').version"

应依次看到 v20.19.5、10.21.0、6.0.13。这是复现原制品的固定组合,不是要求所有软件永远停留在该版本;未来升级版本应由研发重新验证打包工具、运行依赖和业务后再统一调整。

05.3 把运行时独立放进 /opt

先 exit 返回 root;如果进入了额外 Bash,可能需要再 exit 一次。用 whoami 确认是 root。

whoami
BUILD_HOME=$(getent passwd foodsafe-build | cut -d: -f6)
RUNTIME_SOURCE="$BUILD_HOME/.nvm/versions/node/v20.19.5"
test -x "$RUNTIME_SOURCE/bin/node"

下面的复制只在目标目录尚不存在时执行;已经存在先核验,不覆盖。

if [ -e /opt/foodsafe-node-v20.19.5 ] || [ -L /opt/foodsafe-node-v20.19.5 ]; then
  echo "目标已存在,停止复制,先核对现有版本"
else
  cp -a "$RUNTIME_SOURCE" /opt/foodsafe-node-v20.19.5 && \
    chown -R root:root /opt/foodsafe-node-v20.19.5 && \
    chmod -R a+rX /opt/foodsafe-node-v20.19.5
fi

建立稳定路径,只在 /opt/foodsafe-node 不存在时执行:

if [ -e /opt/foodsafe-node ] || [ -L /opt/foodsafe-node ]; then
  echo "稳定路径已存在,停止创建,先核对目标"
else
  ln -s /opt/foodsafe-node-v20.19.5 /opt/foodsafe-node
fi

如果已有该路径,检查:

readlink -f /opt/foodsafe-node
/opt/foodsafe-node/bin/node -v

不要用 ln -sfn 覆盖已有运行环境。其他安装方式也可以,但后面的 Node、PM2 绝对路径必须统一修改并验证。

05.4 核验两个账号都能使用

runuser -u foodsafe-build -- /opt/foodsafe-node/bin/node -v
runuser -u foodsafe -- /opt/foodsafe-node/bin/node -v
runuser -u foodsafe-build -- env PATH=/opt/foodsafe-node/bin:/usr/bin:/bin \
  /opt/foodsafe-node/bin/pnpm -v

nvm 管理文件和下载缓存位于打包账号家目录,通常在系统盘;实际应用运行时复制在 /opt;打包目录与应用在 /data,不是全部软件都安装到 /data。之后不要删除 /opt/foodsafe-node-v20.19.5。

**成功标准:**两个账号均返回相同 Node20;pnpm 版本正确;应用用户无需访问 root 私有目录。

06|核验公网能否用于下载和代码更新

执行位置:新机 foodsafe-build。

sudo -iu foodsafe-build
export PATH=/opt/foodsafe-node/bin:/usr/local/bin:/usr/bin:/bin
curl -I --connect-timeout 10 --max-time 20 https://registry.npmjs.org/
curl -I --connect-timeout 10 --max-time 20 https://nodejs.org/
git ls-remote https://codeup.aliyun.com/60069db88deaa14d9e02b875/food_safety HEAD
  1. HTTP 返回可解释的成功或重定向响应;不是 DNS、证书或超时错误。
  2. 代码仓库可能要求登录,使用客户批准的个人或只读部署凭据。不要把 Token 拼进 URL。
  3. 若代码仓库无法访问,可以用第 09 步的源码离线传入方案;依赖仓库仍需可达,或提前准备完整依赖缓存。
  4. 不用 strict-ssl=false、跳过证书校验来掩盖网络问题。

**成功标准:**Git 与依赖仓库分别可用。普通网页能打开不能代替这两项检查。

07|准备 Nginx 和 Mosquitto

执行位置:新机 root。先 exit 回 root。

07.1 检查软件是否已安装

command -v nginx mosquitto mosquitto_passwd
nginx -v
systemctl list-unit-files | grep -Ei 'nginx|mosquitto'
ss -lntp | grep -E ':1883|:9001|:4201|:4202|:3300|:3301'

如果缺少且麒麟仓库提供,使用实际包管理器安装:

# dnf 示例;只有 yum 时换成 yum
sudo dnf install -y nginx mosquitto

如果提示没有包,不继续随机添加外部仓库。由运维提供同版麒麟、同架构且依赖完整的包。应用制品不包含 Nginx/Mosquitto 本体。

07.2 确认 Nginx 实际路径与 include

command -v nginx
readlink -f "$(command -v nginx)"
nginx -V 2>&1
nginx -T 2>&1 | less

按 q 退出 less。从输出核对:主配置路径,以及 http 块里面加载站点的 include。常见路径:

  • 系统包:程序 /usr/sbin/nginx,站点 /etc/nginx/conf.d。
  • 宝塔:程序 /www/server/nginx/sbin/nginx,站点 /www/server/panel/vhost/nginx。

把实际值填进第 18 步。不是看目录存在就认为它已被 include。不要覆盖整个 nginx.conf;同机还有前厅时,只新增本套站点。

若全新系统包 Nginx 尚未运行:

nginx -t
systemctl enable --now nginx
systemctl is-active nginx

宝塔按实际管理机制启动,不强行套 systemctl 服务名。

07.3 MQTT 分清“装软件”和“业务连到哪”

  1. 安装 Mosquitto 不代表设备会自动使用本机。
  2. 先登记旧系统实际使用的 broker、认证、Topic 和设备配置;厂家要求继续用原 broker 时,先沿用并测试连通,不擅自切到新机。
  3. 只有方案明确要新建本机 broker,才按下面模板配置。已有 broker 服务不覆盖、不另起一个抢 1883。

**仅全新、独立 Mosquitto 的示例:**先 mosquitto -h 核对配置路径;以下按系统包 /etc/mosquitto、服务用户 mosquitto 举例。

id mosquitto
install -d -m 750 -o root -g mosquitto /etc/mosquitto/foodsafe
mosquitto_passwd -c /etc/mosquitto/foodsafe/passwd foodsafe1
mosquitto_passwd /etc/mosquitto/foodsafe/passwd foodsafe2
chown root:mosquitto /etc/mosquitto/foodsafe/passwd
chmod 640 /etc/mosquitto/foodsafe/passwd

上面两次会交互输入密码;-c 只用于首次创建,不要第二次也加,避免覆盖第一套账户。

vi /etc/mosquitto/foodsafe/mosquitto.conf

示例内容:

listener 1883 127.0.0.1
allow_anonymous false
password_file /etc/mosquitto/foodsafe/passwd

这只允许本机应用访问。设备需要跨机器连入时,由网络管理员按设备网段调整监听和防火墙,并结合真实 Topic 配 ACL;两个密码用户本身不等于 Topic 已隔离。需要 WebSocket 9001 时还需确认该构建支持 WebSocket,不能把 TCP1883 直接当浏览器 ws 使用。

此示例配置不会自动被系统服务读取。以下仅用于全新专用broker,将系统服务明确指向本文件。先确认程序路径和已有单元,记录原配置:

command -v mosquitto
systemctl cat mosquitto
systemctl edit mosquitto

在编辑器写入下面覆盖内容。示例程序路径是/usr/sbin/mosquitto,若command -v不同须替换:

[Service]
Type=simple
ExecStart=
ExecStart=/usr/sbin/mosquitto -c /etc/mosquitto/foodsafe/mosquitto.conf
User=mosquitto
Group=mosquitto

保存退出后(编辑器如果是nano,Ctrl+O、回车保存,Ctrl+X退出):

systemctl daemon-reload
systemctl cat mosquitto
# 核对ExecStart已指向专用配置,且端口未被其他broker占用
systemctl enable --now mosquitto
systemctl status mosquitto --no-pager
ss -lntp | grep ':1883'

此最小broker示例未启用持久化和Topic ACL,不能把它当作设备交付验收完成。需要跨重启保留持久会话/排队消息时由设备协议方案补齐持久化目录及权限,并实测断网重连;不需要本机broker的部署可跳过全部新建配置,只核对实际外部broker。

已有服务在维护窗口使用 systemctl restart mosquitto 才会重读修改;共享 broker 必须评估其他设备影响。消息发布订阅测试使用厂家约定的测试 Topic,避免把测试消息发给真实设备。

**成功标准:**Nginx 路径/include 清楚;实际 broker 方案清楚;软件安装与业务配置分别确认。

08|上传并解压本手册配套打包工具

执行位置:管理电脑上传,新机 root 解压。

08.1 上传两个文件

把本次提供的以下文件上传到新机 /workspace/foodsafe-packages/:

  • foodsafe-onsite-build-tools.tar.gz
  • foodsafe-onsite-build-tools.tar.gz.sha256

目录不存在时先由新机 root 创建:

install -d -m 700 /workspace/foodsafe-packages

Mac 示例(把 IP 和本地文件路径换成实际值,SSH 非22端口时给 scp 加 -P 实际端口):

scp /本地路径/foodsafe-onsite-build-tools.tar.gz \
  /本地路径/foodsafe-onsite-build-tools.tar.gz.sha256 \
  root@客户新机IP:/workspace/foodsafe-packages/

Windows 可以用 WinSCP,上传到同一目录;不要上传成文本模式破坏压缩包。

08.2 校验和解压

新机 root:

cd /workspace/foodsafe-packages
sha256sum -c foodsafe-onsite-build-tools.tar.gz.sha256
tar -tzf foodsafe-onsite-build-tools.tar.gz | head -20

应看到 OK,且包内顶层为 foodsafe-onsite-build-tools/。只在目标子目录不存在时解压:

if [ -e /opt/foodsafe-build-tools/foodsafe-onsite-build-tools ]; then
  echo "工具已存在,停止解压,先核对版本"
else
  tar -xzf foodsafe-onsite-build-tools.tar.gz -C /opt/foodsafe-build-tools && \
    chmod -R a+rX /opt/foodsafe-build-tools/foodsafe-onsite-build-tools && \
    ls /opt/foodsafe-build-tools/foodsafe-onsite-build-tools/tools
fi

应有 build-release.sh、assemble.py、apply-release-overlay.py、locks/ 和 template/。

工具目录只读使用,打包输出不会写在里面。以后研发更新工具时解压到新的版本目录,核对后切换,保留旧版,不直接覆盖。

09|取得源码,锁定 standard_wxb 的发布提交

执行位置:新机 foodsafe-build。

09.1 首次从 Git 获取

sudo -iu foodsafe-build
export PATH=/opt/foodsafe-node/bin:/usr/local/bin:/usr/bin:/bin
cd /data/build/food_safety

若当前目录为空,执行:

git clone --branch standard_wxb --single-branch \
  https://codeup.aliyun.com/60069db88deaa14d9e02b875/food_safety .

已有仓库则不 clone,先检查:

git status --short --branch
git remote -v
git branch --show-current

只有工作区干净、分支为 standard_wxb、远端正确,才更新:

git pull --ff-only
git log -5 --oneline
git rev-parse HEAD

有本地改动或无法快进就停止,请研发处理,不能用 reset、clean、强制覆盖。有人持续提交不代表每个 HEAD 都可发布,让研发给出已测试的 40位完整提交号。

09.2 Git 不能访问时,传完整 Git bundle

在研发电脑的正确食安仓库执行,先确认本地分支已包含计划发布提交:

git status --short --branch
git bundle create foodsafe-standard-wxb.bundle standard_wxb

将 bundle 上传至新机打包用户可读的位置。新机的 /data/build/food_safety 必须为空,再以 foodsafe-build 执行:

cd /data/build/food_safety
git clone --branch standard_wxb /实际传入目录/foodsafe-standard-wxb.bundle .
git log -5 --oneline

该仓库 origin 指向 bundle,以后不能当作可联网的远端更新;继续由研发提供更新 bundle 或由运维核对后改为批准的 Git 地址。普通“源码压缩包”没有提交历史,不能直接交给本配套工具;可让研发提供 bundle,而不是在客户机器随便 init 后冒充正式版本。

09.3 核对发布适配

现有基线为 bc3267ef4c97fcfc38c1add26a2a9e1d94263275。原制品还包括:本地二维码、移动/大屏运行时地址、HOST监听、本地播放器与 logo 等适配,单纯拉原分支再 build 不一定包含这些。

配套工具先在独立副本应用精确匹配适配。后续研发已把适配合入源码、改动相关文件或增加运行依赖时,可能匹配失败;**此时让研发同步更新打包工具,不是你删除报错或重复打补丁。**没有把本次暂停的旧配置核查结果当作已修复代码。

**成功标准:**源码干净、分支与远端正确,研发已明确本次提交号、变更内容、数据库升级需求和验收结果。

10|在独立目录执行打包

执行位置:新机 foodsafe-build。打包不需要填写数据库口令。

10.1 设置本次版本号和提交号

export PATH=/opt/foodsafe-node/bin:/usr/local/bin:/usr/bin:/bin
export RELEASE_COMMIT=bc3267ef4c97fcfc38c1add26a2a9e1d94263275
export RELEASE_VERSION=20261020-bc3267ef-r1

上面是已知基线例子。以后把提交号换为研发确认值,版本号也换新,例如 20261025-实际短提交号-r1。版本号只用字母、数字、点、横线、下划线,不含空格。

10.2 限制负载并启动

nice -n 10 bash \
  /opt/foodsafe-build-tools/foodsafe-onsite-build-tools/tools/build-release.sh \
  /data/build/food_safety "$RELEASE_COMMIT" "$RELEASE_VERSION"

尽量选业务低峰时段。nice 只降低 CPU 调度优先级,不限制磁盘 IO、内存或数据库影响;即使有192核,也不在繁忙时段随意打包。系统资源不足时改用同架构构建机,不强行停数据库腾资源。

10.3 工具实际做了什么

  1. 检查 Linux x86_64、Node/pnpm 固定版本、源码干净和提交号归属。
  2. 创建 /data/build/jobs/本次版本号/,不会使用运行目录。
  3. 用 git archive 只导出已提交源码到 src/,不复制旧项目的未跟踪 .env、图片或依赖。
  4. 拒绝源码中真正的 .env 环境文件,防止把客户配置编进前端。
  5. 应用发布适配;在专用 extra/ 用提供的 npm 锁文件安装 qrcode1.5.4。
  6. 在 src/ 执行 pnpm install --frozen-lockfile --ignore-scripts,安装完整构建依赖而非仅生产依赖。
  7. 顺序构建 web-school、web-regulator、screen、mobile-device 四端,再执行 gateway 的 TypeScript build。
  8. 收集四端 dist、后端完整嵌套 dist、libs 和完整 node_modules 依赖树,包括 dmdb。
  9. 把临时 qrcode 外部链接替换为真实依赖;检查符号链接不能越出制品或失效。
  10. 在拷出的制品内部实际加载 dmdb/qrcode/NestJS 等模块,生成版本和 SHA256 记录。
  11. 输出新的部署包,不自动部署、不启动任何后端、不运行 SQL。

--ignore-scripts 是本工具固定策略,避免任意依赖安装脚本在客户机自动执行。以后增加需要 postinstall/native 编译的依赖时,研发必须更新并审核工具;不要现场全量放开脚本。冻结锁文件安装失败时不删除锁文件,也不改用 pnpm update。

10.4 查看结果和日志

ls -lh /data/releases/food_safety/
tail -n 60 "/data/build/jobs/$RELEASE_VERSION/logs/build.log"

成功应出现:

foodsafe-本次版本号-kylin-x64/
foodsafe-本次版本号-kylin-x64.tar.gz
foodsafe-本次版本号-kylin-x64.tar.gz.sha256

看到 打包完成(尚未部署) 才表示构建链完成。出现 tsc 错误,即使某些 .js 已生成,也不准继续发布;请研发修复后用新提交、新版本号重新构建。root 的 pnpm build 只是提示语,不能代替五个项目分别构建。

**失败处理:**保留本次 jobs 目录和日志,换新版本号重试;不要删除运行目录,不要用旧 dist 拼成“成功”。

11|检查制品完整性和达梦驱动

执行位置:新机 foodsafe-build。接第 10 步的同一窗口;换窗口先重设变量。

export PATH=/opt/foodsafe-node/bin:/usr/local/bin:/usr/bin:/bin
export KIT="/data/releases/food_safety/foodsafe-$RELEASE_VERSION-kylin-x64"
cd /data/releases/food_safety
sha256sum -c "foodsafe-$RELEASE_VERSION-kylin-x64.tar.gz.sha256"
cd "$KIT"
sha256sum -c PAYLOAD.sha256 > /dev/null
cat RELEASE.json

预期两个校验均通过。确认 source_commit 是本次指定版本;不是只看压缩包日期。

11.1 驱动到底怎么安装

当前服务 package.json 声明 dmdb: 1.0.52452。**打包目录执行 pnpm install 时安装驱动,组装时把它及依赖带进制品。**应用目录首次安装和以后更新不再执行 pnpm install;不需要安装 PHP 的达梦扩展、Java JDBC 或复制别的应用驱动。

单独验证制品里的驱动:

cd "$KIT/payload"
node - <<'JS'
const path = require('path');
const { createRequire } = require('module');
const req = createRequire(path.resolve('services/gateway-service/package.json'));
for (const name of ['dmdb', 'qrcode', 'dotenv', '@nestjs/core', 'mqtt']) {
  req(name);
  console.log(name + ' OK');
}
console.log('dmdb version:', req('dmdb/package.json').version);
JS

应逐项 OK 且 dmdb 版本符合本次版本清单。这里仅验证模块可加载,不连接数据库。

11.2 制品里应该有什么、不该有什么

payload/apps/web-school/dist/
payload/apps/web-regulator/dist/
payload/apps/screen/dist/
payload/apps/mobile-device/dist/
payload/services/gateway-service/dist/
payload/services/gateway-service/node_modules/
payload/services/gateway-service/package.json
payload/node_modules/                 包括 .pnpm 和完整链接目标
payload/libs/
scripts/ config/                     安装、检查、配置和首次启用工具
RELEASE.json PAYLOAD.sha256 SYMLINKS.json provenance/

不包含真实客户数据库、历史业务图片、实际 .env、证书私钥或许可证。为了 Node 模块正常运行会有依赖包源码、声明文件和少量共享 libs;不是完整业务源码仓库,也不是加密程序。

不得只复制后端 main.js;此项目入口是 services/gateway-service/dist/services/gateway-service/src/main.js,还需要同一 dist 树下其他模块。用 tar/scp/rsync 保留隐藏文件和符号链接,不通过 Windows 解压后挑文件上传。

**成功标准:**SHA、提交号、四端产物和驱动检查全部通过。

12|在隔离环境验收新制品

执行位置:新机 root、DBA、测试人员。此步在首次上线和每次版本更新前执行。

12.1 准备隔离测试数据

  1. DBA 从备份准备可丢弃的达梦测试模式,不能直接连接客户生产模式做新版本试运行。
  2. 确认测试账号只访问该测试模式,无跨生产模式权限;测试数据使用客户批准范围。
  3. 复制一组脱敏测试图片到独立目录,不能指向在线系统可写目录。
  4. 测试不接真实设备 broker、不填生产设备凭据、不共享真实队列,不启用回调/定时任务。
  5. 仅设 AUDIT_ENABLED=false 不能保证应用完全不写库,登录、守卫和业务路径仍可能写入;隔离模式必须真正隔离。

12.2 创建测试部署参数

以下以“第一套配置、但使用测试端口和目录”为例;INSTANCE=1 所以进程名仍是 foodsafe-1,不要执行 activate.sh,只手工使用专用测试进程名启动。

export KIT=/data/releases/food_safety/foodsafe-本次版本号-kylin-x64
cp "$KIT/config/deploy.env.example" /data/config/foodsafe/test.env
vi /data/config/foodsafe/test.env

填入:

INSTANCE=1
APP_ROOT=/data/test-foodsafe/本次版本号
APP_USER=foodsafe
PUBLIC_HOST=127.0.0.1
PUBLIC_SCHEME=http
LISTEN_HOST=127.0.0.1
SCHOOL_PORT=14201
REGULATOR_PORT=14301
SCREEN_PORT=15209
MOBILE_PORT=14211
GATEWAY_PORT=13300
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=

路径和端口必须空闲。测试只启动后端,以上四个前端端口不安装进现有 Nginx。

bash "$KIT/scripts/install.sh" /data/config/foodsafe/test.env
vi /data/test-foodsafe/本次版本号/services/gateway-service/.env

填测试 DM_HOST/PORT/USER/PASSWORD、测试 JWT;确认 PORT=13300、HOST=127.0.0.1,任务开关均0。测试不能直接照抄生产 .env。

12.3 校验并启动专用测试后端

bash "$KIT/scripts/check.sh" /data/config/foodsafe/test.env
APP_HOME=$(getent passwd foodsafe | cut -d: -f6)
TEST_ROOT=/data/test-foodsafe/本次版本号
cd "$TEST_ROOT"
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 start \
  services/gateway-service/dist/services/gateway-service/src/main.js \
  --name foodsafe-build-test --cwd "$TEST_ROOT" --interpreter /opt/foodsafe-node/bin/node \
  --output "$TEST_ROOT/logs/test-out.log" --error "$TEST_ROOT/logs/test-error.log"
curl --fail --max-time 10 http://127.0.0.1:13300/health

先检查该用户 PM2 中没有同名 foodsafe-build-test,若已存在先确认归属,不覆盖。测试期间不要 pm2 save,避免把临时测试进程保存为开机业务。

12.4 前端和业务验收

要验收登录、二维码、视频和上传,使用下面的独立测试Nginx。它只监听本机测试端口,不修改现有Nginx主配置、不reload生产Nginx。先按20.4的说明修正测试目录内的integration.config.json(生产端口换为本次测试端口,不能连接真实设备)。

# 新机root,TEST_ROOT仍为本次独立测试目录
export TEST_ROOT=/data/test-foodsafe/本次版本号
export TEST_NGINX=/usr/sbin/nginx
export TEST_MIME=/etc/nginx/mime.types
# 上面两项从07步的实际安装核对;宝塔mime通常在其conf目录
# 下面用户须为真实存在的Nginx worker用户,如nginx或www
export TEST_WORKER=nginx
id "$TEST_WORKER"
test -f "$TEST_MIME"
ss -lntp | grep -E ':14201|:14301|:15209|:14211'

最后一条应无输出,表示没有监听冲突。若存在测试残留先核实,不直接结束未知进程。再生成独立主配置:

python3 - <<'PYCONF'
import os,pathlib,re
r=pathlib.Path(os.environ['TEST_ROOT']);worker=os.environ['TEST_WORKER'];mime=os.environ['TEST_MIME']
if not re.fullmatch(r'[A-Za-z_][A-Za-z0-9_-]*',worker):raise SystemExit('worker用户非法')
if any(c in str(r)+mime for c in [' ', ';', '{', '}', '\n']):raise SystemExit('路径非法')
(r/'deploy/nginx-test.conf').write_text(f"""user {worker};
worker_processes 1;
pid {r}/deploy/nginx-test.pid;
error_log {r}/logs/nginx-test-error.log;
events {{ worker_connections 256; }}
http {{
  include {mime};
  default_type application/octet-stream;
  access_log {r}/logs/nginx-test-access.log;
  include {r}/deploy/nginx.conf;
}}
""")
PYCONF
"$TEST_NGINX" -p "$TEST_ROOT/deploy/" -c "$TEST_ROOT/deploy/nginx-test.conf" -t

语法通过才启动独立测试Nginx:

"$TEST_NGINX" -p "$TEST_ROOT/deploy/" -c "$TEST_ROOT/deploy/nginx-test.conf"
curl --fail --max-time 10 http://127.0.0.1:14201/api/health
curl --fail --max-time 10 http://127.0.0.1:14201/system/device-iot

在管理电脑另开终端建立SSH隧道(非22端口时加-p 实际SSH端口),保持窗口打开:

ssh -N -L 14201:127.0.0.1:14201 -L 14301:127.0.0.1:14301 \
  -L 15209:127.0.0.1:15209 -L 14211:127.0.0.1:14211 root@客户新机IP

电脑浏览器打开 http://127.0.0.1:14201,监管14301、大屏15209、移动14211。此时runtime-config中的127.0.0.1通过隧道访问,不对外开放测试端口。电脑端口已占用时先处理本机冲突,不任意改其中一处数字。

执行第23步清单和研发本次用例,只使用测试数据/隔离服务。测试后新机root只停止独立测试Nginx,必须带本次专用配置参数:

"$TEST_NGINX" -p "$TEST_ROOT/deploy/" -c "$TEST_ROOT/deploy/nginx-test.conf" -s quit

管理电脑隧道窗口按Ctrl+C退出。不能省略专用配置后对生产Nginx执行quit。

12.5 测试结束清理临时进程

只删除本次明确创建的测试进程;不使用 pm2 kill、delete all:

APP_HOME=$(getent passwd foodsafe | cut -d: -f6)
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 delete foodsafe-build-test

如果此前已经配置开机恢复,确认正式进程列表没变;保留本次测试日志。测试模式是否删除由 DBA 统一处理。

13|保存本次制品,明确是否涉及数据库升级

执行位置:实施人员与研发、DBA。

  1. 记录版本号、40位源码提交号、包SHA、打包与测试结果。
  2. 同一包用于两套系统;两套的数据库、地址和私有配置在部署时区分,不为第二套重新 build。
  3. 向研发确认本次是否有数据库结构变更。
  4. 首次 MySQL→达梦迁移走14~17;已迁达梦后的程序更新不再重新迁整库,按研发提供的达梦增量SQL单独审核执行。
  5. sql-review/zywxb-mysql8-candidate.sql 是历史 MySQL8 升级候选,不是达梦脚本,更不是每个版本都要重复执行。
  6. 制品、SHA和手册另存管理电脑或独立备份存储,不能只在客户同一台机器留一份。

**成功标准:**知道将要发布哪个版本、SQL是否要执行、哪些业务已验收;不知道就不开始正式切换。

14|首次迁移:备份旧机两个库、配置和图片

执行位置:旧机管理员。仅首次 MySQL→达梦迁移使用;已上线后更新程序跳过14~17。

14.1 先确认原程序实际读取的配置

  1. 记录旧机两个后端的进程名、工作目录和启动命令。
  2. 找到每套有效的 services/gateway-service/.env,如旧版从根目录 .env 补读,也保存根目录那份。
  3. 确认 MySQL 实际库名和连接地址,不能只按目录名推断。
  4. 记录旧 Nginx 站点、图片路径、AI盒子文件路径,以及程序保存的完整图片URL。
  5. 配置备份只保存在权限受限位置,不上传进 Git 或公开页面。

14.2 分别导出两个 MySQL8 库

在旧机执行,--host 改真实数据库地址,使用有备份权限的用户。示例数据库用户 backup_user 要替换;-p 后不写密码,回车后交互输入。

umask 077
BACKUP_TIME=$(date +%Y%m%d-%H%M%S)
BACKUP_DIR="/实际备份磁盘/foodsafe-$BACKUP_TIME"
mkdir -p "$BACKUP_DIR"
mysqldump --host=实际MySQL地址 --port=3306 --user=backup_user -p \
  --single-transaction --quick --routines --triggers --events \
  --hex-blob --set-gtid-purged=OFF --no-tablespaces foodsafe \
  > "$BACKUP_DIR/foodsafe.sql"

检查上一条命令退出码,再导第二套:

echo $?
# 上一条必须是0;非0时不要把残缺sql当备份
mysqldump --host=实际MySQL地址 --port=3306 --user=backup_user -p \
  --single-transaction --quick --routines --triggers --events \
  --hex-blob --set-gtid-purged=OFF --no-tablespaces foodsafe2 \
  > "$BACKUP_DIR/foodsafe2.sql"

用 DBA 认可的 MySQL8 客户端;若出现未知选项或权限错误,修正客户端/权限后重新导出,不把报错输出作为SQL备份。--single-transaction 对非事务表不能保证一致性,正式最终备份仍须停写并禁止并发DDL。

ls -lh "$BACKUP_DIR"/*.sql
sha256sum "$BACKUP_DIR"/*.sql > "$BACKUP_DIR/databases.sha256"

14.3 单独备份文件与配置

下面示例源目录须换成旧机真实路径,第一套和第二套分别做:

mkdir -p "$BACKUP_DIR/foodsafe1/files" "$BACKUP_DIR/foodsafe1/private"
rsync -a /第一套旧项目真实路径/data/uploads/ "$BACKUP_DIR/foodsafe1/files/uploads/"
rsync -a /第一套旧项目真实路径/aiboxfile/ "$BACKUP_DIR/foodsafe1/files/aiboxfile/"
cp -a /第一套旧项目真实路径/services/gateway-service/.env "$BACKUP_DIR/foodsafe1/private/gateway.env"

旧版如果图片在根目录 uploads/ 或 services/gateway-service/uploads/,先按数据库真实URL、代码配置核对归属,再整理到统一的 files/uploads/ 暂存结构。**两个来源有同名不同内容文件时不直接合并覆盖,先核对数据库引用。**没有某类文件时登记“不使用”,跳过该命令,不创建空目录冒充备份。

mkdir -p "$BACKUP_DIR/foodsafe2/files" "$BACKUP_DIR/foodsafe2/private"
rsync -a /第二套旧项目真实路径/data/uploads/ "$BACKUP_DIR/foodsafe2/files/uploads/"
rsync -a /第二套旧项目真实路径/aiboxfile/ "$BACKUP_DIR/foodsafe2/files/aiboxfile/"
cp -a /第二套旧项目真实路径/services/gateway-service/.env "$BACKUP_DIR/foodsafe2/private/gateway.env"

另备份旧Nginx站点、证书和有效根 .env,不要只备份后端模板。备份后先做恢复验证,原始备份保留不修改。

15|首次迁移:在隔离 MySQL8 副本审核并升级

执行位置:独立迁移环境,DBA/研发。不能直接在旧生产库试脚本。

  1. 准备与源库兼容的临时 MySQL8,使用与生产不同的地址和专用迁移账号。
  2. 建立 foodsafe_migrate 和 foodsafe2_migrate,字符集/排序规则先与源库核对。
  3. 导入备份,核对无失败、表数、关键表数量与备份来源一致。
mysql --host=隔离MySQL地址 --user=迁移账号 -p foodsafe_migrate \
  < /实际备份路径/foodsafe.sql
mysql --host=隔离MySQL地址 --user=迁移账号 -p foodsafe2_migrate \
  < /实际备份路径/foodsafe2.sql
  1. 获取计划发布版本的 db/upgrade/zywxb.sql。这份历史脚本可能针对 foodsafe2,两套来源结构可能不同,要分别比较。
  2. 研发审核脚本里的 USE、库名前缀、DROP/DELETE/TRUNCATE、重复列/索引、默认值和历史数据修改,改为隔离副本目标库;审核副本另存,不改原始备份。
  3. 在已经恢复的隔离库执行审核后的脚本。不能只做全局库名替换就认定安全;不能重复执行已经执行过的增量。
  4. 核对升级后的结构与本次代码要求一致,抽查关键业务和历史数据。
  5. 保存审核后脚本、执行日志、升级前后数量和版本号;演练通过后再准备正式最终迁移。

**成功标准:**两套隔离副本可重复恢复、升级成功、数据对账正确。**失败处理:**在隔离环境从原始备份恢复重演,不在旧生产库边试边改。

16|首次迁移:由厂家确认达梦账号,用 DTS 迁移

执行位置:DBA、达梦迁移客户端。新机达梦已经安装,不重装。

16.1 请厂家确认以下内容

  • 达梦服务实际版本、实例地址/端口、字符集、大小写相关设置及可用授权。
  • 两个独立业务用户及默认同名模式、对应表空间、所需权限和备份策略。
  • 两套数据库数据文件和日志所在路径,不占用应用构建目录。
  • 应用用各自业务用户,不使用 SYSDBA;不要给两套业务用户不必要的跨模式权限。

表空间物理文件路径由厂家根据现有实例确定。本手册不编造现场文件路径,也不要求新手套用未知CREATE USER/表空间命令。

16.2 DTS 逐套操作

  1. 在能同时连通隔离 MySQL8 和客户达梦的管理电脑打开达梦 DTS;Linux 图形客户端一般在安装目录 tool/ 下。
  2. 新建迁移工程,源选择 MySQL,目标选择达梦。
  3. 源填第一套已升级的 foodsafe_migrate;目标登录第一套达梦用户、选第一套模式。
  4. 测试两个连接。DTS 如缺 MySQL JDBC 驱动,使用与工具版本匹配的驱动,不能拿 Node 的 dmdb 代替。
  5. 选择需要的表、数据、约束、索引等,审核字段映射、长文本/二进制、时间、空值、自增/标识列、默认值和关键字。
  6. 先在演练目标执行,检查错误日志,不忽略失败表或无效对象。
  7. 对第二套 foodsafe2_migrate 重复,目标改为第二套模式,不能仍用第一套用户。
  8. 保存工程与迁移日志。正式迁移时使用最终停写备份形成的副本,不能把演练旧数据当最终数据。

16.3 对账和应用探针

DBA至少对照两边:表数、每表行数、关键主键范围、最新业务时间;抽查人员/权限、设备、晨检/留样、库存和图片路径。自增/标识下一值须验证,避免新记录主键冲突。

迁移对账完成后,才允许本手册 check.sh 用业务账号做 SELECT 探针。探针通过不代表业务验收全部完成。

禁止把 mysqldump 导出的 MySQL SQL直接交达梦执行;也不把它当纯文本全局替换后直接上线。

17|首次迁移:把旧文件和私有配置传到新机暂存

执行位置:旧机管理员上传,新机 root核对。

  1. 把第一套 files/uploads/ 内容放到 /data/migration/foodsafe1/data/uploads/。
  2. 把第一套 files/aiboxfile/ 放到 /data/migration/foodsafe1/aiboxfile/。
  3. 第二套放对应 foodsafe2 暂存目录。
  4. 私有配置放 /data/config/foodsafe/legacy1.env、legacy2.env,权限600,只供逐项合并。
  5. 不把旧项目整目录覆盖进新的运行目录;源码、node_modules、dist和私有.env不能这样混装。
  6. 传输后用 rsync 内容比较或备份SHA确认,抽查图片路径。

root检查:

ls -ld /data/migration/foodsafe1/data/uploads /data/migration/foodsafe2/data/uploads
chmod 600 /data/config/foodsafe/legacy1.env /data/config/foodsafe/legacy2.env

如果图存 OSS 或旧完整URL,单复制文件不能改地址。保持原存储服务时验证访问权限/网络;改本地存储时,必须先导出对象并由研发审核字段与路径转换。不要在全库盲目替换域名。

18|为两套正式系统分别填写部署参数

执行位置:新机 root。这一步不启动服务。

18.1 指定已通过验收的制品

export KIT=/data/releases/food_safety/foodsafe-本次版本号-kylin-x64
test -f "$KIT/RELEASE.json"
cp "$KIT/config/deploy.env.example" /data/config/foodsafe/deploy1.env
cp "$KIT/config/deploy.env.example" /data/config/foodsafe/deploy2.env
chmod 600 /data/config/foodsafe/deploy1.env /data/config/foodsafe/deploy2.env

只有初次创建配置才使用 cp。已有配置先备份和比较,不覆盖。

18.2 第一套 deploy1.env 完整示例

vi /data/config/foodsafe/deploy1.env
INSTANCE=1
APP_ROOT=/data/apps/foodsafe1
APP_USER=foodsafe
PUBLIC_HOST=CHANGE_ME
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 换为客户终端能访问的新IP/域名,只填主机名,不带协议、路径和端口。Nginx两个路径换为07步核实值。需要HTTPS时填客户有效证书和私钥路径,域名要被证书覆盖。

18.3 第二套 deploy2.env 完整示例

vi /data/config/foodsafe/deploy2.env
INSTANCE=2
APP_ROOT=/data/apps/foodsafe2
APP_USER=foodsafe
PUBLIC_HOST=CHANGE_ME
PUBLIC_SCHEME=http
LISTEN_HOST=0.0.0.0
SCHOOL_PORT=4202
REGULATOR_PORT=4302
SCREEN_PORT=5210
MOBILE_PORT=4212
GATEWAY_PORT=3301
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=

独立 MEGO 集成服务确实存在才填写 INTEGRATION_UPSTREAM=http://实际服务IP:端口。没有服务就留空,对应接口会返回503,相关功能不算完成。该服务不是主食安后端,不能凭空填3300凑数。

**成功标准:**两份端口各不相同,目录/实例号正确;不含数据库密码。

19|首次安装两套程序

执行位置:新机 root。APP_ROOT 必须不存在。已有部署走25步,不执行本步覆盖。

bash "$KIT/scripts/install.sh" /data/config/foodsafe/deploy1.env
bash "$KIT/scripts/install.sh" /data/config/foodsafe/deploy2.env

每条成功后才继续下一条。脚本生成但不启用:

/data/apps/foodsafe1/services/gateway-service/.env
/data/apps/foodsafe1/deploy/ecosystem.config.cjs
/data/apps/foodsafe1/deploy/nginx.conf
/data/apps/foodsafe1/apps/*/dist/runtime-config.js

第二套为同样目录结构,前缀改 foodsafe2。

检查:

cp -a "$KIT/RELEASE.json" /data/apps/foodsafe1/deploy/RELEASE.json
cp -a "$KIT/RELEASE.json" /data/apps/foodsafe2/deploy/RELEASE.json
ls -l /data/apps/foodsafe1/deploy /data/apps/foodsafe2/deploy
ls -l /data/apps/foodsafe1/services/gateway-service/dist/services/gateway-service/src/main.js

**失败处理:**目标已存在先查是否运行中的系统或上次部分安装,不删除目录绕过保护。Node路径不通就回05步修正;驱动缺失回11步查制品完整性,不在运行目录临时安装随机版本。

20|逐套合并后端和前端配置

执行位置:新机 root;旧私有.env与新模板逐项比较。

20.1 第一套后端最小配置

vi /data/apps/foodsafe1/services/gateway-service/.env
NODE_ENV=production
HOST=127.0.0.1
PORT=3300
DB_KIND=dm
DM_HOST=客户达梦地址
DM_PORT=厂家实际端口
DM_USER=FOODSAFE_APP
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
FOODSAFE_MOBILE_BASE_URL=http://客户地址:4211
ZHCT_BASE_URL=

BRIGHT_KITCHEN_MODE=2 只适用于客户继续使用原来的本地明厨模式;现场方案不同按旧有效值和研发确认调整,不机械填2。其他示例开关也不能代替客户业务选择。

20.2 第二套的差异

编辑 /data/apps/foodsafe2/services/gateway-service/.env,至少改为:

PORT=3301
DM_USER=FOODSAFE2_APP
DM_PASSWORD="第二套真实口令"
FOODSAFE_MOBILE_BASE_URL=http://客户地址:4212

其余逐项核对,不能复制第一套整份配置后忘了改用户、密钥、设备参数。当前程序没有 DM_SCHEMA 环境变量;实际登录用户/默认模式由厂家配置确定。

20.3 MQTT、视频、OSS等不能漏

下面是“核对清单”,不是要求全填,也不是模板已覆盖全部业务:

配置组 要核对什么
得沃 MQTT DEWO_MQTT_URIS、DEWO_MQTT_USERNAME、DEWO_MQTT_PASSWORD;还要核对库中每台设备的连接和认证配置,设备配置可能优先于全局值
设备和回调 设备厂商/类型、学校食堂绑定、签名密钥、回调地址、Topic;旧新后端不能同时消费同一真实消息
明厨亮灶 前后端模式一致,AIbox/TrustIVS/YLT等实际服务地址及账号凭据、推拉流和播放器网络
YLT 源码中使用的小写 ylt_* 名称按原名核对,不能自行改为大写
OSS / 图片 实际存储模式、桶/区域/域名/凭据、历史完整URL;不需要云存储时不要照搬云凭据,OSS_SWITCH=false 本身不能证明所有业务上传路径都本地化
前厅联动 ZHCT_BASE_URL 与该版本使用的联动账号、密码、权限范围;新IP/协议正确
MEGO 独立集成服务、EQUIPMENT_CODE、实际设备配置;不要保留示例设备编号
Redis / 审计 客户是否实际依赖、连接参数和权限;不把示例 false 直接作为关闭客户业务的决定
上传和公网地址 文件大小、后端限制、Nginx 50m限制是否匹配;设备需访问的图片地址是否包含真实端口
定时任务 正式切换前核对真实任务是否要启用;演示AI任务保持关闭

MQTT格式例子,只填真实客户配置:

DEWO_MQTT_URIS='tcp://实际Broker地址:1883'
DEWO_MQTT_USERNAME=实际账号
DEWO_MQTT_PASSWORD='实际口令'

不要使用聊天里旧环境的IP和密码作为默认值。新后端是否订阅/下发由实际业务与设备配置共同决定,填三行不代表设备闭环已验收。

布尔变量按代码约定填值。任务开关常用 0/1,审计/Redis常用 true/false;不要把所有变量统一写false。特别是设备签名旧逻辑可能把非空字符串 false 也视为启用,沿用厂家/研发确认的有效写法。

20.4 前端 integration.config.json

安装脚本会生成每套 runtime-config.js,但不会自动完成所有前端集成配置。检查:

cat /data/apps/foodsafe1/apps/web-school/dist/integration.config.json
vi /data/apps/foodsafe1/apps/web-school/dist/integration.config.json

依据实际业务填写,例:

{
  "SCHOOL_INTEGRATION_BASE": "/integration",
  "MEGO_CANDIDATES": "http://实际可访问的集成服务地址:实际端口",
  "EQUIPMENT_CODE": "本套真实设备编号",
  "BRIGHT_KITCHEN_MODE": 2,
  "OSS_SWITCH": false
}

这是说明字段的例子:没有 MEGO 时可删除 MEGO_CANDIDATES、EQUIPMENT_CODE,按项目方案禁用或不使用相应功能;有MEGO时填真实值并验收,不能留 localhost:4001/4003 或 DEMO-EC-0001。浏览器里的 localhost 指用户自己的电脑。

JSON不支持注释,修改后验证:

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

第二套单独编辑并验证,同步核对 runtime-config.js 的移动端为4212、大屏为5210。

使用无痕窗口验证,或在客户批准后清除该站点旧集成配置缓存;localStorage可覆盖部分参数。不要在不了解影响时清理浏览器所有站点的数据。前端有些配置在启动早期捕获,必须通过浏览器网络请求确认真实目标,而不是只看配置文件。

20.5 权限和配置优先级

chown foodsafe:"$(id -gn foodsafe)" /data/apps/foodsafe1/services/gateway-service/.env \
  /data/apps/foodsafe2/services/gateway-service/.env
chmod 600 /data/apps/foodsafe1/services/gateway-service/.env \
  /data/apps/foodsafe2/services/gateway-service/.env

私有 .env 是 dotenv文件,不要 source;密码含 #、空格时用引号。公开 deploy.env 则是受控 Shell 参数文件,按本手册内容填写,不能粘贴陌生脚本进去。

后端优先读 services/gateway-service/.env,进程环境变量仍有更高优先级。根 .env 不应再放一份互相冲突的配置;PM2更新时也要检查旧进程环境覆盖。

**成功标准:**两套数据库、设备、存储、前端地址各自正确,所有启用的业务都有配置,禁用项有明确记录。

21|把旧图片和附件放入两套安装目录

执行位置:新机 root;先确认19步已安装。

21.1 第一套

test -f /data/apps/foodsafe1/deploy/ecosystem.config.cjs
mkdir -p /data/apps/foodsafe1/data/uploads /data/apps/foodsafe1/aiboxfile
rsync -a /data/migration/foodsafe1/data/uploads/ /data/apps/foodsafe1/data/uploads/
rsync -a /data/migration/foodsafe1/aiboxfile/ /data/apps/foodsafe1/aiboxfile/
chown -R foodsafe:"$(id -gn foodsafe)" /data/apps/foodsafe1/data /data/apps/foodsafe1/aiboxfile
rsync -rcn --itemize-changes /data/migration/foodsafe1/data/uploads/ /data/apps/foodsafe1/data/uploads/
rsync -rcn --itemize-changes /data/migration/foodsafe1/aiboxfile/ /data/apps/foodsafe1/aiboxfile/

21.2 第二套

test -f /data/apps/foodsafe2/deploy/ecosystem.config.cjs
mkdir -p /data/apps/foodsafe2/data/uploads /data/apps/foodsafe2/aiboxfile
rsync -a /data/migration/foodsafe2/data/uploads/ /data/apps/foodsafe2/data/uploads/
rsync -a /data/migration/foodsafe2/aiboxfile/ /data/apps/foodsafe2/aiboxfile/
chown -R foodsafe:"$(id -gn foodsafe)" /data/apps/foodsafe2/data /data/apps/foodsafe2/aiboxfile
rsync -rcn --itemize-changes /data/migration/foodsafe2/data/uploads/ /data/apps/foodsafe2/data/uploads/
rsync -rcn --itemize-changes /data/migration/foodsafe2/aiboxfile/ /data/apps/foodsafe2/aiboxfile/

rsync -rcn 为只读内容比较,正常没有文件差异输出。末尾 / 表示复制目录里的内容,避免形成 uploads/uploads/。不要加 --delete;确实没有AI文件的系统跳过对应两条同步/比较并记录。

常见对应:

数据库/页面地址 第一套实际文件
/files/view/2026/10/20/a.jpg /data/apps/foodsafe1/data/uploads/2026/10/20/a.jpg
/api/files/view/2026/10/20/a.jpg 同上,Nginx去掉/api后转后端
/static/盒子目录/a.txt 通常为/data/apps/foodsafe1/aiboxfile/盒子目录/a.txt,以代码和旧机实际为准

apps/*/dist/static/ 里的logo等是程序资源,旧业务照片不要随意塞进去。数据库保存的是旧完整URL时另按17步处理。

22|启用第一套,再启用第二套 Nginx 和 PM2

执行位置:新机 root。首次启用专用脚本不用于已运行实例的更新。

22.1 第一套先检查后启动

export KIT=/data/releases/food_safety/foodsafe-本次版本号-kylin-x64
bash "$KIT/scripts/check.sh" /data/config/foodsafe/deploy1.env
bash "$KIT/scripts/activate.sh" /data/config/foodsafe/deploy1.env

只有check通过再执行activate。脚本会:

  1. 校验本套配置、驱动、四端dist,并用真实业务账号做达梦只读探针。
  2. 检查本套五个端口没有占用、PM2没有同名进程。
  3. 把本套 deploy/nginx.conf 安装为实际 include目录下的 foodsafe-release-1.conf。
  4. 执行 nginx -t;语法正确才启动后端。
  5. 使用 foodsafe账号启动 foodsafe-1,cwd是第一套项目根,运行编译后的嵌套入口。
  6. 等待健康检查成功,再 reload Nginx并保存PM2列表。

22.2 检查第一套,再启动第二套

curl --fail --max-time 10 http://127.0.0.1:4201/api/health
curl --fail --max-time 10 http://127.0.0.1:4201/system/device-iot
bash "$KIT/scripts/check.sh" /data/config/foodsafe/deploy2.env
bash "$KIT/scripts/activate.sh" /data/config/foodsafe/deploy2.env
curl --fail --max-time 10 http://127.0.0.1:4202/api/health

深链应返回前端页面;API应返回接口数据,不能是SPA首页。/health成功只是基本响应,还要23步。

22.3 Nginx配置到底在哪、怎么查看

生成位置:/data/apps/foodsafe1/deploy/nginx.conf
启用位置:实际NGINX_VHOST_DIR/foodsafe-release-1.conf
第二套:  /data/apps/foodsafe2/deploy/nginx.conf
          实际NGINX_VHOST_DIR/foodsafe-release-2.conf
less /data/apps/foodsafe1/deploy/nginx.conf
# 实际Nginx不是下面路径时换成登记值
/usr/sbin/nginx -t
/usr/sbin/nginx -T 2>&1 | less

无需另手写一份站点配置。生成文件本身包含四个完整 server,分别服务学校、监管、大屏和移动端,还包含upstream、WebSocket代理、文件转发和SPA深链规则。只展示核心路由帮助排障:

# 节选,实际以本套生成的完整文件为准,不要用这段替换完整配置
location ^~ /api/ { proxy_pass http://foodsafe_release_1/; }
location ^~ /websocket/ { proxy_pass http://foodsafe_release_1; }
location / { try_files $uri $uri/ /index.html; }

/api/ 尾斜杠对应去前缀;不要随便增删。不能把整个 /system/ 都代理后端,/system/device-iot 等是前端页面。生成器对确实属于后端的少量system接口做精确匹配。

本地文件代理、设备图片绝对地址还要确认原始端口是否保留;端口错时核查后端该版本的 PUBLIC_BASE_URL/设备地址配置,以及Nginx转发的Host,不直接全局替换代理头后上线。

22.4 失败如何处理

  • 配置/端口/同名进程冲突:脚本会停止,先查旧服务归属,不 kill 不明进程。
  • Nginx语法失败:不会重载旧站点;修正本套生成配置或安装路径。
  • 后端启动或健康失败:撤回本次新增进程和站点;查看本套logs。
  • reload失败:脚本尝试撤回新增站点和进程;核对服务日志。
  • 仅 pm2 save失败:服务可能已经运行,只是未持久化;修复权限再save,不能重复执行首次activate。

后台监听默认127.0.0.1,由Nginx转发。只有设备确需直连3300/3301时,才按网络方案改HOST并限制来源网段。开放八个前端端口只对客户需要的终端网段;不要因为客户可出公网就把业务端口对全互联网放开。

23|两套系统逐项验收,不能只看首页

执行位置:管理电脑浏览器、实施人员、DBA。正式接设备前先限制访问和订阅。

23.1 确认访问入口

入口 第一套 第二套
学校 http://客户地址:4201 http://客户地址:4202
监管 http://客户地址:4301 http://客户地址:4302
大屏 http://客户地址:5209 http://客户地址:5210
移动 http://客户地址:4211 http://客户地址:4212

客户端打不开但本机curl正常时,检查实际IP、监听、防火墙、网络ACL与HTTPS证书,不重新build碰运气。

23.2 每套都检查并登记

  1. 登录真实客户测试账号,学校/食堂/角色权限正确。
  2. 查出迁移前已知的人员、设备、晨检、留样、库存记录,与DBA对账结果一致。
  3. 抽查健康证、晨检、留样旧图及真实AI盒子文件;响应应为正确图片/文件,不是HTTP200的HTML首页。
  4. 经客户批准在测试范围做一次新增、修改和上传,刷新后仍存在,数据和图片都落本套。
  5. 验证日期筛选、报表、导出内容和北京时间。
  6. 访问 /system/device-iot、/bk-local/channels 后刷新,不能404,也不能返回后端错误JSON。
  7. 第一套二维码进入4211,第二套进入4212;大屏分别5209/5210。扫描打开验证,不只看二维码显示。
  8. 通过浏览器F12的Network看真实请求目标,避免浏览器localhost、开发服务器或另一套端口。
  9. 验证前厅联动、视频、MQTT认证、回调、设备下发与回执、任务去重;未接设备的项目标“未验收”,不能自动算通过。
  10. 第一套查看/新增的数据不能出现在第二套不该出现的位置;使用不同学校和独有记录证明未串模式。
  11. PM2只有两套正式进程;试运行时不同时启用旧机和新机真实任务。
  12. 研发给出的本次变更用例全部通过,记录未通过项和处理结果。

前端确认是dist:页面源代码和网络不出现 /@vite/client、/src/main.ts 开发入口,加载的是带hash的静态资源。数据库确认是达梦:业务配置DB_KIND=dm、实际业务用户探针和DBA真实查询对应,不仅凭页面有数据判断。

**成功标准:**上述验收按两套分别留记录,所有计划上线的业务通过;没有把“系统能打开”当“业务全对”。

24|开机恢复、正式切换和最终备份

执行位置:新机 root、旧机管理员和DBA;按客户窗口。

24.1 PM2注册开机恢复

先保存正式进程列表,确认没有临时测试进程:

APP_HOME=$(getent passwd foodsafe | cut -d: -f6)
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

只在还没有正确服务时注册,已有服务先核对,不覆盖其他业务:

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

核对Node、PM2、HOME、PM2_HOME是本手册路径。pm2 save不等于已通过重启恢复测试。安排演练窗口由运维验证systemd从保存列表恢复,并检查两套接口和文件。整机重启会同时影响达梦、前厅等同机业务,须统一安排,不直接执行reboot。

24.2 正式切换顺序

  1. 演练全部通过后,通知客户维护窗口,记录旧机最后业务时间。
  2. 关闭旧业务入口并停止旧两套后端、定时任务、回调消费和自动拉起机制,等在途请求结束。
  3. 确认旧库不再增长,做最终两库备份和文件增量备份。
  4. 用最终备份重建隔离MySQL升级副本、完成最终DTS迁移;不要覆盖已产生新业务的达梦模式。
  5. 最终文件增量同步,核对新库、新图同一截止点。
  6. 两套新机业务核对通过后,切DNS/IP、客户端与设备地址,按方案只在新机启用真实任务/订阅。
  7. 再做两套登录、数据、图片、设备和报表验收;记录切换时刻及是否已有新写入。
  8. DBA执行新达梦备份并确认恢复能力;文件和配置也备份至独立存储。
  9. 旧机保持冻结用于回退观察,不立即删除、不再作为正常写入端。

24.3 回旧服务器的限制

若新机尚无新业务,可按预案切回旧入口并只恢复旧机任务;新机任务先停。若新机已写入新业务,不能直接切回旧库,否则会丢新记录;先由DBA处理数据追平/回迁和设备消息截止点。

25|以后代码变更:重新制作新制品

执行位置:新机 foodsafe-build。原系统继续运行。

25.1 获取研发批准的新版本

sudo -iu foodsafe-build
export PATH=/opt/foodsafe-node/bin:/usr/local/bin:/usr/bin:/bin
cd /data/build/food_safety
git status --short --branch
git branch --show-current

确认干净、standard_wxb,才执行:

git pull --ff-only
git log -5 --oneline

由研发确认40位提交号、相关SQL和配置变更。若源码已合入发布适配或依赖结构有变,先更新本手册配套工具至研发验证版,再打包,不删工具里的保护判断。

25.2 新版本号重新打包

export RELEASE_COMMIT=CHANGE_ME_研发提供的40位提交号
export RELEASE_VERSION=20261025-CHANGE_ME-r1
nice -n 10 bash \
  /opt/foodsafe-build-tools/foodsafe-onsite-build-tools/tools/build-release.sh \
  /data/build/food_safety "$RELEASE_COMMIT" "$RELEASE_VERSION"

命令中的占位内容必须先替换。走第11~13步校验/隔离验收。新版本失败不会自动更新在线程序。更新纯前端或后端也建议仍产出同一版本完整制品,避免两套系统的模块来源混杂。

通常不需要重装nvm/Node/PM2/Nginx/Mosquitto,不重迁MySQL,不复制整份旧源码覆盖应用,不重新安装运行目录依赖。

26|更新第一套:先备份,再替换程序

执行位置:新机 root;客户维护窗口。这里只更新第一套,通过后才做第二套。

26.1 确认新包、旧运行目录和备份位置

export NEW_KIT=/data/releases/food_safety/foodsafe-新版本号-kylin-x64
export APP_ROOT=/data/apps/foodsafe1
export INSTANCE=1
export DEPLOY_ENV=/data/config/foodsafe/deploy1.env
export BACKUP_DIR="/data/backups/foodsafe/$(date +%Y%m%d-%H%M%S)-instance1"
test -f "$NEW_KIT/RELEASE.json"
test -f "$APP_ROOT/deploy/ecosystem.config.cjs"
cat "$NEW_KIT/RELEASE.json"

26.2 备份程序、配置和站点

install -d -m 700 "$BACKUP_DIR"
rsync -a "$APP_ROOT/" "$BACKUP_DIR/app/" \
  --exclude='/data/' --exclude='/aiboxfile/' --exclude='/logs/'
cp -a "$DEPLOY_ENV" "$BACKUP_DIR/deploy.env"

上述备份不含体量大的业务数据和日志。业务文件由既定备份方案保护;本次SQL或功能会改写图片/附件时,应另做一致性文件快照/备份。没有恢复能力不要上线。

加载本套公开部署参数并备份真实站点:

set -a
source "$DEPLOY_ENV"
set +a
cp -a "$NGINX_VHOST_DIR/foodsafe-release-$INSTANCE.conf" "$BACKUP_DIR/nginx.conf"

.env已经随app备份在权限700目录,不输出内容。DBA在本窗口做达梦备份,确认数据库增量脚本和回退策略;无SQL变更也应有可用近期备份。

26.3 先停止本套写入

  1. 确认本套客户用户已退出或入口处于维护状态。
  2. 暂停/缓冲本套设备写入,避免更新中请求丢失;不能影响第二套设备。
  3. 停止本套进程:
APP_HOME=$(getent passwd foodsafe | cut -d: -f6)
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

若本次数据库变化会影响第二套或其他应用,不能只停第一套就执行;由研发/DBA统一发布顺序。

26.4 替换程序,保留本套数据和私有配置

新payload不含真实.env,但替换node_modules必须删除旧残留依赖,所以限定在明确的程序子目录同步。先核对变量:

printf '新制品=%s\n目标=%s\n' "$NEW_KIT" "$APP_ROOT"
test "$APP_ROOT" = /data/apps/foodsafe1
(cd "$NEW_KIT" && sha256sum -c PAYLOAD.sha256 > /dev/null)

确认后逐项执行;这些 --delete 只限下列程序目录,绝不能扩大到APP_ROOT、data、aiboxfile或备份目录:

rsync -a --delete "$NEW_KIT/payload/node_modules/" "$APP_ROOT/node_modules/"
rsync -a --delete "$NEW_KIT/payload/services/gateway-service/node_modules/" \
  "$APP_ROOT/services/gateway-service/node_modules/"
rsync -a --delete "$NEW_KIT/payload/services/gateway-service/dist/" \
  "$APP_ROOT/services/gateway-service/dist/"
rsync -a --delete "$NEW_KIT/payload/libs/" "$APP_ROOT/libs/"
cp -a "$NEW_KIT/payload/services/gateway-service/package.json" \
  "$APP_ROOT/services/gateway-service/package.json"
for app in web-school web-regulator screen mobile-device; do
  rsync -a --delete --exclude='/runtime-config.js' --exclude='/integration.config.json' \
    "$NEW_KIT/payload/apps/$app/dist/" "$APP_ROOT/apps/$app/dist/"
done

这里主动保留运行时地址和现有 integration.config.json;若新版本这两个文件的字段或语义变化,按研发说明手工合并,不能永远照搬旧文件。

此流程假设仍是本文既定payload结构。以后新增工作区运行依赖、服务或变更入口时,研发必须更新工具和本节替换清单,不能继续靠旧清单漏拷。

26.5 合并新配置、执行审核SQL

  1. 比较新版本发布说明和私有.env,新增参数逐项合并,保留本套DM/JWT/设备/存储配置。
  2. 需要SQL时,由DBA执行与新版本匹配、经过演练的达梦增量SQL,保存回执。不能重新执行MySQL的zywxb.sql。
  3. 用新工具重新生成本套PM2/Nginx/运行地址配置:
set -a
source "$DEPLOY_ENV"
set +a
python3 "$NEW_KIT/scripts/configure.py"
bash "$NEW_KIT/scripts/check.sh" "$DEPLOY_ENV"

生成器不会覆盖已存在的私有.env,但会更新runtime-config.js和deploy目录的配置。确认integration.config.json仍是本套有效值。

26.6 更新站点、启动并验收

install -m 644 "$APP_ROOT/deploy/nginx.conf" \
  "$NGINX_VHOST_DIR/foodsafe-release-$INSTANCE.conf"
"$NGINX_BIN" -t

**上一条不通过立即停下,恢复备份站点,禁止reload。**通过后,以同一用户/同一PM2_HOME重建本套进程,避免旧进程环境残留;不删除其他进程:

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 delete foodsafe-1
cd "$APP_ROOT"
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 start deploy/ecosystem.config.cjs

确认后端响应,再重载Nginx:

curl --fail --max-time 10 http://127.0.0.1:3300/health
"$NGINX_BIN" -s reload
curl --fail --max-time 10 http://127.0.0.1:4201/api/health

启动可能需要数秒,首次curl失败时先查进程和日志,再等待重试,不把失败命令直接跳过。执行23步验收和本次变更用例,通过后保存进程并恢复本套入口/设备:

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
cp -a "$NEW_KIT/RELEASE.json" "$APP_ROOT/deploy/RELEASE.json"

**成功标准:**第一套实际运行新版本、关键业务通过、第二套仍正常;再继续27步。

27|更新第二套:同一制品,独立备份和执行

执行位置:新机root;第一套验收通过后执行。使用与第一套相同的NEW_KIT,不重新打包。下面已经写成第二套命令,不需要你临时修改第一套数字。

27.1 确认新包、旧运行目录和备份位置

export NEW_KIT=/data/releases/food_safety/foodsafe-新版本号-kylin-x64
export APP_ROOT=/data/apps/foodsafe2
export INSTANCE=2
export DEPLOY_ENV=/data/config/foodsafe/deploy2.env
export BACKUP_DIR="/data/backups/foodsafe/$(date +%Y%m%d-%H%M%S)-instance2"
test -f "$NEW_KIT/RELEASE.json"
test -f "$APP_ROOT/deploy/ecosystem.config.cjs"
cat "$NEW_KIT/RELEASE.json"

27.2 备份程序、配置和站点

install -d -m 700 "$BACKUP_DIR"
rsync -a "$APP_ROOT/" "$BACKUP_DIR/app/" \
  --exclude='/data/' --exclude='/aiboxfile/' --exclude='/logs/'
cp -a "$DEPLOY_ENV" "$BACKUP_DIR/deploy.env"

上述备份不含体量大的业务数据和日志。业务文件由既定备份方案保护;本次SQL或功能会改写图片/附件时,应另做一致性文件快照/备份。没有恢复能力不要上线。

加载本套公开部署参数并备份真实站点:

set -a
source "$DEPLOY_ENV"
set +a
cp -a "$NGINX_VHOST_DIR/foodsafe-release-$INSTANCE.conf" "$BACKUP_DIR/nginx.conf"

.env已经随app备份在权限700目录,不输出内容。DBA在本窗口做达梦备份,确认数据库增量脚本和回退策略;无SQL变更也应有可用近期备份。

27.3 先停止本套写入

  1. 确认本套客户用户已退出或入口处于维护状态。
  2. 暂停/缓冲本套设备写入,避免更新中请求丢失;不能影响第二套设备。
  3. 停止本套进程:
APP_HOME=$(getent passwd foodsafe | cut -d: -f6)
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-2

若本次数据库变化会影响第二套或其他应用,不能只停第二套就执行;由研发/DBA统一发布顺序。

27.4 替换程序,保留本套数据和私有配置

新payload不含真实.env,但替换node_modules必须删除旧残留依赖,所以限定在明确的程序子目录同步。先核对变量:

printf '新制品=%s\n目标=%s\n' "$NEW_KIT" "$APP_ROOT"
test "$APP_ROOT" = /data/apps/foodsafe2
(cd "$NEW_KIT" && sha256sum -c PAYLOAD.sha256 > /dev/null)

确认后逐项执行;这些 --delete 只限下列程序目录,绝不能扩大到APP_ROOT、data、aiboxfile或备份目录:

rsync -a --delete "$NEW_KIT/payload/node_modules/" "$APP_ROOT/node_modules/"
rsync -a --delete "$NEW_KIT/payload/services/gateway-service/node_modules/" \
  "$APP_ROOT/services/gateway-service/node_modules/"
rsync -a --delete "$NEW_KIT/payload/services/gateway-service/dist/" \
  "$APP_ROOT/services/gateway-service/dist/"
rsync -a --delete "$NEW_KIT/payload/libs/" "$APP_ROOT/libs/"
cp -a "$NEW_KIT/payload/services/gateway-service/package.json" \
  "$APP_ROOT/services/gateway-service/package.json"
for app in web-school web-regulator screen mobile-device; do
  rsync -a --delete --exclude='/runtime-config.js' --exclude='/integration.config.json' \
    "$NEW_KIT/payload/apps/$app/dist/" "$APP_ROOT/apps/$app/dist/"
done

这里主动保留运行时地址和现有 integration.config.json;若新版本这两个文件的字段或语义变化,按研发说明手工合并,不能永远照搬旧文件。

此流程假设仍是本文既定payload结构。以后新增工作区运行依赖、服务或变更入口时,研发必须更新工具和本节替换清单,不能继续靠旧清单漏拷。

27.5 合并新配置、执行审核SQL

  1. 比较新版本发布说明和私有.env,新增参数逐项合并,保留本套DM/JWT/设备/存储配置。
  2. 需要SQL时,由DBA执行与新版本匹配、经过演练的达梦增量SQL,保存回执。不能重新执行MySQL的zywxb.sql。
  3. 用新工具重新生成本套PM2/Nginx/运行地址配置:
set -a
source "$DEPLOY_ENV"
set +a
python3 "$NEW_KIT/scripts/configure.py"
bash "$NEW_KIT/scripts/check.sh" "$DEPLOY_ENV"

生成器不会覆盖已存在的私有.env,但会更新runtime-config.js和deploy目录的配置。确认integration.config.json仍是本套有效值。

27.6 更新站点、启动并验收

install -m 644 "$APP_ROOT/deploy/nginx.conf" \
  "$NGINX_VHOST_DIR/foodsafe-release-$INSTANCE.conf"
"$NGINX_BIN" -t

**上一条不通过立即停下,恢复备份站点,禁止reload。**通过后,以同一用户/同一PM2_HOME重建本套进程,避免旧进程环境残留;不删除其他进程:

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 delete foodsafe-2
cd "$APP_ROOT"
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 start deploy/ecosystem.config.cjs

确认后端响应,再重载Nginx:

curl --fail --max-time 10 http://127.0.0.1:3301/health
"$NGINX_BIN" -s reload
curl --fail --max-time 10 http://127.0.0.1:4202/api/health

启动可能需要数秒,首次curl失败时先查进程和日志,再等待重试,不把失败命令直接跳过。执行23步验收和本次变更用例,通过后保存进程并恢复本套入口/设备:

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
cp -a "$NEW_KIT/RELEASE.json" "$APP_ROOT/deploy/RELEASE.json"

**成功标准:**第二套实际运行新版本、关键业务通过、第一套仍正常;完成两套交叉验收并归档。

28|程序更新失败如何回退

执行位置:新机 root、DBA;先停止失败实例,暂停本套写入。

28.1 先判断数据库能不能配旧程序

  • 没有结构/数据变更、旧程序仍兼容:可以只回退程序和配置。
  • 有不兼容SQL或新版本已产生旧版不认识的数据:先由DBA/研发确认恢复与追平方案,不只恢复代码。
  • 不能直接把数据库恢复到早期时间点而丢弃切换后的业务;必须记录和处理新增数据。

28.2 第一套程序回退示例

先填写本次真实备份路径,不能用“最近一个”猜测:

export APP_ROOT=/data/apps/foodsafe1
export BACKUP_DIR=/data/backups/foodsafe/本次真实备份目录-instance1
export DEPLOY_ENV=/data/config/foodsafe/deploy1.env
APP_HOME=$(getent passwd foodsafe | cut -d: -f6)
test -f "$BACKUP_DIR/app/deploy/ecosystem.config.cjs"
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

逐个恢复程序目录,不覆盖业务数据目录:

for part in node_modules libs; do
  rsync -a --delete "$BACKUP_DIR/app/$part/" "$APP_ROOT/$part/"
done
for part in dist node_modules; do
  rsync -a --delete "$BACKUP_DIR/app/services/gateway-service/$part/" \
    "$APP_ROOT/services/gateway-service/$part/"
done
cp -a "$BACKUP_DIR/app/services/gateway-service/package.json" \
  "$APP_ROOT/services/gateway-service/package.json"
cp -a "$BACKUP_DIR/app/services/gateway-service/.env" \
  "$APP_ROOT/services/gateway-service/.env"
for app in web-school web-regulator screen mobile-device; do
  rsync -a --delete "$BACKUP_DIR/app/apps/$app/dist/" "$APP_ROOT/apps/$app/dist/"
done
rsync -a --delete "$BACKUP_DIR/app/deploy/" "$APP_ROOT/deploy/"
cp -a "$BACKUP_DIR/deploy.env" "$DEPLOY_ENV"
set -a
source "$DEPLOY_ENV"
set +a
cp -a "$BACKUP_DIR/nginx.conf" "$NGINX_VHOST_DIR/foodsafe-release-$INSTANCE.conf"
"$NGINX_BIN" -t

语法通过且DBA确认数据库适配旧程序后,按26.6同一方式delete/start 本套PM2、检查3300健康、reload Nginx、检查4201,并重新验收/save。

第二套单独选择它自己的备份、配置、PM2名称和端口;不要把第一套备份恢复给第二套。失败日志和失败制品保留给研发,不先删掉证据。

29|以后每次更新,照这张短清单核对

  1. 研发给:已测试提交号、改动说明、配置变更、达梦SQL及回退要求。
  2. 打包用户:拉标准分支,干净工作区,固定提交号,新版本号打包。
  3. 校验:包SHA、payload、驱动、版本清单。
  4. 隔离验收:测试模式、测试端口、测试图片;不消费真实设备。
  5. 维护窗口:数据库备份、程序和私有配置备份、文件恢复能力确认。
  6. 第一套:停写→停进程→替换程序→合并配置/审核SQL→检查→启动→验收→保存。
  7. 第二套:单独备份,重复同样流程;两套不串库不串图不串设备。
  8. 归档:新旧制品、SHA、发布记录、验收记录、备份位置和恢复方法。
  9. 定期清理:由实施人员核对后清理确定无用的打包中间目录;至少保留当前和上一版可恢复制品。不要删除应用目录、数据库文件或正在用的Node运行时。

30|常见报错:先查什么

现象 先检查 不要怎么做
nvm找不到 当前用户、NVM_DIR、是否加载nvm.sh;打包可用固定/opt Node PATH 反复在root下重装
node/pnpm找不到 export PATH=/opt/foodsafe-node/bin:/usr/local/bin:/usr/bin:/bin 在运行目录临时装另一个Node
frozen-lockfile失败 研发是否同步提交package.json和锁文件、pnpm版本是否固定 删锁文件或pnpm update
发布适配断言失败 该提交是否已合入或改过相关源码,交研发更新工具 删除assert、强行重复打补丁
tsc/vite报错 本次日志、提交号、依赖、实际失败文件 使用报错后残留的dist发布
esbuild/native依赖缺失 本机架构、下载是否完整、是否新增需安装脚本的依赖 在客户机无条件开启所有依赖脚本
Cannot find module dmdb/qrcode 完整node_modules、.pnpm和链接、制品校验 只拷main.js或随意安装最新版
达梦连不上 实例地址/端口、厂家用户/口令、网络权限、当前实际配置 把DB_KIND改回mysql凑启动
页面有数据但疑似不是达梦 实际进程环境、DB_KIND、DM用户、探针和DBA查询 仅凭页面判断数据库类型
Nginx502 PM2状态、后端端口、cwd、日志、权限 用另一个root后端抢端口
刷新页面404/JSON SPA规则、是否把整个system代理给后端 把所有路径都代理
旧图404 数据库URL、data/uploads路径、cwd、读权限、OSS来源 把所有图平铺到dist/static
视频/MEGO请求localhost integration.config.json、runtime-config.js、localStorage、真实服务 重新build后不看网络请求
MQTT没反应 全局与设备级连接配置、认证/Topic、任务开关、设备归属 用另一个系统broker密码试
第二套看到第一套数据 deploy2参数、私有DM用户、PM2环境、真实请求端口 重导整个数据库掩盖问题
重启后系统不起来 pm2-foodsafe、HOME/PM2_HOME、dump、Node路径、达梦/Nginx启动 同时起root和foodsafe两套守护

日志查看示例(新机root;只读):

tail -n 100 /data/apps/foodsafe1/logs/error*.log
tail -n 100 /data/apps/foodsafe2/logs/error*.log
journalctl -u pm2-foodsafe -n 80 --no-pager
journalctl -u mosquitto -n 80 --no-pager

日志可能含客户数据和凭据,不直接复制全量到公开文档;提供给研发前先脱敏。

31|现场记录与参考资料

每次记录:执行人、时间、源码提交、工具版本、包SHA、两套DM用户标签、SQL文件与执行结果、备份路径、上线与回退时间、八个入口验收和设备测试结果。口令不写入记录表。

本手册依据 standard_wxb@bc3267ef4c97fcfc38c1add26a2a9e1d94263275 的package.json、锁文件、五个build脚本,以及原制品发布适配、安装/检查/配置/启用脚本整理。后续业务代码变更不由手册自动保证正确,需研发测试和现场验收闭环。

官方参考: