修复 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:
wsb1224 2026-06-09 20:44:53 +08:00
parent 2bd53de4cc
commit b8769d8d54
3 changed files with 55 additions and 18 deletions

View File

@ -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"

View File

@ -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",

View File

@ -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,