diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 1cef22a..c771513 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -21,9 +21,9 @@ class ServerSettings(BaseSettings): class DatabaseSettings(BaseSettings): """数据库配置""" - # MySQL连接格式: mysql+pymysql://username:password@host:port/database_name - DATABASE_URL: str = "mysql+pymysql://root:taiyi1224@localhost:3306/baoxiang" - + # MySQL连接格式: mysql+pymysql://username:password@host:port/database_name?charset=utf8mb4 + DATABASE_URL: str = "mysql+pymysql://root:taiyi1224@localhost:3306/baoxiang?charset=utf8mb4" + # SQLite开发配置 (可选) # SQLite连接格式: sqlite:///./database.db # DATABASE_URL: str = "sqlite:///./treasure_box_game.db" diff --git a/backend/app/core/database.py b/backend/app/core/database.py index 49e599b..11ea0d3 100644 --- a/backend/app/core/database.py +++ b/backend/app/core/database.py @@ -29,6 +29,8 @@ else: pool_recycle=db_settings.DB_POOL_RECYCLE, echo=False, future=True, + # 设置连接时使用的字符集 + connect_args={"charset": "utf8mb4", "collation": "utf8mb4_unicode_ci"} ) # 如果是MySQL,启用严格模式 @@ -37,9 +39,14 @@ if "mysql" in db_settings.DATABASE_URL: def set_sqlite_pragma(dbapi_connection, connection_record): """ 为MySQL连接设置参数(如果使用MySQL) - 注意:不再设置时区,使用服务器本地时间以保持一致性 """ - pass + # 设置字符集为utf8mb4 + try: + cursor = dbapi_connection.cursor() + cursor.execute("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci") + cursor.close() + except Exception: + pass # 创建会话 diff --git a/backend/app/services/game_service.py b/backend/app/services/game_service.py index 81e5e94..68f0d71 100644 --- a/backend/app/services/game_service.py +++ b/backend/app/services/game_service.py @@ -374,20 +374,55 @@ class GameService: ) # 平台抽水 - 直接到管理员账户 - admin_user = UserService.get_admin_user(db) - if admin_user and platform_income > 0: - admin_user.balance += platform_income - db.commit() + try: + admin_user = UserService.get_admin_user(db) + if admin_user and platform_income > 0: + # 记录转账前的余额 + old_balance = admin_user.balance + admin_user.balance += platform_income + db.commit() - UserService.create_transaction( - db=db, - user_id=admin_user.id, - transaction_type="平台抽水", - amount=platform_income, - balance_after=admin_user.balance, - related_id=chest.id, - description=f"宝箱《{chest.title}》平台抽水收益" - ) + try: + UserService.create_transaction( + db=db, + user_id=admin_user.id, + transaction_type="平台抽水", + amount=platform_income, + balance_after=admin_user.balance, + related_id=chest.id, + description=f"宝箱《{chest.title}》平台抽水收益" + ) + except Exception as e: + print(f"创建平台抽水交易记录失败: {str(e)}") + # 即使交易记录创建失败,也不应回滚余额变更 + # 可以在这里添加告警通知或其他补偿措施 + + # 通知管理员余额更新 + import asyncio + try: + # 尝试获取当前事件循环 + loop = asyncio.get_running_loop() + # 局部导入避免循环导入 + from ..routers.websocket import notify_user_balance_update + loop.create_task(notify_user_balance_update(admin_user.id, admin_user.balance)) + except RuntimeError: + # 如果没有运行中的事件循环,则在新线程中处理 + import threading + def run_async(): + async def _run(): + # 局部导入避免循环导入 + from ..routers.websocket import notify_user_balance_update + await notify_user_balance_update(admin_user.id, admin_user.balance) + asyncio.run(_run()) + thread = threading.Thread(target=run_async, daemon=True) + thread.start() + + print(f"平台抽水转账成功: {platform_income} 分至管理员账户 {admin_user.id}") + except Exception as e: + # 如果转账失败,记录错误并回滚 + db.rollback() + print(f"平台抽水转账失败: {str(e)}") + # 可以在这里添加更详细的错误处理逻辑,比如发送告警通知 # 计算赔率 winner_pool = chest.pool_a if winner == "A" else chest.pool_b diff --git a/backend/app/services/streamer_service.py b/backend/app/services/streamer_service.py index fc48c9d..60870b9 100644 --- a/backend/app/services/streamer_service.py +++ b/backend/app/services/streamer_service.py @@ -103,18 +103,32 @@ class StreamerService: active_chests = db.query(func.count(Chest.id)).filter( Chest.streamer_id == streamer.user_id, - Chest.status == "下注中" + Chest.status == 0 # BETTING状态 ).scalar() or 0 - # 计算总获奖(需要根据实际业务逻辑) - total_winnings = 0 + # 查询已结算的宝箱以计算总奖金 + settled_chests = db.query(Chest).filter( + Chest.streamer_id == streamer.user_id, + Chest.status == 3 # FINISHED状态 + ).all() + + # 计算总获奖金额(所有已结算宝箱的奖金池总和) + total_winnings = sum(int(chest.pool_a + chest.pool_b) for chest in settled_chests) + + # 计算抽成收益 total_commission = int(total_winnings * float(streamer.commission_rate) / 100) # 计算平均宝箱价值 average_chest_value = int(total_winnings / total_chests) if total_chests > 0 else 0 - # 计算成功率(这里简化处理) - success_rate = 0.75 # 示例值 + # 计算成功率(已结算宝箱中A选项获胜的比例) + settled_count = len(settled_chests) + if settled_count > 0: + a_win_count = sum(1 for chest in settled_chests if chest.result == "A") + success_rate = a_win_count / settled_count + else: + # 如果没有已结算的宝箱,则使用默认值 + success_rate = 0.0 return StreamerStatistics( total_chests=total_chests, @@ -189,7 +203,7 @@ class StreamerService: active_chests = db.query(func.count(Chest.id)).filter( Chest.streamer_id == streamer.user_id, - Chest.status == "下注中" + Chest.status == 0 # BETTING状态 ).scalar() or 0 max_chests = streamer.max_active_chests diff --git a/frontend/src/pages/TransactionHistoryPage.tsx b/frontend/src/pages/TransactionHistoryPage.tsx index 377c8b8..b93eaee 100644 --- a/frontend/src/pages/TransactionHistoryPage.tsx +++ b/frontend/src/pages/TransactionHistoryPage.tsx @@ -5,7 +5,7 @@ import Loading from '../components/Loading'; import type { Transaction } from '../types'; const TransactionHistoryPage = () => { - const { user } = useAuth(); + const { user: _user } = useAuth(); // 用户信息已在AuthContext中 const [transactions, setTransactions] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -29,30 +29,28 @@ const TransactionHistoryPage = () => { }; const getTransactionTypeText = (type: string) => { - switch (type) { - case 'BET': - return '下注'; - case 'WIN': - return '获胜奖励'; - case '低保': - return '低保'; - case '余额清零奖励': - return '余额清零奖励'; - case 'ADMIN_ADJUST': - return '管理员调整'; - case '管理员调整': - return '管理员调整'; - case 'STREAMER_REVENUE': - return '主播分润'; - case 'PLATFORM_REVENUE': - return '平台抽水'; - case 'REGISTER': - return '新用户注册奖励'; - case '注册奖励': - return '新用户注册奖励'; - default: - return type; - } + // 统一交易类型映射,确保前后端一致性 + const typeMap: Record = { + 'BET': '下注', + 'WIN': '获胜奖励', + '下注': '下注', + '获胜利': '获胜利', + 'WINNING': '获胜利', + '低保': '低保', + 'ALLOWANCE': '低保', + '余额清零奖励': '余额清零奖励', + 'ADMIN_ADJUST': '管理员调整', + '管理员调整': '管理员调整', + 'ADMIN_OPERATION': '管理员调整', + 'STREAMER_REVENUE': '主播分润', + 'PLATFORM_REVENUE': '平台抽水', + 'REGISTER': '新用户注册奖励', + '注册奖励': '新用户注册奖励', + 'REGISTER_REWARD': '新用户注册奖励', + 'NEW_USER_REWARD': '新用户注册奖励' + }; + + return typeMap[type] || type; }; const formatDate = (dateString: string) => { @@ -91,14 +89,7 @@ const TransactionHistoryPage = () => { )} - {loading ? ( -
-
- 加载中... -
-

