From 8430ba0452a5158b93fba8d8520fba05a79b06ab Mon Sep 17 00:00:00 2001 From: yanpeng200212 <32453@DESKTOP-HTJ2T0B> Date: Thu, 8 Oct 2026 10:43:59 +0800 Subject: [PATCH] first commit --- .gitignore | 28 + README.md | 169 ++ TempleRun/GameForm.cs | 132 ++ TempleRun/GameModel.cs | 202 ++ TempleRun/GameRenderer.cs | 902 ++++++++ TempleRun/GameWorld.cs | 605 +++++ TempleRun/Program.cs | 14 + TempleRun/TempleRun.csproj | 18 + TempleRun/_shot/01_ready.png | Bin 0 -> 108842 bytes TempleRun/_shot/02_playing.png | Bin 0 -> 109240 bytes TempleRun/_shot/03_playing2.png | Bin 0 -> 116525 bytes TempleRun/_smoke.ps1 | 53 + TempleRun/app.manifest | 20 + .../Attendance/AttendanceController.cs | 103 + .../Controllers/Auth/AuthController.cs | 109 + .../Controllers/Base/BusinessControllers.cs | 153 ++ .../Notice/NotificationControllers.cs | 191 ++ .../Controllers/Prod/HbjyController.cs | 41 + .../Controllers/Prod/LsjlController.cs | 58 + .../Controllers/Prod/XdjyController.cs | 41 + .../Controllers/SysDepartmentController.cs | 55 + .../Controllers/SysMenuController.cs | 68 + .../Controllers/SysRoleController.cs | 80 + .../Controllers/SysUserController.cs | 87 + .../F9WebAiTest.Api/F9WebAiTest.Api.csproj | 22 + .../F9WebAiTest.Api/Hubs/NotificationHub.cs | 47 + .../LongToStringJsonConverter.cs | 19 + backend/F9WebAiTest.Api/Program.cs | 112 + .../Properties/launchSettings.json | 14 + .../Services/BusinessChangedEventHandler.cs | 22 + .../Services/NotificationDeliveryService.cs | 10 + .../Services/NotificationPusher.cs | 45 + backend/F9WebAiTest.Api/appsettings.json | 26 + .../F9WebAiTest.Api/f9_web_ai_test_rbac.db | Bin 0 -> 352256 bytes .../F9WebAiTest.Api/life_circle_rbac.db.bak | Bin 0 -> 299008 bytes .../Department_ActiveOrDeactivateCommand.cs | 31 + .../Commands/Department_CreateCommand.cs | 35 + .../Commands/Department_DelCommand.cs | 22 + .../Commands/Department_UpdateCommand.cs | 29 + .../Queries/GetDepartmentAllQuery.cs | 13 + .../Menu_ActiveOrDeactivateCommand.cs | 31 + .../RBAC/Menus/Commands/Menu_CreateCommand.cs | 34 + .../RBAC/Menus/Commands/Menu_DelCommand.cs | 22 + .../RBAC/Menus/Commands/Menu_UpdateCommand.cs | 28 + .../Commands/RolePermission_AssignCommand.cs | 80 + .../Commands/RolePermission_RemoveCommand.cs | 37 + .../Role_ActiveOrDeactivateCommand.cs | 26 + .../RBAC/Roles/Commands/Role_CreateCommand.cs | 27 + .../RBAC/Roles/Commands/Role_DelCommand.cs | 22 + .../RBAC/Roles/Commands/Role_UpdateCommand.cs | 25 + .../SYS/RBAC/Roles/Queries/GetRoleAllQuery.cs | 13 + .../Roles/Queries/GetRoleByIdForEditQuery.cs | 13 + .../GetRolePermissionsByResourceTypeQuery.cs | 14 + .../Users/Commands/UserRole_AssignCommand.cs | 40 + .../Users/Commands/UserRole_RemoveCommand.cs | 35 + .../User_ActiveOrDeactivateCommand.cs | 26 + .../RBAC/Users/Commands/User_CreateCommand.cs | 73 + .../RBAC/Users/Commands/User_DelCommand.cs | 22 + .../Commands/User_ResetPasswordCommand.cs | 32 + .../RBAC/Users/Commands/User_UpdateCommand.cs | 49 + .../RBAC/Users/Queries/GetUserByIdQuery.cs | 13 + .../RBAC/Users/Queries/GetUserByIdsQuery.cs | 13 + .../RBAC/Users/Queries/GetUserPagedQuery.cs | 14 + .../Queries/GetUserRolesByUserIdQuery.cs | 13 + .../Common/ApiResult.cs | 24 + .../Common/Exceptions/F9NotFoundException.cs | 12 + .../Common/Exceptions/F9OperationException.cs | 15 + .../Common/ServiceException.cs | 12 + .../Contracts/LoginUserInfo.cs | 9 + .../DependencyInjection.cs | 33 + .../Dtos/Attendance/AttendanceDtos.cs | 87 + .../Dtos/Auth/AuthDtos.cs | 50 + .../Dtos/Base/BusinessDtos.cs | 86 + .../Dtos/Common/PageRequest.cs | 3 + .../Dtos/Common/PagedResult.cs | 10 + .../Dtos/DepartmentDtos.cs | 33 + .../Dtos/EmployeeDtos.cs | 61 + .../F9WebAiTest.Application/Dtos/MenuDtos.cs | 41 + .../Dtos/Notice/NotificationDtos.cs | 96 + .../Dtos/Prod/HbjyDtos.cs | 86 + .../Dtos/Prod/LsjlDtos.cs | 41 + .../Dtos/Prod/XdjyDtos.cs | 93 + .../F9WebAiTest.Application/Dtos/RoleDtos.cs | 31 + .../SYS/Departments/DepartmentReadDto_View.cs | 5 + .../SYS/RBAC/Menus/MenuReadDto_Menu_Edit.cs | 16 + .../SYS/RBAC/Menus/MenuReadDto_Menu_Table.cs | 23 + .../RBAC/Roles/RolePermissionReadDto_View.cs | 5 + .../Dtos/SYS/RBAC/Roles/RoleReadDto_Cache.cs | 3 + .../Dtos/SYS/RBAC/Roles/RoleReadDto_Edit.cs | 3 + .../Dtos/SYS/RBAC/Roles/RoleReadDto_View.cs | 3 + .../Dtos/SYS/RBAC/Users/UserReadDto_Edit.cs | 4 + .../Dtos/SYS/RBAC/Users/UserReadDto_Login.cs | 3 + .../Dtos/SYS/RBAC/Users/UserReadDto_View.cs | 4 + .../SYS/RBAC/Users/UserRoleReadDto_Cache.cs | 3 + .../F9WebAiTest.Application.csproj | 22 + .../LoginUsers/ILoginUserService.cs | 10 + .../Departments/IDepartmentReadRepository.cs | 10 + .../SYS/RBAC/Menus/IMenuReadRepository.cs | 14 + .../SYS/RBAC/Roles/IRoleReadRepository.cs | 21 + .../SYS/RBAC/Users/IUserReadRepository.cs | 30 + .../Interfaces/Security/IPasswordHasher.cs | 10 + .../SnowflakeID/ISnowflakeIdGenerator.cs | 7 + .../Interfaces/UOW/IUnitOfWork_F9.cs | 8 + .../Services/Attendance/AttendanceService.cs | 573 +++++ .../Services/Attendance/IAttendanceService.cs | 26 + .../Services/Auth/AuthService.cs | 300 +++ .../Services/Auth/IAuthService.cs | 16 + .../Services/Auth/IJwtService.cs | 11 + .../Services/Auth/JwtService.cs | 102 + .../Services/Base/BusinessServices.cs | 328 +++ .../Services/BusinessChangedEvent.cs | 14 + .../Services/Notice/INotificationPusher.cs | 26 + .../Services/Notice/NotificationMediatR.cs | 276 +++ .../Services/Notice/NotificationService.cs | 420 ++++ .../Services/Prod/HbjyService.cs | 272 +++ .../Services/Prod/LsjlService.cs | 129 ++ .../Services/Prod/XdjyService.cs | 344 +++ .../F9WebAiTest.Domain/Common/BaseEntity.cs | 13 + backend/F9WebAiTest.Domain/Common/IEntity.cs | 10 + .../F9WebAiTest.Domain/Common/IRepository.cs | 23 + .../Entities/AggregateRoot.cs | 21 + .../Entities/Attendance/AttendanceRecord.cs | 18 + .../Entities/Attendance/LeaveApprover.cs | 18 + .../Entities/Attendance/LeaveRequest.cs | 21 + .../Entities/Base/CraftZhuangKou.cs | 20 + .../Entities/Base/JizuChetai.cs | 14 + .../Entities/Base/ShengsiDingjizhibiao.cs | 20 + .../Entities/Base/ShengsiGuige.cs | 30 + .../Entities/BusinessEntity.cs | 22 + .../F9WebAiTest.Domain/Entities/Department.cs | 17 + .../F9WebAiTest.Domain/Entities/Employee.cs | 22 + .../Entities/EmployeeRole.cs | 13 + backend/F9WebAiTest.Domain/Entities/Entity.cs | 25 + .../Entities/IDomainEvent.cs | 15 + backend/F9WebAiTest.Domain/Entities/Menu.cs | 19 + .../Entities/Notice/NotificationEnums.cs | 64 + .../Entities/Notice/NotificationMessage.cs | 27 + .../Entities/Notice/NotificationReceiver.cs | 20 + .../Entities/Notice/NotificationType.cs | 21 + .../Entities/Notice/TodoItem.cs | 17 + .../Notice/UserNotificationSubscription.cs | 16 + .../Entities/OperationLog.cs | 16 + .../Entities/Prod/HbjyLsjl.cs | 18 + .../Entities/Prod/HbjyRecord.cs | 40 + .../Entities/Prod/LsjlRecord.cs | 25 + .../Entities/Prod/XdjyLsjl.cs | 23 + .../Entities/Prod/XdjyRecord.cs | 24 + .../Entities/RefreshToken.cs | 15 + backend/F9WebAiTest.Domain/Entities/Role.cs | 16 + .../F9WebAiTest.Domain/Entities/RoleMenu.cs | 13 + .../Entities/SYS/Departments/Department.cs | 79 + .../SYS/Departments/DepartmentUser.cs | 27 + .../Entities/SYS/RBAC/Menu.cs | 95 + .../Entities/SYS/RBAC/Permission.cs | 47 + .../Entities/SYS/RBAC/Role.cs | 65 + .../Entities/SYS/RBAC/RolePermission.cs | 24 + .../Entities/SYS/RBAC/User.cs | 98 + .../Entities/SYS/RBAC/UserRole.cs | 27 + .../Departments/Department_DutyType_Enum.cs | 22 + .../Departments/Department_ManageType_Enum.cs | 12 + .../Enums/SYS/RBAC/MenuType_Enum.cs | 9 + .../Enums/SYS/RBAC/Permission_Type_Enum.cs | 11 + .../Enums/SYS/RBAC/Resource_Type_Enum.cs | 13 + .../Department_ChangedDomainEvent.cs | 5 + .../SYS/RBAC/Menu_ChangedDomainEvent.cs | 5 + .../RBAC/RoleDepartment_ChangedDomainEvent.cs | 5 + .../SYS/RBAC/RoleMenu_ChangedDomainEvent.cs | 5 + .../Events/SYS/RBAC/UserCreatedDomainEvent.cs | 8 + .../Events/SYS/RBAC/UserLoginDomainEvent.cs | 10 + .../SYS/RBAC/UserRole_ChangedDomainEvent.cs | 5 + .../F9WebAiTest.Domain.csproj | 15 + .../Interfaces/IDInterface.cs | 6 + .../Departments/IDepartmentUserRepository.cs | 11 + .../Departments/IDepartmentWriteRepository.cs | 15 + .../SYS/RBAC/Menus/IMenuWriteRepository.cs | 13 + .../Permissions/IPermissionWriteRepository.cs | 13 + .../Roles/IRolePermissionWriteRepository.cs | 11 + .../SYS/RBAC/Roles/IRoleWriteRepository.cs | 13 + .../RBAC/Users/IUserRoleWriteRepository.cs | 14 + .../SYS/RBAC/Users/IUserWriteRepository.cs | 18 + .../Data/BusinessSeedData.cs | 501 +++++ .../Data/DbSeeder.cs | 509 +++++ .../DependencyInjection.cs | 83 + .../F9WebAiTest.Infrastructure.csproj | 24 + .../FreeSqls/Persistence/UnitOfWork.cs | 46 + .../FreeSqls/Repositories/F9RepositoryBase.cs | 15 + .../Departments/DepartmentReadRepository.cs | 17 + .../Departments/DepartmentUserRepository.cs | 21 + .../Departments/DepartmentWriteRepository.cs | 38 + .../SYS/RBAC/Menus/MenuReadRepository.cs | 61 + .../SYS/RBAC/Menus/MenuWriteRepository.cs | 38 + .../Permissions/PermissionWriteRepository.cs | 27 + .../Roles/RolePermissionWriteRepository.cs | 22 + .../SYS/RBAC/Roles/RoleReadRepository.cs | 66 + .../SYS/RBAC/Roles/RoleWriteRepository.cs | 38 + .../SYS/RBAC/Users/UserReadRepository.cs | 96 + .../SYS/RBAC/Users/UserRoleWriteRepository.cs | 21 + .../SYS/RBAC/Users/UserWriteRepository.cs | 51 + .../Security/PasswordHasher.cs | 27 + .../SnowflakeID/SnowflakeIdGenerator.cs | 59 + .../Repositories/Repository.cs | 88 + backend/F9WebAiTest.sln | 36 + backend/Properties/launchSettings.json | 14 + client/cert.crt | 18 + client/index.html | 12 + client/package-lock.json | 1943 +++++++++++++++++ client/package.json | 22 + client/src/App.vue | 141 ++ client/src/api.js | 31 + client/src/main.js | 6 + client/src/router.js | 34 + client/src/style.css | 179 ++ client/src/utils.js | 29 + client/src/views/ChatDetail.vue | 489 +++++ client/src/views/ChatList.vue | 233 ++ client/src/views/Housekeeping.vue | 176 ++ client/src/views/LiveHost.vue | 340 +++ client/src/views/LiveList.vue | 177 ++ client/src/views/LiveWatch.vue | 259 +++ client/src/views/Login.vue | 175 ++ client/src/views/Notices.vue | 133 ++ client/src/views/Posts.vue | 420 ++++ client/src/views/Profile.vue | 196 ++ client/src/views/Secondhand.vue | 190 ++ client/vite.config.js | 27 + server/db.js | 89 + server/index.js | 55 + server/life_circle.db | Bin 0 -> 4096 bytes server/middleware.js | 28 + server/package-lock.json | 1411 ++++++++++++ server/package.json | 18 + server/routes/auth.js | 75 + server/routes/housekeeping.js | 38 + server/routes/notices.js | 28 + server/routes/posts.js | 90 + server/routes/secondhand.js | 50 + start.bat | 21 + web/index.html | 12 + web/nswag.json | 72 + web/package-lock.json | 1862 ++++++++++++++++ web/package.json | 25 + web/src/App.vue | 3 + web/src/api/client.ts | 1873 ++++++++++++++++ web/src/directives/permission.js | 24 + web/src/layout/EmptyLayout.vue | 3 + web/src/layout/index.vue | 299 +++ web/src/main.js | 57 + web/src/router/index.js | 169 ++ web/src/store/user.js | 43 + web/src/styles/index.css | 20 + web/src/styles/theme.css | 33 + web/src/utils/dept.js | 14 + web/src/utils/notification.js | 99 + web/src/utils/request.js | 112 + web/src/views/attendance/approve/index.vue | 154 ++ web/src/views/attendance/leave/index.vue | 141 ++ web/src/views/attendance/my/index.vue | 158 ++ web/src/views/attendance/punch/index.vue | 207 ++ web/src/views/attendance/records/index.vue | 131 ++ web/src/views/base/dingjizhibiao/index.vue | 165 ++ web/src/views/base/guige/index.vue | 177 ++ web/src/views/base/jizuchetai/index.vue | 135 ++ web/src/views/base/zhuangkou/index.vue | 164 ++ web/src/views/dashboard/index.vue | 125 ++ web/src/views/login/index.vue | 122 ++ web/src/views/notification/messages/index.vue | 182 ++ .../views/notification/subscribe/index.vue | 93 + web/src/views/notification/type/index.vue | 183 ++ web/src/views/prod/hbjy/index.vue | 300 +++ web/src/views/prod/lsjl/index.vue | 283 +++ web/src/views/prod/xdjy/index.vue | 374 ++++ web/src/views/sys/department/index.vue | 152 ++ web/src/views/sys/menu/index.vue | 163 ++ web/src/views/sys/role/index.vue | 318 +++ web/src/views/sys/user/index.vue | 236 ++ web/src/views/system/department/index.vue | 175 ++ web/src/views/system/employee/index.vue | 284 +++ web/src/views/system/empty.vue | 3 + web/src/views/system/log/index.vue | 14 + web/src/views/system/menu/index.vue | 187 ++ web/src/views/system/role/index.vue | 197 ++ web/vite.config.js | 21 + web/消息推送系统软件设计说明书.docx | Bin 0 -> 20151 bytes 消息推送系统_迁移指南.docx | Bin 0 -> 54324 bytes 消息推送系统_迁移指南.md | 644 ++++++ 消息推送系统与RBAC审批设计说明书.docx | Bin 0 -> 36389 bytes 消息推送系统软件设计说明书.docx | Bin 0 -> 63345 bytes 消息推送系统软件设计说明书.md | 817 +++++++ 288 files changed, 29595 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 TempleRun/GameForm.cs create mode 100644 TempleRun/GameModel.cs create mode 100644 TempleRun/GameRenderer.cs create mode 100644 TempleRun/GameWorld.cs create mode 100644 TempleRun/Program.cs create mode 100644 TempleRun/TempleRun.csproj create mode 100644 TempleRun/_shot/01_ready.png create mode 100644 TempleRun/_shot/02_playing.png create mode 100644 TempleRun/_shot/03_playing2.png create mode 100644 TempleRun/_smoke.ps1 create mode 100644 TempleRun/app.manifest create mode 100644 backend/F9WebAiTest.Api/Controllers/Attendance/AttendanceController.cs create mode 100644 backend/F9WebAiTest.Api/Controllers/Auth/AuthController.cs create mode 100644 backend/F9WebAiTest.Api/Controllers/Base/BusinessControllers.cs create mode 100644 backend/F9WebAiTest.Api/Controllers/Notice/NotificationControllers.cs create mode 100644 backend/F9WebAiTest.Api/Controllers/Prod/HbjyController.cs create mode 100644 backend/F9WebAiTest.Api/Controllers/Prod/LsjlController.cs create mode 100644 backend/F9WebAiTest.Api/Controllers/Prod/XdjyController.cs create mode 100644 backend/F9WebAiTest.Api/Controllers/SysDepartmentController.cs create mode 100644 backend/F9WebAiTest.Api/Controllers/SysMenuController.cs create mode 100644 backend/F9WebAiTest.Api/Controllers/SysRoleController.cs create mode 100644 backend/F9WebAiTest.Api/Controllers/SysUserController.cs create mode 100644 backend/F9WebAiTest.Api/F9WebAiTest.Api.csproj create mode 100644 backend/F9WebAiTest.Api/Hubs/NotificationHub.cs create mode 100644 backend/F9WebAiTest.Api/JsonConverters/LongToStringJsonConverter.cs create mode 100644 backend/F9WebAiTest.Api/Program.cs create mode 100644 backend/F9WebAiTest.Api/Properties/launchSettings.json create mode 100644 backend/F9WebAiTest.Api/Services/BusinessChangedEventHandler.cs create mode 100644 backend/F9WebAiTest.Api/Services/NotificationDeliveryService.cs create mode 100644 backend/F9WebAiTest.Api/Services/NotificationPusher.cs create mode 100644 backend/F9WebAiTest.Api/appsettings.json create mode 100644 backend/F9WebAiTest.Api/f9_web_ai_test_rbac.db create mode 100644 backend/F9WebAiTest.Api/life_circle_rbac.db.bak create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/Departments/Commands/Department_ActiveOrDeactivateCommand.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/Departments/Commands/Department_CreateCommand.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/Departments/Commands/Department_DelCommand.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/Departments/Commands/Department_UpdateCommand.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/Departments/Queries/GetDepartmentAllQuery.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Menus/Commands/Menu_ActiveOrDeactivateCommand.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Menus/Commands/Menu_CreateCommand.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Menus/Commands/Menu_DelCommand.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Menus/Commands/Menu_UpdateCommand.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Commands/RolePermission_AssignCommand.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Commands/RolePermission_RemoveCommand.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Commands/Role_ActiveOrDeactivateCommand.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Commands/Role_CreateCommand.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Commands/Role_DelCommand.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Commands/Role_UpdateCommand.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Queries/GetRoleAllQuery.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Queries/GetRoleByIdForEditQuery.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Queries/GetRolePermissionsByResourceTypeQuery.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/UserRole_AssignCommand.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/UserRole_RemoveCommand.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/User_ActiveOrDeactivateCommand.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/User_CreateCommand.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/User_DelCommand.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/User_ResetPasswordCommand.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/User_UpdateCommand.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Queries/GetUserByIdQuery.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Queries/GetUserByIdsQuery.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Queries/GetUserPagedQuery.cs create mode 100644 backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Queries/GetUserRolesByUserIdQuery.cs create mode 100644 backend/F9WebAiTest.Application/Common/ApiResult.cs create mode 100644 backend/F9WebAiTest.Application/Common/Exceptions/F9NotFoundException.cs create mode 100644 backend/F9WebAiTest.Application/Common/Exceptions/F9OperationException.cs create mode 100644 backend/F9WebAiTest.Application/Common/ServiceException.cs create mode 100644 backend/F9WebAiTest.Application/Contracts/LoginUserInfo.cs create mode 100644 backend/F9WebAiTest.Application/DependencyInjection.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/Attendance/AttendanceDtos.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/Auth/AuthDtos.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/Base/BusinessDtos.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/Common/PageRequest.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/Common/PagedResult.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/DepartmentDtos.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/EmployeeDtos.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/MenuDtos.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/Notice/NotificationDtos.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/Prod/HbjyDtos.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/Prod/LsjlDtos.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/Prod/XdjyDtos.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/RoleDtos.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/SYS/Departments/DepartmentReadDto_View.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Menus/MenuReadDto_Menu_Edit.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Menus/MenuReadDto_Menu_Table.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Roles/RolePermissionReadDto_View.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Roles/RoleReadDto_Cache.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Roles/RoleReadDto_Edit.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Roles/RoleReadDto_View.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Users/UserReadDto_Edit.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Users/UserReadDto_Login.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Users/UserReadDto_View.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Users/UserRoleReadDto_Cache.cs create mode 100644 backend/F9WebAiTest.Application/F9WebAiTest.Application.csproj create mode 100644 backend/F9WebAiTest.Application/Interfaces/LoginUsers/ILoginUserService.cs create mode 100644 backend/F9WebAiTest.Application/Interfaces/Repositories/SYS/Departments/IDepartmentReadRepository.cs create mode 100644 backend/F9WebAiTest.Application/Interfaces/Repositories/SYS/RBAC/Menus/IMenuReadRepository.cs create mode 100644 backend/F9WebAiTest.Application/Interfaces/Repositories/SYS/RBAC/Roles/IRoleReadRepository.cs create mode 100644 backend/F9WebAiTest.Application/Interfaces/Repositories/SYS/RBAC/Users/IUserReadRepository.cs create mode 100644 backend/F9WebAiTest.Application/Interfaces/Security/IPasswordHasher.cs create mode 100644 backend/F9WebAiTest.Application/Interfaces/SnowflakeID/ISnowflakeIdGenerator.cs create mode 100644 backend/F9WebAiTest.Application/Interfaces/UOW/IUnitOfWork_F9.cs create mode 100644 backend/F9WebAiTest.Application/Services/Attendance/AttendanceService.cs create mode 100644 backend/F9WebAiTest.Application/Services/Attendance/IAttendanceService.cs create mode 100644 backend/F9WebAiTest.Application/Services/Auth/AuthService.cs create mode 100644 backend/F9WebAiTest.Application/Services/Auth/IAuthService.cs create mode 100644 backend/F9WebAiTest.Application/Services/Auth/IJwtService.cs create mode 100644 backend/F9WebAiTest.Application/Services/Auth/JwtService.cs create mode 100644 backend/F9WebAiTest.Application/Services/Base/BusinessServices.cs create mode 100644 backend/F9WebAiTest.Application/Services/BusinessChangedEvent.cs create mode 100644 backend/F9WebAiTest.Application/Services/Notice/INotificationPusher.cs create mode 100644 backend/F9WebAiTest.Application/Services/Notice/NotificationMediatR.cs create mode 100644 backend/F9WebAiTest.Application/Services/Notice/NotificationService.cs create mode 100644 backend/F9WebAiTest.Application/Services/Prod/HbjyService.cs create mode 100644 backend/F9WebAiTest.Application/Services/Prod/LsjlService.cs create mode 100644 backend/F9WebAiTest.Application/Services/Prod/XdjyService.cs create mode 100644 backend/F9WebAiTest.Domain/Common/BaseEntity.cs create mode 100644 backend/F9WebAiTest.Domain/Common/IEntity.cs create mode 100644 backend/F9WebAiTest.Domain/Common/IRepository.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/AggregateRoot.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Attendance/AttendanceRecord.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Attendance/LeaveApprover.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Attendance/LeaveRequest.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Base/CraftZhuangKou.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Base/JizuChetai.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Base/ShengsiDingjizhibiao.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Base/ShengsiGuige.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/BusinessEntity.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Department.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Employee.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/EmployeeRole.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Entity.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/IDomainEvent.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Menu.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Notice/NotificationEnums.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Notice/NotificationMessage.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Notice/NotificationReceiver.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Notice/NotificationType.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Notice/TodoItem.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Notice/UserNotificationSubscription.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/OperationLog.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Prod/HbjyLsjl.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Prod/HbjyRecord.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Prod/LsjlRecord.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Prod/XdjyLsjl.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Prod/XdjyRecord.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/RefreshToken.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Role.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/RoleMenu.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/SYS/Departments/Department.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/SYS/Departments/DepartmentUser.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/SYS/RBAC/Menu.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/SYS/RBAC/Permission.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/SYS/RBAC/Role.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/SYS/RBAC/RolePermission.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/SYS/RBAC/User.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/SYS/RBAC/UserRole.cs create mode 100644 backend/F9WebAiTest.Domain/Enums/SYS/Departments/Department_DutyType_Enum.cs create mode 100644 backend/F9WebAiTest.Domain/Enums/SYS/Departments/Department_ManageType_Enum.cs create mode 100644 backend/F9WebAiTest.Domain/Enums/SYS/RBAC/MenuType_Enum.cs create mode 100644 backend/F9WebAiTest.Domain/Enums/SYS/RBAC/Permission_Type_Enum.cs create mode 100644 backend/F9WebAiTest.Domain/Enums/SYS/RBAC/Resource_Type_Enum.cs create mode 100644 backend/F9WebAiTest.Domain/Events/SYS/Departments/Department_ChangedDomainEvent.cs create mode 100644 backend/F9WebAiTest.Domain/Events/SYS/RBAC/Menu_ChangedDomainEvent.cs create mode 100644 backend/F9WebAiTest.Domain/Events/SYS/RBAC/RoleDepartment_ChangedDomainEvent.cs create mode 100644 backend/F9WebAiTest.Domain/Events/SYS/RBAC/RoleMenu_ChangedDomainEvent.cs create mode 100644 backend/F9WebAiTest.Domain/Events/SYS/RBAC/UserCreatedDomainEvent.cs create mode 100644 backend/F9WebAiTest.Domain/Events/SYS/RBAC/UserLoginDomainEvent.cs create mode 100644 backend/F9WebAiTest.Domain/Events/SYS/RBAC/UserRole_ChangedDomainEvent.cs create mode 100644 backend/F9WebAiTest.Domain/F9WebAiTest.Domain.csproj create mode 100644 backend/F9WebAiTest.Domain/Interfaces/IDInterface.cs create mode 100644 backend/F9WebAiTest.Domain/Interfaces/SYS/Departments/IDepartmentUserRepository.cs create mode 100644 backend/F9WebAiTest.Domain/Interfaces/SYS/Departments/IDepartmentWriteRepository.cs create mode 100644 backend/F9WebAiTest.Domain/Interfaces/SYS/RBAC/Menus/IMenuWriteRepository.cs create mode 100644 backend/F9WebAiTest.Domain/Interfaces/SYS/RBAC/Permissions/IPermissionWriteRepository.cs create mode 100644 backend/F9WebAiTest.Domain/Interfaces/SYS/RBAC/Roles/IRolePermissionWriteRepository.cs create mode 100644 backend/F9WebAiTest.Domain/Interfaces/SYS/RBAC/Roles/IRoleWriteRepository.cs create mode 100644 backend/F9WebAiTest.Domain/Interfaces/SYS/RBAC/Users/IUserRoleWriteRepository.cs create mode 100644 backend/F9WebAiTest.Domain/Interfaces/SYS/RBAC/Users/IUserWriteRepository.cs create mode 100644 backend/F9WebAiTest.Infrastructure/Data/BusinessSeedData.cs create mode 100644 backend/F9WebAiTest.Infrastructure/Data/DbSeeder.cs create mode 100644 backend/F9WebAiTest.Infrastructure/DependencyInjection.cs create mode 100644 backend/F9WebAiTest.Infrastructure/F9WebAiTest.Infrastructure.csproj create mode 100644 backend/F9WebAiTest.Infrastructure/FreeSqls/Persistence/UnitOfWork.cs create mode 100644 backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/F9RepositoryBase.cs create mode 100644 backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/Departments/DepartmentReadRepository.cs create mode 100644 backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/Departments/DepartmentUserRepository.cs create mode 100644 backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/Departments/DepartmentWriteRepository.cs create mode 100644 backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Menus/MenuReadRepository.cs create mode 100644 backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Menus/MenuWriteRepository.cs create mode 100644 backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Permissions/PermissionWriteRepository.cs create mode 100644 backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Roles/RolePermissionWriteRepository.cs create mode 100644 backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Roles/RoleReadRepository.cs create mode 100644 backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Roles/RoleWriteRepository.cs create mode 100644 backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Users/UserReadRepository.cs create mode 100644 backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Users/UserRoleWriteRepository.cs create mode 100644 backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Users/UserWriteRepository.cs create mode 100644 backend/F9WebAiTest.Infrastructure/InterfaceImplements/Security/PasswordHasher.cs create mode 100644 backend/F9WebAiTest.Infrastructure/InterfaceImplements/SnowflakeID/SnowflakeIdGenerator.cs create mode 100644 backend/F9WebAiTest.Infrastructure/Repositories/Repository.cs create mode 100644 backend/F9WebAiTest.sln create mode 100644 backend/Properties/launchSettings.json create mode 100644 client/cert.crt create mode 100644 client/index.html create mode 100644 client/package-lock.json create mode 100644 client/package.json create mode 100644 client/src/App.vue create mode 100644 client/src/api.js create mode 100644 client/src/main.js create mode 100644 client/src/router.js create mode 100644 client/src/style.css create mode 100644 client/src/utils.js create mode 100644 client/src/views/ChatDetail.vue create mode 100644 client/src/views/ChatList.vue create mode 100644 client/src/views/Housekeeping.vue create mode 100644 client/src/views/LiveHost.vue create mode 100644 client/src/views/LiveList.vue create mode 100644 client/src/views/LiveWatch.vue create mode 100644 client/src/views/Login.vue create mode 100644 client/src/views/Notices.vue create mode 100644 client/src/views/Posts.vue create mode 100644 client/src/views/Profile.vue create mode 100644 client/src/views/Secondhand.vue create mode 100644 client/vite.config.js create mode 100644 server/db.js create mode 100644 server/index.js create mode 100644 server/life_circle.db create mode 100644 server/middleware.js create mode 100644 server/package-lock.json create mode 100644 server/package.json create mode 100644 server/routes/auth.js create mode 100644 server/routes/housekeeping.js create mode 100644 server/routes/notices.js create mode 100644 server/routes/posts.js create mode 100644 server/routes/secondhand.js create mode 100644 start.bat create mode 100644 web/index.html create mode 100644 web/nswag.json create mode 100644 web/package-lock.json create mode 100644 web/package.json create mode 100644 web/src/App.vue create mode 100644 web/src/api/client.ts create mode 100644 web/src/directives/permission.js create mode 100644 web/src/layout/EmptyLayout.vue create mode 100644 web/src/layout/index.vue create mode 100644 web/src/main.js create mode 100644 web/src/router/index.js create mode 100644 web/src/store/user.js create mode 100644 web/src/styles/index.css create mode 100644 web/src/styles/theme.css create mode 100644 web/src/utils/dept.js create mode 100644 web/src/utils/notification.js create mode 100644 web/src/utils/request.js create mode 100644 web/src/views/attendance/approve/index.vue create mode 100644 web/src/views/attendance/leave/index.vue create mode 100644 web/src/views/attendance/my/index.vue create mode 100644 web/src/views/attendance/punch/index.vue create mode 100644 web/src/views/attendance/records/index.vue create mode 100644 web/src/views/base/dingjizhibiao/index.vue create mode 100644 web/src/views/base/guige/index.vue create mode 100644 web/src/views/base/jizuchetai/index.vue create mode 100644 web/src/views/base/zhuangkou/index.vue create mode 100644 web/src/views/dashboard/index.vue create mode 100644 web/src/views/login/index.vue create mode 100644 web/src/views/notification/messages/index.vue create mode 100644 web/src/views/notification/subscribe/index.vue create mode 100644 web/src/views/notification/type/index.vue create mode 100644 web/src/views/prod/hbjy/index.vue create mode 100644 web/src/views/prod/lsjl/index.vue create mode 100644 web/src/views/prod/xdjy/index.vue create mode 100644 web/src/views/sys/department/index.vue create mode 100644 web/src/views/sys/menu/index.vue create mode 100644 web/src/views/sys/role/index.vue create mode 100644 web/src/views/sys/user/index.vue create mode 100644 web/src/views/system/department/index.vue create mode 100644 web/src/views/system/employee/index.vue create mode 100644 web/src/views/system/empty.vue create mode 100644 web/src/views/system/log/index.vue create mode 100644 web/src/views/system/menu/index.vue create mode 100644 web/src/views/system/role/index.vue create mode 100644 web/vite.config.js create mode 100644 web/消息推送系统软件设计说明书.docx create mode 100644 消息推送系统_迁移指南.docx create mode 100644 消息推送系统_迁移指南.md create mode 100644 消息推送系统与RBAC审批设计说明书.docx create mode 100644 消息推送系统软件设计说明书.docx create mode 100644 消息推送系统软件设计说明书.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f4d799 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# 依赖 +node_modules/ + +# 构建产物 +dist/ +bin/ +obj/ +publish/ + +# 运行时临时文件 / 日志 +*.log +_runlog/ +_tmp_hubtest/ +_rename_backup/ +*.db-shm +*.db-wal + +# 证书私钥 +cert.pem +key.pem + +# IDE / 系统 +.vs/ +.idea/ +.vscode/ +*.user +Thumbs.db +Desktop.ini diff --git a/README.md b/README.md new file mode 100644 index 0000000..f377ebf --- /dev/null +++ b/README.md @@ -0,0 +1,169 @@ +# f9-web-ai-test + +企业级 RBAC 权限管理系统 + +一个基于 .NET 8 分层架构 + Vue 3 + Element Plus 的企业内部管理系统,采用经典的面包屑后台布局,实现完整的 RBAC(基于角色的访问控制)权限体系。 + +## 技术栈 + +- **后端**:.NET 8(分层架构)+ NSwag(OpenAPI 文档) +- **前端**:Vue 3 + Vite + Element Plus + Pinia + Vue Router(绿色主题) +- **数据库**:SQLite(EF Core) +- **认证**:JWT 双令牌(access token + refresh token)+ BCrypt 密码加密 +- **API 客户端**:NSwag 自动生成 TypeScript 客户端(`src/api/client.ts`) + +## 后端分层架构 + +``` +backend/ +├── F9WebAiTest.Domain/ # 实体层 +│ ├── Common/ # 基类 + 仓储接口 IRepository +│ └── Entities/ # 实体(部门/职工/角色/菜单/关联/日志) +├── F9WebAiTest.Infrastructure/ # 仓储实现层 +│ ├── Data/ # DbContext + 种子数据 +│ └── Repositories/ # Repository 通用实现 +├── F9WebAiTest.Application/ # 服务层 +│ ├── Common/ # ApiResult / ServiceException +│ ├── Dtos/ # 数据传输对象 +│ └── Services/ # 业务逻辑(认证/部门/职工/角色/菜单) +└── F9WebAiTest.Api/ # 表示层(Web API 控制器) +``` + +依赖方向:`Api → Application → Domain ← Infrastructure` + +## RBAC 核心功能 + +| 模块 | 说明 | +|------|------| +| 认证登录 | JWT 登录、动态菜单、权限标识下发 | +| 部门管理 | 树形部门结构、负责人、子部门、排序 | +| 职工管理 | 职工增删改查、分页搜索、分配角色、重置密码 | +| 角色管理 | 角色增删改查、菜单权限分配(树形勾选) | +| 菜单管理 | 目录/菜单/按钮三级,动态路由、权限标识 | +| 权限控制 | 动态路由 + 按钮级权限指令 v-permission | +| 考勤打卡 | 上班签到/下班签退,迟到/早退自动判定 | +| 我的考勤 | 个人考勤记录 + 月度统计(出勤/迟到/早退/缺勤/请假) | +| 请假管理 | 请假申请、审批(批准/驳回)、多类型(事假/病假/年假等) | +| 考勤记录 | 管理员按日期/部门/姓名查询全员考勤 | +| 工艺庄口 | 庄口基础数据(年度/本地外地/基地外购/产地等) | +| 生丝规格 | 规格指标(中心范围/野纤/内控/劳动竞赛等上下限) | +| 生丝定级指标 | 等级指标(厂标/国标/个人,偏差/大差/清洁/洁净等) | +| 机组车台 | 车间机组与车台号配置 | + +## 数据模型关系 + +``` +Department (部门) 1 ── N Employee (职工) +Employee (职工) N ── N Role (角色) via EmployeeRole +Role (角色) N ── N Menu (菜单) via RoleMenu +Menu (菜单) 自引用树形(目录/菜单/按钮) +``` + +## 启动方式 + +### 1. 启动后端 + +```bash +cd backend/F9WebAiTest.Api +dotnet run +``` + +后端运行在 `http://localhost:3000`,Swagger 文档 `http://localhost:3000/swagger`。 + +### 2. 启动前端 + +```bash +cd web +npm install +npm run dev +``` + +前端运行在 `http://localhost:5173`,已配置代理 `/api` 转发到后端。 + +### 3. 生成 API 客户端(NSwag) + +后端运行时,用 NSwag 从 OpenAPI 文档自动生成 TypeScript 客户端: + +```bash +# 先安装 NSwag CLI 工具(只需一次) +dotnet tool install --global NSwag.ConsoleCore + +# 生成 TypeScript 客户端到 src/api/client.ts +cd web +npm run gen:api +``` + +生成的 `src/api/client.ts` 包含所有 DTO 类型和 API 方法,类型安全地调用后端接口。 + +### 3. 访问系统 + +打开 `http://localhost:5173` + +**默认账号**:`admin` / `123456`(超级管理员,拥有全部权限) + +## 前端目录结构 + +``` +web/src/ +├── layout/index.vue # 主布局(侧边栏 + 面包屑 + 内容区) +├── router/index.js # 路由 + 动态路由构建 +├── store/user.js # 用户状态(Pinia) +├── directives/permission.js # 权限指令 v-permission +├── utils/request.js # axios 封装(拦截器) +└── views/ + ├── login/ # 登录页 + ├── dashboard/ # 首页 + └── system/ + ├── department/ # 部门管理 + ├── employee/ # 职工管理 + ├── role/ # 角色管理(含菜单分配) + ├── menu/ # 菜单管理 + └── log/ # 操作日志 +``` + +## 权限使用示例 + +```vue + +新增 + + +操作 +``` + +## API 接口 + +- `POST /api/auth/login` - 登录(返回 access token + refresh token) +- `POST /api/auth/refresh` - 刷新令牌(用 refresh token 换新 access token) +- `POST /api/auth/logout` - 退出(撤销 refresh token) +- `GET /api/auth/me` - 当前用户信息 +- `GET /api/auth/menus` - 用户菜单树 +- `GET /api/auth/permissions` - 用户权限标识 +- `PUT /api/auth/password` - 修改密码 +- `PUT /api/auth/profile` - 更新资料 +- `GET /api/department/tree` - 部门树 +- `POST /api/department` - 新增部门 +- `PUT /api/department/:id` - 更新部门 +- `DELETE /api/department/:id` - 删除部门 +- `GET /api/employee` - 职工分页 +- `POST /api/employee` - 新增职工 +- `PUT /api/employee/:id` - 更新职工 +- `DELETE /api/employee/:id` - 删除职工 +- `PUT /api/employee/:id/password` - 重置密码 +- `GET /api/role` - 角色列表 +- `GET /api/role/:id/menus` - 角色菜单 +- `POST /api/role` - 新增角色 +- `PUT /api/role/:id` - 更新角色 +- `DELETE /api/role/:id` - 删除角色 +- `GET /api/menu/tree` - 菜单树 +- `POST /api/menu` - 新增菜单 +- `PUT /api/menu/:id` - 更新菜单 +- `DELETE /api/menu/:id` - 删除菜单 +- `POST /api/attendance/punch` - 打卡(签到/签退) +- `GET /api/attendance/my` - 我的考勤记录 +- `GET /api/attendance/my/summary` - 我的考勤统计 +- `GET /api/attendance` - 全员考勤记录(管理员) +- `POST /api/attendance/leave` - 申请请假 +- `GET /api/attendance/leave/my` - 我的请假 +- `GET /api/attendance/leave` - 全部请假(管理员) +- `PUT /api/attendance/leave/:id/approve` - 审批请假 diff --git a/TempleRun/GameForm.cs b/TempleRun/GameForm.cs new file mode 100644 index 0000000..c8a30b1 --- /dev/null +++ b/TempleRun/GameForm.cs @@ -0,0 +1,132 @@ +using System.Diagnostics; +using System.Drawing.Drawing2D; + +namespace TempleRun; + +/// +/// 主窗体:负责游戏循环(定时器 + 高精度计时)、键盘输入、双缓冲绘制调度。 +/// +public class GameForm : Form +{ + private readonly GameWorld _world = new(); + private readonly System.Windows.Forms.Timer _timer; + private readonly Stopwatch _watch = Stopwatch.StartNew(); + + private double _lastTime; + private double _fpsAccum; + private int _frameCount; + private bool _showFps; + + public GameForm() + { + Text = "神庙逃亡 · Temple Run (WinForms)"; + ClientSize = new Size(1024, 720); + MinimumSize = new Size(760, 540); + StartPosition = FormStartPosition.CenterScreen; + BackColor = Color.FromArgb(12, 12, 20); + DoubleBuffered = true; + ResizeRedraw = true; + AutoScaleMode = AutoScaleMode.None; + SetStyle(ControlStyles.AllPaintingInWmPaint + | ControlStyles.UserPaint + | ControlStyles.OptimizedDoubleBuffer, true); + + try { Icon = BuildIcon(); } catch { /* 图标失败不影响运行 */ } + + _lastTime = _watch.Elapsed.TotalSeconds; + + _timer = new System.Windows.Forms.Timer { Interval = 15 }; // ≈66Hz + _timer.Tick += OnTick; + _timer.Start(); + } + + // ------------------------------------------------------------------ 主循环 + private void OnTick(object sender, EventArgs e) + { + double now = _watch.Elapsed.TotalSeconds; + float dt = (float)(now - _lastTime); + _lastTime = now; + + if (dt <= 0f) return; + if (dt > 0.06f) dt = 0.06f; // 卡顿/拖动窗口后不产生穿模 + + // FPS 统计 + _frameCount++; + _fpsAccum += dt; + if (_fpsAccum >= 0.5) + { + _world.Fps = (float)(_frameCount / _fpsAccum); + _frameCount = 0; + _fpsAccum = 0; + } + + _world.Update(dt); + Invalidate(); + } + + protected override void OnPaint(PaintEventArgs e) + { + GameRenderer.Draw(e.Graphics, ClientSize.Width, ClientSize.Height, _world, _showFps); + } + + // ------------------------------------------------------------------ 输入 + protected override bool ProcessCmdKey(ref Message msg, Keys keyData) + { + Keys key = keyData & Keys.KeyCode; + + if (key == Keys.F1) { _showFps = !_showFps; Invalidate(); return true; } + if (key == Keys.F11) + { + WindowState = WindowState == FormWindowState.Maximized + ? FormWindowState.Normal : FormWindowState.Maximized; + return true; + } + if (key == Keys.F12) { _world.Muted = !_world.Muted; return true; } + + if (_world.HandleKey(key)) return true; + + return base.ProcessCmdKey(ref msg, keyData); + } + + protected override void OnMouseClick(MouseEventArgs e) + { + base.OnMouseClick(e); + // 鼠标左键也能触发"开始/重开",方便触屏设备 + if (e.Button == MouseButtons.Left) + { + if (_world.State == RunState.Ready) _world.Start(); + else if (_world.State == RunState.GameOver && _world.CanRestart) + { + _world.Start(); + } + } + } + + protected override void OnFormClosed(FormClosedEventArgs e) + { + _timer.Stop(); + _timer.Dispose(); + GameRenderer.ResetCache(); + base.OnFormClosed(e); + } + + // ------------------------------------------------------------------ 图标 + private static Icon BuildIcon() + { + using var bmp = new Bitmap(32, 32); + using (var g = Graphics.FromImage(bmp)) + { + g.SmoothingMode = SmoothingMode.AntiAlias; + g.Clear(Color.Transparent); + using (var bg = new SolidBrush(Color.FromArgb(46, 34, 28))) + g.FillEllipse(bg, 0, 0, 31, 31); + using (var gold = new SolidBrush(Cfg.Gold)) + g.FillEllipse(gold, 7, 7, 18, 18); + using (var dark = new Pen(Cfg.GoldDark, 2.4f)) + g.DrawEllipse(dark, 7, 7, 18, 18); + using (var hi = new SolidBrush(Color.FromArgb(230, 255, 250, 220))) + g.FillEllipse(hi, 11, 10, 5, 4); + } + return Icon.FromHandle(bmp.GetHicon()); + } +} diff --git a/TempleRun/GameModel.cs b/TempleRun/GameModel.cs new file mode 100644 index 0000000..015b011 --- /dev/null +++ b/TempleRun/GameModel.cs @@ -0,0 +1,202 @@ +using System.Drawing.Drawing2D; + +namespace TempleRun; + +/// +/// 全局常量与可调参数。想改手感直接改这里。 +/// 世界坐标约定:X 向右为正,Y 离地向上为正,Z 玩家前方为正。 +/// 玩家永远停在 Z = 0,世界向 Z 负方向滚动。 +/// +public static class Cfg +{ + // ---- 相机(透视投影参数)---- + public const float Focal = 520f; // 焦距:越大视野越窄、远处物体越大 + public const float CamZ = 235f; // 相机位于玩家后方多远 + public const float CamHeight = 142f; // 相机离地高度 + public const float FarZ = 3000f; // 渲染最远距离 + public const float MinDepth = 45f; // 相机空间最小深度,防止除零 + + // ---- 跑道 ---- + public const int LaneCount = 3; + public const float LaneWidth = 112f; // 单条车道宽 + public const float RoadHalfWidth = LaneWidth * 1.5f; + + // ---- 玩家 ---- + public const float PlayerHeight = 62f; // 站立高度 + public const float SlideHeight = 30f; // 滑铲高度 + public const float PlayerHalfDepth = 22f; + public const float JumpSpeed = 430f; + public const float Gravity = 950f; + public const float LaneLerp = 13f; // 换道插值速度 + public const float SlideDuration = 0.55f; + + // ---- 速度与难度 ---- + public const float StartSpeed = 620f; + public const float MaxSpeed = 1900f; + public const float SpeedAccel = 3.4f; // 每秒自然加速 + public const float SpeedPerCoin = 5f; // 每金币额外加速 + public const float SpawnIntervalMin = 0.62f; // 相邻障碍组最小时间间隔(秒) + public const float SpawnIntervalMax = 1.15f; + + // ---- 颜色(丛林日落 + 古神庙)---- + public static readonly Color SkyTop = Color.FromArgb(30, 34, 72); + public static readonly Color SkyMid = Color.FromArgb(122, 78, 96); + public static readonly Color SkyHorizon = Color.FromArgb(240, 168, 96); + public static readonly Color Fog = Color.FromArgb(200, 152, 118); + public static readonly Color GroundNear = Color.FromArgb(44, 36, 28); + public static readonly Color GroundFar = Color.FromArgb(122, 100, 78); + public static readonly Color RoadA = Color.FromArgb(178, 150, 110); + public static readonly Color RoadB = Color.FromArgb(156, 130, 94); + public static readonly Color Stone = Color.FromArgb(120, 112, 104); + public static readonly Color Wood = Color.FromArgb(122, 82, 48); + public static readonly Color Vine = Color.FromArgb(74, 116, 62); + public static readonly Color Gold = Color.FromArgb(255, 202, 62); + public static readonly Color GoldDark = Color.FromArgb(196, 140, 24); + public static readonly Color Player = Color.FromArgb(226, 92, 62); + + public static float LaneX(int lane) => (lane - 1) * LaneWidth; + + public static Color Lerp(Color a, Color b, float t) + { + t = Math.Clamp(t, 0f, 1f); + return Color.FromArgb( + (int)(a.R + (b.R - a.R) * t), + (int)(a.G + (b.G - a.G) * t), + (int)(a.B + (b.B - a.B) * t)); + } +} + +public enum RunState { Ready, Playing, Paused, GameOver } + +public enum ObstacleKind +{ + /// 矮障碍:必须跳跃 + Low, + /// 悬空横梁:必须滑铲 + High, + /// 整道石墙:必须换道 + Full +} + +/// 透视相机:把世界坐标投影到屏幕坐标。 +public static class Cam +{ + public static float CenterX; + public static float HorizonY; + public static int Width; + public static int Height; + + public static void Setup(int w, int h) + { + Width = w; + Height = h; + CenterX = w * 0.5f; + HorizonY = h * 0.30f; + } + + /// + /// 投影一个世界点。 + /// scale 为"每世界单位对应多少像素",用于缩放实体自身尺寸。 + /// + public static void Project(float x, float y, float z, out float sx, out float sy, out float scale) + { + float depth = z + Cfg.CamZ; + if (depth < Cfg.MinDepth) depth = Cfg.MinDepth; + scale = Cfg.Focal / depth; + sx = CenterX + x * scale; + sy = HorizonY + (Cfg.CamHeight - y) * scale; + } + + /// 只投影地面上的点(y = 0)。 + public static void ProjectGround(float x, float z, out float sx, out float sy, out float scale) + => Project(x, 0f, z, out sx, out sy, out scale); + + /// 距离雾化系数:0 = 完全清晰,1 = 完全融入雾色。 + public static float FogAmount(float z) + { + float t = Math.Clamp(z / Cfg.FarZ, 0f, 1f); + return t * t; // 平方衰减,近处保持通透 + } +} + +public class Obstacle +{ + public float Z; + public int Lane; + public ObstacleKind Kind; + public bool Scored; // 是否已计入"躲避成功" + + public float Width => Kind == ObstacleKind.Full ? 96f : 90f; + public float Depth => Kind == ObstacleKind.Full ? 54f : 56f; + public float BottomY => Kind == ObstacleKind.High ? 40f : 0f; + public float TopY => Kind switch + { + ObstacleKind.Low => 46f, // 低于跳跃高度,跳过去 + ObstacleKind.High => 98f, // 悬空横梁,滑铲钻过去 + _ => 108f // 整道石墙,只能换道 + }; + public float CenterX => Cfg.LaneX(Lane); +} + +public class Coin +{ + public float Z; + public int Lane; + public float Y = 36f; + public bool Taken; + public float Phase; + public float CenterX => Cfg.LaneX(Lane); +} + +public class Particle +{ + public float X, Y, Z; + public float Vx, Vy, Vz; + public float Life; + public float MaxLife; + public float Size; + public float GravityScale = 1f; + public Color Color; +} + +/// 路边装饰物:石柱、火把、树、雕像碎块。 +public class SideProp +{ + public float Z; + public int Side; // -1 左,+1 右 + public int Type; // 0=石柱 1=火把 2=枯树 3=碎石 + public float Height; + public float Phase; + + public float CenterX => Side * (Cfg.RoadHalfWidth + 205f + (Type == 3 ? 40f : 0f)); +} + +public class Player +{ + public float X; // 当前横向世界坐标(插值中) + public int TargetLane = 1; // 目标车道 0/1/2 + public float Y; // 离地高度 + public float Vy; + public bool OnGround = true; + public bool Sliding; + public float SlideTimer; + public float RunPhase; // 跑动动画相位 + public bool Alive = true; + public float Tilt; // 换道时的侧倾(视觉) + + public float Height => Sliding ? Cfg.SlideHeight : Cfg.PlayerHeight; + + public void Reset() + { + TargetLane = 1; + X = Cfg.LaneX(1); + Y = 0; + Vy = 0; + OnGround = true; + Sliding = false; + SlideTimer = 0; + RunPhase = 0; + Alive = true; + Tilt = 0; + } +} diff --git a/TempleRun/GameRenderer.cs b/TempleRun/GameRenderer.cs new file mode 100644 index 0000000..0e9997b --- /dev/null +++ b/TempleRun/GameRenderer.cs @@ -0,0 +1,902 @@ +using System.Drawing.Drawing2D; +using System.Drawing.Text; + +namespace TempleRun; + +/// +/// 全部绘制逻辑。使用 GDI+ 手工做透视投影,不依赖任何 3D 库。 +/// +public static class GameRenderer +{ + // ---------------------------------------------------------------- 缓存对象 + private static Font _fHuge, _fTitle, _fBig, _fMid, _fSmall, _fTiny; + private static readonly List<(float Z, int Type, object Obj)> _drawList = new(512); + private const int TypeObstacle = 0; + private const int TypeCoin = 1; + + private static void EnsureFonts() + { + if (_fBig != null) return; + const string face = "Microsoft YaHei UI"; + _fHuge = new Font(face, 46f, FontStyle.Bold, GraphicsUnit.Pixel); + _fTitle = new Font(face, 30f, FontStyle.Bold, GraphicsUnit.Pixel); + _fBig = new Font(face, 26f, FontStyle.Bold, GraphicsUnit.Pixel); + _fMid = new Font(face, 19f, FontStyle.Regular, GraphicsUnit.Pixel); + _fSmall = new Font(face, 15f, FontStyle.Regular, GraphicsUnit.Pixel); + _fTiny = new Font(face, 12f, FontStyle.Regular, GraphicsUnit.Pixel); + } + + public static void ResetCache() { _fBig = null; } + + // ---------------------------------------------------------------- 入口 + public static void Draw(Graphics g, int w, int h, GameWorld world, bool showFps = false) + { + if (w <= 0 || h <= 0) return; + EnsureFonts(); + + Cam.Setup(w, h); + g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit; + + g.Clear(Color.FromArgb(12, 12, 20)); + + // 屏幕震动 + float shakeX = 0f, shakeY = 0f; + if (world.Shake > 0.15f) + { + float s = world.Shake; + shakeX = (float)(Random.Shared.NextDouble() * 2 - 1) * s; + shakeY = (float)(Random.Shared.NextDouble() * 2 - 1) * s * 0.7f; + } + + GraphicsState st = g.Save(); + g.TranslateTransform(shakeX, shakeY); + + g.SmoothingMode = SmoothingMode.AntiAlias; + DrawSky(g, w, h); + DrawDistantScenery(g, w, h); + DrawGround(g, w, h, world); + DrawProps(g, world); + DrawEntities(g, world); + DrawParticles(g, world); + DrawPlayer(g, world); + + g.Restore(st); + + // 撞击红闪 + if (world.HitFlash > 0.01f) + { + using var fb = new SolidBrush(Color.FromArgb((int)(130 * world.HitFlash), 220, 40, 30)); + g.FillRectangle(fb, 0, 0, w, h); + } + + DrawHud(g, w, h, world, showFps); + } + + // ---------------------------------------------------------------- 投影辅助 + private static PointF P(float x, float y, float z) + { + Cam.Project(x, y, z, out float sx, out float sy, out _); + return new PointF(sx, sy); + } + + private static float PX(float x, float y, float z) + { + Cam.Project(x, y, z, out float sx, out _, out _); + return sx; + } + + private static float PY(float x, float y, float z) + { + Cam.Project(x, y, z, out _, out float sy, out _); + return sy; + } + + private static float PS(float x, float y, float z) + { + Cam.Project(x, y, z, out _, out _, out float s); + return s; + } + + // ---------------------------------------------------------------- 天空 + private static void DrawSky(Graphics g, int w, int h) + { + int skyBottom = (int)Cam.HorizonY + 30; + var rect = new Rectangle(0, -30, w, skyBottom + 30); + if (rect.Height < 2) return; + + using var br = new LinearGradientBrush(rect, Color.Black, Color.White, 90f) + { + InterpolationColors = new ColorBlend(3) + { + Colors = new[] { Cfg.SkyTop, Cfg.SkyMid, Cfg.SkyHorizon }, + Positions = new[] { 0f, 0.60f, 1f } + } + }; + g.FillRectangle(br, rect); + + // 太阳 + float sunX = w * 0.74f; + float sunY = Cam.HorizonY - h * 0.115f; + float sunR = Math.Max(16f, h * 0.072f); + + using (var path = new GraphicsPath()) + { + path.AddEllipse(sunX - sunR * 3.4f, sunY - sunR * 3.4f, sunR * 6.8f, sunR * 6.8f); + using var glow = new PathGradientBrush(path) + { + CenterColor = Color.FromArgb(140, 255, 220, 150), + SurroundColors = new[] { Color.FromArgb(0, 255, 200, 120) } + }; + g.FillPath(glow, path); + } + using (var sb = new SolidBrush(Color.FromArgb(255, 255, 240, 200))) + g.FillEllipse(sb, sunX - sunR, sunY - sunR, sunR * 2, sunR * 2); + + // 云条 + using (var cb = new SolidBrush(Color.FromArgb(46, 255, 226, 190))) + { + g.FillEllipse(cb, w * 0.10f, Cam.HorizonY - h * 0.20f, w * 0.16f, h * 0.022f); + g.FillEllipse(cb, w * 0.30f, Cam.HorizonY - h * 0.255f, w * 0.11f, h * 0.017f); + g.FillEllipse(cb, w * 0.62f, Cam.HorizonY - h * 0.16f, w * 0.20f, h * 0.020f); + } + } + + // ---------------------------------------------------------------- 远景(山脊 + 神庙) + private static void DrawDistantScenery(Graphics g, int w, int h) + { + float hy = Cam.HorizonY; + + // 远山脊 + using (var far = new SolidBrush(Cfg.Lerp(Cfg.Fog, Color.FromArgb(72, 62, 104), 0.42f))) + { + using var path = new GraphicsPath(); + path.AddPolygon(new[] + { + new PointF(-w * 0.05f, hy + 2), + new PointF(w * 0.08f, hy - h * 0.075f), + new PointF(w * 0.20f, hy - h * 0.012f), + new PointF(w * 0.33f, hy - h * 0.105f), + new PointF(w * 0.47f, hy - h * 0.020f), + new PointF(w * 0.60f, hy - h * 0.088f), + new PointF(w * 0.74f, hy - h * 0.028f), + new PointF(w * 0.88f, hy - h * 0.070f), + new PointF(w * 1.05f, hy + 2) + }); + g.FillPath(far, path); + } + + // 神庙剪影(左) + DrawTemple(g, w * 0.19f, hy + 4, w * 0.115f, h * 0.155f, + Cfg.Lerp(Cfg.Fog, Color.FromArgb(58, 48, 78), 0.55f), 5); + + // 神庙剪影(右,稍小) + DrawTemple(g, w * 0.83f, hy + 4, w * 0.085f, h * 0.115f, + Cfg.Lerp(Cfg.Fog, Color.FromArgb(66, 56, 86), 0.46f), 4); + + // 地平线雾带 + using var haze = new LinearGradientBrush( + new Rectangle(0, (int)hy - 26, w, 58), + Color.FromArgb(0, Cfg.Fog), Color.FromArgb(190, Cfg.Fog), 90f); + g.FillRectangle(haze, 0, hy - 26, w, 58); + } + + private static void DrawTemple(Graphics g, float cx, float baseY, float w, float h, Color c, int steps) + { + using var br = new SolidBrush(c); + float sh = h / steps; + for (int i = 0; i < steps; i++) + { + float frac = 1f - (float)i / steps * 0.70f; + float ww = w * frac; + float y = baseY - sh * (i + 1); + g.FillRectangle(br, cx - ww * 0.5f, y, ww, sh + 1.2f); + } + // 塔尖 + float topW = w * 0.30f; + float topY = baseY - h; + g.FillPolygon(br, new[] + { + new PointF(cx - topW * 0.5f, topY + 1), + new PointF(cx, topY - sh * 0.85f), + new PointF(cx + topW * 0.5f, topY + 1) + }); + } + + // ---------------------------------------------------------------- 地面与跑道 + private static void DrawGround(Graphics g, int w, int h, GameWorld world) + { + // 整体地面渐变 + int gy = (int)Cam.HorizonY; + var rect = new Rectangle(0, gy - 2, w, h - gy + 4); + if (rect.Height > 1) + { + using var gb = new LinearGradientBrush(rect, Color.White, Color.White, 90f) + { + InterpolationColors = new ColorBlend(4) + { + Colors = new[] { Cfg.GroundFar, Cfg.Lerp(Cfg.GroundFar, Cfg.GroundNear, 0.35f), Cfg.GroundNear, Color.FromArgb(52, 40, 30) }, + Positions = new[] { 0f, 0.18f, 0.62f, 1f } + } + }; + g.FillRectangle(gb, rect); + } + + SmoothingMode old = g.SmoothingMode; + g.SmoothingMode = SmoothingMode.None; // 条纹用无抗锯齿更快 + + const float period = 190f; + const float drawNear = -230f; + + // 跑道两侧的暗色沟渠 + DrawSideBand(g, w, -1); + DrawSideBand(g, w, +1); + + // 跑道条纹(世界坐标固定,随世界滚动) + float offset = world.Distance % (period * 2f); + for (int i = -8; i < 40; i++) + { + float z0 = i * period * 2f - offset; + float z1 = z0 + period; + if (z1 < drawNear) continue; + if (z0 > Cfg.FarZ) break; + + z0 = Math.Max(z0, drawNear); + z1 = Math.Min(z1, Cfg.FarZ); + if (z1 <= z0) continue; + + float fog = Cam.FogAmount(z0); + Color baseColor = ((i % 2) + 2) % 2 == 0 ? Cfg.RoadA : Cfg.RoadB; + Color c = Cfg.Lerp(baseColor, Cfg.Fog, fog * 0.92f); + + float lx0 = PX(-Cfg.RoadHalfWidth, 0, z0); + float rx0 = PX(+Cfg.RoadHalfWidth, 0, z0); + float ly0 = PY(0, 0, z0); + float ly1 = PY(0, 0, z1); + float lx1 = PX(-Cfg.RoadHalfWidth, 0, z1); + float rx1 = PX(+Cfg.RoadHalfWidth, 0, z1); + + using var br = new SolidBrush(c); + g.FillPolygon(br, new[] + { + new PointF(lx0, ly0), new PointF(rx0, ly0), + new PointF(rx1, ly1), new PointF(lx1, ly1) + }); + } + + // 跑道边缘描边 + DrawRoadEdge(g, -1); + DrawRoadEdge(g, +1); + + // 车道分隔虚线 + DrawLaneMark(g, -Cfg.LaneWidth * 0.5f, world, period); + DrawLaneMark(g, +Cfg.LaneWidth * 0.5f, world, period); + + g.SmoothingMode = old; + } + + private static void DrawRoadEdge(Graphics g, int side) + { + float x = side * Cfg.RoadHalfWidth; + var pts = new List(); + for (float z = Cfg.FarZ; z >= -230f; z -= 120f) + pts.Add(new PointF(PX(x, 0, z), PY(0, 0, z))); + + if (pts.Count < 2) return; + using var pen = new Pen(Color.FromArgb(120, 58, 44, 32), 3.2f); + g.DrawLines(pen, pts.ToArray()); + } + + private static void DrawSideBand(Graphics g, int w, int side) + { + float xi = side * Cfg.RoadHalfWidth; + float xo = side * (Cfg.RoadHalfWidth + 170f); + + float yFar = PY(0, 0, Cfg.FarZ); + float yNear = PY(0, 0, -230f); + var poly = new[] + { + new PointF(PX(xi, 0, Cfg.FarZ), yFar), new PointF(PX(xo, 0, Cfg.FarZ), yFar), + new PointF(PX(xo, 0, -230f), yNear), new PointF(PX(xi, 0, -230f), yNear) + }; + using var br = new SolidBrush(Color.FromArgb(96, 34, 26, 20)); + g.FillPolygon(br, poly); + } + + private static void DrawLaneMark(Graphics g, float x, GameWorld world, float period) + { + float offset = world.Distance % (period * 2f); + for (int i = -8; i < 40; i++) + { + float z0 = i * period * 2f - offset; + float z1 = z0 + period * 0.45f; + if (z1 < -230f) continue; + if (z0 > Cfg.FarZ) break; + z0 = Math.Max(z0, -230f); + z1 = Math.Min(z1, Cfg.FarZ); + if (z1 <= z0) continue; + + float fog = Cam.FogAmount(z0); + var c = Cfg.Lerp(Color.FromArgb(168, 146, 108), Cfg.Fog, fog * 0.9f); + using var br = new SolidBrush(Color.FromArgb((byte)(120 * (1 - fog) + 30), c)); + g.FillPolygon(br, new[] + { + new PointF(PX(x - 4, 0, z0), PY(0, 0, z0)), new PointF(PX(x + 4, 0, z0), PY(0, 0, z0)), + new PointF(PX(x + 4, 0, z1), PY(0, 0, z1)), new PointF(PX(x - 4, 0, z1), PY(0, 0, z1)) + }); + } + } + + // ---------------------------------------------------------------- 路边装饰 + private static void DrawProps(Graphics g, GameWorld world) + { + var props = world.Props; + for (int i = 0; i < props.Count; i++) + { + var p = props[i]; + if (p.Z < 70f) continue; // 太近会盖住跑道,直接跳过 + + float fog = Cam.FogAmount(p.Z); + if (fog > 0.96f) continue; + + float wx = p.CenterX; // 世界坐标 + float sx = PX(wx, 0f, p.Z); // ★ 屏幕 x(原来误用了世界坐标) + float top = PY(wx, p.Height, p.Z); + float bottom = PY(wx, 0f, p.Z); + float s = PS(wx, 0f, p.Z); + float wpx = Math.Min(40f * s, 220f); // 限制近处被夸张放大的尺寸 + if (bottom < Cam.HorizonY - 60) continue; + + switch (p.Type) + { + case 0: // 石柱 + { + var body = Cfg.Lerp(Color.FromArgb(96, 86, 76), Cfg.Fog, fog); + using var br = new SolidBrush(body); + g.FillRectangle(br, sx - wpx * 0.5f, top, wpx, bottom - top); + using var cap = new SolidBrush(Cfg.Lerp(Color.FromArgb(124, 110, 96), Cfg.Fog, fog)); + g.FillRectangle(cap, sx - wpx * 0.62f, top - wpx * 0.22f, wpx * 1.24f, wpx * 0.26f); + using var pen = new Pen(Color.FromArgb(80, 34, 28, 22), Math.Max(1f, s * 1.4f)); + g.DrawLine(pen, sx - wpx * 0.18f, top, sx - wpx * 0.18f, bottom); + break; + } + case 1: // 火把 + { + using var br = new SolidBrush(Cfg.Lerp(Color.FromArgb(88, 60, 38), Cfg.Fog, fog)); + g.FillRectangle(br, sx - wpx * 0.16f, top, wpx * 0.32f, bottom - top); + float fr = wpx * 0.52f * (1f + 0.14f * MathF.Sin(world.Clock * 6f + p.Phase)); + using var glow = new SolidBrush(Color.FromArgb((int)(130 * (1 - fog)), 255, 170, 62)); + g.FillEllipse(glow, sx - fr, top - fr * 1.1f, fr * 2, fr * 2); + using var core = new SolidBrush(Color.FromArgb((int)(235 * (1 - fog)), 255, 234, 158)); + g.FillEllipse(core, sx - fr * 0.42f, top - fr * 0.95f, fr * 0.84f, fr * 0.95f); + break; + } + case 2: // 枯树 + { + var wood = Cfg.Lerp(Color.FromArgb(64, 50, 38), Cfg.Fog, fog); + using var br = new SolidBrush(wood); + float trunkW = Math.Max(2f, wpx * 0.20f); + g.FillRectangle(br, sx - trunkW * 0.5f, top, trunkW, bottom - top); + using var pen = new Pen(wood, Math.Max(1.6f, wpx * 0.11f)) + { StartCap = LineCap.Round, EndCap = LineCap.Round }; + float midY = top + (bottom - top) * 0.42f; + g.DrawLine(pen, sx, midY, sx - wpx * 0.85f, midY - (bottom - top) * 0.30f); + g.DrawLine(pen, sx, midY + (bottom - top) * 0.12f, sx + wpx * 0.90f, midY - (bottom - top) * 0.20f); + break; + } + default: // 碎石 + { + using var br = new SolidBrush(Cfg.Lerp(Color.FromArgb(84, 76, 68), Cfg.Fog, fog)); + float hgt = (bottom - top) * 1.1f; + g.FillEllipse(br, sx - wpx * 0.7f, bottom - hgt, wpx * 1.4f, hgt); + break; + } + } + } + } + + // ---------------------------------------------------------------- 实体(障碍 + 金币) + private static void DrawEntities(Graphics g, GameWorld world) + { + _drawList.Clear(); + for (int i = 0; i < world.Obstacles.Count; i++) + { + var o = world.Obstacles[i]; + if (o.Z > Cfg.FarZ + 200f || o.Z < -260f) continue; + _drawList.Add((o.Z, TypeObstacle, o)); + } + for (int i = 0; i < world.Coins.Count; i++) + { + var c = world.Coins[i]; + if (c.Taken || c.Z > Cfg.FarZ + 200f || c.Z < -260f) continue; + _drawList.Add((c.Z, TypeCoin, c)); + } + + _drawList.Sort(static (a, b) => b.Z.CompareTo(a.Z)); // 远 → 近 + + for (int i = 0; i < _drawList.Count; i++) + { + var it = _drawList[i]; + if (it.Type == TypeObstacle) DrawObstacle(g, (Obstacle)it.Obj); + else DrawCoin(g, (Coin)it.Obj); + } + } + + private static void DrawObstacle(Graphics g, Obstacle o) + { + float fog = Cam.FogAmount(o.Z); + if (fog > 0.97f) return; + + float cx = o.CenterX; + float hw = o.Width * 0.5f; + float zn = o.Z - o.Depth * 0.5f; + float zf = o.Z + o.Depth * 0.5f; + float yb = o.BottomY, yt = o.TopY; + + // 8 个顶点 + PointF nlb = P(cx - hw, yb, zn), nrb = P(cx + hw, yb, zn); + PointF nlt = P(cx - hw, yt, zn), nrt = P(cx + hw, yt, zn); + PointF flb = P(cx - hw, yb, zf), frb = P(cx + hw, yb, zf); + PointF flt = P(cx - hw, yt, zf), frt = P(cx + hw, yt, zf); + + // 落地阴影(让障碍"坐"在跑道上) + if (o.BottomY <= 1f) + { + Cam.Project(cx, 0.6f, o.Z, out float shx, out float shy, out float shs); + using var shb = new SolidBrush(Color.FromArgb(78, 0, 0, 0)); + g.FillEllipse(shb, shx - o.Width * 0.62f * shs, shy - 8f * shs, + o.Width * 1.24f * shs, 16f * shs); + } + + Color cFront, cTop, cSide; + switch (o.Kind) + { + case ObstacleKind.Low: + cFront = Color.FromArgb(78, 70, 62); + cTop = Color.FromArgb(120, 108, 94); + cSide = Color.FromArgb(56, 50, 44); + break; + case ObstacleKind.High: + cFront = Color.FromArgb(112, 72, 40); + cTop = Color.FromArgb(150, 106, 58); + cSide = Color.FromArgb(80, 52, 28); + break; + default: + cFront = Color.FromArgb(64, 60, 58); + cTop = Color.FromArgb(104, 96, 90); + cSide = Color.FromArgb(46, 44, 42); + break; + } + + cFront = Cfg.Lerp(cFront, Cfg.Fog, fog); + cTop = Cfg.Lerp(cTop, Cfg.Fog, fog); + cSide = Cfg.Lerp(cSide, Cfg.Fog, fog); + + // 顶面 + using (var br = new SolidBrush(cTop)) + g.FillPolygon(br, new[] { nlt, nrt, frt, flt }); + + // 侧面(朝向画面中心的那一面) + bool showLeft = cx > 0f; + using (var br = new SolidBrush(cSide)) + { + var quad = showLeft + ? new[] { nlb, nlt, flt, flb } + : new[] { nrb, nrt, frt, frb }; + g.FillPolygon(br, quad); + } + + // 近面 + using (var br = new SolidBrush(cFront)) + g.FillPolygon(br, new[] { nlb, nrb, nrt, nlt }); + + // 近面描边 + 细节 + float lw = Math.Max(1f, PS(cx, 0, o.Z) * 1.6f); + using (var pen = new Pen(Color.FromArgb(150, 34, 26, 20), lw)) + g.DrawPolygon(pen, new[] { nlb, nrb, nrt, nlt }); + + float s = PS(cx, 0, o.Z); + if (s > 0.35f) + { + if (o.Kind == ObstacleKind.High) + { + // 横梁上的藤蔓 + using var vine = new Pen(Cfg.Lerp(Cfg.Vine, Cfg.Fog, fog), Math.Max(1.5f, s * 2.4f)) + { StartCap = LineCap.Round, EndCap = LineCap.Round }; + float topY = (nlt.Y + nrt.Y) * 0.5f; + float botY = (nlb.Y + nrb.Y) * 0.5f; + for (int k = 0; k < 3; k++) + { + float fx = nlb.X + (nrb.X - nlb.X) * (0.22f + k * 0.28f); + g.DrawLine(vine, fx, topY + (botY - topY) * 0.30f, fx + s * 6f, botY + s * 10f); + } + } + else if (o.Kind == ObstacleKind.Full) + { + // 石墙刻纹 + using var pen = new Pen(Color.FromArgb(90, 40, 34, 30), Math.Max(1f, s * 1.8f)); + for (int k = 1; k <= 2; k++) + { + float t = k / 3f; + float y = nlb.Y + (nlt.Y - nlb.Y) * t; + g.DrawLine(pen, nlb.X + s * 4f, y, nrb.X - s * 4f, y); + } + } + else + { + // 矮石堆的碎石纹理 + using var br2 = new SolidBrush(Color.FromArgb(70, 46, 38, 30)); + g.FillEllipse(br2, nlb.X + (nrb.X - nlb.X) * 0.16f, nlb.Y - (nlb.Y - nlt.Y) * 0.55f, + (nrb.X - nlb.X) * 0.26f, (nlb.Y - nlt.Y) * 0.30f); + } + } + } + + private static void DrawCoin(Graphics g, Coin c) + { + float fog = Cam.FogAmount(c.Z); + if (fog > 0.96f) return; + + Cam.Project(c.CenterX, c.Y, c.Z, out float sx, out float sy, out float s); + float r = Math.Min(10f * s, 46f); + if (r < 0.6f) return; + + float wobble = MathF.Abs(MathF.Cos(c.Phase)); + float rx = r * (0.30f + 0.70f * wobble); + float ry = r; + + int fade = (int)(255 * (1f - fog * 0.9f)); + Color gold = Cfg.Lerp(Cfg.Gold, Cfg.Fog, fog * 0.7f); + Color goldDark = Cfg.Lerp(Cfg.GoldDark, Cfg.Fog, fog * 0.7f); + + // 光晕 + using (var glow = new SolidBrush(Color.FromArgb(Math.Max(0, fade / 6), gold))) + g.FillEllipse(glow, sx - ry * 1.4f, sy - ry * 1.4f, ry * 2.8f, ry * 2.8f); + + using (var br = new SolidBrush(Color.FromArgb(fade, gold))) + g.FillEllipse(br, sx - rx, sy - ry, rx * 2, ry * 2); + + using (var pen = new Pen(Color.FromArgb(fade, goldDark), Math.Max(1f, r * 0.16f))) + g.DrawEllipse(pen, sx - rx, sy - ry, rx * 2, ry * 2); + + // 高光 + if (r > 3f) + { + using var hl = new SolidBrush(Color.FromArgb((int)(fade * 0.75f), 255, 250, 220)); + g.FillEllipse(hl, sx - rx * 0.42f, sy - ry * 0.52f, rx * 0.46f, ry * 0.40f); + } + } + + // ---------------------------------------------------------------- 粒子 + private static void DrawParticles(Graphics g, GameWorld world) + { + var list = world.Particles; + for (int i = 0; i < list.Count; i++) + { + var p = list[i]; + float t = p.Life / p.MaxLife; + int alpha = (int)(255 * (1f - t * t)); + if (alpha <= 4) continue; + + Cam.Project(p.X, p.Y, p.Z, out float sx, out float sy, out float s); + float r = Math.Max(0.8f, p.Size * s * (1f - t * 0.45f)); + + using var br = new SolidBrush(Color.FromArgb(alpha, p.Color)); + g.FillEllipse(br, sx - r, sy - r, r * 2, r * 2); + } + } + + // ---------------------------------------------------------------- 玩家 + private static void DrawPlayer(Graphics g, GameWorld world) + { + var h = world.Hero; + + float footX = PX(h.X, h.Y, 0f); + float footY = PY(h.X, h.Y, 0f); + float headY = PY(h.X, h.Y + h.Height, 0f); + float s = PS(h.X, 0f, 0f); + + float bodyH = Math.Max(3f, footY - headY); + + // ---- 地面阴影 ---- + float shX = PX(h.X, 0f, 0f); + float shY = PY(h.X, 0f, 0f); + float lift = Math.Clamp(h.Y / 220f, 0f, 1f); + float shW = 58f * s * (1f - lift * 0.55f); + float shH = 17f * s * (1f - lift * 0.50f); + if (shW > 1f) + { + using var sb = new SolidBrush(Color.FromArgb((int)(120 * (1f - lift)), 0, 0, 0)); + g.FillEllipse(sb, shX - shW * 0.5f, shY - shH * 0.5f, shW, shH); + } + + if (world.State == RunState.GameOver && !h.Alive) + { + // 倒地:简单压扁 + 变灰 + DrawHero(g, footX, footY, bodyH, s, h, flattened: true); + return; + } + + DrawHero(g, footX, footY, bodyH, s, h, flattened: false); + } + + private static void DrawHero(Graphics g, float footX, float footY, float bodyH, float s, + Player h, bool flattened) + { + // 影子偏移(换道侧倾) + float lean = flattened ? 0f : h.Tilt; + float cx = footX + lean * bodyH * 0.45f; + + Color cloth = flattened ? Color.FromArgb(150, 110, 100, 96) : Cfg.Player; + Color clothDark = flattened ? Color.FromArgb(150, 84, 74, 70) : Color.FromArgb(176, 62, 40); + Color skin = flattened ? Color.FromArgb(150, 170, 150, 132) : Color.FromArgb(242, 196, 152); + Color hair = Color.FromArgb(48, 36, 30); + + float unit = bodyH / 62f; // 1 = 站立时的相对比例 + + using var penBody = new Pen(cloth, Math.Max(2.2f, bodyH * 0.155f)) + { StartCap = LineCap.Round, EndCap = LineCap.Round }; + using var penLimb = new Pen(clothDark, Math.Max(2f, bodyH * 0.115f)) + { StartCap = LineCap.Round, EndCap = LineCap.Round }; + + float hipY = footY - bodyH * 0.42f; + float shoulderY = footY - bodyH * 0.76f; + + if (flattened) + { + // 趴倒 + float len = bodyH * 1.05f; + using var bodyPen = new Pen(cloth, Math.Max(2.4f, bodyH * 0.17f)) + { StartCap = LineCap.Round, EndCap = LineCap.Round }; + g.DrawLine(bodyPen, cx - len * 0.32f, footY - bodyH * 0.16f, cx + len * 0.38f, footY - bodyH * 0.10f); + using var headBr = new SolidBrush(skin); + float hr = bodyH * 0.135f; + g.FillEllipse(headBr, cx + len * 0.34f - hr, footY - bodyH * 0.26f - hr, hr * 2, hr * 2); + return; + } + + // ---- 腿 ---- + float swing = MathF.Sin(h.RunPhase); + bool airborne = !h.OnGround; + float legLen = bodyH * 0.42f; + + void DrawLeg(float sign) + { + float lift = airborne ? 0.42f : MathF.Max(0f, sign * swing) * 0.75f; + float fx = cx + sign * 9f * unit + (airborne ? 0f : sign * swing * 12f * unit); + float fy = footY - legLen * (airborne ? 0.45f : lift); + float kneeX = cx + sign * 6f * unit + (fx - cx) * 0.4f; + float kneeY = (hipY + fy) * 0.5f; + g.DrawLine(penLimb, cx + sign * 5f * unit, hipY, kneeX, kneeY); + g.DrawLine(penLimb, kneeX, kneeY, fx, fy); + } + DrawLeg(-1f); + DrawLeg(+1f); + + // ---- 躯干 ---- + float torsoW = bodyH * 0.30f; + using (var torso = new SolidBrush(cloth)) + { + var rc = new RectangleF(cx - torsoW * 0.5f, shoulderY, torsoW, hipY - shoulderY); + using var rp = RoundRect(rc, torsoW * 0.34f); + g.FillPath(torso, rp); + } + + // ---- 手臂(与腿反向摆动) ---- + void DrawArm(float sign) + { + float a = -sign * swing; + float ex = cx + sign * (bodyH * 0.20f) + a * bodyH * 0.15f; + float ey = shoulderY + bodyH * 0.26f - MathF.Abs(a) * bodyH * 0.10f; + float midX = cx + sign * (bodyH * 0.19f); + float midY = shoulderY + bodyH * 0.13f; + g.DrawLine(penLimb, cx + sign * bodyH * 0.13f, shoulderY + bodyH * 0.03f, midX, midY); + g.DrawLine(penLimb, midX, midY, ex, ey); + } + DrawArm(-1f); + DrawArm(+1f); + + // ---- 头 ---- + float headR = bodyH * 0.145f; + float headCY = shoulderY - headR * 0.95f; + using (var hb = new SolidBrush(skin)) + g.FillEllipse(hb, cx - headR, headCY - headR, headR * 2, headR * 2); + + // 头发(上半圆) + using (var hb = new SolidBrush(hair)) + { + var rc = new RectangleF(cx - headR * 1.02f, headCY - headR * 1.10f, headR * 2.04f, headR * 1.35f); + using var rp = RoundRect(rc, headR * 0.75f); + g.FillPath(hb, rp); + } + + // 滑铲时的速度线 + if (h.Sliding) + { + using var sl = new Pen(Color.FromArgb(130, 255, 240, 210), Math.Max(1.4f, bodyH * 0.05f)) + { StartCap = LineCap.Round, EndCap = LineCap.Round }; + for (int i = 0; i < 3; i++) + { + float yy = footY - bodyH * (0.10f + i * 0.14f); + g.DrawLine(sl, cx - bodyH * 0.42f, yy, cx - bodyH * 0.18f, yy); + } + } + } + + // ---------------------------------------------------------------- HUD + private static void DrawHud(Graphics g, int w, int h, GameWorld world, bool showFps) + { + g.SmoothingMode = SmoothingMode.AntiAlias; + + // ---- 左上:分数 / 金币 ---- + DrawChip(g, 18, 14, 236, 92); + + string score = world.Score.ToString(); + using (var sh = new SolidBrush(Color.FromArgb(130, 0, 0, 0))) + g.DrawString(score, _fHuge, sh, 32 + 2, 16 + 2); + using (var sb = new SolidBrush(Color.White)) + g.DrawString(score, _fHuge, sb, 32, 16); + + // 金币行 + float coinY = 78; + float pop = 1f + world.CoinPop * 0.35f; + float cr = 9f * pop; + using (var gb = new SolidBrush(Cfg.Gold)) + g.FillEllipse(gb, 34 - cr, coinY - cr, cr * 2, cr * 2); + using (var gp = new Pen(Cfg.GoldDark, 2f)) + g.DrawEllipse(gp, 34 - cr, coinY - cr, cr * 2, cr * 2); + using (var sb = new SolidBrush(Color.FromArgb(255, 245, 210))) + g.DrawString("× " + world.CoinCount, _fMid, sb, 52, coinY - 13); + + // ---- 右上:速度 / 最佳 ---- + string speedTxt = "速度 ×" + (world.Speed / Cfg.StartSpeed).ToString("0.00"); + SizeF st = g.MeasureString(speedTxt, _fMid); + DrawChip(g, w - st.Width - 44, 16, st.Width + 26, 34); + using (var sb = new SolidBrush(Color.FromArgb(255, 226, 170))) + g.DrawString(speedTxt, _fMid, sb, w - st.Width - 31, 24); + + string bestTxt = "最佳 " + world.Best; + SizeF bt = g.MeasureString(bestTxt, _fSmall); + using (var sb = new SolidBrush(Color.FromArgb(210, 236, 226, 214))) + g.DrawString(bestTxt, _fSmall, sb, w - bt.Width - 22, 58); + + if (world.Muted) + { + using var sb = new SolidBrush(Color.FromArgb(150, 236, 226, 214)); + g.DrawString("已静音 (M)", _fTiny, sb, w - 96, 82); + } + + // ---- 中间状态层 ---- + switch (world.State) + { + case RunState.Ready: DrawReady(g, w, h, world); break; + case RunState.Paused: DrawPaused(g, w, h); break; + case RunState.GameOver: DrawGameOver(g, w, h, world); break; + } + + // ---- 右下角操作提示 ---- + using (var sb = new SolidBrush(Color.FromArgb(120, 240, 234, 226))) + g.DrawString("← → 换道 ↑ 跳跃 ↓ 滑铲 P 暂停 M 静音 F1 调试 F11 全屏", + _fTiny, sb, 18, h - 26); + + if (showFps) + { + using var db = new SolidBrush(Color.FromArgb(185, 168, 255, 196)); + g.DrawString( + $"FPS {world.Fps:0} 障碍 {world.Obstacles.Count} 金币 {world.Coins.Count}" + + $" 装饰 {world.Props.Count} 粒子 {world.Particles.Count} 速度 {world.Speed:0}", + _fTiny, db, 18, h - 46); + } + } + + private static void DrawReady(Graphics g, int w, int h, GameWorld world) + { + float cy = h * 0.055f; // 贴顶排布,避免压住画面中央的跑者 + + DrawCentered(g, "神庙逃亡", _fHuge, w, cy + 3, Color.FromArgb(130, 0, 0, 0)); + DrawCentered(g, "神庙逃亡", _fHuge, w, cy, Color.FromArgb(255, 232, 176)); + + DrawCentered(g, "TEMPLE RUN", _fTitle, w, cy + 58, Color.FromArgb(214, 176, 120)); + + string[] lines = + { + "← → 左右换道", + "↑ 或 空格 跳跃越过矮石堆", + "↓ 滑铲穿过悬空横梁", + "石墙无法越过,只能换道躲避", + "收集金币可提高速度倍率" + }; + for (int i = 0; i < lines.Length; i++) + DrawCentered(g, lines[i], _fMid, w, cy + 118 + i * 30, Color.FromArgb(226, 216, 202)); + + // 闪烁提示 + float blink = (MathF.Sin(world.Clock * 4.2f) + 1f) * 0.5f; + var c = Cfg.Lerp(Color.FromArgb(120, 255, 236, 190), Color.FromArgb(255, 255, 244, 214), blink); + DrawCentered(g, "按 空格 开始奔跑", _fBig, w, cy + 118 + lines.Length * 30 + 26, c); + } + + private static void DrawPaused(Graphics g, int w, int h) + { + using (var ov = new SolidBrush(Color.FromArgb(140, 10, 10, 18))) + g.FillRectangle(ov, 0, 0, w, h); + DrawCentered(g, "已 暂 停", _fHuge, w, h * 0.38f, Color.White); + DrawCentered(g, "按 P / 空格 继续 按 R 重新开始", _fMid, w, h * 0.38f + 70, + Color.FromArgb(210, 226, 218, 206)); + } + + private static void DrawGameOver(Graphics g, int w, int h, GameWorld world) + { + bool newRecord = world.Score >= world.Best && world.Score > 0; + + float pw = Math.Min(520f, w - 80f); + float ph = 330f; + float px = (w - pw) * 0.5f; + float py = h * 0.5f - ph * 0.5f; + + using (var ov = new SolidBrush(Color.FromArgb(150, 8, 8, 14))) + g.FillRectangle(ov, 0, 0, w, h); + + var panel = new RectangleF(px, py, pw, ph); + using (var pp = RoundRect(panel, 18f)) + { + using var pb = new SolidBrush(Color.FromArgb(226, 26, 24, 34)); + g.FillPath(pb, pp); + using var pen = new Pen(Color.FromArgb(200, 214, 168, 96), 2.4f); + g.DrawPath(pen, pp); + } + + DrawCentered(g, "游 戏 结 束", _fTitle, w, py + 24, Color.FromArgb(255, 138, 106)); + + float y = py + 92; + DrawStatRow(g, px, pw, y, "本局得分", world.Score.ToString(), + newRecord ? Cfg.Gold : Color.White, _fBig); + DrawStatRow(g, px, pw, y + 46, "金币数量", world.CoinCount.ToString(), Color.FromArgb(255, 214, 120), _fBig); + DrawStatRow(g, px, pw, y + 92, "奔跑距离", ((int)(world.Distance / 10f)) + " m", Color.FromArgb(200, 226, 216, 200), _fBig); + DrawStatRow(g, px, pw, y + 138, "历史最佳", world.Best.ToString(), Color.FromArgb(150, 226, 214, 196), _fBig); + + if (newRecord) + DrawCentered(g, "★ 新 纪 录 ★", _fMid, w, py + ph - 88, Cfg.Gold); + + DrawCentered(g, "按 R / 空格 再来一局", _fMid, w, py + ph - 46, + Color.FromArgb(226, 236, 226, 210)); + } + + private static void DrawStatRow(Graphics g, float px, float pw, float y, string label, string value, + Color valueColor, Font vf) + { + using (var lb = new SolidBrush(Color.FromArgb(190, 208, 198, 184))) + g.DrawString(label, _fMid, lb, px + 36, y + 6); + + SizeF vs = g.MeasureString(value, vf); + using (var vb = new SolidBrush(valueColor)) + g.DrawString(value, vf, vb, px + pw - 36 - vs.Width, y); + } + + private static void DrawCentered(Graphics g, string text, Font f, int w, float y, Color color) + { + using var br = new SolidBrush(color); + SizeF sz = g.MeasureString(text, f); + g.DrawString(text, f, br, (w - sz.Width) * 0.5f, y); + } + + private static void DrawChip(Graphics g, float x, float y, float w, float h) + { + var r = new RectangleF(x, y, w, h); + using var p = RoundRect(r, 10f); + using var br = new SolidBrush(Color.FromArgb(118, 12, 14, 22)); + g.FillPath(br, p); + using var pen = new Pen(Color.FromArgb(86, 226, 198, 140), 1.4f); + g.DrawPath(pen, p); + } + + private static GraphicsPath RoundRect(RectangleF r, float radius) + { + var p = new GraphicsPath(); + float d = Math.Min(radius * 2f, Math.Min(r.Width, r.Height)); + p.AddArc(r.X, r.Y, d, d, 180, 90); + p.AddArc(r.Right - d, r.Y, d, d, 270, 90); + p.AddArc(r.Right - d, r.Bottom - d, d, d, 0, 90); + p.AddArc(r.X, r.Bottom - d, d, d, 90, 90); + p.CloseFigure(); + return p; + } +} diff --git a/TempleRun/GameWorld.cs b/TempleRun/GameWorld.cs new file mode 100644 index 0000000..0806aff --- /dev/null +++ b/TempleRun/GameWorld.cs @@ -0,0 +1,605 @@ +using System.Media; + +namespace TempleRun; + +/// +/// 游戏世界:状态机 + 生成器 + 物理 + 碰撞。 +/// 不含任何绘制代码,纯逻辑,便于单独测试。 +/// +public class GameWorld +{ + public RunState State = RunState.Ready; + + public readonly Player Hero = new(); + public readonly List Obstacles = new(); + public readonly List Coins = new(); + public readonly List Particles = new(); + public readonly List Props = new(); + + // 统计 + public float Speed; + public float Distance; // 累计前进距离(世界单位) + public int CoinCount; + public int Best; + public int Score => (int)(Distance / 10f) + CoinCount * 10; + + // 表现层效果 + public float Shake; // 屏幕震动强度(像素) + public float HitFlash; // 撞击红闪 0..1 + public float CoinPop; // 吃金币时的 HUD 弹跳 0..1 + + public bool Muted; + public float Clock; // 累计运行时间(用于 UI 呼吸/闪烁动画) + public float Fps; // 由窗体写入,仅用于调试显示 + + /// 死亡动画播完才允许重开,避免手快误触 + public bool CanRestart => _deathDelay <= 0f; + + private float _nextSpawn; // 距下一组障碍还有多远 + private float _nextProp; // 距下一组路边装饰还有多远 + private float _deathDelay; // 死亡动画剩余时间 + private float _lastCoinSfx; + private float _sfxClock; + + private readonly Random _rng = new(); + private readonly string _bestPath; + + public GameWorld() + { + try + { + string dir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "TempleRun"); + Directory.CreateDirectory(dir); + _bestPath = Path.Combine(dir, "best.txt"); + if (File.Exists(_bestPath) && int.TryParse(File.ReadAllText(_bestPath), out int b)) + Best = b; + } + catch { _bestPath = null; } + + Reset(); + } + + // ------------------------------------------------------------------ + // 生命周期 + // ------------------------------------------------------------------ + + public void Reset() + { + Hero.Reset(); + Obstacles.Clear(); + Coins.Clear(); + Particles.Clear(); + Props.Clear(); + + Speed = Cfg.StartSpeed; + Distance = 0; + CoinCount = 0; + Shake = 0; + HitFlash = 0; + CoinPop = 0; + _deathDelay = 0; + _nextSpawn = 1400f; + _nextProp = 120f; + + // 预铺一条"石柱长廊",避免开局两侧空荡 + for (float z = 400; z < Cfg.FarZ; z += 165f) + SpawnProp(z); + + State = RunState.Ready; + } + + public void Start() + { + Reset(); + State = RunState.Playing; + } + + // ------------------------------------------------------------------ + // 输入 + // ------------------------------------------------------------------ + + public bool HandleKey(Keys key) + { + switch (State) + { + case RunState.Ready: + if (key is Keys.Space or Keys.Enter) { Start(); return true; } + break; + + case RunState.Playing: + if (key is Keys.Left or Keys.A) { ShiftLane(-1); return true; } + if (key is Keys.Right or Keys.D) { ShiftLane(+1); return true; } + if (key is Keys.Up or Keys.W or Keys.Space) { Jump(); return true; } + if (key is Keys.Down or Keys.S) { Slide(); return true; } + if (key is Keys.P or Keys.Escape) { State = RunState.Paused; return true; } + break; + + case RunState.Paused: + if (key is Keys.P or Keys.Escape or Keys.Space) { State = RunState.Playing; return true; } + if (key is Keys.R) { Start(); return true; } + break; + + case RunState.GameOver: + if (!CanRestart) return true; + if (key is Keys.R or Keys.Enter or Keys.Space) { Start(); return true; } + break; + } + + if (key is Keys.M) { Muted = !Muted; return true; } + return false; + } + + private void ShiftLane(int dir) + { + if (!Hero.Alive) return; + int t = Math.Clamp(Hero.TargetLane + dir, 0, Cfg.LaneCount - 1); + if (t != Hero.TargetLane) + Hero.TargetLane = t; + } + + private void Jump() + { + if (!Hero.Alive || !Hero.OnGround) return; + Hero.Vy = Cfg.JumpSpeed; + Hero.OnGround = false; + Hero.Sliding = false; + Hero.SlideTimer = 0; + SpawnDust(Hero.X, Hero.Y, 0f, 6); + } + + private void Slide() + { + if (!Hero.Alive) return; + // 空中按下 → 快速下落(更顺手) + if (!Hero.OnGround) Hero.Vy = Math.Min(Hero.Vy, -420f); + Hero.Sliding = true; + Hero.SlideTimer = Cfg.SlideDuration; + SpawnDust(Hero.X, Hero.Y, 0f, 8); + } + + // ------------------------------------------------------------------ + // 主更新 + // ------------------------------------------------------------------ + + public void Update(float dt) + { + _sfxClock += dt; + Clock += dt; + + if (State == RunState.Paused) return; + + if (State == RunState.Ready) + { + // 待机:背景缓慢滚动 + 原地跑,营造氛围 + RollWorld(300f * dt); + Hero.RunPhase += dt * 7.5f; + UpdateParticles(dt); + DecayEffects(dt); + return; + } + + if (State == RunState.GameOver) + { + UpdateParticles(dt); + DecayEffects(dt); + if (_deathDelay > 0) _deathDelay -= dt; + return; + } + + // ---------------- 游戏中 ---------------- + if (!Hero.Alive) + { + RollWorld(Speed * dt * 0.25f); + UpdateParticles(dt); + DecayEffects(dt); + _deathDelay -= dt; + if (_deathDelay <= 0f) FinishRun(); + return; + } + + // 难度递增 + float target = Cfg.StartSpeed + Distance * 0.00035f + CoinCount * Cfg.SpeedPerCoin; + Speed = Math.Min(Cfg.MaxSpeed, Math.Max(Speed, target)); + Speed += Cfg.SpeedAccel * dt; + Speed = Math.Min(Speed, Cfg.MaxSpeed); + + float move = Speed * dt; + Distance += move; + + UpdatePlayer(dt); + RollWorld(move); + UpdateEntities(dt, move); + Generate(move); + CheckCollisions(); + UpdateParticles(dt); + DecayEffects(dt); + } + + private void DecayEffects(float dt) + { + Shake = Math.Max(0f, Shake - dt * 60f); + HitFlash = Math.Max(0f, HitFlash - dt * 2.6f); + CoinPop = Math.Max(0f, CoinPop - dt * 4f); + } + + // ------------------------------------------------------------------ + // 玩家 + // ------------------------------------------------------------------ + + private void UpdatePlayer(float dt) + { + float targetX = Cfg.LaneX(Hero.TargetLane); + float k = 1f - MathF.Exp(-Cfg.LaneLerp * dt); // 帧率无关的指数插值 + float dx = targetX - Hero.X; + Hero.X += dx * k; + if (Math.Abs(dx) < 0.6f) Hero.X = targetX; + + // 侧倾:换道时身体倾斜 + float wantTilt = Math.Clamp(dx / Cfg.LaneWidth, -1f, 1f) * 0.32f; + Hero.Tilt += (wantTilt - Hero.Tilt) * Math.Min(1f, dt * 9f); + + // 重力 + if (!Hero.OnGround || Hero.Vy > 0f) + { + Hero.Vy -= Cfg.Gravity * dt; + Hero.Y += Hero.Vy * dt; + + if (Hero.Y <= 0f) + { + Hero.Y = 0f; + Hero.Vy = 0f; + if (!Hero.OnGround) + { + Hero.OnGround = true; + SpawnDust(Hero.X, 0f, 0f, 8); + } + } + } + + // 滑铲计时 + if (Hero.Sliding) + { + Hero.SlideTimer -= dt; + if (Hero.SlideTimer <= 0f) Hero.Sliding = false; + } + + // 跑动动画(速度越快腿摆得越快) + Hero.RunPhase += dt * (4.2f + Speed / 165f); + + // 跑动扬尘 + if (Hero.OnGround && !Hero.Sliding && _rng.NextDouble() < dt * 26) + SpawnDust(Hero.X + (float)(_rng.NextDouble() - 0.5) * 30f, 0f, -8f, 1); + } + + /// 世界向玩家方向滚动:所有实体 Z 减少。 + private void RollWorld(float move) + { + Hero.X += 0f; + + for (int i = Obstacles.Count - 1; i >= 0; i--) + Obstacles[i].Z -= move; + for (int i = Coins.Count - 1; i >= 0; i--) + Coins[i].Z -= move; + for (int i = Props.Count - 1; i >= 0; i--) + Props[i].Z -= move; + for (int i = Particles.Count - 1; i >= 0; i--) + Particles[i].Z -= move; + } + + private void UpdateEntities(float dt, float move) + { + // 金币旋转 + 回收 + for (int i = Coins.Count - 1; i >= 0; i--) + { + var c = Coins[i]; + c.Phase += dt * 5.5f; + if (c.Taken || c.Z < -260f) Coins.RemoveAt(i); + } + + // 障碍回收 + for (int i = Obstacles.Count - 1; i >= 0; i--) + if (Obstacles[i].Z < -300f) Obstacles.RemoveAt(i); + + // 装饰回收 + for (int i = Props.Count - 1; i >= 0; i--) + if (Props[i].Z < -300f) Props.RemoveAt(i); + } + + // ------------------------------------------------------------------ + // 生成器 + // ------------------------------------------------------------------ + + private void Generate(float move) + { + _nextSpawn -= move; + if (_nextSpawn <= 0f) + { + SpawnRow(Cfg.FarZ); + float sec = (float)(Cfg.SpawnIntervalMin + + _rng.NextDouble() * (Cfg.SpawnIntervalMax - Cfg.SpawnIntervalMin)); + // 速度越快,按"时间间隔"换算成距离间隔,保证反应时间一致 + _nextSpawn = Math.Max(420f, sec * Speed * 0.62f); + } + + _nextProp -= move; + if (_nextProp <= 0f) + { + SpawnProp(Cfg.FarZ); + _nextProp = 130f + (float)_rng.NextDouble() * 150f; + } + } + + /// 生成一组障碍 + 可能附带的金币。保证至少留一条可通行车道。 + private void SpawnRow(float z) + { + int laneCount = _rng.NextDouble() < 0.78 ? 1 : 2; // 1~2 条道放障碍 + + var lanes = new[] { 0, 1, 2 }; + // 洗牌 + for (int i = lanes.Length - 1; i > 0; i--) + { + int j = _rng.Next(i + 1); + (lanes[i], lanes[j]) = (lanes[j], lanes[i]); + } + + var used = new List(); + for (int i = 0; i < laneCount; i++) + { + int lane = lanes[i]; + used.Add(lane); + + ObstacleKind kind; + double r = _rng.NextDouble(); + // 两条道同时出现时,避免双"整道墙"把路堵死(剩 1 条空道仍可走,允许) + kind = r < 0.40 ? ObstacleKind.Low + : r < 0.72 ? ObstacleKind.High + : ObstacleKind.Full; + + Obstacles.Add(new Obstacle { Z = z, Lane = lane, Kind = kind }); + } + + // 在空道上撒一串金币 + if (_rng.NextDouble() < 0.72) + { + int freeLane = -1; + for (int i = 0; i < Cfg.LaneCount; i++) + if (!used.Contains(i)) { freeLane = i; break; } + + if (freeLane >= 0) + { + int n = 3 + _rng.Next(4); + float dz = 52f; + // 金币以本组障碍为中心分布,稍微错开一点更自然 + float start = z - (n - 1) * dz * 0.5f + (float)(_rng.NextDouble() - 0.5) * 40f; + for (int i = 0; i < n; i++) + { + Coins.Add(new Coin + { + Z = start + i * dz, + Lane = freeLane, + Phase = i * 0.6f + }); + } + } + } + } + + private void SpawnProp(float z) + { + int side = _rng.NextDouble() < 0.5 ? -1 : 1; + int type = _rng.Next(100) switch + { + < 42 => 0, // 石柱 + < 62 => 1, // 火把 + < 90 => 2, // 枯树 + _ => 3 // 碎石 + }; + float h = type switch + { + 0 => 150f + (float)_rng.NextDouble() * 90f, + 1 => 92f, + 2 => 130f + (float)_rng.NextDouble() * 70f, + _ => 26f + (float)_rng.NextDouble() * 22f + }; + Props.Add(new SideProp + { + Z = z, + Side = side, + Type = type, + Height = h, + Phase = (float)_rng.NextDouble() * 6.28f + }); + } + + // ------------------------------------------------------------------ + // 碰撞 + // ------------------------------------------------------------------ + + private void CheckCollisions() + { + float pNear = -Cfg.PlayerHalfDepth; + float pFar = Cfg.PlayerHalfDepth; + float py0 = Hero.Y + 5f; // 判定区间略微收缩,手感宽容 + float py1 = Hero.Y + Hero.Height - 5f; + float px = Hero.X; + const float halfBodyW = 15f; + + // --- 障碍 --- + for (int i = 0; i < Obstacles.Count; i++) + { + var o = Obstacles[i]; + float oNear = o.Z - o.Depth * 0.5f; + float oFar = o.Z + o.Depth * 0.5f; + if (oFar < pNear || oNear > pFar) continue; // Z 不重叠 + + if (Math.Abs(px - o.CenterX) > o.Width * 0.5f + halfBodyW) continue; // 不在同一道 + + // 高度区间是否相交 + bool hit = py0 < o.TopY && py1 > o.BottomY; + //"整道墙"永远相交 + if (o.Kind == ObstacleKind.Full) hit = true; + + if (hit) { Kill(o); return; } + + if (!o.Scored && o.Z < 0f) + { + o.Scored = true; + // 成功躲避的微小正反馈 + Shake = Math.Max(Shake, 1.2f); + } + } + + // --- 金币 --- + float coinNear = -34f, coinFar = 34f; + for (int i = 0; i < Coins.Count; i++) + { + var c = Coins[i]; + if (c.Taken) continue; + if (c.Z < coinNear || c.Z > coinFar) continue; + if (Math.Abs(px - c.CenterX) > Cfg.LaneWidth * 0.45f) continue; + // 金币在 36 高度,跳跃或站立都能吃到,滑铲吃不到 + float cy = c.Y; + if (cy < py0 - 22f || cy > py1 + 22f) continue; + + c.Taken = true; + CoinCount++; + CoinPop = 1f; + Speed = Math.Min(Cfg.MaxSpeed, Speed + Cfg.SpeedPerCoin * 0.35f); + SpawnCoinBurst(c.CenterX, c.Y, c.Z); + PlayCoinSfx(); + } + } + + private void Kill(Obstacle o) + { + if (!Hero.Alive) return; + Hero.Alive = false; + Hero.Sliding = false; + _deathDelay = 0.85f; + Shake = 16f; + HitFlash = 1f; + SpawnDebris(o.CenterX, Hero.Y + 26f, 0f); + PlayHitSfx(); + } + + private void FinishRun() + { + State = RunState.GameOver; + if (Score > Best) + { + Best = Score; + try { if (_bestPath != null) File.WriteAllText(_bestPath, Best.ToString()); } + catch { /* 忽略写盘失败 */ } + } + } + + // ------------------------------------------------------------------ + // 粒子 + // ------------------------------------------------------------------ + + public void AddParticle(Particle p) => Particles.Add(p); + + private void SpawnDust(float x, float y, float z, int count) + { + for (int i = 0; i < count; i++) + { + Particles.Add(new Particle + { + X = x + (float)(_rng.NextDouble() - 0.5) * 26f, + Y = y + (float)_rng.NextDouble() * 8f, + Z = z + (float)(_rng.NextDouble() - 0.5) * 20f, + Vx = (float)(_rng.NextDouble() - 0.5) * 60f, + Vy = 20f + (float)_rng.NextDouble() * 55f, + Vz = -30f - (float)_rng.NextDouble() * 40f, + MaxLife = 0.35f + (float)_rng.NextDouble() * 0.25f, + Life = 0f, + Size = 2.4f + (float)_rng.NextDouble() * 3.2f, + GravityScale = 0.4f, + Color = Color.FromArgb(196, 172, 140) + }); + } + } + + private void SpawnCoinBurst(float x, float y, float z) + { + for (int i = 0; i < 10; i++) + { + float a = (float)(_rng.NextDouble() * Math.PI * 2); + float sp = 40f + (float)_rng.NextDouble() * 90f; + Particles.Add(new Particle + { + X = x, Y = y, Z = z, + Vx = (float)Math.Cos(a) * sp, + Vy = 50f + (float)_rng.NextDouble() * 100f, + Vz = (float)Math.Sin(a) * sp * 0.6f, + MaxLife = 0.45f + (float)_rng.NextDouble() * 0.3f, + Size = 2.8f + (float)_rng.NextDouble() * 3.4f, + GravityScale = 0.8f, + Color = _rng.NextDouble() < 0.5 ? Cfg.Gold : Color.FromArgb(255, 240, 170) + }); + } + } + + private void SpawnDebris(float x, float y, float z) + { + for (int i = 0; i < 26; i++) + { + float a = (float)(_rng.NextDouble() * Math.PI * 2); + float sp = 60f + (float)_rng.NextDouble() * 190f; + Particles.Add(new Particle + { + X = x, Y = y, Z = z, + Vx = (float)Math.Cos(a) * sp, + Vy = 40f + (float)_rng.NextDouble() * 210f, + Vz = (float)Math.Sin(a) * sp, + MaxLife = 0.6f + (float)_rng.NextDouble() * 0.5f, + Size = 3f + (float)_rng.NextDouble() * 5f, + GravityScale = 1f, + Color = _rng.NextDouble() < 0.5 + ? Color.FromArgb(210, 90, 60) + : Color.FromArgb(150, 140, 130) + }); + } + } + + private void UpdateParticles(float dt) + { + for (int i = Particles.Count - 1; i >= 0; i--) + { + var p = Particles[i]; + p.Life += dt; + if (p.Life >= p.MaxLife) { Particles.RemoveAt(i); continue; } + + p.X += p.Vx * dt; + p.Y += p.Vy * dt; + p.Z += p.Vz * dt; + p.Vy -= Cfg.Gravity * p.GravityScale * dt; + + if (p.Y < 0f) + { + p.Y = 0f; + p.Vy *= -0.32f; // 轻微反弹 + p.Vx *= 0.6f; + p.Vz *= 0.6f; + } + } + } + + // ------------------------------------------------------------------ + // 音效(系统提示音,零依赖;M 键静音) + // ------------------------------------------------------------------ + + private void PlayCoinSfx() + { + if (Muted || _sfxClock - _lastCoinSfx < 0.07f) return; + _lastCoinSfx = _sfxClock; + try { SystemSounds.Asterisk.Play(); } catch { } + } + + private void PlayHitSfx() + { + if (Muted) return; + try { SystemSounds.Hand.Play(); } catch { } + } +} diff --git a/TempleRun/Program.cs b/TempleRun/Program.cs new file mode 100644 index 0000000..03cbeb0 --- /dev/null +++ b/TempleRun/Program.cs @@ -0,0 +1,14 @@ +namespace TempleRun; + +internal static class Program +{ + [STAThread] + private static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.SetHighDpiMode(HighDpiMode.SystemAware); + + Application.Run(new GameForm()); + } +} diff --git a/TempleRun/TempleRun.csproj b/TempleRun/TempleRun.csproj new file mode 100644 index 0000000..e210898 --- /dev/null +++ b/TempleRun/TempleRun.csproj @@ -0,0 +1,18 @@ + + + + WinExe + net8.0-windows + true + disable + enable + latest + TempleRun + TempleRun + app.manifest + zh-Hans + + true + + + diff --git a/TempleRun/_shot/01_ready.png b/TempleRun/_shot/01_ready.png new file mode 100644 index 0000000000000000000000000000000000000000..fadb1949a030b666ddf7b07638be933664cba993 GIT binary patch literal 108842 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 literal 0 HcmV?d00001 diff --git a/TempleRun/_shot/02_playing.png b/TempleRun/_shot/02_playing.png new file mode 100644 index 0000000000000000000000000000000000000000..468c07d18e22174d1f8d9727b3c162e1100961ef GIT binary patch literal 109240 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Π{F4dGp8dw4ePa;8uimp`ce%1NGxvqcpRo4f; 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

5G`?@MkhBF3KtvU1uF1NR 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 literal 0 HcmV?d00001 diff --git a/TempleRun/_smoke.ps1 b/TempleRun/_smoke.ps1 new file mode 100644 index 0000000..ebb3571 --- /dev/null +++ b/TempleRun/_smoke.ps1 @@ -0,0 +1,53 @@ +$ErrorActionPreference = 'Stop' +Add-Type -AssemblyName System.Drawing +Add-Type -AssemblyName System.Windows.Forms + +Add-Type @" +using System; +using System.Runtime.InteropServices; +public class Win32 { + [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; } + [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT r); + [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd); +} +"@ + +$root = "c:\Users\32453\CodeBuddy\20260825142915\TempleRun" +$exe = Join-Path $root "bin\Release\net8.0-windows\TempleRun.exe" +$out = Join-Path $root "_shot" +New-Item -ItemType Directory -Force -Path $out | Out-Null + +$p = Start-Process -FilePath $exe -PassThru +Start-Sleep -Seconds 3 +$p.Refresh() + +function Grab([string]$name) { + $h = $p.MainWindowHandle + if ($h -eq [IntPtr]::Zero) { Write-Output "no window"; return } + [Win32]::SetForegroundWindow($h) | Out-Null + Start-Sleep -Milliseconds 700 + $r = New-Object 'Win32+RECT' + [Win32]::GetWindowRect($h, [ref]$r) | Out-Null + $w = $r.Right - $r.Left + $ht = $r.Bottom - $r.Top + if ($w -le 0 -or $ht -le 0) { Write-Output "bad rect $w x $ht"; return } + $bmp = New-Object System.Drawing.Bitmap $w, $ht + $g = [System.Drawing.Graphics]::FromImage($bmp) + $g.CopyFromScreen($r.Left, $r.Top, 0, 0, $bmp.Size) + $path = Join-Path $out "$name.png" + $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) + $g.Dispose(); $bmp.Dispose() + Write-Output "saved $name : $w x $ht" +} + +Grab "01_ready" + +[System.Windows.Forms.SendKeys]::SendWait(" ") +Start-Sleep -Seconds 4 +Grab "02_playing" + +Start-Sleep -Seconds 4 +Grab "03_playing2" + +try { $p.Kill() } catch {} +Write-Output "exit=$($p.HasExited)" diff --git a/TempleRun/app.manifest b/TempleRun/app.manifest new file mode 100644 index 0000000..cdd7f40 --- /dev/null +++ b/TempleRun/app.manifest @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/backend/F9WebAiTest.Api/Controllers/Attendance/AttendanceController.cs b/backend/F9WebAiTest.Api/Controllers/Attendance/AttendanceController.cs new file mode 100644 index 0000000..2ed70c5 --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/Attendance/AttendanceController.cs @@ -0,0 +1,103 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos; +using F9WebAiTest.Application.Dtos.Attendance; +using F9WebAiTest.Application.Services; +using F9WebAiTest.Application.Services.Attendance; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.Attendance; + +[ApiController] +[Route("api/attendance")] +[Authorize] +public class AttendanceController : ControllerBase +{ + private readonly IAttendanceService _service; + + public AttendanceController(IAttendanceService service) + { + _service = service; + } + + // POST /api/attendance/punch 打卡 + [HttpPost("punch")] + public async Task Punch() + { + var result = await _service.PunchAsync(GetUserId()); + return Ok(ApiResult.Ok(result)); + } + + // GET /api/attendance/my 我的考勤记录 + [HttpGet("my")] + public async Task GetMy([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 10) + { + var result = await _service.GetMyRecordsAsync(GetUserId(), pageIndex, pageSize); + return Ok(ApiResult>.Ok(result)); + } + + // GET /api/attendance/my/summary 我的考勤统计 + [HttpGet("my/summary")] + public async Task GetMySummary([FromQuery] string month) + { + var result = await _service.GetMySummaryAsync(GetUserId(), month); + return Ok(ApiResult.Ok(result)); + } + + // GET /api/attendance 全部考勤(管理员) + [HttpGet] + public async Task GetAll([FromQuery] AttendanceQueryDto query) + { + var result = await _service.GetAllRecordsAsync(query); + return Ok(ApiResult>.Ok(result)); + } + + // POST /api/attendance/leave 申请请假 + [HttpPost("leave")] + public async Task ApplyLeave([FromBody] CreateLeaveDto dto) + { + try + { + var result = await _service.ApplyLeaveAsync(GetUserId(), dto); + return Ok(ApiResult.Ok(result, "申请已提交")); + } + catch (ServiceException ex) + { + return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); + } + } + + // GET /api/attendance/leave/my 我的请假 + [HttpGet("leave/my")] + public async Task GetMyLeaves() + { + var result = await _service.GetMyLeavesAsync(GetUserId()); + return Ok(ApiResult>.Ok(result)); + } + + // GET /api/attendance/leave 全部请假(管理员) + [HttpGet("leave")] + public async Task GetAllLeaves([FromQuery] string status) + { + var result = await _service.GetAllLeavesAsync(status); + return Ok(ApiResult>.Ok(result)); + } + + // PUT /api/attendance/leave/{id}/approve 审批请假 + [HttpPut("leave/{id}/approve")] + public async Task ApproveLeave(long id, [FromBody] ApproveLeaveDto dto) + { + try + { + var approverName = User.FindFirst("realName")?.Value ?? "管理员"; + var result = await _service.ApproveLeaveAsync(id, GetUserId(), approverName, dto); + return Ok(ApiResult.Ok(result, "审批完成")); + } + catch (ServiceException ex) + { + return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); + } + } + + private long GetUserId() => long.Parse(User.FindFirst("id")!.Value); +} diff --git a/backend/F9WebAiTest.Api/Controllers/Auth/AuthController.cs b/backend/F9WebAiTest.Api/Controllers/Auth/AuthController.cs new file mode 100644 index 0000000..0bf4861 --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/Auth/AuthController.cs @@ -0,0 +1,109 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos; +using F9WebAiTest.Application.Dtos.Auth; +using F9WebAiTest.Application.Services; +using F9WebAiTest.Application.Services.Auth; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.Auth; + +[ApiController] +[Route("api/auth")] +public class AuthController : ControllerBase +{ + private readonly IAuthService _authService; + + public AuthController(IAuthService authService) + { + _authService = authService; + } + + [HttpPost("login")] + public async Task Login([FromBody] LoginDto dto) + { + try + { + var result = await _authService.LoginAsync(dto); + return Ok(ApiResult.Ok(result, "登录成功")); + } + catch (ServiceException ex) + { + return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); + } + } + + [HttpPost("refresh")] + public async Task Refresh([FromBody] RefreshTokenDto dto) + { + try + { + var result = await _authService.RefreshTokenAsync(dto.RefreshToken); + return Ok(ApiResult.Ok(result, "刷新成功")); + } + catch (ServiceException ex) + { + return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); + } + } + + [Authorize] + [HttpPost("logout")] + public async Task Logout([FromBody] RefreshTokenDto dto) + { + await _authService.RevokeRefreshTokenAsync(dto.RefreshToken); + return Ok(ApiResult.Ok(null, "已退出登录")); + } + + [Authorize] + [HttpGet("me")] + public async Task GetCurrentUser() + { + var id = GetUserId(); + var user = await _authService.GetCurrentUserAsync(id); + return Ok(ApiResult.Ok(user)); + } + + [Authorize] + [HttpGet("menus")] + public async Task GetMenus() + { + var id = GetUserId(); + var menus = await _authService.GetUserMenusAsync(id); + return Ok(ApiResult>.Ok(menus)); + } + + [Authorize] + [HttpGet("permissions")] + public async Task GetPermissions() + { + var id = GetUserId(); + var perms = await _authService.GetUserPermissionsAsync(id); + return Ok(ApiResult>.Ok(perms)); + } + + [Authorize] + [HttpPut("password")] + public async Task ChangePassword([FromBody] ChangePasswordDto dto) + { + try + { + await _authService.ChangePasswordAsync(GetUserId(), dto); + return Ok(ApiResult.Ok(null, "密码修改成功")); + } + catch (ServiceException ex) + { + return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); + } + } + + [Authorize] + [HttpPut("profile")] + public async Task UpdateProfile([FromBody] UpdateProfileDto dto) + { + await _authService.UpdateProfileAsync(GetUserId(), dto); + return Ok(ApiResult.Ok(null, "资料更新成功")); + } + + private long GetUserId() => long.Parse(User.FindFirst("id")!.Value); +} diff --git a/backend/F9WebAiTest.Api/Controllers/Base/BusinessControllers.cs b/backend/F9WebAiTest.Api/Controllers/Base/BusinessControllers.cs new file mode 100644 index 0000000..4376d7e --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/Base/BusinessControllers.cs @@ -0,0 +1,153 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos; +using F9WebAiTest.Application.Dtos.Base; +using F9WebAiTest.Application.Services; +using F9WebAiTest.Application.Services.Base; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.Base; + +// ==================== 工艺庄口 ==================== +[ApiController] +[Route("api/craftzhuangkou")] +[Authorize] +public class CraftZhuangKouController : ControllerBase +{ + private readonly ICraftZhuangKouService _svc; + public CraftZhuangKouController(ICraftZhuangKouService svc) => _svc = svc; + + [HttpGet] + public async Task GetPaged([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20, [FromQuery] string keyword = "", [FromQuery] int? year = null) + => Ok(ApiResult>.Ok(await _svc.GetPagedAsync(pageIndex, pageSize, keyword, year))); + + [HttpPost] + public async Task Create([FromBody] CraftZhuangKouCreateDto dto) + { + try { return Ok(ApiResult.Ok(await _svc.CreateAsync(dto), "创建成功")); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpPut("{id}")] + public async Task Update(long id, [FromBody] CraftZhuangKouCreateDto dto) + { + try { return Ok(ApiResult.Ok(await _svc.UpdateAsync(id, dto), "更新成功")); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try { await _svc.DeleteAsync(id); return Ok(ApiResult.Ok(null, "删除成功")); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} + +// ==================== 生丝规格 ==================== +[ApiController] +[Route("api/shengsiguige")] +[Authorize] +public class ShengsiGuigeController : ControllerBase +{ + private readonly IShengsiGuigeService _svc; + public ShengsiGuigeController(IShengsiGuigeService svc) => _svc = svc; + + [HttpGet] + public async Task GetPaged([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20, [FromQuery] string keyword = "") + => Ok(ApiResult>.Ok(await _svc.GetPagedAsync(pageIndex, pageSize, keyword))); + + [HttpGet("all")] + public async Task GetAllGuige() + => Ok(ApiResult>.Ok(await _svc.GetAllGuigeAsync())); + + [HttpPost] + public async Task Create([FromBody] ShengsiGuigeDto dto) + { + try { return Ok(ApiResult.Ok(await _svc.CreateAsync(dto), "创建成功")); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpPut("{id}")] + public async Task Update(long id, [FromBody] ShengsiGuigeDto dto) + { + try { return Ok(ApiResult.Ok(await _svc.UpdateAsync(id, dto), "更新成功")); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try { await _svc.DeleteAsync(id); return Ok(ApiResult.Ok(null, "删除成功")); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} + +// ==================== 生丝定级指标 ==================== +[ApiController] +[Route("api/shengsidingjizhibiao")] +[Authorize] +public class ShengsiDingjizhibiaoController : ControllerBase +{ + private readonly IShengsiDingjizhibiaoService _svc; + public ShengsiDingjizhibiaoController(IShengsiDingjizhibiaoService svc) => _svc = svc; + + [HttpGet] + public async Task GetPaged([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20, [FromQuery] string keyword = "", [FromQuery] int? standard = null, [FromQuery] string guige = "") + => Ok(ApiResult>.Ok(await _svc.GetPagedAsync(pageIndex, pageSize, keyword, standard, guige))); + + [HttpPost] + public async Task Create([FromBody] ShengsiDingjizhibiaoDto dto) + { + try { return Ok(ApiResult.Ok(await _svc.CreateAsync(dto), "创建成功")); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpPut("{id}")] + public async Task Update(long id, [FromBody] ShengsiDingjizhibiaoDto dto) + { + try { return Ok(ApiResult.Ok(await _svc.UpdateAsync(id, dto), "更新成功")); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try { await _svc.DeleteAsync(id); return Ok(ApiResult.Ok(null, "删除成功")); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} + +// ==================== 机组车台 ==================== +[ApiController] +[Route("api/jizuchetai")] +[Authorize] +public class JizuChetaiController : ControllerBase +{ + private readonly IJizuChetaiService _svc; + public JizuChetaiController(IJizuChetaiService svc) => _svc = svc; + + [HttpGet] + public async Task GetPaged([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20, [FromQuery] string keyword = "") + => Ok(ApiResult>.Ok(await _svc.GetPagedAsync(pageIndex, pageSize, keyword))); + + [HttpPost] + public async Task Create([FromBody] JizuChetaiDto dto) + { + try { return Ok(ApiResult.Ok(await _svc.CreateAsync(dto), "创建成功")); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpPut("{id}")] + public async Task Update(long id, [FromBody] JizuChetaiDto dto) + { + try { return Ok(ApiResult.Ok(await _svc.UpdateAsync(id, dto), "更新成功")); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try { await _svc.DeleteAsync(id); return Ok(ApiResult.Ok(null, "删除成功")); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/Notice/NotificationControllers.cs b/backend/F9WebAiTest.Api/Controllers/Notice/NotificationControllers.cs new file mode 100644 index 0000000..8c0d80b --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/Notice/NotificationControllers.cs @@ -0,0 +1,191 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos; +using F9WebAiTest.Application.Dtos.Notice; +using F9WebAiTest.Application.Services; +using F9WebAiTest.Application.Services.Auth; +using F9WebAiTest.Application.Services.Notice; +using F9WebAiTest.Domain.Entities; +using F9WebAiTest.Domain.Entities.Notice; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.Notice; + +// ==================== 消息类型管理(管理员) ==================== +[ApiController] +[Route("api/notification/type")] +[Authorize] +public class NotificationTypeController : ControllerBase +{ + private readonly INotificationService _svc; + public NotificationTypeController(INotificationService svc) => _svc = svc; + + [HttpGet] + public async Task GetList() + => Ok(ApiResult>.Ok(await _svc.GetTypeListAsync())); + + [HttpPost] + public async Task Create([FromBody] NotificationTypeCreateDto dto) + { + try { return Ok(ApiResult.Ok(await _svc.CreateTypeAsync(dto, GetUserId()), "创建成功")); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpPut("{id}")] + public async Task Update(long id, [FromBody] NotificationTypeCreateDto dto) + { + try { return Ok(ApiResult.Ok(await _svc.UpdateTypeAsync(id, dto), "更新成功")); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try { await _svc.DeleteTypeAsync(id); return Ok(ApiResult.Ok(null, "删除成功")); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + private long GetUserId() => long.Parse(User.FindFirst("id")!.Value); +} + +// ==================== 用户端:订阅 + 消息 ==================== +[ApiController] +[Route("api/notification")] +[Authorize] +public class NotificationController : ControllerBase +{ + private readonly INotificationService _svc; + private readonly IMediator _mediator; + private readonly IAuthService _authService; + public NotificationController(INotificationService svc, IMediator mediator, IAuthService authService) + { + _svc = svc; + _mediator = mediator; + _authService = authService; + } + + // 可订阅的消息类型列表(有权限的才可见/可订阅) + [HttpGet("subscribe")] + public async Task GetSubscribeList() + => Ok(ApiResult>.Ok(await _svc.GetSubscribeListAsync(GetUserId()))); + + // 订阅 / 取消订阅 + [HttpPost("subscribe")] + public async Task Subscribe([FromBody] SubscribeDto dto) + { + try + { + await _svc.SubscribeAsync(GetUserId(), dto.NotificationTypeId, dto.Subscribe); + return Ok(ApiResult.Ok(null, dto.Subscribe ? "订阅成功" : "已取消订阅")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + // 我的消息列表(readStatus: 0未读 1已读;category: 0通知 1待办 2公告) + [HttpGet("messages")] + public async Task GetMyMessages([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20, [FromQuery] int? readStatus = null, [FromQuery] int? category = null) + => Ok(ApiResult>.Ok(await _svc.GetMyMessagesAsync(GetUserId(), pageIndex, pageSize, readStatus, category))); + + // 我的待办列表 + [HttpGet("todos")] + public async Task GetMyTodos([FromQuery] int? status = null) + => Ok(ApiResult>.Ok(await _svc.GetMyTodosAsync(GetUserId(), status))); + + // 完成待办 + [HttpPost("todos/{id}/complete")] + public async Task CompleteTodo(long id, [FromBody] CompleteTodoDto dto) + { + try { await _svc.CompleteTodoAsync(GetUserId(), id, dto.HandleResult); return Ok(ApiResult.Ok(null, "已完成")); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + // 标记已读 + [HttpPut("messages/{id}/read")] + public async Task MarkRead(long id) + { + try { await _svc.MarkReadAsync(GetUserId(), id); return Ok(ApiResult.Ok(null)); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + // 全部已读 + [HttpPut("messages/read-all")] + public async Task MarkAllRead() + { + await _svc.MarkAllReadAsync(GetUserId()); + return Ok(ApiResult.Ok(null)); + } + + // 未读数量 + [HttpGet("unread-count")] + public async Task GetUnreadCount() + => Ok(ApiResult.Ok(new UnreadCountDto { Count = await _svc.GetUnreadCountAsync(GetUserId()) })); + + // 触发一条消息(模拟业务系统触发,用于演示/测试) + [HttpPost("trigger")] + public async Task Trigger([FromBody] TriggerDto dto) + { + try + { + var msgId = await _mediator.Send(new CreateNotificationCommand + { + NotificationTypeCode = dto.Code, + BusinessType = dto.BusinessType ?? "Manual", + BusinessId = dto.BusinessId ?? Guid.NewGuid().ToString("N"), + Title = dto.Title ?? "测试消息", + Content = dto.Content ?? "这是一条测试消息", + Priority = dto.Priority ?? NotificationPriority.Normal, + RoutePath = dto.RoutePath ?? "", + ForceReceivers = dto.AssigneeIds ?? new List(), + AssigneeIds = dto.AssigneeIds ?? new List(), + CreatorId = GetUserId(), + CreatorName = User.FindFirst("realName")?.Value ?? "系统" + }); + return Ok(ApiResult.Ok(new { id = msgId }, "消息已创建并即时推送给在线接收人")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + // 发送广播通知(拥有通知广播权限的用户可发送,空接收人 = 订阅广播) + [HttpPost("broadcast")] + public async Task Broadcast([FromBody] BroadcastNotificationDto dto) + { + try + { + var userId = GetUserId(); + var perms = await _authService.GetUserPermissionsAsync(userId); + if (!perms.Contains("notification:broadcast")) + return StatusCode(403, ApiResult.Fail("您没有发送广播通知的权限", 403)); + + var msgId = await _mediator.Send(new CreateNotificationCommand + { + NotificationTypeCode = "Broadcast", + BusinessType = "Broadcast", + BusinessId = Guid.NewGuid().ToString("N"), + Title = dto.Title, + Content = dto.Content, + Priority = dto.Priority ?? NotificationPriority.Normal, + RoutePath = dto.RoutePath ?? "", + ForceReceivers = new List(), + CreatorId = userId, + CreatorName = User.FindFirst("realName")?.Value ?? "系统" + }); + return Ok(ApiResult.Ok(new { id = msgId }, "广播消息已创建")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + private long GetUserId() => long.Parse(User.FindFirst("id")!.Value); +} + +public class TriggerDto +{ + public string Code { get; set; } = "TeyeFound"; + public string Title { get; set; } + public string Content { get; set; } + public string BusinessType { get; set; } + public string BusinessId { get; set; } + public NotificationPriority? Priority { get; set; } + public string RoutePath { get; set; } + public List AssigneeIds { get; set; } +} diff --git a/backend/F9WebAiTest.Api/Controllers/Prod/HbjyController.cs b/backend/F9WebAiTest.Api/Controllers/Prod/HbjyController.cs new file mode 100644 index 0000000..1967017 --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/Prod/HbjyController.cs @@ -0,0 +1,41 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Application.Services; +using F9WebAiTest.Application.Services.Prod; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.Prod; + +[ApiController] +[Route("api/hbjy")] +[Authorize] +public class HbjyController : ControllerBase +{ + private readonly IHbjyService _svc; + public HbjyController(IHbjyService svc) => _svc = svc; + + // 黑板检验列表 + [HttpGet] + public async Task GetPaged( + [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20, + [FromQuery] DateTime? date = null, [FromQuery] long? deptId = null) + => Ok(ApiResult>.Ok(await _svc.GetPagedAsync(pageIndex, pageSize, date, deptId))); + + // 获取检验编辑数据 + [HttpGet("{lsjlId}")] + public async Task GetEdit(long lsjlId) + { + try { return Ok(ApiResult.Ok(await _svc.GetEditAsync(lsjlId))); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + // 保存黑板检验 + [HttpPost] + public async Task Save([FromBody] HbjySaveDto dto) + { + try { await _svc.SaveAsync(dto); return Ok(ApiResult.Ok(null, "保存成功")); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/Prod/LsjlController.cs b/backend/F9WebAiTest.Api/Controllers/Prod/LsjlController.cs new file mode 100644 index 0000000..9e5f27c --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/Prod/LsjlController.cs @@ -0,0 +1,58 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos; +using F9WebAiTest.Application.Dtos.Base; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Application.Services; +using F9WebAiTest.Application.Services.Base; +using F9WebAiTest.Application.Services.Prod; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.Prod; + +[ApiController] +[Route("api/lsjl")] +[Authorize] +public class LsjlController : ControllerBase +{ + private readonly ILsjlService _svc; + private readonly IJizuChetaiService _jzSvc; + + public LsjlController(ILsjlService svc, IJizuChetaiService jzSvc) + { + _svc = svc; + _jzSvc = jzSvc; + } + + [HttpGet] + public async Task GetPaged( + [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20, + [FromQuery] DateTime? date = null, [FromQuery] long? deptId = null, [FromQuery] string keyword = "") + => Ok(ApiResult>.Ok(await _svc.GetPagedAsync(pageIndex, pageSize, date, deptId, keyword))); + + // 根据车间+机组查询车台号范围 + [HttpGet("chetai")] + public async Task GetChetai([FromQuery] long deptId, [FromQuery] int jizu) + => Ok(ApiResult.Ok(await _jzSvc.GetByDepIdAndJizuAsync(deptId, jizu))); + + [HttpPost] + public async Task Create([FromBody] LsjlCreateDto dto) + { + try { return Ok(ApiResult.Ok(await _svc.CreateAsync(dto), "创建成功")); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpPut("{id}")] + public async Task Update(long id, [FromBody] LsjlCreateDto dto) + { + try { return Ok(ApiResult.Ok(await _svc.UpdateAsync(id, dto), "更新成功")); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try { await _svc.DeleteAsync(id); return Ok(ApiResult.Ok(null, "删除成功")); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/Prod/XdjyController.cs b/backend/F9WebAiTest.Api/Controllers/Prod/XdjyController.cs new file mode 100644 index 0000000..0db64bd --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/Prod/XdjyController.cs @@ -0,0 +1,41 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Application.Services; +using F9WebAiTest.Application.Services.Prod; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.Prod; + +[ApiController] +[Route("api/xdjy")] +[Authorize] +public class XdjyController : ControllerBase +{ + private readonly IXdjyService _svc; + public XdjyController(IXdjyService svc) => _svc = svc; + + // 纤度检验列表(落丝记录 + 检验汇总) + [HttpGet] + public async Task GetPaged( + [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20, + [FromQuery] DateTime? date = null, [FromQuery] long? deptId = null) + => Ok(ApiResult>.Ok(await _svc.GetPagedAsync(pageIndex, pageSize, date, deptId))); + + // 获取检验编辑数据 + [HttpGet("{lsjlId}")] + public async Task GetEdit(long lsjlId) + { + try { return Ok(ApiResult.Ok(await _svc.GetEditAsync(lsjlId))); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + // 保存纤度检验 + [HttpPost] + public async Task Save([FromBody] XdjySaveDto dto) + { + try { await _svc.SaveAsync(dto); return Ok(ApiResult.Ok(null, "保存成功")); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/SysDepartmentController.cs b/backend/F9WebAiTest.Api/Controllers/SysDepartmentController.cs new file mode 100644 index 0000000..a68927b --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/SysDepartmentController.cs @@ -0,0 +1,55 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Common.Exceptions; +using F9WebAiTest.Application.CQRS.SYS.Departments.Commands; +using F9WebAiTest.Application.CQRS.SYS.Departments.Queries; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers; + +/// 系统部门管理(DDD + CQRS) +[ApiController] +[Route("api/sys/department")] +[Authorize] +public class SysDepartmentController : ControllerBase +{ + private readonly IMediator _mediator; + public SysDepartmentController(IMediator mediator) => _mediator = mediator; + + [HttpGet] + public async Task GetAll() + => Ok(ApiResult.Ok(await _mediator.Send(new GetDepartmentAllQuery()))); + + [HttpPost] + public async Task Create([FromBody] Department_CreateCommand command) + { + try { return Ok(ApiResult.Ok(new { id = await _mediator.Send(command) }, "创建成功")); } + catch (F9NotFoundException ex) { return NotFound(ApiResult.Fail(ex.Message, 404)); } + catch (F9OperationException ex) { return BadRequest(ApiResult.Fail(ex.Message, 400)); } + } + + [HttpPut("{id}")] + public async Task Update(long id, [FromBody] Department_UpdateCommand command) + { + try { return Ok(ApiResult.Ok(await _mediator.Send(command with { Id = id }), "修改成功")); } + catch (F9NotFoundException ex) { return NotFound(ApiResult.Fail(ex.Message, 404)); } + catch (F9OperationException ex) { return BadRequest(ApiResult.Fail(ex.Message, 400)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try { return Ok(ApiResult.Ok(await _mediator.Send(new Department_DelCommand(id)), "删除成功")); } + catch (F9NotFoundException ex) { return NotFound(ApiResult.Fail(ex.Message, 404)); } + catch (F9OperationException ex) { return BadRequest(ApiResult.Fail(ex.Message, 400)); } + } + + [HttpPut("{id}/active")] + public async Task ActiveOrDeactivate(long id, [FromBody] Department_ActiveOrDeactivateCommand command) + { + try { return Ok(ApiResult.Ok(await _mediator.Send(command with { Id = id }), "操作成功")); } + catch (F9NotFoundException ex) { return NotFound(ApiResult.Fail(ex.Message, 404)); } + catch (F9OperationException ex) { return BadRequest(ApiResult.Fail(ex.Message, 400)); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/SysMenuController.cs b/backend/F9WebAiTest.Api/Controllers/SysMenuController.cs new file mode 100644 index 0000000..66918fe --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/SysMenuController.cs @@ -0,0 +1,68 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Common.Exceptions; +using F9WebAiTest.Application.CQRS.SYS.RBAC.Menus.Commands; +using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Menus; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers; + +/// 系统菜单管理(DDD + CQRS) +[ApiController] +[Route("api/sys/menu")] +[Authorize] +public class SysMenuController : ControllerBase +{ + private readonly IMediator _mediator; + private readonly IMenuReadRepository _menuReadRepository; + public SysMenuController(IMediator mediator, IMenuReadRepository menuReadRepository) + { + _mediator = mediator; + _menuReadRepository = menuReadRepository; + } + + [HttpGet("tree")] + public async Task GetTree() + => Ok(ApiResult.Ok(await _menuReadRepository.GetTreeMenuAsync())); + + [HttpGet] + public async Task GetAll([FromQuery] string like = "", [FromQuery] long? parentId = null) + => Ok(ApiResult.Ok(await _menuReadRepository.GetAllMenuAsync(like, parentId))); + + [HttpGet("{id}")] + public async Task GetById(long id) + => Ok(ApiResult.Ok(await _menuReadRepository.GetMenuByIdForEditAsync(id))); + + [HttpPost] + public async Task Create([FromBody] Menu_CreateCommand command) + { + try { return Ok(ApiResult.Ok(new { id = await _mediator.Send(command) }, "创建成功")); } + catch (F9NotFoundException ex) { return NotFound(ApiResult.Fail(ex.Message, 404)); } + catch (F9OperationException ex) { return BadRequest(ApiResult.Fail(ex.Message, 400)); } + } + + [HttpPut("{id}")] + public async Task Update(long id, [FromBody] Menu_UpdateCommand command) + { + try { return Ok(ApiResult.Ok(await _mediator.Send(command with { Id = id }), "修改成功")); } + catch (F9NotFoundException ex) { return NotFound(ApiResult.Fail(ex.Message, 404)); } + catch (F9OperationException ex) { return BadRequest(ApiResult.Fail(ex.Message, 400)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try { return Ok(ApiResult.Ok(await _mediator.Send(new Menu_DelCommand(id)), "删除成功")); } + catch (F9NotFoundException ex) { return NotFound(ApiResult.Fail(ex.Message, 404)); } + catch (F9OperationException ex) { return BadRequest(ApiResult.Fail(ex.Message, 400)); } + } + + [HttpPut("{id}/active")] + public async Task ActiveOrDeactivate(long id, [FromBody] Menu_ActiveOrDeactivateCommand command) + { + try { return Ok(ApiResult.Ok(await _mediator.Send(command with { Id = id }), "操作成功")); } + catch (F9NotFoundException ex) { return NotFound(ApiResult.Fail(ex.Message, 404)); } + catch (F9OperationException ex) { return BadRequest(ApiResult.Fail(ex.Message, 400)); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/SysRoleController.cs b/backend/F9WebAiTest.Api/Controllers/SysRoleController.cs new file mode 100644 index 0000000..6a8b5e3 --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/SysRoleController.cs @@ -0,0 +1,80 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Common.Exceptions; +using F9WebAiTest.Application.CQRS.SYS.RBAC.Roles.Commands; +using F9WebAiTest.Application.CQRS.SYS.RBAC.Roles.Queries; +using F9WebAiTest.Domain.Enums.SYS.RBAC; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers; + +/// 系统角色管理(DDD + CQRS) +[ApiController] +[Route("api/sys/role")] +[Authorize] +public class SysRoleController : ControllerBase +{ + private readonly IMediator _mediator; + public SysRoleController(IMediator mediator) => _mediator = mediator; + + [HttpGet] + public async Task GetAll() + => Ok(ApiResult.Ok(await _mediator.Send(new GetRoleAllQuery()))); + + [HttpGet("{id}")] + public async Task GetById(long id) + => Ok(ApiResult.Ok(await _mediator.Send(new GetRoleByIdForEditQuery(id)))); + + [HttpGet("{id}/permissions")] + public async Task GetPermissions(long id, [FromQuery] Resource_Type_Enum resourceType) + => Ok(ApiResult.Ok(await _mediator.Send(new GetRolePermissionsByResourceTypeQuery(id, resourceType)))); + + [HttpPost] + public async Task Create([FromBody] Role_CreateCommand command) + { + try { return Ok(ApiResult.Ok(new { id = await _mediator.Send(command) }, "创建成功")); } + catch (F9NotFoundException ex) { return NotFound(ApiResult.Fail(ex.Message, 404)); } + catch (F9OperationException ex) { return BadRequest(ApiResult.Fail(ex.Message, 400)); } + } + + [HttpPut("{id}")] + public async Task Update(long id, [FromBody] Role_UpdateCommand command) + { + try { return Ok(ApiResult.Ok(await _mediator.Send(command with { Id = id }), "修改成功")); } + catch (F9NotFoundException ex) { return NotFound(ApiResult.Fail(ex.Message, 404)); } + catch (F9OperationException ex) { return BadRequest(ApiResult.Fail(ex.Message, 400)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try { return Ok(ApiResult.Ok(await _mediator.Send(new Role_DelCommand(id)), "删除成功")); } + catch (F9NotFoundException ex) { return NotFound(ApiResult.Fail(ex.Message, 404)); } + catch (F9OperationException ex) { return BadRequest(ApiResult.Fail(ex.Message, 400)); } + } + + [HttpPut("{id}/active")] + public async Task ActiveOrDeactivate(long id, [FromBody] Role_ActiveOrDeactivateCommand command) + { + try { return Ok(ApiResult.Ok(await _mediator.Send(command with { Id = id }), "操作成功")); } + catch (F9NotFoundException ex) { return NotFound(ApiResult.Fail(ex.Message, 404)); } + catch (F9OperationException ex) { return BadRequest(ApiResult.Fail(ex.Message, 400)); } + } + + [HttpPost("{id}/permissions")] + public async Task AssignPermission(long id, [FromBody] RolePermission_AssignCommand command) + { + try { return Ok(ApiResult.Ok(await _mediator.Send(command with { RoleId = id }), "绑定成功")); } + catch (F9NotFoundException ex) { return NotFound(ApiResult.Fail(ex.Message, 404)); } + catch (F9OperationException ex) { return BadRequest(ApiResult.Fail(ex.Message, 400)); } + } + + [HttpDelete("{id}/permissions/{rpId}")] + public async Task RemovePermission(long id, long rpId, [FromQuery] Resource_Type_Enum resourceType) + { + try { return Ok(ApiResult.Ok(await _mediator.Send(new RolePermission_RemoveCommand(rpId, resourceType)), "解绑成功")); } + catch (F9NotFoundException ex) { return NotFound(ApiResult.Fail(ex.Message, 404)); } + catch (F9OperationException ex) { return BadRequest(ApiResult.Fail(ex.Message, 400)); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/SysUserController.cs b/backend/F9WebAiTest.Api/Controllers/SysUserController.cs new file mode 100644 index 0000000..82d52e3 --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/SysUserController.cs @@ -0,0 +1,87 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Common.Exceptions; +using F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Commands; +using F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Queries; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers; + +/// 系统用户管理(DDD + CQRS) +[ApiController] +[Route("api/sys/user")] +[Authorize] +public class SysUserController : ControllerBase +{ + private readonly IMediator _mediator; + public SysUserController(IMediator mediator) => _mediator = mediator; + + [HttpGet] + public async Task GetPaged([FromQuery] long departmentId = 0, [FromQuery] string like = "", [FromQuery] int page = 1, [FromQuery] int pageSize = 10) + => Ok(ApiResult.Ok(await _mediator.Send(new GetUserPagedQuery(departmentId, like, page, pageSize)))); + + [HttpGet("{id}")] + public async Task GetById(long id) + => Ok(ApiResult.Ok(await _mediator.Send(new GetUserByIdQuery(id)))); + + [HttpGet("{id}/roles")] + public async Task GetUserRoles(long id) + => Ok(ApiResult.Ok(await _mediator.Send(new GetUserRolesByUserIdQuery(id)))); + + [HttpPost] + public async Task Create([FromBody] User_CreateCommand command) + { + try { return Ok(ApiResult.Ok(new { id = await _mediator.Send(command) }, "创建成功")); } + catch (F9NotFoundException ex) { return NotFound(ApiResult.Fail(ex.Message, 404)); } + catch (F9OperationException ex) { return BadRequest(ApiResult.Fail(ex.Message, 400)); } + } + + [HttpPut("{id}")] + public async Task Update(long id, [FromBody] User_UpdateCommand command) + { + try { return Ok(ApiResult.Ok(await _mediator.Send(command with { Id = id }), "修改成功")); } + catch (F9NotFoundException ex) { return NotFound(ApiResult.Fail(ex.Message, 404)); } + catch (F9OperationException ex) { return BadRequest(ApiResult.Fail(ex.Message, 400)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try { return Ok(ApiResult.Ok(await _mediator.Send(new User_DelCommand(id)), "删除成功")); } + catch (F9NotFoundException ex) { return NotFound(ApiResult.Fail(ex.Message, 404)); } + catch (F9OperationException ex) { return BadRequest(ApiResult.Fail(ex.Message, 400)); } + } + + [HttpPut("{id}/active")] + public async Task ActiveOrDeactivate(long id, [FromBody] User_ActiveOrDeactivateCommand command) + { + try { return Ok(ApiResult.Ok(await _mediator.Send(command with { Id = id }), "操作成功")); } + catch (F9NotFoundException ex) { return NotFound(ApiResult.Fail(ex.Message, 404)); } + catch (F9OperationException ex) { return BadRequest(ApiResult.Fail(ex.Message, 400)); } + } + + [HttpPut("{id}/reset-password")] + public async Task ResetPassword(long id) + { + try { return Ok(ApiResult.Ok(await _mediator.Send(new User_ResetPasswordCommand(id)), "密码已重置")); } + catch (F9NotFoundException ex) { return NotFound(ApiResult.Fail(ex.Message, 404)); } + catch (F9OperationException ex) { return BadRequest(ApiResult.Fail(ex.Message, 400)); } + } + + [HttpPost("{id}/roles")] + public async Task AssignRole(long id, [FromBody] UserRole_AssignCommand command) + { + try { return Ok(ApiResult.Ok(await _mediator.Send(command with { UserId = id }), "绑定成功")); } + catch (F9NotFoundException ex) { return NotFound(ApiResult.Fail(ex.Message, 404)); } + catch (F9OperationException ex) { return BadRequest(ApiResult.Fail(ex.Message, 400)); } + } + + [HttpDelete("{id}/roles/{roleId}")] + public async Task RemoveRole(long id, long roleId) + { + try { return Ok(ApiResult.Ok(await _mediator.Send(new UserRole_RemoveCommand(id, roleId)), "解绑成功")); } + catch (F9NotFoundException ex) { return NotFound(ApiResult.Fail(ex.Message, 404)); } + catch (F9OperationException ex) { return BadRequest(ApiResult.Fail(ex.Message, 400)); } + } +} diff --git a/backend/F9WebAiTest.Api/F9WebAiTest.Api.csproj b/backend/F9WebAiTest.Api/F9WebAiTest.Api.csproj new file mode 100644 index 0000000..a623f35 --- /dev/null +++ b/backend/F9WebAiTest.Api/F9WebAiTest.Api.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + disable + F9WebAiTest.Api + + + + + + + + + + + + + + + diff --git a/backend/F9WebAiTest.Api/Hubs/NotificationHub.cs b/backend/F9WebAiTest.Api/Hubs/NotificationHub.cs new file mode 100644 index 0000000..1996003 --- /dev/null +++ b/backend/F9WebAiTest.Api/Hubs/NotificationHub.cs @@ -0,0 +1,47 @@ +using System.Collections.Concurrent; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.SignalR; + +namespace F9WebAiTest.Api.Hubs; + +/// 在线用户登记表(用于判断用户是否在线,离线则走"拉取兜底") +public static class OnlineUserRegistry +{ + private static readonly ConcurrentDictionary _users = new(); + + public static void Add(long userId) => _users[userId] = 0; + + public static void Remove(long userId) => _users.TryRemove(userId, out _); + + public static bool IsOnline(long userId) => _users.ContainsKey(userId); +} + +/// 通知推送 Hub(客户端连接后按 UserId 标识,服务端用 Clients.User(userId) 定向推送) +public class NotificationHub : Hub +{ + public override async Task OnConnectedAsync() + { + var userId = GetUserId(); + if (userId > 0) OnlineUserRegistry.Add(userId); + await base.OnConnectedAsync(); + } + public override async Task OnDisconnectedAsync(Exception exception) + { + var userId = GetUserId(); + if (userId > 0) OnlineUserRegistry.Remove(userId); + await base.OnDisconnectedAsync(exception); + } + + private long GetUserId() + { + var raw = Context.User?.FindFirst("id")?.Value; + return long.TryParse(raw, out var id) ? id : 0; + } +} + +/// 自定义用户标识:使用 JWT 中的 "id" 声明作为 SignalR 的 UserIdentifier +public class NotificationUserIdProvider : IUserIdProvider +{ + public string GetUserId(HubConnectionContext connection) + => connection.User?.FindFirst("id")?.Value ?? connection.ConnectionId; +} diff --git a/backend/F9WebAiTest.Api/JsonConverters/LongToStringJsonConverter.cs b/backend/F9WebAiTest.Api/JsonConverters/LongToStringJsonConverter.cs new file mode 100644 index 0000000..c6865ca --- /dev/null +++ b/backend/F9WebAiTest.Api/JsonConverters/LongToStringJsonConverter.cs @@ -0,0 +1,19 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace F9WebAiTest.Api.JsonConverters; + +/// +/// 将 long 序列化为字符串,避免雪花 ID(超过 JS Number 安全整数 2^53)在前后端传递时丢失精度。 +/// 反序列化时兼容字符串与数字两种形式。 +/// +public class LongToStringJsonConverter : JsonConverter +{ + public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.TokenType == JsonTokenType.String + ? long.Parse(reader.GetString()!) + : reader.GetInt64(); + + public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString()); +} diff --git a/backend/F9WebAiTest.Api/Program.cs b/backend/F9WebAiTest.Api/Program.cs new file mode 100644 index 0000000..7fb8ee8 --- /dev/null +++ b/backend/F9WebAiTest.Api/Program.cs @@ -0,0 +1,112 @@ +using System.Text; +using FreeSql; +using F9WebAiTest.Api.Hubs; +using F9WebAiTest.Api.Services; +using F9WebAiTest.Application; +using F9WebAiTest.Application.Services; +using F9WebAiTest.Application.Services.Notice; +using F9WebAiTest.Infrastructure; +using F9WebAiTest.Infrastructure.Data; +using MediatR; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.SignalR; +using Microsoft.IdentityModel.Tokens; + +var builder = WebApplication.CreateBuilder(args); + +// 分层注册 +builder.Services.AddInfrastructure(builder.Configuration); +builder.Services.AddApplication(); + +// JWT 认证 +var jwtKey = builder.Configuration["Jwt:Key"]; +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = builder.Configuration["Jwt:Issuer"], + ValidAudience = builder.Configuration["Jwt:Audience"], + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)) + }; + // SignalR 通过 query string 传递 token,需从 access_token 读取 + options.Events = new JwtBearerEvents + { + OnMessageReceived = context => + { + var accessToken = context.Request.Query["access_token"]; + if (!string.IsNullOrEmpty(accessToken) && context.HttpContext.Request.Path.StartsWithSegments("/hubs")) + { + context.Token = accessToken; + } + return Task.CompletedTask; + } + }; + }); +builder.Services.AddAuthorization(); + +builder.Services.AddControllers() + .AddJsonOptions(options => + { + // long 序列化为字符串,避免雪花 ID 超出 JS 安全整数范围导致精度丢失 + options.JsonSerializerOptions.Converters.Add(new F9WebAiTest.Api.JsonConverters.LongToStringJsonConverter()); + }); + +// SignalR 实时推送 + 自定义用户标识 + 即时推送器(创建消息时直接推送,无后台投递服务) +builder.Services.AddSignalR(); +builder.Services.AddSingleton(); +builder.Services.AddScoped(); + +// 业务状态变更事件 → SignalR 推送(多端状态同步) +builder.Services.AddScoped, BusinessChangedEventHandler>(); + +// CORS +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowAll", policy => + policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()); +}); + +// NSwag(OpenAPI 文档 + Swagger UI + 可生成 TS/C# 客户端) +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddOpenApiDocument(settings => +{ + settings.Title = "企业 RBAC 管理系统 API"; + settings.Version = "v1"; + settings.Description = "基于 NSwag 生成的 OpenAPI 文档,可用于生成 TypeScript/C# 客户端"; + settings.AddSecurity("Bearer", new NSwag.OpenApiSecurityScheme + { + Type = NSwag.OpenApiSecuritySchemeType.Http, + Scheme = "bearer", + BearerFormat = "JWT", + Description = "请输入 access token(格式:Bearer {token})" + }); + settings.OperationProcessors.Add(new NSwag.Generation.Processors.Security.AspNetCoreOperationSecurityScopeProcessor("Bearer")); +}); + +var app = builder.Build(); + +// 初始化数据库(FreeSql CodeFirst 建表 + 种子) +using (var scope = app.Services.CreateScope()) +{ + var fsql = scope.ServiceProvider.GetRequiredService(); + var snowflake = scope.ServiceProvider.GetRequiredService(); + DbSeeder.Seed(fsql, snowflake); +} + +// NSwag 中间件(swagger 文档 + UI) +app.UseOpenApi(); +app.UseSwaggerUi(); + +app.UseCors("AllowAll"); +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); +app.MapHub("/hubs/notification"); + +app.Run(); diff --git a/backend/F9WebAiTest.Api/Properties/launchSettings.json b/backend/F9WebAiTest.Api/Properties/launchSettings.json new file mode 100644 index 0000000..348c87c --- /dev/null +++ b/backend/F9WebAiTest.Api/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "F9WebAiTest.Api": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:3000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/backend/F9WebAiTest.Api/Services/BusinessChangedEventHandler.cs b/backend/F9WebAiTest.Api/Services/BusinessChangedEventHandler.cs new file mode 100644 index 0000000..acc7062 --- /dev/null +++ b/backend/F9WebAiTest.Api/Services/BusinessChangedEventHandler.cs @@ -0,0 +1,22 @@ +using F9WebAiTest.Api.Hubs; +using F9WebAiTest.Application.Services; +using MediatR; +using Microsoft.AspNetCore.SignalR; + +namespace F9WebAiTest.Api.Services; + +/// +/// 业务状态变更事件的 SignalR 推送处理器。 +/// 业务服务发布 BusinessChangedEvent 后,推送给所有在线客户端, +/// 各端监听 "BusinessChanged" 后刷新已打开的相关页面,实现多端实时同步。 +/// +public class BusinessChangedEventHandler : INotificationHandler +{ + private readonly IHubContext _hubContext; + + public BusinessChangedEventHandler(IHubContext hubContext) + => _hubContext = hubContext; + + public Task Handle(BusinessChangedEvent e, CancellationToken ct) + => _hubContext.Clients.All.SendAsync("BusinessChanged", e, ct); +} diff --git a/backend/F9WebAiTest.Api/Services/NotificationDeliveryService.cs b/backend/F9WebAiTest.Api/Services/NotificationDeliveryService.cs new file mode 100644 index 0000000..18e2a8b --- /dev/null +++ b/backend/F9WebAiTest.Api/Services/NotificationDeliveryService.cs @@ -0,0 +1,10 @@ +// 已移除:消息投递不再使用后台服务(BackgroundService)与轮询。 +// +// 现在的投递方式(即时推送): +// CreateNotificationCommandHandler(F9WebAiTest.Application/Services/Notice/NotificationMediatR.cs) +// 在消息创建后立即: +// 1. 确定接收人(ForceReceivers 精确送达,否则按消息类型订阅关系广播); +// 2. 写入 NotificationReceiver(MessageId + UserId 幂等); +// 3. 通过 INotificationPusher(实现在 F9WebAiTest.Api/Services/NotificationPusher.cs)经 SignalR 直接推送。 +// +// 本文件已无任何代码,可直接删除。 diff --git a/backend/F9WebAiTest.Api/Services/NotificationPusher.cs b/backend/F9WebAiTest.Api/Services/NotificationPusher.cs new file mode 100644 index 0000000..c7709cd --- /dev/null +++ b/backend/F9WebAiTest.Api/Services/NotificationPusher.cs @@ -0,0 +1,45 @@ +using F9WebAiTest.Api.Hubs; +using F9WebAiTest.Application.Dtos.Notice; +using F9WebAiTest.Application.Services.Notice; +using Microsoft.AspNetCore.SignalR; + +namespace F9WebAiTest.Api.Services; + +/// +/// 消息实时推送器实现:基于 SignalR 的 IHubContext 直接推送。 +/// 无后台队列、无轮询——消息创建时同步完成投递与推送。 +/// +/// 在线判定:OnlineUserRegistry(Hub 连接/断开时登记的内存表)。 +/// 之所以需要显式判定而不能依赖推送异常:SignalR 对"无连接的 Clients.User 调用"是静默成功的, +/// 不判定的话离线用户会被误记为推送成功。 +/// +/// 用户标识:由 NotificationUserIdProvider 统一取 JWT 的 "id" 声明, +/// 因此 Clients.User(userId) 可精准命中该用户的所有在线连接(支持多端同时在线)。 +/// +public class NotificationPusher : INotificationPusher +{ + private readonly IHubContext _hubContext; + + public NotificationPusher(IHubContext hubContext) + { + _hubContext = hubContext; + } + + /// + public bool IsOnline(long userId) => OnlineUserRegistry.IsOnline(userId); + + /// + public async Task PushNewMessageAsync(long userId, NotificationMessageDto message, CancellationToken ct = default) + { + // 离线用户:不推送(消息已持久化,上线后拉取兜底),返回 false 由调用方标记 IsOffline + if (!OnlineUserRegistry.IsOnline(userId)) return false; + + await _hubContext.Clients.User(userId.ToString()).SendAsync("ReceiveNotification", message, ct); + return true; + } + + /// + public Task PushUnreadCountAsync(long userId, int unreadCount, int todoCount, CancellationToken ct = default) + => _hubContext.Clients.User(userId.ToString()) + .SendAsync("UnreadCountChanged", new { unreadCount, todoCount }, ct); +} diff --git a/backend/F9WebAiTest.Api/appsettings.json b/backend/F9WebAiTest.Api/appsettings.json new file mode 100644 index 0000000..57f452d --- /dev/null +++ b/backend/F9WebAiTest.Api/appsettings.json @@ -0,0 +1,26 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Kestrel": { + "Endpoints": { + "Http": { + "Url": "http://0.0.0.0:3000" + } + } + }, + "Jwt": { + "Key": "f9-web-ai-test-rbac-secret-key-2026-please-change-in-production", + "Issuer": "F9WebAiTest", + "Audience": "F9WebAiTestClient", + "AccessTokenExpireMinutes": 120, + "RefreshTokenExpireDays": 7 + }, + "ConnectionStrings": { + "Default": "Data Source=f9_web_ai_test_rbac.db" + } +} diff --git a/backend/F9WebAiTest.Api/f9_web_ai_test_rbac.db b/backend/F9WebAiTest.Api/f9_web_ai_test_rbac.db new file mode 100644 index 0000000000000000000000000000000000000000..8b5a56ac03164831fc868a27e198a9ca2d90b76c GIT binary patch literal 352256 zcmeFa34B|}c`u5M1V92fT1Zi}ECV7J&IiY)!-JFI*@<{$*Ft0>9M5=gcMNp34Rr;E+S+@%0yPUAwvl0)72Mfxh9Mp1{6=?%uY6gMqzW2Lo-xL;c--K&!W_Z)h8d z)Da5@=fk14`IQ1VY65)+ z+6Fpyw++u4$sbqW3k}uM7Sq99hr$B5&DNw3JvLVLk`caBIIZ!I2&3hMD0Lu zIucreBM!3yFW?eAy^`o8&%LOkoym07&Ad~fUm-X5NY3;_HOhJ&$! zAhm~QLy?hSBorwaWDj^kh>0i^+CkbSXeXkmOl5}oag#OJ1i zrwa$|n1osc;l9_|STPlVg% z=3>zk;aHqJc83o4$3o#)Uo_L#sq4^iU-$muE>PCau2Esnzq?2vS(F3``3# z8JrK!FT}GHB$N6;cqSM-Ry0UPla6X6*?~J77aLOmNbU@so-0r@0P3;%p$Mut3U!3KW<$jT41|O6=xqLMDVD1p3$YmVhE`Gx zL$%V-(6c%E#K7e`LPP>|p*+<~8PV<^r?89A)~MD&~E@ zAN#KQjxyi&UGQD@z2rOPV|@d@j^AKFXwoPG6ak6=MSvne5ugZA1SkR&0g3=c03)!r z(!SN2u|vk`t+M;AL$LL*dpTHhjJb$BZ5e)sON zsnb32spOWUi&WVa2#zLOK}`r7>kd`B6Em)ynJmrB-7YU zF%X>&#~o`u@M<7@G!~9e4n>cJXX6XZU-|5IzYY2KhG!SzmakWq*_$)OLR)O&C0;i` zWRQz*XIzk)J&`jD9h2etU?jegal(tv@EmNqnSre@@!M`HwJSD#XLBd)C!C6$nT(9V z9^1I(D{D(Ykc?f=_Vo^Xqcv-%q}SqE0~jM9Fvf11y~>Kktan@CCr(vq8Ol|<{*QH= zeo+J{0u%v?07ZZzKoOt_Py{Ff6ak6=Mc_9T0kr?$Sw3W8_V^z5E_=H?&z1ia9_fc7 zKoOt_Py{Ff6ak6=MSvne5m+Sxi7n1GmW~dWwOSCoez(={?ciBnzMa)>=fnUfwy;tQ zFE>dtFDW9IiZSFe01nYbq_gw(?E<|`?{vMu;8`NroL?|n0Q&xN|= z#U~b@crkhYb7m;QHAPxb61AnKjB9gc6j~&ug%g@&nUz^Z$|5ATh}Gyf`>odMluoi*fRkG!u0>KIQzaIJG*Ka5Joq-rppdef z1EmpJV2wzeY|41;iFcO2{AE*8oB!&Uo=iUQpb=8%oFdQh3B{Hnr@zrGJ0!HIp?n2V z6jji&2(c{!-y{mEsB%){(&OjT%8njcZIM!X!kTg}-u-;?!lNMPwcmR!`Sf$PR1sQw z_`=d-uU@_UK=S~RD(H}5`5{I5219Lg6jaMq`q{2@}qYf5p$UFs@MCRrL26+!ff2^x$ICo;GQjG07n}%C6c`p5y>xw! zfQ;5zRZ&^qnHrFogQ-Hn1(7@MF2pi5u9sz56|!iMp|_~2k|va{%+d(X3X(YIN@Lk( z(oG+v3!=aYajY#?|R_Ng0e4 zfrnf(l4?@I<91GCdBLs^4Q)9JloJ?6;jlqpS6XYK5x5|+te7>9mGlv~2!nGjZ4`}| zlA(2KWLjiA4bgewcWfX+o6Bv5L>CxJ3ouu~oB2!9p%0~5QPZTfBxiFkJT68tMSjkT z0f5Q7^ir#{AOm@ry<>TEHo@f1X-y(b^RSw_VpfB!Bs3_71l3Auhh^4oWOXiIed+@= zQ4q5D0`Q#Nq$!~9f^1B)6v}Bcte{9}|9=^C#R7Kuf6V-UOp^I)=Fge`oB0~^`|y%} zC;}7#iU37`B0v$K2v7tl0u%v?07ZZzKoR&gBT(wFx~hY2sl#ox!lT3O#`gccuC*5Cuq`$knSAh_r7u0cc=_Y7FZug#U%dA8 z-QR!v-sGi3^8D*4?23U1i8409draY?(c;2!#f0OP48Ds`zq$PIi%YNEyZFW%$)}%A zUb!0ty?XgB*idbX`zuEvLT_RdTsdQDBZ#gfbWxe{@}7xR^3t=(S6|g-KL5(fGOs2A zk$E$lXe=W@yQ`H*q$tq5>ill<`76n1K9^4k@b-3)nS{Wo-m9bf?4yg%etGGs4=+A* zFPI4C`Y;8L<4VP6YEP2 z#FYK}iwh(aL5!@>qC71Og^`XIJ4Z0*b@5ze|Y{F(EkhrU*9XtF*8ZtGb zPRPtC9&cDd>Vr#i9w>4>k}te;?b%N(z4Y0QQ0}dU78fn|iYkg)x)x_iVYHwHkk1@PVaiFCP7ZxeT(YUY~M?^AlhQL1ddnv9+@m3*6eJyPaRb z&s8~s%As0 zN?!O|+gdH?W4vhnt-J7ZC{brkV-H*rW6a)Fh&?BZFum8z^Q{6Q(kkh$NL*%9E2qEF z`M0wAv!=Vv7W^?1z!(uWw(3bRUbHGCMhS#>FyBS7WHFe~_>sGtW_&A)AOKB1zv>Kj$aVyTPx>2`4?q(9B-}EW%!An10&Vw#Z#@ri}=N{ zZL3X~qUi}!*w$N+ynpydJAQ$9%)QWB3AtVWkN>T817Wh`XNU*Nk5c@ib@(sw^RdQN zGc!^q12diIKkMjiAlcqpWu%x0(|UlQ*E&p|54|3b*NR$(r`jmQ(S zt-~aJciy&Y@}f>2%Jt294*v@v0;r{TH0-P-u7U>M7y2tuUpU zRa1gRJ!qy1Qfl9#Q(6nDLrr?Y)X5rd&gC}Gj-L``Z(U2YXKz5KfhzyvG*nDOYJ(E9 zRWC6=BO1`Uj$~%Lf7JphIM&byksoNSB89Yl(`p4`i9kY|yVa^!>(!((+;fwwCId9$3UVcgKcQlB{m%=N2p$(9K2NP*g1bfzYsz9scb~ zJv#0VLEmeo=ouyA2YQKc5qh@P(KxBiMm~l!pr;}woYU$m(n7Hu7EWp_-|<6TBg=>j zM2}!%ALv=nSaw)kF4A=?mQ+kDqp18Cld!aUh-(kt*|TnyvMHLT(<|HYV;y`iNw~dr zG=K^I_O7)l9HHthoDB*bE=JjqUT${$6RDmYjwA2yT9YAGF+#FV$i3sIx>$NO=Qw<5 zm`TY~6nRA>WYUX`*~?u==$5qV`FXI*w`%FgpoedB?f8X`eFe!-=xh#=-9Rqf|0|{! zU<(i@GOY}yYAiyo)aJ5#Jc zDKe}gLyLt9)c5oZa1a;D71`-x=xX6|Wl+f|SKlYSkJeHmRZh9;gq>wMoTB0*Xk;q?f0{ z0|A9>Ge8B3d~zEHDB4n@5x&KQ9lnM4!xzt?^?$cz!s1iCi(aTs4Fnk*51<m_mJMQ3oa5fZ-<&jJ$ zv1=hR5v~dJ9cUZq*xfcz&r4E+;S~#pig^u=M`PgM**}cXF;c8Y-WLhZj!y;)d3Oej zcy`4?3xy&MLV5~%?#Gu^7L;jEBs>Kna!7m28txf&IjgpAwLTb4R}l&=R;iCn!B`fGaWMXo~usZkE zkkx}0CEF8(OL%7|_CNuP2O~tF6|y#8loc=;jK&~g*|n@*=k$hWqFGfL6z#iNr?aZD z(R#587wxQzYO@|)nZ-NH_r@;Z?cr(20KorXI2bDkQhRtd6d4IdLXm<&_JAjZn216} zo3CLwG`Rp(#op*bN3>7_VFu|7&J<`v_Qj&1g>iJ_ac3kxHyu1(IB3Tt)FL2nK4E@U zjnH+aI}WYE8Jv(Tx}Y+NO{LDN{swDeZ)7$UKGi*X7+*SVUMWo;yF-WjV<4b}?^R_M$q znNOve>YV9$V{5aK@37L-^uBdWC*4Lysqw30FUemD8 zR*Kr+gIL+)$EPFn;lpvH?jUv6qdi06vpnl(p)J`4F&oZxS)EnQ&DIMVrkNv4&I?-x z-I^T#|L}#$M7v;#8H`NKcF$%O8eL_PSN}p*iA(eAo-I}n>amLj$*by65u)+`M$aY- z^G)Uwb2syThGU$*zwv$1cfW7Ur}}&_EAY1WQSYSpx4ade?|HuJdBQX6>4FIKLlK|| zPy{Ff6ak6=MSvne5ugbCCLvJ2(T$cW(RyO;YNFC4zyhVlay6ylQ(LRtXlb-?NEQs1 z*OcrJ)o&;uJYS;W=&#B)ooY@sKPW>lVZAEnz~oTR|Pz6s8c(rqgql9x2N^1_dD+)6?gN;fA`cgTwsp-0Dz%?=R#XE&(&Ap@_WILf!;l)R}AKls!Wj0_cu1gsER29 zF4`nnto**m{fxR&9}JO=lm#k>gCTt{WdZ*}Wg@Y8z4J!y1)sfT9o}7W?t@sBGJ4J# z50#lJmEm_|tCb;Y5!KSi1UW4Ol)30y*bNQMa;4EM8|31LhvVTGT4guiROlGx>}4`T zm~k@3De?LmgBEl*H>|;h`<~1kiJLUm=g$e)cylA~tu_;wcVF7RU_5>@igsS-+qDMU zQ>F`#X!;`K$51xpkK1-42peSb`|X0=AmIXjMMgJWGr`Doeu}GVB?=9I^qQ#H`oEQV z)&hU@LlK||Py{Ff6ak6=MSvne5ugZA1SkR&0gAw{1A(A*t)-*G1=su``!%qXpKUvk z=UX_nNfk9oguNcel9Ho z$IAS|0)O;F5ugZA1SkR&0g3=cfFeK>8y2? z5m=e!6mjkCXD#cHfr6FOfPyP)ni9XeItHSqu!<@%O^N@q+6Zvgo|k;LC;s#5BobAP zl{jxx;&)cbK$QeAK<{Zvd~mf9I0cT?mp3KOtTKY4NQx+zv5C`W1S^8p4f>H!F+@dx zO9^=Q+OexBD6GJ-Lg}_` zY+_-RQ6xBD&#NVNJDZqaZ4?fUTtVlmLGVXw_0v1{d)`Z-0CsL0CIw?3Ra`!dI}tj)d*64ybjy7d-|*OfPx#*4~rm~`EGAYCvJ zuc+eH%g-);^4{eyed^lduWOpNS*gPj{e@-HI1MhS7ZyMA`qIM}l9yiKL99*AZz#8} z98qLdL0bIeUDrN+KSTsN5OG?mT`?l7DIzB>oxikv|4S$$80NmIWak|<=XFY73AGK$`MM_{GsDlD~dni2iV z{W^CEqL+O84*$y0IUcNV8{F^Gql+l|De)E@y_o8#GB1ki)yuChKK?~msAO&Bm+#oT zB2yKX91&uly?W)PE2w6$V@dWmQ>i`O=q@FWjTY1+}%g#FkAf z$K^Okaq{5@A+z+vsu0)dYPo5}xH4J_lL=OOTu>4lC`t8-NtQWKDE``A_a^Us5fsdb z4y;Qml#Mugk>a3AvM8zC@@GG?_^~&Taw4o8pOdRRXpOgML?{5l^6M`{fmnL!5f0(x z=-E+;8FFB7zF?gIIH^+d&3BTQ?p=QBBUdj!l|28MQCF( zt;m}f0U!+|UwrLzQ1>job$R)@2Nn~LCa*jW<%+GE+p%^<060~UWiI)`n~SeLp8)^} zvBt4u&B_Ge9I~PUZ@_NpvFAb6uU>v7`OY~gn(X?E?aYd)U{#e><@8isy>cb_z~d+t zkP92t9ljNXU`5azb@7!?EuVWPBNZS7^D)_r3HS?VqoQf5BJcp;@)z!gtOLHR^Bb~f z<#?hBt;*8Vk0d|)JSZ%x8_F-sG`d8oyfE+O59YTx#DixZd2#$wS}u-CtQ>Ep?Zk1oIS@bbei5^ybF zQ%f<+x*~C*9@0b^#d+*5Q>UyymP%F>R^}vyQX z28&l724SJrvah#s;i5Dr)Y4CjVeR)s`AjFL*My&@-%tziRACSLB6q)nN&*?zRJ}Jq^bfm zbD#(sNW}}PayPaCa+u|-xw#8sw?4J zw@QKFmLsT3A>%Yz5<#7=C*u}Bde7psFOzRQ<8-RdM8#S|pyC-PqdE#Y`m&02TY+l* z`YQM37oq$8I#llD2NBDl4PvcXrC6e%NfHM|jhA7x#|zgZ*3yF?TD*Kd`M`szAE+WQ zX_q8?j3HKWeUem>2bfe1i6ujS;(Ekde)L1hOV5yRgj0$t2;^I(Kmv3(L{4Qz=t~N` zbY1$CeC*++hhHP#SRj)s2;^OzKv4W;7HV>YkSxl|bv1v(<1WHjYNmO_Y zfr=}*D8k4ax(U#cL_juR)tjcia~N|1>XjAO2={e>RN;&YFaZ;X>k6*^8O zs}u;z1`E?f&|#6$JnQx0^y;Uf0f9)K!}s;H zD1c4!L-&yHG))4k39o&X^6(1WamYc92IUP-1YB3WwDi)amhQPv|3>waNj2fL;cBTe zQBqguUC($6;3LC;Kvw6&@%ih)W$_Cif_ccL$6j8%_(u9~z=%!MAoG2Aym|ZWMFs&d zfh$Avfyx=^vIqwZu16wc(DbE`Bp?3l;^VJmKjYkkF}o_km$C`%|KINUfW>7DfM_Py+__l`2Vy(;rEa~aMTTx7oHJ;iMFx_lSC zKlOZoK`SftLlK||Py{Ff6ak6=MSvne5ugbCh9Ka!S6PFhnaJ#R#$IdHC#O|e5L@sw z8Zg6TDa^!Z#2F^GfcQ~iUe~|s!Za;3^QEx4(s|739s=g ztOu(KE8=H7Dil<>>s&~GhU4qJ9v^0nU^xvI`|vZYFTvgIXz829&tz6m7B#M!ghvZ1 zXmVC)(w|}K4`ysQ<#xj#?Wloy_-)3sD8VUnL2V?@Xku7^8`|0Tk!MZiGzF%{l?MEb zRud!%CV<3x{aNBTe0&Kkz1ojsE49Kz^ zufcwU%_KbHuYnk@xuCT{k8i9e zRg&=LLQVzoH`h_t8{ze3jCK06v3{_Y#5X7I*Wl;Wyg5Uj&1q{N36Jo_bJ1SnZ%)^G z@N;Umw46Mf^OR*Iyg5PWCeP;loC`mvCdQn^KQ%8!o>S98>G6Sz13#x`MeHtnr4>zf z*nIX{ciL0~vj68`A{OSam@hC7!4LYO2v7tl0u%v?07ZZzKoOt_Py{Ff6ak6=Mc}<7 zaI3?KHUn6#nVU6uICv;DY3R5eC)AbRYR?;r6Ivv>3AT5_{1)F>a;q(WC?0P8gDd|e z*h?V!9ko^xD$jWWwFx%Qh?2%_N9+F%<~tVVhs<}F?=nAs?+K(aDFPG$iU37`B0v$K z2v7tl0u%v?07ZZzKoR)$A>edat=76yFhB@yV+j}_1lRAdl{lTf|Oece{P_?G*Xc>ow};qiQvB=KMuPOfO#`X1tn=zbhweamg{B`%oL zlf-o`b?+rEvYw}`ZMpS5#09H-a8P+oOYM7!E5iLPGSgD?UgEN_BPKPDsDjx_S@3#lEpBUrrTnt< z51f+YW!s|nTizjSgXjI;cBs_&^oq^=?LxmiwYJYx?P#_p>LRnD@Tu<6!+p{D$kE7n za6S^99XdT1jvwxbhQc{-{T%~cZ9`pw;lA$u!(D;yzRs@EKuy8;HG%%VKn{`m8brIM z;qKa-oK?-u)_WG_gJaXV%yJ^$qH`Z=Ywzg_)a3Zr1nL6;Z&?5&3e|vcLtVSN1_FKk z=UQ%dR_)qlJs-!6hU4K_dfdTLc$CM`i6UY0{aHKd)o#M z2KIIx473do^>_CH?cT1wp=~4u9kFn5J{)SBuL*Pl;zQlNU62wISr0Ni7s?Kz`*g=U z!_#4iUK42V@9*hqgIvgp3m9aAltDTj9;6OMf_BH-#^)m^!Ub9B@Q#H;Ly;K>Rwy|g z!P)WfbY>WDS;M&<)y}G_D(i(iapv`gXBXo5Yjb91;CDAR==*}P@a%jVmU?#e1raWN z2igWYcDD`G^ODq{`ylM|`|JzOPv-aOh|bJKXMs_E@9y#FY<{18;n++h9*0tp-+wR~ z1DdH~m7y_%^TGLrc&<0(;Q6wR&Z^p4>%H&CIoK1QnjQ#`M`NLQ+RvLab2H87#?I4` zXzW-g6vM)L-5H)s&-3h(xa;(pV`(ap?Y$>*W+4|VEhH$9a7N{|q)HK|4Yy&PK4{gRJBrQVP&+5z)lIC+`mk>S0g({8| z4241*3>}Pw3hU~@P*<#|_h4|gi2t4`xQ4l43EMNJNyR*i#Z-&MRExz_ip5ll#gvQ1 zlnck)KXvHzcxWgzUNEuyr#c|S&OBATUKx=9ohwo3JGxSc&he=gq&PHvsz``^^W&k> zVoXqgj$%AefS#3D?4Ft~4930))V=vCSg3*`mCh=`Z%ug9Yamog;(GmoiZ*`USgg)T zk73p`_@!CPG(cZpt@8V=7yM?$f!u?u&|2ciq};i2%%+%)cB zW_0nO9zpy6E0_ff^K<4$%=efi^ViH@FyCOl&U}@5i+P=Sh4~coJo5ze2<#3x$NUZi zq#ud^MSvne5ugZA1SkR&0g3=cfFeK)z)MID;R_v_14LfVWP#^fM zxfMH1Ep~i0*zs<`jwgVfazA#;Zo!UwGj?1zW5>A(JEb>ar=%J?j*Zx{S7FDtp~T^K zTPunEzZWgci>nizhN1{i1SkR&0g3=cfFeKWTyoU$uHzxc9rjE1YTF~W+pJ%)4qAR`dFo#agRwdvO0>1E(%(?I)otEl$gu&A zZGl4!g4U$+yrhW2txJ!+ym;}ArAr@QdicT&Z0>H3pN=;#z;+_TB|DJNwjFrWf_z%0 zBk}pcTQLs{=dbi`@mllpP&7egS#{~Px0c>|p0KDNXm9Ii2*J*f@p;&lfw&>kJ0sK6 z;ZW(ZS|a!z)n2c6FeNyv1t1SN$OEoP5=A(MBnFl*ed5{^=fD5 z?3#_2M5bO6Bt1ln5dwp>Z!?z0Qs>|5txPLE8Ck`{Nk2{`VqCraO7fj^se+SE8X-_J zb+eJaEsdKS@_{ZlNs6HItXh5Ttw$E0eP{8LcU}AR{WyJ~K%wSI*!O7uAnE9f&WFdM z(PQX!3-Tv99NCoOwW$yAcVn!mBG3nco+l#jMa<%*qzI zDaC4YL%wW-WXU{OWRGdFiR8D=#hHf6wB_?ny}yip)*`@yWdfRqaCYKYbOEz7p9ss2C^S)U z)G9iQ8JoVCd3Ocoja8?rkm`WlV1*Zw&tF;k_+5)1`P||Imr}ZeXhJ0xj@$RI*DFud zENS_ZA&Q!;DQidrvH0N!u72qm(o6*B=OIhbNv;;sDui3c=4bb<(~&_Wr9vb{1q1rN>?aNV1V2odYwW&G?zH zA6$sckAupki9qrefnY!3H3Vo7Hgkmp5G7R=%1Kj6kjNHb=5&gF^Ne22cLb-AipEl2 zEi-yupRjlg#?9ImrGTQcng;zp9eko(*^IGPr5H5hXOi-^>0oT8V>&v1Eal%qv=F7e zzZ^>d1-aH+s340h9HQqqlIHA2AVn23vF$C>lZ47%WBw!wa9SK_mMlN?O!9$`q?Z)v z9?yqopzAQZfC|Z8_bQ2rvSazBM`5s%hgd8+jcbK|R~oSb^(B@}6zt21`=QFrLk#VX zj-C#aYQ~vXa}~%vOi-|0JNNRn$6wAvFBG1G<^<9^kJ5Xr6jOB9Ro33Vxj^e7z-{b; zOh_$0{$lc-$FVjLY6KW&#(1@3}Zx9BpH>~ zZPs}p&S^ghzh3ht$w#DSj^mCLvn_>~Nunev8p+69W+)$ZX6PqjmKsdy$-}vgOZ7>+ z!SZ&cvOu>4b)$G*B-x$I(#UO{t?`&*tur+|NW+{cwHchb?F|K*Gy$p;sNRWUyi(Z?yq^;<|8j`*U!V03m@`e{D1~_!H7pyQf@qz+#Yr;_Yba*FP zEHYP@V8thWDm(rC;^SXjzUR})`(9pr@a1cFB^Dog_}Ya>(JYY>p`SnXV&LtZLb}&*5cY)cX}Ama&NlRKr3Uthf`UYp2qptrmyK`|z1wCi+ydUJCiCFT>=act62e^^BVaUw_dWfg{jFm$>Sb;&aqRpw#gJJX7{cdw+(ChhoOS$JE8}lXRG$Z+b;CaXw@zr_V-bwG@ z`d;~WOAuv55ugZA1SkR&0g3=cfFeKzfy2>qdl@i>1H#tybeXVmO>n?bWm&!s zj)P}oiaizhv^#TYpbP`?(4q1sxN?u>S+UV!(8%b-O`<%kqL#vAojmC@6>0!n1$}To5Vpbs1>^ zIZ~d2)NU_C10dl+OW`P>%5(lK8fl)K*lsIE19F{(BhI|$&%y)IFg%4eE1{8&2PsG- z7%K@-COE$VNS2>0X);>>FK0f3?En9e`7ZM<=1-XKFt35_|F1HC0I%tXB0v$K2v7tl z0u%v?07ZZzKoOt_Py{Ff6oG$d1WFxNw{?5D(_yt*>q^S-W1|~CHriaobvTK)y_C4N z65=`>*!4T?#I@T>fSTKn?El%ApP-ZsFz+%yDL-X-pL@i0!nw0_zT{EIIs51A>umG3 zd5h&NTUu>tgK4|1TbYqWEzCLWa9J(hN|)EYD|3QDfP*|*lf?20OzIf3N-#48CJKcX z5zaJlGMFBb?HOl8G9I|)M?RUn^my`t2Lo`bCgYgMm(MSreVLpKA;)OIc;3}39|B8r z==jLvU%dAC>+lBV2FYZHAO-#lOc31;b8XlT;`iUaxb&IN1o&n?00yH7o$tSWuOX%$ z$xMJCa)uY0t;M zlvO)1Ly>kyY}1;Jcuummy0^00 z9+So;R^>Ea9D}J-O&Al$kHU=Scqk-rVB1LAVSciNGs#LSXWyP~B_sLB6NydCDy8FK z=DA6Pvvx4cmMdKt4Bv5~P*~w)9&E_Ts>Et6ud;Gz{3y?d$4udNWJza`1uHE|u5|l+ zpi%%>Z7riD8V&HE6lQ}bC@mrwniM!ikoa`*(r^?riq;#S2TP5~doDl$GfN%lj#;`$ zF+v6`6;h~p(I`~+CXL9TCb~thyB}J9DS_2epIF9uYKjHe3-xu zO>Bo5a;y)PR)4BuA%zd(@_ds5`lbm*Y7a40dysQxSFc=1PiXOa!(f%2#QOn@{ z1nG4M*UaUuLkt2$R#}0Wai}VkI~z^0id7n7;;;wjuu*9k#Ur=UfU};w+630bL`_Yd z_58Q08TenJW=NAvmU7E~e>utUB05ZJ7>RE|)B36iQ{e?vSV+%VQwxizuuhrN+XeYo zQQ>T0wU{m`nEK_Lz}zd(7t#Pe zO9QU`-fO6h%dEtf9zRdI2xiYfRCNr@o&l=FNaWl~3=9iH;lFzMf#mt~$%_|^DmzO^ zRued)5KS)J@{>BPm_mrKP^dORqc4i0d6wHG%fVujstM4>6wwHNwHjf9klRnnMI)sV zi+4YtyzuC%G{Q`cAcDcQ)z^AF^gsV0)vje|L_;Rn(7S{+SB;Z2uq;-fQs7pj66x(4 z0X(;}xH^@%bbs=rcNdaUfEo}rbLo}-%;~e+OVVnL4(Lteu3^63v;>Fxo1l-OikiWL zG%-eRn&nurn0}=7rr&dokqb>qKbD^P5NH_!JEUYjeKZrrhkaY^2eHP{JU+)+acJm`f;HMWzX5_tM>b?iMGgWD154W^zcwL6zv8}pz*_P@pxonHXQB_ z;g^G8MRp->dcUt@psQ`DE70B7*)^))D7O~cvYdS_KtmG!)PJ~%c_2;l&?>(qwY+IzYJ zHTYdkpgs`rmIXliPz@wxsB2f(K%lRGD9|_D(-YV?(B0cMa4@j9>tLX5c&NX-4`}pu z^$l$!kvd{v)HWPyo39CU0un>ry5_xJa7we@Ak z?F-I?YXW@-+6Fpyw++<8^#Bb9W0cgqKAqwCcq}r9Oh@MrFc^)2xY5?4I_4bxE zB;;CWRZy`ef<|tEz4i#$!3-i^1L5&-mt^FxH3TdkjQX%g&B`6kTD1u6vQGU(B2{vA* z!30gg!Z`?rr3)1oqclN~ywtm5v1qI}K9N;IQ7LN(IIG?-Srh&FlvRf$Zs^qzT!VpX zz4Y8Q!qgQlVMeHIN;wK@1gx*oe;GWXU~TggS%-_tNqma^!ngQ@9`**xcCVC_Paq^YXQHy255>ao!fDT{q2y zXk{s|PR0m!-XrS`N+ZT&OW0CpOx$Tot;BW_}uy>*I2ud=a zx1bJ$=VPZkz%*OoCbKtqY89cnPR+qJM0t6%N6z3>pDK{*ozd7hm{`->*Zj!fjz|>R z-GXV?Llo&!KsRLqZb>RgVIaCNAKn+7pUe^gYAxtSHdZC9yR2u4eQf9oqPjU<^9ISa zNOR{bTb)%~w_4BdAlITSq>o{bNrKWxh^cNu;+c7f~C&D0mk4V>OA9CaNg zni4P8r|br#P5@$sdyaa?wEcAVToxC|t#j_%NEYnJSoXo2GqWJW z?>}4?81U+uQ@9}XMnemQs;4%5CrcKjI^`96-udz(v98Yf>l%HTgFDpWM8h(@)oeJo z=2mBw-)}u1#R_7M*vuadnTlb)zp<4I|1_ttoC_HOX5@s@cVo?m)?>iMDPd!9wl-!Ol} z{66zB=69J7Fn2IF``-0^%lD@53ExTI0iWcn@czjAC*D`Rk9g<2{oW=t?x!D$07ZZz zKoOt_Py{Ff6ak6=MSvpk>p`Hz;kH<7@pldW-h#ga_}h=aZ^7T2@%PR6dlUY?34d4P z?~VAoD&TN;SkqU8CD>!x!lEtBk|b&BhCBfTwMAr`I9Y`~pj>7C0C0w0RN(G*xYX6} zaJOZ#01B+o#K8s-PF$ad01Lb1geJInj#mX`T}}YJX&C~DiYRNGvNm6U^c}Hl@&!oW zl*;4@kg=`Pmp=gByNUNhdNTs(yPZ10An*5+Nb5t z6Ch*zk}FSu)aE1D*5k|{K;Qcg8!Jll1xVimQ<5)0YR3x8Ir0Ta-vD91#o^vz>WjXO zDYX^A2I|ydUTaUhGq!Ex@xM1|@jdT*gn5&Bi8;@VGlFm2SLd@ck24F*AXCe%XY9To z`38NJ-XAgFWc~;9BPPjo`I5fB@O|C)mhTncIp2b>%clSt`k@F=1SkR&0g3=cfFeK< zpa@U|C;}9LUnc^{T)eqiy*A5Sd`*tI_$^r>0y!c485D2Hc-ov5`{tb3n=-=Rl<`!J zo=nsB8#A7&GD2?1c&f~Js>pa+pYgOVOTM*P&ug-tnXG4DPI|l<^gUVO%X7k)Wrc9( zgm7hraOQ+4&3RLjZsgyQ{$S6LFVp@%I{$BF-bLCu#JuY}?c3=6qIcNyBhRBAw)}sV zPnVaKyjTbTI-hZFFa68XlcgIkSTVc{qS|PB>Ai@SM!13=tU2!KSCEN-E29fs|>@OixARj%+m; z;;p08r*ZrvYbC732B~i^oT!lTQLqWF^3MjaVprx z%;F%o2GpR0}gNs2<#7Jg9LMfU_TY6CpkDE!kOm}5fSjh zQ=9M>%t&s|V36U*Ye#O)p9C;7imbMx1CDU|oz>Fpn-Y)&lmvy9I7#7e&d3Ec$EAC+ zswN+<&7TCq0cK%kbV3g7u% zz#{hcJXJ+Iys2PDlgWM=*?o|u3Envzk^V5hCCv zfdvRo)kJwssz7IZ0*8$&bC47kL`}~HXfaf51y~j(ffd(euevrVEZ+TIiTy0Gm4vugQO}f$K=OIQe}xdTu}rfv}6Ru8SFl%ZUQ%LXGltuswqf?eHjoT z<0H`S5>HV$vc3pJ$Xtlj1Xfo7&#aT8$V+YlLn45X=*@r#b0D6g1`Wfyyc|+%RvNUy zaI_LkAm|+c(@Q{A2@lnU;xRGG@s#DmYmH3EXrA?ucx?}?=zzf_Q&UYMu<{7`&q`{! z2~v(9=+T;DNdRS0ngjuim~&?1Y4R4eCRPD!I*L}7odjsHbWd46!W82G%W@iANe>3C z!2)+CNIc3R5wr-pVr6DFnCuNtSv}$_#sOMMsQp<5`io}ksS|-dj06@$c$pP7cQ#5X zPk}$;EzSX!^`QOY!013$8dxv`567tCn6{|8veRICvZ`{#Q-lL7JqoB5mP8I}y-bwY z77iK)Fig%VvMW0u3{OZscci=s2efu6Hpw!}!a8y$2WW%ADhV8S=D?D@Gb;@#Pf_EJ zl&#DGDkU(_kjX)WUI5nwb_yV6yfZ5eX`TXi*bN+V+a8jgdK#d&qh%L3AR?+w5)XN# zII}W3%})}JxQZtMjKD)C%R;JJHF-m|09_AB5Y*Cakf4{Vd-B?0XYnLp4lMNPvx0_YKpeKIr&;A6pyhtZ0vm1J{BdCL6Zl4409 z99S8K4B1J5p&MM5fO{V$xfmIK0(ZnwED5j%E;NC)tn^-zo(s_WD1Z?sfQ2tdHiwia z6gPV@4luxgemnFd)SM)M7gXwk2KN>y_G}I*PgOl)E6xFpdjz- zgf4(BI~!7-ig4J9Ik>aB^n?s7Tbu<`odS7xX1L$OaL{hH4fAUiX6m`XJu_ zzs>gv!~Xww!2bWY!1DiBna?wyX09-wV4h+gWzI79F=v<JL@F%`M@O{;H#rJvNr-3m2Py{Ff6ak6=MSvne5ugZA z1SkR&0g3=cU{wfUd-b*3w-MLhh~1jT_hENST?2LljrG{|*KNh_E&kiEyV+ld-J2V4 z#qOrMTI}BBufcA$e+zau_ygFj^!u?};lBmD>;0RtyUu?zcGqs-gxxibHxak4nz;Ur z*ku~4h+DUTxc*A)`Wh>+>#bXlU5|eqcFP;rVz#QT@ z|DFCa@~qqcFRdf?|4aSE{(p&|*#9r7BliCtb;SO^y^h%bxB0>TKg=rl(f>sd z6U^^06EGj(@%@AEJ1}SPJj@d;Fh`iZOo{KB@2{9nAWT0L0g3=cfFeKcSw&uMATKM)%L?*xJ$bo~yj)A3)(}62y!4SL zFY)t`r*iUSTmwj6y2wi>aZ1U{65=?BV<&$4{eND_gEH^OEXr~TbaVvJ4oU0}!YLz}(@N^l`hOX7*#c|---FfvpX1g2Z!lkDz5*}lhax}` zpa@U|C;}7#iU37`B0v$K2v7tl0u+H?D*|};f1Qn-0;sdO^=GvH@3PjR^?%p`f!6=w z39bKwgVz7yC0hRn2d)3R++Z0FK5KrhNukUs0u%v?07ZZzKoOt_Py{Ff6ak6=MSvne z5%@Po0NMX9VU`e=1I)Y3PnaLTK7gh2wPkVlpScy+)6Rc(9xnZFrFA9uIIcPN+F!A+ zv&~!o+^Slh`ZpHLlGs#DHZdgb=yGLk-(bPus|41S!EUS|Y(F{1562Fjn(xCa^)}P-4@@0@8s!eHGcjMB!Lg z+Rm|!{Ueh{=VB+13r9yJrFUp@|1M$w$(iBdk>*iV7;D?BvLoR}ty@;c#ofnxxdYPC z=tST6)XByOHx^XFv8l1~zB6N8y>o{~M%vrw8pVSrGo=F~!3Iwe_Q!y9RmG%3w~6v9 z7_(Qmb2~%F#(Mgvrw)zI?h3{aPVC+(&7V3r5t|QB96d0^$B)l-?^JtENyjJ0&$R7p z8#~$D-FP%I8*b|zjt)(A9}La4v4gwY_Dn^4kIjfjyQ2M5Qa@2N zMgjAr$Ot+ZUWN-*`R(29{py}^Q6BD>weeF&LlYw%dxnDD;r5}?;jS3lc5HMm-rhbn z#GV{J9Y5C1?(S&gd-_H@b{!o)Gc-6RHy&>r7zyv5jLq%cdwgu5BRJnUbZ{aKJ2}8= z={yC%Zeu0cE&)#OE4QZ;|?;AmLh+0{8T+TXn}({WH9-Ltb}w(&r3 z_@FW`o!;9kc6OcVJvnl`T^t@79_ruQJ9c8Bxp`)4e&o<(*VxpQymRDa&p`O}Xy?M@ z=uCWcY?m^*KT|lc%WJR{RRFG{0B~U1S4Q_Z2w?FOtl&3J?(Ehk277j~akg=AP&_d* z$|>X1@%Ze~6O#+_X|b2@@9Nprb&QunCmP$EV>|iKz{x!WI~9J_ggY}8&yg*Sr+~6`eKF}+6>Qtf5-7m-zEi!24$O{?4k_}$bok`L$)R0` zMr8TSfdeP@bj;11?Cu*m-lp!2tBoObLW%C&KN0U6o*UG<4zaOVY+{g~nm-d0_D$`T zr#J!jZZ zIN00Weq7jhbo69zf_^sO@{kdS>NZ=~6sdB;A^V((!P&16We$;o=2>-`>?f#0E#yGmZSQ&*X{u5F6V$!p%-b zduHR^dpZvt+BY;cH8Z@>zB4Ch(|B@FDykWyz;89=PZ~S=$0PlGP#Eg! zkDM5vJUQB@?eFLw+!NZb2!QJ!C`{#F`2=yHRi^~Uw+O%+gj6V@p7miEaM~_MextWEj zgOS*9WPgA6vEBRoB8O&$_Q|m`3*+e(F;sPMwLyv}kEHV!lMalILw%yar>{cMebQnvKkMj*st-Pw$>R9qb)GK65CabOujZhJ5uD&R2A8NfX>AA_&@cF%oZ| z?l~Ch=$aMx_8pAvni!tjb#VN|gt$*V#O^;6Jw1J_Cw6q_fkTtsQ_Uld6UW<+P4o;O zhzY|JbMgsMnCm}yOr9PWxn0L2N3{-dpl#}CPLY61U<{V1CN3|G9NZQoHo=uFlA>{S zrzS@h+D`U`PDMDaC#uDFkB=Q4>DQ=`? z`*+VYv&R|-4n~Z(WmaLL5ki1i#Hs~iN+gW{fi`PlOVjXdV}n<`8!vIZJ5G!j92jM{kM0*n z2X-8stW9({17y^ZOyiW)!LXBLkQefxCVte1zI28RR;|E6U^Z%4=eHETnw zSJfXLY1tun_YaH?Z15ck4(w>|4{h4KYuBnBHNjQw`+HXn9~jtD(|#b*(mQZ8xIQ%0 z-MZh~CQas@L<~EMGeC45$xlv69aa>Qw(A$NJ2Ln3}t?~BnI5u+l z;Lf%+y;~3X53O3YepkojVTX%E7*1Sb1nQ8}oQgV;~9#Qa%^IHZEtlNL>VWDS( zFgUuoy(=KM@7U%a+0s97#NXe$u5WmCus(2P{i?u@-lm4V+k?{Db^BU_jl=s_?;YMM zY}&B1Yvjn5)oWI5Yd_c)S{IPEj5hUbo=jaFC$&#_B6YHHC$Q;5v%8mV-Pk(h+t}T? zf7PIGb;Ibkmd%^Gn)?UWZ4nO-29B-ku4xagYuvxObKQ>4)=iDAYuB|jtT`rZ?z^_B zdH?qQ!H&I$jx2*hIkc)}RaZ^tzIFA@+qy<4OE!*ECMG+Qw^Q&AmM3oVLKeGM31{>u7KP&I5hHW%ZkmPNGhSGf3tv&lpd_I}x@W zE>8%BjD%Yn`fTlO}uu3xpi?O>Zf)E4k}9NW4kbnTJmeVtv0 zMhCi%NzI#r8#lH}1MRK*<>vl1*KQB432q(s*Bl;d8m!-UxOsH*`nJt&lO(IdDHG~= zmU?pPaJ2*S-eCYp^VhWu^tSfY*LR6~BLgGy$fkk)`?qa4?7dd#ZffWo-E_ElU+a;h z9S3)uue;O733kZRzfr%sbp~ z#&L>d&hpIoWYi(sUcyZ;xb#8}tdK`HwX9yhX}fpJ*0!}m;NY?0z3n?j+8TEa_P4im z438X*U_l+~?l`jQ!20^VNBhN1%`Nib!!4~nL9u^lU}wWX%U0p26dB!itp8B2+_HaH zPg9rPRivvdZtY??<&cbZY&Np->yv8~cpv0i4BWBehq~LcZD@Go+GR)92mKqmI`{7z zX*tm9UA@fLS>LgJXya&SVC#-;Tf{c8$+u(u@Xj3@*SEJ0^a=;}b#CZew|3LIcK>ig zSBJ1_N9TdYmZQ6_UA1ZLE}0PrXT4v0W}$>eZXm_6q_1D^Tr zuecAnEAUnBUycJg4&*qH<3NrBIS%AFkmEp(133=lIFRGO|2+n>^tlfWl8?!}Crx-Cv9?gq9tTM41g>GP&edUxvMhp-(Sn0T8exlI{t!9~uv z0=KTNu48a;Xy8!yP+gt0$^v*MP`Gd+JVXM>71Fp17eVdVnX{LVKR9;Jv$2z}jShDY zvET8Y(bt}`Y%o(8%SuJ(1bJ{6f?vP|jN`AK7`yPy*xAR%PTz>09g)7C{@5)~#U4Ht zJ9V7=eB}DeXHJvz{|?WenLOY0eAV-3o}YTY0PvdoE_c7X$zAFCmFu5epL3md^}5!&%8NcI z`c~1aMRykU7OgERFZ`hJn}z2JZ!hdAc)j4Rf`bLC3$Akh)cG~%bI$9X*EoGncm7!Z z+xchmhw?Y(FUd(*wmHpe#7{)=PaGXV>UF3(==b}C zkhr8=S(iuBb5cEp%x3@M3T3SkC|^+U2K|fdQPY=9b$U=NPU=O7(O2#F1*AZQHEPZ zO2udNm^a{)McHvxrQ$Vw46$af?3i1r$VQLJ2(kKojyaWzWcZlR7Yqa)o=QbDd`t|; zqU4xXsR%}oNdX^1%no;@Qe*g-Owz3#u1cla=rJ)A5PXsYQm8U~%ojxdwxcNTX48Tc z@)x|-qF=61)=)Sp=73KY{8DgXh0S)GI?Z2$#=K|E-O$QukFBXd)- zL*%Or9}~rZSGGgs^+u2R1E_cFvqR)z!^dPl(irU!d7aT?WL5?Jc8L7LhL8D(l;7aIZ-BrzDW&8bip83DuLg@D&KyHZ(b1dJ4T4oS9Il}e?- zV-j*ZciGC3pa7LxF!053c68Z(nR#K^y89pY5e1dE% zu2kk5K8AB25Y_Zb!(cUK@t)Tq9uq5C)}W!_b{$_?Tbt z2?3kCLYZxZ(+_>?w?SFWG6LociV_45Wi`|AF|QE7dt%8bHGGVWZ`oG(VIt6a=cIwl zw&najNkq}{ge^(FE$1iI#jT{|)gVa*$C6k_hEPK)P~>=yKVd796j!G^d=!+|x&W{$ z6D$a}Sd;BIr0$2XIT>0|!G=s+!zMff=>?@gK?NHyDIW5Ngn;OGRLH(iYVi3L z7%T~AChDUWnTHTlqC79F_!|OIJaVINlo30*k* zGSVH{(=k$Bwi-0=Xxh}MqlG>*IvAPs%yRYyJ%kOt-GESDwrca{jg9rq6ML%|I**o@ zHMiDpYgkvmZ3!mqQVxPa-;?Uk9DD+Syk~z$Uw3ktT?3I6e;FPa8rd^26zLvH#(Mg? zk?!HHp`O8!o`L@4(DBk8T_Zh*fFk)jZ`szgseaqevJH(p%a#x+FLf0xy}sR-SL*Yb zZ`__J*1_(fzMkP>V2cY^_g!rY5$hiLgBNsaamKl@XXNN01TuL#QJD^P4-bqEb#-U> zR8P{0NgyQ^?Y5{Si-+y-kNv;1#;pSSTVskBpntp~+S8BcM6;0VuivT3*V8r7zi0T+zT}44 z|74yJz8Q~jD6;Op&fcSYQcpHwd+gae+S_aJO!^7g+kL3Jck-eY=ex1hnZZ0NAM6;} zpNe;Vg9H6g8Oi?~H?)%eJ?R8h%**mh>*t%58ogn(iHvD#A~i+!kYAc2#D<|?i+pe= zwUm-{)XXOOw)4 zJzwx#^t|GE-t)BQQO^UOJ3TjhuJ`=0XB1fj``okKQ{DNl^{!Q(=^l&wJ@=UVU)*1I zf6@H|_usoOxLweUIpZhlV4epP*hul5xkGk7jXI&4w?shl2o^m(4uXfAs54jg0 z+u*ydORm3jz3cjC*I&BccD?2raUInD>vZA2->Zl~j+h(=avaEUAjg3m2XY+9aUjQm z90zh7_&>yfJe$*OSz`K2`nlNj7p{++$}JV93;3LBDL1{IZ!?=MjtbLx)>vSAjWy<* zUPay785Uc)>9h13+XB;P@QscBVt1Q9&l)b%o2)U_^ag7bnJ&_X<0{kVSYxi~71o$z zI>#Cw)61+e&GZs$xJ@szhRgHxu%^aa+KWBOCp@R;6ajcKO0Xu~$&^c-tkW%@L0%r!mB8gop~u*PiD)2uPe z^b~8%G(E{0rKTrXV}|K*)+jMO#u~+@PqD^y)1$00#q>$m@R%N9jcKNbS;K95h&5cM z2U%mP=?rTWnI2$`Leu@8IYML%W~(T|cs`cYg!Kc+kB$28pk2WqF{{y%(7!To>u@ZkPGe7JG{A3j{T z{|_HUxc|>-b{3NQf1>FjljqMpgPsq$zX1<@HGJ=iry$XR8&^@t-|XHR}}oH z;K_p4f_!B5A9OCs|91WZ`5W@{@-E~Z%3JD)Iv#NBa1_|zwjZ+B+Wy)0$F_Oa&s+Ce z-m~0lDKnol_n0j}ai#ydMGwt~EG@~)%QNHuiRoM{AHweqNyv}0NJ5PyN%fKtF5650 z)xFO?!^KN}@`XFDS!%EQ50s9;zi=D>L+h+F`1fG9kTIHoibez)2CeS>@2ovreC#`6 z{2z^v+3PM7sAnDww{53SgjK?eQ}`5 zLu*%-rbWhOP#**?2CMEDjP>D?!6*K)VCA*p5>CUrTmo&g$QKWWTVnK!gUd60u|SBw zSU?D0MdC-hg;b1tsl}MGn?~L3qPPD^8p!~F?Amhg7F6YjU1M6 z>%Y-Iwe4P-2siam2=GBw?-j!H7`(b)a^h!@k#N;{0+*BDa*mR}dr8LN)sZFxJnbvj z{gTMx3wMM|Ar{DnfPbC6NXyfdjk_0T4Bp4(jrl#7(OCB@1~yzuv0OvIGqB+n_I|q; zWencS!DC*}|e@h7?3%5LCCW> z9F|?J>ndsA{L)!O8JP3)Q};zZ=Nhkf?DL6pBSj39;K ztH+F4%xBDdYy^a-F=4KnpD{rJ4y;g9ywe%14OeAM5QE_*1TEwQsd|BH@m2FOCdf|- z^41sB{gS;*cm{*HVQ$6*c_|E;5X6i7hD~(JZwr^QNxEuIrUVI$Ai3TdHgmN;iz&km zvoj?~@KJ&!LJ&!w#=o$IucLF=nz3qD#ssOr^20W&XjZBkX#GqMgREVKV+i?@WR?+n zxY7;V`TU>HKI=<4EKj;IEyxU3Uf97jRk$ohCAV@${FTg3nt3kytAr5#SB7faI(tTt z=u{=s;s*?dwR6ukCA>Q|hhTf9oK?Jg!i@MKpAq>Co+aEq%(PkCrzrn77RQOpGv^zu zP$9150R1Qh_D371dq@gvJ~Q2nUX{X1m?}S}AWY#R_G#_dy>VKG zv;|PWozO;R%a1wpUF^lJ;r22L@S3$#<9vkbO>Kw;a8iOm)6bi_pEC7ivwidwdv` zU(5swV{?a#gCTRsXj}wBNZ<#zbaN=Ku^2dqX1A;^;!w$KGL!-8$5E(xVG|#0GdZwb zt*Z-luV^%(0)Etd!-_>F)PHjU*w`oLRRM>RZeFQS66V$Sne`ENFsdfatJO|wKj)WX zEyQR`J+A|NtjVT;&vXNresFt={RRdl@E3OD7*sA zLnvaL)6FPHoI2wf#i1q4sGm^(X&s$W_5`wwUXz(1ScTLX8J@}}I%d=k8|OKfXi94v zXEMplB}}OI7&+nTY@){})M{&7l7|EbFD6Vzv!W&s|){M;U@}x1z#=bDX=;p zac<22ZvO50HF;mlJDxYk@j1t4`(^v1_OR_cw(D#a)-PH2z~8^yQf0n`^8+hQ-vrR) z|GGXxX@%hom6B($kcC3EyNZNrkkpeR6oMN=-voY!sxBQx zyIR+_aR?>Qwnlmqhboz;;;$EkB&ssfssc(YIf()Ck_ttXw11rb zA>8&l;lzWHmMh1Cpu=Lu#=7rv|KHBA)v3XTb{3!sx1FPWj_k-7i>ST}q794LNT~ZB z9}#wjO^Y(LAEAbIxb3s#rN7d(orh0|Dl#7HDiTB)_(-Vx9&^mY4yM4h=tBD_!TN5v z?P*HI?rpkPfi_QR8KP={iUpRg>MW&O!gg=Ha`1>UEaFoCJ`XrK*vnv18(?vPHYTv$ zTQUaEVo%$2)0hvi zRaManLNXH~vQdSUZY0WF33$oeUC5>QLpEo_`J5kS#{O{oPSQYhZJ54MWJAVSIK5sH zFyu2pKkda*XL#~FLB(#R%=wQgh?Q3Ww+{TbKuhSthvV>LDoW`=-y;jE> zLtP+oUEn1l!AdUXA8`-5fO+Gv@j_g_?6r2U$rwDJtX?u%D+!P@57zw%QY9h5LN=QK zo`DUwEuvi6-Iy_WK3TnFvR2a0d+UD0^<^Pjz5#wYm*^~tfA{Ju2am~G$wm4Ok2n^w z*a^Ths>5wX6wT%a6+HE7Qrr3=iR%I{)>#9?IbPj?`z2g%5O@%-IS81Tt0qbvO4i3b{zzr_)BbHAbZq312xD%-ISq z+8=YhtH7gUQzroV2k&D?{&R)LmC*++W&?#&{+QMz`aGsx|pS!P)(@C87 z)P{k`m4QrVYdHg3_b!*{6t+kKe2lZpIRX}y&+zOpW)f=?5Jz`d7jX_YZsllhyG zpb9D7G!^(pgG8q30!B~WPx&;R%2$dh?0eN54z~`|H;M!@#=@tmM5gHiE=)DPK8!rUu~C@fIJglyos>Cx!?h9C0D>|~Q$Q`PM~jQ!j@i6|^^lK~nT z77y|66Z{7?w8yPXO{ccA`KS)6-HJ(N8kC*k+r-fjKSHwynU|#n+uFP6C=GKrW%nmk z@Js`yinz&w%w>|`Tuj(U(Dnk=?S#mmB!W*h7XY3Ia}vA?)hsMvwu2TkpXj@MJYy_8 z(30R)sMcX2vl8RZ`gYDLCX&b>UpW??Tk#IJOt=MI_ba9?Ci*Usk7bMniv<~034VoY zITrGz$;6b`MCYaTSOS)mp()4(iv`1?b2e1Vk?-_`Elg*0?2a5wfSXeH0WK6bgRAp0 ze$33FFt^j=USx>RJ;t-hkt@fdb2fg=UH2mXY2#CFf=knNSZCoG&ph>2bS}q_xg%V} zKkeiVhzY-(YpYrG_uYroS4lU$iPy&%Mm!H5dk?#~->vRgPH=2mM^*5c-UbUPsvN=4 zBj6MK5YH!ifxLjD@RMnV2-H z=kYE#m|WbU*Zdp$I`BMVRPd3ZjInSPnBaN5%MB(s$HsXTZf7A7&A+)eV=P<~CU_pI zO{{uRs(;h=9vw!if73ds!(uFN4U5h#;d`WE51VJOynjID{gKFk4z97hxrR;fM}Epo zrm%-=CCyM~5(u}RqX;AY8DrtPFTpMODUbb6;TDu;D1Vy_U&#AuChxvXv4~tpC3q)4 z<6%qgx~Z8e;kK{ScL;MI=$c+td!(D#8WQ58sAl*SKB}fuph$hB#wLuvTj{%OKB#*K zgWwac#^P!t!EsT|a4rY?o?VdxI)H`(7Pz8F@KN65LB^^4vqUBP9BpWxP*28K_=HaI zQQqU>kg5EWr`5&12P)sak^NVWMdzcu$JNDD{;BTraQjm8g!W~Og{zGOA4N6ur}4o$ zlgJf=lt-sEPiSuf7K4SF__{=aC-^LCDy{0x&1MVrAMK2EC%`pWsEOyx;Od zKj*0O9_I39zegfhjs**~nh5-J9=4m#KY50q8cbF_p-5N8Sh(7dNVq}G16K2Dr*q}R z(^AzqubKv2->G6T-p#|5<0g}YsDqZ7Vb9;)-qpcDq}e|O2(A=Jv_Z}P{RQ{Xd5VYX zmxkMxvys#C4rTgBKk6~9q}h$kqEBgZGqEOQ{c>BSm;l6BQ6}PE6|c;x~|cIHBhDi%hg;$axagKsM?K3_|+vo%u{@s|Nl1G|Cii?<%kA98NVznp(v{+zs5@;V(qciiKsw*Q6wXjRm7Zn9AiiKld0&0L7-WDG40vtDWb;77*FM3}R@8*<^k@Pq8l64=9-nSgIh)>N=;AT{ z7-d&PS;QwsdNh6&jn1HdUyV&NS2`XJm|$uvYJ&F_pn>w>N}^6(cKG6^Jp=B;FQIJF z8~9-pH-=}ZaUgDpOfX-oy`^czlT;F|xtxR#*|xnm*8^o6>NJ_}GPkuLeKEvlw@fdflb4ocy$j zlCmgpm8vuJ!xldJXK`t(mhuEcze_=`K0C*h#?r^t2aI5yu^+bZ#dJ20ORMJQL}OoJ zIsBQt3)fFNYhODYGMmR{G-ujDxuu@z>rz;5|CL~goBLra4?E1^oKv0Y35WT1DM`o;>J^*wOLwfq%Ker(Tqki?I&SnU>^Oi1JmtzGz%8QVM`!hit=#*Z%hx&8qn>cE zuS;R412VzFRUetz{A6F)#+IG%Ts|jMkGl1BVzJ>o=hIXyT?#uqa3xrm}7G$)bs{uP7nf+?6?GLyz%;)A52OMtyW3~kHjNUE<=MAn93k-HO*LFhgVcic^ zzu+hvZf)l%b0dl)&K2WLPI7*jM5BCUJy)}Bwd6zY3#z-4?T46>Y5OdFsffZ(DWqRW zaG!~aMwf2;L#_exxtH?}C0>pn$YPTxqTm#RD&sV7qwNf1kyq1hRVFzZcr8t8%BYq0VexHE16XQKcY0Vn zi8m#zwFNSKAMTAcilKF%g9`V8*;)EV4yNC}3%n)&AB?%)Kw z{2E1{!EFql`~C^$znb#qY+L$HUQS2d5#%>t$B`Tt30>l9I?J6)s>$AKu<6VhPSL^d~zc*Nc4?bJn7 z^C2RNaOG$)Yt_?rY9^b9=fSEep4q>Cl4ixV-@BHTE3F^gS`Q?%2ggWB1>uH3(U8*riJGtjx#~LddU` zaQ&qe!gZ<$|7Js0?ELxY#S5{=PiPH7Rvh*WW$sLsti_9sWQd$!5!bVaM8D(@25=pB zHe_8oe=+*{%T<@oKXv)_I}&~&A7kDI%ttjLR=d?5Pi*Vs=w$TX-9#a)sa zLBXJmDoPj#Ay6gB!9X_BynN>5_#HR1pKN%kP#sEfu}YAkiU{CpwDU){86T zal1OydUncv{HeF1XJ3JGXAMS{3L9ii$&f5BZfEz2m}-(=^5GKsY{(kB{g&AI6Vcml zk2i=S&|urK&!&=PJf3_(FX~f?C}j(>{6Rs;hOF_sZi$|KnEfPtibDlK?hFYcx1M{& zpg^t@mL*?S<0*R2>9N!2*iTB31{DOkG9w7n-!DLu6At;k{yo_ZVcV2qr^==@REbg$SG?mtf!HnCt-P@p9=d$@tFZ^3(i&8H8f-`}UnR>} zdXSmV51Sx_5rw5#&dMN&J^a%6?XR<+lpqZ%_>`v-WXvZ?#OjEP=j8zEtU^m=r3awB zW6wVkef^x)AY{d1TNJlLCCivkB1*>+ed&V`eWE01wf@3FiGA{Z_LK5Sg9^jRo*_Y) z8v@d2VX*kckRW8or00 zPu)lYRK;b@kRB-@!%u)l!+eur%Vk5)*fUR$-E=emN%WEiRmrnZwN#?m7cO*7_qbB< z#}9|V9~|i(9?6D|*ynB;f8pM-d!CJ*d`;g5j|8RMta7HR%D@Tm2ZDYqA7q|kEPMUg zsUp>Bdh_1s=@(-6KR@wzDm@L;eB%GlHVv6PJ?_7AuQ30E>q(cZ=uly_aEEEA;LU>N z&U^EJncruw%=?GDI>)n)Y4(rVF4;C&U$xG+D9N4wbl)m93&EC#^lo@ilm&1D>N#E{ zS3;d!xf*3&;WzQIBzeKTY-9+B37R{4;zg}Nctl4Og*`{9S(XvSA*?1LjO+x8MY-1J zuZD;N68u*{TS8`J zk#X-0k?5D;hZ&X~SlP|;c!2HEY76g#8 z(W7D?9Psdi~||Z^cgCHm)`(SsH9;h9w!2CHOEuXcW@tgX5T$ z7X)`>?9PV~r_maOEE3^lu;H97R>?9HAKZOZ)WS{$;%p!e>tQyd3S%6BNcNKvq(Ysi z6faT0f08yy36?a8u1o=Y&C@B^VLwq~CC?u=|#E#fc${}JXP^e`} zak)y6vEclq^aK`S(iPDMe^!En`+Dj8n;+2kg3QSBVMYY7=M_SzO@>0q zZh*h|c=X1Hu=b6s4MJ8NcDYhqmKj;zV89Pui{&|jybQ^byguTR!K@OcP#~BUx5lr3 zjF5Hu6|F(Yio>3%6wl3!EKJ&fAHFY{z9HNJkk!~CdrEKJk9eupAY{d1&m{i8qv)W? zGvNLv_Wz%AO~)^}e>o21IFRE&jsrOk5JAGsH&IjTR>H!J-A4L=H8t5DB z=s#MGPek8pGxF4sXb0CH;TxKA7xZVBgMj$t*vXgRsc8-BscEngoCsw|mKZ`}B63f_ zMiO?;YU3Z_o9InXj-7r(Yf!&dgAHy4Gb0OuE-!M)5Ie{9gxJu?YQG@%^2zbnu8*BO ztu>fmtHPe66bCXSi$s~hDUu9?1N+(Kw;=j<`ROwdqShc}#bF~c*sqdh7#a)(5cihA zC2aQueDK$^65{yd%H=yh!+sKi;!q1*_7>A3+ZXK~v*&>Of_v2Mb-jZd0D4gY@H<5h z6m2dlDEw04#|u{${H)-$g8qUf&T;3n&i&3Ms0H{){uZ3gc{y(=&+GV!<5kDCo|o-K zwl6xC*tXfW*?Y577P;Oz4&*rSzmWs`j{o46$G`sE@zbUvlIDX~*zSxM92UK0BqK(y zl>*gXzaLvMTKY-ipODe%^Y;&o^z7~F>KN%6=&v0f?HukJ>gnvR-8(Sa9~tS6baZtL z*e!L(zc==)>wd}*nh3&>9-&YsdTYIYf-vL@;T(Y+=^pGD8tLoqAE_modbNjohC6$^ zZIuMj2j93a9UuYOG1%Y2#UFY=*lJb*&GEFGzW%{;=XHP@2}O;^zkl=BUcCuluh8>| zk}T>;p3I~8yGtC4-O>dhkA35B7#7OtO-{2@P3FudV?u5aE`DXHR0)3~yV0Sb zF{6)m+v|>h|K#zTkLzA3gqyE4h+-M8FPD6^qPH3;{}RIK2@pGn209{L9m69Q%L*|0 zf%~7-0g|0gt%8`5TwozgA)x!fx^lK%I-uj}8EvOj831yfC;-tLY=-5rC2Lj#98 zdKY|{Lj92rYA6*{WX5>?p#=Gk935o5DQlo`{!0g^Ar%}SavgnQ0-X3TCvuvI?Wko6|0c0kILKI4*r+*(H?eFRA=@_7ZN&^(oojO3vQUOBx0a6?(pnan~ z``9?0LDkTGH|qc-S3^=YQp`mtPYU4J{?U&9eFq1a9w}i=Rdhg74GJ7y!_kKzW2(AP z6t5(2aqoMyKO2*&CLNL&;G1-ejCA)$I{Lf1YuUu@uI(J@pH5|S?+j5&lJcP$mw+d42}DlkDWQoMXR zFnvr1xELv~>R_ZK1xE|vmg-s!SC5R4$}*=U)9(PBnLM zlRdKUd|1Cmy8GDr2_Y9S0D3zqB>*qZPfK17fUckb;ABJXRvjZ!O}+q5p5YJ-2hiO& z*gJ5vn~wT?%FTPes{@jfn*z>f`a>Lu&Xxczd7F-Z?>q1R%^Nx}EqYT7_J~+3`f6pV z8Ye!nUL_hfx&=$WoZ7803Z}jPH!tYGk`?xbfBmNJo4^D_-ddQ(g0~v^2na~{b%cs> zJ`~L;5879w*o*vH(_n*4= zqXOX9UH7sM2 z;(bd?%~RvCbsD9X5T%x?;S)-N&s&FoZR9lh;g8nUk?!ZtkYJrrcVDg7R}GI+Kp`R0 zeHZC|tb=ub>2E)uw!0)$3pg!~jdjv}C+U8)lXbu0Umr2<&OA-TO%P|0NcVQq{YY0` z-3|wcZpe4JObbiQF72I#P?_i_p0HP{M#(C~_=9TnpZy83QbDZt;fNv*3rA1iKK2F< z&Rx88@uDaRg1ybILS2>ws!X6FMuy`acshRIJ5VbCr?(mf3I*(1GpMmAj$eN6R%CRD zaEoo*;lei5ry^IX6++e6N=HUL3be!l18SgF6tFXb118?+soUbzA}_?c)k>&!f(O&* z>EzCNAk>No&Lg6SjD`t7MarH4>OA_0PJ*cM5fDI-#qn29j9qv}%X*=CF0osliamTP zcIr6!`N;K`&z!bnY@0prlNr6m^S)<|XQuly@&Imi?{-(aO|HLnJ?>mT@FxYE3g$Zh%X!gxvva3&Y5vdh|2+S`{O){j zzBTXf@*c~(HZPnv#qqC>vyP8DHah0m|I_}u{Uip&BHK@FU$Wh8`jxHICfZEauUbE8 z?XzBOEwucT<(pEVIr3X@1%~U=~c3r3IvuZ)Al{wk%hfV0`@ zP-R0lm4sT^TPue!$S@j00rSPsb^msJim6%&sLz|V=M%=W&xhTPfP-}S7jz&~`-Xr0 zf0$gfAK2#5`ti*x9>YPclmPGqH;jd+n8@e5rYWYXz+uaw^qQo^n*;) zrV>Kti~c89^KSq7IN7kGOdv9wfK@=mN2o^F5ZNYnwHuyOM~l;3r8KFpnut?l4OAH_ zc9A!|44liAbsEPLkj0-z`9?o^e2kE&tW5(RX)xX(eLhN`Uz6^6nP@ix|KHyxl3J}k zAAffuDNRQNNTC`TFCV(@pJ<1MbREd+O0>f_X@^zmI^f`^mtpui?NFbl11S*~^id37 z4U=~Wr|W>4Bw~nmc!gr9OVfdj&Y+)mc!Og2aJmk@P(WZLy?mUIv@%@>Ns=Xo;YAba zaCMpvIDn0l#FV7hDTWp4I-mfkz+U=!+QFZ^1NQLQ8sihaLD@&=B$Ko+O$XGlK+O%> zfl1n%t^*b^FYCZ0EvM-qLv{NZNlemGx(+@cY6Ve}n54yY9Z)zxlxPPgX(3Gq3E_MG z4ouQD={m?rXJo35NxC{s2b_gQ%}PoVlXO+O4mkejWf+*Gm!;~!mS-6Q3AGGuWKI+9 zTdjO3T?bUr46qJtXe>?B0egd}mq;;?p;4tQQMF#8no%tVQoDsy#74&AbO^A89}H0h zWMot+i_#zv{7@V8`7eW@YGq*>1Sr6YEo0i@uYjOhsWk3@%{!Dm^wAD~VImzW(sjV; z%}{`L_%n*3JY5Hz`wj$YhcD3%3(|B5V$WP)@9>4=gru@`9k59+$P~ld6vO;<9RykC z1LO0w!&PZIz+?;snH*jNNma@`W4WQc74ljsMQlRPO@jcZJ42izHlgRF>wqKB0uuw7 z&{fLpG!&schu2RjV)J=c8U)zsmiW+P^Lb{v4qlQnOEIwd3?q#hxydJwJjgnGQ=6 zI8nV-#c3W#1t4sclgFvzou2M-2^A73AO8W+mrpYy-CvENk1`zuIMLEoQ;mS*X&g?+ zI3tf!9X%!8;{qxG_{if#MOS&!JdWxDsC`EsCkndCo$hhyPDVRX&s8qt$Du_0ux|)- zqMWOW(mhT}t<&L4RP*veV?O#&ohc*};6yK16{G2u%JWjN7l{3xbk`Hyp=;K5u zFV9ay3ldk1NaZ*B=IV*G0SbhE(T0wox1hrDXu9*#K8fZdV*-VYJ#hh!4Q*n*e=(no1 zAZc}dQuB*f?AJB3Auj9g?-f2QLFarn!g4L8F)hBv>6%` zXK^OZO2I^4tKWg6J*cwJ3XIW`{kmp0v@y7psFKWyV)gnRsL_IgvE(QkFWawcW*3UR7NR9Ydj`u%yNs3NVr4`N738e&%Inpx4M zf~KTRxdPH?4S%~2K4D@Uw!Aw7S2pnGIbUXv(ibjkL5Qi`DhFv0D7BgcB|AuWM#Q7lTWPDlO1f?X>)& zwf${cF@B=3cqM*aGaIrr&{UF6ja}&}qxJo5T0wqd9U_cC4&>;X*^s7zrjpFpNJ}e8 zoVY@HOB<`p55=IC`qwqHqKj7jr~Jy(h)T;Z1Szq7Vu8A675ZUusTKcq&8+BBK~sJ? zG}6-Yi`DukHV(>*>Pr6Wn%U6B;8LROS3nfzogms-jecF(e_b;hqO{uo%2b<5lrt@- z5IRE5f4mTi^{b-^!%Nr9h9(A=Flwsx3TUEr|5>SiR`Hou|JOCMp-H;}Kq<7SYRkFM zgX;hAA{VAGhaMQ=&?2+SA#)XXEUHpb!|MOfSKTMIa=-3E09`X1 zA~g)~+SkeWv354e|Ic$jX7YT~^MdC_&u-6E?tjCtxqmqhg)dao7n;pD9;lT7j4v+!YMlR&uz7^q zLzm7!5_{s*_?u5(zW*KvFXhrE53%{G8{c!QI+7q2dz!jtuk<|Zy% zN1a7-@^<`{XED9Tp1G6s-dHU?oEo=q+5jcz5)=XVKR>-dLjbC1p@u&g3$L zsSMK=CvJu4>u*KRo*IAf-b?3kndxVvcbqSa!T5lsd zP~97sdtrtPc$e6@7ojT0E}kF%N33PAdCZ+F6bUAk}~ zdfWZ>k6-_A;(K{Mb+ta=XcF!4Qd_cy{de7cRZ^2=q_%mM7_3SjR)oDLj>Yx)Besn&3RKaQUX2 z#$S1!b~P_wk*^YA*cHb#kf1`Av%3|XCa0Y(Z{SEb*A~;4rJf>z0J*K3sG75nMNi%} z{?_U7(@#+droZ^GLqosN00Oioj<|w>q~|@y)!}IV@d~@v-{{S8v;&JV2GRH{kE=7> zd^&8)h&vMfKPJ%&Tez5px=NoJ}B>ju*{ z1*pnkGTGd(nLOt_dpw0`O9HV6sQZuF5W4u@n3D zcfO&6X?9L{cY)SDM1?{hig(RU1g8BBfhlR1%WN^tF}+|a`eo5)i~gu+O5vXt))sue zAn1J2xiJ6l@;{NkEbrg*Ivwvg{>c8X_A~Ym*)G~PSbuKaZ~3aF0o^A5_nRFfTPxLL z6$@MA->}qCgF7KyTg~+> z_rm4+n0BtE#I?}uaybFM3T3ns>d)|Eji_{qi!2HFaphK2W@MEXEfxY3*JaeGmsP00 zDg`*8!VoQ0fjh+&6Drbh;0Vzpr)2Y9Tz=NQYP6@9T!DkY)8+f0_8=C~FzK04>b`KaH_!`V@(&pv+nffJT_mPAUli$3&tYqW2Z z(4_s1-dLnexk_acIL?^l3^mjzC(J8_q|hdIe^u<{onsH)z=*TVOQcG@;wphC^~)qQ zsehs*HP2Hi!eHlf2^A!gQiS9x6x}1dVuY|TtUZC6{<$i=dFmUQ7c5&k8J@qI-06b5 zcA5u9x;qC34qm=^XY7%;*k-L|mbHf5T~$Nw&e9s}UFe-w+MQZ`b5wj9M^obSV>$%_ zjgg*_E#3WN7oLn>cv?kgVd%(JTNE8>=+J3baP?_bO0?H=rg}X;>Z=L<)dO9lxbbOZ z{Pdd$w~n8EG5Y4cmRSrPxtWWiBMluo?Z&Rvs64`|P)*mQQ<_XV;3g_-20{&kqin~9 z%_SSf^Kq?6NU0{16bcO+1PD}1Y-&DyVeH}!*jtR=@uH>JRzohos)=9bHQ`rAKD{6- z{tO%S(@+6~>6r>mHOMPhBpnl8Z1o1^9YeU{s$oAaXdEW5bN-R&*#}X_Hg@Yxu}|G( zDYi0N60QWJra#^MfgQq9Q7)mL;c24&vIbwL?7-M$Gs#H^e^mhNalT z$eLmR(p>`%069-2mSF7s)ZG3r$)I-uG`W?Quy7wJKwYA0$+#ZKKFz3njuvPcI~yVgv}*NRib zRZWUvjAh%0x!N0NizPucGnxZ{Y$ZH#VF#VHzg@v zRC#=9ZRZyW#PY1s(Esn2-e=~^={k>|6RjWUtsY7!OEXpLN5^CV$PQu{! z(Vh|90ou)YGIq~7G%STyJuU1`O6``r8v6SrI^x%|9n9**92FPlAfZf(+g4spBMMy+6jelDhl=Jk(1bJzH(ml-r0ho;^ew?(V5 zpof-JlTzBeGPBGXG%JUu+U4ZF!mTKfFj=J~9mEu(%xrK5&@$Py21t}sdx3#uLz1`< zDOML4);?LY5*X0f{ZB>Ty5C|~?fc$=eGE+vT5K3_wD_IscOd*QlUUqmSy)q*Y!1kN zLBd7An{W{>lNy)5O~pp!#?kc+phAQGMJE3KTX->Z_iy6G@Qw+Lq=S{{R=BP|W(e7Y zCU2OElRmEx$pLZal8sx=PpisLGIBXTNizX09n~D|$0xpL3LA}Bl~?3+@(U9a5Ni{F zAa_|~CcAmLspy1xdBJ=6uQ@&-4t&86f3t4?*v6;7*j&$H+ z@I7mWy1Tbu+q>gL0N*cNYQC9$55evk>g?#+LoR6;PW)|U!?woy9gSr>>Q`-SEGtj^ zV|m$vh0(sAP3}R1D>Ng~8+tc6C*PR^w;QoRB?mhjZeVyGy$)A7*?CCdx&@nuG z7&pQ0+21j|KRF1nsRNewA<&S5OTz<0BYOaZ5RpPuy|~z%(V`a?=S6={*TK{;*>R|2 zq$9=ab@z4j^d={JA8eQuT-ma%X;b~Son;#uca|+718k|QV5xhlwX_rkYj506N3=E^ z2fK&*dWMJLX~f5`?z`F)qgeOIAN*ME8i=GCpc?b`koZ%|;W5-bJTN-c)t%u}sb@@5 z(Vp1uWl9MH3CcGnUU8eiB&PZOvfAFG8t-(^SOM@KKP=l zuH-A-uVaBuI)biuhVn{rUid^v8$p^ew};y8n)0NE3jONClSk5oPJi&Di88}rN)tXA zjGN@LSP;l7t*bMi*qSH{3QK2fXcC$DhpSV_W#V&x@X|@c{qi_7uT)03a_k?8^K)a@ zWEsVcM}mKNg@H8w@YQW``_C4+J0`k!?x0T);h~_ zr|Fjf!v6a;Irp6l-6RreURW}JY02XFrGE*x`AL!whZ`!{Wr-~MLabR5f2pDN8~C!Se|88{aH@Aku??_{hM}QG$(tX4}XP z%oGxkGp|G?dDr$h;cTZz6l-N%lZnj?1cb}jZu9rwcrbd$onyDZICkpw=#$Uj?$FNB zk&%IZ{kCpxd_Q^bz-WI2o_$AG#{ihU7g0KMfFRqH1Q{-@!MR1`fh56o`KH@5L3Fgc zX91H1#nLhn3&~l)cDf(s`sF|zjehUL5Dkm^4`Ms?P)Dy~FJrjG5^+UoC@z!uMwymI zFmG}S0hPX!;bgxtYa6FN~s!W^jCh<>9cVDVpoZ7`Xq>or4H!R2I3P(51MemeqgTpehZe(TbhjNTJpdwebxYdKf0fu zByFIXrx}pH*J|kl)}A?WTys+4Vu=H@^eF>zTuE{Uc!~iV4k!hk+oATAOD4XFgSqcc^Ks{*CI z6p~;m1I002x&S4aZcWE?0Ve#)Mi-rYO0eyBr zeC!aIGP}cNM9QygANMOxv-?7YF>)fgQWuKQvm2_ zV4Ndc|M3=6oc|WnB(Ju=nam5X zi96G9gLS{9@V_UXo3?c=RtX@x(y0jWN~oAynJmD9EjAimp`Q%tCiYD{u43(36i2ck z6%r|c6Pv!uWPz0$9#x^A@wf)tpT>LFYVj0{9a1%Weokb&#`ANx?D~Rh>;I${fcYQI zgD55Nn~os-2G0NYR>V&{t<;V+ZchxWB<$L!+O#jzZ)fVZZuR^1-R0`38kZ}eVcRqb zo3?E~85wr%(_lK644~=+iQ6A+Y+hpB<|Sf&j0hcWeWL|z-8|Izdk;h zJO7_#KRq*R?phKE$o0&h|lt={KmM7co`ki@8B`w#NWMiJ7)1n@Ii&R8fwNG;@L~Lt5eu_cAM{lX5 z72=a@(Gzsq#{AxeDmv}GoGH=KL+}Kh{v5oeGQ}x)f@f4c0Z;t@*{1y_(|*s8`(NDa zU7vH66`d&jL19b5#ezl7Tk_w{@5uXdp6IyGVYc_${?>N2^%<+((rNy46b-zBe@ZxDi1W^K54$Wql zDOO#2^WNy`7h?B6FXC$fm&5vT+Bn{wYX^?hv>k2hIk2%~HK}DHU-hP1nqZGz~N%txT&yZeGl}hBEU>B_9;XX>r-AGXMNjmtVg_#68%O zKS*xz)HSmqF9AFuaX!kFvs$NVNeqN=gR-Pj7=7bY;2}s3A?rwxy@YzT`D>t%fhQDB z(~6dxXK^OtEM8J3qF{wA2K|oc$p@mJehnqngSd5?+)A!%WXjA4e#+d&kg4d|NL5XImUqGoFK&eP6>Ku`(>f+6SlU1wsSdo6bEaSD6X7bTPP; zD6Is0dLahnfHx4zi`{oq?2%`QN$ z1%Cy^1PM(6+!r2iW<{3@nv$l~g3o|7tW`dreeCvIV&_jpZ@XQ@ER_5bs{hNnW;Ud0 zps6Ue+VJT`NiLX?{Pyv?Zi$|K7zNr1U4C43tZQaN7lTWQ(h9|AKokxxie7v4p3`He z&xzQefR3 zEUln?`jJI0LlAtnOK+Zu9e*VDiUQq^g$WmROOT+}%!V!vH0768Wj+Ib;Tm~4V8d{~ z{Ky$mCUX(@)B9vyGaI@XTtZYSYSr^qJDoWekbS|BY`c8s#^^(DMo&LP6c#ju9P$#A zM{Q<9mIj(iQmcEPUXr-3ASeWFu@@e?eD|xd2c9BkA}rrvfGpTrGaJ%0&{UFI5&iU% zgy4LB*%o{FrSaQehho6+!YVFgQYPf2+*loGaI59Ttd`TEib^C*18d3UkM^!D0<6H>S)54 z(lxW8iNU2bX&1U>K$8S5A&))t^w>=>P>Gs{BE>=-Z)QUigG*>C#658JCS09kaxV0g zP)~Sa3R50gEU;i=pSxxJg?pKAFN|~#k7PrK9vG-0{{Jk~^Cr(;_g}jO*S$rtq78*F z7M2tocm9{tm;YeCJ@0_ytBzXx4Ytd+X6rfB`uh{}f0);y%WwamGE>$jV>*o=@?(fg zz5*DkV^804`JP*t9HG4YK3y{_R;r+hRA)+=ljigJ1AhN}VzIvU@YwAytMs6jp{|)7 zX&f{qO}qlqFf#?&3euv_KOH^s3YH)&lZX%C=9qXhOVSuKg~!J} z|0J_pVROK9qb4kEW<#0=nv%Bs3P^(+B1@IA2kyo=BM~yN3f`my=U3OvhBOT{Ax&Ne z(pYRXty;q@3}IPHxZz5aYkmG|L``8Fg(}DId?I@Ge&TI{YTUO;3~yaC8>%(Xgz9M@ z(ioa-Cq|m7H-NYS2B6l=iY^s2B~9BeN$V8AKJ)qgFmhjdZtTv7i7!l|KIFU~4*;=dHl%5w zsVKEwl(hCF+^CQsO7k$c5C#HWK9PifeDI^RW;S#&xRhVoj!Rm8VOKyFg5J6CRH8TD z7JKndcmfz>_*Ry5&1~q>KoinrvOAO6gvF(nvVeVwgb6F*P=j0@f^~_>IR^n%_7N1@uxhBOT{A#FLi7Me}ibZZs%7ro+yD*#2QR`ON* z$S#>TFdLCEB4?;%%n}JpuCAF4#SAXxq;^?!dQK9{#OIrZ*k<&_J18gNG@=fjEYo?x zo7vE%fu^Ksw?=0`n&gFPK6Cu~#|UYsi8JBH+JU=3i8G-#vms3bO-a)(piWO3)LFnE zoJqDp-nyR#1c_b?;pSOLQ)^~Jng*JXR*Ji-GYnNi_v1Ea65j%4mmj}R^ac=j69Yjw zhw)}sl&PRW8S(!Mo!6Q?-R>{Ct6g^&{j6wr;TH;L6dZQ`7+>f9j81lr3riSjut=l>e^~=7s z^=lfcs~g%!_U$;>-`UtW(zbUvuwgVfx^-k*sBgGwVEX{!kg{}z764dQ!K5?aRijBK zo~?@8l?3=-V$iBDwjI6m0eBGt|JM=e>*-fz76`6A+T7mM5?QsgZ?J99*AUq+Zr-wM zUDJB;V9l|;)s6jo4-M{I-y-(5tiN{W&eg45Ej_F}KY&t5;jV$c!H)i;)%YZ|&aBmF zO&jtIA%qeLEI=G~?Bq*44+1${2x4I8iKcUrCa3j(lsBt^rlf_gfHW}#8nE;9Z1kol z$4zFkMx>Rqy+fBL{CxQlce=+8J9i6w9(mDGE)A7A=yHNLfsx z=*X6oM0O%8QDPAQq1d4kALZyAn^wMC{d#--`aQr9Wk{I;&Ou|UZvFrL@4f%M_g}t$ z@$>btw)iw*?aji1wbSePaeGT1OFASt`aTsWDT$Ud_8>4zDt<8%`Yz3 zrv(_x7)hR6iQawrml3Fe_(3Q)3dV}%&)37+VSF*7cky{^M7f37PFYup#W0$^h>Di0FcJavz-*x8v`B!!#=c8v{IQN-zZ$8^UW+Ob{-g+2?OPI!uk4u?My!7%ejM2Djuo;by@T9#|mj;6Q@eOoGbgzq(WSmxl?qu?8*^$tGh*2W6KB zxw7BO_s6+%w_hxmGAX9lX$QPgc+7Vy`qUA3Xd`>TR;o6Oj!3$dG6OlI?Vyr=N6>6V zY7MUBX~j(bIPNT(a4GO~{@{^^uM#^aM%J+$+1Y7{r=FtdJvg@ziFrGc%oYW&$DPW? z0)vJtCuD6mw#qpM)Zu+cS-f)Q*!&r_!ldD54Dqc6-C@tNDsYGfly(j>1ZD zl64O%*+ws``NYclDyJkUoI{eF1i`)K7{Se0gEpYB$VF&bp5Bj+Bl%2I%cev@sTW6s zQOcKB1!0=W41=zAV-$$09fuU^Nj5_&i!E=W+>55$N!gR9y4`L$$(a3VmyMetp_i@a za=nTj8f=5NB~3{t!}_MJ+_8+M9uYHygb5qmt){3&hDsnfrHo=B5^#3dcqu!I*c$wV ztGSq7v?EBK3GLOefTMl7JEf_DEs}DE3WZj~U+Bg8Lbs&ro~cKaoIx$z6xP=(B)3A7 zV!_=*Te-s(g%kFO(OP1eeNQY!6;x?zV``mN*q86PwBV4{JIUveWS!sovy+F{N&&N}y*+>tZ-VNEU(4;Z4kKUY5I(T^I zx^6bIc#~!K7#whE-sXzw)fiJ?Q=V9Ls8>eWsGM*Gl0hRF8I0VNqt}m7q6=JC8=*nK zYc|GaL++;fMp~bY{l4tb*{XRX_9B(b4>C-A8rD1s^El?_Bsoa##&*exPchOS%Zn7t z^7|E)p9WJ}EoPc7rre>`s2Y{B`9{QE@b~SBzJF*$Gn61?QVzKo>oUWlH0bbwQi;#e zje(h~cGJ~FEKu_&qSbtpbyl?LKwn+1!X1n&bpMno?* zzMMWRb8#t8C8n)Oy%z44yqSC@VA@q%x#O&r!|681>UBw%BY9S|NTcwNlZh9Es)g&XiqpW-E1P z&uqo*S<^_@TN5cLcZ-<{r!t$eW61Vz(G-cU+dYXp>=U^3!FS2pd`z>}o+e#3t(oPz z!*<4SXQFW}RE-Y1xtJ?vI-Lz?B+zo%M%8vLtWErxY^mFw1cwDzO-v2FTr)N8k7_y9 zTTht6(CG~33!Br9A$t>{iG(z8cWFC!^ivFj4VI!2`FcCuGX0g#h^mID_8^wkMK+sK zgnqo)8Vk+3ztM9`4sjfGim7_QRZ2>X&tJ|+o?gXW>LsX1D5v+waktm0WNWJI7n%4V zP>pSJx1?$9u@|>w=bCQA->7q)Uo z<4kM?s+mxEw6PsPeZY$Y)oSki_Vx?kr%oKbhLz(vDeTE*Y&p z+mEy=6(uy{I#e;>Y78QyXgM>U6l!^2RZxR!q320xfojc9M=N$JJYY+{cv}eAseFmc zl$>nQ$EAzaP1y;gin>LUCTsTWcJ8RBXl4(WXBd~neRdE@yMvJ=7f+}@N~}$LrEIFN zdh+dNHeG<>(7=Y;Ef&U3Gw3Kynsg+ZW=ErRrYh0RoS1X9(g|w97z1V69M;)%C0`Bb zs}~Aum!;=4Q4k&b*kfnr``F5LXJJA_KPUY^GJGYGp0d@cSbEtk84m1HmAJiEah z&~z=50@fj&C-?uio!#BJsonU>jd$bz|F5n^uYTsrpI*sde&O=%-GfVic*%3|`!4+M z1@ZjfI`?nSJ#+TUc=@wuYRBCE|DyitfTu9pN3ZMT`iL-C{EKTZZf7{k&yRvMeTQ_K zwU#)tgpe?56ZvtxYc%5Ra^I+DiX*>nj1V!{&>IEThmG6Ia(z8l4=|mW*L0@iVzHBr z(y3UT$yc&Xz1bT~-Ks`SUG9lSJDh$aDV4^nXF>4@I}&&B7(Jh*E9X?gH_#2Bsl+{G z6~dap?576BVKW!b#sl$gXi`fyP18)vgI0nvC$^j(D{-QyFUP0T0o&|p{)t0uwS81K z<4HTI$P`xpDXtvZ2$Cip?OK0QDR5c2y*d$a?Ires1x?)Cc>760<>Wo2vf`}D9w$^z z2H1f>Kj{wX-BF?`CE7tf?;b>H(`;64(UG_0OnImNNG&kV@IEiwC{)_zV!+!~B&ON0 z3yROjhlFU6%Q#&L+ApTKWH-{S8UAThPObAj8{yvc37RX3^kK(H9SYS18Z}dB)Syby?;A*3|TCQY=ha6-*1MZStg;!TzS45 z6g`u+t-(h7op>>nuy=-<&)Ae}6wyrHVbrlCy-K@l8G7@ozG$oPc$d2>At=zG=b8>sIGvg>G??b+@*- zThc^s02cFm!QF8ha$#&`0SG}a(g+sog_vk%f?^@qMBS!!`&n-&$r)k9vDR~5&WKh@ zgK%7-W705KtJcZ`c`^*ghDFt%8+xUD!zWG3RrlCOW!rh1EmLb|~q9|F#qPCe~Jg!R%Nlp}Eqx6aovACjaD=nvNU2oG%}+j~k(q-^V3w@oBfI z_tIX;8)V|xUA<$b%mjl%vmXjK%|J)@_R97~HEC*$6pC>{7?XREan8p^)q*X{X=_JI zX!aT3hwL!?!L9dfpA8(z&4Be-7RhX=ss2VztJ~b|VoS@En@q~yb{V+KbFTb!(ulbk zucr6t@xU>rx&dwE9=qJ^VC?M2r=C_O(Cx?$H|NpCsg!6=>3BrSX0>&pVkN;!d`J_K z&+|mv#iJj*93n z(P_IUl&I>w9O#+0VcRB$Bi?AJL!?@&&rm}Ta>LsQjttWcI#bUP9 zpV*CHK3^LT`>t`L&1b}lFmgz(kY^m~RoSt}wz-m)GwH3Z!=F9bQpXt#4c!%O${z153iK+C^HF8b{xlAT$ zOHEv6$3cfRe>v3D^|sRTO^R~a5T=n$?v^ykq5O$Ca~sV0RFK5w*#j=r2~P257d(=M5u zqoj6Z(FmvW9h`=2rOj!#q{$Nn{?4u3F*3Pa2(tv7eW_G(2ri~y3#6Njslsa_gu|15 zGBzB9HOX*x6)Ibga7L=F*vcYr&(W;il+r_`?isi2UZL8h+@q$*I|7|z#nVXIoSt5I zV|!VlN$lwSN87pBsR)4I9s}=)2@^kXY+o>9d3CConF5hDto=XMq`*uu#I?T7;ZZ`dtI%p zGY(a=WCwUT2eIlK3g{DbQnT#91V@5K10n; zLavfL=+JI&iDLToh=1g$hU@KivDfq03d0_@sj}g6g?qJbx#O)S9N|REw=p4M6|-nc zJZo**x3_bL!w0iR&;y{q_alKxUypIB5)d?1B78@Lm{b^h&sEOs8;gTCQ&oa zcBbyW!6e4+0OcyviBhP>Idjz!Zno2q5YO-t!BckgQ_XJ6Z*q4?Q>3sCu$aGf&fUgh zrr|As(ay5Q^80SRZl6#Uk6s83oLyHlS)vl+)QB}{x$Nu;0Y;UhU3ruYQ|?HbpT>#d$y|ierZO+I!P3*TgD?av)?acAS35j@lTw$4g zIw$+-Qr+KiXBo^jJrHZRCt@L#t2FFPaFA`P>54qm>WxUrh|nVC^EOnQvyp0cLoKx% z&vJU&Gs$^bxm;{#3yiN3;{1UPElnTNMEW3E%Wt1Ex1nipB8M6z*dr1z@8?**ic^iR zgt?py`8-Zmy*&-64K z8efufK4(F+VGF6LEn!lKMHnqFc&BV@5aJV6UKiuz6ubbbR$<)c0?i;DEEf52uQ=Qk z9bAetnlRTuc8_i64z3+}4{m*yI8S0zPBS!bQRze?Oh2RBYWc>*C<~oosFQFcD=N$O z^!6xSjE|LCjPh!(VkIMoTrR0-@6;nnHp*1oJfE~TY=sQl@pb(JqqDIlkjrAx6p>8D z=dZ(&zq|9koioqe_@nE;bM13i$2;%aWiGyS$$PPX;RnzEjis;uK6UX?l0$RmJ@2`x z9z1dRvg_&HGdtHG-o1X=H&>D~qXzu>a4_=}U=WXZ`f&jv*IZ-_!i{w|5h#>B}6!rBNzWe2$`?))eow@V! z7d|=r&evb~*tQNJENcE}ac!IT;M}*f^fUHCzh7(`g{EG#7p$%npSJ4L1dI>T_uqQv zQL@QgzWwk`*MWHEGKr1Yxqf~3(cQ=ApFQ+b;qD`05y8eTtetLg8nI*sU?c{CGpXXF z=xq&}M%FMV^ot+=ftUaD&%FGx&!Z$DI?YgahPm^#Pk#8N|MG>eeg7|i^_Tw6*FOE3 z7k~Lj(C3$a{0lFC@xyoMKYZ@s#UFeAr62js9U8rU`3pbw;un7UrN8&wD?j_avv1v@ z?|k)_es=c77oU3pA|*2a$Y)>r?5FBskk#)`DsBqqzAxg0~sP`j<=Acm5cpX{? zw-`=CUZ4z3qnD7?qZtHuZ}zGMhHw%pdIm> z+!Ya*MH@y@1bQLI-~h|xj18zC!O%+#Q`F|XJ^l0%?&hBb=#;q0UHk^76a@Cf6F0>J z+gx~9g-3rgD{5)@15VE^*r ze0ccG?WJOw%?GrFhXa{VFt41TJ-DUzc0@=s`~x#yNz@PrRUtlOTd;MK(%=rBy=IN@Txrag$EMPNu?fyx2C%0v zy?@g+1M?a*!wP2te;66NT>l5LtFr+=VGhsKA`izQ?g@Z*;lxbIhn{5IvC~Vj>JtJZeF+*$dY`*^kWv|eCOop_wpw`vTfVq zq@&x`a{9gW;h%W%qd)knHnK%;a5!IDZecW;Zz~+>4bQ)Z`N`aW=cSn}t5%Hij!%@+tnsFD zdinc5ibeg^U;4fmKmENg{^*D2TpsNJ01oDl4!5S+NrrSlw6xgjnr=*XcFsO-T`w)2 z<~vrWoiO2Mj}X3ux`&u-fQuCW5>B|+GOOS``s3xk7FgZaPMTGJIi}7r6XgE>g~e$) zbl`$UvW|wwou-Lb0rzHvbg$WK7fTok2o!hH{8%|npRAA;JN>IUM=ur9%Rl}-P-Rd^ zFMj0b@0@t9o_7V2;YWgL=QjiN1*Zb+maFS&^I-kR`I!!S$HO-R*1_7+=f@nZVG6~O z0f1X@2OfV$h3Qk~__HZPw+_~RZS`RN;!k}NsWSf|XX{tK_yaHfoiDAOt>-)F{G%tG ztylO)#{4n56DTae58>7}*9v&Q|F(0piM0^TwmSzLqL^5?;UMwiSPHn4;@a_slgxr` zAX{zao7oi1Th3Z@W`3@^{Pfm2gL4&bsT4~gCjD3nBI182;hTQsVqe3N3!}MU`nc%= zmSK2XlM1}af9Df_ zQLg<3orA}}1(z@k(CyoihacZXe%Rp^@ysH?bLQ2DRTs7fpmHD|XE_@zhd&-6YYa%} z6yVt|IJ>Qap(Dg&@q=a!s>JZRP%IR=B3qIKu7sl|QWS7vp~RI78o8=%+{Dh_f9u;Y zp&To>`^^bO4htl1@F|LzJ)nlxmC)=F&YN%#@zOVaKFt*n%mpW0?vL_JKx|fK)Edvn zobK;G{P6lZo1L}Sbn}b^oR%C02tFzVO0JV2k4UgMpr3^|mRwet2EOE)TvnLIempZP zEN?yc-}2TYaYQCWJbEcWKy`9*RNfxWc^rlqa z`j&O65DViG6WL<6kW2$$$KZChA$WKc5h6+O@7`!qnK6xA!7V%k=WX+feE;D$uZv~t zB0^mh?BW1(hau|7Iwg+;++_uv6ZkijSQbnZJn410ESLrWw!AT24pMJ=#4znfL1ytKLS(tQayRLjxIp#mcwx z^FOx1ZyU#O9?filZ@oM_y)KLkmO5B7Z!Rv!EVGA1ETrEa`&A&mBQFjehYp}Pi6s{NJV-n=I}xqT{L_r)bDE2Z zNM;C9s(F!KmCT=`J$)-hq}?8!_6G@KYA4g)as(XC`s4aEw}}%|`>_-dl_jr__N-5T zE>`%h6sb-3!F?yOW}{Ds*6jIGUNtq&Mq|~QeJlmy0AQpO3_ou?L-*Ex5L^GNw}Ib( znApHq9cPvrSUW2nE~8Z&__5RhWCwym{wsJN=z=A3OW6cKRDA0R7AV>@vOk zLzn*Kl6vv;7jIqo&hvk8-f^~a<~Pn=T-?=P^N)jD%$A4<4!Ig6fEtkH0xB=;D-u;! z(gjDaEKkGn99?nhfpM3Lw-fb1&*`FdhfAZv2&@`SJL$T+m4{Ex-j|UUX>ah{#6j1x zV^cAhphAV{n#&U!)(zj0Xc7b~9MVL%0>YwqcFz2swJEKE7|iYMYVKaOr&J%WpB|C6K9NY>VC)K$r1XdCP zs}95KzE)5AtIcL?%*V7&jjOoj(onTm{EB~)DDaM`IKD(N)<#->hGL34j zC>Z~7+*#%kY=nrH zfs+Wal>ENE>=gN8-5IM_3Ozeg!Hj8t*iX}De9}SusUa(^B%3KT-4#h1I-D%;P}?a( z^hCqFcM_0XO4{b>*V#aUNk~q2L{jY$DQ$8022a5FOPbYk%d=%*xu-bEg1N>O56CaK zk<(Y~Or@y9qfX6c(iKd2n+;<$8D(5jJnBycgSBFr@&zkChhC(Mq7l`r_F!2PfmDzQ z_9Zz}X-21QJ943tRYcVf{2}Dtufrw4Q)i`ETNzQIuu^lS!VR-2=G*mXqAYiFy`kgm;-G0lcoQ+_5sT86t&1fF|5m#E~47odG7?YUSMm z7Y-)tCZfig%&;TJ+f*nK8uv?f4=hGDui7@}sdo@YqbF<8jdLX2w8Nj;aV= zv*bt{zK$~+93_L!RNvk8F;Hr3Q`CAfdN9t%QhZ74rH1X2o%2sK$wXYY(VTA-j`%mZ zJEV!~YgQJqV>@@ai^C|4w0g|E{baDFQAyoDu}ysiZ#2&qZK+N^V^d1gN;RwlDa~sL zJZ1DV*}C0qk0x2$01zjcY15rgNItdXWxSnykP8~wiI?tyac8A{B={NWhzpwV4M27u zI*B{L-5@Fm@tpF0I};y)!J9mJ(M!qrD zEK)@FM?p^v*%O@C^!ETYQLFajIa{GOqQm}id~A=D5*|}=>P1HypeB>Pq?LV&=^FT& zeReXc)%;3vR4Zn@0iZzyW7A}#u9RfQCU*;(3`17MU)#zZL4H6`6ES~c%}yPDITY*l zg`v?>#9SbkuDiT}M58;+hP`TYY=o1OK-FGpqy)rvy1lMI$X;M_8C5ir>3SXM#?fjc zo68P@W`m`R?&?IYIyW@R3QZ2H;+-F~xVyLkUIR~~W#|O1IpR(EGhf{Q&F4S#&fUqOe;Vyl`^hajVPBtdWt(}8F^=^MJdIm)Z#DwLRIiniof=o#VqG~yIJMPI`foVqH5 zu)Au?_Txlo(IioNBo2Fj&fSqXY={u<1>panmI-3)_g#*>9+u2dCZF=86{SPfdqrQ_ z!3{cIcbw7P9jel=gcH$dziJ@af@i~=jq_yVRTm$16_j2vp3wpwu{A>cHsh@2MhQB2 zBuf*CkV~2jS-DTmnL85dY%zypl!Pqt)vQhJ9o3a1wLC?Rre>6OoEAyrcQaF`l3!xNX=?mA&e26YFfA0Dm7S-+8)}& zS}Po#1`Q9BV%gy&pGZv7!H`m@d-`UEf2R{b)@WUex#2>1)fT$1zUU=&-xXk<_bmZ30I;~XM4?X zdD@%W9a^l?Es0z`5Dz-C-k2UKb^L9oXy;Pi4cV>Gq)7h1xSczM{{w;yP8AusJVSuP zJ0d3FV0v|+GgT^uZnjRDbumHveBO3n7xY@*g#RDXv@kn_lei;+R0IG3@&A=6?UUOYfGhp=9MFiw zSaIsB*h@l&Du-QqG^mYJ<^Ck!)Z^85Z0z+khvi{1>Pw2WE#zv|T0@^#ip1Gez;s8P z1AkGqC0iRO6>xV*6JS!19e)yc3}z?_4Y882KXOZMr*jk^Ag^*%Z#9x~*qv}qn4n8^ z4JPf5oQaD=z7-!DV%;S*)UKnI7TjE4oka9}XguusxIj^30Uz2jqjeA0&+?_slM1*a zG&xi{;E1~8wsVJT61NA89Tb)o_nS$Xi}vg-e&p6NX3FUePped@+3(v+{p>KQ#p9Gq zEr)DvIwh2tP?8TR-ng^UOgUX8dz)+dea?x~#ztDnu$}420W;pO(;H_2ay2`oY32X3 zbM7|O9TFMvCI`qa8YdO6!wAv?sy1lEs=h&N(yRK#{)8$ISU2Ny)yBD2G#YEW4UtP` znhuYW%c(g*>y3+APa#7S0gGjk(H}kxbb2O`-luOwh**Q(b<>jwmb?m$c{`H%` zbMq%}4sLpHK62xg8(+Bby*Hk@v3FzV`tMx-$?LuA?&}YuF5oX+``ESoHQUv1?3Q<> z-OE@1>D9Mf`R}j%^(&DpZ%3`bFJAtemvfijyZfiR|K0A7uKshWbLm5u9=iAk7e9UR zJ1>S|L-?Z$pTF?k7t{;yKL01@zjFSEcK+duHN6Za-q@;kSopaA)V@pOG*!Qe5%k z{55iS2gDnY{K%9+?2#;<`J#OPe}C?woxKC`%HjKG7VpnKi~B3^%79jabSm*8Y4E?H z0T0~T5C6gc3fx+&9c&*L7wrfgSTHQ{k^(TC2s%>eWgG&?fB)Z|hj#8h_kX?g^?&?F zE6vy01QGy{NPwpWZxc(&XTB)>l>4yRAN}MHTg?zD4XR6SXH-e%$tF^ z&(EcC=I%jY-qm%Q3(>$4iRu@mr>}tK`w#pJiLZ5W`946u1oHaJgvA5jDW3;2DXu5v z{THqGy{Em85F17!@Bhm$laX@I-(U8fjFbf~2vj}<$dSQC=jHpqXEku0)_`=Eu^Rl( zRs-j04d7wsW)%KMtAXQ`2IPJu@fL;O`Vi^EhfZsNa-XzpHTZ`Xg{MzxKxU^TSPfpV zDEzh48t_O&p0V@`&k>eBcv=Gn3Ai%~pV=V|K5$9{n68nsVX^dii^BeC4N%sEnsxfW zwi*b>H^52la81%s{Jga$XCuv@(g1#76xy;H%to3!tpVzWakB=qk!DY6z{1BM%vhR@ zG;>-5Tw+AYVre$g^l1%{$U!q!gV{(^r!;`k1s-gx!EB`Mr!}z5y9eJk*Lv$}24>zk7Q@ccJaq=n9C5qb>FQ z_c0Q8zxA}&N&X*02JY|e;PrQ$_BycAC62uQV-V8b!JYa0$BzPTIJktdmQ=rNHCV8} ztpSdwhZEyhtOkn?Y-xauE!+hx3jgRiGH&R?VF{-r0-8+>RF6i|i1q$#K`#b!3ju`u z$%h=x7WA!C`Yhm3L5UUxvY_uCJa!64BxO*r-k+`KM^7Pulq{fYSPf?D`H|Baa0F@G zYA{>RIHb)c+m=4_WOdGl?PoCxfhX`jprWs@oM&c}ZU&(R{Q{50B0S>o5h5fQlG2O# z9q!y*HTd6&PdQHb6g;i4x2-QJ5}Y&tl=USt((iwKF4pBaX>BE-nkhM&%~TT3V&8Z4 zz=^lty^!^mWsHONx>GZ*DW3W&}pv&UyNAz z5+-^5H$H^dZ=Cizf*fbO|N0JIzkc%TP$DphTVMZg$=9!)_ByF0XHDO);Pv-kJz0(b zHxXr)u>Oyq!^f|j@^SEuy1wLfqLuGnKIL@=;N-Kf6P^72-BWnMd1E%eh`zjg>6DK{ z_W@Op@IsW?-HWHZPRSS}>vc;BTsVansA9@`n<)Fc=TG}MGJgeYB@xYSJ9lO0=_8la zrG^mzS%%uZG>}Cu;!B2i4MMd=9Je(8W&586{3N!UwD4!{TIq-ZvNKI z=WjM{e&FWS8~@_Q&)%5c2;6w&`sc2H#EFZSU$guKw6n^QwGx z_sT!N^3zuaSKL?La`|6g{)Nl;FDsXy*!?%V-rY-=e*MxD7k}~M^^4!R`^5|83%`5m z`_F%g`~wa4;LigIJdnTx3Hj52q%Z#Jmmk`J)@%RvH-7Hqb_5Zaq7a3N+uO38^xOaT7fx*lOBx*aJlgs5{|D48 z?0zr*+5JB~!GMQ)0Lb40q9N)dz;^QVA3p;&FM4)b|Ni+`jwfeX8_|r0W0xbR02w#& zr++uP^U&UNfBZc^{rr~RaTYKsoLRyioWZD$&Rsn7<^221=Wy);mw)hkA3uQ{LUjO> zB(bP4L<=By^Jji*2W?idjIZU|NaT|IJASa z5`9D72Wj|EK8J>c&#!!f)KEKS)Ge_PY&L@D3@}51+)rkFUdrzXU-^-LXSKuryFOc1 z+KIp$Aor{f2p@)2$k_eb=l;nGwdMJDW_$%Tnm}!^h^fTMYDR4LBRkDs``mMpK&+Z4Hb6|DC|MMUIcM+S@lzTgdWoiHeIOQ{>*s?gMC(6y_D{}!@XSw~ z*}3r1YrlB)8`m2*p1k<3o7%;Hu}h!-r&oRzFZ|$@%a?xWa^mWY91#R3-Sv`TiAMVbXZ2s;}M+Y$c!g$YX-P?D5w^6X~{}jQat&|IO(pX zA_!ES#!3}l^00Ovm&vDx<5b7sb2WEAmnoGZwoEJ;P3DwrBd9U80OM~C+%1%z$kekv zd~_W287uJ*3~mH#>`-s?sQEWS*!&@S8{)mlRcaQhk6;UeEY16l8+4q#R&x{HfTrab4e$Xz5QBox5NM?Jque%zzd$^L8?1<9t z%R3(?y6Q+(#~E{&Ik^6zjxQAM?I>!iu`xZ^4m(F|zb6bL!BJOjbrXpio9G9Pa=suo z>_9p-JrynNlxl5v-mImg8joOO2Zh=<&bBsg^#-=qFjBBL)PZ|ivx2Y=_~aJZA!r3D zDHKYE;Rb_*_<8OQPfCLzijmY}AW27}yx%FYsc@h>7CTi3+oYpIqoBH}vYJ%e(@rPa zwKW5Jb>#8qD(z~*ma40PLW(KZw18<&OQP8|JQ*?OYiwiy8KK0jZ{^Vqm9t3Ga ztcKZvTNxPfM$ns1^a@;1=-IQ$v6kk818=M@jH@lG=&=cOl5UM0D=Lz<82ON$ zV?uG0j<_VlRd-R@zN5j%(vcDG%ezbEq-1s@VYXQ<4*=U62{TIF%%r06TB2Sq`od#h zLNVN^CBDx03ODe#aQp$EIBqh`*+x_?90bJGvX~5Wz7dj#YceXk$~ITGTASpkKsj7# zk5X~3Q8Afx`iOO*Ir*7zm1Hk8580A#cerhHh1?$M|VVN)4Pf#hFRPf@Y#!s19;1C*7LL zwc0cf`1EKjRg#<2YFAP+lFwG?W@^3ohEP|yVQze7$2?|%%~-QECrXJRl8U#fDN*gZ z-LjtbYNlA!?7@zit$48fBMM}i&l=4b*BNVh8zY&MoZ@R2ouLft8Mb4BI4pJ!_}p=` zZgpw_*n$z3^5IEAV#J`sUF)C@}+{f5~fO25Of~a zE{U6C5dVSIqHf_E!w(6eh6Rt{fR$pxE5bugJ`3ZXZP0O)v_W2S7s@@O-R|dUecB8p zY>K?oULjiju=o8Gm%^uX_CC`vH{B_kdyra5f+%C5> zNBCZ2hm{X*rM9dV=vb&RqVh!n2*tN`hApDxXcmD0J+7JLhBPS2V#TdLIyE8#rOtZLR{3K`UE4i7wio44Z%4pD2z@#jxdf4C8y|KWSX!|n$sQ?V2wCUDKu);Zmi`)r3{)pqPqF;abv#N zc3?q8kc~_&HT$aa1k^GLs{+_P(WxZl@N|@`xOGHPyJD?+D?r<7IXbTtMuS$g>To-Z zy5TbNy2K~a?Q|l~_O(RYY_X8w9vSF}J>$m({z#4x$SW_|PeQ z!~70ywS<;g@J+C`0E9--Pd4C_Z|0nV+{Bo~>lDlPv-!MBZANRl5t94it~WXvcubUl zm~_Nu3t03{Th5dejtoX(bILl+eNC*ionTEIm) zVb+DDkq5geZ?ebY>WQQDewvBt!`@_4b%|Boh*7jqkTSjQs4({Q-ASWtjKz>c4;S-H zQ>d1?n!}mTmRqTGCmI~|#A>}3=}#lrL-TGgm8;l{jEBn?kK=pJ4$B_giXJzMXY<6W zos611Bq@4-EQOi0nj1x`T1u{rUD=Mm?>6isf6gN~10C-)8I>BDykty#j(jFuEr^|N zvo2*aWkZ<@~|V|(ZMt;&6&rk_+wsV1^Wiq4WAx0i)Uf`%aZT5`&oSvFA&=Z0dm zt!HSr7Kq1+l-VCDlFimq3-!7>sH$F`cGRc&RJJ`HDXLgC*_?A|TOU~5esN$!w7ekW z7|e@#tN!1$3*WwTGj*eT{Rgl8XIFpj%5PkLW%r-&p1=4f7vG5o5B@xmzyk?9kiY{8 zyjBSuxSs%s(asVd^bo{!7092SAO2C_2ldPdcE~p!sYo~xRdy3{FaFMTY`R%D)*#tv z+y#)jgzFzbJ6VLIk?i(EyW!#yg9=8#_<-?w1!-vIr+5UD?et<4vro_Q-eJl)9du|0 zA9U^U75Je2%ZKo?YvlkAyM${Qzye_l01U%=kMM+GsllF3odmuuzApHnITwexN{cQo z;e)mmqj(KyTjD|d&Z`|^V0s`N16le&Pdp+IJmN_b7LSziEh8-ahm0^TN{8h}uvlL8 z2;aOt>ym}S_Vm^fCLbr2PGRjq`Kl#E=j?84J;K-nq>?@XAxe1NLwsBxmqSdWR*zM+ z=xD`}*D%P!!FPPCHRl)b*T-q2S+{XQ1pF*1{<<_$ES?|_nIVXx-~56juT@Vy`wnZ! zPwOB;{!#cCspv3G0zqYU7F&;yMoFWG67|OGVf^izOSYTI1kl2l2;(&vK5iqNzW5DYr3Lt5`A<(*5;7~TOz%ZpE zFse!g*ogQ7Ltu)NC`^~CHi~x^n8h0itY-=S|9f|y*}3@>H$RBT|A`g#pT72QuKnOO z3K{dk>WcUBS1pAn)GxyKj+4&MaxwCWcz1OXn02IF4-Q8It_aC0yW(O#gs9mW)F?%#S zNHVB-$8(QAxsc5ILlhSjpZ&Hu1;;TI4v!=ZP3nZt7s#AA!Ck;%1=*Q*Y+D{erQCbZ zoWMpG=4S`YLByG^5wIQs4xL!+D2l$l)`gXaq=d)4cb`N7dod}YgfRsy=1Ldv5nxV{ zN>u>Jm-%nKz4aj{KKmpnK*t_442vBPs|n^Hb$l1r zHXNjQuNXEl3g`k#Rb!i?pg2Y5?Q5g3@{j_4>$6WB--Q{4nf?bfFpoN6b0IB=a|k^_ zv;uo;IST8~kch_Ld*=yVAkG;SVnQtfBvda5pbIk>1HWhGL5uujYeHIi2y9~Z-s2}x znCUYs$9Byhx%dbUp}Z_SdOQVQL}Doa?49eDoA&59Tk#lVFp1rdflGJMh4~W_y-Z=b z$=utP1dcwGn0vR6?}GK_y!rvm)GNwt@d#DMc@+PZWs}0}Ap*I% zdv8C1!Xdmpyk#i$xlRFfZ4nD4i>NzI32#0^Vfhdj&S!6J9cZjfq8douN92vJ5|}+f zDg-7Y&GK*B-|~<+{hxgdT{uQ#U^MX==p>{}!WW2a-qnx50syco`20yKH~-LbJ)_y$ zGg`R!=vEmm2nYSq>X2oM-QFU1uPm7A!W#DUY00X z&_<vR|?#q0U|-I6r-8-0ox}pVWhEhQ}m@HeONx^WcuEf(0395OoV15P{g(FKQ(z^cQe3Jxt^Ix^5J&v^FU?#W$% zhQ}ZaKp0)0ti-5-C>Yd)gWvh$Q6ViKGV-&RPVRz5fx?;o$Z|lO4R$xETOdAMJR+l& zXFPrH;z?bA2O2r#xHI5TztIH>j}YmIE9Zry6maD{d?+x_UO1@>GYSZw$5~~A02L;M_Yol@lEu!SKT2Ww5Xo=YH&=bHaKQg3 z$K|%n{Jg8_znh#=u+hW0DzV?h%-~rt>S5m7k{tngZ*3HV_gf z#!TX(K$Mt>$ie-3pOX5r9_E9blAe?XdMPHR0vVbb)~nHcr8jU3c^^|7H-oO0!j@9B zv#EqsHlA`$qs2+w%?)Y4FFn@$VvFH2l8y0=8>_iQ3}dA!FW3Rl>cOqXaXC3)4pt#p zf8Zs9jTDx?eRoiEBwR|?bht+}+evzwi7=lTw(BmJUD1@1YcwwOV#T7J)-_{d)=hI1 zEtNyroS?|rdP-`>9X7@`s)k%))!vQ^gHfyLT*)gT%n^Itf+kNRWw?4=%J598Frgub z6}X~6$5@%ROTK{#ibSVkgJ+uVnlF>rxO}vqvUgikvDF&YJD!QGb~#;+$x^QqFGZM% zt6=2Q6TPcd>*27jXr77P7NR_^PBiS{9Vsr+@wEM(Y*txKCY%X%&JCQR+`+B-R-vNg z2tr0g08b-2aDRfVMxGu!E7_2mFJ&iO%3om{ZJ&*G4C(SH!<9yvNQ(l~h{F_anKGqq8B@6`G8UbiLZ~Qx$Ot@DF3E5)}pu z`?^#QKLQ^Jfi&pU<&jByl(M5$9gmY@Kja7oJE1}`o$gS&94}O+Svjux8i`uNQ}dQw z@qC}IKD#L5~wEPe1;`MA8Yx#GfH zMUW>gq5fUjO;wfHSRG`kXt~cu5EUjm8hyc6?HO5=_v}g~nGLFZEX4KAZkx~b5~!TU zC1Mk?YafRk({Z+G^SIlhGdVIwW1*^r%uS(U&sp0o< zwoF)R(4J`4Ic}Dca@!pXIAy3UvOF_4o?Z*Y9@0;}1m^aunnkB{Nbd&=Mug3>2drHvgp6-;r za@0$;8KzeENvk>tfwMF24r!8FOWZT_3AvhKKif$UQ9cB?r&Ouij!hbBC!b{W)sj%KA=&P1YV7Mi4Frrs=?U4C^j zV?vVX%NZ;aXwty{;0yB!39SBw8iec+Yl42j9f!Q^)zkB;SQ((m)AEyLYS?kh)qc(< zOr2@UQ7)vtj;^i8QTMf+qr%%{qsMlX}<#;dJP#TJfJ z4X?}ehsqO^;rxSu)@!z@Tre7=YCT;-WyI9PQMTo2cWe@#$|+us$g3D6Sj#i+ESe0q zVzMiK$CfcC4p7TMIs?pDoetQc4)rX;UL^W;V`)Uot;$eopkl3*PO(L0;_3^vb+M8t z(j_}0#{Ap2NEI()Vq=R!#4KvK+?FDYE;|bo%E%7uy>v3>Y`WC2Sgwg|%8)&ia@FRY zx~f`?ktS0nRcHX{X%uRd3gfisrvn~WtgQ|0?MiG|Wn8G$9gg`#ew{gz3%;bukP?L5 zW4K#fW+?(Zg}EV%`+YIg@}@F%x#e$#g{0jx>W@${nRVHlm3ZA8@?B4%o2=wHb>MBp zX|ojl1m${v~QehEw)9Px=UH-1hQf*ng|||^E(Gz z^Y~Ny;=&^$!D}V4hkODySmPCzMNmbzzv1$ zhl5+~W2Cyg>;hK>(@~@ZThe7@lx){uoO)WKTy7V9vdf>V4O(@fQ!A*v*PRMdW+#@M z1l_)LpHaPIXDAlx)MGST@_BL4@MaTga+1wenpC@;4vy2A8S>QO{SrG5;sF`+_?{6H%r(3X%K&xf|DZ zZu)`xe{JJd|KKkkNZ^449!TJU1RhA>fdn2%;DH1lNZ^449!TJU1pW$2;NaHa=uMSa z3gCw)IVOP3*prDtisJSq%_Vut`Jvi&x$8(bcX>)mu09UQ(L8J<84!a*jpaWZ>pe)yF1DBThL?#5%=i+ zN!*bCn4c-sc=V6qj+K=1SD(llS zlG01-Y9yA8HyOZLwi0!p(x~-gQDDqZn|a-D>mJ9QMU#Lp5Gc(axLdI3>U5GuwZl0wn%ZXlwMZ(YxkS$GE9<2i zBXzV)C7cNannPz?*4k`cETk1b7pqH5MvXg%y^1fA8@dd7ZWy156$Iz(@UYlO{~vZB BBq;y@ literal 0 HcmV?d00001 diff --git a/backend/F9WebAiTest.Api/life_circle_rbac.db.bak b/backend/F9WebAiTest.Api/life_circle_rbac.db.bak new file mode 100644 index 0000000000000000000000000000000000000000..ecf4c623e73d4d405a036fa505d54a9e06f28cd8 GIT binary patch literal 299008 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 literal 0 HcmV?d00001 diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/Departments/Commands/Department_ActiveOrDeactivateCommand.cs b/backend/F9WebAiTest.Application/CQRS/SYS/Departments/Commands/Department_ActiveOrDeactivateCommand.cs new file mode 100644 index 0000000..602009d --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/Departments/Commands/Department_ActiveOrDeactivateCommand.cs @@ -0,0 +1,31 @@ +using F9WebAiTest.Application.Common.Exceptions; +using F9WebAiTest.Domain.Interfaces.SYS.Departments; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.Departments.Commands; + +/// 命令定义 +public record Department_ActiveOrDeactivateCommand(long Id, bool Active) : IRequest; + +/// 命令处理器 +public class ActiveOrDeactivateDepartmentCommandHandler( + IDepartmentWriteRepository departmentWriteRepository, + IPublisher eventBus) : IRequestHandler +{ + public async Task Handle(Department_ActiveOrDeactivateCommand command, CancellationToken cancellationToken) + { + var dep = await departmentWriteRepository.GetByIdAsync(command.Id, CancellationToken.None); + if (dep == null) throw new F9NotFoundException("部门", command.Id); + + if (command.Active) dep.Activate(); + else dep.Deactivate(); + + var result = await departmentWriteRepository.ActiveOrDeactive(command.Id, dep.IsActive); + if (result == false) throw new F9OperationException("启用/禁用", "操作"); + + foreach (var de in dep.DomainEvents) await eventBus.Publish(de, cancellationToken); + dep.ClearDomainEvents(); + + return result; + } +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/Departments/Commands/Department_CreateCommand.cs b/backend/F9WebAiTest.Application/CQRS/SYS/Departments/Commands/Department_CreateCommand.cs new file mode 100644 index 0000000..34906a2 --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/Departments/Commands/Department_CreateCommand.cs @@ -0,0 +1,35 @@ +using F9WebAiTest.Application.Common.Exceptions; +using F9WebAiTest.Application.Interfaces.SnowflakeID; +using F9WebAiTest.Domain.Entities.SYS.Departments; +using F9WebAiTest.Domain.Enums.SYS.Departments; +using F9WebAiTest.Domain.Interfaces.SYS.Departments; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.Departments.Commands; + +/// 命令定义 +public record Department_CreateCommand(string Code, string Name, long ParentId, Department_ManageType_Enum ManageType, Department_DutyType_Enum DutyType, int SortOrder, string Description) : IRequest; + +/// 命令处理器 +public class CreateDepartmentCommandHandler( + IDepartmentWriteRepository departmentRepository, + ISnowflakeIdGenerator snowflakeIdGenerator, + IPublisher eventBus) : IRequestHandler +{ + public async Task Handle(Department_CreateCommand command, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(command.Code)) throw new F9OperationException("部门编码不能为空"); + if (string.IsNullOrWhiteSpace(command.Name)) throw new F9OperationException("部门名称不能为空"); + + var dep = new Department(snowflakeIdGenerator.CreateId(), command.Code, command.Name, command.ParentId, + command.ManageType, command.DutyType, command.SortOrder, command.Description); + + var result = await departmentRepository.AddAsync(dep); + if (result == false) throw new F9OperationException("部门", "新增"); + + foreach (var de in dep.DomainEvents) await eventBus.Publish(de, cancellationToken); + dep.ClearDomainEvents(); + + return dep.Id; + } +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/Departments/Commands/Department_DelCommand.cs b/backend/F9WebAiTest.Application/CQRS/SYS/Departments/Commands/Department_DelCommand.cs new file mode 100644 index 0000000..d1819d1 --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/Departments/Commands/Department_DelCommand.cs @@ -0,0 +1,22 @@ +using F9WebAiTest.Application.Common.Exceptions; +using F9WebAiTest.Domain.Interfaces.SYS.Departments; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.Departments.Commands; + +/// 命令定义 +public record Department_DelCommand(long Id) : IRequest; + +/// 命令处理器 +public class DelDepartmentCommandHandler( + IDepartmentWriteRepository departmentWriteRepository) : IRequestHandler +{ + public async Task Handle(Department_DelCommand command, CancellationToken cancellationToken) + { + if (command.Id <= 0) throw new F9OperationException("部门ID不能为空"); + + var result = await departmentWriteRepository.SoftDeleteAsync(command.Id); + if (result == false) throw new F9OperationException("部门", "删除"); + return result; + } +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/Departments/Commands/Department_UpdateCommand.cs b/backend/F9WebAiTest.Application/CQRS/SYS/Departments/Commands/Department_UpdateCommand.cs new file mode 100644 index 0000000..e26f6cb --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/Departments/Commands/Department_UpdateCommand.cs @@ -0,0 +1,29 @@ +using F9WebAiTest.Application.Common.Exceptions; +using F9WebAiTest.Application.Interfaces.SnowflakeID; +using F9WebAiTest.Domain.Entities.SYS.Departments; +using F9WebAiTest.Domain.Enums.SYS.Departments; +using F9WebAiTest.Domain.Interfaces.SYS.Departments; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.Departments.Commands; + +/// 命令定义 +public record Department_UpdateCommand(long Id, string Code, string Name, long ParentId, Department_ManageType_Enum ManageType, Department_DutyType_Enum DutyType, int SortOrder, string Description) : IRequest; + +/// 命令处理器 +public class UpdateDepartmentCommandHandler( + IDepartmentWriteRepository departmentRepository) : IRequestHandler +{ + public async Task Handle(Department_UpdateCommand command, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(command.Code)) throw new F9OperationException("部门编码不能为空"); + if (string.IsNullOrWhiteSpace(command.Name)) throw new F9OperationException("部门名称不能为空"); + + var dep = new Department(command.Id, command.Code, command.Name, command.ParentId, + command.ManageType, command.DutyType, command.SortOrder, command.Description); + + var result = await departmentRepository.UpdateAsync(dep); + if (result == false) throw new F9OperationException("部门", "修改"); + return result; + } +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/Departments/Queries/GetDepartmentAllQuery.cs b/backend/F9WebAiTest.Application/CQRS/SYS/Departments/Queries/GetDepartmentAllQuery.cs new file mode 100644 index 0000000..b7859fc --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/Departments/Queries/GetDepartmentAllQuery.cs @@ -0,0 +1,13 @@ +using F9WebAiTest.Application.Dtos.SYS.Departments; +using F9WebAiTest.Application.Interfaces.Repositories.SYS.Departments; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.Departments.Queries; + +public record GetDepartmentAllQuery() : IRequest>; + +public class GetDepartmentAllQueryHandler(IDepartmentReadRepository departmentReadRepository) : IRequestHandler> +{ + public async Task> Handle(GetDepartmentAllQuery query, CancellationToken ct) + => await departmentReadRepository.GetAllAsync(); +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Menus/Commands/Menu_ActiveOrDeactivateCommand.cs b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Menus/Commands/Menu_ActiveOrDeactivateCommand.cs new file mode 100644 index 0000000..7781726 --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Menus/Commands/Menu_ActiveOrDeactivateCommand.cs @@ -0,0 +1,31 @@ +using F9WebAiTest.Application.Common.Exceptions; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Menus; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Menus.Commands; + +/// 命令定义 +public record Menu_ActiveOrDeactivateCommand(long Id, bool Active) : IRequest; + +/// 命令处理器 +public class ActiveOrDeactivateMenuCommandHandler( + IMenuWriteRepository menuWriteRepository, + IPublisher eventBus) : IRequestHandler +{ + public async Task Handle(Menu_ActiveOrDeactivateCommand command, CancellationToken cancellationToken) + { + var menu = await menuWriteRepository.GetByIdAsync(command.Id); + if (menu == null) throw new F9NotFoundException("菜单", command.Id); + + if (command.Active) menu.Activate(); + else menu.Deactivate(); + + var result = await menuWriteRepository.ActiveOrDeactive(command.Id, menu.IsActive); + if (result == false) throw new F9OperationException("启用/禁用", "操作"); + + foreach (var de in menu.DomainEvents) await eventBus.Publish(de, cancellationToken); + menu.ClearDomainEvents(); + + return result; + } +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Menus/Commands/Menu_CreateCommand.cs b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Menus/Commands/Menu_CreateCommand.cs new file mode 100644 index 0000000..928afe4 --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Menus/Commands/Menu_CreateCommand.cs @@ -0,0 +1,34 @@ +using F9WebAiTest.Application.Common.Exceptions; +using F9WebAiTest.Application.Interfaces.SnowflakeID; +using F9WebAiTest.Domain.Entities.SYS.RBAC; +using F9WebAiTest.Domain.Enums.SYS.RBAC; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Menus; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Menus.Commands; + +/// 命令定义 +public record Menu_CreateCommand(string Name, string IconSvg, MenuType_Enum MenuType, string AssemblyName, string TypeFullName, long ParentId, int SortOrder) : IRequest; + +/// 命令处理器 +public class CreateMenuCommandHandler( + IMenuWriteRepository menuRepository, + ISnowflakeIdGenerator snowflakeIdGenerator, + IPublisher eventBus) : IRequestHandler +{ + public async Task Handle(Menu_CreateCommand command, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(command.Name)) throw new F9OperationException("菜单名称不能为空"); + + var menu = new Menu(snowflakeIdGenerator.CreateId(), command.Name, command.IconSvg, + command.MenuType, command.AssemblyName, command.TypeFullName, command.ParentId, command.SortOrder); + + var result = await menuRepository.AddAsync(menu); + if (result == false) throw new F9OperationException("菜单", "新增"); + + foreach (var de in menu.DomainEvents) await eventBus.Publish(de, cancellationToken); + menu.ClearDomainEvents(); + + return menu.Id; + } +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Menus/Commands/Menu_DelCommand.cs b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Menus/Commands/Menu_DelCommand.cs new file mode 100644 index 0000000..0f0c1d6 --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Menus/Commands/Menu_DelCommand.cs @@ -0,0 +1,22 @@ +using F9WebAiTest.Application.Common.Exceptions; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Menus; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Menus.Commands; + +/// 命令定义 +public record Menu_DelCommand(long Id) : IRequest; + +/// 命令处理器 +public class DelMenuCommandHandler( + IMenuWriteRepository menuWriteRepository) : IRequestHandler +{ + public async Task Handle(Menu_DelCommand command, CancellationToken cancellationToken) + { + if (command.Id <= 0) throw new F9OperationException("菜单ID不能为空"); + + var result = await menuWriteRepository.SoftDeleteAsync(command.Id); + if (result == false) throw new F9OperationException("菜单", "删除"); + return result; + } +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Menus/Commands/Menu_UpdateCommand.cs b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Menus/Commands/Menu_UpdateCommand.cs new file mode 100644 index 0000000..8e7ddbf --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Menus/Commands/Menu_UpdateCommand.cs @@ -0,0 +1,28 @@ +using F9WebAiTest.Application.Common.Exceptions; +using F9WebAiTest.Application.Interfaces.SnowflakeID; +using F9WebAiTest.Domain.Entities.SYS.RBAC; +using F9WebAiTest.Domain.Enums.SYS.RBAC; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Menus; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Menus.Commands; + +/// 命令定义 +public record Menu_UpdateCommand(long Id, string Name, string IconSvg, MenuType_Enum MenuType, string AssemblyName, string TypeFullName, long ParentId, int SortOrder) : IRequest; + +/// 命令处理器 +public class UpdateMenuCommandHandler( + IMenuWriteRepository menuRepository) : IRequestHandler +{ + public async Task Handle(Menu_UpdateCommand command, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(command.Name)) throw new F9OperationException("菜单名称不能为空"); + + var menu = new Menu(command.Id, command.Name, command.IconSvg, + command.MenuType, command.AssemblyName, command.TypeFullName, command.ParentId, command.SortOrder); + + var result = await menuRepository.UpdateAsync(menu); + if (result == false) throw new F9OperationException("菜单", "修改"); + return result; + } +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Commands/RolePermission_AssignCommand.cs b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Commands/RolePermission_AssignCommand.cs new file mode 100644 index 0000000..653dc9b --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Commands/RolePermission_AssignCommand.cs @@ -0,0 +1,80 @@ +using F9WebAiTest.Application.Common.Exceptions; +using F9WebAiTest.Application.Interfaces.SnowflakeID; +using F9WebAiTest.Application.Interfaces.UOW; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities.SYS.RBAC; +using F9WebAiTest.Domain.Enums.SYS.RBAC; +using NotificationTypeEntity = F9WebAiTest.Domain.Entities.Notice.NotificationType; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Permissions; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Roles; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Roles.Commands; + +/// 命令定义 +public record RolePermission_AssignCommand(long RoleId, Permission_Type_Enum PermissionType, Resource_Type_Enum ResourceType, string ResourceIdentity, long ResourceId) : IRequest; + +/// 命令处理器 +public class AssignRolePermissionCommandHandler( + IRoleWriteRepository roleRepository, + IUnitOfWork_F9 unitOfWork, + IRolePermissionWriteRepository rolePermissionRep, + IPermissionWriteRepository permissionWriteRepository, + IRepository notificationTypeRepository, + ISnowflakeIdGenerator snowflakeIdGenerator, + IPublisher eventBus) : IRequestHandler +{ + public async Task Handle(RolePermission_AssignCommand command, CancellationToken cancellationToken) + { + if (command.RoleId <= 0) throw new F9OperationException("角色ID不能为空"); + if (command.ResourceId <= 0) throw new F9OperationException("资源ID不能为空"); + if (string.IsNullOrWhiteSpace(command.ResourceIdentity)) throw new F9OperationException("资源标识不能为空"); + + Role role = null; + long rolePermissionId = 0; + + var ok = await unitOfWork.ExecuteWithUow(async () => + { + // 查或创建权限 + var dbPermission = await permissionWriteRepository.GetByIdAsync(command.PermissionType, command.ResourceType, command.ResourceId); + if (dbPermission == null) + { + dbPermission = new Permission( + snowflakeIdGenerator.CreateId(), + command.PermissionType, + command.ResourceType, + command.ResourceIdentity, + command.ResourceId); + + // 消息订阅权限:Code 对齐消息类型的 RequiredPermission(业务权限码),保证订阅校验一致 + if (command.ResourceType == Resource_Type_Enum.消息订阅) + { + var type = await notificationTypeRepository.GetByIdAsync(command.ResourceId); + if (type != null && !string.IsNullOrEmpty(type.RequiredPermission)) + dbPermission.SetCode(type.RequiredPermission); + } + + await permissionWriteRepository.AddRangeAsync(new[] { dbPermission }); + } + + // 绑定角色权限 + var rolePermission = new RolePermission(snowflakeIdGenerator.CreateId(), command.RoleId, dbPermission.Id); + var result = await rolePermissionRep.AssignAsync(rolePermission); + + role = await roleRepository.GetByIdAsync(command.RoleId); + role.AddPermission(dbPermission.Id, command.ResourceType); + + rolePermissionId = rolePermission.Id; + return result; + }); + + if (!ok) throw new F9OperationException("权限", "绑定"); + + // 发布领域事件 + foreach (var domainEvent in role.DomainEvents) + await eventBus.Publish(domainEvent, cancellationToken); + role.ClearDomainEvents(); + + return rolePermissionId; + } +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Commands/RolePermission_RemoveCommand.cs b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Commands/RolePermission_RemoveCommand.cs new file mode 100644 index 0000000..f915a6c --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Commands/RolePermission_RemoveCommand.cs @@ -0,0 +1,37 @@ +using F9WebAiTest.Application.Common.Exceptions; +using F9WebAiTest.Domain.Enums.SYS.RBAC; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Roles; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Roles.Commands; + +/// 命令定义 +public record RolePermission_RemoveCommand(long Id, Resource_Type_Enum ResourceType) : IRequest; + +/// 命令处理器 +public class RemoveRolePermissionCommandHandler( + IRoleWriteRepository roleRepository, + IRolePermissionWriteRepository rolePermissionWriteRepository, + IPublisher eventBus) : IRequestHandler +{ + public async Task Handle(RolePermission_RemoveCommand command, CancellationToken cancellationToken) + { + if (command.Id <= 0) throw new F9OperationException("角色权限ID不能为空"); + + var rolePermission = await rolePermissionWriteRepository.GetByIdAsync(command.Id); + if (rolePermission == null) throw new F9NotFoundException("角色权限", command.Id); + + var role = await roleRepository.GetByIdAsync(rolePermission.RoleId); + role.RemovePermission(rolePermission.PermissionId, command.ResourceType); + + var result = await rolePermissionWriteRepository.RemoveAsync(command.Id); + if (result == false) throw new F9OperationException("权限", "解绑"); + + // 发布领域事件 + foreach (var domainEvent in role.DomainEvents) + await eventBus.Publish(domainEvent, cancellationToken); + role.ClearDomainEvents(); + + return true; + } +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Commands/Role_ActiveOrDeactivateCommand.cs b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Commands/Role_ActiveOrDeactivateCommand.cs new file mode 100644 index 0000000..531581d --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Commands/Role_ActiveOrDeactivateCommand.cs @@ -0,0 +1,26 @@ +using F9WebAiTest.Application.Common.Exceptions; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Roles; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Roles.Commands; + +/// 命令定义 +public record Role_ActiveOrDeactivateCommand(long Id, bool Active) : IRequest; + +/// 命令处理器 +public class ActiveOrDeactivateRoleCommandHandler( + IRoleWriteRepository roleWriteRepository) : IRequestHandler +{ + public async Task Handle(Role_ActiveOrDeactivateCommand command, CancellationToken cancellationToken) + { + var role = await roleWriteRepository.GetByIdAsync(command.Id); + if (role == null) throw new F9NotFoundException("角色", command.Id); + + if (command.Active) role.Activate(); + else role.Deactivate(); + + var result = await roleWriteRepository.ActiveOrDeactive(command.Id, role.IsActive); + if (result == false) throw new F9OperationException("启用/禁用", "操作"); + return result; + } +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Commands/Role_CreateCommand.cs b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Commands/Role_CreateCommand.cs new file mode 100644 index 0000000..f38a501 --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Commands/Role_CreateCommand.cs @@ -0,0 +1,27 @@ +using F9WebAiTest.Application.Common.Exceptions; +using F9WebAiTest.Application.Interfaces.SnowflakeID; +using F9WebAiTest.Domain.Entities.SYS.RBAC; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Roles; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Roles.Commands; + +/// 命令定义 +public record Role_CreateCommand(string Name, string Code, int SortOrder, string Description) : IRequest; + +/// 命令处理器 +public class CreateRoleCommandHandler( + IRoleWriteRepository roleRepository, + ISnowflakeIdGenerator snowflakeIdGenerator) : IRequestHandler +{ + public async Task Handle(Role_CreateCommand command, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(command.Name)) throw new F9OperationException("角色名称不能为空"); + if (string.IsNullOrWhiteSpace(command.Code)) throw new F9OperationException("角色编码不能为空"); + + var newEntity = new Role(snowflakeIdGenerator.CreateId(), command.Name, command.Code, command.SortOrder, command.Description); + var result = await roleRepository.AddAsync(newEntity); + if (result == false) throw new F9OperationException("角色", "新增"); + return newEntity.Id; + } +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Commands/Role_DelCommand.cs b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Commands/Role_DelCommand.cs new file mode 100644 index 0000000..575255b --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Commands/Role_DelCommand.cs @@ -0,0 +1,22 @@ +using F9WebAiTest.Application.Common.Exceptions; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Roles; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Roles.Commands; + +/// 命令定义 +public record Role_DelCommand(long Id) : IRequest; + +/// 命令处理器 +public class DelRoleCommandHandler( + IRoleWriteRepository roleWriteRepository) : IRequestHandler +{ + public async Task Handle(Role_DelCommand command, CancellationToken cancellationToken) + { + if (command.Id <= 0) throw new F9OperationException("角色ID不能为空"); + + var result = await roleWriteRepository.SoftDeleteAsync(command.Id); + if (result == false) throw new F9OperationException("角色", "删除"); + return result; + } +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Commands/Role_UpdateCommand.cs b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Commands/Role_UpdateCommand.cs new file mode 100644 index 0000000..5fb17f1 --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Commands/Role_UpdateCommand.cs @@ -0,0 +1,25 @@ +using F9WebAiTest.Application.Common.Exceptions; +using F9WebAiTest.Domain.Entities.SYS.RBAC; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Roles; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Roles.Commands; + +/// 命令定义 +public record Role_UpdateCommand(long Id, string Name, string Code, int SortOrder, string Description) : IRequest; + +/// 命令处理器 +public class UpdateRoleCommandHandler( + IRoleWriteRepository roleRepository) : IRequestHandler +{ + public async Task Handle(Role_UpdateCommand command, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(command.Name)) throw new F9OperationException("角色名称不能为空"); + if (string.IsNullOrWhiteSpace(command.Code)) throw new F9OperationException("角色编码不能为空"); + + var newEntity = new Role(command.Id, command.Name, command.Code, command.SortOrder, command.Description); + var result = await roleRepository.UpdateAsync(newEntity); + if (result == false) throw new F9OperationException("角色", "修改"); + return result; + } +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Queries/GetRoleAllQuery.cs b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Queries/GetRoleAllQuery.cs new file mode 100644 index 0000000..d8564a5 --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Queries/GetRoleAllQuery.cs @@ -0,0 +1,13 @@ +using F9WebAiTest.Application.Dtos.SYS.RBAC.Roles; +using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Roles; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Roles.Queries; + +public record GetRoleAllQuery() : IRequest>; + +public class GetRoleAllQueryHandler(IRoleReadRepository roleReadRepository) : IRequestHandler> +{ + public async Task> Handle(GetRoleAllQuery query, CancellationToken ct) + => await roleReadRepository.GetAllAsync(); +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Queries/GetRoleByIdForEditQuery.cs b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Queries/GetRoleByIdForEditQuery.cs new file mode 100644 index 0000000..cccafbe --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Queries/GetRoleByIdForEditQuery.cs @@ -0,0 +1,13 @@ +using F9WebAiTest.Application.Dtos.SYS.RBAC.Roles; +using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Roles; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Roles.Queries; + +public record GetRoleByIdForEditQuery(long Id) : IRequest; + +public class GetRoleByIdForEditQueryHandler(IRoleReadRepository roleReadRepository) : IRequestHandler +{ + public async Task Handle(GetRoleByIdForEditQuery query, CancellationToken ct) + => await roleReadRepository.GetByIdAsync(query.Id); +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Queries/GetRolePermissionsByResourceTypeQuery.cs b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Queries/GetRolePermissionsByResourceTypeQuery.cs new file mode 100644 index 0000000..088c289 --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Roles/Queries/GetRolePermissionsByResourceTypeQuery.cs @@ -0,0 +1,14 @@ +using F9WebAiTest.Application.Dtos.SYS.RBAC.Roles; +using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Roles; +using F9WebAiTest.Domain.Enums.SYS.RBAC; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Roles.Queries; + +public record GetRolePermissionsByResourceTypeQuery(long RoleId, Resource_Type_Enum ResourceType) : IRequest>; + +public class GetRolePermissionsByResourceTypeQueryHandler(IRoleReadRepository roleReadRepository) : IRequestHandler> +{ + public async Task> Handle(GetRolePermissionsByResourceTypeQuery query, CancellationToken ct) + => await roleReadRepository.GetRolePermissionsByResourceType(query.RoleId, query.ResourceType); +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/UserRole_AssignCommand.cs b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/UserRole_AssignCommand.cs new file mode 100644 index 0000000..6d988b6 --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/UserRole_AssignCommand.cs @@ -0,0 +1,40 @@ +using F9WebAiTest.Application.Common.Exceptions; +using F9WebAiTest.Application.Interfaces.SnowflakeID; +using F9WebAiTest.Domain.Entities.SYS.RBAC; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Users; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Commands; + +/// 命令定义 +public record UserRole_AssignCommand(long UserId, long RoleId) : IRequest; + +/// 命令处理器 +public class UserRole_AssignCommandHandler( + IUserRoleWriteRepository userRoleWriteRepository, + IUserWriteRepository userWriteRepository, + ISnowflakeIdGenerator snowflakeIdGenerator, + IPublisher eventBus) : IRequestHandler +{ + public async Task Handle(UserRole_AssignCommand command, CancellationToken cancellationToken) + { + if (command.UserId <= 0) throw new F9OperationException("用户ID不能为空"); + if (command.RoleId <= 0) throw new F9OperationException("角色ID不能为空"); + + var newEntity = new UserRole(snowflakeIdGenerator.CreateId(), command.UserId, command.RoleId); + + var user = await userWriteRepository.GetByIdAsync(command.UserId); + if (user == null) throw new F9NotFoundException("用户", command.UserId); + user.AddRole(newEntity.Id); + + var result = await userRoleWriteRepository.AssignAsync(newEntity); + if (result == false) throw new F9OperationException("权限", "绑定"); + + // 发布领域事件 + foreach (var domainEvent in user.DomainEvents) + await eventBus.Publish(domainEvent, cancellationToken); + user.ClearDomainEvents(); + + return newEntity.Id; + } +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/UserRole_RemoveCommand.cs b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/UserRole_RemoveCommand.cs new file mode 100644 index 0000000..b85e0d6 --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/UserRole_RemoveCommand.cs @@ -0,0 +1,35 @@ +using F9WebAiTest.Application.Common.Exceptions; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Users; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Commands; + +/// 命令定义 +public record UserRole_RemoveCommand(long UserId, long RoleId) : IRequest; + +/// 命令处理器 +public class UserRole_RemoveCommandHandler( + IUserWriteRepository userWriteRepository, + IUserRoleWriteRepository userRoleWriteRepository, + IPublisher eventBus) : IRequestHandler +{ + public async Task Handle(UserRole_RemoveCommand command, CancellationToken cancellationToken) + { + if (command.UserId <= 0) throw new F9OperationException("用户ID不能为空"); + if (command.RoleId <= 0) throw new F9OperationException("角色ID不能为空"); + + var user = await userWriteRepository.GetByIdAsync(command.UserId); + if (user == null) throw new F9NotFoundException("用户", command.UserId); + user.RemoveRole(command.RoleId); + + var result = await userRoleWriteRepository.RemoveAsync(command.UserId, command.RoleId); + if (result == false) throw new F9OperationException("权限", "解绑"); + + // 发布领域事件 + foreach (var domainEvent in user.DomainEvents) + await eventBus.Publish(domainEvent, cancellationToken); + user.ClearDomainEvents(); + + return result; + } +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/User_ActiveOrDeactivateCommand.cs b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/User_ActiveOrDeactivateCommand.cs new file mode 100644 index 0000000..c9ba6c8 --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/User_ActiveOrDeactivateCommand.cs @@ -0,0 +1,26 @@ +using F9WebAiTest.Application.Common.Exceptions; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Users; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Commands; + +/// 命令定义 +public record User_ActiveOrDeactivateCommand(long Id, bool Active) : IRequest; + +/// 命令处理器 +public class ActiveOrDeactivateUserCommandHandler( + IUserWriteRepository userWriteRepository) : IRequestHandler +{ + public async Task Handle(User_ActiveOrDeactivateCommand command, CancellationToken cancellationToken) + { + var user = await userWriteRepository.GetByIdAsync(command.Id); + if (user == null) throw new F9NotFoundException("用户", command.Id); + + if (command.Active) user.Activate(); + else user.Deactivate(); + + var result = await userWriteRepository.ActiveOrDeactive(command.Id, user.IsActive); + if (result == false) throw new F9OperationException("启用/禁用", "操作"); + return result; + } +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/User_CreateCommand.cs b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/User_CreateCommand.cs new file mode 100644 index 0000000..47bd7e0 --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/User_CreateCommand.cs @@ -0,0 +1,73 @@ +using F9WebAiTest.Application.Common.Exceptions; +using F9WebAiTest.Application.Interfaces.Security; +using F9WebAiTest.Application.Interfaces.SnowflakeID; +using F9WebAiTest.Application.Interfaces.UOW; +using F9WebAiTest.Domain.Entities.SYS.Departments; +using F9WebAiTest.Domain.Entities.SYS.RBAC; +using F9WebAiTest.Domain.Interfaces.SYS.Departments; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Users; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Commands; + +/// 命令定义 +public record User_CreateCommand(string UserName, string PhoneNumber, string LoginName, long DepartmentId, int SortOrder) : IRequest; + +/// 命令处理器 +public class CreateUserCommandHandler( + IUnitOfWork_F9 unitOfWork, + IUserWriteRepository userRepository, + IDepartmentWriteRepository departmentWriteRepository, + IDepartmentUserRepository departmentUserRepository, + IPasswordHasher passwordHasher, + ISnowflakeIdGenerator snowflakeIdGenerator, + IPublisher eventBus) : IRequestHandler +{ + public async Task Handle(User_CreateCommand command, CancellationToken cancellationToken) + { + // 手写校验 + if (string.IsNullOrWhiteSpace(command.UserName)) throw new F9OperationException("用户名不能为空"); + if (string.IsNullOrWhiteSpace(command.LoginName)) throw new F9OperationException("登录名不能为空"); + if (command.DepartmentId <= 0) throw new F9OperationException("部门不能为空"); + + User createdUser = null; + var ok = await unitOfWork.ExecuteWithUow(async () => + { + // 1. 验证部门是否存在(应用层协调) + var department = await departmentWriteRepository.GetByIdAsync(command.DepartmentId, cancellationToken); + if (department == null) throw new F9NotFoundException("部门不存在"); + + // 2. 创建用户聚合根 + var user = new User( + id: snowflakeIdGenerator.CreateId(), + command.UserName, + command.PhoneNumber, + command.LoginName, + command.DepartmentId, + command.SortOrder); + + // 设置默认密码 + var defaultPasswordMd5 = passwordHasher.GetMd5Hash(user.GetDefaultPasswordText()); + var passwordHash = passwordHasher.HashPassword(defaultPasswordMd5); + user.SetPassword(passwordHash); + + await userRepository.AddAsync(user); + + // 3. 创建部门用户关联 + var depUser = new DepartmentUser(snowflakeIdGenerator.CreateId(), command.DepartmentId, user.Id); + await departmentUserRepository.AssignAsync(depUser); + + createdUser = user; + return true; + }); + + if (!ok) throw new F9OperationException("用户", "新增"); + + // 4. 事务提交后发布领域事件 + foreach (var domainEvent in createdUser.DomainEvents) + await eventBus.Publish(domainEvent, cancellationToken); + createdUser.ClearDomainEvents(); + + return createdUser.Id; + } +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/User_DelCommand.cs b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/User_DelCommand.cs new file mode 100644 index 0000000..f63918e --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/User_DelCommand.cs @@ -0,0 +1,22 @@ +using F9WebAiTest.Application.Common.Exceptions; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Users; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Commands; + +/// 命令定义 +public record User_DelCommand(long Id) : IRequest; + +/// 命令处理器 +public class DelUserCommandHandler( + IUserWriteRepository userWriteRepository) : IRequestHandler +{ + public async Task Handle(User_DelCommand command, CancellationToken cancellationToken) + { + if (command.Id <= 0) throw new F9OperationException("用户ID不能为空"); + + var result = await userWriteRepository.SoftDeleteAsync(command.Id); + if (result == false) throw new F9OperationException("用户", "删除"); + return result; + } +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/User_ResetPasswordCommand.cs b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/User_ResetPasswordCommand.cs new file mode 100644 index 0000000..f320b3e --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/User_ResetPasswordCommand.cs @@ -0,0 +1,32 @@ +using F9WebAiTest.Application.Common.Exceptions; +using F9WebAiTest.Application.Interfaces.Security; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Users; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Commands; + +/// 命令定义 +public record User_ResetPasswordCommand(long Id) : IRequest; + +/// 命令处理器 +public class ResetPasswordUserCommandHandler( + IUserWriteRepository userWriteRepository, + IPasswordHasher passwordHasher) : IRequestHandler +{ + public async Task Handle(User_ResetPasswordCommand command, CancellationToken cancellationToken) + { + if (command.Id <= 0) throw new F9OperationException("用户ID不能为空"); + + var dbUser = await userWriteRepository.GetByIdAsync(command.Id); + if (dbUser == null) throw new F9NotFoundException("用户", command.Id); + + // 设置默认密码 + var defaultPasswordMd5 = passwordHasher.GetMd5Hash(dbUser.GetDefaultPasswordText()); + var passwordHash = passwordHasher.HashPassword(defaultPasswordMd5); + dbUser.SetPassword(passwordHash); + + var result = await userWriteRepository.UpdatePassworkdAsync(command.Id, dbUser.PasswordHash); + if (result == false) throw new F9OperationException("用户", "重置密码"); + return result; + } +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/User_UpdateCommand.cs b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/User_UpdateCommand.cs new file mode 100644 index 0000000..3dc4afc --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Commands/User_UpdateCommand.cs @@ -0,0 +1,49 @@ +using F9WebAiTest.Application.Common.Exceptions; +using F9WebAiTest.Application.Interfaces.SnowflakeID; +using F9WebAiTest.Application.Interfaces.UOW; +using F9WebAiTest.Domain.Entities.SYS.Departments; +using F9WebAiTest.Domain.Interfaces.SYS.Departments; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Users; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Commands; + +/// 命令定义 +public record User_UpdateCommand(long Id, string UserName, string PhoneNumber, string LoginName, long DepartmentId, int SortOrder) : IRequest; + +/// 命令处理器 +public class User_UpdateCommandHandler( + IUnitOfWork_F9 unitOfWork, + ISnowflakeIdGenerator snowflakeIdGenerator, + IUserWriteRepository userWriteRepository, + IDepartmentUserRepository departmentUserRepository) : IRequestHandler +{ + public async Task Handle(User_UpdateCommand command, CancellationToken cancellationToken) + { + if (command.Id <= 0) throw new F9OperationException("用户ID不能为空"); + if (string.IsNullOrWhiteSpace(command.UserName)) throw new F9OperationException("用户名不能为空"); + if (command.DepartmentId <= 0) throw new F9OperationException("部门不能为空"); + + return await unitOfWork.ExecuteWithUow(async () => + { + // 获取数据库用户 + var dbUser = await userWriteRepository.GetByIdAsync(command.Id); + if (dbUser == null) throw new F9NotFoundException("用户", command.Id); + + // 移除原部门关系 + await departmentUserRepository.RemoveAsync(dbUser.DepartmentId, dbUser.Id); + + // 领域更新 + dbUser.Update(command.UserName, command.PhoneNumber, command.LoginName, command.DepartmentId, command.SortOrder); + + // 持久化 + var result = await userWriteRepository.UpdateAsync(dbUser); + + // 添加新部门关系 + await departmentUserRepository.AssignAsync(new DepartmentUser( + snowflakeIdGenerator.CreateId(), dbUser.DepartmentId, dbUser.Id)); + + return result; + }); + } +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Queries/GetUserByIdQuery.cs b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Queries/GetUserByIdQuery.cs new file mode 100644 index 0000000..54c349a --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Queries/GetUserByIdQuery.cs @@ -0,0 +1,13 @@ +using F9WebAiTest.Application.Dtos.SYS.RBAC.Users; +using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Users; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Queries; + +public record GetUserByIdQuery(long id) : IRequest; + +public class GetUserByIdQueryHandler(IUserReadRepository userReadRepository) : IRequestHandler +{ + public async Task Handle(GetUserByIdQuery query, CancellationToken ct) + => await userReadRepository.GetByIdAsync(query.id); +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Queries/GetUserByIdsQuery.cs b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Queries/GetUserByIdsQuery.cs new file mode 100644 index 0000000..e17675b --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Queries/GetUserByIdsQuery.cs @@ -0,0 +1,13 @@ +using F9WebAiTest.Application.Dtos.SYS.RBAC.Users; +using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Users; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Queries; + +public record GetUserByIdsQuery(List Ids) : IRequest>; + +public class GetUserByIdsQueryHandler(IUserReadRepository userReadRepository) : IRequestHandler> +{ + public async Task> Handle(GetUserByIdsQuery query, CancellationToken ct) + => await userReadRepository.GetByIdsAsync(query.Ids); +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Queries/GetUserPagedQuery.cs b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Queries/GetUserPagedQuery.cs new file mode 100644 index 0000000..d426f30 --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Queries/GetUserPagedQuery.cs @@ -0,0 +1,14 @@ +using F9WebAiTest.Application.Dtos.Common; +using F9WebAiTest.Application.Dtos.SYS.RBAC.Users; +using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Users; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Queries; + +public record GetUserPagedQuery(long DepartmentId, string Like, int Page = 1, int PageSize = 10) : IRequest>; + +public class GetUserPagedQueryHandler(IUserReadRepository userReadRepository) : IRequestHandler> +{ + public async Task> Handle(GetUserPagedQuery query, CancellationToken ct) + => await userReadRepository.GetPagedAsync(query.DepartmentId, query.Like, query.Page, query.PageSize); +} diff --git a/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Queries/GetUserRolesByUserIdQuery.cs b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Queries/GetUserRolesByUserIdQuery.cs new file mode 100644 index 0000000..1262b7b --- /dev/null +++ b/backend/F9WebAiTest.Application/CQRS/SYS/RBAC/Users/Queries/GetUserRolesByUserIdQuery.cs @@ -0,0 +1,13 @@ +using F9WebAiTest.Application.Dtos.SYS.RBAC.Users; +using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Users; +using MediatR; + +namespace F9WebAiTest.Application.CQRS.SYS.RBAC.Users.Queries; + +public record GetUserRolesByUserIdQuery(long UserId) : IRequest>; + +public class GetUserRolesByUserIdQueryHandler(IUserReadRepository userReadRepository) : IRequestHandler> +{ + public async Task> Handle(GetUserRolesByUserIdQuery query, CancellationToken ct) + => await userReadRepository.GetUserRolesByUserId(query.UserId); +} diff --git a/backend/F9WebAiTest.Application/Common/ApiResult.cs b/backend/F9WebAiTest.Application/Common/ApiResult.cs new file mode 100644 index 0000000..74b02a2 --- /dev/null +++ b/backend/F9WebAiTest.Application/Common/ApiResult.cs @@ -0,0 +1,24 @@ +namespace F9WebAiTest.Application.Common; + +/// 统一 API 响应 +public class ApiResult +{ + public int Code { get; set; } + public string Message { get; set; } + public T Data { get; set; } + + public static ApiResult Ok(T data, string message = "success") + => new() { Code = 200, Message = message, Data = data }; + + public static ApiResult Fail(string message, int code = 400) + => new() { Code = code, Message = message, Data = default }; +} + +/// 分页结果 +public class PageResult +{ + public List Items { get; set; } = new(); + public long Total { get; set; } + public int PageIndex { get; set; } + public int PageSize { get; set; } +} diff --git a/backend/F9WebAiTest.Application/Common/Exceptions/F9NotFoundException.cs b/backend/F9WebAiTest.Application/Common/Exceptions/F9NotFoundException.cs new file mode 100644 index 0000000..8ccb79a --- /dev/null +++ b/backend/F9WebAiTest.Application/Common/Exceptions/F9NotFoundException.cs @@ -0,0 +1,12 @@ +namespace F9WebAiTest.Application.Common.Exceptions; + +/// 资源不存在异常 +public class F9NotFoundException : Exception +{ + public F9NotFoundException(string resourceName, object key) + : base($"{resourceName}不存在'{key}'。") + { + } + + public F9NotFoundException(string message) : base(message) { } +} diff --git a/backend/F9WebAiTest.Application/Common/Exceptions/F9OperationException.cs b/backend/F9WebAiTest.Application/Common/Exceptions/F9OperationException.cs new file mode 100644 index 0000000..f58b11d --- /dev/null +++ b/backend/F9WebAiTest.Application/Common/Exceptions/F9OperationException.cs @@ -0,0 +1,15 @@ +namespace F9WebAiTest.Application.Common.Exceptions; + +/// 操作失败异常 +public class F9OperationException : Exception +{ + /// 操作失败 + /// 资源名称 + /// 操作名称 + public F9OperationException(string resourceName, string oporation) + : base($"{resourceName}{oporation}失败。") + { + } + + public F9OperationException(string message) : base(message) { } +} diff --git a/backend/F9WebAiTest.Application/Common/ServiceException.cs b/backend/F9WebAiTest.Application/Common/ServiceException.cs new file mode 100644 index 0000000..1cf0b7a --- /dev/null +++ b/backend/F9WebAiTest.Application/Common/ServiceException.cs @@ -0,0 +1,12 @@ +namespace F9WebAiTest.Application.Common; + +/// 业务异常 +public class ServiceException : Exception +{ + public int Code { get; set; } = 400; + + public ServiceException(string message, int code = 400) : base(message) + { + Code = code; + } +} diff --git a/backend/F9WebAiTest.Application/Contracts/LoginUserInfo.cs b/backend/F9WebAiTest.Application/Contracts/LoginUserInfo.cs new file mode 100644 index 0000000..fbbc980 --- /dev/null +++ b/backend/F9WebAiTest.Application/Contracts/LoginUserInfo.cs @@ -0,0 +1,9 @@ +namespace F9WebAiTest.Application.Contracts; + +/// 登录用户信息 +/// 识别码 +/// 用户名称 +/// 登录名 +/// 所属部门Id +/// 是否开发者 +public record LoginUserInfo(long Id, string Name, string Login_Account, long Department_Id, bool IsDeveloper); diff --git a/backend/F9WebAiTest.Application/DependencyInjection.cs b/backend/F9WebAiTest.Application/DependencyInjection.cs new file mode 100644 index 0000000..3c822dd --- /dev/null +++ b/backend/F9WebAiTest.Application/DependencyInjection.cs @@ -0,0 +1,33 @@ +using F9WebAiTest.Application.Services; +using F9WebAiTest.Application.Services.Attendance; +using F9WebAiTest.Application.Services.Auth; +using F9WebAiTest.Application.Services.Base; +using F9WebAiTest.Application.Services.Notice; +using F9WebAiTest.Application.Services.Prod; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace F9WebAiTest.Application; + +public static class DependencyInjection +{ + public static IServiceCollection AddApplication(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + // MediatR:注册命令/查询 Handler + services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(NotificationService).Assembly)); + + return services; + } +} diff --git a/backend/F9WebAiTest.Application/Dtos/Attendance/AttendanceDtos.cs b/backend/F9WebAiTest.Application/Dtos/Attendance/AttendanceDtos.cs new file mode 100644 index 0000000..3058471 --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/Attendance/AttendanceDtos.cs @@ -0,0 +1,87 @@ +namespace F9WebAiTest.Application.Dtos.Attendance; + +public class AttendanceDto +{ + public long Id { get; set; } + public long EmployeeId { get; set; } + public string EmployeeName { get; set; } + public string DepartmentName { get; set; } + public DateTime WorkDate { get; set; } + public DateTime? SignInTime { get; set; } + public DateTime? SignOutTime { get; set; } + public string SignInStatus { get; set; } + public string SignOutStatus { get; set; } + public string Remark { get; set; } +} + +public class PunchResultDto +{ + public string Type { get; set; } // signIn / signOut + public DateTime Time { get; set; } + public string Status { get; set; } // 正常/迟到/早退 + public string Message { get; set; } +} + +public class AttendanceQueryDto +{ + public string Date { get; set; } = ""; // yyyy-MM-dd + public long? DepartmentId { get; set; } + public string Keyword { get; set; } = ""; + public int PageIndex { get; set; } = 1; + public int PageSize { get; set; } = 10; +} + +public class AttendanceSummaryDto +{ + public int NormalDays { get; set; } + public int LateDays { get; set; } + public int EarlyLeaveDays { get; set; } + public int AbsentDays { get; set; } + public int LeaveDays { get; set; } + public int TotalDays { get; set; } +} + +public class LeaveRequestDto +{ + public long Id { get; set; } + public long EmployeeId { get; set; } + public string EmployeeName { get; set; } + public string DepartmentName { get; set; } + public string Type { get; set; } + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + public string Reason { get; set; } + public string Status { get; set; } + public int CurrentOrderNo { get; set; } // 当前审批到第几步 + public string ApproverName { get; set; } + public string ApproveRemark { get; set; } + public DateTime? ApprovedAt { get; set; } + public DateTime CreatedAt { get; set; } + public List Approvers { get; set; } = new(); // 审批流程节点 +} + +public class LeaveApproverDto +{ + public long Id { get; set; } + public long ApproverId { get; set; } + public string ApproverName { get; set; } + public int OrderNo { get; set; } // 序号(审批顺序) + public string Status { get; set; } // 待审批/已批准/已驳回 + public string ApproveRemark { get; set; } + public DateTime? ApprovedAt { get; set; } +} + +public class CreateLeaveDto +{ + public string Type { get; set; } = "事假"; + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + public string Reason { get; set; } = ""; + public List ApproverIds { get; set; } = new(); // 审批人ID列表(顺序=审批顺序) +} + +public class ApproveLeaveDto +{ + public bool Approved { get; set; } + public string Remark { get; set; } = ""; +} diff --git a/backend/F9WebAiTest.Application/Dtos/Auth/AuthDtos.cs b/backend/F9WebAiTest.Application/Dtos/Auth/AuthDtos.cs new file mode 100644 index 0000000..911ee94 --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/Auth/AuthDtos.cs @@ -0,0 +1,50 @@ +namespace F9WebAiTest.Application.Dtos.Auth; + +public class LoginDto +{ + public string Username { get; set; } + public string Password { get; set; } +} + +public class LoginResultDto +{ + public string AccessToken { get; set; } + public string RefreshToken { get; set; } + public DateTime AccessTokenExpiresAt { get; set; } + public EmployeeInfoDto User { get; set; } +} + +public class RefreshTokenDto +{ + public string RefreshToken { get; set; } +} + +public class EmployeeInfoDto +{ + public long Id { get; set; } + public string Username { get; set; } + public string RealName { get; set; } + public string Nickname { get; set; } + public string Avatar { get; set; } + public string Phone { get; set; } + public string Email { get; set; } + public long DepartmentId { get; set; } + public string DepartmentName { get; set; } + public int Status { get; set; } + public List Roles { get; set; } = new(); + public List Permissions { get; set; } = new(); +} + +public class ChangePasswordDto +{ + public string OldPassword { get; set; } + public string NewPassword { get; set; } +} + +public class UpdateProfileDto +{ + public string Nickname { get; set; } + public string Avatar { get; set; } + public string Phone { get; set; } + public string Email { get; set; } +} diff --git a/backend/F9WebAiTest.Application/Dtos/Base/BusinessDtos.cs b/backend/F9WebAiTest.Application/Dtos/Base/BusinessDtos.cs new file mode 100644 index 0000000..d4b0df3 --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/Base/BusinessDtos.cs @@ -0,0 +1,86 @@ +namespace F9WebAiTest.Application.Dtos.Base; + +// ---- 工艺庄口 ---- +public class CraftZhuangKouDto +{ + public long Id { get; set; } + public int BelongingYear { get; set; } + public int BendiWaidi { get; set; } + public string BendiWaidiName { get; set; } + public string JidiWaigou { get; set; } + public string ZhuangKouCode { get; set; } + public string ZhuangKouName { get; set; } + public string ProductionDisplayName { get; set; } + public string Chandi { get; set; } + public int Sort { get; set; } + public string Memo { get; set; } + public string IsJiezhuang { get; set; } +} + +public class CraftZhuangKouCreateDto +{ + public int BelongingYear { get; set; } + public int BendiWaidi { get; set; } + public string JidiWaigou { get; set; } = ""; + public string ZhuangKouCode { get; set; } = ""; + public string ZhuangKouName { get; set; } = ""; + public string ProductionDisplayName { get; set; } = ""; + public string Chandi { get; set; } = ""; + public int Sort { get; set; } + public string Memo { get; set; } = ""; + public string IsJiezhuang { get; set; } = "0"; +} + +// ---- 生丝规格 ---- +public class ShengsiGuigeDto +{ + public long Id { get; set; } + public string Guige { get; set; } + public double CenterrangeLolimit { get; set; } + public double CenterrangeUplimit { get; set; } + public double CenterXiandu { get; set; } + public double ValidLolimit { get; set; } + public double ValidUplimit { get; set; } + public double XiaoyeLolimit { get; set; } + public double XiaoyeUplimit { get; set; } + public double DayeLolimit { get; set; } + public double DayeUplimit { get; set; } + public double TeyeLolimit { get; set; } + public double TeyeUplimit { get; set; } + public double YexianNkLolimitLeft { get; set; } + public double YexianNkLolimitRight { get; set; } + public double YexianNkUplimitLeft { get; set; } + public double YexianNkUplimitRight { get; set; } + public double LaodongJingsaiYexianLolimit { get; set; } + public double LaodongJingsaiYexianUplimit { get; set; } + public int Sort { get; set; } + public string Memo { get; set; } +} + +// ---- 生丝定级指标 ---- +public class ShengsiDingjizhibiaoDto +{ + public long Id { get; set; } + public int Standard { get; set; } + public string StandardName { get; set; } + public string Guige { get; set; } + public string Grade { get; set; } + public double GradeScore { get; set; } + public double Piancha { get; set; } + public double Dacha { get; set; } + public double Erdu { get; set; } + public double Sandu { get; set; } + public double Qingjie { get; set; } + public double Jiejing { get; set; } +} + +// ---- 机组车台 ---- +public class JizuChetaiDto +{ + public long Id { get; set; } + public long DepartmentId { get; set; } + public string DepartmentName { get; set; } + public int Jizu { get; set; } + public int ChetaiStart { get; set; } + public int ChetaiEnd { get; set; } +} diff --git a/backend/F9WebAiTest.Application/Dtos/Common/PageRequest.cs b/backend/F9WebAiTest.Application/Dtos/Common/PageRequest.cs new file mode 100644 index 0000000..23e743f --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/Common/PageRequest.cs @@ -0,0 +1,3 @@ +namespace F9WebAiTest.Application.Dtos.Common; + +public record PageRequest(int Page = 1, int PageSize = 10); diff --git a/backend/F9WebAiTest.Application/Dtos/Common/PagedResult.cs b/backend/F9WebAiTest.Application/Dtos/Common/PagedResult.cs new file mode 100644 index 0000000..d2f120f --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/Common/PagedResult.cs @@ -0,0 +1,10 @@ +namespace F9WebAiTest.Application.Dtos.Common; + +public record PagedResult( + List Items, + int Page, + int PageSize, + long TotalCount) +{ + public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize); +} diff --git a/backend/F9WebAiTest.Application/Dtos/DepartmentDtos.cs b/backend/F9WebAiTest.Application/Dtos/DepartmentDtos.cs new file mode 100644 index 0000000..99c804b --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/DepartmentDtos.cs @@ -0,0 +1,33 @@ +namespace F9WebAiTest.Application.Dtos; + +public class DepartmentDto +{ + public long Id { get; set; } + public string Name { get; set; } + public long? ParentId { get; set; } + public long? LeaderId { get; set; } + public string LeaderName { get; set; } + public string Phone { get; set; } + public int Sort { get; set; } + public int Status { get; set; } + public List Children { get; set; } = new(); +} + +public class CreateDepartmentDto +{ + public string Name { get; set; } + public long? ParentId { get; set; } + public long? LeaderId { get; set; } + public string Phone { get; set; } = ""; + public int Sort { get; set; } +} + +public class UpdateDepartmentDto +{ + public string Name { get; set; } + public long? ParentId { get; set; } + public long? LeaderId { get; set; } + public string Phone { get; set; } = ""; + public int Sort { get; set; } + public int Status { get; set; } +} diff --git a/backend/F9WebAiTest.Application/Dtos/EmployeeDtos.cs b/backend/F9WebAiTest.Application/Dtos/EmployeeDtos.cs new file mode 100644 index 0000000..5982f92 --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/EmployeeDtos.cs @@ -0,0 +1,61 @@ +namespace F9WebAiTest.Application.Dtos; + +public class EmployeeDto +{ + public long Id { get; set; } + public string Username { get; set; } + public string RealName { get; set; } + public string Nickname { get; set; } + public string Avatar { get; set; } + public string Gender { get; set; } + public string Phone { get; set; } + public string Email { get; set; } + public long DepartmentId { get; set; } + public string DepartmentName { get; set; } + public int Status { get; set; } + public string Remark { get; set; } + public List RoleIds { get; set; } = new(); + public List RoleNames { get; set; } = new(); + public DateTime CreatedAt { get; set; } +} + +public class CreateEmployeeDto +{ + public string Username { get; set; } + public string Password { get; set; } + public string RealName { get; set; } + public string Nickname { get; set; } = ""; + public string Gender { get; set; } = "男"; + public string Phone { get; set; } = ""; + public string Email { get; set; } = ""; + public long DepartmentId { get; set; } + public string Remark { get; set; } = ""; + public List RoleIds { get; set; } = new(); +} + +public class UpdateEmployeeDto +{ + public string RealName { get; set; } + public string Nickname { get; set; } = ""; + public string Gender { get; set; } = "男"; + public string Phone { get; set; } = ""; + public string Email { get; set; } = ""; + public long DepartmentId { get; set; } + public int Status { get; set; } = 1; + public string Remark { get; set; } = ""; + public List RoleIds { get; set; } = new(); +} + +public class ResetPasswordDto +{ + public string NewPassword { get; set; } +} + +public class EmployeeQueryDto +{ + public string Keyword { get; set; } = ""; + public long? DepartmentId { get; set; } + public int? Status { get; set; } + public int PageIndex { get; set; } = 1; + public int PageSize { get; set; } = 10; +} diff --git a/backend/F9WebAiTest.Application/Dtos/MenuDtos.cs b/backend/F9WebAiTest.Application/Dtos/MenuDtos.cs new file mode 100644 index 0000000..3674d45 --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/MenuDtos.cs @@ -0,0 +1,41 @@ +namespace F9WebAiTest.Application.Dtos; + +public class MenuDto +{ + public long Id { get; set; } + public long? ParentId { get; set; } + public string Name { get; set; } + public string Type { get; set; } + public string Path { get; set; } + public string Component { get; set; } + public string Icon { get; set; } + public string Permission { get; set; } + public int Sort { get; set; } + public int Status { get; set; } + public List Children { get; set; } = new(); +} + +public class CreateMenuDto +{ + public long? ParentId { get; set; } + public string Name { get; set; } + public string Type { get; set; } = "menu"; + public string Path { get; set; } = ""; + public string Component { get; set; } = ""; + public string Icon { get; set; } = ""; + public string Permission { get; set; } = ""; + public int Sort { get; set; } +} + +public class UpdateMenuDto +{ + public long? ParentId { get; set; } + public string Name { get; set; } + public string Type { get; set; } = "menu"; + public string Path { get; set; } = ""; + public string Component { get; set; } = ""; + public string Icon { get; set; } = ""; + public string Permission { get; set; } = ""; + public int Sort { get; set; } + public int Status { get; set; } = 1; +} diff --git a/backend/F9WebAiTest.Application/Dtos/Notice/NotificationDtos.cs b/backend/F9WebAiTest.Application/Dtos/Notice/NotificationDtos.cs new file mode 100644 index 0000000..b095e2d --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/Notice/NotificationDtos.cs @@ -0,0 +1,96 @@ +using F9WebAiTest.Domain.Entities.Notice; + +namespace F9WebAiTest.Application.Dtos.Notice; + +// ---- 消息类型 ---- +public class NotificationTypeDto +{ + public long Id { get; set; } + public string Code { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string RequiredPermission { get; set; } + public bool IsActive { get; set; } + public long CreatedBy { get; set; } + public int Sort { get; set; } + public int Category { get; set; } // 消息分类:0通知 1待办 2公告 + public string Icon { get; set; } = ""; // 图标 + public string RouteTemplate { get; set; } = ""; // Web 端跳转路由模板 + public bool Subscribed { get; set; } // 当前用户是否已订阅 + public bool CanSubscribe { get; set; } = true; // 当前用户是否有权限订阅 +} + +public class NotificationTypeCreateDto +{ + public string Code { get; set; } = ""; + public string Name { get; set; } = ""; + public string Description { get; set; } = ""; + public string RequiredPermission { get; set; } = ""; + public bool IsActive { get; set; } = true; + public int Sort { get; set; } + public int Category { get; set; } // 消息分类:0通知 1待办 2公告 + public string Icon { get; set; } = ""; + public string RouteTemplate { get; set; } = ""; // Web 端跳转路由模板,支持 {businessId} 占位符 +} + +// ---- 订阅 ---- +public class SubscribeDto +{ + public long NotificationTypeId { get; set; } + public bool Subscribe { get; set; } = true; // true=订阅 false=取消 +} + +// ---- 消息 ---- +public class NotificationMessageDto +{ + public long Id { get; set; } + public long NotificationTypeId { get; set; } + public string MessageTypeCode { get; set; } + public string TypeName { get; set; } + public string Title { get; set; } + public string Content { get; set; } + public int Priority { get; set; } + public string BusinessType { get; set; } + public string BusinessId { get; set; } + public int Category { get; set; } // 消息分类:0通知 1待办 2公告 + public string RoutePath { get; set; } = ""; // Web 端跳转路由(点击消息时跳转) + public DateTime CreatedTime { get; set; } + public int ReadStatus { get; set; } // 当前用户阅读状态 0=未读 1=已读 + public DateTime? ReadTime { get; set; } +} + +public class UnreadCountDto +{ + public int Count { get; set; } +} + +// ---- 待办 ---- +public class TodoItemDto +{ + public long Id { get; set; } + public long MessageId { get; set; } + public string Title { get; set; } = ""; + public string Content { get; set; } = ""; + public int Priority { get; set; } + public string BusinessType { get; set; } = ""; + public string BusinessId { get; set; } = ""; + public string RoutePath { get; set; } = ""; + public int TodoStatus { get; set; } // 0待处理 1处理中 2已完成 3已取消 + public DateTime? DueTime { get; set; } + public DateTime? CompletedTime { get; set; } + public DateTime CreatedTime { get; set; } +} + +public class CompleteTodoDto +{ + public string HandleResult { get; set; } = ""; // 处理结果说明 +} + +// ---- 广播通知 ---- +public class BroadcastNotificationDto +{ + public string Title { get; set; } = ""; + public string Content { get; set; } = ""; + public NotificationPriority? Priority { get; set; } + public string RoutePath { get; set; } = ""; +} diff --git a/backend/F9WebAiTest.Application/Dtos/Prod/HbjyDtos.cs b/backend/F9WebAiTest.Application/Dtos/Prod/HbjyDtos.cs new file mode 100644 index 0000000..fef876c --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/Prod/HbjyDtos.cs @@ -0,0 +1,86 @@ +namespace F9WebAiTest.Application.Dtos.Prod; + +// ---- 黑板检验 ---- + +/// 黑板检验列表行(落丝记录 + 检验汇总) +public class HbjyListItemDto +{ + public long LsjlId { get; set; } + public DateTime WorkDate { get; set; } + public long DeptId { get; set; } + public string DeptName { get; set; } + public int Jizu { get; set; } + public int Huishu { get; set; } + public string Guige { get; set; } + public string ZhuangKouName { get; set; } + public string Yanse { get; set; } + public bool Isbanche { get; set; } + public string IsbancheName { get; set; } + public string LuosiTime { get; set; } + public int Chetaishu { get; set; } + public int ChetaiStart { get; set; } + public int ChetaiEnd { get; set; } + public int YingluPianshu { get; set; } // 应录片数 = 车台数 × 20 + public bool IsChecked { get; set; } // 是否已检验 + public int Pianshu { get; set; } // 已录片数 + public double Qingjie { get; set; } // 清洁分 + public double Jiejing { get; set; } // 洁净分 + public string Dengji { get; set; } // 等级 +} + +/// 黑板检验编辑页数据 +public class HbjyEditDto +{ + public long LsjlId { get; set; } + public string DeptName { get; set; } + public int Jizu { get; set; } + public int Huishu { get; set; } + public string ZhuangKouName { get; set; } + public string Yanse { get; set; } + public bool Isbanche { get; set; } + public string IsbancheName { get; set; } + public string Guige { get; set; } + public int ChetaiStart { get; set; } + public int ChetaiEnd { get; set; } + public int Chetaishu { get; set; } + public int YingluPianshu { get; set; } // 应录片数 + public string Dengji { get; set; } // 等级 + public List Records { get; set; } = new(); +} + +/// 黑板检验车台明细 DTO +public class HbjyRecordDto +{ + public int Chetaihao { get; set; } + public int YdYidu { get; set; } + public int YdErdu { get; set; } + public int YdSandu { get; set; } + public int Jj100 { get; set; } + public int Jj95 { get; set; } + public int Jj90 { get; set; } + public int Jj85 { get; set; } + public int Jj80 { get; set; } + public int Jj75 { get; set; } + public int Jj70 { get; set; } + public int Jj65 { get; set; } + public int Jj60 { get; set; } + public int QjZycdTdc { get; set; } + public int QjCycdFs { get; set; } + public int QjCycdDc { get; set; } + public int QjCycdNfc { get; set; } + public int QjCycdDcj { get; set; } + public int QjCycdZlx { get; set; } + public int QjPtcdXc { get; set; } + public int QjPtcdCj { get; set; } + public int QjPtcdLx { get; set; } + public int QjPtcdHjls { get; set; } + public int Pianshu { get; set; } // 本车台片数(计算得出) +} + +/// 黑板检验保存请求 +public class HbjySaveDto +{ + public long LsjlId { get; set; } + public string Dengji { get; set; } = ""; + public List Records { get; set; } = new(); +} diff --git a/backend/F9WebAiTest.Application/Dtos/Prod/LsjlDtos.cs b/backend/F9WebAiTest.Application/Dtos/Prod/LsjlDtos.cs new file mode 100644 index 0000000..76dbc2a --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/Prod/LsjlDtos.cs @@ -0,0 +1,41 @@ +namespace F9WebAiTest.Application.Dtos.Prod; + +public class LsjlDto +{ + public long Id { get; set; } + public DateTime WorkDate { get; set; } + public long DeptId { get; set; } + public string DeptName { get; set; } + public long GyzkId { get; set; } + public string ZhuangKouName { get; set; } + public int Jizu { get; set; } + public string LuosiTime { get; set; } + public string Yanse { get; set; } + public int Huishu { get; set; } + public int ChetaiStart { get; set; } + public int ChetaiEnd { get; set; } + public int Chetaishu { get; set; } + public bool Isbanche { get; set; } + public string IsbancheName { get; set; } + public double Hours { get; set; } + public string Guige { get; set; } + public double Chesu { get; set; } +} + +public class LsjlCreateDto +{ + public DateTime WorkDate { get; set; } + public long DeptId { get; set; } + public long GyzkId { get; set; } + public int Jizu { get; set; } + public string LuosiTime { get; set; } = ""; + public string Yanse { get; set; } = ""; + public int Huishu { get; set; } + public int ChetaiStart { get; set; } + public int ChetaiEnd { get; set; } + public int Chetaishu { get; set; } + public bool Isbanche { get; set; } + public double Hours { get; set; } + public string Guige { get; set; } = ""; + public double Chesu { get; set; } +} diff --git a/backend/F9WebAiTest.Application/Dtos/Prod/XdjyDtos.cs b/backend/F9WebAiTest.Application/Dtos/Prod/XdjyDtos.cs new file mode 100644 index 0000000..f8f0af9 --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/Prod/XdjyDtos.cs @@ -0,0 +1,93 @@ +using F9WebAiTest.Application.Dtos.Base; + +namespace F9WebAiTest.Application.Dtos.Prod; + +// ---- 纤度检验 ---- + +/// 纤度检验列表行(落丝记录 + 检验汇总) +public class XdjyListItemDto +{ + public long LsjlId { get; set; } // 落丝记录 Id + public DateTime WorkDate { get; set; } // 日期 + public long DeptId { get; set; } + public string DeptName { get; set; } + public int Jizu { get; set; } // 机组 + public int Huishu { get; set; } // 回数 + public string Guige { get; set; } // 规格 + public string ZhuangKouName { get; set; } // 庄口 + public string Yanse { get; set; } // 颜色 + public bool Isbanche { get; set; } // 半车 + public string IsbancheName { get; set; } + public int Chetaishu { get; set; } // 车台数 + public int ChetaiStart { get; set; } + public int ChetaiEnd { get; set; } + public string LuosiTime { get; set; } // 落丝时间 + public int Yingchengnum { get; set; } // 应秤数 = 车台数 × (半车?2:4) + public bool IsChecked { get; set; } // 是否已检验 + public int Jiaoshu { get; set; } // 已秤数 + public double Avgxiandu { get; set; } // 平均纤度 + public double Junfangcha { get; set; } // 均方差 + public double Zongcha { get; set; } // 总差 + public double FanxianLv { get; set; } // 范纤率 + public int FanweiNum { get; set; } // 范围数 + public int XiaoyeNum { get; set; } // 小野数 + public int DayeNum { get; set; } // 大野数 + public int TeyeNum { get; set; } // 特野数 + public int CenterNum { get; set; } // 中心数 + public string Dengji { get; set; } // 等级 +} + +/// 纤度检验编辑页数据 +public class XdjyEditDto +{ + public long LsjlId { get; set; } + public string DeptName { get; set; } + public int Jizu { get; set; } + public int Huishu { get; set; } + public string ZhuangKouName { get; set; } + public string Yanse { get; set; } + public bool Isbanche { get; set; } + public string IsbancheName { get; set; } + public string Guige { get; set; } + public int ChetaiStart { get; set; } + public int ChetaiEnd { get; set; } + public int Chetaishu { get; set; } + public int Yingchengnum { get; set; } // 应秤数 + public string Dengji { get; set; } // 等级 + public ShengsiGuigeDto GuigeConfig { get; set; } // 生丝规格(用于判定/颜色标识) + public List Xdjy { get; set; } = new(); +} + +/// 纤度检验车台明细 DTO +public class XdjyRecordDto +{ + public int Chetaihao { get; set; } // 车台号 + public double Xd1 { get; set; } + public double Xd2 { get; set; } + public double Xd3 { get; set; } + public double Xd4 { get; set; } + public int XdNum { get; set; } // 已称绞数 + public double XdAvg { get; set; } // 平均纤度 + public int FanweiNum { get; set; } // 范围 + public int XiaoyeNum { get; set; } // 小野 + public int DayeNum { get; set; } // 大野 + public int TeyeNum { get; set; } // 特野 + public int CenterNum { get; set; } // 中心 +} + +/// 纤度检验保存请求 +public class XdjySaveDto +{ + public long LsjlId { get; set; } + public string Dengji { get; set; } = ""; + public List Items { get; set; } = new(); +} + +public class XdjySaveItemDto +{ + public int Chetaihao { get; set; } + public double Xd1 { get; set; } + public double Xd2 { get; set; } + public double Xd3 { get; set; } + public double Xd4 { get; set; } +} diff --git a/backend/F9WebAiTest.Application/Dtos/RoleDtos.cs b/backend/F9WebAiTest.Application/Dtos/RoleDtos.cs new file mode 100644 index 0000000..0a75dc7 --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/RoleDtos.cs @@ -0,0 +1,31 @@ +namespace F9WebAiTest.Application.Dtos; + +public class RoleDto +{ + public long Id { get; set; } + public string Name { get; set; } + public string Code { get; set; } + public string Description { get; set; } + public int Sort { get; set; } + public int Status { get; set; } + public int EmployeeCount { get; set; } + public DateTime CreatedAt { get; set; } +} + +public class CreateRoleDto +{ + public string Name { get; set; } + public string Code { get; set; } + public string Description { get; set; } = ""; + public int Sort { get; set; } + public List MenuIds { get; set; } = new(); +} + +public class UpdateRoleDto +{ + public string Name { get; set; } + public string Description { get; set; } = ""; + public int Sort { get; set; } + public int Status { get; set; } = 1; + public List MenuIds { get; set; } = new(); +} diff --git a/backend/F9WebAiTest.Application/Dtos/SYS/Departments/DepartmentReadDto_View.cs b/backend/F9WebAiTest.Application/Dtos/SYS/Departments/DepartmentReadDto_View.cs new file mode 100644 index 0000000..3a2bdb1 --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/SYS/Departments/DepartmentReadDto_View.cs @@ -0,0 +1,5 @@ +using F9WebAiTest.Domain.Enums.SYS.Departments; + +namespace F9WebAiTest.Application.Dtos.SYS.Departments; + +public record DepartmentReadDto_View(long Id, string Code, string Name, long ParentId, Department_ManageType_Enum ManageType, Department_DutyType_Enum DutyType, string Description, bool IsActive, int SortOrder); diff --git a/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Menus/MenuReadDto_Menu_Edit.cs b/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Menus/MenuReadDto_Menu_Edit.cs new file mode 100644 index 0000000..d985303 --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Menus/MenuReadDto_Menu_Edit.cs @@ -0,0 +1,16 @@ +using F9WebAiTest.Domain.Enums.SYS.RBAC; + +namespace F9WebAiTest.Application.Dtos.SYS.RBAC.Menus; + +public record MenuReadDto_Menu_Edit( + long Id, + string Code, + string Name, + string IconSvg, + MenuType_Enum MenuType, + string AssemblyName, + string TypeFullName, + bool IsActive, + long ParentId, + int SortOrder +); diff --git a/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Menus/MenuReadDto_Menu_Table.cs b/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Menus/MenuReadDto_Menu_Table.cs new file mode 100644 index 0000000..0fb3700 --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Menus/MenuReadDto_Menu_Table.cs @@ -0,0 +1,23 @@ +namespace F9WebAiTest.Application.Dtos.SYS.RBAC.Menus; + +// 读模型 +public record MenuReadDto_Menu_Table( + long Id, + long ParentId, + string Code, + string Name, + string IconSvg, + string MenuType, + string AssemblyName, + string TypeFullName, + string ParentName, + int Level, + string IsActive, + int SortOrder, + + // 用于前端 table select + bool Selected = false, + + // 用于前端 table tree + MenuReadDto_Menu_Table[]? Items = default +); diff --git a/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Roles/RolePermissionReadDto_View.cs b/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Roles/RolePermissionReadDto_View.cs new file mode 100644 index 0000000..204b184 --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Roles/RolePermissionReadDto_View.cs @@ -0,0 +1,5 @@ +using F9WebAiTest.Domain.Enums.SYS.RBAC; + +namespace F9WebAiTest.Application.Dtos.SYS.RBAC.Roles; + +public record RolePermissionReadDto_View(long Id, long RoleId, long PermissionId, long ResourceId, Permission_Type_Enum PermissionType); diff --git a/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Roles/RoleReadDto_Cache.cs b/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Roles/RoleReadDto_Cache.cs new file mode 100644 index 0000000..d024f57 --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Roles/RoleReadDto_Cache.cs @@ -0,0 +1,3 @@ +namespace F9WebAiTest.Application.Dtos.SYS.RBAC.Roles; + +public record RoleReadDto_Cache(long Id, string Name, string Code, int SortOrder); diff --git a/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Roles/RoleReadDto_Edit.cs b/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Roles/RoleReadDto_Edit.cs new file mode 100644 index 0000000..1a22655 --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Roles/RoleReadDto_Edit.cs @@ -0,0 +1,3 @@ +namespace F9WebAiTest.Application.Dtos.SYS.RBAC.Roles; + +public record RoleReadDto_Edit(long Id, string Name, string Code, int SortOrder, string Description); diff --git a/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Roles/RoleReadDto_View.cs b/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Roles/RoleReadDto_View.cs new file mode 100644 index 0000000..1d906e0 --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Roles/RoleReadDto_View.cs @@ -0,0 +1,3 @@ +namespace F9WebAiTest.Application.Dtos.SYS.RBAC.Roles; + +public record RoleReadDto_View(long Id, string Name, string Code, int SortOrder, int MenuPermissionCount, int ReportPermissionCount, int DepPermissionCount, bool IsActive); diff --git a/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Users/UserReadDto_Edit.cs b/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Users/UserReadDto_Edit.cs new file mode 100644 index 0000000..b59131b --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Users/UserReadDto_Edit.cs @@ -0,0 +1,4 @@ +namespace F9WebAiTest.Application.Dtos.SYS.RBAC.Users; + +// 用户读模型 +public record UserReadDto_Edit(long Id, string UserName, string PhoneNumber, string LoginName, long DepartmentId, int SortOrder); diff --git a/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Users/UserReadDto_Login.cs b/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Users/UserReadDto_Login.cs new file mode 100644 index 0000000..feb351d --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Users/UserReadDto_Login.cs @@ -0,0 +1,3 @@ +namespace F9WebAiTest.Application.Dtos.SYS.RBAC.Users; + +public record UserReadDto_Login(long Id, string UserName, string LoginName, string PasswordHash, long DepartmentId); diff --git a/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Users/UserReadDto_View.cs b/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Users/UserReadDto_View.cs new file mode 100644 index 0000000..b5673f0 --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Users/UserReadDto_View.cs @@ -0,0 +1,4 @@ +namespace F9WebAiTest.Application.Dtos.SYS.RBAC.Users; + +// 用户读模型(列表视图) +public record UserReadDto_View(long Id, string UserName, string PhoneNumber, string LoginName, string DepartmentName, string RoleName, string LastLoginAt, int SortOrder, bool IsActive); diff --git a/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Users/UserRoleReadDto_Cache.cs b/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Users/UserRoleReadDto_Cache.cs new file mode 100644 index 0000000..80e62ad --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/SYS/RBAC/Users/UserRoleReadDto_Cache.cs @@ -0,0 +1,3 @@ +namespace F9WebAiTest.Application.Dtos.SYS.RBAC.Users; + +public record UserRoleReadDto_Cache(long Id, long UserId, long RoleId, string RoleName); diff --git a/backend/F9WebAiTest.Application/F9WebAiTest.Application.csproj b/backend/F9WebAiTest.Application/F9WebAiTest.Application.csproj new file mode 100644 index 0000000..9b0f42c --- /dev/null +++ b/backend/F9WebAiTest.Application/F9WebAiTest.Application.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + disable + F9WebAiTest.Application + + + + + + + + + + + + + + + diff --git a/backend/F9WebAiTest.Application/Interfaces/LoginUsers/ILoginUserService.cs b/backend/F9WebAiTest.Application/Interfaces/LoginUsers/ILoginUserService.cs new file mode 100644 index 0000000..fcbc8e1 --- /dev/null +++ b/backend/F9WebAiTest.Application/Interfaces/LoginUsers/ILoginUserService.cs @@ -0,0 +1,10 @@ +using F9WebAiTest.Application.Contracts; + +namespace F9WebAiTest.Application.Interfaces.LoginUsers; + +/// 登录用户服务(从当前请求上下文获取登录用户信息) +public interface ILoginUserService +{ + /// 登录用户信息 + LoginUserInfo LoginUserInfo { get; } +} diff --git a/backend/F9WebAiTest.Application/Interfaces/Repositories/SYS/Departments/IDepartmentReadRepository.cs b/backend/F9WebAiTest.Application/Interfaces/Repositories/SYS/Departments/IDepartmentReadRepository.cs new file mode 100644 index 0000000..9d849b8 --- /dev/null +++ b/backend/F9WebAiTest.Application/Interfaces/Repositories/SYS/Departments/IDepartmentReadRepository.cs @@ -0,0 +1,10 @@ +using F9WebAiTest.Application.Dtos.SYS.Departments; +using F9WebAiTest.Domain.Interfaces; + +namespace F9WebAiTest.Application.Interfaces.Repositories.SYS.Departments; + +/// 部门读仓储(返回读模型 DTO) +public interface IDepartmentReadRepository : IDInterface +{ + Task> GetAllAsync(); +} diff --git a/backend/F9WebAiTest.Application/Interfaces/Repositories/SYS/RBAC/Menus/IMenuReadRepository.cs b/backend/F9WebAiTest.Application/Interfaces/Repositories/SYS/RBAC/Menus/IMenuReadRepository.cs new file mode 100644 index 0000000..14ec36d --- /dev/null +++ b/backend/F9WebAiTest.Application/Interfaces/Repositories/SYS/RBAC/Menus/IMenuReadRepository.cs @@ -0,0 +1,14 @@ +using F9WebAiTest.Application.Dtos.SYS.RBAC.Menus; +using F9WebAiTest.Domain.Interfaces; + +namespace F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Menus; + +/// 菜单读仓储(返回读模型 DTO) +public interface IMenuReadRepository : IDInterface +{ + Task GetMenuByIdForEditAsync(long id); + Task ExistMenuByTypeFullnameExcludeIdAsync(string typeFullname, long? excludeId); + + Task> GetAllMenuAsync(string like, long? parentId); + Task> GetTreeMenuAsync(); +} diff --git a/backend/F9WebAiTest.Application/Interfaces/Repositories/SYS/RBAC/Roles/IRoleReadRepository.cs b/backend/F9WebAiTest.Application/Interfaces/Repositories/SYS/RBAC/Roles/IRoleReadRepository.cs new file mode 100644 index 0000000..8249301 --- /dev/null +++ b/backend/F9WebAiTest.Application/Interfaces/Repositories/SYS/RBAC/Roles/IRoleReadRepository.cs @@ -0,0 +1,21 @@ +using F9WebAiTest.Application.Dtos.SYS.RBAC.Roles; +using F9WebAiTest.Domain.Enums.SYS.RBAC; +using F9WebAiTest.Domain.Interfaces; + +namespace F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Roles; + +/// 角色读仓储(返回读模型 DTO) +public interface IRoleReadRepository : IDInterface +{ + /// 查询单个 + Task GetByIdAsync(long id); + + /// 获取所有对象集合 + Task> GetAllAsync(); + + /// 获取所有对象集合(缓存用) + Task> GetAllForCacheAsync(); + + /// 通过资源类型获取角色权限集合 + Task> GetRolePermissionsByResourceType(long roleId, Resource_Type_Enum resourceType); +} diff --git a/backend/F9WebAiTest.Application/Interfaces/Repositories/SYS/RBAC/Users/IUserReadRepository.cs b/backend/F9WebAiTest.Application/Interfaces/Repositories/SYS/RBAC/Users/IUserReadRepository.cs new file mode 100644 index 0000000..40cbad2 --- /dev/null +++ b/backend/F9WebAiTest.Application/Interfaces/Repositories/SYS/RBAC/Users/IUserReadRepository.cs @@ -0,0 +1,30 @@ +using F9WebAiTest.Application.Dtos.Common; +using F9WebAiTest.Application.Dtos.SYS.RBAC.Users; +using F9WebAiTest.Domain.Interfaces; + +namespace F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Users; + +/// 用户读仓储(返回读模型 DTO) +public interface IUserReadRepository : IDInterface +{ + // 单个用户查询 + Task GetByIdAsync(long id); + + // 通过 ID 集合查询用户集合 + Task> GetByIdsAsync(List ids); + + // 通过登录名查询 + Task GetByLoginNameAsync(string loginName); + + // 通过电话查询 + Task GetByPhoneAsync(string phone); + + // 条件分页查询 + Task> GetPagedAsync(long departmentId, string like, int page = 1, int pageSize = 10); + + // 获取用户绑定角色集合 + Task> GetUserRolesByUserId(long userId); + + // 通过角色 ID 集合查询用户集合 + Task> GetByRoleIdsAsync(List roleIds); +} diff --git a/backend/F9WebAiTest.Application/Interfaces/Security/IPasswordHasher.cs b/backend/F9WebAiTest.Application/Interfaces/Security/IPasswordHasher.cs new file mode 100644 index 0000000..d08c948 --- /dev/null +++ b/backend/F9WebAiTest.Application/Interfaces/Security/IPasswordHasher.cs @@ -0,0 +1,10 @@ +namespace F9WebAiTest.Application.Interfaces.Security; + +/// 密码哈希服务 +public interface IPasswordHasher +{ + string Hash256(string input); + string GetMd5Hash(string input); + string HashPassword(string password); + bool VerifyPassword(string password, string hashedPassword); +} diff --git a/backend/F9WebAiTest.Application/Interfaces/SnowflakeID/ISnowflakeIdGenerator.cs b/backend/F9WebAiTest.Application/Interfaces/SnowflakeID/ISnowflakeIdGenerator.cs new file mode 100644 index 0000000..93e8543 --- /dev/null +++ b/backend/F9WebAiTest.Application/Interfaces/SnowflakeID/ISnowflakeIdGenerator.cs @@ -0,0 +1,7 @@ +namespace F9WebAiTest.Application.Interfaces.SnowflakeID; + +/// 雪花 ID 生成器(ID 生成属于应用能力,故接口放在应用层) +public interface ISnowflakeIdGenerator +{ + long CreateId(); +} diff --git a/backend/F9WebAiTest.Application/Interfaces/UOW/IUnitOfWork_F9.cs b/backend/F9WebAiTest.Application/Interfaces/UOW/IUnitOfWork_F9.cs new file mode 100644 index 0000000..5b7a3b6 --- /dev/null +++ b/backend/F9WebAiTest.Application/Interfaces/UOW/IUnitOfWork_F9.cs @@ -0,0 +1,8 @@ +namespace F9WebAiTest.Application.Interfaces.UOW; + +/// 工作单元:在事务内执行一组操作(简化版,替代 F9 的 Begin/Commit/Rollback) +public interface IUnitOfWork_F9 +{ + /// 在事务内执行操作,返回是否提交成功(异常自动回滚) + Task ExecuteWithUow(Func> func); +} diff --git a/backend/F9WebAiTest.Application/Services/Attendance/AttendanceService.cs b/backend/F9WebAiTest.Application/Services/Attendance/AttendanceService.cs new file mode 100644 index 0000000..4aa3bb1 --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Attendance/AttendanceService.cs @@ -0,0 +1,573 @@ +using System.Text.Json; +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos; +using F9WebAiTest.Application.Dtos.Attendance; +using F9WebAiTest.Application.Services.Notice; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities; +using F9WebAiTest.Domain.Entities.Attendance; +using F9WebAiTest.Domain.Entities.Notice; +using RBAC = F9WebAiTest.Domain.Entities.SYS.RBAC; +using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments; +using MediatR; + +namespace F9WebAiTest.Application.Services.Attendance; + +public class AttendanceService : IAttendanceService +{ + private readonly IRepository _attRepo; + private readonly IRepository _leaveRepo; + private readonly IRepository _approverRepo; + private readonly IRepository _empRepo; + private readonly IRepository _deptRepo; + private readonly IRepository _roleRepo; + private readonly IRepository _empRoleRepo; + private readonly IRepository _msgRepo; + private readonly IRepository _receiverRepo; + private readonly IRepository _todoRepo; + private readonly IRepository _typeRepo; + private readonly IMediator _mediator; + + // 上下班时间(可配置,这里简化固定) + private static readonly TimeSpan WorkStart = new(9, 0, 0); + private static readonly TimeSpan WorkEnd = new(18, 0, 0); + + public AttendanceService( + IRepository attRepo, + IRepository leaveRepo, + IRepository approverRepo, + IRepository empRepo, + IRepository deptRepo, + IRepository roleRepo, + IRepository empRoleRepo, + IRepository msgRepo, + IRepository receiverRepo, + IRepository todoRepo, + IRepository typeRepo, + IMediator mediator) + { + _attRepo = attRepo; + _leaveRepo = leaveRepo; + _approverRepo = approverRepo; + _empRepo = empRepo; + _deptRepo = deptRepo; + _roleRepo = roleRepo; + _empRoleRepo = empRoleRepo; + _msgRepo = msgRepo; + _receiverRepo = receiverRepo; + _todoRepo = todoRepo; + _typeRepo = typeRepo; + _mediator = mediator; + } + + public async Task PunchAsync(long employeeId) + { + var now = DateTime.Now; + var today = now.Date; + + var record = await _attRepo.FirstOrDefaultAsync(a => a.EmployeeId == employeeId && a.WorkDate == today); + + // 签到 + if (record == null) + { + var status = now.TimeOfDay <= WorkStart.Add(TimeSpan.FromMinutes(10)) ? "正常" : "迟到"; + var newRecord = new AttendanceRecord + { + EmployeeId = employeeId, + WorkDate = today, + SignInTime = now, + SignInStatus = status, + SignOutStatus = "未签退" + }; + await _attRepo.AddAsync(newRecord); + await _attRepo.SaveChangesAsync(); + + return new PunchResultDto + { + Type = "signIn", + Time = now, + Status = status, + Message = status == "正常" ? "签到成功,辛苦了!" : "签到成功(已迟到)" + }; + } + + // 已签到,进行签退 + if (record.SignOutTime == null) + { + var status = now.TimeOfDay >= WorkEnd ? "正常" : "早退"; + record.SignOutTime = now; + record.SignOutStatus = status; + await _attRepo.UpdateAsync(record); + await _attRepo.SaveChangesAsync(); + + return new PunchResultDto + { + Type = "signOut", + Time = now, + Status = status, + Message = status == "正常" ? "签退成功,明天见!" : "签退成功(早退)" + }; + } + + // 已签退 + return new PunchResultDto + { + Type = "done", + Time = now, + Status = "已完成", + Message = "今日已签到和签退,无需重复打卡" + }; + } + + public async Task> GetMyRecordsAsync(long employeeId, int pageIndex, int pageSize) + { + var records = (await _attRepo.FindAsync(a => a.EmployeeId == employeeId)) + .OrderByDescending(a => a.WorkDate) + .ToList(); + var total = records.Count; + var items = records.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + var emp = await _empRepo.GetByIdAsync(employeeId); + var dept = emp != null ? await _deptRepo.GetByIdAsync(emp.DepartmentId) : null; + + return new PageResult + { + Items = items.Select(a => new AttendanceDto + { + Id = a.Id, + EmployeeId = a.EmployeeId, + EmployeeName = emp?.UserName ?? "", + DepartmentName = dept?.Name ?? "", + WorkDate = a.WorkDate, + SignInTime = a.SignInTime, + SignOutTime = a.SignOutTime, + SignInStatus = a.SignInStatus, + SignOutStatus = a.SignOutStatus, + Remark = a.Remark + }).ToList(), + Total = total, + PageIndex = pageIndex, + PageSize = pageSize + }; + } + + public async Task GetMySummaryAsync(long employeeId, string month) + { + // month 格式 yyyy-MM,默认当前月 + if (string.IsNullOrWhiteSpace(month)) + { + month = DateTime.Now.ToString("yyyy-MM"); + } + var start = DateTime.Parse(month + "-01"); + var end = start.AddMonths(1); + + var records = await _attRepo.FindAsync(a => + a.EmployeeId == employeeId && a.WorkDate >= start && a.WorkDate < end); + + var summary = new AttendanceSummaryDto(); + foreach (var r in records) + { + summary.TotalDays++; + if (r.SignInStatus == "迟到") summary.LateDays++; + if (r.SignOutStatus == "早退") summary.EarlyLeaveDays++; + if (r.SignInTime == null) summary.AbsentDays++; + } + summary.NormalDays = summary.TotalDays - summary.LateDays - summary.EarlyLeaveDays - summary.AbsentDays; + + // 请假天数 + var leaves = await _leaveRepo.FindAsync(l => + l.EmployeeId == employeeId && l.Status == "已批准" && l.StartTime < end && l.EndTime >= start); + summary.LeaveDays = leaves.Sum(l => (l.EndTime - l.StartTime).Days + 1); + + return summary; + } + + public async Task> GetAllRecordsAsync(AttendanceQueryDto query) + { + var records = await _attRepo.GetAllAsync(); + var filtered = records.AsQueryable(); + + if (!string.IsNullOrWhiteSpace(query.Date)) + { + var date = DateTime.Parse(query.Date).Date; + filtered = filtered.Where(a => a.WorkDate == date); + } + + var emps = await _empRepo.GetAllAsync(); + var depts = await _deptRepo.GetAllAsync(); + var empDict = emps.ToDictionary(e => e.Id, e => e); + var deptDict = depts.ToDictionary(d => d.Id, d => d.Name); + + if (query.DepartmentId.HasValue) + { + var deptEmpIds = emps.Where(e => e.DepartmentId == query.DepartmentId.Value).Select(e => e.Id).ToHashSet(); + filtered = filtered.Where(a => deptEmpIds.Contains(a.EmployeeId)); + } + if (!string.IsNullOrWhiteSpace(query.Keyword)) + { + var kwEmpIds = emps.Where(e => e.UserName.Contains(query.Keyword) || e.LoginName.Contains(query.Keyword)) + .Select(e => e.Id).ToHashSet(); + filtered = filtered.Where(a => kwEmpIds.Contains(a.EmployeeId)); + } + + var total = filtered.LongCount(); + var items = filtered.OrderByDescending(a => a.WorkDate).ThenByDescending(a => a.SignInTime) + .Skip((query.PageIndex - 1) * query.PageSize).Take(query.PageSize).ToList(); + + return new PageResult + { + Items = items.Select(a => + { + var emp = empDict.GetValueOrDefault(a.EmployeeId); + return new AttendanceDto + { + Id = a.Id, + EmployeeId = a.EmployeeId, + EmployeeName = emp?.UserName ?? "", + DepartmentName = emp != null ? deptDict.GetValueOrDefault(emp.DepartmentId, "") : "", + WorkDate = a.WorkDate, + SignInTime = a.SignInTime, + SignOutTime = a.SignOutTime, + SignInStatus = a.SignInStatus, + SignOutStatus = a.SignOutStatus, + Remark = a.Remark + }; + }).ToList(), + Total = total, + PageIndex = query.PageIndex, + PageSize = query.PageSize + }; + } + + public async Task ApplyLeaveAsync(long employeeId, CreateLeaveDto dto) + { + if (dto.EndTime < dto.StartTime) + throw new ServiceException("结束时间不能早于开始时间"); + + // 确定审批人:优先使用前端选择的审批人,为空则回退到管理员 + var approverIds = (dto.ApproverIds ?? new List()).Where(id => id > 0).Distinct().ToList(); + if (approverIds.Count == 0) + approverIds = await GetApproverIdsAsync(); + if (approverIds.Count == 0) + throw new ServiceException("没有可用的审批人,请联系管理员"); + + var firstApprover = await _empRepo.GetByIdAsync(approverIds[0]); + var leave = new LeaveRequest + { + EmployeeId = employeeId, + Type = dto.Type, + StartTime = dto.StartTime, + EndTime = dto.EndTime, + Reason = dto.Reason ?? "", + CurrentOrderNo = 1, + ApproverId = approverIds[0], + ApproverName = firstApprover?.UserName ?? "" + }; + await _leaveRepo.AddAsync(leave); + await _leaveRepo.SaveChangesAsync(); + + // 创建审批节点(序号 = 审批顺序) + for (var i = 0; i < approverIds.Count; i++) + { + var emp = await _empRepo.GetByIdAsync(approverIds[i]); + await _approverRepo.AddAsync(new LeaveApprover + { + LeaveId = leave.Id, + ApproverId = approverIds[i], + ApproverName = emp?.UserName ?? "", + OrderNo = i + 1 + }); + } + await _approverRepo.SaveChangesAsync(); + + // 只给第一个审批人发待办(顺序审批,后续审批人待前一节点通过后才收到) + await SendLeaveApprovalAsync(leave, approverIds[0], 1, approverIds.Count); + + // 业务单据已创建,推送事件让各端刷新(多端同步) + await _mediator.Publish(new BusinessChangedEvent("LeaveApproval", leave.Id.ToString(), "Created")); + + return await ToLeaveDto(leave); + } + + public async Task> GetMyLeavesAsync(long employeeId) + { + var leaves = await _leaveRepo.FindAsync(l => l.EmployeeId == employeeId); + var result = new List(); + foreach (var l in leaves.OrderByDescending(l => l.CreatedAt)) + { + result.Add(await ToLeaveDto(l)); + } + return result; + } + + public async Task> GetAllLeavesAsync(string status) + { + var leaves = await _leaveRepo.GetAllAsync(); + if (!string.IsNullOrWhiteSpace(status) && status != "全部") + { + leaves = leaves.Where(l => l.Status == status).ToList(); + } + var result = new List(); + foreach (var l in leaves.OrderByDescending(l => l.CreatedAt)) + { + result.Add(await ToLeaveDto(l)); + } + return result; + } + + public async Task ApproveLeaveAsync(long leaveId, long approverId, string approverName, ApproveLeaveDto dto) + { + var leave = await _leaveRepo.GetByIdAsync(leaveId); + if (leave == null) throw new ServiceException("请假申请不存在", 404); + if (leave.Status != "待审批") throw new ServiceException("该申请已处理"); + + var approvers = (await _approverRepo.FindAsync(a => a.LeaveId == leaveId)) + .OrderBy(a => a.OrderNo).ToList(); + + // 校验当前审批节点 + var current = approvers.FirstOrDefault(a => a.OrderNo == leave.CurrentOrderNo); + if (current == null) throw new ServiceException("审批节点不存在", 404); + if (current.ApproverId != approverId) + throw new ServiceException($"当前应由「{current.ApproverName}」审批,您无权操作", 403); + if (current.Status != "待审批") throw new ServiceException("该节点已处理"); + + // 更新当前节点 + var handleResult = dto.Approved ? "已批准" : "已驳回"; + current.Status = handleResult; + current.ApproveRemark = dto.Remark ?? ""; + current.ApprovedAt = DateTime.Now; + await _approverRepo.UpdateAsync(current); + + if (!dto.Approved) + { + // 驳回:整体驳回,结束流程 + leave.Status = "已驳回"; + leave.ApproverId = approverId; + leave.ApproverName = approverName; + leave.ApproveRemark = dto.Remark ?? ""; + leave.ApprovedAt = DateTime.Now; + await _leaveRepo.UpdateAsync(leave); + await _leaveRepo.SaveChangesAsync(); + await _approverRepo.SaveChangesAsync(); + + // 流程终结:完成该单所有待办 + 标记消息已读,避免残留未读红点 + await FinalizeLeaveApprovalAsync(leaveId, handleResult); + await NotifyLeaveResultAsync(leave, false, dto.Remark); + await _mediator.Publish(new BusinessChangedEvent("LeaveApproval", leaveId.ToString(), "StatusChanged")); + return await ToLeaveDto(leave); + } + + // 批准:判断是否还有下一级审批人 + var next = approvers.FirstOrDefault(a => a.OrderNo == leave.CurrentOrderNo + 1); + if (next != null) + { + // 流转到下一级审批人 + leave.CurrentOrderNo = next.OrderNo; + leave.ApproverId = next.ApproverId; + leave.ApproverName = next.ApproverName; + await _leaveRepo.UpdateAsync(leave); + await _leaveRepo.SaveChangesAsync(); + await _approverRepo.SaveChangesAsync(); + + // 完成当前审批人待办并标记其消息已读,再把同一条消息流转给下一审批人 + await CompleteCurrentApproverAsync(leaveId, approverId, handleResult); + await SendLeaveApprovalAsync(leave, next.ApproverId, next.OrderNo, approvers.Count); + } + else + { + // 最后一级审批人批准:整体批准 + leave.Status = "已批准"; + leave.ApproverId = approverId; + leave.ApproverName = approverName; + leave.ApproveRemark = dto.Remark ?? ""; + leave.ApprovedAt = DateTime.Now; + await _leaveRepo.UpdateAsync(leave); + await _leaveRepo.SaveChangesAsync(); + await _approverRepo.SaveChangesAsync(); + + // 流程终结 + await FinalizeLeaveApprovalAsync(leaveId, handleResult); + await NotifyLeaveResultAsync(leave, true, dto.Remark); + } + + // 业务状态已变更,推送事件让各端刷新(多端同步) + await _mediator.Publish(new BusinessChangedEvent("LeaveApproval", leaveId.ToString(), "StatusChanged")); + return await ToLeaveDto(leave); + } + + /// 查询审批人(admin 角色用户)ID 列表(未指定审批人时的兜底) + private async Task> GetApproverIdsAsync() + { + var adminRole = await _roleRepo.FirstOrDefaultAsync(r => r.Code == "admin"); + if (adminRole == null) return new List(); + var relations = await _empRoleRepo.FindAsync(ur => ur.RoleId == adminRole.Id); + return relations.Select(ur => ur.UserId).Distinct().ToList(); + } + + /// + /// 请假审批待办消息:一张请假单仅保留一条,随审批流转复用。 + /// 首次创建,流转时更新内容与当前审批人并重置为待投递(投递服务幂等,只推新审批人)。 + /// + private async Task SendLeaveApprovalAsync(LeaveRequest leave, long targetApproverId, int currentLevel, int totalLevel) + { + var businessType = NotificationTypeEnum.LeaveApproval.ToString(); + var businessId = leave.Id.ToString(); + var content = $"类型:{leave.Type},时间:{leave.StartTime:MM-dd HH:mm} ~ {leave.EndTime:MM-dd HH:mm},事由:{leave.Reason}(第 {currentLevel}/{totalLevel} 级审批)"; + + var msg = await _msgRepo.FirstOrDefaultAsync(m => + m.BusinessType == businessType && m.BusinessId == businessId); + + if (msg == null) + { + var type = await _typeRepo.FirstOrDefaultAsync(t => t.Code == businessType); + var applicant = await _empRepo.GetByIdAsync(leave.EmployeeId); + msg = new NotificationMessage + { + NotificationTypeId = type?.Id ?? 0, + MessageTypeCode = businessType, + BusinessType = businessType, + BusinessId = businessId, + Title = $"{applicant?.UserName ?? "员工"} 提交了请假申请", + Content = content, + Priority = NotificationPriority.Important, + Status = NotificationStatus.Ready, + ForceReceivers = JsonSerializer.Serialize(new List { targetApproverId }), + CreatorId = leave.EmployeeId, + CreatorName = applicant?.UserName ?? "", + Category = NotificationCategory.Todo, + RoutePath = !string.IsNullOrWhiteSpace(type?.RouteTemplate) + ? type.RouteTemplate.Replace("{businessId}", businessId) + : $"/attendance/approve?leaveId={businessId}" + }; + await _msgRepo.AddAsync(msg); + await _msgRepo.SaveChangesAsync(); + } + else + { + // 复用:更新内容与当前审批人,重置为待投递,投递服务幂等只会推送给新审批人 + msg.Content = content; + msg.ForceReceivers = JsonSerializer.Serialize(new List { targetApproverId }); + msg.Status = NotificationStatus.Ready; + await _msgRepo.UpdateAsync(msg); + await _msgRepo.SaveChangesAsync(); + } + + // 当前审批人的待办(幂等:已存在则跳过) + var existTodo = await _todoRepo.FirstOrDefaultAsync(t => t.MessageId == msg.Id && t.AssigneeId == targetApproverId); + if (existTodo == null) + { + await _todoRepo.AddAsync(new TodoItem + { + MessageId = msg.Id, + AssigneeId = targetApproverId, + TodoStatus = TodoItemStatus.Pending + }); + await _todoRepo.SaveChangesAsync(); + } + } + + /// 完成当前审批人的待办,并标记其消息已读(流转时调用,避免已处理人残留未读红点) + private async Task CompleteCurrentApproverAsync(long leaveId, long approverId, string handleResult) + { + var msgs = await _msgRepo.FindAsync(m => + m.BusinessType == NotificationTypeEnum.LeaveApproval.ToString() && m.BusinessId == leaveId.ToString()); + var msgIds = msgs.Select(m => m.Id).ToList(); + if (msgIds.Count == 0) return; + + var todos = await _todoRepo.FindAsync(t => + msgIds.Contains(t.MessageId) && t.AssigneeId == approverId && t.TodoStatus == TodoItemStatus.Pending); + foreach (var t in todos) + { + t.TodoStatus = TodoItemStatus.Completed; + t.CompletedTime = DateTime.Now; + t.HandleResult = handleResult; + await _todoRepo.UpdateAsync(t); + } + + var receivers = await _receiverRepo.FindAsync(r => + msgIds.Contains(r.MessageId) && r.UserId == approverId && r.ReadStatus == NotificationReadStatus.Unread); + foreach (var r in receivers) + { + r.ReadStatus = NotificationReadStatus.Read; + r.ReadTime = DateTime.Now; + await _receiverRepo.UpdateAsync(r); + } + } + + /// 流程终结:完成该单所有待处理待办,并标记所有未读消息为已读 + private async Task FinalizeLeaveApprovalAsync(long leaveId, string handleResult) + { + var msgs = await _msgRepo.FindAsync(m => + m.BusinessType == NotificationTypeEnum.LeaveApproval.ToString() && m.BusinessId == leaveId.ToString()); + var msgIds = msgs.Select(m => m.Id).ToList(); + if (msgIds.Count == 0) return; + + var todos = await _todoRepo.FindAsync(t => msgIds.Contains(t.MessageId) && t.TodoStatus == TodoItemStatus.Pending); + foreach (var t in todos) + { + t.TodoStatus = TodoItemStatus.Completed; + t.CompletedTime = DateTime.Now; + t.HandleResult = handleResult; + await _todoRepo.UpdateAsync(t); + } + + var receivers = await _receiverRepo.FindAsync(r => msgIds.Contains(r.MessageId) && r.ReadStatus == NotificationReadStatus.Unread); + foreach (var r in receivers) + { + r.ReadStatus = NotificationReadStatus.Read; + r.ReadTime = DateTime.Now; + await _receiverRepo.UpdateAsync(r); + } + } + + /// 通知申请人审批结果 + private async Task NotifyLeaveResultAsync(LeaveRequest leave, bool approved, string remark) + { + await _mediator.Send(new SendNotificationCommand( + NotificationTypeEnum.LeaveResult, + BusinessId: leave.Id.ToString(), + Title: approved ? "请假申请已批准" : "请假申请已驳回", + Content: $"您的请假({leave.Type},{leave.StartTime:MM-dd HH:mm} ~ {leave.EndTime:MM-dd HH:mm})已{(approved ? "批准" : "驳回")}。审批意见:{remark}", + ForceReceivers: new List { leave.EmployeeId } + )); + } + + private async Task ToLeaveDto(LeaveRequest l) + { + var emp = await _empRepo.GetByIdAsync(l.EmployeeId); + var dept = emp != null ? await _deptRepo.GetByIdAsync(emp.DepartmentId) : null; + + var approvers = (await _approverRepo.FindAsync(a => a.LeaveId == l.Id)) + .OrderBy(a => a.OrderNo) + .Select(a => new LeaveApproverDto + { + Id = a.Id, + ApproverId = a.ApproverId, + ApproverName = a.ApproverName, + OrderNo = a.OrderNo, + Status = a.Status, + ApproveRemark = a.ApproveRemark, + ApprovedAt = a.ApprovedAt + }) + .ToList(); + + return new LeaveRequestDto + { + Id = l.Id, + EmployeeId = l.EmployeeId, + EmployeeName = emp?.UserName ?? "", + DepartmentName = dept?.Name ?? "", + Type = l.Type, + StartTime = l.StartTime, + EndTime = l.EndTime, + Reason = l.Reason, + Status = l.Status, + CurrentOrderNo = l.CurrentOrderNo, + ApproverName = l.ApproverName, + ApproveRemark = l.ApproveRemark, + ApprovedAt = l.ApprovedAt, + CreatedAt = l.CreatedAt, + Approvers = approvers + }; + } +} diff --git a/backend/F9WebAiTest.Application/Services/Attendance/IAttendanceService.cs b/backend/F9WebAiTest.Application/Services/Attendance/IAttendanceService.cs new file mode 100644 index 0000000..2814976 --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Attendance/IAttendanceService.cs @@ -0,0 +1,26 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos; +using F9WebAiTest.Application.Dtos.Attendance; + +namespace F9WebAiTest.Application.Services.Attendance; + +public interface IAttendanceService +{ + // 打卡 + Task PunchAsync(long employeeId); + + // 我的考勤记录(分页) + Task> GetMyRecordsAsync(long employeeId, int pageIndex, int pageSize); + + // 我的考勤统计 + Task GetMySummaryAsync(long employeeId, string month); + + // 全部考勤记录(管理员) + Task> GetAllRecordsAsync(AttendanceQueryDto query); + + // 请假 + Task ApplyLeaveAsync(long employeeId, CreateLeaveDto dto); + Task> GetMyLeavesAsync(long employeeId); + Task> GetAllLeavesAsync(string status); + Task ApproveLeaveAsync(long leaveId, long approverId, string approverName, ApproveLeaveDto dto); +} diff --git a/backend/F9WebAiTest.Application/Services/Auth/AuthService.cs b/backend/F9WebAiTest.Application/Services/Auth/AuthService.cs new file mode 100644 index 0000000..77e8368 --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Auth/AuthService.cs @@ -0,0 +1,300 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos; +using F9WebAiTest.Application.Dtos.Auth; +using F9WebAiTest.Application.Interfaces.Security; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities.SYS.Departments; +using F9WebAiTest.Domain.Entities.SYS.RBAC; +using F9WebAiTest.Domain.Enums.SYS.RBAC; +using Microsoft.Extensions.Configuration; +using RefreshToken = F9WebAiTest.Domain.Entities.RefreshToken; + +namespace F9WebAiTest.Application.Services.Auth; + +public class AuthService : IAuthService +{ + private readonly IRepository _userRepo; + private readonly IRepository _userRoleRepo; + private readonly IRepository _rolePermissionRepo; + private readonly IRepository _permissionRepo; + private readonly IRepository _menuRepo; + private readonly IRepository _roleRepo; + private readonly IRepository _deptRepo; + private readonly IRepository _refreshTokenRepo; + private readonly IJwtService _jwt; + private readonly IConfiguration _config; + private readonly IPasswordHasher _passwordHasher; + + public AuthService( + IRepository userRepo, + IRepository userRoleRepo, + IRepository rolePermissionRepo, + IRepository permissionRepo, + IRepository menuRepo, + IRepository roleRepo, + IRepository deptRepo, + IRepository refreshTokenRepo, + IJwtService jwt, + IConfiguration config, + IPasswordHasher passwordHasher) + { + _userRepo = userRepo; + _userRoleRepo = userRoleRepo; + _rolePermissionRepo = rolePermissionRepo; + _permissionRepo = permissionRepo; + _menuRepo = menuRepo; + _roleRepo = roleRepo; + _deptRepo = deptRepo; + _refreshTokenRepo = refreshTokenRepo; + _jwt = jwt; + _config = config; + _passwordHasher = passwordHasher; + } + + public async Task LoginAsync(LoginDto dto) + { + var user = await _userRepo.FirstOrDefaultAsync(u => u.LoginName == dto.Username); + if (user == null || !VerifyPassword(dto.Password, user.PasswordHash)) + throw new ServiceException("用户名或密码错误", 401); + + if (!user.IsActive) + throw new ServiceException("账号已被禁用,请联系管理员", 403); + + return await IssueTokensAsync(user); + } + + public async Task RefreshTokenAsync(string refreshToken) + { + var stored = await _refreshTokenRepo.FirstOrDefaultAsync(rt => rt.Token == refreshToken); + if (stored == null || stored.IsRevoked || stored.ExpiresAt < DateTime.UtcNow) + throw new ServiceException("刷新令牌无效或已过期", 401); + + var user = await _userRepo.GetByIdAsync(stored.EmployeeId); + if (user == null || !user.IsActive) + throw new ServiceException("用户不存在或已禁用", 401); + + // 撤销旧 token,签发新 token(旋转) + stored.IsRevoked = true; + await _refreshTokenRepo.UpdateAsync(stored); + + return await IssueTokensAsync(user); + } + + public async Task RevokeRefreshTokenAsync(string refreshToken) + { + var stored = await _refreshTokenRepo.FirstOrDefaultAsync(rt => rt.Token == refreshToken); + if (stored != null) + { + stored.IsRevoked = true; + await _refreshTokenRepo.UpdateAsync(stored); + await _refreshTokenRepo.SaveChangesAsync(); + } + } + + public async Task GetCurrentUserAsync(long userId) + { + var user = await _userRepo.GetByIdAsync(userId); + if (user == null) throw new ServiceException("用户不存在", 404); + return await BuildUserInfoAsync(user); + } + + public async Task> GetUserMenusAsync(long userId) + { + var roleIds = (await _userRoleRepo.FindAsync(ur => ur.UserId == userId)) + .Select(ur => ur.RoleId).ToList(); + + // 超级管理员返回全部菜单 + var isAdmin = await IsAdminAsync(userId); + + List menus; + if (isAdmin) + { + menus = await _menuRepo.FindAsync(m => m.IsActive); + } + else + { + // 通过 Permission 表关联菜单:RolePermission → Permission(ResourceType=菜单) → ResourceId(=Menu.Id) + var permissionIds = (await _rolePermissionRepo.FindAsync(rp => roleIds.Contains(rp.RoleId))) + .Select(rp => rp.PermissionId).Distinct().ToList(); + var permittedMenuIds = (await _permissionRepo.FindAsync(p => + permissionIds.Contains(p.Id) && p.ResourceType == Resource_Type_Enum.菜单)) + .Select(p => p.ResourceId).Distinct().ToHashSet(); + + // 全部活跃菜单 + var allMenus = await _menuRepo.FindAsync(m => m.IsActive); + var menuDict = allMenus.ToDictionary(m => m.Id); + + // 有权限的菜单 + 它们的祖先目录(父/祖目录自动包含,保证菜单树完整,非管理员也能看到一级菜单) + var resultIds = new HashSet(); + foreach (var id in permittedMenuIds) + { + var currentId = id; + while (currentId != 0 && menuDict.ContainsKey(currentId) && resultIds.Add(currentId)) + { + currentId = menuDict[currentId].ParentId; + } + } + + menus = allMenus.Where(m => resultIds.Contains(m.Id)).ToList(); + } + + // 只返回目录和菜单(分组/页面),按排序 + menus = menus.Where(m => m.MenuType != MenuType_Enum.默认) + .OrderBy(m => m.SortOrder).ToList(); + + return BuildMenuTree(menus); + } + + public async Task> GetUserPermissionsAsync(long userId) + { + var isAdmin = await IsAdminAsync(userId); + if (isAdmin) + { + // 超级管理员:返回所有权限单元 Code(含菜单权限 + 资源权限) + return (await _permissionRepo.FindAsync(p => !string.IsNullOrEmpty(p.Code))) + .Select(p => p.Code).Distinct().ToList(); + } + + // 普通用户:角色 → RolePermission → Permission → Code(统一权限模型) + var roleIds = (await _userRoleRepo.FindAsync(ur => ur.UserId == userId)) + .Select(ur => ur.RoleId).ToList(); + var permissionIds = (await _rolePermissionRepo.FindAsync(rp => roleIds.Contains(rp.RoleId))) + .Select(rp => rp.PermissionId).Distinct().ToList(); + return (await _permissionRepo.FindAsync(p => permissionIds.Contains(p.Id) && !string.IsNullOrEmpty(p.Code))) + .Select(p => p.Code).Distinct().ToList(); + } + + public async Task ChangePasswordAsync(long userId, ChangePasswordDto dto) + { + var user = await _userRepo.GetByIdAsync(userId); + if (user == null) throw new ServiceException("用户不存在", 404); + if (!VerifyPassword(dto.OldPassword, user.PasswordHash)) + throw new ServiceException("原密码错误"); + if ((dto.NewPassword ?? "").Length < 6) + throw new ServiceException("新密码至少6位"); + + // 新密码与创建用户一致:BCrypt(MD5(明文)) + user.SetPassword(_passwordHasher.HashPassword(_passwordHasher.GetMd5Hash(dto.NewPassword))); + await _userRepo.UpdateAsync(user); + await _userRepo.SaveChangesAsync(); + } + + public async Task UpdateProfileAsync(long userId, UpdateProfileDto dto) + { + var user = await _userRepo.GetByIdAsync(userId); + if (user == null) throw new ServiceException("用户不存在", 404); + + user.UpdateProfile(dto.Nickname, dto.Avatar, dto.Email, dto.Phone, null); + await _userRepo.UpdateAsync(user); + await _userRepo.SaveChangesAsync(); + } + + // ---- 私有方法 ---- + + /// 校验密码,兼容旧格式 BCrypt(明文) 与新格式 BCrypt(MD5(明文)) + private bool VerifyPassword(string password, string passwordHash) + { + if (string.IsNullOrEmpty(passwordHash)) return false; + if (_passwordHasher.VerifyPassword(password, passwordHash)) return true; + return _passwordHasher.VerifyPassword(_passwordHasher.GetMd5Hash(password), passwordHash); + } + + private async Task IssueTokensAsync(User user) + { + var roles = await GetRoleCodesAsync(user.Id); + var accessToken = _jwt.GenerateAccessToken(user, roles); + var refreshToken = _jwt.GenerateRefreshToken(); + var expireMinutes = int.Parse(_config["Jwt:AccessTokenExpireMinutes"] ?? "120"); + + // 存储 refresh token + var expireDays = int.Parse(_config["Jwt:RefreshTokenExpireDays"] ?? "7"); + await _refreshTokenRepo.AddAsync(new RefreshToken + { + EmployeeId = user.Id, + Token = refreshToken, + ExpiresAt = DateTime.UtcNow.AddDays(expireDays) + }); + await _refreshTokenRepo.SaveChangesAsync(); + + var userInfo = await BuildUserInfoAsync(user); + return new LoginResultDto + { + AccessToken = accessToken, + RefreshToken = refreshToken, + AccessTokenExpiresAt = DateTime.UtcNow.AddMinutes(expireMinutes), + User = userInfo + }; + } + + private async Task> GetRoleCodesAsync(long userId) + { + var roleIds = (await _userRoleRepo.FindAsync(ur => ur.UserId == userId)) + .Select(ur => ur.RoleId).ToList(); + var roles = await _roleRepo.FindAsync(r => roleIds.Contains(r.Id)); + return roles.Select(r => r.Code).ToList(); + } + + private async Task IsAdminAsync(long userId) + { + var roleIds = (await _userRoleRepo.FindAsync(ur => ur.UserId == userId)) + .Select(ur => ur.RoleId).ToList(); + var roles = await _roleRepo.FindAsync(r => roleIds.Contains(r.Id)); + return roles.Any(r => r.Code == "admin"); + } + + private async Task BuildUserInfoAsync(User user) + { + var dept = await _deptRepo.GetByIdAsync(user.DepartmentId); + var roleIds = (await _userRoleRepo.FindAsync(ur => ur.UserId == user.Id)) + .Select(ur => ur.RoleId).ToList(); + var roles = await _roleRepo.FindAsync(r => roleIds.Contains(r.Id)); + + return new EmployeeInfoDto + { + Id = user.Id, + Username = user.LoginName, + RealName = user.UserName, + Nickname = user.Nickname, + Avatar = user.Avatar, + Phone = user.PhoneNumber, + Email = user.Email, + DepartmentId = user.DepartmentId, + DepartmentName = dept?.Name ?? "", + Status = user.IsActive ? 1 : 0, + Roles = roles.Select(r => r.Code).ToList(), + Permissions = await GetUserPermissionsAsync(user.Id) + }; + } + + private static List BuildMenuTree(List menus) + { + var dict = menus.ToDictionary(m => m.Id, m => new MenuDto + { + Id = m.Id, + ParentId = m.ParentId == 0 ? null : m.ParentId, + Name = m.Name, + Type = m.MenuType == MenuType_Enum.分组 ? "directory" : "menu", + Path = m.Path, + Component = m.Component, + Icon = m.IconSvg, + Permission = m.Permission, + Sort = m.SortOrder, + Status = m.IsActive ? 1 : 0 + }); + + var roots = new List(); + foreach (var menu in menus) + { + var dto = dict[menu.Id]; + if (menu.ParentId == 0 || !dict.ContainsKey(menu.ParentId)) + { + roots.Add(dto); + } + else + { + dict[menu.ParentId].Children.Add(dto); + } + } + return roots.OrderBy(m => m.Sort).ToList(); + } +} diff --git a/backend/F9WebAiTest.Application/Services/Auth/IAuthService.cs b/backend/F9WebAiTest.Application/Services/Auth/IAuthService.cs new file mode 100644 index 0000000..3a12cb4 --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Auth/IAuthService.cs @@ -0,0 +1,16 @@ +using F9WebAiTest.Application.Dtos; +using F9WebAiTest.Application.Dtos.Auth; + +namespace F9WebAiTest.Application.Services.Auth; + +public interface IAuthService +{ + Task LoginAsync(LoginDto dto); + Task RefreshTokenAsync(string refreshToken); + Task RevokeRefreshTokenAsync(string refreshToken); + Task GetCurrentUserAsync(long employeeId); + Task> GetUserMenusAsync(long employeeId); + Task> GetUserPermissionsAsync(long employeeId); + Task ChangePasswordAsync(long employeeId, ChangePasswordDto dto); + Task UpdateProfileAsync(long employeeId, UpdateProfileDto dto); +} diff --git a/backend/F9WebAiTest.Application/Services/Auth/IJwtService.cs b/backend/F9WebAiTest.Application/Services/Auth/IJwtService.cs new file mode 100644 index 0000000..1f49d7c --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Auth/IJwtService.cs @@ -0,0 +1,11 @@ +using F9WebAiTest.Domain.Entities.SYS.RBAC; + +namespace F9WebAiTest.Application.Services.Auth; + +public interface IJwtService +{ + string GenerateAccessToken(User user, IEnumerable roles); + string GenerateRefreshToken(); + (long id, string username) ValidateAccessToken(string token); + long? ValidateRefreshToken(string token); +} diff --git a/backend/F9WebAiTest.Application/Services/Auth/JwtService.cs b/backend/F9WebAiTest.Application/Services/Auth/JwtService.cs new file mode 100644 index 0000000..745ec89 --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Auth/JwtService.cs @@ -0,0 +1,102 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using F9WebAiTest.Domain.Entities.SYS.RBAC; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; + +namespace F9WebAiTest.Application.Services.Auth; + +public class JwtService : IJwtService +{ + private readonly IConfiguration _config; + + public JwtService(IConfiguration config) + { + _config = config; + } + + // 生成 access token(短期,如 2 小时) + public string GenerateAccessToken(User user, IEnumerable roles) + { + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"])); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var claims = new List + { + new("id", user.Id.ToString()), + new(ClaimTypes.Name, user.LoginName), + new("realName", user.UserName) + }; + foreach (var r in roles) + { + claims.Add(new Claim(ClaimTypes.Role, r)); + } + + var expireMinutes = int.Parse(_config["Jwt:AccessTokenExpireMinutes"] ?? "120"); + var token = new JwtSecurityToken( + issuer: _config["Jwt:Issuer"], + audience: _config["Jwt:Audience"], + claims: claims, + expires: DateTime.UtcNow.AddMinutes(expireMinutes), + signingCredentials: creds + ); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + + // 生成 refresh token(随机字符串,长期,如 7 天) + public string GenerateRefreshToken() + { + var randomNumber = new byte[64]; + using var rng = RandomNumberGenerator.Create(); + rng.GetBytes(randomNumber); + return Convert.ToBase64String(randomNumber); + } + + public (long id, string username) ValidateAccessToken(string token) + { + var handler = new JwtSecurityTokenHandler(); + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"])); + var validationParams = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = key, + ValidateIssuer = true, + ValidIssuer = _config["Jwt:Issuer"], + ValidateAudience = true, + ValidAudience = _config["Jwt:Audience"], + ValidateLifetime = true, + ClockSkew = TimeSpan.Zero + }; + var principal = handler.ValidateToken(token, validationParams, out _); + var id = long.Parse(principal.FindFirst("id")!.Value); + var username = principal.Identity!.Name; + return (id, username); + } + + // 校验 refresh token 的签名(refresh token 也是 JWT,但只含 id claim) + public long? ValidateRefreshToken(string token) + { + try + { + var handler = new JwtSecurityTokenHandler(); + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"])); + var validationParams = new TokenValidationParameters + { + ValidateIssuerSigningKey = true, + IssuerSigningKey = key, + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = false + }; + var principal = handler.ValidateToken(token, validationParams, out _); + return long.Parse(principal.FindFirst("id")!.Value); + } + catch + { + return null; + } + } +} diff --git a/backend/F9WebAiTest.Application/Services/Base/BusinessServices.cs b/backend/F9WebAiTest.Application/Services/Base/BusinessServices.cs new file mode 100644 index 0000000..a42415a --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Base/BusinessServices.cs @@ -0,0 +1,328 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos; +using F9WebAiTest.Application.Dtos.Base; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities; +using F9WebAiTest.Domain.Entities.Base; +using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments; + +namespace F9WebAiTest.Application.Services.Base; + +// ==================== 工艺庄口 ==================== +public interface ICraftZhuangKouService +{ + Task> GetPagedAsync(int pageIndex, int pageSize, string keyword, int? year); + Task CreateAsync(CraftZhuangKouCreateDto dto); + Task UpdateAsync(long id, CraftZhuangKouCreateDto dto); + Task DeleteAsync(long id); +} + +public class CraftZhuangKouService : ICraftZhuangKouService +{ + private readonly IRepository _repo; + public CraftZhuangKouService(IRepository repo) => _repo = repo; + + private static string BendiWaidiName(int v) => v switch { 1 => "本地", 2 => "外地", _ => "" }; + + public async Task> GetPagedAsync(int pageIndex, int pageSize, string keyword, int? year) + { + var all = await _repo.GetAllAsync(); + var q = all.AsQueryable(); + if (!string.IsNullOrWhiteSpace(keyword)) + q = q.Where(x => x.ZhuangKouName.Contains(keyword) || x.ZhuangKouCode.Contains(keyword) || x.ProductionDisplayName.Contains(keyword)); + if (year.HasValue) + q = q.Where(x => x.BelongingYear == year.Value); + + var total = q.LongCount(); + var items = q.OrderBy(x => x.Sort).ThenBy(x => x.Id) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + return new PageResult + { + Items = items.Select(ToDto).ToList(), + Total = total, PageIndex = pageIndex, PageSize = pageSize + }; + } + + public async Task CreateAsync(CraftZhuangKouCreateDto dto) + { + if (string.IsNullOrWhiteSpace(dto.ZhuangKouName)) throw new ServiceException("庄口名称不能为空"); + var e = new CraftZhuangKou + { + BelongingYear = dto.BelongingYear, BendiWaidi = dto.BendiWaidi, JidiWaigou = dto.JidiWaigou, + ZhuangKouCode = dto.ZhuangKouCode, ZhuangKouName = dto.ZhuangKouName, + ProductionDisplayName = dto.ProductionDisplayName, Chandi = dto.Chandi, + Sort = dto.Sort, Memo = dto.Memo, IsJiezhuang = dto.IsJiezhuang + }; + await _repo.AddAsync(e); + await _repo.SaveChangesAsync(); + return ToDto(e); + } + + public async Task UpdateAsync(long id, CraftZhuangKouCreateDto dto) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("记录不存在", 404); + e.BelongingYear = dto.BelongingYear; e.BendiWaidi = dto.BendiWaidi; e.JidiWaigou = dto.JidiWaigou; + e.ZhuangKouCode = dto.ZhuangKouCode; e.ZhuangKouName = dto.ZhuangKouName; + e.ProductionDisplayName = dto.ProductionDisplayName; e.Chandi = dto.Chandi; + e.Sort = dto.Sort; e.Memo = dto.Memo; e.IsJiezhuang = dto.IsJiezhuang; + await _repo.UpdateAsync(e); + await _repo.SaveChangesAsync(); + return ToDto(e); + } + + public async Task DeleteAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("记录不存在", 404); + await _repo.DeleteAsync(e); + await _repo.SaveChangesAsync(); + } + + private static CraftZhuangKouDto ToDto(CraftZhuangKou e) => new() + { + Id = e.Id, BelongingYear = e.BelongingYear, BendiWaidi = e.BendiWaidi, BendiWaidiName = BendiWaidiName(e.BendiWaidi), + JidiWaigou = e.JidiWaigou, ZhuangKouCode = e.ZhuangKouCode, ZhuangKouName = e.ZhuangKouName, + ProductionDisplayName = e.ProductionDisplayName, Chandi = e.Chandi, Sort = e.Sort, Memo = e.Memo, IsJiezhuang = e.IsJiezhuang + }; +} + +// ==================== 生丝规格 ==================== +public interface IShengsiGuigeService +{ + Task> GetPagedAsync(int pageIndex, int pageSize, string keyword); + Task> GetAllGuigeAsync(); + Task CreateAsync(ShengsiGuigeDto dto); + Task UpdateAsync(long id, ShengsiGuigeDto dto); + Task DeleteAsync(long id); +} + +public class ShengsiGuigeService : IShengsiGuigeService +{ + private readonly IRepository _repo; + public ShengsiGuigeService(IRepository repo) => _repo = repo; + + public async Task> GetPagedAsync(int pageIndex, int pageSize, string keyword) + { + var all = await _repo.GetAllAsync(); + var q = all.AsQueryable(); + if (!string.IsNullOrWhiteSpace(keyword)) q = q.Where(x => x.Guige.Contains(keyword)); + var total = q.LongCount(); + var items = q.OrderBy(x => x.Sort).ThenBy(x => x.Id).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + return new PageResult { Items = items.Select(ToDto).ToList(), Total = total, PageIndex = pageIndex, PageSize = pageSize }; + } + + public async Task> GetAllGuigeAsync() + => (await _repo.GetAllAsync()).OrderBy(x => x.Sort).Select(x => x.Guige).Distinct().ToList(); + + public async Task CreateAsync(ShengsiGuigeDto dto) + { + if (string.IsNullOrWhiteSpace(dto.Guige)) throw new ServiceException("规格不能为空"); + var e = Map(new ShengsiGuige(), dto); + await _repo.AddAsync(e); + await _repo.SaveChangesAsync(); + return ToDto(e); + } + + public async Task UpdateAsync(long id, ShengsiGuigeDto dto) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("记录不存在", 404); + Map(e, dto); + await _repo.UpdateAsync(e); + await _repo.SaveChangesAsync(); + return ToDto(e); + } + + public async Task DeleteAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("记录不存在", 404); + await _repo.DeleteAsync(e); + await _repo.SaveChangesAsync(); + } + + private static ShengsiGuige Map(ShengsiGuige e, ShengsiGuigeDto d) + { + e.Guige = d.Guige; e.CenterrangeLolimit = d.CenterrangeLolimit; e.CenterrangeUplimit = d.CenterrangeUplimit; + e.CenterXiandu = d.CenterXiandu; e.ValidLolimit = d.ValidLolimit; e.ValidUplimit = d.ValidUplimit; + e.XiaoyeLolimit = d.XiaoyeLolimit; e.XiaoyeUplimit = d.XiaoyeUplimit; + e.DayeLolimit = d.DayeLolimit; e.DayeUplimit = d.DayeUplimit; + e.TeyeLolimit = d.TeyeLolimit; e.TeyeUplimit = d.TeyeUplimit; + e.YexianNkLolimitLeft = d.YexianNkLolimitLeft; e.YexianNkLolimitRight = d.YexianNkLolimitRight; + e.YexianNkUplimitLeft = d.YexianNkUplimitLeft; e.YexianNkUplimitRight = d.YexianNkUplimitRight; + e.LaodongJingsaiYexianLolimit = d.LaodongJingsaiYexianLolimit; e.LaodongJingsaiYexianUplimit = d.LaodongJingsaiYexianUplimit; + e.Sort = d.Sort; e.Memo = d.Memo; + return e; + } + + private static ShengsiGuigeDto ToDto(ShengsiGuige e) => new() + { + Id = e.Id, Guige = e.Guige, CenterrangeLolimit = e.CenterrangeLolimit, CenterrangeUplimit = e.CenterrangeUplimit, + CenterXiandu = e.CenterXiandu, ValidLolimit = e.ValidLolimit, ValidUplimit = e.ValidUplimit, + XiaoyeLolimit = e.XiaoyeLolimit, XiaoyeUplimit = e.XiaoyeUplimit, + DayeLolimit = e.DayeLolimit, DayeUplimit = e.DayeUplimit, + TeyeLolimit = e.TeyeLolimit, TeyeUplimit = e.TeyeUplimit, + YexianNkLolimitLeft = e.YexianNkLolimitLeft, YexianNkLolimitRight = e.YexianNkLolimitRight, + YexianNkUplimitLeft = e.YexianNkUplimitLeft, YexianNkUplimitRight = e.YexianNkUplimitRight, + LaodongJingsaiYexianLolimit = e.LaodongJingsaiYexianLolimit, LaodongJingsaiYexianUplimit = e.LaodongJingsaiYexianUplimit, + Sort = e.Sort, Memo = e.Memo + }; +} + +// ==================== 生丝定级指标 ==================== +public interface IShengsiDingjizhibiaoService +{ + Task> GetPagedAsync(int pageIndex, int pageSize, string keyword, int? standard, string guige); + Task CreateAsync(ShengsiDingjizhibiaoDto dto); + Task UpdateAsync(long id, ShengsiDingjizhibiaoDto dto); + Task DeleteAsync(long id); +} + +public class ShengsiDingjizhibiaoService : IShengsiDingjizhibiaoService +{ + private readonly IRepository _repo; + public ShengsiDingjizhibiaoService(IRepository repo) => _repo = repo; + + private static string StandardName(int v) => v switch { 0 => "厂标", 1 => "国标", 2 => "个人", _ => "" }; + + public async Task> GetPagedAsync(int pageIndex, int pageSize, string keyword, int? standard, string guige) + { + var all = await _repo.GetAllAsync(); + var q = all.AsQueryable(); + if (!string.IsNullOrWhiteSpace(keyword)) q = q.Where(x => x.Grade.Contains(keyword)); + if (standard.HasValue) q = q.Where(x => x.Standard == standard.Value); + if (!string.IsNullOrWhiteSpace(guige)) q = q.Where(x => x.Guige == guige); + var total = q.LongCount(); + var items = q.OrderBy(x => x.Standard).ThenBy(x => x.Guige).ThenBy(x => x.GradeScore) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + return new PageResult { Items = items.Select(ToDto).ToList(), Total = total, PageIndex = pageIndex, PageSize = pageSize }; + } + + public async Task CreateAsync(ShengsiDingjizhibiaoDto dto) + { + if (string.IsNullOrWhiteSpace(dto.Grade)) throw new ServiceException("等级不能为空"); + var e = Map(new ShengsiDingjizhibiao(), dto); + await _repo.AddAsync(e); + await _repo.SaveChangesAsync(); + return ToDto(e); + } + + public async Task UpdateAsync(long id, ShengsiDingjizhibiaoDto dto) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("记录不存在", 404); + Map(e, dto); + await _repo.UpdateAsync(e); + await _repo.SaveChangesAsync(); + return ToDto(e); + } + + public async Task DeleteAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("记录不存在", 404); + await _repo.DeleteAsync(e); + await _repo.SaveChangesAsync(); + } + + private static ShengsiDingjizhibiao Map(ShengsiDingjizhibiao e, ShengsiDingjizhibiaoDto d) + { + e.Standard = d.Standard; e.Guige = d.Guige; e.Grade = d.Grade; e.GradeScore = d.GradeScore; + e.Piancha = d.Piancha; e.Dacha = d.Dacha; e.Erdu = d.Erdu; e.Sandu = d.Sandu; + e.Qingjie = d.Qingjie; e.Jiejing = d.Jiejing; + return e; + } + + private static ShengsiDingjizhibiaoDto ToDto(ShengsiDingjizhibiao e) => new() + { + Id = e.Id, Standard = e.Standard, StandardName = StandardName(e.Standard), Guige = e.Guige, Grade = e.Grade, + GradeScore = e.GradeScore, Piancha = e.Piancha, Dacha = e.Dacha, Erdu = e.Erdu, Sandu = e.Sandu, + Qingjie = e.Qingjie, Jiejing = e.Jiejing + }; +} + +// ==================== 机组车台 ==================== +public interface IJizuChetaiService +{ + Task> GetPagedAsync(int pageIndex, int pageSize, string keyword); + Task GetByDepIdAndJizuAsync(long deptId, int jizu); + Task CreateAsync(JizuChetaiDto dto); + Task UpdateAsync(long id, JizuChetaiDto dto); + Task DeleteAsync(long id); +} + +public class JizuChetaiService : IJizuChetaiService +{ + private readonly IRepository _repo; + private readonly IRepository _deptRepo; + public JizuChetaiService(IRepository repo, IRepository deptRepo) + { + _repo = repo; _deptRepo = deptRepo; + } + + public async Task> GetPagedAsync(int pageIndex, int pageSize, string keyword) + { + var all = await _repo.GetAllAsync(); + var q = all.AsQueryable(); + var depts = await _deptRepo.GetAllAsync(); + var deptDict = depts.ToDictionary(d => d.Id, d => d.Name); + + if (!string.IsNullOrWhiteSpace(keyword)) + { + var kwDeptIds = depts.Where(d => d.Name.Contains(keyword)).Select(d => d.Id).ToHashSet(); + q = q.Where(x => kwDeptIds.Contains(x.DepartmentId) || x.Jizu.ToString().Contains(keyword)); + } + var total = q.LongCount(); + var items = q.OrderBy(x => x.DepartmentId).ThenBy(x => x.Jizu).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + return new PageResult + { + Items = items.Select(x => new JizuChetaiDto + { + Id = x.Id, DepartmentId = x.DepartmentId, DepartmentName = deptDict.GetValueOrDefault(x.DepartmentId, ""), + Jizu = x.Jizu, ChetaiStart = x.ChetaiStart, ChetaiEnd = x.ChetaiEnd + }).ToList(), + Total = total, PageIndex = pageIndex, PageSize = pageSize + }; + } + + public async Task GetByDepIdAndJizuAsync(long deptId, int jizu) + { + var e = await _repo.FirstOrDefaultAsync(x => x.DepartmentId == deptId && x.Jizu == jizu); + if (e == null) return null; + return await GetDto(e); + } + + public async Task CreateAsync(JizuChetaiDto dto) + { + var e = new JizuChetai { DepartmentId = dto.DepartmentId, Jizu = dto.Jizu, ChetaiStart = dto.ChetaiStart, ChetaiEnd = dto.ChetaiEnd }; + await _repo.AddAsync(e); + await _repo.SaveChangesAsync(); + return await GetDto(e); + } + + public async Task UpdateAsync(long id, JizuChetaiDto dto) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("记录不存在", 404); + e.DepartmentId = dto.DepartmentId; e.Jizu = dto.Jizu; e.ChetaiStart = dto.ChetaiStart; e.ChetaiEnd = dto.ChetaiEnd; + await _repo.UpdateAsync(e); + await _repo.SaveChangesAsync(); + return await GetDto(e); + } + + public async Task DeleteAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("记录不存在", 404); + await _repo.DeleteAsync(e); + await _repo.SaveChangesAsync(); + } + + private async Task GetDto(JizuChetai e) + { + var dept = await _deptRepo.GetByIdAsync(e.DepartmentId); + return new JizuChetaiDto { Id = e.Id, DepartmentId = e.DepartmentId, DepartmentName = dept?.Name ?? "", Jizu = e.Jizu, ChetaiStart = e.ChetaiStart, ChetaiEnd = e.ChetaiEnd }; + } +} diff --git a/backend/F9WebAiTest.Application/Services/BusinessChangedEvent.cs b/backend/F9WebAiTest.Application/Services/BusinessChangedEvent.cs new file mode 100644 index 0000000..756590f --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/BusinessChangedEvent.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace F9WebAiTest.Application.Services; + +/// +/// 业务状态变更事件(通用)。 +/// 业务单据状态变化时发布,Api 层订阅后通过 SignalR 推送给所有在线客户端, +/// 各端监听后刷新已打开的相关页面,实现多端实时同步。 +/// +public record BusinessChangedEvent( + string BusinessType, // 业务类型,如 "LeaveApproval" + string BusinessId, // 业务单据 ID + string Action // 动作,如 "StatusChanged" / "Created" / "Completed" +) : INotification; diff --git a/backend/F9WebAiTest.Application/Services/Notice/INotificationPusher.cs b/backend/F9WebAiTest.Application/Services/Notice/INotificationPusher.cs new file mode 100644 index 0000000..5229d19 --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Notice/INotificationPusher.cs @@ -0,0 +1,26 @@ +using F9WebAiTest.Application.Dtos.Notice; + +namespace F9WebAiTest.Application.Services.Notice; + +/// +/// 消息实时推送器(抽象)。 +/// 定义在应用层、实现在 Api 层(依赖 SignalR 的 IHubContext 与在线登记表), +/// 避免应用层反向依赖表示层,同时让"创建消息即推送"这一动作可以写在命令处理器里。 +/// +public interface INotificationPusher +{ + /// + /// 判断用户当前是否在线。 + /// 判定依据:该用户在 SignalR Hub 上是否有活动连接(由 Hub 连接/断开时登记的内存表得出)。 + /// + bool IsOnline(long userId); + + /// + /// 推送新消息给指定用户。 + /// 返回 true 表示已推送给其在线连接;返回 false 表示用户离线(未推送,消息已落库,上线后从"我的消息"拉取)。 + /// + Task PushNewMessageAsync(long userId, NotificationMessageDto message, CancellationToken ct = default); + + /// 推送未读统计,用于消息角标实时同步(用户离线时无需推送)。 + Task PushUnreadCountAsync(long userId, int unreadCount, int todoCount, CancellationToken ct = default); +} diff --git a/backend/F9WebAiTest.Application/Services/Notice/NotificationMediatR.cs b/backend/F9WebAiTest.Application/Services/Notice/NotificationMediatR.cs new file mode 100644 index 0000000..99ed643 --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Notice/NotificationMediatR.cs @@ -0,0 +1,276 @@ +using System.Text.Json; +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos; +using F9WebAiTest.Application.Dtos.Notice; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities; +using F9WebAiTest.Domain.Entities.Notice; +using MediatR; + +namespace F9WebAiTest.Application.Services.Notice; + +/// +/// 业务系统触发消息创建的命令(通用化)。 +/// 用法:await _mediator.Send(new CreateNotificationCommand { NotificationTypeCode = "LeaveApproval", ... }); +/// +public class CreateNotificationCommand : IRequest +{ + public string NotificationTypeCode { get; set; } = ""; // 消息类型编码,如 "LeaveApproval" + public string BusinessType { get; set; } = ""; // 业务类型标识 + public string BusinessId { get; set; } = ""; // 业务单据ID + public string Title { get; set; } = ""; // 标题 + public string Content { get; set; } = ""; // 内容 + public NotificationPriority Priority { get; set; } = NotificationPriority.Normal; + public string BizContext { get; set; } = ""; // 业务上下文(JSON) + public List ForceReceivers { get; set; } = new(); // 强制接收人(绕过订阅) + public string RoutePath { get; set; } = ""; // 跳转路由(可选,覆盖类型模板) + public List AssigneeIds { get; set; } = new(); // 待办处理人(Category=Todo 时生效) + public DateTime? DueTime { get; set; } // 待办截止时间 + public long CreatorId { get; set; } // 创建人ID + public string CreatorName { get; set; } = ""; // 创建人姓名 +} + +/// +/// 业务侧发送消息的简洁命令(基于消息类型枚举)。 +/// 用法(参照 CQRS 惯例): +/// var ok = await mediator.Send(new SendNotificationCommand( +/// NotificationTypeEnum.LeaveApproval, +/// BusinessId: leaveId.ToString(), +/// ForceReceivers: approverIds, +/// AssigneeIds: approverIds)); +/// if (!ok) throw new F9OperationException("请假审批消息发送失败"); +/// +public record SendNotificationCommand( + NotificationTypeEnum Type, + string BusinessId = "", + string Title = "", + string Content = "", + string BizContext = "", + NotificationPriority Priority = NotificationPriority.Normal, + List ForceReceivers = null, + List AssigneeIds = null, + DateTime? DueTime = null +) : IRequest; + +public class SendNotificationCommandHandler : IRequestHandler +{ + private readonly IMediator _mediator; + + public SendNotificationCommandHandler(IMediator mediator) => _mediator = mediator; + + public async Task Handle(SendNotificationCommand command, CancellationToken cancellationToken) + { + // 枚举名即消息类型 Code,复用底层创建命令 + var msgId = await _mediator.Send(new CreateNotificationCommand + { + NotificationTypeCode = command.Type.ToString(), + BusinessType = command.Type.ToString(), + BusinessId = command.BusinessId ?? "", + Title = command.Title ?? "", + Content = command.Content ?? "", + BizContext = command.BizContext ?? "", + Priority = command.Priority, + ForceReceivers = command.ForceReceivers ?? new List(), + AssigneeIds = command.AssigneeIds ?? new List(), + DueTime = command.DueTime + }, cancellationToken); + + return msgId > 0; + } +} + +/// +/// 创建消息(业务触发的统一入口)。 +/// 创建后**立即**确定接收人、写入接收人表并实时推送(已移除后台投递服务,无轮询、无延迟)。 +/// 接收人规则:ForceReceivers 非空 → 精确送达;否则 → 按该消息类型的订阅关系广播。 +/// 可靠性:接收人幂等(MessageId + UserId 唯一);用户离线时推送无接收者但消息已落库,上线后拉取兜底。 +/// +public class CreateNotificationCommandHandler : IRequestHandler +{ + private readonly IRepository _typeRepo; + private readonly IRepository _msgRepo; + private readonly IRepository _todoRepo; + private readonly IRepository _subRepo; + private readonly IRepository _receiverRepo; + private readonly INotificationPusher _pusher; + + public CreateNotificationCommandHandler( + IRepository typeRepo, + IRepository msgRepo, + IRepository todoRepo, + IRepository subRepo, + IRepository receiverRepo, + INotificationPusher pusher) + { + _typeRepo = typeRepo; + _msgRepo = msgRepo; + _todoRepo = todoRepo; + _subRepo = subRepo; + _receiverRepo = receiverRepo; + _pusher = pusher; + } + + public async Task Handle(CreateNotificationCommand cmd, CancellationToken cancellationToken) + { + var type = await _typeRepo.FirstOrDefaultAsync(t => t.Code == cmd.NotificationTypeCode); + if (type == null) throw new ServiceException($"消息类型「{cmd.NotificationTypeCode}」不存在,请先由管理员创建"); + if (!type.IsActive) throw new ServiceException($"消息类型「{type.Name}」已关闭订阅"); + + // 生成跳转路由:命令直接指定 > 类型模板替换业务ID + var routePath = !string.IsNullOrWhiteSpace(cmd.RoutePath) + ? cmd.RoutePath + : (!string.IsNullOrWhiteSpace(type.RouteTemplate) + ? type.RouteTemplate.Replace("{businessId}", cmd.BusinessId ?? "") + : ""); + + var msg = new NotificationMessage + { + NotificationTypeId = type.Id, + MessageTypeCode = type.Code, + BusinessType = cmd.BusinessType, + BusinessId = cmd.BusinessId, + Title = cmd.Title, + Content = cmd.Content, + Priority = cmd.Priority, + Status = NotificationStatus.Sent, // 即时送达:无后台队列,创建即投递 + BizContext = cmd.BizContext ?? "", + ForceReceivers = cmd.ForceReceivers == null || cmd.ForceReceivers.Count == 0 + ? "" + : JsonSerializer.Serialize(cmd.ForceReceivers), + CreatorId = cmd.CreatorId, + CreatorName = cmd.CreatorName, + Category = type.Category, + RoutePath = routePath + }; + + await _msgRepo.AddAsync(msg); + await _msgRepo.SaveChangesAsync(); + + // 待办消息:为每个处理人生成待办事项 + if (type.Category == NotificationCategory.Todo && cmd.AssigneeIds != null && cmd.AssigneeIds.Count > 0) + { + foreach (var assigneeId in cmd.AssigneeIds.Distinct()) + { + await _todoRepo.AddAsync(new TodoItem + { + MessageId = msg.Id, + AssigneeId = assigneeId, + TodoStatus = TodoItemStatus.Pending, + DueTime = cmd.DueTime + }); + } + await _todoRepo.SaveChangesAsync(); + } + + // 即时投递 + 实时推送 + await DeliverAsync(msg, type, cancellationToken); + + return msg.Id; + } + + /// 确定接收人 → 写接收人表 → 实时推送(推送失败不影响业务,消息已落库可拉取) + private async Task DeliverAsync(NotificationMessage msg, NotificationType type, CancellationToken ct) + { + // 1. 确定接收人(精确送达 与 订阅广播 二选一) + var receiverIds = new HashSet(); + var hasForceReceivers = false; + + if (!string.IsNullOrWhiteSpace(msg.ForceReceivers)) + { + try + { + var force = JsonSerializer.Deserialize>(msg.ForceReceivers); + if (force != null && force.Count > 0) + { + hasForceReceivers = true; + foreach (var id in force) receiverIds.Add(id); + } + } + catch { /* 忽略非法 JSON */ } + } + + if (!hasForceReceivers && type.IsActive) + { + var subs = await _subRepo.FindAsync(s => s.NotificationTypeId == msg.NotificationTypeId && s.IsActive); + foreach (var s in subs) receiverIds.Add(s.UserId); + } + + if (receiverIds.Count == 0) return; + + // 2. 写接收人(幂等:MessageId + UserId 唯一) + var added = new List(); + foreach (var userId in receiverIds) + { + var exist = await _receiverRepo.FirstOrDefaultAsync(r => r.MessageId == msg.Id && r.UserId == userId); + if (exist != null) continue; + + var receiver = new NotificationReceiver + { + MessageId = msg.Id, + UserId = userId, + ReadStatus = NotificationReadStatus.Unread, + SendStatus = NotificationSendStatus.NotSent + }; + await _receiverRepo.AddAsync(receiver); + added.Add(receiver); + } + + if (added.Count == 0) return; + await _receiverRepo.SaveChangesAsync(); + + // 3. 实时推送新消息 + 未读统计 + // 在线(Hub 有活动连接)→ 推送并标记成功; + // 离线 → 不推送、标记 IsOffline,消息已落库,用户上线后从"我的消息"拉取兜底。 + var dto = BuildDto(msg, type.Name); + foreach (var receiver in added) + { + try + { + var delivered = await _pusher.PushNewMessageAsync(receiver.UserId, dto, ct); + if (delivered) + { + receiver.SendStatus = NotificationSendStatus.Success; + receiver.SendTime = DateTime.Now; + receiver.IsOffline = false; + + var unread = await _receiverRepo.CountAsync(r => + r.UserId == receiver.UserId && r.ReadStatus == NotificationReadStatus.Unread); + await _pusher.PushUnreadCountAsync(receiver.UserId, (int)unread, 0, ct); + } + else + { + // 用户离线:不推送,等待上线拉取 + receiver.SendStatus = NotificationSendStatus.NotSent; + receiver.IsOffline = true; + } + } + catch (Exception ex) + { + // 推送异常(连接刚好断开等):标记失败与离线,消息已持久化,上线后拉取兜底 + receiver.SendStatus = NotificationSendStatus.Failed; + receiver.IsOffline = true; + receiver.ErrorMsg = ex.Message; + } + + await _receiverRepo.UpdateAsync(receiver); + } + await _receiverRepo.SaveChangesAsync(); + } + + private static NotificationMessageDto BuildDto(NotificationMessage msg, string typeName) => new() + { + Id = msg.Id, + NotificationTypeId = msg.NotificationTypeId, + MessageTypeCode = msg.MessageTypeCode, + TypeName = typeName, + Title = msg.Title, + Content = msg.Content, + Priority = (int)msg.Priority, + BusinessType = msg.BusinessType, + BusinessId = msg.BusinessId, + Category = (int)msg.Category, + RoutePath = msg.RoutePath, + CreatedTime = msg.CreatedAt, + ReadStatus = 0 + }; +} diff --git a/backend/F9WebAiTest.Application/Services/Notice/NotificationService.cs b/backend/F9WebAiTest.Application/Services/Notice/NotificationService.cs new file mode 100644 index 0000000..890daf9 --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Notice/NotificationService.cs @@ -0,0 +1,420 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos; +using F9WebAiTest.Application.Dtos.Notice; +using F9WebAiTest.Application.Services.Auth; +using F9WebAiTest.Application.Interfaces.SnowflakeID; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities; +using F9WebAiTest.Domain.Entities.Notice; +using F9WebAiTest.Domain.Enums.SYS.RBAC; +using RBAC = F9WebAiTest.Domain.Entities.SYS.RBAC; + +namespace F9WebAiTest.Application.Services.Notice; + +public interface INotificationService +{ + Task> GetSubscribeListAsync(long userId); + Task SubscribeAsync(long userId, long typeId, bool subscribe); + Task> GetMyMessagesAsync(long userId, int pageIndex, int pageSize, int? readStatus, int? category); + Task MarkReadAsync(long userId, long messageId); + Task MarkAllReadAsync(long userId); + Task GetUnreadCountAsync(long userId); + + // ---- 待办 ---- + Task> GetMyTodosAsync(long userId, int? status); + Task CompleteTodoAsync(long userId, long todoId, string handleResult); + + // ---- 管理端:消息类型维护 ---- + Task> GetTypeListAsync(); + Task CreateTypeAsync(NotificationTypeCreateDto dto, long creatorId); + Task UpdateTypeAsync(long id, NotificationTypeCreateDto dto); + Task DeleteTypeAsync(long id); +} + +public class NotificationService : INotificationService +{ + private readonly IRepository _typeRepo; + private readonly IRepository _subRepo; + private readonly IRepository _msgRepo; + private readonly IRepository _receiverRepo; + private readonly IRepository _todoRepo; + private readonly IRepository _roleMenuRepo; + private readonly IRepository _permissionRepo; + private readonly ISnowflakeIdGenerator _snowflake; + private readonly IAuthService _authService; + + public NotificationService( + IRepository typeRepo, + IRepository subRepo, + IRepository msgRepo, + IRepository receiverRepo, + IRepository todoRepo, + IRepository roleMenuRepo, + IRepository permissionRepo, + ISnowflakeIdGenerator snowflake, + IAuthService authService) + { + _typeRepo = typeRepo; + _subRepo = subRepo; + _msgRepo = msgRepo; + _receiverRepo = receiverRepo; + _todoRepo = todoRepo; + _roleMenuRepo = roleMenuRepo; + _permissionRepo = permissionRepo; + _snowflake = snowflake; + _authService = authService; + } + + public async Task> GetSubscribeListAsync(long userId) + { + var types = (await _typeRepo.FindAsync(t => t.IsActive)).OrderBy(t => t.Sort).ThenBy(t => t.Id).ToList(); + var perms = await _authService.GetUserPermissionsAsync(userId); + var subs = await _subRepo.FindAsync(s => s.UserId == userId && s.IsActive); + var subIds = subs.Select(s => s.NotificationTypeId).ToHashSet(); + + return types.Select(t => new NotificationTypeDto + { + Id = t.Id, + Code = t.Code, + Name = t.Name, + Description = t.Description, + RequiredPermission = t.RequiredPermission, + IsActive = t.IsActive, + CreatedBy = t.CreatedBy, + Sort = t.Sort, + Category = (int)t.Category, + Icon = t.Icon, + RouteTemplate = t.RouteTemplate, + Subscribed = subIds.Contains(t.Id), + CanSubscribe = string.IsNullOrEmpty(t.RequiredPermission) || perms.Contains(t.RequiredPermission) + }).ToList(); + } + + public async Task SubscribeAsync(long userId, long typeId, bool subscribe) + { + var type = await _typeRepo.GetByIdAsync(typeId); + if (type == null || !type.IsActive) throw new ServiceException("消息类型不存在或已关闭订阅"); + + // 权限校验:RequiredPermission 为空表示所有用户可订阅 + if (!string.IsNullOrEmpty(type.RequiredPermission)) + { + var perms = await _authService.GetUserPermissionsAsync(userId); + if (!perms.Contains(type.RequiredPermission)) + throw new ServiceException($"您没有订阅「{type.Name}」的权限", 403); + } + + var sub = await _subRepo.FirstOrDefaultAsync(s => s.UserId == userId && s.NotificationTypeId == typeId); + if (subscribe) + { + if (sub == null) + { + await _subRepo.AddAsync(new UserNotificationSubscription + { + UserId = userId, + NotificationTypeId = typeId, + IsActive = true, + SubscribedTime = DateTime.Now + }); + } + else + { + sub.IsActive = true; + sub.SubscribedTime = DateTime.Now; + sub.CancelTime = null; + await _subRepo.UpdateAsync(sub); + } + } + else + { + if (sub != null && sub.IsActive) + { + sub.IsActive = false; + sub.CancelTime = DateTime.Now; + await _subRepo.UpdateAsync(sub); + } + } + await _subRepo.SaveChangesAsync(); + } + + public async Task> GetMyMessagesAsync(long userId, int pageIndex, int pageSize, int? readStatus, int? category) + { + var receivers = await _receiverRepo.FindAsync(r => r.UserId == userId); + if (readStatus.HasValue) + receivers = receivers.Where(r => (int)r.ReadStatus == readStatus.Value).ToList(); + + var msgIds = receivers.Select(r => r.MessageId).Distinct().ToList(); + var msgs = await _msgRepo.FindAsync(m => msgIds.Contains(m.Id)); + if (category.HasValue) + msgs = msgs.Where(m => (int)m.Category == category.Value).ToList(); + var msgDict = msgs.ToDictionary(m => m.Id); + + var typeIds = msgs.Select(m => m.NotificationTypeId).Distinct().ToList(); + var types = await _typeRepo.FindAsync(t => typeIds.Contains(t.Id)); + var typeDict = types.ToDictionary(t => t.Id, t => t.Name); + + var items = receivers + .Where(r => msgDict.ContainsKey(r.MessageId)) + .Select(r => + { + var m = msgDict[r.MessageId]; + return new NotificationMessageDto + { + Id = m.Id, + NotificationTypeId = m.NotificationTypeId, + MessageTypeCode = m.MessageTypeCode, + TypeName = typeDict.GetValueOrDefault(m.NotificationTypeId, ""), + Title = m.Title, + Content = m.Content, + Priority = (int)m.Priority, + BusinessType = m.BusinessType, + BusinessId = m.BusinessId, + Category = (int)m.Category, + RoutePath = m.RoutePath, + CreatedTime = m.CreatedAt, + ReadStatus = (int)r.ReadStatus, + ReadTime = r.ReadTime + }; + }) + .OrderByDescending(x => x.CreatedTime) + .ToList(); + + var total = items.Count; + var paged = items.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + return new PageResult + { + Items = paged, + Total = total, + PageIndex = pageIndex, + PageSize = pageSize + }; + } + + public async Task MarkReadAsync(long userId, long messageId) + { + var receiver = await _receiverRepo.FirstOrDefaultAsync(r => r.MessageId == messageId && r.UserId == userId); + if (receiver == null) throw new ServiceException("消息不存在", 404); + receiver.ReadStatus = NotificationReadStatus.Read; + receiver.ReadTime = DateTime.Now; + await _receiverRepo.UpdateAsync(receiver); + await _receiverRepo.SaveChangesAsync(); + } + + public async Task MarkAllReadAsync(long userId) + { + var unread = await _receiverRepo.FindAsync(r => r.UserId == userId && r.ReadStatus == NotificationReadStatus.Unread); + foreach (var r in unread) + { + r.ReadStatus = NotificationReadStatus.Read; + r.ReadTime = DateTime.Now; + await _receiverRepo.UpdateAsync(r); + } + } + + public async Task GetUnreadCountAsync(long userId) + => (int)await _receiverRepo.CountAsync(r => r.UserId == userId && r.ReadStatus == NotificationReadStatus.Unread); + + // ==================== 待办 ==================== + + public async Task> GetMyTodosAsync(long userId, int? status) + { + var todos = await _todoRepo.FindAsync(t => t.AssigneeId == userId); + if (status.HasValue) + todos = todos.Where(t => (int)t.TodoStatus == status.Value).ToList(); + + var msgIds = todos.Select(t => t.MessageId).Distinct().ToList(); + var msgs = await _msgRepo.FindAsync(m => msgIds.Contains(m.Id)); + var msgDict = msgs.ToDictionary(m => m.Id); + + return todos + .Where(t => msgDict.ContainsKey(t.MessageId)) + .OrderByDescending(t => t.CreatedAt) + .Select(t => + { + var m = msgDict[t.MessageId]; + return new TodoItemDto + { + Id = t.Id, + MessageId = t.MessageId, + Title = m.Title, + Content = m.Content, + Priority = (int)m.Priority, + BusinessType = m.BusinessType, + BusinessId = m.BusinessId, + RoutePath = m.RoutePath, + TodoStatus = (int)t.TodoStatus, + DueTime = t.DueTime, + CompletedTime = t.CompletedTime, + CreatedTime = t.CreatedAt + }; + }) + .ToList(); + } + + public async Task CompleteTodoAsync(long userId, long todoId, string handleResult) + { + var todo = await _todoRepo.GetByIdAsync(todoId); + if (todo == null) throw new ServiceException("待办不存在", 404); + if (todo.AssigneeId != userId) throw new ServiceException("您不是该待办的处理人", 403); + if (todo.TodoStatus == TodoItemStatus.Completed) throw new ServiceException("该待办已处理"); + + todo.TodoStatus = TodoItemStatus.Completed; + todo.CompletedTime = DateTime.Now; + todo.HandleResult = handleResult ?? ""; + await _todoRepo.UpdateAsync(todo); + await _todoRepo.SaveChangesAsync(); + + // 完成待办后,标记对应消息已读(处理完了才从"未读提醒"中消失) + var receiver = await _receiverRepo.FirstOrDefaultAsync(r => r.MessageId == todo.MessageId && r.UserId == userId); + if (receiver != null && receiver.ReadStatus == NotificationReadStatus.Unread) + { + receiver.ReadStatus = NotificationReadStatus.Read; + receiver.ReadTime = DateTime.Now; + await _receiverRepo.UpdateAsync(receiver); + await _receiverRepo.SaveChangesAsync(); + } + } + + // ==================== 管理端:消息类型维护 ==================== + + public async Task> GetTypeListAsync() + { + var types = (await _typeRepo.GetAllAsync()).OrderBy(t => t.Sort).ThenBy(t => t.Id).ToList(); + return types.Select(t => new NotificationTypeDto + { + Id = t.Id, + Code = t.Code, + Name = t.Name, + Description = t.Description, + RequiredPermission = t.RequiredPermission, + IsActive = t.IsActive, + CreatedBy = t.CreatedBy, + Sort = t.Sort, + Category = (int)t.Category, + Icon = t.Icon, + RouteTemplate = t.RouteTemplate + }).ToList(); + } + + public async Task CreateTypeAsync(NotificationTypeCreateDto dto, long creatorId) + { + if (string.IsNullOrWhiteSpace(dto.Code)) throw new ServiceException("消息类型编码不能为空"); + if (string.IsNullOrWhiteSpace(dto.Name)) throw new ServiceException("消息类型名称不能为空"); + if (await _typeRepo.AnyAsync(t => t.Code == dto.Code)) throw new ServiceException($"编码「{dto.Code}」已存在"); + + var e = new NotificationType + { + Code = dto.Code.Trim(), + Name = dto.Name, + Description = dto.Description ?? "", + RequiredPermission = dto.RequiredPermission ?? "", + IsActive = dto.IsActive, + CreatedBy = creatorId, + Sort = dto.Sort, + Category = (NotificationCategory)dto.Category, + Icon = dto.Icon ?? "", + RouteTemplate = dto.RouteTemplate ?? "" + }; + await _typeRepo.AddAsync(e); + await _typeRepo.SaveChangesAsync(); + + // 同步订阅权限到 RBAC 权限单元(角色管理里可给角色分配) + await SyncSubscribePermissionAsync(e); + + return new NotificationTypeDto + { + Id = e.Id, Code = e.Code, Name = e.Name, Description = e.Description, + RequiredPermission = e.RequiredPermission, IsActive = e.IsActive, CreatedBy = e.CreatedBy, Sort = e.Sort, + Category = (int)e.Category, Icon = e.Icon, RouteTemplate = e.RouteTemplate + }; + } + + public async Task UpdateTypeAsync(long id, NotificationTypeCreateDto dto) + { + var e = await _typeRepo.GetByIdAsync(id); + if (e == null) throw new ServiceException("消息类型不存在", 404); + if (string.IsNullOrWhiteSpace(dto.Name)) throw new ServiceException("消息类型名称不能为空"); + + e.Name = dto.Name; + e.Description = dto.Description ?? ""; + e.RequiredPermission = dto.RequiredPermission ?? ""; + e.IsActive = dto.IsActive; + e.Sort = dto.Sort; + e.Category = (NotificationCategory)dto.Category; + e.Icon = dto.Icon ?? ""; + e.RouteTemplate = dto.RouteTemplate ?? ""; + await _typeRepo.UpdateAsync(e); + await _typeRepo.SaveChangesAsync(); + + // 同步订阅权限到 RBAC 权限单元 + await SyncSubscribePermissionAsync(e); + + return new NotificationTypeDto + { + Id = e.Id, Code = e.Code, Name = e.Name, Description = e.Description, + RequiredPermission = e.RequiredPermission, IsActive = e.IsActive, CreatedBy = e.CreatedBy, Sort = e.Sort, + Category = (int)e.Category, Icon = e.Icon, RouteTemplate = e.RouteTemplate + }; + } + + public async Task DeleteTypeAsync(long id) + { + var e = await _typeRepo.GetByIdAsync(id); + if (e == null) throw new ServiceException("消息类型不存在", 404); + + // 清理对应的订阅权限及其角色关联 + var permission = await _permissionRepo.FirstOrDefaultAsync(p => + p.ResourceType == Resource_Type_Enum.消息订阅 && p.ResourceId == e.Id); + if (permission != null) + { + var relations = await _roleMenuRepo.FindAsync(rp => rp.PermissionId == permission.Id); + if (relations.Any()) await _roleMenuRepo.HardDeleteRangeAsync(relations); + await _permissionRepo.HardDeleteAsync(permission); + await _permissionRepo.SaveChangesAsync(); + } + + await _typeRepo.DeleteAsync(e); + await _typeRepo.SaveChangesAsync(); + } + + /// + /// 将消息类型的订阅权限同步为 RBAC 的权限单元(ResourceType = 消息订阅), + /// 这样在「角色管理 → 分配权限」里就能给角色勾选该消息类型的订阅权限。 + /// + private async Task SyncSubscribePermissionAsync(NotificationType type) + { + // 无 RequiredPermission 表示所有用户可订阅,无需权限单元(清理旧权限) + if (string.IsNullOrEmpty(type.RequiredPermission)) + { + var old = await _permissionRepo.FirstOrDefaultAsync(p => + p.ResourceType == Resource_Type_Enum.消息订阅 && p.ResourceId == type.Id); + if (old != null) + { + var relations = await _roleMenuRepo.FindAsync(rp => rp.PermissionId == old.Id); + if (relations.Any()) await _roleMenuRepo.HardDeleteRangeAsync(relations); + await _permissionRepo.HardDeleteAsync(old); + } + await _permissionRepo.SaveChangesAsync(); + return; + } + + // 查找该消息类型的订阅权限单元(按 ResourceType + ResourceId 幂等) + var permission = await _permissionRepo.FirstOrDefaultAsync(p => + p.ResourceType == Resource_Type_Enum.消息订阅 && p.ResourceId == type.Id); + if (permission == null) + { + permission = new RBAC.Permission( + _snowflake.CreateId(), + Permission_Type_Enum.Visble, + Resource_Type_Enum.消息订阅, + type.Id.ToString(), + type.Id); + await _permissionRepo.AddAsync(permission); + } + + // 业务权限码与 RBAC 权限码保持一致(SubscribeAsync 用 RequiredPermission 校验) + permission.SetCode(type.RequiredPermission); + await _permissionRepo.UpdateAsync(permission); + await _permissionRepo.SaveChangesAsync(); + } +} diff --git a/backend/F9WebAiTest.Application/Services/Prod/HbjyService.cs b/backend/F9WebAiTest.Application/Services/Prod/HbjyService.cs new file mode 100644 index 0000000..1e17053 --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Prod/HbjyService.cs @@ -0,0 +1,272 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Application.Services.Notice; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities; +using F9WebAiTest.Domain.Entities.Base; +using F9WebAiTest.Domain.Entities.Notice; +using F9WebAiTest.Domain.Entities.Prod; +using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments; +using MediatR; + +namespace F9WebAiTest.Application.Services.Prod; + +public interface IHbjyService +{ + Task> GetPagedAsync(int pageIndex, int pageSize, DateTime? date, long? deptId); + Task GetEditAsync(long lsjlId); + Task SaveAsync(HbjySaveDto dto); +} + +public class HbjyService : IHbjyService +{ + private readonly IRepository _lsjlRepo; + private readonly IRepository _hbjyLsjlRepo; + private readonly IRepository _hbjyRecordRepo; + private readonly IRepository _deptRepo; + private readonly IRepository _zkRepo; + private readonly IMediator _mediator; + + public HbjyService( + IRepository lsjlRepo, + IRepository hbjyLsjlRepo, + IRepository hbjyRecordRepo, + IRepository deptRepo, + IRepository zkRepo, + IMediator mediator) + { + _lsjlRepo = lsjlRepo; + _hbjyLsjlRepo = hbjyLsjlRepo; + _hbjyRecordRepo = hbjyRecordRepo; + _deptRepo = deptRepo; + _zkRepo = zkRepo; + _mediator = mediator; + } + + public async Task> GetPagedAsync(int pageIndex, int pageSize, DateTime? date, long? deptId) + { + var lsjls = await _lsjlRepo.GetAllAsync(); + var q = lsjls.AsQueryable(); + if (date.HasValue) q = q.Where(x => x.WorkDate.Date == date.Value.Date); + if (deptId.HasValue) q = q.Where(x => x.DeptId == deptId.Value); + + var depts = await _deptRepo.GetAllAsync(); + var deptDict = depts.ToDictionary(d => d.Id, d => d.Name); + var zks = await _zkRepo.GetAllAsync(); + var zkDict = zks.ToDictionary(z => z.Id, z => z.ZhuangKouName); + + var sums = (await _hbjyLsjlRepo.GetAllAsync()).ToDictionary(h => h.LsjlId); + + var total = q.LongCount(); + var items = q.OrderByDescending(x => x.WorkDate).ThenByDescending(x => x.Id) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + return new PageResult + { + Items = items.Select(x => + { + sums.TryGetValue(x.Id, out var sum); + return new HbjyListItemDto + { + LsjlId = x.Id, + WorkDate = x.WorkDate, + DeptId = x.DeptId, + DeptName = deptDict.GetValueOrDefault(x.DeptId, ""), + Jizu = x.Jizu, + Huishu = x.Huishu, + Guige = x.Guige, + ZhuangKouName = zkDict.GetValueOrDefault(x.GyzkId, ""), + Yanse = x.Yanse, + Isbanche = x.Isbanche, + IsbancheName = x.Isbanche ? "半车" : "整车", + LuosiTime = x.LuosiTime, + Chetaishu = x.Chetaishu, + ChetaiStart = x.ChetaiStart, + ChetaiEnd = x.ChetaiEnd, + YingluPianshu = x.Chetaishu * 20, + IsChecked = sum != null, + Pianshu = sum?.Pianshu ?? 0, + Qingjie = sum?.Qingjie ?? 0, + Jiejing = sum?.Jiejing ?? 0, + Dengji = sum?.Dengji ?? "" + }; + }).ToList(), + Total = total, PageIndex = pageIndex, PageSize = pageSize + }; + } + + public async Task GetEditAsync(long lsjlId) + { + var lsjl = await _lsjlRepo.GetByIdAsync(lsjlId); + if (lsjl == null) throw new ServiceException("落丝记录不存在", 404); + + var dept = await _deptRepo.GetByIdAsync(lsjl.DeptId); + var zk = await _zkRepo.GetByIdAsync(lsjl.GyzkId); + + var sum = await _hbjyLsjlRepo.FirstOrDefaultAsync(h => h.LsjlId == lsjlId); + var records = await _hbjyRecordRepo.FindAsync(h => h.LsjlId == lsjlId); + + List items; + if (records.Count > 0) + { + items = records.OrderBy(r => r.Chetaihao).Select(ToDto).ToList(); + } + else + { + items = new List(); + for (int i = lsjl.ChetaiStart; i <= lsjl.ChetaiEnd; i++) + { + items.Add(new HbjyRecordDto { Chetaihao = i }); + } + } + + return new HbjyEditDto + { + LsjlId = lsjl.Id, + DeptName = dept?.Name ?? "", + Jizu = lsjl.Jizu, + Huishu = lsjl.Huishu, + ZhuangKouName = zk?.ZhuangKouName ?? "", + Yanse = lsjl.Yanse, + Isbanche = lsjl.Isbanche, + IsbancheName = lsjl.Isbanche ? "半车" : "整车", + Guige = lsjl.Guige, + ChetaiStart = lsjl.ChetaiStart, + ChetaiEnd = lsjl.ChetaiEnd, + Chetaishu = lsjl.Chetaishu, + YingluPianshu = lsjl.Chetaishu * 20, + Dengji = sum?.Dengji ?? "", + Records = items + }; + } + + public async Task SaveAsync(HbjySaveDto dto) + { + if (dto.LsjlId == 0) throw new ServiceException("落丝记录无效"); + var lsjl = await _lsjlRepo.GetByIdAsync(dto.LsjlId); + if (lsjl == null) throw new ServiceException("落丝记录不存在", 404); + + if (dto.Records == null || dto.Records.Count == 0) throw new ServiceException("请先录入检验数据"); + + var invalid = dto.Records.Where(i => i.Chetaihao < lsjl.ChetaiStart || i.Chetaihao > lsjl.ChetaiEnd).ToList(); + if (invalid.Count > 0) throw new ServiceException($"车台号超出范围 [{lsjl.ChetaiStart}-{lsjl.ChetaiEnd}]"); + + // 计算每车台片数 + foreach (var item in dto.Records) + { + item.Pianshu = CalcPianshu(item); + } + + var totalPianshu = dto.Records.Sum(x => x.Pianshu); + if (totalPianshu == 0) throw new ServiceException("请至少录入一个洁净等级片数"); + + // 物理删除旧明细 + var oldRecords = await _hbjyRecordRepo.FindAsync(h => h.LsjlId == dto.LsjlId); + await _hbjyRecordRepo.HardDeleteRangeAsync(oldRecords); + + foreach (var item in dto.Records) + { + await _hbjyRecordRepo.AddAsync(new HbjyRecord + { + LsjlId = dto.LsjlId, + Chetaihao = item.Chetaihao, + YdYidu = item.YdYidu, YdErdu = item.YdErdu, YdSandu = item.YdSandu, + Jj100 = item.Jj100, Jj95 = item.Jj95, Jj90 = item.Jj90, Jj85 = item.Jj85, + Jj80 = item.Jj80, Jj75 = item.Jj75, Jj70 = item.Jj70, Jj65 = item.Jj65, Jj60 = item.Jj60, + QjZycdTdc = item.QjZycdTdc, + QjCycdFs = item.QjCycdFs, QjCycdDc = item.QjCycdDc, QjCycdNfc = item.QjCycdNfc, + QjCycdDcj = item.QjCycdDcj, QjCycdZlx = item.QjCycdZlx, + QjPtcdXc = item.QjPtcdXc, QjPtcdCj = item.QjPtcdCj, QjPtcdLx = item.QjPtcdLx, QjPtcdHjls = item.QjPtcdHjls, + Pianshu = item.Pianshu + }); + } + + // 汇总 + var sum = CalcSummary(dto.Records); + var hbjyLsjl = await _hbjyLsjlRepo.FirstOrDefaultAsync(h => h.LsjlId == dto.LsjlId); + if (hbjyLsjl == null) + { + hbjyLsjl = new HbjyLsjl { LsjlId = dto.LsjlId }; + await _hbjyLsjlRepo.AddAsync(hbjyLsjl); + } + hbjyLsjl.Pianshu = sum.Pianshu; + hbjyLsjl.Qingjie = sum.Qingjie; + hbjyLsjl.Jiejing = sum.Jiejing; + hbjyLsjl.YdErdu = sum.YdErdu; + hbjyLsjl.YdSandu = sum.YdSandu; + hbjyLsjl.Dengji = dto.Dengji ?? ""; + + await _hbjyLsjlRepo.UpdateAsync(hbjyLsjl); + + // 业务系统触发消息:发现特大糙时,通过 MediatR 创建"发现特大糙"消息,由 BackgroundService 投递 + var tdcCount = dto.Records.Sum(x => x.QjZycdTdc); + if (tdcCount > 0) + { + await _mediator.Send(new CreateNotificationCommand + { + NotificationTypeCode = "TdcFound", + BusinessType = "Hbjy", + BusinessId = dto.LsjlId.ToString(), + Title = $"发现特大糙:{lsjl.Guige} 规格", + Content = $"落丝记录(机组 {lsjl.Jizu}、回数 {lsjl.Huishu})黑板检验发现 {tdcCount} 个特大糙,请关注。", + Priority = NotificationPriority.Urgent, + CreatorId = 0, + CreatorName = "系统" + }); + } + } + + // ==================== 计算逻辑 ==================== + + private static HbjyRecordDto ToDto(HbjyRecord r) => new() + { + Chetaihao = r.Chetaihao, + YdYidu = r.YdYidu, YdErdu = r.YdErdu, YdSandu = r.YdSandu, + Jj100 = r.Jj100, Jj95 = r.Jj95, Jj90 = r.Jj90, Jj85 = r.Jj85, + Jj80 = r.Jj80, Jj75 = r.Jj75, Jj70 = r.Jj70, Jj65 = r.Jj65, Jj60 = r.Jj60, + QjZycdTdc = r.QjZycdTdc, + QjCycdFs = r.QjCycdFs, QjCycdDc = r.QjCycdDc, QjCycdNfc = r.QjCycdNfc, + QjCycdDcj = r.QjCycdDcj, QjCycdZlx = r.QjCycdZlx, + QjPtcdXc = r.QjPtcdXc, QjPtcdCj = r.QjPtcdCj, QjPtcdLx = r.QjPtcdLx, QjPtcdHjls = r.QjPtcdHjls, + Pianshu = r.Pianshu + }; + + // 片数 = 洁净等级各列之和 + private static int CalcPianshu(HbjyRecordDto item) + => item.Jj100 + item.Jj95 + item.Jj90 + item.Jj85 + item.Jj80 + item.Jj75 + item.Jj70 + item.Jj65 + item.Jj60; + + // 清洁扣分(特大类1分/个,大类0.4分/个,小类0.1分/个) + private static decimal CalcQingjieDeduction(HbjyRecordDto item) + { + decimal teda = item.QjZycdTdc * 1.0M; + decimal dalei = (item.QjCycdFs + item.QjCycdDc + item.QjCycdNfc + item.QjCycdDcj + item.QjCycdZlx) * 0.4M; + decimal xiaolei = (item.QjPtcdXc + item.QjPtcdCj + item.QjPtcdLx + item.QjPtcdHjls) * 0.1M; + return teda + dalei + xiaolei; + } + + private static HbjyLsjl CalcSummary(List items) + { + int pianshu = items.Sum(x => x.Pianshu); + + // 清洁分 = (片数 - 总扣分) * 100 / 片数 + decimal totalDeduction = items.Sum(CalcQingjieDeduction); + double qingjie = pianshu == 0 ? 0 : Math.Round((double)((pianshu - totalDeduction) * 100 / pianshu), 2, MidpointRounding.AwayFromZero); + + // 洁净分 = (片数*100 - 扣分总和*5) / 片数 + int jj95 = items.Sum(x => x.Jj95), jj90 = items.Sum(x => x.Jj90), jj85 = items.Sum(x => x.Jj85); + int jj80 = items.Sum(x => x.Jj80), jj75 = items.Sum(x => x.Jj75), jj70 = items.Sum(x => x.Jj70); + int jj65 = items.Sum(x => x.Jj65), jj60 = items.Sum(x => x.Jj60); + decimal totalJiejingDeduction = 1 * jj95 + 2 * jj90 + 3 * jj85 + 4 * jj80 + 5 * jj75 + 6 * jj70 + 7 * jj65 + 8 * jj60; + double jiejing = pianshu == 0 ? 0 : Math.Round((double)((pianshu * 100 - totalJiejingDeduction * 5) / (decimal)pianshu), 2, MidpointRounding.AwayFromZero); + + return new HbjyLsjl + { + Pianshu = pianshu, + Qingjie = qingjie, + Jiejing = jiejing, + YdErdu = items.Sum(x => x.YdErdu), + YdSandu = items.Sum(x => x.YdSandu) + }; + } +} diff --git a/backend/F9WebAiTest.Application/Services/Prod/LsjlService.cs b/backend/F9WebAiTest.Application/Services/Prod/LsjlService.cs new file mode 100644 index 0000000..c34cf1e --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Prod/LsjlService.cs @@ -0,0 +1,129 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities; +using F9WebAiTest.Domain.Entities.Base; +using F9WebAiTest.Domain.Entities.Prod; +using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments; + +namespace F9WebAiTest.Application.Services.Prod; + +public interface ILsjlService +{ + Task> GetPagedAsync(int pageIndex, int pageSize, DateTime? date, long? deptId, string keyword); + Task CreateAsync(LsjlCreateDto dto); + Task UpdateAsync(long id, LsjlCreateDto dto); + Task DeleteAsync(long id); +} + +public class LsjlService : ILsjlService +{ + private readonly IRepository _repo; + private readonly IRepository _deptRepo; + private readonly IRepository _zkRepo; + + public LsjlService(IRepository repo, IRepository deptRepo, IRepository zkRepo) + { + _repo = repo; + _deptRepo = deptRepo; + _zkRepo = zkRepo; + } + + public async Task> GetPagedAsync(int pageIndex, int pageSize, DateTime? date, long? deptId, string keyword) + { + var all = await _repo.GetAllAsync(); + var q = all.AsQueryable(); + if (date.HasValue) + q = q.Where(x => x.WorkDate.Date == date.Value.Date); + if (deptId.HasValue) + q = q.Where(x => x.DeptId == deptId.Value); + + var depts = await _deptRepo.GetAllAsync(); + var deptDict = depts.ToDictionary(d => d.Id, d => d.Name); + var zks = await _zkRepo.GetAllAsync(); + var zkDict = zks.ToDictionary(z => z.Id, z => z.ZhuangKouName); + + if (!string.IsNullOrWhiteSpace(keyword)) + { + var kwDeptIds = depts.Where(d => d.Name.Contains(keyword)).Select(d => d.Id).ToHashSet(); + var kwZkIds = zks.Where(z => z.ZhuangKouName.Contains(keyword)).Select(z => z.Id).ToHashSet(); + q = q.Where(x => kwDeptIds.Contains(x.DeptId) || kwZkIds.Contains(x.GyzkId) || x.Guige.Contains(keyword)); + } + + var total = q.LongCount(); + var items = q.OrderByDescending(x => x.WorkDate).ThenByDescending(x => x.Id) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + return new PageResult + { + Items = items.Select(x => ToDto(x, deptDict, zkDict)).ToList(), + Total = total, PageIndex = pageIndex, PageSize = pageSize + }; + } + + public async Task CreateAsync(LsjlCreateDto dto) + { + if (dto.DeptId == 0) throw new ServiceException("请选择车间"); + if (dto.GyzkId == 0) throw new ServiceException("请选择庄口"); + var e = Map(new LsjlRecord(), dto); + await _repo.AddAsync(e); + await _repo.SaveChangesAsync(); + return await ToDto(e); + } + + public async Task UpdateAsync(long id, LsjlCreateDto dto) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("记录不存在", 404); + Map(e, dto); + await _repo.UpdateAsync(e); + await _repo.SaveChangesAsync(); + return await ToDto(e); + } + + public async Task DeleteAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("记录不存在", 404); + await _repo.DeleteAsync(e); + await _repo.SaveChangesAsync(); + } + + private static LsjlRecord Map(LsjlRecord e, LsjlCreateDto d) + { + e.WorkDate = d.WorkDate; e.DeptId = d.DeptId; e.GyzkId = d.GyzkId; e.Jizu = d.Jizu; + e.LuosiTime = d.LuosiTime; e.Yanse = d.Yanse; e.Huishu = d.Huishu; + e.ChetaiStart = d.ChetaiStart; e.ChetaiEnd = d.ChetaiEnd; e.Chetaishu = d.Chetaishu; + e.Isbanche = d.Isbanche; e.Hours = d.Hours; e.Guige = d.Guige; e.Chesu = d.Chesu; + return e; + } + + private async Task ToDto(LsjlRecord e) + { + var dept = await _deptRepo.GetByIdAsync(e.DeptId); + var zk = await _zkRepo.GetByIdAsync(e.GyzkId); + return new LsjlDto + { + Id = e.Id, WorkDate = e.WorkDate, DeptId = e.DeptId, DeptName = dept?.Name ?? "", + GyzkId = e.GyzkId, ZhuangKouName = zk?.ZhuangKouName ?? "", Jizu = e.Jizu, + LuosiTime = e.LuosiTime, Yanse = e.Yanse, Huishu = e.Huishu, + ChetaiStart = e.ChetaiStart, ChetaiEnd = e.ChetaiEnd, Chetaishu = e.Chetaishu, + Isbanche = e.Isbanche, IsbancheName = e.Isbanche ? "半车" : "整车", + Hours = e.Hours, Guige = e.Guige, Chesu = e.Chesu + }; + } + + private static LsjlDto ToDto(LsjlRecord e, Dictionary deptDict, Dictionary zkDict) + { + return new LsjlDto + { + Id = e.Id, WorkDate = e.WorkDate, DeptId = e.DeptId, DeptName = deptDict.GetValueOrDefault(e.DeptId, ""), + GyzkId = e.GyzkId, ZhuangKouName = zkDict.GetValueOrDefault(e.GyzkId, ""), Jizu = e.Jizu, + LuosiTime = e.LuosiTime, Yanse = e.Yanse, Huishu = e.Huishu, + ChetaiStart = e.ChetaiStart, ChetaiEnd = e.ChetaiEnd, Chetaishu = e.Chetaishu, + Isbanche = e.Isbanche, IsbancheName = e.Isbanche ? "半车" : "整车", + Hours = e.Hours, Guige = e.Guige, Chesu = e.Chesu + }; + } +} diff --git a/backend/F9WebAiTest.Application/Services/Prod/XdjyService.cs b/backend/F9WebAiTest.Application/Services/Prod/XdjyService.cs new file mode 100644 index 0000000..2623097 --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Prod/XdjyService.cs @@ -0,0 +1,344 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos; +using F9WebAiTest.Application.Dtos.Base; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Application.Services.Notice; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities; +using F9WebAiTest.Domain.Entities.Base; +using F9WebAiTest.Domain.Entities.Notice; +using F9WebAiTest.Domain.Entities.Prod; +using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments; +using MediatR; + +namespace F9WebAiTest.Application.Services.Prod; + +public interface IXdjyService +{ + Task> GetPagedAsync(int pageIndex, int pageSize, DateTime? date, long? deptId); + Task GetEditAsync(long lsjlId); + Task SaveAsync(XdjySaveDto dto); +} + +public class XdjyService : IXdjyService +{ + private readonly IRepository _lsjlRepo; + private readonly IRepository _xdjyLsjlRepo; + private readonly IRepository _xdjyRecordRepo; + private readonly IRepository _guigeRepo; + private readonly IRepository _deptRepo; + private readonly IRepository _zkRepo; + private readonly IMediator _mediator; + + public XdjyService( + IRepository lsjlRepo, + IRepository xdjyLsjlRepo, + IRepository xdjyRecordRepo, + IRepository guigeRepo, + IRepository deptRepo, + IRepository zkRepo, + IMediator mediator) + { + _lsjlRepo = lsjlRepo; + _xdjyLsjlRepo = xdjyLsjlRepo; + _xdjyRecordRepo = xdjyRecordRepo; + _guigeRepo = guigeRepo; + _deptRepo = deptRepo; + _zkRepo = zkRepo; + _mediator = mediator; + } + + public async Task> GetPagedAsync(int pageIndex, int pageSize, DateTime? date, long? deptId) + { + var lsjls = await _lsjlRepo.GetAllAsync(); + var q = lsjls.AsQueryable(); + if (date.HasValue) q = q.Where(x => x.WorkDate.Date == date.Value.Date); + if (deptId.HasValue) q = q.Where(x => x.DeptId == deptId.Value); + + var depts = await _deptRepo.GetAllAsync(); + var deptDict = depts.ToDictionary(d => d.Id, d => d.Name); + var zks = await _zkRepo.GetAllAsync(); + var zkDict = zks.ToDictionary(z => z.Id, z => z.ZhuangKouName); + + var sums = (await _xdjyLsjlRepo.GetAllAsync()).ToDictionary(x => x.LsjlId); + + var total = q.LongCount(); + var items = q.OrderByDescending(x => x.WorkDate).ThenByDescending(x => x.Id) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + return new PageResult + { + Items = items.Select(x => + { + sums.TryGetValue(x.Id, out var sum); + var yingcheng = x.Chetaishu * (x.Isbanche ? 2 : 4); + return new XdjyListItemDto + { + LsjlId = x.Id, + WorkDate = x.WorkDate, + DeptId = x.DeptId, + DeptName = deptDict.GetValueOrDefault(x.DeptId, ""), + Jizu = x.Jizu, + Huishu = x.Huishu, + Guige = x.Guige, + ZhuangKouName = zkDict.GetValueOrDefault(x.GyzkId, ""), + Yanse = x.Yanse, + Isbanche = x.Isbanche, + IsbancheName = x.Isbanche ? "半车" : "整车", + Chetaishu = x.Chetaishu, + ChetaiStart = x.ChetaiStart, + ChetaiEnd = x.ChetaiEnd, + LuosiTime = x.LuosiTime, + Yingchengnum = yingcheng, + IsChecked = sum != null, + Jiaoshu = sum?.Jiaoshu ?? 0, + Avgxiandu = sum?.Avgxiandu ?? 0, + Junfangcha = sum?.Junfangcha ?? 0, + Zongcha = sum?.Zongcha ?? 0, + FanxianLv = sum?.FanxianLv ?? 0, + FanweiNum = sum?.FanweiNum ?? 0, + XiaoyeNum = sum?.XiaoyeNum ?? 0, + DayeNum = sum?.DayeNum ?? 0, + TeyeNum = sum?.TeyeNum ?? 0, + CenterNum = sum?.CenterNum ?? 0, + Dengji = sum?.Dengji ?? "" + }; + }).ToList(), + Total = total, PageIndex = pageIndex, PageSize = pageSize + }; + } + + public async Task GetEditAsync(long lsjlId) + { + var lsjl = await _lsjlRepo.GetByIdAsync(lsjlId); + if (lsjl == null) throw new ServiceException("落丝记录不存在", 404); + + var dept = await _deptRepo.GetByIdAsync(lsjl.DeptId); + var zk = await _zkRepo.GetByIdAsync(lsjl.GyzkId); + var gg = await _guigeRepo.FirstOrDefaultAsync(g => g.Guige == lsjl.Guige); + if (gg == null) throw new ServiceException($"未找到规格「{lsjl.Guige}」的生丝规格配置,请先维护生丝规格"); + + var sum = await _xdjyLsjlRepo.FirstOrDefaultAsync(x => x.LsjlId == lsjlId); + var records = await _xdjyRecordRepo.FindAsync(x => x.LsjlId == lsjlId); + + List items; + if (records.Count > 0) + { + // 已检验:加载已有明细 + items = records.OrderBy(r => r.Chetaihao).Select(ToDto).ToList(); + } + else + { + // 首次检验:按车台范围初始化空明细 + items = new List(); + for (int i = lsjl.ChetaiStart; i <= lsjl.ChetaiEnd; i++) + { + items.Add(new XdjyRecordDto { Chetaihao = i }); + } + } + + return new XdjyEditDto + { + LsjlId = lsjl.Id, + DeptName = dept?.Name ?? "", + Jizu = lsjl.Jizu, + Huishu = lsjl.Huishu, + ZhuangKouName = zk?.ZhuangKouName ?? "", + Yanse = lsjl.Yanse, + Isbanche = lsjl.Isbanche, + IsbancheName = lsjl.Isbanche ? "半车" : "整车", + Guige = lsjl.Guige, + ChetaiStart = lsjl.ChetaiStart, + ChetaiEnd = lsjl.ChetaiEnd, + Chetaishu = lsjl.Chetaishu, + Yingchengnum = lsjl.Chetaishu * (lsjl.Isbanche ? 2 : 4), + Dengji = sum?.Dengji ?? "", + GuigeConfig = new ShengsiGuigeDto + { + Id = gg.Id, Guige = gg.Guige, + CenterrangeLolimit = gg.CenterrangeLolimit, CenterrangeUplimit = gg.CenterrangeUplimit, + CenterXiandu = gg.CenterXiandu, + XiaoyeLolimit = gg.XiaoyeLolimit, XiaoyeUplimit = gg.XiaoyeUplimit, + DayeLolimit = gg.DayeLolimit, DayeUplimit = gg.DayeUplimit, + TeyeLolimit = gg.TeyeLolimit, TeyeUplimit = gg.TeyeUplimit + }, + Xdjy = items + }; + } + + public async Task SaveAsync(XdjySaveDto dto) + { + if (dto.LsjlId == 0) throw new ServiceException("落丝记录无效"); + var lsjl = await _lsjlRepo.GetByIdAsync(dto.LsjlId); + if (lsjl == null) throw new ServiceException("落丝记录不存在", 404); + + var gg = await _guigeRepo.FirstOrDefaultAsync(g => g.Guige == lsjl.Guige); + if (gg == null) throw new ServiceException($"未找到规格「{lsjl.Guige}」的生丝规格配置"); + + if (dto.Items == null || dto.Items.Count == 0) throw new ServiceException("请先录入纤度数据"); + + // 校验车台号在范围内 + var invalid = dto.Items.Where(i => i.Chetaihao < lsjl.ChetaiStart || i.Chetaihao > lsjl.ChetaiEnd).ToList(); + if (invalid.Count > 0) throw new ServiceException($"车台号超出范围 [{lsjl.ChetaiStart}-{lsjl.ChetaiEnd}]"); + + // 计算每个车台指标 + var calcItems = dto.Items.Select(i => + { + var r = new XdjyRecordDto { Chetaihao = i.Chetaihao, Xd1 = i.Xd1, Xd2 = i.Xd2, Xd3 = i.Xd3, Xd4 = i.Xd4 }; + CalcChetai(r, gg); + return r; + }).ToList(); + + var jiaoshu = calcItems.Sum(x => x.XdNum); + if (jiaoshu == 0) throw new ServiceException("请至少录入一个纤度值"); + + // 物理删除旧明细(避免软删除残留导致唯一索引冲突) + var oldRecords = await _xdjyRecordRepo.FindAsync(x => x.LsjlId == dto.LsjlId); + await _xdjyRecordRepo.HardDeleteRangeAsync(oldRecords); + + foreach (var item in calcItems) + { + await _xdjyRecordRepo.AddAsync(new XdjyRecord + { + LsjlId = dto.LsjlId, + Chetaihao = item.Chetaihao, + Xd1 = item.Xd1, Xd2 = item.Xd2, Xd3 = item.Xd3, Xd4 = item.Xd4, + XdNum = item.XdNum, XdAvg = item.XdAvg, + FanweiNum = item.FanweiNum, XiaoyeNum = item.XiaoyeNum, + DayeNum = item.DayeNum, TeyeNum = item.TeyeNum, CenterNum = item.CenterNum + }); + } + + // 汇总指标 + var sum = CalcSummary(calcItems, gg); + var xdjyLsjl = await _xdjyLsjlRepo.FirstOrDefaultAsync(x => x.LsjlId == dto.LsjlId); + if (xdjyLsjl == null) + { + xdjyLsjl = new XdjyLsjl { LsjlId = dto.LsjlId }; + await _xdjyLsjlRepo.AddAsync(xdjyLsjl); + } + xdjyLsjl.Jiaoshu = sum.Jiaoshu; + xdjyLsjl.Avgxiandu = sum.Avgxiandu; + xdjyLsjl.Junfangcha = sum.Junfangcha; + xdjyLsjl.Zongcha = sum.Zongcha; + xdjyLsjl.FanxianLv = sum.FanxianLv; + xdjyLsjl.FanweiNum = sum.FanweiNum; + xdjyLsjl.XiaoyeNum = sum.XiaoyeNum; + xdjyLsjl.DayeNum = sum.DayeNum; + xdjyLsjl.TeyeNum = sum.TeyeNum; + xdjyLsjl.CenterNum = sum.CenterNum; + xdjyLsjl.Dengji = dto.Dengji ?? ""; + + await _xdjyLsjlRepo.UpdateAsync(xdjyLsjl); + + // 业务系统触发消息:发现特野时,通过 MediatR 创建"发现特野"消息,由 BackgroundService 投递 + if (sum.TeyeNum > 0) + { + await _mediator.Send(new CreateNotificationCommand + { + NotificationTypeCode = "TeyeFound", + BusinessType = "Xdjy", + BusinessId = dto.LsjlId.ToString(), + Title = $"发现特野:{lsjl.Guige} 规格", + Content = $"落丝记录(机组 {lsjl.Jizu}、回数 {lsjl.Huishu})纤度检验发现 {sum.TeyeNum} 个特野值,请关注。", + Priority = NotificationPriority.Urgent, + CreatorId = 0, + CreatorName = "系统" + }); + } + } + + // ==================== 计算逻辑(与 WinForms 版保持一致) ==================== + + private static XdjyRecordDto ToDto(XdjyRecord r) => new() + { + Chetaihao = r.Chetaihao, + Xd1 = r.Xd1, Xd2 = r.Xd2, Xd3 = r.Xd3, Xd4 = r.Xd4, + XdNum = r.XdNum, XdAvg = r.XdAvg, + FanweiNum = r.FanweiNum, XiaoyeNum = r.XiaoyeNum, + DayeNum = r.DayeNum, TeyeNum = r.TeyeNum, CenterNum = r.CenterNum + }; + + private static void CalcChetai(XdjyRecordDto item, ShengsiGuige gg) + { + item.XdNum = (item.Xd1 > 0 ? 1 : 0) + (item.Xd2 > 0 ? 1 : 0) + (item.Xd3 > 0 ? 1 : 0) + (item.Xd4 > 0 ? 1 : 0); + if (item.XdNum > 0) + { + item.XdAvg = Math.Round((item.Xd1 + item.Xd2 + item.Xd3 + item.Xd4) / item.XdNum, 2, MidpointRounding.AwayFromZero); + item.FanweiNum = Fanwei(item.Xd1, gg) + Fanwei(item.Xd2, gg) + Fanwei(item.Xd3, gg) + Fanwei(item.Xd4, gg); + item.XiaoyeNum = Xiaoye(item.Xd1, gg) + Xiaoye(item.Xd2, gg) + Xiaoye(item.Xd3, gg) + Xiaoye(item.Xd4, gg); + item.DayeNum = Daye(item.Xd1, gg) + Daye(item.Xd2, gg) + Daye(item.Xd3, gg) + Daye(item.Xd4, gg); + item.TeyeNum = Teye(item.Xd1, gg) + Teye(item.Xd2, gg) + Teye(item.Xd3, gg) + Teye(item.Xd4, gg); + item.CenterNum = Center(item.Xd1, gg) + Center(item.Xd2, gg) + Center(item.Xd3, gg) + Center(item.Xd4, gg); + } + else + { + item.XdAvg = 0; item.FanweiNum = 0; item.XiaoyeNum = 0; item.DayeNum = 0; item.TeyeNum = 0; item.CenterNum = 0; + } + } + + private static XdjyLsjl CalcSummary(List items, ShengsiGuige gg) + { + var jiaoshu = items.Sum(x => x.XdNum); + var avgXiandu = jiaoshu == 0 ? 0 : items.Sum(x => x.Xd1 + x.Xd2 + x.Xd3 + x.Xd4) / jiaoshu; + + // 均方差:sqrt(Σ(xi-avg)² / n) + double junfangcha = 0; + if (jiaoshu > 0) + { + double sumDiff = 0; + foreach (var it in items) + { + foreach (var xd in new[] { it.Xd1, it.Xd2, it.Xd3, it.Xd4 }) + { + if (xd > 0) { var d = xd - avgXiandu; sumDiff += d * d; } + } + } + junfangcha = Math.Round(Math.Sqrt(Math.Round(sumDiff / jiaoshu, 2)), 2, MidpointRounding.AwayFromZero); + } + + // 总差:抽样绞数 = ceil(n*0.02),最细/最粗各取,与总平均比较取最大差值 + double zongcha = 0; + if (jiaoshu > 0) + { + var xds = new List(); + foreach (var it in items) + { + if (it.Xd1 > 0) xds.Add(it.Xd1); + if (it.Xd2 > 0) xds.Add(it.Xd2); + if (it.Xd3 > 0) xds.Add(it.Xd3); + if (it.Xd4 > 0) xds.Add(it.Xd4); + } + xds.Sort(); + var chouyang = Math.Max(1, (int)Math.Ceiling(xds.Count * 0.02)); + var minAvg = xds.Take(chouyang).Average(); + var maxAvg = xds.Skip(xds.Count - chouyang).Take(chouyang).Average(); + var avgR = Math.Round(avgXiandu, 2, MidpointRounding.AwayFromZero); + zongcha = Math.Round(Math.Max(Math.Abs(minAvg - avgR), Math.Abs(maxAvg - avgR)), 2, MidpointRounding.AwayFromZero); + } + + var fanweiNum = items.Sum(x => x.FanweiNum); + var fanxianLv = jiaoshu == 0 ? 0 : Math.Round((double)fanweiNum * 100 / jiaoshu, 0, MidpointRounding.AwayFromZero); + + return new XdjyLsjl + { + Jiaoshu = jiaoshu, + Avgxiandu = Math.Round(avgXiandu, 2, MidpointRounding.AwayFromZero), + Junfangcha = junfangcha, + Zongcha = zongcha, + FanxianLv = fanxianLv, + FanweiNum = fanweiNum, + XiaoyeNum = items.Sum(x => x.XiaoyeNum), + DayeNum = items.Sum(x => x.DayeNum), + TeyeNum = items.Sum(x => x.TeyeNum), + CenterNum = items.Sum(x => x.CenterNum) + }; + } + + // ---- 判定(纤度 <= 0 视为未称) ---- + private static int Fanwei(double xd, ShengsiGuige gg) => xd > 0 && gg.CenterrangeLolimit <= xd && gg.CenterrangeUplimit >= xd ? 1 : 0; + private static int Xiaoye(double xd, ShengsiGuige gg) => xd > 0 && ((gg.XiaoyeLolimit >= xd && xd > gg.DayeLolimit) || (gg.XiaoyeUplimit <= xd && xd < gg.DayeUplimit)) ? 1 : 0; + private static int Daye(double xd, ShengsiGuige gg) => xd > 0 && ((gg.DayeLolimit >= xd && xd > gg.TeyeLolimit) || (gg.DayeUplimit <= xd && xd < gg.TeyeUplimit)) ? 1 : 0; + private static int Teye(double xd, ShengsiGuige gg) => xd > 0 && (gg.TeyeLolimit >= xd || gg.TeyeUplimit <= xd) ? 1 : 0; + private static int Center(double xd, ShengsiGuige gg) => xd > 0 && gg.CenterXiandu == xd ? 1 : 0; +} diff --git a/backend/F9WebAiTest.Domain/Common/BaseEntity.cs b/backend/F9WebAiTest.Domain/Common/BaseEntity.cs new file mode 100644 index 0000000..f45a2c4 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Common/BaseEntity.cs @@ -0,0 +1,13 @@ +using FreeSql.DataAnnotations; + +namespace F9WebAiTest.Domain.Common; + +/// 所有旧实体的基类(自增主键 + 软删除) +public abstract class BaseEntity : IEntity +{ + [Column(IsPrimary = true, IsIdentity = true)] + public long Id { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.Now; + public DateTime? UpdatedAt { get; set; } + public bool IsDeleted { get; set; } +} diff --git a/backend/F9WebAiTest.Domain/Common/IEntity.cs b/backend/F9WebAiTest.Domain/Common/IEntity.cs new file mode 100644 index 0000000..a5172e1 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Common/IEntity.cs @@ -0,0 +1,10 @@ +namespace F9WebAiTest.Domain.Common; + +/// 实体公共字段接口(Id/CreatedAt/UpdatedAt/IsDeleted),用于统一新旧两套实体基类 +public interface IEntity +{ + long Id { get; set; } + DateTime CreatedAt { get; set; } + DateTime? UpdatedAt { get; set; } + bool IsDeleted { get; set; } +} diff --git a/backend/F9WebAiTest.Domain/Common/IRepository.cs b/backend/F9WebAiTest.Domain/Common/IRepository.cs new file mode 100644 index 0000000..6ff9e38 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Common/IRepository.cs @@ -0,0 +1,23 @@ +using System.Linq.Expressions; + +namespace F9WebAiTest.Domain.Common; + +/// 通用仓储接口 +public interface IRepository where T : IEntity +{ + Task GetByIdAsync(long id); + Task> GetAllAsync(); + Task> FindAsync(Expression> predicate); + Task FirstOrDefaultAsync(Expression> predicate); + Task AnyAsync(Expression> predicate); + Task CountAsync(Expression> predicate); + Task AddAsync(T entity); + Task AddRangeAsync(IEnumerable entities); + Task UpdateAsync(T entity); + Task DeleteAsync(T entity); + Task DeleteRangeAsync(IEnumerable entities); + // 物理删除(不软删除,用于关联表避免唯一索引冲突) + Task HardDeleteAsync(T entity); + Task HardDeleteRangeAsync(IEnumerable entities); + Task SaveChangesAsync(); +} diff --git a/backend/F9WebAiTest.Domain/Entities/AggregateRoot.cs b/backend/F9WebAiTest.Domain/Entities/AggregateRoot.cs new file mode 100644 index 0000000..8159c08 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/AggregateRoot.cs @@ -0,0 +1,21 @@ +namespace F9WebAiTest.Domain.Entities; + +/// +/// DDD 聚合根基类:BusinessEntity + 领域事件。 +/// 聚合根是聚合的入口与唯一代表,外部只能通过聚合根访问聚合内部对象,通过仓储持久化。 +/// 参考 BBIT.F9.Domain.Entities.AggregateRoot +/// +public abstract class AggregateRoot : BusinessEntity, IAggregateRoot +{ + private readonly List _domainEvents = new(); + public IReadOnlyCollection DomainEvents => _domainEvents.AsReadOnly(); + + protected void AddDomainEvent(IDomainEvent domainEvent) => _domainEvents.Add(domainEvent); + + public void ClearDomainEvents() => _domainEvents.Clear(); +} + +/// 聚合根标记接口 +public interface IAggregateRoot +{ +} diff --git a/backend/F9WebAiTest.Domain/Entities/Attendance/AttendanceRecord.cs b/backend/F9WebAiTest.Domain/Entities/Attendance/AttendanceRecord.cs new file mode 100644 index 0000000..b10c635 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Attendance/AttendanceRecord.cs @@ -0,0 +1,18 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Attendance; + +/// 考勤打卡记录 +[Table(Name = "AttendanceRecords")] +[Index("IX_AttendanceRecords_EmployeeId_WorkDate", "EmployeeId, WorkDate", IsUnique = true)] +public class AttendanceRecord : BaseEntity +{ + public long EmployeeId { get; set; } + public DateTime WorkDate { get; set; } // 考勤日期 + public DateTime? SignInTime { get; set; } // 签到时间 + public DateTime? SignOutTime { get; set; } // 签退时间 + public string SignInStatus { get; set; } = ""; // 正常/迟到/缺卡 + public string SignOutStatus { get; set; } = ""; // 正常/早退/缺卡 + public string Remark { get; set; } = ""; +} diff --git a/backend/F9WebAiTest.Domain/Entities/Attendance/LeaveApprover.cs b/backend/F9WebAiTest.Domain/Entities/Attendance/LeaveApprover.cs new file mode 100644 index 0000000..1accc77 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Attendance/LeaveApprover.cs @@ -0,0 +1,18 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Attendance; + +/// 请假审批节点(多级审批流程,OrderNo 表示审批顺序) +[Table(Name = "LeaveApprovers")] +[Index("IX_LeaveApprovers_LeaveId_OrderNo", "LeaveId, OrderNo", IsUnique = true)] +public class LeaveApprover : BaseEntity +{ + public long LeaveId { get; set; } + public long ApproverId { get; set; } // 审批人ID + public string ApproverName { get; set; } = ""; // 审批人姓名(冗余) + public int OrderNo { get; set; } // 序号(从1开始,表示审批顺序) + public string Status { get; set; } = "待审批"; // 待审批/已批准/已驳回 + public string ApproveRemark { get; set; } = ""; + public DateTime? ApprovedAt { get; set; } +} diff --git a/backend/F9WebAiTest.Domain/Entities/Attendance/LeaveRequest.cs b/backend/F9WebAiTest.Domain/Entities/Attendance/LeaveRequest.cs new file mode 100644 index 0000000..f40f1a3 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Attendance/LeaveRequest.cs @@ -0,0 +1,21 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Attendance; + +/// 请假申请 +[Table(Name = "LeaveRequests")] +public class LeaveRequest : BaseEntity +{ + public long EmployeeId { get; set; } + public string Type { get; set; } = "事假"; // 事假/病假/年假/调休/婚假/产假 + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + public string Reason { get; set; } = ""; + public string Status { get; set; } = "待审批"; // 待审批/已批准/已驳回 + public int CurrentOrderNo { get; set; } = 1; // 当前审批到第几步(多级审批) + public long? ApproverId { get; set; } + public string ApproverName { get; set; } = ""; + public string ApproveRemark { get; set; } = ""; + public DateTime? ApprovedAt { get; set; } +} diff --git a/backend/F9WebAiTest.Domain/Entities/Base/CraftZhuangKou.cs b/backend/F9WebAiTest.Domain/Entities/Base/CraftZhuangKou.cs new file mode 100644 index 0000000..1e3f0e9 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Base/CraftZhuangKou.cs @@ -0,0 +1,20 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Base; + +/// 工艺庄口 +[Table(Name = "CraftZhuangKous")] +public class CraftZhuangKou : BaseEntity +{ + public int BelongingYear { get; set; } // 年度 + public int BendiWaidi { get; set; } // 本地外地:1=本地 2=外地 + public string JidiWaigou { get; set; } = ""; // 基地外购 + public string ZhuangKouCode { get; set; } = ""; // 庄口编号 + public string ZhuangKouName { get; set; } = ""; // 庄口名称 + public string ProductionDisplayName { get; set; } = ""; // 生产显示名称 + public string Chandi { get; set; } = ""; // 产地 + public int Sort { get; set; } // 排序 + public string Memo { get; set; } = ""; // 备注 + public string IsJiezhuang { get; set; } = "0"; // 是否结庄 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Base/JizuChetai.cs b/backend/F9WebAiTest.Domain/Entities/Base/JizuChetai.cs new file mode 100644 index 0000000..312b8e4 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Base/JizuChetai.cs @@ -0,0 +1,14 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Base; + +/// 机组车台 +[Table(Name = "JizuChetais")] +public class JizuChetai : BaseEntity +{ + public long DepartmentId { get; set; } // 车间(部门) + public int Jizu { get; set; } // 机组号 + public int ChetaiStart { get; set; } // 开始车台号 + public int ChetaiEnd { get; set; } // 结束车台号 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Base/ShengsiDingjizhibiao.cs b/backend/F9WebAiTest.Domain/Entities/Base/ShengsiDingjizhibiao.cs new file mode 100644 index 0000000..2fd9cd1 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Base/ShengsiDingjizhibiao.cs @@ -0,0 +1,20 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Base; + +/// 生丝定级指标 +[Table(Name = "ShengsiDingjizhibiaos")] +public class ShengsiDingjizhibiao : BaseEntity +{ + public int Standard { get; set; } // 标准:0=厂标 1=国标 2=个人 + public string Guige { get; set; } = ""; // 规格 + public string Grade { get; set; } = ""; // 等级 + public double GradeScore { get; set; } // 等级分值 + public double Piancha { get; set; } // 偏差 + public double Dacha { get; set; } // 大差 + public double Erdu { get; set; } // 二度变化 + public double Sandu { get; set; } // 三度变化 + public double Qingjie { get; set; } // 清洁 + public double Jiejing { get; set; } // 洁净 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Base/ShengsiGuige.cs b/backend/F9WebAiTest.Domain/Entities/Base/ShengsiGuige.cs new file mode 100644 index 0000000..8be6fee --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Base/ShengsiGuige.cs @@ -0,0 +1,30 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Base; + +/// 生丝规格 +[Table(Name = "ShengsiGuiges")] +public class ShengsiGuige : BaseEntity +{ + public string Guige { get; set; } = ""; // 规格 + public double CenterrangeLolimit { get; set; } // 中心范围下限 + public double CenterrangeUplimit { get; set; } // 中心范围上限 + public double CenterXiandu { get; set; } // 中心纤度 + public double ValidLolimit { get; set; } // 有效下限 + public double ValidUplimit { get; set; } // 有效上限 + public double XiaoyeLolimit { get; set; } // 小野下限 + public double XiaoyeUplimit { get; set; } // 小野上限 + public double DayeLolimit { get; set; } // 大野下限 + public double DayeUplimit { get; set; } // 大野上限 + public double TeyeLolimit { get; set; } // 特野下限 + public double TeyeUplimit { get; set; } // 特野上限 + public double YexianNkLolimitLeft { get; set; } // 内控左下 + public double YexianNkLolimitRight { get; set; } // 内控左上 + public double YexianNkUplimitLeft { get; set; } // 内控右下 + public double YexianNkUplimitRight { get; set; } // 内控右上 + public double LaodongJingsaiYexianLolimit { get; set; } // 野纤下限 + public double LaodongJingsaiYexianUplimit { get; set; } // 野纤上限 + public int Sort { get; set; } // 顺序号 + public string Memo { get; set; } = ""; // 备注 +} diff --git a/backend/F9WebAiTest.Domain/Entities/BusinessEntity.cs b/backend/F9WebAiTest.Domain/Entities/BusinessEntity.cs new file mode 100644 index 0000000..a979818 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/BusinessEntity.cs @@ -0,0 +1,22 @@ +using FreeSql.DataAnnotations; + +namespace F9WebAiTest.Domain.Entities; + +/// +/// DDD 业务实体基类:Entity + 审计字段(CreatedBy/UpdateBy/DeletedAt)。 +/// 参考 BBIT.F9.Domain.Entities.BusinessEntity +/// +public abstract class BusinessEntity : Entity +{ + /// 创建人 + [Column(Name = "created_by")] + public string CreatedBy { get; set; } + + /// 修改人 + [Column(Name = "update_by")] + public string UpdateBy { get; set; } + + /// 逻辑删除时间(null=未删除) + [Column(Name = "deleted_at")] + public DateTime? DeletedAt { get; protected set; } +} diff --git a/backend/F9WebAiTest.Domain/Entities/Department.cs b/backend/F9WebAiTest.Domain/Entities/Department.cs new file mode 100644 index 0000000..c0dd3b0 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Department.cs @@ -0,0 +1,17 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities; + +/// 部门(树形结构) +[Table(Name = "Departments")] +public class Department : BaseEntity +{ + public string Name { get; set; } + public string Code { get; set; } = ""; // 部门编码 + public long? ParentId { get; set; } + public long? LeaderId { get; set; } + public string Phone { get; set; } = ""; + public int Sort { get; set; } + public int Status { get; set; } = 1; // 1 启用 0 禁用 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Employee.cs b/backend/F9WebAiTest.Domain/Entities/Employee.cs new file mode 100644 index 0000000..7f5179c --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Employee.cs @@ -0,0 +1,22 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities; + +/// 职工/用户 +[Table(Name = "Employees")] +[Index("IX_Employees_Username", "Username", IsUnique = true)] +public class Employee : BaseEntity +{ + public string Username { get; set; } // 登录账号 + public string Password { get; set; } // 密码哈希 + public string RealName { get; set; } // 真实姓名 + public string Nickname { get; set; } = ""; + public string Avatar { get; set; } = ""; + public string Gender { get; set; } = "男"; + public string Phone { get; set; } = ""; + public string Email { get; set; } = ""; + public long DepartmentId { get; set; } + public int Status { get; set; } = 1; // 1 在职 0 离职/禁用 + public string Remark { get; set; } = ""; +} diff --git a/backend/F9WebAiTest.Domain/Entities/EmployeeRole.cs b/backend/F9WebAiTest.Domain/Entities/EmployeeRole.cs new file mode 100644 index 0000000..d5b42bb --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/EmployeeRole.cs @@ -0,0 +1,13 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities; + +/// 职工-角色 关联 +[Table(Name = "EmployeeRoles")] +[Index("IX_EmployeeRoles_EmployeeId_RoleId", "EmployeeId, RoleId", IsUnique = true)] +public class EmployeeRole : BaseEntity +{ + public long EmployeeId { get; set; } + public long RoleId { get; set; } +} diff --git a/backend/F9WebAiTest.Domain/Entities/Entity.cs b/backend/F9WebAiTest.Domain/Entities/Entity.cs new file mode 100644 index 0000000..1b1763d --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Entity.cs @@ -0,0 +1,25 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities; + +/// +/// DDD 实体基类:具有唯一标识与生命周期。 +/// 实现 IEntity 以兼容通用仓储(IRepository<T>)。 +/// 主键由应用层通过雪花 ID 生成器赋值(非数据库自增)。 +/// 参考 BBIT.F9.Domain.Entities.Entity +/// +public abstract class Entity : IEntity +{ + [Column(IsPrimary = true)] + public long Id { get; set; } + + [Column(Name = "created_at")] + public DateTime CreatedAt { get; set; } + + [Column(Name = "updated_at")] + public DateTime? UpdatedAt { get; set; } + + [Column(Name = "is_deleted")] + public bool IsDeleted { get; set; } +} diff --git a/backend/F9WebAiTest.Domain/Entities/IDomainEvent.cs b/backend/F9WebAiTest.Domain/Entities/IDomainEvent.cs new file mode 100644 index 0000000..500444e --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/IDomainEvent.cs @@ -0,0 +1,15 @@ +using MediatR; + +namespace F9WebAiTest.Domain.Entities; + +/// 领域事件接口(继承 MediatR 的 INotification,便于事件总线发布) +public interface IDomainEvent : INotification +{ + DateTime OccurredOn { get; } +} + +/// 领域事件基类 +public abstract record DomainEvent : IDomainEvent +{ + public DateTime OccurredOn => DateTime.Now; +} diff --git a/backend/F9WebAiTest.Domain/Entities/Menu.cs b/backend/F9WebAiTest.Domain/Entities/Menu.cs new file mode 100644 index 0000000..a739078 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Menu.cs @@ -0,0 +1,19 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities; + +/// 菜单/权限(支持目录、菜单、按钮三种类型) +[Table(Name = "Menus")] +public class Menu : BaseEntity +{ + public long? ParentId { get; set; } + public string Name { get; set; } // 菜单名称 + public string Type { get; set; } = "menu"; // directory 目录 / menu 菜单 / button 按钮 + public string Path { get; set; } = ""; // 路由路径 + public string Component { get; set; } = "";// 组件路径 + public string Icon { get; set; } = ""; // 图标 + public string Permission { get; set; } = "";// 权限标识,如 system:user:list + public int Sort { get; set; } + public int Status { get; set; } = 1; // 1 显示 0 隐藏 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Notice/NotificationEnums.cs b/backend/F9WebAiTest.Domain/Entities/Notice/NotificationEnums.cs new file mode 100644 index 0000000..9c545d3 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Notice/NotificationEnums.cs @@ -0,0 +1,64 @@ +namespace F9WebAiTest.Domain.Entities.Notice; + +/// 消息优先级 +public enum NotificationPriority +{ + Normal = 0, // 普通 + Important = 1, // 重要 + Urgent = 2 // 紧急 +} + +/// 消息状态 +public enum NotificationStatus +{ + Ready = 0, // 准备(待投递) + Sent = 1, // 已发送 + Failed = 2, // 失败 + PartialSuccess = 3, // 部分成功 + Revoked = 4 // 已撤销 +} + +/// 阅读状态 +public enum NotificationReadStatus +{ + Unread = 0, // 未读 + Read = 1 // 已读 +} + +/// 推送状态 +public enum NotificationSendStatus +{ + NotSent = 0, // 未发送 + Success = 1, // 成功 + Failed = 2 // 失败 +} + +/// 待办状态 +public enum TodoItemStatus +{ + Pending = 0, // 待处理 + Processing = 1, // 处理中 + Completed = 2, // 已完成 + Cancelled = 3 // 已取消 +} + +/// 消息分类(决定消息中心展示形态与处理方式) +public enum NotificationCategory +{ + Notification = 0, // 通知:纯提醒,点开查看/标记已读 + Todo = 1, // 待办:需用户处理(审批/任务),关联待办事项 + Announcement = 2 // 公告:广播给全体,重点展示 +} + +/// +/// 消息类型枚举(枚举名与 NotificationType.Code 一一对应)。 +/// 业务侧通过「该枚举 + MediatR 命令」发送消息,无需关心底层实现。 +/// 新增消息类型时:1) 在此加枚举值 2) 在数据库/种子中配置同名 Code 的消息类型(标题、路由模板、接收规则等)。 +/// +public enum NotificationTypeEnum +{ + TeyeFound = 1, // 发现特野 + TdcFound = 2, // 发现特大糙 + LeaveApproval = 3, // 请假审批(待办) + LeaveResult = 4 // 请假结果通知(通知) +} diff --git a/backend/F9WebAiTest.Domain/Entities/Notice/NotificationMessage.cs b/backend/F9WebAiTest.Domain/Entities/Notice/NotificationMessage.cs new file mode 100644 index 0000000..183ae54 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Notice/NotificationMessage.cs @@ -0,0 +1,27 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Notice; + +/// 消息主表(业务系统触发创建,创建时即时投递并实时推送) +[Table(Name = "NotificationMessages")] +[Index("IX_NotificationMessages_MessageTypeCode", "MessageTypeCode")] +public class NotificationMessage : BaseEntity +{ + public long NotificationTypeId { get; set; } // 消息类型ID + public string MessageTypeCode { get; set; } = ""; // 消息类型编码(冗余,路由查询免联表) + public string BusinessType { get; set; } = ""; // 业务类型标识 + public string BusinessId { get; set; } = ""; // 业务单据ID + public string Title { get; set; } = ""; // 标题 + public string Content { get; set; } = ""; // 内容 + public NotificationPriority Priority { get; set; } = NotificationPriority.Normal; // 优先级 + public NotificationStatus Status { get; set; } = NotificationStatus.Ready; // 状态 + public int RetryCount { get; set; } // 重试次数 + public int MaxRetryCount { get; set; } = 3; // 最大重试次数 + public string BizContext { get; set; } = ""; // 业务上下文(JSON) + public string ForceReceivers { get; set; } = ""; // 强制接收人(JSON 数组,如 "[1001,1002]"),空=仅按订阅 + public long CreatorId { get; set; } // 创建人ID + public string CreatorName { get; set; } = ""; // 创建人姓名 + public NotificationCategory Category { get; set; } = NotificationCategory.Notification; // 消息分类(冗余,前端免联表) + public string RoutePath { get; set; } = ""; // Web 端跳转路由(最终值,由模板+业务ID生成) +} diff --git a/backend/F9WebAiTest.Domain/Entities/Notice/NotificationReceiver.cs b/backend/F9WebAiTest.Domain/Entities/Notice/NotificationReceiver.cs new file mode 100644 index 0000000..2ae5a27 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Notice/NotificationReceiver.cs @@ -0,0 +1,20 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Notice; + +/// 消息接收人(投递时生成,记录阅读状态与推送结果:在线推送成功 / 离线待拉取) +[Table(Name = "NotificationReceivers")] +[Index("IX_NotificationReceivers_MessageId_UserId", "MessageId, UserId", IsUnique = true)] +[Index("IX_NotificationReceivers_UserId_ReadStatus", "UserId, ReadStatus")] +public class NotificationReceiver : BaseEntity +{ + public long MessageId { get; set; } // 消息ID + public long UserId { get; set; } // 接收人用户ID + public NotificationReadStatus ReadStatus { get; set; } = NotificationReadStatus.Unread; // 阅读状态 + public DateTime? ReadTime { get; set; } // 阅读时间 + public bool IsOffline { get; set; } // 推送时是否离线 + public NotificationSendStatus SendStatus { get; set; } = NotificationSendStatus.NotSent; // 推送状态 + public DateTime? SendTime { get; set; } // 推送时间 + public string ErrorMsg { get; set; } = ""; // 错误信息 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Notice/NotificationType.cs b/backend/F9WebAiTest.Domain/Entities/Notice/NotificationType.cs new file mode 100644 index 0000000..4abaa24 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Notice/NotificationType.cs @@ -0,0 +1,21 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Notice; + +/// 消息类型(管理员维护,定义订阅所需权限) +[Table(Name = "NotificationTypes")] +[Index("IX_NotificationTypes_Code", "Code", IsUnique = true)] +public class NotificationType : BaseEntity +{ + public string Code { get; set; } = ""; // 消息类型编码,唯一,如 "TeyeFound" + public string Name { get; set; } = ""; // 消息类型名称,如 "发现特野" + public string Description { get; set; } = ""; // 类型描述,展示在订阅页 + public string RequiredPermission { get; set; } = ""; // 订阅该类型所需权限码,空=所有用户可订阅 + public bool IsActive { get; set; } = true; // 是否开放订阅 + public long CreatedBy { get; set; } // 创建人ID + public int Sort { get; set; } // 排序号 + public NotificationCategory Category { get; set; } = NotificationCategory.Notification; // 消息分类 + public string Icon { get; set; } = ""; // 图标(Element Plus 图标名,如 "Bell") + public string RouteTemplate { get; set; } = ""; // Web 端跳转路由模板,支持 {businessId} 占位符 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Notice/TodoItem.cs b/backend/F9WebAiTest.Domain/Entities/Notice/TodoItem.cs new file mode 100644 index 0000000..84cb00f --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Notice/TodoItem.cs @@ -0,0 +1,17 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Notice; + +/// 待办事项(消息可携带待办) +[Table(Name = "TodoItems")] +[Index("IX_TodoItems_AssigneeId_TodoStatus", "AssigneeId, TodoStatus")] +public class TodoItem : BaseEntity +{ + public long MessageId { get; set; } // 关联消息ID + public long AssigneeId { get; set; } // 处理人用户ID + public TodoItemStatus TodoStatus { get; set; } = TodoItemStatus.Pending; // 待办状态 + public DateTime? DueTime { get; set; } // 截止时间 + public DateTime? CompletedTime { get; set; } // 完成时间 + public string HandleResult { get; set; } = ""; // 处理结果 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Notice/UserNotificationSubscription.cs b/backend/F9WebAiTest.Domain/Entities/Notice/UserNotificationSubscription.cs new file mode 100644 index 0000000..a6d76c9 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Notice/UserNotificationSubscription.cs @@ -0,0 +1,16 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Notice; + +/// 用户订阅(用户主动订阅/取消,保留历史) +[Table(Name = "UserNotificationSubscriptions")] +[Index("IX_UserNotificationSubscriptions_UserId_NotificationTypeId", "UserId, NotificationTypeId", IsUnique = true)] +public class UserNotificationSubscription : BaseEntity +{ + public long UserId { get; set; } // 订阅用户ID + public long NotificationTypeId { get; set; } // 消息类型ID + public bool IsActive { get; set; } = true; // 订阅状态:true=已订阅 false=已取消 + public DateTime SubscribedTime { get; set; } = DateTime.Now; // 订阅时间 + public DateTime? CancelTime { get; set; } // 取消订阅时间 +} diff --git a/backend/F9WebAiTest.Domain/Entities/OperationLog.cs b/backend/F9WebAiTest.Domain/Entities/OperationLog.cs new file mode 100644 index 0000000..b2507b6 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/OperationLog.cs @@ -0,0 +1,16 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities; + +/// 操作日志 +[Table(Name = "OperationLogs")] +public class OperationLog : BaseEntity +{ + public long? EmployeeId { get; set; } + public string EmployeeName { get; set; } = ""; + public string Module { get; set; } = ""; + public string Action { get; set; } = ""; + public string Description { get; set; } = ""; + public string Ip { get; set; } = ""; +} diff --git a/backend/F9WebAiTest.Domain/Entities/Prod/HbjyLsjl.cs b/backend/F9WebAiTest.Domain/Entities/Prod/HbjyLsjl.cs new file mode 100644 index 0000000..69d062c --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Prod/HbjyLsjl.cs @@ -0,0 +1,18 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Prod; + +/// 黑板检验-落丝汇总(每个落丝记录一条) +[Table(Name = "HbjyLsjls")] +[Index("IX_HbjyLsjls_LsjlId", "LsjlId", IsUnique = true)] +public class HbjyLsjl : BaseEntity +{ + public long LsjlId { get; set; } // 落丝记录 + public int Pianshu { get; set; } // 片数 + public double Qingjie { get; set; } // 清洁分 + public double Jiejing { get; set; } // 洁净分 + public int YdErdu { get; set; } // 二度变化片数 + public int YdSandu { get; set; } // 三度变化片数 + public string Dengji { get; set; } = ""; // 等级 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Prod/HbjyRecord.cs b/backend/F9WebAiTest.Domain/Entities/Prod/HbjyRecord.cs new file mode 100644 index 0000000..776e60a --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Prod/HbjyRecord.cs @@ -0,0 +1,40 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Prod; + +/// 黑板检验-车台明细(每车台一行) +[Table(Name = "HbjyRecords")] +[Index("IX_HbjyRecords_LsjlId_Chetaihao", "LsjlId, Chetaihao", IsUnique = true)] +public class HbjyRecord : BaseEntity +{ + public long LsjlId { get; set; } // 落丝记录 + public int Chetaihao { get; set; } // 车台号 + public int YdYidu { get; set; } // 一度变化片数 + public int YdErdu { get; set; } // 二度变化片数 + public int YdSandu { get; set; } // 三度变化片数 + // 洁净等级片数(100~60) + public int Jj100 { get; set; } + public int Jj95 { get; set; } + public int Jj90 { get; set; } + public int Jj85 { get; set; } + public int Jj80 { get; set; } + public int Jj75 { get; set; } + public int Jj70 { get; set; } + public int Jj65 { get; set; } + public int Jj60 { get; set; } + // 清洁疵点-特大类 + public int QjZycdTdc { get; set; } // 特大糙 + // 清洁疵点-大类 + public int QjCycdFs { get; set; } // 废丝 + public int QjCycdDc { get; set; } // 大糙 + public int QjCycdNfc { get; set; } // 粘付糙 + public int QjCycdDcj { get; set; } // 大长结 + public int QjCycdZlx { get; set; } // 重螺旋 + // 清洁疵点-小类 + public int QjPtcdXc { get; set; } // 小糙 + public int QjPtcdCj { get; set; } // 长结 + public int QjPtcdLx { get; set; } // 螺旋 + public int QjPtcdHjls { get; set; } // 环结及裂丝 + public int Pianshu { get; set; } // 本车台片数 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Prod/LsjlRecord.cs b/backend/F9WebAiTest.Domain/Entities/Prod/LsjlRecord.cs new file mode 100644 index 0000000..f01ffc2 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Prod/LsjlRecord.cs @@ -0,0 +1,25 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities.Base; + +namespace F9WebAiTest.Domain.Entities.Prod; + +/// 落丝记录(前缫生产) +[Table(Name = "LsjlRecords")] +public class LsjlRecord : BaseEntity +{ + public DateTime WorkDate { get; set; } // 日期 + public long DeptId { get; set; } // 车间 + public long GyzkId { get; set; } // 工艺庄口 + public int Jizu { get; set; } // 机组 + public string LuosiTime { get; set; } = ""; // 落丝时间 + public string Yanse { get; set; } = ""; // 颜色 + public int Huishu { get; set; } // 回数 + public int ChetaiStart { get; set; } // 开始车台号 + public int ChetaiEnd { get; set; } // 结束车台号 + public int Chetaishu { get; set; } // 车台数 + public bool Isbanche { get; set; } // 是否半车 + public double Hours { get; set; } // 缫丝时长 + public string Guige { get; set; } = ""; // 规格 + public double Chesu { get; set; } // 车速 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Prod/XdjyLsjl.cs b/backend/F9WebAiTest.Domain/Entities/Prod/XdjyLsjl.cs new file mode 100644 index 0000000..91e9428 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Prod/XdjyLsjl.cs @@ -0,0 +1,23 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Prod; + +/// 纤度检验-落丝汇总(每个落丝记录一条) +[Table(Name = "XdjyLsjls")] +[Index("IX_XdjyLsjls_LsjlId", "LsjlId", IsUnique = true)] +public class XdjyLsjl : BaseEntity +{ + public long LsjlId { get; set; } // 落丝记录 + public int Jiaoshu { get; set; } // 已称绞数 + public double Avgxiandu { get; set; } // 平均纤度 + public double Junfangcha { get; set; } // 均方差 + public double Zongcha { get; set; } // 总差 + public double FanxianLv { get; set; } // 范纤率(%) + public int FanweiNum { get; set; } // 范围数 + public int XiaoyeNum { get; set; } // 小野数 + public int DayeNum { get; set; } // 大野数 + public int TeyeNum { get; set; } // 特野数 + public int CenterNum { get; set; } // 中心数 + public string Dengji { get; set; } = ""; // 等级 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Prod/XdjyRecord.cs b/backend/F9WebAiTest.Domain/Entities/Prod/XdjyRecord.cs new file mode 100644 index 0000000..3c50d28 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Prod/XdjyRecord.cs @@ -0,0 +1,24 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Prod; + +/// 纤度检验-车台明细(每车台一行,4 个纤度值) +[Table(Name = "XdjyRecords")] +[Index("IX_XdjyRecords_LsjlId_Chetaihao", "LsjlId, Chetaihao", IsUnique = true)] +public class XdjyRecord : BaseEntity +{ + public long LsjlId { get; set; } // 落丝记录 + public int Chetaihao { get; set; } // 车台号 + public double Xd1 { get; set; } // 纤度1 + public double Xd2 { get; set; } // 纤度2 + public double Xd3 { get; set; } // 纤度3 + public double Xd4 { get; set; } // 纤度4 + public int XdNum { get; set; } // 已称绞数 + public double XdAvg { get; set; } // 平均纤度 + public int FanweiNum { get; set; } // 范围 + public int XiaoyeNum { get; set; } // 小野 + public int DayeNum { get; set; } // 大野 + public int TeyeNum { get; set; } // 特野 + public int CenterNum { get; set; } // 中心 +} diff --git a/backend/F9WebAiTest.Domain/Entities/RefreshToken.cs b/backend/F9WebAiTest.Domain/Entities/RefreshToken.cs new file mode 100644 index 0000000..6cd7c4b --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/RefreshToken.cs @@ -0,0 +1,15 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities; + +/// 刷新令牌 +[Table(Name = "RefreshTokens")] +public class RefreshToken : BaseEntity +{ + public long EmployeeId { get; set; } + public string Token { get; set; } + public DateTime ExpiresAt { get; set; } + public bool IsRevoked { get; set; } + public string CreatedByIp { get; set; } = ""; +} diff --git a/backend/F9WebAiTest.Domain/Entities/Role.cs b/backend/F9WebAiTest.Domain/Entities/Role.cs new file mode 100644 index 0000000..676d15e --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Role.cs @@ -0,0 +1,16 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities; + +/// 角色 +[Table(Name = "Roles")] +[Index("IX_Roles_Code", "Code", IsUnique = true)] +public class Role : BaseEntity +{ + public string Name { get; set; } // 角色名称 + public string Code { get; set; } // 角色编码(唯一) + public string Description { get; set; } = ""; + public int Sort { get; set; } + public int Status { get; set; } = 1; +} diff --git a/backend/F9WebAiTest.Domain/Entities/RoleMenu.cs b/backend/F9WebAiTest.Domain/Entities/RoleMenu.cs new file mode 100644 index 0000000..5018726 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/RoleMenu.cs @@ -0,0 +1,13 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities; + +/// 角色-菜单 关联 +[Table(Name = "RoleMenus")] +[Index("IX_RoleMenus_RoleId_MenuId", "RoleId, MenuId", IsUnique = true)] +public class RoleMenu : BaseEntity +{ + public long RoleId { get; set; } + public long MenuId { get; set; } +} diff --git a/backend/F9WebAiTest.Domain/Entities/SYS/Departments/Department.cs b/backend/F9WebAiTest.Domain/Entities/SYS/Departments/Department.cs new file mode 100644 index 0000000..e36286b --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/SYS/Departments/Department.cs @@ -0,0 +1,79 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Enums.SYS.Departments; +using F9WebAiTest.Domain.Events.SYS.Departments; + +namespace F9WebAiTest.Domain.Entities.SYS.Departments; + +/// +/// 部门:用户与部门是独立聚合,仅保存 DepartmentId 关联(避免双向强耦合)。 +/// 部门用户列表仅用于查询展示,通过读模型实现。 +/// +[Table(Name = "sys_departments")] +public class Department : AggregateRoot +{ + [Column(Name = "code")] + public string Code { get; private set; } + + [Column(Name = "name")] + public string Name { get; private set; } + + [Column(Name = "parent_id")] + public long ParentId { get; private set; } + + [Column(Name = "manage_type")] + public Department_ManageType_Enum ManageType { get; private set; } + + [Column(Name = "duty_type")] + public Department_DutyType_Enum DutyType { get; private set; } + + [Column(Name = "description")] + public string? Description { get; private set; } + + [Column(Name = "is_active")] + public bool IsActive { get; private set; } + + [Column(Name = "sort_order")] + public int SortOrder { get; private set; } + + private readonly List _userIds = []; + public IReadOnlyCollection UserIds => _userIds.AsReadOnly(); + + public Department() { } + + public Department(long id, string code, string name, long parentId, + Department_ManageType_Enum manageType, Department_DutyType_Enum dutyType, int sortOrder, string? description = null) + { + Id = id; + Code = code; + Name = name; + ParentId = parentId; + ManageType = manageType; + DutyType = dutyType; + SortOrder = sortOrder; + Description = description; + IsActive = true; + AddDomainEvent(new Department_ChangedDomainEvent(Id)); + } + + public void AddUser(long userId) + { + if (!_userIds.Contains(userId)) + _userIds.Add(userId); + } + + public void RemoveUser(long userId) => _userIds.Remove(userId); + + public void Activate() + { + IsActive = true; + AddDomainEvent(new Department_ChangedDomainEvent(Id)); + } + + public void Deactivate() + { + IsActive = false; + AddDomainEvent(new Department_ChangedDomainEvent(Id)); + } + + public void Deleted() => AddDomainEvent(new Department_ChangedDomainEvent(Id)); +} diff --git a/backend/F9WebAiTest.Domain/Entities/SYS/Departments/DepartmentUser.cs b/backend/F9WebAiTest.Domain/Entities/SYS/Departments/DepartmentUser.cs new file mode 100644 index 0000000..2b612c7 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/SYS/Departments/DepartmentUser.cs @@ -0,0 +1,27 @@ +using FreeSql.DataAnnotations; + +namespace F9WebAiTest.Domain.Entities.SYS.Departments; + +/// 部门用户关联:部门与用户的关联记录。 +[Table(Name = "sys_department_users")] +public class DepartmentUser : BusinessEntity +{ + [Column(Name = "department_id")] + public long DepartmentId { get; private set; } + + [Column(Name = "user_id")] + public long UserId { get; private set; } + + [Column(Name = "assigned_at")] + public DateTime AssignedAt { get; private set; } + + public DepartmentUser() { } + + public DepartmentUser(long id, long departmentId, long userId) + { + Id = id; + DepartmentId = departmentId; + UserId = userId; + AssignedAt = DateTime.UtcNow; + } +} diff --git a/backend/F9WebAiTest.Domain/Entities/SYS/RBAC/Menu.cs b/backend/F9WebAiTest.Domain/Entities/SYS/RBAC/Menu.cs new file mode 100644 index 0000000..e87d831 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/SYS/RBAC/Menu.cs @@ -0,0 +1,95 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Enums.SYS.RBAC; +using F9WebAiTest.Domain.Events.SYS.RBAC; + +namespace F9WebAiTest.Domain.Entities.SYS.RBAC; + +/// 资源:权限作用的对象,如菜单项、按钮、报表等。 +[Table(Name = "sys_rbac_menus")] +public class Menu : AggregateRoot +{ + [Column(Name = "icon_svg")] + public string IconSvg { get; private set; } // 图标 + + [Column(Name = "code")] + public string Code { get; private set; } // 唯一标识 + + [Column(Name = "name")] + public string Name { get; private set; } // 显示名称 + + [Column(Name = "menu_type")] + public MenuType_Enum MenuType { get; private set; } // 菜单类型:分组/页面 + + [Column(Name = "assembly_name")] + public string? AssemblyName { get; private set; } + + [Column(Name = "type_full_name")] + public string? TypeFullName { get; private set; } + + [Column(Name = "parent_id")] + public long ParentId { get; private set; } // 支持菜单层级 + + [Column(Name = "level")] + public int Level { get; private set; } // 层级 + + [Column(Name = "is_active")] + public bool IsActive { get; private set; } + + [Column(Name = "sort_order")] + public int SortOrder { get; private set; } + + [Column(Name = "path")] + public string Path { get; private set; } = ""; // Web 路由路径 + + [Column(Name = "component")] + public string Component { get; private set; } = ""; // 前端组件路径 + + [Column(Name = "permission")] + public string Permission { get; private set; } = ""; // 权限标识,如 sys:user:list + + public Menu() { } + + public Menu(long id, string name, string iconSvg, MenuType_Enum menuType = MenuType_Enum.默认, + string? assemblyName = null, string? typeFullName = null, long parentId = 0, int sortOrder = 0, int parentLevel = 0) + { + Id = id; + Name = name; + IconSvg = iconSvg; + MenuType = menuType; + AssemblyName = assemblyName; + TypeFullName = typeFullName; + ParentId = parentId; + Level = parentId == 0 ? 1 : parentLevel + 1; + IsActive = true; + CreatedAt = DateTime.Now; + SortOrder = sortOrder; + AddDomainEvent(new Menu_ChangedDomainEvent(Id)); + } + + public void GenerateNewCode() => Code = $"menu.{DateTime.Now.Ticks}"; + + /// 设置 Web 路由信息(路径/组件/权限码) + public void SetWebInfo(string path, string component, string permission) + { + Path = path ?? ""; + Component = component ?? ""; + Permission = permission ?? ""; + } + + public IReadOnlySet SupportedPermissionType => + new HashSet { Permission_Type_Enum.Visble }; + + public void SoftDelete() => AddDomainEvent(new Menu_ChangedDomainEvent(Id)); + + public void Activate() + { + IsActive = true; + AddDomainEvent(new Menu_ChangedDomainEvent(Id)); + } + + public void Deactivate() + { + IsActive = false; + AddDomainEvent(new Menu_ChangedDomainEvent(Id)); + } +} diff --git a/backend/F9WebAiTest.Domain/Entities/SYS/RBAC/Permission.cs b/backend/F9WebAiTest.Domain/Entities/SYS/RBAC/Permission.cs new file mode 100644 index 0000000..6cc403a --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/SYS/RBAC/Permission.cs @@ -0,0 +1,47 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Enums.SYS.RBAC; + +namespace F9WebAiTest.Domain.Entities.SYS.RBAC; + +/// 权限:最小的权限单元,如“查看用户列表”、“导出报表”。 +[Table(Name = "sys_rbac_permissions")] +public class Permission : AggregateRoot +{ + /// 编号,格式:资源:操作,例如 user:add, report:sales:view + [Column(Name = "code")] + public string Code { get; private set; } + + [Column(Name = "permission_type")] + public Permission_Type_Enum PermissionType { get; private set; } + + [Column(Name = "resource_type")] + public Resource_Type_Enum ResourceType { get; private set; } + + [Column(Name = "resource_id")] + public long ResourceId { get; private set; } // 关联资源(菜单、报表等) + + public Permission() { } + + public Permission(long id, Permission_Type_Enum permissionType, Resource_Type_Enum resourceType, string resourceIdentity, long resourceId) + { + Id = id; + PermissionType = permissionType; + ResourceType = resourceType; + ResourceId = resourceId; + CreatedAt = DateTime.Now; + Code = $"{GetResorceTypeIdentity}:{resourceIdentity}:{permissionType.ToString().ToLower()}"; + } + + public string GetResorceTypeIdentity => ResourceType switch + { + Resource_Type_Enum.菜单 => "menu", + Resource_Type_Enum.报表 => "report", + Resource_Type_Enum.操作 => "operation", + Resource_Type_Enum.部门 => "department", + Resource_Type_Enum.消息订阅 => "subscribe", + _ => throw new ArgumentOutOfRangeException() + }; + + /// 自定义权限码(用于消息订阅等业务权限码与 RBAC 权限码保持一致) + public void SetCode(string code) => Code = code; +} diff --git a/backend/F9WebAiTest.Domain/Entities/SYS/RBAC/Role.cs b/backend/F9WebAiTest.Domain/Entities/SYS/RBAC/Role.cs new file mode 100644 index 0000000..c0dd114 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/SYS/RBAC/Role.cs @@ -0,0 +1,65 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Enums.SYS.RBAC; +using F9WebAiTest.Domain.Events.SYS.RBAC; + +namespace F9WebAiTest.Domain.Entities.SYS.RBAC; + +/// 角色:一组权限的集合。 +[Table(Name = "sys_rbac_roles")] +public class Role : AggregateRoot +{ + [Column(Name = "name")] + public string Name { get; private set; } // 如 "财务专员", "系统管理员" + + [Column(Name = "code")] + public string Code { get; private set; } // 如:role:xxxx + + [Column(Name = "sort_order")] + public int SortOrder { get; private set; } + + [Column(Name = "description")] + public string Description { get; private set; } + + [Column(Name = "is_active")] + public bool IsActive { get; private set; } + + private readonly List _permissionIds = []; + public IReadOnlyCollection PermissionIds => _permissionIds.AsReadOnly(); + + public Role() { } + + public Role(long id, string name, string code, int sortOrder, string description) + { + Id = id; + Name = name; + Code = code; + SortOrder = sortOrder; + IsActive = true; + Description = description; + CreatedAt = DateTime.Now; + } + + public void Activate() => IsActive = true; + public void Deactivate() => IsActive = false; + + public void AddPermission(long permissionId, Resource_Type_Enum type) + { + if (!_permissionIds.Contains(permissionId)) + _permissionIds.Add(permissionId); + + if (type == Resource_Type_Enum.菜单) + AddDomainEvent(new RoleMenu_ChangedDomainEvent(Id)); + else if (type == Resource_Type_Enum.部门) + AddDomainEvent(new RoleDepartment_ChangedDomainEvent(Id)); + } + + public void RemovePermission(long permissionId, Resource_Type_Enum type) + { + _permissionIds.Remove(permissionId); + + if (type == Resource_Type_Enum.菜单) + AddDomainEvent(new RoleMenu_ChangedDomainEvent(Id)); + else if (type == Resource_Type_Enum.部门) + AddDomainEvent(new RoleDepartment_ChangedDomainEvent(Id)); + } +} diff --git a/backend/F9WebAiTest.Domain/Entities/SYS/RBAC/RolePermission.cs b/backend/F9WebAiTest.Domain/Entities/SYS/RBAC/RolePermission.cs new file mode 100644 index 0000000..443a68d --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/SYS/RBAC/RolePermission.cs @@ -0,0 +1,24 @@ +using FreeSql.DataAnnotations; + +namespace F9WebAiTest.Domain.Entities.SYS.RBAC; + +/// 角色权限关联:角色与权限的多对多关系。 +[Table(Name = "sys_rbac_role_permissions")] +public class RolePermission : BusinessEntity +{ + [Column(Name = "role_id")] + public long RoleId { get; private set; } + + [Column(Name = "permission_id")] + public long PermissionId { get; private set; } + + public RolePermission() { } + + public RolePermission(long id, long roleId, long permissionId) + { + Id = id; + RoleId = roleId; + PermissionId = permissionId; + CreatedAt = DateTime.UtcNow; + } +} diff --git a/backend/F9WebAiTest.Domain/Entities/SYS/RBAC/User.cs b/backend/F9WebAiTest.Domain/Entities/SYS/RBAC/User.cs new file mode 100644 index 0000000..1ac1d26 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/SYS/RBAC/User.cs @@ -0,0 +1,98 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Events.SYS.RBAC; + +namespace F9WebAiTest.Domain.Entities.SYS.RBAC; + +/// 用户:可以拥有多个角色。 +[Table(Name = "sys_rbac_users")] +public class User : AggregateRoot +{ + [Column(Name = "department_id")] + public long DepartmentId { get; private set; } + + [Column(Name = "user_name")] + public string UserName { get; private set; } + + [Column(Name = "phone_number")] + public string PhoneNumber { get; private set; } + + [Column(Name = "login_name")] + public string LoginName { get; private set; } + + [Column(Name = "password_hash")] + public string PasswordHash { get; private set; } + + [Column(Name = "is_active")] + public bool IsActive { get; private set; } + + [Column(Name = "sort_order")] + public int SortOrder { get; private set; } + + [Column(Name = "lastlogin_at")] + public DateTime? LastLoginAt { get; private set; } + + [Column(Name = "nickname")] + public string Nickname { get; private set; } = ""; + + [Column(Name = "avatar")] + public string Avatar { get; private set; } = ""; + + [Column(Name = "email")] + public string Email { get; private set; } = ""; + + [Column(Name = "gender")] + public string Gender { get; private set; } = "男"; + + public User() { } + + public User(long id, string userName, string phoneNumber, string loginName, long departmentId, int sortOrder) + { + Id = id; + UserName = userName; + PhoneNumber = phoneNumber; + LoginName = loginName; + DepartmentId = departmentId; + IsActive = true; + SortOrder = sortOrder; + } + + public void Update(string userName, string phoneNumber, string loginName, long departmentId, int sortOrder) + { + UserName = userName; + PhoneNumber = phoneNumber; + LoginName = loginName; + DepartmentId = departmentId; + IsActive = true; + SortOrder = sortOrder; + } + + public void Activate() => IsActive = true; + public void Deactivate() => IsActive = false; + + /// 获取默认密码明文 + public string GetDefaultPasswordText() => "123456"; + + /// 设置密码(哈希后的密文) + public void SetPassword(string password) => PasswordHash = password; + + /// 更新个人资料(昵称/头像/邮箱/手机号/性别) + public void UpdateProfile(string nickname, string avatar, string email, string phoneNumber, string gender) + { + Nickname = nickname ?? Nickname; + Avatar = avatar ?? Avatar; + Email = email ?? Email; + PhoneNumber = phoneNumber ?? PhoneNumber; + Gender = gender ?? Gender; + } + + /// 登录成功,记录登录时间并发布登录事件 + public void LoginSuccess(string deviceId, string account, bool success, long userId, string userName) + { + LastLoginAt = DateTime.Now; + AddDomainEvent(new UserLoginDomainEvent(deviceId, account, success, userId, userName)); + } + + public void AddRole(long roleId) => AddDomainEvent(new UserRole_ChangedDomainEvent(Id)); + + public void RemoveRole(long roleId) => AddDomainEvent(new UserRole_ChangedDomainEvent(Id)); +} diff --git a/backend/F9WebAiTest.Domain/Entities/SYS/RBAC/UserRole.cs b/backend/F9WebAiTest.Domain/Entities/SYS/RBAC/UserRole.cs new file mode 100644 index 0000000..ec6867d --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/SYS/RBAC/UserRole.cs @@ -0,0 +1,27 @@ +using FreeSql.DataAnnotations; + +namespace F9WebAiTest.Domain.Entities.SYS.RBAC; + +/// +/// 用户角色关联:用户与角色的多对多关系。 +/// DDD 中作为独立关联实体,应用层直接操作其仓储(不通过 User 聚合)。 +/// +[Table(Name = "sys_rbac_user_roles")] +public class UserRole : BusinessEntity +{ + [Column(Name = "user_id")] + public long UserId { get; private set; } + + [Column(Name = "role_id")] + public long RoleId { get; private set; } + + public UserRole() { } + + public UserRole(long id, long userId, long roleId) + { + Id = id; + UserId = userId; + RoleId = roleId; + CreatedAt = DateTime.UtcNow; + } +} diff --git a/backend/F9WebAiTest.Domain/Enums/SYS/Departments/Department_DutyType_Enum.cs b/backend/F9WebAiTest.Domain/Enums/SYS/Departments/Department_DutyType_Enum.cs new file mode 100644 index 0000000..297a656 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Enums/SYS/Departments/Department_DutyType_Enum.cs @@ -0,0 +1,22 @@ +namespace F9WebAiTest.Domain.Enums.SYS.Departments; + +/// 部门职责类型 +public enum Department_DutyType_Enum +{ + 其他 = 5, + 仓储 = 10, + 选剥茧 = 15, + 煮茧 = 20, + 前缫 = 25, + 后缫 = 30, + 设备办 = 35, + 副产品 = 40, + 质检 = 45, + 丝绵 = 50, + 人力资源 = 55, + + 丝业中心 = 100, + 前缫管理 = 105, + 后缫检验 = 110, + 后缫整理 = 115 +} diff --git a/backend/F9WebAiTest.Domain/Enums/SYS/Departments/Department_ManageType_Enum.cs b/backend/F9WebAiTest.Domain/Enums/SYS/Departments/Department_ManageType_Enum.cs new file mode 100644 index 0000000..f6bb8c6 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Enums/SYS/Departments/Department_ManageType_Enum.cs @@ -0,0 +1,12 @@ +namespace F9WebAiTest.Domain.Enums.SYS.Departments; + +/// 部门管理类型 +public enum Department_ManageType_Enum +{ + 公司 = 5, + 部门 = 10, + 车间 = 15, + 科室 = 20, + 岗位 = 25, + 虚拟 = 30 +} diff --git a/backend/F9WebAiTest.Domain/Enums/SYS/RBAC/MenuType_Enum.cs b/backend/F9WebAiTest.Domain/Enums/SYS/RBAC/MenuType_Enum.cs new file mode 100644 index 0000000..ec2c738 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Enums/SYS/RBAC/MenuType_Enum.cs @@ -0,0 +1,9 @@ +namespace F9WebAiTest.Domain.Enums.SYS.RBAC; + +/// 菜单类型 +public enum MenuType_Enum +{ + 默认 = 0, + 分组 = 1, + 页面 = 2 +} diff --git a/backend/F9WebAiTest.Domain/Enums/SYS/RBAC/Permission_Type_Enum.cs b/backend/F9WebAiTest.Domain/Enums/SYS/RBAC/Permission_Type_Enum.cs new file mode 100644 index 0000000..3597fb6 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Enums/SYS/RBAC/Permission_Type_Enum.cs @@ -0,0 +1,11 @@ +namespace F9WebAiTest.Domain.Enums.SYS.RBAC; + +/// 权限类型 +public enum Permission_Type_Enum +{ + /// 查看 + Visble = 1, + + /// 使能(是否可操作) + Enable = 2 +} diff --git a/backend/F9WebAiTest.Domain/Enums/SYS/RBAC/Resource_Type_Enum.cs b/backend/F9WebAiTest.Domain/Enums/SYS/RBAC/Resource_Type_Enum.cs new file mode 100644 index 0000000..e52a9b3 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Enums/SYS/RBAC/Resource_Type_Enum.cs @@ -0,0 +1,13 @@ +namespace F9WebAiTest.Domain.Enums.SYS.RBAC; + +/// 资源类型(权限作用的对象) +public enum Resource_Type_Enum +{ + 菜单 = 1, + // 按钮、功能 + 操作 = 2, + 报表 = 3, + 部门 = 4, + // 消息订阅(如请假审批、考勤通知等消息类型的订阅权限) + 消息订阅 = 5 +} diff --git a/backend/F9WebAiTest.Domain/Events/SYS/Departments/Department_ChangedDomainEvent.cs b/backend/F9WebAiTest.Domain/Events/SYS/Departments/Department_ChangedDomainEvent.cs new file mode 100644 index 0000000..c92032e --- /dev/null +++ b/backend/F9WebAiTest.Domain/Events/SYS/Departments/Department_ChangedDomainEvent.cs @@ -0,0 +1,5 @@ +using F9WebAiTest.Domain.Entities; + +namespace F9WebAiTest.Domain.Events.SYS.Departments; + +public record Department_ChangedDomainEvent(long depId) : DomainEvent; diff --git a/backend/F9WebAiTest.Domain/Events/SYS/RBAC/Menu_ChangedDomainEvent.cs b/backend/F9WebAiTest.Domain/Events/SYS/RBAC/Menu_ChangedDomainEvent.cs new file mode 100644 index 0000000..0374c29 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Events/SYS/RBAC/Menu_ChangedDomainEvent.cs @@ -0,0 +1,5 @@ +using F9WebAiTest.Domain.Entities; + +namespace F9WebAiTest.Domain.Events.SYS.RBAC; + +public record Menu_ChangedDomainEvent(long menuId) : DomainEvent; diff --git a/backend/F9WebAiTest.Domain/Events/SYS/RBAC/RoleDepartment_ChangedDomainEvent.cs b/backend/F9WebAiTest.Domain/Events/SYS/RBAC/RoleDepartment_ChangedDomainEvent.cs new file mode 100644 index 0000000..e6d1d47 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Events/SYS/RBAC/RoleDepartment_ChangedDomainEvent.cs @@ -0,0 +1,5 @@ +using F9WebAiTest.Domain.Entities; + +namespace F9WebAiTest.Domain.Events.SYS.RBAC; + +public record RoleDepartment_ChangedDomainEvent(long roleId) : DomainEvent; diff --git a/backend/F9WebAiTest.Domain/Events/SYS/RBAC/RoleMenu_ChangedDomainEvent.cs b/backend/F9WebAiTest.Domain/Events/SYS/RBAC/RoleMenu_ChangedDomainEvent.cs new file mode 100644 index 0000000..a4d94a1 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Events/SYS/RBAC/RoleMenu_ChangedDomainEvent.cs @@ -0,0 +1,5 @@ +using F9WebAiTest.Domain.Entities; + +namespace F9WebAiTest.Domain.Events.SYS.RBAC; + +public record RoleMenu_ChangedDomainEvent(long roleId) : DomainEvent; diff --git a/backend/F9WebAiTest.Domain/Events/SYS/RBAC/UserCreatedDomainEvent.cs b/backend/F9WebAiTest.Domain/Events/SYS/RBAC/UserCreatedDomainEvent.cs new file mode 100644 index 0000000..2be5d5b --- /dev/null +++ b/backend/F9WebAiTest.Domain/Events/SYS/RBAC/UserCreatedDomainEvent.cs @@ -0,0 +1,8 @@ +using F9WebAiTest.Domain.Entities; + +namespace F9WebAiTest.Domain.Events.SYS.RBAC; + +public record UserCreatedDomainEvent( + long UserId, + string UserName, + long DepartmentId) : DomainEvent; diff --git a/backend/F9WebAiTest.Domain/Events/SYS/RBAC/UserLoginDomainEvent.cs b/backend/F9WebAiTest.Domain/Events/SYS/RBAC/UserLoginDomainEvent.cs new file mode 100644 index 0000000..ae7eb1f --- /dev/null +++ b/backend/F9WebAiTest.Domain/Events/SYS/RBAC/UserLoginDomainEvent.cs @@ -0,0 +1,10 @@ +using F9WebAiTest.Domain.Entities; + +namespace F9WebAiTest.Domain.Events.SYS.RBAC; + +public record UserLoginDomainEvent( + string DeviceId, + string Account, + bool Success, + long UserId, + string UserName) : DomainEvent; diff --git a/backend/F9WebAiTest.Domain/Events/SYS/RBAC/UserRole_ChangedDomainEvent.cs b/backend/F9WebAiTest.Domain/Events/SYS/RBAC/UserRole_ChangedDomainEvent.cs new file mode 100644 index 0000000..a1eec82 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Events/SYS/RBAC/UserRole_ChangedDomainEvent.cs @@ -0,0 +1,5 @@ +using F9WebAiTest.Domain.Entities; + +namespace F9WebAiTest.Domain.Events.SYS.RBAC; + +public record UserRole_ChangedDomainEvent(long userId) : DomainEvent; diff --git a/backend/F9WebAiTest.Domain/F9WebAiTest.Domain.csproj b/backend/F9WebAiTest.Domain/F9WebAiTest.Domain.csproj new file mode 100644 index 0000000..46c7f54 --- /dev/null +++ b/backend/F9WebAiTest.Domain/F9WebAiTest.Domain.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + disable + F9WebAiTest.Domain + + + + + + + + diff --git a/backend/F9WebAiTest.Domain/Interfaces/IDInterface.cs b/backend/F9WebAiTest.Domain/Interfaces/IDInterface.cs new file mode 100644 index 0000000..f77a245 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Interfaces/IDInterface.cs @@ -0,0 +1,6 @@ +namespace F9WebAiTest.Domain.Interfaces; + +/// 标记需要依赖注入的接口(写仓储接口等) +public interface IDInterface +{ +} diff --git a/backend/F9WebAiTest.Domain/Interfaces/SYS/Departments/IDepartmentUserRepository.cs b/backend/F9WebAiTest.Domain/Interfaces/SYS/Departments/IDepartmentUserRepository.cs new file mode 100644 index 0000000..6748209 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Interfaces/SYS/Departments/IDepartmentUserRepository.cs @@ -0,0 +1,11 @@ +using F9WebAiTest.Domain.Entities.SYS.Departments; + +namespace F9WebAiTest.Domain.Interfaces.SYS.Departments; + +/// 部门用户关联写仓储 +public interface IDepartmentUserRepository : IDInterface +{ + Task AssignAsync(DepartmentUser depUser); + + Task RemoveAsync(long depId, long userId); +} diff --git a/backend/F9WebAiTest.Domain/Interfaces/SYS/Departments/IDepartmentWriteRepository.cs b/backend/F9WebAiTest.Domain/Interfaces/SYS/Departments/IDepartmentWriteRepository.cs new file mode 100644 index 0000000..f1e62c5 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Interfaces/SYS/Departments/IDepartmentWriteRepository.cs @@ -0,0 +1,15 @@ +using F9WebAiTest.Domain.Entities.SYS.Departments; + +namespace F9WebAiTest.Domain.Interfaces.SYS.Departments; + +/// 部门写仓储(聚合根仓储) +public interface IDepartmentWriteRepository : IDInterface +{ + /// 返回聚合根 + Task GetByIdAsync(long id, CancellationToken cancellationToken); + + Task AddAsync(Department dep); + Task UpdateAsync(Department dep); + Task SoftDeleteAsync(long id); + Task ActiveOrDeactive(long id, bool active); +} diff --git a/backend/F9WebAiTest.Domain/Interfaces/SYS/RBAC/Menus/IMenuWriteRepository.cs b/backend/F9WebAiTest.Domain/Interfaces/SYS/RBAC/Menus/IMenuWriteRepository.cs new file mode 100644 index 0000000..b885111 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Interfaces/SYS/RBAC/Menus/IMenuWriteRepository.cs @@ -0,0 +1,13 @@ +using F9WebAiTest.Domain.Entities.SYS.RBAC; + +namespace F9WebAiTest.Domain.Interfaces.SYS.RBAC.Menus; + +/// 菜单写仓储(聚合根仓储) +public interface IMenuWriteRepository : IDInterface +{ + Task GetByIdAsync(long id); + Task AddAsync(Menu menu); + Task UpdateAsync(Menu menu); + Task SoftDeleteAsync(long id); + Task ActiveOrDeactive(long id, bool active); +} diff --git a/backend/F9WebAiTest.Domain/Interfaces/SYS/RBAC/Permissions/IPermissionWriteRepository.cs b/backend/F9WebAiTest.Domain/Interfaces/SYS/RBAC/Permissions/IPermissionWriteRepository.cs new file mode 100644 index 0000000..6805748 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Interfaces/SYS/RBAC/Permissions/IPermissionWriteRepository.cs @@ -0,0 +1,13 @@ +using F9WebAiTest.Domain.Entities.SYS.RBAC; +using F9WebAiTest.Domain.Enums.SYS.RBAC; + +namespace F9WebAiTest.Domain.Interfaces.SYS.RBAC.Permissions; + +/// 权限写仓储 +public interface IPermissionWriteRepository : IDInterface +{ + Task GetByIdAsync(Permission_Type_Enum permissionType, Resource_Type_Enum resourceType, long resourceId); + Task AddRangeAsync(Permission[] entites); + Task DeleteByResourceIdAndTypeAsync(Permission_Type_Enum permissionType, Resource_Type_Enum resourceType, long resourceId); + Task DeleteByResourceIdAsync(long resourceId); +} diff --git a/backend/F9WebAiTest.Domain/Interfaces/SYS/RBAC/Roles/IRolePermissionWriteRepository.cs b/backend/F9WebAiTest.Domain/Interfaces/SYS/RBAC/Roles/IRolePermissionWriteRepository.cs new file mode 100644 index 0000000..560dc6f --- /dev/null +++ b/backend/F9WebAiTest.Domain/Interfaces/SYS/RBAC/Roles/IRolePermissionWriteRepository.cs @@ -0,0 +1,11 @@ +using F9WebAiTest.Domain.Entities.SYS.RBAC; + +namespace F9WebAiTest.Domain.Interfaces.SYS.RBAC.Roles; + +/// 角色权限关联写仓储 +public interface IRolePermissionWriteRepository : IDInterface +{ + Task GetByIdAsync(long id); + Task AssignAsync(RolePermission rolePermission); + Task RemoveAsync(long id); +} diff --git a/backend/F9WebAiTest.Domain/Interfaces/SYS/RBAC/Roles/IRoleWriteRepository.cs b/backend/F9WebAiTest.Domain/Interfaces/SYS/RBAC/Roles/IRoleWriteRepository.cs new file mode 100644 index 0000000..7b10769 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Interfaces/SYS/RBAC/Roles/IRoleWriteRepository.cs @@ -0,0 +1,13 @@ +using F9WebAiTest.Domain.Entities.SYS.RBAC; + +namespace F9WebAiTest.Domain.Interfaces.SYS.RBAC.Roles; + +/// 角色写仓储(聚合根仓储) +public interface IRoleWriteRepository : IDInterface +{ + Task GetByIdAsync(long id); + Task AddAsync(Role role); + Task UpdateAsync(Role role); + Task SoftDeleteAsync(long id); + Task ActiveOrDeactive(long id, bool active); +} diff --git a/backend/F9WebAiTest.Domain/Interfaces/SYS/RBAC/Users/IUserRoleWriteRepository.cs b/backend/F9WebAiTest.Domain/Interfaces/SYS/RBAC/Users/IUserRoleWriteRepository.cs new file mode 100644 index 0000000..88911e6 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Interfaces/SYS/RBAC/Users/IUserRoleWriteRepository.cs @@ -0,0 +1,14 @@ +using F9WebAiTest.Domain.Entities.SYS.RBAC; + +namespace F9WebAiTest.Domain.Interfaces.SYS.RBAC.Users; + +/// +/// 用户角色关联写仓储:不把 AssignRole 放在 IUserWriteRepository(避免污染聚合仓储), +/// 而是定义独立的细粒度接口,应用层依赖此抽象,基础设施层实现。 +/// +public interface IUserRoleWriteRepository : IDInterface +{ + Task AssignAsync(UserRole userRole); + + Task RemoveAsync(long userid, long roleId); +} diff --git a/backend/F9WebAiTest.Domain/Interfaces/SYS/RBAC/Users/IUserWriteRepository.cs b/backend/F9WebAiTest.Domain/Interfaces/SYS/RBAC/Users/IUserWriteRepository.cs new file mode 100644 index 0000000..d107040 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Interfaces/SYS/RBAC/Users/IUserWriteRepository.cs @@ -0,0 +1,18 @@ +using F9WebAiTest.Domain.Entities.SYS.RBAC; + +namespace F9WebAiTest.Domain.Interfaces.SYS.RBAC.Users; + +/// 用户写仓储(聚合根仓储) +public interface IUserWriteRepository : IDInterface +{ + /// 返回聚合根 + Task GetByIdAsync(long id); + + Task AddAsync(User user); + Task UpdateAsync(User user); + Task SoftDeleteAsync(long id); + Task ActiveOrDeactive(long id, bool active); + + Task UpdatePassworkdAsync(long id, string passwordHash); + Task UpdateLastLoginAtAsync(long id, DateTime dt); +} diff --git a/backend/F9WebAiTest.Infrastructure/Data/BusinessSeedData.cs b/backend/F9WebAiTest.Infrastructure/Data/BusinessSeedData.cs new file mode 100644 index 0000000..f2f3d22 --- /dev/null +++ b/backend/F9WebAiTest.Infrastructure/Data/BusinessSeedData.cs @@ -0,0 +1,501 @@ +using FreeSql; +using F9WebAiTest.Domain.Entities; +using F9WebAiTest.Domain.Entities.Base; + +namespace F9WebAiTest.Infrastructure.Data; + +/// 业务基础数据种子(部门/工艺庄口/生丝规格/生丝定级指标/机组车台) +public static class BusinessSeedData +{ + public static void Seed(IFreeSql fsql) + { + SeedMenus(fsql); + SeedCraftZhuangKou(fsql); + SeedShengsiGuige(fsql); + SeedShengsiDingjizhibiao(fsql); + SeedDepartmentsAndJizuChetai(fsql); + SeedEmployees(fsql); + } + + // ==================== 部门 + 机组车台(真实组织架构) ==================== + private static readonly (long Id, string Name, long ParentId, int Sort, string Code)[] DeptData = + { + (10920411881541632, "安泰茧丝绸", 0, 0, "123"), + (10929439026188288, "丝业生产中心", 10920411881541632, 100, "100"), + (19667471837433856, "主干运维", 10920411881541632, 9999, "9999"), + (22305637455106048, "人力资源部", 10920411881541632, 800, "800"), + (22305758536273920, "财务部", 10920411881541632, 400, "400"), + (22305841730293760, "营销中心", 10920411881541632, 500, "500"), + (22305998261719040, "计划运营部", 10920411881541632, 200, "200"), + (22306589042020352, "品管部", 10920411881541632, 700, "700"), + (22306677265010688, "采购部", 10920411881541632, 300, "300"), + (26900271946797056, "安全环保后勤部", 10920411881541632, 1000, "1000"), + (26901050904547328, "经理会", 10920411881541632, 1500, "1500"), + (26901341515288576, "监察室", 10920411881541632, 3000, "3000"), + (26901475166785536, "融资部", 10920411881541632, 2000, "2000"), + (34638098675535872, "武胜蚕业公司", 10920411881541632, 4000, "4000"), + (35981234102341632, "安泰丝绸人南店", 10920411881541632, 6200, "6200"), + (35981427686248448, "绸业生产中心", 10920411881541632, 5000, "5000"), + (35981496963567616, "夹江蚕业公司", 10920411881541632, 4100, "4100"), + (35982738976673792, "安泰丝绸融恒店", 10920411881541632, 6000, "6000"), + (35982891737419776, "帛锦汇", 10920411881541632, 6100, "6100"), + (35982956409393152, "蚕业技术服务部", 10920411881541632, 4900, "4900"), + (36277179469795328, "广安蚕业公司", 10920411881541632, 4200, "4200"), + (10929739564847104, "仓储", 10929439026188288, 1000, "1000"), + (22307875854487552, "质检科", 10929439026188288, 1100, "1100"), + (22308241237086208, "试样室", 10929439026188288, 1200, "1200"), + (22308361408090112, "选剥茧车间", 10929439026188288, 1300, "1300"), + (25256354205667328, "前缫车间", 10929439026188288, 1350, "1350"), + (22308805823959040, "后缫车间", 10929439026188288, 1800, "1800"), + (22309110082965504, "设备车间", 10929439026188288, 2100, "2100"), + (22309340027293696, "副产品车间", 10929439026188288, 2200, "2200"), + (68179345423732736, "临时", 10929439026188288, 3000, "3000"), + (16043931070369792, "前缫一车间", 25256354205667328, 1400, "1400"), + (16047387197837312, "前缫二车间", 25256354205667328, 1500, "1500"), + (22308600831545344, "前缫三车间", 25256354205667328, 1600, "1600"), + (22308683820044288, "前缫四车间", 25256354205667328, 1700, "1700"), + (25379428280307712, "煮茧室", 25256354205667328, 1350, "1350"), + (22308918461992960, "检验室", 22308805823959040, 1900, "1900"), + (22309003073687552, "整理室", 22308805823959040, 2000, "2000"), + (24029387057795072, "丝棉车间", 22309340027293696, 0, "999"), + }; + + private static readonly (long DeptId, int Jizu, int Start, int End)[] JizuData = + { + (16043931070369792, 1, 1, 20), (16043931070369792, 2, 21, 40), (16043931070369792, 3, 41, 60), + (16043931070369792, 4, 61, 80), (16043931070369792, 5, 81, 100), (16043931070369792, 6, 101, 120), + (16047387197837312, 1, 1, 20), (16047387197837312, 2, 21, 40), (16047387197837312, 3, 41, 60), + (16047387197837312, 4, 61, 80), (16047387197837312, 5, 81, 100), (16047387197837312, 6, 101, 120), + (22308600831545344, 1, 1, 20), (22308600831545344, 2, 21, 40), (22308600831545344, 3, 41, 60), + (22308600831545344, 4, 61, 80), (22308600831545344, 5, 81, 100), (22308600831545344, 6, 101, 120), + (22308683820044288, 1, 1, 20), (22308683820044288, 2, 21, 40), (22308683820044288, 3, 41, 60), + (22308683820044288, 4, 61, 80), (22308683820044288, 5, 81, 100), (22308683820044288, 6, 101, 120), + }; + + private static void SeedDepartmentsAndJizuChetai(IFreeSql fsql) + { + // 检测是否已有真实部门(以"安泰茧丝绸"为标记) + if (fsql.Select().Any(d => d.Name == "安泰茧丝绸")) return; + + // 物理清空假数据(总公司/技术部等)及关联 + fsql.Ado.ExecuteNonQuery("DELETE FROM JizuChetais"); + fsql.Ado.ExecuteNonQuery("DELETE FROM AttendanceRecords"); + fsql.Ado.ExecuteNonQuery("DELETE FROM LeaveRequests"); + fsql.Ado.ExecuteNonQuery("DELETE FROM Employees"); + fsql.Ado.ExecuteNonQuery("DELETE FROM Departments"); + + // 第一遍:创建所有部门(ParentId 暂不设) + var idMap = new Dictionary(); + foreach (var d in DeptData) + { + var dept = new Department { Name = d.Name, Code = d.Code, Sort = d.Sort }; + var newId = DbSeeder.InsertAndGetId(fsql, dept); + idMap[d.Id] = newId; + } + + // 第二遍:设置 ParentId + foreach (var d in DeptData) + { + if (d.ParentId != 0 && idMap.ContainsKey(d.ParentId)) + { + var dept = fsql.Select().Where(x => x.Id == idMap[d.Id]).First(); + dept.ParentId = idMap[d.ParentId]; + fsql.Update().SetSource(dept).ExecuteAffrows(); + } + } + + // 机组车台 + foreach (var j in JizuData) + { + if (idMap.ContainsKey(j.DeptId)) + { + fsql.Insert(new JizuChetai + { + DepartmentId = idMap[j.DeptId], + Jizu = j.Jizu, + ChetaiStart = j.Start, + ChetaiEnd = j.End + }).ExecuteAffrows(); + } + } + } + + // ==================== 职工 ==================== + private static readonly (long DeptId, string Name, string Phone, string Login)[] EmpData = + { + (16043931070369792, "主干-何启旭", "18840854628", "18840854628"), + (10920411881541632, "测试用户", "0", "test"), + (10920411881541632, "秦燊", "13982683923", "13982683923"), + (10920411881541632, "陈勇", "18908283908", "18908283908"), + (22305758536273920, "汪菊", "18782619006", "18782619006"), + (22305841730293760, "陈林", "18580851234", "18580851234"), + (10929439026188288, "李正平", "15378405846", "15378405846"), + (22305637455106048, "朱莹", "18908283738", "18908283738"), + (22305637455106048, "毛银春", "18180268728", "18180268728"), + (22305637455106048, "陆国君", "13696173949", "13696173949"), + (22305637455106048, "华静", "13882636653", "13882636653"), + (22305637455106048, "余思洁", "18398508220", "18398508220"), + (22305758536273920, "孙瑞", "15982664679", "15982664679"), + (22305758536273920, "唐琳", "18048103256", "18048103256"), + (22305758536273920, "蒋宜", "18982626800", "18982626800"), + (22305758536273920, "王清华", "15282625323", "15282625323"), + (22305998261719040, "李西宴", "13688261711", "13688261711"), + (22305998261719040, "文杨", "13699654826", "13699654826"), + (22306589042020352, "曹庆", "13541993164", "13541993164"), + (22306677265010688, "蒙海波", "15884923167", "15884923167"), + (22305841730293760, "陈菊", "13696182387", "13696182387"), + (22305841730293760, "刘婷", "18382610528", "18382610528"), + (19667471837433856, "主干-张建", "15398380348", "15398380348"), + (19667471837433856, "主干-严鹏", "19162687174", "19162687174"), + (10929739564847104, "游建梅", "13629091099", "13629091099"), + (22307875854487552, "邓滟旖", "18181821168", "18181821168"), + (10929439026188288, "李志颖", "14708217874", "14708217874"), + (22308241237086208, "曹志君", "13882695209", "13882695209"), + (22308241237086208, "胡小琴", "18160129785", "18160129785"), + (22308361408090112, "唐小红", "15983458330", "15983458330"), + (25256354205667328, "余华", "13547542219", "13547542219"), + (22308683820044288, "陈建红", "15182698365", "15182698365"), + (22308683820044288, "谢军辉", "19957339779", "19957339779"), + (22308600831545344, "陈丽华", "15982605862", "15982605862"), + (16047387197837312, "杨静", "15982696700", "15982696700"), + (25256354205667328, "刘芳", "13882653025", "13882653025"), + (22308805823959040, "刘春兰", "13982624682", "13982624682"), + (22309110082965504, "朱再民", "15328288332", "15328288332"), + (22309340027293696, "舒文", "13551613659", "13551613659"), + (10929439026188288, "汪瑜琳", "13541553564", "13541553564"), + (19667471837433856, "范鸿才", "18981977117", "18981977117"), + (22305758536273920, "谭波", "18282636380", "18282636380"), + (22308918461992960, "董宗华", "18384503138", "18384503138"), + (22308918461992960, "易林梅", "18381112479", "18381112479"), + (22309003073687552, "舒红群", "18398526317", "18398526317"), + (25379428280307712, "彭卫东", "18382668003", "18382668003"), + (25379428280307712, "王远辉", "15982666491", "15982666491"), + (25379428280307712, "姚光林", "19160637815", "19160637815"), + (19667471837433856, "李学兴", "18117362915", "18117362915"), + (16043931070369792, "李远玲", "13982638574", "13982638574"), + (26900271946797056, "杨福锋", "13982638165", "13982638165"), + (26901341515288576, "杨利容", "15928659870", "15928659870"), + (19667471837433856, "主干-张罗", "18109064045", "18109064045"), + (26900271946797056, "袁鑫", "14781753888", "14781753888"), + (19667471837433856, "刘香君", "15808107846", "15808107846"), + (22308805823959040, "沈跃成", "13822244141", "13822244141"), + (19667471837433856, "杨凤琼", "18011528128", "18011528128"), + (22308600831545344, "谭爱君", "13699658634", "13699658634"), + }; + + private static void SeedEmployees(IFreeSql fsql) + { + var defaultPwd = BCrypt.Net.BCrypt.HashPassword("123456"); + var deptNameMap = DeptData.ToDictionary(d => d.Id, d => d.Name); + + foreach (var e in EmpData) + { + if (fsql.Select().Any(x => x.Username == e.Login)) continue; + + long deptId = 0; + if (deptNameMap.TryGetValue(e.DeptId, out var deptName)) + { + var dept = fsql.Select().Where(d => d.Name == deptName).First(); + if (dept != null) deptId = dept.Id; + } + + fsql.Insert(new Employee + { + Username = e.Login, + Password = defaultPwd, + RealName = e.Name, + Nickname = e.Name, + Phone = e.Phone, + DepartmentId = deptId, + Status = 1 + }).ExecuteAffrows(); + } + } + + // ==================== 菜单 ==================== + private static void SeedMenus(IFreeSql fsql) + { + var baseDirId = fsql.Select().Where(m => m.Path == "/base").First()?.Id ?? 0; + if (baseDirId == 0) + { + baseDirId = DbSeeder.InsertAndGetId(fsql, new Menu { Name = "基础数据", Type = "directory", Path = "/base", Icon = "Files", Sort = 3 }); + } + + AddMenu(fsql, baseDirId, "工艺庄口", "/base/zhuangkou", "base/zhuangkou/index", "Collection", "base:zhuangkou:list", 1); + AddMenu(fsql, baseDirId, "生丝规格", "/base/guige", "base/guige/index", "Grid", "base:guige:list", 2); + AddMenu(fsql, baseDirId, "生丝定级指标", "/base/dingjizhibiao", "base/dingjizhibiao/index", "TrendCharts", "base:dingjizhibiao:list", 3); + AddMenu(fsql, baseDirId, "机组车台", "/base/jizuchetai", "base/jizuchetai/index", "SetUp", "base:jizuchetai:list", 4); + + // 生产管理目录 + var prodDirId = fsql.Select().Where(m => m.Path == "/prod").First()?.Id ?? 0; + if (prodDirId == 0) + { + prodDirId = DbSeeder.InsertAndGetId(fsql, new Menu { Name = "生产管理", Type = "directory", Path = "/prod", Icon = "Cpu", Sort = 4 }); + } + AddMenu(fsql, prodDirId, "落丝记录", "/prod/lsjl", "prod/lsjl/index", "Document", "prod:lsjl:list", 1); + AddMenu(fsql, prodDirId, "纤度检验", "/prod/xdjy", "prod/xdjy/index", "EditPen", "prod:xdjy:list", 2); + AddMenu(fsql, prodDirId, "黑板检验", "/prod/hbjy", "prod/hbjy/index", "Notebook", "prod:hbjy:list", 3); + + var adminRole = fsql.Select().Where(r => r.Code == "admin").First(); + if (adminRole != null) + { + var baseMenus = fsql.Select().Where(m => m.Path.StartsWith("/base") || m.Path.StartsWith("/prod")).ToList(); + foreach (var m in baseMenus) + { + if (!fsql.Select().Any(rm => rm.RoleId == adminRole.Id && rm.MenuId == m.Id)) + fsql.Insert(new RoleMenu { RoleId = adminRole.Id, MenuId = m.Id }).ExecuteAffrows(); + } + } + } + + private static void AddMenu(IFreeSql fsql, long parentId, string name, string path, string component, string icon, string permission, int sort) + { + if (!fsql.Select().Any(m => m.Path == path)) + { + fsql.Insert(new Menu { ParentId = parentId, Name = name, Type = "menu", Path = path, Component = component, Icon = icon, Permission = permission, Sort = sort }).ExecuteAffrows(); + } + } + + private static void SeedCraftZhuangKou(IFreeSql fsql) + { + if (fsql.Select().Any()) return; + fsql.Insert(new CraftZhuangKou[] + { + new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 1, JidiWaigou = "基地", ZhuangKouCode = "SZJ-2026-武工艺20", ZhuangKouName = "2026年基地春一批(青松皓月)", ProductionDisplayName = "2026年基地春一批(青松皓月)", Chandi = "基地", Sort = 20 }, + new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 1, JidiWaigou = "基地", ZhuangKouCode = "SZJ-2026-武工艺24", ZhuangKouName = "2026年基地春(雄)", ProductionDisplayName = "2026年基地春(雄)", Chandi = "大面积", Sort = 24 }, + new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 2, JidiWaigou = "外购", ZhuangKouCode = "SZJ-2026-武工艺-22", ZhuangKouName = "2026年会东春(7532)", ProductionDisplayName = "2026年会东春(7532)", Chandi = "会东", Sort = 22 }, + new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 2, JidiWaigou = "外购", ZhuangKouCode = "SZJ-2026-武工艺-23", ZhuangKouName = "2026年珙县九丝春(雄)", ProductionDisplayName = "2026年珙县九丝春(雄)", Chandi = "珙县", Sort = 23 }, + new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 2, JidiWaigou = "外购", ZhuangKouCode = "SZJ-2026-武工艺-7", ZhuangKouName = "2026会东二类茧(7532)宁兴", ProductionDisplayName = "2026会东二类茧(7532)宁兴", Chandi = "会东", Sort = 7 }, + new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 1, JidiWaigou = "基地", ZhuangKouCode = "SZJ-武工艺-2026-25", ZhuangKouName = "2026年广安春(青松皓月)", ProductionDisplayName = "2026年广安春(青松皓月)", Chandi = "基地", Sort = 25 }, + new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 2, JidiWaigou = "外购", ZhuangKouCode = "SZJ-2026-武工艺-26", ZhuangKouName = "2026年珙县罗渡春(雄)", ProductionDisplayName = "2026年珙县罗渡春(雄)", Chandi = "珙县", Sort = 26 }, + new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 1, JidiWaigou = "基地", ZhuangKouCode = "SZJ-武工艺-2026-9", ZhuangKouName = "2026年广安春(青松皓月)宁兴", ProductionDisplayName = "2026年广安春(青松皓月)宁兴", Chandi = "广安", Sort = 9 }, + new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 1, JidiWaigou = "基地", ZhuangKouCode = "SZJ-武工艺-2026-28", ZhuangKouName = "2026年基地春三批(青松皓月)", ProductionDisplayName = "2026年基地春三批(青松皓月)", Chandi = "基地", Sort = 28 }, + new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 1, JidiWaigou = "基地", ZhuangKouCode = "SZJ-武工艺-2026-11", ZhuangKouName = "2026年基地春三批(青松皓月)宁兴", ProductionDisplayName = "2026年基地春三批(青松皓月)宁兴", Chandi = "基地", Sort = 11 }, + new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 2, JidiWaigou = "外购", ZhuangKouCode = "SZJ-武工艺-2026-29", ZhuangKouName = "2026年珙县宝山等混六庄春一批(雄)", ProductionDisplayName = "2026年珙县宝山等混六庄春一批(雄)", Chandi = "珙县", Sort = 29 }, + new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 2, JidiWaigou = "外购", ZhuangKouCode = "SZJ-武工艺-2026-27", ZhuangKouName = "2026年珙县曹营等混四庄春一批(雄)", ProductionDisplayName = "2026年珙县曹营等混四庄春一批(雄)", Chandi = "珙县", Sort = 27 }, + new CraftZhuangKou { BelongingYear = 2026, BendiWaidi = 2, JidiWaigou = "外购", ZhuangKouCode = "SZJ-武工艺-2026-10", ZhuangKouName = "2026年珙县罗渡春(雄)宁兴", ProductionDisplayName = "2026年珙县罗渡春(雄)宁兴", Chandi = "珙县", Sort = 10 } + }).ExecuteAffrows(); + } + + private static void SeedShengsiGuige(IFreeSql fsql) + { + if (fsql.Select().Any()) return; + fsql.Insert(new ShengsiGuige[] + { + new ShengsiGuige { Guige = "20/22", CenterrangeLolimit = 19.5, CenterrangeUplimit = 21.5, CenterXiandu = 20.5, ValidLolimit = 15.0, ValidUplimit = 27.0, XiaoyeLolimit = 17.5, XiaoyeUplimit = 24.5, DayeLolimit = 16.5, DayeUplimit = 25.5, TeyeLolimit = 15.5, TeyeUplimit = 26.5, YexianNkLolimitLeft = 15.0, YexianNkLolimitRight = 17.5, YexianNkUplimitLeft = 24.0, YexianNkUplimitRight = 27.5, LaodongJingsaiYexianLolimit = 17.5, LaodongJingsaiYexianUplimit = 23.5, Sort = 0 }, + new ShengsiGuige { Guige = "27/29", CenterrangeLolimit = 26.5, CenterrangeUplimit = 28.5, CenterXiandu = 27.5, ValidLolimit = 23.0, ValidUplimit = 34.0, XiaoyeLolimit = 25.0, XiaoyeUplimit = 32.0, DayeLolimit = 24.0, DayeUplimit = 33.0, TeyeLolimit = 23.0, TeyeUplimit = 34.0, YexianNkLolimitLeft = 22.0, YexianNkLolimitRight = 24.5, YexianNkUplimitLeft = 31.0, YexianNkUplimitRight = 34.5, LaodongJingsaiYexianLolimit = 24.5, LaodongJingsaiYexianUplimit = 30.5, Sort = 100 }, + new ShengsiGuige { Guige = "40/44", CenterrangeLolimit = 40.5, CenterrangeUplimit = 42.5, CenterXiandu = 41.5, ValidLolimit = 23.0, ValidUplimit = 34.0, XiaoyeLolimit = 35.0, XiaoyeUplimit = 49.0, DayeLolimit = 33.0, DayeUplimit = 51.0, TeyeLolimit = 31.0, TeyeUplimit = 53.0, YexianNkLolimitLeft = 0.0, YexianNkLolimitRight = 0.0, YexianNkUplimitLeft = 0.0, YexianNkUplimitRight = 0.0, LaodongJingsaiYexianLolimit = 40.0, LaodongJingsaiYexianUplimit = 42.0, Sort = 200 } + }).ExecuteAffrows(); + } + + private static void SeedShengsiDingjizhibiao(IFreeSql fsql) + { + if (fsql.Select().Any()) return; + + var list = new List(); + void Add(int std, string guige, string grade, double score, double piancha, double dacha, double erdu, double sandu, double qingjie, double jiejing) + { + list.Add(new ShengsiDingjizhibiao { Standard = std, Guige = guige, Grade = grade, GradeScore = score, Piancha = piancha, Dacha = dacha, Erdu = erdu, Sandu = sandu, Qingjie = qingjie, Jiejing = jiejing }); + } + + // ===== 厂标 (standard=0) ===== + Add(0, "20/22", "A", 7.0, 2.0, 6.0, 6.0, 2.0, 90.0, 86.0); + Add(0, "20/22", "A50", 7.5, 1.85, 5.6, 6.0, 2.0, 91.5, 87.0); + Add(0, "20/22", "2A", 8.0, 1.7, 5.2, 5.0, 1.0, 93.0, 88.0); + Add(0, "20/22", "2A50", 8.5, 1.58, 4.8, 5.0, 0.0, 94.5, 89.0); + Add(0, "20/22", "3A", 9.0, 1.45, 4.4, 4.0, 0.0, 95.0, 90.0); + Add(0, "20/22", "3A50", 9.5, 1.33, 4.0, 4.0, 0.0, 96.0, 91.0); + Add(0, "20/22", "4A", 10.0, 1.2, 3.6, 3.0, 0.0, 96.5, 92.0); + Add(0, "20/22", "4A50", 10.5, 1.13, 3.3, 3.0, 0.0, 97.0, 93.0); + Add(0, "20/22", "5A", 11.0, 1.05, 3.0, 2.0, 0.0, 97.5, 94.0); + Add(0, "20/22", "5A50", 11.5, 0.98, 2.8, 2.0, 0.0, 97.8, 94.5); + Add(0, "20/22", "6A", 12.0, 0.9, 2.6, 1.0, 0.0, 98.0, 95.0); + Add(0, "27/29", "A", 7.0, 2.05, 6.1, 6.0, 0.0, 90.0, 86.0); + Add(0, "27/29", "A50", 7.5, 1.9, 5.6, 6.0, 0.0, 91.5, 87.0); + Add(0, "27/29", "2A", 8.0, 1.75, 5.1, 5.0, 0.0, 93.0, 88.0); + Add(0, "27/29", "2A50", 8.5, 1.65, 4.7, 5.0, 0.0, 94.5, 89.0); + Add(0, "27/29", "3A", 9.0, 1.55, 4.3, 4.0, 0.0, 95.0, 90.0); + Add(0, "27/29", "3A50", 9.5, 1.45, 4.0, 4.0, 0.0, 96.0, 91.0); + Add(0, "27/29", "4A", 10.0, 1.3, 3.7, 3.0, 0.0, 96.5, 92.0); + Add(0, "27/29", "4A50", 10.5, 1.25, 3.5, 3.0, 0.0, 97.0, 93.0); + Add(0, "27/29", "5A", 11.0, 1.15, 3.3, 2.0, 0.0, 97.5, 94.0); + Add(0, "27/29", "5A50", 11.5, 1.1, 3.1, 2.0, 0.0, 97.8, 94.5); + Add(0, "27/29", "6A", 12.0, 1.05, 3.0, 1.0, 0.0, 98.0, 95.0); + Add(0, "40/44", "A", 7.0, 2.85, 8.45, 0.0, 0.0, 0.0, 0.0); + Add(0, "40/44", "A50", 7.5, 2.69, 7.88, 0.0, 0.0, 0.0, 0.0); + Add(0, "40/44", "2A", 8.0, 2.53, 7.3, 0.0, 0.0, 0.0, 0.0); + Add(0, "40/44", "2A50", 8.5, 2.36, 6.79, 0.0, 0.0, 0.0, 0.0); + Add(0, "40/44", "3A", 9.0, 2.18, 6.28, 0.0, 0.0, 0.0, 0.0); + Add(0, "40/44", "3A50", 9.5, 2.04, 5.89, 0.0, 0.0, 0.0, 0.0); + Add(0, "40/44", "4A", 10.0, 1.9, 5.5, 0.0, 0.0, 0.0, 0.0); + Add(0, "40/44", "4A50", 10.5, 1.8, 5.2, 0.0, 0.0, 0.0, 0.0); + Add(0, "40/44", "5A", 11.0, 1.7, 4.9, 0.0, 0.0, 0.0, 0.0); + Add(0, "40/44", "5A50", 11.5, 1.6, 4.6, 0.0, 0.0, 0.0, 0.0); + Add(0, "40/44", "6A", 12.0, 1.5, 4.3, 0.0, 0.0, 0.0, 0.0); + Add(0, "20/22", "等外", 6.0, 9.99, 99.99, 100.0, 100.0, 0.0, 0.0); + Add(0, "27/29", "等外", 6.0, 9.99, 99.99, 100.0, 100.0, 0.0, 0.0); + Add(0, "40/44", "等外", 6.0, 9.99, 99.99, 100.0, 100.0, 0.0, 0.0); + + // ===== 个人 (standard=2) ===== + Add(2, "20/22", "A00", 7.0, 2.0, 6.0, 6.0, 2.0, 90.0, 86.0); + Add(2, "20/22", "A10", 7.1, 1.97, 5.92, 6.0, 2.0, 90.3, 86.2); + Add(2, "20/22", "A20", 7.2, 1.94, 5.84, 6.0, 2.0, 90.6, 86.4); + Add(2, "20/22", "A30", 7.3, 1.91, 5.76, 6.0, 2.0, 90.9, 86.6); + Add(2, "20/22", "A40", 7.4, 1.88, 5.68, 6.0, 2.0, 91.2, 86.8); + Add(2, "20/22", "A50", 7.5, 1.85, 5.6, 6.0, 2.0, 91.5, 87.0); + Add(2, "20/22", "A60", 7.6, 1.82, 5.52, 5.8, 1.8, 91.8, 87.2); + Add(2, "20/22", "A70", 7.7, 1.79, 5.44, 5.6, 1.6, 92.1, 87.4); + Add(2, "20/22", "A80", 7.8, 1.76, 5.36, 5.4, 1.4, 92.4, 87.6); + Add(2, "20/22", "A90", 7.9, 1.73, 5.28, 5.2, 1.2, 92.7, 87.8); + Add(2, "20/22", "2A00", 8.0, 1.7, 5.2, 5.0, 1.0, 93.0, 88.0); + Add(2, "20/22", "2A10", 8.1, 1.68, 5.12, 5.0, 0.8, 93.3, 88.2); + Add(2, "20/22", "2A20", 8.2, 1.65, 5.04, 5.0, 0.6, 93.6, 88.4); + Add(2, "20/22", "2A30", 8.3, 1.63, 4.96, 5.0, 0.4, 93.9, 88.6); + Add(2, "20/22", "2A40", 8.4, 1.6, 4.88, 5.0, 0.2, 94.2, 88.8); + Add(2, "20/22", "2A50", 8.5, 1.58, 4.8, 5.0, 0.0, 94.5, 89.0); + Add(2, "20/22", "2A60", 8.6, 1.55, 4.72, 4.8, 0.0, 94.6, 89.2); + Add(2, "20/22", "2A70", 8.7, 1.53, 4.64, 4.6, 0.0, 94.7, 89.4); + Add(2, "20/22", "2A80", 8.8, 1.5, 4.56, 4.4, 0.0, 94.8, 89.6); + Add(2, "20/22", "2A90", 8.9, 1.48, 4.48, 4.2, 0.0, 94.9, 89.8); + Add(2, "20/22", "3A00", 9.0, 1.45, 4.4, 4.0, 0.0, 95.0, 90.0); + Add(2, "20/22", "3A10", 9.1, 1.43, 4.32, 4.0, 0.0, 95.2, 90.2); + Add(2, "20/22", "3A20", 9.2, 1.4, 4.24, 4.0, 0.0, 95.4, 90.4); + Add(2, "20/22", "3A30", 9.3, 1.38, 4.16, 4.0, 0.0, 95.6, 90.6); + Add(2, "20/22", "3A40", 9.4, 1.35, 4.08, 4.0, 0.0, 95.8, 90.8); + Add(2, "20/22", "3A50", 9.5, 1.33, 4.0, 4.0, 0.0, 96.0, 91.0); + Add(2, "20/22", "3A60", 9.6, 1.3, 3.92, 3.8, 0.0, 96.1, 91.2); + Add(2, "20/22", "3A70", 9.7, 1.28, 3.84, 3.6, 0.0, 96.2, 91.4); + Add(2, "20/22", "3A80", 9.8, 1.25, 3.76, 3.4, 0.0, 96.3, 91.6); + Add(2, "20/22", "3A90", 9.9, 1.23, 3.68, 3.2, 0.0, 96.4, 91.8); + Add(2, "20/22", "4A00", 10.0, 1.2, 3.6, 3.0, 0.0, 96.5, 92.0); + Add(2, "20/22", "4A10", 10.1, 1.19, 3.54, 3.0, 0.0, 96.6, 92.2); + Add(2, "20/22", "4A20", 10.2, 1.17, 3.48, 3.0, 0.0, 96.7, 92.4); + Add(2, "20/22", "4A30", 10.3, 1.16, 3.42, 3.0, 0.0, 96.8, 92.6); + Add(2, "20/22", "4A40", 10.4, 1.14, 3.36, 3.0, 0.0, 96.9, 92.8); + Add(2, "20/22", "4A50", 10.5, 1.13, 3.3, 3.0, 0.0, 97.0, 93.0); + Add(2, "20/22", "4A60", 10.6, 1.11, 3.24, 2.8, 0.0, 97.1, 93.2); + Add(2, "20/22", "4A70", 10.7, 1.1, 3.18, 2.6, 0.0, 97.2, 93.4); + Add(2, "20/22", "4A80", 10.8, 1.08, 3.12, 2.4, 0.0, 97.3, 93.6); + Add(2, "20/22", "4A90", 10.9, 1.07, 3.06, 2.2, 0.0, 97.4, 93.8); + Add(2, "20/22", "5A00", 11.0, 1.05, 3.0, 2.0, 0.0, 97.5, 94.0); + Add(2, "20/22", "5A10", 11.1, 1.04, 2.96, 2.0, 0.0, 97.56, 94.1); + Add(2, "20/22", "5A20", 11.2, 1.03, 2.92, 2.0, 0.0, 97.62, 94.2); + Add(2, "20/22", "5A30", 11.3, 1.01, 2.88, 2.0, 0.0, 97.68, 94.3); + Add(2, "20/22", "5A40", 11.4, 0.99, 2.84, 2.0, 0.0, 97.74, 94.4); + Add(2, "20/22", "5A50", 11.5, 0.98, 2.8, 2.0, 0.0, 97.8, 94.5); + Add(2, "20/22", "5A60", 11.6, 0.96, 2.76, 1.8, 0.0, 97.84, 94.6); + Add(2, "20/22", "5A70", 11.7, 0.95, 2.72, 1.6, 0.0, 97.88, 94.7); + Add(2, "20/22", "5A80", 11.8, 0.93, 2.68, 1.4, 0.0, 97.92, 94.8); + Add(2, "20/22", "5A90", 11.9, 0.92, 2.64, 1.2, 0.0, 97.96, 94.9); + Add(2, "20/22", "6A00", 12.0, 0.9, 2.6, 1.0, 0.0, 98.0, 95.0); + + Add(2, "27/29", "A00", 7.0, 2.05, 6.0, 12.0, 0.0, 90.0, 86.0); + Add(2, "27/29", "A10", 7.1, 2.02, 5.9, 11.6, 0.0, 90.3, 86.2); + Add(2, "27/29", "A20", 7.2, 1.99, 5.8, 11.2, 0.0, 90.6, 86.4); + Add(2, "27/29", "A30", 7.3, 1.96, 5.7, 10.8, 0.0, 90.9, 86.6); + Add(2, "27/29", "A40", 7.4, 1.93, 5.6, 10.4, 0.0, 91.2, 86.8); + Add(2, "27/29", "A50", 7.5, 1.9, 5.5, 10.0, 0.0, 91.5, 87.0); + Add(2, "27/29", "A60", 7.6, 1.87, 5.4, 9.6, 0.0, 91.8, 87.2); + Add(2, "27/29", "A70", 7.7, 1.84, 5.3, 9.2, 0.0, 92.1, 87.4); + Add(2, "27/29", "A80", 7.8, 1.81, 5.2, 8.8, 0.0, 92.4, 87.6); + Add(2, "27/29", "A90", 7.9, 1.78, 5.1, 8.4, 0.0, 92.7, 87.8); + Add(2, "27/29", "2A00", 8.0, 1.75, 5.0, 8.0, 0.0, 93.0, 88.0); + Add(2, "27/29", "2A10", 8.1, 1.73, 4.92, 7.7, 0.0, 93.2, 88.2); + Add(2, "27/29", "2A20", 8.2, 1.69, 4.76, 7.1, 0.0, 93.6, 88.6); + Add(2, "27/29", "2A30", 8.3, 1.67, 4.68, 6.8, 0.0, 93.8, 88.8); + Add(2, "27/29", "2A40", 8.4, 1.65, 4.6, 6.5, 0.0, 94.0, 89.0); + Add(2, "27/29", "2A50", 8.5, 1.63, 4.52, 6.2, 0.0, 94.2, 89.2); + Add(2, "27/29", "2A60", 8.6, 1.61, 4.44, 5.9, 0.0, 94.4, 89.4); + Add(2, "27/29", "2A70", 8.7, 1.59, 4.36, 5.6, 0.0, 94.6, 89.6); + Add(2, "27/29", "2A80", 8.8, 1.57, 4.28, 5.3, 0.0, 94.8, 89.8); + Add(2, "27/29", "2A90", 8.9, 1.55, 4.2, 5.0, 0.0, 95.0, 90.0); + Add(2, "27/29", "3A00", 9.0, 1.53, 4.14, 4.8, 0.0, 95.15, 90.2); + Add(2, "27/29", "3A10", 9.1, 1.51, 4.08, 4.6, 0.0, 95.3, 90.4); + Add(2, "27/29", "3A20", 9.2, 1.49, 4.02, 4.4, 0.0, 95.45, 90.6); + Add(2, "27/29", "3A30", 9.3, 1.47, 3.96, 4.2, 0.0, 95.6, 90.8); + Add(2, "27/29", "3A40", 9.4, 1.45, 3.9, 4.0, 0.0, 95.75, 91.0); + Add(2, "27/29", "3A50", 9.5, 1.43, 3.84, 3.8, 0.0, 95.9, 91.2); + Add(2, "27/29", "3A60", 9.6, 1.41, 3.78, 3.6, 0.0, 96.05, 91.4); + Add(2, "27/29", "3A70", 9.7, 1.39, 3.72, 3.4, 0.0, 96.2, 91.6); + Add(2, "27/29", "3A80", 9.8, 1.37, 3.66, 3.2, 0.0, 96.35, 91.8); + Add(2, "27/29", "3A90", 9.9, 1.35, 3.6, 3.0, 0.0, 96.5, 92.0); + Add(2, "27/29", "4A00", 10.0, 1.33, 3.66, 2.85, 0.0, 96.6, 92.2); + Add(2, "27/29", "4A10", 10.1, 1.31, 3.62, 2.7, 0.0, 96.7, 92.4); + Add(2, "27/29", "4A20", 10.2, 1.29, 3.58, 2.55, 0.0, 96.8, 92.6); + Add(2, "27/29", "4A30", 10.3, 1.27, 3.54, 2.4, 0.0, 96.9, 92.8); + Add(2, "27/29", "4A40", 10.4, 1.25, 3.5, 2.25, 0.0, 97.0, 93.0); + Add(2, "27/29", "4A50", 10.5, 1.23, 3.46, 2.1, 0.0, 97.1, 93.2); + Add(2, "27/29", "4A60", 10.6, 1.21, 3.42, 1.95, 0.0, 97.2, 93.4); + Add(2, "27/29", "4A70", 10.7, 1.19, 3.38, 1.8, 0.0, 97.3, 93.6); + Add(2, "27/29", "4A80", 10.8, 1.17, 3.34, 1.65, 0.0, 97.4, 93.8); + Add(2, "27/29", "4A90", 10.9, 1.15, 3.3, 1.5, 0.0, 97.5, 94.0); + Add(2, "27/29", "5A00", 11.0, 1.14, 3.27, 1.45, 0.0, 97.55, 94.1); + Add(2, "27/29", "5A10", 11.1, 1.13, 3.24, 1.4, 0.0, 97.6, 94.2); + Add(2, "27/29", "5A20", 11.2, 1.12, 3.21, 1.35, 0.0, 97.65, 94.3); + Add(2, "27/29", "5A30", 11.3, 1.11, 3.18, 1.3, 0.0, 97.7, 94.4); + Add(2, "27/29", "5A40", 11.4, 1.1, 3.15, 1.25, 0.0, 97.75, 94.5); + Add(2, "27/29", "5A50", 11.5, 1.09, 3.12, 1.2, 0.0, 97.8, 94.6); + Add(2, "27/29", "5A60", 11.6, 1.08, 3.09, 1.15, 0.0, 97.85, 94.7); + Add(2, "27/29", "5A70", 11.7, 1.07, 3.06, 1.1, 0.0, 97.9, 94.8); + Add(2, "27/29", "5A80", 11.8, 1.06, 3.03, 1.05, 0.0, 97.95, 94.9); + Add(2, "27/29", "5A90", 11.9, 1.05, 3.0, 1.0, 0.0, 98.0, 95.0); + Add(2, "27/29", "6A00", 12.0, 1.05, 3.0, 1.0, 0.0, 98.0, 95.0); + + Add(2, "40/44", "A00", 7.0, 2.6, 7.55, 8.0, 0.0, 90.0, 86.0); + Add(2, "40/44", "A10", 7.1, 2.57, 7.43, 7.7, 0.0, 90.3, 86.2); + Add(2, "40/44", "A20", 7.2, 2.53, 7.31, 7.4, 0.0, 90.6, 86.4); + Add(2, "40/44", "A30", 7.3, 2.5, 7.19, 7.1, 0.0, 90.9, 86.6); + Add(2, "40/44", "A40", 7.4, 2.46, 7.07, 6.8, 0.0, 91.2, 86.8); + Add(2, "40/44", "A50", 7.5, 2.43, 6.95, 6.5, 0.0, 91.5, 87.0); + Add(2, "40/44", "A60", 7.6, 2.39, 6.83, 6.2, 0.0, 91.8, 87.2); + Add(2, "40/44", "A70", 7.7, 2.36, 6.71, 5.9, 0.0, 92.1, 87.4); + Add(2, "40/44", "A80", 7.8, 2.32, 6.59, 5.6, 0.0, 92.4, 87.6); + Add(2, "40/44", "A90", 7.9, 2.29, 6.47, 5.3, 0.0, 92.7, 87.8); + Add(2, "40/44", "2A00", 8.0, 2.25, 6.35, 5.0, 0.0, 93.0, 88.0); + Add(2, "40/44", "2A10", 8.1, 2.22, 6.24, 4.8, 0.0, 93.2, 88.2); + Add(2, "40/44", "2A20", 8.2, 2.18, 6.13, 4.6, 0.0, 93.4, 88.4); + Add(2, "40/44", "2A30", 8.3, 2.15, 6.02, 4.4, 0.0, 93.6, 88.6); + Add(2, "40/44", "2A40", 8.4, 2.11, 5.91, 4.2, 0.0, 93.8, 88.8); + Add(2, "40/44", "2A50", 8.5, 2.08, 5.8, 4.0, 0.0, 94.0, 89.0); + Add(2, "40/44", "2A60", 8.6, 2.04, 5.69, 3.8, 0.0, 94.2, 89.2); + Add(2, "40/44", "2A70", 8.7, 2.01, 5.58, 3.6, 0.0, 94.4, 89.4); + Add(2, "40/44", "2A80", 8.8, 1.97, 5.47, 3.4, 0.0, 94.6, 89.6); + Add(2, "40/44", "2A90", 8.9, 1.94, 5.36, 3.2, 0.0, 94.8, 89.8); + Add(2, "40/44", "3A00", 9.0, 1.9, 5.25, 3.0, 0.0, 95.0, 90.0); + Add(2, "40/44", "3A10", 9.1, 1.87, 5.16, 1.95, 0.0, 95.15, 90.2); + Add(2, "40/44", "3A20", 9.2, 1.83, 5.06, 1.9, 0.0, 95.3, 90.4); + Add(2, "40/44", "3A30", 9.3, 1.8, 4.97, 1.85, 0.0, 95.45, 90.6); + Add(2, "40/44", "3A40", 9.4, 1.76, 4.87, 1.8, 0.0, 95.6, 90.8); + Add(2, "40/44", "3A50", 9.5, 1.73, 4.78, 1.75, 0.0, 95.75, 91.0); + Add(2, "40/44", "3A60", 9.6, 1.69, 4.68, 1.7, 0.0, 95.9, 91.2); + Add(2, "40/44", "3A70", 9.7, 1.66, 4.59, 1.65, 0.0, 96.05, 91.4); + Add(2, "40/44", "3A80", 9.8, 1.62, 4.49, 1.6, 0.0, 96.2, 91.6); + Add(2, "40/44", "3A90", 9.9, 1.59, 4.4, 1.55, 0.0, 96.35, 91.8); + Add(2, "40/44", "4A00", 10.0, 1.55, 4.3, 1.5, 0.0, 96.5, 92.0); + Add(2, "40/44", "4A10", 10.1, 1.53, 4.24, 1.45, 0.0, 96.6, 92.2); + Add(2, "40/44", "4A20", 10.2, 1.51, 4.18, 1.4, 0.0, 96.7, 92.4); + Add(2, "40/44", "4A30", 10.3, 1.49, 4.12, 1.35, 0.0, 96.8, 92.6); + Add(2, "40/44", "4A40", 10.4, 1.47, 4.06, 1.3, 0.0, 96.9, 92.8); + Add(2, "40/44", "4A50", 10.5, 1.45, 4.0, 1.25, 0.0, 97.0, 93.0); + Add(2, "40/44", "4A60", 10.6, 1.43, 3.94, 1.2, 0.0, 97.1, 93.2); + Add(2, "40/44", "4A70", 10.7, 1.41, 3.88, 1.15, 0.0, 97.2, 93.4); + Add(2, "40/44", "4A80", 10.8, 1.39, 3.82, 1.1, 0.0, 97.3, 93.6); + Add(2, "40/44", "4A90", 10.9, 1.37, 3.76, 1.05, 0.0, 97.4, 93.8); + Add(2, "40/44", "5A00", 11.0, 1.35, 3.7, 1.0, 0.0, 97.5, 94.0); + Add(2, "40/44", "5A10", 11.1, 1.33, 3.64, 0.0, 0.0, 97.55, 94.1); + Add(2, "40/44", "5A20", 11.2, 1.31, 3.58, 0.0, 0.0, 97.6, 94.2); + Add(2, "40/44", "5A30", 11.3, 1.29, 3.52, 0.0, 0.0, 97.65, 94.3); + Add(2, "40/44", "5A40", 11.4, 1.27, 3.46, 0.0, 0.0, 97.7, 94.4); + Add(2, "40/44", "5A50", 11.5, 1.25, 3.4, 0.0, 0.0, 97.75, 94.5); + Add(2, "40/44", "5A60", 11.6, 1.23, 3.34, 0.0, 0.0, 97.8, 94.6); + Add(2, "40/44", "5A70", 11.7, 1.21, 3.28, 0.0, 0.0, 97.85, 94.7); + Add(2, "40/44", "5A80", 11.8, 1.19, 3.22, 0.0, 0.0, 97.9, 94.8); + Add(2, "40/44", "5A90", 11.9, 1.17, 3.16, 0.0, 0.0, 97.95, 94.9); + Add(2, "40/44", "6A00", 12.0, 1.15, 3.1, 0.0, 0.0, 98.0, 95.0); + Add(2, "20/22", "等外", 6.0, 9.99, 99.99, 100.0, 100.0, 0.0, 0.0); + Add(2, "27/29", "等外", 6.0, 9.99, 99.99, 100.0, 100.0, 0.0, 0.0); + Add(2, "40/44", "等外", 6.0, 9.99, 99.99, 100.0, 100.0, 0.0, 0.0); + + fsql.Insert(list).ExecuteAffrows(); + } +} diff --git a/backend/F9WebAiTest.Infrastructure/Data/DbSeeder.cs b/backend/F9WebAiTest.Infrastructure/Data/DbSeeder.cs new file mode 100644 index 0000000..fceecec --- /dev/null +++ b/backend/F9WebAiTest.Infrastructure/Data/DbSeeder.cs @@ -0,0 +1,509 @@ +using System.Data; +using FreeSql; +using F9WebAiTest.Application.Interfaces.SnowflakeID; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities; +using F9WebAiTest.Domain.Entities.Base; +using F9WebAiTest.Domain.Entities.Notice; +using F9WebAiTest.Domain.Enums.SYS.RBAC; +using SysMenu = F9WebAiTest.Domain.Entities.SYS.RBAC.Menu; +using SysPermission = F9WebAiTest.Domain.Entities.SYS.RBAC.Permission; +using SysRolePermission = F9WebAiTest.Domain.Entities.SYS.RBAC.RolePermission; + +namespace F9WebAiTest.Infrastructure.Data; + +public static class DbSeeder +{ + public static void Seed(IFreeSql fsql, ISnowflakeIdGenerator snowflake) + { + // 1. 建表(CodeFirst SyncStructure,增量同步,兼容旧库) + SyncStructure(fsql); + + // 2. 默认角色 + if (!fsql.Select().Any()) + { + fsql.Insert(new Role { Name = "超级管理员", Code = "admin", Description = "拥有所有权限", Sort = 1 }).ExecuteAffrows(); + fsql.Insert(new Role { Name = "普通用户", Code = "user", Description = "普通员工", Sort = 2 }).ExecuteAffrows(); + } + + // 3. 默认菜单(系统管理) + if (!fsql.Select().Any()) + { + var systemId = InsertAndGetId(fsql, new Menu { Name = "系统管理", Type = "directory", Path = "/system", Icon = "Setting", Sort = 1 }); + var deptId = InsertAndGetId(fsql, new Menu { Name = "部门管理", Type = "menu", Path = "/system/department", Component = "system/department/index", Icon = "OfficeBuilding", Permission = "system:department:list", Sort = 1, ParentId = systemId }); + var empId = InsertAndGetId(fsql, new Menu { Name = "职工管理", Type = "menu", Path = "/system/employee", Component = "system/employee/index", Icon = "User", Permission = "system:employee:list", Sort = 2, ParentId = systemId }); + var roleId = InsertAndGetId(fsql, new Menu { Name = "角色管理", Type = "menu", Path = "/system/role", Component = "system/role/index", Icon = "UserFilled", Permission = "system:role:list", Sort = 3, ParentId = systemId }); + var menuId = InsertAndGetId(fsql, new Menu { Name = "菜单管理", Type = "menu", Path = "/system/menu", Component = "system/menu/index", Icon = "Menu", Permission = "system:menu:list", Sort = 4, ParentId = systemId }); + var logId = InsertAndGetId(fsql, new Menu { Name = "操作日志", Type = "menu", Path = "/system/log", Component = "system/log/index", Icon = "Document", Permission = "system:log:list", Sort = 5, ParentId = systemId }); + fsql.Insert(new Menu[] + { + new Menu { Name = "部门操作", Type = "button", Permission = "system:department:btn", Sort = 1, ParentId = deptId }, + new Menu { Name = "职工操作", Type = "button", Permission = "system:employee:btn", Sort = 1, ParentId = empId }, + new Menu { Name = "角色操作", Type = "button", Permission = "system:role:btn", Sort = 1, ParentId = roleId }, + new Menu { Name = "菜单操作", Type = "button", Permission = "system:menu:btn", Sort = 1, ParentId = menuId }, + }).ExecuteAffrows(); + } + + // 4. 考勤菜单(增量) + SeedAttendanceMenus(fsql); + + // 5. 业务基础数据(部门树 + 机组车台 + 庄口 + 规格 + 定级指标 + 菜单) + BusinessSeedData.Seed(fsql); + + // 6. 通知:消息类型种子 + 菜单 + SeedNotificationData(fsql, snowflake); + + // 6.5 数据迁移:旧 RBAC 表 → 新 DDD RBAC 表(先迁移,确保 admin 角色/用户已在新表) + MigrateRbacData(fsql); + + // 6.6 F9 风格 RBAC 菜单(用户/角色/菜单/部门管理,写入新 sys_rbac_menus 表) + SeedSysRbacMenus(fsql, snowflake); + + // 6.7 为菜单创建权限单元(Permission),并修正旧菜单权限关联(统一权限模型) + FixMenuPermissions(fsql, snowflake); + + // 7. 管理员账号(幂等) + var admin = fsql.Select().Where(e => e.Username == "admin").First(); + if (admin == null) + { + var dept = fsql.Select().Where(d => d.Name == "主干运维").First() + ?? fsql.Select().First(); + var adminId = InsertAndGetId(fsql, new Employee + { + Username = "admin", + Password = BCrypt.Net.BCrypt.HashPassword("123456"), + RealName = "系统管理员", + Nickname = "管理员", + DepartmentId = dept.Id, + Email = "admin@company.com", + Phone = "13800000000", + Status = 1 + }); + admin = fsql.Select().Where(e => e.Id == adminId).First(); + } + + // 8. admin 关联 admin 角色 + var adminRole = fsql.Select().Where(r => r.Code == "admin").First(); + if (!fsql.Select().Any(er => er.EmployeeId == admin.Id && er.RoleId == adminRole.Id)) + { + fsql.Insert(new EmployeeRole { EmployeeId = admin.Id, RoleId = adminRole.Id }).ExecuteAffrows(); + } + + // 9. admin 角色分配所有菜单(幂等补充) + var allMenus = fsql.Select().ToList(); + foreach (var m in allMenus) + { + if (!fsql.Select().Any(rm => rm.RoleId == adminRole.Id && rm.MenuId == m.Id)) + { + fsql.Insert(new RoleMenu { RoleId = adminRole.Id, MenuId = m.Id }).ExecuteAffrows(); + } + } + } + + /// CodeFirst 建表(新表建表,旧表同步失败则跳过)+ 旧库加列兜底 + private static void SyncStructure(IFreeSql fsql) + { + // 扫描 BaseEntity(旧)与 Entity(新 DDD 基类)两类实体的具体子类建表 + var entityTypes = typeof(BaseEntity).Assembly.GetTypes() + .Where(t => t.IsClass && !t.IsAbstract && + (t.IsSubclassOf(typeof(BaseEntity)) || t.IsSubclassOf(typeof(Entity)))); + foreach (var t in entityTypes) + { + try + { + fsql.CodeFirst.SyncStructure(t); + } + catch (Exception ex) + { + // 旧表同步失败(如唯一索引与历史数据冲突),跳过,保持旧库兼容 + Console.WriteLine($"[DbSeeder] 表 {t.Name} 结构同步跳过:{ex.Message}"); + } + } + + // 旧库加列兜底(SyncStructure 不会为已有表加列) + EnsureColumn(fsql, "LeaveRequests", "CurrentOrderNo", "INTEGER NOT NULL DEFAULT 1"); + EnsureColumn(fsql, "NotificationTypes", "Category", "INTEGER NOT NULL DEFAULT 0"); + EnsureColumn(fsql, "NotificationTypes", "Icon", "TEXT NOT NULL DEFAULT ''"); + EnsureColumn(fsql, "NotificationTypes", "RouteTemplate", "TEXT NOT NULL DEFAULT ''"); + EnsureColumn(fsql, "NotificationMessages", "Category", "INTEGER NOT NULL DEFAULT 0"); + EnsureColumn(fsql, "NotificationMessages", "RoutePath", "TEXT NOT NULL DEFAULT ''"); + } + + /// 为已有表补加列(列已存在则忽略) + private static void EnsureColumn(IFreeSql fsql, string table, string column, string definition) + { + try + { + fsql.Ado.ExecuteNonQuery($"ALTER TABLE \"{table}\" ADD COLUMN \"{column}\" {definition};"); + } + catch + { + // 列已存在,忽略 + } + } + + /// 插入并返回自增主键 ID(SQLite 用 last_insert_rowid 可靠获取) + public static long InsertAndGetId(IFreeSql fsql, T entity) where T : BaseEntity + { + fsql.Insert(entity).ExecuteAffrows(); + return Convert.ToInt64(fsql.Ado.ExecuteScalar("SELECT last_insert_rowid();")); + } + + /// 通知种子:消息类型 + 消息中心菜单 + private static void SeedNotificationData(IFreeSql fsql, ISnowflakeIdGenerator snowflake) + { + // 消息类型:发现特野 + if (!fsql.Select().Any(t => t.Code == "TeyeFound")) + { + fsql.Insert(new NotificationType + { + Code = "TeyeFound", Name = "发现特野", Description = "纤度检验发现特野时自动推送提醒", + RequiredPermission = "", IsActive = true, CreatedBy = 0, Sort = 1, + Category = NotificationCategory.Notification, Icon = "Warning", RouteTemplate = "/prod/xdjy" + }).ExecuteAffrows(); + } + + // 消息类型:发现特大糙 + if (!fsql.Select().Any(t => t.Code == "TdcFound")) + { + fsql.Insert(new NotificationType + { + Code = "TdcFound", Name = "发现特大糙", Description = "黑板检验发现特大糙时自动推送提醒", + RequiredPermission = "", IsActive = true, CreatedBy = 0, Sort = 2, + Category = NotificationCategory.Notification, Icon = "Warning", RouteTemplate = "/prod/hbjy" + }).ExecuteAffrows(); + } + + // 消息类型:请假审批(订阅权限用独立码,避免与业务按钮权限冲突) + if (!fsql.Select().Any(t => t.Code == "LeaveApproval")) + { + fsql.Insert(new NotificationType + { + Code = "LeaveApproval", Name = "请假审批", Description = "员工提交请假申请后,推送待办给审批人", + RequiredPermission = "notification:subscribe:leaveapproval", IsActive = true, CreatedBy = 0, Sort = 3, + Category = NotificationCategory.Todo, Icon = "Checked", RouteTemplate = "/attendance/approve?leaveId={businessId}" + }).ExecuteAffrows(); + } + + // 消息类型:请假结果通知 + if (!fsql.Select().Any(t => t.Code == "LeaveResult")) + { + fsql.Insert(new NotificationType + { + Code = "LeaveResult", Name = "请假结果通知", Description = "请假审批完成后通知申请人结果", + RequiredPermission = "", IsActive = true, CreatedBy = 0, Sort = 4, + Category = NotificationCategory.Notification, Icon = "Message", RouteTemplate = "/attendance/my" + }).ExecuteAffrows(); + } + + // 消息类型:广播通知(有权限的用户主动发送的广播,推送给所有订阅者) + if (!fsql.Select().Any(t => t.Code == "Broadcast")) + { + fsql.Insert(new NotificationType + { + Code = "Broadcast", Name = "广播通知", Description = "用户发送的广播通知,推送给所有订阅该类型的用户", + RequiredPermission = "", IsActive = true, CreatedBy = 0, Sort = 5, + Category = NotificationCategory.Announcement, Icon = "Bell", RouteTemplate = "" + }).ExecuteAffrows(); + } + + // 广播发送权限单元(ResourceType=操作,Code=notification:broadcast,admin 自动拥有,普通用户可在角色管理分配) + var broadcastPerm = fsql.Select() + .Where(p => p.Code == "notification:broadcast").First(); + if (broadcastPerm == null) + { + var perm = new SysPermission(snowflake.CreateId(), Permission_Type_Enum.Enable, Resource_Type_Enum.操作, "broadcast", 0); + perm.SetCode("notification:broadcast"); + fsql.Insert(perm).ExecuteAffrows(); + } + + // 菜单:消息中心 + var notiDirId = fsql.Select().Where(m => m.Path == "/notification").First()?.Id ?? 0; + if (notiDirId == 0) + { + notiDirId = InsertAndGetId(fsql, new Menu { Name = "消息中心", Type = "directory", Path = "/notification", Icon = "Bell", Sort = 5 }); + } + + AddNotificationMenu(fsql, notiDirId, "我的消息", "/notification/messages", "notification/messages/index", "Message", "notification:messages:list", 1); + AddNotificationMenu(fsql, notiDirId, "消息订阅", "/notification/subscribe", "notification/subscribe/index", "BellFilled", "notification:subscribe:list", 2); + AddNotificationMenu(fsql, notiDirId, "消息类型管理", "/notification/type", "notification/type/index", "SetUp", "notification:type:list", 3); + + // 清理历史遗留:发现特野/发现特大糙 改为「所有人可订阅」 + foreach (var (code, oldPerm) in new[] { ("TeyeFound", "notification:subscribe:foundteye"), ("TdcFound", "notification:subscribe:foundtedacao") }) + { + var t = fsql.Select().Where(x => x.Code == code).First(); + if (t == null) continue; + + // 1. RequiredPermission 改为空(所有人可订阅) + if (!string.IsNullOrEmpty(t.RequiredPermission)) + { + t.RequiredPermission = ""; + fsql.Update().SetSource(t).ExecuteAffrows(); + } + + // 2. 删除订阅权限单元及其角色关联(新 Permission 表) + var perm = fsql.Select() + .Where(p => p.ResourceType == Resource_Type_Enum.消息订阅 && p.ResourceId == t.Id).First(); + if (perm != null) + { + fsql.Delete().Where(rp => rp.PermissionId == perm.Id).ExecuteAffrows(); + fsql.Delete().Where(p => p.Id == perm.Id).ExecuteAffrows(); + } + + // 3. 删除历史遗留的旧"订阅按钮"菜单及其角色关联(新表 sys_rbac_menus + 旧表 Menus) + fsql.Ado.ExecuteNonQuery($@" + DELETE FROM sys_rbac_role_permissions WHERE permission_id IN + (SELECT id FROM sys_rbac_menus WHERE permission = '{oldPerm}'); + DELETE FROM sys_rbac_menus WHERE permission = '{oldPerm}'; + DELETE FROM RoleMenus WHERE MenuId IN (SELECT Id FROM Menus WHERE Permission = '{oldPerm}'); + DELETE FROM Menus WHERE Permission = '{oldPerm}';"); + } + + // 清理所有旧的"订阅按钮"菜单(已被 Permission 表的消息订阅权限替代,避免与消息订阅权限重复冗余) + fsql.Ado.ExecuteNonQuery(@" + DELETE FROM sys_rbac_role_permissions WHERE permission_id IN + (SELECT id FROM sys_rbac_menus WHERE name LIKE '订阅:%' AND menu_type = 0); + DELETE FROM sys_rbac_menus WHERE name LIKE '订阅:%' AND menu_type = 0; + DELETE FROM RoleMenus WHERE MenuId IN (SELECT Id FROM Menus WHERE Name LIKE '订阅:%' AND Type = 'button'); + DELETE FROM Menus WHERE Name LIKE '订阅:%' AND Type = 'button';"); + + // 清理 RequiredPermission 为空的消息类型的订阅权限(历史遗留,所有人可订阅无需权限单元) + foreach (var t in fsql.Select().Where(x => x.RequiredPermission == "").ToList()) + { + var perm = fsql.Select() + .Where(p => p.ResourceType == Resource_Type_Enum.消息订阅 && p.ResourceId == t.Id).First(); + if (perm != null) + { + fsql.Delete().Where(rp => rp.PermissionId == perm.Id).ExecuteAffrows(); + fsql.Delete().Where(p => p.Id == perm.Id).ExecuteAffrows(); + } + } + + // 同步订阅权限到 RBAC 权限单元(ResourceType=消息订阅,Code=RequiredPermission) + foreach (var t in fsql.Select().ToList()) + { + if (string.IsNullOrEmpty(t.RequiredPermission)) continue; + var existPerm = fsql.Select() + .Where(p => p.ResourceType == Resource_Type_Enum.消息订阅 && p.ResourceId == t.Id).First(); + if (existPerm == null) + { + var perm = new SysPermission(snowflake.CreateId(), Permission_Type_Enum.Visble, Resource_Type_Enum.消息订阅, t.Code, t.Id); + perm.SetCode(t.RequiredPermission); + fsql.Insert(perm).ExecuteAffrows(); + } + else if (existPerm.Code != t.RequiredPermission) + { + // 修正 Code 为业务权限码(覆盖旧的自动生成码) + existPerm.SetCode(t.RequiredPermission); + fsql.Update().SetSource(existPerm).ExecuteAffrows(); + } + } + + // admin 角色分配消息中心菜单权限 + var adminRole = fsql.Select().Where(r => r.Code == "admin").First(); + if (adminRole != null) + { + var notiMenus = fsql.Select().Where(m => m.Path.StartsWith("/notification")).ToList(); + foreach (var m in notiMenus) + { + if (!fsql.Select().Any(rm => rm.RoleId == adminRole.Id && rm.MenuId == m.Id)) + fsql.Insert(new RoleMenu { RoleId = adminRole.Id, MenuId = m.Id }).ExecuteAffrows(); + } + } + } + + private static void AddNotificationMenu(IFreeSql fsql, long parentId, string name, string path, string component, string icon, string permission, int sort) + { + if (!fsql.Select().Any(m => m.Path == path)) + { + fsql.Insert(new Menu { ParentId = parentId, Name = name, Type = "menu", Path = path, Component = component, Icon = icon, Permission = permission, Sort = sort }).ExecuteAffrows(); + } + } + + private static void SeedAttendanceMenus(IFreeSql fsql) + { + var attDirId = fsql.Select().Where(m => m.Path == "/attendance").First()?.Id ?? 0; + if (attDirId == 0) + { + attDirId = InsertAndGetId(fsql, new Menu { Name = "考勤管理", Type = "directory", Path = "/attendance", Icon = "Clock", Sort = 2 }); + } + + void AddMenu(string name, string path, string component, string icon, string permission, int sort) + { + if (!fsql.Select().Any(m => m.Path == path)) + { + fsql.Insert(new Menu { Name = name, Type = "menu", Path = path, Component = component, Icon = icon, Permission = permission, Sort = sort, ParentId = attDirId }).ExecuteAffrows(); + } + } + AddMenu("考勤打卡", "/attendance/punch", "attendance/punch/index", "AlarmClock", "attendance:punch:list", 1); + AddMenu("我的考勤", "/attendance/my", "attendance/my/index", "Calendar", "attendance:my:list", 2); + AddMenu("请假申请", "/attendance/leave", "attendance/leave/index", "Suitcase", "attendance:leave:list", 3); + AddMenu("考勤记录", "/attendance/records", "attendance/records/index", "List", "attendance:records:list", 4); + AddMenu("请假审批", "/attendance/approve", "attendance/approve/index", "Checked", "attendance:approve:list", 5); + + void AddButton(string menuPath, string name, string permission) + { + var menu = fsql.Select().Where(m => m.Path == menuPath).First(); + if (menu != null && !fsql.Select().Any(m => m.Permission == permission)) + { + fsql.Insert(new Menu { Name = name, Type = "button", Permission = permission, Sort = 1, ParentId = menu.Id }).ExecuteAffrows(); + } + } + AddButton("/attendance/punch", "打卡操作", "attendance:punch:btn"); + AddButton("/attendance/leave", "请假操作", "attendance:leave:btn"); + AddButton("/attendance/approve", "审批操作", "attendance:approve:btn"); + + var adminRole = fsql.Select().Where(r => r.Code == "admin").First(); + if (adminRole != null) + { + var attMenus = fsql.Select().Where(m => m.Path.StartsWith("/attendance") || m.Permission.StartsWith("attendance:")).ToList(); + foreach (var m in attMenus) + { + if (!fsql.Select().Any(rm => rm.RoleId == adminRole.Id && rm.MenuId == m.Id)) + fsql.Insert(new RoleMenu { RoleId = adminRole.Id, MenuId = m.Id }).ExecuteAffrows(); + } + } + } + + /// 数据迁移:旧 RBAC 表 → 新 DDD RBAC 表(幂等,仅迁移尚不存在的记录) + private static void MigrateRbacData(IFreeSql fsql) + { + try + { + // 1. 部门:Departments → sys_departments + fsql.Ado.ExecuteNonQuery(@" + INSERT INTO sys_departments (id, code, name, parent_id, manage_type, duty_type, description, is_active, sort_order, created_by, created_at, update_by, updated_at, is_deleted) + SELECT Id, COALESCE(Code, ''), Name, COALESCE(ParentId, 0), 10, 5, '', Status, Sort, '', COALESCE(CreatedAt, datetime('now')), '', UpdatedAt, IsDeleted + FROM Departments WHERE Id NOT IN (SELECT id FROM sys_departments);"); + + // 2. 角色:Roles → sys_rbac_roles + fsql.Ado.ExecuteNonQuery(@" + INSERT INTO sys_rbac_roles (id, name, code, sort_order, description, is_active, created_by, created_at, update_by, updated_at, is_deleted) + SELECT Id, Name, Code, Sort, COALESCE(Description, ''), Status, '', COALESCE(CreatedAt, datetime('now')), '', UpdatedAt, IsDeleted + FROM Roles WHERE Id NOT IN (SELECT id FROM sys_rbac_roles);"); + + // 3. 菜单:Menus → sys_rbac_menus + fsql.Ado.ExecuteNonQuery(@" + INSERT INTO sys_rbac_menus (id, icon_svg, code, name, menu_type, assembly_name, type_full_name, parent_id, level, is_active, sort_order, path, component, permission, created_by, created_at, update_by, updated_at, is_deleted) + SELECT Id, COALESCE(Icon, ''), '', Name, CASE WHEN Type = 'directory' THEN 1 WHEN Type = 'menu' THEN 2 ELSE 0 END, '', '', COALESCE(ParentId, 0), 1, Status, Sort, COALESCE(Path, ''), COALESCE(Component, ''), COALESCE(Permission, ''), '', COALESCE(CreatedAt, datetime('now')), '', UpdatedAt, IsDeleted + FROM Menus WHERE Id NOT IN (SELECT id FROM sys_rbac_menus);"); + + // 4. 用户:Employees → sys_rbac_users + fsql.Ado.ExecuteNonQuery(@" + INSERT INTO sys_rbac_users (id, department_id, user_name, phone_number, login_name, password_hash, is_active, sort_order, lastlogin_at, nickname, avatar, email, gender, created_by, created_at, update_by, updated_at, is_deleted) + SELECT Id, DepartmentId, RealName, COALESCE(Phone, ''), Username, Password, Status, 0, NULL, COALESCE(Nickname, ''), COALESCE(Avatar, ''), COALESCE(Email, ''), COALESCE(Gender, '男'), '', COALESCE(CreatedAt, datetime('now')), '', UpdatedAt, IsDeleted + FROM Employees WHERE Id NOT IN (SELECT id FROM sys_rbac_users);"); + + // 5. 用户角色:EmployeeRoles → sys_rbac_user_roles + fsql.Ado.ExecuteNonQuery(@" + INSERT INTO sys_rbac_user_roles (id, user_id, role_id, created_by, created_at, update_by, updated_at, is_deleted) + SELECT Id, EmployeeId, RoleId, '', COALESCE(CreatedAt, datetime('now')), '', UpdatedAt, IsDeleted + FROM EmployeeRoles WHERE Id NOT IN (SELECT id FROM sys_rbac_user_roles);"); + + // 6. 角色权限:RoleMenus → sys_rbac_role_permissions + fsql.Ado.ExecuteNonQuery(@" + INSERT INTO sys_rbac_role_permissions (id, role_id, permission_id, created_by, created_at, update_by, updated_at, is_deleted) + SELECT Id, RoleId, MenuId, '', COALESCE(CreatedAt, datetime('now')), '', UpdatedAt, IsDeleted + FROM RoleMenus WHERE Id NOT IN (SELECT id FROM sys_rbac_role_permissions);"); + + Console.WriteLine("[DbSeeder] RBAC 数据迁移完成"); + } + catch (Exception ex) + { + Console.WriteLine($"[DbSeeder] RBAC 数据迁移跳过:{ex.Message}"); + } + } + + /// F9 风格 RBAC 前端菜单种子(写入新 sys_rbac_menus 表,驱动前端导航) + private static void SeedSysRbacMenus(IFreeSql fsql, ISnowflakeIdGenerator snowflake) + { + try + { + // 删除旧"系统管理"菜单(/system/*,对接已下线的旧接口),仅保留新的 /sys 管理菜单 + fsql.Ado.ExecuteNonQuery("DELETE FROM sys_rbac_role_permissions WHERE permission_id IN (SELECT id FROM sys_rbac_menus WHERE path LIKE '/system%')"); + fsql.Ado.ExecuteNonQuery("DELETE FROM sys_rbac_menus WHERE path LIKE '/system%'"); + + // sys 目录 + long sysDirId; + var sysDir = fsql.Ado.ExecuteDataTable("SELECT id FROM sys_rbac_menus WHERE path = '/sys'"); + if (sysDir.Rows.Count == 0) + { + sysDirId = snowflake.CreateId(); + fsql.Ado.ExecuteNonQuery($"INSERT INTO sys_rbac_menus (id, icon_svg, name, menu_type, parent_id, level, is_active, sort_order, path, component, permission) VALUES ({sysDirId}, 'UserFilled', '系统管理(RBAC)', 1, 0, 1, 0, 6, '/sys', '', '')"); + } + else + { + sysDirId = Convert.ToInt64(sysDir.Rows[0]["id"]); + } + + void AddMenu(string name, string path, string component, string icon, string permission, int sort) + { + var exist = fsql.Ado.ExecuteDataTable($"SELECT id FROM sys_rbac_menus WHERE path = '{path}'"); + if (exist.Rows.Count > 0) return; + var id = snowflake.CreateId(); + fsql.Ado.ExecuteNonQuery($"INSERT INTO sys_rbac_menus (id, icon_svg, name, menu_type, parent_id, level, is_active, sort_order, path, component, permission) VALUES ({id}, '{icon}', '{name}', 2, {sysDirId}, 2, 0, {sort}, '{path}', '{component}', '{permission}')"); + } + + AddMenu("用户管理", "/sys/user", "sys/user/index", "User", "sys:user:list", 1); + AddMenu("角色管理", "/sys/role", "sys/role/index", "UserFilled", "sys:role:list", 2); + AddMenu("菜单管理", "/sys/menu", "sys/menu/index", "Menu", "sys:menu:list", 3); + AddMenu("部门管理", "/sys/department", "sys/department/index", "OfficeBuilding", "sys:department:list", 4); + + // admin 角色分配 sys 菜单权限 + var adminRole = fsql.Ado.ExecuteDataTable("SELECT id FROM sys_rbac_roles WHERE code = 'admin'"); + if (adminRole.Rows.Count > 0) + { + var roleId = Convert.ToInt64(adminRole.Rows[0]["id"]); + var menus = fsql.Ado.ExecuteDataTable("SELECT id FROM sys_rbac_menus WHERE path LIKE '/sys%'"); + foreach (DataRow row in menus.Rows) + { + var menuId = Convert.ToInt64(row["id"]); + var exist = fsql.Ado.ExecuteDataTable($"SELECT id FROM sys_rbac_role_permissions WHERE role_id = {roleId} AND permission_id = {menuId}"); + if (exist.Rows.Count == 0) + { + fsql.Ado.ExecuteNonQuery($"INSERT INTO sys_rbac_role_permissions (id, role_id, permission_id) VALUES ({snowflake.CreateId()}, {roleId}, {menuId})"); + } + } + } + } + catch (Exception ex) + { + Console.WriteLine($"[DbSeeder] RBAC 菜单种子跳过:{ex.Message}"); + } + } + + /// 为菜单创建权限单元(Permission),并修正旧菜单权限关联(RolePermission.PermissionId 从 Menu.Id 改为 Permission.Id) + private static void FixMenuPermissions(IFreeSql fsql, ISnowflakeIdGenerator snowflake) + { + try + { + foreach (var menu in fsql.Select().ToList()) + { + // 1. 查或创建菜单权限单元(ResourceType=菜单,ResourceId=Menu.Id) + var perm = fsql.Select() + .Where(p => p.ResourceType == Resource_Type_Enum.菜单 && p.ResourceId == menu.Id).First(); + if (perm == null) + { + var resourceIdentity = string.IsNullOrEmpty(menu.Code) ? menu.Id.ToString() : menu.Code; + perm = new SysPermission(snowflake.CreateId(), Permission_Type_Enum.Visble, Resource_Type_Enum.菜单, resourceIdentity, menu.Id); + if (!string.IsNullOrEmpty(menu.Permission)) + perm.SetCode(menu.Permission); + fsql.Insert(perm).ExecuteAffrows(); + } + else if (!string.IsNullOrEmpty(menu.Permission) && perm.Code != menu.Permission) + { + perm.SetCode(menu.Permission); + fsql.Update().SetSource(perm).ExecuteAffrows(); + } + + // 2. 修正旧菜单权限关联(permission_id 旧值 = Menu.Id,改为 Permission.Id) + fsql.Ado.ExecuteNonQuery($"UPDATE sys_rbac_role_permissions SET permission_id = {perm.Id} WHERE permission_id = {menu.Id};"); + } + Console.WriteLine("[DbSeeder] 菜单权限单元同步完成"); + } + catch (Exception ex) + { + Console.WriteLine($"[DbSeeder] 菜单权限单元同步跳过:{ex.Message}"); + } + } +} diff --git a/backend/F9WebAiTest.Infrastructure/DependencyInjection.cs b/backend/F9WebAiTest.Infrastructure/DependencyInjection.cs new file mode 100644 index 0000000..49a9aec --- /dev/null +++ b/backend/F9WebAiTest.Infrastructure/DependencyInjection.cs @@ -0,0 +1,83 @@ +using FreeSql; +using FreeSql.Aop; +using F9WebAiTest.Application.Interfaces.Repositories.SYS.Departments; +using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Menus; +using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Roles; +using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Users; +using F9WebAiTest.Application.Interfaces.Security; +using F9WebAiTest.Application.Interfaces.SnowflakeID; +using F9WebAiTest.Application.Interfaces.UOW; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities; +using F9WebAiTest.Domain.Interfaces.SYS.Departments; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Menus; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Permissions; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Roles; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Users; +using F9WebAiTest.Infrastructure.FreeSqls.Persistence; +using F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.Departments; +using F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Menus; +using F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Permissions; +using F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Roles; +using F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Users; +using F9WebAiTest.Infrastructure.InterfaceImplements.Security; +using F9WebAiTest.Infrastructure.InterfaceImplements.SnowflakeID; +using F9WebAiTest.Infrastructure.Repositories; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace F9WebAiTest.Infrastructure; + +public static class DependencyInjection +{ + public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) + { + // 创建 FreeSql 实例(SQLite),关闭自动同步结构(只建新表,不修改旧库) + var connStr = configuration.GetConnectionString("Default"); + var fsql = new FreeSqlBuilder() + .UseConnectionString(DataType.Sqlite, connStr) + .UseAutoSyncStructure(false) + .Build(); + + // 软删除全局过滤器:自动应用到新旧两套实体基类 + fsql.GlobalFilter.Apply("软删除", a => a.IsDeleted == false); + fsql.GlobalFilter.Apply("软删除", a => a.IsDeleted == false); + + // 枚举统一映射为 int(与旧库 INTEGER 存储保持一致) + fsql.Aop.ConfigEntityProperty += (_, e) => + { + if (e.Property.PropertyType.IsEnum) + e.ModifyResult.MapType = typeof(int); + }; + + services.AddSingleton(fsql); + services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); + + // ==================== F9 风格 RBAC(DDD + CQRS)依赖注册 ==================== + + // 雪花 ID(有状态,单例) + services.AddSingleton(); + // 密码哈希 + services.AddScoped(); + // 工作单元(事务) + services.AddScoped(); + + // 写仓储(Domain 接口) + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + // 读仓储(Application 接口) + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + return services; + } +} diff --git a/backend/F9WebAiTest.Infrastructure/F9WebAiTest.Infrastructure.csproj b/backend/F9WebAiTest.Infrastructure/F9WebAiTest.Infrastructure.csproj new file mode 100644 index 0000000..7bb534f --- /dev/null +++ b/backend/F9WebAiTest.Infrastructure/F9WebAiTest.Infrastructure.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + disable + F9WebAiTest.Infrastructure + + + + + + + + + + + + + + + + + diff --git a/backend/F9WebAiTest.Infrastructure/FreeSqls/Persistence/UnitOfWork.cs b/backend/F9WebAiTest.Infrastructure/FreeSqls/Persistence/UnitOfWork.cs new file mode 100644 index 0000000..0406f95 --- /dev/null +++ b/backend/F9WebAiTest.Infrastructure/FreeSqls/Persistence/UnitOfWork.cs @@ -0,0 +1,46 @@ +using FreeSql; +using F9WebAiTest.Application.Interfaces.UOW; + +namespace F9WebAiTest.Infrastructure.FreeSqls.Persistence; + +/// +/// 工作单元实现:基于 FreeSql 的 UnitOfWorkManager,事务内 Orm 通过 AsyncLocal 暴露给仓储。 +/// 仓储通过 UnitOfWork.CurrentOrm 获取事务感知的 Orm。 +/// +public class UnitOfWork_F9 : IUnitOfWork_F9 +{ + private readonly IFreeSql _fsql; + private readonly UnitOfWorkManager _uowManager; + private static readonly AsyncLocal _currentOrm = new(); + + /// 当前事务内的 Orm(非事务上下文返回 null) + public static IFreeSql CurrentOrm => _currentOrm.Value; + + public UnitOfWork_F9(IFreeSql fsql) + { + _fsql = fsql; + _uowManager = new UnitOfWorkManager(fsql); + } + + public async Task ExecuteWithUow(Func> func) + { + using var uow = _uowManager.Begin(); + _currentOrm.Value = uow.Orm; + try + { + var result = await func(); + if (result) uow.Commit(); + else uow.Rollback(); + return result; + } + catch + { + uow.Rollback(); + throw; + } + finally + { + _currentOrm.Value = null; + } + } +} diff --git a/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/F9RepositoryBase.cs b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/F9RepositoryBase.cs new file mode 100644 index 0000000..9a7fec1 --- /dev/null +++ b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/F9RepositoryBase.cs @@ -0,0 +1,15 @@ +using FreeSql; +using F9WebAiTest.Infrastructure.FreeSqls.Persistence; + +namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories; + +/// F9 风格仓储基类:提供事务感知的 Orm(事务内用事务 Orm,事务外用外部 Orm) +public abstract class F9RepositoryBase +{ + protected readonly IFreeSql _fsql; + + protected F9RepositoryBase(IFreeSql fsql) => _fsql = fsql; + + /// 事务感知的 Orm + protected IFreeSql Orm => UnitOfWork_F9.CurrentOrm ?? _fsql; +} diff --git a/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/Departments/DepartmentReadRepository.cs b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/Departments/DepartmentReadRepository.cs new file mode 100644 index 0000000..4c2139b --- /dev/null +++ b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/Departments/DepartmentReadRepository.cs @@ -0,0 +1,17 @@ +using FreeSql; +using F9WebAiTest.Application.Dtos.SYS.Departments; +using F9WebAiTest.Application.Interfaces.Repositories.SYS.Departments; +using F9WebAiTest.Domain.Entities.SYS.Departments; + +namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.Departments; + +internal class DepartmentReadRepository : F9RepositoryBase, IDepartmentReadRepository +{ + public DepartmentReadRepository(IFreeSql fsql) : base(fsql) { } + + public async Task> GetAllAsync() + => await Orm.Select() + .Where(x => x.DeletedAt == null) + .OrderBy(x => x.SortOrder) + .ToListAsync(x => new DepartmentReadDto_View(x.Id, x.Code, x.Name, x.ParentId, x.ManageType, x.DutyType, x.Description, x.IsActive, x.SortOrder)); +} diff --git a/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/Departments/DepartmentUserRepository.cs b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/Departments/DepartmentUserRepository.cs new file mode 100644 index 0000000..645eec5 --- /dev/null +++ b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/Departments/DepartmentUserRepository.cs @@ -0,0 +1,21 @@ +using FreeSql; +using F9WebAiTest.Domain.Entities.SYS.Departments; +using F9WebAiTest.Domain.Interfaces.SYS.Departments; + +namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.Departments; + +internal class DepartmentUserRepository : F9RepositoryBase, IDepartmentUserRepository +{ + public DepartmentUserRepository(IFreeSql fsql) : base(fsql) { } + + public async Task AssignAsync(DepartmentUser depUser) + { + depUser.CreatedAt = DateTime.Now; + return await Orm.Insert(depUser).ExecuteAffrowsAsync() > 0; + } + + public async Task RemoveAsync(long depId, long userId) + => await Orm.Delete() + .Where(x => x.DepartmentId == depId && x.UserId == userId) + .ExecuteAffrowsAsync() > 0; +} diff --git a/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/Departments/DepartmentWriteRepository.cs b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/Departments/DepartmentWriteRepository.cs new file mode 100644 index 0000000..2e5d205 --- /dev/null +++ b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/Departments/DepartmentWriteRepository.cs @@ -0,0 +1,38 @@ +using FreeSql; +using F9WebAiTest.Domain.Entities.SYS.Departments; +using F9WebAiTest.Domain.Interfaces.SYS.Departments; + +namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.Departments; + +internal class DepartmentWriteRepository : F9RepositoryBase, IDepartmentWriteRepository +{ + public DepartmentWriteRepository(IFreeSql fsql) : base(fsql) { } + + public async Task GetByIdAsync(long id, CancellationToken cancellationToken) + => await Orm.Select().Where(x => x.Id == id ).FirstAsync(cancellationToken); + + public async Task AddAsync(Department dep) + { + dep.CreatedAt = DateTime.Now; + return await Orm.Insert(dep).ExecuteAffrowsAsync() > 0; + } + + public async Task UpdateAsync(Department dep) + { + dep.UpdatedAt = DateTime.Now; + return await Orm.Update().SetSource(dep).ExecuteAffrowsAsync() > 0; + } + + public async Task SoftDeleteAsync(long id) + => await Orm.Update() + .Set(x => x.IsDeleted, true) + .Where(x => x.Id == id ) + .ExecuteAffrowsAsync() > 0; + + public async Task ActiveOrDeactive(long id, bool active) + => await Orm.Update() + .Set(x => x.IsActive, active) + .Set(x => x.UpdatedAt, DateTime.Now) + .Where(x => x.Id == id) + .ExecuteAffrowsAsync() > 0; +} diff --git a/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Menus/MenuReadRepository.cs b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Menus/MenuReadRepository.cs new file mode 100644 index 0000000..b12dbc8 --- /dev/null +++ b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Menus/MenuReadRepository.cs @@ -0,0 +1,61 @@ +using FreeSql; +using F9WebAiTest.Application.Dtos.SYS.RBAC.Menus; +using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Menus; +using F9WebAiTest.Domain.Entities.SYS.RBAC; + +namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Menus; + +internal class MenuReadRepository : F9RepositoryBase, IMenuReadRepository +{ + public MenuReadRepository(IFreeSql fsql) : base(fsql) { } + + public async Task GetMenuByIdForEditAsync(long id) + => await Orm.Select() + .Where(x => x.Id == id && x.DeletedAt == null) + .FirstAsync(x => new MenuReadDto_Menu_Edit(x.Id, x.Code, x.Name, x.IconSvg, x.MenuType, x.AssemblyName, x.TypeFullName, x.IsActive, x.ParentId, x.SortOrder)); + + public async Task ExistMenuByTypeFullnameExcludeIdAsync(string typeFullname, long? excludeId) + { + var query = Orm.Select().Where(x => x.TypeFullName == typeFullname && x.DeletedAt == null); + if (excludeId.HasValue) + query = query.Where(x => x.Id != excludeId.Value); + return await query.AnyAsync(); + } + + public async Task> GetAllMenuAsync(string like, long? parentId) + { + var all = await Orm.Select().Where(x => x.DeletedAt == null).ToListAsync(); + var dict = all.ToDictionary(x => x.Id, x => x.Name); + + var list = all.AsEnumerable(); + if (parentId.HasValue) + list = list.Where(x => x.ParentId == parentId.Value); + if (!string.IsNullOrWhiteSpace(like)) + list = list.Where(x => x.Name.Contains(like) || x.Code.Contains(like)); + + return list.OrderBy(x => x.SortOrder).Select(x => ToTable(x, dict)).ToList(); + } + + public async Task> GetTreeMenuAsync() + { + var all = await Orm.Select().Where(x => x.DeletedAt == null).OrderBy(x => x.SortOrder).ToListAsync(); + var dict = all.ToDictionary(x => x.Id, x => x.Name); + return BuildTree(all, 0, dict); + } + + private static MenuReadDto_Menu_Table ToTable(Menu x, Dictionary dict) + => new(x.Id, x.ParentId, x.Code, x.Name, x.IconSvg, x.MenuType.ToString(), + x.AssemblyName, x.TypeFullName, dict.GetValueOrDefault(x.ParentId, ""), x.Level, + x.IsActive ? "启用" : "禁用", x.SortOrder); + + private static List BuildTree(List all, long parentId, Dictionary dict) + => all.Where(x => x.ParentId == parentId).OrderBy(x => x.SortOrder).Select(x => + { + var children = BuildTree(all, x.Id, dict); + return new MenuReadDto_Menu_Table( + x.Id, x.ParentId, x.Code, x.Name, x.IconSvg, x.MenuType.ToString(), + x.AssemblyName, x.TypeFullName, dict.GetValueOrDefault(x.ParentId, ""), x.Level, + x.IsActive ? "启用" : "禁用", x.SortOrder, false, + children.Count > 0 ? children.ToArray() : default); + }).ToList(); +} diff --git a/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Menus/MenuWriteRepository.cs b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Menus/MenuWriteRepository.cs new file mode 100644 index 0000000..97131c1 --- /dev/null +++ b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Menus/MenuWriteRepository.cs @@ -0,0 +1,38 @@ +using FreeSql; +using F9WebAiTest.Domain.Entities.SYS.RBAC; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Menus; + +namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Menus; + +internal class MenuWriteRepository : F9RepositoryBase, IMenuWriteRepository +{ + public MenuWriteRepository(IFreeSql fsql) : base(fsql) { } + + public async Task GetByIdAsync(long id) + => await Orm.Select().Where(x => x.Id == id ).FirstAsync(); + + public async Task AddAsync(Menu menu) + { + menu.CreatedAt = DateTime.Now; + return await Orm.Insert(menu).ExecuteAffrowsAsync() > 0; + } + + public async Task UpdateAsync(Menu menu) + { + menu.UpdatedAt = DateTime.Now; + return await Orm.Update().SetSource(menu).ExecuteAffrowsAsync() > 0; + } + + public async Task SoftDeleteAsync(long id) + => await Orm.Update() + .Set(x => x.IsDeleted, true) + .Where(x => x.Id == id ) + .ExecuteAffrowsAsync() > 0; + + public async Task ActiveOrDeactive(long id, bool active) + => await Orm.Update() + .Set(x => x.IsActive, active) + .Set(x => x.UpdatedAt, DateTime.Now) + .Where(x => x.Id == id) + .ExecuteAffrowsAsync() > 0; +} diff --git a/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Permissions/PermissionWriteRepository.cs b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Permissions/PermissionWriteRepository.cs new file mode 100644 index 0000000..e93d6a8 --- /dev/null +++ b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Permissions/PermissionWriteRepository.cs @@ -0,0 +1,27 @@ +using FreeSql; +using F9WebAiTest.Domain.Entities.SYS.RBAC; +using F9WebAiTest.Domain.Enums.SYS.RBAC; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Permissions; + +namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Permissions; + +internal class PermissionWriteRepository : F9RepositoryBase, IPermissionWriteRepository +{ + public PermissionWriteRepository(IFreeSql fsql) : base(fsql) { } + + public async Task GetByIdAsync(Permission_Type_Enum permissionType, Resource_Type_Enum resourceType, long resourceId) + => await Orm.Select() + .Where(x => x.PermissionType == permissionType && x.ResourceType == resourceType && x.ResourceId == resourceId ) + .FirstAsync(); + + public async Task AddRangeAsync(Permission[] entites) + => await Orm.Insert(entites).ExecuteAffrowsAsync() > 0; + + public async Task DeleteByResourceIdAndTypeAsync(Permission_Type_Enum permissionType, Resource_Type_Enum resourceType, long resourceId) + => await Orm.Delete() + .Where(x => x.PermissionType == permissionType && x.ResourceType == resourceType && x.ResourceId == resourceId) + .ExecuteAffrowsAsync() > 0; + + public async Task DeleteByResourceIdAsync(long resourceId) + => await Orm.Delete().Where(x => x.ResourceId == resourceId).ExecuteAffrowsAsync() > 0; +} diff --git a/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Roles/RolePermissionWriteRepository.cs b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Roles/RolePermissionWriteRepository.cs new file mode 100644 index 0000000..e1b4b70 --- /dev/null +++ b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Roles/RolePermissionWriteRepository.cs @@ -0,0 +1,22 @@ +using FreeSql; +using F9WebAiTest.Domain.Entities.SYS.RBAC; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Roles; + +namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Roles; + +internal class RolePermissionWriteRepository : F9RepositoryBase, IRolePermissionWriteRepository +{ + public RolePermissionWriteRepository(IFreeSql fsql) : base(fsql) { } + + public async Task GetByIdAsync(long id) + => await Orm.Select().Where(x => x.Id == id).FirstAsync(); + + public async Task AssignAsync(RolePermission rolePermission) + { + rolePermission.CreatedAt = DateTime.Now; + return await Orm.Insert(rolePermission).ExecuteAffrowsAsync() > 0; + } + + public async Task RemoveAsync(long id) + => await Orm.Delete().Where(x => x.Id == id).ExecuteAffrowsAsync() > 0; +} diff --git a/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Roles/RoleReadRepository.cs b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Roles/RoleReadRepository.cs new file mode 100644 index 0000000..6493cda --- /dev/null +++ b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Roles/RoleReadRepository.cs @@ -0,0 +1,66 @@ +using FreeSql; +using F9WebAiTest.Application.Dtos.SYS.RBAC.Roles; +using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Roles; +using F9WebAiTest.Domain.Entities.SYS.RBAC; +using F9WebAiTest.Domain.Enums.SYS.RBAC; + +namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Roles; + +internal class RoleReadRepository : F9RepositoryBase, IRoleReadRepository +{ + public RoleReadRepository(IFreeSql fsql) : base(fsql) { } + + public async Task GetByIdAsync(long id) + => await Orm.Select() + .Where(x => x.Id == id && x.DeletedAt == null) + .FirstAsync(x => new RoleReadDto_Edit(x.Id, x.Name, x.Code, x.SortOrder, x.Description)); + + public async Task> GetAllAsync() + { + var roles = await Orm.Select() + .Where(x => x.DeletedAt == null) + .OrderBy(x => x.SortOrder) + .ToListAsync(x => new { x.Id, x.Name, x.Code, x.SortOrder, x.IsActive }); + + var roleIds = roles.Select(x => x.Id).ToList(); + var permCounts = await GetPermissionCountsAsync(roleIds); + + return roles.Select(x => + { + var c = permCounts.GetValueOrDefault(x.Id); + return new RoleReadDto_View(x.Id, x.Name, x.Code, x.SortOrder, c.Menu, c.Report, c.Dep, x.IsActive); + }).ToList(); + } + + public async Task> GetAllForCacheAsync() + => await Orm.Select() + .Where(x => x.DeletedAt == null) + .OrderBy(x => x.SortOrder) + .ToListAsync(x => new RoleReadDto_Cache(x.Id, x.Name, x.Code, x.SortOrder)); + + public async Task> GetRolePermissionsByResourceType(long roleId, Resource_Type_Enum resourceType) + => await Orm.Select() + .InnerJoin((rp, p) => rp.PermissionId == p.Id && p.DeletedAt == null) + .Where((rp, p) => rp.RoleId == roleId && p.ResourceType == resourceType) + .ToListAsync((rp, p) => new RolePermissionReadDto_View(rp.Id, rp.RoleId, rp.PermissionId, p.ResourceId, p.PermissionType)); + + private async Task> GetPermissionCountsAsync(List roleIds) + { + var result = new Dictionary(); + if (roleIds.Count == 0) return result; + + var perms = await Orm.Select() + .InnerJoin((rp, p) => rp.PermissionId == p.Id && p.DeletedAt == null) + .Where((rp, p) => roleIds.Contains(rp.RoleId)) + .ToListAsync((rp, p) => new { rp.RoleId, p.ResourceType }); + + foreach (var g in perms.GroupBy(x => x.RoleId)) + { + result[g.Key] = ( + g.Count(x => x.ResourceType == Resource_Type_Enum.菜单), + g.Count(x => x.ResourceType == Resource_Type_Enum.报表), + g.Count(x => x.ResourceType == Resource_Type_Enum.部门)); + } + return result; + } +} diff --git a/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Roles/RoleWriteRepository.cs b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Roles/RoleWriteRepository.cs new file mode 100644 index 0000000..9c55043 --- /dev/null +++ b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Roles/RoleWriteRepository.cs @@ -0,0 +1,38 @@ +using FreeSql; +using F9WebAiTest.Domain.Entities.SYS.RBAC; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Roles; + +namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Roles; + +internal class RoleWriteRepository : F9RepositoryBase, IRoleWriteRepository +{ + public RoleWriteRepository(IFreeSql fsql) : base(fsql) { } + + public async Task GetByIdAsync(long id) + => await Orm.Select().Where(x => x.Id == id ).FirstAsync(); + + public async Task AddAsync(Role role) + { + role.CreatedAt = DateTime.Now; + return await Orm.Insert(role).ExecuteAffrowsAsync() > 0; + } + + public async Task UpdateAsync(Role role) + { + role.UpdatedAt = DateTime.Now; + return await Orm.Update().SetSource(role).ExecuteAffrowsAsync() > 0; + } + + public async Task SoftDeleteAsync(long id) + => await Orm.Update() + .Set(x => x.IsDeleted, true) + .Where(x => x.Id == id ) + .ExecuteAffrowsAsync() > 0; + + public async Task ActiveOrDeactive(long id, bool active) + => await Orm.Update() + .Set(x => x.IsActive, active) + .Set(x => x.UpdatedAt, DateTime.Now) + .Where(x => x.Id == id) + .ExecuteAffrowsAsync() > 0; +} diff --git a/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Users/UserReadRepository.cs b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Users/UserReadRepository.cs new file mode 100644 index 0000000..1edd429 --- /dev/null +++ b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Users/UserReadRepository.cs @@ -0,0 +1,96 @@ +using FreeSql; +using F9WebAiTest.Application.Dtos.Common; +using F9WebAiTest.Application.Dtos.SYS.RBAC.Users; +using F9WebAiTest.Application.Interfaces.Repositories.SYS.RBAC.Users; +using F9WebAiTest.Domain.Entities.SYS.Departments; +using F9WebAiTest.Domain.Entities.SYS.RBAC; + +namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Users; + +internal class UserReadRepository : F9RepositoryBase, IUserReadRepository +{ + public UserReadRepository(IFreeSql fsql) : base(fsql) { } + + public async Task GetByLoginNameAsync(string loginName) + => await Orm.Select() + .Where(x => x.LoginName == loginName && x.DeletedAt == null) + .FirstAsync(x => new UserReadDto_Login(x.Id, x.UserName, x.LoginName, x.PasswordHash, x.DepartmentId)); + + public async Task GetByPhoneAsync(string phone) + => await Orm.Select() + .Where(x => x.PhoneNumber == phone && x.DeletedAt == null) + .FirstAsync(x => new UserReadDto_Login(x.Id, x.UserName, x.LoginName, x.PasswordHash, x.DepartmentId)); + + public async Task GetByIdAsync(long id) + => await Orm.Select() + .Where(x => x.Id == id && x.DeletedAt == null) + .FirstAsync(x => new UserReadDto_Edit(x.Id, x.UserName, x.PhoneNumber, x.LoginName, x.DepartmentId, x.SortOrder)); + + public async Task> GetByIdsAsync(List ids) + => await Orm.Select() + .Where(x => ids.Contains(x.Id) && x.DeletedAt == null) + .ToListAsync(x => new UserReadDto_Edit(x.Id, x.UserName, x.PhoneNumber, x.LoginName, x.DepartmentId, x.SortOrder)); + + public async Task> GetPagedAsync(long departmentId, string like, int page = 1, int pageSize = 10) + { + var query = Orm.Select() + .LeftJoin((u, d) => u.DepartmentId == d.Id) + .Where((u, d) => u.DeletedAt == null); + + if (departmentId > 0) + query = query.Where((u, d) => u.DepartmentId == departmentId); + if (!string.IsNullOrWhiteSpace(like)) + query = query.Where((u, d) => u.UserName.Contains(like) || u.LoginName.Contains(like) || u.PhoneNumber.Contains(like)); + + var total = await query.CountAsync(); + + var list = await query + .OrderBy((u, d) => u.SortOrder) + .Page(page, pageSize) + .ToListAsync((u, d) => new + { + u.Id, u.UserName, u.PhoneNumber, u.LoginName, + DepartmentName = d.Name, + u.LastLoginAt, u.SortOrder, u.IsActive + }); + + // 批量查角色名,避免 N+1 + var userIds = list.Select(x => x.Id).ToList(); + var roleDict = await GetRoleNameDictAsync(userIds); + + var items = list.Select(x => new UserReadDto_View( + x.Id, x.UserName, x.PhoneNumber, x.LoginName, + x.DepartmentName, + roleDict.TryGetValue(x.Id, out var roleName) ? roleName : "", + x.LastLoginAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "", + x.SortOrder, x.IsActive)).ToList(); + + return new PagedResult(items, page, pageSize, total); + } + + public async Task> GetUserRolesByUserId(long userId) + => await Orm.Select() + .InnerJoin((x, y) => x.RoleId == y.Id && y.DeletedAt == null) + .Where((x, y) => x.UserId == userId) + .ToListAsync((x, y) => new UserRoleReadDto_Cache(x.Id, x.UserId, x.RoleId, y.Name)); + + public async Task> GetByRoleIdsAsync(List roleIds) + => await Orm.Select() + .InnerJoin((u, x, y) => u.Id == x.UserId) + .InnerJoin((u, x, y) => x.RoleId == y.Id && y.DeletedAt == null) + .Where((u, x, y) => roleIds.Contains(x.RoleId) && u.DeletedAt == null) + .ToListAsync((u, x, y) => new UserReadDto_Edit(u.Id, u.UserName, u.PhoneNumber, u.LoginName, u.DepartmentId, u.SortOrder)); + + private async Task> GetRoleNameDictAsync(List userIds) + { + if (userIds.Count == 0) return new Dictionary(); + + var list = await Orm.Select() + .InnerJoin((x, y) => x.RoleId == y.Id && y.DeletedAt == null) + .Where((x, y) => userIds.Contains(x.UserId)) + .ToListAsync((x, y) => new { x.UserId, y.Name }); + + return list.GroupBy(x => x.UserId) + .ToDictionary(g => g.Key, g => string.Join(",", g.Select(v => v.Name))); + } +} diff --git a/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Users/UserRoleWriteRepository.cs b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Users/UserRoleWriteRepository.cs new file mode 100644 index 0000000..067f7aa --- /dev/null +++ b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Users/UserRoleWriteRepository.cs @@ -0,0 +1,21 @@ +using FreeSql; +using F9WebAiTest.Domain.Entities.SYS.RBAC; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Users; + +namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Users; + +internal class UserRoleWriteRepository : F9RepositoryBase, IUserRoleWriteRepository +{ + public UserRoleWriteRepository(IFreeSql fsql) : base(fsql) { } + + public async Task AssignAsync(UserRole userRole) + { + userRole.CreatedAt = DateTime.Now; + return await Orm.Insert(userRole).ExecuteAffrowsAsync() > 0; + } + + public async Task RemoveAsync(long userid, long roleId) + => await Orm.Delete() + .Where(x => x.UserId == userid && x.RoleId == roleId) + .ExecuteAffrowsAsync() > 0; +} diff --git a/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Users/UserWriteRepository.cs b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Users/UserWriteRepository.cs new file mode 100644 index 0000000..69985e9 --- /dev/null +++ b/backend/F9WebAiTest.Infrastructure/FreeSqls/Repositories/SYS/RBAC/Users/UserWriteRepository.cs @@ -0,0 +1,51 @@ +using FreeSql; +using F9WebAiTest.Domain.Entities.SYS.RBAC; +using F9WebAiTest.Domain.Interfaces.SYS.RBAC.Users; + +namespace F9WebAiTest.Infrastructure.FreeSqls.Repositories.SYS.RBAC.Users; + +internal class UserWriteRepository : F9RepositoryBase, IUserWriteRepository +{ + public UserWriteRepository(IFreeSql fsql) : base(fsql) { } + + public async Task GetByIdAsync(long id) + => await Orm.Select().Where(x => x.Id == id ).FirstAsync(); + + public async Task AddAsync(User user) + { + user.CreatedAt = DateTime.Now; + return await Orm.Insert(user).ExecuteAffrowsAsync() > 0; + } + + public async Task UpdateAsync(User user) + { + user.UpdatedAt = DateTime.Now; + return await Orm.Update().SetSource(user).ExecuteAffrowsAsync() > 0; + } + + public async Task SoftDeleteAsync(long id) + => await Orm.Update() + .Set(x => x.IsDeleted, true) + .Where(x => x.Id == id ) + .ExecuteAffrowsAsync() > 0; + + public async Task ActiveOrDeactive(long id, bool active) + => await Orm.Update() + .Set(x => x.IsActive, active) + .Set(x => x.UpdatedAt, DateTime.Now) + .Where(x => x.Id == id) + .ExecuteAffrowsAsync() > 0; + + public async Task UpdatePassworkdAsync(long id, string passwordHash) + => await Orm.Update() + .Set(x => x.PasswordHash, passwordHash) + .Set(x => x.UpdatedAt, DateTime.Now) + .Where(x => x.Id == id) + .ExecuteAffrowsAsync() > 0; + + public async Task UpdateLastLoginAtAsync(long id, DateTime dt) + => await Orm.Update() + .Set(x => x.LastLoginAt, dt) + .Where(x => x.Id == id) + .ExecuteAffrowsAsync() > 0; +} diff --git a/backend/F9WebAiTest.Infrastructure/InterfaceImplements/Security/PasswordHasher.cs b/backend/F9WebAiTest.Infrastructure/InterfaceImplements/Security/PasswordHasher.cs new file mode 100644 index 0000000..34cc253 --- /dev/null +++ b/backend/F9WebAiTest.Infrastructure/InterfaceImplements/Security/PasswordHasher.cs @@ -0,0 +1,27 @@ +using System.Security.Cryptography; +using System.Text; +using F9WebAiTest.Application.Interfaces.Security; + +namespace F9WebAiTest.Infrastructure.InterfaceImplements.Security; + +/// 密码哈希服务实现(BCrypt) +public class PasswordHasher : IPasswordHasher +{ + public string Hash256(string input) + { + var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input ?? "")); + return Convert.ToHexString(bytes).ToLowerInvariant(); + } + + public string GetMd5Hash(string input) + { + var bytes = MD5.HashData(Encoding.UTF8.GetBytes(input ?? "")); + return Convert.ToHexString(bytes).ToLowerInvariant(); + } + + public string HashPassword(string password) + => BCrypt.Net.BCrypt.HashPassword(password); + + public bool VerifyPassword(string password, string hashedPassword) + => BCrypt.Net.BCrypt.Verify(password, hashedPassword); +} diff --git a/backend/F9WebAiTest.Infrastructure/InterfaceImplements/SnowflakeID/SnowflakeIdGenerator.cs b/backend/F9WebAiTest.Infrastructure/InterfaceImplements/SnowflakeID/SnowflakeIdGenerator.cs new file mode 100644 index 0000000..5e66774 --- /dev/null +++ b/backend/F9WebAiTest.Infrastructure/InterfaceImplements/SnowflakeID/SnowflakeIdGenerator.cs @@ -0,0 +1,59 @@ +using F9WebAiTest.Application.Interfaces.SnowflakeID; + +namespace F9WebAiTest.Infrastructure.InterfaceImplements.SnowflakeID; + +/// +/// 雪花 ID 生成器(自实现,41 时间戳 + 10 worker + 12 序列)。 +/// 参考 BBIT.F9.Infrastructure.InterfaceImplements.SnowflakeID.SnowflakeIdGenerator +/// +public class SnowflakeIdGenerator : ISnowflakeIdGenerator +{ + private readonly object _lock = new(); + private readonly long _workerId; + private readonly long _epoch; + private long _lastTimestamp = -1L; + private long _sequence; + + private const long WorkerIdBits = 10L; + private const long SequenceBits = 12L; + private const long WorkerIdShift = SequenceBits; + private const long TimestampShift = SequenceBits + WorkerIdBits; + private const long SequenceMask = -1L ^ (-1L << (int)SequenceBits); + + public SnowflakeIdGenerator() + { + _workerId = 1; // 单节点默认 workerId = 1 + _epoch = new DateTimeOffset(2025, 10, 1, 0, 0, 0, TimeSpan.Zero).ToUnixTimeMilliseconds(); + } + + public long CreateId() + { + lock (_lock) + { + var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - _epoch; + if (timestamp < _lastTimestamp) + throw new InvalidOperationException("时钟回拨,拒绝生成 ID"); + + if (timestamp == _lastTimestamp) + { + _sequence = (_sequence + 1) & SequenceMask; + if (_sequence == 0) + { + // 序列溢出,等待下一毫秒 + while (timestamp <= _lastTimestamp) + timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - _epoch; + } + } + else + { + _sequence = 0; + } + + _lastTimestamp = timestamp; + + return (timestamp << (int)TimestampShift) + | (_workerId << (int)WorkerIdShift) + | _sequence; + } + } +} diff --git a/backend/F9WebAiTest.Infrastructure/Repositories/Repository.cs b/backend/F9WebAiTest.Infrastructure/Repositories/Repository.cs new file mode 100644 index 0000000..bbabe56 --- /dev/null +++ b/backend/F9WebAiTest.Infrastructure/Repositories/Repository.cs @@ -0,0 +1,88 @@ +using System.Linq.Expressions; +using FreeSql; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Infrastructure.Repositories; + +/// 通用仓储实现(基于 FreeSql) +public class Repository : IRepository where T : class, IEntity +{ + protected readonly IFreeSql _fsql; + + public Repository(IFreeSql fsql) + { + _fsql = fsql; + } + + public virtual async Task GetByIdAsync(long id) + => await _fsql.Select().Where(e => e.Id == id).FirstAsync(); + + public virtual async Task> GetAllAsync() + => await _fsql.Select().ToListAsync(); + + public virtual async Task> FindAsync(Expression> predicate) + => await _fsql.Select().Where(predicate).ToListAsync(); + + public virtual async Task FirstOrDefaultAsync(Expression> predicate) + => await _fsql.Select().Where(predicate).FirstAsync(); + + public virtual async Task AnyAsync(Expression> predicate) + => await _fsql.Select().Where(predicate).AnyAsync(); + + public virtual async Task CountAsync(Expression> predicate) + => await _fsql.Select().Where(predicate).CountAsync(); + + public virtual async Task AddAsync(T entity) + { + entity.CreatedAt = DateTime.Now; + await _fsql.Insert(entity).ExecuteAffrowsAsync(); + // FreeSql 在 SQLite 下不回填自增主键,手动用 last_insert_rowid 获取并回填 + if (entity.Id == 0) + { + entity.Id = Convert.ToInt64(await _fsql.Ado.ExecuteScalarAsync("SELECT last_insert_rowid();")); + } + return entity; + } + + public virtual async Task AddRangeAsync(IEnumerable entities) + { + foreach (var e in entities) e.CreatedAt = DateTime.Now; + await _fsql.Insert(entities).ExecuteAffrowsAsync(); + } + + public virtual async Task UpdateAsync(T entity) + { + entity.UpdatedAt = DateTime.Now; + await _fsql.Update().SetSource(entity).ExecuteAffrowsAsync(); + } + + public virtual async Task DeleteAsync(T entity) + { + entity.IsDeleted = true; + entity.UpdatedAt = DateTime.Now; + await _fsql.Update().SetSource(entity).ExecuteAffrowsAsync(); + } + + public virtual async Task DeleteRangeAsync(IEnumerable entities) + { + foreach (var e in entities) + { + e.IsDeleted = true; + e.UpdatedAt = DateTime.Now; + } + await _fsql.Update().SetSource(entities).ExecuteAffrowsAsync(); + } + + public virtual async Task HardDeleteAsync(T entity) + => await _fsql.Delete().Where(e => e.Id == entity.Id).ExecuteAffrowsAsync(); + + public virtual async Task HardDeleteRangeAsync(IEnumerable entities) + { + var ids = entities.Select(e => e.Id).ToList(); + if (ids.Count > 0) + await _fsql.Delete().Where(e => ids.Contains(e.Id)).ExecuteAffrowsAsync(); + } + + public virtual Task SaveChangesAsync() + => Task.FromResult(1); // FreeSql 即时提交,无 SaveChanges 语义,保持接口兼容 +} diff --git a/backend/F9WebAiTest.sln b/backend/F9WebAiTest.sln new file mode 100644 index 0000000..8ad42fe --- /dev/null +++ b/backend/F9WebAiTest.sln @@ -0,0 +1,36 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "F9WebAiTest.Domain", "F9WebAiTest.Domain\F9WebAiTest.Domain.csproj", "{A1111111-1111-1111-1111-111111111111}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "F9WebAiTest.Infrastructure", "F9WebAiTest.Infrastructure\F9WebAiTest.Infrastructure.csproj", "{B2222222-2222-2222-2222-222222222222}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "F9WebAiTest.Application", "F9WebAiTest.Application\F9WebAiTest.Application.csproj", "{C3333333-3333-3333-3333-333333333333}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "F9WebAiTest.Api", "F9WebAiTest.Api\F9WebAiTest.Api.csproj", "{D4444444-4444-4444-4444-444444444444}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1111111-1111-1111-1111-111111111111}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1111111-1111-1111-1111-111111111111}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1111111-1111-1111-1111-111111111111}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1111111-1111-1111-1111-111111111111}.Release|Any CPU.Build.0 = Release|Any CPU + {B2222222-2222-2222-2222-222222222222}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2222222-2222-2222-2222-222222222222}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2222222-2222-2222-2222-222222222222}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2222222-2222-2222-2222-222222222222}.Release|Any CPU.Build.0 = Release|Any CPU + {C3333333-3333-3333-3333-333333333333}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3333333-3333-3333-3333-333333333333}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3333333-3333-3333-3333-333333333333}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3333333-3333-3333-3333-333333333333}.Release|Any CPU.Build.0 = Release|Any CPU + {D4444444-4444-4444-4444-444444444444}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4444444-4444-4444-4444-444444444444}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4444444-4444-4444-4444-444444444444}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4444444-4444-4444-4444-444444444444}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/backend/Properties/launchSettings.json b/backend/Properties/launchSettings.json new file mode 100644 index 0000000..0afa1ad --- /dev/null +++ b/backend/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://0.0.0.0:3000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/client/cert.crt b/client/cert.crt new file mode 100644 index 0000000..f8bb8dc --- /dev/null +++ b/client/cert.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIICzzCCAbegAwIBAgIJTbg/GofriKv+MA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV +BAMTCTEwLjAuNC43MjAeFw0yNjA4MjUwODI4NDRaFw0yNzA4MjUwODI4NDRaMBQx +EjAQBgNVBAMTCTEwLjAuNC43MjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBALxCDGIxqvu8vn/8HScdURY5+Rg53dAelEKs+O1vzisCJ2qslS5P3dsg7ftc +UjhwgVMUplf8JASRMj21/bobmpeHPuiMqDz2v0uvL20zw24imt46qaR+PKwcWu6m +dBs/4650+ThmRhYtN3ZxK4D9aHo+t2L5+v3+GbL63oq5d9od+e39YZPNmCG/RYOC +IDy5+fnNNxvnEwwRZ5FyCViWDoMoor66EJoEFul/EVHw7k/tOmCrYMvB3nBugahi +/kAvcxYys/sPzF18d6iJA991VqArGS95EjRzvPFqMRng4zdJhezjgIHJQpZMdJT/ +La9UqtFtz5QMftOLGO90BMWpSScCAwEAAaMkMCIwIAYDVR0RBBkwF4IJbG9jYWxo +b3N0hwQKAARIhwR/AAABMA0GCSqGSIb3DQEBBQUAA4IBAQBADaHSuakP/QuzU3eE +GqExbFQD3uZucLaryqMr3oRFExFP82V5SVxg7I/JYlvYEfE9nVjII26CgRhagBYF +759dsJUq54qtZSpeADnLRkJ1JHkJIYrn4O8VaT1XWFCBLTyR4o3PddH8SRHxoEd7 +q3XLOcyrh1UW2MLBZIwRx397M+ZB15s+UL5gNJaNscdRKCkymzbBF7I20CCPS+Hl +XKRuTOepo8AZso/XqwkVYjNiZuDlPP4pxr917EUckVx8EQYVXi3cUw7MyMup7V/q +ajjcNUGIKAh4UHXBhF9bwAn97n/qTecnNzUSm+HrTN46rN/Afgl/zdwfu78OgYY9 +90u3 +-----END CERTIFICATE----- \ No newline at end of file diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..5a8d132 --- /dev/null +++ b/client/index.html @@ -0,0 +1,12 @@ + + + + + + 社区生活圈 + + +
+ + + diff --git a/client/package-lock.json b/client/package-lock.json new file mode 100644 index 0000000..61a5ebf --- /dev/null +++ b/client/package-lock.json @@ -0,0 +1,1943 @@ +{ + "name": "life-circle-client", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "life-circle-client", + "version": "1.0.0", + "dependencies": { + "@microsoft/signalr": "^8.0.7", + "axios": "^1.7.7", + "vue": "^3.4.38", + "vue-router": "^4.4.3" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.1.3", + "selfsigned": "^5.5.0", + "vite": "^5.4.3" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + }, + "node_modules/@microsoft/signalr": { + "version": "8.0.7", + "resolved": "https://registry.npmmirror.com/@microsoft/signalr/-/signalr-8.0.7.tgz", + "integrity": "sha512-PHcdMv8v5hJlBkRHAuKG5trGViQEkPYee36LnJQx4xHOQ5LL4X0nEWIxOp5cCtZ7tu+30quz5V3k0b1YNuc6lw==", + "dependencies": { + "abort-controller": "^3.0.0", + "eventsource": "^2.0.2", + "fetch-cookie": "^2.0.3", + "node-fetch": "^2.6.7", + "ws": "^7.4.5" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.9.4", + "resolved": "https://registry.npmmirror.com/@peculiar/asn1-cms/-/asn1-cms-2.9.4.tgz", + "integrity": "sha512-cben7oxmQsUGZqotus7yt0srYdncOT6RNWcTQ77T2RFOXejYVYkXadrfePdRcrVpO9K95IRLKKglG2k38jKXuw==", + "dev": true, + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "@peculiar/asn1-x509-attr": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.9.4", + "resolved": "https://registry.npmmirror.com/@peculiar/asn1-csr/-/asn1-csr-2.9.4.tgz", + "integrity": "sha512-xd4YN4vpRjkDAQWVfZZkeu12IEND7DOpkqaHSIHxZl1uggUNa9Ju0QxY2jHvDAS9pP0zhRBytg8ifsnGo3V0jw==", + "dev": true, + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.9.4", + "resolved": "https://registry.npmmirror.com/@peculiar/asn1-ecc/-/asn1-ecc-2.9.4.tgz", + "integrity": "sha512-JJXefFshRAuVAjWQo/39bkg1ywc1VaiO44S8RRC+Ykvf/u2KDmYffoDb0ZBPCR5uJy4AGKQhl8mX+Q8ShcWaXQ==", + "dev": true, + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.9.4", + "resolved": "https://registry.npmmirror.com/@peculiar/asn1-pfx/-/asn1-pfx-2.9.4.tgz", + "integrity": "sha512-khuGzHTzNzk4GDlIBEILyIs6Lce0yn0ZBdoI9v93kmNncfZRhD+AQ5ODFqdhvoE8cMJF/JMTQ8yA+t1D14kqCw==", + "dev": true, + "dependencies": { + "@peculiar/asn1-cms": "^2.9.4", + "@peculiar/asn1-pkcs8": "^2.9.4", + "@peculiar/asn1-rsa": "^2.9.4", + "@peculiar/asn1-schema": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.9.4", + "resolved": "https://registry.npmmirror.com/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.9.4.tgz", + "integrity": "sha512-duRdotlUx9eDZe6QrQpQKl61RbWykCHBCkKayP8V8XdEFwlKHZ8qGGDMyS6Pye7OX7nLFttTTpRkJeet78ckwQ==", + "dev": true, + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.9.4", + "resolved": "https://registry.npmmirror.com/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.9.4.tgz", + "integrity": "sha512-kaL4cNxBpdQE2dKlyZBqz4ygCrwffO+8wfoxTEqM1Z8RadvCeELBRzcv0dzM8aY9azHMwODO5nxU65zXmhToOQ==", + "dev": true, + "dependencies": { + "@peculiar/asn1-cms": "^2.9.4", + "@peculiar/asn1-pfx": "^2.9.4", + "@peculiar/asn1-pkcs8": "^2.9.4", + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "@peculiar/asn1-x509-attr": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.9.4", + "resolved": "https://registry.npmmirror.com/@peculiar/asn1-rsa/-/asn1-rsa-2.9.4.tgz", + "integrity": "sha512-pZ96eD1PptovcWQ/GSmuNFXd/7EQJNlKfDaNCyE2rx3W0v6QFelkzquVqRSRyyDXXCYD69ZXJDzZ8GhIiQzKoA==", + "dev": true, + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.9.4", + "resolved": "https://registry.npmmirror.com/@peculiar/asn1-schema/-/asn1-schema-2.9.4.tgz", + "integrity": "sha512-GjzePcT9Iw8NzeOPf73iNS9xM+TBhd/FilAfP+RQGkTMQJTVWtytN3JHJACCjf/ABNau5S7mS3g+DcuxmRgYEg==", + "dev": true, + "dependencies": { + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.9.4", + "resolved": "https://registry.npmmirror.com/@peculiar/asn1-x509/-/asn1-x509-2.9.4.tgz", + "integrity": "sha512-CxhBo/RdEbMMob7T31ZdQjGuoyRFLVwrDzTn25bihzBasRg9kRm/0IxIPvhgQtcK/9dNcO1XQL2fuPugwELL0Q==", + "dev": true, + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/utils": "^2.0.2", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.9.4", + "resolved": "https://registry.npmmirror.com/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.9.4.tgz", + "integrity": "sha512-ehQXbpQaQYycgu8OrvigwSPTFfVRcu0ECNYCWw+yzBp02Lw5paRqzzhUpfOgO2K38+WfFZuEz/0RPtam5g0OMg==", + "dev": true, + "dependencies": { + "@peculiar/asn1-schema": "^2.9.4", + "@peculiar/asn1-x509": "^2.9.4", + "asn1js": "^3.0.10", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "dev": true, + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.14.3", + "resolved": "https://registry.npmmirror.com/@peculiar/x509/-/x509-1.14.3.tgz", + "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", + "dev": true, + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-csr": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.0", + "@peculiar/asn1-pkcs9": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.5.tgz", + "integrity": "sha512-jfkGfTwhQpsiSckPF8r9bU3pn3vyd72NlWaO+TgEO6WPSDnUhXzrNYCHBMOYj0ACaUgjm6eERLF+XV9a6RstoA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.5.tgz", + "integrity": "sha512-oGVqyQlxnrz9/ty89oHpU857VUHEl5/Xu4R2lS+aivCTrNnSsbiENzTnNaBsjxH0CNWGPhzHArOLFwo+oKXveA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.5.tgz", + "integrity": "sha512-bW7B8xMEq8n99Q3ieEcPRGuphurdZAaFzQc9Efyyw3FL6DZO6pMy9xhdN+kBoD7Sy05xNXSr4OyPPnpkYriS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.5.tgz", + "integrity": "sha512-YSwBS86QeHOGlrxJ1PSOIZSkzRL/JmKeunhc+lV6M1a6En8QuVCD/T/qIA0J4Gd2Y86RIOBYrLcOUtqGh9+/1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.5.tgz", + "integrity": "sha512-2fST8lILgl7cKbme/1KDdPCmbXbG+gqoV3bHp19L0ypX/3akYMBVdOunPleRCwonoLnXOZ/0F+Mt/v8POFmfcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.5.tgz", + "integrity": "sha512-cpIxQCP9J+EVad0a6LO1kY3ZGODlk80VlI+2I96B8xMcdHZ4pLVhfQ49JFpYqjPF91FFkQWftf57YlDcTiw9yQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.5.tgz", + "integrity": "sha512-r9fGh3eFs3e/udWh5ZjXQtxiYK/xoFxQaYR/cELxac/Udkl5Th+IsFm0CX3Kl9hmUH/we7EoMpjJgeQNnE0+IA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.5.tgz", + "integrity": "sha512-xdvFdp7OM6KLJviJT2g/YuRSUjnZgGHk4RNgwIbN7X6cPugOucV60DdHXWzsBVCUdrGb6qSXnJQrrAKMmQuj3Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.5.tgz", + "integrity": "sha512-rRqILAndyzHzP7T9NFQrq+4HFWNhqkqkKur7eiBpfLmz01PO0JKx5Vchu3YllE4YXI/Ftgq/szrDWg5GJ0mI8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.5.tgz", + "integrity": "sha512-Gf4X3qVMucayUvux6aXXPgXovocSFUC0rrffDuPI/S2nHhNMhjcZxsrAFYCOF350PRreW1XwzFj3CT/3bKsWCw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.5.tgz", + "integrity": "sha512-+s5qA0TNM0qm8PK/a5gt/1Hpx+NV08uSuCncvhziIlQzT6AEV2fnUQo7eBtFTFO0nA9scauvoR2HusfXmQnO4w==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.5.tgz", + "integrity": "sha512-ybb6QvWwWJCbBWqERpc8K3pYVGIrXlG8MEQ8IIuJY6Y9KdHQxoFoNyfkAOtKn1VHu3KuLidXvwrvGR1mEjeWCw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.5.tgz", + "integrity": "sha512-nZb1DtnOyhCmYvsC8A2CwOkopVg+IS1+fPUa7rMOAXtNw5+lLCLLPqd6XAiNrGtoQKsbvIBOwsHnBH/3wnb4HQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.5.tgz", + "integrity": "sha512-yMbj63Sp89ryrXLWyz+sy+fYD2HpOnMCLGbe4Oa1smclFSUukdtD/BgdiHaAetJNb74URD8U4hM+qG5KVzMEkg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.5.tgz", + "integrity": "sha512-mhoan3OJw2kYV/e1jtIdmvUZgyBFeA6zGWsOswmR0Tg19TQbowZuR+JMLID6spbbBN7Zee2ejrgmy3+FxGrIdA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.5.tgz", + "integrity": "sha512-5ZTLmjWbb1VZdjuyhe83K/8QO0/h11midQCBP+X5OYn32ra7eOBoM0ZqtaY4nkgNsYgmdVhMYPoyVPTjUpHf3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.5.tgz", + "integrity": "sha512-m53kG+br6PGxOTmgBEM2DHSDs9RVjsyEbUwjJPJGTFm1grWOG8EKJggDCTb60unD4Tjby8fi7/m9XfkEWasVWg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.5.tgz", + "integrity": "sha512-6RHPJR1g/uvdYU8uXBnfq3nlqyZCP82Fr6NHgfGoaIeSh0YEqnX/x6uA9MmJJbnSH7swqX4F+CkGdUF+6doiQA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.5.tgz", + "integrity": "sha512-xs+OXQtEXgpXT0DmA5+U3qnRZHdCST/5HRQxS8wSPZTUZN/EMWeHuSIod32LQklTBZBV9DyfncKBQ8n5V3eFdw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.5.tgz", + "integrity": "sha512-e7hD+sl3s+mcLQDZ8pbudBVsdG6r5yN4w3LqG2TJ8sQHDpblWj5lrJs/3m01Cvlxbt4x13zu5thLjgypgtkYzw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.5.tgz", + "integrity": "sha512-GiyJaCf+WpMub/17aPcKk27QMl5W6f+KhdPTjlFOn5akH5Wa/DCM9Stdx5cDfmasyKB08MqpVQ1uJE2RkkpbXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.5.tgz", + "integrity": "sha512-+OQ8U2DdoEfXl8T4Fb18AjmEwbXMerKDKCL8yCPAYhKCEEKoul7rkbeGCBFCbAlaGaa7pmtRTpkAJM2LE/i5FA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.5.tgz", + "integrity": "sha512-KanvAZrPKbDBFwrgiU9yEVpQoox9QPV1WZOXX7HudJQY+eSlu82CtWxDU8WtuRRvtN5EGkLczkd6Y6DTcvm9wA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.5.tgz", + "integrity": "sha512-1aC3UEWTtRl3RK3VpDJ/Tqk1XI4SLTmXIthAq6wRWo8XiSXJNd+VprJM4/1P4+i6HIaFEFlVi9sTTziniD2tOQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.5.tgz", + "integrity": "sha512-/gDJaRs4gl0NPIwqCz+6PkpmhhjRAD2j6P4rSNHBzUkO3naEx2mIU0pRle1vUNRQ7mE/+8OOeXLTv/J56FKiQg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.41.tgz", + "integrity": "sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.41", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.41.tgz", + "integrity": "sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==", + "dependencies": { + "@vue/compiler-core": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.41.tgz", + "integrity": "sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.41", + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-ssr": "3.5.41", + "@vue/shared": "3.5.41", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.41.tgz", + "integrity": "sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==", + "dependencies": { + "@vue/compiler-dom": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.41.tgz", + "integrity": "sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==", + "dependencies": { + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.41.tgz", + "integrity": "sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==", + "dependencies": { + "@vue/reactivity": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.41.tgz", + "integrity": "sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==", + "dependencies": { + "@vue/reactivity": "3.5.41", + "@vue/runtime-core": "3.5.41", + "@vue/shared": "3.5.41", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.41.tgz", + "integrity": "sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==", + "dependencies": { + "@vue/compiler-ssr": "3.5.41", + "@vue/runtime-dom": "3.5.41", + "@vue/shared": "3.5.41" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.41.tgz", + "integrity": "sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/asn1js": { + "version": "3.0.10", + "resolved": "https://registry.npmmirror.com/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "dev": true, + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.19.0", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/bytestreamjs": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/bytestreamjs/-/bytestreamjs-2.0.1.tgz", + "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fetch-cookie": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/fetch-cookie/-/fetch-cookie-2.2.0.tgz", + "integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==", + "dependencies": { + "set-cookie-parser": "^2.4.8", + "tough-cookie": "^4.0.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/pkijs": { + "version": "3.4.0", + "resolved": "https://registry.npmmirror.com/pkijs/-/pkijs-3.4.0.tgz", + "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", + "dev": true, + "dependencies": { + "@noble/hashes": "1.4.0", + "asn1js": "^3.0.6", + "bytestreamjs": "^2.0.1", + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmmirror.com/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmmirror.com/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "dev": true, + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/pvutils/-/pvutils-1.2.0.tgz", + "integrity": "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==", + "dev": true, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmmirror.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/rollup": { + "version": "4.62.5", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.62.5.tgz", + "integrity": "sha512-/tqMfgP7GPA3PHhCmuiS4vIjrSVhHLgY++i+dhbG462euyAj7FpM4D9uq1X3BgjlqRdpcOrYhcQtfiQLNc8tqw==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.5", + "@rollup/rollup-android-arm64": "4.62.5", + "@rollup/rollup-darwin-arm64": "4.62.5", + "@rollup/rollup-darwin-x64": "4.62.5", + "@rollup/rollup-freebsd-arm64": "4.62.5", + "@rollup/rollup-freebsd-x64": "4.62.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.5", + "@rollup/rollup-linux-arm-musleabihf": "4.62.5", + "@rollup/rollup-linux-arm64-gnu": "4.62.5", + "@rollup/rollup-linux-arm64-musl": "4.62.5", + "@rollup/rollup-linux-loong64-gnu": "4.62.5", + "@rollup/rollup-linux-loong64-musl": "4.62.5", + "@rollup/rollup-linux-ppc64-gnu": "4.62.5", + "@rollup/rollup-linux-ppc64-musl": "4.62.5", + "@rollup/rollup-linux-riscv64-gnu": "4.62.5", + "@rollup/rollup-linux-riscv64-musl": "4.62.5", + "@rollup/rollup-linux-s390x-gnu": "4.62.5", + "@rollup/rollup-linux-x64-gnu": "4.62.5", + "@rollup/rollup-linux-x64-musl": "4.62.5", + "@rollup/rollup-openbsd-x64": "4.62.5", + "@rollup/rollup-openharmony-arm64": "4.62.5", + "@rollup/rollup-win32-arm64-msvc": "4.62.5", + "@rollup/rollup-win32-ia32-msvc": "4.62.5", + "@rollup/rollup-win32-x64-gnu": "4.62.5", + "@rollup/rollup-win32-x64-msvc": "4.62.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/selfsigned": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/selfsigned/-/selfsigned-5.5.0.tgz", + "integrity": "sha512-ftnu3TW4+3eBfLRFnDEkzGxSF/10BJBkaLJuBHZX0kiPS7bRdlpZGu6YGt4KngMkdTwJE6MbjavFpqHvqVt+Ew==", + "dev": true, + "dependencies": { + "@peculiar/x509": "^1.14.2", + "pkijs": "^3.3.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true + }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmmirror.com/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "dev": true, + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmmirror.com/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.41", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.41.tgz", + "integrity": "sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==", + "dependencies": { + "@vue/compiler-dom": "3.5.41", + "@vue/compiler-sfc": "3.5.41", + "@vue/runtime-dom": "3.5.41", + "@vue/server-renderer": "3.5.41", + "@vue/shared": "3.5.41" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/ws": { + "version": "7.5.13", + "resolved": "https://registry.npmmirror.com/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..62ae15c --- /dev/null +++ b/client/package.json @@ -0,0 +1,22 @@ +{ + "name": "life-circle-client", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@microsoft/signalr": "^8.0.7", + "axios": "^1.7.7", + "vue": "^3.4.38", + "vue-router": "^4.4.3" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.1.3", + "selfsigned": "^5.5.0", + "vite": "^5.4.3" + } +} diff --git a/client/src/App.vue b/client/src/App.vue new file mode 100644 index 0000000..b413f99 --- /dev/null +++ b/client/src/App.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/client/src/api.js b/client/src/api.js new file mode 100644 index 0000000..88356d1 --- /dev/null +++ b/client/src/api.js @@ -0,0 +1,31 @@ +import axios from 'axios'; + +const api = axios.create({ + baseURL: '/api', + timeout: 10000 +}); + +// 请求拦截:附加 token +api.interceptors.request.use((config) => { + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +// 响应拦截:统一错误处理 +api.interceptors.response.use( + (res) => res.data, + (err) => { + const message = err.response?.data?.message || '请求失败,请稍后重试'; + if (err.response?.status === 401) { + localStorage.removeItem('token'); + localStorage.removeItem('user'); + window.location.href = '/login'; + } + return Promise.reject(new Error(message)); + } +); + +export default api; diff --git a/client/src/main.js b/client/src/main.js new file mode 100644 index 0000000..5f1612b --- /dev/null +++ b/client/src/main.js @@ -0,0 +1,6 @@ +import { createApp } from 'vue'; +import App from './App.vue'; +import router from './router'; +import './style.css'; + +createApp(App).use(router).mount('#app'); diff --git a/client/src/router.js b/client/src/router.js new file mode 100644 index 0000000..b7a97fc --- /dev/null +++ b/client/src/router.js @@ -0,0 +1,34 @@ +import { createRouter, createWebHistory } from 'vue-router'; + +const routes = [ + { path: '/', redirect: '/posts' }, + { path: '/login', name: 'login', component: () => import('./views/Login.vue'), meta: { guest: true } }, + { path: '/posts', name: 'posts', component: () => import('./views/Posts.vue') }, + { path: '/notices', name: 'notices', component: () => import('./views/Notices.vue') }, + { path: '/secondhand', name: 'secondhand', component: () => import('./views/Secondhand.vue') }, + { path: '/housekeeping', name: 'housekeeping', component: () => import('./views/Housekeeping.vue') }, + { path: '/chat', name: 'chat', component: () => import('./views/ChatList.vue') }, + { path: '/chat/:id', name: 'chat-detail', component: () => import('./views/ChatDetail.vue') }, + { path: '/live', name: 'live', component: () => import('./views/LiveList.vue') }, + { path: '/live/host/:id', name: 'live-host', component: () => import('./views/LiveHost.vue') }, + { path: '/live/watch/:id', name: 'live-watch', component: () => import('./views/LiveWatch.vue') }, + { path: '/profile', name: 'profile', component: () => import('./views/Profile.vue') } +]; + +const router = createRouter({ + history: createWebHistory(), + routes +}); + +// 路由守卫:未登录跳转登录页 +router.beforeEach((to) => { + const token = localStorage.getItem('token'); + if (!token && !to.meta.guest) { + return { name: 'login' }; + } + if (token && to.meta.guest) { + return { name: 'posts' }; + } +}); + +export default router; diff --git a/client/src/style.css b/client/src/style.css new file mode 100644 index 0000000..5c4f67f --- /dev/null +++ b/client/src/style.css @@ -0,0 +1,179 @@ +:root { + --primary: #2f7d5c; + --primary-light: #3f9d76; + --accent: #f0a13c; + --bg: #f5f7f6; + --card: #ffffff; + --text: #2c3e50; + --text-light: #7f8c8d; + --border: #e6ebe9; + --danger: #e74c3c; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; + background: var(--bg); + color: var(--text); + -webkit-font-smoothing: antialiased; +} + +a { + text-decoration: none; + color: inherit; +} + +button { + cursor: pointer; + font-family: inherit; +} + +input, textarea, select { + font-family: inherit; + font-size: 14px; + padding: 10px 12px; + border: 1px solid var(--border); + border-radius: 8px; + outline: none; + transition: border-color 0.2s; + width: 100%; +} + +input:focus, textarea:focus, select:focus { + border-color: var(--primary); +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 10px 20px; + border: none; + border-radius: 8px; + background: var(--primary); + color: #fff; + font-size: 14px; + font-weight: 500; + transition: background 0.2s, transform 0.1s; +} + +.btn:hover { + background: var(--primary-light); +} + +.btn:active { + transform: scale(0.98); +} + +.btn-ghost { + background: transparent; + color: var(--text); + border: 1px solid var(--border); +} + +.btn-ghost:hover { + background: #f0f2f1; +} + +.btn-danger { + background: var(--danger); +} + +.btn-sm { + padding: 6px 12px; + font-size: 13px; + border-radius: 6px; +} + +.card { + background: var(--card); + border-radius: 12px; + padding: 20px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} + +.page { + max-width: 720px; + margin: 0 auto; + padding: 20px 16px; +} + +.page-title { + font-size: 22px; + font-weight: 700; + margin-bottom: 20px; +} + +.empty { + text-align: center; + color: var(--text-light); + padding: 40px 0; +} + +.avatar { + width: 40px; + height: 40px; + border-radius: 50%; + background: var(--primary-light); + color: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + font-weight: 600; + flex-shrink: 0; + overflow: hidden; +} + +.avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.time { + color: var(--text-light); + font-size: 12px; +} + +.toast { + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + background: #333; + color: #fff; + padding: 10px 20px; + border-radius: 8px; + font-size: 14px; + z-index: 9999; + animation: fadeIn 0.3s; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateX(-50%) translateY(-10px); } + to { opacity: 1; transform: translateX(-50%) translateY(0); } +} + +.tag { + display: inline-block; + padding: 2px 10px; + border-radius: 20px; + font-size: 12px; + background: #e8f3ee; + color: var(--primary); +} + +.badge { + background: var(--accent); + color: #fff; + font-size: 12px; + padding: 2px 8px; + border-radius: 10px; +} diff --git a/client/src/utils.js b/client/src/utils.js new file mode 100644 index 0000000..1d1d92b --- /dev/null +++ b/client/src/utils.js @@ -0,0 +1,29 @@ +export function toast(message) { + const el = document.createElement('div'); + el.className = 'toast'; + el.textContent = message; + document.body.appendChild(el); + setTimeout(() => el.remove(), 2000); +} + +export function formatTime(str) { + if (!str) return ''; + // 兼容 .NET 的 ISO 时间(可能带 Z 或不带)、SQLite 的 "yyyy-MM-dd HH:mm:ss" + let dateStr = str; + if (typeof str === 'string' && str.includes(' ')) { + dateStr = str.replace(' ', 'T'); + } + const date = new Date(dateStr); + if (isNaN(date.getTime())) return str; + const now = new Date(); + const diff = (now - date) / 1000; + if (diff < 60) return '刚刚'; + if (diff < 3600) return Math.floor(diff / 60) + '分钟前'; + if (diff < 86400) return Math.floor(diff / 3600) + '小时前'; + if (diff < 86400 * 7) return Math.floor(diff / 86400) + '天前'; + // 超过一周显示具体日期 + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, '0'); + const d = String(date.getDate()).padStart(2, '0'); + return `${y}-${m}-${d}`; +} diff --git a/client/src/views/ChatDetail.vue b/client/src/views/ChatDetail.vue new file mode 100644 index 0000000..1681f9d --- /dev/null +++ b/client/src/views/ChatDetail.vue @@ -0,0 +1,489 @@ + + + + + diff --git a/client/src/views/ChatList.vue b/client/src/views/ChatList.vue new file mode 100644 index 0000000..a354a96 --- /dev/null +++ b/client/src/views/ChatList.vue @@ -0,0 +1,233 @@ + + + + + diff --git a/client/src/views/Housekeeping.vue b/client/src/views/Housekeeping.vue new file mode 100644 index 0000000..98517b2 --- /dev/null +++ b/client/src/views/Housekeeping.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/client/src/views/LiveHost.vue b/client/src/views/LiveHost.vue new file mode 100644 index 0000000..1619388 --- /dev/null +++ b/client/src/views/LiveHost.vue @@ -0,0 +1,340 @@ + + + + + diff --git a/client/src/views/LiveList.vue b/client/src/views/LiveList.vue new file mode 100644 index 0000000..69f94ad --- /dev/null +++ b/client/src/views/LiveList.vue @@ -0,0 +1,177 @@ + + + + + diff --git a/client/src/views/LiveWatch.vue b/client/src/views/LiveWatch.vue new file mode 100644 index 0000000..1b70143 --- /dev/null +++ b/client/src/views/LiveWatch.vue @@ -0,0 +1,259 @@ + + + + + diff --git a/client/src/views/Login.vue b/client/src/views/Login.vue new file mode 100644 index 0000000..9d207a5 --- /dev/null +++ b/client/src/views/Login.vue @@ -0,0 +1,175 @@ + + + + + diff --git a/client/src/views/Notices.vue b/client/src/views/Notices.vue new file mode 100644 index 0000000..d7184f5 --- /dev/null +++ b/client/src/views/Notices.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/client/src/views/Posts.vue b/client/src/views/Posts.vue new file mode 100644 index 0000000..9ef5816 --- /dev/null +++ b/client/src/views/Posts.vue @@ -0,0 +1,420 @@ + + + + + diff --git a/client/src/views/Profile.vue b/client/src/views/Profile.vue new file mode 100644 index 0000000..01d46c6 --- /dev/null +++ b/client/src/views/Profile.vue @@ -0,0 +1,196 @@ + + + + + diff --git a/client/src/views/Secondhand.vue b/client/src/views/Secondhand.vue new file mode 100644 index 0000000..668bcb7 --- /dev/null +++ b/client/src/views/Secondhand.vue @@ -0,0 +1,190 @@ + + + + + diff --git a/client/vite.config.js b/client/vite.config.js new file mode 100644 index 0000000..01c0d97 --- /dev/null +++ b/client/vite.config.js @@ -0,0 +1,27 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; +import fs from 'fs'; +import path from 'path'; + +export default defineConfig({ + plugins: [vue()], + server: { + host: '0.0.0.0', + port: 5173, + https: { + key: fs.readFileSync(path.resolve(__dirname, 'key.pem')), + cert: fs.readFileSync(path.resolve(__dirname, 'cert.pem')) + }, + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true + }, + '/hubs': { + target: 'http://localhost:3000', + changeOrigin: true, + ws: true + } + } + } +}); diff --git a/server/db.js b/server/db.js new file mode 100644 index 0000000..273bcbe --- /dev/null +++ b/server/db.js @@ -0,0 +1,89 @@ +import Database from 'better-sqlite3'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const db = new Database(path.join(__dirname, 'life_circle.db')); + +db.pragma('journal_mode = WAL'); +db.pragma('foreign_keys = ON'); + +// 初始化数据表 +db.exec(` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + password TEXT NOT NULL, + nickname TEXT NOT NULL, + avatar TEXT DEFAULT '', + phone TEXT DEFAULT '', + building TEXT DEFAULT '', + bio TEXT DEFAULT '', + role TEXT DEFAULT 'user', + created_at TEXT DEFAULT (datetime('now', 'localtime')) + ); + + CREATE TABLE IF NOT EXISTS posts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + content TEXT NOT NULL, + images TEXT DEFAULT '[]', + category TEXT DEFAULT '动态', + likes INTEGER DEFAULT 0, + comments INTEGER DEFAULT 0, + created_at TEXT DEFAULT (datetime('now', 'localtime')), + FOREIGN KEY (user_id) REFERENCES users(id) + ); + + CREATE TABLE IF NOT EXISTS comments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + post_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + content TEXT NOT NULL, + created_at TEXT DEFAULT (datetime('now', 'localtime')), + FOREIGN KEY (post_id) REFERENCES posts(id), + FOREIGN KEY (user_id) REFERENCES users(id) + ); + + CREATE TABLE IF NOT EXISTS likes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + post_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + UNIQUE(post_id, user_id), + FOREIGN KEY (post_id) REFERENCES posts(id), + FOREIGN KEY (user_id) REFERENCES users(id) + ); + + CREATE TABLE IF NOT EXISTS notices ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content TEXT NOT NULL, + type TEXT DEFAULT '通知', + created_at TEXT DEFAULT (datetime('now', 'localtime')) + ); + + CREATE TABLE IF NOT EXISTS secondhand ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + title TEXT NOT NULL, + description TEXT NOT NULL, + price REAL DEFAULT 0, + images TEXT DEFAULT '[]', + status TEXT DEFAULT '在售', + created_at TEXT DEFAULT (datetime('now', 'localtime')), + FOREIGN KEY (user_id) REFERENCES users(id) + ); + + CREATE TABLE IF NOT EXISTS housekeeping ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + title TEXT NOT NULL, + description TEXT NOT NULL, + price TEXT DEFAULT '', + phone TEXT DEFAULT '', + created_at TEXT DEFAULT (datetime('now', 'localtime')), + FOREIGN KEY (user_id) REFERENCES users(id) + ); +`); + +export default db; diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..60c04cb --- /dev/null +++ b/server/index.js @@ -0,0 +1,55 @@ +import express from 'express'; +import cors from 'cors'; +import authRouter from './routes/auth.js'; +import postsRouter from './routes/posts.js'; +import noticesRouter from './routes/notices.js'; +import secondhandRouter from './routes/secondhand.js'; +import housekeepingRouter from './routes/housekeeping.js'; +import db from './db.js'; +import bcrypt from 'bcryptjs'; + +const app = express(); +app.use(cors()); +app.use(express.json({ limit: '10mb' })); + +// 初始化管理员账号 +function seedAdmin() { + const admin = db.prepare('SELECT id FROM users WHERE username = ?').get('admin'); + if (!admin) { + const hash = bcrypt.hashSync('admin123', 10); + db.prepare('INSERT INTO users (username, password, nickname, role, building) VALUES (?, ?, ?, ?, ?)') + .run('admin', hash, '社区管理员', 'admin', '物业中心'); + console.log('已创建管理员账号: admin / admin123'); + } + // 初始化示例通知 + const count = db.prepare('SELECT COUNT(*) as c FROM notices').get().c; + if (count === 0) { + db.prepare('INSERT INTO notices (title, content, type) VALUES (?, ?, ?)') + .run('停水通知', '因管道维修,本周六 8:00-12:00 小区将停水,请各位业主提前储水。', '通知'); + db.prepare('INSERT INTO notices (title, content, type) VALUES (?, ?, ?)') + .run('关于规范电动车停放的通知', '请将电动车停放在指定充电棚,严禁上楼充电,感谢配合。', '通知'); + } +} +seedAdmin(); + +app.use('/api/auth', authRouter); +app.use('/api/posts', postsRouter); +app.use('/api/notices', noticesRouter); +app.use('/api/secondhand', secondhandRouter); +app.use('/api/housekeeping', housekeepingRouter); + +// 静态文件(前端构建产物) +import path from 'path'; +import { fileURLToPath } from 'url'; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +app.use(express.static(path.join(__dirname, '../client/dist'))); + +app.use((err, req, res, next) => { + console.error(err); + res.status(500).json({ message: '服务器内部错误' }); +}); + +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`生活圈系统后端已启动: http://localhost:${PORT}`); +}); diff --git a/server/life_circle.db b/server/life_circle.db new file mode 100644 index 0000000000000000000000000000000000000000..db7a7459cf0d014b0dc2333abb5541c58c2d6ed1 GIT binary patch literal 4096 zcmWFz^vNtqRY=P(%1ta$FlG>7U}9o$P*7lCU|@t|AVoG{WYBBV;st3JAlr;ljiVtj n8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*O6ovo*g{}s{ literal 0 HcmV?d00001 diff --git a/server/middleware.js b/server/middleware.js new file mode 100644 index 0000000..d6e417c --- /dev/null +++ b/server/middleware.js @@ -0,0 +1,28 @@ +import jwt from 'jsonwebtoken'; + +const SECRET = 'life-circle-secret-key-2026'; + +export function auth(req, res, next) { + const header = req.headers.authorization; + if (!header) { + return res.status(401).json({ message: '未登录' }); + } + const token = header.split(' ')[1]; + try { + const payload = jwt.verify(token, SECRET); + req.user = payload; + next(); + } catch (e) { + return res.status(401).json({ message: '登录已过期,请重新登录' }); + } +} + +export function sign(user) { + return jwt.sign( + { id: user.id, username: user.username, nickname: user.nickname, role: user.role }, + SECRET, + { expiresIn: '7d' } + ); +} + +export { SECRET }; diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000..ce10af2 --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,1411 @@ +{ + "name": "life-circle-server", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "life-circle-server", + "version": "1.0.0", + "dependencies": { + "bcryptjs": "^2.4.3", + "better-sqlite3": "^11.3.0", + "cors": "^2.8.5", + "express": "^4.19.2", + "jsonwebtoken": "^9.0.2", + "multer": "^1.4.5-lts.1" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmmirror.com/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/body-parser": { + "version": "1.20.6", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmmirror.com/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmmirror.com/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmmirror.com/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmmirror.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmmirror.com/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.95.0", + "resolved": "https://registry.npmmirror.com/node-abi/-/node-abi-3.95.0.tgz", + "integrity": "sha512-T9iGctuocf0qIWFFOTxPzjT5q0SILqaBYXt272tlBHvTKC5+3JnkMirLxNJNkXHtFyBjU2Jx+NL4Zipr0B/c6Q==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmmirror.com/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmmirror.com/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmmirror.com/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..235c118 --- /dev/null +++ b/server/package.json @@ -0,0 +1,18 @@ +{ + "name": "life-circle-server", + "version": "1.0.0", + "description": "社区生活圈系统后端服务", + "main": "index.js", + "type": "module", + "scripts": { + "start": "node index.js", + "dev": "node --watch index.js" + }, + "dependencies": { + "bcryptjs": "^2.4.3", + "better-sqlite3": "^11.3.0", + "cors": "^2.8.5", + "express": "^4.19.2", + "jsonwebtoken": "^9.0.2" + } +} diff --git a/server/routes/auth.js b/server/routes/auth.js new file mode 100644 index 0000000..ddcff12 --- /dev/null +++ b/server/routes/auth.js @@ -0,0 +1,75 @@ +import { Router } from 'express'; +import bcrypt from 'bcryptjs'; +import db from '../db.js'; +import { auth, sign } from '../middleware.js'; + +const router = Router(); + +// 注册 +router.post('/register', (req, res) => { + const { username, password, nickname, phone, building } = req.body; + if (!username || !password || !nickname) { + return res.status(400).json({ message: '请填写完整信息' }); + } + if (username.length < 3 || password.length < 6) { + return res.status(400).json({ message: '用户名至少3位,密码至少6位' }); + } + const exists = db.prepare('SELECT id FROM users WHERE username = ?').get(username); + if (exists) { + return res.status(400).json({ message: '用户名已存在' }); + } + const hash = bcrypt.hashSync(password, 10); + const info = db + .prepare('INSERT INTO users (username, password, nickname, phone, building) VALUES (?, ?, ?, ?, ?)') + .run(username, hash, nickname, phone || '', building || ''); + const user = db.prepare('SELECT * FROM users WHERE id = ?').get(info.lastInsertRowid); + res.json({ token: sign(user), user: safeUser(user) }); +}); + +// 登录 +router.post('/login', (req, res) => { + const { username, password } = req.body; + const user = db.prepare('SELECT * FROM users WHERE username = ?').get(username); + if (!user || !bcrypt.compareSync(password, user.password)) { + return res.status(401).json({ message: '用户名或密码错误' }); + } + res.json({ token: sign(user), user: safeUser(user) }); +}); + +// 获取当前用户 +router.get('/me', auth, (req, res) => { + const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.user.id); + if (!user) return res.status(404).json({ message: '用户不存在' }); + res.json(safeUser(user)); +}); + +// 更新个人资料 +router.put('/me', auth, (req, res) => { + const { nickname, phone, building, bio } = req.body; + db.prepare('UPDATE users SET nickname = ?, phone = ?, building = ?, bio = ? WHERE id = ?') + .run(nickname, phone || '', building || '', bio || '', req.user.id); + const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.user.id); + res.json(safeUser(user)); +}); + +// 修改密码 +router.put('/password', auth, (req, res) => { + const { oldPassword, newPassword } = req.body; + const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.user.id); + if (!bcrypt.compareSync(oldPassword, user.password)) { + return res.status(400).json({ message: '原密码错误' }); + } + if (newPassword.length < 6) { + return res.status(400).json({ message: '新密码至少6位' }); + } + const hash = bcrypt.hashSync(newPassword, 10); + db.prepare('UPDATE users SET password = ? WHERE id = ?').run(hash, req.user.id); + res.json({ message: '修改成功' }); +}); + +function safeUser(u) { + const { password, ...rest } = u; + return rest; +} + +export default router; diff --git a/server/routes/housekeeping.js b/server/routes/housekeeping.js new file mode 100644 index 0000000..5956450 --- /dev/null +++ b/server/routes/housekeeping.js @@ -0,0 +1,38 @@ +import { Router } from 'express'; +import db from '../db.js'; +import { auth } from '../middleware.js'; + +const router = Router(); + +// 家政服务列表 +router.get('/', auth, (req, res) => { + const items = db + .prepare('SELECT h.*, u.nickname, u.avatar FROM housekeeping h JOIN users u ON h.user_id = u.id ORDER BY h.created_at DESC') + .all(); + res.json(items); +}); + +// 发布家政服务 +router.post('/', auth, (req, res) => { + const { title, description, price, phone } = req.body; + if (!title || !description) { + return res.status(400).json({ message: '请填写服务名称和描述' }); + } + const info = db + .prepare('INSERT INTO housekeeping (user_id, title, description, price, phone) VALUES (?, ?, ?, ?, ?)') + .run(req.user.id, title, description, price || '', phone || ''); + res.json(db.prepare('SELECT * FROM housekeeping WHERE id = ?').get(info.lastInsertRowid)); +}); + +// 删除服务 +router.delete('/:id', auth, (req, res) => { + const item = db.prepare('SELECT * FROM housekeeping WHERE id = ?').get(req.params.id); + if (!item) return res.status(404).json({ message: '服务不存在' }); + if (item.user_id !== req.user.id && req.user.role !== 'admin') { + return res.status(403).json({ message: '无权删除' }); + } + db.prepare('DELETE FROM housekeeping WHERE id = ?').run(req.params.id); + res.json({ message: '删除成功' }); +}); + +export default router; diff --git a/server/routes/notices.js b/server/routes/notices.js new file mode 100644 index 0000000..90ff191 --- /dev/null +++ b/server/routes/notices.js @@ -0,0 +1,28 @@ +import { Router } from 'express'; +import db from '../db.js'; +import { auth } from '../middleware.js'; + +const router = Router(); + +// 物业通知列表 +router.get('/', (req, res) => { + const notices = db.prepare('SELECT * FROM notices ORDER BY created_at DESC').all(); + res.json(notices); +}); + +// 发布通知(管理员) +router.post('/', auth, (req, res) => { + if (req.user.role !== 'admin') { + return res.status(403).json({ message: '仅管理员可发布通知' }); + } + const { title, content, type } = req.body; + if (!title || !content) { + return res.status(400).json({ message: '请填写标题和内容' }); + } + const info = db + .prepare('INSERT INTO notices (title, content, type) VALUES (?, ?, ?)') + .run(title, content, type || '通知'); + res.json(db.prepare('SELECT * FROM notices WHERE id = ?').get(info.lastInsertRowid)); +}); + +export default router; diff --git a/server/routes/posts.js b/server/routes/posts.js new file mode 100644 index 0000000..e9b7c7f --- /dev/null +++ b/server/routes/posts.js @@ -0,0 +1,90 @@ +import { Router } from 'express'; +import db from '../db.js'; +import { auth } from '../middleware.js'; + +const router = Router(); + +// 获取动态列表(含用户信息、是否已点赞) +router.get('/', auth, (req, res) => { + const { category } = req.query; + let sql = ` + SELECT p.*, u.nickname, u.avatar, + (SELECT COUNT(*) FROM likes l WHERE l.post_id = p.id) as like_count, + EXISTS(SELECT 1 FROM likes l WHERE l.post_id = p.id AND l.user_id = ?) as liked + FROM posts p JOIN users u ON p.user_id = u.id + `; + const params = [req.user.id]; + if (category && category !== '全部') { + sql += ' WHERE p.category = ?'; + params.push(category); + } + sql += ' ORDER BY p.created_at DESC'; + const posts = db.prepare(sql).all(...params); + res.json(posts.map(p => ({ ...p, images: JSON.parse(p.images || '[]') }))); +}); + +// 发布动态 +router.post('/', auth, (req, res) => { + const { content, images, category } = req.body; + if (!content || !content.trim()) { + return res.status(400).json({ message: '内容不能为空' }); + } + const info = db + .prepare('INSERT INTO posts (user_id, content, images, category) VALUES (?, ?, ?, ?)') + .run(req.user.id, content, JSON.stringify(images || []), category || '动态'); + const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(info.lastInsertRowid); + res.json({ ...post, images: JSON.parse(post.images || '[]') }); +}); + +// 删除动态 +router.delete('/:id', auth, (req, res) => { + const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(req.params.id); + if (!post) return res.status(404).json({ message: '动态不存在' }); + if (post.user_id !== req.user.id && req.user.role !== 'admin') { + return res.status(403).json({ message: '无权删除' }); + } + db.prepare('DELETE FROM comments WHERE post_id = ?').run(req.params.id); + db.prepare('DELETE FROM likes WHERE post_id = ?').run(req.params.id); + db.prepare('DELETE FROM posts WHERE id = ?').run(req.params.id); + res.json({ message: '删除成功' }); +}); + +// 点赞/取消点赞 +router.post('/:id/like', auth, (req, res) => { + const post = db.prepare('SELECT id FROM posts WHERE id = ?').get(req.params.id); + if (!post) return res.status(404).json({ message: '动态不存在' }); + const existing = db + .prepare('SELECT id FROM likes WHERE post_id = ? AND user_id = ?') + .get(req.params.id, req.user.id); + if (existing) { + db.prepare('DELETE FROM likes WHERE id = ?').run(existing.id); + res.json({ liked: false }); + } else { + db.prepare('INSERT INTO likes (post_id, user_id) VALUES (?, ?)').run(req.params.id, req.user.id); + res.json({ liked: true }); + } +}); + +// 获取评论 +router.get('/:id/comments', auth, (req, res) => { + const comments = db + .prepare('SELECT c.*, u.nickname, u.avatar FROM comments c JOIN users u ON c.user_id = u.id WHERE c.post_id = ? ORDER BY c.created_at ASC') + .all(req.params.id); + res.json(comments); +}); + +// 发表评论 +router.post('/:id/comments', auth, (req, res) => { + const { content } = req.body; + if (!content || !content.trim()) { + return res.status(400).json({ message: '评论不能为空' }); + } + db.prepare('INSERT INTO comments (post_id, user_id, content) VALUES (?, ?, ?)') + .run(req.params.id, req.user.id, content); + const list = db + .prepare('SELECT c.*, u.nickname, u.avatar FROM comments c JOIN users u ON c.user_id = u.id WHERE c.post_id = ? ORDER BY c.created_at ASC') + .all(req.params.id); + res.json(list); +}); + +export default router; diff --git a/server/routes/secondhand.js b/server/routes/secondhand.js new file mode 100644 index 0000000..447c631 --- /dev/null +++ b/server/routes/secondhand.js @@ -0,0 +1,50 @@ +import { Router } from 'express'; +import db from '../db.js'; +import { auth } from '../middleware.js'; + +const router = Router(); + +// 二手闲置列表 +router.get('/', auth, (req, res) => { + const items = db + .prepare('SELECT s.*, u.nickname, u.avatar FROM secondhand s JOIN users u ON s.user_id = u.id ORDER BY s.created_at DESC') + .all(); + res.json(items.map(i => ({ ...i, images: JSON.parse(i.images || '[]') }))); +}); + +// 发布闲置 +router.post('/', auth, (req, res) => { + const { title, description, price, images } = req.body; + if (!title || !description) { + return res.status(400).json({ message: '请填写标题和描述' }); + } + const info = db + .prepare('INSERT INTO secondhand (user_id, title, description, price, images) VALUES (?, ?, ?, ?, ?)') + .run(req.user.id, title, description, price || 0, JSON.stringify(images || [])); + res.json(db.prepare('SELECT * FROM secondhand WHERE id = ?').get(info.lastInsertRowid)); +}); + +// 更新状态(下架/售出) +router.put('/:id', auth, (req, res) => { + const item = db.prepare('SELECT * FROM secondhand WHERE id = ?').get(req.params.id); + if (!item) return res.status(404).json({ message: '物品不存在' }); + if (item.user_id !== req.user.id && req.user.role !== 'admin') { + return res.status(403).json({ message: '无权操作' }); + } + const { status } = req.body; + db.prepare('UPDATE secondhand SET status = ? WHERE id = ?').run(status, req.params.id); + res.json(db.prepare('SELECT * FROM secondhand WHERE id = ?').get(req.params.id)); +}); + +// 删除闲置 +router.delete('/:id', auth, (req, res) => { + const item = db.prepare('SELECT * FROM secondhand WHERE id = ?').get(req.params.id); + if (!item) return res.status(404).json({ message: '物品不存在' }); + if (item.user_id !== req.user.id && req.user.role !== 'admin') { + return res.status(403).json({ message: '无权删除' }); + } + db.prepare('DELETE FROM secondhand WHERE id = ?').run(req.params.id); + res.json({ message: '删除成功' }); +}); + +export default router; diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..76c5273 --- /dev/null +++ b/start.bat @@ -0,0 +1,21 @@ +@echo off +chcp 65001 >nul +echo ============================================ +echo 启动 f9-web-ai-test(后端 + 前端) +echo ============================================ +echo. + +echo [1/2] 启动后端 (.NET 8, 端口 3000)... +start "F9WebAiTest-Backend" cmd /k "cd /d C:\Users\32453\CodeBuddy\20260825142915\backend\F9WebAiTest.Api && dotnet run" + +echo [2/2] 启动前端 (Vue3, 端口 5173)... +start "F9WebAiTest-Frontend" cmd /k "cd /d C:\Users\32453\CodeBuddy\20260825142915\web && npm run dev" + +echo. +echo 启动完成! +echo 后端: http://localhost:3000 (Swagger: http://localhost:3000/swagger) +echo 前端: http://localhost:5173 +echo 默认账号: admin / 123456 +echo. +echo 请稍等几秒让服务启动,然后浏览器打开 http://localhost:5173 +pause diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..7228f3f --- /dev/null +++ b/web/index.html @@ -0,0 +1,12 @@ + + + + + + 智慧缫丝系统 + + +
+ + + diff --git a/web/nswag.json b/web/nswag.json new file mode 100644 index 0000000..b6346ff --- /dev/null +++ b/web/nswag.json @@ -0,0 +1,72 @@ +{ + "runtime": "Net80", + "defaultVariables": null, + "documentGenerator": { + "fromDocument": { + "url": "http://localhost:3000/swagger/v1/swagger.json", + "output": null, + "newLineBehavior": "Auto" + } + }, + "codeGenerators": { + "openApiToTypeScriptClient": { + "className": "ApiClient", + "moduleName": "", + "namespace": "", + "typeScriptVersion": 4.3, + "template": "Axios", + "promiseType": "Promise", + "httpClass": "HttpClient", + "withCredentials": false, + "useSingletonProvider": false, + "injectionTokenType": "OpaqueToken", + "rxJsVersion": 6.0, + "dateTimeType": "Date", + "nullValue": "Undefined", + "generateClientClasses": true, + "generateClientInterfaces": false, + "generateOptionalParameters": false, + "exportTypes": true, + "wrapDtoExceptions": false, + "exceptionClass": "ApiException", + "clientBaseClass": null, + "wrapResponses": false, + "wrapResponseMethods": [], + "generateResponseClasses": true, + "responseClass": "SwaggerResponse", + "protectedMethods": [], + "configurationClass": null, + "useTransformOptionsMethod": false, + "useTransformResultMethod": false, + "generateDtoTypes": true, + "operationGenerationMode": "MultipleClientsFromOperationId", + "markOptionalProperties": true, + "generateCloneMethod": false, + "typeStyle": "Interface", + "enumStyle": "Enum", + "useLeafType": false, + "classTypes": [], + "extendedClasses": [], + "extensionCode": null, + "generateDefaultValues": true, + "excludedTypeNames": [], + "excludedParameterNames": [], + "handleReferences": false, + "generateTypeCheckFunctions": false, + "generateConstructorInterface": true, + "convertConstructorInterfaceData": false, + "importRequiredTypes": true, + "useGetBaseUrlMethod": false, + "baseUrlTokenName": "API_BASE_URL", + "queryNullValue": "", + "useAbortSignal": false, + "inlineNamedDictionaries": false, + "inlineNamedAny": false, + "includeHttpContext": false, + "templateDirectory": null, + "serviceHost": null, + "serviceSchemes": null, + "output": "src/api/client.ts" + } + } +} diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..3e9e25e --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,1862 @@ +{ + "name": "f9-web-ai-test", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "f9-web-ai-test", + "version": "1.0.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "@microsoft/signalr": "^10.0.11", + "axios": "^1.7.7", + "element-plus": "^2.8.4", + "pinia": "^2.2.2", + "vue": "^3.4.38", + "vue-router": "^4.4.3" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.1.3", + "vite": "^5.4.3" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==" + }, + "node_modules/@microsoft/signalr": { + "version": "10.0.11", + "resolved": "https://registry.npmmirror.com/@microsoft/signalr/-/signalr-10.0.11.tgz", + "integrity": "sha512-FulOJ2EEtKvLQcswe/U7v8pzyXyk7Jua5xgWJPPwyQU/2Z9ORvKcVjyO75VvLsem+CJ1ORRexJ+7Bz1FCei2aw==", + "dependencies": { + "abort-controller": "^3.0.0", + "eventsource": "^2.0.2", + "fetch-cookie": "^2.0.3", + "node-fetch": "^2.6.7", + "ws": "^7.5.10" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true + }, + "node_modules/@types/lodash": { + "version": "4.17.25", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.25.tgz", + "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.42.tgz", + "integrity": "sha512-2Ye1ilMtKXxl8qZUrQ5j0CdgenFp/HFQmta6rfRyfEsTG69L6Wk+tWuNoHYHMx9E8tF2Slvdg1FuwDvAXdy1LQ==", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/shared": "3.5.42", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.42.tgz", + "integrity": "sha512-qbhQZEFmycr+ni/qyuccS4sucNN7VAbDfbkvNxWOX2VfgFm90MNs3/UhRNKoPMEIVn0F8gdlYjLPvqxHwHeQOA==", + "dependencies": { + "@vue/compiler-core": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.42.tgz", + "integrity": "sha512-fkCAFB4okcAANGMThboWnScp/gzWjU0ZSkVnjTIiplmMDq2uq0tIB3j+xVu4rhv5rvOgBySCysudmbMd6xRRqw==", + "dependencies": { + "@babel/parser": "^7.29.8", + "@vue/compiler-core": "3.5.42", + "@vue/compiler-dom": "3.5.42", + "@vue/compiler-ssr": "3.5.42", + "@vue/shared": "3.5.42", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.19", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.42.tgz", + "integrity": "sha512-xmLk3wLkbizPAiLyomjgFFosf2ys9b5Ghb+oh/k2tnvipNz8OFrQOiTcWCzyK7MpBp9KkyGtfvgfLUivbmuGYA==", + "dependencies": { + "@vue/compiler-dom": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.42.tgz", + "integrity": "sha512-TzNNfKpb7hDxbQltwAut8VDQA5YP+BuRlxntHUuRjyKwlMvmAPbs3unhCvieijifY6vFfVBwsS7wG/C7uq+bEQ==", + "dependencies": { + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.42.tgz", + "integrity": "sha512-9uACtuHs7vJGkm5Bp3xu4xRDLFTIYy5DgxpToVjqGIAhAEKwQfsaLvKINhM6nFVp6bZPRFGdDqd1g52MqKsotA==", + "dependencies": { + "@vue/reactivity": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.42.tgz", + "integrity": "sha512-rsCmhiWLaRxGltLwhlCWyYkFn7WAbKRh0q17eZ1A6Dq6eqc2ACQ61IIryxz0LrsvCzHSilLA9JHovVwM8CNE2g==", + "dependencies": { + "@vue/reactivity": "3.5.42", + "@vue/runtime-core": "3.5.42", + "@vue/shared": "3.5.42", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.42.tgz", + "integrity": "sha512-2++5dUyYS4gvo7xQXSECUDhB7TS0aOl5SeVfC5qSq1Jgfhjvegw1zqhwTIR3imZ+QYPJQw9gfcFvXGAjGZ7ajQ==", + "dependencies": { + "@vue/compiler-ssr": "3.5.42", + "@vue/runtime-dom": "3.5.42", + "@vue/shared": "3.5.42" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.42.tgz", + "integrity": "sha512-2rPxex1jQf4jvl9MOHl6YaXCPcrNqz/FstMOEh3QWY+/OME9nQTvl9WYeCwhW7AFjaR0SnngZGlp/wkR6rkI6g==" + }, + "node_modules/@vueuse/core": { + "version": "14.4.0", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-14.4.0.tgz", + "integrity": "sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.4.0", + "@vueuse/shared": "14.4.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.4.0", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-14.4.0.tgz", + "integrity": "sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.4.0", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-14.4.0.tgz", + "integrity": "sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.20.0", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" + }, + "node_modules/dayjs": { + "version": "1.11.23", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.23.tgz", + "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/element-plus": { + "version": "2.14.5", + "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.14.5.tgz", + "integrity": "sha512-bghYy/S+qg87enHPXELirhEdDqsVAUGcGpbGIeG8dz0kwpIkGz7gYsifulBshXX74iRtHib85XWQj0uSH2A1Yg==", + "dependencies": { + "@ctrl/tinycolor": "^4.2.0", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.8.0", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8", + "@types/lodash": "^4.17.24", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "14.4.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.20", + "lodash": "^4.18.1", + "lodash-es": "^4.18.1", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0", + "vue-component-type-helpers": "^3.3.9" + }, + "peerDependencies": { + "vue": "^3.3.7" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fetch-cookie": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/fetch-cookie/-/fetch-cookie-2.2.0.tgz", + "integrity": "sha512-h9AgfjURuCgA2+2ISl8GbavpUdR+WGAM2McW/ovn4tVccegp8ZqCKWSBR8uRdM8dDNlx5WdKRWxBYUwteLDCNQ==", + "dependencies": { + "set-cookie-parser": "^2.4.8", + "tough-cookie": "^4.0.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmmirror.com/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/rollup": { + "version": "4.63.1", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmmirror.com/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.42", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.42.tgz", + "integrity": "sha512-4RyHQTbQvOPs3MfvUO1Sg0YRrKNnA0mAVtvpd12Tg1fKDN7OHBUl1IqSn8zGJjK9nI3NkNp8cgTpVrSZC5TTcA==", + "dependencies": { + "@vue/compiler-dom": "3.5.42", + "@vue/compiler-sfc": "3.5.42", + "@vue/runtime-dom": "3.5.42", + "@vue/server-renderer": "3.5.42", + "@vue/shared": "3.5.42" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/vue-component-type-helpers/-/vue-component-type-helpers-3.3.11.tgz", + "integrity": "sha512-LwcxzeliO9fkQcpJG0PoX8X5kmAhKmH9wkpDLxNabwzkQ9Zeib2YVHwFV4pcWmMLfXVfjr/dSV+DaJ3cIPgSNA==" + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/ws": { + "version": "7.5.13", + "resolved": "https://registry.npmmirror.com/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..f9e6afe --- /dev/null +++ b/web/package.json @@ -0,0 +1,25 @@ +{ + "name": "f9-web-ai-test", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "gen:api": "nswag run nswag.json" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "@microsoft/signalr": "^10.0.11", + "axios": "^1.7.7", + "element-plus": "^2.8.4", + "pinia": "^2.2.2", + "vue": "^3.4.38", + "vue-router": "^4.4.3" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.1.3", + "vite": "^5.4.3" + } +} diff --git a/web/src/App.vue b/web/src/App.vue new file mode 100644 index 0000000..98240ae --- /dev/null +++ b/web/src/App.vue @@ -0,0 +1,3 @@ + diff --git a/web/src/api/client.ts b/web/src/api/client.ts new file mode 100644 index 0000000..3e2ee3f --- /dev/null +++ b/web/src/api/client.ts @@ -0,0 +1,1873 @@ +//---------------------- +// +// Generated using the NSwag toolchain v14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org) +// +//---------------------- + +/* tslint:disable */ +/* eslint-disable */ +// ReSharper disable InconsistentNaming + +import axios, { AxiosError } from 'axios'; +import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, CancelToken } from 'axios'; + +export class ApiClient { + protected instance: AxiosInstance; + protected baseUrl: string; + protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined; + + constructor(baseUrl?: string, instance?: AxiosInstance) { + + this.instance = instance || axios.create(); + + this.baseUrl = baseUrl ?? "http://localhost:3000"; + + } + + login(dto: LoginDto, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/auth/login"; + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(dto); + + let options_: AxiosRequestConfig = { + data: content_, + responseType: "blob", + method: "POST", + url: url_, + headers: { + "Content-Type": "application/json", + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processLogin(_response); + }); + } + + protected processLogin(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + refresh(dto: RefreshTokenDto, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/auth/refresh"; + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(dto); + + let options_: AxiosRequestConfig = { + data: content_, + responseType: "blob", + method: "POST", + url: url_, + headers: { + "Content-Type": "application/json", + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processRefresh(_response); + }); + } + + protected processRefresh(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + logout(dto: RefreshTokenDto, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/auth/logout"; + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(dto); + + let options_: AxiosRequestConfig = { + data: content_, + responseType: "blob", + method: "POST", + url: url_, + headers: { + "Content-Type": "application/json", + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processLogout(_response); + }); + } + + protected processLogout(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + getCurrentUser( cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/auth/me"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: AxiosRequestConfig = { + responseType: "blob", + method: "GET", + url: url_, + headers: { + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processGetCurrentUser(_response); + }); + } + + protected processGetCurrentUser(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + getMenus( cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/auth/menus"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: AxiosRequestConfig = { + responseType: "blob", + method: "GET", + url: url_, + headers: { + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processGetMenus(_response); + }); + } + + protected processGetMenus(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + getPermissions( cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/auth/permissions"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: AxiosRequestConfig = { + responseType: "blob", + method: "GET", + url: url_, + headers: { + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processGetPermissions(_response); + }); + } + + protected processGetPermissions(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + changePassword(dto: ChangePasswordDto, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/auth/password"; + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(dto); + + let options_: AxiosRequestConfig = { + data: content_, + responseType: "blob", + method: "PUT", + url: url_, + headers: { + "Content-Type": "application/json", + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processChangePassword(_response); + }); + } + + protected processChangePassword(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + updateProfile(dto: UpdateProfileDto, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/auth/profile"; + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(dto); + + let options_: AxiosRequestConfig = { + data: content_, + responseType: "blob", + method: "PUT", + url: url_, + headers: { + "Content-Type": "application/json", + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processUpdateProfile(_response); + }); + } + + protected processUpdateProfile(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } +} + +export class ApiClient { + protected instance: AxiosInstance; + protected baseUrl: string; + protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined; + + constructor(baseUrl?: string, instance?: AxiosInstance) { + + this.instance = instance || axios.create(); + + this.baseUrl = baseUrl ?? "http://localhost:3000"; + + } + + getTree( cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/department/tree"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: AxiosRequestConfig = { + responseType: "blob", + method: "GET", + url: url_, + headers: { + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processGetTree(_response); + }); + } + + protected processGetTree(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + create(dto: CreateDepartmentDto, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/department"; + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(dto); + + let options_: AxiosRequestConfig = { + data: content_, + responseType: "blob", + method: "POST", + url: url_, + headers: { + "Content-Type": "application/json", + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processCreate(_response); + }); + } + + protected processCreate(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + update(id: number, dto: UpdateDepartmentDto, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/department/{id}"; + if (id === undefined || id === null) + throw new Error("The parameter 'id' must be defined."); + url_ = url_.replace("{id}", encodeURIComponent("" + id)); + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(dto); + + let options_: AxiosRequestConfig = { + data: content_, + responseType: "blob", + method: "PUT", + url: url_, + headers: { + "Content-Type": "application/json", + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processUpdate(_response); + }); + } + + protected processUpdate(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + delete(id: number, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/department/{id}"; + if (id === undefined || id === null) + throw new Error("The parameter 'id' must be defined."); + url_ = url_.replace("{id}", encodeURIComponent("" + id)); + url_ = url_.replace(/[?&]$/, ""); + + let options_: AxiosRequestConfig = { + responseType: "blob", + method: "DELETE", + url: url_, + headers: { + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processDelete(_response); + }); + } + + protected processDelete(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } +} + +export class ApiClient { + protected instance: AxiosInstance; + protected baseUrl: string; + protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined; + + constructor(baseUrl?: string, instance?: AxiosInstance) { + + this.instance = instance || axios.create(); + + this.baseUrl = baseUrl ?? "http://localhost:3000"; + + } + + getPaged(keyword: string | null | undefined, departmentId: number | null | undefined, status: number | null | undefined, pageIndex: number | undefined, pageSize: number | undefined, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/employee?"; + if (keyword !== undefined && keyword !== null) + url_ += "Keyword=" + encodeURIComponent("" + keyword) + "&"; + if (departmentId !== undefined && departmentId !== null) + url_ += "DepartmentId=" + encodeURIComponent("" + departmentId) + "&"; + if (status !== undefined && status !== null) + url_ += "Status=" + encodeURIComponent("" + status) + "&"; + if (pageIndex === null) + throw new Error("The parameter 'pageIndex' cannot be null."); + else if (pageIndex !== undefined) + url_ += "PageIndex=" + encodeURIComponent("" + pageIndex) + "&"; + if (pageSize === null) + throw new Error("The parameter 'pageSize' cannot be null."); + else if (pageSize !== undefined) + url_ += "PageSize=" + encodeURIComponent("" + pageSize) + "&"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: AxiosRequestConfig = { + responseType: "blob", + method: "GET", + url: url_, + headers: { + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processGetPaged(_response); + }); + } + + protected processGetPaged(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + create(dto: CreateEmployeeDto, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/employee"; + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(dto); + + let options_: AxiosRequestConfig = { + data: content_, + responseType: "blob", + method: "POST", + url: url_, + headers: { + "Content-Type": "application/json", + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processCreate(_response); + }); + } + + protected processCreate(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + getById(id: number, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/employee/{id}"; + if (id === undefined || id === null) + throw new Error("The parameter 'id' must be defined."); + url_ = url_.replace("{id}", encodeURIComponent("" + id)); + url_ = url_.replace(/[?&]$/, ""); + + let options_: AxiosRequestConfig = { + responseType: "blob", + method: "GET", + url: url_, + headers: { + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processGetById(_response); + }); + } + + protected processGetById(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + update(id: number, dto: UpdateEmployeeDto, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/employee/{id}"; + if (id === undefined || id === null) + throw new Error("The parameter 'id' must be defined."); + url_ = url_.replace("{id}", encodeURIComponent("" + id)); + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(dto); + + let options_: AxiosRequestConfig = { + data: content_, + responseType: "blob", + method: "PUT", + url: url_, + headers: { + "Content-Type": "application/json", + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processUpdate(_response); + }); + } + + protected processUpdate(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + delete(id: number, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/employee/{id}"; + if (id === undefined || id === null) + throw new Error("The parameter 'id' must be defined."); + url_ = url_.replace("{id}", encodeURIComponent("" + id)); + url_ = url_.replace(/[?&]$/, ""); + + let options_: AxiosRequestConfig = { + responseType: "blob", + method: "DELETE", + url: url_, + headers: { + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processDelete(_response); + }); + } + + protected processDelete(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + resetPassword(id: number, dto: ResetPasswordDto, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/employee/{id}/password"; + if (id === undefined || id === null) + throw new Error("The parameter 'id' must be defined."); + url_ = url_.replace("{id}", encodeURIComponent("" + id)); + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(dto); + + let options_: AxiosRequestConfig = { + data: content_, + responseType: "blob", + method: "PUT", + url: url_, + headers: { + "Content-Type": "application/json", + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processResetPassword(_response); + }); + } + + protected processResetPassword(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } +} + +export class ApiClient { + protected instance: AxiosInstance; + protected baseUrl: string; + protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined; + + constructor(baseUrl?: string, instance?: AxiosInstance) { + + this.instance = instance || axios.create(); + + this.baseUrl = baseUrl ?? "http://localhost:3000"; + + } + + getTree( cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/menu/tree"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: AxiosRequestConfig = { + responseType: "blob", + method: "GET", + url: url_, + headers: { + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processGetTree(_response); + }); + } + + protected processGetTree(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + getAll( cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/menu/all"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: AxiosRequestConfig = { + responseType: "blob", + method: "GET", + url: url_, + headers: { + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processGetAll(_response); + }); + } + + protected processGetAll(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + create(dto: CreateMenuDto, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/menu"; + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(dto); + + let options_: AxiosRequestConfig = { + data: content_, + responseType: "blob", + method: "POST", + url: url_, + headers: { + "Content-Type": "application/json", + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processCreate(_response); + }); + } + + protected processCreate(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + update(id: number, dto: UpdateMenuDto, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/menu/{id}"; + if (id === undefined || id === null) + throw new Error("The parameter 'id' must be defined."); + url_ = url_.replace("{id}", encodeURIComponent("" + id)); + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(dto); + + let options_: AxiosRequestConfig = { + data: content_, + responseType: "blob", + method: "PUT", + url: url_, + headers: { + "Content-Type": "application/json", + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processUpdate(_response); + }); + } + + protected processUpdate(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + delete(id: number, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/menu/{id}"; + if (id === undefined || id === null) + throw new Error("The parameter 'id' must be defined."); + url_ = url_.replace("{id}", encodeURIComponent("" + id)); + url_ = url_.replace(/[?&]$/, ""); + + let options_: AxiosRequestConfig = { + responseType: "blob", + method: "DELETE", + url: url_, + headers: { + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processDelete(_response); + }); + } + + protected processDelete(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } +} + +export class ApiClient { + protected instance: AxiosInstance; + protected baseUrl: string; + protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined; + + constructor(baseUrl?: string, instance?: AxiosInstance) { + + this.instance = instance || axios.create(); + + this.baseUrl = baseUrl ?? "http://localhost:3000"; + + } + + getAll( cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/role"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: AxiosRequestConfig = { + responseType: "blob", + method: "GET", + url: url_, + headers: { + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processGetAll(_response); + }); + } + + protected processGetAll(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + create(dto: CreateRoleDto, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/role"; + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(dto); + + let options_: AxiosRequestConfig = { + data: content_, + responseType: "blob", + method: "POST", + url: url_, + headers: { + "Content-Type": "application/json", + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processCreate(_response); + }); + } + + protected processCreate(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + getById(id: number, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/role/{id}"; + if (id === undefined || id === null) + throw new Error("The parameter 'id' must be defined."); + url_ = url_.replace("{id}", encodeURIComponent("" + id)); + url_ = url_.replace(/[?&]$/, ""); + + let options_: AxiosRequestConfig = { + responseType: "blob", + method: "GET", + url: url_, + headers: { + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processGetById(_response); + }); + } + + protected processGetById(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + update(id: number, dto: UpdateRoleDto, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/role/{id}"; + if (id === undefined || id === null) + throw new Error("The parameter 'id' must be defined."); + url_ = url_.replace("{id}", encodeURIComponent("" + id)); + url_ = url_.replace(/[?&]$/, ""); + + const content_ = JSON.stringify(dto); + + let options_: AxiosRequestConfig = { + data: content_, + responseType: "blob", + method: "PUT", + url: url_, + headers: { + "Content-Type": "application/json", + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processUpdate(_response); + }); + } + + protected processUpdate(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + delete(id: number, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/role/{id}"; + if (id === undefined || id === null) + throw new Error("The parameter 'id' must be defined."); + url_ = url_.replace("{id}", encodeURIComponent("" + id)); + url_ = url_.replace(/[?&]$/, ""); + + let options_: AxiosRequestConfig = { + responseType: "blob", + method: "DELETE", + url: url_, + headers: { + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processDelete(_response); + }); + } + + protected processDelete(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } + + getRoleMenus(id: number, cancelToken?: CancelToken): Promise { + let url_ = this.baseUrl + "/api/role/{id}/menus"; + if (id === undefined || id === null) + throw new Error("The parameter 'id' must be defined."); + url_ = url_.replace("{id}", encodeURIComponent("" + id)); + url_ = url_.replace(/[?&]$/, ""); + + let options_: AxiosRequestConfig = { + responseType: "blob", + method: "GET", + url: url_, + headers: { + "Accept": "application/octet-stream" + }, + cancelToken + }; + + return this.instance.request(options_).catch((_error: any) => { + if (isAxiosError(_error) && _error.response) { + return _error.response; + } else { + throw _error; + } + }).then((_response: AxiosResponse) => { + return this.processGetRoleMenus(_response); + }); + } + + protected processGetRoleMenus(response: AxiosResponse): Promise { + const status = response.status; + let _headers: any = {}; + if (response.headers && typeof response.headers === "object") { + for (const k in response.headers) { + if (response.headers.hasOwnProperty(k)) { + _headers[k] = response.headers[k]; + } + } + } + if (status === 200 || status === 206) { + const contentDisposition = response.headers ? response.headers["content-disposition"] : undefined; + let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined; + let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined; + if (fileName) { + fileName = decodeURIComponent(fileName); + } else { + fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined; + fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined; + } + return Promise.resolve({ fileName: fileName, status: status, data: new Blob([response.data], { type: response.headers["content-type"] }), headers: _headers }); + } else if (status !== 200 && status !== 204) { + const _responseText = response.data; + return throwException("An unexpected server error occurred.", status, _responseText, _headers); + } + return Promise.resolve(null as any); + } +} + +export interface LoginDto { + username?: string | undefined; + password?: string | undefined; +} + +export interface RefreshTokenDto { + refreshToken?: string | undefined; +} + +export interface ChangePasswordDto { + oldPassword?: string | undefined; + newPassword?: string | undefined; +} + +export interface UpdateProfileDto { + nickname?: string | undefined; + avatar?: string | undefined; + phone?: string | undefined; + email?: string | undefined; +} + +export interface CreateDepartmentDto { + name?: string | undefined; + parentId?: number | undefined; + leaderId?: number | undefined; + phone?: string | undefined; + sort?: number; +} + +export interface UpdateDepartmentDto { + name?: string | undefined; + parentId?: number | undefined; + leaderId?: number | undefined; + phone?: string | undefined; + sort?: number; + status?: number; +} + +export interface CreateEmployeeDto { + username?: string | undefined; + password?: string | undefined; + realName?: string | undefined; + nickname?: string | undefined; + gender?: string | undefined; + phone?: string | undefined; + email?: string | undefined; + departmentId?: number; + remark?: string | undefined; + roleIds?: number[] | undefined; +} + +export interface UpdateEmployeeDto { + realName?: string | undefined; + nickname?: string | undefined; + gender?: string | undefined; + phone?: string | undefined; + email?: string | undefined; + departmentId?: number; + status?: number; + remark?: string | undefined; + roleIds?: number[] | undefined; +} + +export interface ResetPasswordDto { + newPassword?: string | undefined; +} + +export interface CreateMenuDto { + parentId?: number | undefined; + name?: string | undefined; + type?: string | undefined; + path?: string | undefined; + component?: string | undefined; + icon?: string | undefined; + permission?: string | undefined; + sort?: number; +} + +export interface UpdateMenuDto { + parentId?: number | undefined; + name?: string | undefined; + type?: string | undefined; + path?: string | undefined; + component?: string | undefined; + icon?: string | undefined; + permission?: string | undefined; + sort?: number; + status?: number; +} + +export interface CreateRoleDto { + name?: string | undefined; + code?: string | undefined; + description?: string | undefined; + sort?: number; + menuIds?: number[] | undefined; +} + +export interface UpdateRoleDto { + name?: string | undefined; + description?: string | undefined; + sort?: number; + status?: number; + menuIds?: number[] | undefined; +} + +export interface FileResponse { + data: Blob; + status: number; + fileName?: string; + headers?: { [name: string]: any }; +} + +export class ApiException extends Error { + override message: string; + status: number; + response: string; + headers: { [key: string]: any; }; + result: any; + + constructor(message: string, status: number, response: string, headers: { [key: string]: any; }, result: any) { + super(); + + this.message = message; + this.status = status; + this.response = response; + this.headers = headers; + this.result = result; + } + + protected isApiException = true; + + static isApiException(obj: any): obj is ApiException { + return obj.isApiException === true; + } +} + +function throwException(message: string, status: number, response: string, headers: { [key: string]: any; }, result?: any): any { + if (result !== null && result !== undefined) + throw result; + else + throw new ApiException(message, status, response, headers, null); +} + +function isAxiosError(obj: any): obj is AxiosError { + return obj && obj.isAxiosError === true; +} \ No newline at end of file diff --git a/web/src/directives/permission.js b/web/src/directives/permission.js new file mode 100644 index 0000000..b4ac80c --- /dev/null +++ b/web/src/directives/permission.js @@ -0,0 +1,24 @@ +import { useUserStore } from '../store/user'; + +// 权限判断工具函数 +export function hasPermission(value) { + const userStore = useUserStore(); + const permissions = userStore.permissions || []; + // admin 角色拥有所有权限 + const roles = userStore.userInfo?.roles || []; + if (roles.includes('admin')) return true; + + if (Array.isArray(value)) { + return value.some((p) => permissions.includes(p)); + } + return permissions.includes(value); +} + +// 权限指令 v-permission="'system:user:add'" +export default { + mounted(el, binding) { + if (!hasPermission(binding.value)) { + el.parentNode && el.parentNode.removeChild(el); + } + } +}; diff --git a/web/src/layout/EmptyLayout.vue b/web/src/layout/EmptyLayout.vue new file mode 100644 index 0000000..98240ae --- /dev/null +++ b/web/src/layout/EmptyLayout.vue @@ -0,0 +1,3 @@ + diff --git a/web/src/layout/index.vue b/web/src/layout/index.vue new file mode 100644 index 0000000..df1fb56 --- /dev/null +++ b/web/src/layout/index.vue @@ -0,0 +1,299 @@ + + + + + diff --git a/web/src/main.js b/web/src/main.js new file mode 100644 index 0000000..c6b250b --- /dev/null +++ b/web/src/main.js @@ -0,0 +1,57 @@ +import { createApp } from 'vue'; +import { createPinia } from 'pinia'; +import ElementPlus from 'element-plus'; +import 'element-plus/dist/index.css'; +import zhCn from 'element-plus/es/locale/lang/zh-cn'; +import * as ElementPlusIconsVue from '@element-plus/icons-vue'; +import App from './App.vue'; +import router, { buildRoutes, markRoutesLoaded } from './router'; +import permissionDirective from './directives/permission'; +import { getAccessToken } from './utils/request'; +import request from './utils/request'; +import './styles/index.css'; +import './styles/theme.css'; + +const app = createApp(App); + +// 注册所有图标 +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component); +} + +// 注册权限指令 +app.directive('permission', permissionDirective); + +app.use(createPinia()); + +// 在挂载前同步初始化动态路由(避免 "No match" 警告) +async function bootstrap() { + if (getAccessToken()) { + try { + const [menusRes, permsRes] = await Promise.all([ + request.get('/auth/menus'), + request.get('/auth/permissions') + ]); + const routes = buildRoutes(menusRes.data); + routes.forEach((r) => { + if (!router.hasRoute(r.name)) { + router.addRoute('Layout', r); + } + }); + // 缓存到 store + const { useUserStore } = await import('./store/user'); + const store = useUserStore(); + store.menus = menusRes.data; + store.permissions = permsRes.data; + markRoutesLoaded(); + } catch (e) { + // 菜单加载失败,可能是 token 失效,交给路由守卫跳登录 + } + } + + app.use(router); + app.use(ElementPlus, { locale: zhCn }); + app.mount('#app'); +} + +bootstrap(); diff --git a/web/src/router/index.js b/web/src/router/index.js new file mode 100644 index 0000000..fc91232 --- /dev/null +++ b/web/src/router/index.js @@ -0,0 +1,169 @@ +import { createRouter, createWebHistory } from 'vue-router'; +import { getAccessToken } from '../utils/request'; +import request from '../utils/request'; + +// 静态路由 +export const constantRoutes = [ + { + path: '/login', + name: 'Login', + component: () => import('../views/login/index.vue'), + meta: { title: '登录', hidden: true } + }, + { + path: '/', + name: 'Layout', + component: () => import('../layout/index.vue'), + redirect: '/dashboard', + children: [ + { + path: 'dashboard', + name: 'Dashboard', + component: () => import('../views/dashboard/index.vue'), + meta: { title: '首页', icon: 'HomeFilled' } + }, + // 请假审批:静态注册,所有登录用户可访问(待办消息"去处理"跳转入口,不依赖菜单权限) + { + path: 'attendance/approve', + name: 'AttendanceApprove', + component: () => import('../views/attendance/approve/index.vue'), + meta: { title: '请假审批', icon: 'Checked' } + } + ] + } +]; + +// 已静态注册的路由(不随菜单动态注册,避免重复) +const STATIC_ROUTES = ['/attendance/approve']; + +// 组件映射:后端菜单的 component 字段 -> 前端组件 +export const componentMap = { + 'system/department/index': () => import('../views/system/department/index.vue'), + 'system/employee/index': () => import('../views/system/employee/index.vue'), + 'system/role/index': () => import('../views/system/role/index.vue'), + 'system/menu/index': () => import('../views/system/menu/index.vue'), + 'system/log/index': () => import('../views/system/log/index.vue'), + 'attendance/punch/index': () => import('../views/attendance/punch/index.vue'), + 'attendance/my/index': () => import('../views/attendance/my/index.vue'), + 'attendance/leave/index': () => import('../views/attendance/leave/index.vue'), + 'attendance/records/index': () => import('../views/attendance/records/index.vue'), + 'attendance/approve/index': () => import('../views/attendance/approve/index.vue'), + 'base/zhuangkou/index': () => import('../views/base/zhuangkou/index.vue'), + 'base/guige/index': () => import('../views/base/guige/index.vue'), + 'base/dingjizhibiao/index': () => import('../views/base/dingjizhibiao/index.vue'), + 'base/jizuchetai/index': () => import('../views/base/jizuchetai/index.vue'), + 'prod/lsjl/index': () => import('../views/prod/lsjl/index.vue'), + 'prod/xdjy/index': () => import('../views/prod/xdjy/index.vue'), + 'prod/hbjy/index': () => import('../views/prod/hbjy/index.vue'), + 'notification/messages/index': () => import('../views/notification/messages/index.vue'), + 'notification/subscribe/index': () => import('../views/notification/subscribe/index.vue'), + 'notification/type/index': () => import('../views/notification/type/index.vue'), + 'sys/user/index': () => import('../views/sys/user/index.vue'), + 'sys/role/index': () => import('../views/sys/role/index.vue'), + 'sys/menu/index': () => import('../views/sys/menu/index.vue'), + 'sys/department/index': () => import('../views/sys/department/index.vue') +}; + +// 空壳布局组件(用于目录节点,透传子路由) +const EmptyLayout = () => import('../layout/EmptyLayout.vue'); + +const router = createRouter({ + history: createWebHistory(), + routes: constantRoutes +}); + +// 白名单 +const whiteList = ['/login']; + +// 动态路由是否已注册 +let routesLoaded = false; + +// 标记路由已加载(main.js bootstrap 调用) +export function markRoutesLoaded() { + routesLoaded = true; +} + +router.beforeEach(async (to, from, next) => { + const token = getAccessToken(); + if (token) { + if (to.path === '/login') { + next({ path: '/' }); + return; + } + // 兜底:如果 bootstrap 未注册路由(比如登录后首次跳转),这里加载 + if (!routesLoaded) { + try { + const [menusRes, permsRes] = await Promise.all([ + request.get('/auth/menus'), + request.get('/auth/permissions') + ]); + const menus = menusRes.data; + const routes = buildRoutes(menus); + routes.forEach((r) => { + if (!router.hasRoute(r.name)) { + router.addRoute('Layout', r); + } + }); + const { useUserStore } = await import('../store/user'); + const store = useUserStore(); + store.menus = menus; + store.permissions = permsRes.data; + routesLoaded = true; + // 重新导航到目标路由(此时路由已注册) + next({ ...to, replace: true }); + return; + } catch (e) { + next('/login'); + return; + } + } + next(); + } else { + if (whiteList.includes(to.path)) { + next(); + } else { + next(`/login?redirect=${to.path}`); + } + } +}); + +// 重置路由加载状态(退出登录时调用) +export function resetRoutes() { + routesLoaded = false; +} + +// 将后端菜单树转换为 Vue Router 路由(平铺注册到 Layout 下,用绝对路径) +export function buildRoutes(menus) { + const routes = []; + const walk = (list) => { + list.forEach((menu) => { + // 只处理目录和菜单类型(按钮不生成路由) + if (menu.type === 'directory') { + routes.push({ + path: menu.path, + name: menu.path.replace(/\//g, '-') + '-' + menu.id, + component: EmptyLayout, + meta: { title: menu.name, icon: menu.icon } + }); + } else if (menu.type === 'menu') { + // 已静态注册的路由跳过,避免重复 + if (STATIC_ROUTES.includes(menu.path)) return; + routes.push({ + path: menu.path, + name: menu.path.replace(/\//g, '-') + '-' + menu.id, + component: componentMap[menu.component] || (() => import('../views/system/empty.vue')), + meta: { title: menu.name, icon: menu.icon } + }); + } + // 递归处理子节点(平铺) + if (menu.children && menu.children.length > 0) { + walk(menu.children); + } + }); + }; + + walk(menus); + return routes; +} + +export default router; diff --git a/web/src/store/user.js b/web/src/store/user.js new file mode 100644 index 0000000..78d2b1b --- /dev/null +++ b/web/src/store/user.js @@ -0,0 +1,43 @@ +import { defineStore } from 'pinia'; +import request, { getAccessToken, setTokens, clearTokens } from '../utils/request'; + +export const useUserStore = defineStore('user', { + state: () => ({ + token: getAccessToken() || '', + userInfo: JSON.parse(localStorage.getItem('user') || 'null'), + menus: [], + permissions: [] + }), + getters: { + isLoggedIn: (state) => !!state.token + }, + actions: { + async login(loginForm) { + const res = await request.post('/auth/login', loginForm); + const { accessToken, refreshToken, user } = res.data; + setTokens(accessToken, refreshToken); + this.token = accessToken; + this.userInfo = user; + localStorage.setItem('user', JSON.stringify(user)); + return res; + }, + async fetchMenus() { + const res = await request.get('/auth/menus'); + this.menus = res.data; + return res.data; + }, + async fetchPermissions() { + const res = await request.get('/auth/permissions'); + this.permissions = res.data; + return res.data; + }, + logout() { + clearTokens(); + this.token = ''; + this.userInfo = null; + this.menus = []; + this.permissions = []; + localStorage.removeItem('user'); + } + } +}); diff --git a/web/src/styles/index.css b/web/src/styles/index.css new file mode 100644 index 0000000..e98c049 --- /dev/null +++ b/web/src/styles/index.css @@ -0,0 +1,20 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body, #app { + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; +} + +body { + background: #f6f4ef; + -webkit-font-smoothing: antialiased; +} + +a { + text-decoration: none; + color: inherit; +} diff --git a/web/src/styles/theme.css b/web/src/styles/theme.css new file mode 100644 index 0000000..da603af --- /dev/null +++ b/web/src/styles/theme.css @@ -0,0 +1,33 @@ +/* ============================================================ + 智慧缫丝系统主题 + 蚕桑黛绿(沉稳专业)+ 丝绸金(光泽点缀)+ 暖白(丝绸质感) + ============================================================ */ +:root { + /* Element Plus 主色改为黛绿 */ + --el-color-primary: #1e6b52; + --el-color-primary-light-3: #4d8f74; + --el-color-primary-light-5: #82b39d; + --el-color-primary-light-7: #bad5c8; + --el-color-primary-light-8: #d6e7de; + --el-color-primary-light-9: #ecf4f0; + --el-color-primary-dark-2: #175544; + + /* 成功色统一为黛绿系 */ + --el-color-success: #1e6b52; + --el-color-success-light-3: #4d8f74; + --el-color-success-light-5: #82b39d; + --el-color-success-light-7: #bad5c8; + --el-color-success-light-8: #d6e7de; + --el-color-success-light-9: #ecf4f0; + --el-color-success-dark-2: #175544; + + /* 自定义全局变量 */ + --lc-primary: #1e6b52; /* 主色:黛绿 */ + --lc-primary-dark: #175544; /* 深黛绿 */ + --lc-primary-deep: #0c3b2e; /* 墨绿(侧边栏) */ + --lc-accent: #c9a54a; /* 丝绸金 */ + --lc-accent-light: #e8c877; + --lc-bg: #f6f4ef; /* 暖白背景(丝绸质感) */ + --lc-text: #2c3a35; /* 主文字 */ + --lc-text-secondary: #6b7a74; /* 次要文字 */ +} diff --git a/web/src/utils/dept.js b/web/src/utils/dept.js new file mode 100644 index 0000000..66f8a27 --- /dev/null +++ b/web/src/utils/dept.js @@ -0,0 +1,14 @@ +// 新部门接口(/sys/department)返回扁平列表,这里转换为 el-tree 所需树结构 +export function buildDeptTree(list) { + const map = {}; + (list || []).forEach((d) => { + map[d.id] = { id: d.id, name: d.name, code: d.code, parentId: d.parentId, children: [] }; + }); + const roots = []; + (list || []).forEach((d) => { + const node = map[d.id]; + if (d.parentId && map[d.parentId]) map[d.parentId].children.push(node); + else roots.push(node); + }); + return roots; +} diff --git a/web/src/utils/notification.js b/web/src/utils/notification.js new file mode 100644 index 0000000..272af0c --- /dev/null +++ b/web/src/utils/notification.js @@ -0,0 +1,99 @@ +import { ref } from 'vue'; +import * as signalR from '@microsoft/signalr'; +import { getAccessToken } from './request'; +import request from './request'; + +// 全局未读数量(layout 徽章 与 消息页共用) +export const unreadCount = ref(0); + +let connection = null; +const messageHandlers = []; +const businessChangeHandlers = []; + +/** 加载未读数量 */ +export async function loadUnreadCount() { + try { + const res = await request.get('/notification/unread-count'); + unreadCount.value = res.data.count; + } catch (e) {} +} + +/** + * 初始化 SignalR 通知连接(幂等,重复调用返回同一连接) + * @param {Function} onMessage 收到新消息时的回调 (msg) => void + */ +export function initNotification(onMessage) { + if (connection) { + if (onMessage) messageHandlers.push(onMessage); + return connection; + } + + if (onMessage) messageHandlers.push(onMessage); + + connection = new signalR.HubConnectionBuilder() + .withUrl('/hubs/notification', { + accessTokenFactory: () => getAccessToken() || '' + }) + .withAutomaticReconnect() + .configureLogging(signalR.LogLevel.Warning) + .build(); + + connection.on('ReceiveNotification', (msg) => { + unreadCount.value++; // 全局未读 +1 + messageHandlers.forEach((h) => { try { h(msg); } catch (e) {} }); + }); + + // 业务状态变更事件(多端同步:某端操作后,其它端刷新已打开的页面) + connection.on('BusinessChanged', (e) => { + businessChangeHandlers.forEach((h) => { try { h(e); } catch (err) {} }); + }); + + connection.start().catch(() => {}); + return connection; +} + +/** + * 订阅业务状态变更事件(用于多端同步刷新)。 + * @param {Function} handler (e) => void,e 含 businessType / businessId / action + */ +export function onBusinessChanged(handler) { + if (handler) businessChangeHandlers.push(handler); + initNotification(); // 确保连接已建立(幂等) +} + +/** + * 打开消息:标记已读 + 跳转到业务页面(消息自带 routePath 时)。 + * @param {Object} msg 消息对象(含 id/readStatus/routePath/category) + * @param {Object} router vue-router 实例 + * @returns {boolean} 是否发生了页面跳转 + */ +export async function openMessage(msg, router) { + // 待办消息(category=1)点击"去处理"不标记已读,保持未读提醒; + // 只有真正完成待办后(后端完成待办时会标记已读)才消失 + if (msg.readStatus === 0 && msg.category !== 1) { + try { + await request.put(`/notification/messages/${msg.id}/read`); + msg.readStatus = 1; + loadUnreadCount(); + } catch (e) {} + } + if (msg.routePath) { + router.push(msg.routePath); + return true; + } + return false; +} + +/** 停止连接(登出时调用) */ +export function stopNotification() { + if (connection) { + connection.stop().catch(() => {}); + connection = null; + } + messageHandlers.length = 0; + unreadCount.value = 0; +} + +export function getNotificationConnection() { + return connection; +} diff --git a/web/src/utils/request.js b/web/src/utils/request.js new file mode 100644 index 0000000..902ee1c --- /dev/null +++ b/web/src/utils/request.js @@ -0,0 +1,112 @@ +import axios from 'axios'; +import { ElMessage } from 'element-plus'; + +const request = axios.create({ + baseURL: '/api', + timeout: 15000 +}); + +// 令牌存取 +const TOKEN_KEY = 'access_token'; +const REFRESH_KEY = 'refresh_token'; + +export function getAccessToken() { + return localStorage.getItem(TOKEN_KEY); +} +export function setTokens(accessToken, refreshToken) { + localStorage.setItem(TOKEN_KEY, accessToken); + localStorage.setItem(REFRESH_KEY, refreshToken); +} +export function clearTokens() { + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem(REFRESH_KEY); +} +export function getRefreshToken() { + return localStorage.getItem(REFRESH_KEY); +} + +// 请求拦截:附加 access token +request.interceptors.request.use((config) => { + const token = getAccessToken(); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +// 刷新令牌(避免并发重复刷新) +let isRefreshing = false; +let pendingQueue = []; + +async function refreshAccessToken() { + const refreshToken = getRefreshToken(); + if (!refreshToken) return null; + try { + const res = await axios.post('/api/auth/refresh', { refreshToken }); + if (res.data?.code === 200) { + setTokens(res.data.data.accessToken, res.data.data.refreshToken); + return res.data.data.accessToken; + } + return null; + } catch (e) { + return null; + } +} + +// 响应拦截 +request.interceptors.response.use( + (res) => { + const data = res.data; + if (data.code === 200) { + return data; + } + ElMessage.error(data.message || '请求失败'); + return Promise.reject(new Error(data.message || '请求失败')); + }, + async (err) => { + const { response, config } = err; + + // 401:尝试用 refresh token 刷新 access token 后重试 + if (response?.status === 401 && !config._retry && getRefreshToken()) { + if (isRefreshing) { + // 等待正在进行的刷新完成 + return new Promise((resolve) => { + pendingQueue.push((newToken) => { + if (newToken) { + config.headers.Authorization = `Bearer ${newToken}`; + resolve(request(config)); + } else { + resolve(Promise.reject(err)); + } + }); + }); + } + + config._retry = true; + isRefreshing = true; + const newToken = await refreshAccessToken(); + isRefreshing = false; + + if (newToken) { + pendingQueue.forEach((cb) => cb(newToken)); + pendingQueue = []; + config.headers.Authorization = `Bearer ${newToken}`; + return request(config); + } else { + pendingQueue.forEach((cb) => cb(null)); + pendingQueue = []; + clearTokens(); + localStorage.removeItem('user'); + ElMessage.error('登录已过期,请重新登录'); + window.location.href = '/login'; + return Promise.reject(err); + } + } + + const msg = response?.data?.message || '请求失败,请稍后重试'; + ElMessage.error(msg); + return Promise.reject(err); + } +); + +export default request; diff --git a/web/src/views/attendance/approve/index.vue b/web/src/views/attendance/approve/index.vue new file mode 100644 index 0000000..c698ec6 --- /dev/null +++ b/web/src/views/attendance/approve/index.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/web/src/views/attendance/leave/index.vue b/web/src/views/attendance/leave/index.vue new file mode 100644 index 0000000..c4b7ab4 --- /dev/null +++ b/web/src/views/attendance/leave/index.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/web/src/views/attendance/my/index.vue b/web/src/views/attendance/my/index.vue new file mode 100644 index 0000000..3dfbf25 --- /dev/null +++ b/web/src/views/attendance/my/index.vue @@ -0,0 +1,158 @@ + + + + + diff --git a/web/src/views/attendance/punch/index.vue b/web/src/views/attendance/punch/index.vue new file mode 100644 index 0000000..653d015 --- /dev/null +++ b/web/src/views/attendance/punch/index.vue @@ -0,0 +1,207 @@ + + + + + diff --git a/web/src/views/attendance/records/index.vue b/web/src/views/attendance/records/index.vue new file mode 100644 index 0000000..542cb33 --- /dev/null +++ b/web/src/views/attendance/records/index.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/web/src/views/base/dingjizhibiao/index.vue b/web/src/views/base/dingjizhibiao/index.vue new file mode 100644 index 0000000..9b412e4 --- /dev/null +++ b/web/src/views/base/dingjizhibiao/index.vue @@ -0,0 +1,165 @@ + + + + + diff --git a/web/src/views/base/guige/index.vue b/web/src/views/base/guige/index.vue new file mode 100644 index 0000000..c0ec8dc --- /dev/null +++ b/web/src/views/base/guige/index.vue @@ -0,0 +1,177 @@ + + + + + diff --git a/web/src/views/base/jizuchetai/index.vue b/web/src/views/base/jizuchetai/index.vue new file mode 100644 index 0000000..ce4919c --- /dev/null +++ b/web/src/views/base/jizuchetai/index.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/web/src/views/base/zhuangkou/index.vue b/web/src/views/base/zhuangkou/index.vue new file mode 100644 index 0000000..f586642 --- /dev/null +++ b/web/src/views/base/zhuangkou/index.vue @@ -0,0 +1,164 @@ + + + + + diff --git a/web/src/views/dashboard/index.vue b/web/src/views/dashboard/index.vue new file mode 100644 index 0000000..aa9f6dd --- /dev/null +++ b/web/src/views/dashboard/index.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/web/src/views/login/index.vue b/web/src/views/login/index.vue new file mode 100644 index 0000000..bab6879 --- /dev/null +++ b/web/src/views/login/index.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/web/src/views/notification/messages/index.vue b/web/src/views/notification/messages/index.vue new file mode 100644 index 0000000..80ef136 --- /dev/null +++ b/web/src/views/notification/messages/index.vue @@ -0,0 +1,182 @@ + + + + + diff --git a/web/src/views/notification/subscribe/index.vue b/web/src/views/notification/subscribe/index.vue new file mode 100644 index 0000000..ad9ab09 --- /dev/null +++ b/web/src/views/notification/subscribe/index.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/web/src/views/notification/type/index.vue b/web/src/views/notification/type/index.vue new file mode 100644 index 0000000..7ab487a --- /dev/null +++ b/web/src/views/notification/type/index.vue @@ -0,0 +1,183 @@ + + + + + diff --git a/web/src/views/prod/hbjy/index.vue b/web/src/views/prod/hbjy/index.vue new file mode 100644 index 0000000..8470368 --- /dev/null +++ b/web/src/views/prod/hbjy/index.vue @@ -0,0 +1,300 @@ + + + + + diff --git a/web/src/views/prod/lsjl/index.vue b/web/src/views/prod/lsjl/index.vue new file mode 100644 index 0000000..a946a59 --- /dev/null +++ b/web/src/views/prod/lsjl/index.vue @@ -0,0 +1,283 @@ + + + + + diff --git a/web/src/views/prod/xdjy/index.vue b/web/src/views/prod/xdjy/index.vue new file mode 100644 index 0000000..e49757b --- /dev/null +++ b/web/src/views/prod/xdjy/index.vue @@ -0,0 +1,374 @@ + + + + + diff --git a/web/src/views/sys/department/index.vue b/web/src/views/sys/department/index.vue new file mode 100644 index 0000000..15d6911 --- /dev/null +++ b/web/src/views/sys/department/index.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/web/src/views/sys/menu/index.vue b/web/src/views/sys/menu/index.vue new file mode 100644 index 0000000..ffaae90 --- /dev/null +++ b/web/src/views/sys/menu/index.vue @@ -0,0 +1,163 @@ + + + + + diff --git a/web/src/views/sys/role/index.vue b/web/src/views/sys/role/index.vue new file mode 100644 index 0000000..cf6451d --- /dev/null +++ b/web/src/views/sys/role/index.vue @@ -0,0 +1,318 @@ + + + + + diff --git a/web/src/views/sys/user/index.vue b/web/src/views/sys/user/index.vue new file mode 100644 index 0000000..6dbe458 --- /dev/null +++ b/web/src/views/sys/user/index.vue @@ -0,0 +1,236 @@ + + + + + diff --git a/web/src/views/system/department/index.vue b/web/src/views/system/department/index.vue new file mode 100644 index 0000000..09f4b73 --- /dev/null +++ b/web/src/views/system/department/index.vue @@ -0,0 +1,175 @@ + + + + + diff --git a/web/src/views/system/employee/index.vue b/web/src/views/system/employee/index.vue new file mode 100644 index 0000000..77afe55 --- /dev/null +++ b/web/src/views/system/employee/index.vue @@ -0,0 +1,284 @@ + + + + + diff --git a/web/src/views/system/empty.vue b/web/src/views/system/empty.vue new file mode 100644 index 0000000..7b8b46c --- /dev/null +++ b/web/src/views/system/empty.vue @@ -0,0 +1,3 @@ + diff --git a/web/src/views/system/log/index.vue b/web/src/views/system/log/index.vue new file mode 100644 index 0000000..f7d7b32 --- /dev/null +++ b/web/src/views/system/log/index.vue @@ -0,0 +1,14 @@ + + + diff --git a/web/src/views/system/menu/index.vue b/web/src/views/system/menu/index.vue new file mode 100644 index 0000000..bad7ae1 --- /dev/null +++ b/web/src/views/system/menu/index.vue @@ -0,0 +1,187 @@ + + + + + diff --git a/web/src/views/system/role/index.vue b/web/src/views/system/role/index.vue new file mode 100644 index 0000000..73ace2f --- /dev/null +++ b/web/src/views/system/role/index.vue @@ -0,0 +1,197 @@ + + + + + diff --git a/web/vite.config.js b/web/vite.config.js new file mode 100644 index 0000000..c8a27ff --- /dev/null +++ b/web/vite.config.js @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; + +export default defineConfig({ + plugins: [vue()], + server: { + host: '0.0.0.0', + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true + }, + '/hubs': { + target: 'http://localhost:3000', + changeOrigin: true, + ws: true + } + } + } +}); diff --git a/web/消息推送系统软件设计说明书.docx b/web/消息推送系统软件设计说明书.docx new file mode 100644 index 0000000000000000000000000000000000000000..bc3a5331d1bb620ccda33b19c0b1285fc6edd81b GIT binary patch literal 20151 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 P3_L){2uz`}7ePD#N((9h literal 0 HcmV?d00001 diff --git a/消息推送系统_迁移指南.md b/消息推送系统_迁移指南.md new file mode 100644 index 0000000..b2fd037 --- /dev/null +++ b/消息推送系统_迁移指南.md @@ -0,0 +1,644 @@ +# 消息推送系统 迁移指南(迁移到已有 RBAC 系统) + +> 适用场景:目标系统与当前系统**架构一致**(.NET 8 分层 + FreeSql + MediatR + SignalR + `IRepository` 泛型仓储 + JWT)。 +> 本文按**可执行顺序**拆分为 14 步,每步含:**要建什么 → 关键代码要点 → 验证方法**。 +> 当前实现基线:`F9WebAiTest` 生活圈管理系统(消息推送 V2.0,即时推送,无后台服务)。 + +--- + +## 0. 迁移前须知 + +### 0.1 目标系统前置条件自查(缺一不可) + +| # | 依赖项 | 具体要求 | 不满足时的处理 | +|---|--------|---------|--------------| +| 1 | 运行时 | .NET 8 / ASP.NET Core | 必须 | +| 2 | ORM | FreeSql(`IFreeSql` 单例),支持 CodeFirst | 若用 EF Core,需重写仓储调用(数据访问层代码量约 200 行) | +| 3 | 仓储 | `IRepository` 泛型仓储,含 `GetByIdAsync / GetAllAsync / FindAsync / FirstOrDefaultAsync / AnyAsync / CountAsync / AddAsync / AddRangeAsync / UpdateAsync / DeleteAsync / HardDeleteAsync / HardDeleteRangeAsync / SaveChangesAsync` | 方法名不同需批量替换(约 40 处调用) | +| 4 | MediatR | 已接入 `AddMediatR(...)` | 必须(消息创建入口) | +| 5 | SignalR | .NET 8 内置 `Microsoft.AspNetCore.SignalR` | 必须(实时推送) | +| 6 | JWT | token 中含**用户 ID 声明** | 当前实现用 `"id"`;若目标系统用 `ClaimTypes.NameIdentifier` / `"userId"`,需改 **3 处**(见 4.1) | +| 7 | RBAC | `Permission`、`RolePermission` 实体;按用户查权限码的能力(`IAuthService.GetUserPermissionsAsync(userId)`);枚举含 `Resource_Type_Enum.消息订阅`、`Permission_Type_Enum.Visble` | 缺失则**订阅权限校验**与**角色分配订阅权限**两处功能需降级(可先改为“所有人可订阅”) | +| 8 | 主键生成 | 雪花 ID `ISnowflakeIdGenerator` | 或改用目标系统的主键策略 | +| 9 | 前端 | Vue3 + Vite + Element Plus + Pinia + axios 封装(`request`)+ 依赖 `@microsoft/signalr` | 缺 `@microsoft/signalr` 需 `npm i @microsoft/signalr` | +| 10 | 实体基类 | `BaseEntity`(含 `Id` + 审计字段 + 软删除 `IsDeleted`) | 必须(唯一索引/软删过滤依赖它) | + +### 0.2 命名空间与类型适配表(迁移时全局替换) + +| 当前系统 | 目标系统改为 | +|---------|-------------| +| `F9WebAiTest.Domain.Entities.Notice` | `{目标}.Domain.Entities.Notice`(或你们既有实体目录) | +| `F9WebAiTest.Application.Dtos.Notice` | `{目标}.Application.Dtos.Notice` | +| `F9WebAiTest.Application.Services.Notice` | `{目标}.Application.Services.Notice` | +| `F9WebAiTest.Api.Hubs` / `F9WebAiTest.Api.Services` | `{目标}.Api.Hubs` / `{目标}.Api.Services` | +| `BaseEntity` / `IRepository` / `ServiceException` | 目标系统的同名/等价类型 | +| `ApiResult` / `PageResult` | 目标系统的返回包装(**分页字段名可能不同**) | +| `SysPermission` / `SysRolePermission`(种子中的别名) | 目标系统权限/角色权限实体实际名 | +| `ISnowflakeIdGenerator` | 目标系统主键生成器 | +| JWT claim `"id"` | 目标系统用户 ID 声明名 | +| 前端 `request`(axios 封装)、`getAccessToken` | 目标系统对应工具 | + +--- + +## 1. 迁移文件总览 + +**后端(12 个文件)** + +| # | 源路径(F9WebAiTest) | 层 | 迁移动作 | +|---|---------------------|----|---------| +| 1 | `Domain/Entities/Notice/NotificationEnums.cs` | Domain | 新增 | +| 2 | `Domain/Entities/Notice/NotificationType.cs` | Domain | 新增 | +| 3 | `Domain/Entities/Notice/NotificationMessage.cs` | Domain | 新增 | +| 4 | `Domain/Entities/Notice/NotificationReceiver.cs` | Domain | 新增 | +| 5 | `Domain/Entities/Notice/TodoItem.cs` | Domain | 新增 | +| 6 | `Domain/Entities/Notice/UserNotificationSubscription.cs` | Domain | 新增 | +| 7 | `Application/Dtos/Notice/NotificationDtos.cs` | Application | 新增(9 个 DTO) | +| 8 | `Application/Services/Notice/INotificationPusher.cs` | Application | 新增(推送抽象,3 方法) | +| 9 | `Application/Services/Notice/NotificationService.cs` | Application | 新增(`INotificationService` 接口 + 实现,12 方法) | +| 10 | `Application/Services/Notice/NotificationMediatR.cs` | Application | 新增(创建命令 + **投递实现**) | +| 11 | `Api/Hubs/NotificationHub.cs` | Api | 新增(Hub + 在线表 + UserIdProvider) | +| 12 | `Api/Services/NotificationPusher.cs` | Api | 新增(推送实现) | +| 13 | `Api/Controllers/Notice/NotificationControllers.cs` | Api | 新增(2 控制器 / 15 端点) | +| 14 | `Infrastructure/Data/DbSeeder.cs` 的 `SeedNotificationData` | Infrastructure | **片段移植**(种子数据) | +| 15 | `Application/DependencyInjection.cs` + `Api/Program.cs` | 装配 | **片段修改**(DI 注册) | + +**前端(4 个文件)** + +| # | 源路径 | 动作 | +|---|--------|------| +| 16 | `web/src/utils/notification.js` | 新增 | +| 17 | `web/src/views/notification/messages/index.vue` | 新增 | +| 18 | `web/src/views/notification/subscribe/index.vue` | 新增 | +| 19 | `web/src/views/notification/type/index.vue` | 新增 | +| 20 | 路由 `componentMap`(或目标系统动态组件映射表) | **必改**(否则页面打不开,见 Step 14) | + +--- + +## 2. 分步实施 + +### Step 1|领域层:枚举 + +**做什么**:新增 `NotificationEnums.cs`,定义 6 个枚举。 + +```csharp +public enum NotificationPriority { Normal = 0, Important = 1, Urgent = 2 } // 优先级 +public enum NotificationStatus { Ready = 0, Sent = 1, Failed = 2, PartialSuccess = 3, Revoked = 4 } +public enum NotificationReadStatus { Unread = 0, Read = 1 } // 阅读状态 +public enum NotificationSendStatus { NotSent = 0, Success = 1, Failed = 2 } // 推送状态 +public enum TodoItemStatus { Pending = 0, Processing = 1, Completed = 2, Cancelled = 3 } +public enum NotificationCategory { Notification = 0, Todo = 1, Announcement = 2 } +public enum NotificationTypeEnum { TeyeFound = 1, TdcFound = 2, LeaveApproval = 3, LeaveResult = 4 } // 枚举名 = 消息类型 Code +``` + +**要点**:`NotificationTypeEnum` 的**枚举名必须与消息类型的 `Code` 一致**(业务侧按枚举名发送)。 + +**验证**:编译通过。 + +--- + +### Step 2|领域层:5 个实体 + +**做什么**:`Notice/` 目录下新增 5 个实体文件(每个实体独立文件)。 + +| 实体 | 表名 | 关键字段 | 索引(**必须**) | +|------|------|---------|----------------| +| `NotificationType` | `NotificationTypes` | `Code`(唯一)、`Name`、`Description`、`RequiredPermission`、`IsActive`、`CreatedBy`、`Sort`、`Category`、`Icon`、`RouteTemplate` | `Code` 唯一 | +| `NotificationMessage` | `NotificationMessages` | `NotificationTypeId`、`MessageTypeCode`(冗余)、`BusinessType`、`BusinessId`、`Title`、`Content`、`Priority`、`Status`、`RetryCount`、`MaxRetryCount`、`BizContext`、`ForceReceivers`(JSON)、`CreatorId`、`CreatorName`、`Category`(冗余)、`RoutePath` | `MessageTypeCode` 普通索引 | +| `NotificationReceiver` | `NotificationReceivers` | `MessageId`、`UserId`、`ReadStatus`、`ReadTime`、`IsOffline`、`SendStatus`、`SendTime`、`ErrorMsg` | **`MessageId + UserId` 唯一**、`UserId + ReadStatus` | +| `TodoItem` | `TodoItems` | `MessageId`、`AssigneeId`、`TodoStatus`、`DueTime`、`CompletedTime`、`HandleResult` | `AssigneeId + TodoStatus` | +| `UserNotificationSubscription` | `UserNotificationSubscriptions` | `UserId`、`NotificationTypeId`、`IsActive`、`SubscribedTime`、`CancelTime` | **`UserId + NotificationTypeId` 唯一** | + +**代码要点**: + +```csharp +[Table(Name = "NotificationMessages")] +[Index("IX_NotificationMessages_MessageTypeCode", "MessageTypeCode")] +public class NotificationMessage : BaseEntity +{ + public long NotificationTypeId { get; set; } + public string MessageTypeCode { get; set; } = ""; + public NotificationPriority Priority { get; set; } = NotificationPriority.Normal; + public NotificationStatus Status { get; set; } = NotificationStatus.Ready; + public string ForceReceivers { get; set; } = ""; // JSON 数组,如 "[1001,1002]",空 = 按订阅广播 + // …其余字段见源文件 +} +``` + +**要点**: +- 实体为**纯属性 POCO**(状态由应用服务赋值后 `UpdateAsync` 持久化),不要加仓储接口——统一用 `IRepository`。 +- 枚举字段在 FreeSql 中统一映射为 `int`(目标系统若没配,实体枚举会存成字符串,导致与其他表不一致)。 + +**验证**:CodeFirst 同步后 5 张表存在,且两个唯一索引已建立(见 Step 3)。 + +--- + +### Step 3|数据库:建表 + +**方式 A(推荐,与当前一致)**:FreeSql CodeFirst + +```csharp +// 在目标系统的表结构同步处(当前系统在 DbSeeder 内) +fsql.CodeFirst.SyncStructure(); +fsql.CodeFirst.SyncStructure(); +fsql.CodeFirst.SyncStructure(); +fsql.CodeFirst.SyncStructure(); +fsql.CodeFirst.SyncStructure(); +``` + +**方式 B(旧库已有表时)**:只补列 + +```csharp +EnsureColumn(fsql, "NotificationTypes", "Category", "INTEGER NOT NULL DEFAULT 0"); +EnsureColumn(fsql, "NotificationTypes", "Icon", "TEXT NOT NULL DEFAULT ''"); +EnsureColumn(fsql, "NotificationTypes", "RouteTemplate", "TEXT NOT NULL DEFAULT ''"); +EnsureColumn(fsql, "NotificationMessages", "Category", "INTEGER NOT NULL DEFAULT 0"); +EnsureColumn(fsql, "NotificationMessages", "RoutePath", "TEXT NOT NULL DEFAULT ''"); +``` + +**要点**:`SyncStructure` **不会为已存在的表新增列**,旧库必须走"补列"。 + +**验证**: + +```sql +SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Notification%'; +SELECT name FROM sqlite_master WHERE type='table' AND name='TodoItems'; +-- 唯一索引 +SELECT name, sql FROM sqlite_master WHERE type='index' AND tbl_name='NotificationReceivers'; +``` + +--- + +### Step 4|应用层:DTO(9 个) + +**做什么**:新增 `Application/Dtos/Notice/NotificationDtos.cs`。 + +| DTO | 类型 | 用途 | 关键字段 | +|-----|------|------|---------| +| `NotificationTypeDto` | 出参 | 类型列表/订阅列表 | `Id, Code, Name, Description, RequiredPermission, IsActive, CreatedBy, Sort, Category, Icon, RouteTemplate, **Subscribed**, **CanSubscribe**` | +| `NotificationTypeCreateDto` | 入参 | 创建/更新类型 | `Code, Name, Description, RequiredPermission, IsActive, Sort, Category, Icon, RouteTemplate` | +| `SubscribeDto` | 入参 | 订阅/退订 | `NotificationTypeId, Subscribe` | +| `NotificationMessageDto` | 出参 **+ SignalR 推送载荷** | 消息列表/实时推送 | `Id, NotificationTypeId, MessageTypeCode, TypeName, Title, Content, Priority, BusinessType, BusinessId, Category, RoutePath, CreatedTime, ReadStatus, ReadTime` | +| `UnreadCountDto` | 出参 | 未读数 | `Count` | +| `TodoItemDto` | 出参 | 待办列表 | `Id, MessageId, Title, Content, Priority, BusinessType, BusinessId, RoutePath, TodoStatus, DueTime, CompletedTime, CreatedTime` | +| `CompleteTodoDto` | 入参 | 完成待办 | `HandleResult` | +| `BroadcastNotificationDto` | 入参 | 广播 | `Title, Content, Priority, RoutePath` | +| `TriggerDto` | 入参 | 测试触发 | `Code, Title, Content, BusinessType, BusinessId, Priority, RoutePath, AssigneeIds` | + +**要点**: +- `NotificationMessageDto` **同时是 SignalR 推送载荷**(服务端直接推送该对象,前端按同名字段消费)——字段名必须与前端一致。 +- **待办类消息没有独立 DTO**:`TodoItemDto` 的 `Title/Content/RoutePath` 取自关联消息(查询时拼装)。 + +**验证**:编译通过;`NotificationMessageDto` 字段与前端 `messages/index.vue` 使用字段逐一对齐。 + +--- + +### Step 5|应用层:推送抽象接口 `INotificationPusher` + +**做什么**:新增 `Application/Services/Notice/INotificationPusher.cs`(**接口放应用层**)。 + +```csharp +public interface INotificationPusher +{ + bool IsOnline(long userId); + Task PushNewMessageAsync(long userId, NotificationMessageDto message, CancellationToken ct = default); + Task PushUnreadCountAsync(long userId, int unreadCount, int todoCount, CancellationToken ct = default); +} +``` + +**为什么必须有这层抽象**:投递逻辑在 Application 层,但 SignalR 的 `IHubContext` 属于 Api 层。若 Application 直接引用会形成循环依赖。接口在 Application、实现在 Api(依赖倒置)。 + +**验证**:编译通过(实现留到 Step 9)。 + +--- + +### Step 6|应用层:消息服务 `INotificationService` + `NotificationService` + +**做什么**:新增 `Application/Services/Notice/NotificationService.cs`(接口与实现同文件)。 + +**接口(12 方法)**: + +```csharp +public interface INotificationService +{ + // 订阅 + Task> GetSubscribeListAsync(long userId); + Task SubscribeAsync(long userId, long typeId, bool subscribe); + // 消息 + Task> GetMyMessagesAsync(long userId, int pageIndex, int pageSize, int? readStatus, int? category); + Task MarkReadAsync(long userId, long messageId); + Task MarkAllReadAsync(long userId); + Task GetUnreadCountAsync(long userId); + // 待办 + Task> GetMyTodosAsync(long userId, int? status); + Task CompleteTodoAsync(long userId, long todoId, string handleResult); + // 管理端:类型维护 + Task> GetTypeListAsync(); + Task CreateTypeAsync(NotificationTypeCreateDto dto, long creatorId); + Task UpdateTypeAsync(long id, NotificationTypeCreateDto dto); + Task DeleteTypeAsync(long id); +} +``` + +**依赖注入(构造函数)**: + +```csharp +IRepository _typeRepo; +IRepository _subRepo; +IRepository _msgRepo; +IRepository _receiverRepo; +IRepository _todoRepo; +IRepository _roleMenuRepo; // 目标系统权限关联实体 +IRepository _permissionRepo; // 目标系统权限实体 +ISnowflakeIdGenerator _snowflake; +IAuthService _authService; // GetUserPermissionsAsync(userId) +``` + +**实现要点(4 处易错点)**: + +1. **查询以接收人表为主表**:`GetMyMessagesAsync` 先查 `NotificationReceiver(UserId)`,再联消息与类型名,`ReadStatus` 取自接收人(同一消息对不同用户状态不同)。 +2. **未读数**:`CountAsync(r => r.UserId == userId && r.ReadStatus == Unread)`。 +3. **完成待办后标记该消息已读**:避免"处理完了还挂未读红点"。 +4. **RBAC 联动 `SyncSubscribePermissionAsync`**(`CreateTypeAsync` / `UpdateTypeAsync` 后调用,`DeleteTypeAsync` 反向清理): + - `RequiredPermission` 为空 → 删除该类型对应的权限单元及其角色关联(所有人可订阅,无需权限) + - 非空 → 按 `ResourceType = Resource_Type_Enum.消息订阅 && ResourceId = type.Id` 查或建 `Permission`,并 `permission.SetCode(type.RequiredPermission)` 覆盖权限码 + - **必须覆盖权限码**:否则默认生成的码(如 `subscribe:3:visble`)与 `RequiredPermission` 不一致,订阅校验永远失败 + +**验证**:调 `GET /api/notification/subscribe` 返回类型列表且含 `subscribed` / `canSubscribe`。 + +--- + +### Step 7|应用层:MediatR 命令 + 投递实现(核心步骤) + +**做什么**:新增 `Application/Services/Notice/NotificationMediatR.cs`。 + +**两类命令**: + +```csharp +// ① 底层命令(按 Code,返回消息ID) +public class CreateNotificationCommand : IRequest +{ + public string NotificationTypeCode { get; set; } = ""; + public string BusinessType { get; set; } = ""; + public string BusinessId { get; set; } = ""; + public string Title { get; set; } = ""; + public string Content { get; set; } = ""; + public NotificationPriority Priority { get; set; } = NotificationPriority.Normal; + public string BizContext { get; set; } = ""; + public List ForceReceivers { get; set; } = new(); // 精确送达(绕过订阅) + public string RoutePath { get; set; } = ""; // 覆盖路由模板 + public List AssigneeIds { get; set; } = new(); // 待办处理人(Category=Todo) + public DateTime? DueTime { get; set; } + public long CreatorId { get; set; } + public string CreatorName { get; set; } = ""; +} + +// ② 简洁命令(业务侧推荐):枚举名即 Code +public record SendNotificationCommand( + NotificationTypeEnum Type, + string BusinessId = "", string Title = "", string Content = "", string BizContext = "", + NotificationPriority Priority = NotificationPriority.Normal, + List ForceReceivers = null, List AssigneeIds = null, DateTime? DueTime = null) : IRequest; +``` + +**`CreateNotificationCommandHandler` 处理流程(6 步,一次请求内同步完成)**: + +``` +① 按 Code 查 NotificationType → 不存在/已停用则抛异常 +② 生成 RoutePath:命令直接指定 > 类型模板替换 {businessId} +③ 建消息(Status 直接 = Sent),ForceReceivers 序列化为 JSON 存库 +④ Category = Todo 且有 AssigneeIds → 为每个处理人建 TodoItem(Pending) +⑤ DeliverAsync(投递):见下 +⑥ 返回消息 ID +``` + +**`DeliverAsync` 投递逻辑(投递的全部实现就在这里)**: + +``` +a. 确定接收人: + ForceReceivers 非空 → 精确送达(绕过订阅) + 否则 → 查该类型 IsActive 的订阅记录,收集 UserId +b. 幂等写接收人:按 MessageId + UserId 判重,只写不存在的 +c. 保存接收人(先落库,再推送 —— 保证离线可拉取) +d. 逐个推送并按结果回写: + pusher.PushNewMessageAsync(userId, dto) 返回 true + → SendStatus = Success、SendTime = now、IsOffline = false + → 追加推送未读统计 PushUnreadCountAsync(userId, unread, todo) + 返回 false(用户离线) + → SendStatus = NotSent、IsOffline = true(不推送) + 抛异常(连接瞬断) + → SendStatus = Failed、IsOffline = true、ErrorMsg = ex.Message +e. 回写 receiver 并保存 +``` + +**注意**:推送给**在线**用户时,未读数要**先保存接收人再统计**,否则统计不含本条。 + +**验证**:调 `POST /api/notification/trigger` 创建一条消息,检查: +- `NotificationMessages` 有记录且 `Status = 1(Sent)` +- `NotificationReceivers` 有对应接收人,`SendStatus`/`IsOffline` 与用户当时是否在线一致 + +--- + +### Step 8|表示层:SignalR Hub + 在线表 + 用户标识 + +**做什么**:新增 `Api/Hubs/NotificationHub.cs`(**3 个类型放同一文件**)。 + +```csharp +/// 在线用户登记表(进程内内存,用于在线/离线判定) +public static class OnlineUserRegistry +{ + private static readonly ConcurrentDictionary _users = new(); + public static void Add(long userId) => _users[userId] = 0; + public static void Remove(long userId) => _users.TryRemove(userId, out _); + public static bool IsOnline(long userId) => _users.ContainsKey(userId); +} + +/// 通知 Hub:连接/断开时维护在线表 +public class NotificationHub : Hub +{ + public override async Task OnConnectedAsync() + { + var userId = GetUserId(); + if (userId > 0) OnlineUserRegistry.Add(userId); + await base.OnConnectedAsync(); + } + public override async Task OnDisconnectedAsync(Exception exception) + { + var userId = GetUserId(); + if (userId > 0) OnlineUserRegistry.Remove(userId); + await base.OnDisconnectedAsync(exception); + } + private long GetUserId() // ★ 与目标系统 JWT 声明保持一致 + => long.TryParse(Context.User?.FindFirst("id")?.Value, out var id) ? id : 0; +} + +/// 用户标识提供器:决定 Clients.User(...) 用什么匹配连接 +public class NotificationUserIdProvider : IUserIdProvider +{ + public string GetUserId(HubConnectionContext connection) // ★ 同上,必须与 GetUserId 同源 + => connection.User?.FindFirst("id")?.Value ?? connection.ConnectionId; +} +``` + +**要点**: +- 用户 ID 的**声明名必须三处一致**:Hub `GetUserId`、`UserIdProvider`、REST 控制器 `GetUserId()`,且与登录签发 token 时的声明名一致。 +- 在线表是**进程内内存**:单实例正确;多实例需 Redis + SignalR 背板(见 4.8)。 + +**验证**:连上 Hub 后,`OnlineUserRegistry.IsOnline(userId)` 为 true;断开为 false。 + +--- + +### Step 9|表示层:推送实现 `NotificationPusher` + +**做什么**:新增 `Api/Services/NotificationPusher.cs`,实现 Step 5 的接口。 + +```csharp +public class NotificationPusher : INotificationPusher +{ + private readonly IHubContext _hubContext; + public NotificationPusher(IHubContext hubContext) => _hubContext = hubContext; + + public bool IsOnline(long userId) => OnlineUserRegistry.IsOnline(userId); + + public async Task PushNewMessageAsync(long userId, NotificationMessageDto message, CancellationToken ct = default) + { + if (!OnlineUserRegistry.IsOnline(userId)) return false; // 离线:不推送 + await _hubContext.Clients.User(userId.ToString()) + .SendAsync("ReceiveNotification", message, ct); // 事件名必须与前端一致 + return true; + } + + public Task PushUnreadCountAsync(long userId, int unreadCount, int todoCount, CancellationToken ct = default) + => _hubContext.Clients.User(userId.ToString()) + .SendAsync("UnreadCountChanged", new { unreadCount, todoCount }, ct); +} +``` + +**关键设计(迁移时必须保留)**:**不能靠 try/catch 判断是否送达** —— SignalR 对"该用户无连接"的 `Clients.User(...)` 调用是**静默成功**的,不判在线会把离线用户误记为推送成功。 + +**验证**:在线用户收到 `ReceiveNotification`;离线用户 `receiver.IsOffline = true`。 + +--- + +### Step 10|表示层:REST 控制器 + +**做什么**:新增 `Api/Controllers/Notice/NotificationControllers.cs`(2 个控制器)。 + +| 控制器 | 路由前缀 | 端点 | +|--------|---------|------| +| `NotificationTypeController` | `/api/notification/type` | `GET` 列表 / `POST` 创建 / `PUT {id}` 更新 / `DELETE {id}` 删除 | +| `NotificationController` | `/api/notification` | `GET subscribe`、`POST subscribe`、`GET messages`、`GET todos`、`POST todos/{id}/complete`、`PUT messages/{id}/read`、`PUT messages/read-all`、`GET unread-count`、`POST trigger`、`POST broadcast` | + +**要点**: +- 全部 `[Authorize]`;用户 ID 从 token 声明取(`User.FindFirst("id")`)。 +- `unread-count` 返回 `{ count }`(前端按此字段读取)。 +- `broadcast` 需校验权限码 `notification:broadcast`。 + +**验证**:Swagger 中可见全部端点;`GET /api/notification/unread-count` 返回 200。 + +--- + +### Step 11|装配:依赖注入与 SignalR 配置 + +**① 应用层注册**(`Application/DependencyInjection.cs`) + +```csharp +services.AddScoped(); +services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(NotificationService).Assembly)); +``` + +**② 启动配置**(`Program.cs`) + +```csharp +// SignalR + 用户标识 + 即时推送器(无后台投递服务) +builder.Services.AddSignalR(); +builder.Services.AddSingleton(); +builder.Services.AddScoped(); + +// JWT:让 Hub 能从 query 取 token(WebSocket 不能带自定义请求头) +.AddJwtBearer(options => { + options.Events = new JwtBearerEvents { + OnMessageReceived = context => { + var accessToken = context.Request.Query["access_token"]; + if (!string.IsNullOrEmpty(accessToken) && + context.HttpContext.Request.Path.StartsWithSegments("/hubs")) + context.Token = accessToken; // 仅对 Hub 路径生效 + return Task.CompletedTask; + } + }; +}); + +// 终点映射 +app.MapHub("/hubs/notification"); +``` + +**③ 基础设施层**:无需新增注册(复用 `AddScoped(typeof(IRepository<>), typeof(Repository<>))`)。 + +**要点**:若目标系统的 JWT 已配置 `OnMessageReceived`,需**追加** `/hubs` 分支(不要覆盖原有逻辑)。 + +**验证**:启动无异常;`POST /hubs/notification/negotiate` 未带 token 返回 401;带合法 token 返回 200。 + +--- + +### Step 12|种子数据(消息类型 + 权限 + 菜单) + +**做什么**:把 `DbSeeder.SeedNotificationData` 片段移植到目标系统的种子类。 + +**① 消息类型(5 个,幂等:按 Code 判存)** + +| Code | 名称 | 分类 | 订阅权限码 | 路由模板 | +|------|------|------|-----------|---------| +| `TeyeFound` | 发现特野 | 通知 | 空(所有人可订阅) | `/prod/xdjy` | +| `TdcFound` | 发现特大糙 | 通知 | 空 | `/prod/hbjy` | +| `LeaveApproval` | 请假审批 | **待办** | `notification:subscribe:leaveapproval` | `/attendance/approve?leaveId={businessId}` | +| `LeaveResult` | 请假结果通知 | 通知 | 空 | `/attendance/my` | +| `Broadcast` | 广播通知 | 公告 | 空 | 空 | + +**② 广播发送权限单元**:`Permission{ ResourceType=操作, Code="notification:broadcast" }`(admin 自动拥有)。 + +**③ 菜单(1 目录 + 3 菜单)**: + +``` +消息中心(directory, path=/notification, icon=Bell) + ├─ 我的消息 path=/notification/messages component=notification/messages/index + ├─ 消息订阅 path=/notification/subscribe component=notification/subscribe/index + └─ 消息类型管理 path=/notification/type component=notification/type/index +``` + +**④ admin 角色分配消息中心菜单**(否则 admin 看不到菜单)。 + +**⑤ 订阅权限同步**:遍历所有 `RequiredPermission` 非空的消息类型,按 `ResourceType=消息订阅 + ResourceId` 幂等创建/更新 `Permission`(`Code = RequiredPermission`),使「角色管理 → 分配权限」可勾选。 + +**前置**:目标系统枚举中需存在 `Resource_Type_Enum.消息订阅`(缺失则需新增枚举值,或改为复用目标系统已有的"操作/资源"类型)。 + +**验证**:`NotificationTypes` 有 5 条;`RoleMenus` 中 admin 关联了 4 条消息中心菜单。 + +--- + +### Step 13|前端:SignalR 客户端 + +**做什么**:新增 `web/src/utils/notification.js`。 + +**核心实现**: + +```javascript +export const unreadCount = ref(0); // 全局未读数(角标与消息页共用) + +export function initNotification(onMessage) { + if (connection) { if (onMessage) messageHandlers.push(onMessage); return connection; } + if (onMessage) messageHandlers.push(onMessage); + + connection = new signalR.HubConnectionBuilder() + .withUrl('/hubs/notification', { accessTokenFactory: () => getAccessToken() || '' }) + .withAutomaticReconnect() + .configureLogging(signalR.LogLevel.Warning) + .build(); + + connection.on('ReceiveNotification', (msg) => { // 事件名与后端一致 + unreadCount.value++; + messageHandlers.forEach(h => { try { h(msg); } catch (e) {} }); + }); + + connection.start().catch(() => {}); + return connection; +} +``` + +导出清单:`unreadCount`、`initNotification(onMessage)`、`loadUnreadCount()`、`openMessage(msg, router)`、`onBusinessChanged(handler)`(可选)、`stopNotification()`。 + +**要点**: +- `accessTokenFactory` 必须提供 token(WebSocket 不能用请求头)。 +- 开发环境 `/hubs` 需在 Vite 代理中配置 **`ws: true`**(否则 WebSocket 握手失败)。 +- `openMessage`:非待办且未读 → `PUT /notification/messages/{id}/read` → 按 `msg.routePath` 跳转。 + +**验证**:浏览器 Network → WS 出现 `/hubs/notification` 且状态 101。 + +--- + +### Step 14|前端:页面 + 路由/菜单注册 + +**做什么**: +1. 新增 3 个页面:`views/notification/{messages,subscribe,type}/index.vue` +2. **在路由的组件映射表中注册这 3 个组件**(当前 F9WebAiTest 的 `componentMap` / 目标系统的动态组件解析表): + +```javascript +'notification/messages/index': () => import('../views/notification/messages/index.vue'), +'notification/subscribe/index': () => import('../views/notification/subscribe/index.vue'), +'notification/type/index': () => import('../views/notification/type/index.vue') +``` + +3. **(可选,推荐)顶部铃铛角标**:在 `layout` 侧边/顶部放一个铃铛 + `unreadCount` 角标,点击进消息中心;登录后调用 `initNotification(...)` 与 `loadUnreadCount()`,登出调用 `stopNotification()`。 + +**⚠️ 重要提醒**:当前 F9WebAiTest 系统中,**后端的菜单种子(Step 12)已写入 `component=notification/.../index`,但前端 `router` 的组件映射表尚未注册这 3 个组件**——因此菜单点开可能报组件未找到。迁移到目标系统时**务必补上第 2 条**(顺带建议回补当前系统)。 + +**验证**:菜单「消息中心」下三个页面可正常打开;收到消息时角标 +1、消息页可见新消息。 + +--- + +## 3. 迁移后验收清单 + +| # | 验收项 | 期望结果 | +|---|--------|---------| +| 1 | 5 张表 + 唯一索引创建 | 表与索引均存在 | +| 2 | 种子:5 个消息类型 | `NotificationTypes` 5 条 | +| 3 | 种子:菜单与 admin 授权 | admin 可见「消息中心」4 个菜单 | +| 4 | 接口:类型 CRUD / 订阅 / 消息 / 待办 / 未读 | 全部 200,数据正确 | +| 5 | Hub 连接与鉴权 | 未带 token → 401;带 token → 101 | +| 6 | 在线表 | 连接后 `IsOnline = true`;断开 = false | +| 7 | 在线实时推送 | 另一端发消息 → 1 秒内收到 `ReceiveNotification` | +| 8 | 离线标记 | 离线用户接收人记录 `IsOffline = true`、`SendStatus = 0/NotSent` | +| 9 | 上线拉取兜底 | 离线期间的消息在消息中心可见、未读数正确 | +| 10 | 已读 | 标记已读后未读数减少 | +| 11 | 待办闭环 | 待办可查询、可完成;完成后消息转已读 | +| 12 | 订阅权限联动 | 受限类型:无权限用户订阅被拒(403);角色分配权限后可订阅 | +| 13 | 广播 | 有 `notification:broadcast` 权限者发送成功,订阅者收到 | +| 14 | 前端页面与角标 | 三页面可打开,角标实时更新 | + +--- + +## 4. 已知坑与适配点(迁移时逐条核对) + +| # | 坑 | 处理 | +|---|----|------| +| 4.1 | **JWT 用户 ID 声明名不同** | 改 3 处:`NotificationHub.GetUserId`、`NotificationUserIdProvider.GetUserId`、控制器 `GetUserId()`;必须与登录签发 token 的声明一致(当前为 `"id"`) | +| 4.2 | **枚举字段未映射为 int** | 在 FreeSql 配置中加 `Aop.ConfigEntityProperty`:枚举 → `typeof(int)`;否则枚举存字符串,与旧库/其它表不一致 | +| 4.3 | **软删除全局过滤器缺失** | 需为实体基类注册 `GlobalFilter.Apply("软删除", a => a.IsDeleted == false)`,否则查询结果与唯一索引行为异常 | +| 4.4 | **唯一索引未创建** | `MessageId+UserId`(接收人幂等)、`UserId+NotificationTypeId`(订阅幂等)必须建,否则重复投递/重复订阅 | +| 4.5 | **WebSocket token 传递** | `OnMessageReceived` 必须有 `/hubs` 分支读 `access_token`;否则 Hub 连不上(普通接口正常,容易误判) | +| 4.6 | **Vite 代理未开 ws** | `'/hubs': { target, changeOrigin: true, ws: true }` | +| 4.7 | **`Clients.User` 命中不到** | 前提是 `IUserIdProvider` 与登录 token 声明一致;取不到时会退化为 `ConnectionId`,导致定向推送全部失效 | +| 4.8 | **多实例部署** | 在线表是进程内内存,多实例下在线判定不准 → 需 Redis + SignalR 背板(`AddStackExchangeRedis`),并把在线判定交给背板 | +| 4.9 | **同步推送影响请求耗时** | 接收人很多(如全员广播)时,推送在同一请求内执行;量大时改为后台队列/批量推送(投递入口集中在 `CreateNotificationCommandHandler`,改造点单一) | +| 4.10 | **种子插入 NOT NULL 字段** | `BaseEntity.CreatedAt` 等审计字段若非空,插入种子时需赋值(当前系统曾因 `created_at` NOT NULL 导致种子失败) | +| 4.11 | **前端组件未注册** | 菜单的 `component` 路径必须在前端组件映射表中注册,否则页面打不开(见 Step 14) | +| 4.12 | **路由跳转目标不存在** | 消息类型 `RouteTemplate` 指向的业务页面需在目标系统存在(如 `/prod/xdjy`、`/attendance/approve`),否则点击消息跳转 404 | +| 4.13 | **CORS** | 前端与后端不同源时需允许跨域;SignalR 若启用 `AllowCredentials` 则不能 `AllowAnyOrigin`(二者互斥) | +| 4.14 | **命名空间/类型差异** | 按 0.2 表格全局替换后再编译,避免逐个报错 | + +--- + +## 5. 迁移顺序速查(TL;DR) + +``` +后端 + 1. 枚举 NotificationEnums.cs + 2. 5 个实体 Notice/*.cs(含唯一索引) + 3. 建表 SyncStructure / 补列 + 4. 9 个 DTO NotificationDtos.cs + 5. 推送接口 INotificationPusher.cs + 6. 消息服务 NotificationService.cs(含 RBAC 权限联动) + 7. 创建命令+投递 NotificationMediatR.cs ★核心 + 8. Hub+在线表+标识 NotificationHub.cs + 9. 推送实现 NotificationPusher.cs +10. 控制器 NotificationControllers.cs +11. DI 与 SignalR DI + Program.cs(含 access_token query) +12. 种子 消息类型 + 广播权限 + 菜单 + admin 授权 + 订阅权限同步 +前端 +13. SignalR 客户端 utils/notification.js +14. 3 个页面 + 组件映射注册 +(可选)顶部角标 +``` + +> 每完成一步都编译一次;Step 7 完成后即可用 `POST /api/notification/trigger` 做端到端验证,再往下做前端。 + +--- + +**文档结束**(迁移指南 v1.0,对应消息推送系统 V2.0 实现) diff --git a/消息推送系统与RBAC审批设计说明书.docx b/消息推送系统与RBAC审批设计说明书.docx new file mode 100644 index 0000000000000000000000000000000000000000..2f67db6e24b2b04e36c3b059b5761a3bbd148add GIT binary patch literal 36389 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 literal 0 HcmV?d00001 diff --git a/消息推送系统软件设计说明书.md b/消息推送系统软件设计说明书.md new file mode 100644 index 0000000..2a1f98b --- /dev/null +++ b/消息推送系统软件设计说明书.md @@ -0,0 +1,817 @@ +# 消息推送系统 软件设计说明书 + +| 项目名称 | 生活圈管理系统(F9WebAiTest)- 消息推送子系统 | +|---------|------------------------------------------| +| 文档类型 | 软件设计说明书(SDD) | +| 版本号 | **V2.0** | +| 编写日期 | 2026-09-15 | +| 后端技术栈 | .NET 8 / FreeSql(CodeFirst)/ MediatR / SignalR / JWT / SQLite | +| 前端技术栈 | Vue 3 / Vite / Element Plus / Pinia / @microsoft/signalr | + +--- + +## 修订记录 + +| 版本 | 日期 | 变更说明 | +|------|------|---------| +| V1.1 | 2026-09-09 | 初版:后台投递服务(BackgroundService 每 3 秒轮询扫描 Ready 消息)异步投递 | +| **V2.0** | **2026-09-15** | **取消后台投递服务与轮询,改为"创建消息即同步投递 + 立即推送"**;投递逻辑由 Api 层后台服务迁入 Application 层命令处理器;新增 `INotificationPusher` 推送抽象;明确在线/离线判定机制;同步修订可靠性设计与接口说明 | + +### V2.0 架构变更说明(重点) + +| | V1.1(旧) | V2.0(现) | +|---|---|---| +| 投递触发 | 后台服务每 3 秒轮询 `Status = Ready` 的消息 | 业务创建消息的**同一次请求内**完成投递 | +| 投递位置 | `F9WebAiTest.Api/Services/NotificationDeliveryService.cs`(BackgroundService) | `F9WebAiTest.Application/Services/Notice/NotificationMediatR.cs`(命令处理器) | +| 消息状态 | 创建 `Ready` → 投递后 `Sent` | 创建即 `Sent`(无中间态) | +| 推送方式 | 后台服务持有 `IHubContext` 直接推送 | 经 `INotificationPusher` 抽象推送(接口在应用层、实现在 Api 层) | +| 延迟 | 0 ~ 3 秒(取决于轮询周期) | **无延迟**(创建即推送) | +| 失败重试 | `RetryCount++`,超 `MaxRetryCount` 标记 `Failed` | **无自动重试**(见 7.3 取舍说明) | + +**为什么取消后台服务**:投递本身是一次内存 + 一次数据库写入的轻量操作,用后台轮询换取"解耦"的收益远小于代价——引入了 0~3 秒延迟、常驻线程与定时器、Ready 状态的中间态管理、以及 SQLite 上的高频无谓查询。改为同步投递后,链路更短、行为更可预期、代码量显著减少。 + +--- + +## 1. 引言 + +### 1.1 编写目的 + +本文档描述消息推送子系统的总体架构、数据模型、**各模块的具体实现方式、所用技术与关键流程**,特别是**用户在线/离线如何判定**、离线用户如何兜底,作为开发、测试、维护与后续扩展的依据。 + +### 1.2 项目背景 + +业务系统(请假审批、考勤、质量检验等)在运行过程中需要向相关人员推送提醒、通知与待办。为统一承载这些消息,构建一套通用、可扩展、多端协同的消息推送子系统。 + +### 1.3 术语定义 + +| 术语 | 英文/类型 | 说明 | +|------|----------|------| +| 消息类型 | NotificationType | 消息的元数据模板:编码、名称、分类、订阅权限码、跳转路由模板 | +| 消息 | NotificationMessage | 一条具体的消息实例,由业务触发创建 | +| 接收人 | NotificationReceiver | 消息 × 用户 的关联记录,含该用户的推送状态与阅读状态 | +| 订阅 | UserNotificationSubscription | 用户对某个消息类型的订阅关系 | +| 待办 | TodoItem | 待办类消息附带的事项,含处理人、状态、处理结果 | +| 强制接收人 | ForceReceivers | 绕过订阅、精准指定送达的用户 ID 集合(JSON 数组) | +| 在线 | Online | 该用户在 SignalR Hub 上存在**活动连接**(见 4.6) | +| 离线 | Offline | 该用户在 Hub 上无活动连接,推送无接收者 | +| 拉取兜底 | Pull Fallback | 离线用户上线后通过 REST 接口读取历史消息,保证消息不丢 | + +--- + +## 2. 系统概述 + +### 2.1 设计目标 + +1. **统一触达**:业务侧一行命令即可发送消息,不关心投递细节。 +2. **双送达机制**:订阅式广播(按消息类型订阅关系)+ 强制接收(精准必达)。 +3. **实时推送**:在线用户 SignalR **即时**推送,离线用户持久化后拉取兜底。 +4. **三类消息**:通知(Notification)/ 待办(Todo)/ 公告(Announcement)。 +5. **待办闭环**:待办类消息自动生成 `TodoItem`,支持查询与完成。 +6. **多端跳转与同步**:消息携带业务标识与路由,多端一致。 +7. **架构简洁**:无后台任务、无轮询、无中间队列。 + +### 2.2 功能范围 + +| 模块 | 功能点 | +|------|--------| +| 消息类型管理 | 类型增删改查、分类、订阅权限码、跳转路由模板 | +| 消息订阅 | 可订阅列表(含权限判定)、订阅/退订 | +| 消息发送 | 业务侧经 MediatR 命令触发(枚举式 / 底层式两种命令) | +| 消息投递 | 创建即投递:确定接收人 → 写接收人表 → 立即推送 | +| 消息读取 | 我的消息(分页 + 分类/已读筛选)、标记已读、全部已读、未读数 | +| 待办管理 | 我的待办、完成待办 | +| 实时推送 | 新消息推送、未读统计推送、业务变更事件推送 | +| 广播通知 | 拥有权限的用户发送广播(订阅广播) | + +### 2.3 运行环境与技术栈 + +| 项目 | 选型 | 用途 | +|------|------|------| +| 后端框架 | .NET 8(ASP.NET Core) | Web API 宿主 | +| ORM | **FreeSql**(`UseConnectionString(DataType.Sqlite, ...)`) | 实体映射、CodeFirst 建表、仓储实现 | +| 数据结构 | SQLite(`f9_web_ai_test_rbac.db`) | 单文件数据库,便于开发与部署 | +| 应用层模式 | **MediatR** | 命令/查询分发(CQRS 风格) | +| 实时通信 | **SignalR**(WebSocket,自动降级 SSE / Long Polling) | 服务端 → 客户端实时推送 | +| 鉴权 | JWT(`System.IdentityModel.Tokens.Jwt`) | REST 与 Hub 统一鉴权 | +| API 文档 | NSwag(OpenAPI + Swagger UI) | 接口文档与客户端生成 | +| 在线登记 | `System.Collections.Concurrent.ConcurrentDictionary` | 进程内在线用户表 | +| 前端 | Vue 3 + Vite + Element Plus + Pinia | 管理端界面 | +| 前端实时 | `@microsoft/signalr` | SignalR 客户端 | + +### 2.4 架构总览 + +``` +┌────────────────────────────────────────────────────────────────────────┐ +│ 业务子系统(Application) │ +│ 请假审批 AttendanceService 考勤 质量检验 … │ +│ │ │ +│ │ await mediator.Send(new SendNotificationCommand(...)) │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────────────────┐ │ +│ │ CreateNotificationCommandHandler ← 投递入口(同步执行) │ │ +│ │ ① 校验消息类型 ② 建消息(Sent) ③ 生成待办(Todo) │ │ +│ │ ④ 确定接收人 ⑤ 写接收人表 ⑥ 立即推送 │ │ +│ └───────────────────────────┬──────────────────────────────────┘ │ +│ │ INotificationPusher(应用层接口) │ +└──────────────────────────────┼─────────────────────────────────────────┘ + │ 实现(Api 层) + ▼ + ┌──────────────────────────────────┐ + │ NotificationPusher │ + │ · IsOnline() ← 在线表判定 │ + │ · PushNewMessageAsync() │ + │ · PushUnreadCountAsync() │ + └──────────────┬───────────────────┘ + │ IHubContext + ▼ + ┌───────────────────────────────────────────────────┐ + │ SignalR Hub /hubs/notification │ + │ OnConnectedAsync → OnlineUserRegistry.Add │ + │ OnDisconnectedAsync → OnlineUserRegistry.Remove │ + │ UserIdentifier → JWT "id" 声明 │ + └───────────────┬───────────────────────────────────┘ + │ 事件:ReceiveNotification / UnreadCountChanged + ▼ + ┌───────────────────────────────────────────────────┐ + │ 前端 notification.js(SignalR 客户端,自动重连) │ + │ unreadCount+1 → 弹窗 / 角标 / 页面刷新 │ + └───────────────────────────────────────────────────┘ +``` + +--- + +## 3. 数据模型 + +### 3.1 枚举定义 + +文件:`F9WebAiTest.Domain/Entities/Notice/NotificationEnums.cs` + +```csharp +public enum NotificationPriority { Normal = 0, Important = 1, Urgent = 2 } // 优先级 +public enum NotificationStatus { Ready = 0, Sent = 1, Failed = 2, PartialSuccess = 3, Revoked = 4 } +public enum NotificationReadStatus { Unread = 0, Read = 1 } // 阅读状态 +public enum NotificationSendStatus { NotSent = 0, Success = 1, Failed = 2 } // 推送状态 +public enum TodoItemStatus { Pending = 0, Processing = 1, Completed = 2, Cancelled = 3 } +public enum NotificationCategory { Notification = 0, Todo = 1, Announcement = 2 } +public enum NotificationTypeEnum { TeyeFound = 1, TdcFound = 2, LeaveApproval = 3, LeaveResult = 4 } +``` + +> `NotificationStatus`:V2.0 下消息创建即为 `Sent`,`Ready`/`Failed`/`Revoked` 保留以兼容历史数据与后续扩展。 + +### 3.2 实体一览(5 张表) + +目录:`F9WebAiTest.Domain/Entities/Notice/`(每个实体独立文件) + +| 实体 | 表名 | 关键字段 | 用途 | +|------|------|---------|------| +| `NotificationType` | `NotificationTypes` | `Code`(唯一)、`Name`、`Category`、`RequiredPermission`、`RouteTemplate`、`IsActive`、`Sort` | 消息类型模板 | +| `NotificationMessage` | `NotificationMessages` | `NotificationTypeId`、`MessageTypeCode`、`Title`、`Content`、`Priority`、`Status`、`ForceReceivers`、`Category`、`RoutePath` | 消息主表 | +| `NotificationReceiver` | `NotificationReceivers` | `MessageId`、`UserId`、`ReadStatus`、`ReadTime`、`IsOffline`、`SendStatus`、`SendTime`、`ErrorMsg` | 每人一条,独立推送/阅读状态 | +| `TodoItem` | `TodoItems` | `MessageId`、`AssigneeId`、`TodoStatus`、`DueTime`、`CompletedTime`、`HandleResult` | 待办事项 | +| `UserNotificationSubscription` | `UserNotificationSubscriptions` | `UserId`、`NotificationTypeId`、`IsActive`、`SubscribedTime`、`CancelTime` | 订阅关系 | + +### 3.3 关系 + +``` +NotificationType 1 ── N NotificationMessage 1 ── N NotificationReceiver N ── 1 User + │ │ + │ 1 │ 1 + └── N UserNotificationSubscription └── N TodoItem (AssigneeId → User) +``` + +### 3.4 关键约束(幂等保障) + +| 约束 | 位置 | 作用 | +|------|------|------| +| `Code` 唯一 | `NotificationTypes` | 类型编码唯一,业务按 Code 发送 | +| `MessageId + UserId` 唯一 | `NotificationReceivers` | **接收人幂等**,重复投递不会产生重复记录 | +| `UserId + NotificationTypeId` 唯一 | `UserNotificationSubscriptions` | 订阅幂等 | +| 软删除 | 全部实体(`BaseEntity`) | 逻辑删除,审计可追溯 | + +建表方式:FreeSql CodeFirst(`Publish` + 种子由 `Infrastructure/Data/DbSeeder.cs` 完成)。 + +### 3.5 实体 ↔ DTO ↔ 接口 对照总表 + +> 说明:本项目 DTO 以 `class` 定义在 `F9WebAiTest.Application/Dtos/Notice/NotificationDtos.cs`(集中文件); +> 实体为**纯属性 POCO**(无领域方法,状态变更由应用服务直接赋值后 `UpdateAsync` 持久化)。 + +| 实体(表) | 出参 DTO(读) | 入参 DTO / 命令(写) | 数据访问接口 | 业务处理接口 | +|---|---|---|---|---| +| `NotificationType`(`NotificationTypes`) | `NotificationTypeDto` | `NotificationTypeCreateDto` | `IRepository` | `INotificationService.CreateTypeAsync / UpdateTypeAsync / DeleteTypeAsync / GetTypeListAsync / GetSubscribeListAsync`(内部 `SyncSubscribePermissionAsync` 同步 RBAC 权限单元) | +| `NotificationMessage`(`NotificationMessages`) | `NotificationMessageDto`(列表 + **SignalR 推送载荷**) | `CreateNotificationCommand`、`SendNotificationCommand`、`BroadcastNotificationDto`、`TriggerDto` | `IRepository` | `CreateNotificationCommandHandler`(创建 + 即时投递)、`INotificationService.GetMyMessagesAsync` | +| `NotificationReceiver`(`NotificationReceivers`) | `NotificationMessageDto.ReadStatus / ReadTime`、`UnreadCountDto` | 无(由投递过程内部生成,不接受外部入参) | `IRepository` | `CreateNotificationCommandHandler.DeliverAsync`(写入 + 回写推送状态)、`INotificationService.MarkReadAsync / MarkAllReadAsync / GetUnreadCountAsync` | +| `TodoItem`(`TodoItems`) | `TodoItemDto` | `CompleteTodoDto` | `IRepository` | `CreateNotificationCommandHandler`(按 `AssigneeIds` 生成待办)、`INotificationService.GetMyTodosAsync / CompleteTodoAsync` | +| `UserNotificationSubscription`(`UserNotificationSubscriptions`) | `NotificationTypeDto.Subscribed`(并入类型列表) | `SubscribeDto` | `IRepository` | `INotificationService.SubscribeAsync`(含 `RequiredPermission` 权限校验) | + +### 3.6 DTO 明细(字段级) + +文件:`F9WebAiTest.Application/Dtos/Notice/NotificationDtos.cs`(`TriggerDto` 定义在控制器文件内) + +**① 消息类型** + +| DTO | 字段 | 类型 | 说明 | +|-----|------|------|------| +| `NotificationTypeDto`(出参) | Id / Code / Name / Description | long / string ×3 | 基础信息 | +| | RequiredPermission | string | 订阅所需权限码,空 = 所有人可订阅 | +| | IsActive / CreatedBy / Sort | bool / long / int | 开放订阅 / 创建人 / 排序 | +| | Category / Icon / RouteTemplate | int / string / string | 分类(0通知1待办2公告) / 图标 / 路由模板 | +| | **Subscribed** | bool | **当前用户是否已订阅**(`GetSubscribeListAsync` 填充) | +| | **CanSubscribe** | bool | **当前用户是否有权订阅**(权限码比对结果) | +| `NotificationTypeCreateDto`(入参) | Code / Name / Description / RequiredPermission / IsActive / Sort / Category / Icon / RouteTemplate | 同上(无 Id/CreatedBy/Subscribed/CanSubscribe) | 创建与更新共用 | + +**② 订阅** + +| DTO | 字段 | 类型 | 说明 | +|-----|------|------|------| +| `SubscribeDto` | NotificationTypeId | long | 目标消息类型 | +| | Subscribe | bool | true=订阅,false=退订 | + +**③ 消息** + +| DTO | 字段 | 类型 | 说明 | +|-----|------|------|------| +| `NotificationMessageDto` | Id / NotificationTypeId | long | 消息ID / 类型ID | +| | MessageTypeCode / TypeName | string | 类型编码(冗余,免联表)/ 类型名称 | +| | Title / Content | string | 标题 / 内容 | +| | Priority | int | 0普通 1重要 2紧急 | +| | BusinessType / BusinessId | string | 业务类型 / 业务单据ID(多端跳转依据) | +| | Category | int | 0通知 1待办 2公告 | +| | RoutePath | string | 最终跳转路由(模板 + 业务ID 生成) | +| | CreatedTime | DateTime | 创建时间 | +| | **ReadStatus** / ReadTime | int / DateTime? | **当前用户**阅读状态(来自接收人表) | +| `UnreadCountDto` | Count | int | 未读消息数 | + +**④ 待办** + +| DTO | 字段 | 类型 | 说明 | +|-----|------|------|------| +| `TodoItemDto` | Id / MessageId | long | 待办ID / 关联消息ID | +| | Title / Content / Priority | string / string / int | 取自关联消息 | +| | BusinessType / BusinessId / RoutePath | string | 取自关联消息(供"去处理"跳转) | +| | TodoStatus | int | 0待处理 1处理中 2已完成 3已取消 | +| | DueTime / CompletedTime / CreatedTime | DateTime? / DateTime? / DateTime | 截止 / 完成 / 创建时间 | +| `CompleteTodoDto` | HandleResult | string | 处理结果说明 | + +**⑤ 广播与测试** + +| DTO | 字段 | 说明 | +|-----|------|------| +| `BroadcastNotificationDto` | Title、Content、Priority(`NotificationPriority?`)、RoutePath | 广播入参(需 `notification:broadcast` 权限) | +| `TriggerDto` | Code(默认 `TeyeFound`)、Title、Content、BusinessType、BusinessId、Priority、RoutePath、AssigneeIds | 测试/联调触发(`POST /api/notification/trigger`) | + +**通用返回包装** + +| 类型 | 说明 | +|------|------| +| `ApiResult` | `{ success, message, code, data }`,`ApiResult.Ok/Fail` | +| `PageResult` | `{ items, total, pageIndex, pageSize }`(`NotificationMessageDto` 分页) | + +### 3.7 接口清单与实现绑定 + +**① 领域层 —— 通用仓储(所有实体共用)** + +| 接口 | 方法 | 实现类 | 注册 | +|------|------|--------|------| +| `IRepository`(`Domain/Common/IRepository.cs`) | `GetByIdAsync`、`GetAllAsync`、`FindAsync`、`FirstOrDefaultAsync`、`AnyAsync`、`CountAsync`、`AddAsync`、`AddRangeAsync`、`UpdateAsync`、`DeleteAsync`、`DeleteRangeAsync`、`HardDeleteAsync`、`HardDeleteRangeAsync`、`SaveChangesAsync` | `Repository`(`Infrastructure/Repositories`) | `AddScoped(typeof(IRepository<>), typeof(Repository<>))` | + +> 5 个通知实体**不各自定义仓储接口**,统一使用泛型 `IRepository`(FreeSql 实现,内含软删除全局过滤器)。 + +**② 应用层 —— 通知服务** + +| 接口 | 方法(11) | 实现类 | 生命周期 | +|------|-----------|--------|---------| +| `INotificationService`(`Application/Services/Notice/NotificationService.cs`) | `GetSubscribeListAsync`、`SubscribeAsync`、`GetMyMessagesAsync`、`MarkReadAsync`、`MarkAllReadAsync`、`GetUnreadCountAsync`、`GetMyTodosAsync`、`CompleteTodoAsync`、`GetTypeListAsync`、`CreateTypeAsync`、`UpdateTypeAsync`、`DeleteTypeAsync` | `NotificationService` | `AddScoped()` | + +依赖(构造函数注入):`IRepository`、`IRepository`、`IRepository`、`IRepository`、`IRepository`、`IRepository`、`IRepository`、`ISnowflakeIdGenerator`、`IAuthService`。 + +**③ 应用层 —— 推送抽象** + +| 接口 | 方法(3) | 实现类(所在层) | 生命周期 | +|------|----------|----------------|---------| +| `INotificationPusher`(`Application/Services/Notice/INotificationPusher.cs`) | `bool IsOnline(long userId)`、`Task PushNewMessageAsync(long userId, NotificationMessageDto, CancellationToken)`、`Task PushUnreadCountAsync(long userId, int, int, CancellationToken)` | `NotificationPusher`(**Api 层**,`Api/Services/NotificationPusher.cs`) | `Program.cs`:`AddScoped()` | + +**④ 应用层 —— MediatR 命令(消息创建的接口实现)** + +| 类型 | 实现的接口 | 处理器 | 返回 | +|------|-----------|--------|------| +| `CreateNotificationCommand` | `IRequest` | `CreateNotificationCommandHandler : IRequestHandler` | 消息 ID | +| `SendNotificationCommand`(record) | `IRequest` | `SendNotificationCommandHandler : IRequestHandler` | 是否成功(内部转调 Create) | + +注册:`services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(NotificationService).Assembly))`。 + +**⑤ 表示层 —— SignalR 与控制器** + +| 类型 | 实现的接口/基类 | 职责 | 注册 | +|------|---------------|------|------| +| `NotificationHub` | `Hub`(`Microsoft.AspNetCore.SignalR`) | 连接生命周期 → 在线表登记与移除 | `AddSignalR()` + `MapHub("/hubs/notification")` | +| `NotificationUserIdProvider` | `IUserIdProvider` | 用户标识 = JWT `"id"` 声明(决定 `Clients.User` 命中规则) | `AddSingleton()` | +| `NotificationTypeController` | `ControllerBase`(`[Authorize]`) | 类型管理 REST | `AddControllers()` | +| `NotificationController` | `ControllerBase`(`[Authorize]`) | 订阅/消息/待办/未读/触发/广播 REST | 依赖 `INotificationService`、`IMediator`、`IAuthService` | + +**⑥ 业务变更事件(多端同步)** + +| 类型 | 实现的接口 | 说明 | +|------|-----------|------| +| `BusinessChangedEvent` | `INotification`(MediatR 通知) | 业务状态变化事件 | +| `BusinessChangedEventHandler` | `INotificationHandler` | 经 SignalR 推送 `BusinessChanged` 事件;注册 `AddScoped, BusinessChangedEventHandler>()` | + +**⑦ 依赖的框架级接口** + +| 接口 | 用途 | 实现/注册 | +|------|------|----------| +| `ISnowflakeIdGenerator` | 雪花 ID(消息/接收人/待办/权限单元主键) | `SnowflakeIdGenerator`,`AddSingleton` | +| `IAuthService` | `GetUserPermissionsAsync(userId)` → 订阅权限校验 | `AuthService`,`AddScoped` | +| `IJwtService` | 签发 access/refresh token(含 `"id"` 声明) | `JwtService`,`AddScoped` | + +--- + +## 4. 模块设计(各模块如何实现) + +### 4.1 领域层:实体与枚举 + +- 位置:`F9WebAiTest.Domain/Entities/Notice/` +- 实现要点:每个实体独立文件、继承 `BaseEntity`(含 `Id`、审计字段与软删除);实体为**纯数据 POCO**(属性 + 默认值),状态变更由应用服务赋值(如 `receiver.ReadStatus = Read`)后经仓储 `UpdateAsync` 持久化。 +- 字段映射:`[Table(Name = "...")]` + `[Index(...)]` 特性声明表名与索引;枚举统一映射为 `int`(`Infrastructure/DependencyInjection.cs` 的 `Aop.ConfigEntityProperty`)。 +- 技术:纯 POCO,无框架依赖(仅 `FreeSql.DataAnnotations` 特性,领域层不引用基础设施)。 + +### 4.2 应用层:消息创建命令 —— 投递核心 ★ + +- 位置:`F9WebAiTest.Application/Services/Notice/NotificationMediatR.cs` +- 类型: + - `SendNotificationCommand`(**简洁命令**,基于 `NotificationTypeEnum`,业务侧推荐) + - `CreateNotificationCommand`(**底层命令**,按字符串 Code,灵活控制) + - `SendNotificationCommandHandler`:枚举名 → Code,转调底层命令(复用同一套投递逻辑) + - `CreateNotificationCommandHandler`:**投递实现** + +**处理流程(一次请求内同步完成)**: + +``` +① 校验消息类型:按 Code 查 NotificationType,不存在/已关闭订阅 → 抛 ServiceException +② 生成跳转路由:命令指定的 RoutePath 优先,否则用类型模板替换 {businessId} +③ 创建消息:状态直接置 NotificationStatus.Sent(无后台队列、无 Ready 中间态) + ForceReceivers 序列化为 JSON 存入消息(空 = 走订阅广播) +④ 待办类消息(Category = Todo 且有 AssigneeIds):为每个处理人生成 TodoItem(Pending) +⑤ 投递 DeliverAsync: + a. 确定接收人: + ForceReceivers 非空 → 精确送达(绕过订阅) + 否则 → 查该类型 IsActive 的订阅记录,取 UserId 集合 + b. 幂等写接收人:按 MessageId + UserId 判重,仅写入新增的 + c. 逐个推送(见 4.3/4.4),并按结果回写推送状态 +⑥ 返回消息 ID +``` + +**接收人回写状态(V2.0 明确区分三种结果)**: + +| 推送结果 | `SendStatus` | `IsOffline` | 说明 | +|---------|-------------|------------|------| +| 在线且推送成功 | `Success` + `SendTime` | `false` | 同时推送未读统计 | +| 用户离线 | `NotSent` | `true` | 不推送,等待上线拉取 | +| 推送抛异常(连接刚断开) | `Failed` + `ErrorMsg` | `true` | 消息已持久化,仍可拉取 | + +**分层实现要点**:命令处理器位于 **Application 层**,此处不能引用 Api 层的 `IHubContext`(SignalR 属于表示层),因此推送能力被抽象为 `INotificationPusher`(见 4.3)。 + +### 4.3 应用层:推送抽象 `INotificationPusher` + +- 位置:`F9WebAiTest.Application/Services/Notice/INotificationPusher.cs` + +```csharp +public interface INotificationPusher +{ + bool IsOnline(long userId); // 在线判定 + Task PushNewMessageAsync(long userId, NotificationMessageDto message, CancellationToken ct = default); + Task PushUnreadCountAsync(long userId, int unreadCount, int todoCount, CancellationToken ct = default); +} +``` + +**为什么接口在应用层、实现在 Api 层**:SignalR 的 `IHubContext` 由 ASP.NET Core 提供,属于表示层;依赖方向必须是 `Api → Application`,若 Application 直接依赖 `IHubContext` 会形成循环依赖。抽象后,依赖倒置,Application 只依赖自己定义的接口,DI 容器在启动时把 Api 层实现注入进去。 + +### 4.4 表示层:推送实现 `NotificationPusher` + +- 位置:`F9WebAiTest.Api/Services/NotificationPusher.cs` +- 注册:`Program.cs` → `builder.Services.AddScoped()` + +```csharp +public bool IsOnline(long userId) => OnlineUserRegistry.IsOnline(userId); + +public async Task PushNewMessageAsync(long userId, NotificationMessageDto message, CancellationToken ct = default) +{ + if (!OnlineUserRegistry.IsOnline(userId)) return false; // 离线:不推送,返回 false + await _hubContext.Clients.User(userId.ToString()) + .SendAsync("ReceiveNotification", message, ct); // 在线:定向推送 + return true; +} + +public Task PushUnreadCountAsync(long userId, int unreadCount, int todoCount, CancellationToken ct = default) + => _hubContext.Clients.User(userId.ToString()) + .SendAsync("UnreadCountChanged", new { unreadCount, todoCount }, ct); +``` + +**技术要点**: +- `Clients.User(userId)` —— SignalR 按 **UserIdentifier** 定向推送,可一次命中该用户的**所有在线连接**(支持同一用户多端/多标签页同时在线)。 +- **必须显式判在线**:SignalR 对"该用户当前无连接"的 `Clients.User(...)` 调用是**静默成功**的(不抛异常、无返回)。若只靠 try/catch,离线用户会被误记为 `SendStatus = Success`。因此推送前用在线表判定,离线直接返回 `false` 交由上层标记 `IsOffline`。 + +### 4.5 表示层:SignalR Hub 与用户标识 + +- 位置:`F9WebAiTest.Api/Hubs/NotificationHub.cs` +- 端点映射:`Program.cs` → `app.MapHub("/hubs/notification")` + +```csharp +public class NotificationHub : Hub +{ + public override async Task OnConnectedAsync() + { + var userId = GetUserId(); + if (userId > 0) OnlineUserRegistry.Add(userId); // 连接建立 → 登记在线 + await base.OnConnectedAsync(); + } + + public override async Task OnDisconnectedAsync(Exception exception) + { + var userId = GetUserId(); + if (userId > 0) OnlineUserRegistry.Remove(userId); // 连接断开 → 移除在线 + await base.OnDisconnectedAsync(exception); + } + + private long GetUserId() // 用户ID来源:JWT 的 "id" 声明 + => long.TryParse(Context.User?.FindFirst("id")?.Value, out var id) ? id : 0; +} + +/// 自定义 UserIdentifier:决定 Clients.User(...) 用哪个标识匹配连接 +public class NotificationUserIdProvider : IUserIdProvider +{ + public string GetUserId(HubConnectionContext connection) + => connection.User?.FindFirst("id")?.Value ?? connection.ConnectionId; +} +``` + +**用户标识如何统一**(这是定向推送能命中的前提): + +| 环节 | 取值方式 | +|------|---------| +| 登录签发 JWT | `JwtService.GenerateAccessToken`:`new Claim("id", user.Id.ToString())` | +| REST 控制器 | `User.FindFirst("id").Value` | +| SignalR 连接标识 | `NotificationUserIdProvider` → JWT `"id"` 声明 | +| 推送目标 | `Clients.User(userId.ToString())` | + +四者一致,因此"REST 里的当前用户"与"SignalR 里的连接"指向同一用户。 + +**WebSocket 携带 JWT**:WebSocket 无法自定义请求头,浏览器 SignalR 客户端自动把 token 拼到查询串 `?access_token=xxx`;后端在 `Program.cs` 的 `JwtBearerEvents.OnMessageReceived` 中针对 `/hubs` 路径从 query 读取: + +```csharp +options.Events = new JwtBearerEvents { + OnMessageReceived = context => { + var accessToken = context.Request.Query["access_token"]; + if (!string.IsNullOrEmpty(accessToken) && context.HttpContext.Request.Path.StartsWithSegments("/hubs")) + context.Token = accessToken; // 仅对 Hub 路径生效,不影响普通接口 + return Task.CompletedTask; + } +}; +``` + +### 4.6 用户在线 / 离线判定机制 ★ + +**判定方式:进程内在线登记表(内存)** + +```csharp +public static class OnlineUserRegistry +{ + private static readonly ConcurrentDictionary _users = new(); + + public static void Add(long userId) => _users[userId] = 0; + public static void Remove(long userId) => _users.TryRemove(userId, out _); + public static bool IsOnline(long userId) => _users.ContainsKey(userId); +} +``` + +| 问题 | 答案 | +|------|------| +| 在哪判定? | `NotificationPusher.IsOnline(userId)` → `OnlineUserRegistry.IsOnline(userId)` | +| 何时登记? | `NotificationHub.OnConnectedAsync`(SignalR 连接建立时) | +| 何时移除? | `NotificationHub.OnDisconnectedAsync`(连接断开、超时、客户端关闭浏览器/窗口时) | +| 为何用 `ConcurrentDictionary`? | Hub 回调在多个连接线程并发执行,必须线程安全;`byte` 作为占位值(仅需键) | +| 为何不查数据库? | 在线是**连接级瞬时状态**,不适合落库;内存表读取为 O(1),无 IO | +| 为何不用 `Clients.User` 是否报错来判断? | SignalR 对无连接用户是**静默成功**,无法据此区分在线/离线(见 4.4) | +| 一个用户多端在线? | `Add` 幂等(同一 userId 只登记一次);`Clients.User` 会推给该用户**全部**连接,任一端断开不影响其它端(断开即 `Remove`,若仍有其它端在线则出现偏差——见下方限制) | +| 多实例部署? | ❌ **不适用**:在线表是进程内内存,A 实例无法知道用户在 B 实例上在线。需引入 Redis + SignalR 背板(见 9.3) | + +**已如实说明的实现限制**:`Remove` 在任一连接断开时直接移除,若用户有两个连接(如两个浏览器窗口),关闭其中一个会导致在线表短暂误判为离线,直到其余连接的心跳/重连重新登记。业务影响可控(该场景下消息仍会落库,用户重新触发任意连接动作或重连即恢复)。若需严格的多连接计数,可将 `_users` 的值由 `byte` 改为连接数 `int`(`Add` 时 `+1`、`Remove` 时 `-1`,归零再移除)——这是后续可选的优化点。 + +### 4.7 未读统计推送(`UnreadCountChanged`) + +- 触发时机:新消息推送成功后(在线用户) +- 载荷:`{ unreadCount, todoCount }` +- 实现:`NotificationPusher.PushUnreadCountAsync`,未读数由 `IRepository.CountAsync(r => r.UserId == userId && r.ReadStatus == Unread)` 统计(此时接收人已落库,数值包含本条) +- 前端:`web/src/utils/notification.js` 目前通过 `ReceiveNotification` 自增 `unreadCount`;若需**服务端权威值**,可直接订阅 `UnreadCountChanged`(事件已就绪) + +### 4.8 消息读取与已读 + +- 位置:`F9WebAiTest.Application/Services/Notice/NotificationService.cs` +- 接口(REST):我的消息分页查询(按 `readStatus` / `category` 筛选)、`MarkReadAsync`、`MarkAllReadAsync`、`GetUnreadCountAsync` +- 实现要点:**查询以接收人表为主表**(每人一条状态),联表消息与类型名;已读时写 `ReadStatus = Read` 与 `ReadTime` + +### 4.9 待办机制 + +- 待办类消息(`Category = Todo`)在创建消息、生成待办后,走同一套"确定接收人 → 推送"逻辑(通常用 `ForceReceivers` 精确送审批人) +- `TodoItem` 状态:`Pending → Processing → Completed / Cancelled` +- 审批流转复用同一条消息与待办记录,避免多级审批产生冗余消息 + +### 4.10 业务变更事件(多端同步) + +- 位置:`F9WebAiTest.Application/Services/BusinessChangedEvent.cs` + `F9WebAiTest.Api/Services/BusinessChangedEventHandler.cs` +- 机制:业务变更时发布 `BusinessChangedEvent`(实现 `INotificationHandler` 的处理器),经 SignalR 推送 `BusinessChanged` 事件 +- 前端:`onBusinessChanged(handler)` 订阅,某端操作后其它端刷新已打开的页面 +- 与消息推送共用同一个 Hub 连接与用户标识 + +### 4.11 前端模块 + +- 位置:`web/src/utils/notification.js`;页面:`web/src/views/notification/{messages,subscribe,type}/` + +```javascript +// 建立连接(幂等):token 由 accessTokenFactory 提供,自动重连 +connection = new signalR.HubConnectionBuilder() + .withUrl('/hubs/notification', { accessTokenFactory: () => getAccessToken() || '' }) + .withAutomaticReconnect() + .build(); + +connection.on('ReceiveNotification', (msg) => { unreadCount.value++; /* 弹窗 + 页面刷新 */ }); +connection.on('BusinessChanged', (e) => { /* 多端同步刷新 */ }); +connection.start().catch(() => {}); +``` + +| 导出 | 作用 | +|------|------| +| `unreadCount` | 全局未读数(Pinia 之外用 `ref` 共享,供角标与消息页使用) | +| `initNotification(onMessage)` | 建立连接并注册回调(幂等,重复调用复用同一连接) | +| `loadUnreadCount()` | 拉取未读数(首次进入、标记已读后校准) | +| `openMessage(msg, router)` | 打开消息:非待办且未读 → 标记已读 → 按 `routePath` 跳转业务页 | +| `onBusinessChanged(handler)` | 订阅业务变更(多端同步) | +| `stopNotification()` | 登出时停止连接并清空状态 | + +**技术要点**: +- `accessTokenFactory`:浏览器 WebSocket 不能带自定义头,SignalR 客户端自动以 `access_token` 查询串传递,后端 `OnMessageReceived` 已适配。 +- `withAutomaticReconnect()`:网络抖动/服务重启后自动重连,重连成功后 Hub 会再次登记在线,恢复推送能力。 +- 消息跳转:`openMessage` 读取消息自带 `routePath` 直达业务页面(待办类不在此处标记已读,保持提醒直到真正处理完成)。 + +--- + +## 5. 关键流程(时序) + +### 5.1 在线用户:创建 → 推送 → 展示 + +``` +业务代码(如请假审批 ApproveLeave) + │ mediator.Send(new SendNotificationCommand(LeaveResult, ...)) + ▼ +CreateNotificationCommandHandler + ├─ 校验类型 / 生成路由 + ├─ 写 NotificationMessage (Status = Sent) + ├─ 待办类:写 TodoItem + ├─ 确定接收人(ForceReceivers 或 订阅广播) + ├─ 写 NotificationReceiver(幂等) + └─ NotificationPusher.PushNewMessageAsync(userId, dto) + ├─ OnlineUserRegistry.IsOnline(userId) == true + ├─ Clients.User(userId).SendAsync("ReceiveNotification", dto) ← 实时 + └─ 回写 receiver:SendStatus=Success, SendTime=now + └─ PushUnreadCountAsync → "UnreadCountChanged" { unreadCount, todoCount } + ▼ +前端 notification.js + ├─ unreadCount++ → 顶部角标更新 + ├─ 弹窗提示(标题 + 内容) + └─ 消息页/待办页订阅回调刷新 + ▼ +用户点击消息 → openMessage() + ├─ PUT /api/notification/messages/{id}/read + └─ router.push(msg.routePath) → 直达业务页面 +``` + +### 5.2 离线用户:落库 + 拉取兜底 + +``` +业务创建消息 → 接收人含离线用户 U + └─ IsOnline(U) == false + ├─ 不推送,receiver.IsOffline = true、SendStatus = NotSent + └─ 消息与接收人记录均已持久化 ✅ +用户 U 之后登录 / 打开页面 + └─ GET /api/notification/unread-count → 未读数正确(含离线期间的消息) + └─ GET /api/notification/messages → 消息列表完整可见 + └─ SignalR 连接建立 → OnlineUserRegistry.Add(U) → 后续消息恢复实时推送 +``` + +> **重要语义**:实时推送只在"消息产生那一刻收件人在线"时触达。离线期间的消息**不会在用户上线时补弹窗**,而是通过未读数与消息列表体现(避免上线瞬间弹出一堆历史消息)。如需"上线补推",可在 Hub 的 `OnConnectedAsync` 中查询未读并推送一次通知——属于可扩展项。 + +### 5.3 待办闭环 + +``` +提交请假 → SendNotificationCommand(LeaveApproval, ForceReceivers:[审批人], AssigneeIds:[审批人]) + → 消息 + TodoItem(Pending) + 推送给审批人 +审批人处理 → POST /api/notification/todos/{id}/complete + → TodoItem → Completed,写 HandleResult + → 有下一级:再发待办给下一审批人 + → 流程结束:SendNotificationCommand(LeaveResult, ForceReceivers:[申请人]) +``` + +--- + +## 6. 接口设计 + +### 6.1 REST 接口 + +**消息类型管理**(`/api/notification/type`,需登录) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/notification/type` | 类型列表 | +| POST | `/api/notification/type` | 创建类型 | +| PUT | `/api/notification/type/{id}` | 更新类型 | +| DELETE | `/api/notification/type/{id}` | 删除类型 | + +**用户端**(`/api/notification`,需登录) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/notification/subscribe` | 可订阅类型列表(含订阅状态与可订阅判定) | +| POST | `/api/notification/subscribe` | 订阅 / 退订(`{ notificationTypeId, subscribe }`) | +| GET | `/api/notification/messages` | 我的消息(`pageIndex`/`pageSize`/`readStatus`/`category`) | +| GET | `/api/notification/todos` | 我的待办(`status` 可选) | +| POST | `/api/notification/todos/{id}/complete` | 完成待办(`{ handleResult }`) | +| PUT | `/api/notification/messages/{id}/read` | 标记已读 | +| PUT | `/api/notification/messages/read-all` | 全部已读 | +| GET | `/api/notification/unread-count` | 未读数量(返回 `{ count }`) | +| POST | `/api/notification/trigger` | 触发测试消息(演示/联调用) | +| POST | `/api/notification/broadcast` | 发送广播(需 `notification:broadcast` 权限) | + +统一响应包装:`ApiResult`(`Ok` / `Fail`),分页为 `PageResult`。 + +### 6.2 SignalR 事件契约 + +端点:`/hubs/notification`;鉴权:JWT(query `access_token`)。 + +| 事件 | 方向 | 载荷 | 说明 | +|------|------|------|------| +| `ReceiveNotification` | 服务端 → 客户端 | `NotificationMessageDto` | 新消息实时推送 | +| `UnreadCountChanged` | 服务端 → 客户端 | `{ unreadCount, todoCount }` | 未读统计变化 | +| `BusinessChanged` | 服务端 → 客户端 | `{ businessType, businessId, action }` | 业务数据变更(多端同步) | + +`NotificationMessageDto` 主要字段:`Id`、`NotificationTypeId`、`MessageTypeCode`、`TypeName`、`Title`、`Content`、`Priority`、`BusinessType`、`BusinessId`、`Category`、`RoutePath`、`CreatedTime`、`ReadStatus`。 + +### 6.3 业务侧发送消息的两种命令 + +```csharp +// ① 简洁命令(推荐):枚举名即消息类型 Code +await mediator.Send(new SendNotificationCommand( + NotificationTypeEnum.LeaveApproval, + BusinessId: leaveId.ToString(), + Title: "请假审批", + Content: "张三提交了请假申请", + Priority: NotificationPriority.Important, + ForceReceivers: new List { approverId }, // 精确送达;留空 = 按订阅广播 + AssigneeIds: new List { approverId })); // 生成待办 + +// ② 底层命令:按字符串 Code,完全控制 +await mediator.Send(new CreateNotificationCommand { + NotificationTypeCode = "TeyeFound", + BusinessType = "Xdjy", + BusinessId = lsjlId.ToString(), + Title = "发现特野", + Content = "…", + ForceReceivers = new List() +}); +``` + +--- + +## 7. 可靠性设计 + +### 7.1 保障措施 + +| 风险 | 措施 | 实现位置 | +|------|------|---------| +| 消息丢失 | **先持久化再推送**:消息与接收人记录先落库,推送失败仍可拉取 | `CreateNotificationCommandHandler` | +| 重复投递 | `MessageId + UserId` 唯一约束 + 投递前判重 | 实体约束 + `DeliverAsync` | +| 推送异常 | 单接收人 try/catch 隔离,仅回写 `Failed`,不影响业务主流程 | `DeliverAsync` | +| 用户离线 | `IsOffline` 标记 + 上线后 REST 拉取兜底 | `NotificationPusher` + 前端 | +| 连接断开 | SignalR `withAutomaticReconnect()` 自动重连,重连后重新登记在线 | 前端 + Hub | +| 订阅越权 | 订阅时校验消息类型的 `RequiredPermission` | `NotificationService.SubscribeAsync` | +| 业务被消息拖累 | 消息发送失败不回滚业务(业务侧可自行选择 try/catch) | 业务调用处 | + +### 7.2 与 V1.1 的可靠性差异(如实说明) + +| 能力 | V1.1 | V2.0 | +|------|------|------| +| 失败自动重试 | ✅ `RetryCount++`,超上限标记 `Failed` | ❌ 无(无后台任务) | +| 过期撤销 | ✅ 扫描时检查 `ExpireTime` → `Revoked` | ❌ 未实现(字段保留) | +| 部分成功统计 | ✅ `PartialSuccess` | ⚠️ 逐接收人状态已记录,消息级汇总未启用 | +| 投递延迟 | 0~3 秒 | **0(同步即时)** | + +### 7.3 V2.0 新增的已知限制 + +1. **无自动重试**:推送依赖 SignalR 连接,瞬时故障只记录 `Failed`,不做补偿。补偿手段:业务侧重发、或后续增加"失败消息人工重发"接口。 +2. **在线表是进程内内存**:多实例部署下在线判定不准(见 4.6 / 9.3)。 +3. **同步推送增加请求耗时**:推送为本地内存 + Socket 写入,开销在毫秒级;接收人很多(如全员广播)时建议改为后台队列/批量推送。 +4. **推送不保证送达**:`Success` 仅代表"已写入该用户的在线连接",不代表客户端已渲染(浏览器后台标签页可能延迟处理)。 + +--- + +## 8. 技术选型说明(技术 → 用途 → 原因) + +| 技术 | 用途 | 选型理由 | +|------|------|---------| +| **SignalR** | 实时推送通道 | 免手写 WebSocket 协议;自动协商传输(WebSocket → SSE → Long Polling)、自动重连、按用户/组定向推送、与 ASP.NET Core 鉴权无缝集成 | +| **JWT** | 统一鉴权 | REST 与 Hub 共用一套 token;用户标识 `"id"` 声明贯穿两端,使 `Clients.User` 可精准命中 | +| **MediatR** | 命令/查询分发 | 业务侧只需 `Send` 一条命令即可发消息;投递逻辑集中在命令处理器,便于统一维护 | +| **FreeSql** | ORM / CodeFirst | 支持多数据库切换(SQLite/MySQL/PG)、CodeFirst 建表、`IRepository` 泛型仓储简化数据访问 | +| **SQLite** | 数据库 | 单文件、零部署,适合当前规模;切换数据库只需改连接串与 `DataType` | +| **ConcurrentDictionary** | 在线用户登记 | 读多写少、O(1) 判定、线程安全,Hub 并发回调下无锁竞争 | +| **抽象接口(依赖倒置)** | 跨层推送能力 | 接口在 Application、实现在 Api,避免应用层反向依赖表示层 | +| **Vue3 + @microsoft/signalr** | 前端实时接入 | 官方 JS 客户端;`accessTokenFactory` 自动处理 WebSocket token;组合式 API 便于共享 `unreadCount` | +| **NSwag** | 接口文档 | 自动生成 OpenAPI + Swagger UI,便于联调与生成 TS 客户端 | + +--- + +## 9. 扩展指南 + +### 9.1 新增一种消息类型 + +1. `NotificationEnums.cs` 的 `NotificationTypeEnum` 增加枚举值(枚举名 = 类型 `Code`) +2. 配置消息类型(管理端「消息类型管理」页面,或 `DbSeeder` 种子):名称、分类、`RouteTemplate`(含 `{businessId}` 占位符)、订阅权限码(可选) +3. 业务侧一行发送: + ```csharp + await mediator.Send(new SendNotificationCommand( + NotificationTypeEnum.YourNewType, + BusinessId: bizId.ToString(), + Title: "…", Content: "…", + ForceReceivers: receivers)); // 留空 = 按订阅广播 + ``` + **无需改动投递与推送代码**(投递链路对类型无感知)。 + +### 9.2 新增推送渠道(短信 / 企业微信 / 邮件) + +实现 `INotificationPusher` 的多渠道组合(装饰器或策略): + +```csharp +public class CompositePusher : INotificationPusher +{ + // 站内(SignalR)为主渠道;离线或紧急消息可扩展短信/企微 +} +``` + +或新增独立接口(如 `ISmsSender`)在 `CreateNotificationCommandHandler` 中并行调用——**投递编排集中在命令处理器**,扩展点清晰。 + +### 9.3 多实例部署改造(重要) + +当前 `OnlineUserRegistry` 为进程内内存表,多实例下在线判定失效。改造步骤: + +1. 引入 **Redis** 与 **SignalR Redis 背板**(`Microsoft.AspNetCore.SignalR.StackExchangeRedis`): + ```csharp + builder.Services.AddSignalR().AddStackExchangeRedis("redis-connection"); + ``` + 背板使 `Clients.User(...)` 可跨实例路由到用户所在实例。 +2. 在线表改用 Redis(`SET notice:online:{userId}` + 过期时间),或直接依赖背板的 `All` 通道做推送,不再自行判定在线(把 `IsOnline` 改为"始终返回 true,交由背板路由")。 +3. 消息/接收人表迁移到 PostgreSQL 或 MySQL(SQLite 不适合多实例并发写)。 + +### 9.4 恢复"后台投递 + 重试"能力 + +若业务量增长需要异步化:保留 `CreateNotificationCommandHandler` 的建消息部分,把 `DeliverAsync` 投递部分改为写入队列(本地 Channel / MQ),由消费者执行投递与重试——**接口 `INotificationPusher` 与 Hub 无需改动**。 + +--- + +## 10. 附录 + +### 10.1 关键文件索引 + +| 功能 | 文件 | +|------|------| +| 枚举定义 | `F9WebAiTest.Domain/Entities/Notice/NotificationEnums.cs` | +| 实体(5 个) | `F9WebAiTest.Domain/Entities/Notice/{NotificationType, NotificationMessage, NotificationReceiver, TodoItem, UserNotificationSubscription}.cs` | +| 消息创建命令 + 投递实现 ★ | `F9WebAiTest.Application/Services/Notice/NotificationMediatR.cs` | +| 推送抽象(接口) | `F9WebAiTest.Application/Services/Notice/INotificationPusher.cs` | +| 推送实现 | `F9WebAiTest.Api/Services/NotificationPusher.cs` | +| Hub + 在线表 + 用户标识 | `F9WebAiTest.Api/Hubs/NotificationHub.cs` | +| 订阅/消息/待办服务 | `F9WebAiTest.Application/Services/Notice/NotificationService.cs` | +| REST 控制器 | `F9WebAiTest.Api/Controllers/Notice/NotificationControllers.cs` | +| 业务变更事件 | `F9WebAiTest.Application/Services/BusinessChangedEvent.cs` + `F9WebAiTest.Api/Services/BusinessChangedEventHandler.cs` | +| 数据种子 | `F9WebAiTest.Infrastructure/Data/DbSeeder.cs` | +| 启动装配(SignalR/Hub/推送器/JWT query) | `F9WebAiTest.Api/Program.cs` | +| 前端实时客户端 | `web/src/utils/notification.js` | +| 前端页面 | `web/src/views/notification/{messages, subscribe, type}/` | +| JWT 声明(`id`) | `F9WebAiTest.Application/Services/Auth/JwtService.cs` | + +### 10.2 端到端排查清单(联调时按此顺序) + +| 现象 | 检查点 | +|------|--------| +| 前端收不到任何推送 | ① 浏览器控制器 Network → WS 是否连上 `/hubs/notification`(101 Switching Protocols)② 连接 URL 是否带 `access_token` ③ 后端是否返回 401(token 无效) | +| 消息创建了但没推送 | ① 消息 `ForceReceivers` 与订阅关系是否命中该用户 ② 该用户当时是否在线(`OnlineUserRegistry`)③ 接收人表是否有该用户记录、`SendStatus`/`IsOffline` 值 | +| 在线却没收到 | ① JWT 的 `id` 声明是否存在(`NotificationUserIdProvider` 取不到会退化为 `ConnectionId`,导致 `Clients.User(用户ID)` 无法命中)② 推送时的 userId 与连接登记的 userId 是否一致 | +| 偶发收不到 | 连接是否处于重连中(`withAutomaticReconnect` 期间 `OnDisconnectedAsync` 会移除在线登记) | + +### 10.3 参考资料 + +- Microsoft ASP.NET Core SignalR 官方文档(Hubs / IUserIdProvider / 背板 / JWT 鉴权) +- MediatR 官方文档 +- FreeSql 官方文档(CodeFirst / 连接串配置) +- IEEE 1016 软件设计描述标准 + +--- + +**文档结束**(V2.0,与 2026-09-15 代码实现一致)