fix(race): 代码质量审查修复 + commands包拆分 + 赛马取消命令

- P1: bet.py赔率计算移入锁内防竞态
- P1: config.py TESTERS解析失败添加warning日志
- P2: 新增赛马取消命令(积分退还/任务取消/状态重置)
- P3: bet.py清理未使用的_send_to_scope导入
- 将commands.py拆分为commands/包(access/bet/help/race/register)
- OpenSpec变更提案: fix-race-conditions-and-logs
This commit is contained in:
2026-05-02 14:33:34 +08:00
parent 5869618a9c
commit fe081f43cf
11 changed files with 1229 additions and 983 deletions

View File

@@ -0,0 +1,187 @@
from nonebot import on_command
from nonebot.adapters.onebot.v11 import Bot, Event
from . import (
room_store, points_service, config, logger,
get_scope, check_access, get_event_id,
_resolve_horse_selector, _format_horse_label,
calculate_odds,
)
@cancel_cmd.handle()
async def handle_cancel(bot: Bot, event: Event):
"""Handle cancel registration."""
if not await check_access(bot, event):
await cancel_cmd.finish("无权限访问此功能")
return
scope = get_scope(event)
user_id = get_event_id(event)
lock = room_store.get_lock(scope)
async with lock:
room = room_store.get_room(scope)
if not room:
await cancel_cmd.finish("房间不存在")
return
if room.state != RoomState.WAITING:
await cancel_cmd.finish("比赛正在进行中,无法取消报名")
return
user_horse = _find_user_horse(room, user_id)
if not user_horse:
await cancel_cmd.finish("你还没有报名")
return
bets_to_refund = [b for b in room.bets if b.horse_name == user_horse.name]
for bet in bets_to_refund:
await points_service.refund_bet_points(bet.user_id, bet.amount, "取消报名退还下注")
room.bets = [b for b in room.bets if b.horse_name != user_horse.name]
del room.horses[user_horse.name]
await cancel_cmd.finish(f"已取消报名,{_format_horse_label(user_horse)} 已退出")
bet_cmd = on_command("赛马下注", priority=5)
cancel_bet_cmd = on_command("赛马取消下注", priority=5)
@cancel_bet_cmd.handle()
async def handle_cancel_bet(bot: Bot, event: Event):
"""Handle cancel bet - refund all bets placed by the user in current room."""
if not await check_access(bot, event):
await cancel_bet_cmd.finish("无权限访问此功能")
return
scope = get_scope(event)
user_id = get_event_id(event)
lock = room_store.get_lock(scope)
async with lock:
room = room_store.get_room(scope)
if not room:
await cancel_bet_cmd.finish("房间不存在")
return
if room.state != RoomState.WAITING:
await cancel_bet_cmd.finish("比赛已开始,无法取消下注")
return
user_bets = [b for b in room.bets if b.user_id == user_id]
if not user_bets:
await cancel_bet_cmd.finish("你还没有下注")
return
total_refund = 0
refund_errors = []
for bet in user_bets:
try:
await points_service.refund_bet_points(bet.user_id, bet.amount, "取消下注退还")
total_refund += bet.amount
except Exception as e:
logger.error(f"退还下注失败 user={bet.user_id} amount={bet.amount}: {e}")
refund_errors.append(bet)
# 只移除已成功退还的下注
if refund_errors:
failed_amount = sum(b.amount for b in refund_errors)
room.bets = [b for b in room.bets if b.user_id != user_id or b in refund_errors]
await cancel_bet_cmd.finish(f"退还部分失败:成功退还 {total_refund} 积分,{len(refund_errors)} 笔退还失败({failed_amount} 积分),请联系管理员")
return
else:
room.bets = [b for b in room.bets if b.user_id != user_id]
await cancel_bet_cmd.finish(f"已取消 {len(user_bets)} 笔下注,退还 {total_refund} 积分")
@bet_cmd.handle()
async def handle_bet(bot: Bot, event: Event):
"""Handle bet placement."""
if not await check_access(bot, event):
await bet_cmd.finish("无权限访问此功能")
return
msg = str(event.get_message()).strip()
parts = msg.split()
if len(parts) < 3:
await bet_cmd.finish("请使用:/赛马下注 <序号|马匹名> <金额>")
return
horse_selector = parts[1]
try:
amount = int(parts[2])
except ValueError:
await bet_cmd.finish("金额必须是正整数")
return
if amount < config.MIN_BET:
await bet_cmd.finish(f"最低下注金额为 {config.MIN_BET}")
return
scope = get_scope(event)
user_id = get_event_id(event)
lock = room_store.get_lock(scope)
async with lock:
room = room_store.get_room(scope)
if not room:
await bet_cmd.finish("房间不存在,请先报名")
return
if room.state != RoomState.WAITING:
await bet_cmd.finish("比赛正在进行中,无法下注")
return
horse = _resolve_horse_selector(room, horse_selector)
if not horse:
await bet_cmd.finish(f"马匹序号/名称 \"{horse_selector}\" 不存在")
return
success, balance = await points_service.spend_bet_points(user_id, amount, f"下注 {_format_horse_label(horse)}")
if not success:
await bet_cmd.finish(f"积分不足(当前余额:{balance}")
return
room.bets.append(Bet(user_id=user_id, horse_name=horse.name, amount=amount))
odds = calculate_odds(room)
await bet_cmd.finish(f"下注成功!{_format_horse_label(horse)} {amount}积分(当前赔率:{odds.get(horse.name, config.MIN_ODDS):.2f}")
odds_cmd = on_command("赛马赔率", priority=5)
@odds_cmd.handle()
async def handle_odds(bot: Bot, event: Event):
"""Handle odds display."""
if not await check_access(bot, event):
await odds_cmd.finish("无权限访问此功能")
return
scope = get_scope(event)
room = room_store.get_room(scope)
if not room:
await odds_cmd.finish("房间不存在,请先报名")
return
if not room.horses:
await odds_cmd.finish("还没有马匹报名")
return
odds = calculate_odds(room)
lines = ["当前赔率:"]
total_bet = sum(b.amount for b in room.bets)
for horse in _get_horses_in_order(room):
odd = odds.get(horse.name, config.MIN_ODDS)
horse_bet = sum(b.amount for b in room.bets if b.horse_name == horse.name)
lines.append(f" {_format_horse_label(horse)} - {odd:.2f}倍 (总下注: {horse_bet})")
lines.append(f"总下注池: {total_bet}")
await odds_cmd.finish("\n".join(lines))
race_list_cmd = on_command("赛马列表", priority=5)