From 2cc72c02e286d5cc05bff6bdc24640814782ce35 Mon Sep 17 00:00:00 2001 From: zhouda1fu <825542578@qq.com> Date: Wed, 21 Jan 2026 00:14:39 +0800 Subject: [PATCH 01/37] =?UTF-8?q?feat=EF=BC=9A=E6=B7=BB=E5=8A=A0Admin?= =?UTF-8?q?=E5=90=8E=E5=8F=B0=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 27 + template/.template.config/template.json | 13 + .../DeliverAggregate/DeliverRecord.cs | 4 +- .../AggregatesModel/DeptAggregate/Dept.cs | 197 + .../AggregatesModel/OrderAggregate/Order.cs | 4 +- .../AggregatesModel/RoleAggregate/Role.cs | 90 + .../RoleAggregate/RolePermission.cs | 33 + .../AggregatesModel/UserAggregate/User.cs | 178 + .../AggregatesModel/UserAggregate/UserDept.cs | 65 + .../UserAggregate/UserRefreshToken.cs | 43 + .../AggregatesModel/UserAggregate/UserRole.cs | 25 + .../DeptEvents/DeptInfoChangedDomainEvent.cs | 10 + .../DomainEvents/OrderDomainEvents.cs | 2 + .../DomainEvents/RoleEvents.cs | 8 + .../src/ABC.Template.Domain/ErrorCodes.cs | 116 + .../ApplicationDbContext.cs | 22 +- .../DeliverRecordConfiguration.cs | 4 +- .../DeptEntityTypeConfiguration.cs | 49 + .../OrderEntityTypeConfiguration.cs | 4 +- .../RoleEntityTypeConfiguration.cs | 50 + .../UserEntityTypeConfiguration.cs | 103 + .../Repositories/DeliverRecordRepository.cs | 4 +- .../Repositories/DeptRepository.cs | 15 + .../Repositories/OrderRepository.cs | 4 +- .../Repositories/RoleRepository.cs | 9 + .../Repositories/UserRepository.cs | 9 + .../AppPermissions/AppPermission.cs | 74 + .../AppPermissions/AppPermissionGroup.cs | 47 + .../AppPermissions/PermissionCodes.cs | 40 + .../PermissionDefinitionContext.cs | 70 + .../Commands/Delivers/DeliverGoodsCommand.cs | 6 +- .../Admin/DeptCommands/CreateDeptCommand.cs | 40 + .../Admin/DeptCommands/DeleteDeptCommand.cs | 37 + .../Admin/DeptCommands/UpdateDeptCommand.cs | 36 + .../Admin/RoleCommands/ActivateRoleCommand.cs | 38 + .../Admin/RoleCommands/CreateRoleCommand.cs | 35 + .../RoleCommands/DeactivateRoleCommand.cs | 38 + .../Admin/RoleCommands/DeleteRoleCommand.cs | 47 + .../RoleCommands/UpdateRoleInfoCommand.cs | 47 + .../Admin/UserCommands/CreateUserCommand.cs | 55 + .../Admin/UserCommands/DeleteUserCommand.cs | 39 + .../UserCommands/PasswordResetCommand.cs | 40 + .../RevokeUserRefreshTokensCommand.cs | 23 + .../Admin/UserCommands/UpdateUserCommand.cs | 55 + .../UserCommands/UpdateUserDeptNameCommand.cs | 41 + .../UpdateUserLoginTimeCommand.cs | 21 + .../UserCommands/UpdateUserRoleNameCommand.cs | 43 + .../UserCommands/UpdateUserRolesCommand.cs | 46 + .../Commands/Orders/CreateOrderCommand.cs | 4 +- .../Commands/Orders/PayOrderCommand.cs | 4 +- ...DomainEventHandlerForUpdateUserDeptName.cs | 30 + ...reatedDomainEventHandlerForDeliverGoods.cs | 4 +- ...DomainEventHandlerForUpdateUserRoleName.cs | 30 + .../OrderPaidIntegrationEventConverter.cs | 4 +- ...dIntegrationEventHandlerForDeliverGoods.cs | 4 +- .../OrderPaidIntegrationEvent.cs | 2 + .../Application/Queries/DeptQuery.cs | 200 + .../Application/Queries/Orders/OrderQuery.cs | 2 + .../Application/Queries/RoleQuery.cs | 91 + .../Application/Queries/UserQuery.cs | 160 + .../Admin/DeptEndpoints/CreateDeptEndpoint.cs | 50 + .../Admin/DeptEndpoints/DeleteDeptEndpoint.cs | 45 + .../Admin/DeptEndpoints/GetDeptEndpoint.cs | 71 + .../DeptEndpoints/GetDeptTreeEndpoint.cs | 37 + .../Admin/DeptEndpoints/UpdateDeptEndpoint.cs | 49 + .../RoleEndpoints/ActivateRoleEndpoint.cs | 38 + .../Admin/RoleEndpoints/CreateRoleEndpoint.cs | 49 + .../RoleEndpoints/DeactivateRoleEndpoint.cs | 38 + .../Admin/RoleEndpoints/DeleteRoleEndpoint.cs | 39 + .../RoleEndpoints/GetAllRolesEndpoint.cs | 30 + .../Admin/RoleEndpoints/GetRoleEndpoint.cs | 48 + .../Admin/RoleEndpoints/UpdateRoleEndpoint.cs | 48 + .../Admin/UserEndpoints/AuthEndpoint.cs | 18 + .../Admin/UserEndpoints/CreateUserEndpoint.cs | 74 + .../Admin/UserEndpoints/DeleteUserEndpoint.cs | 38 + .../UserEndpoints/GetAllUsersEndpoint.cs | 31 + .../GetUserAccessCodesEndpoint.cs | 58 + .../Admin/UserEndpoints/GetUserEndpoint.cs | 43 + .../UserEndpoints/GetUserProfileEndpoint.cs | 75 + .../Admin/UserEndpoints/LoginEndpoint.cs | 96 + .../Admin/UserEndpoints/LogoutEndpoint.cs | 56 + .../UserEndpoints/PasswordResetEndpoint.cs | 47 + .../Admin/UserEndpoints/RegisterEndpoint.cs | 73 + .../Admin/UserEndpoints/UpdateUserEndpoint.cs | 78 + .../UserEndpoints/UpdateUserRolesEndpoint.cs | 49 + .../OrderEndpoints/CreateOrderEndpoint.cs | 4 +- .../OrderEndpoints/GetOrderByIdEndpoint.cs | 2 + .../OrderEndpoints/PayOrderEndpoint.cs | 4 +- .../Extensions/ServiceCollectionExtensions.cs | 33 + template/src/ABC.Template.Web/Program.cs | 48 +- .../Utils/AppConfiguration.cs | 19 + .../ABC.Template.Web/Utils/PasswordHasher.cs | 57 + .../Utils/SeedDatabaseExtension.cs | 144 + .../ABC.Template.Web/Utils/TokenGenerator.cs | 25 + .../appsettings.Development.json | 15 + .../src/ABC.Template.Web/appsettings.json | 82 +- template/src/frontend/.browserslistrc | 4 + template/src/frontend/.changeset/README.md | 5 + template/src/frontend/.changeset/config.json | 18 + template/src/frontend/.commitlintrc.js | 1 + template/src/frontend/.dockerignore | 7 + template/src/frontend/.editorconfig | 18 + template/src/frontend/.gitattributes | 11 + template/src/frontend/.gitconfig | 2 + template/src/frontend/.github/CODEOWNERS | 14 + .../.github/ISSUE_TEMPLATE/bug-report.yml | 74 + .../frontend/.github/ISSUE_TEMPLATE/docs.yml | 38 + .../ISSUE_TEMPLATE/feature-request.yml | 70 + .../.github/actions/setup-node/action.yml | 40 + .../src/frontend/.github/commit-convention.md | 89 + template/src/frontend/.github/config.yml | 39 + template/src/frontend/.github/contributing.md | 40 + template/src/frontend/.github/dependabot.yml | 17 + .../frontend/.github/pull_request_template.md | 33 + .../src/frontend/.github/release-drafter.yml | 61 + template/src/frontend/.github/semantic.yml | 13 + .../src/frontend/.github/workflows/build.yml | 48 + .../.github/workflows/changeset-version.yml | 42 + .../src/frontend/.github/workflows/ci.yml | 125 + .../src/frontend/.github/workflows/codeql.yml | 94 + .../src/frontend/.github/workflows/deploy.yml | 172 + .../src/frontend/.github/workflows/draft.yml | 25 + .../.github/workflows/issue-close-require.yml | 31 + .../.github/workflows/issue-labeled.yml | 46 + .../src/frontend/.github/workflows/lock.yml | 24 + .../.github/workflows/release-tag.yml | 80 + .../src/frontend/.github/workflows/rerun.yml | 19 + .../workflows/semantic-pull-request.yml | 41 + .../src/frontend/.github/workflows/stale.yml | 19 + template/src/frontend/.gitignore | 52 + template/src/frontend/.gitpod.yml | 6 + template/src/frontend/.node-version | 1 + template/src/frontend/.npmrc | 13 + template/src/frontend/.prettierignore | 18 + template/src/frontend/.prettierrc.mjs | 1 + template/src/frontend/.stylelintignore | 4 + template/src/frontend/.vscode/extensions.json | 30 + .../src/frontend/.vscode/global.code-snippets | 37 + template/src/frontend/.vscode/launch.json | 42 + template/src/frontend/.vscode/settings.json | 241 + template/src/frontend/LICENSE | 9 + template/src/frontend/README.ja-JP.md | 157 + template/src/frontend/README.md | 157 + template/src/frontend/README.zh-CN.md | 157 + template/src/frontend/apps/admin-antd/.env | 8 + .../src/frontend/apps/admin-antd/.env.analyze | 7 + .../frontend/apps/admin-antd/.env.development | 16 + .../frontend/apps/admin-antd/.env.production | 19 + .../src/frontend/apps/admin-antd/index.html | 35 + .../src/frontend/apps/admin-antd/package.json | 50 + .../apps/admin-antd/postcss.config.mjs | 1 + .../apps/admin-antd/public/favicon.ico | Bin 0 -> 5430 bytes .../admin-antd/src/adapter/component/index.ts | 600 + .../apps/admin-antd/src/adapter/form.ts | 49 + .../apps/admin-antd/src/adapter/vxe-table.ts | 286 + .../apps/admin-antd/src/api/core/auth.ts | 65 + .../apps/admin-antd/src/api/core/index.ts | 3 + .../apps/admin-antd/src/api/core/menu.ts | 19 + .../apps/admin-antd/src/api/core/user.ts | 15 + .../frontend/apps/admin-antd/src/api/index.ts | 2 + .../apps/admin-antd/src/api/request.ts | 113 + .../apps/admin-antd/src/api/system/dept.ts | 87 + .../apps/admin-antd/src/api/system/index.ts | 2 + .../apps/admin-antd/src/api/system/role.ts | 113 + .../apps/admin-antd/src/api/system/user.ts | 134 + .../src/frontend/apps/admin-antd/src/app.vue | 39 + .../frontend/apps/admin-antd/src/bootstrap.ts | 76 + .../src/constants/permission-codes.ts | 32 + .../apps/admin-antd/src/layouts/auth.vue | 25 + .../apps/admin-antd/src/layouts/basic.vue | 206 + .../apps/admin-antd/src/layouts/index.ts | 6 + .../apps/admin-antd/src/locales/README.md | 3 + .../apps/admin-antd/src/locales/index.ts | 102 + .../src/locales/langs/en-US/demos.json | 13 + .../src/locales/langs/en-US/page.json | 15 + .../src/locales/langs/en-US/system.json | 48 + .../src/locales/langs/zh-CN/demos.json | 13 + .../src/locales/langs/zh-CN/page.json | 15 + .../src/locales/langs/zh-CN/system.json | 49 + .../src/frontend/apps/admin-antd/src/main.ts | 31 + .../apps/admin-antd/src/preferences.ts | 14 + .../apps/admin-antd/src/router/access.ts | 35 + .../apps/admin-antd/src/router/guard.ts | 158 + .../apps/admin-antd/src/router/index.ts | 37 + .../apps/admin-antd/src/router/routes/core.ts | 97 + .../admin-antd/src/router/routes/index.ts | 37 + .../src/router/routes/modules/dashboard.ts | 38 + .../src/router/routes/modules/demos.ts | 28 + .../src/router/routes/modules/system.ts | 52 + .../src/router/routes/modules/vben.ts | 104 + .../apps/admin-antd/src/store/auth.ts | 152 + .../apps/admin-antd/src/store/index.ts | 1 + .../admin-antd/src/utils/permission-tree.ts | 144 + .../apps/admin-antd/src/views/_core/README.md | 3 + .../src/views/_core/about/index.vue | 9 + .../views/_core/authentication/code-login.vue | 69 + .../_core/authentication/forget-password.vue | 43 + .../src/views/_core/authentication/login.vue | 53 + .../_core/authentication/qrcode-login.vue | 10 + .../views/_core/authentication/register.vue | 96 + .../src/views/_core/fallback/coming-soon.vue | 7 + .../src/views/_core/fallback/forbidden.vue | 9 + .../views/_core/fallback/internal-error.vue | 9 + .../src/views/_core/fallback/not-found.vue | 9 + .../src/views/_core/fallback/offline.vue | 9 + .../src/views/_core/profile/base-setting.vue | 50 + .../src/views/_core/profile/index.vue | 49 + .../_core/profile/notification-setting.vue | 31 + .../views/_core/profile/password-setting.vue | 66 + .../views/_core/profile/security-setting.vue | 43 + .../dashboard/analytics/analytics-trends.vue | 98 + .../analytics/analytics-visits-data.vue | 82 + .../analytics/analytics-visits-sales.vue | 46 + .../analytics/analytics-visits-source.vue | 65 + .../dashboard/analytics/analytics-visits.vue | 55 + .../src/views/dashboard/analytics/index.vue | 90 + .../src/views/dashboard/workspace/index.vue | 266 + .../admin-antd/src/views/demos/antd/index.vue | 66 + .../admin-antd/src/views/system/dept/data.ts | 136 + .../admin-antd/src/views/system/dept/list.vue | 143 + .../src/views/system/dept/modules/form.vue | 97 + .../admin-antd/src/views/system/role/data.ts | 136 + .../admin-antd/src/views/system/role/list.vue | 184 + .../src/views/system/role/modules/form.vue | 137 + .../admin-antd/src/views/system/user/data.ts | 242 + .../admin-antd/src/views/system/user/list.vue | 181 + .../src/views/system/user/modules/form.vue | 176 + .../apps/admin-antd/tailwind.config.mjs | 1 + .../frontend/apps/admin-antd/tsconfig.json | 12 + .../apps/admin-antd/tsconfig.node.json | 10 + .../frontend/apps/admin-antd/vite.config.mts | 20 + template/src/frontend/cspell.json | 70 + template/src/frontend/eslint.config.mjs | 5 + .../lint-configs/commitlint-config/index.mjs | 153 + .../commitlint-config/package.json | 33 + .../eslint-config/build.config.ts | 7 + .../lint-configs/eslint-config/package.json | 56 + .../eslint-config/src/configs/command.ts | 9 + .../eslint-config/src/configs/comments.ts | 24 + .../eslint-config/src/configs/disableds.ts | 28 + .../eslint-config/src/configs/ignores.ts | 52 + .../eslint-config/src/configs/import.ts | 25 + .../eslint-config/src/configs/index.ts | 17 + .../eslint-config/src/configs/javascript.ts | 241 + .../eslint-config/src/configs/jsdoc.ts | 34 + .../eslint-config/src/configs/jsonc.ts | 258 + .../eslint-config/src/configs/node.ts | 57 + .../src/configs/perfectionist.ts | 88 + .../eslint-config/src/configs/prettier.ts | 19 + .../eslint-config/src/configs/regexp.ts | 20 + .../eslint-config/src/configs/test.ts | 45 + .../eslint-config/src/configs/turbo.ts | 17 + .../eslint-config/src/configs/typescript.ts | 71 + .../eslint-config/src/configs/unicorn.ts | 45 + .../eslint-config/src/configs/vue.ts | 152 + .../eslint-config/src/custom-config.ts | 168 + .../lint-configs/eslint-config/src/index.ts | 60 + .../lint-configs/eslint-config/src/util.ts | 8 + .../lint-configs/eslint-config/tsconfig.json | 9 + .../lint-configs/prettier-config/index.mjs | 18 + .../lint-configs/prettier-config/package.json | 28 + .../lint-configs/stylelint-config/index.mjs | 141 + .../stylelint-config/package.json | 43 + .../internal/node-utils/build.config.ts | 7 + .../frontend/internal/node-utils/package.json | 43 + .../node-utils/src/__tests__/hash.test.ts | 52 + .../node-utils/src/__tests__/path.test.ts | 67 + .../internal/node-utils/src/constants.ts | 6 + .../frontend/internal/node-utils/src/date.ts | 12 + .../frontend/internal/node-utils/src/fs.ts | 39 + .../frontend/internal/node-utils/src/git.ts | 34 + .../frontend/internal/node-utils/src/hash.ts | 18 + .../frontend/internal/node-utils/src/index.ts | 19 + .../internal/node-utils/src/monorepo.ts | 46 + .../frontend/internal/node-utils/src/path.ts | 11 + .../internal/node-utils/src/prettier.ts | 21 + .../internal/node-utils/src/spinner.ts | 26 + .../internal/node-utils/tsconfig.json | 6 + .../internal/tailwind-config/build.config.ts | 10 + .../internal/tailwind-config/package.json | 67 + .../internal/tailwind-config/src/index.ts | 266 + .../internal/tailwind-config/src/module.d.ts | 3 + .../tailwind-config/src/plugins/entry.ts | 53 + .../tailwind-config/src/postcss.config.ts | 15 + .../internal/tailwind-config/tsconfig.json | 9 + .../src/frontend/internal/tsconfig/base.json | 40 + .../frontend/internal/tsconfig/library.json | 13 + .../src/frontend/internal/tsconfig/node.json | 12 + .../frontend/internal/tsconfig/package.json | 25 + .../frontend/internal/tsconfig/web-app.json | 8 + .../src/frontend/internal/tsconfig/web.json | 14 + .../internal/vite-config/build.config.ts | 7 + .../internal/vite-config/package.json | 59 + .../vite-config/src/config/application.ts | 125 + .../internal/vite-config/src/config/common.ts | 13 + .../internal/vite-config/src/config/index.ts | 37 + .../vite-config/src/config/library.ts | 59 + .../internal/vite-config/src/index.ts | 4 + .../internal/vite-config/src/options.ts | 45 + .../vite-config/src/plugins/archiver.ts | 75 + .../src/plugins/extra-app-config.ts | 92 + .../vite-config/src/plugins/importmap.ts | 245 + .../internal/vite-config/src/plugins/index.ts | 247 + .../src/plugins/inject-app-loading/README.md | 3 + .../default-loading-antd.html | 107 + .../inject-app-loading/default-loading.html | 113 + .../src/plugins/inject-app-loading/index.ts | 66 + .../src/plugins/inject-metadata.ts | 111 + .../vite-config/src/plugins/license.ts | 63 + .../vite-config/src/plugins/nitro-mock.ts | 98 + .../internal/vite-config/src/plugins/print.ts | 28 + .../vite-config/src/plugins/vxe-table.ts | 20 + .../internal/vite-config/src/typing.ts | 351 + .../internal/vite-config/src/utils/env.ts | 110 + .../internal/vite-config/tsconfig.json | 6 + template/src/frontend/lefthook.yml | 76 + template/src/frontend/package.json | 114 + template/src/frontend/pnpm-lock.yaml | 18933 ++++++++++++++++ template/src/frontend/pnpm-workspace.yaml | 195 + template/src/frontend/scripts/clean.mjs | 141 + .../src/frontend/scripts/deploy/Dockerfile | 37 + .../deploy/build-local-docker-image.sh | 55 + .../src/frontend/scripts/deploy/nginx.conf | 75 + .../src/frontend/scripts/turbo-run/README.md | 59 + .../scripts/turbo-run/build.config.ts | 7 + .../frontend/scripts/turbo-run/package.json | 29 + .../frontend/scripts/turbo-run/src/index.ts | 29 + .../src/frontend/scripts/turbo-run/src/run.ts | 67 + .../frontend/scripts/turbo-run/tsconfig.json | 6 + template/src/frontend/scripts/vsh/README.md | 56 + .../src/frontend/scripts/vsh/build.config.ts | 7 + .../src/frontend/scripts/vsh/package.json | 31 + .../scripts/vsh/src/check-circular/index.ts | 170 + .../scripts/vsh/src/check-dep/index.ts | 194 + .../scripts/vsh/src/code-workspace/index.ts | 78 + .../src/frontend/scripts/vsh/src/index.ts | 74 + .../frontend/scripts/vsh/src/lint/index.ts | 48 + .../frontend/scripts/vsh/src/publint/index.ts | 185 + .../src/frontend/scripts/vsh/tsconfig.json | 6 + template/src/frontend/stylelint.config.mjs | 4 + template/src/frontend/tea.yaml | 6 + template/src/frontend/turbo.json | 45 + template/src/frontend/vitest.config.ts | 11 + template/src/frontend/vitest.workspace.ts | 3 + .../test/ABC.Template.Web.Tests/DemoTests.cs | 2 + .../test/ABC.Template.Web.Tests/DeptTests.cs | 524 + .../Fixtures/AuthenticatedTestBase.cs | 40 + .../test/ABC.Template.Web.Tests/OrderTests.cs | 2 + .../test/ABC.Template.Web.Tests/RoleTests.cs | 446 + .../test/ABC.Template.Web.Tests/UserTests.cs | 439 +- 350 files changed, 39720 insertions(+), 40 deletions(-) create mode 100644 template/src/ABC.Template.Domain/AggregatesModel/DeptAggregate/Dept.cs create mode 100644 template/src/ABC.Template.Domain/AggregatesModel/RoleAggregate/Role.cs create mode 100644 template/src/ABC.Template.Domain/AggregatesModel/RoleAggregate/RolePermission.cs create mode 100644 template/src/ABC.Template.Domain/AggregatesModel/UserAggregate/User.cs create mode 100644 template/src/ABC.Template.Domain/AggregatesModel/UserAggregate/UserDept.cs create mode 100644 template/src/ABC.Template.Domain/AggregatesModel/UserAggregate/UserRefreshToken.cs create mode 100644 template/src/ABC.Template.Domain/AggregatesModel/UserAggregate/UserRole.cs create mode 100644 template/src/ABC.Template.Domain/DomainEvents/DeptEvents/DeptInfoChangedDomainEvent.cs create mode 100644 template/src/ABC.Template.Domain/DomainEvents/RoleEvents.cs create mode 100644 template/src/ABC.Template.Domain/ErrorCodes.cs create mode 100644 template/src/ABC.Template.Infrastructure/EntityConfigurations/DeptEntityTypeConfiguration.cs create mode 100644 template/src/ABC.Template.Infrastructure/EntityConfigurations/RoleEntityTypeConfiguration.cs create mode 100644 template/src/ABC.Template.Infrastructure/EntityConfigurations/UserEntityTypeConfiguration.cs create mode 100644 template/src/ABC.Template.Infrastructure/Repositories/DeptRepository.cs create mode 100644 template/src/ABC.Template.Infrastructure/Repositories/RoleRepository.cs create mode 100644 template/src/ABC.Template.Infrastructure/Repositories/UserRepository.cs create mode 100644 template/src/ABC.Template.Web/AppPermissions/AppPermission.cs create mode 100644 template/src/ABC.Template.Web/AppPermissions/AppPermissionGroup.cs create mode 100644 template/src/ABC.Template.Web/AppPermissions/PermissionCodes.cs create mode 100644 template/src/ABC.Template.Web/AppPermissions/PermissionDefinitionContext.cs create mode 100644 template/src/ABC.Template.Web/Application/Commands/Identity/Admin/DeptCommands/CreateDeptCommand.cs create mode 100644 template/src/ABC.Template.Web/Application/Commands/Identity/Admin/DeptCommands/DeleteDeptCommand.cs create mode 100644 template/src/ABC.Template.Web/Application/Commands/Identity/Admin/DeptCommands/UpdateDeptCommand.cs create mode 100644 template/src/ABC.Template.Web/Application/Commands/Identity/Admin/RoleCommands/ActivateRoleCommand.cs create mode 100644 template/src/ABC.Template.Web/Application/Commands/Identity/Admin/RoleCommands/CreateRoleCommand.cs create mode 100644 template/src/ABC.Template.Web/Application/Commands/Identity/Admin/RoleCommands/DeactivateRoleCommand.cs create mode 100644 template/src/ABC.Template.Web/Application/Commands/Identity/Admin/RoleCommands/DeleteRoleCommand.cs create mode 100644 template/src/ABC.Template.Web/Application/Commands/Identity/Admin/RoleCommands/UpdateRoleInfoCommand.cs create mode 100644 template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/CreateUserCommand.cs create mode 100644 template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/DeleteUserCommand.cs create mode 100644 template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/PasswordResetCommand.cs create mode 100644 template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/RevokeUserRefreshTokensCommand.cs create mode 100644 template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/UpdateUserCommand.cs create mode 100644 template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/UpdateUserDeptNameCommand.cs create mode 100644 template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/UpdateUserLoginTimeCommand.cs create mode 100644 template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/UpdateUserRoleNameCommand.cs create mode 100644 template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/UpdateUserRolesCommand.cs create mode 100644 template/src/ABC.Template.Web/Application/DomainEventHandlers/DeptInfoChangedDomainEventHandlerForUpdateUserDeptName.cs create mode 100644 template/src/ABC.Template.Web/Application/DomainEventHandlers/RoleInfoChangedDomainEventHandlerForUpdateUserRoleName.cs create mode 100644 template/src/ABC.Template.Web/Application/Queries/DeptQuery.cs create mode 100644 template/src/ABC.Template.Web/Application/Queries/RoleQuery.cs create mode 100644 template/src/ABC.Template.Web/Application/Queries/UserQuery.cs create mode 100644 template/src/ABC.Template.Web/Endpoints/Identity/Admin/DeptEndpoints/CreateDeptEndpoint.cs create mode 100644 template/src/ABC.Template.Web/Endpoints/Identity/Admin/DeptEndpoints/DeleteDeptEndpoint.cs create mode 100644 template/src/ABC.Template.Web/Endpoints/Identity/Admin/DeptEndpoints/GetDeptEndpoint.cs create mode 100644 template/src/ABC.Template.Web/Endpoints/Identity/Admin/DeptEndpoints/GetDeptTreeEndpoint.cs create mode 100644 template/src/ABC.Template.Web/Endpoints/Identity/Admin/DeptEndpoints/UpdateDeptEndpoint.cs create mode 100644 template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/ActivateRoleEndpoint.cs create mode 100644 template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/CreateRoleEndpoint.cs create mode 100644 template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/DeactivateRoleEndpoint.cs create mode 100644 template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/DeleteRoleEndpoint.cs create mode 100644 template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/GetAllRolesEndpoint.cs create mode 100644 template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/GetRoleEndpoint.cs create mode 100644 template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/UpdateRoleEndpoint.cs create mode 100644 template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/AuthEndpoint.cs create mode 100644 template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/CreateUserEndpoint.cs create mode 100644 template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/DeleteUserEndpoint.cs create mode 100644 template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/GetAllUsersEndpoint.cs create mode 100644 template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/GetUserAccessCodesEndpoint.cs create mode 100644 template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/GetUserEndpoint.cs create mode 100644 template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/GetUserProfileEndpoint.cs create mode 100644 template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/LoginEndpoint.cs create mode 100644 template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/LogoutEndpoint.cs create mode 100644 template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/PasswordResetEndpoint.cs create mode 100644 template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/RegisterEndpoint.cs create mode 100644 template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/UpdateUserEndpoint.cs create mode 100644 template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/UpdateUserRolesEndpoint.cs create mode 100644 template/src/ABC.Template.Web/Extensions/ServiceCollectionExtensions.cs create mode 100644 template/src/ABC.Template.Web/Utils/AppConfiguration.cs create mode 100644 template/src/ABC.Template.Web/Utils/PasswordHasher.cs create mode 100644 template/src/ABC.Template.Web/Utils/SeedDatabaseExtension.cs create mode 100644 template/src/ABC.Template.Web/Utils/TokenGenerator.cs create mode 100644 template/src/frontend/.browserslistrc create mode 100644 template/src/frontend/.changeset/README.md create mode 100644 template/src/frontend/.changeset/config.json create mode 100644 template/src/frontend/.commitlintrc.js create mode 100644 template/src/frontend/.dockerignore create mode 100644 template/src/frontend/.editorconfig create mode 100644 template/src/frontend/.gitattributes create mode 100644 template/src/frontend/.gitconfig create mode 100644 template/src/frontend/.github/CODEOWNERS create mode 100644 template/src/frontend/.github/ISSUE_TEMPLATE/bug-report.yml create mode 100644 template/src/frontend/.github/ISSUE_TEMPLATE/docs.yml create mode 100644 template/src/frontend/.github/ISSUE_TEMPLATE/feature-request.yml create mode 100644 template/src/frontend/.github/actions/setup-node/action.yml create mode 100644 template/src/frontend/.github/commit-convention.md create mode 100644 template/src/frontend/.github/config.yml create mode 100644 template/src/frontend/.github/contributing.md create mode 100644 template/src/frontend/.github/dependabot.yml create mode 100644 template/src/frontend/.github/pull_request_template.md create mode 100644 template/src/frontend/.github/release-drafter.yml create mode 100644 template/src/frontend/.github/semantic.yml create mode 100644 template/src/frontend/.github/workflows/build.yml create mode 100644 template/src/frontend/.github/workflows/changeset-version.yml create mode 100644 template/src/frontend/.github/workflows/ci.yml create mode 100644 template/src/frontend/.github/workflows/codeql.yml create mode 100644 template/src/frontend/.github/workflows/deploy.yml create mode 100644 template/src/frontend/.github/workflows/draft.yml create mode 100644 template/src/frontend/.github/workflows/issue-close-require.yml create mode 100644 template/src/frontend/.github/workflows/issue-labeled.yml create mode 100644 template/src/frontend/.github/workflows/lock.yml create mode 100644 template/src/frontend/.github/workflows/release-tag.yml create mode 100644 template/src/frontend/.github/workflows/rerun.yml create mode 100644 template/src/frontend/.github/workflows/semantic-pull-request.yml create mode 100644 template/src/frontend/.github/workflows/stale.yml create mode 100644 template/src/frontend/.gitignore create mode 100644 template/src/frontend/.gitpod.yml create mode 100644 template/src/frontend/.node-version create mode 100644 template/src/frontend/.npmrc create mode 100644 template/src/frontend/.prettierignore create mode 100644 template/src/frontend/.prettierrc.mjs create mode 100644 template/src/frontend/.stylelintignore create mode 100644 template/src/frontend/.vscode/extensions.json create mode 100644 template/src/frontend/.vscode/global.code-snippets create mode 100644 template/src/frontend/.vscode/launch.json create mode 100644 template/src/frontend/.vscode/settings.json create mode 100644 template/src/frontend/LICENSE create mode 100644 template/src/frontend/README.ja-JP.md create mode 100644 template/src/frontend/README.md create mode 100644 template/src/frontend/README.zh-CN.md create mode 100644 template/src/frontend/apps/admin-antd/.env create mode 100644 template/src/frontend/apps/admin-antd/.env.analyze create mode 100644 template/src/frontend/apps/admin-antd/.env.development create mode 100644 template/src/frontend/apps/admin-antd/.env.production create mode 100644 template/src/frontend/apps/admin-antd/index.html create mode 100644 template/src/frontend/apps/admin-antd/package.json create mode 100644 template/src/frontend/apps/admin-antd/postcss.config.mjs create mode 100644 template/src/frontend/apps/admin-antd/public/favicon.ico create mode 100644 template/src/frontend/apps/admin-antd/src/adapter/component/index.ts create mode 100644 template/src/frontend/apps/admin-antd/src/adapter/form.ts create mode 100644 template/src/frontend/apps/admin-antd/src/adapter/vxe-table.ts create mode 100644 template/src/frontend/apps/admin-antd/src/api/core/auth.ts create mode 100644 template/src/frontend/apps/admin-antd/src/api/core/index.ts create mode 100644 template/src/frontend/apps/admin-antd/src/api/core/menu.ts create mode 100644 template/src/frontend/apps/admin-antd/src/api/core/user.ts create mode 100644 template/src/frontend/apps/admin-antd/src/api/index.ts create mode 100644 template/src/frontend/apps/admin-antd/src/api/request.ts create mode 100644 template/src/frontend/apps/admin-antd/src/api/system/dept.ts create mode 100644 template/src/frontend/apps/admin-antd/src/api/system/index.ts create mode 100644 template/src/frontend/apps/admin-antd/src/api/system/role.ts create mode 100644 template/src/frontend/apps/admin-antd/src/api/system/user.ts create mode 100644 template/src/frontend/apps/admin-antd/src/app.vue create mode 100644 template/src/frontend/apps/admin-antd/src/bootstrap.ts create mode 100644 template/src/frontend/apps/admin-antd/src/constants/permission-codes.ts create mode 100644 template/src/frontend/apps/admin-antd/src/layouts/auth.vue create mode 100644 template/src/frontend/apps/admin-antd/src/layouts/basic.vue create mode 100644 template/src/frontend/apps/admin-antd/src/layouts/index.ts create mode 100644 template/src/frontend/apps/admin-antd/src/locales/README.md create mode 100644 template/src/frontend/apps/admin-antd/src/locales/index.ts create mode 100644 template/src/frontend/apps/admin-antd/src/locales/langs/en-US/demos.json create mode 100644 template/src/frontend/apps/admin-antd/src/locales/langs/en-US/page.json create mode 100644 template/src/frontend/apps/admin-antd/src/locales/langs/en-US/system.json create mode 100644 template/src/frontend/apps/admin-antd/src/locales/langs/zh-CN/demos.json create mode 100644 template/src/frontend/apps/admin-antd/src/locales/langs/zh-CN/page.json create mode 100644 template/src/frontend/apps/admin-antd/src/locales/langs/zh-CN/system.json create mode 100644 template/src/frontend/apps/admin-antd/src/main.ts create mode 100644 template/src/frontend/apps/admin-antd/src/preferences.ts create mode 100644 template/src/frontend/apps/admin-antd/src/router/access.ts create mode 100644 template/src/frontend/apps/admin-antd/src/router/guard.ts create mode 100644 template/src/frontend/apps/admin-antd/src/router/index.ts create mode 100644 template/src/frontend/apps/admin-antd/src/router/routes/core.ts create mode 100644 template/src/frontend/apps/admin-antd/src/router/routes/index.ts create mode 100644 template/src/frontend/apps/admin-antd/src/router/routes/modules/dashboard.ts create mode 100644 template/src/frontend/apps/admin-antd/src/router/routes/modules/demos.ts create mode 100644 template/src/frontend/apps/admin-antd/src/router/routes/modules/system.ts create mode 100644 template/src/frontend/apps/admin-antd/src/router/routes/modules/vben.ts create mode 100644 template/src/frontend/apps/admin-antd/src/store/auth.ts create mode 100644 template/src/frontend/apps/admin-antd/src/store/index.ts create mode 100644 template/src/frontend/apps/admin-antd/src/utils/permission-tree.ts create mode 100644 template/src/frontend/apps/admin-antd/src/views/_core/README.md create mode 100644 template/src/frontend/apps/admin-antd/src/views/_core/about/index.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/_core/authentication/code-login.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/_core/authentication/forget-password.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/_core/authentication/login.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/_core/authentication/qrcode-login.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/_core/authentication/register.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/_core/fallback/coming-soon.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/_core/fallback/forbidden.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/_core/fallback/internal-error.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/_core/fallback/not-found.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/_core/fallback/offline.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/_core/profile/base-setting.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/_core/profile/index.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/_core/profile/notification-setting.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/_core/profile/password-setting.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/_core/profile/security-setting.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/dashboard/analytics/analytics-trends.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/dashboard/analytics/analytics-visits-data.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/dashboard/analytics/analytics-visits-sales.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/dashboard/analytics/analytics-visits-source.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/dashboard/analytics/analytics-visits.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/dashboard/analytics/index.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/dashboard/workspace/index.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/demos/antd/index.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/system/dept/data.ts create mode 100644 template/src/frontend/apps/admin-antd/src/views/system/dept/list.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/system/dept/modules/form.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/system/role/data.ts create mode 100644 template/src/frontend/apps/admin-antd/src/views/system/role/list.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/system/role/modules/form.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/system/user/data.ts create mode 100644 template/src/frontend/apps/admin-antd/src/views/system/user/list.vue create mode 100644 template/src/frontend/apps/admin-antd/src/views/system/user/modules/form.vue create mode 100644 template/src/frontend/apps/admin-antd/tailwind.config.mjs create mode 100644 template/src/frontend/apps/admin-antd/tsconfig.json create mode 100644 template/src/frontend/apps/admin-antd/tsconfig.node.json create mode 100644 template/src/frontend/apps/admin-antd/vite.config.mts create mode 100644 template/src/frontend/cspell.json create mode 100644 template/src/frontend/eslint.config.mjs create mode 100644 template/src/frontend/internal/lint-configs/commitlint-config/index.mjs create mode 100644 template/src/frontend/internal/lint-configs/commitlint-config/package.json create mode 100644 template/src/frontend/internal/lint-configs/eslint-config/build.config.ts create mode 100644 template/src/frontend/internal/lint-configs/eslint-config/package.json create mode 100644 template/src/frontend/internal/lint-configs/eslint-config/src/configs/command.ts create mode 100644 template/src/frontend/internal/lint-configs/eslint-config/src/configs/comments.ts create mode 100644 template/src/frontend/internal/lint-configs/eslint-config/src/configs/disableds.ts create mode 100644 template/src/frontend/internal/lint-configs/eslint-config/src/configs/ignores.ts create mode 100644 template/src/frontend/internal/lint-configs/eslint-config/src/configs/import.ts create mode 100644 template/src/frontend/internal/lint-configs/eslint-config/src/configs/index.ts create mode 100644 template/src/frontend/internal/lint-configs/eslint-config/src/configs/javascript.ts create mode 100644 template/src/frontend/internal/lint-configs/eslint-config/src/configs/jsdoc.ts create mode 100644 template/src/frontend/internal/lint-configs/eslint-config/src/configs/jsonc.ts create mode 100644 template/src/frontend/internal/lint-configs/eslint-config/src/configs/node.ts create mode 100644 template/src/frontend/internal/lint-configs/eslint-config/src/configs/perfectionist.ts create mode 100644 template/src/frontend/internal/lint-configs/eslint-config/src/configs/prettier.ts create mode 100644 template/src/frontend/internal/lint-configs/eslint-config/src/configs/regexp.ts create mode 100644 template/src/frontend/internal/lint-configs/eslint-config/src/configs/test.ts create mode 100644 template/src/frontend/internal/lint-configs/eslint-config/src/configs/turbo.ts create mode 100644 template/src/frontend/internal/lint-configs/eslint-config/src/configs/typescript.ts create mode 100644 template/src/frontend/internal/lint-configs/eslint-config/src/configs/unicorn.ts create mode 100644 template/src/frontend/internal/lint-configs/eslint-config/src/configs/vue.ts create mode 100644 template/src/frontend/internal/lint-configs/eslint-config/src/custom-config.ts create mode 100644 template/src/frontend/internal/lint-configs/eslint-config/src/index.ts create mode 100644 template/src/frontend/internal/lint-configs/eslint-config/src/util.ts create mode 100644 template/src/frontend/internal/lint-configs/eslint-config/tsconfig.json create mode 100644 template/src/frontend/internal/lint-configs/prettier-config/index.mjs create mode 100644 template/src/frontend/internal/lint-configs/prettier-config/package.json create mode 100644 template/src/frontend/internal/lint-configs/stylelint-config/index.mjs create mode 100644 template/src/frontend/internal/lint-configs/stylelint-config/package.json create mode 100644 template/src/frontend/internal/node-utils/build.config.ts create mode 100644 template/src/frontend/internal/node-utils/package.json create mode 100644 template/src/frontend/internal/node-utils/src/__tests__/hash.test.ts create mode 100644 template/src/frontend/internal/node-utils/src/__tests__/path.test.ts create mode 100644 template/src/frontend/internal/node-utils/src/constants.ts create mode 100644 template/src/frontend/internal/node-utils/src/date.ts create mode 100644 template/src/frontend/internal/node-utils/src/fs.ts create mode 100644 template/src/frontend/internal/node-utils/src/git.ts create mode 100644 template/src/frontend/internal/node-utils/src/hash.ts create mode 100644 template/src/frontend/internal/node-utils/src/index.ts create mode 100644 template/src/frontend/internal/node-utils/src/monorepo.ts create mode 100644 template/src/frontend/internal/node-utils/src/path.ts create mode 100644 template/src/frontend/internal/node-utils/src/prettier.ts create mode 100644 template/src/frontend/internal/node-utils/src/spinner.ts create mode 100644 template/src/frontend/internal/node-utils/tsconfig.json create mode 100644 template/src/frontend/internal/tailwind-config/build.config.ts create mode 100644 template/src/frontend/internal/tailwind-config/package.json create mode 100644 template/src/frontend/internal/tailwind-config/src/index.ts create mode 100644 template/src/frontend/internal/tailwind-config/src/module.d.ts create mode 100644 template/src/frontend/internal/tailwind-config/src/plugins/entry.ts create mode 100644 template/src/frontend/internal/tailwind-config/src/postcss.config.ts create mode 100644 template/src/frontend/internal/tailwind-config/tsconfig.json create mode 100644 template/src/frontend/internal/tsconfig/base.json create mode 100644 template/src/frontend/internal/tsconfig/library.json create mode 100644 template/src/frontend/internal/tsconfig/node.json create mode 100644 template/src/frontend/internal/tsconfig/package.json create mode 100644 template/src/frontend/internal/tsconfig/web-app.json create mode 100644 template/src/frontend/internal/tsconfig/web.json create mode 100644 template/src/frontend/internal/vite-config/build.config.ts create mode 100644 template/src/frontend/internal/vite-config/package.json create mode 100644 template/src/frontend/internal/vite-config/src/config/application.ts create mode 100644 template/src/frontend/internal/vite-config/src/config/common.ts create mode 100644 template/src/frontend/internal/vite-config/src/config/index.ts create mode 100644 template/src/frontend/internal/vite-config/src/config/library.ts create mode 100644 template/src/frontend/internal/vite-config/src/index.ts create mode 100644 template/src/frontend/internal/vite-config/src/options.ts create mode 100644 template/src/frontend/internal/vite-config/src/plugins/archiver.ts create mode 100644 template/src/frontend/internal/vite-config/src/plugins/extra-app-config.ts create mode 100644 template/src/frontend/internal/vite-config/src/plugins/importmap.ts create mode 100644 template/src/frontend/internal/vite-config/src/plugins/index.ts create mode 100644 template/src/frontend/internal/vite-config/src/plugins/inject-app-loading/README.md create mode 100644 template/src/frontend/internal/vite-config/src/plugins/inject-app-loading/default-loading-antd.html create mode 100644 template/src/frontend/internal/vite-config/src/plugins/inject-app-loading/default-loading.html create mode 100644 template/src/frontend/internal/vite-config/src/plugins/inject-app-loading/index.ts create mode 100644 template/src/frontend/internal/vite-config/src/plugins/inject-metadata.ts create mode 100644 template/src/frontend/internal/vite-config/src/plugins/license.ts create mode 100644 template/src/frontend/internal/vite-config/src/plugins/nitro-mock.ts create mode 100644 template/src/frontend/internal/vite-config/src/plugins/print.ts create mode 100644 template/src/frontend/internal/vite-config/src/plugins/vxe-table.ts create mode 100644 template/src/frontend/internal/vite-config/src/typing.ts create mode 100644 template/src/frontend/internal/vite-config/src/utils/env.ts create mode 100644 template/src/frontend/internal/vite-config/tsconfig.json create mode 100644 template/src/frontend/lefthook.yml create mode 100644 template/src/frontend/package.json create mode 100644 template/src/frontend/pnpm-lock.yaml create mode 100644 template/src/frontend/pnpm-workspace.yaml create mode 100644 template/src/frontend/scripts/clean.mjs create mode 100644 template/src/frontend/scripts/deploy/Dockerfile create mode 100644 template/src/frontend/scripts/deploy/build-local-docker-image.sh create mode 100644 template/src/frontend/scripts/deploy/nginx.conf create mode 100644 template/src/frontend/scripts/turbo-run/README.md create mode 100644 template/src/frontend/scripts/turbo-run/build.config.ts create mode 100644 template/src/frontend/scripts/turbo-run/package.json create mode 100644 template/src/frontend/scripts/turbo-run/src/index.ts create mode 100644 template/src/frontend/scripts/turbo-run/src/run.ts create mode 100644 template/src/frontend/scripts/turbo-run/tsconfig.json create mode 100644 template/src/frontend/scripts/vsh/README.md create mode 100644 template/src/frontend/scripts/vsh/build.config.ts create mode 100644 template/src/frontend/scripts/vsh/package.json create mode 100644 template/src/frontend/scripts/vsh/src/check-circular/index.ts create mode 100644 template/src/frontend/scripts/vsh/src/check-dep/index.ts create mode 100644 template/src/frontend/scripts/vsh/src/code-workspace/index.ts create mode 100644 template/src/frontend/scripts/vsh/src/index.ts create mode 100644 template/src/frontend/scripts/vsh/src/lint/index.ts create mode 100644 template/src/frontend/scripts/vsh/src/publint/index.ts create mode 100644 template/src/frontend/scripts/vsh/tsconfig.json create mode 100644 template/src/frontend/stylelint.config.mjs create mode 100644 template/src/frontend/tea.yaml create mode 100644 template/src/frontend/turbo.json create mode 100644 template/src/frontend/vitest.config.ts create mode 100644 template/src/frontend/vitest.workspace.ts create mode 100644 template/test/ABC.Template.Web.Tests/DeptTests.cs create mode 100644 template/test/ABC.Template.Web.Tests/Fixtures/AuthenticatedTestBase.cs create mode 100644 template/test/ABC.Template.Web.Tests/RoleTests.cs diff --git a/README.md b/README.md index a9ea2609..b4042142 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,8 @@ dotnet new list | `--MessageQueue` | `-M` | 消息队列提供程序 | `RabbitMQ`, `Kafka`, `AzureServiceBus`, `AmazonSQS`, `NATS`, `RedisStreams`, `Pulsar` | `RabbitMQ` | | `--UseAspire` | `-U` | 启用 Aspire Dashboard 支持 | `true`, `false` | `false` | | `--IncludeCopilotInstructions` | `-I` | 是否包含 Copilot 指令文件 | `true`, `false` | `true` | +| `--UseAdmin` | | 是否包含 Admin 功能(用户、角色、部门管理、权限系统、前端) | `true`, `false` | `false` | +| `--UseDemoCode` | | 是否包含演示代码(OrderAggregate、DeliverAggregate) | `true`, `false` | `false` | #### 使用示例 @@ -168,6 +170,31 @@ dotnet new netcorepal-web -n My.Project.Name -I false > **提示:** 创建项目后,请根据选择的数据库和消息队列配置,使用对应的基础设施初始化脚本来启动所需的服务。详细说明请参考生成项目中的 `scripts/README.md` 文件。 +### 关于 Admin 功能 + +当启用 `--UseAdmin` 选项时,模板会包含以下功能: + ++ **用户管理**:用户的创建、更新、删除、查询等功能 ++ **角色管理**:角色的创建、更新、删除、权限分配等功能 ++ **部门管理**:部门的创建、更新、删除、树形结构管理等功能 ++ **权限系统**:基于角色的权限控制(RBAC),支持权限码定义和验证 ++ **JWT 认证**:完整的 JWT Token 认证和刷新 Token 机制 ++ **前端界面**:基于 Vue 3 + Vben Admin 的管理后台界面 + +启用 Admin 功能后,系统会自动创建默认的管理员账户(用户名:`admin`,密码:`123456`)。 + +### 关于演示代码 + +当启用 `--UseDemoCode` 选项时,模板会包含以下演示代码: + ++ **OrderAggregate**:订单聚合根示例,展示领域驱动设计(DDD)的基本用法 ++ **DeliverAggregate**:配送记录聚合根示例 ++ **领域事件**:OrderCreatedDomainEvent、OrderPaidDomainEvent 等 ++ **集成事件**:OrderPaidIntegrationEvent 等 ++ **相关测试**:OrderTests、DemoTests 等 + +演示代码主要用于学习和参考,展示如何使用 NetCorePal 框架构建 DDD 应用。 + ### 关于 Aspire Dashboard 当启用 `--UseAspire` 选项时,模板会生成以下额外的项目: diff --git a/template/.template.config/template.json b/template/.template.config/template.json index 4c3784d4..93ef801a 100644 --- a/template/.template.config/template.json +++ b/template/.template.config/template.json @@ -117,6 +117,18 @@ "defaultValue": "true", "description": "Include Copilot instructions files (.github directory) in the generated template" }, + "UseAdmin": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "description": "Include Admin functionality (user, role, department management, permissions, and frontend)" + }, + "UseDemoCode": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "true", + "description": "Include demo code (OrderAggregate and DeliverAggregate)" + }, "SdkVersion": { "type": "generated", "generator": "switch", @@ -209,6 +221,7 @@ { "condition": "(Framework == \"net8.0\")", "exclude": [ "*.slnx" ] }, { "condition": "(Framework == \"net9.0\" || Framework == \"net10.0\")", "exclude": [ "*.sln" ] }, { "condition": "(!UseAspire)", "exclude": [ "src/ABC.Template.AppHost/**", "src/ABC.Template.ServiceDefaults/**", "test/ABC.Template.TestAppHost/**", "test/ABC.Template.AppHost.Tests/**" ] }, + { "condition": "(!UseAdmin)", "exclude": [ "src/frontend/**" ] }, // SqlServer { "condition": "(Database == \"SqlServer\" && Framework == \"net8.0\")", "rename": { diff --git a/template/src/ABC.Template.Domain/AggregatesModel/DeliverAggregate/DeliverRecord.cs b/template/src/ABC.Template.Domain/AggregatesModel/DeliverAggregate/DeliverRecord.cs index 56410d39..155e3faa 100644 --- a/template/src/ABC.Template.Domain/AggregatesModel/DeliverAggregate/DeliverRecord.cs +++ b/template/src/ABC.Template.Domain/AggregatesModel/DeliverAggregate/DeliverRecord.cs @@ -1,4 +1,5 @@ -using ABC.Template.Domain.AggregatesModel.OrderAggregate; +//#if (UseDemoCode) +using ABC.Template.Domain.AggregatesModel.OrderAggregate; namespace ABC.Template.Domain.AggregatesModel.DeliverAggregate; @@ -16,4 +17,5 @@ public DeliverRecord(OrderId orderId) public OrderId OrderId { get; private set; } = default!; } +//#endif diff --git a/template/src/ABC.Template.Domain/AggregatesModel/DeptAggregate/Dept.cs b/template/src/ABC.Template.Domain/AggregatesModel/DeptAggregate/Dept.cs new file mode 100644 index 00000000..89cef9c5 --- /dev/null +++ b/template/src/ABC.Template.Domain/AggregatesModel/DeptAggregate/Dept.cs @@ -0,0 +1,197 @@ +//#if (UseAdmin) +using System.ComponentModel.DataAnnotations.Schema; +using ABC.Template.Domain.DomainEvents.DeptEvents; +using ABC.Template.Domain; + +namespace ABC.Template.Domain.AggregatesModel.DeptAggregate; + +/// +/// 部门ID(强类型ID) +/// +public partial record DeptId : IInt64StronglyTypedId; + +/// +/// 部门聚合根 +/// 用于管理企业部门的层级结构 +/// +public class Dept : Entity, IAggregateRoot +{ + /// + /// 部门名称 + /// + public string Name { get; private set; } = string.Empty; + + /// + /// 备注 + /// + public string Remark { get; private set; } = string.Empty; + + /// + /// 上级部门ID + /// + public DeptId ParentId { get; private set; } = default!; + + /// + /// 状态(0=禁用,1=启用) + /// + public int Status { get; private set; } = 1; + + /// + /// 创建时间 + /// + public DateTimeOffset CreatedAt { get; init; } + + /// + /// 是否删除 + /// + public Deleted IsDeleted { get; private set; } = new Deleted(false); + + /// + /// 删除时间 + /// + public DeletedTime DeletedAt { get; private set; } = new DeletedTime(DateTimeOffset.UtcNow); + + /// + /// 更新时间 + /// + public UpdateTime UpdateTime { get; private set; } = new UpdateTime(DateTimeOffset.UtcNow); + + /// + /// 子部门(不映射到数据库,用于内存中的树形结构) + /// + [NotMapped] + public virtual ICollection Children { get; } = []; + + protected Dept() + { + } + + /// + /// 创建部门 + /// + /// 部门名称 + /// 备注 + /// 上级部门ID + /// 状态(0=禁用,1=启用) + public Dept(string name, string remark, DeptId parentId, int status) + { + CreatedAt = DateTimeOffset.Now; + Name = name; + Remark = remark; + ParentId = parentId; + Status = status; + } + + /// + /// 更新部门信息 + /// + /// 部门名称 + /// 备注 + /// 上级部门ID + /// 状态(0=禁用,1=启用) + public void UpdateInfo(string name, string remark, DeptId parentId, int status) + { + Name = name; + Remark = remark; + ParentId = parentId; + Status = status; + UpdateTime = new UpdateTime(DateTimeOffset.UtcNow); + + AddDomainEvent(new DeptInfoChangedDomainEvent(this)); + } + + /// + /// 激活部门 + /// + public void Activate() + { + if (Status == 1) + { + throw new KnownException("部门已经是激活状态", ErrorCodes.DeptAlreadyActivated); + } + + Status = 1; + UpdateTime = new UpdateTime(DateTimeOffset.UtcNow); + } + + /// + /// 停用部门 + /// + public void Deactivate() + { + if (Status == 0) + { + throw new KnownException("部门已经被停用", ErrorCodes.DeptAlreadyDeactivated); + } + + Status = 0; + UpdateTime = new UpdateTime(DateTimeOffset.UtcNow); + } + + /// + /// 软删除部门 + /// + public void SoftDelete() + { + if (IsDeleted) + { + throw new KnownException("部门已经被删除", ErrorCodes.DeptAlreadyDeleted); + } + + IsDeleted = true; + UpdateTime = new UpdateTime(DateTimeOffset.UtcNow); + } + + /// + /// 添加子部门 + /// + /// 子部门 + public void AddChild(Dept child) + { + if (child == null) + { + throw new KnownException("子部门不能为空", ErrorCodes.ChildDeptCannotBeEmpty); + } + + Children.Add(child); + } + + /// + /// 移除子部门 + /// + /// 子部门 + public void RemoveChild(Dept child) + { + if (child == null) + { + throw new KnownException("子部门不能为空", ErrorCodes.ChildDeptCannotBeEmpty); + } + + Children.Remove(child); + } + + /// + /// 获取所有子部门(包括子级的子级) + /// + /// 所有子部门 + public IEnumerable GetAllChildren() + { + var result = new List(); + foreach (var child in Children) + { + result.Add(child); + result.AddRange(child.GetAllChildren()); + } + return result; + } + + /// + /// 获取部门层级路径 + /// + /// 层级路径 + public string GetPath() + { + return Name; + } +} +//#endif diff --git a/template/src/ABC.Template.Domain/AggregatesModel/OrderAggregate/Order.cs b/template/src/ABC.Template.Domain/AggregatesModel/OrderAggregate/Order.cs index 570b8201..5779158e 100644 --- a/template/src/ABC.Template.Domain/AggregatesModel/OrderAggregate/Order.cs +++ b/template/src/ABC.Template.Domain/AggregatesModel/OrderAggregate/Order.cs @@ -1,4 +1,5 @@ -using ABC.Template.Domain.DomainEvents; +//#if (UseDemoCode) +using ABC.Template.Domain.DomainEvents; using NetCorePal.Extensions.Domain; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; @@ -50,3 +51,4 @@ public void OrderPaid() } } } +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Domain/AggregatesModel/RoleAggregate/Role.cs b/template/src/ABC.Template.Domain/AggregatesModel/RoleAggregate/Role.cs new file mode 100644 index 00000000..7295b00e --- /dev/null +++ b/template/src/ABC.Template.Domain/AggregatesModel/RoleAggregate/Role.cs @@ -0,0 +1,90 @@ +//#if (UseAdmin) +using ABC.Template.Domain.DomainEvents.RoleEvents; +using ABC.Template.Domain; + +namespace ABC.Template.Domain.AggregatesModel.RoleAggregate; + +public partial record RoleId : IGuidStronglyTypedId; + +public class Role : Entity, IAggregateRoot +{ + protected Role() + { + } + + public string Name { get; private set; } = string.Empty; + public string Description { get; private set; } = string.Empty; + public DateTimeOffset CreatedAt { get; init; } + public bool IsActive { get; private set; } = true; + public Deleted IsDeleted { get; private set; } = new Deleted(false); + public DeletedTime DeletedAt { get; private set; } = new DeletedTime(DateTimeOffset.UtcNow); + + public virtual ICollection Permissions { get; init; } = []; + + public Role(string name, string description, IEnumerable permissions) + { + CreatedAt = DateTimeOffset.Now; + Name = name; + Description = description; + Permissions = new List(permissions); + IsActive = true; + } + + public void UpdateRoleInfo(string name, string description) + { + Name = name; + Description = description; + AddDomainEvent(new RoleInfoChangedDomainEvent(this)); + } + + public void UpdateRolePermissions(IEnumerable newPermissions) + { + var currentPermissionMap = Permissions.ToDictionary(p => p.PermissionCode); + var newPermissionMap = newPermissions.ToDictionary(p => p.PermissionCode); + + var permissionsToRemove = currentPermissionMap.Keys.Except(newPermissionMap.Keys).ToList(); + foreach (var permissionCode in permissionsToRemove) + { + Permissions.Remove(currentPermissionMap[permissionCode]); + } + + var permissionsToAdd = newPermissionMap.Keys.Except(currentPermissionMap.Keys).ToList(); + foreach (var permissionCode in permissionsToAdd) + { + Permissions.Add(newPermissionMap[permissionCode]); + } + + AddDomainEvent(new RolePermissionChangedDomainEvent(this)); + } + + public void Deactivate() + { + if (!IsActive) + { + throw new KnownException("角色已经被停用", ErrorCodes.RoleAlreadyDeactivated); + } + + IsActive = false; + } + + public void Activate() + { + if (IsActive) + { + throw new KnownException("角色已经是激活状态", ErrorCodes.RoleAlreadyActivated); + } + + IsActive = true; + } + + public void SoftDelete() + { + IsDeleted = true; + } + + public bool HasPermission(string permissionCode) + { + return Permissions.Any(p => p.PermissionCode == permissionCode); + } +} +//#endif diff --git a/template/src/ABC.Template.Domain/AggregatesModel/RoleAggregate/RolePermission.cs b/template/src/ABC.Template.Domain/AggregatesModel/RoleAggregate/RolePermission.cs new file mode 100644 index 00000000..e5ecc3c4 --- /dev/null +++ b/template/src/ABC.Template.Domain/AggregatesModel/RoleAggregate/RolePermission.cs @@ -0,0 +1,33 @@ +//#if (UseAdmin) +namespace ABC.Template.Domain.AggregatesModel.RoleAggregate; + +public class RolePermission +{ + protected RolePermission() + { + } + + public RoleId RoleId { get; internal set; } = default!; + public string PermissionCode { get; private set; } = string.Empty; + public string PermissionName { get; private set; } = string.Empty; + public string PermissionDescription { get; private set; } = string.Empty; + + public RolePermission(string permissionCode) + { + PermissionCode = permissionCode; + } + + public RolePermission(string permissionCode, string permissionName, string permissionDescription) + { + PermissionCode = permissionCode; + PermissionName = permissionName; + PermissionDescription = permissionDescription; + } + + public void UpdatePermissionInfo(string permissionName, string permissionDescription) + { + PermissionName = permissionName; + PermissionDescription = permissionDescription; + } +} +//#endif diff --git a/template/src/ABC.Template.Domain/AggregatesModel/UserAggregate/User.cs b/template/src/ABC.Template.Domain/AggregatesModel/UserAggregate/User.cs new file mode 100644 index 00000000..23ae8285 --- /dev/null +++ b/template/src/ABC.Template.Domain/AggregatesModel/UserAggregate/User.cs @@ -0,0 +1,178 @@ +//#if (UseAdmin) +using ABC.Template.Domain.AggregatesModel.RoleAggregate; +using ABC.Template.Domain.AggregatesModel.DeptAggregate; + +namespace ABC.Template.Domain.AggregatesModel.UserAggregate; + +public partial record UserId : IInt64StronglyTypedId; + +public class User : Entity, IAggregateRoot +{ + protected User() + { + } + + public string Name { get; private set; } = string.Empty; + public string Email { get; private set; } = string.Empty; + public string Phone { get; private set; } = string.Empty; + public string RealName { get; private set; } = string.Empty; + public int Status { get; private set; } + public string PasswordHash { get; private set; } = string.Empty; + public bool IsActive { get; private set; } = true; + public DateTimeOffset CreatedAt { get; init; } + public DateTimeOffset? LastLoginTime { get; private set; } + public UpdateTime UpdateTime { get; private set; } = new UpdateTime(DateTimeOffset.UtcNow); + public Deleted IsDeleted { get; private set; } = new Deleted(false); + public DeletedTime DeletedAt { get; private set; } = new DeletedTime(DateTimeOffset.UtcNow); + public string Gender { get; private set; } = string.Empty; + public int Age { get; private set; } + public DateTimeOffset BirthDate { get; private set; } = default!; + + public virtual ICollection Roles { get; } = []; + + public virtual UserDept Dept { get; private set; } = default!; + + public ICollection RefreshTokens { get; } = []; + + public User(string name, string phone, string password, IEnumerable roles, string realName, int status, string email, string gender, DateTimeOffset birthDate) + { + CreatedAt = DateTimeOffset.Now; + Name = name; + Phone = phone; + PasswordHash = password; + RealName = realName; + Status = status; + Email = email; + Gender = gender; + Age = CalculateAge(birthDate); + BirthDate = birthDate; + foreach (var userRole in roles) + { + Roles.Add(userRole); + } + } + + public void SoftDelete() + { + IsDeleted = true; + } + + public void PasswordReset(string password) + { + PasswordHash = password; + } + + public void SetUserRefreshToken(string refreshToken) + { + var refreshTokenInfo = new UserRefreshToken(refreshToken); + RefreshTokens.Add(refreshTokenInfo); + } + + public void UpdateLastLoginTime(DateTimeOffset loginTime) + { + LastLoginTime = loginTime; + UpdateTime = new UpdateTime(DateTimeOffset.UtcNow); + } + + public static int CalculateAge(DateTimeOffset birthDate) + { + var today = DateTimeOffset.Now.Date; + int age = today.Year - birthDate.Year; + if (birthDate.Date > today.AddYears(-age)) + { + age--; + } + return age; + } + + public void UpdateUserInfo(string name, string phone, string realName, int status, string email, string gender, DateTimeOffset birthDate) + { + Name = name; + Phone = phone; + RealName = realName; + Status = status; + Email = email; + Gender = gender; + Age = CalculateAge(birthDate); + BirthDate = birthDate; + } + + public void UpdateRoleInfo(RoleId roleId, string roleName) + { + var savedRole = Roles.FirstOrDefault(r => r.RoleId == roleId); + savedRole?.UpdateRoleInfo(roleName); + UpdateTime = new UpdateTime(DateTimeOffset.UtcNow); + } + + public void UpdatePassword(string newPasswordHash) + { + if (!string.IsNullOrEmpty(newPasswordHash)) + { + PasswordHash = newPasswordHash; + UpdateTime = new UpdateTime(DateTimeOffset.UtcNow); + } + } + + public void UpdateRoles(IEnumerable rolesToBeAssigned) + { + var currentRoleMap = Roles.ToDictionary(r => r.RoleId); + var targetRoleMap = rolesToBeAssigned.ToDictionary(r => r.RoleId); + + var roleIdsToRemove = currentRoleMap.Keys.Except(targetRoleMap.Keys); + foreach (var roleId in roleIdsToRemove) + { + Roles.Remove(currentRoleMap[roleId]); + } + + var roleIdsToAdd = targetRoleMap.Keys.Except(currentRoleMap.Keys); + foreach (var roleId in roleIdsToAdd) + { + var targetRole = targetRoleMap[roleId]; + Roles.Add(targetRole); + } + } + + /// + /// 分配部门 + /// + /// 部门 + public void AssignDept(UserDept dept) + { + ArgumentNullException.ThrowIfNull(dept); + + Dept = dept; + } + + /// + /// 更新部门名称 + /// + /// 新的部门名称 + public void UpdateDeptName(string deptName) + { + if (Dept == null) + { + return; + } + + Dept.UpdateDeptName(deptName); + UpdateTime = new UpdateTime(DateTimeOffset.UtcNow); + } + + /// + /// 撤销所有未过期的刷新令牌(用于退出登录) + /// + public void RevokeAllRefreshTokens() + { + var now = DateTimeOffset.UtcNow; + foreach (var token in RefreshTokens) + { + // 只撤销未过期且未使用的令牌 + if (!token.IsRevoked && !token.IsUsed && token.ExpiresTime > now) + { + token.Revoke(); + } + } + UpdateTime = new UpdateTime(now); + } +} +//#endif diff --git a/template/src/ABC.Template.Domain/AggregatesModel/UserAggregate/UserDept.cs b/template/src/ABC.Template.Domain/AggregatesModel/UserAggregate/UserDept.cs new file mode 100644 index 00000000..193bdbb2 --- /dev/null +++ b/template/src/ABC.Template.Domain/AggregatesModel/UserAggregate/UserDept.cs @@ -0,0 +1,65 @@ +//#if (UseAdmin) +using ABC.Template.Domain.AggregatesModel.DeptAggregate; +using ABC.Template.Domain; + +namespace ABC.Template.Domain.AggregatesModel.UserAggregate; + +/// +/// 用户部门关系实体 +/// 表示用户与部门的一对一关系 +/// +public class UserDept +{ + /// + /// 用户ID + /// + public UserId UserId { get; private set; } = default!; + + /// + /// 部门ID + /// + public DeptId DeptId { get; private set; } = default!; + + /// + /// 部门名称 + /// + public string DeptName { get; private set; } = string.Empty; + + /// + /// 分配时间 + /// + public DateTimeOffset AssignedAt { get; init; } + + protected UserDept() + { + } + + /// + /// 创建用户部门关系 + /// + /// 用户ID + /// 部门ID + /// 部门名称 + public UserDept(UserId userId, DeptId deptId, string deptName) + { + UserId = userId; + DeptId = deptId; + AssignedAt = DateTimeOffset.UtcNow; + DeptName = deptName; + } + + /// + /// 更新部门名称 + /// + /// 新的部门名称 + public void UpdateDeptName(string deptName) + { + if (string.IsNullOrWhiteSpace(deptName)) + { + throw new KnownException("部门名称不能为空", ErrorCodes.DeptNameCannotBeEmpty); + } + + DeptName = deptName; + } +} +//#endif diff --git a/template/src/ABC.Template.Domain/AggregatesModel/UserAggregate/UserRefreshToken.cs b/template/src/ABC.Template.Domain/AggregatesModel/UserAggregate/UserRefreshToken.cs new file mode 100644 index 00000000..a4c719d3 --- /dev/null +++ b/template/src/ABC.Template.Domain/AggregatesModel/UserAggregate/UserRefreshToken.cs @@ -0,0 +1,43 @@ +//#if (UseAdmin) +using ABC.Template.Domain; + +namespace ABC.Template.Domain.AggregatesModel.UserAggregate; + +public partial record UserRefreshTokenId : IInt64StronglyTypedId; + +public class UserRefreshToken : Entity +{ + protected UserRefreshToken() + { + } + + public UserRefreshToken(string token) + { + Token = token; + CreatedTime = DateTimeOffset.Now; + ExpiresTime = CreatedTime.AddDays(1); + } + + public UserId UserId { get; private set; } = null!; + public string Token { get; private set; } = string.Empty; + public DateTimeOffset CreatedTime { get; init; } + public DateTimeOffset ExpiresTime { get; init; } + public bool IsUsed { get; private set; } + public bool IsRevoked { get; private set; } + + public void Use() + { + if (IsUsed || + IsRevoked || + ExpiresTime < DateTimeOffset.Now) + throw new KnownException("无效的刷新令牌", ErrorCodes.InvalidRefreshToken); + + IsUsed = true; + } + + public void Revoke() + { + IsRevoked = true; + } +} +//#endif diff --git a/template/src/ABC.Template.Domain/AggregatesModel/UserAggregate/UserRole.cs b/template/src/ABC.Template.Domain/AggregatesModel/UserAggregate/UserRole.cs new file mode 100644 index 00000000..6a204235 --- /dev/null +++ b/template/src/ABC.Template.Domain/AggregatesModel/UserAggregate/UserRole.cs @@ -0,0 +1,25 @@ +//#if (UseAdmin) +using ABC.Template.Domain.AggregatesModel.RoleAggregate; + +namespace ABC.Template.Domain.AggregatesModel.UserAggregate; + +public class UserRole +{ + protected UserRole() { } + + public UserId UserId { get; private set; } = default!; + public RoleId RoleId { get; private set; } = default!; + public string RoleName { get; private set; } = string.Empty; + + public UserRole(RoleId roleId, string roleName) + { + RoleId = roleId; + RoleName = roleName; + } + + public void UpdateRoleInfo(string roleName) + { + RoleName = roleName; + } +} +//#endif diff --git a/template/src/ABC.Template.Domain/DomainEvents/DeptEvents/DeptInfoChangedDomainEvent.cs b/template/src/ABC.Template.Domain/DomainEvents/DeptEvents/DeptInfoChangedDomainEvent.cs new file mode 100644 index 00000000..3bf5c8ba --- /dev/null +++ b/template/src/ABC.Template.Domain/DomainEvents/DeptEvents/DeptInfoChangedDomainEvent.cs @@ -0,0 +1,10 @@ +//#if (UseAdmin) +using ABC.Template.Domain.AggregatesModel.DeptAggregate; + +namespace ABC.Template.Domain.DomainEvents.DeptEvents; + +/// +/// 部门信息变更领域事件 +/// +public record DeptInfoChangedDomainEvent(Dept Dept) : IDomainEvent; +//#endif diff --git a/template/src/ABC.Template.Domain/DomainEvents/OrderDomainEvents.cs b/template/src/ABC.Template.Domain/DomainEvents/OrderDomainEvents.cs index 2e7f5478..90f7efe4 100644 --- a/template/src/ABC.Template.Domain/DomainEvents/OrderDomainEvents.cs +++ b/template/src/ABC.Template.Domain/DomainEvents/OrderDomainEvents.cs @@ -1,3 +1,4 @@ +//#if (UseDemoCode) using ABC.Template.Domain.AggregatesModel.OrderAggregate; namespace ABC.Template.Domain.DomainEvents; @@ -5,3 +6,4 @@ namespace ABC.Template.Domain.DomainEvents; public record OrderCreatedDomainEvent(Order Order) : IDomainEvent; public record OrderPaidDomainEvent(Order Order) : IDomainEvent; +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Domain/DomainEvents/RoleEvents.cs b/template/src/ABC.Template.Domain/DomainEvents/RoleEvents.cs new file mode 100644 index 00000000..0ccb80bd --- /dev/null +++ b/template/src/ABC.Template.Domain/DomainEvents/RoleEvents.cs @@ -0,0 +1,8 @@ +//#if (UseAdmin) +using ABC.Template.Domain.AggregatesModel.RoleAggregate; + +namespace ABC.Template.Domain.DomainEvents.RoleEvents; + +public record RoleInfoChangedDomainEvent(Role Role) : IDomainEvent; +public record RolePermissionChangedDomainEvent(Role Role) : IDomainEvent; +//#endif diff --git a/template/src/ABC.Template.Domain/ErrorCodes.cs b/template/src/ABC.Template.Domain/ErrorCodes.cs new file mode 100644 index 00000000..0d5ea024 --- /dev/null +++ b/template/src/ABC.Template.Domain/ErrorCodes.cs @@ -0,0 +1,116 @@ +namespace ABC.Template.Domain; + +/// +/// 错误码定义 +/// +public sealed class ErrorCodes +{ + private ErrorCodes() + { + + } + +//#if (UseAdmin) + #region 用户相关错误 (100xxx) + + /// + /// 未找到账户 + /// + public const int AccountNotFound = 100001; + + /// + /// 用户名或密码错误 + /// + public const int UserNameOrPasswordError = 100002; + + /// + /// 未找到用户 + /// + public const int UserNotFound = 100003; + + /// + /// 无效的用户身份 + /// + public const int InvalidUserIdentity = 100004; + + /// + /// 无效的用户 + /// + public const int InvalidUser = 100005; + + /// + /// 无效的令牌 + /// + public const int InvalidToken = 100006; + + /// + /// 无效的刷新令牌 + /// + public const int InvalidRefreshToken = 100007; + + #endregion + + #region 角色相关错误 (110xxx) + + /// + /// 未找到角色 + /// + public const int RoleNotFound = 110001; + + /// + /// 不能删除管理员角色 + /// + public const int CannotDeleteAdminRole = 110002; + + /// + /// 角色已经被停用 + /// + public const int RoleAlreadyDeactivated = 110003; + + /// + /// 角色已经是激活状态 + /// + public const int RoleAlreadyActivated = 110004; + + #endregion + + #region 部门相关错误 (120xxx) + + /// + /// 未找到部门 + /// + public const int DeptNotFound = 120001; + + /// + /// 该部门下存在子部门,无法删除 + /// + public const int DeptHasChildrenCannotDelete = 120002; + + /// + /// 部门已经是激活状态 + /// + public const int DeptAlreadyActivated = 120003; + + /// + /// 部门已经被停用 + /// + public const int DeptAlreadyDeactivated = 120004; + + /// + /// 部门已经被删除 + /// + public const int DeptAlreadyDeleted = 120005; + + /// + /// 部门名称不能为空 + /// + public const int DeptNameCannotBeEmpty = 120006; + + /// + /// 子部门不能为空 + /// + public const int ChildDeptCannotBeEmpty = 120007; + + #endregion +//#endif +} diff --git a/template/src/ABC.Template.Infrastructure/ApplicationDbContext.cs b/template/src/ABC.Template.Infrastructure/ApplicationDbContext.cs index 28ff925c..6c15bab6 100644 --- a/template/src/ABC.Template.Infrastructure/ApplicationDbContext.cs +++ b/template/src/ABC.Template.Infrastructure/ApplicationDbContext.cs @@ -1,11 +1,18 @@ -using ABC.Template.Domain.AggregatesModel.OrderAggregate; -using MediatR; +using MediatR; using Microsoft.EntityFrameworkCore; //#if (UseDMDB) using Microsoft.EntityFrameworkCore.Storage; //#endif using NetCorePal.Extensions.DistributedTransactions.CAP.Persistence; +//#if (UseDemoCode) +using ABC.Template.Domain.AggregatesModel.OrderAggregate; using ABC.Template.Domain.AggregatesModel.DeliverAggregate; +//#endif +//#if (UseAdmin) +using ABC.Template.Domain.AggregatesModel.UserAggregate; +using ABC.Template.Domain.AggregatesModel.RoleAggregate; +using ABC.Template.Domain.AggregatesModel.DeptAggregate; +//#endif namespace ABC.Template.Infrastructure; @@ -53,6 +60,17 @@ protected override void ConfigureConventions(ModelConfigurationBuilder configura base.ConfigureConventions(configurationBuilder); } +//#if (UseDemoCode) public DbSet Orders => Set(); public DbSet DeliverRecords => Set(); +//#endif + +//#if (UseAdmin) + public DbSet Users => Set(); + public DbSet Roles => Set(); + public DbSet RolePermissions => Set(); + public DbSet UserRoles => Set(); + public DbSet Depts => Set(); + public DbSet UserDepts => Set(); +//#endif } diff --git a/template/src/ABC.Template.Infrastructure/EntityConfigurations/DeliverRecordConfiguration.cs b/template/src/ABC.Template.Infrastructure/EntityConfigurations/DeliverRecordConfiguration.cs index d5d026f8..71d7267c 100644 --- a/template/src/ABC.Template.Infrastructure/EntityConfigurations/DeliverRecordConfiguration.cs +++ b/template/src/ABC.Template.Infrastructure/EntityConfigurations/DeliverRecordConfiguration.cs @@ -1,4 +1,5 @@ -using ABC.Template.Domain.AggregatesModel.DeliverAggregate; +//#if (UseDemoCode) +using ABC.Template.Domain.AggregatesModel.DeliverAggregate; namespace ABC.Template.Infrastructure.EntityConfigurations { @@ -13,3 +14,4 @@ public void Configure(EntityTypeBuilder builder) } } +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Infrastructure/EntityConfigurations/DeptEntityTypeConfiguration.cs b/template/src/ABC.Template.Infrastructure/EntityConfigurations/DeptEntityTypeConfiguration.cs new file mode 100644 index 00000000..4008830a --- /dev/null +++ b/template/src/ABC.Template.Infrastructure/EntityConfigurations/DeptEntityTypeConfiguration.cs @@ -0,0 +1,49 @@ +//#if (UseAdmin) +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using ABC.Template.Domain.AggregatesModel.DeptAggregate; + +namespace ABC.Template.Infrastructure.EntityConfigurations; + +/// +/// 部门实体类型配置 +/// +internal class DeptEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("dept"); + + builder.HasKey(d => d.Id); + builder.Property(t => t.Id).UseSnowFlakeValueGenerator(); + + builder.Property(d => d.Name) + .IsRequired() + .HasMaxLength(100); + + builder.Property(d => d.Remark) + .HasMaxLength(500); + + builder.Property(d => d.Status) + .IsRequired(); + + builder.Property(d => d.CreatedAt) + .IsRequired(); + + builder.Property(d => d.IsDeleted) + .IsRequired(); + + builder.Property(d => d.DeletedAt); + + builder.Property(d => d.UpdateTime); + + // 索引 + builder.HasIndex(d => d.ParentId); + builder.HasIndex(d => d.Status); + builder.HasIndex(d => d.IsDeleted); + + // 软删除过滤器 + builder.HasQueryFilter(d => !d.IsDeleted); + } +} +//#endif diff --git a/template/src/ABC.Template.Infrastructure/EntityConfigurations/OrderEntityTypeConfiguration.cs b/template/src/ABC.Template.Infrastructure/EntityConfigurations/OrderEntityTypeConfiguration.cs index 18d9d005..054a04cd 100644 --- a/template/src/ABC.Template.Infrastructure/EntityConfigurations/OrderEntityTypeConfiguration.cs +++ b/template/src/ABC.Template.Infrastructure/EntityConfigurations/OrderEntityTypeConfiguration.cs @@ -1,4 +1,5 @@ -using ABC.Template.Domain.AggregatesModel.OrderAggregate; +//#if (UseDemoCode) +using ABC.Template.Domain.AggregatesModel.OrderAggregate; namespace ABC.Template.Infrastructure.EntityConfigurations { @@ -16,3 +17,4 @@ public void Configure(EntityTypeBuilder builder) } } +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Infrastructure/EntityConfigurations/RoleEntityTypeConfiguration.cs b/template/src/ABC.Template.Infrastructure/EntityConfigurations/RoleEntityTypeConfiguration.cs new file mode 100644 index 00000000..def5661e --- /dev/null +++ b/template/src/ABC.Template.Infrastructure/EntityConfigurations/RoleEntityTypeConfiguration.cs @@ -0,0 +1,50 @@ +//#if (UseAdmin) +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using ABC.Template.Domain.AggregatesModel.RoleAggregate; + +namespace ABC.Template.Infrastructure.EntityConfigurations; + +internal class RoleEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("role"); + builder.HasKey(t => t.Id); + builder.Property(t => t.Id).UseGuidVersion7ValueGenerator(); + + builder.Property(b => b.Name).HasMaxLength(50).IsRequired(); + builder.Property(b => b.Description).HasMaxLength(200); + builder.Property(b => b.CreatedAt); + builder.Property(b => b.IsActive); + builder.Property(b => b.IsDeleted); + + builder.HasIndex(b => b.Name).IsUnique(); + + builder.HasMany(r => r.Permissions).WithOne().HasForeignKey(rp => rp.RoleId); + builder.Navigation(e => e.Permissions).AutoInclude(); + + builder.HasQueryFilter(b => !b.IsDeleted); + } +} + +internal class RolePermissionEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("role_permission"); + + builder.HasKey(t => new { t.RoleId, t.PermissionCode }); + + builder.Property(b => b.RoleId); + builder.Property(b => b.PermissionCode).HasMaxLength(100).IsRequired(); + builder.Property(b => b.PermissionName).HasMaxLength(100); + builder.Property(b => b.PermissionDescription).HasMaxLength(200); + + builder.HasOne() + .WithMany(r => r.Permissions) + .HasForeignKey(rp => rp.RoleId) + .OnDelete(DeleteBehavior.Cascade); + } +} +//#endif diff --git a/template/src/ABC.Template.Infrastructure/EntityConfigurations/UserEntityTypeConfiguration.cs b/template/src/ABC.Template.Infrastructure/EntityConfigurations/UserEntityTypeConfiguration.cs new file mode 100644 index 00000000..2da72af0 --- /dev/null +++ b/template/src/ABC.Template.Infrastructure/EntityConfigurations/UserEntityTypeConfiguration.cs @@ -0,0 +1,103 @@ +//#if (UseAdmin) +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using ABC.Template.Domain.AggregatesModel.UserAggregate; + +namespace ABC.Template.Infrastructure.EntityConfigurations; + +internal class UserEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("user"); + builder.HasKey(t => t.Id); + builder.Property(t => t.Id).UseSnowFlakeValueGenerator(); + + builder.Property(b => b.Name).HasMaxLength(50).IsRequired(); + builder.Property(b => b.Email).HasMaxLength(100).IsRequired(); + builder.Property(b => b.PasswordHash).HasMaxLength(255).IsRequired(); + builder.Property(b => b.Phone).HasMaxLength(20); + builder.Property(b => b.RealName).HasMaxLength(50); + builder.Property(b => b.Gender).HasMaxLength(10); + builder.Property(b => b.Age); + builder.Property(b => b.BirthDate); + builder.Property(b => b.IsActive); + builder.Property(b => b.CreatedAt); + builder.Property(b => b.LastLoginTime); + builder.Property(b => b.UpdateTime); + + builder.HasIndex(b => b.Name); + builder.HasIndex(b => b.Email); + + builder.HasMany(au => au.Roles) + .WithOne() + .HasForeignKey(aur => aur.UserId) + .OnDelete(DeleteBehavior.ClientCascade); + builder.Navigation(au => au.Roles).AutoInclude(); + + builder.HasMany(u => u.RefreshTokens) + .WithOne() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + // 配置 User 与 UserDept 的一对一关系 + builder.HasOne(au => au.Dept) + .WithOne() + .HasForeignKey(ud => ud.UserId) + .OnDelete(DeleteBehavior.ClientCascade); + builder.Navigation(au => au.Dept).AutoInclude(); + } +} + +internal class UserDeptEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("user_dept"); + + builder.HasKey(ud => ud.UserId); + + builder.Property(ud => ud.UserId); + builder.Property(ud => ud.DeptId); + builder.Property(ud => ud.DeptName).HasMaxLength(100); + builder.Property(ud => ud.AssignedAt) + .IsRequired(); + + // 索引 + builder.HasIndex(ud => ud.UserId); + builder.HasIndex(ud => ud.DeptId); + } +} + +internal class UserRoleEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("user_role"); + + builder.HasKey(t => new { t.UserId, t.RoleId }); + + builder.Property(b => b.UserId); + builder.Property(b => b.RoleId); + builder.Property(b => b.RoleName).HasMaxLength(50).IsRequired(); + + builder.HasOne() + .WithMany(u => u.Roles) + .HasForeignKey(ur => ur.UserId) + .OnDelete(DeleteBehavior.Cascade); + } +} + +internal class UserRefreshTokenConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("user_refresh_token"); + builder.HasKey(x => x.Id); + builder.Property(x => x.Id).UseSnowFlakeValueGenerator(); + builder.Property(x => x.Token).HasMaxLength(500).IsRequired(); + builder.Property(x => x.CreatedTime).IsRequired(); + builder.Property(x => x.ExpiresTime).IsRequired(); + } +} +//#endif diff --git a/template/src/ABC.Template.Infrastructure/Repositories/DeliverRecordRepository.cs b/template/src/ABC.Template.Infrastructure/Repositories/DeliverRecordRepository.cs index cec24061..574ad7e0 100644 --- a/template/src/ABC.Template.Infrastructure/Repositories/DeliverRecordRepository.cs +++ b/template/src/ABC.Template.Infrastructure/Repositories/DeliverRecordRepository.cs @@ -1,4 +1,5 @@ -using ABC.Template.Domain.AggregatesModel.DeliverAggregate; +//#if (UseDemoCode) +using ABC.Template.Domain.AggregatesModel.DeliverAggregate; namespace ABC.Template.Infrastructure.Repositories; @@ -9,4 +10,5 @@ public interface IDeliverRecordRepository : IRepository(context), IDeliverRecordRepository { } +//#endif diff --git a/template/src/ABC.Template.Infrastructure/Repositories/DeptRepository.cs b/template/src/ABC.Template.Infrastructure/Repositories/DeptRepository.cs new file mode 100644 index 00000000..39578d5a --- /dev/null +++ b/template/src/ABC.Template.Infrastructure/Repositories/DeptRepository.cs @@ -0,0 +1,15 @@ +//#if (UseAdmin) +using ABC.Template.Domain.AggregatesModel.DeptAggregate; + +namespace ABC.Template.Infrastructure.Repositories; + +/// +/// 部门仓储接口 +/// +public interface IDeptRepository : IRepository { } + +/// +/// 部门仓储实现 +/// +public class DeptRepository(ApplicationDbContext context) : RepositoryBase(context), IDeptRepository { } +//#endif diff --git a/template/src/ABC.Template.Infrastructure/Repositories/OrderRepository.cs b/template/src/ABC.Template.Infrastructure/Repositories/OrderRepository.cs index 4d47a938..8f3f82db 100644 --- a/template/src/ABC.Template.Infrastructure/Repositories/OrderRepository.cs +++ b/template/src/ABC.Template.Infrastructure/Repositories/OrderRepository.cs @@ -1,4 +1,5 @@ -using NetCorePal.Extensions.Repository.EntityFrameworkCore; +//#if (UseDemoCode) +using NetCorePal.Extensions.Repository.EntityFrameworkCore; using ABC.Template.Domain.AggregatesModel.OrderAggregate; using NetCorePal.Extensions.Repository; @@ -11,4 +12,5 @@ public interface IOrderRepository : IRepository public class OrderRepository(ApplicationDbContext context) : RepositoryBase(context), IOrderRepository { } +//#endif diff --git a/template/src/ABC.Template.Infrastructure/Repositories/RoleRepository.cs b/template/src/ABC.Template.Infrastructure/Repositories/RoleRepository.cs new file mode 100644 index 00000000..8ee889d3 --- /dev/null +++ b/template/src/ABC.Template.Infrastructure/Repositories/RoleRepository.cs @@ -0,0 +1,9 @@ +//#if (UseAdmin) +using ABC.Template.Domain.AggregatesModel.RoleAggregate; + +namespace ABC.Template.Infrastructure.Repositories; + +public interface IRoleRepository : IRepository { } + +public class RoleRepository(ApplicationDbContext context) : RepositoryBase(context), IRoleRepository { } +//#endif diff --git a/template/src/ABC.Template.Infrastructure/Repositories/UserRepository.cs b/template/src/ABC.Template.Infrastructure/Repositories/UserRepository.cs new file mode 100644 index 00000000..4482ad94 --- /dev/null +++ b/template/src/ABC.Template.Infrastructure/Repositories/UserRepository.cs @@ -0,0 +1,9 @@ +//#if (UseAdmin) +using ABC.Template.Domain.AggregatesModel.UserAggregate; + +namespace ABC.Template.Infrastructure.Repositories; + +public interface IUserRepository : IRepository { } + +public class UserRepository(ApplicationDbContext context) : RepositoryBase(context), IUserRepository { } +//#endif diff --git a/template/src/ABC.Template.Web/AppPermissions/AppPermission.cs b/template/src/ABC.Template.Web/AppPermissions/AppPermission.cs new file mode 100644 index 00000000..376ce353 --- /dev/null +++ b/template/src/ABC.Template.Web/AppPermissions/AppPermission.cs @@ -0,0 +1,74 @@ +//#if (UseAdmin) +using System.Collections.Immutable; + +namespace ABC.Template.Web.AppPermissions; + +/// +/// 表示一个权限对象,包含权限的基本信息及子权限 +/// +public sealed class AppPermission +{ + /// + /// 权限的唯一名称(代码) + /// + public string Code { get; } + + /// + /// 权限的显示名称 + /// + public string DisplayName { get; } + + /// + /// 当前权限的所有子权限 + /// 子权限是只读的 + /// + public IReadOnlyList Children => _children.ToImmutableList(); + + private readonly List _children; + + /// + /// 指示当前权限是否启用 + /// 默认情况下权限是启用的 + /// 禁用的权限无法被授予,但仍然可以检查其值(始终为 false) + /// 禁用权限可以用来隐藏相关的应用功能 + /// 默认值:true(启用) + /// + public bool IsEnabled { get; } + + /// + /// 创建一个新的权限对象。 + /// + /// 权限的唯一代码。 + /// 权限的显示名称。 + /// 是否启用此权限,默认为 true。 + internal AppPermission( + string code, + string displayName, + bool isEnabled = true) + { + ArgumentException.ThrowIfNullOrWhiteSpace(code); + ArgumentException.ThrowIfNullOrWhiteSpace(displayName); + + Code = code; + DisplayName = displayName; + IsEnabled = isEnabled; + _children = []; + } + + /// + /// 向当前权限添加一个子权限。 + /// + /// 子权限的唯一代码。 + /// 子权限的显示名称。 + /// 子权限是否启用,默认为 true。 + /// 返回创建的子权限对象。 + public AppPermission AddChild(string code, string displayName, bool isEnabled = true) + { + var child = new AppPermission(code, displayName, isEnabled); + + _children.Add(child); + + return child; + } +} +//#endif diff --git a/template/src/ABC.Template.Web/AppPermissions/AppPermissionGroup.cs b/template/src/ABC.Template.Web/AppPermissions/AppPermissionGroup.cs new file mode 100644 index 00000000..6deb07f9 --- /dev/null +++ b/template/src/ABC.Template.Web/AppPermissions/AppPermissionGroup.cs @@ -0,0 +1,47 @@ +//#if (UseAdmin) +using System.Collections.Immutable; + +namespace ABC.Template.Web.AppPermissions; + +/// +/// 表示一个权限组,包含该组的所有权限以及相关操作。 +/// +public sealed class AppPermissionGroup +{ + /// + /// 权限组的唯一名称。 + /// + public string Name { get; } + + /// + /// 权限组内的所有权限,权限是只读的。 + /// + public IReadOnlyList Permissions => _permissions.ToImmutableList(); + + private readonly List _permissions = []; + + /// + /// 创建一个新的权限组。 + /// + /// 权限组的名称。 + internal AppPermissionGroup(string name) + { + // 初始化权限组的名称和权限集合 + Name = name; + } + + /// + /// 向权限组中添加一个权限。 + /// + /// 权限的唯一代码。 + /// 权限的名称。 + /// 是否启用该权限,默认为 true。 + /// 返回创建的权限对象。 + public AppPermission AddPermission(string code, string name, bool isEnabled = true) + { + var permission = new AppPermission(code, name, isEnabled); + _permissions.Add(permission); // 将权限添加到权限组中 + return permission; + } +} +//#endif diff --git a/template/src/ABC.Template.Web/AppPermissions/PermissionCodes.cs b/template/src/ABC.Template.Web/AppPermissions/PermissionCodes.cs new file mode 100644 index 00000000..d6c6b0f3 --- /dev/null +++ b/template/src/ABC.Template.Web/AppPermissions/PermissionCodes.cs @@ -0,0 +1,40 @@ +//#if (UseAdmin) +namespace ABC.Template.Web.AppPermissions; + +/// +/// 权限常量定义 +/// +public static class PermissionCodes +{ + #region 角色管理权限 + public const string RoleManagement = nameof(RoleManagement); + public const string RoleCreate = nameof(RoleCreate); + public const string RoleEdit = nameof(RoleEdit); + public const string RoleDelete = nameof(RoleDelete); + public const string RoleView = nameof(RoleView); + public const string RoleUpdatePermissions = nameof(RoleUpdatePermissions); + #endregion + + #region 用户管理权限 + public const string UserManagement = nameof(UserManagement); + public const string UserCreate = nameof(UserCreate); + public const string UserEdit = nameof(UserEdit); + public const string UserDelete = nameof(UserDelete); + public const string UserView = nameof(UserView); + public const string UserRoleAssign = nameof(UserRoleAssign); + public const string UserResetPassword = nameof(UserResetPassword); + #endregion + + #region 部门管理权限 + public const string DeptManagement = nameof(DeptManagement); + public const string DeptCreate = nameof(DeptCreate); + public const string DeptEdit = nameof(DeptEdit); + public const string DeptDelete = nameof(DeptDelete); + public const string DeptView = nameof(DeptView); + #endregion + + #region 所有接口访问权限 + public const string AllApiAccess = nameof(AllApiAccess); + #endregion +} +//#endif diff --git a/template/src/ABC.Template.Web/AppPermissions/PermissionDefinitionContext.cs b/template/src/ABC.Template.Web/AppPermissions/PermissionDefinitionContext.cs new file mode 100644 index 00000000..eb2bc778 --- /dev/null +++ b/template/src/ABC.Template.Web/AppPermissions/PermissionDefinitionContext.cs @@ -0,0 +1,70 @@ +//#if (UseAdmin) +using System.Collections.Immutable; + +namespace ABC.Template.Web.AppPermissions; + +/// +/// 管理权限定义的上下文类,负责初始化和提供权限组及其权限项。 +/// +public static class PermissionDefinitionContext +{ + // 存储权限组的字典,键为权限组名称,值为权限组对象 + private static Dictionary Groups { get; } = new(); + + // 静态构造函数,在类初始化时创建默认的权限组和权限项 + static PermissionDefinitionContext() + { + var systemAccess = AddGroup("SystemAccess"); + + // 用户管理权限 + var adminUserManagement = systemAccess.AddPermission(PermissionCodes.UserManagement, "用户管理"); + adminUserManagement.AddChild(PermissionCodes.UserCreate, "创建用户"); + adminUserManagement.AddChild(PermissionCodes.UserEdit, "编辑用户"); + adminUserManagement.AddChild(PermissionCodes.UserDelete, "删除用户"); + adminUserManagement.AddChild(PermissionCodes.UserView, "查看用户"); + adminUserManagement.AddChild(PermissionCodes.UserRoleAssign, "分配用户角色"); + adminUserManagement.AddChild(PermissionCodes.UserResetPassword, "重置用户密码"); + + // 角色管理权限 + var roleManagement = systemAccess.AddPermission(PermissionCodes.RoleManagement, "角色管理"); + roleManagement.AddChild(PermissionCodes.RoleCreate, "创建角色"); + roleManagement.AddChild(PermissionCodes.RoleEdit, "编辑角色"); + roleManagement.AddChild(PermissionCodes.RoleDelete, "删除角色"); + roleManagement.AddChild(PermissionCodes.RoleView, "查看角色"); + roleManagement.AddChild(PermissionCodes.RoleUpdatePermissions, "更新角色权限"); + + // 部门管理权限 + var deptManagement = systemAccess.AddPermission(PermissionCodes.DeptManagement, "部门管理"); + deptManagement.AddChild(PermissionCodes.DeptCreate, "创建部门"); + deptManagement.AddChild(PermissionCodes.DeptEdit, "编辑部门"); + deptManagement.AddChild(PermissionCodes.DeptDelete, "删除部门"); + deptManagement.AddChild(PermissionCodes.DeptView, "查看部门"); + + // 所有接口访问权限 + var allApiAccess = systemAccess.AddPermission(PermissionCodes.AllApiAccess, "所有接口访问权限"); + } + + /// + /// 添加一个新的权限组,如果权限组名称已存在则抛出异常。 + /// + /// 权限组名称 + /// 返回创建的权限组 + /// 如果权限组名称已经存在,则抛出异常 + private static AppPermissionGroup AddGroup(string name) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + + if (Groups.ContainsKey(name)) + { + throw new ArgumentException($"There is already an existing permission group with name: {name}"); + } + + return Groups[name] = new AppPermissionGroup(name); + } + + /// + /// 获取所有的权限组。 + /// + public static IReadOnlyList PermissionGroups => Groups.Values.ToImmutableList(); +} +//#endif diff --git a/template/src/ABC.Template.Web/Application/Commands/Delivers/DeliverGoodsCommand.cs b/template/src/ABC.Template.Web/Application/Commands/Delivers/DeliverGoodsCommand.cs index faf96ad4..40971cf7 100644 --- a/template/src/ABC.Template.Web/Application/Commands/Delivers/DeliverGoodsCommand.cs +++ b/template/src/ABC.Template.Web/Application/Commands/Delivers/DeliverGoodsCommand.cs @@ -1,4 +1,5 @@ -using ABC.Template.Domain.AggregatesModel.DeliverAggregate; +//#if (UseDemoCode) +using ABC.Template.Domain.AggregatesModel.DeliverAggregate; using ABC.Template.Domain.AggregatesModel.OrderAggregate; using ABC.Template.Infrastructure.Repositories; using NetCorePal.Extensions.Primitives; @@ -16,4 +17,5 @@ public Task Handle(DeliverGoodsCommand request, CancellationTok deliverRecordRepository.Add(record); return Task.FromResult(record.Id); } -} \ No newline at end of file +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/DeptCommands/CreateDeptCommand.cs b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/DeptCommands/CreateDeptCommand.cs new file mode 100644 index 00000000..f56c82d8 --- /dev/null +++ b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/DeptCommands/CreateDeptCommand.cs @@ -0,0 +1,40 @@ +//#if (UseAdmin) +using FluentValidation; +using ABC.Template.Domain.AggregatesModel.DeptAggregate; +using ABC.Template.Infrastructure.Repositories; +using ABC.Template.Web.Application.Queries; + +namespace ABC.Template.Web.Application.Commands.Identity.Admin.DeptCommands; + +/// +/// 创建部门命令 +/// +public record CreateDeptCommand(string Name, string Remark, DeptId? ParentId, int Status) : ICommand; + +public class CreateDeptCommandValidator : AbstractValidator +{ + public CreateDeptCommandValidator(DeptQuery deptQuery) + { + RuleFor(d => d.Name).NotEmpty().WithMessage("部门名称不能为空"); + RuleFor(d => d.Name).MustAsync(async (n, ct) => !await deptQuery.DoesDeptExist(n, ct)) + .WithMessage(d => $"该部门已存在,Name={d.Name}"); + RuleFor(d => d.Status).InclusiveBetween(0, 1).WithMessage("状态值必须为0或1"); + } +} + +/// +/// 创建部门命令处理器 +/// +public class CreateDeptCommandHandler(IDeptRepository deptRepository) : ICommandHandler +{ + public async Task Handle(CreateDeptCommand request, CancellationToken cancellationToken) + { + var parentId = request.ParentId ?? new DeptId(0); + var dept = new Dept(request.Name, request.Remark, parentId, request.Status); + + await deptRepository.AddAsync(dept, cancellationToken); + + return dept.Id; + } +} +//#endif diff --git a/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/DeptCommands/DeleteDeptCommand.cs b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/DeptCommands/DeleteDeptCommand.cs new file mode 100644 index 00000000..35d0de23 --- /dev/null +++ b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/DeptCommands/DeleteDeptCommand.cs @@ -0,0 +1,37 @@ +//#if (UseAdmin) +using Microsoft.EntityFrameworkCore; +using ABC.Template.Domain.AggregatesModel.DeptAggregate; +using ABC.Template.Infrastructure; +using ABC.Template.Infrastructure.Repositories; +using ABC.Template.Domain; + +namespace ABC.Template.Web.Application.Commands.Identity.Admin.DeptCommands; + +/// +/// 删除部门命令 +/// +public record DeleteDeptCommand(DeptId Id) : ICommand; + +/// +/// 删除部门命令处理器 +/// +public class DeleteDeptCommandHandler(IDeptRepository deptRepository, ApplicationDbContext dbContext) : ICommandHandler +{ + public async Task Handle(DeleteDeptCommand request, CancellationToken cancellationToken) + { + var dept = await deptRepository.GetAsync(request.Id, cancellationToken) + ?? throw new KnownException($"未找到部门,Id = {request.Id}", ErrorCodes.DeptNotFound); + + // 检查是否有子部门 + var hasChildren = await dbContext.Depts + .AnyAsync(d => d.ParentId == request.Id && !d.IsDeleted, cancellationToken); + + if (hasChildren) + { + throw new KnownException("该部门下存在子部门,无法删除", ErrorCodes.DeptHasChildrenCannotDelete); + } + + dept.SoftDelete(); + } +} +//#endif diff --git a/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/DeptCommands/UpdateDeptCommand.cs b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/DeptCommands/UpdateDeptCommand.cs new file mode 100644 index 00000000..fe0253c4 --- /dev/null +++ b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/DeptCommands/UpdateDeptCommand.cs @@ -0,0 +1,36 @@ +//#if (UseAdmin) +using FluentValidation; +using ABC.Template.Domain.AggregatesModel.DeptAggregate; +using ABC.Template.Infrastructure.Repositories; +using ABC.Template.Domain; + +namespace ABC.Template.Web.Application.Commands.Identity.Admin.DeptCommands; + +/// +/// 更新部门命令 +/// +public record UpdateDeptCommand(DeptId Id, string Name, string Remark, DeptId ParentId, int Status) : ICommand; + +public class UpdateDeptCommandValidator : AbstractValidator +{ + public UpdateDeptCommandValidator() + { + RuleFor(d => d.Name).NotEmpty().WithMessage("部门名称不能为空"); + RuleFor(d => d.Status).InclusiveBetween(0, 1).WithMessage("状态值必须为0或1"); + } +} + +/// +/// 更新部门命令处理器 +/// +public class UpdateDeptCommandHandler(IDeptRepository deptRepository) : ICommandHandler +{ + public async Task Handle(UpdateDeptCommand request, CancellationToken cancellationToken) + { + var dept = await deptRepository.GetAsync(request.Id, cancellationToken) + ?? throw new KnownException($"未找到部门,Id = {request.Id}", ErrorCodes.DeptNotFound); + + dept.UpdateInfo(request.Name, request.Remark, request.ParentId, request.Status); + } +} +//#endif diff --git a/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/RoleCommands/ActivateRoleCommand.cs b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/RoleCommands/ActivateRoleCommand.cs new file mode 100644 index 00000000..3d9493b4 --- /dev/null +++ b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/RoleCommands/ActivateRoleCommand.cs @@ -0,0 +1,38 @@ +//#if (UseAdmin) +using FluentValidation; +using ABC.Template.Domain.AggregatesModel.RoleAggregate; +using ABC.Template.Infrastructure.Repositories; +using ABC.Template.Domain; + +namespace ABC.Template.Web.Application.Commands.Identity.Admin.RoleCommands; + +/// +/// 激活角色命令 +/// +/// 角色ID +public record ActivateRoleCommand(RoleId RoleId) : ICommand; + +/// +/// 激活角色命令验证器 +/// +public class ActivateRoleCommandValidator : AbstractValidator +{ + public ActivateRoleCommandValidator() + { + RuleFor(x => x.RoleId).NotEmpty(); + } +} + +/// +/// 激活角色命令处理器 +/// +public class ActivateRoleCommandHandler(IRoleRepository roleRepository) : ICommandHandler +{ + public async Task Handle(ActivateRoleCommand request, CancellationToken cancellationToken) + { + var role = await roleRepository.GetAsync(request.RoleId, cancellationToken) ?? + throw new KnownException($"未找到角色,RoleId = {request.RoleId}", ErrorCodes.RoleNotFound); + role.Activate(); + } +} +//#endif diff --git a/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/RoleCommands/CreateRoleCommand.cs b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/RoleCommands/CreateRoleCommand.cs new file mode 100644 index 00000000..7d24628e --- /dev/null +++ b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/RoleCommands/CreateRoleCommand.cs @@ -0,0 +1,35 @@ +//#if (UseAdmin) +using FluentValidation; +using ABC.Template.Domain.AggregatesModel.RoleAggregate; +using ABC.Template.Infrastructure.Repositories; +using ABC.Template.Web.Application.Queries; + +namespace ABC.Template.Web.Application.Commands.Identity.Admin.RoleCommands; + +public record CreateRoleCommand(string Name, string Description, IEnumerable PermissionCodes) : ICommand; + +public class CreateRoleCommandValidator : AbstractValidator +{ + public CreateRoleCommandValidator(RoleQuery roleQuery) + { + RuleFor(r => r.Name).NotEmpty().WithMessage("角色名称不能为空"); + RuleFor(r => r.Description).MaximumLength(200).WithMessage("角色描述长度不能超过200个字符"); + RuleFor(r => r.Name).MustAsync(async (n, ct) => !await roleQuery.DoesRoleExist(n, ct)) + .WithMessage(r => $"该角色已存在,Name={r.Name}"); + } +} + +public class CreateRoleCommandHandler(IRoleRepository roleRepository) : ICommandHandler +{ + public async Task Handle(CreateRoleCommand request, CancellationToken cancellationToken) + { + var permissions = request.PermissionCodes.Select(perm => new RolePermission(perm)); + + var role = new Role(request.Name, request.Description, permissions); + + await roleRepository.AddAsync(role, cancellationToken); + + return role.Id; + } +} +//#endif diff --git a/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/RoleCommands/DeactivateRoleCommand.cs b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/RoleCommands/DeactivateRoleCommand.cs new file mode 100644 index 00000000..7057b164 --- /dev/null +++ b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/RoleCommands/DeactivateRoleCommand.cs @@ -0,0 +1,38 @@ +//#if (UseAdmin) +using FluentValidation; +using ABC.Template.Domain.AggregatesModel.RoleAggregate; +using ABC.Template.Infrastructure.Repositories; +using ABC.Template.Domain; + +namespace ABC.Template.Web.Application.Commands.Identity.Admin.RoleCommands; + +/// +/// 停用角色命令 +/// +/// 角色ID +public record DeactivateRoleCommand(RoleId RoleId) : ICommand; + +/// +/// 停用角色命令验证器 +/// +public class DeactivateRoleCommandValidator : AbstractValidator +{ + public DeactivateRoleCommandValidator() + { + RuleFor(x => x.RoleId).NotEmpty(); + } +} + +/// +/// 停用角色命令处理器 +/// +public class DeactivateRoleCommandHandler(IRoleRepository roleRepository) : ICommandHandler +{ + public async Task Handle(DeactivateRoleCommand request, CancellationToken cancellationToken) + { + var role = await roleRepository.GetAsync(request.RoleId, cancellationToken) ?? + throw new KnownException($"未找到角色,RoleId = {request.RoleId}", ErrorCodes.RoleNotFound); + role.Deactivate(); + } +} +//#endif diff --git a/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/RoleCommands/DeleteRoleCommand.cs b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/RoleCommands/DeleteRoleCommand.cs new file mode 100644 index 00000000..e1534445 --- /dev/null +++ b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/RoleCommands/DeleteRoleCommand.cs @@ -0,0 +1,47 @@ +//#if (UseAdmin) +using FluentValidation; +using ABC.Template.Domain.AggregatesModel.RoleAggregate; +using ABC.Template.Infrastructure.Repositories; +using ABC.Template.Domain; + +namespace ABC.Template.Web.Application.Commands.Identity.Admin.RoleCommands; + +/// +/// 删除角色命令 +/// +/// 角色ID +public record DeleteRoleCommand(RoleId RoleId) : ICommand; + +/// +/// 删除角色命令验证器 +/// +public class DeleteRoleCommandValidator : AbstractValidator +{ + public DeleteRoleCommandValidator() + { + RuleFor(x => x.RoleId).NotEmpty().WithMessage("角色ID不能为空"); + } +} + +/// +/// 删除角色命令处理器 +/// +public class DeleteRoleCommandHandler(IRoleRepository roleRepository) : ICommandHandler +{ + public async Task Handle(DeleteRoleCommand request, CancellationToken cancellationToken) + { + var role = await roleRepository.GetAsync(request.RoleId, cancellationToken); + if (role == null) + { + throw new KnownException("角色不存在", ErrorCodes.RoleNotFound); + } + + // 检查是否为管理员用户,防止删除管理员 + if (role.Name.ToLower() == "admin") + { + throw new KnownException("不能删除管理员角色", ErrorCodes.CannotDeleteAdminRole); + } + role.SoftDelete(); + } +} +//#endif diff --git a/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/RoleCommands/UpdateRoleInfoCommand.cs b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/RoleCommands/UpdateRoleInfoCommand.cs new file mode 100644 index 00000000..ed2e29d9 --- /dev/null +++ b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/RoleCommands/UpdateRoleInfoCommand.cs @@ -0,0 +1,47 @@ +//#if (UseAdmin) +using FluentValidation; +using ABC.Template.Domain.AggregatesModel.RoleAggregate; +using ABC.Template.Infrastructure.Repositories; +using ABC.Template.Web.Application.Queries; +using ABC.Template.Domain; + +namespace ABC.Template.Web.Application.Commands.Identity.Admin.RoleCommands; + +/// +/// 更新角色信息命令 +/// +/// 角色ID +/// 角色名称 +/// 角色描述 +/// 权限代码列表 +public record UpdateRoleInfoCommand(RoleId RoleId, string Name, string Description, IEnumerable PermissionCodes) : ICommand; + +/// +/// 更新角色信息命令验证器 +/// +public class UpdateRoleInfoCommandValidator : AbstractValidator +{ + public UpdateRoleInfoCommandValidator(RoleQuery roleQuery) + { + RuleFor(x => x.RoleId).NotEmpty(); + RuleFor(x => x.Name).NotEmpty(); + } +} + +/// +/// 更新角色信息命令处理器 +/// +public class UpdateRoleInfoCommandHandler(IRoleRepository roleRepository) : ICommandHandler +{ + public async Task Handle(UpdateRoleInfoCommand request, CancellationToken cancellationToken) + { + var role = await roleRepository.GetAsync(request.RoleId, cancellationToken) ?? + throw new KnownException($"未找到角色,RoleId = {request.RoleId}", ErrorCodes.RoleNotFound); + role.UpdateRoleInfo(request.Name, request.Description); + + // 更新角色权限 + var permissions = request.PermissionCodes.Select(perm => new RolePermission(perm)); + role.UpdateRolePermissions(permissions); + } +} +//#endif diff --git a/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/CreateUserCommand.cs b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/CreateUserCommand.cs new file mode 100644 index 00000000..78ab9316 --- /dev/null +++ b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/CreateUserCommand.cs @@ -0,0 +1,55 @@ +//#if (UseAdmin) +using FluentValidation; +using ABC.Template.Domain.AggregatesModel.UserAggregate; +using ABC.Template.Domain.AggregatesModel.DeptAggregate; +using ABC.Template.Infrastructure.Repositories; +using ABC.Template.Web.Application.Queries; +using Serilog; + +namespace ABC.Template.Web.Application.Commands.Identity.Admin.UserCommands; + +/// +/// 创建用户命令 +/// +public record CreateUserCommand(string Name, string Email, string Password, string Phone, string RealName, int Status, string Gender, DateTimeOffset BirthDate, DeptId? DeptId, string? DeptName, IEnumerable RolesToBeAssigned) : ICommand; + +public class CreateUserCommandValidator : AbstractValidator +{ + public CreateUserCommandValidator(UserQuery userQuery) + { + RuleFor(u => u.Name).NotEmpty().WithMessage("用户名不能为空"); + RuleFor(u => u.Password).NotEmpty().WithMessage("密码不能为空"); + RuleFor(u => u.Name).MustAsync(async (n, ct) => !await userQuery.DoesUserExist(n, ct)) + .WithMessage(u => $"该用户已存在,Name={u.Name}"); + } +} + +public class CreateUserCommandHandler(IUserRepository userRepository) : ICommandHandler +{ + public async Task Handle(CreateUserCommand request, CancellationToken cancellationToken) + { + Log.Information("开始创建用户: {UserName}, Email: {Email}", request.Name, request.Email); + + var passwordHash = Utils.PasswordHasher.HashPassword(request.Password); + + var roles = request.RolesToBeAssigned + .Select(r => new UserRole(r.RoleId, r.RoleName)) + .ToList(); + + var user = new User(request.Name, request.Phone, passwordHash, roles, request.RealName, request.Status, request.Email, request.Gender, request.BirthDate); + if (request.DeptId != null && !string.IsNullOrEmpty(request.DeptName)) + { + var dept = new UserDept(user.Id, request.DeptId, request.DeptName); + user.AssignDept(dept); + Log.Debug("为用户分配部门: UserId={UserId}, DeptId={DeptId}, DeptName={DeptName}", user.Id, request.DeptId, request.DeptName); + } + + await userRepository.AddAsync(user, cancellationToken); + + Log.Information("用户创建成功: UserId={UserId}, UserName={UserName}, Email={Email}, RoleCount={RoleCount}", + user.Id, request.Name, request.Email, roles.Count); + + return user.Id; + } +} +//#endif diff --git a/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/DeleteUserCommand.cs b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/DeleteUserCommand.cs new file mode 100644 index 00000000..e3c7f483 --- /dev/null +++ b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/DeleteUserCommand.cs @@ -0,0 +1,39 @@ +//#if (UseAdmin) +using FluentValidation; +using ABC.Template.Domain.AggregatesModel.UserAggregate; +using ABC.Template.Infrastructure.Repositories; +using ABC.Template.Domain; + +namespace ABC.Template.Web.Application.Commands.Identity.Admin.UserCommands; + +/// +/// 删除用户命令 +/// +/// 用户ID +public record DeleteUserCommand(UserId UserId) : ICommand; + +/// +/// 删除用户命令验证器 +/// +public class DeleteUserCommandValidator : AbstractValidator +{ + public DeleteUserCommandValidator() + { + RuleFor(x => x.UserId).NotEmpty().WithMessage("用户ID不能为空"); + } +} + +/// +/// 删除用户命令处理器 +/// +public class DeleteUserCommandHandler(IUserRepository userRepository) : ICommandHandler +{ + public async Task Handle(DeleteUserCommand request, CancellationToken cancellationToken) + { + var user = await userRepository.GetAsync(request.UserId, cancellationToken) ?? + throw new KnownException($"未找到用户,UserId = {request.UserId}", ErrorCodes.UserNotFound); + + user.SoftDelete(); + } +} +//#endif diff --git a/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/PasswordResetCommand.cs b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/PasswordResetCommand.cs new file mode 100644 index 00000000..93ae9e9f --- /dev/null +++ b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/PasswordResetCommand.cs @@ -0,0 +1,40 @@ +//#if (UseAdmin) +using FluentValidation; +using ABC.Template.Domain.AggregatesModel.UserAggregate; +using ABC.Template.Infrastructure.Repositories; +using ABC.Template.Web.Application.Queries; +using ABC.Template.Domain; + +namespace ABC.Template.Web.Application.Commands.Identity.Admin.UserCommands; + +/// +/// 密码重置命令 +/// +public record PasswordResetCommand(UserId UserId, string Password) : ICommand; + +/// +/// 密码重置命令验证器 +/// +public class PasswordResetCommandValidator : AbstractValidator +{ + public PasswordResetCommandValidator(UserQuery userQuery) + { + RuleFor(u => u.UserId).NotEmpty().WithMessage("用户ID不能为空"); + RuleFor(u => u.Password).NotEmpty().WithMessage("密码不能为空"); + } +} + +/// +/// 密码重置命令处理器 +/// +public class PasswordResetCommandHandler(IUserRepository userRepository) : ICommandHandler +{ + public async Task Handle(PasswordResetCommand request, CancellationToken cancellationToken) + { + var user = await userRepository.GetAsync(request.UserId, cancellationToken) + ?? throw new KnownException($"用户不存在,UserId={request.UserId}", ErrorCodes.UserNotFound); + user.PasswordReset(request.Password); + return user.Id; + } +} +//#endif diff --git a/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/RevokeUserRefreshTokensCommand.cs b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/RevokeUserRefreshTokensCommand.cs new file mode 100644 index 00000000..65f7e3c8 --- /dev/null +++ b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/RevokeUserRefreshTokensCommand.cs @@ -0,0 +1,23 @@ +//#if (UseAdmin) +using ABC.Template.Domain.AggregatesModel.UserAggregate; +using ABC.Template.Infrastructure.Repositories; +using ABC.Template.Domain; + +namespace ABC.Template.Web.Application.Commands.Identity.Admin.UserCommands; + +/// +/// 撤销用户所有刷新令牌的命令(用于退出登录) +/// +public record RevokeUserRefreshTokensCommand(UserId UserId) : ICommand; + +public class RevokeUserRefreshTokensCommandHandler(IUserRepository userRepository) : ICommandHandler +{ + public async Task Handle(RevokeUserRefreshTokensCommand request, CancellationToken cancellationToken) + { + var user = await userRepository.GetAsync(request.UserId, cancellationToken) + ?? throw new KnownException($"未找到用户,UserId = {request.UserId}", ErrorCodes.UserNotFound); + + user.RevokeAllRefreshTokens(); + } +} +//#endif diff --git a/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/UpdateUserCommand.cs b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/UpdateUserCommand.cs new file mode 100644 index 00000000..5cba3aaa --- /dev/null +++ b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/UpdateUserCommand.cs @@ -0,0 +1,55 @@ +//#if (UseAdmin) +using FluentValidation; +using ABC.Template.Domain.AggregatesModel.UserAggregate; +using ABC.Template.Domain.AggregatesModel.DeptAggregate; +using ABC.Template.Infrastructure.Repositories; +using ABC.Template.Domain; + +namespace ABC.Template.Web.Application.Commands.Identity.Admin.UserCommands; + +/// +/// 更新用户命令 +/// +public record UpdateUserCommand(UserId UserId, string Name, string Email, string Phone, string RealName, int Status, string Gender, int Age, DateTimeOffset BirthDate, DeptId DeptId, string DeptName, string PasswordHash) : ICommand; + +/// +/// 更新用户命令验证器 +/// +public class UpdateUserCommandValidator : AbstractValidator +{ + public UpdateUserCommandValidator() + { + RuleFor(x => x.UserId).NotEmpty().WithMessage("用户ID不能为空"); + RuleFor(x => x.Name).NotEmpty().WithMessage("用户名不能为空"); + } +} + +/// +/// 更新用户命令处理器 +/// +public class UpdateUserCommandHandler(IUserRepository userRepository) : ICommandHandler +{ + public async Task Handle(UpdateUserCommand request, CancellationToken cancellationToken) + { + var user = await userRepository.GetAsync(request.UserId, cancellationToken) ?? + throw new KnownException($"未找到用户,UserId = {request.UserId}", ErrorCodes.UserNotFound); + + user.UpdateUserInfo(request.Name, request.Phone, request.RealName, request.Status, request.Email, request.Gender, request.BirthDate); + + // 如果提供了新密码,则更新密码 + if (!string.IsNullOrEmpty(request.PasswordHash)) + { + user.UpdatePassword(request.PasswordHash); + } + + // 分配部门 + if (request.DeptId != new DeptId(0) && !string.IsNullOrEmpty(request.DeptName)) + { + var dept = new UserDept(user.Id, request.DeptId, request.DeptName); + user.AssignDept(dept); + } + + return user.Id; + } +} +//#endif diff --git a/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/UpdateUserDeptNameCommand.cs b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/UpdateUserDeptNameCommand.cs new file mode 100644 index 00000000..d91d1a90 --- /dev/null +++ b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/UpdateUserDeptNameCommand.cs @@ -0,0 +1,41 @@ +//#if (UseAdmin) +using FluentValidation; +using ABC.Template.Domain.AggregatesModel.UserAggregate; +using ABC.Template.Infrastructure.Repositories; +using ABC.Template.Domain; + +namespace ABC.Template.Web.Application.Commands.Identity.Admin.UserCommands; + +/// +/// 更新用户部门名称命令 +/// +public record UpdateUserDeptNameCommand(UserId UserId, string DeptName) : ICommand; + +/// +/// 更新用户部门名称命令验证器 +/// +public class UpdateUserDeptNameCommandValidator : AbstractValidator +{ + public UpdateUserDeptNameCommandValidator() + { + RuleFor(x => x.UserId).NotEmpty().WithMessage("用户ID不能为空"); + RuleFor(x => x.DeptName).NotEmpty().WithMessage("部门名称不能为空"); + } +} + +/// +/// 更新用户部门名称命令处理器 +/// +public class UpdateUserDeptNameCommandHandler(IUserRepository userRepository) : ICommandHandler +{ + public async Task Handle(UpdateUserDeptNameCommand request, CancellationToken cancellationToken) + { + var user = await userRepository.GetAsync(request.UserId, cancellationToken) ?? + throw new KnownException($"未找到用户,UserId = {request.UserId}", ErrorCodes.UserNotFound); + + user.UpdateDeptName(request.DeptName); + + return user.Id; + } +} +//#endif diff --git a/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/UpdateUserLoginTimeCommand.cs b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/UpdateUserLoginTimeCommand.cs new file mode 100644 index 00000000..005703cf --- /dev/null +++ b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/UpdateUserLoginTimeCommand.cs @@ -0,0 +1,21 @@ +//#if (UseAdmin) +using ABC.Template.Domain.AggregatesModel.UserAggregate; +using ABC.Template.Infrastructure.Repositories; +using ABC.Template.Domain; + +namespace ABC.Template.Web.Application.Commands.Identity.Admin.UserCommands; + +public record UpdateUserLoginTimeCommand(UserId UserId, DateTimeOffset LoginTime, string RefreshToken) : ICommand; + +public class UpdateUserLoginTimeCommandHandler(IUserRepository userRepository) : ICommandHandler +{ + public async Task Handle(UpdateUserLoginTimeCommand request, CancellationToken cancellationToken) + { + var user = await userRepository.GetAsync(request.UserId, cancellationToken) + ?? throw new KnownException($"未找到用户,UserId = {request.UserId}", ErrorCodes.UserNotFound); + + user.UpdateLastLoginTime(request.LoginTime); + user.SetUserRefreshToken(request.RefreshToken); + } +} +//#endif diff --git a/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/UpdateUserRoleNameCommand.cs b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/UpdateUserRoleNameCommand.cs new file mode 100644 index 00000000..7c6d21c4 --- /dev/null +++ b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/UpdateUserRoleNameCommand.cs @@ -0,0 +1,43 @@ +//#if (UseAdmin) +using FluentValidation; +using ABC.Template.Domain.AggregatesModel.UserAggregate; +using ABC.Template.Domain.AggregatesModel.RoleAggregate; +using ABC.Template.Infrastructure.Repositories; +using ABC.Template.Domain; + +namespace ABC.Template.Web.Application.Commands.Identity.Admin.UserCommands; + +/// +/// 更新用户角色名称命令 +/// +public record UpdateUserRoleNameCommand(UserId UserId, RoleId RoleId, string RoleName) : ICommand; + +/// +/// 更新用户角色名称命令验证器 +/// +public class UpdateUserRoleNameCommandValidator : AbstractValidator +{ + public UpdateUserRoleNameCommandValidator() + { + RuleFor(x => x.UserId).NotEmpty().WithMessage("用户ID不能为空"); + RuleFor(x => x.RoleId).NotEmpty().WithMessage("角色ID不能为空"); + RuleFor(x => x.RoleName).NotEmpty().WithMessage("角色名称不能为空"); + } +} + +/// +/// 更新用户角色名称命令处理器 +/// +public class UpdateUserRoleNameCommandHandler(IUserRepository userRepository) : ICommandHandler +{ + public async Task Handle(UpdateUserRoleNameCommand request, CancellationToken cancellationToken) + { + var user = await userRepository.GetAsync(request.UserId, cancellationToken) ?? + throw new KnownException($"未找到用户,UserId = {request.UserId}", ErrorCodes.UserNotFound); + + user.UpdateRoleInfo(request.RoleId, request.RoleName); + + return user.Id; + } +} +//#endif diff --git a/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/UpdateUserRolesCommand.cs b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/UpdateUserRolesCommand.cs new file mode 100644 index 00000000..7d0373d8 --- /dev/null +++ b/template/src/ABC.Template.Web/Application/Commands/Identity/Admin/UserCommands/UpdateUserRolesCommand.cs @@ -0,0 +1,46 @@ +//#if (UseAdmin) +using FluentValidation; +using ABC.Template.Domain.AggregatesModel.UserAggregate; +using ABC.Template.Infrastructure.Repositories; +using ABC.Template.Web.Application.Queries; +using ABC.Template.Domain; + +namespace ABC.Template.Web.Application.Commands.Identity.Admin.UserCommands; + +/// +/// 更新用户角色命令 +/// +public record UpdateUserRolesCommand(UserId UserId, List RolesToBeAssigned) : ICommand; + +/// +/// 更新用户角色命令验证器 +/// +public class UpdateUserRolesCommandValidator : AbstractValidator +{ + public UpdateUserRolesCommandValidator() + { + RuleFor(x => x.UserId).NotEmpty().WithMessage("用户ID不能为空"); + } +} + +/// +/// 更新用户角色命令处理器 +/// +public class UpdateUserRolesCommandHandler(IUserRepository userRepository) : ICommandHandler +{ + public async Task Handle(UpdateUserRolesCommand request, CancellationToken cancellationToken) + { + var user = await userRepository.GetAsync(request.UserId, cancellationToken) + ?? throw new KnownException($"未找到用户,UserId = {request.UserId}", ErrorCodes.UserNotFound); + + List roles = []; + + foreach (var assignAdminUserRoleDto in request.RolesToBeAssigned) + { + roles.Add(new UserRole(assignAdminUserRoleDto.RoleId, assignAdminUserRoleDto.RoleName)); + } + + user.UpdateRoles(roles); + } +} +//#endif diff --git a/template/src/ABC.Template.Web/Application/Commands/Orders/CreateOrderCommand.cs b/template/src/ABC.Template.Web/Application/Commands/Orders/CreateOrderCommand.cs index d198ae9b..b422572e 100644 --- a/template/src/ABC.Template.Web/Application/Commands/Orders/CreateOrderCommand.cs +++ b/template/src/ABC.Template.Web/Application/Commands/Orders/CreateOrderCommand.cs @@ -1,3 +1,4 @@ +//#if (UseDemoCode) using ABC.Template.Domain.AggregatesModel.OrderAggregate; using ABC.Template.Infrastructure.Repositories; @@ -24,4 +25,5 @@ public async Task Handle(CreateOrderCommand request, CancellationToken logger.LogInformation("order created, id:{OrderId}", order.Id); return order.Id; } -} \ No newline at end of file +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Application/Commands/Orders/PayOrderCommand.cs b/template/src/ABC.Template.Web/Application/Commands/Orders/PayOrderCommand.cs index 3a72d1ee..cbdfb488 100644 --- a/template/src/ABC.Template.Web/Application/Commands/Orders/PayOrderCommand.cs +++ b/template/src/ABC.Template.Web/Application/Commands/Orders/PayOrderCommand.cs @@ -1,3 +1,4 @@ +//#if (UseDemoCode) using ABC.Template.Domain.AggregatesModel.OrderAggregate; using ABC.Template.Infrastructure.Repositories; @@ -22,4 +23,5 @@ public async Task Handle(PayOrderCommand request, CancellationToken cancellation throw new KnownException($"未找到订单,OrderId = {request.OrderId}"); order.OrderPaid(); } -} \ No newline at end of file +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Application/DomainEventHandlers/DeptInfoChangedDomainEventHandlerForUpdateUserDeptName.cs b/template/src/ABC.Template.Web/Application/DomainEventHandlers/DeptInfoChangedDomainEventHandlerForUpdateUserDeptName.cs new file mode 100644 index 00000000..783a1d77 --- /dev/null +++ b/template/src/ABC.Template.Web/Application/DomainEventHandlers/DeptInfoChangedDomainEventHandlerForUpdateUserDeptName.cs @@ -0,0 +1,30 @@ +//#if (UseAdmin) +using ABC.Template.Domain.DomainEvents.DeptEvents; +using ABC.Template.Web.Application.Commands.Identity.Admin.UserCommands; +using ABC.Template.Web.Application.Queries; + +namespace ABC.Template.Web.Application.DomainEventHandlers; + +/// +/// 部门信息变更领域事件处理器 - 用于更新用户部门名称 +/// +public class DeptInfoChangedDomainEventHandlerForUpdateUserDeptName(IMediator mediator, UserQuery userQuery) : IDomainEventHandler +{ + public async Task Handle(DeptInfoChangedDomainEvent domainEvent, CancellationToken cancellationToken) + { + var dept = domainEvent.Dept; + var deptId = dept.Id; + var newDeptName = dept.Name; + + // 查询所有属于该部门的用户ID + var userIds = await userQuery.GetUserIdsByDeptIdAsync(deptId, cancellationToken); + + // 遍历用户ID,通过Command更新每个用户的部门名称 + foreach (var userId in userIds) + { + var command = new UpdateUserDeptNameCommand(userId, newDeptName); + await mediator.Send(command, cancellationToken); + } + } +} +//#endif diff --git a/template/src/ABC.Template.Web/Application/DomainEventHandlers/OrderCreatedDomainEventHandlerForDeliverGoods.cs b/template/src/ABC.Template.Web/Application/DomainEventHandlers/OrderCreatedDomainEventHandlerForDeliverGoods.cs index 660e0f4f..8a7d0c38 100644 --- a/template/src/ABC.Template.Web/Application/DomainEventHandlers/OrderCreatedDomainEventHandlerForDeliverGoods.cs +++ b/template/src/ABC.Template.Web/Application/DomainEventHandlers/OrderCreatedDomainEventHandlerForDeliverGoods.cs @@ -1,3 +1,4 @@ +//#if (UseDemoCode) using ABC.Template.Domain.DomainEvents; using ABC.Template.Web.Application.Commands.Delivers; @@ -9,4 +10,5 @@ public Task Handle(OrderCreatedDomainEvent notification, CancellationToken cance { return mediator.Send(new DeliverGoodsCommand(notification.Order.Id), cancellationToken); } -} \ No newline at end of file +} +//#endif diff --git a/template/src/ABC.Template.Web/Application/DomainEventHandlers/RoleInfoChangedDomainEventHandlerForUpdateUserRoleName.cs b/template/src/ABC.Template.Web/Application/DomainEventHandlers/RoleInfoChangedDomainEventHandlerForUpdateUserRoleName.cs new file mode 100644 index 00000000..d9032416 --- /dev/null +++ b/template/src/ABC.Template.Web/Application/DomainEventHandlers/RoleInfoChangedDomainEventHandlerForUpdateUserRoleName.cs @@ -0,0 +1,30 @@ +//#if (UseAdmin) +using ABC.Template.Domain.DomainEvents.RoleEvents; +using ABC.Template.Web.Application.Commands.Identity.Admin.UserCommands; +using ABC.Template.Web.Application.Queries; + +namespace ABC.Template.Web.Application.DomainEventHandlers; + +/// +/// 角色信息变更领域事件处理器 - 用于更新用户角色名称 +/// +public class RoleInfoChangedDomainEventHandlerForUpdateUserRoleName(IMediator mediator, UserQuery userQuery) : IDomainEventHandler +{ + public async Task Handle(RoleInfoChangedDomainEvent domainEvent, CancellationToken cancellationToken) + { + var role = domainEvent.Role; + var roleId = role.Id; + var newRoleName = role.Name; + + // 查询所有拥有该角色的用户ID + var userIds = await userQuery.GetUserIdsByRoleIdAsync(roleId, cancellationToken); + + // 遍历用户ID,通过Command更新每个用户的角色名称 + foreach (var userId in userIds) + { + var command = new UpdateUserRoleNameCommand(userId, roleId, newRoleName); + await mediator.Send(command, cancellationToken); + } + } +} +//#endif diff --git a/template/src/ABC.Template.Web/Application/IntegrationEventConverters/OrderPaidIntegrationEventConverter.cs b/template/src/ABC.Template.Web/Application/IntegrationEventConverters/OrderPaidIntegrationEventConverter.cs index 8df21ef6..d5666706 100644 --- a/template/src/ABC.Template.Web/Application/IntegrationEventConverters/OrderPaidIntegrationEventConverter.cs +++ b/template/src/ABC.Template.Web/Application/IntegrationEventConverters/OrderPaidIntegrationEventConverter.cs @@ -1,3 +1,4 @@ +//#if (UseDemoCode) using ABC.Template.Domain.DomainEvents; using ABC.Template.Web.Application.IntegrationEvents; @@ -10,4 +11,5 @@ public OrderPaidIntegrationEvent Convert(OrderPaidDomainEvent domainEvent) { return new OrderPaidIntegrationEvent(domainEvent.Order.Id); } -} \ No newline at end of file +} +//#endif diff --git a/template/src/ABC.Template.Web/Application/IntegrationEventHandlers/OrderPaidIntegrationEventHandlerForDeliverGoods.cs b/template/src/ABC.Template.Web/Application/IntegrationEventHandlers/OrderPaidIntegrationEventHandlerForDeliverGoods.cs index d6f3162c..9b6ee028 100644 --- a/template/src/ABC.Template.Web/Application/IntegrationEventHandlers/OrderPaidIntegrationEventHandlerForDeliverGoods.cs +++ b/template/src/ABC.Template.Web/Application/IntegrationEventHandlers/OrderPaidIntegrationEventHandlerForDeliverGoods.cs @@ -1,3 +1,4 @@ +//#if (UseDemoCode) using ABC.Template.Web.Application.Commands.Delivers; using ABC.Template.Web.Application.IntegrationEvents; @@ -10,4 +11,5 @@ public async Task HandleAsync(OrderPaidIntegrationEvent eventData, CancellationT var cmd = new DeliverGoodsCommand(eventData.OrderId); _ = await mediator.Send(cmd, cancellationToken); } -} \ No newline at end of file +} +//#endif diff --git a/template/src/ABC.Template.Web/Application/IntegrationEvents/OrderPaidIntegrationEvent.cs b/template/src/ABC.Template.Web/Application/IntegrationEvents/OrderPaidIntegrationEvent.cs index 65735943..45a58aa0 100644 --- a/template/src/ABC.Template.Web/Application/IntegrationEvents/OrderPaidIntegrationEvent.cs +++ b/template/src/ABC.Template.Web/Application/IntegrationEvents/OrderPaidIntegrationEvent.cs @@ -1,5 +1,7 @@ +//#if (UseDemoCode) using ABC.Template.Domain.AggregatesModel.OrderAggregate; namespace ABC.Template.Web.Application.IntegrationEvents; public record OrderPaidIntegrationEvent(OrderId OrderId); +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Application/Queries/DeptQuery.cs b/template/src/ABC.Template.Web/Application/Queries/DeptQuery.cs new file mode 100644 index 00000000..74e2d0a6 --- /dev/null +++ b/template/src/ABC.Template.Web/Application/Queries/DeptQuery.cs @@ -0,0 +1,200 @@ +//#if (UseAdmin) +using Microsoft.EntityFrameworkCore; +using ABC.Template.Domain.AggregatesModel.DeptAggregate; + +namespace ABC.Template.Web.Application.Queries; + +/// +/// 部门查询DTO +/// +public record DeptQueryDto(DeptId Id, string Name, string Remark, DeptId ParentId, int Status, DateTimeOffset CreatedAt, DeletedTime? DeletedAt); + +/// +/// 部门查询输入参数 +/// +public class DeptQueryInput +{ + public string? Name { get; set; } + public string? Remark { get; set; } + public int? Status { get; set; } + public DeptId? ParentId { get; set; } +} + +/// +/// 部门树形DTO - 应用层数据传输对象 +/// +public record DeptTreeDto( + DeptId Id, + string Name, + string Remark, + DeptId ParentId, + int Status, + DateTimeOffset CreatedAt, + IEnumerable Children); + +/// +/// 部门查询服务 +/// +public class DeptQuery(ApplicationDbContext applicationDbContext) : IQuery +{ + private DbSet DeptSet { get; } = applicationDbContext.Depts; + + /// + /// 检查部门名称是否存在 + /// + public async Task DoesDeptExist(string name, CancellationToken cancellationToken) + { + return await DeptSet.AsNoTracking() + .AnyAsync(d => d.Name == name, cancellationToken: cancellationToken); + } + + /// + /// 检查部门ID是否存在 + /// + public async Task DoesDeptExist(DeptId id, CancellationToken cancellationToken) + { + return await DeptSet.AsNoTracking() + .AnyAsync(d => d.Id == id, cancellationToken: cancellationToken); + } + + /// + /// 根据ID获取部门 + /// + public async Task GetDeptByIdAsync(DeptId id, CancellationToken cancellationToken = default) + { + return await DeptSet.AsNoTracking() + .Where(d => d.Id == id) + .Select(d => new DeptQueryDto(d.Id, d.Name, d.Remark, d.ParentId, d.Status, d.CreatedAt, d.DeletedAt)) + .FirstOrDefaultAsync(cancellationToken); + } + + /// + /// 获取所有部门 + /// + public async Task> GetAllDeptsAsync(DeptQueryInput query, CancellationToken cancellationToken) + { + return await DeptSet.AsNoTracking() + .WhereIf(!string.IsNullOrWhiteSpace(query.Name), d => d.Name.Contains(query.Name!)) + .WhereIf(!string.IsNullOrWhiteSpace(query.Remark), d => d.Remark.Contains(query.Remark!)) + .WhereIf(query.Status.HasValue, d => d.Status == query.Status) + .WhereIf(query.ParentId != null, d => d.ParentId == query.ParentId) + .OrderBy(d => d.CreatedAt) + .Select(d => new DeptQueryDto(d.Id, d.Name, d.Remark, d.ParentId, d.Status, d.CreatedAt, d.DeletedAt)) + .ToListAsync(cancellationToken); + } + + /// + /// 获取部门树 + /// 优化:使用投影只选择需要的字段,减少内存占用 + /// + public async Task> GetDeptTreeAsync(bool includeInactive = false, CancellationToken cancellationToken = default) + { + // 使用投影只选择构建树所需的字段,减少内存占用 + var allDepts = await DeptSet.AsNoTracking() + .WhereIf(!includeInactive, d => d.Status != 0) + .Select(d => new DeptTreeNode + { + Id = d.Id, + Name = d.Name, + Remark = d.Remark, + ParentId = d.ParentId, + Status = d.Status, + CreatedAt = d.CreatedAt + }) + .ToListAsync(cancellationToken); + + // 构建树形结构 + var treeStructure = BuildTreeStructureFromProjection(allDepts); + + // 转换为应用层DTO + return treeStructure.Select(d => ConvertToTreeDtoFromProjection(d)); + } + + /// + /// 构建部门树形结构(基于投影数据) + /// + private static List BuildTreeStructureFromProjection( + List allDepts) + { + var deptDict = allDepts.ToDictionary(d => d.Id); + var result = new List(); + + foreach (var dept in allDepts) + { + // 只处理根节点(ParentId为0) + if (dept.ParentId == new DeptId(0)) + { + result.Add(BuildTreeDtoFromProjection(dept, deptDict)); + } + } + + return result.OrderBy(d => d.CreatedAt).ToList(); + } + + /// + /// 构建单个部门的树形结构(基于投影数据) + /// + private static DeptTreeNode BuildTreeDtoFromProjection( + DeptTreeNode dept, + Dictionary allDepts) + { + var children = new List(); + + // 查找所有以当前部门为父级的子部门 + var childDepts = allDepts.Values + .Where(d => d.ParentId == dept.Id) + .OrderBy(d => d.CreatedAt); + + foreach (var child in childDepts) + { + children.Add(BuildTreeDtoFromProjection(child, allDepts)); + } + + return new DeptTreeNode + { + Id = dept.Id, + Name = dept.Name, + Remark = dept.Remark, + ParentId = dept.ParentId, + Status = dept.Status, + CreatedAt = dept.CreatedAt, + Children = children + }; + } + + /// + /// 将投影节点转换为树形DTO + /// + private static DeptTreeDto ConvertToTreeDtoFromProjection(DeptTreeNode node) + { + var children = node.Children + .OrderBy(d => d.CreatedAt) + .Select(d => ConvertToTreeDtoFromProjection(d)) + .ToList(); + + return new DeptTreeDto( + node.Id, + node.Name, + node.Remark, + node.ParentId, + node.Status, + node.CreatedAt, + children + ); + } + + /// + /// 部门树节点(用于内存中的树构建) + /// + private sealed class DeptTreeNode + { + public DeptId Id { get; set; }=default!; + public string Name { get; set; } = string.Empty; + public string Remark { get; set; } = string.Empty; + public DeptId ParentId { get; set; }=default!; + public int Status { get; set; } + public DateTimeOffset CreatedAt { get; set; } + public List Children { get; set; } = new(); + } +} +//#endif diff --git a/template/src/ABC.Template.Web/Application/Queries/Orders/OrderQuery.cs b/template/src/ABC.Template.Web/Application/Queries/Orders/OrderQuery.cs index 50bf4e6e..e0a18a85 100644 --- a/template/src/ABC.Template.Web/Application/Queries/Orders/OrderQuery.cs +++ b/template/src/ABC.Template.Web/Application/Queries/Orders/OrderQuery.cs @@ -1,3 +1,4 @@ +//#if (UseDemoCode) using ABC.Template.Domain.AggregatesModel.OrderAggregate; using Microsoft.EntityFrameworkCore; @@ -19,3 +20,4 @@ public async Task Handle(QueryOrder request, CancellationToken return result ?? throw new KnownException("Order not found"); } } +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Application/Queries/RoleQuery.cs b/template/src/ABC.Template.Web/Application/Queries/RoleQuery.cs new file mode 100644 index 00000000..99d9ab6b --- /dev/null +++ b/template/src/ABC.Template.Web/Application/Queries/RoleQuery.cs @@ -0,0 +1,91 @@ +//#if (UseAdmin) +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using ABC.Template.Domain.AggregatesModel.RoleAggregate; + +namespace ABC.Template.Web.Application.Queries; + +public record RoleQueryDto(RoleId RoleId, string Name, string Description, bool IsActive, DateTimeOffset CreatedAt, IEnumerable PermissionCodes); + +public class RoleQueryInput : PageRequest +{ + public string? Name { get; set; } + public string? Description { get; set; } + public bool? IsActive { get; set; } + public DateTimeOffset? StartTime { get; set; } + public DateTimeOffset? EndTime { get; set; } +} + +public record AssignAdminUserRoleQueryDto(RoleId RoleId, string RoleName, IEnumerable PermissionCodes); + +public class RoleQuery(ApplicationDbContext applicationDbContext, IMemoryCache memoryCache) : IQuery +{ + private DbSet RoleSet { get; } = applicationDbContext.Roles; + private const string RoleCacheKeyPrefix = "role:"; + private static readonly TimeSpan RoleCacheExpiry = TimeSpan.FromMinutes(10); + + public async Task DoesRoleExist(string name, CancellationToken cancellationToken) + { + return await RoleSet.AsNoTracking() + .AnyAsync(r => r.Name == name, cancellationToken: cancellationToken); + } + + public async Task> GetAdminRolesForAssignmentAsync(IEnumerable ids, CancellationToken cancellationToken) + { + return await RoleSet.AsNoTracking() + .Where(r => ids.Contains(r.Id)) + .Select(r => new AssignAdminUserRoleQueryDto( + r.Id, + r.Name, + r.Permissions.Select(rp => rp.PermissionCode))) + .ToListAsync(cancellationToken); + } + + public async Task> GetAssignedPermissionCodesAsync(IEnumerable ids, CancellationToken cancellationToken) + { + if (!ids.Any()) + { + return Enumerable.Empty(); + } + + // 将 ids 转换为列表,避免多次枚举 + var roleIds = ids.ToList(); + + // 直接在数据库查询中使用 Distinct,让数据库处理去重,提高性能 + return await RoleSet.AsNoTracking() + .Where(r => roleIds.Contains(r.Id)) + .SelectMany(r => r.Permissions) + .Select(rp => rp.PermissionCode) + .Distinct() + .ToListAsync(cancellationToken); + } + + public async Task GetRoleByIdAsync(RoleId id, CancellationToken cancellationToken = default) + { + var cacheKey = $"{RoleCacheKeyPrefix}{id}"; + + return await memoryCache.GetOrCreateAsync(cacheKey, async entry => + { + entry.AbsoluteExpirationRelativeToNow = RoleCacheExpiry; + + return await RoleSet.AsNoTracking() + .Where(r => r.Id == id) + .Select(r => new RoleQueryDto(r.Id, r.Name, r.Description, r.IsActive, r.CreatedAt, r.Permissions.Select(rp => rp.PermissionCode))) + .FirstOrDefaultAsync(cancellationToken); + }); + } + + public async Task> GetAllRolesAsync(RoleQueryInput query, CancellationToken cancellationToken) + { + return await RoleSet.AsNoTracking() + .WhereIf(!string.IsNullOrWhiteSpace(query.Name), r => r.Name.Contains(query.Name!)) + .WhereIf(!string.IsNullOrWhiteSpace(query.Description), r => r.Description.Contains(query.Description!)) + .WhereIf(query.IsActive.HasValue, r => r.IsActive == query.IsActive) + .WhereIf(query.StartTime.HasValue, r => r.CreatedAt >= query.StartTime!.Value) + .WhereIf(query.EndTime.HasValue, r => r.CreatedAt <= query.EndTime!.Value) + .OrderBy(r => r.Id) + .Select(r => new RoleQueryDto(r.Id, r.Name, r.Description, r.IsActive, r.CreatedAt, r.Permissions.Select(rp => rp.PermissionCode))) + .ToPagedDataAsync(query, cancellationToken); + } +} +//#endif diff --git a/template/src/ABC.Template.Web/Application/Queries/UserQuery.cs b/template/src/ABC.Template.Web/Application/Queries/UserQuery.cs new file mode 100644 index 00000000..46014411 --- /dev/null +++ b/template/src/ABC.Template.Web/Application/Queries/UserQuery.cs @@ -0,0 +1,160 @@ +//#if (UseAdmin) +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using ABC.Template.Domain.AggregatesModel.RoleAggregate; +using ABC.Template.Domain.AggregatesModel.UserAggregate; +using ABC.Template.Domain.AggregatesModel.DeptAggregate; +using ABC.Template.Domain; + +namespace ABC.Template.Web.Application.Queries; + +/// +/// 用户信息查询DTO +/// +public record UserInfoQueryDto(UserId UserId, string Name, string Phone, IEnumerable Roles, string RealName, int Status, string Email, DateTimeOffset CreatedAt, string Gender, int Age, DateTimeOffset BirthDate, DeptId? DeptId, string DeptName); + +public record UserLoginInfoQueryDto(UserId UserId, string Name, string Email, string PasswordHash, IEnumerable UserRoles); + +public class UserQueryInput : PageRequest +{ + public string? Keyword { get; set; } + public int? Status { get; set; } +} + +public class UserQuery(ApplicationDbContext applicationDbContext, IMemoryCache memoryCache) : IQuery +{ + private DbSet UserSet { get; } = applicationDbContext.Users; + private const string UserCacheKeyPrefix = "user:"; + private static readonly TimeSpan UserCacheExpiry = TimeSpan.FromMinutes(10); + + public async Task GetUserIdByRefreshTokenAsync(string refreshToken, CancellationToken cancellationToken) + { + return await UserSet.AsNoTracking() + .SelectMany(u => u.RefreshTokens) + .Where(t => t.Token == refreshToken) + .Select(t => t.UserId) + .SingleOrDefaultAsync(cancellationToken) + ?? throw new KnownException("无效的令牌", ErrorCodes.InvalidToken); + } + + public async Task DoesUserExist(string username, CancellationToken cancellationToken) + { + return await UserSet.AsNoTracking() + .AnyAsync(u => u.Name == username, cancellationToken: cancellationToken); + } + + public async Task DoesUserExist(UserId userId, CancellationToken cancellationToken) + { + return await UserSet.AsNoTracking() + .AnyAsync(u => u.Id == userId, cancellationToken: cancellationToken); + } + + public async Task DoesEmailExist(string email, CancellationToken cancellationToken) + { + return await UserSet.AsNoTracking() + .AnyAsync(u => u.Email == email, cancellationToken: cancellationToken); + } + + /// + /// 根据ID获取用户信息(带缓存) + /// + public async Task GetUserByIdAsync(UserId userId, CancellationToken cancellationToken) + { + var cacheKey = $"{UserCacheKeyPrefix}{userId}"; + + return await memoryCache.GetOrCreateAsync(cacheKey, async entry => + { + entry.AbsoluteExpirationRelativeToNow = UserCacheExpiry; + + return await UserSet.AsNoTracking() + .Where(u => u.Id == userId) + .Select(au => new UserInfoQueryDto( + au.Id, + au.Name, + au.Phone, + au.Roles.Select(r => r.RoleName), + au.RealName, + au.Status, + au.Email, + au.CreatedAt, + au.Gender, + au.Age, + au.BirthDate, + au.Dept != null ? au.Dept.DeptId : null, + au.Dept != null ? au.Dept.DeptName : string.Empty)) + .FirstOrDefaultAsync(cancellationToken); + }); + } + + public async Task> GetUserIdsByRoleIdAsync(RoleId roleId, CancellationToken cancellationToken = default) + { + return await UserSet.AsNoTracking() + .Where(u => u.Roles.Any(r => r.RoleId == roleId)) + .Select(u => u.Id) + .ToListAsync(cancellationToken); + } + + /// + /// 根据部门ID获取所有用户ID列表 + /// + /// 部门ID + /// 取消令牌 + /// 属于指定部门的所有用户ID列表 + public async Task> GetUserIdsByDeptIdAsync(DeptId deptId, CancellationToken cancellationToken = default) + { + return await UserSet.AsNoTracking() + .Where(u => u.Dept != null && u.Dept.DeptId == deptId) + .Select(u => u.Id) + .ToListAsync(cancellationToken); + } + + public async Task GetUserInfoForLoginAsync(string name, CancellationToken cancellationToken) + { + return await UserSet + .Where(u => u.Name == name) + .Select(u => new UserLoginInfoQueryDto(u.Id, u.Name, u.Email, u.PasswordHash, u.Roles)) + .FirstOrDefaultAsync(cancellationToken); + } + + public async Task GetUserInfoForLoginByIdAsync(UserId userId, CancellationToken cancellationToken) + { + return await UserSet + .Where(u => u.Id == userId) + .Select(u => new UserLoginInfoQueryDto(u.Id, u.Name, u.Email, u.PasswordHash, u.Roles)) + .FirstOrDefaultAsync(cancellationToken); + } + + public async Task> GetAllUsersAsync(UserQueryInput query, CancellationToken cancellationToken) + { + var queryable = UserSet.AsNoTracking(); + + if (!string.IsNullOrWhiteSpace(query.Keyword)) + { + queryable = queryable.Where(u => u.Name.Contains(query.Keyword!) || u.Email.Contains(query.Keyword!)); + } + + if (query.Status.HasValue) + { + queryable = queryable.Where(u => u.Status == query.Status); + } + + return await queryable + .OrderByDescending(u => u.Id) + .Select(u => new UserInfoQueryDto( + u.Id, + u.Name, + u.Phone, + u.Roles.Select(r => r.RoleName), + u.RealName, + u.Status, + u.Email, + u.CreatedAt, + u.Gender, + u.Age, + u.BirthDate, + u.Dept != null ? u.Dept.DeptId : null, + u.Dept != null ? u.Dept.DeptName : string.Empty)) + .ToPagedDataAsync(query, cancellationToken); + } +} +//#endif diff --git a/template/src/ABC.Template.Web/Endpoints/Identity/Admin/DeptEndpoints/CreateDeptEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/DeptEndpoints/CreateDeptEndpoint.cs new file mode 100644 index 00000000..784aa451 --- /dev/null +++ b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/DeptEndpoints/CreateDeptEndpoint.cs @@ -0,0 +1,50 @@ +//#if (UseAdmin) +using FastEndpoints; +using MediatR; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using ABC.Template.Domain.AggregatesModel.DeptAggregate; +using ABC.Template.Web.Application.Commands.Identity.Admin.DeptCommands; +using ABC.Template.Web.AppPermissions; + +namespace ABC.Template.Web.Endpoints.Identity.Admin.DeptEndpoints; + +/// +/// 创建部门的请求模型 +/// +/// 部门名称 +/// 备注 +/// 父级部门ID,可为空表示顶级部门 +/// 状态(0=禁用,1=启用) +public record CreateDeptRequest(string Name, string Remark, DeptId? ParentId, int Status); + +/// +/// 创建部门的响应模型 +/// +/// 新创建的部门ID +/// 部门名称 +/// 备注 +public record CreateDeptResponse(DeptId Id, string Name, string Remark); + +/// +/// 创建部门的API端点 +/// 该端点用于在系统中创建新的部门,支持层级结构 +/// +[Tags("Depts")] +public class CreateDeptEndpoint(IMediator mediator) : Endpoint> +{ + public override void Configure() + { + Post("/api/admin/dept"); + AuthSchemes(JwtBearerDefaults.AuthenticationScheme); + Permissions(PermissionCodes.AllApiAccess, PermissionCodes.DeptCreate); + } + + public override async Task HandleAsync(CreateDeptRequest req, CancellationToken ct) + { + var cmd = new CreateDeptCommand(req.Name, req.Remark, req.ParentId, req.Status); + var deptId = await mediator.Send(cmd, ct); + var response = new CreateDeptResponse(deptId, req.Name, req.Remark); + await Send.OkAsync(response.AsResponseData(), cancellation: ct); + } +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/Identity/Admin/DeptEndpoints/DeleteDeptEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/DeptEndpoints/DeleteDeptEndpoint.cs new file mode 100644 index 00000000..1c0f3dd8 --- /dev/null +++ b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/DeptEndpoints/DeleteDeptEndpoint.cs @@ -0,0 +1,45 @@ +//#if (UseAdmin) +using FastEndpoints; +using MediatR; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using ABC.Template.Domain.AggregatesModel.DeptAggregate; +using ABC.Template.Web.Application.Commands.Identity.Admin.DeptCommands; +using ABC.Template.Web.AppPermissions; + +namespace ABC.Template.Web.Endpoints.Identity.Admin.DeptEndpoints; + +/// +/// 删除部门的请求模型 +/// +/// 部门ID +public record DeleteDeptRequest(DeptId Id); + +/// +/// 删除部门的API端点 +/// 该端点用于删除指定的部门(软删除) +/// +[Tags("Depts")] +public class DeleteDeptEndpoint(IMediator mediator) : Endpoint> +{ + public override void Configure() + { + Delete("/api/admin/dept/{id}"); + AuthSchemes(JwtBearerDefaults.AuthenticationScheme); + Permissions(PermissionCodes.AllApiAccess, PermissionCodes.DeptDelete); + } + + /// + /// 处理HTTP请求的核心方法 + /// 删除指定的部门 + /// + /// 取消令牌,用于支持异步操作的取消 + /// 异步任务 + public override async Task HandleAsync(DeleteDeptRequest request,CancellationToken ct) + { + + var command = new DeleteDeptCommand(request.Id); + await mediator.Send(command, ct); + await Send.OkAsync(true.AsResponseData(), cancellation: ct); + } +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/Identity/Admin/DeptEndpoints/GetDeptEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/DeptEndpoints/GetDeptEndpoint.cs new file mode 100644 index 00000000..3a2f2512 --- /dev/null +++ b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/DeptEndpoints/GetDeptEndpoint.cs @@ -0,0 +1,71 @@ +//#if (UseAdmin) +using FastEndpoints; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using ABC.Template.Domain.AggregatesModel.DeptAggregate; +using ABC.Template.Web.Application.Queries; +using ABC.Template.Web.AppPermissions; + +namespace ABC.Template.Web.Endpoints.Identity.Admin.DeptEndpoints; + +/// +/// 获取单个部门的请求模型 +/// +/// 部门ID +public record GetDeptRequest(DeptId Id); + +/// +/// 获取单个部门的响应模型 +/// +/// 部门ID +/// 部门名称 +/// 备注 +/// 父级部门ID +/// 状态(0=禁用,1=启用) +/// 创建时间 +public record GetDeptResponse(DeptId Id, string Name, string Remark, DeptId ParentId, int Status, DateTimeOffset CreatedAt); + +/// +/// 获取单个部门的API端点 +/// 该端点用于根据ID查询特定部门的详细信息 +/// +[Tags("Depts")] +public class GetDeptEndpoint(DeptQuery deptQuery) : Endpoint> +{ + + public override void Configure() + { + + Get("/api/admin/dept/{id}"); + AuthSchemes(JwtBearerDefaults.AuthenticationScheme); + Permissions(PermissionCodes.AllApiAccess, PermissionCodes.DeptView); + } + + public override async Task HandleAsync(GetDeptRequest req, CancellationToken ct) + { + + + // 通过查询服务获取部门详细信息 + var dept = await deptQuery.GetDeptByIdAsync(req.Id, ct); + + // 验证部门是否存在 + if (dept == null) + { + await Send.NotFoundAsync(ct); + } + else + { + // 创建响应对象 + var response = new GetDeptResponse( + dept.Id, + dept.Name, + dept.Remark, + dept.ParentId, + dept.Status, + dept.CreatedAt + ); + // 返回成功响应,使用统一的响应数据格式包装 + await Send.OkAsync(response.AsResponseData(), cancellation: ct); + } + } +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/Identity/Admin/DeptEndpoints/GetDeptTreeEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/DeptEndpoints/GetDeptTreeEndpoint.cs new file mode 100644 index 00000000..d4793ce9 --- /dev/null +++ b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/DeptEndpoints/GetDeptTreeEndpoint.cs @@ -0,0 +1,37 @@ +//#if (UseAdmin) +using FastEndpoints; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using ABC.Template.Web.Application.Queries; +using ABC.Template.Web.AppPermissions; + +namespace ABC.Template.Web.Endpoints.Identity.Admin.DeptEndpoints; + +/// +/// 获取部门树的请求模型 +/// +/// 是否包含非激活的部门 +public record GetDeptTreeRequest(bool IncludeInactive = false); + +/// +/// 获取部门树的API端点 +/// 该端点用于查询系统中的部门树形结构 +/// +[Tags("Depts")] +public class GetDeptTreeEndpoint(DeptQuery deptQuery) : Endpoint>> +{ + + public override void Configure() + { + Get("/api/admin/dept/tree"); + AuthSchemes(JwtBearerDefaults.AuthenticationScheme); + Permissions(PermissionCodes.AllApiAccess, PermissionCodes.DeptView); + } + + + public override async Task HandleAsync(GetDeptTreeRequest req, CancellationToken ct) + { + var tree = await deptQuery.GetDeptTreeAsync(req.IncludeInactive, ct); + await Send.OkAsync(tree.AsResponseData(), cancellation: ct); + } +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/Identity/Admin/DeptEndpoints/UpdateDeptEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/DeptEndpoints/UpdateDeptEndpoint.cs new file mode 100644 index 00000000..fa0fe8f9 --- /dev/null +++ b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/DeptEndpoints/UpdateDeptEndpoint.cs @@ -0,0 +1,49 @@ +//#if (UseAdmin) +using FastEndpoints; +using MediatR; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using ABC.Template.Domain.AggregatesModel.DeptAggregate; +using ABC.Template.Web.Application.Commands.Identity.Admin.DeptCommands; +using ABC.Template.Web.AppPermissions; + +namespace ABC.Template.Web.Endpoints.Identity.Admin.DeptEndpoints; + +/// +/// 更新部门的请求模型 +/// +/// 部门ID +/// 部门名称 +/// 备注 +/// 父级部门ID,可为空表示顶级部门 +/// 状态(0=禁用,1=启用) +public record UpdateDeptRequest(DeptId Id, string Name, string Remark, DeptId? ParentId, int Status); + +/// +/// 更新部门的API端点 +/// 该端点用于更新现有部门的信息 +/// +[Tags("Depts")] +public class UpdateDeptEndpoint(IMediator mediator) : Endpoint> +{ + public override void Configure() + { + Put("/api/admin/dept"); + AuthSchemes(JwtBearerDefaults.AuthenticationScheme); + Permissions(PermissionCodes.AllApiAccess, PermissionCodes.DeptEdit); + } + + public override async Task HandleAsync(UpdateDeptRequest req, CancellationToken ct) + { + // 如果父级ID为空,则设置为根部门(ID为0) + var command = new UpdateDeptCommand( + req.Id, + req.Name, + req.Remark, + req.ParentId ?? new DeptId(0), + req.Status + ); + await mediator.Send(command, ct); + await Send.OkAsync(true.AsResponseData(), cancellation: ct); + } +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/ActivateRoleEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/ActivateRoleEndpoint.cs new file mode 100644 index 00000000..1017f778 --- /dev/null +++ b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/ActivateRoleEndpoint.cs @@ -0,0 +1,38 @@ +//#if (UseAdmin) +using FastEndpoints; +using MediatR; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using ABC.Template.Domain.AggregatesModel.RoleAggregate; +using ABC.Template.Web.Application.Commands.Identity.Admin.RoleCommands; +using ABC.Template.Web.AppPermissions; + +namespace ABC.Template.Web.Endpoints.Identity.Admin.RoleEndpoints; + +/// +/// 激活角色的请求模型 +/// +/// 要激活的角色ID +public record ActivateRoleRequest(RoleId RoleId); + +/// +/// 激活角色的API端点 +/// 该端点用于激活已停用的角色 +/// +[Tags("Roles")] +public class ActivateRoleEndpoint(IMediator mediator) : Endpoint> +{ + public override void Configure() + { + Put("/api/admin/roles/activate"); + AuthSchemes(JwtBearerDefaults.AuthenticationScheme); + Permissions(PermissionCodes.AllApiAccess, PermissionCodes.RoleEdit); + } + + public override async Task HandleAsync(ActivateRoleRequest req, CancellationToken ct) + { + var cmd = new ActivateRoleCommand(req.RoleId); + await mediator.Send(cmd, ct); + await Send.OkAsync(true.AsResponseData(), cancellation: ct); + } +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/CreateRoleEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/CreateRoleEndpoint.cs new file mode 100644 index 00000000..36619977 --- /dev/null +++ b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/CreateRoleEndpoint.cs @@ -0,0 +1,49 @@ +//#if (UseAdmin) +using FastEndpoints; +using MediatR; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using ABC.Template.Domain.AggregatesModel.RoleAggregate; +using ABC.Template.Web.Application.Commands.Identity.Admin.RoleCommands; +using ABC.Template.Web.AppPermissions; + +namespace ABC.Template.Web.Endpoints.Identity.Admin.RoleEndpoints; + +/// +/// 创建角色的请求模型 +/// +/// 角色名称 +/// 角色描述 +/// 权限代码列表 +public record CreateRoleRequest(string Name, string Description, IEnumerable PermissionCodes); + +/// +/// 创建角色的响应模型 +/// +/// 新创建的角色ID +/// 角色名称 +/// 角色描述 +public record CreateRoleResponse(RoleId RoleId, string Name, string Description); + +/// +/// 创建角色的API端点 +/// 该端点用于在系统中创建新的角色,并分配相应的权限 +/// +[Tags("Roles")] +public class CreateRoleEndpoint(IMediator mediator) : Endpoint> +{ + public override void Configure() + { + Post("/api/admin/roles"); + AuthSchemes(JwtBearerDefaults.AuthenticationScheme); + Permissions(PermissionCodes.AllApiAccess, PermissionCodes.RoleCreate); + } + + public override async Task HandleAsync(CreateRoleRequest req, CancellationToken ct) + { + var cmd = new CreateRoleCommand(req.Name, req.Description, req.PermissionCodes); + var result = await mediator.Send(cmd, ct); + var response = new CreateRoleResponse(result, req.Name, req.Description); + await Send.OkAsync(response.AsResponseData(), cancellation: ct); + } +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/DeactivateRoleEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/DeactivateRoleEndpoint.cs new file mode 100644 index 00000000..66ad8005 --- /dev/null +++ b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/DeactivateRoleEndpoint.cs @@ -0,0 +1,38 @@ +//#if (UseAdmin) +using FastEndpoints; +using MediatR; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using ABC.Template.Domain.AggregatesModel.RoleAggregate; +using ABC.Template.Web.Application.Commands.Identity.Admin.RoleCommands; +using ABC.Template.Web.AppPermissions; + +namespace ABC.Template.Web.Endpoints.Identity.Admin.RoleEndpoints; + +/// +/// 停用角色的请求模型 +/// +/// 要停用的角色ID +public record DeactivateRoleRequest(RoleId RoleId); + +/// +/// 停用角色的API端点 +/// 该端点用于停用已激活的角色 +/// +[Tags("Roles")] +public class DeactivateRoleEndpoint(IMediator mediator) : Endpoint> +{ + public override void Configure() + { + Put("/api/admin/roles/deactivate"); + AuthSchemes(JwtBearerDefaults.AuthenticationScheme); + Permissions(PermissionCodes.AllApiAccess, PermissionCodes.RoleEdit); + } + + public override async Task HandleAsync(DeactivateRoleRequest req, CancellationToken ct) + { + var cmd = new DeactivateRoleCommand(req.RoleId); + await mediator.Send(cmd, ct); + await Send.OkAsync(true.AsResponseData(), cancellation: ct); + } +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/DeleteRoleEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/DeleteRoleEndpoint.cs new file mode 100644 index 00000000..fbcb82a0 --- /dev/null +++ b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/DeleteRoleEndpoint.cs @@ -0,0 +1,39 @@ +//#if (UseAdmin) +using FastEndpoints; +using MediatR; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using ABC.Template.Domain.AggregatesModel.RoleAggregate; +using ABC.Template.Web.Application.Commands.Identity.Admin.RoleCommands; +using ABC.Template.Web.AppPermissions; + +namespace ABC.Template.Web.Endpoints.Identity.Admin.RoleEndpoints; + +/// +/// 删除角色的请求模型 +/// +/// 要删除的角色ID +public record DeleteRoleRequest(RoleId RoleId); + +/// +/// 删除角色的API端点 +/// 该端点用于从系统中删除指定的角色(软删除) +/// +[Tags("Roles")] +public class DeleteRoleEndpoint(IMediator mediator) : Endpoint> +{ + public override void Configure() + { + Delete("/api/admin/roles/{roleId}"); + AuthSchemes(JwtBearerDefaults.AuthenticationScheme); + Permissions(PermissionCodes.AllApiAccess, PermissionCodes.RoleDelete); + } + + + public override async Task HandleAsync(DeleteRoleRequest request, CancellationToken ct) + { + var command = new DeleteRoleCommand(request.RoleId); + await mediator.Send(command, ct); + await Send.OkAsync(true.AsResponseData(), cancellation: ct); + } +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/GetAllRolesEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/GetAllRolesEndpoint.cs new file mode 100644 index 00000000..9994e466 --- /dev/null +++ b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/GetAllRolesEndpoint.cs @@ -0,0 +1,30 @@ +//#if (UseAdmin) +using FastEndpoints; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using ABC.Template.Web.Application.Queries; +using ABC.Template.Web.AppPermissions; + +namespace ABC.Template.Web.Endpoints.Identity.Admin.RoleEndpoints; + +/// +/// 获取所有角色的API端点 +/// 该端点用于查询系统中的所有角色信息,支持分页和筛选 +/// +[Tags("Roles")] +public class GetAllRolesEndpoint(RoleQuery roleQuery) : Endpoint>> +{ + + public override void Configure() + { + Get("/api/admin/roles"); + AuthSchemes(JwtBearerDefaults.AuthenticationScheme); + Permissions(PermissionCodes.AllApiAccess, PermissionCodes.RoleView); + } + + public override async Task HandleAsync(RoleQueryInput req, CancellationToken ct) + { + var roleInfo = await roleQuery.GetAllRolesAsync(req, ct); + await Send.OkAsync(roleInfo.AsResponseData(), cancellation: ct); + } +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/GetRoleEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/GetRoleEndpoint.cs new file mode 100644 index 00000000..c83ba00c --- /dev/null +++ b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/GetRoleEndpoint.cs @@ -0,0 +1,48 @@ +//#if (UseAdmin) +using FastEndpoints; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using ABC.Template.Domain.AggregatesModel.RoleAggregate; +using ABC.Template.Web.Application.Queries; +using ABC.Template.Web.AppPermissions; + +namespace ABC.Template.Web.Endpoints.Identity.Admin.RoleEndpoints; + +/// +/// 获取角色信息的请求模型 +/// +/// 要查询的角色ID +public record GetRoleRequest(RoleId Id); + +/// +/// 获取角色信息的API端点 +/// 该端点用于根据角色ID查询角色的详细信息,包括权限列表 +/// +[Tags("Roles")] +public class GetRoleEndpoint(RoleQuery roleQuery) : Endpoint> +{ + + public override void Configure() + { + Get("/api/admin/roles/{id}"); + AuthSchemes(JwtBearerDefaults.AuthenticationScheme); + Permissions(PermissionCodes.AllApiAccess, PermissionCodes.RoleView); + } + + + public override async Task HandleAsync(GetRoleRequest req, CancellationToken ct) + { + // 通过查询服务获取角色详细信息 + var roleInfo = await roleQuery.GetRoleByIdAsync(req.Id, ct); + + // 验证角色是否存在 + if (roleInfo == null) + { + await Send.NotFoundAsync(ct); + } + else + { + await Send.OkAsync(roleInfo!.AsResponseData(), cancellation: ct); + } + } +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/UpdateRoleEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/UpdateRoleEndpoint.cs new file mode 100644 index 00000000..d7eefa33 --- /dev/null +++ b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/RoleEndpoints/UpdateRoleEndpoint.cs @@ -0,0 +1,48 @@ +//#if (UseAdmin) +using FastEndpoints; +using MediatR; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using ABC.Template.Domain.AggregatesModel.RoleAggregate; +using ABC.Template.Web.Application.Commands.Identity.Admin.RoleCommands; +using ABC.Template.Web.AppPermissions; + +namespace ABC.Template.Web.Endpoints.Identity.Admin.RoleEndpoints; + +/// +/// 更新角色信息的请求模型 +/// +/// 要更新的角色ID +/// 新的角色名称 +/// 新的角色描述 +/// 新的权限代码列表 +public record UpdateRoleInfoRequest(RoleId RoleId, string Name, string Description, IEnumerable PermissionCodes); + +/// +/// 更新角色信息的API端点 +/// 该端点用于修改现有角色的基本信息和权限分配 +/// +[Tags("Roles")] +public class UpdateRoleEndpoint(IMediator mediator) : Endpoint> +{ + + public override void Configure() + { + Put("/api/admin/roles/update"); + AuthSchemes(JwtBearerDefaults.AuthenticationScheme); + Permissions(PermissionCodes.AllApiAccess, PermissionCodes.RoleEdit); + } + + + public override async Task HandleAsync(UpdateRoleInfoRequest request, CancellationToken ct) + { + var cmd = new UpdateRoleInfoCommand( + request.RoleId, + request.Name, + request.Description, + request.PermissionCodes + ); + await mediator.Send(cmd, ct); + await Send.OkAsync(true.AsResponseData(), cancellation: ct); + } +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/AuthEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/AuthEndpoint.cs new file mode 100644 index 00000000..5017bff5 --- /dev/null +++ b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/AuthEndpoint.cs @@ -0,0 +1,18 @@ +//#if (UseAdmin) +using FastEndpoints; +using Microsoft.AspNetCore.Authorization; +using NetCorePal.Extensions.Dto; + +namespace ABC.Template.Web.Endpoints.Identity.Admin.UserEndpoints; + +[Tags("Users")] +[Authorize(AuthenticationSchemes = "Bearer")] +[HttpGet("/api/admin/user/auth")] +public class AuthEndpoint : EndpointWithoutRequest> +{ + public override Task HandleAsync(CancellationToken ct) + { + return Send.OkAsync(true.AsResponseData(), cancellation: ct); + } +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/CreateUserEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/CreateUserEndpoint.cs new file mode 100644 index 00000000..80c623d5 --- /dev/null +++ b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/CreateUserEndpoint.cs @@ -0,0 +1,74 @@ +//#if (UseAdmin) +using FastEndpoints; +using MediatR; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using ABC.Template.Domain.AggregatesModel.RoleAggregate; +using ABC.Template.Domain.AggregatesModel.UserAggregate; +using ABC.Template.Domain.AggregatesModel.DeptAggregate; +using ABC.Template.Web.Application.Commands.Identity.Admin.UserCommands; +using ABC.Template.Web.Application.Queries; +using ABC.Template.Web.AppPermissions; +using ABC.Template.Web.Utils; + +namespace ABC.Template.Web.Endpoints.Identity.Admin.UserEndpoints; + +/// +/// 创建用户的请求模型 +/// +/// 用户名 +/// 邮箱地址 +/// 密码 +/// 电话号码 +/// 真实姓名 +/// 用户状态(0=禁用,1=启用) +/// 性别 +/// 出生日期 +/// 部门ID(可选) +/// 部门名称(可选) +/// 要分配的角色ID列表 +public record CreateUserRequest(string Name, string Email, string Password, string Phone, string RealName, int Status, string Gender, DateTimeOffset BirthDate, DeptId? DeptId, string? DeptName, IEnumerable RoleIds); + +/// +/// 创建用户的响应模型 +/// +/// 新创建的用户ID +/// 用户名 +/// 邮箱地址 +public record CreateUserResponse(UserId UserId, string Name, string Email); + +/// +/// 创建用户的API端点 +/// 该端点用于管理员在系统中创建新的用户账户,支持角色分配和组织单位设置 +/// +[Tags("Users")] +public class CreateUserEndpoint(IMediator mediator, RoleQuery roleQuery) : Endpoint> +{ + public override void Configure() + { + Post("/api/admin/users"); + AuthSchemes(JwtBearerDefaults.AuthenticationScheme); + Permissions(PermissionCodes.AllApiAccess, PermissionCodes.UserCreate); + } + + public override async Task HandleAsync(CreateUserRequest request, CancellationToken ct) + { + var rolesToBeAssigned = await roleQuery.GetAdminRolesForAssignmentAsync(request.RoleIds, ct); + var cmd = new CreateUserCommand( + request.Name, + request.Email, + request.Password, + request.Phone, + request.RealName, + request.Status, + request.Gender, + request.BirthDate, + request.DeptId, + request.DeptName, + rolesToBeAssigned + ); + var userId = await mediator.Send(cmd, ct); + var response = new CreateUserResponse(userId, request.Name, request.Email); + await Send.OkAsync(response.AsResponseData(), cancellation: ct); + } +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/DeleteUserEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/DeleteUserEndpoint.cs new file mode 100644 index 00000000..71bd9b5b --- /dev/null +++ b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/DeleteUserEndpoint.cs @@ -0,0 +1,38 @@ +//#if (UseAdmin) +using FastEndpoints; +using MediatR; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using ABC.Template.Domain.AggregatesModel.UserAggregate; +using ABC.Template.Web.Application.Commands.Identity.Admin.UserCommands; +using ABC.Template.Web.AppPermissions; + +namespace ABC.Template.Web.Endpoints.Identity.Admin.UserEndpoints; + +/// +/// 删除用户的请求模型 +/// +/// 要删除的用户ID +public record DeleteUserRequest(UserId UserId); + +/// +/// 删除用户的API端点 +/// 该端点用于从系统中删除指定的用户账户(软删除) +/// +[Tags("Users")] +public class DeleteUserEndpoint(IMediator mediator) : Endpoint> +{ + public override void Configure() + { + Delete("/api/admin/users/{userId}"); + AuthSchemes(JwtBearerDefaults.AuthenticationScheme); + Permissions(PermissionCodes.AllApiAccess, PermissionCodes.UserDelete); + } + + public override async Task HandleAsync(DeleteUserRequest request, CancellationToken ct) + { + var command = new DeleteUserCommand(request.UserId); + await mediator.Send(command, ct); + await Send.OkAsync(true.AsResponseData(), cancellation: ct); + } +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/GetAllUsersEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/GetAllUsersEndpoint.cs new file mode 100644 index 00000000..88d6e8c6 --- /dev/null +++ b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/GetAllUsersEndpoint.cs @@ -0,0 +1,31 @@ +//#if (UseAdmin) +using FastEndpoints; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using ABC.Template.Web.Application.Queries; +using ABC.Template.Web.AppPermissions; + +namespace ABC.Template.Web.Endpoints.Identity.Admin.UserEndpoints; + +/// +/// 获取所有用户信息的API端点 +/// 该端点用于查询系统中的所有用户信息,支持分页、筛选和搜索 +/// +[Tags("Users")] +public class GetAllUsersEndpoint(UserQuery userQuery) : Endpoint>> +{ + + public override void Configure() + { + Get("/api/admin/users"); + AuthSchemes(JwtBearerDefaults.AuthenticationScheme); + Permissions(PermissionCodes.AllApiAccess, PermissionCodes.UserView); + } + + + public override async Task HandleAsync(UserQueryInput req, CancellationToken ct) + { + var result = await userQuery.GetAllUsersAsync(req, ct); + await Send.OkAsync(result.AsResponseData(), cancellation: ct); + } +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/GetUserAccessCodesEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/GetUserAccessCodesEndpoint.cs new file mode 100644 index 00000000..1133be60 --- /dev/null +++ b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/GetUserAccessCodesEndpoint.cs @@ -0,0 +1,58 @@ +//#if (UseAdmin) +using System.Security.Claims; +using FastEndpoints; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using ABC.Template.Domain.AggregatesModel.UserAggregate; +using ABC.Template.Web.Application.Queries; +using ABC.Template.Domain; + +namespace ABC.Template.Web.Endpoints.Identity.Admin.UserEndpoints; + +/// +/// 获取当前用户权限码的API端点 +/// 该端点用于获取当前登录用户的所有权限代码 +/// +[Tags("Users")] +public class GetUserAccessCodesEndpoint(RoleQuery roleQuery, UserQuery userQuery) : EndpointWithoutRequest>> +{ + /// + /// 配置端点的基本设置 + /// 包括HTTP方法、认证方案、权限要求等 + /// + public override void Configure() + { + Get("/api/admin/auth/codes"); + AuthSchemes(JwtBearerDefaults.AuthenticationScheme); + } + + public override async Task HandleAsync(CancellationToken ct) + { + // 从JWT token中获取用户ID + var userIdString = User.FindFirstValue(ClaimTypes.NameIdentifier); + if (string.IsNullOrEmpty(userIdString) || !long.TryParse(userIdString, out var userIdValue)) + { + throw new KnownException("无效的用户身份", ErrorCodes.InvalidUserIdentity); + } + + var userId = new UserId(userIdValue); + + // 获取用户信息(包含角色) + var userInfo = await userQuery.GetUserInfoForLoginByIdAsync(userId, ct); + if (userInfo == null) + { + throw new KnownException("用户不存在", ErrorCodes.UserNotFound); + } + + // 获取用户角色ID列表 + var roleIds = userInfo.UserRoles.Select(r => r.RoleId).ToList(); + + // 查询权限代码(如果用户没有角色,则返回空列表) + var permissionCodes = roleIds.Count > 0 + ? await roleQuery.GetAssignedPermissionCodesAsync(roleIds, ct) + : Enumerable.Empty(); + + // 返回成功响应,使用统一的响应数据格式包装 + await Send.OkAsync(permissionCodes.AsResponseData(), cancellation: ct); + } +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/GetUserEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/GetUserEndpoint.cs new file mode 100644 index 00000000..836950d8 --- /dev/null +++ b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/GetUserEndpoint.cs @@ -0,0 +1,43 @@ +//#if (UseAdmin) +using FastEndpoints; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using ABC.Template.Domain.AggregatesModel.UserAggregate; +using ABC.Template.Web.Application.Queries; +using ABC.Template.Web.AppPermissions; + +namespace ABC.Template.Web.Endpoints.Identity.Admin.UserEndpoints; + +/// +/// 获取用户信息的请求模型 +/// +/// 要查询的用户ID +public record GetUserRequest(UserId Id); + +/// +/// 获取用户信息的API端点 +/// 该端点用于根据用户ID查询用户的详细信息 +/// +[Tags("Users")] +public class GetUserEndpoint(UserQuery userQuery) : Endpoint> +{ + public override void Configure() + { + Get("/api/admin/users/{id}"); + AuthSchemes(JwtBearerDefaults.AuthenticationScheme); + Permissions(PermissionCodes.AllApiAccess, PermissionCodes.UserView); + } + + public override async Task HandleAsync(GetUserRequest req, CancellationToken ct) + { + var userInfo = await userQuery.GetUserByIdAsync(req.Id, ct); + if (userInfo == null) + { + await Send.NotFoundAsync(ct); + } + else + { + await Send.OkAsync(userInfo.AsResponseData(), cancellation: ct); + } + } +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/GetUserProfileEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/GetUserProfileEndpoint.cs new file mode 100644 index 00000000..5549eac5 --- /dev/null +++ b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/GetUserProfileEndpoint.cs @@ -0,0 +1,75 @@ +//#if (UseAdmin) +using FastEndpoints; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using ABC.Template.Domain.AggregatesModel.UserAggregate; +using ABC.Template.Domain.AggregatesModel.DeptAggregate; +using ABC.Template.Web.Application.Queries; +using ABC.Template.Domain; +using ABC.Template.Web.AppPermissions; + +namespace ABC.Template.Web.Endpoints.Identity.Admin.UserEndpoints; + +/// +/// 获取用户资料的请求模型 +/// +/// 要查询的用户ID +public record GetUserProfileRequest(UserId UserId); + +/// +/// 用户资料的响应模型 +/// +/// 用户ID +/// 用户名 +/// 电话号码 +/// 用户角色列表 +/// 真实姓名 +/// 用户状态 +/// 邮箱地址 +/// 创建时间 +/// 性别 +/// 年龄 +/// 出生日期 + /// 部门ID(可为空) + /// 部门名称 +public record UserProfileResponse(UserId UserId, string Name, string Phone, IEnumerable Roles, string RealName, int Status, string Email, DateTimeOffset CreatedAt, string Gender, int Age, DateTimeOffset BirthDate, DeptId? DeptId, string DeptName); + +/// +/// 获取用户资料的API端点 +/// 该端点用于根据用户ID查询用户的详细资料信息,包括基本信息、角色、状态和组织单位等 +/// +[Tags("Users")] +public class GetUserProfileEndpoint(UserQuery userQuery) : Endpoint> +{ + public override void Configure() + { + Get("/api/admin/user/profile/{userId}"); + AuthSchemes(JwtBearerDefaults.AuthenticationScheme); + Permissions(PermissionCodes.AllApiAccess, PermissionCodes.UserView); + } + + public override async Task HandleAsync(GetUserProfileRequest req, CancellationToken ct) + { + var userInfo = await userQuery.GetUserByIdAsync(req.UserId, ct); + if (userInfo == null) + { + throw new KnownException("无效的用户", ErrorCodes.InvalidUser); + } + var response = new UserProfileResponse( + userInfo.UserId, + userInfo.Name, + userInfo.Phone, + userInfo.Roles, + userInfo.RealName, + userInfo.Status, + userInfo.Email, + userInfo.CreatedAt, + userInfo.Gender, + userInfo.Age, + userInfo.BirthDate, + userInfo.DeptId, + userInfo.DeptName + ); + await Send.OkAsync(response.AsResponseData(), cancellation: ct); + } +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/LoginEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/LoginEndpoint.cs new file mode 100644 index 00000000..58892e75 --- /dev/null +++ b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/LoginEndpoint.cs @@ -0,0 +1,96 @@ +//#if (UseAdmin) +using System.Security.Claims; +using System.Text.Json; +using FastEndpoints; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Options; +using NetCorePal.Extensions.Dto; +using NetCorePal.Extensions.Jwt; +using ABC.Template.Domain.AggregatesModel.UserAggregate; +using ABC.Template.Web.Application.Commands.Identity.Admin.UserCommands; +using ABC.Template.Web.Application.Queries; +using ABC.Template.Domain; +using ABC.Template.Web.Utils; +using Serilog; + +namespace ABC.Template.Web.Endpoints.Identity.Admin.UserEndpoints; + +public record LoginRequest(string Username, string Password); + +public record LoginResponse(string Token, string RefreshToken, UserId UserId, string Name, string Email, string Roles, IEnumerable PermissionCodes, DateTimeOffset TokenExpiryTime); + +[Tags("Users")] +[HttpPost("/api/admin/user/login")] +[AllowAnonymous] +public class LoginEndpoint(IMediator mediator, UserQuery userQuery, IJwtProvider jwtProvider, IOptions appConfiguration, RoleQuery roleQuery) : Endpoint> +{ + private const string PermissionClaimType = "permissions"; + + public override async Task HandleAsync(LoginRequest req, CancellationToken ct) + { + Log.Information("用户登录尝试: Username={Username}", req.Username); + + var loginInfo = await userQuery.GetUserInfoForLoginAsync(req.Username, ct); + + if (loginInfo == null || !PasswordHasher.VerifyHashedPassword(req.Password, loginInfo.PasswordHash)) + { + throw new KnownException("用户名或密码错误", ErrorCodes.UserNameOrPasswordError); + } + + var nowTime = DateTimeOffset.UtcNow; + var tokenExpiryTime = nowTime.AddMinutes(appConfiguration.Value.TokenExpiryInMinutes); + var refreshToken = TokenGenerator.GenerateRefreshToken(); + var roles = loginInfo.UserRoles.Select(r => r.RoleId).ToList(); + var assignedPermissionCodes = roles.Count > 0 + ? await roleQuery.GetAssignedPermissionCodesAsync(roles, ct) + : Enumerable.Empty(); + var claims = BuildClaims(loginInfo, assignedPermissionCodes); + var config = appConfiguration.Value; + var token = await jwtProvider.GenerateJwtToken( + new JwtData( + config.JwtIssuer, + config.JwtAudience, + claims, + nowTime.UtcDateTime, + tokenExpiryTime.UtcDateTime), + ct); + var response = new LoginResponse( + token, + refreshToken, + loginInfo.UserId, + loginInfo.Name, + loginInfo.Email, + JsonSerializer.Serialize(roles) ?? "[]", + assignedPermissionCodes, + tokenExpiryTime + ); + + // 更新用户登录时间和刷新令牌 + var updateCmd = new UpdateUserLoginTimeCommand(loginInfo.UserId, nowTime, refreshToken); + await mediator.Send(updateCmd, ct); + + Log.Information("用户登录成功: UserId={UserId}, Username={Username}, Email={Email}, RoleCount={RoleCount}, PermissionCount={PermissionCount}", + loginInfo.UserId, loginInfo.Name, loginInfo.Email, roles.Count, assignedPermissionCodes.Count()); + + await Send.OkAsync(response.AsResponseData(), cancellation: ct); + } + + /// + /// 构建JWT Claims + /// + private static List BuildClaims(UserLoginInfoQueryDto loginInfo, IEnumerable permissionCodes) + { + var userIdString = loginInfo.UserId.ToString(); + var claims = new List + { + new(ClaimTypes.Name, loginInfo.Name), + new(ClaimTypes.Email, loginInfo.Email), + new(ClaimTypes.NameIdentifier, userIdString) + }; + + claims.AddRange(permissionCodes.Select(code => new Claim(PermissionClaimType, code))); + + return claims; + } +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/LogoutEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/LogoutEndpoint.cs new file mode 100644 index 00000000..54a0233f --- /dev/null +++ b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/LogoutEndpoint.cs @@ -0,0 +1,56 @@ +//#if (UseAdmin) +using System.Security.Claims; +using FastEndpoints; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using NetCorePal.Extensions.Dto; +using ABC.Template.Domain.AggregatesModel.UserAggregate; +using ABC.Template.Web.Application.Commands.Identity.Admin.UserCommands; +using Serilog; + +namespace ABC.Template.Web.Endpoints.Identity.Admin.UserEndpoints; + +/// +/// 用户退出登录端点 +/// +[Tags("Users")] +public class LogoutEndpoint(IMediator mediator) : EndpointWithoutRequest> +{ + public override void Configure() + { + Post("/api/admin/auth/logout"); + AuthSchemes(JwtBearerDefaults.AuthenticationScheme); + AllowAnonymous(); // 允许匿名访问,因为token可能已过期 + } + + public override async Task HandleAsync(CancellationToken ct) + { + // 尝试从JWT token中获取用户ID(如果token有效) + var userIdString = User.FindFirstValue(ClaimTypes.NameIdentifier); + if (!string.IsNullOrEmpty(userIdString) && long.TryParse(userIdString, out var userIdValue)) + { + var userId = new UserId(userIdValue); + + try + { + // 撤销用户的所有刷新令牌,防止token被继续使用 + var revokeCmd = new RevokeUserRefreshTokensCommand(userId); + await mediator.Send(revokeCmd, ct); + + Log.Information("用户退出登录成功: UserId={UserId}", userId); + } + catch (Exception ex) + { + // 即使撤销token失败,也记录日志但不影响退出流程 + Log.Warning(ex, "用户退出登录时撤销刷新令牌失败: UserId={UserId}", userId); + } + } + else + { + Log.Information("用户退出登录: Token已失效或未提供"); + } + + // 返回成功响应 + await Send.OkAsync(true.AsResponseData(), cancellation: ct); + } +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/PasswordResetEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/PasswordResetEndpoint.cs new file mode 100644 index 00000000..4d3e124b --- /dev/null +++ b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/PasswordResetEndpoint.cs @@ -0,0 +1,47 @@ +//#if (UseAdmin) +using FastEndpoints; +using MediatR; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using ABC.Template.Domain.AggregatesModel.UserAggregate; +using ABC.Template.Web.Application.Commands.Identity.Admin.UserCommands; +using ABC.Template.Web.AppPermissions; +using ABC.Template.Web.Utils; + +namespace ABC.Template.Web.Endpoints.Identity.Admin.UserEndpoints; + +/// +/// 密码重置的请求模型 +/// +/// 需要重置密码的用户ID +public record PasswordResetRequest(UserId UserId); + +/// +/// 密码重置的响应模型 +/// +/// 已重置密码的用户ID +public record PasswordResetResponse(UserId UserId); + +/// +/// 密码重置的API端点 +/// 该端点用于重置指定用户的密码为默认密码(123456) +/// +[Tags("Users")] +public class PasswordResetEndpoint(IMediator mediator) : Endpoint> +{ + public override void Configure() + { + Put("/api/admin/user/password-reset"); + AuthSchemes(JwtBearerDefaults.AuthenticationScheme); + Permissions(PermissionCodes.AllApiAccess, PermissionCodes.UserEdit); + } + + public override async Task HandleAsync(PasswordResetRequest request, CancellationToken ct) + { + var passwordHash = PasswordHasher.HashPassword("123456"); + var cmd = new PasswordResetCommand(request.UserId, passwordHash); + var userId = await mediator.Send(cmd, ct); + var response = new PasswordResetResponse(userId); + await Send.OkAsync(response.AsResponseData(), cancellation: ct); + } +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/RegisterEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/RegisterEndpoint.cs new file mode 100644 index 00000000..a5c8188a --- /dev/null +++ b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/RegisterEndpoint.cs @@ -0,0 +1,73 @@ +//#if (UseAdmin) +using FastEndpoints; +using MediatR; +using ABC.Template.Domain.AggregatesModel.RoleAggregate; +using ABC.Template.Domain.AggregatesModel.UserAggregate; +using ABC.Template.Domain.AggregatesModel.DeptAggregate; +using ABC.Template.Web.Application.Commands.Identity.Admin.UserCommands; +using ABC.Template.Web.Application.Queries; +using ABC.Template.Web.Utils; + +namespace ABC.Template.Web.Endpoints.Identity.Admin.UserEndpoints; + +/// +/// 用户注册的请求模型 +/// +/// 用户名 +/// 邮箱地址 +/// 密码 +/// 电话号码 +/// 真实姓名 +/// 用户状态 +/// 性别 +/// 年龄 +/// 出生日期 + /// 部门ID(可选) + /// 部门名称(可选) + /// 要分配的角色ID列表 +public record RegisterRequest(string Name, string Email, string Password, string Phone, string RealName, int Status, string Gender, int Age, DateTimeOffset BirthDate, DeptId? DeptId, string? DeptName, IEnumerable RoleIds); + +/// +/// 用户注册的响应模型 +/// +/// 新创建的用户ID +/// 用户名 +/// 邮箱地址 +public record RegisterResponse(UserId UserId, string Name, string Email); + +/// +/// 用户注册的API端点 +/// 该端点用于在系统中创建新的用户账户,支持角色分配和组织单位设置 +/// +[Tags("Users")] +public class RegisterEndpoint(IMediator mediator, RoleQuery roleQuery) : Endpoint> +{ + public override void Configure() + { + Post("/api/admin/user/register"); + AllowAnonymous(); + } + + public override async Task HandleAsync(RegisterRequest request, CancellationToken ct) + { + var rolesToBeAssigned = await roleQuery.GetAdminRolesForAssignmentAsync(request.RoleIds, ct); + var passwordHash = PasswordHasher.HashPassword(request.Password); + var cmd = new CreateUserCommand( + request.Name, + request.Email, + passwordHash, + request.Phone, + request.RealName, + request.Status, + request.Gender, + request.BirthDate, + request.DeptId, + request.DeptName, + rolesToBeAssigned + ); + var userId = await mediator.Send(cmd, ct); + var response = new RegisterResponse(userId, request.Name, request.Email); + await Send.OkAsync(response.AsResponseData(), cancellation: ct); + } +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/UpdateUserEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/UpdateUserEndpoint.cs new file mode 100644 index 00000000..f9f4726c --- /dev/null +++ b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/UpdateUserEndpoint.cs @@ -0,0 +1,78 @@ +//#if (UseAdmin) +using FastEndpoints; +using MediatR; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using ABC.Template.Domain.AggregatesModel.UserAggregate; +using ABC.Template.Domain.AggregatesModel.DeptAggregate; +using ABC.Template.Web.Application.Commands.Identity.Admin.UserCommands; +using ABC.Template.Web.AppPermissions; +using ABC.Template.Web.Utils; + +namespace ABC.Template.Web.Endpoints.Identity.Admin.UserEndpoints; + +/// +/// 更新用户信息的请求模型 +/// +/// 要更新的用户ID +/// 用户名 +/// 邮箱地址 +/// 电话号码 +/// 真实姓名 +/// 用户状态 +/// 性别 +/// 年龄 +/// 出生日期 + /// 部门ID + /// 部门名称 + /// 密码(可选,为空则不更新) +public record UpdateUserRequest(UserId UserId, string Name, string Email, string Phone, string RealName, int Status, string Gender, int Age, DateTimeOffset BirthDate, DeptId DeptId, string DeptName, string Password); + +/// +/// 更新用户信息的响应模型 +/// +/// 已更新的用户ID +/// 用户名 +/// 邮箱地址 +public record UpdateUserResponse(UserId UserId, string Name, string Email); + +/// +/// 更新用户信息的API端点 +/// 该端点用于修改现有用户的基本信息,包括个人信息、组织单位和密码 +/// +[Tags("Users")] +public class UpdateUserEndpoint(IMediator mediator) : Endpoint> +{ + public override void Configure() + { + Put("/api/admin/user/update"); + AuthSchemes(JwtBearerDefaults.AuthenticationScheme); + Permissions(PermissionCodes.AllApiAccess, PermissionCodes.UserEdit); + } + + public override async Task HandleAsync(UpdateUserRequest request, CancellationToken ct) + { + var passwordHash = string.Empty; + if (!string.IsNullOrWhiteSpace(request.Password)) + { + passwordHash = PasswordHasher.HashPassword(request.Password); + } + var cmd = new UpdateUserCommand( + request.UserId, + request.Name, + request.Email, + request.Phone, + request.RealName, + request.Status, + request.Gender, + request.Age, + request.BirthDate, + request.DeptId, + request.DeptName, + passwordHash + ); + var userId = await mediator.Send(cmd, ct); + var response = new UpdateUserResponse(userId, request.Name, request.Email); + await Send.OkAsync(response.AsResponseData(), cancellation: ct); + } +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/UpdateUserRolesEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/UpdateUserRolesEndpoint.cs new file mode 100644 index 00000000..55c84900 --- /dev/null +++ b/template/src/ABC.Template.Web/Endpoints/Identity/Admin/UserEndpoints/UpdateUserRolesEndpoint.cs @@ -0,0 +1,49 @@ +//#if (UseAdmin) +using FastEndpoints; +using MediatR; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using ABC.Template.Domain.AggregatesModel.UserAggregate; +using ABC.Template.Domain.AggregatesModel.RoleAggregate; +using ABC.Template.Web.Application.Commands.Identity.Admin.UserCommands; +using ABC.Template.Web.Application.Queries; +using ABC.Template.Web.AppPermissions; + +namespace ABC.Template.Web.Endpoints.Identity.Admin.UserEndpoints; + +/// +/// 更新用户角色的请求模型 +/// +/// 要更新角色的用户ID +/// 要分配给用户的角色ID列表 +public record UpdateUserRolesRequest(UserId UserId, IEnumerable RoleIds); + +/// +/// 更新用户角色的响应模型 +/// +/// 已更新角色的用户ID +public record UpdateUserRolesResponse(UserId UserId); + +/// +/// 更新用户角色的API端点 +/// 该端点用于修改指定用户的角色分配,支持批量角色分配 +/// +[Tags("Users")] +public class UpdateUserRolesEndpoint(IMediator mediator, RoleQuery roleQuery) : Endpoint> +{ + public override void Configure() + { + Put("/api/admin/users/update-roles"); + AuthSchemes(JwtBearerDefaults.AuthenticationScheme); + Permissions(PermissionCodes.AllApiAccess, PermissionCodes.UserRoleAssign); + } + + public override async Task HandleAsync(UpdateUserRolesRequest request, CancellationToken ct) + { + var rolesToBeAssigned = await roleQuery.GetAdminRolesForAssignmentAsync(request.RoleIds, ct); + var cmd = new UpdateUserRolesCommand(request.UserId, rolesToBeAssigned); + await mediator.Send(cmd, ct); + var response = new UpdateUserRolesResponse(request.UserId); + await Send.OkAsync(response.AsResponseData(), cancellation: ct); + } +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/OrderEndpoints/CreateOrderEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/OrderEndpoints/CreateOrderEndpoint.cs index 9611656b..d20dd239 100644 --- a/template/src/ABC.Template.Web/Endpoints/OrderEndpoints/CreateOrderEndpoint.cs +++ b/template/src/ABC.Template.Web/Endpoints/OrderEndpoints/CreateOrderEndpoint.cs @@ -1,3 +1,4 @@ +//#if (UseDemoCode) using ABC.Template.Domain.AggregatesModel.OrderAggregate; using ABC.Template.Web.Application.Commands.Orders; using FastEndpoints; @@ -20,4 +21,5 @@ public override async Task HandleAsync(CreateOrderRequest req, CancellationToken var id = await mediator.Send(cmd, ct); await Send.OkAsync(id.AsResponseData(), cancellation: ct); } -} \ No newline at end of file +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/OrderEndpoints/GetOrderByIdEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/OrderEndpoints/GetOrderByIdEndpoint.cs index 57579de3..73929ec7 100644 --- a/template/src/ABC.Template.Web/Endpoints/OrderEndpoints/GetOrderByIdEndpoint.cs +++ b/template/src/ABC.Template.Web/Endpoints/OrderEndpoints/GetOrderByIdEndpoint.cs @@ -1,3 +1,4 @@ +//#if (UseDemoCode) using ABC.Template.Domain.AggregatesModel.OrderAggregate; using ABC.Template.Web.Application.Queries.Orders; using FastEndpoints; @@ -19,3 +20,4 @@ public override async Task HandleAsync(GetOrderByIdRequest req, CancellationToke await Send.OkAsync(order.AsResponseData(), cancellation: ct); } } +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Endpoints/OrderEndpoints/PayOrderEndpoint.cs b/template/src/ABC.Template.Web/Endpoints/OrderEndpoints/PayOrderEndpoint.cs index b7f7a2ac..cedc5f8c 100644 --- a/template/src/ABC.Template.Web/Endpoints/OrderEndpoints/PayOrderEndpoint.cs +++ b/template/src/ABC.Template.Web/Endpoints/OrderEndpoints/PayOrderEndpoint.cs @@ -1,3 +1,4 @@ +//#if (UseDemoCode) using ABC.Template.Domain.AggregatesModel.OrderAggregate; using ABC.Template.Web.Application.Commands.Orders; using FastEndpoints; @@ -19,4 +20,5 @@ public override async Task HandleAsync(PayOrderRequest req, CancellationToken ct await mediator.Send(new PayOrderCommand(req.Id), ct); await Send.OkAsync(true.AsResponseData(), cancellation: ct); } -} \ No newline at end of file +} +//#endif \ No newline at end of file diff --git a/template/src/ABC.Template.Web/Extensions/ServiceCollectionExtensions.cs b/template/src/ABC.Template.Web/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..76703fa7 --- /dev/null +++ b/template/src/ABC.Template.Web/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,33 @@ +//#if (UseAdmin) +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using ABC.Template.Web.Application.Queries; + +namespace ABC.Template.Web.Extensions; + +/// +/// 服务集合扩展方法 +/// +public static class ServiceCollectionExtensions +{ + /// + /// 自动注册所有实现 IQuery 接口的查询类 + /// 参考框架的 AddRepositories 模式 + /// + /// 服务集合 + /// 要扫描的程序集 + /// 服务集合 + public static IServiceCollection AddQueries(this IServiceCollection services, Assembly assembly) + { + var queryTypes = assembly.GetTypes() + .Where(t => t.IsClass && !t.IsAbstract && typeof(IQuery).IsAssignableFrom(t)); + + foreach (var queryType in queryTypes) + { + services.AddScoped(queryType); + } + + return services; + } +} +//#endif diff --git a/template/src/ABC.Template.Web/Program.cs b/template/src/ABC.Template.Web/Program.cs index b99d5d03..9f7ab3b2 100644 --- a/template/src/ABC.Template.Web/Program.cs +++ b/template/src/ABC.Template.Web/Program.cs @@ -9,6 +9,9 @@ using ABC.Template.Web.Application.IntegrationEventHandlers; using ABC.Template.Web.Clients; using ABC.Template.Web.Extensions; +//#if (UseAdmin) +using ABC.Template.Web.Utils; +//#endif using FastEndpoints; using Serilog; using Serilog.Formatting.Json; @@ -91,6 +94,26 @@ #endregion +//#if (UseAdmin) + #region CORS + + var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get() + ?? new[] { "http://localhost:5666", "http://localhost:5173", "http://localhost:3000" }; + + builder.Services.AddCors(options => + { + options.AddDefaultPolicy(policy => + { + policy.WithOrigins(allowedOrigins) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials(); + }); + }); + + #endregion +//#endif + #region Controller builder.Services.AddControllers().AddNetCorePalSystemTextJson(); @@ -116,6 +139,13 @@ #endregion +//#if (UseAdmin) + #region Query + // 自动注册所有实现 IQuery 接口的查询类 + builder.Services.AddQueries(Assembly.GetExecutingAssembly()); + #endregion +//#endif + #region 基础设施 builder.Services.AddRepositories(typeof(ApplicationDbContext).Assembly); @@ -399,7 +429,11 @@ app.UseStaticFiles(); //app.UseHttpsRedirection(); +//#if (UseAdmin) + app.UseCors(); // CORS 必须在 UseRouting 之前 +//#endif app.UseRouting(); + app.UseAuthentication(); // Authentication 必须在 Authorization 之前 app.UseAuthorization(); app.MapControllers(); @@ -423,15 +457,23 @@ // Code analysis endpoint app.MapGet("/code-analysis", () => { + var assemblies = new List { typeof(Program).Assembly, typeof(ApplicationDbContext).Assembly }; +//#if (UseDemoCode) + assemblies.Add(typeof(ABC.Template.Domain.AggregatesModel.OrderAggregate.Order).Assembly); +//#endif +//#if (UseAdmin) + assemblies.Add(typeof(ABC.Template.Domain.AggregatesModel.UserAggregate.User).Assembly); +//#endif var html = VisualizationHtmlBuilder.GenerateVisualizationHtml( - CodeFlowAnalysisHelper.GetResultFromAssemblies(typeof(Program).Assembly, - typeof(ApplicationDbContext).Assembly, - typeof(ABC.Template.Domain.AggregatesModel.OrderAggregate.Order).Assembly) + CodeFlowAnalysisHelper.GetResultFromAssemblies(assemblies.ToArray()) ); return Results.Content(html, "text/html; charset=utf-8"); }); app.UseHangfireDashboard(); +//#if (UseAdmin) + app.SeedDatabase(); +//#endif await app.RunAsync(); } catch (Exception ex) diff --git a/template/src/ABC.Template.Web/Utils/AppConfiguration.cs b/template/src/ABC.Template.Web/Utils/AppConfiguration.cs new file mode 100644 index 00000000..69fba385 --- /dev/null +++ b/template/src/ABC.Template.Web/Utils/AppConfiguration.cs @@ -0,0 +1,19 @@ +//#if (UseAdmin) +namespace ABC.Template.Web.Utils; + +public class AppConfiguration +{ + public string Secret { get; set; } = string.Empty; + public int TokenExpiryInMinutes { get; set; } + + /// + /// JWT Issuer(签发者) + /// + public string JwtIssuer { get; set; } = "netcorepal"; + + /// + /// JWT Audience(受众) + /// + public string JwtAudience { get; set; } = "netcorepal"; +} +//#endif diff --git a/template/src/ABC.Template.Web/Utils/PasswordHasher.cs b/template/src/ABC.Template.Web/Utils/PasswordHasher.cs new file mode 100644 index 00000000..4e900997 --- /dev/null +++ b/template/src/ABC.Template.Web/Utils/PasswordHasher.cs @@ -0,0 +1,57 @@ +//#if (UseAdmin) +using Microsoft.AspNetCore.Cryptography.KeyDerivation; +using System.Security.Cryptography; +using System.Text; + +namespace ABC.Template.Web.Utils; + +public static class PasswordHasher +{ + private static string HashPassword(string value, string salt) + { + var valueBytes = KeyDerivation.Pbkdf2( + password: value, + salt: Encoding.UTF8.GetBytes(salt), + prf: KeyDerivationPrf.HMACSHA512, + iterationCount: 100000, + numBytesRequested: 256 / 8); + + return Convert.ToBase64String(valueBytes); + } + + private static string GenerateSalt() + { + var randomBytes = new byte[128 / 8]; + using var generator = RandomNumberGenerator.Create(); + generator.GetBytes(randomBytes); + return Convert.ToBase64String(randomBytes); + } + + public static string HashPassword(string password) + { + var salt = GenerateSalt(); + var hash = HashPassword(password, salt); + var result = $"{salt}.{hash}"; + return result; + } + + public static bool VerifyHashedPassword(string password, string storePassword) + { + if (string.IsNullOrEmpty(password)) + { + throw new ArgumentNullException(nameof(password)); + } + + if (string.IsNullOrEmpty(storePassword)) + { + throw new ArgumentNullException(nameof(storePassword)); + } + + var parts = storePassword.Split('.'); + var salt = parts[0]; + var hash = parts[1]; + + return HashPassword(password, salt) == hash; + } +} +//#endif diff --git a/template/src/ABC.Template.Web/Utils/SeedDatabaseExtension.cs b/template/src/ABC.Template.Web/Utils/SeedDatabaseExtension.cs new file mode 100644 index 00000000..397c9134 --- /dev/null +++ b/template/src/ABC.Template.Web/Utils/SeedDatabaseExtension.cs @@ -0,0 +1,144 @@ +//#if (UseAdmin) +using ABC.Template.Domain.AggregatesModel.DeptAggregate; +using ABC.Template.Domain.AggregatesModel.RoleAggregate; +using ABC.Template.Domain.AggregatesModel.UserAggregate; +using ABC.Template.Infrastructure; +using ABC.Template.Web.AppPermissions; + +namespace ABC.Template.Web.Utils; + +/// +/// 数据库种子数据扩展方法 +/// 用于在开发环境中初始化基础数据(角色、权限、组织架构、用户等) +/// +public static class SeedDatabaseExtension +{ + /// + /// 初始化数据库种子数据 + /// 包括:角色和权限、组织架构、管理员用户、测试用户 + /// + /// 应用程序构建器 + /// 应用程序构建器 + internal static IApplicationBuilder SeedDatabase(this IApplicationBuilder app) + { + using var serviceScope = app.ApplicationServices.CreateScope(); + var dbContext = serviceScope.ServiceProvider.GetRequiredService(); + + // 初始化角色和权限 + if (!dbContext.Roles.Any()) + { + var adminPermissions = new List + { + // 父权限码(用于菜单和路由权限控制) + new RolePermission(PermissionCodes.UserManagement, "用户管理", "用户管理"), + new RolePermission(PermissionCodes.RoleManagement, "角色管理", "角色管理"), + new RolePermission(PermissionCodes.DeptManagement, "部门管理", "部门管理"), + + // 用户管理权限 + new RolePermission(PermissionCodes.UserCreate, "创建用户", "创建新用户"), + new RolePermission(PermissionCodes.UserView, "查看用户", "查看用户信息"), + new RolePermission(PermissionCodes.UserEdit, "更新用户", "更新用户信息"), + new RolePermission(PermissionCodes.UserDelete, "删除用户", "删除用户"), + new RolePermission(PermissionCodes.UserRoleAssign, "分配用户角色", "分配用户角色权限"), + new RolePermission(PermissionCodes.UserResetPassword, "重置用户密码", "重置用户密码"), + + // 角色管理权限 + new RolePermission(PermissionCodes.RoleCreate, "创建角色", "创建新角色"), + new RolePermission(PermissionCodes.RoleView, "查看角色", "查看角色信息"), + new RolePermission(PermissionCodes.RoleEdit, "更新角色", "更新角色信息"), + new RolePermission(PermissionCodes.RoleDelete, "删除角色", "删除角色"), + new RolePermission(PermissionCodes.RoleUpdatePermissions, "更新角色权限", "更新角色的权限"), + + // 部门管理权限 + new RolePermission(PermissionCodes.DeptCreate, "创建部门", "创建部门"), + new RolePermission(PermissionCodes.DeptView, "查看部门", "查看部门信息"), + new RolePermission(PermissionCodes.DeptEdit, "更新部门", "更新部门信息"), + new RolePermission(PermissionCodes.DeptDelete, "删除部门", "删除部门"), + + // 所有接口访问权限 + new RolePermission(PermissionCodes.AllApiAccess, "所有接口访问权限", "所有接口访问权限"), + }; + + var userPermissions = new List + { + new RolePermission(PermissionCodes.UserView, "查看用户", "查看用户信息"), + new RolePermission(PermissionCodes.UserEdit, "更新用户", "更新自己的用户信息"), + new RolePermission(PermissionCodes.AllApiAccess, "所有接口访问权限", "所有接口访问权限"), + }; + + var adminRole = new Role("管理员", "系统管理员", adminPermissions); + var userRole = new Role("普通用户", "普通用户", userPermissions); + + dbContext.Roles.Add(adminRole); + dbContext.Roles.Add(userRole); + dbContext.SaveChanges(); + } + + // 初始化部门 + if (!dbContext.Depts.Any()) + { + var dept = new Dept("研发", "根节点", new DeptId(0), 1); + + dbContext.Depts.Add(dept); + dbContext.SaveChanges(); + + var deptId = dbContext.Depts.First(r => r.Name == "研发").Id; + var childGroup = new Dept("Net", "第一个子节点", deptId, 1); + dbContext.Depts.Add(childGroup); + dbContext.SaveChanges(); + + var childGroupId = dbContext.Depts.First(r => r.Name == "Net").Id; + var childIndividual = new Dept("C#", "第一个子节点的子节点", childGroupId, 1); + dbContext.Depts.Add(childIndividual); + dbContext.SaveChanges(); + } + + // 初始化管理员用户 + if (!dbContext.Users.Any(u => u.Name == "admin")) + { + var dept = dbContext.Depts.First(r => r.Name == "研发"); + var adminRole = dbContext.Roles.First(r => r.Name == "管理员"); + var adminUser = new User( + "admin", + "13800138000", + PasswordHasher.HashPassword("123456"), + new List { new UserRole(adminRole.Id, adminRole.Name) }, + "系统管理员", + 1, + "admin@example.com", + "男", + DateTimeOffset.Now.AddYears(-30) // 假设管理员年龄为30岁 + ); + + // 设置部门关系 + adminUser.AssignDept(new UserDept(adminUser.Id, dept.Id, dept.Name)); + dbContext.Users.Add(adminUser); + dbContext.SaveChanges(); + } + + // 初始化测试用户 + if (!dbContext.Users.Any(u => u.Name == "test")) + { + var dept = dbContext.Depts.First(r => r.Name == "研发"); + var userRole = dbContext.Roles.First(r => r.Name == "普通用户"); + var testUser = new User( + "test", + "13800138001", + PasswordHasher.HashPassword("123456"), + new List { new UserRole(userRole.Id, userRole.Name) }, + "测试用户", + 1, + "test@example.com", + "女", + DateTimeOffset.Now.AddYears(-25) // 假设测试用户年龄为25岁 + ); + + testUser.AssignDept(new UserDept(testUser.Id, dept.Id, dept.Name)); + dbContext.Users.Add(testUser); + dbContext.SaveChanges(); + } + + return app; + } +} +//#endif diff --git a/template/src/ABC.Template.Web/Utils/TokenGenerator.cs b/template/src/ABC.Template.Web/Utils/TokenGenerator.cs new file mode 100644 index 00000000..f22253fe --- /dev/null +++ b/template/src/ABC.Template.Web/Utils/TokenGenerator.cs @@ -0,0 +1,25 @@ +//#if (UseAdmin) +using System.Security.Cryptography; + +namespace ABC.Template.Web.Utils; + +public static class TokenGenerator +{ + public static string GenerateCryptographicallySecureGuid() + { + using var rng = RandomNumberGenerator.Create(); + var bytes = new byte[16]; + rng.GetBytes(bytes); + return new Guid(bytes).ToString("N"); + } + + public static string GenerateRefreshToken() + { + var randomNumber = new byte[32]; + using var rnd = RandomNumberGenerator.Create(); + rnd.GetBytes(randomNumber); + + return Convert.ToBase64String(randomNumber); + } +} +//#endif diff --git a/template/src/ABC.Template.Web/appsettings.Development.json b/template/src/ABC.Template.Web/appsettings.Development.json index f1a56b66..aa761fb8 100644 --- a/template/src/ABC.Template.Web/appsettings.Development.json +++ b/template/src/ABC.Template.Web/appsettings.Development.json @@ -47,6 +47,21 @@ "Pulsar": { "ServiceUrl": "pulsar://localhost:6650" }, +//#endif +//#if (UseAdmin) + "Cors": { + "AllowedOrigins": [ + "http://localhost:5666", + "http://localhost:5173", + "http://localhost:3000" + ] + }, + "AppConfiguration": { + "Secret": "your-secret-key-here", + "TokenExpiryInMinutes": 1440, + "JwtIssuer": "netcorepal", + "JwtAudience": "netcorepal" + }, //#endif "Services": { "user": { diff --git a/template/src/ABC.Template.Web/appsettings.json b/template/src/ABC.Template.Web/appsettings.json index ec04bc12..9345c0c6 100644 --- a/template/src/ABC.Template.Web/appsettings.json +++ b/template/src/ABC.Template.Web/appsettings.json @@ -5,5 +5,85 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", +//#if (UseAdmin) + "Cors": { + "AllowedOrigins": [ + "http://localhost:5666", + "http://localhost:5173", + "http://localhost:3000" + ] + }, + "AppConfiguration": { + "Secret": "your-secret-key-here", + "TokenExpiryInMinutes": 1440, + "JwtIssuer": "netcorepal", + "JwtAudience": "netcorepal" + }, +//#endif + "ConnectionStrings": { +//#if (UseMySql) + "MySql": "Server=localhost;User ID=root;Password=123456;Database=dev;SslMode=none", +//#elif (UseSqlServer) + "SqlServer": "Server=localhost;Database=dev;User ID=sa;Password=Test123456!;TrustServerCertificate=true", +//#elif (UsePostgreSQL) + "PostgreSQL": "Host=localhost;Database=dev;Username=postgres;Password=123456", +//#elif (UseSqlite) + "Sqlite": "Data Source=demo.db", +//#elif (UseGaussDB) + "GaussDB": "Host=localhost;Database=dev;Username=postgres;Password=123456", +//#elif (UseDMDB) + "DMDB": "Server=localhost;User ID=SYSDBA;Password=SYSDBA;Database=dev", +//#elif (UseMongoDB) + "MongoDB": "mongodb://localhost:27017/dev", +//#endif + "Redis": "localhost:6379" + }, +//#if (UseRabbitMQ) + "RabbitMQ": { + "HostName": "localhost", + "UserName": "guest", + "Password": "guest", + "VirtualHost": "/", + "Port": 5672 + }, +//#elif (UseKafka) + "Kafka": { + "Servers": "localhost:9092" + }, +//#elif (UseAzureServiceBus) + "AzureServiceBus": { + "ConnectionString": "Endpoint=sb://your-servicebus.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=your-key" + }, +//#elif (UseAmazonSQS) + "AmazonSQS": { + "Region": "us-east-1", + "AccessKey": "your-access-key", + "SecretKey": "your-secret-key" + }, +//#elif (UseNATS) + "NATS": { + "Servers": "nats://localhost:4222" + }, +//#elif (UseRedisStreams) + "RedisStreams": { + "ConnectionString": "localhost:6379" + }, +//#elif (UsePulsar) + "Pulsar": { + "ServiceUrl": "pulsar://localhost:6650" + }, +//#endif + "Services": { + "user": { + "https": [ + "https://user:8443" + ] + }, + "user-v2": { + "https": [ + "https://user-v2:8443" + ] + } + } } \ No newline at end of file diff --git a/template/src/frontend/.browserslistrc b/template/src/frontend/.browserslistrc new file mode 100644 index 00000000..dc3bc09a --- /dev/null +++ b/template/src/frontend/.browserslistrc @@ -0,0 +1,4 @@ +> 1% +last 2 versions +not dead +not ie 11 diff --git a/template/src/frontend/.changeset/README.md b/template/src/frontend/.changeset/README.md new file mode 100644 index 00000000..5654e898 --- /dev/null +++ b/template/src/frontend/.changeset/README.md @@ -0,0 +1,5 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works with multi-package repos, or single-package repos to help you version and publish your code. You can find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/template/src/frontend/.changeset/config.json b/template/src/frontend/.changeset/config.json new file mode 100644 index 00000000..f954fb4b --- /dev/null +++ b/template/src/frontend/.changeset/config.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", + "changelog": [ + "@changesets/changelog-github", + { "repo": "vbenjs/vue-vben-admin" } + ], + "commit": false, + "fixed": [["@vben-core/*", "@vben/*"]], + "snapshot": { + "prereleaseTemplate": "{tag}-{datetime}" + }, + "privatePackages": { "version": true, "tag": true }, + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/template/src/frontend/.commitlintrc.js b/template/src/frontend/.commitlintrc.js new file mode 100644 index 00000000..02e33fa6 --- /dev/null +++ b/template/src/frontend/.commitlintrc.js @@ -0,0 +1 @@ +export { default } from '@vben/commitlint-config'; diff --git a/template/src/frontend/.dockerignore b/template/src/frontend/.dockerignore new file mode 100644 index 00000000..52b833a9 --- /dev/null +++ b/template/src/frontend/.dockerignore @@ -0,0 +1,7 @@ +node_modules +.git +.gitignore +*.md +dist +.turbo +dist.zip diff --git a/template/src/frontend/.editorconfig b/template/src/frontend/.editorconfig new file mode 100644 index 00000000..179aec6f --- /dev/null +++ b/template/src/frontend/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset=utf-8 +end_of_line=lf +insert_final_newline=true +indent_style=space +indent_size=2 +max_line_length = 100 +trim_trailing_whitespace = true +quote_type = single + +[*.{yml,yaml,json}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/template/src/frontend/.gitattributes b/template/src/frontend/.gitattributes new file mode 100644 index 00000000..d4e5bd3e --- /dev/null +++ b/template/src/frontend/.gitattributes @@ -0,0 +1,11 @@ +# https://docs.github.com/cn/get-started/getting-started-with-git/configuring-git-to-handle-line-endings + +# Automatically normalize line endings (to LF) for all text-based files. +* text=auto eol=lf + +# Declare files that will always have CRLF line endings on checkout. +*.{cmd,[cC][mM][dD]} text eol=crlf +*.{bat,[bB][aA][tT]} text eol=crlf + +# Denote all files that are truly binary and should not be modified. +*.{ico,png,jpg,jpeg,gif,webp,svg,woff,woff2} binary \ No newline at end of file diff --git a/template/src/frontend/.gitconfig b/template/src/frontend/.gitconfig new file mode 100644 index 00000000..4b28a69c --- /dev/null +++ b/template/src/frontend/.gitconfig @@ -0,0 +1,2 @@ +[core] + ignorecase = false diff --git a/template/src/frontend/.github/CODEOWNERS b/template/src/frontend/.github/CODEOWNERS new file mode 100644 index 00000000..b95ff946 --- /dev/null +++ b/template/src/frontend/.github/CODEOWNERS @@ -0,0 +1,14 @@ +# default onwer +* anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com + +# vben core onwer +/.github/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com +/.vscode/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com +/packages/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com +/packages/@core/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com +/internal/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com +/scripts/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com jinmao88@qq.com + +# vben team onwer +apps/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com @vbenjs/team-v5 jinmao88@qq.com +docs/ anncwb@126.com vince292007@gmail.com netfan@foxmail.com @vbenjs/team-v5 jinmao88@qq.com diff --git a/template/src/frontend/.github/ISSUE_TEMPLATE/bug-report.yml b/template/src/frontend/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 00000000..ae927803 --- /dev/null +++ b/template/src/frontend/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,74 @@ +name: 🐞 Bug Report +description: Report an issue with Vben Admin to help us make it better. +title: 'Bug: ' +labels: ['bug: pending triage'] + +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! + - type: dropdown + id: version + attributes: + label: Version + description: What version of our software are you running? + options: + - Vben Admin V5 + - Vben Admin V2 + default: 0 + validations: + required: true + + - type: textarea + id: bug-desc + attributes: + label: Describe the bug? + description: A clear and concise description of what the bug is. If you intend to submit a PR for this issue, tell us in the description. Thanks! + placeholder: Bug Description + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Reproduction + description: Please provide a link to [StackBlitz](https://stackblitz.com/fork/github/vitest-dev/vitest/tree/main/examples/basic?initialPath=__vitest__/) (you can also use [examples](https://github.com/vitest-dev/vitest/tree/main/examples)) or a github repo that can reproduce the problem you ran into. A [minimal reproduction](https://stackoverflow.com/help/minimal-reproducible-example) is required unless you are absolutely sure that the issue is obvious and the provided information is enough to understand the problem. If a report is vague (e.g. just a generic error message) and has no reproduction, it will receive a "needs reproduction" label. If no reproduction is provided after 3 days, it will be auto-closed. + placeholder: Reproduction + validations: + required: true + + - type: textarea + id: system-info + attributes: + label: System Info + description: Output of `npx envinfo --system --npmPackages '{vue}' --binaries --browsers` + render: shell + placeholder: System, Binaries, Browsers + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell + + - type: checkboxes + id: terms + attributes: + label: Validations + description: Before submitting the issue, please make sure you do the following + # description: By submitting this issue, you agree to follow our [Code of Conduct](https://example.com). + options: + - label: Read the [docs](https://doc.vben.pro/) + required: true + - label: Ensure the code is up to date. (Some issues have been fixed in the latest version) + required: true + - label: I have searched the [existing issues](https://github.com/vbenjs/vue-vben-admin/issues) and checked that my issue does not duplicate any existing issues. + required: true + - label: Check that this is a concrete bug. For Q&A open a [GitHub Discussion](https://github.com/vbenjs/vue-vben-admin/discussions) or join our [Discord Chat Server](https://discord.gg/8GuAdwDhj6). + required: true + - label: The provided reproduction is a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) of the bug. + required: true diff --git a/template/src/frontend/.github/ISSUE_TEMPLATE/docs.yml b/template/src/frontend/.github/ISSUE_TEMPLATE/docs.yml new file mode 100644 index 00000000..d2bf16ef --- /dev/null +++ b/template/src/frontend/.github/ISSUE_TEMPLATE/docs.yml @@ -0,0 +1,38 @@ +name: 📚 Documentation +description: Report an issue with Vben Admin Website to help us make it better. +title: 'Docs: ' +labels: [documentation] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this issue! + - type: checkboxes + id: documentation_is + attributes: + label: Documentation is + options: + - label: Missing + - label: Outdated + - label: Confusing + - label: Not sure? + - type: textarea + id: description + attributes: + label: Explain in Detail + description: A clear and concise description of your suggestion. If you intend to submit a PR for this issue, tell us in the description. Thanks! + placeholder: The description of ... page is not clear. I thought it meant ... but it wasn't. + validations: + required: true + - type: textarea + id: suggestion + attributes: + label: Your Suggestion for Changes + validations: + required: true + - type: textarea + id: reproduction-steps + attributes: + label: Steps to reproduce + description: Please provide any reproduction steps that may need to be described. E.g. if it happens only when running the dev or build script make sure it's clear which one to use. + placeholder: Run `pnpm install` followed by `pnpm run docs:dev` diff --git a/template/src/frontend/.github/ISSUE_TEMPLATE/feature-request.yml b/template/src/frontend/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 00000000..393334e8 --- /dev/null +++ b/template/src/frontend/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,70 @@ +name: ✨ New Feature Proposal +description: Propose a new feature to be added to Vben Admin +title: 'FEATURE: ' +labels: ['enhancement: pending triage'] +body: + - type: markdown + attributes: + value: | + Thank you for suggesting a feature for our project! Please fill out the information below to help us understand and implement your request! + - type: dropdown + id: version + attributes: + label: Version + description: What version of our software are you running? + options: + - Vben Admin V5 + - Vben Admin V2 + default: 0 + validations: + required: true + + - type: textarea + id: description + attributes: + label: Description + description: A detailed description of the feature request. + placeholder: Please describe the feature you would like to see, and why it would be useful. + validations: + required: true + + - type: textarea + id: proposed-solution + attributes: + label: Proposed Solution + description: A clear and concise description of what you want to happen. + placeholder: Describe the solution you'd like to see + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: | + A clear and concise description of any alternative solutions or features you've considered. + placeholder: Describe any alternative solutions or features you've considered + validations: + required: false + + - type: input + id: additional-context + attributes: + label: Additional Context + description: Add any other context or screenshots about the feature request here. + placeholder: Any additional information + validations: + required: false + + - type: checkboxes + id: checkboxes + attributes: + label: Validations + description: Before submitting the issue, please make sure you do the following + options: + - label: Read the [docs](https://doc.vben.pro/) + required: true + - label: Ensure the code is up to date. (Some issues have been fixed in the latest version) + required: true + - label: I have searched the [existing issues](https://github.com/vbenjs/vue-vben-admin/issues) and checked that my issue does not duplicate any existing issues. + required: true diff --git a/template/src/frontend/.github/actions/setup-node/action.yml b/template/src/frontend/.github/actions/setup-node/action.yml new file mode 100644 index 00000000..35fa41c8 --- /dev/null +++ b/template/src/frontend/.github/actions/setup-node/action.yml @@ -0,0 +1,40 @@ +name: 'Setup Node' + +description: 'Setup node and pnpm' + +runs: + using: 'composite' + steps: + - name: Install pnpm + uses: pnpm/action-setup@v4 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .node-version + cache: 'pnpm' + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache@v4 + name: Setup pnpm cache + if: ${{ github.ref_name == 'main' }} + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - uses: actions/cache/restore@v4 + if: ${{ github.ref_name != 'main' }} + with: + path: ${{ env.STORE_PATH }} + key: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + shell: bash + run: pnpm install --frozen-lockfile diff --git a/template/src/frontend/.github/commit-convention.md b/template/src/frontend/.github/commit-convention.md new file mode 100644 index 00000000..a1a969e9 --- /dev/null +++ b/template/src/frontend/.github/commit-convention.md @@ -0,0 +1,89 @@ +## Git Commit Message Convention + +> This is adapted from [Angular's commit convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular). + +#### TL;DR: + +Messages must be matched by the following regex: + +```js +/^(revert: )?(feat|fix|docs|style|refactor|perf|test|workflow|build|ci|chore|types|wip): .{1,50}/; +``` + +#### Examples + +Appears under "Features" header, `dev` subheader: + +``` +feat(dev): add 'comments' option +``` + +Appears under "Bug Fixes" header, `dev` subheader, with a link to issue #28: + +``` +fix(dev): fix dev error + +close #28 +``` + +Appears under "Performance Improvements" header, and under "Breaking Changes" with the breaking change explanation: + +``` +perf(build): remove 'foo' option + +BREAKING CHANGE: The 'foo' option has been removed. +``` + +The following commit and commit `667ecc1` do not appear in the changelog if they are under the same release. If not, the revert commit appears under the "Reverts" header. + +``` +revert: feat(compiler): add 'comments' option + +This reverts commit 667ecc1654a317a13331b17617d973392f415f02. +``` + +### Full Message Format + +A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**: + +``` +(): + + + +