[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:
Simon
2026-08-11 11:29:20 +08:00
parent 9fcee9df0e
commit c1d5dd584c
11 changed files with 985 additions and 0 deletions
+103
View File
@@ -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>
+103
View File
@@ -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;
+115
View File
@@ -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>