添加有趣的玩意...

This commit is contained in:
milimoe 2026-05-09 06:50:39 +08:00
parent 108bb295e9
commit dd46e85606
Signed by: milimoe
GPG Key ID: 9554D37E4B8991D0
11 changed files with 1181 additions and 9 deletions

View File

@ -0,0 +1,212 @@
using System.Text.Json.Serialization;
namespace Oshima.FunGame.WebAPI.Model
{
/// <summary>
/// 赛事状态
/// </summary>
public enum EventStatus
{
/// <summary>
/// 未开始
/// </summary>
Upcoming,
/// <summary>
/// 进行中
/// </summary>
InProgress,
/// <summary>
/// 已结束
/// </summary>
Completed
}
/// <summary>
/// 比赛状态
/// </summary>
public enum MatchStatus
{
/// <summary>
/// 未开始
/// </summary>
Scheduled,
/// <summary>
/// 正在进行
/// </summary>
Live,
/// <summary>
/// 已结束
/// </summary>
Finished
}
/// <summary>
/// 投注选项类型
/// </summary>
public enum BetOptionType
{
/// <summary>
/// 队伍1胜
/// </summary>
Team1Win,
/// <summary>
/// 队伍2胜
/// </summary>
Team2Win,
/// <summary>
/// 精确比分
/// </summary>
Score,
/// <summary>
/// 赛事MVP
/// </summary>
MVP
}
/// <summary>
/// CS 队伍
/// </summary>
public class CSTeam
{
public int Id { get; set; }
public string Name { get; set; } = "";
public string LogoUrl { get; set; } = "";
public List<string> Players { get; set; } = [];
}
/// <summary>
/// 赛事Event
/// </summary>
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<CSMatch> Matches { get; set; } = [];
/// <summary>
/// MVP 候选人列表(玩家 OpenId 或游戏内 UID
/// </summary>
public List<long> MVPCandidates { get; set; } = [];
}
/// <summary>
/// 单场比赛
/// </summary>
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; } = "";
/// <summary>
/// 竞猜截止时间
/// </summary>
public DateTime BetDeadline { get; set; }
/// <summary>
/// 比赛结果(如 “16:14” 或 “2:1”
/// </summary>
///
public string? Result { get; set; }
/// <summary>
/// 获胜方1=Team12=Team20=平局/未定)
/// </summary>
public int Winner { get; set; }
/// <summary>
/// 支持的投注选项类型(常规包含 Team1Win/Team2Win总决赛增加 Score
/// </summary>
public List<BetOptionType> AvailableOptions { get; set; } = [BetOptionType.Team1Win, BetOptionType.Team2Win];
}
/// <summary>
/// 用户投注记录
/// </summary>
public class BetRecord
{
public long Id { get; set; }
public long UserId { get; set; }
public int MatchId { get; set; }
public BetOptionType OptionType { get; set; }
/// <summary>
/// 投注选项内容(如 “Team1Win” 或具体比分 “16:14”
/// </summary>
public string OptionValue { get; set; } = "";
/// <summary>
/// 投注金币
/// </summary>
public long Amount { get; set; }
/// <summary>
/// 投注时间
/// </summary>
public DateTime BetTime { get; set; }
/// <summary>
/// 是否已结算
/// </summary>
public bool IsSettled { get; set; }
/// <summary>
/// 结算金额(含本金),未结算为 null
/// </summary>
public long? Payout { get; set; }
/// <summary>
/// 结算备注
/// </summary>
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";
}
}

View File

@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Oshima.FunGame.OshimaServers.Models
namespace Oshima.FunGame.OshimaServers.Model
{
public class Payload
{

View File

@ -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<string> 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;
}
}
}

View File

@ -1,4 +1,4 @@
using Oshima.FunGame.OshimaServers.Models;
using Oshima.FunGame.OshimaServers.Model;
namespace Oshima.FunGame.OshimaServers.Service
{

View File

@ -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

View File

@ -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<CSBettingController> logger) : ControllerBase
{
private ILogger<CSBettingController> 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) } };
}
// ---------- 需要用户锁的操作 ----------
/// <summary>
/// 投注指令(内部 + API
/// </summary>
[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);
}
}
/// <summary>
/// 领奖指令(内部 + API
/// </summary>
[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);
}
}
// ---------- 管理员操作 ----------
/// <summary>
/// 结算比赛(管理员)
/// </summary>
[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;
}
}
/// <summary>
/// 创建赛事(管理员)
/// </summary>
[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;
}
}
/// <summary>
/// 创建比赛(管理员)
/// </summary>
[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;
}
}
}
}

View File

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

View File

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

View File

@ -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<FunGameController>();
builder.Services.AddScoped<QQController>();
builder.Services.AddScoped<TestController>();
builder.Services.AddScoped<CSBettingController>();
// 使用 Configure<BotConfig> 从配置源绑定
builder.Services.Configure<BotConfig>(builder.Configuration.GetSection("Bot"));
}

View File

@ -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
{

View File

@ -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<RainBOTService> logger, IMemoryCache memoryCache, TestController testController)
public class RainBOTService(FunGameController controller, QQController qqcontroller, QQBotService service, ILogger<RainBOTService> logger, IMemoryCache memoryCache, TestController testController, CSBettingController bettingController)
{
private static List<string> FunGameItemType { get; } = ["卡包", "武器", "防具", "鞋子", "饰品", "消耗品", "魔法卡", "收藏品", "特殊物品", "任务物品", "礼包", "其他"];
private bool FunGameSimulation { get; set; } = false;
@ -27,6 +28,7 @@ namespace Oshima.FunGame.WebAPI.Services
private ILogger<RainBOTService> 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);
// 构建投注键盘(填充“竞猜 <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);