Skip to content

Commit 6478f3e

Browse files
committed
fix: 今日高光由于超时返回错误结果。
1 parent b9f51a7 commit 6478f3e

1 file changed

Lines changed: 192 additions & 117 deletions

File tree

Bleatingsheep.NewHydrant.Bot.Public/Osu/Highlight.cs

Lines changed: 192 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Text;
77
using System.Text.RegularExpressions;
88
using System.Threading;
9+
using System.Threading.Channels;
910
using System.Threading.Tasks;
1011
using Bleatingsheep.NewHydrant.Attributions;
1112
using Bleatingsheep.NewHydrant.Core;
@@ -16,156 +17,230 @@
1617
using Sisters.WudiLib.Posts;
1718
using MessageContext = Sisters.WudiLib.Posts.Message;
1819

19-
namespace Bleatingsheep.NewHydrant.Osu
20+
namespace Bleatingsheep.NewHydrant.Osu;
21+
22+
[Component("highlight")]
23+
public sealed partial class Highlight : Service, IMessageCommand
2024
{
21-
[Component("highlight")]
22-
public sealed class Highlight : Service, IMessageCommand
25+
private readonly IDbContextFactory<NewbieContext> _dbContextFactory;
26+
27+
public Highlight(OsuApiClient osuApi, IDbContextFactory<NewbieContext> dbContextFactory)
2328
{
24-
private readonly IDbContextFactory<NewbieContext> _dbContextFactory;
29+
OsuApi = osuApi;
30+
_dbContextFactory = dbContextFactory;
31+
}
2532

26-
public Highlight(OsuApiClient osuApi, IDbContextFactory<NewbieContext> dbContextFactory)
33+
private OsuApiClient OsuApi { get; }
34+
35+
public async Task ProcessAsync(MessageContext superContext, HttpApiClient api)
36+
{
37+
var context = superContext as GroupMessage;
38+
var groupMembers = await api.GetGroupMemberListAsync(context.GroupId);
39+
Logger.Info($"群 {context.GroupId} 开启今日高光,成员共 {groupMembers.Length} 名。");
40+
41+
var stopwatch = Stopwatch.StartNew();
42+
43+
await using var dbContext = _dbContextFactory.CreateDbContext();
44+
//var bindings = await (from b in dbContext.Bindings
45+
// join mi in groupMembers on b.UserId equals mi.UserId
46+
// select new { Info = mi, Binding = b.OsuId }).ToListAsync();
47+
////var history = (from bi in bindings.AsQueryable()
48+
//// join ui in motherShip.Userinfo on bi.Binding equals ui.UserId into histories
49+
//// select new { bi.Info, bi.Binding, History = histories.OrderByDescending(ui => ui.QueryDate).First() }).ToList();
50+
//var osuIds = bindings.Select(b => b.Binding).Distinct().ToList();
51+
var qqs = groupMembers.Select(mi => mi.UserId).ToList();
52+
var osuIds = await dbContext
53+
.Bindings.Where(bi => qqs.Contains(bi.UserId))
54+
.Select(bi => bi.OsuId)
55+
.Distinct()
56+
.ToListAsync();
57+
58+
Logger.Info($"找到 {osuIds.Count} 个绑定信息,耗时 {stopwatch.ElapsedMilliseconds}ms。");
59+
if (osuIds.Count > 100)
2760
{
28-
OsuApi = osuApi;
29-
_dbContextFactory = dbContextFactory;
61+
await api.SendMessageAsync(
62+
context.Endpoint,
63+
"开始查询今日高光,本群人数较多,可能耗时较长,请耐心等待。"
64+
);
3065
}
3166

32-
private OsuApiClient OsuApi { get; }
33-
34-
public async Task ProcessAsync(MessageContext superContext, HttpApiClient api)
67+
Bleatingsheep.Osu.Mode mode = 0;
68+
if (!string.IsNullOrEmpty(ModeString))
3569
{
36-
var context = superContext as GroupMessage;
37-
var groupMembers = await api.GetGroupMemberListAsync(context.GroupId);
38-
Logger.Debug($"群 {context.GroupId} 开启今日高光,成员共 {groupMembers.Length} 名。");
39-
40-
var stopwatch = Stopwatch.StartNew();
41-
42-
await using var dbContext = _dbContextFactory.CreateDbContext();
43-
//var bindings = await (from b in dbContext.Bindings
44-
// join mi in groupMembers on b.UserId equals mi.UserId
45-
// select new { Info = mi, Binding = b.OsuId }).ToListAsync();
46-
////var history = (from bi in bindings.AsQueryable()
47-
//// join ui in motherShip.Userinfo on bi.Binding equals ui.UserId into histories
48-
//// select new { bi.Info, bi.Binding, History = histories.OrderByDescending(ui => ui.QueryDate).First() }).ToList();
49-
//var osuIds = bindings.Select(b => b.Binding).Distinct().ToList();
50-
var qqs = groupMembers.Select(mi => mi.UserId).ToList();
51-
var osuIds = await dbContext.Bindings.Where(bi => qqs.Contains(bi.UserId)).Select(bi => bi.OsuId).Distinct().ToListAsync();
52-
53-
Logger.Debug($"找到 {osuIds.Count} 个绑定信息,耗时 {stopwatch.ElapsedMilliseconds}ms。");
54-
55-
Bleatingsheep.Osu.Mode mode = 0;
56-
if (!string.IsNullOrEmpty(ModeString))
70+
try
5771
{
58-
try
59-
{
60-
mode = Bleatingsheep.Osu.ModeExtensions.Parse(ModeString);
61-
}
62-
catch (FormatException)
63-
{
64-
// ignore
65-
}
72+
mode = Bleatingsheep.Osu.ModeExtensions.Parse(ModeString);
6673
}
74+
catch (FormatException)
75+
{
76+
// ignore
77+
}
78+
}
6779

68-
stopwatch = Stopwatch.StartNew();
69-
List<UserSnapshot> history = await GetHistories(osuIds, mode).ConfigureAwait(false);
70-
71-
Logger.Debug($"找到 {history.Count} 个历史信息,耗时 {stopwatch.ElapsedMilliseconds}ms。");
72-
73-
// Using ConcurrentBag is enough here. ConcurrentDictionary is unnecessary and costly.
74-
var nowInfos = new ConcurrentDictionary<int, UserInfo>(10, history.Count);
75-
var fails = new BlockingCollection<int>();
76-
stopwatch = Stopwatch.StartNew();
77-
var fetchIds = history.Select(h => (int)h.UserId).Distinct().ToList();
78-
int completes = 0;
79-
var cancellationToken = new CancellationTokenSource(TimeSpan.FromMinutes(1)).Token;
80-
var tasks = fetchIds.Concat(fails.GetConsumingEnumerable()).Select(async bi =>
80+
stopwatch = Stopwatch.StartNew();
81+
List<UserSnapshot> history = await GetHistories(osuIds, mode).ConfigureAwait(false);
82+
83+
Logger.Info($"找到 {history.Count} 个历史信息,耗时 {stopwatch.ElapsedMilliseconds}ms。");
84+
85+
var nowInfos = new ConcurrentDictionary<int, UserInfo>(-1, history.Count);
86+
var fails = Channel.CreateBounded<int>(history.Count);
87+
var (failsTx, failsRx) = (fails.Writer, fails.Reader);
88+
stopwatch = Stopwatch.StartNew();
89+
var fetchIds = history.Select(h => h.UserId).Distinct().ToList();
90+
int completes = 0;
91+
var cancellationToken = new CancellationTokenSource(TimeSpan.FromMinutes(5)).Token;
92+
var tasks = await fetchIds
93+
.ToAsyncEnumerable()
94+
.Concat(failsRx.ReadAllAsync())
95+
.Select(async bi =>
8196
{
82-
var (success, userInfo) = await OsuApi.GetCachedUserInfo(bi, (Bleatingsheep.Osu.Mode)mode).ConfigureAwait(false);
97+
var (success, userInfo) = await OsuApi
98+
.GetCachedUserInfo(bi, mode)
99+
.ConfigureAwait(false);
83100
if (!success)
84101
{
85102
if (cancellationToken.IsCancellationRequested)
86-
fails.CompleteAdding();
87-
if (!fails.IsAddingCompleted)
88-
try
89-
{
90-
fails.Add(bi);
91-
}
92-
catch (InvalidOperationException)
93-
{
94-
}
103+
{
104+
Logger.Error("查询用户信息超时,取消中...");
105+
_ = failsTx.TryComplete();
106+
}
107+
108+
_ = failsTx.TryWrite(bi);
109+
// 如果写入时 Channel 已关闭,则会写入失败,这种情况下不需要再写入了。
95110
}
96111
else
97112
{
98113
Interlocked.Increment(ref completes);
99114
if (completes == fetchIds.Count)
100-
fails.CompleteAdding();
115+
{
116+
_ = failsTx.TryComplete();
117+
}
118+
101119
if (userInfo != null)
120+
{
102121
nowInfos[bi] = userInfo;
122+
}
103123
}
104-
}).ToArray();
105-
await Task.WhenAll(tasks).ConfigureAwait(false);
106-
Logger.Debug($"查询 API 花费 {stopwatch.ElapsedMilliseconds}ms,失败 {fails.Count} 个。");
107-
108-
var cps = (from his in history
109-
join now in nowInfos on his.UserId equals now.Key
110-
where his.UserInfo.PlayCount != now.Value.PlayCount
111-
orderby now.Value.Performance - his.UserInfo.Performance descending
112-
select new { Old = his.UserInfo, New = now.Value, Meta = his }).ToList();
124+
})
125+
.ToArrayAsync();
126+
await Task.WhenAll(tasks).ConfigureAwait(false);
127+
Logger.Info(
128+
$"查询 API 花费 {stopwatch.ElapsedMilliseconds}ms,失败 {tasks.Length - fetchIds.Count} 个。"
129+
);
130+
131+
var errorMessages = new List<string>();
132+
if (cancellationToken.IsCancellationRequested)
133+
{
134+
errorMessages.Add("查询用户信息超时,部分数据可能不完整。");
135+
}
136+
if (tasks.Length - fetchIds.Count > 0)
137+
{
138+
errorMessages.Add($"有 {tasks.Length - fetchIds.Count} 人增量数据查询失败。");
139+
}
113140

114-
if (fails.Count > 0)
115-
{
116-
await api.SendGroupMessageAsync(context.GroupId, $"失败了 {fails.Count} 人。");
117-
}
118-
if (cps.Count == 0)
141+
var cps = (
142+
from his in history
143+
join now in nowInfos on his.UserId equals now.Key
144+
where his.UserInfo.PlayCount != now.Value.PlayCount
145+
orderby now.Value.Performance - his.UserInfo.Performance descending
146+
select new
119147
{
120-
await api.SendMessageAsync(context.Endpoint, "你群根本没有人屙屎。");
121-
return;
148+
Old = his.UserInfo,
149+
New = now.Value,
150+
Meta = his,
122151
}
123-
else
152+
).ToList();
153+
154+
if (cps.Count == 0)
155+
{
156+
var message = "你群根本没有人屙屎。";
157+
if (errorMessages.Count > 0)
124158
{
125-
var increase = cps.Find(cp => cp.Old.Performance != 0 && cp.New.Performance != cp.Old.Performance);
126-
var mostPlay = cps.OrderByDescending(cp => cp.New.TotalHits - cp.Old.TotalHits).First();
127-
var longestPlay = cps.OrderByDescending(cp => cp.New.TotalSecondsPlayed - cp.Old.TotalSecondsPlayed).First();
128-
var sb = new StringBuilder(100);
129-
sb.AppendLine("最飞升:");
130-
if (increase != null)
131-
// sb.AppendLine($"{increase.New.Name} 增加了 {increase.New.Performance - increase.Old.Performance:#.##} PP。")
132-
sb.Append(increase.New.Name).Append(" 增加了 ").AppendFormat("{0:#.##}", increase.New.Performance - increase.Old.Performance).AppendLine(" PP。")
133-
// .AppendLine($"({increase.Old.Performance:#.##} -> {increase.New.Performance:#.##})");
134-
.Append('(').AppendFormat("{0:#.##}", increase.Old.Performance).Append(" -> ").AppendFormat("{0:#.##}", increase.New.Performance).AppendLine(")");
135-
else
136-
sb.AppendLine("你群没有人飞升。");
137-
sb.AppendLine("最肝:")
138-
// .Append($"{mostPlay.New.Name} 打了 {mostPlay.New.TotalHits - mostPlay.Old.TotalHits} 下。");
139-
.Append(mostPlay.New.Name).Append(" 打了 ").Append(mostPlay.New.TotalHits - mostPlay.Old.TotalHits).Append(" 下。");
140-
sb.AppendLine();
141-
sb.Append($"{longestPlay.New.Name} 玩儿了 {TimeSpan.FromSeconds(longestPlay.New.TotalSecondsPlayed - longestPlay.Old.TotalSecondsPlayed).TotalHours:#.##} 小时。");
142-
143-
await api.SendMessageAsync(context.Endpoint, sb.ToString());
159+
var noOsuSB = new StringBuilder("你群根本没有人屙屎。\r\n\r\n错误信息:\r\n");
160+
noOsuSB.AppendJoin("\r\n", errorMessages);
161+
message = noOsuSB.ToString();
144162
}
163+
await api.SendMessageAsync(context.Endpoint, message);
164+
return;
145165
}
146166

147-
private async Task<List<UserSnapshot>> GetHistories(List<int> osuIds, Bleatingsheep.Osu.Mode mode)
167+
var increase = cps.Find(cp =>
168+
cp.Old.Performance != 0 && cp.New.Performance != cp.Old.Performance
169+
);
170+
var mostPlay = cps.OrderByDescending(cp => cp.New.TotalHits - cp.Old.TotalHits).First();
171+
var longestPlay = cps.OrderByDescending(cp =>
172+
cp.New.TotalSecondsPlayed - cp.Old.TotalSecondsPlayed
173+
)
174+
.First();
175+
var sb = new StringBuilder(100);
176+
sb.AppendLine("最飞升:");
177+
if (increase != null)
178+
{
179+
sb.AppendLine(
180+
$"{increase.New.Name} 增加了 {increase.New.Performance - increase.Old.Performance:#.##} PP。"
181+
)
182+
.AppendLine(
183+
$"({increase.Old.Performance:#.##} -> {increase.New.Performance:#.##})"
184+
);
185+
}
186+
else
148187
{
149-
await using var newbieContext = _dbContextFactory.CreateDbContext();
150-
var now = DateTimeOffset.UtcNow;
151-
var snaps = await newbieContext.UserSnapshots
152-
.Where(s => now.Subtract(TimeSpan.FromHours(36)) < s.Date && s.Mode == mode && osuIds.Contains(s.UserId))
153-
.ToListAsync().ConfigureAwait(false);
154-
155-
return snaps
156-
.GroupBy(s => s.UserId)
157-
.Select(g => g.OrderBy(s => Utilities.DateUtility.GetError(now - TimeSpan.FromHours(24), s.Date)).First())
158-
.ToList();
188+
sb.AppendLine("你群没有人飞升。");
159189
}
160190

161-
[Parameter("mode")]
162-
private string ModeString { get; set; }
191+
sb.AppendLine("最肝:")
192+
.AppendLine(
193+
$"{mostPlay.New.Name} 打了 {mostPlay.New.TotalHits - mostPlay.Old.TotalHits} 下。"
194+
);
195+
sb.Append(
196+
$"{longestPlay.New.Name} 玩儿了 {TimeSpan.FromSeconds(longestPlay.New.TotalSecondsPlayed - longestPlay.Old.TotalSecondsPlayed).TotalHours:#.##} 小时。"
197+
);
163198

164-
private static readonly Regex s_regex = new Regex(@"^今日高光\s*(?:[,,]\s*(?<mode>.+?)\s*)?$", RegexOptions.Compiled | RegexOptions.CultureInvariant);
199+
if (errorMessages.Count > 0)
200+
{
201+
sb.Append("\r\n\r\n错误信息:\r\n");
202+
sb.AppendJoin("\r\n", errorMessages);
203+
}
165204

166-
public bool ShouldResponse(MessageContext context)
167-
=> context is GroupMessage g
168-
&& g.Content.TryGetPlainText(out string text)
169-
&& RegexCommand(s_regex, text.Trim());
205+
await api.SendMessageAsync(context.Endpoint, sb.ToString());
170206
}
207+
208+
private async Task<List<UserSnapshot>> GetHistories(
209+
List<int> osuIds,
210+
Bleatingsheep.Osu.Mode mode
211+
)
212+
{
213+
await using var newbieContext = _dbContextFactory.CreateDbContext();
214+
var now = DateTimeOffset.UtcNow;
215+
var snaps = await newbieContext
216+
.UserSnapshots.Where(s =>
217+
now.Subtract(TimeSpan.FromHours(36)) < s.Date
218+
&& s.Mode == mode
219+
&& osuIds.Contains(s.UserId)
220+
)
221+
.ToListAsync()
222+
.ConfigureAwait(false);
223+
224+
return snaps
225+
.GroupBy(s => s.UserId)
226+
.Select(g =>
227+
g.OrderBy(s => Utilities.DateUtility.GetError(now - TimeSpan.FromHours(24), s.Date))
228+
.First()
229+
)
230+
.ToList();
231+
}
232+
233+
[Parameter("mode")]
234+
private string ModeString { get; set; }
235+
236+
[GeneratedRegex(
237+
@"^今日高光\s*(?:[,,]\s*(?<mode>.+?)\s*)?$",
238+
RegexOptions.Compiled | RegexOptions.CultureInvariant
239+
)]
240+
private static partial Regex MatchRegex();
241+
242+
public bool ShouldResponse(MessageContext context) =>
243+
context is GroupMessage g
244+
&& g.Content.TryGetPlainText(out string text)
245+
&& RegexCommand(MatchRegex(), text.Trim());
171246
}

0 commit comments

Comments
 (0)