Files
wecom_it_smart_desk/HOTFIX-ROLLBACK-PLAN.md
T
Simon 78f60c6857 feat(v0.7.1): P0 修复 + 企微 SSO + RBAC 细粒度 + audit_log
P0 修复:
- /api/ready import 错误 (_get_engine + settings.create_redis_client)
- 删 agent.otp_secret/otp_enabled 双字段 (migration 026)
- 重建 021_rbac migration (IF NOT EXISTS 兼容)

P1 新增:
- 企微 SSO (auth_wecom_sso.py, useWeChatWorkSSO composable, PortalSelect UA 检测)
- RBAC 5 角色 × 4 资源 × 4 操作 × 3 范围 (rbac_service + seed_rbac + require_permission)
- audit_log 模型 + migration 027 + 服务 + API
- 管理后台 RBAC 权限矩阵 UI (PermissionsMatrix.vue)

质量:
- pytest 405 passed / 33 pre-existing failed / 4 xfailed (v0.7.1 引入失败 = 0)
- conftest GBK patch 强制 UTF-8 读 .env
- .gitignore 排除 *.b64 (含 admin token 凭据)
- DEPLOY-v0.7.1.md 7 步 runbook + 4 坑 + 回滚预案
2026-06-22 17:38:47 +08:00

22 KiB

v0.7.0 Hotfix #63 回滚方案

场景: 生产 backend 容器 wecom_it_backend 已回滚到 v0.7.0-backup-pre-qrfix 镜像(因 v0.7.0.1-hotfix1 失败)。现在通过 jumpserver 终端用 base64 分段 echo 上传 auth_qrcode.py + qrcode_service.py/tmp/,然后 docker cp 到容器,pip install qrcode[pil],restart。本文件给出失败时的回滚方案

目标读者: 运维小白(用户)。每步带中文注释,失败兜底齐全。

生效条件: 当且仅当 curl /api/auth_qrcode/create 行为异常时触发。

回滚总目标: 1 分钟内把 backend 拉回到 v0.7.0-backup-pre-qrfix 镜像,业务不中断。


0. 当前状态快照(回滚前必看)

回滚前先确认现在到底在跑哪个镜像、哪 2 个文件、pip 装了什么。3 条命令 30 秒:

# 1) 看当前容器用的镜像 ID
docker inspect wecom_it_backend --format '{{.Image}}' | head -c 12
# 期望: 现在(回滚后)应该是 v0.7.0-backup-pre-qrfix 镜像 ID
# 如果 hotfix 装好,可能是 wecom-it-desk-backend:patched 或 latest

# 2) 看容器内 2 个文件的修改时间(确认 hotfix 是否真生效)
docker exec wecom_it_backend stat -c '%Y %n' \
  /app/app/api/auth_qrcode.py \
  /app/app/services/qrcode_service.py
# 期望 hotfix 装好后: 数字是最近的(今天/刚刚);否则是 6/15 左右的旧时间

# 3) 看 qrcode 是否真装上
docker exec wecom_it_backend pip show qrcode 2>&1 | head -5
# 期望装好: Name: qrcode  Version: 7.4.2
# 没装: WARNING: Package(s) not found: qrcode

把这 3 个输出截图给 Claude,后续诊断直接定位问题。


1. 失败可能性清单(7 种 + 回滚命令)

# 失败模式 现象 检测命令 回滚命令
F1 qrcode pip 安装失败 restart 后容器立刻 exit docker ps -a | grep wecom_it_backend 看到 RestartingExited 见 §1.1
F2 容器启动失败(模块导入报错) backend 启动循环重启 docker logs wecom_it_backend --tail 30 看到 ModuleNotFoundError / ImportError / SyntaxError 见 §1.2
F3 curl /api/auth_qrcode/create 返回 500 容器 healthy 但端点挂 curl -k -X POST https://itsupport.servyou.com.cn/api/auth_qrcode/create 见 §1.3
F4 curl 返回 200 但 qrcode_png_base64 字段 代码覆盖不彻底(还是旧文件) curl ... | python -m json.tool | grep qrcode_png_base64 见 §1.4
F5 curl 返回 502/504 nginx 找不到 backend 容器 docker ps | grep backend 见 §1.5
F6 端口冲突(8000 被占) 容器一直 restarting docker logs wecom_it_backend --tail 50 | grep -i "address already" 见 §1.6
F7 镜像 ID 错乱/标签漂移 restart 后跑的镜像不是预期的 docker images | grep wecom-it-desk-backend 见 §1.7

