修复toekn权限和语音聊天功能
This commit is contained in:
parent
5822ca6aee
commit
60931d408c
@ -83,12 +83,16 @@ async def list_users(
|
||||
wallet_result = await db.execute(select(UserWallet).where(UserWallet.user_id == user.id))
|
||||
wallet = wallet_result.scalar_one_or_none()
|
||||
|
||||
# 将状态从整数转换为字符串格式
|
||||
# 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,
|
||||
"nickname": user.nickname,
|
||||
"is_admin": user.role == 2,
|
||||
"status": user.status,
|
||||
"status": status_str,
|
||||
"available_tokens": wallet.available_tokens if wallet else 0,
|
||||
"daily_quota": wallet.daily_quota if wallet else 0,
|
||||
"extra_quota": wallet.extra_quota if wallet else 0,
|
||||
|
||||
@ -61,9 +61,12 @@ async def send_message_endpoint(
|
||||
实现分层提示:基础提示 + 场景提示 + 记忆 + 用户输入
|
||||
"""
|
||||
try:
|
||||
# 解析用户ID为数字类型
|
||||
numeric_user_id = await resolve_user_id(current_user["id"], db)
|
||||
|
||||
token_service = TokenService(db)
|
||||
|
||||
token_check = await token_service.check_available_tokens(current_user["id"])
|
||||
token_check = await token_service.check_available_tokens(numeric_user_id)
|
||||
if not token_check["has_tokens"]:
|
||||
raise HTTPException(
|
||||
status_code=402,
|
||||
@ -76,7 +79,7 @@ async def send_message_endpoint(
|
||||
|
||||
result = await send_message(
|
||||
db,
|
||||
user_id=current_user["id"],
|
||||
user_id=numeric_user_id,
|
||||
message=message_data.message,
|
||||
pet_id=message_data.pet_id,
|
||||
background_id=message_data.background_id,
|
||||
@ -84,14 +87,14 @@ async def send_message_endpoint(
|
||||
)
|
||||
|
||||
charge_result = await token_service.charge_tokens(
|
||||
user_id=current_user["id"],
|
||||
user_id=numeric_user_id,
|
||||
mode="text_chat",
|
||||
tokens_used=result.get("tokens_used"),
|
||||
fallback_amount=1
|
||||
)
|
||||
if not charge_result.get("success"):
|
||||
logger.warning(
|
||||
f"Failed to charge tokens for user {current_user['id']} in text_chat_send: "
|
||||
f"Failed to charge tokens for user {numeric_user_id} in text_chat_send: "
|
||||
f"{charge_result.get('message')}"
|
||||
)
|
||||
|
||||
@ -224,22 +227,20 @@ async def websocket_chat_endpoint(websocket: WebSocket, user_id: str):
|
||||
|
||||
async for db_session in get_db():
|
||||
try:
|
||||
is_guest = isinstance(user_id, str) and user_id.startswith("guest_")
|
||||
numeric_user_id = await resolve_user_id(user_id, db_session)
|
||||
token_service = TokenService(db_session)
|
||||
|
||||
# guest用户跳过token检查
|
||||
if not is_guest:
|
||||
token_check = await token_service.check_available_tokens(numeric_user_id)
|
||||
if not token_check["has_tokens"]:
|
||||
await websocket.send_text(json.dumps({
|
||||
"type": "error",
|
||||
"error": "insufficient_tokens",
|
||||
"message": "您的可用token已耗尽,请联系管理员充值",
|
||||
"available_tokens": token_check["available_tokens"],
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}))
|
||||
break
|
||||
# 检查用户token
|
||||
token_check = await token_service.check_available_tokens(numeric_user_id)
|
||||
if not token_check["has_tokens"]:
|
||||
await websocket.send_text(json.dumps({
|
||||
"type": "error",
|
||||
"error": "insufficient_tokens",
|
||||
"message": "您的可用token已耗尽,请联系管理员充值",
|
||||
"available_tokens": token_check["available_tokens"],
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}))
|
||||
break
|
||||
|
||||
await websocket.send_text(json.dumps({
|
||||
"type": "typing_start",
|
||||
@ -258,7 +259,7 @@ async def websocket_chat_endpoint(websocket: WebSocket, user_id: str):
|
||||
):
|
||||
if isinstance(chunk, dict):
|
||||
remaining_tokens = None
|
||||
if chunk.get("type") == "stats" and not is_guest:
|
||||
if chunk.get("type") == "stats" and not user_id.startswith("guest_"):
|
||||
charge_result = await token_service.charge_tokens(
|
||||
user_id=numeric_user_id,
|
||||
mode="text_chat",
|
||||
@ -1000,6 +1001,8 @@ async def websocket_volcano_bidirectional_endpoint(websocket: WebSocket, user_id
|
||||
3. 支持流式音频返回
|
||||
"""
|
||||
from config.settings import settings
|
||||
from services.token_service import TokenService
|
||||
from utils.database import get_db
|
||||
import uuid
|
||||
|
||||
await websocket.accept()
|
||||
@ -1023,6 +1026,27 @@ async def websocket_volcano_bidirectional_endpoint(websocket: WebSocket, user_id
|
||||
message_type = message.get("type")
|
||||
|
||||
if message_type == "start_session":
|
||||
# 检查用户token
|
||||
token_check_result = None
|
||||
async for db_session in get_db():
|
||||
try:
|
||||
numeric_user_id = await resolve_user_id(user_id, db_session)
|
||||
token_service = TokenService(db_session)
|
||||
token_check_result = await token_service.check_available_tokens(numeric_user_id)
|
||||
finally:
|
||||
await db_session.close()
|
||||
break
|
||||
|
||||
if token_check_result and not token_check_result.get("has_tokens"):
|
||||
await websocket.send_text(json.dumps({
|
||||
"type": "error",
|
||||
"error": "insufficient_tokens",
|
||||
"message": "您的可用token已耗尽,请联系管理员充值",
|
||||
"available_tokens": token_check_result.get("available_tokens", 0),
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}))
|
||||
continue
|
||||
|
||||
text = message.get("text", "欢迎使用AI宠物语音助手")
|
||||
voice_type = message.get("voice_type", "zh_female_xiaohe_uranus_bigtts")
|
||||
encoding = message.get("encoding", "mp3")
|
||||
|
||||
@ -60,6 +60,8 @@ async def _charge_voice_tokens(
|
||||
) -> Dict[str, Any]:
|
||||
"""统一语音扣费入口,补充清晰日志。"""
|
||||
session_state = session_state or {}
|
||||
|
||||
# 检查是否已经扣费
|
||||
if session_state.get("token_charged"):
|
||||
logger.info(
|
||||
"[VOICE_CHARGE_SKIP] user_id=%s mode=%s reason=%s conversation_id=%s trace_id=%s already_charged=True",
|
||||
@ -71,27 +73,49 @@ async def _charge_voice_tokens(
|
||||
)
|
||||
return {"success": True, "skipped": True, "message": "already charged"}
|
||||
|
||||
# 获取或创建数据库会话和token服务
|
||||
token_service = session_state.get("token_service")
|
||||
db_session = session_state.get("db_session")
|
||||
if token_service is None:
|
||||
|
||||
# 如果没有提供会话,则创建新会话并在同一上下文中完成所有操作
|
||||
if token_service is None or db_session is None:
|
||||
async with AsyncSessionLocal() as db:
|
||||
token_service = TokenService(db)
|
||||
numeric_user_id = await resolve_user_id(user_id, db)
|
||||
return await _charge_voice_tokens(
|
||||
user_id=user_id,
|
||||
|
||||
charge_cfg_fallback = fallback_amount if fallback_amount is not None else VOICE_TOKEN_FALLBACKS.get(mode, 200)
|
||||
result = await token_service.charge_tokens(
|
||||
user_id=numeric_user_id,
|
||||
mode=mode,
|
||||
session_state={**session_state, "token_service": token_service, "db_session": db},
|
||||
tokens_used=tokens_used,
|
||||
fallback_amount=fallback_amount,
|
||||
reason=reason,
|
||||
fallback_amount=charge_cfg_fallback,
|
||||
)
|
||||
|
||||
if db_session is None:
|
||||
async with AsyncSessionLocal() as db:
|
||||
numeric_user_id = await resolve_user_id(user_id, db)
|
||||
else:
|
||||
numeric_user_id = await resolve_user_id(user_id, db_session)
|
||||
|
||||
token_used_amount = int(result.get("amount") or 0)
|
||||
logger.info(
|
||||
"[VOICE_CHARGE_%s] user_id=%s mode=%s reason=%s conversation_id=%s trace_id=%s amount=%s available_tokens=%s success=%s message=%s",
|
||||
"OK" if result.get("success") else "FAIL",
|
||||
user_id,
|
||||
mode,
|
||||
reason,
|
||||
session_state.get("conversation_id"),
|
||||
session_state.get("trace_id"),
|
||||
token_used_amount,
|
||||
result.get("available_tokens"),
|
||||
result.get("success"),
|
||||
result.get("message"),
|
||||
)
|
||||
if result.get("success"):
|
||||
session_state["token_charged"] = True
|
||||
session_state["tokens_used"] = tokens_used or session_state.get("tokens_used")
|
||||
|
||||
# 确保事务提交(虽然charge_tokens已经提交,但这里再次确认)
|
||||
await db.commit()
|
||||
return result
|
||||
|
||||
# 使用已提供的会话
|
||||
numeric_user_id = await resolve_user_id(user_id, db_session)
|
||||
|
||||
charge_cfg_fallback = fallback_amount if fallback_amount is not None else VOICE_TOKEN_FALLBACKS.get(mode, 200)
|
||||
result = await token_service.charge_tokens(
|
||||
user_id=numeric_user_id,
|
||||
@ -677,13 +701,6 @@ def _is_guest_user_id(user_id: Optional[str]) -> bool:
|
||||
|
||||
async def _check_user_has_tokens(user_id: str) -> Dict[str, Any]:
|
||||
"""检查用户是否还有可用token"""
|
||||
if _is_guest_user_id(user_id):
|
||||
return {
|
||||
"has_tokens": True,
|
||||
"available_tokens": 999999,
|
||||
"message": "Guest user bypassed token check"
|
||||
}
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
token_service = TokenService(db)
|
||||
try:
|
||||
|
||||
@ -52,7 +52,6 @@ class TokenService:
|
||||
Returns:
|
||||
包含统计信息的字典
|
||||
"""
|
||||
# 计算总Token数
|
||||
total_tokens = input_tokens + output_tokens
|
||||
|
||||
# 创建聊天记录
|
||||
@ -70,14 +69,13 @@ class TokenService:
|
||||
created_at=datetime.now()
|
||||
)
|
||||
|
||||
# 保存聊天记录(直接使用 db_session,不依赖 ChatLogManager)
|
||||
# 保存聊天记录
|
||||
self.db_session.add(chat_log)
|
||||
await self.db_session.commit()
|
||||
|
||||
# 检查用户配额
|
||||
quota_info = await self._check_user_quota(user_id)
|
||||
if not quota_info["has_quota"]:
|
||||
logger.warning(f"User {user_id} has no remaining quota")
|
||||
logger.warning(f"[TOKEN] User {user_id} has no remaining quota")
|
||||
return {
|
||||
"success": False,
|
||||
"reason": "insufficient_quota",
|
||||
@ -87,10 +85,10 @@ class TokenService:
|
||||
"total_tokens": total_tokens
|
||||
}
|
||||
|
||||
# 扣除可用token(按实际消耗的token数)
|
||||
# 扣除可用token
|
||||
consume_result = await self.consume_available_tokens(user_id, total_tokens)
|
||||
if not consume_result["success"]:
|
||||
logger.warning(f"User {user_id} failed to consume tokens: {consume_result['message']}")
|
||||
logger.warning(f"[TOKEN] User {user_id} failed to consume tokens: {consume_result['message']}")
|
||||
return {
|
||||
"success": False,
|
||||
"reason": "insufficient_quota",
|
||||
@ -100,8 +98,11 @@ class TokenService:
|
||||
"total_tokens": total_tokens
|
||||
}
|
||||
|
||||
# 提交事务
|
||||
await self.db_session.commit()
|
||||
|
||||
logger.info(
|
||||
f"Token usage recorded: user={user_id}, "
|
||||
f"[TOKEN] Token usage recorded: user={user_id}, "
|
||||
f"input_tokens={input_tokens}, output_tokens={output_tokens}, "
|
||||
f"total={total_tokens}, remaining={consume_result['available_tokens']}"
|
||||
)
|
||||
@ -119,12 +120,6 @@ class TokenService:
|
||||
async def _check_user_quota(self, user_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
检查用户配额情况
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
配额信息字典
|
||||
"""
|
||||
wallet = await self.wallet_manager.get_user_wallet(user_id)
|
||||
|
||||
@ -153,12 +148,6 @@ class TokenService:
|
||||
async def check_available_tokens(self, user_id: int) -> Dict[str, Any]:
|
||||
"""
|
||||
检查用户可用token是否充足
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
可用token信息字典
|
||||
"""
|
||||
wallet = await self.wallet_manager.get_user_wallet(user_id)
|
||||
|
||||
@ -183,21 +172,28 @@ class TokenService:
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
amount: 消耗数量(按对话次数)
|
||||
amount: 消耗数量
|
||||
|
||||
Returns:
|
||||
消耗结果信息字典
|
||||
"""
|
||||
wallet = await self.wallet_manager.get_user_wallet(user_id)
|
||||
logger.info(f"[TOKEN] Attempting to consume {amount} tokens for user {user_id}")
|
||||
|
||||
# 使用FOR UPDATE锁定记录,防止并发问题
|
||||
wallet = await self._get_wallet_for_update(user_id)
|
||||
|
||||
if not wallet:
|
||||
logger.error(f"[TOKEN] User {user_id} wallet not found")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "User wallet not found"
|
||||
}
|
||||
|
||||
available_tokens = wallet.available_tokens or 0
|
||||
logger.debug(f"[TOKEN] User {user_id} has {available_tokens} tokens available, needs {amount}")
|
||||
|
||||
if available_tokens < amount:
|
||||
logger.warning(f"[TOKEN] User {user_id} insufficient tokens: available={available_tokens}, required={amount}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Insufficient available tokens",
|
||||
@ -205,21 +201,33 @@ class TokenService:
|
||||
"required": amount
|
||||
}
|
||||
|
||||
# 执行扣费
|
||||
wallet.available_tokens = available_tokens - amount
|
||||
wallet.total_consumed = (wallet.total_consumed or 0) + amount
|
||||
wallet.updated_at = datetime.now()
|
||||
|
||||
await self.wallet_manager.update(wallet)
|
||||
# 使用merge确保对象被正确跟踪
|
||||
updated_wallet = await self.db_session.merge(wallet)
|
||||
|
||||
logger.info(f"Consumed {amount} tokens from user {user_id}, remaining: {wallet.available_tokens}")
|
||||
logger.info(f"[TOKEN] Consumed {amount} tokens from user {user_id}, remaining: {updated_wallet.available_tokens}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Tokens consumed successfully",
|
||||
"available_tokens": wallet.available_tokens,
|
||||
"available_tokens": updated_wallet.available_tokens,
|
||||
"consumed": amount
|
||||
}
|
||||
|
||||
async def _get_wallet_for_update(self, user_id: int):
|
||||
"""
|
||||
获取钱包记录并加锁,防止并发修改
|
||||
"""
|
||||
from models.database import UserWallet
|
||||
result = await self.db_session.execute(
|
||||
select(UserWallet).where(UserWallet.user_id == user_id).with_for_update()
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def charge_tokens(
|
||||
self,
|
||||
user_id: int,
|
||||
@ -239,8 +247,11 @@ class TokenService:
|
||||
Returns:
|
||||
计费结果信息字典
|
||||
"""
|
||||
logger.info(f"[TOKEN] Charging tokens for user {user_id}, mode={mode}")
|
||||
|
||||
token_check = await self.check_available_tokens(user_id)
|
||||
if not token_check.get("has_tokens"):
|
||||
logger.warning(f"[TOKEN] User {user_id} has no tokens remaining")
|
||||
return {
|
||||
"success": False,
|
||||
"reason": "insufficient_tokens",
|
||||
@ -252,12 +263,20 @@ class TokenService:
|
||||
|
||||
amount = 0
|
||||
if isinstance(tokens_used, dict):
|
||||
amount = int(tokens_used.get("total") or 0)
|
||||
# 优先查找 total 或 total_tokens 字段
|
||||
amount = int(tokens_used.get("total") or tokens_used.get("total_tokens") or 0)
|
||||
|
||||
# 如果没有 total 字段,则计算 input + output
|
||||
if amount <= 0:
|
||||
input_tokens = int(tokens_used.get("input") or tokens_used.get("input_tokens") or 0)
|
||||
output_tokens = int(tokens_used.get("output") or tokens_used.get("output_tokens") or 0)
|
||||
amount = input_tokens + output_tokens
|
||||
|
||||
if amount <= 0:
|
||||
amount = int(fallback_amount or 0)
|
||||
|
||||
if amount <= 0:
|
||||
logger.info(f"[TOKEN] No charge required for user {user_id}, amount={amount}")
|
||||
return {
|
||||
"success": True,
|
||||
"mode": mode,
|
||||
@ -268,6 +287,7 @@ class TokenService:
|
||||
|
||||
consume_result = await self.consume_available_tokens(user_id, amount)
|
||||
if not consume_result.get("success"):
|
||||
logger.warning(f"[TOKEN] Failed to consume tokens for user {user_id}: {consume_result.get('message')}")
|
||||
return {
|
||||
"success": False,
|
||||
"reason": "insufficient_tokens",
|
||||
@ -277,6 +297,11 @@ class TokenService:
|
||||
"message": consume_result.get("message", "Failed to consume tokens")
|
||||
}
|
||||
|
||||
# 提交事务
|
||||
await self.db_session.commit()
|
||||
|
||||
logger.info(f"[TOKEN] Successfully charged {amount} tokens for user {user_id}, mode={mode}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"mode": mode,
|
||||
@ -315,12 +340,12 @@ class TokenService:
|
||||
input_tokens = int(tokens_used.get("input") or 0)
|
||||
output_tokens = int(tokens_used.get("output") or 0)
|
||||
if input_tokens + output_tokens <= 0:
|
||||
total = int(tokens_used.get("total") or 0)
|
||||
total = int(tokens_used.get("total") or tokens_used.get("total_tokens") or 0)
|
||||
input_tokens = total
|
||||
if input_tokens + output_tokens <= 0:
|
||||
input_tokens = int(fallback_amount or 0)
|
||||
|
||||
# 直接通过 db_session 写入 ChatLog,不依赖 ChatLogManager(其 create 方法为桩实现)
|
||||
# 写入 ChatLog
|
||||
chat_log = ChatLog(
|
||||
user_id=user_id,
|
||||
trace_id=trace_id,
|
||||
@ -335,9 +360,9 @@ class TokenService:
|
||||
created_at=datetime.now()
|
||||
)
|
||||
self.db_session.add(chat_log)
|
||||
await self.db_session.commit()
|
||||
|
||||
if skip_charge:
|
||||
await self.db_session.commit()
|
||||
return {
|
||||
"success": True,
|
||||
"mode": mode,
|
||||
@ -355,54 +380,40 @@ class TokenService:
|
||||
async def _reset_daily_quota_if_needed(self, wallet: UserWallet) -> None:
|
||||
"""
|
||||
检查并重置每日配额(如果跨天了)
|
||||
|
||||
Args:
|
||||
wallet: 用户钱包对象
|
||||
"""
|
||||
today = date.today()
|
||||
|
||||
# 如果上次重置时间不是今天,则重置每日配额
|
||||
if wallet.last_reset_time != today:
|
||||
logger.info(f"Resetting daily quota for user {wallet.user_id}")
|
||||
wallet.daily_quota = 20 # 重置为默认每日配额
|
||||
logger.info(f"[TOKEN] Resetting daily quota for user {wallet.user_id}")
|
||||
wallet.daily_quota = 20
|
||||
wallet.last_reset_time = today
|
||||
await self.wallet_manager.update(wallet)
|
||||
await self.db_session.merge(wallet)
|
||||
await self.db_session.commit()
|
||||
|
||||
async def _consume_quota(self, user_id: int, amount: int = 1) -> bool:
|
||||
"""
|
||||
扣除用户配额
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
amount: 扣除数量
|
||||
|
||||
Returns:
|
||||
是否成功扣除
|
||||
扣除用户配额(旧版方法,保留兼容性)
|
||||
"""
|
||||
wallet = await self.wallet_manager.get_user_wallet(user_id)
|
||||
wallet = await self._get_wallet_for_update(user_id)
|
||||
|
||||
if not wallet:
|
||||
return False
|
||||
|
||||
# 检查剩余配额
|
||||
remaining = wallet.daily_quota + wallet.extra_quota
|
||||
if remaining < amount:
|
||||
return False
|
||||
|
||||
# 优先扣除每日配额,再扣除额外配额
|
||||
if wallet.daily_quota >= amount:
|
||||
wallet.daily_quota -= amount
|
||||
else:
|
||||
# 扣除所有每日配额
|
||||
amount_to_deduct_from_extra = amount - wallet.daily_quota
|
||||
wallet.daily_quota = 0
|
||||
wallet.extra_quota -= amount_to_deduct_from_extra
|
||||
|
||||
# 增加总消耗数
|
||||
wallet.total_consumed += amount
|
||||
|
||||
# 更新钱包
|
||||
await self.wallet_manager.update(wallet)
|
||||
await self.db_session.merge(wallet)
|
||||
await self.db_session.commit()
|
||||
|
||||
return True
|
||||
|
||||
@ -414,16 +425,7 @@ class TokenService:
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取用户Token使用统计
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
start_date: 开始日期
|
||||
end_date: 结束日期
|
||||
|
||||
Returns:
|
||||
统计信息字典
|
||||
"""
|
||||
# 构建查询条件(使用 coalesce 避免 NULL)
|
||||
base_conditions = [ChatLog.user_id == user_id]
|
||||
if start_date:
|
||||
base_conditions.append(ChatLog.created_at >= start_date)
|
||||
@ -449,24 +451,20 @@ class TokenService:
|
||||
ChatLog.conversation_id.like("realtime-%")
|
||||
)
|
||||
|
||||
# 执行查询
|
||||
total_input = (await self.db_session.execute(total_input_query)).scalar() or 0
|
||||
total_output = (await self.db_session.execute(total_output_query)).scalar() or 0
|
||||
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
|
||||
|
||||
# 钱包维度的消耗:以钱包总消耗为准,更适合反映实际扣费
|
||||
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
|
||||
|
||||
# 计算文字聊天Token(ChatLog总Token - 语音聊天Token - 实时通话Token)
|
||||
text_chat_tokens = max(0, chatlog_total - voice_tokens - realtime_tokens)
|
||||
|
||||
return {
|
||||
@ -498,37 +496,24 @@ class TokenService:
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
获取系统整体Token使用统计
|
||||
|
||||
Args:
|
||||
start_date: 开始日期
|
||||
end_date: 结束日期
|
||||
|
||||
Returns:
|
||||
系统统计信息字典
|
||||
"""
|
||||
# 构建基础查询
|
||||
base_query = select(ChatLog)
|
||||
|
||||
# 添加日期过滤
|
||||
if start_date:
|
||||
base_query = base_query.where(ChatLog.created_at >= start_date)
|
||||
|
||||
if end_date:
|
||||
base_query = base_query.where(ChatLog.created_at <= end_date)
|
||||
|
||||
# 获取所有记录
|
||||
result = await self.db_session.execute(base_query)
|
||||
all_logs = result.scalars().all()
|
||||
|
||||
# 计算统计信息
|
||||
total_input_tokens = sum(log.tokens_input for log in all_logs)
|
||||
total_output_tokens = sum(log.tokens_output for log in all_logs)
|
||||
total_chats = len(all_logs)
|
||||
|
||||
# 获取唯一用户数
|
||||
unique_users = len(set(log.user_id for log in all_logs))
|
||||
|
||||
# 计算平均响应时间
|
||||
avg_response_time = 0
|
||||
if all_logs:
|
||||
avg_response_time = sum(log.duration_ms for log in all_logs) / len(all_logs)
|
||||
@ -549,26 +534,66 @@ class TokenService:
|
||||
async def gift_quota(self, user_id: int, amount: int, quota_type: str = "extra") -> bool:
|
||||
"""
|
||||
管理员赠送配额给用户
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
amount: 赠送数量
|
||||
quota_type: 配额类型 ("daily" 或 "extra")
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
wallet = await self.wallet_manager.get_user_wallet(user_id)
|
||||
wallet = await self._get_wallet_for_update(user_id)
|
||||
|
||||
if not wallet:
|
||||
return False
|
||||
|
||||
if quota_type == "daily":
|
||||
wallet.daily_quota += amount
|
||||
else: # extra
|
||||
else:
|
||||
wallet.extra_quota += amount
|
||||
|
||||
await self.wallet_manager.update(wallet)
|
||||
await self.db_session.merge(wallet)
|
||||
await self.db_session.commit()
|
||||
|
||||
logger.info(f"Gifted {amount} {quota_type} quota to user {user_id}")
|
||||
logger.info(f"[TOKEN] Gifted {amount} {quota_type} quota to user {user_id}")
|
||||
return True
|
||||
|
||||
async def set_available_tokens(self, user_id: int, amount: int) -> bool:
|
||||
"""
|
||||
设置用户可用token(管理员操作)
|
||||
"""
|
||||
wallet = await self._get_wallet_for_update(user_id)
|
||||
|
||||
if not wallet:
|
||||
# 创建新钱包
|
||||
wallet = UserWallet(
|
||||
user_id=user_id,
|
||||
available_tokens=amount,
|
||||
daily_quota=0,
|
||||
extra_quota=0,
|
||||
total_consumed=0,
|
||||
last_reset_time=date.today()
|
||||
)
|
||||
self.db_session.add(wallet)
|
||||
else:
|
||||
wallet.available_tokens = amount
|
||||
|
||||
await self.db_session.commit()
|
||||
logger.info(f"[TOKEN] Set available tokens for user {user_id} to {amount}")
|
||||
return True
|
||||
|
||||
async def add_available_tokens(self, user_id: int, amount: int) -> bool:
|
||||
"""
|
||||
增加用户可用token(管理员操作)
|
||||
"""
|
||||
wallet = await self._get_wallet_for_update(user_id)
|
||||
|
||||
if not wallet:
|
||||
wallet = UserWallet(
|
||||
user_id=user_id,
|
||||
available_tokens=amount,
|
||||
daily_quota=0,
|
||||
extra_quota=0,
|
||||
total_consumed=0,
|
||||
last_reset_time=date.today()
|
||||
)
|
||||
self.db_session.add(wallet)
|
||||
else:
|
||||
wallet.available_tokens = (wallet.available_tokens or 0) + amount
|
||||
|
||||
await self.db_session.commit()
|
||||
logger.info(f"[TOKEN] Added {amount} tokens to user {user_id}")
|
||||
return True
|
||||
|
||||
115
backend/test_token_charge.py
Normal file
115
backend/test_token_charge.py
Normal file
@ -0,0 +1,115 @@
|
||||
"""
|
||||
Token扣费流程测试脚本
|
||||
用于验证token扣费逻辑是否正常工作
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from sqlalchemy import select
|
||||
from services.token_service import TokenService
|
||||
from utils.database import AsyncSessionLocal
|
||||
|
||||
async def test_token_charge():
|
||||
"""测试token扣费流程"""
|
||||
print("=== Token扣费流程测试 ===")
|
||||
|
||||
async with AsyncSessionLocal() as session:
|
||||
# 创建测试用户钱包
|
||||
test_user_id = 1
|
||||
|
||||
from models.database import UserWallet
|
||||
|
||||
# 先检查是否已有钱包
|
||||
result = await session.execute(
|
||||
select(UserWallet).where(UserWallet.user_id == test_user_id)
|
||||
)
|
||||
wallet = result.scalar_one_or_none()
|
||||
|
||||
if not wallet:
|
||||
# 创建新钱包
|
||||
wallet = UserWallet(
|
||||
user_id=test_user_id,
|
||||
available_tokens=100,
|
||||
daily_quota=0,
|
||||
extra_quota=0,
|
||||
total_consumed=0
|
||||
)
|
||||
session.add(wallet)
|
||||
await session.commit()
|
||||
await session.refresh(wallet)
|
||||
print(f"创建测试钱包: user_id={wallet.user_id}, available_tokens={wallet.available_tokens}")
|
||||
else:
|
||||
print(f"找到现有钱包: user_id={wallet.user_id}, available_tokens={wallet.available_tokens}")
|
||||
|
||||
# 重置钱包金额以便测试
|
||||
wallet.available_tokens = 100
|
||||
wallet.total_consumed = 0
|
||||
await session.commit()
|
||||
await session.refresh(wallet)
|
||||
|
||||
# 创建token服务
|
||||
token_service = TokenService(session)
|
||||
|
||||
# 测试1: 检查可用token
|
||||
print("\n--- 测试1: 检查可用token ---")
|
||||
check_result = await token_service.check_available_tokens(test_user_id)
|
||||
print(f"检查结果: {check_result}")
|
||||
|
||||
# 测试2: 执行扣费
|
||||
print("\n--- 测试2: 执行扣费 ---")
|
||||
print(f"扣费前可用token: {wallet.available_tokens}")
|
||||
|
||||
charge_result = await token_service.charge_tokens(
|
||||
user_id=test_user_id,
|
||||
mode="test",
|
||||
tokens_used={"total": 10},
|
||||
fallback_amount=5
|
||||
)
|
||||
|
||||
print(f"扣费结果: {charge_result}")
|
||||
|
||||
# 重新查询钱包状态
|
||||
await session.refresh(wallet)
|
||||
print(f"扣费后可用token: {wallet.available_tokens}")
|
||||
print(f"总消耗token: {wallet.total_consumed}")
|
||||
|
||||
# 测试3: 再次扣费(测试多次扣费)
|
||||
print("\n--- 测试3: 再次扣费 ---")
|
||||
charge_result2 = await token_service.charge_tokens(
|
||||
user_id=test_user_id,
|
||||
mode="test2",
|
||||
tokens_used=None,
|
||||
fallback_amount=20
|
||||
)
|
||||
|
||||
print(f"扣费结果: {charge_result2}")
|
||||
|
||||
await session.refresh(wallet)
|
||||
print(f"第二次扣费后可用token: {wallet.available_tokens}")
|
||||
print(f"总消耗token: {wallet.total_consumed}")
|
||||
|
||||
# 测试4: 测试余额不足的情况
|
||||
print("\n--- 测试4: 测试余额不足 ---")
|
||||
# 设置余额为0
|
||||
wallet.available_tokens = 0
|
||||
await session.commit()
|
||||
|
||||
check_result = await token_service.check_available_tokens(test_user_id)
|
||||
print(f"余额为0时检查结果: {check_result}")
|
||||
|
||||
charge_result3 = await token_service.charge_tokens(
|
||||
user_id=test_user_id,
|
||||
mode="test3",
|
||||
tokens_used={"total": 5}
|
||||
)
|
||||
|
||||
print(f"余额不足时扣费结果: {charge_result3}")
|
||||
|
||||
print("\n=== 测试完成 ===")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(test_token_charge())
|
||||
@ -22,8 +22,6 @@ async def get_db() -> AsyncGenerator[Session, None]:
|
||||
获取数据库会话
|
||||
这是一个简化版本,实际应该使用异步数据库会话
|
||||
"""
|
||||
# 在实际应用中,这里应该创建异步数据库会话
|
||||
# 目前返回模拟会话
|
||||
class MockSession:
|
||||
def execute(self, query):
|
||||
return MockResult()
|
||||
@ -80,9 +78,11 @@ class UserWalletManager(BaseManager):
|
||||
|
||||
async def update(self, wallet):
|
||||
"""更新钱包记录"""
|
||||
# 使用 merge 方法确保对象被当前会话跟踪
|
||||
merged_wallet = await self.db_session.merge(wallet)
|
||||
await self.db_session.commit()
|
||||
await self.db_session.refresh(wallet)
|
||||
return wallet
|
||||
await self.db_session.refresh(merged_wallet)
|
||||
return merged_wallet
|
||||
|
||||
class ChatLogManager(BaseManager):
|
||||
async def get_user_chat_history(self, user_id: int, conversation_id: str, limit: int = 20, offset: int = 0):
|
||||
|
||||
@ -8,6 +8,28 @@ import apiClient from '@/api/index'
|
||||
import { chatAPI } from '@/api/index'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
// 统计更新事件回调
|
||||
let onStatsUpdateCallbacks = []
|
||||
|
||||
// 注册统计更新回调
|
||||
export function onChatStatsUpdate(callback) {
|
||||
onStatsUpdateCallbacks.push(callback)
|
||||
return () => {
|
||||
onStatsUpdateCallbacks = onStatsUpdateCallbacks.filter(cb => cb !== callback)
|
||||
}
|
||||
}
|
||||
|
||||
// 触发统计更新
|
||||
function triggerStatsUpdate() {
|
||||
onStatsUpdateCallbacks.forEach(cb => {
|
||||
try {
|
||||
cb()
|
||||
} catch (error) {
|
||||
console.error('[ChatStore] Stats update callback error:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const useChatStore = defineStore('chat', () => {
|
||||
// ============ 状态 ============
|
||||
const messages = ref([])
|
||||
@ -24,7 +46,8 @@ export const useChatStore = defineStore('chat', () => {
|
||||
const audioContext = ref(null)
|
||||
const mediaRecorder = ref(null)
|
||||
const audioChunks = ref([])
|
||||
|
||||
const permissionStream = ref(null)
|
||||
|
||||
// 麦克风权限状态
|
||||
const microphonePermissionState = ref('prompt') // 'granted' | 'denied' | 'prompt'
|
||||
const hasRequestedPermission = ref(false)
|
||||
@ -474,7 +497,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
break
|
||||
|
||||
case 'chat_ended':
|
||||
// AI回复结束
|
||||
// AI 回复结束
|
||||
console.log('[ChatStore] Chat ended, total content:', pendingChatContent.value?.slice(0, 50))
|
||||
const streamingMsg = messages.value.find(m => m.isStreaming)
|
||||
if (streamingMsg) {
|
||||
@ -482,6 +505,8 @@ export const useChatStore = defineStore('chat', () => {
|
||||
streamingMsg.isStreaming = false
|
||||
}
|
||||
pendingChatContent.value = ''
|
||||
// 触发统计更新
|
||||
triggerStatsUpdate()
|
||||
break
|
||||
|
||||
case 'tts_start':
|
||||
@ -520,6 +545,8 @@ export const useChatStore = defineStore('chat', () => {
|
||||
case 'session_stopped':
|
||||
console.log('[ChatStore] Voice session stopped')
|
||||
resetAudioState()
|
||||
// 触发统计更新
|
||||
triggerStatsUpdate()
|
||||
break
|
||||
|
||||
case 'dialog_error':
|
||||
@ -600,6 +627,8 @@ export const useChatStore = defineStore('chat', () => {
|
||||
background_id: currentBackgroundId.value
|
||||
}
|
||||
|
||||
voiceSessionReady.value = false
|
||||
|
||||
const sent = sendVoiceMessage({
|
||||
type: 'start_session',
|
||||
...sessionConfig
|
||||
@ -608,6 +637,37 @@ export const useChatStore = defineStore('chat', () => {
|
||||
if (!sent) {
|
||||
throw new Error('语音连接中断,请稍后重试')
|
||||
}
|
||||
|
||||
const isSessionReady = await waitForVoiceSessionReady(5000)
|
||||
if (!isSessionReady) {
|
||||
throw new Error('语音会话启动失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
function waitForVoiceSessionReady(timeoutMs = 5000) {
|
||||
return new Promise((resolve) => {
|
||||
if (voiceSessionReady.value) {
|
||||
resolve(true)
|
||||
return
|
||||
}
|
||||
|
||||
const start = Date.now()
|
||||
const tick = () => {
|
||||
if (voiceSessionReady.value) {
|
||||
resolve(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (Date.now() - start >= timeoutMs) {
|
||||
resolve(false)
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(tick, 50)
|
||||
}
|
||||
|
||||
tick()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@ -700,6 +760,7 @@ export const useChatStore = defineStore('chat', () => {
|
||||
}, 300)
|
||||
}
|
||||
isVoiceConnected.value = false
|
||||
voiceSessionReady.value = false
|
||||
}
|
||||
|
||||
// ============ 实时通话功能 ============
|
||||
@ -889,6 +950,8 @@ export const useChatStore = defineStore('chat', () => {
|
||||
console.log('[ChatStore] Call ended')
|
||||
callStartTime.value = null
|
||||
resetAudioState()
|
||||
// 触发统计更新
|
||||
triggerStatsUpdate()
|
||||
break
|
||||
|
||||
case 'error':
|
||||
@ -1073,18 +1136,20 @@ export const useChatStore = defineStore('chat', () => {
|
||||
|
||||
try {
|
||||
console.log('[ChatStore] Requesting microphone permission...');
|
||||
// 先尝试简单的约束,提高移动端兼容性
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: true
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
sampleRate: 16000,
|
||||
channelCount: 1,
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true
|
||||
}
|
||||
})
|
||||
|
||||
console.log('[ChatStore] Microphone permission granted');
|
||||
microphonePermissionState.value = 'granted'
|
||||
hasRequestedPermission.value = true
|
||||
|
||||
// 获取到权限后立即释放流,避免占用麦克风
|
||||
stream.getTracks().forEach(track => track.stop())
|
||||
|
||||
permissionStream.value = stream
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('请求麦克风权限失败:', error)
|
||||
@ -1134,21 +1199,22 @@ export const useChatStore = defineStore('chat', () => {
|
||||
return false
|
||||
}
|
||||
|
||||
let stream;
|
||||
try {
|
||||
// 尝试最佳配置
|
||||
stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
sampleRate: 16000,
|
||||
channelCount: 1,
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.warn('[ChatStore] Failed to get user media with strict constraints, falling back to basic audio', e);
|
||||
// 回退到基础音频配置
|
||||
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
let stream = permissionStream.value
|
||||
if (!stream || stream.getTracks().every(t => t.readyState === 'ended')) {
|
||||
try {
|
||||
stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
sampleRate: 16000,
|
||||
channelCount: 1,
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.warn('[ChatStore] Failed to get user media with strict constraints, falling back to basic audio', e);
|
||||
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||
}
|
||||
permissionStream.value = stream
|
||||
}
|
||||
|
||||
// 创建或恢复 AudioContext
|
||||
|
||||
@ -60,6 +60,7 @@ export const useUserStore = defineStore('user', {
|
||||
|
||||
localStorage.setItem('access_token', access_token)
|
||||
localStorage.setItem('refresh_token', refresh_token)
|
||||
localStorage.setItem('user', JSON.stringify(user))
|
||||
|
||||
console.log('[UserStore] Token saved to localStorage, checking:', localStorage.getItem('access_token')?.substring(0, 50) + '...')
|
||||
|
||||
@ -86,6 +87,7 @@ export const useUserStore = defineStore('user', {
|
||||
|
||||
localStorage.setItem('access_token', access_token)
|
||||
localStorage.setItem('refresh_token', refresh_token)
|
||||
localStorage.setItem('user', JSON.stringify(user))
|
||||
|
||||
return { success: true, user }
|
||||
} catch (error) {
|
||||
@ -110,6 +112,7 @@ export const useUserStore = defineStore('user', {
|
||||
|
||||
localStorage.setItem('access_token', access_token)
|
||||
localStorage.setItem('refresh_token', refresh_token)
|
||||
localStorage.setItem('user', JSON.stringify(user))
|
||||
|
||||
return { success: true, user }
|
||||
} catch (error) {
|
||||
@ -171,6 +174,7 @@ export const useUserStore = defineStore('user', {
|
||||
|
||||
localStorage.removeItem('access_token')
|
||||
localStorage.removeItem('refresh_token')
|
||||
localStorage.removeItem('user')
|
||||
},
|
||||
|
||||
// 加载用户资料
|
||||
|
||||
@ -429,7 +429,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
UserFilled,
|
||||
@ -454,6 +454,7 @@ import { useUserStore } from '@/stores/userStore'
|
||||
import { backgroundAPI } from '@/api'
|
||||
import { useRouter } from 'vue-router'
|
||||
import userApi from '@/services/userApi'
|
||||
import { onChatStatsUpdate } from '@/stores/chatStore'
|
||||
|
||||
export default {
|
||||
name: 'ProfileView',
|
||||
@ -856,11 +857,27 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// 统计更新清理函数
|
||||
let cleanupStatsListener = null
|
||||
|
||||
// 组件挂载
|
||||
onMounted(async () => {
|
||||
await loadUserInfo()
|
||||
await loadUserBackgrounds()
|
||||
await loadUserStats() // 加载用户统计信息
|
||||
|
||||
// 注册聊天统计更新监听
|
||||
cleanupStatsListener = onChatStatsUpdate(() => {
|
||||
console.log('[ProfileView] Chat stats updated, refreshing...')
|
||||
loadUserStats()
|
||||
})
|
||||
})
|
||||
|
||||
// 组件卸载时清理监听器
|
||||
onUnmounted(() => {
|
||||
if (cleanupStatsListener) {
|
||||
cleanupStatsListener()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@ -5,8 +5,10 @@
|
||||
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
import { resolve, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const env = loadEnv(process.env.NODE_ENV || 'development', process.cwd(), '')
|
||||
|
||||
const proxyUrl = env.VITE_PROXY_TARGET || env.PROXY_URL || 'http://8.152.169.84:8001'
|
||||
@ -121,7 +123,9 @@ export default defineConfig({
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {}
|
||||
scss: {
|
||||
api: 'modern-compiler'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user