正在加载交易记录...

-
- ) : transactions.length === 0 ? ( + {transactions.length === 0 ? (
📊

暂无交易记录

diff --git a/frontend/src/pages/UserProfile.tsx b/frontend/src/pages/UserProfile.tsx index e506dd1..0b3676c 100644 --- a/frontend/src/pages/UserProfile.tsx +++ b/frontend/src/pages/UserProfile.tsx @@ -55,8 +55,8 @@ const UserProfile = () => { setAllowanceInfo(allowanceData); // 计算用户统计数据 - const bets = txData.filter(tx => tx.type === 'BET'); - const wins = txData.filter(tx => tx.type === 'WIN'); + const bets = txData.filter(tx => tx.type === 'BET' || tx.type === '下注'); + const wins = txData.filter(tx => tx.type === 'WIN' || tx.type === '获胜利' || tx.type === 'WINNING'); const totalBets = bets.length; const totalWinnings = wins.reduce((sum, tx) => sum + tx.amount, 0); const winRate = totalBets > 0 ? (wins.length / totalBets) * 100 : 0; @@ -92,30 +92,28 @@ const UserProfile = () => { }; const getTransactionTypeText = (type: string) => { - switch (type) { - case 'BET': - return '下注'; - case 'WIN': - return '获胜奖励'; - case '低保': - return '低保'; - case '余额清零奖励': - return '余额清零奖励'; - case 'ADMIN_ADJUST': - return '管理员调整'; - case '管理员调整': - return '管理员调整'; - case 'STREAMER_REVENUE': - return '主播分润'; - case 'PLATFORM_REVENUE': - return '平台抽水'; - case 'REGISTER': - return '新用户注册奖励'; - case '注册奖励': - return '新用户注册奖励'; - default: - return type; - } + // 统一交易类型映射,确保前后端一致性 + const typeMap: Record = { + 'BET': '下注', + 'WIN': '获胜奖励', + '下注': '下注', + '获胜利': '获胜利', + 'WINNING': '获胜利', + '低保': '低保', + 'ALLOWANCE': '低保', + '余额清零奖励': '余额清零奖励', + 'ADMIN_ADJUST': '管理员调整', + '管理员调整': '管理员调整', + 'ADMIN_OPERATION': '管理员调整', + 'STREAMER_REVENUE': '主播分润', + 'PLATFORM_REVENUE': '平台抽水', + 'REGISTER': '新用户注册奖励', + '注册奖励': '新用户注册奖励', + 'REGISTER_REWARD': '新用户注册奖励', + 'NEW_USER_REWARD': '新用户注册奖励' + }; + + return typeMap[type] || type; }; const getRoleText = (role: string) => { @@ -327,14 +325,7 @@ const UserProfile = () => {
)} - {loading ? ( -
-
- 加载中... -
-

正在加载交易记录...

-
- ) : transactions.length === 0 ? ( + {transactions.length === 0 ? (
📊

暂无交易记录

diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index e873063..ba4999d 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -172,9 +172,9 @@ export interface StreamerProfile { // 主播统计数据 export interface StreamerStatistics { total_chests: number; - total_bets: number; - total_pool: number; - total_earnings: number; - avg_participants: number; + active_chests: number; + total_winnings: number; + total_commission: number; + average_chest_value: number; success_rate: number; } \ No newline at end of file