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
This commit is contained in:
Simon
2026-07-11 23:13:10 +08:00
parent 3d152fc8eb
commit bea288e414
928 changed files with 85169 additions and 54205 deletions
@@ -1,10 +1,10 @@
# 00 · 标准故障排查手册
> **版本**: v1.1 | **日期**: 2026-07-08 | **维护人**: 宋献 / 助理
> **版本**: v1.4 | **日期**: 2026-07-10 | **维护人**: 宋献 / 助理
> **定位**: 所有故障排查前**首先查看本手册**。
> **最新**: 新增 CASE-20260708-01~06OTP 路由404 / nginx 404 / 扫码角色 / 用户角色 / 员工端路由 / OTP列表结构)+ nginx 配错急救流程
> **最新**: 方案 C(卷挂载)已上线,§1.4 更新为 volume 挂载验证 + CASE-20260710-02 标注根因已消除 + 错误码速查更新
| v1.1 | 2026-07-08 | 新增 6 天 7.8 案例 + nginx 急救流程 + 端到端验证更新 |
| v1.4 | 2026-07-10 | 方案 C 上线:§1.4 改为 volume 挂载验证 + 错误码速查更新 + CASE-20260710-02 根因已消除标注 |
> **前置阅读**: [运维手册(部署/回滚/备份/应急)](../01-项目总览/01-智能IT服务系统运维手册-20260704.md) · [SOP-04 应急响应](../10-项目管理/SOPs-标准流程/SOP-04-应急响应.md)
---
@@ -24,13 +24,26 @@
| 版本 | 日期 | 变更 |
|------|------|------|
| v1.0 | 2026-07-07 | 整合 9 份散落文档 + 新增 CASE-20260707-01Redis urlparse 挂起)|
| v1.1 | 2026-07-08 | 新增 CASE-20260708-01~06 + nginx 急救流程 + 端到端验证更新 |
| v1.2 | 2026-07-10 | 新增 CASE-20260710-01CSP 拦截内联 JS+ Step 0 响应头检查 + sed -i inode 教训 |
| v1.4 | 2026-07-10 | 方案 C 上线:§1.4 改为 volume 挂载验证 + 错误码速查更新 + CASE-20260710-02 根因已消除标注 |
| v1.3 | 2026-07-10 | 新增 CASE-20260710-02(镜像缺文件导致 API 404)+ 判定矩阵扩展 + 部署前同步检查清单 |
---
## 1 快速诊断决策树
### 1.1 三步隔离法(通用)
任何"页面打不开 / 网络连接失败 / 接口无响应 / 422"都先用三步隔离,定位是 nginx、后端、还是依赖(DB / Redis)的问题
任何"页面打不开 / 网络连接失败 / 接口无响应 / 422"都先用三步隔离,定位是 nginx、后端、还是依赖(DB / Redis)的问题
#### Step 0HTTP 响应头检查(页面 200 但功能异常时首先执行)
```bash
# 检查响应头,特别关注 Content-Security-Policy / X-Frame-Options / X-Content-Type-Options
curl -ksI https://itsupport.servyou.com.cn/h5/ | grep -iE 'content-security|x-frame|content-type'
```
**为什么要有 Step 0**:页面返回 HTTP 200、HTML 正常加载,但 JavaScript 不执行——这类问题无法被三步隔离法捕获。根因通常是 CSPContent-Security-Policy)头限制了 `script-src`,导致内联 `<script>` 被浏览器静默拦截。详见 CASE-20260710-01。
```bash
# 第1步:nginx 层可达性(在服务器执行;浏览器走 HTTPS,故用 https 而非 localhost
@@ -56,6 +69,8 @@ docker compose exec postgres pg_isready -U wecom # 期望 accepting
| 某端点 502 | ❌502 | ❌后端 down | — | 后端未起 / 缺 `PYTHONPATH=/app` |
| /itdesk/ 200 但 /api/... 404 | ✅ | — | — | nginx 代理路径不匹配 |
| 422 | ✅ | API 校验失败 | — | 请求体缺字段(见 §2)|
| 页面 200 但 JS 不执行 | ✅200 | — | — | **CSP 拦截内联 JS**(检查 `Content-Security-Policy` 头的 `script-src` 是否含 `'unsafe-inline'`,见 Step 0 / CASE-20260710-01|
| API 404 但本地代码存在 | ✅200 | ❌404 | — | ~~镜像缺文件~~(方案 C 前根因)。卷挂载模式下检查 volume 挂载 + `./app/` 代码完整性(见 CASE-20260710-02 / §1.4|
### 1.2 关键陷阱:URL 特殊字符导致依赖"静默挂起"
详见案例 **CASE-20260707-01**。密码含 `@` `#` 时,`urlparse` 把它们当 URL 分隔符,连到不存在的 host,连接**无限挂起**(浏览器表现为"网络连接失败",curl 永远等不到返回)。这是最隐蔽的一类故障——容器全 Up、nginx 全 200、唯独业务接口卡死。
@@ -70,6 +85,57 @@ python jms_ops.py exec -c "docker compose ps" -c "curl -ksI https://itsupport.se
> 原"服务器端跑诊断"的 3 种手工方式(PuTTY 跳堡垒机 / scp 上传 / 服务器下载)已不推荐,统一用上述 jumpserver-ops 自动化。
### 1.4 部署前检查清单(卷挂载模式 — 方案 C)
**触发条件**:任何涉及后端代码变更的部署(新增/修改 `.py` 文件、新增 Python 依赖)。
#### ⛔ 硬规则:后端代码部署方式(方案 C 卷挂载,2026-07-10 上线)
| 变更类型 | 正确命令 | ❌ 禁止操作 | 耗时 |
|---------|---------|---------|------|
| `.py` 文件变更 | `cd /opt/wecom-it-desk && docker compose restart backend` | ❌ `docker compose build` | ~15-30 秒 |
| `requirements.txt` 变更 | `docker compose build backend && docker compose up -d backend` | — | ~60-90 秒 |
| `.env` / `docker-compose.yml` 变更 | `docker compose up -d backend` | — | ~10 秒 |
> 代码通过 `./app:/app/app` volume 挂载到容器,不烘焙进镜像。改代码只需 restart 让 uvicorn 重新加载,无需重建镜像。`docker compose build` 只在 Python 依赖变化时才需要。
**背景**:方案 C(卷挂载)已于 2026-07-10 上线。代码不再烘焙进 Docker 镜像,而是通过 `./app:/app/app` volume 挂载到容器。`backend/app/` 旧代码目录已删除。部署前只需验证 `./app/` 代码目录完整性和 volume 挂载状态。
> ⚠️ **历史背景**(已消除):方案 C 前,服务器存在两份代码目录 `app/` 和 `backend/app/`,不同步导致镜像缺文件(见 CASE-20260710-02)。方案 C 消除了此根因。
**检查清单(经堡垒机执行)**
```bash
# 1. 验证 ./app/ 关键文件存在
for f in app/__init__.py app/main.py app/api/auth.py; do
[ -f "/opt/wecom-it-desk/$f" ] && echo "PASS: $f" || echo "FAIL: $f missing"
done
# 2. 验证 volume 挂载正常(容器内可访问代码)
docker exec wecom_it_backend ls /app/app/main.py
# 3. 验证代码一致性(宿主机与容器内 MD5 一致)
HOST=$(md5sum /opt/wecom-it-desk/app/main.py | awk '{print $1}')
CONTAINER=$(docker exec wecom_it_backend md5sum /app/app/main.py | awk '{print $1}')
[ "$HOST" = "$CONTAINER" ] && echo "PASS: code match" || echo "FAIL: code mismatch"
# 4. 如有新依赖,检查 requirements.txt
# diff 本地 requirements.txt 与服务器上的(如需要)
# 5. 代码更新后只需重启(无需重建镜像)
cd /opt/wecom-it-desk && docker compose restart backend
```
**简化版(用 jumpserver-ops 一键检查)**
```bash
python jms_ops.py exec \
-c "for f in app/__init__.py app/main.py app/api/auth.py; do [ -f /opt/wecom-it-desk/\$f ] && echo \"PASS: \$f\" || echo \"FAIL: \$f\"; done" \
-c "docker exec wecom_it_backend ls /app/app/main.py && echo PASS_mount" \
--reuse
```
> ⚠️ 代码更新后 `docker compose restart` 即可(15-30 秒)。仅当 `requirements.txt` 有变化时才需要 `docker compose build`。
---
## 2 常见错误码速查(E5xx
@@ -82,6 +148,8 @@ python jms_ops.py exec -c "docker compose ps" -c "curl -ksI https://itsupport.se
| **E403** | IP 白名单 / 无权限 | `grep allow /opt/wecom-it-desk/nginx/nginx.conf`admin 角色不足 |
| **E422** | 请求体校验失败(Pydantic)| 确认必填字段齐全(如登录需 `user_id`+`name`|
| **网络失败 / 连接挂起** | 依赖不可达(最常见 Redis 配置错)| 见 §1.2 / CASE-20260707-01 |
| **页面 200 但 JS 不执行** | CSP 头 `script-src``'unsafe-inline'`,内联 `<script>` 被浏览器静默拦截 | `curl -ksI <URL> \| grep content-security`;检查 `script-src` 是否含 `'unsafe-inline'`(见 CASE-20260710-01|
| **API 404 但本地代码存在** | ~~镜像缺文件~~(方案 C 前根因已消除)。卷挂载模式下检查:volume 是否正常挂载 + `./app/` 是否有该文件 | `docker exec <容器> ls /app/app/api/auth.py``docker exec <容器> md5sum /app/app/main.py` 对比宿主机(见 §1.4 / CASE-20260710-02|
### 2.1 E500 常见根因速查
- 数据库缺列 → `ALTER TABLE ... ADD COLUMN IF NOT EXISTS ...`
@@ -133,6 +201,47 @@ docker logs wecom_it_backend | grep -i websocket
## 4 案例库(倒序,编号 CASE-YYYYMMDD-序号)
### CASE-20260710-02 · 坐席端/管理端登录"获取二维码失败"(Docker 镜像缺文件)⭐⭐
- **现象**:坐席端和管理端登录均报"获取二维码失败",浏览器控制台显示 `WebSocket connection failed` + `/api/auth/qrcode` 返回 404。
- **误判历程**
1. 误判为 Nginx 未 reload → `nginx -s reload``/auth_qrcode/create` 返回 200,但前端实际调用的是 `/auth/qrcode`(不同路由)
2. 尝试 `docker cp` 临时复制 `auth.py` 到容器 → 重启后文件丢失(临时文件系统)
3. 发现连锁依赖:`auth.py` 依赖 `schemas/auth.py``services/auth_service.py``models/login_log.py`,全部缺失
4. **真正根因**:服务器存在两份代码——`/opt/wecom-it-desk/app/`(较新,开发用)和 `/opt/wecom-it-desk/backend/app/`(较旧,Docker 构建用)。新增的 `auth.py` 只放到了 `app/`,未同步到 `backend/app/`,导致镜像构建时打包的是旧代码。
- **根因**:服务器上 `app/`(开发目录)与 `backend/app/`Docker 构建目录)不同步,`docker compose build` 打包了旧代码。同时 `requirements.txt` 也未同步(新增 `neo4j` 依赖),首次重建后容器因缺包 crash。
- **修复**
1. 用 Python 脚本同步 `/opt/wecom-it-desk/app/``/opt/wecom-it-desk/backend/app/`
2. 上传最新 `requirements.txt`(含 `neo4j>=5.26.0`)到 `/opt/wecom-it-desk/backend/`
3. `docker compose build backend`(约 65s
4. `docker compose up -d backend`
5. 验证:后端日志显示 `GET /auth/qrcode → 200 OK`,容器状态 healthy
- **⚠️ 教训**
1. **部署新功能时必须检查两份代码一致性**`app/``backend/app/` 不同步 = 镜像缺文件。
2. **`docker cp` 是临时方案**:容器重启后丢失。永久修复必须重建镜像。
3. **全链路检查**:代码同步 → requirements 更新 → 镜像重建 → 容器重启,四步缺一不可。
- **防护措施**:见 §1.4 部署前检查清单。
- **✅ 根因已消除**2026-07-10):方案 C(卷挂载)已上线。代码不再烘焙进 Docker 镜像,通过 `./app:/app/app` volume 挂载到容器。`backend/app/` 旧代码目录已删除,不再存在"两份代码不同步"问题。此案例保留作为历史参考。
### CASE-20260710-01 · 扫码登录成功页 JS 不执行(CSP 拦截内联脚本)⭐⭐
- **现象**:企微扫码登录成功后,"登录成功"页面显示但不会自动关闭。页面停在"JS加载中…",所有内联 `<script>` 从未执行。后端返回 HTTP 200,HTML 内容正确。
- **误判历程**(5 轮,供反思):
1. 误判为未引入企微 JS-SDK → 添加后仍不工作
2. 误判为 JSAPI 签名 URL 协议不一致(HTTP vs HTTPS)→ 修复 `X-Forwarded-Proto` 后仍不工作
3. 误判为外部 JS-SDK 加载失败 → 移除外部依赖改用 `WeixinJSBridge` 仍不工作
4. 误判为 `WeixinJSBridgeReady` 事件未触发 → 改为轮询检测仍不工作
5. **真正根因**Nginx CSP 头 `script-src 'self' 'unsafe-eval' https://res.wx.qq.com` 缺少 `'unsafe-inline'`,所有内联 `<script>` 被浏览器静默拦截。**JS 从未执行过**——前面 4 轮的代码修改全部白费。
- **根因**`Content-Security-Policy``script-src` 指令未包含 `'unsafe-inline'`,浏览器根据 CSP 规范静默拦截所有内联脚本(`<script>...</script>``onclick=` 等内联事件处理器)。页面 HTML 正常返回,但 JS 引擎从未解析执行任何脚本。
- **修复**Nginx 配置 CSP 头 `script-src` 加入 `'unsafe-inline'`
```
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://res.wx.qq.com; ..." always;
```
修改后 `docker restart wecom_it_nginx`(不能用 `reload`,见下方教训)。
- **验证**:用户手机企微扫码 → 显示"登录成功"→ 1 秒后自动关闭返回企微 ✅
- **⚠️ 教训**
1. **页面 200 + HTML 正确 ≠ JS 会执行**。排查前端功能异常时,Step 0 应检查 HTTP 响应头(CSP / X-Frame-Options),而非直接深入代码逻辑。
2. **CSP 拦截是静默的**——浏览器控制台报错但页面无任何错误提示,后端日志也完全正常。如果不主动检查响应头,会一直在代码层面打转。
3. **5 轮误判的共同盲点**:每次都在"JS 代码为什么不对"上做文章,从未怀疑"JS 根本没执行"。正确的排查路径应该是先在浏览器 F12 控制台输入 `1+1` 确认 JS 引擎可用,再看 Console 是否有 CSP 报错。
### CASE-20260707-01 · 管理后台登录"网络连接失败"(Redis 密码 URL 解析挂起)⭐
- **现象**:浏览器登录 `/itadmin/` 一直转圈 / "网络连接失败"API 永远不返回;curl 超时。
- **根因**`REDIS_URL=redis://:R3d!s@2026#Secure@redis:6379/0`,密码含 `@` 和 `#`。`urlparse()` 把 `#` 当 fragment、`@` 当 host 分隔符 → 解析出 host=`2026`、password=`R3d!s` → 连到不存在的 host → **无限挂起**。后端 `token_service.create_token()` 调 `redis.setex` 时卡死。
@@ -220,6 +329,7 @@ docker logs wecom_it_backend | grep -i websocket
| 5 | 检查容器状态:`docker ps --filter name=wecom_it_nginx` |
| 6 | 用 `agent-browser` 打开 URL 做端到端验证 |
| 7 | 避免用 sed 修改 nginx 配置——`$uri`/`$host` 等变量会被 shell 解释;用 Python 脚本或直接上传文件 |
| 8 | ⚠️ **`sed -i` 会创建新 inode**Docker bind mount 不跟踪新 inode,导致容器内看到的仍是旧文件。用 `sed -i` 修改后必须 `docker restart`(而非 `nginx -s reload`),或改用 `sed` 不带 `-i` + 重定向写入原文件(保持 inode 不变) |
---
@@ -0,0 +1,385 @@
# 智能IT服务系统运维手册
> **版本**: v1.0 | **日期**: 2026-07-04 | **维护人**: 助理(小米)
> **目标读者**: 运维工程师 / IT支持组
> **📖 关联文档**:
> - [README.md](../README.md) — 项目快速入门
> - [01-项目总览与部署手册](./01-项目总览与部署手册-20260704.md) — 完整架构设计
> - [CHANGELOG.md](../CHANGELOG.md) — 版本变更概览
> - [docs/archive/](./archive/) — 历史版本详情
---
## 目录
1. [系统概述](#一系统概述)
2. [环境信息](#二环境信息)
3. [部署操作](#三部署操作)
4. [日常运维](#四日常运维)
5. [故障排查](#五故障排查)
6. [回滚方案](#六回滚方案)
7. [备份恢复](#七备份恢复)
8. [应急响应](#八应急响应)
---
## 一、系统概述
### 1.1 系统架构
```
浏览器 ──→ itsupport.servyou.com.cn:443
┌─── nginx (容器) ───────────────┐
│ │
│ /itdesk/* → H5 员工端 SPA │
│ /itagent/* → 坐席工作台 SPA │
│ /itadmin/* → 管理后台 SPA │
│ /itportal/* → Portal 选择页 │
│ /api/* → backend:8000 │
│ /ws/* → backend:8000 (WS)│
│ │
└──────────────┬───────────────────┘
│ 本机 Docker 网络
┌─────────────┼─────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ backend │ │ postgres │ │ redis │
│ :8000 │ │ :5432 │ │ :6379 │
└──────────┘ └──────────┘ └──────────┘
```
### 1.2 组件清单
| 服务 | 镜像 | 端口 | 说明 |
|------|------|------|------|
| nginx | nginx:alpine | 443→80 (对外) | 反向代理 + SSL |
| backend | 自构建 | 8000 (内部) | FastAPI 后端 |
| postgres | postgres:16 | 5432 (内部) | 数据库 |
| redis | redis:7 | 6379 (内部) | 缓存 + Session |
### 1.3 访问端点
| 端点 | 说明 |
|------|------|
| `https://itsupport.servyou.com.cn/itdesk/` | H5 员工端 |
| `https://itsupport.servyou.com.cn/itagent/` | 坐席工作台 |
| `https://itsupport.servyou.com.cn/itadmin/` | 管理后台 |
| `https://itsupport.servyou.com.cn/itportal/` | Portal 角色选择 |
| `https://itsupport.servyou.com.cn/api/docs` | API Swagger 文档 |
---
## 二、环境信息
### 2.1 服务器信息
| 环境 | IP | 域名 | 用途 |
|------|-----|------|------|
| 生产 | 10.90.5.110 (内网) | itsupport.servyou.com.cn | 正式环境 |
| 运维入口 | 10.212.189.210:2222 | - | 堡垒机 SSH |
### 2.2 关键配置
| 配置项 | 值 |
|--------|-----|
| 企微 CorpID | `ww...` (见 .env) |
| 企微 AgentID | `1000xxx` |
| 数据库 | PostgreSQL 16 |
| 缓存 | Redis 7 |
| 域名证书 | `*.servyou.com.cn` (GeoTrust/DigiCert) |
### 2.3 部署路径
```
/opt/wecom-it-desk/
├── docker-compose.yml
├── .env # 环境变量(不提交 Git)
├── backend/ # 后端代码
├── frontend-h5/dist/ # H5 前端构建产物
├── frontend-agent/dist/ # 坐席前端构建产物
├── frontend-admin/dist/ # 管理后台构建产物
├── frontend-portal/dist/ # Portal 构建产物
├── nginx/ # Nginx 配置
└── logs/ # 日志目录
```
---
## 三、部署操作
### 3.1 部署流程概览
```
1. 打包代码 → 2. 上传服务器 → 3. 配置环境变量 → 4. 启动容器 → 5. 验证
```
### 3.2 打包命令(本地)
```bash
# 在项目根目录执行
cd D:\资\03-项目开发\wecom_it_smart_desk
# 使用部署脚本打包
powershell -File deploy-server\build-package.ps1
# 或手动打包
tar czf deploy.tar.gz \
backend/ frontend-h5/dist/ frontend-agent/dist/ \
frontend-admin/dist/ frontend-portal/dist/ \
nginx/ docker-compose.yml .env.production scripts/
```
### 3.3 上传到服务器
> **注意**: 公司服务器只能通过堡垒机上传,无法直接从本地 scp
1. **通过堡垒机上传到 `/tmp/`**
- 使用 SFTP 或 Web 界面上传到堡垒机
2. **SSH 登录服务器**
```bash
# 堡垒机: sxn@10.212.189.210:2222
ssh sxn@10.90.5.110 # 跳转目标服务器
```
3. **移动到目标目录**
```bash
mv /tmp/deploy.tar.gz /opt/wecom-it-desk/
cd /opt/wecom-it-desk/
tar xzf deploy.tar.gz
```
### 3.4 配置环境变量
```bash
# 创建环境配置
cp .env.production .env
vim .env # 编辑真实配置
# 必填项:
# - WECOM_CORP_ID
# - WECOM_AGENT_ID
# - WECOM_SECRET
# - WECOM_TOKEN
# - WECOM_ENCODING_AES_KEY
# - POSTGRES_PASSWORD
# - REDIS_PASSWORD
```
### 3.5 启动服务
```bash
# 启动所有容器
docker compose up -d --build
# 或分步启动
docker compose up -d postgres redis # 先启动基础服务
docker compose up -d backend # 再启动后端
docker compose up -d nginx # 最后启动前端
```
### 3.6 验证部署
```bash
# 1. 检查容器状态
docker compose ps
# 预期:4 个容器全部 Up/healthy
# 2. 健康检查
curl -ksI https://itsupport.servyou.com.cn/api/health
# 3. 各端点验证
curl -ksI https://itsupport.servyou.com.cn/itdesk/
curl -ksI https://itsupport.servyou.com.cn/itagent/
curl -ksI https://itsupport.servyou.com.cn/itadmin/
curl -ksI https://itsupport.servyou.com.cn/itportal/
```
---
## 四、日常运维
### 4.1 日常检查
```bash
# 每日必做检查
docker compose ps # 容器状态
docker compose logs --tail=50 backend # 后端日志
docker compose logs --tail=50 nginx # 前端日志
df -h # 磁盘空间
```
### 4.2 常用操作
| 操作 | 命令 |
|------|------|
| 重启后端 | `docker compose restart backend` |
| 重启 nginx | `docker compose restart nginx` |
| 查看实时日志 | `docker compose logs -f backend` |
| 进入后端容器 | `docker compose exec backend bash` |
| 查看容器资源 | `docker stats` |
### 4.3 监控指标
| 指标 | 阈值 | 说明 |
|------|------|------|
| CPU 使用率 | < 80% | 主机层面 |
| 内存使用率 | < 80% | 主机层面 |
| 磁盘使用率 | < 70% | 主机层面 |
| 容器状态 | 全部 Up | docker compose ps |
| API 响应时间 | P95 < 500ms | 业务层面 |
### 4.4 日志位置
| 服务 | 日志命令 |
|------|----------|
| 后端 | `docker compose logs backend` |
| Nginx | `docker compose logs nginx` |
| PostgreSQL | `docker compose logs postgres` |
| Redis | `docker compose logs redis` |
---
## 五、故障排查
> **本章已整合至标准故障排查手册**:[09-部署运维/00-标准故障排查手册.md](../09-部署运维/00-标准故障排查手册.md)
>
> 手册涵盖:三步隔离法、错误码速查(500/502/503/403/422/网络挂起)、诊断脚本与命令、案例库(含 Redis urlparse 挂起、502、各类修复记录)、端到端验证完成标准(含"宣布修复前必须提供真实浏览器截图"硬规则)。
>
> **日常排故请直接打开该手册**,本文档不再重复故障排查细节。
---
## 六、回滚方案
### 6.1 快速回滚
```bash
# 停止当前版本
docker compose down
# 恢复上一个版本(需提前备份)
# 方法1: 从 Git 拉取上一个 commit
git checkout {上一个commit-hash}
# 重新构建部署
# 方法2: 保留上一个版本的部署包
cd /opt/wecom-it-desk-backup
tar xzf deploy-v0.x.x.tar.gz
docker compose up -d
```
### 6.2 回滚检查清单
- [ ] 确认上一个版本可用
- [ ] 通知相关人员
- [ ] 记录当前版本问题
- [ ] 执行回滚
- [ ] 验证回滚后功能正常
- [ ] 发送回滚通知
---
## 七、备份恢复
### 7.1 备份策略
| 备份对象 | 方法 | 频率 | 保留 |
|---------|------|------|------|
| PostgreSQL | pg_dump | 每日凌晨 | 7 天 |
| Redis | redis-cli SAVE | 每日凌晨 | 7 天 |
| 配置文件 | tar 归档 | 每次部署 | 4 个版本 |
| 日志文件 | logrotate | 每周 | 4 周 |
### 7.2 备份命令
```bash
# 备份数据库
docker compose exec postgres pg_dump -U postgres wecom_it > /tmp/backup_$(date +%Y%m%d).sql
# 备份 Redis
docker compose exec redis redis-cli SAVE
cp /var/lib/docker/volumes/wecom-it-desk_redis_data/_data/dump.rdb /tmp/redis_$(date +%Y%m%d).rdb
# 备份配置
tar czf /tmp/config_$(date +%Y%m%d).tar.gz /opt/wecom-it-desk/.env /opt/wecom-it-desk/nginx/
```
### 7.3 恢复命令
```bash
# 恢复数据库
docker compose exec -T postgres psql -U postgres wecom_it < backup_20260701.sql
# 恢复 Redis
docker compose exec -T redis redis-cli FLUSHALL
# 停止服务后复制 dump.rdb 到数据目录
```
---
## 八、应急响应
### 8.1 事件分级
| 等级 | 场景 | 响应时间 |
|------|------|----------|
| 🔴 P0 | 鉴权漏洞 / 数据泄露 / 服务全停 | 5 min |
| 🟠 P1 | 功能故障 / 单服务降级 | 30 min |
| 🟡 P2 | 性能问题 / UI 异常 | 4 h |
| 🟢 P3 | 体验优化 | 1 周 |
### 8.2 P0 应急流程
#### 立即止血
```bash
# 1. 关闭外网访问
sudo iptables -A INPUT -p tcp --dport 443 -j DROP
# 2. 停可疑服务
docker compose stop backend
# 3. 保留现场(不删文件)
docker compose logs backend > /tmp/incident-backend.log
docker compose logs nginx > /tmp/incident-nginx.log
```
#### 通知
- 微信/电话通知项目负责人
- 邮件通知:`wecom-it-desk-incident@servyou-it.com`
### 8.3 应急联系
| 角色 | 联系人 |
|------|--------|
| 项目负责人 | 宋献 |
| 运维 | IT 支持组 |
| 企微技术支持 | 企微客服 |
---
## 附录
### 版本历史
| 版本 | 日期 | 更新内容 |
|------|------|----------|
| v1.0 | 2026-07-04 | 初始版本,整合部署/运维/故障排查文档 |
### 相关文档
| 文档 | 说明 |
|------|------|
| `docs/01-项目总览/01-项目总览与部署手册-20260704.md` | 完整项目背景与架构设计 |
| `docs/RELEASE_NOTES_v0.7.1.md` | 版本发布说明 |
| `docs/SOPs/SOP-004-应急响应.md` | 详细应急响应流程 |
| `09-部署运维/00-标准故障排查手册.md` | 标准故障排查手册(故障排查唯一入口)|
---
> **维护说明**: 本文档由助理(小米)维护,随每次发布更新。
> 如有更新,请同步更新本文档的版本号和日期。
@@ -0,0 +1,731 @@
# 企微智能IT支持服务台 — 项目总览与部署手册
> **版本**: v2.2 | **日期**: 2026-07-04 | **编制**: 宋献(IT支持组组长)
> **目标读者**: **管理者 / 架构师 / 运维** — 了解项目全貌、架构决策、部署与运维操作
> **📖 与 README.md 的关系**: 本文是 [README.md](../README.md) 的**详细版本**,侧重完整的架构设计和部署运维。README 适合新人快速入门,本文适合深入了解。
> **⚠️ 运维手册更新**: 部署与运维操作已整合到独立文档 [智能IT服务系统运维手册](./智能IT服务系统运维手册.md),该文档由助理(小米)维护,随每次发布更新。本文档保留架构设计与背景信息。
---
## 目录
1. [项目概述](#一项目概述)
2. [系统架构](#二系统架构)
3. [三步演进路径](#三三步演进路径)
4. [现有系统复用评估](#四现有系统复用评估)
5. [正式环境部署方案](#五正式环境部署方案)
6. [部署操作手册](#六部署操作手册)
7. [运维管理](#七运维管理)
8. [开发交付状态](#八开发交付状态)
9. [附录](#九附录)
---
## 一、项目概述
### 1.1 背景与痛点
公司约 **6000 人**,全国设分子机构,使用企业微信作为内部 IM。当前 IT 服务存在三大痛点:
| 痛点 | 现状 | 影响 |
|------|------|------|
| 员工绕过 AI 直接找人工 | 可通过关键词直通人工坐席,首次后永久记忆 | AI 筛选率极低,人工成本高 |
| AI 转人工需另开窗口 | 跳转到企微"员工服务"模块,与 AI 对话割裂 | 体验差,员工困惑 |
| 无法跨主体共享 | 企微"员工服务"不支持互联企业应用共享 | 跨企业服务不可达 |
### 1.2 核心方案
**自研 IT 服务坐席系统**,替代企微内置的"员工服务"模块:
- 基于企微自建应用消息 API,所有消息由自己的服务器接管
- 分三步渐进式构建:M1 消息接管 → M2 AI 接入 → M3 知识库闭环
- 当前处于 **M1(消息接管 + 极简坐席)开发完成,部署配置中**
### 1.3 核心设计理念
传统"串行排队"改为**"并行协作"**——AI 全程在线,人工随时介入:
| 角色 | 工作方式 |
|------|---------|
| AI | 全程在线,所有对话可见 |
| 坐席 | 随时介入,AI 始终在旁辅助 |
| 员工 | 同一窗口,AI 和人工无缝切换 |
---
## 二、系统架构
### 2.1 部署架构总览(预生产环境)
> **当前阶段**:预生产环境。智能咨询系统与 IT 数据查询平台**分别部署在不同主机**,通过 Nginx 路径路由共用域名 `it-dataquery.dc.servyou-it.com`。正式环境将迁移到 K8s 集群。
```
浏览器 ──→ it-dataquery.dc.servyou-it.com:80
┌─── nginx (本系统主机) ───────────────┐
│ │
│ /itdesk/* → H5 员工端 SPA │
│ /itagent/* → 坐席工作台 SPA │
│ /api/* → backend:8000 (FastAPI) │
│ /ws/* → backend:8000 (WS) │
│ /* → 数据平台主机(远程IP) │ ← 跨主机代理
│ │
└──────────────┬───────────────────────┘
│ 本机 Docker 网络
┌─────────────┼─────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ backend │ │ postgres │ │ redis │
│ :8000 │ │ :5432 │ │ :6379 │
└──────────┘ └──────────┘ └──────────┘
```
| 对比项 | 预生产(当前) | 正式环境(未来) |
|--------|-------------|---------------|
| 部署方式 | Docker Compose(单主机) | K8s 集群(高可用) |
| 与数据平台关系 | 不同主机,Nginx 远程代理 | 独立 K8s 集群 |
| 域名 | 共用 `it-dataquery.dc.servyou-it.com` | 独立域名或 K8s Ingress |
### 2.2 技术栈
| 层级 | 技术选型 | 说明 |
|------|---------|------|
| 反向代理 | Nginx | 统一入口、路径路由、WebSocket 代理 |
| 后端框架 | FastAPI (Python 3.12) | 异步、自动 OpenAPI 文档、类型安全 |
| 数据库 | PostgreSQL 16 | 会话/消息/坐席/配置 持久化(9 张表) |
| 缓存 | Redis 7 | access_token 缓存(TTL 7200s)、JWT 会话 |
| ORM | SQLAlchemy 2.0 (async) | 异步 session、声明式模型 |
| 数据库迁移 | Alembic | 所有表结构变更通过迁移脚本管理 |
| 坐席前端 | Vue3 + ElementPlus + Pinia | 企业级组件库,三栏工作台 |
| 员工 H5 | Vue3 + Vant4 + Pinia | 移动端组件库,企微 WebView 兼容 |
| 容器化 | Docker + Docker Compose | 4 容器一键启停 |
### 2.3 数据库核心表(9 张)
| 表名 | 用途 | 关键字段 |
|------|------|---------|
| `conversations` | 会话主表 | employee_id, status, urgency_score(1-5), tags(JSON), is_vip, participants(JSON) |
| `messages` | 消息记录 | sender_type(employee/agent/ai/system), content, msg_type |
| `agents` | 坐席信息 | user_id, status(online/offline/busy), current_load |
| `quick_reply_templates` | 快速回复模板 | category, title, content(支持 {变量}) |
| `system_configs` | 系统配置 | config_key, config_value(关键词/阈值/话术等) |
| `funny_phrases` | 趣味话术 | scene(6 种场景), content, tone, is_active |
| `approval_links` | 审批流程链接 | category(IT/HR/行政/财务), title, url |
| `software_downloads` | 软件下载入口 | category, name, version, platform, download_url |
| `agent_notes` | 坐席备注 | conversation_id, agent_id, content |
### 2.4 API 接口分组
| 分组 | 路径前缀 | 核心接口 |
|------|---------|---------|
| 企微回调 | `/api/wecom/callback` | GET 验证 URL、POST 接收消息 |
| 会话管理 | `/api/conversations` | 列表/详情/状态/置顶/代办/接单/邀请/退出/移除参与者 |
| 消息管理 | `/api/conversations/{id}/messages` | 消息列表/发送 |
| 坐席管理 | `/api/agents` | 列表/登录/状态切换 |
| H5 用户端 | `/api/h5/*` | 会话/摇人/审批链接/软件下载/OAuth |
| WebSocket | `/ws/{agent_id}` | 实时推送(坐席端) |
统一响应格式:`{ "code": 0, "data": {}, "message": "success" }`
### 2.5 消息收发全链路
```
员工发消息 → 企微回调解密 → 消息路由 → 评分标记 → 入库 → 坐席 WS 推送 → 坐席回复 → 企微主动推送 → 员工同一窗口收到
```
**坐席端通信**:已升级为 WebSocket 实时推送(2026-06-03),替代原计划的短轮询:
- 心跳保活:前端每 30s 发 ping,后端回 pong
- 断线重连:指数退避(1s→2s→4s→...→30s 上限)
- 降级策略:WS 断连时自动降级为 3s 轮询
### 2.6 会话排序与评分规则
**排序**: 紧急 → 举手 → 需介入 → 活跃 → AI处理中 → 已结单(同级按时间倒序)
**紧急度评分**: `基础分(关键词) + 情绪加成 + VIP加成 + 重复追问加成`,范围 1-5
**标记系统**:
| 标记 | 图标 | 触发条件 |
|------|------|---------|
| VIP | 红色 | 企微通讯录规则匹配 |
| 举手 | 黄色 | 员工说关键词或点击摇人按钮 |
| 需介入 | 橙红 | 同一问题追问 >3 轮 |
| 情绪 | 红色 | 关键词匹配(急/崩溃/投诉等) |
---
## 三、三步演进路径
| 里程碑 | 周期 | 核心交付 | 状态 |
|--------|------|---------|------|
| **M1** 消息接管 + 极简坐席 | 6-8 周 | 企微 API 链路验证 · 坐席三栏工作台 · 员工 H5 双栏 · 邀请功能(多人会话协作) | ✅ 代码完成,部署中 |
| **M2** AI 机器人接入 | M1 后 4-6 周 | 千问/Dify/RAGFlow 接入 · AI 前置筛选 · 排队系统 | 📋 计划中 |
| **M3** 知识库闭环迭代 | M2 后 4-6 周 | 坐席标注系统 · 千问自动分析 · 知识库自优化 | 📋 计划中 |
### M1 当前进度(2026-06-03
| 模块 | 状态 |
|------|------|
| PRD + 架构设计 | ✅ 完成 |
| 后端代码(45+ 文件,7 API 组) | ✅ 完成 |
| 坐席前端(三栏工作台 + WebSocket | ✅ 完成 |
| 员工 H5(双栏 + 摇人按钮 + 呼叫坐席) | ✅ 完成 |
| 邀请功能(多人会话协作 — PRD §21) | 📋 计划中(M1 范围) |
| 前端功能联调验证 | ✅ 完成(2026-06-03 |
| 测试用例(116 条 pytest | ✅ 完成 |
| Alembic 数据库迁移 | ✅ 完成 |
| 前端构建产物(dist/) | ✅ 完成 |
| 远程服务器部署 | 🔧 待 SSH 账号 |
### M2 核心改动
只改路由层逻辑,其余不动:
```
M1: 新会话 → 坐席队列
M2: 新会话 → AI 先回答 → AI判断/用户触发 → 坐席队列
```
新增:千问对话模型、RAGFlow 知识库检索、Dify 编排平台、排队系统。目标:AI 首答率 ≥ 80%。
### M3 知识库迭代闭环
```
坐席日常标注 ──→ 千问分析 ──→ 自动处理 ──→ 知识库增强
(正确/错误) (缺文档/过时) │
↑ │
└──────────── 持续循环 ─────────────────────┘
```
---
## 四、现有系统复用评估
### 4.1 核心复用(直接影响新系统架构)
| # | 资源 | 复用方式 | 新系统对应 |
|---|------|---------|-----------|
| 1 | Dify Workflow | 直接复用,M2 阶段接入 AI 回复 | 坐席助手 AI 面板 + 自动回复 |
| 2 | dify2openai 桥接 | 直接复用 API | 后端调用 AI 的入口 |
| 3 | RAGFlow 知识库 | 直接复用,M3 阶段混合标注迭代 | 知识库管理 + 标注闭环 |
| 4 | Qwen3-30B 大模型 | 直接复用 | AI 对话底层模型 |
| 5 | bge-m3 向量模型 | RAGFlow 内置,直接复用 | 知识库检索向量化 |
| 6 | Dify 数据库(只读) | 读取 messages 表,同步历史数据 | 历史会话数据迁移 + 统计 |
| 7 | 企微自建应用 | 直接复用应用凭证 | 消息收发的企微入口 |
### 4.2 基础设施复用(零耦合)
| 资源 | 复用方式 | 耦合度 |
|------|---------|--------|
| 企微自建应用凭证 | 配置文件引用(只读) | 零耦合 |
| Dify Workflow API | HTTP 调用 | 外部依赖 |
| RAGFlow 知识库 | HTTP 调用 | 外部依赖 |
| Qwen3-30B 大模型 | HTTP 调用 | 外部依赖 |
| SSL 证书文件 | Nginx 挂载只读 | 零耦合 |
### 4.3 关键结论
> 代码层面复用率约 15%(主要是业务逻辑和 SQL 查询),基础设施和 AI 能力复用率约 70%。
> 新系统用 **FastAPI + SQLAlchemy 2.0**,不沿用旧 Django 代码。底层业务逻辑可参考移植。
---
## 五、正式环境部署方案
### 5.1 核心决策原则
基于四个约束条件:
| # | 约束 | 推导原则 |
|---|------|---------|
| 1 | 对现有正式环境架构影响最小 | **物理隔离 > 逻辑隔离** |
| 2 | 避免变更影响现有服务 | **独立 Nginx 入口** |
| 3 | 减少服务依赖 | **最小化外部依赖** |
| 4 | 避免责任不清 | **独立数据库 + 独立 Redis** |
> **一句话**:新系统作为**独立服务单元**部署,与现有智能 IT 数据平台(Django)在物理资源层面完全解耦,仅通过 HTTP API 调用共享 AI 能力。
### 5.2 关键隔离策略
| 隔离层面 | 方案 | 效果 |
|---------|------|------|
| 服务器级 | 独立 VM,不共用宿主机 | 挂了不影响旧系统 |
| 网络级 | Docker 内部网络,PG/Redis 不暴露宿主机端口 | 外部无法直连数据库 |
| 存储级 | 独立命名卷,不共用 Volume | 数据完全隔离 |
| 域名级 | 路径路由(共用域名) + 独立 Nginx 容器 | `/itdesk/``/itagent/``/api/` 归属本系统 |
| 认证级 | JWT + 独立 Redis | 账户体系独立 |
| 依赖级 | 仅 HTTP 调用外部 AI 服务 | 外部服务故障只影响 M2 功能 |
### 5.3 与现有系统的解耦修正
原复用评估中的部分共享方案已修正为独立部署:
| 原建议 | 修正方案 | 理由 |
|--------|---------|------|
| 同机部署于 10.80.0.86 | 独立服务器/VM(或同机端口分离+独立 compose) | 避免端口冲突、资源争抢 |
| Redis 复用同实例 | 独立 Redis 容器 | FLUSHDB 误操作、内存 OOM 互相影响 |
| 使用旧系统 Nginx | 独立 Nginx 容器 | 变更反代配置不影响旧系统路由 |
| 复用旧 PG 实例 | 独立 PostgreSQL 容器 | 数据库是责任边界核心 |
### 5.4 Docker Compose 服务清单
| 服务 | 镜像 | 端口 | 健康检查 |
|------|------|------|---------|
| postgres | postgres:16 | 5432(内部) | pg_isready |
| redis | redis:7 | 6379(内部) | redis-cli ping |
| backend | 自构建 Dockerfile | 8000(内部) | GET /health |
| nginx | nginx:alpine | 18080:80(对外) | GET /health |
Docker 网络:`it-desk-internal`(内部,连接 backend/postgres/redis
### 5.5 资源需求
| 资源 | 配置 | 说明 |
|------|------|------|
| 服务器 | 4C8G + 100GB SSD(最低)/ 8C16G + 200GB SSD(推荐) | Docker Engine 环境 |
| 域名 | `it-dataquery.dc.servyou-it.com`(已就绪,共用) | 路径路由 `/itdesk/` `/itagent/` `/api/` |
| 企微自建应用 | 1 个(已创建) | CorpID/AgentID/Secret/Token/EncodingAESKey |
| 防火墙 | 办公网→服务器:80/443, 企微→服务器:443 | 出站: 企微 API/ Dify/ RAGFlow/ Qwen |
### 5.6 风险矩阵
| 风险 | 概率 | 影响 | 缓解措施 |
|------|------|------|---------|
| 新服务器申请被拒/延迟 | 中 | 部署延期 | 退化方案:旧服务器端口分离+独立 compose |
| SSL 证书到期 | 低 | HTTPS 不可用 | 复用现有通配符证书 |
| 企微应用配置变更 | 低 | 双系统消息中断 | 建立变更通知机制 |
| Dify/RAGFlow 不可用 | 中 | M2 AI 功能不可用 | 降级:纯坐席模式仍正常工作 |
| Docker 宿主机故障 | 低 | 新系统全宕 | Compose 配置即代码,重建快 |
---
## 六、部署操作手册
> **预生产部署**:本系统与数据平台部署在**不同主机**,通过 Nginx 路径路由共用域名。数据平台请求通过远程 IP 反代(非 Docker 网络)。正式环境将迁移到 K8s。
### 6.1 前置条件
- 服务器已安装 Docker Engine 24+ + Docker Compose v2
- IT 数据查询平台已部署运行
- 有 SSH 登录权限
### 6.2 配置数据平台反代地址
预生产环境中,数据平台部署在**独立主机**。部署前需修改 `nginx/nginx.conf` 中的数据平台上游地址:
```nginx
# nginx/nginx.conf — 将 DATAQUERY_HOST 替换为数据平台主机的实际 IP
upstream dataquery {
server 10.80.0.86:80; # ← 替换为数据平台实际 IP:端口
}
```
> **为什么不创建 Docker 共享网络?** 预生产两台主机不在同一 Docker Engine,无法使用 `docker network create` 互联。正式环境迁移 K8s 后由 Ingress/Service 处理路由。
### 6.3 上传部署包
在本地(Windows)执行打包上传:
```bash
# 方式 A:使用 deploy.sh 打包
bash scripts/deploy.sh --pack
scp it-smart-desk-*.tar.gz user@server:/opt/
# 方式 B:手动打包
tar czf deploy.tar.gz \
backend/ frontend-h5/dist/ frontend-agent/dist/ \
nginx/ docker-compose.yml .env.production scripts/
scp deploy.tar.gz user@server:/opt/it-smart-desk/
```
### 6.4 服务器配置与启动
```bash
ssh user@server
cd /opt/it-smart-desk
tar xzf it-smart-desk-*.tar.gz
# 创建环境配置
cp .env.production .env
vim .env # 填入真实企微凭证
```
`.env` 必填项:
| 配置项 | 说明 | 获取位置 |
|--------|------|---------|
| `WECOM_CORP_ID` | 企业 ID | 企微管理后台 > 我的企业 |
| `WECOM_AGENT_ID` | 应用 AgentId | 企微管理后台 > 应用管理 |
| `WECOM_SECRET` | 应用 Secret | 企微管理后台 > 应用管理 |
| `WECOM_TOKEN` | 回调 Token | 企微管理后台 > 接收消息 |
| `WECOM_ENCODING_AES_KEY` | 回调 AES 密钥 | 企微管理后台 > 接收消息 |
| `POSTGRES_PASSWORD` | 数据库密码 | 自定义强密码 |
| `CORS_ORIGINS` | `http://it-dataquery.dc.servyou-it.com` | CORS 白名单 |
启动:
```bash
bash scripts/deploy.sh
# 自动执行:检查前置条件 → 构建后端镜像 → 启动所有容器 → 运行数据库迁移
```
### 6.5 验证部署
```bash
# 检查容器状态
docker compose ps
# 预期:4 个容器全部 Up/healthy
# 健康检查
curl http://localhost:18080/api/health
# 浏览器验证
# http://it-dataquery.dc.servyou-it.com/itdesk/ → H5 员工咨询页面
# http://it-dataquery.dc.servyou-it.com/itagent/ → 坐席工作台登录页
# http://it-dataquery.dc.servyou-it.com/ → IT 数据查询平台(不变)
# http://it-dataquery.dc.servyou-it.com/api/docs → FastAPI Swagger 文档
```
### 6.6 常见问题
**nginx 启动失败,报 `host not found in upstream "dataquery"`**
`nginx/nginx.conf``DATAQUERY_HOST` 未替换为数据平台的实际 IP。确保已在部署前完成替换。
**nginx 启动但数据平台页面 502**
→ 本系统主机无法访问数据平台主机 IP。检查防火墙策略是否放行两台主机间的 80 端口。
**访问 `/itdesk/` 返回 404**
→ 检查前端 dist 是否正确挂载:`docker exec wecom_it_nginx ls -la /usr/share/nginx/html/itdesk/`
**API 返回 CORS 错误**
→ 检查 `.env``CORS_ORIGINS` 是否包含 `http://it-dataquery.dc.servyou-it.com`
**数据库迁移失败**
→ PostgreSQL 可能未就绪,等 30 秒后执行:`docker compose restart backend`
### 6.7 更新部署
```bash
# 仅更新前端
bash scripts/deploy.sh --build
docker compose restart nginx
# 仅更新后端
docker compose build backend
docker compose up -d backend
# 全量更新
bash scripts/deploy.sh --down
bash scripts/deploy.sh
```
### 6.8 回滚
```bash
docker compose down # 停止新系统所有容器
# 旧系统不受任何影响(独立资源)
```
---
## 七、运维管理
### 7.1 责任矩阵
| 运维操作 | 影响范围 | 备注 |
|---------|---------|------|
| 重启 PostgreSQL | 仅新系统 | 独立实例 |
| 重启 Redis | 仅新系统 | 独立实例 |
| 修改 Nginx 配置 | 仅新系统路由 | 独立容器 |
| 更新后端/前端代码 | 仅新系统 | 独立容器 |
| 企微应用配置变更 | **双系统** | ⚠️ 唯一共享点,需通知双方 |
### 7.2 监控指标
```yaml
主机层面:
- CPU 使用率 < 80%
- 内存使用率 < 80%
- 磁盘使用率 < 70%
容器层面:
- docker compose ps 全部 "Up" 状态
- Nginx 健康检查: GET /health → 200
- Backend 健康检查: GET /health → 200
业务层面(后续接入):
- 企微消息回调成功率 > 99%
- API 响应时间 P95 < 500ms
```
### 7.3 备份策略
| 备份对象 | 方法 | 频率 | 保留 |
|---------|------|------|------|
| PostgreSQL 数据 | `pg_dump` + 卷快照 | 每日凌晨 | 7 天 |
| Redis 数据 | `SAVE` + 复制 dump.rdb | 每日凌晨 | 7 天 |
| Docker 卷 | `tar czf` 归档 | 每周 | 4 周 |
### 7.4 关键对接参数(M2 阶段)
| 参数 | 值 | 用途 |
|------|-----|------|
| dify2openai API | `http://yw-dify.dc.servyou-it.com/dify2openai/v1/chat/completions` | AI 对话 |
| RAGFlow | `http://10.80.0.85:8080` | 知识库管理 |
| Qwen3-30B | `http://10.80.0.49:5000/api/llm/servyou/v1/chat/completions` | 大模型 |
| Dify DB(生产只读) | `10.80.128.40:5432` DB=dify User=difyro | 历史数据同步 |
| 数据平台 | `http://it-dataquery.dc.servyou-it.com` (10.80.0.86) | 部署服务器 |
### 7.5 应急预案可选技术项
> **评估日期**: 2026-06-03 | **来源**: 企微原生1对1方案(PRD §3.2 方式五)可行性评估
#### 7.5.1 备用方案概述
当 H5 WebView 方案(当前主方案)出现以下情况时,可切换至**企微原生1对1方案**作为降级/备用:
| 应急场景 | 当前方案症状 | 备用方案动作 |
|---------|------------|------------|
| H5 前端服务不可用 | Nginx 静态文件丢失/构建产物损坏 | 员工直接在企微与应用1对1聊天,走 `/message/send` 回复 |
| H5 页面性能问题 | WebView 加载慢/白屏/兼容性问题 | 放弃 H5 入口,改用企微原生聊天窗口交互 |
| OAuth2 鉴权异常 | 静默授权失败,H5 无法获取员工身份 | 原生方案无需 OAuth2,回调自带 UserID |
| 跨平台接入需求 | 需接入钉钉/飞书/浏览器用户 | **不适合切换**——原生方案无法跨平台,此时应修复 H5 |
| 外部专家协作 | 坐席需要拉入第三方专家协助 | 启用 `/appchat/create` 创建临时群聊 |
#### 7.5.2 备用方案技术架构
```
┌─────────────────────────────────────────────────────┐
│ 员工端(企微原生1对1聊天窗口) │
│ │
│ 员工 ←─消息─→ 自建应用(IT智能助手) │
│ │ │
│ ├─ AI回复 → /message/send → 同一窗口 │
│ ├─ 坐席回复 → /message/send → 同一窗口 │
│ └─ 外援 → /appchat/create → 新群聊窗口 │
│ │
│ 坐席工作台(保留,不变) │
│ ├─ WebSocket 接收员工消息 │
│ ├─ 坐席回复 → 后端 → /message/send → 员工窗口 │
│ └─ 外援指令 → 后端 → /appchat/create → 新群聊 │
│ │
└─────────────────────────────────────────────────────┘
```
**核心能力**:项目**已经具备**备用方案所需的全部后端代码:
- `wecom_callback.py`:接收企微回调 ✅
- `message_router._try_ai_reply()``wecom_service.send_text_message()`AI回复走 `/message/send`
- `scoring_service.detect_hand_raise()`:关键词举手检测 ✅
- `wecom_service.send_text_message()`:应用消息推送 ✅
**仅需新增**
- 交互卡片消息发送(`msgtype="template_card"`)— 用于"转人工"按钮、满意度评分
- AppChat API 封装(`/cgi-bin/appchat/*`)— 用于外援群聊场景
- AI/人工身份区分前缀(如 `🤖 AI回复:` / `👨‍💻 人工坐席(张三):`
#### 7.5.3 切换流程
**从 H5 方案切换到原生1对1方案**
| 步骤 | 操作 | 负责人 | 预计耗时 |
|------|------|--------|---------|
| 1 | 确认企微回调 URL 已配置且可达(H5 方案已配置则无需改动) | 运维 | 0 min |
| 2 | 确认 `message_router._try_ai_reply()``/message/send`(已实现) | 开发 | 0 min |
| 3 | 通知员工:直接在企微与应用聊天即可,不再进入 H5 | 运维 | 5 min |
| 4 | (可选)关闭 H5 入口:Nginx 配置注释 `/itdesk/` 路由 | 运维 | 2 min |
| 5 | (可选)启用交互卡片:部署 template_card 消息发送代码 | 开发 | 1-2 天 |
> **关键点**:步骤1-3 **零代码改动**即可完成基本切换,因为核心回调+消息推送链路已在运行。
**从原生1对1方案切回 H5 方案**
| 步骤 | 操作 | 负责人 |
|------|------|--------|
| 1 | 恢复 Nginx `/itdesk/` 路由(如已注释) | 运维 |
| 2 | 确认 H5 构建产物存在且可访问 | 运维 |
| 3 | 通知员工:点击应用 → 进入 H5 咨询页面 | 运维 |
#### 7.5.4 企微 API 限制与容量评估
| API | 限制 | 当前业务量(月均 188 次 AI 会话/天) | 风险 |
|-----|------|--------------------------------|------|
| `/message/send` | ≤账号上限×200人次/天,同一人≤30次/分 | 预估 < 500 人次/天 | ✅ 充裕 |
| `/appchat/create` | ≤1000群/天 | 外援场景低频(预估 < 10群/天) | ✅ 充裕 |
| `/appchat/send` | ≤2万人次/分,同一人≤200条/分 | 群内消息量极小 | ✅ 充裕 |
| 回调消息 | 无硬限制 | 企微服务器推送到回调 URL | ✅ 无风险 |
#### 7.5.5 备用方案局限性与适用边界
| 局限 | 说明 | 影响 |
|------|------|------|
| **无法跨主体企微** | 企微原生1对1仅限同一企微主体内员工 | 无法服务供应商/外包人员 |
| **无法跨平台** | 原生方案绑定企微,无法嵌入钉钉/飞书/浏览器 | H5 扩展场景不可用 |
| **AI/人工区分不直观** | 都以应用身份推送,需内容前缀区分 | 体验不如 H5 的丰富身份标识 |
| **交互卡片需开发** | "转人工"按钮、满意度评分需 template_card 消息类型 | 降级期可用关键词替代("转人工" |
| **群聊外援需审批** | appchat API 要求可见范围=根部门 | 需企微管理员配合 |
> **决策建议**:当 H5 不可用且影响范围仅限企微主体内员工时,**立即切换**原生1对1方案(零代码改动);当需要跨平台/跨主体服务时,**优先修复 H5**,不切换原生方案。
---
---
## 八、开发交付状态
### TL;DR
企微智能IT支持服务台第一步(消息接管 + 极简坐席台)全部代码已完成并通过测试,共 **110+ 文件****116/116 测试全部通过**,覆盖后端 API、坐席工作台、用户端 H5 三个子系统。
### 交付状态
| 阶段 | 状态 | 产出 |
|------|------|------|
| PRD | ✅ 完成 | `PRD.md` — 31 需求(P0/P1/P2),7 用户故事 |
| 架构设计 | ✅ 完成 | `docs/03-技术架构/00-系统架构设计文档-v1.3.md` — 9 表 DDL,7 API 组,4 时序图,5 任务分解 |
| T01 项目脚手架 | ✅ 完成 | 57 文件 — docker-compose, nginx, .env, 后端/前端骨架 |
| T02 后端核心服务 | ✅ 完成 | 16 文件 — 企微加解密, 消息路由, 评分, 会话, 趣味话术, 7 API 路由 |
| T03 坐席工作台 | ✅ 完成 | 25 文件 — 三栏布局, 会话管理, 聊天, AI助手面板(5Tab) |
| T04 用户端H5 | ✅ 完成 | 12 文件 — 聊天面板, 摇人按钮, AI助手, 审批链接, 软件下载 |
| QA 测试用例 | ✅ 完成 | 8 文件, 116 测试用例(原 93 + 新增 23) |
| Bug 修复 | ✅ 完成 | 7 个 Bug 修复(详见下方) |
| PostgreSQL/SQLite兼容 | ✅ 完成 | 9 个模型文件全部兼容 SQLite |
| database.py 懒加载 | ✅ 完成 | 避免测试导入时连接 PostgreSQL |
| WecomCrypto 懒加载 | ✅ 完成 | 避免默认 AES Key 导入报错 |
| **pytest 全量验证** | **✅ 116/116 通过** | 1.71 秒完成,0 失败 |
### 关键文件
```
wecom_it_smart_desk/
├── README.md # 项目主文档(GitHub 首页)
├── docker-compose.yml # Docker Compose 容器编排
├── .env # 环境变量(数据库密码等,不提交 Git)
├── backend/ # FastAPI 后端服务
│ ├── app/
│ │ ├── main.py # FastAPI 应用入口
│ │ ├── config.py # 配置管理(从 .env 读取)
│ │ ├── database.py # 懒加载数据库引擎
│ │ ├── models/ # 11 个 ORM 模型(兼容 PostgreSQL/SQLite
│ │ ├── schemas/ # Pydantic Schema(请求/响应校验)
│ │ ├── utils/
│ │ │ └── wecom_crypto.py # 企微消息加解密(AES-CBC-256
│ │ ├── services/
│ │ │ ├── wecom_service.py # 企微回调处理
│ │ │ ├── message_router.py # 消息路由 + 评分 + 举手检测
│ │ │ ├── scoring_service.py # 紧急度评分引擎
│ │ │ ├── session_service.py # 会话生命周期管理
│ │ │ └── funny_phrase_service.py # 摇人趣味话术生成
│ │ └── api/ # 8 个 API 路由模块
│ └── tests/ # 116+ 个测试用例
├── frontend-agent/ # 坐席工作台(Vue 3 + Element Plus
│ └── src/
│ ├── views/ # LoginView + WorkspaceView
│ ├── components/
│ │ ├── TopBar/ # 顶部栏(主题切换 + 用户信息)
│ │ ├── conversation/ # 会话列表 + 会话条目
│ │ ├── chat/ # 聊天区 + 消息气泡 + 输入框
│ │ ├── assistant/ # AI 推荐内联组件
│ │ ├── troubleshooting/ # 排查步骤栏(FlowchartNode
│ │ ├── quickreply/ # 快速回复面板(三层导航)
│ │ └── todo/ # 待办面板 + 任务详情视图
│ ├── stores/ # Pinia Storeconversation/agent/quickReply/theme/todo
│ └── api/ # API 调用模块
├── frontend-h5/ # 员工端 H5Vue 3 + Vant
│ └── src/
│ ├── views/ # ChatView
│ └── components/ # ChatPanel + 摇人按钮 + AI助手
├── nginx/ # Nginx 反向代理配置
│ └── nginx.conf
├── scripts/ # 部署和运维脚本
│ ├── start_backend.bat # Windows 快速启动后端(相对路径)
│ └── restart_backend.ps1 # Windows 重启后端(自动查找 PG/Redis/Python
└── docs/ # 项目文档(全部文档统一存放)
├── PRD.md # 产品需求文档 v1.0
├── PRD-v53-incremental.md # v5.3 增量需求
├── ARCHITECTURE.md # 系统架构设计(合并版)
├── 01-项目总览与部署手册.md # 管理者视角部署手册
├── 开发交付概览.md # 开发交付状态总览
├── 智能IT支持服务台-项目迁移文档.md # 工作区迁移记录
├── testing/ # 测试报告目录
│ └── QA_COMPREHENSIVE_REPORT.md # 综合 QA 报告
├── diagrams/ # Mermaid 图表
│ ├── sequence-diagram.mermaid
│ ├── sequence-shake.mermaid
│ ├── sequence-scoring.mermaid
│ ├── sequence-polling.mermaid
│ └── class-diagram.mermaid
└── prototypes/ # 原型文件
├── agent-workspace-v5_3.html # 当前锁定版本(v5.3
├── qr_data_full.json # 快速回复数据(180条)
└── archive/ # 历史原型归档
```
### Bug 修复清单(7 个)
| # | 文件 | 问题 | 修复 |
|---|------|------|------|
| 1 | `message_router.py` | `calculate_urgency()` 是 async 但未 `await` | 添加 `await` |
| 2 | `app/main.py` | 中文引号 `""` 嵌入 Python 双引号字符串,SyntaxError | 转义引号 |
| 3 | `wecom_callback.py` | `WecomCrypto` 模块级初始化,默认 AES Key 不合法导致 `binascii.Error` | 改为懒加载单例 `_get_wecom_crypto()` |
| 4 | `tests/conftest.py` | `aioredis.from_url` mock 路径错误 | 修正为 `redis.asyncio.from_url` |
| 5 | `tests/conftest.py` | `create_test_conversation()` 缺少 `is_pinned`/`is_todo` 参数 | 添加可选参数 |
| 6 | `session_service.py` | `conversation_id` UUID 对象 vs String(36) 列类型不匹配 | 先转字符串再查询 |
| 7 | `scoring_service.py` | 关键词大小写不敏感缺失 + `_check_vip` 缺短路 | `.lower()` + 短路返回 |
### 用户下一步操作
1. **(已验证)pytest 全量通过**:116/116 测试已在开发环境验证通过,本地无需再跑
2. **配置企微应用凭证**
- 复制 `.env.example``.env`
- 填入企微应用的 CorpID、AgentID、Secret、Token、EncodingAESKey
3. **Docker Compose 启动**(需 PostgreSQL + Redis):
```powershell
cd C:\Users\simon\wecom_it_smart_desk
docker-compose up -d
```
4. **前端开发启动**
```powershell
# 坐席工作台
cd frontend-agent && npm install && npm run dev
# 用户端 H5
cd frontend-h5 && npm install && npm run dev
```
5. **企微回调配置**:在企微管理后台配置消息回调 URL 指向你的服务器
## 九、附录
### 8.1 需要团队协助的事项
| # | 事项 | 需要谁 | 紧急度 |
|---|------|--------|--------|
| 1 | **服务器 SSH 账号**:用于 Docker 部署 | 运维 | 🔴 高(当前阻塞) |
| 2 | **企微通讯录权限**:确认 API 权限(VIP 功能依赖) | 运维/企微管理员 | 中(M1 可用 mock) |
| 3 | **千问/Dify/RAGFlow 环境**M2 阶段) | 架构/开发 | 低(M2 前准备) |
### 8.2 项目文件索引
```
wecom_it_smart_desk/
├── README.md # 入口索引
├── PRD.md # 产品需求文档
├── docs/
│ ├── 01-项目总览与部署手册.md # ← 本文档(运维/架构/管理者)
│ ├── 02-技术架构与开发指南.md # 开发者文档
│ └── 03-测试验证文档.md # 测试文档
├── backend/ # FastAPI 后端
├── frontend-agent/ # 坐席工作台前端
├── frontend-h5/ # 员工 H5 前端
├── nginx/nginx.conf # Nginx 反代配置
├── docker-compose.yml # Docker Compose 编排
├── .env.production # 生产环境变量模板
└── scripts/ # 部署/构建脚本
```
---
> 本文档合并自原 `docs/团队沟通文档-架构消息知识库.md`、`docs/正式环境独立部署架构方案.md`、`docs/DEPLOY_NAS.md`。详细技术规格见 `docs/02-技术架构与开发指南.md`。
@@ -0,0 +1,93 @@
%% IT 智能服务台 — 架构组件图(卷挂载重构前后对比)
%% 文件: class-diagram.mermaid
classDiagram
class HostServer {
+String path: /opt/wecom-it-desk/
+String ip: 10.90.5.110
+String domain: itsupport.servyou.com.cn
}
class AppDirectory {
+String path: /opt/wecom-it-desk/app/
+String role: 唯一代码源
+List~File~ pythonFiles
+Boolean hasAuthPy
}
class BackendDirectory {
+String path: /opt/wecom-it-desk/backend/
+File dockerfile
+File requirementsTxt
+String note: 不再包含 app/ 子目录
}
class DockerImage {
+String name: wecom-it-desk-backend:latest
+String base: python:3.12-slim
+String pythonVersion: 3.12
+Boolean containsCode: false
+Boolean containsDeps: true
+String envPythondontWriteBytecode: "1"
}
class DockerContainer {
+String name: wecom_it_backend
+String workdir: /app
+String command: uvicorn app.main:app
+Integer port: 8000
+Boolean healthy
}
class VolumeMount {
+String hostPath: /opt/wecom-it-desk/app/
+String containerPath: /app/app/
+String type: bind
+String mode: rw
}
class UploadVolume {
+String name: backend-uploads
+String containerPath: /app/uploads/
+String type: named_volume
}
class LogMount {
+String hostPath: /var/log/wecom-it-desk/
+String containerPath: /app/logs/
+String type: bind
}
class DockerCompose {
+String file: docker-compose.yml
+String buildContext: ./backend
+List~Service~ services
}
class HealthCheck {
+String endpoint: /health
+Integer interval: 30
+Integer timeout: 10
+Integer retries: 3
+Integer startPeriod: 40
}
HostServer --> AppDirectory : contains
HostServer --> BackendDirectory : contains
HostServer --> DockerCompose : contains
DockerCompose --> DockerImage : builds
DockerCompose --> DockerContainer : runs
DockerCompose --> VolumeMount : configures
DockerCompose --> UploadVolume : configures
DockerCompose --> LogMount : configures
DockerImage --> DockerContainer : basis for
VolumeMount --> DockerContainer : mounts code to /app/app/
UploadVolume --> DockerContainer : mounts uploads to /app/uploads/
LogMount --> DockerContainer : mounts logs to /app/logs/
AppDirectory --> VolumeMount : source of
DockerContainer --> HealthCheck : monitored by
BackendDirectory --> DockerImage : provides Dockerfile + requirements.txt
@@ -0,0 +1,52 @@
%% IT 智能服务台 — 部署时序图(卷挂载重构)
%% 文件: sequence-diagram.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 验证当前状态
Host-->>Ops: 200 OK
Ops->>Host: 备份 docker-compose.yml → .bak.{TIMESTAMP}
Ops->>Host: 备份 Dockerfile → .bak.{TIMESTAMP}
Ops->>Host: 写入 .rollback-info 文件
Note over Ops,Container: S2: 代码验证
Ops->>Host: 检查 app/__init__.py, app/main.py
Ops->>Host: 检查 app/api/auth.py(关键!)
Host-->>Ops: 全部存在
Note over Ops,Container: S3: 修改 Dockerfile
Ops->>Host: 重写 Dockerfile
Note right of Host: 删除 COPY . .<br/>新增 ENV PYTHONDONTWRITEBYTECODE=1
Ops->>Host: grep 验证 COPY . . 已删除
Note over Ops,Container: S4: 修改 docker-compose.yml
Ops->>Host: sed 插入 ./app:/app/app 卷挂载
Ops->>Host: grep 验证卷挂载已添加
Note over Ops,Container: S5: 重建镜像并重启
Ops->>Docker: docker compose build backend
Docker->>Docker: 构建镜像(仅 site-packages
Note right of Docker: 镜像不含业务代码
Ops->>Docker: docker compose up -d backend
Docker->>Container: 创建容器
Docker->>Host: 挂载 ./app → /app/app (bind mount)
Container->>Host: 运行时读取 /opt/wecom-it-desk/app/ 代码
Container->>Container: uvicorn app.main:app 启动
Note over Ops,Container: S6: 部署后验证
Ops->>Container: curl http://localhost:8000/health
Container-->>Ops: 200 OK
Ops->>Container: docker exec ... from app.auth import router
Container-->>Ops: auth module: OK
Ops->>Host: md5sum app/main.py
Ops->>Container: docker exec md5sum /app/app/main.py
Note right of Ops: 对比 hash 一致 → 卷挂载正常
Note over Ops,Container: S7: 清理(48小时后)
Ops->>Host: rm -rf backend/app/
Ops->>Host: 确认服务仍正常
+76
View File
@@ -0,0 +1,76 @@
# 部署运维工具箱
> **版本**: v1.0 | **日期**: 2026-07-10 | **维护人**: 宋献
> **定位**: 部署运维过程中可复用的脚本、配置模板和调试工具的统一存放点。
> **规则**: 每次故障排查或部署完成后,可复用的工具应归档到此目录并在本 README 中登记。
---
## 工具索引
### 上传部署工具
| 工具 | 用途 | 使用方式 |
|------|------|----------|
| `fast_upload.py` | 堡垒机大文件快速上传(8000-char base64 分块,比 jms_ops.py 的 500-char 快 16 倍) | `python fast_upload.py <本地文件> <远程路径>` |
| `deploy_to_container.py` | 一键部署到 Docker 容器(打包→上传→cp→重启) | `python deploy_to_container.py <服务名> <本地路径> <容器路径>` |
### Nginx 配置模板
| 工具 | 用途 | 使用方式 |
|------|------|----------|
| `nginx-access-control.conf` | 三端访问控制配置模板(企微 UA OR IP 白名单双条件放行 + CSP 头) | 上传到服务器 `/opt/wecom-it-desk/nginx/nginx.conf``docker restart wecom_it_nginx` |
### 调试检查工具
| 工具 | 用途 | 使用方式 |
|------|------|----------|
| `check_html.py` | Python f-string HTML 花括号平衡检查器(排查 `{{ }}` 转义问题) | `python check_html.py <file.py>` |
| `render_test.py` | 容器内 HTML 渲染验证(模拟 f-string 渲染并输出实际 HTML/JS | `docker exec wecom_it_backend python /tmp/render_test.py` |
| `extract_html.py` | 从 Python f-string 中提取 HTML 模板到独立文件 | `python extract_html.py <file.py>` |
### 历史修复脚本(archive/
以下脚本为一次性修复用途,保留在 `archive/` 子目录中供参考,不建议直接复用。
| 脚本 | 修复场景 | 日期 |
|------|----------|------|
| `fix_admin_role.py` | 修复扫码登录角色写死为 agent 的问题 | 2026-07-08 |
| `fix_compose_redis.py` / `fix_compose_redis2.py` | 修复 docker-compose Redis 密码 URL 编码 | 2026-07-07 |
| `fix_itdesk.py` / `fix_itdesk2.py` / `fix_itdesk3.py` | 修复 nginx /itdesk/ 路由问题 | 2026-07-08 |
| `patch.py` / `patch-mini.py` / `patch-redis-url.py` | Redis 密码 URL 编码补丁 | 2026-07-02 |
| `update_password.py` | 数据库密码更新脚本 | 2026-07-05 |
| `check_logs.py` / `check_roles.py` / `verify_logs.py` | 日志和角色检查(一次性诊断) | 2026-07-08~09 |
| `upload_chunked.py` / `upload_split.py` | 早期分块上传方案(已被 fast_upload.py 替代) | 2026-07-08 |
| `nginx_*.conf` / `itdesk-nginx-block.conf` | 历史 nginx 配置快照 | 2026-07-06~08 |
| `extract_and_migrate.py` / `_ctrt_transform.py` | 数据迁移和格式转换 | 2026-07-06 |
---
## 工具沉淀流程
1. **排查完成** → 评估是否有可复用的脚本/配置模板
2. **归档** → 复制到 `toolbox/`(活跃工具)或 `toolbox/archive/`(历史脚本)
3. **登记** → 在本 README 的工具索引表中添加条目
4. **清理** → 删除项目根目录的临时文件(渲染输出、中间产物等)
## 堡垒机使用说明
所有工具涉及服务器操作时,通过堡垒机(`sxn@10.212.189.210:2222`)执行:
- 统一使用 `jms_ops.py`jumpserver-ops 技能)或 `fast_upload.py`(大文件上传)
- 详见 `docs/09-部署运维/11-堡垒机运维工具.md`
---
## 相关文档
| 文档 | 位置 |
|------|------|
| 标准故障排查手册 | `docs/09-部署运维/00-标准故障排查手册.md` |
| 堡垒机运维工具 | `docs/09-部署运维/11-堡垒机运维工具.md` |
| 项目管理 SOP | `docs/10-项目管理/IT智能服务台-标准作业流程SOP.md` |
| Nginx 基线配置 | `docs/09-部署运维/01-nginx-prod-baseline-20260708.conf` |
---
> **维护说明**: 新增工具时请同步更新本 README。archive/ 中的脚本仅供历史参考,不保证可用性。
@@ -0,0 +1,7 @@
fp='/opt/wecom-it-desk/nginx/nginx.conf'
c=open(fp).read()
p='\n # 真实 IP 还原(2026-06-15 v0.5.1)\n set_real_ip_from 10.0.0.0/8;\n set_real_ip_from 172.16.0.0/12;\n set_real_ip_from 192.168.0.0/16;\n set_real_ip_from 10.212.0.0/16;\n real_ip_header X-Forwarded-For;\n real_ip_recursive on;\n'
o='error_log /var/log/nginx/error.log warn;'
n=c.replace(o,o+p,1)
open(fp,'w').write(n)
print('patched, +%d bytes'%(len(n)-len(c)))
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""
修复 docker-compose.yml 中 REDIS_URL 默认值的 URL-encode 问题。
背景:
- 2026-06-15 故障:REDIS_URL 里的密码 R3d!s@2026#Secure 含 @ # 两个 URL 保留字符,
Python redis 库解析时密码被截断成 R3d!s,导致鉴权失败 → Redis 连接超时
- 修复:把密码 URL-encode(@→%40, #→%23, !→%21)
- ⚠️ 关键: 只 URL-encode REDIS_URL 那行的密码,redis-server --requirepass
和 healthcheck 的 redis-cli -a 都必须保持**明文**(否则 Redis 容器启动失败/鉴权失败)
用法:
sudo python3 /tmp/patch-redis-url.py
"""
fp = '/opt/wecom-it-desk/docker-compose.yml'
# 读取当前内容
with open(fp, encoding='utf-8') as f:
c = f.read()
# 旧值(精确匹配 REDIS_URL 那一行,带 redis://://@ 上下文,避免误改 --requirepass)
old = 'REDIS_URL=redis://:${REDIS_PASSWORD:-R3d!s@2026#Secure}@redis:6379/0'
# 新值:URL-encode 后的密码(!→%21, @→%40, #→%23),仅 REDIS_URL 这一行
new = 'REDIS_URL=redis://:${REDIS_PASSWORD:-R3d%21s%402026%23Secure}@redis:6379/0'
# 检查是否已经修复过(幂等性)
if old in c:
print('[OK] 检测到未编码版本,准备修复...')
c2 = c.replace(old, new, 1) # 只替换第一次出现(更安全)
with open(fp, 'w', encoding='utf-8') as f:
f.write(c2)
delta = len(c2) - len(c)
print('[OK] 已修复:REDIS_URL 行的密码已 URL-encode')
print(f'[OK] 文件长度变化:{delta:+d} 字节')
elif new in c:
print('[OK] 已经修复过,跳过(幂等性 OK)')
else:
print('[ERROR] 既没找到旧值也没找到新值,请人工检查 docker-compose.yml')
print('---')
print('当前 REDIS_URL 相关配置:')
import subprocess
result = subprocess.run(['grep', '-n', 'REDIS_URL\\|REDIS_PASSWORD\\|--requirepass\\|redis-cli', fp],
capture_output=True, text=True)
print(result.stdout)
exit(1)
# 验证:确保 --requirepass 和 redis-cli 仍然是明文(没被误改)
import subprocess
result = subprocess.run(['grep', '-nE', 'REDIS_URL|--requirepass|redis-cli.*-a', fp],
capture_output=True, text=True)
print('---')
print('当前所有密码相关行(应只有 REDIS_URL 一行是 URL-encoded,其他保持明文):')
print(result.stdout)
@@ -0,0 +1,20 @@
fp = '/opt/wecom-it-desk/nginx/nginx.conf'
with open(fp) as f:
c = f.read()
patch = '''
# ------------------------------------------------------------------
# 真实 IP 还原(2026-06-15 v0.5.1 修复)
# ------------------------------------------------------------------
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
set_real_ip_from 10.212.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
'''
old = 'error_log /var/log/nginx/error.log warn;'
new = old + patch
new_c = c.replace(old, new, 1)
with open(fp, 'w') as f:
f.write(new_c)
print('patched, +{} bytes'.format(len(new_c) - len(c)))
@@ -0,0 +1,30 @@
#!/usr/bin/env python3
import os
import psycopg2
# 从环境变量获取 DATABASE_URL
db_url = os.environ.get('DATABASE_URL')
if not db_url:
# 尝试从 docker-compose 生成的变量拼接
db_url = "postgresql://wecom_user:wecom_pass@10.90.5.110:5432/wecom_it_desk"
conn = psycopg2.connect(db_url)
cur = conn.cursor()
# A表
cur.execute("SELECT COUNT(*) FROM config_change_logs")
a_count = cur.fetchone()[0]
# B表 audit_logs 中 action='config_change' 的数量
cur.execute("SELECT COUNT(*) FROM audit_logs WHERE action = 'config_change'")
b_config_change_count = cur.fetchone()[0]
# B表总数
cur.execute("SELECT COUNT(*) FROM audit_logs")
b_total = cur.fetchone()[0]
print(f"config_change_logs (A): {a_count}")
print(f"audit_logs (B) - config_change events: {b_config_change_count}")
print(f"audit_logs (B) - total: {b_total}")
conn.close()
@@ -0,0 +1,19 @@
import re
with open(
r'D:\资料\03-项目开发\wecom_it_smart_desk\backend\app\api\auth_qrcode.py',
'r',
encoding='utf-8',
) as f:
content = f.read()
match = re.search(r'html = f"""(.+?)"""', content, re.DOTALL)
if match:
html = match.group(1)
print('HTML length:', len(html))
out_path = r'D:\资料\03-项目开发\wecom_it_smart_desk\tmp_scan_html.html'
with open(out_path, 'w', encoding='utf-8') as out:
out.write(html)
print('Saved to', out_path)
else:
print('HTML template not found')
+136
View File
@@ -0,0 +1,136 @@
#!/usr/bin/env python3
"""
快速 base64 上传脚本 — 大分块版本
将本地文件通过 JumpServer PTY base64 通道上传到远程服务器
使用 8000 字符/块(远大于 jms_ops.py 的 500 字符),大幅减少命令数
"""
import sys
import base64
import hashlib
from pathlib import Path
# 添加 jms_ops.py 所在目录
SKILL_DIR = Path(r"C:\Users\simon\.workbuddy\skills\jumpserver-ops\scripts")
sys.path.insert(0, str(SKILL_DIR))
# 导入 jms_ops 中的核心函数
from jms_ops import get_connection_tokens, PlinkSession
def fast_upload(local_path: str, remote_path: str, chunk_size: int = 8000):
"""大分块 base64 上传"""
local_file = Path(local_path)
if not local_file.exists():
print(f"ERROR: file not found: {local_path}")
return False
local_data = local_file.read_bytes()
local_md5 = hashlib.md5(local_data).hexdigest()
b64_data = base64.b64encode(local_data).decode("ascii")
# 分块
chunks = [b64_data[i:i+chunk_size] for i in range(0, len(b64_data), chunk_size)]
total_chunks = len(chunks)
print(f"File: {local_file.name}")
print(f"Size: {len(local_data)} bytes")
print(f"Base64: {len(b64_data)} chars")
print(f"Chunks: {total_chunks} x {chunk_size} chars")
print(f"MD5: {local_md5}")
print(f"Target: {remote_path}")
print()
# 获取 token + 启动会话
tokens = get_connection_tokens(1)
if not tokens:
print("ERROR: failed to get connection tokens")
return False
token_id, token_secret = tokens[0]
session = PlinkSession(f"JMS-{token_id}", token_secret)
if not session.connect():
print("ERROR: failed to connect session")
return False
try:
# 1. 清空目标文件
print("Clearing target file...")
session.run_command(f"> {remote_path}", timeout=5)
# 2. 逐块追加
for i, chunk in enumerate(chunks):
# 用 printf 避免 echo 的换行符问题
cmd = f"printf '%s' '{chunk}' >> {remote_path}.b64"
r = session.run_command(cmd, timeout=10)
if not r["success"]:
print(f" FAIL chunk {i+1}/{total_chunks}")
return False
# 进度报告
if (i+1) % 20 == 0 or (i+1) == total_chunks:
pct = (i+1) * 100 // total_chunks
print(f" [{pct:3d}%] chunk {i+1}/{total_chunks}")
# 3. base64 解码
print(f"\nDecoding base64 -> {remote_path}...")
r = session.run_command(f"base64 -d {remote_path}.b64 > {remote_path}", timeout=30)
if not r["success"]:
print(f" WARN decode result: {r}")
# 4. 验证大小
print("Verifying size...")
r = session.run_command(f"wc -c < {remote_path}", timeout=5)
if r["success"]:
remote_size_str = r["output"].strip()
remote_size = int(remote_size_str) if remote_size_str.isdigit() else -1
if remote_size == len(local_data):
print(f" OK size match: {remote_size} bytes")
else:
print(f" SIZE MISMATCH: local={len(local_data)}, remote={remote_size}")
return False
else:
print(f" WARN cannot verify size: {r}")
return True # 仍然认为成功
# 5. MD5 验证
print("Verifying MD5...")
r = session.run_command(f"md5sum {remote_path}", timeout=10)
if r["success"]:
remote_md5 = r["output"].split()[0]
if remote_md5 == local_md5:
print(f" OK MD5 match: {remote_md5}")
else:
print(f" MD5 MISMATCH: local={local_md5}, remote={remote_md5}")
# 大小匹配但 MD5 不匹配,可能是 PTY 换行符问题
print(" (size matches, trying gzip test instead)")
r2 = session.run_command(f"gzip -t {remote_path} 2>&1 && echo GZIP_OK || echo GZIP_FAIL", timeout=10)
if r2["success"] and "GZIP_OK" in r2["output"]:
print(" OK gzip integrity test passed")
# 清理临时文件
session.run_command(f"rm -f {remote_path}.b64", timeout=5)
return True
else:
print(f" GZIP FAIL: {r2}")
return False
else:
print(f" WARN cannot verify MD5")
# 6. 清理临时文件
session.run_command(f"rm -f {remote_path}.b64", timeout=5)
print("\nUpload complete!")
return True
finally:
session.close()
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Fast base64 upload via JumpServer PTY")
parser.add_argument("local", help="Local file path")
parser.add_argument("remote", help="Remote file path")
parser.add_argument("--chunk-size", type=int, default=8000, help="Chunk size in chars (default: 8000)")
args = parser.parse_args()
success = fast_upload(args.local, args.remote, args.chunk_size)
sys.exit(0 if success else 1)
+181
View File
@@ -0,0 +1,181 @@
"""在容器内运行,渲染 scan 端点的实际 HTML 输出"""
import sys
sys.path.insert(0, '/app')
# 模拟 f-string 中的变量
user_name = "测试用户"
jsapi_signature = "test_sig_abc123"
jsapi_timestamp = 1752105600
jsapi_nonce = "test_nonce_xyz"
jsapi_appid = "ww_test_corp_id"
current_url = "https://itsupport.servyou.com.cn/api/auth_qrcode/scan?code=test&state=test"
html = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录成功 - IT智能服务台</title>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: linear-gradient(135deg, #07C160 0%, #06AD56 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; }}
.card {{ background: rgba(255,255,255,0.95); border-radius: 20px; padding: 48px 32px; max-width: 360px; width: 100%; text-align: center; box-shadow: 0 20px 60px rgba(0,0,0,0.3); }}
.check {{ width: 64px; height: 64px; margin: 0 auto 16px; }}
.title {{ color: #1f2937; font-size: 24px; font-weight: 600; margin-bottom: 8px; }}
.subtitle {{ color: #6b7280; font-size: 14px; margin-bottom: 24px; }}
.status {{ display: inline-flex; align-items: center; gap: 6px; background: #dcfce7; color: #166534; padding: 10px 20px; border-radius: 50px; font-size: 14px; font-weight: 500; }}
.footer {{ margin-top: 20px; color: #9ca3af; font-size: 12px; }}
.back-btn {{ display: none; margin-top: 20px; padding: 12px 32px; background: #07C160; color: white; border: none; border-radius: 50px; font-size: 16px; font-weight: 500; cursor: pointer; }}
.debug {{ margin-top: 16px; color: #6b7280; font-size: 11px; line-height: 1.6; word-break: break-all; text-align: left; background: #f3f4f6; padding: 10px 12px; border-radius: 8px; }}
.debug b {{ color: #07C160; }}
</style>
</head>
<body>
<div class="card">
<svg class="check" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="32" cy="32" r="30" fill="#07C160" stroke="#06AD56" stroke-width="4"/>
<path d="M20 32l8 8 16-16" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<h1 class="title">登录成功</h1>
<div class="status">已自动确认登录</div>
<p class="subtitle">你好,{user_name}<br>请返回电脑端查看</p>
<button class="back-btn" id="backBtn" onclick="manualClose()">点击返回企微</button>
<div class="footer">页面即将自动关闭 · 税友集团</div>
<div class="debug" id="debugInfo">
<b>签名:</b> {'成功' if jsapi_signature else '未生成'}<br>
<b>URL:</b> {current_url}<br>
<b>状态:</b> <span id="jsStatus">JS加载中...</span>
</div>
</div>
<script>
(function() {{
var statusEl = document.getElementById('jsStatus');
var btnEl = document.getElementById('backBtn');
var startTime = Date.now();
var tried = {{}};
function setStatus(msg) {{
if (statusEl) statusEl.textContent = msg + ' (' + (Date.now() - startTime) + 'ms)';
}}
function showBtn() {{
if (btnEl) btnEl.style.display = 'inline-block';
}}
function tryClose(forceShowBtn) {{
setStatus('尝试关闭');
if (!tried.wxClose && typeof wx !== 'undefined' && wx.closeWindow) {{
tried.wxClose = true;
try {{
setStatus('wx.closeWindow');
wx.closeWindow();
return true;
}} catch(e) {{ setStatus('wx.closeWindow失败:' + (e.message || e)); }}
}}
if (!tried.wxInvoke && typeof wx !== 'undefined' && wx.invoke) {{
tried.wxInvoke = true;
try {{
setStatus('wx.invoke closeWindow');
wx.invoke('closeWindow', {{}}, function(){{}});
return true;
}} catch(e) {{ setStatus('wx.invoke失败:' + (e.message || e)); }}
}}
if (!tried.jsBridge && typeof WeixinJSBridge !== 'undefined' && WeixinJSBridge.call) {{
tried.jsBridge = true;
try {{
setStatus('WeixinJSBridge.closeWindow');
WeixinJSBridge.call('closeWindow');
return true;
}} catch(e) {{ setStatus('JSBridge失败:' + (e.message || e)); }}
}}
if (!tried.windowClose) {{
tried.windowClose = true;
try {{
setStatus('window.close');
window.close();
return true;
}} catch(e) {{}}
}}
if (!tried.historyBack) {{
tried.historyBack = true;
try {{
setStatus('history.back');
history.back();
return true;
}} catch(e) {{}}
}}
if (forceShowBtn) {{
setStatus('无法自动关闭,请手动返回');
showBtn();
}}
return false;
}}
function manualClose() {{
tryClose(true);
}}
var checkCount = 0;
var maxChecks = 50;
var interval = setInterval(function() {{
checkCount++;
var hasWx = typeof wx !== 'undefined';
var hasBridge = typeof WeixinJSBridge !== 'undefined';
setStatus('检测中 wx=' + hasWx + ' bridge=' + hasBridge + ' count=' + checkCount);
if (hasWx || hasBridge) {{
clearInterval(interval);
setStatus('已检测到关闭API1秒后尝试关闭');
setTimeout(function() {{
tryClose(true);
}}, 1000);
return;
}}
if (checkCount >= maxChecks) {{
clearInterval(interval);
setStatus('未检测到API,直接尝试关闭');
tryClose(true);
}}
}}, 100);
setTimeout(function() {{
showBtn();
}}, 3000);
}})();
</script>
</body>
</html>"""
# 保存渲染后的 HTML
with open('/tmp/test_scan.html', 'w', encoding='utf-8') as f:
f.write(html)
print(f"HTML rendered: {len(html)} chars")
print("Saved to /tmp/test_scan.html")
# 检查 script 部分
import re
script_match = re.search(r'<script>(.+?)</script>', html, re.DOTALL)
if script_match:
js = script_match.group(1)
print(f"\nJS section: {len(js)} chars")
# 检查是否有 {{ 残留(f-string 未正确渲染)
if '{{' in js:
print("ERROR: Found unrendered {{ in JS!")
for i, line in enumerate(js.split('\n')):
if '{{' in line:
print(f" Line {i+1}: {line.strip()[:80]}")
else:
print("OK: No unrendered braces in JS")
# 打印前 20 行 JS
print("\nFirst 20 lines of rendered JS:")
for i, line in enumerate(js.split('\n')[:20]):
print(f" {i+1}: {line}")
@@ -0,0 +1,208 @@
# 会议室预定-小鱼易联终端 部署指南
> **日期**: 2026-07-11
> **版本**: v1.0
> **代码状态**: 40/40 测试通过,待部署
> **预估部署时间**: 30-45 分钟
---
## 一、部署前置条件
### 1.1 企微配置
- [x] 企微会议室 Secret 已申请
- [x] 企微会议室已在管理后台创建
- [ ] 确认会议室 `meetingroom_id` 列表
### 1.2 服务器环境
- [ ] PostgreSQL 可用(需执行 Alembic 迁移)
- [ ] Redis 可用(会议室缓存依赖)
- [ ] Nginx 可用(终端前端静态文件)
- [ ] Docker Compose 可用
### 1.3 终端设备
- [ ] 确认小鱼易联终端型号(当前按浏览器方案设计)
- [ ] 终端浏览器支持 WebSocket + ES6
---
## 二、部署步骤
### Step 1: 数据库迁移
```bash
# 进入后端容器
docker compose exec backend alembic upgrade head
# 验证新表
docker compose exec backend python -c "
from app.database import engine
from sqlalchemy import inspect
insp = inspect(engine)
tables = insp.get_table_names()
print('meetingroom_booking_snapshots' in tables) # 应为 True
print('terminal_room_bindings' in tables) # 应为 True
"
```
### Step 2: 环境变量配置
`docker-compose.yml` 中添加:
```yaml
backend:
environment:
- WECOM_MEETINGROOM_SECRET=<your_secret>
- MEETINGROOM_CACHE_TTL_ROOMS=600
- MEETINGROOM_CACHE_TTL_BOOKING=30
- MEETINGROOM_CACHE_TTL_STATUS=10
```
重启后端:
```bash
docker compose up -d backend
```
### Step 3: 后端验证
```bash
# 验证 API 端点注册
curl -sk https://localhost/api/itportal/meetingroom/rooms \
-H "Authorization: Bearer $TOKEN"
# 验证 WS 端点
wscat -c "wss://localhost/ws/terminal/TEST-SN-001"
# 验证管理端绑定 API
curl -sk https://localhost/api/itportal/admin/terminal-bindings \
-H "Authorization: Bearer $ADMIN_TOKEN"
```
### Step 4: 终端前端部署
```bash
# 1. 本地构建
cd frontend-terminal
npm run build
# 2. 打包
tar -czf terminal-dist.tar.gz dist/
# 3. 上传到服务器(通过堡垒机)
python C:\Users\simon\.workbuddy\skills\jumpserver-ops\scripts\jms_ops.py \
pack-upload ./terminal-dist.tar.gz /opt/wecom-it-desk/frontend-terminal/
# 4. 服务器解压
cd /opt/wecom-it-desk/frontend-terminal/
tar -xzf /tmp/terminal-dist.tar.gz
# 5. Nginx 配置
# 在 nginx.conf 中添加:
location /itterminal/ {
alias /usr/share/nginx/html/terminal/;
try_files $uri $uri/ /itterminal/index.html;
}
```
### Step 5: Nginx 重启
```bash
docker compose restart nginx
```
### Step 6: H5 端入口验证
H5 端已内置会议室入口(`MeetingroomView.vue`),无需额外部署。
验证:
```bash
curl -sk https://localhost/ith5/ | grep -o 'meetingroom'
```
---
## 三、部署后验证
### 3.1 功能验证清单
| # | 验证项 | 验证方法 | 预期结果 |
|---|--------|---------|---------|
| 1 | 会议室列表 | `GET /api/itportal/meetingroom/rooms` | 返回会议室列表 |
| 2 | 会议室状态 | `GET /api/itportal/meetingroom/rooms/{id}/status` | 返回当前状态 |
| 3 | 预定会议室 | `POST /api/itportal/meetingroom/bookings` | 创建预定成功 |
| 4 | 时间线 | `GET /api/itportal/meetingroom/rooms/{id}/timeline` | 返回当日时间线 |
| 5 | 终端绑定 | `POST /api/itportal/admin/terminal-bindings` | 绑定终端-会议室 |
| 6 | 终端WS | `WS /ws/terminal/{sn}` | 终端状态实时推送 |
| 7 | 终端前端 | 浏览器打开 `/itterminal/` | 深色主题大屏页面 |
| 8 | H5入口 | H5 端点击会议室入口 | 跳转 MeetingroomView |
| 9 | 扫码登录 | 终端扫码 | 企微扫码登录成功 |
| 10 | 状态同步 | 终端修改状态 → H5 实时更新 | WS 推送正常 |
### 3.2 Redis 缓存验证
```bash
# 会议室 token
redis-cli -a $REDIS_PASSWORD get wecom:meetingroom_access_token
# 会议室列表缓存
redis-cli -a $REDIS_PASSWORD get meetingroom:room_list
# 预定缓存
redis-cli -a $REDIS_PASSWORD get "meetingroom:booking:{room_id}:{date}"
# 状态缓存
redis-cli -a $REDIS_PASSWORD get "meetingroom:status:{room_id}"
```
---
## 四、回滚方案
### 4.1 数据库回滚
```bash
# 回退迁移 050
docker compose exec backend alembic downgrade -1
```
### 4.2 后端回滚
```bash
# 恢复 .py 文件(bind mount 自动生效)
git checkout HEAD~1 -- backend/app/api/meetingroom.py
git checkout HEAD~1 -- backend/app/services/meetingroom_service.py
# ... 其他文件
docker compose restart backend
```
### 4.3 前端回滚
```bash
# 恢复旧 dist
mv /opt/wecom-it-desk/frontend-terminal/dist /opt/wecom-it-desk/frontend-terminal/dist.bak
# 恢复上一版本
docker compose restart nginx
```
### 4.4 Nginx 配置回滚
```bash
# 移除 /itterminal/ location 块
docker compose restart nginx
```
---
## 五、已知限制
1. **企微API时间限制**: 会议室预定查询范围限制 31 天
2. **终端身份**: 当前按访客可查看设计,管理操作需扫码登录
3. **WS重连**: 终端断线后自动重连(指数退避),超过 5 次降级为轮询(10s间隔)
4. **缓存TTL**: 会议室列表 600s / 预定 30s / 状态 10s,手动刷新可跳过缓存
---
## 六、配置参数速查
| 参数 | 默认值 | 环境变量 |
|------|--------|---------|
| 会议室Token TTL | 6900s | - |
| 会议室列表缓存 | 600s | `MEETINGROOM_CACHE_TTL_ROOMS` |
| 预定缓存 | 30s | `MEETINGROOM_CACHE_TTL_BOOKING` |
| 状态缓存 | 10s | `MEETINGROOM_CACHE_TTL_STATUS` |
| WS重连次数 | 5 | - |
| WS降级轮询间隔 | 10s | - |
File diff suppressed because it is too large Load Diff