1.1 F1: qrcode pip 安装失败回滚

原因: pip install qrcode[pil] 网络抽风 / 镜像精简版没 gcc / 版本冲突。

回滚命令(jumpserver 终端执行,root 用户):

# 1) 停容器
docker stop wecom_it_backend

# 2) 删容器(保留数据卷 / 网络)
docker rm wecom_it_backend

# 3) 用回滚镜像起新容器(关键: 命令行要跟当前生产容器完全一致)
#    抄一下当前容器的完整 run 命令,免得环境变量 / 挂载丢了
docker run -d \
  --name wecom_it_backend \
  --restart=always \
  --network wecom_it_network \
  -e DATABASE_URL='...' \
  -e REDIS_URL='...' \
  -e WECOM_CORP_ID='...' \
  -v /opt/wecom-it-desk/backend:/app:rw \
  wecom-it-desk-backend:v0.7.0-backup-pre-qrfix

# 4) 验证
docker ps | grep wecom_it_backend
# 期望: STATUS = Up X seconds (healthy)

简化方案(如果你之前记录了完整 run 命令):

# 直接用 docker commit 出来的镜像
docker run -d --name wecom_it_backend <完整原参数> \
  wecom-it-desk-backend:v0.7.0-backup-pre-qrfix

1.2 F2: 模块导入报错回滚

原因: auth_qrcode.pyqrcode_service.py 上传时 base64 解码坏掉 / Python 缩进错。

检测:

docker logs wecom_it_backend --tail 30 2>&1 | grep -E "(ModuleNotFoundError|ImportError|SyntaxError|IndentationError)"

回滚命令(比 F1 简单,只用覆盖文件 + 重启,不用换镜像):

# 1) 从 backup 镜像里把原版文件拷出来
docker create --name tmp_rollback wecom-it-desk-backend:v0.7.0-backup-pre-qrfix
docker cp tmp_rollback:/app/app/api/auth_qrcode.py /tmp/auth_qrcode.py.bak
docker cp tmp_rollback:/app/app/services/qrcode_service.py /tmp/qrcode_service.py.bak
docker rm tmp_rollback

# 2) 覆盖回滚(注意: bind mount 模式下必须改宿主机路径)
docker cp /tmp/auth_qrcode.py.bak wecom_it_backend:/app/app/api/auth_qrcode.py
docker cp /tmp/qrcode_service.py.bak wecom_it_backend:/app/app/services/qrcode_service.py

# 3) 重启
docker restart wecom_it_backend

# 4) 验证
sleep 5
docker ps | grep wecom_it_backend
curl -k -X POST https://itsupport.servyou.com.cn/api/auth_qrcode/create | python -m json.tool

1.3 F3: create 端点 500 回滚

原因: qrcode_service.py 内的 _render_qrcode_png 抛异常(qrcode 没装好 / PIL 缺包)。

检测:

# 拿返回内容
curl -k -X POST https://itsupport.servyou.com.cn/api/auth_qrcode/create -v 2>&1 | tail -20

# 看后端日志,找 traceback
docker logs wecom_it_backend --tail 50 2>&1 | grep -A 20 "Traceback"

回滚命令: 同 §1.2(覆盖文件 + restart)。如果还 500,升级到 §1.1(换镜像)。

1.4 F4: 没 qrcode_png_base64 字段回滚

原因: docker cp 后容器内文件没真覆盖(典型 bind mount / overlay fs 坑)。

检测:

curl -k -X POST https://itsupport.servyou.com.cn/api/auth_qrcode/create | python -m json.tool
# 看 data 字段里有没有 "qrcode_png_base64"
# 没有 → 文件没真覆盖

回滚命令(强制覆盖):

# 1) 确认宿主机上 bind mount 的文件位置
docker inspect wecom_it_backend --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}' | grep app
# 输出: /opt/wecom-it-desk/backend -> /app

