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);