diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml index c29a842..54f7a69 100644 --- a/.woodpecker/build.yml +++ b/.woodpecker/build.yml @@ -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 diff --git a/frondend/src/views/profile/ProfileView.vue b/frondend/src/views/profile/ProfileView.vue index ba63a94..6a883ac 100644 --- a/frondend/src/views/profile/ProfileView.vue +++ b/frondend/src/views/profile/ProfileView.vue @@ -102,6 +102,9 @@ function maskDeviceCode(code: string): string { return code.slice(0, 4) + '****' + code.slice(-4) } +// 是否已绑定设备码(决定「立即绑定 / 重新绑定」按钮文案) +const hasDeviceCode = computed(() => !!userInfo.value?.mobileCode) + onMounted(async () => { try { const res = await authApi.getUserInfo() @@ -458,7 +461,7 @@ async function handleRedeemInvite() { @click="goRebind" > - 重新绑定 + {{ hasDeviceCode ? '重新绑定' : '立即绑定' }}