diff --git a/OshimaServers/Model/CSBettingModels.cs b/OshimaServers/Model/CSBettingModels.cs new file mode 100644 index 0000000..c673817 --- /dev/null +++ b/OshimaServers/Model/CSBettingModels.cs @@ -0,0 +1,212 @@ +using System.Text.Json.Serialization; + +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 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]; + } + + /// + /// 用户投注记录 + /// + 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")] + public long Uid { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } = ""; + + [JsonPropertyName("start_time")] + public DateTime StartTime { get; set; } + + [JsonPropertyName("end_time")] + public DateTime EndTime { get; set; } + } + + public class CreateMatchRequest + { + [JsonPropertyName("uid")] + public long Uid { get; set; } + + [JsonPropertyName("event_id")] + public int EventId { get; set; } + + [JsonPropertyName("team1_name")] + public string Team1Name { get; set; } = ""; + + [JsonPropertyName("team2_name")] + public string Team2Name { get; set; } = ""; + + [JsonPropertyName("stage")] + public string Stage { get; set; } = ""; + + [JsonPropertyName("start_time")] + public DateTime StartTime { get; set; } + + [JsonPropertyName("bet_deadline")] + public DateTime BetDeadline { get; set; } + + [JsonPropertyName("available_options")] + public string AvailableOptions { get; set; } = "team1_win,team2_win"; + } +} \ No newline at end of file diff --git a/OshimaServers/Model/QQBot.cs b/OshimaServers/Model/QQBot.cs index cfcc8b1..3df7117 100644 --- a/OshimaServers/Model/QQBot.cs +++ b/OshimaServers/Model/QQBot.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Oshima.FunGame.OshimaServers.Models +namespace Oshima.FunGame.OshimaServers.Model { public class Payload { diff --git a/OshimaServers/Service/CSBettingSQLService.cs b/OshimaServers/Service/CSBettingSQLService.cs new file mode 100644 index 0000000..579995a --- /dev/null +++ b/OshimaServers/Service/CSBettingSQLService.cs @@ -0,0 +1,399 @@ +using System.Data; +using System.Security.Cryptography; +using System.Text; +using Milimoe.FunGame.Core.Api.Transmittal; +using Milimoe.FunGame.Core.Api.Utility; +using Oshima.FunGame.OshimaServers.Model; + +namespace Oshima.FunGame.WebAPI.Services +{ + public class CSBettingSQLService + { + public static string GetEventsOverview() + { + using SQLHelper? sql = Factory.OpenFactory.GetSQLHelper(); + if (sql != null) + { + sql.ExecuteDataSet("SELECT id, name, status, start_time FROM csbetting_events ORDER BY start_time DESC"); + if (!sql.Success || sql.DataSet.Tables.Count == 0) return "暂无赛事。"; + StringBuilder sb = new(); + foreach (DataRow row in sql.DataSet.Tables[0].Rows) + { + int id = Convert.ToInt32(row["id"]); + string name = row["name"].ToString() ?? ""; + int status = Convert.ToInt32(row["status"]); + string statusStr = status switch { 0 => "未开始", 1 => "进行中", 2 => "已结束", _ => "未知" }; + sb.AppendLine($"🏆 [{id}] {name.CreateCmdInput($"赛事详情 {id}")} ({statusStr})"); + } + return sb.ToString().TrimEnd(); + } + return "数据库连接失败。"; + } + + public static string GetEventDetail(int eventId) + { + using SQLHelper? sql = Factory.OpenFactory.GetSQLHelper(); + if (sql != null) + { + sql.Parameters["@id"] = eventId; + sql.ExecuteDataSet("SELECT * FROM csbetting_events WHERE id = @id"); + if (!sql.Success || sql == null || sql.DataSet.Tables[0].Rows.Count == 0) return "赛事不存在。"; + DataRow evt = sql.DataSet.Tables[0].Rows[0]; + string name = evt["name"].ToString() ?? ""; + int status = Convert.ToInt32(evt["status"]); + DateTime start = Convert.ToDateTime(evt["start_time"]); + DateTime end = Convert.ToDateTime(evt["end_time"]); + string statusStr = status switch { 0 => "未开始", 1 => "进行中", 2 => "已结束", _ => "未知" }; + + StringBuilder sb = new(); + sb.AppendLine($"赛事:{name}"); + sb.AppendLine($"状态:{statusStr}"); + sb.AppendLine($"时间:{start:yyyy/MM/dd} ~ {end:yyyy/MM/dd}"); + sb.AppendLine("比赛列表:"); + + sql.Parameters["@eid"] = eventId; + sql.ExecuteDataSet("SELECT id, team1_name, team2_name, status, bet_deadline, stage FROM csbetting_matches WHERE event_id = @eid ORDER BY start_time"); + if (sql.Success && sql.DataSet?.Tables[0].Rows.Count > 0) + { + foreach (DataRow row in sql.DataSet.Tables[0].Rows) + { + int mid = Convert.ToInt32(row["id"]); + string t1 = row["team1_name"].ToString() ?? ""; + string t2 = row["team2_name"].ToString() ?? ""; + int mstatus = Convert.ToInt32(row["status"]); + DateTime deadline = Convert.ToDateTime(row["bet_deadline"]); + string stage = row["stage"].ToString() ?? ""; + string mStatusStr = mstatus switch { 0 => "未开始", 1 => "进行中", 2 => "已结束", _ => "未知" }; + string matchLabel = $"{t1} vs {t2}"; + string clickableMatch = matchLabel.CreateCmdInput($"比赛详情 {mid}"); + sb.AppendLine($" [{mid}] {(stage != "" ? $"{stage} - " : "")} {clickableMatch} (状态:{mStatusStr}, 截止:{deadline:MM-dd HH:mm})"); + } + } + return sb.ToString(); + } + return "数据库连接失败。"; + } + + public static string GetMatchDetail(int matchId) + { + using SQLHelper? sql = Factory.OpenFactory.GetSQLHelper(); + if (sql != null) + { + 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]; + long eventId = Convert.ToInt64(row["event_id"]); + string t1 = row["team1_name"].ToString() ?? ""; + string t2 = row["team2_name"].ToString() ?? ""; + int status = Convert.ToInt32(row["status"]); + DateTime start = Convert.ToDateTime(row["start_time"]); + DateTime deadline = Convert.ToDateTime(row["bet_deadline"]); + string stage = row["stage"].ToString() ?? ""; + string available = row["available_options"]?.ToString() ?? "[]"; + + string eventName = ""; + sql.Parameters["@eid"] = eventId; + DataRow? rowEvent = sql.ExecuteDataRow("SELECT name FROM csbetting_events WHERE id = @eid"); + if (rowEvent != null) + { + eventName = rowEvent["name"].ToString() ?? ""; + } + + string statusStr = status switch { 0 => "未开始", 1 => "进行中", 2 => "已结束", _ => "未知" }; + StringBuilder sb = new(); + sb.AppendLine($"比赛 #{matchId}"); + if (eventName.Trim() != "") + { + sb.AppendLine($"> {eventName.CreateCmdInput($"赛事详情 {eventId}")}"); + if (stage.Trim() != "") sb.AppendLine($"{stage}"); + sb.AppendLine(); + } + 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($"状态:{statusStr}"); + 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("mvp")) sb.AppendLine($" - 赛事MVP (x 3.5)"); + return sb.ToString(); + } + return "数据库连接失败。"; + } + + public static bool PlaceBet(long uid, int matchId, string option, long amount, out string error) + { + error = ""; + using SQLHelper? sql = Factory.OpenFactory.GetSQLHelper(); + if (sql != null) + { + // 检查比赛 + sql.Parameters["@mid"] = matchId; + sql.ExecuteDataSet("SELECT * FROM csbetting_matches WHERE id = @mid"); + if (!sql.Success || sql.DataSet.Tables[0].Rows.Count == 0) + { + error = "比赛不存在。"; + return false; + } + 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) + { + error = "当前比赛已截止或非投注期。"; + return false; + } + + string available = row["available_options"]?.ToString() ?? "[]"; + int optionType; + string optionValue = option; + if (option == "team1" && available.Contains("team1_win")) + optionType = 1; + else if (option == "team2" && available.Contains("team2_win")) + optionType = 2; + else if (option.StartsWith("score:") && available.Contains("score")) + { + optionType = 3; + optionValue = option.Replace("score:", "").Trim(); + } + else if (option.StartsWith("mvp:") && available.Contains("mvp")) + { + optionType = 4; + optionValue = option.Replace("mvp:", "").Trim(); + } + else + { + 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["@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)"); + if (!sql.Success) + { + error = "投注记录写入失败。"; + return false; + } + return true; + } + error = "数据库连接失败。"; + return false; + } + + public static string SettleMatch(int matchId, string winner, string result) + { + using SQLHelper? sql = Factory.OpenFactory.GetSQLHelper(); + if (sql != null) + { + 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]; + int status = Convert.ToInt32(row["status"]); + if (status == 2) + return "比赛已结算。"; + + int winTeam; + if (winner == "team1") winTeam = 1; + else if (winner == "team2") winTeam = 2; + else return "请指定获胜方为 team1 或 team2。"; + + // 更新比赛结果 + 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"); + + // 获取所有未结算投注 + 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) + { + foreach (DataRow bet in sql.DataSet.Tables[0].Rows) + { + 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"]); + + bool win = false; + if (otype == 1 && winTeam == 1) win = true; + else if (otype == 2 && winTeam == 2) win = true; + else if (otype == 3 && ovalue == result) win = true; + + long payout = 0; + string note = "未中奖"; + if (win) + { + double odds = otype switch { 2 or 3 => 3.5, _ => 2.5 }; + payout = (long)(amount * odds); + 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"); + } + } + return $"比赛 {matchId} 结算完成。"; + } + return "数据库连接失败。"; + } + + public static string GetMyBets(long uid) + { + using SQLHelper? sql = Factory.OpenFactory.GetSQLHelper(); + if (sql != null) + { + sql.Parameters["@uid"] = uid; + sql.ExecuteDataSet(@" + SELECT br.id, br.match_id, br.option_type, br.option_value, br.amount, br.bet_time, + br.is_settled, br.payout, br.is_claimed, br.result_note, + m.team1_name, m.team2_name + FROM csbetting_bet_records br + JOIN csbetting_matches m ON br.match_id = m.id + WHERE br.user_id = @uid + ORDER BY br.bet_time DESC"); + if (!sql.Success || sql.DataSet.Tables[0].Rows.Count == 0) return "你还没有任何竞猜记录。"; + StringBuilder sb = new(); + foreach (DataRow row in sql.DataSet.Tables[0].Rows) + { + long bid = Convert.ToInt64(row["id"]); + long mid = Convert.ToInt64(row["match_id"]); + string t1 = row["team1_name"].ToString() ?? ""; + string t2 = row["team2_name"].ToString() ?? ""; + int otype = Convert.ToInt32(row["option_type"]); + string ovalue = row["option_value"].ToString() ?? ""; + long amt = Convert.ToInt64(row["amount"]); + bool settled = Convert.ToBoolean(row["is_settled"]); + long? payout = row["payout"] as long?; + bool claimed = Convert.ToBoolean(row["is_claimed"]); + + string optStr = otype switch { 1 => $"{t1}胜", 2 => $"{t2}胜", 3 => $"比分 {ovalue}", 4 => $"MVP {ovalue}", _ => ovalue }; + string statusStr = settled ? (payout > 0 ? (claimed ? $"+{payout} (已领)" : $"+{payout} (可领)") : "未中奖") : "进行中"; + string matchLabel = $"{t1} vs {t2}"; + string clickableMatch = matchLabel.CreateCmdInput($"比赛详情 {mid}"); + sb.AppendLine($"[{bid}] {clickableMatch} | 选项:{optStr} | 投注:{amt} | 状态:{statusStr}"); + } + return sb.ToString(); + } + return "数据库连接失败。"; + } + + public static long ClaimRewards(long uid) + { + // 返回领取的总金币,由上层加到用户身上 + using SQLHelper? sql = Factory.OpenFactory.GetSQLHelper(); + if (sql != null) + { + sql.Parameters["@uid"] = uid; + sql.ExecuteDataSet("SELECT id, payout FROM csbetting_bet_records WHERE user_id=@uid AND is_settled=1 AND payout>0 AND is_claimed=0"); + if (!sql.Success || sql.DataSet.Tables[0].Rows.Count == 0) return 0; + + long total = 0; + foreach (DataRow row in sql.DataSet.Tables[0].Rows) + { + long bid = Convert.ToInt64(row["id"]); + long payout = Convert.ToInt64(row["payout"]); + total += payout; + sql.Parameters["@bid"] = bid; + sql.Execute("UPDATE csbetting_bet_records SET is_claimed=1 WHERE id=@bid"); + } + return total; + } + return 0; + } + + // 创建赛事 + public static bool CreateEvent(string name, DateTime startTime, DateTime endTime, out string error, out long? newEventId) + { + error = ""; + newEventId = null; + using SQLHelper? sql = Factory.OpenFactory.GetSQLHelper(); + if (sql != null) + { + // 简单校验 + if (string.IsNullOrWhiteSpace(name)) + { + error = "赛事名称不能为空。"; + return false; + } + if (endTime <= startTime) + { + error = "结束时间必须晚于开始时间。"; + return false; + } + + sql.Parameters["@name"] = name; + sql.Parameters["@start"] = startTime; + sql.Parameters["@end"] = endTime; + sql.Execute("INSERT INTO csbetting_events (name, status, start_time, end_time) VALUES (@name, 0, @start, @end)"); + + if (sql.Success) + { + newEventId = sql.LastInsertId; + return true; + } + error = "赛事创建失败,数据库错误。"; + return false; + } + error = "数据库连接失败。"; + return false; + } + + // 创建比赛 + public static bool CreateMatch(int eventId, string team1Name, string team2Name, string stage, DateTime startTime, DateTime betDeadline, string availableOptions, out string error, out long? newMatchId) + { + error = ""; + newMatchId = null; + using var sql = Factory.OpenFactory.GetSQLHelper(); + if (sql != null) + { + // 检查赛事存在 + sql.Parameters["@eid"] = eventId; + sql.ExecuteDataSet("SELECT id FROM csbetting_events WHERE id = @eid"); + if (!sql.Success || sql.DataSet?.Tables[0].Rows.Count == 0) + { + error = "赛事不存在。"; + return false; + } + + // 处理可用选项 JSON + List options = [.. availableOptions.Split(',', StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()) + .Where(s => s.Length > 0)]; + string optionsJson = System.Text.Json.JsonSerializer.Serialize(options); + + sql.Parameters["@eid"] = eventId; + sql.Parameters["@t1"] = team1Name; + sql.Parameters["@t2"] = team2Name; + sql.Parameters["@stage"] = stage; + sql.Parameters["@start"] = startTime; + sql.Parameters["@deadline"] = betDeadline; + sql.Parameters["@opts"] = optionsJson; + 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)"); + + if (sql.Success) + { + newMatchId = sql.LastInsertId; + return true; + } + error = "比赛创建失败,数据库错误。"; + return false; + } + error = "数据库连接失败。"; + return false; + } + } +} diff --git a/OshimaServers/Service/FunGameOrderList.cs b/OshimaServers/Service/FunGameOrderList.cs index 1a5ac4a..c6ce412 100644 --- a/OshimaServers/Service/FunGameOrderList.cs +++ b/OshimaServers/Service/FunGameOrderList.cs @@ -1,4 +1,4 @@ -using Oshima.FunGame.OshimaServers.Models; +using Oshima.FunGame.OshimaServers.Model; namespace Oshima.FunGame.OshimaServers.Service { diff --git a/OshimaServers/Service/FunGameService.cs b/OshimaServers/Service/FunGameService.cs index fb1a8c3..b08fc44 100644 --- a/OshimaServers/Service/FunGameService.cs +++ b/OshimaServers/Service/FunGameService.cs @@ -13,7 +13,7 @@ using Oshima.FunGame.OshimaModules.Regions; using Oshima.FunGame.OshimaModules.Skills; using Oshima.FunGame.OshimaModules.Units; using Oshima.FunGame.OshimaServers.Model; -using Oshima.FunGame.OshimaServers.Models; +using Oshima.FunGame.OshimaServers.Model; using ProjectRedbud.FunGame.SQLQueryExtension; namespace Oshima.FunGame.OshimaServers.Service diff --git a/OshimaWebAPI/Controllers/CSBettingController.cs b/OshimaWebAPI/Controllers/CSBettingController.cs new file mode 100644 index 0000000..65a5cd3 --- /dev/null +++ b/OshimaWebAPI/Controllers/CSBettingController.cs @@ -0,0 +1,256 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Milimoe.FunGame.Core.Api.Utility; +using Milimoe.FunGame.Core.Entity; +using Milimoe.FunGame.Core.Library.Constant; +using Oshima.FunGame.OshimaModules.Models; +using Oshima.FunGame.OshimaServers.Model; +using Oshima.FunGame.OshimaServers.Service; +using Oshima.FunGame.WebAPI.Model; +using Oshima.FunGame.WebAPI.Services; + +namespace Oshima.FunGame.WebAPI.Controllers +{ + [Authorize(AuthenticationSchemes = "CustomBearer")] + [ApiController] + [Route("[controller]")] + public class CSBettingController(ILogger logger) : ControllerBase + { + private ILogger Logger { get; set; } = logger; + + private const string noSaved = "你还没有创建存档!请发送【创建存档】创建。"; + private const string refused = "暂时无法使用此指令。"; + private const string busy = "服务器繁忙,请稍后再试。"; + + // ---------- 查询类(无需锁)---------- + [AllowAnonymous] + [HttpGet("events")] + public BotReply GetEventsOverview() + { + return new BotReply { Markdown = new MarkdownMessage { Content = CSBettingSQLService.GetEventsOverview() } }; + } + + [AllowAnonymous] + [HttpGet("event/{eventId:int}")] + public BotReply GetEventDetail(int eventId) + { + return new BotReply { Markdown = new MarkdownMessage { Content = CSBettingSQLService.GetEventDetail(eventId) } }; + } + + [AllowAnonymous] + [HttpGet("match/{matchId:int}")] + public BotReply GetMatchDetail(int matchId) + { + return new BotReply { Markdown = new MarkdownMessage { Content = CSBettingSQLService.GetMatchDetail(matchId) } }; + } + + [AllowAnonymous] + [HttpGet("mybets/{uid:long}")] + public BotReply GetMyBets(long uid) + { + return new BotReply { Markdown = new MarkdownMessage { Content = CSBettingSQLService.GetMyBets(uid) } }; + } + + // ---------- 需要用户锁的操作 ---------- + + /// + /// 投注指令(内部 + API) + /// + [HttpPost("bet")] + public BotReply PlaceBet([FromQuery] long uid, [FromQuery] int matchId, [FromQuery] string option, [FromQuery] long amount = 1000) + { + MarkdownMessage md = new() { Content = busy }; + BotReply reply = new() { Markdown = md }; + try + { + PluginConfig pc = FunGameService.GetUserConfig(uid, out bool isTimeout); + if (isTimeout) return reply; + if (pc.Count == 0) + { + md.Content = noSaved; + return reply; + } + + User user = FunGameService.GetUser(pc); + + // 校验金额 + if (amount < 1000) + { + md.Content = "最低投注额为 1000 {General.GameplayEquilibriumConstant.InGameCurrency}。"; + FunGameService.SetUserConfigButNotRelease(uid, pc, user); + return reply; + } + if (user.Inventory.Credits < amount) + { + md.Content = $"{General.GameplayEquilibriumConstant.InGameCurrency}不足,你的{General.GameplayEquilibriumConstant.InGameCurrency}:{user.Inventory.Credits}。"; + FunGameService.SetUserConfigButNotRelease(uid, pc, user); + return reply; + } + + if (CSBettingSQLService.PlaceBet(uid, matchId, option, amount, out string error)) + { + user.Inventory.Credits -= (int)amount; + FunGameService.SetUserConfigButNotRelease(uid, pc, user); + md.Content = $"投注成功!{amount} {General.GameplayEquilibriumConstant.InGameCurrency}已扣除。"; + } + else + { + FunGameService.SetUserConfigButNotRelease(uid, pc, user); + md.Content = error; + } + return reply; + } + catch (Exception e) + { + Logger.LogError(e, "CSBetting PlaceBet 异常"); + return reply; + } + finally + { + FunGameService.ReleaseUserSemaphoreSlim(uid); + } + } + + /// + /// 领奖指令(内部 + API) + /// + [HttpPost("claim")] + public BotReply ClaimRewards([FromQuery] long uid) + { + MarkdownMessage md = new() { Content = busy }; + BotReply reply = new() { Markdown = md }; + try + { + PluginConfig pc = FunGameService.GetUserConfig(uid, out bool isTimeout); + if (isTimeout) return reply; + if (pc.Count == 0) + { + md.Content = noSaved; + return reply; + } + + User user = FunGameService.GetUser(pc); + long total = CSBettingSQLService.ClaimRewards(uid); + if (total > 0) + { + user.Inventory.Credits += (int)total; + FunGameService.SetUserConfigButNotRelease(uid, pc, user); + md.Content = $"领取成功!获得 {total} {General.GameplayEquilibriumConstant.InGameCurrency}。"; + } + else + { + FunGameService.SetUserConfigButNotRelease(uid, pc, user); + md.Content = "没有可领取的奖励。"; + } + return reply; + } + catch (Exception e) + { + Logger.LogError(e, "CSBetting ClaimRewards 异常"); + return reply; + } + finally + { + FunGameService.ReleaseUserSemaphoreSlim(uid); + } + } + + // ---------- 管理员操作 ---------- + + /// + /// 结算比赛(管理员) + /// + [HttpPost("settle")] + public BotReply SettleMatch([FromQuery] long uid, [FromQuery] int matchId, [FromQuery] string winner, [FromQuery] string result = "") + { + // 这里可以添加管理员权限检查,假设调用者已经过授权 + 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; + } + + md.Content = CSBettingSQLService.SettleMatch(matchId, winner, result); + return reply; + } + catch (Exception e) + { + Logger.LogError(e, "CSBetting SettleMatch 异常"); + return reply; + } + } + + /// + /// 创建赛事(管理员) + /// + [HttpPost("create-event")] + public BotReply CreateEvent([FromBody] CreateEventRequest request) + { + MarkdownMessage md = new() { Content = busy }; + BotReply reply = new() { Markdown = md }; + try + { + if (!FunGameConstant.UserIdAndUsername.TryGetValue(request.Uid, out User? user) || (!user.IsAdmin && !user.IsOperator)) + { + md.Content = "你没有权限执行此操作。"; + return reply; + } + + if (CSBettingSQLService.CreateEvent(request.Name, request.StartTime, request.EndTime, out string error, out long? newId)) + { + md.Content = $"赛事创建成功!新赛事ID:{newId}"; + } + else + { + md.Content = error; + } + return reply; + } + catch (Exception e) + { + Logger.LogError(e, "CreateEvent 异常"); + return reply; + } + } + + /// + /// 创建比赛(管理员) + /// + [HttpPost("create-match")] + public BotReply CreateMatch([FromBody] CreateMatchRequest request) + { + MarkdownMessage md = new() { Content = busy }; + BotReply reply = new() { Markdown = md }; + try + { + if (!FunGameConstant.UserIdAndUsername.TryGetValue(request.Uid, out User? user) || (!user.IsAdmin && !user.IsOperator)) + { + md.Content = "你没有权限执行此操作。"; + return reply; + } + + if (CSBettingSQLService.CreateMatch(request.EventId, request.Team1Name, request.Team2Name, request.Stage, + request.StartTime, request.BetDeadline, request.AvailableOptions, out string error, out long? newId)) + { + md.Content = $"比赛创建成功!新比赛ID:{newId}"; + } + else + { + md.Content = error; + } + return reply; + } + catch (Exception e) + { + Logger.LogError(e, "CreateMatch 异常"); + return reply; + } + } + } +} diff --git a/OshimaWebAPI/Controllers/FunGameController.cs b/OshimaWebAPI/Controllers/FunGameController.cs index e7da60b..e1fabee 100644 --- a/OshimaWebAPI/Controllers/FunGameController.cs +++ b/OshimaWebAPI/Controllers/FunGameController.cs @@ -15,7 +15,6 @@ using Oshima.FunGame.OshimaModules.Items; using Oshima.FunGame.OshimaModules.Models; using Oshima.FunGame.OshimaModules.Regions; using Oshima.FunGame.OshimaServers.Model; -using Oshima.FunGame.OshimaServers.Models; using Oshima.FunGame.OshimaServers.Service; using ProjectRedbud.FunGame.SQLQueryExtension; diff --git a/OshimaWebAPI/Controllers/QQBotController.cs b/OshimaWebAPI/Controllers/QQBotController.cs index cd30da7..f3606be 100644 --- a/OshimaWebAPI/Controllers/QQBotController.cs +++ b/OshimaWebAPI/Controllers/QQBotController.cs @@ -5,7 +5,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Oshima.FunGame.OshimaServers.Models; +using Oshima.FunGame.OshimaServers.Model; using Oshima.FunGame.WebAPI.Services; using Rebex.Security.Cryptography; diff --git a/OshimaWebAPI/OshimaWebAPI.cs b/OshimaWebAPI/OshimaWebAPI.cs index 57f9308..b22780f 100644 --- a/OshimaWebAPI/OshimaWebAPI.cs +++ b/OshimaWebAPI/OshimaWebAPI.cs @@ -13,7 +13,7 @@ using Oshima.Core.Constant; using Oshima.FunGame.OshimaModules.Characters; using Oshima.FunGame.OshimaModules.Items; using Oshima.FunGame.OshimaModules.Models; -using Oshima.FunGame.OshimaServers.Models; +using Oshima.FunGame.OshimaServers.Model; using Oshima.FunGame.OshimaServers.Service; using Oshima.FunGame.WebAPI.Constant; using Oshima.FunGame.WebAPI.Controllers; @@ -224,6 +224,7 @@ namespace Oshima.FunGame.WebAPI builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); // 使用 Configure 从配置源绑定 builder.Services.Configure(builder.Configuration.GetSection("Bot")); } diff --git a/OshimaWebAPI/Services/QQBotService.cs b/OshimaWebAPI/Services/QQBotService.cs index 54da8ef..7643add 100644 --- a/OshimaWebAPI/Services/QQBotService.cs +++ b/OshimaWebAPI/Services/QQBotService.cs @@ -4,7 +4,7 @@ using System.Text.Json; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Oshima.FunGame.OshimaServers.Models; +using Oshima.FunGame.OshimaServers.Model; namespace Oshima.FunGame.WebAPI.Services { diff --git a/OshimaWebAPI/Services/RainBOTService.cs b/OshimaWebAPI/Services/RainBOTService.cs index c19750c..75fbaa4 100644 --- a/OshimaWebAPI/Services/RainBOTService.cs +++ b/OshimaWebAPI/Services/RainBOTService.cs @@ -9,15 +9,16 @@ using Milimoe.FunGame.Core.Library.Constant; using Oshima.Core.Configs; using Oshima.Core.Constant; using Oshima.FunGame.OshimaModules.Models; -using Oshima.FunGame.OshimaServers.Models; +using Oshima.FunGame.OshimaServers.Model; using Oshima.FunGame.OshimaServers.Service; using Oshima.FunGame.WebAPI.Constant; using Oshima.FunGame.WebAPI.Controllers; +using Oshima.FunGame.WebAPI.Model; using Oshima.FunGame.WebAPI.Models; namespace Oshima.FunGame.WebAPI.Services { - public class RainBOTService(FunGameController controller, QQController qqcontroller, QQBotService service, ILogger logger, IMemoryCache memoryCache, TestController testController) + public class RainBOTService(FunGameController controller, QQController qqcontroller, QQBotService service, ILogger logger, IMemoryCache memoryCache, TestController testController, CSBettingController bettingController) { private static List FunGameItemType { get; } = ["卡包", "武器", "防具", "鞋子", "饰品", "消耗品", "魔法卡", "收藏品", "特殊物品", "任务物品", "礼包", "其他"]; private bool FunGameSimulation { get; set; } = false; @@ -27,6 +28,7 @@ namespace Oshima.FunGame.WebAPI.Services private ILogger Logger { get; } = logger; private IMemoryCache MemoryCache { get; set; } = memoryCache; private TestController TestController { get; set; } = testController; + private CSBettingController BettingController { get; set; } = bettingController; private async Task SendAsync(IBotMessage msg, string title, BotReply reply, int? msgSeq = null) { @@ -3876,6 +3878,309 @@ namespace Oshima.FunGame.WebAPI.Services return result; } + if (e.Detail == "竞猜帮助") + { + e.UseNotice = false; + BotReply reply = new() + { + Markdown = new MarkdownMessage + { + Content = "🎮 CS赛事竞猜帮助:\r\n" + + $"✨ {"赛事列表".CreateCmdInput()} - 查看所有赛事\r\n" + + $"✨ {"我的竞猜".CreateCmdInput()} - 查看我的投注记录\r\n" + + $"✨ {"竞猜领奖".CreateCmdInput()} - 领取竞猜奖励\r\n" + + $"✨ {"比赛详情".CreateCmdInput()} - 查看单场比赛并投注" + }, + Keyboard = new KeyboardMessage() + .AppendButtons(2, + Button.CreateCmdButton("📋 赛事列表", "赛事列表"), + Button.CreateCmdButton("📜 我的竞猜", "我的竞猜"), + Button.CreateCmdButton("💰 竞猜领奖", "竞猜领奖"), + Button.CreateCmdButton("❓ 竞猜帮助", "竞猜帮助")) + }; + await SendAsync(e, "CS赛事竞猜", reply); + return true; + } + + // 赛事列表 + if (e.Detail == "赛事列表") + { + BotReply reply = BettingController.GetEventsOverview(); + await SendAsync(e, "CS赛事竞猜", reply); + return true; + } + + if (e.Detail.StartsWith("比赛详情")) + { + string detail = e.Detail.Replace("比赛详情", "").Trim(); + if (int.TryParse(detail, out int matchId)) + { + BotReply reply = BettingController.GetMatchDetail(matchId); + // 构建投注键盘(填充“竞猜 <选项> ”) + reply.Keyboard = new KeyboardMessage() + .AppendButtons(2, + Button.CreateCmdButton("⚔️ 队伍1胜", $"竞猜 {matchId} team1 ", enter: false), + Button.CreateCmdButton("🛡️ 队伍2胜", $"竞猜 {matchId} team2 ", enter: false), + Button.CreateCmdButton("🎯 精确比分", $"竞猜 {matchId} score:", enter: false), + Button.CreateCmdButton("🏆 MVP", $"竞猜 {matchId} mvp:", enter: false)) + .AppendButtonsWithNewRow(2, + Button.CreateCmdButton("📋 赛事列表", "赛事列表"), + Button.CreateCmdButton("💰 竞猜领奖", "竞猜领奖")); + await SendAsync(e, "CS赛事竞猜", reply); + } + else + { + await SendAsync(e, "CS赛事竞猜", "格式:比赛详情 <比赛ID>"); + } + return true; + } + + // 赛事详情:用于查看某一赛事下的所有比赛 + if (e.Detail.StartsWith("赛事详情")) + { + string detail = e.Detail.Replace("赛事详情", "").Trim(); + if (int.TryParse(detail, out int eventId)) + { + // 调用控制器获取详情(返回BotReply) + BotReply reply = BettingController.GetEventDetail(eventId); + reply.Keyboard = new KeyboardMessage() + .AppendButtons(2, + Button.CreateCmdButton("🔍 比赛详情 ", "比赛详情 ", enter: false), + Button.CreateCmdButton("📋 赛事列表", "赛事列表"), + Button.CreateCmdButton("📜 我的竞猜", "我的竞猜"), + Button.CreateCmdButton("💰 竞猜领奖", "竞猜领奖")); + await SendAsync(e, "CS赛事竞猜", reply); + } + else + { + BotReply reply = new() + { + Markdown = new() + { + Content = "格式:赛事详情 <赛事ID>" + }, + Keyboard = new KeyboardMessage() + .AppendButtons(2, + Button.CreateCmdButton("📋 赛事列表", "赛事列表"), + Button.CreateCmdButton("❓ 竞猜帮助", "竞猜帮助")) + }; + await SendAsync(e, "CS赛事竞猜", reply); + } + return true; + } + + if (e.Detail == "我的竞猜") + { + BotReply reply = BettingController.GetMyBets(uid); + reply.Keyboard = new KeyboardMessage() + .AppendButtons(3, + Button.CreateCmdButton("💰 竞猜领奖", "竞猜领奖", enter: true), + Button.CreateCmdButton("📋 赛事列表", "赛事列表"), + Button.CreateCmdButton("❓ 竞猜帮助", "竞猜帮助")); + await SendAsync(e, "CS赛事竞猜", reply); + return true; + } + + if (e.Detail == "竞猜领奖") + { + BotReply reply = BettingController.ClaimRewards(uid); + reply.Keyboard = new KeyboardMessage() + .AppendButtons(2, + Button.CreateCmdButton("📜 我的竞猜", "我的竞猜"), + Button.CreateCmdButton("📋 赛事列表", "赛事列表")); + await SendAsync(e, "CS赛事竞猜", reply); + return true; + } + + if (e.Detail.StartsWith("竞猜")) + { + 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> <选项> <金额>\r\n选项:team1 / team2 / score:16:14 / 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(3, + Button.CreateCmdButton("📜 我的竞猜", "我的竞猜"), + Button.CreateCmdButton("📋 赛事列表", "赛事列表"), + Button.CreateCmdButton("❓ 竞猜帮助", "竞猜帮助")); + + // 成功时追加“继续查看该场比赛”按钮(填充指令) + if (success) + { + kb.AppendButtonsWithNewRow(2, + Button.CreateCmdButton("🔄 再次投注", e.Detail, enter: false), + Button.CreateCmdButton("🔍 比赛详情", $"比赛详情 {mid}")); + } + + reply.Keyboard = kb; + await SendAsync(e, "CS赛事竞猜", reply); + return true; + } + + // 管理员结算 + if (e.Detail.StartsWith("结算比赛")) + { + string detail = e.Detail.Replace("结算比赛", "").Trim(); + string[] parts = detail.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length >= 2 && int.TryParse(parts[0], out int mid)) + { + string winner = "", mResult = ""; + foreach (var p in parts[1..]) + { + if (p.StartsWith("winner=")) winner = p[7..]; + if (p.StartsWith("result=")) mResult = p[7..]; + } + BotReply reply = BettingController.SettleMatch(uid, mid, winner, mResult); + reply.Keyboard = new KeyboardMessage() + .AppendButtons(2, + Button.CreateCmdButton("📋 赛事列表", "赛事列表"), + Button.CreateCmdButton("⚙️ 继续结算", "结算比赛 ", enter: false)); + await SendAsync(e, "CS赛事竞猜", reply); + } + else + await SendAsync(e, "CS赛事竞猜", "格式:结算比赛 <比赛ID> winner=team1 result=16:14"); + return true; + } + + // 指令:创建赛事 <名称> <开始时间> <结束时间> + // 示例:创建赛事 春季赛 2026-03-01 2026-03-10 + // 示例:创建赛事 总决赛 2026-04-01 2026-04-05 1001,1002 + if (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("创建赛事", "").Trim(); + string[] parts = detail.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length < 3) + { + await SendAsync(e, "创建赛事", "格式:创建赛事 <名称> <开始时间> <结束时间>\r\n" + + "时间格式:yyyy-MM-dd HH:mm 或 yyyy-MM-dd(默认00:00)\r\n" + + "示例:创建赛事 春季赛 2026-03-01 2026-03-10\r\n" + + "示例:创建赛事 总决赛 2026-04-01 12:00 2026-04-05 18:00"); + return true; + } + + string name = parts[0]; + if (name.Length > 100) + { + await SendAsync(e, "创建赛事", "赛事名称不能超过100个字符。"); + return true; + } + + // 尝试解析时间(支持 yyyy-MM-dd 或 yyyy-MM-dd HH:mm) + string startStr = parts[1] + (parts[2].Contains(':') ? " " + parts[2] : " 00:00"); + int nextIndex = parts[2].Contains(':') ? 3 : 2; + string endStr = parts[nextIndex] + (parts.Length > nextIndex + 1 && parts[nextIndex + 1].Contains(':') ? " " + parts[nextIndex + 1] : " 00:00"); + int mvpStart = parts[nextIndex].Contains(':') ? nextIndex + 2 : nextIndex + 1; + + if (!DateTime.TryParseExact(startStr, new[] { "yyyy-MM-dd HH:mm", "yyyy-MM-dd" }, null, System.Globalization.DateTimeStyles.None, out DateTime startTime) || + !DateTime.TryParseExact(endStr, new[] { "yyyy-MM-dd HH:mm", "yyyy-MM-dd" }, null, System.Globalization.DateTimeStyles.None, out DateTime endTime)) + { + await SendAsync(e, "创建赛事", "时间格式错误,请使用 yyyy-MM-dd 或 yyyy-MM-dd HH:mm。"); + return true; + } + + BotReply reply = BettingController.CreateEvent(new CreateEventRequest + { + Uid = uid, + Name = name, + StartTime = startTime, + EndTime = endTime + }); + await SendAsync(e, "创建赛事", reply); + 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,score + if (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("创建比赛", "").Trim(); + string[] parts = detail.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length < 7) // 最少需要 eventId, team1, team2, stage, start date, start time, deadline date, deadline time + { + await SendAsync(e, "创建比赛", + "格式:创建比赛 <赛事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"); + return true; + } + + if (!int.TryParse(parts[0], out int eventId)) + { + await SendAsync(e, "创建比赛", "赛事ID必须为数字。"); + return true; + } + + string team1 = parts[1]; + string team2 = parts[2]; + string stage = parts[3]; + + // 开始时间(parts[4] + parts[5]) + string startDate = parts[4]; + string startTime = parts[5]; + // 截止时间(parts[6] + parts[7] 如果存在) + if (parts.Length < 8) + { + await SendAsync(e, "创建比赛", "投注截止时间需要完整日期和时间,示例:2026-03-05 13:55"); + return true; + } + string deadlineDate = parts[6]; + string deadlineTime = parts[7]; + + if (!DateTime.TryParseExact(startDate + " " + startTime, "yyyy-MM-dd HH:mm", null, System.Globalization.DateTimeStyles.None, out DateTime startDt) || + !DateTime.TryParseExact(deadlineDate + " " + deadlineTime, "yyyy-MM-dd HH:mm", null, System.Globalization.DateTimeStyles.None, out DateTime deadlineDt)) + { + await SendAsync(e, "创建比赛", "时间格式错误,请使用 yyyy-MM-dd HH:mm(开始时间和截止时间各两段)。"); + return true; + } + + string options = "team1_win,team2_win"; + if (parts.Length > 8) + { + options = string.Join(",", parts[8..]); // 剩余部分视为选项列表 + } + + BotReply reply = BettingController.CreateMatch(new CreateMatchRequest + { + Uid = uid, + EventId = eventId, + Team1Name = team1, + Team2Name = team2, + Stage = stage, + StartTime = startDt, + BetDeadline = deadlineDt, + AvailableOptions = options + }); + await SendAsync(e, "创建比赛", reply); + return true; + } + if (uid == GeneralSettings.Master && e.Detail.StartsWith("重载FunGame", StringComparison.CurrentCultureIgnoreCase)) { string msg = Controller.Relaod(uid);