v1.2.58:改为真·蓝绿部署(消除部署期 502)+ 设备码按钮文案随绑定状态变化
ci/woodpecker/tag/build Pipeline was successful

① 部署方式重构为真正的蓝绿(修复 v1.2.55~57 每次部署约 3 分钟的 502 窗口):
   根因:旧脚本是「翻到备用端口 → 1 秒后翻回 → 在活跃端口重启容器」,最终稳态只剩一个容器;且两次 nginx -s reload 仅隔 1 秒,openresty 未设 worker_shutdown_timeout(仅 keepalive_timeout 60),旧 worker 会继续持有连接并按旧配置往已删除的备用端口转发 → 502 持续到旧 worker 排空(实测 17:28:27~17:31:21,8885 Connection refused 86 次,8884 为 0)。
   新方案:两个固定槽位(blue=8884 / green=8885)常驻。每次部署只更新「当前无流量的备用槽」→ 容器内健康检查 → nginx -t 校验 → 翻一次反代指向它 → 等旧 worker 全部退出;活跃槽容器保留作为上一版本供秒级回滚。因两端口始终都有活容器,旧 worker 无论是否排空都不可能出现 502。
   另含:旧命名容器一次性收养(cpdaily-xiaozhushou → cpdaily-xiaozhushou-<端口>,避免下次部署端口冲突)、清理 cpdaily-blue 陈尸容器、反代配置切换前备份与 nginx -t 失败自动回滚、健康检查失败时不动反代。脚本已用 mock docker 干跑验证:两次连续部署正确轮换(8885→8884)、健康检查失败分支正确中止且不改反代。
