This commit is contained in:
milimoe 2026-05-09 16:44:33 +08:00
parent dd46e85606
commit a5d91d6fc5
Signed by: milimoe
GPG Key ID: 9554D37E4B8991D0
5 changed files with 520 additions and 351 deletions

View File

@ -1,19 +1,21 @@
using System.Data;
using System.Security.Cryptography;
using System.Text;
using Milimoe.FunGame.Core.Api.Transmittal;
using Milimoe.FunGame.Core.Api.Utility;
using Milimoe.FunGame.Core.Library.Constant;
using Oshima.FunGame.OshimaServers.Model;
namespace Oshima.FunGame.WebAPI.Services
{
public class CSBettingSQLService
public class CSBettingService
{
public static string GetEventsOverview()
{
using SQLHelper? sql = Factory.OpenFactory.GetSQLHelper();
if (sql != null)
{
UpdateStatuses(sql);
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();
@ -35,6 +37,8 @@ namespace Oshima.FunGame.WebAPI.Services
using SQLHelper? sql = Factory.OpenFactory.GetSQLHelper();
if (sql != null)
{
UpdateStatuses(sql);
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 "赛事不存在。";
@ -66,7 +70,7 @@ namespace Oshima.FunGame.WebAPI.Services
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})");
sb.AppendLine($" [{mid}] {(stage != "" ? $"{stage} " : "")} {clickableMatch} (状态:{mStatusStr}, 截止:{deadline:MM-dd HH:mm})");
}
}
return sb.ToString();
@ -74,11 +78,14 @@ namespace Oshima.FunGame.WebAPI.Services
return "数据库连接失败。";
}
public static string GetMatchDetail(int matchId)
public static string GetMatchDetail(int matchId, out int status)
{
status = 0;
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 "比赛不存在。";
@ -86,11 +93,13 @@ namespace Oshima.FunGame.WebAPI.Services
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"]);
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 result = row["result"] != DBNull.Value ? row["result"].ToString() ?? "" : "";
long winner = row["winner"] != DBNull.Value ? Convert.ToInt64(row["winner"]) : 0;
string eventName = "";
sql.Parameters["@eid"] = eventId;
@ -113,12 +122,22 @@ namespace Oshima.FunGame.WebAPI.Services
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();
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("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}");
}
return sb.ToString().Trim();
}
return "数据库连接失败。";
}
@ -142,7 +161,24 @@ namespace Oshima.FunGame.WebAPI.Services
DateTime deadline = Convert.ToDateTime(row["bet_deadline"]);
if (status != 0 || DateTime.Now > deadline)
{
error = "当前比赛已截止或非投注期。";
error = "当前比赛已结束或非投注期。";
return false;
}
// --- 单场比赛投注上限检查 ---
long alreadyBet = 0;
long totalBet = amount;
sql.Parameters["@uid"] = uid;
sql.Parameters["@mid"] = matchId;
sql.ExecuteDataSet("SELECT COALESCE(SUM(amount), 0) AS total FROM csbetting_bet_records WHERE user_id = @uid AND match_id = @mid");
if (sql.Success && sql.DataSet.Tables[0].Rows.Count > 0)
{
alreadyBet = Convert.ToInt64(sql.DataSet.Tables[0].Rows[0]["total"] ?? 0L);
totalBet += alreadyBet;
}
if (totalBet > 5000)
{
error = $"本场比赛你的投注总额不能超过 5000 {General.GameplayEquilibriumConstant.InGameCurrency}(已投 {alreadyBet})。";
return false;
}
@ -193,19 +229,26 @@ 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;
if (winner == "team1") winTeam = 1;
else if (winner == "team2") winTeam = 2;
else return "请指定获胜方为 team1 或 team2。";
int winTeam = 3;
if (!isMvp)
{
if (winner == "team1") winTeam = 1;
else if (winner == "team2") winTeam = 2;
else return "请指定获胜方为 team1 或 team2。MVP 赛事获胜方请直接指定选手 ID。";
}
// 更新比赛结果
sql.Parameters["@res"] = result;
@ -228,7 +271,8 @@ namespace Oshima.FunGame.WebAPI.Services
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;
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 = "未中奖";
@ -249,46 +293,127 @@ namespace Oshima.FunGame.WebAPI.Services
return "数据库连接失败。";
}
public static string GetMyBets(long uid)
public static string GetMyBets(long uid, long mid = -1)
{
using SQLHelper? sql = Factory.OpenFactory.GetSQLHelper();
if (sql != null)
{
UpdateStatuses(sql);
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");
string matchFilter = "";
if (mid > 0)
{
sql.Parameters["@mid"] = mid;
matchFilter = " AND br.match_id = @mid";
}
sql.ExecuteDataSet($@"SELECT br.match_id, m.team1_name, m.team2_name, m.status AS match_status,
GROUP_CONCAT(CONCAT(br.option_type, ':', br.option_value, ':', br.amount) 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,
SUM(CASE WHEN br.is_settled = 1 AND br.payout > 0 AND br.is_claimed = 0 THEN 1 ELSE 0 END) AS unclaimed_count,
SUM(CASE WHEN br.is_settled = 1 AND br.payout = 0 THEN 1 ELSE 0 END) AS lost_count,
SUM(CASE WHEN br.is_settled = 1 THEN br.payout ELSE 0 END) AS total_payout
FROM csbetting_bet_records br
JOIN csbetting_matches m ON br.match_id = m.id
WHERE br.user_id = @uid {matchFilter}
GROUP BY br.match_id, m.team1_name, m.team2_name, m.status
ORDER BY MAX(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"]);
int matchId = Convert.ToInt32(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"]);
int matchStatus = Convert.ToInt32(row["match_status"]);
long totalAmount = Convert.ToInt64(row["total_amount"]);
long totalPayout = Convert.ToInt64(row["total_payout"]);
int allSettled = Convert.ToInt32(row["all_settled"]);
int claimedCount = Convert.ToInt32(row["claimed_count"]);
int unclaimedCount = Convert.ToInt32(row["unclaimed_count"]);
int lostCount = Convert.ToInt32(row["lost_count"]);
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}");
// 解析投注详情
string detailsStr = row["details"].ToString() ?? "";
string[]?parts = detailsStr.Split('|');
List<string> summary = [];
foreach (string part in parts)
{
string[] items = part.Split(':');
if (items.Length >= 3)
{
int otype = int.Parse(items[0]);
string ovalue = items[1];
long oamount = long.Parse(items[2]);
string optStr = otype switch
{
1 => $"{t1}胜",
2 => $"{t2}胜",
3 => $"比分 {ovalue}",
4 => $"MVP {ovalue}",
_ => ovalue
};
summary.Add($"{optStr} {oamount}G");
}
}
string detailLine = string.Join(", ", summary);
string statusLine;
if (allSettled == 0)
{
statusLine = "待开奖";
}
else
{
if (claimedCount > 0 && unclaimedCount == 0 && lostCount == 0)
statusLine = "已领取";
else if (unclaimedCount > 0)
statusLine = "待领奖";
else if (lostCount > 0 && claimedCount == 0 && unclaimedCount == 0)
statusLine = "未中奖";
else
statusLine = "部分已领";
}
string matchLabel = $"{t1} vs {t2}".CreateCmdInput($"比赛详情 {matchId}");
sb.Append($"[比赛{matchId}] {matchLabel} | ");
sb.Append($"投注:{totalAmount}G ({detailLine}) | ");
sb.Append($"状态:{statusLine}");
if (totalPayout > 0)
sb.Append($" (+{totalPayout}G)");
sb.AppendLine();
}
return sb.ToString();
return sb.ToString().TrimEnd();
}
return "数据库连接失败。";
}
/// <summary>
/// 根据当前时间更新赛事和比赛的状态(仅更新未结束的记录)
/// </summary>
private static void UpdateStatuses(SQLHelper sql)
{
DateTime now = DateTime.Now;
// 更新赛事状态0→1 (进行中)1→2 (已结束)
sql.Parameters["@now"] = now;
sql.Execute("UPDATE csbetting_events SET status = 1 WHERE status = 0 AND start_time <= @now AND end_time > @now");
sql.Parameters["@now"] = now;
sql.Execute("UPDATE csbetting_events SET status = 2 WHERE status <= 1 AND end_time <= @now");
// 更新比赛状态0→1 (进行中)1→2 (已结束) - 注意不要覆盖已结算的比赛winner 为 null 时视为未结束)
sql.Parameters["@now"] = now;
sql.Execute("UPDATE csbetting_matches SET status = 1 WHERE status = 0 AND start_time <= @now AND bet_deadline < @now AND winner IS NULL");
// 对于已经过了开始时间但还没有 winner 且状态为 1 的,可保留为进行中;实际上只要 winner 为 null状态应为 1进行中
// 如果有结果但 winner 不为 null管理员应该已经手动结算状态会设为 2这里不做额外修改。
// 安全起见,只更新未开始的,以及当比赛时间已过且无 winner 时自动变成进行中。
// 如果需要自动结束(比如时间过长),可再添加规则,但竞猜系统通常由管理员手动结算结束。
// 这里只做基础更新。
}
public static long ClaimRewards(long uid)
{
// 返回领取的总金币,由上层加到用户身上

View File

@ -13,7 +13,6 @@ using Oshima.FunGame.OshimaModules.Regions;
using Oshima.FunGame.OshimaModules.Skills;
using Oshima.FunGame.OshimaModules.Units;
using Oshima.FunGame.OshimaServers.Model;
using Oshima.FunGame.OshimaServers.Model;
using ProjectRedbud.FunGame.SQLQueryExtension;
namespace Oshima.FunGame.OshimaServers.Service

View File

@ -28,28 +28,35 @@ namespace Oshima.FunGame.WebAPI.Controllers
[HttpGet("events")]
public BotReply GetEventsOverview()
{
return new BotReply { Markdown = new MarkdownMessage { Content = CSBettingSQLService.GetEventsOverview() } };
return new BotReply { Markdown = new MarkdownMessage { Content = CSBettingService.GetEventsOverview() } };
}
[AllowAnonymous]
[HttpGet("event/{eventId:int}")]
public BotReply GetEventDetail(int eventId)
{
return new BotReply { Markdown = new MarkdownMessage { Content = CSBettingSQLService.GetEventDetail(eventId) } };
return new BotReply { Markdown = new MarkdownMessage { Content = CSBettingService.GetEventDetail(eventId) } };
}
[AllowAnonymous]
[HttpGet("match/{matchId:int}")]
public BotReply GetMatchDetail(int matchId)
{
return new BotReply { Markdown = new MarkdownMessage { Content = CSBettingSQLService.GetMatchDetail(matchId) } };
return new BotReply { Markdown = new MarkdownMessage { Content = CSBettingService.GetMatchDetail(matchId, out int status) + (status == 0 ? $"\r\n竞猜指令{"".CreateCmdInput()} <比赛ID> <选项> <{General.GameplayEquilibriumConstant.InGameCurrency}数>\r\n👇🏻 点击下方按钮快速竞猜" : "")} };
}
[AllowAnonymous]
[HttpGet("mybets/{uid:long}")]
public BotReply GetMyBets(long uid)
{
return new BotReply { Markdown = new MarkdownMessage { Content = CSBettingSQLService.GetMyBets(uid) } };
return new BotReply { Markdown = new MarkdownMessage { Content = CSBettingService.GetMyBets(uid) } };
}
[AllowAnonymous]
[HttpGet("mybets/{uid:long}/{mid:long}")]
public BotReply GetMyBets(long uid, long mid)
{
return new BotReply { Markdown = new MarkdownMessage { Content = CSBettingService.GetMyBets(uid, mid) } };
}
// ---------- 需要用户锁的操作 ----------
@ -88,7 +95,7 @@ namespace Oshima.FunGame.WebAPI.Controllers
return reply;
}
if (CSBettingSQLService.PlaceBet(uid, matchId, option, amount, out string error))
if (CSBettingService.PlaceBet(uid, matchId, option, amount, out string error))
{
user.Inventory.Credits -= (int)amount;
FunGameService.SetUserConfigButNotRelease(uid, pc, user);
@ -131,7 +138,7 @@ namespace Oshima.FunGame.WebAPI.Controllers
}
User user = FunGameService.GetUser(pc);
long total = CSBettingSQLService.ClaimRewards(uid);
long total = CSBettingService.ClaimRewards(uid);
if (total > 0)
{
user.Inventory.Credits += (int)total;
@ -176,7 +183,7 @@ namespace Oshima.FunGame.WebAPI.Controllers
return reply;
}
md.Content = CSBettingSQLService.SettleMatch(matchId, winner, result);
md.Content = CSBettingService.SettleMatch(matchId, winner, result);
return reply;
}
catch (Exception e)
@ -202,7 +209,7 @@ namespace Oshima.FunGame.WebAPI.Controllers
return reply;
}
if (CSBettingSQLService.CreateEvent(request.Name, request.StartTime, request.EndTime, out string error, out long? newId))
if (CSBettingService.CreateEvent(request.Name, request.StartTime, request.EndTime, out string error, out long? newId))
{
md.Content = $"赛事创建成功新赛事ID{newId}";
}
@ -235,7 +242,7 @@ namespace Oshima.FunGame.WebAPI.Controllers
return reply;
}
if (CSBettingSQLService.CreateMatch(request.EventId, request.Team1Name, request.Team2Name, request.Stage,
if (CSBettingService.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}";

View File

@ -0,0 +1,336 @@
using Milimoe.FunGame.Core.Entity;
using Milimoe.FunGame.Core.Library.Constant;
using Oshima.FunGame.OshimaModules.Models;
using Oshima.FunGame.OshimaServers.Model;
using Oshima.FunGame.WebAPI.Model;
namespace Oshima.FunGame.WebAPI.Services
{
public partial class RainBOTService
{
public async Task<bool> HandleCSBettingInput(IBotMessage e, long uid)
{
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);
KeyboardMessage kb = new();
// 构建投注键盘(填充“竞猜 <matchId> <选项> ”)
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));
}
kb.AppendButtonsWithNewRow(2,
Button.CreateCmdButton("📋 赛事列表", "赛事列表"),
Button.CreateCmdButton("💰 竞猜领奖", "竞猜领奖"));
reply.Keyboard = kb;
BotReply reply2 = BettingController.GetMyBets(uid, matchId);
if (reply.Markdown != null && reply.Markdown.Content != null && !(reply2.Markdown?.Content?.Equals("你还没有任何竞猜记录。") ?? true))
{
reply.Markdown.Content = $"{reply.Markdown.Content.Trim()}\r\n你的本场竞猜记录\r\n{reply2.Markdown.Content}";
}
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(2,
Button.CreateCmdButton("💰 竞猜领奖", "竞猜领奖", enter: true),
Button.CreateCmdButton("📋 赛事列表", "赛事列表"),
Button.CreateCmdButton("❓ 竞猜帮助", "竞猜帮助"));
if (reply.Markdown?.Content?.Contains("创建存档") ?? false)
{
reply.Keyboard.AppendButtons(2, 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> <选项> <{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("❓ 竞猜帮助", "竞猜帮助"));
if (reply.Markdown?.Content?.Contains("创建存档") ?? false)
{
kb.AppendButtons(2, 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=2:0");
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");
if (!DateTime.TryParseExact(startStr, CheckDateTimeFormat, null, System.Globalization.DateTimeStyles.None, out DateTime startTime) ||
!DateTime.TryParseExact(endStr, CheckDateTimeFormat, 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;
}
return false;
}
}
}

View File

@ -13,14 +13,14 @@ 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<RainBOTService> logger, IMemoryCache memoryCache, TestController testController, CSBettingController bettingController)
public partial class RainBOTService(FunGameController controller, QQController qqcontroller, QQBotService service, ILogger<RainBOTService> logger, IMemoryCache memoryCache, TestController testController, CSBettingController bettingController)
{
private static List<string> FunGameItemType { get; } = ["卡包", "武器", "防具", "鞋子", "饰品", "消耗品", "魔法卡", "收藏品", "特殊物品", "任务物品", "礼包", "其他"];
private static string[] CheckDateTimeFormat { get; } = ["yyyy-MM-dd HH:mm", "yyyy-MM-dd"];
private bool FunGameSimulation { get; set; } = false;
private FunGameController Controller { get; } = controller;
private QQController QQController { get; } = qqcontroller;
@ -3878,306 +3878,8 @@ namespace Oshima.FunGame.WebAPI.Services
return result;
}
if (e.Detail == "竞猜帮助")
if (await HandleCSBettingInput(e, uid))
{
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);
// 构建投注键盘(填充“竞猜 <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;
}