修复 token 统计不一致:统一扣费来源并支持分类查询
- chat_service: 文字聊天 ChatLog 改用独立会话保存,避免路由会话 生命周期干扰导致记录丢失 - token_service: 分类查询改用 mode 字段精确匹配替代 conversation_id 模糊匹配;总消耗直接读钱包数据与后台管理一致 - admin_router: 用户列表和详情新增文字/语音/通话分类 token 消耗 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2bd53de4cc
commit
b8769d8d54
@ -82,11 +82,22 @@ async def list_users(
|
||||
for user in users:
|
||||
wallet_result = await db.execute(select(UserWallet).where(UserWallet.user_id == user.id))
|
||||
wallet = wallet_result.scalar_one_or_none()
|
||||
|
||||
|
||||
# 查询该用户各模式的 token 消耗
|
||||
mode_tokens_result = await db.execute(
|
||||
select(
|
||||
ChatLog.mode,
|
||||
func.coalesce(func.sum(ChatLog.tokens_input), 0) +
|
||||
func.coalesce(func.sum(ChatLog.tokens_output), 0).label("tokens")
|
||||
).where(ChatLog.user_id == user.id)
|
||||
.group_by(ChatLog.mode)
|
||||
)
|
||||
mode_tokens = {row.mode or "text_chat": int(row.tokens or 0) for row in mode_tokens_result.fetchall()}
|
||||
|
||||
# 将状态从整数转换为字符串格式
|
||||
# status: 1 或非零 = active, 0 或 null = disabled
|
||||
status_str = 'active' if (user.status and user.status != 0) else 'disabled'
|
||||
|
||||
|
||||
user_dict = {
|
||||
"id": user.id,
|
||||
"phone": user.phone,
|
||||
@ -97,6 +108,9 @@ async def list_users(
|
||||
"daily_quota": wallet.daily_quota if wallet else 0,
|
||||
"extra_quota": wallet.extra_quota if wallet else 0,
|
||||
"total_consumed": wallet.total_consumed if wallet else 0,
|
||||
"text_chat_tokens": mode_tokens.get("text_chat", 0),
|
||||
"voice_chat_tokens": mode_tokens.get("voice_chat", 0),
|
||||
"realtime_call_tokens": mode_tokens.get("voice_realtime_call", 0),
|
||||
"created_at": user.created_at.isoformat() if user.created_at else None
|
||||
}
|
||||
user_list.append(user_dict)
|
||||
@ -129,6 +143,17 @@ async def get_user_detail(
|
||||
wallet_result = await db.execute(select(UserWallet).where(UserWallet.user_id == user_id))
|
||||
wallet = wallet_result.scalar_one_or_none()
|
||||
|
||||
# 查询该用户各模式的 token 消耗
|
||||
mode_tokens_result = await db.execute(
|
||||
select(
|
||||
ChatLog.mode,
|
||||
func.coalesce(func.sum(ChatLog.tokens_input), 0) +
|
||||
func.coalesce(func.sum(ChatLog.tokens_output), 0).label("tokens")
|
||||
).where(ChatLog.user_id == user_id)
|
||||
.group_by(ChatLog.mode)
|
||||
)
|
||||
mode_tokens = {row.mode or "text_chat": int(row.tokens or 0) for row in mode_tokens_result.fetchall()}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
@ -141,7 +166,10 @@ async def get_user_detail(
|
||||
"wallet": {
|
||||
"daily_quota": wallet.daily_quota if wallet else 0,
|
||||
"extra_quota": wallet.extra_quota if wallet else 0,
|
||||
"total_consumed": wallet.total_consumed if wallet else 0
|
||||
"total_consumed": wallet.total_consumed if wallet else 0,
|
||||
"text_chat_tokens": mode_tokens.get("text_chat", 0),
|
||||
"voice_chat_tokens": mode_tokens.get("voice_chat", 0),
|
||||
"realtime_call_tokens": mode_tokens.get("voice_realtime_call", 0)
|
||||
}
|
||||
},
|
||||
"message": "User details retrieved successfully"
|
||||
|
||||
@ -15,6 +15,7 @@ import asyncio
|
||||
from models.database import ChatLog, Pet, Background, BackgroundPetConfig, UserWallet
|
||||
from services.doubao_service import doubao_service
|
||||
from config.settings import get_chat_context_rounds
|
||||
from utils.database import AsyncSessionLocal
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -307,15 +308,18 @@ async def send_message_stream(db_session: AsyncSession, user_id: int, message: s
|
||||
duration_ms=duration_ms,
|
||||
mode="text_chat"
|
||||
)
|
||||
db_session.add(chat_log)
|
||||
await db_session.commit()
|
||||
|
||||
# 查询用户实际剩余token
|
||||
wallet_result = await db_session.execute(
|
||||
sa_select(UserWallet).where(UserWallet.user_id == user_id)
|
||||
)
|
||||
wallet = wallet_result.scalar_one_or_none()
|
||||
remaining_tokens = (wallet.available_tokens or 0) if wallet else 0
|
||||
# 使用独立会话保存记录,避免受路由 get_db() 生命周期影响
|
||||
async with AsyncSessionLocal() as save_session:
|
||||
save_session.add(chat_log)
|
||||
await save_session.commit()
|
||||
|
||||
# 查询用户实际剩余token
|
||||
wallet_result = await save_session.execute(
|
||||
sa_select(UserWallet).where(UserWallet.user_id == user_id)
|
||||
)
|
||||
wallet = wallet_result.scalar_one_or_none()
|
||||
remaining_tokens = (wallet.available_tokens or 0) if wallet else 0
|
||||
|
||||
yield {
|
||||
"type": "stats",
|
||||
|
||||
@ -485,14 +485,21 @@ class TokenService:
|
||||
func.coalesce(func.sum(ChatLog.tokens_output), 0)
|
||||
).where(
|
||||
*base_conditions,
|
||||
ChatLog.conversation_id.like("voice-%")
|
||||
ChatLog.mode == "voice_chat"
|
||||
)
|
||||
realtime_query = select(
|
||||
func.coalesce(func.sum(ChatLog.tokens_input), 0) +
|
||||
func.coalesce(func.sum(ChatLog.tokens_output), 0)
|
||||
).where(
|
||||
*base_conditions,
|
||||
ChatLog.conversation_id.like("realtime-%")
|
||||
ChatLog.mode == "voice_realtime_call"
|
||||
)
|
||||
text_query = select(
|
||||
func.coalesce(func.sum(ChatLog.tokens_input), 0) +
|
||||
func.coalesce(func.sum(ChatLog.tokens_output), 0)
|
||||
).where(
|
||||
*base_conditions,
|
||||
ChatLog.mode == "text_chat"
|
||||
)
|
||||
|
||||
total_input = (await self.db_session.execute(total_input_query)).scalar() or 0
|
||||
@ -500,16 +507,14 @@ class TokenService:
|
||||
total_chats = (await self.db_session.execute(total_chats_query)).scalar() or 0
|
||||
voice_tokens = (await self.db_session.execute(voice_query)).scalar() or 0
|
||||
realtime_tokens = (await self.db_session.execute(realtime_query)).scalar() or 0
|
||||
text_chat_tokens = (await self.db_session.execute(text_query)).scalar() or 0
|
||||
|
||||
quota_info = await self._check_user_quota(user_id)
|
||||
wallet_consumed = quota_info.get("total_consumed", 0) or 0
|
||||
chatlog_total = total_input + total_output
|
||||
|
||||
total_tokens = max(chatlog_total, wallet_consumed)
|
||||
if total_tokens <= 0:
|
||||
total_tokens = chatlog_total or wallet_consumed or 0
|
||||
|
||||
text_chat_tokens = max(0, chatlog_total - voice_tokens - realtime_tokens)
|
||||
# 直接使用钱包累计消耗,与后台管理一致
|
||||
total_tokens = wallet_consumed
|
||||
|
||||
return {
|
||||
"user_id": user_id,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user