② 个人中心设备码按钮:未绑定设备码时显示「立即绑定」,已绑定显示「重新绑定」(新增 hasDeviceCode computed)。
③ 验证:vue-tsc --noEmit、npm run build(含 postbuild-css)全过。前端改动 + CI 配置,无后端逻辑变更。
This commit is contained in:
becomer
2026-09-17 18:07:21 +08:00
parent 200edd75bb
commit 8435095b4f
2 changed files with 116 additions and 64 deletions
+112 -63
View File
@@ -29,37 +29,67 @@ steps:
# 应用配置由宿主机 /opt/cpdaily-xiaozhushou/config.toml 运维维护
# (已移出版本库,避免密钥入库;容器以只读挂载方式读取)
# 两个固定槽位(蓝/绿),任一时刻都保持"两个容器都活着":
# 活跃槽 = 反代当前指向的端口;备用槽 = 另一个端口(上一版,供秒级回滚)
SLOT_BLUE=8884
SLOT_GREEN=8885
# ============================================================
# 第一步:读取当前反代端口,确定蓝绿状态
# 第一步:读取当前反代指向的端口,确定活跃/备用槽
# 反代入口为 1Panel 站点 jinrixiaoyuan.top(frps 公网链路):
# /www/sites/jinrixiaoyuan.top/proxy/root.conf → proxy_pass http://127.0.0.1:8884|8885
# 该文件经 conf.d/jinrixiaoyuan.top.conf 的 include /www/sites/.../proxy/*.conf 载入。
# ============================================================
CURRENT_PORT=$(docker exec 1Panel-openresty-ESmn \
grep -oE 'proxy_pass http://[0-9.]+:[0-9]+' \
/www/sites/jinrixiaoyuan.top/proxy/root.conf | grep -o '[0-9]\+$')
echo "=== 当前反代端口: $CURRENT_PORT ==="
if [ "$CURRENT_PORT" = "8884" ]; then
PORT_ACTIVE=8884
PORT_STANDBY=8885
elif [ "$CURRENT_PORT" = "8885" ]; then
PORT_ACTIVE=8885
PORT_STANDBY=8884
if [ "$CURRENT_PORT" = "$SLOT_BLUE" ]; then
PORT_ACTIVE=$SLOT_BLUE
PORT_STANDBY=$SLOT_GREEN
elif [ "$CURRENT_PORT" = "$SLOT_GREEN" ]; then
PORT_ACTIVE=$SLOT_GREEN
PORT_STANDBY=$SLOT_BLUE
else
echo "ERROR: 未知反代端口 $CURRENT_PORT,中止部署(期望 8884/8885)"
echo "ERROR: 未知反代端口 $CURRENT_PORT,中止部署(期望 $SLOT_BLUE/$SLOT_GREEN)"
exit 1
fi
echo " 活跃: $PORT_ACTIVE | 备用: $PORT_STANDBY"
# 清理之前可能残留的临时容器
docker rm -f cpdaily-xiaozhushou-new 2>/dev/null || true
echo " 活跃槽: $PORT_ACTIVE | 备用槽: $PORT_STANDBY"
echo " 目标: 更新备用槽 :$PORT_STANDBY 到 $IMAGE_TAG 后,切换反代指向它(活跃槽保留,可回滚)"
# ============================================================
# 第二步:在备用端口启动新容器
# 第一步附:一次性「收养」旧命名容器
# 迁移前的容器名为 cpdaily-xiaozhushou(无端口后缀),本脚本改用
# cpdaily-xiaozhushou-<端口>。若不改名,下次部署 rm -f 找不到它 → 端口
# 被占用导致 docker run 冲突、部署失败。此处按它实际映射的端口改名(幂等)。
# 同时清理历史遗留的 cpdaily-blue 陈尸容器。
# ============================================================
echo "=== 蓝绿部署: 启动新容器 on :$PORT_STANDBY (image: $IMAGE_TAG) ==="
LEGACY_NAME=cpdaily-xiaozhushou
if docker inspect "$LEGACY_NAME" >/dev/null 2>&1; then
LEGACY_PORT=$(docker port "$LEGACY_NAME" 8093 2>/dev/null | head -1 | grep -oE '[0-9]+$' || true)
if [ -n "$LEGACY_PORT" ]; then
if docker inspect "cpdaily-xiaozhushou-$LEGACY_PORT" >/dev/null 2>&1; then
echo " 发现旧命名容器,但 cpdaily-xiaozhushou-$LEGACY_PORT 已存在,移除旧容器"
docker rm -f "$LEGACY_NAME" || true
else
echo " 收养旧命名容器: $LEGACY_NAME → cpdaily-xiaozhushou-$LEGACY_PORT"
docker rename "$LEGACY_NAME" "cpdaily-xiaozhushou-$LEGACY_PORT"
fi
else
echo " 旧命名容器未映射 8093 端口,跳过收养"
fi
fi
docker rm -f cpdaily-blue 2>/dev/null || true
docker run -d --name cpdaily-xiaozhushou-new \
# ============================================================
# 第二步:更新「备用槽」容器(此槽当前无流量,停它不会影响线上)
# ============================================================
echo "=== 停止并移除备用槽旧容器(:$PORT_STANDBY)==="
docker rm -f cpdaily-xiaozhushou-$PORT_STANDBY 2>/dev/null || true
echo "=== 在备用槽启动新容器 :$PORT_STANDBY (image: $IMAGE_TAG) ==="
docker run -d --name cpdaily-xiaozhushou-$PORT_STANDBY \
--restart unless-stopped \
--network likedream-net \
-e TZ=Asia/Shanghai \
@@ -68,73 +98,92 @@ steps:
cpdaily-xiaozhushou:$IMAGE_TAG
# ============================================================
# 第三步:健康检查
# 第三步:健康检查(新容器必须就绪,否则直接失败且不动反代)
# 用 docker exec 在容器内自测,避免依赖宿主机到容器的网络路径。
# ============================================================
HEALTHY=false
for i in $(seq 1 15); do
if wget -q --timeout=3 -O /dev/null http://10.10.10.128:$PORT_STANDBY/health 2>/dev/null; then
for i in $(seq 1 30); do
if docker exec cpdaily-xiaozhushou-$PORT_STANDBY \
wget -q --timeout=3 -O /dev/null http://127.0.0.1:8093/health 2>/dev/null; then
HEALTHY=true
break
fi
echo " 等待新容器就绪... ($i/15)"
echo " 等待新容器就绪... ($i/30)"
sleep 2
done
if [ "$HEALTHY" = false ]; then
echo "ERROR: 新容器未通过健康检查,回滚"
docker stop cpdaily-xiaozhushou-new
docker rm cpdaily-xiaozhushou-new
echo "ERROR: 新容器未通过健康检查,中止部署(线上仍由活跃槽 :$PORT_ACTIVE 提供服务,未受影响)"
docker rm -f cpdaily-xiaozhushou-$PORT_STANDBY 2>/dev/null || true
exit 1
fi
echo " 新容器已就绪 ✓"
# ============================================================
# 第四步:切换 1Panel 反代到备用端口
# 第四步:切换 1Panel 反代到备用槽(唯一一次代理切换)
# 切换前先备份配置,nginx -t 通过后再 reload;reload 后等待旧 worker 退出,
# 确保旧连接不再按旧配置转发。
# ============================================================
echo "=== 切换 1Panel 反代 :$PORT_ACTIVE → :$PORT_STANDBY ==="
PROXY_CONF=/www/sites/jinrixiaoyuan.top/proxy/root.conf
docker exec 1Panel-openresty-ESmn \
sed -i "s|:$PORT_ACTIVE|:$PORT_STANDBY|g" \
/www/sites/jinrixiaoyuan.top/proxy/root.conf
docker exec 1Panel-openresty-ESmn nginx -s reload
echo " Nginx 热加载完成(零中断)✓"
# ============================================================
# 第五步:停止旧容器
# ============================================================
echo "=== 清理旧容器 ==="
docker stop cpdaily-xiaozhushou 2>/dev/null || true
docker rm cpdaily-xiaozhushou 2>/dev/null || true
# ============================================================
# 第六步:在活跃端口启动正式容器
# ============================================================
echo "=== 启动正式容器 on :$PORT_ACTIVE ==="
docker run -d --name cpdaily-xiaozhushou \
--restart unless-stopped \
--network likedream-net \
-e TZ=Asia/Shanghai \
-p $PORT_ACTIVE:8093 \
-v /opt/cpdaily-xiaozhushou/config.toml:/app/config.toml:ro \
cpdaily-xiaozhushou:$IMAGE_TAG
# ============================================================
# 第七步:切回 1Panel 反代到活跃端口
# ============================================================
echo "=== 切回 1Panel 反代 :$PORT_STANDBY → :$PORT_ACTIVE ==="
cp $PROXY_CONF $PROXY_CONF.bak
docker exec 1Panel-openresty-ESmn \
sed -i "s|:$PORT_STANDBY|:$PORT_ACTIVE|g" \
/www/sites/jinrixiaoyuan.top/proxy/root.conf
docker exec 1Panel-openresty-ESmn nginx -s reload
echo " Nginx 热加载完成(零中断)✓"
sed -i "s|:$PORT_ACTIVE|:$PORT_STANDBY|g" $PROXY_CONF
# ============================================================
# 第八步:清理
# ============================================================
echo "=== 清理 ==="
docker rm -f cpdaily-xiaozhushou-new 2>/dev/null || true
docker images cpdaily-xiaozhushou --format '{{.Tag}}' | grep -v latest | head -n -3 | while read tag; do
docker rmi cpdaily-xiaozhushou:$tag 2>/dev/null || true
if ! docker exec 1Panel-openresty-ESmn nginx -t 2>&1; then
echo "ERROR: nginx 配置校验失败,回滚反代配置"
docker exec 1Panel-openresty-ESmn cp $PROXY_CONF.bak $PROXY_CONF
docker rm -f cpdaily-xiaozhushou-$PORT_STANDBY 2>/dev/null || true
exit 1
fi
# 记录 reload 前的 worker pid,reload 后等其全部退出,避免旧 worker 按旧配置继续转发
OLD_WORKERS=$(docker exec 1Panel-openresty-ESmn \
sh -c 'ps -eo pid,cmd | grep "[n]ginx: worker" | awk "{print \$1}"' | tr '\n' ' ')
docker exec 1Panel-openresty-ESmn nginx -s reload
echo " Nginx 热加载完成,等待旧 worker 退出..."
for i in $(seq 1 15); do
STILL=0
for p in $OLD_WORKERS; do
if docker exec 1Panel-openresty-ESmn sh -c "[ -d /proc/$p ]" 2>/dev/null; then
STILL=$((STILL + 1))
fi
done
if [ "$STILL" = "0" ]; then
echo " 旧 worker 已全部退出 ✓"
break
fi
echo " 等待旧 worker 释放连接... ($i/15, 剩余 $STILL)"
sleep 2
done
echo "=== 部署完成 ✓ ==="
echo " 反代已指向 :$PORT_STANDBY ✓"
# ============================================================
# 第五步:验证线上确实由新槽服务
# ============================================================
if ! docker exec 1Panel-openresty-ESmn \
sh -c "wget -q --timeout=5 -O /dev/null http://127.0.0.1:$PORT_STANDBY/health" 2>/dev/null; then
echo "WARN: 新槽健康检查异常,但反代已切换,请人工核查"
else
echo " 线上验证通过(:$PORT_STANDBY 正常服务)✓"
fi
# ============================================================
# 第六步:保留活跃槽用于回滚说明;清理多余历史镜像
# 注:旧槽容器不删除 —— 它即上一版本,仍需回滚时可直接把反代切回。
# 下次部署时旧槽会被当作「备用槽」重建,因此不会无限堆积。
# ============================================================
echo "=== 当前容器 ==="
docker ps --filter "name=cpdaily-xiaozhushou-" --format '{{.Names}} → {{.Ports}} ({{.Status}})'
echo "=== 清理历史镜像(保留 latest 与最近 2 个版本)==="
docker images cpdaily-xiaozhushou --format '{{.Tag}}' \
| grep -v '^latest$' | head -n -2 | while read tag; do
docker rmi cpdaily-xiaozhushou:$tag 2>/dev/null || true
done
echo "=== 部署完成 ✓ 活跃槽 :$PORT_STANDBY(旧槽 :$PORT_ACTIVE 保留,可回滚)==="
volumes:
- /var/run/docker.sock:/var/run/docker.sock