diff --git a/OshimaServers/Model/CSBettingModels.cs b/OshimaServers/Model/CSBettingModels.cs index c673817..05f03cf 100644 --- a/OshimaServers/Model/CSBettingModels.cs +++ b/OshimaServers/Model/CSBettingModels.cs @@ -125,6 +125,16 @@ namespace Oshima.FunGame.WebAPI.Model /// 支持的投注选项类型(常规包含 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; } /// @@ -208,5 +218,11 @@ namespace Oshima.FunGame.WebAPI.Model [JsonPropertyName("available_options")] public string AvailableOptions { get; set; } = "team1_win,team2_win"; + + [JsonPropertyName("team1_win_odds")] + public decimal? Team1WinOdds { get; set; } + + [JsonPropertyName("team2_win_odds")] + public decimal? Team2WinOdds { get; set; } } } \ No newline at end of file diff --git a/OshimaServers/Service/CSBettingService.cs b/OshimaServers/Service/CSBettingService.cs index 14a1ee0..5532e71 100644 --- a/OshimaServers/Service/CSBettingService.cs +++ b/OshimaServers/Service/CSBettingService.cs @@ -147,6 +147,8 @@ namespace Oshima.FunGame.WebAPI.Services string available = row["available_options"]?.ToString() ?? "[]"; string result = row["result"] != DBNull.Value ? row["result"].ToString() ?? "" : ""; long winner = row["winner"] != DBNull.Value ? Convert.ToInt64(row["winner"]) : 0; + decimal team1Odds = Convert.ToDecimal(row["team1_win_odds"]); + decimal team2Odds = Convert.ToDecimal(row["team2_win_odds"]); string eventName = ""; sql.Parameters["@eid"] = eventId; @@ -172,9 +174,9 @@ namespace Oshima.FunGame.WebAPI.Services if (status == 0) { sb.AppendLine($"可用选项:"); - if (available.Contains("team1_win")) sb.AppendLine($" - {t1}胜 (x 2.5)"); - if (available.Contains("team2_win")) sb.AppendLine($" - {t2}胜 (x 2.5)"); - if (available.Contains("score")) sb.AppendLine($" - 精确比分 (x 3.5)"); + if (available.Contains("team1_win")) sb.AppendLine($" - {t1}胜 (x {team1Odds})"); + if (available.Contains("team2_win")) sb.AppendLine($" - {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) @@ -308,6 +310,8 @@ namespace Oshima.FunGame.WebAPI.Services 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) { long betId = Convert.ToInt64(bet["id"]); @@ -325,7 +329,14 @@ namespace Oshima.FunGame.WebAPI.Services string note = "未中奖"; if (win) { - double odds = otype switch { 2 or 3 => 3.5, _ => 2.5 }; + double odds = otype switch + { + 1 => (double)team1Odds, + 2 => (double)team2Odds, + 3 => 4, + 4 => 3.5, + _ => 2.5 + }; payout = (long)(amount * odds); note = "中奖"; } @@ -559,7 +570,7 @@ namespace Oshima.FunGame.WebAPI.Services } // 创建比赛 - public static bool CreateMatch(int eventId, string team1Name, string team2Name, string stage, DateTime startTime, DateTime betDeadline, string availableOptions, 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, out string error, out long? newMatchId) { error = ""; newMatchId = null; @@ -581,6 +592,22 @@ namespace Oshima.FunGame.WebAPI.Services .Where(s => s.Length > 0)]; string optionsJson = System.Text.Json.JsonSerializer.Serialize(options); + // 如果比赛包含比分或 MVP 选项,不允许自定义猜胜者赔率 + bool hasSpecialOption = options.Contains("score") || options.Contains("mvp"); + if ((team1WinOdds.HasValue || team2WinOdds.HasValue) && hasSpecialOption) + { + error = "比赛包含比分或MVP选项时,不能自定义猜胜者赔率。"; + return false; + } + + decimal t1Odds = team1WinOdds ?? 2.50m; + decimal t2Odds = team2WinOdds ?? 2.50m; + if (t1Odds <= 0 || t2Odds <= 0) + { + error = "赔率必须大于0。"; + return false; + } + sql.Parameters["@eid"] = eventId; sql.Parameters["@t1"] = team1Name; sql.Parameters["@t2"] = team2Name; @@ -588,9 +615,11 @@ namespace Oshima.FunGame.WebAPI.Services sql.Parameters["@start"] = startTime; sql.Parameters["@deadline"] = betDeadline; sql.Parameters["@opts"] = optionsJson; + sql.Parameters["@t1_odds"] = t1Odds; + sql.Parameters["@t2_odds"] = t2Odds; sql.Execute(@"INSERT INTO csbetting_matches - (event_id, team1_name, team2_name, stage, start_time, bet_deadline, available_options, status) - VALUES (@eid, @t1, @t2, @stage, @start, @deadline, @opts, 0)"); + (event_id, team1_name, team2_name, stage, start_time, bet_deadline, available_options, team1_win_odds, team2_win_odds, status) + VALUES (@eid, @t1, @t2, @stage, @start, @deadline, @opts, @t1_odds, @t2_odds, 0)"); if (sql.Success) { @@ -603,5 +632,48 @@ namespace Oshima.FunGame.WebAPI.Services error = "数据库连接失败。"; return false; } + + /// + /// 管理员提前结束竞猜(将未开始比赛标记为进行中) + /// + public static bool CloseBetting(int matchId, out string message) + { + using SQLHelper? sql = Factory.OpenFactory.GetSQLHelper(); + if (sql == null) + { + message = "数据库连接失败。"; + return false; + } + + UpdateStatuses(sql); + + sql.Parameters["@mid"] = matchId; + sql.ExecuteDataSet("SELECT status, team1_name, team2_name FROM csbetting_matches WHERE id = @mid"); + if (!sql.Success || sql.DataSet.Tables[0].Rows.Count == 0) + { + message = "比赛不存在。"; + return false; + } + + int status = Convert.ToInt32(sql.DataSet.Tables[0].Rows[0]["status"]); + if (status != 0) + { + message = "该比赛已开始或已结束,无需关闭投注。"; + return false; + } + + sql.Execute("UPDATE csbetting_matches SET status = 1 WHERE id = @mid"); + if (sql.Success) + { + 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} 的竞猜已提前关闭。"; + return true; + } + + message = "更新比赛状态失败。"; + return false; + } } } diff --git a/OshimaWebAPI/Constant/CSBetting.sql b/OshimaWebAPI/Constant/CSBetting.sql index 9bdf28d..c9e78ac 100644 --- a/OshimaWebAPI/Constant/CSBetting.sql +++ b/OshimaWebAPI/Constant/CSBetting.sql @@ -60,3 +60,10 @@ CREATE TABLE IF NOT EXISTS `csbetting_matches` ( KEY `idx_start_time` (`start_time`), CONSTRAINT `fk_matches_event` FOREIGN KEY (`event_id`) REFERENCES `csbetting_events` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='CS比赛表'; + +-- PATCH + +-- 为比赛表添加猜胜者赔率字段 +ALTER TABLE `csbetting_matches` + ADD COLUMN `team1_win_odds` DECIMAL(5,2) NOT NULL DEFAULT 2.50 COMMENT '队伍1胜赔率' AFTER `available_options`, + ADD COLUMN `team2_win_odds` DECIMAL(5,2) NOT NULL DEFAULT 2.50 COMMENT '队伍2胜赔率' AFTER `team1_win_odds`; diff --git a/OshimaWebAPI/Controllers/CSBettingController.cs b/OshimaWebAPI/Controllers/CSBettingController.cs index 4d6c89c..cd0a1b3 100644 --- a/OshimaWebAPI/Controllers/CSBettingController.cs +++ b/OshimaWebAPI/Controllers/CSBettingController.cs @@ -94,7 +94,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; } @@ -253,7 +253,7 @@ namespace Oshima.FunGame.WebAPI.Controllers } if (CSBettingService.CreateMatch(request.EventId, request.Team1Name, request.Team2Name, request.Stage, - request.StartTime, request.BetDeadline, request.AvailableOptions, out string error, out long? newId)) + request.StartTime, request.BetDeadline, request.AvailableOptions, request.Team1WinOdds, request.Team2WinOdds, out string error, out long? newId)) { md.Content = $"比赛创建成功!新比赛ID:{newId}"; } @@ -269,5 +269,32 @@ namespace Oshima.FunGame.WebAPI.Controllers return reply; } } + + [HttpPost("close-betting")] + public BotReply CloseBetting([FromQuery] long uid, [FromQuery] int matchId) + { + MarkdownMessage md = new() { Content = busy }; + BotReply reply = new() { Markdown = md }; + try + { + if (!FunGameConstant.UserIdAndUsername.TryGetValue(uid, out User? admin) || (!admin.IsAdmin && !admin.IsOperator)) + { + md.Content = "你没有权限执行此操作。"; + return reply; + } + + if (CSBettingService.CloseBetting(matchId, out string msg)) + { + md.Content = msg; + } + + return reply; + } + catch (Exception e) + { + Logger.LogError(e, "CloseBetting 异常"); + return reply; + } + } } } diff --git a/OshimaWebAPI/Services/CSBettingInputHandler.cs b/OshimaWebAPI/Services/CSBettingInputHandler.cs index a9310d4..e49e7ed 100644 --- a/OshimaWebAPI/Services/CSBettingInputHandler.cs +++ b/OshimaWebAPI/Services/CSBettingInputHandler.cs @@ -143,6 +143,10 @@ namespace Oshima.FunGame.WebAPI.Services .AppendButtons(2, Button.CreateCmdButton("📜 我的竞猜", "我的竞猜"), Button.CreateCmdButton("📋 赛事列表", "赛事列表")); + if (reply.Markdown?.Content?.Contains("创建存档") ?? false) + { + reply.Keyboard.AppendButtons(2, Button.CreateCmdButton("⚙️ 创建存档", "创建存档")); + } await SendAsync(e, "CS赛事竞猜", reply); return true; } @@ -268,7 +272,8 @@ namespace Oshima.FunGame.WebAPI.Services // 指令:创建比赛 <赛事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,score + // 示例:创建比赛 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 if (e.Detail.StartsWith("创建比赛")) { e.UseNotice = false; @@ -286,7 +291,7 @@ namespace Oshima.FunGame.WebAPI.Services "格式:创建比赛 <赛事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 ,可额外添加 score,mvp"); + "选项默认 team1_win,team2_win ,比分和MVP选项只允许独立添加:score,mvp"); return true; } @@ -319,11 +324,28 @@ namespace Oshima.FunGame.WebAPI.Services return true; } - string options = "team1_win,team2_win"; - if (parts.Length > 8) + // 解析剩余参数:选项和赔率 + List optionParts = []; + decimal? team1Odds = null, team2Odds = null; + for (int i = 8; i < parts.Length; i++) { - options = string.Join(",", parts[8..]); // 剩余部分视为选项列表 + string segment = parts[i]; + if (segment.StartsWith("team1_win=")) + { + if (decimal.TryParse(segment[11..], out decimal odds1)) + team1Odds = odds1; + } + else if (segment.StartsWith("team2_win=")) + { + if (decimal.TryParse(segment[11..], out decimal odds2)) + team2Odds = odds2; + } + else + { + optionParts.Add(segment); + } } + string options = optionParts.Count > 0 ? string.Join(",", optionParts) : "team1_win,team2_win"; BotReply reply = BettingController.CreateMatch(new CreateMatchRequest { @@ -334,12 +356,44 @@ namespace Oshima.FunGame.WebAPI.Services Stage = stage, StartTime = startDt, BetDeadline = deadlineDt, - AvailableOptions = options + AvailableOptions = options, + Team1WinOdds = team1Odds, + Team2WinOdds = team2Odds }); await SendAsync(e, "创建比赛", reply); return true; } + // 指令:关闭投注 <比赛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, "关闭投注", "你没有权限执行此操作。"); + return true; + } + + string detail = e.Detail + .Replace("关闭投注", "") + .Replace("结束竞猜", "") + .Trim(); + if (int.TryParse(detail, out int matchId)) + { + BotReply reply = BettingController.CloseBetting(uid, matchId); + reply.Keyboard = new KeyboardMessage() + .AppendButtons(2, + Button.CreateCmdButton("📋 赛事列表", "赛事列表"), + Button.CreateCmdButton("🔍 比赛详情", $"比赛详情 {matchId}")); + await SendAsync(e, "关闭投注", reply); + } + else + { + await SendAsync(e, "关闭投注", "格式:关闭投注 <比赛ID>"); + } + return true; + } + return false; } }