[REQ-通用-006] 预生产测试通道:三端测试登录入口 + 文档链 + 部署脚本
- 三端 Login.vue(坐席/管理/H5)新增「测试账号登录」面板:探测 /api/dev/health 决定可见性,公网 403 自动隐藏,免企微扫码登录(token 写入对应 localStorage 键) - 新增 REQ-通用-006 文档链五件套:PRD / 技术方案 / 任务说明书 / 测试用例 / 部署方案(product-doc-standard 规范) - 版本迭代总览追加 v5.1(预生产测试通道)行 - 部署辅助脚本:nginx /api/dev/ 内网闸门注入、H5 版本化 v20260808→v20260811 升级 部署已落地预生产(10.90.5.110):公网 /api/dev/* 403、内网 200、三端登录页新 hash 在线。
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""REQ-通用-006 预生产测试通道 — nginx /api/dev/ 内网闸门注入脚本(幂等)"""
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
NGINX_CONF = "/opt/wecom-it-desk/nginx/nginx.conf"
|
||||
BAK = "/opt/wecom-it-desk/nginx/nginx.conf.bak-testch-20260811"
|
||||
|
||||
GATE_BLOCK = """ # ============================================================
|
||||
# 预生产测试通道 (REQ-通用-006) - nginx 内网闸门
|
||||
# 仅内网可访问 /api/dev/*,公网 403。部署后须双向 curl 验证
|
||||
# ============================================================
|
||||
location /api/dev/ {
|
||||
allow 10.0.0.0/8;
|
||||
allow 172.16.0.0/12;
|
||||
allow 192.168.0.0/16;
|
||||
deny all;
|
||||
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;
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def main() -> int:
|
||||
with open(NGINX_CONF, encoding="utf-8") as f:
|
||||
src = f.read()
|
||||
|
||||
if "location /api/dev/" in src:
|
||||
print("ALREADY_EXISTS")
|
||||
return 0
|
||||
|
||||
anchor = "location /api/ {\n"
|
||||
idx = src.find(anchor)
|
||||
if idx < 0:
|
||||
print("ANCHOR_NOT_FOUND")
|
||||
return 1
|
||||
|
||||
shutil.copy2(NGINX_CONF, BAK)
|
||||
new_src = src[:idx] + GATE_BLOCK + src[idx:]
|
||||
with open(NGINX_CONF, "w", encoding="utf-8") as f:
|
||||
f.write(new_src)
|
||||
print("INSERTED_OK")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,67 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""REQ-通用-006 预生产测试通道 — H5 版本化升级 v20260808 -> v20260811(幂等)"""
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
NGINX_CONF = "/opt/wecom-it-desk/nginx/nginx.conf"
|
||||
BAK = "/opt/wecom-it-desk/nginx/nginx.conf.bak-h5v20260811-20260811"
|
||||
|
||||
NEW_VER = "v20260811"
|
||||
OLD_VER = "v20260808"
|
||||
|
||||
|
||||
def build_ver_block(prefix: str) -> str:
|
||||
return f"""location /{prefix}/{NEW_VER}/ {{
|
||||
alias /usr/share/nginx/html/h5/;
|
||||
index index.html;
|
||||
try_files $uri /{prefix}/{NEW_VER}/index.html;
|
||||
add_header Cache-Control "no-store" always;
|
||||
add_header Strict-Transport-Security "max-age=31536000" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
}}
|
||||
"""
|
||||
|
||||
|
||||
def main() -> int:
|
||||
with open(NGINX_CONF, encoding="utf-8") as f:
|
||||
src = f.read()
|
||||
|
||||
if NEW_VER in src:
|
||||
print("ALREADY_EXISTS")
|
||||
return 0
|
||||
|
||||
shutil.copy2(NGINX_CONF, BAK)
|
||||
|
||||
# 1. 插 /h5/v20260811/ 与 /itservice/v20260811/ 块(在各自 v20260808 块后)
|
||||
for prefix in ("h5", "itservice"):
|
||||
anchor_old = f"location /{prefix}/{OLD_VER}/ {{\n"
|
||||
anchor_tail = "add_header X-Frame-Options \"SAMEORIGIN\" always;\n}\n"
|
||||
i = src.find(anchor_old)
|
||||
if i < 0:
|
||||
print(f"ANCHOR_NOT_FOUND: {prefix}/{OLD_VER}")
|
||||
return 1
|
||||
j = src.find(anchor_tail, i)
|
||||
if j < 0:
|
||||
print(f"TAIL_NOT_FOUND: {prefix}/{OLD_VER}")
|
||||
return 1
|
||||
j += len(anchor_tail)
|
||||
src = src[:j] + build_ver_block(prefix) + src[j:]
|
||||
|
||||
# 2. 生产 server 的 go 302 重指新版本(替换第一次出现的 return 302)
|
||||
for prefix in ("h5", "itservice"):
|
||||
old_ret = f"return 302 /{prefix}/{OLD_VER}/$is_args$args;"
|
||||
new_ret = f"return 302 /{prefix}/{NEW_VER}/$is_args$args;"
|
||||
if old_ret in src:
|
||||
src = src.replace(old_ret, new_ret, 1)
|
||||
else:
|
||||
print(f"GO_RETURN_NOT_FOUND: {prefix}")
|
||||
return 1
|
||||
|
||||
with open(NGINX_CONF, "w", encoding="utf-8") as f:
|
||||
f.write(src)
|
||||
print("H5_VERSION_UPGRADED_OK")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -38,6 +38,7 @@
|
||||
| v1.2.2(Patch) | 2026-07-25 | REQ-AI-001(复杂场景与统一路由) | 02-技术文档/实现配置/AI对话链路全栈改造实施计划-v1.0.md | E2E打字机验证报告(2026-07-08) | - | AI回复打字机逐字显示效果(前端 H5 + Agent 双端) |
|
||||
| v1.2.3(Bugfix) | 2026-07-26 | - | - | - | 04-运维文档/部署运维/00-标准故障排查手册.md (CASE-20260726-01~08) | AI选项交互全链路修复:结构化回复渲染、白屏三连、消息排序、轮询去重、选中状态、Dify超时 |
|
||||
| **v1.2(管理后台 IA 调整)** | 2026-07-28 | PRD-REQ-集成-002-管理后台-v1.0.md (v1.2) | 技术方案-REQ-集成-002-管理后台v1.2-分配模式Tab收编.md | TC-集成-002-分配模式Tab收编.md | 任务说明书-REQ-集成-002-分配模式Tab收编.md | **分配模式收编到坐席管理 Tab**:原 `/admin/assignment-mode` 独立菜单/独立页 → 收编为 `/admin/agents` 第 2 Tab「分配策略」;菜单导航「知识与 AI」8 → 7 项,总菜单数 27 → 26;后端 API 零改动;6 张模式卡片(手动接单 P0 + 轮询/最少活跃 P2 + 加权/技能匹配/优先队列 P3)全部迁移嵌入 Tab 2 |
|
||||
| v5.1(预生产测试通道) | 2026-08-11 | PRD-REQ-通用-006-预生产测试通道-v1.0.md | 技术方案-REQ-通用-006-预生产测试通道-v1.0.md | TC-REQ-通用-006-预生产测试通道.md | DEPLOY-REQ-通用-006-预生产测试通道.md | **预生产测试通道**:10.90.5.110 定位预生产;后端 DEV_MODE=true 启用 /api/dev/*;nginx /api/dev/ 内网白名单闸门(公网 403);三端登录页测试账号入口(内网可见)——支撑自动化测试免企微 |
|
||||
|
||||
## 四,回填来源
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
# PRD-REQ-通用-006-预生产测试通道
|
||||
|
||||
> **版本**: v1.0 | **日期**: 2026-08-11 | **状态**: [待评审]
|
||||
> **作者**: 宋献(产品) | **审核**: —
|
||||
> **需求编号**: REQ-通用-006
|
||||
> **需求类型**: 新增
|
||||
> **关联模块**: 通用(跨端基础设施)
|
||||
> **关联文档**:
|
||||
> - 技术方案: `../../02-技术文档/技术方案-REQ-通用-006-预生产测试通道-v1.0.md`
|
||||
> - 任务说明书: `../../07-项目管理/任务说明书/任务说明书-REQ-通用-006-预生产测试通道.md`
|
||||
> - 测试用例: `../../03-测试文档/03-功能测试用例/TC-REQ-通用-006-预生产测试通道.md`
|
||||
> - 部署方案: `../../04-运维文档/部署运维/DEPLOY-REQ-通用-006-预生产测试通道.md`
|
||||
> - 关联规范: `00-产品开发流程与文档管理规范.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与问题
|
||||
|
||||
### 1.1 业务背景
|
||||
|
||||
IT 智能服务台三端(用户端 H5、坐席端、管理后台)的登录已统一为**企微 OAuth 扫码认证**(见 `PRD-REQ-认证-统一认证与登录-v1.1.md`),系统无账号密码入口。这带来一个工程问题:
|
||||
|
||||
> **自动化测试无法完整执行**——三端登录均需企微扫码/OAuth 交互,脚本无法模拟,导致端到端(E2E)测试链路被阻断。
|
||||
|
||||
### 1.2 环境定位决策(2026-08-11 已确认)
|
||||
|
||||
| 决策项 | 结论 |
|
||||
|--------|------|
|
||||
| 10.90.5.110(itsupport.servyou.com.cn) | 定位为**预生产环境** |
|
||||
| 未来正式生产 | 单独申请**分布式服务资源**(容器平台/K8s),预生产就绪后域名迁移 |
|
||||
| 公网暴露面 | **暂不收敛**,待新生产就绪后一次性迁移(过渡期保持现状) |
|
||||
| 自动化测试落点 | **预生产开启测试通道**(本需求) |
|
||||
| 生产形态 | 容器平台/K8s(本需求不涉及,另立评估) |
|
||||
|
||||
### 1.3 核心矛盾
|
||||
|
||||
「预生产仍公网可达」+「预生产开测试通道」两者叠加,若直接启用 `DEV_MODE=true`,**公网任何人可调用 `/api/dev/login` 获取任意角色(含 admin)的有效 token**——比当前登录页暴露严重一个数量级。因此测试通道必须带**访问控制闸门**,不得裸开。
|
||||
|
||||
---
|
||||
|
||||
## 2. 需求描述
|
||||
|
||||
### 2.1 目标
|
||||
|
||||
在预生产环境提供**仅内网可用的免企微登录通道**,支撑自动化测试(后端 API + 前端 E2E)完整执行,同时保证公网无法触达测试后门。
|
||||
|
||||
### 2.2 范围(In Scope)
|
||||
|
||||
1. 预生产后端启用 `DEV_MODE=true`,挂载既有 `/api/dev/*` 测试端点(login / users / health)
|
||||
2. nginx 新增 `/api/dev/` 内网 IP 白名单闸门(公网 403)
|
||||
3. 三端(坐席/管理/H5)登录页新增「测试账号登录」入口(仅内网可见),调 `/api/dev/login` 获取 token 并注入本地会话
|
||||
|
||||
### 2.3 非目标(Non-goals)
|
||||
|
||||
- ❌ 不新增生产环境的账号密码登录(安全红线,明确排除)
|
||||
- ❌ 不改动公网域名/WAF 映射(过渡期维持现状)
|
||||
- ❌ 不改造现有企微 OAuth 主登录流程(仅新增旁路测试通道)
|
||||
- ❌ 不涉及新生产环境(K8s)的架构改造(另立 REQ 评估)
|
||||
|
||||
---
|
||||
|
||||
## 3. 用户故事
|
||||
|
||||
- **作为**测试工程师,**我希望**在内网访问预生产时能用测试账号一键登录三端,**以便**完整执行自动化测试而无需企微扫码。
|
||||
- **作为**安全管理员,**我希望**公网无法访问任何测试后门接口,**以便**测试通道不成为攻击面。
|
||||
- **作为**运维人员,**我希望**测试通道可一键开关(环境变量/nginx 配置),**以便**新生产上线时可干净撤离。
|
||||
|
||||
---
|
||||
|
||||
## 4. 验收标准
|
||||
|
||||
| # | 验收项 | 判定 |
|
||||
|---|--------|------|
|
||||
| AC-1 | 内网调用 `GET /api/dev/login?userid=dev-agent-001&role=agent` | 返回有效 token(Redis 可校验),HTTP 200 |
|
||||
| AC-2 | 公网调用 `/api/dev/*` | HTTP 403(nginx 闸门拦截),且非 `/api/dev/` 前缀业务接口不受影响 |
|
||||
| AC-3 | 前端测试登录入口(坐席/管理/H5) | 内网可一键登录并进入业务页;登录后业务 API 携带 token 正常响应 |
|
||||
| AC-4 | 既有企微扫码登录 | 不受影响,回归通过 |
|
||||
| AC-5 | 预生产环境配置可追溯 | `DEV_MODE` 注入方式、nginx 闸门配置均落文档,可回滚 |
|
||||
| AC-6 | 生产环境安全隔离 | 新生产环境默认 `DEV_MODE=false`,不包含测试通道配置 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 技术约束与依赖
|
||||
|
||||
- 后端 `/api/dev/login` 已存在(`app/api/dev_auth.py`),**零后端开发**,仅配置启用;接口内部有 `_dev_mode_enabled()` 二次校验
|
||||
- 前端 token 存储:坐席 `localStorage.TOKEN_KEY`、H5 `localStorage.h5_token`、管理后台同构——测试登录需复用同一存储键
|
||||
- nginx 闸门依赖内网网段规划(10/8、172.16/12、192.168/16)
|
||||
- 依赖 `jumpserver-V2` 工具链完成预生产配置变更与验证
|
||||
|
||||
---
|
||||
|
||||
## 6. 风险
|
||||
|
||||
| 风险 | 等级 | 缓解 |
|
||||
|------|------|------|
|
||||
| DEV_MODE 误入生产 | 高 | compose 显式注入 + `.dockerignore` 排除 `.env`(已有)+ 上线 checklist 检查 |
|
||||
| 闸门配置错误导致公网可访问 dev 接口 | 高 | 部署后必须公网/内网双向 curl 验证(AC-2) |
|
||||
| 前端测试入口内网判定失效(如内网 IP 变化) | 中 | 判定逻辑与 nginx 闸门同源(内网网段常量),配置化 |
|
||||
| 测试数据污染预生产 | 中 | 测试账号使用独立 userid 前缀(dev-*),与真实账号隔离 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 变更记录
|
||||
|
||||
| 日期 | 版本 | 变更内容 | 变更人 | 变更原因 |
|
||||
|------|------|----------|--------|----------|
|
||||
| 2026-08-11 | v1.0 | 初版 | 宋献 | 预生产测试通道需求确认 |
|
||||
@@ -0,0 +1,162 @@
|
||||
# 技术方案-REQ-通用-006-预生产测试通道
|
||||
|
||||
> **版本**: v1.0 | **日期**: 2026-08-11 | **状态**: [待评审]
|
||||
> **作者**: 宋献(技术) | **审核**: —
|
||||
> **REQ编号**: REQ-通用-006
|
||||
> **关联PRD**: `../01-产品文档/00-产品规划/PRD-REQ-通用-006-预生产测试通道-v1.0.md`
|
||||
> **关联测试**: `../03-测试文档/03-功能测试用例/TC-REQ-通用-006-预生产测试通道.md`
|
||||
> **需了解的现有代码**:
|
||||
> - 后端: `src/backend/app/api/dev_auth.py`(Mock 登录,已存在)
|
||||
> - 后端: `src/backend/app/main.py`(`_is_dev_mode()` + dev 路由挂载)
|
||||
> - 坐席前端: `src/frontend-agent/src/views/Login.vue`、`src/frontend-agent/src/api/auth.ts`、`src/frontend-agent/src/stores/agent.ts`
|
||||
> - 管理前端: `src/frontend-admin/src/views/Login.vue`
|
||||
> - H5前端: `src/frontend-h5/src/`(登录与 token 存储)
|
||||
|
||||
---
|
||||
|
||||
## 1. 现状分析
|
||||
|
||||
### 1.1 认证架构现状
|
||||
|
||||
- 三端统一企微扫码/OAuth(`/api/auth/qrcode` + `/api/auth/oauth2/*`),无账号密码入口
|
||||
- 后端 `/api/agents/login` 已废弃(DEPRECATED),且依赖企微通讯录验证 user_id,不满足"免企微"测试诉求
|
||||
- **既有测试基建**:`/api/dev/login`(`dev_auth.py`)在 `DEV_MODE=true` 时挂载,走真实 TokenService 流程、自动同步 employees 表、带角色预设(user/agent/admin/supervisor/security/多角色);本地 `src/backend/.env` 已 `DEV_MODE=true`,后端测试 conftest 亦 mock 企微
|
||||
|
||||
### 1.2 前端 token 存储机制(已核实)
|
||||
|
||||
| 端 | 存储键 | 用途 |
|
||||
|----|--------|------|
|
||||
| 坐席 | `localStorage.TOKEN_KEY`(store: agent.ts) | 请求拦截器自动附加 Bearer |
|
||||
| H5 | `localStorage.h5_token` | 同上 |
|
||||
| 管理 | 与坐席同构(login 后写入 store) | 同上 |
|
||||
|
||||
→ 测试登录仅需把 `/api/dev/login` 返回的 token 写入对应键,即可进入业务态。
|
||||
|
||||
---
|
||||
|
||||
## 2. 总体设计
|
||||
|
||||
```
|
||||
公网用户 ──► WAF ──► nginx ──┬── /itdesk|/itagent|/itadmin/ 静态页(登录页,含测试入口按钮)
|
||||
├── /api/auth/* 企微扫码主登录(不变)
|
||||
├── /api/dev/* ◄── nginx 闸门:allow 内网网段;deny all
|
||||
│ │
|
||||
│ ▼
|
||||
└── backend (DEV_MODE=true) ── /api/dev/login → TokenService → Redis token
|
||||
```
|
||||
|
||||
**三层防线**:
|
||||
1. **nginx 闸门**(主闸门):`location /api/dev/ { allow 内网; deny all; }` —— 公网直接 403
|
||||
2. **后端二次校验**:`dev_auth.py` 各端点内部 `_dev_mode_enabled()` 再校验(`DEV_MODE=true` 才放行)
|
||||
3. **前端可见性**:测试登录入口仅在内网判定(hostname 非公网域名 / 内网网段探测)时渲染
|
||||
|
||||
---
|
||||
|
||||
## 3. 详细设计
|
||||
|
||||
### 3.1 后端配置(零代码改动)
|
||||
|
||||
预生产 `docker-compose.yml`(`/opt/wecom-it-desk/`)backend 服务 `environment:` 追加:
|
||||
|
||||
```yaml
|
||||
- DEV_MODE=true
|
||||
```
|
||||
|
||||
重启后端容器(`docker compose up -d backend` 或 recreate)后:
|
||||
- `/api/dev/login`、`/api/dev/users`、`/api/dev/health` 挂载
|
||||
- 启动日志出现 `🧪 DEV_MODE 已启用 - Mock OAuth 端点已挂载`
|
||||
|
||||
**生产安全隔离**:新生产 compose **不得**注入 `DEV_MODE`;`.dockerignore` 已排除 `.env`(防止本地 DEV_MODE 进镜像),双保险。
|
||||
|
||||
### 3.2 nginx 闸门(预生产主控)
|
||||
|
||||
在线上 `/opt/wecom-it-desk/nginx/nginx.conf` 生产 server 块 `location /api/` **之前**插入:
|
||||
|
||||
```nginx
|
||||
location /api/dev/ {
|
||||
allow 10.0.0.0/8;
|
||||
allow 172.16.0.0/12;
|
||||
allow 192.168.0.0/16;
|
||||
deny all;
|
||||
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;
|
||||
}
|
||||
```
|
||||
|
||||
> 注意:`location /api/dev/` 前缀比 `location /api/` 更长,nginx 最长前缀优先匹配,无需改动既有 `/api/` 块。
|
||||
|
||||
### 3.3 前端三端测试登录入口
|
||||
|
||||
**判定内网**(复用常量,与 nginx 闸门网段同源):
|
||||
|
||||
```ts
|
||||
const TEST_NETS = ['10.', '172.16.', '172.17.', '172.18.', '172.19.', '172.20.', '172.21.', '172.22.', '172.23.', '172.24.', '172.25.', '172.26.', '172.27.', '172.28.', '172.29.', '172.30.', '172.31.', '192.168.']
|
||||
function isIntranet(): boolean {
|
||||
// 通过 /api/dev/health 探测:内网 200 → 显示测试入口;公网 403 → 隐藏
|
||||
}
|
||||
```
|
||||
|
||||
**推荐实现**:登录页 onMounted 时静默探测 `GET /api/dev/health`:
|
||||
- HTTP 200 → 渲染「测试账号登录」面板(角色下拉:user/agent/admin,对应 `PRESET_DEV_USERS`)
|
||||
- 403/网络错误 → 不渲染(公网用户不可见)
|
||||
|
||||
**登录动作**(坐席端示例,管理/H5 同构):
|
||||
|
||||
```ts
|
||||
const data = await apiClient.get('/dev/login', { params: { userid: 'dev-agent-001', name: '李四(IT坐席)', role: 'agent' } })
|
||||
localStorage.setItem(TOKEN_KEY, data.data.token) // 与企微扫码登录写入同一键
|
||||
// 刷新/跳转业务页,拦截器自动携带 token
|
||||
```
|
||||
|
||||
> 兜底:若 `health` 探测失败(如中间层拦截),可降级为"仅 hostname 非 `itsupport.servyou.com.cn` 时显示"——测试环境通常走内网 IP/测试域名。
|
||||
|
||||
### 3.4 token 生命周期
|
||||
|
||||
- `/api/dev/login` 返回的 token 与企微登录同源(TokenService,TTL 8h),Redis 可校验、登出接口可吊销
|
||||
- 测试账号 userid 前缀 `dev-*`,与真实账号隔离,不污染统计
|
||||
|
||||
---
|
||||
|
||||
## 4. 关键决策与取舍
|
||||
|
||||
| 决策点 | 选择 | 理由 |
|
||||
|--------|------|------|
|
||||
| 测试通道形态 | 启用既有 DEV_MODE + `/api/dev/*` | 零后端开发;接口已含二次校验;预设用户即测即用 |
|
||||
| 闸门层级 | nginx IP 白名单(主)+ 后端校验(次) | nginx 层拦截最前置、可独立回滚;后端校验防配置遗漏 |
|
||||
| 公网可见性 | 前端探测 `/api/dev/health` 决定是否显示测试入口 | 公网 403 → 入口自动隐藏,双保险 |
|
||||
| 不做的事 | 不新增生产密码登录、不改企微主流程 | 安全红线,见 PRD § 2.3 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 验证方式
|
||||
|
||||
| # | 验证项 | 方法 | 预期 |
|
||||
|---|--------|------|------|
|
||||
| V-1 | 内网 dev/login 可用 | 内网 curl `GET /api/dev/login?userid=dev-agent-001&role=agent` | 200 + token |
|
||||
| V-2 | 公网 dev 接口 403 | 公网 curl `GET /api/dev/health` | 403 |
|
||||
| V-3 | 业务接口未误伤 | 公网 curl `GET /api/health` | 200 |
|
||||
| V-4 | 前端测试入口 | 内网打开三端登录页 | 显示测试账号面板,一键登录进业务页 |
|
||||
| V-5 | 主登录回归 | 企微扫码/OAuth 流程 | 不受影响 |
|
||||
| V-6 | token 真实有效 | Redis `GET user:token:*` | 存在且 TTL 正常 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 风险与回滚
|
||||
|
||||
| 风险 | 缓解/回滚 |
|
||||
|------|----------|
|
||||
| 闸门配置语法错误 | `nginx -t` 先行校验;失败即回滚 conf(备份已建) |
|
||||
| 公网仍可访问 dev 接口 | 立即回滚:删除 `location /api/dev/` 块 + 移除 `DEV_MODE` → 重启后端 |
|
||||
| 前端构建问题 | 三端 dist 备份,回滚到上一版本 dist |
|
||||
| 误伤 `/api/` 业务 | 闸门仅精确前缀 `/api/dev/`,最长前缀匹配不影响其他;验证 V-3 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 变更记录
|
||||
|
||||
| 日期 | 版本 | 变更内容 | 变更人 | 变更原因 |
|
||||
|------|------|----------|--------|----------|
|
||||
| 2026-08-11 | v1.0 | 初版 | 宋献 | 预生产测试通道技术方案 |
|
||||
@@ -0,0 +1,77 @@
|
||||
# TC-REQ-通用-006-预生产测试通道
|
||||
|
||||
> **版本**: v1.0 | **日期**: 2026-08-11 | **状态**: [待评审]
|
||||
> **作者**: 宋献(测试) | **审核**: —
|
||||
> **关联REQ**: REQ-通用-006
|
||||
> **关联PRD**: `../../01-产品文档/00-产品规划/PRD-REQ-通用-006-预生产测试通道-v1.0.md`
|
||||
> **关联技术方案**: `../../02-技术文档/技术方案-REQ-通用-006-预生产测试通道-v1.0.md`
|
||||
> **测试环境**: 预生产(10.90.5.110 / itsupport.servyou.com.cn)
|
||||
|
||||
---
|
||||
|
||||
## 1. 测试范围
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| 在测功能 | 预生产测试通道:DEV_MODE mock 登录、nginx 内网闸门、前端测试登录入口 |
|
||||
| 不在测 | 企微 OAuth 主登录(仅回归)、新生产 K8s 改造 |
|
||||
| 前置条件 | 预生产 compose 注入 `DEV_MODE=true` 且后端已重启;nginx 闸门已生效 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 测试用例
|
||||
|
||||
### 2.1 后端测试通道
|
||||
|
||||
| 用例ID | 场景 | 步骤 | 预期结果 |
|
||||
|--------|------|------|----------|
|
||||
| TC-001 | 内网 mock 登录(坐席角色) | 内网 `GET /api/dev/login?userid=dev-agent-001&name=李四&role=agent` | HTTP 200,`data.token` 非空 |
|
||||
| TC-002 | 内网 mock 登录(管理员角色) | 内网 `GET /api/dev/login?userid=dev-admin-001&role=admin` | HTTP 200,token 有效 |
|
||||
| TC-003 | 多角色登录 | 内网 `GET /api/dev/login?userid=dev-multi-001&role=user,agent,supervisor` | HTTP 200,roles 数组含三角色 |
|
||||
| TC-004 | token 真实有效 | Redis 查询 `user:token:{hash}` | 存在,TTL≈8h |
|
||||
| TC-005 | dev/users 列表 | 内网 `GET /api/dev/users` | HTTP 200,6 个预设用户 |
|
||||
| TC-006 | dev/health 自检 | 内网 `GET /api/dev/health` | HTTP 200,`dev_mode: true` |
|
||||
| TC-007 | 带 token 调业务接口 | 用 TC-001 token 调 `GET /api/auth/me`(带 Bearer) | HTTP 200,返回 userid=dev-agent-001 |
|
||||
|
||||
### 2.2 nginx 闸门(安全)
|
||||
|
||||
| 用例ID | 场景 | 步骤 | 预期结果 |
|
||||
|--------|------|------|----------|
|
||||
| TC-101 | 公网访问 dev 接口 | 公网 `GET /api/dev/health` | **HTTP 403** |
|
||||
| TC-102 | 公网访问 dev/login | 公网 `GET /api/dev/login` | **HTTP 403** |
|
||||
| TC-103 | 业务接口未误伤 | 公网 `GET /api/health` | HTTP 200 |
|
||||
| TC-104 | 主登录未误伤 | 公网 `GET /api/auth/qrcode` | HTTP 200,返回 ticket |
|
||||
| TC-105 | 静态页未误伤 | 公网 `GET /itagent/`、`/itadmin/` | HTTP 200 |
|
||||
|
||||
### 2.3 前端测试登录入口
|
||||
|
||||
| 用例ID | 场景 | 步骤 | 预期结果 |
|
||||
|--------|------|------|----------|
|
||||
| TC-201 | 内网坐席登录页显示测试入口 | 内网打开 `/itagent/` | 显示「测试账号登录」面板,角色下拉可选 |
|
||||
| TC-202 | 内网一键登录坐席端 | 选 agent 角色 → 点击登录 | 进入坐席工作台,业务 API 正常 |
|
||||
| TC-203 | 内网一键登录管理后台 | 内网打开 `/itadmin/`,选 admin 登录 | 进入管理后台 |
|
||||
| TC-204 | 内网一键登录 H5 | 内网打开 H5 入口,选 user 登录 | 进入 H5 员工端 |
|
||||
| TC-205 | 公网登录页无测试入口 | 公网打开三端登录页 | 不显示测试账号面板(health 探测 403) |
|
||||
|
||||
### 2.4 回归
|
||||
|
||||
| 用例ID | 场景 | 步骤 | 预期结果 |
|
||||
|--------|------|------|----------|
|
||||
| TC-301 | 企微扫码登录回归 | 企微内打开任意端 | OAuth 流程正常 |
|
||||
| TC-302 | 登出清理 | 测试登录后调 `/api/auth/logout` | token 失效,Redis 黑名单生效 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 缺陷单关联
|
||||
|
||||
| 缺陷编号 | 关联用例 | 状态 |
|
||||
|----------|----------|------|
|
||||
| (无) | — | — |
|
||||
|
||||
---
|
||||
|
||||
## 4. 变更记录
|
||||
|
||||
| 日期 | 版本 | 变更内容 | 变更人 | 变更原因 |
|
||||
|------|------|----------|--------|----------|
|
||||
| 2026-08-11 | v1.0 | 初版 | 宋献 | 预生产测试通道测试用例 |
|
||||
@@ -0,0 +1,129 @@
|
||||
# DEPLOY-REQ-通用-006-预生产测试通道
|
||||
|
||||
> **版本**: v1.1 | **日期**: 2026-08-11 | **状态**: [已执行 ✅]
|
||||
> **作者**: 宋献(运维) | **审核**: —
|
||||
> **关联REQ**: REQ-通用-006
|
||||
> **关联PRD**: `../../01-产品文档/00-产品规划/PRD-REQ-通用-006-预生产测试通道-v1.0.md`
|
||||
> **关联技术方案**: `../../02-技术文档/技术方案-REQ-通用-006-预生产测试通道-v1.0.md`
|
||||
> **关联测试**: `../../03-测试文档/03-功能测试用例/TC-REQ-通用-006-预生产测试通道.md`
|
||||
> **目标环境**: 预生产 10.90.5.110(itsupport.servyou.com.cn)
|
||||
> **操作通道**: jumpserver-V2(资产 `hz-oa-ai-g-dataquery-90-5-110`,系统用户 `生产环境admin用户`)
|
||||
|
||||
---
|
||||
|
||||
## 一、部署前置检查
|
||||
|
||||
- [ ] jumpserver cache 有效(`v2_ops.py status`)
|
||||
- [ ] 预生产 nginx.conf 已备份(`cp nginx.conf nginx.conf.bak-testch-<ts>`)
|
||||
- [ ] 预生产 compose 已备份
|
||||
- [ ] 三端 dist 已备份(回滚用)
|
||||
|
||||
---
|
||||
|
||||
## 二、部署步骤
|
||||
|
||||
### 步骤 1:后端启用 DEV_MODE
|
||||
|
||||
```bash
|
||||
# 预生产 /opt/wecom-it-desk/docker-compose.yml
|
||||
# backend 服务 environment: 追加
|
||||
# - DEV_MODE=true
|
||||
# 重启后端(recreate 使环境变量生效)
|
||||
docker compose up -d backend
|
||||
# 验证启动日志出现 DEV_MODE 警告
|
||||
docker logs wecom_it_backend --tail 30 | grep -i "DEV_MODE"
|
||||
```
|
||||
|
||||
### 步骤 2:nginx 内网闸门
|
||||
|
||||
```bash
|
||||
# 编辑 /opt/wecom-it-desk/nginx/nginx.conf
|
||||
# 在 location /api/ 之前插入 /api/dev/ 白名单块(见技术方案 § 3.2)
|
||||
docker exec wecom_it_nginx nginx -t # 语法校验
|
||||
docker exec wecom_it_nginx nginx -s reload # 热加载
|
||||
```
|
||||
|
||||
### 步骤 3:前端三端构建 + 部署
|
||||
|
||||
```bash
|
||||
# 本地构建(三端)
|
||||
# src/frontend-agent / frontend-admin / frontend-h5: npm run build
|
||||
# 上传并解压到预生产 html 目录(沿用既有 H5/坐席/管理部署流程)
|
||||
# 换 dist inode 须 docker restart wecom_it_nginx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、部署后验证(对照 TC 用例)
|
||||
|
||||
| # | 验证 | 命令 | 预期 |
|
||||
|---|------|------|------|
|
||||
| 1 | 内网 mock 登录 | 内网 `curl /api/dev/login?userid=dev-agent-001&role=agent` | 200 + token |
|
||||
| 2 | 公网 dev 403 | 公网 `curl -sI /api/dev/health` | **403** |
|
||||
| 3 | 业务未误伤 | 公网 `curl /api/health` | 200 |
|
||||
| 4 | 前端入口 | 内网打开三端登录页 | 显示测试账号面板 |
|
||||
| 5 | 主登录回归 | 企微扫码 | 正常 |
|
||||
|
||||
> ⚠️ 公网验证须**绕过 WAF 缓存**(加 `?_=时间戳`),并从公网出口(非预生产本机)测试。
|
||||
|
||||
---
|
||||
|
||||
## 三.1 执行记录(2026-08-11 已完成 ✅)
|
||||
|
||||
| 步骤 | 结果 |
|
||||
|------|------|
|
||||
| compose 注入 DEV_MODE=true(备份 `docker-compose.yml.bak-testch-20260811`) | ✅ `docker compose config` 校验通过 |
|
||||
| 后端 recreate(`docker compose up -d backend`) | ✅ startup complete;容器 env 确认 DEV_MODE=true |
|
||||
| nginx 闸门注入(备份 `nginx.conf.bak-testch-20260811`,脚本 `deploy-server/tmp_modify_nginx_testch.py`) | ✅ `nginx -t` ok + reload |
|
||||
| 三端构建(`NODE_OPTIONS=` 绕 WorkBuddy safe-delete shim;node 直调 vite build) | ✅ agent 7.1s / admin 11.8s / h5 3.2s |
|
||||
| 三端 dist 部署(备份 `dist.bak-testch-20260811`;容器内资源引用与本地 hash 一致) | ✅ agent=index-BNny7ONd.js / admin=index-68C5Q3N_.js / h5=index-BM3ko024.js |
|
||||
| H5 版本化 v20260808→v20260811(备份 `nginx.conf.bak-h5v20260811-20260811`,脚本 `deploy-server/tmp_upgrade_h5_ver.py`) | ✅ 两个 go 302 已重指;nginx -t + reload |
|
||||
| 服务器临时脚本清理 | ✅ |
|
||||
|
||||
**部署后验证结果**(对照 TC):
|
||||
|
||||
| TC 用例 | 验证 | 结果 |
|
||||
|---------|------|------|
|
||||
| TC-101/102 | 公网 `/api/dev/health`、`/api/dev/login` | ✅ **403**(nginx 闸门) |
|
||||
| TC-103 | 公网 `/api/health` | ✅ 200 |
|
||||
| TC-104 | 公网 `/api/auth/qrcode` | ✅ 200(主登录未误伤) |
|
||||
| TC-001/006 | 后端 `/dev/health`、`/dev/login`(容器内) | ✅ 200 + token(login_source=dev) |
|
||||
| TC-007 | token 调 `/auth/me` | ✅ 返回 dev-agent-001 |
|
||||
| TC-004 | Redis `user:token:*` | ✅ 存在 |
|
||||
| 部署层 | 内网 `http://10.90.5.110/api/dev/health`(经 nginx) | ✅ 200(10/8 放行) |
|
||||
| 前端 | 三端登录页 200 + 新资源 hash + 测试通道特征字符串 | ✅ agent/admin/h5 全部通过 |
|
||||
| H5 | `/h5/go`、`/itservice/go` 302 → v20260811;v20260811 页面 200 | ✅ |
|
||||
|
||||
**遗留说明**:
|
||||
- 本机(WorkBuddy 沙箱)`NODE_OPTIONS` 注入 safe-delete shim 会拦截 vite 清空 dist(trash 失败)——**构建必须 `NODE_OPTIONS=` 前缀 + node 直调 vite**(已沉淀至 skill 待办)
|
||||
- 自动化测试脚本调用入口:`GET /api/dev/login?userid=dev-agent-001&role=agent`(仅内网)
|
||||
|
||||
---
|
||||
|
||||
## 四、回滚预案(30 min 内可逆)
|
||||
|
||||
| 场景 | 回滚动作 |
|
||||
|------|----------|
|
||||
| nginx 语法错误/闸门失效 | 恢复 `nginx.conf.bak-testch-<ts>` → `nginx -t` → reload |
|
||||
| 公网仍可访问 dev 接口 | 同上(回滚 nginx)+ 移除 compose `DEV_MODE` → `docker compose up -d backend` |
|
||||
| 前端异常 | 回滚三端 dist(备份恢复)→ `docker restart wecom_it_nginx` |
|
||||
| 全量回滚 | nginx conf 恢复 + DEV_MODE 移除 + dist 恢复,三步依序执行 |
|
||||
|
||||
---
|
||||
|
||||
## 五、上线审批检查项
|
||||
|
||||
- [ ] PRD/技术方案/任务说明书/测试用例/DEPLOY 五件套齐备(本链)
|
||||
- [ ] TC-001~TC-302 全部通过
|
||||
- [ ] commit message 含 `[REQ-通用-006]`
|
||||
- [ ] 新生产环境 compose 无 `DEV_MODE`(安全隔离)
|
||||
- [ ] 版本迭代总览已更新
|
||||
|
||||
---
|
||||
|
||||
## 六、变更记录
|
||||
|
||||
| 日期 | 版本 | 变更内容 | 变更人 | 变更原因 |
|
||||
|------|------|----------|--------|----------|
|
||||
| 2026-08-11 | v1.0 | 初版 | 宋献 | 预生产测试通道部署方案 |
|
||||
| 2026-08-11 | v1.1 | 执行完成 + 验证结果回填 + 遗留说明 | 宋献 | 部署落地 |
|
||||
@@ -0,0 +1,70 @@
|
||||
# 任务说明书-REQ-通用-006-预生产测试通道
|
||||
|
||||
> **版本**: v1.0 | **日期**: 2026-08-11 | **状态**: [待评审]
|
||||
> **作者**: 宋献 | **审核**: —
|
||||
> **关联需求编号**: REQ-通用-006
|
||||
> **关联PRD**: `../../01-产品文档/00-产品规划/PRD-REQ-通用-006-预生产测试通道-v1.0.md`
|
||||
> **关联技术方案**: `../../02-技术文档/技术方案-REQ-通用-006-预生产测试通道-v1.0.md`
|
||||
> **需求类型**: 新增
|
||||
|
||||
---
|
||||
|
||||
## 📥 输入项来源
|
||||
|
||||
| 输入 | 来源 |
|
||||
|------|------|
|
||||
| 产品需求 | `PRD-REQ-通用-006-预生产测试通道-v1.0.md` |
|
||||
| 技术方案 | `技术方案-REQ-通用-006-预生产测试通道-v1.0.md` |
|
||||
| 需了解的现有代码 | `src/backend/app/api/dev_auth.py`(已存在,零改动)<br>`src/backend/app/main.py`(DEV_MODE 挂载逻辑)<br>`src/frontend-agent/src/views/Login.vue` + `stores/agent.ts`(坐席)<br>`src/frontend-admin/src/views/Login.vue`(管理)<br>`src/frontend-h5/src/`(H5 登录与 token)<br>线上 `/opt/wecom-it-desk/docker-compose.yml` + `nginx/nginx.conf` |
|
||||
|
||||
---
|
||||
|
||||
## 📤 输出成果要求
|
||||
|
||||
| # | 产出 | 位置 | 说明 |
|
||||
|---|------|------|------|
|
||||
| O-1 | 预生产 compose 注入 `DEV_MODE=true` | 预生产 `/opt/wecom-it-desk/docker-compose.yml` | 仅预生产;新生产禁止 |
|
||||
| O-2 | nginx `/api/dev/` 内网白名单闸门 | 预生产 `nginx/nginx.conf` | allow 内网三网段 + deny all |
|
||||
| O-3 | 三端前端测试登录入口 | `src/frontend-agent|admin|h5` 登录页 | 探测 `/api/dev/health` 决定可见性 |
|
||||
| O-4 | 三端 dist 构建产物 | `src/frontend-*/dist/` | 部署预生产 |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 验证方式
|
||||
|
||||
| # | 验证 | 方法 |
|
||||
|---|------|------|
|
||||
| V-1 | 内网 mock 登录 | 内网 `curl GET /api/dev/login?userid=dev-agent-001&role=agent` → 200 + token |
|
||||
| V-2 | 公网 403 | 公网 `curl GET /api/dev/health` → 403 |
|
||||
| V-3 | 业务未误伤 | 公网 `curl GET /api/health` → 200 |
|
||||
| V-4 | 前端入口 | 内网浏览器打开三端登录页 → 显示测试账号面板 → 一键登录进业务页 |
|
||||
| V-5 | 主登录回归 | 企微扫码/OAuth 正常 |
|
||||
| V-6 | token 有效 | Redis 可查 token,TTL 8h |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 完成标准
|
||||
|
||||
- [ ] V-1 ~ V-6 全部通过
|
||||
- [ ] 预生产 DEV_MODE 注入方式、nginx 闸门配置已落 DEPLOY 文档
|
||||
- [ ] 新生产环境 compose 无 `DEV_MODE`(安全隔离确认项,写入上线 checklist)
|
||||
- [ ] 变更记录已追加
|
||||
|
||||
---
|
||||
|
||||
## WBS 阶段
|
||||
|
||||
| 阶段 | 内容 | 优先级 | 状态 |
|
||||
|------|------|--------|------|
|
||||
| P0 | 预生产 compose 注入 DEV_MODE + 后端重启 | P0 | ☐ |
|
||||
| P1 | nginx `/api/dev/` 闸门 + reload + 双向 curl 验证 | P1 | ☐ |
|
||||
| P2 | 三端前端测试登录入口开发 + 构建 | P2 | ☐ |
|
||||
| P3 | 预生产部署 dist + 全量验证(V-1~V-6) | P2 | ☐ |
|
||||
|
||||
---
|
||||
|
||||
## 变更记录
|
||||
|
||||
| 日期 | 版本 | 变更内容 | 变更人 | 变更原因 |
|
||||
|------|------|----------|--------|----------|
|
||||
| 2026-08-11 | v1.0 | 初版 | 宋献 | 预生产测试通道任务书 |
|
||||
@@ -46,6 +46,18 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 测试账号登录(REQ-通用-006 预生产测试通道,仅内网探测成功时显示) -->
|
||||
<div v-if="showTestPanel" class="test-login">
|
||||
<el-divider><span class="test-login-divider-text">测试账号登录</span></el-divider>
|
||||
<div class="test-login-row">
|
||||
<el-select v-model="testUser" size="small" placeholder="选择测试角色" style="width: 180px">
|
||||
<el-option v-for="u in testUsers" :key="u.userid" :label="u.label" :value="u.userid" />
|
||||
</el-select>
|
||||
<el-button type="primary" size="small" :loading="testLogining" @click="testLogin">免企微登录</el-button>
|
||||
</div>
|
||||
<p class="test-login-hint">仅内网可用,用于自动化测试</p>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-container">
|
||||
<el-icon class="is-loading" size="32"><Loading /></el-icon>
|
||||
@@ -94,6 +106,26 @@ let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
/** 当前扫码登录票据 */
|
||||
let currentTicket = ''
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 测试通道状态(REQ-通用-006:/api/dev/* 仅内网可用,公网 403 则隐藏)
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
/** 是否显示测试账号登录面板 */
|
||||
const showTestPanel = ref(false)
|
||||
|
||||
/** 测试登录中 */
|
||||
const testLogining = ref(false)
|
||||
|
||||
/** 选中的测试账号 userid */
|
||||
const testUser = ref('dev-admin-001')
|
||||
|
||||
/** 预设测试账号(与后端 PRESET_DEV_USERS 对应) */
|
||||
const testUsers = [
|
||||
{ userid: 'dev-user-001', label: '员工(user)', role: 'user' },
|
||||
{ userid: 'dev-agent-001', label: '坐席(agent)', role: 'agent' },
|
||||
{ userid: 'dev-admin-001', label: '管理员(admin)', role: 'admin' },
|
||||
]
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 方法
|
||||
// --------------------------------------------------------------------------
|
||||
@@ -293,11 +325,58 @@ async function redirectToOAuth(): Promise<void> {
|
||||
window.location.href = oauthUrl
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 测试通道方法(REQ-通用-006)
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 探测测试通道可用性(内网 200 → 显示测试入口;公网 403 → 隐藏)
|
||||
*/
|
||||
async function checkTestChannel(): Promise<void> {
|
||||
try {
|
||||
const apiClient = (await import('@/api/index')).default
|
||||
const resp = await apiClient.get('/dev/health', { timeout: 3000 })
|
||||
if (resp?.data?.data?.dev_mode) {
|
||||
showTestPanel.value = true
|
||||
}
|
||||
} catch {
|
||||
showTestPanel.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试账号免企微登录(仅内网):/api/dev/login → token 写入 admin_token
|
||||
*/
|
||||
async function testLogin(): Promise<void> {
|
||||
const target = testUsers.find((u) => u.userid === testUser.value)
|
||||
if (!target) return
|
||||
|
||||
testLogining.value = true
|
||||
try {
|
||||
const apiClient = (await import('@/api/index')).default
|
||||
const resp = await apiClient.get('/dev/login', {
|
||||
params: { userid: target.userid, name: target.label, role: target.role },
|
||||
})
|
||||
await handleLoginSuccess(resp.data.data.token, {
|
||||
userid: target.userid,
|
||||
name: target.label,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('测试登录失败:', error)
|
||||
ElMessage.error('测试登录失败,请确认内网环境')
|
||||
} finally {
|
||||
testLogining.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 生命周期
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
onMounted(async () => {
|
||||
// 探测测试通道(不阻塞主登录流程)
|
||||
checkTestChannel()
|
||||
|
||||
// 检查 URL 中的 code 参数(OAuth2 回调)
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const code = urlParams.get('code')
|
||||
@@ -467,4 +546,28 @@ onUnmounted(() => {
|
||||
padding: 40px 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 测试账号登录(REQ-通用-006) */
|
||||
.test-login {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.test-login-divider-text {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.test-login-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.test-login-hint {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -45,6 +45,18 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 测试账号登录(REQ-通用-006 预生产测试通道,仅内网探测成功时显示) -->
|
||||
<div v-if="showTestPanel" class="test-login">
|
||||
<el-divider><span class="test-login-divider-text">测试账号登录</span></el-divider>
|
||||
<div class="test-login-row">
|
||||
<el-select v-model="testUser" size="small" placeholder="选择测试角色" style="width: 180px">
|
||||
<el-option v-for="u in testUsers" :key="u.userid" :label="u.label" :value="u.userid" />
|
||||
</el-select>
|
||||
<el-button type="primary" size="small" :loading="testLogining" @click="testLogin">免企微登录</el-button>
|
||||
</div>
|
||||
<p class="test-login-hint">仅内网可用,用于自动化测试</p>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-container">
|
||||
<el-icon class="is-loading" size="32"><Loading /></el-icon>
|
||||
@@ -96,6 +108,26 @@ let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
/** 当前扫码登录票据 */
|
||||
let currentTicket = ''
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 测试通道状态(REQ-通用-006:/api/dev/* 仅内网可用,公网 403 则隐藏)
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
/** 是否显示测试账号登录面板 */
|
||||
const showTestPanel = ref(false)
|
||||
|
||||
/** 测试登录中 */
|
||||
const testLogining = ref(false)
|
||||
|
||||
/** 选中的测试账号 userid */
|
||||
const testUser = ref('dev-agent-001')
|
||||
|
||||
/** 预设测试账号(与后端 PRESET_DEV_USERS 对应) */
|
||||
const testUsers = [
|
||||
{ userid: 'dev-user-001', label: '员工(user)', role: 'user' },
|
||||
{ userid: 'dev-agent-001', label: '坐席(agent)', role: 'agent' },
|
||||
{ userid: 'dev-admin-001', label: '管理员(admin)', role: 'admin' },
|
||||
]
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 方法
|
||||
// --------------------------------------------------------------------------
|
||||
@@ -291,11 +323,58 @@ async function redirectToOAuth(): Promise<void> {
|
||||
window.location.href = oauthUrl
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 测试通道方法(REQ-通用-006)
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 探测测试通道可用性(内网 200 → 显示测试入口;公网 403 → 隐藏)
|
||||
*/
|
||||
async function checkTestChannel(): Promise<void> {
|
||||
try {
|
||||
const apiClient = (await import('@/api/index')).default
|
||||
const resp = await apiClient.get('/dev/health', { timeout: 3000 })
|
||||
if (resp?.data?.data?.dev_mode) {
|
||||
showTestPanel.value = true
|
||||
}
|
||||
} catch {
|
||||
showTestPanel.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试账号免企微登录(仅内网):/api/dev/login → token 写入 agent_token
|
||||
*/
|
||||
async function testLogin(): Promise<void> {
|
||||
const target = testUsers.find((u) => u.userid === testUser.value)
|
||||
if (!target) return
|
||||
|
||||
testLogining.value = true
|
||||
try {
|
||||
const apiClient = (await import('@/api/index')).default
|
||||
const resp = await apiClient.get('/dev/login', {
|
||||
params: { userid: target.userid, name: target.label, role: target.role },
|
||||
})
|
||||
await handleLoginSuccess(resp.data.data.token, {
|
||||
userid: target.userid,
|
||||
name: target.label,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('测试登录失败:', error)
|
||||
ElMessage.error('测试登录失败,请确认内网环境')
|
||||
} finally {
|
||||
testLogining.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 生命周期
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
onMounted(async () => {
|
||||
// 探测测试通道(不阻塞主登录流程)
|
||||
checkTestChannel()
|
||||
|
||||
// 检查 URL 中的 code 参数(OAuth2 回调)
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const code = urlParams.get('code')
|
||||
@@ -471,6 +550,30 @@ onUnmounted(() => {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* 测试账号登录(REQ-通用-006) */
|
||||
.test-login {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.test-login-divider-text {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary, #909399);
|
||||
}
|
||||
|
||||
.test-login-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.test-login-hint {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary, #909399);
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
|
||||
@@ -40,6 +40,18 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 测试账号登录(REQ-通用-006 预生产测试通道,仅内网探测成功时显示) -->
|
||||
<div v-if="showTestPanel" class="test-login">
|
||||
<div class="test-login-title">测试账号登录</div>
|
||||
<div class="test-login-row">
|
||||
<select v-model="testUser" class="test-login-select">
|
||||
<option v-for="u in testUsers" :key="u.userid" :value="u.userid">{{ u.label }}</option>
|
||||
</select>
|
||||
<van-button type="primary" size="small" :loading="testLogining" @click="testLogin">免企微登录</van-button>
|
||||
</div>
|
||||
<p class="test-login-hint">仅内网可用,用于自动化测试</p>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-container">
|
||||
<van-loading type="spinner" size="40px" />
|
||||
@@ -87,6 +99,26 @@ let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
/** 当前扫码登录票据 */
|
||||
let currentTicket = ''
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 测试通道状态(REQ-通用-006:/api/dev/* 仅内网可用,公网 403 则隐藏)
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
/** 是否显示测试账号登录面板 */
|
||||
const showTestPanel = ref(false)
|
||||
|
||||
/** 测试登录中 */
|
||||
const testLogining = ref(false)
|
||||
|
||||
/** 选中的测试账号 userid */
|
||||
const testUser = ref('dev-user-001')
|
||||
|
||||
/** 预设测试账号(与后端 PRESET_DEV_USERS 对应) */
|
||||
const testUsers = [
|
||||
{ userid: 'dev-user-001', label: '员工(user)', role: 'user' },
|
||||
{ userid: 'dev-agent-001', label: '坐席(agent)', role: 'agent' },
|
||||
{ userid: 'dev-admin-001', label: '管理员(admin)', role: 'admin' },
|
||||
]
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 方法
|
||||
// --------------------------------------------------------------------------
|
||||
@@ -251,11 +283,58 @@ async function processOAuthCallback(code: string, state?: string): Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 测试通道方法(REQ-通用-006)
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 探测测试通道可用性(内网 200 → 显示测试入口;公网 403 → 隐藏)
|
||||
*/
|
||||
async function checkTestChannel(): Promise<void> {
|
||||
try {
|
||||
const apiClient = (await import('@/api/index')).default
|
||||
const resp = await apiClient.get('/dev/health', { timeout: 3000 })
|
||||
if (resp?.data?.data?.dev_mode) {
|
||||
showTestPanel.value = true
|
||||
}
|
||||
} catch {
|
||||
showTestPanel.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试账号免企微登录(仅内网):/api/dev/login → token 写入 h5_token
|
||||
*/
|
||||
async function testLogin(): Promise<void> {
|
||||
const target = testUsers.find((u) => u.userid === testUser.value)
|
||||
if (!target) return
|
||||
|
||||
testLogining.value = true
|
||||
try {
|
||||
const apiClient = (await import('@/api/index')).default
|
||||
const resp = await apiClient.get('/dev/login', {
|
||||
params: { userid: target.userid, name: target.label, role: target.role },
|
||||
})
|
||||
await handleLoginSuccess(resp.data.data.token, {
|
||||
userid: target.userid,
|
||||
name: target.label,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('测试登录失败:', error)
|
||||
showToast('测试登录失败,请确认内网环境')
|
||||
} finally {
|
||||
testLogining.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 生命周期
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
onMounted(async () => {
|
||||
// 探测测试通道(不阻塞主登录流程)
|
||||
checkTestChannel()
|
||||
|
||||
// 检查 URL 中的 code 参数(OAuth2 回调)
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const code = urlParams.get('code')
|
||||
@@ -421,4 +500,40 @@ onUnmounted(() => {
|
||||
padding: 40px 0;
|
||||
color: var(--text-secondary, #606266);
|
||||
}
|
||||
|
||||
/* 测试账号登录(REQ-通用-006) */
|
||||
.test-login {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.test-login-title {
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary, #606266);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.test-login-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.test-login-select {
|
||||
height: 30px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
padding: 0 8px;
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.test-login-hint {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary, #909399);
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user