Files
wecom_it_smart_desk/docs/09-部署运维/卷挂载重构方案.md
T
Simon bea288e414 feat: 2026-07-11 全量更新 - 代办集成+会议室预定+知识迭代修复+UI统一+Bug修复
== 已部署上线 (9项) ==
- 代办事项真实数据源集成 (企微审批API 8bug修复链)
- H5/坐席端 Logo样式统一+绿色背景
- 视频引导页修复 (localStorage key v2)
- 坐席端 v9 Vue版本修复 (ElMessage._context)
- 截图按钮 v10 修复 (getDisplayMedia user gesture)
- 扫码样式恢复+H5扫码登录跳转修复
- H5截图快捷键提示

== 代码完成待部署 (3项) ==
- 知识迭代3Bug修复 (#8 POST端点/#7 MERGE幂等/#6 过期检查)
- 会议室预定-小鱼易联终端 (40文件, 40/40测试通过)
- IT资产升级审批推送 (asset_service.py)

== 需求文档 (2项) ==
- 坐席端AI辅助消息框-PRD (4项新功能确认)
- 坐席端布局优化建议 v2.0 (7天计划)

== 新增文档 ==
- 日报-2026-07-11.md
- 知识迭代Bug修复报告-20260711.md
- 会议室预定-部署指南.md
- CHANGELOG.md 更新

== 测试 ==
- test_todo_integration.py: 40/40
- test_meetingroom.py: 40/40
- test_bugfix_ki_suggestions.py: 21/21
2026-07-11 23:13:10 +08:00

1123 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# IT 智能服务台 — 部署架构重构方案(镜像烘焙 → 代码卷挂载)
> **文档编号**: ARCH-VOLMOUNT-001
> **编写人**: 架构师 高见远
> **日期**: 2025-07-10
> **目标服务器**: 10.90.5.110 (itsupport.servyou.com.cn)
> **方案编号**: 方案 C — 代码卷挂载替代镜像烘焙
---
## 1. 架构变更说明
### 1.1 当前架构问题(镜像烘焙模式)
当前后端代码通过 Dockerfile 的 `COPY . .` 指令在 **构建时** 烘焙进镜像。这一模式存在以下根本性缺陷:
| 问题 | 描述 | 后果 |
|------|------|------|
| **双代码源** | 服务器上存在两份代码:`/opt/wecom-it-desk/app/`(较新,部署用)+ `/opt/wecom-it-desk/backend/app/`(较旧,构建用) | 构建出的镜像可能包含过期/缺失代码 |
| **构建上下文陷阱** | `docker compose build` 的 context 为 `./backend``COPY . .` 复制的是 `backend/app/`(旧代码),而非 `app/`(新代码) | 07-07 和 07-10 两次认证故障的根因 |
| **重建即风险** | 每次代码更新都必须 `docker compose build --no-cache`,重建过程耗时 3-5 分钟,期间服务不可用 | 部署窗口长,故障概率高 |
| **代码与依赖耦合** | 代码和 Python 依赖包混在同一镜像层,更新代码也必须重建依赖层 | 浪费时间,增加出错面 |
| **回滚困难** | 回滚需要重新构建镜像,无法快速切换到上一版本 | 故障恢复时间长 |
**事故复盘**
- **07-07**Docker 镜像未重新构建 → 路由缺失(旧镜像运行新代码期望)
- **07-10**:镜像从 `backend/app/`(旧代码)构建 → 缺少 `auth.py` 等文件 → 认证全断
### 1.2 目标架构设计(卷挂载模式)
```
┌─────────────────────────────────────────────────────────┐
│ 宿主机 /opt/wecom-it-desk/ │
│ │
│ ┌──────────────┐ ┌──────────────────────────────┐ │
│ │ backend/ │ │ app/ │ │
│ │ ├ Dockerfile│ │ ├ main.py │ │
│ │ ├ req...txt │ │ ├ auth.py │ │
│ │ └ (无app/) │ │ ├ api/ │ │
│ │ (仅构建配置) │ │ ├ services/ │ │
│ │ │ │ └ ... │ │
│ └──────┬───────┘ └──────────┬────────────────────┘ │
│ │ │ volume mount │
│ │ build context │ ./app → /app/app │
│ │ (仅 requirements) │ │
└─────────┼──────────────────────┼─────────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────┐
│ Docker Container (wecom_it_backend) │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ /app/ │ │
│ │ ├ app/ ← 卷挂载(运行时读取宿主机代码) │ │
│ │ │ ├ main.py │ │
│ │ │ └ ... │ │
│ │ ├ uploads/ ← Docker named volume │ │
│ │ ├ logs/ ← 宿主机目录挂载 │ │
│ │ └ site-packages/ ← 镜像内置(构建时安装) │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ 镜像 = Python 运行时 + 依赖包(无业务代码) │
└─────────────────────────────────────────────────────────┘
```
**核心原理**
- **镜像** 只包含 Python 运行时 + `site-packages`(依赖包),不含业务代码
- **代码** 通过 Docker volume 挂载到容器 `/app/app/`,运行时从宿主机实时读取
- **代码更新** = 更新宿主机文件 + 重启容器(秒级,无需重建镜像)
- **依赖更新** = 更新 `requirements.txt` + 重建镜像(仅在依赖变化时)
### 1.3 核心变更点
| # | 变更项 | 变更前 | 变更后 |
|---|--------|--------|--------|
| 1 | Dockerfile `COPY . .` | 代码烘焙进镜像 | **删除**,代码通过 volume 提供 |
| 2 | Dockerfile `ENV` | 无 | 新增 `PYTHONDONTWRITEBYTECODE=1` |
| 3 | docker-compose.yml `volumes` | 仅 uploads + logs | 新增 `./app:/app/app` 代码挂载 |
| 4 | 服务器目录 `backend/app/` | 旧代码(构建用) | **删除**(不再需要) |
| 5 | 服务器目录 `app/` | 新代码(部署用) | **唯一代码源**volume 挂载源) |
| 6 | 部署流程 | 每次重建镜像 | 代码更新仅重启,依赖更新才重建 |
---
## 2. docker-compose.yml 变更
### 2.1 变更对比
**变更前**backend 服务 volumes 部分):
```yaml
volumes:
- backend-uploads:/app/uploads
- ${RUNTIME_LOG_HOST_DIR:-/var/log/wecom-it-desk}:/app/logs
```
**变更后**
```yaml
volumes:
- ./app:/app/app # [新增] 代码卷挂载
- backend-uploads:/app/uploads
- ${RUNTIME_LOG_HOST_DIR:-/var/log/wecom-it-desk}:/app/logs
```
### 2.2 卷挂载说明
| 卷挂载 | 类型 | 目的 | 变更 |
|--------|------|------|------|
| `./app:/app/app` | Bind mount | **代码目录**:容器运行时从宿主机 `/opt/wecom-it-desk/app/` 读取 Python 业务代码 | **新增** |
| `backend-uploads:/app/uploads` | Named volume | **上传文件**:持久化用户上传的文件 | 不变 |
| `${RUNTIME_LOG_HOST_DIR:-/var/log/wecom-it-desk}:/app/logs` | Bind mount | **运行日志**:持久化运行期日志,供日志管理页面读取 | 不变 |
### 2.3 路径映射关系
```
宿主机路径 → 容器路径
/opt/wecom-it-desk/app/ → /app/app/ (代码)
/opt/wecom-it-desk/backend/requirements.txt → (仅构建时使用,不挂载)
Docker volume: wecom_it_backend_uploads → /app/uploads/ (上传文件)
/var/log/wecom-it-desk/ → /app/logs/ (日志)
```
容器内 `/app/` 目录结构(运行时):
```
/app/
├── app/ ← 卷挂载(来自宿主机 ./app/)
│ ├── __init__.py
│ ├── main.py ← uvicorn app.main:app 入口
│ ├── config.py
│ ├── constants.py
│ ├── database.py
│ ├── api/
│ │ ├── auth.py ← 认证路由(曾缺失导致故障)
│ │ └── ...
│ ├── core/
│ ├── services/
│ ├── models/
│ └── ...
├── uploads/ ← Docker named volume
├── logs/ ← 宿主机目录挂载
└── (site-packages 在 /usr/local/lib/python3.12/site-packages/)
```
### 2.4 服务器目录结构调整
**变更前**(两份代码):
```
/opt/wecom-it-desk/
├── docker-compose.yml
├── app/ ← 较新代码(部署用,但不参与构建)
│ ├── main.py
│ └── ...
├── backend/
│ ├── Dockerfile
│ ├── requirements.txt
│ └── app/ ← 较旧代码(构建用,COPY . . 复制源) ← 问题根源
│ ├── main.py
│ └── ...
└── ...
```
**变更后**(单一代码源):
```
/opt/wecom-it-desk/
├── docker-compose.yml
├── app/ ← 唯一代码源(volume 挂载到容器)
│ ├── __init__.py
│ ├── main.py
│ ├── auth.py
│ ├── api/
│ ├── core/
│ ├── services/
│ └── ...
├── backend/
│ ├── Dockerfile ← 构建配置(已修改,无 COPY . .)
│ ├── requirements.txt ← 依赖声明(构建时使用)
│ └── (app/ 已删除) ← 不再需要
└── ...
```
**调整步骤**
1. 确保 `./app/` 包含最新代码(部署包已解压,或从 git 同步)
2. 验证 `./app/` 关键文件存在:`main.py`, `auth.py`, `__init__.py`
3. 部署验证通过后 48 小时,删除 `./backend/app/`(回滚安全网保留期)
> **注意**`./backend/app/` 在回滚期间保留。回滚时需恢复原始 Dockerfile(含 `COPY . .`),此时 `backend/app/` 作为构建上下文代码源。48 小时稳定运行后可安全删除。
---
## 3. Dockerfile 变更
### 3.1 变更后完整 Dockerfile
```dockerfile
# =============================================================================
# 企微IT智能服务台 — 后端 Docker 镜像构建文件
# =============================================================================
# 说明:基于 Python 3.12 构建后端镜像
# 变更:2025-07-10 方案C — 代码改为 volume 挂载,镜像不再 COPY 业务代码
# 用法:docker build -t wecom-it-desk-backend .
# =============================================================================
# --------------------------------------------------------------------------
# 第一阶段:构建阶段
# --------------------------------------------------------------------------
FROM python:3.12-slim AS builder
# 设置工作目录
WORKDIR /app
# 安装系统依赖(psycopg2 编译需要 + qrcode 图片处理需要 + healthcheck 需要 curl
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc libpq-dev libjpeg-dev zlib1g-dev curl && \
rm -rf /var/lib/apt/lists/*
# 复制依赖声明文件并安装(利用 Docker 层缓存,依赖不变则不重新安装)
# 使用阿里云 PyPI 镜像(比清华镜像更快)
COPY requirements.txt .
RUN pip install --no-cache-dir \
--timeout 180 \
--retries 5 \
-i https://mirrors.aliyun.com/pypi/simple/ \
--trusted-host mirrors.aliyun.com \
-r requirements.txt
# --------------------------------------------------------------------------
# 第二阶段:运行阶段(更小的镜像体积)
# --------------------------------------------------------------------------
FROM python:3.12-slim
# 设置标签信息
LABEL maintainer="IT服务台开发团队"
LABEL description="企微IT智能服务台后端服务"
LABEL changelog="2025-07-10: 移除 COPY . .,代码改为 volume 挂载"
# 安装运行时依赖(psycopg2 运行时需要 libpq + healthcheck 需要 curl
RUN apt-get update && \
apt-get install -y --no-install-recommends libpq5 curl && \
rm -rf /var/lib/apt/lists/*
# 设置工作目录
WORKDIR /app
# 禁止 Python 写入 __pycache__(防止污染宿主机代码目录)
ENV PYTHONDONTWRITEBYTECODE=1
# 从构建阶段复制已安装的 Python 包
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
# 业务代码通过 docker-compose volumes 挂载(./app:/app/app),不再 COPY 进镜像
# 暴露端口
EXPOSE 8000
# 启动命令(Docker Compose 中会覆盖)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
```
### 3.2 变更明细
| 行号 | 原始内容 | 变更后 | 操作 |
|------|----------|--------|------|
| 6 | `# 用法:...` | 增加 `# 变更:2025-07-10 方案C ...` | 修改注释 |
| 38 | `LABEL description=...` | 增加 `LABEL changelog=...` | 新增标签 |
| 46-47 | (无) | `ENV PYTHONDONTWRITEBYTECODE=1` | **新增** |
| 52-53 | `# 复制项目代码`<br>`COPY . .` | `# 业务代码通过 docker-compose volumes 挂载...` | **删除 COPY . .**,替换为注释 |
**保留不变的部分**
- 第一阶段(builder):`COPY requirements.txt .` + `pip install` — 依赖安装逻辑不变
- 第二阶段:`COPY --from=builder` — 从 builder 复制 site-packages 逻辑不变
- `EXPOSE 8000` + `CMD` — 启动命令不变
---
## 4. 部署流程对比
### 4.1 代码更新流程(仅业务代码变更,无依赖变化)
| 步骤 | 变更前(镜像烘焙模式) | 变更后(卷挂载模式) |
|------|----------------------|---------------------|
| 1 | 上传/解压代码到 `./app/` | 上传/解压代码到 `./app/` |
| 2 | 手动同步到 `./backend/app/`**易遗漏,根因** | ~~无需同步~~ |
| 3 | `docker compose build --no-cache backend`3-5 分钟) | ~~无需重建镜像~~ |
| 4 | `docker compose up -d backend`(重启容器) | `docker compose restart backend`10 秒) |
| 5 | 等待健康检查通过(30-40 秒) | 等待健康检查通过(10-15 秒) |
| **总计** | **4-6 分钟** | **15-30 秒** |
**变更后代码更新命令**
```bash
# 1. 更新代码(解压部署包或 git pull)
cd /opt/wecom-it-desk
tar -xf /tmp/deploy-backend.tar -C ./
# 2. 重启容器(无需重建镜像)
docker compose restart backend
# 3. 等待启动 + 验证
sleep 15
curl -sf http://localhost:8000/health && echo "OK" || echo "FAIL"
```
### 4.2 依赖更新流程(requirements.txt 变化)
| 步骤 | 变更前 | 变更后 |
|------|--------|--------|
| 1 | 更新 `./backend/requirements.txt` | 更新 `./backend/requirements.txt` |
| 2 | `docker compose build --no-cache backend` | `docker compose build backend`(可用缓存) |
| 3 | `docker compose up -d backend` | `docker compose up -d backend` |
| 4 | 验证 | 验证 |
| **总计** | **3-5 分钟** | **2-4 分钟**(Docker 层缓存命中时更快) |
> **注意**:依赖更新流程变化不大,因为依赖仍然烘焙在镜像中。但代码更新不再需要重建镜像,这是主要改进。
### 4.3 流程对比总结
```
变更前:代码更新 = 解压代码 → 同步到 backend/app/ → 重建镜像 → 重启容器
↑ 易出错
变更后:代码更新 = 解压代码 → 重启容器
↑ 简单可靠
```
---
## 5. 回滚策略(CRITICAL
### 5.0 版本固定策略(部署前执行,增强回滚能力)
卷挂载后镜像不含业务代码,回滚时缺少"已知可用状态"的锚点。通过在合并成功时打双 tag,可将回滚从分钟级降到秒级。
#### 5.0.1 前提条件
服务器 `/opt/wecom-it-desk/app/` 目录需要是 git 仓库(或从远程 clone):
```bash
# 检查服务器上 app/ 是否有 .git
cd /opt/wecom-it-desk && git rev-parse --git-dir 2>/dev/null && echo "OK: git 仓库已初始化" || echo "WARN: 需要 git init 或 clone"
```
> 如果当前没有 git 仓库,部署前先执行:
> ```bash
> cd /opt/wecom-it-desk && git init && git add -A && git commit -m "baseline: 方案C 部署前快照"
> ```
#### 5.0.2 双 Tag 固定
| Tag 类型 | 何时打 | 固定了什么 | 命令 |
|---------|--------|-----------|------|
| **Git tag**(代码版本) | 每次代码合并成功后、部署前 | 代码快照 | `cd /opt/wecom-it-desk && git tag deploy-$(date +%Y%m%d-%H%M)` |
| **Docker tag**(依赖版本) | `requirements.txt` 有变更且 `docker compose build` 成功后 | 依赖镜像(site-packages | `docker tag wecom-it-desk-backend:latest wecom-it-desk-backend:deps-$(date +%Y%m%d-%H%M)` |
**关键规则**
- Docker tag **不需要每次合并都打** — 只在 `requirements.txt` 有变化时才需要
- 代码变了但依赖没变 → 只打 git tag 即可
- 依赖变了(新增/删除/升级 pip 包)→ 两个 tag 都打
#### 5.0.3 快速回滚路径(版本固定后可用)
有了双 tag 后,回滚分为三种路径:
| 回滚类型 | 触发场景 | 操作步骤 | 耗时 |
|---------|---------|---------|------|
| **代码回滚**(99% 的情况) | 代码有 bug,需退到上一版本 | `git checkout <上一个 deploy-tag>``docker compose restart backend` | **3 秒** |
| **依赖回滚**(极少) | 新依赖导致问题 | 改 compose `image:` 指向 `<上一个 deps-tag>``docker compose up -d backend` | **10 秒** |
| **完整回滚**(代码+依赖都退) | 需退到更早的整体状态 | 两个 tag 都切 | **10 秒** |
**快速回滚命令(代码回滚)**
```bash
#!/bin/bash
# 快速代码回滚(需提前打好 git tag)
# 用法: bash fast-rollback.sh [git-tag-name]
# 不带参数则回滚到上一个 deploy tag
cd /opt/wecom-it-desk
if [ -z "$1" ]; then
# 自动找上一个 deploy tag
PREV_TAG=$(git tag -l "deploy-*" --sort=-creatordate | sed -n '2p')
if [ -z "$PREV_TAG" ]; then
PREV_TAG=$(git tag -l "deploy-*" --sort=-creatordate | sed -n '1p')
fi
echo "回滚到: $PREV_TAG"
git checkout "$PREV_TAG"
else
echo "回滚到: $1"
git checkout "$1"
fi
docker compose restart backend
sleep 5
# 验证
curl -sf http://localhost:8000/health && echo "PASS: 服务已恢复" || echo "FAIL: 需执行完整回滚(见 §5.2"
```
**快速回滚命令(依赖回滚)**
```bash
#!/bin/bash
# 快速依赖回滚(需提前打好 docker tag)
# 用法: bash fast-rollback-deps.sh <deps-tag-name>
cd /opt/wecom-it-desk
if [ -z "$1" ]; then
echo "用法: bash fast-rollback-deps.sh <deps-tag-name>"
echo "可用 deps tag:"
docker images --format "{{.Repository}}:{{.Tag}}" | grep "deps-"
exit 1
fi
# 修改 compose 使用指定 tag 镜像
sed -i "s|image: wecom-it-desk-backend:.*|image: wecom-it-desk-backend:$1|" docker-compose.yml
docker compose up -d backend
sleep 10
# 验证
curl -sf http://localhost:8000/health && echo "PASS: 服务已恢复" || echo "FAIL: 需执行完整回滚(见 §5.2"
```
#### 5.0.4 回滚路径选择决策
```
需要回滚
├── 有 git/docker tag
│ ├── 是 → 先尝试快速回滚(§5.0.3)
│ │ ├── 快速回滚成功 → 完成(秒级)
│ │ └── 快速回滚失败 → 执行完整回滚(§5.2)
│ └── 否 → 直接执行完整回滚(§5.2)
└── 问题涉及配置文件(docker-compose.yml / Dockerfile)?
└── 是 → 直接执行完整回滚(§5.2),恢复配置文件备份
```
### 5.1 回滚触发条件
满足以下 **任一** 条件即触发回滚:
| # | 触发条件 | 检测方法 |
|---|----------|----------|
| 1 | 容器启动失败(反复重启) | `docker compose ps backend` 状态为 `restarting``unhealthy` |
| 2 | 健康检查连续失败 | `curl -sf http://localhost:8000/health` 返回非 200 |
| 3 | 关键模块导入失败(如 auth) | `docker exec wecom_it_backend python -c "from app.auth import router"` 报错 |
| 4 | 卷挂载路径不存在或权限拒绝 | 容器日志出现 `ModuleNotFoundError``PermissionError` |
| 5 | 业务接口大面积 500 错误 | Nginx 日志或后端日志大量 500 状态码 |
### 5.2 回滚步骤(完整命令序列)
以下命令可通过 jumpserver-ops 在服务器 10.90.5.110 上执行:
```bash
#!/bin/bash
# =============================================================================
# 回滚脚本:卷挂载方案 → 镜像烘焙方案
# 执行方式:通过 jumpserver-ops 在 10.90.5.110 上执行
# 前提:备份文件存在(部署时已创建 .bak.{TIMESTAMP} 后缀文件)
# =============================================================================
set -e
cd /opt/wecom-it-desk
echo "===== 回滚开始: $(date) ====="
# --------------------------------------------------------------------------
# 步骤 1: 查找并恢复备份的配置文件
# --------------------------------------------------------------------------
echo ">>> [1/6] 恢复配置文件..."
# 查找最新的 docker-compose.yml 备份
COMPOSE_BAK=$(ls -t /opt/wecom-it-desk/docker-compose.yml.bak.* 2>/dev/null | head -1)
if [ -z "$COMPOSE_BAK" ]; then
echo "ERROR: 未找到 docker-compose.yml 备份文件!"
echo "可手动从 git 恢复: git checkout -- docker-compose.yml"
exit 1
fi
cp "$COMPOSE_BAK" /opt/wecom-it-desk/docker-compose.yml
echo " 已恢复 docker-compose.yml <- $COMPOSE_BAK"
# 查找最新的 Dockerfile 备份
DOCKERFILE_BAK=$(ls -t /opt/wecom-it-desk/backend/Dockerfile.bak.* 2>/dev/null | head -1)
if [ -z "$DOCKERFILE_BAK" ]; then
echo "ERROR: 未找到 Dockerfile 备份文件!"
echo "可手动从 git 恢复: git checkout -- backend/Dockerfile"
exit 1
fi
cp "$DOCKERFILE_BAK" /opt/wecom-it-desk/backend/Dockerfile
echo " 已恢复 Dockerfile <- $DOCKERFILE_BAK"
# --------------------------------------------------------------------------
# 步骤 2: 确保 backend/app/ 存在(回滚安全网)
# --------------------------------------------------------------------------
echo ">>> [2/6] 检查 backend/app/ 目录..."
if [ ! -d /opt/wecom-it-desk/backend/app/ ] || [ -z "$(ls -A /opt/wecom-it-desk/backend/app/ 2>/dev/null)" ]; then
echo " backend/app/ 不存在或为空,从 app/ 同步代码..."
mkdir -p /opt/wecom-it-desk/backend/app/
cp -r /opt/wecom-it-desk/app/* /opt/wecom-it-desk/backend/app/
cp -r /opt/wecom-it-desk/app/.* /opt/wecom-it-desk/backend/app/ 2>/dev/null || true
echo " 已同步代码到 backend/app/"
else
echo " backend/app/ 已存在,跳过同步"
fi
# --------------------------------------------------------------------------
# 步骤 3: 重新构建镜像(使用原始 Dockerfile,含 COPY . .
# --------------------------------------------------------------------------
echo ">>> [3/6] 重新构建后端镜像..."
docker compose build --no-cache backend
# --------------------------------------------------------------------------
# 步骤 4: 重启容器
# --------------------------------------------------------------------------
echo ">>> [4/6] 重启后端容器..."
docker compose up -d backend
# --------------------------------------------------------------------------
# 步骤 5: 等待服务就绪
# --------------------------------------------------------------------------
echo ">>> [5/6] 等待服务启动..."
echo " 等待 45 秒(healthcheck start_period..."
sleep 45
# --------------------------------------------------------------------------
# 步骤 6: 验证
# --------------------------------------------------------------------------
echo ">>> [6/6] 验证回滚结果..."
# 容器状态
echo ""
echo "--- 容器状态 ---"
docker compose ps backend
# 健康检查
echo ""
echo "--- 健康检查 ---"
if curl -sf http://localhost:8000/health > /dev/null 2>&1; then
echo " PASS: /health 返回正常"
else
echo " WARN: /health 未就绪,再等待 15 秒..."
sleep 15
if curl -sf http://localhost:8000/health > /dev/null 2>&1; then
echo " PASS: /health 返回正常(延迟就绪)"
else
echo " FAIL: /health 仍然失败"
echo " 查看日志: docker compose logs --tail=50 backend"
fi
fi
# Auth 模块验证
echo ""
echo "--- Auth 模块验证 ---"
if docker exec wecom_it_backend python -c "from app.auth import router; print('auth OK')" 2>/dev/null; then
echo " PASS: auth 模块可导入"
else
echo " FAIL: auth 模块导入失败"
echo " 查看日志: docker compose logs --tail=50 backend"
fi
# 最近日志
echo ""
echo "--- 最近 20 行日志 ---"
docker compose logs --tail=20 backend
echo ""
echo "===== 回滚完成: $(date) ====="
echo ""
echo "如回滚后仍有问题,请检查:"
echo " 1. backend/app/ 代码是否完整: ls -la /opt/wecom-it-desk/backend/app/"
echo " 2. 镜像构建是否成功: docker images | grep wecom-it-desk-backend"
echo " 3. 容器日志: docker compose logs -f backend"
```
### 5.3 回滚后验证
| 验证项 | 命令 | 预期结果 |
|--------|------|----------|
| 容器运行状态 | `docker compose ps backend` | Status 为 `Up (healthy)` |
| 健康检查 | `curl -sf http://localhost:8000/health` | 返回 `{"status":"healthy"}` 或类似 |
| Auth 模块 | `docker exec wecom_it_backend python -c "from app.auth import router; print('OK')"` | 输出 `auth OK` |
| Auth API | `curl -sf http://localhost:8000/api/auth/wecom/login -X POST -H "Content-Type: application/json" -d '{}'` | 返回 JSON(非 500 |
| Nginx 代理 | `curl -sf http://localhost:80/itdesk/health` | 返回正常 |
| 日志无异常 | `docker compose logs --tail=50 backend` | 无 `ModuleNotFoundError` / `ImportError` |
### 5.4 回滚时间预估
| 回滚路径 | 适用场景 | 耗时 | 前提 |
|---------|---------|------|------|
| **快速回滚(代码)** | 代码 bug,退到上一版本 | **3 秒** | 已打 git tag(§5.0 |
| **快速回滚(依赖)** | 新依赖导致问题 | **10 秒** | 已打 docker tag(§5.0 |
| **完整回滚** | 配置文件变更/快速回滚失败 | **3.5-5.5 分钟** | 备份文件存在 |
**完整回滚各步骤耗时**
| 步骤 | 耗时 | 说明 |
|------|------|------|
| 恢复配置文件 | 5 秒 | cp 命令 |
| 检查/同步 backend/app/ | 5-30 秒 | 如需同步代码 |
| 重建镜像 | 2-4 分钟 | `docker compose build --no-cache` |
| 重启容器 | 10 秒 | `docker compose up -d` |
| 等待就绪 | 45 秒 | healthcheck start_period |
| 验证 | 15 秒 | curl + docker exec |
| **总计** | **3.5-5.5 分钟** | |
> **建议**:始终先尝试快速回滚(秒级),失败后再执行完整回滚(分钟级)。版本固定策略(§5.0)是快速回滚的前提。
---
## 6. 风险评估
### 6.1 风险矩阵
| # | 风险 | 影响 | 概率 | 等级 | 对策 |
|---|------|------|------|------|------|
| R1 | `__pycache__` 污染宿主机代码目录 | 权限错误 / 缓存过期导致行为异常 | 中 | 中 | Dockerfile 设置 `PYTHONDONTWRITEBYTECODE=1` |
| R2 | 宿主机代码被意外修改导致运行中服务异常 | 服务不稳定 | 低 | 中 | 限制 `/opt/wecom-it-desk/app/` 目录写权限;部署外时间窗口操作 |
| R3 | `backend/app/` 被提前删除导致回滚失败 | 回滚无法执行 | 低 | 高 | 48 小时内不删除;回滚脚本含自动同步逻辑 |
| R4 | 卷挂载路径与现有挂载冲突 | 容器启动失败 | 极低 | 低 | `/app/app``/app/uploads``/app/logs` 无交集 |
| R5 | requirements.txt 与代码不同步(新增依赖未安装) | `ImportError` 运行时崩溃 | 低 | 中 | 部署前对比 requirements.txt 差异;新增依赖时先重建镜像 |
| R6 | Docker Compose 版本不兼容 bind mount | 挂载行为异常 | 极低 | 低 | 部署前验证 `docker compose version` |
| R7 | 宿主机代码目录包含敏感文件(.env 等)被容器读取 | 信息泄露 | 低 | 中 | 部署包不含 `.env``.dockerignore` 已排除敏感文件 |
| R8 | 容器以 root 运行,挂载目录权限过松 | 安全风险 | 低 | 低 | 长期可考虑添加 `user:` 指令;短期可接受 |
### 6.2 详细风险分析
#### R1: `__pycache__` 污染
**场景**:容器运行时 Python 默认在 `__pycache__/` 目录下写入 `.pyc` 字节码缓存。由于代码目录是 volume 挂载,这些文件会写入宿主机 `/opt/wecom-it-desk/app/__pycache__/`
**影响**
- 如果宿主机和容器 Python 版本不同,`.pyc` 文件可能不兼容
- `__pycache__` 目录可能因权限问题导致写入失败(静默失败,不影响运行但产生日志噪音)
- 代码更新后 stale `.pyc` 可能导致旧代码被执行(Python 3.12 已大幅改善此问题,但仍有边缘情况)
**对策**Dockerfile 设置 `ENV PYTHONDONTWRITEBYTECODE=1`,完全禁止 `.pyc` 写入。生产环境不需要字节码缓存(uvicorn 启动时编译一次即可)。
#### R2: 宿主机代码意外修改
**场景**:卷挂载是双向的(默认 read-write),宿主机上的文件修改会实时反映到容器中。
**影响**:如果在服务运行期间有人修改了 `/opt/wecom-it-desk/app/` 中的代码文件,可能导致运行中服务行为异常(虽然 uvicorn 不带 `--reload` 不会自动重载,但下次 import 可能读到修改后的代码)。
**对策**
- 生产环境 docker-compose.yml 的 command 不含 `--reload`,代码修改不会自动生效
- 限制 `/opt/wecom-it-desk/app/` 目录的写权限
- 所有代码变更通过正式部署流程进行
#### R3: backend/app/ 提前删除
**场景**:部署完成后立即删除 `backend/app/`,若随后需要回滚,原始 Dockerfile 的 `COPY . .` 找不到代码。
**对策**
- 回滚脚本(5.2 节)包含自动同步逻辑:若 `backend/app/` 不存在,从 `app/` 复制
- 建议 48 小时稳定运行后再删除 `backend/app/`
#### R5: 依赖与代码不同步
**场景**:开发者在新代码中引入了新的第三方库(如 `import httpx`),但未更新 `requirements.txt` 或未重建镜像。
**影响**:容器启动时 `ImportError: No module named 'httpx'`
**对策**
- 部署前检查:`diff` 对比新旧 `requirements.txt`
- 若有变化,先执行 `docker compose build backend` 再重启
- 在部署流程中增加自动化检查
---
## 7. 任务分解
### 7.1 部署步骤(有序)
| 步骤 | 名称 | 命令 / 操作 | 依赖 | 耗时 | 验证方法 |
|------|------|-------------|------|------|----------|
| S1 | 前置验证与备份 | 见下方命令块 S1 | 无 | 2 分钟 | 备份文件存在;当前 `/health` 返回正常 |
| S2 | 代码同步与验证 | 见下方命令块 S2 | S1 | 1 分钟 | `./app/main.py``./app/api/auth.py` 存在 |
| S3 | 修改 Dockerfile | 见下方命令块 S3 | S2 | 1 分钟 | `grep -c "COPY . ." Dockerfile` 返回 0 |
| S4 | 修改 docker-compose.yml | 见下方命令块 S4 | S3 | 1 分钟 | `grep "app:/app/app" docker-compose.yml` 返回匹配 |
| S5 | 重建镜像并重启 | 见下方命令块 S5 | S4 | 3 分钟 | `docker compose ps backend` 状态为 `Up` |
| S6 | 部署后验证 | 见下方命令块 S6 | S5 | 2 分钟 | 健康检查通过 + auth 模块可导入 |
| S7 | 清理(48h 后) | 见下方命令块 S7 | S6 + 48h | 1 分钟 | `backend/app/` 已删除 |
### 7.2 完整命令块
**S1: 前置验证与备份**
```bash
#!/bin/bash
set -e
cd /opt/wecom-it-desk
# 1. 验证当前服务正常
echo ">>> 验证当前服务状态..."
curl -sf http://localhost:8000/health > /dev/null && echo " PASS: 当前服务正常" || { echo " FAIL: 当前服务异常,请先修复再部署"; exit 1; }
# 2. 备份配置文件
BACKUP_TS=$(date +%Y%m%d%H%M%S)
cp /opt/wecom-it-desk/docker-compose.yml /opt/wecom-it-desk/docker-compose.yml.bak.${BACKUP_TS}
cp /opt/wecom-it-desk/backend/Dockerfile /opt/wecom-it-desk/backend/Dockerfile.bak.${BACKUP_TS}
echo " PASS: 备份完成 (timestamp: ${BACKUP_TS})"
# 3. 记录回滚信息
cat > /opt/wecom-it-desk/.rollback-info << EOF
ROLLBACK_TIMESTAMP=${BACKUP_TS}
COMPOSE_BAK=/opt/wecom-it-desk/docker-compose.yml.bak.${BACKUP_TS}
DOCKERFILE_BAK=/opt/wecom-it-desk/backend/Dockerfile.bak.${BACKUP_TS}
DEPLOY_DATE=$(date)
EOF
echo " PASS: 回滚信息已记录到 .rollback-info"
echo ""
echo "===== S1 完成 ====="
```
**S2: 代码同步与验证**
```bash
#!/bin/bash
set -e
cd /opt/wecom-it-desk
# 1. 验证 app/ 目录存在且包含关键文件
echo ">>> 验证代码目录..."
for f in app/__init__.py app/main.py app/config.py app/database.py; do
if [ -f "$f" ]; then
echo " PASS: $f 存在"
else
echo " FAIL: $f 不存在!请先解压部署包: tar -xf /tmp/deploy-backend.tar -C ./"
exit 1
fi
done
# 2. 验证 auth 模块(关键!曾因缺失导致故障)
if [ -f "app/api/auth.py" ]; then
echo " PASS: app/api/auth.py 存在"
else
echo " FAIL: app/api/auth.py 不存在!认证功能将不可用"
exit 1
fi
# 3. 统计代码文件数
FILE_COUNT=$(find app/ -name "*.py" | wc -l)
echo " INFO: app/ 目录共 ${FILE_COUNT} 个 Python 文件"
echo ""
echo "===== S2 完成 ====="
```
**S3: 修改 Dockerfile**
```bash
#!/bin/bash
set -e
DOCKERFILE=/opt/wecom-it-desk/backend/Dockerfile
echo ">>> 修改 Dockerfile..."
# 方法:直接写入完整文件(最可靠)
cat > "$DOCKERFILE" << 'DOCKERFILE_EOF'
# =============================================================================
# 企微IT智能服务台 — 后端 Docker 镜像构建文件
# =============================================================================
# 说明:基于 Python 3.12 构建后端镜像
# 变更:2025-07-10 方案C — 代码改为 volume 挂载,镜像不再 COPY 业务代码
# 用法:docker build -t wecom-it-desk-backend .
# =============================================================================
# --------------------------------------------------------------------------
# 第一阶段:构建阶段
# --------------------------------------------------------------------------
FROM python:3.12-slim AS builder
# 设置工作目录
WORKDIR /app
# 安装系统依赖(psycopg2 编译需要 + qrcode 图片处理需要 + healthcheck 需要 curl
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc libpq-dev libjpeg-dev zlib1g-dev curl && \
rm -rf /var/lib/apt/lists/*
# 复制依赖声明文件并安装(利用 Docker 层缓存,依赖不变则不重新安装)
# 使用阿里云 PyPI 镜像(比清华镜像更快)
COPY requirements.txt .
RUN pip install --no-cache-dir \
--timeout 180 \
--retries 5 \
-i https://mirrors.aliyun.com/pypi/simple/ \
--trusted-host mirrors.aliyun.com \
-r requirements.txt
# --------------------------------------------------------------------------
# 第二阶段:运行阶段(更小的镜像体积)
# --------------------------------------------------------------------------
FROM python:3.12-slim
# 设置标签信息
LABEL maintainer="IT服务台开发团队"
LABEL description="企微IT智能服务台后端服务"
LABEL changelog="2025-07-10: 移除 COPY . .,代码改为 volume 挂载"
# 安装运行时依赖(psycopg2 运行时需要 libpq + healthcheck 需要 curl
RUN apt-get update && \
apt-get install -y --no-install-recommends libpq5 curl && \
rm -rf /var/lib/apt/lists/*
# 设置工作目录
WORKDIR /app
# 禁止 Python 写入 __pycache__(防止污染宿主机代码目录)
ENV PYTHONDONTWRITEBYTECODE=1
# 从构建阶段复制已安装的 Python 包
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
# 业务代码通过 docker-compose volumes 挂载(./app:/app/app),不再 COPY 进镜像
# 暴露端口
EXPOSE 8000
# 启动命令(Docker Compose 中会覆盖)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
DOCKERFILE_EOF
# 验证
if grep -q "COPY . ." "$DOCKERFILE"; then
echo " FAIL: Dockerfile 仍包含 COPY . ."
exit 1
fi
if grep -q "PYTHONDONTWRITEBYTECODE" "$DOCKERFILE"; then
echo " PASS: PYTHONDONTWRITEBYTECODE 已设置"
else
echo " FAIL: PYTHONDONTWRITEBYTECODE 未找到"
exit 1
fi
echo " PASS: Dockerfile 已更新"
echo ""
echo "===== S3 完成 ====="
```
**S4: 修改 docker-compose.yml**
```bash
#!/bin/bash
set -e
cd /opt/wecom-it-desk
COMPOSE_FILE=/opt/wecom-it-desk/docker-compose.yml
echo ">>> 修改 docker-compose.yml..."
# 在 backend-uploads 行之前插入代码卷挂载行
# 使用 sed 的 insert (i) 命令
sed -i '/backend-uploads:\/app\/uploads/i\ - ./app:/app/app # 代码卷挂载(方案C)' "$COMPOSE_FILE"
# 验证
if grep -q "./app:/app/app" "$COMPOSE_FILE"; then
echo " PASS: 代码卷挂载已添加"
else
echo " FAIL: 代码卷挂载未找到,请手动编辑 docker-compose.yml"
echo " 在 backend 服务的 volumes: 下添加:"
echo " - ./app:/app/app"
exit 1
fi
# 显示变更后的 volumes 段
echo ""
echo " 变更后的 volumes 配置:"
sed -n '/^ backend:/,/^ [a-z]/p' "$COMPOSE_FILE" | grep -A 5 "volumes:"
echo ""
echo "===== S4 完成 ====="
```
**S5: 重建镜像并重启**
```bash
#!/bin/bash
set -e
cd /opt/wecom-it-desk
echo ">>> 重建后端镜像..."
docker compose build backend
echo " PASS: 镜像构建完成"
echo ""
echo ">>> 重启后端容器..."
docker compose up -d backend
echo " PASS: 容器已启动"
echo ""
echo ">>> 等待服务就绪 (45秒)..."
sleep 45
echo ""
echo "===== S5 完成 ====="
```
**S6: 部署后验证**
```bash
#!/bin/bash
cd /opt/wecom-it-desk
echo "===== 部署后验证 ====="
echo ""
# 1. 容器状态
echo "--- 1. 容器状态 ---"
docker compose ps backend
echo ""
# 2. 健康检查
echo "--- 2. 健康检查 ---"
if curl -sf http://localhost:8000/health; then
echo ""
echo " PASS: /health 正常"
else
echo " FAIL: /health 异常"
fi
echo ""
# 3. Auth 模块验证(曾因缺失导致故障)
echo "--- 3. Auth 模块验证 ---"
if docker exec wecom_it_backend python -c "from app.auth import router; print(' auth module: OK')" 2>/dev/null; then
echo " PASS: auth 模块可导入"
else
echo " FAIL: auth 模块导入失败"
echo " 查看日志: docker compose logs --tail=50 backend"
fi
echo ""
# 4. 卷挂载验证
echo "--- 4. 卷挂载验证 ---"
if docker exec wecom_it_backend ls -la /app/app/main.py > /dev/null 2>&1; then
echo " PASS: /app/app/main.py 可访问(卷挂载正常)"
else
echo " FAIL: /app/app/main.py 不可访问(卷挂载异常)"
fi
echo ""
# 5. 代码来源验证(确认代码来自宿主机而非镜像)
echo "--- 5. 代码来源验证 ---"
HOST_HASH=$(md5sum /opt/wecom-it-desk/app/main.py | awk '{print $1}')
CONTAINER_HASH=$(docker exec wecom_it_backend md5sum /app/app/main.py 2>/dev/null | awk '{print $1}')
if [ "$HOST_HASH" = "$CONTAINER_HASH" ]; then
echo " PASS: 宿主机与容器代码一致 (md5: ${HOST_HASH})"
else
echo " WARN: 宿主机与容器代码不一致"
echo " 宿主机: $HOST_HASH"
echo " 容器: $CONTAINER_HASH"
fi
echo ""
# 6. 日志检查
echo "--- 6. 最近 20 行日志 ---"
docker compose logs --tail=20 backend
echo ""
echo "===== 验证完成 ====="
echo ""
echo "如全部 PASS,部署成功。"
echo "如出现 FAIL,请执行回滚脚本(见回滚策略章节)。"
```
**S7: 清理(48 小时后执行)**
```bash
#!/bin/bash
set -e
cd /opt/wecom-it-desk
echo ">>> 清理旧代码目录..."
echo " 注意:仅在部署成功 48 小时后执行此步骤!"
echo ""
# 确认服务稳定
curl -sf http://localhost:8000/health > /dev/null && echo " 服务正常" || { echo " 服务异常,取消清理"; exit 1; }
# 删除 backend/app/(不再需要)
if [ -d /opt/wecom-it-desk/backend/app/ ]; then
echo " 删除 backend/app/..."
rm -rf /opt/wecom-it-desk/backend/app/
echo " PASS: backend/app/ 已删除"
else
echo " INFO: backend/app/ 已不存在,跳过"
fi
# 可选:删除备份文件(建议保留 7 天)
# find /opt/wecom-it-desk/ -name "*.bak.*" -mtime +7 -delete
echo ""
echo "===== 清理完成 ====="
```
### 7.3 任务依赖图
```mermaid
graph TD
S1[S1: 前置验证与备份<br/>2分钟] --> S2[S2: 代码同步与验证<br/>1分钟]
S2 --> S3[S3: 修改 Dockerfile<br/>1分钟]
S3 --> S4[S4: 修改 docker-compose.yml<br/>1分钟]
S4 --> S5[S5: 重建镜像并重启<br/>3分钟]
S5 --> S6[S6: 部署后验证<br/>2分钟]
S6 --> S7[S7: 清理旧代码<br/>48小时后 / 1分钟]
S6 -.->|如验证失败| ROLLBACK[执行回滚脚本<br/>3.5-5.5分钟]
ROLLBACK -.->|回滚后| RECOVERY[恢复到变更前状态]
style S1 fill:#4CAF50,color:#fff
style S5 fill:#FF9800,color:#fff
style S6 fill:#2196F3,color:#fff
style S7 fill:#9E9E9E,color:#fff
style ROLLBACK fill:#f44336,color:#fff
```
### 7.4 预估总时间
| 阶段 | 步骤 | 耗时 |
|------|------|------|
| 准备 | S1 + S2 | 3 分钟 |
| 变更 | S3 + S4 | 2 分钟 |
| 部署 | S5 | 3 分钟 |
| 验证 | S6 | 2 分钟 |
| **总计** | S1-S6 | **约 10 分钟** |
| 清理 | S748h 后) | 1 分钟 |
| **回滚(如需)** | 回滚脚本 | **3.5-5.5 分钟** |
---
## 8. 附录
### 8.1 架构对比图
```mermaid
graph LR
subgraph BEFORE["变更前:镜像烘焙模式"]
direction TB
B1["宿主机 app/<br/>(新代码)"] -.->|"不同步!"| B2["宿主机 backend/app/<br/>(旧代码)"]
B2 -->|"COPY . ."| B3["Docker 镜像<br/>(含代码)"]
B3 --> B4["容器 /app/app/"]
end
subgraph AFTER["变更后:卷挂载模式"]
direction TB
A1["宿主机 app/<br/>(唯一代码源)"] -->|"volume mount"| A2["容器 /app/app/"]
A3["Docker 镜像<br/>(仅含依赖)"] --> A2
end
BEFORE -.->|"重构"| AFTER
style BEFORE fill:#ffebee
style AFTER fill:#e8f5e9
```
### 8.2 部署时序图
```mermaid
sequenceDiagram
participant Ops as 运维人员
participant Host as 宿主机 10.90.5.110
participant Docker as Docker Engine
participant Container as Backend 容器
Note over Ops,Container: S1: 前置验证与备份
Ops->>Host: curl /health 验证当前状态
Ops->>Host: 备份 docker-compose.yml + Dockerfile
Note over Ops,Container: S2: 代码验证
Ops->>Host: 检查 app/main.py, app/api/auth.py 存在
Note over Ops,Container: S3-S4: 修改配置文件
Ops->>Host: 重写 Dockerfile(删除 COPY . .
Ops->>Host: 编辑 docker-compose.yml(添加 ./app:/app/app
Note over Ops,Container: S5: 重建并重启
Ops->>Docker: docker compose build backend
Docker->>Docker: 构建镜像(仅 site-packages,无业务代码)
Ops->>Docker: docker compose up -d backend
Docker->>Container: 创建容器
Docker->>Host: 挂载 ./app → /app/app
Container->>Host: 运行时读取 /opt/wecom-it-desk/app/ 代码
Container->>Container: uvicorn app.main:app 启动
Note over Ops,Container: S6: 验证
Ops->>Container: curl /health
Ops->>Container: docker exec ... from app.auth import router
Ops->>Container: md5sum 对比宿主机与容器代码
```
### 8.3 关键设计决策
| 决策点 | 选择 | 理由 |
|--------|------|------|
| 挂载路径 `./app` vs `./backend/app` | `./app` | 部署包 tar 解压到 `./app/`,与现有部署流程兼容;无需修改打包脚本 |
| `PYTHONDONTWRITEBYTECODE` 位置 | Dockerfile `ENV` | 属于镜像属性,所有基于此镜像的容器都生效 |
| `backend/app/` 删除时机 | 48 小时后 | 保留回滚安全网;回滚脚本含自动同步逻辑作为兜底 |
| 卷挂载读写模式 | 默认 read-write | 虽然生产不需要写入代码目录,但 read-only 模式可能导致边缘问题;用 `PYTHONDONTWRITEBYTECODE` 替代 |
| 是否挂载 alembic/scripts | 否 | 生产 command 跳过迁移;需要时可手动 `docker exec` 执行 |
### 8.4 Shared Knowledge(工程师注意事项)
- **代码唯一源**`/opt/wecom-it-desk/app/` 是生产环境唯一的代码目录,所有代码更新只操作此目录
- **镜像不含代码**:重建镜像只更新依赖包,代码通过 volume 挂载提供
- **部署区分**:代码更新 → `docker compose restart`;依赖更新 → `docker compose build && docker compose up -d`
- **回滚文件**:部署时创建的 `.bak.{TIMESTAMP}` 文件是回滚的关键,不要手动删除
- **回滚信息**`/opt/wecom-it-desk/.rollback-info` 文件记录了备份文件路径,回滚时可直接读取
- **dev 环境**:本地开发使用 `docker-compose.dev.yml`,已使用 `./backend/app:/app/app` 挂载,不受此次变更影响
- **健康检查**:容器 healthcheck 的 `start_period` 为 40 秒,回滚验证需等待至少 45 秒
---
> **文档结束**
> 如有疑问请联系架构师 高见远。
> 回滚策略已完整覆盖所有场景,可直接通过 jumpserver-ops 执行。