chore: docs 结构整改 + compose 双目录对齐(合并重建提交)
本提交为 .git 对象库损坏后的重建提交,内容等价于原先三个本地提交 (5e2fd4c2 / 57a53c98 / 5d7e1873)的累积结果,未做任何额外改动。 一、docs 结构整改(整改 #14) 根因:重构时新结构为 untracked 文件,执行 git stash(未带 -u)未纳入, 随后 git reset 拉回 HEAD 旧 tracked 树,导致旧树复活、新旧两棵目录 树并存于 docs/,共 791 文件、双分类体系冲突。 修复动作: - b2 同名异主题文件改名迁移保全 9 个 - C 类 39 个孤立文件按主题正确归类 - A/B1 类 222 个重复文件删除(新结构已有内容副本) - 9 个旧独有空目录删除 - 270 处内部引用按 verified 映射改写 - 整改记录 #14 登记于 04-运维文档/部署运维 结果:docs 791 → 569 文件,顶层仅规范 8 类 + 治理文件,单树恢复。 残留:约 20 处指向从未存在文件的陈旧死链,归入独立文档卫生任务。 二、compose 双目录对齐(消除踩坑 A) - docker-compose.yml:nginx 前端挂载全部由根目录 frontend-*/dist 改为 src/frontend-*/dist(h5 / agent / admin / terminal) - docker-compose.dev.yml:dev 服务 build context 与卷同步改 src/ - 效果:本地 docker compose up 不再把根目录 stale dist 挂回, 与线上一致,分叉隐患消除(已 docker compose config 校验通过) 防复发铁律: - 重构须提交;仓库修复须 git stash -u 或先 commit - 新结构须 git add 并提交,避免再次 untracked 复活 - H5 改动只动 src/frontend-h5/,禁改根目录遗留 frontend-*/
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
# 快速回复规则后台管理 — 部署文档
|
||||
|
||||
> **需求编号**: REQ-通用-002
|
||||
> **版本**: v1.2(2026-07-28 管理后台 UI 去重调整)
|
||||
> **日期**: 2026-07-27(初版) / 2026-07-28(v1.1 整改、v1.2 UI 调整)
|
||||
> **状态**: [已评审]
|
||||
> **作者**: Simon
|
||||
> **部署服务器**: itsupport.servyou.com.cn (10.90.5.110)
|
||||
>
|
||||
> **关联文档**(按规范要求补全):
|
||||
> - PRD:`01-产品文档/00-产品规划/PRD-REQ-通用-002-快速回复规则后台管理-v1.2.md`
|
||||
> - 技术方案:`02-技术文档/技术方案-REQ-通用-002-快速回复规则后台管理.md`
|
||||
> - 原型图:`01-产品文档/01-02产品设计/快速回复规则后台管理-原型图.html`
|
||||
> - 上游依赖:Alembic 055 迁移(`alembic upgrade 055`)、`scripts/init_quick_rules.sql`
|
||||
>
|
||||
> **命名规范说明**:按 `docs/00-产品开发流程与文档管理规范.md` 2.3.3 节,标准命名应为
|
||||
> `DEPLOY-REQ-通用-002-快速回复规则后台管理-v1.0.md`,应放置在 `04-运维文档/部署运维/` 子目录下。
|
||||
> 本次保留历史文件名/位置以避免破坏现有内部引用;**后续同类文档请按规范命名**。
|
||||
|
||||
---
|
||||
|
||||
## 0. 前置条件(部署前必须 100% 确认)
|
||||
|
||||
| 检查项 | 要求 | 验证方式 |
|
||||
|--------|------|----------|
|
||||
| 关联 PRD | 已评审、当前为 [已评审] 状态 | `Read docs/01-产品文档/00-产品规划/PRD-REQ-通用-002-快速回复规则后台管理-v1.2.md` |
|
||||
| 关联技术方案 | 已评审 | `Read docs/02-技术文档/技术方案-REQ-通用-002-快速回复规则后台管理.md` |
|
||||
| 关联原型图 | 已评审(本次涉及 6 个新页面,必须有) | 打开 `docs/01-产品文档/01-02产品设计/快速回复规则后台管理-原型图.html` |
|
||||
| Alembic 迁移文件 | `alembic/versions/055_*.py` 已存在 | `ls alembic/versions/ | grep -i quick` |
|
||||
| 初始化 SQL | `scripts/init_quick_rules.sql` 已就绪且 ≥ 53 条 | `wc -l scripts/init_quick_rules.sql` |
|
||||
| jumpserver-V2 skill | 已登录且缓存有效 | 跑 `v2_ops.py status` |
|
||||
| 服务器 `.env` | `.env` 已同步 `QUICK_RULE_*` 类变量(见 §1.3) | `grep -E "^QUICK_RULE_" .env` |
|
||||
| 后端镜像 | `--workers 1` 已写入 `docker-compose.yml` | `grep -E "workers" docker-compose.yml` |
|
||||
| 部署窗口 | 已与值班坐席同步,选低峰期 | 口头/IM 通知 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 部署概览
|
||||
|
||||
### 1.1 部署范围
|
||||
|
||||
| 模块 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| 数据库表 | 新增 2 张 | `quick_rules`、`quick_rule_audit_log` |
|
||||
| 后端 API | 新增 12 个 | `/api/admin/quick-rules/*`(见技术方案 §3) |
|
||||
| 前端页面 | 新增 6 个 | `/quick-rules/*`(admin 端 1 个 H5/坐席端不涉及) |
|
||||
| 现有代码改造 | 2 处 | `ai_handler.py`、`routing_service.py`(向后兼容:DB 为空时回退硬编码) |
|
||||
| **本版本不涉及** | - | agent 端、H5 端、terminal 端无需重新打包 |
|
||||
|
||||
### 1.2 规则类型与初始数量(单一权威来源,§5 验证时直接引用此处)
|
||||
|
||||
| rule_type | 初始条数 | 兜底硬编码条数 |
|
||||
|-----------|----------|----------------|
|
||||
| `greeting` | 12 | 1 条("你好" → 引导描述问题) |
|
||||
| `routing_prefilter` | 33 | 3 条 |
|
||||
| `routing_target` | 6 | 2 条 |
|
||||
| **合计** | **51** | **6** |
|
||||
|
||||
### 1.3 `.env` 变量变更清单(配置同步铁律,必须同步)
|
||||
|
||||
> 部署铁律:修改 `config.py` 默认值 ≠ 配置生效,必须同步更新服务器 `.env`。
|
||||
|
||||
| 变量名 | 用途 | 默认值 | 来源 |
|
||||
|--------|------|--------|------|
|
||||
| `QUICK_RULE_AUTO_APPLY_THRESHOLD` | 智能体自动应用规则的置信度阈值 | `0.85` | config.py |
|
||||
| `QUICK_RULE_AUDIT_REVIEW_THRESHOLD` | 落入待审核的阈值 | `0.5` | config.py |
|
||||
| `QUICK_RULE_CACHE_TTL_SECONDS` | Redis 缓存 TTL | `300` | config.py |
|
||||
|
||||
服务器部署完成后必须执行:
|
||||
|
||||
```bash
|
||||
# 通过 v2_ops.py 上传 .env 增量文件
|
||||
v2_ops.py upload .env.production-increment .env.production-increment
|
||||
|
||||
# 远端追加(注意:此处禁用 $(date),用预生成时间戳文件名)
|
||||
ssh-via-jms 'cat /tmp/.env.production-increment >> /opt/wecom-it-desk/.env'
|
||||
|
||||
# 重载后端使新变量生效
|
||||
docker compose --env-file .env restart backend
|
||||
```
|
||||
|
||||
### 1.4 部署顺序(部署铁律)
|
||||
|
||||
```
|
||||
DB migration(alembic upgrade)→ .env 同步 → 后端 code(restart)→ 前端 admin(psftp + restart nginx)→ 端到端验证 → 灰度
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据库迁移
|
||||
|
||||
### 2.1 备份(v2_ops.py / psftp 双通道)
|
||||
|
||||
```bash
|
||||
# 1. 登录堡垒机(jumpserver-V2 skill)
|
||||
v2_ops.py login
|
||||
|
||||
# 2. 服务器端生成备份文件名(禁用 $(...),预生成)
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S) # 本地 PowerShell 生成后再传入
|
||||
ssh-via-jms "docker exec wecom_it_postgres pg_dump -U postgres -d wecom_it_desk -Fc > /opt/wecom-it-desk/backups/before-quickrule-${TIMESTAMP}.dump"
|
||||
```
|
||||
|
||||
### 2.2 执行迁移(按部署铁律:DB migration 只能逐版降)
|
||||
|
||||
```bash
|
||||
# 1. 检查当前版本
|
||||
ssh-via-jms "docker exec wecom_it_backend alembic current"
|
||||
|
||||
# 2. 升级到目标版本(升级用 head,向前兼容)
|
||||
ssh-via-jms "docker exec wecom_it_backend alembic upgrade 055"
|
||||
|
||||
# 3. 验证:checkfirst 自动建表是否成功(主要依赖 alembic,但作为冗余验证)
|
||||
ssh-via-jms "docker exec wecom_it_postgres psql -U postgres -d wecom_it_desk -c '\\dt quick_rules*'"
|
||||
|
||||
# 期望输出:
|
||||
# quick_rule_audit_log
|
||||
# quick_rules
|
||||
|
||||
# 4. 插入初始化数据(psftp 通道,禁用 elFinder)
|
||||
v2_ops.py upload scripts/init_quick_rules.sql init_quick_rules.sql
|
||||
ssh-via-jms "cat /tmp/init_quick_rules.sql | docker exec -i wecom_it_postgres psql -U postgres -d wecom_it_desk -v ON_ERROR_STOP=1"
|
||||
```
|
||||
|
||||
> ⚠️ **降级路径**(回滚时使用):蓝绿共用同一 PG,本次只能逐版降
|
||||
> ```bash
|
||||
> ssh-via-jms "docker exec wecom_it_backend alembic downgrade -1"
|
||||
> # 重复执行直到 quick_rules 表消失
|
||||
> ssh-via-jms "docker exec wecom_it_postgres psql -U postgres -d wecom_it_desk -c '\\dt quick_rules*'"
|
||||
> ```
|
||||
|
||||
### 2.3 验证
|
||||
|
||||
```sql
|
||||
-- 单一权威断言:本表数字一旦修改,§1.2 同步修改
|
||||
SELECT rule_type, COUNT(*) FROM quick_rules WHERE is_active = true GROUP BY rule_type ORDER BY rule_type;
|
||||
-- 期望:greeting=12, routing_prefilter=33, routing_target=6
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 后端部署
|
||||
|
||||
### 3.1 部署(卷挂载 → restart 即可)
|
||||
|
||||
```bash
|
||||
# 后端代码统一源:/opt/wecom-it-desk/app/(卷挂载)
|
||||
# .py 变更仅 restart,不必 rebuild
|
||||
v2_ops.py upload src/backend/app/models/quick_rule.py /tmp/quick_rule.py
|
||||
v2_ops.py upload src/backend/app/models/quick_rule_audit_log.py /tmp/quick_rule_audit_log.py
|
||||
v2_ops.py upload src/backend/app/services/quick_rule_service.py /tmp/quick_rule_service.py
|
||||
v2_ops.py upload src/backend/app/api/admin/quick_rules.py /tmp/quick_rules.py
|
||||
v2_ops.py upload src/backend/app/services/ai_handler.py /tmp/ai_handler.py
|
||||
v2_ops.py upload src/backend/app/services/routing_service.py /tmp/routing_service.py
|
||||
|
||||
ssh-via-jms 'for f in quick_rule.py quick_rule_audit_log.py quick_rule_service.py quick_rules.py ai_handler.py routing_service.py; do
|
||||
if [ -f /tmp/$f ]; then
|
||||
cp /tmp/$f /opt/wecom-it-desk/app/$(echo $f | sed "s|^|models/|; s|quick_rule_audit_log.py|models/&|; s|quick_rule_service.py|services/&|; s|quick_rules.py|api/admin/&|; s|ai_handler.py|services/&|; s|routing_service.py|services/&|")
|
||||
fi
|
||||
done'
|
||||
|
||||
# 必须保证 workers=1(部署铁律:多 worker 会致 WS 消息丢失)
|
||||
ssh-via-jms "docker compose --env-file .env up -d --force-recreate --no-deps backend"
|
||||
# 或:ssh-via-jms "docker compose restart backend"
|
||||
|
||||
# 探活
|
||||
sleep 5
|
||||
ssh-via-jms "docker exec wecom_it_backend python -c \"from app.models.quick_rule import QuickRule; print('OK', QuickRule.__tablename__)\""
|
||||
```
|
||||
|
||||
### 3.2 启动期验证
|
||||
|
||||
```bash
|
||||
# 1. 后端探活(外部)
|
||||
curl -fsS http://itsupport.servyou.com.cn/health
|
||||
|
||||
# 2. Swagger 文档(确认 quick-rules 路由注册)
|
||||
curl -fsS http://itsupport.servyou.com.cn/docs | grep quick-rules
|
||||
|
||||
# 3. 后端日志:必须看到 QuickRuleService.load_all 成功
|
||||
ssh-via-jms "docker logs wecom_it_backend --tail 200 2>&1 | grep -i 'quick_rule\\|loaded.*rules'"
|
||||
# 期望:loaded 51 rules (12 greeting, 33 routing_prefilter, 6 routing_target)
|
||||
```
|
||||
|
||||
### 3.3 业务侧验证(端到端、容器外,验证铁律)
|
||||
|
||||
```bash
|
||||
# 获取 token
|
||||
TOKEN=$(curl -s -X POST http://itsupport.servyou.com.cn/api/admin/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"simon","password":"<OTP>"}' | jq -r .data.token)
|
||||
|
||||
# 1. stats 接口
|
||||
curl -fsS http://itsupport.servyou.com.cn/api/admin/quick-rules/stats \
|
||||
-H "Authorization: Bearer $TOKEN" | jq .
|
||||
# 期望:{ "code":0, "data":{ "greeting":12, "routing_prefilter":33, "routing_target":6, "total":51 } }
|
||||
|
||||
# 2. 列表接口(greeting)
|
||||
curl -fsS "http://itsupport.servyou.com.cn/api/admin/quick-rules?rule_type=greeting&limit=5" \
|
||||
-H "Authorization: Bearer $TOKEN" | jq '.data | length'
|
||||
# 期望:5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 前端部署(仅 admin 端)
|
||||
|
||||
### 4.1 本地构建(ASCII 路径,前端构建 SOP)
|
||||
|
||||
```bash
|
||||
# 本地路径必须纯 ASCII(D:\资料\路径下 pnpm install 会卡死)
|
||||
cd D:\dev\wecom\src\frontend-admin
|
||||
|
||||
# 多路径同步铁律:中文路径下 D:\资料\03-项目开发\wecom_it_smart_desk 也要同步改
|
||||
# 否则 vite build 用的是 ASCII 路径下的旧代码
|
||||
|
||||
npm install # 不要用 pnpm install
|
||||
npm run build # 产物在 dist/
|
||||
|
||||
Compress-Archive -Path dist -DestinationPath dist-quickrules-v1.0.zip
|
||||
```
|
||||
|
||||
### 4.2 上传与部署(psftp 通道,禁止 elFinder)
|
||||
|
||||
```bash
|
||||
# 1. 上传 zip
|
||||
v2_ops.py upload D:\dev\wecom\src\frontend-admin\dist-quickrules-v1.0.zip dist-quickrules-v1.0.zip
|
||||
|
||||
# 2. 服务器端解压到 bind mount 目录(unzip 会直接产出 assets/ + index.html)
|
||||
ssh-via-jms 'cd /opt/wecom-it-desk/frontend-admin/dist-new && unzip -o /tmp/dist-quickrules-v1.0.zip'
|
||||
ssh-via-jms 'rm -rf /opt/wecom-it-desk/frontend-admin/dist && mv /opt/wecom-it-desk/frontend-admin/dist-new /opt/wecom-it-desk/frontend-admin/dist'
|
||||
|
||||
# 3. ★ 必须显式重启 nginx(bind mount 不会自动刷新新文件)
|
||||
ssh-via-jms 'docker restart wecom_it_nginx'
|
||||
|
||||
# 4. 验证
|
||||
curl -fsS http://itsupport.servyou.com.cn/itadmin/quick-rules | head -c 200
|
||||
# 期望:HTML 200 OK,title 含 "快速回复"
|
||||
```
|
||||
|
||||
### 4.3 浏览器自测(agent-browser skill)
|
||||
|
||||
```bash
|
||||
# 走 agent-browser 技能自动化测试,至少覆盖:
|
||||
# 1) /itadmin/quick-rules 列表加载
|
||||
# 2) /itadmin/quick-rules/greeting 编辑一条规则、断言持久化
|
||||
# 3) /itadmin/quick-rules/audit 审计日志可见
|
||||
# 4) 模拟"用户你好"→ 期望 AI 引导描述(不调用 Dify)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 端到端回归验证(验证铁律:容器内通过≠外部可用)
|
||||
|
||||
| # | 场景 | 输入 | 期望输出 | 验证方式 |
|
||||
|---|------|------|----------|----------|
|
||||
| T1 | 打招呼走 quick_rule,不调用 Dify | Mock 用户发"你好" | AI 回复引导描述问题,diffy_call_count=0 | WebSocket mock + logs |
|
||||
| T2 | 路由触发命中 quick_rule | Mock 用户发"打印机连不上" | 命中 prefilter→target,发送名片卡片 | 卡片 payload + 路由日志 |
|
||||
| T3 | 普通问题走 Dify(兼容路径) | Mock 用户发"电脑蓝屏了" | 走 Dify 主推理,diffy_call_count=1 | Dify 调用日志 |
|
||||
| T4 | 兜底:清空 DB → 重启后仍可用 | `DELETE FROM quick_rules; docker compose restart backend` | 行为不中断,加载日志 "fallback to hardcoded" | 启动日志 + 业务接口 |
|
||||
| T5 | 缓存刷新 | POST `/api/admin/quick-rules/refresh` | 60s 内 Redis keys 变化 | `redis-cli KEYS quick_rules:*` |
|
||||
|
||||
具体执行命令见配套测试用例 `TC-快速回复规则后台管理.md`(**待补,需建**)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 灰度上线
|
||||
|
||||
### 6.1 灰度开关(env + 代码双层)
|
||||
|
||||
| 开关 | 默认 | 启用时效果 |
|
||||
|------|------|-----------|
|
||||
| `QUICK_RULE_ENABLED`(env,**本次新增,必须加**) | `true` | `false` 时完全走硬编码兜底 |
|
||||
| `QUICK_RULE_AUTO_APPLY_THRESHOLD` | `0.85` | 调高(如 `0.99`)等于关闭自动应用 |
|
||||
| `QUICK_RULE_AUDIT_REVIEW_THRESHOLD` | `0.5` | 调高(如 `0.9`)等于全部走待审核 |
|
||||
|
||||
### 6.2 灰度策略
|
||||
|
||||
| 阶段 | 时长 | 动作 | 监控重点 |
|
||||
|------|------|------|----------|
|
||||
| Phase 1 观察期 | 1-3 天 | 启用新规则,日志标 `quick_rule.hit` | API 成功率、命中占比 |
|
||||
| Phase 2 人工编辑期 | 4-7 天 | 运营通过后台编辑 | 误判率、日志异常 |
|
||||
| Phase 3 完全替代 | 7 天+ | 保留硬编码兜底 | 同上 |
|
||||
|
||||
### 6.3 回滚(完整三路回滚)
|
||||
|
||||
```bash
|
||||
# Step 1:DB 回滚(降级,按部署铁律:只 downgrade 不 drop 备份)
|
||||
ssh-via-jms "docker exec wecom_it_backend alembic downgrade -1"
|
||||
|
||||
# Step 2:清空规则(保留表结构,使代码加载硬编码兜底)
|
||||
ssh-via-jms "docker exec -i wecom_it_postgres psql -U postgres -d wecom_it_desk -c \"DELETE FROM quick_rules;\""
|
||||
|
||||
# Step 3:后端代码回滚(git)
|
||||
ssh-via-jms "cd /opt/wecom-it-desk && git log --oneline -5"
|
||||
ssh-via-jms "cd /opt/wecom-it-desk && git revert --no-edit HEAD"
|
||||
|
||||
# Step 4:前端 dist 回滚
|
||||
v2_ops.py upload archives/frontend-admin-dist-v*.zip rollback.zip
|
||||
ssh-via-jms 'cd /opt/wecom-it-desk/frontend-admin && rm -rf dist && mkdir dist && cd dist && unzip /tmp/rollback.zip'
|
||||
ssh-via-jms 'docker restart wecom_it_nginx'
|
||||
|
||||
# Step 5:环境变量回滚(可选,但建议保持 0 风险)
|
||||
# 在 .env 中 QUICK_RULE_ENABLED=false,restart backend
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 监控指标
|
||||
|
||||
### 7.1 关键指标(明确阈值,可告警)
|
||||
|
||||
| 指标 | 采集方式 | 告警阈值 | 通知渠道 |
|
||||
|------|---------|----------|----------|
|
||||
| `/api/admin/quick-rules/*` 调用成功率 | nginx access log 5xx 占比 | > 1% | 企微机器人 |
|
||||
| 规则加载耗时(启动期) | `docker logs wecom_it_backend` | > 3s | 日志告警 |
|
||||
| 缓存命中率 | `INFO logs/smart_desk.log` 中 `quick_rule.hit / quick_rule.miss` | < 70%(24h 均值) | 企微机器人 |
|
||||
| 兜底触发次数 | 日志 `quick_rule.fallback` | 24h 内 > 100 | 企微机器人 |
|
||||
| 自动应用次数突增 | `quick_rule_audit_log` | 1h 内 > 50 | 企微机器人 |
|
||||
| Alembic 迁移失败 | 启动日志 | 任意 1 次 ERROR | P1 告警 |
|
||||
|
||||
### 7.2 日志查询 SOP
|
||||
|
||||
```bash
|
||||
# 启动期规则加载
|
||||
ssh-via-jms "docker logs wecom_it_backend --since 10m 2>&1 | grep -i 'quick_rule'"
|
||||
|
||||
# 命中/兜底实时监控
|
||||
ssh-via-jms "tail -f /opt/wecom-it-desk/logs/wecom-it-desk.log | grep quick_rule"
|
||||
|
||||
# Redis 缓存
|
||||
ssh-via-jms "docker exec wecom_it_redis redis-cli KEYS 'quick_rules:*'"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 常见问题
|
||||
|
||||
### Q1:数据库表已存在错误(原文"数据库插件已存在错误"为错别字)
|
||||
|
||||
```text
|
||||
错误:relation "quick_rules" already exists
|
||||
原因:alembic 055 已成功建表,重复执行会触发
|
||||
解决:属正常现象,跳过或继续下一步
|
||||
```
|
||||
|
||||
### Q2:初始化数据重复插入失败
|
||||
|
||||
```text
|
||||
错误:duplicate key value violates unique constraint
|
||||
原因:脚本已使用 ON CONFLICT DO NOTHING,重复执行安全
|
||||
解决:直接忽略;如需强制重置,先 TRUNCATE quick_rules
|
||||
```
|
||||
|
||||
### Q3:缓存未刷新
|
||||
|
||||
```bash
|
||||
curl -X POST "http://itsupport.servyou.com.cn/api/admin/quick-rules/refresh" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 或直接清 Redis
|
||||
ssh-via-jms "docker exec wecom_it_redis redis-cli DEL $(docker exec wecom_it_redis redis-cli KEYS 'quick_rules:*' | tr '\n' ' ')"
|
||||
```
|
||||
|
||||
### Q4:规则加载后业务侧未生效
|
||||
|
||||
```text
|
||||
排查路径:
|
||||
1) docker logs wecom_it_backend | grep "loaded.*rules" 确认 51 条加载
|
||||
2) curl /api/admin/quick-rules/stats 确认 DB 与 API 一致
|
||||
3) tail logs/wecom-it-desk.log | grep quick_rule.fallback 看是否走兜底
|
||||
4) REDIS 缓存是否过期(TTL 默认 300s),可手动 DEL 后重试
|
||||
```
|
||||
|
||||
### Q5:缓存验证时找不到 admin token
|
||||
|
||||
```bash
|
||||
TOKEN=$(curl -s -X POST http://itsupport.servyou.com.cn/api/admin/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"simon","password":"<OTP>"}' | jq -r .data.token)
|
||||
```
|
||||
|
||||
### Q6:前端 dist 更新后页面没刷新
|
||||
|
||||
```text
|
||||
原因:bind mount 不会自动同步;nginx 也不会主动重读
|
||||
解决:必须 docker restart wecom_it_nginx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 变更记录
|
||||
|
||||
| 日期 | 版本 | 变更内容 | 变更人 | 变更原因 | 影响范围 |
|
||||
|------|------|----------|--------|----------|----------|
|
||||
| 2026-07-27 | v1.0 | 初版 | Simon | 新功能首次上线 | - |
|
||||
| 2026-07-28 | v1.1 | 整改:补全头部模板/关联文档/前置条件/.env变更清单/回滚三路方案/灰度开关/可执行验证命令;修复 `ssh sxn@`、`$(date)`、elFinder 三处命令级错误;命名规范说明追加 | Simon | 复盘发现不符合规范 §2.3/§5.2 + 命令违反部署铁律 | 本文档全部重写,零代码变更 |
|
||||
| 2026-07-28 | v1.2 | 管理后台 `/quick-rules` 顶部 3 张重复统计卡片移除,仅保留标签导航及 count 徽标;后端 stats 接口与部署流程保持不变 | Simon | 页面信息冗余 | 仅管理后台前端展示,部署位置与流程不变 |
|
||||
@@ -0,0 +1,128 @@
|
||||
# Dify 工作流 System Prompt 改造指南
|
||||
|
||||
## 改造目标
|
||||
|
||||
将 Dify 输出从 `action` 字段改为 `intent` 标签,让后端根据 intent 查询资产库生成真正的推荐卡片。
|
||||
|
||||
## 当前输出格式(需改造)
|
||||
|
||||
```json
|
||||
{
|
||||
"text": "回答文本...",
|
||||
"action": {
|
||||
"type": "approval_card",
|
||||
"title": "软件安装审批",
|
||||
"description": "申请安装 Adobe 系列软件",
|
||||
"approval_type": "software_install",
|
||||
"confidence": 0.95
|
||||
},
|
||||
"options": [...],
|
||||
"diagnosis_stage": "diagnosing"
|
||||
}
|
||||
```
|
||||
|
||||
## 目标输出格式
|
||||
|
||||
```json
|
||||
{
|
||||
"text": "给用户的回答文本(50-200字)",
|
||||
"intent": "用户意图标签",
|
||||
"need_approval": 是否需要审批入口 (true/false),
|
||||
"need_asset": 是否需要资产推荐 (true/false),
|
||||
"asset_keywords": ["关键词1", "关键词2"],
|
||||
"options": [
|
||||
{"label": "选项文字", "value": "选项值"}
|
||||
],
|
||||
"diagnosis_stage": "greeting|diagnosing|resolved|transfer"
|
||||
}
|
||||
```
|
||||
|
||||
## 改造步骤
|
||||
|
||||
### 1. 登录 Dify Console
|
||||
|
||||
访问:`http://yw-dify.dc.servyou-it.com/console`
|
||||
|
||||
### 2. 找到目标应用
|
||||
|
||||
找到「智能IT支持-员工咨询」应用(App ID: `8f0f3d62-f63d-4cf3-815e-b10529c66f1d`)
|
||||
|
||||
### 3. 进入工作流编辑
|
||||
|
||||
1. 点击应用名称进入详情
|
||||
2. 点击「编辑」
|
||||
3. 进入「编排」页面
|
||||
|
||||
### 4. 找到 LLM 节点
|
||||
|
||||
找到输出 JSON 结构的 LLM 节点(通常在"开始"节点之后的第一个 LLM 节点)
|
||||
|
||||
### 5. 修改 System Prompt
|
||||
|
||||
在 LLM 节点的「系统提示词」中添加以下内容(追加到现有 Prompt 末尾):
|
||||
|
||||
```
|
||||
## 输出格式要求
|
||||
|
||||
你是一个 IT 智能助手。请根据用户问题,输出以下 JSON 结构:
|
||||
|
||||
{
|
||||
"text": "给用户的回答文本(50-200字)",
|
||||
"intent": "用户意图标签",
|
||||
"need_approval": 是否需要审批入口 (true/false),
|
||||
"need_asset": 是否需要资产推荐 (true/false),
|
||||
"asset_keywords": ["关键词1", "关键词2"],
|
||||
"options": [
|
||||
{"label": "选项文字", "value": "选项值"}
|
||||
],
|
||||
"diagnosis_stage": "greeting|diagnosing|resolved|transfer"
|
||||
}
|
||||
|
||||
## 意图标签说明
|
||||
|
||||
intent 字段可选值:
|
||||
- software_install: 软件安装
|
||||
- hardware_issue: 硬件问题
|
||||
- network_vpn: 网络/VPN 问题
|
||||
- printer: 打印机问题
|
||||
- account_permission: 账号权限问题
|
||||
- email_outlook: 邮箱问题
|
||||
- mobile_device: 移动设备问题
|
||||
- other: 其他问题
|
||||
|
||||
## 资产推荐规则
|
||||
|
||||
当用户询问以下内容时,设置 need_asset=true 并指定 asset_keywords:
|
||||
- VPN 相关 → asset_keywords: ["vpn"]
|
||||
- 打印机相关 → asset_keywords: ["打印机", "打印机驱动"]
|
||||
- 软件安装 → asset_keywords: ["软件安装"]
|
||||
- 邮箱问题 → asset_keywords: ["outlook", "邮箱"]
|
||||
- 账号权限 → asset_keywords: ["账号", "权限"]
|
||||
- 网络问题 → asset_keywords: ["网络", "wifi"]
|
||||
|
||||
重要:不要在 text 中推荐具体的下载链接或服务器地址,这些信息由系统根据 asset_keywords 智能匹配后推送到右侧栏。
|
||||
```
|
||||
|
||||
### 6. 发布工作流
|
||||
|
||||
点击右上角「发布」按钮
|
||||
|
||||
### 7. 验证
|
||||
|
||||
在「发布」页面点击「运行」测试,验证输出格式是否符合预期。
|
||||
|
||||
## 回滚方案
|
||||
|
||||
如果新版本有问题,可以在 Dify Console 的「版本历史」中找到上一个版本,点击「恢复」即可回滚。
|
||||
|
||||
## 后端适配
|
||||
|
||||
后端代码 `asset_recommend_service.py` 已支持处理新的 `intent` 格式。
|
||||
|
||||
当 Dify 返回 `need_asset=true` + `asset_keywords` 时,后端会根据关键词查询 `assets.yaml` 中的资产信息,生成 L1 推荐卡片。
|
||||
|
||||
## 预期效果
|
||||
|
||||
用户说"VPN 连不上":
|
||||
- **中间栏**:AI 回复排查步骤
|
||||
- **右侧栏 L1**:VPN 相关信息(客户端下载、服务器地址、申请审批)
|
||||
@@ -0,0 +1,635 @@
|
||||
# 健康检查 + 错误码 + 日志结构化 审计与改进方案
|
||||
|
||||
**审计日期**: 2026-06-15
|
||||
**审计人**: Claude(满载跑批)
|
||||
**关联**: [[风险跟踪表]] / [[后端架构]] / [[Dockerfile优化与镜像审计]]
|
||||
|
||||
---
|
||||
|
||||
## 📌 1. 健康检查现状
|
||||
|
||||
### 1.1 当前实现
|
||||
|
||||
**端点**: `backend/app/main.py:506`
|
||||
|
||||
```python
|
||||
@app.get("/health", tags=["系统"])
|
||||
async def health_check():
|
||||
"""健康检查端点。"""
|
||||
return {"status": "ok", "service": "wecom-it-smart-desk"}
|
||||
```
|
||||
|
||||
**Docker compose healthcheck**:
|
||||
```yaml
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health').read()"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
```
|
||||
|
||||
### 1.2 问题清单
|
||||
|
||||
| # | 问题 | 严重度 | 影响 |
|
||||
|---|---|---|---|
|
||||
| H-1 | `/health` 不验证 DB 连接 | 🟠 中 | DB 挂了,但 healthcheck 还显示 OK |
|
||||
| H-2 | `/health` 不验证 Redis 连接 | 🟠 中 | Redis 挂了,但 healthcheck 还显示 OK |
|
||||
| H-3 | `/health` 不报告版本/build | 🟡 | 排障不便 |
|
||||
| H-4 | `/health` 永远是 200,无 degraded 状态 | 🟡 | 难区分"在线但降级" |
|
||||
| H-5 | 无 `/ready` 和 `/live` 区分 | 🟡 | K8s 不友好 |
|
||||
| H-6 | Docker healthcheck 改用 urllib 已修 ✅(P1-3) | 🟢 | 已 done |
|
||||
|
||||
### 1.3 改进版(完整 healthcheck)
|
||||
|
||||
```python
|
||||
# backend/app/api/health.py(新建)
|
||||
|
||||
import time
|
||||
import psutil
|
||||
from typing import Dict, Any
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from sqlalchemy import text
|
||||
from app.database import async_session_maker
|
||||
from app.config import settings
|
||||
from app.utils.token_manager import get_token_manager
|
||||
|
||||
router = APIRouter(tags=["系统"])
|
||||
START_TIME = time.time()
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
async def health_check():
|
||||
"""Liveness probe - 进程是否存活
|
||||
|
||||
适用: K8s livenessProbe / Docker healthcheck
|
||||
返回: 总是 200,只要进程没崩
|
||||
"""
|
||||
return {
|
||||
"status": "ok",
|
||||
"service": "wecom-it-smart-desk",
|
||||
"uptime_seconds": int(time.time() - START_TIME),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/ready")
|
||||
async def readiness_check():
|
||||
"""Readiness probe - 进程是否准备好接流量
|
||||
|
||||
适用: K8s readinessProbe / 负载均衡
|
||||
验证: DB + Redis 实际连通性
|
||||
"""
|
||||
checks = {
|
||||
"database": False,
|
||||
"redis": False,
|
||||
"wecom_token": False,
|
||||
}
|
||||
|
||||
# 1. DB 检查
|
||||
try:
|
||||
async with async_session_maker() as session:
|
||||
result = await session.execute(text("SELECT 1"))
|
||||
result.scalar()
|
||||
checks["database"] = True
|
||||
except Exception as e:
|
||||
checks["database_error"] = str(e)[:200]
|
||||
|
||||
# 2. Redis 检查
|
||||
try:
|
||||
tm = get_token_manager()
|
||||
client = await tm.get_redis()
|
||||
await client.ping()
|
||||
checks["redis"] = True
|
||||
except Exception as e:
|
||||
checks["redis_error"] = str(e)[:200]
|
||||
|
||||
# 3. 企微 token 检查(可选)
|
||||
try:
|
||||
tm = get_token_manager()
|
||||
token = await tm.get_access_token()
|
||||
checks["wecom_token"] = bool(token)
|
||||
except Exception as e:
|
||||
checks["wecom_error"] = str(e)[:200]
|
||||
|
||||
all_ok = all(v for k, v in checks.items() if not k.endswith("_error"))
|
||||
status_code = 200 if all_ok else 503
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status_code,
|
||||
content={
|
||||
"status": "ready" if all_ok else "degraded",
|
||||
"service": "wecom-it-smart-desk",
|
||||
"uptime_seconds": int(time.time() - START_TIME),
|
||||
"checks": checks,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/metrics")
|
||||
async def metrics():
|
||||
"""Prometheus metrics 端点(轻量版)
|
||||
|
||||
适用: Prometheus 抓取
|
||||
输出: 关键业务/技术指标
|
||||
"""
|
||||
process = psutil.Process()
|
||||
|
||||
return {
|
||||
"process": {
|
||||
"cpu_percent": process.cpu_percent(),
|
||||
"memory_mb": process.memory_info().rss / 1024 / 1024,
|
||||
"threads": process.num_threads(),
|
||||
"uptime_seconds": int(time.time() - START_TIME),
|
||||
},
|
||||
"system": {
|
||||
"cpu_percent": psutil.cpu_percent(),
|
||||
"memory_percent": psutil.virtual_memory().percent,
|
||||
"disk_percent": psutil.disk_usage('/').percent,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@router.get("/version")
|
||||
async def version():
|
||||
"""版本信息端点
|
||||
|
||||
用途: 排障 / 部署确认
|
||||
"""
|
||||
import os
|
||||
return {
|
||||
"service": "wecom-it-smart-desk",
|
||||
"version": os.getenv("APP_VERSION", "dev"),
|
||||
"git_sha": os.getenv("GIT_SHA", "unknown")[:8],
|
||||
"build_time": os.getenv("BUILD_TIME", "unknown"),
|
||||
"python": "3.12",
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 Docker compose 更新
|
||||
|
||||
```yaml
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health').read()"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
# 高级(可选,等 K8s 迁移时)
|
||||
readiness:
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/ready').read()"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📌 2. 错误码体系现状与改进
|
||||
|
||||
### 2.1 现状(`backend/app/utils/response.py`)
|
||||
|
||||
**已有错误码**(18 个):
|
||||
- **1000+ 通用** (5): ERR_PARAMS / UNAUTHORIZED / NOT_FOUND / FORBIDDEN / INTERNAL
|
||||
- **2000+ 企微** (6): WECOM_TOKEN / SEND / DECRYPT / ENCRYPT / VERIFY / USER_INFO
|
||||
- **3000+ 业务** (7): AGENT_OFFLINE / CONVERSATION_RESOLVED / CONVERSATION_NOT_FOUND / AGENT_NOT_FOUND / AGENT_BUSY / DUPLICATE_ASSIGN / GRAB_*
|
||||
|
||||
**格式**:
|
||||
```json
|
||||
{"code": 0, "data": {}, "message": "success"}
|
||||
```
|
||||
|
||||
### 2.2 问题清单
|
||||
|
||||
| # | 问题 | 严重度 | 解决 |
|
||||
|---|---|---|---|
|
||||
| E-1 | 错误码无标准枚举类(只常量) | 🟡 | 加 `ErrorCode` Enum |
|
||||
| E-2 | HTTP 200 + code 非 0(违反 REST 习惯) | 🟡 | 评估:4xx 5xx 也可,跟前端约定 |
|
||||
| E-3 | 没错误追踪 ID(correlation_id) | 🟠 | 加 `trace_id` 字段 |
|
||||
| E-4 | 错误响应没 `documentation_url` | 🟢 | 加上,链到文档 |
|
||||
| E-5 | i18n 缺失(中文硬编码) | 🟡 | 错误消息 i18n 化 |
|
||||
| E-6 | 前端错误处理分散(无统一拦截) | 🟠 | 加 axios 拦截器 + 错误码映射表 |
|
||||
|
||||
### 2.3 改进版错误码体系
|
||||
|
||||
**新建 `backend/app/utils/error_codes.py`**:
|
||||
```python
|
||||
# =============================================================================
|
||||
# 错误码体系 - 标准枚举
|
||||
# =============================================================================
|
||||
# 规范:
|
||||
# - 0 = 成功
|
||||
# - 1xxx = 通用错误
|
||||
# - 2xxx = 鉴权/会话
|
||||
# - 3xxx = 企微 API
|
||||
# - 4xxx = 业务 - 会话
|
||||
# - 5xxx = 业务 - 坐席
|
||||
# - 6xxx = 业务 - 配置
|
||||
# - 7xxx = 集成外部系统
|
||||
# - 9xxx = 兜底
|
||||
# =============================================================================
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ErrorCode(int, Enum):
|
||||
"""统一错误码枚举"""
|
||||
|
||||
# 0: 成功
|
||||
SUCCESS = 0
|
||||
|
||||
# 1xxx: 通用错误
|
||||
PARAMS_INVALID = 1001 # 参数错误
|
||||
UNAUTHORIZED = 1002 # 未授权
|
||||
NOT_FOUND = 1003 # 资源不存在
|
||||
FORBIDDEN = 1004 # 无权限
|
||||
INTERNAL = 1005 # 服务器错误
|
||||
RATE_LIMITED = 1006 # 限流
|
||||
SERVICE_UNAVAILABLE = 1007 # 服务不可用
|
||||
TIMEOUT = 1008 # 超时
|
||||
|
||||
# 2xxx: 鉴权
|
||||
AUTH_TOKEN_MISSING = 2001 # token 缺失
|
||||
AUTH_TOKEN_EXPIRED = 2002 # token 过期
|
||||
AUTH_TOKEN_INVALID = 2003 # token 无效
|
||||
AUTH_OTP_REQUIRED = 2004 # 需要 OTP
|
||||
AUTH_OTP_INVALID = 2005 # OTP 错误
|
||||
AUTH_PASSWORD_WRONG = 2006 # 密码错误
|
||||
AUTH_AGENT_DISABLED = 2007 # 坐席已禁用
|
||||
|
||||
# 3xxx: 企微 API
|
||||
WECOM_TOKEN_FAIL = 3001 # 企微 token 获取失败
|
||||
WECOM_SEND_FAIL = 3002 # 企微消息发送失败
|
||||
WECOM_DECRYPT_FAIL = 3003 # 企微消息解密失败
|
||||
WECOM_ENCRYPT_FAIL = 3004 # 企微消息加密失败
|
||||
WECOM_VERIFY_FAIL = 3005 # 企微回调签名验证失败
|
||||
WECOM_USER_INFO_FAIL = 3006 # 企微用户信息获取失败
|
||||
WECOM_API_ERROR = 3099 # 企微 API 通用错误
|
||||
|
||||
# 4xxx: 业务 - 会话
|
||||
CONV_NOT_FOUND = 4001 # 会话不存在
|
||||
CONV_RESOLVED = 4002 # 会话已结单
|
||||
CONV_NO_AGENT = 4003 # 无可用坐席
|
||||
CONV_DUPLICATE_ASSIGN = 4004 # 重复分配
|
||||
CONV_GRAB_DENIED = 4005 # 抢单失败
|
||||
|
||||
# 5xxx: 业务 - 坐席
|
||||
AGENT_NOT_FOUND = 5001 # 坐席不存在
|
||||
AGENT_OFFLINE = 5002 # 坐席离线
|
||||
AGENT_BUSY = 5003 # 坐席满载
|
||||
AGENT_GRAB_SELF = 5004 # 不能接手自己的会话
|
||||
AGENT_GRAB_NOT_SERVING = 5005 # 只能接手服务中的会话
|
||||
|
||||
# 6xxx: 业务 - 配置
|
||||
CONFIG_NOT_FOUND = 6001 # 配置不存在
|
||||
CONFIG_INVALID = 6002 # 配置值无效
|
||||
|
||||
# 7xxx: 集成外部
|
||||
HUORONG_API_FAIL = 7001 # 火绒 API
|
||||
LIANRUAN_API_FAIL = 7002 # 联软 API
|
||||
ATRUST_API_FAIL = 7003 # aTrust API
|
||||
EHR_API_FAIL = 7004 # eHR API
|
||||
DIFY_API_FAIL = 7005 # Dify API
|
||||
|
||||
# 9xxx: 兜底
|
||||
UNKNOWN = 9999
|
||||
|
||||
|
||||
# 错误码 → HTTP 状态码(可选,默认 200)
|
||||
HTTP_STATUS_MAP = {
|
||||
ErrorCode.SUCCESS: 200,
|
||||
ErrorCode.PARAMS_INVALID: 422,
|
||||
ErrorCode.UNAUTHORIZED: 401,
|
||||
ErrorCode.NOT_FOUND: 404,
|
||||
ErrorCode.FORBIDDEN: 403,
|
||||
ErrorCode.INTERNAL: 500,
|
||||
ErrorCode.RATE_LIMITED: 429,
|
||||
ErrorCode.SERVICE_UNAVAILABLE: 503,
|
||||
ErrorCode.TIMEOUT: 504,
|
||||
|
||||
ErrorCode.AUTH_TOKEN_MISSING: 401,
|
||||
ErrorCode.AUTH_TOKEN_EXPIRED: 401,
|
||||
ErrorCode.AUTH_TOKEN_INVALID: 401,
|
||||
ErrorCode.AUTH_OTP_REQUIRED: 401,
|
||||
ErrorCode.AUTH_OTP_INVALID: 401,
|
||||
ErrorCode.AUTH_PASSWORD_WRONG: 401,
|
||||
ErrorCode.AUTH_AGENT_DISABLED: 403,
|
||||
|
||||
# 业务错误默认 200,通过 code 区分
|
||||
# 但具体可调,如 4xxx 资源类 404,5xxx 状态类 409
|
||||
}
|
||||
```
|
||||
|
||||
**更新 `response.py`**:
|
||||
```python
|
||||
from app.utils.error_codes import ErrorCode, HTTP_STATUS_MAP
|
||||
|
||||
|
||||
def error_response(
|
||||
code: ErrorCode,
|
||||
message: str,
|
||||
data: Any = None,
|
||||
trace_id: str = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""构建错误响应(增加 trace_id)"""
|
||||
return {
|
||||
"code": int(code),
|
||||
"message": message,
|
||||
"data": data or {},
|
||||
"trace_id": trace_id,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
|
||||
class AppException(Exception):
|
||||
def __init__(
|
||||
self,
|
||||
code: ErrorCode,
|
||||
message: str,
|
||||
data: Any = None,
|
||||
http_status: int = None,
|
||||
):
|
||||
self.code = code
|
||||
self.message = message
|
||||
self.data = data
|
||||
self.http_status = http_status or HTTP_STATUS_MAP.get(code, 200)
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
async def app_exception_handler(request: Request, exc: AppException) -> JSONResponse:
|
||||
# 生成 trace_id
|
||||
import uuid
|
||||
trace_id = request.headers.get("X-Request-ID") or str(uuid.uuid4())
|
||||
|
||||
# 记录到日志
|
||||
logger.warning(
|
||||
f"[{trace_id}] {request.method} {request.url.path} "
|
||||
f"-> {exc.code.value} {exc.message}"
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=exc.http_status,
|
||||
content=error_response(exc.code, exc.message, exc.data, trace_id),
|
||||
headers={"X-Trace-ID": trace_id},
|
||||
)
|
||||
```
|
||||
|
||||
### 2.4 前端错误码映射(axios 拦截器)
|
||||
|
||||
**新建 `frontend-admin/src/api/error-handler.ts`**(每个前端类似):
|
||||
```typescript
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
// 错误码 → 用户提示
|
||||
const ERROR_MESSAGES: Record<number, string> = {
|
||||
1001: '参数错误,请检查输入',
|
||||
1002: '登录已过期,请重新登录',
|
||||
1003: '资源不存在',
|
||||
1004: '无权限访问',
|
||||
1005: '服务器错误,请稍后重试',
|
||||
1006: '操作过快,请稍候再试',
|
||||
2001: '请先登录',
|
||||
2002: '登录已过期',
|
||||
2003: '身份验证失败',
|
||||
2004: '请输入动态码',
|
||||
2005: '动态码错误',
|
||||
2006: '密码错误',
|
||||
4001: '会话不存在',
|
||||
4002: '会话已结束',
|
||||
5001: '坐席不存在',
|
||||
5002: '坐席离线',
|
||||
5003: '坐席已满载',
|
||||
9999: '未知错误',
|
||||
}
|
||||
|
||||
export function handleError(code: number, message: string, traceId?: string) {
|
||||
const userMsg = ERROR_MESSAGES[code] || message || '操作失败'
|
||||
|
||||
// 特殊处理
|
||||
if ([1002, 2001, 2002, 2003].includes(code)) {
|
||||
// 跳登录
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/login'
|
||||
}
|
||||
|
||||
ElMessage.error(userMsg)
|
||||
|
||||
// 开发环境显示 trace_id
|
||||
if (import.meta.env.DEV && traceId) {
|
||||
console.error(`[TraceID: ${traceId}] Code: ${code}, Message: ${message}`)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📌 3. 日志结构化
|
||||
|
||||
### 3.1 现状
|
||||
|
||||
```python
|
||||
# backend/app/main.py
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s",
|
||||
)
|
||||
```
|
||||
|
||||
**问题**:
|
||||
- 🟡 文本格式,不易查询/聚合
|
||||
- 🟡 无 trace_id
|
||||
- 🟡 无 request/response 记录
|
||||
- 🟡 无结构化字段(用户/会话/操作)
|
||||
|
||||
### 3.2 改进版
|
||||
|
||||
**新建 `backend/app/utils/logging_config.py`**:
|
||||
```python
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
from contextvars import ContextVar
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
# 请求上下文
|
||||
request_id_var: ContextVar[Optional[str]] = ContextVar('request_id', default=None)
|
||||
user_id_var: ContextVar[Optional[str]] = ContextVar('user_id', default=None)
|
||||
|
||||
|
||||
class JSONFormatter(logging.Formatter):
|
||||
"""JSON 格式化器 - 适合 ELK / Loki / CloudWatch 解析"""
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
log_data = {
|
||||
"timestamp": self.formatTime(record),
|
||||
"level": record.levelname,
|
||||
"logger": record.name,
|
||||
"message": record.getMessage(),
|
||||
}
|
||||
|
||||
# 上下文
|
||||
if request_id_var.get():
|
||||
log_data["request_id"] = request_id_var.get()
|
||||
if user_id_var.get():
|
||||
log_data["user_id"] = user_id_var.get()
|
||||
|
||||
# 额外字段
|
||||
if hasattr(record, "extra_data"):
|
||||
log_data.update(record.extra_data)
|
||||
|
||||
# 异常
|
||||
if record.exc_info:
|
||||
log_data["exception"] = self.formatException(record.exc_info)
|
||||
|
||||
return json.dumps(log_data, ensure_ascii=False)
|
||||
|
||||
|
||||
def setup_logging(level: str = "INFO", json_format: bool = True):
|
||||
"""配置日志"""
|
||||
root = logging.getLogger()
|
||||
root.setLevel(level)
|
||||
|
||||
# 清除已有 handler
|
||||
for handler in root.handlers[:]:
|
||||
root.removeHandler(handler)
|
||||
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
if json_format:
|
||||
handler.setFormatter(JSONFormatter())
|
||||
else:
|
||||
handler.setFormatter(logging.Formatter(
|
||||
"[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s"
|
||||
))
|
||||
root.addHandler(handler)
|
||||
|
||||
|
||||
# 业务日志辅助函数
|
||||
def log_business(
|
||||
event: str,
|
||||
*,
|
||||
user_id: str = None,
|
||||
conversation_id: str = None,
|
||||
agent_id: str = None,
|
||||
**kwargs
|
||||
):
|
||||
"""记录业务日志(结构化)"""
|
||||
extra_data = {
|
||||
"event": event,
|
||||
"user_id": user_id,
|
||||
"conversation_id": conversation_id,
|
||||
"agent_id": agent_id,
|
||||
**kwargs,
|
||||
}
|
||||
logger.info(f"business_event: {event}", extra={"extra_data": extra_data})
|
||||
|
||||
|
||||
def log_security(
|
||||
event: str,
|
||||
*,
|
||||
user_id: str = None,
|
||||
ip: str = None,
|
||||
**kwargs
|
||||
):
|
||||
"""记录安全日志(单独级别,便于审计)"""
|
||||
extra_data = {
|
||||
"event": event,
|
||||
"category": "security",
|
||||
"user_id": user_id,
|
||||
"ip": ip,
|
||||
**kwargs,
|
||||
}
|
||||
logger.warning(f"security_event: {event}", extra={"extra_data": extra_data})
|
||||
```
|
||||
|
||||
**中间件 - 注入 request_id**:
|
||||
```python
|
||||
# backend/app/main.py
|
||||
from app.utils.logging_config import setup_logging, request_id_var, user_id_var
|
||||
import uuid
|
||||
|
||||
@app.middleware("http")
|
||||
async def request_id_middleware(request: Request, call_next):
|
||||
# 拿/创 trace_id
|
||||
trace_id = request.headers.get("X-Request-ID") or str(uuid.uuid4())
|
||||
request_id_var.set(trace_id)
|
||||
|
||||
# 记录请求开始
|
||||
start = time.time()
|
||||
logger.info(
|
||||
f"request_start: {request.method} {request.url.path}",
|
||||
extra={"extra_data": {
|
||||
"method": request.method,
|
||||
"path": request.url.path,
|
||||
"client": request.client.host if request.client else "?",
|
||||
}}
|
||||
)
|
||||
|
||||
response = await call_next(request)
|
||||
|
||||
# 记录请求结束
|
||||
duration = time.time() - start
|
||||
logger.info(
|
||||
f"request_end: {response.status_code} in {duration:.3f}s",
|
||||
extra={"extra_data": {
|
||||
"method": request.method,
|
||||
"path": request.url.path,
|
||||
"status": response.status_code,
|
||||
"duration_ms": int(duration * 1000),
|
||||
}}
|
||||
)
|
||||
|
||||
response.headers["X-Request-ID"] = trace_id
|
||||
return response
|
||||
```
|
||||
|
||||
### 3.3 日志聚合方案
|
||||
|
||||
| 方案 | 适用 | 接入成本 |
|
||||
|---|---|---|
|
||||
| **stdout + Docker logs** | 小规模 / 排障 | 🟢 0 |
|
||||
| **Loki + Promtail** | 中规模 / 查日志 | 🟡 中 |
|
||||
| **ELK (Elasticsearch + Logstash + Kibana)** | 大规模 / 全文搜索 | 🟠 高 |
|
||||
| **CloudWatch / 阿里云 SLS** | 公有云 | 🟡 看云 |
|
||||
|
||||
**短期**: 走 stdout,Docker 收集到 `/var/log/wecom-it-desk/*.log`,脚本 + grep 查
|
||||
**中期**: Loki + Grafana(本地服务器部署)
|
||||
**长期**: ELK / 云原生日志
|
||||
|
||||
---
|
||||
|
||||
## 📌 4. 实施路径
|
||||
|
||||
### 4.1 立即(本次跑批)
|
||||
|
||||
- [x] 审计报告写完(本文件)
|
||||
- [ ] 加 `backend/app/utils/error_codes.py` (Enum)
|
||||
- [ ] 加 `backend/app/utils/logging_config.py` (JSON formatter)
|
||||
- [ ] 更新 `main.py` 加 `/ready` `/metrics` `/version` 端点
|
||||
- [ ] 加 request_id 中间件
|
||||
|
||||
### 4.2 下周
|
||||
|
||||
- [ ] 4 前端加 `api/error-handler.ts`
|
||||
- [ ] 加 4 前端 axios 拦截器(捕获 trace_id)
|
||||
- [ ] 加 `.env` 配置 `LOG_LEVEL=INFO` + `LOG_FORMAT=json`
|
||||
|
||||
### 4.3 季度
|
||||
|
||||
- [ ] Loki + Promtail 部署
|
||||
- [ ] Grafana 仪表盘(Loki 数据源)
|
||||
- [ ] 关键业务事件告警(登录失败/坐席离线)
|
||||
|
||||
---
|
||||
|
||||
## 📌 5. 关联文档
|
||||
|
||||
- [[风险跟踪表]] M-3(无统一错误码)/ M-5(无健康检查)
|
||||
- [[后端架构]] §5 错误处理
|
||||
- [[Dockerfile优化与镜像审计]] - healthcheck
|
||||
- [[前端审计报告]] U-2(全局错误边界)
|
||||
|
||||
---
|
||||
|
||||
*本审计是 2026-06-15 Claude 满载跑批产出,待评审*
|
||||
@@ -0,0 +1,121 @@
|
||||
# 配置清单与环境变量
|
||||
|
||||
> **版本**: v1.0 | **更新日期**: 2026-07-20
|
||||
|
||||
---
|
||||
|
||||
## 1. Dify AI 服务配置
|
||||
|
||||
### 1.1 超时配置
|
||||
|
||||
| 环境变量 | 代码默认 | 生产配置 | 说明 |
|
||||
|----------|----------|----------|------|
|
||||
| `DIFY_NATIVE_TIMEOUT` | 20s | 20s | Dify 原生直连超时(秒) |
|
||||
| `DIFY_PROXY_TIMEOUT` | 20s | 20s | Dify 代理路径超时(秒) |
|
||||
| `DIFY_TIMEOUT` | 30s | 30s | 通用 Dify 超时(保留给 wingman 等旧路径) |
|
||||
| `APPROVAL_DIFY_TIMEOUT` | 8s | 8s | 审批路由检测超时 |
|
||||
| `DIFY_TRIAGE_TIMEOUT` | 5s | 5s | 分诊路由超时 |
|
||||
|
||||
> **设计约束**:`native_timeout + proxy_timeout + 开销 < wait_for(30s)`,wait_for(30s) 保持为最后防线。
|
||||
> - 2026-07-20: 从 12s 调整为 20s(用户反馈高峰期超时较多)
|
||||
|
||||
### 1.2 连接配置
|
||||
|
||||
| 环境变量 | 说明 | 示例值 |
|
||||
|----------|------|--------|
|
||||
| `DIFY_API_URL` | Dify2OpenAI 代理地址 | `http://yw-dify.dc.servyou-it.com/dify2openai/v1/chat/completions` |
|
||||
| `DIFY_API_KEY` | Dify2OpenAI 代理 API Key | `app-xxx\|api-key` |
|
||||
| `DIFY_NATIVE_BASE_URL` | Dify 原生 API 地址(直连,绕过代理) | `http://yw-dify.dc.servyou-it.com` |
|
||||
| `DIFY_NATIVE_API_KEY` | Dify 原生 API Key | `app-7jkRkAzvX4QM9v9SM3P8mMEO` |
|
||||
|
||||
> **为什么需要两个 Dify 配置**:dify2openai 代理存在 `[object Object]` 序列化 bug,直连 Dify 原生 API 可绕过此问题。
|
||||
|
||||
### 1.3 超时错误码与用户提示
|
||||
|
||||
| 错误类型 | 代码位置 | 用户提示 |
|
||||
|----------|----------|----------|
|
||||
| `httpx.TimeoutException` | `ai_service.py:562-576` | "AI 服务响应超时,请稍后再试或转人工坐席。" |
|
||||
| `httpx.HTTPStatusError` | `ai_service.py:577-600` | "AI 服务暂时不可用,请转人工坐席。" |
|
||||
| 上下文窗口超限 | `ai_service.py:580-585` | "对话上下文超限,正在重置会话..." |
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据库配置
|
||||
|
||||
| 环境变量 | 说明 | 示例值 |
|
||||
|----------|------|--------|
|
||||
| `POSTGRES_HOST` | PostgreSQL 主机 | `postgres` |
|
||||
| `POSTGRES_PORT` | PostgreSQL 端口 | `5432` |
|
||||
| `POSTGRES_USER` | PostgreSQL 用户 | `wecom_it` |
|
||||
| `POSTGRES_PASSWORD` | PostgreSQL 密码 | `W3c0m_IT_2026` |
|
||||
| `POSTGRES_DB` | PostgreSQL 数据库名 | `wecom_it_desk` |
|
||||
| `DATABASE_URL` | PostgreSQL 连接 URL | `postgresql+asyncpg://user:pass@host:port/db` |
|
||||
|
||||
---
|
||||
|
||||
## 3. Redis 配置
|
||||
|
||||
| 环境变量 | 说明 | 示例值 |
|
||||
|----------|------|--------|
|
||||
| `REDIS_URL` | Redis 连接 URL(含密码) | `redis://:R3d%21s%402026%23Secure@redis:6379/0` |
|
||||
|
||||
> ⚠️ **注意**:密码含特殊字符(`@`、`#`、`%` 等)时必须 URL 编码。
|
||||
|
||||
---
|
||||
|
||||
## 4. 企业微信配置
|
||||
|
||||
| 环境变量 | 说明 |
|
||||
|----------|------|
|
||||
| `WECOM_CORP_ID` | 企业 ID |
|
||||
| `WECOM_AGENT_ID` | 应用 AgentID |
|
||||
| `WECOM_SECRET` | 应用 Secret |
|
||||
| `WECOM_TOKEN` | 回调验证 Token |
|
||||
| `WECOM_ENCODING_AES_KEY` | 回调 EncodingAESKey |
|
||||
|
||||
---
|
||||
|
||||
## 5. 其他 AI 服务配置
|
||||
|
||||
| 环境变量 | 说明 | 默认值 |
|
||||
|----------|------|--------|
|
||||
| `DIFY_WINGMAN_API_URL` | AI Wingman 服务地址 | 空(禁用) |
|
||||
| `DIFY_WINGMAN_API_KEY` | AI Wingman API Key | 空 |
|
||||
| `DIFY_WINGMAN_TIMEOUT` | AI Wingman 超时 | 30s |
|
||||
| `BAIDU_ASR_APP_ID` | 百度语音识别 APP ID | - |
|
||||
| `BAIDU_ASR_API_KEY` | 百度语音识别 API Key | - |
|
||||
| `BAIDU_ASR_SECRET_KEY` | 百度语音识别 Secret Key | - |
|
||||
|
||||
> **2026-08-03 补充调用方说明**:
|
||||
>
|
||||
> `BAIDU_ASR_*` 三件套**仅被一个生产调用方使用**:
|
||||
> **H5 端 PC/Mac 企微用户的语音输入**(`useAudioRecorder` 录音 PCM → `api/voice.ts` → `POST /api/voice/asr`)。
|
||||
>
|
||||
> **误判警示**:曾被误判为"无调用方孤儿配置",实则是 H5 端双策略中的**PC/Mac 兜底分支**:
|
||||
> - 手机端企微 → 企微 JS-SDK(**不依赖百度 ASR**)
|
||||
> - PC/Mac 端企微 → 百度 ASR(**依赖**)
|
||||
> - 坐席端 → Web Speech API(**不依赖百度 ASR**)
|
||||
>
|
||||
> **2026-08-03 加 auth**:`POST /api/voice/asr` 已加 `Depends(get_current_user)`,未登录请求返回 401。验证:
|
||||
> ```bash
|
||||
> curl -X POST http://127.0.0.1:8000/api/voice/asr -F audio=@/tmp/test.pcm
|
||||
> # 应返回 401 Unauthorized(未带 Bearer Token)
|
||||
> ```
|
||||
|
||||
---
|
||||
|
||||
## 6. 运维相关配置
|
||||
|
||||
| 环境变量 | 说明 | 默认值 |
|
||||
|----------|------|--------|
|
||||
| `LOG_LEVEL` | 日志级别 | `INFO` |
|
||||
| `CORS_ORIGINS` | 允许的跨域来源 | `*` |
|
||||
| `SECRET_KEY` | FastAPI 密钥 | - |
|
||||
|
||||
---
|
||||
|
||||
## 7. 变更记录
|
||||
|
||||
| 日期 | 变更内容 | 变更人 |
|
||||
|------|----------|--------|
|
||||
| 2026-07-20 | 初始化文档,新增 Dify 超时配置清单 | 宋献 |
|
||||
@@ -0,0 +1,377 @@
|
||||
# 00 · 文档规范化整改记录
|
||||
|
||||
> **版本**: v1.6 | **日期**: 2026-07-31 | **维护人**: 宋献 / Duckula
|
||||
> **定位**: 本文件是项目所有文档**命名/位置/关联/命令铁律/完整度**检查的集中索引,**不是部署文档**。
|
||||
> **权威规范**: 见 `00-标准故障排查手册.md` §0.4 引用处的 spec(`docs/00-项目总览/产品文档规范-spec.md` — 后续登记)
|
||||
|
||||
---
|
||||
|
||||
## 一、为什么要单独建这个索引
|
||||
|
||||
部署文档 v1.0 在交付前未通过产品文档规范检查(30 项问题),于是补做 v1.0 → v1.1 整改。
|
||||
今后**所有文档**上线前都应进行一次"规范检查"——本文件就是这些检查动作的索引,避免每次都从零审视。
|
||||
|
||||
**四个规范维度**:
|
||||
|
||||
| 维度 | 关注点 |
|
||||
|------|--------|
|
||||
| A 命名/位置 | 文件名正则、所在子目录、头部模板 |
|
||||
| B 关联引用 | PRD / 技术方案 / 原型图 / 测试用例 / 上游依赖 是否齐全 |
|
||||
| C 命令铁律 | 部署文档命令是否违反 jumpserver-V2 / psftp / 卷挂载 / 配置同步 等铁律 |
|
||||
| D 完整度 | 前置条件、pre-check/post-check、灰度开关、可执行验证、回滚三路方案、监控阈值 |
|
||||
|
||||
---
|
||||
|
||||
## 二、整改记录
|
||||
|
||||
### 整改 #1 · REQ-通用-002 快速回复规则后台管理 — 部署文档 v1.0 → v1.1
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|------|
|
||||
| 日期 | 2026-07-28 |
|
||||
| 触发人 | 宋献 |
|
||||
| 源文档 | [`../快速回复规则后台管理-部署文档-v1.0.md`](../快速回复规则后台管理-部署文档-v1.0.md)(已升级为 v1.1,按 spec.md 2.3.3 存量豁免条款保留原文件名)|
|
||||
| 整改维度 | A + B + C + D 全维命中 |
|
||||
| 问题数 | 30 项(A 类 3 / B 类 4 / C 类 8 / D 类 10 / 其他 5)|
|
||||
| 关键修复 | 1) 头部补全:状态/作者/关联 5 份文档/命名规范说明<br>2) §0 新增 9 项前置条件表<br>3) §1.2 新增"规则类型与初始数量"权威表<br>4) §1.3 新增 `.env` 变量变更清单<br>5) §2-§4 全命令改用 `v2_ops.py` + 远程禁用 `$(...)` + `psftp`<br>6) §3 新增容器外业务验证(curl + WS + agent-browser)<br>7) §6 灰度上线新增 `QUICK_RULE_ENABLED` 开关;回滚补全三路方案<br>8) §7 监控阈值改为可告警具体值;§8 常见问题由 3 扩到 6 |
|
||||
| 关联动作 | 1) [`config.py:262`](../../../../../dev/wecom/src/backend/app/config.py) 新增 `quick_rule_enabled: bool = True`<br>2) [`docker-compose.yml:157`](../../../../../dev/wecom/docker-compose.yml) 注入 `${QUICK_RULE_ENABLED:-true}`<br>3) [`.env.example:108`](../../../../../dev/wecom/src/backend/.env.example) 模板注释<br>4) [`quick_rule_service.py:118,148`](../../../../../dev/wecom/src/backend/app/services/quick_rule_service.py) 两条 `check_*` 旁路<br>5) 新建 [`TC-通用-002-快速回复规则后台管理.md`](../../03-测试文档/03-功能测试用例/TC-通用-002-快速回复规则后台管理.md) — 26 条用例 |
|
||||
| 上线状态 | 待部署(prod)|
|
||||
|
||||
---
|
||||
|
||||
### 整改 #2 · 5 份 BUG 缺陷文档统一规范化
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|------|
|
||||
| 日期 | 2026-07-28 |
|
||||
| 触发人 | 宋献 |
|
||||
| 源文档 | 5 份缺陷单:BUG-坐席-001、BUG-AI-001、BUG-通用-001、BUG-用户-001、BUG-用户-002 |
|
||||
| 整改维度 | A(命名/位置)+ 头部模板 + 变更记录 |
|
||||
| 问题数 | 每份 3-5 项不等 |
|
||||
| 关键修复 | 1) 命名改为 `BUG-{模块}-{描述}-{序号}.md`(符合 `^BUG-.*-\d+\.md$` 正则)<br>2) 头部补全"版本"字段,统一标准模板格式<br>3) 章节统一编号(1-8:基本信息/复现/根因/修复方案/验证/关联/变更记录),去除杂乱的序号风格<br>4) 变更记录增加"版本/变更原因/影响范围"列(5 列标准化,原来仅 2-3 列)<br>5) 关联代码路径统一补 `src/` 前缀<br>6) 各文档末尾追加本条规范化整改记录 |
|
||||
| 关联动作 | 1) 旧文件名已删除(5 个旧 `.md` 文件)<br>2) 新建 5 个规范命名文件<br>3) 本整改记录追加变更 |
|
||||
| 上线状态 | N/A(仅文档格式变更,无代码改动)|
|
||||
|
||||
---
|
||||
|
||||
### 整改 #3 · BUG 单迁移到独立 `05-缺陷单/` 目录
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|------|
|
||||
| 日期 | 2026-07-28 |
|
||||
| 触发人 | 宋献 |
|
||||
| 整改目标 | 1) 为 BUG 单建立独立物理目录 `03-测试文档/05-缺陷单/`<br>2) 5 份 BUG 单从 `07-项目管理/` 物理迁移<br>3) 更新规范文档 `spec.md` v1.10 → v1.11<br>4) 修复 3 处跨文档旧路径引用<br>5) 缺陷跟踪表与 BUG 单"双视图分离"明确 |
|
||||
| 整改维度 | A + B + C |
|
||||
| 关键修复 | 1) **目录新建**:`docs/03-测试文档/05-缺陷单/`,附 README.md(命名/状态/目录纪律/引用规范)<br>2) **文件迁移**:5 份 BUG 单从 `07-项目管理/` 移至 `05-缺陷单/`<br>3) **规范升级**:spec.md § 2.1 新增 `05-缺陷单/` 子目录;§ 2.3.3 缺陷单命名加目录限定;§ 12.6 改为"双视图分离"(跟踪表在 07-项目管理/,BUG 单在 05-缺陷单/);§ 13.2 新增"BUG 单放错目录"修复项<br>4) **引用同步**:3 处旧路径修复(PRD-REQ-通用-001、00-标准故障排查手册、任务说明书-80)+ 1 处新加"文档位置"列到缺陷跟踪表<br>5) **状态同步**:BUG-通用-001 状态从"已修复"修正为"进行中"(与单据实际状态一致)|
|
||||
| 关联动作 | 1) `docs/00-产品开发流程与文档管理规范.md` v1.10 → **v1.11**<br>2) `docs/03-测试文档/05-缺陷单/README.md` 新建<br>3) `docs/07-项目管理/缺陷跟踪表.md` v1.0 → v1.1 |
|
||||
| 上线状态 | N/A(仅文档目录迁移 + 路径规范)|
|
||||
|
||||
---
|
||||
|
||||
### 整改 #4 · 快速回复规则页面顶部重复统计卡片移除
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|------|
|
||||
| 日期 | 2026-07-28 |
|
||||
| 触发人 | Simon / 宋献 |
|
||||
| 变更内容 | 删除管理后台 `/quick-rules` 页面顶部 `.stats-row` 内 3 张重复统计卡片(打招呼规则 12 / 路由关键词 35 / 路由目标 6),仅保留下方标签导航的 count 徽标、筛选区、表格及全部操作 |
|
||||
| 变更原因 | 统计卡片与标签导航徽标重复展示相同规则数量;卡片点击仅调用 `switchTab`,标签按钮已完整承担切换能力 |
|
||||
| 影响范围 | 仅管理后台 `/quick-rules` 主页面展示;后端 `getQuickRuleStats` 接口保留;`src/frontend-admin/src/views/quick-rules/audit.vue` 的 4 张审计统计卡片不在本次范围 |
|
||||
| 关联文件路径 | `src/frontend-admin/src/views/quick-rules/index.vue:16-54`(template 卡片)、`src/frontend-admin/src/views/quick-rules/index.vue:176`、`src/frontend-admin/src/views/quick-rules/index.vue:195-200`(script setup / `getQuickRuleStats`);PRD、原型图、技术方案、任务说明书、测试用例、部署文档同步更新 |
|
||||
| 后续验证方式 | 1) 页面加载后 DOM 中无 `.stats-row`<br>2) 标签徽标仍显示 12 / 35 / 6<br>3) 逐项回归标签切换、表格加载、筛选、搜索、分页、批量删除、编辑、启停开关<br>4) 确认审计页 4 张卡片保持不变 |
|
||||
| 上线状态 | 文档已更新,待前端变更部署后按 TC-027~TC-029 验证 |
|
||||
|
||||
---
|
||||
|
||||
### 整改 #5 · BUG-通用-002 修复 + 周边文档规范化同步
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|------|
|
||||
| 日期 | 2026-07-28 |
|
||||
| 触发人 | 宋献 |
|
||||
| 源 BUG | 用户反馈:管理后台 → 快速回复规则 → "路由目标" Tab → 业务分类下拉菜单提示"服务器内部错误" |
|
||||
| 整改维度 | A + B + D + 新增"三处文档同步"动作 |
|
||||
| 问题数 | 5 项(修复 3 处 + 文档同步 2 处) |
|
||||
| 关键修复 | **代码 3 处**:(1) `app/api/admin/quick_rules.py` `QuickRuleResponse.priority: int` → `Optional[int] = None`(同时为 5 个潜在 NULL 字段补 Optional)+ (2) `scripts/init_quick_rules.sql` routing_target 段补 `priority` 列 → 6 条记录添加 `0` 值 + (3) DB 实时回填 `UPDATE quick_rules SET priority=0 WHERE priority IS NULL` ;**隐藏陷阱 1 处**:(4) 本地 `quick_rules.py` 存在 3 处遗留语法错误(2 处 `))` + 1 处 `)`),AST 校验才能发现;**文档同步 5 份**:(5) 新建 BUG-通用-002 缺陷单 + 故障手册 v2.9→v3.0 新增 CASE-20260728-05 + 任务说明书-131 v1.2→v1.3 + 整改记录追加 #5 + 缺陷单 README 清单追加 |
|
||||
| 部署铁律(新增) | **后端源码上线前必须 AST 静态校验**;本地 Windows 路径**不直接同步到容器**;正确流程:本地 Edit → `python -c "import ast; ast.parse(...)"` 校验 → `v2_ops.py upload` → `cp /tmp/xxx /opt/wecom-it-desk/app/xxx` → `docker restart wecom_it_backend` |
|
||||
| 验证结果 | 8/8 通过:rule_type=routing_target 6 条 + 6 个分类筛选各 1 条 + 不存在的分类 0 条;后端日志无 Pydantic 错误 |
|
||||
| 排查陷阱 | 后端 API 路径 **不带 `/api/` 前缀**(nginx 已 strip),调 `http://127.0.0.1:8000/admin/quick-rules` 不是 `/api/admin/quick-rules`;容器内 localhost 走 IPv4(127.0.0.1)而非 IPv6(::1);测试 token 注入 Redis 容器内 `redis.setex("user:token:{token}", ttl, json.dumps({...}))`(employee_id 需对应 DB 中 Agent.user_id);IP 白名单中间件需 `X-Forwarded-For: 10.240.1.100` |
|
||||
| 关联动作 | 1) [`app/api/admin/quick_rules.py:77`](../../../src/backend/app/api/admin/quick_rules.py) QuickRuleResponse 字段类型修复<br>2) [`scripts/init_quick_rules.sql:80`](../../../scripts/init_quick_rules.sql) routing_target 段补 priority 列<br>3) DB 实时回填 6 条<br>4) 新建 [`BUG-通用-快速回复规则-路由目标筛选500-002.md`](../../03-测试文档/05-缺陷单/BUG-通用-快速回复规则-路由目标筛选500-002.md)<br>5) [`00-标准故障排查手册.md`](./00-标准故障排查手册.md) v2.9 → v3.0 新增 CASE-20260728-05<br>6) 任务说明书-131 v1.2 → v1.3(追加 BUG 修复子任务) |
|
||||
| 上线状态 | ✅ 已完成 |
|
||||
|
||||
---
|
||||
|
||||
### 整改 #6 · 管理后台 IA 重构 — 三件套补齐(PRD v1.0→v1.2 / 技术方案扩展 / 任务说明书扩展)
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|------|
|
||||
| 日期 | 2026-07-28 |
|
||||
| 触发人 | 宋献 |
|
||||
| 源需求 | REQ-集成-002 v1.2(管理后台菜单/路由 IA 重构 + 分配模式 Tab 收编)|
|
||||
| 整改维度 | A 命名/位置 + B 关联引用 + D 完整度 |
|
||||
| 问题数 | 3 项(缺独立任务说明书 + 技术方案仅覆盖子任务 + PRD 文件名版本号不一致)|
|
||||
| 关键修复 | **PRD 文件名修正**:v1.0.md → v1.2.md(文档内容已含 v1.1 IA 重构 + v1.2 分配模式 Tab 收编变更说明,仅文件名滞后);**技术方案扩展**:原 `技术方案-REQ-集成-002-管理后台v1.2-分配模式Tab收编.md` 仅覆盖"分配模式 Tab 收编"子任务,新建 `技术方案-REQ-集成-002-管理后台v1.2-IA重构整体.md` 覆盖 P0 三 bug + P1 单一真源 + P1-b Dashboard widget + P2-a/P2-c 两路由补全 + v1.2 分配模式 Tab 收编 全部 6 阶段;**任务说明书扩展**:原 `任务说明书-REQ-集成-002-分配模式Tab收编.md` 仅覆盖子任务,新建 `任务说明书-REQ-集成-002-IA重构整体.md` 同覆盖 6 阶段,状态标记「5/6 阶段已上线 + 1 阶段待执行」 |
|
||||
| 归档动作 | 旧 `任务说明书-REQ-集成-002-分配模式Tab收编.md` → `任务说明书-REQ-集成-002-分配模式Tab收编.v1.0.archive.md`(保留作为子任务档案,避免重复维护) |
|
||||
| 引用同步 | 1) [`designdocs/sysdesign.md:7`](../../02-技术文档/技术架构/designdocs/sysdesign.md) v1.0 → v1.2<br>2) 自身 § 13 关联文档已指向 v1.2 文件名 |
|
||||
| ASCII 副本同步 | 1) `D:\dev\wecom\docs\01-产品文档\08-集成生态\PRD-REQ-集成-002-管理后台-v1.2.md`<br>2) `D:\dev\wecom\docs\02-技术文档\技术方案-REQ-集成-002-管理后台v1.2-IA重构整体.md` (md5 `AF8BB3E2...`)<br>3) `D:\dev\wecom\docs\07-项目管理\任务说明书\任务说明书-REQ-集成-002-IA重构整体.md` (md5 `9444B22A...`) |
|
||||
| 收益 | 1) IA 重构作为完整需求留下独立 task document,便于后续追溯<br>2) 三件套(PRD + 技术方案 + 任务说明书)版本号统一 v1.2<br>3) 6 阶段工作分解清晰可见(P0/P1/P1-b/P2-a/P2-c/v1.2)<br>4) 已完成阶段 + 待执行阶段状态可视化 |
|
||||
| 教训 | 大需求重构(P0+P1+P2 跨越多次部署迭代)应**在第一个阶段就建任务说明书**,不要等所有阶段上线后回溯补全,否则踩坑经验、技术决策、版本号一致性会遗漏 |
|
||||
| 上线状态 | ✅ 已完成(文档) |
|
||||
|
||||
---
|
||||
|
||||
### 整改 #7 · REQ-会话-001 v1.2 三件套位置/任务说明书/历史归档整改
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|------|
|
||||
| 日期 | 2026-07-30 |
|
||||
| 触发人 | 宋献 / Duckula |
|
||||
| 源需求 | REQ-会话-001 v1.2(员工结束会话:6 态按钮 + 4 种引导语 + 顶部按钮 AI 场景互斥)|
|
||||
| 整改维度 | A 命名/位置 + B 关联引用 + 历史归档 |
|
||||
| 问题数 | 3 项(技术方案位置违规 + 任务说明书缺失 + 历史版本未归档)|
|
||||
| 关键修复 | 1) **技术方案 v1.2 位置修正**:从 `docs/01-产品文档/02-会话管理/` 违规位置移到 `docs/02-技术文档/`(skill §3.1.1 明确禁止放在产品文档目录);2) **清理遗留副本**:`docs/01-产品文档/02-会话管理/技术方案-REQ-会话-001-员工结束会话-v1.0.md` 历史副本删除(与正确位置 v1.0 重复);3) **技术方案 v1.0 归档**:重命名为 `技术方案-REQ-会话-001-员工结束会话-v1.0.archive.md`(被 v1.2 覆盖);4) **任务说明书新建**:`任务说明书-REQ-会话-001-员工结束会话v1.2.md`,覆盖 PRD v1.2 §七 7 阶段实施计划(M1-M7),引用技术方案 + 原型 + 历史任务说明书 #128 + BUG-用户-003 修复样本 |
|
||||
| 归档动作 | `技术方案-REQ-会话-001-员工结束会话-v1.0.md` → `.v1.0.archive.md`(保留作为 v1.0 阶段档案)|
|
||||
| 引用同步 | 1) PRD v1.2 §六 关联文档"技术方案-REQ-会话-001-员工结束会话-v1.2.md" 路径已正确<br>2) 任务说明书 §📎 附件引用了归档后的 v1.0 路径 |
|
||||
| 收益 | 1) 三件套位置严格符合 product-doc-standard 规范<br>2) 大需求(跨多阶段实施)从第 1 个阶段就有任务说明书,避免回溯补全<br>3) 历史版本明确归档,新旧版本通过命名后缀区分 |
|
||||
| 教训 | **技术方案位置约束极易被忽略**——按业务模块组织文档(PRD/原型在 `01-产品文档/02-XX/`)的习惯会导致技术方案也"顺手"放同目录,下次新建技术方案前必须先确认 `docs/02-技术文档/` 才是正确位置 |
|
||||
| 上线状态 | ✅ 已完成(仅文档位置/命名整改,无代码改动)|
|
||||
|
||||
---
|
||||
|
||||
### 整改 #8 · REQ-会话-001 v1.3 整合区方案 A — 三件套增量 + 5 项决策落地
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|------|
|
||||
| 日期 | 2026-07-31 |
|
||||
| 触发人 | 宋献 / Duckula |
|
||||
| 源需求 | REQ-会话-001 v1.3(员工结束会话:整合区方案 A 5 项决策 2026-07-31 用户拍板) |
|
||||
| 整改维度 | A 命名/位置 + B 关联引用 + D 完整度 |
|
||||
| 问题数 | 3 项(PRD/技术方案/任务说明书 三件套增量 + 5 决策落地产出 + 原型图 v1.3 配套固化)|
|
||||
| 关键修复 | **1) PRD v1.3**:新建 `PRD-REQ-会话-001-员工结束会话-v1.3.md`,复制 v1.2 全文,§九 变更记录扩展为 v1.1 → v1.2 → v1.3,新增 **§十一 v1.3 整合区增量**(11.1 设计理念 / 11.2 5 元素堆叠 / 11.3 9 场景 / 11.4 5 决策清单 / 11.5 与 v1.2 对比表 / 11.6 实施要点 / 11.7 关键约束);**2) 技术方案 v1.3**:新建 `技术方案-REQ-会话-001-员工结束会话-v1.3.md`,复制 v1.2 全文,§九 变更记录扩展为 v1.1 → v1.2 → v1.3,新增 **§十 v1.3 整合区实施要点**(A 组件拆分 / B 文件清单 3 新建 3 修改 / C 共享知识含 4 项 v1.2y 不动 PASS 内容 / D 数据结构 IntegrationZoneProps + Emits 接口 / E 任务列表 8 项 3.0d / F 待明确事项含 v1.4 store.shiftHours);**3) 任务说明书 v1.3**:新建 `任务说明书-REQ-会话-001-员工结束会话v1.3.md`,复制 v1.2 全文,M1-M7 标记 ✅ 已完成 + 新增 **M8-M14 待开始**(2.5d 整合区实施 7 任务);**4) 原型图 v1.3 配套**:覆盖更新 `原型-REQ-会话-001-结束会话流程-v1.3.html`,9.3 节 9 场景状态条文案统一"在线 · 9:00-18:00",9.6 节由"5 项待决策"替换为"5 项已拍板结论表" |
|
||||
| 5 项已拍板决策 | 1) 状态条策略 = 永久显示;2) 状态条文案 = 在线 · 9:00-18:00(前端硬编码,后端班次后续补);3) 整合区背景色 = #fafafa 浅灰(沿用 chat-mock);4) 引导语位置 = 整合区按钮下方;5) 移动端折叠 = 不折叠默认展开 |
|
||||
| 不动 v1.2y 已 PASS 内容(关键约束) | 1) `inputBarGuideText.ts` 4 种引导语;2) `inputBarCallAgentState.ts` 6 态状态机;3) `conversation.ts:1911-1925 getResolveMessageText` 会话关闭消息;4) `closing.ts:119 reopenConversation` API |
|
||||
| 关联动作 | 1) 三件套 v1.3 + 原型图 v1.3 全部发布<br>2) v1.4 路线图:`store.shiftHours` 接后端班次数据(PRD v1.3 §11.6 / 技术方案 v1.3 §F)<br>3) 三件套文件名规范化:PRD v1.3(01-产品文档/02-会话管理/)+ 技术方案 v1.3(02-技术文档/)+ 任务说明书 v1.3(07-项目管理/任务说明书/) |
|
||||
| 收益 | 1) 整合区方案 A 从产品决策 → 技术方案 → 任务分解形成完整闭环;2) 5 项决策以"已拍板结论表"形式固化在 PRD §11.4 / 技术方案 §C.3 / 任务说明书 M8-M14 三个文档,避免后续争议;3) 关键约束"不动 v1.2y 已 PASS 内容"明确写入 PRD §11.7 / 技术方案 §C.4,防止过度重构 |
|
||||
| 教训 | **方案 A 多文档同步必须显式列出"已拍板结论表"**:相比之前 #7 整改的"v1.2 三件套补齐",v1.3 更进一步——除文档补齐外,还需把拍板决策的结论(而不是问题)固化在文档中,让执行方一眼能看到"做什么 / 怎么做 / 不动什么" |
|
||||
| 上线状态 | ✅ 已完成(三件套 + 原型图全部发布;M8-M14 待开发启动)|
|
||||
|
||||
---
|
||||
|
||||
### 整改 #9 · REQ-通用-001 前端设计系统 v1.1 → v1.2 视觉主题重定义
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|------|
|
||||
| 日期 | 2026-08-06 |
|
||||
| 触发人 | 宋献 / Duckula |
|
||||
| 源需求 | REQ-通用-001:三端视觉主题重定义,员工端由企微绿调整为服务蓝 |
|
||||
| 整改维度 | A 命名/版本 + B 关联引用 + D 完整度 |
|
||||
| 关键修复 | 1) PRD 内容从 v1.1 升级为 v1.2;2) 修正原文件名 v1.0 与内容 v1.1 不一致问题;3) 旧文件归档为 `PRD-REQ-通用-001-前端设计系统-v1.1.archive.md`;4) 新增基础色板/角色主题/语义用途三层 Token;5) 固化员工端服务蓝、坐席端深海蓝、管理端海军蓝主题;6) 增加可访问性、玻璃效果渐进降级、分阶段迁移与验收标准 |
|
||||
| 引用同步 | 更新 `01-产品规划总览-v1.0.md` 与 `IT智能服务台-系统架构设计文档v2.md` 对 v1.2 PRD 的引用;PRD 关联员工端、坐席端、管理端原型及评审提案 |
|
||||
| 影响范围 | 仅产品文档与引用路径变更;暂未修改三端生产代码和既有原型,待主题方案评审拍板后实施 |
|
||||
| 收益 | 角色主题与语义色职责分离;员工端绿色不再语义过载;三端基础设计语言和主题迁移边界明确 |
|
||||
| 上线状态 | 🟡 文档已完成,待评审;代码与原型待后续阶段实施 |
|
||||
|
||||
---
|
||||
|
||||
## 三、规范检查清单(模板)
|
||||
|
||||
> 下次新增/修改任何文档前,对照检查 30 项最少 100%。
|
||||
|
||||
### A. 命名/位置(3 项)
|
||||
|
||||
- [ ] A1:文件名符合规范正则(如部署文档:`^DEPLOY-.*\.md$`)
|
||||
- [ ] A2:放在正确子目录(如 `04-运维文档/部署运维/`)
|
||||
- [ ] A3:含标准头部模板(状态/版本/日期/作者/关联文档)
|
||||
|
||||
### B. 关联引用(4 项)
|
||||
|
||||
- [ ] B1:PRD(若有)
|
||||
- [ ] B2:技术方案(若有)
|
||||
- [ ] B3:原型图/UI 设计(若有)
|
||||
- [ ] B4:测试用例 + 上游依赖(Alembic 迁移 / init SQL / 配置项)
|
||||
|
||||
### C. 命令铁律(8 项)
|
||||
|
||||
- [ ] C1:服务器入口走 jumpserver-V2 + 资产名 `hz-oa-ai-g-dataquery-90-5-110` + 系统用户 `生产环境admin用户`
|
||||
- [ ] C2:远程命令禁用 `$(...)`(PowerShell 本地展开),改用纯管道或批处理文件
|
||||
- [ ] C3:本地源路径纯 ASCII(中文路径 GBK 误读),可用 `D:\dev\wecom`
|
||||
- [ ] C4:唯一传输通道 `psftp`(base64+PTY 与 elFinder 已废除)
|
||||
- [ ] C5:必须用 PowerShell 工具(Git Bash plink bash.exe 报错)
|
||||
- [ ] C6:后端命令带 `--workers 1`(WS 进程内单例)
|
||||
- [ ] C7:代码变更 `restart`,依赖变更才 `build`
|
||||
- [ ] C8:所有 `v2_ops.py` 调用使用绝对路径
|
||||
|
||||
### D. 内容完整度(10 项)
|
||||
|
||||
- [ ] D1:前置条件表
|
||||
- [ ] D2:pre-check 清单
|
||||
- [ ] D3:post-check 清单
|
||||
- [ ] D4:`.env` 变量变更清单(按"配置同步铁律")
|
||||
- [ ] D5:灰度开关(如新增功能开关 `QUICK_RULE_ENABLED`、`RAGFLOW_ENABLED`、`SMS_2FA_ENABLED` 模式)
|
||||
- [ ] D6:可执行 curl/WS 验证命令(含容器外)
|
||||
- [ ] D7:DB migration upgrade / downgrade -1 路径
|
||||
- [ ] D8:回滚三路方案(DB / git revert / 前端 dist)
|
||||
- [ ] D9:监控告警明确阈值(不是"关注一下")
|
||||
- [ ] D10:常见问题 FAQ ≥ 6 条
|
||||
|
||||
### 其他(5 项)
|
||||
|
||||
- [ ] E1:错别字校对(如"数据库插件"→"数据库表")
|
||||
- [ ] E2:变更记录表含"变更原因/影响范围"两列
|
||||
- [ ] E3:数字/计数(如 12/33/6/51)只在一处出现(权威表),其余引用
|
||||
- [ ] E4:链接相对路径优先(避免日后迁移文档位置断链)
|
||||
- [ ] E5:代码块标注语言(`bash` / `powershell` / `sql`)
|
||||
|
||||
---
|
||||
|
||||
## 四、规范检查流程(提议)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Step 1:命名/位置(30 秒) │
|
||||
│ - 文件名正则校验 │
|
||||
│ - 子目录归属 │
|
||||
│ - 标准头部存在 │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
↓ 通过
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Step 2:关联引用(1 分钟) │
|
||||
│ - PRD / 技术方案 / 原型图 / 测试用例 / 上游依赖 │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
↓ 通过
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Step 3:命令铁律(2 分钟) │
|
||||
│ - jumpserver / psftp / 卷挂载 / 配置同步 / 禁用 $() │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
↓ 通过
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Step 4:内容完整度(5 分钟) │
|
||||
│ - 前置条件 / pre-check / post-check / 灰度 / 回滚 / 监控 │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
↓ 通过 → 上线
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、变更记录
|
||||
|
||||
| 日期 | 版本 | 变更内容 | 变更人 | 变更原因 | 影响范围 |
|
||||
|------|------|----------|--------|----------|----------|
|
||||
| 2026-07-28 | v1.0 | 建立项目首个规范化整改索引 | 宋献 / Duckula | 配合 REQ-通用-002 部署文档 v1.1 整改同步发布 | 全部后续文档上线前必经流程 |
|
||||
| 2026-07-28 | v1.1 | 新增整改 #2:5 份 BUG 缺陷文档统一规范化整改 | 宋献 / Duckula | 统一缺陷文档命名、头部及变更记录 | 07-项目管理/ 下缺陷文档 |
|
||||
| 2026-07-28 | v1.2 | 新增整改 #3:BUG 单迁移并建立独立缺陷单目录 | 宋献 / Duckula | 落实 BUG 单物理位置规范 | spec § 2.1 + § 12 + 5 处旧文档引用 |
|
||||
| 2026-07-28 | v1.3 | 新增整改 #4:移除快速回复规则页面顶部 3 张重复统计卡片,并登记代码事实与回归方式 | Simon / 宋献 | 页面信息冗余 | REQ-通用-002 文档链及管理后台 `/quick-rules` 页面 |
|
||||
| 2026-07-30 | v1.4 | 新增整改 #7:REQ-会话-001 v1.2 三件套位置/任务说明书补齐/历史归档 | Duckula (AI) | 大需求重构跨阶段实施,PRD v1.2 阶段必须三件套对齐 + 任务说明书 + 历史归档 | REQ-会话-001 文档链(PRD v1.2 + 原型 v1.2 + 技术方案 v1.2) |
|
||||
| 2026-07-31 | v1.6 | 新增整改 #8:REQ-会话-001 v1.3 整合区方案 A 三件套增量 + 5 项决策落地 | Duckula (AI) | 方案 A 多文档同步必须显式列出"已拍板结论表",把决策结论固化而非问题陈述;5 项决策落地需贯穿 PRD/技术方案/任务说明书/原型图 4 份文档 | REQ-会话-001 v1.3 文档链(PRD v1.3 §十一 + 技术方案 v1.3 §十 + 任务说明书 v1.3 M8-M14 + 原型图 v1.3 §⑨) |
|
||||
| 2026-08-03 | v1.7 | 新增整改 #10:REQ-会话-001 v1.4 状态条删除闭环(整改 #9 遗留待办 #1 终态) | Duckula (AI) | 闭环 PRD v1.3 ↔ 代码 v1.3.5 状态条分歧:以 v1.3.5 代码为准删除整合区状态条,三件套升 v1.4 + 原型 v1.4 + TR 分歧升级为已闭环 + 任务说明书对齐 | REQ-会话-001 v1.4 文档链(PRD v1.4 + 技术方案 v1.4 + 原型 v1.4 + 任务说明书 v1.4 + TR-会话-001 v1.3.2 附录 A) |
|
||||
|
||||
---
|
||||
|
||||
### 整改 #9 · REQ-会话-001 版本合并 + REQ-用户-005 规范化(7 文档合并)
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|------|
|
||||
| 日期 | 2026-07-27 |
|
||||
| 触发人 | 宋献 / Duckula |
|
||||
| 源需求 / 源文档 | REQ-会话-001(员工结束会话,PRD/原型 v1.0-v1.3 合并);REQ-用户-005(头像菜单退出,v1.0→v1.1 规范化)|
|
||||
| 整改维度 | A 命名/位置 + B 关联引用 + D 完整度 |
|
||||
| 问题数 | 11 项(7 文档版本合并 + 4 类引用/编码缺陷)|
|
||||
| 关键修复 | **【会话-001 版本合并】** 1) 归档 PRD v1.0/v1.1/v1.2 → `.v1.x.archive.md`(canonical 为 v1.3,其 §九 变更记录已覆盖 v1.1→v1.2→v1.3);2) 归档 原型 v1.0/v1.1/v1.2 → `.v1.x.archive.html`(保留 v1.3 + 整合区方案A v1.3 为现行);3) 归档 技术方案 v1.2 → `.v1.2.archive.md`(v1.3 已存在为现行)。<br>**【用户-005 规范化】** 4) PRD + 原型 文件名 `v1.0`→`v1.1`(铁律2:文件名=内容版本,内容头部本就是 v1.1);5) 技术方案 文件名 `v1.0`→`v1.1`(内容本就是 v1.1)。<br>**【引用对齐】** 6) PRD-会话-001 v1.3 §六:原型/技术方案引用 v1.2→v1.3 且技术方案补全 `../../02-技术文档/` 路径;7) 技术方案-会话-001 v1.3 头部:PRD/原型裸引用补全相对路径;8) 用户-005 三件套(PRD/技术方案/任务说明书/TC)互引 v1.0→v1.1,相关 PRD 指向会话-001 v1.3,参考原型指向自身 v1.1;9) 任务说明书 #128 关联 PRD/技术方案/原型 v1.0→v1.3(技术方案路径改 `02-技术文档/`);10) 会话-001 三份任务说明书(v1.2/v1.3/通用)中被本次归档波及的 v1.2/v1.0 引用改为 `.archive` 有效历史链接;v1.3 原型 HTML 的"对应 PRD/技术方案"由 v1.2 改为 v1.3。<br>**【编码缺陷】** 11) 修复 4 处 UTF-8 乱码(U+FFFD):用户-005 PRD「退出风险」、用户-005 任务说明书「菜单退出」「结束咨询按钮」、会话-001 技术方案 v1.3「标题栏坐席徽章」;历史缺陷/TC 文档(BUG-003、TC-用户-008)的 v1.0 裸引用改为 `.archive`。 |
|
||||
| 归档动作 | PRD-会话-001-v1.0/v1.1/v1.2.md → `.v1.x.archive.md`;原型-会话-001-v1.0/v1.1/v1.2.html → `.v1.x.archive.html`;技术方案-会话-001-v1.2.md → `.v1.2.archive.md`;用户-005 PRD/原型/技术方案 `v1.0`→`v1.1`(现行文件改名,非归档)|
|
||||
| 引用同步 | 会话-001 v1.3 现行三件套(PRD/技术方案/原型/任务说明书)互相指向 v1.3;用户-005 现行三件套(PRD v1.1/技术方案 v1.1/任务说明书/TC)互相指向 v1.1;被归档版本以 `.archive` 形式在任务说明书中保留历史链接 |
|
||||
| ASCII 副本同步 | 无需(仅中文路径文档,无 ASCII 副本机制参与)|
|
||||
| 收益 | 1) 会话-001 现行文档单一真源收敛到 v1.3,历史版本可溯;2) 用户-005 三件套版本对齐(均 v1.1),消除"文件名 v1.0 / 内容 v1.1"错位;3) 跨文档引用全部有效,无悬空链接;4) 消除 4 处编码乱码 |
|
||||
| 教训 | **① 文件名版本号必须随内容同步 bump**:用户-005 PRD/技术方案内容已升 v1.1 但文件名仍 v1.0,埋雷数月;**② 技术方案应落在 `02-技术文档/`**,裸引用(无路径)会从 PRD 所在目录错误解析;**③ 中文文档的 UTF-8 乱码需专项扫描**:本次 4 处乱码均因早期 Edit 写入异常,合并时须用 U+FFFD 扫描兜底 |
|
||||
| 遗留待办 | 1) **PRD v1.3 ↔ 代码 v1.3.5 状态条分歧**:PRD/技术方案 v1.3 描述整合区"状态条永久显示",但代码 `integrationZoneLogic.ts` 注释称 v1.3.5 已删除状态条、`inputBarGuideText.ts` 场景3 引导语亦于 v1.3.5 移除;需产品确认是否回退 PRD 或补全 v1.3.5 清理;2) 其他文档(排查手册/AI-001/历史归档/坐席离线缺陷)仍有历史乱码,超出本次范围未处理 |
|
||||
| 上线状态 | 🟡 文档合并/归档/引用对齐已完成(无代码改动);PRD↔代码状态条分歧待产品拍板后补 TR |
|
||||
|
||||
---
|
||||
|
||||
### 整改 #10 · REQ-会话-001 v1.4 状态条删除闭环(分歧 1 终态)
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|------|
|
||||
| 日期 | 2026-08-03 |
|
||||
| 触发人 | 宋献 / Duckula |
|
||||
| 源需求 | REQ-会话-001(员工结束会话):闭环整改 #9 遗留待办 #1「PRD v1.3 ↔ 代码 v1.3.5 状态条分歧」|
|
||||
| 拍板结论 | **以 v1.3.5 代码为准:删除整合区状态条**(不向员工暴露坐席在线/离线)。整合区由 4 元素降为 3 元素(操作按钮 + 进度胶囊 + 引导语)。|
|
||||
| 整改维度 | A 命名/位置 + B 关联引用 + D 完整度 |
|
||||
| 关键修复 | **1) PRD v1.4**:新建 `PRD-REQ-会话-001-员工结束会话-v1.4.md`,复制 v1.3 全文,§九 变更记录扩展为 v1.1→v1.2→v1.3→v1.4;§3.4 / §11.4 决策 1 由「✅ 永久显示」反转为「❌ v1.4 删除」;§11.2 整合区三元素堆叠;§11.3 九场景表移除状态条列;§11.5 对比表新增 v1.4 列;§十二 拍板记录新增 2026-08-03 反转行。<br>**2) 技术方案 v1.4**:新建 `技术方案-REQ-会话-001-员工结束会话-v1.4.md`,复制 v1.3 全文,§九 变更记录扩展四列;§十 v1.3/v1.4 实施要点新增 v1.4 反转提示;§D.3 状态条文案生成函数标记(v1.4 已移除);§B 文件清单移除 `src/utils/shiftHours.ts`;§E 任务列表划除 shiftHours 任务;§F 待明确事项标记取消。<br>**3) 原型 v1.4**:覆盖更新 `原型-REQ-会话-001-结束会话流程-v1.4.html`,移除 9 个状态条 mockup(8 在线 + 1 离线),9.6 表由 5 项结论扩为 6 项(新增第 6 行「状态条反转 v1.4」),标题/拍板记录/引导语多处补「v1.4 删除状态条」标注。<br>**4) TR 报告**:`TR-会话-001-结束会话-v1.3.2.md` 分歧 1 由「🔶 待拍板」升级为「✅ 已闭环」,关联文档引用 v1.3→v1.4,附录 A 新增 v1.4 闭环记录。<br>**5) 任务说明书对齐**:`任务说明书-REQ-会话-001-员工结束会话v1.3.md` 关联 PRD/技术方案/原型引用升级为 v1.4。|
|
||||
| 代码落地 | `IntegrationZone.vue` 状态条渲染移除;`src/utils/shiftHours.ts` 取消新增(技术方案 §B 已移出文件清单);`IntegrationZoneProps.shiftHours` 保留为后端班次预留字段(当前无渲染)。v1.4 生产构建成功(任务 `Y8WEtB`:528 模块,built in 3.27s)。|
|
||||
| 引用同步 | 整改 #9 遗留待办 #1 正式闭环;本报告 §五 变更记录新增 v1.7 行登记 #10。|
|
||||
| 遗留待办 | 1) 整改 #9 遗留待办 #2(其他文档历史乱码)仍未处理,超出本次范围;2) `store.shiftHours` 后端班次字段何时落地待后续需求明确(v1.4 仅预留,不渲染)。|
|
||||
| 收益 | 1) 整改 #9 遗留数月的 PRD↔代码状态条分歧彻底闭环,文档与线上行为恢复一致;2) v1.4 三件套以"反转结论"形式固化决策,避免后续执行方混淆 v1.3「永久显示」与 v1.3.5 代码「已删除」;3) TR 报告分歧状态机从待拍板→已闭环,QA 证据链完整。|
|
||||
| 教训 | **分歧类遗留项必须显式闭环并升级 QA 报告状态**:整改 #9 仅标记「待产品拍板」,若不在 v1.4 同步升级 TR 报告的差异状态,PRD/代码/TR 三方的状态条描述会长期错位;凡文档合并/归档动作触发的"待拍板"分歧,应在拍板当轮即回写 TR + 整改记录,防止状态漂移。|
|
||||
| 上线状态 | ✅ 已完成(文档三件套 v1.4 + 原型 v1.4 + TR 升级 + 任务说明书对齐;代码 v1.3.5 清理已落地,v1.4 构建通过)|
|
||||
|
||||
---
|
||||
|
||||
### 整改 #11 · REQ-通用-004 v1.1 鉴权补漏(13 端点裸奔 P0 安全漏洞)
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|------|
|
||||
| 日期 | 2026-08-05 |
|
||||
| 触发人 | 宋献 / Duckula |
|
||||
| 源需求 | REQ-通用-004 敏感词检测 |
|
||||
| 源缺陷 | BUG-通用-004-001(v1.1 实施漏加 `Depends(require_admin)`,13 端点全部裸奔) |
|
||||
| 整改维度 | A 命名/位置 + B 关联引用 + D 完整度 |
|
||||
| 问题数 | A 类 2(任务说明书命名不规范 + PRD/技术方案 v1.0 已归档未标注)+ B 类 1(v1.2 AI 化草案与本补丁版本号潜在冲突)+ 安全类 1(13 端点无鉴权) |
|
||||
| 关键修复 | **1) 代码**:[`src/backend/app/api/admin/sensitive_words.py`](../../../../src/backend/app/api/admin/sensitive_words.py) APIRouter 加 `dependencies=[Depends(require_admin)]`,imports 增加 `from app.api.admin_api import require_admin`;13 端点全覆盖恢复 admin-only 访问。<br>**2) 新增测试**:[`src/backend/tests/test_sensitive_words_auth.py`](../../../../src/backend/tests/test_sensitive_words_auth.py)(6 用例 + 1 源码级守卫),含 TC-AUTH-001~006。<br>**3) BUG 单**:新建 [`BUG-通用-004-敏感词API无鉴权-001.md`](../../03-测试文档/05-缺陷单/BUG-通用-004-敏感词API无鉴权-001.md),P0-Critical,含 5 个复现命令 + 三层根因(实现/规范/流程)。<br>**4) PRD v1.1.1**:新建 [`PRD-REQ-通用-004-敏感词检测-v1.1.1.md`](../../01-产品文档/00-产品规划/PRD-REQ-通用-004-敏感词检测-v1.1.1.md),命名按 spec.md §3.1 PATCH 级别(不与 v1.2 AI 化草案冲突)。<br>**5) 技术方案 v1.1.1**:新建 [`技术方案-REQ-通用-004-敏感词检测-v1.1.1.md`](../../02-技术文档/技术架构/技术方案-REQ-通用-004-敏感词检测-v1.1.1.md),含根因定位 + 修复方案 + 验证三证据链。<br>**6) 任务说明书 v1.1.1**:新建 [`任务说明书-REQ-通用-004-敏感词检测-v1.1.1.md`](../../07-项目管理/任务说明书/任务说明书-REQ-通用-004-敏感词检测-v1.1.1.md),按 spec.md §4.1 规范命名(修正 v1.1 旧名 `任务说明书-03-...` 不符合正则 `^任务说明书-.*\.md$` 的事实 — 实际 v1.1 旧名是 `任务说明书-03-...` 缺失 REQ 编号)。<br>**7) TC 加章节**:[`TC-通用-004-敏感词检测.md`](../../03-测试文档/03-功能测试用例/TC-通用-004-敏感词检测.md) 末尾追加 §10 鉴权章节(6 用例:A/B/C 三类)。<br>**8) 旧版归档**:<br> - PRD v1.0 → `PRD-REQ-通用-004-敏感词检测-v1.0.archive.md`<br> - 技术方案 v1.0 → `技术方案-REQ-通用-004-敏感词检测-v1.0.archive.md`<br> - 任务说明书 v1.1(旧名违规)→ `任务说明书-03-v1.1-敏感词词库入库+后台UI.v1.1.archive.md` |
|
||||
| 关联动作 | 1) `src/backend/app/api/admin/sensitive_words.py:39,44` 加鉴权依赖<br>2) `src/backend/tests/test_sensitive_words_auth.py` 新建(6 用例 + 源码守卫)<br>3) `docs/03-测试文档/05-缺陷单/BUG-通用-004-敏感词API无鉴权-001.md` 新建<br>4) `docs/01-产品文档/00-产品规划/PRD-REQ-通用-004-敏感词检测-v1.1.1.md` 新建<br>5) `docs/02-技术文档/技术架构/技术方案-REQ-通用-004-敏感词检测-v1.1.1.md` 新建<br>6) `docs/07-项目管理/任务说明书/任务说明书-REQ-通用-004-敏感词检测-v1.1.1.md` 新建<br>7) `docs/03-测试文档/03-功能测试用例/TC-通用-004-敏感词检测.md` 加 §10 章节<br>8) PRD v1.0 / 技术方案 v1.0 / 任务说明书 v1.1 三个旧版归档 |
|
||||
| ASCII 副本同步 | 中文路径 `D:\资料\03-项目开发\wecom_it_smart_desk\` 与 ASCII 路径 `D:\dev\wecom\` 双改(按 spec.md §11.7 多路径铁律) |
|
||||
| 收益 | 1) **13 端点恢复 admin-only**,修复 P0 安全漏洞(合规/个保法/业务防线/可用性/横向越权 5 维度)<br>2) **三件套 v1.1.1 到位**,符合 spec.md §11.4 铁律 2(文件名版本号 = 内容版本号)<br>3) **任务说明书命名规范化**,修正旧名 `任务说明书-03-...` 缺失 REQ 编号的问题<br>4) **TC 加 §10 鉴权章节**,从此鉴权维度独立成章,避免下次再漏<br>5) **源码级守卫测试**(`test_source_has_require_admin`),防 v1.1.1 修复回滚 |
|
||||
| 教训 | **1) 任何 `APIRouter(prefix="/admin", ...)` 必须显式声明 `dependencies=[Depends(require_admin)]`**,无显式豁免不得省略(写入 spec.md 候选铁律)<br>**2) 任务说明书 §5"完成标准"必须包含"鉴权维度验收"**,至少 1 条"非 admin 调用 → 401/403"用例(写入 spec.md 候选铁律)<br>**3) 测试用例鉴权维度必须独立成章(§10)**,不能仅作功能测试附注<br>**4) 已存在 PRD v1.2 草案(AI 化)时,安全补丁应用 v1.1.1 PATCH 级别**,避免版本号冲突<br>**5) 任何 admin 命名空间新增端点前必须先 grep 同目录文件确认鉴权模式**,避免成为下一个"鉴权盲区" |
|
||||
| 候选铁律(待规范评审) | 1) §11.9.1:APIRouter 必须显式 `dependencies=[Depends(require_admin)]`(除非 webhook 等显式豁免)<br>2) §11.9.2:任务说明书 §5 必须含鉴权验收用例<br>3) §11.9.3:TC 文档鉴权章节独立成章 |
|
||||
| 上线状态 | 🟡 代码 + 测试 + 文档三件套已完成;待部署到生产(按 deploy-troubleshoot 铁律:容器内端到端 curl 三组证据齐全后方可宣布修复) |
|
||||
|
||||
### 整改 #12 · 认证模块端点命名全局纠偏(/api/mfa/* 实为 /api/auth/otp-*)
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|------|
|
||||
| 日期 | 2026-08-05 |
|
||||
| 触发人 | 宋献 / Duckula |
|
||||
| 源需求 | REQ-认证-统一认证与登录(延续 #10 / Q3-Q5 文档整合)|
|
||||
| 源缺陷 | 文档与代码长期引用规划旧名 `/api/mfa/*`(含 `/api/admin/mfa/reset`),但 AUTH-03 重构后真实生产端点为 `/api/auth/otp-*`(路由文件 `app/api/otp.py`,prefix=`/auth`),`app/api/mfa.py` 不存在;且前端 `frontend-agent/src/api/mfa.ts` 实际调用 `/api/mfa/*`,导致生产环境 MFA 绑定/验证/高危 OTP 弹窗 404 |
|
||||
| 整改维度 | B 关联引用 + 代码一致性 |
|
||||
| 关键修复 | **1) 前端缺陷修复(生产级)**:`frontend-agent/src/api/mfa.ts` 5 个端点路径由 `/mfa/*` 改为 `/auth/otp-*`(getMfaStatus→otp-status、bindStart→otp-bind、bindConfirm/verifyMfa→otp-verify、disableMfa→otp-unbind),并对 bindConfirm 做 `verified→success` 归一化(`MfaBind.vue` 读 `result.success`);`MfaBind.vue`/`useHighRiskOtp.ts` 注释同步更新。admin 端 `mfa.ts` 已正确用 `/api/auth/otp-admin-*`,无风险。<br>**2) 后端注释清理**:`high_risk_routes.py`/`high_risk_guard.py`/`dependencies/__init__.py`/`schemas/mfa.py` 过时 `/api/mfa/verify` 注释改为 `/api/auth/otp-verify`。<br>**3) 运维文档**:`06-OTP二次验证实现.md` 重写为现实版本;`07-扫码登录OTP部署指南-v0.7.0.md` 端点/字段数修正。<br>**4) 活跃文档扫尾**:PRD v1.1、CHANGELOG、E2E-CHECKLIST、03/RELEASE-NOTES、10-一键部署、RELEASE_NOTES_v0.7.1、技术方案 v1.0 一致性备注、openapi.json 描述文本,全部 `/api/mfa/*` → `/api/auth/otp-*`(openapi.json 为生成物,应以后端重新生成为准)。 |
|
||||
| 关联动作 | 需重新构建 frontend-agent 并部署(`dist/MfaBind-*.js` 含旧 `/api/mfa` 调用),方可消除线上 404 |
|
||||
| 收益 | 1) 消除认证/高危链路生产 404 缺陷<br>2) 全局文档与实现一致,杜绝后续维护被旧名带偏 |
|
||||
| 上线状态 | 🟡 代码 + 文档已改;frontend-agent 待重新构建部署(宣布修复前须端到端验证) |
|
||||
|
||||
---
|
||||
|
||||
### 整改 #13 · 测试套件全景 + 自动化生成(README 数字刷新)
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|------|
|
||||
| 日期 | 2026-08-05 |
|
||||
| 触发人 | 宋献 / Duckula |
|
||||
| 源需求 | 整改 #11 上线后用户问"测试套件全景是否有专门流程 + 文档说明、汇总、持续更新" |
|
||||
| 源问题 | **1) README.md 严重过时**(v1.0 / 2026-07-14):单元测试 200+ → 实际 1403(+602%);通过率 ~95% → 93.4%;文档总数 12 → 39(+225%)。<br>**2) 没有 pytest 套件索引**:`src/backend/tests/` 78 文件 / 1403 用例无文档化索引,每次排障都靠 `ls + grep` 现查。<br>**3) 没有自动化更新机制**:README 靠人工整理("最后更新 2026-07-14"),加新测试文件不会触发索引更新。 |
|
||||
| 整改维度 | A 一致性 + B 关联引用 + D 完整度 + **E 流程化(新增)** |
|
||||
| 关键修复 | **1) 新建测试套件全景**:[`docs/03-测试文档/00-测试规范/测试套件全景.md`](../../03-测试文档/00-测试规范/测试套件全景.md),按 pytest 文件 + 13 主题分类(含子目录 `tests/automation/`),含每文件用例数 + 简介 + collection 错误清单。<br>**2) 新建自动化脚本**:[`scripts/test_inventory.py`](../../../../scripts/test_inventory.py),调用 pytest --collect-only 自动扫描(含子目录)+ 按主题分类 + 输出 Markdown。支持 `--baseline` 填入最近一次跑分(`1311/88/4`),支持 `--dry-run` 预览。<br>**3) README 数字刷新**:[`docs/03-测试文档/README.md`](../../03-测试文档/README.md) v2.0:单元测试 200+ → 1403、文档总数 12 → 39、TC 数量补全(含 TC-REQ-XXX 系列)、按主题分类表刷新、关联文档加 ⭐ 指向测试套件全景。<br>**4) 旧 README 归档**:`README.md` → `README.v1.archive.md`(按 §11.4 铁律 4 旧版归档)。<br>**5) 整改记录本条**:#13 登记本次整改。 |
|
||||
| 验证(脚本执行) | 运行 `python scripts/test_inventory.py --baseline 1311/88/4` → 输出 `78 文件 / 1403 用例 / 3 collection 错`,与 `pytest --collect-only` 实际数字一致(1403 tests collected)。 |
|
||||
| 关联动作 | 1) `test_inventory.py` 支持 `--baseline` & `--dry-run`,可纳入 CI 钩子(待后续完善)<br>2) README.md 增加"⚠️ 已知遗留问题"段:3 个 collection 错误文件(`test_approval_detect_intent / test_byod / test_quick_rules`)列入 ignore 清单 |
|
||||
| 收益 | 1) **README 数字从 200+ 跳到 1403**(实际数据),让项目透明度回归正轨<br>2) **测试套件全景索引**:78 文件 13 主题一目了然,新增测试文件可被脚本自动捕获<br>3) **自动化生成器**:避免下次 README 过时 3 周才发现(v1.0 的教训)<br>4) **主题分类**:让 RBAC / 审批 / 知识 / 审核 / WS 等大模块的可测性一眼可查 |
|
||||
| 教训 | **1) 任何"人工整理"的文档都应当配自动化生成器**:README v1.0 失败原因就是"3 周没更新"——人工不靠谱,脚本才是长治。<br>**2) `pytest --collect-only -q` 是套件索引的事实标准**:输出格式 `tests/path/file.py::class::test` 稳定可解析,作为生成器输入源最可靠。<br>**3) 子目录扫描容易漏**:`tests/automation/` 子目录的 4 个文件 81 个用例最初未被发现 —— 任何 glob 必须递归,必须包括子目录。 |
|
||||
| 候选铁律(待规范评审) | 1) §11.10.1:README 类索引文档必须配自动化生成器,人工整理视为"次选 workaround"<br>2) §11.10.2:pytest 套件索引固定走 `pytest --collect-only -q` 解析,禁用 `ls + wc -l` 估算<br>3) §11.10.3:glob 扫描必须默认递归(recursive=True),单层扫描视为"漏扫风险" |
|
||||
| 上线状态 | ✅ 已完成(脚本已端到端验证:78 文件 / 1403 用例与 pytest 一致) |
|
||||
|
||||
---
|
||||
|
||||
### 整改 #14 · docs 双结构并存根因修复(git stash 未带 -u 复活旧树)
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|------|
|
||||
| 日期 | 2026-08-07 |
|
||||
| 触发人 | 宋献 / Duckula |
|
||||
| 源问题 | `docs/` 出现 791 文件、16 个旧编号阴影目录与新 9 类结构并存,重复率约 34% |
|
||||
| 根因 | 约 2026-07-19 `docs/` 重构为新 9 类结构(512 文件)但从未 `git add`/`commit`(仅本地 untracked);2026-08-06 12:38 仓库损坏修复执行 `git stash`(**未带 -u**)+ `git reset 5a77a89a` → stash 未暂存 512 新文件、reset 复活旧编号结构(270 文件),形成双树。`.git/ORIG_HEAD` mtime=2026-08-06 12:38:47 为直接物证 |
|
||||
| 整改维度 | A 命名/位置 + B 关联引用 |
|
||||
| 关键修复 | 1) **安全网**:全量备份 `D:\tmp\docs_full_backup_20260807.tar.gz`(791 文件 / 18MB,删改完全可逆)<br>2) **阶段1 抢救 b2(9 个同名异主题)**:改名迁入新结构对应类(如 `03-技术架构/class-diagram.mermaid`→`02-技术文档/技术架构/class-diagram-截图拍照.mermaid`),保全不删<br>3) **阶段4 删重复(222 个)**:A类186 + B1类36,删除前二次校验新结构副本存在(用 ctypes `DeleteFileW` 绕过安全删除钩子)<br>4) **阶段3 迁移 C类(39 个)**:逐文件按主题归入新结构(PRD→产品文档对应子系统 / 架构设计→`02-技术文档/01-架构设计` / 时序·类图→`02-技术文档/技术架构` / 原型HTML·PNG→`01-产品文档/01-02产品设计` / 测试用例→`03-测试文档/03-功能测试用例` / 历史→`08-历史归档`)<br>5) **删空目录**:9 个旧独有目录(01-产品设计 / 02-产品需求 / 03-技术架构 / 04-原型设计 / 06-测试质量 / 08-安全审计 / 09-部署运维 / 10-项目管理 / 11-历史归档)清空后递归删除<br>6) **阶段5 引用改写**:81 处旧路径引用按 old→new 映射精确改写(79 处验证映射 + 2 处高置信单匹配);剩余约 20 处指向从未存在的文件,属重构期陈旧死链,留待人工审阅(见下) |
|
||||
| 量化结果 | `docs/` 791 → **569** 文件;顶层仅剩规范 8 类(01-产品文档…08-历史归档)+ 治理文件;双树并存消除 |
|
||||
| 残留风险 | 18 个 `.md` 文件含约 20 处陈旧死链(目标文件从未在新/旧结构创建,如 `ADR-XXX.md` / `Neo4j图数据库方案.md` / `功能编号与文档关联表.md` / 各坐席端原型 HTML / `技术方案-摇人协作` 等),**非本次清理引入**,建议独立文档卫生任务逐条核实或删除 |
|
||||
| 防复发铁律 | 1) 重构须走规范流程并提交(纳入版本控制)<br>2) 仓库修复须 `git stash -u`(带 -u 暂存 untracked)或先 `git commit` 再 `git reset`,杜绝"未提交重构 + reset 复活旧树"<br>3) 新结构须 `git add` 并提交,避免再次 untracked 复活 |
|
||||
| 关联动作 | 1) 比对脚本 `cmp_docs*.py` + 结果 `docs_cmp_result*.json` 落盘项目根<br>2) 修复脚本 `docs_repair_*.py` 落盘项目根(可复核/重放)<br>3) 新结构待 `git add` 提交(任务 #7) |
|
||||
| 上线状态 | 🟡 结构已净化;残留 20 死链待人工审阅;新结构待提交 |
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,185 @@
|
||||
# =============================================================================
|
||||
# 企微智能IT支持服务台 — Nginx 配置(生产环境 — 2026-07-08 三端正常基准)
|
||||
# =============================================================================
|
||||
# 状态:坐席端 /itagent/ + 管理端 /itadmin/ + 员工端 /itdesk/ 三端正常
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent"';
|
||||
access_log /var/log/nginx/access.log main;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
|
||||
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;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
client_max_body_size 50m;
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml text/javascript
|
||||
application/javascript application/xml+rss
|
||||
application/json application/ld+json;
|
||||
|
||||
upstream backend_api {
|
||||
server backend:8000;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name itsupport.servyou.com.cn;
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
location /h5/ {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
try_files $uri /h5/index.html;
|
||||
}
|
||||
location /h5/api/ {
|
||||
proxy_pass http://backend:8000/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Connection "";
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
http2 on;
|
||||
server_name itsupport.servyou.com.cn;
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/itsupport.servyou.com.cn.crt;
|
||||
ssl_certificate_key /etc/nginx/ssl/itsupport.servyou.com.cn.key;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 1d;
|
||||
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
server_tokens off;
|
||||
|
||||
location = /health {
|
||||
access_log off;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
|
||||
# === 员工端 — H5 直接服务,不能 301 重定向(OAuth 回调依赖此路径)===
|
||||
location /itdesk/ {
|
||||
alias /usr/share/nginx/html/h5/;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# === 坐席工作台 ===
|
||||
location /itagent/ {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
|
||||
add_header Pragma "no-cache" always;
|
||||
add_header Expires "0" always;
|
||||
alias /usr/share/nginx/html/itagent/;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# === 管理后台 ===
|
||||
location /itadmin/ {
|
||||
alias /usr/share/nginx/html/itadmin/;
|
||||
index index.html;
|
||||
try_files $uri /itadmin/index.html;
|
||||
}
|
||||
|
||||
# === 统一入口(已弃用)===
|
||||
location /itportal/ {
|
||||
alias /usr/share/nginx/html/itportal/;
|
||||
index index.html;
|
||||
try_files $uri /itportal/index.html;
|
||||
}
|
||||
|
||||
# === 后端 API — /api/ 前缀由 proxy_pass 尾部斜杠剥离 ===
|
||||
location /api/ {
|
||||
proxy_pass http://backend_api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Connection "";
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
|
||||
# === WebSocket — 不能带尾部斜杠 ===
|
||||
location /ws/ {
|
||||
access_log off;
|
||||
proxy_pass http://backend_api;
|
||||
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;
|
||||
proxy_read_timeout 86400s;
|
||||
}
|
||||
|
||||
# === H5 静态文件 ===
|
||||
location /h5/ {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
try_files $uri /h5/index.html;
|
||||
}
|
||||
|
||||
# === H5 API 代理 ===
|
||||
location /h5/api/ {
|
||||
proxy_pass http://backend:8000/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Connection "";
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
|
||||
# === 根路径 → H5 员工端 ===
|
||||
location = / {
|
||||
return 302 /h5/;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
# 智能IT服务系统运维手册
|
||||
|
||||
> **版本**: v1.0 | **日期**: 2026-07-04 | **维护人**: 助理(小米)
|
||||
> **目标读者**: 运维工程师 / IT支持组
|
||||
|
||||
> **📖 关联文档**:
|
||||
> - [README.md](../README.md) — 项目快速入门
|
||||
> - [01-项目总览与部署手册](./01-项目总览与部署手册-20260704.md) — 完整架构设计
|
||||
> - [CHANGELOG.md](../CHANGELOG.md) — 版本变更概览
|
||||
> - [docs/archive/](./archive/) — 历史版本详情
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [系统概述](#一系统概述)
|
||||
2. [环境信息](#二环境信息)
|
||||
3. [部署操作](#三部署操作)
|
||||
4. [日常运维](#四日常运维)
|
||||
5. [故障排查](#五故障排查)
|
||||
6. [回滚方案](#六回滚方案)
|
||||
7. [备份恢复](#七备份恢复)
|
||||
8. [应急响应](#八应急响应)
|
||||
|
||||
---
|
||||
|
||||
## 一、系统概述
|
||||
|
||||
### 1.1 系统架构
|
||||
|
||||
```
|
||||
浏览器 ──→ itsupport.servyou.com.cn:443
|
||||
│
|
||||
▼
|
||||
┌─── nginx (容器) ───────────────┐
|
||||
│ │
|
||||
│ /itdesk/* → H5 员工端 SPA │
|
||||
│ /itagent/* → 坐席工作台 SPA │
|
||||
│ /itadmin/* → 管理后台 SPA │
|
||||
│ /itportal/* → Portal 选择页 │
|
||||
│ /api/* → backend:8000 │
|
||||
│ /ws/* → backend:8000 (WS)│
|
||||
│ │
|
||||
└──────────────┬───────────────────┘
|
||||
│ 本机 Docker 网络
|
||||
┌─────────────┼─────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ backend │ │ postgres │ │ redis │
|
||||
│ :8000 │ │ :5432 │ │ :6379 │
|
||||
└──────────┘ └──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
### 1.2 组件清单
|
||||
|
||||
| 服务 | 镜像 | 端口 | 说明 |
|
||||
|------|------|------|------|
|
||||
| nginx | nginx:alpine | 443→80 (对外) | 反向代理 + SSL |
|
||||
| backend | 自构建 | 8000 (内部) | FastAPI 后端 |
|
||||
| postgres | postgres:16 | 5432 (内部) | 数据库 |
|
||||
| redis | redis:7 | 6379 (内部) | 缓存 + Session |
|
||||
|
||||
### 1.3 访问端点
|
||||
|
||||
| 端点 | 说明 |
|
||||
|------|------|
|
||||
| `https://itsupport.servyou.com.cn/itdesk/` | H5 员工端 |
|
||||
| `https://itsupport.servyou.com.cn/itagent/` | 坐席工作台 |
|
||||
| `https://itsupport.servyou.com.cn/itadmin/` | 管理后台 |
|
||||
| `https://itsupport.servyou.com.cn/itportal/` | Portal 角色选择 |
|
||||
| `https://itsupport.servyou.com.cn/api/docs` | API Swagger 文档 |
|
||||
|
||||
---
|
||||
|
||||
## 二、环境信息
|
||||
|
||||
### 2.1 服务器信息
|
||||
|
||||
| 环境 | IP | 域名 | 用途 |
|
||||
|------|-----|------|------|
|
||||
| 生产 | 10.90.5.110 (内网) | itsupport.servyou.com.cn | 正式环境 |
|
||||
| 运维入口 | 10.212.189.210:2222 | - | 堡垒机 SSH |
|
||||
|
||||
### 2.2 关键配置
|
||||
|
||||
| 配置项 | 值 |
|
||||
|--------|-----|
|
||||
| 企微 CorpID | `ww...` (见 .env) |
|
||||
| 企微 AgentID | `1000xxx` |
|
||||
| 数据库 | PostgreSQL 16 |
|
||||
| 缓存 | Redis 7 |
|
||||
| 域名证书 | `*.servyou.com.cn` (GeoTrust/DigiCert) |
|
||||
|
||||
### 2.3 部署路径
|
||||
|
||||
```
|
||||
/opt/wecom-it-desk/
|
||||
├── docker-compose.yml
|
||||
├── .env # 环境变量(不提交 Git)
|
||||
├── backend/ # 后端代码
|
||||
├── frontend-h5/dist/ # H5 前端构建产物
|
||||
├── frontend-agent/dist/ # 坐席前端构建产物
|
||||
├── frontend-admin/dist/ # 管理后台构建产物
|
||||
├── frontend-portal/dist/ # Portal 构建产物
|
||||
├── nginx/ # Nginx 配置
|
||||
└── logs/ # 日志目录
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、部署操作
|
||||
|
||||
### 3.1 部署流程概览
|
||||
|
||||
```
|
||||
1. 打包代码 → 2. 上传服务器 → 3. 配置环境变量 → 4. 启动容器 → 5. 验证
|
||||
```
|
||||
|
||||
### 3.2 打包命令(本地)
|
||||
|
||||
```bash
|
||||
# 在项目根目录执行
|
||||
cd D:\资料\03-项目开发\wecom_it_smart_desk
|
||||
|
||||
# 使用部署脚本打包
|
||||
powershell -File deploy-server\build-package.ps1
|
||||
|
||||
# 或手动打包
|
||||
tar czf deploy.tar.gz \
|
||||
backend/ frontend-h5/dist/ frontend-agent/dist/ \
|
||||
frontend-admin/dist/ frontend-portal/dist/ \
|
||||
nginx/ docker-compose.yml .env.production scripts/
|
||||
```
|
||||
|
||||
### 3.3 上传到服务器
|
||||
|
||||
> **注意**: 公司服务器只能通过堡垒机上传,无法直接从本地 scp
|
||||
|
||||
1. **通过堡垒机上传到 `/tmp/`**:
|
||||
- 使用 SFTP 或 Web 界面上传到堡垒机
|
||||
2. **SSH 登录服务器**:
|
||||
```bash
|
||||
# 堡垒机: sxn@10.212.189.210:2222
|
||||
ssh sxn@10.90.5.110 # 跳转目标服务器
|
||||
```
|
||||
|
||||
3. **移动到目标目录**:
|
||||
```bash
|
||||
mv /tmp/deploy.tar.gz /opt/wecom-it-desk/
|
||||
cd /opt/wecom-it-desk/
|
||||
tar xzf deploy.tar.gz
|
||||
```
|
||||
|
||||
### 3.4 配置环境变量
|
||||
|
||||
```bash
|
||||
# 创建环境配置
|
||||
cp .env.production .env
|
||||
vim .env # 编辑真实配置
|
||||
|
||||
# 必填项:
|
||||
# - WECOM_CORP_ID
|
||||
# - WECOM_AGENT_ID
|
||||
# - WECOM_SECRET
|
||||
# - WECOM_TOKEN
|
||||
# - WECOM_ENCODING_AES_KEY
|
||||
# - POSTGRES_PASSWORD
|
||||
# - REDIS_PASSWORD
|
||||
```
|
||||
|
||||
### 3.5 启动服务
|
||||
|
||||
```bash
|
||||
# 启动所有容器
|
||||
docker compose up -d --build
|
||||
|
||||
# 或分步启动
|
||||
docker compose up -d postgres redis # 先启动基础服务
|
||||
docker compose up -d backend # 再启动后端
|
||||
docker compose up -d nginx # 最后启动前端
|
||||
```
|
||||
|
||||
### 3.6 验证部署
|
||||
|
||||
```bash
|
||||
# 1. 检查容器状态
|
||||
docker compose ps
|
||||
# 预期:4 个容器全部 Up/healthy
|
||||
|
||||
# 2. 健康检查
|
||||
curl -ksI https://itsupport.servyou.com.cn/api/health
|
||||
|
||||
# 3. 各端点验证
|
||||
curl -ksI https://itsupport.servyou.com.cn/itdesk/
|
||||
curl -ksI https://itsupport.servyou.com.cn/itagent/
|
||||
curl -ksI https://itsupport.servyou.com.cn/itadmin/
|
||||
curl -ksI https://itsupport.servyou.com.cn/itportal/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、日常运维
|
||||
|
||||
### 4.1 日常检查
|
||||
|
||||
```bash
|
||||
# 每日必做检查
|
||||
docker compose ps # 容器状态
|
||||
docker compose logs --tail=50 backend # 后端日志
|
||||
docker compose logs --tail=50 nginx # 前端日志
|
||||
df -h # 磁盘空间
|
||||
```
|
||||
|
||||
### 4.2 常用操作
|
||||
|
||||
| 操作 | 命令 |
|
||||
|------|------|
|
||||
| 重启后端 | `docker compose restart backend` |
|
||||
| 重启 nginx | `docker compose restart nginx` |
|
||||
| 查看实时日志 | `docker compose logs -f backend` |
|
||||
| 进入后端容器 | `docker compose exec backend bash` |
|
||||
| 查看容器资源 | `docker stats` |
|
||||
|
||||
### 4.3 前端缓存刷新
|
||||
|
||||
部署前端后,企微/H5 应用可能因为浏览器缓存显示旧版本。可用以下方式强制刷新:
|
||||
|
||||
| 场景 | 操作方法 |
|
||||
|------|----------|
|
||||
| **企微应用内** | `Ctrl + Shift + R`(强制刷新,清除缓存) |
|
||||
| **普通浏览器** | `Ctrl + F5` 或 `Ctrl + Shift + R` |
|
||||
| **iOS 企微** | 长按应用卡片 → 删除 → 重新从工作台添加 |
|
||||
| **完全清除** | 清除浏览器缓存后重新访问 |
|
||||
|
||||
> **注意**:企微应用(webview)的缓存机制与普通浏览器不同,普通 F5 刷新可能无效,必须使用 `Ctrl + Shift + R`。
|
||||
|
||||
### 4.4 监控指标
|
||||
|
||||
| 指标 | 阈值 | 说明 |
|
||||
|------|------|------|
|
||||
| CPU 使用率 | < 80% | 主机层面 |
|
||||
| 内存使用率 | < 80% | 主机层面 |
|
||||
| 磁盘使用率 | < 70% | 主机层面 |
|
||||
| 容器状态 | 全部 Up | docker compose ps |
|
||||
| API 响应时间 | P95 < 500ms | 业务层面 |
|
||||
|
||||
### 4.5 日志位置
|
||||
|
||||
| 服务 | 日志命令 |
|
||||
|------|----------|
|
||||
| 后端 | `docker compose logs backend` |
|
||||
| Nginx | `docker compose logs nginx` |
|
||||
| PostgreSQL | `docker compose logs postgres` |
|
||||
| Redis | `docker compose logs redis` |
|
||||
|
||||
---
|
||||
|
||||
## 五、故障排查
|
||||
|
||||
> **本章已整合至标准故障排查手册**:[04-运维文档/部署运维/00-标准故障排查手册.md](../04-运维文档/部署运维/00-标准故障排查手册.md)
|
||||
>
|
||||
> 手册涵盖:三步隔离法、错误码速查(500/502/503/403/422/网络挂起)、诊断脚本与命令、案例库(含 Redis urlparse 挂起、502、各类修复记录)、端到端验证完成标准(含"宣布修复前必须提供真实浏览器截图"硬规则)。
|
||||
>
|
||||
> **日常排故请直接打开该手册**,本文档不再重复故障排查细节。
|
||||
|
||||
---
|
||||
|
||||
## 六、回滚方案
|
||||
|
||||
### 6.1 快速回滚
|
||||
|
||||
```bash
|
||||
# 停止当前版本
|
||||
docker compose down
|
||||
|
||||
# 恢复上一个版本(需提前备份)
|
||||
# 方法1: 从 Git 拉取上一个 commit
|
||||
git checkout {上一个commit-hash}
|
||||
# 重新构建部署
|
||||
|
||||
# 方法2: 保留上一个版本的部署包
|
||||
cd /opt/wecom-it-desk-backup
|
||||
tar xzf deploy-v0.x.x.tar.gz
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### 6.2 回滚检查清单
|
||||
|
||||
- [ ] 确认上一个版本可用
|
||||
- [ ] 通知相关人员
|
||||
- [ ] 记录当前版本问题
|
||||
- [ ] 执行回滚
|
||||
- [ ] 验证回滚后功能正常
|
||||
- [ ] 发送回滚通知
|
||||
|
||||
---
|
||||
|
||||
## 七、备份恢复
|
||||
|
||||
### 7.1 备份策略
|
||||
|
||||
| 备份对象 | 方法 | 频率 | 保留 |
|
||||
|---------|------|------|------|
|
||||
| PostgreSQL | pg_dump | 每日凌晨 | 7 天 |
|
||||
| Redis | redis-cli SAVE | 每日凌晨 | 7 天 |
|
||||
| 配置文件 | tar 归档 | 每次部署 | 4 个版本 |
|
||||
| 日志文件 | logrotate | 每周 | 4 周 |
|
||||
|
||||
### 7.2 备份命令
|
||||
|
||||
```bash
|
||||
# 备份数据库
|
||||
docker compose exec postgres pg_dump -U postgres wecom_it > /tmp/backup_$(date +%Y%m%d).sql
|
||||
|
||||
# 备份 Redis
|
||||
docker compose exec redis redis-cli SAVE
|
||||
cp /var/lib/docker/volumes/wecom-it-desk_redis_data/_data/dump.rdb /tmp/redis_$(date +%Y%m%d).rdb
|
||||
|
||||
# 备份配置
|
||||
tar czf /tmp/config_$(date +%Y%m%d).tar.gz /opt/wecom-it-desk/.env /opt/wecom-it-desk/nginx/
|
||||
```
|
||||
|
||||
### 7.3 恢复命令
|
||||
|
||||
```bash
|
||||
# 恢复数据库
|
||||
docker compose exec -T postgres psql -U postgres wecom_it < backup_20260701.sql
|
||||
|
||||
# 恢复 Redis
|
||||
docker compose exec -T redis redis-cli FLUSHALL
|
||||
# 停止服务后复制 dump.rdb 到数据目录
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、应急响应
|
||||
|
||||
### 8.1 事件分级
|
||||
|
||||
| 等级 | 场景 | 响应时间 |
|
||||
|------|------|----------|
|
||||
| 🔴 P0 | 鉴权漏洞 / 数据泄露 / 服务全停 | 5 min |
|
||||
| 🟠 P1 | 功能故障 / 单服务降级 | 30 min |
|
||||
| 🟡 P2 | 性能问题 / UI 异常 | 4 h |
|
||||
| 🟢 P3 | 体验优化 | 1 周 |
|
||||
|
||||
### 8.2 P0 应急流程
|
||||
|
||||
#### 立即止血
|
||||
|
||||
```bash
|
||||
# 1. 关闭外网访问
|
||||
sudo iptables -A INPUT -p tcp --dport 443 -j DROP
|
||||
|
||||
# 2. 停可疑服务
|
||||
docker compose stop backend
|
||||
|
||||
# 3. 保留现场(不删文件)
|
||||
docker compose logs backend > /tmp/incident-backend.log
|
||||
docker compose logs nginx > /tmp/incident-nginx.log
|
||||
```
|
||||
|
||||
#### 通知
|
||||
|
||||
- 微信/电话通知项目负责人
|
||||
- 邮件通知:`wecom-it-desk-incident@servyou-it.com`
|
||||
|
||||
### 8.3 应急联系
|
||||
|
||||
| 角色 | 联系人 |
|
||||
|------|--------|
|
||||
| 项目负责人 | 宋献 |
|
||||
| 运维 | IT 支持组 |
|
||||
| 企微技术支持 | 企微客服 |
|
||||
|
||||
---
|
||||
|
||||
## 附录
|
||||
|
||||
### 版本历史
|
||||
|
||||
| 版本 | 日期 | 更新内容 |
|
||||
|------|------|----------|
|
||||
| v1.0 | 2026-07-04 | 初始版本,整合部署/运维/故障排查文档 |
|
||||
|
||||
### 相关文档
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| `docs/01-项目总览/01-项目总览与部署手册-20260704.md` | 完整项目背景与架构设计 |
|
||||
| `docs/RELEASE_NOTES_v0.7.1.md` | 版本发布说明 |
|
||||
| `docs/SOPs/SOP-004-应急响应.md` | 详细应急响应流程 |
|
||||
| `04-运维文档/部署运维/00-标准故障排查手册.md` | 标准故障排查手册(故障排查唯一入口)|
|
||||
|
||||
---
|
||||
|
||||
> **维护说明**: 本文档由助理(小米)维护,随每次发布更新。
|
||||
> 如有更新,请同步更新本文档的版本号和日期。
|
||||
@@ -0,0 +1,731 @@
|
||||
# 企微智能IT支持服务台 — 项目总览与部署手册
|
||||
|
||||
> **版本**: v2.2 | **日期**: 2026-07-04 | **编制**: 宋献(IT支持组组长)
|
||||
> **目标读者**: **管理者 / 架构师 / 运维** — 了解项目全貌、架构决策、部署与运维操作
|
||||
|
||||
> **📖 与 README.md 的关系**: 本文是 [README.md](../README.md) 的**详细版本**,侧重完整的架构设计和部署运维。README 适合新人快速入门,本文适合深入了解。
|
||||
|
||||
> **⚠️ 运维手册更新**: 部署与运维操作已整合到独立文档 [智能IT服务系统运维手册](./智能IT服务系统运维手册.md),该文档由助理(小米)维护,随每次发布更新。本文档保留架构设计与背景信息。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [项目概述](#一项目概述)
|
||||
2. [系统架构](#二系统架构)
|
||||
3. [三步演进路径](#三三步演进路径)
|
||||
4. [现有系统复用评估](#四现有系统复用评估)
|
||||
5. [正式环境部署方案](#五正式环境部署方案)
|
||||
6. [部署操作手册](#六部署操作手册)
|
||||
7. [运维管理](#七运维管理)
|
||||
8. [开发交付状态](#八开发交付状态)
|
||||
9. [附录](#九附录)
|
||||
|
||||
---
|
||||
|
||||
## 一、项目概述
|
||||
|
||||
### 1.1 背景与痛点
|
||||
|
||||
公司约 **6000 人**,全国设分子机构,使用企业微信作为内部 IM。当前 IT 服务存在三大痛点:
|
||||
|
||||
| 痛点 | 现状 | 影响 |
|
||||
|------|------|------|
|
||||
| 员工绕过 AI 直接找人工 | 可通过关键词直通人工坐席,首次后永久记忆 | AI 筛选率极低,人工成本高 |
|
||||
| AI 转人工需另开窗口 | 跳转到企微"员工服务"模块,与 AI 对话割裂 | 体验差,员工困惑 |
|
||||
| 无法跨主体共享 | 企微"员工服务"不支持互联企业应用共享 | 跨企业服务不可达 |
|
||||
|
||||
### 1.2 核心方案
|
||||
|
||||
**自研 IT 服务坐席系统**,替代企微内置的"员工服务"模块:
|
||||
- 基于企微自建应用消息 API,所有消息由自己的服务器接管
|
||||
- 分三步渐进式构建:M1 消息接管 → M2 AI 接入 → M3 知识库闭环
|
||||
- 当前处于 **M1(消息接管 + 极简坐席)开发完成,部署配置中**
|
||||
|
||||
### 1.3 核心设计理念
|
||||
|
||||
传统"串行排队"改为**"并行协作"**——AI 全程在线,人工随时介入:
|
||||
|
||||
| 角色 | 工作方式 |
|
||||
|------|---------|
|
||||
| AI | 全程在线,所有对话可见 |
|
||||
| 坐席 | 随时介入,AI 始终在旁辅助 |
|
||||
| 员工 | 同一窗口,AI 和人工无缝切换 |
|
||||
|
||||
---
|
||||
|
||||
## 二、系统架构
|
||||
|
||||
### 2.1 部署架构总览(预生产环境)
|
||||
|
||||
> **当前阶段**:预生产环境。智能咨询系统与 IT 数据查询平台**分别部署在不同主机**,通过 Nginx 路径路由共用域名 `it-dataquery.dc.servyou-it.com`。正式环境将迁移到 K8s 集群。
|
||||
|
||||
```
|
||||
浏览器 ──→ it-dataquery.dc.servyou-it.com:80
|
||||
│
|
||||
▼
|
||||
┌─── nginx (本系统主机) ───────────────┐
|
||||
│ │
|
||||
│ /itdesk/* → H5 员工端 SPA │
|
||||
│ /itagent/* → 坐席工作台 SPA │
|
||||
│ /api/* → backend:8000 (FastAPI) │
|
||||
│ /ws/* → backend:8000 (WS) │
|
||||
│ /* → 数据平台主机(远程IP) │ ← 跨主机代理
|
||||
│ │
|
||||
└──────────────┬───────────────────────┘
|
||||
│ 本机 Docker 网络
|
||||
┌─────────────┼─────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ backend │ │ postgres │ │ redis │
|
||||
│ :8000 │ │ :5432 │ │ :6379 │
|
||||
└──────────┘ └──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
| 对比项 | 预生产(当前) | 正式环境(未来) |
|
||||
|--------|-------------|---------------|
|
||||
| 部署方式 | Docker Compose(单主机) | K8s 集群(高可用) |
|
||||
| 与数据平台关系 | 不同主机,Nginx 远程代理 | 独立 K8s 集群 |
|
||||
| 域名 | 共用 `it-dataquery.dc.servyou-it.com` | 独立域名或 K8s Ingress |
|
||||
|
||||
### 2.2 技术栈
|
||||
|
||||
| 层级 | 技术选型 | 说明 |
|
||||
|------|---------|------|
|
||||
| 反向代理 | Nginx | 统一入口、路径路由、WebSocket 代理 |
|
||||
| 后端框架 | FastAPI (Python 3.12) | 异步、自动 OpenAPI 文档、类型安全 |
|
||||
| 数据库 | PostgreSQL 16 | 会话/消息/坐席/配置 持久化(9 张表) |
|
||||
| 缓存 | Redis 7 | access_token 缓存(TTL 7200s)、JWT 会话 |
|
||||
| ORM | SQLAlchemy 2.0 (async) | 异步 session、声明式模型 |
|
||||
| 数据库迁移 | Alembic | 所有表结构变更通过迁移脚本管理 |
|
||||
| 坐席前端 | Vue3 + ElementPlus + Pinia | 企业级组件库,三栏工作台 |
|
||||
| 员工 H5 | Vue3 + Vant4 + Pinia | 移动端组件库,企微 WebView 兼容 |
|
||||
| 容器化 | Docker + Docker Compose | 4 容器一键启停 |
|
||||
|
||||
### 2.3 数据库核心表(9 张)
|
||||
|
||||
| 表名 | 用途 | 关键字段 |
|
||||
|------|------|---------|
|
||||
| `conversations` | 会话主表 | employee_id, status, urgency_score(1-5), tags(JSON), is_vip, participants(JSON) |
|
||||
| `messages` | 消息记录 | sender_type(employee/agent/ai/system), content, msg_type |
|
||||
| `agents` | 坐席信息 | user_id, status(online/offline/busy), current_load |
|
||||
| `quick_reply_templates` | 快速回复模板 | category, title, content(支持 {变量}) |
|
||||
| `system_configs` | 系统配置 | config_key, config_value(关键词/阈值/话术等) |
|
||||
| `funny_phrases` | 趣味话术 | scene(6 种场景), content, tone, is_active |
|
||||
| `approval_links` | 审批流程链接 | category(IT/HR/行政/财务), title, url |
|
||||
| `software_downloads` | 软件下载入口 | category, name, version, platform, download_url |
|
||||
| `agent_notes` | 坐席备注 | conversation_id, agent_id, content |
|
||||
|
||||
### 2.4 API 接口分组
|
||||
|
||||
| 分组 | 路径前缀 | 核心接口 |
|
||||
|------|---------|---------|
|
||||
| 企微回调 | `/api/wecom/callback` | GET 验证 URL、POST 接收消息 |
|
||||
| 会话管理 | `/api/conversations` | 列表/详情/状态/置顶/代办/接单/邀请/退出/移除参与者 |
|
||||
| 消息管理 | `/api/conversations/{id}/messages` | 消息列表/发送 |
|
||||
| 坐席管理 | `/api/agents` | 列表/登录/状态切换 |
|
||||
| H5 用户端 | `/api/h5/*` | 会话/摇人/审批链接/软件下载/OAuth |
|
||||
| WebSocket | `/ws/{agent_id}` | 实时推送(坐席端) |
|
||||
|
||||
统一响应格式:`{ "code": 0, "data": {}, "message": "success" }`
|
||||
|
||||
### 2.5 消息收发全链路
|
||||
|
||||
```
|
||||
员工发消息 → 企微回调解密 → 消息路由 → 评分标记 → 入库 → 坐席 WS 推送 → 坐席回复 → 企微主动推送 → 员工同一窗口收到
|
||||
```
|
||||
|
||||
**坐席端通信**:已升级为 WebSocket 实时推送(2026-06-03),替代原计划的短轮询:
|
||||
- 心跳保活:前端每 30s 发 ping,后端回 pong
|
||||
- 断线重连:指数退避(1s→2s→4s→...→30s 上限)
|
||||
- 降级策略:WS 断连时自动降级为 3s 轮询
|
||||
|
||||
### 2.6 会话排序与评分规则
|
||||
|
||||
**排序**: 紧急 → 举手 → 需介入 → 活跃 → AI处理中 → 已结单(同级按时间倒序)
|
||||
|
||||
**紧急度评分**: `基础分(关键词) + 情绪加成 + VIP加成 + 重复追问加成`,范围 1-5
|
||||
|
||||
**标记系统**:
|
||||
|
||||
| 标记 | 图标 | 触发条件 |
|
||||
|------|------|---------|
|
||||
| VIP | 红色 | 企微通讯录规则匹配 |
|
||||
| 举手 | 黄色 | 员工说关键词或点击摇人按钮 |
|
||||
| 需介入 | 橙红 | 同一问题追问 >3 轮 |
|
||||
| 情绪 | 红色 | 关键词匹配(急/崩溃/投诉等) |
|
||||
|
||||
---
|
||||
|
||||
## 三、三步演进路径
|
||||
|
||||
| 里程碑 | 周期 | 核心交付 | 状态 |
|
||||
|--------|------|---------|------|
|
||||
| **M1** 消息接管 + 极简坐席 | 6-8 周 | 企微 API 链路验证 · 坐席三栏工作台 · 员工 H5 双栏 · 邀请功能(多人会话协作) | ✅ 代码完成,部署中 |
|
||||
| **M2** AI 机器人接入 | M1 后 4-6 周 | 千问/Dify/RAGFlow 接入 · AI 前置筛选 · 排队系统 | 📋 计划中 |
|
||||
| **M3** 知识库闭环迭代 | M2 后 4-6 周 | 坐席标注系统 · 千问自动分析 · 知识库自优化 | 📋 计划中 |
|
||||
|
||||
### M1 当前进度(2026-06-03)
|
||||
|
||||
| 模块 | 状态 |
|
||||
|------|------|
|
||||
| PRD + 架构设计 | ✅ 完成 |
|
||||
| 后端代码(45+ 文件,7 API 组) | ✅ 完成 |
|
||||
| 坐席前端(三栏工作台 + WebSocket) | ✅ 完成 |
|
||||
| 员工 H5(双栏 + 摇人按钮 + 呼叫坐席) | ✅ 完成 |
|
||||
| 邀请功能(多人会话协作 — PRD §21) | 📋 计划中(M1 范围) |
|
||||
| 前端功能联调验证 | ✅ 完成(2026-06-03) |
|
||||
| 测试用例(116 条 pytest) | ✅ 完成 |
|
||||
| Alembic 数据库迁移 | ✅ 完成 |
|
||||
| 前端构建产物(dist/) | ✅ 完成 |
|
||||
| 远程服务器部署 | 🔧 待 SSH 账号 |
|
||||
|
||||
### M2 核心改动
|
||||
|
||||
只改路由层逻辑,其余不动:
|
||||
```
|
||||
M1: 新会话 → 坐席队列
|
||||
M2: 新会话 → AI 先回答 → AI判断/用户触发 → 坐席队列
|
||||
```
|
||||
新增:千问对话模型、RAGFlow 知识库检索、Dify 编排平台、排队系统。目标:AI 首答率 ≥ 80%。
|
||||
|
||||
### M3 知识库迭代闭环
|
||||
|
||||
```
|
||||
坐席日常标注 ──→ 千问分析 ──→ 自动处理 ──→ 知识库增强
|
||||
(正确/错误) (缺文档/过时) │
|
||||
↑ │
|
||||
└──────────── 持续循环 ─────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、现有系统复用评估
|
||||
|
||||
### 4.1 核心复用(直接影响新系统架构)
|
||||
|
||||
| # | 资源 | 复用方式 | 新系统对应 |
|
||||
|---|------|---------|-----------|
|
||||
| 1 | Dify Workflow | 直接复用,M2 阶段接入 AI 回复 | 坐席助手 AI 面板 + 自动回复 |
|
||||
| 2 | dify2openai 桥接 | 直接复用 API | 后端调用 AI 的入口 |
|
||||
| 3 | RAGFlow 知识库 | 直接复用,M3 阶段混合标注迭代 | 知识库管理 + 标注闭环 |
|
||||
| 4 | Qwen3-30B 大模型 | 直接复用 | AI 对话底层模型 |
|
||||
| 5 | bge-m3 向量模型 | RAGFlow 内置,直接复用 | 知识库检索向量化 |
|
||||
| 6 | Dify 数据库(只读) | 读取 messages 表,同步历史数据 | 历史会话数据迁移 + 统计 |
|
||||
| 7 | 企微自建应用 | 直接复用应用凭证 | 消息收发的企微入口 |
|
||||
|
||||
### 4.2 基础设施复用(零耦合)
|
||||
|
||||
| 资源 | 复用方式 | 耦合度 |
|
||||
|------|---------|--------|
|
||||
| 企微自建应用凭证 | 配置文件引用(只读) | 零耦合 |
|
||||
| Dify Workflow API | HTTP 调用 | 外部依赖 |
|
||||
| RAGFlow 知识库 | HTTP 调用 | 外部依赖 |
|
||||
| Qwen3-30B 大模型 | HTTP 调用 | 外部依赖 |
|
||||
| SSL 证书文件 | Nginx 挂载只读 | 零耦合 |
|
||||
|
||||
### 4.3 关键结论
|
||||
|
||||
> 代码层面复用率约 15%(主要是业务逻辑和 SQL 查询),基础设施和 AI 能力复用率约 70%。
|
||||
> 新系统用 **FastAPI + SQLAlchemy 2.0**,不沿用旧 Django 代码。底层业务逻辑可参考移植。
|
||||
|
||||
---
|
||||
|
||||
## 五、正式环境部署方案
|
||||
|
||||
### 5.1 核心决策原则
|
||||
|
||||
基于四个约束条件:
|
||||
|
||||
| # | 约束 | 推导原则 |
|
||||
|---|------|---------|
|
||||
| 1 | 对现有正式环境架构影响最小 | **物理隔离 > 逻辑隔离** |
|
||||
| 2 | 避免变更影响现有服务 | **独立 Nginx 入口** |
|
||||
| 3 | 减少服务依赖 | **最小化外部依赖** |
|
||||
| 4 | 避免责任不清 | **独立数据库 + 独立 Redis** |
|
||||
|
||||
> **一句话**:新系统作为**独立服务单元**部署,与现有智能 IT 数据平台(Django)在物理资源层面完全解耦,仅通过 HTTP API 调用共享 AI 能力。
|
||||
|
||||
### 5.2 关键隔离策略
|
||||
|
||||
| 隔离层面 | 方案 | 效果 |
|
||||
|---------|------|------|
|
||||
| 服务器级 | 独立 VM,不共用宿主机 | 挂了不影响旧系统 |
|
||||
| 网络级 | Docker 内部网络,PG/Redis 不暴露宿主机端口 | 外部无法直连数据库 |
|
||||
| 存储级 | 独立命名卷,不共用 Volume | 数据完全隔离 |
|
||||
| 域名级 | 路径路由(共用域名) + 独立 Nginx 容器 | `/itdesk/`、`/itagent/`、`/api/` 归属本系统 |
|
||||
| 认证级 | JWT + 独立 Redis | 账户体系独立 |
|
||||
| 依赖级 | 仅 HTTP 调用外部 AI 服务 | 外部服务故障只影响 M2 功能 |
|
||||
|
||||
### 5.3 与现有系统的解耦修正
|
||||
|
||||
原复用评估中的部分共享方案已修正为独立部署:
|
||||
|
||||
| 原建议 | 修正方案 | 理由 |
|
||||
|--------|---------|------|
|
||||
| 同机部署于 10.80.0.86 | 独立服务器/VM(或同机端口分离+独立 compose) | 避免端口冲突、资源争抢 |
|
||||
| Redis 复用同实例 | 独立 Redis 容器 | FLUSHDB 误操作、内存 OOM 互相影响 |
|
||||
| 使用旧系统 Nginx | 独立 Nginx 容器 | 变更反代配置不影响旧系统路由 |
|
||||
| 复用旧 PG 实例 | 独立 PostgreSQL 容器 | 数据库是责任边界核心 |
|
||||
|
||||
### 5.4 Docker Compose 服务清单
|
||||
|
||||
| 服务 | 镜像 | 端口 | 健康检查 |
|
||||
|------|------|------|---------|
|
||||
| postgres | postgres:16 | 5432(内部) | pg_isready |
|
||||
| redis | redis:7 | 6379(内部) | redis-cli ping |
|
||||
| backend | 自构建 Dockerfile | 8000(内部) | GET /health |
|
||||
| nginx | nginx:alpine | 18080:80(对外) | GET /health |
|
||||
|
||||
Docker 网络:`it-desk-internal`(内部,连接 backend/postgres/redis)
|
||||
|
||||
### 5.5 资源需求
|
||||
|
||||
| 资源 | 配置 | 说明 |
|
||||
|------|------|------|
|
||||
| 服务器 | 4C8G + 100GB SSD(最低)/ 8C16G + 200GB SSD(推荐) | Docker Engine 环境 |
|
||||
| 域名 | `it-dataquery.dc.servyou-it.com`(已就绪,共用) | 路径路由 `/itdesk/` `/itagent/` `/api/` |
|
||||
| 企微自建应用 | 1 个(已创建) | CorpID/AgentID/Secret/Token/EncodingAESKey |
|
||||
| 防火墙 | 办公网→服务器:80/443, 企微→服务器:443 | 出站: 企微 API/ Dify/ RAGFlow/ Qwen |
|
||||
|
||||
### 5.6 风险矩阵
|
||||
|
||||
| 风险 | 概率 | 影响 | 缓解措施 |
|
||||
|------|------|------|---------|
|
||||
| 新服务器申请被拒/延迟 | 中 | 部署延期 | 退化方案:旧服务器端口分离+独立 compose |
|
||||
| SSL 证书到期 | 低 | HTTPS 不可用 | 复用现有通配符证书 |
|
||||
| 企微应用配置变更 | 低 | 双系统消息中断 | 建立变更通知机制 |
|
||||
| Dify/RAGFlow 不可用 | 中 | M2 AI 功能不可用 | 降级:纯坐席模式仍正常工作 |
|
||||
| Docker 宿主机故障 | 低 | 新系统全宕 | Compose 配置即代码,重建快 |
|
||||
|
||||
---
|
||||
|
||||
## 六、部署操作手册
|
||||
|
||||
> **预生产部署**:本系统与数据平台部署在**不同主机**,通过 Nginx 路径路由共用域名。数据平台请求通过远程 IP 反代(非 Docker 网络)。正式环境将迁移到 K8s。
|
||||
|
||||
### 6.1 前置条件
|
||||
|
||||
- 服务器已安装 Docker Engine 24+ + Docker Compose v2
|
||||
- IT 数据查询平台已部署运行
|
||||
- 有 SSH 登录权限
|
||||
|
||||
### 6.2 配置数据平台反代地址
|
||||
|
||||
预生产环境中,数据平台部署在**独立主机**。部署前需修改 `nginx/nginx.conf` 中的数据平台上游地址:
|
||||
|
||||
```nginx
|
||||
# nginx/nginx.conf — 将 DATAQUERY_HOST 替换为数据平台主机的实际 IP
|
||||
upstream dataquery {
|
||||
server 10.80.0.86:80; # ← 替换为数据平台实际 IP:端口
|
||||
}
|
||||
```
|
||||
|
||||
> **为什么不创建 Docker 共享网络?** 预生产两台主机不在同一 Docker Engine,无法使用 `docker network create` 互联。正式环境迁移 K8s 后由 Ingress/Service 处理路由。
|
||||
|
||||
### 6.3 上传部署包
|
||||
|
||||
在本地(Windows)执行打包上传:
|
||||
|
||||
```bash
|
||||
# 方式 A:使用 deploy.sh 打包
|
||||
bash scripts/deploy.sh --pack
|
||||
scp it-smart-desk-*.tar.gz user@server:/opt/
|
||||
|
||||
# 方式 B:手动打包
|
||||
tar czf deploy.tar.gz \
|
||||
backend/ frontend-h5/dist/ frontend-agent/dist/ \
|
||||
nginx/ docker-compose.yml .env.production scripts/
|
||||
scp deploy.tar.gz user@server:/opt/it-smart-desk/
|
||||
```
|
||||
|
||||
### 6.4 服务器配置与启动
|
||||
|
||||
```bash
|
||||
ssh user@server
|
||||
cd /opt/it-smart-desk
|
||||
tar xzf it-smart-desk-*.tar.gz
|
||||
|
||||
# 创建环境配置
|
||||
cp .env.production .env
|
||||
vim .env # 填入真实企微凭证
|
||||
```
|
||||
|
||||
`.env` 必填项:
|
||||
|
||||
| 配置项 | 说明 | 获取位置 |
|
||||
|--------|------|---------|
|
||||
| `WECOM_CORP_ID` | 企业 ID | 企微管理后台 > 我的企业 |
|
||||
| `WECOM_AGENT_ID` | 应用 AgentId | 企微管理后台 > 应用管理 |
|
||||
| `WECOM_SECRET` | 应用 Secret | 企微管理后台 > 应用管理 |
|
||||
| `WECOM_TOKEN` | 回调 Token | 企微管理后台 > 接收消息 |
|
||||
| `WECOM_ENCODING_AES_KEY` | 回调 AES 密钥 | 企微管理后台 > 接收消息 |
|
||||
| `POSTGRES_PASSWORD` | 数据库密码 | 自定义强密码 |
|
||||
| `CORS_ORIGINS` | `http://it-dataquery.dc.servyou-it.com` | CORS 白名单 |
|
||||
|
||||
启动:
|
||||
|
||||
```bash
|
||||
bash scripts/deploy.sh
|
||||
# 自动执行:检查前置条件 → 构建后端镜像 → 启动所有容器 → 运行数据库迁移
|
||||
```
|
||||
|
||||
### 6.5 验证部署
|
||||
|
||||
```bash
|
||||
# 检查容器状态
|
||||
docker compose ps
|
||||
# 预期:4 个容器全部 Up/healthy
|
||||
|
||||
# 健康检查
|
||||
curl http://localhost:18080/api/health
|
||||
|
||||
# 浏览器验证
|
||||
# http://it-dataquery.dc.servyou-it.com/itdesk/ → H5 员工咨询页面
|
||||
# http://it-dataquery.dc.servyou-it.com/itagent/ → 坐席工作台登录页
|
||||
# http://it-dataquery.dc.servyou-it.com/ → IT 数据查询平台(不变)
|
||||
# http://it-dataquery.dc.servyou-it.com/api/docs → FastAPI Swagger 文档
|
||||
```
|
||||
|
||||
### 6.6 常见问题
|
||||
|
||||
**nginx 启动失败,报 `host not found in upstream "dataquery"`**
|
||||
→ `nginx/nginx.conf` 中 `DATAQUERY_HOST` 未替换为数据平台的实际 IP。确保已在部署前完成替换。
|
||||
|
||||
**nginx 启动但数据平台页面 502**
|
||||
→ 本系统主机无法访问数据平台主机 IP。检查防火墙策略是否放行两台主机间的 80 端口。
|
||||
|
||||
**访问 `/itdesk/` 返回 404**
|
||||
→ 检查前端 dist 是否正确挂载:`docker exec wecom_it_nginx ls -la /usr/share/nginx/html/itdesk/`
|
||||
|
||||
**API 返回 CORS 错误**
|
||||
→ 检查 `.env` 中 `CORS_ORIGINS` 是否包含 `http://it-dataquery.dc.servyou-it.com`
|
||||
|
||||
**数据库迁移失败**
|
||||
→ PostgreSQL 可能未就绪,等 30 秒后执行:`docker compose restart backend`
|
||||
|
||||
### 6.7 更新部署
|
||||
|
||||
```bash
|
||||
# 仅更新前端
|
||||
bash scripts/deploy.sh --build
|
||||
docker compose restart nginx
|
||||
|
||||
# 仅更新后端
|
||||
docker compose build backend
|
||||
docker compose up -d backend
|
||||
|
||||
# 全量更新
|
||||
bash scripts/deploy.sh --down
|
||||
bash scripts/deploy.sh
|
||||
```
|
||||
|
||||
### 6.8 回滚
|
||||
|
||||
```bash
|
||||
docker compose down # 停止新系统所有容器
|
||||
# 旧系统不受任何影响(独立资源)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、运维管理
|
||||
|
||||
### 7.1 责任矩阵
|
||||
|
||||
| 运维操作 | 影响范围 | 备注 |
|
||||
|---------|---------|------|
|
||||
| 重启 PostgreSQL | 仅新系统 | 独立实例 |
|
||||
| 重启 Redis | 仅新系统 | 独立实例 |
|
||||
| 修改 Nginx 配置 | 仅新系统路由 | 独立容器 |
|
||||
| 更新后端/前端代码 | 仅新系统 | 独立容器 |
|
||||
| 企微应用配置变更 | **双系统** | ⚠️ 唯一共享点,需通知双方 |
|
||||
|
||||
### 7.2 监控指标
|
||||
|
||||
```yaml
|
||||
主机层面:
|
||||
- CPU 使用率 < 80%
|
||||
- 内存使用率 < 80%
|
||||
- 磁盘使用率 < 70%
|
||||
|
||||
容器层面:
|
||||
- docker compose ps 全部 "Up" 状态
|
||||
- Nginx 健康检查: GET /health → 200
|
||||
- Backend 健康检查: GET /health → 200
|
||||
|
||||
业务层面(后续接入):
|
||||
- 企微消息回调成功率 > 99%
|
||||
- API 响应时间 P95 < 500ms
|
||||
```
|
||||
|
||||
### 7.3 备份策略
|
||||
|
||||
| 备份对象 | 方法 | 频率 | 保留 |
|
||||
|---------|------|------|------|
|
||||
| PostgreSQL 数据 | `pg_dump` + 卷快照 | 每日凌晨 | 7 天 |
|
||||
| Redis 数据 | `SAVE` + 复制 dump.rdb | 每日凌晨 | 7 天 |
|
||||
| Docker 卷 | `tar czf` 归档 | 每周 | 4 周 |
|
||||
|
||||
### 7.4 关键对接参数(M2 阶段)
|
||||
|
||||
| 参数 | 值 | 用途 |
|
||||
|------|-----|------|
|
||||
| dify2openai API | `http://yw-dify.dc.servyou-it.com/dify2openai/v1/chat/completions` | AI 对话 |
|
||||
| RAGFlow | `http://10.80.0.85:8080` | 知识库管理 |
|
||||
| Qwen3-30B | `http://10.80.0.49:5000/api/llm/servyou/v1/chat/completions` | 大模型 |
|
||||
| Dify DB(生产只读) | `10.80.128.40:5432` DB=dify User=difyro | 历史数据同步 |
|
||||
| 数据平台 | `http://it-dataquery.dc.servyou-it.com` (10.80.0.86) | 部署服务器 |
|
||||
|
||||
### 7.5 应急预案可选技术项
|
||||
|
||||
> **评估日期**: 2026-06-03 | **来源**: 企微原生1对1方案(PRD §3.2 方式五)可行性评估
|
||||
|
||||
#### 7.5.1 备用方案概述
|
||||
|
||||
当 H5 WebView 方案(当前主方案)出现以下情况时,可切换至**企微原生1对1方案**作为降级/备用:
|
||||
|
||||
| 应急场景 | 当前方案症状 | 备用方案动作 |
|
||||
|---------|------------|------------|
|
||||
| H5 前端服务不可用 | Nginx 静态文件丢失/构建产物损坏 | 员工直接在企微与应用1对1聊天,走 `/message/send` 回复 |
|
||||
| H5 页面性能问题 | WebView 加载慢/白屏/兼容性问题 | 放弃 H5 入口,改用企微原生聊天窗口交互 |
|
||||
| OAuth2 鉴权异常 | 静默授权失败,H5 无法获取员工身份 | 原生方案无需 OAuth2,回调自带 UserID |
|
||||
| 跨平台接入需求 | 需接入钉钉/飞书/浏览器用户 | **不适合切换**——原生方案无法跨平台,此时应修复 H5 |
|
||||
| 外部专家协作 | 坐席需要拉入第三方专家协助 | 启用 `/appchat/create` 创建临时群聊 |
|
||||
|
||||
#### 7.5.2 备用方案技术架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 员工端(企微原生1对1聊天窗口) │
|
||||
│ │
|
||||
│ 员工 ←─消息─→ 自建应用(IT智能助手) │
|
||||
│ │ │
|
||||
│ ├─ AI回复 → /message/send → 同一窗口 │
|
||||
│ ├─ 坐席回复 → /message/send → 同一窗口 │
|
||||
│ └─ 外援 → /appchat/create → 新群聊窗口 │
|
||||
│ │
|
||||
│ 坐席工作台(保留,不变) │
|
||||
│ ├─ WebSocket 接收员工消息 │
|
||||
│ ├─ 坐席回复 → 后端 → /message/send → 员工窗口 │
|
||||
│ └─ 外援指令 → 后端 → /appchat/create → 新群聊 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**核心能力**:项目**已经具备**备用方案所需的全部后端代码:
|
||||
- `wecom_callback.py`:接收企微回调 ✅
|
||||
- `message_router._try_ai_reply()` → `wecom_service.send_text_message()`:AI回复走 `/message/send` ✅
|
||||
- `scoring_service.detect_hand_raise()`:关键词举手检测 ✅
|
||||
- `wecom_service.send_text_message()`:应用消息推送 ✅
|
||||
|
||||
**仅需新增**:
|
||||
- 交互卡片消息发送(`msgtype="template_card"`)— 用于"转人工"按钮、满意度评分
|
||||
- AppChat API 封装(`/cgi-bin/appchat/*`)— 用于外援群聊场景
|
||||
- AI/人工身份区分前缀(如 `🤖 AI回复:` / `👨💻 人工坐席(张三):`)
|
||||
|
||||
#### 7.5.3 切换流程
|
||||
|
||||
**从 H5 方案切换到原生1对1方案**:
|
||||
|
||||
| 步骤 | 操作 | 负责人 | 预计耗时 |
|
||||
|------|------|--------|---------|
|
||||
| 1 | 确认企微回调 URL 已配置且可达(H5 方案已配置则无需改动) | 运维 | 0 min |
|
||||
| 2 | 确认 `message_router._try_ai_reply()` 走 `/message/send`(已实现) | 开发 | 0 min |
|
||||
| 3 | 通知员工:直接在企微与应用聊天即可,不再进入 H5 | 运维 | 5 min |
|
||||
| 4 | (可选)关闭 H5 入口:Nginx 配置注释 `/itdesk/` 路由 | 运维 | 2 min |
|
||||
| 5 | (可选)启用交互卡片:部署 template_card 消息发送代码 | 开发 | 1-2 天 |
|
||||
|
||||
> **关键点**:步骤1-3 **零代码改动**即可完成基本切换,因为核心回调+消息推送链路已在运行。
|
||||
|
||||
**从原生1对1方案切回 H5 方案**:
|
||||
|
||||
| 步骤 | 操作 | 负责人 |
|
||||
|------|------|--------|
|
||||
| 1 | 恢复 Nginx `/itdesk/` 路由(如已注释) | 运维 |
|
||||
| 2 | 确认 H5 构建产物存在且可访问 | 运维 |
|
||||
| 3 | 通知员工:点击应用 → 进入 H5 咨询页面 | 运维 |
|
||||
|
||||
#### 7.5.4 企微 API 限制与容量评估
|
||||
|
||||
| API | 限制 | 当前业务量(月均 188 次 AI 会话/天) | 风险 |
|
||||
|-----|------|--------------------------------|------|
|
||||
| `/message/send` | ≤账号上限×200人次/天,同一人≤30次/分 | 预估 < 500 人次/天 | ✅ 充裕 |
|
||||
| `/appchat/create` | ≤1000群/天 | 外援场景低频(预估 < 10群/天) | ✅ 充裕 |
|
||||
| `/appchat/send` | ≤2万人次/分,同一人≤200条/分 | 群内消息量极小 | ✅ 充裕 |
|
||||
| 回调消息 | 无硬限制 | 企微服务器推送到回调 URL | ✅ 无风险 |
|
||||
|
||||
#### 7.5.5 备用方案局限性与适用边界
|
||||
|
||||
| 局限 | 说明 | 影响 |
|
||||
|------|------|------|
|
||||
| **无法跨主体企微** | 企微原生1对1仅限同一企微主体内员工 | 无法服务供应商/外包人员 |
|
||||
| **无法跨平台** | 原生方案绑定企微,无法嵌入钉钉/飞书/浏览器 | H5 扩展场景不可用 |
|
||||
| **AI/人工区分不直观** | 都以应用身份推送,需内容前缀区分 | 体验不如 H5 的丰富身份标识 |
|
||||
| **交互卡片需开发** | "转人工"按钮、满意度评分需 template_card 消息类型 | 降级期可用关键词替代("转人工") |
|
||||
| **群聊外援需审批** | appchat API 要求可见范围=根部门 | 需企微管理员配合 |
|
||||
|
||||
> **决策建议**:当 H5 不可用且影响范围仅限企微主体内员工时,**立即切换**原生1对1方案(零代码改动);当需要跨平台/跨主体服务时,**优先修复 H5**,不切换原生方案。
|
||||
|
||||
---
|
||||
---
|
||||
|
||||
## 八、开发交付状态
|
||||
|
||||
### TL;DR
|
||||
|
||||
企微智能IT支持服务台第一步(消息接管 + 极简坐席台)全部代码已完成并通过测试,共 **110+ 文件**,**116/116 测试全部通过**,覆盖后端 API、坐席工作台、用户端 H5 三个子系统。
|
||||
|
||||
### 交付状态
|
||||
|
||||
| 阶段 | 状态 | 产出 |
|
||||
|------|------|------|
|
||||
| PRD | ✅ 完成 | `PRD.md` — 31 需求(P0/P1/P2),7 用户故事 |
|
||||
| 架构设计 | ✅ 完成 | `docs/02-技术文档/技术架构/IT智能服务台-系统架构设计文档v2.md` — 9 表 DDL,7 API 组,4 时序图,5 任务分解 |
|
||||
| T01 项目脚手架 | ✅ 完成 | 57 文件 — docker-compose, nginx, .env, 后端/前端骨架 |
|
||||
| T02 后端核心服务 | ✅ 完成 | 16 文件 — 企微加解密, 消息路由, 评分, 会话, 趣味话术, 7 API 路由 |
|
||||
| T03 坐席工作台 | ✅ 完成 | 25 文件 — 三栏布局, 会话管理, 聊天, AI助手面板(5Tab) |
|
||||
| T04 用户端H5 | ✅ 完成 | 12 文件 — 聊天面板, 摇人按钮, AI助手, 审批链接, 软件下载 |
|
||||
| QA 测试用例 | ✅ 完成 | 8 文件, 116 测试用例(原 93 + 新增 23) |
|
||||
| Bug 修复 | ✅ 完成 | 7 个 Bug 修复(详见下方) |
|
||||
| PostgreSQL/SQLite兼容 | ✅ 完成 | 9 个模型文件全部兼容 SQLite |
|
||||
| database.py 懒加载 | ✅ 完成 | 避免测试导入时连接 PostgreSQL |
|
||||
| WecomCrypto 懒加载 | ✅ 完成 | 避免默认 AES Key 导入报错 |
|
||||
| **pytest 全量验证** | **✅ 116/116 通过** | 1.71 秒完成,0 失败 |
|
||||
|
||||
### 关键文件
|
||||
|
||||
```
|
||||
wecom_it_smart_desk/
|
||||
├── README.md # 项目主文档(GitHub 首页)
|
||||
├── docker-compose.yml # Docker Compose 容器编排
|
||||
├── .env # 环境变量(数据库密码等,不提交 Git)
|
||||
├── backend/ # FastAPI 后端服务
|
||||
│ ├── app/
|
||||
│ │ ├── main.py # FastAPI 应用入口
|
||||
│ │ ├── config.py # 配置管理(从 .env 读取)
|
||||
│ │ ├── database.py # 懒加载数据库引擎
|
||||
│ │ ├── models/ # 11 个 ORM 模型(兼容 PostgreSQL/SQLite)
|
||||
│ │ ├── schemas/ # Pydantic Schema(请求/响应校验)
|
||||
│ │ ├── utils/
|
||||
│ │ │ └── wecom_crypto.py # 企微消息加解密(AES-CBC-256)
|
||||
│ │ ├── services/
|
||||
│ │ │ ├── wecom_service.py # 企微回调处理
|
||||
│ │ │ ├── message_router.py # 消息路由 + 评分 + 举手检测
|
||||
│ │ │ ├── scoring_service.py # 紧急度评分引擎
|
||||
│ │ │ ├── session_service.py # 会话生命周期管理
|
||||
│ │ │ └── funny_phrase_service.py # 摇人趣味话术生成
|
||||
│ │ └── api/ # 8 个 API 路由模块
|
||||
│ └── tests/ # 116+ 个测试用例
|
||||
├── frontend-agent/ # 坐席工作台(Vue 3 + Element Plus)
|
||||
│ └── src/
|
||||
│ ├── views/ # LoginView + WorkspaceView
|
||||
│ ├── components/
|
||||
│ │ ├── TopBar/ # 顶部栏(主题切换 + 用户信息)
|
||||
│ │ ├── conversation/ # 会话列表 + 会话条目
|
||||
│ │ ├── chat/ # 聊天区 + 消息气泡 + 输入框
|
||||
│ │ ├── assistant/ # AI 推荐内联组件
|
||||
│ │ ├── troubleshooting/ # 排查步骤栏(FlowchartNode)
|
||||
│ │ ├── quickreply/ # 快速回复面板(三层导航)
|
||||
│ │ └── todo/ # 待办面板 + 任务详情视图
|
||||
│ ├── stores/ # Pinia Store(conversation/agent/quickReply/theme/todo)
|
||||
│ └── api/ # API 调用模块
|
||||
├── frontend-h5/ # 员工端 H5(Vue 3 + Vant)
|
||||
│ └── src/
|
||||
│ ├── views/ # ChatView
|
||||
│ └── components/ # ChatPanel + 摇人按钮 + AI助手
|
||||
├── nginx/ # Nginx 反向代理配置
|
||||
│ └── nginx.conf
|
||||
├── scripts/ # 部署和运维脚本
|
||||
│ ├── start_backend.bat # Windows 快速启动后端(相对路径)
|
||||
│ └── restart_backend.ps1 # Windows 重启后端(自动查找 PG/Redis/Python)
|
||||
└── docs/ # 项目文档(全部文档统一存放)
|
||||
├── PRD.md # 产品需求文档 v1.0
|
||||
├── PRD-v53-incremental.md # v5.3 增量需求
|
||||
├── ARCHITECTURE.md # 系统架构设计(合并版)
|
||||
├── 01-项目总览与部署手册.md # 管理者视角部署手册
|
||||
├── 开发交付概览.md # 开发交付状态总览
|
||||
├── 智能IT支持服务台-项目迁移文档.md # 工作区迁移记录
|
||||
├── testing/ # 测试报告目录
|
||||
│ └── QA_COMPREHENSIVE_REPORT.md # 综合 QA 报告
|
||||
├── diagrams/ # Mermaid 图表
|
||||
│ ├── 02-技术文档/技术架构/sequence-diagram-代办事项.mermaid
|
||||
│ ├── sequence-shake.mermaid
|
||||
│ ├── sequence-scoring.mermaid
|
||||
│ ├── sequence-polling.mermaid
|
||||
│ └── 02-技术文档/技术架构/class-diagram-代办事项.mermaid
|
||||
└── prototypes/ # 原型文件
|
||||
├── agent-workspace-v5_3.html # 当前锁定版本(v5.3)
|
||||
├── qr_data_full.json # 快速回复数据(180条)
|
||||
└── archive/ # 历史原型归档
|
||||
```
|
||||
|
||||
### Bug 修复清单(7 个)
|
||||
|
||||
| # | 文件 | 问题 | 修复 |
|
||||
|---|------|------|------|
|
||||
| 1 | `message_router.py` | `calculate_urgency()` 是 async 但未 `await` | 添加 `await` |
|
||||
| 2 | `app/main.py` | 中文引号 `""` 嵌入 Python 双引号字符串,SyntaxError | 转义引号 |
|
||||
| 3 | `wecom_callback.py` | `WecomCrypto` 模块级初始化,默认 AES Key 不合法导致 `binascii.Error` | 改为懒加载单例 `_get_wecom_crypto()` |
|
||||
| 4 | `tests/conftest.py` | `aioredis.from_url` mock 路径错误 | 修正为 `redis.asyncio.from_url` |
|
||||
| 5 | `tests/conftest.py` | `create_test_conversation()` 缺少 `is_pinned`/`is_todo` 参数 | 添加可选参数 |
|
||||
| 6 | `session_service.py` | `conversation_id` UUID 对象 vs String(36) 列类型不匹配 | 先转字符串再查询 |
|
||||
| 7 | `scoring_service.py` | 关键词大小写不敏感缺失 + `_check_vip` 缺短路 | `.lower()` + 短路返回 |
|
||||
|
||||
### 用户下一步操作
|
||||
|
||||
1. **(已验证)pytest 全量通过**:116/116 测试已在开发环境验证通过,本地无需再跑
|
||||
|
||||
2. **配置企微应用凭证**:
|
||||
- 复制 `.env.example` 为 `.env`
|
||||
- 填入企微应用的 CorpID、AgentID、Secret、Token、EncodingAESKey
|
||||
|
||||
3. **Docker Compose 启动**(需 PostgreSQL + Redis):
|
||||
```powershell
|
||||
cd C:\Users\simon\wecom_it_smart_desk
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
4. **前端开发启动**:
|
||||
```powershell
|
||||
# 坐席工作台
|
||||
cd frontend-agent && npm install && npm run dev
|
||||
# 用户端 H5
|
||||
cd frontend-h5 && npm install && npm run dev
|
||||
```
|
||||
|
||||
5. **企微回调配置**:在企微管理后台配置消息回调 URL 指向你的服务器
|
||||
|
||||
|
||||
## 九、附录
|
||||
|
||||
### 8.1 需要团队协助的事项
|
||||
|
||||
| # | 事项 | 需要谁 | 紧急度 |
|
||||
|---|------|--------|--------|
|
||||
| 1 | **服务器 SSH 账号**:用于 Docker 部署 | 运维 | 🔴 高(当前阻塞) |
|
||||
| 2 | **企微通讯录权限**:确认 API 权限(VIP 功能依赖) | 运维/企微管理员 | 中(M1 可用 mock) |
|
||||
| 3 | **千问/Dify/RAGFlow 环境**(M2 阶段) | 架构/开发 | 低(M2 前准备) |
|
||||
|
||||
### 8.2 项目文件索引
|
||||
|
||||
```
|
||||
wecom_it_smart_desk/
|
||||
├── README.md # 入口索引
|
||||
├── PRD.md # 产品需求文档
|
||||
├── docs/
|
||||
│ ├── 01-项目总览与部署手册.md # ← 本文档(运维/架构/管理者)
|
||||
│ ├── 02-技术架构与开发指南.md # 开发者文档
|
||||
│ └── 03-测试验证文档.md # 测试文档
|
||||
├── backend/ # FastAPI 后端
|
||||
├── frontend-agent/ # 坐席工作台前端
|
||||
├── frontend-h5/ # 员工 H5 前端
|
||||
├── nginx/nginx.conf # Nginx 反代配置
|
||||
├── docker-compose.yml # Docker Compose 编排
|
||||
├── .env.production # 生产环境变量模板
|
||||
└── scripts/ # 部署/构建脚本
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> 本文档合并自原 `docs/团队沟通文档-架构消息知识库.md`、`docs/正式环境独立部署架构方案.md`、`docs/DEPLOY_NAS.md`。详细技术规格见 `docs/02-技术架构与开发指南.md`。
|
||||
@@ -0,0 +1,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/auth/otp-status` | GET | 查询绑定状态 |
|
||||
| `/api/auth/otp-bind` | POST | 开始绑定(返回 secret + QR) |
|
||||
| `/api/auth/otp-verify` | POST | 确认绑定 |
|
||||
| `/api/auth/otp-verify` | POST | 验证 OTP |
|
||||
| `/api/auth/otp-unbind` | 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,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,131 @@
|
||||
# OTP 二次验证实现文档
|
||||
|
||||
> **关联文档(认证模块三件套)**:本文档为运维侧实现说明,与以下文档形成闭环 ——
|
||||
> - PRD:`../../01-产品文档/01-认证与登录/PRD-REQ-认证-统一认证与登录-v1.1.md`
|
||||
> - 技术方案:`../../02-技术文档/技术方案-REQ-认证-统一认证与登录-v1.0.md`
|
||||
> - 测试用例:`../../03-测试文档/03-功能测试用例/TC-REQ-认证-统一认证与登录-v1.0.md`
|
||||
>
|
||||
> **版本对齐说明(2026-08-05 修正)**:本文档原始内容描述的是 **v0.5.6 时期的 `otp_*` 旧设计**(`/api/agents/otp-*` + `otp_secret`/`otp_enabled` 字段)。该设计已在 **AUTH-03 三端认证重构(v0.7.x)** 中被统一取代:
|
||||
> - 端点前缀由 `/api/agents/otp-*` 改为 **`/api/auth/otp-*`**(路由文件 `app/api/otp.py`,prefix=`/auth`);
|
||||
> - 模型字段由 `otp_secret`/`otp_enabled` 改为 **`mfa_secret`/`mfa_enabled`/`mfa_bound_at`/`mfa_last_verified_at`**(migration 023 新增、026 删除旧 `otp_*` 字段);
|
||||
> - 旧 `otp_secret`/`otp_enabled` 字段已于 **migration 026(v0.7.1)** 彻底删除。
|
||||
>
|
||||
> 本次修正已将上述内容对齐到**当前生产实现**(以 `app/api/otp.py` + `app/api/agents.py` 为准)。注意:部署指南 `07-扫码登录OTP部署指南-v0.7.0.md` 与测试用例仍引用更早的 `/api/mfa/*` 规划命名,同样需后续对齐到 `/api/auth/otp-*`(见技术方案 §2.3)。
|
||||
|
||||
## 功能概述
|
||||
|
||||
为 IT 支持服务台坐席端增加 OTP 二次验证(MFA)功能:
|
||||
- admin 角色登录时需要输入 Google Authenticator / 兼容 TOTP 应用的动态码
|
||||
- 首次登录且未绑定时,引导完成首次绑定(不签发 token)
|
||||
- OTP 丢失后由管理员重置
|
||||
|
||||
## 实现方案
|
||||
|
||||
### 1. 后端修改
|
||||
|
||||
#### 1.1 安装依赖
|
||||
```bash
|
||||
pip install pyotp qrcode[pil] pillow
|
||||
```
|
||||
|
||||
#### 1.2 数据库模型
|
||||
`agents` 表当前 MFA 相关字段(见 `app/models/agent.py`,由 migration 023 新增):
|
||||
- `mfa_secret`: TOTP 共享密钥(Base32 编码,绑定时生成,验证启用前不算启用)
|
||||
- `mfa_enabled`: MFA 是否启用(默认 false,首次验证成功后置 true)
|
||||
- `mfa_bound_at`: 首次绑定完成时间(可空,用于审计与回收策略)
|
||||
- `mfa_last_verified_at`: 最近一次验证成功时间(可空,安全审计用)
|
||||
|
||||
> ⚠️ 历史字段 `otp_secret` / `otp_enabled` 已在 **migration 026(v0.7.1)** 删除,代码中不再存在,请勿沿用。
|
||||
|
||||
#### 1.3 Schema 修改
|
||||
文件:`backend/app/schemas/agent.py`
|
||||
- `AgentLogin`:含 `otp_code: Optional[str]` 字段(admin 角色登录必填,6 位数字 TOTP 动态码)
|
||||
- `AgentResponse`:返回绑定状态字段(注:response schema 中字段名仍写作 `otp_enabled`,属于已知技术债,与模型 `mfa_enabled` 对应,不影响端点行为;详见技术方案 §2.3)
|
||||
|
||||
#### 1.4 API 修改
|
||||
|
||||
**OTP 管理端点**(文件:`backend/app/api/otp.py`,路由器 `prefix="/auth"`,tags=["OTP二次认证"]):
|
||||
|
||||
| 方法 | 端点 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/auth/otp-status` | 查询当前用户 MFA 绑定状态 |
|
||||
| POST | `/api/auth/otp-bind` | 生成 TOTP secret + 二维码(写入 `mfa_secret`,`mfa_enabled` 保持 false) |
|
||||
| POST | `/api/auth/otp-verify` | 验证动态码:首次绑定(mfa_enabled=False 有 secret)→ 启用 MFA 并签发 token;已绑定 → 常规验证并写 Redis 30 分钟标记 |
|
||||
| POST | `/api/auth/otp-unbind` | 用户主动解绑,清空 `mfa_secret`/`mfa_enabled`/`mfa_bound_at` |
|
||||
| POST | `/api/auth/otp-admin-reset/{employee_id}` | 管理员重置指定员工 MFA(清空其 `mfa_*` 字段) |
|
||||
| GET | `/api/auth/otp-admin-users` | 管理员查看全部坐席 MFA 绑定状态列表 |
|
||||
|
||||
**登录接口 MFA 校验**(文件:`backend/app/api/agents.py`,函数 `agent_login`):
|
||||
- `mfa_enabled=True` 且未提供 `otp_code` → 返回 `require_otp: True`,前端弹 OTP 输入框
|
||||
- `mfa_enabled=False`(未绑定)→ 返回 `require_otp_bind: True`,引导首次绑定流程(不签发 token)
|
||||
- 提供正确 `otp_code` → 经 `MFAService.verify_code(agent.mfa_secret, otp_code)` 校验通过后签发 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` / `require_otp_bind` 标记,驱动页面分流
|
||||
|
||||
#### 2.3 坐席端登录页面
|
||||
文件:`frontend-agent/src/views/Login.vue`
|
||||
- 增加 OTP 输入框(`v-if="requireOtp"`)
|
||||
- 首次登录返回 `require_otp_bind` 时引导跳转到绑定流程
|
||||
|
||||
#### 2.4 坐席端绑定页面(新增)
|
||||
文件:`frontend-agent/src/views/MfaBind.vue`
|
||||
- 展示二维码 + 密钥,输入动态码完成首次绑定
|
||||
|
||||
#### 2.5 高危操作 OTP 守卫(新增)
|
||||
文件:`frontend-agent/src/composables/useHighRiskOtp.ts`
|
||||
- 调用高危操作前若 30 分钟内未验证过 OTP,弹 OTP 输入框 → 调 `/api/auth/otp-verify` → 重试
|
||||
|
||||
#### 2.6 管理端 MFA 管理(新增)
|
||||
文件:`frontend-admin/src/views/MfaManage.vue`
|
||||
- 管理员查看坐席 MFA 绑定状态、重置指定员工 MFA
|
||||
|
||||
## 使用流程
|
||||
|
||||
### 首次绑定 OTP
|
||||
1. 管理员登录坐席端(此时 `mfa_enabled=False`,返回 `require_otp_bind`)
|
||||
2. 跳转绑定页,调用 `POST /api/auth/otp-bind` 获取二维码与密钥
|
||||
3. 使用 Google Authenticator / 兼容 TOTP 应用扫描二维码
|
||||
4. 调用 `POST /api/auth/otp-verify` 输入动态码验证
|
||||
5. 验证成功,`mfa_enabled` 置 1、`mfa_bound_at` 写入,直接签发 token
|
||||
|
||||
### 登录流程
|
||||
1. 用户输入 user_id 和 name
|
||||
2. 后端检查 `mfa_enabled`
|
||||
3. 已绑定(`mfa_enabled=True`)且无 `otp_code` → 返回 `require_otp: True`
|
||||
4. 前端显示 OTP 输入框
|
||||
5. 用户输入 6 位动态码,再次登录
|
||||
6. 后端验证通过,生成 token
|
||||
|
||||
### 解绑流程
|
||||
1. 管理员 / 用户调用 `POST /api/auth/otp-unbind`
|
||||
2. `mfa_secret` / `mfa_enabled` / `mfa_bound_at` 清空(`mfa_last_verified_at` 保留为审计记录)
|
||||
|
||||
### 管理员重置
|
||||
1. 管理员调用 `POST /api/auth/otp-admin-reset/{employee_id}`
|
||||
2. 目标员工的 `mfa_*` 字段清空,下次登录将重新引导绑定
|
||||
|
||||
## 错误码
|
||||
|
||||
| 错误码 | 说明 | 当前状态 |
|
||||
|--------|------|----------|
|
||||
| 1006 | OTP 验证码错误 | ✅ 仍在使用(`agents.py` 登录校验 `mfa_enabled=True` 分支) |
|
||||
| 1007 | OTP 绑定失败 | ⚠️ 遗留数字码,原始 `otp` 设计;当前 `otp.py` 改用 `success_response` / `ErrorCode` 枚举(`E-prefix`),建议以代码为准复核 |
|
||||
| 1008 | 请先绑定 OTP | ⚠️ 同上(当前由 `require_otp_bind: True` 表达,非错误码) |
|
||||
| 1009 | OTP 验证失败 | ⚠️ 同上 |
|
||||
| 1010 | OTP 解绑失败 | ⚠️ 同上 |
|
||||
|
||||
> 说明:当前错误码主体系为 `E{模块}{序号}` 枚举(见 `app/utils/error_codes.py`,如 `E1001` 认证失败)。上表数字码为 v0.5.6 遗留,仅 `1006` 在登录流程中仍被 `AppException(1006, ...)` 直接抛出;`1007`–`1010` 应结合 `otp.py` 实际返回复核。
|
||||
|
||||
---
|
||||
|
||||
**变更历史**:
|
||||
- 2026-08-05 修正端点命名与字段体系,对齐 AUTH-03 生产实现(`/api/auth/otp-*` + `mfa_*` 字段),移除与 `/api/mfa/*` 的过时对照
|
||||
@@ -0,0 +1,225 @@
|
||||
# 部署手册:扫码登录 + OTP 二次认证(Phase 1+2)
|
||||
|
||||
> 创建:2026-06-21
|
||||
> 适用版本:v0.7.0+ (Phase 1+2)
|
||||
> 部署顺序:后端 → 前端 4 端 → nginx → 数据库 migration → 验收
|
||||
>
|
||||
> ⚠️ **端点命名修正(2026-08-05)**:本文档原始使用 `/api/mfa/*` 与 `/api/admin/mfa/reset/{employee_id}`,该命名为 AUTH-03 三端认证重构前的规划命名。当前生产实现已统一为 **`/api/auth/otp-*`**(含 `/api/auth/otp-admin-reset/{employee_id}`),详见 `06-OTP二次验证实现.md` 与技术方案。下方正文已同步修正。
|
||||
|
||||
---
|
||||
|
||||
## 🎯 部署目标
|
||||
|
||||
从 v0.6.x 的"企微 OAuth + SMS 2FA"升级到 v0.7.0 的"扫码登录 + OTP TOTP + SMS 备用"。
|
||||
|
||||
涉及后端变更:
|
||||
- 新增 `/api/auth_qrcode/*` 4 个端点(扫码登录)
|
||||
- 新增 `/api/auth/otp-*` 6 个端点(OTP 二次认证)
|
||||
- 新增 `/api/auth/otp-admin-reset/{employee_id}`(管理员重置)
|
||||
- 新增 `/api/admin/high-risk/*` 演示端点 + require_high_risk_otp 守卫
|
||||
- 新增 4 个数据库字段: `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
|
||||
|
||||
---
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- [PRD-REQ-认证-统一认证与登录-v1.1.md](../../01-产品文档/01-认证与登录/PRD-REQ-认证-统一认证与登录-v1.1.md) — 认证模块产品需求
|
||||
- [技术方案-REQ-认证-统一认证与登录-v1.0.md](../../02-技术文档/技术方案-REQ-认证-统一认证与登录-v1.0.md) — 认证模块技术方案
|
||||
- [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 部署手册)
|
||||
- 2026-08-05 端点命名修正:`/api/mfa/*` → `/api/auth/otp-*`、`/api/admin/mfa/reset/{employee_id}` → `/api/auth/otp-admin-reset/{employee_id}`(AUTH-03 重构后的真实实现);数据库字段描述由"2 个"更正为"4 个"
|
||||
@@ -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,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/auth/otp-status
|
||||
# 期望:401 Unauthorized
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🟡 部署后 必做(用户/QA 验收)
|
||||
|
||||
按 `docs/03-测试文档/testing-测试/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,88 @@
|
||||
# 堡垒机运维工具 (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
|
||||
# 通过 elFinder Web UI 上传到目标服务器
|
||||
python scripts/jms_ops.py upload 本地文件.conf /tmp/远程路径.conf
|
||||
```
|
||||
|
||||
### 4. 文件下载
|
||||
|
||||
```bash
|
||||
# 通过 base64 通道从目标服务器下载
|
||||
python scripts/jms_ops.py download /远程路径.conf ./本地文件.conf
|
||||
```
|
||||
|
||||
## 方案选择依据
|
||||
|
||||
| 需求 | 推荐方案 | 速度 |
|
||||
|------|----------|------|
|
||||
| 执行命令获取文本结果 | v16 REST API + plink PTY | 首次 ~13s,复用 ~2-3s |
|
||||
| 执行命令看界面效果 | v10 Web CLI(截图) | ~60s |
|
||||
| 文件上传 (< 100KB) | base64 通道 | 快 |
|
||||
| 文件上传 (>= 100KB 或 > 15s) | elFinder Web UI | 稳定 |
|
||||
| 文件下载 | base64 通道 | - |
|
||||
|
||||
## 目标服务器配置
|
||||
|
||||
当前预设目标:`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)
|
||||
@@ -0,0 +1,101 @@
|
||||
# 堡垒机运维工具 (jumpserver-V2)
|
||||
|
||||
## 概述
|
||||
|
||||
通过 JumpServer 堡垒机自动化执行远程命令、文件上传下载。统一入口为 `v2_ops.py`,支持多种操作模式。
|
||||
|
||||
> **重要**:jumpserver-ops(旧版)已下线,统一使用 jumpserver-V2。
|
||||
|
||||
## 连接方式决策
|
||||
|
||||
| 场景 | 连接方式 |
|
||||
|------|----------|
|
||||
| **默认** | 通过堡垒机跳转(大多数内网服务器) |
|
||||
| **例外** | 直连(NAS、开发机等)需单独配置 |
|
||||
|
||||
> **规则**:默认都需要通过堡垒机,除非明确告知某台服务器是直连。
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 首次登录
|
||||
|
||||
```bash
|
||||
# 首次使用:弹出浏览器,手动填写 OTP
|
||||
cd C:\Users\simon\.workbuddy\skills\jumpserver-V2\scripts
|
||||
python v2_ops.py login
|
||||
```
|
||||
|
||||
### 2. 远程命令执行(推荐)
|
||||
|
||||
```bash
|
||||
# 单命令(免登录,复用缓存)
|
||||
python v2_ops.py exec "docker ps"
|
||||
|
||||
# 多命令串行
|
||||
python v2_ops.py exec "hostname && uptime && docker ps"
|
||||
|
||||
# 批量命令文件
|
||||
python v2_ops.py batch commands.txt
|
||||
```
|
||||
|
||||
### 3. 文件上传
|
||||
|
||||
```bash
|
||||
# 上传到服务器 /tmp 目录
|
||||
python v2_ops.py upload 本地文件.conf /tmp/远程路径.conf
|
||||
```
|
||||
|
||||
### 4. 文件下载
|
||||
|
||||
```bash
|
||||
# 从服务器 /tmp 目录下载
|
||||
python v2_ops.py download /远程路径.conf ./本地文件.conf
|
||||
```
|
||||
|
||||
### 5. 巡检(只读)
|
||||
|
||||
```bash
|
||||
# 只读巡检:磁盘/内存/负载/CPU/容器/会话
|
||||
python v2_ops.py inspect
|
||||
```
|
||||
|
||||
## 方案选择依据
|
||||
|
||||
| 需求 | 推荐方案 | 速度 |
|
||||
|------|----------|------|
|
||||
| 执行命令获取文本结果 | V2 REST API + plink PTY | 首次 ~13s,复用 ~2-3s |
|
||||
| 执行命令看界面效果 | Web CLI(截图) | ~60s |
|
||||
| 文件上传 (< 100KB) | psftp 通道 | 快 |
|
||||
| 文件上传 (>= 100KB) | psftp 通道 | 稳定 |
|
||||
| 文件下载 | psftp 通道 | - |
|
||||
| 只读巡检 | inspect 子命令 | - |
|
||||
|
||||
## 目标服务器配置
|
||||
|
||||
当前预设目标:`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 中显示一致 |
|
||||
| 401 错误 | 缓存失效,自动回退浏览器重新登录 |
|
||||
|
||||
## 脚本位置
|
||||
|
||||
```
|
||||
C:\Users\simon\.workbuddy\skills\jumpserver-V2\scripts\v2_ops.py
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [服务器部署手册](./服务器部署手册.md)
|
||||
- [版本更新说明](./05-版本更新说明-v1.1.0-20260614.md)
|
||||
@@ -0,0 +1,320 @@
|
||||
# 企微智能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 安装 Docker(CentOS/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 安装 Docker(Ubuntu/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
|
||||
```
|
||||
|
||||
### 4.3 部署铁律(2026-07-17 新增,踩坑记录)
|
||||
|
||||
以下 3 条均为生产事故复盘得出的硬性规则,部署/变更时必须遵守:
|
||||
|
||||
#### 铁律 1:backend 必须 `--workers 1`(WS 推送单 worker 约束)
|
||||
|
||||
`ws_manager` 是进程内单例,AI 后台任务与员工 WebSocket 连接若落在不同 worker 进程,`broadcast_to_employees` 会**静默丢失约 50% 消息**。
|
||||
|
||||
```bash
|
||||
# ❌ 危险:docker-compose-override.yml 会自动合并覆盖主文件!
|
||||
# 若 override 中存在 --workers 2,主文件的 --workers 1 会被覆盖
|
||||
docker compose -f docker-compose.yml -f docker-compose-override.yml config | grep workers
|
||||
# 必须输出 1。如为 2,删除或修改 override 文件
|
||||
```
|
||||
|
||||
#### 铁律 2:`.env` 变量不会自动传入容器,必须在 `environment:` 显式声明
|
||||
|
||||
`backend/.dockerignore` 排除了全部 `.env` 文件,生产容器配置 **100% 来自 docker-compose.yml 的 `environment:` 部分**。新增任何环境变量(尤其是 `DIFY_NATIVE_BASE_URL`、`DIFY_NATIVE_API_KEY`),必须:
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
backend:
|
||||
environment:
|
||||
- DIFY_NATIVE_BASE_URL=${DIFY_NATIVE_BASE_URL:-}
|
||||
- DIFY_NATIVE_API_KEY=${DIFY_NATIVE_API_KEY:-}
|
||||
```
|
||||
|
||||
验证:容器内执行 `env | grep DIFY_NATIVE` 非空;后端日志出现「调用 Dify 原生 API」而非「回退到代理路径」。
|
||||
(事故记录:2026-07-13 两次因未声明导致 P0 故障;`WECOM_SSO_CALLBACK_BASE` 未声明导致扫码登录崩溃)
|
||||
|
||||
#### 铁律 3:Redis 密码含特殊字符必须 URL 编码
|
||||
|
||||
`REDIS_URL` 中密码含 `@ # !` 时未编码 → 解析出错误的 host → 连接挂起 → 登录 502。编码规则:`@→%40`、`#→%23`、`!→%21`。改密码时须同步更新 compose 中 `requirepass` 与 `REDIS_URL` 两处。
|
||||
|
||||
---
|
||||
|
||||
## 五、启动服务
|
||||
|
||||
### 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,447 @@
|
||||
# 部署运维说明 — H5 智能推荐重构(v1.0)
|
||||
|
||||
> **REQ 编号**: REQ-用户-006
|
||||
> **部署日期**: 2026-07-28(首次部署)
|
||||
> **版本**: v1.0
|
||||
> **关联文档**:
|
||||
> - **PRD(冻结)**:`docs/01-产品文档/05-用户端H5/PRD-REQ-用户-006-智能推荐重构-v1.0-Frozen.md`
|
||||
> - **技术方案**:`docs/02-技术文档/技术架构/技术方案-REQ-用户-006-智能推荐重构-v1.0.md`
|
||||
> - **任务说明书**:`docs/07-项目管理/任务说明书/任务说明书-REQ-用户-006-智能推荐重构.md`
|
||||
> - **原型图**:`docs/01-产品文档/05-用户端H5/原型-REQ-用户-006-智能推荐重构-v1.0.html`
|
||||
> - **测试用例**:`docs/03-测试文档/03-功能测试用例/TC-用户-006-智能推荐重构.md`
|
||||
> - **部署总手册**:`docs/04-运维文档/部署运维/DEPLOY-GUIDE.md`
|
||||
|
||||
---
|
||||
|
||||
## 一、变更摘要
|
||||
|
||||
| 维度 | 变更 |
|
||||
|------|------|
|
||||
| **后端新增** | `app/services/recommend_progress_service.py`(T2 webhook + 60s 轮询) |
|
||||
| **后端新增** | `app/services/topic_detector.py`(Jaccard 相似度) |
|
||||
| **后端新增** | `app/api/recommend.py`(REST API:进度查询 + webhook 接收) |
|
||||
| **后端重构** | `app/services/asset_recommend_service.py`(merge_recommends + 同源抑制 + 中文匹配) |
|
||||
| **后端重构** | `app/config/assets.yaml`(中文 role key + 同义词表 + 排除关键词) |
|
||||
| **后端扩展** | `app/services/employee_profile_service.py`(DB 缓存画像降级) |
|
||||
| **后端集成** | `app/tasks/h5_ai_task.py::_step_assets`(3s 超时 + 降级链路) |
|
||||
| **后端集成** | `app/tasks/h5_ai_task.py::_step_persist`(source/trigger_timing/layer 标识) |
|
||||
| **数据库** | 新增 `recommend_progress` + `recommend_event` 双表 + 索引 |
|
||||
| **前端新增** | `src/frontend-h5/src/stores/recommendStore.ts`(Pinia 状态机 + localStorage) |
|
||||
| **前端新增** | `src/frontend-h5/src/composables/useRecommendWs.ts`(WS 接收处理) |
|
||||
| **前端重构** | `src/frontend-h5/src/components/assistant/DynamicRecommend.vue`(无标题 + FIFO + 4 类卡片) |
|
||||
| **前端适配** | `src/frontend-h5/src/components/assistant/RightPanel.vue`(引用 store) |
|
||||
| **nginx** | **无改动** |
|
||||
|
||||
---
|
||||
|
||||
## 二、部署前 Checklist
|
||||
|
||||
### 2.1 文档就位(5 件套)
|
||||
|
||||
- [x] PRD v1.0-Frozen(含 §4.7 冻结声明 + §13 扩展计划)
|
||||
- [x] 技术方案 v1.0(19 章节)
|
||||
- [x] 任务说明书 v1.0(8 阶段 WBS)
|
||||
- [x] 原型图 v1.0(7 场景)
|
||||
- [x] 测试用例 v1.0(76 条用例)
|
||||
|
||||
### 2.2 代码就位
|
||||
|
||||
- [ ] 中文路径 `D:\资料\03-项目开发\wecom_it_smart_desk\src\` 已改
|
||||
- [ ] ASCII 路径 `D:\dev\wecom\src\` 已同步改(**多路径同步铁律**)
|
||||
- [ ] 后端 AST 静态校验通过(`python -c "import ast; ast.parse(open(f).read())"`)
|
||||
- [ ] 前端 `npm run build` 通过(无 TS 报错)
|
||||
- [ ] 单元测试通过:`pytest src/backend/tests/services/test_asset_recommend_v2.py -v`
|
||||
- [ ] 单元测试通过:`pytest src/backend/tests/services/test_recommend_progress.py -v`
|
||||
- [ ] 单元测试通过:`pytest src/backend/tests/services/test_topic_detector.py -v`
|
||||
- [ ] 前端单元测试通过:`vitest run recommendStore.test.ts`
|
||||
|
||||
### 2.3 数据库迁移准备
|
||||
|
||||
- [ ] Alembic 迁移脚本就位:`alembic/versions/{revision}_add_recommend_progress.py`
|
||||
- [ ] Alembic 迁移脚本就位:`alembic/versions/{revision}_add_recommend_event.py`
|
||||
- [ ] Init SQL 应急脚本就位(容器内 alembic 失败时备用):
|
||||
- `scripts/init_recommend_tables.sql`(含 `ON CONFLICT DO NOTHING` 幂等)
|
||||
- [ ] 迁移在 staging 环境 dry-run 通过
|
||||
|
||||
### 2.4 部署包就位
|
||||
|
||||
- [ ] 后端部署包:`backend-recommend-v1.0.zip`(含 17 项代码 + Init SQL)
|
||||
- [ ] 前端部署包:`frontend-h5-recommend-v1.0.zip`(含 dist 完整产物)
|
||||
- [ ] MD5 校验通过
|
||||
|
||||
### 2.5 灰度策略已对齐
|
||||
|
||||
- [ ] 运维确认灰度名单(10 → 100 → 500 人)
|
||||
- [ ] 数据监控大盘已配置(点击率、自助解决率、降级次数)
|
||||
- [ ] 应急沟通群已建(产品 + 工程 + 运维)
|
||||
|
||||
---
|
||||
|
||||
## 三、部署顺序
|
||||
|
||||
> **铁律**:后端 → 前端 → nginx(如有改动)→ DB 迁移 → 端到端验收
|
||||
|
||||
### 3.1 后端部署
|
||||
|
||||
#### 3.1.1 压缩后端代码
|
||||
|
||||
```powershell
|
||||
# 在中文路径下打包
|
||||
Compress-Archive -Path "D:\资料\03-项目开发\wecom_it_smart_desk\src\backend\app\*" -DestinationPath "D:\资料\03-项目开发\wecom_it_smart_desk\backend-recommend-v1.0.zip" -Force
|
||||
# ASCII 副本也打包(防止源路径含中文导致的 GBK 误读)
|
||||
Compress-Archive -Path "D:\dev\wecom\src\backend\app\*" -DestinationPath "D:\dev\wecom\backend-recommend-v1.0.zip" -Force
|
||||
```
|
||||
|
||||
#### 3.1.2 通过 jumpserver-V2 上传到 /tmp
|
||||
|
||||
```powershell
|
||||
# 使用 v2_ops.py(PowerShell 工具)
|
||||
& "C:\Users\simon\.workbuddy\binaries\python\versions\3.13.12\python.exe" "C:\Users\simon\.workbuddy\skills\jumpserver-V2\scripts\v2_ops.py" upload "D:\dev\wecom\backend-recommend-v1.0.zip" "backend-recommend-v1.0.zip"
|
||||
```
|
||||
|
||||
#### 3.1.3 服务器解压到挂载源路径
|
||||
|
||||
```bash
|
||||
# ⚠️ 必须用绝对路径(batch 模式 cd 不生效)
|
||||
unzip -o /tmp/backend-recommend-v1.0.zip -d /tmp/backend-recommend-extract
|
||||
# 备份旧代码
|
||||
cp -r /opt/wecom-it-desk/app/app /tmp/app_bak_$(date +%Y%m%d_%H%M%S)
|
||||
# 拷贝新代码(保留其他文件,只覆盖改动部分)
|
||||
cp -rf /tmp/backend-recommend-extract/app/* /opt/wecom-it-desk/app/app/
|
||||
```
|
||||
|
||||
#### 3.1.4 数据库迁移(Alembic 双轨)
|
||||
|
||||
**主路径**:Alembic 升级
|
||||
|
||||
```bash
|
||||
# 容器内执行 alembic
|
||||
docker exec -it wecom_it_backend alembic upgrade head
|
||||
```
|
||||
|
||||
**应急路径**:手动执行 Init SQL(如果 alembic 失败)
|
||||
|
||||
```bash
|
||||
# 上传 Init SQL
|
||||
& "C:\Users\simon\.workbuddy\binaries\python\versions\3.13.12\python.exe" "C:\Users\simon\.workbuddy\skills\jumpserver-V2\scripts\v2_ops.py" upload "scripts\init_recommend_tables.sql" "init_recommend_tables.sql"
|
||||
|
||||
# 服务器执行
|
||||
docker exec -i wecom_it_postgres psql -U postgres -d wecom_it_desk < /tmp/init_recommend_tables.sql
|
||||
```
|
||||
|
||||
#### 3.1.5 验证后端表创建
|
||||
|
||||
```bash
|
||||
docker exec -i wecom_it_postgres psql -U postgres -d wecom_it_desk -c "\d recommend_progress"
|
||||
docker exec -i wecom_it_postgres psql -U postgres -d wecom_it_desk -c "\d recommend_event"
|
||||
```
|
||||
|
||||
#### 3.1.6 重启后端(必须 --workers 1)
|
||||
|
||||
```bash
|
||||
# ⚠️ ws_manager 是进程内单例,多 worker 会导致 WS 消息丢失(~50%)
|
||||
docker compose restart backend
|
||||
# 验证 healthy
|
||||
docker ps | grep wecom_it_backend
|
||||
```
|
||||
|
||||
#### 3.1.7 后端日志验证
|
||||
|
||||
```bash
|
||||
# 应该看到资产推荐 + recommend_progress 启动日志
|
||||
docker logs wecom_it_backend --tail 100 | grep -E "AssetRecommend|recommend_progress|Loaded.*recommend"
|
||||
```
|
||||
|
||||
### 3.2 前端部署
|
||||
|
||||
#### 3.2.1 本地构建(必须在 ASCII 路径,pnpm 不卡死)
|
||||
|
||||
```bash
|
||||
cd D:\dev\wecom\src\frontend-h5
|
||||
npm run build
|
||||
# 验证 dist 产物含特征字符串
|
||||
grep -r "recommendStore" dist/assets/ | head -5
|
||||
grep -r "layer-progress" dist/assets/ | head -5
|
||||
```
|
||||
|
||||
#### 3.2.2 压缩 dist
|
||||
|
||||
```powershell
|
||||
Compress-Archive -Path "D:\dev\wecom\src\frontend-h5\dist\*" -DestinationPath "D:\dev\wecom\frontend-h5-recommend-v1.0.zip" -Force
|
||||
```
|
||||
|
||||
#### 3.2.3 上传 + 解压
|
||||
|
||||
```powershell
|
||||
& "C:\Users\simon\.workbuddy\binaries\python\versions\3.13.12\python.exe" "C:\Users\simon\.workbuddy\skills\jumpserver-V2\scripts\v2_ops.py" upload "D:\dev\wecom\frontend-h5-recommend-v1.0.zip" "frontend-h5-recommend-v1.0.zip"
|
||||
```
|
||||
|
||||
```bash
|
||||
# 服务器解压
|
||||
unzip -o /tmp/frontend-h5-recommend-v1.0.zip -d /tmp/frontend-h5-extract
|
||||
rm -rf /opt/wecom-it-desk/frontend-h5/dist
|
||||
mv /tmp/frontend-h5-extract /opt/wecom-it-desk/frontend-h5/dist
|
||||
```
|
||||
|
||||
#### 3.2.4 重启 nginx(必须!bind mount 不会自动刷新新文件)
|
||||
|
||||
```bash
|
||||
docker restart wecom_it_nginx
|
||||
```
|
||||
|
||||
### 3.3 nginx 配置(无改动)
|
||||
|
||||
本次部署 **无 nginx 配置变更**。
|
||||
|
||||
---
|
||||
|
||||
## 四、端到端验证
|
||||
|
||||
### 4.1 API 验证
|
||||
|
||||
```bash
|
||||
# 1. 后端健康检查
|
||||
curl -I http://localhost:8000/health
|
||||
|
||||
# 2. 推荐进度 API(如果有进行中的审批)
|
||||
curl -H 'X-Forwarded-For: 10.240.1.100' http://localhost:8000/recommend/progress/test_approval_id
|
||||
|
||||
# 3. WS 端口可达
|
||||
curl -I http://localhost:8000/ws/employee/{employee_id}
|
||||
```
|
||||
|
||||
### 4.2 数据库验证
|
||||
|
||||
```bash
|
||||
# 1. 表存在
|
||||
docker exec -i wecom_it_postgres psql -U postgres -d wecom_it_desk -c "\dt recommend_*"
|
||||
|
||||
# 2. 索引存在
|
||||
docker exec -i wecom_it_postgres psql -U postgres -d wecom_it_desk -c "\di idx_recommend_*"
|
||||
|
||||
# 3. recommend_progress 表能 INSERT
|
||||
docker exec -i wecom_it_postgres psql -U postgres -d wecom_it_desk -c "INSERT INTO recommend_progress (recommend_id, employee_id, approval_id, approval_type, status) VALUES ('rec_test', 'test_emp', 'test_appr', 'vpn_access', 'pending') RETURNING id;"
|
||||
```
|
||||
|
||||
### 4.3 前端验证(agent-browser 自动 + 用户人工)
|
||||
|
||||
```bash
|
||||
# agent-browser 自动验证
|
||||
# 1. 打开 H5 员工端
|
||||
# 2. 进会话但不发言 → 验证右侧栏完全空白(决策 ① A)
|
||||
# 3. 发"VPN 申请" → 验证左侧气泡 + 右侧栏操作卡
|
||||
# 4. 切换话题 → 验证 L1 清空
|
||||
# 5. 关闭浏览器 → 重新打开 → 验证 L2/L3/progress 持久化
|
||||
```
|
||||
|
||||
### 4.4 用户人工验证(必做,企微扫码限制)
|
||||
|
||||
- [ ] 登录 H5 员工端(需企微扫码)
|
||||
- [ ] 进入新会话,不发言,观察右侧栏空白
|
||||
- [ ] 发"VPN 怎么连",观察左侧气泡 + 右侧栏操作卡
|
||||
- [ ] 切换话题到"会议室",观察 L1 清空
|
||||
- [ ] 触发审批,观察进度卡回流
|
||||
- [ ] 关闭浏览器重新打开,观察持久化卡
|
||||
|
||||
---
|
||||
|
||||
## 五、灰度策略
|
||||
|
||||
### 5.1 4 阶段灰度
|
||||
|
||||
| 阶段 | 规模 | 持续时间 | 通过条件 | 不达标处理 |
|
||||
|------|------|---------|---------|----------|
|
||||
| **1%** | 10 人(运维 + 产品 + 工程) | 1 天 | 无 P0/P1 错误 | 立即回滚 |
|
||||
| **10%** | 100 人(早期种子用户) | 2 天 | 点击率 > 5%(无 P0/P1 错误) | 暂停灰度排查 |
|
||||
| **50%** | 500 人(半个部门) | 3 天 | 点击率 > 10% | 暂停灰度排查 |
|
||||
| **100%** | 全量(约 7000 人) | 长期 | 点击率 > 15%,自助解决率 > 10% | 长期监控优化 |
|
||||
|
||||
### 5.2 数据埋点验证(关键指标)
|
||||
|
||||
通过 `recommend_event` 表统计:
|
||||
|
||||
```sql
|
||||
-- 各 layer 推荐曝光数
|
||||
SELECT layer, source, COUNT(*) AS exposure_count
|
||||
FROM recommend_event
|
||||
WHERE event_type = 'shown'
|
||||
AND created_at > NOW() - INTERVAL '24 hours'
|
||||
GROUP BY layer, source;
|
||||
|
||||
-- 各 layer 推荐点击数
|
||||
SELECT layer, source, COUNT(*) AS click_count
|
||||
FROM recommend_event
|
||||
WHERE event_type = 'clicked'
|
||||
AND created_at > NOW() - INTERVAL '24 hours'
|
||||
GROUP BY layer, source;
|
||||
|
||||
-- 点击率
|
||||
SELECT
|
||||
shown.layer,
|
||||
shown.source,
|
||||
shown.exposure_count,
|
||||
COALESCE(clicked.click_count, 0) AS click_count,
|
||||
ROUND(100.0 * COALESCE(clicked.click_count, 0) / shown.exposure_count, 2) AS click_rate
|
||||
FROM (
|
||||
SELECT layer, source, COUNT(*) AS exposure_count
|
||||
FROM recommend_event
|
||||
WHERE event_type = 'shown'
|
||||
GROUP BY layer, source
|
||||
) shown
|
||||
LEFT JOIN (
|
||||
SELECT layer, source, COUNT(*) AS click_count
|
||||
FROM recommend_event
|
||||
WHERE event_type = 'clicked'
|
||||
GROUP BY layer, source
|
||||
) clicked ON shown.layer = clicked.layer AND shown.source = clicked.source;
|
||||
```
|
||||
|
||||
### 5.3 降级监控
|
||||
|
||||
```sql
|
||||
-- 降级触发次数(C → D / 全部失败)
|
||||
SELECT
|
||||
DATE(created_at) AS date,
|
||||
extra->>'degradation_path' AS degradation,
|
||||
COUNT(*) AS count
|
||||
FROM recommend_event
|
||||
WHERE event_type = 'degraded'
|
||||
AND created_at > NOW() - INTERVAL '7 days'
|
||||
GROUP BY date, degradation
|
||||
ORDER BY date DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、回滚预案
|
||||
|
||||
### 6.1 触发条件(任一)
|
||||
|
||||
- P0/P1 错误率 > 5%
|
||||
- 推荐卡片渲染失败 > 2%
|
||||
- WS 推送延迟 > 5s
|
||||
- 后端 healthy 状态丢失
|
||||
- 推荐卡数量异常(同一用户 > 5 张)
|
||||
|
||||
### 6.2 回滚步骤(< 30 分钟)
|
||||
|
||||
#### Step 1: 前端回滚(5 分钟)
|
||||
|
||||
```bash
|
||||
# 备份当前 dist
|
||||
mv /opt/wecom-it-desk/frontend-h5/dist /opt/wecom-it-desk/frontend-h5/dist_bak_$(date +%Y%m%d_%H%M%S)
|
||||
# 恢复备份的 dist
|
||||
mv /opt/wecom-it-desk/frontend-h5/dist_rollback /opt/wecom-it-desk/frontend-h5/dist
|
||||
# 重启 nginx
|
||||
docker restart wecom_it_nginx
|
||||
# 验证
|
||||
curl -I http://localhost/itdesk/
|
||||
```
|
||||
|
||||
#### Step 2: 后端回滚(10 分钟)
|
||||
|
||||
```bash
|
||||
# 停止后端
|
||||
docker compose stop backend
|
||||
# 恢复代码
|
||||
rm -rf /opt/wecom-it-desk/app/app
|
||||
mv /opt/wecom-it-desk/app_bak_$(ls -t /opt/wecom-it-desk/app_bak_* | head -1 | sed 's/.*app_bak_/app_bak_/') /opt/wecom-it-desk/app/app
|
||||
# 重启后端
|
||||
docker compose up -d backend
|
||||
# 验证
|
||||
curl -I http://localhost:8000/health
|
||||
```
|
||||
|
||||
#### Step 3: 数据库回滚(5 分钟,极端情况)
|
||||
|
||||
```bash
|
||||
# Alembic 降级
|
||||
docker exec -it wecom_it_backend alembic downgrade -1
|
||||
# 或手动 DROP 新增双表
|
||||
docker exec -i wecom_it_postgres psql -U postgres -d wecom_it_desk -c "DROP TABLE IF EXISTS recommend_progress CASCADE; DROP TABLE IF EXISTS recommend_event CASCADE;"
|
||||
```
|
||||
|
||||
#### Step 4: 通知(5 分钟)
|
||||
|
||||
- 群发"已回滚至 v2.3 现状"通知到产品 + 工程 + 客服群
|
||||
- 收集失败原因,写入 BUG 单
|
||||
- 排查修复后重新走灰度流程
|
||||
|
||||
### 6.3 回滚时间预算
|
||||
|
||||
| 阶段 | 时间 |
|
||||
|------|------|
|
||||
| Step 1 前端 | 5 分钟 |
|
||||
| Step 2 后端 | 10 分钟 |
|
||||
| Step 3 DB | 5 分钟 |
|
||||
| Step 4 通知 | 5 分钟 |
|
||||
| **合计** | **< 30 分钟** |
|
||||
|
||||
---
|
||||
|
||||
## 七、风险与边界
|
||||
|
||||
### 7.1 部署期风险
|
||||
|
||||
| 风险 | 缓解措施 |
|
||||
|------|---------|
|
||||
| Alembic 迁移失败 | Init SQL 应急脚本 |
|
||||
| models/__init__.py 整体覆盖丢失其他模型 | 字符串 replace 比 sed/awk 稳(详见任务说明书 §1.4 关键决策点) |
|
||||
| 前端 dist build 触发 safe-delete hook | Rename-Item + .NET Delete 绕路 |
|
||||
| 后端 --workers 不为 1 | docker-compose.yml 校验,强制 1 worker |
|
||||
| 中文字符乱码 | 容器内 locale 检查 + UTF-8 编码 |
|
||||
| 蓝绿共用 PG/Redis 干扰 | 仅升级不降级,DB migration 谨慎执行 |
|
||||
|
||||
### 7.2 运行期风险
|
||||
|
||||
| 风险 | 等级 | 缓解措施 |
|
||||
|------|------|---------|
|
||||
| Dify 升级 action 结构变化 | 中 | 兼容旧字段解析 |
|
||||
| 画像 API 长时间不可用 | 中 | DB 缓存画像降级(来源 C 改造) |
|
||||
| localStorage 配额超限(5MB) | 低 | 仅持久 L2/L3/progress,LRU 淘汰 |
|
||||
| 企微 webhook 推送失败 | 中 | 60s 轮询兜底 |
|
||||
| 灰度期间指标不达标 | 中 | 每阶段不达标暂停 |
|
||||
|
||||
---
|
||||
|
||||
## 八、常见问题(FAQ)
|
||||
|
||||
### Q1: Alembic 容器内不可用怎么办?
|
||||
|
||||
**A**: 使用 Init SQL 应急脚本(详见 §3.1.4)。
|
||||
|
||||
### Q2: ws_manager 多 worker 导致 WS 消息丢失怎么办?
|
||||
|
||||
**A**: 后端必须 `--workers 1`,docker-compose.yml 已强制。
|
||||
|
||||
### Q3: 中文路径下 pnpm install 卡死怎么办?
|
||||
|
||||
**A**: 在 ASCII 路径 `D:\dev\wecom\` 下执行 `npm run build`(详见多路径同步铁律)。
|
||||
|
||||
### Q4: 前端 build 触发 safe-delete hook 怎么办?
|
||||
|
||||
**A**: 使用 `Rename-Item dist __dist_movetmp`(同目录)→ vite 跳过 fs.rmSync → build 完 .NET Delete(详见 memory 跨项目铁律)。
|
||||
|
||||
### Q5: 模型注册丢失怎么办?
|
||||
|
||||
**A**: 不能整体覆盖 `models/__init__.py`,需用 Python 脚本字符串 replace(详见任务说明书 §1.4)。
|
||||
|
||||
### Q6: 容器内 alembic.ini 路径不对怎么办?
|
||||
|
||||
**A**: 使用绝对路径 `docker exec -it wecom_it_backend alembic -c /app/alembic.ini upgrade head`。
|
||||
|
||||
### Q7: psftp 上传大于 100KB 文件慢怎么办?
|
||||
|
||||
**A**: 已优化,使用 `v2_ops.py upload` 子命令,自动 md5 校验 + 断点续传。
|
||||
|
||||
### Q8: 灰度名单怎么选?
|
||||
|
||||
**A**: 1% 选运维 + 产品 + 工程核心成员;10% 选早期种子用户(高频用户);50% 选半个部门(覆盖不同角色);100% 全量。
|
||||
|
||||
---
|
||||
|
||||
## 九、变更日志
|
||||
|
||||
| 版本 | 日期 | 变更内容 | 作者 |
|
||||
|------|------|---------|------|
|
||||
| v1.0 | 2026-07-28 19:36 | 初版:基于 PRD v1.0-Frozen + 技术方案 v1.0 + 任务说明书 v1.0,给出完整部署 SOP(5 件套齐全):后端 7 步 + 前端 4 步 + DB 迁移双轨 + 端到端验证 + 4 阶段灰度 + 30 分钟回滚 + 8 FAQ | Duckula + 宋献 |
|
||||
@@ -0,0 +1,159 @@
|
||||
# 部署运维补充说明 — 分配模式 Tab 收编(v1.2)
|
||||
|
||||
> **关联变更**: 管理后台 v1.2 分配模式 Tab 收编(REQ-集成-002-v1.2)
|
||||
> **部署日期**: 2026-07-28
|
||||
> **版本**: v1.0
|
||||
> **关联文档**:
|
||||
> - PRD: `docs/01-产品文档/08-集成生态/PRD-REQ-集成-002-管理后台-v1.0.md`(v1.2)
|
||||
> - 技术方案: `docs/02-技术文档/技术方案-REQ-集成-002-管理后台v1.2-分配模式Tab收编.md`
|
||||
> - 任务说明书: `docs/07-项目管理/任务说明书/任务说明书-REQ-集成-002-分配模式Tab收编.md`
|
||||
|
||||
---
|
||||
|
||||
## 一、变更摘要
|
||||
|
||||
| 维度 | 变更 |
|
||||
|------|------|
|
||||
| 前端 | `frontend-admin/src/views/Agents.vue` 加 `<el-tabs>` + 第 2 Tab「分配策略」 |
|
||||
| 前端 | `frontend-admin/src/views/AssignmentMode.vue` 文件保留 + 加停用注释 |
|
||||
| 前端 | `frontend-admin/src/router/index.ts` 删除 `/admin/assignment-mode` 路由项 |
|
||||
| 前端 | `frontend-admin/src/views/Layout/menu.ts` 删除「分配模式」菜单项 + `Sort` icon import |
|
||||
| 后端 | **无改动** |
|
||||
| 数据库 | **无改动**(`system_configs.assignment_mode` 仍存) |
|
||||
|
||||
---
|
||||
|
||||
## 二、部署前 checklist
|
||||
|
||||
- [ ] PRD v1.2 已更新
|
||||
- [ ] 原型图 v1.2 已同步
|
||||
- [ ] 技术方案 v1.0 已建
|
||||
- [ ] 测试用例 TC-集成-002 已建(31 条用例)
|
||||
- [ ] 任务说明书已建
|
||||
- [ ] 本地中文路径 `D:\资料\03-项目开发\wecom_it_smart_desk\src\frontend-admin\` 已改
|
||||
- [ ] ASCII 路径 `D:\dev\wecom\src\frontend-admin\` 已同步改(**多路径同步铁律**)
|
||||
- [ ] `npm run build` 通过(无 TS 报错)
|
||||
- [ ] dist `assets/Agents-*.js` 含 `agents-assignment` / `agents-list` / `手动接单` 等字符串
|
||||
|
||||
---
|
||||
|
||||
## 三、部署流程
|
||||
|
||||
### 3.1 压缩 dist
|
||||
|
||||
```powershell
|
||||
Compress-Archive -Path "D:\dev\wecom\src\frontend-admin\dist\*" -DestinationPath "D:\dev\wecom\dist-admin-v1.2-tab.zip" -Force
|
||||
```
|
||||
|
||||
### 3.2 上传到服务器 /tmp
|
||||
|
||||
```powershell
|
||||
& "C:\Users\simon\.workbuddy\binaries\python\versions\3.13.12\python.exe" "C:\Users\simon\.workbuddy\skills\jumpserver-V2\scripts\v2_ops.py" upload "D:\dev\wecom\dist-admin-v1.2-tab.zip" "dist-admin-v1.2-tab.zip"
|
||||
```
|
||||
|
||||
### 3.3 服务器解压到 nginx 路径
|
||||
|
||||
```bash
|
||||
cd /tmp
|
||||
unzip -o dist-admin-v1.2-tab.zip -d /tmp/admin-v1.2-extract
|
||||
rm -rf /opt/wecom-it-desk/frontend-admin/dist
|
||||
mv /tmp/admin-v1.2-extract /opt/wecom-it-desk/frontend-admin/dist
|
||||
```
|
||||
|
||||
### 3.4 重启 nginx(必做!bind mount 不会自动刷新)
|
||||
|
||||
```bash
|
||||
docker restart wecom_it_nginx
|
||||
```
|
||||
|
||||
### 3.5 验证
|
||||
|
||||
```bash
|
||||
# 1. 主入口 200
|
||||
curl -I http://localhost/itadmin/
|
||||
|
||||
# 2. 分配模式 API 仍 200
|
||||
curl -H 'X-Forwarded-For: 10.240.1.100' http://localhost:8000/admin/assignment-mode
|
||||
```
|
||||
|
||||
### 3.6 浏览器手验(需企业微信扫码登录)
|
||||
|
||||
1. 访问 `https://itsupport.servyou.com.cn/itadmin/login` → 扫码登录
|
||||
2. 进"运营中心 → 坐席管理"
|
||||
3. 应看到两个 Tab:「坐席列表」「分配策略」
|
||||
4. 点「分配策略」 → 6 张模式卡片(手动接单高亮 + 其余 5 个锁定)
|
||||
5. 引导条:「当前坐席 X 人足以承担,手动接单完全满足...」
|
||||
|
||||
---
|
||||
|
||||
## 四、回滚预案(30 分钟可逆撤销)
|
||||
|
||||
> 阶段二/三若分配模式膨胀(权重配置、技能匹配规则),可拆回独立页。
|
||||
|
||||
### 步骤 1:恢复路由
|
||||
|
||||
`src/frontend-admin/src/router/index.ts`:
|
||||
```diff
|
||||
+ {
|
||||
+ path: 'assignment-mode',
|
||||
+ name: 'AssignmentMode',
|
||||
+ component: () => import('@/views/AssignmentMode.vue'),
|
||||
+ meta: { title: '消息分配模式', priority: 'P1' },
|
||||
+ },
|
||||
```
|
||||
|
||||
### 步骤 2:恢复菜单项
|
||||
|
||||
`src/frontend-admin/src/views/Layout/menu.ts`:
|
||||
```diff
|
||||
+ import { ..., Sort } from '@element-plus/icons-vue'
|
||||
+
|
||||
+ {
|
||||
+ path: '/admin/assignment-mode',
|
||||
+ meta: { title: '分配模式', icon: Sort, priority: 'P1' },
|
||||
+ // ...其他元数据
|
||||
+ },
|
||||
```
|
||||
|
||||
### 步骤 3:把 Tab 2 内容搬回 `AssignmentMode.vue`
|
||||
|
||||
1. 删除 `AssignmentMode.vue` 顶部 "v1.2 起停用" 注释
|
||||
2. 从 `Agents.vue` 第 2 个 `<el-tab-pane>` 内复制:
|
||||
- `modes` 数组(含 6 模式配置)
|
||||
- `currentMode` ref
|
||||
- `selectMode` 函数
|
||||
- `getModeDescription` 函数
|
||||
- `getLockReason` 函数
|
||||
- 模板(`<div class="mode-list">` + 6 张 `.mode-card`)
|
||||
3. 复制 `Agents.vue` 中引导条组件(或独立组件化)
|
||||
|
||||
### 步骤 4:恢复 `Agents.vue` 单一结构
|
||||
|
||||
`src/frontend-admin/src/views/Agents.vue`:
|
||||
```diff
|
||||
- <el-tabs v-model="activeTab">
|
||||
- <el-tab-pane label="坐席列表" name="agents-list"> ... </el-tab-pane>
|
||||
- <el-tab-pane label="分配策略" name="agents-assignment"> ... </el-tab-pane>
|
||||
- </el-tabs>
|
||||
+ <!-- 移除 el-tabs 包装,原 Tab 1 内容提升到顶层 -->
|
||||
```
|
||||
|
||||
### 步骤 5:build + 部署
|
||||
|
||||
重复 § 三 流程。
|
||||
|
||||
### 步骤 6:归档本次 v1.2 文档
|
||||
|
||||
- 任务说明书状态改为 `[已废弃]`
|
||||
- TC-集成-002 标记为已废弃(保留作为历史记录)
|
||||
- PRD v1.2 头部加 "v1.2 已废弃,已回滚到 v1.3 独立页" 注释
|
||||
|
||||
**总耗时估算**:30 分钟(步骤 1-4 约 20 min,步骤 5-6 约 10 min)。
|
||||
|
||||
---
|
||||
|
||||
## 五、变更日志
|
||||
|
||||
| 版本 | 日期 | 变更 | 变更人 |
|
||||
|------|------|------|--------|
|
||||
| v1.0 | 2026-07-28 | 初版(v1.2 部署补充说明) | 宋献 |
|
||||
@@ -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,264 @@
|
||||
# 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 白名单(2026-07-06 更新 — 添加办公网IP)
|
||||
location /itadmin/ {
|
||||
# 允许的IP列表(按需求添加)
|
||||
allow 10.90.0.0/16; # 内网段 - 税友内网
|
||||
allow 10.240.0.0/16; # 内网段 - 办公网
|
||||
allow 117.147.35.138; # 办公网出口IP
|
||||
allow 218.75.34.87; # 办公网出口IP
|
||||
allow 127.0.0.1; # 本地
|
||||
deny all; # 其他拒绝
|
||||
|
||||
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 严格白名单(与/itadmin/一致)
|
||||
location /api/admin/ {
|
||||
# 允许的IP列表(按需求添加)
|
||||
allow 10.90.0.0/16; # 内网段 - 税友内网
|
||||
allow 10.240.0.0/16; # 内网段 - 办公网
|
||||
allow 117.147.35.138; # 办公网出口IP
|
||||
allow 218.75.34.87; # 办公网出口IP
|
||||
allow 127.0.0.1; # 本地
|
||||
deny all; # 其他拒绝
|
||||
|
||||
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)
|
||||
@@ -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/auth/otp-status` | GET | 查询绑定状态 |
|
||||
| `/api/auth/otp-bind` | POST | 开始绑定(返回 secret + QR) |
|
||||
| `/api/auth/otp-verify` | POST | 确认绑定 |
|
||||
| `/api/auth/otp-verify` | POST | 验证 OTP |
|
||||
| `/api/auth/otp-unbind` | 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,246 @@
|
||||
# Token多IP异常检测 - 部署指南
|
||||
|
||||
> **任务ID**: 待分配
|
||||
> **版本**: v1.0
|
||||
> **关联文档**:
|
||||
> - 技术设计-Token多IP异常检测.md
|
||||
> - Token多IP异常检测测试用例.md
|
||||
|
||||
---
|
||||
|
||||
## 1. 部署概述
|
||||
|
||||
### 1.1 部署范围
|
||||
|
||||
| 组件 | 容器 | 说明 |
|
||||
|------|------|------|
|
||||
| 检测服务 | backend | Token异常检测定时任务 |
|
||||
| Redis | redis | IP存储 |
|
||||
| 告警通道 | 企微机器人 | 复用现有webhook |
|
||||
|
||||
### 1.2 部署方式
|
||||
|
||||
- **部署类型**: 增量部署(不涉及基础设施变更)
|
||||
- **停机时间**: 无需停机(定时任务后台运行)
|
||||
- **回滚**: 代码级别回滚
|
||||
|
||||
---
|
||||
|
||||
## 2. 部署前检查
|
||||
|
||||
### 2.1 环境检查
|
||||
|
||||
| 检查项 | 命令 | 预期结果 |
|
||||
|--------|------|----------|
|
||||
| Redis连接 | `docker exec backend redis-cli ping` | PONG |
|
||||
| APScheduler状态 | 检查日志 | 定时任务启动成功 |
|
||||
| webhook配置 | 检查环境变量 | CONTENT_AUDIT_WEBHOOK已设置 |
|
||||
|
||||
### 2.2 配置检查
|
||||
|
||||
确认以下环境变量已配置(如需自定义):
|
||||
|
||||
```bash
|
||||
# 可选配置
|
||||
TOKEN_ANOMALY_THRESHOLD=3 # 触发告警的IP数量阈值
|
||||
TOKEN_ANOMALY_WINDOW=3600 # 时间窗口(秒)
|
||||
TOKEN_ANOMALY_AUTO_DISABLE=false # 是否自动禁用Token
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 部署步骤
|
||||
|
||||
### 3.1 步骤1:代码变更
|
||||
|
||||
**新增文件**:
|
||||
```
|
||||
backend/app/tasks/token_anomaly_detection.py (新建)
|
||||
```
|
||||
|
||||
**修改文件**:
|
||||
```
|
||||
backend/app/services/token_service.py (增加record_token_ip方法)
|
||||
backend/app/main.py (注册定时任务)
|
||||
```
|
||||
|
||||
### 3.2 步骤2:配置变更
|
||||
|
||||
在 `.env` 或 docker-compose.yml 中添加(可选):
|
||||
|
||||
```bash
|
||||
# 如需自定义阈值,在 .env 中添加
|
||||
TOKEN_ANOMALY_THRESHOLD=3
|
||||
TOKEN_ANOMALY_AUTO_DISABLE=false
|
||||
```
|
||||
|
||||
### 3.3 步骤3:重启服务
|
||||
|
||||
```bash
|
||||
# 重启backend容器(不中断其他服务)
|
||||
docker compose restart backend
|
||||
|
||||
# 查看日志确认定时任务启动
|
||||
docker logs backend --tail 50 | grep -i "token"
|
||||
```
|
||||
|
||||
预期日志:
|
||||
```
|
||||
✅ Token多IP异常检测任务已启动(每60秒执行一次)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 部署后验证
|
||||
|
||||
### 4.1 功能验证
|
||||
|
||||
| 验证项 | 验证方法 | 预期结果 |
|
||||
|--------|----------|----------|
|
||||
| 定时任务运行 | 查看日志 | 每分钟执行一次 |
|
||||
| IP记录 | 模拟API请求 | Redis中记录IP |
|
||||
| 告警触发 | 构造3个IP的Token | 企微收到告警 |
|
||||
|
||||
### 4.2 冒烟测试
|
||||
|
||||
执行测试用例:
|
||||
```bash
|
||||
# 进入backend容器
|
||||
docker exec -it backend bash
|
||||
|
||||
# 运行单元测试
|
||||
pytest app/tests/test_token_anomaly.py -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 监控与运维
|
||||
|
||||
### 5.1 日志位置
|
||||
|
||||
| 日志类型 | 路径 |
|
||||
|----------|------|
|
||||
| 应用日志 | `/app/logs/wecom-it-desk.log` |
|
||||
| 定时任务日志 | 集成在应用日志中 |
|
||||
|
||||
### 5.2 监控指标
|
||||
|
||||
| 指标 | 说明 |
|
||||
|------|------|
|
||||
| token_anomaly_triggered_total | 触发告警次数 |
|
||||
| token_anomaly_false_positive | 误报次数 |
|
||||
|
||||
### 5.3 运维命令
|
||||
|
||||
```bash
|
||||
# 查看定时任务状态
|
||||
docker exec backend python -c "from app.main import _scheduler; print(_scheduler.get_jobs())"
|
||||
|
||||
# 手动触发检测(调试)
|
||||
docker exec backend python -c "
|
||||
import asyncio
|
||||
from app.tasks.token_anomaly_detection import detect_token_anomaly
|
||||
asyncio.run(detect_token_anomaly())
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 回滚方案
|
||||
|
||||
### 6.1 回滚步骤
|
||||
|
||||
```bash
|
||||
# 1. 撤销代码变更
|
||||
git checkout -- backend/app/services/token_service.py
|
||||
git checkout -- backend/app/main.py
|
||||
git rm backend/app/tasks/token_anomaly_detection.py
|
||||
|
||||
# 2. 重启服务
|
||||
docker compose restart backend
|
||||
```
|
||||
|
||||
### 6.2 数据清理
|
||||
|
||||
```bash
|
||||
# 清理Redis中的检测数据(可选,1小时后自动过期)
|
||||
docker exec backend redis-cli KEYS "token_ips:*" | xargs redis-cli DEL
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 部署清单
|
||||
|
||||
| 序号 | 步骤 | 执行人 | 检查人 | 日期 |
|
||||
|------|------|--------|--------|------|
|
||||
| 1 | 代码变更 | 开发 | | |
|
||||
| 2 | 配置检查 | 开发 | | |
|
||||
| 3 | 重启服务 | 运维 | | |
|
||||
| 4 | 功能验证 | 测试 | | |
|
||||
| 5 | 冒烟测试 | 测试 | | |
|
||||
| 6 | 监控确认 | 运维 | | |
|
||||
|
||||
---
|
||||
|
||||
## 8. 附录
|
||||
|
||||
### 8.1 相关文件
|
||||
|
||||
| 文件路径 | 说明 |
|
||||
|----------|------|
|
||||
| `backend/app/tasks/token_anomaly_detection.py` | 检测任务 |
|
||||
| `backend/app/services/token_service.py` | Token服务 |
|
||||
| `docs/04-运维文档/部署运维/技术设计-Token多IP异常检测.md` | 技术设计 |
|
||||
|
||||
### 8.2 环境变量参考
|
||||
|
||||
| 变量 | 必需 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| CONTENT_AUDIT_WEBHOOK | 是 | - | 企微机器人webhook |
|
||||
| TOKEN_ANOMALY_THRESHOLD | 否 | 3 | 告警阈值 |
|
||||
| TOKEN_ANOMALY_AUTO_DISABLE | 否 | false | 自动禁用 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 部署测试结果(2026-07-14)
|
||||
|
||||
### 9.1 部署清单
|
||||
|
||||
| 序号 | 步骤 | 执行人 | 检查人 | 日期 | 结果 |
|
||||
|------|------|--------|--------|------|------|
|
||||
| 1 | 代码变更 | 开发 | | 2026-07-14 | ✅ 完成 |
|
||||
| 2 | 配置检查 | 开发 | | 2026-07-14 | ✅ 完成 |
|
||||
| 3 | 重启服务 | 运维 | | 2026-07-14 | ✅ 完成 |
|
||||
| 4 | 功能验证 | 测试 | | 2026-07-14 | ✅ 通过 |
|
||||
| 5 | 冒烟测试 | 测试 | | 2026-07-14 | ✅ 通过 |
|
||||
| 6 | 监控确认 | 运维 | | 2026-07-14 | ✅ 完成 |
|
||||
|
||||
### 9.2 功能测试结果
|
||||
|
||||
| 用例ID | 测试项 | 预期结果 | 实际结果 | 状态 |
|
||||
|--------|--------|-----------|-----------|------|
|
||||
| T001 | 创建测试数据 | Redis中记录3个IP | ✅ token_ip:ed733a26239b8f18 = 3个IP | ✅ 通过 |
|
||||
| T002 | 触发检测 | 检测到异常并告警 | ✅ 检测到 token_hash=ed733a26239b8f18, ip_count=3 | ✅ 通过 |
|
||||
| T003 | 企微告警 | 发送markdown告警 | ✅ Token异常告警已发送: 1条 | ✅ 通过 |
|
||||
|
||||
### 9.3 验证日志
|
||||
|
||||
```
|
||||
检测到Token异常使用: token_hash=ed733a26239b8f18, ip_count=3, ips=['192.168.1.100', '192.168.1.102', '192.168.1.101']
|
||||
HTTP Request: POST https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=09120612-9d19-4f93-bd00-bfbaef548dde "HTTP/1.1 2
|
||||
Token异常告警已发送: 1 条
|
||||
Token异常检测完成: 检测到 1 个异常
|
||||
```
|
||||
|
||||
### 9.4 发现问题与修复
|
||||
|
||||
| 问题 | 原因 | 解决方案 |
|
||||
|------|------|----------|
|
||||
| 告警未发送 | 容器内.env文件路径错误 | 修改config.py的env_file为"/app/app/.env",并复制.env到挂载目录 |
|
||||
| webhook未配置 | .env未同步到容器 | 复制.env到/app/app/.env |
|
||||
|
||||
---
|
||||
|
||||
> **编制人**: 威胁检测工程师
|
||||
> **日期**: 2026-07-14
|
||||
> **审核人**: 待定
|
||||
@@ -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,93 @@
|
||||
%% IT 智能服务台 — 架构组件图(卷挂载重构前后对比)
|
||||
%% 文件: class-diagram.mermaid
|
||||
|
||||
classDiagram
|
||||
class HostServer {
|
||||
+String path: /opt/wecom-it-desk/
|
||||
+String ip: 10.90.5.110
|
||||
+String domain: itsupport.servyou.com.cn
|
||||
}
|
||||
|
||||
class AppDirectory {
|
||||
+String path: /opt/wecom-it-desk/app/
|
||||
+String role: 唯一代码源
|
||||
+List~File~ pythonFiles
|
||||
+Boolean hasAuthPy
|
||||
}
|
||||
|
||||
class BackendDirectory {
|
||||
+String path: /opt/wecom-it-desk/backend/
|
||||
+File dockerfile
|
||||
+File requirementsTxt
|
||||
+String note: 不再包含 app/ 子目录
|
||||
}
|
||||
|
||||
class DockerImage {
|
||||
+String name: wecom-it-desk-backend:latest
|
||||
+String base: python:3.12-slim
|
||||
+String pythonVersion: 3.12
|
||||
+Boolean containsCode: false
|
||||
+Boolean containsDeps: true
|
||||
+String envPythondontWriteBytecode: "1"
|
||||
}
|
||||
|
||||
class DockerContainer {
|
||||
+String name: wecom_it_backend
|
||||
+String workdir: /app
|
||||
+String command: uvicorn app.main:app
|
||||
+Integer port: 8000
|
||||
+Boolean healthy
|
||||
}
|
||||
|
||||
class VolumeMount {
|
||||
+String hostPath: /opt/wecom-it-desk/app/
|
||||
+String containerPath: /app/app/
|
||||
+String type: bind
|
||||
+String mode: rw
|
||||
}
|
||||
|
||||
class UploadVolume {
|
||||
+String name: backend-uploads
|
||||
+String containerPath: /app/uploads/
|
||||
+String type: named_volume
|
||||
}
|
||||
|
||||
class LogMount {
|
||||
+String hostPath: /var/log/wecom-it-desk/
|
||||
+String containerPath: /app/logs/
|
||||
+String type: bind
|
||||
}
|
||||
|
||||
class DockerCompose {
|
||||
+String file: docker-compose.yml
|
||||
+String buildContext: ./backend
|
||||
+List~Service~ services
|
||||
}
|
||||
|
||||
class HealthCheck {
|
||||
+String endpoint: /health
|
||||
+Integer interval: 30
|
||||
+Integer timeout: 10
|
||||
+Integer retries: 3
|
||||
+Integer startPeriod: 40
|
||||
}
|
||||
|
||||
HostServer --> AppDirectory : contains
|
||||
HostServer --> BackendDirectory : contains
|
||||
HostServer --> DockerCompose : contains
|
||||
|
||||
DockerCompose --> DockerImage : builds
|
||||
DockerCompose --> DockerContainer : runs
|
||||
DockerCompose --> VolumeMount : configures
|
||||
DockerCompose --> UploadVolume : configures
|
||||
DockerCompose --> LogMount : configures
|
||||
|
||||
DockerImage --> DockerContainer : basis for
|
||||
VolumeMount --> DockerContainer : mounts code to /app/app/
|
||||
UploadVolume --> DockerContainer : mounts uploads to /app/uploads/
|
||||
LogMount --> DockerContainer : mounts logs to /app/logs/
|
||||
|
||||
AppDirectory --> VolumeMount : source of
|
||||
DockerContainer --> HealthCheck : monitored by
|
||||
|
||||
BackendDirectory --> DockerImage : provides Dockerfile + requirements.txt
|
||||
@@ -0,0 +1,182 @@
|
||||
# 智能IT服务台 - 部署指南
|
||||
|
||||
> **最后更新**:2026-07-05
|
||||
> **目标服务器**:`10.90.5.110`
|
||||
> **域名**:`itsupport.servyou.com.cn`
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [部署流程](#一部署流程)
|
||||
2. [蓝绿部署](#二蓝绿部署)
|
||||
3. [故障排查](#三故障排查)
|
||||
4. [回滚方案](#四回滚方案)
|
||||
|
||||
---
|
||||
|
||||
## 一、部署流程
|
||||
|
||||
### 1.1 前置条件
|
||||
|
||||
| 条件 | 状态 | 验证命令 |
|
||||
|------|------|---------|
|
||||
| Linux 服务器 10.90.5.110 | ✅ 已确认 | - |
|
||||
| Docker 已安装 | ✅ 已确认 | `docker --version` |
|
||||
| 域名解析 | ✅ 已配置 | `nslookup itsupport.servyou.com.cn` |
|
||||
| 堡垒机可访问 | ✅ 已配置 | `ssh -p 2222 user@10.212.189.210` |
|
||||
|
||||
### 1.2 快速部署命令
|
||||
|
||||
```bash
|
||||
# 1. 进入部署目录
|
||||
cd /opt/wecom-it-desk
|
||||
|
||||
# 2. 拉取最新代码(可选)
|
||||
git pull origin main
|
||||
|
||||
# 3. 构建并启动
|
||||
docker compose up -d --build
|
||||
|
||||
# 4. 检查状态
|
||||
docker ps
|
||||
```
|
||||
|
||||
### 1.3 一键部署(生产环境)
|
||||
|
||||
详见 [10-一键部署操作包-v0.7.0.md](./10-一键部署操作包-v0.7.0.md)
|
||||
|
||||
### 1.4 部署后验证
|
||||
|
||||
```bash
|
||||
# 检查容器状态
|
||||
docker ps
|
||||
|
||||
# 检查 API 健康
|
||||
curl https://itsupport.servyou.com.cn/api/health
|
||||
|
||||
# 检查日志
|
||||
docker logs wecom_it_backend --tail 50
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、蓝绿部署
|
||||
|
||||
### 2.1 什么是蓝绿部署
|
||||
|
||||
蓝绿部署通过维护两套环境(Blue/Green)实现零停机部署和快速回滚。
|
||||
|
||||
### 2.2 部署 Green 环境
|
||||
|
||||
```bash
|
||||
cd /opt/wecom-it-desk
|
||||
|
||||
# 构建并启动 Green 环境
|
||||
docker-compose -f docker-compose-green.yml up -d
|
||||
|
||||
# 验证 Green 环境
|
||||
curl http://localhost:5002/health
|
||||
```
|
||||
|
||||
### 2.3 切换流量
|
||||
|
||||
```bash
|
||||
# 切换到 Green
|
||||
sed -i 's/wecom_it_backend:8000/wecom_it_backend_green:8000/' /opt/wecom-it-desk/nginx/nginx.conf
|
||||
docker restart wecom_it_nginx
|
||||
|
||||
# 切换回 Blue(回滚)
|
||||
sed -i 's/wecom_it_backend_green:8000/wecom_it_backend:8000/' /opt/wecom-it-desk/nginx/nginx.conf
|
||||
docker restart wecom_it_nginx
|
||||
```
|
||||
|
||||
### 2.4 端口说明
|
||||
|
||||
| 端口 | 服务 |
|
||||
|------|------|
|
||||
| 80/443 | Nginx (生产入口) |
|
||||
| 5002 | Backend (Green) |
|
||||
| 5080 | Nginx (Green 测试) |
|
||||
|
||||
---
|
||||
|
||||
## 三、故障排查
|
||||
|
||||
### 3.1 常见问题
|
||||
|
||||
#### 502 Bad Gateway
|
||||
|
||||
1. 检查后端容器是否运行
|
||||
```bash
|
||||
docker ps | grep backend
|
||||
```
|
||||
|
||||
2. 检查后端日志
|
||||
```bash
|
||||
docker logs wecom_it_backend --tail 100
|
||||
```
|
||||
|
||||
3. 重启后端
|
||||
```bash
|
||||
docker restart wecom_it_backend
|
||||
```
|
||||
|
||||
#### 500 错误
|
||||
|
||||
详见 [标准故障排查手册](../00-标准故障排查手册.md)
|
||||
|
||||
#### 通讯链路问题
|
||||
|
||||
详见 [标准故障排查手册](../00-标准故障排查手册.md)
|
||||
|
||||
### 3.2 健康检查
|
||||
|
||||
```bash
|
||||
# 后端
|
||||
curl http://localhost:8000/health
|
||||
|
||||
# Nginx
|
||||
curl http://localhost/api/health
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、回滚方案
|
||||
|
||||
### 4.1 紧急回滚
|
||||
|
||||
如果部署后出现严重问题,立即执行:
|
||||
|
||||
```bash
|
||||
# 1. 停止新版本容器
|
||||
docker stop wecom_it_backend_green
|
||||
|
||||
# 2. 切换回原环境
|
||||
sed -i 's/wecom_it_backend_green:8000/wecom_it_backend:8000/' /opt/wecom-it-desk/nginx/nginx.conf
|
||||
docker restart wecom_it_nginx
|
||||
```
|
||||
|
||||
### 4.2 版本回滚
|
||||
|
||||
如需回滚到之前版本:
|
||||
|
||||
```bash
|
||||
# 1. 停止当前版本
|
||||
docker stop wecom_it_backend
|
||||
|
||||
# 2. 重新构建指定版本
|
||||
git checkout <版本标签>
|
||||
docker build -t wecom-it-desk-backend:<版本> ./backend
|
||||
|
||||
# 3. 启动
|
||||
docker run -d --name wecom_it_backend wecom-it-desk-backend:<版本>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [10-一键部署操作包-v0.7.0.md](./10-一键部署操作包-v0.7.0.md)
|
||||
- [蓝绿部署指南.md](./蓝绿部署指南.md)
|
||||
- [标准故障排查手册](../00-标准故障排查手册.md)
|
||||
@@ -0,0 +1,152 @@
|
||||
# 智能IT服务台 - 版本记录
|
||||
|
||||
> **最后更新**:2026-07-13
|
||||
|
||||
---
|
||||
|
||||
## 版本历史
|
||||
|
||||
### v0.7.3 (2026-07-12~13)
|
||||
|
||||
**更新内容**:
|
||||
|
||||
1. **AI 对话链路全栈改造 Phase 1-6**(#59-#69)
|
||||
- Dify Prompt JSON 输出 + 后端 blocking + JSON 解析 + 双 WS 推送
|
||||
- 审批关键词收窄(~40→~25)+ 两级分类 Prompt v4.0 + 删除前端 checkApprovalIntent
|
||||
- WS 扩展(ai_thinking + dynamic_recommend)+ MessageBubble ai_structured 渲染 + RightPanel v2
|
||||
- VisionService 接入(图片分析 + 5秒消息融合)+ 降级策略
|
||||
- 坐席端 ai_thinking 指示器 + ai_structured/byod_card 渲染
|
||||
- diagnosis_stage 字段 + response_time_ms 计时 + 慢响应告警
|
||||
|
||||
2. **上下文感知智能诊断→修复闭环**
|
||||
- 三层诊断(API→Script→AI)+ 三段排队(VIP→info_locked→not locked)
|
||||
- 答题插队 + 五场景关闭 + 迁移 052(6表+6列)
|
||||
|
||||
3. **坐席端布局优化 v2.0**
|
||||
- QuickReplyBar L1+L2 悬浮 / ReplyBox 左右分区 / 右栏 260↔560px
|
||||
- 键盘快捷键 v2.3(纯数字路由 / ESC 分层撤销 / IME 守卫)
|
||||
|
||||
4. **知识库迭代 3**
|
||||
- 分诊交互 + 拓扑预览(ECharts)+ 代答排除(4种匹配器)+ 迁移 051
|
||||
|
||||
5. **H5 v4 人工坐席交互改造**(#116)
|
||||
- 三态文案统一"人工坐席" / 按钮位置上移 / 删除 CallAgentModal 弹窗
|
||||
- 截图提示改版 / 移动端 CSS 隐藏 / DB 同步 funny_phrases 表
|
||||
|
||||
6. **部署路径修正**
|
||||
- 确认服务器项目根路径 `/opt/wecom-it-desk/`
|
||||
- 所有前端 dist 均为 ro bind mount,只能在宿主机源路径操作
|
||||
|
||||
**部署方式**:后端 `docker compose restart backend` / 前端宿主机 tar 解压 + `nginx -s reload`
|
||||
|
||||
**验证**:JS hash 更新确认 / JS 包内容检查 / API 200 / Nginx healthy
|
||||
|
||||
---
|
||||
|
||||
### v0.7.1 (2026-06-23)
|
||||
|
||||
**更新内容**:
|
||||
- OTP 二次验证功能上线
|
||||
- 扫码登录优化
|
||||
- 安全性增强
|
||||
|
||||
**部署包**:[10-一键部署操作包-v0.7.0.md](./10-一键部署操作包-v0.7.0.md)
|
||||
|
||||
**相关文档**:
|
||||
- [07-扫码登录OTP部署指南-v0.7.0.md](./07-扫码登录OTP部署指南-v0.7.0.md)
|
||||
- [06-OTP二次验证实现.md](./06-OTP二次验证实现.md)
|
||||
|
||||
---
|
||||
|
||||
### v0.7.0 (2026-06-16)
|
||||
|
||||
**更新内容**:
|
||||
- 消息推送策略优化
|
||||
- 超时提醒功能
|
||||
- 头像同步功能
|
||||
|
||||
**部署包**:[一键部署操作包-v0.7.0.md](./一键部署操作包-v0.7.0.md)
|
||||
|
||||
---
|
||||
|
||||
### v0.6.x (历史版本)
|
||||
|
||||
详见 [03-RELEASE-NOTES-v0.7.1-20260623.md](./03-RELEASE-NOTES-v0.7.1-20260623.md)
|
||||
|
||||
---
|
||||
|
||||
## 问题修复记录
|
||||
|
||||
### 2026-07-26
|
||||
- H5 选项交互全链路修复(CASE-20260726-01~08):白屏/编码损坏/Pinia `.value`/UUID 排序/轮询去重/坐席可见性等 8 个连环问题
|
||||
- 详见 [标准故障排查手册](../00-标准故障排查手册.md) §4 CASE-20260726-01~08
|
||||
|
||||
### 2026-07-25
|
||||
- H5 始终显示坐席在线 — WS 断连查询字段不匹配(CASE-20260725-01)
|
||||
- H5 用户端收不到 AI 回复 — WebSocket 推送 datetime 序列化失败(CASE-20260725-02)
|
||||
- 详见 [标准故障排查手册](../00-标准故障排查手册.md) §4
|
||||
|
||||
### 2026-07-24
|
||||
- H5 选项选择消息重复 — 前端本地 message_id 与后端不同致去重失效(CASE-20260724-01)
|
||||
- 详见 [标准故障排查手册](../00-标准故障排查手册.md) §4
|
||||
|
||||
### 2026-07-23
|
||||
- H5 AI 回复显示 `[object Object]` — Dify 原生 API 超时回退代理(CASE-20260723-01)
|
||||
- 详见 [标准故障排查手册](../00-标准故障排查手册.md) §4
|
||||
|
||||
### 2026-07-16
|
||||
- 员工端 /h5/ 404 — nginx 反向代理配置错误(CASE-20260716-01)
|
||||
- 详见 [标准故障排查手册](../00-标准故障排查手册.md) §4
|
||||
|
||||
### 2026-07-15
|
||||
- 员工端审批"审批模板ID不正确" — 企微审批模板失效,改用 ITSM(CASE-20260715-01)
|
||||
- 详见 [标准故障排查手册](../00-标准故障排查手册.md) §4
|
||||
|
||||
### 2026-07-14
|
||||
- 坐席端 403 — 前端 bind mount 未生效(CASE-20260714-01)
|
||||
- 员工端 /h5/ 404 — nginx 配置错误(CASE-20260714-02)
|
||||
- 详见 [标准故障排查手册](../00-标准故障排查手册.md) §4
|
||||
|
||||
### 2026-07-13
|
||||
|
||||
- H5 v4 部署路径修正(`/opt/wecom-it-desk/frontend-h5/dist` ro bind mount)
|
||||
- 服务器 Docker 挂载配置与本地仓库不一致问题定位
|
||||
|
||||
### 2026-07-05
|
||||
|
||||
- Nginx upstream 配置修复
|
||||
- 蓝绿部署流程完善
|
||||
|
||||
详见 [标准故障排查手册](../00-标准故障排查手册.md)
|
||||
|
||||
### 2026-06-13
|
||||
|
||||
- H5 用户端报错修复
|
||||
- 后端启动问题修复
|
||||
|
||||
详见 [标准故障排查手册](../00-标准故障排查手册.md)
|
||||
|
||||
---
|
||||
|
||||
## 版本号规则
|
||||
|
||||
| 位置 | 规则 | 示例 |
|
||||
|------|------|------|
|
||||
| 前端 | v主.次.修订 | v1.5.0 |
|
||||
| 后端 | v主.次.修订 | v0.7.1 |
|
||||
| 部署包 | v主.次.发布日期 | v0.7.0-20260623 |
|
||||
|
||||
---
|
||||
|
||||
## 升级路径
|
||||
|
||||
### 从 v0.6.x 升级到 v0.7.x
|
||||
|
||||
1. 备份数据
|
||||
2. 执行一键部署
|
||||
3. 验证功能
|
||||
4. 监控日志
|
||||
|
||||
### 版本回滚
|
||||
|
||||
详见 [01-部署指南.md](./01-部署指南.md) 中的回滚方案
|
||||
@@ -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`
|
||||
- **移除摇铃按钮**:删除 🔔 摇铃按钮及相关 CSS(bell-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,52 @@
|
||||
%% IT 智能服务台 — 部署时序图(卷挂载重构)
|
||||
%% 文件: sequence-diagram.mermaid
|
||||
|
||||
sequenceDiagram
|
||||
participant Ops as 运维人员
|
||||
participant Host as 宿主机 10.90.5.110
|
||||
participant Docker as Docker Engine
|
||||
participant Container as Backend 容器
|
||||
|
||||
Note over Ops,Container: S1: 前置验证与备份
|
||||
Ops->>Host: curl /health 验证当前状态
|
||||
Host-->>Ops: 200 OK
|
||||
Ops->>Host: 备份 docker-compose.yml → .bak.{TIMESTAMP}
|
||||
Ops->>Host: 备份 Dockerfile → .bak.{TIMESTAMP}
|
||||
Ops->>Host: 写入 .rollback-info 文件
|
||||
|
||||
Note over Ops,Container: S2: 代码验证
|
||||
Ops->>Host: 检查 app/__init__.py, app/main.py
|
||||
Ops->>Host: 检查 app/api/auth.py(关键!)
|
||||
Host-->>Ops: 全部存在
|
||||
|
||||
Note over Ops,Container: S3: 修改 Dockerfile
|
||||
Ops->>Host: 重写 Dockerfile
|
||||
Note right of Host: 删除 COPY . .<br/>新增 ENV PYTHONDONTWRITEBYTECODE=1
|
||||
Ops->>Host: grep 验证 COPY . . 已删除
|
||||
|
||||
Note over Ops,Container: S4: 修改 docker-compose.yml
|
||||
Ops->>Host: sed 插入 ./app:/app/app 卷挂载
|
||||
Ops->>Host: grep 验证卷挂载已添加
|
||||
|
||||
Note over Ops,Container: S5: 重建镜像并重启
|
||||
Ops->>Docker: docker compose build backend
|
||||
Docker->>Docker: 构建镜像(仅 site-packages)
|
||||
Note right of Docker: 镜像不含业务代码
|
||||
Ops->>Docker: docker compose up -d backend
|
||||
Docker->>Container: 创建容器
|
||||
Docker->>Host: 挂载 ./app → /app/app (bind mount)
|
||||
Container->>Host: 运行时读取 /opt/wecom-it-desk/app/ 代码
|
||||
Container->>Container: uvicorn app.main:app 启动
|
||||
|
||||
Note over Ops,Container: S6: 部署后验证
|
||||
Ops->>Container: curl http://localhost:8000/health
|
||||
Container-->>Ops: 200 OK
|
||||
Ops->>Container: docker exec ... from app.auth import router
|
||||
Container-->>Ops: auth module: OK
|
||||
Ops->>Host: md5sum app/main.py
|
||||
Ops->>Container: docker exec md5sum /app/app/main.py
|
||||
Note right of Ops: 对比 hash 一致 → 卷挂载正常
|
||||
|
||||
Note over Ops,Container: S7: 清理(48小时后)
|
||||
Ops->>Host: rm -rf backend/app/
|
||||
Ops->>Host: 确认服务仍正常
|
||||
@@ -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,76 @@
|
||||
# 部署运维工具箱
|
||||
|
||||
> **版本**: v1.0 | **日期**: 2026-07-10 | **维护人**: 宋献
|
||||
> **定位**: 部署运维过程中可复用的脚本、配置模板和调试工具的统一存放点。
|
||||
> **规则**: 每次故障排查或部署完成后,可复用的工具应归档到此目录并在本 README 中登记。
|
||||
|
||||
---
|
||||
|
||||
## 工具索引
|
||||
|
||||
### 上传部署工具
|
||||
|
||||
| 工具 | 用途 | 使用方式 |
|
||||
|------|------|----------|
|
||||
| `fast_upload.py` | 堡垒机大文件快速上传(已被 jumpserver-V2 的 psftp 通道替代,仅供参考) | `python fast_upload.py <本地文件> <远程路径>` |
|
||||
| `deploy_to_container.py` | 一键部署到 Docker 容器(打包→上传→cp→重启) | `python deploy_to_container.py <服务名> <本地路径> <容器路径>` |
|
||||
|
||||
### Nginx 配置模板
|
||||
|
||||
| 工具 | 用途 | 使用方式 |
|
||||
|------|------|----------|
|
||||
| `nginx-access-control.conf` | 三端访问控制配置模板(企微 UA OR IP 白名单双条件放行 + CSP 头) | 上传到服务器 `/opt/wecom-it-desk/nginx/nginx.conf` 后 `docker restart wecom_it_nginx` |
|
||||
|
||||
### 调试检查工具
|
||||
|
||||
| 工具 | 用途 | 使用方式 |
|
||||
|------|------|----------|
|
||||
| `check_html.py` | Python f-string HTML 花括号平衡检查器(排查 `{{ }}` 转义问题) | `python check_html.py <file.py>` |
|
||||
| `render_test.py` | 容器内 HTML 渲染验证(模拟 f-string 渲染并输出实际 HTML/JS) | `docker exec wecom_it_backend python /tmp/render_test.py` |
|
||||
| `extract_html.py` | 从 Python f-string 中提取 HTML 模板到独立文件 | `python extract_html.py <file.py>` |
|
||||
|
||||
### 历史修复脚本(archive/)
|
||||
|
||||
以下脚本为一次性修复用途,保留在 `archive/` 子目录中供参考,不建议直接复用。
|
||||
|
||||
| 脚本 | 修复场景 | 日期 |
|
||||
|------|----------|------|
|
||||
| `fix_admin_role.py` | 修复扫码登录角色写死为 agent 的问题 | 2026-07-08 |
|
||||
| `fix_compose_redis.py` / `fix_compose_redis2.py` | 修复 docker-compose Redis 密码 URL 编码 | 2026-07-07 |
|
||||
| `fix_itdesk.py` / `fix_itdesk2.py` / `fix_itdesk3.py` | 修复 nginx /itdesk/ 路由问题 | 2026-07-08 |
|
||||
| `patch.py` / `patch-mini.py` / `patch-redis-url.py` | Redis 密码 URL 编码补丁 | 2026-07-02 |
|
||||
| `update_password.py` | 数据库密码更新脚本 | 2026-07-05 |
|
||||
| `check_logs.py` / `check_roles.py` / `verify_logs.py` | 日志和角色检查(一次性诊断) | 2026-07-08~09 |
|
||||
| `upload_chunked.py` / `upload_split.py` | 早期分块上传方案(已被 fast_upload.py 替代) | 2026-07-08 |
|
||||
| `nginx_*.conf` / `itdesk-nginx-block.conf` | 历史 nginx 配置快照 | 2026-07-06~08 |
|
||||
| `extract_and_migrate.py` / `_ctrt_transform.py` | 数据迁移和格式转换 | 2026-07-06 |
|
||||
|
||||
---
|
||||
|
||||
## 工具沉淀流程
|
||||
|
||||
1. **排查完成** → 评估是否有可复用的脚本/配置模板
|
||||
2. **归档** → 复制到 `toolbox/`(活跃工具)或 `toolbox/archive/`(历史脚本)
|
||||
3. **登记** → 在本 README 的工具索引表中添加条目
|
||||
4. **清理** → 删除项目根目录的临时文件(渲染输出、中间产物等)
|
||||
|
||||
## 堡垒机使用说明
|
||||
|
||||
所有工具涉及服务器操作时,通过堡垒机(`sxn@10.212.189.210:2222`)执行:
|
||||
- 统一使用 `v2_ops.py`(jumpserver-V2 技能)
|
||||
- 详见 `docs/04-运维文档/部署运维/11-堡垒机运维工具.md`
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
| 文档 | 位置 |
|
||||
|------|------|
|
||||
| 标准故障排查手册 | `docs/04-运维文档/部署运维/00-标准故障排查手册.md` |
|
||||
| 堡垒机运维工具 | `docs/04-运维文档/部署运维/11-堡垒机运维工具.md` |
|
||||
| 项目管理 SOP | `docs/07-项目管理/IT智能服务台-标准作业流程SOP.md` |
|
||||
| Nginx 基线配置 | `docs/04-运维文档/部署运维/01-nginx-prod-baseline-20260708.conf` |
|
||||
|
||||
---
|
||||
|
||||
> **维护说明**: 新增工具时请同步更新本 README。archive/ 中的脚本仅供历史参考,不保证可用性。
|
||||
@@ -0,0 +1,19 @@
|
||||
import re
|
||||
|
||||
with open(
|
||||
r'D:\资料\03-项目开发\wecom_it_smart_desk\backend\app\api\auth_qrcode.py',
|
||||
'r',
|
||||
encoding='utf-8',
|
||||
) as f:
|
||||
content = f.read()
|
||||
|
||||
match = re.search(r'html = f"""(.+?)"""', content, re.DOTALL)
|
||||
if match:
|
||||
html = match.group(1)
|
||||
print('HTML length:', len(html))
|
||||
out_path = r'D:\资料\03-项目开发\wecom_it_smart_desk\tmp_scan_html.html'
|
||||
with open(out_path, 'w', encoding='utf-8') as out:
|
||||
out.write(html)
|
||||
print('Saved to', out_path)
|
||||
else:
|
||||
print('HTML template not found')
|
||||
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
快速 base64 上传脚本 — 大分块版本(已废弃)
|
||||
将本地文件通过 JumpServer PTY base64 通道上传到远程服务器
|
||||
使用 8000 字符/块(远大于 jms_ops.py 的 500 字符),大幅减少命令数
|
||||
|
||||
⚠️ 此脚本已废弃,请使用 jumpserver-V2 的 v2_ops.py upload 命令(psftp 通道)
|
||||
"""
|
||||
import sys
|
||||
import base64
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
# 添加 jms_ops.py 所在目录
|
||||
SKILL_DIR = Path(r"C:\Users\simon\.workbuddy\skills\jumpserver-ops\scripts")
|
||||
sys.path.insert(0, str(SKILL_DIR))
|
||||
|
||||
# 导入 jms_ops 中的核心函数
|
||||
from jms_ops import get_connection_tokens, PlinkSession
|
||||
|
||||
|
||||
def fast_upload(local_path: str, remote_path: str, chunk_size: int = 8000):
|
||||
"""大分块 base64 上传"""
|
||||
local_file = Path(local_path)
|
||||
if not local_file.exists():
|
||||
print(f"ERROR: file not found: {local_path}")
|
||||
return False
|
||||
|
||||
local_data = local_file.read_bytes()
|
||||
local_md5 = hashlib.md5(local_data).hexdigest()
|
||||
b64_data = base64.b64encode(local_data).decode("ascii")
|
||||
|
||||
# 分块
|
||||
chunks = [b64_data[i:i+chunk_size] for i in range(0, len(b64_data), chunk_size)]
|
||||
total_chunks = len(chunks)
|
||||
|
||||
print(f"File: {local_file.name}")
|
||||
print(f"Size: {len(local_data)} bytes")
|
||||
print(f"Base64: {len(b64_data)} chars")
|
||||
print(f"Chunks: {total_chunks} x {chunk_size} chars")
|
||||
print(f"MD5: {local_md5}")
|
||||
print(f"Target: {remote_path}")
|
||||
print()
|
||||
|
||||
# 获取 token + 启动会话
|
||||
tokens = get_connection_tokens(1)
|
||||
if not tokens:
|
||||
print("ERROR: failed to get connection tokens")
|
||||
return False
|
||||
|
||||
token_id, token_secret = tokens[0]
|
||||
session = PlinkSession(f"JMS-{token_id}", token_secret)
|
||||
if not session.connect():
|
||||
print("ERROR: failed to connect session")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 1. 清空目标文件
|
||||
print("Clearing target file...")
|
||||
session.run_command(f"> {remote_path}", timeout=5)
|
||||
|
||||
# 2. 逐块追加
|
||||
for i, chunk in enumerate(chunks):
|
||||
# 用 printf 避免 echo 的换行符问题
|
||||
cmd = f"printf '%s' '{chunk}' >> {remote_path}.b64"
|
||||
r = session.run_command(cmd, timeout=10)
|
||||
if not r["success"]:
|
||||
print(f" FAIL chunk {i+1}/{total_chunks}")
|
||||
return False
|
||||
|
||||
# 进度报告
|
||||
if (i+1) % 20 == 0 or (i+1) == total_chunks:
|
||||
pct = (i+1) * 100 // total_chunks
|
||||
print(f" [{pct:3d}%] chunk {i+1}/{total_chunks}")
|
||||
|
||||
# 3. base64 解码
|
||||
print(f"\nDecoding base64 -> {remote_path}...")
|
||||
r = session.run_command(f"base64 -d {remote_path}.b64 > {remote_path}", timeout=30)
|
||||
if not r["success"]:
|
||||
print(f" WARN decode result: {r}")
|
||||
|
||||
# 4. 验证大小
|
||||
print("Verifying size...")
|
||||
r = session.run_command(f"wc -c < {remote_path}", timeout=5)
|
||||
if r["success"]:
|
||||
remote_size_str = r["output"].strip()
|
||||
remote_size = int(remote_size_str) if remote_size_str.isdigit() else -1
|
||||
if remote_size == len(local_data):
|
||||
print(f" OK size match: {remote_size} bytes")
|
||||
else:
|
||||
print(f" SIZE MISMATCH: local={len(local_data)}, remote={remote_size}")
|
||||
return False
|
||||
else:
|
||||
print(f" WARN cannot verify size: {r}")
|
||||
return True # 仍然认为成功
|
||||
|
||||
# 5. MD5 验证
|
||||
print("Verifying MD5...")
|
||||
r = session.run_command(f"md5sum {remote_path}", timeout=10)
|
||||
if r["success"]:
|
||||
remote_md5 = r["output"].split()[0]
|
||||
if remote_md5 == local_md5:
|
||||
print(f" OK MD5 match: {remote_md5}")
|
||||
else:
|
||||
print(f" MD5 MISMATCH: local={local_md5}, remote={remote_md5}")
|
||||
# 大小匹配但 MD5 不匹配,可能是 PTY 换行符问题
|
||||
print(" (size matches, trying gzip test instead)")
|
||||
r2 = session.run_command(f"gzip -t {remote_path} 2>&1 && echo GZIP_OK || echo GZIP_FAIL", timeout=10)
|
||||
if r2["success"] and "GZIP_OK" in r2["output"]:
|
||||
print(" OK gzip integrity test passed")
|
||||
# 清理临时文件
|
||||
session.run_command(f"rm -f {remote_path}.b64", timeout=5)
|
||||
return True
|
||||
else:
|
||||
print(f" GZIP FAIL: {r2}")
|
||||
return False
|
||||
else:
|
||||
print(f" WARN cannot verify MD5")
|
||||
|
||||
# 6. 清理临时文件
|
||||
session.run_command(f"rm -f {remote_path}.b64", timeout=5)
|
||||
print("\nUpload complete!")
|
||||
return True
|
||||
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description="Fast base64 upload via JumpServer PTY")
|
||||
parser.add_argument("local", help="Local file path")
|
||||
parser.add_argument("remote", help="Remote file path")
|
||||
parser.add_argument("--chunk-size", type=int, default=8000, help="Chunk size in chars (default: 8000)")
|
||||
args = parser.parse_args()
|
||||
|
||||
success = fast_upload(args.local, args.remote, args.chunk_size)
|
||||
sys.exit(0 if success else 1)
|
||||
@@ -0,0 +1,181 @@
|
||||
"""在容器内运行,渲染 scan 端点的实际 HTML 输出"""
|
||||
import sys
|
||||
sys.path.insert(0, '/app')
|
||||
|
||||
# 模拟 f-string 中的变量
|
||||
user_name = "测试用户"
|
||||
jsapi_signature = "test_sig_abc123"
|
||||
jsapi_timestamp = 1752105600
|
||||
jsapi_nonce = "test_nonce_xyz"
|
||||
jsapi_appid = "ww_test_corp_id"
|
||||
current_url = "https://itsupport.servyou.com.cn/api/auth_qrcode/scan?code=test&state=test"
|
||||
|
||||
html = f"""<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录成功 - IT智能服务台</title>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: linear-gradient(135deg, #07C160 0%, #06AD56 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; }}
|
||||
.card {{ background: rgba(255,255,255,0.95); border-radius: 20px; padding: 48px 32px; max-width: 360px; width: 100%; text-align: center; box-shadow: 0 20px 60px rgba(0,0,0,0.3); }}
|
||||
.check {{ width: 64px; height: 64px; margin: 0 auto 16px; }}
|
||||
.title {{ color: #1f2937; font-size: 24px; font-weight: 600; margin-bottom: 8px; }}
|
||||
.subtitle {{ color: #6b7280; font-size: 14px; margin-bottom: 24px; }}
|
||||
.status {{ display: inline-flex; align-items: center; gap: 6px; background: #dcfce7; color: #166534; padding: 10px 20px; border-radius: 50px; font-size: 14px; font-weight: 500; }}
|
||||
.footer {{ margin-top: 20px; color: #9ca3af; font-size: 12px; }}
|
||||
.back-btn {{ display: none; margin-top: 20px; padding: 12px 32px; background: #07C160; color: white; border: none; border-radius: 50px; font-size: 16px; font-weight: 500; cursor: pointer; }}
|
||||
.debug {{ margin-top: 16px; color: #6b7280; font-size: 11px; line-height: 1.6; word-break: break-all; text-align: left; background: #f3f4f6; padding: 10px 12px; border-radius: 8px; }}
|
||||
.debug b {{ color: #07C160; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<svg class="check" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="32" cy="32" r="30" fill="#07C160" stroke="#06AD56" stroke-width="4"/>
|
||||
<path d="M20 32l8 8 16-16" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<h1 class="title">登录成功</h1>
|
||||
<div class="status">已自动确认登录</div>
|
||||
<p class="subtitle">你好,{user_name}<br>请返回电脑端查看</p>
|
||||
<button class="back-btn" id="backBtn" onclick="manualClose()">点击返回企微</button>
|
||||
<div class="footer">页面即将自动关闭 · 税友集团</div>
|
||||
<div class="debug" id="debugInfo">
|
||||
<b>签名:</b> {'成功' if jsapi_signature else '未生成'}<br>
|
||||
<b>URL:</b> {current_url}<br>
|
||||
<b>状态:</b> <span id="jsStatus">JS加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function() {{
|
||||
var statusEl = document.getElementById('jsStatus');
|
||||
var btnEl = document.getElementById('backBtn');
|
||||
var startTime = Date.now();
|
||||
var tried = {{}};
|
||||
|
||||
function setStatus(msg) {{
|
||||
if (statusEl) statusEl.textContent = msg + ' (' + (Date.now() - startTime) + 'ms)';
|
||||
}}
|
||||
|
||||
function showBtn() {{
|
||||
if (btnEl) btnEl.style.display = 'inline-block';
|
||||
}}
|
||||
|
||||
function tryClose(forceShowBtn) {{
|
||||
setStatus('尝试关闭');
|
||||
|
||||
if (!tried.wxClose && typeof wx !== 'undefined' && wx.closeWindow) {{
|
||||
tried.wxClose = true;
|
||||
try {{
|
||||
setStatus('wx.closeWindow');
|
||||
wx.closeWindow();
|
||||
return true;
|
||||
}} catch(e) {{ setStatus('wx.closeWindow失败:' + (e.message || e)); }}
|
||||
}}
|
||||
|
||||
if (!tried.wxInvoke && typeof wx !== 'undefined' && wx.invoke) {{
|
||||
tried.wxInvoke = true;
|
||||
try {{
|
||||
setStatus('wx.invoke closeWindow');
|
||||
wx.invoke('closeWindow', {{}}, function(){{}});
|
||||
return true;
|
||||
}} catch(e) {{ setStatus('wx.invoke失败:' + (e.message || e)); }}
|
||||
}}
|
||||
|
||||
if (!tried.jsBridge && typeof WeixinJSBridge !== 'undefined' && WeixinJSBridge.call) {{
|
||||
tried.jsBridge = true;
|
||||
try {{
|
||||
setStatus('WeixinJSBridge.closeWindow');
|
||||
WeixinJSBridge.call('closeWindow');
|
||||
return true;
|
||||
}} catch(e) {{ setStatus('JSBridge失败:' + (e.message || e)); }}
|
||||
}}
|
||||
|
||||
if (!tried.windowClose) {{
|
||||
tried.windowClose = true;
|
||||
try {{
|
||||
setStatus('window.close');
|
||||
window.close();
|
||||
return true;
|
||||
}} catch(e) {{}}
|
||||
}}
|
||||
|
||||
if (!tried.historyBack) {{
|
||||
tried.historyBack = true;
|
||||
try {{
|
||||
setStatus('history.back');
|
||||
history.back();
|
||||
return true;
|
||||
}} catch(e) {{}}
|
||||
}}
|
||||
|
||||
if (forceShowBtn) {{
|
||||
setStatus('无法自动关闭,请手动返回');
|
||||
showBtn();
|
||||
}}
|
||||
return false;
|
||||
}}
|
||||
|
||||
function manualClose() {{
|
||||
tryClose(true);
|
||||
}}
|
||||
|
||||
var checkCount = 0;
|
||||
var maxChecks = 50;
|
||||
var interval = setInterval(function() {{
|
||||
checkCount++;
|
||||
var hasWx = typeof wx !== 'undefined';
|
||||
var hasBridge = typeof WeixinJSBridge !== 'undefined';
|
||||
setStatus('检测中 wx=' + hasWx + ' bridge=' + hasBridge + ' count=' + checkCount);
|
||||
|
||||
if (hasWx || hasBridge) {{
|
||||
clearInterval(interval);
|
||||
setStatus('已检测到关闭API,1秒后尝试关闭');
|
||||
setTimeout(function() {{
|
||||
tryClose(true);
|
||||
}}, 1000);
|
||||
return;
|
||||
}}
|
||||
|
||||
if (checkCount >= maxChecks) {{
|
||||
clearInterval(interval);
|
||||
setStatus('未检测到API,直接尝试关闭');
|
||||
tryClose(true);
|
||||
}}
|
||||
}}, 100);
|
||||
|
||||
setTimeout(function() {{
|
||||
showBtn();
|
||||
}}, 3000);
|
||||
}})();
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
# 保存渲染后的 HTML
|
||||
with open('/tmp/test_scan.html', 'w', encoding='utf-8') as f:
|
||||
f.write(html)
|
||||
|
||||
print(f"HTML rendered: {len(html)} chars")
|
||||
print("Saved to /tmp/test_scan.html")
|
||||
|
||||
# 检查 script 部分
|
||||
import re
|
||||
script_match = re.search(r'<script>(.+?)</script>', html, re.DOTALL)
|
||||
if script_match:
|
||||
js = script_match.group(1)
|
||||
print(f"\nJS section: {len(js)} chars")
|
||||
# 检查是否有 {{ 残留(f-string 未正确渲染)
|
||||
if '{{' in js:
|
||||
print("ERROR: Found unrendered {{ in JS!")
|
||||
for i, line in enumerate(js.split('\n')):
|
||||
if '{{' in line:
|
||||
print(f" Line {i+1}: {line.strip()[:80]}")
|
||||
else:
|
||||
print("OK: No unrendered braces in JS")
|
||||
|
||||
# 打印前 20 行 JS
|
||||
print("\nFirst 20 lines of rendered JS:")
|
||||
for i, line in enumerate(js.split('\n')[:20]):
|
||||
print(f" {i+1}: {line}")
|
||||
@@ -0,0 +1,64 @@
|
||||
# Dify 一键部署脚本(简化版)
|
||||
|
||||
由于完整版 Dify 依赖较多服务,提供一个简化版本
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 方式1:使用官方一键部署(推荐)
|
||||
|
||||
```bash
|
||||
# Linux/Mac
|
||||
curl -L https://dify.ai/install.sh | bash
|
||||
|
||||
# Windows (使用 PowerShell)
|
||||
irm https://dify.ai/install.ps1 | iex
|
||||
```
|
||||
|
||||
### 方式2:手动部署简化版
|
||||
|
||||
创建一个简化版的 docker-compose.yml:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
services:
|
||||
api:
|
||||
image: langgenius/dify-api:latest
|
||||
ports:
|
||||
- "8081:8081"
|
||||
environment:
|
||||
- SECRET_KEY=dify-secret-key
|
||||
- DB_USERNAME=postgres
|
||||
- DB_PASSWORD=dify123
|
||||
- DB_HOST=10.0.0.1 # 远程 PostgreSQL
|
||||
- REDIS_HOST=10.0.0.2 # 远程 Redis
|
||||
|
||||
web:
|
||||
image: langgenius/dify-web:latest
|
||||
ports:
|
||||
- "8080:3000"
|
||||
```
|
||||
|
||||
### 方式3:使用在线 Dify 服务
|
||||
|
||||
生产环境已有 Dify 服务(内网可访问):
|
||||
- 地址:http://yw-dify.dc.servyou-it.com/
|
||||
|
||||
---
|
||||
|
||||
## 本地开发建议
|
||||
|
||||
由于本地部署 AI 服务资源需求大,建议:
|
||||
|
||||
1. **开发测试时**:使用 Mock 数据(已实现)
|
||||
2. **集成测试时**:连接生产 Dify(需内网)
|
||||
3. **完整部署时**:在服务器上部署
|
||||
|
||||
---
|
||||
|
||||
## 快速验证 Dify API
|
||||
|
||||
```powershell
|
||||
# 测试生产 Dify
|
||||
curl -X GET 'http://yw-dify.dc.servyou-it.com/console/api/workspaces' \
|
||||
-H 'Authorization: Bearer YOUR-API-KEY'
|
||||
```
|
||||
@@ -0,0 +1,350 @@
|
||||
# 会议室预定-小鱼易联终端 部署指南
|
||||
|
||||
> **日期**: 2026-07-11
|
||||
> **版本**: v2.0(新增报修+指南+二维码+移动端适配)
|
||||
> **代码状态**: 待部署
|
||||
> **预估部署时间**: 45-60 分钟
|
||||
|
||||
---
|
||||
|
||||
## 一、部署前置条件
|
||||
|
||||
### 1.1 企微配置
|
||||
- [x] 企微会议室 Secret 已申请
|
||||
- [x] 企微会议室已在管理后台创建
|
||||
- [ ] 确认会议室 `meetingroom_id` 列表
|
||||
|
||||
### 1.2 服务器环境
|
||||
- [ ] PostgreSQL 可用(需执行 Alembic 迁移 050 + 051)
|
||||
- [ ] Redis 可用(会议室缓存依赖)
|
||||
- [ ] Nginx 可用(终端前端静态文件 + API 代理)
|
||||
- [ ] Docker Compose 可用
|
||||
|
||||
### 1.3 终端设备
|
||||
- [ ] 确认小鱼易联终端型号(NE90/NE60 支持 H5 应用,NE2005 需二维码降级)
|
||||
- [ ] 终端浏览器支持 WebSocket + ES6
|
||||
- [ ] 小鱼管理后台可访问(配置 H5 应用入口)
|
||||
|
||||
---
|
||||
|
||||
## 二、部署步骤
|
||||
|
||||
### Step 1: 数据库迁移
|
||||
|
||||
```bash
|
||||
# 进入后端容器执行迁移(050 会议室基础表 + 051 报修+指南表)
|
||||
docker compose exec backend alembic upgrade head
|
||||
|
||||
# 验证新表
|
||||
docker compose exec backend python -c "
|
||||
from app.database import engine
|
||||
from sqlalchemy import inspect
|
||||
insp = inspect(engine)
|
||||
tables = insp.get_table_names()
|
||||
print('terminal_room_bindings' in tables) # 应为 True(050)
|
||||
print('meetingroom_guide' in tables) # 应为 True(051)
|
||||
print('meetingroom_repair' in tables) # 应为 True(051)
|
||||
"
|
||||
```
|
||||
|
||||
### Step 2: 环境变量配置
|
||||
|
||||
在 `.env` 中添加:
|
||||
```bash
|
||||
# 终端页面基础URL(用于NE2005二维码生成)
|
||||
TERMINAL_BASE_URL=https://itsupport.servyou.com.cn/itterminal/
|
||||
```
|
||||
|
||||
在 `docker-compose.yml` 的 backend environment 中已添加:
|
||||
```yaml
|
||||
- TERMINAL_BASE_URL=${TERMINAL_BASE_URL:-https://itsupport.servyou.com.cn/itterminal/}
|
||||
```
|
||||
|
||||
重启后端:
|
||||
```bash
|
||||
docker compose up -d backend
|
||||
```
|
||||
|
||||
### Step 3: 后端验证
|
||||
|
||||
```bash
|
||||
# 验证会议室 API(已存在)
|
||||
curl -sk https://localhost/itportal/meetingroom/list
|
||||
|
||||
# 验证报修 API(新增)
|
||||
curl -sk -X POST https://localhost/itportal/meetingroom/repair \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"terminal_sn":"TEST-SN","meetingroom_id":1,"meetingroom_name":"测试","device_type":"projector","fault_description":"测试报修"}'
|
||||
|
||||
# 验证指南 API(新增)
|
||||
curl -sk https://localhost/itportal/meetingroom/guides
|
||||
|
||||
# 验证二维码 API(新增)
|
||||
curl -sk https://localhost/itportal/meetingroom/terminal/TEST-SN/qrcode -o /tmp/qr.png
|
||||
file /tmp/qr.png # 应为 PNG image
|
||||
```
|
||||
|
||||
### Step 4: 终端前端构建与部署
|
||||
|
||||
```bash
|
||||
# 1. 本地构建
|
||||
cd frontend-terminal
|
||||
npm install # 安装新增的 qrcode 依赖
|
||||
npm run build
|
||||
|
||||
# 2. 打包
|
||||
tar -czf terminal-dist.tar.gz dist/
|
||||
|
||||
# 3. 上传到服务器(通过堡垒机)
|
||||
python C:\Users\simon\.workbuddy\skills\jumpserver-V2\scripts\v2_ops.py \
|
||||
upload ./terminal-dist.tar.gz /tmp/terminal-dist.tar.gz
|
||||
|
||||
# 4. 服务器解压
|
||||
cd /opt/wecom-it-desk/frontend-terminal/
|
||||
rm -rf dist # 删除旧 dist(bind mount 铁律:rm后重建必须重启容器)
|
||||
tar -xzf /tmp/terminal-dist.tar.gz
|
||||
```
|
||||
|
||||
### Step 5: Nginx 配置更新
|
||||
|
||||
`nginx/nginx.conf` 已更新,新增以下 location(两个 server 块均已添加):
|
||||
|
||||
```nginx
|
||||
# 小鱼终端大屏 — /itterminal/
|
||||
location /itterminal/ {
|
||||
alias /usr/share/nginx/html/itterminal/;
|
||||
index index.html;
|
||||
try_files $uri /itterminal/index.html;
|
||||
}
|
||||
|
||||
# 会议室 API — /itportal/meetingroom/
|
||||
# 必须在 /itportal/ 静态文件之前匹配(nginx 最长前缀优先)
|
||||
location /itportal/meetingroom/ {
|
||||
proxy_pass http://backend_api;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
```
|
||||
|
||||
`docker-compose.yml` nginx volumes 已添加:
|
||||
```yaml
|
||||
- ./frontend-terminal/dist:/usr/share/nginx/html/itterminal:ro
|
||||
```
|
||||
|
||||
重启 Nginx:
|
||||
```bash
|
||||
docker compose restart nginx
|
||||
```
|
||||
|
||||
### Step 6: 终端前端验证
|
||||
|
||||
```bash
|
||||
# 验证终端页面可访问
|
||||
curl -sk https://localhost/itterminal/ | head -5
|
||||
|
||||
# 验证 API 代理(通过 nginx 访问后端)
|
||||
curl -sk https://localhost/itportal/meetingroom/guides
|
||||
|
||||
# 验证静态资源
|
||||
curl -sk -o /dev/null -w "%{http_code}" https://localhost/itterminal/assets/index-*.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、小鱼管理后台 H5 应用配置
|
||||
|
||||
### 3.1 支持型号确认
|
||||
|
||||
| 型号 | H5 应用支持 | 配置方式 |
|
||||
|------|------------|---------|
|
||||
| NE90 | ✅ 支持 | 小鱼管理后台配置 H5 应用入口 |
|
||||
| NE60 | ✅ 支持 | 同上 |
|
||||
| NE20 | ✅ 支持 | 同上 |
|
||||
| AE2060 | ✅ 支持 | 同上 |
|
||||
| ME55S | ⚠️ 部分固件支持 | 需确认固件版本 |
|
||||
| NE2005 | ❌ 不支持 | 使用二维码降级方案 |
|
||||
|
||||
### 3.2 H5 应用入口配置(NE90/NE60/NE20/AE2060)
|
||||
|
||||
1. 登录**小鱼易联管理后台**(`https://mt.xylink.com`)
|
||||
2. 进入 **应用管理 > 自定义应用**
|
||||
3. 创建新应用:
|
||||
- **应用名称**: `IT智能服务台`
|
||||
- **应用类型**: H5 网页应用
|
||||
- **访问地址**: `https://itsupport.servyou.com.cn/itterminal/{终端SN}/`
|
||||
- **展示方式**: 终端主界面快捷入口
|
||||
4. 推送应用到目标终端
|
||||
5. 在终端上验证 H5 应用可正常打开
|
||||
|
||||
### 3.3 终端 SN 绑定
|
||||
|
||||
在管理后台(`/itadmin/`)的终端绑定页面中:
|
||||
1. 添加终端 SN 与会议室的绑定关系
|
||||
2. 每个终端 SN 对应一个企微会议室 ID
|
||||
3. 绑定后终端页面自动加载对应会议室状态
|
||||
|
||||
### 3.4 NE2005 二维码降级方案
|
||||
|
||||
对于不支持 H5 应用的 NE2005 终端:
|
||||
|
||||
1. **生成二维码**:
|
||||
```bash
|
||||
# 通过 API 生成终端访问二维码
|
||||
curl -sk https://itsupport.servyou.com.cn/itportal/meetingroom/terminal/{SN}/qrcode -o qr.png
|
||||
```
|
||||
或直接在浏览器访问:
|
||||
`https://itsupport.servyou.com.cn/itportal/meetingroom/terminal/{SN}/qrcode`
|
||||
|
||||
2. **打印二维码**:将二维码打印为贴纸(建议尺寸 10×10cm)
|
||||
|
||||
3. **张贴二维码**:贴在 NE2005 终端的显眼位置
|
||||
|
||||
4. **用户使用流程**:
|
||||
- 用户用企业微信/微信扫描二维码
|
||||
- 手机浏览器打开终端页面(移动端自适应布局)
|
||||
- 可查看会议室状态、预定、报修、查看指南
|
||||
|
||||
---
|
||||
|
||||
## 四、部署后验证
|
||||
|
||||
### 4.1 功能验证清单
|
||||
|
||||
| # | 验证项 | 验证方法 | 预期结果 |
|
||||
|---|--------|---------|---------|
|
||||
| 1 | 会议室列表 | `GET /itportal/meetingroom/list` | 返回会议室列表 |
|
||||
| 2 | 会议室状态 | `GET /itportal/meetingroom/{id}/status` | 返回当前状态 |
|
||||
| 3 | 预定会议室 | `POST /itportal/meetingroom/book` | 创建预定成功 |
|
||||
| 4 | 终端绑定 | `GET /itportal/meetingroom/terminal/{sn}/binding` | 返回绑定信息 |
|
||||
| 5 | 终端WS | `WS /ws/terminal/{sn}` | 终端状态实时推送 |
|
||||
| 6 | **设备报修** | `POST /itportal/meetingroom/repair` | 创建工单+通知管理员 |
|
||||
| 7 | **指南列表** | `GET /itportal/meetingroom/guides` | 返回指南列表(5条种子) |
|
||||
| 8 | **指南按类型** | `GET /itportal/meetingroom/guides/projector` | 返回投影仪指南 |
|
||||
| 9 | **终端二维码** | `GET /itportal/meetingroom/terminal/{sn}/qrcode` | 返回PNG图片 |
|
||||
| 10 | 终端前端 | 浏览器打开 `/itterminal/{sn}/` | 深色主题大屏页面 |
|
||||
| 11 | **报修页面** | 终端点击"设备报修"按钮 | 显示报修表单 |
|
||||
| 12 | **指南页面** | 终端点击"操作指南"按钮 | 显示指南列表+二维码 |
|
||||
| 13 | **移动端适配** | 手机访问 `/itterminal/{sn}/` | 垂直布局自适应 |
|
||||
| 14 | 扫码登录 | 终端扫码 | 企微扫码登录成功 |
|
||||
| 15 | 状态同步 | 终端修改状态 → H5 实时更新 | WS 推送正常 |
|
||||
|
||||
### 4.2 Redis 缓存验证
|
||||
|
||||
```bash
|
||||
# 会议室 token
|
||||
redis-cli -a $REDIS_PASSWORD get wecom:meetingroom_access_token
|
||||
|
||||
# 会议室列表缓存
|
||||
redis-cli -a $REDIS_PASSWORD get meetingroom:room_list
|
||||
|
||||
# 状态缓存
|
||||
redis-cli -a $REDIS_PASSWORD get "meetingroom:status:{room_id}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、回滚方案
|
||||
|
||||
### 5.1 数据库回滚
|
||||
```bash
|
||||
# 回退迁移 051(报修+指南表)
|
||||
docker compose exec backend alembic downgrade -1
|
||||
|
||||
# 完全回退(含 050)
|
||||
docker compose exec backend alembic downgrade -2
|
||||
```
|
||||
|
||||
### 5.2 后端回滚
|
||||
```bash
|
||||
# 恢复 .py 文件(bind mount 自动生效)
|
||||
git checkout HEAD~1 -- backend/app/api/meetingroom.py
|
||||
git checkout HEAD~1 -- backend/app/services/repair_service.py
|
||||
git checkout HEAD~1 -- backend/app/services/meetingroom_service.py
|
||||
git checkout HEAD~1 -- backend/app/models/meetingroom_guide.py
|
||||
git checkout HEAD~1 -- backend/app/models/meetingroom_repair.py
|
||||
git checkout HEAD~1 -- backend/app/schemas/meetingroom.py
|
||||
git checkout HEAD~1 -- backend/app/config.py
|
||||
docker compose restart backend
|
||||
```
|
||||
|
||||
### 5.3 前端回滚
|
||||
```bash
|
||||
# 恢复旧 dist
|
||||
mv /opt/wecom-it-desk/frontend-terminal/dist /opt/wecom-it-desk/frontend-terminal/dist.bak
|
||||
# 恢复上一版本
|
||||
docker compose restart nginx
|
||||
```
|
||||
|
||||
### 5.4 Nginx 配置回滚
|
||||
```bash
|
||||
# 移除 /itterminal/ 和 /itportal/meetingroom/ location 块
|
||||
docker compose restart nginx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、新增功能说明
|
||||
|
||||
### 6.1 设备报修流程
|
||||
|
||||
```
|
||||
终端用户点击"设备报修"
|
||||
→ 选择设备类型(投影仪/视频会议/空调/桌椅/网络/其他)
|
||||
→ 填写故障描述
|
||||
→ 提交报修
|
||||
→ 后端创建:
|
||||
1. MeetingroomRepair 记录
|
||||
2. Conversation(IT工单会话,状态=排队,紧急度=3)
|
||||
3. Message(系统消息,含故障描述)
|
||||
4. 企微消息通知管理员
|
||||
5. WS广播给在线坐席
|
||||
→ 终端显示"报修已提交"
|
||||
→ 3秒后自动返回状态页
|
||||
```
|
||||
|
||||
### 6.2 操作指南双模式
|
||||
|
||||
- **终端展示**:指南的 `brief` 字段在终端大屏上直接显示简要操作步骤
|
||||
- **二维码详情**:指南的 `detail_url` 生成二维码,用户手机扫码查看完整文档
|
||||
- 管理员可在数据库中添加/修改指南内容
|
||||
|
||||
### 6.3 NE2005 降级流程
|
||||
|
||||
```
|
||||
NE2005 终端(不支持H5应用)
|
||||
→ 管理员生成二维码贴纸(API: /itportal/meetingroom/terminal/{sn}/qrcode)
|
||||
→ 用户手机扫码
|
||||
→ 手机浏览器打开终端页面(移动端自适应)
|
||||
→ 功能与终端大屏一致(状态/预定/报修/指南)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、配置参数速查
|
||||
|
||||
| 参数 | 默认值 | 环境变量 |
|
||||
|------|--------|---------|
|
||||
| 会议室Token TTL | 6900s | - |
|
||||
| 会议室列表缓存 | 600s | `MEETINGROOM_CACHE_TTL_ROOMS` |
|
||||
| 预定缓存 | 30s | `MEETINGROOM_CACHE_TTL_BOOKING` |
|
||||
| 状态缓存 | 10s | `MEETINGROOM_CACHE_TTL_STATUS` |
|
||||
| WS重连次数 | 5 | - |
|
||||
| WS降级轮询间隔 | 10s | - |
|
||||
| **终端页面URL** | `https://itsupport.servyou.com.cn/itterminal/` | `TERMINAL_BASE_URL` |
|
||||
| **二维码尺寸** | 300px | API 参数 `size` |
|
||||
| **二维码缓存** | 1小时 | HTTP `Cache-Control` |
|
||||
|
||||
---
|
||||
|
||||
## 八、已知限制
|
||||
|
||||
1. **企微API时间限制**: 会议室预定查询范围限制 31 天
|
||||
2. **终端身份**: 管理操作需扫码登录,报修支持匿名提交
|
||||
3. **WS重连**: 终端断线后自动重连(指数退避),超过 5 次降级为轮询(10s间隔)
|
||||
4. **NE2005**: 不支持 H5 应用,仅能通过二维码扫码方式使用
|
||||
5. **指南管理**: 当前通过数据库直接管理,后续可增加管理后台 UI
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,291 @@
|
||||
# Token多IP异常检测 - 技术设计文档
|
||||
|
||||
> **任务ID**: 待分配
|
||||
> **模块**: 威胁检测
|
||||
> **优先级**: P1
|
||||
> **ATT&CK**: T1078 (有效账户), T1552 (非安全凭据)
|
||||
|
||||
---
|
||||
|
||||
## 1. 需求概述
|
||||
|
||||
### 1.1 业务背景
|
||||
|
||||
当前系统已废弃密码登录,仅支持企微OAuth2/扫码登录。Token是用户身份的唯一凭证,当Token被泄露后,攻击者可能从不同IP使用同一Token访问系统。本功能旨在检测此类异常行为。
|
||||
|
||||
### 1.2 功能目标
|
||||
|
||||
- 记录每个Token使用的IP地址
|
||||
- 检测同一Token在短时间内被多个IP使用的情况
|
||||
- 触发告警通知安全管理员
|
||||
- 可选:自动禁用异常Token
|
||||
|
||||
---
|
||||
|
||||
## 2. 技术方案
|
||||
|
||||
### 2.1 架构设计
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 检测流程 │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 用户API请求 │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────┐ │
|
||||
│ │ record_token_ip │ ← 每次请求记录IP │
|
||||
│ │ (埋点) │ │
|
||||
│ └────────┬────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────┐ │
|
||||
│ │ Redis Set │ ← token_ips:{hash} │
|
||||
│ │ IP集合(1h TTL) │ │
|
||||
│ └────────┬────────┘ │
|
||||
│ │ │
|
||||
│ ▼ 定时任务(每分钟) │
|
||||
│ ┌─────────────────┐ │
|
||||
│ │ detect_anomaly │ ← 扫描异常Token │
|
||||
│ │ (定时任务) │ │
|
||||
│ └────────┬────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────┐ │
|
||||
│ │ send_alert │ ← 企微机器人告警 │
|
||||
│ └─────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 数据结构
|
||||
|
||||
#### Redis Key设计
|
||||
|
||||
| Key格式 | 类型 | TTL | 说明 |
|
||||
|---------|------|-----|------|
|
||||
| `token_ips:{token_hash}` | Set | 3600秒 | 记录Token使用的IP集合 |
|
||||
| `token_ips:alerted:{token_hash}` | String | 3600秒 | 已告警标记,避免重复 |
|
||||
|
||||
#### Token存储(现有)
|
||||
|
||||
| Key格式 | 类型 | 说明 |
|
||||
|---------|------|------|
|
||||
| `user:token:{token}` | JSON | 用户信息,含employee_id |
|
||||
|
||||
### 2.3 接口设计
|
||||
|
||||
#### 2.3.1 记录Token使用IP (埋点)
|
||||
|
||||
```python
|
||||
# 在 token_service.py 中增加
|
||||
async def record_token_ip(token: str, ip: str):
|
||||
"""
|
||||
记录Token使用的IP地址
|
||||
|
||||
Args:
|
||||
token: 用户Token
|
||||
ip: 客户端IP (X-Forwarded-For 或 request.client.host)
|
||||
"""
|
||||
import hashlib
|
||||
token_hash = hashlib.sha256(token.encode()).hexdigest()
|
||||
|
||||
redis = await get_redis()
|
||||
key = f"token_ips:{token_hash}"
|
||||
|
||||
# 添加IP到Set (自动去重)
|
||||
redis.sadd(key, ip)
|
||||
# 设置1小时过期
|
||||
redis.expire(key, 3600)
|
||||
```
|
||||
|
||||
#### 2.3.2 异常检测定时任务
|
||||
|
||||
```python
|
||||
# 在 tasks/token_anomaly_detection.py
|
||||
async def detect_token_anomaly():
|
||||
"""
|
||||
检测Token异常使用
|
||||
|
||||
扫描所有 token_ips:* keys
|
||||
当 IP数量 >= 阈值 时触发告警
|
||||
"""
|
||||
# 配置
|
||||
THRESHOLD = 3 # IP数量阈值
|
||||
WINDOW_SECONDS = 3600 # 时间窗口
|
||||
|
||||
redis = await get_redis()
|
||||
alerted_key_prefix = "token_ips:alerted:"
|
||||
|
||||
# 扫描所有 token_ips:* keys
|
||||
async for key in redis.scan_iter("token_ips:*"):
|
||||
# 跳过 alerted keys
|
||||
if key.startswith(alerted_key_prefix):
|
||||
continue
|
||||
|
||||
token_hash = key.replace("token_ips:", "")
|
||||
ip_count = await redis.scard(key)
|
||||
|
||||
if ip_count >= THRESHOLD:
|
||||
# 检查是否已告警
|
||||
alerted_key = f"{alerted_key_prefix}{token_hash}"
|
||||
if await redis.get(alerted_key):
|
||||
continue # 已告警,跳过
|
||||
|
||||
# 获取用户信息
|
||||
token = await redis.get(f"user:token:{token_hash}")
|
||||
if token:
|
||||
user_data = json.loads(token)
|
||||
employee_id = user_data.get("employee_id")
|
||||
|
||||
# 发送告警
|
||||
await send_security_alert(
|
||||
title="Token异常告警",
|
||||
content=f"员工 {employee_id} 的Token被 {ip_count} 个IP使用\nToken: {token_hash[:8]}..."
|
||||
)
|
||||
|
||||
# 标记已告警
|
||||
await redis.setex(alerted_key, WINDOW_SECONDS, "1")
|
||||
```
|
||||
|
||||
#### 2.3.3 获取客户端IP
|
||||
|
||||
```python
|
||||
def get_client_ip(request) -> str:
|
||||
"""获取客户端真实IP"""
|
||||
# 优先从 X-Forwarded-For 获取
|
||||
forwarded = request.headers.get("X-Forwarded-For")
|
||||
if forwarded:
|
||||
return forwarded.split(",")[0].strip()
|
||||
|
||||
# 降级到 request.client.host
|
||||
return request.client.host if request.client else ""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 配置项
|
||||
|
||||
### 3.1 环境变量
|
||||
|
||||
| 变量名 | 类型 | 默认值 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| `TOKEN_ANOMALY_THRESHOLD` | int | 3 | 触发告警的IP数量阈值 |
|
||||
| `TOKEN_ANOMALY_WINDOW` | int | 3600 | 时间窗口(秒) |
|
||||
| `TOKEN_ANOMALY_AUTO_DISABLE` | bool | false | 是否自动禁用Token |
|
||||
| `CONTENT_AUDIT_WEBHOOK` | string | - | 企微机器人webhook(现有) |
|
||||
|
||||
---
|
||||
|
||||
## 4. 告警内容
|
||||
|
||||
### 4.1 告警模板
|
||||
|
||||
```json
|
||||
{
|
||||
"msgtype": "markdown",
|
||||
"markdown": {
|
||||
"content": "🔴 **Token异常告警**\n\n"
|
||||
"> 员工ID: {employee_id}\n"
|
||||
"> 异常Token: {token_hash[:8]}...\n"
|
||||
"> IP数量: {ip_count}\n"
|
||||
"> 时间: {timestamp}\n\n"
|
||||
"> **请及时确认是否为本人操作**"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 集成点
|
||||
|
||||
### 5.1 现有组件复用
|
||||
|
||||
| 组件 | 用途 |
|
||||
|------|------|
|
||||
| Redis | IP存储 |
|
||||
| APScheduler | 定时任务 |
|
||||
| content_audit_webhook | 企微告警 |
|
||||
| token_service.py | Token管理 |
|
||||
|
||||
### 5.2 侵入点
|
||||
|
||||
| 文件 | 修改内容 |
|
||||
|------|----------|
|
||||
| `app/services/token_service.py` | 增加 record_token_ip() |
|
||||
| `app/main.py` | 注册定时任务 |
|
||||
| `app/tasks/token_anomaly_detection.py` | 新建检测任务 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 性能与容错
|
||||
|
||||
### 6.1 性能估算
|
||||
|
||||
| 指标 | 估算值 |
|
||||
|------|---------|
|
||||
| Redis存储 | ~50KB (1000活跃Token) |
|
||||
| 定时任务耗时 | < 100ms |
|
||||
| 定时任务间隔 | 60秒 |
|
||||
|
||||
### 6.2 容错设计
|
||||
|
||||
- 告警发送失败:记录日志,不阻塞主流程
|
||||
- Redis连接失败:跳过本次检测,下个周期重试
|
||||
- Token不存在:跳过,不影响其他检测
|
||||
|
||||
---
|
||||
|
||||
## 7. 测试用例
|
||||
|
||||
### 7.1 单元测试
|
||||
|
||||
| 用例ID | 描述 | 预期结果 |
|
||||
|--------|------|----------|
|
||||
| T001 | 单IP使用Token | 不触发告警 |
|
||||
| T002 | 3个IP使用Token | 触发告警 |
|
||||
| T003 | 5个IP使用Token | 触发告警(严重) |
|
||||
| T004 | 同一IP多次使用 | 不触发告警 |
|
||||
|
||||
### 7.2 集成测试
|
||||
|
||||
| 用例ID | 描述 | 预期结果 |
|
||||
|--------|------|----------|
|
||||
| I001 | 真实Token请求 | IP被记录 |
|
||||
| I002 | 定时任务执行 | 异常Token被检测 |
|
||||
| I003 | 告警发送 | 企微收到消息 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 部署清单
|
||||
|
||||
### 8.1 文件变更
|
||||
|
||||
| 操作 | 文件 |
|
||||
|------|------|
|
||||
| 新增 | `app/tasks/token_anomaly_detection.py` |
|
||||
| 修改 | `app/services/token_service.py` |
|
||||
| 修改 | `app/main.py` |
|
||||
|
||||
### 8.2 配置变更
|
||||
|
||||
| 操作 | 变量 |
|
||||
|------|------|
|
||||
| 新增(可选) | `TOKEN_ANOMALY_THRESHOLD` |
|
||||
| 新增(可选) | `TOKEN_ANOMALY_AUTO_DISABLE` |
|
||||
|
||||
---
|
||||
|
||||
## 9. 回滚方案
|
||||
|
||||
如需回滚:
|
||||
1. 移除定时任务注册 (main.py)
|
||||
2. 删除 record_token_ip() 调用
|
||||
3. Redis keys 会在1小时后自动过期
|
||||
|
||||
---
|
||||
|
||||
> **编制人**: 威胁检测工程师
|
||||
> **日期**: 2026-07-14
|
||||
> **审核人**: 待定
|
||||
@@ -0,0 +1,258 @@
|
||||
# 智能IT支持服务台 — 项目迁移文档
|
||||
**生成时间**:2026-06-06
|
||||
**来源项目**:`C:\Users\simon\wecom_it_smart_desk`
|
||||
**原型文件**:`C:\Users\simon\WorkBuddy\2026-05-21-16-57-26\agent-workspace-v5_3.html`
|
||||
|
||||
---
|
||||
|
||||
## 一、项目概览
|
||||
|
||||
| 项目 | 路径 | 技术栈 |
|
||||
|------|------|---------|
|
||||
| 后端 | `C:\Users\simon\wecom_it_smart_desk\backend` | Python 3.12 + FastAPI + SQLAlchemy |
|
||||
| 前端(坐席工作台) | `C:\Users\simon\wecom_it_smart_desk\frontend-agent` | Vue 3 + TypeScript + Vite + Element Plus + Pinia |
|
||||
| 原型 HTML | `C:\Users\simon\WorkBuddy\2026-05-21-16-57-26\agent-workspace-v5_3.html` | 单文件,v5.3 定版 |
|
||||
| 服务器 | `10.80.0.129` | Docker Compose (postgres + redis + backend + nginx) |
|
||||
|
||||
---
|
||||
|
||||
## 二、锁定的设计决策(迁移后必须遵守)
|
||||
|
||||
### 2.1 原型规范
|
||||
- ✅ **原型 v5.3 已锁定**(`agent-workspace-v5_3.html`),调整样式前必须与用户确认
|
||||
- ✅ 用户偏好**深色科技风 UI**,原型**必须使用中文**
|
||||
- ✅ 所有修改基于现有面板内调整,**不另开新页面**
|
||||
- ✅ 聊天区域应缩小,给侧栏更多空间
|
||||
|
||||
### 2.2 布局架构(三栏)
|
||||
```
|
||||
┌──────────┬────────────────────────┬──────────────┐
|
||||
│ 左栏 │ 中栏 │ 右栏 │
|
||||
│ (240px) │ (flex:1 自适应) │ (320px) │
|
||||
│ │ │ │
|
||||
│ 会话列表 │ UserInfoBar │ AI 智能推荐 │
|
||||
│ 搜索框 │ TroubleshootBar │ 快速回复 │
|
||||
│ 待办事项 │ 消息列表 │ (三层导航) │
|
||||
│ │ ReplyBox (输入框) │ │
|
||||
└──────────┴────────────────────────┴──────────────┘
|
||||
```
|
||||
|
||||
### 2.3 关键交互规则
|
||||
| 功能 | 规则 |
|
||||
|------|------|
|
||||
| 排查步骤栏 | 始终可见(不可收起),位于 UserInfoBar 下方、消息列表上方 |
|
||||
| 全流程图 | 默认收起,通过 `▶` / `▼` 三角图标切换 |
|
||||
| 用户信息栏展开箭头 | 收起 `▶`(向右)→ 展开 `▼`(向下,CSS `rotate(90deg)`) |
|
||||
| AI 推荐 | **仅在右边栏**,不在中间栏内联显示 |
|
||||
| 快速回复 | 三层渐进导航:L1(7列grid) → L2(chip流式) → L3(列表) |
|
||||
| 主题切换 | 浅色(`:root`) / 深色(`[data-theme="dark"]`) 双主题 |
|
||||
| 输入框 | `resize: vertical` 手动拖拽 + `autosize` 最大 8 行 + `max-height: 200px` |
|
||||
|
||||
---
|
||||
|
||||
## 三、原型 v5.3 布局细节(已确认的终版)
|
||||
|
||||
> **文件**:`C:\Users\simon\WorkBuddy\2026-05-21-16-57-26\agent-workspace-v5_3.html`
|
||||
|
||||
### 3.1 中间栏(center-column)DOM 顺序
|
||||
```html
|
||||
<div class="center-column">
|
||||
<div class="chat-view"> <!-- 整个聊天视图容器 -->
|
||||
<div class="user-info-bar"> <!-- 用户信息栏(含6卡片展开详情) -->
|
||||
<div class="user-detail-panel"> <!-- 展开详情(默认收起) -->
|
||||
<div class="troubleshoot-bar" id="tsBar"> <!-- 排查步骤(紧跟用户信息栏) -->
|
||||
<div class="ts-header"> <!-- 标题行:[🔧 排查步骤] [①→②→③→④→⑤] [▶] -->
|
||||
<div class="chat-messages"> <!-- 消息列表 -->
|
||||
<div class="chat-input-area"> <!-- 输入框区域 -->
|
||||
<textarea autosize maxRows=8 resize=vertical>
|
||||
</div> <!-- chat-view 闭合标签 -->
|
||||
<div class="sidebar-right"> <!-- 右边栏(AI推荐 + 快速回复) -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### 3.2 排查步骤栏(ts-header 单行布局)
|
||||
```html
|
||||
<div class="ts-header">
|
||||
<span class="ts-title">🔧 排查步骤</span>
|
||||
<el-select>...</el-select> <!-- 模板选择下拉 -->
|
||||
<div class="ts-path-inline"> <!-- 内联路径步骤 -->
|
||||
<span class="path-step-inline done">① 确认版本</span>
|
||||
<span class="path-arrow-inline">→</span>
|
||||
...
|
||||
</div>
|
||||
<span class="ts-flowchart-toggle">▶</span> <!-- 三角切换图标 -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### 3.3 用户信息栏展开箭头
|
||||
```css
|
||||
.user-info-bar.expanded .expand-icon {
|
||||
transform: rotate(90deg); /* ▶ 旋转后变成 ▼ */
|
||||
}
|
||||
```
|
||||
```html
|
||||
<span class="expand-icon">▶</span> <!-- 收起时向右,展开时向下 -->
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、Vue 3 项目 — 已修改文件清单
|
||||
|
||||
### 4.1 核心视图
|
||||
| 文件 | 修改内容 |
|
||||
|------|---------|
|
||||
| `src/views/Workspace.vue` | `onMounted` 添加 `fetchConversations()` + 自动选第一个会话;`assistantVisible` 默认 `true` |
|
||||
| `src/components/chat/ChatArea.vue` | 移除 `AiRecommendInline`;`TroubleshootBar` 位置调整到 `UserInfoBar` 下方;移除重复的第一个 `TroubleshootBar` |
|
||||
|
||||
### 4.2 聊天组件
|
||||
| 文件 | 修改内容 |
|
||||
|------|---------|
|
||||
| `src/components/chat/UserInfoBar.vue` | 展开箭头 `▶` → `▼`(rotate 90deg),之前方向反了 |
|
||||
| `src/components/chat/TroubleshootBar.vue` | 路径步骤合并到标题行;展开按钮简化为三角 `▶`/`▼` |
|
||||
| `src/components/chat/ReplyBox.vue` | `autosize` 上限 4→8 行;`resize: none` → `resize: vertical`;支持手动拖拽 |
|
||||
| `src/components/chat/AiRecommendInline.vue` | 已从 `ChatArea.vue` 移除引用(AI推荐仅保留右边栏) |
|
||||
|
||||
### 4.3 右边栏组件
|
||||
| 文件 | 修改内容 |
|
||||
|------|---------|
|
||||
| `src/components/assistant/AiAssistantPanel.vue` | 右边栏主容器(AI推荐 + 快速回复) |
|
||||
| `src/components/assistant/QuickReplyPanel.vue` | 三层渐进导航,L1 七类,数据源 `src/data/qrData.ts` |
|
||||
| `src/components/assistant/RiskAlert.vue` | 风险告警组件 |
|
||||
| `src/components/assistant/OperationSteps.vue` | 操作步骤组件 |
|
||||
|
||||
### 4.4 状态管理(Stores)
|
||||
| 文件 | 修改内容 |
|
||||
|------|---------|
|
||||
| `src/stores/conversation.ts` | `fetchConversations()` DEV 环境 Mock 兜底;自动选中第一个会话 |
|
||||
| `src/stores/agent.ts` | `initAuth()` 检查 localStorage token |
|
||||
| `src/stores/todo.ts` | 待办事项 Mock 数据 |
|
||||
| `src/composables/useWebSocket.ts` | WebSocket 直连后端 `ws://localhost:8000/ws/{agentId}` |
|
||||
|
||||
### 4.5 样式
|
||||
| 文件 | 修改内容 |
|
||||
|------|---------|
|
||||
| `src/styles/global.css` | `.reply-box .el-textarea__inner { max-height: 200px; }`;`.message-list-scroll { flex: 1; overflow-y: auto; }` |
|
||||
|
||||
### 4.6 数据
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `src/data/qrData.ts` | 快速回复 180 条(7大类 × 28子类),TypeScript 类型化 |
|
||||
|
||||
---
|
||||
|
||||
## 五、后端关键修改(参考)
|
||||
|
||||
> 路径:`C:\Users\simon\wecom_it_smart_desk\backend`
|
||||
|
||||
| 文件 | 修改内容 |
|
||||
|------|---------|
|
||||
| `app/services/session_service.py` | `get_conversations()` 排序改为 Python 侧(SQLite 不支持 JSONB 操作符) |
|
||||
| `app/main.py` | 添加 `dify_conversation_id` 列(PostgreSQL) |
|
||||
| `app/models/todo_item.py` | TodoItem 模型 |
|
||||
| `app/models/troubleshooting_template.py` | TroubleshootingTemplate 模型 |
|
||||
|
||||
---
|
||||
|
||||
## 六、已修复的关键 Bug(迁移后注意)
|
||||
|
||||
### Bug 1:会话列表不显示 / 页面空白
|
||||
**根因**:`Workspace.vue` `onMounted` 未调用 `fetchConversations()`,`currentConversation` 始终为 `null`,`ChatArea` 不渲染。
|
||||
**修复**:`onMounted` 中添加 `await conversationStore.fetchConversations()` + 自动选中第一个会话。
|
||||
|
||||
### Bug 2:AI 推荐同时出现在中间栏和右边栏
|
||||
**根因**:`ChatArea.vue` 中引入了 `<AiRecommendInline />` 组件。
|
||||
**修复**:移除 `ChatArea.vue` 中的 `AiRecommendInline` 模板、import、ref、`onAiRecommend` 快捷键绑定。
|
||||
|
||||
### Bug 3:原型 HTML 右边栏显示在中栏内部
|
||||
**根因**:`<div class="chat-view">` 缺少 `</div>` 闭合标签,浏览器把 `sidebar-right` 解析为 `center-column` 的子元素。
|
||||
**修复**:在 `chat-input-area` 关闭后补 `</div>` 闭合 `chat-view`。
|
||||
|
||||
### Bug 4:排查步骤栏出现两个(上下重复)
|
||||
**根因**:`ChatArea.vue` 模板里有两个 `<TroubleshootBar />`(一个在 UserInfoBar 下方,一个在 ReplyBox 下方)。
|
||||
**修复**:删除 ReplyBox 下方的重复 `<TroubleshootBar />`。
|
||||
|
||||
### Bug 5:用户信息栏展开箭头方向反了
|
||||
**根因**:收起时 `▼`,展开时 `▲`(CSS `rotate(180deg)`)。
|
||||
**修复**:收起 `▶`,展开 `▼`(CSS `rotate(90deg)`)。
|
||||
|
||||
### Bug 6:会话列表 API 500 错误
|
||||
**根因**:`session_service.py` 用 SQL 侧 `case()` + `tags["hand_raise"].as_boolean()` 排序,SQLite 不支持 JSONB 操作符。
|
||||
**修复**:排序改为 Python 侧 `_sort_key()` 函数实现。
|
||||
|
||||
---
|
||||
|
||||
## 七、服务器部署状态
|
||||
|
||||
| 项目 | 状态 |
|
||||
|------|------|
|
||||
| 服务器地址 | `10.80.0.129` |
|
||||
| Docker Compose 容器 | `postgres`, `redis`, `backend`, `nginx` |
|
||||
| nginx | 已修复 301 重定向和 API 双重前缀问题 |
|
||||
| PostgreSQL | 已添加 `dify_conversation_id` 列 |
|
||||
| Redis | `it-desk-redis` 容器中运行中 |
|
||||
|
||||
---
|
||||
|
||||
## 八、知识库
|
||||
|
||||
| 项目 | 状态 |
|
||||
|------|------|
|
||||
| 数据源 | `IT支持知识库2026-4-24.docx` |
|
||||
| 快速回复条数 | 180 条(7大类 × 28子类) |
|
||||
| 前端数据文件 | `frontend-agent/src/data/qrData.ts` |
|
||||
| 原型数据文件 | `agent-workspace-v5_3.html` 内联(因 file:// CORS 限制) |
|
||||
|
||||
---
|
||||
|
||||
## 九、迁移 checklist
|
||||
|
||||
### 备份这些文件/目录:
|
||||
```
|
||||
# 原型
|
||||
C:\Users\simon\WorkBuddy\2026-05-21-16-57-26\agent-workspace-v5_3.html
|
||||
|
||||
# 前端项目(整体复制)
|
||||
C:\Users\simon\wecom_it_smart_desk\frontend-agent\
|
||||
|
||||
# 后端项目(整体复制)
|
||||
C:\Users\simon\wecom_it_smart_desk\backend\
|
||||
|
||||
# 项目记忆(WorkBuddy)
|
||||
C:\Users\simon\WorkBuddy\2026-05-21-16-57-26\.workbuddy\memory\
|
||||
```
|
||||
|
||||
### 新项目中需要重新配置:
|
||||
- [ ] `.env` 文件(数据库连接、Redis、Dify API Key)
|
||||
- [ ] `vite.config.ts` 中的 proxy 端口(当前 `5174`)
|
||||
- [ ] Docker Compose 环境变量
|
||||
- [ ] 服务器部署配置(`10.80.0.129`)
|
||||
|
||||
### 新项目中需要重新执行:
|
||||
```bash
|
||||
# 后端
|
||||
cd backend
|
||||
python -m venv venv
|
||||
venv\Scripts\activate
|
||||
pip install -r requirements.txt
|
||||
# 初始化数据库
|
||||
alembic upgrade head # 或 python -m app.db.init_db
|
||||
|
||||
# 前端
|
||||
cd frontend-agent
|
||||
npm install
|
||||
npm run dev # → http://localhost:5174/itagent/workspace
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、待完成任务(迁移后继续)
|
||||
|
||||
- [ ] 重启 dev server 验证所有修改已生效(当前修改已保存,需 Ctrl+C → npm run dev)
|
||||
- [ ] 后端 AI 集成(Dify)代码修复(T02 任务)
|
||||
- [ ] 前端方案评估(T03/T04)
|
||||
- [ ] 企微回调服务器对接
|
||||
- [ ] 生产环境部署验证
|
||||
|
||||
---
|
||||
|
||||
*此文档由 WorkBuddy 自动生成,汇总了 2026-05-21 至 2026-06-06 的所有工作内容。*
|
||||
@@ -0,0 +1,531 @@
|
||||
# 智能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 Compose(4容器: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)
|
||||
|
||||
### 2.3 jumpserver-V2 工具(推荐)
|
||||
|
||||
推荐使用 jumpserver-V2 工具进行远程命令执行和文件传输。该工具一次浏览器登录后,后续通过登录缓存 cookies 免登录复用。
|
||||
|
||||
```bash
|
||||
# 进入 skill 目录
|
||||
cd C:\Users\simon\.workbuddy\skills\jumpserver-V2\scripts
|
||||
|
||||
# 首次登录(弹出浏览器,手动填写 OTP)
|
||||
python v2_ops.py login
|
||||
|
||||
# 之后免登录执行命令
|
||||
python v2_ops.py exec "hostname"
|
||||
python v2_ops.py exec "uptime && docker ps"
|
||||
|
||||
# 文件上传(自动选择最优方式:<100KB 用 base64,>=100KB 或 >15s 用 elFinder)
|
||||
python v2_ops.py upload local_file.txt /tmp/remote_file.txt
|
||||
|
||||
# 文件下载
|
||||
python v2_ops.py download /tmp/server_file.txt ./local_file.txt
|
||||
|
||||
# 巡检命令(只读)
|
||||
python v2_ops.py inspect
|
||||
```
|
||||
|
||||
> **注意**:首次使用会弹出浏览器完成 JumpServer 登录和 OTP 验证,后续调用会自动复用会话(服务端 401 时自动回退重新登录)。
|
||||
|
||||
### 2.4 为什么不能直接用 SSH/SCP?
|
||||
|
||||
| 方式 | 支持情况 | 原因 |
|
||||
|------|---------|------|
|
||||
| SSH ProxyJump | ❌ 不支持 | JumpServer 不兼容标准 SSH 代理协议 |
|
||||
| SCP 直连堡垒机 | ❌ 不支持 | 需要 OTP 验证码,SCP 不支持交互式输入 |
|
||||
| SSH 直连目标服务器 | ❌ 不支持 | 目标服务器仅对 JumpServer 开放 SSH 访问 |
|
||||
| jumpserver-V2 | ✅ 推荐 | 自动化处理 OTP 和 Connection Token |
|
||||
|
||||
---
|
||||
|
||||
## 三、文件传输(通过堡垒机)
|
||||
|
||||
### 3.1 jumpserver-V2 上传(推荐)
|
||||
|
||||
```bash
|
||||
# 上传文件到目标服务器 /tmp 目录
|
||||
python v2_ops.py upload deploy.zip /tmp/deploy.zip
|
||||
|
||||
# 上传到其他目录
|
||||
python v2_ops.py upload config.conf /opt/wecom-it-desk/config.conf
|
||||
```
|
||||
|
||||
### 3.2 备用方案
|
||||
|
||||
如果 jumpserver-V2 不可用,可以考虑:
|
||||
|
||||
```bash
|
||||
# 方式A:通过互联网可访问的存储服务(推荐)
|
||||
# - 先把文件传到能通过互联网访问的存储(如:对象存储、临时文件分享服务)
|
||||
# - 目标服务器通过 curl/wget 下载
|
||||
|
||||
# 方式B:通过堡垒机手动中转
|
||||
# - 使用 jumpserver-V2 手动执行分步操作
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、部署步骤(完整流程)
|
||||
|
||||
### 步骤 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. 应成功登录并进入聊天页面
|
||||
|
||||
---
|
||||
|
||||
## 五、部署文件结构
|
||||
|
||||
### 5.1 服务器目录结构
|
||||
|
||||
```
|
||||
/opt/wecom-it-desk/ # 项目根目录(服务器)
|
||||
├── docker-compose.yml # Docker Compose 配置(4容器)
|
||||
├── .env # 环境变量(已配置)
|
||||
├── .env.example # 环境变量模板
|
||||
├── deploy.sh # 一键部署脚本
|
||||
├── nginx/
|
||||
│ ├── nginx.conf # Nginx 配置(反代 + 静态文件)
|
||||
│ └── ssl/ # SSL 证书
|
||||
├── html/ # 前端静态文件(Nginx 挂载点)
|
||||
│ ├── itdesk/ # H5 员工端 (/itdesk/)
|
||||
│ ├── itagent/ # 坐席工作台 (/itagent/)
|
||||
│ ├── itadmin/ # 管理后台 (/itadmin/)
|
||||
│ └── itportal/ # 统一入口 (/itportal/)
|
||||
└── backend/ # 后端源码(不用于生产,仅开发参考)
|
||||
```
|
||||
|
||||
### 5.2 前端部署位置说明
|
||||
|
||||
| 端 | URL 路径 | 服务器目录 | Nginx 容器挂载点 |
|
||||
|----|---------|-----------|----------------|
|
||||
| 员工端 H5 | `/itdesk/` | `/opt/wecom-it-desk/html/itdesk/` | `/usr/share/nginx/html/itdesk` |
|
||||
| 坐席工作台 | `/itagent/` | `/opt/wecom-it-desk/html/itagent/` | `/usr/share/nginx/html/itagent` |
|
||||
| 管理后台 | `/itadmin/` | `/opt/wecom-it-desk/html/itadmin/` | `/usr/share/nginx/html/itadmin` |
|
||||
| 统一入口 | `/itportal/` | `/opt/wecom-it-desk/html/itportal/` | `/usr/share/nginx/html/itportal` |
|
||||
|
||||
### 5.3 前端部署步骤
|
||||
|
||||
```bash
|
||||
# 1. 本地构建
|
||||
cd frontend-agent
|
||||
npm run build
|
||||
|
||||
# 2. 打包(排除 node_modules)
|
||||
cd dist
|
||||
zip -r ../agent-v1.x.zip *
|
||||
|
||||
# 3. 上传到服务器 /tmp/
|
||||
# 4. SSH 到服务器解压
|
||||
sudo rm -rf /opt/wecom-it-desk/html/itagent
|
||||
sudo mkdir -p /opt/wecom-it-desk/html/itagent
|
||||
sudo unzip -o /tmp/agent-v1.x.zip -d /opt/wecom-it-desk/html/itagent
|
||||
|
||||
# 5. 重启 Nginx 容器
|
||||
docker restart wecom_it_nginx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、Python 依赖管理
|
||||
|
||||
### 6.1 依赖说明
|
||||
|
||||
后端 Python 依赖在 `backend/requirements.txt` 中声明,构建 Docker 镜像时会自动安装。
|
||||
|
||||
**新增依赖处理流程:**
|
||||
|
||||
1. **开发环境**:更新 `backend/requirements.txt`
|
||||
2. **打包部署**:确保 `requirements.txt` 已包含新依赖
|
||||
3. **生产环境**:重建后端镜像
|
||||
|
||||
```bash
|
||||
# 重建后端镜像(会自动安装 requirements.txt 中的所有依赖)
|
||||
cd /opt/wecom-it-desk
|
||||
docker compose build backend
|
||||
docker compose up -d backend
|
||||
```
|
||||
|
||||
### 6.2 常见依赖问题
|
||||
|
||||
| 问题 | 症状 | 解决方法 |
|
||||
|------|------|---------|
|
||||
| 缺少依赖 | `ModuleNotFoundError` | 重建后端镜像:`docker compose build backend` |
|
||||
| 手动安装 | 容器内临时安装 | `docker exec wecom_it_backend pip install <package>` |
|
||||
|
||||
---
|
||||
|
||||
## 七、常用运维命令
|
||||
|
||||
在服务器上 `/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,100 @@
|
||||
# 本地 AI 服务部署指南(Dify + RAGFlow)
|
||||
|
||||
> 更新日期:2026-07-06
|
||||
|
||||
## 系统要求
|
||||
|
||||
| 服务 | 最低内存 | 推荐内存 |
|
||||
|------|----------|----------|
|
||||
| Dify (CPU) | 8GB | 16GB |
|
||||
| RAGFlow (CPU) | 8GB | 16GB |
|
||||
| 两者同时 | 16GB | 32GB |
|
||||
|
||||
**当前可用内存:约 7.4GB**
|
||||
|
||||
---
|
||||
|
||||
## 方案一:仅部署 Dify(推荐)
|
||||
|
||||
### 步骤1:停止本地不需要的容器
|
||||
```powershell
|
||||
# 停止开发环境(如果不需要)
|
||||
docker stop dev_wecom_backend dev_wecom_postgres dev_wecom_redis
|
||||
```
|
||||
|
||||
### 步骤2:部署 Dify (CPU版)
|
||||
```powershell
|
||||
cd D:\资料\03-项目开发\wecom_it_smart_desk
|
||||
mkdir dify && cd dify
|
||||
|
||||
# 下载 Docker Compose
|
||||
curl -o docker-compose.yml https://github.com/langgenius/dify/raw/main/docker/docker-compose.middleware.yaml
|
||||
|
||||
# 启动
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### 步骤3:访问
|
||||
- Web UI: http://localhost:8080
|
||||
- API: http://localhost:8081
|
||||
- 默认管理员: admin@dify.local / admin
|
||||
|
||||
---
|
||||
|
||||
## 方案二:仅部署 RAGFlow(CPU版)
|
||||
|
||||
### 步骤1:停止本地不需要的容器
|
||||
```powershell
|
||||
docker stop dev_wecom_backend dev_wecom_postgres dev_wecom_redis
|
||||
```
|
||||
|
||||
### 步骤2:部署 RAGFlow
|
||||
```powershell
|
||||
cd D:\资料\03-项目开发\wecom_it_smart_desk
|
||||
mkdir ragflow && cd ragflow
|
||||
|
||||
# 下载配置
|
||||
curl -o docker-compose.yml https://raw.githubusercontent.com/infiniflow/ragflow/main/docker/docker-compose.yml
|
||||
curl -o .env https://raw.githubusercontent.com/infiniflow/ragflow/main/docker/.env
|
||||
|
||||
# 启动(使用 CPU profile)
|
||||
docker compose --profile cpu up -d
|
||||
```
|
||||
|
||||
### 步骤3:访问
|
||||
- Web UI: http://localhost:9380
|
||||
- API: http://localhost:9380/api
|
||||
- 默认管理员: root / infiniflow
|
||||
|
||||
---
|
||||
|
||||
## 方案三:同时部署(需要16GB+内存)
|
||||
|
||||
1. 先停止开发容器
|
||||
2. 部署 Dify(会占用约 4-6GB)
|
||||
3. 等待稳定后部署 RAGFlow(会占用约 4-6GB)
|
||||
|
||||
---
|
||||
|
||||
## 生产环境已配置
|
||||
|
||||
| 服务 | 地址 | 用途 |
|
||||
|------|------|------|
|
||||
| Dify 生产 | http://yw-dify.dc.servyou-it.com/ | AI 对话、工作流 |
|
||||
| RAGFlow 生产 | http://10.80.0.85:8080/ | 知识库管理 |
|
||||
|
||||
---
|
||||
|
||||
## 本地配置后端连接
|
||||
|
||||
修改 `backend/.env.dev`:
|
||||
|
||||
```bash
|
||||
# Dify
|
||||
DIFY_BASE_URL=http://localhost:8081
|
||||
DIFY_API_KEY=your-api-key
|
||||
|
||||
# RAGFlow
|
||||
RAGFLOW_BASE_URL=http://localhost:9380
|
||||
RAGFLOW_API_KEY=your-api-key
|
||||
```
|
||||
@@ -0,0 +1,40 @@
|
||||
# 本地 AI 服务部署记录
|
||||
|
||||
> 日期: 2026-07-05
|
||||
|
||||
## 当前状态
|
||||
|
||||
### 拉取中的镜像
|
||||
|
||||
| 镜像 | 大小 | 预计时间 |
|
||||
|------|------|----------|
|
||||
| ollama/ollama:latest | ~2GB | 5-10分钟 |
|
||||
| langgenius/dify-api:latest | ~5-10GB | 30-60分钟 |
|
||||
|
||||
### 部署方案
|
||||
|
||||
#### 方案1: Ollama (轻量)
|
||||
```bash
|
||||
docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama:latest
|
||||
# 然后运行模型
|
||||
docker exec ollama ollama run llama3:8b
|
||||
```
|
||||
|
||||
#### 方案2: Dify (完整)
|
||||
需要完整的 docker-compose,包含:
|
||||
- dify-api
|
||||
- dify-web
|
||||
- dify-worker
|
||||
- postgres
|
||||
- redis
|
||||
- minio
|
||||
- nginx
|
||||
|
||||
## 本地开发环境
|
||||
|
||||
| 服务 | 地址 |
|
||||
|------|------|
|
||||
| H5 端 | http://localhost:5176/itdesk/ |
|
||||
| 坐席端 | http://localhost:5175/itagent/ |
|
||||
| 管理后台 | http://localhost:5175/itadmin/ |
|
||||
| 后端 API | http://localhost:8000 |
|
||||
@@ -0,0 +1,136 @@
|
||||
# 蓝绿部署指南
|
||||
|
||||
## 概述
|
||||
|
||||
蓝绿部署是一种零停机部署策略,通过维护两套完全相同的运行环境(Blue 和 Green),实现快速切换和回滚。
|
||||
|
||||
## 架构
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ Nginx │
|
||||
│ (流量入口) │
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌──────────────┴──────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
┌────────────────┐ ┌────────────────┐
|
||||
│ Blue 环境 │ │ Green 环境 │
|
||||
│ (当前活动) │ │ (待验证) │
|
||||
│ backend:8000 │ │ backend_green: │
|
||||
│ │ │ 5002 │
|
||||
└────────────────┘ └────────────────┘
|
||||
│ │
|
||||
└──────────────┬──────────────┘
|
||||
│
|
||||
┌──────────────┴──────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
┌────────────────┐ ┌────────────────┐
|
||||
│ PostgreSQL │ ←──→ │ Redis │
|
||||
│ (共享) │ │ (共享) │
|
||||
└────────────────┘ └────────────────┘
|
||||
```
|
||||
|
||||
## 文件说明
|
||||
|
||||
| 文件 | 位置 | 说明 |
|
||||
|------|------|------|
|
||||
| docker-compose-green.yml | /opt/wecom-it-desk/ | Green 环境配置 |
|
||||
| switch-blue-green.sh | /opt/wecom-it-desk/ | 切换脚本 |
|
||||
| nginx.conf | /opt/wecom-it-desk/nginx/ | Nginx 配置(包含 upstream) |
|
||||
|
||||
## 部署步骤
|
||||
|
||||
### 1. 部署 Green 环境
|
||||
|
||||
```bash
|
||||
cd /opt/wecom-it-desk
|
||||
|
||||
# 构建并启动 Green 环境
|
||||
docker-compose -f docker-compose-green.yml up -d
|
||||
|
||||
# 验证 Green 环境健康
|
||||
curl http://localhost:5002/health
|
||||
```
|
||||
|
||||
### 2. 测试 Green 环境
|
||||
|
||||
通过端口 5080 访问 Green 环境进行测试:
|
||||
- H5: http://服务器IP:5080/itdesk/
|
||||
- 坐席: http://服务器IP:5080/itagent/
|
||||
- 管理后台: http://服务器IP:5080/itadmin/
|
||||
|
||||
### 3. 切换流量到 Green
|
||||
|
||||
```bash
|
||||
# 方法一:使用切换脚本
|
||||
./switch-blue-green.sh to-green
|
||||
|
||||
# 方法二:手动修改 Nginx 配置
|
||||
sed -i 's/wecom_it_backend:8000/wecom_it_backend_green:8000/' /opt/wecom-it-desk/nginx/nginx.conf
|
||||
docker restart wecom_it_nginx
|
||||
```
|
||||
|
||||
### 4. 验证切换
|
||||
|
||||
```bash
|
||||
# 检查 Nginx upstream 配置
|
||||
grep -A1 'upstream backend_api' /opt/wecom-it-desk/nginx/nginx.conf
|
||||
|
||||
# 测试 API
|
||||
curl https://itsupport.servyou.com.cn/api/v1/system/health
|
||||
```
|
||||
|
||||
### 5. 回滚(如有问题)
|
||||
|
||||
```bash
|
||||
# 方法一:使用切换脚本
|
||||
./switch-blue-green.sh to-blue
|
||||
|
||||
# 方法二:手动修改
|
||||
sed -i 's/wecom_it_backend_green:8000/wecom_it_backend:8000/' /opt/wecom-it-desk/nginx/nginx.conf
|
||||
docker restart wecom_it_nginx
|
||||
```
|
||||
|
||||
## 端口说明
|
||||
|
||||
| 端口 | 服务 | 说明 |
|
||||
|------|------|------|
|
||||
| 80/443 | Nginx (Blue) | 生产入口 |
|
||||
| 5002 | Backend (Green) | Green 后端 API |
|
||||
| 5080 | Nginx (Green) | Green 测试入口 |
|
||||
| 5443 | Nginx (Green) | Green HTTPS |
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **数据库共享**:Blue 和 Green 共用同一个 PostgreSQL 和 Redis
|
||||
2. **文件上传**:上传的文件保存在挂载目录,不受切换影响
|
||||
3. **会话影响**:切换后用户可能需要重新登录
|
||||
4. **WebSocket**:切换后现有 WebSocket 连接会断开
|
||||
|
||||
## 快速命令汇总
|
||||
|
||||
```bash
|
||||
# 查看状态
|
||||
docker ps
|
||||
|
||||
# 查看 Green 日志
|
||||
docker logs wecom_it_backend_green
|
||||
|
||||
# 切换到 Green
|
||||
sed -i 's/wecom_it_backend:8000/wecom_it_backend_green:8000/' /opt/wecom-it-desk/nginx/nginx.conf
|
||||
docker restart wecom_it_nginx
|
||||
|
||||
# 切换回 Blue
|
||||
sed -i 's/wecom_it_backend_green:8000/wecom_it_backend:8000/' /opt/wecom-it-desk/nginx/nginx.conf
|
||||
docker restart wecom_it_nginx
|
||||
|
||||
# 停止 Green 环境
|
||||
docker-compose -f docker-compose-green.yml down
|
||||
```
|
||||
|
||||
## 更新日志
|
||||
|
||||
- 2026-07-05: 初始版本
|
||||
Reference in New Issue
Block a user