|
4 | 4 | using iCourse.ViewModels; |
5 | 5 | using Microsoft.Extensions.DependencyInjection; |
6 | 6 | using Newtonsoft.Json.Linq; |
| 7 | +using System.Collections.Concurrent; |
7 | 8 | using System.Diagnostics; |
8 | 9 | using System.Net.Http; |
9 | 10 | using System.Text; |
@@ -273,144 +274,121 @@ private async Task<List<Course>> GetFavoriteCoursesAsync() |
273 | 274 | } |
274 | 275 |
|
275 | 276 | private static readonly Random _random = new(); |
| 277 | + private readonly ConcurrentQueue<(Course course, TaskCompletionSource<(bool, string?)> tcs)> _taskQueue = new(); |
| 278 | + private readonly int _workerCount = Environment.ProcessorCount * 2; |
| 279 | + private readonly CancellationTokenSource _cts = new(); |
276 | 280 |
|
277 | | - private async Task<(bool isSuccess, string? msg)> TrySelectCourseOnceAsync(Course courseInfo) |
| 281 | + private string BuildRequestBody(Course course) |
278 | 282 | { |
279 | | - client.SetReferer($"https://icourses.jlu.edu.cn/xsxk/elective/grablessons?batchId={batch.batchId}"); |
| 283 | + return $"clazzId={Uri.EscapeDataString(course.CourseId)}&secretVal={Uri.EscapeDataString(course.SecretVal)}&clazzType={Uri.EscapeDataString(course.SelectType.ToString())}"; |
| 284 | + } |
280 | 285 |
|
281 | | - try |
282 | | - { |
283 | | - var content = new FormUrlEncodedContent(new Dictionary<string, string> |
| 286 | + public void StartWorkerPool() |
284 | 287 | { |
285 | | - { "clazzId", courseInfo.CourseId }, |
286 | | - { "secretVal", courseInfo.SecretVal }, |
287 | | - { "clazzType", courseInfo.SelectType.ToString() } |
288 | | - }); |
289 | | - |
290 | | - var response = await client.HttpPostAsync("xsxk/sc/clazz/addxk", content); |
291 | | - var json = JObject.Parse(response); |
292 | | - var code = json["code"].ToObject<int>(); |
293 | | - var msg = json["msg"].ToString(); |
294 | | - |
295 | | - if (code == 200) |
296 | | - { |
297 | | - MessageBox.Show(msg); |
298 | | - Logger.WriteLine("已选课程: " + courseInfo.Name); |
299 | | - return (true, null); |
300 | | - } |
301 | | - |
302 | | - if (msg == "该课程已在选课结果中") |
303 | | - { |
304 | | - Logger.WriteLine($"{courseInfo.Name} : 已放弃,尝试选下一门课程"); |
305 | | - return (true, null); |
306 | | - } |
307 | | - |
308 | | - if (msg == "课容量已满") |
309 | | - { |
310 | | - Logger.WriteLine($"{courseInfo.Name} : 容量已满,放弃"); |
311 | | - return (false, msg); |
312 | | - } |
313 | | - |
314 | | - Logger.WriteLine($"{courseInfo.Name} : {msg}"); |
315 | | - return (false, msg); |
316 | | - } |
317 | | - catch (Exception ex) |
| 288 | + for (int i = 0; i < _workerCount; i++) |
318 | 289 | { |
319 | | - Logger.WriteLine($"{courseInfo.Name} : 异常 -> {ex.Message}"); |
320 | | - return (false, ex.Message); |
| 290 | + Task.Factory.StartNew(async () => |
| 291 | + { |
| 292 | + while (!_cts.IsCancellationRequested) |
| 293 | + { |
| 294 | + if (_taskQueue.TryDequeue(out var workItem)) |
| 295 | + { |
| 296 | + var (course, tcs) = workItem; |
| 297 | + var result = await TrySelectCourseWithBackoffAsync(course); |
| 298 | + tcs.TrySetResult(result); |
| 299 | + } |
| 300 | + else |
| 301 | + { |
| 302 | + // 无任务时等待 |
| 303 | + await Task.Delay(10); |
| 304 | + } |
| 305 | + } |
| 306 | + }, TaskCreationOptions.LongRunning); |
321 | 307 | } |
322 | 308 | } |
323 | 309 |
|
324 | | - private async Task<(bool isSuccess, string? msg)> SelectCourseConcurrentlyAsync(Course courseInfo, int parallelCount = 3) |
| 310 | + private async Task<(bool isSuccess, string? msg)> TrySelectCourseWithBackoffAsync(Course course) |
325 | 311 | { |
326 | | - var cts = new CancellationTokenSource(); |
| 312 | + client.DefaultRequestHeaders.Referrer = new Uri($"https://icourses.jlu.edu.cn/xsxk/elective/grablessons?batchId={batch.batchId}"); |
327 | 313 |
|
328 | | - var tasks = new List<Task<(bool isSuccess, string? msg)>>(); |
| 314 | + int failCount = 0; |
329 | 315 |
|
330 | | - object lockObj = new(); |
| 316 | + string requestBody = BuildRequestBody(course); |
| 317 | + var content = new StringContent(requestBody, Encoding.UTF8, "application/x-www-form-urlencoded"); |
331 | 318 |
|
332 | | - (bool isSuccess, string? msg) finalResult = (false, null); |
333 | | - bool completed = false; |
334 | | - |
335 | | - for (int i = 0; i < parallelCount; i++) |
| 319 | + while (true) |
336 | 320 | { |
337 | | - tasks.Add(Task.Run(async () => |
| 321 | + try |
338 | 322 | { |
339 | | - while (!cts.IsCancellationRequested && !completed) |
| 323 | + var response = await client.PostAsync("xsxk/sc/clazz/addxk", content); |
| 324 | + var respStr = await response.Content.ReadAsStringAsync(); |
| 325 | + |
| 326 | + var json = JObject.Parse(respStr); |
| 327 | + var code = json["code"].ToObject<int>(); |
| 328 | + var msg = json["msg"].ToString(); |
| 329 | + |
| 330 | + if (code == 200) |
340 | 331 | { |
341 | | - var result = await TrySelectCourseOnceAsync(courseInfo); |
| 332 | + Logger.WriteLine($"成功选课: {course.Name}"); |
| 333 | + return (true, null); |
| 334 | + } |
342 | 335 |
|
343 | | - if (result.isSuccess) |
344 | | - { |
345 | | - lock (lockObj) |
346 | | - { |
347 | | - if (!completed) |
348 | | - { |
349 | | - completed = true; |
350 | | - finalResult = result; |
351 | | - cts.Cancel(); |
352 | | - } |
353 | | - } |
354 | | - break; |
355 | | - } |
356 | | - else if (result.msg == "课容量已满" || result.msg == "该课程已在选课结果中") |
357 | | - { |
358 | | - lock (lockObj) |
359 | | - { |
360 | | - if (!completed) |
361 | | - { |
362 | | - completed = true; |
363 | | - finalResult = result; |
364 | | - cts.Cancel(); |
365 | | - } |
366 | | - } |
367 | | - break; |
368 | | - } |
| 336 | + Logger.WriteLine($"{course.Name} : {msg}"); |
369 | 337 |
|
370 | | - // 避免所有线程同时请求 |
371 | | - await Task.Delay(30 + _random.Next(0, 30)); |
| 338 | + if (msg == "该课程已在选课结果中" || msg == "课容量已满") |
| 339 | + { |
| 340 | + return (msg == "课容量已满" ? (false, msg) : (true, null)); |
372 | 341 | } |
373 | 342 |
|
374 | | - return finalResult; |
375 | | - })); |
376 | | - } |
377 | 343 |
|
378 | | - await Task.WhenAll(tasks); |
| 344 | + failCount++; |
| 345 | + int delay = Math.Min(500, 100 + failCount * 100 + _random.Next(0, 50)); |
| 346 | + await Task.Delay(delay); |
| 347 | + } |
| 348 | + catch (Exception ex) |
| 349 | + { |
| 350 | + Logger.WriteLine($"{course.Name} : 异常 {ex.Message}"); |
| 351 | + failCount++; |
| 352 | + int delay = Math.Min(500, 100 + failCount * 100 + _random.Next(0, 50)); |
| 353 | + await Task.Delay(delay); |
| 354 | + } |
| 355 | + } |
| 356 | + } |
379 | 357 |
|
380 | | - return finalResult; |
| 358 | + public async Task<(bool isSuccess, string? msg)> EnqueueSelectCourseAsync(Course course) |
| 359 | + { |
| 360 | + var tcs = new TaskCompletionSource<(bool, string?)>(TaskCreationOptions.RunContinuationsAsynchronously); |
| 361 | + _taskQueue.Enqueue((course, tcs)); |
| 362 | + return await tcs.Task; |
381 | 363 | } |
382 | 364 |
|
383 | | - public async void StartSelectClassAsync() |
| 365 | + public async Task StartSelectClassAsync() |
384 | 366 | { |
385 | 367 | var list = await GetFavoriteCoursesAsync(); |
386 | 368 |
|
387 | | - int totalTasks = list.Count; |
388 | | - int completedTasks = 0; |
| 369 | + StartWorkerPool(); |
| 370 | + |
| 371 | + int total = list.Count; |
| 372 | + int completed = 0; |
389 | 373 |
|
390 | 374 | var tasks = list.Select(async course => |
391 | 375 | { |
392 | | - // 3 个并发任务 |
393 | | - var (isSuccess, msg) = await SelectCourseConcurrentlyAsync(course, parallelCount: 3); |
394 | | - |
395 | | - int currentCompleted = Interlocked.Increment(ref completedTasks); |
396 | | - WeakReferenceMessenger.Default.Send(new SelectCourseFinishedMessage(currentCompleted, totalTasks)); |
397 | | - |
| 376 | + var (isSuccess, msg) = await EnqueueSelectCourseAsync(course); |
| 377 | + Interlocked.Increment(ref completed); |
| 378 | + WeakReferenceMessenger.Default.Send(new SelectCourseFinishedMessage(completed, total)); |
398 | 379 | return new { course.Name, isSuccess, msg }; |
399 | 380 | }).ToList(); |
400 | 381 |
|
401 | 382 | var results = await Task.WhenAll(tasks); |
402 | 383 |
|
403 | 384 | Logger.WriteLine("选课完成!"); |
404 | 385 |
|
405 | | - var failedCourses = results.Where(r => !r.isSuccess).ToList(); |
406 | | - var succeeded = results.Count(r => r.isSuccess); |
| 386 | + foreach (var r in results.Where(r => !r.isSuccess)) |
| 387 | + Logger.WriteLine($"课程失败: {r.Name}, 原因: {r.msg}"); |
407 | 388 |
|
408 | | - foreach (var r in failedCourses) |
409 | | - { |
410 | | - Logger.WriteLine($"课程选择失败: {r.Name}, 原因: {r.msg}"); |
411 | | - } |
| 389 | + Logger.WriteLine($"成功数: {results.Count(r => r.isSuccess)}"); |
412 | 390 |
|
413 | | - Logger.WriteLine($"成功选择课程数: {succeeded}"); |
| 391 | + _cts.Cancel(); |
414 | 392 | } |
415 | 393 |
|
416 | 394 | private void KeepOnline() |
|
0 commit comments