From 699b80f5a5d010c9b09d7268b089b9199c636d87 Mon Sep 17 00:00:00 2001 From: milimoe Date: Mon, 11 May 2026 19:54:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=87=E6=9C=AC=E5=92=8C=E7=AE=97=E6=B3=95?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OshimaServers/Model/CSBettingModels.cs | 183 +--------- OshimaServers/Service/CSBettingService.cs | 334 ++++++++++++------ OshimaWebAPI/Constant/CSBetting.sql | 9 + .../Controllers/CSBettingController.cs | 13 +- .../Services/CSBettingInputHandler.cs | 146 ++++---- 5 files changed, 327 insertions(+), 358 deletions(-) diff --git a/OshimaServers/Model/CSBettingModels.cs b/OshimaServers/Model/CSBettingModels.cs index 110c300..d64aaaa 100644 --- a/OshimaServers/Model/CSBettingModels.cs +++ b/OshimaServers/Model/CSBettingModels.cs @@ -2,183 +2,6 @@ namespace Oshima.FunGame.WebAPI.Model { - /// - /// 赛事状态 - /// - public enum EventStatus - { - /// - /// 未开始 - /// - Upcoming, - /// - /// 进行中 - /// - InProgress, - /// - /// 已结束 - /// - Completed - } - - /// - /// 比赛状态 - /// - public enum MatchStatus - { - /// - /// 未开始 - /// - Scheduled, - /// - /// 正在进行 - /// - Live, - /// - /// 已结束 - /// - Finished - } - - /// - /// 投注选项类型 - /// - public enum BetOptionType - { - /// - /// 队伍1胜 - /// - Team1Win, - /// - /// 队伍2胜 - /// - Team2Win, - /// - /// 精确比分 - /// - Score, - /// - /// 赛事MVP - /// - MVP - } - - /// - /// CS 队伍 - /// - public class CSTeam - { - public int Id { get; set; } - public string Name { get; set; } = ""; - public string LogoUrl { get; set; } = ""; - public List Players { get; set; } = []; - } - - /// - /// 赛事(Event) - /// - public class CSEvent - { - public int Id { get; set; } - public string Name { get; set; } = ""; - public EventStatus Status { get; set; } - public DateTime StartTime { get; set; } - public DateTime EndTime { get; set; } - public List Matches { get; set; } = []; - - /// - /// MVP 候选人列表(玩家 OpenId 或游戏内 UID) - /// - public List MVPCandidates { get; set; } = []; - } - - /// - /// 单场比赛 - /// - public class CSMatch - { - public int Id { get; set; } - public int EventId { get; set; } - public int Team1Id { get; set; } - public int Team2Id { get; set; } - public MatchStatus Status { get; set; } - public DateTime StartTime { get; set; } - public string Stage { get; set; } = ""; - public string Description { get; set; } = ""; - - /// - /// 竞猜截止时间 - /// - public DateTime BetDeadline { get; set; } - - /// - /// 比赛结果(如 “16:14” 或 “2:1”) - /// - /// - public string? Result { get; set; } - - /// - /// 获胜方(1=Team1,2=Team2,0=平局/未定) - /// - public int Winner { get; set; } - - /// - /// 支持的投注选项类型(常规包含 Team1Win/Team2Win,总决赛增加 Score) - /// - public List AvailableOptions { get; set; } = [BetOptionType.Team1Win, BetOptionType.Team2Win]; - - /// - /// 队伍1胜赔率,默认2.5 - /// - public decimal Team1WinOdds { get; set; } = 2.50m; - - /// - /// 队伍2胜赔率,默认2.5 - /// - public decimal Team2WinOdds { get; set; } = 2.50m; - } - - /// - /// 用户投注记录 - /// - public class BetRecord - { - public long Id { get; set; } - public long UserId { get; set; } - public int MatchId { get; set; } - public BetOptionType OptionType { get; set; } - - /// - /// 投注选项内容(如 “Team1Win” 或具体比分 “16:14”) - /// - public string OptionValue { get; set; } = ""; - - /// - /// 投注金币 - /// - public long Amount { get; set; } - - /// - /// 投注时间 - /// - public DateTime BetTime { get; set; } - - /// - /// 是否已结算 - /// - public bool IsSettled { get; set; } - - /// - /// 结算金额(含本金),未结算为 null - /// - public long? Payout { get; set; } - - /// - /// 结算备注 - /// - public string? ResultNote { get; set; } - } - public class CreateEventRequest { [JsonPropertyName("uid")] @@ -225,6 +48,9 @@ namespace Oshima.FunGame.WebAPI.Model [JsonPropertyName("team2_win_odds")] public decimal? Team2WinOdds { get; set; } + + [JsonPropertyName("team1_win_probability")] + public decimal? Team1WinProbability { get; set; } } public class UpdateMatchRequest @@ -249,5 +75,8 @@ namespace Oshima.FunGame.WebAPI.Model [JsonPropertyName("description")] public string? Description { get; set; } + + [JsonPropertyName("team1_win_probability")] + public decimal? Team1WinProbability { get; set; } } } \ No newline at end of file diff --git a/OshimaServers/Service/CSBettingService.cs b/OshimaServers/Service/CSBettingService.cs index 4701545..6668e65 100644 --- a/OshimaServers/Service/CSBettingService.cs +++ b/OshimaServers/Service/CSBettingService.cs @@ -233,28 +233,27 @@ namespace Oshima.FunGame.WebAPI.Services } sb.AppendLine($"{t1} vs {t2}".CreateCmdInput($"比赛详情 {matchId}")); sb.AppendLine($"开赛:{start:yyyy/MM/dd HH:mm}"); - sb.AppendLine($"竞猜截止:{deadline:yyyy/MM/dd HH:mm}"); + sb.AppendLine($"预测截止:{deadline:yyyy/MM/dd HH:mm}"); sb.AppendLine($"状态:{statusStr}"); - if (status == 0) - { - sb.AppendLine($"可用选项:"); - if (available.Contains("team1_win")) sb.AppendLine($" - 队伍1 {t1} 胜 (x {team1Odds})"); - if (available.Contains("team2_win")) sb.AppendLine($" - 队伍2 {t2} 胜 (x {team2Odds})"); - if (available.Contains("score")) sb.AppendLine($" - 精确比分 (x 4)"); - if (available.Contains("mvp")) sb.AppendLine($" - 赛事MVP (x 3.5)"); - } - else if (status == 2) - { - string winnerName = winner switch { 1 => t1, 2 => t2, 3 => result, _ => "待定" }; - sb.AppendLine($"胜者:{winnerName}"); - if (winner != 3) sb.AppendLine($"结果:{result}"); - } if (!string.IsNullOrWhiteSpace(description)) { sb.AppendLine($"> 📝 {description}\r\n"); } + if (status == 0) sb.AppendLine($"可用选项:"); + else sb.AppendLine($"该比赛已截止预测。"); + if (available.Contains("team1_win")) sb.AppendLine($" - 队伍1 {t1} 胜 (x {team1Odds})"); + if (available.Contains("team2_win")) sb.AppendLine($" - 队伍2 {t2} 胜 (x {team2Odds})"); + if (available.Contains("score")) sb.AppendLine($" - 精确比分 (x 4)"); + if (available.Contains("mvp")) sb.AppendLine($" - 赛事MVP (x 3.5)"); + if (status == 2) + { + string winnerName = winner switch { 1 => t1, 2 => t2, 3 => result, _ => "待定" }; + sb.AppendLine($"胜者:{winnerName}"); + if (winner != 3) sb.AppendLine($"结果:{result}"); + } + return sb.ToString(); } return "数据库连接失败。"; @@ -277,13 +276,15 @@ namespace Oshima.FunGame.WebAPI.Services DataRow row = sql.DataSet.Tables[0].Rows[0]; int status = Convert.ToInt32(row["status"]); DateTime deadline = Convert.ToDateTime(row["bet_deadline"]); - if (status != 0 || DateTime.Now > deadline) + decimal team1Odds = Convert.ToDecimal(row["team1_win_odds"]); + decimal team2Odds = Convert.ToDecimal(row["team2_win_odds"]); + if (status == 2 || DateTime.Now > deadline) { - error = "当前比赛已结束或非投注期。"; + error = "当前比赛已结束或非可预测阶段。"; return false; } - // --- 单场比赛投注上限检查 --- + // --- 单场比赛助力上限检查 --- long alreadyBet = 0; long totalBet = amount; sql.Parameters["@uid"] = uid; @@ -296,44 +297,54 @@ namespace Oshima.FunGame.WebAPI.Services } if (totalBet > 5000) { - error = $"本场比赛你的投注总额不能超过 5000 {General.GameplayEquilibriumConstant.InGameCurrency}(已投 {alreadyBet})。"; + error = $"本场比赛你的助力总额不能超过 5000 {General.GameplayEquilibriumConstant.InGameCurrency}(已助力 {alreadyBet})。"; return false; } string available = row["available_options"]?.ToString() ?? "[]"; int optionType; string optionValue = option; + decimal oddsAtBet = 0; if (option == "team1" && available.Contains("team1_win")) + { optionType = 1; + oddsAtBet = team1Odds; + } else if (option == "team2" && available.Contains("team2_win")) + { optionType = 2; + oddsAtBet = team2Odds; + } else if (option.StartsWith("score:") && available.Contains("score")) { optionType = 3; optionValue = option.Replace("score:", "").Trim(); + oddsAtBet = 4.0m; } else if (option.StartsWith("mvp:") && available.Contains("mvp")) { optionType = 4; optionValue = option.Replace("mvp:", "").Trim(); + oddsAtBet = 3.5m; } else { - error = "无效的投注选项。"; + error = "无效的预测选项。"; return false; } - // 写入投注记录 + // 写入预测记录 sql.Parameters["@uid"] = uid; sql.Parameters["@mid"] = matchId; sql.Parameters["@otype"] = optionType; sql.Parameters["@oval"] = optionValue; sql.Parameters["@amt"] = amount; + sql.Parameters["@odds"] = oddsAtBet; sql.Parameters["@time"] = DateTime.Now; - sql.Execute("INSERT INTO csbetting_bet_records (user_id, match_id, option_type, option_value, amount, bet_time) VALUES (@uid, @mid, @otype, @oval, @amt, @time)"); + sql.Execute("INSERT INTO csbetting_bet_records (user_id, match_id, option_type, option_value, amount, odds_at_bet, bet_time) VALUES (@uid, @mid, @otype, @oval, @amt, @odds, @time)"); if (!sql.Success) { - error = "投注记录写入失败。"; + error = "预测记录写入失败。"; return false; } return true; @@ -347,75 +358,102 @@ namespace Oshima.FunGame.WebAPI.Services using SQLHelper? sql = Factory.OpenFactory.GetSQLHelper(); if (sql != null) { - UpdateStatuses(sql); - - sql.Parameters["@mid"] = matchId; - sql.ExecuteDataSet("SELECT * FROM csbetting_matches WHERE id = @mid"); - if (!sql.Success || sql.DataSet.Tables[0].Rows.Count == 0) - return "比赛不存在。"; - DataRow row = sql.DataSet.Tables[0].Rows[0]; - string available = row["available_options"]?.ToString() ?? "[]"; - bool isMvp = available.Contains("mvp", StringComparison.CurrentCultureIgnoreCase); - int status = Convert.ToInt32(row["status"]); - if (status == 2) - return "比赛已结算。"; - - int winTeam = 3; - if (!isMvp) + try { - if (winner == "team1") winTeam = 1; - else if (winner == "team2") winTeam = 2; - else return "请指定获胜方为 team1 或 team2。MVP 赛事获胜方请直接指定选手 ID。"; - } + sql.NewTransaction(); - // 更新比赛结果 - sql.Parameters["@res"] = result; - sql.Parameters["@win"] = winTeam; - sql.Parameters["@mid"] = matchId; - sql.Execute("UPDATE csbetting_matches SET status=2, result=@res, winner=@win WHERE id=@mid"); + UpdateStatuses(sql); - // 获取所有未结算投注 - sql.Parameters["@mid"] = matchId; - sql.ExecuteDataSet("SELECT * FROM csbetting_bet_records WHERE match_id=@mid AND is_settled=0"); - if (sql.Success && sql.DataSet.Tables[0].Rows.Count > 0) - { - decimal team1Odds = Convert.ToDecimal(row["team1_win_odds"]); - decimal team2Odds = Convert.ToDecimal(row["team2_win_odds"]); - foreach (DataRow bet in sql.DataSet.Tables[0].Rows) + sql.Parameters["@mid"] = matchId; + sql.ExecuteDataSet("SELECT * FROM csbetting_matches WHERE id = @mid"); + if (!sql.Success || sql.DataSet.Tables[0].Rows.Count == 0) + return "比赛不存在。"; + DataRow row = sql.DataSet.Tables[0].Rows[0]; + string available = row["available_options"]?.ToString() ?? "[]"; + bool isMvp = available.Contains("mvp", StringComparison.CurrentCultureIgnoreCase); + int status = Convert.ToInt32(row["status"]); + if (status == 2) + return "比赛已结算。"; + + int winTeam = 3; + if (!isMvp) { - long betId = Convert.ToInt64(bet["id"]); - int otype = Convert.ToInt32(bet["option_type"]); - string ovalue = bet["option_value"].ToString() ?? ""; - long amount = Convert.ToInt64(bet["amount"]); + if (winner == "team1") winTeam = 1; + else if (winner == "team2") winTeam = 2; + else return "请指定获胜方为 team1 或 team2。MVP 赛事获胜方请直接指定选手 ID。"; + } - bool win = false; - if (otype == 1 && winTeam == 1) win = true; - else if (otype == 2 && winTeam == 2) win = true; - else if (otype == 3 && ovalue.Replace(":", ":").Equals(result, StringComparison.CurrentCultureIgnoreCase)) win = true; - else if (otype == 4 && ovalue.Equals(result, StringComparison.CurrentCultureIgnoreCase)) win = true; - - long payout = 0; - string note = "未中奖"; - if (win) + // 更新比赛结果 + sql.Parameters["@res"] = result; + sql.Parameters["@win"] = winTeam; + sql.Parameters["@mid"] = matchId; + sql.Execute("UPDATE csbetting_matches SET status=2, result=@res, winner=@win WHERE id=@mid"); + if (sql.Success) + { + // 获取所有未结算助力 + sql.Parameters["@mid"] = matchId; + sql.ExecuteDataSet("SELECT * FROM csbetting_bet_records WHERE match_id=@mid AND is_settled=0"); + if (sql.Success && sql.DataSet.Tables[0].Rows.Count > 0) { - double odds = otype switch + decimal team1Odds = Convert.ToDecimal(row["team1_win_odds"]); + decimal team2Odds = Convert.ToDecimal(row["team2_win_odds"]); + foreach (DataRow bet in sql.DataSet.Tables[0].Rows) { - 1 => (double)team1Odds, - 2 => (double)team2Odds, - 3 => 4, - 4 => 3.5, - _ => 2.5 - }; - payout = (long)(amount * odds); - note = "中奖"; + long betId = Convert.ToInt64(bet["id"]); + int otype = Convert.ToInt32(bet["option_type"]); + string ovalue = bet["option_value"].ToString() ?? ""; + long amount = Convert.ToInt64(bet["amount"]); + decimal oddsAtBet = Convert.ToDecimal(bet["odds_at_bet"]); + + bool win = false; + if (otype == 1 && winTeam == 1) win = true; + else if (otype == 2 && winTeam == 2) win = true; + else if (otype == 3 && ovalue.Replace(":", ":").Equals(result, StringComparison.CurrentCultureIgnoreCase)) win = true; + else if (otype == 4 && ovalue.Equals(result, StringComparison.CurrentCultureIgnoreCase)) win = true; + + long payout = 0; + string note = "未中奖"; + if (win) + { + if (oddsAtBet <= 0) + { + oddsAtBet = otype switch + { + 1 => team1Odds, + 2 => team2Odds, + 3 => 4, + 4 => 3.5m, + _ => 2.5m + }; + } + payout = (long)(amount * oddsAtBet); + note = "中奖"; + } + sql.Parameters["@payout"] = payout; + sql.Parameters["@note"] = note; + sql.Parameters["@bid"] = betId; + sql.Execute("UPDATE csbetting_bet_records SET is_settled=1, payout=@payout, result_note=@note WHERE id=@bid"); + if (!sql.Success) + { + sql.Rollback(); + throw sql.LastException ?? new Milimoe.FunGame.SQLQueryException(); + } + } } - sql.Parameters["@payout"] = payout; - sql.Parameters["@note"] = note; - sql.Parameters["@bid"] = betId; - sql.Execute("UPDATE csbetting_bet_records SET is_settled=1, payout=@payout, result_note=@note WHERE id=@bid"); + sql.Commit(); + return $"比赛 {matchId} 结算完成。"; + } + else + { + sql.Rollback(); + throw sql.LastException ?? new Milimoe.FunGame.SQLQueryException(); } } - return $"比赛 {matchId} 结算完成。"; + catch (Exception e) + { + sql.Rollback(); + return $"比赛 {matchId} 结算失败,发生异常:{e.Message}"; + } } return "数据库连接失败。"; } @@ -451,7 +489,7 @@ namespace Oshima.FunGame.WebAPI.Services if (page > totalPages) page = totalPages; if (page < 1) page = 1; if (total == 0) - return ("你还没有任何竞猜记录。", 1); + return ("你还没有任何预测记录。", 1); } // 构建分页SQL片段 @@ -466,7 +504,8 @@ namespace Oshima.FunGame.WebAPI.Services sql.Parameters["@uid"] = uid; sql.ExecuteDataSet($@" SELECT br.match_id, m.team1_name, m.team2_name, m.status AS match_status, m.start_time, - GROUP_CONCAT(CONCAT(br.option_type, ':', br.option_value, ':', br.amount) ORDER BY br.id SEPARATOR '|') AS details, + MAX(m.team1_win_odds) AS team1_odds, MAX(m.team2_win_odds) AS team2_odds, + GROUP_CONCAT(CONCAT(br.option_type, ':', br.option_value, ':', br.amount, ':', br.odds_at_bet) ORDER BY br.id SEPARATOR '|') AS details, SUM(br.amount) AS total_amount, MIN(br.is_settled) AS all_settled, SUM(CASE WHEN br.is_settled = 1 AND br.payout > 0 AND br.is_claimed = 1 THEN 1 ELSE 0 END) AS claimed_count, @@ -484,17 +523,19 @@ namespace Oshima.FunGame.WebAPI.Services {limitClause}"); if (!sql.Success || sql.DataSet.Tables[0].Rows.Count == 0) - return ("你还没有任何竞猜记录。", 1); + return ("你还没有任何预测记录。", 1); StringBuilder sb = new(); - if (mid > 0) sb.Append("你的本场竞猜记录:\r\n> "); - else sb.Append($"我的竞猜{(paged && totalPages > 1 ? $"(第 {page}/{totalPages} 页)" : "")}\r\n> "); + if (mid > 0) sb.Append("你的本场预测记录:\r\n> "); + else sb.Append($"我的预测{(paged && totalPages > 1 ? $"(第 {page}/{totalPages} 页)" : "")}\r\n> "); foreach (DataRow row in sql.DataSet.Tables[0].Rows) { int matchId = Convert.ToInt32(row["match_id"]); string t1 = row["team1_name"].ToString() ?? ""; string t2 = row["team2_name"].ToString() ?? ""; int matchStatus = Convert.ToInt32(row["match_status"]); + decimal t1Odds = Convert.ToDecimal(row["team1_odds"]); + decimal t2Odds = Convert.ToDecimal(row["team2_odds"]); long totalAmount = Convert.ToInt64(row["total_amount"]); long totalPayout = Convert.ToInt64(row["total_payout"]); int allSettled = Convert.ToInt32(row["all_settled"]); @@ -502,7 +543,7 @@ namespace Oshima.FunGame.WebAPI.Services int unclaimedCount = Convert.ToInt32(row["unclaimed_count"]); int lostCount = Convert.ToInt32(row["lost_count"]); - // 解析投注详情 + // 解析助力详情 string detailsStr = row["details"].ToString() ?? ""; string[]? parts = detailsStr.Split('|'); List summary = []; @@ -514,6 +555,18 @@ namespace Oshima.FunGame.WebAPI.Services int otype = int.Parse(items[0]); string ovalue = items[1]; long oamount = long.Parse(items[2]); + decimal odds = decimal.Parse(items[3]); + if (odds <= 0) + { + odds = otype switch + { + 1 => t1Odds, + 2 => t2Odds, + 3 => 4m, + 4 => 3.5m, + _ => 2.5m + }; + } string optStr = otype switch { 1 => $"{t1}胜", @@ -522,7 +575,7 @@ namespace Oshima.FunGame.WebAPI.Services 4 => $"MVP {ovalue}", _ => ovalue }; - summary.Add($"{optStr} {oamount}G"); + summary.Add($"{optStr} {oamount}G [x {odds}]"); } } @@ -546,7 +599,7 @@ namespace Oshima.FunGame.WebAPI.Services string matchLabel = $"{t1} vs {t2}".CreateCmdInput($"比赛详情 {matchId}"); sb.Append($"[比赛{matchId}] {matchLabel} | "); - sb.Append($"投注:{totalAmount}G ({detailLine}) | "); + sb.Append($"助力:{totalAmount}G ({detailLine}) | "); sb.Append($"状态:{statusLine}"); if (totalPayout > 0) sb.Append($" (+{totalPayout}G)"); @@ -574,7 +627,7 @@ namespace Oshima.FunGame.WebAPI.Services // 对于已经过了开始时间但还没有 winner 且状态为 1 的,可保留为进行中;实际上只要 winner 为 null,状态应为 1(进行中) // 如果有结果但 winner 不为 null,管理员应该已经手动结算,状态会设为 2,这里不做额外修改。 // 安全起见,只更新未开始的,以及当比赛时间已过且无 winner 时自动变成进行中。 - // 如果需要自动结束(比如时间过长),可再添加规则,但竞猜系统通常由管理员手动结算结束。 + // 如果需要自动结束(比如时间过长),可再添加规则,但预测系统通常由管理员手动结算结束。 // 这里只做基础更新。 } @@ -640,7 +693,7 @@ namespace Oshima.FunGame.WebAPI.Services } // 创建比赛 - public static bool CreateMatch(int eventId, string team1Name, string team2Name, string stage, DateTime startTime, DateTime betDeadline, string availableOptions, decimal? team1WinOdds, decimal? team2WinOdds, out string error, out long? newMatchId) + public static bool CreateMatch(int eventId, string team1Name, string team2Name, string stage, DateTime startTime, DateTime betDeadline, string availableOptions, decimal? team1WinOdds, decimal? team2WinOdds, decimal? team1WinProbability, out string error, out long? newMatchId) { error = ""; newMatchId = null; @@ -670,8 +723,34 @@ namespace Oshima.FunGame.WebAPI.Services return false; } - decimal t1Odds = team1WinOdds ?? 2.50m; - decimal t2Odds = team2WinOdds ?? 2.50m; + // 处理默认奖励率或基于胜率计算 + decimal t1Odds, t2Odds; + if (team1WinProbability.HasValue) + { + decimal prob = team1WinProbability.Value; + try + { + (t1Odds, t2Odds) = CalculateOdds(prob); + } + catch (Exception e) + { + error = e.Message; + return false; + } + } + else if (team1WinOdds.HasValue && team2WinOdds.HasValue) + { + t1Odds = team1WinOdds.Value; + t2Odds = team2WinOdds.Value; + } + else + { + // 默认双方各 2.0 + t1Odds = 2.0m; + t2Odds = 2.0m; + } + + // 校验奖励率大于0 if (t1Odds <= 0 || t2Odds <= 0) { error = "奖励率必须大于0。"; @@ -704,7 +783,7 @@ namespace Oshima.FunGame.WebAPI.Services } /// - /// 管理员提前结束竞猜(将未开始比赛标记为进行中) + /// 管理员提前结束预测(将未开始比赛标记为进行中) /// public static bool CloseBetting(int matchId, out string message) { @@ -728,7 +807,7 @@ namespace Oshima.FunGame.WebAPI.Services int status = Convert.ToInt32(sql.DataSet.Tables[0].Rows[0]["status"]); if (status != 0) { - message = "该比赛已开始或已结束,无需关闭投注。"; + message = "该比赛已开始或已结束,无需关闭预测。"; return false; } @@ -739,7 +818,7 @@ namespace Oshima.FunGame.WebAPI.Services string t1 = sql.DataSet.Tables[0].Rows[0]["team1_name"].ToString() ?? ""; string t2 = sql.DataSet.Tables[0].Rows[0]["team2_name"].ToString() ?? ""; string matchlabel = $"{t1} vs {t2}".CreateCmdInput($"比赛详情 {matchId}"); - message = $"[比赛{matchId}] {matchlabel} 的竞猜已提前关闭。"; + message = $"[比赛{matchId}] {matchlabel} 的预测已提前关闭。"; return true; } @@ -775,30 +854,53 @@ namespace Oshima.FunGame.WebAPI.Services string available = sql.DataSet.Tables[0].Rows[0]["available_options"]?.ToString() ?? "[]"; bool hasSpecial = available.Contains("score", StringComparison.OrdinalIgnoreCase) || available.Contains("mvp", StringComparison.OrdinalIgnoreCase); + decimal? newTeam1Odds = request.Team1WinOdds; + decimal? newTeam2Odds = request.Team2WinOdds; // 校验奖励率逻辑(同创建比赛) - if ((request.Team1WinOdds.HasValue || request.Team2WinOdds.HasValue) && hasSpecial) + if (request.Team1WinProbability.HasValue) { - error = "比赛包含比分或MVP选项时,不能修改猜胜者奖励率。"; - return false; + if (hasSpecial) + { + error = "比赛包含比分或MVP选项,不能修改猜胜者赔率。"; + return false; + } + decimal prob = request.Team1WinProbability.Value; + try + { + (newTeam1Odds, newTeam2Odds) = CalculateOdds(prob); + } + catch (Exception e) + { + error = e.Message; + return false; + } } - if ((request.Team1WinOdds.HasValue && request.Team1WinOdds <= 0) || - (request.Team2WinOdds.HasValue && request.Team2WinOdds <= 0)) + else { - error = "奖励率必须大于0。"; - return false; + if ((newTeam1Odds.HasValue || newTeam2Odds.HasValue) && hasSpecial) + { + error = "比赛包含比分或MVP选项时,不能修改猜胜者奖励率。"; + return false; + } + if ((newTeam1Odds.HasValue && newTeam1Odds <= 0) || + (newTeam2Odds.HasValue && newTeam2Odds <= 0)) + { + error = "奖励率必须大于0。"; + return false; + } } // 构建动态UPDATE StringBuilder setClause = new(); - if (request.Team1WinOdds.HasValue) + if (newTeam1Odds.HasValue) { - sql.Parameters["@t1od"] = request.Team1WinOdds.Value; + sql.Parameters["@t1od"] = newTeam1Odds.Value; setClause.Append("team1_win_odds = @t1od, "); } - if (request.Team2WinOdds.HasValue) + if (newTeam2Odds.HasValue) { - sql.Parameters["@t2od"] = request.Team2WinOdds.Value; + sql.Parameters["@t2od"] = newTeam2Odds.Value; setClause.Append("team2_win_odds = @t2od, "); } if (request.StartTime.HasValue) @@ -836,5 +938,21 @@ namespace Oshima.FunGame.WebAPI.Services return true; } + + public static (decimal oddsA, decimal oddsB) CalculateOdds(decimal team1WinProbability, decimal margin = 0.08m) + { + if (team1WinProbability <= 0 || team1WinProbability >= 1) + throw new ArgumentException("胜率必须大于0且小于1。"); + + if (margin < 0) margin = 0.08m; + + decimal probB = 1m - team1WinProbability; + decimal adj = 1m + margin; + + decimal oddsA = Math.Round(1m / (team1WinProbability * adj), 2); + decimal oddsB = Math.Round(1m / (probB * adj), 2); + + return (oddsA, oddsB); + } } } diff --git a/OshimaWebAPI/Constant/CSBetting.sql b/OshimaWebAPI/Constant/CSBetting.sql index 63cc425..440d487 100644 --- a/OshimaWebAPI/Constant/CSBetting.sql +++ b/OshimaWebAPI/Constant/CSBetting.sql @@ -71,3 +71,12 @@ ALTER TABLE `csbetting_matches` -- 为比赛表添加描述字段 ALTER TABLE `csbetting_matches` ADD COLUMN `description` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '比赛描述信息' AFTER `team2_win_odds`; + +-- 将队伍赔率的默认值改为 2.00 +ALTER TABLE `csbetting_matches` + MODIFY COLUMN `team1_win_odds` DECIMAL(5,2) NOT NULL DEFAULT 2.00 COMMENT '队伍1胜赔率', + MODIFY COLUMN `team2_win_odds` DECIMAL(5,2) NOT NULL DEFAULT 2.00 COMMENT '队伍2胜赔率'; + +-- 为投注记录表增加投注时的赔率字段,确保结算公平 +ALTER TABLE `csbetting_bet_records` + ADD COLUMN `odds_at_bet` DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT '投注时的赔率' AFTER `amount`; diff --git a/OshimaWebAPI/Controllers/CSBettingController.cs b/OshimaWebAPI/Controllers/CSBettingController.cs index 2e30bca..ac37947 100644 --- a/OshimaWebAPI/Controllers/CSBettingController.cs +++ b/OshimaWebAPI/Controllers/CSBettingController.cs @@ -20,7 +20,6 @@ namespace Oshima.FunGame.WebAPI.Controllers private ILogger Logger { get; set; } = logger; private const string noSaved = "你还没有创建存档!请发送【创建存档】创建。"; - private const string refused = "暂时无法使用此指令。"; private const string busy = "服务器繁忙,请稍后再试。"; // ---------- 查询类(无需锁)---------- @@ -61,20 +60,18 @@ namespace Oshima.FunGame.WebAPI.Controllers [HttpGet("match/{matchId:int}")] public BotReply GetMatchDetail(int matchId) { - return new BotReply { Markdown = new MarkdownMessage { Content = CSBettingService.GetMatchDetail(matchId, out int status) + (status == 0 ? $"竞猜指令:{"竞猜".CreateCmdInput()} <比赛ID> <选项> <{General.GameplayEquilibriumConstant.InGameCurrency}数>\r\n👇🏻 点击下方按钮快速竞猜" : "")} }; + return new BotReply { Markdown = new MarkdownMessage { Content = CSBettingService.GetMatchDetail(matchId, out int status) + (status == 0 ? $"预测指令:{"预测".CreateCmdInput()} <比赛ID> <选项> <{General.GameplayEquilibriumConstant.InGameCurrency}数>\r\n👇🏻 点击下方按钮快速预测" : "")} }; } - [AllowAnonymous] [HttpGet("mybets/{uid:long}")] public BotReply GetMyBets(long uid, [FromQuery] int page = 1, [FromQuery] int pageSize = 10) { var (content, totalPages) = CSBettingService.GetMyBets(uid, -1, page, pageSize); KeyboardMessage kb = new(); - if (totalPages > 1) kb = new KeyboardMessage().AddPaginationRow("我的竞猜 ", page, totalPages); + if (totalPages > 1) kb = new KeyboardMessage().AddPaginationRow("我的预测 ", page, totalPages); return new BotReply { Markdown = new MarkdownMessage { Content = content }, Keyboard = kb }; } - [AllowAnonymous] [HttpGet("mybets/{uid:long}/{mid:long}")] public BotReply GetMyBets(long uid, long mid) { @@ -107,7 +104,7 @@ namespace Oshima.FunGame.WebAPI.Controllers // 校验金额 if (amount < 1000) { - md.Content = $"最低投注额为 1000 {General.GameplayEquilibriumConstant.InGameCurrency}。"; + md.Content = $"最低助力 1000 {General.GameplayEquilibriumConstant.InGameCurrency}。"; FunGameService.SetUserConfigButNotRelease(uid, pc, user); return reply; } @@ -122,7 +119,7 @@ namespace Oshima.FunGame.WebAPI.Controllers { user.Inventory.Credits -= (int)amount; FunGameService.SetUserConfigButNotRelease(uid, pc, user); - md.Content = $"投注成功!{amount} {General.GameplayEquilibriumConstant.InGameCurrency}已扣除。"; + md.Content = $"{"助力".CreateCmdInput($"比赛详情 {matchId}")}成功!{amount} {General.GameplayEquilibriumConstant.InGameCurrency}已扣除。"; } else { @@ -266,7 +263,7 @@ namespace Oshima.FunGame.WebAPI.Controllers } if (CSBettingService.CreateMatch(request.EventId, request.Team1Name, request.Team2Name, request.Stage, - request.StartTime, request.BetDeadline, request.AvailableOptions, request.Team1WinOdds, request.Team2WinOdds, out string error, out long? newId)) + request.StartTime, request.BetDeadline, request.AvailableOptions, request.Team1WinOdds, request.Team2WinOdds, request.Team1WinProbability, out string error, out long? newId)) { md.Content = $"比赛创建成功!新比赛ID:{newId}"; } diff --git a/OshimaWebAPI/Services/CSBettingInputHandler.cs b/OshimaWebAPI/Services/CSBettingInputHandler.cs index 3eb859d..96ecc9a 100644 --- a/OshimaWebAPI/Services/CSBettingInputHandler.cs +++ b/OshimaWebAPI/Services/CSBettingInputHandler.cs @@ -10,43 +10,43 @@ namespace Oshima.FunGame.WebAPI.Services { public async Task HandleCSBettingInput(IBotMessage e, long uid) { - if (e.Detail == "竞猜帮助") + if (e.Detail == "预测帮助" || e.Detail == "赛事帮助") { e.UseNotice = false; BotReply reply = new() { Markdown = new MarkdownMessage { - Content = "🎮 CS赛事竞猜帮助:\r\n" + Content = "🎮 CS赛事预测帮助:\r\n" + $"✨ {"赛事列表".CreateCmdInput()} - 查看所有赛事\r\n" + $"✨ {"比赛列表".CreateCmdInput()} - 查看所有比赛\r\n" - + $"✨ {"创建存档".CreateCmdInput()} - 创建存档后可竞猜\r\n" - + $"✨ {"我的竞猜".CreateCmdInput()} - 查看我的投注记录\r\n" - + $"✨ {"竞猜领奖".CreateCmdInput()} - 领取竞猜奖励\r\n" - + $"✨ {"比赛详情".CreateCmdInput()} - 查看单场比赛并投注" + + $"✨ {"创建存档".CreateCmdInput()} - 创建存档后可预测\r\n" + + $"✨ {"我的预测".CreateCmdInput()} - 查看我的预测记录\r\n" + + $"✨ {"预测领奖".CreateCmdInput()} - 领取预测奖励\r\n" + + $"✨ {"比赛详情".CreateCmdInput()} - 查看单场比赛并预测" }, Keyboard = new KeyboardMessage() .AppendButtons(2, Button.CreateCmdButton("📋 赛事列表", "赛事列表"), Button.CreateCmdButton("📅 比赛列表", "比赛列表"), Button.CreateCmdButton("⚙️ 创建存档", "创建存档"), - Button.CreateCmdButton("📜 我的竞猜", "我的竞猜"), - Button.CreateCmdButton("💰 竞猜领奖", "竞猜领奖"), - Button.CreateCmdButton("❓ 竞猜帮助", "竞猜帮助")) + Button.CreateCmdButton("📜 我的预测", "我的预测"), + Button.CreateCmdButton("💰 预测领奖", "预测领奖"), + Button.CreateCmdButton("❓ 预测帮助", "预测帮助")) }; - await SendAsync(e, "CS赛事竞猜", reply); + await SendAsync(e, "CS赛事预测", reply); return true; } // 赛事列表 - if (e.Detail.StartsWith("赛事列表")) + if (e.Detail.StartsWith("赛事") || e.Detail.StartsWith("赛事列表")) { int page = 1; - string detail = e.Detail.Replace("赛事列表", "").Trim(); + string detail = e.Detail.Replace("赛事", "").Replace("赛事列表", "").Trim(); System.Text.RegularExpressions.Match match = GetFirstNumber().Match(detail); if (match.Success && int.TryParse(match.Value, out int p)) page = p; BotReply reply = BettingController.GetEventsOverview(page); - await SendAsync(e, "CS赛事竞猜", reply); + await SendAsync(e, "CS赛事预测", reply); return true; } @@ -63,8 +63,8 @@ namespace Oshima.FunGame.WebAPI.Services reply.Keyboard.AppendButtonsWithNewRow(2, Button.CreateCmdButton("📋 赛事列表", "赛事列表"), Button.CreateCmdButton("📅 比赛列表", "比赛列表"), - Button.CreateCmdButton("📜 我的竞猜", "我的竞猜"), - Button.CreateCmdButton("❓ 竞猜帮助", "竞猜帮助")); + Button.CreateCmdButton("📜 我的预测", "我的预测"), + Button.CreateCmdButton("❓ 预测帮助", "预测帮助")); await SendAsync(e, "比赛赛程", reply); return true; } @@ -76,31 +76,31 @@ namespace Oshima.FunGame.WebAPI.Services { BotReply reply = BettingController.GetMatchDetail(matchId); KeyboardMessage kb = new(); - // 构建投注键盘(填充“竞猜 <选项> ”) + // 构建预测键盘(填充“预测 <选项> ”) if (reply.Markdown?.Content?.Contains("可用选项") ?? false) { kb.AppendButtons(2, - Button.CreateCmdButton("⚔️ 队伍1胜", $"竞猜 {matchId} team1 1000", enter: false), - Button.CreateCmdButton("🛡️ 队伍2胜", $"竞猜 {matchId} team2 1000", enter: false), - Button.CreateCmdButton("🎯 精确比分", $"竞猜 {matchId} score:", enter: false), - Button.CreateCmdButton("🏆 MVP", $"竞猜 {matchId} mvp:", enter: false)); + Button.CreateCmdButton("⚔️ 队伍1胜", $"预测 {matchId} team1 1000", enter: false), + Button.CreateCmdButton("🛡️ 队伍2胜", $"预测 {matchId} team2 1000", enter: false), + Button.CreateCmdButton("🎯 精确比分", $"预测 {matchId} score:", enter: false), + Button.CreateCmdButton("🏆 MVP", $"预测 {matchId} mvp:", enter: false)); } kb.AppendButtonsWithNewRow(2, Button.CreateCmdButton("📋 赛事列表", "赛事列表"), Button.CreateCmdButton("📅 比赛列表", "比赛列表"), - Button.CreateCmdButton("💰 竞猜领奖", "竞猜领奖"), - Button.CreateCmdButton("❓ 竞猜帮助", "竞猜帮助")); + Button.CreateCmdButton("💰 预测领奖", "预测领奖"), + Button.CreateCmdButton("❓ 预测帮助", "预测帮助")); reply.Keyboard = kb; BotReply reply2 = BettingController.GetMyBets(uid, mid: matchId); - if (reply.Markdown != null && reply.Markdown.Content != null && !(reply2.Markdown?.Content?.Equals("你还没有任何竞猜记录。") ?? true)) + if (reply.Markdown != null && reply.Markdown.Content != null && !(reply2.Markdown?.Content?.Equals("你还没有任何预测记录。") ?? true)) { - reply.Markdown.Content += (reply.Markdown.Content.Contains("点击下方按钮快速竞猜") ? "\r\n" : "") + reply2.Markdown.Content; + reply.Markdown.Content += (reply.Markdown.Content.Contains("点击下方按钮快速预测") ? "\r\n" : "") + reply2.Markdown.Content; } - await SendAsync(e, "CS赛事竞猜", reply); + await SendAsync(e, "CS赛事预测", reply); } else { - await SendAsync(e, "CS赛事竞猜", "格式:比赛详情 <比赛ID>"); + await SendAsync(e, "CS赛事预测", "格式:比赛详情 <比赛ID>"); } return true; } @@ -121,9 +121,9 @@ namespace Oshima.FunGame.WebAPI.Services reply.Keyboard.AppendButtonsWithNewRow(2, Button.CreateCmdButton("📋 赛事列表", "赛事列表"), Button.CreateCmdButton("📅 比赛列表", "比赛列表"), - Button.CreateCmdButton("📜 我的竞猜", "我的竞猜"), - Button.CreateCmdButton("💰 竞猜领奖", "竞猜领奖")); - await SendAsync(e, "CS赛事竞猜", reply); + Button.CreateCmdButton("📜 我的预测", "我的预测"), + Button.CreateCmdButton("💰 预测领奖", "预测领奖")); + await SendAsync(e, "CS赛事预测", reply); } else { @@ -137,74 +137,74 @@ namespace Oshima.FunGame.WebAPI.Services .AppendButtons(2, Button.CreateCmdButton("📋 赛事列表", "赛事列表"), Button.CreateCmdButton("📅 比赛列表", "比赛列表"), - Button.CreateCmdButton("❓ 竞猜帮助", "竞猜帮助"), - Button.CreateCmdButton("📜 我的竞猜", "我的竞猜")) + Button.CreateCmdButton("❓ 预测帮助", "预测帮助"), + Button.CreateCmdButton("📜 我的预测", "我的预测")) }; - await SendAsync(e, "CS赛事竞猜", reply); + await SendAsync(e, "CS赛事预测", reply); } return true; } - if (e.Detail.StartsWith("我的竞猜")) + if (e.Detail.StartsWith("我的预测")) { int page = 1; - string detail = e.Detail.Replace("我的竞猜", "").Trim(); + string detail = e.Detail.Replace("我的预测", "").Trim(); System.Text.RegularExpressions.Match match = GetFirstNumber().Match(detail); if (match.Success && int.TryParse(match.Value, out int p)) page = p; BotReply reply = BettingController.GetMyBets(uid, page); reply.Keyboard ??= new KeyboardMessage(); reply.Keyboard.AppendButtonsWithNewRow(2, - Button.CreateCmdButton("💰 竞猜领奖", "竞猜领奖", enter: true), + Button.CreateCmdButton("💰 预测领奖", "预测领奖", enter: true), Button.CreateCmdButton("📋 赛事列表", "赛事列表"), Button.CreateCmdButton("📅 比赛列表", "比赛列表"), - Button.CreateCmdButton("❓ 竞猜帮助", "竞猜帮助")); + Button.CreateCmdButton("❓ 预测帮助", "预测帮助")); if (reply.Markdown?.Content?.Contains("创建存档") ?? false) { reply.Keyboard.AppendButtons(2, Button.CreateCmdButton("⚙️ 创建存档", "创建存档")); } - await SendAsync(e, "CS赛事竞猜", reply); + await SendAsync(e, "CS赛事预测", reply); return true; } - if (e.Detail == "竞猜领奖") + if (e.Detail == "预测领奖") { BotReply reply = BettingController.ClaimRewards(uid); reply.Keyboard = new KeyboardMessage() .AppendButtons(2, - Button.CreateCmdButton("📜 我的竞猜", "我的竞猜"), + Button.CreateCmdButton("📜 我的预测", "我的预测"), Button.CreateCmdButton("📋 赛事列表", "赛事列表"), Button.CreateCmdButton("📅 比赛列表", "比赛列表")); if (reply.Markdown?.Content?.Contains("创建存档") ?? false) { reply.Keyboard.AppendButtons(2, Button.CreateCmdButton("⚙️ 创建存档", "创建存档")); } - await SendAsync(e, "CS赛事竞猜", reply); + await SendAsync(e, "CS赛事预测", reply); return true; } - if (e.Detail.StartsWith("竞猜")) + if (e.Detail.StartsWith("预测")) { - string detail = e.Detail.Replace("竞猜", "").Trim(); + string detail = e.Detail.Replace("预测", "").Trim(); string[] parts = detail.Split(' ', StringSplitOptions.RemoveEmptyEntries); if (parts.Length < 3 || !int.TryParse(parts[0], out int mid) || !long.TryParse(parts[^1], out long amt)) { - await SendAsync(e, "CS赛事竞猜", $"格式:竞猜 <比赛ID> <选项> <{General.GameplayEquilibriumConstant.InGameCurrency}数>\r\n选项:team1 / team2 / score:2:0 / mvp:选手UID"); + await SendAsync(e, "CS赛事预测", $"格式:预测 <比赛ID> <选项> <{General.GameplayEquilibriumConstant.InGameCurrency}数>\r\n选项:team1 / team2 / score:2:0 / mvp:选手UID"); return true; } string option = string.Join(" ", parts[1..^1]).ToLower(); BotReply reply = BettingController.PlaceBet(uid, mid, option, amt); - // 根据控制器返回的消息判断投注结果(简单判断是否包含"成功") + // 根据控制器返回的消息判断预测结果(简单判断是否包含"成功") bool success = reply.Markdown?.Content?.Contains("成功") ?? false; KeyboardMessage kb = new(); // 成功与失败通用的按钮 kb.AppendButtons(2, - Button.CreateCmdButton("📜 我的竞猜", "我的竞猜"), + Button.CreateCmdButton("📜 我的预测", "我的预测"), Button.CreateCmdButton("📋 赛事列表", "赛事列表"), Button.CreateCmdButton("📅 比赛列表", "比赛列表"), - Button.CreateCmdButton("❓ 竞猜帮助", "竞猜帮助")); + Button.CreateCmdButton("❓ 预测帮助", "预测帮助")); if (reply.Markdown?.Content?.Contains("创建存档") ?? false) { @@ -215,12 +215,12 @@ namespace Oshima.FunGame.WebAPI.Services if (success) { kb.AppendButtonsWithNewRow(2, - Button.CreateCmdButton("🔄 再次投注", e.Detail, enter: false), + Button.CreateCmdButton("🔄 再次预测", e.Detail, enter: false), Button.CreateCmdButton("🔍 比赛详情", $"比赛详情 {mid}")); } reply.Keyboard = kb; - await SendAsync(e, "CS赛事竞猜", reply); + await SendAsync(e, "CS赛事预测", reply); return true; } @@ -243,10 +243,10 @@ namespace Oshima.FunGame.WebAPI.Services Button.CreateCmdButton("📋 赛事列表", "赛事列表"), Button.CreateCmdButton("📅 比赛列表", "比赛列表"), Button.CreateCmdButton("⚙️ 继续结算", "结算比赛 ", enter: false)); - await SendAsync(e, "CS赛事竞猜", reply); + await SendAsync(e, "CS赛事预测", reply); } else - await SendAsync(e, "CS赛事竞猜", "格式:结算比赛 <比赛ID> winner=team1 result=2:0"); + await SendAsync(e, "CS赛事预测", "格式:结算比赛 <比赛ID> winner=team1 result=2:0"); return true; } @@ -303,10 +303,11 @@ namespace Oshima.FunGame.WebAPI.Services return true; } - // 指令:创建比赛 <赛事ID> <队伍1> <队伍2> <阶段> <开始时间> <投注截止时间> [选项列表(逗号分隔)] - // 示例:创建比赛 1 NAVI FaZe Quarter-final 2026-03-05 14:00 2026-03-05 13:55 - // 示例:创建比赛 1 G2 Vitality Semi-final 2026-04-02 18:00 2026-04-02 17:55 team1_win,team2_win - // 示例:创建比赛 1 G2 Vitality Semi-final 2026-04-02 18:00 2026-04-02 17:55 team1_win=2.5 team2_win=2.5 team1_win,team2_win + // 指令:创建比赛 <赛事ID> <队伍1> <队伍2> <阶段> <开始时间> <预测截止时间> [选项列表(逗号分隔)] + // 示例(创建默认形式的比赛):创建比赛 1 NAVI FaZe Quarter-final 2026-03-05 14:00 2026-03-05 13:55 + // 示例(提供比赛助力选项):创建比赛 1 G2 Vitality Semi-final 2026-04-02 18:00 2026-04-02 17:55 team1_win,team2_win + // 示例(提供自定义奖励率):创建比赛 1 G2 Vitality Semi-final 2026-04-02 18:00 2026-04-02 17:55 team1_win=2.5 team2_win=2.5 team1_win,team2_win + // 示例(提供队伍1的胜率自动计算奖励率):创建比赛 1 Vitality FaZe Final 2026-05-12 20:00 2026-05-12 19:55 prob=0.6 if (e.Detail.StartsWith("创建比赛")) { e.UseNotice = false; @@ -321,7 +322,7 @@ namespace Oshima.FunGame.WebAPI.Services if (parts.Length < 7) // 最少需要 eventId, team1, team2, stage, start date, start time, deadline date, deadline time { await SendAsync(e, "创建比赛", - "格式:创建比赛 <赛事ID> <队伍1> <队伍2> <阶段> <开始时间> <投注截止时间> [选项列表(逗号分隔)]\r\n" + + "格式:创建比赛 <赛事ID> <队伍1> <队伍2> <阶段> <开始时间> <预测截止时间> [选项列表(逗号分隔)]\r\n" + "时间格式:yyyy-MM-dd HH:mm(开始/截止各占两段)\r\n" + "示例:创建比赛 1 NAVI FaZe Quarter-final 2026-03-05 14:00 2026-03-05 13:55\r\n" + "选项默认 team1_win,team2_win ,比分和MVP选项只允许独立添加:score,mvp"); @@ -344,7 +345,7 @@ namespace Oshima.FunGame.WebAPI.Services // 截止时间(parts[6] + parts[7] 如果存在) if (parts.Length < 8) { - await SendAsync(e, "创建比赛", "投注截止时间需要完整日期和时间,示例:2026-03-05 13:55"); + await SendAsync(e, "创建比赛", "预测截止时间需要完整日期和时间,示例:2026-03-05 13:55"); return true; } string deadlineDate = parts[6]; @@ -359,7 +360,7 @@ namespace Oshima.FunGame.WebAPI.Services // 解析剩余参数:选项和奖励率 List optionParts = []; - decimal? team1Odds = null, team2Odds = null; + decimal? team1Odds = null, team2Odds = null, team1WinProbability = null; for (int i = 8; i < parts.Length; i++) { string segment = parts[i]; @@ -373,6 +374,16 @@ namespace Oshima.FunGame.WebAPI.Services if (decimal.TryParse(segment[11..], out decimal odds2)) team2Odds = odds2; } + else if (segment.StartsWith("prob=")) + { + if (decimal.TryParse(segment[5..], out decimal prob)) + team1WinProbability = prob; + else + { + await SendAsync(e, "创建比赛", "胜率值无效,应为0~1之间的小数。"); + return true; + } + } else { optionParts.Add(segment); @@ -391,25 +402,26 @@ namespace Oshima.FunGame.WebAPI.Services BetDeadline = deadlineDt, AvailableOptions = options, Team1WinOdds = team1Odds, - Team2WinOdds = team2Odds + Team2WinOdds = team2Odds, + Team1WinProbability = team1WinProbability }); await SendAsync(e, "创建比赛", reply); return true; } - // 指令:关闭投注 <比赛ID> - if (e.Detail.StartsWith("关闭投注") || e.Detail.StartsWith("结束竞猜")) + // 指令:关闭预测 <比赛ID> + if (e.Detail.StartsWith("关闭预测") || e.Detail.StartsWith("结束预测")) { e.UseNotice = false; if (!FunGameConstant.UserIdAndUsername.TryGetValue(uid, out User? user) || (!user.IsAdmin && !user.IsOperator)) { - await SendAsync(e, "关闭投注", "你没有权限执行此操作。"); + await SendAsync(e, "关闭预测", "你没有权限执行此操作。"); return true; } string detail = e.Detail - .Replace("关闭投注", "") - .Replace("结束竞猜", "") + .Replace("关闭预测", "") + .Replace("结束预测", "") .Trim(); if (int.TryParse(detail, out int matchId)) { @@ -418,11 +430,11 @@ namespace Oshima.FunGame.WebAPI.Services .AppendButtons(2, Button.CreateCmdButton("📋 赛事列表", "赛事列表"), Button.CreateCmdButton("🔍 比赛详情", $"比赛详情 {matchId}")); - await SendAsync(e, "关闭投注", reply); + await SendAsync(e, "关闭预测", reply); } else { - await SendAsync(e, "关闭投注", "格式:关闭投注 <比赛ID>"); + await SendAsync(e, "关闭预测", "格式:关闭预测 <比赛ID>"); } return true; } @@ -475,6 +487,10 @@ namespace Oshima.FunGame.WebAPI.Services if (decimal.TryParse(val, out decimal t2od)) request.Team2WinOdds = t2od; else { await SendAsync(e, "修改比赛", "t2od 值无效。"); return true; } break; + case "prob": + if (decimal.TryParse(val, out decimal prob)) request.Team1WinProbability = prob; + else { await SendAsync(e, "修改比赛", "prob 值无效。"); return true; } + break; case "st": if (DateTime.TryParse(val, out DateTime st)) request.StartTime = st; else { await SendAsync(e, "修改比赛", "开始时间格式错误,请用 yyyy-MM-dd HH:mm。"); return true; }