diff --git a/TechHelper.Client/Exam/ExamService.cs b/TechHelper.Client/Exam/ExamService.cs index 9c40579..bcf65ed 100644 --- a/TechHelper.Client/Exam/ExamService.cs +++ b/TechHelper.Client/Exam/ExamService.cs @@ -1,24 +1,21 @@ -using System.Xml.Serialization; +using System.Xml.Serialization; // 用于 XML 序列化/反序列化 using TechHelper.Client.AI; using TechHelper.Services; using Entities.DTO; -using System.Net.Http.Json; -using Newtonsoft.Json; -using TechHelper.Client.Pages.Exam; - +using System.Net.Http.Json; // 用于 PostAsJsonAsync +using Newtonsoft.Json; // 用于 JSON 反序列化 namespace TechHelper.Client.Exam { public class ExamService : IExamService { - private IAIService aIService; - private IHttpClientFactory httpClientFactory; + private readonly IAIService _aIService; // 遵循命名规范,字段前加下划线 + private readonly HttpClient _client; // 直接注入 HttpClient - public ExamService(IAIService aIService, - IHttpClientFactory httpClientFactory) + public ExamService(IAIService aIService, HttpClient client) // 修正点:直接注入 HttpClient { - this.aIService = aIService; - this.httpClientFactory = httpClientFactory; + _aIService = aIService; + _client = client; // 赋值注入的 HttpClient 实例 } public ApiResponse ConvertToXML(string xmlContent) @@ -31,32 +28,16 @@ namespace TechHelper.Client.Exam try { T deserializedObject = (T)serializer.Deserialize(reader); - - // 成功时返回 ApiResponse - return new ApiResponse - { - Status = true, - Result = deserializedObject, - Message = "XML 反序列化成功。" - }; + return ApiResponse.Success(result: deserializedObject, message: "XML 反序列化成功。"); } catch (InvalidOperationException ex) { - return new ApiResponse - { - Status = false, - Result = null, - Message = $"XML 反序列化操作错误: {ex.Message}. 内部异常: {ex.InnerException?.Message ?? "无"}" - }; + return ApiResponse.Error( + message: $"XML 反序列化操作错误: {ex.Message}. 内部异常: {ex.InnerException?.Message ?? "无"}"); } catch (Exception ex) { - return new ApiResponse - { - Status = false, - Result = null, - Message = $"处理 XML 反序列化时发生未知错误: {ex.Message}" - }; + return ApiResponse.Error(message: $"处理 XML 反序列化时发生未知错误: {ex.Message}"); } } } @@ -65,35 +46,21 @@ namespace TechHelper.Client.Exam { try { - string respon = await aIService.CallGLM(examContent, AIConfiguration.BreakQuestions); + string? response = await _aIService.CallGLM(examContent, AIConfiguration.BreakQuestions); - if (respon != null) + if (response != null) { - return new ApiResponse - { - Status = true, - Result = respon, - Message = "试题分割成功。" - }; + return ApiResponse.Success(result: response, message: "试题分割成功。"); } else { - return new ApiResponse - { - Status = false, - Result = null, - Message = "AI 服务未能返回有效内容,或返回内容为空。" - }; + return ApiResponse.Error(message: "AI 服务未能返回有效内容,或返回内容为空。"); } } catch (Exception ex) { - return new ApiResponse - { - Status = false, - Result = null, - Message = $"处理试题分割时发生内部错误: {ex.Message}" - }; + // 实际应用中,这里应该加入日志记录 + return ApiResponse.Error(message: $"处理试题分割时发生内部错误: {ex.Message}"); } } @@ -101,112 +68,96 @@ namespace TechHelper.Client.Exam { try { - string respon = await aIService.CallGLM(examContent, AIConfiguration.Format); + string? response = await _aIService.CallGLM(examContent, AIConfiguration.Format); - if (respon != null) + if (response != null) { - return new ApiResponse - { - Status = true, - Result = respon, - Message = "试题格式化成功。" - }; + return ApiResponse.Success(result: response, message: "试题格式化成功。"); } else { - return new ApiResponse - { - Status = false, - Result = null, - Message = "AI 服务未能返回有效内容,或返回内容为空。" - }; + return ApiResponse.Error(message: "AI 服务未能返回有效内容,或返回内容为空。"); } } catch (Exception ex) { - return new ApiResponse - { - Status = false, - Result = null, - Message = $"处理试题格式化时发生内部错误: {ex.Message}" - }; + // 实际应用中,这里应该加入日志记录 + return ApiResponse.Error(message: $"处理试题格式化时发生内部错误: {ex.Message}"); } } public async Task GetAllExam(string user) { - using (var client = httpClientFactory.CreateClient("Default")) + // 直接使用注入的 _client 实例 + var response = await _client.GetAsync($"exam/getAllPreview?user={user}"); + + if (response.IsSuccessStatusCode) { - var response = await client.GetAsync($"exam/getAllPreview?user={user}"); - - - if (response.IsSuccessStatusCode) - { - var content = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject>(content); - return ApiResponse.Success(result: result); - } - else - { - return ApiResponse.Error(await response.Content.ReadAsStringAsync()); - } + var content = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject>(content); + return ApiResponse.Success(result: result); + } + else + { + // 读取错误信息,并返回 ApiResponse + var errorContent = await response.Content.ReadAsStringAsync(); + return ApiResponse.Error(message: $"获取所有试题失败: {response.StatusCode} - {errorContent}"); } } public async Task GetExam(Guid guid) { - return ApiResponse.Success("HELLO"); + + var response = await _client.GetAsync($"exam/get?id={guid}"); + if (response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync(); + var exam = JsonConvert.DeserializeObject(content); + return ApiResponse.Success(result: exam); + } + else + { + var errorContent = await response.Content.ReadAsStringAsync(); + return ApiResponse.Error(message: $"获取试题失败: {response.StatusCode} - {errorContent}"); + } } public async Task ParseSingleQuestionGroup(string examContent) { try { - string respon = await aIService.CallGLM(examContent, AIConfiguration.ParseSignelQuestion2); + string? response = await _aIService.CallGLM(examContent, AIConfiguration.ParseSignelQuestion2); - if (respon != null) + if (response != null) { - return new ApiResponse - { - Status = true, - Result = respon, - Message = "试题解析成功。" - }; + return ApiResponse.Success(result: response, message: "试题解析成功。"); } else { - return new ApiResponse - { - Status = false, - Result = null, - Message = "AI 服务未能返回有效内容,或返回内容为空。" - }; + return ApiResponse.Error(message: "AI 服务未能返回有效内容,或返回内容为空。"); } } catch (Exception ex) { - return new ApiResponse - { - Status = false, - Result = null, - Message = $"处理试题解析时发生内部错误: {ex.Message}" - }; + // 实际应用中,这里应该加入日志记录 + return ApiResponse.Error(message: $"处理试题解析时发生内部错误: {ex.Message}"); } } public async Task SaveParsedExam(ExamDto examDto) { - using (var client = httpClientFactory.CreateClient("Default")) - { - var respont = await client.PostAsJsonAsync("exam/add", - examDto); + // 直接使用注入的 _client 实例 + var response = await _client.PostAsJsonAsync("exam/add", examDto); - if (respont.StatusCode == System.Net.HttpStatusCode.OK) - { - return new ApiResponse(true, "ok"); - } - return new ApiResponse("false"); + if (response.IsSuccessStatusCode) // 检查是否是成功的状态码,例如 200 OK, 201 Created 等 + { + return ApiResponse.Success(message: "试题保存成功。"); + } + else + { + var errorContent = await response.Content.ReadAsStringAsync(); + return ApiResponse.Error(message: $"保存试题失败: {response.StatusCode} - {errorContent}"); } } } -} +} \ No newline at end of file diff --git a/TechHelper.Client/Exam/ExamStruct.cs b/TechHelper.Client/Exam/ExamStruct.cs new file mode 100644 index 0000000..f335709 --- /dev/null +++ b/TechHelper.Client/Exam/ExamStruct.cs @@ -0,0 +1,92 @@ +using Entities.DTO; + +namespace TechHelper.Client.Exam +{ + public class ExamStruct + { + public string Title { get; set; } + public List Questions { get; set; } = new List(); + + + public class QuestionItem + { + public string Sequence { get; set; } = string.Empty; + public string QuestionText { get; set; } = string.Empty; + public float Score { get; set; } + } + } + + public class Student + { + public Guid Id { get; set; } = Guid.NewGuid(); + public string Name { get; set; } = string.Empty; + } + + public class QuestionAnswerStatus + { + public string QuestionSequence { get; set; } = string.Empty; // 题目序号,例如 "1.1" + public string QuestionText { get; set; } = string.Empty; // 题目文本 + public float QuestionScore { get; set; } // 题目分值 + public Dictionary StudentCorrectStatus { get; set; } = new Dictionary(); + // Key: Student.Id, Value: true 表示正确,false 表示错误 + } + + public class QuestionRowData + { + public ExamStruct.QuestionItem QuestionItem { get; set; } // 原始题目信息 + public Dictionary StudentAnswers { get; set; } = new Dictionary(); + } + + + public static class ExamStructExtensions + { + public static ExamStruct GetStruct(this ExamDto dto) + { + if (dto == null) + { + return new ExamStruct { Title = "无效试卷", Questions = new List() }; + } + + var examStruct = new ExamStruct + { + Title = dto.AssignmentTitle + }; + + GetSeqRecursive(dto.QuestionGroups, null, examStruct.Questions); + + return examStruct; + } + + /// + /// 递归方法,用于生成所有题目和子题目的完整序号。 + /// + /// 当前正在处理的题目组。 + /// 当前题目组的父级序号(例如:"1", "2.1")。如果为空,则表示顶级题目组。 + /// 用于收集所有生成题目项的列表。 + private static void GetSeqRecursive( + QuestionGroupDto currentGroup, + string? parentSequence, + List allQuestions) + { + string currentGroupSequence = parentSequence != null + ? $"{parentSequence}.{currentGroup.Index}" + : currentGroup.Index.ToString(); + + foreach (var subQuestion in currentGroup.SubQuestions) + { + string fullSequence = $"{currentGroupSequence}.{subQuestion.Index}"; + allQuestions.Add(new ExamStruct.QuestionItem + { + Sequence = fullSequence, + QuestionText = subQuestion.Stem ?? string.Empty, + Score = subQuestion.Score + }); + } + + foreach (var subGroup in currentGroup.SubQuestionGroups) + { + GetSeqRecursive(subGroup, currentGroupSequence, allQuestions); + } + } + } +} diff --git a/TechHelper.Client/HttpInterceptor/HttpInterceptorHandlerService.cs b/TechHelper.Client/HttpInterceptor/HttpInterceptorHandlerService.cs index e2ebb36..2dacf93 100644 --- a/TechHelper.Client/HttpInterceptor/HttpInterceptorHandlerService.cs +++ b/TechHelper.Client/HttpInterceptor/HttpInterceptorHandlerService.cs @@ -1,30 +1,22 @@ -using TechHelper.Client.HttpRepository; -using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components; using System.Net; -using System.Net.Http.Headers; -using MudBlazor; -using Microsoft.Extensions.DependencyInjection; -using System; +using System.Net.Http.Headers; +using TechHelper.Client.HttpRepository; + namespace BlazorProducts.Client.HttpInterceptor { public class HttpInterceptorHandlerService : DelegatingHandler { - private readonly NavigationManager _navManager; - - private readonly RefreshTokenService _refreshTokenService; - - private readonly ISnackbar _serviceProvider; - private ISnackbar toastService = null; + private readonly NavigationManager _navManager; + private readonly RefreshTokenService2 _refreshTokenService; public HttpInterceptorHandlerService( NavigationManager navManager, - RefreshTokenService refreshTokenService, - ISnackbar serviceProvider) + RefreshTokenService2 refreshTokenService) { _navManager = navManager; _refreshTokenService = refreshTokenService; - _serviceProvider = serviceProvider; } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) @@ -33,53 +25,58 @@ namespace BlazorProducts.Client.HttpInterceptor if (absolutePath != null && !absolutePath.Contains("token") && !absolutePath.Contains("account")) { - var token = await _refreshTokenService.TryRefreshToken(); + var token = await _refreshTokenService.TryRefreshToken(); if (!string.IsNullOrEmpty(token)) { - request.Headers.Authorization = - new AuthenticationHeaderValue("bearer", token); + + request.Headers.Authorization = new AuthenticationHeaderValue("bearer", token); } } + var response = await base.SendAsync(request, cancellationToken); - HandleResponse(response); + await HandleResponse(response); return response; } - private void HandleResponse(HttpResponseMessage response) + private async Task HandleResponse(HttpResponseMessage response) { if (response is null) { + _navManager.NavigateTo("/error"); - throw new HttpResponseException("Server not available."); + throw new HttpResponseException("服务器不可用。"); } - var message = ""; - + if (!response.IsSuccessStatusCode) { + + var errorContent = await response.Content.ReadAsStringAsync(); + Console.WriteLine($"HTTP 错误: {response.StatusCode}. 详情: {errorContent}"); + switch (response.StatusCode) { case HttpStatusCode.NotFound: + // 404 Not Found error, navigate to a 404 page. _navManager.NavigateTo("/404"); - message = "Resource not found."; break; case HttpStatusCode.BadRequest: - message = "Invalid request. Please try again."; + // 400 Bad Request error. Often, you don't navigate for this; you display validation errors on the UI. break; case HttpStatusCode.Unauthorized: - _navManager.NavigateTo("/unauthorized"); - message = "Unauthorized access"; + // 401 Unauthorized error. Navigate to an unauthorized page or login page. + _navManager.NavigateTo("/unauthorized"); // Or: _navManager.NavigateTo("/authentication/login"); break; default: + // For all other errors, navigate to a general error page. _navManager.NavigateTo("/error"); - message = "Something went wrong. Please contact the administrator."; break; } - } } } -} + +} \ No newline at end of file diff --git a/TechHelper.Client/HttpRepository/AuthenticationClientService.cs b/TechHelper.Client/HttpRepository/AuthenticationClientService.cs index c3be32f..02167d5 100644 --- a/TechHelper.Client/HttpRepository/AuthenticationClientService.cs +++ b/TechHelper.Client/HttpRepository/AuthenticationClientService.cs @@ -13,21 +13,20 @@ namespace TechHelper.Client.HttpRepository { public class AuthenticationClientService : IAuthenticationClientService { - private HttpClient _client; - private readonly IHttpClientFactory _clientFactory; + private readonly HttpClient _client; private readonly JsonSerializerOptions _options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; private readonly AuthenticationStateProvider _stateProvider; private readonly ILocalStorageService _localStorageService; private readonly NavigationManager _navigationManager; - public AuthenticationClientService(IHttpClientFactory httpClientFactory, + // 构造函数现在直接接收 HttpClient + public AuthenticationClientService(HttpClient client, // <-- 修正点:直接注入 HttpClient AuthenticationStateProvider authenticationStateProvider, ILocalStorageService localStorageService, NavigationManager navigationManager) { - _clientFactory = httpClientFactory; - //_client = httpClientFactory.CreateClient("Default"); + _client = client; // <-- 修正点:直接赋值 _localStorageService = localStorageService; _stateProvider = authenticationStateProvider; _navigationManager = navigationManager; @@ -35,121 +34,105 @@ namespace TechHelper.Client.HttpRepository public async Task LoginAsync(UserForAuthenticationDto userForAuthenticationDto) { - using (_client = _clientFactory.CreateClient("Default")) - { + // 移除 using (_client = _clientFactory.CreateClient("Default")) + // _client 已经是注入的实例,直接使用它 + var reponse = await _client.PostAsJsonAsync("account/login", + userForAuthenticationDto); - var reponse = await _client.PostAsJsonAsync("account/login", - userForAuthenticationDto); + var content = await reponse.Content.ReadAsStringAsync(); - var content = await reponse.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(content, _options); - var result = JsonSerializer.Deserialize(content, _options); + if (!reponse.IsSuccessStatusCode || result.Is2StepVerificationRequired) + return result; - if (!reponse.IsSuccessStatusCode || result.Is2StepVerificationRequired) - return result; + _localStorageService.SetItem("authToken", result.Token); + _localStorageService.SetItem("refreshToken", result.RefreshToken); + ((AuthStateProvider)_stateProvider).NotifyUserAuthentication( + result.Token); - _localStorageService.SetItem("authToken", result.Token); - _localStorageService.SetItem("refreshToken", result.RefreshToken); - ((AuthStateProvider)_stateProvider).NotifyUserAuthentication( - result.Token); + // 直接在注入的 _client 实例上设置默认请求头 + _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue( + "bearer", result.Token); - _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue( - "bearer", result.Token); - - return new AuthResponseDto { IsAuthSuccessful = true }; - } + return new AuthResponseDto { IsAuthSuccessful = true }; } public async Task LogoutAsync() { - using (_client = _clientFactory.CreateClient("Default")) - { - _localStorageService.RemoveItem("authToken"); - _localStorageService.RemoveItem("refreshToken"); - ((AuthStateProvider)_stateProvider).NotifyUserLogout(); + // 移除 using (_client = _clientFactory.CreateClient("Default")) + _localStorageService.RemoveItem("authToken"); + _localStorageService.RemoveItem("refreshToken"); + ((AuthStateProvider)_stateProvider).NotifyUserLogout(); - _client.DefaultRequestHeaders.Authorization = null; - } + // 直接在注入的 _client 实例上清除默认请求头 + _client.DefaultRequestHeaders.Authorization = null; } public async Task RefreshTokenAsync() { - using (_client = _clientFactory.CreateClient("Default")) - { - var token = _localStorageService.GetItem("authToken"); - var refreshToken = _localStorageService.GetItem("refreshToken"); + // 移除 using (_client = _clientFactory.CreateClient("Default")) + var token = _localStorageService.GetItem("authToken"); + var refreshToken = _localStorageService.GetItem("refreshToken"); - var response = await _client.PostAsJsonAsync("token/refresh", - new RefreshTokenDto - { - Token = token, - RefreshToken = refreshToken - }); + var response = await _client.PostAsJsonAsync("token/refresh", + new RefreshTokenDto + { + Token = token, + RefreshToken = refreshToken + }); - var content = await response.Content.ReadAsStringAsync(); - var result = JsonSerializer.Deserialize(content, _options); + var content = await response.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(content, _options); - _localStorageService.SetItem("authToken", result.Token); - _localStorageService.SetItem("refreshToken", result.RefreshToken); + _localStorageService.SetItem("authToken", result.Token); + _localStorageService.SetItem("refreshToken", result.RefreshToken); - _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", result.Token); + // 直接在注入的 _client 实例上设置默认请求头 + _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", result.Token); - return result.Token; - } + return result.Token; } public async Task RegisterUserAsync(UserForRegistrationDto userForRegistrationDto) { - using (_client = _clientFactory.CreateClient("Default")) + // 移除 using (_client = _clientFactory.CreateClient("Default")) + userForRegistrationDto.ClientURI = Path.Combine( + _navigationManager.BaseUri, "emailconfirmation"); + + var reponse = await _client.PostAsJsonAsync("account/register", + userForRegistrationDto); + + if (!reponse.IsSuccessStatusCode) { - - - userForRegistrationDto.ClientURI = Path.Combine( - _navigationManager.BaseUri, "emailconfirmation"); - - var reponse = await _client.PostAsJsonAsync("account/register", - userForRegistrationDto); - - if (!reponse.IsSuccessStatusCode) - { - var content = await reponse.Content.ReadAsStringAsync(); - - var result = JsonSerializer.Deserialize(content, _options); - - return result; - } - - return new ResponseDto { IsSuccessfulRegistration = true }; + var content = await reponse.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(content, _options); + return result; } + + return new ResponseDto { IsSuccessfulRegistration = true }; } public async Task ForgotPasswordAsync(ForgotPasswordDto forgotPasswordDto) { - using (_client = _clientFactory.CreateClient("Default")) - { + // 移除 using (_client = _clientFactory.CreateClient("Default")) + forgotPasswordDto.ClientURI = Path.Combine(_navigationManager.BaseUri, "resetpassword"); + var result = await _client.PostAsJsonAsync("account/forgotpassword", + forgotPasswordDto); - - forgotPasswordDto.ClientURI = Path.Combine(_navigationManager.BaseUri, "resetpassword"); - var result = await _client.PostAsJsonAsync("account/forgotpassword", - forgotPasswordDto); - - return result.StatusCode; - } + return result.StatusCode; } public async Task ResetPasswordAsync(ResetPasswordDto resetPasswordDto) { - using (_client = _clientFactory.CreateClient("Default")) - { - var resetresult = await _client.PostAsJsonAsync("account/resetpassword", - resetPasswordDto); + // 移除 using (_client = _clientFactory.CreateClient("Default")) + var resetresult = await _client.PostAsJsonAsync("account/resetpassword", + resetPasswordDto); - var resetContent = await resetresult.Content.ReadAsStringAsync(); + var resetContent = await resetresult.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(resetContent, _options); - var result = JsonSerializer.Deserialize(resetContent, _options); - - return result; - } + return result; } public async Task EmailConfirmationAsync(string email, string token) @@ -159,40 +142,35 @@ namespace TechHelper.Client.HttpRepository ["email"] = email, ["token"] = token }; - using (_client = _clientFactory.CreateClient("Default")) - { - var response = await _client.GetAsync(QueryHelpers.AddQueryString( - "account/emailconfirmation", queryStringParam)); + // 移除 using (_client = _clientFactory.CreateClient("Default")) + var response = await _client.GetAsync(QueryHelpers.AddQueryString( + "account/emailconfirmation", queryStringParam)); - return response.StatusCode; - } + return response.StatusCode; } public async Task LoginVerfication(TwoFactorVerificationDto twoFactorVerificationDto) { - using (_client = _clientFactory.CreateClient("Default")) - { + // 移除 using (_client = _clientFactory.CreateClient("Default")) + var reponse = await _client.PostAsJsonAsync("account/twostepverification", + twoFactorVerificationDto); - var reponse = await _client.PostAsJsonAsync("account/twostepverification", - twoFactorVerificationDto); + var content = await reponse.Content.ReadAsStringAsync(); - var content = await reponse.Content.ReadAsStringAsync(); + var result = JsonSerializer.Deserialize(content, _options); - var result = JsonSerializer.Deserialize(content, _options); + if (!reponse.IsSuccessStatusCode) + return result; - if (!reponse.IsSuccessStatusCode) - return result; + _localStorageService.SetItem("authToken", result.Token); + _localStorageService.SetItem("refreshToken", result.RefreshToken); + ((AuthStateProvider)_stateProvider).NotifyUserAuthentication( + result.Token); - _localStorageService.SetItem("authToken", result.Token); - _localStorageService.SetItem("refreshToken", result.RefreshToken); - ((AuthStateProvider)_stateProvider).NotifyUserAuthentication( - result.Token); + _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue( + "bearer", result.Token); - _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue( - "bearer", result.Token); - - return new AuthResponseDto { IsAuthSuccessful = true }; - } + return new AuthResponseDto { IsAuthSuccessful = true }; } } -} +} \ No newline at end of file diff --git a/TechHelper.Client/HttpRepository/IRefreshTokenService.cs b/TechHelper.Client/HttpRepository/IRefreshTokenService.cs new file mode 100644 index 0000000..78afaaa --- /dev/null +++ b/TechHelper.Client/HttpRepository/IRefreshTokenService.cs @@ -0,0 +1,7 @@ +namespace TechHelper.Client.HttpRepository +{ + public interface IRefreshTokenService + { + public Task TryRefreshToken(); + } +} diff --git a/TechHelper.Client/HttpRepository/RefreshTokenService.cs b/TechHelper.Client/HttpRepository/RefreshTokenService.cs index 31da3cf..d11fcdc 100644 --- a/TechHelper.Client/HttpRepository/RefreshTokenService.cs +++ b/TechHelper.Client/HttpRepository/RefreshTokenService.cs @@ -1,34 +1,51 @@ using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.Extensions.DependencyInjection; // 需要这个命名空间来获取 IServiceProvider +using System; // 需要这个命名空间来使用 Lazy namespace TechHelper.Client.HttpRepository { - public class RefreshTokenService + public class RefreshTokenService : IRefreshTokenService { - private readonly AuthenticationStateProvider _authenticationStateProvider; - private readonly IAuthenticationClientService _authenticationClientService; - public RefreshTokenService(AuthenticationStateProvider authenticationStateProvider, IAuthenticationClientService authenticationClientService) + private readonly Lazy _authenticationStateProvider; + private readonly Lazy _authenticationClientService; + + public RefreshTokenService(IServiceProvider serviceProvider) { - _authenticationStateProvider = authenticationStateProvider; - _authenticationClientService = authenticationClientService; + _authenticationStateProvider = new Lazy( + () => serviceProvider.GetRequiredService()); + + _authenticationClientService = new Lazy( + () => serviceProvider.GetRequiredService()); } public async Task TryRefreshToken() { - var authState = await _authenticationStateProvider.GetAuthenticationStateAsync(); + var authState = await _authenticationStateProvider.Value.GetAuthenticationStateAsync(); var user = authState.User; - var expClaim = user.FindFirst(c => c.Type.Equals("exp")).Value; + + // 如果 user 或 claims 为空,表示用户未认证,直接返回空字符串 + if (user?.Identity == null || !user.Identity.IsAuthenticated) + { + return string.Empty; + } + + var expClaim = user.FindFirst(c => c.Type.Equals("exp"))?.Value; // 使用 ?. 防止空引用 + if (string.IsNullOrEmpty(expClaim)) + { + return string.Empty; // 没有过期时间声明,也直接返回 + } + var expTime = DateTimeOffset.FromUnixTimeSeconds( Convert.ToInt64(expClaim)); var diff = expTime - DateTime.UtcNow; + + // 只有当令牌即将过期时才尝试刷新 if (diff.TotalMinutes <= 2) - return await _authenticationClientService.RefreshTokenAsync(); + return await _authenticationClientService.Value.RefreshTokenAsync(); // 访问 .Value 来调用方法 return string.Empty; } - - - } -} +} \ No newline at end of file diff --git a/TechHelper.Client/HttpRepository/RefreshTokenService2.cs b/TechHelper.Client/HttpRepository/RefreshTokenService2.cs new file mode 100644 index 0000000..a17f8cf --- /dev/null +++ b/TechHelper.Client/HttpRepository/RefreshTokenService2.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Components.Authorization; + +namespace TechHelper.Client.HttpRepository +{ + public class RefreshTokenService2 + { + private readonly Lazy _authenticationStateProvider; + private readonly Lazy _authenticationClientService; + + public RefreshTokenService2(IServiceProvider serviceProvider) + { + _authenticationStateProvider = new Lazy( + () => serviceProvider.GetRequiredService()); + + _authenticationClientService = new Lazy( + () => serviceProvider.GetRequiredService()); + } + + public async Task TryRefreshToken() + { + var authState = await _authenticationStateProvider.Value.GetAuthenticationStateAsync(); + var user = authState.User; + + if (user?.Identity == null || !user.Identity.IsAuthenticated) + { + return string.Empty; + } + + var expClaim = user.FindFirst(c => c.Type.Equals("exp"))?.Value; + if (string.IsNullOrEmpty(expClaim)) + { + return string.Empty; + } + + var expTime = DateTimeOffset.FromUnixTimeSeconds( + Convert.ToInt64(expClaim)); + + var diff = expTime - DateTime.UtcNow; + + if (diff.TotalMinutes <= 2) + return await _authenticationClientService.Value.RefreshTokenAsync(); + + return string.Empty; + } + } +} diff --git a/TechHelper.Client/Pages/Exam/ExamCheck.razor b/TechHelper.Client/Pages/Exam/ExamCheck.razor new file mode 100644 index 0000000..92fd91e --- /dev/null +++ b/TechHelper.Client/Pages/Exam/ExamCheck.razor @@ -0,0 +1,223 @@ +@using Entities.DTO +@using TechHelper.Client.Exam +@page "/exam/check/{ExamID}" + + +试卷批改预览: @ExamDto.AssignmentTitle + + +@if (_isLoading) +{ + + 正在加载试卷和学生数据... +} +else if (_questionsForTable.Any() && _students.Any()) +{ + + + 序号 + 分值 + @foreach (var student in _students) + { + + @student.Name + + + + + } + + + @context.QuestionItem.Sequence + @context.QuestionItem.Score + @foreach (var student in _students) + { + + @if (context.StudentAnswers.ContainsKey(student.Id)) + { + + } + else + { + N/A + } + + } + + + + + + + + 学生总分预览: + @foreach (var student in _students) + { + + @student.Name: @GetStudentTotalScore(student.Id) + + } + + 提交批改结果 (模拟) + + + +} +else +{ + 无法加载试卷或题目信息。 + 返回试卷列表 +} + + +@code { + [Parameter] + public string ExamId { get; set; } // 从路由获取的试卷ID + + [Inject] + public IExamService ExamService { get; set; } // 注入试卷服务 + + [Inject] + private ISnackbar Snackbar { get; set; } // 注入 Snackbar 用于消息提示 + + [Inject] + private NavigationManager Navigation { get; set; } // 注入导航管理器 + + private MudTable _table = new(); // MudTable 实例引用 + private ExamDto ExamDto { get; set; } = new ExamDto(); // 原始试卷数据 + private ExamStruct _examStruct = new ExamStruct(); // 处理后的试卷结构,包含带序号的题目 + + private List _students = new List(); // 临时生成的学生列表 + private List _questionsForTable = new List(); // 用于 MudTable 的数据源 + + private bool _isLoading = true; // 加载状态 + + // 在组件初始化时加载数据 + protected override async Task OnInitializedAsync() + { + _isLoading = true; + await LoadExamData(); + GenerateTemporaryStudentsAndAnswers(); // 生成学生和初始作答数据 + _isLoading = false; + } + + // 加载试卷数据的方法 + private async Task LoadExamData() + { + if (Guid.TryParse(ExamId, out Guid parsedExamId)) + { + try + { + var result = await ExamService.GetExam(parsedExamId); + if (result.Status) + { + ExamDto = result.Result as ExamDto ?? new ExamDto(); + _examStruct = ExamDto.GetStruct(); // 将 ExamDto 转换为 ExamStruct + } + else + { + Snackbar?.Add($"获取试卷失败: {result.Message}", Severity.Error); + Navigation.NavigateTo("/exam/manager"); // 导航回管理页 + } + } + catch (Exception ex) + { + Console.Error.WriteLine($"获取试卷时发生错误: {ex.Message}"); + Snackbar?.Add($"获取试卷失败: {ex.Message}", Severity.Error); + Navigation.NavigateTo("/exam/manager"); + } + } + else + { + Console.Error.WriteLine($"错误:路由参数 ExamId '{ExamId}' 不是一个有效的 GUID 格式。"); + Snackbar?.Add("无效的试卷ID,无法加载。", Severity.Error); + Navigation.NavigateTo("/exam/manager"); + } + } + + // 生成临时学生和作答数据 + private void GenerateTemporaryStudentsAndAnswers() + { + _students = new List(); + // 生成 40 个学生 + for (int i = 1; i <= 40; i++) + { + _students.Add(new Student { Name = $"学生{i}" }); + } + + _questionsForTable = _examStruct.Questions.Select(qItem => + { + var rowData = new QuestionRowData + { + QuestionItem = qItem, + StudentAnswers = new Dictionary() + }; + + // 为每个学生随机生成初始的对错状态 + var random = new Random(); + foreach (var student in _students) + { + // 模拟随机对错,50%的概率 + rowData.StudentAnswers[student.Id] = random.Next(0, 2) == 1; + } + return rowData; + }).ToList(); + } + + // 当某个学生的某个题目的作答状态改变时触发 + private void OnAnswerChanged(string questionSequence, Guid studentId, bool isCorrect) + { + // 可以在这里添加额外的逻辑,例如记录更改 + Console.WriteLine($"题目 {questionSequence}, 学生 {studentId} 的答案变为: {isCorrect}"); + // 由于是 @bind-Checked,数据模型已经自动更新,这里只是日志 + } + + // 计算某个学生的总分 + private float GetStudentTotalScore(Guid studentId) + { + float totalScore = 0; + foreach (var row in _questionsForTable) + { + if (row.StudentAnswers.TryGetValue(studentId, out bool isCorrect) && isCorrect) + { + totalScore += row.QuestionItem.Score; + } + } + return totalScore; + } + + // 切换某个学生所有题目的对错状态 (用于快速批改) + private void ToggleStudentAllAnswers(Guid studentId) + { + bool allCorrect = _questionsForTable.All(row => row.StudentAnswers.ContainsKey(studentId) && row.StudentAnswers[studentId]); + + foreach (var row in _questionsForTable) + { + if (row.StudentAnswers.ContainsKey(studentId)) + { + row.StudentAnswers[studentId] = !allCorrect; // 全部取反 + } + } + StateHasChanged(); // 手动通知 Blazor 刷新 UI + } + + // 提交批改结果(模拟) + private void SubmitGrading() + { + Console.WriteLine("--- 提交批改结果 ---"); + foreach (var student in _students) + { + Console.WriteLine($"学生: {student.Name}, 总分: {GetStudentTotalScore(student.Id)}"); + foreach (var row in _questionsForTable) + { + if (row.StudentAnswers.TryGetValue(student.Id, out bool isCorrect)) + { + Console.WriteLine($" - 题目 {row.QuestionItem.Sequence}: {(isCorrect ? "正确" : "错误")}"); + } + } + } + Snackbar?.Add("批改结果已提交(模拟)", Severity.Success); + // 实际应用中,这里会将 _questionsForTable 和 _students 的数据发送到后端API + } +} \ No newline at end of file diff --git a/TechHelper.Client/Pages/Exam/ExamEdit.razor b/TechHelper.Client/Pages/Exam/ExamEdit.razor index ffc57c5..79bf546 100644 --- a/TechHelper.Client/Pages/Exam/ExamEdit.razor +++ b/TechHelper.Client/Pages/Exam/ExamEdit.razor @@ -1,15 +1,45 @@ -@page "/exam/edit/{ExamId:Guid}" +@page "/exam/edit/{ExamId}" +@using Entities.DTO @using TechHelper.Client.Exam + + @code { [Parameter] - public Guid ExamId { get; set; } + public string ExamId { get; set; } [Inject] public IExamService ExamService { get; set; } + [Inject] + private NavigationManager Navigation { get; set; } + + [Inject] + private ISnackbar Snackbar { get; set; } + + private ExamDto ExamDto { get; set; } + protected override async Task OnInitializedAsync() { - await ExamService.GetExam(Guid.NewGuid()); + + if (Guid.TryParse(ExamId, out Guid parsedExamId)) + { + Console.WriteLine($"ExamId 字符串成功解析为 Guid: {parsedExamId}"); + try + { + var result = await ExamService.GetExam(parsedExamId); + if (result.Status) ExamDto = result.Result as ExamDto ?? new ExamDto(); + } + catch (Exception ex) + { + Snackbar?.Add($"获取试卷失败: {ex.Message}", Severity.Error); + } + } + else + { + Console.Error.WriteLine($"错误:路由参数 ExamId '{ExamId}' 不是一个有效的 GUID 格式。无法获取试卷信息。"); + Navigation.NavigateTo("/exam/manager"); + Snackbar?.Add("无效的试卷ID,无法加载。", Severity.Error); + } } -} +} \ No newline at end of file diff --git a/TechHelper.Client/Pages/Exam/ExamGroupView.razor b/TechHelper.Client/Pages/Exam/ExamGroupView.razor index 074ca81..e6b2b65 100644 --- a/TechHelper.Client/Pages/Exam/ExamGroupView.razor +++ b/TechHelper.Client/Pages/Exam/ExamGroupView.razor @@ -38,7 +38,7 @@ @code { [Parameter] - public List MajorQGList { get; set; } + public List MajorQGList { get; set; } = new List(); [Parameter] public string Class { get; set; } = "my-2 pa-1"; diff --git a/TechHelper.Client/Pages/Exam/ExamManager.razor b/TechHelper.Client/Pages/Exam/ExamManager.razor index 0c49f95..6b16783 100644 --- a/TechHelper.Client/Pages/Exam/ExamManager.razor +++ b/TechHelper.Client/Pages/Exam/ExamManager.razor @@ -16,11 +16,12 @@ else } -@foreach (var item in examDtos) -{ - -} - + + @foreach (var item in examDtos) + { + + } + @code { [Inject] diff --git a/TechHelper.Client/Pages/Exam/ExamPreview.razor b/TechHelper.Client/Pages/Exam/ExamPreview.razor index bcbd632..e5b93b9 100644 --- a/TechHelper.Client/Pages/Exam/ExamPreview.razor +++ b/TechHelper.Client/Pages/Exam/ExamPreview.razor @@ -1,18 +1,25 @@ @using Entities.DTO - - - - @examDto.AssignmentTitle - + + - - @examDto.Description - - + @examDto.AssignmentTitle + + + + + + + 详情 + + + + + + + - @code { [Inject] @@ -23,15 +30,28 @@ [Parameter] - public string? Width { get; set; } = "200"; + public string? Width { get; set; } = "256"; [Parameter] - public string? Height { get; set; } = "400"; + public string? Height { get; set; } = "64"; + + + [Parameter] + public string? MaxWidth { get; set; } = "256"; + + + [Parameter] + public string? MaxHeight { get; set; } = "64"; private void ExamClick() { - navigationManager.NavigateTo($"exam/Edit/{examDto.AssignmentId}"); + navigationManager.NavigateTo($"exam/edit/{examDto.AssignmentId}"); + } + + private void CheckExam() + { + navigationManager.NavigateTo($"exam/check/{examDto.AssignmentId}"); } } diff --git a/TechHelper.Client/Pages/Exam/ExamPreview.razor.css b/TechHelper.Client/Pages/Exam/ExamPreview.razor.css new file mode 100644 index 0000000..ea89439 --- /dev/null +++ b/TechHelper.Client/Pages/Exam/ExamPreview.razor.css @@ -0,0 +1,16 @@ +.hover-effect-paper { + transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out; /* 添加平滑过渡效果 */ + cursor: pointer; +} + + .hover-effect-paper:hover { + transform: translateY(-5px) scale(1.02); /* 向上轻微移动并放大 */ + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); /* 增加阴影,使其看起来浮起 */ + /* 您还可以尝试改变背景色,例如: */ + /* background-color: var(--mud-palette-action-hover); */ + } + + +.test{ + background-color: gray; +} \ No newline at end of file diff --git a/TechHelper.Client/Pages/Exam/ExamView.razor b/TechHelper.Client/Pages/Exam/ExamView.razor index b9e5361..3d4e1a9 100644 --- a/TechHelper.Client/Pages/Exam/ExamView.razor +++ b/TechHelper.Client/Pages/Exam/ExamView.razor @@ -1,12 +1,25 @@ @using Entities.DTO @using TechHelper.Client.Exam - - @ParsedExam.AssignmentTitle - @ParsedExam.Description - - +@if (ParsedExam != null) +{ + + @ParsedExam.AssignmentTitle + @ParsedExam.Description + + + + +} +else +{ + + + + 加载试卷中... + +} @code { diff --git a/TechHelper.Client/Pages/Exam/QuestionCard.razor b/TechHelper.Client/Pages/Exam/QuestionCard.razor index 72c27bc..745340b 100644 --- a/TechHelper.Client/Pages/Exam/QuestionCard.razor +++ b/TechHelper.Client/Pages/Exam/QuestionCard.razor @@ -26,7 +26,7 @@ @code { [Parameter] - public SubQuestionDto Question { get; set; } + public SubQuestionDto Question { get; set; } = new SubQuestionDto(); [Parameter] public string Class { get; set; } diff --git a/TechHelper.Client/Program.cs b/TechHelper.Client/Program.cs index 8851624..5e6d4fe 100644 --- a/TechHelper.Client/Program.cs +++ b/TechHelper.Client/Program.cs @@ -14,6 +14,7 @@ using Microsoft.Extensions.Options; using Microsoft.AspNetCore.Components.WebAssembly.Authentication; using TechHelper.Client.AI; using TechHelper.Client.Exam; +using Microsoft.AspNetCore.Components; var builder = WebAssemblyHostBuilder.CreateDefault(args); @@ -27,36 +28,46 @@ builder.Services.AddOidcAuthentication(options => }); +builder.Services.Configure(builder.Configuration.GetSection("ApiConfiguration")); builder.Services.AddAuthorizationCore(); builder.Services.AddCascadingAuthenticationState(); builder.Services.AddLocalStorageServices(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.Configure(builder.Configuration.GetSection("ApiConfiguration")); -builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddTransient(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddHttpClient("WebApiClient", client => { var baseAddress = builder.Configuration.GetSection("ApiConfiguration:BaseAddress").Value; - client.BaseAddress = new Uri(baseAddress); + client.BaseAddress = new Uri(baseAddress+"/api/"); client.Timeout = TimeSpan.FromSeconds(30); client.DefaultRequestHeaders.Add("Accept", "application/json"); }).AddHttpMessageHandler(); builder.Services.AddScoped(sp => sp.GetRequiredService().CreateClient("WebApiClient")); -builder.Services.AddHttpClient("Default", (sp, cl) => +builder.Services.AddHttpClient("AuthClient", client => { - var apiConfiguration = sp.GetRequiredService>().Value; - cl.BaseAddress = new Uri(apiConfiguration.BaseAddress + "/api/"); -}) - .AddHttpMessageHandler(); + var baseAddress = builder.Configuration.GetSection("ApiConfiguration:BaseAddress").Value; + client.BaseAddress = new Uri(baseAddress); + client.Timeout = TimeSpan.FromSeconds(30); + client.DefaultRequestHeaders.Add("Accept", "application/json"); +}); + +//builder.Services.AddHttpClient("Default", (sp, cl) => +//{ +// var apiConfiguration = sp.GetRequiredService>().Value; +// cl.BaseAddress = new Uri(apiConfiguration.BaseAddress + "/api/"); +//}) +// .AddHttpMessageHandler(); -builder.Services.AddScoped(); //builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); //builder.Services.AddScoped(sp => sp.GetRequiredService().CreateClient("Default")); diff --git a/TechHelper.Client/Services/ClasssServices.cs b/TechHelper.Client/Services/ClasssServices.cs index 0e8fef8..63fd362 100644 --- a/TechHelper.Client/Services/ClasssServices.cs +++ b/TechHelper.Client/Services/ClasssServices.cs @@ -1,19 +1,15 @@ using Entities.DTO; -using System.Net; -using System.Net.Http.Json; -using System.Text.Json; +using System.Net.Http.Json; namespace TechHelper.Client.Services { public class ClasssServices : IClassServices { - private HttpClient _client; - private IHttpClientFactory _httpClientFactory; + private readonly HttpClient _client; - public ClasssServices(HttpClient client, IHttpClientFactory httpClientFactory) + public ClasssServices(HttpClient client) { _client = client; - _httpClientFactory = httpClientFactory; } public Task CreateClass(UserRegistrationToClassDto userClass) @@ -23,23 +19,24 @@ namespace TechHelper.Client.Services public async Task UserRegister(UserRegistrationToClassDto userRegistrationToClassDto) { - using (_client = _httpClientFactory.CreateClient("Default")) + try { + var result = await _client.PostAsJsonAsync("class/userRegiste", + userRegistrationToClassDto); + var data = await result.Content.ReadAsStringAsync(); - try + return new ResponseDto { - - var result = await _client.PostAsJsonAsync("class/userRegiste", - userRegistrationToClassDto); - var data = await result.Content.ReadAsStringAsync(); - return new ResponseDto { IsSuccessfulRegistration = result.IsSuccessStatusCode, Errors = new string[] { data } }; - } - catch (Exception ex) - { - return new ResponseDto { IsSuccessfulRegistration = false, Errors = new string[] { ex.Message } }; - } + IsSuccessfulRegistration = result.IsSuccessStatusCode, + Errors = result.IsSuccessStatusCode ? null : new string[] { data } + }; + } + catch (Exception ex) + { + // 实际应用中,这里应该加入日志记录 + Console.WriteLine($"Error in UserRegister: {ex.Message}"); + return new ResponseDto { IsSuccessfulRegistration = false, Errors = new string[] { ex.Message } }; } } - } -} +} \ No newline at end of file diff --git a/TechHelper.Server/Controllers/ExamController.cs b/TechHelper.Server/Controllers/ExamController.cs index ae6fee4..2410bcd 100644 --- a/TechHelper.Server/Controllers/ExamController.cs +++ b/TechHelper.Server/Controllers/ExamController.cs @@ -46,8 +46,10 @@ namespace TechHelper.Server.Controllers { var result = await _examService.GetAsync(id); - - return Ok(result); + if (result.Status) + return Ok(result.Result); + else + return BadRequest("查找失败"); }