新增 token 余额前置检查,余额不足时阻止使用功能
问题:用户 token 余额为 0 时仍能正常使用文字聊天、语音聊天、实时 通话。根因是前端没有任何前置检查,完全依赖后端"先用后扣"模式,而 后端扣费失败仅记日志不阻断响应。 修复措施: - 后端新增 GET /user/balance 轻量接口,返回 available_tokens - 前端 chatStore 新增 fetchUserBalance(),页面初始化时查询余额并 设置 _tokenExhausted / _realtimeTokenExhausted 标记 - sendTextMessage、toggleCallMode、toggleVoiceInput 入口处增加 isTokenExhausted() 前置检查 - canSend 计算属性加入 token 检查,余额不足时发送按钮置灰 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
141f984772
commit
1d21d8b236
@ -211,6 +211,23 @@ async def change_password(
|
||||
}
|
||||
|
||||
|
||||
@user_router.get("/balance")
|
||||
async def get_user_balance(
|
||||
current_user: Dict = Depends(get_current_user),
|
||||
db = Depends(get_db)
|
||||
):
|
||||
"""Get user available token balance"""
|
||||
from services.token_service import TokenService
|
||||
token_service = TokenService(db)
|
||||
quota = await token_service._check_user_quota(current_user.get('id'))
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"available_tokens": quota["available_tokens"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@user_router.get("/stats")
|
||||
async def get_user_stats(
|
||||
current_user: Dict = Depends(get_current_user),
|
||||
|
||||
@ -76,7 +76,8 @@ export const userAPI = {
|
||||
login: (loginData) => apiClient.post(API_ENDPOINTS.USER.LOGIN, loginData),
|
||||
getProfile: () => apiClient.get(API_ENDPOINTS.USER.PROFILE),
|
||||
update: (data) => apiClient.put(API_ENDPOINTS.USER.UPDATE, data),
|
||||
delete: () => apiClient.delete(API_ENDPOINTS.USER.PROFILE)
|
||||
delete: () => apiClient.delete(API_ENDPOINTS.USER.PROFILE),
|
||||
getBalance: () => apiClient.get('/api/v1/user/balance')
|
||||
}
|
||||
|
||||
export const authAPI = userAPI
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import apiClient from '@/api/index'
|
||||
import { chatAPI } from '@/api/index'
|
||||
import { chatAPI, userAPI } from '@/api/index'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
// 统计更新事件回调
|
||||
@ -1744,6 +1744,19 @@ export const useChatStore = defineStore('chat', () => {
|
||||
_realtimeTokenExhausted = false
|
||||
}
|
||||
|
||||
async function fetchUserBalance() {
|
||||
try {
|
||||
const response = await userAPI.getBalance()
|
||||
const available = response.data?.data?.available_tokens ?? 0
|
||||
_tokenExhausted = available <= 0
|
||||
_realtimeTokenExhausted = available <= 0
|
||||
return available
|
||||
} catch (error) {
|
||||
console.warn('[ChatStore] Failed to fetch user balance:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function updateSessionConfig(config) {
|
||||
currentSessionConfig.value = {
|
||||
...currentSessionConfig.value,
|
||||
@ -1814,6 +1827,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
updateSessionConfig,
|
||||
isTokenExhausted,
|
||||
resetTokenExhausted,
|
||||
fetchUserBalance,
|
||||
|
||||
// 其他
|
||||
clearMessages,
|
||||
|
||||
@ -1153,7 +1153,7 @@ export default {
|
||||
}
|
||||
|
||||
const canSend = computed(() => {
|
||||
return inputMessage.value.trim() && !sending.value && !isRecording.value && !isInCall.value
|
||||
return inputMessage.value.trim() && !sending.value && !isRecording.value && !isInCall.value && !chatStore.isTokenExhausted()
|
||||
})
|
||||
|
||||
const normalizeDateValue = (value) => {
|
||||
@ -1315,6 +1315,7 @@ export default {
|
||||
cacheSelectedPet(currentPet.value)
|
||||
saveChatConfig()
|
||||
syncStoreState()
|
||||
await chatStore.fetchUserBalance()
|
||||
// ElMessage.success('聊天初始化成功')
|
||||
} catch (err) {
|
||||
console.error('Failed to init chat:', err)
|
||||
@ -1604,6 +1605,10 @@ export default {
|
||||
|
||||
const sendTextMessage = async () => {
|
||||
if (!inputMessage.value.trim() || sending.value) return
|
||||
if (chatStore.isTokenExhausted()) {
|
||||
ElMessage.error('Token已耗尽,请充值后再使用')
|
||||
return
|
||||
}
|
||||
|
||||
const messageText = inputMessage.value.trim()
|
||||
inputMessage.value = ''
|
||||
@ -1693,6 +1698,10 @@ export default {
|
||||
try {
|
||||
const hasPermission = await chatStore.requestMicrophonePermission()
|
||||
if (hasPermission) {
|
||||
if (chatStore.isTokenExhausted()) {
|
||||
ElMessage.error('Token已耗尽,请充值后再使用')
|
||||
return
|
||||
}
|
||||
await forceStopVoiceAndCall()
|
||||
// 先显示面板但不切换 chatMode,避免预连接期间的竞态
|
||||
showInputPanel.value = true
|
||||
@ -1826,6 +1835,11 @@ export default {
|
||||
|
||||
pendingCallMode = true
|
||||
try {
|
||||
if (chatStore.isTokenExhausted()) {
|
||||
ElMessage.error('Token已耗尽,请充值后再使用')
|
||||
return
|
||||
}
|
||||
|
||||
const hasPermission = await chatStore.requestMicrophonePermission()
|
||||
if (!hasPermission) {
|
||||
return
|
||||
|
||||
Loading…
Reference in New Issue
Block a user