# 2) 直接改宿主机路径(这是 bind mount 唯一能稳定生效的方式)
ls -la /opt/wecom-it-desk/backend/app/api/auth_qrcode.py /opt/wecom-it-desk/backend/app/services/qrcode_service.py

# 3) 如果是新文件没生效,先 rm 再 cp
rm -f /opt/wecom-it-desk/backend/app/api/auth_qrcode.py
rm -f /opt/wecom-it-desk/backend/app/services/qrcode_service.py
cp /tmp/auth_qrcode.py /opt/wecom-it-desk/backend/app/api/
cp /tmp/qrcode_service.py /opt/wecom-it-desk/backend/app/services/

# 4) 必须 restart 容器(overlay 不会自动 sync bind mount)
docker restart wecom_it_backend

# 5) 验证
sleep 5
curl -k -X POST https://itsupport.servyou.com.cn/api/auth_qrcode/create | python -m json.tool | grep qrcode_png_base64

1.5 F5: 502/504 回滚

原因: nginx 解析到旧 backend 容器,或容器网络断了。

检测:

# 1) 看 backend 容器在不在
docker ps | grep wecom_it_backend

# 2) nginx 容器内直接测 backend
docker exec wecom_it_nginx wget -qO- --timeout=3 http://wecom_it_backend:8000/api/ready
# 期望: {"status":"ready",...}
# 502 → 网络通但 backend 内部挂
# timeout → 网络都不通

回滚命令(全链路重拉):

# 1) 停 backend
docker stop wecom_it_backend

# 2) 删容器
docker rm wecom_it_backend

# 3) 用回滚镜像起(完整参数)
docker run -d --name wecom_it_backend \
  --restart=always --network wecom_it_network \
  <完整原参数> \
  wecom-it-desk-backend:v0.7.0-backup-pre-qrfix

# 4) 重新加载 nginx(让 upstream 刷新)
docker exec wecom_it_nginx nginx -s reload

# 5) 验证
sleep 10
curl -k https://itsupport.servyou.com.cn/api/ready

1.6 F6: 端口冲突回滚

原因: 旧容器没删干净 / 8000 被别的进程占。

检测:

docker logs wecom_it_backend --tail 50 2>&1 | grep -i "address already in use"
# 或
ss -tlnp | grep 8000

回滚命令:

# 1) 看谁占 8000
ss -tlnp | grep ':8000'

# 2) 通常是僵尸容器,删它
docker ps -a | grep ":8000"  # 不一定能直接看到
docker rm -f wecom_it_backend  # 强制删当前容器

# 3) 再起
docker run -d --name wecom_it_backend <完整原参数> wecom-it-desk-backend:v0.7.0-backup-pre-qrfix

1.7 F7: 镜像 ID 错乱回滚

原因: docker run 时没指定 tag,默认拉 latest,可能不是预期的。

检测:

docker images --format '{{.Repository}}:{{.Tag}} {{.ID}} {{.CreatedSince}}' | grep wecom-it-desk-backend
# 应该看到 3 个:
#   wecom-it-desk-backend:v0.7.0-backup-pre-qrfix  (回滚用的)
#   wecom-it-desk-backend:latest                   (可能等于上面那个,也可能等于 patched)
#   wecom-it-desk-backend:patched                  (hotfix 试装版,如果有)

回滚命令(显式指定 tag):

# 拿到回滚镜像的精确 ID
ROLLBACK_IMAGE=$(docker images -q wecom-it-desk-backend:v0.7.0-backup-pre-qrfix)
echo "回滚镜像 ID: $ROLLBACK_IMAGE"

# 删旧容器
docker stop wecom_it_backend && docker rm wecom_it_backend

# 用**精确 ID** 起(避免 tag 被覆盖)
docker run -d --name wecom_it_backend <完整原参数> $ROLLBACK_IMAGE

2. 健康检查命令速查

2.1 容器层

# 状态(看是不是 healthy)
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}' | grep wecom_it_backend
# 期望: wecom_it_backend  Up X minutes (healthy)  wecom-it-desk-backend:v0.7.0-backup-pre-qrfix

# 看 healthcheck 详细日志
docker inspect wecom_it_backend --format '{{json .State.Health}}' | python -m json.tool

