添加项目文件。
This commit is contained in:
		
							
								
								
									
										30
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
**/.classpath
 | 
			
		||||
**/.dockerignore
 | 
			
		||||
**/.env
 | 
			
		||||
**/.git
 | 
			
		||||
**/.gitignore
 | 
			
		||||
**/.project
 | 
			
		||||
**/.settings
 | 
			
		||||
**/.toolstarget
 | 
			
		||||
**/.vs
 | 
			
		||||
**/.vscode
 | 
			
		||||
**/*.*proj.user
 | 
			
		||||
**/*.dbmdl
 | 
			
		||||
**/*.jfm
 | 
			
		||||
**/azds.yaml
 | 
			
		||||
**/bin
 | 
			
		||||
**/charts
 | 
			
		||||
**/docker-compose*
 | 
			
		||||
**/Dockerfile*
 | 
			
		||||
**/node_modules
 | 
			
		||||
**/npm-debug.log
 | 
			
		||||
**/obj
 | 
			
		||||
**/secrets.dev.yaml
 | 
			
		||||
**/values.dev.yaml
 | 
			
		||||
LICENSE
 | 
			
		||||
README.md
 | 
			
		||||
!**/.gitignore
 | 
			
		||||
!.git/HEAD
 | 
			
		||||
!.git/config
 | 
			
		||||
!.git/packed-refs
 | 
			
		||||
!.git/refs/heads/**
 | 
			
		||||
							
								
								
									
										12
									
								
								EmailLib/EmailConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								EmailLib/EmailConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
namespace TechHelper.Features
 | 
			
		||||
{
 | 
			
		||||
	public static class EmailConfiguration
 | 
			
		||||
	{
 | 
			
		||||
		public static string EmailFrom { get; set; } = "1468441589@qq.com";
 | 
			
		||||
		public static string Password { get; set; } = "pfxhtoztjimtbahc";
 | 
			
		||||
		public static string SubDescribe { get; set; } = "这是你的验证凭证";
 | 
			
		||||
		public static string SmtpHost { get; set; } = "smtp.qq.com";
 | 
			
		||||
		public static int SmtpPort { get; set; } = 587;
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								EmailLib/EmailLib.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								EmailLib/EmailLib.csproj
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
<Project Sdk="Microsoft.NET.Sdk">
 | 
			
		||||
 | 
			
		||||
  <PropertyGroup>
 | 
			
		||||
    <TargetFramework>net8.0</TargetFramework>
 | 
			
		||||
    <ImplicitUsings>enable</ImplicitUsings>
 | 
			
		||||
    <Nullable>enable</Nullable>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
    <PackageReference Include="MailKit" Version="4.12.1" />
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
 | 
			
		||||
</Project>
 | 
			
		||||
							
								
								
									
										109
									
								
								EmailLib/EmailTemap.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								EmailLib/EmailTemap.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,109 @@
 | 
			
		||||
namespace Email
 | 
			
		||||
{
 | 
			
		||||
	public class EmailTemap
 | 
			
		||||
	{
 | 
			
		||||
		public static string _email = @"
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset=""utf-8"" /> 
 | 
			
		||||
    <title>{{AppName}} - Verification Code</title>
 | 
			
		||||
    <style>
 | 
			
		||||
        /* 基本的样式重置和通用样式 */
 | 
			
		||||
        body {
 | 
			
		||||
            font-family: -apple-system, BlinkMacSystemFont, ""Segoe UI"", Roboto, Helvetica, Arial, sans-serif, ""Apple Color Emoji"", ""Segoe UI Emoji"", ""Segoe UI Symbol"";
 | 
			
		||||
            line-height: 1.6;
 | 
			
		||||
            color: #333;
 | 
			
		||||
            margin: 0;
 | 
			
		||||
            padding: 20px;
 | 
			
		||||
            background-color: #f4f4f4; /* 轻微的背景色 */
 | 
			
		||||
        }
 | 
			
		||||
        .container {
 | 
			
		||||
            max-width: 600px; /* 内容最大宽度 */
 | 
			
		||||
            margin: 20px auto; /* 居中 */
 | 
			
		||||
            background-color: #fff; /* 白色背景 */
 | 
			
		||||
            padding: 30px; /* 内边距 */
 | 
			
		||||
            border-radius: 8px; /* 圆角 */
 | 
			
		||||
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); /* 轻微阴影 */
 | 
			
		||||
        }
 | 
			
		||||
        .header {
 | 
			
		||||
            text-align: center; /* 头部居中 */
 | 
			
		||||
            margin-bottom: 30px; /* 底部外边距 */
 | 
			
		||||
            border-bottom: 1px solid #eee; /* 底部细线 */
 | 
			
		||||
            padding-bottom: 20px; /* 底部内边距 */
 | 
			
		||||
        }
 | 
			
		||||
        .header h1 { /* 修正了 h1 和 { 之间的空格 */
 | 
			
		||||
            color: #007bff; /* 主题色,例如蓝色 */
 | 
			
		||||
            font-size: 24px;
 | 
			
		||||
            margin: 0;
 | 
			
		||||
        }
 | 
			
		||||
        .code-box {
 | 
			
		||||
            text-align: center; /* 验证码框内容居中 */
 | 
			
		||||
            margin: 30px 0; /* 上下外边距 */
 | 
			
		||||
            padding: 20px; /* 内边距 */
 | 
			
		||||
            background-color: #e9e9e9; /* 浅灰色背景 */
 | 
			
		||||
            border: 1px dashed #ccc; /* 虚线边框 */
 | 
			
		||||
            border-radius: 4px; /* 圆角 */
 | 
			
		||||
            overflow: auto; /* 防止代码过长溢出 */
 | 
			
		||||
        }
 | 
			
		||||
        .verification-code {
 | 
			
		||||
            font-size: 28px; /* 验证码字体大小 */
 | 
			
		||||
            font-weight: bold; /* 加粗 */
 | 
			
		||||
            color: #333; /* 深灰色 */
 | 
			
		||||
            letter-spacing: 5px; /* 字母间距 */
 | 
			
		||||
            word-break: break-all; /* 防止长代码换行问题 */
 | 
			
		||||
            display: block; /* 独占一行 */
 | 
			
		||||
        }
 | 
			
		||||
        .instructions {
 | 
			
		||||
            margin-bottom: 20px; /* 底部外边距 */
 | 
			
		||||
        }
 | 
			
		||||
        .warning {
 | 
			
		||||
            color: #dc3545; /* 警告色,例如红色 */
 | 
			
		||||
            font-weight: bold; /* 加粗 */
 | 
			
		||||
            margin-bottom: 20px; /* 底部外边距 */
 | 
			
		||||
        }
 | 
			
		||||
        .footer {
 | 
			
		||||
            text-align: center; /* 底部居中 */
 | 
			
		||||
            margin-top: 30px; /* 顶部外边距 */
 | 
			
		||||
            font-size: 12px; /* 字体大小 */
 | 
			
		||||
            color: #888; /* 灰色 */
 | 
			
		||||
            border-top: 1px solid #eee; /* 顶部细线 */
 | 
			
		||||
            padding-top: 20px; /* 顶部内边距 */
 | 
			
		||||
        }
 | 
			
		||||
        .footer p { /* 修正了 p 和 { 之间的空格 */
 | 
			
		||||
            margin: 5px 0;
 | 
			
		||||
        }
 | 
			
		||||
    </style>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
    <div class=""container""> 
 | 
			
		||||
        <div class=""header""> 
 | 
			
		||||
            <h1>您的验证码</h1>
 | 
			
		||||
        </div>
 | 
			
		||||
        <p>您好,</p>
 | 
			
		||||
        <div class=""instructions"">
 | 
			
		||||
            <p>您收到这封邮件是因为您请求验证您在 <strong>{{AppName}}</strong> 的邮箱地址。</p>
 | 
			
		||||
            <p>请使用以下验证码完成操作:</p>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class=""code-box"">
 | 
			
		||||
            <span class=""verification-code"">{{VerificationCode}}</span> 
 | 
			
		||||
        </div>
 | 
			
		||||
        <p class=""instructions"">请在应用程序或网站中输入此验证码。</p>
 | 
			
		||||
        <p class=""warning"">为了您的账户安全,请勿将此验证码透露给任何人。</p>
 | 
			
		||||
        <p class=""instructions"">此验证码在 {{ExpirationMinutes}} 分钟内有效。</p>
 | 
			
		||||
        <p>如果您没有进行此操作,请忽略本邮件。</p>
 | 
			
		||||
        <div class=""footer""> 
 | 
			
		||||
            <p>这是一封自动发送的邮件,请勿直接回复。</p>
 | 
			
		||||
            <p>© {{AppName}}</p>
 | 
			
		||||
            <p>支持: <a href=""mailto:{{SupportEmail}}"">Tech Helper</a></p>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</body>
 | 
			
		||||
</html>"; 
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								EmailLib/IEmailSender.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								EmailLib/IEmailSender.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
namespace TechHelper.Features
 | 
			
		||||
{
 | 
			
		||||
	public interface IEmailSender
 | 
			
		||||
	{
 | 
			
		||||
		/// <summary>
 | 
			
		||||
		/// 使用 MailKit 通过邮箱发送邮件
 | 
			
		||||
		/// </summary>
 | 
			
		||||
		/// <param name="toEmail">收件人邮箱地址</param>
 | 
			
		||||
		/// <param name="subject">邮件主题</param>
 | 
			
		||||
		/// <param name="body">邮件正文 (支持 HTML)</param>
 | 
			
		||||
		/// <returns></returns>
 | 
			
		||||
		public Task SendEmailAsync(string toEmail,string subject, string body);
 | 
			
		||||
 | 
			
		||||
		public Task SendEmailAsync(string toEmail, string body);
 | 
			
		||||
 | 
			
		||||
		public Task SendEmailAuthcodeAsync(string toEmail, string authCode);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										79
									
								
								EmailLib/QEmailSender.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								EmailLib/QEmailSender.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,79 @@
 | 
			
		||||
using MailKit.Net.Smtp;
 | 
			
		||||
using MailKit.Security;
 | 
			
		||||
using MimeKit;
 | 
			
		||||
using Email;
 | 
			
		||||
using System.Text;
 | 
			
		||||
 | 
			
		||||
namespace TechHelper.Features
 | 
			
		||||
{
 | 
			
		||||
	public class QEmailSender : IEmailSender
 | 
			
		||||
	{
 | 
			
		||||
		public async Task SendEmailAsync(string toEmail, string subject, string body)
 | 
			
		||||
		{
 | 
			
		||||
			var secureSocketOption = SecureSocketOptions.StartTls;
 | 
			
		||||
			try
 | 
			
		||||
			{
 | 
			
		||||
				var message = new MimeMessage();
 | 
			
		||||
				message.From.Add(new MailboxAddress("TechHelper", EmailConfiguration.EmailFrom));
 | 
			
		||||
				message.To.Add(new MailboxAddress("", toEmail));
 | 
			
		||||
				message.Subject = subject;
 | 
			
		||||
				message.Body = new TextPart(MimeKit.Text.TextFormat.Html)
 | 
			
		||||
				{
 | 
			
		||||
					Text = body
 | 
			
		||||
				};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
				using (var client = new SmtpClient())
 | 
			
		||||
				{
 | 
			
		||||
					await client.ConnectAsync(EmailConfiguration.SmtpHost, EmailConfiguration.SmtpPort, secureSocketOption);
 | 
			
		||||
 | 
			
		||||
					// 移除 XOAUTH2 认证机制,避免某些环境下出现异常
 | 
			
		||||
					// 如果不加这行,某些情况下可能会遇到 MailKit.Security.AuthenticationException: XOAUTH2 is not supported
 | 
			
		||||
					client.AuthenticationMechanisms.Remove("XOAUTH2");
 | 
			
		||||
 | 
			
		||||
					await client.AuthenticateAsync(EmailConfiguration.EmailFrom, EmailConfiguration.Password);
 | 
			
		||||
					await client.SendAsync(message);
 | 
			
		||||
					await client.DisconnectAsync(true);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				Console.WriteLine("邮件发送成功 (QQ 邮箱)!");
 | 
			
		||||
			}
 | 
			
		||||
			catch (TypeInitializationException ex)
 | 
			
		||||
			{
 | 
			
		||||
				Console.WriteLine("捕获到 SmtpClient 的 TypeInitializationException!");
 | 
			
		||||
				Console.WriteLine($"异常消息: {ex.Message}");
 | 
			
		||||
 | 
			
		||||
				Exception? inner = ex.InnerException; // 获取第一层内部异常
 | 
			
		||||
				int innerCount = 1;
 | 
			
		||||
				while (inner != null)
 | 
			
		||||
				{
 | 
			
		||||
					Console.WriteLine($"--> 内部异常 {innerCount}: {inner.GetType().Name}");
 | 
			
		||||
					Console.WriteLine($"--> 内部异常消息: {inner.Message}");
 | 
			
		||||
					inner = inner.InnerException; // 获取下一层内部异常
 | 
			
		||||
					innerCount++;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			catch (Exception ex)
 | 
			
		||||
			{
 | 
			
		||||
				Console.WriteLine($"捕获到其他类型的异常: {ex.GetType().Name}");
 | 
			
		||||
				Console.WriteLine($"异常消息: {ex.Message}");
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public async Task SendEmailAsync(string toEmail , string body)
 | 
			
		||||
		{
 | 
			
		||||
			await SendEmailAsync(toEmail, EmailConfiguration.SubDescribe, body);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public async Task SendEmailAuthcodeAsync(string toEmail, string authCode)
 | 
			
		||||
		{
 | 
			
		||||
			string htmlTemplateString = EmailTemap._email;
 | 
			
		||||
			string htmlBody = htmlTemplateString
 | 
			
		||||
								.Replace("{{AppName}}", "TechHelper")
 | 
			
		||||
								.Replace("{{VerificationCode}}", authCode)
 | 
			
		||||
								.Replace("{{ExpirationMinutes}}", "30")
 | 
			
		||||
								.Replace("{{SupportEmail}}", "TechHelper");
 | 
			
		||||
			await SendEmailAsync(toEmail, htmlBody);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								Entities/Configuration/ApiConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								Entities/Configuration/ApiConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
namespace Entities.Configuration
 | 
			
		||||
{
 | 
			
		||||
    public class ApiConfiguration
 | 
			
		||||
	{
 | 
			
		||||
		public string? BaseAddress { get; set; } /*= "http://localhost:5099";*/
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								Entities/Configuration/JwtConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								Entities/Configuration/JwtConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
namespace Entities.Configuration
 | 
			
		||||
{
 | 
			
		||||
    public class JwtConfiguration
 | 
			
		||||
	{
 | 
			
		||||
		public string? SecurityKey { get; set; }
 | 
			
		||||
		public string? ValidIssuer { get; set; }
 | 
			
		||||
		public string? ValidAudience { get; set; }
 | 
			
		||||
		public int ExpiryInMinutes { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										57
									
								
								Entities/Context/IPagedList.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								Entities/Context/IPagedList.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
			
		||||
namespace SharedDATA.Context
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    using System.Collections.Generic;
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Provides the interface(s) for paged list of any type.
 | 
			
		||||
    /// 为任何类型的分页列表提供接口
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <typeparam name="T">The type for paging.分页的类型</typeparam>
 | 
			
		||||
    public interface IPagedList<T>
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the index start value.
 | 
			
		||||
        /// 获取索引起始值
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The index start value.</value>
 | 
			
		||||
        int IndexFrom { get; }
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the page index (current).
 | 
			
		||||
        /// 获取页索引(当前)
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        int PageIndex { get; }
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the page size.
 | 
			
		||||
        /// 获取页面大小
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        int PageSize { get; }
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the total count of the list of type <typeparamref name="T"/>
 | 
			
		||||
        /// 获取类型列表的总计数
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        int TotalCount { get; }
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the total pages.
 | 
			
		||||
        /// 获取页面总数
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        int TotalPages { get; }
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the current page items.
 | 
			
		||||
        /// 获取当前页项
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        IList<T> Items { get; }
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the has previous page.
 | 
			
		||||
        /// 获取前一页
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The has previous page.</value>
 | 
			
		||||
        bool HasPreviousPage { get; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the has next page.
 | 
			
		||||
        /// 获取下一页
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The has next page.</value>
 | 
			
		||||
        bool HasNextPage { get; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										238
									
								
								Entities/Context/PagedList.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								Entities/Context/PagedList.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,238 @@
 | 
			
		||||
namespace SharedDATA.Context
 | 
			
		||||
{
 | 
			
		||||
    using System;
 | 
			
		||||
    using System.Collections.Generic;
 | 
			
		||||
    using System.Linq;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Represents the default implementation of the <see cref="IPagedList{T}"/> interface.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <typeparam name="T">The type of the data to page 页类型的数据</typeparam>
 | 
			
		||||
    public class PagedList<T> : IPagedList<T>
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the index of the page.
 | 
			
		||||
        /// 获得页的起始页
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The index of the page.</value>
 | 
			
		||||
        public int PageIndex { get; set; }
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the size of the page.
 | 
			
		||||
        /// 获得页大小
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The size of the page.</value>
 | 
			
		||||
        public int PageSize { get; set; }
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the total count.
 | 
			
		||||
        /// 获得总数
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The total count.</value>
 | 
			
		||||
        public int TotalCount { get; set; }
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the total pages.
 | 
			
		||||
        /// 获得总页数
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The total pages.</value>
 | 
			
		||||
        public int TotalPages { get; set; }
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the index from.
 | 
			
		||||
        /// 从索引起
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The index from.</value>
 | 
			
		||||
        public int IndexFrom { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets the items.
 | 
			
		||||
        /// 数据
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The items.</value>
 | 
			
		||||
        public IList<T> Items { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the has previous page.
 | 
			
		||||
        /// 获取前一页
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The has previous page.</value>
 | 
			
		||||
        public bool HasPreviousPage => PageIndex - IndexFrom > 0;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the has next page.
 | 
			
		||||
        /// 获取下一页
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The has next page.</value>
 | 
			
		||||
        public bool HasNextPage => PageIndex - IndexFrom + 1 < TotalPages;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="PagedList{T}" /> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="source">The source.</param>
 | 
			
		||||
        /// <param name="pageIndex">The index of the page.</param>
 | 
			
		||||
        /// <param name="pageSize">The size of the page.</param>
 | 
			
		||||
        /// <param name="indexFrom">The index from.</param>
 | 
			
		||||
        public PagedList(IEnumerable<T> source, int pageIndex, int pageSize, int indexFrom)
 | 
			
		||||
        {
 | 
			
		||||
            if (indexFrom > pageIndex)
 | 
			
		||||
            {
 | 
			
		||||
                throw new ArgumentException($"indexFrom: {indexFrom} > pageIndex: {pageIndex}, must indexFrom <= pageIndex");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (source is IQueryable<T> querable)
 | 
			
		||||
            {
 | 
			
		||||
                PageIndex = pageIndex;
 | 
			
		||||
                PageSize = pageSize;
 | 
			
		||||
                IndexFrom = indexFrom;
 | 
			
		||||
                TotalCount = querable.Count();
 | 
			
		||||
                TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize);
 | 
			
		||||
 | 
			
		||||
                Items = querable.Skip((PageIndex - IndexFrom) * PageSize).Take(PageSize).ToList();
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                PageIndex = pageIndex;
 | 
			
		||||
                PageSize = pageSize;
 | 
			
		||||
                IndexFrom = indexFrom;
 | 
			
		||||
                TotalCount = source.Count();
 | 
			
		||||
                TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize);
 | 
			
		||||
 | 
			
		||||
                Items = source.Skip((PageIndex - IndexFrom) * PageSize).Take(PageSize).ToList();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="PagedList{T}" /> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public PagedList() => Items = new T[0];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Provides the implementation of the <see cref="IPagedList{T}"/> and converter.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <typeparam name="TSource">The type of the source.</typeparam>
 | 
			
		||||
    /// <typeparam name="TResult">The type of the result.</typeparam>
 | 
			
		||||
    public class PagedList<TSource, TResult> : IPagedList<TResult>
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the index of the page.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The index of the page.</value>
 | 
			
		||||
        public int PageIndex { get; }
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the size of the page.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The size of the page.</value>
 | 
			
		||||
        public int PageSize { get; }
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the total count.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The total count.</value>
 | 
			
		||||
        public int TotalCount { get; }
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the total pages.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The total pages.</value>
 | 
			
		||||
        public int TotalPages { get; }
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the index from.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The index from.</value>
 | 
			
		||||
        public int IndexFrom { get; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the items.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The items.</value>
 | 
			
		||||
        public IList<TResult> Items { get; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the has previous page.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The has previous page.</value>
 | 
			
		||||
        public bool HasPreviousPage => PageIndex - IndexFrom > 0;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the has next page.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <value>The has next page.</value>
 | 
			
		||||
        public bool HasNextPage => PageIndex - IndexFrom + 1 < TotalPages;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="PagedList{TSource, TResult}" /> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="source">The source.</param>
 | 
			
		||||
        /// <param name="converter">The converter.</param>
 | 
			
		||||
        /// <param name="pageIndex">The index of the page.</param>
 | 
			
		||||
        /// <param name="pageSize">The size of the page.</param>
 | 
			
		||||
        /// <param name="indexFrom">The index from.</param>
 | 
			
		||||
        public PagedList(IEnumerable<TSource> source, Func<IEnumerable<TSource>, IEnumerable<TResult>> converter, int pageIndex, int pageSize, int indexFrom)
 | 
			
		||||
        {
 | 
			
		||||
            if (indexFrom > pageIndex)
 | 
			
		||||
            {
 | 
			
		||||
                throw new ArgumentException($"indexFrom: {indexFrom} > pageIndex: {pageIndex}, must indexFrom <= pageIndex");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (source is IQueryable<TSource> querable)
 | 
			
		||||
            {
 | 
			
		||||
                PageIndex = pageIndex;
 | 
			
		||||
                PageSize = pageSize;
 | 
			
		||||
                IndexFrom = indexFrom;
 | 
			
		||||
                TotalCount = querable.Count();
 | 
			
		||||
                TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize);
 | 
			
		||||
 | 
			
		||||
                var items = querable.Skip((PageIndex - IndexFrom) * PageSize).Take(PageSize).ToArray();
 | 
			
		||||
 | 
			
		||||
                Items = new List<TResult>(converter(items));
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                PageIndex = pageIndex;
 | 
			
		||||
                PageSize = pageSize;
 | 
			
		||||
                IndexFrom = indexFrom;
 | 
			
		||||
                TotalCount = source.Count();
 | 
			
		||||
                TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize);
 | 
			
		||||
 | 
			
		||||
                var items = source.Skip((PageIndex - IndexFrom) * PageSize).Take(PageSize).ToArray();
 | 
			
		||||
 | 
			
		||||
                Items = new List<TResult>(converter(items));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="PagedList{TSource, TResult}" /> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="source">The source.</param>
 | 
			
		||||
        /// <param name="converter">The converter.</param>
 | 
			
		||||
        public PagedList(IPagedList<TSource> source, Func<IEnumerable<TSource>, IEnumerable<TResult>> converter)
 | 
			
		||||
        {
 | 
			
		||||
            PageIndex = source.PageIndex;
 | 
			
		||||
            PageSize = source.PageSize;
 | 
			
		||||
            IndexFrom = source.IndexFrom;
 | 
			
		||||
            TotalCount = source.TotalCount;
 | 
			
		||||
            TotalPages = source.TotalPages;
 | 
			
		||||
 | 
			
		||||
            Items = new List<TResult>(converter(source.Items));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Provides some help methods for <see cref="IPagedList{T}"/> interface.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static class PagedList
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Creates an empty of <see cref="IPagedList{T}"/>.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <typeparam name="T">The type for paging </typeparam>
 | 
			
		||||
        /// <returns>An empty instance of <see cref="IPagedList{T}"/>.</returns>
 | 
			
		||||
        public static IPagedList<T> Empty<T>() => new PagedList<T>();
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Creates a new instance of <see cref="IPagedList{TResult}"/> from source of <see cref="IPagedList{TSource}"/> instance.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <typeparam name="TResult">The type of the result.</typeparam>
 | 
			
		||||
        /// <typeparam name="TSource">The type of the source.</typeparam>
 | 
			
		||||
        /// <param name="source">The source.</param>
 | 
			
		||||
        /// <param name="converter">The converter.</param>
 | 
			
		||||
        /// <returns>An instance of <see cref="IPagedList{TResult}"/>.</returns>
 | 
			
		||||
        public static IPagedList<TResult> From<TResult, TSource>(IPagedList<TSource> source, Func<IEnumerable<TSource>, IEnumerable<TResult>> converter) => new PagedList<TSource, TResult>(source, converter);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,39 @@
 | 
			
		||||
namespace SharedDATA.Api
 | 
			
		||||
{
 | 
			
		||||
	using SharedDATA.Context;
 | 
			
		||||
	using System;
 | 
			
		||||
    using System.Collections.Generic;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Provides some extension methods for <see cref="IEnumerable{T}"/> to provide paging capability.
 | 
			
		||||
    /// 提供一些扩展方法来提供分页功能
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static class IEnumerablePagedListExtensions
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Converts the specified source to <see cref="IPagedList{T}"/> by the specified <paramref name="pageIndex"/> and <paramref name="pageSize"/>.
 | 
			
		||||
        /// 通过起始页和页大小把数据转换成页集合
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <typeparam name="T">The type of the source.源的类型</typeparam>
 | 
			
		||||
        /// <param name="source">The source to paging.分页的源</param>
 | 
			
		||||
        /// <param name="pageIndex">The index of the page.起始页</param>
 | 
			
		||||
        /// <param name="pageSize">The size of the page.页大小</param>
 | 
			
		||||
        /// <param name="indexFrom">The start index value.开始索引值</param>
 | 
			
		||||
        /// <returns>An instance of the inherited from <see cref="IPagedList{T}"/> interface.接口继承的实例</returns>
 | 
			
		||||
        public static IPagedList<T> ToPagedList<T>(this IEnumerable<T> source, int pageIndex, int pageSize, int indexFrom = 0) => new PagedList<T>(source, pageIndex, pageSize, indexFrom);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Converts the specified source to <see cref="IPagedList{T}"/> by the specified <paramref name="converter"/>, <paramref name="pageIndex"/> and <paramref name="pageSize"/>
 | 
			
		||||
        /// 通过转换器,起始页和页大小把数据转换成页集合
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <typeparam name="TSource">The type of the source.源的类型</typeparam>
 | 
			
		||||
        /// <typeparam name="TResult">The type of the result.反馈的类型</typeparam>
 | 
			
		||||
        /// <param name="source">The source to convert.要转换的源</param>
 | 
			
		||||
        /// <param name="converter">The converter to change the <typeparamref name="TSource"/> to <typeparamref name="TResult"/>.转换器来改变源到反馈</param>
 | 
			
		||||
        /// <param name="pageIndex">The page index.起始页</param>
 | 
			
		||||
        /// <param name="pageSize">The page size.页大小</param>
 | 
			
		||||
        /// <param name="indexFrom">The start index value.开始索引值</param>
 | 
			
		||||
        /// <returns>An instance of the inherited from <see cref="IPagedList{T}"/> interface.接口继承的实例</returns>
 | 
			
		||||
        public static IPagedList<TResult> ToPagedList<TSource, TResult>(this IEnumerable<TSource> source, Func<IEnumerable<TSource>, IEnumerable<TResult>> converter, int pageIndex, int pageSize, int indexFrom = 0) => new PagedList<TSource, TResult>(source, converter, pageIndex, pageSize, indexFrom);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										52
									
								
								Entities/Context/UnitOfWork/IQueryablePageListExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								Entities/Context/UnitOfWork/IQueryablePageListExtensions.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
namespace SharedDATA.Api
 | 
			
		||||
{
 | 
			
		||||
    using System;
 | 
			
		||||
    using System.Linq;
 | 
			
		||||
    using System.Threading;
 | 
			
		||||
    using System.Threading.Tasks;
 | 
			
		||||
    using Microsoft.EntityFrameworkCore;
 | 
			
		||||
    using SharedDATA.Context;
 | 
			
		||||
 | 
			
		||||
    public static class IQueryablePageListExtensions
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Converts the specified source to <see cref="IPagedList{T}"/> by the specified <paramref name="pageIndex"/> and <paramref name="pageSize"/>.
 | 
			
		||||
        /// 根据起始页和页大小转换成源
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <typeparam name="T">The type of the source.源的类型</typeparam>
 | 
			
		||||
        /// <param name="source">The source to paging.分页的源</param>
 | 
			
		||||
        /// <param name="pageIndex">The index of the page.起始页</param>
 | 
			
		||||
        /// <param name="pageSize">The size of the page.页大小</param>
 | 
			
		||||
        /// <param name="cancellationToken">
 | 
			
		||||
        ///     A <see cref="CancellationToken" /> to observe while waiting for the task to complete.
 | 
			
		||||
        ///     在等待任务完成时观察
 | 
			
		||||
        /// </param>
 | 
			
		||||
        /// <param name="indexFrom">The start index value.值的起始索引</param>
 | 
			
		||||
        /// <returns>An instance of the inherited from <see cref="IPagedList{T}"/> interface.接口继承的实例</returns>
 | 
			
		||||
        public static async Task<IPagedList<T>> ToPagedListAsync<T>(this IQueryable<T> source, int pageIndex, int pageSize, int indexFrom = 0, CancellationToken cancellationToken = default(CancellationToken))
 | 
			
		||||
        {
 | 
			
		||||
            //如果索引比起始页大,则异常
 | 
			
		||||
            if (indexFrom > pageIndex)
 | 
			
		||||
            {
 | 
			
		||||
                throw new ArgumentException($"indexFrom: {indexFrom} > pageIndex: {pageIndex}, must indexFrom <= pageIndex");
 | 
			
		||||
            }
 | 
			
		||||
            //数据源大小
 | 
			
		||||
            var count = await source.CountAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            var items = await source.Skip((pageIndex - indexFrom) * pageSize)
 | 
			
		||||
                                    .Take(pageSize).ToListAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            var pagedList = new PagedList<T>()
 | 
			
		||||
            {
 | 
			
		||||
                PageIndex = pageIndex,
 | 
			
		||||
                PageSize = pageSize,
 | 
			
		||||
                IndexFrom = indexFrom,
 | 
			
		||||
                TotalCount = count,
 | 
			
		||||
                Items = items,
 | 
			
		||||
                TotalPages = (int)Math.Ceiling(count / (double)pageSize)
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            return pagedList;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										463
									
								
								Entities/Context/UnitOfWork/IRepository.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										463
									
								
								Entities/Context/UnitOfWork/IRepository.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,463 @@
 | 
			
		||||
namespace SharedDATA.Api
 | 
			
		||||
{
 | 
			
		||||
    using System;
 | 
			
		||||
    using System.Collections.Generic;
 | 
			
		||||
    using System.Linq;
 | 
			
		||||
    using System.Linq.Expressions;
 | 
			
		||||
    using System.Threading;
 | 
			
		||||
    using System.Threading.Tasks;
 | 
			
		||||
    using Microsoft.EntityFrameworkCore.Query;
 | 
			
		||||
    using Microsoft.EntityFrameworkCore.ChangeTracking;
 | 
			
		||||
    using Microsoft.EntityFrameworkCore;
 | 
			
		||||
    using SharedDATA.Context;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Defines the interfaces for generic repository.
 | 
			
		||||
    /// 为通用存储库定义接口
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <typeparam name="TEntity">The type of the entity.实体类型</typeparam>
 | 
			
		||||
    public interface IRepository<TEntity> where TEntity : class
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Changes the table name. This require the tables in the same database.
 | 
			
		||||
        /// 更改表名。这需要相同数据库中的表
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="table"></param>
 | 
			
		||||
        /// <remarks>
 | 
			
		||||
        /// This only been used for supporting multiple tables in the same model. This require the tables in the same database.
 | 
			
		||||
        /// 这只用于支持同一个模型中的多个表。这需要相同数据库中的表。
 | 
			
		||||
        /// </remarks>
 | 
			
		||||
        void ChangeTable(string table);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the <see cref="IPagedList{TEntity}"/> based on a predicate, orderby delegate and page information. This method default no-tracking query.
 | 
			
		||||
        /// 基于谓词、orderby委托和页面信息获取<see cref="IPagedList{TEntity}"/>。此方法默认无跟踪查询。
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate">A function to test each element for a condition.用于测试条件的每个元素的函数</param>
 | 
			
		||||
        /// <param name="orderBy">A function to order elements.对元素进行排序的函数</param>
 | 
			
		||||
        /// <param name="include">A function to include navigation properties 包含导航属性的函数</param>
 | 
			
		||||
        /// <param name="pageIndex">The index of page.起始页</param>
 | 
			
		||||
        /// <param name="pageSize">The size of the page.页大小</param>
 | 
			
		||||
        /// <param name="disableTracking"><c>True</c> to disable changing tracking; otherwise, 禁用更改跟踪<c>false</c>. Default to <c>true</c>.</param>
 | 
			
		||||
        /// <param name="ignoreQueryFilters">Ignore query filters 忽略查询过滤器</param>
 | 
			
		||||
        /// <returns>An <see cref="IPagedList{TEntity}"/> that contains elements that satisfy the condition specified by 包含满足指定条件的元素<paramref name="predicate"/>.</returns>
 | 
			
		||||
        /// <remarks>This method default no-tracking query.</remarks>
 | 
			
		||||
        IPagedList<TEntity> GetPagedList(Expression<Func<TEntity, bool>> predicate = null,
 | 
			
		||||
                                         Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
 | 
			
		||||
                                         Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
 | 
			
		||||
                                         int pageIndex = 0,
 | 
			
		||||
                                         int pageSize = 20,
 | 
			
		||||
                                         bool disableTracking = true,
 | 
			
		||||
                                         bool ignoreQueryFilters = false);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the <see cref="IPagedList{TEntity}"/> based on a predicate, orderby delegate and page information. This method default no-tracking query.
 | 
			
		||||
        /// 基于谓词、orderby委托和页面信息获取<see cref="IPagedList{TEntity}"/>。此方法默认无跟踪查询。
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate">A function to test each element for a condition.用于测试条件的每个元素的函数</param>
 | 
			
		||||
        /// <param name="orderBy">A function to order elements.对元素进行排序的函数</param>
 | 
			
		||||
        /// <param name="include">A function to include navigation properties 包含导航属性的函数</param>
 | 
			
		||||
        /// <param name="pageIndex">The index of page.起始页</param>
 | 
			
		||||
        /// <param name="pageSize">The size of the page.页大小</param>
 | 
			
		||||
        /// <param name="disableTracking"><c>True</c> to disable changing tracking;禁用更改跟踪; otherwise, <c>false</c>. Default to <c>true</c>.</param>
 | 
			
		||||
        /// <param name="cancellationToken">
 | 
			
		||||
        ///     A <see cref="CancellationToken" /> to observe while waiting for the task to complete.
 | 
			
		||||
        /// </param>
 | 
			
		||||
        /// <param name="ignoreQueryFilters">Ignore query filters 忽略查询过滤器</param>
 | 
			
		||||
        /// <returns>An <see cref="IPagedList{TEntity}"/> that contains elements that satisfy the condition specified by <paramref name="predicate"/>.</returns>
 | 
			
		||||
        /// <remarks>This method default no-tracking query.此方法默认无跟踪查询</remarks>
 | 
			
		||||
        Task<IPagedList<TEntity>> GetPagedListAsync(Expression<Func<TEntity, bool>> predicate = null,
 | 
			
		||||
                                                    Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
 | 
			
		||||
                                                    Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
 | 
			
		||||
                                                    int pageIndex = 0,
 | 
			
		||||
                                                    int pageSize = 20,
 | 
			
		||||
                                                    bool disableTracking = true,
 | 
			
		||||
                                                    CancellationToken cancellationToken = default(CancellationToken),
 | 
			
		||||
                                                    bool ignoreQueryFilters = false);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the <see cref="IPagedList{TResult}"/> based on a predicate, orderby delegate and page information. This method default no-tracking query.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="selector">The selector for projection.</param>
 | 
			
		||||
        /// <param name="predicate">A function to test each element for a condition.</param>
 | 
			
		||||
        /// <param name="orderBy">A function to order elements.</param>
 | 
			
		||||
        /// <param name="include">A function to include navigation properties</param>
 | 
			
		||||
        /// <param name="pageIndex">The index of page.</param>
 | 
			
		||||
        /// <param name="pageSize">The size of the page.</param>
 | 
			
		||||
        /// <param name="disableTracking"><c>True</c> to disable changing tracking; otherwise, <c>false</c>. Default to <c>true</c>.</param>
 | 
			
		||||
        /// <param name="ignoreQueryFilters">Ignore query filters</param>
 | 
			
		||||
        /// <returns>An <see cref="IPagedList{TResult}"/> that contains elements that satisfy the condition specified by <paramref name="predicate"/>.</returns>
 | 
			
		||||
        /// <remarks>This method default no-tracking query.</remarks>
 | 
			
		||||
        IPagedList<TResult> GetPagedList<TResult>(Expression<Func<TEntity, TResult>> selector,
 | 
			
		||||
                                                  Expression<Func<TEntity, bool>> predicate = null,
 | 
			
		||||
                                                  Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
 | 
			
		||||
                                                  Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
 | 
			
		||||
                                                  int pageIndex = 0,
 | 
			
		||||
                                                  int pageSize = 20,
 | 
			
		||||
                                                  bool disableTracking = true,
 | 
			
		||||
                                                  bool ignoreQueryFilters = false) where TResult : class;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the <see cref="IPagedList{TEntity}"/> based on a predicate, orderby delegate and page information. This method default no-tracking query.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="selector">The selector for projection.</param>
 | 
			
		||||
        /// <param name="predicate">A function to test each element for a condition.</param>
 | 
			
		||||
        /// <param name="orderBy">A function to order elements.</param>
 | 
			
		||||
        /// <param name="include">A function to include navigation properties</param>
 | 
			
		||||
        /// <param name="pageIndex">The index of page.</param>
 | 
			
		||||
        /// <param name="pageSize">The size of the page.</param>
 | 
			
		||||
        /// <param name="disableTracking"><c>True</c> to disable changing tracking; otherwise, <c>false</c>. Default to <c>true</c>.</param>
 | 
			
		||||
        /// <param name="cancellationToken">
 | 
			
		||||
        ///     A <see cref="CancellationToken" /> to observe while waiting for the task to complete.
 | 
			
		||||
        /// </param>
 | 
			
		||||
        /// <param name="ignoreQueryFilters">Ignore query filters</param>
 | 
			
		||||
        /// <returns>An <see cref="IPagedList{TEntity}"/> that contains elements that satisfy the condition specified by <paramref name="predicate"/>.</returns>
 | 
			
		||||
        /// <remarks>This method default no-tracking query.</remarks>
 | 
			
		||||
        Task<IPagedList<TResult>> GetPagedListAsync<TResult>(Expression<Func<TEntity, TResult>> selector,
 | 
			
		||||
                                                             Expression<Func<TEntity, bool>> predicate = null,
 | 
			
		||||
                                                             Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
 | 
			
		||||
                                                             Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
 | 
			
		||||
                                                             int pageIndex = 0,
 | 
			
		||||
                                                             int pageSize = 20,
 | 
			
		||||
                                                             bool disableTracking = true,
 | 
			
		||||
                                                             CancellationToken cancellationToken = default(CancellationToken),
 | 
			
		||||
                                                             bool ignoreQueryFilters = false) where TResult : class;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the first or default entity based on a predicate, orderby delegate and include delegate. This method defaults to a read-only, no-tracking query.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate">A function to test each element for a condition.</param>
 | 
			
		||||
        /// <param name="orderBy">A function to order elements.</param>
 | 
			
		||||
        /// <param name="include">A function to include navigation properties</param>
 | 
			
		||||
        /// <param name="disableTracking"><c>true</c> to disable changing tracking; otherwise, <c>false</c>. Default to <c>true</c>.</param>
 | 
			
		||||
        /// <param name="ignoreQueryFilters">Ignore query filters</param>
 | 
			
		||||
        /// <returns>An <see cref="IPagedList{TEntity}"/> that contains elements that satisfy the condition specified by <paramref name="predicate"/>.</returns>
 | 
			
		||||
        /// <remarks>This method defaults to a read-only, no-tracking query.</remarks>
 | 
			
		||||
        TEntity GetFirstOrDefault(Expression<Func<TEntity, bool>> predicate = null,
 | 
			
		||||
                                  Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
 | 
			
		||||
                                  Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
 | 
			
		||||
                                  bool disableTracking = true,
 | 
			
		||||
                                  bool ignoreQueryFilters = false);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the first or default entity based on a predicate, orderby delegate and include delegate. This method defaults to a read-only, no-tracking query.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="selector">The selector for projection.</param>
 | 
			
		||||
        /// <param name="predicate">A function to test each element for a condition.</param>
 | 
			
		||||
        /// <param name="orderBy">A function to order elements.</param>
 | 
			
		||||
        /// <param name="include">A function to include navigation properties</param>
 | 
			
		||||
        /// <param name="disableTracking"><c>true</c> to disable changing tracking; otherwise, <c>false</c>. Default to <c>true</c>.</param>
 | 
			
		||||
        /// <param name="ignoreQueryFilters">Ignore query filters</param>
 | 
			
		||||
        /// <returns>An <see cref="IPagedList{TEntity}"/> that contains elements that satisfy the condition specified by <paramref name="predicate"/>.</returns>
 | 
			
		||||
        /// <remarks>This method defaults to a read-only, no-tracking query.</remarks>
 | 
			
		||||
        TResult GetFirstOrDefault<TResult>(Expression<Func<TEntity, TResult>> selector,
 | 
			
		||||
                                           Expression<Func<TEntity, bool>> predicate = null,
 | 
			
		||||
                                           Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
 | 
			
		||||
                                           Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
 | 
			
		||||
                                           bool disableTracking = true,
 | 
			
		||||
                                           bool ignoreQueryFilters = false);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the first or default entity based on a predicate, orderby delegate and include delegate. This method defaults to a read-only, no-tracking query.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="selector">The selector for projection.</param>
 | 
			
		||||
        /// <param name="predicate">A function to test each element for a condition.</param>
 | 
			
		||||
        /// <param name="orderBy">A function to order elements.</param>
 | 
			
		||||
        /// <param name="include">A function to include navigation properties</param>
 | 
			
		||||
        /// <param name="disableTracking"><c>true</c> to disable changing tracking; otherwise, <c>false</c>. Default to <c>true</c>.</param>
 | 
			
		||||
        /// <param name="ignoreQueryFilters">Ignore query filters</param>
 | 
			
		||||
        /// <returns>An <see cref="IPagedList{TEntity}"/> that contains elements that satisfy the condition specified by <paramref name="predicate"/>.</returns>
 | 
			
		||||
        /// <remarks>Ex: This method defaults to a read-only, no-tracking query.</remarks>
 | 
			
		||||
        Task<TResult> GetFirstOrDefaultAsync<TResult>(Expression<Func<TEntity, TResult>> selector,
 | 
			
		||||
            Expression<Func<TEntity, bool>> predicate = null,
 | 
			
		||||
            Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
 | 
			
		||||
            Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
 | 
			
		||||
            bool disableTracking = true,
 | 
			
		||||
            bool ignoreQueryFilters = false);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the first or default entity based on a predicate, orderby delegate and include delegate. This method defaults to a read-only, no-tracking query.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate">A function to test each element for a condition.</param>
 | 
			
		||||
        /// <param name="orderBy">A function to order elements.</param>
 | 
			
		||||
        /// <param name="include">A function to include navigation properties</param>
 | 
			
		||||
        /// <param name="disableTracking"><c>true</c> to disable changing tracking; otherwise, <c>false</c>. Default to <c>true</c>.</param>
 | 
			
		||||
        /// <param name="ignoreQueryFilters">Ignore query filters</param>
 | 
			
		||||
        /// <returns>An <see cref="IPagedList{TEntity}"/> that contains elements that satisfy the condition specified by <paramref name="predicate"/>.</returns>
 | 
			
		||||
        /// <remarks>Ex: This method defaults to a read-only, no-tracking query. </remarks>
 | 
			
		||||
        Task<TEntity> GetFirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate = null,
 | 
			
		||||
            Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
 | 
			
		||||
            Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
 | 
			
		||||
            bool disableTracking = true,
 | 
			
		||||
            bool ignoreQueryFilters = false);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Uses raw SQL queries to fetch the specified <typeparamref name="TEntity" /> data.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="sql">The raw SQL.</param>
 | 
			
		||||
        /// <param name="parameters">The parameters.</param>
 | 
			
		||||
        /// <returns>An <see cref="IQueryable{TEntity}" /> that contains elements that satisfy the condition specified by raw SQL.</returns>
 | 
			
		||||
        IQueryable<TEntity> FromSql(string sql, params object[] parameters);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Finds an entity with the given primary key values. If found, is attached to the context and returned. If no entity is found, then null is returned.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="keyValues">The values of the primary key for the entity to be found.</param>
 | 
			
		||||
        /// <returns>The found entity or null.</returns>
 | 
			
		||||
        TEntity Find(params object[] keyValues);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Finds an entity with the given primary key values. If found, is attached to the context and returned. If no entity is found, then null is returned.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="keyValues">The values of the primary key for the entity to be found.</param>
 | 
			
		||||
        /// <returns>A <see cref="Task{TEntity}"/> that represents the asynchronous find operation. The task result contains the found entity or null.</returns>
 | 
			
		||||
        ValueTask<TEntity> FindAsync(params object[] keyValues);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Finds an entity with the given primary key values. If found, is attached to the context and returned. If no entity is found, then null is returned.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="keyValues">The values of the primary key for the entity to be found.</param>
 | 
			
		||||
        /// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete.</param>
 | 
			
		||||
        /// <returns>A <see cref="Task{TEntity}"/> that represents the asynchronous find operation. The task result contains the found entity or null.</returns>
 | 
			
		||||
        ValueTask<TEntity> FindAsync(object[] keyValues, CancellationToken cancellationToken);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets all entities. This method is not recommended
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <returns>The <see cref="IQueryable{TEntity}"/>.</returns>
 | 
			
		||||
        IQueryable<TEntity> GetAll();
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets all entities. This method is not recommended
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate">A function to test each element for a condition.</param>
 | 
			
		||||
        /// <param name="orderBy">A function to order elements.</param>
 | 
			
		||||
        /// <param name="include">A function to include navigation properties</param>
 | 
			
		||||
        /// <param name="disableTracking"><c>true</c> to disable changing tracking; otherwise, <c>false</c>. Default to <c>true</c>.</param>
 | 
			
		||||
        /// <param name="ignoreQueryFilters">Ignore query filters</param>
 | 
			
		||||
        /// <returns>An <see cref="IPagedList{TEntity}"/> that contains elements that satisfy the condition specified by <paramref name="predicate"/>.</returns>
 | 
			
		||||
        /// <remarks>Ex: This method defaults to a read-only, no-tracking query.</remarks>
 | 
			
		||||
        IQueryable<TEntity> GetAll(Expression<Func<TEntity, bool>> predicate = null,
 | 
			
		||||
                                                  Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
 | 
			
		||||
                                                  Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
 | 
			
		||||
                                                  bool disableTracking = true,
 | 
			
		||||
                                                  bool ignoreQueryFilters = false);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets all entities. This method is not recommended
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <returns>The <see cref="IQueryable{TEntity}"/>.</returns>
 | 
			
		||||
        Task<IList<TEntity>> GetAllAsync();
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets all entities. This method is not recommended
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate">A function to test each element for a condition.</param>
 | 
			
		||||
        /// <param name="orderBy">A function to order elements.</param>
 | 
			
		||||
        /// <param name="include">A function to include navigation properties</param>
 | 
			
		||||
        /// <param name="disableTracking"><c>true</c> to disable changing tracking; otherwise, <c>false</c>. Default to <c>true</c>.</param>
 | 
			
		||||
        /// <param name="ignoreQueryFilters">Ignore query filters</param>
 | 
			
		||||
        /// <returns>An <see cref="IPagedList{TEntity}"/> that contains elements that satisfy the condition specified by <paramref name="predicate"/>.</returns>
 | 
			
		||||
        /// <remarks>Ex: This method defaults to a read-only, no-tracking query.</remarks>
 | 
			
		||||
        Task<IList<TEntity>> GetAllAsync(Expression<Func<TEntity, bool>> predicate = null,
 | 
			
		||||
                                                  Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
 | 
			
		||||
                                                  Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
 | 
			
		||||
                                                  bool disableTracking = true,
 | 
			
		||||
                                                  bool ignoreQueryFilters = false);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the count based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate"></param>
 | 
			
		||||
        /// <returns></returns>
 | 
			
		||||
        int Count(Expression<Func<TEntity, bool>> predicate = null);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets async the count based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate"></param>
 | 
			
		||||
        /// <returns></returns>
 | 
			
		||||
        Task<int> CountAsync(Expression<Func<TEntity, bool>> predicate = null);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the long count based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate"></param>
 | 
			
		||||
        /// <returns></returns>
 | 
			
		||||
        long LongCount(Expression<Func<TEntity, bool>> predicate = null);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets async the long count based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate"></param>
 | 
			
		||||
        /// <returns></returns>
 | 
			
		||||
        Task<long> LongCountAsync(Expression<Func<TEntity, bool>> predicate = null);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the max based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate"></param>
 | 
			
		||||
        ///  /// <param name="selector"></param>
 | 
			
		||||
        /// <returns>decimal</returns>
 | 
			
		||||
        T Max<T>(Expression<Func<TEntity, bool>> predicate = null, Expression<Func<TEntity, T>> selector = null);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the async max based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate"></param>
 | 
			
		||||
        ///  /// <param name="selector"></param>
 | 
			
		||||
        /// <returns>decimal</returns>
 | 
			
		||||
        Task<T> MaxAsync<T>(Expression<Func<TEntity, bool>> predicate = null, Expression<Func<TEntity, T>> selector = null);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the min based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate"></param>
 | 
			
		||||
        /// <param name="selector"></param>
 | 
			
		||||
        /// <returns>decimal</returns>
 | 
			
		||||
        T Min<T>(Expression<Func<TEntity, bool>> predicate = null, Expression<Func<TEntity, T>> selector = null);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the async min based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate"></param>
 | 
			
		||||
        /// <param name="selector"></param>
 | 
			
		||||
        /// <returns>decimal</returns>
 | 
			
		||||
        Task<T> MinAsync<T>(Expression<Func<TEntity, bool>> predicate = null, Expression<Func<TEntity, T>> selector = null);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the average based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate"></param>
 | 
			
		||||
        ///  /// <param name="selector"></param>
 | 
			
		||||
        /// <returns>decimal</returns>
 | 
			
		||||
        decimal Average(Expression<Func<TEntity, bool>> predicate = null, Expression<Func<TEntity, decimal>> selector = null);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the async average based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate"></param>
 | 
			
		||||
        ///  /// <param name="selector"></param>
 | 
			
		||||
        /// <returns>decimal</returns>
 | 
			
		||||
        Task<decimal> AverageAsync(Expression<Func<TEntity, bool>> predicate = null, Expression<Func<TEntity, decimal>> selector = null);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the sum based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate"></param>
 | 
			
		||||
        ///  /// <param name="selector"></param>
 | 
			
		||||
        /// <returns>decimal</returns>
 | 
			
		||||
        decimal Sum(Expression<Func<TEntity, bool>> predicate = null, Expression<Func<TEntity, decimal>> selector = null);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the async sum based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate"></param>
 | 
			
		||||
        ///  /// <param name="selector"></param>
 | 
			
		||||
        /// <returns>decimal</returns>
 | 
			
		||||
        Task<decimal> SumAsync(Expression<Func<TEntity, bool>> predicate = null, Expression<Func<TEntity, decimal>> selector = null);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the Exists record based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="selector"></param>
 | 
			
		||||
        /// <returns></returns>
 | 
			
		||||
        bool Exists(Expression<Func<TEntity, bool>> selector = null);
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the Async Exists record based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="selector"></param>
 | 
			
		||||
        /// <returns></returns>
 | 
			
		||||
        Task<bool> ExistsAsync(Expression<Func<TEntity, bool>> selector = null);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Inserts a new entity synchronously.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entity">The entity to insert.</param>
 | 
			
		||||
        TEntity Insert(TEntity entity);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Inserts a range of entities synchronously.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entities">The entities to insert.</param>
 | 
			
		||||
        void Insert(params TEntity[] entities);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Inserts a range of entities synchronously.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entities">The entities to insert.</param>
 | 
			
		||||
        void Insert(IEnumerable<TEntity> entities);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Inserts a new entity asynchronously.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entity">The entity to insert.</param>
 | 
			
		||||
        /// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete.</param>
 | 
			
		||||
        /// <returns>A <see cref="Task"/> that represents the asynchronous insert operation.</returns>
 | 
			
		||||
        ValueTask<EntityEntry<TEntity>> InsertAsync(TEntity entity, CancellationToken cancellationToken = default(CancellationToken));
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Inserts a range of entities asynchronously.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entities">The entities to insert.</param>
 | 
			
		||||
        /// <returns>A <see cref="Task"/> that represents the asynchronous insert operation.</returns>
 | 
			
		||||
        Task InsertAsync(params TEntity[] entities);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Inserts a range of entities asynchronously.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entities">The entities to insert.</param>
 | 
			
		||||
        /// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete.</param>
 | 
			
		||||
        /// <returns>A <see cref="Task"/> that represents the asynchronous insert operation.</returns>
 | 
			
		||||
        Task InsertAsync(IEnumerable<TEntity> entities, CancellationToken cancellationToken = default(CancellationToken));
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Updates the specified entity.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entity">The entity.</param>
 | 
			
		||||
        void Update(TEntity entity);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Updates the specified entities.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entities">The entities.</param>
 | 
			
		||||
        void Update(params TEntity[] entities);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Updates the specified entities.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entities">The entities.</param>
 | 
			
		||||
        void Update(IEnumerable<TEntity> entities);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Deletes the entity by the specified primary key.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="id">The primary key value.</param>
 | 
			
		||||
        void Delete(object id);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Deletes the specified entity.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entity">The entity to delete.</param>
 | 
			
		||||
        void Delete(TEntity entity);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Deletes the specified entities.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entities">The entities.</param>
 | 
			
		||||
        void Delete(params TEntity[] entities);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Deletes the specified entities.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entities">The entities.</param>
 | 
			
		||||
        void Delete(IEnumerable<TEntity> entities);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Change entity state for patch method on web api.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entity">The entity.</param>
 | 
			
		||||
        /// /// <param name="state">The entity state.</param>
 | 
			
		||||
        void ChangeEntityState(TEntity entity, EntityState state);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								Entities/Context/UnitOfWork/IRepositoryFactory.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								Entities/Context/UnitOfWork/IRepositoryFactory.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Text;
 | 
			
		||||
 | 
			
		||||
namespace SharedDATA.Api
 | 
			
		||||
{
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Defines the interfaces for <see cref="IRepository{TEntity}"/> interfaces.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public interface IRepositoryFactory
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the specified repository for the <typeparamref name="TEntity"/>.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="hasCustomRepository"><c>True</c> if providing custom repositry</param>
 | 
			
		||||
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
 | 
			
		||||
        /// <returns>An instance of type inherited from <see cref="IRepository{TEntity}"/> interface.</returns>
 | 
			
		||||
        IRepository<TEntity> GetRepository<TEntity>(bool hasCustomRepository = false) where TEntity : class;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										78
									
								
								Entities/Context/UnitOfWork/IUnitOfWork.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								Entities/Context/UnitOfWork/IUnitOfWork.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
namespace SharedDATA.Api
 | 
			
		||||
{
 | 
			
		||||
    using System;
 | 
			
		||||
    using System.Linq;
 | 
			
		||||
    using System.Threading.Tasks;
 | 
			
		||||
    using Microsoft.EntityFrameworkCore;
 | 
			
		||||
    using Microsoft.EntityFrameworkCore.ChangeTracking;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Defines the interface(s) for unit of work.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public interface IUnitOfWork : IDisposable
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Changes the database name. This require the databases in the same machine. NOTE: This only work for MySQL right now.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="database">The database name.</param>
 | 
			
		||||
        /// <remarks>
 | 
			
		||||
        /// This only been used for supporting multiple databases in the same model. This require the databases in the same machine.
 | 
			
		||||
        /// </remarks>
 | 
			
		||||
        void ChangeDatabase(string database);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the specified repository for the <typeparamref name="TEntity"/>.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="hasCustomRepository"><c>True</c> if providing custom repositry</param>
 | 
			
		||||
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
 | 
			
		||||
        /// <returns>An instance of type inherited from <see cref="IRepository{TEntity}"/> interface.</returns>
 | 
			
		||||
        IRepository<TEntity> GetRepository<TEntity>(bool hasCustomRepository = false) where TEntity : class;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the db context.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <returns></returns>
 | 
			
		||||
        TContext GetDbContext<TContext>() where TContext : DbContext;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Saves all changes made in this context to the database.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="ensureAutoHistory"><c>True</c> if sayve changes ensure auto record the change history.</param>
 | 
			
		||||
        /// <returns>The number of state entries written to the database.</returns>
 | 
			
		||||
        int SaveChanges(bool ensureAutoHistory = false);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Asynchronously saves all changes made in this unit of work to the database.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="ensureAutoHistory"><c>True</c> if save changes ensure auto record the change history.</param>
 | 
			
		||||
        /// <returns>A <see cref="Task{TResult}"/> that represents the asynchronous save operation. The task result contains the number of state entities written to database.</returns>
 | 
			
		||||
        Task<int> SaveChangesAsync(bool ensureAutoHistory = false);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Executes the specified raw SQL command.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="sql">The raw SQL.</param>
 | 
			
		||||
        /// <param name="parameters">The parameters.</param>
 | 
			
		||||
        /// <returns>The number of state entities written to database.</returns>
 | 
			
		||||
        int ExecuteSqlCommand(string sql, params object[] parameters);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Uses raw SQL queries to fetch the specified <typeparamref name="TEntity"/> data.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
 | 
			
		||||
        /// <param name="sql">The raw SQL.</param>
 | 
			
		||||
        /// <param name="parameters">The parameters.</param>
 | 
			
		||||
        /// <returns>An <see cref="IQueryable{T}"/> that contains elements that satisfy the condition specified by raw SQL.</returns>
 | 
			
		||||
        IQueryable<TEntity> FromSql<TEntity>(string sql, params object[] parameters) where TEntity : class;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Uses TrakGrap Api to attach disconnected entities
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="rootEntity"> Root entity</param>
 | 
			
		||||
        /// <param name="callback">Delegate to convert Object's State properities to Entities entry state.</param>
 | 
			
		||||
        void TrackGraph(object rootEntity, Action<EntityEntryGraphNode> callback);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								Entities/Context/UnitOfWork/IUnitOfWorkOfT.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								Entities/Context/UnitOfWork/IUnitOfWorkOfT.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
namespace SharedDATA.Api
 | 
			
		||||
{
 | 
			
		||||
    using Microsoft.EntityFrameworkCore;
 | 
			
		||||
    using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Defines the interface(s) for generic unit of work.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public interface IUnitOfWork<TContext> : IUnitOfWork where TContext : DbContext
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the db context.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <returns>The instance of type <typeparamref name="TContext"/>.</returns>
 | 
			
		||||
        TContext DbContext { get; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Saves all changes made in this context to the database with distributed transaction.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="ensureAutoHistory"><c>True</c> if save changes ensure auto record the change history.</param>
 | 
			
		||||
        /// <param name="unitOfWorks">An optional <see cref="IUnitOfWork"/> array.</param>
 | 
			
		||||
        /// <returns>A <see cref="Task{TResult}"/> that represents the asynchronous save operation. The task result contains the number of state entities written to database.</returns>
 | 
			
		||||
        Task<int> SaveChangesAsync(bool ensureAutoHistory = false, params IUnitOfWork[] unitOfWorks);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										942
									
								
								Entities/Context/UnitOfWork/Repository.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										942
									
								
								Entities/Context/UnitOfWork/Repository.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,942 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
namespace SharedDATA.Api
 | 
			
		||||
{
 | 
			
		||||
    using System;
 | 
			
		||||
    using System.Collections.Generic;
 | 
			
		||||
    using System.Linq;
 | 
			
		||||
    using System.Linq.Expressions;
 | 
			
		||||
    using System.Reflection;
 | 
			
		||||
    using System.Threading;
 | 
			
		||||
    using System.Threading.Tasks;
 | 
			
		||||
    using Microsoft.EntityFrameworkCore;
 | 
			
		||||
    using Microsoft.EntityFrameworkCore.Metadata;
 | 
			
		||||
    using Microsoft.EntityFrameworkCore.Query;
 | 
			
		||||
    using Microsoft.EntityFrameworkCore.ChangeTracking;
 | 
			
		||||
    using SharedDATA.Context;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Represents a default generic repository implements the <see cref="IRepository{TEntity}"/> interface.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <typeparam name="TEntity">The type of the entity.</typeparam>
 | 
			
		||||
    public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
 | 
			
		||||
    {
 | 
			
		||||
        protected readonly DbContext _dbContext;
 | 
			
		||||
        protected readonly DbSet<TEntity> _dbSet;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="Repository{TEntity}"/> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="dbContext">The database context.</param>
 | 
			
		||||
        public Repository(DbContext dbContext)
 | 
			
		||||
        {
 | 
			
		||||
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
 | 
			
		||||
            _dbSet = _dbContext.Set<TEntity>();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Changes the table name. This require the tables in the same database.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="table"></param>
 | 
			
		||||
        /// <remarks>
 | 
			
		||||
        /// This only been used for supporting multiple tables in the same model. This require the tables in the same database.
 | 
			
		||||
        /// </remarks>
 | 
			
		||||
        public virtual void ChangeTable(string table)
 | 
			
		||||
        {
 | 
			
		||||
            if (_dbContext.Model.FindEntityType(typeof(TEntity)) is IConventionEntityType relational)
 | 
			
		||||
            {
 | 
			
		||||
                relational.SetTableName(table);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets all entities. This method is not recommended
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <returns>The <see cref="IQueryable{TEntity}"/>.</returns>
 | 
			
		||||
        public IQueryable<TEntity> GetAll()
 | 
			
		||||
        {
 | 
			
		||||
            return _dbSet;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets all entities. This method is not recommended
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate">A function to test each element for a condition.</param>
 | 
			
		||||
        /// <param name="orderBy">A function to order elements.</param>
 | 
			
		||||
        /// <param name="include">A function to include navigation properties</param>
 | 
			
		||||
        /// <param name="disableTracking"><c>true</c> to disable changing tracking; otherwise, <c>false</c>. Default to <c>true</c>.</param>
 | 
			
		||||
        /// <param name="ignoreQueryFilters">Ignore query filters</param>
 | 
			
		||||
        /// <returns>An <see cref="IPagedList{TEntity}"/> that contains elements that satisfy the condition specified by <paramref name="predicate"/>.</returns>
 | 
			
		||||
        /// <remarks>Ex: This method defaults to a read-only, no-tracking query.</remarks>
 | 
			
		||||
        public IQueryable<TEntity> GetAll(
 | 
			
		||||
            Expression<Func<TEntity, bool>> predicate = null,
 | 
			
		||||
            Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
 | 
			
		||||
            Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null, bool disableTracking = true, bool ignoreQueryFilters = false)
 | 
			
		||||
        {
 | 
			
		||||
            IQueryable<TEntity> query = _dbSet;
 | 
			
		||||
 | 
			
		||||
            if (disableTracking)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.AsNoTracking();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (include != null)
 | 
			
		||||
            {
 | 
			
		||||
                query = include(query);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (predicate != null)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.Where(predicate);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (ignoreQueryFilters)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.IgnoreQueryFilters();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (orderBy != null)
 | 
			
		||||
            {
 | 
			
		||||
                return orderBy(query);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return query;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the <see cref="IPagedList{TEntity}"/> based on a predicate, orderby delegate and page information. This method default no-tracking query.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate">A function to test each element for a condition.</param>
 | 
			
		||||
        /// <param name="orderBy">A function to order elements.</param>
 | 
			
		||||
        /// <param name="include">A function to include navigation properties</param>
 | 
			
		||||
        /// <param name="pageIndex">The index of page.</param>
 | 
			
		||||
        /// <param name="pageSize">The size of the page.</param>
 | 
			
		||||
        /// <param name="disableTracking"><c>True</c> to disable changing tracking; otherwise, <c>false</c>. Default to <c>true</c>.</param>
 | 
			
		||||
        /// <param name="ignoreQueryFilters">Ignore query filters</param>
 | 
			
		||||
        /// <returns>An <see cref="IPagedList{TEntity}"/> that contains elements that satisfy the condition specified by <paramref name="predicate"/>.</returns>
 | 
			
		||||
        /// <remarks>This method default no-tracking query.</remarks>
 | 
			
		||||
        public virtual IPagedList<TEntity> GetPagedList(Expression<Func<TEntity, bool>> predicate = null,
 | 
			
		||||
                                                Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
 | 
			
		||||
                                                Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
 | 
			
		||||
                                                int pageIndex = 0,
 | 
			
		||||
                                                int pageSize = 20,
 | 
			
		||||
                                                bool disableTracking = true,
 | 
			
		||||
                                                bool ignoreQueryFilters = false)
 | 
			
		||||
        {
 | 
			
		||||
            IQueryable<TEntity> query = _dbSet;
 | 
			
		||||
 | 
			
		||||
            if (disableTracking)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.AsNoTracking();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (include != null)
 | 
			
		||||
            {
 | 
			
		||||
                query = include(query);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (predicate != null)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.Where(predicate);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (ignoreQueryFilters)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.IgnoreQueryFilters();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (orderBy != null)
 | 
			
		||||
            {
 | 
			
		||||
                return orderBy(query).ToPagedList(pageIndex, pageSize);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return query.ToPagedList(pageIndex, pageSize);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the <see cref="IPagedList{TEntity}"/> based on a predicate, orderby delegate and page information. This method default no-tracking query.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate">A function to test each element for a condition.</param>
 | 
			
		||||
        /// <param name="orderBy">A function to order elements.</param>
 | 
			
		||||
        /// <param name="include">A function to include navigation properties</param>
 | 
			
		||||
        /// <param name="pageIndex">The index of page.</param>
 | 
			
		||||
        /// <param name="pageSize">The size of the page.</param>
 | 
			
		||||
        /// <param name="disableTracking"><c>True</c> to disable changing tracking; otherwise, <c>false</c>. Default to <c>true</c>.</param>
 | 
			
		||||
        /// <param name="cancellationToken">
 | 
			
		||||
        ///     A <see cref="CancellationToken" /> to observe while waiting for the task to complete.
 | 
			
		||||
        /// </param>
 | 
			
		||||
        /// <param name="ignoreQueryFilters">Ignore query filters</param>
 | 
			
		||||
        /// <returns>An <see cref="IPagedList{TEntity}"/> that contains elements that satisfy the condition specified by <paramref name="predicate"/>.</returns>
 | 
			
		||||
        /// <remarks>This method default no-tracking query.</remarks>
 | 
			
		||||
        public virtual Task<IPagedList<TEntity>> GetPagedListAsync(Expression<Func<TEntity, bool>> predicate = null,
 | 
			
		||||
                                                           Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
 | 
			
		||||
                                                           Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
 | 
			
		||||
                                                           int pageIndex = 0,
 | 
			
		||||
                                                           int pageSize = 20,
 | 
			
		||||
                                                           bool disableTracking = true,
 | 
			
		||||
                                                           CancellationToken cancellationToken = default(CancellationToken),
 | 
			
		||||
                                                           bool ignoreQueryFilters = false)
 | 
			
		||||
        {
 | 
			
		||||
            IQueryable<TEntity> query = _dbSet;
 | 
			
		||||
 | 
			
		||||
            if (disableTracking)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.AsNoTracking();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (include != null)
 | 
			
		||||
            {
 | 
			
		||||
                query = include(query);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (predicate != null)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.Where(predicate);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (ignoreQueryFilters)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.IgnoreQueryFilters();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (orderBy != null)
 | 
			
		||||
            {
 | 
			
		||||
                return orderBy(query).ToPagedListAsync(pageIndex, pageSize, 0, cancellationToken);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return query.ToPagedListAsync(pageIndex, pageSize, 0, cancellationToken);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the <see cref="IPagedList{TResult}"/> based on a predicate, orderby delegate and page information. This method default no-tracking query.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="selector">The selector for projection.</param>
 | 
			
		||||
        /// <param name="predicate">A function to test each element for a condition.</param>
 | 
			
		||||
        /// <param name="orderBy">A function to order elements.</param>
 | 
			
		||||
        /// <param name="include">A function to include navigation properties</param>
 | 
			
		||||
        /// <param name="pageIndex">The index of page.</param>
 | 
			
		||||
        /// <param name="pageSize">The size of the page.</param>
 | 
			
		||||
        /// <param name="disableTracking"><c>True</c> to disable changing tracking; otherwise, <c>false</c>. Default to <c>true</c>.</param>
 | 
			
		||||
        /// <param name="ignoreQueryFilters">Ignore query filters</param>
 | 
			
		||||
        /// <returns>An <see cref="IPagedList{TResult}"/> that contains elements that satisfy the condition specified by <paramref name="predicate"/>.</returns>
 | 
			
		||||
        /// <remarks>This method default no-tracking query.</remarks>
 | 
			
		||||
        public virtual IPagedList<TResult> GetPagedList<TResult>(Expression<Func<TEntity, TResult>> selector,
 | 
			
		||||
                                                         Expression<Func<TEntity, bool>> predicate = null,
 | 
			
		||||
                                                         Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
 | 
			
		||||
                                                         Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
 | 
			
		||||
                                                         int pageIndex = 0,
 | 
			
		||||
                                                         int pageSize = 20,
 | 
			
		||||
                                                         bool disableTracking = true,
 | 
			
		||||
                                                         bool ignoreQueryFilters = false)
 | 
			
		||||
            where TResult : class
 | 
			
		||||
        {
 | 
			
		||||
            IQueryable<TEntity> query = _dbSet;
 | 
			
		||||
 | 
			
		||||
            if (disableTracking)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.AsNoTracking();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (include != null)
 | 
			
		||||
            {
 | 
			
		||||
                query = include(query);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (predicate != null)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.Where(predicate);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (ignoreQueryFilters)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.IgnoreQueryFilters();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (orderBy != null)
 | 
			
		||||
            {
 | 
			
		||||
                return orderBy(query).Select(selector).ToPagedList(pageIndex, pageSize);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return query.Select(selector).ToPagedList(pageIndex, pageSize);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the <see cref="IPagedList{TEntity}"/> based on a predicate, orderby delegate and page information. This method default no-tracking query.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="selector">The selector for projection.</param>
 | 
			
		||||
        /// <param name="predicate">A function to test each element for a condition.</param>
 | 
			
		||||
        /// <param name="orderBy">A function to order elements.</param>
 | 
			
		||||
        /// <param name="include">A function to include navigation properties</param>
 | 
			
		||||
        /// <param name="pageIndex">The index of page.</param>
 | 
			
		||||
        /// <param name="pageSize">The size of the page.</param>
 | 
			
		||||
        /// <param name="disableTracking"><c>True</c> to disable changing tracking; otherwise, <c>false</c>. Default to <c>true</c>.</param>
 | 
			
		||||
        /// <param name="cancellationToken">
 | 
			
		||||
        ///     A <see cref="CancellationToken" /> to observe while waiting for the task to complete.
 | 
			
		||||
        /// </param>
 | 
			
		||||
        /// <param name="ignoreQueryFilters">Ignore query filters</param>
 | 
			
		||||
        /// <returns>An <see cref="IPagedList{TEntity}"/> that contains elements that satisfy the condition specified by <paramref name="predicate"/>.</returns>
 | 
			
		||||
        /// <remarks>This method default no-tracking query.</remarks>
 | 
			
		||||
        public virtual Task<IPagedList<TResult>> GetPagedListAsync<TResult>(Expression<Func<TEntity, TResult>> selector,
 | 
			
		||||
                                                                    Expression<Func<TEntity, bool>> predicate = null,
 | 
			
		||||
                                                                    Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
 | 
			
		||||
                                                                    Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
 | 
			
		||||
                                                                    int pageIndex = 0,
 | 
			
		||||
                                                                    int pageSize = 20,
 | 
			
		||||
                                                                    bool disableTracking = true,
 | 
			
		||||
                                                                    CancellationToken cancellationToken = default(CancellationToken),
 | 
			
		||||
                                                                    bool ignoreQueryFilters = false)
 | 
			
		||||
            where TResult : class
 | 
			
		||||
        {
 | 
			
		||||
            IQueryable<TEntity> query = _dbSet;
 | 
			
		||||
 | 
			
		||||
            if (disableTracking)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.AsNoTracking();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (include != null)
 | 
			
		||||
            {
 | 
			
		||||
                query = include(query);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (predicate != null)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.Where(predicate);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (ignoreQueryFilters)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.IgnoreQueryFilters();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (orderBy != null)
 | 
			
		||||
            {
 | 
			
		||||
                return orderBy(query).Select(selector).ToPagedListAsync(pageIndex, pageSize, 0, cancellationToken);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return query.Select(selector).ToPagedListAsync(pageIndex, pageSize, 0, cancellationToken);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the first or default entity based on a predicate, orderby delegate and include delegate. This method default no-tracking query.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate">A function to test each element for a condition.</param>
 | 
			
		||||
        /// <param name="orderBy">A function to order elements.</param>
 | 
			
		||||
        /// <param name="include">A function to include navigation properties</param>
 | 
			
		||||
        /// <param name="disableTracking"><c>True</c> to disable changing tracking; otherwise, <c>false</c>. Default to <c>true</c>.</param>
 | 
			
		||||
        /// <param name="ignoreQueryFilters">Ignore query filters</param>
 | 
			
		||||
        /// <returns>An <see cref="IPagedList{TEntity}"/> that contains elements that satisfy the condition specified by <paramref name="predicate"/>.</returns>
 | 
			
		||||
        /// <remarks>This method default no-tracking query.</remarks>
 | 
			
		||||
        public virtual TEntity GetFirstOrDefault(Expression<Func<TEntity, bool>> predicate = null,
 | 
			
		||||
                                         Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
 | 
			
		||||
                                         Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
 | 
			
		||||
                                         bool disableTracking = true,
 | 
			
		||||
                                         bool ignoreQueryFilters = false)
 | 
			
		||||
        {
 | 
			
		||||
            IQueryable<TEntity> query = _dbSet;
 | 
			
		||||
 | 
			
		||||
            if (disableTracking)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.AsNoTracking();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (include != null)
 | 
			
		||||
            {
 | 
			
		||||
                query = include(query);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (predicate != null)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.Where(predicate);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (ignoreQueryFilters)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.IgnoreQueryFilters();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (orderBy != null)
 | 
			
		||||
            {
 | 
			
		||||
                return orderBy(query).FirstOrDefault();
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return query.FirstOrDefault();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        public virtual async Task<TEntity> GetFirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate = null,
 | 
			
		||||
            Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
 | 
			
		||||
            Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
 | 
			
		||||
            bool disableTracking = true,
 | 
			
		||||
            bool ignoreQueryFilters = false)
 | 
			
		||||
        {
 | 
			
		||||
            IQueryable<TEntity> query = _dbSet;
 | 
			
		||||
 | 
			
		||||
            if (disableTracking)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.AsNoTracking();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (include != null)
 | 
			
		||||
            {
 | 
			
		||||
                query = include(query);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (predicate != null)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.Where(predicate);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (ignoreQueryFilters)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.IgnoreQueryFilters();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (orderBy != null)
 | 
			
		||||
            {
 | 
			
		||||
                return await orderBy(query).FirstOrDefaultAsync();
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return await query.FirstOrDefaultAsync();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the first or default entity based on a predicate, orderby delegate and include delegate. This method default no-tracking query.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="selector">The selector for projection.</param>
 | 
			
		||||
        /// <param name="predicate">A function to test each element for a condition.</param>
 | 
			
		||||
        /// <param name="orderBy">A function to order elements.</param>
 | 
			
		||||
        /// <param name="include">A function to include navigation properties</param>
 | 
			
		||||
        /// <param name="disableTracking"><c>True</c> to disable changing tracking; otherwise, <c>false</c>. Default to <c>true</c>.</param>
 | 
			
		||||
        /// <param name="ignoreQueryFilters">Ignore query filters</param>
 | 
			
		||||
        /// <returns>An <see cref="IPagedList{TEntity}"/> that contains elements that satisfy the condition specified by <paramref name="predicate"/>.</returns>
 | 
			
		||||
        /// <remarks>This method default no-tracking query.</remarks>
 | 
			
		||||
        public virtual TResult GetFirstOrDefault<TResult>(Expression<Func<TEntity, TResult>> selector,
 | 
			
		||||
                                                  Expression<Func<TEntity, bool>> predicate = null,
 | 
			
		||||
                                                  Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
 | 
			
		||||
                                                  Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
 | 
			
		||||
                                                  bool disableTracking = true,
 | 
			
		||||
                                                  bool ignoreQueryFilters = false)
 | 
			
		||||
        {
 | 
			
		||||
            IQueryable<TEntity> query = _dbSet;
 | 
			
		||||
 | 
			
		||||
            if (disableTracking)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.AsNoTracking();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (include != null)
 | 
			
		||||
            {
 | 
			
		||||
                query = include(query);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (predicate != null)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.Where(predicate);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (ignoreQueryFilters)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.IgnoreQueryFilters();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (orderBy != null)
 | 
			
		||||
            {
 | 
			
		||||
                return orderBy(query).Select(selector).FirstOrDefault();
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return query.Select(selector).FirstOrDefault();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <inheritdoc />
 | 
			
		||||
        public virtual async Task<TResult> GetFirstOrDefaultAsync<TResult>(Expression<Func<TEntity, TResult>> selector,
 | 
			
		||||
                                                  Expression<Func<TEntity, bool>> predicate = null,
 | 
			
		||||
                                                  Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
 | 
			
		||||
                                                  Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
 | 
			
		||||
                                                  bool disableTracking = true, bool ignoreQueryFilters = false)
 | 
			
		||||
        {
 | 
			
		||||
            IQueryable<TEntity> query = _dbSet;
 | 
			
		||||
 | 
			
		||||
            if (disableTracking)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.AsNoTracking();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (include != null)
 | 
			
		||||
            {
 | 
			
		||||
                query = include(query);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (predicate != null)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.Where(predicate);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (ignoreQueryFilters)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.IgnoreQueryFilters();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (orderBy != null)
 | 
			
		||||
            {
 | 
			
		||||
                return await orderBy(query).Select(selector).FirstOrDefaultAsync();
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return await query.Select(selector).FirstOrDefaultAsync();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Uses raw SQL queries to fetch the specified <typeparamref name="TEntity" /> data.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="sql">The raw SQL.</param>
 | 
			
		||||
        /// <param name="parameters">The parameters.</param>
 | 
			
		||||
        /// <returns>An <see cref="IQueryable{TEntity}" /> that contains elements that satisfy the condition specified by raw SQL.</returns>
 | 
			
		||||
        public virtual IQueryable<TEntity> FromSql(string sql, params object[] parameters) => _dbSet.FromSqlRaw(sql, parameters);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Finds an entity with the given primary key values. If found, is attached to the context and returned. If no entity is found, then null is returned.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="keyValues">The values of the primary key for the entity to be found.</param>
 | 
			
		||||
        /// <returns>The found entity or null.</returns>
 | 
			
		||||
        public virtual TEntity Find(params object[] keyValues) => _dbSet.Find(keyValues);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Finds an entity with the given primary key values. If found, is attached to the context and returned. If no entity is found, then null is returned.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="keyValues">The values of the primary key for the entity to be found.</param>
 | 
			
		||||
        /// <returns>A <see cref="Task{TEntity}" /> that represents the asynchronous insert operation.</returns>
 | 
			
		||||
        public virtual ValueTask<TEntity> FindAsync(params object[] keyValues) => _dbSet.FindAsync(keyValues);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Finds an entity with the given primary key values. If found, is attached to the context and returned. If no entity is found, then null is returned.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="keyValues">The values of the primary key for the entity to be found.</param>
 | 
			
		||||
        /// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete.</param>
 | 
			
		||||
        /// <returns>A <see cref="Task{TEntity}"/> that represents the asynchronous find operation. The task result contains the found entity or null.</returns>
 | 
			
		||||
        public virtual ValueTask<TEntity> FindAsync(object[] keyValues, CancellationToken cancellationToken) => _dbSet.FindAsync(keyValues, cancellationToken);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the count based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate"></param>
 | 
			
		||||
        /// <returns></returns>
 | 
			
		||||
        public virtual int Count(Expression<Func<TEntity, bool>> predicate = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (predicate == null)
 | 
			
		||||
            {
 | 
			
		||||
                return _dbSet.Count();
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return _dbSet.Count(predicate);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets async the count based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate"></param>
 | 
			
		||||
        /// <returns></returns>
 | 
			
		||||
        public virtual async Task<int> CountAsync(Expression<Func<TEntity, bool>> predicate = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (predicate == null)
 | 
			
		||||
            {
 | 
			
		||||
                return await _dbSet.CountAsync();
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return await _dbSet.CountAsync(predicate);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the long count based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate"></param>
 | 
			
		||||
        /// <returns></returns>
 | 
			
		||||
        public virtual long LongCount(Expression<Func<TEntity, bool>> predicate = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (predicate == null)
 | 
			
		||||
            {
 | 
			
		||||
                return _dbSet.LongCount();
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return _dbSet.LongCount(predicate);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets async the long count based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate"></param>
 | 
			
		||||
        /// <returns></returns>
 | 
			
		||||
        public virtual async Task<long> LongCountAsync(Expression<Func<TEntity, bool>> predicate = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (predicate == null)
 | 
			
		||||
            {
 | 
			
		||||
                return await _dbSet.LongCountAsync();
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return await _dbSet.LongCountAsync(predicate);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the max based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate"></param>
 | 
			
		||||
        ///  /// <param name="selector"></param>
 | 
			
		||||
        /// <returns>decimal</returns>
 | 
			
		||||
        public virtual T Max<T>(Expression<Func<TEntity, bool>> predicate = null, Expression<Func<TEntity, T>> selector = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (predicate == null)
 | 
			
		||||
                return _dbSet.Max(selector);
 | 
			
		||||
            else
 | 
			
		||||
                return _dbSet.Where(predicate).Max(selector);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the async max based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate"></param>
 | 
			
		||||
        ///  /// <param name="selector"></param>
 | 
			
		||||
        /// <returns>decimal</returns>
 | 
			
		||||
        public virtual async Task<T> MaxAsync<T>(Expression<Func<TEntity, bool>> predicate = null, Expression<Func<TEntity, T>> selector = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (predicate == null)
 | 
			
		||||
                return await _dbSet.MaxAsync(selector);
 | 
			
		||||
            else
 | 
			
		||||
                return await _dbSet.Where(predicate).MaxAsync(selector);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the min based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate"></param>
 | 
			
		||||
        ///  /// <param name="selector"></param>
 | 
			
		||||
        /// <returns>decimal</returns>
 | 
			
		||||
        public virtual T Min<T>(Expression<Func<TEntity, bool>> predicate = null, Expression<Func<TEntity, T>> selector = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (predicate == null)
 | 
			
		||||
                return _dbSet.Min(selector);
 | 
			
		||||
            else
 | 
			
		||||
                return _dbSet.Where(predicate).Min(selector);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the async min based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate"></param>
 | 
			
		||||
        ///  /// <param name="selector"></param>
 | 
			
		||||
        /// <returns>decimal</returns>
 | 
			
		||||
        public virtual async Task<T> MinAsync<T>(Expression<Func<TEntity, bool>> predicate = null, Expression<Func<TEntity, T>> selector = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (predicate == null)
 | 
			
		||||
                return await _dbSet.MinAsync(selector);
 | 
			
		||||
            else
 | 
			
		||||
                return await _dbSet.Where(predicate).MinAsync(selector);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the average based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate"></param>
 | 
			
		||||
        ///  /// <param name="selector"></param>
 | 
			
		||||
        /// <returns>decimal</returns>
 | 
			
		||||
        public virtual decimal Average(Expression<Func<TEntity, bool>> predicate = null, Expression<Func<TEntity, decimal>> selector = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (predicate == null)
 | 
			
		||||
                return _dbSet.Average(selector);
 | 
			
		||||
            else
 | 
			
		||||
                return _dbSet.Where(predicate).Average(selector);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the async average based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate"></param>
 | 
			
		||||
        ///  /// <param name="selector"></param>
 | 
			
		||||
        /// <returns>decimal</returns>
 | 
			
		||||
        public virtual async Task<decimal> AverageAsync(Expression<Func<TEntity, bool>> predicate = null, Expression<Func<TEntity, decimal>> selector = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (predicate == null)
 | 
			
		||||
                return await _dbSet.AverageAsync(selector);
 | 
			
		||||
            else
 | 
			
		||||
                return await _dbSet.Where(predicate).AverageAsync(selector);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the sum based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate"></param>
 | 
			
		||||
        ///  /// <param name="selector"></param>
 | 
			
		||||
        /// <returns>decimal</returns>
 | 
			
		||||
        public virtual decimal Sum(Expression<Func<TEntity, bool>> predicate = null, Expression<Func<TEntity, decimal>> selector = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (predicate == null)
 | 
			
		||||
                return _dbSet.Sum(selector);
 | 
			
		||||
            else
 | 
			
		||||
                return _dbSet.Where(predicate).Sum(selector);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the async sum based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate"></param>
 | 
			
		||||
        ///  /// <param name="selector"></param>
 | 
			
		||||
        /// <returns>decimal</returns>
 | 
			
		||||
        public virtual async Task<decimal> SumAsync(Expression<Func<TEntity, bool>> predicate = null, Expression<Func<TEntity, decimal>> selector = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (predicate == null)
 | 
			
		||||
                return await _dbSet.SumAsync(selector);
 | 
			
		||||
            else
 | 
			
		||||
                return await _dbSet.Where(predicate).SumAsync(selector);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the exists based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="selector"></param>
 | 
			
		||||
        /// <returns></returns>
 | 
			
		||||
        public bool Exists(Expression<Func<TEntity, bool>> selector = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (selector == null)
 | 
			
		||||
            {
 | 
			
		||||
                return _dbSet.Any();
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return _dbSet.Any(selector);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the async exists based on a predicate.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="selector"></param>
 | 
			
		||||
        /// <returns></returns>
 | 
			
		||||
        public async Task<bool> ExistsAsync(Expression<Func<TEntity, bool>> selector = null)
 | 
			
		||||
        {
 | 
			
		||||
            if (selector == null)
 | 
			
		||||
            {
 | 
			
		||||
                return await _dbSet.AnyAsync();
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return await _dbSet.AnyAsync(selector);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Inserts a new entity synchronously.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entity">The entity to insert.</param>
 | 
			
		||||
        public virtual TEntity Insert(TEntity entity)
 | 
			
		||||
        {
 | 
			
		||||
            return _dbSet.Add(entity).Entity;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Inserts a range of entities synchronously.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entities">The entities to insert.</param>
 | 
			
		||||
        public virtual void Insert(params TEntity[] entities) => _dbSet.AddRange(entities);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Inserts a range of entities synchronously.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entities">The entities to insert.</param>
 | 
			
		||||
        public virtual void Insert(IEnumerable<TEntity> entities) => _dbSet.AddRange(entities);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Inserts a new entity asynchronously.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entity">The entity to insert.</param>
 | 
			
		||||
        /// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete.</param>
 | 
			
		||||
        /// <returns>A <see cref="Task"/> that represents the asynchronous insert operation.</returns>
 | 
			
		||||
        public virtual ValueTask<EntityEntry<TEntity>> InsertAsync(TEntity entity, CancellationToken cancellationToken = default(CancellationToken))
 | 
			
		||||
        {
 | 
			
		||||
            return _dbSet.AddAsync(entity, cancellationToken);
 | 
			
		||||
 | 
			
		||||
            // Shadow properties?
 | 
			
		||||
            //var property = _dbContext.Entry(entity).Property("Created");
 | 
			
		||||
            //if (property != null) {
 | 
			
		||||
            //property.CurrentValue = DateTime.Now;
 | 
			
		||||
            //}
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Inserts a range of entities asynchronously.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entities">The entities to insert.</param>
 | 
			
		||||
        /// <returns>A <see cref="Task" /> that represents the asynchronous insert operation.</returns>
 | 
			
		||||
        public virtual Task InsertAsync(params TEntity[] entities) => _dbSet.AddRangeAsync(entities);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Inserts a range of entities asynchronously.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entities">The entities to insert.</param>
 | 
			
		||||
        /// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while waiting for the task to complete.</param>
 | 
			
		||||
        /// <returns>A <see cref="Task"/> that represents the asynchronous insert operation.</returns>
 | 
			
		||||
        public virtual Task InsertAsync(IEnumerable<TEntity> entities, CancellationToken cancellationToken = default(CancellationToken)) => _dbSet.AddRangeAsync(entities, cancellationToken);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Updates the specified entity.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entity">The entity.</param>
 | 
			
		||||
        public virtual void Update(TEntity entity)
 | 
			
		||||
        {
 | 
			
		||||
            _dbSet.Update(entity);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Updates the specified entity.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entity">The entity.</param>
 | 
			
		||||
        public virtual void UpdateAsync(TEntity entity)
 | 
			
		||||
        {
 | 
			
		||||
            _dbSet.Update(entity);
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Updates the specified entities.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entities">The entities.</param>
 | 
			
		||||
        public virtual void Update(params TEntity[] entities) => _dbSet.UpdateRange(entities);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Updates the specified entities.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entities">The entities.</param>
 | 
			
		||||
        public virtual void Update(IEnumerable<TEntity> entities) => _dbSet.UpdateRange(entities);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Deletes the specified entity.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entity">The entity to delete.</param>
 | 
			
		||||
        public virtual void Delete(TEntity entity) => _dbSet.Remove(entity);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Deletes the entity by the specified primary key.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="id">The primary key value.</param>
 | 
			
		||||
        public virtual void Delete(object id)
 | 
			
		||||
        {
 | 
			
		||||
            // using a stub entity to mark for deletion
 | 
			
		||||
            var typeInfo = typeof(TEntity).GetTypeInfo();
 | 
			
		||||
            var key = _dbContext.Model.FindEntityType(typeInfo).FindPrimaryKey().Properties.FirstOrDefault();
 | 
			
		||||
            var property = typeInfo.GetProperty(key?.Name);
 | 
			
		||||
            if (property != null)
 | 
			
		||||
            {
 | 
			
		||||
                var entity = Activator.CreateInstance<TEntity>();
 | 
			
		||||
                property.SetValue(entity, id);
 | 
			
		||||
                _dbContext.Entry(entity).State = EntityState.Deleted;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                var entity = _dbSet.Find(id);
 | 
			
		||||
                if (entity != null)
 | 
			
		||||
                {
 | 
			
		||||
                    Delete(entity);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Deletes the specified entities.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entities">The entities.</param>
 | 
			
		||||
        public virtual void Delete(params TEntity[] entities) => _dbSet.RemoveRange(entities);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Deletes the specified entities.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entities">The entities.</param>
 | 
			
		||||
        public virtual void Delete(IEnumerable<TEntity> entities) => _dbSet.RemoveRange(entities);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets all entities. This method is not recommended
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <returns>The <see cref="IQueryable{TEntity}"/>.</returns>
 | 
			
		||||
        public async Task<IList<TEntity>> GetAllAsync()
 | 
			
		||||
        {
 | 
			
		||||
            return await _dbSet.ToListAsync();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets all entities. This method is not recommended
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="predicate">A function to test each element for a condition.</param>
 | 
			
		||||
        /// <param name="orderBy">A function to order elements.</param>
 | 
			
		||||
        /// <param name="include">A function to include navigation properties</param>
 | 
			
		||||
        /// <param name="disableTracking"><c>true</c> to disable changing tracking; otherwise, <c>false</c>. Default to <c>true</c>.</param>
 | 
			
		||||
        /// <param name="ignoreQueryFilters">Ignore query filters</param>
 | 
			
		||||
        /// <returns>An <see cref="IPagedList{TEntity}"/> that contains elements that satisfy the condition specified by <paramref name="predicate"/>.</returns>
 | 
			
		||||
        /// <remarks>Ex: This method defaults to a read-only, no-tracking query.</remarks>
 | 
			
		||||
        public async Task<IList<TEntity>> GetAllAsync(Expression<Func<TEntity, bool>> predicate = null,
 | 
			
		||||
            Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
 | 
			
		||||
            Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null,
 | 
			
		||||
            bool disableTracking = true, bool ignoreQueryFilters = false)
 | 
			
		||||
        {
 | 
			
		||||
            IQueryable<TEntity> query = _dbSet;
 | 
			
		||||
 | 
			
		||||
            if (disableTracking)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.AsNoTracking();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (include != null)
 | 
			
		||||
            {
 | 
			
		||||
                query = include(query);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (predicate != null)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.Where(predicate);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (ignoreQueryFilters)
 | 
			
		||||
            {
 | 
			
		||||
                query = query.IgnoreQueryFilters();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (orderBy != null)
 | 
			
		||||
            {
 | 
			
		||||
                return await orderBy(query).ToListAsync();
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                return await query.ToListAsync();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Change entity state for patch method on web api.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="entity">The entity.</param>
 | 
			
		||||
        /// /// <param name="state">The entity state.</param>
 | 
			
		||||
        public void ChangeEntityState(TEntity entity, EntityState state)
 | 
			
		||||
        {
 | 
			
		||||
            _dbContext.Entry(entity).State = state;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										225
									
								
								Entities/Context/UnitOfWork/UnitOfWork.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								Entities/Context/UnitOfWork/UnitOfWork.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,225 @@
 | 
			
		||||
 | 
			
		||||
namespace SharedDATA.Api
 | 
			
		||||
{
 | 
			
		||||
    using System;
 | 
			
		||||
    using System.Collections.Generic;
 | 
			
		||||
    using System.Data;
 | 
			
		||||
    using System.Linq;
 | 
			
		||||
    using System.Text.RegularExpressions;
 | 
			
		||||
    using System.Threading.Tasks;
 | 
			
		||||
    using System.Transactions;
 | 
			
		||||
    using Microsoft.EntityFrameworkCore;
 | 
			
		||||
    using Microsoft.EntityFrameworkCore.ChangeTracking;
 | 
			
		||||
    using Microsoft.EntityFrameworkCore.Infrastructure;
 | 
			
		||||
    using Microsoft.EntityFrameworkCore.Metadata;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Represents the default implementation of the <see cref="IUnitOfWork"/> and <see cref="IUnitOfWork{TContext}"/> interface.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <typeparam name="TContext">The type of the db context.</typeparam>
 | 
			
		||||
    public class UnitOfWork<TContext> : IRepositoryFactory, IUnitOfWork<TContext>, IUnitOfWork where TContext : DbContext
 | 
			
		||||
    {
 | 
			
		||||
        private readonly TContext _context;
 | 
			
		||||
        private bool disposed = false;
 | 
			
		||||
        private Dictionary<Type, object> repositories;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Initializes a new instance of the <see cref="UnitOfWork{TContext}"/> class.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="context">The context.</param>
 | 
			
		||||
        public UnitOfWork(TContext context)
 | 
			
		||||
        {
 | 
			
		||||
            _context = context ?? throw new ArgumentNullException(nameof(context));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the db context.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <returns>The instance of type <typeparamref name="TContext"/>.</returns>
 | 
			
		||||
        public TContext DbContext => _context;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Changes the database name. This require the databases in the same machine. NOTE: This only work for MySQL right now.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="database">The database name.</param>
 | 
			
		||||
        /// <remarks>
 | 
			
		||||
        /// This only been used for supporting multiple databases in the same model. This require the databases in the same machine.
 | 
			
		||||
        /// </remarks>
 | 
			
		||||
        public void ChangeDatabase(string database)
 | 
			
		||||
        {
 | 
			
		||||
            var connection = _context.Database.GetDbConnection();
 | 
			
		||||
            if (connection.State.HasFlag(ConnectionState.Open))
 | 
			
		||||
            {
 | 
			
		||||
                connection.ChangeDatabase(database);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                var connectionString = Regex.Replace(connection.ConnectionString.Replace(" ", ""), @"(?<=[Dd]atabase=)\w+(?=;)", database, RegexOptions.Singleline);
 | 
			
		||||
                connection.ConnectionString = connectionString;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Following code only working for mysql.
 | 
			
		||||
            var items = _context.Model.GetEntityTypes();
 | 
			
		||||
            foreach (var item in items)
 | 
			
		||||
            {
 | 
			
		||||
                if (item is IConventionEntityType entityType)
 | 
			
		||||
                {
 | 
			
		||||
                    entityType.SetSchema(database);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the db context.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <returns></returns>
 | 
			
		||||
        public TDbContext GetDbContext<TDbContext>() where TDbContext : DbContext
 | 
			
		||||
        {
 | 
			
		||||
            if (_context == null)
 | 
			
		||||
                return null;
 | 
			
		||||
            return _context as TDbContext;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets the specified repository for the <typeparamref name="TEntity"/>.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="hasCustomRepository"><c>True</c> if providing custom repositry</param>
 | 
			
		||||
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
 | 
			
		||||
        /// <returns>An instance of type inherited from <see cref="IRepository{TEntity}"/> interface.</returns>
 | 
			
		||||
        public IRepository<TEntity> GetRepository<TEntity>(bool hasCustomRepository = false) where TEntity : class
 | 
			
		||||
        {
 | 
			
		||||
            if (repositories == null)
 | 
			
		||||
            {
 | 
			
		||||
                repositories = new Dictionary<Type, object>();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // what's the best way to support custom reposity?
 | 
			
		||||
            if (hasCustomRepository)
 | 
			
		||||
            {
 | 
			
		||||
                var customRepo = _context.GetService<IRepository<TEntity>>();
 | 
			
		||||
                if (customRepo != null)
 | 
			
		||||
                {
 | 
			
		||||
                    return customRepo;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var type = typeof(TEntity);
 | 
			
		||||
            if (!repositories.ContainsKey(type))
 | 
			
		||||
            {
 | 
			
		||||
                repositories[type] = new Repository<TEntity>(_context);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return (IRepository<TEntity>)repositories[type];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Executes the specified raw SQL command.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="sql">The raw SQL.</param>
 | 
			
		||||
        /// <param name="parameters">The parameters.</param>
 | 
			
		||||
        /// <returns>The number of state entities written to database.</returns>
 | 
			
		||||
        public int ExecuteSqlCommand(string sql, params object[] parameters) => _context.Database.ExecuteSqlRaw(sql, parameters);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Uses raw SQL queries to fetch the specified <typeparamref name="TEntity" /> data.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
 | 
			
		||||
        /// <param name="sql">The raw SQL.</param>
 | 
			
		||||
        /// <param name="parameters">The parameters.</param>
 | 
			
		||||
        /// <returns>An <see cref="IQueryable{T}" /> that contains elements that satisfy the condition specified by raw SQL.</returns>
 | 
			
		||||
        public IQueryable<TEntity> FromSql<TEntity>(string sql, params object[] parameters) where TEntity : class => _context.Set<TEntity>().FromSqlRaw(sql, parameters);
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Saves all changes made in this context to the database.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="ensureAutoHistory"><c>True</c> if save changes ensure auto record the change history.</param>
 | 
			
		||||
        /// <returns>The number of state entries written to the database.</returns>
 | 
			
		||||
        public int SaveChanges(bool ensureAutoHistory = false)
 | 
			
		||||
        {
 | 
			
		||||
            if (ensureAutoHistory)
 | 
			
		||||
            {
 | 
			
		||||
				_context.EnsureAutoHistory();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
            return _context.SaveChanges();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Asynchronously saves all changes made in this unit of work to the database.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="ensureAutoHistory"><c>True</c> if save changes ensure auto record the change history.</param>
 | 
			
		||||
        /// <returns>A <see cref="Task{TResult}"/> that represents the asynchronous save operation. The task result contains the number of state entities written to database.</returns>
 | 
			
		||||
        public async Task<int> SaveChangesAsync(bool ensureAutoHistory = false)
 | 
			
		||||
        {
 | 
			
		||||
            if (ensureAutoHistory)
 | 
			
		||||
            {
 | 
			
		||||
                _context.EnsureAutoHistory();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return await _context.SaveChangesAsync();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Saves all changes made in this context to the database with distributed transaction.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="ensureAutoHistory"><c>True</c> if save changes ensure auto record the change history.</param>
 | 
			
		||||
        /// <param name="unitOfWorks">An optional <see cref="IUnitOfWork"/> array.</param>
 | 
			
		||||
        /// <returns>A <see cref="Task{TResult}"/> that represents the asynchronous save operation. The task result contains the number of state entities written to database.</returns>
 | 
			
		||||
        public async Task<int> SaveChangesAsync(bool ensureAutoHistory = false, params IUnitOfWork[] unitOfWorks)
 | 
			
		||||
        {
 | 
			
		||||
            using (var ts = new TransactionScope())
 | 
			
		||||
            {
 | 
			
		||||
                var count = 0;
 | 
			
		||||
                foreach (var unitOfWork in unitOfWorks)
 | 
			
		||||
                {
 | 
			
		||||
                    count += await unitOfWork.SaveChangesAsync(ensureAutoHistory);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                count += await SaveChangesAsync(ensureAutoHistory);
 | 
			
		||||
 | 
			
		||||
                ts.Complete();
 | 
			
		||||
 | 
			
		||||
                return count;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public void Dispose()
 | 
			
		||||
        {
 | 
			
		||||
            Dispose(true);
 | 
			
		||||
 | 
			
		||||
            GC.SuppressFinalize(this);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="disposing">The disposing.</param>
 | 
			
		||||
        protected virtual void Dispose(bool disposing)
 | 
			
		||||
        {
 | 
			
		||||
            if (!disposed)
 | 
			
		||||
            {
 | 
			
		||||
                if (disposing)
 | 
			
		||||
                {
 | 
			
		||||
                    // clear repositories
 | 
			
		||||
                    if (repositories != null)
 | 
			
		||||
                    {
 | 
			
		||||
                        repositories.Clear();
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // dispose the db context.
 | 
			
		||||
                    _context.Dispose();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            disposed = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void TrackGraph(object rootEntity, Action<EntityEntryGraphNode> callback)
 | 
			
		||||
        {
 | 
			
		||||
            _context.ChangeTracker.TrackGraph(rootEntity, callback);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,118 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
namespace SharedDATA.Api
 | 
			
		||||
{
 | 
			
		||||
    using Microsoft.EntityFrameworkCore;
 | 
			
		||||
    using Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Extension methods for setting up unit of work related services in an <see cref="IServiceCollection"/>.
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public static class UnitOfWorkServiceCollectionExtensions
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Registers the unit of work given context as a service in the <see cref="IServiceCollection"/>.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <typeparam name="TContext">The type of the db context.</typeparam>
 | 
			
		||||
        /// <param name="services">The <see cref="IServiceCollection"/> to add services to.</param>
 | 
			
		||||
        /// <returns>The same service collection so that multiple calls can be chained.</returns>
 | 
			
		||||
        /// <remarks>
 | 
			
		||||
        /// This method only support one db context, if been called more than once, will throw exception.
 | 
			
		||||
        /// </remarks>
 | 
			
		||||
        public static IServiceCollection AddUnitOfWork<TContext>(this IServiceCollection services) where TContext : DbContext
 | 
			
		||||
        {
 | 
			
		||||
            services.AddScoped<IRepositoryFactory, UnitOfWork<TContext>>();
 | 
			
		||||
            // Following has a issue: IUnitOfWork cannot support multiple dbcontext/database, 
 | 
			
		||||
            // that means cannot call AddUnitOfWork<TContext> multiple times.
 | 
			
		||||
            // Solution: check IUnitOfWork whether or null
 | 
			
		||||
            services.AddScoped<IUnitOfWork, UnitOfWork<TContext>>();
 | 
			
		||||
            services.AddScoped<IUnitOfWork<TContext>, UnitOfWork<TContext>>();
 | 
			
		||||
 | 
			
		||||
            return services;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Registers the unit of work given context as a service in the <see cref="IServiceCollection"/>.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <typeparam name="TContext1">The type of the db context.</typeparam>
 | 
			
		||||
        /// <typeparam name="TContext2">The type of the db context.</typeparam>
 | 
			
		||||
        /// <param name="services">The <see cref="IServiceCollection"/> to add services to.</param>
 | 
			
		||||
        /// <returns>The same service collection so that multiple calls can be chained.</returns>
 | 
			
		||||
        /// <remarks>
 | 
			
		||||
        /// This method only support one db context, if been called more than once, will throw exception.
 | 
			
		||||
        /// </remarks>
 | 
			
		||||
        public static IServiceCollection AddUnitOfWork<TContext1, TContext2>(this IServiceCollection services)
 | 
			
		||||
            where TContext1 : DbContext
 | 
			
		||||
            where TContext2 : DbContext
 | 
			
		||||
        {
 | 
			
		||||
            services.AddScoped<IUnitOfWork<TContext1>, UnitOfWork<TContext1>>();
 | 
			
		||||
            services.AddScoped<IUnitOfWork<TContext2>, UnitOfWork<TContext2>>();
 | 
			
		||||
 | 
			
		||||
            return services;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Registers the unit of work given context as a service in the <see cref="IServiceCollection"/>.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <typeparam name="TContext1">The type of the db context.</typeparam>
 | 
			
		||||
        /// <typeparam name="TContext2">The type of the db context.</typeparam>
 | 
			
		||||
        /// <typeparam name="TContext3">The type of the db context.</typeparam>
 | 
			
		||||
        /// <param name="services">The <see cref="IServiceCollection"/> to add services to.</param>
 | 
			
		||||
        /// <returns>The same service collection so that multiple calls can be chained.</returns>
 | 
			
		||||
        /// <remarks>
 | 
			
		||||
        /// This method only support one db context, if been called more than once, will throw exception.
 | 
			
		||||
        /// </remarks>
 | 
			
		||||
        public static IServiceCollection AddUnitOfWork<TContext1, TContext2, TContext3>(this IServiceCollection services)
 | 
			
		||||
            where TContext1 : DbContext
 | 
			
		||||
            where TContext2 : DbContext
 | 
			
		||||
            where TContext3 : DbContext
 | 
			
		||||
        {
 | 
			
		||||
            services.AddScoped<IUnitOfWork<TContext1>, UnitOfWork<TContext1>>();
 | 
			
		||||
            services.AddScoped<IUnitOfWork<TContext2>, UnitOfWork<TContext2>>();
 | 
			
		||||
            services.AddScoped<IUnitOfWork<TContext3>, UnitOfWork<TContext3>>();
 | 
			
		||||
 | 
			
		||||
            return services;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Registers the unit of work given context as a service in the <see cref="IServiceCollection"/>.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <typeparam name="TContext1">The type of the db context.</typeparam>
 | 
			
		||||
        /// <typeparam name="TContext2">The type of the db context.</typeparam>
 | 
			
		||||
        /// <typeparam name="TContext3">The type of the db context.</typeparam>
 | 
			
		||||
        /// <typeparam name="TContext4">The type of the db context.</typeparam>
 | 
			
		||||
        /// <param name="services">The <see cref="IServiceCollection"/> to add services to.</param>
 | 
			
		||||
        /// <returns>The same service collection so that multiple calls can be chained.</returns>
 | 
			
		||||
        /// <remarks>
 | 
			
		||||
        /// This method only support one db context, if been called more than once, will throw exception.
 | 
			
		||||
        /// </remarks>
 | 
			
		||||
        public static IServiceCollection AddUnitOfWork<TContext1, TContext2, TContext3, TContext4>(this IServiceCollection services)
 | 
			
		||||
            where TContext1 : DbContext
 | 
			
		||||
            where TContext2 : DbContext
 | 
			
		||||
            where TContext3 : DbContext
 | 
			
		||||
            where TContext4 : DbContext
 | 
			
		||||
        {
 | 
			
		||||
            services.AddScoped<IUnitOfWork<TContext1>, UnitOfWork<TContext1>>();
 | 
			
		||||
            services.AddScoped<IUnitOfWork<TContext2>, UnitOfWork<TContext2>>();
 | 
			
		||||
            services.AddScoped<IUnitOfWork<TContext3>, UnitOfWork<TContext3>>();
 | 
			
		||||
            services.AddScoped<IUnitOfWork<TContext4>, UnitOfWork<TContext4>>();
 | 
			
		||||
 | 
			
		||||
            return services;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Registers the custom repository as a service in the <see cref="IServiceCollection"/>.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
 | 
			
		||||
        /// <typeparam name="TRepository">The type of the custom repositry.</typeparam>
 | 
			
		||||
        /// <param name="services">The <see cref="IServiceCollection"/> to add services to.</param>
 | 
			
		||||
        /// <returns>The same service collection so that multiple calls can be chained.</returns>
 | 
			
		||||
        public static IServiceCollection AddCustomRepository<TEntity, TRepository>(this IServiceCollection services)
 | 
			
		||||
            where TEntity : class
 | 
			
		||||
            where TRepository : class, IRepository<TEntity>
 | 
			
		||||
        {
 | 
			
		||||
            services.AddScoped<IRepository<TEntity>, TRepository>();
 | 
			
		||||
            return services;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										63
									
								
								Entities/Contracts/Assignment.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								Entities/Contracts/Assignment.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.ComponentModel.DataAnnotations.Schema;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Entities.Contracts
 | 
			
		||||
{
 | 
			
		||||
	[Table("assignments")]
 | 
			
		||||
	public class Assignment
 | 
			
		||||
	{
 | 
			
		||||
		[Key]
 | 
			
		||||
		[Column("id")]
 | 
			
		||||
		public Guid Id { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Required]
 | 
			
		||||
		[Column("title")]
 | 
			
		||||
		[StringLength(255)]
 | 
			
		||||
		public string Title { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("description")]
 | 
			
		||||
		public string Description { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Required]
 | 
			
		||||
		[Column("due_date")]
 | 
			
		||||
		public DateTime DueDate { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("total_points")]
 | 
			
		||||
		public decimal? TotalPoints { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("created_by")]
 | 
			
		||||
		[ForeignKey("Creator")]
 | 
			
		||||
		public Guid CreatedBy { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("created_at")]
 | 
			
		||||
		public DateTime CreatedAt { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("updated_at")]
 | 
			
		||||
		public DateTime UpdatedAt { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("deleted")]
 | 
			
		||||
		public bool IsDeleted { get; set; }
 | 
			
		||||
 | 
			
		||||
		// Navigation Properties
 | 
			
		||||
		public User Creator { get; set; }
 | 
			
		||||
		public ICollection<AssignmentClass> AssignmentClasses { get; set; }
 | 
			
		||||
		public ICollection<AssignmentGroup> AssignmentGroups { get; set; }
 | 
			
		||||
		public ICollection<AssignmentAttachment> AssignmentAttachments { get; set; }
 | 
			
		||||
		public ICollection<Submission> Submissions { get; set; }
 | 
			
		||||
 | 
			
		||||
		public Assignment()
 | 
			
		||||
		{
 | 
			
		||||
			Id = Guid.NewGuid();
 | 
			
		||||
 | 
			
		||||
			Submissions = new HashSet<Submission>();
 | 
			
		||||
			AssignmentGroups = new HashSet<AssignmentGroup>();
 | 
			
		||||
			AssignmentClasses = new HashSet<AssignmentClass>();
 | 
			
		||||
			AssignmentAttachments = new HashSet<AssignmentAttachment>();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										49
									
								
								Entities/Contracts/AssignmentAttachment.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								Entities/Contracts/AssignmentAttachment.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.ComponentModel.DataAnnotations.Schema;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using System.Diagnostics.CodeAnalysis;
 | 
			
		||||
 | 
			
		||||
namespace Entities.Contracts
 | 
			
		||||
{
 | 
			
		||||
	[Table("assignment_attachments")]
 | 
			
		||||
	public class AssignmentAttachment
 | 
			
		||||
	{
 | 
			
		||||
		[Key]
 | 
			
		||||
		[Column("id")]
 | 
			
		||||
		public Guid Id { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Required]
 | 
			
		||||
		[Column("assignment_id")]
 | 
			
		||||
		[ForeignKey("Assignment")]
 | 
			
		||||
		public Guid AssignmentId { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Required]
 | 
			
		||||
		[Column("file_path")]
 | 
			
		||||
		[StringLength(255)]
 | 
			
		||||
		public string FilePath { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Required]
 | 
			
		||||
		[Column("file_name")]
 | 
			
		||||
		[StringLength(255)]
 | 
			
		||||
		public string FileName { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("uploaded_at")]
 | 
			
		||||
		public DateTime UploadedAt { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("deleted")]
 | 
			
		||||
		public bool IsDeleted { get; set; }
 | 
			
		||||
 | 
			
		||||
		// Navigation Properties
 | 
			
		||||
		public Assignment Assignment { get; set; }
 | 
			
		||||
 | 
			
		||||
		public AssignmentAttachment()
 | 
			
		||||
		{
 | 
			
		||||
			Id = Guid.NewGuid();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										36
									
								
								Entities/Contracts/AssignmentClass.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								Entities/Contracts/AssignmentClass.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.ComponentModel.DataAnnotations.Schema;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Entities.Contracts
 | 
			
		||||
{
 | 
			
		||||
	[Table("assignment_class")]
 | 
			
		||||
	public class AssignmentClass
 | 
			
		||||
	{
 | 
			
		||||
		[Key]
 | 
			
		||||
		[Column("assignment_id", Order = 0)]
 | 
			
		||||
		[ForeignKey("Assignment")]
 | 
			
		||||
		public Guid AssignmentId { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Key]
 | 
			
		||||
		[Column("class_id", Order = 1)]
 | 
			
		||||
		[ForeignKey("Class")]
 | 
			
		||||
		public Guid ClassId { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("assigned_at")]
 | 
			
		||||
		public DateTime AssignedAt { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("deleted")]
 | 
			
		||||
		public bool IsDeleted { get; set; }
 | 
			
		||||
 | 
			
		||||
		// Navigation Properties
 | 
			
		||||
		public Assignment Assignment { get; set; }
 | 
			
		||||
		public Class Class { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										59
									
								
								Entities/Contracts/AssignmentGroup.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								Entities/Contracts/AssignmentGroup.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,59 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.ComponentModel.DataAnnotations.Schema;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Entities.Contracts
 | 
			
		||||
{
 | 
			
		||||
	[Table("assignment_group")]
 | 
			
		||||
	public class AssignmentGroup
 | 
			
		||||
	{
 | 
			
		||||
		[Key]
 | 
			
		||||
		[Column("id")]
 | 
			
		||||
		public Guid Id { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Required]
 | 
			
		||||
		[Column("assignment")]
 | 
			
		||||
		[ForeignKey("Assignment")]
 | 
			
		||||
		public Guid AssignmentId { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Required]
 | 
			
		||||
		[Column("title")]
 | 
			
		||||
		[MaxLength(65535)] 
 | 
			
		||||
		public string Title { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("descript")]
 | 
			
		||||
		[MaxLength(65535)] 
 | 
			
		||||
		public string Descript { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		[Column("total_points")]
 | 
			
		||||
		public decimal? TotalPoints { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("number")]
 | 
			
		||||
		public byte Number { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("parent_group")]
 | 
			
		||||
		public Guid? ParentGroup { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("deleted")]
 | 
			
		||||
		public bool IsDeleted { get; set; }
 | 
			
		||||
 | 
			
		||||
		// Navigation Properties
 | 
			
		||||
		public Assignment Assignment { get; set; }
 | 
			
		||||
		public AssignmentGroup ParentAssignmentGroup { get; set;}
 | 
			
		||||
		public ICollection<AssignmentGroup> ChildAssignmentGroups { get; set; }
 | 
			
		||||
		public ICollection<AssignmentQuestion> AssignmentQuestions { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		public AssignmentGroup()
 | 
			
		||||
		{
 | 
			
		||||
			Id = Guid.NewGuid();
 | 
			
		||||
			ChildAssignmentGroups = new HashSet<AssignmentGroup>();
 | 
			
		||||
			AssignmentQuestions = new HashSet<AssignmentQuestion>();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										50
									
								
								Entities/Contracts/AssignmentQuestion.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								Entities/Contracts/AssignmentQuestion.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.ComponentModel.DataAnnotations.Schema;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Entities.Contracts
 | 
			
		||||
{
 | 
			
		||||
	[Table("assignment_questions")]
 | 
			
		||||
	public class AssignmentQuestion
 | 
			
		||||
	{
 | 
			
		||||
		[Key]
 | 
			
		||||
		[Column("id")]
 | 
			
		||||
		public Guid Id { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Required]
 | 
			
		||||
		[Column("question_id")]
 | 
			
		||||
		[ForeignKey("Question")]
 | 
			
		||||
		public Guid QuestionId { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Required]
 | 
			
		||||
		[Column("question_number")]
 | 
			
		||||
		public uint QuestionNumber { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("created_at")]
 | 
			
		||||
		public DateTime CreatedAt { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Required]
 | 
			
		||||
		[Column("detail_id")]
 | 
			
		||||
		[ForeignKey("AssignmentGroup")]
 | 
			
		||||
		public Guid AssignmentGroupId { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("deleted")]
 | 
			
		||||
		public bool IsDeleted { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		public Question Question { get; set; }
 | 
			
		||||
		public ICollection<SubmissionDetail> SubmissionDetails { get; set; }
 | 
			
		||||
		public AssignmentGroup AssignmentGroup { get; set; }
 | 
			
		||||
 | 
			
		||||
		public AssignmentQuestion()
 | 
			
		||||
		{
 | 
			
		||||
			Id = Guid.NewGuid();
 | 
			
		||||
			SubmissionDetails = new HashSet<SubmissionDetail>();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										55
									
								
								Entities/Contracts/Class.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								Entities/Contracts/Class.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.ComponentModel.DataAnnotations.Schema;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Entities.Contracts
 | 
			
		||||
{
 | 
			
		||||
	[Table("classes")]
 | 
			
		||||
	public class Class
 | 
			
		||||
	{
 | 
			
		||||
		[Key]
 | 
			
		||||
		[Column("id")]
 | 
			
		||||
		public Guid Id { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("grade")]
 | 
			
		||||
		public byte Grade { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("class")]
 | 
			
		||||
		public byte Number { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("description")]
 | 
			
		||||
		public string Description { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("head_teacher_id")]
 | 
			
		||||
		public Guid? HeadTeacherId { get; set; }
 | 
			
		||||
		public User HeadTeacher { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("created_at")]
 | 
			
		||||
		public DateTime CreatedAt { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("updated_at")]
 | 
			
		||||
		public DateTime UpdatedAt { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("deleted")]
 | 
			
		||||
		public bool IsDeleted { get; set; }
 | 
			
		||||
 | 
			
		||||
		// Navigation Properties
 | 
			
		||||
		public ICollection<ClassTeacher> ClassTeachers { get; set; }
 | 
			
		||||
		public ICollection<ClassStudent> ClassStudents { get; set; }
 | 
			
		||||
		public ICollection<AssignmentClass> AssignmentClasses { get; set; }
 | 
			
		||||
 | 
			
		||||
		public Class()
 | 
			
		||||
		{
 | 
			
		||||
			Id = Guid.NewGuid();
 | 
			
		||||
			Grade = 0;
 | 
			
		||||
			Number = 0;
 | 
			
		||||
			ClassStudents = new HashSet<ClassStudent>();
 | 
			
		||||
			ClassTeachers = new HashSet<ClassTeacher>();
 | 
			
		||||
			AssignmentClasses = new HashSet<AssignmentClass>();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										34
									
								
								Entities/Contracts/ClassStudent.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								Entities/Contracts/ClassStudent.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.ComponentModel.DataAnnotations.Schema;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Entities.Contracts
 | 
			
		||||
{
 | 
			
		||||
	[Table("class_student")]
 | 
			
		||||
	public class ClassStudent
 | 
			
		||||
	{
 | 
			
		||||
		[Key]
 | 
			
		||||
		[Column("class_id", Order = 0)]
 | 
			
		||||
		[ForeignKey("Class")]
 | 
			
		||||
		public Guid ClassId { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Key]
 | 
			
		||||
		[Column("student_id", Order = 1)]
 | 
			
		||||
		[ForeignKey("Student")]
 | 
			
		||||
		public Guid StudentId { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("enrollment_date")]
 | 
			
		||||
		public DateTime EnrollmentDate { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("deleted")]
 | 
			
		||||
		public bool IsDeleted { get; set; }
 | 
			
		||||
 | 
			
		||||
		// Navigation Properties
 | 
			
		||||
		public Class Class { get; set; }
 | 
			
		||||
		public User Student { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								Entities/Contracts/ClassTeacher.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								Entities/Contracts/ClassTeacher.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.ComponentModel.DataAnnotations.Schema;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Entities.Contracts
 | 
			
		||||
{
 | 
			
		||||
	[Table("class_teachers")]
 | 
			
		||||
	public class ClassTeacher
 | 
			
		||||
	{
 | 
			
		||||
		[Key] 
 | 
			
		||||
		[Column("class_id")]
 | 
			
		||||
		public Guid ClassId { get; set; }
 | 
			
		||||
		public Class Class { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Key] 
 | 
			
		||||
		[Column("teacher_id")] 
 | 
			
		||||
		public Guid TeacherId { get; set; }
 | 
			
		||||
		public User Teacher { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("subject_taught")]
 | 
			
		||||
		public string SubjectTaught { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										99
									
								
								Entities/Contracts/Question.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								Entities/Contracts/Question.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,99 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.ComponentModel.DataAnnotations.Schema;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Entities.Contracts
 | 
			
		||||
{
 | 
			
		||||
	[Table("questions")]
 | 
			
		||||
	public class Question
 | 
			
		||||
	{
 | 
			
		||||
		[Key]
 | 
			
		||||
		[Column("id")]
 | 
			
		||||
		public Guid Id { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Required]
 | 
			
		||||
		[Column("question_text")]
 | 
			
		||||
		[MaxLength(65535)]
 | 
			
		||||
		public string QuestionText { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Required]
 | 
			
		||||
		[Column("question_type")]
 | 
			
		||||
		[MaxLength(20)]
 | 
			
		||||
		public QuestionType QuestionType { get; set; } 
 | 
			
		||||
 | 
			
		||||
		[Column("correct_answer")]
 | 
			
		||||
		[MaxLength(65535)]
 | 
			
		||||
		public string CorrectAnswer { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("difficulty_level")]
 | 
			
		||||
		[MaxLength(10)] 
 | 
			
		||||
		public DifficultyLevel DifficultyLevel { get; set; } 
 | 
			
		||||
 | 
			
		||||
		[Column("subject_area")]
 | 
			
		||||
		public SubjectAreaEnum SubjectArea { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Required]
 | 
			
		||||
		[Column("created_by")]
 | 
			
		||||
		[ForeignKey("Creator")]
 | 
			
		||||
		public Guid CreatedBy { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("created_at")]
 | 
			
		||||
		public DateTime CreatedAt { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("updated_at")]
 | 
			
		||||
		public DateTime UpdatedAt { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("deleted")]
 | 
			
		||||
		public bool IsDeleted { get; set; }
 | 
			
		||||
 | 
			
		||||
		// Navigation Properties
 | 
			
		||||
		public User Creator { get; set; }
 | 
			
		||||
		public ICollection<AssignmentQuestion> AssignmentQuestions { get; set; }
 | 
			
		||||
 | 
			
		||||
		public Question()
 | 
			
		||||
		{
 | 
			
		||||
			Id = Guid.NewGuid();
 | 
			
		||||
			AssignmentQuestions = new HashSet<AssignmentQuestion>();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public enum DifficultyLevel
 | 
			
		||||
	{
 | 
			
		||||
		easy,
 | 
			
		||||
		medium,
 | 
			
		||||
		hard
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public enum QuestionType
 | 
			
		||||
	{
 | 
			
		||||
		Unknown, // 可以有一个未知类型或作为默认
 | 
			
		||||
		Spelling,           // 拼写
 | 
			
		||||
		Pronunciation,      // 给带点字选择正确读音
 | 
			
		||||
		WordFormation,      // 组词
 | 
			
		||||
		FillInTheBlanks,    // 选词填空 / 补充词语
 | 
			
		||||
		SentenceDictation,  // 默写句子
 | 
			
		||||
		SentenceRewriting,  // 仿句 / 改写句子
 | 
			
		||||
		ReadingComprehension, // 阅读理解
 | 
			
		||||
		Composition         // 作文
 | 
			
		||||
							// ... 添加您其他题目类型
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public enum SubjectAreaEnum // 建议命名为 SubjectAreaEnum 以避免与属性名冲突
 | 
			
		||||
	{
 | 
			
		||||
		Unknown,         // 未知或默认
 | 
			
		||||
		Mathematics,     // 数学
 | 
			
		||||
		Physics,         // 物理
 | 
			
		||||
		Chemistry,       // 化学
 | 
			
		||||
		Biology,         // 生物
 | 
			
		||||
		History,         // 历史
 | 
			
		||||
		Geography,       // 地理
 | 
			
		||||
		Literature,      // 语文/文学
 | 
			
		||||
		English,         // 英语
 | 
			
		||||
		ComputerScience, // 计算机科学
 | 
			
		||||
						 // ... 你可以根据需要添加更多科目
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										80
									
								
								Entities/Contracts/Submission.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								Entities/Contracts/Submission.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,80 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.ComponentModel.DataAnnotations.Schema;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
 | 
			
		||||
namespace Entities.Contracts
 | 
			
		||||
{
 | 
			
		||||
	[Table("submissions")]
 | 
			
		||||
	public class Submission
 | 
			
		||||
	{
 | 
			
		||||
		[Key]
 | 
			
		||||
		[Column("id")]
 | 
			
		||||
		public Guid Id { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Required]
 | 
			
		||||
		[Column("assignment_id")]
 | 
			
		||||
		[ForeignKey("Assignment")]
 | 
			
		||||
		public Guid AssignmentId { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Required]
 | 
			
		||||
		[Column("student_id")]
 | 
			
		||||
		[ForeignKey("Student")]
 | 
			
		||||
		public Guid StudentId { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Required]
 | 
			
		||||
		[Column("attempt_number")]
 | 
			
		||||
		public Guid AttemptNumber { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("submission_time")]
 | 
			
		||||
		public DateTime SubmissionTime { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("overall_grade")]
 | 
			
		||||
		[Precision(5, 2)]
 | 
			
		||||
		public decimal? OverallGrade { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("overall_feedback")]
 | 
			
		||||
		public string OverallFeedback { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("graded_by")]
 | 
			
		||||
		[ForeignKey("Grader")]
 | 
			
		||||
		public Guid? GradedBy { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("graded_at")]
 | 
			
		||||
		public DateTime? GradedAt { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("deleted")]
 | 
			
		||||
		public bool IsDeleted { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Required]
 | 
			
		||||
		[Column("status")]
 | 
			
		||||
		public SubmissionStatus Status { get; set; }
 | 
			
		||||
 | 
			
		||||
		// Navigation Properties
 | 
			
		||||
		public Assignment Assignment { get; set; }
 | 
			
		||||
		public User Student { get; set; }
 | 
			
		||||
		public User Grader { get; set; }
 | 
			
		||||
		public ICollection<SubmissionDetail> SubmissionDetails { get; set; }
 | 
			
		||||
 | 
			
		||||
		public Submission()
 | 
			
		||||
		{
 | 
			
		||||
			Id = Guid.NewGuid();
 | 
			
		||||
			SubmissionDetails = new HashSet<SubmissionDetail>();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public enum SubmissionStatus
 | 
			
		||||
	{
 | 
			
		||||
		Pending,        // 待提交/未开始
 | 
			
		||||
		Submitted,      // 已提交
 | 
			
		||||
		Graded,         // 已批改
 | 
			
		||||
		Resubmission,   // 待重新提交 (如果允许)
 | 
			
		||||
		Late,           // 迟交
 | 
			
		||||
		Draft,          // 草稿
 | 
			
		||||
						// ... 添加你需要的其他状态
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										66
									
								
								Entities/Contracts/SubmissionDetail.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								Entities/Contracts/SubmissionDetail.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.ComponentModel.DataAnnotations.Schema;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Entities.Contracts
 | 
			
		||||
{
 | 
			
		||||
	[Table("submission_details")]
 | 
			
		||||
	public class SubmissionDetail
 | 
			
		||||
	{
 | 
			
		||||
		[Key]
 | 
			
		||||
		[Column("id")]
 | 
			
		||||
		public Guid Id { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Required]
 | 
			
		||||
		[Column("submission_id")]
 | 
			
		||||
		[ForeignKey("Submission")]
 | 
			
		||||
		public Guid SubmissionId { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Required]
 | 
			
		||||
		[Column("student_id")]
 | 
			
		||||
		[ForeignKey("User")]
 | 
			
		||||
		public Guid StudentId { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Required]
 | 
			
		||||
		[Column("assignment_question_id")]
 | 
			
		||||
		[ForeignKey("AssignmentQuestion")]
 | 
			
		||||
		public Guid AssignmentQuestionId { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("student_answer")]
 | 
			
		||||
		public string StudentAnswer { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("is_correct")]
 | 
			
		||||
		public bool? IsCorrect { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("points_awarded")]
 | 
			
		||||
		[Precision(5, 2)]
 | 
			
		||||
		public decimal? PointsAwarded { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("teacher_feedback")]
 | 
			
		||||
		public string TeacherFeedback { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("created_at")]
 | 
			
		||||
		public DateTime CreatedAt { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("updated_at")]
 | 
			
		||||
		public DateTime UpdatedAt { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Column("deleted")]
 | 
			
		||||
		public bool IsDeleted { get; set; }
 | 
			
		||||
 | 
			
		||||
		// Navigation Properties
 | 
			
		||||
		public Submission Submission { get; set; }
 | 
			
		||||
		public User User { get; set; }
 | 
			
		||||
		public AssignmentQuestion AssignmentQuestion { get; set; }
 | 
			
		||||
 | 
			
		||||
		public SubmissionDetail()
 | 
			
		||||
		{
 | 
			
		||||
			Id = Guid.NewGuid();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										47
									
								
								Entities/Contracts/User.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								Entities/Contracts/User.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
using Microsoft.AspNetCore.Identity;
 | 
			
		||||
using System.ComponentModel.DataAnnotations.Schema;
 | 
			
		||||
 | 
			
		||||
namespace Entities.Contracts
 | 
			
		||||
{
 | 
			
		||||
	public enum UserRoles 
 | 
			
		||||
	{
 | 
			
		||||
		Student,
 | 
			
		||||
		Teacher,
 | 
			
		||||
		Administrator
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public class User : IdentityUser<Guid>
 | 
			
		||||
	{
 | 
			
		||||
		public string? RefreshToken { get; set; }
 | 
			
		||||
		public DateTime? RefreshTokenExpiryTime { get; set; }
 | 
			
		||||
		public string? Address { get; set; }	
 | 
			
		||||
		public string? DisplayName { get; set; }	
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		[Column("deleted")]
 | 
			
		||||
		public bool IsDeleted { get; set; }
 | 
			
		||||
 | 
			
		||||
		public ICollection<ClassTeacher> TaughtClassesLink { get; set; }
 | 
			
		||||
		public ICollection<ClassStudent> EnrolledClassesLink { get; set; }
 | 
			
		||||
 | 
			
		||||
		public ICollection<Question> CreatedQuestions { get; set; }
 | 
			
		||||
		public ICollection<Assignment> CreatedAssignments { get; set; }
 | 
			
		||||
 | 
			
		||||
		public ICollection<SubmissionDetail> SubmissionDetails { get; set; }
 | 
			
		||||
		public ICollection<Submission> SubmissionsAsStudent { get; set; }
 | 
			
		||||
		public ICollection<Submission> GradedSubmissions { get; set; }
 | 
			
		||||
 | 
			
		||||
		public User()
 | 
			
		||||
		{
 | 
			
		||||
			Id = Guid.NewGuid();
 | 
			
		||||
			SecurityStamp = Guid.NewGuid().ToString();
 | 
			
		||||
 | 
			
		||||
			CreatedQuestions = new HashSet<Question>();
 | 
			
		||||
			TaughtClassesLink = new HashSet<ClassTeacher>();
 | 
			
		||||
			EnrolledClassesLink = new HashSet<ClassStudent>();
 | 
			
		||||
			CreatedAssignments = new HashSet<Assignment>();
 | 
			
		||||
			GradedSubmissions = new HashSet<Submission>();
 | 
			
		||||
			SubmissionsAsStudent = new HashSet<Submission>();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										23
									
								
								Entities/DTO/ApiResponse.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								Entities/DTO/ApiResponse.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
namespace TechHelper.Services
 | 
			
		||||
{
 | 
			
		||||
	public class ApiResponse
 | 
			
		||||
	{
 | 
			
		||||
		public ApiResponse(string message, bool status = false)
 | 
			
		||||
		{
 | 
			
		||||
			this.Message = message;
 | 
			
		||||
			this.Status = status;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public ApiResponse(bool status, object result)
 | 
			
		||||
		{
 | 
			
		||||
			this.Status = status;
 | 
			
		||||
			this.Result = result;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public string Message { get; set; }
 | 
			
		||||
 | 
			
		||||
		public bool Status { get; set; }
 | 
			
		||||
 | 
			
		||||
		public object Result { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								Entities/DTO/AuthResponseDto.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								Entities/DTO/AuthResponseDto.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
namespace Entities.DTO
 | 
			
		||||
{
 | 
			
		||||
    public class AuthResponseDto
 | 
			
		||||
	{
 | 
			
		||||
		public bool IsAuthSuccessful { get; set; }
 | 
			
		||||
		public string? ErrorMessage { get; set; }
 | 
			
		||||
		public string? Token { get; set; }
 | 
			
		||||
		public string? RefreshToken { get; set; }
 | 
			
		||||
		public bool Is2StepVerificationRequired { get; set; }	
 | 
			
		||||
		public string Provider { get; set; }	
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								Entities/DTO/ClassDto.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								Entities/DTO/ClassDto.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
 | 
			
		||||
namespace Entities.DTO
 | 
			
		||||
{
 | 
			
		||||
	public class ClassDto
 | 
			
		||||
	{
 | 
			
		||||
		public byte Class { get; set; }
 | 
			
		||||
 | 
			
		||||
		[StringLength(50, ErrorMessage = "班级名称不能超过 50 个字符。")]
 | 
			
		||||
		public string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Required(ErrorMessage = "年级是必填项。")]
 | 
			
		||||
		[Range(1, 12, ErrorMessage = "年级编号必须在 1 到 12 之间。")] 
 | 
			
		||||
		public byte Grade { get; set; }
 | 
			
		||||
 | 
			
		||||
		public string Description { get; set; } = "HELLO WORLD";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		public int? HeadTeacherId { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								Entities/DTO/ForgotPasswordDto.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								Entities/DTO/ForgotPasswordDto.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Entities.DTO
 | 
			
		||||
{
 | 
			
		||||
	public class ForgotPasswordDto
 | 
			
		||||
	{
 | 
			
		||||
		[Required]
 | 
			
		||||
		[EmailAddress]
 | 
			
		||||
		public string Email { get; set; }
 | 
			
		||||
		public string ClientURI { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								Entities/DTO/RefreshTokenDto.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								Entities/DTO/RefreshTokenDto.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
namespace Entities.DTO
 | 
			
		||||
{
 | 
			
		||||
    public class RefreshTokenDto
 | 
			
		||||
	{
 | 
			
		||||
		public string? Token { get; set; }
 | 
			
		||||
		public string? RefreshToken { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								Entities/DTO/ResetPasswordDto.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								Entities/DTO/ResetPasswordDto.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Entities.DTO
 | 
			
		||||
{
 | 
			
		||||
	public class ResetPasswordDto
 | 
			
		||||
	{
 | 
			
		||||
		[Required(ErrorMessage = "Password is required")]
 | 
			
		||||
		public string Password { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Compare(nameof(Password), ErrorMessage = "The password and confirmation password do not match")]
 | 
			
		||||
		public string ConfirmPassword {  get; set; }
 | 
			
		||||
 | 
			
		||||
		public string Email { get; set; }
 | 
			
		||||
 | 
			
		||||
		public string Token { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								Entities/DTO/ResetPasswordResponseDto.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								Entities/DTO/ResetPasswordResponseDto.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Entities.DTO
 | 
			
		||||
{
 | 
			
		||||
	public class ResetPasswordResponseDto
 | 
			
		||||
	{
 | 
			
		||||
		public bool IsResetPasswordSuccessful { get; set; }	
 | 
			
		||||
		public IEnumerable<string> Errors { get; set; } = Enumerable.Empty<string>();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								Entities/DTO/ResponseDto.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								Entities/DTO/ResponseDto.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Entities.DTO
 | 
			
		||||
{
 | 
			
		||||
	public class ResponseDto
 | 
			
		||||
	{
 | 
			
		||||
		public bool IsSuccessfulRegistration { get; set; }
 | 
			
		||||
		public IEnumerable<string> Errors { get; set; } = Enumerable.Empty<string>();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								Entities/DTO/TwoFactorVerificationDto.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								Entities/DTO/TwoFactorVerificationDto.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Entities.DTO
 | 
			
		||||
{
 | 
			
		||||
	public class TwoFactorVerificationDto
 | 
			
		||||
	{
 | 
			
		||||
		public string Email { get; set; }	
 | 
			
		||||
		public string Provider { get; set; }
 | 
			
		||||
		[Required(ErrorMessage = "Token is required")]
 | 
			
		||||
		public string TwoFactorToken { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								Entities/DTO/UserForAuthenticationDto.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								Entities/DTO/UserForAuthenticationDto.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
 | 
			
		||||
namespace Entities.DTO
 | 
			
		||||
{
 | 
			
		||||
    public class UserForAuthenticationDto
 | 
			
		||||
	{
 | 
			
		||||
		[Required(ErrorMessage = "Email is required")]
 | 
			
		||||
		public string? Email { get; set; }
 | 
			
		||||
		[Required(ErrorMessage = "Password is required")]
 | 
			
		||||
		public string? Password { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										51
									
								
								Entities/DTO/UserForRegistrationDto.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								Entities/DTO/UserForRegistrationDto.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
using Entities.Contracts;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.ComponentModel.DataAnnotations;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Entities.DTO
 | 
			
		||||
{
 | 
			
		||||
	public class UserForRegistrationDto
 | 
			
		||||
	{
 | 
			
		||||
		[Required(ErrorMessage = "姓名是必填项。")]
 | 
			
		||||
		[StringLength(50, MinimumLength = 2, ErrorMessage = "姓名长度必须在 2 到 50 个字符之间。")]
 | 
			
		||||
		public string Name { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Required(ErrorMessage = "电子邮件是必填项。")]
 | 
			
		||||
		[EmailAddress(ErrorMessage = "电子邮件格式不正确。")] // 添加 Email 格式验证
 | 
			
		||||
		public string Email { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Required(ErrorMessage = "密码是必填项。")]
 | 
			
		||||
		[StringLength(100, MinimumLength = 6, ErrorMessage = "密码长度至少为 6 个字符。")] // 确保长度验证
 | 
			
		||||
		[RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&.])[A-Za-z\d@$!%*?&.]{6,}$",
 | 
			
		||||
		 ErrorMessage = "密码必须包含至少一个大写字母、一个小写字母、一个数字和一个特殊字符。特殊字符包含 @$!%*?&. ")]
 | 
			
		||||
		public string Password { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Compare(nameof(Password), ErrorMessage = "密码和确认密码不匹配。")]
 | 
			
		||||
		[Required(ErrorMessage = "确认密码是必填项。")] // 确保确认密码也必须填写
 | 
			
		||||
		public string ConfirmPassword { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Required(ErrorMessage = "至少选择一个角色。")]
 | 
			
		||||
		public UserRoles Roles { get; set; } = UserRoles.Student; // 根据你当前的 MudRadioGroup 设定,这里是单选
 | 
			
		||||
 | 
			
		||||
		[Required(ErrorMessage = "班级是必填项。")]
 | 
			
		||||
		[Range(1, 14, ErrorMessage = "班级编号必须在 1 到 14 之间。")] // 班级范围已调整为 1-14
 | 
			
		||||
		public int Class { get; set; } = 1;
 | 
			
		||||
 | 
			
		||||
		[Required(ErrorMessage = "年级是必填项。")]
 | 
			
		||||
		[Range(1, 6, ErrorMessage = "年级编号必须在 1 到 6 之间。")] // 年级范围已调整为 1-6
 | 
			
		||||
		public int Grade { get; set; } = 1;
 | 
			
		||||
 | 
			
		||||
		[Phone(ErrorMessage = "电话号码格式不正确。")]
 | 
			
		||||
		[StringLength(11, MinimumLength = 11, ErrorMessage = "电话号码必须是 11 位数字。")] // 电话号码长度已调整为固定 11 位
 | 
			
		||||
		public string PhoneNumber { get; set; }
 | 
			
		||||
 | 
			
		||||
		[StringLength(200, ErrorMessage = "家庭住址不能超过 200 个字符。")]
 | 
			
		||||
		public string HomeAddress { get; set; }
 | 
			
		||||
 | 
			
		||||
		public string ClientURI { get; set; }	
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								Entities/DTO/UserRegistrationToClassDto.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								Entities/DTO/UserRegistrationToClassDto.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
using Entities.Contracts;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
 | 
			
		||||
namespace Entities.DTO
 | 
			
		||||
{
 | 
			
		||||
	public class UserRegistrationToClassDto
 | 
			
		||||
	{
 | 
			
		||||
		public string User { get; set; }
 | 
			
		||||
		public byte ClassId { get; set; }
 | 
			
		||||
		public byte GradeId { get; set; }
 | 
			
		||||
		public UserRoles Roles { get; set; } = UserRoles.Student;
 | 
			
		||||
		public SubjectAreaEnum SubjectArea { get; set; } = SubjectAreaEnum.Unknown;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								Entities/Entities.csproj
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								Entities/Entities.csproj
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
<Project Sdk="Microsoft.NET.Sdk">
 | 
			
		||||
 | 
			
		||||
  <PropertyGroup>
 | 
			
		||||
    <TargetFramework>net8.0</TargetFramework>
 | 
			
		||||
    <ImplicitUsings>enable</ImplicitUsings>
 | 
			
		||||
    <Nullable>enable</Nullable>
 | 
			
		||||
  </PropertyGroup>
 | 
			
		||||
 | 
			
		||||
  <ItemGroup>
 | 
			
		||||
    <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.16" />
 | 
			
		||||
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.16" />
 | 
			
		||||
    <PackageReference Include="Microsoft.EntityFrameworkCore.AutoHistory" Version="6.0.0" />
 | 
			
		||||
  </ItemGroup>
 | 
			
		||||
 | 
			
		||||
</Project>
 | 
			
		||||
							
								
								
									
										12
									
								
								Entities/RequestFeatures/MetaData.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								Entities/RequestFeatures/MetaData.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
namespace Entities.RequestFeatures
 | 
			
		||||
{
 | 
			
		||||
    public class MetaData
 | 
			
		||||
	{
 | 
			
		||||
		public int CurrentPage { get; set; }
 | 
			
		||||
		public int TotalPages { get; set; }
 | 
			
		||||
		public int PageSize { get; set; }
 | 
			
		||||
		public int TotalCount { get; set; }
 | 
			
		||||
		public bool HasPrevious => CurrentPage > 1;
 | 
			
		||||
		public bool HasNext => CurrentPage < TotalPages;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								Entities/RequestFeatures/ProductParameters.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								Entities/RequestFeatures/ProductParameters.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
namespace Entities.RequestFeatures
 | 
			
		||||
{
 | 
			
		||||
    public class ProductParameters
 | 
			
		||||
	{
 | 
			
		||||
        const int maxPageSize = 50;
 | 
			
		||||
        public int PageNumber { get; set; } = 1;
 | 
			
		||||
        private int _pageSize = 4;
 | 
			
		||||
        public int PageSize
 | 
			
		||||
        {
 | 
			
		||||
            get
 | 
			
		||||
            {
 | 
			
		||||
                return _pageSize;
 | 
			
		||||
            }
 | 
			
		||||
            set
 | 
			
		||||
            {
 | 
			
		||||
                _pageSize = (value > maxPageSize) ? maxPageSize : value;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
		public string? SearchTerm { get; set; }
 | 
			
		||||
        public string OrderBy { get; set; } = "name";
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										108
									
								
								TechHelper.Client/AI/AIConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								TechHelper.Client/AI/AIConfiguration.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,108 @@
 | 
			
		||||
namespace TechHelper.Client.AI
 | 
			
		||||
{
 | 
			
		||||
	public static class AIConfiguration
 | 
			
		||||
	{
 | 
			
		||||
		public static string APIkey = "c62e38f2b74e47a080487a7a2fef014a.Q6Gx5HBy6Pj0OhkI";
 | 
			
		||||
 | 
			
		||||
		public static string ExamAnsConfig = "你是一个专业的试题生成助手,请根据提供的科目、年级和主题," +
 | 
			
		||||
			"严格按照下方指定的 JSON 格式和内容生成规则,输出高质量的试题数据。\r\n\r\n---\r\n**输出格式要求:" +
 | 
			
		||||
			"**\r\n\r\n请严格输出一个 **JSON 数组**。数组的每个元素都是一个**试题对象**。\r\n\r\n```json\r\n[\r\n  " +
 | 
			
		||||
			"{\r\n    \"题号\": \"X\",\r\n    \"标题\": \"XXXXXX\",\r\n    \"分值\": N,\r\n    \"分值问题标记\": \"(若" +
 | 
			
		||||
			"分值存在问题,请在此处简要说明,例如:'该题分值分配需复核';否则,此字段留空)\",\r\n    \"题目引用\": \"(若" +
 | 
			
		||||
			"为阅读理解等引用原文的题型,请在此处补充完整原文内容,并支持换行,例如:'(一)葡萄沟(节选)\\\\n葡萄种在山坡上" +
 | 
			
		||||
			"的梯田里...';否则,此字段留空)\",\r\n    \"子题目\": [\r\n      {\r\n        \"子题号\": \"X\",\r\n      " +
 | 
			
		||||
			"  \"题干\": \"XXXXXXX(独立成题,例如:'一( )大象';或拼音题:'pú táo( )';或成语填空题的单个成语:'( )心( )意')\",\r\n     " +
 | 
			
		||||
			"   \"分值\": N,\r\n        \"分值问题标记\": \"(若子题分值存在问题,请在此处简要说明,例如:'该子题分值偏高';否则,此字段留空)\",\r\n    " +
 | 
			
		||||
			"    \"选项\": [\"A\", \"B\", \"C\"],\r\n        \"示例答案\": \"XXXXXXX\"\r\n      }\r\n    ],\r\n    \"子" +
 | 
			
		||||
			"题组\": []\r\n  }\r\n]\r\n内容生成规则:\r\n\r\n强制拆分独立考察点:\r\n核心规则: 如果一个概念题干中包含多个独立的、" +
 | 
			
		||||
			"可分别作答或评分的部分(例如:多个量词填空、每个成语填空、每个拼音写词语),必须将其拆分为各自独立的 子题目 对象。\r\n示" +
 | 
			
		||||
			"例:\r\n原题干:“一( )大象 一( )大船 一( )菜叶” → 拆分为 3 个 子题目。\r\n原题干:“看拼音,写词语:pú táo( )、xiāng " +
 | 
			
		||||
			"jiāo( )” → 拆分为 2 个 子题目。\r\n每个拆分后的 子题目 必须有独立的 子题号 和 分值。\r\n合并重复题号:\r\n在 JSON 数组中,如果多" +
 | 
			
		||||
			"个题组(如阅读理解下的不同文段 (一)、(二))属于同一大题,它们可以有相同的 题号 字段值。但每个题组必须是 JSON 数组中的一个独" +
 | 
			
		||||
			"立对象。\r\n题目引用完整性:\r\n若有引用文本(如阅读理解的原文),必须在 题目引用 字段中补充完整原文内容。支持 \\n 进行换行。如果" +
 | 
			
		||||
			"无引用文本,此字段留空 \"\"。\r\n分值分配与转换:\r\n所有 子题目 的 分值 总和必须等于对应大题(QuestionGroup)的 分值。\r\n如果原始" +
 | 
			
		||||
			"分值以百分比表示,必须将其转换为具体的数值。\r\n选择题的每个选项的分值必须相同。\r\n标点规范:\r\n所有中文标点统一使用全角符号。\r\n长文" +
 | 
			
		||||
			"本换行:\r\n题目引用、题干 等长文本内容应自动换行,每行不超过 80 个字符(使用 \\n)。\r\n特殊处理要求:\r\n\r\n选择题 (选项 字段" +
 | 
			
		||||
			"不为空):\r\n选项 字段必须包含一个字符串数组,格式为 [\"选项A内容\", \"选项B内容\", \"选项C内容\"]。\r\n示例答案 字段必须提供一个具体" +
 | 
			
		||||
			"的示例答案。\r\n填空题 (选项 字段为空):\r\n示例答案 字段必须提供一个具体的示例答案(例如:“纷纷扬扬”)。\r\n阅读理解统计题(例如:句" +
 | 
			
		||||
			"数/小节数): 需在 题干 中明确标注单位。\r\n主旨题(若需多选): 请在 题干 描述中清晰体现其多选特性。\r\n子题组 字段: 在当前 JSON 结构中,子" +
 | 
			
		||||
			"题组 字段应始终保持为空数组 [],除非我另行通知需要更深层次的题组嵌套。\r\n格式验证要求(模型内部验证,确保合规性):\r\n\r\n题目引用 字" +
 | 
			
		||||
			"段若为空字符串 \"\",表示该题型无原文引用。\r\n分值问题标记 字段若不为空字符串 \"\",表示该分值需人工复核,模型无需自动修改分值。\r\n禁止 " +
 | 
			
		||||
			"子题目 与 题目引用 以外的内容混杂在 题干 或 标题 中。\r\n每个 子题目 必须独立成题,具备独立的 分值 和 题干。\r\nJSON 语法必须正确," +
 | 
			
		||||
			"能够通过任何在线 JSON 校验工具的验证。\r\n所有分值分配合理(子题目 总分等于大题 分值),且百分比分值已转换为具体数值。\r\n所有标" +
 | 
			
		||||
			"点符号 100% 使用中文全角。\r\n长文本自动换行(每行不超过 80 字符,使用 \\n)。\r\n题号 合并与 子题目 区分应通过 JSON 数组中的独立对象体现。";
 | 
			
		||||
 | 
			
		||||
		public static string ExamToXML = "文本试卷解析请求模板 你只需要给出转换后的结果,不需要其他任何无关的输出\r\n请将我提供的" +
 | 
			
		||||
			"文本试卷内容,按照以下精简版 XAML 风格的标记规则进行解析和结构化输出。\r\n\r\n输出" +
 | 
			
		||||
			"格式要求:\r\n请以精简版 XAML 风格的文本标记形式输出,并确保以下标签和属性的正确" +
 | 
			
		||||
			"使用:\r\n\r\n根元素: <EP>\r\n大题: <QG Id=\"X\" T=\"标题\" S=\"分值\" SPM=\"分值问" +
 | 
			
		||||
			"题标记\"/>\r\nId: 大题题号(应从文本中提取)。\r\nT: 大题标题(应从文本中提取)。\r\nS: 大题总分" +
 | 
			
		||||
			"(应从文本中提取,百分比请转换为具体数值)。\r\nSPM: 分值问题标记(如果文本中无则为空字符串 \"\";如果有任何" +
 | 
			
		||||
			"分值分配上的疑问或需要复核,请在此标记)。\r\n题目引用: <QR>引用文本</QR> (如果原始文本中无引用部分,则省" +
 | 
			
		||||
			"略此标签)\r\n子题目列表容器: <SQs>\r\n子题目: <SQ Id=\"X\" T=\"题干\" S=\"分值\" SPM=\"分值问题标记\" SA=\"示例" +
 | 
			
		||||
			"答案\"/>\r\nId: 子题号(应从文本中提取,例如 \"1.1\", \"2.3a\")。\r\nT: 子题目题干。\r\n【核心解析规则】 除了明显的拼" +
 | 
			
		||||
			"写和成语填空类题目外,所有子题的 T 属性都必须包含完整的原始题目表述,包括所有选项、括号等,以最大程度地保留原始文本结构。\r\nS: 子题" +
 | 
			
		||||
			"分值(应从文本中提取)。\r\nSPM: 分值问题标记(如果文本中无则为空字符串 \"\";如果有任何分值分配上的疑问或需要复核,请在此标记)。\r\nSA: 示例" +
 | 
			
		||||
			"答案。\r\n对于有多个填空或判断的题目(其 T 属性包含多个空位),答案请用空格分隔,并按题干中出现的顺序排列。\r\n示例: SA=\"× √ √\" (对应多" +
 | 
			
		||||
			"个判断)\r\n示例: SA=\"“ 哪 ? ” 。\" (对应多个标点填空)\r\n选项列表容器: <Os> (仅用于原始文本中明确给出选项的选择题,如果无选项则省" +
 | 
			
		||||
			"略此标签)\r\n选项: <O V=\"选项值\"/> (选项值应从原始文本中提取)\r\n子题组列表容器: <SQGs/> (如果原始文本中无嵌套题组,则为空标签)\r\n内容解" +
 | 
			
		||||
			"析规则:\r\n准确识别大题与子题: 根据题号、标题和分值模式,准确识别并划分大题 (<QG>) 和其下的子题目 (<SQ>)。\r\n细致拆分独立" +
 | 
			
		||||
			"考察点: 将文本中所有可独立评分或作答的部分,尽可能地拆分为独立的 <SQ> 标记块。但请注意,对于单个逻辑题但包含多个填空/选项/判断点(如填空" +
 | 
			
		||||
			"题和判断题),请将所有相关内容合并到一个 <SQ> 的 T 属性中,并提供序列化的 SA。\r\n提取题目引用: 识别阅读理解等题型中的引用段落,并" +
 | 
			
		||||
			"将其完整放入 <QR> 标签中。\r\n精确提取分值: 从原文中提取大题和子题的分值,并将其转换为阿拉伯数字。如果原文是百分比,请转换为具体分数。\r\n规范标" +
 | 
			
		||||
			"点: 确保所有中文标点在输出中统一使用全角符号。";
 | 
			
		||||
 | 
			
		||||
		public static string ExamToXML2 = "文本试卷解析请求模板 你只需要给出转换后的结果,不需要其他任何无关的输出 " +
 | 
			
		||||
			" 1. 整体结构与根元素\r\n<EP> (试卷):\r\n\r\n根元素,表示一份完整的试卷。\r\n作为最外层的容器,所有试卷内容都" +
 | 
			
		||||
			"将嵌套在其内部。\r\n没有属性,其直接子元素必须是 <QGs>。\r\n<QGs> (题组集合):\r\n\r\n作为 <EP> 的直接子" +
 | 
			
		||||
			"元素。\r\n没有属性,其内容将是一个或多个 <QG> 标签。\r\n2. 大题/题组的标记 (<QG>)\r\n<QG Id=\"X\" T=\"标题\" S=\"分值\" " +
 | 
			
		||||
			"SPM=\"分值问题标记\" QR=\"引用文本\"/>\r\n目的:用于标记试卷中的每一个“大题”或“题组”。\r\n属性要求:\r\nId:必填。从文本" +
 | 
			
		||||
			"中提取的大题题号(例如:“一”、“I”、“1.”)。\r\nT:必填。从文本中提取的大题标题(例如:“选择题”、“填空题”)。\r\nS:必" +
 | 
			
		||||
			"填。从文本中提取的大题总分。\r\n转换规则:如果原始文本是百分比(例如:“20%”),请转换为具体数值(例如:“20”)。\r\nSPM:可" +
 | 
			
		||||
			"选。\r\n如果文本中没有特殊标记,则为空字符串 \"\"。\r\n用途:用于标记在分值分配上存在疑问或需要复核的大题。\r\nQR:可选。\r\n用" +
 | 
			
		||||
			"途:如果原始文本中包含阅读理解、材料分析等需要引用的段落,请将完整的引用文本内容放入此属性。\r\n省略规则:如果原始文本中没有引用" +
 | 
			
		||||
			"部分,则整个 QR 属性应被省略(不要保留空属性或空标签)。\r\n子元素:\r\n一个 <QG> 标签内部可以包含 <SQs>(用于普通子题)或 <SQGs>(用" +
 | 
			
		||||
			"于嵌套题组)。两者不能同时存在。\r\n3. 子题目与选项的标记 (<SQ> & <O>)\r\n<SQs> (子题目列表容器):\r\n\r\n目的:作为 <QG> 的子元素,用" +
 | 
			
		||||
			"于封装一个大题下的所有独立小题。\r\n没有属性,其内容将是一个或多个 <SQ> 标签。\r\n<SQ Id=\"X\" T=\"题干\" S=\"分值\" SPM=\"分值问题标" +
 | 
			
		||||
			"记\" SA=\"示例答案\"/> (子题目):\r\n\r\n目的:用于标记试卷中的每一个“小题”。\r\n属性要求:\r\nId:必填。从文本中提取的子题号(例" +
 | 
			
		||||
			"如:“1.1”、“2.3a”、“(1)”)。\r\nT:必填。子题目的完整题干内容。\r\n核心解析规则:除了明显的单个填空(例如:“C#是一个____语言。”)和" +
 | 
			
		||||
			"单个判断类题目(例如:“是/否判断题。”)外,所有子题的 T 属性必须包含完整的原始题目表述,包括所有选项文字、括号、多个空位等,以最大程度" +
 | 
			
		||||
			"地保留原始文本结构。\r\nS:必填。从文本中提取的子题分值。\r\nSPM:可选。规则同 <QG> 中的 SPM 属性。\r\nSA:必填。小题的示例答案。\r\n多" +
 | 
			
		||||
			"空/多判断:对于包含多个空位或判断点的题目,答案请用空格分隔,并按照题干中出现的顺序排列。\r\n示例:SA=\"× √ √\" (对应多个判断)\r\n示" +
 | 
			
		||||
			"例:SA=\"“ 哪 ? ” 。\" (对应多个标点填空)\r\n示例:SA=\"北京 长城\" (对应两个填空)\r\n选择题:填写正确选项的字母或编号,例如 SA=\"A\" 或" +
 | 
			
		||||
			" SA=\"A,C\"。\r\n单个填空/判断题:直接填写答案,例如 SA=\"C#\" 或 SA=\"正确\"。\r\n<Os> (选项列表容器):\r\n\r\n目的:作为 <SQ> 的子" +
 | 
			
		||||
			"元素,用于封装选择题的选项。\r\n限制:仅用于原始文本中明确给出选项的选择题。\r\n省略规则:如果原始文本中无选项(例如:填空题、简答题)," +
 | 
			
		||||
			"则整个 <Os> 标签应被省略。\r\n没有属性,其内容将是一个或多个 <O> 标签。\r\n<O V=\"选项值\"/> (选项):\r\n\r\n目的:标记单个选项。\r\n属" +
 | 
			
		||||
			"性要求:\r\nV:必填。选项的完整内容,应从原始文本中提取,包括选项的字母/编号(例如:“A. 选项A”、“B) 选项B”)。\r\n4. 子题组的" +
 | 
			
		||||
			"标记 (<SQGs>)\r\n<SQGs> (子题组列表容器):\r\n目的:作为 <QG> 的子元素,用于表示嵌套的题组结构(例如:一个大题下面又包含几个小的大" +
 | 
			
		||||
			"题组)。\r\n省略规则:如果原始文本中无嵌套题组,则整个 <SQGs> 标签应被省略。\r\n没有属性,其内容将是一个或多个嵌套的 <QG> 元素。\r\n注意:嵌" +
 | 
			
		||||
			"套的 <QG> 结构与顶层的 <QG> 相同,可以递归包含 <SQs> 或 <SQGs>。\r\n5. 核心内容解析与转换规范\r\n识别与划分:\r\n优先识别大题 (<QG>),然后" +
 | 
			
		||||
			"识别其下的子题目 (<SQ>)。识别依据主要是题号、标题和分值模式。\r\n独立考察点:\r\n原则:将文本中所有可独立评分或作答的部分,尽可能地拆分" +
 | 
			
		||||
			"为独立的 <SQ> 标记块。\r\n例外:对于单个逻辑题但包含多个填空/选项/判断点(如一个句子中有多个空需要填写,或一个问题下有多个判断题),请将所" +
 | 
			
		||||
			"有相关内容合并到同一个 <SQ> 的 T 属性中,并提供序列化的 SA。\r\n分值提取:\r\n从原文中精确提取大题和子题的分值,并将其转换为阿拉伯数字。\r\n如果原" +
 | 
			
		||||
			"文是百分比,务必转换为具体分数。\r\n标点规范:\r\n确保所有中文标点在输出的XML文本中统一使用全角符号。";
 | 
			
		||||
 | 
			
		||||
		public static string RecorrectXML = "按下面的要求校验XML文本,如果有错误修正他,没有错误则直接返回,你只需要给出修正或原来的结果,不需要其他任何无" +
 | 
			
		||||
			"关的输出, 要求:请检查这段 XML 代码的语法是否正确,包括标签的开闭、嵌套和属性的引号。请检查 XML 数据中是否存在逻辑错误或不一" +
 | 
			
		||||
			"致的地方。XML 中的数据类型是否符合预期?例如,某个字段应该是数字却包含了文本。是否存在缺失的必需字段?XML 中的数据值是否在合理的范围内?检查" +
 | 
			
		||||
			"属性值是否有效且完整。 XML结构为<EP> (根元素,必需) 包含 <QGs> (必需)。<QGs> (容器,必需) 包含一个或多个 <QG>。<QG> (问题组,必需) 包含" +
 | 
			
		||||
			"在 <QGs> 或嵌套的 <SQGs> 内部,具有 Id (必需), T (必需), S (必需), SPM (可选) 属性,可包含子元素 <QR> (可选), <SQs> (可选,包含一" +
 | 
			
		||||
			"个或多个 <SQ>), <SQGs> (可选,包含一个或多个嵌套的 <QG>)。<SQs> (子题目容器,必需) 包含在 <QG> 内部,包含一个或多个 <SQ>。<SQ> (子题目,必需) 包含" +
 | 
			
		||||
			"在 <SQs> 内部,具有 Id (必需), T (必需), S (必需), SPM (可选), SA (可选) 属性,可包含子元素 <Os> (可选,包含一个或多个 <O>)。<Os> (选项容器,必需) 包" +
 | 
			
		||||
			"含在 <SQ> 内部,包含一个或多个 <O>。<O> (选项,必需) 包含在 <Os> 内部,具有 V (必需) 属性。";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		public static string BreakQuestions = "请识别以下文本中的每一道大题。将其转换为XML格式,你只需要给出转换后的结果,不需要其他任何无关的输出,其中 <EP> 是XML的根元素。用<Q> 和</Q> 标记来包裹每一道大题。请确保标记后的文本保持原始格式和内容。";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		public static string ParseSignelQuestion = "请将以下提供的一道大题内容,转换为符合以下 C# 类结构的 XML 格式,你只需要给出转换后的结果,不需要其他任何无关的输出:" +
 | 
			
		||||
			"XML 的根元素为 <QG>。并填充以下属性:Id:对应 QuestionGroup.Id(从大题开头的题号中提取)。T:对应 QuestionGroup.Title(" +
 | 
			
		||||
			"从大题的标题中提取)。S:对应 QuestionGroup.Score(从大题中识别的分值)。<QR> 元素:对应 QuestionGroup.QuestionReference。**子题目" +
 | 
			
		||||
			"(SubQuestion)**将作为 <QG> 内部的 <SQs> 元素列表中的 <SQ> 元素。如果大题下有子题目,则将它们包裹在 <SQs> 元素中。对于每个 <SQ> 元素," +
 | 
			
		||||
			"填充以下属性:Id:对应 SubQuestion.SubId(从子题号中提取)。T:对应 SubQuestion.Stem(从子题目的题干中提取)。S:对应 SubQuestion.Score" +
 | 
			
		||||
			"(从子题目中识别的分值)。SPM:对应 SubQuestion.ScoreProblemMarker。SA:对应 SubQuestion.SampleAnswer。**选项(Option)**将作为" +
 | 
			
		||||
			" <SQ> 内部的 <Os> 元素列表中的 <O> 元素。如果子题目有选项,则将它们包裹在 <Os> 元素中。对于每个 <O> 元素,填充 V 属性:V:对应 Option.Value" +
 | 
			
		||||
			"(从选项内容中提取)。嵌套题组:如果大题内部包含其他大题,则作为 <QG> 内部的 <SQGs> 元素列表中的 <QG> 元素(即嵌套 <QG>)。" +
 | 
			
		||||
			"请确保生成的 XML 严格遵循上述结构和命名约定,以方便 C# XmlSerializer 进行反序列化。";
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										41
									
								
								TechHelper.Client/AI/AIModels.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								TechHelper.Client/AI/AIModels.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
			
		||||
using System.ComponentModel;
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
 | 
			
		||||
namespace TechHelper.Client.AI
 | 
			
		||||
{
 | 
			
		||||
	public enum AIModelsEnum
 | 
			
		||||
	{
 | 
			
		||||
		[Description("glm-4v-flash")]
 | 
			
		||||
		GLM4VFlash,
 | 
			
		||||
 | 
			
		||||
		[Description("cogview-3-flash")]
 | 
			
		||||
		Cogview3Flash,
 | 
			
		||||
 | 
			
		||||
		[Description("cogvideox-flash")]
 | 
			
		||||
		CogVideoXFlash,
 | 
			
		||||
 | 
			
		||||
		[Description("glm-4-flash-250414")]
 | 
			
		||||
		GLM4Flash25,
 | 
			
		||||
 | 
			
		||||
		[Description("glm-z1-flash")]
 | 
			
		||||
		GLMZ1Flash
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	public static class EnumExtensions
 | 
			
		||||
	{
 | 
			
		||||
		public static string GetDescription(this Enum value)
 | 
			
		||||
		{
 | 
			
		||||
			FieldInfo field = value.GetType().GetField(value.ToString());
 | 
			
		||||
			DescriptionAttribute attribute = Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) as DescriptionAttribute;
 | 
			
		||||
			return attribute == null ? value.ToString() : attribute.Description;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public class AIModels
 | 
			
		||||
	{
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										58
									
								
								TechHelper.Client/AI/AiService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								TechHelper.Client/AI/AiService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
using Entities.Contracts;
 | 
			
		||||
using Newtonsoft.Json;
 | 
			
		||||
 | 
			
		||||
namespace TechHelper.Client.AI
 | 
			
		||||
{
 | 
			
		||||
	public class AiService : IAIService
 | 
			
		||||
	{
 | 
			
		||||
		private readonly GLMZ1Client _glmClient;
 | 
			
		||||
 | 
			
		||||
		public AiService()
 | 
			
		||||
		{
 | 
			
		||||
			_glmClient = new GLMZ1Client(AIConfiguration.APIkey);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		public async Task<string> CallGLM(string userContent, string AnsConfig, AIModelsEnum aIModels/* = AIModelsEnum.GLMZ1Flash*/)
 | 
			
		||||
		{
 | 
			
		||||
			string model = aIModels.GetDescription();
 | 
			
		||||
			var request = new ChatCompletionRequest
 | 
			
		||||
			{
 | 
			
		||||
				Model = model,
 | 
			
		||||
				Messages = new List<Message>
 | 
			
		||||
			{
 | 
			
		||||
				new UserMessage(AnsConfig + userContent)
 | 
			
		||||
			}
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			try
 | 
			
		||||
			{
 | 
			
		||||
				var response = await _glmClient.ChatCompletionsSync(request);
 | 
			
		||||
				if (response?.Choices != null && response.Choices.Count > 0)
 | 
			
		||||
				{
 | 
			
		||||
					string content = response.Choices[0].Message?.Content;
 | 
			
		||||
					if (!string.IsNullOrEmpty(content))
 | 
			
		||||
					{
 | 
			
		||||
						// 移除 <think>...</think> 标签及其内容
 | 
			
		||||
						int startIndex = content.IndexOf("<think>");
 | 
			
		||||
						int endIndex = content.IndexOf("</think>");
 | 
			
		||||
						if (startIndex != -1 && endIndex != -1 && endIndex > startIndex)
 | 
			
		||||
						{
 | 
			
		||||
							content = content.Remove(startIndex, endIndex - startIndex + "</think>".Length);
 | 
			
		||||
						}
 | 
			
		||||
						return content.Trim();
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			catch (HttpRequestException ex)
 | 
			
		||||
			{
 | 
			
		||||
				Console.WriteLine($"API 请求错误:{ex.Message}");
 | 
			
		||||
			}
 | 
			
		||||
			catch (Exception ex)
 | 
			
		||||
			{
 | 
			
		||||
				Console.WriteLine($"发生未知错误:{ex.Message}");
 | 
			
		||||
			}
 | 
			
		||||
			return null;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										217
									
								
								TechHelper.Client/AI/GLMZ1Api.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										217
									
								
								TechHelper.Client/AI/GLMZ1Api.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,217 @@
 | 
			
		||||
using Newtonsoft.Json;
 | 
			
		||||
using System.Text;
 | 
			
		||||
 | 
			
		||||
namespace TechHelper.Client.AI
 | 
			
		||||
{
 | 
			
		||||
	public class Message
 | 
			
		||||
	{
 | 
			
		||||
		[JsonProperty("role")]
 | 
			
		||||
		public string Role { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("content")]
 | 
			
		||||
		public string Content { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 系统消息
 | 
			
		||||
	public class SystemMessage : Message
 | 
			
		||||
	{
 | 
			
		||||
		public SystemMessage(string content)
 | 
			
		||||
		{
 | 
			
		||||
			Role = "system";
 | 
			
		||||
			Content = content;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 用户消息
 | 
			
		||||
	public class UserMessage : Message
 | 
			
		||||
	{
 | 
			
		||||
		public UserMessage(string content)
 | 
			
		||||
		{
 | 
			
		||||
			Role = "user";
 | 
			
		||||
			Content = content;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 助手消息
 | 
			
		||||
	public class AssistantMessage : Message
 | 
			
		||||
	{
 | 
			
		||||
		public AssistantMessage(string content)
 | 
			
		||||
		{
 | 
			
		||||
			Role = "assistant";
 | 
			
		||||
			Content = content;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// GLM-Z1 Chat Completions API 请求体
 | 
			
		||||
	public class ChatCompletionRequest
 | 
			
		||||
	{
 | 
			
		||||
		[JsonProperty("model")]
 | 
			
		||||
		public string Model { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("messages")]
 | 
			
		||||
		public List<Message> Messages { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("request_id")]
 | 
			
		||||
		public string RequestId { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("do_sample")]
 | 
			
		||||
		public bool? DoSample { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("stream")]
 | 
			
		||||
		public bool? Stream { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("temperature")]
 | 
			
		||||
		public float? Temperature { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("top_p")]
 | 
			
		||||
		public float? TopP { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("max_tokens")]
 | 
			
		||||
		public int? MaxTokens { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("stop")]
 | 
			
		||||
		public List<string> Stop { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("user_id")]
 | 
			
		||||
		public string UserId { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// GLM-Z1 Chat Completions API 响应体
 | 
			
		||||
	public class ChatCompletionResponse
 | 
			
		||||
	{
 | 
			
		||||
		[JsonProperty("id")]
 | 
			
		||||
		public string Id { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("created")]
 | 
			
		||||
		public long Created { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("model")]
 | 
			
		||||
		public string Model { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("choices")]
 | 
			
		||||
		public List<CompletionChoice> Choices { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("usage")]
 | 
			
		||||
		public CompletionUsage Usage { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public class CompletionChoice
 | 
			
		||||
	{
 | 
			
		||||
		[JsonProperty("index")]
 | 
			
		||||
		public int Index { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("finish_reason")]
 | 
			
		||||
		public string FinishReason { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("message")]
 | 
			
		||||
		public CompletionMessage Message { get; set; }
 | 
			
		||||
 | 
			
		||||
		// 流式响应中的delta
 | 
			
		||||
		[JsonProperty("delta")]
 | 
			
		||||
		public CompletionMessage Delta { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public class CompletionMessage
 | 
			
		||||
	{
 | 
			
		||||
		[JsonProperty("role")]
 | 
			
		||||
		public string Role { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("content")]
 | 
			
		||||
		public string Content { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public class CompletionUsage
 | 
			
		||||
	{
 | 
			
		||||
		[JsonProperty("prompt_tokens")]
 | 
			
		||||
		public int PromptTokens { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("completion_tokens")]
 | 
			
		||||
		public int CompletionTokens { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("total_tokens")]
 | 
			
		||||
		public int TotalTokens { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 异步调用响应
 | 
			
		||||
	public class AsyncTaskStatus
 | 
			
		||||
	{
 | 
			
		||||
		[JsonProperty("request_id")]
 | 
			
		||||
		public string RequestId { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("id")]
 | 
			
		||||
		public string Id { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("model")]
 | 
			
		||||
		public string Model { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("task_status")]
 | 
			
		||||
		public string TaskStatus { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 异步查询结果响应
 | 
			
		||||
	public class AsyncCompletion : AsyncTaskStatus
 | 
			
		||||
	{
 | 
			
		||||
		[JsonProperty("choices")]
 | 
			
		||||
		public List<CompletionChoice> Choices { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("usage")]
 | 
			
		||||
		public CompletionUsage Usage { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// GLM-Z1 API 客户端
 | 
			
		||||
	public class GLMZ1Client
 | 
			
		||||
	{
 | 
			
		||||
		private readonly HttpClient _httpClient;
 | 
			
		||||
		private readonly string _apiKey;
 | 
			
		||||
		private const string BaseUrl = "https://open.bigmodel.cn/api/paas/v4/";
 | 
			
		||||
 | 
			
		||||
		public GLMZ1Client(string apiKey)
 | 
			
		||||
		{
 | 
			
		||||
			_apiKey = apiKey;
 | 
			
		||||
			_httpClient = new HttpClient();
 | 
			
		||||
			_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_apiKey}");
 | 
			
		||||
			_httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 同步调用 Chat Completions API
 | 
			
		||||
		public async Task<ChatCompletionResponse> ChatCompletionsSync(ChatCompletionRequest request)
 | 
			
		||||
		{
 | 
			
		||||
			request.Stream = false; // 确保同步调用时 stream 为 false
 | 
			
		||||
			var jsonContent = JsonConvert.SerializeObject(request);
 | 
			
		||||
			var httpContent = new StringContent(jsonContent, Encoding.UTF8, "application/json");
 | 
			
		||||
 | 
			
		||||
			var response = await _httpClient.PostAsync($"{BaseUrl}chat/completions", httpContent);
 | 
			
		||||
			response.EnsureSuccessStatusCode(); // 确保请求成功
 | 
			
		||||
 | 
			
		||||
			var responseString = await response.Content.ReadAsStringAsync();
 | 
			
		||||
			return JsonConvert.DeserializeObject<ChatCompletionResponse>(responseString);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 异步调用 Chat Completions API
 | 
			
		||||
		public async Task<AsyncTaskStatus> ChatCompletionsAsync(ChatCompletionRequest request)
 | 
			
		||||
		{
 | 
			
		||||
			var jsonContent = JsonConvert.SerializeObject(request);
 | 
			
		||||
			var httpContent = new StringContent(jsonContent, Encoding.UTF8, "application/json");
 | 
			
		||||
 | 
			
		||||
			var response = await _httpClient.PostAsync($"{BaseUrl}async/chat/completions", httpContent);
 | 
			
		||||
			response.EnsureSuccessStatusCode();
 | 
			
		||||
 | 
			
		||||
			var responseString = await response.Content.ReadAsStringAsync();
 | 
			
		||||
			return JsonConvert.DeserializeObject<AsyncTaskStatus>(responseString);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 查询异步任务结果
 | 
			
		||||
		public async Task<AsyncCompletion> RetrieveCompletionResult(string taskId)
 | 
			
		||||
		{
 | 
			
		||||
			var response = await _httpClient.GetAsync($"{BaseUrl}async-result/{taskId}");
 | 
			
		||||
			response.EnsureSuccessStatusCode();
 | 
			
		||||
 | 
			
		||||
			var responseString = await response.Content.ReadAsStringAsync();
 | 
			
		||||
			return JsonConvert.DeserializeObject<AsyncCompletion>(responseString);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// TODO: 实现流式调用,这会涉及循环读取 HttpResponseMessage.Content.ReadAsStreamAsync()
 | 
			
		||||
		// 并解析 SSE 事件,此处为简化暂不提供完整实现。
 | 
			
		||||
		// public async IAsyncEnumerable<ChatCompletionResponse> ChatCompletionsStream(ChatCompletionRequest request) { ... }
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								TechHelper.Client/AI/IAIService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								TechHelper.Client/AI/IAIService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
namespace TechHelper.Client.AI
 | 
			
		||||
{
 | 
			
		||||
	public interface IAIService
 | 
			
		||||
	{
 | 
			
		||||
		public Task<string> CallGLM(string userContent, string AnsConfig, AIModelsEnum aIModels = AIModelsEnum.GLMZ1Flash);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										25
									
								
								TechHelper.Client/App.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								TechHelper.Client/App.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
<CascadingAuthenticationState>
 | 
			
		||||
    <Router AppAssembly="@typeof(App).Assembly">
 | 
			
		||||
        <Found Context="routeData">
 | 
			
		||||
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
 | 
			
		||||
                <NotAuthorized>
 | 
			
		||||
                    @if (context.User.Identity?.IsAuthenticated != true)
 | 
			
		||||
                    {
 | 
			
		||||
                        <RedirectToLogin />
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        <p role="alert">You are not authorized to access this resource.</p>
 | 
			
		||||
                    }
 | 
			
		||||
                </NotAuthorized>
 | 
			
		||||
            </AuthorizeRouteView>
 | 
			
		||||
            <FocusOnNavigate RouteData="@routeData" Selector="h1" />
 | 
			
		||||
        </Found>
 | 
			
		||||
        <NotFound>
 | 
			
		||||
            <PageTitle>Not found</PageTitle>
 | 
			
		||||
            <LayoutView Layout="@typeof(MainLayout)">
 | 
			
		||||
                <p role="alert">Sorry, there's nothing at this address.</p>
 | 
			
		||||
            </LayoutView>
 | 
			
		||||
        </NotFound>
 | 
			
		||||
    </Router>
 | 
			
		||||
</CascadingAuthenticationState>
 | 
			
		||||
							
								
								
									
										64
									
								
								TechHelper.Client/AuthProviders/AuthStateProvider.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								TechHelper.Client/AuthProviders/AuthStateProvider.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,64 @@
 | 
			
		||||
using Microsoft.AspNetCore.Components.Authorization;
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
using System.Net.Http.Headers;
 | 
			
		||||
using TechHelper.Features;
 | 
			
		||||
using Microsoft.JSInterop;
 | 
			
		||||
 | 
			
		||||
namespace TechHelper.Client.AuthProviders
 | 
			
		||||
{
 | 
			
		||||
	public class AuthStateProvider : AuthenticationStateProvider
 | 
			
		||||
	{
 | 
			
		||||
		private readonly HttpClient _httpClient;
 | 
			
		||||
		private readonly AuthenticationState _anonymous;
 | 
			
		||||
		private readonly ILocalStorageService _localStorageService;
 | 
			
		||||
 | 
			
		||||
		public AuthStateProvider(ILocalStorageService localStorageService, HttpClient httpClient)
 | 
			
		||||
		{
 | 
			
		||||
			_localStorageService = localStorageService;
 | 
			
		||||
			_httpClient = httpClient;
 | 
			
		||||
			_anonymous = new AuthenticationState(
 | 
			
		||||
				new ClaimsPrincipal(new ClaimsIdentity()));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		public override async Task<AuthenticationState> GetAuthenticationStateAsync()
 | 
			
		||||
		{
 | 
			
		||||
			string? token = null;
 | 
			
		||||
 | 
			
		||||
			try
 | 
			
		||||
			{
 | 
			
		||||
				token =  _localStorageService.GetItem<string>("authToken");
 | 
			
		||||
			}
 | 
			
		||||
			catch (Exception ex)
 | 
			
		||||
			{
 | 
			
		||||
				Console.WriteLine($"Error accessing LocalStorage or parsing token: {ex.Message}"); 
 | 
			
		||||
				return _anonymous; 
 | 
			
		||||
			}
 | 
			
		||||
			if (string.IsNullOrEmpty(token))
 | 
			
		||||
				return _anonymous;
 | 
			
		||||
 | 
			
		||||
			_httpClient.DefaultRequestHeaders.Authorization = 
 | 
			
		||||
				new AuthenticationHeaderValue("bearer", token);
 | 
			
		||||
 | 
			
		||||
			return new AuthenticationState(new ClaimsPrincipal(
 | 
			
		||||
				new ClaimsIdentity(JWTParser.ParseClaimsFromJwt(token), "jwtAuthType")));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public void NotifyUserAuthentication(string token)
 | 
			
		||||
		{
 | 
			
		||||
			var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(
 | 
			
		||||
				JWTParser.ParseClaimsFromJwt(token), "jwtAuthType"));
 | 
			
		||||
 | 
			
		||||
			var authState = Task.FromResult(new AuthenticationState(authenticatedUser));	
 | 
			
		||||
 | 
			
		||||
			NotifyAuthenticationStateChanged(authState);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public void NotifyUserLogout()
 | 
			
		||||
		{
 | 
			
		||||
			var authState = Task.FromResult(_anonymous);
 | 
			
		||||
 | 
			
		||||
			NotifyAuthenticationStateChanged(authState);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										23
									
								
								TechHelper.Client/AuthProviders/TestAuthStateProvider.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								TechHelper.Client/AuthProviders/TestAuthStateProvider.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
using Microsoft.AspNetCore.Components.Authorization;
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
 | 
			
		||||
namespace TechHelper.Client.AuthProviders
 | 
			
		||||
{
 | 
			
		||||
	public class TestAuthStateProvider : AuthenticationStateProvider
 | 
			
		||||
	{
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		public override async Task<AuthenticationState> GetAuthenticationStateAsync()
 | 
			
		||||
		{
 | 
			
		||||
			var claims = new List<Claim>
 | 
			
		||||
			{
 | 
			
		||||
				new Claim(ClaimTypes.Name, "John Doe"),
 | 
			
		||||
				new Claim(ClaimTypes.Role, "Administrator")
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			var anonymous = new ClaimsIdentity();
 | 
			
		||||
 | 
			
		||||
			return await Task.FromResult(new AuthenticationState(new ClaimsPrincipal(anonymous)));
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										258
									
								
								TechHelper.Client/Exam/Exam.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										258
									
								
								TechHelper.Client/Exam/Exam.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,258 @@
 | 
			
		||||
using Newtonsoft.Json;
 | 
			
		||||
using System.Xml.Serialization; // 确保引用此命名空间
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.IO; // 用于 XML 反序列化
 | 
			
		||||
 | 
			
		||||
namespace TechHelper.Client.Exam
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
	[XmlRoot("EP")] 
 | 
			
		||||
	public class StringsList
 | 
			
		||||
	{
 | 
			
		||||
 | 
			
		||||
		[XmlElement("Q")] 
 | 
			
		||||
		public List<string> Items { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// XML 根元素 <EP>
 | 
			
		||||
	[XmlRoot("EP")]
 | 
			
		||||
	public class ExamPaper
 | 
			
		||||
	{
 | 
			
		||||
		// XML 特性:<QGs> 包含 <QG> 列表
 | 
			
		||||
		[XmlArray("QGs")]
 | 
			
		||||
		[XmlArrayItem("QG")]
 | 
			
		||||
		[JsonProperty("QuestionGroups")]
 | 
			
		||||
		public List<QuestionGroup> QuestionGroups { get; set; } = new List<QuestionGroup>();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	[XmlRoot("QG")]
 | 
			
		||||
	public class QuestionGroup
 | 
			
		||||
	{
 | 
			
		||||
		// JSON 特性
 | 
			
		||||
		[JsonProperty("题号")]
 | 
			
		||||
		// XML 特性:作为 <QG Id="X"> 属性
 | 
			
		||||
		[XmlAttribute("Id")]
 | 
			
		||||
		public string Id { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("标题")]
 | 
			
		||||
		[XmlAttribute("T")] // T for Title
 | 
			
		||||
		public string Title { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("分值")]
 | 
			
		||||
		[XmlAttribute("S")] // S for Score
 | 
			
		||||
		public int Score { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("分值问题标记")]
 | 
			
		||||
		[XmlAttribute("SPM")] // SPM for ScoreProblemMarker
 | 
			
		||||
		public string ScoreProblemMarker { get; set; } = ""; // 初始化为空字符串,避免 null
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("题目引用")]
 | 
			
		||||
		[XmlElement("QR")] // QR for QuestionReference,作为 <QR> 元素
 | 
			
		||||
		public string QuestionReference { get; set; } = ""; // 初始化为空字符串
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("子题目")]
 | 
			
		||||
		[XmlArray("SQs")] // SQs 包含 <SQ> 列表
 | 
			
		||||
		[XmlArrayItem("SQ")]
 | 
			
		||||
		public List<SubQuestion> SubQuestions { get; set; } = new List<SubQuestion>();
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("子题组")]
 | 
			
		||||
		[XmlArray("SQGs")] // SQGs 包含 <QG> 列表 (嵌套题组)
 | 
			
		||||
		[XmlArrayItem("QG")]
 | 
			
		||||
		public List<QuestionGroup> SubQuestionGroups { get; set; } = new List<QuestionGroup>();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 子题目类
 | 
			
		||||
	public class SubQuestion
 | 
			
		||||
	{
 | 
			
		||||
		[JsonProperty("子题号")]
 | 
			
		||||
		[XmlAttribute("Id")] // Id for SubId
 | 
			
		||||
		public string SubId { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("题干")]
 | 
			
		||||
		[XmlAttribute("T")] // T for Text (Stem)
 | 
			
		||||
		public string Stem { get; set; }
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("分值")]
 | 
			
		||||
		[XmlAttribute("S")] // S for Score
 | 
			
		||||
		public int Score { get; set; } // 分值通常为整数
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("分值问题标记")]
 | 
			
		||||
		[XmlAttribute("SPM")] // SPM for ScoreProblemMarker
 | 
			
		||||
		public string ScoreProblemMarker { get; set; } = "";
 | 
			
		||||
 | 
			
		||||
		// 注意:这里的 Options 需要适配 XML 结构 <Os><O V="X"/></Os>
 | 
			
		||||
		// 因此它不再是 List<string>,而是 List<Option>
 | 
			
		||||
		[JsonProperty("选项")]
 | 
			
		||||
		[XmlArray("Os")] // Os 包含 <O> 列表
 | 
			
		||||
		[XmlArrayItem("O")]
 | 
			
		||||
		public List<Option> Options { get; set; } = new List<Option>();
 | 
			
		||||
 | 
			
		||||
		[JsonProperty("示例答案")]
 | 
			
		||||
		[XmlAttribute("SA")] // SA for SampleAnswer
 | 
			
		||||
		public string SampleAnswer { get; set; } = "";
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 选项类,用于适配 <O V="X"/> 结构
 | 
			
		||||
	public class Option
 | 
			
		||||
	{
 | 
			
		||||
		// XML 特性:作为 <O V="X"> 属性
 | 
			
		||||
		[XmlAttribute("V")] // V for Value
 | 
			
		||||
							// JSON 特性:如果 JSON 中的选项是 {"Value": "A"} 这样的对象,则需要 JsonProperty("Value")
 | 
			
		||||
							// 但如果 JSON 选项只是 ["A", "B"] 这样的字符串数组,则此Option类不适合JSON Options
 | 
			
		||||
							// 需要明确你的JSON Options的结构。我假设你JSON Options是 List<string>
 | 
			
		||||
							// 如果是 List<string>,则Options属性在SubQuestion中直接是List<string>,Option类则不需要
 | 
			
		||||
							// 但根据你的精简XML需求,Option类是必要的。
 | 
			
		||||
							// 所以这里需要你自己根据实际JSON Options结构选择。
 | 
			
		||||
							// 为了兼容XML,我会保留Option类,但如果JSON是List<string>,Options属性会很复杂
 | 
			
		||||
		public string Value { get; set; }
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 独立的服务类来处理序列化和反序列化
 | 
			
		||||
	public static class ExamParser
 | 
			
		||||
	{
 | 
			
		||||
		// JSON 反序列化方法
 | 
			
		||||
		public static List<T> ParseExamJson<T>(string jsonContent)
 | 
			
		||||
		{
 | 
			
		||||
			string cleanedJson = jsonContent.Trim();
 | 
			
		||||
 | 
			
		||||
			// 移除可能存在的 Markdown 代码块标记
 | 
			
		||||
			if (cleanedJson.StartsWith("```json") && cleanedJson.EndsWith("```"))
 | 
			
		||||
			{
 | 
			
		||||
				cleanedJson = cleanedJson.Substring("```json".Length, cleanedJson.Length - "```json".Length - "```".Length).Trim();
 | 
			
		||||
			}
 | 
			
		||||
			// 移除可能存在的单引号包围(如果 AI 偶尔会这样输出)
 | 
			
		||||
			if (cleanedJson.StartsWith("'") && cleanedJson.EndsWith("'"))
 | 
			
		||||
			{
 | 
			
		||||
				cleanedJson = cleanedJson.Substring(1, cleanedJson.Length - 2).Trim();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			try
 | 
			
		||||
			{
 | 
			
		||||
				// 注意:这里假设你的 JSON 根直接是一个 QuestionGroup 列表
 | 
			
		||||
				// 如果你的 JSON 根是 { "QuestionGroups": [...] },则需要先反序列化到 ExamPaper
 | 
			
		||||
				List<T> examQuestions = JsonConvert.DeserializeObject<List<T>>(cleanedJson);
 | 
			
		||||
				return examQuestions;
 | 
			
		||||
			}
 | 
			
		||||
			catch (JsonSerializationException ex)
 | 
			
		||||
			{
 | 
			
		||||
				Console.WriteLine($"JSON 反序列化错误: {ex.Message}");
 | 
			
		||||
				Console.WriteLine($"内部异常: {ex.InnerException?.Message}");
 | 
			
		||||
				return null;
 | 
			
		||||
			}
 | 
			
		||||
			catch (Exception ex)
 | 
			
		||||
			{
 | 
			
		||||
				Console.WriteLine($"处理错误: {ex.Message}");
 | 
			
		||||
				return null;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		#region TEST
 | 
			
		||||
		[XmlRoot("User")]
 | 
			
		||||
		public class User
 | 
			
		||||
		{
 | 
			
		||||
			[XmlAttribute("id")]
 | 
			
		||||
			public string Id { get; set; }
 | 
			
		||||
 | 
			
		||||
			[XmlElement("PersonalInfo")]
 | 
			
		||||
			public PersonalInfo PersonalInfo { get; set; }
 | 
			
		||||
 | 
			
		||||
			[XmlArray("Roles")] // 包装元素 <Roles>
 | 
			
		||||
			[XmlArrayItem("Role")] // 集合中的每个项是 <Role>
 | 
			
		||||
			public List<Role> Roles { get; set; } = new List<Role>();
 | 
			
		||||
 | 
			
		||||
			// 构造函数,方便测试
 | 
			
		||||
			public User() { }
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public class PersonalInfo
 | 
			
		||||
		{
 | 
			
		||||
			[XmlElement("FullName")]
 | 
			
		||||
			public string FullName { get; set; }
 | 
			
		||||
 | 
			
		||||
			[XmlElement("EmailAddress")]
 | 
			
		||||
			public string EmailAddress { get; set; }
 | 
			
		||||
 | 
			
		||||
			// 构造函数,方便测试
 | 
			
		||||
			public PersonalInfo() { }
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public class Role
 | 
			
		||||
		{
 | 
			
		||||
			[XmlAttribute("type")]
 | 
			
		||||
			public string Type { get; set; }
 | 
			
		||||
 | 
			
		||||
			// 构造函数,方便测试
 | 
			
		||||
			public Role() { }
 | 
			
		||||
		}
 | 
			
		||||
		#endregion
 | 
			
		||||
 | 
			
		||||
		// XML 反序列化方法
 | 
			
		||||
		public static T ParseExamXml<T>(string xmlContent)
 | 
			
		||||
		{
 | 
			
		||||
			string cleanedXml = xmlContent.Trim();
 | 
			
		||||
			if (cleanedXml.StartsWith("'") && cleanedXml.EndsWith("'"))
 | 
			
		||||
			{
 | 
			
		||||
				cleanedXml = cleanedXml.Substring(1, cleanedXml.Length - 2);
 | 
			
		||||
			}
 | 
			
		||||
			if (cleanedXml.StartsWith("```xml") && cleanedXml.EndsWith("```"))
 | 
			
		||||
			{
 | 
			
		||||
				cleanedXml = cleanedXml.Substring("```xml".Length, cleanedXml.Length - "```xml".Length - "```".Length).Trim();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			XmlSerializer serializer = new XmlSerializer(typeof(T));
 | 
			
		||||
 | 
			
		||||
			using (StringReader reader = new StringReader(cleanedXml))
 | 
			
		||||
			{
 | 
			
		||||
				try
 | 
			
		||||
				{
 | 
			
		||||
					T user = (T)serializer.Deserialize(reader);
 | 
			
		||||
					return user;
 | 
			
		||||
				}
 | 
			
		||||
				catch (InvalidOperationException ex)
 | 
			
		||||
				{
 | 
			
		||||
					Console.WriteLine($"XML 反序列化操作错误: {ex.Message}");
 | 
			
		||||
					Console.WriteLine($"内部异常: {ex.InnerException?.Message}");
 | 
			
		||||
					return default(T);
 | 
			
		||||
				}
 | 
			
		||||
				catch (Exception ex)
 | 
			
		||||
				{
 | 
			
		||||
					Console.WriteLine($"处理错误: {ex.Message}");
 | 
			
		||||
					return default(T);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public static List<QuestionGroup> ParseExamXmlFormQG(string xmlContent)
 | 
			
		||||
		{
 | 
			
		||||
			// 移除可能存在的 Markdown 代码块标记
 | 
			
		||||
			if (xmlContent.StartsWith("```xml") && xmlContent.EndsWith("```"))
 | 
			
		||||
			{
 | 
			
		||||
				xmlContent = xmlContent.Substring("```xml".Length, xmlContent.Length - "```xml".Length - "```".Length).Trim();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			var serializer = new XmlSerializer(typeof(List<QuestionGroup>), new XmlRootAttribute("QGs"));
 | 
			
		||||
 | 
			
		||||
			using (StringReader reader = new StringReader(xmlContent))
 | 
			
		||||
			{
 | 
			
		||||
				try
 | 
			
		||||
				{
 | 
			
		||||
					List<QuestionGroup> questionGroups = (List<QuestionGroup>)serializer.Deserialize(reader);
 | 
			
		||||
					return questionGroups;
 | 
			
		||||
				}
 | 
			
		||||
				catch (InvalidOperationException ex)
 | 
			
		||||
				{
 | 
			
		||||
					Console.WriteLine($"XML 反序列化操作错误: {ex.Message}");
 | 
			
		||||
					Console.WriteLine($"内部异常: {ex.InnerException?.Message}");
 | 
			
		||||
					return null;
 | 
			
		||||
				}
 | 
			
		||||
				catch (Exception ex)
 | 
			
		||||
				{
 | 
			
		||||
					Console.WriteLine($"处理错误: {ex.Message}");
 | 
			
		||||
					return null;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										57
									
								
								TechHelper.Client/Features/JWTParser.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								TechHelper.Client/Features/JWTParser.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
 | 
			
		||||
namespace TechHelper.Features
 | 
			
		||||
{
 | 
			
		||||
	public static class JWTParser
 | 
			
		||||
	{
 | 
			
		||||
		public static IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
 | 
			
		||||
		{
 | 
			
		||||
			var claims = new List<Claim>();
 | 
			
		||||
			var payload = jwt.Split(".")[1];
 | 
			
		||||
 | 
			
		||||
			var jsonBytes = ParseBase64WithoutPadding(payload);
 | 
			
		||||
 | 
			
		||||
			var keyValuePairs = JsonSerializer
 | 
			
		||||
				.Deserialize<Dictionary<string, object>>(jsonBytes);
 | 
			
		||||
 | 
			
		||||
			ExtractRolesFromJWT(claims, keyValuePairs);
 | 
			
		||||
 | 
			
		||||
			claims.AddRange(keyValuePairs
 | 
			
		||||
				.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString())));
 | 
			
		||||
 | 
			
		||||
			return claims;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		private static byte[] ParseBase64WithoutPadding(string base64)
 | 
			
		||||
		{
 | 
			
		||||
			switch (base64.Length % 4)
 | 
			
		||||
			{
 | 
			
		||||
				case 2: base64 += "=="; break;
 | 
			
		||||
				case 3: base64 += "="; break;
 | 
			
		||||
			}
 | 
			
		||||
			return Convert.FromBase64String(base64);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		private static void ExtractRolesFromJWT(List<Claim> claims, Dictionary<string, object> keyValuePairs)
 | 
			
		||||
		{
 | 
			
		||||
			keyValuePairs.TryGetValue(ClaimTypes.Role, out object roles);
 | 
			
		||||
			if (roles != null)
 | 
			
		||||
			{
 | 
			
		||||
				var parsedRoles = roles.ToString().Trim().TrimStart('[').TrimEnd(']').Split(',');
 | 
			
		||||
				if (parsedRoles.Length > 1)
 | 
			
		||||
				{
 | 
			
		||||
					foreach (var parsedRole in parsedRoles)
 | 
			
		||||
					{
 | 
			
		||||
						claims.Add(new Claim(ClaimTypes.Role, parsedRole.Trim('"')));
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				else
 | 
			
		||||
				{
 | 
			
		||||
					claims.Add(new Claim(ClaimTypes.Role, parsedRoles[0]));
 | 
			
		||||
				}
 | 
			
		||||
				keyValuePairs.Remove(ClaimTypes.Role);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,86 @@
 | 
			
		||||
using TechHelper.Client.HttpRepository;
 | 
			
		||||
using Microsoft.AspNetCore.Components;
 | 
			
		||||
using System.Net;
 | 
			
		||||
using System.Net.Http.Headers;
 | 
			
		||||
using MudBlazor;
 | 
			
		||||
using Microsoft.Extensions.DependencyInjection;
 | 
			
		||||
using System;
 | 
			
		||||
 | 
			
		||||
namespace BlazorProducts.Client.HttpInterceptor
 | 
			
		||||
{
 | 
			
		||||
	public class HttpInterceptorHandlerService : DelegatingHandler
 | 
			
		||||
	{
 | 
			
		||||
		private readonly NavigationManager _navManager;
 | 
			
		||||
 | 
			
		||||
		private readonly RefreshTokenService _refreshTokenService;
 | 
			
		||||
 | 
			
		||||
		private readonly ISnackbar _serviceProvider;
 | 
			
		||||
		private ISnackbar toastService = null;
 | 
			
		||||
 | 
			
		||||
		public HttpInterceptorHandlerService(
 | 
			
		||||
			NavigationManager navManager,
 | 
			
		||||
			RefreshTokenService refreshTokenService,
 | 
			
		||||
			ISnackbar serviceProvider)
 | 
			
		||||
		{
 | 
			
		||||
			_navManager = navManager;
 | 
			
		||||
			_refreshTokenService = refreshTokenService;
 | 
			
		||||
			_serviceProvider = serviceProvider;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
 | 
			
		||||
		{
 | 
			
		||||
			var absolutePath = request.RequestUri?.AbsolutePath;
 | 
			
		||||
 | 
			
		||||
			_serviceProvider.Add("HELLO");
 | 
			
		||||
			if (absolutePath != null && !absolutePath.Contains("token") && !absolutePath.Contains("account"))
 | 
			
		||||
			{
 | 
			
		||||
				var token = await _refreshTokenService.TryRefreshToken();
 | 
			
		||||
				if (!string.IsNullOrEmpty(token))
 | 
			
		||||
				{
 | 
			
		||||
					request.Headers.Authorization =
 | 
			
		||||
						new AuthenticationHeaderValue("bearer", token);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			var response = await base.SendAsync(request, cancellationToken);
 | 
			
		||||
 | 
			
		||||
			HandleResponse(response);
 | 
			
		||||
 | 
			
		||||
			return response;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		private void HandleResponse(HttpResponseMessage response)
 | 
			
		||||
		{
 | 
			
		||||
			if (response is null)
 | 
			
		||||
			{
 | 
			
		||||
				_navManager.NavigateTo("/error");
 | 
			
		||||
				throw new HttpResponseException("Server not available.");
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			var message = "";
 | 
			
		||||
 | 
			
		||||
			if (!response.IsSuccessStatusCode)
 | 
			
		||||
			{
 | 
			
		||||
				switch (response.StatusCode)
 | 
			
		||||
				{
 | 
			
		||||
					case HttpStatusCode.NotFound:
 | 
			
		||||
						_navManager.NavigateTo("/404");
 | 
			
		||||
						message = "Resource not found.";
 | 
			
		||||
						break;
 | 
			
		||||
					case HttpStatusCode.BadRequest:
 | 
			
		||||
						message = "Invalid request. Please try again.";
 | 
			
		||||
						break;
 | 
			
		||||
					case HttpStatusCode.Unauthorized:
 | 
			
		||||
						_navManager.NavigateTo("/unauthorized");
 | 
			
		||||
						message = "Unauthorized access";
 | 
			
		||||
						break;
 | 
			
		||||
					default:
 | 
			
		||||
						_navManager.NavigateTo("/error");
 | 
			
		||||
						message = "Something went wrong. Please contact the administrator.";
 | 
			
		||||
						break;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								TechHelper.Client/HttpInterceptor/HttpResponseException.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								TechHelper.Client/HttpInterceptor/HttpResponseException.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
using System;
 | 
			
		||||
using System.Runtime.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace BlazorProducts.Client.HttpInterceptor
 | 
			
		||||
{
 | 
			
		||||
	[Serializable]
 | 
			
		||||
	internal class HttpResponseException : Exception
 | 
			
		||||
	{
 | 
			
		||||
		public HttpResponseException()
 | 
			
		||||
		{
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public HttpResponseException(string message) : base(message)
 | 
			
		||||
		{
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public HttpResponseException(string message, Exception innerException) 
 | 
			
		||||
			: base(message, innerException)
 | 
			
		||||
		{
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		protected HttpResponseException(SerializationInfo info, StreamingContext context) 
 | 
			
		||||
			: base(info, context)
 | 
			
		||||
		{
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										198
									
								
								TechHelper.Client/HttpRepository/AuthenticationClientService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								TechHelper.Client/HttpRepository/AuthenticationClientService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,198 @@
 | 
			
		||||
using TechHelper.Client.AuthProviders;
 | 
			
		||||
using Entities.DTO;
 | 
			
		||||
using Microsoft.AspNetCore.Components.Authorization;
 | 
			
		||||
using System.Net.Http;
 | 
			
		||||
using System.Net.Http.Json;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
using System.Net;
 | 
			
		||||
using Microsoft.AspNetCore.Components;
 | 
			
		||||
using Microsoft.AspNetCore.WebUtilities;
 | 
			
		||||
using Microsoft.JSInterop;
 | 
			
		||||
 | 
			
		||||
namespace TechHelper.Client.HttpRepository
 | 
			
		||||
{
 | 
			
		||||
	public class AuthenticationClientService : IAuthenticationClientService
 | 
			
		||||
	{
 | 
			
		||||
		private HttpClient _client;
 | 
			
		||||
		private readonly IHttpClientFactory _clientFactory;
 | 
			
		||||
		private readonly JsonSerializerOptions _options =
 | 
			
		||||
			new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
 | 
			
		||||
		private readonly AuthenticationStateProvider _stateProvider;
 | 
			
		||||
		private readonly ILocalStorageService _localStorageService;
 | 
			
		||||
		private readonly NavigationManager _navigationManager;
 | 
			
		||||
 | 
			
		||||
		public AuthenticationClientService(IHttpClientFactory httpClientFactory,
 | 
			
		||||
			AuthenticationStateProvider authenticationStateProvider,
 | 
			
		||||
			ILocalStorageService localStorageService,
 | 
			
		||||
			NavigationManager navigationManager)
 | 
			
		||||
		{
 | 
			
		||||
			_clientFactory = httpClientFactory;
 | 
			
		||||
			//_client = httpClientFactory.CreateClient("Default");
 | 
			
		||||
			_localStorageService = localStorageService;
 | 
			
		||||
			_stateProvider = authenticationStateProvider;
 | 
			
		||||
			_navigationManager = navigationManager;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public async Task<AuthResponseDto> LoginAsync(UserForAuthenticationDto userForAuthenticationDto)
 | 
			
		||||
		{
 | 
			
		||||
			using (_client = _clientFactory.CreateClient("Default"))
 | 
			
		||||
			{
 | 
			
		||||
 | 
			
		||||
				var reponse = await _client.PostAsJsonAsync("account/login",
 | 
			
		||||
					userForAuthenticationDto);
 | 
			
		||||
 | 
			
		||||
				var content = await reponse.Content.ReadAsStringAsync();
 | 
			
		||||
 | 
			
		||||
				var result = JsonSerializer.Deserialize<AuthResponseDto>(content, _options);
 | 
			
		||||
 | 
			
		||||
				if (!reponse.IsSuccessStatusCode || result.Is2StepVerificationRequired)
 | 
			
		||||
					return result;
 | 
			
		||||
 | 
			
		||||
				 _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);
 | 
			
		||||
 | 
			
		||||
				return new AuthResponseDto { IsAuthSuccessful = true };
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public async Task LogoutAsync()
 | 
			
		||||
		{
 | 
			
		||||
			using (_client = _clientFactory.CreateClient("Default"))
 | 
			
		||||
			{
 | 
			
		||||
				 _localStorageService.RemoveItem("authToken");
 | 
			
		||||
				 _localStorageService.RemoveItem("refreshToken");
 | 
			
		||||
				((AuthStateProvider)_stateProvider).NotifyUserLogout();
 | 
			
		||||
 | 
			
		||||
				_client.DefaultRequestHeaders.Authorization = null;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public async Task<string> RefreshTokenAsync()
 | 
			
		||||
		{
 | 
			
		||||
			using (_client = _clientFactory.CreateClient("Default"))
 | 
			
		||||
			{
 | 
			
		||||
				var token =  _localStorageService.GetItem<string>("authToken");
 | 
			
		||||
				var refreshToken =  _localStorageService.GetItem<string>("refreshToken");
 | 
			
		||||
 | 
			
		||||
				var response = await _client.PostAsJsonAsync("token/refresh",
 | 
			
		||||
					new RefreshTokenDto
 | 
			
		||||
					{
 | 
			
		||||
						Token = token,
 | 
			
		||||
						RefreshToken = refreshToken
 | 
			
		||||
					});
 | 
			
		||||
 | 
			
		||||
				var content = await response.Content.ReadAsStringAsync();
 | 
			
		||||
				var result = JsonSerializer.Deserialize<AuthResponseDto>(content, _options);
 | 
			
		||||
 | 
			
		||||
				 _localStorageService.SetItem("authToken", result.Token);
 | 
			
		||||
				 _localStorageService.SetItem("refreshToken", result.RefreshToken);
 | 
			
		||||
 | 
			
		||||
				_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", result.Token);
 | 
			
		||||
 | 
			
		||||
				return result.Token;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public async Task<ResponseDto> RegisterUserAsync(UserForRegistrationDto userForRegistrationDto)
 | 
			
		||||
		{
 | 
			
		||||
			using (_client = _clientFactory.CreateClient("Default"))
 | 
			
		||||
			{
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
				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<ResponseDto>(content, _options);
 | 
			
		||||
 | 
			
		||||
					return result;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				return new ResponseDto { IsSuccessfulRegistration = true };
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public async Task<HttpStatusCode> ForgotPasswordAsync(ForgotPasswordDto forgotPasswordDto)
 | 
			
		||||
		{
 | 
			
		||||
			using (_client = _clientFactory.CreateClient("Default"))
 | 
			
		||||
			{
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
				forgotPasswordDto.ClientURI = Path.Combine(_navigationManager.BaseUri, "resetpassword");
 | 
			
		||||
				var result = await _client.PostAsJsonAsync("account/forgotpassword",
 | 
			
		||||
					forgotPasswordDto);
 | 
			
		||||
 | 
			
		||||
				return result.StatusCode;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public async Task<ResetPasswordResponseDto> ResetPasswordAsync(ResetPasswordDto resetPasswordDto)
 | 
			
		||||
		{
 | 
			
		||||
			using (_client = _clientFactory.CreateClient("Default"))
 | 
			
		||||
			{
 | 
			
		||||
				var resetresult = await _client.PostAsJsonAsync("account/resetpassword",
 | 
			
		||||
					resetPasswordDto);
 | 
			
		||||
 | 
			
		||||
				var resetContent = await resetresult.Content.ReadAsStringAsync();
 | 
			
		||||
 | 
			
		||||
				var result = JsonSerializer.Deserialize<ResetPasswordResponseDto>(resetContent, _options);
 | 
			
		||||
 | 
			
		||||
				return result;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public async Task<HttpStatusCode> EmailConfirmationAsync(string email, string token)
 | 
			
		||||
		{
 | 
			
		||||
			var queryStringParam = new Dictionary<string, string>
 | 
			
		||||
			{
 | 
			
		||||
				["email"] = email,
 | 
			
		||||
				["token"] = token
 | 
			
		||||
			};
 | 
			
		||||
			using (_client = _clientFactory.CreateClient("Default"))
 | 
			
		||||
			{
 | 
			
		||||
				var response = await _client.GetAsync(QueryHelpers.AddQueryString(
 | 
			
		||||
					"account/emailconfirmation", queryStringParam));
 | 
			
		||||
 | 
			
		||||
				return response.StatusCode;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public async Task<AuthResponseDto> LoginVerfication(TwoFactorVerificationDto twoFactorVerificationDto)
 | 
			
		||||
		{
 | 
			
		||||
			using (_client = _clientFactory.CreateClient("Default"))
 | 
			
		||||
			{
 | 
			
		||||
 | 
			
		||||
				var reponse = await _client.PostAsJsonAsync("account/twostepverification",
 | 
			
		||||
					twoFactorVerificationDto);
 | 
			
		||||
 | 
			
		||||
				var content = await reponse.Content.ReadAsStringAsync();
 | 
			
		||||
 | 
			
		||||
				var result = JsonSerializer.Deserialize<AuthResponseDto>(content, _options);
 | 
			
		||||
 | 
			
		||||
				if (!reponse.IsSuccessStatusCode)
 | 
			
		||||
					return result;
 | 
			
		||||
 | 
			
		||||
				 _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);
 | 
			
		||||
 | 
			
		||||
				return new AuthResponseDto { IsAuthSuccessful = true };
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,17 @@
 | 
			
		||||
using Entities.DTO;
 | 
			
		||||
using System.Net;
 | 
			
		||||
 | 
			
		||||
namespace TechHelper.Client.HttpRepository
 | 
			
		||||
{
 | 
			
		||||
	public interface IAuthenticationClientService
 | 
			
		||||
	{
 | 
			
		||||
		Task<ResponseDto> RegisterUserAsync(UserForRegistrationDto userForRegistrationDto);
 | 
			
		||||
		Task<AuthResponseDto> LoginAsync(UserForAuthenticationDto userForAuthenticationDto);
 | 
			
		||||
		Task LogoutAsync();
 | 
			
		||||
		Task<string> RefreshTokenAsync();
 | 
			
		||||
		Task<HttpStatusCode> ForgotPasswordAsync(ForgotPasswordDto forgotPasswordDto);
 | 
			
		||||
		Task<ResetPasswordResponseDto> ResetPasswordAsync(ResetPasswordDto resetPasswordDto);
 | 
			
		||||
		Task<HttpStatusCode> EmailConfirmationAsync(string email, string token);
 | 
			
		||||
		Task<AuthResponseDto> LoginVerfication(TwoFactorVerificationDto twoFactorVerificationDto);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										34
									
								
								TechHelper.Client/HttpRepository/RefreshTokenService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								TechHelper.Client/HttpRepository/RefreshTokenService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
using Microsoft.AspNetCore.Components.Authorization;
 | 
			
		||||
 | 
			
		||||
namespace TechHelper.Client.HttpRepository
 | 
			
		||||
{
 | 
			
		||||
	public class RefreshTokenService
 | 
			
		||||
	{
 | 
			
		||||
		private readonly AuthenticationStateProvider _authenticationStateProvider;
 | 
			
		||||
		private readonly IAuthenticationClientService _authenticationClientService;
 | 
			
		||||
 | 
			
		||||
		public RefreshTokenService(AuthenticationStateProvider authenticationStateProvider, IAuthenticationClientService authenticationClientService)
 | 
			
		||||
		{
 | 
			
		||||
			_authenticationStateProvider = authenticationStateProvider;
 | 
			
		||||
			_authenticationClientService = authenticationClientService;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public async Task<string> TryRefreshToken()
 | 
			
		||||
		{
 | 
			
		||||
			var authState = await _authenticationStateProvider.GetAuthenticationStateAsync();
 | 
			
		||||
			var user = authState.User;
 | 
			
		||||
			var expClaim = user.FindFirst(c => c.Type.Equals("exp")).Value;
 | 
			
		||||
			var expTime = DateTimeOffset.FromUnixTimeSeconds(
 | 
			
		||||
				Convert.ToInt64(expClaim));
 | 
			
		||||
 | 
			
		||||
			var diff = expTime - DateTime.UtcNow;
 | 
			
		||||
			if (diff.TotalMinutes <= 2)
 | 
			
		||||
				return await _authenticationClientService.RefreshTokenAsync();
 | 
			
		||||
 | 
			
		||||
			return string.Empty;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								TechHelper.Client/Layout/LoginDisplay.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								TechHelper.Client/Layout/LoginDisplay.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
 | 
			
		||||
@inject NavigationManager Navigation
 | 
			
		||||
 | 
			
		||||
<AuthorizeView>
 | 
			
		||||
    <Authorized>
 | 
			
		||||
        Hello, @context.User.Identity?.Name!
 | 
			
		||||
        <button class="nav-link btn btn-link" @onclick="BeginLogOut">Log out</button>
 | 
			
		||||
    </Authorized>
 | 
			
		||||
    <NotAuthorized>
 | 
			
		||||
        <a href="authentication/login">Log in</a>
 | 
			
		||||
    </NotAuthorized>
 | 
			
		||||
</AuthorizeView>
 | 
			
		||||
 | 
			
		||||
@code{
 | 
			
		||||
    public void BeginLogOut()
 | 
			
		||||
    {
 | 
			
		||||
        Navigation.NavigateToLogout("authentication/logout");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										42
									
								
								TechHelper.Client/Layout/MainLayout.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								TechHelper.Client/Layout/MainLayout.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
@inherits LayoutComponentBase
 | 
			
		||||
<MudThemeProvider />
 | 
			
		||||
<MudDialogProvider />
 | 
			
		||||
<MudSnackbarProvider />
 | 
			
		||||
<MudPopoverProvider />
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<MudPaper Class="d-flex flex-column flex-grow-1" Style="height: 100vh;">
 | 
			
		||||
 | 
			
		||||
	
 | 
			
		||||
	<MudPaper Class="d-flex flex-column flex-grow-1 overflow-hidden">
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		<MudPaper Height="5%" Class=" d-flex flex-grow-1" Style="background-color:mediumseagreen">
 | 
			
		||||
			<MudSpacer> </MudSpacer>
 | 
			
		||||
			<AuthLinks/>
 | 
			
		||||
		</MudPaper>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		<MudPaper Height="95%" Class="d-flex flex-row flex-grow-1 overflow-hidden">
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
			<MudPaper Width="5%" Class="pa-2 mr-1 d-flex  flex-column flex-grow-0 justify-content-between">
 | 
			
		||||
				<NavBar Class="flex-column flex-grow-0 rounded-pill" />
 | 
			
		||||
				<AccountView Class="flex-column flex-grow-0 rounded-pill" />
 | 
			
		||||
			</MudPaper>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
			<MudPaper Class="d-flex flex-grow-1 pa-3 ma-1 ">
 | 
			
		||||
				@Body
 | 
			
		||||
			</MudPaper>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		</MudPaper>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	</MudPaper>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
</MudPaper>
 | 
			
		||||
							
								
								
									
										85
									
								
								TechHelper.Client/Layout/MainLayout.razor.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								TechHelper.Client/Layout/MainLayout.razor.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,85 @@
 | 
			
		||||
.page {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main {
 | 
			
		||||
    flex: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.sidebar {
 | 
			
		||||
    background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.my-hover-paper:hover {
 | 
			
		||||
    background-color: var(--mud-palette-primary); /* <20><>ͣʱ<CDA3><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ϊ<EFBFBD><CEAA><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɫ */
 | 
			
		||||
    border-color: var(--mud-palette-primary-lighten); /* <20><>ͣʱ<CDA3>߿<EFBFBD><DFBF><EFBFBD>ɫ<EFBFBD>仯 */
 | 
			
		||||
    color: var(--mud-palette-primary-contrasttext); /* <20><>ͣʱ<CDA3><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɫ<EFBFBD>仯 */
 | 
			
		||||
    transform: translateY(-3px); /* <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ƶ<EFBFBD><C6B6><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӷ<EFBFBD><D3B6><EFBFBD> */
 | 
			
		||||
    box-shadow: var(--mud-elevation-4); /* <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӱ<EFBFBD><D3B0><EFBFBD><EFBFBD> */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.top-row {
 | 
			
		||||
    background-color: #f7f7f7;
 | 
			
		||||
    border-bottom: 1px solid #d6d5d5;
 | 
			
		||||
    justify-content: flex-end;
 | 
			
		||||
    height: 3.5rem;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    .top-row ::deep a, .top-row ::deep .btn-link {
 | 
			
		||||
        white-space: nowrap;
 | 
			
		||||
        margin-left: 1.5rem;
 | 
			
		||||
        text-decoration: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
 | 
			
		||||
        text-decoration: underline;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .top-row ::deep a:first-child {
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
        text-overflow: ellipsis;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@media (max-width: 640.98px) {
 | 
			
		||||
    .top-row {
 | 
			
		||||
        justify-content: space-between;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .top-row ::deep a, .top-row ::deep .btn-link {
 | 
			
		||||
        margin-left: 0;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (min-width: 641px) {
 | 
			
		||||
    .page {
 | 
			
		||||
        flex-direction: row;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .sidebar {
 | 
			
		||||
        width: 250px;
 | 
			
		||||
        height: 100vh;
 | 
			
		||||
        position: sticky;
 | 
			
		||||
        top: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .top-row {
 | 
			
		||||
        position: sticky;
 | 
			
		||||
        top: 0;
 | 
			
		||||
        z-index: 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .top-row.auth ::deep a:first-child {
 | 
			
		||||
        flex: 1;
 | 
			
		||||
        text-align: right;
 | 
			
		||||
        width: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .top-row, article {
 | 
			
		||||
        padding-left: 2rem !important;
 | 
			
		||||
        padding-right: 1.5rem !important;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								TechHelper.Client/Layout/NavBar.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								TechHelper.Client/Layout/NavBar.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
<MudPaper Class=@Class>
 | 
			
		||||
	<MudNavLink Icon="@Icons.Material.Filled.Home" Class="py-5 px-3" Href="" Match="NavLinkMatch.All"></MudNavLink>
 | 
			
		||||
	<MudNavLink Icon="@Icons.Material.Filled.Person" Class="py-5 px-3" Href="Account/Manage"></MudNavLink>
 | 
			
		||||
	<MudNavLink Icon="@Icons.Material.Filled.Edit" Class="py-5 px-3" Href="Edit"></MudNavLink>
 | 
			
		||||
	<MudNavLink Icon="@Icons.Material.Filled.Air" Class="py-5 px-3" Href="ai"></MudNavLink>
 | 
			
		||||
	<MudNavLink Icon="@Icons.Material.Filled.SportsTennis" Class="py-5 px-3" Href="test"></MudNavLink>
 | 
			
		||||
</MudPaper>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@code {
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public string? Class { get; set; }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										52
									
								
								TechHelper.Client/Layout/NavMenu.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								TechHelper.Client/Layout/NavMenu.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
<div class="top-row ps-3 navbar navbar-dark">
 | 
			
		||||
    <div class="container-fluid">
 | 
			
		||||
        <a class="navbar-brand" href="">TechHelper.Client</a>
 | 
			
		||||
        <button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
 | 
			
		||||
            <span class="navbar-toggler-icon"></span>
 | 
			
		||||
        </button>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
 | 
			
		||||
    <nav class="flex-column">
 | 
			
		||||
        <div class="nav-item px-3">
 | 
			
		||||
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
 | 
			
		||||
                <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
 | 
			
		||||
            </NavLink>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="nav-item px-3">
 | 
			
		||||
            <NavLink class="nav-link" href="Account/Manage">
 | 
			
		||||
                <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> 个人中心
 | 
			
		||||
            </NavLink>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="nav-item px-3">
 | 
			
		||||
            <NavLink class="nav-link" href="Edit">
 | 
			
		||||
                <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> 编辑器
 | 
			
		||||
            </NavLink>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="nav-item px-3">
 | 
			
		||||
            <NavLink class="nav-link" href="test">
 | 
			
		||||
                <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> 测试页面
 | 
			
		||||
            </NavLink>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="nav-item px-3">
 | 
			
		||||
            <NavLink class="nav-link" href="ai">
 | 
			
		||||
                <span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> AI
 | 
			
		||||
            </NavLink>
 | 
			
		||||
        </div>
 | 
			
		||||
    </nav>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@code {
 | 
			
		||||
    private bool collapseNavMenu = true;
 | 
			
		||||
 | 
			
		||||
    private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
 | 
			
		||||
 | 
			
		||||
    private void ToggleNavMenu()
 | 
			
		||||
    {
 | 
			
		||||
        collapseNavMenu = !collapseNavMenu;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										83
									
								
								TechHelper.Client/Layout/NavMenu.razor.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								TechHelper.Client/Layout/NavMenu.razor.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,83 @@
 | 
			
		||||
.navbar-toggler {
 | 
			
		||||
    background-color: rgba(255, 255, 255, 0.1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.top-row {
 | 
			
		||||
    height: 3.5rem;
 | 
			
		||||
    background-color: rgba(0,0,0,0.4);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navbar-brand {
 | 
			
		||||
    font-size: 1.1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.bi {
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    width: 1.25rem;
 | 
			
		||||
    height: 1.25rem;
 | 
			
		||||
    margin-right: 0.75rem;
 | 
			
		||||
    top: -1px;
 | 
			
		||||
    background-size: cover;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.bi-house-door-fill-nav-menu {
 | 
			
		||||
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.bi-plus-square-fill-nav-menu {
 | 
			
		||||
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.bi-list-nested-nav-menu {
 | 
			
		||||
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nav-item {
 | 
			
		||||
    font-size: 0.9rem;
 | 
			
		||||
    padding-bottom: 0.5rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    .nav-item:first-of-type {
 | 
			
		||||
        padding-top: 1rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .nav-item:last-of-type {
 | 
			
		||||
        padding-bottom: 1rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .nav-item ::deep a {
 | 
			
		||||
        color: #d7d7d7;
 | 
			
		||||
        border-radius: 4px;
 | 
			
		||||
        height: 3rem;
 | 
			
		||||
        display: flex;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        line-height: 3rem;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
.nav-item ::deep a.active {
 | 
			
		||||
    background-color: rgba(255,255,255,0.37);
 | 
			
		||||
    color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nav-item ::deep a:hover {
 | 
			
		||||
    background-color: rgba(255,255,255,0.1);
 | 
			
		||||
    color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (min-width: 641px) {
 | 
			
		||||
    .navbar-toggler {
 | 
			
		||||
        display: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .collapse {
 | 
			
		||||
        /* Never collapse the sidebar for wide screens */
 | 
			
		||||
        display: block;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .nav-scrollable {
 | 
			
		||||
        /* Allow sidebar to scroll for tall menus */
 | 
			
		||||
        height: calc(100vh - 3.5rem);
 | 
			
		||||
        overflow-y: auto;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								TechHelper.Client/Layout/RedirectToLogin.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								TechHelper.Client/Layout/RedirectToLogin.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
 | 
			
		||||
@inject NavigationManager Navigation
 | 
			
		||||
 | 
			
		||||
@code {
 | 
			
		||||
    protected override void OnInitialized()
 | 
			
		||||
    {
 | 
			
		||||
        Navigation.NavigateToLogin("authentication/login");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										196
									
								
								TechHelper.Client/Pages/AI/AIDialog.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								TechHelper.Client/Pages/AI/AIDialog.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,196 @@
 | 
			
		||||
@page "/ai"
 | 
			
		||||
@using Newtonsoft.Json
 | 
			
		||||
@using TechHelper.Client
 | 
			
		||||
@using TechHelper.Client.AI
 | 
			
		||||
@using TechHelper.Client.Exam
 | 
			
		||||
@inject IAIService AiService
 | 
			
		||||
@inject ISnackbar Snackbar
 | 
			
		||||
 | 
			
		||||
<MudPaper Elevation="3" Height="100%" Width="100%">
 | 
			
		||||
 | 
			
		||||
	<MudContainer>
 | 
			
		||||
 | 
			
		||||
		<MudAppBar Elevation="1">
 | 
			
		||||
			<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@((e) => ToggleDrawer())" />
 | 
			
		||||
			<MudText Typo="Typo.h6" Class="ml-4">AI 对话</MudText>
 | 
			
		||||
			<MudSpacer />
 | 
			
		||||
			<MudButton Variant="Variant.Text" Color="Color.Inherit" OnClick="ClearChat">清空对话</MudButton>
 | 
			
		||||
			<MudIconButton Icon="@Icons.Material.Filled.Settings" Color="Color.Inherit" OnClick="@(() => IsSettingsOpen = !IsSettingsOpen)" />
 | 
			
		||||
		</MudAppBar>
 | 
			
		||||
		<MudDrawer @bind-Open="@_drawerOpen" Elevation="1" Variant="@DrawerVariant.Temporary">
 | 
			
		||||
			<MudDrawerHeader>
 | 
			
		||||
				<MudText Typo="Typo.h6">对话历史 / 设置</MudText>
 | 
			
		||||
			</MudDrawerHeader>
 | 
			
		||||
			<MudNavMenu>
 | 
			
		||||
				<MudNavLink Href="/ai-chat" Icon="@Icons.Material.Filled.Chat">新对话</MudNavLink>
 | 
			
		||||
				<MudDivider />
 | 
			
		||||
				@* 假设这里可以显示对话历史,点击切换
 | 
			
		||||
            @foreach (var history in ChatHistorySummaries)
 | 
			
		||||
            {
 | 
			
		||||
                <MudNavLink @onclick="@(() => LoadChat(history.Id))">@history.Title</MudNavLink>
 | 
			
		||||
            }
 | 
			
		||||
            *@
 | 
			
		||||
				<MudExpansionPanel Text="模型设置" @bind-Expanded="@IsSettingsOpen" Class="my-2">
 | 
			
		||||
					<MudCard>
 | 
			
		||||
						<MudCardContent>
 | 
			
		||||
							<MudSelect Label="模型选择" @bind-Value="SelectedModel" Variant="Variant.Filled" Margin="Margin.Dense">
 | 
			
		||||
								@foreach (var model in Enum.GetValues<AIModelsEnum>())
 | 
			
		||||
								{
 | 
			
		||||
									<MudSelectItem Value="@model.GetDescription()">@model.ToString()</MudSelectItem>
 | 
			
		||||
								}
 | 
			
		||||
							</MudSelect>
 | 
			
		||||
							<MudSlider @bind-Value="Temperature" Min="0.0m" Max="1.0m" Step="0.01m" T="decimal" Label="Temperature" Class="mt-4">
 | 
			
		||||
								<ChildContent>
 | 
			
		||||
									<MudText Typo="Typo.body2">@Temperature.ToString("F2")</MudText>
 | 
			
		||||
								</ChildContent>
 | 
			
		||||
							</MudSlider>
 | 
			
		||||
							<MudNumericField @bind-Value="MaxTokens" Label="最大Token数" Min="1" Max="32000" Step="1" Variant="Variant.Filled" Margin="Margin.Dense" Class="mt-4" />
 | 
			
		||||
						</MudCardContent>
 | 
			
		||||
					</MudCard>
 | 
			
		||||
				</MudExpansionPanel>
 | 
			
		||||
			</MudNavMenu>
 | 
			
		||||
		</MudDrawer>
 | 
			
		||||
 | 
			
		||||
		<MudContainer MaxWidth="MaxWidth.Medium">
 | 
			
		||||
			@foreach (var message in ChatMessages)
 | 
			
		||||
			{
 | 
			
		||||
				<MudChat Color="Color.Success" Dense="true" Elevation="0" Variant="Variant.Text" ChatPosition="@(message.Role == "user" ? ChatBubblePosition.End : ChatBubblePosition.Start)">
 | 
			
		||||
					<MudChatHeader Name="@(message.Role == "user" ? "user" : "ai")" Time="12:46" />
 | 
			
		||||
					<MudAvatar Size=" Size.Small">
 | 
			
		||||
						<MudImage Src="images/toiletvisit.jpg" />
 | 
			
		||||
					</MudAvatar>
 | 
			
		||||
					<MudChatBubble> @message.Content </MudChatBubble>
 | 
			
		||||
					<MudChatFooter Text="Seen at 12:46" />
 | 
			
		||||
				</MudChat>
 | 
			
		||||
			}
 | 
			
		||||
			@if (IsLoading)
 | 
			
		||||
			{
 | 
			
		||||
				<MudPaper Class="ai-message mb-2 pa-3 loading-indicator" Elevation="1">
 | 
			
		||||
					<MudText Typo="Typo.body2"><strong>AI:</strong></MudText>
 | 
			
		||||
					<MudProgressCircular Indeterminate="true" Size="Size.Small" />
 | 
			
		||||
					<MudText Typo="Typo.body2" Class="ml-2">思考中...</MudText>
 | 
			
		||||
				</MudPaper>
 | 
			
		||||
			}
 | 
			
		||||
			<div @ref="messagesEndRef" class="scroll-anchor"></div> @* 滚动锚点 *@
 | 
			
		||||
		</MudContainer>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		<MudPaper Class="input-area d-flex align-center pa-3" Elevation="8" Style="position: fixed; bottom: 0; left: 0; right: 0; z-index: 1000; width: 100%;">
 | 
			
		||||
			<MudContainer MaxWidth="MaxWidth.Medium" Class="d-flex" Style="width: 100%;">
 | 
			
		||||
				<MudTextField @bind-Value="UserInput"
 | 
			
		||||
				Label="输入你的消息..."
 | 
			
		||||
				Variant="Variant.Filled"
 | 
			
		||||
				Lines="1"
 | 
			
		||||
				Adornment="Adornment.End"
 | 
			
		||||
				AdornmentIcon="@Icons.Material.Filled.Send"
 | 
			
		||||
				OnAdornmentClick="SendMessage"
 | 
			
		||||
				Class="flex-grow-1 mr-2"
 | 
			
		||||
				Disabled="@IsLoading"
 | 
			
		||||
				KeyDown="@HandleKeyDown" />
 | 
			
		||||
			</MudContainer>
 | 
			
		||||
		</MudPaper>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	</MudContainer>
 | 
			
		||||
</MudPaper>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@code {
 | 
			
		||||
	private bool _drawerOpen = false;
 | 
			
		||||
	private bool IsSettingsOpen = false;
 | 
			
		||||
	private bool IsExamCheckOpen = false;
 | 
			
		||||
 | 
			
		||||
	private List<Message> ChatMessages = new List<Message>();
 | 
			
		||||
	private string UserInput { get; set; } = "";
 | 
			
		||||
	private bool IsLoading { get; set; } = false;
 | 
			
		||||
 | 
			
		||||
	private ElementReference messagesEndRef; // 用于自动滚动到底部的引用
 | 
			
		||||
 | 
			
		||||
	// AI 模型设置
 | 
			
		||||
	private string SelectedModel { get; set; } = AIModelsEnum.GLMZ1Flash.GetDescription();
 | 
			
		||||
	private decimal Temperature { get; set; } = 0.75m; // MudSlider 使用 decimal
 | 
			
		||||
	private int MaxTokens { get; set; } = 1000;
 | 
			
		||||
 | 
			
		||||
	// 试题检查相关
 | 
			
		||||
	private string ExamJsonInput { get; set; } = "";
 | 
			
		||||
	private string ExamCheckResult { get; set; } = "";
 | 
			
		||||
 | 
			
		||||
	protected override void OnInitialized()
 | 
			
		||||
	{
 | 
			
		||||
		ChatMessages.Add(new SystemMessage("你是一个乐于助人的AI助手。"));
 | 
			
		||||
		Snackbar.Add("欢迎使用AI对话!", Severity.Info);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	private void ToggleDrawer()
 | 
			
		||||
	{
 | 
			
		||||
		_drawerOpen = !_drawerOpen;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void ClearChat()
 | 
			
		||||
	{
 | 
			
		||||
		ChatMessages.Clear();
 | 
			
		||||
		ChatMessages.Add(new SystemMessage("你是一个乐于助人的AI助手。"));
 | 
			
		||||
		Snackbar.Add("对话已清空。", Severity.Success);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private async Task HandleKeyDown(KeyboardEventArgs e)
 | 
			
		||||
	{
 | 
			
		||||
		if (e.Key == "Enter" && !e.ShiftKey)
 | 
			
		||||
		{
 | 
			
		||||
			await SendMessage();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private async Task SendMessage()
 | 
			
		||||
	{
 | 
			
		||||
		if (string.IsNullOrWhiteSpace(UserInput)) return;
 | 
			
		||||
 | 
			
		||||
		IsLoading = true;
 | 
			
		||||
		ChatMessages.Add(new UserMessage(UserInput));
 | 
			
		||||
		var currentInput = UserInput; // 保存当前输入,因为会清空
 | 
			
		||||
 | 
			
		||||
		try
 | 
			
		||||
		{
 | 
			
		||||
			// 构建请求消息列表,不包括SystemMessage(通常System Message只在API调用时作为参数传递,不显示在聊天记录中)
 | 
			
		||||
			var requestMessages = new List<Message>();
 | 
			
		||||
			requestMessages.Add(new SystemMessage("你是一个乐于助人的AI助手,请简洁明了地回答问题。")); // 每次请求都包含系统提示
 | 
			
		||||
			requestMessages.AddRange(ChatMessages.Where(m => m.Role != "system")); // 添加除系统消息外的所有历史对话
 | 
			
		||||
 | 
			
		||||
			var request = new ChatCompletionRequest
 | 
			
		||||
				{
 | 
			
		||||
					Model = SelectedModel,
 | 
			
		||||
					Messages = requestMessages,
 | 
			
		||||
					Temperature = (float)Temperature, // MudSlider返回decimal,需要转换为float
 | 
			
		||||
					MaxTokens = MaxTokens
 | 
			
		||||
				};
 | 
			
		||||
 | 
			
		||||
			var response = await AiService.CallGLM(UserInput, AIConfiguration.ExamAnsConfig);
 | 
			
		||||
 | 
			
		||||
			if (!string.IsNullOrEmpty(response))
 | 
			
		||||
			{
 | 
			
		||||
				// var exam = ExamParser.ParseExamJson(response);
 | 
			
		||||
				ChatMessages.Add(new AssistantMessage(response));
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		catch (HttpRequestException httpEx)
 | 
			
		||||
		{
 | 
			
		||||
			ChatMessages.Add(new AssistantMessage($"API 请求失败: {httpEx.Message}. 请检查API Key和网络连接。"));
 | 
			
		||||
			Snackbar.Add($"API 请求失败: {httpEx.Message}", Severity.Error);
 | 
			
		||||
			Console.WriteLine($"HTTP Request Error: {httpEx.Message}");
 | 
			
		||||
		}
 | 
			
		||||
		catch (Exception ex)
 | 
			
		||||
		{
 | 
			
		||||
			ChatMessages.Add(new AssistantMessage($"发生错误: {ex.Message}"));
 | 
			
		||||
			Snackbar.Add($"发生错误: {ex.Message}", Severity.Error);
 | 
			
		||||
			Console.WriteLine($"Error: {ex.Message}");
 | 
			
		||||
		}
 | 
			
		||||
		finally
 | 
			
		||||
		{
 | 
			
		||||
			IsLoading = false;
 | 
			
		||||
			UserInput = ""; // 清空输入框
 | 
			
		||||
			StateHasChanged();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								TechHelper.Client/Pages/AccountView.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								TechHelper.Client/Pages/AccountView.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
<MudPaper Class=@Class>
 | 
			
		||||
	<MudNavLink Icon="@Icons.Material.Filled.Settings" Href="" Match="NavLinkMatch.All" Class="py-5 px-3"></MudNavLink>
 | 
			
		||||
	<MudNavLink Icon="@Icons.Material.Filled.Person4" Href="Account/Manage" Class="py-5 px-3"></MudNavLink>
 | 
			
		||||
</MudPaper>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@code {
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public string? Class { get; set; }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										26
									
								
								TechHelper.Client/Pages/AuthLinks.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								TechHelper.Client/Pages/AuthLinks.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
@inject IAuthenticationClientService AuthenticationClientService
 | 
			
		||||
@inject NavigationManager NavigationManager
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<AuthorizeView>
 | 
			
		||||
	<Authorized>
 | 
			
		||||
		<MudText>
 | 
			
		||||
			Hello, @context.User.Identity.Name!
 | 
			
		||||
		</MudText>
 | 
			
		||||
		<MudButton OnClick="Logout"> LOGOUT </MudButton>
 | 
			
		||||
	</Authorized>
 | 
			
		||||
	<NotAuthorized>
 | 
			
		||||
		<MudButton Class="" Href="Register"> Register </MudButton>
 | 
			
		||||
		<MudButton Class="" Href="Login"> Login </MudButton>
 | 
			
		||||
	</NotAuthorized>
 | 
			
		||||
</AuthorizeView>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@code {
 | 
			
		||||
 | 
			
		||||
	private async Task Logout()
 | 
			
		||||
	{
 | 
			
		||||
		await AuthenticationClientService.LogoutAsync();
 | 
			
		||||
		NavigationManager.NavigateTo("/");
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								TechHelper.Client/Pages/Authentication.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								TechHelper.Client/Pages/Authentication.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
@page "/authentication/{action}"
 | 
			
		||||
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
 | 
			
		||||
<RemoteAuthenticatorView Action="@Action" />
 | 
			
		||||
 | 
			
		||||
@code{
 | 
			
		||||
    [Parameter] public string? Action { get; set; }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								TechHelper.Client/Pages/Author/EmailConfirmation.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								TechHelper.Client/Pages/Author/EmailConfirmation.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
@page "/emailconfirmation"
 | 
			
		||||
 | 
			
		||||
<h3>EmailConfirmation</h3>
 | 
			
		||||
<MudButton OnClick="ConfirmToEmail"> 点击确认 </MudButton>
 | 
			
		||||
 | 
			
		||||
<MudPaper>
 | 
			
		||||
	@if (_showSuccess)
 | 
			
		||||
	{
 | 
			
		||||
		<h3> Successful </h3>
 | 
			
		||||
	}
 | 
			
		||||
</MudPaper>
 | 
			
		||||
							
								
								
									
										42
									
								
								TechHelper.Client/Pages/Author/EmailConfirmation.razor.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								TechHelper.Client/Pages/Author/EmailConfirmation.razor.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
using Microsoft.AspNetCore.Components;
 | 
			
		||||
using Microsoft.AspNetCore.WebUtilities;
 | 
			
		||||
using System.Net;
 | 
			
		||||
using TechHelper.Client.HttpRepository;
 | 
			
		||||
 | 
			
		||||
namespace TechHelper.Client.Pages.Author
 | 
			
		||||
{
 | 
			
		||||
	public partial class EmailConfirmation
 | 
			
		||||
	{
 | 
			
		||||
		private bool _showSuccess;
 | 
			
		||||
		private bool _showError;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		[Inject]
 | 
			
		||||
		public IAuthenticationClientService AuthenticationClientService { get; set; }
 | 
			
		||||
		[Inject]
 | 
			
		||||
		public NavigationManager NavigationManager { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		protected async void ConfirmToEmail()
 | 
			
		||||
		{
 | 
			
		||||
			_showError = _showSuccess = false;
 | 
			
		||||
 | 
			
		||||
			var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
 | 
			
		||||
 | 
			
		||||
			var queryStrings = QueryHelpers.ParseQuery(uri.Query);
 | 
			
		||||
			if (queryStrings.TryGetValue("email", out var email) &&
 | 
			
		||||
				queryStrings.TryGetValue("token", out var token))
 | 
			
		||||
			{
 | 
			
		||||
				var result = await AuthenticationClientService.EmailConfirmationAsync(email, token);
 | 
			
		||||
				if (result == HttpStatusCode.OK)
 | 
			
		||||
					_showSuccess = true;
 | 
			
		||||
				else
 | 
			
		||||
					_showError = true;
 | 
			
		||||
			}
 | 
			
		||||
			else
 | 
			
		||||
				NavigationManager.NavigateTo("/");
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										38
									
								
								TechHelper.Client/Pages/Author/ForgotPassword.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								TechHelper.Client/Pages/Author/ForgotPassword.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
@page "/forgotpassword"
 | 
			
		||||
 | 
			
		||||
<EditForm Model="@_forgotPassDto" OnValidSubmit="Submit" FormName="ForgotForm">
 | 
			
		||||
	<DataAnnotationsValidator />
 | 
			
		||||
	<MudGrid>
 | 
			
		||||
		<MudItem xs="12" sm="7">
 | 
			
		||||
			<MudCard>
 | 
			
		||||
				<MudCardContent>
 | 
			
		||||
					<MudTextField Label="Email" Class="mt-3"
 | 
			
		||||
								  @bind-Value="_forgotPassDto.Email" For="@(() => _forgotPassDto.Email)" />
 | 
			
		||||
				</MudCardContent>
 | 
			
		||||
				<MudCardActions>
 | 
			
		||||
					<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto">Register</MudButton>
 | 
			
		||||
				</MudCardActions>
 | 
			
		||||
			</MudCard>
 | 
			
		||||
		</MudItem>
 | 
			
		||||
		<MudItem xs="12" sm="5">
 | 
			
		||||
			<MudPaper Class="pa-4 mud-height-full">
 | 
			
		||||
				<MudText Typo="Typo.subtitle2">Validation Summary</MudText>
 | 
			
		||||
				@if (_showSuccess)
 | 
			
		||||
				{
 | 
			
		||||
					<MudText Color="Color.Success">Success</MudText>
 | 
			
		||||
				}
 | 
			
		||||
				else if(_showError)
 | 
			
		||||
				{
 | 
			
		||||
					<MudText Color="@Color.Error">
 | 
			
		||||
						<ValidationSummary />
 | 
			
		||||
					</MudText>
 | 
			
		||||
				}
 | 
			
		||||
			</MudPaper>
 | 
			
		||||
		</MudItem>
 | 
			
		||||
		<MudItem xs="12">
 | 
			
		||||
			<MudText Typo="Typo.body2" Align="Align.Center">
 | 
			
		||||
				Fill out the form correctly to see the success message.
 | 
			
		||||
			</MudText>
 | 
			
		||||
		</MudItem>
 | 
			
		||||
	</MudGrid>
 | 
			
		||||
</EditForm>
 | 
			
		||||
							
								
								
									
										30
									
								
								TechHelper.Client/Pages/Author/ForgotPassword.razor.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								TechHelper.Client/Pages/Author/ForgotPassword.razor.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
using Entities.DTO;
 | 
			
		||||
using Microsoft.AspNetCore.Components;
 | 
			
		||||
using TechHelper.Client.HttpRepository;
 | 
			
		||||
 | 
			
		||||
namespace TechHelper.Client.Pages.Author
 | 
			
		||||
{
 | 
			
		||||
	public partial class ForgotPassword
 | 
			
		||||
	{
 | 
			
		||||
		private ForgotPasswordDto _forgotPassDto = new ForgotPasswordDto();
 | 
			
		||||
 | 
			
		||||
		private bool _showSuccess;
 | 
			
		||||
		private bool _showError;
 | 
			
		||||
 | 
			
		||||
		[Inject]
 | 
			
		||||
		public IAuthenticationClientService AuthenticationClientService { get; set; }
 | 
			
		||||
 | 
			
		||||
		private async Task Submit()
 | 
			
		||||
		{
 | 
			
		||||
			_showError = _showSuccess = false;
 | 
			
		||||
 | 
			
		||||
			var result = await AuthenticationClientService.ForgotPasswordAsync(_forgotPassDto);
 | 
			
		||||
			if (result == System.Net.HttpStatusCode.OK)
 | 
			
		||||
			{
 | 
			
		||||
				_showSuccess = true;
 | 
			
		||||
			}
 | 
			
		||||
			_showError = true;
 | 
			
		||||
			
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										48
									
								
								TechHelper.Client/Pages/Author/Login.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								TechHelper.Client/Pages/Author/Login.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
			
		||||
@page "/login"
 | 
			
		||||
 | 
			
		||||
<MudText Typo="Typo.h2"> Login Account </MudText>
 | 
			
		||||
 | 
			
		||||
<EditForm Model="@_userForAuth" OnValidSubmit="Logining" FormName="LoginingForm">
 | 
			
		||||
	<DataAnnotationsValidator />
 | 
			
		||||
	<MudGrid>
 | 
			
		||||
		<MudItem xs="12" sm="7">
 | 
			
		||||
			<MudCard>
 | 
			
		||||
				<MudCardContent>
 | 
			
		||||
					<MudTextField Label="Email" Class="mt-3"
 | 
			
		||||
								  @bind-Value="_userForAuth.Email" For="@(() => _userForAuth.Email)" />
 | 
			
		||||
					<MudTextField Label="Password" HelperText="Choose a strong password" Class="mt-3"
 | 
			
		||||
								  @bind-Value="_userForAuth.Password" For="@(() => _userForAuth.Password)" InputType="InputType.Password" />
 | 
			
		||||
				</MudCardContent>
 | 
			
		||||
				<MudCardActions>
 | 
			
		||||
					<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto">Register</MudButton>
 | 
			
		||||
				</MudCardActions>
 | 
			
		||||
 | 
			
		||||
				<div class="nav-item px-3">
 | 
			
		||||
					<NavLink class="nav-link"  href="forgotpassword"> 
 | 
			
		||||
						<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span>Forgot Password
 | 
			
		||||
					</NavLink>
 | 
			
		||||
				</div>
 | 
			
		||||
			</MudCard>
 | 
			
		||||
		</MudItem>
 | 
			
		||||
		<MudItem xs="12" sm="5">
 | 
			
		||||
			<MudPaper Class="pa-4 mud-height-full">
 | 
			
		||||
				<MudText Typo="Typo.subtitle2">Validation Summary</MudText>
 | 
			
		||||
				@if (!ShowRegistrationErrors)
 | 
			
		||||
				{
 | 
			
		||||
					<MudText Color="Color.Success">Success</MudText>
 | 
			
		||||
				}
 | 
			
		||||
				else
 | 
			
		||||
				{
 | 
			
		||||
					<MudText Color="@Color.Error">
 | 
			
		||||
						<ValidationSummary />
 | 
			
		||||
					</MudText>
 | 
			
		||||
				}
 | 
			
		||||
			</MudPaper>
 | 
			
		||||
		</MudItem>
 | 
			
		||||
		<MudItem xs="12">
 | 
			
		||||
			<MudText Typo="Typo.body2" Align="Align.Center">
 | 
			
		||||
				Fill out the form correctly to see the success message.
 | 
			
		||||
			</MudText>
 | 
			
		||||
		</MudItem>
 | 
			
		||||
	</MudGrid>
 | 
			
		||||
</EditForm>
 | 
			
		||||
							
								
								
									
										51
									
								
								TechHelper.Client/Pages/Author/Login.razor.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								TechHelper.Client/Pages/Author/Login.razor.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
using TechHelper.Client.HttpRepository;
 | 
			
		||||
using Entities.DTO;
 | 
			
		||||
using Microsoft.AspNetCore.Components;
 | 
			
		||||
using Microsoft.AspNetCore.WebUtilities;
 | 
			
		||||
 | 
			
		||||
namespace TechHelper.Client.Pages.Author
 | 
			
		||||
{
 | 
			
		||||
	public partial class Login
 | 
			
		||||
	{
 | 
			
		||||
 | 
			
		||||
		private UserForAuthenticationDto _userForAuth = new UserForAuthenticationDto();
 | 
			
		||||
 | 
			
		||||
		[Inject]
 | 
			
		||||
		public IAuthenticationClientService AuthenticationService { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Inject]
 | 
			
		||||
		public NavigationManager NavigationManager { get; set; }
 | 
			
		||||
 | 
			
		||||
		public bool ShowRegistrationErrors { get; set; }
 | 
			
		||||
		public string Error { get; set; }
 | 
			
		||||
 | 
			
		||||
		public async Task Logining()
 | 
			
		||||
		{
 | 
			
		||||
			ShowRegistrationErrors = false;
 | 
			
		||||
			var result = await AuthenticationService.LoginAsync(_userForAuth);
 | 
			
		||||
 | 
			
		||||
			if (result.Is2StepVerificationRequired)
 | 
			
		||||
			{
 | 
			
		||||
				var queryParams = new Dictionary<string, object?>
 | 
			
		||||
				{
 | 
			
		||||
					["provider"] = result.Provider,
 | 
			
		||||
					["Email"] = _userForAuth.Email
 | 
			
		||||
				};
 | 
			
		||||
 | 
			
		||||
				var uri = NavigationManager.GetUriWithQueryParameters("/twostepverification", queryParams);
 | 
			
		||||
 | 
			
		||||
				NavigationManager.NavigateTo(uri);
 | 
			
		||||
			}
 | 
			
		||||
			else if (!result.IsAuthSuccessful)
 | 
			
		||||
			{
 | 
			
		||||
				Error = result.ErrorMessage;
 | 
			
		||||
				ShowRegistrationErrors = true;
 | 
			
		||||
			}
 | 
			
		||||
			else
 | 
			
		||||
			{
 | 
			
		||||
				NavigationManager.NavigateTo("/");
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										100
									
								
								TechHelper.Client/Pages/Author/Registration.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								TechHelper.Client/Pages/Author/Registration.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,100 @@
 | 
			
		||||
@page "/register"
 | 
			
		||||
@using System.ComponentModel.DataAnnotations
 | 
			
		||||
@using Entities.Contracts
 | 
			
		||||
 | 
			
		||||
@inject ISnackbar Snackbar
 | 
			
		||||
 | 
			
		||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" @onclick="@(() => Snackbar.Add("Simple Snackbar"))">
 | 
			
		||||
	Open Snackbar
 | 
			
		||||
</MudButton>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<MudText Typo="Typo.h2"> Create Account </MudText>
 | 
			
		||||
 | 
			
		||||
<EditForm Model="@_userForRegistration" OnValidSubmit="Register" FormName="RegistrationForm">
 | 
			
		||||
	<DataAnnotationsValidator />
 | 
			
		||||
	<MudGrid>
 | 
			
		||||
		<MudItem xs="12" sm="7">
 | 
			
		||||
			<MudCard>
 | 
			
		||||
				<MudCardContent>
 | 
			
		||||
					<MudTextField Label="Name" HelperText="Max. 8 characters"
 | 
			
		||||
								  @bind-Value="_userForRegistration.Name" For="@(() => _userForRegistration.Email)" />
 | 
			
		||||
					<MudTextField Label="Email" Class="mt-3"
 | 
			
		||||
								  @bind-Value="_userForRegistration.Email" For="@(() => _userForRegistration.Email)" />
 | 
			
		||||
					<MudTextField Label="Password" HelperText="Choose a strong password" Class="mt-3"
 | 
			
		||||
								  @bind-Value="_userForRegistration.Password" For="@(() => _userForRegistration.Password)" InputType="InputType.Password" />
 | 
			
		||||
					<MudTextField Label="Password" HelperText="Repeat the password" Class="mt-3"
 | 
			
		||||
								  @bind-Value="_userForRegistration.ConfirmPassword" For="@(() => _userForRegistration.ConfirmPassword)" InputType="InputType.Password" />
 | 
			
		||||
					<MudRadioGroup T="UserRoles" Label="Roles" @bind-Value="_userForRegistration.Roles">
 | 
			
		||||
						@foreach (UserRoles item in Enum.GetValues(typeof(UserRoles)))
 | 
			
		||||
						{
 | 
			
		||||
							if (item != UserRoles.Administrator)
 | 
			
		||||
							{
 | 
			
		||||
								<MudRadio Value="@item">@item.ToString()</MudRadio>
 | 
			
		||||
							}
 | 
			
		||||
						}
 | 
			
		||||
					</MudRadioGroup>
 | 
			
		||||
					<MudStack Row="true">
 | 
			
		||||
 | 
			
		||||
						<MudTextField Label="Class"
 | 
			
		||||
									  HelperText="Enter a class number between 1 and 14."
 | 
			
		||||
									  Class="mt-3"
 | 
			
		||||
									  @bind-Value="_userForRegistration.Class"
 | 
			
		||||
									  For="@(() => _userForRegistration.Class)"
 | 
			
		||||
									  InputType="InputType.Number"
 | 
			
		||||
									  Required="true"
 | 
			
		||||
									  RequiredError="Class is required." />
 | 
			
		||||
 | 
			
		||||
						<MudTextField Label="Grade"
 | 
			
		||||
									  HelperText="Enter a grade number between 1 and 6."
 | 
			
		||||
									  Class="mt-3"
 | 
			
		||||
									  @bind-Value="_userForRegistration.Grade"
 | 
			
		||||
									  For="@(() => _userForRegistration.Grade)"
 | 
			
		||||
									  InputType="InputType.Number"
 | 
			
		||||
									  Required="true"
 | 
			
		||||
									  RequiredError="Grade is required." />
 | 
			
		||||
					</MudStack>
 | 
			
		||||
 | 
			
		||||
					<MudTextField Label="Phone Number"
 | 
			
		||||
								  HelperText="Enter your phone number (optional, 7-20 digits)."
 | 
			
		||||
								  Class="mt-3"
 | 
			
		||||
								  @bind-Value="_userForRegistration.PhoneNumber"
 | 
			
		||||
								  For="@(() => _userForRegistration.PhoneNumber)"
 | 
			
		||||
								  InputType="InputType.Telephone" /> <MudTextField Label="Home Address"
 | 
			
		||||
																				   HelperText="Enter your home address (optional, max 200 characters)."
 | 
			
		||||
																				   Class="mt-3"
 | 
			
		||||
																				   @bind-Value="_userForRegistration.HomeAddress"
 | 
			
		||||
																				   For="@(() => _userForRegistration.HomeAddress)"
 | 
			
		||||
																				   Lines="3" />
 | 
			
		||||
				</MudCardContent>
 | 
			
		||||
				<MudCardActions>
 | 
			
		||||
					<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto">Register</MudButton>
 | 
			
		||||
				</MudCardActions>
 | 
			
		||||
			</MudCard>
 | 
			
		||||
 | 
			
		||||
		</MudItem>
 | 
			
		||||
		<MudItem xs="12" sm="5">
 | 
			
		||||
			<MudPaper Class="pa-4 mud-height-full">
 | 
			
		||||
				<MudText Typo="Typo.subtitle2">Validation Summary</MudText>
 | 
			
		||||
				@if (success)
 | 
			
		||||
				{
 | 
			
		||||
					<MudText Color="Color.Success">Success</MudText>
 | 
			
		||||
				}
 | 
			
		||||
				else
 | 
			
		||||
				{
 | 
			
		||||
					<MudText Color="@Color.Error">
 | 
			
		||||
						<ValidationSummary />
 | 
			
		||||
					</MudText>
 | 
			
		||||
				}
 | 
			
		||||
			</MudPaper>
 | 
			
		||||
		</MudItem>
 | 
			
		||||
		<MudItem xs="12">
 | 
			
		||||
			<MudText Typo="Typo.body2" Align="Align.Center">
 | 
			
		||||
				Fill out the form correctly to see the success message.
 | 
			
		||||
			</MudText>
 | 
			
		||||
		</MudItem>
 | 
			
		||||
	</MudGrid>
 | 
			
		||||
 | 
			
		||||
</EditForm>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										62
									
								
								TechHelper.Client/Pages/Author/Registration.razor.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								TechHelper.Client/Pages/Author/Registration.razor.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
			
		||||
using TechHelper.Client.HttpRepository;
 | 
			
		||||
using Entities.DTO;
 | 
			
		||||
using Microsoft.AspNetCore.Components;
 | 
			
		||||
using MudBlazor;
 | 
			
		||||
using System.Text.RegularExpressions;
 | 
			
		||||
using TechHelper.Features;
 | 
			
		||||
using Entities.Contracts;
 | 
			
		||||
 | 
			
		||||
namespace TechHelper.Client.Pages.Author
 | 
			
		||||
{
 | 
			
		||||
	public partial class Registration
 | 
			
		||||
	{
 | 
			
		||||
 | 
			
		||||
		private UserForRegistrationDto _userForRegistration = new UserForRegistrationDto();
 | 
			
		||||
 | 
			
		||||
		[Inject]
 | 
			
		||||
		public IAuthenticationClientService AuthenticationService { get; set; }
 | 
			
		||||
 | 
			
		||||
		[Inject]
 | 
			
		||||
		public NavigationManager NavigationManager { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		public bool ShowRegistrationErrors { get; set; }
 | 
			
		||||
		public string[] Errors { get; set; }
 | 
			
		||||
		private bool success;
 | 
			
		||||
 | 
			
		||||
		public async Task Register()
 | 
			
		||||
		{
 | 
			
		||||
			ShowRegistrationErrors = false;
 | 
			
		||||
 | 
			
		||||
			var result = await AuthenticationService.RegisterUserAsync(_userForRegistration);
 | 
			
		||||
			if (!result.IsSuccessfulRegistration)
 | 
			
		||||
			{
 | 
			
		||||
				int index = 0;
 | 
			
		||||
				foreach (var error in result.Errors)
 | 
			
		||||
				{
 | 
			
		||||
					Errors[index] = error;
 | 
			
		||||
					index++;
 | 
			
		||||
				}
 | 
			
		||||
				ShowRegistrationErrors = true;
 | 
			
		||||
				Snackbar.Add(Errors.ToString());
 | 
			
		||||
			}
 | 
			
		||||
			else
 | 
			
		||||
			{
 | 
			
		||||
				NavigationManager.NavigateTo("/");
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		[Inject]
 | 
			
		||||
		public IEmailSender emailSender { get; set; }
 | 
			
		||||
 | 
			
		||||
		public async void SendEmail()
 | 
			
		||||
		{
 | 
			
		||||
			string eamilTo = "1928360026@qq.com";
 | 
			
		||||
			string authCode = "123456";
 | 
			
		||||
 | 
			
		||||
			await emailSender.SendEmailAuthcodeAsync(eamilTo, authCode);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										49
									
								
								TechHelper.Client/Pages/Author/ResetPassword.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								TechHelper.Client/Pages/Author/ResetPassword.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
			
		||||
@page "/resetpassword"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<MudText Typo="Typo.h2"> Reset Password Account </MudText>
 | 
			
		||||
 | 
			
		||||
<EditForm Model="@_resetPassDto" OnValidSubmit="Submit" FormName="ResetPasswordForm">
 | 
			
		||||
	<DataAnnotationsValidator />
 | 
			
		||||
	<MudGrid>
 | 
			
		||||
		<MudItem xs="12" sm="7">
 | 
			
		||||
			<MudCard>
 | 
			
		||||
				<MudCardContent>
 | 
			
		||||
					<MudTextField Label="Password" HelperText="Choose a strong password" Class="mt-3"
 | 
			
		||||
								  @bind-Value="_resetPassDto.Password" For="@(() => _resetPassDto.Password)" InputType="InputType.Password" />
 | 
			
		||||
					<MudTextField Label="Confirm Password" HelperText="Repet Password" Class="mt-3"
 | 
			
		||||
								  @bind-Value="_resetPassDto.ConfirmPassword" For="@(() => _resetPassDto.ConfirmPassword)" InputType="InputType.Password" />
 | 
			
		||||
				</MudCardContent>
 | 
			
		||||
				<MudCardActions>
 | 
			
		||||
					<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto">Register</MudButton>
 | 
			
		||||
				</MudCardActions>
 | 
			
		||||
 | 
			
		||||
				<div class="nav-item px-3">
 | 
			
		||||
					<NavLink class="nav-link" href="forgotpassword">
 | 
			
		||||
						<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span>Forgot Password
 | 
			
		||||
					</NavLink>
 | 
			
		||||
				</div>
 | 
			
		||||
			</MudCard>
 | 
			
		||||
		</MudItem>
 | 
			
		||||
		<MudItem xs="12" sm="5">
 | 
			
		||||
			<MudPaper Class="pa-4 mud-height-full">
 | 
			
		||||
				<MudText Typo="Typo.subtitle2">Validation Summary</MudText>
 | 
			
		||||
				@if (_showSuccess)
 | 
			
		||||
				{
 | 
			
		||||
					<MudText Color="Color.Success">Success</MudText>
 | 
			
		||||
				}
 | 
			
		||||
				else if (_showError)
 | 
			
		||||
				{
 | 
			
		||||
					<MudText Color="@Color.Error">
 | 
			
		||||
						<ValidationSummary />
 | 
			
		||||
					</MudText>
 | 
			
		||||
				}
 | 
			
		||||
			</MudPaper>
 | 
			
		||||
		</MudItem>
 | 
			
		||||
		<MudItem xs="12">
 | 
			
		||||
			<MudText Typo="Typo.body2" Align="Align.Center">
 | 
			
		||||
				Fill out the form correctly to see the success message.
 | 
			
		||||
			</MudText>
 | 
			
		||||
		</MudItem>
 | 
			
		||||
	</MudGrid>
 | 
			
		||||
</EditForm>
 | 
			
		||||
							
								
								
									
										55
									
								
								TechHelper.Client/Pages/Author/ResetPassword.razor.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								TechHelper.Client/Pages/Author/ResetPassword.razor.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
			
		||||
using Entities.DTO;
 | 
			
		||||
using Microsoft.AspNetCore.Components;
 | 
			
		||||
using Microsoft.AspNetCore.WebUtilities;
 | 
			
		||||
using TechHelper.Client.HttpRepository;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
namespace TechHelper.Client.Pages.Author
 | 
			
		||||
{
 | 
			
		||||
	public partial class ResetPassword : ComponentBase
 | 
			
		||||
	{
 | 
			
		||||
		private readonly ResetPasswordDto _resetPassDto = new ResetPasswordDto();
 | 
			
		||||
 | 
			
		||||
		private bool _showSuccess;
 | 
			
		||||
		private bool _showError;
 | 
			
		||||
		private IEnumerable<string> _errors;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		[Inject]
 | 
			
		||||
		public IAuthenticationClientService AuthenticationClientService { get; set; }
 | 
			
		||||
		[Inject]
 | 
			
		||||
		public NavigationManager NavigationManager { get; set; }
 | 
			
		||||
 | 
			
		||||
		protected override void OnInitialized()
 | 
			
		||||
		{
 | 
			
		||||
			var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri);
 | 
			
		||||
			var queryStrings = QueryHelpers.ParseQuery(uri.Query);
 | 
			
		||||
			if (queryStrings.TryGetValue("email", out var email) &&
 | 
			
		||||
				queryStrings.TryGetValue("token", out var token))
 | 
			
		||||
			{
 | 
			
		||||
				_resetPassDto.Email = email;
 | 
			
		||||
				_resetPassDto.Token = token;
 | 
			
		||||
			}
 | 
			
		||||
			else
 | 
			
		||||
			{
 | 
			
		||||
				NavigationManager.NavigateTo("/");
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		private async Task Submit()
 | 
			
		||||
		{
 | 
			
		||||
			_showSuccess = _showError = false;
 | 
			
		||||
			var result = await AuthenticationClientService.ResetPasswordAsync(_resetPassDto);
 | 
			
		||||
 | 
			
		||||
			if(result.IsResetPasswordSuccessful)
 | 
			
		||||
				_showSuccess = true;
 | 
			
		||||
			else
 | 
			
		||||
			{
 | 
			
		||||
				_errors = result.Errors;
 | 
			
		||||
				_showError = true;	
 | 
			
		||||
			}
 | 
			
		||||
			NavigationManager.NavigateTo("/");
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								TechHelper.Client/Pages/Author/RoleDetailInfo.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								TechHelper.Client/Pages/Author/RoleDetailInfo.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
@page "/rodetail"
 | 
			
		||||
<h3>RoleDetailInfo</h3>
 | 
			
		||||
 | 
			
		||||
<AuthorizeView>
 | 
			
		||||
 | 
			
		||||
<MudText> @context.User.Identity.Name </MudText>
 | 
			
		||||
 | 
			
		||||
</AuthorizeView>
 | 
			
		||||
@code {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										47
									
								
								TechHelper.Client/Pages/Author/Signout.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								TechHelper.Client/Pages/Author/Signout.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
@page "/logout"
 | 
			
		||||
@inject IAuthenticationClientService AuthenticationClientService
 | 
			
		||||
@inject NavigationManager NavigationManager
 | 
			
		||||
 | 
			
		||||
<MudText>HELLO WORLD </MudText>
 | 
			
		||||
 | 
			
		||||
@code {
 | 
			
		||||
	protected override async Task OnAfterRenderAsync(bool firstRender)
 | 
			
		||||
	{
 | 
			
		||||
		// 作用:这个方法在组件渲染到 UI 后被调用。
 | 
			
		||||
		// 意义:这是执行 JS 互操作的安全时机,特别是在启用了预渲染的情况下,
 | 
			
		||||
		//      此时客户端的 JS 运行时已经可用(在 Blazor Server 中通过 SignalR,在 Blazor WASM 中是 WASM 环境本身)。
 | 
			
		||||
		// firstRender 参数:
 | 
			
		||||
		// 作用:指示本次调用是否是组件首次在客户端渲染完成。
 | 
			
		||||
		// 意义:注销逻辑只需要执行一次。使用 firstRender = true 可以避免在组件后续状态变化触发重新渲染时重复执行。
 | 
			
		||||
 | 
			
		||||
		if (firstRender)
 | 
			
		||||
		{
 | 
			
		||||
			Console.WriteLine("Signout: OnAfterRenderAsync - First Render. Executing Logout..."); // 可选日志
 | 
			
		||||
 | 
			
		||||
			try
 | 
			
		||||
			{
 | 
			
		||||
				// === 在这里安全地调用依赖 JS Interop 的注销方法 ===
 | 
			
		||||
				// AuthenticationClientService.Logout() 方法内部会调用 _localStorageService.RemoveItemAsync(),
 | 
			
		||||
				// 现在是调用它的安全时机。
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
				// 注销完成后,执行导航重定向
 | 
			
		||||
				NavigationManager.NavigateTo("/");
 | 
			
		||||
			}
 | 
			
		||||
			catch (Exception ex)
 | 
			
		||||
			{
 | 
			
		||||
				// === 处理 Logout 中可能发生的异常 ===
 | 
			
		||||
				// 例如,如果 Local Storage 操作失败,或者 Logout 方法内部有其他错误
 | 
			
		||||
				Console.WriteLine($"Error during logout in Signout component: {ex.Message}");
 | 
			
		||||
				// 你可能需要在这里显示错误信息给用户,或者决定是否依然重定向
 | 
			
		||||
				// 即使注销失败,通常也希望将用户导航到某个页面(如首页或错误页)
 | 
			
		||||
				// 例如:
 | 
			
		||||
				NavigationManager.NavigateTo("/"); // 即使失败也重定向到首页
 | 
			
		||||
															// 或者 NavigationManager.NavigateTo("/error"); // 重定向到错误页
 | 
			
		||||
			}
 | 
			
		||||
			Console.WriteLine("Signout: OnAfterRenderAsync - First Render. Logout execution finished."); // 可选日志
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										39
									
								
								TechHelper.Client/Pages/Author/TwoStepVerification.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								TechHelper.Client/Pages/Author/TwoStepVerification.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
@page "/twostepverification"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<EditForm Model="@_twoFactorVerificationDto" OnValidSubmit="Submit" FormName="ForgotForm">
 | 
			
		||||
	<DataAnnotationsValidator />
 | 
			
		||||
	<MudGrid>
 | 
			
		||||
		<MudItem xs="12" sm="7">
 | 
			
		||||
			<MudCard>
 | 
			
		||||
				<MudCardContent>
 | 
			
		||||
					<MudTextField Label="Email" Class="mt-3"
 | 
			
		||||
								  @bind-Value="_twoFactorVerificationDto.TwoFactorToken" For="@(() => _twoFactorVerificationDto.TwoFactorToken)" />
 | 
			
		||||
				</MudCardContent>
 | 
			
		||||
				<MudCardActions>
 | 
			
		||||
					<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="ml-auto">Register</MudButton>
 | 
			
		||||
				</MudCardActions>
 | 
			
		||||
			</MudCard>
 | 
			
		||||
		</MudItem>
 | 
			
		||||
		<MudItem xs="12" sm="5">
 | 
			
		||||
			<MudPaper Class="pa-4 mud-height-full">
 | 
			
		||||
				<MudText Typo="Typo.subtitle2">Validation Summary</MudText>
 | 
			
		||||
				@if (_showSuccess)
 | 
			
		||||
				{
 | 
			
		||||
					<MudText Color="Color.Success">Success</MudText>
 | 
			
		||||
				}
 | 
			
		||||
				else if (_showError)
 | 
			
		||||
				{
 | 
			
		||||
					<MudText Color="@Color.Error">
 | 
			
		||||
						<ValidationSummary />
 | 
			
		||||
					</MudText>
 | 
			
		||||
				}
 | 
			
		||||
			</MudPaper>
 | 
			
		||||
		</MudItem>
 | 
			
		||||
		<MudItem xs="12">
 | 
			
		||||
			<MudText Typo="Typo.body2" Align="Align.Center">
 | 
			
		||||
				Fill out the form correctly to see the success message.
 | 
			
		||||
			</MudText>
 | 
			
		||||
		</MudItem>
 | 
			
		||||
	</MudGrid>
 | 
			
		||||
</EditForm>
 | 
			
		||||
							
								
								
									
										60
									
								
								TechHelper.Client/Pages/Author/TwoStepVerification.razor.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								TechHelper.Client/Pages/Author/TwoStepVerification.razor.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
			
		||||
using Entities.DTO;
 | 
			
		||||
using Microsoft.AspNetCore.Components;
 | 
			
		||||
using Microsoft.AspNetCore.WebUtilities;
 | 
			
		||||
using TechHelper.Client.HttpRepository;
 | 
			
		||||
 | 
			
		||||
namespace TechHelper.Client.Pages.Author
 | 
			
		||||
{
 | 
			
		||||
	public partial class TwoStepVerification : ComponentBase
 | 
			
		||||
	{
 | 
			
		||||
		private TwoFactorVerificationDto _twoFactorVerificationDto = new TwoFactorVerificationDto();
 | 
			
		||||
 | 
			
		||||
		private bool _showSuccess;
 | 
			
		||||
		private string? _error;
 | 
			
		||||
		private bool _showError;
 | 
			
		||||
		[Inject]
 | 
			
		||||
		public IAuthenticationClientService AuthenticationClientService { get; set; }
 | 
			
		||||
		[Inject]
 | 
			
		||||
		public NavigationManager NavigationManager { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		[SupplyParameterFromQuery]
 | 
			
		||||
		[Parameter]
 | 
			
		||||
		public string? Email { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		[SupplyParameterFromQuery]
 | 
			
		||||
		[Parameter]
 | 
			
		||||
		public string? Provider { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		protected override void OnInitialized()
 | 
			
		||||
		{
 | 
			
		||||
			if(string.IsNullOrEmpty(Email) || string.IsNullOrEmpty(Provider))
 | 
			
		||||
			{
 | 
			
		||||
 | 
			
		||||
				NavigationManager.NavigateTo("/");
 | 
			
		||||
			}
 | 
			
		||||
			else
 | 
			
		||||
			{
 | 
			
		||||
				_twoFactorVerificationDto.Email = Email;
 | 
			
		||||
				_twoFactorVerificationDto.Provider = Provider;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		private async Task Submit()
 | 
			
		||||
		{
 | 
			
		||||
			_showError = false;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
			var result = await AuthenticationClientService.LoginVerfication(_twoFactorVerificationDto);
 | 
			
		||||
			if(result.IsAuthSuccessful) 
 | 
			
		||||
				NavigationManager.NavigateTo("/");
 | 
			
		||||
			else
 | 
			
		||||
			{
 | 
			
		||||
				_error = result.ErrorMessage;
 | 
			
		||||
				_showError = true;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										81
									
								
								TechHelper.Client/Pages/Components/AssignmentView.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								TechHelper.Client/Pages/Components/AssignmentView.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
			
		||||
@rendermode InteractiveAuto
 | 
			
		||||
@inject NavigationManager NavigationManager
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<MudPaper Class="pa-10 ma-5">
 | 
			
		||||
	<MudText Typo="Typo.h1"> @user.Username</MudText>
 | 
			
		||||
	<MudText Typo="Typo.h6"> Role :@user.Role</MudText>
 | 
			
		||||
	<MudText Typo="Typo.h6"> Email : @user.Email</MudText>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	<MudDataGrid T="Submission" Items="@Submissions" SortMode="@_sortMode">
 | 
			
		||||
		<Columns>
 | 
			
		||||
			<PropertyColumn Property="x => x.Assignment.Title" />
 | 
			
		||||
			<PropertyColumn Property="x => x.Assignment.Description" />
 | 
			
		||||
			<PropertyColumn Property="x => x.AttemptNumber" />
 | 
			
		||||
			<PropertyColumn Property="x => x.GradedAt" />
 | 
			
		||||
			<TemplateColumn CellClass="d-flex justify-end">
 | 
			
		||||
				<CellTemplate>
 | 
			
		||||
					<MudStack Row>
 | 
			
		||||
						<MudButton Size="@Size.Small" Variant="@Variant.Filled" Color="@Color.Primary" OnClick="@(() => DetailsButtonClicked(context.Item))">详情</MudButton>
 | 
			
		||||
					</MudStack>
 | 
			
		||||
				</CellTemplate>
 | 
			
		||||
			</TemplateColumn>
 | 
			
		||||
		</Columns>
 | 
			
		||||
		<PagerContent>
 | 
			
		||||
			<MudDataGridPager T="Submission" />
 | 
			
		||||
		</PagerContent>
 | 
			
		||||
	</MudDataGrid>
 | 
			
		||||
	@* 
 | 
			
		||||
	<MudDataGrid T="SubmissionDetail" Items="@questions" SortMode="@_sortMode">
 | 
			
		||||
		<Columns>
 | 
			
		||||
			<PropertyColumn Property="x => x.AssignmentQuestion.QuestionNumber" />
 | 
			
		||||
			<PropertyColumn Property="x => x.AssignmentQuestion.Question.QuestionText" />
 | 
			
		||||
			<PropertyColumn Property="x => x.AssignmentQuestion.Question.DifficultyLevel" />
 | 
			
		||||
			<PropertyColumn Property="x => x.AssignmentQuestion.Question.CorrectAnswer" />
 | 
			
		||||
			<PropertyColumn Property="x => x.IsCorrect" />
 | 
			
		||||
		</Columns>
 | 
			
		||||
		<PagerContent>
 | 
			
		||||
			<MudDataGridPager T="SubmissionDetail" />
 | 
			
		||||
		</PagerContent>
 | 
			
		||||
	</MudDataGrid> 
 | 
			
		||||
	*@
 | 
			
		||||
</MudPaper>
 | 
			
		||||
 | 
			
		||||
@code {
 | 
			
		||||
 | 
			
		||||
	[Inject]
 | 
			
		||||
	ISubmissionService submissionService { get; set; }
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public int UserId { get; set; } = 0;
 | 
			
		||||
	private SortMode _sortMode = SortMode.Multiple;
 | 
			
		||||
 | 
			
		||||
	private User user = new User();
 | 
			
		||||
	private User student = new User();
 | 
			
		||||
	private IEnumerable<Submission> Submissions = new List<Submission>();
 | 
			
		||||
 | 
			
		||||
	protected override async Task OnInitializedAsync()
 | 
			
		||||
	{
 | 
			
		||||
		var result = await submissionService.GetByUserId(14);
 | 
			
		||||
		if (result.Successed)
 | 
			
		||||
		{
 | 
			
		||||
			Submissions = result.Data.Items;
 | 
			
		||||
		}
 | 
			
		||||
		else
 | 
			
		||||
		{
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void DetailsButtonClicked(Submission submission)
 | 
			
		||||
	{
 | 
			
		||||
 | 
			
		||||
		if (submission != null)
 | 
			
		||||
		{
 | 
			
		||||
			NavigationManager.NavigateTo($"/submissiondetails/{submission.Id}");
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										45
									
								
								TechHelper.Client/Pages/Components/ErrorDis.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								TechHelper.Client/Pages/Components/ErrorDis.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
@rendermode InteractiveServer
 | 
			
		||||
 | 
			
		||||
<MudPaper Class="pa-5 ma-5 rounded-lg" Width="@Width">
 | 
			
		||||
	<MudChart ChartType=@ChartType @bind-SelectedIndex="Index" InputData="@data" InputLabels="@labels" Width=@Width Height=@Height> </MudChart>
 | 
			
		||||
</MudPaper>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@code {
 | 
			
		||||
 | 
			
		||||
	private int Index = -1;
 | 
			
		||||
	private ChartOptions options = new ChartOptions();
 | 
			
		||||
	public double[] data = { 50, 25, 20, 5 };
 | 
			
		||||
	public string[] labels = { "Fossil", "Nuclear", "Solar", "Wind" };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	[Category("Behavior")]
 | 
			
		||||
	public ChartType ChartType { get; set; } = ChartType.Donut;
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	[Category("Appearance")]
 | 
			
		||||
	public string Width { get; set; } = "80%";
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	[Category("Appearance")]
 | 
			
		||||
	public ChartOptions ChartOptions { get; set; } = new ChartOptions();
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	[Category("Appearance")]
 | 
			
		||||
	public string Height { get; set; } = "80%";
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public string XAxis { get; set; }
 | 
			
		||||
 | 
			
		||||
	public string[] XAxisLabels = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep" };
 | 
			
		||||
 | 
			
		||||
	protected override async Task OnInitializedAsync()
 | 
			
		||||
	{
 | 
			
		||||
		options.InterpolationOption = InterpolationOption.NaturalSpline;
 | 
			
		||||
		options.YAxisFormat = "c2";
 | 
			
		||||
 | 
			
		||||
		ChartOptions = options;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										33
									
								
								TechHelper.Client/Pages/Components/Exam/Blank.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								TechHelper.Client/Pages/Components/Exam/Blank.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
<MudPaper Elevation="0" Class="ma-1 pa-1">
 | 
			
		||||
	@if (IsSelected)
 | 
			
		||||
	{
 | 
			
		||||
 | 
			
		||||
		<MudStack>
 | 
			
		||||
 | 
			
		||||
			<MudText Typo="Typo.caption" Color="Color.Primary">(选中状态,在此编辑具体题目内容)</MudText>
 | 
			
		||||
 | 
			
		||||
			<MudTextField Label="Question" @bind-Value="QuestionItem.Question.QuestionText" AutoGrow="true"></MudTextField>
 | 
			
		||||
		</MudStack>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		<MudTextField Label="Answer" @bind-Value="QuestionItem.Question.CorrectAnswer"></MudTextField>
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
	else
 | 
			
		||||
	{
 | 
			
		||||
		<MudTextField @bind-Value=QuestionItem.Question.QuestionText AutoGrow="true">  </MudTextField>
 | 
			
		||||
	}
 | 
			
		||||
</MudPaper>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@code {
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public QuestionItem QuestionItem { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public bool IsSelected { get; set; }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								TechHelper.Client/Pages/Components/Exam/Blank.razor.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								TechHelper.Client/Pages/Components/Exam/Blank.razor.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
 | 
			
		||||
.hover-highlight-class
 | 
			
		||||
{
 | 
			
		||||
	background-color: black; /* 使用 MudBlazor 的悬停背景颜色变量 */
 | 
			
		||||
	cursor: pointer; /* 改变鼠标指针,提示可点击 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 可选:如果您希望选中状态有特殊的背景色,可以添加这个 */
 | 
			
		||||
.selected-highlight-class {
 | 
			
		||||
	/* 例如:浅蓝色背景 */
 | 
			
		||||
	/* background-color: var(--mud-palette-primary-lighten); */
 | 
			
		||||
	/* 或者仅是边框颜色变化 (已经在 Wrapper 中实现了) */
 | 
			
		||||
	/* border-color: var(--mud-palette-primary) !important; */
 | 
			
		||||
	/* border-width: 1px !important; */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
	/* 可选:当同时处于选中和悬停状态时的样式 */
 | 
			
		||||
	.selected-highlight-class.hover-highlight-class {
 | 
			
		||||
		/* 例如:比单独悬停颜色更深一点的背景 */
 | 
			
		||||
		background-color: var(--mud-palette-primary-darken);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
/* 确保 MudCard 的边框在选中时可见 */
 | 
			
		||||
.mud-card.selected-highlight-class {
 | 
			
		||||
	border-style: solid; /* 确保边框样式为实线 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										198
									
								
								TechHelper.Client/Pages/Components/Exam/CommonGroup.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								TechHelper.Client/Pages/Components/Exam/CommonGroup.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,198 @@
 | 
			
		||||
@rendermode InteractiveServer
 | 
			
		||||
@if (GroupSelected)
 | 
			
		||||
{
 | 
			
		||||
	<MudPaper @onclick="HandleClick" Class="pa-1 ma-1" Outlined="false" Elevation="0">
 | 
			
		||||
 | 
			
		||||
		<MudStack Row="true">
 | 
			
		||||
 | 
			
		||||
			<MudButton Color="Color.Surface" Variant="Variant.Outlined" OnClick="OnAddText"> TEXT </MudButton>
 | 
			
		||||
			<MudButton Color="Color.Surface" Variant="Variant.Outlined" OnClick="OnAddRadio"> Radio </MudButton>
 | 
			
		||||
			
 | 
			
		||||
@* 			<MudSelect @bind-Value="GropType" Variant="Variant.Outlined">
 | 
			
		||||
				<MudSelectItem Value="@(GropType.Stack)" />
 | 
			
		||||
				<MudSelectItem Value="@(GropType.Grid)" />
 | 
			
		||||
			</MudSelect> *@
 | 
			
		||||
		</MudStack>
 | 
			
		||||
 | 
			
		||||
		<MudDivider/>
 | 
			
		||||
		@switch (@GropType)
 | 
			
		||||
		{
 | 
			
		||||
			case GropType.Stack:
 | 
			
		||||
				<MudStack Spacing="6">
 | 
			
		||||
 | 
			
		||||
					@foreach (var question in QuestionGroupElement.GroupsQuestions)
 | 
			
		||||
					{
 | 
			
		||||
						<QuestionBase QuestionElement="question"
 | 
			
		||||
									  IsSelected="question.IsSelected"
 | 
			
		||||
									  MoveDown="HandleMoveDown"
 | 
			
		||||
									  MoveUp="HandleMoveUp"
 | 
			
		||||
									  OnDeleted="HandleQuestionDeleted"
 | 
			
		||||
									  OnSelected="HandleSelected" />
 | 
			
		||||
					}
 | 
			
		||||
				</MudStack>
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			case GropType.Grid:
 | 
			
		||||
				<MudGrid Spacing="6" xs="3">
 | 
			
		||||
 | 
			
		||||
					@foreach (var question in QuestionGroupElement.GroupsQuestions)
 | 
			
		||||
					{
 | 
			
		||||
						<MudItem>
 | 
			
		||||
							<QuestionBase QuestionElement="question"
 | 
			
		||||
										  IsSelected="question.IsSelected"
 | 
			
		||||
										  MoveDown="HandleMoveDown"
 | 
			
		||||
										  MoveUp="HandleMoveUp"
 | 
			
		||||
										  OnDeleted="HandleQuestionDeleted"
 | 
			
		||||
										  OnSelected="HandleSelected" />
 | 
			
		||||
						</MudItem>
 | 
			
		||||
					}
 | 
			
		||||
				</MudGrid>
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			default:
 | 
			
		||||
				<MudText Typo="Typo.h6">布局类型: 未知</MudText>
 | 
			
		||||
				break;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	</MudPaper>
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
else
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
	@switch (@GropType)
 | 
			
		||||
	{
 | 
			
		||||
		case GropType.Stack:
 | 
			
		||||
			<MudStack Spacing="6">
 | 
			
		||||
 | 
			
		||||
				@foreach (var question in QuestionGroupElement.GroupsQuestions)
 | 
			
		||||
				{
 | 
			
		||||
					<QuestionBase QuestionElement="question"
 | 
			
		||||
								  IsSelected="question.IsSelected"
 | 
			
		||||
								  MoveDown="HandleMoveDown"
 | 
			
		||||
								  MoveUp="HandleMoveUp"
 | 
			
		||||
								  OnDeleted="HandleQuestionDeleted"
 | 
			
		||||
								  OnSelected="HandleSelected" />
 | 
			
		||||
				}
 | 
			
		||||
			</MudStack>
 | 
			
		||||
			break;
 | 
			
		||||
 | 
			
		||||
		case GropType.Grid:
 | 
			
		||||
			<MudGrid Spacing="6" xs="3">
 | 
			
		||||
 | 
			
		||||
				@foreach (var question in QuestionGroupElement.GroupsQuestions)
 | 
			
		||||
				{
 | 
			
		||||
					<MudItem>
 | 
			
		||||
						<QuestionBase QuestionElement="question"
 | 
			
		||||
									  IsSelected="question.IsSelected"
 | 
			
		||||
									  MoveDown="HandleMoveDown"
 | 
			
		||||
									  MoveUp="HandleMoveUp"
 | 
			
		||||
									  OnDeleted="HandleQuestionDeleted"
 | 
			
		||||
									  OnSelected="HandleSelected" />
 | 
			
		||||
					</MudItem>
 | 
			
		||||
				}
 | 
			
		||||
			</MudGrid>
 | 
			
		||||
			break;
 | 
			
		||||
 | 
			
		||||
		default:
 | 
			
		||||
			<MudText Typo="Typo.h6">布局类型: 未知</MudText>
 | 
			
		||||
			break;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@code {
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public GropType GropType { get; set; } = GropType.Stack;
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public bool GroupSelected { get; set; }
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public QuestionGroupElement QuestionGroupElement { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public bool IsSelected { get; set; }
 | 
			
		||||
 | 
			
		||||
	private int preSelected = 0;
 | 
			
		||||
 | 
			
		||||
	protected override void OnInitialized()
 | 
			
		||||
	{
 | 
			
		||||
		QuestionGroupElement = new QuestionGroupElement();
 | 
			
		||||
		QuestionGroupElement.GroupsQuestions = new List<QuestionElement>();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void HandleMoveUp(int index)
 | 
			
		||||
	{
 | 
			
		||||
		if (index >= QuestionGroupElement.GroupsQuestions.Count) return;
 | 
			
		||||
 | 
			
		||||
		QuestionGroupElement.GroupsQuestions.MoveUp(QuestionGroupElement.GroupsQuestions[index]);
 | 
			
		||||
		ReOrderIndex();
 | 
			
		||||
		StateHasChanged();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void HandleMoveDown(int index)
 | 
			
		||||
	{
 | 
			
		||||
		if (index >= QuestionGroupElement.GroupsQuestions.Count) return;
 | 
			
		||||
 | 
			
		||||
		QuestionGroupElement.GroupsQuestions.MoveDown(QuestionGroupElement.GroupsQuestions[index]);
 | 
			
		||||
		ReOrderIndex();
 | 
			
		||||
		StateHasChanged();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void HandleQuestionDeleted(int questionId)
 | 
			
		||||
	{
 | 
			
		||||
		var questionToRemove = QuestionGroupElement.GroupsQuestions.FirstOrDefault(q => q.Index == questionId);
 | 
			
		||||
		if (questionToRemove != null)
 | 
			
		||||
		{
 | 
			
		||||
			QuestionGroupElement.GroupsQuestions.Remove(questionToRemove);
 | 
			
		||||
 | 
			
		||||
			StateHasChanged();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		ReOrderIndex();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void ReOrderIndex()
 | 
			
		||||
	{
 | 
			
		||||
		foreach (var que in QuestionGroupElement.GroupsQuestions)
 | 
			
		||||
		{
 | 
			
		||||
			que.Index = QuestionGroupElement.GroupsQuestions.IndexOf(que);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public void OnAddText()
 | 
			
		||||
	{
 | 
			
		||||
		QuestionGroupElement.GroupsQuestions.Add(new QuestionElement { Index = QuestionGroupElement.GroupsQuestions.Count });
 | 
			
		||||
		StateHasChanged();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	public void OnAddRadio()
 | 
			
		||||
	{
 | 
			
		||||
		QuestionGroupElement.GroupsQuestions.Add(new QuestionElement { Index = QuestionGroupElement.GroupsQuestions.Count, QuestionType = BaseQuestionType.Radio });
 | 
			
		||||
		StateHasChanged();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void HandleClick(MouseEventArgs e)
 | 
			
		||||
	{
 | 
			
		||||
		HandleSelected(-1);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void HandleSelected(int id)
 | 
			
		||||
	{
 | 
			
		||||
		var ques = QuestionGroupElement.GroupsQuestions.FirstOrDefault(x => x.Index == preSelected);
 | 
			
		||||
		if (ques != null) ques.IsSelected = false;
 | 
			
		||||
 | 
			
		||||
		if (id < 0) return;
 | 
			
		||||
 | 
			
		||||
		var ques2 = QuestionGroupElement.GroupsQuestions.FirstOrDefault(x => x.Index == id);
 | 
			
		||||
		if (ques2 != null) ques2.IsSelected = true;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		preSelected = id;
 | 
			
		||||
		StateHasChanged();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										98
									
								
								TechHelper.Client/Pages/Components/Exam/QuestionBase.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								TechHelper.Client/Pages/Components/Exam/QuestionBase.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,98 @@
 | 
			
		||||
<MudPaper @onclick:stopPropagation>
 | 
			
		||||
	<MudPaper Outlined="false" Elevation="0"
 | 
			
		||||
			  @onclick="HandleClick"
 | 
			
		||||
			  @onmouseover="HandleMouseOver"
 | 
			
		||||
			  @onmouseout="HandleMouseOut">
 | 
			
		||||
 | 
			
		||||
		@if (IsSelected)
 | 
			
		||||
		{
 | 
			
		||||
 | 
			
		||||
			<MudIconButton Icon="@Icons.Material.Filled.MoveUp" Color="Color.Success" Size="Size.Small" OnClick="HandleMoveUp" />
 | 
			
		||||
			<MudIconButton Icon="@Icons.Material.Filled.MoveDown" Color="Color.Success" Size="Size.Small" OnClick="HandleMoveDown" />
 | 
			
		||||
			<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small" OnClick="HandleDelete" />
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		@switch (QuestionElement.QuestionType)
 | 
			
		||||
		{
 | 
			
		||||
			case BaseQuestionType.Text:
 | 
			
		||||
				<Blank IsSelected="IsSelected" QuestionItem="QuestionElement.QuestionItem" />
 | 
			
		||||
				break;
 | 
			
		||||
			case BaseQuestionType.Radio:
 | 
			
		||||
				<RadioChoice IsSelected="IsSelected" QuestionItem="QuestionElement.QuestionItem" />
 | 
			
		||||
				break;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	</MudPaper>
 | 
			
		||||
</MudPaper>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@code {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public QuestionElement QuestionElement { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public bool IsSelected { get; set; }
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public EventCallback<int> OnSelected { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public EventCallback<int> OnDeleted { get; set; }
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public EventCallback<int> MoveUp { get; set; }
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public EventCallback<int> MoveDown { get; set; }
 | 
			
		||||
 | 
			
		||||
	private bool _isHovered = false;
 | 
			
		||||
 | 
			
		||||
	private async Task HandleDelete()
 | 
			
		||||
	{
 | 
			
		||||
		await OnDeleted.InvokeAsync(QuestionElement.Index);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private async Task HandleMoveUp()
 | 
			
		||||
	{
 | 
			
		||||
		await MoveUp.InvokeAsync(QuestionElement.Index);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private async Task HandleMoveDown()
 | 
			
		||||
	{
 | 
			
		||||
		await MoveDown.InvokeAsync(QuestionElement.Index);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	private async Task HandleClick(MouseEventArgs args)
 | 
			
		||||
	{
 | 
			
		||||
		if (QuestionElement.QuestionItem != null && OnSelected.HasDelegate)
 | 
			
		||||
		{
 | 
			
		||||
 | 
			
		||||
			await OnSelected.InvokeAsync(QuestionElement.Index);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	private void HandleMouseOver()
 | 
			
		||||
	{
 | 
			
		||||
		_isHovered = true;
 | 
			
		||||
		StateHasChanged();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void HandleMouseOut()
 | 
			
		||||
	{
 | 
			
		||||
		_isHovered = false;
 | 
			
		||||
		StateHasChanged();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void OnCompeli()
 | 
			
		||||
	{
 | 
			
		||||
		IsSelected = false;
 | 
			
		||||
		StateHasChanged();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										124
									
								
								TechHelper.Client/Pages/Components/Exam/QuestionTemplate.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								TechHelper.Client/Pages/Components/Exam/QuestionTemplate.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,124 @@
 | 
			
		||||
@rendermode InteractiveServer
 | 
			
		||||
 | 
			
		||||
<MudPaper @onclick="HandleClick" Outlined="true" Elevation="@(IsSelected ? 8 : 2)" Class="ma-5 pa-2">
 | 
			
		||||
 | 
			
		||||
	@if (IsSelected)
 | 
			
		||||
	{
 | 
			
		||||
		<MudPaper Elevation="0" Class="my-2">
 | 
			
		||||
 | 
			
		||||
			<MudIconButton Icon="@Icons.Material.Filled.MoveUp" Color="Color.Success" Size="Size.Small" OnClick="HandleMoveUp" />
 | 
			
		||||
			<MudIconButton Icon="@Icons.Material.Filled.MoveDown" Color="Color.Success" Size="Size.Small" OnClick="HandleMoveDown" />
 | 
			
		||||
			<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small" OnClick="HandleDelete" />
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
			<MudDivider />
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
			<MudStack Row="true">
 | 
			
		||||
 | 
			
		||||
				<MudText> @QuestionGroupElement.Number </MudText>
 | 
			
		||||
				<MudTextField @bind-Value=QuestionGroupElement.Title></MudTextField>
 | 
			
		||||
			</MudStack>
 | 
			
		||||
			<MudDivider />
 | 
			
		||||
			<MudTextField Label="Descript" @bind-Value=QuestionGroupElement.Descript AutoGrow></MudTextField>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		</MudPaper>
 | 
			
		||||
	}
 | 
			
		||||
	else
 | 
			
		||||
	{
 | 
			
		||||
 | 
			
		||||
		<MudStack>
 | 
			
		||||
 | 
			
		||||
			<MudStack Row="true">
 | 
			
		||||
				<MudText> @QuestionGroupElement.Number </MudText>
 | 
			
		||||
 | 
			
		||||
				<MudText Typo="Typo.h6">@QuestionGroupElement.Title</MudText>
 | 
			
		||||
			</MudStack>
 | 
			
		||||
			<MudDivider />
 | 
			
		||||
			@if (!string.IsNullOrEmpty(QuestionGroupElement.Descript))
 | 
			
		||||
			{
 | 
			
		||||
 | 
			
		||||
				<MudTextField ReadOnly="true" @bind-Value=QuestionGroupElement.Descript AutoGrow></MudTextField>
 | 
			
		||||
			}
 | 
			
		||||
		</MudStack>
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@switch (QuestionGroupElement.QuestionType)
 | 
			
		||||
	{
 | 
			
		||||
		case QuestionType.Spelling:
 | 
			
		||||
			<CommonGroup QuestionGroupElement="QuestionGroupElement" IsSelected="QuestionGroupElement.IsSelected" GroupSelected="IsSelected" />
 | 
			
		||||
			break;
 | 
			
		||||
		default:
 | 
			
		||||
			<MudText Color="Color.Warning">未知或未实现的编辑器类型: @QuestionGroupElement.QuestionType</MudText>
 | 
			
		||||
			@if (IsSelected)
 | 
			
		||||
			{
 | 
			
		||||
				<MudText Typo="Typo.body2">选中此题,但无对应编辑器可编辑内容。</MudText>
 | 
			
		||||
			}
 | 
			
		||||
			break;
 | 
			
		||||
	}
 | 
			
		||||
	<MudDivider />
 | 
			
		||||
</MudPaper>
 | 
			
		||||
 | 
			
		||||
@code {
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public QuestionGroupElement QuestionGroupElement { get; set; }
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public bool IsSelected { get; set; }
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public EventCallback<int> OnSelected { get; set; }
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public EventCallback<int> OnDeleted { get; set; }
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public EventCallback<int> MoveUp { get; set; }
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public EventCallback<int> MoveDown { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	private async Task HandleClick()
 | 
			
		||||
	{
 | 
			
		||||
		if (!IsSelected)
 | 
			
		||||
		{
 | 
			
		||||
			await OnSelected.InvokeAsync(QuestionGroupElement.Number);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private async Task HandleDelete()
 | 
			
		||||
	{
 | 
			
		||||
		await OnDeleted.InvokeAsync(QuestionGroupElement.Number);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private async Task HandleMoveUp()
 | 
			
		||||
	{
 | 
			
		||||
		await MoveUp.InvokeAsync(QuestionGroupElement.Number);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private async Task HandleMoveDown()
 | 
			
		||||
	{
 | 
			
		||||
		await MoveDown.InvokeAsync(QuestionGroupElement.Number);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	private string GetQuestionTypeName(QuestionType type)
 | 
			
		||||
	{
 | 
			
		||||
		return type switch
 | 
			
		||||
		{
 | 
			
		||||
			QuestionType.Spelling => "拼写题",
 | 
			
		||||
			QuestionType.Pronunciation => "读音选择题",
 | 
			
		||||
			QuestionType.WordFormation => "组词题",
 | 
			
		||||
			QuestionType.FillInTheBlanks => "选词填空/补充词语",
 | 
			
		||||
			QuestionType.SentenceDictation => "默写句子",
 | 
			
		||||
			QuestionType.SentenceRewriting => "仿句/改写",
 | 
			
		||||
			QuestionType.ReadingComprehension => "阅读理解",
 | 
			
		||||
			QuestionType.Composition => "作文题",
 | 
			
		||||
			_ => "未知类型"
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										165
									
								
								TechHelper.Client/Pages/Components/Exam/RadioChoice.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								TechHelper.Client/Pages/Components/Exam/RadioChoice.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,165 @@
 | 
			
		||||
@if (IsSelected)
 | 
			
		||||
{
 | 
			
		||||
	<MudPaper Outlined="true" Class="ma-1 pa-1">
 | 
			
		||||
		<MudGrid>
 | 
			
		||||
			<MudItem xs="12" md="8">
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
				<MudStack>
 | 
			
		||||
					<MudStack Row="true" Spacing="2">
 | 
			
		||||
						<MudText Typo="Typo.caption" Color="Color.Primary">(选中状态,在此编辑具体题目内容)</MudText>
 | 
			
		||||
					</MudStack>
 | 
			
		||||
 | 
			
		||||
					<MudTextField @bind-Value="QuestionItem.Question.QuestionText" AutoGrow="true"></MudTextField>
 | 
			
		||||
					<MudNumericField @bind-Value="Num" Label="选项数量" Variant="Variant.Outlined" Dense="true" Min="0" Max="byte.MaxValue" />
 | 
			
		||||
					<MudTextField @bind-Value="QuestionItem.Question.CorrectAnswer" AutoGrow="true"></MudTextField>
 | 
			
		||||
				</MudStack>
 | 
			
		||||
			</MudItem>
 | 
			
		||||
			<MudItem xs="12" md="4">
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
				<MudStack>
 | 
			
		||||
 | 
			
		||||
					@for (int index = 0; index < QuestionItem.Radio.Count; index++)
 | 
			
		||||
					{
 | 
			
		||||
						var tempIndex = index;
 | 
			
		||||
 | 
			
		||||
						<MudTextField @bind-Value="QuestionItem.Radio[tempIndex]"
 | 
			
		||||
						Label="@($"选项 {tempIndex + 1}")"
 | 
			
		||||
						Color="Color.Primary"
 | 
			
		||||
						Variant="Variant.Outlined" Dense="true">
 | 
			
		||||
						</MudTextField>
 | 
			
		||||
					}
 | 
			
		||||
				</MudStack>
 | 
			
		||||
 | 
			
		||||
				@if (QuestionItem.Radio.Count > 0)
 | 
			
		||||
				{
 | 
			
		||||
					<MudRadioGroup T="string" @bind-Value="QuestionItem.Question.CorrectAnswer">
 | 
			
		||||
						<MudText Typo="Typo.body2">设置正确选项:</MudText>
 | 
			
		||||
						@foreach (var optionText in QuestionItem.Radio)
 | 
			
		||||
						{
 | 
			
		||||
							<MudRadio Value="@optionText" Color="Color.Success">@optionText</MudRadio>
 | 
			
		||||
						}
 | 
			
		||||
					</MudRadioGroup>
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
			</MudItem>
 | 
			
		||||
 | 
			
		||||
		</MudGrid>
 | 
			
		||||
	</MudPaper>
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
<MudStack Spacing="1">
 | 
			
		||||
	<MudText> @QuestionItem.Title </MudText>
 | 
			
		||||
	@if (QuestionItem.Radio.Count > 0)
 | 
			
		||||
	{
 | 
			
		||||
		<MudRadioGroup T="string" ReadOnly="true">
 | 
			
		||||
			@foreach (var optionText in QuestionItem.Radio)
 | 
			
		||||
			{
 | 
			
		||||
				<MudRadio Value="@optionText">@optionText</MudRadio>
 | 
			
		||||
			}
 | 
			
		||||
		</MudRadioGroup>
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	<MudText Typo="Typo.body2">
 | 
			
		||||
		正确答案: @(QuestionItem?.Question.CorrectAnswer ?? "(未设置)")
 | 
			
		||||
	</MudText>
 | 
			
		||||
 | 
			
		||||
</MudStack>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@using Microsoft.AspNetCore.Components.Web
 | 
			
		||||
@code {
 | 
			
		||||
	private bool _oldIsSelected;
 | 
			
		||||
	private const string OptionsDelimiter = "[OPTIONS]";
 | 
			
		||||
	private const string ItemDelimiter = "[SEP]";
 | 
			
		||||
 | 
			
		||||
	public string SelectedOption { get; set; }
 | 
			
		||||
	public int Num
 | 
			
		||||
	{
 | 
			
		||||
		get { return QuestionItem.Radio.Count; }
 | 
			
		||||
		set
 | 
			
		||||
		{
 | 
			
		||||
			if (QuestionItem.Radio == null)
 | 
			
		||||
			{
 | 
			
		||||
				QuestionItem.Radio = new List<string>();
 | 
			
		||||
			}
 | 
			
		||||
			int targetCount = value;
 | 
			
		||||
			while (QuestionItem.Radio.Count < targetCount)
 | 
			
		||||
			{
 | 
			
		||||
				QuestionItem.Radio.Add($"选项 {QuestionItem.Radio.Count + 1}");
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			while (QuestionItem.Radio.Count > targetCount)
 | 
			
		||||
			{
 | 
			
		||||
				QuestionItem.Radio.RemoveAt(QuestionItem.Radio.Count - 1);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			StateHasChanged();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public QuestionItem QuestionItem { get; set; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public bool IsSelected { get; set; }
 | 
			
		||||
 | 
			
		||||
	protected override async Task OnParametersSetAsync()
 | 
			
		||||
	{
 | 
			
		||||
		if (IsSelected && !_oldIsSelected)
 | 
			
		||||
		{
 | 
			
		||||
			ParseCombinedString(QuestionItem.Question.QuestionText);
 | 
			
		||||
		}
 | 
			
		||||
		else if (!IsSelected && _oldIsSelected)
 | 
			
		||||
		{
 | 
			
		||||
			CombineDataIntoString(QuestionItem.Question);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_oldIsSelected = IsSelected;
 | 
			
		||||
 | 
			
		||||
		await base.OnParametersSetAsync();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void ParseCombinedString(string combinedText)
 | 
			
		||||
	{
 | 
			
		||||
		QuestionItem.Radio.Clear();
 | 
			
		||||
 | 
			
		||||
		if (!string.IsNullOrEmpty(combinedText) && combinedText.Contains(OptionsDelimiter))
 | 
			
		||||
		{
 | 
			
		||||
			var parts = combinedText.Split(new[] { OptionsDelimiter }, 2, StringSplitOptions.None);
 | 
			
		||||
 | 
			
		||||
			if (parts.Length == 2)
 | 
			
		||||
			{
 | 
			
		||||
				QuestionItem.Question.QuestionText = parts[0];
 | 
			
		||||
 | 
			
		||||
				var optionStrings = parts[1].Split(new[] { ItemDelimiter }, StringSplitOptions.None);
 | 
			
		||||
 | 
			
		||||
				foreach (var opt in optionStrings)
 | 
			
		||||
				{
 | 
			
		||||
					QuestionItem.Radio.Add(opt);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			else
 | 
			
		||||
			{
 | 
			
		||||
				QuestionItem.Question.QuestionText = combinedText;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		else
 | 
			
		||||
		{
 | 
			
		||||
		}
 | 
			
		||||
		StateHasChanged();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private void CombineDataIntoString(Question questionToUpdate)
 | 
			
		||||
	{
 | 
			
		||||
		QuestionItem.Title = QuestionItem.Question.QuestionText;
 | 
			
		||||
		string combinedText = questionToUpdate.QuestionText + OptionsDelimiter + string.Join(ItemDelimiter, QuestionItem.Radio);
 | 
			
		||||
 | 
			
		||||
		questionToUpdate.QuestionText = combinedText;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										41
									
								
								TechHelper.Client/Pages/Components/GrageView.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								TechHelper.Client/Pages/Components/GrageView.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
			
		||||
<MudPaper Class="pa-5 ma-5 rounded-lg" Width="@Width" >
 | 
			
		||||
	<MudChart ChartType=@ChartType ChartSeries="@Series" XAxisLabels="@XAxisLabels" Width=@Width Height=@Height ChartOptions=@ChartOptions></MudChart>
 | 
			
		||||
</MudPaper>
 | 
			
		||||
@code {
 | 
			
		||||
	private ChartOptions options = new ChartOptions();
 | 
			
		||||
	public List<ChartSeries> Series = new List<ChartSeries>()
 | 
			
		||||
	{
 | 
			
		||||
		new ChartSeries() { Name = "Series 1", Data = new double[] { 90, 79, 72, 69, 62, 62, 55, 65, 70 } },
 | 
			
		||||
		new ChartSeries() { Name = "Series 2", Data = new double[] { 35, 41, 35, 51, 49, 62, 69, 91, 148 } },
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	[Category("Behavior")]
 | 
			
		||||
	public ChartType ChartType { get; set; } = ChartType.Line;
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	[Category("Appearance")]
 | 
			
		||||
	public string Width { get; set; } = "80%";
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	[Category("Appearance")]
 | 
			
		||||
	public ChartOptions ChartOptions { get; set; } = new ChartOptions();
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	[Category("Appearance")]
 | 
			
		||||
	public string Height { get; set; } = "80%";
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public string XAxis { get; set; }
 | 
			
		||||
 | 
			
		||||
	public string[] XAxisLabels = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep" };
 | 
			
		||||
 | 
			
		||||
	protected override async Task OnInitializedAsync()
 | 
			
		||||
	{
 | 
			
		||||
		options.InterpolationOption = InterpolationOption.NaturalSpline;
 | 
			
		||||
		options.YAxisFormat = "c2";
 | 
			
		||||
 | 
			
		||||
		ChartOptions = options;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										5
									
								
								TechHelper.Client/Pages/Components/HeaderLine.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								TechHelper.Client/Pages/Components/HeaderLine.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@code {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										33
									
								
								TechHelper.Client/Pages/Components/UserBaseView.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								TechHelper.Client/Pages/Components/UserBaseView.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
<MudPaper Class="pa-10 ma-5">
 | 
			
		||||
	<MudText Typo="Typo.h1"> @user.Username</MudText>
 | 
			
		||||
	<MudText Typo="Typo.h6"> Role :@user.Role</MudText>
 | 
			
		||||
	<MudText Typo="Typo.h6"> Email : @user.Email</MudText>
 | 
			
		||||
 | 
			
		||||
</MudPaper>
 | 
			
		||||
 | 
			
		||||
@code {
 | 
			
		||||
 | 
			
		||||
	[Inject]
 | 
			
		||||
	IUserService userService { get; set; }
 | 
			
		||||
 | 
			
		||||
	[Parameter]
 | 
			
		||||
	public int UserId { get; set; } = 0;
 | 
			
		||||
 | 
			
		||||
	private User user = new User();
 | 
			
		||||
 | 
			
		||||
	protected override async Task OnInitializedAsync()
 | 
			
		||||
	{
 | 
			
		||||
		var result = await userService.GetByIDAsync((uint)UserId);
 | 
			
		||||
		if (result.Successed)
 | 
			
		||||
		{
 | 
			
		||||
			user = result.Data;
 | 
			
		||||
		}
 | 
			
		||||
		else
 | 
			
		||||
		{
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								TechHelper.Client/Pages/Counter.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								TechHelper.Client/Pages/Counter.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
@page "/counter"
 | 
			
		||||
 | 
			
		||||
<PageTitle>Counter</PageTitle>
 | 
			
		||||
 | 
			
		||||
<h1>Counter</h1>
 | 
			
		||||
 | 
			
		||||
<p role="status">Current count: @currentCount</p>
 | 
			
		||||
 | 
			
		||||
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
 | 
			
		||||
 | 
			
		||||
@code {
 | 
			
		||||
    private int currentCount = 0;
 | 
			
		||||
 | 
			
		||||
    private void IncrementCount()
 | 
			
		||||
    {
 | 
			
		||||
        currentCount++;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										113
									
								
								TechHelper.Client/Pages/Editor/EditorMain.razor
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								TechHelper.Client/Pages/Editor/EditorMain.razor
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,113 @@
 | 
			
		||||
@page "/Edit"
 | 
			
		||||
@using Blazored.TextEditor
 | 
			
		||||
@using System.Text.RegularExpressions
 | 
			
		||||
@using TechHelper.Client.Pages.Exam
 | 
			
		||||
<MudPaper Class="d-flex flex-column flex-grow-1">
 | 
			
		||||
	<MudPaper class="d-flex flex-grow-0 flex-column">
 | 
			
		||||
 | 
			
		||||
		@if (@lode == true)
 | 
			
		||||
		{
 | 
			
		||||
			<MudStack Row="true">
 | 
			
		||||
				<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
 | 
			
		||||
			</MudStack>
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
		<MudButtonGroup Color="Color.Primary" Variant="Variant.Filled">
 | 
			
		||||
			<MudButton OnClick="GetHTML">One</MudButton>
 | 
			
		||||
			<MudButton OnClick="ParseQuestions">ParseQuestions</MudButton>
 | 
			
		||||
			<MudButton OnClick="ParseXML">ParseXML</MudButton>
 | 
			
		||||
			<MudButton OnClick="ParseWithAI">ParseWithAI</MudButton>
 | 
			
		||||
			<MudButton OnClick="ReCorrectXMLAsync">ReCorrectXML</MudButton>
 | 
			
		||||
			<MudButton OnClick="ReCorrectXMLAsync">AplyAIResult</MudButton>
 | 
			
		||||
			<MudButton OnClick="ReCorrectXMLAsync">Save</MudButton>
 | 
			
		||||
			<MudButton OnClick="ReCorrectXMLAsync">Public</MudButton>
 | 
			
		||||
		</MudButtonGroup>
 | 
			
		||||
 | 
			
		||||
	</MudPaper>
 | 
			
		||||
	<MudPaper Class="d-flex flex-row flex-grow-1 overflow-hidden">
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		<MudPaper Width="33%" Class="d-flex flex-column flex-grow-1 overflow-auto">
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
			@if (QuestionS != null && QuestionS.Any())
 | 
			
		||||
			{
 | 
			
		||||
				@foreach (var item in QuestionS)
 | 
			
		||||
				{
 | 
			
		||||
					<QuestionGroupDisplay QuestionGroup="item" IsNested="false" />
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			else
 | 
			
		||||
			{
 | 
			
		||||
				<MudText Typo="Typo.body1">暂无试题内容。</MudText>
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		</MudPaper>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		<MudPaper Width="33%" Class="d-flex flex-column flex-grow-1 justify-content-between overflow-auto">
 | 
			
		||||
			<MudText Typo="Typo.body1">@ProgStatues</MudText>
 | 
			
		||||
 | 
			
		||||
			@for (int i = 0; i < ParseResult.Count; i++)
 | 
			
		||||
			{
 | 
			
		||||
				int index = i;
 | 
			
		||||
				
 | 
			
		||||
				<MudTextField Class="ma-3" AutoGrow="true" @bind-Value="ParseResult[index]"></MudTextField>
 | 
			
		||||
			}
 | 
			
		||||
			<MudText>@Error</MudText>
 | 
			
		||||
		</MudPaper>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		<MudPaper Width="33%" Class="d-flex flex-column flex-grow-1 overflow-auto">
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
			<BlazoredTextEditor @ref="@QuillHtml">
 | 
			
		||||
				<ToolbarContent>
 | 
			
		||||
					<select class="ql-header">
 | 
			
		||||
						<option selected=""></option>
 | 
			
		||||
						<option value="1"></option>
 | 
			
		||||
						<option value="2"></option>
 | 
			
		||||
						<option value="3"></option>
 | 
			
		||||
						<option value="4"></option>
 | 
			
		||||
						<option value="5"></option>
 | 
			
		||||
					</select>
 | 
			
		||||
					<span class="ql-formats">
 | 
			
		||||
						<button class="ql-bold"></button>
 | 
			
		||||
						<button class="ql-italic"></button>
 | 
			
		||||
						<button class="ql-underline"></button>
 | 
			
		||||
						<button class="ql-strike"></button>
 | 
			
		||||
					</span>
 | 
			
		||||
					<span class="ql-formats">
 | 
			
		||||
						<select class="ql-color"></select>
 | 
			
		||||
						<select class="ql-background"></select>
 | 
			
		||||
					</span>
 | 
			
		||||
					<span class="ql-formats">
 | 
			
		||||
						<button class="ql-list" value="ordered"></button>
 | 
			
		||||
						<button class="ql-list" value="bullet"></button>
 | 
			
		||||
					</span>
 | 
			
		||||
					<span class="ql-formats">
 | 
			
		||||
						<button class="ql-link"></button>
 | 
			
		||||
					</span>
 | 
			
		||||
				</ToolbarContent>
 | 
			
		||||
 | 
			
		||||
				<EditorContent>
 | 
			
		||||
				</EditorContent>
 | 
			
		||||
			</BlazoredTextEditor>
 | 
			
		||||
 | 
			
		||||
		</MudPaper>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	</MudPaper>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
</MudPaper>
 | 
			
		||||
							
								
								
									
										214
									
								
								TechHelper.Client/Pages/Editor/EditorMain.razor.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										214
									
								
								TechHelper.Client/Pages/Editor/EditorMain.razor.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,214 @@
 | 
			
		||||
using Blazored.TextEditor;
 | 
			
		||||
using Entities.Contracts;
 | 
			
		||||
using Microsoft.AspNetCore.Components;
 | 
			
		||||
using MudBlazor;
 | 
			
		||||
using System.Text.RegularExpressions;
 | 
			
		||||
using TechHelper.Client.AI;
 | 
			
		||||
using TechHelper.Client.Exam;
 | 
			
		||||
using static Org.BouncyCastle.Crypto.Engines.SM2Engine;
 | 
			
		||||
 | 
			
		||||
namespace TechHelper.Client.Pages.Editor
 | 
			
		||||
{
 | 
			
		||||
	public enum ProgEnum
 | 
			
		||||
	{
 | 
			
		||||
		AIPrase,
 | 
			
		||||
		AIRectify
 | 
			
		||||
	}
 | 
			
		||||
	public partial class EditorMain
 | 
			
		||||
	{
 | 
			
		||||
		private List<QuestionGroup> QuestionS = new List<QuestionGroup>();
 | 
			
		||||
 | 
			
		||||
		private bool lode = false;
 | 
			
		||||
		BlazoredTextEditor QuillHtml;
 | 
			
		||||
		string QuillHTMLContent;
 | 
			
		||||
		string AIParseResult;
 | 
			
		||||
		string Error;
 | 
			
		||||
		string ProgStatues = string.Empty;
 | 
			
		||||
		List<string> ParseResult = new List<string>();
 | 
			
		||||
 | 
			
		||||
		public async Task GetHTML()
 | 
			
		||||
		{
 | 
			
		||||
			QuillHTMLContent = await this.QuillHtml.GetHTML();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		public async Task GetText()
 | 
			
		||||
		{
 | 
			
		||||
			QuillHTMLContent = await this.QuillHtml.GetText();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		private string EditorHtmlContent { get; set; } = string.Empty;
 | 
			
		||||
		private List<ParsedQuestion>? ParsedQuestions { get; set; }
 | 
			
		||||
		private bool _parseAttempted = false;
 | 
			
		||||
 | 
			
		||||
		public class ParsedQuestion
 | 
			
		||||
		{
 | 
			
		||||
			public int Id { get; set; } // <20><>Ŀ<EFBFBD><C4BF><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
 | 
			
		||||
			public string? Content { get; set; } // <20><>Ŀ<EFBFBD><C4BF><EFBFBD><EFBFBD><EFBFBD><EFBFBD> HTML <20><><EFBFBD><EFBFBD>
 | 
			
		||||
			public string? Title { get; set; } // <20><>Ŀ<EFBFBD>ı<EFBFBD><C4B1>ⲿ<EFBFBD>֣<EFBFBD><D6A3><EFBFBD>ѡ<EFBFBD><D1A1><EFBFBD><EFBFBD><EFBFBD>Դ<EFBFBD> Content <20><><EFBFBD><EFBFBD>ȡ<EFBFBD><C8A1>
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		[Inject]
 | 
			
		||||
		public IAIService aIService { get; set; }
 | 
			
		||||
		[Inject]
 | 
			
		||||
		public ISnackbar Snackbar { get; set; }
 | 
			
		||||
		private async void ParseWithAI()
 | 
			
		||||
		{
 | 
			
		||||
			QuestionS.Clear();
 | 
			
		||||
			ParseResult.Clear();
 | 
			
		||||
 | 
			
		||||
			await GetText();
 | 
			
		||||
			lode = true;
 | 
			
		||||
			StateHasChanged();
 | 
			
		||||
 | 
			
		||||
			ProgStatues = ProgEnum.AIPrase.ToString();
 | 
			
		||||
			ProgStatues = $"<22><><EFBFBD>ڽ<EFBFBD><DABD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<2C><><EFBFBD>ȴ<EFBFBD>";
 | 
			
		||||
			Snackbar.Add("<22><><EFBFBD>ڽ<EFBFBD><DABD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,<2C><><EFBFBD>ȴ<EFBFBD>");
 | 
			
		||||
			StateHasChanged();
 | 
			
		||||
			string respon = await aIService.CallGLM(QuillHTMLContent, AIConfiguration.BreakQuestions);
 | 
			
		||||
			if (respon == null)
 | 
			
		||||
			{
 | 
			
		||||
				lode = false;
 | 
			
		||||
				Snackbar.Add("<22><><EFBFBD>˵<EFBFBD><CBB5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD>°<EFBFBD>");
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			var ParRespon = ExamParser.ParseExamXml<StringsList>(respon);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
			if (ParRespon != null)
 | 
			
		||||
			{
 | 
			
		||||
				int i = 1;
 | 
			
		||||
				foreach (var item in ParRespon.Items)
 | 
			
		||||
				{
 | 
			
		||||
					ProgStatues = $"<22><><EFBFBD>ڽ<EFBFBD><DABD><EFBFBD><EFBFBD><EFBFBD>{i}<7D><>, <20><><EFBFBD>ȴ<EFBFBD>";
 | 
			
		||||
					Snackbar.Add($"<22><><EFBFBD>ڽ<EFBFBD><DABD><EFBFBD><EFBFBD><EFBFBD>{i}<7D><>, <20><><EFBFBD>ȴ<EFBFBD>");
 | 
			
		||||
					StateHasChanged();
 | 
			
		||||
					i++;
 | 
			
		||||
					try
 | 
			
		||||
					{
 | 
			
		||||
						var parResult = await aIService.CallGLM(item, AIConfiguration.ParseSignelQuestion);
 | 
			
		||||
						ParseResult.Add(parResult);
 | 
			
		||||
 | 
			
		||||
					}
 | 
			
		||||
					catch (Exception ex)
 | 
			
		||||
					{
 | 
			
		||||
						Snackbar.Add($"<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>{i}<7D><>ʱ<EFBFBD><CAB1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>, <20><><EFBFBD>Ժ<EFBFBD><D4BA><EFBFBD><EFBFBD><EFBFBD>. <20><><EFBFBD><EFBFBD>Ϊ:{ex.Message}");
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			AIParseResult = respon;
 | 
			
		||||
 | 
			
		||||
			ProgStatues = ProgEnum.AIRectify.ToString();
 | 
			
		||||
			//await ReCorrectXMLAsync();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
			ProgStatues = string.Empty;
 | 
			
		||||
			lode = false;
 | 
			
		||||
			StateHasChanged();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		private async Task ReCorrectXMLAsync()
 | 
			
		||||
		{
 | 
			
		||||
			string respon = string.Empty;
 | 
			
		||||
			try
 | 
			
		||||
			{
 | 
			
		||||
				foreach (var item in ParseResult)
 | 
			
		||||
				{
 | 
			
		||||
					//respon = await aIService.CallGLM(AIParseResult, AIConfiguration.ParseSignelQuestion);
 | 
			
		||||
					var xmlResult = ExamParser.ParseExamXml<QuestionGroup>(item);
 | 
			
		||||
					QuestionS.Add(xmlResult);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
			catch (Exception ex)
 | 
			
		||||
			{
 | 
			
		||||
				Snackbar.Add("<22><><EFBFBD>˵<EFBFBD><CBB5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD>°<EFBFBD>" + ex.Message);
 | 
			
		||||
			}
 | 
			
		||||
			if (string.IsNullOrEmpty(respon))
 | 
			
		||||
			{
 | 
			
		||||
				lode = false;
 | 
			
		||||
				Snackbar.Add("<22><><EFBFBD>˵<EFBFBD><CBB5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD>°<EFBFBD>");
 | 
			
		||||
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			AIParseResult = respon;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		private void ParseXML()
 | 
			
		||||
		{
 | 
			
		||||
			try
 | 
			
		||||
			{
 | 
			
		||||
				var paper = ExamParser.ParseExamXml<QuestionGroup>(AIParseResult);
 | 
			
		||||
				//QuestionS = paper.QuestionGroups;
 | 
			
		||||
				Error = string.Empty;
 | 
			
		||||
			}
 | 
			
		||||
			catch (InvalidOperationException ex)
 | 
			
		||||
			{
 | 
			
		||||
				Snackbar.Add("<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>" + ex.Message);
 | 
			
		||||
			}
 | 
			
		||||
			catch (Exception ex)
 | 
			
		||||
			{
 | 
			
		||||
				Snackbar.Add("<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>" + ex.Message);
 | 
			
		||||
				Error = ex.Message;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		private void ParseQuestions()
 | 
			
		||||
		{
 | 
			
		||||
			ParsedQuestions = new List<ParsedQuestion>();
 | 
			
		||||
			_parseAttempted = true;
 | 
			
		||||
 | 
			
		||||
			if (string.IsNullOrWhiteSpace(QuillHTMLContent))
 | 
			
		||||
			{
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ŀ<EFBFBD><C4BF>ʼ<EFBFBD>ͽ<EFBFBD><CDBD><EFBFBD><EFBFBD>ı<EFBFBD><C4B1><EFBFBD>
 | 
			
		||||
			string startTag = "[<5B><>Ŀ<EFBFBD><C4BF>ʼ]";
 | 
			
		||||
			string endTag = "[<5B><>Ŀ<EFBFBD><C4BF><EFBFBD><EFBFBD>]";
 | 
			
		||||
 | 
			
		||||
			// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ<EFBFBD><CABD>ƥ<EFBFBD><C6A5><EFBFBD>ӿ<EFBFBD>ʼ<EFBFBD><CABC><EFBFBD>ǵ<EFBFBD><C7B5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֮<EFBFBD><D6AE><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ݣ<EFBFBD><DDA3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>У<EFBFBD>
 | 
			
		||||
			// (?s) <20><><EFBFBD>õ<EFBFBD><C3B5><EFBFBD>ģʽ<C4A3><CABD><EFBFBD><EFBFBD> . ƥ<>任<EFBFBD>з<EFBFBD>
 | 
			
		||||
			string pattern = Regex.Escape(startTag) + "(.*?)" + Regex.Escape(endTag);
 | 
			
		||||
			var matches = Regex.Matches(QuillHTMLContent, pattern, RegexOptions.Singleline);
 | 
			
		||||
 | 
			
		||||
			int questionId = 1;
 | 
			
		||||
			foreach (Match match in matches)
 | 
			
		||||
			{
 | 
			
		||||
				// <20><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֮<EFBFBD><D6AE><EFBFBD>Ĵ<EFBFBD><C4B4><EFBFBD><EFBFBD><EFBFBD>
 | 
			
		||||
				string rawQuestionHtml = match.Groups[1].Value;
 | 
			
		||||
 | 
			
		||||
				// <20>Ƴ<EFBFBD><C6B3><EFBFBD><EFBFBD>ܲ<EFBFBD><DCB2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>еı<D0B5><C4B1>ǣ<EFBFBD><C7A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʽ<EFBFBD>Ѿ<EFBFBD><D1BE>ų<EFBFBD><C5B3><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǣ<EFBFBD><C7A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
 | 
			
		||||
				string cleanedQuestionContent = rawQuestionHtml
 | 
			
		||||
												.Replace(startTag, "")
 | 
			
		||||
												.Replace(endTag, "")
 | 
			
		||||
												.Trim();
 | 
			
		||||
 | 
			
		||||
				// <20><><EFBFBD>Դ<EFBFBD><D4B4><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD>⣨<EFBFBD><E2A3A8><EFBFBD>磬ƥ<E7A3AC>䡰һ<E4A1B0><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>1<EFBFBD><31><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͷ<EFBFBD><CDB7><EFBFBD>У<EFBFBD>
 | 
			
		||||
				string? questionTitle = null;
 | 
			
		||||
				try
 | 
			
		||||
				{
 | 
			
		||||
 | 
			
		||||
					var firstLineMatch = Regex.Match(cleanedQuestionContent, @"^(<p>)?\s*([һ<><D2BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>߰˾<DFB0>ʮ]+\s*[<5B><><EFBFBD><EFBFBD>]|\d+\s*[<5B><>\.]).*?</p>", RegexOptions.IgnoreCase | RegexOptions.Singleline);
 | 
			
		||||
					if (firstLineMatch.Success)
 | 
			
		||||
					{
 | 
			
		||||
						// <20><> HTML <20><><EFBFBD><EFBFBD>ȡ<EFBFBD><C8A1><EFBFBD>ı<EFBFBD><C4B1><EFBFBD>Ϊ<EFBFBD><CEAA><EFBFBD><EFBFBD>
 | 
			
		||||
						questionTitle = Regex.Replace(firstLineMatch.Value, "<[^>]*>", "").Trim();
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				catch (Exception ex) { }
 | 
			
		||||
 | 
			
		||||
				ParsedQuestions.Add(new ParsedQuestion
 | 
			
		||||
				{
 | 
			
		||||
					Id = questionId++,
 | 
			
		||||
					Content = cleanedQuestionContent,
 | 
			
		||||
					Title = questionTitle
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user