diff --git a/FlaskAPI/app.py b/FlaskAPI/app.py new file mode 100644 index 0000000000000000000000000000000000000000..31acf958d687021a715d557488c96e8f715d52bc --- /dev/null +++ b/FlaskAPI/app.py @@ -0,0 +1,67 @@ +import spacy +import json +from flask import Flask, jsonify, request, Response + +# 加载 SpaCy 中文模型 +nlp = spacy.load("zh_core_web_sm") + +app = Flask(__name__) + +def preprocess_text(text): + # 使用 SpaCy 进行命名实体识别 + doc = nlp(text) + + # 识别实体 + entities = [{"Text": ent.text, "Label": ent.label_} for ent in doc.ents] + + # 按句子拆分文本 + sentences = [sent.text for sent in doc.sents] + + # 返回预处理后的数据 + result = { + "Entities": list(entities), # 确保将实体列表转换为标准列表 + "Sentences": sentences + } + + return result + +@app.route('/spacy', methods=['POST']) +def preprocess(): + data = request.get_json() + text = data.get("text", "") + + # 进行预处理 + processed_result = preprocess_text(text) + print(f"Entities: {processed_result.values()}") + + # 返回预处理后的数据 + res = Response(json.dumps(processed_result, ensure_ascii=False), content_type="application/json; charset=utf-8") + + return res + +@app.route('/parse_input', methods=['POST']) +def parse_text(): + # 从请求中获取用户输入 + content = request.json.get('content', '') + + if not content: + return jsonify({"error": "Content is required"}), 400 + + # 使用 spaCy 进行文本处理 + doc = nlp(content) + + # 提取实体和关键词 + entities = [{"text": ent.text, "label": ent.label_} for ent in doc.ents] + tokens = [token.text for token in doc if not token.is_stop] # 去除停用词 + + # 构造响应 + response = { + "entities": entities, + "tokens": tokens, + "text": content + } + + return jsonify(response) + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/FlaskAPI/dockerfile b/FlaskAPI/dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..e8671ed86fb0539b529d195bf885d0ce23a5b89c --- /dev/null +++ b/FlaskAPI/dockerfile @@ -0,0 +1,12 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt \ + && python -m spacy download zh_core_web_sm + +COPY . . + +EXPOSE 5000 +CMD ["gunicorn", "-b", "0.0.0.0:5000", "app:app"] diff --git a/FlaskAPI/requirement.txt b/FlaskAPI/requirement.txt new file mode 100644 index 0000000000000000000000000000000000000000..9d80114edf69eaceedf82130d8378a9586d6c26a --- /dev/null +++ b/FlaskAPI/requirement.txt @@ -0,0 +1,2 @@ +Flask +spacy \ No newline at end of file diff --git a/backend/UniversalAdminSystem.sln b/backend/UniversalAdminSystem.sln new file mode 100644 index 0000000000000000000000000000000000000000..c65262f760ac4e68d281100d9baae651d78b2719 --- /dev/null +++ b/backend/UniversalAdminSystem.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{9032CBF4-BB53-4E64-9978-E90FEB71CC7C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UniversalAdminSystem.Api", "src\UniversalAdminSystem.Api\UniversalAdminSystem.Api.csproj", "{2AD39004-1226-419A-946B-72964C31CFF2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UniversalAdminSystem.Application", "src\UniversalAdminSystem.Application\UniversalAdminSystem.Application.csproj", "{1B84D865-ECF7-4F61-A45C-81641308405F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UniversalAdminSystem.Domian", "src\UniversalAdminSystem.Domian\UniversalAdminSystem.Domian.csproj", "{6430BA75-BA96-4EED-9F4E-D7DB30C583A3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UniversalAdminSystem.Infrastructure", "src\UniversalAdminSystem.Infrastructure\UniversalAdminSystem.Infrastructure.csproj", "{D497ABDA-6594-4364-9BE7-90AB35E2B194}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2AD39004-1226-419A-946B-72964C31CFF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2AD39004-1226-419A-946B-72964C31CFF2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2AD39004-1226-419A-946B-72964C31CFF2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2AD39004-1226-419A-946B-72964C31CFF2}.Release|Any CPU.Build.0 = Release|Any CPU + {1B84D865-ECF7-4F61-A45C-81641308405F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1B84D865-ECF7-4F61-A45C-81641308405F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1B84D865-ECF7-4F61-A45C-81641308405F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1B84D865-ECF7-4F61-A45C-81641308405F}.Release|Any CPU.Build.0 = Release|Any CPU + {6430BA75-BA96-4EED-9F4E-D7DB30C583A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6430BA75-BA96-4EED-9F4E-D7DB30C583A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6430BA75-BA96-4EED-9F4E-D7DB30C583A3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6430BA75-BA96-4EED-9F4E-D7DB30C583A3}.Release|Any CPU.Build.0 = Release|Any CPU + {D497ABDA-6594-4364-9BE7-90AB35E2B194}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D497ABDA-6594-4364-9BE7-90AB35E2B194}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D497ABDA-6594-4364-9BE7-90AB35E2B194}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D497ABDA-6594-4364-9BE7-90AB35E2B194}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {2AD39004-1226-419A-946B-72964C31CFF2} = {9032CBF4-BB53-4E64-9978-E90FEB71CC7C} + {1B84D865-ECF7-4F61-A45C-81641308405F} = {9032CBF4-BB53-4E64-9978-E90FEB71CC7C} + {6430BA75-BA96-4EED-9F4E-D7DB30C583A3} = {9032CBF4-BB53-4E64-9978-E90FEB71CC7C} + {D497ABDA-6594-4364-9BE7-90AB35E2B194} = {9032CBF4-BB53-4E64-9978-E90FEB71CC7C} + EndGlobalSection +EndGlobal diff --git a/backend/dockerfile b/backend/dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..1c63d6f8f1f6199266935248d965a3be3e8855eb --- /dev/null +++ b/backend/dockerfile @@ -0,0 +1,15 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +WORKDIR /app +EXPOSE 5101 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src +COPY . . +RUN dotnet restore + +RUN dotnet publish -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=build /app/publish . +ENTRYPOINT ["dotnet", "UniversalAdminSystem.dll"] diff --git a/backend/src/UniversalAdminSystem.Api/AllowedFiles.json b/backend/src/UniversalAdminSystem.Api/AllowedFiles.json new file mode 100644 index 0000000000000000000000000000000000000000..34cf87d0ed7d6d44fae59336dc503165ca20389b --- /dev/null +++ b/backend/src/UniversalAdminSystem.Api/AllowedFiles.json @@ -0,0 +1,86 @@ +{ + "AllowedFiles": { + "AllowedFiles": [ + { + "Mime": "image/jpeg", + "Ext": "jpg", + "Signature": "FF D8" + }, + { + "Mime": "image/png", + "Ext": "png", + "Signature": "89 50 4E 47 0D 0A 1A 0A" + }, + { + "Mime": "image/gif", + "Ext": "gif", + "Signature": "47 49 46 38 37 61" + }, + { + "Mime": "image/gif", + "Ext": "gif", + "Signature": "47 49 46 38 39 61" + }, + { + "Mime": "image/bmp", + "Ext": "bmp", + "Signature": "42 4D" + }, + { + "Mime": "image/webp", + "Ext": "webp", + "Signature": "52 49 46 46 ?? ?? ?? ?? 57 45 42 50" + }, + { + "Mime": "application/pdf", + "Ext": "pdf", + "Signature": "25 50 44 46" + }, + { + "Mime": "application/json", + "Ext": "json", + "Signature": "" + }, + { + "Mime": "text/markdown", + "Ext": "md", + "Signature": "" + }, + { + "Mime": "text/plain", + "Ext": "txt", + "Signature": "" + }, + { + "Mime": "application/xml", + "Ext": "xml", + "Signature": "" + }, + { + "Mime": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "Ext": "docx", + "Signature": "50 4B 03 04" + }, + { + "Mime": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "Ext": "xlsx", + "Signature": "50 4B 03 04" + }, + { + "Mime": "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "Ext": "pptx", + "Signature": "50 4B 03 04" + }, + { + "Mime": "application/vnd.ms-excel", + "Ext": "xls", + "Signature": "D0 CF 11 E0 A1 B1 1A E1" + }, + { + "Mime": "application/msword", + "Ext": "doc", + "Signature": "D0 CF 11 E0 A1 B1 1A E1" + } + ] + } +} diff --git a/backend/src/UniversalAdminSystem.Api/Attributes/RequirePermissionAttribute.cs b/backend/src/UniversalAdminSystem.Api/Attributes/RequirePermissionAttribute.cs new file mode 100644 index 0000000000000000000000000000000000000000..5a563303a252b335819e1f574ee349f4f5c395ac --- /dev/null +++ b/backend/src/UniversalAdminSystem.Api/Attributes/RequirePermissionAttribute.cs @@ -0,0 +1,61 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using System.Security.Claims; +using UniversalAdminSystem.Application.PermissionManagement.Interfaces; + +namespace UniversalAdminSystem.Api.Attributes; + +/// +/// 权限验证特性 +/// 用于标记需要特定权限的API接口 +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] //指定该特性可用于 类(Controller) 或 方法(Action) 级别。 +public class RequirePermissionAttribute : Attribute, IAsyncAuthorizationFilter +{ + private readonly string _permissionCode; + + public RequirePermissionAttribute(string permissionCode) + { + _permissionCode = permissionCode; + } + + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) + { + try + { + + // 获取用户ID + var userId = context.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + System.Console.WriteLine($"userId: {userId}" ?? "null"); + if (string.IsNullOrEmpty(userId)) + { + Console.WriteLine($"userId: {context.HttpContext.User.Identity?.Name}"); + context.Result = new UnauthorizedResult(); + return; + } + + // 获取权限检查服务 + var service = context.HttpContext.RequestServices.GetService(); + if (service == null) + { + context.Result = new StatusCodeResult(500); + return; + } + + // 检查用户是否有指定权限 + var hasPermission = await service.CheckUserPermissionAsync(Guid.Parse(userId), _permissionCode); + System.Console.WriteLine($"hasPermission: {hasPermission}"); + if (!hasPermission) + { + context.Result = new StatusCodeResult(401); + return; + } + } + catch (Exception ex) + { + // 记录错误并返回500 + Console.WriteLine($"权限验证错误: {ex.Message}"); + context.Result = new StatusCodeResult(500); + } + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Api/Attributes/RequireRoleAttribute.cs b/backend/src/UniversalAdminSystem.Api/Attributes/RequireRoleAttribute.cs new file mode 100644 index 0000000000000000000000000000000000000000..66a24bd8d31e21b0de4fb7bf4b451f13b7d3996b --- /dev/null +++ b/backend/src/UniversalAdminSystem.Api/Attributes/RequireRoleAttribute.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using System.Security.Claims; + +namespace UniversalAdminSystem.Api.Attributes; + +/// +/// 角色验证特性 +/// 用于标记需要特定角色的API接口 +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public class RequireRoleAttribute : Attribute, IAsyncAuthorizationFilter +{ + private readonly string _roleName; + + public RequireRoleAttribute(string roleName) + { + _roleName = roleName; + } + + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) + { + try + { + // 获取用户角色 + var userRole = context.HttpContext.User.FindFirst(ClaimTypes.Role)?.Value; + if (string.IsNullOrEmpty(userRole)) + { + context.Result = new UnauthorizedResult(); + return; + } + + // 检查用户角色是否匹配 + if (userRole != _roleName) + { + context.Result = new ForbidResult(); + return; + } + } + catch (Exception ex) + { + Console.WriteLine($"角色验证错误: {ex.Message}"); + context.Result = new StatusCodeResult(500); + } + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Api/Controllers/AIQuestionsController.cs b/backend/src/UniversalAdminSystem.Api/Controllers/AIQuestionsController.cs new file mode 100644 index 0000000000000000000000000000000000000000..3ba2d39fb87463451339a819af4bba4e094b483e --- /dev/null +++ b/backend/src/UniversalAdminSystem.Api/Controllers/AIQuestionsController.cs @@ -0,0 +1,99 @@ +using Microsoft.AspNetCore.Mvc; +using UniversalAdminSystem.Api.Attributes; +using UniversalAdminSystem.Application.AIQuestions.DTOs; +using UniversalAdminSystem.Application.AIQuestions.Interfaces; +using UniversalAdminSystem.Application.Common.Results; + +namespace UniversalAdminSystem.Api.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class AIQuestionsController : ControllerBase +{ + private readonly IAIQusetionsAppService _AIQusetionAppService; + + public AIQuestionsController(IAIQusetionsAppService aIQusetionsAppService) + { + _AIQusetionAppService = aIQusetionsAppService; + } + + [HttpPost("visitor")] + public Task VisitorAccess(ContentDto content) + { + throw new NotImplementedException();//尚未实现 + } + + [HttpPost("chat/{conversationsId}")] + [RequirePermission("document:Read")] + public async Task SendMessageAsync(Guid conversationsId, [FromBody] string userMessage) + { + try + { + + var result = await _AIQusetionAppService.Chat(conversationsId, userMessage); + return Ok(Result.Success(result)); + } + catch (System.Exception e) + { + return BadRequest(Result.Failure(e.Message)); + } + } + + [HttpPost("user")] + [RequirePermission("document:Read")] + public async Task CreateUserConversation(ConversationsDto conversationsDto) + { + try + { + var result = await _AIQusetionAppService.CreateConversation(conversationsDto); + return Ok(Result.Success(result)); + } + catch (System.Exception e) + { + return BadRequest(Result.Failure(e.Message)); + } + } + + [HttpGet("user/{userid}")] + [RequirePermission("document:Read")] + public async Task GetUsersConversationsByUserId(Guid userid) + { + try + { + var list = await _AIQusetionAppService.GetUsersConversationsByUserId(userid); + return Ok(Result>.Success(list)); + } + catch (System.Exception e) + { + return BadRequest(Result.Failure(e.Message)); + } + } + + [HttpDelete("user/{conversationsId}")] + public async Task DeleteUserConversations(Guid ConversationsId) + { + try + { + await _AIQusetionAppService.DeleteUserConversations(ConversationsId); + return Ok(Result.Success()); + } + catch (System.Exception e) + { + return BadRequest(Result.Failure(e.Message)); + } + } + + [HttpGet("user/message/{ConversationsId}")] + public async Task GetAllConversationMessage(Guid ConversationsId) + { + try + { + var list = await _AIQusetionAppService.GetConversationMessage(ConversationsId); + return Ok(Result>.Success(list)); + } + catch (System.Exception e) + { + return BadRequest(Result.Failure(e.Message)); + } + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Api/Controllers/AuthenticationController.cs b/backend/src/UniversalAdminSystem.Api/Controllers/AuthenticationController.cs new file mode 100644 index 0000000000000000000000000000000000000000..dd1e8cd2cff4d46ea9b8ac5c9c629a4f6e578636 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Api/Controllers/AuthenticationController.cs @@ -0,0 +1,97 @@ +using Microsoft.AspNetCore.Mvc; +using UniversalAdminSystem.Application.Authentication.DTOs; +using UniversalAdminSystem.Application.Authentication.Interfaces; +using UniversalAdminSystem.Application.Common.Results; +using UniversalAdminSystem.Domian.UserManagement.ValueObj; + +namespace UniversalAdminSystem.Api.Controllers; + +[ApiController] +[Route("api/auth")] +public class AuthenticationController : ControllerBase +{ + private readonly ILoginAppService _loginAppService; + private readonly IRegisterAppService _registerAppService; + private readonly IJwtTokenService _jwtTokenService; + + public AuthenticationController( + ILoginAppService loginAppService, + IRegisterAppService registerAppService, + IJwtTokenService jwtTokenService) + { + _loginAppService = loginAppService; + _registerAppService = registerAppService; + _jwtTokenService = jwtTokenService; + } + + [HttpPost("login")] + public async Task Login([FromBody] LoginDto loginDto) + { + try + { + if (string.IsNullOrEmpty(loginDto.Account) || string.IsNullOrEmpty(loginDto.Password)) + { + return BadRequest(Result.Failure("账号和密码不能为空")); + } + + var result = await _loginAppService.LoginAsync(loginDto); + return Ok(Result.Success(result)); + } + catch (Exception ex) + { + return BadRequest(Result.Failure(ex.Message)); + } + } + + [HttpPost("register")] + public async Task Register([FromBody] RegisterDto registerDto) + { + try + { + if (string.IsNullOrEmpty(registerDto.Account) || + string.IsNullOrEmpty(registerDto.Password) || + string.IsNullOrEmpty(registerDto.Email)) + { + return BadRequest(Result.Failure("账号、密码和邮箱不能为空")); + } + + var result = await _registerAppService.RegisterAsync(registerDto); + return Ok(Result.Success(result)); + } + catch (Exception ex) + { + return BadRequest(Result.Failure(ex.Message)); + } + } + + [HttpPost("refresh-token")] + public IActionResult RefreshToken([FromBody] RefreshTokenDto refreshTokenDto) + { + try + { + if (string.IsNullOrEmpty(refreshTokenDto.Token)) + { + return BadRequest(Result.Failure("Token不能为空")); + } + + var newToken = _jwtTokenService.RefreshToken( + refreshTokenDto.Token, + out string userId, + out string roleId, + out UserStatus status); + + var result = new RefreshTokenResultDto(newToken, userId, roleId); + return Ok(Result.Success(result)); + } + catch (Exception ex) + { + return BadRequest(Result.Failure(ex.Message)); + } + } + + [HttpPost("logout")] + public IActionResult Logout() + { + return Ok(Result.Success("登出成功")); + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Api/Controllers/FileController.cs b/backend/src/UniversalAdminSystem.Api/Controllers/FileController.cs new file mode 100644 index 0000000000000000000000000000000000000000..dc9c96e15c19593e6959765f349d900d1f82bb0b --- /dev/null +++ b/backend/src/UniversalAdminSystem.Api/Controllers/FileController.cs @@ -0,0 +1,125 @@ +using Microsoft.AspNetCore.Mvc; +using UniversalAdminSystem.Application.FileStorage.DTOs; +using UniversalAdminSystem.Application.FileStorage.Interfaces; +using UniversalAdminSystem.Api.Attributes; +using UniversalAdminSystem.Domian.FileStorage.ValueObjects; +using UniversalAdminSystem.Application.Common.Results; +using UniversalAdminSystem.Application.Common.Interfaces; + +namespace UniversalAdminSystem.Api.Controllers; + +[ApiController] +[Route("api/files")] +public class FileController : ControllerBase +{ + private readonly IFileAppService _fileAppService; + private readonly IFileStorageService _fileStorageService; + private readonly IUnitOfWork _unitOfWork; + + public FileController(IFileAppService fileAppService, IFileStorageService fileStorageService, IUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + _fileAppService = fileAppService; + _fileStorageService = fileStorageService; + } + + [HttpPost("upload")] + [RequirePermission("file:Create")] + public async Task Upload(IFormFile file, Guid? parentId = null) + { + if (file == null || file.Length == 0) return BadRequest(Result.Failure("请选择要上传的文件")); + try + { + var res = await _fileAppService.UploadAsync(file); + return Ok(Result.Success(res)); + } + catch (Exception ex) + { + return BadRequest(Result.Failure(ex.Message)); + } + } + + [HttpGet("test-permission")] + [RequirePermission("file:Create")] + public IActionResult TestPermission() + { + return Ok(Result.Success("权限检查通过")); + } + + // [HttpGet("download/{fileId}")] + // [RequirePermission("file:download")] + // public async Task Download(Guid fileId) + // { + // var result = await _fileAppService.DownloadAsync((FileId)fileId); + // return result.IsSuccess ? Ok(result) : BadRequest(result); + // } + + [HttpGet("list")] + [RequirePermission("file:Read")] + public async Task List() + { + var result = await _fileAppService.GetList(); + return Ok(result); + } + + // [HttpPost("folder")] + // [RequirePermission("file:create")] + // public async Task CreateFolder([FromBody] string name, [FromQuery] Guid? parentId) + // { + // var result = await _fileAppService.CreateFolderAsync(name, parentId.HasValue ? (FileId)parentId.Value : null); + // return result.IsSuccess ? Ok(result) : BadRequest(result); + // } + + // [HttpDelete("{fileId}")] + // [RequirePermission("file:delete")] + // public async Task Delete(Guid fileId) + // { + // var result = await _fileAppService.DeleteAsync((FileId)fileId); + // return result.IsSuccess ? Ok(result) : BadRequest(result); + // } + + // [HttpGet("{fileId}")] + // [RequirePermission("file:read")] + // public async Task GetFileById(Guid fileId) + // { + // var result = await _fileAppService.GetFileByIdAsync((FileId)fileId); + // return result.IsSuccess ? Ok(result) : BadRequest(result); + // } + + [HttpGet("{id}")] + [RequirePermission("file:Read")] + public async Task Download(Guid id) + { + try + { + + var entity = await _fileAppService.GetFileById(id); + var file = await _fileStorageService.DownloadAsync(entity.Name); + return new FileStreamResult(file, entity.Type); + } + catch (System.Exception) + { + return BadRequest(Result.Failure("")); + } + } + + [HttpDelete("{id}")] + [RequirePermission("file:Delete")] + public async Task RemoveFile(Guid id) + { + try + { + await _unitOfWork.BeginTransactionAsync(); + var entity = await _fileAppService.GetFileById(id); + await _fileStorageService.DeleteAsync(entity.Name); + await _fileAppService.RemoveFile(entity.Id); + await _unitOfWork.CommitAsync(); + return Ok(Result.Success()); + } + catch (System.Exception) + { + await _unitOfWork.RollbackAsync(); + return BadRequest(Result.Failure("操作错误")); + } + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Api/Controllers/K2ModelController.cs b/backend/src/UniversalAdminSystem.Api/Controllers/K2ModelController.cs new file mode 100644 index 0000000000000000000000000000000000000000..0138549c4767c3026f86b95a78fe3835fdef3ba2 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Api/Controllers/K2ModelController.cs @@ -0,0 +1,93 @@ +// using Microsoft.AspNetCore.Mvc; +// using UniversalAdminSystem.Infrastructure.Services; + +// namespace UniversalAdminSystem.Api.Controllers; + +// [ApiController] +// [Route("api/[controller]")] +// public class K2ModelController : ControllerBase +// { +// private readonly K2ModelService _k2ModelService; +// private readonly ILogger _logger; + +// public K2ModelController(K2ModelService k2ModelService, ILogger logger) +// { +// _k2ModelService = k2ModelService; +// _logger = logger; +// } + +// /// +// /// 发送简单文本请求到K2模型 +// /// +// /// 请求模型 +// /// 模型响应 +// [HttpPost("chat")] +// public async Task SendChatRequest([FromBody] K2ChatRequest request) +// { +// try +// {2 +// _logger.LogInformation("收到K2模型聊天请求: {Prompt}", request.Prompt); + +// var response = await _k2ModelService.SendSimpleRequestAsync( +// request.Prompt, +// request.Model ?? "qwen-turbo"); + +// return Ok(new { response = response }); +// } +// catch (Exception ex) +// { +// _logger.LogError(ex, "K2模型请求失败"); +// return StatusCode(500, new { error = "K2模型请求失败", message = ex.Message }); +// } +// } + +// /// +// /// 发送多轮对话请求到K2模型 +// /// +// /// 多轮对话请求 +// /// 模型响应 +// [HttpPost("conversation")] +// public async Task SendConversationRequest([FromBody] K2ConversationRequest request) +// { +// try +// { +// _logger.LogInformation("收到K2模型多轮对话请求,消息数量: {MessageCount}", request.Messages.Count); + +// var response = await _k2ModelService.SendChatRequestAsync( +// request.Messages, +// request.Model ?? "qwen-turbo", +// request.Temperature ?? 0.7f, +// request.MaxTokens ?? 1000); + +// return Ok(response); +// } +// catch (Exception ex) +// { +// _logger.LogError(ex, "K2模型多轮对话请求失败"); +// return StatusCode(500, new { error = "K2模型请求失败", message = ex.Message }); +// } +// } + +// /// +// /// 获取K2模型配置信息 +// /// +// /// 配置信息 +// [HttpGet("config")] +// public IActionResult GetConfig([FromServices] K2ConfigService configService) +// { +// try +// { +// var config = configService.GetK2Config(); +// return Ok(new +// { +// baseUrl = config.BaseUrl, +// hasApiKey = !string.IsNullOrEmpty(config.ApiKey) +// }); +// } +// catch (Exception ex) +// { +// _logger.LogError(ex, "获取K2配置失败"); +// return StatusCode(500, new { error = "获取配置失败", message = ex.Message }); +// } +// } +// } \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Api/Controllers/LogController.cs b/backend/src/UniversalAdminSystem.Api/Controllers/LogController.cs new file mode 100644 index 0000000000000000000000000000000000000000..62ca15e9272e38f8530255aed9c32f0851a5ea5e --- /dev/null +++ b/backend/src/UniversalAdminSystem.Api/Controllers/LogController.cs @@ -0,0 +1,71 @@ +using Microsoft.AspNetCore.Mvc; +using UniversalAdminSystem.Application.LogManagement.DTOs; +using UniversalAdminSystem.Application.LogManagement.Interfaces; +using UniversalAdminSystem.Api.Attributes; +using UniversalAdminSystem.Application.Common.Results; + +namespace UniversalAdminSystem.Api.Controllers; + +[ApiController] +[Route("api/logs")] +public class LogController : ControllerBase +{ + private readonly ILogManagementAppService _logService; + public LogController(ILogManagementAppService logService) => _logService = logService; + + [HttpGet] + //[RequirePermission("log:read")] + public async Task GetAllLogsAsync() + { + var result = await _logService.GetAllLogsAsync(); + return result.IsSuccess ? Ok(result) : BadRequest(result); + } + + [HttpGet("{id}")] + //[RequirePermission("log:read")] + public async Task GetLogByIdAsync(Guid id) + { + var result = await _logService.GetLogByIdAsync(id); + return result.IsSuccess ? Ok(result) : BadRequest(result); + } + + [HttpPost] + //[RequirePermission("log:create")] + public async Task CreateLogAsync([FromBody] LogCreateDto dto) + { + var result = await _logService.CreateLogAsync(dto); + return result.IsSuccess ? Ok(result) : BadRequest(result); + } + + [HttpGet("level/{level}")] + //[RequirePermission("log:read")] + public async Task GetLogsByLevelAsync(string level) + { + var result = await _logService.GetLogsByLevelAsync(level); + return result.IsSuccess ? Ok(result) : BadRequest(result); + } + + [HttpGet("user/{userId}")] + //[RequirePermission("log:read")] + public async Task GetLogsByUserAsync(Guid userId) + { + var result = await _logService.GetLogsByUserAsync(userId); + return result.IsSuccess ? Ok(result) : BadRequest(result); + } + + [HttpGet("date")] + //[RequirePermission("log:read")] + public async Task GetLogsByDateRangeAsync([FromQuery] DateTime start, [FromQuery] DateTime end) + { + var result = await _logService.GetLogsByDateRangeAsync(start, end); + return result.IsSuccess ? Ok(result) : BadRequest(result); + } + + [HttpGet("source/{source}")] + //[RequirePermission("log:read")] + public async Task GetLogsBySourceAsync(string source) + { + var result = await _logService.GetLogsBySourceAsync(source); + return result.IsSuccess ? Ok(result) : BadRequest(result); + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Api/Controllers/PermissionController.cs b/backend/src/UniversalAdminSystem.Api/Controllers/PermissionController.cs new file mode 100644 index 0000000000000000000000000000000000000000..d4bea70c6a5d664c80c46cbfac17dc6342cea7b6 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Api/Controllers/PermissionController.cs @@ -0,0 +1,163 @@ +using Microsoft.AspNetCore.Mvc; +using UniversalAdminSystem.Application.PermissionManagement.DTOs; +using UniversalAdminSystem.Application.PermissionManagement.Interfaces; +using UniversalAdminSystem.Infrastructure.Services; +using UniversalAdminSystem.Api.Attributes; +using UniversalAdminSystem.Application.Common.Results; + +namespace UniversalAdminSystem.Api.Controllers; + +/// +/// 权限管理控制器 +/// +[ApiController] +[Route("api/permissions")] +public class PermissionController : ControllerBase +{ + private readonly IPermissionManagementAppService _permissionAppService; + private readonly PermissionRuleConfigService _permissionRuleConfigService; + + public PermissionController(IPermissionManagementAppService permissionAppService, PermissionRuleConfigService permissionRuleConfigService) + { + _permissionAppService = permissionAppService; + _permissionRuleConfigService = permissionRuleConfigService; + } + + /// + /// 获取所有权限列表 + /// + [HttpGet] + [RequirePermission("permission:Read")] + public async Task GetAllPermissionsAsync() + { + try + { + var permissions = await _permissionAppService.GetAllPermissionAsync(); + return Ok(Result>.Success(permissions)); + } + catch (Exception ex) + { + return BadRequest(Result>.Failure(ex.Message)); + } + } + + /// + /// 创建新权限 + /// + [HttpPost("create")] + [RequirePermission("permission:Create")] + public async Task CreatePermissionAsync([FromBody] PermissionCreateDto createDto) + { + try + { + var permission = await _permissionAppService.CreatePermissionAsync(createDto); + return Ok(Result.Success(permission)); + } + catch (InvalidOperationException ex) + { + // 检查是否是重复权限错误 + if (ex.Message.Contains("已存在")) + { + return Conflict(Result.Failure(ex.Message)); + } + return BadRequest(Result.Failure(ex.Message)); + } + catch (Exception ex) + { + return BadRequest(Result.Failure($"创建权限失败: {ex.Message}")); + } + } + + /// + /// 删除权限 + /// + [HttpDelete("{permissionId}")] + [RequirePermission("permission:Delete")] + public async Task DeletePermissionAsync(Guid permissionId) + { + try + { + await _permissionAppService.RemovePermissionAsync(permissionId); + return Ok(Result.Success("权限删除成功")); + } + catch (Exception ex) + { + return BadRequest(Result.Failure(ex.Message)); + } + } + + + // 已废弃(请使用Role Controller的分配api) + // /// + // /// 为角色分配权限 + // /// + // [HttpPost("assign")] + // [RequirePermission("permission:Update")] + // public async Task AssignPermissionToRoleAsync([FromBody] AssignPermissionDto assignDto) + // { + // try + // { + // await _permissionAppService.AssignPermissionToRoleAsync(assignDto); + // return Ok(Result.Success("权限分配成功")); + // } + // catch (Exception ex) + // { + // return BadRequest(Result.Failure(ex.Message)); + // } + // } + + /// + /// 获取权限规则配置 + /// + [HttpGet("rules")] + [RequirePermission("permission:Read")] + public IActionResult GetPermissionRulesAsync() + { + try + { + var rules = _permissionRuleConfigService.GetCurrentConfig(); + return Ok(Result>>.Success(rules)); + } + catch (Exception ex) + { + return BadRequest(Result>>.Failure(ex.Message)); + } + } + + /// + /// 更新权限规则配置 + /// + [HttpPut("rules")] + [RequirePermission("permission:Update")] + public IActionResult UpdatePermissionRulesAsync([FromBody] Dictionary> newRules) + { + try + { + // TODO: 这里应该校验当前用户是否为超级管理员 + _permissionRuleConfigService.UpdateConfig(newRules); + return Ok(Result.Success("权限规则更新成功")); + } + catch (Exception ex) + { + return BadRequest(Result.Failure(ex.Message)); + } + } + + /// + /// 刷新权限规则配置 + /// + [HttpPost("rules/refresh")] + [RequirePermission("permission:Update")] + public IActionResult RefreshPermissionRulesAsync() + { + try + { + _permissionRuleConfigService.RefreshConfig(); + return Ok(Result.Success("权限规则刷新成功")); + } + catch (Exception ex) + { + return BadRequest(Result.Failure(ex.Message)); + } + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Api/Controllers/RoleController.cs b/backend/src/UniversalAdminSystem.Api/Controllers/RoleController.cs new file mode 100644 index 0000000000000000000000000000000000000000..b975d81b7f92b69bbe3c11a245ef288252ca578f --- /dev/null +++ b/backend/src/UniversalAdminSystem.Api/Controllers/RoleController.cs @@ -0,0 +1,225 @@ +using Microsoft.AspNetCore.Mvc; +using UniversalAdminSystem.Application.PermissionManagement.DTOs; +using UniversalAdminSystem.Application.PermissionManagement.Interfaces; +using UniversalAdminSystem.Api.Attributes; +using UniversalAdminSystem.Application.Common.Results; + +namespace UniversalAdminSystem.Api.Controllers; + +/// +/// 角色管理控制器 +/// 提供角色的创建、查询、权限分配等操作 +/// +[ApiController] +[Route("api/roles")] +public class RoleController : ControllerBase +{ + private readonly IRoleManagementAppService _roleManagementAppService; + private readonly IPermissionManagementAppService _permissionAppService; + + public RoleController( + IRoleManagementAppService roleManagementAppService, + IPermissionManagementAppService permissionAppService) + { + _roleManagementAppService = roleManagementAppService; + _permissionAppService = permissionAppService; + } + + /// + /// 获取所有角色列表 + /// + [HttpGet] + [RequirePermission("role:Read")] + public async Task GetAllRolesAsync() + { + try + { + var roles = await _roleManagementAppService.GetAllRolesAsync(); + return Ok(Result>.Success(roles)); + } + catch (Exception ex) + { + return BadRequest(Result>.Failure(ex.Message)); + } + } + + /// + /// 根据ID获取角色 + /// + [HttpGet("{roleId}")] + [RequirePermission("role:Read")] + public async Task GetRoleByIdAsync(Guid roleId) + { + try + { + var role = await _roleManagementAppService.GetRoleByIdAsync(roleId); + if (role == null) + { + return NotFound(Result.Failure("角色不存在")); + } + return Ok(Result.Success(role)); + } + catch (Exception ex) + { + return BadRequest(Result.Failure(ex.Message)); + } + } + + /// + /// 创建新角色 + /// + [HttpPost] + [RequirePermission("role:Create")] + public async Task CreateRoleAsync([FromBody] RoleCreateDto createDto) + { + try + { + var role = await _roleManagementAppService.CreateRoleAsync(createDto); + return Ok(Result.Success(role)); + } + catch (Exception ex) + { + return BadRequest(Result.Failure(ex.Message)); + } + } + + /// + /// 更新角色信息 + /// + [HttpPut("{roleId}")] + [RequirePermission("role:Update")] + public async Task UpdateRoleAsync(Guid roleId, [FromBody] RoleUpdateDto updateDto) + { + try + { + var role = await _roleManagementAppService.UpdateRoleAsync(roleId, updateDto); + return Ok(Result.Success(role)); + } + catch (KeyNotFoundException) + { + return NotFound(Result.Failure("角色不存在")); + } + catch (Exception ex) + { + return BadRequest(Result.Failure(ex.Message)); + } + } + + /// + /// 删除角色 + /// + [HttpDelete("{roleId}")] + [RequirePermission("role:Delete")] + public async Task DeleteRoleAsync(Guid roleId) + { + try + { + await _roleManagementAppService.DeleteRoleAsync(roleId); + return Ok(Result.Success("角色删除成功")); + } + catch (KeyNotFoundException) + { + return NotFound(Result.Failure("角色不存在")); + } + catch (Exception ex) + { + return BadRequest(Result.Failure(ex.Message)); + } + } + + /// + /// 为角色分配权限 + /// + [HttpPost("{roleId}/permissions")] + [RequirePermission("role:Update")] + public async Task AssignPermissionsToRoleAsync( + Guid roleId, + [FromBody] List permissionCodes) + { + try + { + // 将权限编码转换为权限ID(这里需要根据实际需求调整) + + foreach (var permissionCode in permissionCodes) + { + var assignDto = new AssignPermissionDto(permissionCode, roleId); + await _permissionAppService.AssignPermissionToRoleAsync(assignDto); + } + return Ok(Result.Success("角色权限分配成功")); + } + catch (Exception ex) + { + return BadRequest(Result.Failure(ex.Message)); + } + } + + /// + /// 为角色分配权限(使用权限ID) + /// + [HttpPost("{roleId}/permissions/ids")] + [RequirePermission("role:Update")] + public async Task AssignPermissionsToRoleByIdsAsync( + Guid roleId, + [FromBody] List permissionIds) + { + try + { + await _roleManagementAppService.AssignPermissionsToRoleAsync(roleId, permissionIds); + return Ok(Result.Success("角色权限分配成功")); + } + catch (KeyNotFoundException) + { + return NotFound(Result.Failure("角色不存在")); + } + catch (Exception ex) + { + return BadRequest(Result.Failure(ex.Message)); + } + } + + /// + /// 移除角色的权限 + /// + [HttpDelete("{roleId}/permissions")] + [RequirePermission("role:Update")] + public async Task RemovePermissionsFromRoleAsync( + Guid roleId, + [FromBody] List permissionIds) + { + try + { + await _roleManagementAppService.RemovePermissionsFromRoleAsync(roleId, permissionIds); + return Ok(Result.Success("角色权限移除成功")); + } + catch (KeyNotFoundException) + { + return NotFound(Result.Failure("角色不存在")); + } + catch (Exception ex) + { + return BadRequest(Result.Failure(ex.Message)); + } + } + + /// + /// 获取角色的所有权限 + /// + [HttpGet("{roleId}/permissions")] + [RequirePermission("role:Read")] + public async Task GetRolePermissionsAsync(Guid roleId) + { + try + { + var permissions = await _roleManagementAppService.GetRolePermissionsAsync(roleId); + return Ok(Result>.Success(permissions)); + } + catch (KeyNotFoundException) + { + return NotFound(Result>.Failure("角色不存在")); + } + catch (Exception ex) + { + return BadRequest(Result>.Failure(ex.Message)); + } + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Api/Controllers/SystemSettingsController.cs b/backend/src/UniversalAdminSystem.Api/Controllers/SystemSettingsController.cs new file mode 100644 index 0000000000000000000000000000000000000000..57b42c9fbcdc7fe318410e7b8ce26907fbb33dde --- /dev/null +++ b/backend/src/UniversalAdminSystem.Api/Controllers/SystemSettingsController.cs @@ -0,0 +1,63 @@ +using Microsoft.AspNetCore.Mvc; +using UniversalAdminSystem.Application.SystemSettings.DTOs; +using UniversalAdminSystem.Application.SystemSettings.Interfaces; +using UniversalAdminSystem.Api.Attributes; +using UniversalAdminSystem.Application.Common.Results; + +namespace UniversalAdminSystem.Api.Controllers; + +[ApiController] +[Route("api/system-settings")] +public class SystemSettingsController : ControllerBase +{ + private readonly ISystemSettingAppService _settingService; + public SystemSettingsController(ISystemSettingAppService settingService) => _settingService = settingService; + + [HttpGet] + //[RequirePermission("systemsetting:read")] + public async Task GetAllSettingsAsync() + { + var result = await _settingService.GetAllSettingsAsync(); + return result.IsSuccess ? Ok(result) : BadRequest(result); + } + + [HttpGet("key/{key}")] + //[RequirePermission("systemsetting:read")] + public async Task GetSettingByKeyAsync(string key) + { + var result = await _settingService.GetSettingByKeyAsync(key); + return result.IsSuccess ? Ok(result) : BadRequest(result); + } + + [HttpGet("group/{group}")] + //[RequirePermission("systemsetting:read")] + public async Task GetSettingsByGroupAsync(string group) + { + var result = await _settingService.GetSettingsByGroupAsync(group); + return result.IsSuccess ? Ok(result) : BadRequest(result); + } + + [HttpPost] + //[RequirePermission("systemsetting:create")] + public async Task CreateSettingAsync([FromBody] SystemSettingCreateDto dto) + { + var result = await _settingService.CreateSettingAsync(dto); + return result.IsSuccess ? Ok(result) : BadRequest(result); + } + + [HttpPut("{id}")] + //[RequirePermission("systemsetting:update")] + public async Task UpdateSetting(Guid id, [FromBody] SystemSettingUpdateDto updateDto) + { + var result = await _settingService.UpdateSettingAsync(id, updateDto); + return result.IsSuccess ? Ok(result) : BadRequest(result); + } + + [HttpDelete("{id}")] + //[RequirePermission("systemsetting:delete")] + public async Task DeleteSettingAsync(Guid id) + { + var result = await _settingService.DeleteSettingAsync(id); + return result.IsSuccess ? Ok(result) : BadRequest(result); + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Api/Controllers/TestController.cs b/backend/src/UniversalAdminSystem.Api/Controllers/TestController.cs new file mode 100644 index 0000000000000000000000000000000000000000..8d6789b8d5fb919f8601c891de00a8b547f3a74c --- /dev/null +++ b/backend/src/UniversalAdminSystem.Api/Controllers/TestController.cs @@ -0,0 +1,53 @@ +using System.Text; +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using UniversalAdminSystem.Infrastructure.Services; + +namespace UniversalAdminSystem.Api.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class TestController : ControllerBase +{ + private readonly ILogger _logger; + private readonly SpaCyService _spacy; + private readonly K2ModelService _k2; + private readonly EmbeddingService _embedding; + + public TestController(ILogger logger, K2ModelService k2, SpaCyService spacy, EmbeddingService embedding) + { + _logger = logger; + _k2 = k2; + _spacy = spacy; + _embedding = embedding; + } + + [HttpPost("spacy")] + public async Task Spacy([FromBody] SpaCyRequest text) + { + // 文本分片 + _logger.LogInformation("开始分片文件: {Text}", text); + var preprocessResult = await _spacy.AnalyzeTextAsync(text.Text); + Console.WriteLine(preprocessResult.Entities); + Console.WriteLine(preprocessResult.Sentences); + + var preprocessResultJson = JsonSerializer.Serialize(preprocessResult, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + var chunks = await _k2.SendChunkingRequestAsync(preprocessResultJson); + _logger.LogInformation("分片完成,生成 {Count} 个 chunk", chunks.Count); + + // Embedding + _logger.LogInformation("开始Embedding"); + var embeddings = new List(); + embeddings.AddRange(await _embedding.GetEmbeddingAsync(chunks)); + _logger.LogInformation("Embedding 完成: {Count} 个向量", embeddings.Count); + return Ok(embeddings); + } +} + +public class SpaCyRequest(string text) +{ + public string Text { get; set; } = text; +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Api/Controllers/UserManagementController.cs b/backend/src/UniversalAdminSystem.Api/Controllers/UserManagementController.cs new file mode 100644 index 0000000000000000000000000000000000000000..b4b75f731a443fa648e912d538c3ad8bd5e7e9db --- /dev/null +++ b/backend/src/UniversalAdminSystem.Api/Controllers/UserManagementController.cs @@ -0,0 +1,176 @@ +using Microsoft.AspNetCore.Mvc; +using UniversalAdminSystem.Application.Common.Results; +using UniversalAdminSystem.Application.UserManagement.Dtos; +using UniversalAdminSystem.Application.UserManagement.Interface; +using UniversalAdminSystem.Api.Attributes; + +namespace UniversalAdminSystem.Api.Controllers; + +/// +/// 用户管理控制器 +/// +[ApiController] +[Route("api/user")] +public class UserManagementController : ControllerBase +{ + private readonly IUserManagementAppService _userRepoService; + + public UserManagementController(IUserManagementAppService userService) + { + _userRepoService = userService; + } + + [HttpGet] + [RequirePermission("user:Read")] + public async Task GetUsersAsync() + { + var result = await _userRepoService.GetUsersAsync(); + + if (result.IsSuccess) + { + return Ok(result); + } + else + { + return BadRequest(result); + } + } + + [HttpPost("create")] + [RequirePermission("user:Create")] + public async Task CreateUserAsync([FromBody] UserCreateDto userCreate) + { + var result = await _userRepoService.CreateUserAsync(userCreate); + + if (result.IsSuccess) + { + return Ok(result); + } + else + { + return BadRequest(result); + } + } + + [HttpDelete("{id}")] + [RequirePermission("user:Delete")] + public async Task DeleteUserAsync(Guid id) + { + var result = await _userRepoService.DeleteUserAsync(id); + + if (result.IsSuccess) + { + return Ok(result); + } + else + { + return BadRequest(result); + } + } + + [HttpGet("{id}")] + [RequirePermission("user:Read")] + public async Task GetUserByIdAsync(Guid id) + { + var result = await _userRepoService.GetUserByIdAsync(id); + + if (result.IsSuccess) + { + return Ok(result); + } + else + { + return BadRequest(result); + } + } + + [HttpPut("{id}")] + [RequirePermission("user:Update")] + public async Task UpdateUserAsync(Guid id, [FromBody] UserUpdateDto updateDto) + { + var result = await _userRepoService.UpdateUserAsync(id, updateDto); + + if (result.IsSuccess) + { + return Ok(result); + } + else + { + return BadRequest(result); + } + } + + // 已废弃(批量获取角色) + // [HttpPost("{id}/role")] + // [RequirePermission("user:Update")] + // public async Task AssignRoleAsync(Guid id, [FromBody] List roleIds) + // { + // var result = await _userRepoService.AssignRoleAsync(id, roleIds); + + // if (result.IsSuccess) + // { + // return Ok(result); + // } + // else + // { + // return BadRequest(result); + // } + // } + + /// + /// 移除用户角色 + /// + [HttpDelete("{id}/roles/{roleId}")] + [RequirePermission("user:Update")] + public async Task RemoveRoleAsync(Guid id, Guid roleId) + { + var result = await _userRepoService.RemoveRoleAsync(id, roleId); + + if (result.IsSuccess) + { + return Ok(result); + } + else + { + return BadRequest(result); + } + } + + /// + /// 获取用户的所有权限 + /// + [HttpGet("{id}/permissions")] + [RequirePermission("user:Read")] + public async Task GetUserPermissionsAsync(Guid id) + { + var result = await _userRepoService.GetUserPermissionsAsync(id); + + if (result.IsSuccess) + { + return Ok(result); + } + else + { + return BadRequest(result); + } + } + + /// + /// 检查用户是否有指定权限 + /// + [HttpPost("{id}/permissions/check")] + [RequirePermission("user:Read")] + public async Task CheckUserPermissionAsync(Guid id, [FromBody] string permissionCode) + { + var result = await _userRepoService.CheckUserPermissionAsync(id, permissionCode); + + if (result.IsSuccess) + { + return Ok(result); + } + else + { + return BadRequest(result); + } + } +} diff --git a/backend/src/UniversalAdminSystem.Api/Controllers/UserRoleController.cs b/backend/src/UniversalAdminSystem.Api/Controllers/UserRoleController.cs new file mode 100644 index 0000000000000000000000000000000000000000..935fd991b9e9e44f2a04a0d40a53a3600314c91b --- /dev/null +++ b/backend/src/UniversalAdminSystem.Api/Controllers/UserRoleController.cs @@ -0,0 +1,122 @@ +using Microsoft.AspNetCore.Mvc; +using UniversalAdminSystem.Application.Common.Results; +using UniversalAdminSystem.Application.PermissionManagement.Interfaces; +using UniversalAdminSystem.Api.Attributes; + +namespace UniversalAdminSystem.Api.Controllers; + +/// +/// 用户角色管理控制器 +/// 提供用户角色分配和管理功能 +/// +[ApiController] +[Route("api/users")] +public class UserRoleController : ControllerBase +{ + private readonly IPermissionCheckService _permissionCheckService; + + public UserRoleController(IPermissionCheckService permissionCheckService) + { + _permissionCheckService = permissionCheckService; + } + + /// + /// 获取用户的所有权限 + /// + [HttpGet("{userId}/permissions")] + [RequirePermission("user:Read")] + public async Task GetUserPermissionsAsync(Guid userId) + { + try + { + var permissions = await _permissionCheckService.GetUserPermissionsAsync(userId); + var result = Result>.Success(permissions); + return Ok(result); + } + catch (Exception ex) + { + var result = Result>.Failure($"获取用户权限失败: {ex.Message}"); + return BadRequest(result); + } + } + + /// + /// 获取用户的所有角色 + /// + [HttpGet("{userId}/roles")] + [RequirePermission("user:Read")] + public async Task GetUserRolesAsync(Guid userId) + { + try + { + var roles = await _permissionCheckService.GetUserRoleAsync(userId); + var result = Result>.Success(roles); + return Ok(result); + } + catch (Exception ex) + { + var result = Result>.Failure($"获取用户角色失败: {ex.Message}"); + return BadRequest(result); + } + } + + /// + /// 检查用户是否有指定权限 + /// + [HttpPost("{userId}/permissions/check")] + [RequirePermission("user:Read")] + public async Task CheckUserPermissionAsync(Guid userId, [FromBody] string permissionCode) + { + try + { + var hasPermission = await _permissionCheckService.CheckUserPermissionAsync(userId, permissionCode); + var result = Result.Success(hasPermission); + return Ok(result); + } + catch (Exception ex) + { + var result = Result.Failure($"检查用户权限失败: {ex.Message}"); + return BadRequest(result); + } + } + + /// + /// 检查用户是否有任意一个指定权限 + /// + [HttpPost("{userId}/permissions/check-any")] + [RequirePermission("user:Read")] + public async Task CheckUserAnyPermissionAsync(Guid userId, [FromBody] List permissionCodes) + { + try + { + var hasPermission = await _permissionCheckService.CheckUserAnyPermissionAsync(userId, permissionCodes); + var result = Result.Success(hasPermission); + return Ok(result); + } + catch (Exception ex) + { + var result = Result.Failure($"检查用户权限失败: {ex.Message}"); + return BadRequest(result); + } + } + + /// + /// 检查用户是否有所有指定权限 + /// + [HttpPost("{userId}/permissions/check-all")] + [RequirePermission("user:Read")] + public async Task CheckUserAllPermissionsAsync(Guid userId, [FromBody] List permissionCodes) + { + try + { + var hasPermission = await _permissionCheckService.CheckUserAllPermissionsAsync(userId, permissionCodes); + var result = Result.Success(hasPermission); + return Ok(result); + } + catch (Exception ex) + { + var result = Result.Failure($"检查用户权限失败: {ex.Message}"); + return BadRequest(result); + } + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Api/K2ModelTests.http b/backend/src/UniversalAdminSystem.Api/K2ModelTests.http new file mode 100644 index 0000000000000000000000000000000000000000..ae1fd04ad9a8157616f3f7f656bb59c4492def38 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Api/K2ModelTests.http @@ -0,0 +1,20 @@ +### +POST https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions +Content-Type: application/json +Authorization: Bearer sk-a31163f6b59e44dcbdb87c668482ce96 + +{ + "model": "Moonshot-Kimi-K2-Instruct", + "messages": [ + { + "role": "user", + "content": "你好,请介绍一下你自己" + } + ], + "temperature": 0.7, + "max_tokens": 1000, + "stream": true, + "stream_options": { + "include_usage": true + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Api/Middleware/JwtAuthenticationMiddleware.cs b/backend/src/UniversalAdminSystem.Api/Middleware/JwtAuthenticationMiddleware.cs new file mode 100644 index 0000000000000000000000000000000000000000..c528bfa46d414d333ce26d0c85b4cc72cacc63c8 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Api/Middleware/JwtAuthenticationMiddleware.cs @@ -0,0 +1,126 @@ +using System.Security.Claims; +using UniversalAdminSystem.Application.Authentication.Interfaces; +using UniversalAdminSystem.Domian.UserManagement.ValueObj; + +namespace UniversalAdminSystem.Api.Middleware; + +public class JwtAuthenticationMiddleware +{ + private readonly RequestDelegate _next; + private readonly IJwtTokenService _jwtService; + private readonly ILogger _logger; + + public JwtAuthenticationMiddleware( + RequestDelegate next, + IJwtTokenService jwtService, + ILogger logger) + { + _next = next; + _jwtService = jwtService; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + try + { + var token = ExtractTokenFromHeader(context); + Console.WriteLine($"提取到的Token: {(token != null ? "存在" : "不存在")}"); + if (!string.IsNullOrEmpty(token)) + { + Console.WriteLine($"Token内容: {token.Substring(0, Math.Min(50, token.Length))}..."); + var userInfo = await AuthenticateTokenAsync(token); + if (userInfo.HasValue) + { + Console.WriteLine($"Token解析结果 - 用户ID: {userInfo.Value.UserId}, 角色ID: {userInfo.Value.RoleId}, 状态: {userInfo.Value.status}"); + SetUserClaims(context, userInfo.Value); + _logger.LogInformation("用户 {UserId} 认证成功", userInfo.Value.UserId); + } + else + { + Console.WriteLine("Token解析失败"); + } + } + else + { + Console.WriteLine("Token不存在"); + } + } + catch (Exception ex) + { + _logger.LogWarning("JWT认证中间件异常: {Message}", ex.Message); + } + + await _next(context); + } + + private string? ExtractTokenFromHeader(HttpContext context) + { + var authHeader = context.Request.Headers["Authorization"].FirstOrDefault(); + Console.WriteLine($"Authorization头: {authHeader ?? "null"}"); + + if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine("Authorization头为空或不以Bearer开头"); + return null; + } + + var token = authHeader.Substring("Bearer ".Length).Trim(); + Console.WriteLine($"提取的Token长度: {token.Length}"); + Console.WriteLine($"Token前50个字符: {token.Substring(0, Math.Min(50, token.Length))}"); + + // 检查token格式 + if (string.IsNullOrEmpty(token)) + { + Console.WriteLine("Token为空"); + return null; + } + + // JWT应该包含两个点,格式为: header.payload.signature + var parts = token.Split('.'); + if (parts.Length != 3) + { + Console.WriteLine($"Token格式错误,包含{parts.Length}个部分,应该是3个部分"); + return null; + } + + Console.WriteLine("Token格式检查通过"); + return token; + } + + private async Task<(string UserId, string RoleId, UserStatus status)?> AuthenticateTokenAsync(string token) + { + try + { + var (userId, roleId, status) = _jwtService.ParseToken(token); + Console.WriteLine($"Token解析结果 - 用户ID: {userId}, 角色ID: {roleId}, 状态: {status}"); + return (userId, roleId, status); + } + catch (Exception ex) + { + _logger.LogWarning("Token解析失败: {Message}", ex.Message); + return null; + } + } + + private void SetUserClaims(HttpContext context, (string UserId, string RoleId, UserStatus status) userInfo) + { + var claims = new List + { + new Claim(ClaimTypes.NameIdentifier, userInfo.UserId), + new Claim(ClaimTypes.Role, userInfo.RoleId), + new Claim(ClaimTypes.StateOrProvince, userInfo.status.ToString()), + }; + + var identity = new ClaimsIdentity(claims, "jwt"); + context.User = new ClaimsPrincipal(identity); + } +} + +public static class JwtAuthenticationMiddlewareExtensions +{ + public static IApplicationBuilder UseJwtAuthentication(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Api/PermissionRules.json b/backend/src/UniversalAdminSystem.Api/PermissionRules.json new file mode 100644 index 0000000000000000000000000000000000000000..2c8a6a266227f8916da510946f9e11f0468f95c2 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Api/PermissionRules.json @@ -0,0 +1,13 @@ +{ + "Read": ["data", "file", "user", "role", "permission", "config", "system", "report","document"], + "Create": ["data", "user", "file", "role", "permission"], + "Update": ["data", "user", "config", "role", "permission"], + "Delete": ["data", "user", "file", "role", "permission"], + "Manage": ["system", "user"], + "Execute": ["job", "script"], + "Import": ["data", "file"], + "Export": ["data", "file", "report"], + "Public": ["document"], + "Restricted": ["document"], + "Private": ["document"] +} diff --git a/backend/src/UniversalAdminSystem.Api/Program.cs b/backend/src/UniversalAdminSystem.Api/Program.cs new file mode 100644 index 0000000000000000000000000000000000000000..5fb4110917d7238c6096064ce66bb217d5743e52 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Api/Program.cs @@ -0,0 +1,105 @@ +using UniversalAdminSystem.Infrastructure.DependencyInject; +using UniversalAdminSystem.Infrastructure.Services; +using UniversalAdminSystem.Api.Middleware; +using Microsoft.OpenApi.Models; + +var builder = WebApplication.CreateBuilder(args); + +// 加载外部的允许文件配置,避免 appsettings.json 膨胀 +builder.Configuration.AddJsonFile("AllowedFiles.json", optional: false, reloadOnChange: true); + +builder.Services.AddHttpClient(); +// 添加CORS服务 +builder.Services.AddCors(options => +{ + // 开发环境策略 - 允许所有来源 + options.AddPolicy("AllowAll", policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); + + // 生产环境策略 - 指定允许的来源 + options.AddPolicy("AllowSpecific", policy => + { + policy.WithOrigins( + "http://localhost:5173", + "http://localhost:3000", // React开发服务器 + "http://localhost:8080", // Vue开发服务器 + "http://localhost:4200", // Angular开发服务器 + "https://mikuslittlenest.cn", // 生产域名 + "http://www.mikuslittlenest.cn", + "http://localhost:8081", + "http://mikuslittlenest.cn" + ) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials(); + }); +}); + +// Add services to the container. +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new OpenApiInfo { Title = "Universal Admin System API", Version = "v1" }); + + // 添加JWT认证配置 + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey, + Scheme = "Bearer" + }); + + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + new string[] {} + } + }); +}); + +// 在应用启动时初始化权限规则配置 +var configPath = Path.Combine(Directory.GetCurrentDirectory(), "PermissionRules.json"); +var permissionRuleConfigService = new PermissionRuleConfigService(configPath); +permissionRuleConfigService.Initialize(); + +builder.Services.AddSingleton(permissionRuleConfigService); + +// 显式注册 AllowedFilesConfig +builder.Services.Configure( + builder.Configuration.GetSection("AllowedFiles")); + +builder.Services.AddAllService(builder.Configuration); +builder.Services.AddControllers(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseCors("AllowSpecific"); + +// 添加JWT认证中间件 +Console.WriteLine("正在注册JWT认证中间件..."); +app.UseJwtAuthentication(); +Console.WriteLine("JWT认证中间件注册完成"); + +app.MapControllers(); +app.Run(); diff --git a/backend/src/UniversalAdminSystem.Api/Properties/launchSettings.json b/backend/src/UniversalAdminSystem.Api/Properties/launchSettings.json new file mode 100644 index 0000000000000000000000000000000000000000..db616011b26ea66a5e4440373d36404056175094 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Api/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:56387", + "sslPort": 44340 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5101", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7242;http://localhost:5101", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/backend/src/UniversalAdminSystem.Api/SystemPermissions.json b/backend/src/UniversalAdminSystem.Api/SystemPermissions.json new file mode 100644 index 0000000000000000000000000000000000000000..7aa7b88cdcfc5f1a9bfb34e096149736975e79b6 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Api/SystemPermissions.json @@ -0,0 +1,89 @@ +{ + "SystemPermissions": [ + { + "Resource": "file", + "Action": "Create", + "Name": "文件上传" + }, + { + "Resource": "file", + "Action": "Read", + "Name": "文件下载" + }, + { + "Resource": "file", + "Action": "Delete", + "Name": "文件删除" + }, + { + "Resource": "user", + "Action": "Create", + "Name": "创建用户" + }, + { + "Resource": "user", + "Action": "Read", + "Name": "查看用户" + }, + { + "Resource": "user", + "Action": "Update", + "Name": "更新用户" + }, + { + "Resource": "user", + "Action": "Delete", + "Name": "删除用户" + }, + { + "Resource": "role", + "Action": "Create", + "Name": "创建角色" + }, + { + "Resource": "role", + "Action": "Read", + "Name": "查看角色" + }, + { + "Resource": "role", + "Action": "Update", + "Name": "更新角色" + }, + { + "Resource": "role", + "Action": "Delete", + "Name": "删除角色" + }, + { + "Resource": "permission", + "Action": "Create", + "Name": "创建权限" + }, + { + "Resource": "permission", + "Action": "Read", + "Name": "查看权限" + }, + { + "Resource": "permission", + "Action": "Update", + "Name": "更新权限" + }, + { + "Resource": "permission", + "Action": "Delete", + "Name": "删除权限" + }, + { + "Resource": "config", + "Action": "Update", + "Name": "更新系统配置" + }, + { + "Resource": "system", + "Action": "Manage", + "Name": "管理安全策略" + } + ] +} diff --git a/backend/src/UniversalAdminSystem.Api/UniversalAdminSystem.Api.csproj b/backend/src/UniversalAdminSystem.Api/UniversalAdminSystem.Api.csproj new file mode 100644 index 0000000000000000000000000000000000000000..5e7a731a722430a09a19f0350d1d876d0aec3084 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Api/UniversalAdminSystem.Api.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/backend/src/UniversalAdminSystem.Api/UniversalAdminSystem.Api.http b/backend/src/UniversalAdminSystem.Api/UniversalAdminSystem.Api.http new file mode 100644 index 0000000000000000000000000000000000000000..1596c1b6f35b2a20307bc62e11cae2f3b6813cc5 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Api/UniversalAdminSystem.Api.http @@ -0,0 +1,18 @@ +@url = http://localhost:5101 + +### login +POST {{url}}/api/auth/login +Content-Type: application/json + +{ + "account": "manager", + "password": "manager123" +} + +### test spacy +POST {{url}}/api/test/spacy +Content-Type: application/json + +{ + "text": "初音未来(日语:初音 ミク/はつねミク Hatsune Miku)是克理普敦未来媒体(简称Crypton Future Media)以雅马哈的VOCALOID语音合成引擎为基础开发的虚拟歌手。她的形象是一位留着青绿色双马尾、身穿未来感服装的16岁少女。自2007年发布以来,她在全球范围内获得了大量粉丝,演唱会甚至使用全息投影技术现场表演。她的粉丝遍布中国、日本、美国等地。Her popularity spread rapidly through platforms like YouTube, NicoNico Douga, and Bilibili.\n\nCrypton在2020年发布了基于全新M9引擎的初音未来NT版本,该版本改进了发音的自然度和表现力。初音未来不仅仅是一款软件,更是音乐创作文化的象征。许多音乐人,包括著名制作人ryo(supercell)、DECO*27,都为她创作了脍炙人口的作品,如《千本樱》《Tell Your World》。这些作品在全球播放量超过数亿次,并被翻唱成多种语言版本。\n以下是一个超长的无标点测试段落它包含了许多描述但是没有任何中文句号逗号或感叹号这种情况下我们的分片算法应该能够在达到最大token限制时自动切割同时保持尽可能的语义完整性而不是让分片在一个人名例如初音未来的中间被截断因为那样会影响RAG的召回效果\n此外,初音未来的成功还催生了大量衍生角色和跨界合作。例如,雪初音是她的冬季特别版本,已经连续多年成为札幌冰雪节的形象大使。她还与赛车、旅游、食品、服装等行业合作,形成了庞大的商业生态。In the future, AI voice synthesis technology may allow for even more natural and expressive performances, and Hatsune Miku could evolve into an interactive virtual idol capable of real-time conversations with fans." +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Api/appsettings.json b/backend/src/UniversalAdminSystem.Api/appsettings.json new file mode 100644 index 0000000000000000000000000000000000000000..070b4fdfe3bf7dc8183251b8c216f0d051696bd2 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Api/appsettings.json @@ -0,0 +1,37 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "UniversalAdminSystem.Infrastructure.Services.SystemPermissionConfigLoader": "Debug" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "pgSql": "Server=localhost;Port=5432;Username=admin;Password=031028@yue;Database=rag_vector_db", + "RabbitMq": "amqp://admin:admin@localhost:5672/" + }, + "Jwt": { + "Key": "YourSuperSecretKey1232347509872093oiqewupori", + "Issuer": "UniversalAdminSystem", + "Audience": "api-web-admin", + "ExpireHours": 2 + }, + "K2": { + "ApiKey": "sk-a31163f6b59e44dcbdb87c668482ce96", + "BaseUrl": "https://dashscope.aliyuncs.com/compatible-mode/v1/" + }, + "Tongyi": { + "ApiKey": "sk-a31163f6b59e44dcbdb87c668482ce96", + "BaseUrl": "https://dashscope.aliyuncs.com/" + }, + "RabbitMq": { + "Host": "localhost", + "Port": 5672, + "Username": "guest", + "Password": "guest", + "Exchange": "file-processing", + "Queue": "file-processing-queue", + "RoutingKey": "file-processing" + } +} diff --git a/backend/src/UniversalAdminSystem.Application/AIQuestions/DTOs/ContentDto.cs b/backend/src/UniversalAdminSystem.Application/AIQuestions/DTOs/ContentDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..9bdf02857f872aafe0d952dd12925064e2f16874 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/AIQuestions/DTOs/ContentDto.cs @@ -0,0 +1,3 @@ +namespace UniversalAdminSystem.Application.AIQuestions.DTOs; + +public record ContentDto(string Content); \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/AIQuestions/DTOs/ContentResultDto.cs b/backend/src/UniversalAdminSystem.Application/AIQuestions/DTOs/ContentResultDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..98786f9d68ac12a339056adf7e2758ec7b85433d --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/AIQuestions/DTOs/ContentResultDto.cs @@ -0,0 +1,3 @@ +namespace UniversalAdminSystem.Application.AIQuestions.DTOs; + +public record ContentResultDto(string Role,string Content); \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/AIQuestions/DTOs/ConversationsDto.cs b/backend/src/UniversalAdminSystem.Application/AIQuestions/DTOs/ConversationsDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..e28eaf9e585223b76569e81b7feb1800c7b9b88c --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/AIQuestions/DTOs/ConversationsDto.cs @@ -0,0 +1,3 @@ +namespace UniversalAdminSystem.Application.AIQuestions.DTOs; + +public record ConversationsDto(Guid UserId,string Title); \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/AIQuestions/DTOs/ConversationsResultDto.cs b/backend/src/UniversalAdminSystem.Application/AIQuestions/DTOs/ConversationsResultDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..1349ba6a7f445ab293128b6894b86db9f3d51069 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/AIQuestions/DTOs/ConversationsResultDto.cs @@ -0,0 +1,3 @@ +namespace UniversalAdminSystem.Application.AIQuestions.DTOs; + +public record ConversationsResultDto(Guid ConversationId,string? Title); \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/AIQuestions/Interfaces/IAIQusetionsAppService.cs b/backend/src/UniversalAdminSystem.Application/AIQuestions/Interfaces/IAIQusetionsAppService.cs new file mode 100644 index 0000000000000000000000000000000000000000..c00924e7bfb4195f5fda679c14dcb45abefbacfa --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/AIQuestions/Interfaces/IAIQusetionsAppService.cs @@ -0,0 +1,21 @@ +using UniversalAdminSystem.Application.AIQuestions.DTOs; + +namespace UniversalAdminSystem.Application.AIQuestions.Interfaces; + +public interface IAIQusetionsAppService +{ + + Task> GetUsersConversationsByUserId(Guid userId); + + Task DeleteUserConversations(Guid ConversationsId); + + Task VisitorAccess(ContentDto content); + + Task UserAccess(Guid id, ContentDto content); + + Task> GetConversationMessage(Guid Id); + + Task CreateConversation(ConversationsDto conversationsDto); + + Task Chat(Guid conversationId, string userMessage); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/AIQuestions/Interfaces/IAnswerService.cs b/backend/src/UniversalAdminSystem.Application/AIQuestions/Interfaces/IAnswerService.cs new file mode 100644 index 0000000000000000000000000000000000000000..4ae1201492acfbec96e882627e672760c6af1c32 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/AIQuestions/Interfaces/IAnswerService.cs @@ -0,0 +1,8 @@ +using UniversalAdminSystem.Domian.UserConversations.Aggregates; + +namespace UniversalAdminSystem.Application.AIQuestions.Interfaces; + +public interface IAnswerService +{ + Task AnswerAsync(string userInput, IEnumerable? messages); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/AIQuestions/Service/AIQusetionsAppService.cs b/backend/src/UniversalAdminSystem.Application/AIQuestions/Service/AIQusetionsAppService.cs new file mode 100644 index 0000000000000000000000000000000000000000..dbebcef9da932223db7c1d3b5ed1b92108cb6a50 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/AIQuestions/Service/AIQusetionsAppService.cs @@ -0,0 +1,142 @@ +using UniversalAdminSystem.Application.AIQuestions.DTOs; +using UniversalAdminSystem.Application.AIQuestions.Interfaces; +using UniversalAdminSystem.Application.Common.Interfaces; +using UniversalAdminSystem.Application.PermissionManagement.Interfaces; +using UniversalAdminSystem.Domian.Core.ValueObjects; +using UniversalAdminSystem.Domian.UserConversations.Aggregates; +using UniversalAdminSystem.Domian.UserConversations.IRepository; +using UniversalAdminSystem.Domian.UserManagement.IRepository; + +namespace UniversalAdminSystem.Application.AIQuestions.Service; + +public class AIQusetionsAppService : IAIQusetionsAppService +{ + private readonly IMessageRepository _messageRepo; + + private readonly IConversationsRepository _conversationsRepo; + + private readonly IPermissionCheckService _permissioncheck; + + private readonly IUserRepository _userRepository; + + private readonly IUnitOfWork _work; + private readonly IAnswerService _answerService; + + public AIQusetionsAppService(IMessageRepository message, + IConversationsRepository conversations, + IPermissionCheckService permissioncheck, + IUnitOfWork work, + IUserRepository userRepository, + IAnswerService answerService) + { + _messageRepo = message; + _conversationsRepo = conversations; + _permissioncheck = permissioncheck; + _work = work; + _userRepository = userRepository; + _answerService = answerService; + } + + public async Task CreateConversation(ConversationsDto conversationsDto) + { + try + { + var user = await _userRepository.GetByGuidAsync(conversationsDto.UserId) ?? throw new Exception("用户不存在"); + await _work.BeginTransactionAsync(); + var Conversation = Conversations.Create(conversationsDto.UserId, conversationsDto.Title); + var entity = await _conversationsRepo.AddAsync(Conversation); + await _work.CommitAsync(); + return new ConversationsResultDto(entity.Id.Value, entity.Title); + } + catch (System.Exception e) + { + await _work.RollbackAsync(); + throw new Exception($"用户会话创建失败:{e.Message}"); + } + } + + public async Task DeleteUserConversations(Guid ConversationsId) + { + try + { + await _work.BeginTransactionAsync(); + await _messageRepo.RemoveByConversationIdAsync(ConversationsId); + await _conversationsRepo.RemoveConversation(ConversationsId); + await _work.CommitAsync(); + } + catch (System.Exception) + { + await _work.RollbackAsync(); + throw new Exception("删除异常"); + } + } + + public async Task> GetConversationMessage(Guid Id) + { + try + { + var list = await _messageRepo.GetByConversationIdAsync(Id); + return list.Select(m => new ContentResultDto(m.Role, m.Content)); + } + catch (System.Exception) + { + throw new Exception("获取用户会话消息失败"); + } + } + + public async Task> GetUsersConversationsByUserId(Guid userId) + { + try + { + var user = await _userRepository.GetByGuidAsync(userId) ?? throw new Exception("用户不存在"); + var list = await _conversationsRepo.GetByUserIdAsync(userId); + return list.Select(m => new ConversationsResultDto(m.Id.Value, m.Title)); + } + catch (System.Exception e) + { + throw new Exception($"获取用户会话失败:{e.Message}"); + } + } + + public async Task Chat(Guid conversationId, string userInput) + { + try + { + // 开启事务 + await _work.BeginTransactionAsync(); + + // 创建消息并持久化 + var message = Message.Create(ConversationId.Create(conversationId), "user", userInput); + await _messageRepo.AddAsync(message); + + await _work.CommitAsync(); + // 回答 + await _work.BeginTransactionAsync(); + var messages = await _messageRepo.GetByConversationIdAsync(conversationId); + System.Console.WriteLine("消息列表:",messages); + var temp = await _answerService.AnswerAsync(userInput, messages); + System.Console.WriteLine(temp); + var Systemmessage = Message.Create(ConversationId.Create(conversationId), "system", temp); + await _messageRepo.AddAsync(Systemmessage); + await _work.CommitAsync(); + return temp; + } + catch (System.Exception e) + { + await _work.RollbackAsync(); + throw new Exception(e.Message); + } + + + } + + public Task UserAccess(Guid id, ContentDto content) + { + throw new NotImplementedException(); + } + + public Task VisitorAccess(ContentDto content) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/Authentication/DTOs/CredentialDto.cs b/backend/src/UniversalAdminSystem.Application/Authentication/DTOs/CredentialDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..bd00463569e418a98f1008911948dad8eab28e5d --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/Authentication/DTOs/CredentialDto.cs @@ -0,0 +1,3 @@ +namespace UniversalAdminSystem.Application.Authentication.DTOs; + +public record CredentialDto(string Message, string? Account = null, string? Token = null); diff --git a/backend/src/UniversalAdminSystem.Application/Authentication/DTOs/LoginDto.cs b/backend/src/UniversalAdminSystem.Application/Authentication/DTOs/LoginDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..14bb597dc0e88473ec907bd0e58928820e843484 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/Authentication/DTOs/LoginDto.cs @@ -0,0 +1,3 @@ +namespace UniversalAdminSystem.Application.Authentication.DTOs; + +public record LoginDto(string Account, string Password); \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/Authentication/DTOs/LoginResultDto.cs b/backend/src/UniversalAdminSystem.Application/Authentication/DTOs/LoginResultDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..742c9c124afd2c3c14b5d24e116b7a7a05bde8d6 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/Authentication/DTOs/LoginResultDto.cs @@ -0,0 +1,5 @@ +using UniversalAdminSystem.Domian.UserManagement.Entities; + +namespace UniversalAdminSystem.Application.Authentication.DTOs; + +public record LoginResultDto(string Token, string UserId, string RoleId, string UserName,UserInfo UserInfo); \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/Authentication/DTOs/RegisterDto.cs b/backend/src/UniversalAdminSystem.Application/Authentication/DTOs/RegisterDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..8006203034c7633dc9dc6516a3672ca4c8aad500 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/Authentication/DTOs/RegisterDto.cs @@ -0,0 +1,3 @@ +namespace UniversalAdminSystem.Application.Authentication.DTOs; + +public record RegisterDto(string Account, string Password, string Email); \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/Authentication/DTOs/RegisterResultDto.cs b/backend/src/UniversalAdminSystem.Application/Authentication/DTOs/RegisterResultDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..00c7d44b1b6895cbd8fde95ff42cb88bbf6574eb --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/Authentication/DTOs/RegisterResultDto.cs @@ -0,0 +1,3 @@ +namespace UniversalAdminSystem.Application.Authentication.DTOs; + +public record RegisterResultDto(string UserId, string Account, string Message); \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/Authentication/DTOs/TokenDto.cs b/backend/src/UniversalAdminSystem.Application/Authentication/DTOs/TokenDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..e91dd4d16d383e05deebcb78a57740dab76d59cd --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/Authentication/DTOs/TokenDto.cs @@ -0,0 +1,26 @@ +namespace UniversalAdminSystem.Application.Authentication.DTOs; + +/// +/// 刷新Token请求DTO +/// +public record RefreshTokenDto(string Token); + +/// +/// 刷新Token结果DTO +/// +public record RefreshTokenResultDto(string Token, string UserId, string RoleId); + +/// +/// 验证Token请求DTO +/// +public record ValidateTokenDto(string Token); + +/// +/// Token验证结果DTO +/// +public record TokenValidationResultDto(bool IsValid, string UserId, string RoleId); + +/// +/// 登出响应DTO +/// +public record LogoutResultDto(string Message); \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/Authentication/Interfaces/IJwtTokenService.cs b/backend/src/UniversalAdminSystem.Application/Authentication/Interfaces/IJwtTokenService.cs new file mode 100644 index 0000000000000000000000000000000000000000..b9413a3df28a446ab9708c82a9d9a1ccfb93fa39 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/Authentication/Interfaces/IJwtTokenService.cs @@ -0,0 +1,10 @@ +using UniversalAdminSystem.Domian.UserManagement.ValueObj; + +namespace UniversalAdminSystem.Application.Authentication.Interfaces; + +public interface IJwtTokenService +{ + string GenerateToken(string userId, string roleId, UserStatus status); + (string userId, string roleId, UserStatus status) ParseToken(string token); + string RefreshToken(string oldToken, out string userId, out string roleId, out UserStatus status); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/Authentication/Interfaces/ILoginAppService.cs b/backend/src/UniversalAdminSystem.Application/Authentication/Interfaces/ILoginAppService.cs new file mode 100644 index 0000000000000000000000000000000000000000..80adb82212c043cd90b77b03ed8d1665b45f3fed --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/Authentication/Interfaces/ILoginAppService.cs @@ -0,0 +1,8 @@ +using UniversalAdminSystem.Application.Authentication.DTOs; + +namespace UniversalAdminSystem.Application.Authentication.Interfaces; + +public interface ILoginAppService +{ + Task LoginAsync(LoginDto dto); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/Authentication/Interfaces/IRegisterAppService.cs b/backend/src/UniversalAdminSystem.Application/Authentication/Interfaces/IRegisterAppService.cs new file mode 100644 index 0000000000000000000000000000000000000000..7070f3a78f5afdf627cc73bb709e67fc13761a4c --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/Authentication/Interfaces/IRegisterAppService.cs @@ -0,0 +1,8 @@ +using UniversalAdminSystem.Application.Authentication.DTOs; + +namespace UniversalAdminSystem.Application.Authentication.Interfaces; + +public interface IRegisterAppService +{ + Task RegisterAsync(RegisterDto dto); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/Authentication/Service/LoginAppService.cs b/backend/src/UniversalAdminSystem.Application/Authentication/Service/LoginAppService.cs new file mode 100644 index 0000000000000000000000000000000000000000..70edb566165c26011b07e3da000a9b9f2455816f --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/Authentication/Service/LoginAppService.cs @@ -0,0 +1,40 @@ +using UniversalAdminSystem.Application.Authentication.DTOs; +using UniversalAdminSystem.Application.Authentication.Interfaces; +using UniversalAdminSystem.Domian.UserManagement.IRepository; +using UniversalAdminSystem.Application.UserManagement.Interface; +using UniversalAdminSystem.Domian.UserManagement.ValueObj; + +namespace UniversalAdminSystem.Application.Authentication.Service; + +public class LoginAppService : ILoginAppService +{ + private readonly IUserRepository _userRepository; + private readonly IJwtTokenService _jwtService; + private readonly IPasswordHelper _passwordHelper; + private readonly IUserInfoRepository _userInfoRepository; + + public LoginAppService( + IUserRepository userRepository, + IJwtTokenService jwtService, + IPasswordHelper passwordHelper, + IUserInfoRepository userInfoRepository) + { + _userRepository = userRepository; + _jwtService = jwtService; + _passwordHelper = passwordHelper; + _userInfoRepository = userInfoRepository; + } + + public async Task LoginAsync(LoginDto dto) + { + + var user = await _userRepository.GetUserByAccountAsync(dto.Account) ?? throw new Exception("账号不存在"); + var userinfo = await _userInfoRepository.GetByGuidAsync(user.UserInfoId ?? throw new Exception("用户信息未绑定")) ?? throw new Exception("用户信息不存在"); + // 使用密码验证服务进行安全比对 + if (!_passwordHelper.VerifyPasswordWithSeparateSalt(dto.Password, user.Password, user.Salt)) throw new Exception("密码错误"); + // 生成JWT令牌 + var token = _jwtService.GenerateToken(user.UserId.Value.ToString(), user.RoleId?.Value.ToString() ?? "", user.Status); + // 返回登录结果 + return new LoginResultDto(token, user.UserId.Value.ToString(), user.RoleId?.Value.ToString() ?? "", user.Account.Value.ToString(),userinfo); + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/Authentication/Service/RegisterAppService.cs b/backend/src/UniversalAdminSystem.Application/Authentication/Service/RegisterAppService.cs new file mode 100644 index 0000000000000000000000000000000000000000..5577a52231a07311a7b1494891d20dddb6d5e664 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/Authentication/Service/RegisterAppService.cs @@ -0,0 +1,91 @@ +using UniversalAdminSystem.Application.Authentication.DTOs; +using UniversalAdminSystem.Application.Authentication.Interfaces; +using UniversalAdminSystem.Domian.UserManagement.Aggregates; +using UniversalAdminSystem.Domian.UserManagement.IRepository; +using UniversalAdminSystem.Domian.UserManagement.ValueObj; +using UniversalAdminSystem.Application.UserManagement.Interface; +using UniversalAdminSystem.Domian.UserManagement.Entities; +using UniversalAdminSystem.Application.Common.Interfaces; +using UniversalAdminSystem.Domian.PermissionManagement.IRepository; + +namespace UniversalAdminSystem.Application.Authentication.Service; + +public class RegisterAppService : IRegisterAppService +{ + private readonly IUserRepository _userRepository; + private readonly IPasswordHelper _passwordHelper; + private readonly IUnitOfWork _unitOfWork; + + private readonly IRoleRepository _RoleRepo; + + public RegisterAppService( + IUserRepository userRepository, + IPasswordHelper passwordHelper, + IUnitOfWork unitOfWork, + IRoleRepository roleRepository) + { + _userRepository = userRepository; + _passwordHelper = passwordHelper; + _unitOfWork = unitOfWork; + _RoleRepo = roleRepository; + } + + public async Task RegisterAsync(RegisterDto dto) + { + try + { + // 开始事务 + await _unitOfWork.BeginTransactionAsync(); + + // 检查账号是否已存在 + var existingUser = await _userRepository.GetUserByAccountAsync(dto.Account); + if (existingUser != null) + { + throw new Exception("账号已存在"); + } + + // 检查邮箱是否已存在 + var existingUserByEmail = await _userRepository.GetUserByEmailAsync(dto.Email); + if (existingUserByEmail != null) + { + throw new Exception("邮箱已被注册"); + } + + // 密码加密 + var (hashedPassword, salt) = _passwordHelper.HashPasswordWithSeparateSalt(dto.Password); + + // 创建用户信息 + var userInfo = UserInfo.CreateUserInfo(); + await _userRepository.AddUserInfoAsync(userInfo); + var Role = await _RoleRepo.GetByNameAsync("普通用户") ?? throw new Exception("用户创建失败"); + // 创建用户 + var user = User.CreateUser( + userInfo.UserInfoId, + dto.Account, + hashedPassword, + dto.Email, + salt, + UserStatus.Normal, + Role.RoleId.Value + + ); + + await _userRepository.AddAsync(user); + + // 提交事务 + await _unitOfWork.CommitAsync(); + + return new RegisterResultDto( + user.UserId.Value.ToString(), + user.Account.Value.ToString(), + "注册成功" + ); + } + catch (Exception) + { + // 回滚事务 + await _unitOfWork.RollbackAsync(); + throw; + } + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/Common/Exceptions/BusinessException.cs b/backend/src/UniversalAdminSystem.Application/Common/Exceptions/BusinessException.cs new file mode 100644 index 0000000000000000000000000000000000000000..ef09cbe4c38b034895e71acddf6677b7ddce5ed3 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/Common/Exceptions/BusinessException.cs @@ -0,0 +1,43 @@ +namespace UniversalAdminSystem.Application.Common.Exceptions; + +/// +/// 业务异常基类 +/// +public class BusinessException : Exception +{ + public string ErrorCode { get; } + + public BusinessException(string message, string errorCode = "BUSINESS_ERROR") : base(message) + { + ErrorCode = errorCode; + } + + public BusinessException(string message, Exception innerException, string errorCode = "BUSINESS_ERROR") : base(message, innerException) + { + ErrorCode = errorCode; + } +} + +/// +/// 菜单相关业务异常 +/// +public class MenuBusinessException : BusinessException +{ + public MenuBusinessException(string message, string errorCode = "MENU_ERROR") : base(message, errorCode) { } +} + +/// +/// 未找到异常 +/// +public class NotFoundException : BusinessException +{ + public NotFoundException(string message) : base(message, "NOT_FOUND") { } +} + +/// +/// 验证异常 +/// +public class ValidationException : BusinessException +{ + public ValidationException(string message) : base(message, "VALIDATION_ERROR") { } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/Common/Results/Result.cs b/backend/src/UniversalAdminSystem.Application/Common/Results/Result.cs new file mode 100644 index 0000000000000000000000000000000000000000..b7251037e074bc227da196a8ba4158f990f853d9 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/Common/Results/Result.cs @@ -0,0 +1,54 @@ +namespace UniversalAdminSystem.Application.Common.Results; + +/// +/// 统一响应结果基类 +/// +public class Result +{ + public bool IsSuccess { get; } + public string Message { get; } + public string[] Errors { get; } + + protected Result(bool isSuccess, string message, string[]? errors = null) + { + IsSuccess = isSuccess; + Message = message; + Errors = errors ?? Array.Empty(); + } + + public static Result Success(string message = "操作成功") + { + return new Result(true, message); + } + + public static Result Failure(string message, string[]? errors = null) + { + return new Result(false, message, errors); + } +} + +/// +/// 带数据的响应结果 +/// +public class Result : Result +{ + public T? Data { get; } + + protected Result(bool isSuccess, string message, T? data, string[]? errors = null) + : base(isSuccess, message, errors) + { + Data = data; + } + + public static Result Success(T data, string message = "操作成功") + { + return new Result(true, message, data); + } + + public static new Result Failure(string message, string[]? errors = null) + { + return new Result(false, message, default, errors); + } + + public static implicit operator Result(T data) => Success(data); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/Common/interfaces/IEventBus.cs b/backend/src/UniversalAdminSystem.Application/Common/interfaces/IEventBus.cs new file mode 100644 index 0000000000000000000000000000000000000000..c8646a98b3260d796d23194de0671e8df1b3aaa7 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/Common/interfaces/IEventBus.cs @@ -0,0 +1,6 @@ +namespace UniversalAdminSystem.Application.Common.Interfaces; + +public interface IEventBus +{ + Task PublishAsync(string exchange, string routingKey, string payload, string? messageId = null, CancellationToken ct = default); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/Common/interfaces/IUnitOfWork.cs b/backend/src/UniversalAdminSystem.Application/Common/interfaces/IUnitOfWork.cs new file mode 100644 index 0000000000000000000000000000000000000000..c4f6fab73471c00a0d8c3a4d98067992f6014e93 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/Common/interfaces/IUnitOfWork.cs @@ -0,0 +1,9 @@ +namespace UniversalAdminSystem.Application.Common.Interfaces; + +public interface IUnitOfWork +{ + Task BeginTransactionAsync(); + Task CommitAsync(); + Task RollbackAsync(); + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/FileStorage/DTOs/FileDto.cs b/backend/src/UniversalAdminSystem.Application/FileStorage/DTOs/FileDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..d75714257f3df0e6eed479825c9cad0bf9f40829 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/FileStorage/DTOs/FileDto.cs @@ -0,0 +1,38 @@ +namespace UniversalAdminSystem.Application.FileStorage.DTOs; + +public record FileDto( + Guid Id, + string Name, + string Path, + long Size, + string Type, + Guid OwnerId, + DateTime UploadTime, + bool IsFolder, + Guid? ParentId, + string AccessLevel +); + +public record FileUploadDto( + string Name, + string Path, + long Size, + string Type, + Guid? ParentId = null +); + +public record FileDownloadDto( + Guid Id, + string Name, + string Path, + string Type +); + +public record FileUploadResultDto( + Guid? Id = null, + string? Name = null, + string? Path = null, + long? Size = null, + string? Type = null, + string? Message = null +); \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/FileStorage/DTOs/FileProcessingJobDto.cs b/backend/src/UniversalAdminSystem.Application/FileStorage/DTOs/FileProcessingJobDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..17815855ccb7b65ef89e39329fa7412e8c8af38c --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/FileStorage/DTOs/FileProcessingJobDto.cs @@ -0,0 +1,3 @@ +namespace UniversalAdminSystem.Application.FileStorage.DTOs; + +public record FileProcessingJobDto(Guid FileId, string FilePath, string ContentType, long Size); \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/FileStorage/Interfaces/IDocParser.cs b/backend/src/UniversalAdminSystem.Application/FileStorage/Interfaces/IDocParser.cs new file mode 100644 index 0000000000000000000000000000000000000000..dc2f7ddd699bc3c116f93e3a7f11d5b22a94109f --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/FileStorage/Interfaces/IDocParser.cs @@ -0,0 +1,7 @@ +namespace UniversalAdminSystem.Application.FileStorage.Interfaces; + +public interface IDocParser +{ + // bool CanParse(); + Task ParseAsync(string path); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/FileStorage/Interfaces/IFileAppService.cs b/backend/src/UniversalAdminSystem.Application/FileStorage/Interfaces/IFileAppService.cs new file mode 100644 index 0000000000000000000000000000000000000000..bb7ec7a70955bea8995410ad2f799c569ceb6ae2 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/FileStorage/Interfaces/IFileAppService.cs @@ -0,0 +1,24 @@ +using UniversalAdminSystem.Domian.FileStorage.ValueObjects; +using UniversalAdminSystem.Domian.UserManagement.ValueObj; +using UniversalAdminSystem.Application.FileStorage.DTOs; +using UniversalAdminSystem.Domian.Core.ValueObjects; +using UniversalAdminSystem.Application.Common.Results; +using Microsoft.AspNetCore.Http; + +namespace UniversalAdminSystem.Application.FileStorage.Interfaces; + +public interface IFileAppService +{ + Task UploadAsync(IFormFile file); + + Task> GetList(); + // Task> DownloadAsync(FileId fileId); + // Task>> ListAsync(FileId? parentId); + // Task> CreateFolderAsync(string name, FileId? parentId); + // Task DeleteAsync(FileId fileId); + // Task> GetFileByIdAsync(FileId fileId); + + Task GetFileById(Guid id); + + Task RemoveFile(Guid id); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/FileStorage/Interfaces/IFileProcessingQueue.cs b/backend/src/UniversalAdminSystem.Application/FileStorage/Interfaces/IFileProcessingQueue.cs new file mode 100644 index 0000000000000000000000000000000000000000..1c0b5a6f1fe3fb92b23e5378c6f370e9c32ed8e8 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/FileStorage/Interfaces/IFileProcessingQueue.cs @@ -0,0 +1,8 @@ +using UniversalAdminSystem.Application.FileStorage.DTOs; + +namespace UniversalAdminSystem.Application.FileStorage.Interfaces; + +public interface IFileProcessingQueue +{ + Task EnqueueAsync(FileProcessingJobDto jd, CancellationToken ct = default); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/FileStorage/Interfaces/IFileStorageService.cs b/backend/src/UniversalAdminSystem.Application/FileStorage/Interfaces/IFileStorageService.cs new file mode 100644 index 0000000000000000000000000000000000000000..5aa8613bae0e314d29badd670531574331820b4d --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/FileStorage/Interfaces/IFileStorageService.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Http; +using UniversalAdminSystem.Application.FileStorage.DTOs; + +namespace UniversalAdminSystem.Application.FileStorage.Interfaces; + +public interface IFileStorageService +{ + Task SaveAsync(IFormFile file); + Task DownloadAsync(string fileName); + Task DeleteAsync(string fileName); + Task> ListFilesAsync(); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/FileStorage/Interfaces/IFileValidationService.cs b/backend/src/UniversalAdminSystem.Application/FileStorage/Interfaces/IFileValidationService.cs new file mode 100644 index 0000000000000000000000000000000000000000..a871dba3bedfbf64f8f9f0f9365c6e4cd75a4dae --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/FileStorage/Interfaces/IFileValidationService.cs @@ -0,0 +1,6 @@ +namespace UniversalAdminSystem.Application.FileStorage.Interfaces; + +public interface IFileValidationService +{ + (bool isValid, string message, string? format) ValidateFile(string fileName, long fileSize, Stream stream); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/FileStorage/Services/FileAppService.cs b/backend/src/UniversalAdminSystem.Application/FileStorage/Services/FileAppService.cs new file mode 100644 index 0000000000000000000000000000000000000000..84d144aad614dd0d18a1c2169b4d14ff4c476583 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/FileStorage/Services/FileAppService.cs @@ -0,0 +1,122 @@ +using Microsoft.AspNetCore.Http; +using UniversalAdminSystem.Application.Common.Interfaces; +using UniversalAdminSystem.Application.FileStorage.DTOs; +using UniversalAdminSystem.Application.FileStorage.Interfaces; +using UniversalAdminSystem.Domian.Core.ValueObjects; +using UniversalAdminSystem.Domian.FileStorage.IRepository; +using UniversalAdminSystem.Domian.FileStorage.ValueObjects; +using File = UniversalAdminSystem.Domian.FileStorage.Aggregates.File; + +namespace UniversalAdminSystem.Application.FileStorage.Services; + +public class FileAppService : IFileAppService +{ + private readonly IFileRepository _fileRepository; + private readonly IFileValidationService _fileValidator; + private readonly IFileStorageService _localfileStorage; + private readonly IUnitOfWork _unitOfWork; + private readonly IFileProcessingQueue _fileProcessingQueue; + + + public FileAppService(IFileRepository fileRepository, IFileValidationService fileValidator, IFileStorageService localfileStorage, IUnitOfWork unitOfWork, IFileProcessingQueue fileProcessingQueue) + { + _fileRepository = fileRepository; + _fileValidator = fileValidator; + _localfileStorage = localfileStorage; + _unitOfWork = unitOfWork; + _fileProcessingQueue = fileProcessingQueue; + } + + public async Task UploadAsync(IFormFile file) + { + try + { + using var fileStream = file.OpenReadStream(); + var (isValid, message, format) = _fileValidator.ValidateFile(file.FileName, file.Length, fileStream); + if (!isValid) throw new Exception($"校验结果: {message}"); + var fileInfo = await _localfileStorage.SaveAsync(file); + Console.WriteLine("开启事务------------------"); + await _unitOfWork.BeginTransactionAsync(); + var fileEntity = File.Create + ( + FileName.Create(fileInfo.Name), + FilePath.Create(fileInfo.FullName), + FileSize.Create(file.Length), + FileType.Create(file.ContentType), + (UserId)Guid.NewGuid(), + false, + null + ); + Console.WriteLine($"文件信息打印---------------"); + Console.WriteLine($"文件名:{fileEntity.Name}"); + Console.WriteLine($"文件url:{fileEntity.Path}"); + Console.WriteLine($"文件大小:{fileEntity.Size}"); + Console.WriteLine($"文件类型:{fileEntity.Type}"); + Console.WriteLine("---------------------------"); + await _fileRepository.AddAsync(fileEntity); + Console.WriteLine("添加数据成功!"); + + // 文件处理任务入队 + await _fileProcessingQueue.EnqueueAsync(new FileProcessingJobDto( + fileEntity.Id.Value, + fileInfo.FullName, + file.ContentType, + file.Length), CancellationToken.None); + Console.WriteLine("文件处理任务入队成功!提交事务!"); + await _unitOfWork.CommitAsync(); + Console.WriteLine("提交事务成功!"); + + return new FileUploadResultDto + ( + fileEntity.Id.Value, + fileInfo.Name, + fileInfo.FullName, + file.Length, + file.ContentType, + message + ); + } + catch (Exception ex) + { + Console.WriteLine("添加数据到数据库失败!回滚事务!"); + await _unitOfWork.RollbackAsync(); + return new FileUploadResultDto(Message: ex.Message); + } + } + + public async Task> GetList() + { + var files = await _fileRepository.GetAllAsync(); + return files.Select(file => new FileUploadResultDto( + file.Id, + file.Name, + file.Path, + file.Size, + file.Type + )); + } + + public async Task GetFileById(Guid id) + { + var file = await _fileRepository.GetByGuidAsync(id); + if (file == null) + { + throw new FileNotFoundException(); + } + return new FileDownloadDto(file.Id, file.Name, file.Path, file.Type); + } + + public async Task RemoveFile(Guid id) + { + try + { + await _fileRepository.RemoveAsync(id); + } + catch (System.Exception) + { + + throw new Exception("删除异常"); + } + + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/LogManagement/DTOs/LogEntryDto.cs b/backend/src/UniversalAdminSystem.Application/LogManagement/DTOs/LogEntryDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..0883fcde3347213813b5e6c6d46d46ccb63f5b5f --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/LogManagement/DTOs/LogEntryDto.cs @@ -0,0 +1,21 @@ +namespace UniversalAdminSystem.Application.LogManagement.DTOs; + +public record LogEntryDto( + Guid Id, + string Level, + string Message, + string Source, + Guid? UserId, + DateTime Timestamp, + string? Context, + string? Exception +); + +public record LogCreateDto( + string Level, + string Message, + string Source, + Guid? UserId = null, + string? Context = null, + string? Exception = null +); \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/LogManagement/Interfaces/ILogManagementAppService.cs b/backend/src/UniversalAdminSystem.Application/LogManagement/Interfaces/ILogManagementAppService.cs new file mode 100644 index 0000000000000000000000000000000000000000..61b7d96c6977504a2c1e3c8d119f2ba467439e37 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/LogManagement/Interfaces/ILogManagementAppService.cs @@ -0,0 +1,52 @@ +using UniversalAdminSystem.Application.LogManagement.DTOs; +using UniversalAdminSystem.Application.Common.Results; + +namespace UniversalAdminSystem.Application.LogManagement.Interfaces; + +public interface ILogManagementAppService +{ + /// + /// 获取所有日志 + /// + Task>> GetAllLogsAsync(); + + /// + /// 根据ID获取日志 + /// + Task> GetLogByIdAsync(Guid id); + + /// + /// 创建日志 + /// + Task> CreateLogAsync(LogCreateDto createDto); + + /// + /// 根据级别获取日志 + /// + Task>> GetLogsByLevelAsync(string level); + + /// + /// 根据用户获取日志 + /// + Task>> GetLogsByUserAsync(Guid userId); + + /// + /// 根据日期范围获取日志 + /// + Task>> GetLogsByDateRangeAsync(DateTime start, DateTime end); + + /// + /// 根据来源获取日志 + /// + Task>> GetLogsBySourceAsync(string source); + + /// + /// 删除日志 + /// + Task DeleteLogAsync(Guid id); + + /// + /// 清空所有日志 + /// + Task ClearLogsAsync(); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/LogManagement/Services/LogManagementAppService.cs b/backend/src/UniversalAdminSystem.Application/LogManagement/Services/LogManagementAppService.cs new file mode 100644 index 0000000000000000000000000000000000000000..0b812233c4a7aa3ef598f3e8360d97251cc612a8 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/LogManagement/Services/LogManagementAppService.cs @@ -0,0 +1,182 @@ +using UniversalAdminSystem.Application.LogManagement.DTOs; +using UniversalAdminSystem.Application.LogManagement.Interfaces; +using UniversalAdminSystem.Domian.LogManagement.Aggregates; +using UniversalAdminSystem.Domian.Core.Interfaces; +using UniversalAdminSystem.Application.Common.Results; +using UniversalAdminSystem.Domian.Core.ValueObjects; +using UniversalAdminSystem.Application.Common.Interfaces; + +namespace UniversalAdminSystem.Application.LogManagement.Services; + +public class LogManagementAppService : ILogManagementAppService +{ + private readonly IRepository _logRepository; + private readonly IUnitOfWork _unitOfWork; + + public LogManagementAppService( + IRepository logRepository, + IUnitOfWork unitOfWork) + { + _logRepository = logRepository; + _unitOfWork = unitOfWork; + } + + public async Task>> GetAllLogsAsync() + { + try + { + var logs = await _logRepository.GetAllAsync(); + var logDtos = logs.Select(MapToDto); + return Result>.Success(logDtos); + } + catch (Exception ex) + { + return Result>.Failure($"获取日志列表失败: {ex.Message}"); + } + } + + public async Task> GetLogByIdAsync(Guid id) + { + try + { + var log = await _logRepository.GetByGuidAsync(id); + if (log == null) + { + return Result.Success(null); + } + + return Result.Success(MapToDto(log)); + } + catch (Exception ex) + { + return Result.Failure($"获取日志详情失败: {ex.Message}"); + } + } + + public async Task> CreateLogAsync(LogCreateDto createDto) + { + try + { + var log = LogEntry.Create( + createDto.Level, + createDto.Message, + createDto.Source, + createDto.UserId.HasValue ? (UserId)createDto.UserId.Value : null, + createDto.Context, + createDto.Exception + ); + + await _logRepository.AddAsync(log); + await _unitOfWork.SaveChangesAsync(); + return Result.Success(MapToDto(log)); + } + catch (Exception ex) + { + return Result.Failure($"创建日志失败: {ex.Message}"); + } + } + + public async Task>> GetLogsByLevelAsync(string level) + { + try + { + var logs = await _logRepository.GetAllAsync(); + var filteredLogs = logs.Where(l => l.Level == level).Select(MapToDto); + return Result>.Success(filteredLogs); + } + catch (Exception ex) + { + return Result>.Failure($"获取日志失败: {ex.Message}"); + } + } + + public async Task>> GetLogsByUserAsync(Guid userId) + { + try + { + var logs = await _logRepository.GetAllAsync(); + var userLogs = logs.Where(l => l.UserId != null && l.UserId.Value == userId); + + var logDtos = userLogs.Select(MapToDto); + return Result>.Success(logDtos); + } + catch (Exception ex) + { + return Result>.Failure($"获取用户日志失败: {ex.Message}"); + } + } + + public async Task>> GetLogsByDateRangeAsync(DateTime start, DateTime end) + { + try + { + var logs = await _logRepository.GetAllAsync(); + var dateRangeLogs = logs.Where(l => l.Timestamp >= start && l.Timestamp <= end); + + var logDtos = dateRangeLogs.Select(MapToDto); + return Result>.Success(logDtos); + } + catch (Exception ex) + { + return Result>.Failure($"获取日志失败: {ex.Message}"); + } + } + + public async Task>> GetLogsBySourceAsync(string source) + { + try + { + var logs = await _logRepository.GetAllAsync(); + var sourceLogs = logs.Where(l => l.Source == source); + + var logDtos = sourceLogs.Select(MapToDto); + return Result>.Success(logDtos); + } + catch (Exception ex) + { + return Result>.Failure($"获取日志失败: {ex.Message}"); + } + } + + public async Task DeleteLogAsync(Guid id) + { + try + { + var log = await _logRepository.GetByGuidAsync(id); + if (log == null) + { + return Result.Failure("日志不存在"); + } + + await _logRepository.RemoveAsync(id); + await _unitOfWork.SaveChangesAsync(); + return Result.Success("日志删除成功"); + } + catch (Exception ex) + { + return Result.Failure($"删除日志失败: {ex.Message}"); + } + } + + public async Task ClearLogsAsync() + { + try + { + var logs = await _logRepository.GetAllAsync(); + foreach (var log in logs) + { + await _logRepository.RemoveAsync(log.Id); + } + await _unitOfWork.SaveChangesAsync(); + return Result.Success("日志清空成功"); + } + catch (Exception ex) + { + return Result.Failure($"清空日志失败: {ex.Message}"); + } + } + + private static LogEntryDto MapToDto(LogEntry l) => new( + l.Id, l.Level, l.Message, l.Source, l.UserId?.Value, l.Timestamp, l.Context, l.Exception + ); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/PermissionManagement/DTOs/AssignPermissionDto.cs b/backend/src/UniversalAdminSystem.Application/PermissionManagement/DTOs/AssignPermissionDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..a87563835e77fe7bc57706bb6dfaccaa6a9d12b0 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/PermissionManagement/DTOs/AssignPermissionDto.cs @@ -0,0 +1,11 @@ +namespace UniversalAdminSystem.Application.PermissionManagement.DTOs; + +/// +/// 权限分配数据传输对象 +/// +/// 权限编码 +/// 角色ID +public record AssignPermissionDto( + string PermissionCode, + Guid RoleId +); \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/PermissionManagement/DTOs/PermissionCreateDto.cs b/backend/src/UniversalAdminSystem.Application/PermissionManagement/DTOs/PermissionCreateDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..79124bac8dffbc3551c12f2941c5e3c95a70b74e --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/PermissionManagement/DTOs/PermissionCreateDto.cs @@ -0,0 +1,15 @@ +namespace UniversalAdminSystem.Application.PermissionManagement.DTOs; + +/// +/// 权限创建数据传输对象 +/// +/// 权限名称 +/// 权限作用资源 +/// 权限操作类型值 +/// 权限描述 +public record PermissionCreateDto( + string Name, + string Resource, + int ActionValue, + string? Description = null +); \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/PermissionManagement/DTOs/PermissionDto.cs b/backend/src/UniversalAdminSystem.Application/PermissionManagement/DTOs/PermissionDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..3c8cf10ab48d7355557ffbe921efe4a8887beb96 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/PermissionManagement/DTOs/PermissionDto.cs @@ -0,0 +1,13 @@ +namespace UniversalAdminSystem.Application.PermissionManagement.DTOs; + +/// +/// 权限数据传输对象 +/// +/// 权限名称 +/// 权限编码 +/// 权限描述 +public record PermissionDto( + string Name, + string Code, + string Description +); \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/PermissionManagement/DTOs/RoleDto.cs b/backend/src/UniversalAdminSystem.Application/PermissionManagement/DTOs/RoleDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..a72356d6c5a214810477e519bbb41a8dd1abd819 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/PermissionManagement/DTOs/RoleDto.cs @@ -0,0 +1,46 @@ +namespace UniversalAdminSystem.Application.PermissionManagement.DTOs; + +/// +/// 角色数据传输对象 +/// +/// 角色ID +/// 角色名称 +/// 角色描述 +/// 是否为系统角色 +/// 是否为超级管理员 +/// 创建时间 +/// 更新时间 +/// 权限数量 +public record RoleDto( + Guid RoleId, + string Name, + string? Description, + bool IsSystem, + bool IsSupper, + DateTime CreateTime, + DateTime UpdateTime, + int PermissionCount = 0 +); + +/// +/// 角色创建数据传输对象 +/// +/// 角色名称 +/// 角色描述 +/// 是否为系统角色 +/// 是否为超级管理员 +public record RoleCreateDto( + string Name, + string? Description = null, + bool IsSupper = false +); + +/// +/// 角色更新数据传输对象 +/// +/// 角色名称 +/// 角色描述 +public record RoleUpdateDto( + string Name, + string? Description = null +); \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/PermissionManagement/Interfaces/IPermissionCheckService.cs b/backend/src/UniversalAdminSystem.Application/PermissionManagement/Interfaces/IPermissionCheckService.cs new file mode 100644 index 0000000000000000000000000000000000000000..f355206c53746e1b688b828b748afcbabca8a972 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/PermissionManagement/Interfaces/IPermissionCheckService.cs @@ -0,0 +1,46 @@ +namespace UniversalAdminSystem.Application.PermissionManagement.Interfaces; + +/// +/// 权限检查服务接口 +/// 提供用户权限验证功能 +/// +public interface IPermissionCheckService +{ + /// + /// 检查用户是否有指定权限 + /// + /// 用户ID + /// 权限编码 + /// 是否有权限 + Task CheckUserPermissionAsync(Guid userId, string permissionCode); + + /// + /// 检查用户是否有任意一个指定权限 + /// + /// 用户ID + /// 权限编码列表 + /// 是否有权限 + Task CheckUserAnyPermissionAsync(Guid userId, IEnumerable permissionCodes); + + /// + /// 检查用户是否有所有指定权限 + /// + /// 用户ID + /// 权限编码列表 + /// 是否有权限 + Task CheckUserAllPermissionsAsync(Guid userId, IEnumerable permissionCodes); + + /// + /// 获取用户的所有权限 + /// + /// 用户ID + /// 权限编码列表 + Task> GetUserPermissionsAsync(Guid userId); + + /// + /// 获取用户的所有角色 + /// + /// 用户ID + /// 角色名称列表 + Task> GetUserRoleAsync(Guid userId); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/PermissionManagement/Interfaces/IPermissionManagementAppService.cs b/backend/src/UniversalAdminSystem.Application/PermissionManagement/Interfaces/IPermissionManagementAppService.cs new file mode 100644 index 0000000000000000000000000000000000000000..9d3caa5d7c746771ffb9a91ac550da23a332ddab --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/PermissionManagement/Interfaces/IPermissionManagementAppService.cs @@ -0,0 +1,37 @@ +using UniversalAdminSystem.Application.PermissionManagement.DTOs; + +namespace UniversalAdminSystem.Application.PermissionManagement.Interfaces; + +/// +/// 权限管理应用服务接口 +/// 提供权限的创建、查询、分配等业务操作 +/// +public interface IPermissionManagementAppService +{ + /// + /// 获取所有权限列表 + /// + /// 权限列表 + Task> GetAllPermissionAsync(); + + /// + /// 创建新权限 + /// + /// 权限创建数据传输对象 + /// 创建的权限信息 + Task CreatePermissionAsync(PermissionCreateDto createDto); + + /// + /// 删除权限 + /// + /// 权限ID + /// 删除操作结果 + Task RemovePermissionAsync(Guid permissionId); + + /// + /// 为角色分配权限 + /// + /// 权限分配数据传输对象 + /// 分配操作结果 + Task AssignPermissionToRoleAsync(AssignPermissionDto assignDto); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/PermissionManagement/Interfaces/IRoleManagementAppService.cs b/backend/src/UniversalAdminSystem.Application/PermissionManagement/Interfaces/IRoleManagementAppService.cs new file mode 100644 index 0000000000000000000000000000000000000000..6667c1ea8941566d2a1da90921a46867459eafc7 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/PermissionManagement/Interfaces/IRoleManagementAppService.cs @@ -0,0 +1,68 @@ +using UniversalAdminSystem.Application.PermissionManagement.DTOs; + +namespace UniversalAdminSystem.Application.PermissionManagement.Interfaces; + +/// +/// 角色管理应用服务接口 +/// 提供角色的创建、查询、更新、删除等业务操作 +/// +public interface IRoleManagementAppService +{ + /// + /// 获取所有角色列表 + /// + /// 角色列表 + Task> GetAllRolesAsync(); + + /// + /// 根据ID获取角色 + /// + /// 角色ID + /// 角色信息 + Task GetRoleByIdAsync(Guid roleId); + + /// + /// 创建新角色 + /// + /// 角色创建数据传输对象 + /// 创建的角色信息 + Task CreateRoleAsync(RoleCreateDto createDto); + + /// + /// 更新角色信息 + /// + /// 角色ID + /// 角色更新数据传输对象 + /// 更新后的角色信息 + Task UpdateRoleAsync(Guid roleId, RoleUpdateDto updateDto); + + /// + /// 删除角色 + /// + /// 角色ID + /// 删除操作结果 + Task DeleteRoleAsync(Guid roleId); + + /// + /// 为角色分配权限 + /// + /// 角色ID + /// 权限ID列表 + /// 分配操作结果 + Task AssignPermissionsToRoleAsync(Guid roleId, IEnumerable permissionIds); + + /// + /// 移除角色的权限 + /// + /// 角色ID + /// 权限ID列表 + /// 移除操作结果 + Task RemovePermissionsFromRoleAsync(Guid roleId, IEnumerable permissionIds); + + /// + /// 获取角色的所有权限 + /// + /// 角色ID + /// 权限列表 + Task> GetRolePermissionsAsync(Guid roleId); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/PermissionManagement/Services/PermissionCheckService.cs b/backend/src/UniversalAdminSystem.Application/PermissionManagement/Services/PermissionCheckService.cs new file mode 100644 index 0000000000000000000000000000000000000000..022244fe3c712f5d0595abb0f8f9115284471078 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/PermissionManagement/Services/PermissionCheckService.cs @@ -0,0 +1,349 @@ +using UniversalAdminSystem.Application.PermissionManagement.Interfaces; +using UniversalAdminSystem.Domian.PermissionManagement.IRepository; +using UniversalAdminSystem.Domian.UserManagement.IRepository; +using UniversalAdminSystem.Domian.PermissionManagement.ValueObjects; + +namespace UniversalAdminSystem.Application.PermissionManagement.Services; + +/// +/// 权限检查服务实现 +/// 提供用户权限验证的具体逻辑 +/// +public class PermissionCheckService : IPermissionCheckService +{ + private readonly IUserRepository _userRepository; + private readonly IRoleRepository _roleRepository; + private readonly IPermissionRepository _permissionRepository; + + public PermissionCheckService( + IUserRepository userRepository, + IRoleRepository roleRepository, + IPermissionRepository permissionRepository) + { + _userRepository = userRepository; + _roleRepository = roleRepository; + _permissionRepository = permissionRepository; + } + + public async Task CheckUserPermissionAsync(Guid userId, string permissionCode) + { + try + { + var user = await _userRepository.GetByGuidAsync(userId); + if (user == null || user.RoleId == null) return false; + var role = await _roleRepository.GetByGuidAsync(user.RoleId.Value); + if (role == null) return false; + var permission = await _permissionRepository.GetByCodeAsync(PermissionCode.Create(permissionCode)); + if (permission == null) return false; + + Console.WriteLine($"检查权限: {permissionCode}"); + Console.WriteLine($"角色权限数量: {role.Permissions.Count}"); + + // 打印角色拥有的所有权限 + foreach (var perm in role.Permissions) + { + Console.WriteLine($"角色权限: {perm.Code} (类型: {perm.Code.GetType().Name})"); + } + + // 修复比较逻辑:使用值对象比较 + var hasPermission = role.Permissions.Any(p => p.Code == PermissionCode.Create(permissionCode)); + Console.WriteLine($"权限检查结果: {hasPermission}"); + + return hasPermission; + } + catch (Exception ex) + { + Console.WriteLine($"权限检查错误: {ex.Message}"); + return false; + } + } + + /// + /// 检查用户是否有任意一个指定权限 + /// + public async Task CheckUserAnyPermissionAsync(Guid userId, IEnumerable permissionCodes) + { + foreach (var permissionCode in permissionCodes) + { + if (await CheckUserPermissionAsync(userId, permissionCode)) return true; + } + return false; + } + + /// + /// 检查用户是否有所有指定权限 + /// + public async Task CheckUserAllPermissionsAsync(Guid userId, IEnumerable permissionCodes) + { + foreach (var permissionCode in permissionCodes) + { + if (!await CheckUserPermissionAsync(userId, permissionCode)) return false; + } + return true; + } + + /// + /// 获取用户的所有权限编码 + /// + public async Task> GetUserPermissionsAsync(Guid userId) + { + try + { + if (userId == Guid.Empty) throw new ArgumentException("用户ID为空", nameof(userId)); + var user = await _userRepository.GetByGuidAsync(userId) ?? throw new InvalidOperationException("用户不存在"); + if (user.RoleId == null) return Enumerable.Empty(); + var role = await _roleRepository.GetByGuidAsync(user.RoleId.Value) ?? throw new InvalidDataException("用户角色不存在"); + return role.Permissions.Select(p => p.Code.Value); + } + catch (Exception ex) + { + Console.WriteLine($"获取用户权限错误: {ex.Message}"); + return Enumerable.Empty(); + } + } + + /// + /// 获取用户的所有角色 + /// + public async Task> GetUserRoleAsync(Guid userId) + { + try + { + var user = await _userRepository.GetByGuidAsync(userId); + if (user == null) return Enumerable.Empty(); + if (user.RoleId == null) return Enumerable.Empty(); + var role = await _roleRepository.GetByGuidAsync(user.RoleId.Value); + if (role == null) return Enumerable.Empty(); + return new[] { role.Name.Value }; + } + catch (Exception ex) + { + Console.WriteLine($"获取用户角色错误: {ex.Message}"); + return Enumerable.Empty(); + } + } + + /// + /// 检查用户是否有菜单访问权限 + /// + public async Task CheckMenuPermissionAsync(Guid userId, string? menuPermissionCode) + { + // 如果菜单没有设置权限要求,则所有用户都可以访问 + if (string.IsNullOrEmpty(menuPermissionCode)) + { + return true; + } + + return await CheckUserPermissionAsync(userId, menuPermissionCode); + } + + /// + /// 检查用户是否有文件访问权限 + /// + public async Task CheckFilePermissionAsync(Guid userId, string fileAccessLevel, Guid? fileOwnerId = null) + { + try + { + // 超级管理员可以访问所有文件 + if (await IsSuperAdminAsync(userId)) + { + return true; + } + + // 管理员可以访问除超级管理员文件外的所有文件 + if (await IsAdminAsync(userId)) + { + if (fileOwnerId.HasValue) + { + var fileOwner = await _userRepository.GetByGuidAsync(fileOwnerId.Value); + return fileOwner == null || !await IsSuperAdminAsync(fileOwnerId.Value); + } + return true; + } + + // 普通用户只能访问自己的文件或公开文件 + switch (fileAccessLevel.ToLower()) + { + case "public": + return true; + case "private": + return fileOwnerId.HasValue && fileOwnerId.Value == userId; + case "restricted": + return false; + default: + return false; + } + } + catch (Exception ex) + { + Console.WriteLine($"文件权限检查错误: {ex.Message}"); + return false; + } + } + + /// + /// 检查用户是否有日志查看权限 + /// + public async Task CheckLogPermissionAsync(Guid userId, string logLevel) + { + try + { + // 超级管理员可以查看所有日志 + if (await IsSuperAdminAsync(userId)) + { + return true; + } + + // 管理员可以查看除系统级日志外的所有日志 + if (await IsAdminAsync(userId)) + { + return logLevel.ToLower() != "system"; + } + + // 普通用户只能查看基本日志 + return logLevel.ToLower() == "basic" || logLevel.ToLower() == "info"; + } + catch (Exception ex) + { + Console.WriteLine($"日志权限检查错误: {ex.Message}"); + return false; + } + } + + /// + /// 检查用户是否有系统设置管理权限 + /// + public async Task CheckSystemSettingPermissionAsync(Guid userId, string settingKey) + { + try + { + // 超级管理员可以管理所有设置 + if (await IsSuperAdminAsync(userId)) + { + return true; + } + + // 管理员可以管理除系统核心设置外的所有设置 + if (await IsAdminAsync(userId)) + { + var restrictedSettings = new[] { "system.core", "security", "database" }; + return !restrictedSettings.Any(s => settingKey.StartsWith(s)); + } + + // 普通用户只能查看基本设置 + var basicSettings = new[] { "ui", "theme", "language" }; + return basicSettings.Any(s => settingKey.StartsWith(s)); + } + catch (Exception ex) + { + Console.WriteLine($"系统设置权限检查错误: {ex.Message}"); + return false; + } + } + + + /// + /// 检查用户是否为超级管理员 + /// + public async Task IsSuperAdminAsync(Guid userId) + { + try + { + var permissions = await GetUserPermissionsAsync(userId); + return permissions.Any(p => p.Contains("super") || p.Contains("system")); + } + catch (Exception ex) + { + Console.WriteLine($"超级管理员检查错误: {ex.Message}"); + return false; + } + } + + /// + /// 检查用户是否为管理员 + /// + public async Task IsAdminAsync(Guid userId) + { + try + { + var user = await _userRepository.GetByGuidAsync(userId); + if (user == null || user.RoleId == null) return false; + var role = await _roleRepository.GetByGuidAsync(user.RoleId.Value); + if (role == null) return false; + return role.IsSupper; + } + catch (Exception ex) + { + Console.WriteLine($"管理员检查错误: {ex.Message}"); + return false; + } + } + + /// + /// 获取用户可访问的菜单权限列表 + /// + public async Task> GetUserMenuPermissionsAsync(Guid userId) + { + try + { + var allPermissions = await GetUserPermissionsAsync(userId); + return allPermissions.Where(p => p.StartsWith("menu:")); + } + catch (Exception ex) + { + Console.WriteLine($"获取菜单权限错误: {ex.Message}"); + return Enumerable.Empty(); + } + } + + /// + /// 获取用户可访问的文件权限列表 + /// + public async Task> GetUserFilePermissionsAsync(Guid userId) + { + try + { + var allPermissions = await GetUserPermissionsAsync(userId); + return allPermissions.Where(p => p.StartsWith("file:")); + } + catch (Exception ex) + { + Console.WriteLine($"获取文件权限错误: {ex.Message}"); + return Enumerable.Empty(); + } + } + + /// + /// 获取用户可访问的日志权限列表 + /// + public async Task> GetUserLogPermissionsAsync(Guid userId) + { + try + { + var allPermissions = await GetUserPermissionsAsync(userId); + return allPermissions.Where(p => p.StartsWith("log:")); + } + catch (Exception ex) + { + Console.WriteLine($"获取日志权限错误: {ex.Message}"); + return Enumerable.Empty(); + } + } + + /// + /// 获取用户可访问的系统设置权限列表 + /// + public async Task> GetUserSystemSettingPermissionsAsync(Guid userId) + { + try + { + var allPermissions = await GetUserPermissionsAsync(userId); + return allPermissions.Where(p => p.StartsWith("setting:")); + } + catch (Exception ex) + { + Console.WriteLine($"获取系统设置权限错误: {ex.Message}"); + return Enumerable.Empty(); + } + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/PermissionManagement/Services/PermissionManagementAppService.cs b/backend/src/UniversalAdminSystem.Application/PermissionManagement/Services/PermissionManagementAppService.cs new file mode 100644 index 0000000000000000000000000000000000000000..f6b7ee28573e34f8ebad4d7136d39fe4a8b82ff3 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/PermissionManagement/Services/PermissionManagementAppService.cs @@ -0,0 +1,141 @@ +using UniversalAdminSystem.Application.Common.Interfaces; +using UniversalAdminSystem.Application.PermissionManagement.DTOs; +using UniversalAdminSystem.Application.PermissionManagement.Interfaces; +using UniversalAdminSystem.Domian.PermissionManagement.Aggregate; +using UniversalAdminSystem.Domian.PermissionManagement.Interfaces; +using UniversalAdminSystem.Domian.PermissionManagement.IRepository; +using UniversalAdminSystem.Domian.PermissionManagement.ValueObjects; + +namespace UniversalAdminSystem.Application.PermissionManagement.Services; + +/// +/// 权限管理应用服务实现 +/// 提供权限的创建、查询、分配等业务操作的具体实现 +/// +public class PermissionManagementAppService : IPermissionManagementAppService +{ + private readonly IUnitOfWork _unitOfWork; + private readonly IPermissionRepository _permissionRepository; + private readonly IRoleRepository _roleRepository; + private readonly IAssignPermissionDomainService _assignPermissionDomainService; + + public PermissionManagementAppService( + IUnitOfWork unitOfWork, + IPermissionRepository permissionRepository, + IRoleRepository roleRepository, + IAssignPermissionDomainService assignPermissionDomainService + ) + { + _unitOfWork = unitOfWork; + _permissionRepository = permissionRepository; + _roleRepository = roleRepository; + _assignPermissionDomainService = assignPermissionDomainService; + } + + /// + /// 为角色分配权限 + /// + public async Task AssignPermissionToRoleAsync(AssignPermissionDto assignDto) + { + try + { + await _unitOfWork.BeginTransactionAsync(); + var permission = await _permissionRepository.GetByCodeAsync((PermissionCode)assignDto.PermissionCode) + ?? throw new KeyNotFoundException("未找到对应的权限"); + var role = await _roleRepository.GetByGuidAsync(assignDto.RoleId) + ?? throw new KeyNotFoundException("未找到对应的角色"); + var state = _assignPermissionDomainService.AssignPermission(permission, role); + if (state) + { + await _roleRepository.Update(role); + + } + await _unitOfWork.CommitAsync(); + } + catch (KeyNotFoundException) + { + await _unitOfWork.RollbackAsync(); + throw; + } + } + + /// + /// 创建新权限 + /// + public async Task CreatePermissionAsync(PermissionCreateDto createDto) + { + try + { + if (!Enum.IsDefined(typeof(PermissionAction), createDto.ActionValue)) + { + throw new InvalidOperationException($"无效的操作类型值: {createDto.ActionValue}"); + } + PermissionAction enumValue = (PermissionAction)createDto.ActionValue; + // 检查权限代码是否已存在 + var existingPermission = await _permissionRepository.GetByCodeAsync(PermissionCode.Create($"{createDto.Resource}:{enumValue}")); + if (existingPermission != null) + { + throw new InvalidOperationException($"权限代码 '{createDto.Resource}:{enumValue}' 已存在,无法创建重复权限"); + } + + // 开启事务 + await _unitOfWork.BeginTransactionAsync(); + + // 创建权限并保存 + var permission = Permission.CreateStandardPermission( + createDto.Name, + createDto.Resource, + createDto.ActionValue, + createDto.Description + ); + + try + { + var savedPermission = await _permissionRepository.AddAsync(permission); + await _unitOfWork.CommitAsync(); + return new PermissionDto( + savedPermission.Name, + savedPermission.Code, + savedPermission.Description! + ); + } + catch (System.Exception ex) + { + throw new InvalidOperationException(ex.Message); + } + } + catch (System.Exception ex) + { + await _unitOfWork.RollbackAsync(); + throw new InvalidOperationException(ex.Message); + } + } + + /// + /// 获取所有权限列表 + /// + public async Task> GetAllPermissionAsync() + { + var permissions = await _permissionRepository.GetAllAsync(); + var permissionList = permissions.Select(p => new PermissionDto(p.Name, p.Code, p.Description!)); + return permissionList; + } + + /// + /// 删除权限 + /// + public async Task RemovePermissionAsync(Guid permissionId) + { + try + { + await _unitOfWork.BeginTransactionAsync(); + await _permissionRepository.RemoveAsync(permissionId); + await _unitOfWork.CommitAsync(); + } + catch (System.Exception ex) + { + await _unitOfWork.RollbackAsync(); + throw new System.Exception(ex.Message); + } + } +} diff --git a/backend/src/UniversalAdminSystem.Application/PermissionManagement/Services/RoleManagementAppService.cs b/backend/src/UniversalAdminSystem.Application/PermissionManagement/Services/RoleManagementAppService.cs new file mode 100644 index 0000000000000000000000000000000000000000..2603384f7e9a1e9f70a2fa1a66cbc3fa7110161a --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/PermissionManagement/Services/RoleManagementAppService.cs @@ -0,0 +1,301 @@ +using UniversalAdminSystem.Application.Common.Interfaces; +using UniversalAdminSystem.Application.PermissionManagement.DTOs; +using UniversalAdminSystem.Application.PermissionManagement.Interfaces; +using UniversalAdminSystem.Domian.PermissionManagement.Aggregate; +using UniversalAdminSystem.Domian.PermissionManagement.IRepository; +using UniversalAdminSystem.Domian.PermissionManagement.ValueObjects; + +namespace UniversalAdminSystem.Application.PermissionManagement.Services; + +/// +/// 角色管理应用服务实现 +/// 提供角色的创建、查询、更新、删除等业务操作的具体实现 +/// +public class RoleManagementAppService : IRoleManagementAppService +{ + private readonly IUnitOfWork _unitOfWork; + private readonly IRoleRepository _roleRepository; + private readonly IPermissionRepository _permissionRepository; + + public RoleManagementAppService( + IUnitOfWork unitOfWork, + IRoleRepository roleRepository, + IPermissionRepository permissionRepository) + { + _unitOfWork = unitOfWork; + _roleRepository = roleRepository; + _permissionRepository = permissionRepository; + } + + /// + /// 获取所有角色列表 + /// + public async Task> GetAllRolesAsync() + { + try + { + var roles = await _roleRepository.GetAllRolesWithPermissionsAsync(); + return roles.Select(r => new RoleDto( + r.RoleId, + r.Name, + r.Description ?? string.Empty, + r.IsSystem, + r.IsSupper, + r.CreateTime, + r.UpdateTime, + r.PermissionCount + )); + } + catch (Exception ex) + { + throw new InvalidOperationException($"获取角色列表失败: {ex.Message}"); + } + } + + /// + /// 根据ID获取角色 + /// + public async Task GetRoleByIdAsync(Guid roleId) + { + try + { + var role = await _roleRepository.GetRoleWithPermissionsAsync(roleId); + if (role == null) return null; + + return new RoleDto( + role.RoleId, + role.Name, + role.Description ?? string.Empty, + role.IsSystem, + role.IsSupper, + role.CreateTime, + role.UpdateTime, + role.PermissionCount + ); + } + catch (Exception ex) + { + throw new InvalidOperationException($"获取角色失败: {ex.Message}"); + } + } + + /// + /// 创建新角色 + /// + public async Task CreateRoleAsync(RoleCreateDto createDto) + { + try + { + await _unitOfWork.BeginTransactionAsync(); + + // 检查角色名称是否已存在 + if (await _roleRepository.ExistsAsync(createDto.Name)) + { + throw new InvalidOperationException($"角色名称 '{createDto.Name}' 已存在"); + } + + // 创建角色 + var role = Role.Create( + createDto.Name, + createDto.Description, + false, + createDto.IsSupper + ); + + var savedRole = await _roleRepository.AddAsync(role); + await _unitOfWork.CommitAsync(); + + return new RoleDto( + savedRole.RoleId, + savedRole.Name, + savedRole.Description, + savedRole.IsSystem, + savedRole.IsSupper, + savedRole.CreateTime, + savedRole.UpdateTime + ); + } + catch (Exception ex) + { + await _unitOfWork.RollbackAsync(); + throw new InvalidOperationException($"创建角色失败: {ex.Message}"); + } + } + + /// + /// 更新角色信息 + /// + public async Task UpdateRoleAsync(Guid roleId, RoleUpdateDto updateDto) + { + try + { + await _unitOfWork.BeginTransactionAsync(); + + var role = await _roleRepository.GetByGuidAsync(roleId); + if (role == null) + { + throw new KeyNotFoundException($"角色ID '{roleId}' 不存在"); + } + + // 检查是否为系统角色 + if (role.IsSystem) + { + throw new InvalidOperationException("系统角色不允许修改"); + } + + // 检查角色名称是否已存在(排除当前角色) + var existingRole = await _roleRepository.GetByNameAsync(updateDto.Name); + if (existingRole != null && existingRole.RoleId != roleId) + { + throw new InvalidOperationException($"角色名称 '{updateDto.Name}' 已存在"); + } + + // 更新角色信息 + role.SetName(updateDto.Name); + role.SetDescription(updateDto.Description); + + await _roleRepository.Update(role); + await _unitOfWork.CommitAsync(); + + return new RoleDto( + role.RoleId, + role.Name, + role.Description, + role.IsSystem, + role.IsSupper, + role.CreateTime, + role.UpdateTime + ); + } + catch (Exception ex) + { + await _unitOfWork.RollbackAsync(); + throw new InvalidOperationException($"更新角色失败: {ex.Message}"); + } + } + + /// + /// 删除角色 + /// + public async Task DeleteRoleAsync(Guid roleId) + { + try + { + await _unitOfWork.BeginTransactionAsync(); + + var role = await _roleRepository.GetByGuidAsync(roleId); + if (role == null) + { + throw new KeyNotFoundException($"角色ID '{roleId}' 不存在"); + } + + // 检查是否为系统角色 + if (role.IsSystem) + { + throw new InvalidOperationException("系统角色不允许删除"); + } + + // 检查是否为超级管理员角色 + if (role.IsSupper) + { + throw new InvalidOperationException("超级管理员角色不允许删除"); + } + + await _roleRepository.RemoveAsync(roleId); + await _unitOfWork.CommitAsync(); + } + catch (Exception ex) + { + await _unitOfWork.RollbackAsync(); + throw new InvalidOperationException($"删除角色失败: {ex.Message}"); + } + } + + /// + /// 为角色分配权限 + /// + public async Task AssignPermissionsToRoleAsync(Guid roleId, IEnumerable permissionIds) + { + try + { + await _unitOfWork.BeginTransactionAsync(); + + var role = await _roleRepository.GetByGuidAsync(roleId); + if (role == null) + { + throw new KeyNotFoundException($"角色ID '{roleId}' 不存在"); + } + + // 验证权限是否存在 + var permissions = await _permissionRepository.GetByIdsAsync(permissionIds); + var validPermissionIds = permissions.Select(p => p.PermissionId).ToHashSet(); + var invalidPermissionIds = permissionIds.Except(validPermissionIds).ToList(); + + if (invalidPermissionIds.Any()) + { + throw new InvalidOperationException($"以下权限ID不存在: {string.Join(", ", invalidPermissionIds)}"); + } + + // 为角色分配权限(使用Permission实体) + role.AddPermissions(permissions); + + await _roleRepository.Update(role); + await _unitOfWork.CommitAsync(); + } + catch (Exception ex) + { + await _unitOfWork.RollbackAsync(); + throw new InvalidOperationException($"为角色分配权限失败: {ex.Message}"); + } + } + + /// + /// 移除角色的权限 + /// + public async Task RemovePermissionsFromRoleAsync(Guid roleId, IEnumerable permissionIds) + { + try + { + await _unitOfWork.BeginTransactionAsync(); + + var role = await _roleRepository.GetByGuidAsync(roleId); + if (role == null) + { + throw new KeyNotFoundException($"角色ID '{roleId}' 不存在"); + } + + // 获取要移除的权限实体 + var permissionsToRemove = await _permissionRepository.GetByIdsAsync(permissionIds); + + // 移除角色权限(使用Permission实体) + role.RemovePermissions(permissionsToRemove); + + await _roleRepository.Update(role); + await _unitOfWork.CommitAsync(); + } + catch (Exception ex) + { + await _unitOfWork.RollbackAsync(); + throw new InvalidOperationException($"移除角色权限失败: {ex.Message}"); + } + } + + /// + /// 获取角色的所有权限 + /// + public async Task> GetRolePermissionsAsync(Guid roleId) + { + var role = await _roleRepository.GetByGuidAsync(roleId); + if (role == null) + { + throw new KeyNotFoundException($"角色ID '{roleId}' 不存在"); + } + + // 直接使用角色的权限导航属性 + return role.Permissions.Select(p => new PermissionDto( + p.Name, + p.Code, + p.Description ?? string.Empty + )); + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/SystemSettings/DTOs/SystemSettingDto.cs b/backend/src/UniversalAdminSystem.Application/SystemSettings/DTOs/SystemSettingDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..fc2982d346a9b46ad5055b1bef759bafc280eb82 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/SystemSettings/DTOs/SystemSettingDto.cs @@ -0,0 +1,23 @@ +namespace UniversalAdminSystem.Application.SystemSettings.DTOs; + +public record SystemSettingDto( + Guid Id, + string Key, + string Value, + string? Description, + string? Group, + DateTime CreateTime, + DateTime UpdateTime +); + +public record SystemSettingCreateDto( + string Key, + string Value, + string? Description = null, + string? Group = null +); + +public record SystemSettingUpdateDto( + string Value, + string? Description = null +); \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/SystemSettings/Interfaces/ISystemSettingAppService.cs b/backend/src/UniversalAdminSystem.Application/SystemSettings/Interfaces/ISystemSettingAppService.cs new file mode 100644 index 0000000000000000000000000000000000000000..1fa29350dfa65582b434129ae3d9e193feb4ce07 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/SystemSettings/Interfaces/ISystemSettingAppService.cs @@ -0,0 +1,37 @@ +using UniversalAdminSystem.Application.SystemSettings.DTOs; +using UniversalAdminSystem.Application.Common.Results; + +namespace UniversalAdminSystem.Application.SystemSettings.Interfaces; + +public interface ISystemSettingAppService +{ + /// + /// 获取所有系统设置 + /// + Task>> GetAllSettingsAsync(); + + /// + /// 根据键获取系统设置 + /// + Task> GetSettingByKeyAsync(string key); + + /// + /// 根据组获取系统设置 + /// + Task>> GetSettingsByGroupAsync(string group); + + /// + /// 创建系统设置 + /// + Task> CreateSettingAsync(SystemSettingCreateDto createDto); + + /// + /// 更新系统设置 + /// + Task> UpdateSettingAsync(Guid id, SystemSettingUpdateDto updateDto); + + /// + /// 删除系统设置 + /// + Task DeleteSettingAsync(Guid id); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/SystemSettings/Services/SystemSettingAppService.cs b/backend/src/UniversalAdminSystem.Application/SystemSettings/Services/SystemSettingAppService.cs new file mode 100644 index 0000000000000000000000000000000000000000..66951bc553027ae2e079fed1018b1a88bbe35e07 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/SystemSettings/Services/SystemSettingAppService.cs @@ -0,0 +1,154 @@ +using UniversalAdminSystem.Application.SystemSettings.DTOs; +using UniversalAdminSystem.Application.SystemSettings.Interfaces; +using UniversalAdminSystem.Domian.SystemSettings.Aggregates; +using UniversalAdminSystem.Domian.Core.Interfaces; +using UniversalAdminSystem.Application.Common.Results; +using UniversalAdminSystem.Domian.SystemSettings.ValueObjects; +using UniversalAdminSystem.Application.Common.Interfaces; + +namespace UniversalAdminSystem.Application.SystemSettings.Services; + +public class SystemSettingAppService : ISystemSettingAppService +{ + private readonly IRepository _settingRepository; + private readonly IUnitOfWork _unitOfWork; + + public SystemSettingAppService( + IRepository settingRepository, + IUnitOfWork unitOfWork) + { + _settingRepository = settingRepository; + _unitOfWork = unitOfWork; + } + + public async Task>> GetAllSettingsAsync() + { + try + { + var settings = await _settingRepository.GetAllAsync(); + var settingDtos = settings.Select(MapToDto); + return Result>.Success(settingDtos); + } + catch (Exception ex) + { + return Result>.Failure($"获取系统设置失败: {ex.Message}"); + } + } + + public async Task> GetSettingByKeyAsync(string key) + { + try + { + var setting = (await _settingRepository.GetAllAsync()).FirstOrDefault(s => s.Key.Value == key); + if (setting == null) + { + return Result.Failure("设置不存在"); + } + + return Result.Success(MapToDto(setting)); + } + catch (Exception ex) + { + return Result.Failure($"获取系统设置失败: {ex.Message}"); + } + } + + public async Task>> GetSettingsByGroupAsync(string group) + { + try + { + var settings = await _settingRepository.GetAllAsync(); + var groupSettings = settings.Where(s => s.Key.Value.StartsWith(group)); + var settingDtos = groupSettings.Select(MapToDto); + return Result>.Success(settingDtos); + } + catch (Exception ex) + { + return Result>.Failure($"获取系统设置失败: {ex.Message}"); + } + } + + public async Task> CreateSettingAsync(SystemSettingCreateDto createDto) + { + try + { + await _unitOfWork.BeginTransactionAsync(); + + var setting = SystemSetting.Create( + SettingKey.Create(createDto.Key), + SettingValue.Create(createDto.Value), + SettingDescription.Create(createDto.Description ?? "") + ); + + var savedSetting = await _settingRepository.AddAsync(setting); + await _unitOfWork.CommitAsync(); + + return Result.Success(MapToDto(savedSetting)); + } + catch (Exception ex) + { + await _unitOfWork.RollbackAsync(); + return Result.Failure($"创建系统设置失败: {ex.Message}"); + } + } + + public async Task> UpdateSettingAsync(Guid id, SystemSettingUpdateDto updateDto) + { + try + { + await _unitOfWork.BeginTransactionAsync(); + + var setting = await _settingRepository.GetByGuidAsync(id); + if (setting == null) + { + return Result.Failure("设置不存在"); + } + + setting.UpdateValue(SettingValue.Create(updateDto.Value)); + + await _settingRepository.Update(setting); + await _unitOfWork.CommitAsync(); + + return Result.Success(MapToDto(setting)); + } + catch (Exception ex) + { + await _unitOfWork.RollbackAsync(); + return Result.Failure($"更新系统设置失败: {ex.Message}"); + } + } + + public async Task DeleteSettingAsync(Guid id) + { + try + { + await _unitOfWork.BeginTransactionAsync(); + + var setting = await _settingRepository.GetByGuidAsync(id); + if (setting == null) + { + return Result.Failure("设置不存在"); + } + + await _settingRepository.RemoveAsync(id); + await _unitOfWork.CommitAsync(); + + return Result.Success("删除成功"); + } + catch (Exception ex) + { + await _unitOfWork.RollbackAsync(); + return Result.Failure($"删除系统设置失败: {ex.Message}"); + } + } + + private static SystemSettingDto MapToDto(SystemSetting s) => new( + s.Id, + s.Key.Value, + s.Value.Value, + s.Description?.Value, + s.Group, + s.CreateTime, + s.UpdateTime + ); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/UniversalAdminSystem.Application.csproj b/backend/src/UniversalAdminSystem.Application/UniversalAdminSystem.Application.csproj new file mode 100644 index 0000000000000000000000000000000000000000..0f8f7107617b5269711aecaf00717b697627aa9d --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/UniversalAdminSystem.Application.csproj @@ -0,0 +1,17 @@ + + + + + + + + + + + + net8.0 + enable + enable + + + diff --git a/backend/src/UniversalAdminSystem.Application/UserManagement/Dtos/UserCreateDto.cs b/backend/src/UniversalAdminSystem.Application/UserManagement/Dtos/UserCreateDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..4c7fc36c3af7c787f556c33c54415441a7bca125 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/UserManagement/Dtos/UserCreateDto.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace UniversalAdminSystem.Application.UserManagement.Dtos; + +public record UserCreateDto( + [Required] + [MinLength(1)] + [MaxLength(20)] + string Account, + + [Required] + [MinLength(6)] + [MaxLength(20)] + string Password, + + [Required] + [EmailAddress] + string Email); \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/UserManagement/Dtos/UserDetailDto.cs b/backend/src/UniversalAdminSystem.Application/UserManagement/Dtos/UserDetailDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..34687607336df1ac6843f0dd02327ff679b1060e --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/UserManagement/Dtos/UserDetailDto.cs @@ -0,0 +1,26 @@ +using UniversalAdminSystem.Domian.UserManagement.Entities; +using UniversalAdminSystem.Domian.UserManagement.ValueObj; + +namespace UniversalAdminSystem.Application.UserManagement.Dtos; + + +public record UserDetailDto +{ + public Guid Id { get; set; } + public string Account { get; set; } + public string Email { get; set; } + public UserStatus UserStatus { get; set; } + + public string? RoleName { get; set; } + public UserInfo? UserInfo { get; set; } + + public UserDetailDto(Guid id, string account, string email, UserStatus userStatus, UserInfo? userInfo = null, string? rolename = null) + { + Id = id; + Account = account; + Email = email; + UserStatus = userStatus; + RoleName = rolename; + UserInfo = userInfo; + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/UserManagement/Dtos/UserDto.cs b/backend/src/UniversalAdminSystem.Application/UserManagement/Dtos/UserDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..f8151c8ac22a4cf7a3f7fceb40bbcb6fd4b4e821 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/UserManagement/Dtos/UserDto.cs @@ -0,0 +1,24 @@ +using UniversalAdminSystem.Domian.UserManagement.ValueObj; + +namespace UniversalAdminSystem.Application.UserManagement.Dtos; + +public record UserDto +{ + public Guid Id { get; set; } + public string Account { get; set; } + public string Email { get; set; } + public UserStatus UserStatus { get; set; } + + public string? RoleName { get; set; } + + + public UserDto(Guid id, string account, string email, UserStatus userStatus, string? rolename = null) + { + Id = id; + Account = account; + Email = email; + UserStatus = userStatus; + RoleName = rolename; + } +} + diff --git a/backend/src/UniversalAdminSystem.Application/UserManagement/Dtos/UserUpdateDto.cs b/backend/src/UniversalAdminSystem.Application/UserManagement/Dtos/UserUpdateDto.cs new file mode 100644 index 0000000000000000000000000000000000000000..b4a34fc9f6f508fea6f03215692634d5151a5610 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/UserManagement/Dtos/UserUpdateDto.cs @@ -0,0 +1,8 @@ +namespace UniversalAdminSystem.Application.UserManagement.Dtos; + +public record UserUpdateDto( + string Account, + string Email, + int StatusCode, + string? Password = null +); \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/UserManagement/Interface/IPasswordHelper.cs b/backend/src/UniversalAdminSystem.Application/UserManagement/Interface/IPasswordHelper.cs new file mode 100644 index 0000000000000000000000000000000000000000..af0c2c0fa51620991dc3c368b34c097ab1f9198c --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/UserManagement/Interface/IPasswordHelper.cs @@ -0,0 +1,11 @@ +namespace UniversalAdminSystem.Application.UserManagement.Interface; + +public interface IPasswordHelper +{ + + (string hashedPassword, string salt) HashPasswordWithSeparateSalt(string password); + bool VerifyPasswordWithSeparateSalt( + string password, + string storedHashedPassword, + string storedSalt); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/UserManagement/Interface/IUserManagementAppService.cs b/backend/src/UniversalAdminSystem.Application/UserManagement/Interface/IUserManagementAppService.cs new file mode 100644 index 0000000000000000000000000000000000000000..376a00ad207f5271ad8cb2cec7d4bcd731205cfb --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/UserManagement/Interface/IUserManagementAppService.cs @@ -0,0 +1,77 @@ +using UniversalAdminSystem.Application.Common.Results; +using UniversalAdminSystem.Application.UserManagement.Dtos; + +namespace UniversalAdminSystem.Application.UserManagement.Interface; + +/// +/// 用户管理应用服务接口 +/// 提供用户的基本CRUD操作和权限管理功能 +/// +public interface IUserManagementAppService +{ + /// + /// 获取所有用户列表 + /// + /// 用户列表 + Task>> GetUsersAsync(); + + /// + /// 创建新用户 + /// + /// 用户创建数据传输对象 + /// 创建的用户信息 + Task> CreateUserAsync(UserCreateDto createDto); + + /// + /// 删除用户 + /// + /// 用户ID + /// 删除操作结果 + Task DeleteUserAsync(Guid id); + + /// + /// 获取用户详情 + /// + /// 用户ID + /// 用户详情 + Task> GetUserByIdAsync(Guid userId); + + /// + /// 更新用户信息 + /// + /// 用户ID + /// 用户更新数据传输对象 + /// 更新操作结果 + Task UpdateUserAsync(Guid userId, UserUpdateDto updateDto); + + /// + /// 为用户分配角色 + /// + /// 用户ID + /// 角色ID列表 + /// 分配操作结果 + Task AssignRoleAsync(Guid userId, List roleIds); + + /// + /// 移除用户角色 + /// + /// 用户ID + /// 角色ID + /// 移除操作结果 + Task RemoveRoleAsync(Guid userId, Guid roleId); + + /// + /// 获取用户的所有权限 + /// + /// 用户ID + /// 权限编码列表 + Task>> GetUserPermissionsAsync(Guid userId); + + /// + /// 检查用户是否有指定权限 + /// + /// 用户ID + /// 权限编码 + /// 是否有权限 + Task> CheckUserPermissionAsync(Guid userId, string permissionCode); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/UserManagement/Service/UserManagementAppService.cs b/backend/src/UniversalAdminSystem.Application/UserManagement/Service/UserManagementAppService.cs new file mode 100644 index 0000000000000000000000000000000000000000..c6f7e1cfe664bfdff82d3e9fa4b424d47496a168 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/UserManagement/Service/UserManagementAppService.cs @@ -0,0 +1,386 @@ +using UniversalAdminSystem.Application.Common.Results; +using UniversalAdminSystem.Application.UserManagement.Dtos; +using UniversalAdminSystem.Application.UserManagement.Interface; +using UniversalAdminSystem.Domian.UserManagement.Aggregates; +using UniversalAdminSystem.Domian.UserManagement.Entities; +using UniversalAdminSystem.Domian.UserManagement.IRepository; +using UniversalAdminSystem.Domian.UserManagement.ValueObj; +using UniversalAdminSystem.Application.PermissionManagement.Interfaces; +using UniversalAdminSystem.Domian.Core.ValueObjects; +using UniversalAdminSystem.Domian.PermissionManagement.IRepository; +using UniversalAdminSystem.Application.Common.Interfaces; + +namespace UniversalAdminSystem.Application.UserManagement.Service; + +public class UserManagementAppService : IUserManagementAppService +{ + private readonly IUserRepository _userRepo; + private readonly IUserInfoRepository _userInfoRepo; + private readonly IRoleRepository _roleRepo; + private readonly IPasswordHelper _passwordHelper; + private readonly IUnitOfWork _unitOfWork; + private readonly IPermissionCheckService _permissionCheckService; + // private readonly UserPermissionIntegrationService _userPermissionIntegration; + // private readonly ICurrentUserContext _currentUserContext; + + public UserManagementAppService( + IUserRepository user, + IUserInfoRepository userInfo, + IRoleRepository role, + IPasswordHelper password, + IUnitOfWork Wokr, + IPermissionCheckService permissionCheckService) + { + _userRepo = user; + _userInfoRepo = userInfo; + _roleRepo = role; + _passwordHelper = password; + _unitOfWork = Wokr; + _permissionCheckService = permissionCheckService; + // _userPermissionIntegration = userPermissionIntegration; + // _currentUserContext = currentUserContext; + } + + public async Task>> GetUsersAsync() + { + try + { + // 权限验证:检查当前用户是否有权限查看用户列表 + // var currentUserId = _currentUserContext.GetCurrentUserId(); + // if (!currentUserId.HasValue) + // { + // return Result>.Failure("用户未登录"); + // } + + // if (!await _permissionCheckService.CheckUserPermissionAsync(currentUserId.Value, "user:read")) + // { + // return Result>.Failure("没有权限查看用户列表"); + // } + + var users = await _userRepo.GetAllAsync(); + var Roles = await _roleRepo.GetByNameAsync("普通用户") ?? throw new Exception("用户角色查询失败"); + var RoleId = users.Select(x => x.RoleId != null ? x.RoleId.Value : Roles.RoleId.Value); + var Role = await _roleRepo.GetByIdsAsync(RoleId); + var list = users.Select(x => new UserDto(x.UserId.Value, x.Account, x.Email, x.Status, + Role.FirstOrDefault(r => r.RoleId.Value == x.RoleId.Value).Name)); + return Result>.Success(list); + } + catch (Exception ex) + { + return Result>.Failure($"获取用户列表失败: {ex.Message}"); + } + } + + public async Task> CreateUserAsync(UserCreateDto createDto) + { + try + { + // 权限验证:检查当前用户是否有权限创建用户 + // var currentUserId = _currentUserContext.GetCurrentUserId(); + // if (!currentUserId.HasValue) + // { + // return Result.Failure("用户未登录"); + // } + + // if (!await _permissionCheckService.CheckUserPermissionAsync(currentUserId.Value, "user:create")) + // { + // return Result.Failure("没有权限创建用户"); + // } + + await _unitOfWork.BeginTransactionAsync(); + + // 1. 创建用户信息模板并保存 + var userInfo = UserInfo.CreateUserInfo(); + var info = await _userInfoRepo.AddAsync(userInfo); + + // 2. 密码加密与盐生成 + var (hashedPassword, salt) = _passwordHelper.HashPasswordWithSeparateSalt(createDto.Password); + var Role = await _roleRepo.GetByNameAsync("普通用户") ?? throw new Exception("用户创建失败"); + // 3. 创建用户账户并保存 + var user = User.CreateUser(info.UserInfoId, createDto.Account, hashedPassword, createDto.Email, salt, UserStatus.Normal, Role.RoleId.Value); + try + { + var savedUser = await _userRepo.AddAsync(user); + await _unitOfWork.CommitAsync(); + // 4. 返回基础信息DTO + return Result.Success(new UserDto(savedUser.UserId.Value, savedUser.Account, savedUser.Email, savedUser.Status)); + } + catch (System.Exception) + { + throw new Exception("用户已存在"); + } + } + catch (System.Exception e) + { + await _unitOfWork.RollbackAsync(); + return Result.Failure($"创建用户失败: {e.Message}"); + } + } + + public async Task DeleteUserAsync(Guid id) + { + try + { + var user = await _userRepo.GetByGuidAsync(id); + if (user == null) return Result.Failure("用户不存在"); + if (user.Status == UserStatus.Logout) return Result.Failure("用户已被注销"); + if (user.Status == UserStatus.Freeze) return Result.Failure("用户已被冻结"); + + // 权限验证:检查当前用户是否有权限删除目标用户 + // var currentUserId = _currentUserContext.GetCurrentUserId(); + // if (!currentUserId.HasValue) + // { + // return Result.Failure("用户未登录"); + // } + + // if (!await _userPermissionIntegration.CanDeleteUserAsync(currentUserId.Value, id)) + // { + // return Result.Failure("没有权限删除该用户"); + // } + + await _unitOfWork.BeginTransactionAsync(); + await _userRepo.DeleteUserSoftAsync(id); + await _unitOfWork.CommitAsync(); + return Result.Success("用户删除成功"); + } + catch (Exception e) + { + await _unitOfWork.RollbackAsync(); + return Result.Failure($"删除用户失败: {e.Message}"); + } + } + + public async Task AssignRoleAsync(Guid userId, List roleIds) + { + try + { + var user = await _userRepo.GetByGuidAsync(userId); + if (user == null) return Result.Failure("用户不存在"); + + // 权限验证:检查当前用户是否有权限为目标用户分配角色 + // var currentUserId = _currentUserContext.GetCurrentUserId(); + // if (!currentUserId.HasValue) + // { + // return Result.Failure("用户未登录"); + // } + + // if (!await _userPermissionIntegration.CanAssignRolesAsync(currentUserId.Value, userId, roleIds)) + // { + // return Result.Failure("没有权限为该用户分配角色"); + // } + + foreach (var rid in roleIds) + { + user.AssignRole((RoleId)rid); + } + await _unitOfWork.SaveChangesAsync(); + return Result.Success("角色分配成功"); + } + catch (Exception ex) + { + return Result.Failure($"分配角色失败: {ex.Message}"); + } + } + + public async Task RemoveRoleAsync(Guid userId, Guid roleId) + { + try + { + var user = await _userRepo.GetByGuidAsync(userId); + if (user == null) return Result.Failure("用户不存在"); + + // 权限验证:检查当前用户是否有权限移除用户角色 + // var currentUserId = _currentUserContext.GetCurrentUserId(); + // if (!currentUserId.HasValue) + // { + // return Result.Failure("用户未登录"); + // } + + // if (!await _permissionCheckService.CheckUserPermissionAsync(currentUserId.Value, "user:manage")) + // { + // return Result.Failure("没有权限管理用户角色"); + // } + + user.RemoveRole((RoleId)roleId); + await _unitOfWork.SaveChangesAsync(); + return Result.Success("角色移除成功"); + } + catch (Exception ex) + { + return Result.Failure($"移除角色失败: {ex.Message}"); + } + } + + public async Task> GetUserByIdAsync(Guid userId) + { + try + { + // 权限验证:检查当前用户是否有权限查看用户详情 + // var currentUserId = _currentUserContext.GetCurrentUserId(); + // if (!currentUserId.HasValue) + // { + // return Result.Failure("用户未登录"); + // } + + // if (!await _permissionCheckService.CheckUserPermissionAsync(currentUserId.Value, "user:read")) + // { + // return Result.Failure("没有权限查看用户详情"); + // } + + var user = await _userRepo.GetByGuidAsync(userId); + if (user == null) return Result.Failure("用户不存在"); + UserInfo? userInfo = null; + if (user.UserInfoId != null) + { + userInfo = await _userInfoRepo.GetByGuidAsync(user.UserInfoId); + } + return Result.Success(new UserDetailDto(user.UserId.Value, user.Account, user.Email, user.Status, userInfo)); + } + catch (Exception ex) + { + return Result.Failure($"获取用户详情失败: {ex.Message}"); + } + } + + public async Task UpdateUserAsync(Guid userId, UserUpdateDto updateDto) + { + try + { + var user = await _userRepo.GetByGuidAsync(userId); + if (user == null) + { + return Result.Failure("用户不存在"); + } + if (updateDto == null) + { + return Result.Failure("为传入数据"); + } + Console.WriteLine("检查更新用户数据传输对象-------------- "); + Console.WriteLine($"检查账号: {updateDto.Account}"); + Console.WriteLine($"检查账号: {updateDto.Email}"); + Console.WriteLine($"检查账号: {updateDto.StatusCode}"); + Console.WriteLine($"检查账号: {updateDto.Password}"); + Console.WriteLine("检查结束-----------"); + + + // 权限验证:检查当前用户是否有权限更新用户信息 + // var currentUserId = _currentUserContext.GetCurrentUserId(); + // if (!currentUserId.HasValue) + // { + // return Result.Failure("用户未登录"); + // } + + // if (!await _permissionCheckService.CheckUserPermissionAsync(currentUserId.Value, "user:update")) + // { + // return Result.Failure("没有权限更新用户信息"); + // } + + await _unitOfWork.BeginTransactionAsync(); + + // 更新用户信息 + if (!string.IsNullOrEmpty(updateDto.Email)) + { + user.UpdateUserEmail(updateDto.Email); + } + + // 如果提供了新密码,则更新密码 + if (!string.IsNullOrEmpty(updateDto.Password)) + { + var (hashedPassword, salt) = _passwordHelper.HashPasswordWithSeparateSalt(updateDto.Password); + user.UpdateUserPassword(hashedPassword, salt); + } + var userStatus = (UserStatus)updateDto.StatusCode; + user.UpdateUserStatus((UserStatus)updateDto.StatusCode); + + await _userRepo.Update(user); + await _unitOfWork.CommitAsync(); + + return Result.Success("用户信息更新成功"); + } + catch (Exception e) + { + await _unitOfWork.RollbackAsync(); + return Result.Failure($"更新用户信息失败: {e.Message}"); + } + } + + public async Task>> GetUserPermissionsAsync(Guid userId) + { + try + { + // 权限验证:检查当前用户是否有权限查看用户权限 + // var currentUserId = _currentUserContext.GetCurrentUserId(); + // if (!currentUserId.HasValue) + // { + // return Result>.Failure("用户未登录"); + // } + + // if (!await _permissionCheckService.CheckUserPermissionAsync(currentUserId.Value, "user:read")) + // { + // return Result>.Failure("没有权限查看用户权限"); + // } + + var user = await _userRepo.GetByGuidAsync(userId); + if (user == null) return Result>.Failure("用户不存在"); + if (user.RoleId == null) return Result>.Failure("用户角色不存在"); + var role = await _roleRepo.GetByGuidAsync(user.RoleId.Value); + if (role == null) return Result>.Failure("用户角色不存在"); + var permissions = role.Permissions.Select(x => x.Code.Value); + return Result>.Success(permissions); + } + catch (Exception ex) + { + return Result>.Failure($"获取用户权限失败: {ex.Message}"); + } + } + + // public async Task>> GetUserRolesAsync(Guid userId) + // { + // try + // { + // // 权限验证:检查当前用户是否有权限查看用户角色 + // var currentUserId = _currentUserContext.GetCurrentUserId(); + // if (!currentUserId.HasValue) + // { + // return Result>.Failure("用户未登录"); + // } + + // if (!await _permissionCheckService.CheckUserPermissionAsync(currentUserId.Value, "user:read")) + // { + // return Result>.Failure("没有权限查看用户角色"); + // } + + // var roles = await _permissionCheckService.Get(userId); + // return Result>.Success(roles); + // } + // catch (Exception ex) + // { + // return Result>.Failure($"获取用户角色失败: {ex.Message}"); + // } + // } + + public async Task> CheckUserPermissionAsync(Guid userId, string permissionCode) + { + try + { + // 权限验证:检查当前用户是否有权限检查其他用户权限 + // var currentUserId = _currentUserContext.GetCurrentUserId(); + // if (!currentUserId.HasValue) + // { + // return Result.Failure("用户未登录"); + // } + + // if (!await _permissionCheckService.CheckUserPermissionAsync(currentUserId.Value, "user:read")) + // { + // return Result.Failure("没有权限检查用户权限"); + // } + + var hasPermission = await _permissionCheckService.CheckUserPermissionAsync(userId, permissionCode); + return Result.Success(hasPermission); + } + catch (Exception ex) + { + return Result.Failure($"检查用户权限失败: {ex.Message}"); + } + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Application/UserManagement/Service/UserPermissionIntegrationService.cs b/backend/src/UniversalAdminSystem.Application/UserManagement/Service/UserPermissionIntegrationService.cs new file mode 100644 index 0000000000000000000000000000000000000000..31c096e0d76dc021a85c8fd538969a1b5859ff5d --- /dev/null +++ b/backend/src/UniversalAdminSystem.Application/UserManagement/Service/UserPermissionIntegrationService.cs @@ -0,0 +1,142 @@ +// using UniversalAdminSystem.Application.PermissionManagement.Interfaces; +// using UniversalAdminSystem.Domian.UserManagement.IRepository; +// using UniversalAdminSystem.Domian.PermissionManagement.IRepository; + +// namespace UniversalAdminSystem.Application.UserManagement.Service; + +// /// +// /// 用户权限集成服务 +// /// 提供用户管理与权限系统的集成功能 +// /// +// public class UserPermissionIntegrationService +// { +// private readonly IUserRepository _userRepository; +// private readonly IRoleRepository _roleRepository; +// private readonly IPermissionCheckService _permissionCheckService; + +// public UserPermissionIntegrationService( +// IUserRepository userRepository, +// IRoleRepository roleRepository, +// IPermissionCheckService permissionCheckService) +// { +// _userRepository = userRepository; +// _roleRepository = roleRepository; +// _permissionCheckService = permissionCheckService; +// } + +// /// +// /// 验证用户是否有管理其他用户的权限 +// /// +// public async Task CanManageUserAsync(Guid currentUserId, Guid targetUserId) +// { +// // 超级管理员可以管理所有用户 +// if (await IsSuperAdminAsync(currentUserId)) +// { +// return true; +// } + +// // 普通管理员只能管理普通用户 +// if (await IsAdminAsync(currentUserId)) +// { +// var targetUser = await _userRepository.GetByGuidAsync(targetUserId); +// return targetUser != null && !await IsAdminAsync(targetUserId); +// } + +// return false; +// } + +// /// +// /// 验证用户是否有删除其他用户的权限 +// /// +// public async Task CanDeleteUserAsync(Guid currentUserId, Guid targetUserId) +// { +// // 不能删除自己 +// if (currentUserId == targetUserId) +// { +// return false; +// } + +// // 超级管理员可以删除任何用户 +// if (await IsSuperAdminAsync(currentUserId)) +// { +// return true; +// } + +// // 普通管理员只能删除普通用户 +// if (await IsAdminAsync(currentUserId)) +// { +// var targetUser = await _userRepository.GetByGuidAsync(targetUserId); +// return targetUser != null && !await IsAdminAsync(targetUserId); +// } + +// return false; +// } + +// /// +// /// 验证用户是否有分配角色的权限 +// /// +// public async Task CanAssignRolesAsync(Guid currentUserId, Guid targetUserId, List roleIds) +// { +// // 超级管理员可以分配任何角色 +// if (await IsSuperAdminAsync(currentUserId)) +// { +// return true; +// } + +// // 普通管理员只能分配普通角色 +// if (await IsAdminAsync(currentUserId)) +// { +// var roles = await _roleRepository.GetByIdsAsync(roleIds); +// return roles.All(r => !r.Name.Value.Contains("Admin") && !r.Name.Value.Contains("Super")); +// } + +// return false; +// } + +// /// +// /// 检查是否为超级管理员 +// /// +// private async Task IsSuperAdminAsync(Guid userId) +// { +// var permissions = await _permissionCheckService.GetUserPermissionsAsync(userId); +// return permissions.Any(p => p.Contains("super") || p.Contains("system")); +// } + +// /// +// /// 检查是否为管理员 +// /// +// private async Task IsAdminAsync(Guid userId) +// { +// var roles = await _permissionCheckService.GetUserRolesAsync(userId); +// return roles.Any(r => r.Contains("Admin") || r.Contains("Manager")); +// } + +// /// +// /// 获取用户的权限级别 +// /// +// public async Task GetUserPermissionLevelAsync(Guid userId) +// { +// if (await IsSuperAdminAsync(userId)) +// { +// return UserPermissionLevel.SuperAdmin; +// } +// else if (await IsAdminAsync(userId)) +// { +// return UserPermissionLevel.Admin; +// } +// else +// { +// return UserPermissionLevel.User; +// } +// } +// } + +// /// +// /// 用户权限级别枚举 +// /// +// public enum UserPermissionLevel +// { +// User, // 普通用户 +// Admin, // 管理员 +// SuperAdmin // 超级管理员 +// } \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/Core/AggregateRoot.cs b/backend/src/UniversalAdminSystem.Domian/Core/AggregateRoot.cs new file mode 100644 index 0000000000000000000000000000000000000000..f25d0718bc294995c4bca48d346615a411ae2508 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/Core/AggregateRoot.cs @@ -0,0 +1,8 @@ +namespace UniversalAdminSystem.Domian.Core; + +/// +/// 聚合根基类 +/// +public abstract class AggregateRoot +{ +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/Core/DomainEvent.cs b/backend/src/UniversalAdminSystem.Domian/Core/DomainEvent.cs new file mode 100644 index 0000000000000000000000000000000000000000..015f6acc2b07d6b6c415e6be7c778412ab3b778e --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/Core/DomainEvent.cs @@ -0,0 +1,23 @@ +namespace UniversalAdminSystem.Domian.Core; + +/// +/// 领域事件基类 +/// 为所有领域事件提供统一的基础属性 +/// +public abstract class DomainEvent +{ + /// + /// 事件ID + /// + public Guid Id { get; } = Guid.NewGuid(); + + /// + /// 事件发生时间 + /// + public DateTime OccurredOn { get; } = DateTime.UtcNow; + + /// + /// 事件类型名称 + /// + public string EventType => GetType().Name; +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/Core/Events/PermissionEvents.cs b/backend/src/UniversalAdminSystem.Domian/Core/Events/PermissionEvents.cs new file mode 100644 index 0000000000000000000000000000000000000000..273a140be63814271ba455c697ab8b34478e8a48 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/Core/Events/PermissionEvents.cs @@ -0,0 +1,78 @@ +using UniversalAdminSystem.Domian.Core.ValueObjects; + +namespace UniversalAdminSystem.Domian.Core.Events; + +/// +/// 权限创建事件 +/// +public class PermissionCreatedEvent : DomainEvent +{ + public PermissionId PermissionId { get; } + public string PermissionCode { get; } + + public PermissionCreatedEvent(PermissionId permissionId, string permissionCode) + { + PermissionId = permissionId; + PermissionCode = permissionCode; + } +} + +/// +/// 权限分配给角色事件 +/// +public class PermissionAssignedToRoleEvent : DomainEvent +{ + public RoleId RoleId { get; } + public PermissionId PermissionId { get; } + + public PermissionAssignedToRoleEvent(RoleId roleId, PermissionId permissionId) + { + RoleId = roleId; + PermissionId = permissionId; + } +} + +/// +/// 权限从角色移除事件 +/// +public class PermissionRemovedFromRoleEvent : DomainEvent +{ + public RoleId RoleId { get; } + public PermissionId PermissionId { get; } + + public PermissionRemovedFromRoleEvent(RoleId roleId, PermissionId permissionId) + { + RoleId = roleId; + PermissionId = permissionId; + } +} + +/// +/// 权限删除事件 +/// +public class PermissionDeletedEvent : DomainEvent +{ + public PermissionId PermissionId { get; } + public string PermissionCode { get; } + + public PermissionDeletedEvent(PermissionId permissionId, string permissionCode) + { + PermissionId = permissionId; + PermissionCode = permissionCode; + } +} + +/// +/// 权限变更事件 +/// +public class PermissionUpdatedEvent : DomainEvent +{ + public PermissionId PermissionId { get; } + public string PermissionCode { get; } + + public PermissionUpdatedEvent(PermissionId permissionId, string permissionCode) + { + PermissionId = permissionId; + PermissionCode = permissionCode; + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/Core/Events/RoleEvents.cs b/backend/src/UniversalAdminSystem.Domian/Core/Events/RoleEvents.cs new file mode 100644 index 0000000000000000000000000000000000000000..c21d924ff1770f2b66ceffc0ce70baff7f06c139 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/Core/Events/RoleEvents.cs @@ -0,0 +1,50 @@ +using UniversalAdminSystem.Domian.Core.ValueObjects; + +namespace UniversalAdminSystem.Domian.Core.Events; + +/// +/// 角色创建事件 +/// +public class RoleCreatedEvent : DomainEvent +{ + public RoleId RoleId { get; } + public string RoleName { get; } + + public RoleCreatedEvent(RoleId roleId, string roleName) + { + RoleId = roleId; + RoleName = roleName; + } +} + +/// +/// 角色更新事件 +/// +public class RoleUpdatedEvent : DomainEvent +{ + public RoleId RoleId { get; } + public string OldName { get; } + public string NewName { get; } + + public RoleUpdatedEvent(RoleId roleId, string oldName, string newName) + { + RoleId = roleId; + OldName = oldName; + NewName = newName; + } +} + +/// +/// 角色删除事件 +/// +public class RoleDeletedEvent : DomainEvent +{ + public RoleId RoleId { get; } + public string RoleName { get; } + + public RoleDeletedEvent(RoleId roleId, string roleName) + { + RoleId = roleId; + RoleName = roleName; + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/Core/Events/UserEvents.cs b/backend/src/UniversalAdminSystem.Domian/Core/Events/UserEvents.cs new file mode 100644 index 0000000000000000000000000000000000000000..c78aaf9b67b58ccdc4ee37286ac09f5cf4f05a49 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/Core/Events/UserEvents.cs @@ -0,0 +1,66 @@ +using UniversalAdminSystem.Domian.Core.ValueObjects; +using UniversalAdminSystem.Domian.UserManagement.ValueObj; + +namespace UniversalAdminSystem.Domian.Core.Events; + +// /// +// /// 用户创建事件 +// /// +// public class UserCreatedEvent : DomainEvent +// { +// public UserId UserId { get; } +// public string Account { get; } + +// public UserCreatedEvent(UserId userId, string account) +// { +// UserId = userId; +// Account = account; +// } +// } + +// /// +// /// 用户角色分配事件 +// /// +// public class UserRoleAssignedEvent : DomainEvent +// { +// public UserId UserId { get; } +// public RoleId RoleId { get; } + +// public UserRoleAssignedEvent(UserId userId, RoleId roleId) +// { +// UserId = userId; +// RoleId = roleId; +// } +// } + +// /// +// /// 用户角色移除事件 +// /// +// public class UserRoleRemovedEvent : DomainEvent +// { +// public UserId UserId { get; } +// public RoleId RoleId { get; } + +// public UserRoleRemovedEvent(UserId userId, RoleId roleId) +// { +// UserId = userId; +// RoleId = roleId; +// } +// } + +/// +/// 用户状态变更事件 +/// +public class UserStatusChangedEvent : DomainEvent +{ + public UserId UserId { get; } + public UserStatus OldStatus { get; } + public UserStatus NewStatus { get; } + + public UserStatusChangedEvent(UserId userId, UserStatus oldStatus, UserStatus newStatus) + { + UserId = userId; + OldStatus = oldStatus; + NewStatus = newStatus; + } +} \ No newline at end of file diff --git a/backend/txt.txt b/backend/src/UniversalAdminSystem.Domian/Core/Exceptions/DomainException.cs similarity index 100% rename from backend/txt.txt rename to backend/src/UniversalAdminSystem.Domian/Core/Exceptions/DomainException.cs diff --git a/frontend/txt.txt "b/backend/src/UniversalAdminSystem.Domian/Core/Exceptions/\351\242\206\345\237\237\345\274\202\345\270\270.txt" similarity index 100% rename from frontend/txt.txt rename to "backend/src/UniversalAdminSystem.Domian/Core/Exceptions/\351\242\206\345\237\237\345\274\202\345\270\270.txt" diff --git a/backend/src/UniversalAdminSystem.Domian/Core/Interfaces/IRepository.cs b/backend/src/UniversalAdminSystem.Domian/Core/Interfaces/IRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..b5563bc89b93221efb457cdb17f52c2be8047171 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/Core/Interfaces/IRepository.cs @@ -0,0 +1,40 @@ +namespace UniversalAdminSystem.Domian.Core.Interfaces; + +/// +/// 通用仓储接口,提供通用的数据访问方法(CRUD) +/// +public interface IRepository where T : class +{ + /// + /// 新增 + /// + /// + /// + Task AddAsync(T entity); + + /// + /// 删除 + /// + /// + /// + Task RemoveAsync(Guid guid); + + /// + /// 更新 + /// + /// + /// + Task Update(T entity); + + /// + /// 查询所有 + /// + /// + Task> GetAllAsync(); + + /// + /// 查询指定 + /// + /// + Task GetByGuidAsync(Guid guid); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/Core/ValueObjects/ChunkId.cs b/backend/src/UniversalAdminSystem.Domian/Core/ValueObjects/ChunkId.cs new file mode 100644 index 0000000000000000000000000000000000000000..858b370a681a1a4f8b72bebbd84f5c87f7b194b3 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/Core/ValueObjects/ChunkId.cs @@ -0,0 +1,20 @@ +namespace UniversalAdminSystem.Domian.Core.ValueObjects; + +public record ChunkId +{ + public Guid Value { get; init; } + private ChunkId(Guid value) + { + if (Guid.Empty == value) + { + throw new ArgumentException("ChunkId不能为空"); + } + + Value = value; + } + + public static ChunkId Create(Guid Id) => new(Id); + + public static explicit operator ChunkId(Guid value) => Create(value); + public static implicit operator Guid(ChunkId Id) => Id.Value; +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/Core/ValueObjects/ConversationId.cs b/backend/src/UniversalAdminSystem.Domian/Core/ValueObjects/ConversationId.cs new file mode 100644 index 0000000000000000000000000000000000000000..9361c397f0dbf44bc55b8041f68a11c67e0021a0 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/Core/ValueObjects/ConversationId.cs @@ -0,0 +1,22 @@ +namespace UniversalAdminSystem.Domian.Core.ValueObjects; + +public record ConversationId +{ + public Guid Value { get; init; } + + private ConversationId(Guid value) + { + if (value == Guid.Empty) + { + throw new ArgumentException("ConversationId不能为空"); + } + Value = value; + } + + public static ConversationId Create(Guid value) + { + return new ConversationId(value); + } + + public override string ToString() => Value.ToString(); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/Core/ValueObjects/MessageId.cs b/backend/src/UniversalAdminSystem.Domian/Core/ValueObjects/MessageId.cs new file mode 100644 index 0000000000000000000000000000000000000000..2a2e6a7495ad3c864fd44584788cf3397ac651f8 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/Core/ValueObjects/MessageId.cs @@ -0,0 +1,22 @@ +namespace UniversalAdminSystem.Domian.Core.ValueObjects; + +public record MessageId +{ + public Guid Value { get; init; } + + private MessageId(Guid value) + { + if (value == Guid.Empty) + { + throw new ArgumentException("MessageId不能为空"); + } + Value = value; + } + + public static MessageId Create(Guid value) + { + return new MessageId(value); + } + + public override string ToString() => Value.ToString(); +} diff --git a/backend/src/UniversalAdminSystem.Domian/Core/ValueObjects/PermissionId.cs b/backend/src/UniversalAdminSystem.Domian/Core/ValueObjects/PermissionId.cs new file mode 100644 index 0000000000000000000000000000000000000000..535f3a8edbddbe212168ac7c14c5a9645e9f8a45 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/Core/ValueObjects/PermissionId.cs @@ -0,0 +1,22 @@ +namespace UniversalAdminSystem.Domian.Core.ValueObjects; + +/// +/// 权限ID值对象 +/// +public record PermissionId +{ + public Guid Value { get; init; } + + public PermissionId(Guid value) + { + Value = value; + } + + public static PermissionId Create(Guid value) + { + return new PermissionId(value); + } + + public static implicit operator Guid(PermissionId permissionId) => permissionId.Value; + public static implicit operator PermissionId(Guid value) => new(value); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/Core/ValueObjects/RoleId.cs b/backend/src/UniversalAdminSystem.Domian/Core/ValueObjects/RoleId.cs new file mode 100644 index 0000000000000000000000000000000000000000..a1e9ba093d369d8da138b3ba7adaa1bec4bcdec0 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/Core/ValueObjects/RoleId.cs @@ -0,0 +1,19 @@ +namespace UniversalAdminSystem.Domian.Core.ValueObjects; + +public record RoleId +{ + public Guid Value { get; init; } + + private RoleId(Guid value) + { + if (Guid.Empty == value) + throw new ArgumentException("RoleId不能为空"); + + Value = value; + } + + public static RoleId Create(Guid value) => new(value); + + public static explicit operator RoleId(Guid value) => Create(value); + public static implicit operator Guid(RoleId roleId) => roleId.Value; +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/Core/ValueObjects/UserId.cs b/backend/src/UniversalAdminSystem.Domian/Core/ValueObjects/UserId.cs new file mode 100644 index 0000000000000000000000000000000000000000..f7d5a448ed88eecae11e0f09be5d9c6d225b3b37 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/Core/ValueObjects/UserId.cs @@ -0,0 +1,14 @@ +namespace UniversalAdminSystem.Domian.Core.ValueObjects; + +public record UserId +{ + public Guid Value { get; init; } + + public static UserId Create(Guid id) + { + return new UserId{ Value = id }; + } + + public static explicit operator UserId(Guid id) => Create(id); + public static implicit operator Guid(UserId userId) => userId.Value; +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/Core/ValueObjects/UserInfoId.cs b/backend/src/UniversalAdminSystem.Domian/Core/ValueObjects/UserInfoId.cs new file mode 100644 index 0000000000000000000000000000000000000000..0cc676454a4e2ce0235f589d850241b1e66bbaa0 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/Core/ValueObjects/UserInfoId.cs @@ -0,0 +1,50 @@ +using System; + +namespace UniversalAdminSystem.Domian.Core.ValueObjects; + +/// +/// 用户信息ID值对象 +/// +public class UserInfoId +{ + public Guid Value { get; private set; } + + private UserInfoId(Guid value) + { + Value = value; + } + + public static UserInfoId Create(Guid value) + { + return new UserInfoId(value); + } + + public static implicit operator Guid(UserInfoId userInfoId) + { + return userInfoId.Value; + } + + public static implicit operator UserInfoId(Guid value) + { + return new UserInfoId(value); + } + + public override bool Equals(object? obj) + { + if (obj is UserInfoId other) + { + return Value.Equals(other.Value); + } + return false; + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } + + public override string ToString() + { + return Value.ToString(); + } +} \ No newline at end of file diff --git "a/backend/src/UniversalAdminSystem.Domian/Core/ValueObjects/\351\200\232\347\224\250\345\200\274\345\257\271\350\261\241.txt" "b/backend/src/UniversalAdminSystem.Domian/Core/ValueObjects/\351\200\232\347\224\250\345\200\274\345\257\271\350\261\241.txt" new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/src/UniversalAdminSystem.Domian/DomainServices.cs b/backend/src/UniversalAdminSystem.Domian/DomainServices.cs new file mode 100644 index 0000000000000000000000000000000000000000..6fe24545fbf856149d4114c518ceb80eeb1b6fcf --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/DomainServices.cs @@ -0,0 +1,24 @@ +using System.Reflection; + +namespace UniversalAdminSystem.Domian; + +public class DomainServices +{ + public static IEnumerable<(Type ServiceType, Type ImplementationType)> DiscoverServices() + { + var domainAssembly = Assembly.GetExecutingAssembly(); + var serviceInterfaces = domainAssembly.GetTypes() + .Where(x => x.IsInterface && x.Name.EndsWith("DomainService")); + var serviceImplementations = domainAssembly.GetTypes() + .Where(x => x.IsClass && !x.IsAbstract && x.Name.EndsWith("DomainService")); + + foreach (var iface in serviceInterfaces) + { + var impl = serviceImplementations.FirstOrDefault(x => iface.IsAssignableFrom(x)); + if (impl != null) + { + yield return (iface, impl); + } + } + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/FileStorage/Aggregates/File.cs b/backend/src/UniversalAdminSystem.Domian/FileStorage/Aggregates/File.cs new file mode 100644 index 0000000000000000000000000000000000000000..f5e30c3ec6111c203ae5bc198afb8a7bd7efa528 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/FileStorage/Aggregates/File.cs @@ -0,0 +1,81 @@ +using UniversalAdminSystem.Domian.Core; +using UniversalAdminSystem.Domian.Core.ValueObjects; +using UniversalAdminSystem.Domian.FileStorage.ValueObjects; + +namespace UniversalAdminSystem.Domian.FileStorage.Aggregates; + +public class File : AggregateRoot +{ + public FileId Id { get; private set; } + public FileName Name { get; private set; } + public FilePath Path { get; private set; } + public FileSize Size { get; private set; } + public FileType Type { get; private set; } + public UserId OwnerId { get; private set; } + public DateTime UploadTime { get; private set; } + public bool IsFolder { get; private set; } + public FileId? ParentId { get; private set; } + public FileAccessLevel AccessLevel { get; private set; } + public string? SecurityCheckResult { get; private set; } + + private File() { } + + public static File Create( + FileName name, + FilePath path, + FileSize size, + FileType type, + UserId ownerId, + bool isFolder = false, + FileId? parentId = null, + FileAccessLevel accessLevel = FileAccessLevel.Private + ) + { + if (size.Value > FileSize.MaxSize) throw new ArgumentException($"文件大小不能超过{FileSize.MaxSize / 1024 / 1024}MB"); + return new File + { + Id = FileId.Create(), + Name = name, + Path = path, + Size = size, + Type = type, + OwnerId = ownerId, + UploadTime = DateTime.UtcNow, + IsFolder = isFolder, + ParentId = parentId, + AccessLevel = accessLevel + }; + } + + public void SetSecurityCheckResult(string result) + { + SecurityCheckResult = result; + } + + public void Rename(FileName newName) + { + Name = newName; + } + + public void Move(FileId? newParentId) + { + ParentId = newParentId; + } + + public void ChangeAccessLevel(FileAccessLevel accessLevel) + { + AccessLevel = accessLevel; + } + + public bool CanAccess(UserId userId, bool isSuperAdmin, bool isAdmin) + { + if (isSuperAdmin) return true; + if (isAdmin) + { + // 管理员可访问公开和受限文件 + return AccessLevel == FileAccessLevel.Public || AccessLevel == FileAccessLevel.Restricted || OwnerId == userId; + } + // 普通用户只能访问公开文件或自己拥有的文件 + return AccessLevel == FileAccessLevel.Public || OwnerId == userId; + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/FileStorage/IRepository/IFileRepository.cs b/backend/src/UniversalAdminSystem.Domian/FileStorage/IRepository/IFileRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..8ea664a5523965cccfff90d079528df4ebb1efef --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/FileStorage/IRepository/IFileRepository.cs @@ -0,0 +1,14 @@ +using UniversalAdminSystem.Domian.Core.Interfaces; +using UniversalAdminSystem.Domian.Core.ValueObjects; +using UniversalAdminSystem.Domian.FileStorage.ValueObjects; + +namespace UniversalAdminSystem.Domian.FileStorage.IRepository; + +public interface IFileRepository : IRepository +{ + Task> GetByOwnerAsync(UserId ownerId); + Task> GetByParentAsync(FileId? parentId); + Task> GetByTypeAsync(FileType type); + Task> GetFilesWithAccessAsync(UserId userId, FileAccessLevel accessLevel); + Task> GetAllFoldersAsync(UserId ownerId); +} diff --git a/backend/src/UniversalAdminSystem.Domian/FileStorage/Interfaces/IFileDomainService.cs b/backend/src/UniversalAdminSystem.Domian/FileStorage/Interfaces/IFileDomainService.cs new file mode 100644 index 0000000000000000000000000000000000000000..3872ef9ce8422bfc5338aabe647230c565fa3977 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/FileStorage/Interfaces/IFileDomainService.cs @@ -0,0 +1,6 @@ +namespace UniversalAdminSystem.Domian.FileStorage.Interfaces; + +public interface IFileDomainService +{ + bool CheckFileSecurity(out string result, string? name = null, string? type = null); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/FileStorage/Services/FileDomainService.cs b/backend/src/UniversalAdminSystem.Domian/FileStorage/Services/FileDomainService.cs new file mode 100644 index 0000000000000000000000000000000000000000..835a7cceeefa8677bc5b1594ae638006bdaec89f --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/FileStorage/Services/FileDomainService.cs @@ -0,0 +1,18 @@ +using UniversalAdminSystem.Domian.FileStorage.Interfaces; + +namespace UniversalAdminSystem.Domian.FileStorage.Services; + +public class FileDomainService : IFileDomainService +{ + public bool CheckFileSecurity(out string result, string? name = null, string? type = null) + { + if (name != null && name.Contains("..")) + { + result = "文件名非法"; + return false; + } + + result = "安全"; + return true; + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/FileStorage/ValueObjects/FileAccessLevel.cs b/backend/src/UniversalAdminSystem.Domian/FileStorage/ValueObjects/FileAccessLevel.cs new file mode 100644 index 0000000000000000000000000000000000000000..42fb0776dfc6d79611086d6a0d2d0c4d4e199cb4 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/FileStorage/ValueObjects/FileAccessLevel.cs @@ -0,0 +1,8 @@ +namespace UniversalAdminSystem.Domian.FileStorage.ValueObjects; + +public enum FileAccessLevel +{ + Public, + Restricted, + Private, +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/FileStorage/ValueObjects/FileId.cs b/backend/src/UniversalAdminSystem.Domian/FileStorage/ValueObjects/FileId.cs new file mode 100644 index 0000000000000000000000000000000000000000..6ba91d8b7bc4dc835d32a338e7d972aa4c18da47 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/FileStorage/ValueObjects/FileId.cs @@ -0,0 +1,11 @@ +namespace UniversalAdminSystem.Domian.FileStorage.ValueObjects; + +public record FileId +{ + public Guid Value { get; init; } + private FileId(Guid value) { Value = value; } + public static FileId Create() => new(Guid.NewGuid()); + public static FileId Create(Guid value) => new(value); + public static implicit operator Guid(FileId id) => id.Value; + public static implicit operator FileId(Guid value) => new(value); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/FileStorage/ValueObjects/FileName.cs b/backend/src/UniversalAdminSystem.Domian/FileStorage/ValueObjects/FileName.cs new file mode 100644 index 0000000000000000000000000000000000000000..303662efff3b74aa7433d465f4d9eb8d4ea773c5 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/FileStorage/ValueObjects/FileName.cs @@ -0,0 +1,16 @@ +namespace UniversalAdminSystem.Domian.FileStorage.ValueObjects; + +public record FileName +{ + public string Value { get; } + private FileName(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentException("文件名不能为空"); + if (value.Length > 200) + throw new ArgumentException("文件名不能超过200字符"); + Value = value; + } + public static FileName Create(string value) => new(value); + public static implicit operator string(FileName name) => name.Value; +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/FileStorage/ValueObjects/FilePath.cs b/backend/src/UniversalAdminSystem.Domian/FileStorage/ValueObjects/FilePath.cs new file mode 100644 index 0000000000000000000000000000000000000000..e7b71c30904356043e9992feab57dd2147423f00 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/FileStorage/ValueObjects/FilePath.cs @@ -0,0 +1,16 @@ +namespace UniversalAdminSystem.Domian.FileStorage.ValueObjects; + +public record FilePath +{ + public string Value { get; } + private FilePath(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentException("文件路径不能为空"); + if (value.Length > 500) + throw new ArgumentException("文件路径不能超过500字符"); + Value = value; + } + public static FilePath Create(string value) => new(value); + public static implicit operator string(FilePath path) => path.Value; +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/FileStorage/ValueObjects/FileSize.cs b/backend/src/UniversalAdminSystem.Domian/FileStorage/ValueObjects/FileSize.cs new file mode 100644 index 0000000000000000000000000000000000000000..15a50ef40a9872cb49fbd8fc8983b6f813567d83 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/FileStorage/ValueObjects/FileSize.cs @@ -0,0 +1,17 @@ +namespace UniversalAdminSystem.Domian.FileStorage.ValueObjects; + +public record FileSize +{ + public long Value { get; } + public const long MaxSize = 50 * 1024 * 1024; // 50MB + private FileSize(long value) + { + if (value < 0) + throw new ArgumentException("文件大小不能为负数"); + if (value > MaxSize) + throw new ArgumentException($"文件大小不能超过{MaxSize / 1024 / 1024}MB"); + Value = value; + } + public static FileSize Create(long value) => new(value); + public static implicit operator long(FileSize size) => size.Value; +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/FileStorage/ValueObjects/FileType.cs b/backend/src/UniversalAdminSystem.Domian/FileStorage/ValueObjects/FileType.cs new file mode 100644 index 0000000000000000000000000000000000000000..95eb03736678849b1b54ccc9bdac090c2c8100b1 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/FileStorage/ValueObjects/FileType.cs @@ -0,0 +1,16 @@ +namespace UniversalAdminSystem.Domian.FileStorage.ValueObjects; + +public record FileType +{ + public string Value { get; } + public static readonly string[] AllowedTypes = new[] { "jpg", "jpeg", "png", ".gif", ".bmp", ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".txt", ".zip", ".rar" }; + private FileType(string value) + { + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("文件类型不能为空"); + // if (!AllowedTypes.Contains(value.ToLower())) + // throw new ArgumentException($"不支持的文件类型: {value}"); + Value = value.ToLower(); + } + public static FileType Create(string value) => new(value); + public static implicit operator string(FileType type) => type.Value; +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/LogManagement/Aggregates/LogEntry.cs b/backend/src/UniversalAdminSystem.Domian/LogManagement/Aggregates/LogEntry.cs new file mode 100644 index 0000000000000000000000000000000000000000..b45f15d1128a5226103d29cc2e07a1ff5a23ba11 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/LogManagement/Aggregates/LogEntry.cs @@ -0,0 +1,51 @@ +using UniversalAdminSystem.Domian.Core; +using UniversalAdminSystem.Domian.Core.ValueObjects; + +namespace UniversalAdminSystem.Domian.LogManagement.Aggregates; + +/// +/// 日志条目聚合根 +/// +public class LogEntry : AggregateRoot +{ + public Guid Id { get; private set; } + public string Level { get; private set; } + public string Message { get; private set; } + public string Source { get; private set; } + public UserId? UserId { get; private set; } + public DateTime Timestamp { get; private set; } + public string? Context { get; private set; } + public string? Exception { get; private set; } + + private LogEntry() { } + + public static LogEntry Create( + string level, + string message, + string source, + UserId? userId = null, + string? context = null, + string? exception = null) + { + if (string.IsNullOrWhiteSpace(level)) + throw new ArgumentException("日志级别不能为空", nameof(level)); + + if (string.IsNullOrWhiteSpace(message)) + throw new ArgumentException("日志消息不能为空", nameof(message)); + + if (string.IsNullOrWhiteSpace(source)) + throw new ArgumentException("日志来源不能为空", nameof(source)); + + return new LogEntry + { + Id = Guid.NewGuid(), + Level = level, + Message = message, + Source = source, + UserId = userId, + Timestamp = DateTime.UtcNow, + Context = context, + Exception = exception + }; + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/LogManagement/IRepository/ILogEntryRepository.cs b/backend/src/UniversalAdminSystem.Domian/LogManagement/IRepository/ILogEntryRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..02fe2d612d01be58032bdf63a6546f2e0eca5d03 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/LogManagement/IRepository/ILogEntryRepository.cs @@ -0,0 +1,12 @@ +using UniversalAdminSystem.Domian.Core.Interfaces; +using UniversalAdminSystem.Domian.LogManagement.Aggregates; + +namespace UniversalAdminSystem.Domian.LogManagement.IRepository; + +public interface ILogEntryRepository : IRepository +{ + Task> GetByLevelAsync(string level); + Task> GetByUserAsync(Guid userId); + Task> GetByDateRangeAsync(DateTime start, DateTime end); + Task> GetBySourceAsync(string source); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/PermissionManagement/Aggregate/Permission.cs b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/Aggregate/Permission.cs new file mode 100644 index 0000000000000000000000000000000000000000..3b6d879229d1f7f16a2b26ddc2cfdc6536c2247e --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/Aggregate/Permission.cs @@ -0,0 +1,220 @@ +using UniversalAdminSystem.Domian.PermissionManagement.Services; +using UniversalAdminSystem.Domian.PermissionManagement.ValueObjects; +using UniversalAdminSystem.Domian.Core; +using UniversalAdminSystem.Domian.Core.Events; +using UniversalAdminSystem.Domian.Core.ValueObjects; + +namespace UniversalAdminSystem.Domian.PermissionManagement.Aggregate; + +/// +/// 权限实体,一个具体的权限项 +/// +public class Permission : AggregateRoot +{ + /// + /// 唯一标识 + /// + public Guid PermissionId { get; private set; } + + /// + /// 权限编码(不能为空,不变性):提供机器可读的权限标识,用于程序中的权限检查 + /// + public PermissionCode Code { get; private init; } = null!; + + /// + /// 权限名称(不能为空) + /// + public PermissionName Name { get; private set; } = null!; + + /// + /// 描述:提供人类可读的权限描述,用于界面展示 + /// + public string? Description { get; private set; } + + /// + /// 类型:分类权限,便于管理和使用 + /// + public PermissionType PermissionType { get; private set; } + + /// + /// 作用资源:指明该权限控制的是哪个资源 + /// + public PermissionResource Resource { get; private set; } = null!; + + /// + /// 操作类型:指明对资源的具体操作 + /// + public PermissionAction Action { get; private set; } + + /// + /// 是否为系统权限(默认为false):系统权限不可删除,不可修改 + /// + public bool IsSystem { get; private set; } = false; + + /// + /// 创建时间:创建权限时的当前事件,不可修改 + /// + public DateTime CreateTime { get; private set; } = DateTime.UtcNow; + + /// + /// 更新时间:更新权限时的当前时间,每次更新权限时都会更新 + /// + public DateTime UpdateTime { get; private set; } = DateTime.UtcNow; + + + /// + /// 无参构造函数,供EFCore使用 + /// + private Permission() { } + + /// + /// 私有构造函数供工厂使用 + /// + /// + /// + private Permission( + Guid permissionId, + PermissionCode code, + PermissionName name, + string? description, + PermissionType permissionType, + PermissionResource resource, + PermissionAction action, + bool isSystem, + DateTime createTime, + DateTime updateTime + ) + { + PermissionId = permissionId; + Code = code; + Name = name; + Description = description; + PermissionType = permissionType; + Resource = resource; + Action = action; + IsSystem = isSystem; + CreateTime = createTime; + UpdateTime = updateTime; + } + + /// + /// 根据权限作用资源获取权限类型 + /// + /// + /// + private static PermissionType GetTypeByResource(string resource) + { + return resource switch + { + string r when r.StartsWith("data.", StringComparison.OrdinalIgnoreCase) => PermissionType.DATA, + string r when r.StartsWith("user.", StringComparison.OrdinalIgnoreCase) => PermissionType.USER, + string r when r.StartsWith("file.", StringComparison.OrdinalIgnoreCase) => PermissionType.FILE, + string r when r.StartsWith("enum.", StringComparison.OrdinalIgnoreCase) => PermissionType.ENUN, + string r when r.StartsWith("system.", StringComparison.OrdinalIgnoreCase) => PermissionType.SYSTEM, + _ => PermissionType.OTHER, + }; + } + + /// + /// 创建标准权限的工厂方法 + /// + /// + /// + /// + /// + /// + public static Permission CreateStandardPermission( + string name, + string resource, + int actionValue, + string? description = null + ) + { + // 校验数据完整性 + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("权限名称不能为空", nameof(name)); + if (string.IsNullOrWhiteSpace(resource)) + throw new ArgumentException("作用资源不能为空", nameof(resource)); + if (!Enum.IsDefined(typeof(PermissionAction), actionValue)) + throw new ArgumentException("无效的操作类型值", nameof(actionValue)); + + if (!ResourceActionValidator.IsValidCombination(resource, actionValue, out var permResource, out var permAction)) + throw new ArgumentException("资源与操作类型组合不合法"); + + // 显示转换类型,内部校验格式 + PermissionCode permCode = (PermissionCode)$"{resource}:{permAction}"; + PermissionName permName = (PermissionName)name; + PermissionType permType = GetTypeByResource(resource); + + // 创建标准权限 + var permission = new Permission( + Guid.NewGuid(), + permCode, + permName, + string.IsNullOrWhiteSpace(description) ? "暂无描述" : description, + permType, + permResource, + permAction, + false, + DateTime.UtcNow, + DateTime.UtcNow + ); + + return permission; + } + + /// + /// 创建系统权限的工厂方法 + /// + /// + /// + /// + /// + /// + public static Permission CreateSystemPermission( + string name, + string resource, + int actionValue, + string? description = null + ) + { + var perm = CreateStandardPermission(name, resource, actionValue, description); + perm.IsSystem = true; + return perm; + } + + /// + /// 修改权限名称 + /// + /// + /// + public void UpdateName(string name) + { + Name = PermissionName.Create(name); + UpdateTime = DateTime.Now; + } + + /// + /// 修改权限描述 + /// + /// + /// + public void UpdateDescription(string? description = null) + { + // 允许传入null表示清空权限描述 + if (string.IsNullOrWhiteSpace(description)) + { + Description = "暂无描述"; + return; + } + + // 参数非null才进行规则校验 + if (description.Length > 50) + { + throw new ArgumentException("权限描述长度不能超过50字符", nameof(description)); + } + + Description = description; + UpdateTime = DateTime.Now; + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/PermissionManagement/Aggregate/Role.cs b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/Aggregate/Role.cs new file mode 100644 index 0000000000000000000000000000000000000000..56b9148d8290a6aaf0d73e9f98c5df8240c7fdbb --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/Aggregate/Role.cs @@ -0,0 +1,186 @@ +using UniversalAdminSystem.Domian.Core; +using UniversalAdminSystem.Domian.Core.ValueObjects; +using UniversalAdminSystem.Domian.PermissionManagement.ValueObjects; + +namespace UniversalAdminSystem.Domian.PermissionManagement.Aggregate; + +public class Role : AggregateRoot +{ + public RoleId RoleId { get; private set; } = null!; + public RoleName Name { get; private set; } = null!; + public RoleDescription? Description { get; private set; } + public bool IsSystem { get; private set; } = false; + public bool IsSupper { get; private set; } = false; + public DateTime CreateTime { get; private set; } = DateTime.UtcNow; + public DateTime UpdateTime { get; private set; } = DateTime.UtcNow; + private readonly List _permissions = []; + public IReadOnlyCollection Permissions => _permissions.AsReadOnly(); + public IReadOnlyCollection PermissionIds => _permissions.Select(p => p.PermissionId).ToList().AsReadOnly(); + + private Role() { } + + private Role(RoleName name, RoleDescription? description = null, bool isSystem = false, bool isSupper = false) + { + RoleId = RoleId.Create(Guid.NewGuid()); + Name = name; + Description = description ?? RoleDescription.Create("暂无描述"); + IsSystem = isSystem; + IsSupper = isSupper; + CreateTime = DateTime.UtcNow; + UpdateTime = DateTime.UtcNow; + _permissions = new List(); + } + + /// + /// 角色工厂方法(支持描述、系统标记、超级管理员标记) + /// + public static Role Create(string name, string? description = null, bool isSystem = false, bool isSupper = false) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("角色名称不能为空"); + var role = new Role(RoleName.Create(name), string.IsNullOrWhiteSpace(description) ? null : RoleDescription.Create(description), isSystem, isSupper); + + return role; + } + + /// + /// 设置名称 + /// + /// + /// + public void SetName(string name) + { + Name = RoleName.Create(name); + } + + /// + /// 设置描述 + /// + /// + public void SetDescription(string? description) + { + Description = string.IsNullOrWhiteSpace(description) ? null : RoleDescription.Create(description); + } + + /// + /// 标记为超级管理员 + /// + public void MarkAsSupper() + { + IsSupper = true; + } + + /// + /// 添加权限 + /// + /// 权限实体 + /// + public void AddPermission(Permission permission) + { + if (permission == null) + throw new ArgumentException("权限不能为空", nameof(permission)); + + if (!HasPermission(permission.PermissionId)) + { + _permissions.Add(permission); + UpdateTime = DateTime.UtcNow; + + // 添加权限分配事件 + } + } + + /// + /// 移除权限 + /// + /// 权限实体 + /// + /// + /// + public void RemovePermission(Permission permission) + { + if (permission == null) + throw new ArgumentException("权限不能为空", nameof(permission)); + + var existingPermission = _permissions.FirstOrDefault(p => p.PermissionId == permission.PermissionId); + if (existingPermission != null) + { + _permissions.Remove(existingPermission); + UpdateTime = DateTime.UtcNow; + + // 添加权限移除事件 + } + } + + /// + /// 批量分配权限 + /// + public void AddPermissions(IEnumerable permissions) + { + foreach (var permission in permissions) + { + AddPermission(permission); + } + } + + /// + /// 批量移除权限 + /// + public void RemovePermissions(IEnumerable permissions) + { + foreach (var permission in permissions) + { + RemovePermission(permission); + } + } + + /// + /// 校验角色是否拥有某权限 + /// + public bool HasPermission(Guid permissionId) + { + return _permissions.Any(p => p.PermissionId == permissionId); + } + + /// + /// 校验角色是否拥有全部指定权限 + /// + public bool HasAllPermissions(IEnumerable permissionIds) + { + return permissionIds.All(pid => _permissions.Any(p => p.PermissionId == pid)); + } + + /// + /// 校验角色是否拥有任意指定权限 + /// + public bool HasAnyPermission(IEnumerable permissionIds) + { + return permissionIds.Any(pid => _permissions.Any(p => p.PermissionId == pid)); + } + + /// + /// 检查角色是否有指定权限编码(通过权限ID匹配) + /// + /// 权限编码 + /// 对应的权限ID + /// 是否有权限 + public bool HasPermission(string permissionCode, Guid permissionId) + { + // 这里需要根据权限编码找到对应的权限ID + // 在实际使用中,可能需要通过权限服务来获取权限ID + return HasPermission(permissionId); + } + + /// + /// 清空所有权限 + /// + public void ClearPermissions() + { + _permissions.Clear(); + UpdateTime = DateTime.UtcNow; + } + + /// + /// 获取权限数量 + /// + public int PermissionCount => _permissions.Count; +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/PermissionManagement/Exceptions/PermissionDomainException.cs b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/Exceptions/PermissionDomainException.cs new file mode 100644 index 0000000000000000000000000000000000000000..9f1965eb273e6929b204a895344898f45a2d884e --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/Exceptions/PermissionDomainException.cs @@ -0,0 +1,8 @@ +namespace UniversalAdminSystem.Domian.PermissionManagement.Exceptions; + +public class PermissionDomainException : Exception +{ + public PermissionDomainException(string message) : base(message) { } + + public PermissionDomainException(string message, Exception innerException) : base(message, innerException) { } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/PermissionManagement/IRepository/IPermissionRepository.cs b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/IRepository/IPermissionRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..930892f5f10a585ece9d942c20bd85047dc7cf00 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/IRepository/IPermissionRepository.cs @@ -0,0 +1,55 @@ +using UniversalAdminSystem.Domian.Core.Interfaces; +using UniversalAdminSystem.Domian.PermissionManagement.Aggregate; +using UniversalAdminSystem.Domian.PermissionManagement.ValueObjects; + +namespace UniversalAdminSystem.Domian.PermissionManagement.IRepository; + +/// +/// 权限仓储接口 +/// 提供权限数据的持久化操作 +/// +public interface IPermissionRepository : IRepository +{ + /// + /// 批量添加系统权限 + /// + /// 系统权限集合 + /// 添加操作结果 + Task AddSystemPermissionsAsync(IEnumerable systemPermissions); + + /// + /// 根据权限ID集合获取权限集合 + /// + /// 权限ID集合 + /// 权限集合 + Task> GetByIdsAsync(IEnumerable permissionIds); + + /// + /// 根据权限编码获取权限 + /// + /// 权限编码 + /// 权限实体,如果不存在则返回null + Task GetByCodeAsync(PermissionCode permissionCode); + + /// + /// 根据条件查询权限 + /// + /// 权限名称(可选) + /// 权限资源(可选) + /// 权限操作类型(可选) + /// 权限类型(可选) + /// 符合条件的权限集合 + Task> QueryAsync( + string? name = null, + string? resource = null, + int? action = null, + PermissionType? type = null + ); + + /// + /// 根据权限编码判断权限是否存在 + /// + /// 权限编码 + /// 权限是否存在 + Task ExistsAsync(PermissionCode permissionCode); +} diff --git a/backend/src/UniversalAdminSystem.Domian/PermissionManagement/IRepository/IRoleRepository.cs b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/IRepository/IRoleRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..d70e25575d6aac7b9593bf9665049bb0d761dbf6 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/IRepository/IRoleRepository.cs @@ -0,0 +1,52 @@ +using UniversalAdminSystem.Domian.Core.Interfaces; +using UniversalAdminSystem.Domian.PermissionManagement.Aggregate; + +namespace UniversalAdminSystem.Domian.PermissionManagement.IRepository; + +/// +/// 角色仓储接口 +/// 提供角色数据的持久化操作 +/// +public interface IRoleRepository : IRepository +{ + /// + /// 根据角色名称查找角色 + /// + /// 角色名称 + /// 角色实体,如果不存在则返回null + Task GetByNameAsync(string roleName); + + /// + /// 根据角色ID集合获取角色列表 + /// + /// 角色ID集合 + /// 角色集合 + Task> GetByIdsAsync(IEnumerable roleIds); + + /// + /// 查询角色下的所有权限ID + /// + /// 角色ID + /// 权限ID集合 + Task> GetPermissionIdsAsync(Guid roleId); + + /// + /// 根据角色名称判断角色是否存在 + /// + /// 角色名称 + /// 角色是否存在 + Task ExistsAsync(string roleName); + + /// + /// 获取角色及其权限(包含权限导航属性) + /// + /// 角色ID + /// 角色实体,如果不存在则返回null + Task GetRoleWithPermissionsAsync(Guid roleId); + + /// + /// 获取所有角色及其权限 + /// + /// 角色集合 + Task> GetAllRolesWithPermissionsAsync(); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/PermissionManagement/Interfaces/IAssignPermissionDomainService.cs b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/Interfaces/IAssignPermissionDomainService.cs new file mode 100644 index 0000000000000000000000000000000000000000..e398a930d550461c132e5562dbee02e739d3495b --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/Interfaces/IAssignPermissionDomainService.cs @@ -0,0 +1,42 @@ +using UniversalAdminSystem.Domian.PermissionManagement.Aggregate; + +namespace UniversalAdminSystem.Domian.PermissionManagement.Interfaces; + +/// +/// 权限分配领域服务接口 +/// 提供权限分配给角色的领域业务逻辑 +/// +public interface IAssignPermissionDomainService +{ + /// + /// 为角色分配权限 + /// + /// 权限实体 + /// 角色实体 + /// 分配是否成功 + bool AssignPermission(Permission permission, Role role); + + /// + /// 为角色移除权限 + /// + /// 权限实体 + /// 角色实体 + /// 移除是否成功 + bool RemovePermission(Permission permission, Role role); + + /// + /// 批量分配权限 + /// + /// 权限集合 + /// 角色实体 + /// 分配是否成功 + bool AssignPermissions(IEnumerable permissions, Role role); + + /// + /// 批量移除权限 + /// + /// 权限集合 + /// 角色实体 + /// 移除是否成功 + bool RemovePermissions(IEnumerable permissions, Role role); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/PermissionManagement/Services/AssignPermissionDomainService.cs b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/Services/AssignPermissionDomainService.cs new file mode 100644 index 0000000000000000000000000000000000000000..a1b27c0d9f06546ced7cfbf0de33600824f12e1f --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/Services/AssignPermissionDomainService.cs @@ -0,0 +1,87 @@ +using UniversalAdminSystem.Domian.PermissionManagement.Aggregate; +using UniversalAdminSystem.Domian.PermissionManagement.Interfaces; + +namespace UniversalAdminSystem.Domian.PermissionManagement.Services; + +/// +/// 权限分配领域服务实现 +/// 提供权限分配给角色的领域业务逻辑实现 +/// +public class AssignPermissionDomainService : IAssignPermissionDomainService +{ + /// + /// 为角色分配权限 + /// + /// 权限实体 + /// 角色实体 + /// 分配是否成功 + public bool AssignPermission(Permission permission, Role role) + { + try + { + if (role == null) + throw new InvalidOperationException("权限分配失败!该角色不存在"); + + if (permission == null) + throw new InvalidOperationException("权限分配失败!该权限不存在"); + + role.AddPermission(permission); + return true; + } + catch (ArgumentException) + { + throw; + } + } + + public bool RemovePermission(Permission permission, Role role) + { + try + { + if (role == null) + throw new InvalidOperationException("权限移除失败!该角色不存在"); + if (permission == null) + throw new InvalidOperationException("权限移除失败!该权限不存在"); + role.RemovePermission(permission); + return true; + } + catch (ArgumentException) + { + throw; + } + } + + public bool AssignPermissions(IEnumerable permissions, Role role) + { + try + { + if (role == null) + throw new InvalidOperationException("权限分配失败!该角色不存在"); + if (permissions == null) + throw new InvalidOperationException("权限分配失败!权限集合为空"); + role.AddPermissions(permissions); + return true; + } + catch (ArgumentException) + { + throw; + } + } + + public bool RemovePermissions(IEnumerable permissions, Role role) + { + try + { + if (role == null) + throw new InvalidOperationException("权限移除失败!该角色不存在"); + if (permissions == null) + throw new InvalidOperationException("权限移除失败!权限集合为空"); + role.RemovePermissions(permissions); + return true; + } + catch (ArgumentException) + { + throw; + } + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/PermissionManagement/Services/ResourceActionValidator.cs b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/Services/ResourceActionValidator.cs new file mode 100644 index 0000000000000000000000000000000000000000..28c9901b5e61c91366ec5b9a15c0991e64f56ab0 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/Services/ResourceActionValidator.cs @@ -0,0 +1,52 @@ +using UniversalAdminSystem.Domian.PermissionManagement.Exceptions; +using UniversalAdminSystem.Domian.PermissionManagement.ValueObjects; + +namespace UniversalAdminSystem.Domian.PermissionManagement.Services; + +/// +/// 资源-操作组合校验器 +/// 职责:验证资源与操作类型的组合是否合法 +/// +public class ResourceActionValidator +{ + private static readonly object _lock = new object(); + private static Dictionary> _validResources = new(); + + /// + /// 加载权限规则配置 + /// + public static void LoadRules(Dictionary> rules) + { + lock (_lock) + { + _validResources = rules.ToDictionary(kv => kv.Key, kv => kv.Value.ToHashSet()); + } + } + + /// + /// 获取当前配置的规则 + /// + public static Dictionary> GetCurrentRules() + { + lock (_lock) + { + return _validResources.ToDictionary(kv => kv.Key, kv => kv.Value.ToHashSet()); + } + } + + /// + /// 验证资源-操作组合有效性 + /// + public static bool IsValidCombination(string resource, int actionValue, out PermissionResource permSource, out PermissionAction permAction) + { + lock (_lock) + { + permSource = PermissionResource.Create(resource); + var resourceModule = permSource.GetModule(); + if (!Enum.TryParse(actionValue.ToString(), out PermissionAction validAction)) + throw new PermissionDomainException("无效的操作类型值"); + permAction = validAction; + return _validResources.TryGetValue(validAction, out var validModules) && validModules.Contains(resourceModule); + } + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/PermissionManagement/ValueObjects/PermissionAction.cs b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/ValueObjects/PermissionAction.cs new file mode 100644 index 0000000000000000000000000000000000000000..b8fdaa27b97fae0872218077da8403efa78c19e2 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/ValueObjects/PermissionAction.cs @@ -0,0 +1,70 @@ +namespace UniversalAdminSystem.Domian.PermissionManagement.ValueObjects; + +public enum PermissionAction +{ + // 基础操作 + Read, // 读取资源 + Create, // 创建资源 + Update, // 更新资源 + Delete, // 删除资源 + + // 管理操作 + Manage, // 完全管理 + Approve, // 审批操作 + Audit, // 审计访问 + + // 特殊操作 + Execute, // 执行操作 + Import, // 导入数据 + Export, // 导出数据 + Restore, // 恢复数据 + Archive, // 归档数据 + + // 可访问文档权限等级 + Public, //公开 + Restricted, //受限 + Private //私有 + +} + +/// +/// 扩展方法静态类 +/// +public static class PermissionActionExtensions +{ + /// + /// 获取操作类型的分类 + /// + /// + /// + public static string GetCategory(this PermissionAction action) + { + return action switch + { + PermissionAction.Read or + PermissionAction.Create or + PermissionAction.Update or + PermissionAction.Delete + => "Basic", + + PermissionAction.Manage or + PermissionAction.Approve or + PermissionAction.Audit + => "Management", + + _ + => "Special" + }; + } + + /// + /// 检查操作是否允许批量执行 + /// + /// + /// + public static bool AllowBulk(this PermissionAction action) + { + return action != PermissionAction.Manage && + action != PermissionAction.Audit; + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/PermissionManagement/ValueObjects/PermissionCode.cs b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/ValueObjects/PermissionCode.cs new file mode 100644 index 0000000000000000000000000000000000000000..2f782ec7a52657bc572d056373420428b230015e --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/ValueObjects/PermissionCode.cs @@ -0,0 +1,89 @@ +using System.Text.RegularExpressions; + +namespace UniversalAdminSystem.Domian.PermissionManagement.ValueObjects; + +public sealed partial class PermissionCode +{ + public string Value { get; private set; } + + private PermissionCode(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentException("权限编码不能为空", nameof(value)); + + if (!PermissionRegex().IsMatch(value)) + throw new ArgumentException("权限编码格式错误,应为'资源:操作'格式", nameof(value)); + + Value = value; + } + + [GeneratedRegex(@"^([a-z]+(\.[a-z]+(_[a-z]+)*){0,4})+:[a-zA-Z0-9]+$")] + private static partial Regex PermissionRegex(); + + /// + /// 工厂方法 + /// + /// + /// + public static PermissionCode Create(string value) {System.Console.WriteLine($"value: {value}"); return new PermissionCode(value);} + + /// + /// 隐式转换为string + /// + /// + public static implicit operator string(PermissionCode code) => code.Value; + + /// + /// 显式转换为PermissionCode,带验证逻辑 + /// + /// + public static explicit operator PermissionCode(string value) => Create(value); + + /// + /// 比较两个权限编码是否相等 + /// + /// + /// + public bool Equals(PermissionCode? other) => + other is not null && Value == other.Value; + + /// + /// 比较两个权限编码是否相等 + /// + /// + /// + public override bool Equals(object? obj) => + obj is PermissionCode other && Equals(other); + + /// + /// 获取哈希值 + /// + /// + public override int GetHashCode() => Value.GetHashCode(); + + /// + /// 获取字符串表示形式 + /// + /// + public override string ToString() => Value; + + /// + /// 比较两个权限编码是否相等 + /// + /// + /// + /// + public static bool operator ==(PermissionCode? left, PermissionCode? right) => + Equals(left, right); + + /// + /// 比较两个权限编码是否不相等 + /// + /// + /// + /// + public static bool operator !=(PermissionCode? left, PermissionCode? right) => + !Equals(left, right); + + +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/PermissionManagement/ValueObjects/PermissionName.cs b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/ValueObjects/PermissionName.cs new file mode 100644 index 0000000000000000000000000000000000000000..dc1587a10baac89b4ed86fd90873f046c53a95c8 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/ValueObjects/PermissionName.cs @@ -0,0 +1,54 @@ +using System.Text.RegularExpressions; + +namespace UniversalAdminSystem.Domian.PermissionManagement.ValueObjects; + +public sealed partial class PermissionName +{ + public string Value { get; private set; } = null!; + + private PermissionName(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("权限名称不能为空", nameof(value)); + } + + if (!MyRegex().IsMatch(value)) + { + throw new ArgumentException("权限名称只能包含中文、英文、数字、下划线和连字符", nameof(value)); + } + + if (value.Length > 20) + { + throw new ArgumentException("权限名称长度不能超过20个字符", nameof(value)); + } + + Value = value; + } + + /// + /// 正则表达式:格式校验规则 + /// + /// + [GeneratedRegex(@"^[\p{IsCJKUnifiedIdeographs}a-zA-Z0-9_-]{1,20}$")] + private static partial Regex MyRegex(); + + /// + /// 工厂方法 + /// + /// + /// + public static PermissionName Create(string value) => new(value); + + /// + /// 隐式转换为string + /// + /// + public static implicit operator string(PermissionName name) => name.Value; + + /// + /// 显式转换为PermissionName,带验证逻辑 + /// + /// + public static explicit operator PermissionName(string value) => Create(value); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/PermissionManagement/ValueObjects/PermissionResource.cs b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/ValueObjects/PermissionResource.cs new file mode 100644 index 0000000000000000000000000000000000000000..bbed0984593bb53dcf8266376890a06356483b86 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/ValueObjects/PermissionResource.cs @@ -0,0 +1,66 @@ +using System.Text.RegularExpressions; + +namespace UniversalAdminSystem.Domian.PermissionManagement.ValueObjects; + +public partial class PermissionResource +{ + public string Value { get; private set; } = null!; + + private PermissionResource(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("资源名称不能为空", nameof(value)); + } + + if (!ENRegex().IsMatch(value)) + { + throw new ArgumentException("资源名称只能包含英文(小写)、下划线,以 “.” 结构分层,最多支持五层。例如:hantsune.m_i_k_u或初音.未_来", nameof(value)); + } + + Value = value; + } + + // [GeneratedRegex(@"^[\p{IsCJKUnifiedIdeographs}]+(\.[\p{IsCJKUnifiedIdeographs}]+(_[\p{IsCJKUnifiedIdeographs}]+)*){1,4}$", RegexOptions.Compiled)] + // private static partial Regex CNRegex(); + + [GeneratedRegex(@"^[a-z]+(\.[a-z]+(_[a-z]+)*){0,4}$", RegexOptions.Compiled)] + private static partial Regex ENRegex(); + + /// + /// 工厂方法 + /// + /// + /// + public static PermissionResource Create(string value) => new(value); + + /// + /// 隐式转换string + /// + /// + public static implicit operator string(PermissionResource resource) => resource.Value; + + /// + /// 显示转换PermissionResource,带验证逻辑 + /// + /// + public static explicit operator PermissionResource(string resource) => Create(resource); + + /// + /// 解析资源层级 + /// + /// + public IReadOnlyList GetResourceParts() + { + return Value.Split('.'); + } + + /// + /// 获取顶级模块 + /// + /// + public string GetModule() + { + return Value.Split('.')[0]; + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/PermissionManagement/ValueObjects/PermissionType.cs b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/ValueObjects/PermissionType.cs new file mode 100644 index 0000000000000000000000000000000000000000..c43aa166029586cfa7429f4b2486baa23d2f693f --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/ValueObjects/PermissionType.cs @@ -0,0 +1,34 @@ +namespace UniversalAdminSystem.Domian.PermissionManagement.ValueObjects; + +public enum PermissionType +{ + /// + /// 菜单 + /// + ENUN, + + /// + /// 数据 + /// + DATA, + + /// + /// 文件 + /// + FILE, + + /// + /// 用户 + /// + USER, + + /// + /// 系统 + /// + SYSTEM, + + /// + /// 其他 + /// + OTHER +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/PermissionManagement/ValueObjects/RoleDescription.cs b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/ValueObjects/RoleDescription.cs new file mode 100644 index 0000000000000000000000000000000000000000..e13d84bb45f233f4ea51dfac6657b23af84df482 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/ValueObjects/RoleDescription.cs @@ -0,0 +1,14 @@ +namespace UniversalAdminSystem.Domian.PermissionManagement.ValueObjects; + +public record RoleDescription +{ + public string? Value { get; } + private RoleDescription(string? value) + { + if (value?.Length > 200) + throw new ArgumentException("角色描述不能超过200个字符"); + Value = value; + } + public static RoleDescription Create(string? value) => new(value); + public static implicit operator string?(RoleDescription desc) => desc.Value; +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/PermissionManagement/ValueObjects/RoleName.cs b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/ValueObjects/RoleName.cs new file mode 100644 index 0000000000000000000000000000000000000000..76ffbe4886584c3ef0a6fe27cad3375c1e969bb3 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/ValueObjects/RoleName.cs @@ -0,0 +1,18 @@ +namespace UniversalAdminSystem.Domian.PermissionManagement.ValueObjects; + +public record RoleName +{ + public string Value { get; private set; } + private RoleName(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentException("角色名称不能为空"); + if (value.Length > 50) + throw new ArgumentException("角色名称不能超过50个字符"); + Value = value; + } + public static RoleName Create(string value) => new(value); + + public static explicit operator RoleName(string value) => Create(value); + public static implicit operator string(RoleName name) => name.Value; +} \ No newline at end of file diff --git "a/backend/src/UniversalAdminSystem.Domian/PermissionManagement/\346\235\203\351\231\220\347\256\241\347\220\206\344\270\212\344\270\213\346\226\207.txt" "b/backend/src/UniversalAdminSystem.Domian/PermissionManagement/\346\235\203\351\231\220\347\256\241\347\220\206\344\270\212\344\270\213\346\226\207.txt" new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/src/UniversalAdminSystem.Domian/SystemSettings/Aggregates/SystemSetting.cs b/backend/src/UniversalAdminSystem.Domian/SystemSettings/Aggregates/SystemSetting.cs new file mode 100644 index 0000000000000000000000000000000000000000..f59732e0d8f07e2a6ee01632ec122b85f077a75e --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/SystemSettings/Aggregates/SystemSetting.cs @@ -0,0 +1,36 @@ +using System; +using UniversalAdminSystem.Domian.Core; +using UniversalAdminSystem.Domian.SystemSettings.ValueObjects; + +namespace UniversalAdminSystem.Domian.SystemSettings.Aggregates; + +public class SystemSetting : AggregateRoot +{ + public Guid Id { get; private set; } + public SettingKey Key { get; private set; } = null!; + public SettingValue Value { get; private set; } = null!; + public SettingDescription? Description { get; private set; } + public string? Group { get; private set; } + public DateTime CreateTime { get; private set; } + public DateTime UpdateTime { get; private set; } + + private SystemSetting() { } + public static SystemSetting Create(SettingKey key, SettingValue value, SettingDescription? description = null, string? group = null) + { + return new SystemSetting + { + Id = Guid.NewGuid(), + Key = key, + Value = value, + Description = description, + Group = group, + CreateTime = DateTime.UtcNow, + UpdateTime = DateTime.UtcNow + }; + } + public void UpdateValue(SettingValue value) + { + Value = value; + UpdateTime = DateTime.UtcNow; + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/SystemSettings/IRepository/ISystemSettingRepository.cs b/backend/src/UniversalAdminSystem.Domian/SystemSettings/IRepository/ISystemSettingRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..97078d9c338c70aacd14f2b2d064f1efc37e2a94 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/SystemSettings/IRepository/ISystemSettingRepository.cs @@ -0,0 +1,10 @@ +using UniversalAdminSystem.Domian.Core.Interfaces; +using UniversalAdminSystem.Domian.SystemSettings.Aggregates; + +namespace UniversalAdminSystem.Domian.SystemSettings.IRepository; + +public interface ISystemSettingRepository : IRepository +{ + Task GetByKeyAsync(string key); + Task> GetByGroupAsync(string group); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/SystemSettings/ValueObjects/SettingDescription.cs b/backend/src/UniversalAdminSystem.Domian/SystemSettings/ValueObjects/SettingDescription.cs new file mode 100644 index 0000000000000000000000000000000000000000..f7ac9953b2b60a893e8a12fbc38c123ae5f8b004 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/SystemSettings/ValueObjects/SettingDescription.cs @@ -0,0 +1,14 @@ +namespace UniversalAdminSystem.Domian.SystemSettings.ValueObjects; + +public record SettingDescription +{ + public string? Value { get; } + private SettingDescription(string? value) + { + if (value?.Length > 200) + throw new ArgumentException("设置描述不能超过200字符"); + Value = value; + } + public static SettingDescription Create(string? value) => new(value); + public static implicit operator string?(SettingDescription desc) => desc.Value; +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/SystemSettings/ValueObjects/SettingKey.cs b/backend/src/UniversalAdminSystem.Domian/SystemSettings/ValueObjects/SettingKey.cs new file mode 100644 index 0000000000000000000000000000000000000000..eda4e827f7212dca148eaf1a239af8ee530384bc --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/SystemSettings/ValueObjects/SettingKey.cs @@ -0,0 +1,16 @@ +namespace UniversalAdminSystem.Domian.SystemSettings.ValueObjects; + +public record SettingKey +{ + public string Value { get; } + private SettingKey(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentException("设置Key不能为空"); + if (value.Length > 100) + throw new ArgumentException("设置Key不能超过100字符"); + Value = value; + } + public static SettingKey Create(string value) => new(value); + public static implicit operator string(SettingKey key) => key.Value; +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/SystemSettings/ValueObjects/SettingValue.cs b/backend/src/UniversalAdminSystem.Domian/SystemSettings/ValueObjects/SettingValue.cs new file mode 100644 index 0000000000000000000000000000000000000000..fed3c2820c7219e56fdf664bf3188ebf0d93fd92 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/SystemSettings/ValueObjects/SettingValue.cs @@ -0,0 +1,16 @@ +namespace UniversalAdminSystem.Domian.SystemSettings.ValueObjects; + +public record SettingValue +{ + public string Value { get; } + private SettingValue(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentException("设置值不能为空"); + if (value.Length > 500) + throw new ArgumentException("设置值不能超过500字符"); + Value = value; + } + public static SettingValue Create(string value) => new(value); + public static implicit operator string(SettingValue val) => val.Value; +} \ No newline at end of file diff --git "a/backend/src/UniversalAdminSystem.Domian/SystemSettings/\347\263\273\347\273\237\350\256\276\347\275\256\344\270\212\344\270\213\346\226\207.txt" "b/backend/src/UniversalAdminSystem.Domian/SystemSettings/\347\263\273\347\273\237\350\256\276\347\275\256\344\270\212\344\270\213\346\226\207.txt" new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/src/UniversalAdminSystem.Domian/UniversalAdminSystem.Domian.csproj b/backend/src/UniversalAdminSystem.Domian/UniversalAdminSystem.Domian.csproj new file mode 100644 index 0000000000000000000000000000000000000000..fa71b7ae6a34999a3f96c40d9a0b870b311d11dd --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/UniversalAdminSystem.Domian.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/backend/src/UniversalAdminSystem.Domian/UserConversations/Aggregates/Conversations.cs b/backend/src/UniversalAdminSystem.Domian/UserConversations/Aggregates/Conversations.cs new file mode 100644 index 0000000000000000000000000000000000000000..8efcd71d4b5b0db3c053106b4ead6ab422732c43 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/UserConversations/Aggregates/Conversations.cs @@ -0,0 +1,45 @@ +using System.Threading.Tasks; +using UniversalAdminSystem.Domian.Core.ValueObjects; + +namespace UniversalAdminSystem.Domian.UserConversations.Aggregates; + +public class Conversations +{ + public ConversationId Id { get; private set; } = null!; + + public UserId UserId { get; private set; } = null!; + + public string? Title { get; private set; } + + public DateTime CreateDate { get; private set; } + + public DateTime UpdateDate { get; private set; } + + protected Conversations() { } + + private Conversations(ConversationId id, UserId userid, string? title, DateTime createDate, DateTime updateDate) + { + Id = id; + UserId = userid; + Title = title; + CreateDate = createDate; + UpdateDate = updateDate; + } + + public static Conversations Create(Guid userid, string? title) + { + if (userid == Guid.Empty) throw new ArgumentException("用户id不能为空"); + return new Conversations( + ConversationId.Create(Guid.NewGuid()), + UserId.Create(userid), + title, + DateTime.UtcNow, + DateTime.UtcNow + ); + } + + public void UpdateUpdateDate(DateTime time) + { + UpdateDate = time; + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/UserConversations/Aggregates/Message.cs b/backend/src/UniversalAdminSystem.Domian/UserConversations/Aggregates/Message.cs new file mode 100644 index 0000000000000000000000000000000000000000..1e4c6ccdac3e60e8ffdbde38a239be6a862825e9 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/UserConversations/Aggregates/Message.cs @@ -0,0 +1,41 @@ +using UniversalAdminSystem.Domian.Core.ValueObjects; + +namespace UniversalAdminSystem.Domian.UserConversations.Aggregates; + +public class Message +{ + public MessageId Id { get; private set; } = null!; + + public ConversationId ConversationId { get; private set; } = null!; + + public string Role { get; private set; } = null!; + + public string Content { get; private set; } = null!; + + public DateTime CreateDate { get; private set; } + + protected Message() { } + + private Message(MessageId id, ConversationId conversationId, string role, string content, DateTime time) + { + Id = id; + ConversationId = conversationId; + Role = role; + Content = content; + CreateDate = time; + + } + + public static Message Create(ConversationId conversationId, string role, string content) + { + if (role == null || role.Length == 0) throw new ArgumentException("角色不得为空"); + if (content == null || content.Length == 0) throw new ArgumentException("内容不得为空"); + return new Message( + MessageId.Create(Guid.NewGuid()), + conversationId, + role, + content, + DateTime.UtcNow + ); + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/UserConversations/IRepository/IConversationsRepository.cs b/backend/src/UniversalAdminSystem.Domian/UserConversations/IRepository/IConversationsRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..d9e3b4929145947b7b30de765ba587f50b6bd187 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/UserConversations/IRepository/IConversationsRepository.cs @@ -0,0 +1,29 @@ +using UniversalAdminSystem.Domian.Core.Interfaces; +using UniversalAdminSystem.Domian.UserConversations.Aggregates; + +namespace UniversalAdminSystem.Domian.UserConversations.IRepository; + +public interface IConversationsRepository : IRepository +{ + /// + /// 获取所有用户Id的会话 + /// + /// + /// + Task> GetByUserIdAsync(Guid userId); + + /// + /// 删除所有用户id的会话 + /// + /// + /// + Task RemoveByUserIdAsync(Guid userId); + + + /// + /// 删除会话及相关消息 + /// + /// + /// + Task RemoveConversation(Guid ConversationId); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/UserConversations/IRepository/IMessageRepository.cs b/backend/src/UniversalAdminSystem.Domian/UserConversations/IRepository/IMessageRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..faa6bebec544b489fa967dacd4f9b30e2019664b --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/UserConversations/IRepository/IMessageRepository.cs @@ -0,0 +1,21 @@ +using UniversalAdminSystem.Domian.Core.Interfaces; +using UniversalAdminSystem.Domian.UserConversations.Aggregates; + +namespace UniversalAdminSystem.Domian.UserConversations.IRepository; + +public interface IMessageRepository : IRepository +{ + /// + /// 获取某个会话的所有消息 + /// + /// + /// + Task> GetByConversationIdAsync(Guid conversationId); + + /// + /// 删除某个会话的所有消息 + /// + /// + /// + Task RemoveByConversationIdAsync(Guid conversationId); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/UserManagement/Aggregates/User.cs b/backend/src/UniversalAdminSystem.Domian/UserManagement/Aggregates/User.cs new file mode 100644 index 0000000000000000000000000000000000000000..4d36edf9a608c1c5f913073932b5a364d8690639 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/UserManagement/Aggregates/User.cs @@ -0,0 +1,123 @@ +using System.Dynamic; +using System.Text.RegularExpressions; +using UniversalAdminSystem.Domian.Core; +using UniversalAdminSystem.Domian.Core.ValueObjects; +using UniversalAdminSystem.Domian.UserManagement.ValueObj; + +namespace UniversalAdminSystem.Domian.UserManagement.Aggregates; + +public partial class User : AggregateRoot +{ + public UserId UserId { get; private set; } = null!; + public UserAccount Account { get; private set; } = null!; + public string Password { get; private set; } = null!; + public string Salt { get; private set; } = null!; + public UserEmail Email { get; private set; } = null!; + public UserStatus Status { get; private set; } + public UserInfoId? UserInfoId { get; private set; } + + public RoleId? RoleId { get; private set; } + + private User() { } + + private User( + UserInfoId userInfoId, + UserAccount account, + string password, + UserEmail email, + string salt, + UserStatus userStatus, + RoleId? role = null + ) + { + UserId = UserId.Create(Guid.NewGuid()); + UserInfoId = userInfoId; + Account = account; + Password = password; + Email = email; + Salt = salt; + Status = userStatus; + RoleId = role; + } + + public static User CreateUser(UserInfoId userInfoId, string account, string password, string email, string salt, UserStatus userStatus = UserStatus.Normal, Guid? roleId = null) + { + var user = new User( + userInfoId, + UserAccount.Create(account), + password, + UserEmail.Create(email), + salt, + userStatus, + roleId.HasValue ? RoleId.Create(roleId.Value) : null + ); + + return user; + } + + public User UpdateUserEmail(string email) + { + Email = UserEmail.Create(email); + return this; + } + + public User UpdateUserPassword(string password, string salt) + { + if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("密码不得为空"); + if (string.IsNullOrWhiteSpace(salt)) throw new ArgumentException("盐值不得为空"); + Password = password; + Salt = salt; + return this; + } + + public User UpdateUserStatus(UserStatus userStatus) + { + Status = userStatus; + return this; + } + + public User AssignRole(RoleId roleId) + { + if (RoleId != roleId) + { + RoleId = roleId; + } + return this; + } + + public User RemoveRole(RoleId roleId) + { + if (RoleId == roleId) + { + RoleId = null; + } + return this; + } + + public User ChangeStatus(UserStatus newStatus) + { + if (Status != newStatus) + { + var oldStatus = Status; + Status = newStatus; + } + return this; + } + + public User ChangePassword(string newPassword, string newSalt) + { + if (string.IsNullOrWhiteSpace(newPassword)) throw new ArgumentException("密码不得为空"); + Password = newPassword; + Salt = newSalt; + return this; + } + + public User SoftDelete() + { + Status = UserStatus.Deleted; + return this; + } + + [GeneratedRegex(@"^[a-zA-Z][a-zA-Z0-9_]{5,19}$", RegexOptions.IgnoreCase, "zh-CN")] + private static partial Regex MyRegex(); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/UserManagement/Entities/UserInfo.cs b/backend/src/UniversalAdminSystem.Domian/UserManagement/Entities/UserInfo.cs new file mode 100644 index 0000000000000000000000000000000000000000..cbea2c35c2fa3605b60aad55ced23dcb976fa6c3 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/UserManagement/Entities/UserInfo.cs @@ -0,0 +1,77 @@ +using UniversalAdminSystem.Domian.UserManagement.ValueObj; +using UniversalAdminSystem.Domian.Core.ValueObjects; + +namespace UniversalAdminSystem.Domian.UserManagement.Entities; + +public class UserInfo +{ + /// + /// 信息唯一标识 + /// + public UserInfoId UserInfoId { get; private set; } = null!; + + /// + /// 用户性别 + /// + public UserGender? Gender { get; private set; } + + /// + /// 用户年龄 + /// + public int? Age { get; private set; } + + /// + /// 用户地址 + /// + public string? Address { get; private set; } + + /// + /// 用户姓名 + /// + public string? Name { get; private set; } + + /// + /// 用户电话 + /// + public string? Phone { get; private set; } + + /// + /// 供Efcore使用 + /// + private UserInfo() { } + + /// + /// 创建用户信息的工厂方法 + /// + /// 姓名 + /// 性别 + /// 电话 + /// 地址 + /// + public static UserInfo CreateUserInfo(string? name = null, UserGender? gender = null, string? phone = null, string? address = null) + { + var userInfo = new UserInfo + { + UserInfoId = UserInfoId.Create(Guid.NewGuid()), + Name = name, + Gender = gender, + Phone = phone, + Address = address + }; + return userInfo; + } + + public UserInfo UpdateUserInfo(UserGender? gender, int? age, string? addrss) + { + if (age < 0) + { + throw new ArgumentException("出现了!时间穿越!(年龄错误)"); + } + + Gender = gender; + Age = age; + Address = addrss; + + return this; + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/UserManagement/IRepository/IUserInfoRepository.cs b/backend/src/UniversalAdminSystem.Domian/UserManagement/IRepository/IUserInfoRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..4fee19dbfedbf220584331efd7912b0e1e37905b --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/UserManagement/IRepository/IUserInfoRepository.cs @@ -0,0 +1,11 @@ +using UniversalAdminSystem.Domian.Core.Interfaces; +using UniversalAdminSystem.Domian.UserManagement.Entities; + +namespace UniversalAdminSystem.Domian.UserManagement.IRepository; + +/// +/// 用户信息仓储接口,提供特定于用户信息的额外方法 +/// +public interface IUserInfoRepository : IRepository +{ +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/UserManagement/IRepository/IUserRepository.cs b/backend/src/UniversalAdminSystem.Domian/UserManagement/IRepository/IUserRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..28e0fb88fa645341023a6b68f4e90dae4f8dfdae --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/UserManagement/IRepository/IUserRepository.cs @@ -0,0 +1,39 @@ +using UniversalAdminSystem.Domian.Core.Interfaces; +using UniversalAdminSystem.Domian.UserManagement.Aggregates; +using UniversalAdminSystem.Domian.UserManagement.Entities; + +namespace UniversalAdminSystem.Domian.UserManagement.IRepository; + +/// +/// 用户仓储接口,提供特定于User的额外方法 +/// +public interface IUserRepository : IRepository +{ + /// + /// 用户软删除 + /// + /// + /// + Task DeleteUserSoftAsync(Guid id); + + /// + /// 通过账号查询用户 + /// + /// + /// + Task GetUserByAccountAsync(string account); + + /// + /// 通过邮箱查询用户 + /// + /// + /// + Task GetUserByEmailAsync(string email); + + /// + /// 添加用户信息 + /// + /// + /// + Task AddUserInfoAsync(UserInfo userInfo); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/UserManagement/ValueObj/UserAccount.cs b/backend/src/UniversalAdminSystem.Domian/UserManagement/ValueObj/UserAccount.cs new file mode 100644 index 0000000000000000000000000000000000000000..f811ef33a8a8f41138945a9e6cc78bd01c1d9504 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/UserManagement/ValueObj/UserAccount.cs @@ -0,0 +1,16 @@ +namespace UniversalAdminSystem.Domian.UserManagement.ValueObj; + +public record UserAccount +{ + public string Value { get; } + private UserAccount(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentException("账号不能为空"); + if (!System.Text.RegularExpressions.Regex.IsMatch(value, @"^[a-zA-Z0-9_]{6,20}$")) + throw new ArgumentException("账号格式不正确,账号长度为6-20字符"); + Value = value; + } + public static UserAccount Create(string value) => new(value); + public static implicit operator string(UserAccount account) => account.Value; +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/UserManagement/ValueObj/UserEmail.cs b/backend/src/UniversalAdminSystem.Domian/UserManagement/ValueObj/UserEmail.cs new file mode 100644 index 0000000000000000000000000000000000000000..69fb5fe5a0e50ada2f1800db650faa3172cbf937 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/UserManagement/ValueObj/UserEmail.cs @@ -0,0 +1,16 @@ +namespace UniversalAdminSystem.Domian.UserManagement.ValueObj; + +public record UserEmail +{ + public string Value { get; } + private UserEmail(string value) + { + if (string.IsNullOrWhiteSpace(value)) + throw new ArgumentException("邮箱不能为空"); + if (!System.Text.RegularExpressions.Regex.IsMatch(value, @"^[^@]+@[^@]+\.[^@]+$")) + throw new ArgumentException("邮箱格式不正确"); + Value = value; + } + public static UserEmail Create(string value) => new(value); + public static implicit operator string(UserEmail email) => email.Value; +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/UserManagement/ValueObj/UserGender.cs b/backend/src/UniversalAdminSystem.Domian/UserManagement/ValueObj/UserGender.cs new file mode 100644 index 0000000000000000000000000000000000000000..38f5d0f5156a18a475281b8c5d3f523e7f9638c1 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/UserManagement/ValueObj/UserGender.cs @@ -0,0 +1,7 @@ +namespace UniversalAdminSystem.Domian.UserManagement.ValueObj; + +public enum UserGender +{ + Man, + Female +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/UserManagement/ValueObj/UsersStatus.cs b/backend/src/UniversalAdminSystem.Domian/UserManagement/ValueObj/UsersStatus.cs new file mode 100644 index 0000000000000000000000000000000000000000..697696217c9a95cfcd3749d3338711ee47f30f49 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/UserManagement/ValueObj/UsersStatus.cs @@ -0,0 +1,9 @@ +namespace UniversalAdminSystem.Domian.UserManagement.ValueObj; + +public enum UserStatus +{ + Normal, + Freeze, + Logout, + Deleted +} \ No newline at end of file diff --git "a/backend/src/UniversalAdminSystem.Domian/UserManagement/\347\224\250\346\210\267\347\256\241\347\220\206\344\270\212\344\270\213\346\226\207.txt" "b/backend/src/UniversalAdminSystem.Domian/UserManagement/\347\224\250\346\210\267\347\256\241\347\220\206\344\270\212\344\270\213\346\226\207.txt" new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/src/UniversalAdminSystem.Domian/knowledge/Aggregates/DocumentChunk.cs b/backend/src/UniversalAdminSystem.Domian/knowledge/Aggregates/DocumentChunk.cs new file mode 100644 index 0000000000000000000000000000000000000000..8dbefeccaf78c4c7ebebcbadd11ee452cad24b44 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/knowledge/Aggregates/DocumentChunk.cs @@ -0,0 +1,50 @@ +using System.ComponentModel.DataAnnotations.Schema; +using UniversalAdminSystem.Domian.Core; +using UniversalAdminSystem.Domian.Core.ValueObjects; +using UniversalAdminSystem.Domian.FileStorage.ValueObjects; +using UniversalAdminSystem.Domian.knowledge.ValueObj; + +namespace UniversalAdminSystem.Domian.knowledge.Aggregates; + + +public class DocumentChunk : AggregateRoot +{ + public ChunkId Id { get; private set; } = null!; + + public FileId FileId { get; private set; } = null!; + + public string Content { get; private set; } = string.Empty; + + public TextEmbedding Embedding { get; private set; } = null!; + [NotMapped] // EF Core 不会映射到表 + public double SimilarityScore { get; set; } + public FileAccessLevel Level { get; private set; } + + protected DocumentChunk() { } + + private DocumentChunk(ChunkId id, FileId fileId, string content, TextEmbedding embedding, FileAccessLevel level) + { + Id = id; + FileId = fileId; + Content = content; + Embedding = embedding; + Level = level; + } + + public static DocumentChunk CreateDocumentChunk(ChunkId id, FileId fileId, string content, TextEmbedding embedding, FileAccessLevel level = FileAccessLevel.Public) + { + if (string.IsNullOrWhiteSpace(content)) + { + throw new ArgumentException("内容不能为空"); + } + + if (embedding == null) + { + throw new ArgumentException("嵌入向量不能为空"); + } + + return new DocumentChunk(id, fileId, content, embedding, level); + } + + +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/knowledge/IRepository/IDocumentChunkRepository.cs b/backend/src/UniversalAdminSystem.Domian/knowledge/IRepository/IDocumentChunkRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..33617901bbf720c4d07c79f8dd03973e314a6d8c --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/knowledge/IRepository/IDocumentChunkRepository.cs @@ -0,0 +1,41 @@ +using UniversalAdminSystem.Domian.Core.Interfaces; +using UniversalAdminSystem.Domian.FileStorage.ValueObjects; +using UniversalAdminSystem.Domian.knowledge.Aggregates; +using UniversalAdminSystem.Domian.knowledge.ValueObj; + +namespace UniversalAdminSystem.Domian.knowledge.IRepository; + +public interface IDocumentChunkRepository : IRepository +{ + /// + /// 数据库中匹配余弦值的前topK个相似文档 + /// + /// + /// + /// + /// + Task> FindSimilarDocumentsAsync(TextEmbedding queryEmbedding, FileAccessLevel level, int topK = 5); + + /// + /// 批量文件id删除 + /// + /// + /// + Task BatchDeleteDocumentChunkAsync(Guid Id); + + /// + /// 以id批量修改文本块等级 + /// + /// + /// + /// + Task BatchModificationChunkLevelAsync(Guid FileId, FileAccessLevel level); + + /// + /// 批量添加文本块 + /// + /// + /// + Task BulkAddDocumentChunkAsync(IEnumerable chunks); + Task ExistsByFileIdAsync(Guid fileId); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Domian/knowledge/ValueObj/TextEmbedding.cs b/backend/src/UniversalAdminSystem.Domian/knowledge/ValueObj/TextEmbedding.cs new file mode 100644 index 0000000000000000000000000000000000000000..ad08a0d0dea7aeef861010de26af3421b3487222 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Domian/knowledge/ValueObj/TextEmbedding.cs @@ -0,0 +1,20 @@ +namespace UniversalAdminSystem.Domian.knowledge.ValueObj; + +public record TextEmbedding +{ + public float[] Value { get; init; } + + private TextEmbedding(float[] value) + { + if (value == null) + { + throw new ArgumentException("向量不得为空"); + } + + Value = value; + } + + public static TextEmbedding Create(float[] Vector) => new(Vector); + + +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Auth/JwtTokenBuilder.cs b/backend/src/UniversalAdminSystem.Infrastructure/Auth/JwtTokenBuilder.cs new file mode 100644 index 0000000000000000000000000000000000000000..d12bae52e05256bb655bee946d1cc9f137f1735b --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Auth/JwtTokenBuilder.cs @@ -0,0 +1,50 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using UniversalAdminSystem.Infrastructure.Configs; + +namespace UniversalAdminSystem.Infrastructure.Auth; + +public class JwtTokenBuilder +{ + private readonly JwtConfig _jwtConfig; + + public JwtTokenBuilder(IOptions options) + { + _jwtConfig = options.Value; + } + + public string BuildToken(List claims, int? expireHours = null) + { + Console.WriteLine($"开始构建JWT Token,Claims数量: {claims.Count}"); + Console.WriteLine($"JWT密钥长度: {_jwtConfig.Key.Length}"); + + if (string.IsNullOrEmpty(_jwtConfig.Key)) + { + throw new InvalidOperationException("JWT密钥不能为空"); + } + + if (_jwtConfig.Key.Length < 16) + { + throw new InvalidOperationException($"JWT密钥长度不足,当前长度: {_jwtConfig.Key.Length},建议至少16位"); + } + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtConfig.Key)); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var token = new JwtSecurityToken( + issuer: _jwtConfig.Issuer, + audience: _jwtConfig.Audience, + claims: claims, + expires: DateTime.UtcNow.AddHours(expireHours ?? _jwtConfig.ExpireHours), + signingCredentials: creds + ); + + var tokenString = new JwtSecurityTokenHandler().WriteToken(token); + Console.WriteLine($"JWT Token生成成功,长度: {tokenString.Length}"); + + return tokenString; + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Auth/JwtTokenService.cs b/backend/src/UniversalAdminSystem.Infrastructure/Auth/JwtTokenService.cs new file mode 100644 index 0000000000000000000000000000000000000000..cebcfecf5851303581ca7b39f211b519745181a5 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Auth/JwtTokenService.cs @@ -0,0 +1,88 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using UniversalAdminSystem.Application.Authentication.Interfaces; +using UniversalAdminSystem.Domian.UserManagement.ValueObj; + +namespace UniversalAdminSystem.Infrastructure.Auth; + +public class JwtTokenService : IJwtTokenService +{ + private readonly JwtTokenBuilder _builder; + + public JwtTokenService(JwtTokenBuilder builder) + { + _builder = builder; + } + + public string GenerateToken(string userId, string roleId, UserStatus status) + { + var claims = new List + { + new Claim(ClaimTypes.NameIdentifier, userId), + new Claim(ClaimTypes.Role, roleId), + new Claim(ClaimTypes.StateOrProvince, status.ToString()) + }; + + return _builder.BuildToken(claims); + } + +public (string userId, string roleId, UserStatus status) ParseToken(string token) +{ + try + { + Console.WriteLine($"开始解析Token,长度: {token.Length}"); + + // 检查token是否为空 + if (string.IsNullOrEmpty(token)) + { + throw new ArgumentException("Token不能为空"); + } + + // 检查token格式 + var parts = token.Split('.'); + if (parts.Length != 3) + { + throw new ArgumentException($"Token格式错误,包含{parts.Length}个部分,应该是3个部分"); + } + + var handler = new JwtSecurityTokenHandler(); + + // 检查token是否可以读取 + if (!handler.CanReadToken(token)) + { + throw new ArgumentException("Token格式不正确,无法读取"); + } + + var jwt = handler.ReadJwtToken(token); + Console.WriteLine($"JWT解析成功,Claims数量: {jwt.Claims.Count()}"); + + var userId = jwt.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value ?? ""; + var roleId = jwt.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Role)?.Value ?? ""; + var status = jwt.Claims.FirstOrDefault(c => c.Type == ClaimTypes.StateOrProvince)?.Value ?? ""; + + Console.WriteLine($"JWT解析 - 用户ID: {userId}, 角色ID: {roleId}, 状态: {status}"); + + if (string.IsNullOrEmpty(status)) + { + throw new ArgumentException("Token中缺少用户状态信息"); + } + + return (userId, roleId, (UserStatus)Enum.Parse(typeof(UserStatus), status)); + } + catch (Exception ex) + { + Console.WriteLine($"JWT解析失败: {ex.Message}"); + Console.WriteLine($"异常类型: {ex.GetType().Name}"); + throw; + } +} + + public string RefreshToken(string token, out string userId, out string roleId, out UserStatus status) + { + var (_userId, _roleId, _status) = ParseToken(token); + userId = _userId; + roleId = _roleId; + status = _status; + return GenerateToken(userId, roleId, status); + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Configs/AllowedFileConfig.cs b/backend/src/UniversalAdminSystem.Infrastructure/Configs/AllowedFileConfig.cs new file mode 100644 index 0000000000000000000000000000000000000000..86986c94541578e2fc8550dc321222ac56eacf1f --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Configs/AllowedFileConfig.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace UniversalAdminSystem.Infrastructure.Configs; + +public class AllowedFileConfig +{ + [property: JsonPropertyName("Mime")] + public string Mime { get; set; } = string.Empty; + + [property: JsonPropertyName("Ext")] + public string Ext { get; set; } = string.Empty; + + [property: JsonPropertyName("Signature")] + public string Signature { get; set; } = string.Empty; +} + +public class AllowedFilesConfig +{ + [property: JsonPropertyName("AllowedFiles")] + public IEnumerable AllowedFiles { get; set; } = new List(); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Configs/JwtConfig.cs b/backend/src/UniversalAdminSystem.Infrastructure/Configs/JwtConfig.cs new file mode 100644 index 0000000000000000000000000000000000000000..d433712e933976a8cc9d5e527f2674b669e2d713 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Configs/JwtConfig.cs @@ -0,0 +1,9 @@ +namespace UniversalAdminSystem.Infrastructure.Configs; + +public class JwtConfig +{ + public string Key { get; set; } = string.Empty; // 密钥 + public string Issuer { get; set; } = string.Empty; // 发布者 + public string Audience { get; set; } = string.Empty; // 受众 + public int ExpireHours { get; set; } = 2; // 过期时间 +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Configs/K2Config.cs b/backend/src/UniversalAdminSystem.Infrastructure/Configs/K2Config.cs new file mode 100644 index 0000000000000000000000000000000000000000..35fff4ba54ebaa1639fe77142578bb76cd706dd2 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Configs/K2Config.cs @@ -0,0 +1,93 @@ +using System.Text.Json.Serialization; + +namespace UniversalAdminSystem.Infrastructure.Configs; + +public class K2Config +{ + public string BaseUrl { get; set; } = string.Empty; + public string ApiKey { get; set; } = string.Empty; +} + +public class K2Request +{ + public string Model { get; set; } = string.Empty; + public List Messages { get; set; } = new(); + public float? Temperature { get; set; } + public int? MaxTokens { get; set; } + public bool Stream { get; set; } = true; + public StreamOptions StreamOptions { get; set; } = new(); +} + +public class StreamOptions +{ + public bool IncludeUsage { get; set; } = true; +} + +public class K2Message +{ + public string Role { get; set; } = string.Empty; + public string Content { get; set; } = string.Empty; // +} + +public class K2Response +{ + public string Id { get; set; } = string.Empty; + public string Object { get; set; } = string.Empty; + public long Created { get; set; } + public string Model { get; set; } = string.Empty; + public List Choices { get; set; } = new(); + public K2Usage Usage { get; set; } = new(); +} + +public class K2Choice +{ + public int Index { get; set; } + public K2Message Message { get; set; } = new(); + public string FinishReason { get; set; } = string.Empty; +} + +public class K2Usage +{ + public int PromptTokens { get; set; } + public int CompletionTokens { get; set; } + public int TotalTokens { get; set; } +} + +public class K2StreamResponse +{ + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("object")] + public string Object { get; set; } = string.Empty; + + [JsonPropertyName("created")] + public long Created { get; set; } + + [JsonPropertyName("model")] + public string Model { get; set; } = string.Empty; + + [JsonPropertyName("choices")] + public List Choices { get; set; } = new(); +} + +public class K2StreamChoice +{ + [JsonPropertyName("index")] + public int Index { get; set; } + + [JsonPropertyName("delta")] + public K2StreamDelta Delta { get; set; } = new(); + + [JsonPropertyName("finish_reason")] + public string? FinishReason { get; set; } +} + +public class K2StreamDelta +{ + [JsonPropertyName("role")] + public string? Role { get; set; } + + [JsonPropertyName("content")] + public string? Content { get; set; } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Configs/RabbitMqConfig.cs b/backend/src/UniversalAdminSystem.Infrastructure/Configs/RabbitMqConfig.cs new file mode 100644 index 0000000000000000000000000000000000000000..edd279c4abd2deea20cd1452603fa2b445e5dcb3 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Configs/RabbitMqConfig.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace UniversalAdminSystem.Infrastructure.Configs; + +public class RabbitMqConfig +{ + [property: JsonPropertyName("Host")] + public string Host { get; set; } = string.Empty; + + [property: JsonPropertyName("Port")] + public int Port { get; set; } + + [property: JsonPropertyName("Username")] + public string Username { get; set; } = string.Empty; + + [property: JsonPropertyName("Password")] + public string Password { get; set; } = string.Empty; + + [property: JsonPropertyName("Exchange")] + public string Exchange { get; set; } = string.Empty; + + [property: JsonPropertyName("Queue")] + public string Queue { get; set; } = string.Empty; + + [property: JsonPropertyName("RoutingKey")] + public string RoutingKey { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Configs/SystemPermissionConfig.cs b/backend/src/UniversalAdminSystem.Infrastructure/Configs/SystemPermissionConfig.cs new file mode 100644 index 0000000000000000000000000000000000000000..7d49f2e25078041a90123dc5e55e9b93d64e1200 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Configs/SystemPermissionConfig.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace UniversalAdminSystem.Infrastructure.Configs; + +/// +/// 系统权限配置类,映射配置文件 +/// +/// +/// +/// +public sealed record SystemPermissionConfig( + [property: JsonPropertyName("Resource")] + string Resource, + + [property: JsonPropertyName("Action"), Required] + string Action, + + [property: JsonPropertyName("Name"), Required] + string Name +); + +/// +/// 系统权限配置根类,映射配置文件 +/// +public sealed class SystemPermissionConfigRoot +{ + [JsonPropertyName("SystemPermissions")] + public required IReadOnlyCollection Permissions { get; init; } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Configs/TongyiConfig.cs b/backend/src/UniversalAdminSystem.Infrastructure/Configs/TongyiConfig.cs new file mode 100644 index 0000000000000000000000000000000000000000..a0a0f2ea831aaf34990be9544fc9de376c799a68 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Configs/TongyiConfig.cs @@ -0,0 +1,7 @@ +namespace UniversalAdminSystem.Infrastructure.Configs; + +public class TongyiConfig +{ + public string BaseUrl { get; set; } = string.Empty; + public string ApiKey { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/DependencyInject/AddApplicationService.cs b/backend/src/UniversalAdminSystem.Infrastructure/DependencyInject/AddApplicationService.cs new file mode 100644 index 0000000000000000000000000000000000000000..4a969f4b1900fbb3cbda5530afcddd3e0826a105 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/DependencyInject/AddApplicationService.cs @@ -0,0 +1,101 @@ +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using UniversalAdminSystem.Application.Common.Interfaces; +using UniversalAdminSystem.Application.UserManagement.Interface; +using UniversalAdminSystem.Application.UserManagement.Service; +using UniversalAdminSystem.Infrastructure.Persistence.Transaction; +using UniversalAdminSystem.Application.Authentication.Interfaces; +using UniversalAdminSystem.Application.Authentication.Service; +using FluentValidation; +using UniversalAdminSystem.Application.PermissionManagement.Services; +using UniversalAdminSystem.Application.PermissionManagement.Interfaces; +using UniversalAdminSystem.Application.LogManagement.Interfaces; +using UniversalAdminSystem.Application.LogManagement.Services; +using UniversalAdminSystem.Application.SystemSettings.Interfaces; +using UniversalAdminSystem.Application.SystemSettings.Services; +using UniversalAdminSystem.Application.FileStorage.Interfaces; +using UniversalAdminSystem.Application.FileStorage.Services; +using UniversalAdminSystem.Application.AIQuestions.Interfaces; +using UniversalAdminSystem.Application.AIQuestions.Service; +using UniversalAdminSystem.Infrastructure.Configs; + +namespace UniversalAdminSystem.Infrastructure.DependencyInject; + +public static class AddApplicationService +{ + /// + /// 自动扫描并批量注册服务接口及其实现(支持接口和实现分布在不同程序集,约定接口名为IXXXService,实现名为XXXService) + /// + public static IServiceCollection AddServiceByConvention( + this IServiceCollection services, + Assembly[] interfaceAssemblies, + Assembly[] implementationAssemblies) + { + var interfaces = interfaceAssemblies + .SelectMany(a => a.GetTypes()) + .Where(t => t.IsInterface && t.Name.EndsWith("Service")); + var implementations = implementationAssemblies + .SelectMany(a => a.GetTypes()) + .Where(t => t.IsClass && !t.IsAbstract && t.Name.EndsWith("Service")); + + foreach (var iface in interfaces) + { + var impl = implementations.FirstOrDefault(t => iface.IsAssignableFrom(t)); + if (impl != null) + { + services.AddScoped(iface, impl); + } + } + return services; + } + + public static IServiceCollection AddApplication(this IServiceCollection services, IConfiguration configuration) + { + // 自动注册应用服务 + services.AddServiceByConvention( + new Assembly[] + { + typeof(IUserManagementAppService).Assembly, + typeof(ILoginAppService).Assembly, + typeof(IFileAppService).Assembly, + typeof(ILogManagementAppService).Assembly, + typeof(ISystemSettingAppService).Assembly, + typeof(IPermissionManagementAppService).Assembly, + typeof(IAIQusetionsAppService).Assembly + }, + new Assembly[] + { + typeof(UserManagementAppService).Assembly, + typeof(LoginAppService).Assembly, + typeof(FileAppService).Assembly, + typeof(LogManagementAppService).Assembly, + typeof(SystemSettingAppService).Assembly, + typeof(PermissionManagementAppService).Assembly, + typeof(AIQusetionsAppService).Assembly + } + ); + + // 手动注册应用服务(确保所有服务都被注册) + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + // 注册用户权限集成服务 + // services.AddScoped(); + + // 注册JWT配置 + services.Configure(configuration.GetSection("Jwt")); + + // 注册工作单元 + services.AddScoped(); + + return services; + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/DependencyInject/AddDomainService.cs b/backend/src/UniversalAdminSystem.Infrastructure/DependencyInject/AddDomainService.cs new file mode 100644 index 0000000000000000000000000000000000000000..79cf9637f9205ee0605d47eb52ca4c904f1bf931 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/DependencyInject/AddDomainService.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.DependencyInjection; +using UniversalAdminSystem.Domian; +using UniversalAdminSystem.Domian.PermissionManagement.Services; +using UniversalAdminSystem.Domian.FileStorage.Services; +using UniversalAdminSystem.Domian.PermissionManagement.Interfaces; + +namespace UniversalAdminSystem.Infrastructure.DependencyInject; + +public static class AddDomainService +{ + public static IServiceCollection AddDomain(this IServiceCollection services) + { + // 自动发现并注册领域服务 + var list = DomainServices.DiscoverServices(); + foreach (var (serviceType, implementationType) in list) + { + services.AddScoped(serviceType, implementationType); + } + + // 手动注册领域服务(确保所有领域服务都被注册) + services.AddScoped(); + services.AddScoped(); + + return services; + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/DependencyInject/AddInfrastructureService.cs b/backend/src/UniversalAdminSystem.Infrastructure/DependencyInject/AddInfrastructureService.cs new file mode 100644 index 0000000000000000000000000000000000000000..18f400e7939b3c4ec1f71d1e550ff5febe7b8c84 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/DependencyInject/AddInfrastructureService.cs @@ -0,0 +1,210 @@ +using System.Reflection; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using UniversalAdminSystem.Application.UserManagement.Interface; +using UniversalAdminSystem.Domian.PermissionManagement.IRepository; +using UniversalAdminSystem.Domian.UserManagement.IRepository; +using UniversalAdminSystem.Domian.FileStorage.IRepository; +using UniversalAdminSystem.Domian.LogManagement.IRepository; +using UniversalAdminSystem.Domian.SystemSettings.IRepository; +using UniversalAdminSystem.Domian.Core.Interfaces; +using UniversalAdminSystem.Domian.LogManagement.Aggregates; +using UniversalAdminSystem.Domian.SystemSettings.Aggregates; +using UniversalAdminSystem.Infrastructure.Auth; +using UniversalAdminSystem.Infrastructure.Persistence.DbContexts; +using UniversalAdminSystem.Infrastructure.Persistence.Repositories; +using UniversalAdminSystem.Infrastructure.Services; +using UniversalAdminSystem.Application.PermissionManagement.Interfaces; +using UniversalAdminSystem.Application.PermissionManagement.Services; +using UniversalAdminSystem.Application.Authentication.Interfaces; +using UniversalAdminSystem.Infrastructure.FileStorage; +using FluentValidation; +using UniversalAdminSystem.Domian.UserConversations.IRepository; +using UniversalAdminSystem.Domian.knowledge.IRepository; +using UniversalAdminSystem.Application.FileStorage.Interfaces; +using UniversalAdminSystem.Application.Common.Interfaces; +using UniversalAdminSystem.Infrastructure.RabbitMQ.Queues; +using UniversalAdminSystem.Infrastructure.RabbitMQ; +using UniversalAdminSystem.Infrastructure.RabbitMQ.Publishers; +using UniversalAdminSystem.Infrastructure.RabbitMQ.Consumers; +using RabbitMQ.Client; +using UniversalAdminSystem.Application.AIQuestions.Interfaces; + +namespace UniversalAdminSystem.Infrastructure.DependencyInject; + +public static class AddInfrastrutureService +{ + /// + /// 自动扫描并批量注册仓储接口及其实现(支持接口和实现分布在不同程序集,约定接口名为IXXXRepository,实现名为XXXRepository) + /// + public static IServiceCollection AddRepositoriesByConvention(this IServiceCollection services, Assembly[] interfaceAssemblies, Assembly[] implementationAssemblies) + { + var interfaces = interfaceAssemblies + .SelectMany(a => a.GetTypes()) + .Where(t => t.IsInterface && t.Name.EndsWith("Repository")); + var implementations = implementationAssemblies + .SelectMany(a => a.GetTypes()) + .Where(t => t.IsClass && !t.IsAbstract && t.Name.EndsWith("Repository")); + + foreach (var iface in interfaces) + { + var impl = implementations.FirstOrDefault(t => iface.IsAssignableFrom(t)); + if (impl != null) + { + services.AddScoped(iface, impl); + } + } + return services; + } + + public static IServiceCollection AddAllConfig(this IServiceCollection services, IConfiguration configuration) + { + var asm = Assembly.GetExecutingAssembly(); + var configTypes = asm.GetTypes().Where(t => t.Name.EndsWith("Config")).ToList(); + + Console.WriteLine($"找到 {configTypes.Count} 个配置类型:"); + foreach (var type in configTypes) + { + Console.WriteLine($" - {type.Name}"); + } + + foreach (var type in configTypes) + { + var sectionName = type.Name.Replace("Config", ""); + Console.WriteLine($"正在注册配置类型: {type.Name} -> 节名: {sectionName}"); + + // 检查配置节是否存在 + var section = configuration.GetSection(sectionName); + Console.WriteLine($" 配置节 {sectionName} 是否存在: {section.Exists()}"); + + if (section.Exists()) + { + // 使用反射调用泛型方法 + var configureMethod = typeof(OptionsConfigurationServiceCollectionExtensions) + .GetMethods() + .FirstOrDefault(m => m.Name == "Configure" && m.IsGenericMethod && m.GetParameters().Length == 2); + + if (configureMethod != null) + { + try + { + var genericMethod = configureMethod.MakeGenericMethod(type); + genericMethod.Invoke(null, new object[] { services, section }); + Console.WriteLine($" 成功注册配置类型: {type.Name}"); + } + catch (Exception ex) + { + Console.WriteLine($" 注册配置类型 {type.Name} 失败: {ex.Message}"); + } + } + else + { + Console.WriteLine($" 未找到 Configure 方法"); + } + } + else + { + Console.WriteLine($" 配置节 {sectionName} 不存在,跳过注册"); + } + } + return services; + } + + public static IServiceCollection AddInfrastruture(this IServiceCollection services, IConfiguration configuration) + { + // 注册数据库上下文 + services.AddDbContext(x => x.UseNpgsql(configuration.GetConnectionString("pgSql"), x => x.UseVector())); + + // 自动注册所有配置 + services.AddAllConfig(configuration); + + // 自动注册所有仓储 + services.AddRepositoriesByConvention( + new Assembly[] + { + typeof(IUserRepository).Assembly, + typeof(IUserInfoRepository).Assembly, + typeof(IPermissionRepository).Assembly, + typeof(IRoleRepository).Assembly, + typeof(IFileRepository).Assembly, + typeof(ILogEntryRepository).Assembly, + typeof(ISystemSettingRepository).Assembly, + typeof(IConversationsRepository).Assembly, + typeof(IDocumentChunkRepository).Assembly, + + }, + new Assembly[] + { + typeof(UserRepository).Assembly, + typeof(UserInfoRepository).Assembly, + typeof(PermissionRepository).Assembly, + typeof(RoleRepository).Assembly, + typeof(FileRepository).Assembly, + typeof(LogEntryRepository).Assembly, + typeof(SystemSettingRepository).Assembly, + } + ); + + // 注册通用IRepository接口到具体实现 + services.AddScoped(typeof(IRepository), typeof(LogEntryRepository)); + services.AddScoped(typeof(IRepository), typeof(SystemSettingRepository)); + + // 注册JWT相关服务 + services.AddSingleton(); + services.AddSingleton(); + services.AddScoped(); + + // 注册ali的HttpClient服务 + services.AddHttpClient(); + // 注册SpaCy的HttpClient服务 + services.AddHttpClient(); + + // 注册K2模型相关服务 + services.AddScoped(); + services.AddScoped(); + + // 注册SpaCy服务 + services.AddScoped(); + + // 注册Embedding服务 + services.AddScoped(); + + // 注册权限相关服务 + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + // 注册缓存服务 + services.AddMemoryCache(); + + // 注册文件存储服务 + services.AddScoped(); + + // 注册权限规则配置服务 + services.AddHostedService(); + services.AddHostedService(); + services.AddSingleton(); + + // // 注册文件处理相关服务 + services.AddScoped(); + services.AddSingleton(sp => + { + var factory = new ConnectionFactory + { + Uri = new Uri(sp.GetRequiredService().GetConnectionString("RabbitMq") + ?? throw new InvalidOperationException("Missing RabbitMq connection string")), + DispatchConsumersAsync = true + }; + return factory.CreateConnection(); + }); + services.AddHostedService(); + services.AddSingleton(); + services.AddHostedService(); + services.AddHostedService(); + + // 注册聊天相关服务 + services.AddScoped(); + return services; + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/DependencyInject/ServiceCollectionExtensions.cs b/backend/src/UniversalAdminSystem.Infrastructure/DependencyInject/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..99b73ed37ebd4d6e73de0fc6ea2c3d7d5197567d --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/DependencyInject/ServiceCollectionExtensions.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace UniversalAdminSystem.Infrastructure.DependencyInject; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddAllService(this IServiceCollection services, IConfiguration configuration) + { + services.AddDomain(); + services.AddApplication(configuration); + services.AddInfrastruture(configuration); + return services; + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/FileStorage/FileValidationService.cs b/backend/src/UniversalAdminSystem.Infrastructure/FileStorage/FileValidationService.cs new file mode 100644 index 0000000000000000000000000000000000000000..621d7a3a936c83ced196df4afe7b819fa6ebe9b9 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/FileStorage/FileValidationService.cs @@ -0,0 +1,109 @@ +using FileSignatures; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using UniversalAdminSystem.Application.FileStorage.Interfaces; +using UniversalAdminSystem.Infrastructure.Configs; + +namespace UniversalAdminSystem.Infrastructure.FileStorage; + +public class FileValidationService : IFileValidationService +{ + private readonly AllowedFilesConfig _allowedFiles; + private readonly FileFormatInspector _fileFormatInspector; + private readonly ILogger _logger; + + public FileValidationService(IOptions allowedFiles, ILogger logger) + { + _allowedFiles = allowedFiles.Value; + _fileFormatInspector = new FileFormatInspector(); + _logger = logger; + } + + public (bool isValid, string message, string? format) ValidateFile(string fileName, long fileSize, Stream stream) + { + try + { + if (stream == null) return (false, "❌ 文件为空", string.Empty); + + // 检查文件大小 + if (fileSize > 100 * 1024 * 1024) return (false, $"❌ 文件大小超过限制:100MB", string.Empty); + _logger.LogInformation($"大小: {fileSize:N0} bytes ({FormatFileSize(fileSize)})"); + + // 检查流状态 + Console.WriteLine($"流状态 - CanRead: {stream.CanRead}, CanSeek: {stream.CanSeek}, Length: {stream.Length}"); + + // 允许的无签名类型映射 + var noSignatureMimeMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { ".txt", new[] { "text/plain" } }, + { ".md", new[] { "text/markdown" } }, + { ".json", new[] { "application/json" } }, + { ".xml", new[] { "application/xml", "text/xml" } } + }; + + // 用 FileSignatures 检测文件签名 + Console.WriteLine("开始检查文件格式..."); + var fileFormat = _fileFormatInspector.DetermineFileFormat(stream); + Console.WriteLine($"文件格式检测结果: {fileFormat?.MediaType ?? "未知"}"); + + Console.WriteLine($"_allowedFiles 是否为空: {_allowedFiles == null}"); + Console.WriteLine($"_allowedFiles.AllowedFiles 是否为空: {_allowedFiles.AllowedFiles == null}"); + Console.WriteLine($"fileFormat.MediaType 是否为空: {string.IsNullOrEmpty(fileFormat?.MediaType)}"); + + + // 有签名 + if (!string.IsNullOrEmpty(fileFormat?.MediaType) && _allowedFiles.AllowedFiles?.Any(f => f.Mime == fileFormat.MediaType) == true) + { + ResetStream(stream); + return (true, $"✅ 文件校验通过: {fileFormat.MediaType}", fileFormat.MediaType); + } + + Console.WriteLine($"✅ 文件名称+扩展名:{fileName}"); + // 无签名(比如 txt/md/json/xml),走扩展名映射 + var ext = Path.GetExtension(fileName); + if (!string.IsNullOrEmpty(ext) && noSignatureMimeMap.TryGetValue(ext, out var mimeList)) + { + if (mimeList.Any(mime => _allowedFiles.AllowedFiles?.Any(f => f.Mime == mime) == true)) + { + ResetStream(stream); + return (true, $"✅ 文件校验通过: {string.Join(", ", mimeList)}", mimeList.First()); + } + } + + return (false, $"❌ 不允许的文件类型: {fileFormat?.MediaType ?? "未知"}", fileFormat?.MediaType); + } + catch (Exception ex) + { + _logger.LogError(ex, "文件验证过程中发生异常"); + return (false, $"❌ 文件验证失败: {ex.Message}", string.Empty); + } + } + + private string FormatFileSize(long bytes) + { + string[] sizes = { "B", "KB", "MB", "GB" }; + double len = bytes; + int order = 0; + while (len >= 1024 && order < sizes.Length - 1) + { + order++; + len = len / 1024; + } + return $"{len:0.##} {sizes[order]}"; + } + private void ResetStream(Stream stream) + { + if (stream.CanSeek) + stream.Position = 0; + } + + // public static bool IsPlainText(Stream stream, int maxBytes = 512) + // { + // stream.Position = 0; + // Span buf = stackalloc byte[maxBytes]; + // int read = stream.Read(buf); + // return !buf[..read].ContainsAny(stackalloc byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 127 }); + // } +} + +public record ValidationResult(bool IsValid, string Format, string Message); \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/FileStorage/LocalFileStorageService.cs b/backend/src/UniversalAdminSystem.Infrastructure/FileStorage/LocalFileStorageService.cs new file mode 100644 index 0000000000000000000000000000000000000000..8caeebe4e9bb06ddd050205346a99c1dc0e148a1 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/FileStorage/LocalFileStorageService.cs @@ -0,0 +1,60 @@ +using System.Collections.Concurrent; +using Microsoft.AspNetCore.Http; +using UniversalAdminSystem.Application.FileStorage.DTOs; +using UniversalAdminSystem.Application.FileStorage.Interfaces; + +namespace UniversalAdminSystem.Infrastructure.FileStorage; + +public class LocalFileStorageService : IFileStorageService +{ + private readonly string _basePath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads"); + private static readonly ConcurrentDictionary _fileIndex = new(); + + public LocalFileStorageService() + { + if (!Directory.Exists(_basePath)) Directory.CreateDirectory(_basePath); + } + + public async Task SaveAsync(IFormFile file) + { + try + { + var uniqueFileName = Guid.NewGuid().ToString() + "_" + file.FileName; + var filePath = Path.Combine(_basePath, uniqueFileName); + using var stream = new FileStream(filePath, FileMode.Create); + await file.CopyToAsync(stream); + return new FileInfo(filePath); + } + catch + { + throw; + } + } + + public async Task DownloadAsync(string fileName) + { + var filePath = Path.Combine(_basePath, fileName); + + if (!File.Exists(filePath)) throw new FileNotFoundException(); + var ms = new MemoryStream(); + using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read)) + { + await fs.CopyToAsync(ms); + } + ms.Position = 0; + return ms; + } + + public Task DeleteAsync(string fileName) + { + var filePath = Path.Combine(_basePath, fileName); + if (File.Exists(filePath)) File.Delete(filePath); + _fileIndex.TryRemove(fileName, out _); + return Task.CompletedTask; + } + + public Task> ListFilesAsync() + { + return Task.FromResult(_fileIndex.Values.AsEnumerable()); + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/FileStorage/Parsers/DocParserFactory.cs b/backend/src/UniversalAdminSystem.Infrastructure/FileStorage/Parsers/DocParserFactory.cs new file mode 100644 index 0000000000000000000000000000000000000000..e35f46478af75275d07d53f16a02e5e65ac5130a --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/FileStorage/Parsers/DocParserFactory.cs @@ -0,0 +1,17 @@ +using UniversalAdminSystem.Application.FileStorage.Interfaces; + +namespace UniversalAdminSystem.Infrastructure.FileStorage.Parsers; + +public class DocParserFactory +{ + public IDocParser GetParser(string mimeType) + { + return mimeType.ToLower() switch + { + "application/pdf" => new PdfParseService(), + "text/markdown" => new MdParseService(), + "text/plain" => new TxtParseService(), + _ => throw new NotImplementedException($"没有为 MIME 类型 '{mimeType}' 实现解析器") + }; + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/FileStorage/Parsers/MdParseService.cs b/backend/src/UniversalAdminSystem.Infrastructure/FileStorage/Parsers/MdParseService.cs new file mode 100644 index 0000000000000000000000000000000000000000..89e9b89f163a4be2caa6e22d3cfacb44f5d324a0 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/FileStorage/Parsers/MdParseService.cs @@ -0,0 +1,34 @@ +using System.Net; +using System.Text.RegularExpressions; +using HtmlAgilityPack; +using Markdig; +using UniversalAdminSystem.Application.FileStorage.Interfaces; + +namespace UniversalAdminSystem.Infrastructure.FileStorage.Parsers; + +public sealed partial class MdParseService : IDocParser +{ + private static readonly Regex _blankLines = MyRegex(); + + public async Task ParseAsync(string path) + { + if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("文件路径不能为空", nameof(path)); + using var reader = new StreamReader(path); + var mdContent = await reader.ReadToEndAsync(); + return Convert(mdContent); + } + + private static string Convert(string markdown) + { + if (string.IsNullOrWhiteSpace(markdown)) return string.Empty; + var html = Markdown.ToHtml(markdown, new MarkdownPipelineBuilder().UseAdvancedExtensions().Build()); + var doc = new HtmlDocument(); + doc.LoadHtml(html); + doc.DocumentNode.SelectNodes("//script|//style|//comment()")?.ToList().ForEach(n => n.Remove()); + var text = WebUtility.HtmlDecode(doc.DocumentNode.InnerText); + return _blankLines.Replace(text.Trim(), "\n\n"); + } + + [GeneratedRegex(@"\r?\n{2,}", RegexOptions.Compiled)] + private static partial Regex MyRegex(); +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/FileStorage/Parsers/PdfParseService.cs b/backend/src/UniversalAdminSystem.Infrastructure/FileStorage/Parsers/PdfParseService.cs new file mode 100644 index 0000000000000000000000000000000000000000..8875eff810059ad1e5ed1173bd45e204ad8979cf --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/FileStorage/Parsers/PdfParseService.cs @@ -0,0 +1,30 @@ +using System.Text; +using iText.Kernel.Pdf; +using iText.Kernel.Pdf.Canvas.Parser; +using UniversalAdminSystem.Application.FileStorage.Interfaces; + +namespace UniversalAdminSystem.Infrastructure.FileStorage.Parsers; + +public class PdfParseService : IDocParser +{ + public async Task ParseAsync(string path) + { + return await Task.Run(() => + { + var text = new StringBuilder(); + using (var pdfReader = new PdfReader(path)) + { + using var pdfDocument = new PdfDocument(pdfReader); + int numberOfPages = pdfDocument.GetNumberOfPages(); + for (int i = 1; i <= numberOfPages; i++) + { + var page = pdfDocument.GetPage(i); + var pageText = PdfTextExtractor.GetTextFromPage(page); + text.Append(pageText); + } + + } + return text.ToString(); + }); + } +} diff --git a/backend/src/UniversalAdminSystem.Infrastructure/FileStorage/Parsers/TxtParseService.cs b/backend/src/UniversalAdminSystem.Infrastructure/FileStorage/Parsers/TxtParseService.cs new file mode 100644 index 0000000000000000000000000000000000000000..d240da3ac5dca835953c4c2328d018e7e841789c --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/FileStorage/Parsers/TxtParseService.cs @@ -0,0 +1,11 @@ +using UniversalAdminSystem.Application.FileStorage.Interfaces; + +namespace UniversalAdminSystem.Infrastructure.FileStorage.Parsers; + +public class TxtParseService : IDocParser +{ + public async Task ParseAsync(string path) + { + return await File.ReadAllTextAsync(path); + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250807030354_RAG1.Designer.cs b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250807030354_RAG1.Designer.cs new file mode 100644 index 0000000000000000000000000000000000000000..9d2ff06b3e1801697a125c3d76c9b627bb78bd4f --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250807030354_RAG1.Designer.cs @@ -0,0 +1,392 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Pgvector; +using UniversalAdminSystem.Infrastructure.Persistence.DbContexts; + +#nullable disable + +namespace UniversalAdminSystem.Infrastructure.Migrations +{ + [DbContext(typeof(UniversalAdminSystemDbContext))] + [Migration("20250807030354_RAG1")] + partial class RAG1 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RolePermissions", b => + { + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("PermissionId") + .HasColumnType("uuid"); + + b.HasKey("RoleId", "PermissionId"); + + b.HasIndex("PermissionId"); + + b.HasIndex("RoleId"); + + b.ToTable("RolePermissions"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.FileStorage.Aggregates.File", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessLevel") + .HasColumnType("integer"); + + b.Property("IsFolder") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("SecurityCheckResult") + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UploadTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.LogManagement.Aggregates.LogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Context") + .HasColumnType("text"); + + b.Property("Exception") + .HasColumnType("text"); + + b.Property("Level") + .IsRequired() + .HasColumnType("text"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("LogEntries"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Permission", b => + { + b.Property("PermissionId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .HasColumnType("integer"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsSystem") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PermissionType") + .HasColumnType("integer"); + + b.Property("Resource") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("PermissionId"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Role", b => + { + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsSupper") + .HasColumnType("boolean"); + + b.Property("IsSystem") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("RoleId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.SystemSettings.Aggregates.SystemSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Group") + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("SystemSettings"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserConversations.Aggregates.Conversations", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("UpdateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Conversations"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserConversations.Aggregates.Message", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserManagement.Aggregates.User", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Account") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserInfoId") + .HasColumnType("uuid"); + + b.HasKey("UserId"); + + b.HasIndex("Account") + .IsUnique(); + + b.HasIndex("RoleId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserManagement.Entities.UserInfo", b => + { + b.Property("UserInfoId") + .HasColumnType("uuid"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("Age") + .HasColumnType("integer"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.HasKey("UserInfoId"); + + b.ToTable("UserInfos"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.knowledge.Aggregates.DocumentChunk", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("Embedding") + .IsRequired() + .HasColumnType("vector(1536)"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Chunks"); + }); + + modelBuilder.Entity("RolePermissions", b => + { + b.HasOne("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Permission", null) + .WithMany() + .HasForeignKey("PermissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserManagement.Aggregates.User", b => + { + b.HasOne("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.SetNull); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250807030354_RAG1.cs b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250807030354_RAG1.cs new file mode 100644 index 0000000000000000000000000000000000000000..326eafebb5644f9830c616bdde212720fe08b257 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250807030354_RAG1.cs @@ -0,0 +1,291 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Pgvector; + +#nullable disable + +namespace UniversalAdminSystem.Infrastructure.Migrations +{ + /// + public partial class RAG1 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterDatabase() + .Annotation("Npgsql:PostgresExtension:vector", ",,"); + + migrationBuilder.CreateTable( + name: "Chunks", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + FileId = table.Column(type: "uuid", nullable: false), + Content = table.Column(type: "text", nullable: false), + Embedding = table.Column(type: "vector(1536)", nullable: false), + Level = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Chunks", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Conversations", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + Title = table.Column(type: "text", nullable: true), + CreateDate = table.Column(type: "timestamp with time zone", nullable: false), + UpdateDate = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Conversations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Files", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Path = table.Column(type: "text", nullable: false), + Size = table.Column(type: "bigint", nullable: false), + Type = table.Column(type: "text", nullable: false), + OwnerId = table.Column(type: "uuid", nullable: false), + UploadTime = table.Column(type: "timestamp with time zone", nullable: false), + IsFolder = table.Column(type: "boolean", nullable: false), + ParentId = table.Column(type: "uuid", nullable: true), + AccessLevel = table.Column(type: "integer", nullable: false), + SecurityCheckResult = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Files", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "LogEntries", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Level = table.Column(type: "text", nullable: false), + Message = table.Column(type: "text", nullable: false), + Source = table.Column(type: "text", nullable: false), + UserId = table.Column(type: "uuid", nullable: true), + Timestamp = table.Column(type: "timestamp with time zone", nullable: false), + Context = table.Column(type: "text", nullable: true), + Exception = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_LogEntries", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Messages", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ConversationId = table.Column(type: "uuid", nullable: false), + Role = table.Column(type: "text", nullable: false), + Content = table.Column(type: "text", nullable: false), + CreateDate = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Messages", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Permissions", + columns: table => new + { + PermissionId = table.Column(type: "uuid", nullable: false), + Code = table.Column(type: "text", nullable: false), + Name = table.Column(type: "text", nullable: false), + Description = table.Column(type: "text", nullable: true), + PermissionType = table.Column(type: "integer", nullable: false), + Resource = table.Column(type: "text", nullable: false), + Action = table.Column(type: "integer", nullable: false), + IsSystem = table.Column(type: "boolean", nullable: false), + CreateTime = table.Column(type: "timestamp with time zone", nullable: false), + UpdateTime = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Permissions", x => x.PermissionId); + }); + + migrationBuilder.CreateTable( + name: "Roles", + columns: table => new + { + RoleId = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "text", nullable: false), + Description = table.Column(type: "text", nullable: true), + IsSystem = table.Column(type: "boolean", nullable: false), + IsSupper = table.Column(type: "boolean", nullable: false), + CreateTime = table.Column(type: "timestamp with time zone", nullable: false), + UpdateTime = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Roles", x => x.RoleId); + }); + + migrationBuilder.CreateTable( + name: "SystemSettings", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Key = table.Column(type: "text", nullable: false), + Value = table.Column(type: "text", nullable: false), + Description = table.Column(type: "text", nullable: true), + Group = table.Column(type: "text", nullable: true), + CreateTime = table.Column(type: "timestamp with time zone", nullable: false), + UpdateTime = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SystemSettings", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "UserInfos", + columns: table => new + { + UserInfoId = table.Column(type: "uuid", nullable: false), + Gender = table.Column(type: "integer", nullable: true), + Age = table.Column(type: "integer", nullable: true), + Address = table.Column(type: "text", nullable: true), + Name = table.Column(type: "text", nullable: true), + Phone = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_UserInfos", x => x.UserInfoId); + }); + + migrationBuilder.CreateTable( + name: "RolePermissions", + columns: table => new + { + RoleId = table.Column(type: "uuid", nullable: false), + PermissionId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RolePermissions", x => new { x.RoleId, x.PermissionId }); + table.ForeignKey( + name: "FK_RolePermissions_Permissions_PermissionId", + column: x => x.PermissionId, + principalTable: "Permissions", + principalColumn: "PermissionId", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_RolePermissions_Roles_RoleId", + column: x => x.RoleId, + principalTable: "Roles", + principalColumn: "RoleId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + UserId = table.Column(type: "uuid", nullable: false), + Account = table.Column(type: "text", nullable: false), + Password = table.Column(type: "text", nullable: false), + Salt = table.Column(type: "text", nullable: false), + Email = table.Column(type: "text", nullable: false), + Status = table.Column(type: "integer", nullable: false), + UserInfoId = table.Column(type: "uuid", nullable: true), + RoleId = table.Column(type: "uuid", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.UserId); + table.ForeignKey( + name: "FK_Users_Roles_RoleId", + column: x => x.RoleId, + principalTable: "Roles", + principalColumn: "RoleId", + onDelete: ReferentialAction.SetNull); + }); + + migrationBuilder.CreateIndex( + name: "IX_Permissions_Code", + table: "Permissions", + column: "Code", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_RolePermissions_PermissionId", + table: "RolePermissions", + column: "PermissionId"); + + migrationBuilder.CreateIndex( + name: "IX_RolePermissions_RoleId", + table: "RolePermissions", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "IX_Roles_Name", + table: "Roles", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Users_Account", + table: "Users", + column: "Account", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Users_RoleId", + table: "Users", + column: "RoleId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Chunks"); + + migrationBuilder.DropTable( + name: "Conversations"); + + migrationBuilder.DropTable( + name: "Files"); + + migrationBuilder.DropTable( + name: "LogEntries"); + + migrationBuilder.DropTable( + name: "Messages"); + + migrationBuilder.DropTable( + name: "RolePermissions"); + + migrationBuilder.DropTable( + name: "SystemSettings"); + + migrationBuilder.DropTable( + name: "UserInfos"); + + migrationBuilder.DropTable( + name: "Users"); + + migrationBuilder.DropTable( + name: "Permissions"); + + migrationBuilder.DropTable( + name: "Roles"); + } + } +} diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250812085226_last.Designer.cs b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250812085226_last.Designer.cs new file mode 100644 index 0000000000000000000000000000000000000000..5334287b04ed11df5ed92e0e2051aad042fec345 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250812085226_last.Designer.cs @@ -0,0 +1,394 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Pgvector; +using UniversalAdminSystem.Infrastructure.Persistence.DbContexts; + +#nullable disable + +namespace UniversalAdminSystem.Infrastructure.Migrations +{ + [DbContext(typeof(UniversalAdminSystemDbContext))] + [Migration("20250812085226_last")] + partial class last + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RolePermissions", b => + { + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("PermissionId") + .HasColumnType("uuid"); + + b.HasKey("RoleId", "PermissionId"); + + b.HasIndex("PermissionId"); + + b.HasIndex("RoleId"); + + b.ToTable("RolePermissions"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.FileStorage.Aggregates.File", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessLevel") + .HasColumnType("integer"); + + b.Property("IsFolder") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("SecurityCheckResult") + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UploadTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.LogManagement.Aggregates.LogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Context") + .HasColumnType("text"); + + b.Property("Exception") + .HasColumnType("text"); + + b.Property("Level") + .IsRequired() + .HasColumnType("text"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("LogEntries"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Permission", b => + { + b.Property("PermissionId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .HasColumnType("integer"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsSystem") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PermissionType") + .HasColumnType("integer"); + + b.Property("Resource") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("PermissionId"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Role", b => + { + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsSupper") + .HasColumnType("boolean"); + + b.Property("IsSystem") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("RoleId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.SystemSettings.Aggregates.SystemSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Group") + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("SystemSettings"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserConversations.Aggregates.Conversations", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("UpdateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Conversations"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserConversations.Aggregates.Message", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserManagement.Aggregates.User", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Account") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserInfoId") + .HasColumnType("uuid"); + + b.HasKey("UserId"); + + b.HasIndex("Account") + .IsUnique(); + + b.HasIndex("RoleId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserManagement.Entities.UserInfo", b => + { + b.Property("UserInfoId") + .HasColumnType("uuid"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("Age") + .HasColumnType("integer"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.HasKey("UserInfoId"); + + b.ToTable("UserInfos"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.knowledge.Aggregates.DocumentChunk", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("Embedding") + .IsRequired() + .HasColumnType("vector(1536)"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Embedding"); + + b.ToTable("Chunks"); + }); + + modelBuilder.Entity("RolePermissions", b => + { + b.HasOne("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Permission", null) + .WithMany() + .HasForeignKey("PermissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserManagement.Aggregates.User", b => + { + b.HasOne("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.SetNull); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250812085226_last.cs b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250812085226_last.cs new file mode 100644 index 0000000000000000000000000000000000000000..d47c9b5d56cd48fcd684c8c5a352fe6461240293 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250812085226_last.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace UniversalAdminSystem.Infrastructure.Migrations +{ + /// + public partial class last : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_Chunks_Embedding", + table: "Chunks", + column: "Embedding"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Chunks_Embedding", + table: "Chunks"); + } + } +} diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250812093344_postgre_vector.Designer.cs b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250812093344_postgre_vector.Designer.cs new file mode 100644 index 0000000000000000000000000000000000000000..a18b7afefccfd0c9db06c6bcafd8a1a278cf3a95 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250812093344_postgre_vector.Designer.cs @@ -0,0 +1,433 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Pgvector; +using UniversalAdminSystem.Infrastructure.Persistence.DbContexts; + +#nullable disable + +namespace UniversalAdminSystem.Infrastructure.Migrations +{ + [DbContext(typeof(UniversalAdminSystemDbContext))] + [Migration("20250812093344_postgre_vector")] + partial class postgre_vector + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RolePermissions", b => + { + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("PermissionId") + .HasColumnType("uuid"); + + b.HasKey("RoleId", "PermissionId"); + + b.HasIndex("PermissionId"); + + b.HasIndex("RoleId"); + + b.ToTable("RolePermissions"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.FileStorage.Aggregates.File", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessLevel") + .HasColumnType("integer"); + + b.Property("IsFolder") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("SecurityCheckResult") + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UploadTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.LogManagement.Aggregates.LogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Context") + .HasColumnType("text"); + + b.Property("Exception") + .HasColumnType("text"); + + b.Property("Level") + .IsRequired() + .HasColumnType("text"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("LogEntries"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Permission", b => + { + b.Property("PermissionId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .HasColumnType("integer"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsSystem") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PermissionType") + .HasColumnType("integer"); + + b.Property("Resource") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("PermissionId"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Role", b => + { + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsSupper") + .HasColumnType("boolean"); + + b.Property("IsSystem") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("RoleId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.SystemSettings.Aggregates.SystemSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Group") + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("SystemSettings"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserConversations.Aggregates.Conversations", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("UpdateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Conversations"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserConversations.Aggregates.Message", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserManagement.Aggregates.User", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Account") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserInfoId") + .HasColumnType("uuid"); + + b.HasKey("UserId"); + + b.HasIndex("Account") + .IsUnique(); + + b.HasIndex("RoleId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserManagement.Entities.UserInfo", b => + { + b.Property("UserInfoId") + .HasColumnType("uuid"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("Age") + .HasColumnType("integer"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.HasKey("UserInfoId"); + + b.ToTable("UserInfos"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.knowledge.Aggregates.DocumentChunk", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("Embedding") + .IsRequired() + .HasColumnType("vector(1536)"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Chunks"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Infrastructure.RabbitMQ.Jobs.FileProcessingJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("text"); + + b.Property("NextAttemptAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RetryCount") + .HasColumnType("integer"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.HasKey("Id"); + + b.HasIndex("Status", "NextAttemptAt", "CreatedAt"); + + b.ToTable("FileProcessingJobs"); + }); + + modelBuilder.Entity("RolePermissions", b => + { + b.HasOne("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Permission", null) + .WithMany() + .HasForeignKey("PermissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserManagement.Aggregates.User", b => + { + b.HasOne("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.SetNull); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250812093344_postgre_vector.cs b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250812093344_postgre_vector.cs new file mode 100644 index 0000000000000000000000000000000000000000..3c14a143eca58c074c98de39c34052eb81822b08 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250812093344_postgre_vector.cs @@ -0,0 +1,46 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace UniversalAdminSystem.Infrastructure.Migrations +{ + /// + public partial class postgre_vector : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "FileProcessingJobs", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + FileId = table.Column(type: "uuid", nullable: false), + FilePath = table.Column(type: "text", nullable: false), + ContentType = table.Column(type: "text", nullable: false), + Size = table.Column(type: "bigint", nullable: false), + Status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + RetryCount = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + NextAttemptAt = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_FileProcessingJobs", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_FileProcessingJobs_Status_NextAttemptAt_CreatedAt", + table: "FileProcessingJobs", + columns: new[] { "Status", "NextAttemptAt", "CreatedAt" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "FileProcessingJobs"); + } + } +} diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250812104226_postgre_vector_2.Designer.cs b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250812104226_postgre_vector_2.Designer.cs new file mode 100644 index 0000000000000000000000000000000000000000..ddf64368a82354615abae9e93c6ff5ee615a6dc2 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250812104226_postgre_vector_2.Designer.cs @@ -0,0 +1,433 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Pgvector; +using UniversalAdminSystem.Infrastructure.Persistence.DbContexts; + +#nullable disable + +namespace UniversalAdminSystem.Infrastructure.Migrations +{ + [DbContext(typeof(UniversalAdminSystemDbContext))] + [Migration("20250812104226_postgre_vector_2")] + partial class postgre_vector_2 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RolePermissions", b => + { + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("PermissionId") + .HasColumnType("uuid"); + + b.HasKey("RoleId", "PermissionId"); + + b.HasIndex("PermissionId"); + + b.HasIndex("RoleId"); + + b.ToTable("RolePermissions"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.FileStorage.Aggregates.File", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessLevel") + .HasColumnType("integer"); + + b.Property("IsFolder") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("SecurityCheckResult") + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UploadTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.LogManagement.Aggregates.LogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Context") + .HasColumnType("text"); + + b.Property("Exception") + .HasColumnType("text"); + + b.Property("Level") + .IsRequired() + .HasColumnType("text"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("LogEntries"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Permission", b => + { + b.Property("PermissionId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .HasColumnType("integer"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsSystem") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PermissionType") + .HasColumnType("integer"); + + b.Property("Resource") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("PermissionId"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Role", b => + { + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsSupper") + .HasColumnType("boolean"); + + b.Property("IsSystem") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("RoleId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.SystemSettings.Aggregates.SystemSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Group") + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("SystemSettings"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserConversations.Aggregates.Conversations", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("UpdateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Conversations"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserConversations.Aggregates.Message", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserManagement.Aggregates.User", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Account") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserInfoId") + .HasColumnType("uuid"); + + b.HasKey("UserId"); + + b.HasIndex("Account") + .IsUnique(); + + b.HasIndex("RoleId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserManagement.Entities.UserInfo", b => + { + b.Property("UserInfoId") + .HasColumnType("uuid"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("Age") + .HasColumnType("integer"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.HasKey("UserInfoId"); + + b.ToTable("UserInfos"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.knowledge.Aggregates.DocumentChunk", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("Embedding") + .IsRequired() + .HasColumnType("vector(1536)"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Chunks"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Infrastructure.RabbitMQ.Jobs.FileProcessingJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("text"); + + b.Property("NextAttemptAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RetryCount") + .HasColumnType("integer"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.HasKey("Id"); + + b.HasIndex("Status", "NextAttemptAt", "CreatedAt"); + + b.ToTable("FileProcessingJobs"); + }); + + modelBuilder.Entity("RolePermissions", b => + { + b.HasOne("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Permission", null) + .WithMany() + .HasForeignKey("PermissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserManagement.Aggregates.User", b => + { + b.HasOne("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.SetNull); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250812104226_postgre_vector_2.cs b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250812104226_postgre_vector_2.cs new file mode 100644 index 0000000000000000000000000000000000000000..18522764d898da0c53e2b6f932df9bd0d7b97499 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250812104226_postgre_vector_2.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace UniversalAdminSystem.Infrastructure.Migrations +{ + /// + public partial class postgre_vector_2 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250813061435_RAG2.Designer.cs b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250813061435_RAG2.Designer.cs new file mode 100644 index 0000000000000000000000000000000000000000..fadf9b943c9c301db2646f0574439f9368cc5788 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250813061435_RAG2.Designer.cs @@ -0,0 +1,435 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Pgvector; +using UniversalAdminSystem.Infrastructure.Persistence.DbContexts; + +#nullable disable + +namespace UniversalAdminSystem.Infrastructure.Migrations +{ + [DbContext(typeof(UniversalAdminSystemDbContext))] + [Migration("20250813061435_RAG2")] + partial class RAG2 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RolePermissions", b => + { + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("PermissionId") + .HasColumnType("uuid"); + + b.HasKey("RoleId", "PermissionId"); + + b.HasIndex("PermissionId"); + + b.HasIndex("RoleId"); + + b.ToTable("RolePermissions"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.FileStorage.Aggregates.File", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessLevel") + .HasColumnType("integer"); + + b.Property("IsFolder") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("SecurityCheckResult") + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UploadTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.LogManagement.Aggregates.LogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Context") + .HasColumnType("text"); + + b.Property("Exception") + .HasColumnType("text"); + + b.Property("Level") + .IsRequired() + .HasColumnType("text"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("LogEntries"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Permission", b => + { + b.Property("PermissionId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .HasColumnType("integer"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsSystem") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PermissionType") + .HasColumnType("integer"); + + b.Property("Resource") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("PermissionId"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Role", b => + { + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsSupper") + .HasColumnType("boolean"); + + b.Property("IsSystem") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("RoleId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.SystemSettings.Aggregates.SystemSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Group") + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("SystemSettings"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserConversations.Aggregates.Conversations", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("UpdateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Conversations"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserConversations.Aggregates.Message", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserManagement.Aggregates.User", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Account") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserInfoId") + .HasColumnType("uuid"); + + b.HasKey("UserId"); + + b.HasIndex("Account") + .IsUnique(); + + b.HasIndex("RoleId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserManagement.Entities.UserInfo", b => + { + b.Property("UserInfoId") + .HasColumnType("uuid"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("Age") + .HasColumnType("integer"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.HasKey("UserInfoId"); + + b.ToTable("UserInfos"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.knowledge.Aggregates.DocumentChunk", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("Embedding") + .IsRequired() + .HasColumnType("vector(1536)"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Embedding"); + + b.ToTable("Chunks"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Infrastructure.RabbitMQ.Jobs.FileProcessingJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("text"); + + b.Property("NextAttemptAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RetryCount") + .HasColumnType("integer"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.HasKey("Id"); + + b.HasIndex("Status", "NextAttemptAt", "CreatedAt"); + + b.ToTable("FileProcessingJobs"); + }); + + modelBuilder.Entity("RolePermissions", b => + { + b.HasOne("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Permission", null) + .WithMany() + .HasForeignKey("PermissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserManagement.Aggregates.User", b => + { + b.HasOne("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.SetNull); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250813061435_RAG2.cs b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250813061435_RAG2.cs new file mode 100644 index 0000000000000000000000000000000000000000..f7dc21c9e9088b0d095b7c04488e7e23355ff964 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250813061435_RAG2.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace UniversalAdminSystem.Infrastructure.Migrations +{ + /// + public partial class RAG2 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git "a/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250813131630_vector\357\274\2101024\357\274\211.Designer.cs" "b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250813131630_vector\357\274\2101024\357\274\211.Designer.cs" new file mode 100644 index 0000000000000000000000000000000000000000..cf937a139ac82b4058d15d90dd20c46695ca3aac --- /dev/null +++ "b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250813131630_vector\357\274\2101024\357\274\211.Designer.cs" @@ -0,0 +1,435 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Pgvector; +using UniversalAdminSystem.Infrastructure.Persistence.DbContexts; + +#nullable disable + +namespace UniversalAdminSystem.Infrastructure.Migrations +{ + [DbContext(typeof(UniversalAdminSystemDbContext))] + [Migration("20250813131630_vector(1024)")] + partial class vector1024 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RolePermissions", b => + { + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("PermissionId") + .HasColumnType("uuid"); + + b.HasKey("RoleId", "PermissionId"); + + b.HasIndex("PermissionId"); + + b.HasIndex("RoleId"); + + b.ToTable("RolePermissions"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.FileStorage.Aggregates.File", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessLevel") + .HasColumnType("integer"); + + b.Property("IsFolder") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("SecurityCheckResult") + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UploadTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.LogManagement.Aggregates.LogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Context") + .HasColumnType("text"); + + b.Property("Exception") + .HasColumnType("text"); + + b.Property("Level") + .IsRequired() + .HasColumnType("text"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("LogEntries"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Permission", b => + { + b.Property("PermissionId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .HasColumnType("integer"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsSystem") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PermissionType") + .HasColumnType("integer"); + + b.Property("Resource") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("PermissionId"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Role", b => + { + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsSupper") + .HasColumnType("boolean"); + + b.Property("IsSystem") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("RoleId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.SystemSettings.Aggregates.SystemSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Group") + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("SystemSettings"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserConversations.Aggregates.Conversations", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("UpdateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Conversations"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserConversations.Aggregates.Message", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserManagement.Aggregates.User", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Account") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserInfoId") + .HasColumnType("uuid"); + + b.HasKey("UserId"); + + b.HasIndex("Account") + .IsUnique(); + + b.HasIndex("RoleId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserManagement.Entities.UserInfo", b => + { + b.Property("UserInfoId") + .HasColumnType("uuid"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("Age") + .HasColumnType("integer"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.HasKey("UserInfoId"); + + b.ToTable("UserInfos"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.knowledge.Aggregates.DocumentChunk", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("Embedding") + .IsRequired() + .HasColumnType("vector(1024)"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Embedding"); + + b.ToTable("Chunks"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Infrastructure.RabbitMQ.Jobs.FileProcessingJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("text"); + + b.Property("NextAttemptAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RetryCount") + .HasColumnType("integer"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.HasKey("Id"); + + b.HasIndex("Status", "NextAttemptAt", "CreatedAt"); + + b.ToTable("FileProcessingJobs"); + }); + + modelBuilder.Entity("RolePermissions", b => + { + b.HasOne("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Permission", null) + .WithMany() + .HasForeignKey("PermissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserManagement.Aggregates.User", b => + { + b.HasOne("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.SetNull); + }); +#pragma warning restore 612, 618 + } + } +} diff --git "a/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250813131630_vector\357\274\2101024\357\274\211.cs" "b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250813131630_vector\357\274\2101024\357\274\211.cs" new file mode 100644 index 0000000000000000000000000000000000000000..e8761a9dd6d6dcfc94c9f2c837b231b30cebf14b --- /dev/null +++ "b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250813131630_vector\357\274\2101024\357\274\211.cs" @@ -0,0 +1,50 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Pgvector; + +#nullable disable + +namespace UniversalAdminSystem.Infrastructure.Migrations +{ + public partial class vector1024 : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + // 修改列类型为 vector(1024) + migrationBuilder.AlterColumn( + name: "Embedding", + table: "Chunks", + type: "vector(1024)", + nullable: false, + oldClrType: typeof(Vector), + oldType: "vector(1536)"); + + // 删除旧索引(如果存在) + migrationBuilder.Sql(@"DROP INDEX IF EXISTS ""IX_Chunks_Embedding_Vector"";"); + + // 建立向量索引(pgvector ivfflat) + migrationBuilder.Sql(@" + CREATE INDEX ""IX_Chunks_Embedding_Vector"" + ON ""Chunks"" + USING ivfflat (""Embedding"" vector_l2_ops) WITH (lists = 100); + "); + + // 可选:分析表统计信息 + migrationBuilder.Sql(@"ANALYZE ""Chunks"";"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // 删除向量索引 + migrationBuilder.Sql(@"DROP INDEX IF EXISTS ""IX_Chunks_Embedding_Vector"";"); + + // 恢复列类型为 vector(1536) + migrationBuilder.AlterColumn( + name: "Embedding", + table: "Chunks", + type: "vector(1536)", + nullable: false, + oldClrType: typeof(Vector), + oldType: "vector(1024)"); + } + } +} \ No newline at end of file diff --git "a/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250813133848_vector\357\274\2101024\357\274\211_2.Designer.cs" "b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250813133848_vector\357\274\2101024\357\274\211_2.Designer.cs" new file mode 100644 index 0000000000000000000000000000000000000000..812f8ee5e9b082110c4895d5099a495989c05569 --- /dev/null +++ "b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250813133848_vector\357\274\2101024\357\274\211_2.Designer.cs" @@ -0,0 +1,433 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Pgvector; +using UniversalAdminSystem.Infrastructure.Persistence.DbContexts; + +#nullable disable + +namespace UniversalAdminSystem.Infrastructure.Migrations +{ + [DbContext(typeof(UniversalAdminSystemDbContext))] + [Migration("20250813133848_vector(1024)_2")] + partial class vector1024_2 + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RolePermissions", b => + { + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("PermissionId") + .HasColumnType("uuid"); + + b.HasKey("RoleId", "PermissionId"); + + b.HasIndex("PermissionId"); + + b.HasIndex("RoleId"); + + b.ToTable("RolePermissions"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.FileStorage.Aggregates.File", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessLevel") + .HasColumnType("integer"); + + b.Property("IsFolder") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("SecurityCheckResult") + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UploadTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.LogManagement.Aggregates.LogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Context") + .HasColumnType("text"); + + b.Property("Exception") + .HasColumnType("text"); + + b.Property("Level") + .IsRequired() + .HasColumnType("text"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("LogEntries"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Permission", b => + { + b.Property("PermissionId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .HasColumnType("integer"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsSystem") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PermissionType") + .HasColumnType("integer"); + + b.Property("Resource") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("PermissionId"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Role", b => + { + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsSupper") + .HasColumnType("boolean"); + + b.Property("IsSystem") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("RoleId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.SystemSettings.Aggregates.SystemSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Group") + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("SystemSettings"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserConversations.Aggregates.Conversations", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("UpdateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Conversations"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserConversations.Aggregates.Message", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserManagement.Aggregates.User", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Account") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserInfoId") + .HasColumnType("uuid"); + + b.HasKey("UserId"); + + b.HasIndex("Account") + .IsUnique(); + + b.HasIndex("RoleId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserManagement.Entities.UserInfo", b => + { + b.Property("UserInfoId") + .HasColumnType("uuid"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("Age") + .HasColumnType("integer"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.HasKey("UserInfoId"); + + b.ToTable("UserInfos"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.knowledge.Aggregates.DocumentChunk", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("Embedding") + .IsRequired() + .HasColumnType("vector(1024)"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Chunks"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Infrastructure.RabbitMQ.Jobs.FileProcessingJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("text"); + + b.Property("NextAttemptAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RetryCount") + .HasColumnType("integer"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.HasKey("Id"); + + b.HasIndex("Status", "NextAttemptAt", "CreatedAt"); + + b.ToTable("FileProcessingJobs"); + }); + + modelBuilder.Entity("RolePermissions", b => + { + b.HasOne("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Permission", null) + .WithMany() + .HasForeignKey("PermissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserManagement.Aggregates.User", b => + { + b.HasOne("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.SetNull); + }); +#pragma warning restore 612, 618 + } + } +} diff --git "a/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250813133848_vector\357\274\2101024\357\274\211_2.cs" "b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250813133848_vector\357\274\2101024\357\274\211_2.cs" new file mode 100644 index 0000000000000000000000000000000000000000..88b3f4ac97c1bfddfa409f1ece4da3277cbf9ad3 --- /dev/null +++ "b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/20250813133848_vector\357\274\2101024\357\274\211_2.cs" @@ -0,0 +1,51 @@ +using Pgvector; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace UniversalAdminSystem.Infrastructure.Migrations +{ + /// + public partial class vector1024_2 : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + // 修改列类型为 vector(1024) + migrationBuilder.AlterColumn( + name: "Embedding", + table: "Chunks", + type: "vector(1024)", + nullable: false, + oldClrType: typeof(Vector), + oldType: "vector(1536)"); + + // 删除旧索引(如果存在) + migrationBuilder.Sql(@"DROP INDEX IF EXISTS ""IX_Chunks_Embedding_Vector"";"); + + // 建立向量索引(pgvector ivfflat) + migrationBuilder.Sql(@" + CREATE INDEX ""IX_Chunks_Embedding_Vector"" + ON ""Chunks"" + USING ivfflat (""Embedding"" vector_l2_ops) WITH (lists = 100); + "); + + // 可选:分析表统计信息 + migrationBuilder.Sql(@"ANALYZE ""Chunks"";"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // 删除向量索引 + migrationBuilder.Sql(@"DROP INDEX IF EXISTS ""IX_Chunks_Embedding_Vector"";"); + + // 恢复列类型为 vector(1536) + migrationBuilder.AlterColumn( + name: "Embedding", + table: "Chunks", + type: "vector(1536)", + nullable: false, + oldClrType: typeof(Vector), + oldType: "vector(1024)"); + } + } +} diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Migrations/UniversalAdminSystemDbContextModelSnapshot.cs b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/UniversalAdminSystemDbContextModelSnapshot.cs new file mode 100644 index 0000000000000000000000000000000000000000..fe4f5d2457753ab3efa360fada92461f9c8d0d73 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Migrations/UniversalAdminSystemDbContextModelSnapshot.cs @@ -0,0 +1,430 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Pgvector; +using UniversalAdminSystem.Infrastructure.Persistence.DbContexts; + +#nullable disable + +namespace UniversalAdminSystem.Infrastructure.Migrations +{ + [DbContext(typeof(UniversalAdminSystemDbContext))] + partial class UniversalAdminSystemDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector"); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("RolePermissions", b => + { + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("PermissionId") + .HasColumnType("uuid"); + + b.HasKey("RoleId", "PermissionId"); + + b.HasIndex("PermissionId"); + + b.HasIndex("RoleId"); + + b.ToTable("RolePermissions"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.FileStorage.Aggregates.File", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessLevel") + .HasColumnType("integer"); + + b.Property("IsFolder") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("ParentId") + .HasColumnType("uuid"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text"); + + b.Property("SecurityCheckResult") + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UploadTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Files"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.LogManagement.Aggregates.LogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Context") + .HasColumnType("text"); + + b.Property("Exception") + .HasColumnType("text"); + + b.Property("Level") + .IsRequired() + .HasColumnType("text"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("LogEntries"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Permission", b => + { + b.Property("PermissionId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Action") + .HasColumnType("integer"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsSystem") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PermissionType") + .HasColumnType("integer"); + + b.Property("Resource") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("PermissionId"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Role", b => + { + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsSupper") + .HasColumnType("boolean"); + + b.Property("IsSystem") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("RoleId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.SystemSettings.Aggregates.SystemSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Group") + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("SystemSettings"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserConversations.Aggregates.Conversations", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .HasColumnType("text"); + + b.Property("UpdateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Conversations"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserConversations.Aggregates.Message", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CreateDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Messages"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserManagement.Aggregates.User", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Account") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Password") + .IsRequired() + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserInfoId") + .HasColumnType("uuid"); + + b.HasKey("UserId"); + + b.HasIndex("Account") + .IsUnique(); + + b.HasIndex("RoleId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserManagement.Entities.UserInfo", b => + { + b.Property("UserInfoId") + .HasColumnType("uuid"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("Age") + .HasColumnType("integer"); + + b.Property("Gender") + .HasColumnType("integer"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.HasKey("UserInfoId"); + + b.ToTable("UserInfos"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.knowledge.Aggregates.DocumentChunk", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("Embedding") + .IsRequired() + .HasColumnType("vector(1024)"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("Level") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Chunks"); + }); + + modelBuilder.Entity("UniversalAdminSystem.Infrastructure.RabbitMQ.Jobs.FileProcessingJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileId") + .HasColumnType("uuid"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("text"); + + b.Property("NextAttemptAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RetryCount") + .HasColumnType("integer"); + + b.Property("Size") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.HasKey("Id"); + + b.HasIndex("Status", "NextAttemptAt", "CreatedAt"); + + b.ToTable("FileProcessingJobs"); + }); + + modelBuilder.Entity("RolePermissions", b => + { + b.HasOne("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Permission", null) + .WithMany() + .HasForeignKey("PermissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("UniversalAdminSystem.Domian.UserManagement.Aggregates.User", b => + { + b.HasOne("UniversalAdminSystem.Domian.PermissionManagement.Aggregate.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.SetNull); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Persistence/DbContexts/UniversalAdminSystemDbContext.cs b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/DbContexts/UniversalAdminSystemDbContext.cs new file mode 100644 index 0000000000000000000000000000000000000000..c42369cdf5eadd585965d6eea343c8c35522a51c --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/DbContexts/UniversalAdminSystemDbContext.cs @@ -0,0 +1,303 @@ +using Microsoft.EntityFrameworkCore; +using UniversalAdminSystem.Domian.Core.ValueObjects; +using UniversalAdminSystem.Domian.PermissionManagement.Aggregate; +using UniversalAdminSystem.Domian.PermissionManagement.ValueObjects; +using UniversalAdminSystem.Domian.UserManagement.Aggregates; +using UniversalAdminSystem.Domian.UserManagement.Entities; +using UniversalAdminSystem.Domian.UserManagement.ValueObj; +using UniversalAdminSystem.Domian.LogManagement.Aggregates; +using UniversalAdminSystem.Domian.SystemSettings.Aggregates; +using UniversalAdminSystem.Domian.SystemSettings.ValueObjects; +using UniversalAdminSystem.Domian.FileStorage.ValueObjects; +using File = UniversalAdminSystem.Domian.FileStorage.Aggregates.File; +using UniversalAdminSystem.Domian.knowledge.Aggregates; +using System.Data.Common; +using UniversalAdminSystem.Domian.knowledge.ValueObj; +using NpgsqlTypes; +using Pgvector; +using UniversalAdminSystem.Domian.UserConversations.Aggregates; +using UniversalAdminSystem.Infrastructure.RabbitMQ.Jobs; + +namespace UniversalAdminSystem.Infrastructure.Persistence.DbContexts; + +public class UniversalAdminSystemDbContext : DbContext +{ + public UniversalAdminSystemDbContext(DbContextOptions options) : base(options) { } + + public DbSet UserInfos { get; set; } + public DbSet Users { get; set; } + public DbSet Roles { get; set; } + public DbSet Permissions { get; set; } + public DbSet LogEntries { get; set; } + public DbSet SystemSettings { get; set; } + public DbSet Files { get; set; } + public DbSet Conversations { get; set; } + public DbSet Messages { get; set; } + public DbSet Chunks { get; set; } + public DbSet FileProcessingJobs { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // 启用"vector"扩展 + modelBuilder.HasPostgresExtension("vector"); + + // 配置UserInfo实体 + modelBuilder.Entity(entity => + { + entity.HasKey(u => u.UserInfoId); + entity.Property(u => u.UserInfoId) + .HasConversion( + id => id.Value, + value => UserInfoId.Create(value) + ); + }); + + // 配置User实体 + modelBuilder.Entity(entity => + { + entity.HasKey(u => u.UserId); + entity.HasIndex(u => u.Account).IsUnique(); + + // 配置UserId值对象转换 + entity.Property(u => u.UserId) + .HasConversion( + id => id.Value, + value => UserId.Create(value) + ); + + // 配置UserInfoId值对象转换 + entity.Property(u => u.UserInfoId) + .HasConversion( + id => id!.Value, + value => UserInfoId.Create(value) + ); + + // 配置UserEmail值对象转换 + entity.Property(u => u.Email) + .HasConversion( + email => email.Value, + value => UserEmail.Create(value) + ); + + // 配置UserAccount值对象转换 + entity.Property(u => u.Account) + .HasConversion( + account => account.Value, + value => UserAccount.Create(value) + ); + + // 配置User和Role的一对多关系 + entity.HasOne() + .WithMany() + .HasForeignKey("RoleId") + .IsRequired(false) + .OnDelete(DeleteBehavior.SetNull); + }); + + // 配置Permission实体 + modelBuilder.Entity(entity => + { + entity.HasKey(p => p.PermissionId); + entity.HasIndex(p => p.Code).IsUnique(); + entity.Property(p => p.Code).HasConversion( + code => code.Value, + value => PermissionCode.Create(value) + ); + entity.Property(p => p.Name).HasConversion( + name => name.Value, + value => PermissionName.Create(value) + ); + + entity.Property(p => p.Code).HasConversion( + code => code.Value, + value => PermissionCode.Create(value) + ); + + entity.Property(p => p.Resource).HasConversion( + resource => resource.Value, + value => PermissionResource.Create(value) + ); + }); + + // 配置Role + modelBuilder.Entity(entity => + { + entity.HasKey(r => r.RoleId); + entity.HasIndex(r => r.Name).IsUnique(); + + // 配置RoleId值对象转换 + entity.Property(r => r.RoleId) + .HasConversion( + id => id.Value, + value => RoleId.Create(value) + ); + + // 配置RoleName值对象转换 + entity.Property(r => r.Name) + .HasConversion( + name => name.Value, + value => RoleName.Create(value) + ); + + // 配置RoleDescription值对象转换 + entity.Property(r => r.Description) + .HasConversion( + desc => desc!.Value, + value => RoleDescription.Create(value) + ); + + // 配置多对多关系 + entity.HasMany(r => r.Permissions) + .WithMany() + .UsingEntity( + "RolePermissions", + l => l.HasOne(typeof(Permission)).WithMany().HasForeignKey("PermissionId"), + r => r.HasOne(typeof(Role)).WithMany().HasForeignKey("RoleId"), + j => + { + j.HasKey("RoleId", "PermissionId"); + j.HasIndex("RoleId"); + j.HasIndex("PermissionId"); + } + ); + }); + + // 配置LogEntry实体 + modelBuilder.Entity(entity => + { + entity.HasKey(l => l.Id); + entity.Property(l => l.UserId) + .HasConversion( + userId => userId!.Value, + value => UserId.Create(value) + ); + }); + + // 配置SystemSetting实体 + modelBuilder.Entity(entity => + { + entity.HasKey(s => s.Id); + entity.Property(s => s.Key) + .HasConversion( + key => key.Value, + value => SettingKey.Create(value) + ); + entity.Property(s => s.Value) + .HasConversion( + value => value.Value, + val => SettingValue.Create(val) + ); + entity.Property(s => s.Description) + .HasConversion( + desc => desc!.Value, + value => SettingDescription.Create(value) + ); + }); + + // 配置File实体 + modelBuilder.Entity(entity => + { + entity.HasKey(f => f.Id); + entity.Property(f => f.Id) + .HasConversion( + id => id.Value, + value => FileId.Create(value) + ); + entity.Property(f => f.Name) + .HasConversion( + name => name.Value, + value => FileName.Create(value) + ); + entity.Property(f => f.Path) + .HasConversion( + path => path.Value, + value => FilePath.Create(value) + ); + entity.Property(f => f.Size) + .HasConversion( + size => size.Value, + value => FileSize.Create(value) + ); + entity.Property(f => f.Type) + .HasConversion( + type => type.Value, + value => FileType.Create(value) + ); + entity.Property(f => f.OwnerId) + .HasConversion( + ownerId => ownerId.Value, + value => UserId.Create(value) + ); + entity.Property(f => f.ParentId) + .HasConversion( + parentId => parentId!.Value, + value => FileId.Create(value) + ); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(d => d.Id); + + + entity.Property(d => d.FileId) + .HasConversion( + fileId => fileId.Value, + value => FileId.Create(value) + ); + + entity.Property(d => d.Id) + .HasConversion(id => id.Value, + value => ChunkId.Create(value)); + + entity.Property(d => d.Embedding) + .HasColumnType("vector(1024)") + .HasConversion( + embedding => new Vector(embedding.Value), + value => TextEmbedding.Create(value.ToArray()) + ); + + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(c => c.Id); + + entity.Property(c => c.Id) + .HasConversion(id => id.Value, + value => ConversationId.Create(value) + ); + + entity.Property(c => c.UserId) + .HasConversion(userid => userid.Value, + value => UserId.Create(value) + ); + + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(m => m.Id); + + entity.Property(m => m.Id) + .HasConversion(id => id.Value, + value => MessageId.Create(value) + ); + + entity.Property(m => m.ConversationId) + .HasConversion(Conversationid => Conversationid.Value, + value => ConversationId.Create(value) + ); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(fpje => fpje.Id); + entity.HasIndex(fpje => new { fpje.Status, fpje.NextAttemptAt, fpje.CreatedAt }); + entity.Property(fpje => fpje.Status).HasMaxLength(32); + }); + + // 忽略值对象 + modelBuilder.Ignore(); + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/BaseRepository.cs b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/BaseRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..a0d10fe686de4a966bad276517690c470a813892 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/BaseRepository.cs @@ -0,0 +1,70 @@ +using Microsoft.EntityFrameworkCore; +using Npgsql; +using UniversalAdminSystem.Domian.Core.Interfaces; +using UniversalAdminSystem.Infrastructure.Persistence.DbContexts; + +namespace UniversalAdminSystem.Infrastructure.Persistence.Repositories; + +/// +/// 基础仓储,基本CRUD可重写 +/// +/// +public class BaseRepository : IRepository where T : class +{ + protected readonly UniversalAdminSystemDbContext _DbContext; + protected readonly DbSet _TDs; + + + public BaseRepository(UniversalAdminSystemDbContext dbContext) + { + _DbContext = dbContext; + _TDs = dbContext.Set(); + } + + public virtual async Task AddAsync(T entity) + { + try + { + var list = (await _TDs.AddAsync(entity)).Entity; + + return list; + } + catch (DbUpdateException ex) + { + // 处理数据库更新异常 + throw new Exception("数据库更新异常", ex); + } + catch (ArgumentNullException ex) + { + // 处理空参数异常 + throw new ArgumentNullException("实体不能为空", ex); + } + } + + public virtual async Task> GetAllAsync() + { + var list = await _TDs.AsNoTracking().ToListAsync(); + return list; + } + + public virtual async Task GetByGuidAsync(Guid guid) + { + var list = await _TDs.FindAsync(guid); + return list; + } + + public virtual async Task RemoveAsync(Guid guid) + { + var user = await GetByGuidAsync(guid); + + if (user == null) return; + + _TDs.Remove(user); + + } + + public async Task Update(T entity) + { + await Task.Run(() => _TDs.Update(entity)); + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/ConversationsRepository.cs b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/ConversationsRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..bfa2c997058b5668ef2c359ac8b64d0743cbdf02 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/ConversationsRepository.cs @@ -0,0 +1,34 @@ +using System.Net.WebSockets; +using Microsoft.EntityFrameworkCore; +using UniversalAdminSystem.Domian.Core.ValueObjects; +using UniversalAdminSystem.Domian.UserConversations.Aggregates; +using UniversalAdminSystem.Domian.UserConversations.IRepository; +using UniversalAdminSystem.Infrastructure.Persistence.DbContexts; + +namespace UniversalAdminSystem.Infrastructure.Persistence.Repositories; + +public class ConversationsRepository : BaseRepository, IConversationsRepository +{ + public ConversationsRepository(UniversalAdminSystemDbContext dbContext) : base(dbContext) + { + } + + public async Task> GetByUserIdAsync(Guid userId) + { + var usersid = UserId.Create(userId); + var list = await _TDs.Where(c => c.UserId == usersid).OrderBy(c=>c.UpdateDate).AsNoTracking().ToListAsync(); + return list; + } + + public async Task RemoveByUserIdAsync(Guid userId) + { + var usersId = UserId.Create(userId); + await _TDs.Where(c => c.UserId == usersId).ExecuteDeleteAsync(); + } + + public async Task RemoveConversation(Guid ConsId) + { + var ConId = ConversationId.Create(ConsId); + await _TDs.Where(c => c.Id == ConId).ExecuteDeleteAsync(); + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/DocumentChunkRepository.cs b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/DocumentChunkRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..83a5d6909838d35cf036e43d335dadb4652657a9 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/DocumentChunkRepository.cs @@ -0,0 +1,78 @@ +using System.Linq; +using Pgvector; +using Microsoft.EntityFrameworkCore; +using Pgvector.EntityFrameworkCore; +using UniversalAdminSystem.Domian.FileStorage.ValueObjects; +using UniversalAdminSystem.Domian.knowledge.Aggregates; +using UniversalAdminSystem.Domian.knowledge.IRepository; +using UniversalAdminSystem.Domian.knowledge.ValueObj; +using UniversalAdminSystem.Infrastructure.Persistence.DbContexts; + +namespace UniversalAdminSystem.Infrastructure.Persistence.Repositories; + +public class DocumentChunkRepository : BaseRepository, IDocumentChunkRepository +{ + public DocumentChunkRepository(UniversalAdminSystemDbContext dbContext) : base(dbContext) + { + } + + public async Task BatchDeleteDocumentChunkAsync(Guid Id) + { + var fileid = FileId.Create(Id); + await _TDs.Where(d => d.FileId == fileid).ExecuteDeleteAsync(); + } + + public async Task BatchModificationChunkLevelAsync(Guid fileId, FileAccessLevel level) + { + var filesid = FileId.Create(fileId); + await _TDs.Where(d => d.FileId == filesid) + .ExecuteUpdateAsync( + set => + set.SetProperty(d => d.Level, level) + ); + } + + public async Task BulkAddDocumentChunkAsync(IEnumerable chunks) + { + await _TDs.AddRangeAsync(chunks); + } + + public async Task> FindSimilarDocumentsAsync(TextEmbedding queryEmbedding, FileAccessLevel level, int topK = 5) + { + if (queryEmbedding == null) + { + throw new ArgumentNullException(nameof(queryEmbedding), "Query embedding cannot be null"); + } + + // 确保 _TDs 是非空的 + if (_TDs == null) + { + throw new InvalidOperationException("Document data set (_TDs) is not initialized."); + } + + + try + { + var results = await _TDs + .Where(c => c.Level <= level) // 根据权限级别过滤 + .OrderBy(c => c.Embedding.CosineDistance(new Vector(queryEmbedding.Value))) // 根据相似度排序 + .Take(topK) // 限制返回的文档数量 + .ToListAsync(); // 异步执行查询 + return results; + } + catch (Exception ex) + { + // 捕获并记录错误 + System.Console.WriteLine($"Error during document retrieval: {ex.Message}"); + return Enumerable.Empty(); // 返回一个空的集合以避免进一步的错误 + } + } + + public async Task ExistsByFileIdAsync(Guid fileId) + { + var fileid = FileId.Create(fileId); + return await _TDs + .AsNoTracking() + .AnyAsync(x => x.FileId == fileid); + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/FileRepository.cs b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/FileRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..1811b67a7af26733f5944de86314f64a3a516031 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/FileRepository.cs @@ -0,0 +1,36 @@ +using UniversalAdminSystem.Domian.FileStorage.IRepository; +using UniversalAdminSystem.Domian.FileStorage.ValueObjects; +using UniversalAdminSystem.Infrastructure.Persistence.DbContexts; +using Microsoft.EntityFrameworkCore; +using UniversalAdminSystem.Domian.Core.ValueObjects; +using File = UniversalAdminSystem.Domian.FileStorage.Aggregates.File; +using Microsoft.Extensions.FileProviders; + +namespace UniversalAdminSystem.Infrastructure.Persistence.Repositories; + +public class FileRepository : BaseRepository, IFileRepository +{ + public FileRepository(UniversalAdminSystemDbContext dbContext) : base(dbContext) { } + + public async Task> GetByOwnerAsync(UserId ownerId) + => await _TDs.Where(f => f.OwnerId == ownerId).ToListAsync(); + + public async Task> GetByParentAsync(FileId? parentId) + => await _TDs.Where(f => f.ParentId == parentId).ToListAsync(); + + public async Task> GetByTypeAsync(FileType type) + => await _TDs.Where(f => f.Type == type).ToListAsync(); + + public async Task> GetFilesWithAccessAsync(UserId userId, FileAccessLevel accessLevel) + => await _TDs.Where(f => f.AccessLevel == accessLevel && (f.OwnerId == userId || f.AccessLevel == FileAccessLevel.Public)).ToListAsync(); + + public async Task> GetAllFoldersAsync(UserId ownerId) + => await _TDs.Where(f => f.OwnerId == ownerId && f.IsFolder).ToListAsync(); + + public override async Task GetByGuidAsync(Guid id) + { + var fileId = FileId.Create(id); + var file =await _TDs.FirstOrDefaultAsync(f => f.Id == fileId); + return file; + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/LogEntryRepository.cs b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/LogEntryRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..c85e12e4d3929481274f0665b3beb59cbefb1f39 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/LogEntryRepository.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore; +using UniversalAdminSystem.Domian.Core.Interfaces; +using UniversalAdminSystem.Domian.LogManagement.Aggregates; +using UniversalAdminSystem.Domian.LogManagement.IRepository; +using UniversalAdminSystem.Infrastructure.Persistence.DbContexts; + +namespace UniversalAdminSystem.Infrastructure.Persistence.Repositories; + +public class LogEntryRepository : BaseRepository, ILogEntryRepository +{ + public LogEntryRepository(UniversalAdminSystemDbContext context) : base(context) + { + } + + public async Task> GetByLevelAsync(string level) + { + return await _DbContext.Set() + .Where(l => l.Level == level) + .ToListAsync(); + } + + public async Task> GetByUserAsync(Guid userId) + { + return await _DbContext.Set() + .Where(l => l.UserId.Value == userId) + .ToListAsync(); + } + + public async Task> GetByDateRangeAsync(DateTime start, DateTime end) + { + return await _DbContext.Set() + .Where(l => l.Timestamp >= start && l.Timestamp <= end) + .ToListAsync(); + } + + public async Task> GetBySourceAsync(string source) + { + return await _DbContext.Set() + .Where(l => l.Source == source) + .ToListAsync(); + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/MessageRepository.cs b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/MessageRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..d25bb422fe7ba7a0b9eab6f4c0665b0bde24701f --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/MessageRepository.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; +using UniversalAdminSystem.Domian.Core.ValueObjects; +using UniversalAdminSystem.Domian.UserConversations.Aggregates; +using UniversalAdminSystem.Domian.UserConversations.IRepository; +using UniversalAdminSystem.Infrastructure.Persistence.DbContexts; + +namespace UniversalAdminSystem.Infrastructure.Persistence.Repositories; + +public class MessageRepository : BaseRepository, IMessageRepository +{ + public MessageRepository(UniversalAdminSystemDbContext dbContext) : base(dbContext) + { + } + + public async Task> GetByConversationIdAsync(Guid conId) + { + var ConId = ConversationId.Create(conId); + var list = await _TDs.Where(m => m.ConversationId == ConId).OrderBy(m=>m.CreateDate).AsNoTracking().ToListAsync(); + return list; + } + + public async Task RemoveByConversationIdAsync(Guid conversationId) + { + var ConId = ConversationId.Create(conversationId); + await _TDs.Where(m => m.ConversationId == ConId).ExecuteDeleteAsync(); + } +} + diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/PermissionRepository.cs b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/PermissionRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..05db86d8b93dcbf6b8b7740f2924905b67f65a6e --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/PermissionRepository.cs @@ -0,0 +1,109 @@ +using Microsoft.EntityFrameworkCore; +using UniversalAdminSystem.Domian.PermissionManagement.Aggregate; +using UniversalAdminSystem.Domian.PermissionManagement.IRepository; +using UniversalAdminSystem.Domian.PermissionManagement.ValueObjects; +using UniversalAdminSystem.Infrastructure.Persistence.DbContexts; + +namespace UniversalAdminSystem.Infrastructure.Persistence.Repositories; + +public class PermissionRepository : BaseRepository, IPermissionRepository +{ + public PermissionRepository(UniversalAdminSystemDbContext dbContext) : base(dbContext) + { + } + + /// + /// 批量持久化系统权限 + /// + /// + /// + public async Task AddSystemPermissionsAsync(IEnumerable perms) + { + if (perms == null) throw new ArgumentNullException(nameof(perms)); + var permList = perms.ToList(); + if (!permList.Any()) return; + + using var transaction = await _DbContext.Database.BeginTransactionAsync(); + try + { + // 获取所有已存在的权限代码 + var existingCodes = await _TDs + .Select(p => p.Code.Value) + .ToListAsync(); + + Console.WriteLine($"数据库中已存在的权限代码数量: {existingCodes.Count}"); + + // 过滤出新的权限 + var newPermissions = permList + .Where(p => !existingCodes.Contains(p.Code.Value)) + .ToList(); + + Console.WriteLine($"需要添加的新权限数量: {newPermissions.Count}"); + + if (newPermissions.Count > 0) + { + await _TDs.AddRangeAsync(newPermissions); + await _DbContext.SaveChangesAsync(); + Console.WriteLine($"成功添加 {newPermissions.Count} 个新权限"); + } + else + { + Console.WriteLine("所有权限都已存在,跳过添加"); + } + + await transaction.CommitAsync(); + } + catch (Exception ex) + { + await transaction.RollbackAsync(); + Console.WriteLine($"添加系统权限失败: {ex.Message}"); + throw; + } + } + + public async Task GetByCodeAsync(PermissionCode code) + { + if (code == null) throw new ArgumentNullException(nameof(code)); + + // 使用隐式转换,让EF Core能够正确转换查询 + var permission = await _TDs.FirstOrDefaultAsync(p => p.Code == code); + + if (permission != null) + { + System.Console.WriteLine($"找到权限: {permission.Code}"); + } + else + { + System.Console.WriteLine($"未找到权限代码: {code}"); + } + + return permission; + } + + public async Task ExistsAsync(PermissionCode code) + { + if (code == null) throw new ArgumentNullException(nameof(code)); + return await _TDs.AnyAsync(p => p.Code == code); + } + + public async Task> GetByIdsAsync(IEnumerable ids) + { + if (ids == null) throw new ArgumentNullException(nameof(ids)); + return await _TDs.Where(p => ids.Contains(p.PermissionId)).ToListAsync(); + } + + public async Task> QueryAsync(string? name = null, string? resource = null, int? action = null, PermissionType? type = null) + { + var query = _TDs.AsQueryable(); + if (!string.IsNullOrWhiteSpace(name)) + query = query.Where(p => p.Name.Value.Contains(name)); + if (!string.IsNullOrWhiteSpace(resource)) + query = query.Where(p => p.Resource.Value == resource); + if (action.HasValue) + query = query.Where(p => (int)p.Action == action.Value); + if (type.HasValue) + query = query.Where(p => p.PermissionType == type.Value); + return await query.ToListAsync(); + } +} + diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/RoleRepository.cs b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/RoleRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..61158eb0949fc4ec8a63c42f2c53e1ef01d94c99 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/RoleRepository.cs @@ -0,0 +1,78 @@ +using Microsoft.EntityFrameworkCore; +using UniversalAdminSystem.Domian.Core.ValueObjects; +using UniversalAdminSystem.Domian.PermissionManagement.Aggregate; +using UniversalAdminSystem.Domian.PermissionManagement.IRepository; +using UniversalAdminSystem.Infrastructure.Persistence.DbContexts; + +namespace UniversalAdminSystem.Infrastructure.Persistence.Repositories; + +public class RoleRepository : BaseRepository, IRoleRepository +{ + public RoleRepository(UniversalAdminSystemDbContext dbContext) : base(dbContext) + { + } + + public override async Task GetByGuidAsync(Guid guid) + { + var roleId = RoleId.Create(guid); + var role = await _TDs + .Include(r => r.Permissions) // 包含权限关联 + .FirstOrDefaultAsync(r => r.RoleId == roleId); + return role; + } + public async Task ExistsAsync(string name) + { + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); + return await _TDs.AnyAsync(r => r.Name == name); + } + + public async Task> GetByIdsAsync(IEnumerable ids) + { + if (ids == null) throw new ArgumentNullException(nameof(ids)); + return await _TDs + .Include(r => r.Permissions) + .Where(r => ids.Contains(r.RoleId)) + .ToListAsync(); + } + + public async Task GetByNameAsync(string name) + { + if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name)); + + // 使用更明确的比较方式 + var allRoles = await _TDs + .Include(r => r.Permissions) + .ToListAsync(); + + return allRoles.FirstOrDefault(r => r.Name.Value == name); + } + + public async Task> GetPermissionIdsAsync(Guid roleId) + { + var role = await _TDs + .Include(r => r.Permissions) + .FirstOrDefaultAsync(r => r.RoleId == roleId); + if (role == null) throw new KeyNotFoundException("角色不存在"); + return role.PermissionIds.ToList().AsReadOnly(); + } + + /// + /// 获取角色及其权限(包含权限导航属性) + /// + public async Task GetRoleWithPermissionsAsync(Guid roleId) + { + return await _TDs + .Include(r => r.Permissions) + .FirstOrDefaultAsync(r => r.RoleId == roleId); + } + + /// + /// 获取所有角色及其权限 + /// + public async Task> GetAllRolesWithPermissionsAsync() + { + return await _TDs + .Include(r => r.Permissions) + .ToListAsync(); + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/SystemSettingRepository.cs b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/SystemSettingRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..dae2f41ca42a82c455095a8324367eef3aa3116b --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/SystemSettingRepository.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore; +using UniversalAdminSystem.Domian.Core.Interfaces; +using UniversalAdminSystem.Domian.SystemSettings.Aggregates; +using UniversalAdminSystem.Domian.SystemSettings.IRepository; +using UniversalAdminSystem.Infrastructure.Persistence.DbContexts; + +namespace UniversalAdminSystem.Infrastructure.Persistence.Repositories; + +public class SystemSettingRepository : BaseRepository, ISystemSettingRepository +{ + public SystemSettingRepository(UniversalAdminSystemDbContext context) : base(context) + { + } + + public async Task GetByKeyAsync(string key) + { + return await _DbContext.Set() + .FirstOrDefaultAsync(s => s.Key.Value == key); + } + + public async Task> GetByGroupAsync(string group) + { + return await _DbContext.Set() + .Where(s => s.Key.Value.StartsWith(group)) + .ToListAsync(); + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/UserInfoRepository.cs b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/UserInfoRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..36a8f081bdea701626c3395178a16ff3a9c8432b --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/UserInfoRepository.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; +using UniversalAdminSystem.Domian.Core.ValueObjects; +using UniversalAdminSystem.Domian.UserManagement.Entities; +using UniversalAdminSystem.Domian.UserManagement.IRepository; +using UniversalAdminSystem.Infrastructure.Persistence.DbContexts; + +namespace UniversalAdminSystem.Infrastructure.Persistence.Repositories; + +public class UserInfoRepository : BaseRepository, IUserInfoRepository +{ + public UserInfoRepository(UniversalAdminSystemDbContext dbContext) : base(dbContext) + { + } + + public override async Task GetByGuidAsync(Guid id) + { + var userInfoId = UserInfoId.Create(id); + var userInfo =await _TDs.FirstOrDefaultAsync(f => f.UserInfoId == userInfoId); + return userInfo; + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/UserRepository.cs b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/UserRepository.cs new file mode 100644 index 0000000000000000000000000000000000000000..064ff16904c9cdbd39c5f8a65e64e2ed33b3c4e7 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Repositories/UserRepository.cs @@ -0,0 +1,69 @@ +using Microsoft.EntityFrameworkCore; +using UniversalAdminSystem.Domian.UserManagement.Aggregates; +using UniversalAdminSystem.Domian.UserManagement.IRepository; +using UniversalAdminSystem.Domian.UserManagement.Entities; +using UniversalAdminSystem.Infrastructure.Persistence.DbContexts; +using UniversalAdminSystem.Domian.UserManagement.ValueObj; +using UniversalAdminSystem.Domian.Core.ValueObjects; + +namespace UniversalAdminSystem.Infrastructure.Persistence.Repositories; + +public class UserRepository : BaseRepository, IUserRepository +{ + public UserRepository(UniversalAdminSystemDbContext dbContext) : base(dbContext) + { + } + + public override async Task GetByGuidAsync(Guid guid) + { + var userId = UserId.Create(guid); + var user = await _TDs.FirstOrDefaultAsync(u => u.UserId == userId); + System.Console.WriteLine($"user:{user.Account}"); + return user; + } + + /// + /// 用户软删除 + /// + /// + /// + /// + public async Task DeleteUserSoftAsync(Guid id) + { + var user = await GetByGuidAsync(id) ?? throw new NullReferenceException("用户不存在"); + user.UpdateUserStatus(UserStatus.Deleted); + await Update(user); + } + + /// + /// 通过账号查询用户 + /// + /// + /// + public async Task GetUserByAccountAsync(string account) + { + var user = await _TDs.FirstOrDefaultAsync(u => u.Account == account); + return user; + } + + /// + /// 通过邮箱查询用户 + /// + /// + /// + public async Task GetUserByEmailAsync(string email) + { + var user = await _TDs.FirstOrDefaultAsync(u => u.Email == email); + return user; + } + + /// + /// 添加用户信息 + /// + /// + /// + public async Task AddUserInfoAsync(UserInfo userInfo) + { + await _DbContext.UserInfos.AddAsync(userInfo); + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Transaction/UnitOfWork.cs b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Transaction/UnitOfWork.cs new file mode 100644 index 0000000000000000000000000000000000000000..8317faa16324103d8277a660528cb77047aad704 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Persistence/Transaction/UnitOfWork.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Storage; +using UniversalAdminSystem.Application.Common.Interfaces; +using UniversalAdminSystem.Infrastructure.Persistence.DbContexts; + +namespace UniversalAdminSystem.Infrastructure.Persistence.Transaction; + +public class UnitOfWork : IUnitOfWork +{ + private readonly UniversalAdminSystemDbContext _context; + private IDbContextTransaction _transaction; + + public UnitOfWork(UniversalAdminSystemDbContext context) + { + _context = context; + } + public async Task BeginTransactionAsync() + { + _transaction = await _context.Database.BeginTransactionAsync(); + } + + public async Task CommitAsync() + { + await _context.SaveChangesAsync(); + await _transaction.CommitAsync(); + } + + public async Task RollbackAsync() + { + if (_transaction != null) + { + await _transaction.RollbackAsync(); + _transaction = null; + } + } + + public async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + return await _context.SaveChangesAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/RabbitMQ/Consumers/FileProcessingJobConsumer.cs b/backend/src/UniversalAdminSystem.Infrastructure/RabbitMQ/Consumers/FileProcessingJobConsumer.cs new file mode 100644 index 0000000000000000000000000000000000000000..ac40e67c08a32f5db0f848f0ee9d64554acf958c --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/RabbitMQ/Consumers/FileProcessingJobConsumer.cs @@ -0,0 +1,224 @@ +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using UniversalAdminSystem.Domian.Core.ValueObjects; +using UniversalAdminSystem.Domian.knowledge.Aggregates; +using UniversalAdminSystem.Domian.knowledge.IRepository; +using UniversalAdminSystem.Domian.knowledge.ValueObj; +using UniversalAdminSystem.Infrastructure.FileStorage.Parsers; +using UniversalAdminSystem.Infrastructure.RabbitMQ.Models.Messages; +using UniversalAdminSystem.Infrastructure.Services; +using UniversalAdminSystem.Application.Common.Interfaces; + + +namespace UniversalAdminSystem.Infrastructure.RabbitMQ.Consumers; + +public class FileProcessingJobConsumer : BackgroundService +{ + private readonly IConnection _conn; + private readonly ILogger _logger; + private readonly IServiceScopeFactory _scopeFactory; + + private readonly int _consumerCount = 5; + + public FileProcessingJobConsumer(IConnection conn, ILogger logger, IServiceScopeFactory scopeFactory) + { + _conn = conn; + _logger = logger; + _scopeFactory = scopeFactory; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("正在连接到 RabbitMQ..."); + try + { + var consumerTasks = new List(); + for (int i = 0; i < _consumerCount; i++) + { + consumerTasks.Add(AssignConsumerAsync(stoppingToken, i)); + } + await Task.WhenAll(consumerTasks); + } + catch (Exception ex) { _logger.LogError(ex, "文件处理任务消费失败"); } + } + + private async Task AssignConsumerAsync(CancellationToken stoppingToken, int consumerIndex) + { + var exchange = "file-processing"; + var queue = "file-processing-queue"; + var routingKey = "file-processing"; + + using var ch = _conn.CreateModel(); + ch.ExchangeDeclare(exchange, "topic", durable: true, autoDelete: false); + ch.QueueDeclare(queue, durable: true, exclusive: false, autoDelete: false, arguments: null); + ch.QueueBind(queue, exchange, routingKey); + ch.BasicQos(0, 1, false); + + _logger.LogInformation("消费者{consumerIndex}正在启动并等待消息...", consumerIndex); + + var consumer = new AsyncEventingBasicConsumer(ch); + consumer.Received += async (sender, ea) => + { + var message = Encoding.UTF8.GetString(ea.Body.ToArray()); + + _logger.LogInformation("消费者 {consumerIndex} 收到文件处理任务: {message}", consumerIndex, message); + + try + { + _logger.LogInformation("消费者 {consumerIndex} 收到文件处理任务: {message}", consumerIndex, message); + var fpm = JsonSerializer.Deserialize(message); + + if (fpm == null) + { + _logger.LogError("消息格式错误,跳过处理: {message}", message); + ch.BasicNack(ea.DeliveryTag, false, false); + return; + } + + if (fpm.NextAttemptAt.HasValue && fpm.NextAttemptAt > DateTime.UtcNow) + { + _logger.LogInformation("等待重试,当前时间:{DateTime.UtcNow},下一次尝试时间:{fpm.NextAttemptAt.Value}", DateTime.UtcNow, fpm.NextAttemptAt.Value); + await Task.Delay(fpm.NextAttemptAt.Value - DateTime.UtcNow, stoppingToken); + } + + await ProcessMessageAsync(fpm, stoppingToken, ea, ch); + + ch.BasicAck(ea.DeliveryTag, false); + _logger.LogInformation("消费者 {consumerIndex} 文件处理任务处理成功: {message}", consumerIndex, message); + } + catch (Exception ex) + { + var fpm = JsonSerializer.Deserialize(message); + if (fpm == null) + { + ch.BasicNack(ea.DeliveryTag, false, requeue: false); + return; + } + fpm.RetryCount++; + if (fpm.RetryCount >= 3) + { + _logger.LogError("消息 {fpm.JobId} 达到最大重试次数,丢弃消息: {message}", fpm.Id, message); + ch.BasicNack(ea.DeliveryTag, false, false); + return; + } + var retryDelay = Math.Min(300, Math.Pow(2, fpm.RetryCount)); + fpm.NextAttemptAt = DateTime.UtcNow.AddSeconds(retryDelay); + // 先 nack 再重新发布(都在同一个 ch) + ch.BasicNack(ea.DeliveryTag, false, requeue: false); + ch.BasicPublish("file-processing", "file-processing", null, Encoding.UTF8.GetBytes(JsonSerializer.Serialize(fpm))); + + _logger.LogError(ex, "消费者 {consumerIndex} 文件处理任务失败: {message}, 重试次数: {fpm.RetryCount}, 下次重试延迟: {retryDelay} 秒", consumerIndex, message, fpm.RetryCount, retryDelay); + } + }; + + var consumerTag = ch.BasicConsume(queue: queue, autoAck: false, consumer: consumer); + + try + { + // 阻塞直到停止 + while (!stoppingToken.IsCancellationRequested) + { + await Task.Delay(1000, stoppingToken); + } + } + finally + { + // 关闭 + try { ch.BasicCancel(consumerTag); } catch { } + try { ch.Close(); } catch { } + } + } + + private async Task ProcessMessageAsync(FileProcessingMessage fpm, CancellationToken stoppingToken, BasicDeliverEventArgs ea, IModel ch) + { + try + { + if (!File.Exists(fpm.FilePath)) + { + _logger.LogWarning("文件不存在,丢弃任务: {Path}", fpm.FilePath); + ch.BasicAck(ea.DeliveryTag, false); // 确认消费,RabbitMQ 删除消息 + return; + } + + // 文本提取 + _logger.LogInformation("开始处理文件: {FilePath}", fpm.FilePath); + var parser = new DocParserFactory().GetParser(fpm.ContentType); + var text = await parser.ParseAsync(fpm.FilePath); + _logger.LogInformation("文件处理完成: {FilePath}", fpm.FilePath); + + using var scope = _scopeFactory.CreateScope(); + var spacy = scope.ServiceProvider.GetRequiredService(); + var k2 = scope.ServiceProvider.GetRequiredService(); + var embedding = scope.ServiceProvider.GetRequiredService(); + var docChunkRepo = scope.ServiceProvider.GetRequiredService(); + var work = scope.ServiceProvider.GetRequiredService(); + + // 去重 + var exists = await docChunkRepo.ExistsByFileIdAsync(fpm.FileId); + if (exists) + { + _logger.LogInformation("文件 {fpm.FileId} 已处理,跳过", fpm.FileId); + return; + } + + // 文本分片 + _logger.LogInformation("开始分片文件: {Text}", text); + var preprocessResult = await spacy.AnalyzeTextAsync(text); + var preprocessResultJson = JsonSerializer.Serialize(preprocessResult, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + var chunks = await k2.SendChunkingRequestAsync(preprocessResultJson); + _logger.LogInformation("分片完成,生成 {Count} 个 chunk", chunks.Count); + + // Embedding + _logger.LogInformation("开始Embedding"); + var embeddings = new List(); + embeddings.AddRange(await embedding.GetEmbeddingAsync(chunks)); + _logger.LogInformation("Embedding 完成: {Count} 个向量", embeddings.Count); + try + { + await work.BeginTransactionAsync(); + + // Vector Store + _logger.LogInformation("开始Vector Store文件: {FilePath}", fpm.FilePath); + var docChunks = new List(); + for (int i = 0; i < chunks.Count; i++) + { + var c = chunks[i]; + var e = embeddings[i]; + + var docChunk = DocumentChunk.CreateDocumentChunk( + ChunkId.Create(Guid.NewGuid()), + fpm.FileId, + c, + TextEmbedding.Create(e) + ); + + docChunks.Add(docChunk); + } + + await docChunkRepo.BulkAddDocumentChunkAsync(docChunks); + await work.CommitAsync(); + + // 文件处理完成 + _logger.LogInformation("文件处理完成: {FilePath}", fpm.FilePath); + } + catch (System.Exception) + { + await work.RollbackAsync(); + throw; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "文件处理任务失败: {Message}", fpm); + throw; + } + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/RabbitMQ/Publishers/FileProcessingJobPublisher.cs b/backend/src/UniversalAdminSystem.Infrastructure/RabbitMQ/Publishers/FileProcessingJobPublisher.cs new file mode 100644 index 0000000000000000000000000000000000000000..597597c81ad35d2166b4f76ce9077a568ec3ddba --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/RabbitMQ/Publishers/FileProcessingJobPublisher.cs @@ -0,0 +1,73 @@ +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using UniversalAdminSystem.Application.Common.Interfaces; +using UniversalAdminSystem.Infrastructure.Persistence.DbContexts; +using UniversalAdminSystem.Infrastructure.RabbitMQ.Models.Messages; + +namespace UniversalAdminSystem.Infrastructure.RabbitMQ.Publishers; + +public class FileProcessingJobPublisher : BackgroundService +{ + private readonly IServiceScopeFactory _sf; + private readonly IEventBus _bus; + private readonly ILogger _logger; + + public FileProcessingJobPublisher(IServiceScopeFactory sf, IEventBus bus, ILogger logger) + { _sf = sf; _bus = bus; _logger = logger; } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + await Task.Run(async () => + { + try + { + using var scope = _sf.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + var jobs = await db.FileProcessingJobs + .Where(x => x.Status == "Pending" && (x.NextAttemptAt == null || x.NextAttemptAt <= DateTime.UtcNow)) + .OrderBy(x => x.CreatedAt) + .Take(100) + .ToListAsync(stoppingToken); + + foreach (var j in jobs) j.Status = "Processing"; + await db.SaveChangesAsync(stoppingToken); + + foreach (var j in jobs) + { + var message = new FileProcessingMessage + { + Id = j.Id, + FileId = j.FileId, + FilePath = j.FilePath, + ContentType = j.ContentType, + Size = j.Size, + RetryCount = j.RetryCount + }; + var payload = JsonSerializer.Serialize(message); + try + { + await _bus.PublishAsync("file-processing", "file-processing", payload, messageId: j.Id.ToString(), stoppingToken); + j.Status = "Succeeded"; + } + catch (Exception ex) + { + j.RetryCount++; + j.Status = "Pending"; + j.NextAttemptAt = DateTime.UtcNow.AddSeconds(Math.Min(300, Math.Pow(2, j.RetryCount))); + _logger.LogError(ex, "发布失败: {JobId}", j.Id); + } + } + await db.SaveChangesAsync(stoppingToken); + } + catch (Exception ex) { _logger.LogError(ex, "文件处理任务发布循环错误"); } + }); + + await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); + } + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/RabbitMQ/Queues/EfFileProcessingQueue.cs b/backend/src/UniversalAdminSystem.Infrastructure/RabbitMQ/Queues/EfFileProcessingQueue.cs new file mode 100644 index 0000000000000000000000000000000000000000..26339aecb01bd86b6c664ffd49b264f79ac55418 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/RabbitMQ/Queues/EfFileProcessingQueue.cs @@ -0,0 +1,50 @@ +using Microsoft.Extensions.Logging; +using UniversalAdminSystem.Application.FileStorage.DTOs; +using UniversalAdminSystem.Application.FileStorage.Interfaces; +using UniversalAdminSystem.Infrastructure.Persistence.DbContexts; +using UniversalAdminSystem.Infrastructure.RabbitMQ.Jobs; + +namespace UniversalAdminSystem.Infrastructure.RabbitMQ.Queues; + +public class EfFileProcessingQueue : IFileProcessingQueue +{ + private readonly UniversalAdminSystemDbContext _dbContext; + private readonly ILogger _logger; + + public EfFileProcessingQueue(UniversalAdminSystemDbContext dbContext, ILogger logger) + { + _dbContext = dbContext; + _logger = logger; + } + + public async Task EnqueueAsync(FileProcessingJobDto jd, CancellationToken ct = default) + { + try + { + _logger.LogInformation("准备插入任务: {FileId}, FilePath: {FilePath}", jd.FileId, jd.FilePath); + _dbContext.FileProcessingJobs.Add(new FileProcessingJob + { + FileId = jd.FileId, + FilePath = jd.FilePath, + ContentType = jd.ContentType, + Size = jd.Size + }); + _logger.LogInformation("DbContext IsConnected: {IsConnected}", _dbContext.Database.CanConnect()); + await _dbContext.SaveChangesAsync(ct); + _logger.LogInformation("文件处理任务已入队: {FileId}", jd.FileId); + } + catch (Exception ex) + { + _logger.LogError(ex, "文件处理任务入队失败: {FileId}", jd.FileId); + throw; + } + } +} +// public class EfFileProcessingQueue : IFileProcessingQueue +// { +// public Task EnqueueAsync(FileProcessingJobDto jd, CancellationToken ct = default) +// { +// // 什么都不做,直接返回 +// return Task.CompletedTask; +// } +// } \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/RabbitMQ/RabbitMqEventBus.cs b/backend/src/UniversalAdminSystem.Infrastructure/RabbitMQ/RabbitMqEventBus.cs new file mode 100644 index 0000000000000000000000000000000000000000..66cd7b98c734fbb937bdcb3bfd9e95f11a6226cc --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/RabbitMQ/RabbitMqEventBus.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.Configuration; +using UniversalAdminSystem.Application.Common.Interfaces; +using System.Text; +using RabbitMQ.Client; + +namespace UniversalAdminSystem.Infrastructure.RabbitMQ; + +public class RabbitMqEventBus : IEventBus, IDisposable +{ + private readonly IConnection _conn; + + public RabbitMqEventBus(IConnection conn, IConfiguration _) + { + _conn = conn; + } + + public Task PublishAsync(string exchange, string routingKey, string payload, string? messageId = null, CancellationToken ct = default) + { + // 每次发布各自创建 channel,线程安全 & 生命周期正确 + using var ch = _conn.CreateModel(); + ch.ConfirmSelect(); + var props = ch.CreateBasicProperties(); + props.Persistent = true; + props.MessageId = messageId; + props.ContentType = "application/json"; + var body = Encoding.UTF8.GetBytes(payload); + ch.BasicPublish(exchange, routingKey, mandatory: false, basicProperties: props, body: body); + + // publisher confirm,确保消息真正进 broker + ch.WaitForConfirmsOrDie(TimeSpan.FromSeconds(5)); + return Task.CompletedTask; + } + + public void Dispose() { } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/RabbitMQ/RabbitMqTopologyInitializer.cs b/backend/src/UniversalAdminSystem.Infrastructure/RabbitMQ/RabbitMqTopologyInitializer.cs new file mode 100644 index 0000000000000000000000000000000000000000..04da06a094212dce66c300541768b192ce882fe4 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/RabbitMQ/RabbitMqTopologyInitializer.cs @@ -0,0 +1,37 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Configuration; +using RabbitMQ.Client; + +namespace UniversalAdminSystem.Infrastructure.RabbitMQ; + +public class RabbitMqTopologyInitializer : IHostedService +{ + private readonly IConnection _conn; + private readonly IConfiguration _cfg; + private readonly ILogger _logger; + + public RabbitMqTopologyInitializer(IConnection conn, IConfiguration cfg, ILogger logger) + { + _conn = conn; _cfg = cfg; _logger = logger; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + using var ch = _conn.CreateModel(); + var exchange = _cfg["RabbitMq:Exchange"] ?? "file-processing"; + var queue = _cfg["RabbitMq:Queue"] ?? "file-processing-queue"; + var routingKey = _cfg["RabbitMq:RoutingKey"] ?? "file-processing"; + + ch.ExchangeDeclare(exchange, type: "topic", durable: true, autoDelete: false); + ch.QueueDeclare(queue, durable: true, exclusive: false, autoDelete: false, arguments: null); + ch.QueueBind(queue, exchange, routingKey); + + _logger.LogInformation("RabbitMQ 拓扑已就绪: exchange={Exchange}, queue={Queue}, routingKey={RoutingKey}", + exchange, queue, routingKey); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Services/AnswerService.cs b/backend/src/UniversalAdminSystem.Infrastructure/Services/AnswerService.cs new file mode 100644 index 0000000000000000000000000000000000000000..2b635ab2ca51e0ed4d596ec503edc37a4808d5e5 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Services/AnswerService.cs @@ -0,0 +1,120 @@ +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.DependencyInjection; +using UniversalAdminSystem.Application.AIQuestions.Interfaces; +using UniversalAdminSystem.Domian.FileStorage.ValueObjects; +using UniversalAdminSystem.Domian.knowledge.Aggregates; +using UniversalAdminSystem.Domian.knowledge.IRepository; +using UniversalAdminSystem.Domian.knowledge.ValueObj; +using UniversalAdminSystem.Domian.UserConversations.Aggregates; +using UniversalAdminSystem.Domian.UserConversations.IRepository; +using UniversalAdminSystem.Infrastructure.Configs; + +namespace UniversalAdminSystem.Infrastructure.Services; + +public class AnswerService : IAnswerService +{ + private readonly IServiceScopeFactory _scopeFactory; + private readonly IMessageRepository _messageRepo; + private readonly IDocumentChunkRepository _docChunkRepo; + + public AnswerService(IServiceScopeFactory scopeFactory, IMessageRepository messageRepo, IDocumentChunkRepository docChunkRepo) + { + _scopeFactory = scopeFactory; + _messageRepo = messageRepo; + _docChunkRepo = docChunkRepo; + } + + public async Task AnswerAsync(string userInput, IEnumerable? messages) + { + using var scope = _scopeFactory.CreateScope(); + var spacy = scope.ServiceProvider.GetRequiredService(); + var k2 = scope.ServiceProvider.GetRequiredService(); + var embedding = scope.ServiceProvider.GetRequiredService(); + + System.Console.WriteLine($"用户输入参数:{userInput}"); + + // 1. 解析用户输入 + var parsedUserInput = await spacy.ParseUserInputAsync(userInput); + System.Console.WriteLine($"用户输入解析:{parsedUserInput}"); + var parsedUserInputJson = JsonSerializer.Serialize(parsedUserInput, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + System.Console.WriteLine($"用户输入解析结果:{parsedUserInputJson}"); + + // 2. 让 K2 进行分片 + var chunksText = await k2.SendChunkingRequestAsync(parsedUserInputJson); + + // 3. 向量化分片 + var userInputEmbeddings = await embedding.GetEmbeddingAsync(chunksText); + + System.Console.WriteLine($"向量化:{userInputEmbeddings.Count}"); + + // 4. 相似文档检索(并行) + var allChunks = new List(); + + var retrievalTasks = userInputEmbeddings.Select(async vec => + { + System.Console.WriteLine("查询文档开始"); + var publicDocs = await _docChunkRepo.FindSimilarDocumentsAsync(TextEmbedding.Create(vec), FileAccessLevel.Public); + System.Console.WriteLine($"公开文档数:{publicDocs.Count()}"); + var restrictedDocs = await _docChunkRepo.FindSimilarDocumentsAsync(TextEmbedding.Create(vec), FileAccessLevel.Restricted); + System.Console.WriteLine($"受限文档数:{publicDocs.Count()}"); + return publicDocs.Concat(restrictedDocs); + }); + + + + var retrievedResults = await Task.WhenAll(retrievalTasks); + allChunks.AddRange(retrievedResults.SelectMany(r => r)); + + System.Console.WriteLine($"文档检索条数:{allChunks.Count}"); + + // 5. 重排文档 + var sortedChunks = allChunks + .Where(c => !string.IsNullOrWhiteSpace(c.Content)) + .OrderByDescending(chunk => chunk.SimilarityScore) + .Distinct() + .ToList(); + + // 6. 构造增强 Prompt(取前 N 个最相关文档) + var topContext = string.Join("\n\n", sortedChunks.Take(5).Select(c => c.Content)); + + var systemPrompt = $@"你是一个具有超长记忆的语言大师,你的名字叫做初音未来。请基于以下参考文档内容和用户问题,精准、详细地回答问题。现在是 {DateTime.Now:yyyy/MM/dd HH:mm:ss dddd}, 参考文档:{topContext}".Trim(); + + // 7. 构造对话消息 + List chatMessages; + if (messages != null && messages.Any()) + { + System.Console.WriteLine("列表为不为空"); + System.Console.WriteLine($"输入消息列表为:{messages}"); + chatMessages = messages.Select(m => new K2Message + { + Role = m.Role, + Content = m.Content + }).ToList(); + System.Console.WriteLine($"输出消息列表为:{chatMessages}"); + chatMessages.Insert(0, new K2Message { Role = "system", Content = systemPrompt }); + } + else + { + System.Console.WriteLine("列表为为空"); + chatMessages = new List + { + new K2Message { Role = "system", Content = systemPrompt }, + new K2Message { Role = "user", Content = userInput } + }; + } + + // 8. 调用 K2 模型并接收流式结果 + var sb = new StringBuilder(); + await foreach (var chunk in k2.SendChatRequestAsync(chatMessages)) + { + sb.Append(chunk); + } + + return sb.ToString(); + } + +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Services/EmbeddingService.cs b/backend/src/UniversalAdminSystem.Infrastructure/Services/EmbeddingService.cs new file mode 100644 index 0000000000000000000000000000000000000000..218a343954d02a33e35356c151dfc6badc1620b7 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Services/EmbeddingService.cs @@ -0,0 +1,172 @@ +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using Microsoft.Extensions.Options; +using UniversalAdminSystem.Infrastructure.Configs; + +namespace UniversalAdminSystem.Infrastructure.Services; + +public class EmbeddingService +{ + private readonly TongyiConfig _tongyiConfig; + private readonly HttpClient _httpClient; + + public EmbeddingService(IOptions tongyiConfig, IHttpClientFactory httpClientFactory) + { + _tongyiConfig = tongyiConfig.Value; + _httpClient = httpClientFactory.CreateClient("EmbeddingClient"); + _httpClient.BaseAddress = new Uri(_tongyiConfig.BaseUrl); + _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_tongyiConfig.ApiKey}"); + _httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); + } + + public async Task> GetEmbeddingAsync(List texts) + { + HttpRequestMessage request; + HttpResponseMessage response; + const int batchSize = 10; + var allEmbeddings = new List(); + + for (int i = 0; i < texts.Count; i += batchSize) + { + var batch = texts.Skip(i).Take(batchSize).ToArray(); + var payload = new + { + model = "text-embedding-v4", + input = batch, + encodingFormat = "float" + }; + Console.WriteLine("Request Body: " + JsonSerializer.Serialize(payload)); + request = new HttpRequestMessage(HttpMethod.Post, $"{_tongyiConfig.BaseUrl}compatible-mode/v1/embeddings") + { + Content = new StringContent(JsonSerializer.Serialize(payload, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }), Encoding.UTF8, "application/json") + }; + response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None); + // response.EnsureSuccessStatusCode(); + var json = await response.Content.ReadAsStringAsync(); + Console.WriteLine($"状态码: {response.StatusCode}, 响应: {json}"); + var embeddingResponse = JsonSerializer.Deserialize(json, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + if (embeddingResponse?.Data != null) + { + allEmbeddings.AddRange(embeddingResponse.Data.Select(d => d.Embedding.ToArray())); + } + } + + return allEmbeddings; + } + + public async Task SubmitEmbeddingTaskAsync(string url) + { + HttpRequestMessage request; + HttpResponseMessage response; + var payload = new + { + model = "text-embedding-async-v2", + input = url, + parameters = new { textType = "query" } + }; + request = new HttpRequestMessage(HttpMethod.Post, $"{_tongyiConfig.BaseUrl}api/v1/services/embeddings/text-embedding/text-embedding") + { + Content = new StringContent(JsonSerializer.Serialize(payload, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }), Encoding.UTF8, "application/json") + }; + request.Headers.Add("X-DashScope-Async", "enable"); + response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(); + var res = JsonSerializer.Deserialize(json); + + return res.Output.TaskId; + } + + public async Task WaitForTaskAsync(string taskId, int pollIntervalSeconds = 5) + { + while (true) + { + var response = await _httpClient.GetAsync($"tasks/{taskId}"); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(); + var status = JsonSerializer.Deserialize(json); + + Console.WriteLine($"任务状态: {status.Output.TaskStatus}"); + + if (status.Output.TaskStatus == "SUCCEEDED") + { + return status.Output.ResultUrl; + } + else if (status.Output.TaskStatus == "FAILED") + { + throw new Exception("向量化任务失败"); + } + + await Task.Delay(TimeSpan.FromSeconds(pollIntervalSeconds)); + } + } + + public async Task DownloadResultAsync(string resultUrl) + { + var result = await _httpClient.GetStringAsync(resultUrl); + return result; + } +} +public class EmbeddingResponse +{ + public string Id { get; set; } + + public List Data { get; set; } + + public string Object { get; set; } + + public string Model { get; set; } + + public Usage Usage { get; set; } + +} + +public class EmbeddingData +{ + public string Object { get; set; } + public int Index { get; set; } + public List Embedding { get; set; } +} + +public class Usage +{ + public int PromptTokens { get; set; } + + public int TotalTokens { get; set; } +} + +public class AsyncEmbeddingResponse +{ + public string RequestId { get; set; } + public OutputData Output { get; set; } +} + +public class OutputData +{ + public string TaskId { get; set; } + public string TaskStatus { get; set; } +} + +public class TaskStatusResponse +{ + public string RequestId { get; set; } + public TaskOutput Output { get; set; } +} + +public class TaskOutput +{ + public string TaskStatus { get; set; } + public string ResultUrl { get; set; } +} diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Services/FileScaner.cs b/backend/src/UniversalAdminSystem.Infrastructure/Services/FileScaner.cs new file mode 100644 index 0000000000000000000000000000000000000000..b2e4c5edc4ba667639d9ecc5b24cf79f54a31d5a --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Services/FileScaner.cs @@ -0,0 +1,38 @@ +namespace UniversalAdminSystem.Infrastructure.Services; + +public class FileScaner +{ + public static IEnumerable ScanDirectory(string path, string searchPattern) + { + var fileSystemInfos = new List(); + if (!Directory.Exists(path)) throw new DirectoryNotFoundException($"目录不存在: {path}"); + try + { + ScanInternal(path, searchPattern, fileSystemInfos); + } + catch (UnauthorizedAccessException) + { + throw new UnauthorizedAccessException($"无权限访问: {path}"); + } + return fileSystemInfos; + } + + private static void ScanInternal(string path, string searchPattern, List fileSystemInfos) + { + var directoryInfo = new DirectoryInfo(path); + fileSystemInfos.Add(directoryInfo); + var files = directoryInfo.GetFiles(searchPattern); + fileSystemInfos.AddRange(files); + foreach (var subDirectory in directoryInfo.GetDirectories(searchPattern)) + { + try + { + ScanInternal(subDirectory.FullName, searchPattern, fileSystemInfos); + } + catch (UnauthorizedAccessException) + { + continue; + } + } + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Services/K2ModelService.cs b/backend/src/UniversalAdminSystem.Infrastructure/Services/K2ModelService.cs new file mode 100644 index 0000000000000000000000000000000000000000..7422ee855a2a9885240bdb3735e3553bff1cdd9a --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Services/K2ModelService.cs @@ -0,0 +1,196 @@ +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using UniversalAdminSystem.Infrastructure.Configs; + +namespace UniversalAdminSystem.Infrastructure.Services; + +public partial class K2ModelService +{ + private readonly K2Config _k2Config; + private readonly ILogger _logger; + private readonly HttpClient _httpClient; + public event OnContentChunkReceivedHandler? OnContentChunkReceived; + + public delegate void OnContentChunkReceivedHandler(string chunk); + + public K2ModelService(IOptions k2Config, ILogger logger, IHttpClientFactory httpClientFactory) + { + _k2Config = k2Config.Value; + _logger = logger; + _httpClient = httpClientFactory.CreateClient("K2Client"); + _httpClient.BaseAddress = new Uri(_k2Config.BaseUrl); + _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_k2Config.ApiKey}"); + _httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); + } + + public async IAsyncEnumerable SendChatRequestAsync(List messages, string model = "Moonshot-Kimi-K2-Instruct", float temperature = 0.7f, int maxTokens = 1000) + { + HttpRequestMessage request; + HttpResponseMessage response; + try + { + _logger.LogInformation("开始发送K2模型请求,使用模型: {Model}", model); + var k2Request = new K2Request + { + Model = model, + Messages = messages, + Temperature = temperature, + MaxTokens = maxTokens + }; + request = new HttpRequestMessage(HttpMethod.Post, $"{_k2Config.BaseUrl}chat/completions") + { + Content = new StringContent(JsonSerializer.Serialize(k2Request, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }), Encoding.UTF8, "application/json") + }; + response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None); + response.EnsureSuccessStatusCode(); + } + catch (Exception ex) + { + _logger.LogError(ex, "K2模型请求过程中发生错误"); + throw; + } + await using var responseStream = await response.Content.ReadAsStreamAsync(); + var sb = new StringBuilder(); + await foreach (var chunk in ParseStreamingResponseAsync(responseStream, sb)) + { + yield return chunk; + } + _logger.LogInformation("K2模型请求成功完成"); + } + + private async IAsyncEnumerable ParseStreamingResponseAsync(Stream responseStream, StringBuilder answer) + { + using var reader = new StreamReader(responseStream); + var responseId = ""; + var responseModel = ""; + long responseCreated = 0; + var responseObject = ""; + + var chunk = new K2StreamResponse(); + while (!reader.EndOfStream) + { + var line = await reader.ReadLineAsync(); + if (string.IsNullOrWhiteSpace(line)) continue; + var trimmedLine = line.Trim(); + if (trimmedLine.StartsWith("data: ")) + { + var jsonData = trimmedLine.Substring(6); + if (jsonData.Trim() == "[DONE]") continue; + try + { + // 解析每个数据块 + chunk = JsonSerializer.Deserialize(jsonData, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + } + catch (JsonException ex) + { + _logger.LogWarning("解析数据块失败: {JsonData}, 错误: {Error}", jsonData, ex.Message); + } + + if (chunk != null) + { + // 保存响应元数据 + if (!string.IsNullOrEmpty(chunk.Id)) responseId = chunk.Id; + if (!string.IsNullOrEmpty(chunk.Model)) responseModel = chunk.Model; + if (!string.IsNullOrEmpty(chunk.Object)) responseObject = chunk.Object; + if (chunk.Created > 0) responseCreated = chunk.Created; + + // 处理choices + if (chunk.Choices != null && chunk.Choices.Count > 0) + { + var choice = chunk.Choices[0]; + if (choice.Delta != null && !string.IsNullOrEmpty(choice.Delta.Content)) + { + answer.Append(choice.Delta.Content); + OnContentChunkReceived?.Invoke(choice.Delta.Content); + yield return choice.Delta.Content; + } + } + } + } + } + } + + public async Task> SendChunkingRequestAsync(string preprocessedJson) + { + HttpRequestMessage request; + HttpResponseMessage response; + try + { + string model = "Moonshot-Kimi-K2-Instruct"; float temperature = 0.7f; int maxTokens = 1000; + _logger.LogInformation("开始发送K2模型请求,使用模型: {Model}", model); + var k2Request = new K2Request + { + Model = model, + Messages = { + new K2Message { Role = "system", Content = "你是一个文本分片专家,任务是将文本分割成合适大小的块(chunk),每个 chunk 不超过 500 个 token。在分片时需要遵守以下规则:1. 尽量在句子边界进行切分。2. 不要将命名实体(例如人名、地名、作品名等)拆分到不同 chunk 中。3. 尽可能保证每个 chunk 的语义完整。4. 如果文本中有无标点长句,可以在合适位置手动加切分点,但要保证语义不被破坏。" }, + new K2Message { Role = "user", Content = $"帮我完成精准的分片,以下是文本的预处理结果:\n{preprocessedJson}。\n必须以Json格式返回结果,每个分片包含chunk_id、content、token_count字段" }, + }, + Temperature = temperature, + MaxTokens = maxTokens, + }; + request = new HttpRequestMessage(HttpMethod.Post, $"{_k2Config.BaseUrl}chat/completions") + { + Content = new StringContent(JsonSerializer.Serialize(k2Request, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }), Encoding.UTF8, "application/json") + }; + response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None); + response.EnsureSuccessStatusCode(); + } + catch (Exception ex) + { + _logger.LogError(ex, "K2模型请求过程中发生错误"); + throw; + } + await using var responseStream = await response.Content.ReadAsStreamAsync(); + var sb = new StringBuilder(); + await foreach (var chunk in ParseStreamingResponseAsync(responseStream, sb)) { } + + return ParseChunk(sb.ToString()); + } + + public List ParseChunk(string chunkJson) + { + var chunks = new List(); + using var jsonDoc = JsonDocument.Parse(chunkJson); + + JsonElement root = jsonDoc.RootElement; + + // 如果根是对象并且包含 "chunks" + if (root.ValueKind == JsonValueKind.Object && root.TryGetProperty("chunks", out var chunksProp)) + { + if (chunksProp.ValueKind == JsonValueKind.Array) + { + foreach (var chunk in chunksProp.EnumerateArray()) + ExtractContent(chunk, chunks); + } + } + else if (root.ValueKind == JsonValueKind.Array) + { + foreach (var chunk in root.EnumerateArray()) + ExtractContent(chunk, chunks); + } + + return chunks; + } + + private void ExtractContent(JsonElement chunk, List chunks) + { + if (chunk.TryGetProperty("content", out var jsonContent)) + { + var content = jsonContent.GetString(); + if (!string.IsNullOrEmpty(content)) + chunks.Add(content); + } + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Services/PasswordHelper.cs b/backend/src/UniversalAdminSystem.Infrastructure/Services/PasswordHelper.cs new file mode 100644 index 0000000000000000000000000000000000000000..1a7261cf71fe05e8a1f94826013289fed31aa03c --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Services/PasswordHelper.cs @@ -0,0 +1,62 @@ +using System.Security.Cryptography; +using System.Text.RegularExpressions; +using UniversalAdminSystem.Application.UserManagement.Interface; + +namespace UniversalAdminSystem.Infrastructure.Services; + +public class PasswordHelper : IPasswordHelper +{ + private const int SaltSize = 16; + private const int HashSize = 32; + private const int Iterations = 1000; + + /// + /// 密码加密 + /// + /// + /// + public (string hashedPassword, string salt) HashPasswordWithSeparateSalt(string password) + { + if (!Regex.IsMatch(password, "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z][^\u4e00-\u9fa5]{5,19}$", RegexOptions.IgnoreCase)) throw new ArgumentException("密码必须是数字+字母组合并且不含中文 长度6至20位"); + byte[] salt = new byte[SaltSize]; + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(salt); + } + + using (var pbkdf2 = new Rfc2898DeriveBytes(password, salt, Iterations, HashAlgorithmName.SHA256)) + { + byte[] hash = pbkdf2.GetBytes(HashSize); + return (hashedPassword: Convert.ToBase64String(hash), + salt: Convert.ToBase64String(salt)); + } + + } + /// + /// 密码比对 + /// + /// + /// + /// + /// + public bool VerifyPasswordWithSeparateSalt(string password, string storedHashedPassword, string storedSalt) + { + byte[] storedHash = Convert.FromBase64String(storedHashedPassword); + byte[] salt = Convert.FromBase64String(storedSalt); + + using (var pbkdf2 = new Rfc2898DeriveBytes( + password, + salt, + Iterations, + HashAlgorithmName.SHA256)) + { + byte[] computedHash = pbkdf2.GetBytes(HashSize); + + // 安全比较 + return CryptographicOperations.FixedTimeEquals( + storedHash.AsSpan(), + computedHash.AsSpan()); + } + } + +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Services/PermissionCacheService.cs b/backend/src/UniversalAdminSystem.Infrastructure/Services/PermissionCacheService.cs new file mode 100644 index 0000000000000000000000000000000000000000..f7ae1a9fc349f1d5cbd0560eefe0f86f4c4b38b0 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Services/PermissionCacheService.cs @@ -0,0 +1,105 @@ +// using Microsoft.Extensions.Caching.Memory; +// using UniversalAdminSystem.Application.PermissionManagement.Interfaces; + +// namespace UniversalAdminSystem.Infrastructure.Services; + +// /// +// /// 权限缓存服务 +// /// 提供权限信息的缓存功能,提高权限检查性能 +// /// +// public class PermissionCacheService +// { +// private readonly IMemoryCache _cache; +// private readonly IPermissionCheckService _permissionCheckService; +// private readonly TimeSpan _cacheExpiration = TimeSpan.FromMinutes(30); // 缓存30分钟 + +// public PermissionCacheService(IMemoryCache cache, IPermissionCheckService permissionCheckService) +// { +// _cache = cache; +// _permissionCheckService = permissionCheckService; +// } + +// /// +// /// 检查用户是否有指定权限(带缓存) +// /// +// /// 用户ID +// /// 权限编码 +// /// 是否有权限 +// public async Task CheckUserPermissionAsync(Guid userId, string permissionCode) +// { +// var cacheKey = $"user_permission_{userId}_{permissionCode}"; + +// if (_cache.TryGetValue(cacheKey, out bool cachedResult)) +// { +// return cachedResult; +// } + +// var result = await _permissionCheckService.CheckUserPermissionAsync(userId, permissionCode); +// _cache.Set(cacheKey, result, _cacheExpiration); + +// return result; +// } + +// /// +// /// 获取用户的所有权限(带缓存) +// /// +// /// 用户ID +// /// 权限编码列表 +// public async Task> GetUserPermissionsAsync(Guid userId) +// { +// var cacheKey = $"user_permissions_{userId}"; + +// if (_cache.TryGetValue(cacheKey, out IEnumerable cachedPermissions)) +// { +// return cachedPermissions; +// } + +// var permissions = await _permissionCheckService.GetUserPermissionAsync(userId); +// _cache.Set(cacheKey, permissions, _cacheExpiration); + +// return permissions; +// } + +// /// +// /// 获取用户的所有角色(带缓存) +// /// +// /// 用户ID +// /// 角色名称列表 +// public async Task> GetUserRolesAsync(Guid userId) +// { +// var cacheKey = $"user_roles_{userId}"; + +// if (_cache.TryGetValue(cacheKey, out IEnumerable cachedRoles)) +// { +// return cachedRoles; +// } + +// var roles = await _permissionCheckService.GetUserRolesAsync(userId); +// _cache.Set(cacheKey, roles, _cacheExpiration); + +// return roles; +// } + +// /// +// /// 清除用户权限缓存 +// /// +// /// 用户ID +// public void ClearUserCache(Guid userId) +// { +// var pattern = $"user_*_{userId}*"; +// // 注意:IMemoryCache不支持模式匹配删除,这里只是示例 +// // 在实际项目中,可能需要使用Redis或其他支持模式匹配的缓存 +// _cache.Remove($"user_permissions_{userId}"); +// _cache.Remove($"user_roles_{userId}"); +// } + +// /// +// /// 清除所有权限缓存 +// /// +// public void ClearAllCache() +// { +// // 注意:IMemoryCache不支持清空所有缓存 +// // 在实际项目中,可能需要使用Redis或其他缓存方案 +// Console.WriteLine("权限缓存已清除"); +// } +// } \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Services/PermissionInitializationService.cs b/backend/src/UniversalAdminSystem.Infrastructure/Services/PermissionInitializationService.cs new file mode 100644 index 0000000000000000000000000000000000000000..cfc412c25f6e427f8bebf9128f7976c4b4398c36 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Services/PermissionInitializationService.cs @@ -0,0 +1,106 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using UniversalAdminSystem.Domian.PermissionManagement.Aggregate; +using UniversalAdminSystem.Domian.PermissionManagement.IRepository; +using UniversalAdminSystem.Domian.PermissionManagement.ValueObjects; +using UniversalAdminSystem.Domian.PermissionManagement.Services; + +namespace UniversalAdminSystem.Infrastructure.Services; + +public class PermissionInitializationService : IHostedService +{ + private readonly SystemPermissionConfigLoader _loader; + private readonly IServiceScopeFactory _scopeFactory; + private static bool _initialized = false; + + public PermissionInitializationService( + SystemPermissionConfigLoader loader, + IServiceScopeFactory scopeFactory + ) + { + _loader = loader; + _scopeFactory = scopeFactory; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + if (_initialized) + { + return; + } + + cancellationToken.ThrowIfCancellationRequested(); + + try + { + // 首先加载权限规则配置 + var rulesConfigPath = Path.Combine(AppContext.BaseDirectory, "PermissionRules.json"); + if (File.Exists(rulesConfigPath)) + { + var rulesContent = await File.ReadAllTextAsync(rulesConfigPath, cancellationToken); + var rulesDict = System.Text.Json.JsonSerializer.Deserialize>>(rulesContent); + + if (rulesDict != null) + { + var rules = rulesDict.ToDictionary( + kv => Enum.Parse(kv.Key, true), + kv => kv.Value.ToHashSet() + ); + + ResourceActionValidator.LoadRules(rules); + Console.WriteLine("权限规则配置加载成功"); + } + } + else + { + Console.WriteLine("权限规则配置文件不存在,使用默认配置"); + // 设置默认规则 + var defaultRules = new Dictionary> + { + { PermissionAction.Read, new HashSet { "data", "file", "user", "role", "permission", "config", "system" } }, + { PermissionAction.Create, new HashSet { "data", "user", "file", "role", "permission" } }, + { PermissionAction.Update, new HashSet { "data", "user", "config", "role", "permission" } }, + { PermissionAction.Delete, new HashSet { "data", "user", "file", "role", "permission" } }, + { PermissionAction.Manage, new HashSet { "system", "user" } } + }; + ResourceActionValidator.LoadRules(defaultRules); + } + + // 加载配置文件 + var configPath = Path.Combine(AppContext.BaseDirectory, "SystemPermissions.json"); + var permissions = await _loader.LoadFromFileAsync(configPath); + + // 初始化到数据库 + using (var scope = _scopeFactory.CreateScope()) + { + var permissionRepository = scope.ServiceProvider.GetRequiredService(); + + var systemPermissions = new List(); + foreach (var perm in permissions) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (!Enum.TryParse(perm.Action, out var action)) + { + throw new InvalidOperationException($"无效的权限操作: {perm.Action}"); + } + + var sysPerm = Permission.CreateSystemPermission(perm.Name, perm.Resource, (int)action); + systemPermissions.Add(sysPerm); + } + + // 持久化 + await permissionRepository.AddSystemPermissionsAsync(systemPermissions); + Console.WriteLine($"成功初始化 {systemPermissions.Count} 个系统权限"); + } + + _initialized = true; + } + catch (Exception ex) + { + throw new InvalidOperationException("初始化系统权限失败", ex); + } + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Services/PermissionRuleConfigService.cs b/backend/src/UniversalAdminSystem.Infrastructure/Services/PermissionRuleConfigService.cs new file mode 100644 index 0000000000000000000000000000000000000000..bf10a035c7624728dd040c9d7ce0b8c8475e158d --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Services/PermissionRuleConfigService.cs @@ -0,0 +1,112 @@ +using UniversalAdminSystem.Domian.PermissionManagement.Services; +using UniversalAdminSystem.Domian.PermissionManagement.ValueObjects; + +namespace UniversalAdminSystem.Infrastructure.Services; + +/// +/// 权限规则配置服务 +/// 职责:统一管理权限规则配置的加载、更新、热重载 +/// +public class PermissionRuleConfigService : IDisposable +{ + private readonly PermissionRuleFileService _fileService; + private readonly string _configFilePath; + + public PermissionRuleConfigService(string configFilePath) + { + _configFilePath = configFilePath; + _fileService = new PermissionRuleFileService(configFilePath, OnConfigChanged); + } + + /// + /// 初始化配置(应用启动时调用) + /// + public void Initialize() + { + try + { + if (File.Exists(_configFilePath)) + { + var rules = _fileService.LoadFromFile(); + ResourceActionValidator.LoadRules(rules); + Console.WriteLine("权限规则配置加载成功"); + + // 启动文件监听 + _fileService.StartFileWatcher(); + } + else + { + Console.WriteLine("权限规则配置文件不存在,使用默认配置"); + } + } + catch (Exception ex) + { + Console.WriteLine($"权限规则配置初始化失败: {ex.Message}"); + } + } + + /// + /// 获取当前配置 + /// + public Dictionary> GetCurrentConfig() + { + return ResourceActionValidator.GetCurrentRules(); + } + + /// + /// 更新配置(支持后台维护) + /// + public void UpdateConfig(Dictionary> newConfig) + { + try + { + // 转换为领域模型格式 + var rules = newConfig.ToDictionary( + kv => Enum.Parse(kv.Key, true), + kv => kv.Value.ToHashSet() + ); + + // 更新校验器 + ResourceActionValidator.LoadRules(rules); + + // 保存到文件 + _fileService.SaveToFile(rules); + + Console.WriteLine("权限规则配置更新成功"); + } + catch (Exception ex) + { + throw new InvalidOperationException($"更新权限规则配置失败: {ex.Message}", ex); + } + } + + /// + /// 手动刷新配置 + /// + public void RefreshConfig() + { + try + { + var rules = _fileService.LoadFromFile(); + ResourceActionValidator.LoadRules(rules); + Console.WriteLine("权限规则配置刷新成功"); + } + catch (Exception ex) + { + throw new InvalidOperationException($"刷新权限规则配置失败: {ex.Message}", ex); + } + } + + /// + /// 配置变更回调 + /// + private void OnConfigChanged(Dictionary> rules) + { + ResourceActionValidator.LoadRules(rules); + } + + public void Dispose() + { + _fileService?.Dispose(); + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Services/PermissionRuleFileService.cs b/backend/src/UniversalAdminSystem.Infrastructure/Services/PermissionRuleFileService.cs new file mode 100644 index 0000000000000000000000000000000000000000..9c18a2995398bfa63f2c5690bb94341b14a4020f --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Services/PermissionRuleFileService.cs @@ -0,0 +1,155 @@ +using System.Text.Json; +using System.Threading.Tasks; +using UniversalAdminSystem.Domian.PermissionManagement.Exceptions; +using UniversalAdminSystem.Domian.PermissionManagement.ValueObjects; + +namespace UniversalAdminSystem.Infrastructure.Services; + +/// +/// 权限规则文件服务 +/// 职责:负责权限规则配置文件的读取、写入和文件监听 +/// +public class PermissionRuleFileService : IDisposable +{ + private readonly string _configFilePath; + private FileSystemWatcher? _fileWatcher; + private readonly Action>> _onConfigChanged; + + public PermissionRuleFileService(string configFilePath, Action>> onConfigChanged) + { + _configFilePath = configFilePath; + _onConfigChanged = onConfigChanged; + } + + /// + /// 从文件加载权限规则配置 + /// + public Dictionary> LoadFromFile() + { + try + { + if (!File.Exists(_configFilePath)) + { + throw new FileNotFoundException($"权限规则配置文件不存在: {_configFilePath}"); + } + + // await using var stream = File.OpenRead(_configFilePath); + var jsonContent = File.ReadAllText(_configFilePath); + var configDict = JsonSerializer.Deserialize>>(jsonContent) ?? throw new PermissionDomainException("权限规则配置文件格式错误"); + + // 转换为领域模型格式 + var rules = configDict.ToDictionary( + kv => Enum.Parse(kv.Key, true), + kv => kv.Value.ToHashSet() + ); + + return rules; + } + catch (Exception ex) when (ex is not PermissionDomainException) + { + throw new PermissionDomainException($"加载权限规则配置文件失败: {ex.Message}", ex); + } + } + + /// + /// 将权限规则配置写入文件 + /// + public void SaveToFile(Dictionary> rules) + { + try + { + // 转换为JSON格式 + var configDict = rules.ToDictionary( + kv => kv.Key.ToString(), + kv => kv.Value.ToList() + ); + + var jsonContent = JsonSerializer.Serialize(configDict, new JsonSerializerOptions + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + // 确保目录存在 + var directory = Path.GetDirectoryName(_configFilePath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + File.WriteAllText(_configFilePath, jsonContent); + } + catch (Exception ex) + { + throw new PermissionDomainException($"保存权限规则配置文件失败: {ex.Message}", ex); + } + } + + /// + /// 启动文件监听(热重载) + /// + public void StartFileWatcher() + { + try + { + _fileWatcher?.Dispose(); + + var directory = Path.GetDirectoryName(_configFilePath); + var fileName = Path.GetFileName(_configFilePath); + + if (string.IsNullOrEmpty(directory) || string.IsNullOrEmpty(fileName)) + { + throw new ArgumentException("配置文件路径无效"); + } + + _fileWatcher = new FileSystemWatcher(directory, fileName) + { + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime, + EnableRaisingEvents = true + }; + + _fileWatcher.Changed += OnConfigFileChanged; + _fileWatcher.Created += OnConfigFileChanged; + } + catch (Exception ex) + { + throw new PermissionDomainException($"启动文件监听失败: {ex.Message}", ex); + } + } + + /// + /// 文件变更事件处理 + /// + private void OnConfigFileChanged(object sender, FileSystemEventArgs e) + { + try + { + // 延迟加载,避免文件写入过程中读取 + Thread.Sleep(100); + + var rules = LoadFromFile(); + _onConfigChanged?.Invoke(rules); + + Console.WriteLine($"权限规则配置文件已重新加载: {e.FullPath}"); + } + catch (Exception ex) + { + // 记录错误但不抛出异常,避免影响系统运行 + Console.WriteLine($"权限规则配置文件变更加载失败: {ex.Message}"); + } + } + + /// + /// 停止文件监听 + /// + public void StopFileWatcher() + { + _fileWatcher?.Dispose(); + _fileWatcher = null; + } + + public void Dispose() + { + StopFileWatcher(); + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Services/SpaCyService.cs b/backend/src/UniversalAdminSystem.Infrastructure/Services/SpaCyService.cs new file mode 100644 index 0000000000000000000000000000000000000000..b6a5e567c8e53a7a418710bb5b28d84c2f38787b --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Services/SpaCyService.cs @@ -0,0 +1,126 @@ +using System.Text.Json; + +namespace UniversalAdminSystem.Infrastructure.Services; + +public class SpaCyService +{ + private readonly HttpClient _httpClient; + private readonly string _baseUrl = "http://flaskapi:5000"; + + // 构造函数,注入 IHttpClientFactory + public SpaCyService(IHttpClientFactory httpClientFactory) + { + _httpClient = httpClientFactory.CreateClient("SpaCyClient"); + _httpClient.BaseAddress = new Uri(_baseUrl); + } + + // 发送请求到 Flask API 获取 文本分析 结果 + public async Task AnalyzeTextAsync(string text) + { + HttpRequestMessage request; + HttpResponseMessage response; + try + { + // 构造请求内容 + request = new HttpRequestMessage(HttpMethod.Post, $"{_baseUrl}/spacy") + { + Content = new StringContent(JsonSerializer.Serialize(new { text }), System.Text.Encoding.UTF8, "application/json") + }; + + // 发送请求 + response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None); + response.EnsureSuccessStatusCode(); + + // 读取响应内容 + var result = await response.Content.ReadAsStringAsync(); + Console.WriteLine("Response from Flask: " + result); // 打印响应数据,方便调试 + + // 解析响应 JSON + var preprocessResult = JsonSerializer.Deserialize(result); + Console.WriteLine("preprocessResult: " + preprocessResult); + + if (preprocessResult != null && preprocessResult.Entities.Count > 0) + { + // 打印解析后的对象(调试) + Console.WriteLine("Entities:"); + foreach (var entity in preprocessResult.Entities) + { + Console.WriteLine($"Text: {entity.Text}, Label: {entity.Label}"); + } + + Console.WriteLine("\nSentences:"); + foreach (var sentence in preprocessResult.Sentences) + { + Console.WriteLine(sentence); + } + } + + return preprocessResult ?? throw new Exception("文本预处理错误"); + } + catch (Exception ex) + { + // 处理错误,例如网络问题或 API 服务不可用 + Console.WriteLine($"Error: {ex.Message}"); + throw; + } + } + + // 发送请求到 Flask API 获取 用户输入解析 结果 + public async Task ParseUserInputAsync(string userInput) + { + HttpRequestMessage request; + HttpResponseMessage response; + try + { + // 构造请求内容 + request = new HttpRequestMessage(HttpMethod.Post, $"{_baseUrl}/parse_input") + { + Content = new StringContent(JsonSerializer.Serialize(new { content = userInput }), System.Text.Encoding.UTF8, "application/json") + }; + + // 发送请求 + response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None); + response.EnsureSuccessStatusCode(); + + // 读取响应内容 + var result = await response.Content.ReadAsStringAsync(); + Console.WriteLine("Response from Flask: " + result); // 打印响应数据,方便调试 + + // 解析响应结果 + var userInputParseResult = JsonSerializer.Deserialize(result, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + + return userInputParseResult ?? throw new Exception("文本预处理错误"); + } + catch (Exception ex) + { + // 处理错误,例如网络问题或 API 服务不可用 + Console.WriteLine($"Error: {ex.Message}"); + throw; + } + } +} + +// 预处理结果类 +public class PreprocessResult +{ + public List Entities { get; set; } = []; + public List Sentences { get; set; } = []; +} + +// 实体类 +public class Entity +{ + public string Text { get; set; } = string.Empty; + public string Label { get; set; } = string.Empty; +} + +// 用户输入解析结果类 +public class UserInputParseResult +{ + public string Text { get; set; } = string.Empty; + public List Entities { get; set; } = []; + public List Tokens { get; set; } = []; +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Services/SystemInitializationService.cs b/backend/src/UniversalAdminSystem.Infrastructure/Services/SystemInitializationService.cs new file mode 100644 index 0000000000000000000000000000000000000000..0ad45070705fe6f0bdd61b3dfde7466ff9f11105 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Services/SystemInitializationService.cs @@ -0,0 +1,535 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using UniversalAdminSystem.Domian.PermissionManagement.Aggregate; +using UniversalAdminSystem.Domian.PermissionManagement.IRepository; +using UniversalAdminSystem.Domian.PermissionManagement.ValueObjects; +using UniversalAdminSystem.Domian.PermissionManagement.Services; +using UniversalAdminSystem.Domian.UserManagement.Aggregates; +using UniversalAdminSystem.Domian.UserManagement.IRepository; +using UniversalAdminSystem.Domian.UserManagement.ValueObj; +using UniversalAdminSystem.Application.Common.Interfaces; +using UniversalAdminSystem.Domian.UserManagement.Entities; +using UniversalAdminSystem.Application.UserManagement.Interface; + +namespace UniversalAdminSystem.Infrastructure.Services; + +public class SystemInitializationService : IHostedService +{ + private readonly SystemPermissionConfigLoader _loader; + private readonly IServiceScopeFactory _scopeFactory; + private static bool _initialized = false; + + public SystemInitializationService( + SystemPermissionConfigLoader loader, + IServiceScopeFactory scopeFactory + ) + { + _loader = loader; + _scopeFactory = scopeFactory; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + if (_initialized) + { + return; + } + + cancellationToken.ThrowIfCancellationRequested(); + + try + { + // 首先加载权限规则配置 + var rulesConfigPath = Path.Combine(AppContext.BaseDirectory, "PermissionRules.json"); + if (File.Exists(rulesConfigPath)) + { + var rulesContent = await File.ReadAllTextAsync(rulesConfigPath, cancellationToken); + var rulesDict = System.Text.Json.JsonSerializer.Deserialize>>(rulesContent); + + if (rulesDict != null) + { + var rules = rulesDict.ToDictionary( + kv => Enum.Parse(kv.Key, true), + kv => kv.Value.ToHashSet() + ); + + ResourceActionValidator.LoadRules(rules); + Console.WriteLine("权限规则配置加载成功"); + } + } + else + { + Console.WriteLine("权限规则配置文件不存在,使用默认配置"); + // 设置默认规则 + var defaultRules = new Dictionary> + { + { PermissionAction.Read, new HashSet { "data", "file", "user", "role", "permission", "config", "system" } }, + { PermissionAction.Create, new HashSet { "data", "user", "file", "role", "permission" } }, + { PermissionAction.Update, new HashSet { "data", "user", "config", "role", "permission" } }, + { PermissionAction.Delete, new HashSet { "data", "user", "file", "role", "permission" } }, + { PermissionAction.Manage, new HashSet { "system", "user" } } + }; + ResourceActionValidator.LoadRules(defaultRules); + } + + // 初始化到数据库 + using (var scope = _scopeFactory.CreateScope()) + { + var permissionRepository = scope.ServiceProvider.GetRequiredService(); + var roleRepository = scope.ServiceProvider.GetRequiredService(); + var userRepository = scope.ServiceProvider.GetRequiredService(); + var userInfoRepository = scope.ServiceProvider.GetRequiredService(); + var unitOfWork = scope.ServiceProvider.GetRequiredService(); + var passwordHelper = scope.ServiceProvider.GetRequiredService(); + + // 1. 创建系统权限 + Console.WriteLine("开始创建系统权限..."); + var systemPermissions = CreateSystemPermissions(); + await permissionRepository.AddSystemPermissionsAsync(systemPermissions); + Console.WriteLine($"成功创建 {systemPermissions.Count} 个系统权限"); + + // 2. 创建角色 + Console.WriteLine("开始创建角色..."); + var roles = await CreateRolesAsync(roleRepository, permissionRepository); + Console.WriteLine($"成功创建 {roles.Count} 个角色"); + + // 3. 提交角色创建到数据库 + Console.WriteLine("提交角色创建到数据库..."); + await unitOfWork.SaveChangesAsync(); + Console.WriteLine("角色创建提交成功"); + + // 4. 为超级管理员分配所有权限(使用安全方法) + Console.WriteLine("开始为超级管理员分配权限..."); + var superAdminRole = await roleRepository.GetByNameAsync("超级管理员"); + if (superAdminRole != null) + { + await AssignAllPermissionsToRoleSafely(superAdminRole, permissionRepository); + Console.WriteLine("超级管理员权限分配完成"); + } + + // 5. 为管理员分配管理权限 + Console.WriteLine("开始为管理员分配权限..."); + var adminRole = await roleRepository.GetByNameAsync("管理员"); + if (adminRole != null) + { + await AssignAdminPermissionsToRoleSafely(adminRole, permissionRepository); + Console.WriteLine("管理员权限分配完成"); + } + + // 6. 为普通用户分配基本权限 + Console.WriteLine("开始为普通用户分配权限..."); + var normalUserRole = await roleRepository.GetByNameAsync("普通用户"); + if (normalUserRole != null) + { + await AssignBasicPermissionsToRoleSafely(normalUserRole, permissionRepository); + Console.WriteLine("普通用户权限分配完成"); + } + + // 7. 创建超级管理员用户 + await CreateSuperAdminAsync(userRepository, userInfoRepository, roleRepository, passwordHelper); + Console.WriteLine("成功创建超级管理员用户"); + + // 8. 创建管理员用户 + await CreateAdminUserAsync(userRepository, userInfoRepository, roleRepository, passwordHelper); + Console.WriteLine("成功创建管理员用户"); + + // 9. 提交所有更改到数据库 + Console.WriteLine("开始提交数据库更改..."); + await unitOfWork.BeginTransactionAsync(); + await unitOfWork.CommitAsync(); + Console.WriteLine("数据库更改提交成功"); + + _initialized = true; + Console.WriteLine("系统初始化完成"); + } + } + catch (Exception ex) + { + throw new InvalidOperationException("初始化系统失败", ex); + } + } + + private List CreateSystemPermissions() + { + var permissions = new List(); + + // 文件权限 + permissions.Add(Permission.CreateSystemPermission("文件上传", "file", (int)PermissionAction.Create)); + permissions.Add(Permission.CreateSystemPermission("文件下载", "file", (int)PermissionAction.Read)); + permissions.Add(Permission.CreateSystemPermission("文件删除", "file", (int)PermissionAction.Delete)); + permissions.Add(Permission.CreateSystemPermission("文档权限", "document", (int)PermissionAction.Private)); + permissions.Add(Permission.CreateSystemPermission("文档权限", "document", (int)PermissionAction.Public)); + permissions.Add(Permission.CreateSystemPermission("文档权限", "document", (int)PermissionAction.Restricted)); + permissions.Add(Permission.CreateSystemPermission("文档权限", "document", (int)PermissionAction.Read)); + + // 用户权限 + permissions.Add(Permission.CreateSystemPermission("创建用户", "user", (int)PermissionAction.Create)); + permissions.Add(Permission.CreateSystemPermission("查看用户", "user", (int)PermissionAction.Read)); + permissions.Add(Permission.CreateSystemPermission("更新用户", "user", (int)PermissionAction.Update)); + permissions.Add(Permission.CreateSystemPermission("删除用户", "user", (int)PermissionAction.Delete)); + + // 角色权限 + permissions.Add(Permission.CreateSystemPermission("创建角色", "role", (int)PermissionAction.Create)); + permissions.Add(Permission.CreateSystemPermission("查看角色", "role", (int)PermissionAction.Read)); + permissions.Add(Permission.CreateSystemPermission("更新角色", "role", (int)PermissionAction.Update)); + permissions.Add(Permission.CreateSystemPermission("删除角色", "role", (int)PermissionAction.Delete)); + + // 权限管理 + permissions.Add(Permission.CreateSystemPermission("创建权限", "permission", (int)PermissionAction.Create)); + permissions.Add(Permission.CreateSystemPermission("查看权限", "permission", (int)PermissionAction.Read)); + permissions.Add(Permission.CreateSystemPermission("更新权限", "permission", (int)PermissionAction.Update)); + permissions.Add(Permission.CreateSystemPermission("删除权限", "permission", (int)PermissionAction.Delete)); + + // 系统管理 + permissions.Add(Permission.CreateSystemPermission("系统管理", "system", (int)PermissionAction.Manage)); + + return permissions; + } + + private async Task> CreateRolesAsync(IRoleRepository roleRepository, IPermissionRepository permissionRepository) + { + var roles = new List(); + + // 检查是否已存在超级管理员角色 + var existingSuperAdmin = await roleRepository.GetByNameAsync("超级管理员"); + if (existingSuperAdmin == null) + { + // 1. 超级管理员角色 + var superAdminRole = Role.Create("超级管理员", "系统最高权限管理员", true, true); + Console.WriteLine($"创建超级管理员角色: {superAdminRole.Name.Value} (ID: {superAdminRole.RoleId.Value})"); + await roleRepository.AddAsync(superAdminRole); + roles.Add(superAdminRole); + } + else + { + Console.WriteLine($"超级管理员角色已存在: {existingSuperAdmin.Name.Value}"); + roles.Add(existingSuperAdmin); + } + + // 检查是否已存在管理员角色 + var existingAdmin = await roleRepository.GetByNameAsync("管理员"); + if (existingAdmin == null) + { + // 2. 管理员角色 + var adminRole = Role.Create("管理员", "系统管理员", true, false); + Console.WriteLine($"创建管理员角色: {adminRole.Name.Value} (ID: {adminRole.RoleId.Value})"); + await roleRepository.AddAsync(adminRole); + roles.Add(adminRole); + } + else + { + Console.WriteLine($"管理员角色已存在: {existingAdmin.Name.Value}"); + roles.Add(existingAdmin); + } + + // 检查是否已存在普通用户角色 + var existingNormalUser = await roleRepository.GetByNameAsync("普通用户"); + if (existingNormalUser == null) + { + // 3. 普通用户角色 + var normalUserRole = Role.Create("普通用户", "普通用户", true, false); + Console.WriteLine($"创建普通用户角色: {normalUserRole.Name.Value} (ID: {normalUserRole.RoleId.Value})"); + await roleRepository.AddAsync(normalUserRole); + roles.Add(normalUserRole); + } + else + { + Console.WriteLine($"普通用户角色已存在: {existingNormalUser.Name.Value}"); + roles.Add(existingNormalUser); + } + + Console.WriteLine("角色创建完成,暂时跳过权限分配"); + + return roles; + } + + private async Task AssignAllPermissionsToRoleSafely(Role role, IPermissionRepository permissionRepository) + { + Console.WriteLine($"开始为角色 {role.Name.Value} 分配所有权限..."); + + // 获取所有权限ID(避免实体跟踪冲突) + var allPermissions = await permissionRepository.GetAllAsync(); + var permissionIds = allPermissions.Select(p => p.PermissionId).ToList(); + + Console.WriteLine($"找到 {permissionIds.Count} 个权限"); + + // 为角色分配所有权限(使用ID而不是实体) + foreach (var permissionId in permissionIds) + { + if (!role.HasPermission(permissionId)) + { + // 通过ID查找权限实体,避免重复跟踪 + var permission = await permissionRepository.GetByGuidAsync(permissionId); + if (permission != null) + { + role.AddPermission(permission); + } + } + } + + Console.WriteLine($"成功为角色 {role.Name.Value} 分配了 {permissionIds.Count} 个权限"); + } + + private async Task AssignAdminPermissionsToRoleSafely(Role role, IPermissionRepository permissionRepository) + { + // 获取管理员需要的权限 + var adminPermissionCodes = new[] + { + "user:Create", "user:Read", "user:Update", "user:Delete", + "role:Create", "role:Read", "role:Update", "role:Delete", + "permission:Create", "permission:Read", "permission:Update", "permission:Delete", + "system:Manage", + "file:Read","file:Create","file:Delete", + "document:Restricted","document:Read" + }; + + var adminPermissionIds = new List(); + foreach (var code in adminPermissionCodes) + { + var permission = await permissionRepository.GetByCodeAsync(PermissionCode.Create(code)); + if (permission != null) + { + adminPermissionIds.Add(permission.PermissionId); + } + } + + // 为角色分配权限(使用ID而不是实体) + foreach (var permissionId in adminPermissionIds) + { + if (!role.HasPermission(permissionId)) + { + // 通过ID查找权限实体,避免重复跟踪 + var permission = await permissionRepository.GetByGuidAsync(permissionId); + if (permission != null) + { + role.AddPermission(permission); + } + } + } + Console.WriteLine($"为角色 {role.Name.Value} 分配了 {adminPermissionIds.Count} 个管理权限"); + } + + private async Task AssignBasicPermissionsToRoleSafely(Role role, IPermissionRepository permissionRepository) + { + // 获取普通用户需要的基本权限 + var basicPermissionCodes = new[] + { + "user:Read", "role:Read", "permission:Read", + "file:Create", "file:Read", + "document:Read","document:Restricted" + }; + + var basicPermissionIds = new List(); + foreach (var code in basicPermissionCodes) + { + var permission = await permissionRepository.GetByCodeAsync(PermissionCode.Create(code)); + if (permission != null) + { + basicPermissionIds.Add(permission.PermissionId); + } + } + + // 为角色分配权限(使用ID而不是实体) + foreach (var permissionId in basicPermissionIds) + { + if (!role.HasPermission(permissionId)) + { + // 通过ID查找权限实体,避免重复跟踪 + var permission = await permissionRepository.GetByGuidAsync(permissionId); + if (permission != null) + { + role.AddPermission(permission); + } + } + } + Console.WriteLine($"为角色 {role.Name.Value} 分配了 {basicPermissionIds.Count} 个基本权限"); + } + + private async Task CreateSuperAdminAsync( + IUserRepository userRepository, + IUserInfoRepository userInfoRepository, + IRoleRepository roleRepository, + IPasswordHelper passwordHelper) + { + // 检查是否已存在超级管理员 + var existingSuperAdmin = await userRepository.GetUserByAccountAsync(UserAccount.Create("admin123")); + if (existingSuperAdmin != null) + { + Console.WriteLine("超级管理员已存在,跳过创建"); + return; + } + + Console.WriteLine("开始创建超级管理员..."); + + // 创建用户信息 + var userInfo = UserInfo.CreateUserInfo( + "超级管理员", + UserGender.Man, + null, + "系统超级管理员" + ); + await userInfoRepository.AddAsync(userInfo); + Console.WriteLine("用户信息创建成功"); + + // 获取超级管理员角色 + Console.WriteLine("开始查找超级管理员角色..."); + var superAdminRole = await roleRepository.GetByNameAsync("超级管理员"); + if (superAdminRole == null) + { + // 尝试获取所有角色来调试 + var allRoles = await roleRepository.GetAllAsync(); + Console.WriteLine($"数据库中所有角色数量: {allRoles.Count()}"); + foreach (var role in allRoles) + { + Console.WriteLine($"角色: {role.Name.Value} (ID: {role.RoleId.Value})"); + } + + // 尝试直接通过ID查找 + Console.WriteLine("尝试通过ID查找角色..."); + var rolesById = await roleRepository.GetAllRolesWithPermissionsAsync(); + foreach (var role in rolesById) + { + Console.WriteLine($"通过ID找到角色: {role.Name.Value} (ID: {role.RoleId.Value})"); + } + + throw new InvalidOperationException("超级管理员角色不存在"); + } + Console.WriteLine($"获取超级管理员角色成功: {superAdminRole.Name.Value} (ID: {superAdminRole.RoleId.Value})"); + + // 加密密码 + var (hashedPassword, salt) = passwordHelper.HashPasswordWithSeparateSalt("admin123"); + + // 创建超级管理员用户 + var superAdmin = User.CreateUser( + userInfo.UserInfoId, + "admin123", // 修改为符合格式要求的账号 + hashedPassword, // 加密后的密码 + "admin@system.com", + salt, // 加密盐值 + UserStatus.Normal, + superAdminRole.RoleId.Value + ); + + await userRepository.AddAsync(superAdmin); + Console.WriteLine("超级管理员用户创建成功"); + + Console.WriteLine("超级管理员创建成功"); + Console.WriteLine("账号: admin123"); + Console.WriteLine("密码: admin123"); + } + + private async Task CreateAdminUserAsync( + IUserRepository userRepository, + IUserInfoRepository userInfoRepository, + IRoleRepository roleRepository, + IPasswordHelper passwordHelper) + { + // 检查是否已存在管理员用户 + var existingAdmin = await userRepository.GetUserByAccountAsync(UserAccount.Create("manager")); + if (existingAdmin != null) + { + Console.WriteLine("管理员用户已存在,跳过创建"); + return; + } + + Console.WriteLine("开始创建管理员用户..."); + + // 创建用户信息 + var userInfo = UserInfo.CreateUserInfo( + "系统管理员", + UserGender.Man, + null, + "系统管理员,负责日常系统管理" + ); + await userInfoRepository.AddAsync(userInfo); + Console.WriteLine("管理员用户信息创建成功"); + + // 获取管理员角色 + Console.WriteLine("开始查找管理员角色..."); + var adminRole = await roleRepository.GetByNameAsync("管理员"); + if (adminRole == null) + { + // 尝试获取所有角色来调试 + var allRoles = await roleRepository.GetAllAsync(); + Console.WriteLine($"数据库中所有角色数量: {allRoles.Count()}"); + foreach (var role in allRoles) + { + Console.WriteLine($"角色: {role.Name.Value} (ID: {role.RoleId.Value})"); + } + + throw new InvalidOperationException("管理员角色不存在"); + } + Console.WriteLine($"获取管理员角色成功: {adminRole.Name.Value} (ID: {adminRole.RoleId.Value})"); + + // 加密密码 + var (hashedPassword, salt) = passwordHelper.HashPasswordWithSeparateSalt("manager123"); + + // 创建管理员用户 + var admin = User.CreateUser( + userInfo.UserInfoId, + "manager", // 管理员账号 + hashedPassword, // 加密后的密码 + "manager@system.com", + salt, // 加密盐值 + UserStatus.Normal, + adminRole.RoleId.Value + ); + + await userRepository.AddAsync(admin); + Console.WriteLine("管理员用户创建成功"); + + Console.WriteLine("管理员用户创建成功"); + Console.WriteLine("账号: manager"); + Console.WriteLine("密码: manager123"); + Console.WriteLine("角色: 管理员"); + } + + /// + /// 清理重复的权限数据 + /// + private async Task CleanupDuplicatePermissionsAsync(IPermissionRepository permissionRepository) + { + try + { + Console.WriteLine("开始清理重复的权限数据..."); + + // 获取所有权限 + var allPermissions = await permissionRepository.GetAllAsync(); + var permissionGroups = allPermissions.GroupBy(p => p.Code.Value).ToList(); + + var duplicates = permissionGroups.Where(g => g.Count() > 1).ToList(); + + if (duplicates.Any()) + { + Console.WriteLine($"发现 {duplicates.Count} 个重复的权限代码"); + + foreach (var group in duplicates) + { + Console.WriteLine($"权限代码 '{group.Key}' 有 {group.Count()} 个重复项"); + + // 保留第一个,删除其他的 + var toKeep = group.First(); + var toDelete = group.Skip(1).ToList(); + + Console.WriteLine($"保留权限ID: {toKeep.PermissionId}, 删除 {toDelete.Count} 个重复项"); + + // 这里需要实现删除方法,暂时跳过 + // foreach (var perm in toDelete) + // { + // await permissionRepository.DeleteAsync(perm.PermissionId); + // } + } + } + else + { + Console.WriteLine("没有发现重复的权限数据"); + } + } + catch (Exception ex) + { + Console.WriteLine($"清理重复权限时出错: {ex.Message}"); + } + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/Services/SystemPermissionConfigLoader.cs b/backend/src/UniversalAdminSystem.Infrastructure/Services/SystemPermissionConfigLoader.cs new file mode 100644 index 0000000000000000000000000000000000000000..b43ab2d92cef571d73a808ed40e90e64c12601bf --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/Services/SystemPermissionConfigLoader.cs @@ -0,0 +1,83 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using Microsoft.Extensions.Logging; +using UniversalAdminSystem.Infrastructure.Configs; + +namespace UniversalAdminSystem.Infrastructure.Services; + +public sealed class SystemPermissionConfigLoader(ILogger logger) +{ + private readonly ILogger _logger = logger; + + /// + /// json反序列化规则 + /// + private static readonly JsonSerializerOptions SerializerOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // 属性名使用驼峰命名 + AllowTrailingCommas = true, // 允许 JSON 文件中存在尾随逗号 + ReadCommentHandling = JsonCommentHandling.Skip, // 跳过注释 + }; + + /// + /// 异步加载配置文件 + /// + /// + /// + /// + /// + public async Task> LoadFromFileAsync(string filePath) + { + // 文件存在性检查 + if (!File.Exists(filePath)) + { + _logger.LogError("系统权限配置文件未找到", [filePath]); + throw new FileNotFoundException("系统权限配置文件未找到", filePath); + } + + // 读取并解析JSON + try + { + await using var stream = File.OpenRead(filePath); // 打开文件流 + var configRoot = await JsonSerializer.DeserializeAsync( // 开始异步JSON反序列化,完成并关闭文件流 + stream, + SerializerOptions + ); + + if (configRoot?.Permissions is null || configRoot.Permissions.Count == 0) + { + _logger.LogWarning("配置文件中未找到有效的系统权限", [filePath]); + return []; + } + + // 校验系统权限配置集合 + ValidateConfigs(configRoot.Permissions); + + return configRoot.Permissions; + } + catch (JsonException ex) + { + _logger.LogError(ex, "JSON解析错误", [filePath]); + throw new InvalidOperationException("JSON 结构与目标类型不匹配", ex); + } + } + + /// + /// 校验配置集合的有效性 + /// + /// + private void ValidateConfigs(IEnumerable configs) + { + foreach (var config in configs) + { + if (string.IsNullOrWhiteSpace(config.Resource)) + { + _logger.LogWarning("权限配置缺少资源", [config]); + } + + // 利用 System.ComponentModel.DataAnnotations 的验证 + var context = new ValidationContext(config); + Validator.ValidateObject(config, context, validateAllProperties: true); + } + } +} \ No newline at end of file diff --git a/backend/src/UniversalAdminSystem.Infrastructure/UniversalAdminSystem.Infrastructure.csproj b/backend/src/UniversalAdminSystem.Infrastructure/UniversalAdminSystem.Infrastructure.csproj new file mode 100644 index 0000000000000000000000000000000000000000..b2eab81aed733abe56c4f223e5ee4152e6002267 --- /dev/null +++ b/backend/src/UniversalAdminSystem.Infrastructure/UniversalAdminSystem.Infrastructure.csproj @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + net8.0 + enable + enable + + + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..78842154e1d366e71532e88b1b3a82b183b56911 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,100 @@ +version: '3.8' + +services: + postgresql: + image: ankane/pgvector:latest + container_name: rag_postgres + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: 031028@yue + POSTGRES_DB: rag_db + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + networks: + - ragnet + + rabbitmq: + image: rabbitmq:3.13-management + container_name: rag_rabbitmq + restart: always + ports: + - "5672:5672" + - "15672:15672" + environment: + RABBITMQ_DEFAULT_USER: admin + RABBITMQ_DEFAULT_PASS: admin + networks: + - ragnet + + flaskapi: + build: ./FlaskAPI + container_name: flaskapi + restart: always + ports: + - "5000:5000" + depends_on: + - postgres + networks: + - ragnet + + backend: + build: ./backend + container_name: backend + restart: always + ports: + - "5101:5101" + environment: + ConnectionStrings__pgSql: "Server=postgresql;Port=5432;Username=postgres;Password=031028@yue;Database=rag_vector_db" + ConnectionStrings__RabbitMq: "amqp://admin:admin@rabbitmq:5672/" + depends_on: + - flaskapi + - postgres + - rabbitmq + networks: + - ragnet + + vite-frontend: + build: ./frontend/vite-frontend + container_name: vite-frontend + restart: always + ports: + - "8081:80" + networks: + - ragnet + + uniapp-h5: + build: ./frontend/uniapp + container_name: uniapp-h5 + restart: always + ports: + - "8082:80" + networks: + - ragnet + + nginx: + image: nginx:alpine + container_name: nginx + restart: always + ports: + - "80:80" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + - ./frontend/vite-frontend/dist:/usr/share/nginx/html/vite + - ./frontend/uniapp/dist:/usr/share/nginx/html/uniapp + depends_on: + - vite-frontend + - uniapp-h5 + - backend + - flaskapi + networks: + - ragnet + +networks: + ragnet: + driver: bridge + +volumes: + pgdata: diff --git a/frontend/uniapp/App.vue b/frontend/uniapp/App.vue new file mode 100644 index 0000000000000000000000000000000000000000..f9768a596f899dd55a4fb68d43e470eabd6b6fa0 --- /dev/null +++ b/frontend/uniapp/App.vue @@ -0,0 +1,17 @@ + + + diff --git a/frontend/uniapp/api/auth.js b/frontend/uniapp/api/auth.js new file mode 100644 index 0000000000000000000000000000000000000000..42a306882f16467ae4324e8521d92e4d1b33b802 --- /dev/null +++ b/frontend/uniapp/api/auth.js @@ -0,0 +1,33 @@ +import { useRequest } from "./config/index.js"; + +export function useAuthApi() { + const { request } = useRequest(); + + const login = async (credentials) => { + const loginData = credentials; + + const response = await request({ + url: "/auth/login", + method: "POST", + data: loginData, + showLoading: true, + }); + + return response; + }; + + const register = async (createUser) => { + const user = createUser; + + const respose = await request({ + url: "/auth/register", + method: "POST", + data: user, + showLoading: true, + }); + + return respose; + }; + + return { login, register }; +} diff --git a/frontend/uniapp/api/chat.js b/frontend/uniapp/api/chat.js new file mode 100644 index 0000000000000000000000000000000000000000..453342399e9fef4dd388ff11fddb722aca1a8c29 --- /dev/null +++ b/frontend/uniapp/api/chat.js @@ -0,0 +1,62 @@ +import { useRequest } from "./config"; + +const { request } = useRequest(); + +export function useChatApi() { + const createChat = async (chatDto) => { + var res = await request({ + url: "/AIQuestions/user", + method: "POST", + data: chatDto, + showLoading: true, + }); + + return res; + }; + + const getChat = async (id) => { + var res = await request({ + url: `/AIQuestions/user/${id}`, + method: "GET", + data: null, + showLoading: true, + }); + + return res; + }; + + const getAllMessage = async (id) => { + var res = await request({ + url: `/AIQuestions/user/message/${id}`, + method: "GET", + data: null, + showLoading: true, + }); + + return res; + }; + + const deleteChat = async (id) => { + var res = await request({ + url: `/AIQuestions/user/${id}`, + method: "DELETE", + data: null, + showLoading: true, + }); + + return res; + }; + + const conversation = async (id, userMessage) => { + var res = await request({ + url: `/AIQuestions/chat/${id}`, + method: "POST", + data: JSON.stringify(userMessage), + showLoading: true, + }); + + return res; + }; + + return { conversation, createChat, deleteChat, getAllMessage, getChat }; +} diff --git a/frontend/uniapp/api/config/Config.js b/frontend/uniapp/api/config/Config.js new file mode 100644 index 0000000000000000000000000000000000000000..25223b0976c9aa676865e415edbc917cc5cce4e7 --- /dev/null +++ b/frontend/uniapp/api/config/Config.js @@ -0,0 +1,17 @@ +const env = process.env.NODE_ENV || "development"; + +const configs = { + development: { + baseUrl: "http://localhost:5101/api", // 简化结构 + }, + production: { + baseUrl: "http://backend:5101/api", + }, +}; + +export default { + ...configs[env], + // 公共配置 + appName: "多端应用", + version: "1.0.0", +}; diff --git a/frontend/uniapp/api/config/index.js b/frontend/uniapp/api/config/index.js new file mode 100644 index 0000000000000000000000000000000000000000..8a778766ef7c5a0828ceef5b247416b3a6fcb087 --- /dev/null +++ b/frontend/uniapp/api/config/index.js @@ -0,0 +1,119 @@ +import { ref } from "vue"; +import { useUserStore } from "../../stores/User"; +import config from "./config.js"; + +const getBaseUrl = () => { + return config.baseUrl; // 直接使用配置中的 baseUrl +}; + +export function useRequest() { + const loading = ref(false); + const appStore = useUserStore(); + + const request = async (config) => { + // 合并配置 + const baseUrl = getBaseUrl(); + let finalUrl = config.url.startsWith("http") + ? config.url + : `${baseUrl}${config.url.startsWith("/") ? "" : "/"}${config.url}`; + + console.log("完整请求 URL:", finalUrl); + const mergedConfig = { + url: finalUrl, + timeout: 150000, + header: { + "Content-Type": "application/json", + "X-Requested-With": "XMLHttpRequest", + "X-Client-Platform": uni.getSystemInfoSync().platform, + }, + method: config.method, + data: config.data, + showLoading: config.showLoading, + }; + + // 请求拦截 + if (appStore.token) { + mergedConfig.header.Authorization = `Bearer ${appStore.token}`; + } + + // 显示加载状态 + if (mergedConfig.showLoading) { + loading.value = true; + uni.showLoading({ + title: "加载中...", + mask: true, + }); + } + + try { + const response = await uni.request(mergedConfig); + + // 响应拦截 + if (response.statusCode === 401) { + handleUnauthorized(); + throw new Error("登录状态已过期"); + } + + if (response.statusCode >= 400) { + throw new Error(`请求失败: ${response.data.message}`); + } + + return response.data; + } catch (error) { + // 统一错误处理 + // handleRequestError(error, mergedConfig) + throw error; + } finally { + if (mergedConfig.showLoading) { + loading.value = false; + uni.hideLoading(); + } + } + }; + + return { + request, + loading, + }; +} + +// 处理未授权情况 +function handleUnauthorized() { + const appStore = useAppStore(); + appStore.clearToken(); + + // 跳转到登录页(不同平台处理) + // #ifdef H5 || APP + uni.navigateTo({ + url: "/pages/login/login", + }); + // #endif + + // #ifdef MP-WEIXIN + uni.reLaunch({ + url: "/pages/login/login", + }); + // #endif +} + +// // 错误处理 +// function handleRequestError(error, config) { +// console.error('请求错误:', error) + +// // 不显示静默请求的错误 +// if (config.silent) return + +// let message = '网络请求失败' + +// if (error.errMsg.includes('timeout')) { +// message = '请求超时,请检查网络' +// } else if (error.errMsg.includes('network')) { +// message = '网络不可用,请检查连接' +// } + +// uni.showToast({ +// title: message, +// icon: 'none', +// duration: 3000 +// }) +// } diff --git a/frontend/uniapp/deps/_metadata.json b/frontend/uniapp/deps/_metadata.json new file mode 100644 index 0000000000000000000000000000000000000000..0229e3f37d629173b8f971f47244f242510e0d34 --- /dev/null +++ b/frontend/uniapp/deps/_metadata.json @@ -0,0 +1,8 @@ +{ + "hash": "4bee26e5", + "configHash": "3efba968", + "lockfileHash": "e3b0c442", + "browserHash": "ed815da7", + "optimized": {}, + "chunks": {} +} diff --git a/frontend/uniapp/deps/package.json b/frontend/uniapp/deps/package.json new file mode 100644 index 0000000000000000000000000000000000000000..3dbc1ca591c0557e35b6004aeba250e6a70b56e3 --- /dev/null +++ b/frontend/uniapp/deps/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/frontend/uniapp/dockerfile b/frontend/uniapp/dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..55e1a7761f067f4925a9441cbd05a8709ebba187 --- /dev/null +++ b/frontend/uniapp/dockerfile @@ -0,0 +1,19 @@ +# 构建阶段 +FROM node:20 AS build +WORKDIR /app +COPY package*.json ./ +RUN npm install +COPY . . +RUN npm run build:h5 + +# # 部署阶段 +# FROM nginx:alpine +# COPY --from=build /app/dist/build/h5 /usr/share/nginx/html +# EXPOSE 80 +# CMD ["nginx", "-g", "daemon off;"] + +# 输出 dist 用于集中式 Nginx +FROM alpine:3.18 +WORKDIR /output +COPY --from=build /app/dist ./ +CMD ["sh", "-c", "cp -r /output/* /dist && tail -f /dev/null"] \ No newline at end of file diff --git a/frontend/uniapp/index.html b/frontend/uniapp/index.html new file mode 100644 index 0000000000000000000000000000000000000000..b5d330d136f2ef4198b59bfd92ec3dc44df9292e --- /dev/null +++ b/frontend/uniapp/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + +
+ + + diff --git a/frontend/uniapp/main.js b/frontend/uniapp/main.js new file mode 100644 index 0000000000000000000000000000000000000000..2ced1002822f2c229a26e17dff9502df838d58b0 --- /dev/null +++ b/frontend/uniapp/main.js @@ -0,0 +1,26 @@ +import App from "./App"; + +// #ifndef VUE3 +import Vue from "vue"; +import "./uni.promisify.adaptor"; +Vue.config.productionTip = false; +App.mpType = "app"; +const app = new Vue({ + ...App, +}); +app.$mount(); +// #endif + +// #ifdef VUE3 +import { createSSRApp } from "vue"; +import { createPinia } from "pinia"; +export function createApp() { + const app = createSSRApp(App); + const pinia = createPinia(); + + app.use(pinia); + return { + app, + }; +} +// #endif diff --git a/frontend/uniapp/manifest.json b/frontend/uniapp/manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..bd1468156bf1213b45ffbfe1445257df8d5f601d --- /dev/null +++ b/frontend/uniapp/manifest.json @@ -0,0 +1,80 @@ +{ + "name": "uniapp-ai", + "appid": "__UNI__3924A7E", + "description": "", + "versionName": "1.0.0", + "versionCode": "100", + "transformPx": false, + /* 5+App特有相关 */ + "app-plus": { + "usingComponents": true, + "nvueStyleCompiler": "uni-app", + "compilerVersion": 3, + "splashscreen": { + "alwaysShowBeforeRender": true, + "waiting": true, + "autoclose": true, + "delay": 0 + }, + /* 模块配置 */ + "modules": {}, + /* 应用发布信息 */ + "distribute": { + /* android打包配置 */ + "android": { + "permissions": [ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ], + "minSdkVersion": 21 + }, + /* ios打包配置 */ + "ios": { + "dSYMs": false + }, + /* SDK配置 */ + "sdkConfigs": {} + } + }, + /* 快应用特有相关 */ + "quickapp": {}, + /* 小程序特有相关 */ + "mp-weixin": { + "appid": "", + "setting": { + "urlCheck": false + }, + "usingComponents": true + }, + "mp-alipay": { + "usingComponents": true + }, + "mp-baidu": { + "usingComponents": true + }, + "mp-toutiao": { + "usingComponents": true + }, + "uniStatistics": { + "enable": false + }, + "vueVersion": "3", + "h5": { + "devServer": { + "port": 8081 + } + } +} diff --git a/frontend/uniapp/package.json b/frontend/uniapp/package.json new file mode 100644 index 0000000000000000000000000000000000000000..2ec6424c628bb9c259746a3d969b5a2a5fd1501c --- /dev/null +++ b/frontend/uniapp/package.json @@ -0,0 +1,10 @@ +{ + "dependencies": { + "pinia": "^3.0.3" + }, + "scripts": { + "dev": "uniapp dev", + "build": "uniapp build", + "dev:h5": "uniapp dev:h5" + } +} diff --git a/frontend/uniapp/pages.json b/frontend/uniapp/pages.json new file mode 100644 index 0000000000000000000000000000000000000000..822f629937252bfc567c2e19ac10d60b987f0623 --- /dev/null +++ b/frontend/uniapp/pages.json @@ -0,0 +1,36 @@ +{ + "pages": [ + //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages + { + "path": "pages/login/login", + "style": { + "navigationBarTitleText": "" + } + }, + { + "path": "pages/chat/chat", + "style": { + "navigationBarTitleText": "" + } + }, + { + "path": "pages/register/register", + "style": { + "navigationBarTitleText": "" + } + }, + { + "path": "pages/document/document", + "style": { + "navigationBarTitleText": "" + } + } + ], + "globalStyle": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "uni-app", + "navigationBarBackgroundColor": "#F8F8F8", + "backgroundColor": "#F8F8F8" + }, + "uniIdRouter": {} +} diff --git a/frontend/uniapp/pages/chat/chat.vue b/frontend/uniapp/pages/chat/chat.vue new file mode 100644 index 0000000000000000000000000000000000000000..e77d597fe843fb92407001f30fc506d1509e382c --- /dev/null +++ b/frontend/uniapp/pages/chat/chat.vue @@ -0,0 +1,705 @@ + + + + + diff --git a/frontend/uniapp/pages/document/document.vue b/frontend/uniapp/pages/document/document.vue new file mode 100644 index 0000000000000000000000000000000000000000..e9cc8795bfa5794605190e273fc2627ac74e074d --- /dev/null +++ b/frontend/uniapp/pages/document/document.vue @@ -0,0 +1,166 @@ + + + + + diff --git a/frontend/uniapp/pages/login/login.vue b/frontend/uniapp/pages/login/login.vue new file mode 100644 index 0000000000000000000000000000000000000000..5d32b0b8058940ca0a26aa1185e06e28156773f5 --- /dev/null +++ b/frontend/uniapp/pages/login/login.vue @@ -0,0 +1,209 @@ + + + + + diff --git a/frontend/uniapp/pages/register/register.vue b/frontend/uniapp/pages/register/register.vue new file mode 100644 index 0000000000000000000000000000000000000000..5a2dd3f6eb3cc9137837bdd435165cef148c85ef --- /dev/null +++ b/frontend/uniapp/pages/register/register.vue @@ -0,0 +1,196 @@ + + + + + diff --git a/frontend/uniapp/static/logo.png b/frontend/uniapp/static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b5771e209bb677e2ebd5ff766ad5ee11790f305a Binary files /dev/null and b/frontend/uniapp/static/logo.png differ diff --git a/frontend/uniapp/stores/User.js b/frontend/uniapp/stores/User.js new file mode 100644 index 0000000000000000000000000000000000000000..5bd9f105a7710cb26d75abd72dea52f5efc48a6a --- /dev/null +++ b/frontend/uniapp/stores/User.js @@ -0,0 +1,26 @@ +import { defineStore } from "pinia"; +import { ref } from "vue"; + +export const useUserStore = defineStore("User", () => { + const token = ref(uni.getStorageSync("token") || ""); + const userId = ref(uni.getStorageSync("userId") || ""); + + const setToken = (newToken) => { + token.value = newToken; + uni.setStorageSync("token", newToken); + }; + + const setUserId = (newUserId) => { + userId.value = newUserId; + uni.setStorageSync("userId", newUserId); + }; + + const loginOut = () => { + token.value = ""; + userId.value = ""; + uni.removeStorageSync("userId"); + uni.removeStorageSync("token"); + }; + + return { userId, token, setToken, setUserId, loginOut }; +}); diff --git a/frontend/uniapp/uni.promisify.adaptor.js b/frontend/uniapp/uni.promisify.adaptor.js new file mode 100644 index 0000000000000000000000000000000000000000..5fec4f3304b8017e7da59574f948ceea6aafe116 --- /dev/null +++ b/frontend/uniapp/uni.promisify.adaptor.js @@ -0,0 +1,13 @@ +uni.addInterceptor({ + returnValue (res) { + if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) { + return res; + } + return new Promise((resolve, reject) => { + res.then((res) => { + if (!res) return resolve(res) + return res[0] ? reject(res[0]) : resolve(res[1]) + }); + }); + }, +}); \ No newline at end of file diff --git a/frontend/uniapp/uni.scss b/frontend/uniapp/uni.scss new file mode 100644 index 0000000000000000000000000000000000000000..b9249e9d9fab23b573e4add469e22710dce91e2a --- /dev/null +++ b/frontend/uniapp/uni.scss @@ -0,0 +1,76 @@ +/** + * 这里是uni-app内置的常用样式变量 + * + * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量 + * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App + * + */ + +/** + * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能 + * + * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件 + */ + +/* 颜色变量 */ + +/* 行为相关颜色 */ +$uni-color-primary: #007aff; +$uni-color-success: #4cd964; +$uni-color-warning: #f0ad4e; +$uni-color-error: #dd524d; + +/* 文字基本颜色 */ +$uni-text-color:#333;//基本色 +$uni-text-color-inverse:#fff;//反色 +$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息 +$uni-text-color-placeholder: #808080; +$uni-text-color-disable:#c0c0c0; + +/* 背景颜色 */ +$uni-bg-color:#ffffff; +$uni-bg-color-grey:#f8f8f8; +$uni-bg-color-hover:#f1f1f1;//点击状态颜色 +$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色 + +/* 边框颜色 */ +$uni-border-color:#c8c7cc; + +/* 尺寸变量 */ + +/* 文字尺寸 */ +$uni-font-size-sm:12px; +$uni-font-size-base:14px; +$uni-font-size-lg:16px; + +/* 图片尺寸 */ +$uni-img-size-sm:20px; +$uni-img-size-base:26px; +$uni-img-size-lg:40px; + +/* Border Radius */ +$uni-border-radius-sm: 2px; +$uni-border-radius-base: 3px; +$uni-border-radius-lg: 6px; +$uni-border-radius-circle: 50%; + +/* 水平间距 */ +$uni-spacing-row-sm: 5px; +$uni-spacing-row-base: 10px; +$uni-spacing-row-lg: 15px; + +/* 垂直间距 */ +$uni-spacing-col-sm: 4px; +$uni-spacing-col-base: 8px; +$uni-spacing-col-lg: 12px; + +/* 透明度 */ +$uni-opacity-disabled: 0.3; // 组件禁用态的透明度 + +/* 文章场景相关 */ +$uni-color-title: #2C405A; // 文章标题颜色 +$uni-font-size-title:20px; +$uni-color-subtitle: #555555; // 二级标题颜色 +$uni-font-size-subtitle:26px; +$uni-color-paragraph: #3F536E; // 文章段落颜色 +$uni-font-size-paragraph:15px; diff --git a/frontend/uniapp/vue.config.js b/frontend/uniapp/vue.config.js new file mode 100644 index 0000000000000000000000000000000000000000..52bb92943c838b9991db7fc27ee9cb200a817046 --- /dev/null +++ b/frontend/uniapp/vue.config.js @@ -0,0 +1,20 @@ +module.exports = { + devServer: { + proxy: { + "/api": { + // 代理所有以/api开头的请求 + target: "http://localhost:5101", // 后端服务器地址 + changeOrigin: true, + pathRewrite: { + "^/api": "/api", // 将路径中的/api保留 + }, + }, + }, + }, +}; + +export default { + server: { + port: 8081, // 修改为你需要的端口号 + }, +}; diff --git a/frontend/vite-frontend/.gitignore b/frontend/vite-frontend/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..251ce6d2bd9308e5975611a1ad18308ba6da8117 --- /dev/null +++ b/frontend/vite-frontend/.gitignore @@ -0,0 +1,23 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/vite-frontend/README.md b/frontend/vite-frontend/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d71bb76a96e15c5afcea21e275afc3c091eac228 --- /dev/null +++ b/frontend/vite-frontend/README.md @@ -0,0 +1,228 @@ +# 通用后台管理系统前端 + +一个基于 Vue 3 + Element Plus 的现代化企业级后台管理系统前端项目。 + +## 🚀 技术栈 + +- **框架**: Vue 3.5.17 +- **构建工具**: Vite 7.0.4 +- **UI 组件库**: Element Plus 2.10.4 +- **路由管理**: Vue Router 4 +- **状态管理**: Pinia 3.0.3 +- **HTTP 客户端**: Axios 1.11.0 +- **图标库**: @element-plus/icons-vue 2.3.1 +- **样式预处理**: Sass 1.89.2 +- **代码规范**: ESLint 9.31.0 + Prettier 3.6.2 + +## ✨ 功能特性 + +### 🎨 现代化UI设计 +- 响应式布局,支持多种屏幕尺寸 +- 现代化渐变背景和动画效果 +- 毛玻璃效果和阴影设计 +- 流畅的过渡动画 + +### 🔐 用户管理 +- 用户列表展示和管理 +- 用户角色权限控制 +- 用户状态管理(正常/锁定) +- 搜索和筛选功能 +- 分页显示 + +### 🛡️ 权限管理 +- 角色管理 +- 菜单权限控制 +- 功能权限分配 +- 权限验证机制 + +### ⚙️ 系统管理 +- 系统基本设置 +- 安全配置 +- 日志管理 +- 系统监控 + +### 📝 内容管理 +- 文章管理 +- 文件管理 +- 内容编辑和发布 + +## 📁 项目结构 + +``` +vite-frontend/ +├── src/ +│ ├── assets/ # 静态资源 +│ ├── router/ # 路由配置 +│ ├── stores/ # 状态管理 +│ ├── view/ # 页面组件 +│ │ ├── Dashboard.vue # 主仪表板 +│ │ ├── Login.vue # 登录页面 +│ │ ├── UserList.vue # 用户列表 +│ │ ├── UserManagement.vue # 用户管理 +│ │ ├── RoleManagement.vue # 角色管理 +│ │ ├── MenuManagement.vue # 菜单管理 +│ │ ├── PermissionControl.vue # 权限控制 +│ │ ├── SystemSettings.vue # 系统设置 +│ │ ├── LogManagement.vue # 日志管理 +│ │ ├── ArticleManagement.vue # 文章管理 +│ │ ├── FileManagement.vue # 文件管理 +│ │ └── Register.vue # 注册页面 +│ ├── App.vue # 根组件 +│ ├── main.js # 入口文件 +│ └── style.css # 全局样式 +├── public/ # 公共资源 +├── package.json # 项目配置 +├── vite.config.js # Vite配置 +└── README.md # 项目说明 +``` + +## 🚀 快速开始 + +### 环境要求 +- Node.js >= 16.0.0 +- npm >= 8.0.0 或 pnpm >= 7.0.0 + +### 安装依赖 +```bash +# 使用 npm +npm install + +# 使用 pnpm (推荐) +pnpm install +``` + +### 启动开发服务器 +```bash +# 使用 npm +npm run dev + +# 使用 pnpm +pnpm dev +``` + +### 构建生产版本 +```bash +# 使用 npm +npm run build + +# 使用 pnpm +pnpm build +``` + +### 预览生产版本 +```bash +# 使用 npm +npm run preview + +# 使用 pnpm +pnpm preview +``` + +## 🎯 功能模块 + +### 1. 登录系统 +- 用户名密码登录 +- 记住密码功能 +- 社交登录支持(微信、钉钉) +- 忘记密码功能 + +### 2. 仪表板 +- 侧边栏导航菜单 +- 顶部导航栏 +- 面包屑导航 +- 用户信息下拉菜单 +- 搜索功能 +- 通知系统 + +### 3. 用户管理 +- 用户列表展示 +- 用户信息编辑 +- 用户状态管理 +- 角色分配 +- 搜索和筛选 +- 分页显示 + +### 4. 权限管理 +- 角色创建和编辑 +- 权限分配 +- 菜单权限控制 +- 功能权限管理 + +### 5. 系统设置 +- 基本系统配置 +- 安全设置 +- 语言和时区设置 +- 系统参数配置 + +## 🎨 设计特色 + +### 响应式设计 +- 支持桌面端、平板和移动端 +- 自适应布局 +- 触摸友好的交互 + +### 现代化UI +- 渐变背景 +- 毛玻璃效果 +- 流畅动画 +- 阴影和圆角设计 + +### 用户体验 +- 直观的导航 +- 清晰的信息层级 +- 友好的交互反馈 +- 加载状态提示 + +## 🔧 开发指南 + +### 代码规范 +项目使用 ESLint + Prettier 进行代码规范检查: + +```bash +# 代码检查 +npm run lint + +# 代码格式化 +npm run format +``` + +### 组件开发 +- 使用 Vue 3 Composition API +- 遵循 Element Plus 设计规范 +- 保持组件的可复用性 + +### 样式开发 +- 使用 SCSS 预处理器 +- 采用 BEM 命名规范 +- 响应式设计优先 + +## 📱 浏览器支持 + +- Chrome >= 88 +- Firefox >= 85 +- Safari >= 14 +- Edge >= 88 + +## 🤝 贡献指南 + +1. Fork 本仓库 +2. 创建特性分支 (`git checkout -b feature/AmazingFeature`) +3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) +4. 推送到分支 (`git push origin feature/AmazingFeature`) +5. 打开 Pull Request + +## 📄 许可证 + +本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。 + +## 📞 联系方式 + +如有问题或建议,请通过以下方式联系: + +- 提交 Issue +- 发送邮件 +- 项目讨论区 + +--- + +**注意**: 这是一个演示项目,部分功能为模拟实现,实际使用时需要配合后端API进行开发。 diff --git a/frontend/vite-frontend/dockerfile b/frontend/vite-frontend/dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..aacd5e19625f59b2b34d5167b6656229cceb7b10 --- /dev/null +++ b/frontend/vite-frontend/dockerfile @@ -0,0 +1,23 @@ +# 构建阶段 +FROM node:20 AS build +WORKDIR /app +COPY package*.json ./ +COPY pnpm*.yaml ./ +COPY vite.config.js ./ +RUN npm install +RUN pnpm install +COPY . . +RUN npm run build + +# # 部署阶段(Nginx) +# FROM nginx:alpine +# RUN rm -rf /usr/share/nginx/html/* +# COPY --from=build /app/dist /usr/share/nginx/html +# EXPOSE 80 +# CMD ["nginx", "-g", "daemon off;"] + +# 输出 dist 用于集中式 Nginx +FROM alpine:3.18 +WORKDIR /output +COPY --from=build /app/dist ./ +CMD ["sh", "-c", "cp -r /output/* /dist && tail -f /dev/null"] diff --git a/frontend/vite-frontend/env.d.ts b/frontend/vite-frontend/env.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..59bc7f53125653854a944f01188936ae813a72bf --- /dev/null +++ b/frontend/vite-frontend/env.d.ts @@ -0,0 +1,7 @@ +interface ImportMetaEnv { + readonly VITE_BASE_URL: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/frontend/vite-frontend/index.html b/frontend/vite-frontend/index.html new file mode 100644 index 0000000000000000000000000000000000000000..8388c4bb136ea674b07747cc20b113af58222ffe --- /dev/null +++ b/frontend/vite-frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Vue + + +
+ + + diff --git a/frontend/vite-frontend/package.json b/frontend/vite-frontend/package.json new file mode 100644 index 0000000000000000000000000000000000000000..fe5b803fa2d2a2f9a761c9811c4d43816e37ebbb --- /dev/null +++ b/frontend/vite-frontend/package.json @@ -0,0 +1,28 @@ +{ + "name": "vite-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "axios": "^1.11.0", + "element-plus": "^2.10.4", + "pinia": "^3.0.3", + "vue": "^3.5.17", + "vue-router": "4" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.0", + "eslint": "^9.31.0", + "husky": "^9.1.7", + "lint-staged": "^16.1.2", + "prettier": "^3.6.2", + "sass": "^1.89.2", + "vite": "^7.0.4" + } +} diff --git a/frontend/vite-frontend/pnpm-lock.yaml b/frontend/vite-frontend/pnpm-lock.yaml new file mode 100644 index 0000000000000000000000000000000000000000..29e54ce49a16dec0e243bb53dd5ff1692983f4e7 --- /dev/null +++ b/frontend/vite-frontend/pnpm-lock.yaml @@ -0,0 +1,2423 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@element-plus/icons-vue': + specifier: ^2.3.1 + version: 2.3.1(vue@3.5.18) + axios: + specifier: ^1.11.0 + version: 1.11.0 + element-plus: + specifier: ^2.10.4 + version: 2.10.4(vue@3.5.18) + pinia: + specifier: ^3.0.3 + version: 3.0.3(vue@3.5.18) + vue: + specifier: ^3.5.17 + version: 3.5.18 + vue-router: + specifier: '4' + version: 4.5.1(vue@3.5.18) + devDependencies: + '@vitejs/plugin-vue': + specifier: ^6.0.0 + version: 6.0.0(vite@7.0.5(sass@1.89.2)(yaml@2.8.0))(vue@3.5.18) + eslint: + specifier: ^9.31.0 + version: 9.31.0 + husky: + specifier: ^9.1.7 + version: 9.1.7 + lint-staged: + specifier: ^16.1.2 + version: 16.1.2 + prettier: + specifier: ^3.6.2 + version: 3.6.2 + sass: + specifier: ^1.89.2 + version: 1.89.2 + vite: + specifier: ^7.0.4 + version: 7.0.5(sass@1.89.2)(yaml@2.8.0) + +packages: + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.0': + resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.28.1': + resolution: {integrity: sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==} + engines: {node: '>=6.9.0'} + + '@ctrl/tinycolor@3.6.1': + resolution: {integrity: sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==} + engines: {node: '>=10'} + + '@element-plus/icons-vue@2.3.1': + resolution: {integrity: sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==} + peerDependencies: + vue: ^3.2.0 + + '@esbuild/aix-ppc64@0.25.8': + resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.8': + resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.8': + resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.8': + resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.8': + resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.8': + resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.8': + resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.8': + resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.8': + resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.8': + resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.8': + resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.8': + resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.8': + resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.8': + resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.8': + resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.8': + resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.8': + resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.8': + resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.8': + resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.8': + resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.8': + resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.8': + resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.8': + resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.8': + resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.8': + resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.8': + resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.7.0': + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.3.0': + resolution: {integrity: sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.1': + resolution: {integrity: sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.31.0': + resolution: {integrity: sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.3.4': + resolution: {integrity: sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.7.2': + resolution: {integrity: sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==} + + '@floating-ui/dom@1.7.2': + resolution: {integrity: sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==} + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/sourcemap-codec@1.5.4': + resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@rolldown/pluginutils@1.0.0-beta.19': + resolution: {integrity: sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==} + + '@rollup/rollup-android-arm-eabi@4.45.1': + resolution: {integrity: sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.45.1': + resolution: {integrity: sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.45.1': + resolution: {integrity: sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.45.1': + resolution: {integrity: sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.45.1': + resolution: {integrity: sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.45.1': + resolution: {integrity: sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.45.1': + resolution: {integrity: sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.45.1': + resolution: {integrity: sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.45.1': + resolution: {integrity: sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.45.1': + resolution: {integrity: sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.45.1': + resolution: {integrity: sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.45.1': + resolution: {integrity: sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.45.1': + resolution: {integrity: sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.45.1': + resolution: {integrity: sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.45.1': + resolution: {integrity: sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.45.1': + resolution: {integrity: sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.45.1': + resolution: {integrity: sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.45.1': + resolution: {integrity: sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.45.1': + resolution: {integrity: sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.45.1': + resolution: {integrity: sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==} + cpu: [x64] + os: [win32] + + '@sxzz/popperjs-es@2.11.7': + resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + + '@types/web-bluetooth@0.0.16': + resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==} + + '@vitejs/plugin-vue@6.0.0': + resolution: {integrity: sha512-iAliE72WsdhjzTOp2DtvKThq1VBC4REhwRcaA+zPAAph6I+OQhUXv+Xu2KS7ElxYtb7Zc/3R30Hwv1DxEo7NXQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + vue: ^3.2.25 + + '@vue/compiler-core@3.5.18': + resolution: {integrity: sha512-3slwjQrrV1TO8MoXgy3aynDQ7lslj5UqDxuHnrzHtpON5CBinhWjJETciPngpin/T3OuW3tXUf86tEurusnztw==} + + '@vue/compiler-dom@3.5.18': + resolution: {integrity: sha512-RMbU6NTU70++B1JyVJbNbeFkK+A+Q7y9XKE2EM4NLGm2WFR8x9MbAtWxPPLdm0wUkuZv9trpwfSlL6tjdIa1+A==} + + '@vue/compiler-sfc@3.5.18': + resolution: {integrity: sha512-5aBjvGqsWs+MoxswZPoTB9nSDb3dhd1x30xrrltKujlCxo48j8HGDNj3QPhF4VIS0VQDUrA1xUfp2hEa+FNyXA==} + + '@vue/compiler-ssr@3.5.18': + resolution: {integrity: sha512-xM16Ak7rSWHkM3m22NlmcdIM+K4BMyFARAfV9hYFl+SFuRzrZ3uGMNW05kA5pmeMa0X9X963Kgou7ufdbpOP9g==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/devtools-api@7.7.7': + resolution: {integrity: sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==} + + '@vue/devtools-kit@7.7.7': + resolution: {integrity: sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==} + + '@vue/devtools-shared@7.7.7': + resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==} + + '@vue/reactivity@3.5.18': + resolution: {integrity: sha512-x0vPO5Imw+3sChLM5Y+B6G1zPjwdOri9e8V21NnTnlEvkxatHEH5B5KEAJcjuzQ7BsjGrKtfzuQ5eQwXh8HXBg==} + + '@vue/runtime-core@3.5.18': + resolution: {integrity: sha512-DUpHa1HpeOQEt6+3nheUfqVXRog2kivkXHUhoqJiKR33SO4x+a5uNOMkV487WPerQkL0vUuRvq/7JhRgLW3S+w==} + + '@vue/runtime-dom@3.5.18': + resolution: {integrity: sha512-YwDj71iV05j4RnzZnZtGaXwPoUWeRsqinblgVJwR8XTXYZ9D5PbahHQgsbmzUvCWNF6x7siQ89HgnX5eWkr3mw==} + + '@vue/server-renderer@3.5.18': + resolution: {integrity: sha512-PvIHLUoWgSbDG7zLHqSqaCoZvHi6NNmfVFOqO+OnwvqMz/tqQr3FuGWS8ufluNddk7ZLBJYMrjcw1c6XzR12mA==} + peerDependencies: + vue: 3.5.18 + + '@vue/shared@3.5.18': + resolution: {integrity: sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==} + + '@vueuse/core@9.13.0': + resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==} + + '@vueuse/metadata@9.13.0': + resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==} + + '@vueuse/shared@9.13.0': + resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-escapes@7.0.0: + resolution: {integrity: sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==} + engines: {node: '>=18'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + async-validator@4.2.5: + resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.11.0: + resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + birpc@2.5.0: + resolution: {integrity: sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.4.1: + resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-truncate@4.0.0: + resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} + engines: {node: '>=18'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@14.0.0: + resolution: {integrity: sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==} + engines: {node: '>=20'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + element-plus@2.10.4: + resolution: {integrity: sha512-UD4elWHrCnp1xlPhbXmVcaKFLCRaRAY6WWRwemGfGW3ceIjXm9fSYc9RNH3AiOEA6Ds1p9ZvhCs76CR9J8Vd+A==} + peerDependencies: + vue: ^3.2.0 + + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.25.8: + resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==} + engines: {node: '>=18'} + hasBin: true + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.31.0: + resolution: {integrity: sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-east-asian-width@1.3.0: + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + immutable@5.1.3: + resolution: {integrity: sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@4.0.0: + resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} + engines: {node: '>=12'} + + is-fullwidth-code-point@5.0.0: + resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} + engines: {node: '>=18'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lint-staged@16.1.2: + resolution: {integrity: sha512-sQKw2Si2g9KUZNY3XNvRuDq4UJqpHwF0/FQzZR2M7I5MvtpWvibikCjUVJzZdGE0ByurEl3KQNvsGetd1ty1/Q==} + engines: {node: '>=20.17'} + hasBin: true + + listr2@8.3.3: + resolution: {integrity: sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==} + engines: {node: '>=18.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash-unified@1.0.3: + resolution: {integrity: sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==} + peerDependencies: + '@types/lodash-es': '*' + lodash: '*' + lodash-es: '*' + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nano-spawn@1.0.2: + resolution: {integrity: sha512-21t+ozMQDAL/UGgQVBbZ/xXvNO10++ZPuTmKRO8k9V3AClVRht49ahtDjfY8l1q6nSHOrE5ASfthzH3ol6R/hg==} + engines: {node: '>=20.17'} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + normalize-wheel-es@1.2.0: + resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + + pinia@3.0.3: + resolution: {integrity: sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==} + peerDependencies: + typescript: '>=4.4.4' + vue: ^2.7.0 || ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup@4.45.1: + resolution: {integrity: sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + sass@1.89.2: + resolution: {integrity: sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==} + engines: {node: '>=14.0.0'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slice-ansi@5.0.0: + resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} + engines: {node: '>=12'} + + slice-ansi@7.1.0: + resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + superjson@2.2.2: + resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==} + engines: {node: '>=16'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite@7.0.5: + resolution: {integrity: sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-router@4.5.1: + resolution: {integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==} + peerDependencies: + vue: ^3.2.0 + + vue@3.5.18: + resolution: {integrity: sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@9.0.0: + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} + + yaml@2.8.0: + resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} + engines: {node: '>= 14.6'} + hasBin: true + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/parser@7.28.0': + dependencies: + '@babel/types': 7.28.1 + + '@babel/types@7.28.1': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@ctrl/tinycolor@3.6.1': {} + + '@element-plus/icons-vue@2.3.1(vue@3.5.18)': + dependencies: + vue: 3.5.18 + + '@esbuild/aix-ppc64@0.25.8': + optional: true + + '@esbuild/android-arm64@0.25.8': + optional: true + + '@esbuild/android-arm@0.25.8': + optional: true + + '@esbuild/android-x64@0.25.8': + optional: true + + '@esbuild/darwin-arm64@0.25.8': + optional: true + + '@esbuild/darwin-x64@0.25.8': + optional: true + + '@esbuild/freebsd-arm64@0.25.8': + optional: true + + '@esbuild/freebsd-x64@0.25.8': + optional: true + + '@esbuild/linux-arm64@0.25.8': + optional: true + + '@esbuild/linux-arm@0.25.8': + optional: true + + '@esbuild/linux-ia32@0.25.8': + optional: true + + '@esbuild/linux-loong64@0.25.8': + optional: true + + '@esbuild/linux-mips64el@0.25.8': + optional: true + + '@esbuild/linux-ppc64@0.25.8': + optional: true + + '@esbuild/linux-riscv64@0.25.8': + optional: true + + '@esbuild/linux-s390x@0.25.8': + optional: true + + '@esbuild/linux-x64@0.25.8': + optional: true + + '@esbuild/netbsd-arm64@0.25.8': + optional: true + + '@esbuild/netbsd-x64@0.25.8': + optional: true + + '@esbuild/openbsd-arm64@0.25.8': + optional: true + + '@esbuild/openbsd-x64@0.25.8': + optional: true + + '@esbuild/openharmony-arm64@0.25.8': + optional: true + + '@esbuild/sunos-x64@0.25.8': + optional: true + + '@esbuild/win32-arm64@0.25.8': + optional: true + + '@esbuild/win32-ia32@0.25.8': + optional: true + + '@esbuild/win32-x64@0.25.8': + optional: true + + '@eslint-community/eslint-utils@4.7.0(eslint@9.31.0)': + dependencies: + eslint: 9.31.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.21.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.1 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.3.0': {} + + '@eslint/core@0.15.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.1 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.31.0': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.3.4': + dependencies: + '@eslint/core': 0.15.1 + levn: 0.4.1 + + '@floating-ui/core@1.7.2': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.2': + dependencies: + '@floating-ui/core': 1.7.2 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/utils@0.2.10': {} + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/sourcemap-codec@1.5.4': {} + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + optional: true + + '@rolldown/pluginutils@1.0.0-beta.19': {} + + '@rollup/rollup-android-arm-eabi@4.45.1': + optional: true + + '@rollup/rollup-android-arm64@4.45.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.45.1': + optional: true + + '@rollup/rollup-darwin-x64@4.45.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.45.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.45.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.45.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.45.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.45.1': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.45.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.45.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.45.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.45.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.45.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.45.1': + optional: true + + '@sxzz/popperjs-es@2.11.7': {} + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.20 + + '@types/lodash@4.17.20': {} + + '@types/web-bluetooth@0.0.16': {} + + '@vitejs/plugin-vue@6.0.0(vite@7.0.5(sass@1.89.2)(yaml@2.8.0))(vue@3.5.18)': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.19 + vite: 7.0.5(sass@1.89.2)(yaml@2.8.0) + vue: 3.5.18 + + '@vue/compiler-core@3.5.18': + dependencies: + '@babel/parser': 7.28.0 + '@vue/shared': 3.5.18 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.18': + dependencies: + '@vue/compiler-core': 3.5.18 + '@vue/shared': 3.5.18 + + '@vue/compiler-sfc@3.5.18': + dependencies: + '@babel/parser': 7.28.0 + '@vue/compiler-core': 3.5.18 + '@vue/compiler-dom': 3.5.18 + '@vue/compiler-ssr': 3.5.18 + '@vue/shared': 3.5.18 + estree-walker: 2.0.2 + magic-string: 0.30.17 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.18': + dependencies: + '@vue/compiler-dom': 3.5.18 + '@vue/shared': 3.5.18 + + '@vue/devtools-api@6.6.4': {} + + '@vue/devtools-api@7.7.7': + dependencies: + '@vue/devtools-kit': 7.7.7 + + '@vue/devtools-kit@7.7.7': + dependencies: + '@vue/devtools-shared': 7.7.7 + birpc: 2.5.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.2 + + '@vue/devtools-shared@7.7.7': + dependencies: + rfdc: 1.4.1 + + '@vue/reactivity@3.5.18': + dependencies: + '@vue/shared': 3.5.18 + + '@vue/runtime-core@3.5.18': + dependencies: + '@vue/reactivity': 3.5.18 + '@vue/shared': 3.5.18 + + '@vue/runtime-dom@3.5.18': + dependencies: + '@vue/reactivity': 3.5.18 + '@vue/runtime-core': 3.5.18 + '@vue/shared': 3.5.18 + csstype: 3.1.3 + + '@vue/server-renderer@3.5.18(vue@3.5.18)': + dependencies: + '@vue/compiler-ssr': 3.5.18 + '@vue/shared': 3.5.18 + vue: 3.5.18 + + '@vue/shared@3.5.18': {} + + '@vueuse/core@9.13.0(vue@3.5.18)': + dependencies: + '@types/web-bluetooth': 0.0.16 + '@vueuse/metadata': 9.13.0 + '@vueuse/shared': 9.13.0(vue@3.5.18) + vue-demi: 0.14.10(vue@3.5.18) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@vueuse/metadata@9.13.0': {} + + '@vueuse/shared@9.13.0(vue@3.5.18)': + dependencies: + vue-demi: 0.14.10(vue@3.5.18) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-escapes@7.0.0: + dependencies: + environment: 1.1.0 + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + argparse@2.0.1: {} + + async-validator@4.2.5: {} + + asynckit@0.4.0: {} + + axios@1.11.0: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + balanced-match@1.0.2: {} + + birpc@2.5.0: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + callsites@3.1.0: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.4.1: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-truncate@4.0.0: + dependencies: + slice-ansi: 5.0.0 + string-width: 7.2.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorette@2.0.20: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@14.0.0: {} + + concat-map@0.0.1: {} + + copy-anything@3.0.5: + dependencies: + is-what: 4.1.16 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.1.3: {} + + dayjs@1.11.13: {} + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + delayed-stream@1.0.0: {} + + detect-libc@1.0.3: + optional: true + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + element-plus@2.10.4(vue@3.5.18): + dependencies: + '@ctrl/tinycolor': 3.6.1 + '@element-plus/icons-vue': 2.3.1(vue@3.5.18) + '@floating-ui/dom': 1.7.2 + '@popperjs/core': '@sxzz/popperjs-es@2.11.7' + '@types/lodash': 4.17.20 + '@types/lodash-es': 4.17.12 + '@vueuse/core': 9.13.0(vue@3.5.18) + async-validator: 4.2.5 + dayjs: 1.11.13 + escape-html: 1.0.3 + lodash: 4.17.21 + lodash-es: 4.17.21 + lodash-unified: 1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21) + memoize-one: 6.0.0 + normalize-wheel-es: 1.2.0 + vue: 3.5.18 + transitivePeerDependencies: + - '@vue/composition-api' + + emoji-regex@10.4.0: {} + + entities@4.5.0: {} + + environment@1.1.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild@0.25.8: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.8 + '@esbuild/android-arm': 0.25.8 + '@esbuild/android-arm64': 0.25.8 + '@esbuild/android-x64': 0.25.8 + '@esbuild/darwin-arm64': 0.25.8 + '@esbuild/darwin-x64': 0.25.8 + '@esbuild/freebsd-arm64': 0.25.8 + '@esbuild/freebsd-x64': 0.25.8 + '@esbuild/linux-arm': 0.25.8 + '@esbuild/linux-arm64': 0.25.8 + '@esbuild/linux-ia32': 0.25.8 + '@esbuild/linux-loong64': 0.25.8 + '@esbuild/linux-mips64el': 0.25.8 + '@esbuild/linux-ppc64': 0.25.8 + '@esbuild/linux-riscv64': 0.25.8 + '@esbuild/linux-s390x': 0.25.8 + '@esbuild/linux-x64': 0.25.8 + '@esbuild/netbsd-arm64': 0.25.8 + '@esbuild/netbsd-x64': 0.25.8 + '@esbuild/openbsd-arm64': 0.25.8 + '@esbuild/openbsd-x64': 0.25.8 + '@esbuild/openharmony-arm64': 0.25.8 + '@esbuild/sunos-x64': 0.25.8 + '@esbuild/win32-arm64': 0.25.8 + '@esbuild/win32-ia32': 0.25.8 + '@esbuild/win32-x64': 0.25.8 + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: {} + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.31.0: + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.31.0) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.0 + '@eslint/core': 0.15.1 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.31.0 + '@eslint/plugin-kit': 0.3.4 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.1 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + + eventemitter3@5.0.1: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.4.6(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + follow-redirects@1.15.9: {} + + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-east-asian-width@1.3.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + gopd@1.2.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hookable@5.5.3: {} + + husky@9.1.7: {} + + ignore@5.3.2: {} + + immutable@5.1.3: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@4.0.0: {} + + is-fullwidth-code-point@5.0.0: + dependencies: + get-east-asian-width: 1.3.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-what@4.1.16: {} + + isexe@2.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@3.1.3: {} + + lint-staged@16.1.2: + dependencies: + chalk: 5.4.1 + commander: 14.0.0 + debug: 4.4.1 + lilconfig: 3.1.3 + listr2: 8.3.3 + micromatch: 4.0.8 + nano-spawn: 1.0.2 + pidtree: 0.6.0 + string-argv: 0.3.2 + yaml: 2.8.0 + transitivePeerDependencies: + - supports-color + + listr2@8.3.3: + dependencies: + cli-truncate: 4.0.0 + colorette: 2.0.20 + eventemitter3: 5.0.1 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.17.21: {} + + lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21): + dependencies: + '@types/lodash-es': 4.17.12 + lodash: 4.17.21 + lodash-es: 4.17.21 + + lodash.merge@4.6.2: {} + + lodash@4.17.21: {} + + log-update@6.1.0: + dependencies: + ansi-escapes: 7.0.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.0 + strip-ansi: 7.1.0 + wrap-ansi: 9.0.0 + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + + math-intrinsics@1.1.0: {} + + memoize-one@6.0.0: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mimic-function@5.0.1: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + mitt@3.0.1: {} + + ms@2.1.3: {} + + nano-spawn@1.0.2: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + node-addon-api@7.1.1: + optional: true + + normalize-wheel-es@1.2.0: {} + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pidtree@0.6.0: {} + + pinia@3.0.3(vue@3.5.18): + dependencies: + '@vue/devtools-api': 7.7.7 + vue: 3.5.18 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier@3.6.2: {} + + proxy-from-env@1.1.0: {} + + punycode@2.3.1: {} + + readdirp@4.1.2: {} + + resolve-from@4.0.0: {} + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + rfdc@1.4.1: {} + + rollup@4.45.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.45.1 + '@rollup/rollup-android-arm64': 4.45.1 + '@rollup/rollup-darwin-arm64': 4.45.1 + '@rollup/rollup-darwin-x64': 4.45.1 + '@rollup/rollup-freebsd-arm64': 4.45.1 + '@rollup/rollup-freebsd-x64': 4.45.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.45.1 + '@rollup/rollup-linux-arm-musleabihf': 4.45.1 + '@rollup/rollup-linux-arm64-gnu': 4.45.1 + '@rollup/rollup-linux-arm64-musl': 4.45.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.45.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.45.1 + '@rollup/rollup-linux-riscv64-gnu': 4.45.1 + '@rollup/rollup-linux-riscv64-musl': 4.45.1 + '@rollup/rollup-linux-s390x-gnu': 4.45.1 + '@rollup/rollup-linux-x64-gnu': 4.45.1 + '@rollup/rollup-linux-x64-musl': 4.45.1 + '@rollup/rollup-win32-arm64-msvc': 4.45.1 + '@rollup/rollup-win32-ia32-msvc': 4.45.1 + '@rollup/rollup-win32-x64-msvc': 4.45.1 + fsevents: 2.3.3 + + sass@1.89.2: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.3 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.1 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + slice-ansi@5.0.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 4.0.0 + + slice-ansi@7.1.0: + dependencies: + ansi-styles: 6.2.1 + is-fullwidth-code-point: 5.0.0 + + source-map-js@1.2.1: {} + + speakingurl@14.0.1: {} + + string-argv@0.3.2: {} + + string-width@7.2.0: + dependencies: + emoji-regex: 10.4.0 + get-east-asian-width: 1.3.0 + strip-ansi: 7.1.0 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strip-json-comments@3.1.1: {} + + superjson@2.2.2: + dependencies: + copy-anything: 3.0.5 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tinyglobby@0.2.14: + dependencies: + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite@7.0.5(sass@1.89.2)(yaml@2.8.0): + dependencies: + esbuild: 0.25.8 + fdir: 6.4.6(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.45.1 + tinyglobby: 0.2.14 + optionalDependencies: + fsevents: 2.3.3 + sass: 1.89.2 + yaml: 2.8.0 + + vue-demi@0.14.10(vue@3.5.18): + dependencies: + vue: 3.5.18 + + vue-router@4.5.1(vue@3.5.18): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.18 + + vue@3.5.18: + dependencies: + '@vue/compiler-dom': 3.5.18 + '@vue/compiler-sfc': 3.5.18 + '@vue/runtime-dom': 3.5.18 + '@vue/server-renderer': 3.5.18(vue@3.5.18) + '@vue/shared': 3.5.18 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrap-ansi@9.0.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 7.2.0 + strip-ansi: 7.1.0 + + yaml@2.8.0: {} + + yocto-queue@0.1.0: {} diff --git a/frontend/vite-frontend/src/App.vue b/frontend/vite-frontend/src/App.vue new file mode 100644 index 0000000000000000000000000000000000000000..3005dfa3550fd2b6d0b31b7c04d589ceb185a614 --- /dev/null +++ b/frontend/vite-frontend/src/App.vue @@ -0,0 +1,81 @@ + + + + + \ No newline at end of file diff --git a/frontend/vite-frontend/src/api/auth.js b/frontend/vite-frontend/src/api/auth.js new file mode 100644 index 0000000000000000000000000000000000000000..834f328189116ca1b1e6e22bf8e58ee4e9a6d40b --- /dev/null +++ b/frontend/vite-frontend/src/api/auth.js @@ -0,0 +1,34 @@ +import api from './config' + +// 认证相关API - 基于后端AuthenticationController +export const authApi = { + // 用户登录 + login: (credentials) => { + const requestData = { + account: credentials.account || credentials.username, + password: credentials.password + } + return api.post('/auth/login', requestData) + }, + + // 用户注册 + register: (userData) => { + return api.post('/auth/register', { + account: userData.account || userData.username, + password: userData.password, + email: userData.email + }) + }, + + // 刷新token + refreshToken: (token) => { + return api.post('/auth/refresh-token', { + token: token + }) + }, + + // 用户登出 + logout: () => { + return api.post('/auth/logout') + } +} \ No newline at end of file diff --git a/frontend/vite-frontend/src/api/config.js b/frontend/vite-frontend/src/api/config.js new file mode 100644 index 0000000000000000000000000000000000000000..e9c51d55044cf42800b5eb8b3e2450bbc7eb3a82 --- /dev/null +++ b/frontend/vite-frontend/src/api/config.js @@ -0,0 +1,203 @@ +import axios from "axios"; +import { ElMessage } from "element-plus"; + +/** + * @type {string} API 基础地址 + */ + +// 创建axios实例 +const api = axios.create({ + baseURL: import.meta.env.VITE_BASE_URL, + timeout: 10000, + headers: { + "Content-Type": "application/json", + }, +}); + +// 不需要token的API路径 +const publicApis = ["/auth/login", "/auth/register", "/auth/refresh-token"]; + +// 请求拦截器 +api.interceptors.request.use( + (config) => { + console.log( + "发送请求:", + config.method?.toUpperCase(), + config.url, + config.data + ); + + // 检查是否是公开API(不需要token) + const isPublicApi = publicApis.some((api) => config.url?.includes(api)); + + if (isPublicApi) { + console.log("公开API,不需要token:", config.url); + return config; + } + + // 从localStorage获取token + const token = localStorage.getItem("token"); + console.log("当前token:", token ? "存在" : "不存在"); + + if (token) { + config.headers.Authorization = `Bearer ${token}`; + console.log( + "设置Authorization头:", + `Bearer ${token.substring(0, 20)}...` + ); + } else { + console.log("未找到token,请求将不包含Authorization头"); + } + + return config; + }, + (error) => { + console.error("请求错误:", error); + return Promise.reject(error); + } +); + +// 响应拦截器 +api.interceptors.response.use( + (response) => { + console.log("收到响应:", response.status, response.data); + + // 对于文件下载,直接返回响应 + if (response.config.responseType === "blob") { + console.log("文件下载响应,直接返回"); + return response; + } + + // 如果响应成功,检查后端统一响应格式 + const data = response.data; + console.log("响应数据格式检查:", { + isObject: typeof data === "object", + hasIsSuccess: data && "IsSuccess" in data, + isSuccess: data?.IsSuccess, + message: data?.Message, + }); + + if ( + data && + typeof data === "object" && + ("IsSuccess" in data || "Success" in data) + ) { + // 后端使用统一响应格式 + const isSuccess = + data.IsSuccess !== undefined ? data.IsSuccess : data.Success; + if (isSuccess) { + console.log("后端返回成功,数据:", data.Data); + return data.Data || data; + } else { + // 如果后端返回失败,抛出错误 + console.log("后端返回失败:", data.Message); + const error = new Error(data.Message || "请求失败"); + error.response = { data: data }; + throw error; + } + } + // 如果不是统一格式,直接返回数据 + console.log("非统一格式响应,直接返回数据"); + return data; + }, + (error) => { + console.error("响应错误:", error.response); + // 处理错误响应 + if (error.response) { + const { status, data } = error.response; + + switch (status) { + case 400: + ElMessage.error(data?.message || "请求参数错误"); + break; + case 401: + console.log("收到401错误,当前路径:", window.location.pathname); + console.log("401错误详情:", { + url: error.config?.url, + method: error.config?.method, + headers: error.config?.headers, + responseData: error.response?.data, + }); + + // 分析401错误的原因 + const responseData = error.response?.data; + if ( + responseData && + typeof responseData === "object" && + "Message" in responseData + ) { + console.log("后端返回的401错误信息:", responseData.Message); + } + + // 检查是否是用户管理相关的API调用 + const isUserManagementApi = error.config?.url?.includes("/user/"); + const isAuthApi = error.config?.url?.includes("/auth/"); + + if (isUserManagementApi) { + // 对于用户管理API的401错误,可能是权限不足或用户状态问题 + console.log("用户管理API 401错误,可能是权限问题或用户状态问题"); + ElMessage.error("操作失败:权限不足或账户状态异常"); + return Promise.reject(error); + } + + if (isAuthApi) { + // 对于认证API的401错误,可能是登录失败 + console.log("认证API 401错误,登录失败"); + ElMessage.error("登录失败:用户名或密码错误"); + return Promise.reject(error); + } + + // 其他API的401错误,可能是token过期或无效 + console.log("其他API 401错误,可能是token过期"); + ElMessage.error("登录已过期,请重新登录"); + localStorage.removeItem("token"); + localStorage.removeItem("userInfo"); + + // 只有在非登录页面才跳转,避免在登录页面时重复跳转 + if ( + window.location.pathname !== "/login" && + window.location.pathname !== "/register" + ) { + console.log("跳转到登录页面"); + window.location.href = "/login"; + } else { + console.log("当前已在登录页面,不进行跳转"); + } + break; + case 403: + ElMessage.error("权限不足,无法执行此操作"); + break; + case 404: + ElMessage.error("请求的资源不存在"); + break; + case 409: + ElMessage.error("数据冲突:" + (data?.message || "资源已存在")); + break; + case 422: + ElMessage.error( + "数据验证失败:" + (data?.message || "输入数据格式不正确") + ); + break; + case 500: + ElMessage.error("服务器内部错误,请稍后重试"); + break; + case 502: + ElMessage.error("网关错误,请稍后重试"); + break; + case 503: + ElMessage.error("服务暂时不可用,请稍后重试"); + break; + default: + ElMessage.error(data?.message || `请求失败 (${status})`); + } + } else if (error.request) { + ElMessage.error("网络连接失败"); + } else { + ElMessage.error("请求配置错误"); + } + + return Promise.reject(error); + } +); + +export default api; diff --git a/frontend/vite-frontend/src/api/example.js b/frontend/vite-frontend/src/api/example.js new file mode 100644 index 0000000000000000000000000000000000000000..9efdedcb3731a5be7545fc8000d04d0487756125 --- /dev/null +++ b/frontend/vite-frontend/src/api/example.js @@ -0,0 +1,289 @@ +// API使用示例 +import { + authApi, + userApi, + roleApi, + permissionApi, + menuApi, + fileApi, + logApi, + systemApi +} from './index' + +// 认证相关示例 +export const authExamples = { + // 用户登录 + async loginExample() { + try { + const credentials = { + username: 'admin', + password: '123456' + } + const response = await authApi.login(credentials) + console.log('登录成功:', response) + return response + } catch (error) { + console.error('登录失败:', error) + throw error + } + }, + + // 获取当前用户信息 + async getCurrentUserExample() { + try { + const response = await authApi.getCurrentUser() + console.log('当前用户信息:', response) + return response + } catch (error) { + console.error('获取用户信息失败:', error) + throw error + } + } +} + +// 用户管理示例 +export const userExamples = { + // 获取用户列表 + async getUsersExample() { + try { + const params = { + page: 1, + pageSize: 10, + keyword: '', + status: 'active' + } + const response = await userApi.getUsers(params) + console.log('用户列表:', response) + return response + } catch (error) { + console.error('获取用户列表失败:', error) + throw error + } + }, + + // 创建用户 + async createUserExample() { + try { + const userData = { + username: 'newuser', + email: 'newuser@example.com', + password: '123456', + roleIds: [1, 2] + } + const response = await userApi.createUser(userData) + console.log('创建用户成功:', response) + return response + } catch (error) { + console.error('创建用户失败:', error) + throw error + } + }, + + // 更新用户 + async updateUserExample(userId) { + try { + const userData = { + email: 'updated@example.com', + status: 'active' + } + const response = await userApi.updateUser(userId, userData) + console.log('更新用户成功:', response) + return response + } catch (error) { + console.error('更新用户失败:', error) + throw error + } + } +} + +// 角色管理示例 +export const roleExamples = { + // 获取角色列表 + async getRolesExample() { + try { + const response = await roleApi.getRoles() + console.log('角色列表:', response) + return response + } catch (error) { + console.error('获取角色列表失败:', error) + throw error + } + }, + + // 分配角色权限 + async assignRolePermissionsExample(roleId) { + try { + const permissionIds = [1, 2, 3, 4] + const response = await roleApi.assignRolePermissions(roleId, permissionIds) + console.log('分配权限成功:', response) + return response + } catch (error) { + console.error('分配权限失败:', error) + throw error + } + } +} + +// 权限管理示例 +export const permissionExamples = { + // 获取权限树 + async getPermissionTreeExample() { + try { + const response = await permissionApi.getPermissionTree() + console.log('权限树:', response) + return response + } catch (error) { + console.error('获取权限树失败:', error) + throw error + } + }, + + // 检查权限 + async checkPermissionExample() { + try { + const permissionCode = 'user:create' + const response = await permissionApi.checkPermission(permissionCode) + console.log('权限检查结果:', response) + return response + } catch (error) { + console.error('权限检查失败:', error) + throw error + } + } +} + +// 菜单管理示例 +export const menuExamples = { + // 获取用户菜单 + async getUserMenusExample() { + try { + const response = await menuApi.getUserMenus() + console.log('用户菜单:', response) + return response + } catch (error) { + console.error('获取用户菜单失败:', error) + throw error + } + } +} + +// 文件管理示例 +export const fileExamples = { + // 上传文件 + async uploadFileExample(file) { + try { + const onProgress = (progressEvent) => { + const percentCompleted = Math.round( + (progressEvent.loaded * 100) / progressEvent.total + ) + console.log('上传进度:', percentCompleted + '%') + } + + const response = await fileApi.uploadFile(file, onProgress) + console.log('文件上传成功:', response) + return response + } catch (error) { + console.error('文件上传失败:', error) + throw error + } + }, + + // 获取文件列表 + async getFilesExample() { + try { + const params = { + page: 1, + pageSize: 20, + keyword: '', + fileType: 'all' + } + const response = await fileApi.getFiles(params) + console.log('文件列表:', response) + return response + } catch (error) { + console.error('获取文件列表失败:', error) + throw error + } + } +} + +// 日志管理示例 +export const logExamples = { + // 获取日志列表 + async getLogsExample() { + try { + const params = { + page: 1, + pageSize: 50, + startDate: '2024-01-01', + endDate: '2024-12-31', + level: 'info', + operationType: 'create' + } + const response = await logApi.getLogs(params) + console.log('日志列表:', response) + return response + } catch (error) { + console.error('获取日志列表失败:', error) + throw error + } + }, + + // 导出日志 + async exportLogsExample() { + try { + const params = { + startDate: '2024-01-01', + endDate: '2024-12-31', + format: 'excel' + } + const response = await logApi.exportLogs(params) + + // 创建下载链接 + const blob = new Blob([response], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) + const url = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = 'logs.xlsx' + link.click() + window.URL.revokeObjectURL(url) + + console.log('日志导出成功') + } catch (error) { + console.error('日志导出失败:', error) + throw error + } + } +} + +// 系统设置示例 +export const systemExamples = { + // 获取系统信息 + async getSystemInfoExample() { + try { + const response = await systemApi.getSystemInfo() + console.log('系统信息:', response) + return response + } catch (error) { + console.error('获取系统信息失败:', error) + throw error + } + }, + + // 更新系统设置 + async updateSystemSettingsExample() { + try { + const settings = [ + { key: 'site_name', value: '通用管理系统' }, + { key: 'site_description', value: '一个功能强大的管理系统' }, + { key: 'max_file_size', value: '10MB' } + ] + const response = await systemApi.batchUpdateSettings(settings) + console.log('系统设置更新成功:', response) + return response + } catch (error) { + console.error('系统设置更新失败:', error) + throw error + } + } +} \ No newline at end of file diff --git a/frontend/vite-frontend/src/api/file.js b/frontend/vite-frontend/src/api/file.js new file mode 100644 index 0000000000000000000000000000000000000000..40022dee3204d65e58bbd1634a3aa7a1a59e29ed --- /dev/null +++ b/frontend/vite-frontend/src/api/file.js @@ -0,0 +1,41 @@ +import api from './config' + +// 文件管理相关API - 基于后端FileController +export const fileApi = { + // 上传文件 + uploadFile: (file, parentId = null) => { + const formData = new FormData() + formData.append('file', file) + if (parentId) { + formData.append('parentId', parentId) + } + + return api.post('/files/upload', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + }, + + // 获取文件列表 + getFiles: () => { + return api.get('/files/list') + }, + + // 下载文件 + downloadFile: (fileId) => { + return api.get(`/files/${fileId}`, { + responseType: 'blob' + }) + }, + + // 删除文件 + deleteFile: (fileId) => { + return api.delete(`/files/${fileId}`) + }, + + // 测试权限 + testPermission: () => { + return api.get('/files/test-permission') + } +} \ No newline at end of file diff --git a/frontend/vite-frontend/src/api/index.js b/frontend/vite-frontend/src/api/index.js new file mode 100644 index 0000000000000000000000000000000000000000..acff7acda50cda06a946ca857a61733b369c6d14 --- /dev/null +++ b/frontend/vite-frontend/src/api/index.js @@ -0,0 +1,11 @@ +// 统一导出所有API模块 +export { authApi } from './auth' +export { userApi } from './user' +export { roleApi } from './role' +export { permissionApi } from './permission' +export { fileApi } from './file' +export { logApi } from './log' +// export { systemApi } from './system' + +// 导出API配置 +export { default as api } from './config' \ No newline at end of file diff --git a/frontend/vite-frontend/src/api/log.js b/frontend/vite-frontend/src/api/log.js new file mode 100644 index 0000000000000000000000000000000000000000..74b29d3221176e14a7814788cd58384ba7f473f4 --- /dev/null +++ b/frontend/vite-frontend/src/api/log.js @@ -0,0 +1,41 @@ +import api from './config' + +// 日志管理相关API - 基于后端LogController +export const logApi = { + // 获取所有日志 + getLogs: () => { + return api.get('/logs') + }, + + // 获取单个日志 + getLog: (id) => { + return api.get(`/logs/${id}`) + }, + + // 创建日志 + createLog: (logData) => { + return api.post('/logs', logData) + }, + + // 根据级别获取日志 + getLogsByLevel: (level) => { + return api.get(`/logs/level/${level}`) + }, + + // 根据用户获取日志 + getLogsByUser: (userId) => { + return api.get(`/logs/user/${userId}`) + }, + + // 根据日期范围获取日志 + getLogsByDateRange: (start, end) => { + return api.get('/logs/date', { + params: { start, end } + }) + }, + + // 根据来源获取日志 + getLogsBySource: (source) => { + return api.get(`/logs/source/${source}`) + } +} \ No newline at end of file diff --git a/frontend/vite-frontend/src/api/permission.js b/frontend/vite-frontend/src/api/permission.js new file mode 100644 index 0000000000000000000000000000000000000000..174c18f61ef540a7dad68a3b4acdc4cab01ef542 --- /dev/null +++ b/frontend/vite-frontend/src/api/permission.js @@ -0,0 +1,33 @@ +import api from './config' + +export const permissionApi = { + // 获取所有权限列表 + getPermissions: () => { + return api.get('/permissions') + }, + + // 创建新权限 + createPermission: (permissionData) => { + return api.post('/permissions/create', permissionData) + }, + + // 删除权限 + deletePermission: (permissionId) => { + return api.delete(`/permissions/${permissionId}`) + }, + + // 获取权限规则配置 + getPermissionRules: () => { + return api.get('/permissions/rules') + }, + + // 更新权限规则配置 + updatePermissionRules: (rules) => { + return api.put('/permissions/rules', rules) + }, + + // 刷新权限规则配置 + refreshPermissionRules: () => { + return api.post('/permissions/rules/refresh') + } +} \ No newline at end of file diff --git a/frontend/vite-frontend/src/api/role.js b/frontend/vite-frontend/src/api/role.js new file mode 100644 index 0000000000000000000000000000000000000000..860d8c3ce485d0d32cf4ee42deea9beafca5e04d --- /dev/null +++ b/frontend/vite-frontend/src/api/role.js @@ -0,0 +1,49 @@ +import api from './config' + +// 角色管理相关API - 基于后端RoleController +export const roleApi = { + // 获取角色列表 + getRoles: () => { + return api.get('/roles') + }, + + // 获取单个角色 + getRole: (id) => { + return api.get(`/roles/${id}`) + }, + + // 创建角色 + createRole: (roleData) => { + return api.post('/roles', roleData) + }, + + // 更新角色 + updateRole: (id, roleData) => { + return api.put(`/roles/${id}`, roleData) + }, + + // 删除角色 + deleteRole: (id) => { + return api.delete(`/roles/${id}`) + }, + + // 为角色分配权限(使用权限编码) + assignPermissionsToRole: (id, permissionCodes) => { + return api.post(`/roles/${id}/permissions`, permissionCodes) + }, + + // 为角色分配权限(使用权限ID) + assignPermissionsToRoleByIds: (id, permissionIds) => { + return api.post(`/roles/${id}/permissions/ids`, permissionIds) + }, + + // 移除角色权限 + removePermissionsFromRole: (id, permissionIds) => { + return api.delete(`/roles/${id}/permissions`, { data: permissionIds }) + }, + + // 获取角色权限 + getRolePermissions: (id) => { + return api.get(`/roles/${id}/permissions`) + } +} \ No newline at end of file diff --git a/frontend/vite-frontend/src/api/user.js b/frontend/vite-frontend/src/api/user.js new file mode 100644 index 0000000000000000000000000000000000000000..301cf02af336d0c93fce2f466f049a5c884be88c --- /dev/null +++ b/frontend/vite-frontend/src/api/user.js @@ -0,0 +1,54 @@ +import api from './config' + +// 用户管理相关API - 基于后端UserManagementController +export const userApi = { + // 获取用户列表 + getUsers: () => { + return api.get('/user') + }, + + // 创建用户 + createUser: (userData) => { + return api.post('/user/create', userData) + }, + + // 获取单个用户 + getUser: (id) => { + return api.get(`/user/${id}`) + }, + + // 更新用户 + updateUser: (id, userData) => { + return api.put(`/user/${id}`, userData) + }, + + // 删除用户 + deleteUser: (id) => { + return api.delete(`/user/${id}`) + }, + + // 恢复用户(软删除恢复) + restoreUser: (id) => { + return api.post(`/user/${id}/restore`) + }, + + // 分配用户角色 + assignUserRoles: (id, roleIds) => { + return api.post(`/user/${id}/role`, roleIds) + }, + + // 移除用户角色 + removeUserRole: (id, roleId) => { + return api.delete(`/user/${id}/roles/${roleId}`) + }, + + // 获取用户权限 + getUserPermissions: (id) => { + return api.get(`/user/${id}/permissions`) + }, + + // 检查用户权限 + checkUserPermission: (id, permissionCode) => { + return api.post(`/user/${id}/permissions/check`, { permissionCode }) + } +} \ No newline at end of file diff --git a/frontend/vite-frontend/src/assets/image/bd26f78c344b3ad6afef7b12b1421227.jpg b/frontend/vite-frontend/src/assets/image/bd26f78c344b3ad6afef7b12b1421227.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f4ae368623fee0b542355016ba92a8a6392070bb Binary files /dev/null and b/frontend/vite-frontend/src/assets/image/bd26f78c344b3ad6afef7b12b1421227.jpg differ diff --git a/frontend/vite-frontend/src/assets/vue.svg b/frontend/vite-frontend/src/assets/vue.svg new file mode 100644 index 0000000000000000000000000000000000000000..770e9d333ee70e75fe7c0bad7fb13e4f6ed4627a --- /dev/null +++ b/frontend/vite-frontend/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/vite-frontend/src/components/AppLayout.vue b/frontend/vite-frontend/src/components/AppLayout.vue new file mode 100644 index 0000000000000000000000000000000000000000..ae2ee4563bbbcc2199fc7b2be1002bc3e8e6cc3d --- /dev/null +++ b/frontend/vite-frontend/src/components/AppLayout.vue @@ -0,0 +1,250 @@ + + + + + \ No newline at end of file diff --git a/frontend/vite-frontend/src/components/Header.vue b/frontend/vite-frontend/src/components/Header.vue new file mode 100644 index 0000000000000000000000000000000000000000..02640bc59ae4ab0d70de2ac4ae18421d52d04152 --- /dev/null +++ b/frontend/vite-frontend/src/components/Header.vue @@ -0,0 +1,479 @@ + + + + + \ No newline at end of file diff --git a/frontend/vite-frontend/src/components/Layout.vue b/frontend/vite-frontend/src/components/Layout.vue new file mode 100644 index 0000000000000000000000000000000000000000..bbbb3220b2ebdf0c51e2bc54f683ff1c2c5c24b0 --- /dev/null +++ b/frontend/vite-frontend/src/components/Layout.vue @@ -0,0 +1,250 @@ + + + + + \ No newline at end of file diff --git a/frontend/vite-frontend/src/components/Sidebar.vue b/frontend/vite-frontend/src/components/Sidebar.vue new file mode 100644 index 0000000000000000000000000000000000000000..bf791f7999f0c663fc9d763fd7b839fe2d28a057 --- /dev/null +++ b/frontend/vite-frontend/src/components/Sidebar.vue @@ -0,0 +1,301 @@ + + + + + \ No newline at end of file diff --git a/frontend/vite-frontend/src/main.js b/frontend/vite-frontend/src/main.js new file mode 100644 index 0000000000000000000000000000000000000000..3fb3727a8f45e5ace6db4dd33f5b2042a5153b9b --- /dev/null +++ b/frontend/vite-frontend/src/main.js @@ -0,0 +1,21 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import './style.css' +import App from './App.vue' +import { router } from './router' +import ElementPlus from 'element-plus'; +import 'element-plus/dist/index.css'; +import { useUserStore } from './stores/user' + +const app = createApp(App) +const pinia = createPinia() + +app.use(pinia) +app.use(router) +app.use(ElementPlus) + +// 初始化用户状态 +const userStore = useUserStore() +userStore.initUserState() + +app.mount('#app') \ No newline at end of file diff --git a/frontend/vite-frontend/src/public/vite.svg b/frontend/vite-frontend/src/public/vite.svg new file mode 100644 index 0000000000000000000000000000000000000000..e7b8dfb1b2a60bd50538bec9f876511b9cac21e3 --- /dev/null +++ b/frontend/vite-frontend/src/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/vite-frontend/src/router/index.js b/frontend/vite-frontend/src/router/index.js new file mode 100644 index 0000000000000000000000000000000000000000..580a53fe27b5b8fb392382fb69f6de62c80fc719 --- /dev/null +++ b/frontend/vite-frontend/src/router/index.js @@ -0,0 +1,7 @@ +import { createRouter, createWebHistory } from 'vue-router'; +import { routes } from './routes.js'; + +export const router = createRouter({ + history: createWebHistory(), + routes: routes +}) \ No newline at end of file diff --git a/frontend/vite-frontend/src/router/routes.js b/frontend/vite-frontend/src/router/routes.js new file mode 100644 index 0000000000000000000000000000000000000000..932e1098dacb726126351acb2fe27c775aed4e39 --- /dev/null +++ b/frontend/vite-frontend/src/router/routes.js @@ -0,0 +1,97 @@ +export const routes = [ + { + path: '/', + redirect: '/login' + }, + { + path: '/login', + component: () => import('../view/Login.vue') + }, + { + path: '/register', + component: () => import('../view/Register.vue') + }, + { + path: '/app', + component: () => import('../components/AppLayout.vue'), + children: [ + { + path: '', + redirect: '/dashboard' + }, + { + path: 'dashboard', + component: () => import('../view/Dashboard.vue') + }, + { + path: 'user/list', + component: () => import('../view/UserList.vue') + }, + { + path: 'user/manage', + component: () => import('../view/UserManagement.vue') + }, + { + path: 'role', + component: () => import('../view/RoleManagement.vue') + }, + { + path: 'permission/control', + component: () => import('../view/PermissionControl.vue') + }, + { + path: 'system/settings', + component: () => import('../view/SystemSettings.vue') + }, + { + path: 'system/logs', + component: () => import('../view/LogManagement.vue') + }, + { + path: 'article', + component: () => import('../view/ArticleManagement.vue') + }, + { + path: 'file', + component: () => import('../view/FileManagement.vue') + } + ] + }, + // 重定向路由,保持原有路径兼容性 + { + path: '/dashboard', + redirect: '/app/dashboard' + }, + { + path: '/user/list', + redirect: '/app/user/list' + }, + { + path: '/user/manage', + redirect: '/app/user/manage' + }, + { + path: '/role', + redirect: '/app/role' + }, + { + path: '/permission/control', + redirect: '/app/permission/control' + }, + { + path: '/system/settings', + redirect: '/app/system/settings' + }, + { + path: '/system/logs', + redirect: '/app/system/logs' + }, + { + path: '/article', + redirect: '/app/article' + }, + { + path: '/file', + redirect: '/app/file' + } +]; \ No newline at end of file diff --git a/frontend/vite-frontend/src/stores/user.js b/frontend/vite-frontend/src/stores/user.js new file mode 100644 index 0000000000000000000000000000000000000000..699675288ad0f86a8c95c56e940690cfa8891d14 --- /dev/null +++ b/frontend/vite-frontend/src/stores/user.js @@ -0,0 +1,134 @@ +// src/stores/user.js +import { defineStore } from 'pinia' +import { ElMessage } from 'element-plus' +import { userApi } from '@/api' + +export const useUserStore = defineStore('user', { + state: () => ({ + // 用户信息 + userInfo: null, + // 用户权限 + permissions: [], + // 用户菜单 + menus: [], + // 登录状态 + isLoggedIn: false, + // token(从localStorage中获取) + token: localStorage.getItem('token') || null + }), + + getters: { + // 获取用户名 + username: (state) => state.userInfo?.username || '', + // 获取用户邮箱 + email: (state) => state.userInfo?.email || '', + // 获取用户角色 + role: (state) => state.userInfo?.role || '', + // 检查是否有某个权限 + hasPermission: (state) => (permissionCode) => { + return state.permissions.includes(permissionCode) + }, + }, + + actions: { + // 设置登录状态 + async setLoginState(data) { + // 后端返回的是 LoginResultDto: { Token, UserId, RoleId, UserName, |UserInfo| } + + // 保存token和用户信息 + this.token = data.token + this.userInfo = { + userId: data.userId, + username: data.userName, + roleId: data.roleId, + } + console.log('userInfo:', this.userInfo) + this.isLoggedIn = true + + // 保存到localStorage + localStorage.setItem('token', data.token) + localStorage.setItem('userInfo', JSON.stringify(this.userInfo)) + + console.log('Token已保存到localStorage:', localStorage.getItem('token') ? '成功' : '失败') + + // 获取用户权限 + try { + const permissions = await this.fetchUserPermissions(data.userId) + localStorage.setItem('userPermissions', JSON.stringify(permissions)) + console.log('用户权限已保存到localStorage') + } catch (error) { + console.error('获取用户权限失败:', error) + } + + ElMessage.success('登录成功') + }, + + // 用户登出 + logout() { + // 清除本地数据 + this.clearUserData() + ElMessage.success('已退出登录') + }, + + // 更新用户信息 + updateUserInfo(data) { + this.userInfo = { ...this.userInfo, ...data } + localStorage.setItem('userInfo', JSON.stringify(this.userInfo)) + }, + + // 清除用户数据 + clearUserData() { + this.userInfo = null + this.permissions = [] + this.menus = [] + this.isLoggedIn = false + this.token = null + + localStorage.removeItem('token') + localStorage.removeItem('userInfo') + localStorage.removeItem('userPermissions') + }, + + // 获取用户权限 + async fetchUserPermissions(userId) { + try { + console.log('获取用户权限,用户ID:', userId) + const permissions = await userApi.getUserPermissions(userId) + console.log('获取到的用户权限:', permissions) + this.permissions = permissions || [] + return permissions + } catch (error) { + console.error('获取用户权限失败:', error) + this.permissions = [] + return [] + } + }, + + // 初始化用户状态(从localStorage恢复) + initUserState() { + const token = localStorage.getItem('token') + const userInfo = localStorage.getItem('userInfo') + + console.log('初始化用户状态:', { + token: token ? '存在' : '不存在', + userInfo: userInfo ? '存在' : '不存在' + }) + + if (token && userInfo) { + this.token = token + this.userInfo = JSON.parse(userInfo) + this.isLoggedIn = true + console.log('用户状态已恢复:', this.userInfo) + + // 恢复权限信息 + const savedPermissions = localStorage.getItem('userPermissions') + if (savedPermissions) { + this.permissions = JSON.parse(savedPermissions) + console.log('权限信息已恢复:', this.permissions) + } + } else { + console.log('未找到保存的用户状态') + } + } + } +}) diff --git a/frontend/vite-frontend/src/style.css b/frontend/vite-frontend/src/style.css new file mode 100644 index 0000000000000000000000000000000000000000..16c4237331f1529d97156004771a70e43f498a3e --- /dev/null +++ b/frontend/vite-frontend/src/style.css @@ -0,0 +1,556 @@ +/* 现代化设计系统 - 全局样式优化 */ + +/* CSS 变量定义 */ +:root { + /* 主色调 */ + --primary-color: #3b82f6; + --primary-light: #60a5fa; + --primary-dark: #2563eb; + --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + + /* 辅助色 */ + --success-color: #10b981; + --warning-color: #f59e0b; + --danger-color: #ef4444; + --info-color: #06b6d4; + + /* 中性色 */ + --text-primary: #1f2937; + --text-secondary: #6b7280; + --text-muted: #9ca3af; + --text-inverse: #ffffff; + + /* 背景色 */ + --bg-primary: #ffffff; + --bg-secondary: #f8fafc; + --bg-tertiary: #f1f5f9; + --bg-dark: #1e293b; + + /* 边框色 */ + --border-light: #e5e7eb; + --border-medium: #d1d5db; + --border-dark: #9ca3af; + + /* 阴影 */ + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + + /* 圆角 */ + --radius-sm: 0.375rem; + --radius-md: 0.5rem; + --radius-lg: 0.75rem; + --radius-xl: 1rem; + --radius-full: 9999px; + + /* 间距 */ + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 0.75rem; + --space-4: 1rem; + --space-5: 1.25rem; + --space-6: 1.5rem; + --space-8: 2rem; + --space-10: 2.5rem; + --space-12: 3rem; + --space-16: 4rem; + --space-20: 5rem; + + /* 字体 */ + --font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + --font-size-xs: 0.75rem; + --font-size-sm: 0.875rem; + --font-size-base: 1rem; + --font-size-lg: 1.125rem; + --font-size-xl: 1.25rem; + --font-size-2xl: 1.5rem; + --font-size-3xl: 1.875rem; + --font-size-4xl: 2.25rem; + + /* 行高 */ + --line-height-tight: 1.25; + --line-height-normal: 1.5; + --line-height-relaxed: 1.75; + + /* 过渡动画 */ + --transition-fast: 0.15s ease; + --transition-normal: 0.3s ease; + --transition-slow: 0.5s ease; +} + +/* 深色模式变量 */ +@media (prefers-color-scheme: dark) { + :root { + --text-primary: #f9fafb; + --text-secondary: #d1d5db; + --text-muted: #9ca3af; + --bg-primary: #111827; + --bg-secondary: #1f2937; + --bg-tertiary: #374151; + --border-light: #374151; + --border-medium: #4b5563; + --border-dark: #6b7280; + } +} + +/* 基础重置 */ +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + padding: 0; + font-family: var(--font-family); + font-size: var(--font-size-base); + line-height: var(--line-height-normal); + color: var(--text-primary); + background-color: var(--bg-secondary); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +/* 链接样式 */ +a { + color: var(--primary-color); + text-decoration: none; + transition: color var(--transition-fast); +} + +a:hover { + color: var(--primary-dark); +} + +/* 标题样式 */ +h1, h2, h3, h4, h5, h6 { + margin: 0; + font-weight: 600; + line-height: var(--line-height-tight); + color: var(--text-primary); +} + +h1 { font-size: var(--font-size-4xl); } +h2 { font-size: var(--font-size-3xl); } +h3 { font-size: var(--font-size-2xl); } +h4 { font-size: var(--font-size-xl); } +h5 { font-size: var(--font-size-lg); } +h6 { font-size: var(--font-size-base); } + +/* 段落样式 */ +p { + margin: 0 0 var(--space-4) 0; + color: var(--text-secondary); + line-height: var(--line-height-relaxed); +} + +/* 按钮基础样式 */ +button { + font-family: inherit; + font-size: inherit; + line-height: inherit; + border: none; + background: none; + cursor: pointer; + transition: all var(--transition-fast); +} + +/* 输入框基础样式 */ +input, textarea, select { + font-family: inherit; + font-size: inherit; + line-height: inherit; + border: 1px solid var(--border-medium); + border-radius: var(--radius-md); + padding: var(--space-3) var(--space-4); + background-color: var(--bg-primary); + color: var(--text-primary); + transition: all var(--transition-fast); +} + +input:focus, textarea:focus, select:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +/* 列表样式 */ +ul, ol { + margin: 0; + padding: 0; + list-style: none; +} + +/* 图片样式 */ +img { + max-width: 100%; + height: auto; + display: block; +} + +/* 表格样式 */ +table { + border-collapse: collapse; + width: 100%; +} + +th, td { + padding: var(--space-3) var(--space-4); + text-align: left; + border-bottom: 1px solid var(--border-light); +} + +th { + font-weight: 600; + color: var(--text-primary); + background-color: var(--bg-tertiary); +} + +/* 卡片样式 */ +.card { + background-color: var(--bg-primary); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-md); + padding: var(--space-6); + transition: all var(--transition-normal); +} + +.card:hover { + box-shadow: var(--shadow-lg); + transform: translateY(-2px); +} + +/* 应用容器样式 */ +#app { + width: 100%; + min-height: 100vh; + margin: 0; + padding: 0; + text-align: left; +} + +/* 滚动条美化 */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--bg-tertiary); + border-radius: var(--radius-full); +} + +::-webkit-scrollbar-thumb { + background: var(--border-medium); + border-radius: var(--radius-full); + transition: background var(--transition-fast); +} + +::-webkit-scrollbar-thumb:hover { + background: var(--border-dark); +} + +/* 选择文本样式 */ +::selection { + background-color: var(--primary-color); + color: var(--text-inverse); +} + +/* 焦点样式 */ +:focus-visible { + outline: 2px solid var(--primary-color); + outline-offset: 2px; +} + +/* 加载动画 */ +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.animate-spin { + animation: spin 1s linear infinite; +} + +/* 淡入动画 */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.animate-fade-in { + animation: fadeIn 0.3s ease-out; +} + +/* 滑入动画 */ +@keyframes slideIn { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.animate-slide-in { + animation: slideIn 0.3s ease-out; +} + +/* 脉冲动画 */ +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +.animate-pulse { + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +/* 渐变背景 */ +.gradient-primary { + background: var(--primary-gradient); +} + +.gradient-success { + background: linear-gradient(135deg, #10b981 0%, #059669 100%); +} + +.gradient-warning { + background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); +} + +.gradient-danger { + background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); +} + +/* 玻璃效果 */ +.glass { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.2); +} + +/* 文本渐变 */ +.text-gradient { + background: var(--primary-gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* 工具类 */ +.text-center { text-align: center; } +.text-left { text-align: left; } +.text-right { text-align: right; } + +.font-bold { font-weight: 700; } +.font-semibold { font-weight: 600; } +.font-medium { font-weight: 500; } +.font-normal { font-weight: 400; } +.font-light { font-weight: 300; } + +.text-xs { font-size: var(--font-size-xs); } +.text-sm { font-size: var(--font-size-sm); } +.text-base { font-size: var(--font-size-base); } +.text-lg { font-size: var(--font-size-lg); } +.text-xl { font-size: var(--font-size-xl); } +.text-2xl { font-size: var(--font-size-2xl); } +.text-3xl { font-size: var(--font-size-3xl); } +.text-4xl { font-size: var(--font-size-4xl); } + +.text-primary { color: var(--text-primary); } +.text-secondary { color: var(--text-secondary); } +.text-muted { color: var(--text-muted); } +.text-inverse { color: var(--text-inverse); } + +.bg-primary { background-color: var(--bg-primary); } +.bg-secondary { background-color: var(--bg-secondary); } +.bg-tertiary { background-color: var(--bg-tertiary); } + +.border { border: 1px solid var(--border-light); } +.border-t { border-top: 1px solid var(--border-light); } +.border-b { border-bottom: 1px solid var(--border-light); } +.border-l { border-left: 1px solid var(--border-light); } +.border-r { border-right: 1px solid var(--border-light); } + +.rounded-sm { border-radius: var(--radius-sm); } +.rounded-md { border-radius: var(--radius-md); } +.rounded-lg { border-radius: var(--radius-lg); } +.rounded-xl { border-radius: var(--radius-xl); } +.rounded-full { border-radius: var(--radius-full); } + +.shadow-sm { box-shadow: var(--shadow-sm); } +.shadow-md { box-shadow: var(--shadow-md); } +.shadow-lg { box-shadow: var(--shadow-lg); } +.shadow-xl { box-shadow: var(--shadow-xl); } + +/* 间距工具类 */ +.m-0 { margin: 0; } +.m-1 { margin: var(--space-1); } +.m-2 { margin: var(--space-2); } +.m-3 { margin: var(--space-3); } +.m-4 { margin: var(--space-4); } +.m-5 { margin: var(--space-5); } +.m-6 { margin: var(--space-6); } +.m-8 { margin: var(--space-8); } +.m-10 { margin: var(--space-10); } +.m-12 { margin: var(--space-12); } +.m-16 { margin: var(--space-16); } +.m-20 { margin: var(--space-20); } + +.p-0 { padding: 0; } +.p-1 { padding: var(--space-1); } +.p-2 { padding: var(--space-2); } +.p-3 { padding: var(--space-3); } +.p-4 { padding: var(--space-4); } +.p-5 { padding: var(--space-5); } +.p-6 { padding: var(--space-6); } +.p-8 { padding: var(--space-8); } +.p-10 { padding: var(--space-10); } +.p-12 { padding: var(--space-12); } +.p-16 { padding: var(--space-16); } +.p-20 { padding: var(--space-20); } + +/* 弹性布局工具类 */ +.flex { display: flex; } +.inline-flex { display: inline-flex; } +.flex-col { flex-direction: column; } +.flex-row { flex-direction: row; } +.flex-wrap { flex-wrap: wrap; } +.flex-nowrap { flex-wrap: nowrap; } + +.items-start { align-items: flex-start; } +.items-center { align-items: center; } +.items-end { align-items: flex-end; } +.items-stretch { align-items: stretch; } + +.justify-start { justify-content: flex-start; } +.justify-center { justify-content: center; } +.justify-end { justify-content: flex-end; } +.justify-between { justify-content: space-between; } +.justify-around { justify-content: space-around; } +.justify-evenly { justify-content: space-evenly; } + +.flex-1 { flex: 1 1 0%; } +.flex-auto { flex: 1 1 auto; } +.flex-initial { flex: 0 1 auto; } +.flex-none { flex: none; } + +/* 网格布局工具类 */ +.grid { display: grid; } +.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); } +.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } +.grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } +.grid-cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); } +.grid-cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); } + +.gap-1 { gap: var(--space-1); } +.gap-2 { gap: var(--space-2); } +.gap-3 { gap: var(--space-3); } +.gap-4 { gap: var(--space-4); } +.gap-5 { gap: var(--space-5); } +.gap-6 { gap: var(--space-6); } +.gap-8 { gap: var(--space-8); } +.gap-10 { gap: var(--space-10); } +.gap-12 { gap: var(--space-12); } + +/* 动画工具类 */ +.transition { transition: all var(--transition-normal); } +.transition-fast { transition: all var(--transition-fast); } +.transition-slow { transition: all var(--transition-slow); } + +.hover\:scale-105:hover { transform: scale(1.05); } +.hover\:scale-110:hover { transform: scale(1.1); } +.hover\:scale-95:hover { transform: scale(0.95); } + +.hover\:shadow-lg:hover { box-shadow: var(--shadow-lg); } +.hover\:shadow-xl:hover { box-shadow: var(--shadow-xl); } + +/* 响应式工具类 */ +@media (min-width: 640px) { + .sm\:hidden { display: none; } + .sm\:block { display: block; } + .sm\:flex { display: flex; } + .sm\:grid { display: grid; } +} + +@media (min-width: 768px) { + .md\:hidden { display: none; } + .md\:block { display: block; } + .md\:flex { display: flex; } + .md\:grid { display: grid; } +} + +@media (min-width: 1024px) { + .lg\:hidden { display: none; } + .lg\:block { display: block; } + .lg\:flex { display: flex; } + .lg\:grid { display: grid; } +} + +@media (min-width: 1280px) { + .xl\:hidden { display: none; } + .xl\:block { display: block; } + .xl\:flex { display: flex; } + .xl\:grid { display: grid; } +} + +/* 响应式容器 */ +.container { + width: 100%; + margin-left: auto; + margin-right: auto; + padding-left: var(--space-4); + padding-right: var(--space-4); +} + +@media (min-width: 640px) { + .container { + max-width: 640px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 768px; + } +} + +@media (min-width: 1024px) { + .container { + max-width: 1024px; + } +} + +@media (min-width: 1280px) { + .container { + max-width: 1280px; + } +} + +@media (min-width: 1536px) { + .container { + max-width: 1536px; + } +} + + diff --git a/frontend/vite-frontend/src/utils/permission.js b/frontend/vite-frontend/src/utils/permission.js new file mode 100644 index 0000000000000000000000000000000000000000..dd6090b4cb42f46842194fc5150a1c30edd41057 --- /dev/null +++ b/frontend/vite-frontend/src/utils/permission.js @@ -0,0 +1,101 @@ +import { useUserStore } from '../stores/user' + +// 权限检查工具函数 +export const permissionUtils = { + // 检查用户是否有指定权限 + hasPermission(permissionCode) { + const userStore = useUserStore() + return userStore.hasPermission(permissionCode) + }, + + // 检查用户是否有指定角色 + hasRole(roleName) { + const userStore = useUserStore() + return userStore.hasRole(roleName) + }, + + // 检查用户是否有任意一个权限 + hasAnyPermission(permissionCodes) { + const userStore = useUserStore() + return permissionCodes.some(code => userStore.hasPermission(code)) + }, + + // 检查用户是否有所有权限 + hasAllPermissions(permissionCodes) { + const userStore = useUserStore() + return permissionCodes.every(code => userStore.hasPermission(code)) + }, + + // 检查用户是否有任意一个角色 + hasAnyRole(roleNames) { + const userStore = useUserStore() + return roleNames.some(role => userStore.hasRole(role)) + }, + + // 检查用户是否有所有角色 + hasAllRoles(roleNames) { + const userStore = useUserStore() + return roleNames.every(role => userStore.hasRole(role)) + } +} + +// 权限指令 +export const permissionDirective = { + mounted(el, binding) { + const { value } = binding + const userStore = useUserStore() + + if (value) { + let hasPermission = false + + if (typeof value === 'string') { + // 单个权限检查 + hasPermission = userStore.hasPermission(value) + } else if (Array.isArray(value)) { + // 多个权限检查 + if (value.length === 0) { + hasPermission = true + } else { + hasPermission = value.some(permission => userStore.hasPermission(permission)) + } + } else if (typeof value === 'object') { + // 复杂权限检查 + const { permissions, roles, logic = 'or' } = value + + if (logic === 'and') { + hasPermission = (!permissions || userStore.permissions.every(p => permissions.includes(p))) && + (!roles || userStore.roles.every(r => roles.includes(r))) + } else { + hasPermission = (!permissions || permissions.some(p => userStore.hasPermission(p))) || + (!roles || roles.some(r => userStore.hasRole(r))) + } + } + + if (!hasPermission) { + el.style.display = 'none' + } + } + } +} + +// 角色指令 +export const roleDirective = { + mounted(el, binding) { + const { value } = binding + const userStore = useUserStore() + + if (value) { + let hasRole = false + + if (typeof value === 'string') { + hasRole = userStore.hasRole(value) + } else if (Array.isArray(value)) { + hasRole = value.some(role => userStore.hasRole(role)) + } + + if (!hasRole) { + el.style.display = 'none' + } + } + } +} \ No newline at end of file diff --git a/frontend/vite-frontend/src/view/ArticleManagement.vue b/frontend/vite-frontend/src/view/ArticleManagement.vue new file mode 100644 index 0000000000000000000000000000000000000000..c9a92aa1e48f410a9d8d5535f22569b0d8803c89 --- /dev/null +++ b/frontend/vite-frontend/src/view/ArticleManagement.vue @@ -0,0 +1,335 @@ + + + + + \ No newline at end of file diff --git a/frontend/vite-frontend/src/view/Dashboard.vue b/frontend/vite-frontend/src/view/Dashboard.vue new file mode 100644 index 0000000000000000000000000000000000000000..c995c16dc8efe2a5a69ac1bafdbd47d8ada343a3 --- /dev/null +++ b/frontend/vite-frontend/src/view/Dashboard.vue @@ -0,0 +1,769 @@ + + + + + \ No newline at end of file diff --git a/frontend/vite-frontend/src/view/FileManagement.vue b/frontend/vite-frontend/src/view/FileManagement.vue new file mode 100644 index 0000000000000000000000000000000000000000..73b8d1c3afd88d075e28d37007fa91505716fd0b --- /dev/null +++ b/frontend/vite-frontend/src/view/FileManagement.vue @@ -0,0 +1,1402 @@ + + + + + \ No newline at end of file diff --git a/frontend/vite-frontend/src/view/LogManagement.vue b/frontend/vite-frontend/src/view/LogManagement.vue new file mode 100644 index 0000000000000000000000000000000000000000..5afb87fe56fbaaf9f20d820ff512991bbe4a44f8 --- /dev/null +++ b/frontend/vite-frontend/src/view/LogManagement.vue @@ -0,0 +1,1171 @@ + + + + + \ No newline at end of file diff --git a/frontend/vite-frontend/src/view/Login.vue b/frontend/vite-frontend/src/view/Login.vue new file mode 100644 index 0000000000000000000000000000000000000000..a10bee2229281ba684a3e1c91079fde8091bc80d --- /dev/null +++ b/frontend/vite-frontend/src/view/Login.vue @@ -0,0 +1,405 @@ + + + + + \ No newline at end of file diff --git a/frontend/vite-frontend/src/view/PermissionControl.vue b/frontend/vite-frontend/src/view/PermissionControl.vue new file mode 100644 index 0000000000000000000000000000000000000000..e5ad94d86f1e01f2716ad481d3c2da1111e61c94 --- /dev/null +++ b/frontend/vite-frontend/src/view/PermissionControl.vue @@ -0,0 +1,1844 @@ + + + + + \ No newline at end of file diff --git a/frontend/vite-frontend/src/view/Register.vue b/frontend/vite-frontend/src/view/Register.vue new file mode 100644 index 0000000000000000000000000000000000000000..22cdd4f57b623df138d6847377672466f29c959d --- /dev/null +++ b/frontend/vite-frontend/src/view/Register.vue @@ -0,0 +1,431 @@ + + + + + \ No newline at end of file diff --git a/frontend/vite-frontend/src/view/RoleManagement.vue b/frontend/vite-frontend/src/view/RoleManagement.vue new file mode 100644 index 0000000000000000000000000000000000000000..e48dcf1ea0c7d604157f1f77b481580e12bca13e --- /dev/null +++ b/frontend/vite-frontend/src/view/RoleManagement.vue @@ -0,0 +1,519 @@ + + + + + \ No newline at end of file diff --git a/frontend/vite-frontend/src/view/SystemSettings.vue b/frontend/vite-frontend/src/view/SystemSettings.vue new file mode 100644 index 0000000000000000000000000000000000000000..01a60dd9470c0438b5cc88ba78914a8511e20adf --- /dev/null +++ b/frontend/vite-frontend/src/view/SystemSettings.vue @@ -0,0 +1,1452 @@ + + + + + \ No newline at end of file diff --git a/frontend/vite-frontend/src/view/UserList.vue b/frontend/vite-frontend/src/view/UserList.vue new file mode 100644 index 0000000000000000000000000000000000000000..602bfa86b7271a98751187237ba3b9b0ad2c75fe --- /dev/null +++ b/frontend/vite-frontend/src/view/UserList.vue @@ -0,0 +1,1534 @@ + + + + + \ No newline at end of file diff --git a/frontend/vite-frontend/src/view/UserManagement.vue b/frontend/vite-frontend/src/view/UserManagement.vue new file mode 100644 index 0000000000000000000000000000000000000000..bf2330a9aa58221a3ae19aa0848b723b05a53cb7 --- /dev/null +++ b/frontend/vite-frontend/src/view/UserManagement.vue @@ -0,0 +1,196 @@ + + + + + \ No newline at end of file diff --git a/frontend/vite-frontend/vite.config.js b/frontend/vite-frontend/vite.config.js new file mode 100644 index 0000000000000000000000000000000000000000..f166c93ea977e34982ee77cf601d6800914a3d0e --- /dev/null +++ b/frontend/vite-frontend/vite.config.js @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import path from 'path' + +// https://vite.dev/config/ +export default defineConfig({ + resolve:{ + alias:{ + '@':path.resolve(__dirname,'src') + } + }, + plugins: [vue()], +}) diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000000000000000000000000000000000000..aa56b3a3f405cb529cef6ad86334963d19ee7bec --- /dev/null +++ b/nginx.conf @@ -0,0 +1,36 @@ +server { + listen 80; + server_name www.mikuslittlenest.com; # 域名 + + # 处理 uniapp 前端(用户端) + location / { + root /usr/share/nginx/html/uniapp; + index index.html; + try_files $uri /uniapp/index.html; # 使用 index.html 来处理单页应用 + } + + # 处理 Vite 前端(管理端) + location /vite/ { + root /usr/share/nginx/html/vite; + index index.html; + try_files $uri /vite/index.html; # 使用 index.html 来处理单页应用 + } + + # 后端 API 反向代理 + location /api/ { + proxy_pass http://backend:5101/; # 反向代理到后端服务 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Flask API 反向代理 + location /flaskapi/ { + proxy_pass http://flaskapi:5000/; # 反向代理到 Flask API 服务 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} \ No newline at end of file