2.2 进程层

# Python 进程在不在
docker exec wecom_it_backend ps aux | grep -E "uvicorn|gunicorn" | grep -v grep
# 期望: 1 行 uvicorn 进程

# 端口监听
docker exec wecom_it_backend ss -tlnp | grep 8000
# 期望: LISTEN 0  128  0.0.0.0:8000  ...

2.3 端点层

# readiness 端点(由 /api/ready 提供)
curl -k https://itsupport.servyou.com.cn/api/ready
# 期望: {"code":200,"data":{"status":"ready","checks":{...}}}

# health 端点
curl -k https://itsupport.servyou.com.cn/api/health
# 期望: {"status":"ok"}

2.4 业务层(create 端点)

# 标准 create 调用
curl -k -X POST https://itsupport.servyou.com.cn/api/auth_qrcode/create \
  -H 'Content-Type: application/json' | python -m json.tool

期望返回(200 + data 里 qrcode_png_base64):

{
    "code": 200,
    "message": "ok",
    "data": {
        "ticket": "AbCdEf123456...",
        "qrcode_url": "https://open.weixin.qq.com/connect/oauth2/authorize?...",
        "qrcode_png_base64": "iVBORw0KGgoAAAANSUhEUgAA...(超长 base64 字符串)...",
        "expires_in": 120,
        "expires_at": "2026-06-22T10:30:45.123456"
    }
}

关键判定:

  • HTTP 200 + qrcode_png_base64 长度 > 100 字符 = hotfix 生效
  • HTTP 200 + 字段缺失 = §1.4 文件没覆盖
  • HTTP 500 = §1.3
  • HTTP 502/504 = §1.5

3. 验证 hotfix 真正生效(5 步)

# Step 1: 文件 md5 对比(确认是 hotfix 版)
docker exec wecom_it_backend md5sum /app/app/api/auth_qrcode.py /app/app/services/qrcode_service.py
# 跟宿主机 /tmp/ 里那 2 个文件的 md5 对比,必须一致
md5sum /tmp/auth_qrcode.py /tmp/qrcode_service.py

# Step 2: 关键代码片段存在性
docker exec wecom_it_backend grep -n "_render_qrcode_png\|qrcode_png_base64" \
  /app/app/api/auth_qrcode.py /app/app/services/qrcode_service.py
# 期望: 至少 3 行匹配(import / def / return)

# Step 3: qrcode 装上了
docker exec wecom_it_backend python -c "import qrcode; print(qrcode.__version__)"
# 期望: 7.4.2

