Skip to content

Commit 45f8057

Browse files
committed
feat: add token quota management for chat users with API integration
1 parent 9cac6c4 commit 45f8057

6 files changed

Lines changed: 228 additions & 38 deletions

File tree

‎apps/system_manage/models/chat_user_token_quota.py‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,17 @@ def consume(cls, user_id, amount):
7070
if amount <= 0:
7171
return
7272
quota = cls.objects.filter(user_id=user_id).first()
73-
if quota is None or quota.quota_type == QuotaType.UNLIMITED:
73+
if quota is None:
74+
# 所有消费用户都创建统计行,匿名用户也累计使用量
75+
quota, _ = cls.objects.get_or_create(
76+
user_id=user_id,
77+
defaults={'quota_type': QuotaType.UNLIMITED},
78+
)
79+
if quota.quota_type == QuotaType.UNLIMITED:
80+
# 不限额:只累计使用量,不校验上限
81+
quota.used_tokens += amount
82+
quota.total_tokens += amount
83+
quota.save(update_fields=['used_tokens', 'total_tokens'])
7484
return
7585
quota.check_and_reset()
7686
if quota.used_tokens + amount > quota.token_limit:

‎ui/src/api/admin/system/chat-user.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { del, get, post, put } from '../core/request'
22
import type { ParamsPage, ResponsePage, PasswordRequest } from '../core/types'
33
import type {
4+
BatchSetChatUserQuotaRequest,
5+
BatchSetChatUserQuotaResult,
46
BatchSetChatUserGroupsRequest,
57
ChatUserBase,
68
ChatUser,
79
ChatUserPayload,
10+
ChatUserQuota,
11+
ChatUserQuotaPayload,
812
ChatUserSyncResult,
913
ChatUserUpdateRequest,
1014
RequestParams,
@@ -55,6 +59,24 @@ const postBatchSetChatUserGroups = (request: BatchSetChatUserGroupsRequest) => {
5559
return post<BatchSetChatUserGroupsRequest, boolean>(`${prefix}/batch_add_group`, request)
5660
}
5761

62+
/** 获取对话用户 Token 配额。 */
63+
const getChatUserQuota = (userId: string) => {
64+
return get<ChatUserQuota>(`${prefix}/${userId}/quota`)
65+
}
66+
67+
/** 设置对话用户 Token 配额。 */
68+
const postChatUserQuota = (userId: string, payload: ChatUserQuotaPayload) => {
69+
return post<ChatUserQuotaPayload, ChatUserQuota>(`${prefix}/${userId}/quota`, payload)
70+
}
71+
72+
/** 批量设置对话用户 Token 配额。 */
73+
const postBatchSetChatUserQuota = (request: BatchSetChatUserQuotaRequest) => {
74+
return post<BatchSetChatUserQuotaRequest, BatchSetChatUserQuotaResult>(
75+
`${prefix}/batch_quota`,
76+
request,
77+
)
78+
}
79+
5880
/** 获取可导入的对话用户来源。 */
5981
const getChatUserSyncTypes = () => {
6082
return get<string[]>(`${prefix}/sync/types`)
@@ -67,12 +89,15 @@ const postSyncChatUsers = (syncType: string) => {
6789

6890
export default {
6991
deleteChatUser,
92+
getChatUserQuota,
7093
getChatUserPage,
7194
getChatUser,
7295
getChatUserSyncTypes,
7396
postBatchDeleteChatUsers,
7497
postBatchSetChatUserGroups,
98+
postBatchSetChatUserQuota,
7599
postChatUser,
100+
postChatUserQuota,
76101
postSyncChatUsers,
77102
putChatUser,
78103
putChatUserPassword,

‎ui/src/api/enums/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/** API 枚举值的唯一公共入口。 */
22
export * from './application'
3+
export * from './chat-user'
34
export * from './login'
45
export * from './system-role'
56
export * from './model'

‎ui/src/api/types/chat-user.ts‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/** 对话用户 API 与管理页面共用的业务类型。 */
22

3+
import { PERIOD_TYPE, QUOTA_TYPE } from '@/api/enums'
4+
35
export interface ChatUserBase {
46
id: string
57
username: string
@@ -15,6 +17,7 @@ export interface ChatUserBase {
1517
export interface ChatUser extends ChatUserBase {
1618
user_group_ids: string[]
1719
user_group_names: string[]
20+
token_quota?: ChatUserTokenQuota | null
1821
}
1922

2023
export interface ChatUserPayload {
@@ -51,3 +54,49 @@ export interface ChatUserSyncResult {
5154
success_count: number
5255
conflict_users: ChatUserSyncConflict[]
5356
}
57+
58+
/** 对话用户列表中的 Token 配额概要(由列表接口按用户合并返回)。 */
59+
export interface ChatUserTokenQuota {
60+
quota_type: QuotaType
61+
used_tokens: number
62+
token_limit: number | null
63+
total_tokens: number
64+
period_end: string | null
65+
}
66+
67+
/** 对话用户 Token 配额类型。 */
68+
export type QuotaType = (typeof QUOTA_TYPE)[keyof typeof QUOTA_TYPE]
69+
export type PeriodType = (typeof PERIOD_TYPE)[keyof typeof PERIOD_TYPE]
70+
71+
/** 对话用户 Token 配额。 */
72+
export interface ChatUserQuota {
73+
user_id: string
74+
quota_type: QuotaType
75+
quota_type_label: string
76+
period_type?: PeriodType | null
77+
period_type_label?: string | null
78+
period_value?: number | null
79+
token_limit?: number | null
80+
used_tokens?: number
81+
total_tokens?: number
82+
period_end?: string | null
83+
}
84+
85+
/** 设置对话用户 Token 配额请求体。 */
86+
export interface ChatUserQuotaPayload {
87+
quota_type: QuotaType
88+
period_type: PeriodType | null
89+
period_value: number | null
90+
token_limit: number | null
91+
}
92+
93+
/** 批量设置对话用户 Token 配额请求体。 */
94+
export interface BatchSetChatUserQuotaRequest extends ChatUserQuotaPayload {
95+
user_ids: string[]
96+
}
97+
98+
/** 批量设置对话用户 Token 配额结果。 */
99+
export interface BatchSetChatUserQuotaResult {
100+
success_count: number
101+
failed_count: number
102+
}

‎ui/src/views/system/chat/users/UserListView.vue‎

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { onMounted, ref, useTemplateRef } from 'vue'
33
import ChatUserApi from '@/api/admin/system/chat-user'
44
import type { ChatUser, LoginMethod, OptionItem, RequestParams } from '@/api/types'
5-
import { LOGIN_METHOD } from '@/api/enums'
5+
import { LOGIN_METHOD, QUOTA_TYPE } from '@/api/enums'
66
import { LOGIN_METHOD_LABELS } from '@/constants'
77
import { datetimeFormat } from '@/utils/time'
88
import { MsgConfirm, MsgSuccess } from '@/utils/message'
@@ -57,6 +57,32 @@ const searchFields: OptionItem<string>[] = [
5757
]
5858
const chatUserQuery = ref<RequestParams>()
5959
60+
function formatTokenAmount(value?: number | null) {
61+
if (typeof value !== 'number' || Number.isNaN(value)) return '-'
62+
const units = ['', 'K', 'M', 'B', 'T']
63+
let unitIndex = 0
64+
let num = value
65+
let abs = Math.abs(value)
66+
while (abs >= 1000 && unitIndex < units.length - 1) {
67+
num /= 1000
68+
abs /= 1000
69+
unitIndex++
70+
}
71+
if (unitIndex === 0) return String(Math.round(num))
72+
return `${num.toFixed(1)}${units[unitIndex]}`
73+
}
74+
75+
function formatPeriodEnd(value?: string | null) {
76+
return value ? datetimeFormat(value) : '-'
77+
}
78+
79+
function formatQuotaUsage(tokenQuota?: ChatUser['token_quota']) {
80+
if (!tokenQuota) return '-'
81+
const used = formatTokenAmount(tokenQuota.used_tokens)
82+
if (tokenQuota.quota_type === QUOTA_TYPE.UNLIMITED) return `${used} / ∞`
83+
return `${used} / ${formatTokenAmount(tokenQuota.token_limit)}`
84+
}
85+
6086
function handleSearchChange(query?: RequestParams) {
6187
chatUserQuery.value = query
6288
paginationConfig.value.currentPage = 1
@@ -155,6 +181,10 @@ function openBatchSetUserGroupDialog() {
155181
batchSetUserGroupDialogRef.value?.open(batchSelectedUsers.value.map(({ id }) => id))
156182
}
157183
184+
function openBatchQuotaSettingsDialog() {
185+
quotaSettingsDialogRef.value?.open(batchSelectedUsers.value.map(({ id }) => id))
186+
}
187+
158188
onMounted(() => loadChatUsers())
159189
</script>
160190

@@ -222,6 +252,18 @@ onMounted(() => loadChatUsers())
222252
</template>
223253
</el-table-column>
224254

255+
<el-table-column label="Tokens使用量/总量" min-width="150">
256+
<template #default="{ row }">
257+
{{ formatQuotaUsage(row.token_quota) }}
258+
</template>
259+
</el-table-column>
260+
261+
<el-table-column label="Tokens到期时间" min-width="180">
262+
<template #default="{ row }">
263+
{{ formatPeriodEnd(row.token_quota?.period_end) }}
264+
</template>
265+
</el-table-column>
266+
225267
<el-table-column label="创建时间" width="180">
226268
<template #default="{ row }">
227269
{{ datetimeFormat(row.create_time) }}
@@ -273,6 +315,9 @@ onMounted(() => loadChatUsers())
273315
<el-button type="primary" plain @click="openBatchSetUserGroupDialog">
274316
设置用户组
275317
</el-button>
318+
<el-button type="primary" plain @click="openBatchQuotaSettingsDialog">
319+
配额设置
320+
</el-button>
276321
<el-button type="danger" plain @click="handleBatchDelete">删除</el-button>
277322
</template>
278323
</MkTable>
@@ -282,5 +327,5 @@ onMounted(() => loadChatUsers())
282327
<ImportUsersDialog ref="importUsersDialogRef" @refresh="loadChatUsers(true)" />
283328
<UserPwdDialog ref="userPwdDialogRef" @refresh="loadChatUsers(false)" />
284329
<BatchSetUserGroupDialog ref="batchSetUserGroupDialogRef" @refresh="loadChatUsers(false)" />
285-
<QuotaSettingsDialog ref="quotaSettingsDialogRef" />
330+
<QuotaSettingsDialog ref="quotaSettingsDialogRef" @refresh="loadChatUsers" />
286331
</template>

0 commit comments

Comments
 (0)