feat(密钥管理): 后台分组过滤+欠费状态识别

- 后台密钥列表:主管只看本组子账户(created_by_id=自己),超管全量可按创建人筛选;行 VO 新增 createdById/createdByUsername
- 代理提取接口欠费(message=余额不足)识别为 insufficient_balance,不再静默回退直连
- 上游 LLM 网关欠费(code=insufficient_user_quota/预扣费额度失败)同样归为欠费并透传额度明细
- 前端:桌面设置面板显示"欠费",admin 后台单格药丸显示"欠费"、新增所属管理员列与超管筛选
This commit is contained in:
2026-09-13 13:22:14 +08:00
parent 4bc4969e5a
commit 05ae0c62d6
11 changed files with 278 additions and 34 deletions
@@ -19,6 +19,8 @@ export interface AdminUserSecretModule {
export interface AdminUserSecretRow {
userId: number
username: string
createdById: number | null
createdByUsername: string
similarAsin: AdminUserSecretModule
appearancePatent: AdminUserSecretModule
proxy: AdminUserSecretModule
@@ -34,9 +36,17 @@ export interface AdminUserSecretPage {
pageSize: number
}
/** 管理员下拉项(超管筛选用)。 */
export interface AdminOption {
id: number
username: string
}
export interface UserSecretQuery {
keyword?: string
checkStatus?: string
/** 按创建人筛选(仅超管生效)。 */
createdById?: number
page: number
pageSize: number
}
@@ -3,6 +3,8 @@ import { computed, onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { formatDateTime } from '@/utils/datetime'
import OldPagination from '@/components/OldPagination.vue'
import { useAdminSessionStore } from '@/stores/admin-session'
import { fetchUserList } from '@/api/users'
import {
checkUserSecret,
deleteUserSecret,
@@ -11,6 +13,7 @@ import {
type AdminUserSecretRow,
} from '@/api/user-secrets'
const session = useAdminSessionStore()
const loading = ref(false)
const rows = ref<AdminUserSecretRow[]>([])
const total = ref(0)
@@ -18,6 +21,9 @@ const page = ref(1)
const pageSize = ref(15)
const keyword = ref('')
const statusFilter = ref('')
const createdByIdFilter = ref<number | null>(null)
/** 管理员下拉(仅超管可见/加载)。 */
const adminOptions = ref<Array<{ id: number; username: string }>>([])
/** 正在检测的行 userId,用于按钮 loading 态。 */
const checkingId = ref<number | null>(null)
@@ -48,7 +54,7 @@ function rowStatusMeta(status: string) {
}
}
/** 单格状态药丸:未配置时统一灰色。 */
/** 单格状态药丸:未配置时统一灰色;欠费单独标识。 */
function moduleStatusMeta(module: AdminUserSecretModule | undefined) {
if (!module || !module.exists) {
return { label: '未配置', tone: 'is-unknown' }
@@ -57,6 +63,9 @@ function moduleStatusMeta(module: AdminUserSecretModule | undefined) {
case 'passed':
return { label: '通过', tone: 'is-allowed' }
case 'failed':
if (module.checkCode === 'insufficient_balance') {
return { label: '欠费', tone: 'is-warn' }
}
return { label: '失败', tone: 'is-blocked' }
case 'error':
return { label: '无法判定', tone: 'is-warn' }
@@ -82,12 +91,23 @@ function rowStatusTooltip(row: AdminUserSecretRow) {
return parts.join('') || '暂无检测记录'
}
async function loadAdminOptions() {
if (!session.isSuperAdmin) return
try {
const result = await fetchUserList({ page: 1, pageSize: 999, role: 'admin' })
adminOptions.value = result.admins || []
} catch (error) {
console.warn('[user-secrets] 管理员下拉加载失败', error)
}
}
async function load() {
loading.value = true
try {
const result = await fetchUserSecretList({
keyword: keyword.value.trim() || undefined,
checkStatus: statusFilter.value || undefined,
createdById: createdByIdFilter.value || undefined,
page: page.value,
pageSize: pageSize.value,
})
@@ -108,6 +128,7 @@ function search() {
function reset() {
keyword.value = ''
statusFilter.value = ''
createdByIdFilter.value = null
page.value = 1
load()
}
@@ -162,7 +183,10 @@ function changeSize(size: number) {
load()
}
onMounted(load)
onMounted(() => {
loadAdminOptions()
load()
})
</script>
<template>
@@ -183,6 +207,13 @@ onMounted(load)
<option v-for="option in STATUS_OPTIONS" :key="option.value" :value="option.value">{{ option.label }}</option>
</select>
</div>
<div class="form-group" v-if="session.isSuperAdmin" style="min-width: 170px">
<label>所属管理员</label>
<select v-model="createdByIdFilter">
<option :value="null">全部管理员</option>
<option v-for="admin in adminOptions" :key="admin.id" :value="admin.id">{{ admin.username }}</option>
</select>
</div>
<div class="form-group">
<label>&nbsp;</label>
<div class="filter-actions">
@@ -198,6 +229,7 @@ onMounted(load)
<tr>
<th style="width: 58px">序号</th>
<th style="width: 260px">用户</th>
<th style="width: 130px">所属管理员</th>
<th style="width: 230px">货源查询密钥</th>
<th style="width: 230px">外观专利密钥</th>
<th style="width: 230px">代理设置</th>
@@ -212,6 +244,9 @@ onMounted(load)
<td>
<span class="user-name">{{ row.username || '—' }}</span>
</td>
<td>
<span class="creator-name">{{ row.createdByUsername || (row.createdById ? `UID ${row.createdById}` : '—') }}</span>
</td>
<td>
<div class="module-cell">
<span v-if="row.similarAsin?.exists" class="mono-mask" :title="moduleTooltip(row.similarAsin)">{{ row.similarAsin.masked }}</span>
@@ -258,10 +293,10 @@ onMounted(load)
</tr>
</template>
<tr v-else-if="loading">
<td colspan="7" class="empty-tip">加载中...</td>
<td colspan="8" class="empty-tip">加载中...</td>
</tr>
<tr v-else>
<td colspan="7" class="empty-tip">{{ keyword || statusFilter ? '暂无匹配记录' : '暂无用户密钥记录' }}</td>
<td colspan="8" class="empty-tip">{{ keyword || statusFilter || createdByIdFilter ? '暂无匹配记录' : '暂无用户密钥记录' }}</td>
</tr>
</tbody>
</table>
@@ -408,7 +443,7 @@ h3 {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
min-width: 1310px;
min-width: 1440px;
}
.secrets-table-scroll th,
.secrets-table-scroll td {
@@ -443,6 +478,10 @@ h3 {
white-space: normal;
overflow-wrap: anywhere;
}
.creator-name {
color: #5b6f83;
font-size: 12.5px;
}
.module-cell {
display: flex;
align-items: center;