chore: 整理项目结构,清理归档文件,更新部署配置

This commit is contained in:
Simon
2026-07-04 21:01:39 +08:00
parent 8bd4ab0366
commit 64ff1bf7d5
508 changed files with 43575 additions and 14129 deletions
@@ -0,0 +1,185 @@
# Release Notes v0.7.1
> 📅 发布日期:2026-06-23 | 类型:🔐 安全 + ✨ 功能 | 紧急度:🟡 重要
> 🔄 补充:2026-06-24 hotfix #116/#118/#119/#120 已部署(详见文末"补充 hotfix"小节)
## 🎯 本版本重点
1. **修复扫码登录 iOS NSURLErrorCannotFindHost** — 企微 App 用户扫码不再报错
2. **完整 MFA + RBAC 权限体系** — 5 角色细粒度权限 + 高危操作二次认证
3. **H5 员工端企微 OAuth 登录** — 员工不再需要输账号密码
4. **🆕 扫码登录去掉手机 confirm 步骤** — 企微 App 无确认 UI,扫码成功 = 直接登录
## 🔐 安全修复
### #109 P0:扫码登录 NSURLError 修复
**问题**:用户企微 App 访问 `https://itsupport.servyou.com.cn/itportal/`,扫描二维码后跳转报 `NSURLErrorCannotFindHost`(iOS WebView DNS 解析失败)
**根因**:`backend/app/config.py` 新字段 `qrcode_oauth_callback` 用了 pydantic 默认行为,**不剥环境变量前缀**
- 字段名:`qrcode_oauth_callback`
- pydantic-settings 期望 env:`QRCODE_OAUTH_CALLBACK`
- 实际 env:`WECOM_QRCODE_CALLBACK`(带前缀,匹配不上)
- 结果:`settings.qrcode_oauth_callback = ""`,qrcode_service 走兜底返回**相对路径** `/api/auth_qrcode/scan`
- iOS WebView 拼接相对路径失败 → DNS 解析错误
**修复**:
```python
from pydantic import Field
class Settings(BaseSettings):
qrcode_oauth_callback: str = Field(
default="",
validation_alias="WECOM_QRCODE_CALLBACK",
)
```
**部署**:docker commit + restart + 验证,详见 [[phase1-progress#109-hotfix-二轮修复]]
### RBAC 5 角色 × 4 资源 × 4 操作 × 3 范围
- **5 角色**:super_admin / admin / agent / user / guest
- **4 资源**:user / conversation / config / audit_log
- **4 操作**:read / create / update / delete
- **3 范围**:all / department / self
### 高危路由白名单 + 中间件
5 类高危操作需 OTP 二次认证:
- `role_change`(角色变更)
- `config_change`(配置变更)
- `data_export`(数据导出)
- `account_disable`(账户禁用)
- `account_create_reset`(账户创建/重置)
## ✨ 新功能
### 后端扫码登录(端点 4 个)
| 端点 | 方法 | 说明 |
|---|---|---|
| `/api/auth_qrcode/create` | POST | 生成二维码(含 PNG base64) |
| `/api/auth_qrcode/poll/{ticket}` | POST | 扫码端轮询 |
| `/api/auth_qrcode/scan` | POST | 企微回调 |
| `/api/auth_qrcode/confirm` | POST | 用户确认登录 |
### 后端 MFA + pyotp(端点 5 个)
| 端点 | 方法 | 说明 |
|---|---|---|
| `/api/mfa/status` | GET | 查询绑定状态 |
| `/api/mfa/bind/start` | POST | 开始绑定(返回 secret + QR) |
| `/api/mfa/bind/confirm` | POST | 确认绑定 |
| `/api/mfa/verify` | POST | 验证 OTP |
| `/api/mfa/disable` | POST | 禁用 MFA |
### 前端 MFA UI
- `MfaBind.vue` — 绑定弹窗
- `useHighRiskOtp.ts` composable — 高危操作 OTP 弹窗
- `Login.vue` / `TopBar.vue` — 集成 MFA 状态显示
- 路由 + store 全链路集成
### H5 员工端 OAuth 登录
- 部署路径:`https://itsupport.servyou.com.cn/itdesk/`
- 自动 OAuth 静默授权(`snsapi_base`)
- 会话列表 + 消息收发
- 已完成 E2E 验证(#104)
## 🛠️ 部署要点
### 必备环境变量
```bash
# /opt/wecom-it-desk/.env
WECOM_QRCODE_CALLBACK=https://itsupport.servyou.com.cn/api/auth_qrcode/scan
WECOM_SSO_ENABLED=true
WECOM_SSO_CALLBACK_BASE=https://itsupport.servyou.com.cn
```
### 部署清单
- [x] Backend commit `78f60c6`(v0.7.1-dev)
- [x] docker 镜像 `wecom-it-desk-backend:latest` 已 commit 新 config
- [x] 容器已重启 + /api/ready 200
- [x] H5 dist 已部署到 `/opt/wecom-it-desk/nginx/html/itdesk/`
- [x] nginx 已 reload
- [x] /api/auth_qrcode/create 返回绝对 URL
- [ ] Gitea push(等用户重授权 token)
- [ ] 企微 App 实际扫码验证
## ⚠️ 已知问题
1. **Gitea push 阻塞**:`workbuddy-claude` token 2026-06-15 被吊销,需去 Gitea Web 重授权
2. **#108 still pending**:v0.7.1-dev 78f60c6 未推到 origin
3. **/api/admin/ IP 白名单**仍是 0.0.0.0/0(临时方案),v1.0 前必须收窄
## 📚 相关文档
- [[H5-DEPLOY-RUNBOOK-v0.7.1]] — H5 部署 runbook
- [[DEPLOY-LOGIN-MIGRATION-v0.7.0]] — 旧版扫码登录
- [[E2E-CHECKLIST-v0.7.0]] — 端到端验收
- [[NGINX-DOMAIN-ROUTING]] — nginx 域名分发
- [[USER-GUIDE-QRCODE-MFA]] — 用户手册
- [[phase1-progress]] — 进展跟踪
## 📊 改动统计
- **代码**:22 文件 / +4705 行
- **测试**:78 passed + 4 xfail + 33 pre-existing failures(已分类)
- **文档**:5 新增 / 3 更新
- **Agent**:4 并行 worktree 合并
---
## 🔄 补充 hotfix(2026-06-24 部署)
### #116 P0 + #118 P0:扫码端点 405 + ticket≠state
**问题**:
- 坐席扫码登录报 `405 Method Not Allowed`
- 企微 OAuth 回调走 GET(`?code=xxx&state=<ticket>`),旧代码只支持 POST
- 即使改双方法,`Optional[str] = None``Query()` 装饰器,FastAPI 当 body 参数处理
- 函数参数叫 `ticket`,企微用 `state`,参数名不匹配
**修复**:`backend/app/api/auth_qrcode.py` scan 端点
```python
@router.api_route("/scan", methods=["GET", "POST"], response_model=None)
async def scan_qrcode(
body: Optional[QrcodeScanRequest] = None,
ticket: Optional[str] = Query(None, description="兼容旧参数名"),
state: Optional[str] = Query(None, description="企微 OAuth state 标准参数名"),
code: Optional[str] = Query(None, description="企微 OAuth 授权码"),
redis_client = Depends(dep_redis),
):
if body is not None:
final_ticket, final_code = body.ticket, body.code
else:
final_ticket = state or ticket # 优先 state(企微标准),回退 ticket
final_code = code
```
**部署**:`deploy-staging/hotfix-116-qrcode-scan-get/` — webcli v7 一键,20/20 pytest 过
### #119:webcli v7 全自动部署链路
复用 jumpserver Playwright + Luna webcli + OTP 自动,实现 18-31KB 命令脚本单次输入,2-3 分钟跑完。
### #120 P0:扫码成功 = 自动登录(去掉 confirm)
**问题**:企微 App 端没有"确认登录" UI,旧 confirm 步骤在生产永远走不通(用户报告"扫码成功后,手机端没有出现确认登录按钮")
**用户决策**:方案 A — 扫码成功 = 自动登录(推荐),2026-06-24 拍板
**修复**:`backend/app/services/qrcode_service.py` `process_scan` 默认 `auto_confirm=True`
- 扫码成功直接写 `qrcode:confirm:{ticket}`(含 token)
- 前端 poll 立即拿到 status=confirmed + token
- 跳过手机 confirm 步骤
- 默认 roles=["user"],admin/agent 走 portal 端角色选择
**测试**:20/20 pytest 过(`test_scan_then_poll_returns_confirmed` + `test_scan_auto_confirm_writes_confirm_key` 等)
**部署**:`deploy-staging/hotfix-120-qrcode-auto-confirm/` — webcli v7 一键,容器内 `/api/ready` 200 OK
**残留**:外部域名 `https://itsupport.servyou.com.cn/api/ready` 不通(nginx `/api/` upstream 配错 + 8000 端口未暴露),**等 #48 修复**。容器内 backend 完全健康。
@@ -0,0 +1,185 @@
# 智能IT支持服务台 - 部署修复记录
**日期**2026-06-13
**负责人**:宋献
**状态**:待部署验证
---
## 一、问题概述
### 1.1 部署后 H5 用户端报错
```
POST /api/h5/conversations/current/messages 返回 500 错误:
- 错误1column conversations.impact_scope does not exist
- 错误2AIHandler.__init__() missing 1 required positional argument: 'ai_service'
```
### 1.2 影响范围
| 系统 | 影响 | 说明 |
|------|------|------|
| H5 用户端 | 阻塞 | 无法发送消息触发 AI 回复 |
| Dify AI | 无法测试 | 依赖 H5 消息发送 |
| 管理后台 | 已修复 | admin001 已设为管理员 |
---
## 二、根因分析
### 2.1 数据库缺列
服务器上数据库 `conversations` 表缺少4个新增列:
- `impact_scope` — 影响范围
- `is_blocking` — 是否阻塞
- `emotion_state` — 情绪状态
- `dify_conversation_id` — Dify 会话ID
### 2.2 AIHandler 初始化错误
代码重构后 `AIHandler.__init__` 需要传入 `AIService` 实例,但 `dependencies.py` 中两处调用仍使用无参构造函数:
```python
# 错误代码
return AIHandler()
# 正确代码
return AIHandler(ai_service=AIService())
```
---
## 三、修复内容
### 3.1 数据库修复(已完成)
```sql
ALTER TABLE conversations ADD COLUMN IF NOT EXISTS impact_scope VARCHAR(50);
ALTER TABLE conversations ADD COLUMN IF NOT EXISTS is_blocking BOOLEAN DEFAULT false;
ALTER TABLE conversations ADD COLUMN IF NOT EXISTS emotion_state VARCHAR(50);
ALTER TABLE conversations ADD COLUMN IF NOT EXISTS dify_conversation_id VARCHAR(255);
```
### 3.2 代码修复
**文件**`backend/app/dependencies.py`
**修复内容**2处 AIHandler 调用补上 ai_service 参数
| 位置 | 修复前 | 修复后 |
|------|--------|--------|
| get_shared_ai_handler() | `return AIHandler()` | `return AIHandler(ai_service=AIService())` |
| dep_ai_handler() | `return AIHandler()` | `return AIHandler(ai_service=AIService())` |
---
## 四、部署步骤
### 4.1 本地打包
```powershell
cd D:\资料\03-项目开发\wecom_it_smart_desk\deploy-server
.\打包部署.bat
```
生成文件:
- `it-smart-desk-server-deploy.zip` — 前端+nginx+docker-compose
- `deploy-backend.tar` — 后端 Docker 镜像(含修复)
### 4.2 上传服务器
通过堡垒机将文件上传到服务器 `/tmp/`
- `it-smart-desk-server-deploy.zip`
- `deploy-backend.tar`
### 4.3 服务器部署
```bash
# 1. 加载后端镜像
docker load -i /tmp/deploy-backend.tar
# 2. 重启后端容器
docker stop wecom_it_backend && docker rm wecom_it_backend
docker run -d --name wecom_it_backend ... (原启动命令)
# 3. 验证后端健康
curl https://itsupport.servyou.com.cn/health
```
---
## 五、验证检查项
### 5.1 后端健康检查
```bash
curl https://itsupport.servyou.com.cn/health
# 预期返回:{"status":"ok"}
```
### 5.2 H5 消息发送测试
1. H5 Mock 登录:`POST /api/h5/mock-login`
2. 发送消息:`POST /api/h5/conversations/current/messages`
3. 预期:返回 AI 回复(调用 Dify 成功)
### 5.3 Dify AI 集成状态
管理后台 → 集成配置 → Dify AI 状态应为 `connected`
---
## 六、相关配置
### 6.1 服务器信息
| 项目 | 值 |
|------|------|
| 服务器 IP | 10.90.5.110 |
| 域名 | itsupport.servyou.com.cn |
| WAF | 115.236.188.3 |
### 6.2 企微配置
| 项目 | 值 |
|------|------|
| CorpID | wwa8c87970b2011f41 |
| AgentID | 1000133 |
| Token | wAqMCP |
| EncodingAESKey | KQY3cEsBc3rdi3xua9rPd5WxH8kYOhyASzWZQf75aJS |
### 6.3 Dify 配置
| 项目 | 值 |
|------|------|
| API URL | http://yw-dify.dc.servyou-it.com/dify2openai/v1/chat/completions |
| API Key | http://yw-dify.dc.servyou-it.com/v1\|app-UaTWYdBSwN6VktKQlbh5YN5H\|Chat |
### 6.4 数据库配置
| 项目 | 值 |
|------|------|
| 数据库 | PostgreSQL |
| 库名 | wecom_it_desk |
| 用户 | wecom |
| 密码 | wecom_secret_2026 |
---
## 七、相关文件
| 文件路径 | 说明 |
|---------|------|
| `backend/app/dependencies.py` | 修复后的代码 |
| `deploy-server/build-and-deploy.ps1` | 打包部署脚本 |
| `deploy-server/打包部署.bat` | 一键执行入口 |
| `docs/IT服务台PRDv1.0.md` | 产品需求文档 |
---
**更新历史**
| 日期 | 更新内容 |
|------|---------|
| 2026-06-13 | 初始记录,数据库修复 + 代码修复 + 打包脚本 |
@@ -0,0 +1,186 @@
# 智能IT支持服务台 - 版本更新说明
**版本**: v1.1.0
**更新日期**: 2026-06-14
**文档状态**: 待审核
---
## 一、本次更新内容
### 1.1 新增功能
| 功能 | 说明 | 优先级 |
|------|------|--------|
| 消息撤回 | 2分钟内可撤回自己的消息 | P0 |
| 消息删除 | 删除自己的消息 | P0 |
| 消息状态 | 支持 sending/sent/delivered/read/recalled 状态 | P0 |
| 标记已读 | 一键标记会话已读 | P1 |
| 图片上传 | 支持图片上传(≤10MB) | P1 |
| 文件上传 | 支持文件上传(≤10MB) | P1 |
### 1.2 架构优化
| 优化项 | 说明 |
|--------|------|
| Health Check | 所有容器已配置健康检查 |
| 自动重启 | 容器崩溃自动重启(restart: unless-stopped |
| AI Gateway 设计 | 预留多模型切换架构 |
### 1.3 安全增强
| 功能 | 说明 |
|------|------|
| OTP 双因素认证 | 访问管理后台时二次验证 |
| 操作审计 | 关键操作日志记录(规划中) |
---
## 二、需要同步的代码
### 2.1 后端文件
| 文件 | 改动 | 说明 |
|------|------|------|
| `app/models/message.py` | 修改 | 添加 status、recallable_until 字段 |
| `app/api/messages.py` | 修改 | 添加撤回/删除/标记已读/上传 API |
| `app/api/ws_manager.py` | 修改 | 添加消息状态广播 |
| `docker-compose.yml` | 修改 | healthcheck 已配置 |
### 2.2 数据库变更
```sql
-- 需要执行的 SQL 迁移
ALTER TABLE messages ADD COLUMN status VARCHAR(20) DEFAULT 'sent';
ALTER TABLE messages ADD COLUMN recallable_until TIMESTAMP;
```
### 2.3 前端文件(如果有)
| 文件 | 说明 |
|------|------|
| 消息操作菜单 | 撤回/删除按钮 |
| 消息状态显示 | 状态图标 |
| 已读标记 | 一键已读 |
---
## 三、部署步骤
### 3.1 本地打包
```bash
# 后端打包
cd backend
docker build -t wecom-it-desk-backend:latest .
# 导出镜像
docker save wecom-it-desk-backend:latest -o wecom-it-desk-backend.tar
```
### 3.2 服务器部署
```bash
# 1. 上传镜像到堡垒机
# 堡垒机: sxn@10.212.189.210:2222 (OTP)
# 目标路径: /tmp/
# 2. SSH 到正式服务器
ssh sxn@10.212.189.210 -p 2222
ssh 10.90.5.110
# 3. 导入镜像
docker load -i /tmp/wecom-it-desk-backend.tar
# 4. 解决容器冲突(重要!)
docker rm -f wecom_it_redis wecom_it_backend wecom_it_postgres wecom_it_nginx 2>/dev/null
# 5. 重新启动
docker compose -p root up -d
# 6. 执行数据库迁移
docker compose exec backend python -c "from app.database import engine; engine.execute('ALTER TABLE messages ADD COLUMN status VARCHAR(20) DEFAULT 'sent'')"
```
---
## 四、验证清单
### 4.1 健康检查
| 检查项 | 命令 | 预期结果 |
|--------|------|----------|
| 后端服务 | `curl -s http://localhost:8000/health` | `{"status": "ok"}` |
| Nginx | `curl -s http://localhost:80/itdesk/health` | `{"status": "ok"}` |
| 数据库 | `docker compose exec backend python -c "from app.database import engine; print('OK')"` | OK |
| Redis | `docker compose exec redis redis-cli ping` | PONG |
### 4.2 API 测试
| API | 方法 | 测试命令 |
|-----|------|----------|
| 撤回消息 | POST | `curl -X POST http://localhost:8000/api/messages/{id}/recall` |
| 删除消息 | DELETE | `curl -X DELETE http://localhost:8000/api/messages/{id}` |
| 标记已读 | POST | `curl -X POST http://localhost:8000/api/conversations/{id}/mark-read` |
| 图片上传 | POST | `curl -X POST -F "file=@test.jpg" http://localhost:8000/api/messages/image` |
### 4.3 功能测试
| 功能 | 测试场景 | 预期结果 |
|------|----------|----------|
| 消息发送 | 发送文本消息 | 消息正常显示 |
| 消息撤回 | 2分钟内撤回 | 状态变为 recalled |
| 消息撤回 | 超过2分钟 | 返回 403 错误 |
| 标记已读 | 点击已读 | 所有消息标记为已读 |
---
## 五、已知问题与限制
### 5.1 待解决
| 问题 | 状态 | 说明 |
|------|------|------|
| 容器冲突 | 进行中 | 需指定项目名 `-p root` |
| 前端同步 | 待确认 | 可能需要更新前端代码 |
### 5.2 已知限制
| 限制 | 说明 |
|------|------|
| 单节点部署 | MVP 阶段保持单节点 |
| 文件大小 | 单文件 ≤10MB |
| 撤回时间 | 2分钟后不可撤回 |
---
## 六、回滚方案
如果部署失败,执行:
```bash
# 停止服务
docker compose -p root down
# 恢复旧镜像(如果有备份)
docker load -i wecom-it-desk-backend-old.tar
# 使用备份的配置启动
docker compose -p root up -d
```
---
## 七、联系人
| 角色 | 联系人 | 说明 |
|------|--------|------|
| 产品负责人 | 许清楚 | PRD 确认 |
| 技术负责人 | 寇豆码 | 代码审查 |
| QA 负责人 | 严过关 | 测试验证 |
| 运维负责人 | 宋献 | 部署执行 |
---
**文档版本**: 1.0
**审核状态**: 待审核
@@ -0,0 +1,89 @@
# OTP 二次验证实现文档
## 功能概述
为 IT 支持服务台坐席端增加 OTP 二次验证功能:
- admin 角色登录时需要输入 Google Authenticator 动态码
- 首次绑定需要验证一次码才启用
- OTP 丢失后需管理员重置
## 实现方案
### 1. 后端修改
#### 1.1 安装依赖
```bash
pip install pyotp qrcode[pil] pillow
```
#### 1.2 数据库模型
Agent 模型已有字段:
- `otp_secret`: OTP 密钥(Base32编码)
- `otp_enabled`: OTP 是否启用(0=否, 1=是)
#### 1.3 Schema 修改
文件:`backend/app/schemas/agent.py`
- `AgentLogin` 增加 `otp_code` 可选字段
- `AgentResponse` 增加 `otp_enabled` 字段
#### 1.4 API 修改
文件:`backend/app/api/agents.py`
新增接口:
- `POST /api/agents/otp-bind` - 生成 OTP 密钥和二维码
- `POST /api/agents/otp-verify` - 验证并启用 OTP
- `POST /api/agents/otp-unbind` - 解绑 OTP
登录接口修改:
- admin 角色且 otp_enabled=1 时,检查 otp_code
- 未提供 otp_code 返回 `require_otp: True`
- 验证 OTP 码正确后生成 token
### 2. 前端修改
#### 2.1 坐席端 API
文件:`frontend-agent/src/api/agent.ts`
- `login()` 增加 `otpCode` 参数
#### 2.2 坐席端 Store
文件:`frontend-agent/src/stores/agent.ts`
- `login()` 增加 `otpCode` 参数
- 返回 `require_otp` 标记让页面处理
#### 2.3 坐席端登录页面
文件:`frontend-agent/src/views/Login.vue`
- 增加 OTP 输入框(v-if="requireOtp"
- 首次登录返回 require_otp 时显示输入框
- 用户输入 OTP 后再次登录
## 使用流程
### 首次绑定 OTP
1. 管理员登录坐席端
2. 调用 `POST /api/agents/otp-bind`
3. 获取二维码和密钥
4. 使用 Google Authenticator 扫描二维码
5. 调用 `POST /api/agents/otp-verify` 输入动态码验证
6. 验证成功,otp_enabled 设为 1
### 登录流程
1. 用户输入 user_id 和 name
2. 后端检查 admin 角色且 otp_enabled=1
3. 返回 `require_otp: True`
4. 前端显示 OTP 输入框
5. 用户输入 6 位动态码
6. 后端验证通过,生成 token
### 解绑流程
1. 管理员调用 `POST /api/agents/otp-unbind`
2. otp_secret 和 otp_enabled 清空
## 错误码
| 错误码 | 说明 |
|--------|------|
| 1006 | OTP 验证码错误 |
| 1007 | OTP 绑定失败 |
| 1008 | 请先绑定 OTP |
| 1009 | OTP 验证失败 |
| 1010 | OTP 解绑失败 |
@@ -0,0 +1,220 @@
# 部署手册:扫码登录 + OTP 二次认证(Phase 1+2)
> 创建:2026-06-21
> 适用版本:v0.7.0+ (Phase 1+2)
> 部署顺序:后端 → 前端 4 端 → nginx → 数据库 migration → 验收
---
## 🎯 部署目标
从 v0.6.x 的"企微 OAuth + SMS 2FA"升级到 v0.7.0 的"扫码登录 + OTP TOTP + SMS 备用"。
涉及后端变更:
- 新增 `/api/auth_qrcode/*` 4 个端点(扫码登录)
- 新增 `/api/mfa/*` 6 个端点(OTP 二次认证)
- 新增 `/api/admin/mfa/reset/{employee_id}`(管理员重置)
- 新增 `/api/admin/high-risk/*` 演示端点 + require_high_risk_otp 守卫
- 新增 2 个数据库字段: `users.mfa_secret`, `users.mfa_enabled`, `users.mfa_bound_at`, `users.mfa_last_verified_at`
涉及前端变更:
- frontend-agent:Login.vue 重写(扫码 UI)+ 新增 MfaBind.vue + useHighRiskOtp
- frontend-portal:新增 QrcodeLogin.vue + 默认路由
- frontend-admin:新增 MfaManage.vue(管理员 MFA 重置 UI)
- frontend-h5:**不变**(仍走企微 OAuth)
涉及 nginx 变更:
- `/itportal/` 新增 location(扫码入口)
- 其余 4 个 location 已有,配置按 docs/NGINX-DOMAIN-ROUTING.md
---
## 📋 部署前检查
### 1. 后端镜像依赖
`backend/requirements.txt` 必须包含:
```
pyotp==2.9.0 # TOTP 生成
qrcode[pil]==7.4.2 # 二维码生成
redis==5.0.7 # 已存在
```
### 2. 数据库迁移文件
确认以下 migration 已存在:
- `backend/alembic/versions/023_mfa_fields.py`(加 4 个 MFA 字段)
- `backend/alembic/versions/024_*.py`(可选:其他变更)
### 3. 配置文件
`backend/.env` 确认:
```bash
# 新增(扫码登录)
WECOM_OAUTH_REDIRECT_URI=https://itsupport.servyou.com.cn/itportal/qrcode-callback
WECOM_CORP_ID=ww1234567890abcdef
WECOM_AGENT_ID=1000002
# 已有(OTP)
SMS_2FA_ENABLED=true # 蜂鸟 SMS 备用通道
```
### 4. 域名 / DNS
- `itsupport.servyou.com.cn`(主域名,已有)
- 子路径:`/itportal/` `/itagent/` `/itadmin/` `/itdesk/`(同一域名,nginx 分发)
- 证书:`itsupport.servyou.com.cn.crt`(公司统一管理)
---
## 🚀 部署步骤
### 步骤 1:部署后端(注意 RO bind mount)
```bash
# 1. 上传新 backend 包到堡垒机
scp backend-v070-p1.tar.gz user@bastion:/tmp/
# 2. 通过堡垒机 PuTTY(不用 ssh -J)登录生产服务器
# 参考:feedback-putty-not-openssh.md
# 3. 解压并复制到 backend 目录(走宿主机路径,避开 RO bind mount 陷阱)
cd /opt/wecom-it-desk/
tar -xzf /tmp/backend-v070-p1.tar.gz
# 注意:用 cp -r 不是 docker cp(避开 RO bind mount 假成功陷阱)
sudo cp -r backend-v070-p1/* backend/
# 4. 数据库 migration
cd /opt/wecom-it-desk/backend
sudo docker exec wecom_it_backend alembic upgrade head
# 验证:
sudo docker exec wecom_it_backend alembic current
# 期望:023_mfa_fields (head)
# 5. 重启 backend(注意:backend 不在 compose 里,直接 docker restart)
sudo docker restart wecom_it_backend
# 验证:等待 ~30s,看健康检查
curl http://localhost:8000/health
# 期望:{"status":"ok",...}
```
### 步骤 2:部署前端 4 端
```bash
# 1. 各前端 build(本地)
cd frontend-agent && npm run build
cd frontend-portal && npm run build
cd frontend-admin && npm run build
# frontend-h5 不变,不用 build
# 2. 上传 dist 到生产服务器
scp -r frontend-agent/dist user@bastion:/tmp/agent-dist/
scp -r frontend-portal/dist user@bastion:/tmp/portal-dist/
scp -r frontend-admin/dist user@bastion:/tmp/admin-dist/
# 3. 通过堡垒机,复制到 nginx 容器挂载的目录
# 路径可能是 /opt/wecom-it-desk/frontend-*/
sudo cp -r /tmp/agent-dist/* /opt/wecom-it-desk/frontend-agent/dist/
sudo cp -r /tmp/portal-dist/* /opt/wecom-it-desk/frontend-portal/dist/
sudo cp -r /tmp/admin-dist/* /opt/wecom-it-desk/frontend-admin/dist/
# 4. 验证:curl HTML 文件
curl -I https://itsupport.servyou.com.cn/itportal/
# 期望:200 OK,content-type: text/html
```
### 步骤 3:更新 nginx 配置
```bash
# 1. 上传新 nginx 配置
# 新增 /itportal/ location,更新其他 location
# 参考:docs/NGINX-DOMAIN-ROUTING.md
# 2. 验证配置(在 nginx 容器里)
sudo docker exec wecom_it_nginx nginx -t
# 注意容器名是 wecom_it_nginx 不是 wecom-nginx
# 期望:nginx: configuration file /etc/nginx/nginx.conf test is successful
# 3. reload(不重启容器)
sudo docker exec wecom_it_nginx nginx -s reload
```
### 步骤 4:验收测试
按 docs/NGINX-DOMAIN-ROUTING.md 末"验证清单"逐条测试。
---
## 🔄 回滚方案
### 后端回滚
```bash
# 1. 用上次 patch1 备份
sudo cp -r /opt/wecom-it-desk/backend-v070-patch1/* /opt/wecom-it-desk/backend/
sudo docker restart wecom_it_backend
# 2. 数据库回滚(谨慎!)
sudo docker exec wecom_it_backend alembic downgrade -1
# 注意:只能降 1 个版本,如果已经升到 023,降到 022
```
### 前端回滚
```bash
# 直接覆盖 dist
sudo cp -r /opt/wecom-it-desk/frontend-*-bak/* /opt/wecom-it-desk/frontend-*/dist/
```
### nginx 回滚
```bash
# 容器内 sed -i 改回旧配置(避开 RO bind mount 假成功陷阱)
sudo docker exec wecom_it_nginx cp /etc/nginx/nginx.conf.bak /etc/nginx/nginx.conf
sudo docker exec wecom_it_nginx nginx -s reload
```
---
## ⚠️ 已知风险
| 风险 | 影响 | 缓解 |
|---|---|---|
| OTP 二维码渲染失败(后端 base64 生成出错) | 用户绑不上 OTP | 前端降级显示 qrcode_url 让用户手动复制 |
| pyotp 库版本升级导致不兼容 | OTP 验证失败 | 锁版本 pyotp==2.9.0,生产前跑 pytest |
| Admin MFA 重置端点被未授权访问 | 安全 | require_admin + 后续可加 IP 白名单 |
| 蜂鸟 SMS API 未上线 | 备用通道不可用 | 不影响 OTP 主通道,先上线 OTP,后接 SMS |
| nginx IP 白名单临时全开 | 安全 | v1.0 前必须收窄(task #48) |
---
## 📊 部署后验证
### 业务指标
- [ ] 扫码登录成功率 > 95%
- [ ] OTP 验证成功率 > 99%
- [ ] 高危操作 OTP 触发率 100%
- [ ] 蜂鸟 SMS fallback 触发 < 5%(绝大多数人用 OTP)
### 技术指标
- [ ] 扫码登录端到端 < 5s(从扫码到进入工作台)
- [ ] OTP 验证 < 500ms
- [ ] 高危操作 OTP 弹窗响应 < 200ms
---
## 📚 相关文档
- [USER-GUIDE-QRCODE-MFA.md](./USER-GUIDE-QRCODE-MFA.md) — 用户手册
- [NGINX-DOMAIN-ROUTING.md](./NGINX-DOMAIN-ROUTING.md) — nginx 域名分发
- [v070-alpha-deploy-runbook.md](../memory/v070-alpha-deploy-runbook.md) — v0.7.0-alpha 总览
- [docker-cp-readonly-bind-mount-fake-success.md](../memory/docker-cp-readonly-bind-mount-fake-success.md) — RO bind mount 陷阱
- [nginx-container-name-wecom-it-nginx.md](../memory/nginx-container-name-wecom-it-nginx.md) — 容器名坑
- [feedback-putty-not-openssh.md](../memory/feedback-putty-not-openssh.md) — 堡垒机 PuTTY
---
**变更历史**:
- 2026-06-21 创建(Phase 1+2 部署手册)
@@ -0,0 +1,227 @@
# 企微智能IT支持服务台 — 远程服务器部署指南(预生产)
> **预生产环境**:本系统与 IT 数据查询平台部署在**不同主机**。正式环境将迁移到 K8s。
## 部署架构
```
浏览器 ──→ it-dataquery.dc.servyou-it.com:80
┌─── nginx (本系统主机) ──────────────────────┐
│ │
│ /itdesk/* → H5 员工端 SPA │
│ /itagent/* → 坐席工作台 SPA │
│ /api/* → backend:8000 (FastAPI) │
│ /ws/* → backend:8000 (WebSocket) │
│ /* → 远程代理到数据平台主机 IP │ ← 跨主机
│ │
└──────────────┬─────────────────────────────┘
│ 本机 Docker 网络
┌─────────────┼─────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ backend │ │ postgres │ │ redis │
│ :8000 │ │ :5432 │ │ :6379 │
└──────────┘ └──────────┘ └──────────┘
```
## 网络互联
预生产环境中,数据平台在独立主机,**不需要 Docker 网络互联**。Nginx 通过远程 IP 直接反代数据平台:
```
本系统主机 (Docker) 数据平台主机
┌──────────────────┐ ┌─────────────────┐
│ nginx ──────────┼── HTTP 反代 ──→ │ 数据平台 :80 │
│ │ │ 远程 IP │ │
│ ▼ │ └─────────────────┘
│ backend:8000 │
│ postgres:5432 │
│ redis:6379 │
└──────────────────┘
```
## 前置条件
- 服务器已安装 Docker + Docker Compose
- IT 数据查询平台已部署运行
- 有 SSH 登录权限
## 部署步骤
### 1. 配置数据平台反代地址
预生产环境中,数据平台在**独立主机**。部署前,必须将 nginx 配置中的数据平台上游地址改为实际 IP。
编辑 `nginx/nginx.conf`
```nginx
# 将 DATAQUERY_HOST 替换为数据平台主机的实际 IP:端口
upstream dataquery {
server 10.80.0.86:80; # ← 改为数据平台实际 IP
}
```
> 不再需要创建 `it-platform-net` —— Docker 网络无法跨主机互联。nginx 通过 HTTP 直接反代到远程 IP。
### 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/
```
### 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` | 数据库密码 | 自定义强密码 |
### 5. 启动服务
```bash
bash scripts/deploy.sh
```
这会自动执行:检查前置条件 → 构建后端镜像 → 启动所有容器
### 6. 验证部署
```bash
# 检查容器状态
docker compose ps
# 健康检查
curl http://localhost:18080/itdesk/health
# 查看 H5 员工端
curl -I http://localhost:18080/itdesk/
# 查看坐席工作台
curl -I http://localhost:18080/itagent/
# 查看后端 API
curl http://localhost:18080/api/docs
```
浏览器验证:
| 地址 | 预期 |
|------|------|
| `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 文档 |
## 两种网络接入方式
### 方式 A:数据平台 nginx 反代到本项目 nginx(推荐)
数据平台的 nginx 配置添加:
```nginx
# IT 智能服务台路由
location /itdesk/ {
proxy_pass http://wecom_it_nginx:80/itdesk/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /itagent/ {
proxy_pass http://wecom_it_nginx:80/itagent/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /api/ {
proxy_pass http://wecom_it_nginx:80/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location /ws/ {
proxy_pass http://wecom_it_nginx:80/ws/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
```
此方式下本项目 nginx 不暴露 80 端口,docker-compose.yml 的 `ports` 可以删除。
### 方式 B:本项目 nginx 直接监听 80 端口
如果数据平台没有自己的 nginx,或者想用本项目的 nginx 统一管理:
1. 停止数据平台的端口映射
2. 本项目 nginx 的 80 端口直接对外
3. nginx.conf 中的 `location /` 反代到数据平台容器
## 常见问题
### Q: nginx 启动失败,报 `host not found in upstream "dataquery"`
A: `nginx/nginx.conf` 中的 `DATAQUERY_HOST` 未替换为数据平台实际 IP。编辑 nginx.conf,将占位符改为实际 IP:端口。
### Q: 访问 /itdesk/ 返回 404
A: 检查前端 dist 是否正确挂载:
```bash
docker exec wecom_it_nginx ls -la /usr/share/nginx/html/itdesk/
```
### Q: API 返回 CORS 错误
A: 检查 `.env` 中的 `CORS_ORIGINS` 是否包含 `http://it-dataquery.dc.servyou-it.com`
### Q: 数据库迁移失败
A: 查看 backend 日志:
```bash
docker compose logs backend
```
如果 PostgreSQL 未就绪,等 30 秒后重启 backend
```bash
docker compose restart backend
```
## 更新部署
只需更新变更的部分:
```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
```
@@ -0,0 +1,438 @@
# 群晖 NAS + Cloudflare Tunnel + 未认证企微 部署指南
> **适用范围**:阶段一功能测试
> **目标域名**`itdesk.amanzac.com`
> **最后更新**2026-06-07
---
## 架构总览
```
HTTPS HTTP
员工手机 ──────────→ Cloudflare Edge ──────────→ cloudflared ──→ nginx:80
(企微H5) (自动SSL+CDN) Tunnel 容器 │
┌────┴────┐
│ 路由分发 │
└────┬────┘
┌──────┼──────┐
│ │ │
/itdesk/ /itagent/ /api/
H5员工端 坐席工作台 后端
```
**关键特点**
- ✅ 无需公网 IP
- ✅ 无需 SSL 证书(Cloudflare 自动处理)
- ✅ 无需开放 NAS 端口
- ✅ 未认证企微可正常使用 OAuth2 + 消息 API
---
## §1 前置条件检查清单
| # | 条件 | 你的状态 | 说明 |
|---|------|---------|------|
| 1 | 群晖 NASDS220+ 及以上) | ✅ 已确认 | 需支持 Docker(ARM 机型需确认镜像兼容) |
| 2 | Container Manager 已安装 | ✅ 已确认 | 套件中心安装 |
| 3 | Cloudflare 账号 | ✅ 已确认 | 免费版即可 |
| 4 | 域名 `amanzac.com` 已托管 Cloudflare | ✅ 已确认 | DNS 管理 → Cloudflare |
| 5 | 企微管理后台权限 | ✅ 已确认 | 需配置自建应用 |
| 6 | SSH 访问 NAS | ⬜ 待确认 | 需开启 SSH 以执行 docker compose 命令 |
---
## §2 Cloudflare Tunnel 配置
### 2.1 创建 Tunnel
1. 登录 [Cloudflare Zero Trust](https://one.dash.cloudflare.com/)
2. 左侧菜单 → **Networks****Tunnels**
3. 点击 **Create a tunnel**
4. 选择 **Cloudflared** 类型
5. 输入 Tunnel 名称,如 `itdesk-nas`
6. 点击 **Save tunnel**
### 2.2 获取 Tunnel Token
创建完成后,页面会显示安装命令,其中包含 Token:
```bash
# 示例安装命令
cloudflared service install eyJhIjoiNjM1...
# ^^^^^^^^^^^^
# 这就是 Token
```
**复制这个 Token**,后面要填到 `.env` 文件中。
### 2.3 配置 Tunnel 路由(Public Hostname
在 Tunnel 创建页面,配置 **Public Hostname**
| 字段 | 填写 | 说明 |
|------|------|------|
| Subdomain | `itdesk` | 前缀 |
| Domain | `amanzac.com` | 你的域名 |
| Type | `HTTP` | 容器内是 HTTP |
| URL | `nginx` | Docker 容器名(同一网络内) |
> ⚠️ 注意:Type 选 **HTTP**(不是 HTTPS),因为 cloudflared 和 nginx 之间走的是容器内网 HTTP。SSL 由 Cloudflare Edge 终止。
点击 **Save tunnel**
### 2.4 验证 DNS 记录
Cloudflare 会自动创建一条 CNAME 记录:
- `itdesk.amanzac.com``cfargotunnel.com`
可在 Cloudflare Dashboard → DNS → Records 中确认。
---
## §3 项目文件部署到 NAS
### 3.1 上传项目文件
**方式一:Git Clone(推荐)**
如果 NAS 上有 Git
```bash
# SSH 登录 NAS
ssh admin@NAS_IP
# 创建项目目录
mkdir -p /volume1/docker/wecom-it-desk
cd /volume1/docker/wecom-it-desk
# 克隆项目
git clone <你的仓库地址> .
```
**方式二:SCP 上传**
从开发机上传构建好的文件:
```powershell
# 在 Windows PowerShell 中执行
# 上传核心文件(不含 node_modules 和 .git
scp -r "D:\资料\03-项目开发\wecom_it_smart_desk\docker-compose.nas.yml" admin@NAS_IP:/volume1/docker/wecom-it-desk/
scp -r "D:\资料\03-项目开发\wecom_it_smart_desk\.env.nas" admin@NAS_IP:/volume1/docker/wecom-it-desk/
scp -r "D:\资料\03-项目开发\wecom_it_smart_desk\nginx" admin@NAS_IP:/volume1/docker/wecom-it-desk/
scp -r "D:\资料\03-项目开发\wecom_it_smart_desk\backend" admin@NAS_IP:/volume1/docker/wecom-it-desk/
scp -r "D:\资料\03-项目开发\wecom_it_smart_desk\frontend-h5\dist" admin@NAS_IP:/volume1/docker/wecom-it-desk/frontend-h5/dist/
scp -r "D:\资料\03-项目开发\wecom_it_smart_desk\frontend-agent\dist" admin@NAS_IP:/volume1/docker/wecom-it-desk/frontend-agent/dist/
```
**方式三:群晖 File Station**
把构建产物打包成 zip,通过 File Station 上传到 `/docker/wecom-it-desk/` 然后解压。
### 3.2 配置环境变量
```bash
cd /volume1/docker/wecom-it-desk
# 复制模板
cp .env.nas .env
# 编辑 .env 文件
vi .env
```
**必须修改的项**
```bash
# 1. 填入 Cloudflare Tunnel Token(从 §2.2 获取)
CF_TUNNEL_TOKEN=eyJhIjoiNjM1... # ← 替换为你的实际 Token
# 2. 修改数据库密码
POSTGRES_PASSWORD=YourStrongPassword123! # ← 替换为强密码
# 3. 如果 NAS 能访问公司内网 Dify,填入 Dify 配置
# 如果不能访问,留空即可(AI 功能暂不可用,不影响阶段一)
DIFY_API_URL=
DIFY_API_KEY=
```
### 3.3 构建前端(如果还没构建)
前端需要先在开发机(Windows)上构建,再上传 dist/ 目录:
```powershell
# 在 Windows 开发机上
cd "D:\资料\03-项目开发\wecom_it_smart_desk"
# 构建坐席端
cd frontend-agent
npm install
npx vite build
# 构建 H5 员工端
cd ..\frontend-h5
npm install
npx vite build
```
构建产物在 `frontend-agent/dist/``frontend-h5/dist/` 中。
---
## §4 启动服务
### 4.1 SSH 登录 NAS 启动
```bash
# SSH 登录 NAS
ssh admin@NAS_IP
# 进入项目目录
cd /volume1/docker/wecom-it-desk
# 启动所有容器(5 个容器)
docker compose -f docker-compose.nas.yml up -d
# 等待约 30 秒,检查状态
docker compose -f docker-compose.nas.yml ps
```
**预期输出**
| 容器名 | 状态 | 说明 |
|--------|------|------|
| wecom_it_cloudflared | Running | Cloudflare Tunnel |
| wecom_it_nginx | Up (healthy) | 反向代理 |
| wecom_it_backend | Up | FastAPI 后端 |
| wecom_it_postgres | Up (healthy) | PostgreSQL |
| wecom_it_redis | Up (healthy) | Redis |
### 4.2 验证服务
```bash
# 1. 内网验证(在 NAS 上执行)
curl http://localhost:18080/api/health
# 预期输出: {"status":"ok","service":"wecom-it-smart-desk"}
# 2. 内网验证前端
curl http://localhost:18080/itdesk/health
# 预期输出: healthy
# 3. 公网验证(从任意有网络的设备)
curl https://itdesk.amanzac.com/api/health
# 预期输出: {"status":"ok","service":"wecom-it-smart-desk"}
# 4. 浏览器访问
# H5 员工端: https://itdesk.amanzac.com/itdesk/
# 坐席工作台: https://itdesk.amanzac.com/itagent/
```
### 4.3 常用运维命令
```bash
# 查看日志
docker compose -f docker-compose.nas.yml logs -f backend # 后端日志
docker compose -f docker-compose.nas.yml logs -f cloudflared # Tunnel 日志
docker compose -f docker-compose.nas.yml logs -f nginx # Nginx 日志
# 重启某个服务
docker compose -f docker-compose.nas.yml restart backend
# 停止所有服务
docker compose -f docker-compose.nas.yml down
# 更新并重启(代码更新后)
docker compose -f docker-compose.nas.yml up -d --build
```
---
## §5 企微自建应用配置
### 5.1 创建自建应用
1. 登录 [企微管理后台](https://work.weixin.qq.com/wework_admin/frame)
2. **应用管理****自建****创建应用**
3. 填写:
- 应用名称:`智能IT支持服务台`
- 应用logo:上传一个图标
- 可见范围:选择测试部门/人员
### 5.2 配置网页授权(OAuth2
在应用详情页 → **网页授权及JS-SDK**
| 配置项 | 填写 | 说明 |
|--------|------|------|
| 可信域名 | `itdesk.amanzac.com` | OAuth2 回调域名 |
> **验证方式**Cloudflare Tunnel 已提供 HTTPS,下载企微提供的验证文件,放到 `frontend-h5/dist/` 根目录后重新构建。
### 5.3 配置应用主页
在应用详情页 → **应用主页**
```
https://itdesk.amanzac.com/itdesk/
```
员工点击企微中的应用入口,直接打开 H5 页面。
### 5.4 配置接收消息(回调 URL)
在应用详情页 → **接收消息****设置API接收**
| 配置项 | 填写 | 说明 |
|--------|------|------|
| URL | `https://itdesk.amanzac.com/api/wecom/callback` | 企微消息推送地址 |
| Token | `wAqMCP` | 与 .env 中 WECOM_TOKEN 一致 |
| EncodingAESKey | `KQY3cEsBc3rdi3xua9rPd5WxH8kYOhyASzWZQf75aJS` | 与 .env 中一致 |
> 点击保存时,企微会向 URL 发送验证请求,后端必须正常响应才能保存成功。
### 5.5 修改 AI 机器人转人工链接
在现有 AI 机器人的 Dify 工作流中,将转人工关键字触发的链接从:
```
旧链接:https://work.weixin.qq.com/XXXX(员工服务入口)
```
改为:
```
新链接:https://itdesk.amanzac.com/itdesk/
```
> 这样员工点击转人工链接后,会跳转到 H5 自建应用页面(而非企微员工服务窗口)。
---
## §6 阶段一功能测试清单
### 6.1 基础连通性测试
| # | 测试项 | 方法 | 预期结果 | 状态 |
|---|--------|------|---------|------|
| 1 | Cloudflare Tunnel 连通 | 浏览器访问 `https://itdesk.amanzac.com/` | 页面正常加载 | ⬜ |
| 2 | 后端 API 健康 | 浏览器访问 `https://itdesk.amanzac.com/api/health` | 返回 `{"status":"ok"}` | ⬜ |
| 3 | H5 员工端页面 | 浏览器访问 `https://itdesk.amanzac.com/itdesk/` | H5 页面渲染 | ⬜ |
| 4 | 坐席工作台页面 | 浏览器访问 `https://itdesk.amanzac.com/itagent/` | 工作台页面渲染 | ⬜ |
### 6.2 OAuth2 登录测试
| # | 测试项 | 方法 | 预期结果 | 状态 |
|---|--------|------|---------|------|
| 5 | OAuth2 静默授权 | 在企微内点击应用入口 | H5 页面自动登录,显示员工身份 | ⬜ |
| 6 | 身份识别 | 授权后查看 H5 页面 | 显示当前用户姓名/工号 | ⬜ |
### 6.3 坐席工作台测试
| # | 测试项 | 方法 | 预期结果 | 状态 |
|---|--------|------|---------|------|
| 7 | 会话列表 | 坐席登录工作台 | 显示进行中的会话 | ⬜ |
| 8 | 聊天窗口 | 点击某个会话 | 显示完整对话记录 | ⬜ |
| 9 | 发送消息 | 坐席输入文本发送 | 消息发送成功 | ⬜ |
| 10 | 快速回复 | 点击快速回复面板 | 三级导航正常,模板可填入 | ⬜ |
### 6.4 端到端流程测试
| # | 测试项 | 方法 | 预期结果 | 状态 |
|---|--------|------|---------|------|
| 11 | AI 对话 → 转人工 | 员工与 AI 对话,触发转人工关键字 | 推送 H5 链接 | ⬜ |
| 12 | 员工点击 H5 链接 | 点击推送的链接 | 跳转到 H5 页面,自动登录 | ⬜ |
| 13 | 坐席收到会话 | 员工进入 H5 后 | 坐席工作台出现新会话 | ⬜ |
| 14 | 坐席回复 | 坐席使用快速回复 | 员工 H5 页面显示回复 | ⬜ |
| 15 | 企微通知 | 坐席回复后 | 员工收到企微应用消息通知 | ⬜ |
---
## §7 故障排查
### 7.1 Cloudflare Tunnel 连不上
```bash
# 检查 cloudflared 容器日志
docker compose -f docker-compose.nas.yml logs cloudflared
# 常见错误:
# ERR error="failed to connect to Cloudflare edge"
# → 检查 Token 是否正确
# → 检查 NAS 是否能访问外网
```
### 7.2 企微回调验证失败
```bash
# 检查后端是否收到回调请求
docker compose -f docker-compose.nas.yml logs backend | grep callback
# 常见原因:
# 1. Token / EncodingAESKey 与 .env 不一致
# 2. 后端回调路由路径不对(应为 /api/wecom/callback
# 3. Nginx 反代配置未正确转发
```
### 7.3 OAuth2 授权失败
```
常见原因:
1. 可信域名未配置或未验证 → 企微管理后台检查
2. redirect_uri 与可信域名不匹配 → 检查回调 URL
3. CorpID 不正确 → 检查 .env 中的 WECOM_CORP_ID
```
### 7.4 容器状态异常
```bash
# 查看所有容器状态
docker compose -f docker-compose.nas.yml ps
# 查看特定容器详细日志
docker compose -f docker-compose.nas.yml logs --tail 100 backend
# 重启所有容器
docker compose -f docker-compose.nas.yml restart
# 完全重建(代码更新后)
docker compose -f docker-compose.nas.yml down
docker compose -f docker-compose.nas.yml up -d --build
```
---
## §8 与正式部署的区别
| 维度 | NAS 部署(测试) | 正式部署 |
|------|----------------|---------|
| 域名 | `itdesk.amanzac.com` | `it-dataquery.dc.servyou-it.com` |
| 内网穿透 | Cloudflare Tunnel | 公司内网直连 |
| HTTPS | Cloudflare 自动 | Nginx + 公司 CA 证书 |
| 数据库密码 | 测试密码 | 强密码 + 审计 |
| AI 引擎 | 可能不可用(Dify 在内网) | 可用 |
| 员工数 | 测试人员(<10人) | 全公司 |
| 企业微信认证 | 未认证(200人上限) | 已认证 |
| 数据持久化 | Docker Volume | K8s PVC / 独立 PG 集群 |
---
## 附录 ACloudflare Tunnel 原理简述
```
传统方式 Cloudflare Tunnel
┌─────────────────┐ ┌─────────────────┐
互联网 ────→ │ 开放端口 + 公网IP │ 互联网 ────→ │ Cloudflare Edge │
│ + SSL 证书 │ │ (自动HTTPS) │
│ + DDNS/域名解析 │ └────────┬────────┘
└─────────────────┘ │
↑ │ Tunnel(长连接)
│ │
┌─────────────────┐ ┌─────────────────┐
│ NAS/服务器 │ cloudflared │ NAS/服务器 │
│ (必须可达) │ ←──主动连接──→│ (无需开放端口) │
└─────────────────┘ └─────────────────┘
优势:
1. 无需公网 IP — cloudflared 主动外连,不需要入站端口
2. 无需 SSL 证书 — Cloudflare Edge 自动处理 HTTPS
3. 无需 DDNS — 域名始终指向 Cloudflare
4. 更安全 — 不暴露 NAS 任何端口到公网
```
@@ -0,0 +1,252 @@
# v0.7.0 一键部署操作包(给生产运维)
> **目的**:把所有部署命令按顺序排好,生产运维复制粘贴即可完成 v0.7.0 部署。
> **预计时间**:15-20 分钟(含等 docker pull)
> **回滚**:每步都有 rollback 命令,任意一步失败立即回滚。
---
## 🔴 部署前 必做(用户自己操作)
### 1. 撤销并重签 Gitea token
```
1. 浏览器打开 http://100.85.152.112:8418
2. 右上角头像 → Settings → Applications → Manage Access Tokens
3. 找到旧 token(workbuddy-claude),点 Revoke
4. 点 Generate New Token,scope 选 "All",点 Generate
5. 复制新 token(只显示一次),临时存到 ~/Downloads/gitea-new-token.txt
```
### 2. 推送代码到 Gitea(用新 token)
```bash
# 在本地工作目录(D:\资料\03-项目开发\wecom_it_smart_desk-claude\backend)
cd /d/资料/03-项目开发/wecom_it_smart_desk-claude
# 临时把新 token 加进 remote URL(push 后立刻删除)
git remote set-url origin "http://workbuddy-claude:新TOKEN@100.85.152.112:8418/simon/wecom_it_smart_desk.git"
# 推送 main + tag
git push origin main
git push origin v0.7.0
# push 成功后,立刻从 URL 移除 token
git remote set-url origin "http://workbuddy-claude@100.85.152.112:8418/simon/wecom_it_smart_desk.git"
# 验证 token 已移除
git remote -v
# 期望:没有 token 字样
```
---
## 🟢 部署操作(在生产服务器,SSH/PuTTY)
> 服务器 IP: **10.90.5.110** (内网),**115.236.188.3** (公网入口)
> SSH 用户:堡垒机登录后跳转
### 步骤 1/6:备份当前生产状态
```bash
# 1.1 备份 backend 当前镜像
sudo docker tag wecom-it-desk-backend:latest wecom-it-desk-backend:v0.6.0-backup
# 1.2 备份 4 端 dist
sudo mkdir -p /opt/wecom-it-desk/dist-backup-2026-06-21
sudo cp -r /opt/wecom-it-desk/frontend-admin/dist /opt/wecom-it-desk/dist-backup-2026-06-21/admin
sudo cp -r /opt/wecom-it-desk/frontend-agent/dist /opt/wecom-it-desk/dist-backup-2026-06-21/agent
sudo cp -r /opt/wecom-it-desk/frontend-portal/dist /opt/wecom-it-desk/dist-backup-2026-06-21/portal
sudo cp -r /opt/wecom-it-desk/frontend-h5/dist /opt/wecom-it-desk/dist-backup-2026-06-21/h5
echo "备份完成"
# 1.3 备份 alembic 版本号(用于回滚确认)
sudo docker exec wecom_it_postgres psql -U postgres -d wecom_it -c "SELECT version_num FROM alembic_version;"
```
### 步骤 2/6:拉新 backend 镜像并跑 migration
```bash
# 2.1 拉新镜像
sudo docker pull wecom-it-desk-backend:v0.7.0
# 2.2 跑 migration(只 PG,SQLite 跳过)
sudo docker exec wecom_it_backend alembic upgrade head
# 期望输出:
# Running upgrade 024 -> 025, messages.id UUID
# Running upgrade <old> -> 022, qrcode_login
# Running upgrade <old> -> 023, mfa_fields
# 2.3 验证 migration head
sudo docker exec wecom_it_postgres psql -U postgres -d wecom_it -c "SELECT version_num FROM alembic_version;"
# 期望:025_messages_id_uuid
# 2.4 验证 messages.id 已改为 UUID
sudo docker exec wecom_it_postgres psql -U postgres -d wecom_it -c "\d messages" | grep "^ id"
# 期望:类型为 uuid
```
**🚨 若 migration 失败**:
```bash
sudo docker exec wecom_it_backend alembic downgrade -1
# 联系 Claude 排查
```
### 步骤 3/6:重启 backend 容器
```bash
# 3.1 重启(用 v0.7.0 镜像)
sudo docker restart wecom_it_backend
# 3.2 等 10 秒,检查启动日志
sudo docker logs wecom_it_backend --tail 50
# 期望看到:
# Application startup complete
# Uvicorn running on http://0.0.0.0:8000
# 没有 "ModuleNotFoundError" / "relation already exists" / "Restarting" 循环
# 3.3 健康检查
sudo docker ps | grep wecom_it_backend
# 期望:STATUS = Up X minutes (healthy)
```
**🚨 若 backend 启动失败,回滚**:
```bash
sudo docker tag wecom-it-desk-backend:v0.6.0-backup wecom-it-desk-backend:latest
sudo docker restart wecom_it_backend
```
### 步骤 4/6:上传 4 端 dist 到宿主机
```bash
# 4.1 在本地(Windows)打包 4 端 dist
cd /d/资料/03-项目开发/wecom_it_smart_desk-claude
tar -czf /tmp/frontend-v0.7.0.tar.gz \
frontend-admin/dist frontend-agent/dist frontend-portal/dist frontend-h5/dist
ls -la /tmp/frontend-v0.7.0.tar.gz
# 4.2 上传到生产服务器(走堡垒机)
scp /tmp/frontend-v0.7.0.tar.gz <堡垒机用户>@<堡垒机>:/tmp/
# 4.3 在生产服务器解压
ssh <堡垒机> # 跳到生产
cd /opt/wecom-it-desk
sudo tar -xzf /tmp/frontend-v0.7.0.tar.gz
ls -la frontend-*/dist | head -20
# 期望:每个 dist 都有 index.html + assets/
# 4.4 清理压缩包
sudo rm /tmp/frontend-v0.7.0.tar.gz
```
**🚨 若上传失败,回滚**:
```bash
# 4 端用备份恢复
sudo cp -r /opt/wecom-it-desk/dist-backup-2026-06-21/admin/* /opt/wecom-it-desk/frontend-admin/dist/
sudo cp -r /opt/wecom-it-desk/dist-backup-2026-06-21/agent/* /opt/wecom-it-desk/frontend-agent/dist/
sudo cp -r /opt/wecom-it-desk/dist-backup-2026-06-21/portal/* /opt/wecom-it-desk/frontend-portal/dist/
sudo cp -r /opt/wecom-it-desk/dist-backup-2026-06-21/h5/* /opt/wecom-it-desk/frontend-h5/dist/
```
### 步骤 5/6:应用 nginx access_log 脱敏 + reload
```bash
# 5.1 验证当前 nginx 容器名(下划线不是横杠!)
sudo docker ps | grep wecom_it_nginx
# 期望:0.0.0.0:80->80/tcp wecom_it_nginx
# 5.2 进入容器加 log_format 脱敏配置
sudo docker exec wecom_it_nginx bash -c '
cat > /etc/nginx/conf.d/log-format.conf << "EOF"
log_format secure $remote_addr - $remote_user [$time_local] "$request_method $uri $server_protocol" $status $body_bytes_sent "$http_referer" "$http_user_agent";
access_log /var/log/nginx/access.log secure;
EOF
'
# 验证写入
sudo docker exec wecom_it_nginx cat /etc/nginx/conf.d/log-format.conf
# 5.3 验证配置
sudo docker exec wecom_it_nginx nginx -t
# 期望:nginx: configuration file /etc/nginx/nginx.conf test is successful
# 5.4 reload(不重启容器)
sudo docker exec wecom_it_nginx nginx -s reload
# 5.5 验证 reload 生效
sudo docker exec wecom_it_nginx tail -3 /var/log/nginx/access.log
# 期望:没有 Authorization: Bearer xxx 字样
```
**🚨 若 nginx reload 失败**:
```bash
# 恢复默认 access_log
sudo docker exec wecom_it_nginx bash -c 'echo "access_log /var/log/nginx/access.log;" > /etc/nginx/conf.d/log-format.conf'
sudo docker exec wecom_it_nginx nginx -t
sudo docker exec wecom_it_nginx nginx -s reload
```
### 步骤 6/6:验证域名路由
```bash
# 6.1 验证 4 个 location 都返回 200
curl -I https://<生产域名>/itportal/ # 应 200
curl -I https://<生产域名>/itagent/ # 应 200
curl -I https://<生产域名>/itadmin/ # 应 200
curl -I https://<生产域名>/itdesk/ # 应 200
# 6.2 验证 API 端点
curl https://<生产域名>/api/health
# 期望:{"code":0,"data":{"status":"ok"}}
# 6.3 验证扫码登录端点
curl -X POST https://<生产域名>/api/auth_qrcode/create -H "Content-Type: application/json" -d '{}'
# 期望:{"code":0,"data":{"ticket":"...","qrcode_url":"...","expires_in":120}}
# 6.4 验证 MFA 端点(无 token 应 401)
curl https://<生产域名>/api/mfa/status
# 期望:401 Unauthorized
```
---
## 🟡 部署后 必做(用户/QA 验收)
`docs/E2E-CHECKLIST-v0.7.0.md` 35 项,逐项打勾。
**关键项**:
- [ ] 浏览器扫码登录全流程(5 子项)
- [ ] MFA 绑定 + 30 分钟有效期
- [ ] 高危操作守卫(5 类端点)
- [ ] WS 推送无 missing argument 错误
- [ ] 消息 ID 改为 UUID,无 500
- [ ] nginx access_log 无 Authorization/Cookie
---
## 🔴 部署后 1 周观察(用户拍板)
- 一切正常 → 清理 `/opt/wecom-it-desk/dist-backup-2026-06-21/``~/Downloads/patch1/`
- 任何 regression → 用 `DEPLOY-LOGIN-MIGRATION-v0.7.0.md` 末尾的"回滚预案"恢复
---
## 📊 部署时间预估
| 步骤 | 预计时间 | 风险 |
|---|---|---|
| 1. 备份 | 1 min | 低 |
| 2. migration | 1 min | 中(若冲突需手动) |
| 3. 重启 backend | 2 min(含等健康) | 中(若镜像问题需回滚) |
| 4. 上传 4 端 | 5 min(含上传) | 低 |
| 5. nginx reload | 1 min | 低 |
| 6. 验证 | 5 min | 低 |
| **总计** | **15 min** | |
---
## 🆘 紧急联系人
- 部署问题:本会话 + Claude
- backend 代码:Claude session
- 生产服务器:IT 基础设施组
@@ -0,0 +1,87 @@
# 堡垒机运维工具 (jumpserver-ops)
## 概述
通过 JumpServer 堡垒机自动化执行远程命令、文件上传下载。统一入口为 `jms_ops.py`,支持 4 种操作模式。
## 连接方式决策
| 场景 | 连接方式 |
|------|----------|
| **默认** | 通过堡垒机跳转(大多数内网服务器) |
| **例外** | 直连(NAS、开发机等)需单独配置 |
> **规则**:默认都需要通过堡垒机,除非明确告知某台服务器是直连。
## 使用方法
### 1. 远程命令执行(推荐)
```bash
# 单命令(纯文本输出)
python scripts/jms_ops.py exec -c "docker ps"
# 多命令串行(一次登录,5x 提速)
python scripts/jms_ops.py exec -c "hostname" -c "uptime" -c "docker ps"
# 并行模式(不冲突的长命令)
python scripts/jms_ops.py exec -c "docker logs nginx --tail 100" -c "df -h" --parallel
```
### 2. 批量命令
```bash
python scripts/jms_ops.py batch -f commands.txt
```
### 3. 文件上传
```bash
# 本地 → 堡垒机 → 目标服务器
python scripts/jms_ops.py upload 本地文件.conf /tmp/远程路径.conf
```
### 4. 文件下载
```bash
# 目标服务器 → 堡垒机 → 本地
python scripts/jms_ops.py download /远程路径.conf ./本地文件.conf
```
## 方案选择依据
| 需求 | 推荐方案 | 速度 |
|------|----------|------|
| 执行命令获取文本结果 | v16 REST API + plink PTY | ~15s |
| 执行命令看界面效果 | v10 Web CLI(截图) | ~60s |
| 小文件传输 (<100KB) | upload/download | - |
| 大文件传输 | elFinder Web UI | - |
## 目标服务器配置
当前预设目标:`hz-oa-ai-g-dataquery-90-5-110` (10.90.5.110)
新增直连服务器时,需提供:
- IP 地址
- 端口(默认 22
- 用户名
- 认证方式(密码或 SSH 密钥)
## 故障排除
| 问题 | 解决方法 |
|------|----------|
| 登录失败 | 检查 `config/jumpserver_config.json` 密码是否正确(Base64 编码) |
| MFA 失败 | 确认 `scripts/otp_secret.key` 存在且系统时间准确 |
| 资产未找到 | 确认目标名称与 JumpServer 中显示一致 |
## 脚本位置
```
C:\Users\simon\.workbuddy\skills\jumpserver-ops\scripts\jms_ops.py
```
## 相关文档
- [服务器部署手册](./服务器部署手册.md)
- [版本更新说明](./05-版本更新说明-v1.1.0-20260614.md)
+286
View File
@@ -0,0 +1,286 @@
# 企微智能IT支持服务台 — 服务器部署指南
> 目标服务器:`10.90.5.110`Linux
> 域名:`itsupport.servyou.com.cn`
> 更新日期:2026-06-12
---
## 一、前置条件
- [x] 服务器可访问内网(火绒 `huorong.oa.servyou-it.com`、Dify `yw-dify.dc.servyou-it.com`
- [ ] 服务器已安装 Docker + Docker Compose
- [ ] 域名 `itsupport.servyou.com.cn` DNS 已解析到 `10.90.5.110`(或先用 IP 访问)
---
## 二、安装 Docker(如已安装跳过)
### 2.1 检查是否已安装
```bash
docker --version # 应显示 Docker version 24.x+
docker compose version # 应显示 Docker Compose version v2.x+
```
如果已安装,跳到第三步。
### 2.2 安装 DockerCentOS/RHEL
```bash
# 1. 卸载旧版本(如有)
sudo yum remove -y docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-engine
# 2. 安装 yum 工具
sudo yum install -y yum-utils
# 3. 添加 Docker 官方仓库(国内用阿里云镜像加速)
sudo yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
# 4. 安装 Docker Engine + Compose 插件
sudo yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# 5. 启动 Docker 并设置开机自启
sudo systemctl start docker
sudo systemctl enable docker
# 6. 验证安装
docker --version
docker compose version
```
### 2.3 安装 DockerUbuntu/Debian
```bash
# 1. 卸载旧版本
sudo apt-get remove -y docker docker-engine docker.io containerd runc
# 2. 安装依赖
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
# 3. 添加 Docker GPG 密钥
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# 4. 添加仓库
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://mirrors.aliyun.com/docker-ce/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 5. 安装
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# 6. 启动
sudo systemctl start docker
sudo systemctl enable docker
# 7. 验证
docker --version
docker compose version
```
### 2.4(可选)非 root 用户使用 Docker
```bash
# 将当前用户加入 docker 组,避免每次 sudo
sudo usermod -aG docker $USER
# 重新登录生效
newgrp docker
```
---
## 三、上传部署包
### 3.1 在服务器创建目录
```bash
sudo mkdir -p /opt/wecom-it-desk
sudo chown $USER:$USER /opt/wecom-it-desk
```
### 3.2 上传文件
在本地 Windows 用 SCP/SFTP 上传部署包:
```powershell
# 方法1:用 scp 命令(Git Bash 或 PowerShell
scp it-smart-desk-server-deploy.zip user@10.90.5.110:/opt/wecom-it-desk/
# 方法2:用 WinSCP / FileZilla 图形化工具上传
```
### 3.3 解压
```bash
cd /opt/wecom-it-desk
unzip it-smart-desk-server-deploy.zip
# 解压后目录结构:
# /opt/wecom-it-desk/
# ├── docker-compose.yml
# ├── .env
# ├── nginx/
# │ └── nginx.conf
# ├── backend/
# │ ├── Dockerfile
# │ ├── app/
# │ ├── alembic/
# │ ├── alembic.ini
# │ └── requirements.txt
# ├── frontend-h5/dist/
# ├── frontend-agent/dist/
# └── frontend-admin/dist/
```
---
## 四、修改配置
### 4.1 编辑环境变量
```bash
cd /opt/wecom-it-desk
vim .env
```
**必须确认的配置项:**
| 配置项 | 当前值 | 说明 |
|--------|--------|------|
| `WECOM_CORP_ID` | `wwa8c87970b2011f41` | 企微企业ID |
| `WECOM_AGENT_ID` | `1000133` | 企微应用AgentId |
| `WECOM_SECRET` | `EOtQsl...` | 企微应用Secret |
| `MOCK_LOGIN_ENABLED` | `true` | 测试阶段用 true,正式上线改为 false |
| `DIFY_API_KEY` | `http://...` | Dify AI 服务 Key |
| `POSTGRES_PASSWORD` | `wecom_secret_2026` | 数据库密码(首次初始化后不可改) |
### 4.2 确认域名解析
```bash
# 测试域名是否指向本机
ping itsupport.servyou.com.cn
# 如果还没配 DNS,可以先在 .env 中把 CORS_ORIGINS 改为:
# CORS_ORIGINS=http://10.90.5.110
```
---
## 五、启动服务
### 5.1 首次启动
```bash
cd /opt/wecom-it-desk
# 构建后端镜像 + 启动所有容器
docker compose up -d --build
# 首次启动需要 2-3 分钟(下载镜像 + 构建后端 + 数据库迁移)
```
### 5.2 查看启动状态
```bash
# 查看所有容器状态(应全部 healthy/running
docker compose ps
# 查看实时日志(Ctrl+C 退出)
docker compose logs -f
# 只看后端日志
docker compose logs -f backend
```
**预期输出(`docker compose ps`):**
```
NAME STATUS PORTS
wecom_it_postgres Up (healthy) 5432/tcp
wecom_it_redis Up (healthy) 6379/tcp
wecom_it_backend Up (healthy) 8000/tcp
wecom_it_nginx Up (healthy) 0.0.0.0:80->80/tcp
```
### 5.3 验证服务
```bash
# 1. 健康检查
curl http://localhost/itdesk/health
# 预期:healthy
# 2. 后端 API
curl http://localhost/api/health
# 预期:{"status":"ok"}
# 3. 浏览器访问
# H5 员工端:http://itsupport.servyou.com.cn/itdesk/
# 坐席工作台:http://itsupport.servyou.com.cn/itagent/
# 管理后台:http://itsupport.servyou.com.cn/itadmin/
```
---
## 六、常用运维命令
```bash
cd /opt/wecom-it-desk
# 重启所有服务
docker compose restart
# 只重启后端(代码更新后)
docker compose restart backend
# 查看某个容器的日志
docker compose logs -f --tail=100 backend
# 进入后端容器调试
docker compose exec backend /bin/sh
# 停止所有服务
docker compose down
# 停止并删除数据卷(⚠️ 会清空数据库!)
docker compose down -v
# 查看磁盘使用
docker system df
```
---
## 七、代码更新流程
当有新代码需要部署时:
```bash
# 1. 上传新的部署包,覆盖旧文件
# 2. 重新构建并启动
cd /opt/wecom-it-desk
docker compose up -d --build
# 如果只有前端更新,不需要重建后端镜像:
docker compose up -d --no-deps --build nginx
```
---
## 八、故障排查
| 问题 | 排查命令 | 常见原因 |
|------|----------|----------|
| 容器反复重启 | `docker compose logs backend` | 数据库连接失败、环境变量缺失 |
| 页面空白 | `docker compose logs nginx` | 前端 dist 目录为空或路径错误 |
| API 404 | `curl http://localhost:8000/health` | 后端未启动或 nginx proxy 配置错误 |
| 数据库连接失败 | `docker compose logs postgres` | POSTGRES_PASSWORD 与 DATABASE_URL 不一致 |
| 端口被占用 | `sudo lsof -i :80` | 其他服务占用 80 端口 |
---
## 九、安全建议(后续)
- [ ] 配置 HTTPS(Nginx 反代 + 证书,或使用反向代理)
- [ ] 修改默认数据库密码
- [ ] 关闭 Mock 登录(`MOCK_LOGIN_ENABLED=false`
- [ ] 限制 80 端口访问来源(防火墙规则)
@@ -0,0 +1,595 @@
# 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 秒**:
```bash
# 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` 看到 `Restarting``Exited` | 见 §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 用户):
```bash
# 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 命令):
>
> ```bash
> # 直接用 docker commit 出来的镜像
> docker run -d --name wecom_it_backend <完整原参数> \
> wecom-it-desk-backend:v0.7.0-backup-pre-qrfix
> ```
### 1.2 F2: 模块导入报错回滚
**原因**: `auth_qrcode.py``qrcode_service.py` 上传时 base64 解码坏掉 / Python 缩进错。
**检测**:
```bash
docker logs wecom_it_backend --tail 30 2>&1 | grep -E "(ModuleNotFoundError|ImportError|SyntaxError|IndentationError)"
```
**回滚命令**(比 F1 简单,只用覆盖文件 + 重启,不用换镜像):
```bash
# 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 缺包)。
**检测**:
```bash
# 拿返回内容
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 坑)。
**检测**:
```bash
curl -k -X POST https://itsupport.servyou.com.cn/api/auth_qrcode/create | python -m json.tool
# 看 data 字段里有没有 "qrcode_png_base64"
# 没有 → 文件没真覆盖
```
**回滚命令**(强制覆盖):
```bash
# 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 容器,或容器网络断了。
**检测**:
```bash
# 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 → 网络都不通
```
**回滚命令**(全链路重拉):
```bash
# 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 被别的进程占。
**检测**:
```bash
docker logs wecom_it_backend --tail 50 2>&1 | grep -i "address already in use"
# 或
ss -tlnp | grep 8000
```
**回滚命令**:
```bash
# 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`,可能不是预期的。
**检测**:
```bash
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):
```bash
# 拿到回滚镜像的精确 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 容器层
```bash
# 状态(看是不是 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 进程层
```bash
# 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 端点层
```bash
# 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 端点)
```bash
# 标准 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`):
```json
{
"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 步)
```bash
# 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
```bash
# 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 清理多余镜像(谨慎)
```bash
# 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 清理宿主机临时文件
```bash
# 删 /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`
```bash
#!/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 终端):
```bash
# 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` / `-e` 在 `docker 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` 确认。
@@ -0,0 +1,256 @@
# Nginx 域名路由分发配置(Phase 1.3 task #16)
> 创建:2026-06-21
> 适用版本:v0.7.0+ (Phase 1.3 扫码登录上线后)
## 🎯 目标
不同入口域名/子路径 → 不同前端应用,但所有请求共用同一个后端 API。
| 入口 | URL | 前端应用 | 用途 |
|---|---|---|---|
| **坐席端** | `https://itsupport.servyou.com.cn/itagent/` | `frontend-agent/dist` | 坐席工作台 |
| **管理端** | `https://itsupport.servyou.com.cn/itadmin/` | `frontend-admin/dist` | 管理后台 |
| **Portal 统一入口** | `https://itsupport.servyou.com.cn/itportal/` | `frontend-portal/dist` | 扫码登录 + 多角色选择 |
| **H5 员工端** | `https://itsupport.servyou.com.cn/itdesk/` | `frontend-h5/dist` | 员工端(企微内) |
> **两种方案**:单域名多路径(本项目当前)+ 多子域名(可选升级)
---
## 🅰️ 方案 A:单域名 + 多子路径(推荐,运维简单)
### nginx server block
```nginx
server {
listen 443 ssl;
server_name itsupport.servyou.com.cn;
# SSL 证书(由公司统一管理)
ssl_certificate /etc/nginx/certs/itsupport.servyou.com.cn.crt;
ssl_certificate_key /etc/nginx/certs/itsupport.servyou.com.cn.key;
# 通用安全头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# ========================================================================
# 1. Portal 统一入口(扫码登录)
# ========================================================================
location /itportal/ {
alias /opt/wecom-it-desk/frontend-portal/dist/;
try_files $uri $uri/ /itportal/index.html;
# 允许企业微信 OAuth 回调(测试期)
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# ========================================================================
# 2. 坐席工作台
# ========================================================================
location /itagent/ {
alias /opt/wecom-it-desk/frontend-agent/dist/;
try_files $uri $uri/ /itagent/index.html;
}
# ========================================================================
# 3. 管理后台
# ========================================================================
# IP 白名单(临时方案,v1.0 前收窄 — 见 ip-whitelist-trust-proxies-todo.md)
location /itadmin/ {
allow 0.0.0.0/0; # ⚠️ 临时全开
# allow 10.90.0.0/16; # TODO 收窄到内网
# allow 115.236.188.3; # 公网入口 IP
alias /opt/wecom-it-desk/frontend-admin/dist/;
try_files $uri $uri/ /itadmin/index.html;
}
# ========================================================================
# 4. H5 员工端
# ========================================================================
location /itdesk/ {
alias /opt/wecom-it-desk/frontend-h5/dist/;
try_files $uri $uri/ /itdesk/index.html;
# 允许嵌入到企微 WebView
add_header X-Frame-Options "ALLOW-FROM https://work.weixin.qq.com" always;
}
# ========================================================================
# 5. 后端 API(4 个端共用)
# ========================================================================
location /api/ {
# 管理端 API 严格白名单
location /api/admin/ {
allow 0.0.0.0/0; # ⚠️ 临时全开
# allow 10.90.0.0/16; # TODO 收窄
# allow 115.236.188.3;
proxy_pass http://wecom_it_backend;
}
# 其他 API 放行
proxy_pass http://wecom_it_backend;
}
# ========================================================================
# 6. WebSocket(坐席端 WS-01 鉴权)
# ========================================================================
location /ws/ {
proxy_pass http://wecom_it_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# WS 心跳
proxy_read_timeout 600s;
}
# ========================================================================
# 7. 静态资源(图片/上传文件)
# ========================================================================
location /api/media/ {
proxy_pass http://wecom_it_backend;
proxy_set_header Host $host;
# 上传文件 30 天缓存
expires 30d;
add_header Cache-Control "public, immutable";
}
# ========================================================================
# 8. 根路径 → Portal 统一入口
# ========================================================================
location = / {
return 302 /itportal/;
}
}
# upstream 后端(内网容器)
upstream wecom_it_backend {
server 127.0.0.1:8000; # 容器映射到宿主机的端口
}
```
### 部署步骤
```bash
# 1. 上传 dist 文件(各前端 build 产物)
scp -r frontend-portal/dist root@10.90.5.110:/opt/wecom-it-desk/frontend-portal/
scp -r frontend-agent/dist root@10.90.5.110:/opt/wecom-it-desk/frontend-agent/
scp -r frontend-admin/dist root@10.90.5.110:/opt/wecom-it-desk/frontend-admin/
scp -r frontend-h5/dist root@10.90.5.110:/opt/wecom-it-desk/frontend-h5/
# 2. 上传 nginx 配置(本地 + 堡垒机 PuTTY)
# 参考:feedback-putty-not-openssh.md(用 PuTTY 操作)
# 3. 验证配置
sudo nginx -t
# 4. reload
sudo nginx -s reload
# 5. 验证(本地或企微)
curl -I https://itsupport.servyou.com.cn/itportal/
```
---
## 🅱️ 方案 B:多子域名(可选升级,需要 DNS 解析)
| 子域名 | 解析到 | 用途 |
|---|---|---|
| `portal.itsupport.servyou.com.cn` | nginx:443 | 统一入口 |
| `agent.itsupport.servyou.com.cn` | nginx:443 | 坐席工作台 |
| `admin.itsupport.servyou.com.cn` | nginx:443 | 管理后台(内网白名单) |
| `h5.itsupport.servyou.com.cn` | nginx:443 | H5 员工端 |
### 优点
- 跨域 cookie 隔离更清晰
- 每个子域可独立上 HTTPS 证书
- 内网白名单更容易配置(直接 deny all 到 admin.*)
### 缺点
- 需要运维额外加 4 个 A 记录
- 前端跨域 API 调用要 CORS 配全
- 坐席/管理员跨域切换要 CORS preflight
**当前 v0.7.0 推荐方案 A**,v1.0 再考虑方案 B。
---
## 🔄 扫码登录流程(方案 A 下)
```
[1] 用户访问 https://itsupport.servyou.com.cn/itagent/
→ nginx 命中 location /itagent/ → 返回 frontend-agent/dist/index.html
→ 前端路由守卫检查 localStorage.agent_token,没有 → 跳 /itportal/
[2] 用户访问 https://itsupport.servyou.com.cn/itportal/
→ nginx 命中 location /itportal/ → 返回 frontend-portal/dist/index.html
→ QrcodeLogin.vue 显示二维码
[3] 员工用企微扫码
→ 企微 OAuth 回调到后端 → 后端写 Redis qrcode:scan:{ticket}
→ Portal 轮询 /api/auth_qrcode/poll/{ticket} → 拿到 status=scanned
→ UI 显示"请在手机上确认登录"
[4] 员工在手机上点"确认登录"
→ 后端 /api/auth_qrcode/confirm → 创建 token → 写 Redis qrcode:confirm:{ticket}
→ Portal 轮询拿到 status=confirmed + token + roles
[5] Portal 按角色分发(见 QrcodeLogin.vue dispatchToRole)
- 只有 agent → window.location.href = /itagent/?token=xxx
- 只有 admin → window.location.href = /itadmin/?token=xxx
- admin + agent → window.location.href = /itportal/select(让用户选)
- 默认 user → window.location.href = /itdesk/?token=xxx
[6] 目标端 Login.vue 读 ?token=xxx 写入 localStorage + 跳 /workspace
```
---
## ⚠️ 已知问题 & TODO
| 问题 | 状态 | 备注 |
|---|---|---|
| `/itadmin/` IP 白名单临时全开 | 🟡 临时 | v1.0 前必须收窄(见 `ip-whitelist-trust-proxies-todo.md`) |
| `/api/admin/` IP 白名单临时全开 | 🟡 临时 | 同上 |
| H5 端需要企微内访问 | 🟢 保持 | 用户决策,H5 仍在企微内是主场景 |
| 跨子路径刷新 404 | 🟢 已处理 | `try_files $uri $uri/ /itagent/index.html` |
| 静态资源 cache | 🟡 待优化 | 可加 version hash 强制刷新 |
| admin Login.vue 仍用表单 | 🟡 待改 | 后续 task:重写 admin Login 为扫码 UI |
---
## 🧪 验证清单
部署完成后,在以下场景测试:
- [ ] 浏览器直接访问 `/itportal/` → 显示扫码二维码
- [ ] 用企微扫码 + 确认 → Portal 自动跳到对应端
- [ ] 坐席(只有 agent 角色)扫码 → 自动跳 `/itagent/?token=xxx` → 自动登录进 /workspace
- [ ] 管理员(只有 admin 角色)扫码 → 自动跳 `/itadmin/?token=xxx` → 进 admin dashboard
- [ ] 多角色用户(admin + agent)扫码 → 跳 `/itportal/select` → 看到选择页
- [ ] H5(企微内) → 仍走企微 OAuth,扫码二维码区域正常
- [ ] 浏览器直接访问 `/itagent/workspace`(没 token)→ 跳 `/itportal/`
- [ ] 扫码登录 120s 过期 → UI 显示"已过期,点击刷新"
---
## 📚 相关文档
- [project-knowledge-base.md](../memory/project-knowledge-base.md) — 项目知识库
- [feedback-wecom-only-external-urls.md](../memory/feedback-wecom-only-external-urls.md) — 企微入口约束(部分解除)
- [phase1-progress.md](../memory/phase1-progress.md) — Phase 1+2 进度
- [deployment.md](../memory/deployment.md) — 部署经验
- [nginx-container-name-wecom-it-nginx.md](../memory/nginx-container-name-wecom-it-nginx.md) — 容器名坑
---
**变更历史**:
- 2026-06-21 创建(Phase 1.3 task #16)
+43
View File
@@ -0,0 +1,43 @@
# H5用户端原型图 → Vue3代码实现概览
## 完成时间
2026-06-09
## 变更摘要
根据已锁定的原型图 v1.1 修复版,将 H5 用户端设计实现为 Vue3 代码。
## 修改文件清单
### 1. `frontend-h5/src/components/chat/ChatPanel.vue`
- **标题栏重构**:左侧(标题 + 坐席在线/离线状态胶囊) + 右侧(🔔呼叫按钮 + 主题切换)
- **🔔摇铃按钮**:从输入栏移至标题栏(桌面端+手机端统一)
- **排查步骤固定顶部**:从消息列表内移出,固定在标题栏下方、所有消息之上,不随滚动消失
- **移除 InputBar 事件**:不再需要 @call-agent 事件(摇铃直接在 ChatPanel 内控制)
### 2. `frontend-h5/src/components/chat/InputBar.vue`
- **移除摇铃按钮**:删除 🔔 摇铃按钮及相关 CSSbell-btn/bell-icon/bell-idle/bell-ring 动画)
- **新增工具栏**:😊表情 / 🖼️图片 / 📎文件 / 📸拍照(4个圆形按钮)
- **布局改为两行**:工具栏(上) + 输入行(输入框+发送按钮)(下)
- **新增方法**handleEmoji/handleImage/handleFile/handleCamera(阶段二实现具体功能)
- **引导条文案更新**:"点击标题栏铃铛呼叫 IT 坐席"
### 3. `frontend-h5/src/components/assistant/RightPanel.vue`(新建)
- **三段式面板**:AI推送区 / 常用资源标签页 / 趣味问答
- **AI推送区**3种卡片类型(guide/process/download) + 动态图标+颜色
- **常用资源**:2个Tab(申请流程/必装软件) + 资源列表
- **趣味问答**:题目+4选项+积分+答题结果反馈
- **阶段一静态数据**,阶段二接入 Dify 动态推送
### 4. `frontend-h5/src/views/ChatView.vue`
- **替换右侧面板**AiHelperPanel → RightPanel(三段式面板)
- **响应式断点**:从768px改为500px(与原型图对齐)
- **移动端**<500px 不显示右侧面板
- **拖拽逻辑修复**:只固定左侧宽度,右侧 flex:1 自动填满(消除拖拽后空白)
- **移除浮动按钮**:不再需要移动端AI助手浮动按钮
### 5. `frontend-h5/src/stores/conversation.ts`
- **新增 agentOnline 状态**:默认true,阶段一简化处理
- **暴露到 return 语句**:使组件可以访问
## 构建验证
`npx vite build` 构建成功,无编译错误
@@ -0,0 +1,362 @@
# nginx 真实 IP 还原 — 生产部署(小白友好版)
> 术语速查:**nginx** = 你这台服务器的"门卫",负责把用户请求分发给后端 / 把静态文件返回给浏览器
> **配置** = nginx 的工作规则,改配置 = 改门卫的工作方式
---
## 我们要做啥(整体目标)
**一句话目标**:`https://itsupport.servyou.com.cn/itadmin/` 之前返回 403(被门卫拦了),原因是门卫把"代理服务器 IP"当成了"用户 IP",而代理 IP 不在白名单里。这次我们改门卫的规则,让它从请求头里读"真实用户 IP"。
**一共 7 个动作**:
| # | 动作 | 大概多久 | 风险 |
|---|---|---|---|
| 1 | PuTTY 连上服务器 | 1 分钟 | ⚪ 无风险 |
| 2 | 备份当前配置 | 几秒 | 🟢 备份原文件,可还原 |
| 3 | 写入 13 行新规则 | 几秒 | 🟡 改配置,但有备份 |
| 4 | 确认写入正确 | 几秒 | ⚪ 只读不写 |
| 5 | 检查配置语法 | 几秒 | ⚪ 只读不写 |
| 6 | 让 nginx 重新读规则 | 1 秒 | 🟡 短暂重载,服务不中断 |
| 7 | 浏览器看效果 | 几秒 | ⚪ 只读 |
**总耗时**:第一次大概 5-10 分钟;熟练了 2 分钟
**整体风险**:🟢 **低** — 每一步都给了"回滚"按钮,改坏了随时能恢复
---
## PuTTY 是啥?在哪儿打开?
**PuTTY** = 一个 SSH 客户端软件,作用是让你从你的 Windows 电脑远程连到公司的 Linux 服务器
**打开方式**:
-`Win 键` → 输入 `putty` → 回车
- 或者开始菜单 → 找到 PuTTY 图标
打开后会看到一个灰底配置界面,我们要填 4 项:
```
┌──────────────────────────────────────┐
│ Host Name (or IP address) │ ← 填: 10.212.189.210
│ Port │ ← 填: 2222
│ Connection type │ ← 选: SSH(默认就是)
│ Saved Sessions │ ← 填: wecom-bastion(起个名)
└──────────────────────────────────────┘
点 Save 保存 → 点 Open 开始连接
```
连接后会黑底白字,提示 `login as:` → 输入 `sxn` 回车 → 提示 `password:` → 输入你的堡垒机密码(输入时屏幕不显示,正常,输完回车就行)
**注意**:输错密码不会锁账号,直接重新输
---
## 动作 1:PuTTY 连服务器(⚪ 无风险)
> **为啥要连服务器?**:改配置必须在服务器上操作,你 Windows 这边只是"遥控器"
连上堡垒机后,黑底白字会显示一个类似 `sxn@jump-host:~$` 的提示符,说明你已经到堡垒机了。
**决策树**:
```
你现在看到了堡垒机提示符(类似 sxn@jump-host:~$)
├─ 是 → 在 PuTTY 里继续输入下面命令
└─ 否 → 截图发给我,卡哪儿了
```
贴下面的命令(右键 = 粘贴,Enter = 执行):
```bash
# 从堡垒机跳到真正的生产服务器
ssh sxn@10.90.5.110
```
回车后可能要输密码(堡垒机和目标机密码可能不同,试一下你之前用过的那个)
**✅ 成功长这样**:
```text
sxn@prod-server:~$
```
**❌ 失败常见**:
- `Permission denied` → 密码错了,重输
- `Connection timed out` → 网络问题,可能 VPN 没连
- 卡住不动 → 可能需要输 `yes` 确认服务器指纹,看到 `(yes/no/[fingerprint])?` 就输 `yes` 回车
---
## 动作 2:备份当前配置(🟢 低风险,改坏了能还原)
> **为啥要备份?**:运维铁律 — **改任何东西之前先备份**,这样改坏了能用备份还原,不会把生产搞挂
```bash
# 进入 nginx 配置所在目录
cd /opt/wecom-it-desk/nginx
# 复制一份当前配置,文件名带当前时间(分),方便区分
sudo cp nginx.conf nginx.conf.bak-$(date +%H%M)
# 列出所有备份文件,确认刚才那行成功
ls -la nginx.conf.bak-*
```
**为啥用 `$(date +%H%M)`?**:这个写法会自动拼上当前时间(比如 1430 表示 14:30),每次备份文件名都不一样,不会覆盖之前的备份
**✅ 成功长这样**:
```text
-rw-r--r-- 1 root root 4821 Jun 15 14:30 nginx.conf.bak-1430
```
**❌ 失败常见**:
- `cp: cannot stat 'nginx.conf'` → 当前不在 nginx 目录,先 `cd /opt/wecom-it-desk/nginx` 进去
- `Permission denied` → 缺 `sudo`,命令前面加 `sudo` 重试
---
## 动作 3:写入 13 行新规则(🟡 中风险,但有备份兜底)
> **写入啥?**:13 行 nginx 配置,告诉 nginx"从请求头 X-Forwarded-For 里读真实用户 IP"
>
> **为啥要这样做?**:用户通过公司 WAF/堡垒机访问,WAF 会把真实 IP 放在 `X-Forwarded-For` 请求头里,但 nginx 默认只看直连 IP,所以才误判 403
**重要**:把下面**从 `cat > /tmp/patch.py``PYEOF`** 的**整段**一次性粘贴进 PuTTY(右键 = 粘贴)。整段会作为一条命令执行。
```bash
# 创建一个 python 脚本到 /tmp/patch.py
cat > /tmp/patch.py << 'PYEOF'
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)))
PYEOF
# 运行这个 python 脚本,它会自动把上面那 13 行插入到 nginx.conf
sudo python3 /tmp/patch.py
```
**术语解释**:
- `cat > /tmp/patch.py` → 创建一个文件,内容是后面所有内容
- `<< 'PYEOF' ... PYEOF` → 这种写法叫 **heredoc**(直译"这里是文档"),作用是把多行文字原样写入文件
- `sudo` → 以管理员身份运行(改系统文件需要权限)
**✅ 成功长这样**:
```text
patched, +492 bytes
```
**❌ 失败常见**:
- `Permission denied` → 缺 `sudo`,或者 nginx.conf 不存在
- `NameError: name 'fp' is not defined` → heredoc 没贴完整,最末尾的 `PYEOF` 没贴上
- 没任何输出 → python 没运行,看光标有没有新行,可能没回车
---
## 动作 4:确认写入正确(⚪ 无风险,只读)
> **为啥要确认?**:虽然脚本说写入了,但**人眼看到才真的算**。这步只读不写,放心跑
```bash
# 在 nginx.conf 里搜索"真实 IP 还原"关键字,并显示后面 13 行
sudo grep -A 13 "真实 IP 还原" /opt/wecom-it-desk/nginx/nginx.conf
```
**✅ 成功长这样**(应该看到完整 13 行):
```nginx
# 真实 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;
```
**❌ 失败**:
- 啥也没输出 → 写入失败,回到动作 3 重做
- 只输出一两行 → heredoc 没贴全,需要回滚后重来
---
## 动作 5:检查配置语法(⚪ 无风险,只读不执行)
> **为啥要检查?**:这个命令 nginx 会"假装"按新配置启动,只检查语法,不会真的重启。**通过 = 配置写得对,放心用;不通过 = 写得有问题,继续走会出问题**
```bash
# 在 nginx 容器(就是跑 nginx 服务的那个小 Linux)内,做配置语法检查
docker compose exec nginx nginx -t
```
**术语解释**:
- `docker compose` → 管理这台服务器上所有"容器"的命令
- `exec` → "钻进"某个容器里执行命令
- `nginx -t` → nginx 自带的"语法检查"工具(全称 `--test`)
**✅ 成功长这样**:
```text
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
```
**❌ 失败**(`test is successful` 没出现):
- `unexpected "}"` / `unknown directive` → 写错字了,回去动作 4 看看哪里对不上
- **直接停下,不要继续** → 复制错误信息贴回给我
---
## 动作 6:让 nginx 重新读规则(🟡 中风险,但服务不中断)
> **"重新读"是啥意思?**:nginx 现在用的还是旧配置,我们让 nginx 不用重启(不会断服务)就把新配置加载进来。这个动作叫"热加载"或 "reload"
>
> **会断网吗?**:不会,reload 是无缝的,用户那边无感知
```bash
# 通知 nginx 容器内的 master 进程重新读配置
docker compose exec nginx nginx -s reload
```
**术语解释**:`-s reload` = 发信号(英文 signal)给 nginx,告诉它"重读配置"
**✅ 成功长这样**(没报错即成功):
```text
2026/06/15 14:35:12 [notice] 1#1: signal process started
```
**❌ 失败**:
- `nginx: [error]` 开头 → 配置没通过,回去动作 5 看哪里没对
- 啥也没输出 → 命令没执行,看光标位置
---
## 动作 7:浏览器看效果(⚪ 无风险)
**为啥这步是浏览器而不是 curl?**:curl 看响应头,浏览器看真实页面。**人眼看到才作数**
**操作步骤**:
1. 打开浏览器
2. **开隐身模式**(`Ctrl + Shift + N`,Chrome / Edge 都是这个快捷键)
- **为啥要隐身?**:隐身模式不读本地缓存,看到的就是 nginx **当下**返回的
3. 地址栏输入 `https://itsupport.servyou.com.cn/itadmin/`
4. 按回车
**✅ 成功长这样**:
- 页面正常显示
-`F12` 打开开发者工具 → `Network` 选项卡 → 顶部那一行状态码是 **200**(不是 403)
**❌ 失败**:
- 仍然是 403 → 见下面"如果还是 403"段
- 502 / 504 → nginx 后面那个服务挂了,贴错误给我
- 页面打不开(连接被拒) → DNS 没配,联系 IT 运维
---
## 如果还是 403 — 看 WAF 出口 IP(诊断)
> **啥是 WAF?**:公司部署在 nginx 前面的"统一入口",所有用户请求先经过 WAF 再到 nginx。WAF 自己的 IP 不一定在你写的 4 段内网里,所以还得加
```bash
# 看 nginx 最后 20 条访问日志,找 $remote_addr 是不是 WAF 的 IP
docker compose exec nginx tail -20 /var/log/nginx/access.log
```
**日志长这样**:
```text
10.80.5.123 - - [15/Jun/2026:14:35:45 +0800] "GET /itadmin/ HTTP/1.1" 403 ...
^^^^^^^
这就是 $remote_addr
```
把那个 IP 数字(比如 `10.80.5.123`)贴回给我,我会:
1. 给你追加一行 `set_real_ip_from 10.80.5.123;`
2. 让你重跑动作 5 + 动作 6
---
## 如果改坏了 — 回滚(啥时候都能用)
> **啥时候用?**:任何一个动作出问题,你都可以直接回滚到动作 2 备份的版本
```bash
# 列出所有备份,挑最近的一个
ls -la /opt/wecom-it-desk/nginx/nginx.conf.bak-*
```
```bash
# 用最近那个备份覆盖当前配置(把 1430 换成上面列出的真实时间)
sudo cp /opt/wecom-it-desk/nginx/nginx.conf.bak-1430 /opt/wecom-it-desk/nginx/nginx.conf
```
```bash
# 重新加载回滚后的配置
docker compose exec nginx nginx -s reload
```
回滚后页面应该回到改之前的状态(403 回来),说明回滚成功
---
## 一张图看懂流程
```
PuTTY 连服务器
备份原配置
写入 13 行新规则
确认写入正确 ──→ ❌ 不对 ──→ 重做写入 / 回滚
│ ✅
检查配置语法 ──→ ❌ 语法错 ──→ 复制错误贴回给我,不要继续
│ ✅
重载 nginx ─────→ ❌ 报错 ──→ 检查容器状态 / 找 Claude
│ ✅
浏览器看效果 ──→ ❌ 还是 403 ──→ 看 WAF 出口 IP,贴给 Claude
│ ✅
🎉 完成
```
---
## 我建议你第一次做
**第一次建议**:动作 1 → 动作 2 → **停一下,截图发给我** → 我确认备份成功 → 你再继续动作 3 之后
**熟练了以后**:一口气跑完动作 1-7,中间不打断
---
## 关联
- 评审报告:`review-p0-security-2026-06-14.md` P0-3
- 待办:`ip-whitelist-trust-proxies-todo.md` — v1.0 前必须收窄 4 段 → 4 个 IP
- 本地配置:`deploy-server/nginx/nginx.conf`(已包含 patch,下次重打包自动带)
- 服务器 IP 变更:`project-production-server-ip-2026-06-15.md` — 10.80.0.136 已下线,用 10.90.5.110
- 客户端约束:`feedback-putty-not-openssh.md` — 用 PuTTY,不用 `ssh -J`
- 命令行规范:`feedback-cmd-step-by-step.md` — 每行一条 + 中文注释
- 小白引导规范:`feedback-beginner-friendly-guide.md` — 讲清目标+风险、术语解释、出错兜底
@@ -0,0 +1,81 @@
# 快速诊断 /itdesk/ 500 错误
**Claude 无法直接 SSH(Windows known_hosts 权限 + 堡垒机交互登录限制),需你跑下面命令并把输出贴回。**
---
## 🚀 一键跑法(推荐)
**完整脚本已写到** `D:\资料\03-项目开发\wecom_it_smart_desk-claude\diagnose-500.sh`(3484 字节)
**步骤**:
1. **上传脚本到服务器**(`/tmp/`):
```powershell
# 你在 PowerShell(堡垒机后的 Windows)跑:
scp "D:\资料\03-项目开发\wecom_it_smart_desk-claude\diagnose-500.sh" user@10.90.5.110:/tmp/
# (用你自己的文件传输方式,因为堡垒机禁 scp ProxyJump)
```
2. **PuTTY 登录**:
- Host:`10.212.189.210`,Port:`2222`,SSH → Open
- 用户 `sxn` + 密码
- 堡垒机内 `ssh sxn@10.90.5.110` 跳目标机
3. **在服务器上跑**:
```bash
sudo cp /tmp/diagnose-500.sh /opt/wecom-it-desk/
cd /opt/wecom-it-desk
bash diagnose-500.sh > /tmp/diag.log 2>&1
cat /tmp/diag.log
```
4. **把 /tmp/diag.log 的内容贴回 Claude**
---
## 🛠️ 或者手敲(精简版)
```bash
# 1. 容器状态
docker compose ps
# 2. dist 目录在不在
ls /opt/wecom-it-desk/frontend-h5/dist/
ls /opt/wecom-it-desk/frontend-h5/dist/assets/
# 3. nginx 容器内能看到 dist 吗
docker compose exec nginx ls /usr/share/nginx/html/itdesk/
docker compose exec nginx ls /usr/share/nginx/html/itdesk/assets/
# 4. SSL 证书
docker compose exec nginx ls /etc/nginx/ssl/
# 5. 直接 curl 测试
curl -ksI https://itsupport.servyou.com.cn/itdesk/ | head -10
curl -ksI https://itsupport.servyou.com.cn/itportal/ | head -10
curl -ksI https://itsupport.servyou.com.cn/itagent/ | head -10
curl -ksI https://itsupport.servyou.com.cn/itadmin/ | head -10
# 6. nginx 日志
docker compose logs --tail=20 nginx
docker compose logs --tail=20 backend
```
---
## 🎯 我会关注
| 现象 | 诊断 |
|---|---|
| `ls /opt/wecom-it-desk/frontend-h5/dist/` 显示 **No such file** | 部署包没含 H5 dist(nginx 会 404 → 但一般不会 500) |
| `docker compose exec nginx ls /usr/share/nginx/html/itdesk/` 失败 | nginx 容器挂载路径错了,或 dist 没拷贝进去 |
| `curl -ksI https://itsupport.servyou.com.cn/itdesk/` 返回 **HTTP/1.1 500** | 后端代理或 SPA 内部错误 |
| `curl -ksI https://itsupport.servyou.com.cn/itportal/` 也 500 | **全站问题**,看 nginx 日志 |
| `curl -ksI https://itsupport.servyou.com.cn/itportal/` 200 但 /itdesk/ 500 | **H5 端特定问题**,看 nginx 容器内的文件 |
| nginx 错误日志有 **proxy_pass 错误** | 后端没启动或端口不通 |
| nginx 错误日志有 **"rewrite ... cycle"** | try_files 死循环,需修 nginx 配置 |
---
> 把输出贴回 Claude 后,我会精确定位 500 根因并给出最小修复。
@@ -0,0 +1,54 @@
# 手敲 6 段命令(脚本上传失败时用)
**PuTTY 登录**:
- Host:`10.212.189.210`,Port:`2222`,SSH → Open
- 用户 `sxn` + 密码
- 堡垒机内再 `ssh sxn@10.90.5.110` 跳目标机
**逐段跑(每段贴回输出)**:
```bash
# === 段 1: 容器 + dist 目录 ===
docker compose ps
echo "--- H5 dist ---"
ls -la /opt/wecom-it-desk/frontend-h5/dist/ 2>&1
echo "--- H5 dist/assets ---"
ls -la /opt/wecom-it-desk/frontend-h5/dist/assets/ 2>&1
# === 段 2: nginx 容器内挂载 ===
docker compose exec nginx ls -la /usr/share/nginx/html/ 2>&1
echo "--- nginx 容器内 itdesk ---"
docker compose exec nginx ls -la /usr/share/nginx/html/itdesk/ 2>&1
echo "--- nginx 容器内 SSL ---"
docker compose exec nginx ls -la /etc/nginx/ssl/ 2>&1
# === 段 3: 各路径 curl 头(用主机端口绕开 nginx 容器内)===
echo "--- /itdesk/ ---"
curl -ksI https://itsupport.servyou.com.cn/itdesk/ 2>&1 | head -8
echo "--- /itportal/ ---"
curl -ksI https://itsupport.servyou.com.cn/itportal/ 2>&1 | head -8
echo "--- /itagent/ ---"
curl -ksI https://itsupport.servyou.com.cn/itagent/ 2>&1 | head -8
echo "--- /itadmin/ ---"
curl -ksI https://itsupport.servyou.com.cn/itadmin/ 2>&1 | head -8
echo "--- /itdesk/index.html(直接抓 index)---"
curl -ks https://itsupport.servyou.com.cn/itdesk/ 2>&1 | head -20
# === 段 4: 容器内 curl 443 测 ===
docker compose exec nginx curl -ksI https://localhost/itdesk/ 2>&1 | head -8
echo "---"
docker compose exec nginx curl -ksI https://localhost/itportal/ 2>&1 | head -8
# === 段 5: nginx + backend 日志 ===
echo "--- nginx 日志 ---"
docker compose logs --tail=30 nginx 2>&1
echo "--- backend 日志 ---"
docker compose logs --tail=30 backend 2>&1
# === 段 6: 容器内 nginx 错误日志 ===
docker compose exec nginx tail -30 /var/log/nginx/error.log 2>&1
echo "--- access.log ---"
docker compose exec nginx tail -30 /var/log/nginx/access.log 2>&1
```
**把全部输出贴回 Claude。**
@@ -0,0 +1,101 @@
# 3 种方法在服务器上跑诊断脚本
**目标**:在 10.90.5.110 服务器上跑 diagnose-500.sh,把输出粘回给我
---
## 方法 1(推荐):PuTTY 连进去,一行命令恢复 + 跑
**步骤 1**:PuTTY 客户端
- Host:`10.212.189.210`,Port:`2222`,SSH → Open
- 用户 `sxn` + 密码
- 堡垒机内再 `ssh sxn@10.90.5.110` 跳目标机
**步骤 2**:服务器内贴这一行(整段一次性):
```bash
cat > /tmp/diag.sh << 'ENDOFSCRIPT'
#!/bin/bash
docker compose ps
echo "---"
ls -la /opt/wecom-it-desk/frontend-h5/dist/ 2>&1 | head -10
echo "--- assets ---"
ls -la /opt/wecom-it-desk/frontend-h5/dist/assets/ 2>&1 | head -10
echo "--- nginx 容器内 ---"
docker compose exec nginx ls -la /usr/share/nginx/html/itdesk/ 2>&1 | head -10
echo "--- nginx 容器内 assets ---"
docker compose exec nginx ls -la /usr/share/nginx/html/itdesk/assets/ 2>&1 | head -10
echo "--- SSL ---"
docker compose exec nginx ls -la /etc/nginx/ssl/ 2>&1 | head -10
echo "--- /itdesk/ 头 ---"
curl -ksI https://itsupport.servyou.com.cn/itdesk/ 2>&1 | head -8
echo "--- /itportal/ 头 ---"
curl -ksI https://itsupport.servyou.com.cn/itportal/ 2>&1 | head -8
echo "--- /itagent/ 头 ---"
curl -ksI https://itsupport.servyou.com.cn/itagent/ 2>&1 | head -8
echo "--- /itadmin/ 头 ---"
curl -ksI https://itsupport.servyou.com.cn/itadmin/ 2>&1 | head -8
echo "--- /itdesk/ 完整 body 前 20 行 ---"
curl -ks https://itsupport.servyou.com.cn/itdesk/ 2>&1 | head -20
echo "--- nginx 错误日志 ---"
docker compose exec nginx tail -30 /var/log/nginx/error.log 2>&1
echo "--- nginx 访问日志 ---"
docker compose exec nginx tail -20 /var/log/nginx/access.log 2>&1
echo "--- backend 日志 ---"
docker compose logs --tail=20 backend 2>&1
ENDOFSCRIPT
bash /tmp/diag.sh 2>&1
```
**步骤 3**:把输出整段粘回给我
---
## 方法 2:用 scp 上传本地脚本
**前提**:你能 scp 到 10.90.5.110(堡垒机后的方式)
```bash
scp "C:\Users\simon\Downloads\diagnose-500 (1).sh" sxn@10.90.5.110:/tmp/
# (如果直连 scp 不通,可能要用堡垒机的文件传输功能)
```
然后 PuTTY 连进去跑:
- Host:`10.212.189.210`,Port:`2222`,SSH → Open
- 堡垒机内 `ssh sxn@10.90.5.110` 跳目标机
```bash
sudo cp /tmp/diagnose-500.sh /opt/wecom-it-desk/
cd /opt/wecom-it-desk
bash diagnose-500.sh > /tmp/diag.log 2>&1
cat /tmp/diag.log
```
`cat /tmp/diag.log` 的输出粘回
---
## 方法 3:服务器直接下载(若服务器能上外网)
```bash
# PuTTY 连:Host 10.212.189.210 Port 2222 → 堡垒机内 ssh sxn@10.90.5.110
cd /tmp
# 如果服务器能访问 GitHub raw / Gitea
curl -O https://你的存放点/diagnose-500.sh
bash diagnose-500.sh > /tmp/diag.log 2>&1
cat /tmp/diag.log
```
---
## 最简版(只要 5 行输出)
如果方法 1 太长,**只要这 5 行**就够我定位:
```bash
docker compose ps 2>&1
ls -la /opt/wecom-it-desk/frontend-h5/dist/assets/ 2>&1
docker compose exec nginx ls -la /usr/share/nginx/html/itdesk/ 2>&1
docker compose exec nginx tail -10 /var/log/nginx/error.log 2>&1
curl -ksI https://itsupport.servyou.com.cn/itdesk/ 2>&1 | head -8
```
**把这 5 段输出粘回,我能立刻定位 500 原因。**
@@ -0,0 +1,470 @@
# 智能IT支持服务台 — 新服务器部署手册
> **目标服务器**`10.90.5.110`(公司内网,**2026-06-15 起替代 10.80.0.136**
> **域名**`itsupport.servyou.com.cn`
> **访问方式**:通过堡垒机 `10.212.189.210:2222`(用户 `sxn`OTP 动态口令认证)
> **Docker**:已安装
> **部署方式**Docker Compose4容器:nginx + backend + postgres + redis
---
## 一、前置条件检查清单
| 条件 | 状态 | 验证命令 |
|------|------|---------|
| Linux 服务器 10.90.5.110(替代旧 10.80.0.136) | ✅ 已确认 | 2026-06-15 起使用 |
| Docker 已安装 | ✅ 已确认 | `docker --version` |
| Docker Compose V2 | 待确认 | `docker compose version` |
| 端口 80 未被占用 | 待确认 | `ss -tlnp \| grep :80` |
| DNS 解析 | 待配置 | `nslookup itsupport.servyou.com.cn` |
| 堡垒机可访问 | 待确认 | `ssh -p 2222 user@10.212.189.210` |
---
## 二、SSH 通过堡垒机连接
### 2.1 什么是堡垒机?
堡垒机(跳板机)是公司内网的安全访问入口。你不能直接 SSH 到目标服务器,必须先登录堡垒机,再从堡垒机跳转到目标服务器。OTP(One-Time Password)是指每次登录需要输入动态验证码(通常来自手机令牌 App)。
### 2.2 连接方式
**PuTTY 客户端(用户实际使用)**:
- 打开 PuTTY
- Host Name(IP 地址):`10.212.189.210`
- Port:`2222`
- Connection type:SSH
- Saved Sessions:起名(如 `wecom-bastion`)→ Save
- 点 Open
- 用户 `sxn` + 密码
- **堡垒机内再跳目标机**:
```bash
ssh sxn@10.90.5.110
```
> **OpenSSH `ssh -J` 方式不再使用**(用户已确认用 PuTTY,2026-06-15)
# 登录成功后:
ssh sxn@10.90.5.110
```
### 2.3 配置 SSH 快捷方式(推荐)
在开发机上编辑 `~/.ssh/config`,添加以下内容,以后只需要 `ssh itdesk` 即可:
```
# 堡垒机
Host bastion
HostName 10.212.189.210
Port 2222
User sxn
# 智能IT支持服务台服务器
Host itdesk
HostName 10.90.5.110
User sxn
ProxyJump bastion
```
> **堡垒机用户名为 `sxn`,已填入下方命令中**
之后只需:
```bash
ssh itdesk # 自动通过堡垒机跳转
scp file itdesk:/opt/ # 文件传输也会自动走堡垒机
```
---
## 三、文件传输(通过堡垒机)
### 3.1 SCP 传输(推荐小文件/单次传输)
```bash
# 上传单个文件
scp -o "ProxyJump=sxn@10.212.189.210:2222" \
it-smart-desk-server-deploy.zip \
sxn@10.90.5.110:/opt/
# 如果已配置 ~/.ssh/config
scp it-smart-desk-server-deploy.zip itdesk:/opt/
```
### 3.2 大文件传输优化
部署包可能较大(含后端源码 + 前端产物),如果 SCP 速度慢,可以先传到堡垒机再转:
```bash
# 步骤1:传到堡垒机
scp -P 2222 it-smart-desk-server-deploy.zip sxn@10.212.189.210:/tmp/
# 步骤2SSH 到堡垒机
ssh -p 2222 sxn@10.212.189.210
# 步骤3:从堡垒机传到目标服务器
scp /tmp/it-smart-desk-server-deploy.zip sxn@10.90.5.110:/opt/
```
---
## 四、部署步骤(完整流程)
### 步骤 1:在开发机上构建前端并打包
```bash
# 在开发机(Windows)上,进入项目根目录
cd D:\资料\03-项目开发\wecom_it_smart_desk
# 方法A:使用打包脚本(自动构建前端 + 组装 + 打包)
bash deploy-server/package.sh
# 方法B:手动构建
# H5 员工端
cd frontend-h5
npm install && npm run build
# 坐席工作台
cd ../frontend-agent
npm install && npm run build
# 手动打包(如果不用 package.sh
# 需要把 frontend-h5/dist/、frontend-agent/dist/、backend/、deploy-server/ 下的配置文件一起打包
```
打包完成后,项目根目录下会生成 `it-smart-desk-server-deploy.zip`。
### 步骤 2:上传部署包到服务器
```bash
# 在开发机上执行
scp -o "ProxyJump=sxn@10.212.189.210:2222" \
it-smart-desk-server-deploy.zip \
sxn@10.90.5.110:/tmp/
```
> 上传到 `/tmp/` 而非 `/opt/`,因为普通用户对 `/opt/` 没有写权限
### 步骤 3:登录服务器并解压
**PuTTY 登录**(见 §2.2):
- Host:`10.212.189.210`,Port:`2222`,SSH
- 堡垒机内再 `ssh sxn@10.90.5.110`
```bash
# 切换 root(普通用户对 /opt 无写权限)
sudo -i
# 移动并解压部署包
mv /tmp/it-smart-desk-server-deploy.zip /opt/
cd /opt
unzip it-smart-desk-server-deploy.zip
# 重命名目录为更简短的名称
mv it-smart-desk-server-deploy wecom-it-desk
cd wecom-it-desk
```
### 步骤 4:配置环境变量
```bash
cd /opt/wecom-it-desk
# 从模板创建 .env
cp .env.example .env
# 编辑 .env
vi .env
```
**阶段一(Mock 模式)最小配置** — 只需确认以下默认值:
```ini
# 数据库密码(默认即可,首次初始化后不可更改)
POSTGRES_PASSWORD=wecom_secret_2024
# 企微配置(阶段一 Mock 模式可以留空)
WECOM_CORP_ID=
WECOM_AGENT_ID=1000002
WECOM_SECRET=
WECOM_TOKEN=
WECOM_ENCODING_AES_KEY=
# Mock 登录(阶段一设为 true
MOCK_LOGIN_ENABLED=true
# Dify AI(暂时可以留空)
DIFY_API_URL=http://yw-dify.dc.servyou-it.com/dify2openai/v1/chat/completions
DIFY_API_KEY=
```
> **重要**`POSTGRES_PASSWORD` 首次启动时写入数据库,之后修改 `.env` 不会生效。如需修改密码,必须删除数据卷重建。
### 步骤 5:部署
```bash
cd /opt/wecom-it-desk
# 添加执行权限
chmod +x deploy.sh
# 执行部署
./deploy.sh
```
脚本会自动:
1. ✅ 检查 Docker 环境
2. ✅ 检查 .env 配置
3. ✅ 检查前端文件
4. ✅ 构建后端 Docker 镜像
5. ✅ 启动 4 个容器
6. ✅ 等待服务就绪
7. ✅ 验证部署
### 步骤 6:验证部署
```bash
# 在服务器上验证
curl http://localhost/api/health
# 应返回 {"status":"healthy"}
curl http://localhost/itdesk/
# 应返回 H5 前端 HTML
# 查看所有容器状态
docker compose ps
# 应显示 4 个容器都是 Up 状态
# 如果有容器未启动,查看日志
docker compose logs --tail 50 backend
docker compose logs --tail 50 postgres
```
### 步骤 7:配置 DNS
需要联系公司 IT 运维,在公司 DNS 上添加 A 记录:
```
itsupport.servyou.com.cn A 10.90.5.110
```
**DNS 未生效前**,可以通过本地 hosts 文件测试:
```
# Windows: C:\Windows\System32\drivers\etc\hosts
# macOS/Linux: /etc/hosts
# 添加一行:
10.90.5.110 itsupport.servyou.com.cn
```
> 注意:修改 hosts 文件后,浏览器可能有 DNS 缓存。Chrome 可访问 `chrome://net-internals/#dns` 清除缓存,或用无痕窗口测试。
### 步骤 8:浏览器验证
DNS 生效后(或配置了本地 hosts),在浏览器中访问:
| 页面 | URL | 预期结果 |
|------|-----|---------|
| H5 员工端 | `http://itsupport.servyou.com.cn/itdesk/` | 看到登录页面 |
| 坐席工作台 | `http://itsupport.servyou.com.cn/itagent/` | 看到坐席工作台 |
| API 健康检查 | `http://itsupport.servyou.com.cn/api/health` | `{"status":"healthy"}` |
**Mock 登录测试**
1. 访问 `http://itsupport.servyou.com.cn/itdesk/login`
2. 输入任意工号和姓名(如 `test001` / `测试用户`
3. 应成功登录并进入聊天页面
---
## 五、部署文件结构
```
/opt/wecom-it-desk/
├── docker-compose.yml # Docker Compose 配置(4容器)
├── .env # 环境变量(已配置)
├── .env.example # 环境变量模板
├── deploy.sh # 一键部署脚本
├── README.md # 本手册
├── nginx/
│ └── nginx.conf # Nginx 配置(反代 + 静态文件)
├── backend/
│ ├── Dockerfile # 后端镜像构建文件
│ ├── requirements.txt # Python 依赖
│ └── app/ # 后端源代码
├── frontend-h5/
│ └── dist/ # H5 员工端构建产物
└── frontend-agent/
└── dist/ # 坐席工作台构建产物
```
---
## 六、常用运维命令
在服务器上 `/opt/wecom-it-desk` 目录下执行:
| 操作 | 命令 |
|------|------|
| 查看服务状态 | `./deploy.sh status` |
| 查看后端日志 | `./deploy.sh logs` |
| 停止所有服务 | `./deploy.sh stop` |
| 重新构建后端 | `./deploy.sh rebuild` |
| 重置数据库 | `./deploy.sh reset-db` |
| 手动启动 | `docker compose up -d` |
| 手动停止 | `docker compose down` |
| 只重启后端 | `docker compose restart backend` |
| 查看数据库 | `docker exec -it wecom_it_postgres psql -U wecom -d wecom_it_desk` |
| 查看 Redis | `docker exec -it wecom_it_redis redis-cli` |
| 重载 Nginx | `docker exec wecom_it_nginx nginx -s reload` |
| 查看容器日志 | `docker compose logs --tail 50 <容器名>` |
---
## 七、升级前端
当有新的前端版本需要部署时:
```bash
# 1. 在开发机上构建新版本
cd frontend-h5 && npm run build
cd frontend-agent && npm run build
# 2. 上传到服务器(通过堡垒机)
scp -o "ProxyJump=sxn@10.212.189.210:2222" \
-r frontend-h5/dist/ \
sxn@10.90.5.110:/opt/wecom-it-desk/frontend-h5/dist/
scp -o "ProxyJump=sxn@10.212.189.210:2222" \
-r frontend-agent/dist/ \
sxn@10.90.5.110:/opt/wecom-it-desk/frontend-agent/dist/
# 3. 重载 Nginx(不需要重启整个服务)
ssh itdesk # 如果已配置 SSH 快捷方式
cd /opt/wecom-it-desk
docker exec wecom_it_nginx nginx -s reload
```
---
## 八、升级后端
```bash
# 1. 上传新代码到服务器
scp -o "ProxyJump=sxn@10.212.189.210:2222" \
-r backend/ \
sxn@10.90.5.110:/opt/wecom-it-desk/backend/
# 2. 重新构建并启动
ssh itdesk
cd /opt/wecom-it-desk
./deploy.sh rebuild
```
---
## 九、故障排查
### 后端容器一直重启
```bash
# 1. 查看容器状态
docker compose ps
# 2. 查看后端日志(最常见原因:数据库连接失败)
docker compose logs --tail 100 backend
# 3. 检查 PostgreSQL 是否健康
docker exec wecom_it_postgres pg_isready -U wecom -d wecom_it_desk
# 4. 检查 Redis 是否健康
docker exec wecom_it_redis redis-cli ping
```
### PostgreSQL 密码错误
```bash
# ⚠️ 这会清空所有数据!只有首次部署密码错误时才需要
docker compose down
docker volume rm wecom-it-desk_postgres_data
docker compose up -d
```
### H5/坐席端白屏
```bash
# 检查前端文件是否存在
docker exec wecom_it_nginx ls /usr/share/nginx/html/itdesk/
docker exec wecom_it_nginx ls /usr/share/nginx/html/itagent/
# 检查 index.html 中的 base 路径是否正确
docker exec wecom_it_nginx cat /usr/share/nginx/html/itdesk/index.html | grep /itdesk/
docker exec wecom_it_nginx cat /usr/share/nginx/html/itagent/index.html | grep /itagent/
```
### DNS 未生效
```bash
# 在服务器上验证
nslookup itsupport.servyou.com.cn
# 如果 DNS 未配置,临时用 IP 直接访问
curl http://10.90.5.110/itdesk/
curl http://10.90.5.110/api/health
```
### Mock 登录返回 401
```bash
# 1. 确认 .env 中 MOCK_LOGIN_ENABLED=true
cat /opt/wecom-it-desk/.env | grep MOCK
# 2. 检查后端日志
docker compose logs --tail 50 backend | grep mock
# 3. 直接测试 mock-login 接口
curl -X POST http://localhost/api/h5/mock-login \
-H "Content-Type: application/json" \
-d '{"employee_id":"test001","employee_name":"测试用户"}'
```
---
## 十、HTTPS 配置(可选)
如果公司要求 HTTPS,有两种方式:
### 方式一:公司统一 SSL 终端(推荐)
```
客户端 → HTTPS → 公司SSL终端(F5/网关,公网 115.236.188.3) → HTTP → 10.90.5.110:80
```
不需要在本服务器上配置证书。联系运维配置 SSL 终端即可。
### 方式二:本机 SSL
编辑 `nginx/nginx.conf`,取消 HTTPS server 块注释,配置证书路径。
---
## 十一、部署说明
> ⚠️ NAS部署方案(itdesk.amanzac.com)已于2026年6月15日下线,现统一使用公司内网服务器部署。
| 维度 | NAS 部署(已下线) | 当前服务器部署(10.90.5.110 |
|------|---------------------------|-------------------------------|
| 容器数量 | 5个(含 cloudflared | 4个(无 cloudflared |
| 外网访问 | Cloudflare Tunnel | 公司 DNS 直连 |
| 域名 | itdesk.amanzac.com | itsupport.servyou.com.cn |
| SSL | Cloudflare 自动 | 无(内网 HTTP)或公司统一 SSL |
| 数据平台反代 | 需要(共用域名) | 不需要(独立域名) |
| 部署目录 | `/volume1/docker/wecom-it-desk` | `/opt/wecom-it-desk` |
| 文件传输 | File Station / 7z | SCP 通过堡垒机 |
---
## 十二、相关文档
| 文档 | 说明 |
|------|------|
| [堡垒机运维工具](./11-堡垒机运维工具.md) | 通过 JumpServer 自动化执行远程命令、文件上传下载 |
| [版本更新说明](./05-版本更新说明-v1.1.0-20260614.md) | 各版本功能变更记录 |
| [NAS 部署指南](./08-NAS部署指南-预生产.md) | 群晖 NAS 测试环境部署 |
@@ -0,0 +1,165 @@
# 用户手册:扫码登录 + OTP 二次认证(Phase 1+2)
> 创建:2026-06-21
> 适用版本:v0.7.0+ (Phase 1+2 上线后)
> 读者:全体员工、坐席、管理员
---
## 📖 这是什么?
从 v0.7.0 开始,登录方式升级为**扫码登录 + OTP 二次认证**:
- ✅ 不再依赖企业微信应用入口,任意浏览器都能打开
- ✅ 多角色用户(坐席+管理员)可在 Portal 选角色自动跳转
- ✅ 管理员每次登录强制 OTP,高危操作也强制 OTP
- ✅ 备用通道:蜂鸟短信(手机丢/没装 Authenticator 时用)
---
## 🧑‍💼 员工端(H5)
### 入口
- 仍在企微内打开"IT智能服务台"应用
- 不需要扫码登录,沿用企微 OAuth
### 使用场景
- 提工单
- 看历史会话
- 查知识库
---
## 🧑‍🔧 坐席端(Agent)
### 首次登录(扫码)
```
步骤 1:浏览器访问 https://itsupport.servyou.com.cn/itportal/
步骤 2:页面显示二维码(120 秒有效)
步骤 3:用企业微信扫 → 确认登录
步骤 4:自动跳到 /itagent/workspace
```
### 日常登录
- 浏览器直接打开 `https://itsupport.servyou.com.cn/itagent/`
- 没登录 → 自动跳到 Portal 扫码
- 第二次扫码可免重复(浏览器记住 localStorage)
### 高危操作时 OTP
如果你是**坐席+管理员**(双角色),触发以下操作前会弹 OTP 输入框:
- 改权限
- 改系统配置
- 导出数据
- 封号
- 新增账号/MFA 重置
弹框出现 → 输入 Authenticator 6 位码 → 验证通过(30 分钟内免重输)
---
## 🛡️ 管理员端(Admin)
### 入口
- 浏览器直接打开 `https://itsupport.servyou.com.cn/itadmin/`
- 没登录 → 自动跳到 Portal 扫码
### 强制 OTP
管理员**每次登录都需要 OTP**:
- 扫码登录成功后,会跳到 MFA 绑定页(首次)或 OTP 验证页
- 输入 Authenticator 6 位码 → 进入管理后台
- 高危操作前还要再验一次(30 分钟内免重输)
### 首次绑定 OTP(强制)
```
步骤 1:登录后 → 自动跳 /mfa-bind
步骤 2:用 Google Authenticator / 微软 Authenticator / Authy 扫描二维码
步骤 3:输入 Authenticator 显示的 6 位码 → 点"启用 OTP"
步骤 4:绑定成功,后续登录用 OTP 验证
```
### 丢手机兜底(管理员后台重置)
如果你手机丢了/坏了,**找其他管理员重置**:
```
步骤 1:其他管理员登录 /itadmin/
步骤 2:进入"用户管理" → "MFA 管理"
步骤 3:搜索你的姓名 → 点"重置 MFA"
步骤 4:你下次登录时重新绑定 OTP
```
---
## 🆘 备用通道:蜂鸟 SMS
什么情况下用:
- 📱 手机丢了/坏了
- 🆕 刚入职,还没装 Authenticator
- 🔧 Authenticator 客户端不兼容
怎么用:
- 在 OTP 输入页点"收不到验证码?短信验证"
- 输入手机号(企微已绑定)→ 收短信码 → 验证
---
## 📱 推荐 OTP 客户端
| 客户端 | 平台 | 推荐度 |
|---|---|---|
| Google Authenticator | iOS / Android | ⭐⭐⭐⭐⭐ |
| 微软 Authenticator | iOS / Android | ⭐⭐⭐⭐ |
| Authy | iOS / Android / 桌面 | ⭐⭐⭐⭐⭐ |
| 1Password | 全平台 | ⭐⭐⭐ |
公司偏好:**Google Authenticator**(零依赖,离线可用)
---
## ❓ 常见问题
### Q1:扫码登录过期了怎么办?
A:二维码有效期 120 秒,过期后点"刷新二维码"按钮。
### Q2:扫码登录失败?
A:
- 确认用的是企业微信(不是普通微信)
- 确认企微里能看到"IT智能服务台"应用
- 刷新页面重新生成二维码
### Q3:OTP 输入错误?
A:连续 5 次错误会被锁定 5 分钟,等 5 分钟后再试。
### Q4:换手机了怎么办?
A:登录前在旧手机上导出 OTP(Google Authenticator 支持),或者找管理员后台重置。
### Q5:多角色用户(admin + agent)怎么登录?
A:扫码登录成功后,Portal 自动跳到角色选择页,选你要进入的工作台。
### Q6:H5 员工端也需要扫码吗?
A:不需要。H5 员工端仍在企微内,沿用企微 OAuth。
### Q7:扫码登录安全吗?
A:扫码登录比企微 OAuth 还安全:
- 员工必须用企微扫(企微已经做了员工身份认证)
- 二维码 120 秒过期
- Token 8 小时过期
- 高危操作还要再 OTP 一次
---
## 📞 技术支持
- 内部: 信息技术部 服务台
- 紧急: 群里 @ IT 主管
- 反馈: https://itsupport.servyou.com.cn/itdesk/feedback
---
**变更历史**:
- 2026-06-21 创建(Phase 1+2 培训)
@@ -0,0 +1,114 @@
# WAF 转发配置申请
## 问题描述
`itsupport.servyou.com.cn` 域名无法访问,浏览器超时。需 WAF 配置转发规则。
---
## 证据链
### 1. 服务器本地 — 服务正常 ✅
```
# HTTP 已强制跳转 HTTPSnginx 配置 301 重定向)
[root@hz-oa-ai-g-dataquery-90-5-110 ~]# curl http://localhost/itdesk/health
<html><head><title>301 Moved Permanently</title></head>...nginx/1.27.5</html>
# HTTPS 正常响应
[root@hz-oa-ai-g-dataquery-90-5-110 ~]# curl -k https://127.0.0.1/itdesk/health -H "Host: itsupport.servyou.com.cn"
healthy
```
### 2. SSL 证书 — 有效 ✅
```
[root@hz-oa-ai-g-dataquery-90-5-110 ~]# echo | openssl s_client -connect 127.0.0.1:443 -servername itsupport.servyou.com.cn
CONNECTED(00000003)
depth=2 C=US, O=DigiCert Inc, CN=DigiCert Global Root G2
depth=1 C=US, O=DigiCert, Inc., CN=GeoTrust G2 TLS CN RSA4096 SHA256 2022 CA1
depth=0 C=CN, ST=浙江省, L=杭州市, O=税友软件集团股份有限公司, CN=*.servyou.com.cn
Verification: OK
Protocol: TLSv1.3, Cipher: TLS_AES_256_GCM_SHA384
Verify return code: 0 (ok)
```
证书信息:
- 主体:`CN=*.servyou.com.cn`(通配符证书)
- 颁发者:`GeoTrust G2 TLS CN RSA4096 SHA256 2022 CA1`
- 有效期:2025-12-23 ~ 2027-01-12
### 3. DNS 解析 — 指向 WAF ✅
```
# 服务器 DNS 解析到 WAF 公网 IP
[root@hz-oa-ai-g-dataquery-90-5-110 ~]# ping -c 1 itsupport.servyou.com.cn
PING itsupport.servyou.com.cn (115.236.188.3): 56(84) bytes of data.
--- itsupport.servyou.com.cn ping statistics ---
1 packets transmitted, 0 received, 100% packet loss
```
- 解析结果:`115.236.188.3`WAF 公网 IP
- ping 100% 丢失(WAF 禁 ICMP,正常)
### 4. WAF 转发 — 不通 ❌
```
# 从服务器通过域名访问 HTTP(超时)
[root@hz-oa-ai-g-dataquery-90-5-110 ~]# curl -v http://itsupport.servyou.com.cn/itdesk/health
* Trying 115.236.188.3:80...
^C(超时无响应)
# 从服务器通过域名访问 HTTPS(超时)
[root@hz-oa-ai-g-dataquery-90-5-110 ~]# curl -v https://itsupport.servyou.com.cn/itdesk/health
* Trying 115.236.188.3:443...
^C(超时无响应)
```
### 5. 服务器外网连通性 — 正常 ✅
```
# 企微 API 可达
[root@hz-oa-ai-g-dataquery-90-5-110 ~]# curl -s https://qyapi.weixin.qq.com/cgi-bin/gettoken
{"errcode":41004,"errmsg":"corpsecret missing", "from ip": "218.75.34.87"}
# PyPI 镜像可达
[root@hz-oa-ai-g-dataquery-90-5-110 ~]# curl -s https://pypi.tuna.tsinghua.edu.cn/
<html><head><title>302 Found</title></head>...nginx/1.22.1</html>
```
---
## 结论
| 环节 | 状态 |
|------|------|
| 服务器(10.90.5.110 | ✅ HTTP/HTTPS 服务正常 |
| SSL 证书(*.servyou.com.cn | ✅ 有效,TLSv1.3 |
| DNS 解析 | ✅ 指向 WAF115.236.188.3 |
| 服务器外网连通性 | ✅ 企微 API / PyPI 均可达 |
| **WAF 转发到后端** | **❌ 未配置 — 流量未到达 10.90.5.110** |
---
## 需要配置
请 WAF/网络团队配置转发规则:
```
域名:itsupport.servyou.com.cn
源端口:80HTTP/ 443HTTPS
转发目标:10.90.5.110:80
```
---
## 服务器信息
| 项目 | 值 |
|------|-----|
| 服务器 IP | 10.90.5.110 |
| 服务端口 | 80HTTP→HTTPS 重定向)+ 443HTTPS |
| 域名 | itsupport.servyou.com.cn |
| SSL 证书 | *.servyou.com.cnDigiCert,有效期至 2027-01-12 |
| 系统 | LinuxDocker 部署,nginx 反向代理) |
@@ -0,0 +1,138 @@
# 通讯链路诊断方案
> 日期:2026-07-03
> 目标:诊断当前系统通讯问题,无论结果启动重构方案
---
## 一、通讯链路架构
```
┌─────────────────────────────────────────────────────────────────────────┐
│ 完整通讯链路 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 【用户 → 坐席】 │
│ ┌──────────┐ 企微回调 ┌──────────┐ 路由 ┌─────────┐ │
│ │ 用户发送 │ ──────────────→ │ 后端API │ ──────────→ │ Message │ │
│ │ 消息 │ /wecom/ │ 回调入口 │ │ Router │ │
│ └──────────┘ callback └──────────┘ └────┬────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌───────────┐ │
│ │ │ 消息入库 │ │
│ │ │ (DB存储) │ │
│ │ └───────────┘ │
│ │ │ │
│ │ ┌────────────────┘ │
│ │ ▼ │
│ │ ┌──────────┐ │
│ │ │ 坐席收到 │ │
│ │ │(WS/轮询) │ │
│ │ └──────────┘ │
│ │ │
│ 【坐席 → 用户】 │
│ ┌──────────┐ API调用 ┌──────────┐ 企微API ┌────────┐ │
│ │ 坐席发送 │ ──────────────→ │ 后端API │ ──────────→ │企微 │ │
│ │ 消息 │ POST │ 发送消息 │ /message │服务器 │ │
│ └──────────┘ /conversations└──────────┘ /send └────┬───┘ │
│ │ /{id}/messages │ │ │
│ │ ▼ ▼ │
│ │ ┌──────────┐ ┌────────┐ │
│ │ │ 消息入库 │ │用户收到 │ │
│ │ │(DB存储) │ │消息 │ │
│ │ └──────────┘ └────────┘ │
│ │ │
└─────────────────────────────────────────────────────────────────┘
```
---
## 二、诊断检查点
### 2.1 企微回调链路(用户 → 系统)
| 检查点 | 文件位置 | 检查内容 | 预期结果 |
|--------|---------|---------|---------|
| C-01 | `wecom_callback.py` GET `/wecom/callback` | 企微URL验证 | 返回解密后的echostr |
| C-02 | `wecom_callback.py` POST `/wecom/callback` | 消息解密 | 正确解析XML并解密 |
| C-03 | `message_router.py` | 消息路由 | 正确分配会话/坐席 |
| C-04 | 数据库 `messages` 表 | 消息存储 | 消息正确写入 |
### 2.2 坐席发送链路(系统 → 用户)
| 检查点 | 文件位置 | 检查内容 | 预期结果 |
|--------|---------|---------|---------|
| C-05 | `messages.py` POST `/conversations/{id}/messages` | API入口 | 正确接收坐席消息 |
| C-06 | `wecom_service.py` `send_text_message()` | 企微API调用 | errcode=0 |
| C-07 | 企微客户端 | 用户收到消息 | 正常展示 |
### 2.3 H5 实时推送
| 检查点 | 文件位置 | 检查内容 | 预期结果 |
|--------|---------|---------|---------|
| C-08 | `ws_manager.py` | WS连接管理 | 坐席WS连接 |
| C-09 | `frontend-agent` | WS接收 | 消息实时展示 |
| C-10 | `frontend-h5` | 轮询/WebSocket | 新消息实时更新 |
---
## 三、已发现的问题
### 问题1:非文本消息不推送(messages.py:210-233
```python
# 只有 text 类型消息才调用企微 API 推送给员工
if body.msg_type == "text":
# 调用企微API
```
**影响**:图片、文件等消息无法推送到用户微信端
### 问题2dev_mode 短路(messages.py:215-216
```python
if getattr(settings, 'dev_mode', False):
logger.debug(f"[DEV] 跳过企微推送: msg_id={message.id}")
```
**影响**:测试环境下消息不会推送到用户
### 问题3:企微API错误处理(messages.py:231-233
```python
except Exception as e:
# 企微 API 调用失败不阻塞消息存储
logger.warning(f"企微消息发送失败(消息已存储): {e}")
```
**影响**:企微API失败时仅记录日志,用户实际未收到消息
---
## 四、诊断执行记录
| 时间 | 检查项 | 结果 | 说明 |
|------|--------|------|------|
| 2026-07-03 | 代码审查 | ✅ | 完成链路分析 |
| - | C-01 企微回调 | ⏳ | 待部署环境验证 |
| - | C-05 坐席发送 | ⏳ | 待部署环境验证 |
| - | C-07 用户收到 | ⏳ | 待实际测试 |
---
## 五、结论
**当前系统通讯链路代码完整**,但存在以下已知风险:
1. 非文本消息(图片/文件)无法推送
2. dev_mode 会跳过企微推送
3. 企微API失败时静默失败
这些问题可通过系统重构进一步优化消息通讯能力。
---
## 六、下一步
**下一步**:根据诊断结果优化现有通讯链路