# Step 4: create 端点返回 qrcode_png_base64
RESP=$(curl -k -s -X POST https://itsupport.servyou.com.cn/api/auth_qrcode/create)
echo "$RESP" | python -c "import json,sys; d=json.load(sys.stdin); print('has_field:', 'qrcode_png_base64' in d.get('data',{})); print('len:', len(d.get('data',{}).get('qrcode_png_base64','')))"
# 期望: has_field: True   len: 500~2000

# Step 5: 浏览器实测(用户手工)
# 打开 https://itsupport.servyou.com.cn/itportal/
# 应该看到二维码图片(不是空白)

5 步全过 = hotfix 真生效。任何一步失败,跳到 §1 对应章节回滚。


4. 决策树:何时回滚 vs 何时修复

                        ┌──────────────────────────┐
                        │  hotfix 装好,开始验证     │
                        │  (curl /api/auth_qrcode/  │
                        │   create)                │
                        └────────────┬─────────────┘
                                     │
                                     ▼
                        ┌──────────────────────────┐
                        │ HTTP 200 + 有 base64 字段? │
                        └────┬──────────────┬──────┘
                             │              │
                           Yes             No
                             │              │
                             ▼              ▼
                  ┌─────────────────┐   ┌──────────────────┐
                  │ ✅ hotfix 生效  │   │ 看 HTTP 状态码   │
                  │ 跑 §3 后 5 步   │   └────┬───────┬─────┘
                  │ 浏览器实测       │        │       │
                  └─────────────────┘     500    502/504
                                              │       │
                                              ▼       ▼
                                  ┌──────────┐  ┌──────────────┐
                                  │ 看 trace │  │ 看容器在不在  │
                                  │ 见 §1.3  │  │ 见 §1.5      │
                                  └────┬─────┘  └──────┬───────┘
                                       │               │
                              ┌────────┴────┐         │
                              ▼             ▼         │
                       修不好(< 5 分钟)  修得好        │
                              │             │         │
                              ▼             ▼         │
                        §1.2 覆盖文件   继续验证        │
                        完整走完 §3      ┌────┴────────┘
                                            │
                                            ▼
                                  ┌──────────────────┐
                                  │ 走完 §3 五步验证 │
                                  └────┬─────────┬───┘
                                       │         │
                                  全过(5/5)   有失败
                                       │         │
                                       ▼         ▼
                                  浏览器实测  §1.4 文件覆盖
                                  /itportal/  (bind mount)
                                  看到二维码
                                       │
                                  ┌────┴────┐
                                  ▼         ▼
                              看到二维码   还是空白
                                  │         │
                                  ▼         ▼
                              ✅ 成功    截图给 Claude
                                            走 §1.1 换镜像

何时回滚的硬性触发条件(任一即回滚):

  1. 容器健康检查连续 3 次失败(每 30s 一次,> 90s 不 healthy)
  2. 其他业务端点挂掉(扫一下 /api/ready / /api/health / 别的 create 端点)
  3. 修复尝试超过 5 分钟无进展
  4. 用户报告前端页面打不开 / 报 500

何时继续修复的判断:

  • 容器 healthy + 仅 create 端点 500 → 尝试 §1.2 覆盖文件,5 分钟内没好就走 §1.1
  • 容器 healthy + create 端点正常 + 没 base64 字段 → §1.4 强制覆盖(这是文件问题,不是代码问题)
  • 容器 not healthy + 启动报错 → 直接 §1.1 换镜像(别浪费时间)

5. 回滚后清理步骤(2 步)

回滚成功 + 业务恢复后,把现场收拾干净。

5.1 恢复 image tag

# 1) 看现在有哪些镜像
docker images | grep wecom-it-desk-backend
# 期望看到:
#   REPOSITORY                    TAG                        IMAGE ID       CREATED
#   wecom-it-desk-backend         v0.7.0-backup-pre-qrfix    abc123...      3 days ago
#   wecom-it-desk-backend         patched                   def456...      10 minutes ago  (hotfix 试装版)
#   wecom-it-desk-backend         latest                    abc123...      3 days ago       (跟 backup 同 ID)

# 2) 把 latest 重新指向回滚镜像
docker tag wecom-it-desk-backend:v0.7.0-backup-pre-qrfix wecom-it_desk-backend:latest
# 防止下次 pull latest 时拉到错版本

# 3) 给 hotfix 试装镜像打孤 tag(留底,后面排查用)
docker tag wecom-it-desk-backend:patched wecom-it-desk-backend:hotfix-63-failed
# 避免被下次构建覆盖

5.2 清理多余镜像(谨慎)

# 1) 先看磁盘占用
docker system df

# 2) 看哪些镜像没人用
docker images --filter "dangling=true"  # 悬空镜像(<none>:<none>)
# 期望: 如果有 hotfix 中间层,会列出来

# 3) 删悬空镜像(安全)
docker image prune -f

# 4) 看 patched 镜像是否还有容器引用
docker ps -a --filter "ancestor=wecom-it-desk-backend:patched" --format '{{.ID}} {{.Names}} {{.Status}}'
# 期望: 0 行(回滚后应该没容器在用 patched)

# 5) 删 patched 镜像
docker rmi wecom-it-desk-backend:patched

# 6) 删 failed 留底(可选,建议先保留 7 天)
# docker rmi wecom-it-desk-backend:hotfix-63-failed

# 7) 再看一次
docker images | grep wecom-it-desk-backend
# 期望只剩 v0.7.0-backup-pre-qrfix + latest(同 ID)

5.3 清理宿主机临时文件

# 删 /tmp/ 里那 2 个 base64 上传用的文件
rm -f /tmp/auth_qrcode.py /tmp/qrcode_service.py
rm -f /tmp/auth_qrcode.py.bak /tmp/qrcode_service.py.bak  # 回滚时产生的
ls -la /tmp/ | grep -E "(qrcode|auth_qrcode)"
# 期望: 无输出

6. 一键回滚脚本(把 §1.1 打包)

如果手动操作太烦,把回滚流程封装成一个脚本(jumpserver 上直接跑):

文件: /opt/wecom-it-desk/rollback-hotfix63.sh

#!/bin/bash
# v0.7.0 hotfix #63 一键回滚
# 用法: bash /opt/wecom-it-desk/rollback-hotfix63.sh

set -e  # 任一命令失败立即退出

echo "===== hotfix #63 一键回滚 ====="

# 1) 停 + 删当前容器
docker stop wecom_it_backend
docker rm wecom_it_backend

# 2) 用 backup 镜像起
docker run -d \
  --name wecom_it_backend \
  --restart=always \
  --network wecom_it_network \
  $(cat /opt/wecom-it-desk/backend-run.env) \
  wecom-it-desk-backend:v0.7.0-backup-pre-qrfix

# 3) 等 5 秒让容器启动
sleep 5

# 4) 健康检查
echo "===== 验证 ====="
docker ps | grep wecom_it_backend
curl -kf https://itsupport.servyou.com.cn/api/ready && echo "READY OK" || echo "READY FAIL"

echo "===== 回滚完成 ====="

部署方式(在 jumpserver 终端):

# 1) 创建文件
cat > /opt/wecom-it-desk/rollback-hotfix63.sh << 'EOF'
# (上面那段内容)
EOF

# 2) 加执行权限
chmod +x /opt/wecom-it-desk/rollback-hotfix63.sh

# 3) 提取当前 backend 容器的 run 参数(给脚本里的 $(cat ...) 用)
docker inspect wecom_it_backend --format '{{range .Config.Env}}export {{.}}{{"\n"}}{{end}}' \
  > /opt/wecom-it-desk/backend-run.env 2>/dev/null || true

# 4) 跑回滚
bash /opt/wecom-it-desk/rollback-hotfix63.sh

注意: --env-file / -edocker run 里比脚本里 export 更稳。生产建议把完整 docker run 命令存到 /opt/wecom-it-desk/backend-run.sh,回滚脚本里直接 bash backend-run.sh。这个留给后续优化。


7. 回滚后通知清单

回滚完 = 业务恢复,但还要做 3 件事:

  1. 更新 CURRENT-FOCUS.md: 在「最近搞定」加一行 ❌ v0.7.0 hotfix #63 失败已回滚到 v0.7.0-backup-pre-qrfix,前端 /itportal/ 二维码仍不显示,等下一轮修复
  2. 记入 memory: 在 memory/hotfix-63-rollback-2026-06-22.md,写清楚: 失败在哪一步 / 用了哪个回滚命令 / 跟 Claude 复盘结论
  3. 贴 logs 给 Claude: 把 docker logs wecom_it_backend --tail 200 输出贴回来,分析根因,准备下一轮 hotfix 方案(v0.7.0.2-hotfix2)

8. 速查表(贴在屏幕边上)

我看到 跑这个
容器 restarting docker logs wecom_it_backend --tail 30 看启动错误 → §1.1
容器 healthy 但 create 500 §1.3 拿 traceback → §1.2 覆盖文件
容器 healthy + create 200 + 无 base64 §1.4 强制 bind mount 覆盖
502/504 §1.5 看网络 + 容器
8000 占用 §1.6
完全不知道啥情况 §1.1 一键换镜像(最稳)
不知道回滚到哪个镜像 docker images | grep backup
不知道完整 run 命令 docker inspect wecom_it_backend --format '{{.Config.Cmd}} {{json .Config.Env}}' | head -c 500
想一键回滚 bash /opt/wecom-it-desk/rollback-hotfix63.sh
验证 hotfix 生效 §3 五步全过 =
回滚后清理 §5 三步

文档结束。所有命令都在 jumpserver 终端以 root 跑,docker exec 都假设容器名叫 wecom_it_backend(生产实际名,见 memory/container-names-wecom-it-backend.md)。如果容器名变了,先跑 docker ps --format '{{.Names}}' \| grep backend 确认。