diff --git a/aspnet-core/templates/aio/content/Directory.Packages.props b/aspnet-core/templates/aio/content/Directory.Packages.props
index f464588df..b71e9419d 100644
--- a/aspnet-core/templates/aio/content/Directory.Packages.props
+++ b/aspnet-core/templates/aio/content/Directory.Packages.props
@@ -7,51 +7,11 @@
8.0.0
8.0.0
8.0.0
- 3.1.1
- 8.0.0
- 2.6.1
- 1.8.1
- 6.8.0
- 17.8.0
- 1.12.0
- 6.2.0
- 8.2.0
- 3.7.0
- 1.8.6
- 1.0.5
- 1.0.2
- 16.18.9
- 3.0.2
- 6.0.0
- 3.0.0
- 5.1.0
- 4.2.1
- 2.5.3
- 1.5.10
- 2.13.0
- 1.6.9
- 0.34.0
- 13.0.3
- 7.15.1
- 0.9.2
- 20.0.0
- 4.0.0
- 2023.3.0
- 2.0.1
- 2.7.4
- 6.5.0
- 2.0.3
- 1.0.0-beta.11
- 5.0.0
- 5.4.37
- 3.0.712
- 2.1.0
- 5.5.0
true
-
+
@@ -106,8 +66,11 @@
+
+
+
+
-
@@ -176,6 +139,8 @@
+
+
@@ -236,10 +201,9 @@
-
-
+
@@ -255,10 +219,9 @@
-
-
+
+
-
@@ -276,7 +239,6 @@
-
@@ -289,70 +251,68 @@
-
-
-
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
@@ -362,8 +322,8 @@
-
-
+
+
@@ -372,14 +332,14 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
@@ -403,135 +363,134 @@
-
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/templates/aio/content/README.md b/aspnet-core/templates/aio/content/README.md
index 7d7b52ed3..65291e29c 100644
--- a/aspnet-core/templates/aio/content/README.md
+++ b/aspnet-core/templates/aio/content/README.md
@@ -1,135 +1,133 @@
-# LINGYUN.Abp.Templates
+# PackageName.CompanyName.ProjectName
[English](README.md) | [中文](README.zh-CN.md)
-## Introduction
+## Quick Start Guide
-LINGYUN.Abp.Templates provides two types of project templates based on ABP Framework:
+This guide will help you quickly set up and run the project. Follow these steps to get started.
-1. **Microservice Template**: A complete microservice architecture template with distributed services.
-2. **All-in-One Template**: A single-application template that combines all services into one project.
+### Prerequisites
-## Features
+- .NET SDK 9.0 or higher
+- A supported database (SQL Server, MySQL, PostgreSQL, Oracle, or SQLite)
+- PowerShell 7.0+ (recommended for running migration scripts)
-### Common Features
+### Step 1: Restore and Build the Project
-- Integrated authentication (IdentityServer4/OpenIddict)
-- Database integration (multiple databases supported)
-- Unified configuration management
-- Distributed event bus support
-- Background job processing
-
-### Microservice Template Features
+```bash
+# Navigate to the project root directory
+cd /path/to/project
-- Complete microservice project structure
-- Service discovery and registration
-- Distributed deployment support
+# Restore dependencies
+dotnet restore
-### All-in-One Template Features
+# Build the solution
+dotnet build
+```
-- Simplified deployment
-- Easier maintenance
-- Lower resource requirements
+### Step 2: Create Database Schema
-## How to Use
+Use the Migrate.ps1 script to create the database tables structure:
-### Install labp CLI Tool
+```powershell
+# Navigate to the migrations directory
+cd migrations
-```bash
-dotnet tool install --global LINGYUN.Abp.Cli
+# Run the migration script
+./Migrate.ps1
```
-### Install Templates
+The script will:
+
+1. Detect available DbContext classes in the project
+2. Ask you to select which DbContext to use for migration
+3. Prompt for a migration name
+4. Create the migration
+5. Optionally generate SQL scripts for the migration
+
+### Step 3: Initialize Seed Data
+
+Run the DbMigrator project to initialize seed data:
```bash
-# Install Microservice Template
-dotnet new install LINGYUN.Abp.MicroService.Templates
+# Navigate to the DbMigrator project directory
+cd migrations/PackageName.CompanyName.ProjectName.AIO.DbMigrator
-# Install All-in-One Template
-dotnet new install LINGYUN.Abp.AllInOne.Templates
+# Run the DbMigrator project
+dotnet run
```
-### Create New Project
+The DbMigrator will:
-#### For Microservice Project
+1. Apply all database migrations
+2. Seed initial data (users, roles, etc.)
+3. Set up tenant configurations if applicable
-```bash
-# Short name: lam (LINGYUN Abp Microservice)
-labp create YourCompanyName.YourProjectName -pk YourPackageName -t lam -o /path/to/output --dbms MySql --cs "Server=127.0.0.1;Database=Platform-V70;User Id=root;Password=123456;SslMode=None" --no-random-port
-```
+### Step 4: Launch the Application
-#### For All-in-One Project
+After successfully setting up the database, you can run the host project:
```bash
-# Short name: laa (LINGYUN Abp AllInOne)
-labp create YourCompanyName.YourProjectName -pk YourPackageName -t laa -o /path/to/output --dbms MySql --cs "Server=127.0.0.1;Database=Platform-V70;User Id=root;Password=123456;SslMode=None" --no-random-port
+# Navigate to the host project directory
+cd host/PackageName.CompanyName.ProjectName.AIO.Host
+
+# Run the host project
+dotnet run --launch-profile "PackageName.CompanyName.ProjectName.Development"
```
-## How to Run
+The application will start and be accessible at the configured URL (typically [https://localhost:44300](https://localhost:44300)).
-After creating your project, you can run it using the following command:
+## Database-based Unit Testing
-### For Microservice Project
+To run database-based unit tests, follow these steps:
-```bash
-cd /path/to/output/host/YourPackageName.YourCompanyName.YourProjectName.HttpApi.Host
-dotnet run --launch-profile "YourPackageName.YourCompanyName.YourProjectName.Development"
-```
+### Step 1: Prepare Test Database
-### For All-in-One Project
+Before running tests, make sure the test database exists. The test database connection string is defined in the `ProjectNameEntityFrameworkCoreTestModule.cs` file.
-```bash
-cd /path/to/output/host/YourPackageName.YourCompanyName.YourProjectName.AIO.Host
-dotnet run --launch-profile "YourPackageName.YourCompanyName.YourProjectName.Development"
-```
+The default connection string is:
-## How to Package and Publish
+```csharp
+private const string DefaultPostgresConnectionString =
+ "Host=127.0.0.1;Port=5432;Database=test_db;User Id=postgres;Password=postgres;";
+```
-1. Clone the Project
+You can either create this database manually or modify the connection string to use an existing database.
-```bash
-git clone
-cd /aspnet-core/templates/content
-```
+### Step 2: Configure Test Environment
-2. Modify Version
- Edit the project files to update versions:
- - For Microservice: `../PackageName.CompanyName.ProjectName.csproj`
- - For All-in-One: `../PackageName.CompanyName.ProjectName.AIO.csproj`
+Modify the connection string in `ProjectNameEntityFrameworkCoreTestModule.cs` if needed:
-```xml
-8.3.0
+```csharp
+// You can also set an environment variable to override the default connection string
+var connectionString = Environment.GetEnvironmentVariable("TEST_CONNECTION_STRING") ??
+ DefaultPostgresConnectionString;
```
-3. Execute Packaging Script
+### Step 3: Run Tests
-```powershell
-# Windows PowerShell
-.\pack.ps1
+Run the Application.Tests project:
+
+```bash
+# Navigate to the test project directory
+cd tests/PackageName.CompanyName.ProjectName.Application.Tests
-# PowerShell Core (Windows/Linux/macOS)
-pwsh pack.ps1
+# Run the tests
+dotnet test
```
-The script will prompt you to choose which template to package:
+The test framework will:
-1. Microservice Template
-2. All-in-One Template
-3. Both Templates
+1. Create a clean test database environment
+2. Run all unit tests
+3. Report test results
-## Supported Databases
+## Note About Naming
-- SqlServer
-- MySQL
-- PostgreSQL
-- Oracle
-- SQLite
+This is a template project, so all project names contain placeholders that will be replaced when the template is used to create a new project:
-## Notes
+- `PackageName` will be replaced with your package name
+- `CompanyName` will be replaced with your company name
+- `ProjectName` will be replaced with your project name
-- Ensure .NET SDK 8.0 or higher is installed
-- Choose the appropriate template based on your needs:
- - Microservice Template: For large-scale distributed applications
- - All-in-One Template: For smaller applications or simpler deployment requirements
-- Pay attention to NuGet publish address and key when packaging
-- Complete testing is recommended before publishing
+When creating a new project from this template, you'll specify these values and they'll be substituted throughout the entire solution.
diff --git a/aspnet-core/templates/aio/content/README.zh-CN.md b/aspnet-core/templates/aio/content/README.zh-CN.md
index ae492e5b8..fff301c57 100644
--- a/aspnet-core/templates/aio/content/README.zh-CN.md
+++ b/aspnet-core/templates/aio/content/README.zh-CN.md
@@ -1,135 +1,133 @@
-# LINGYUN.Abp.Templates
+# PackageName.CompanyName.ProjectName
[English](README.md) | [中文](README.zh-CN.md)
-## 简介
+## 快速启动指南
-LINGYUN.Abp.Templates 基于 ABP Framework 提供两种项目模板:
+本指南将帮助您快速设置和运行项目。请按照以下步骤开始。
-1. **微服务模板**:完整的分布式微服务架构模板
-2. **单体应用模板**:将所有服务集成到一个项目中的单体应用模板
+### 前提条件
-## 特性
+- .NET SDK 9.0 或更高版本
+- 支持的数据库(SQL Server、MySQL、PostgreSQL、Oracle 或 SQLite)
+- PowerShell 7.0+(推荐用于运行迁移脚本)
-### 共同特性
+### 第一步:还原和构建项目
-- 集成身份认证(支持 IdentityServer4/OpenIddict)
-- 数据库集成(支持多种数据库)
-- 统一配置管理
-- 分布式事件总线支持
-- 后台作业处理
+```bash
+# 导航到项目根目录
+cd /path/to/project
-### 微服务模板特性
+# 还原依赖项
+dotnet restore
-- 完整的微服务项目结构
-- 服务发现与注册
-- 支持分布式部署
+# 构建解决方案
+dotnet build
+```
-### 单体应用模板特性
+### 第二步:创建数据库结构
-- 简化的部署流程
-- 更容易的维护
-- 更低的资源需求
+使用 Migrate.ps1 脚本创建数据库表结构:
-## 使用方法
+```powershell
+# 导航到 migrations 目录
+cd migrations
-### 安装模板
+# 运行迁移脚本
+./Migrate.ps1
+```
-```bash
-# 安装微服务模板:lam
-dotnet new install LINGYUN.Abp.MicroService.Templates
+该脚本将:
-# 安装单体应用模板:laa
-dotnet new install LINGYUN.Abp.AllInOne.Templates
-```
+1. 检测项目中可用的 DbContext 类
+2. 要求您选择用于迁移的 DbContext
+3. 提示输入迁移名称
+4. 创建迁移
+5. 可选地为迁移生成 SQL 脚本
-### 安装 labp 命令行工具
+### 第三步:初始化种子数据
+
+运行 DbMigrator 项目来初始化种子数据:
```bash
- dotnet tool install --global LINGYUN.Abp.Cli
+# 导航到 DbMigrator 项目目录
+cd migrations/PackageName.CompanyName.ProjectName.AIO.DbMigrator
+
+# 运行 DbMigrator 项目
+dotnet run
```
-### 创建新项目
+DbMigrator 将:
-#### 创建微服务项目
+1. 应用所有数据库迁移
+2. 初始化种子数据(用户、角色等)
+3. 如适用,设置租户配置
-```bash
-# 简写名称:lam (LINGYUN Abp Microservice)
-labp create YourCompanyName.YourProjectName -pk YourPackageName -t lam -o /path/to/output --dbms MySql --cs "Server=127.0.0.1;Database=Platform-V70;User Id=root;Password=123456;SslMode=None" --no-random-port
-```
+### 第四步:启动应用程序
-#### 创建单体应用项目
+成功设置数据库后,您可以运行 host 项目:
```bash
-# 简写名称:laa (LINGYUN Abp AllInOne)
-labp create YourCompanyName.YourProjectName -pk YourPackageName -t laa -o /path/to/output --dbms MySql --cs "Server=127.0.0.1;Database=Platform-V70;User Id=root;Password=123456;SslMode=None" --no-random-port
+# 导航到 host 项目目录
+cd host/PackageName.CompanyName.ProjectName.AIO.Host
+
+# 运行 host 项目
+dotnet run --launch-profile "PackageName.CompanyName.ProjectName.Development"
```
-## 运行项目
+应用程序将启动并可通过配置的 URL 访问(通常是 [https://localhost:44300](https://localhost:44300))。
-创建项目后,可以使用以下命令运行:
+## 基于数据库的单元测试
-### 运行微服务项目
+要运行基于数据库的单元测试,请按照以下步骤操作:
-```bash
-cd /path/to/output/host/YourPackageName.YourCompanyName.YourProjectName.HttpApi.Host
-dotnet run --launch-profile "YourPackageName.YourCompanyName.YourProjectName.Development"
-```
+### 第一步:准备测试数据库
-### 运行单体应用项目
+在运行测试之前,确保测试数据库存在。测试数据库连接字符串在 `ProjectNameEntityFrameworkCoreTestModule.cs` 文件中定义。
-```bash
-cd /path/to/output/host/YourPackageName.YourCompanyName.YourProjectName.AIO.Host
-dotnet run --launch-profile "YourPackageName.YourCompanyName.YourProjectName.Development"
-```
+默认连接字符串是:
-## 打包与发布
+```csharp
+private const string DefaultPostgresConnectionString =
+ "Host=127.0.0.1;Port=5432;Database=test_db;User Id=postgres;Password=postgres;";
+```
-1. 克隆项目
+您可以手动创建此数据库或修改连接字符串以使用现有数据库。
-```bash
-git clone
-cd /aspnet-core/templates/content
-```
+### 第二步:配置测试环境
-2. 修改版本号
- 编辑项目文件更新版本号:
- - 微服务模板:`../PackageName.CompanyName.ProjectName.csproj`
- - 单体应用模板:`../PackageName.CompanyName.ProjectName.AIO.csproj`
+如需修改 `ProjectNameEntityFrameworkCoreTestModule.cs` 中的连接字符串:
-```xml
-8.3.0
+```csharp
+// 您也可以设置环境变量来覆盖默认连接字符串
+var connectionString = Environment.GetEnvironmentVariable("TEST_CONNECTION_STRING") ??
+ DefaultPostgresConnectionString;
```
-3. 执行打包脚本
+### 第三步:运行测试
-```powershell
-# Windows PowerShell
-.\pack.ps1
+运行 Application.Tests 项目:
+
+```bash
+# 导航到测试项目目录
+cd tests/PackageName.CompanyName.ProjectName.Application.Tests
-# PowerShell Core (Windows/Linux/macOS)
-pwsh pack.ps1
+# 运行测试
+dotnet test
```
-脚本会提示您选择要打包的模板:
+测试框架将:
-1. 微服务模板
-2. 单体应用模板
-3. 两种模板都打包
+1. 创建清洁的测试数据库环境
+2. 运行所有单元测试
+3. 报告测试结果
-## 支持的数据库
+## 关于命名的说明
-- SqlServer
-- MySQL
-- PostgreSQL
-- Oracle
-- SQLite
+这是一个模板项目,所以所有项目名称包含的占位符在使用模板创建新项目时将被替换:
-## 注意事项
+- `PackageName` 将被替换为您的包名
+- `CompanyName` 将被替换为您的公司名
+- `ProjectName` 将被替换为您的项目名
-- 确保已安装 .NET SDK 8.0 或更高版本
-- 根据需求选择合适的模板:
- - 微服务模板:适用于大规模分布式应用
- - 单体应用模板:适用于小型应用或简单部署需求
-- 打包时注意 NuGet 发布地址和密钥
-- 发布前建议进行完整测试
+当从此模板创建新项目时,您将指定这些值,它们将在整个解决方案中进行替换。
diff --git a/aspnet-core/templates/aio/content/common.props b/aspnet-core/templates/aio/content/common.props
index d7d7622cf..877d09535 100644
--- a/aspnet-core/templates/aio/content/common.props
+++ b/aspnet-core/templates/aio/content/common.props
@@ -3,13 +3,15 @@
latest
8.2.1
colin
- $(NoWarn);CS1591;CS0436;CS8618;NU1803
+ $(NoWarn);CS1591;CS0436;CS8618;NU1803;NU1900
https://github.com/colinin/abp-next-admin
- $(SolutionDir)LocalNuget
8.2.1
MIT
git
https://github.com/colinin/abp-next-admin
+
+ true
+ $(NoWarn);1591
true
@@ -30,9 +32,4 @@
-
-
- $(SolutionDir)LocalNuget
-
-
\ No newline at end of file
diff --git a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/MicroServiceApplicationsSingleModule.Configure.cs b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/MicroServiceApplicationsSingleModule.Configure.cs
index 765a6ceb2..7a729b009 100644
--- a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/MicroServiceApplicationsSingleModule.Configure.cs
+++ b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/MicroServiceApplicationsSingleModule.Configure.cs
@@ -1,8 +1,8 @@
using Elsa;
using Elsa.Options;
+using Hangfire;
+using Hangfire.Redis.StackExchange;
using LINGYUN.Abp.Aliyun.Localization;
-using LINGYUN.Abp.BackgroundTasks;
-using LINGYUN.Abp.DataProtectionManagement;
using LINGYUN.Abp.ExceptionHandling;
using LINGYUN.Abp.ExceptionHandling.Emailing;
using LINGYUN.Abp.Exporter.MiniExcel;
@@ -29,7 +29,6 @@ using LINGYUN.Abp.WeChat.Work;
using LINGYUN.Abp.Wrapper;
using LINGYUN.Platform.Localization;
using PackageName.CompanyName.ProjectName.AIO.Host.Microsoft.Extensions.DependencyInjection;
-using PackageName.CompanyName.ProjectName.EntityFrameworkCore;
using Medallion.Threading;
using Medallion.Threading.Redis;
using Microsoft.AspNetCore.Authentication.Cookies;
@@ -41,13 +40,11 @@ using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.Caching.StackExchangeRedis;
using Microsoft.IdentityModel.Logging;
using Microsoft.OpenApi.Models;
-using MiniExcelLibs.Attributes;
using OpenIddict.Server;
using OpenIddict.Server.AspNetCore;
using PackageName.CompanyName.ProjectName.AIO.Host.Authentication;
using PackageName.CompanyName.ProjectName.AIO.Host.IdentityResources;
using PackageName.CompanyName.ProjectName.AIO.Host.WeChat.Official.Messages;
-using Quartz;
using StackExchange.Redis;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
@@ -56,9 +53,11 @@ using System.Text.Unicode;
using Volo.Abp;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.AntiForgery;
+using Volo.Abp.AspNetCore.Mvc.Libs;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.Auditing;
using Volo.Abp.Authorization.Permissions;
+using Volo.Abp.BackgroundWorkers;
using Volo.Abp.BlobStoring;
using Volo.Abp.BlobStoring.FileSystem;
using Volo.Abp.Caching;
@@ -77,13 +76,13 @@ using Volo.Abp.MultiTenancy;
using Volo.Abp.OpenIddict;
using Volo.Abp.OpenIddict.Localization;
using Volo.Abp.PermissionManagement;
-using Volo.Abp.Quartz;
using Volo.Abp.Security.Claims;
using Volo.Abp.SettingManagement;
using Volo.Abp.SettingManagement.Localization;
using Volo.Abp.Threading;
using Volo.Abp.UI.Navigation.Urls;
using Volo.Abp.VirtualFileSystem;
+using PackageName.CompanyName.ProjectName.EntityFrameworkCore;
using VoloAbpExceptionHandlingOptions = Volo.Abp.AspNetCore.ExceptionHandling.AbpExceptionHandlingOptions;
namespace PackageName.CompanyName.ProjectName.AIO.Host;
@@ -232,34 +231,6 @@ public partial class MicroServiceApplicationsSingleModule
}
}
- private void PreConfigureQuartz(IConfiguration configuration)
- {
- PreConfigure(options =>
- {
- // 如果使用持久化存储, 则配置quartz持久层
- if (configuration.GetSection("Quartz:UsePersistentStore").Get())
- {
- var settings = configuration.GetSection("Quartz:Properties").Get>();
- if (settings != null)
- {
- foreach (var setting in settings)
- {
- options.Properties[setting.Key] = setting.Value;
- }
- }
-
- options.Configurator += (config) =>
- {
- config.UsePersistentStore(store =>
- {
- store.UseProperties = false;
- store.UseNewtonsoftJsonSerializer();
- });
- };
- }
- });
- }
-
private void PreConfigureElsa(IServiceCollection services, IConfiguration configuration)
{
var elsaSection = configuration.GetSection("Elsa");
@@ -406,17 +377,6 @@ public partial class MicroServiceApplicationsSingleModule
});
}
- private void ConfigureBackgroundTasks()
- {
- Configure(options =>
- {
- options.NodeName = ApplicationName;
- options.JobCleanEnabled = true;
- options.JobFetchEnabled = true;
- options.JobCheckEnabled = true;
- });
- }
-
private void ConfigureTextTemplating(IConfiguration configuration)
{
if (configuration.GetValue("TextTemplating:IsDynamicStoreEnabled"))
@@ -767,6 +727,28 @@ public partial class MicroServiceApplicationsSingleModule
// }
//);
});
+
+ Configure(options =>
+ {
+ options.CheckLibs = false;
+ });
+ }
+
+ private void ConfigureHangfire(IServiceCollection services, IConfiguration configuration)
+ {
+ // 配置Hangfire存储和设置
+ Configure(options =>
+ {
+ options.IsEnabled = true;
+ });
+
+ var redis = ConnectionMultiplexer.Connect(configuration["Redis:Configuration"]);
+ // 配置Hangfire
+ services.AddHangfire(config =>
+ {
+ config.UseRedisStorage(redis);
+ });
+
}
private void ConfigureLocalization()
diff --git a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/MicroServiceApplicationsSingleModule.cs b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/MicroServiceApplicationsSingleModule.cs
index c3b097cba..578b8eb24 100644
--- a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/MicroServiceApplicationsSingleModule.cs
+++ b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/MicroServiceApplicationsSingleModule.cs
@@ -10,22 +10,11 @@ using LINGYUN.Abp.AuditLogging.EntityFrameworkCore;
using LINGYUN.Abp.Authentication.QQ;
using LINGYUN.Abp.Authentication.WeChat;
using LINGYUN.Abp.Authorization.OrganizationUnits;
-using LINGYUN.Abp.BackgroundTasks;
-using LINGYUN.Abp.BackgroundTasks.Activities;
-using LINGYUN.Abp.BackgroundTasks.DistributedLocking;
-using LINGYUN.Abp.BackgroundTasks.EventBus;
-using LINGYUN.Abp.BackgroundTasks.ExceptionHandling;
-using LINGYUN.Abp.BackgroundTasks.Jobs;
-using LINGYUN.Abp.BackgroundTasks.Notifications;
-using LINGYUN.Abp.BackgroundTasks.Quartz;
using LINGYUN.Abp.CachingManagement;
using LINGYUN.Abp.CachingManagement.StackExchangeRedis;
-using LINGYUN.Abp.Dapr.Client;
using LINGYUN.Abp.Data.DbMigrator;
using LINGYUN.Abp.DataProtectionManagement;
using LINGYUN.Abp.DataProtectionManagement.EntityFrameworkCore;
-// using LINGYUN.Abp.Demo;
-// using LINGYUN.Abp.Demo.EntityFrameworkCore;
using LINGYUN.Abp.ExceptionHandling;
using LINGYUN.Abp.ExceptionHandling.Emailing;
using LINGYUN.Abp.Exporter.MiniExcel;
@@ -66,7 +55,6 @@ using LINGYUN.Abp.OpenIddict.WeChat;
using LINGYUN.Abp.OpenIddict.WeChat.Work;
using LINGYUN.Abp.OssManagement;
using LINGYUN.Abp.OssManagement.FileSystem;
-// using LINGYUN.Abp.OssManagement.Imaging;
using LINGYUN.Abp.OssManagement.SettingManagement;
using LINGYUN.Abp.PermissionManagement;
using LINGYUN.Abp.PermissionManagement.HttpApi;
@@ -77,8 +65,6 @@ using LINGYUN.Abp.Serilog.Enrichers.Application;
using LINGYUN.Abp.Serilog.Enrichers.UniqueId;
using LINGYUN.Abp.SettingManagement;
using LINGYUN.Abp.Sms.Aliyun;
-using LINGYUN.Abp.TaskManagement;
-using LINGYUN.Abp.TaskManagement.EntityFrameworkCore;
using LINGYUN.Abp.Tencent.QQ;
using LINGYUN.Abp.Tencent.SettingManagement;
using LINGYUN.Abp.TextTemplating;
@@ -111,8 +97,11 @@ using Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic;
using Volo.Abp.AspNetCore.Serilog;
using Volo.Abp.Autofac;
+using Volo.Abp.BackgroundJobs.Hangfire;
+using Volo.Abp.BackgroundWorkers.Hangfire;
using Volo.Abp.Caching.StackExchangeRedis;
using Volo.Abp.Data;
+using Volo.Abp.EntityFrameworkCore.PostgreSql;
using Volo.Abp.EventBus;
using Volo.Abp.FeatureManagement.EntityFrameworkCore;
using Volo.Abp.Imaging;
@@ -124,20 +113,9 @@ using Volo.Abp.PermissionManagement.OpenIddict;
using Volo.Abp.SettingManagement;
using Volo.Abp.SettingManagement.EntityFrameworkCore;
using Volo.Abp.Threading;
-#if MySQL
-using Volo.Abp.EntityFrameworkCore.MySQL;
-#elif SqlServer
-using Volo.Abp.EntityFrameworkCore.SqlServer;
-using Microsoft.EntityFrameworkCore.Infrastructure;
-#elif Sqlite
-using Volo.Abp.EntityFrameworkCore.Sqlite;
-#elif Oracle
-using Volo.Abp.EntityFrameworkCore.Oracle;
-#elif OracleDevart
-using Volo.Abp.EntityFrameworkCore.Oracle.Devart;
-#elif PostgreSql
-using Volo.Abp.EntityFrameworkCore.PostgreSql;
-#endif
+// using LINGYUN.Abp.Demo;
+// using LINGYUN.Abp.Demo.EntityFrameworkCore;
+// using LINGYUN.Abp.OssManagement.Imaging;
namespace PackageName.CompanyName.ProjectName.AIO.Host;
@@ -172,12 +150,6 @@ namespace PackageName.CompanyName.ProjectName.AIO.Host;
typeof(AbpNotificationsApplicationModule),
typeof(AbpNotificationsHttpApiModule),
typeof(AbpNotificationsEntityFrameworkCoreModule),
-
- //typeof(AbpIdentityServerSessionModule),
- //typeof(AbpIdentityServerApplicationModule),
- //typeof(AbpIdentityServerHttpApiModule),
- //typeof(AbpIdentityServerEntityFrameworkCoreModule),
-
typeof(AbpOpenIddictAspNetCoreModule),
typeof(AbpOpenIddictAspNetCoreSessionModule),
typeof(AbpOpenIddictApplicationModule),
@@ -188,6 +160,9 @@ namespace PackageName.CompanyName.ProjectName.AIO.Host;
typeof(AbpOpenIddictWeChatModule),
typeof(AbpOpenIddictWeChatWorkModule),
+ typeof(AbpBackgroundWorkersHangfireModule),
+ typeof(AbpBackgroundJobsHangfireModule),
+
//typeof(AbpOssManagementMinioModule), // 取消注释以使用Minio
typeof(AbpOssManagementFileSystemModule),
// typeof(AbpOssManagementImagingModule),
@@ -210,11 +185,6 @@ namespace PackageName.CompanyName.ProjectName.AIO.Host;
typeof(AbpSaasHttpApiModule),
typeof(AbpSaasEntityFrameworkCoreModule),
- typeof(TaskManagementDomainModule),
- typeof(TaskManagementApplicationModule),
- typeof(TaskManagementHttpApiModule),
- typeof(TaskManagementEntityFrameworkCoreModule),
-
typeof(AbpTextTemplatingDomainModule),
typeof(AbpTextTemplatingApplicationModule),
typeof(AbpTextTemplatingHttpApiModule),
@@ -246,19 +216,7 @@ namespace PackageName.CompanyName.ProjectName.AIO.Host;
typeof(AbpPermissionManagementEntityFrameworkCoreModule),
typeof(AbpPermissionManagementDomainOrganizationUnitsModule), // 组织机构权限管理
-#if MySQL
- typeof(AbpEntityFrameworkCoreMySQLModule),
-#elif SqlServer
- typeof(AbpEntityFrameworkCoreSqlServerModule),
-#elif Sqlite
- typeof(AbpEntityFrameworkCoreSqliteModule),
-#elif Oracle
- typeof(AbpEntityFrameworkCoreOracleModule),
-#elif OracleDevart
- typeof(AbpEntityFrameworkCoreOracleDevartModule),
-#elif PostgreSql
typeof(AbpEntityFrameworkCorePostgreSqlModule),
-#endif
typeof(AbpAliyunSmsModule),
typeof(AbpAliyunSettingManagementModule),
@@ -268,24 +226,9 @@ namespace PackageName.CompanyName.ProjectName.AIO.Host;
typeof(AbpAuthorizationOrganizationUnitsModule),
typeof(AbpIdentityOrganizaztionUnitsModule),
- typeof(AbpBackgroundTasksModule),
- typeof(AbpBackgroundTasksActivitiesModule),
- typeof(AbpBackgroundTasksDistributedLockingModule),
- typeof(AbpBackgroundTasksEventBusModule),
- typeof(AbpBackgroundTasksExceptionHandlingModule),
- typeof(AbpBackgroundTasksJobsModule),
- typeof(AbpBackgroundTasksNotificationsModule),
- typeof(AbpBackgroundTasksQuartzModule),
-
typeof(AbpDataProtectionManagementApplicationModule),
typeof(AbpDataProtectionManagementHttpApiModule),
typeof(AbpDataProtectionManagementEntityFrameworkCoreModule),
-
- // typeof(AbpDemoApplicationModule),
- // typeof(AbpDemoHttpApiModule),
- // typeof(AbpDemoEntityFrameworkCoreModule),
-
- typeof(AbpDaprClientModule),
typeof(AbpExceptionHandlingModule),
typeof(AbpEmailingExceptionHandlingModule),
typeof(AbpFeaturesLimitValidationModule),
@@ -327,17 +270,6 @@ namespace PackageName.CompanyName.ProjectName.AIO.Host;
typeof(AbpAccountTemplatesModule),
typeof(AbpAspNetCoreAuthenticationJwtBearerModule),
typeof(AbpCachingStackExchangeRedisModule),
- // typeof(AbpElsaModule),
- // typeof(AbpElsaServerModule),
- // typeof(AbpElsaActivitiesModule),
- // typeof(AbpElsaEntityFrameworkCoreModule),
- // typeof(AbpElsaEntityFrameworkCorePostgreSqlModule),
- // typeof(AbpElsaModule),
- // typeof(AbpElsaServerModule),
- // typeof(AbpElsaActivitiesModule),
- // typeof(AbpElsaEntityFrameworkCoreModule),
- // typeof(AbpElsaEntityFrameworkCoreMySqlModule),
-
typeof(AbpExporterMiniExcelModule),
typeof(AbpAspNetCoreMvcUiMultiTenancyModule),
typeof(AbpAspNetCoreSerilogModule),
@@ -348,7 +280,7 @@ namespace PackageName.CompanyName.ProjectName.AIO.Host;
typeof(AbpAspNetCoreMvcUiBasicThemeModule),
typeof(AbpEventBusModule),
typeof(AbpAutofacModule),
-
+
typeof(ProjectNameApplicationModule),
typeof(ProjectNameHttpApiModule),
typeof(ProjectNameEntityFrameworkCoreModule),
@@ -365,7 +297,6 @@ public partial class MicroServiceApplicationsSingleModule : AbpModule
PreConfigureFeature();
PreConfigureIdentity();
PreConfigureApp(configuration);
- PreConfigureQuartz(configuration);
PreConfigureAuthServer(configuration);
PreConfigureElsa(context.Services, configuration);
PreConfigureCertificate(configuration, hostingEnvironment);
@@ -386,7 +317,7 @@ public partial class MicroServiceApplicationsSingleModule : AbpModule
ConfigureDataSeeder();
ConfigureLocalization();
ConfigureKestrelServer();
- ConfigureBackgroundTasks();
+ ConfigureHangfire(context.Services, configuration);
ConfigureExceptionHandling();
ConfigureVirtualFileSystem();
ConfigureEntityDataProtected();
diff --git a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/PackageName.CompanyName.ProjectName.AIO.Host.csproj b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/PackageName.CompanyName.ProjectName.AIO.Host.csproj
index 9e7ea06d7..f5c2d27ba 100644
--- a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/PackageName.CompanyName.ProjectName.AIO.Host.csproj
+++ b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/PackageName.CompanyName.ProjectName.AIO.Host.csproj
@@ -2,264 +2,260 @@
net9.0
enable
+ $(NoWarn);CS1591;CS0436;CS8618;NU1803;NU1900
-
+
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/Program.cs b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/Program.cs
index 3daf07fe4..cbbf3048a 100644
--- a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/Program.cs
+++ b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/Program.cs
@@ -76,6 +76,7 @@ app.UseSwaggerUI(options =>
options.SwaggerEndpoint("/swagger/v1/swagger.json", "Support App API");
});
app.UseAuditing();
+app.UseAbpHangfireDashboard();
app.UseAbpSerilogEnrichers();
app.UseConfiguredEndpoints();
diff --git a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/appsettings.Development.json b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/appsettings.Development.json
index 07d801a94..89e46c01d 100644
--- a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/appsettings.Development.json
+++ b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/appsettings.Development.json
@@ -92,22 +92,6 @@
"BaseUrl": "http://127.0.0.1:30000"
}
},
- "Quartz": {
- "UsePersistentStore": false,
- "Properties": {
- "quartz.jobStore.dataSource": "tkm",
- "quartz.jobStore.type": "Quartz.Impl.AdoJobStore.JobStoreTX,Quartz",
- "quartz.dataSource.tkm.connectionStringName": "Default",
- "quartz.jobStore.driverDelegateType": "Quartz.Impl.AdoJobStore.MySQLDelegate,Quartz",
- "quartz.dataSource.tkm.connectionString": "Default": "Server=127.0.0.1;Database=ProjectName;User Id=root;Password=123456",
- "quartz.dataSource.tkm.provider": "DatabaseManagementNameConnector",
-// "quartz.jobStore.driverDelegateType": "Quartz.Impl.AdoJobStore.PostgreSQLDelegate,Quartz",
-// "quartz.dataSource.tkm.connectionString": "Default": "Server=127.0.0.1;Database=ProjectName;User Id=root;Password=123456",
-// "quartz.dataSource.tkm.provider": "Npgsql",
- "quartz.jobStore.clustered": "true",
- "quartz.serializer.type": "json"
- }
- },
"Redis": {
"IsEnabled": true,
"Configuration": "127.0.0.1,defaultDatabase=15",
diff --git a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/appsettings.json b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/appsettings.json
index ff9beea3e..6ba75de37 100644
--- a/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/appsettings.json
+++ b/aspnet-core/templates/aio/content/host/PackageName.CompanyName.ProjectName.AIO.Host/appsettings.json
@@ -39,24 +39,6 @@
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] [{ProcessId}] [{ThreadId}] - {Message:lj}{NewLine}{Exception}"
}
},
- {
- "Name": "File",
- "Args": {
- "path": "Logs/Debug-.log",
- "restrictedToMinimumLevel": "Debug",
- "rollingInterval": "Day",
- "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] [{ProcessId}] [{ThreadId}] - {Message:lj}{NewLine}{Exception}"
- }
- },
- {
- "Name": "File",
- "Args": {
- "path": "Logs/Info-.log",
- "restrictedToMinimumLevel": "Information",
- "rollingInterval": "Day",
- "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{SourceContext}] [{ProcessId}] [{ThreadId}] - {Message:lj}{NewLine}{Exception}"
- }
- },
{
"Name": "File",
"Args": {
diff --git a/aspnet-core/templates/aio/content/migrations/Migrate.ps1 b/aspnet-core/templates/aio/content/migrations/Migrate.ps1
index b983af60a..dd0543c2c 100755
--- a/aspnet-core/templates/aio/content/migrations/Migrate.ps1
+++ b/aspnet-core/templates/aio/content/migrations/Migrate.ps1
@@ -9,15 +9,76 @@ $env:FROM_MIGRATION = "true"
# 定义项目路径
$projectPath = Resolve-Path (Join-Path $PSScriptRoot "..")
-# 定义可用的DbContext
-$dbContexts = @{
- "1" = @{
- Name = "PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore.DatabaseManagementName"
- Context = "SingleMigrationsDbContext"
- Factory = "SingleMigrationsDbContextFactory"
+# 定义函数来动态查找所有可用的DbContext
+function Get-AvailableDbContexts {
+ $migrationsPath = Join-Path $projectPath "migrations"
+ $dbContexts = @{}
+ $counter = 1
+
+ # 查找所有包含EntityFrameworkCore的目录
+ $efCoreDirectories = Get-ChildItem -Path $migrationsPath -Directory | Where-Object { $_.Name -like "*EntityFrameworkCore*" }
+
+ foreach ($dir in $efCoreDirectories) {
+ # 优先查找 DbContextFactory 文件
+ $factoryFiles = Get-ChildItem -Path $dir.FullName -Filter "*DbContextFactory.cs" -Recurse -File
+
+ # 如果找到了 Factory 文件
+ $foundFactory = $false
+ foreach ($factoryFile in $factoryFiles) {
+ $factoryContent = Get-Content $factoryFile.FullName -Raw
+
+ # 查找 Factory 类名和对应的 DbContext 类
+ if ($factoryContent -match 'class\s+(\w+Factory)\s*:\s*IDesignTimeDbContextFactory<(\w+)>') {
+ $factoryName = $matches[1]
+ $contextName = $matches[2]
+
+ # 如果找到了上下文和工厂,添加到列表中
+ $dbContexts["$counter"] = @{
+ Name = $dir.Name
+ Context = $contextName
+ Factory = $factoryName
+ }
+ $counter++
+ $foundFactory = $true
+ }
+ }
+
+ # 只有当没有找到 Factory 时,才查找 DbContext 文件作为备选
+ if (-not $foundFactory) {
+ $dbContextFiles = Get-ChildItem -Path $dir.FullName -Filter "*DbContext.cs" -Recurse -File
+
+ foreach ($contextFile in $dbContextFiles) {
+ $contextContent = Get-Content $contextFile.FullName -Raw
+ if ($contextContent -match 'class\s+(\w+DbContext)') {
+ $contextName = $matches[1]
+
+ # 添加到列表中,但没有对应的 Factory
+ $dbContexts["$counter"] = @{
+ Name = $dir.Name
+ Context = $contextName
+ Factory = $null
+ }
+ $counter++
+ }
+ }
+ }
+ }
+
+ # 如果没有找到任何上下文,使用默认的
+ if ($dbContexts.Count -eq 0) {
+ $dbContexts["1"] = @{
+ Name = "PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore.DatabaseManagementName"
+ Context = "SingleMigrationsDbContext"
+ Factory = "SingleMigrationsDbContextFactory"
+ }
}
+
+ return $dbContexts
}
+# 获取可用的DbContext
+$dbContexts = Get-AvailableDbContexts
+
# 显示DbContext选择菜单
function Show-DbContextMenu {
$host.UI.RawUI.BackgroundColor = "Black"
diff --git a/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore.DatabaseManagementName/SingleMigrationsEntityFrameworkCoreDatabaseManagementNameModule.cs b/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore.DatabaseManagementName/SingleMigrationsEntityFrameworkCoreDatabaseManagementNameModule.cs
index 6ef5f8205..40f03bfdd 100644
--- a/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore.DatabaseManagementName/SingleMigrationsEntityFrameworkCoreDatabaseManagementNameModule.cs
+++ b/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore.DatabaseManagementName/SingleMigrationsEntityFrameworkCoreDatabaseManagementNameModule.cs
@@ -60,7 +60,7 @@ public class SingleMigrationsEntityFrameworkCoreDatabaseManagementNameModule : A
options.UseOracle();
options.UseOracle();
#elif PostgreSql
- AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);//解决PostgreSql设置为utc时间后无法写入local时区的问题
+ System.AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);//解决PostgreSql设置为utc时间后无法写入local时区的问题
options.UseNpgsql();
options.UseNpgsql();
#endif
diff --git a/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore/DataSeeder/IProjectNameDataSeeder.cs b/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore/DataSeeder/IProjectNameDataSeeder.cs
new file mode 100644
index 000000000..a1e6c8708
--- /dev/null
+++ b/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore/DataSeeder/IProjectNameDataSeeder.cs
@@ -0,0 +1,15 @@
+using System.Threading.Tasks;
+using Volo.Abp.Data;
+
+namespace PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore.DataSeeder
+{
+ public interface IProjectNameDataSeeder
+ {
+ ///
+ /// 初始化数据
+ ///
+ /// 数据种子上下文
+ /// 任务
+ Task SeedAsync(DataSeedContext context);
+ }
+}
diff --git a/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore/DataSeeder/ProjectNameDataSeeder.cs b/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore/DataSeeder/ProjectNameDataSeeder.cs
new file mode 100644
index 000000000..d322894fc
--- /dev/null
+++ b/aspnet-core/templates/aio/content/migrations/PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore/DataSeeder/ProjectNameDataSeeder.cs
@@ -0,0 +1,226 @@
+using Microsoft.Extensions.Logging;
+using PackageName.CompanyName.ProjectName.Users;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Volo.Abp.Data;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Domain.Repositories;
+using Volo.Abp.Guids;
+using Volo.Abp.Identity;
+using Volo.Abp.MultiTenancy;
+using Volo.Abp.Uow;
+using IdentityRole = Volo.Abp.Identity.IdentityRole;
+using IdentityUser = Volo.Abp.Identity.IdentityUser;
+
+namespace PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore.DataSeeder
+{
+ public class ProjectNameDataSeeder : IProjectNameDataSeeder, ITransientDependency
+ {
+ private readonly ICurrentTenant _currentTenant;
+ private readonly IGuidGenerator _guidGenerator;
+ private readonly ILogger _logger;
+ private readonly IRepository _userRepository;
+ private readonly IdentityUserManager _identityUserManager;
+ private readonly IdentityRoleManager _identityRoleManager;
+ private readonly IUnitOfWorkManager _unitOfWorkManager;
+
+ ///
+ /// 构造函数
+ ///
+ public ProjectNameDataSeeder(
+ ICurrentTenant currentTenant,
+ IGuidGenerator guidGenerator,
+ ILogger logger,
+ IRepository userRepository,
+ IdentityUserManager identityUserManager,
+ IdentityRoleManager identityRoleManager,
+ IUnitOfWorkManager unitOfWorkManager)
+ {
+ _currentTenant = currentTenant;
+ _guidGenerator = guidGenerator;
+ _logger = logger;
+ _userRepository = userRepository;
+ _identityUserManager = identityUserManager;
+ _identityRoleManager = identityRoleManager;
+ _unitOfWorkManager = unitOfWorkManager;
+ }
+
+ ///
+ /// 初始化数据
+ ///
+ /// 数据种子上下文
+ /// 任务
+ public async Task SeedAsync(DataSeedContext context)
+ {
+ using (_currentTenant.Change(context.TenantId))
+ {
+ _logger.LogInformation("开始初始化数据...");
+
+ // 初始化角色
+ using (var uow = _unitOfWorkManager.Begin(requiresNew: true))
+ {
+ await SeedRolesAsync();
+ await uow.CompleteAsync();
+ }
+
+ // 初始化用户数据
+ using (var uow = _unitOfWorkManager.Begin(requiresNew: true))
+ {
+ await SeedUsersAsync();
+ await uow.CompleteAsync();
+ }
+
+ _logger.LogInformation("数据初始化完成");
+ }
+ }
+
+ ///
+ /// 初始化角色数据
+ ///
+ private async Task SeedRolesAsync()
+ {
+ // 超级管理员
+ await CreateRoleIfNotExistsAsync(
+ "超级管理员",
+ "系统超级管理员,拥有所有权限");
+ }
+
+ ///
+ /// 创建角色(如果不存在)
+ ///
+ private async Task CreateRoleIfNotExistsAsync(string roleName, string description)
+ {
+ if (await _identityRoleManager.FindByNameAsync(roleName) == null)
+ {
+ var role = new IdentityRole(
+ _guidGenerator.Create(),
+ roleName,
+ _currentTenant.Id)
+ {
+ IsStatic = true,
+ IsPublic = true
+ };
+
+ await _identityRoleManager.CreateAsync(role);
+
+ _logger.LogInformation($"创建角色:{roleName}");
+ }
+ }
+
+ ///
+ /// 初始化用户数据
+ ///
+ private async Task SeedUsersAsync()
+ {
+
+ // 查找超级管理员角色
+ var superAdminRole = await _identityRoleManager.FindByNameAsync("超级管理员");
+ if (superAdminRole == null)
+ {
+ _logger.LogError("未找到超级管理员角色,无法为用户分配角色");
+ return;
+ }
+
+ // 创建用户数据(使用固定用户名避免生成问题)
+ await CreateUserIfNotExistsAsync("user1", "user1", "超级管理员");
+ await CreateUserIfNotExistsAsync("user2", "user2", "超级管理员");
+ await CreateUserIfNotExistsAsync("user3", "user3", "超级管理员");
+ await CreateUserIfNotExistsAsync("user4", "user4", "超级管理员");
+ }
+
+ ///
+ /// 创建用户(如果不存在)
+ ///
+ private async Task CreateUserIfNotExistsAsync(
+ string name,
+ string userName,
+ string roles)
+ {
+ // 检查用户是否已存在
+ var existingUser = await _userRepository.FindAsync(u => u.NickName == name);
+ if (existingUser != null)
+ {
+ _logger.LogInformation($"用户[{name}]已存在,跳过创建");
+ return;
+ }
+
+ var identityUser = await _identityUserManager.FindByNameAsync(userName);
+ if (identityUser == null)
+ {
+ // 创建Identity用户
+ identityUser = new IdentityUser(
+ _guidGenerator.Create(),
+ userName,
+ $"{userName}@example.com",
+ _currentTenant.Id)
+ {
+ Name = name,
+ Surname = ""
+ };
+
+ // 设置默认密码 123456
+ var identityResult = await _identityUserManager.CreateAsync(identityUser, "123456");
+ if (!identityResult.Succeeded)
+ {
+ _logger.LogError($"创建Identity用户[{name}]失败: {string.Join(", ", identityResult.Errors.Select(e => e.Description))}");
+ return;
+ }
+
+ // 分配角色
+ if (!string.IsNullOrWhiteSpace(roles))
+ {
+ var roleNames = roles.Split(',', StringSplitOptions.RemoveEmptyEntries);
+ foreach (var roleName in roleNames)
+ {
+ var trimmedRoleName = roleName.Trim();
+ var role = await _identityRoleManager.FindByNameAsync(trimmedRoleName);
+ if (role != null)
+ {
+ var roleResult = await _identityUserManager.AddToRoleAsync(identityUser, trimmedRoleName);
+ if (!roleResult.Succeeded)
+ {
+ _logger.LogWarning($"为用户[{name}]分配角色[{trimmedRoleName}]失败: {string.Join(", ", roleResult.Errors.Select(e => e.Description))}");
+ }
+ }
+ else
+ {
+ _logger.LogWarning($"角色[{trimmedRoleName}]不存在,无法为用户[{name}]分配");
+ }
+ }
+ }
+
+ // 创建系统用户
+ var user = new User(
+ _guidGenerator.Create(),
+ name,
+ identityUser.Id);
+
+ // 保存用户
+ await _userRepository.InsertAsync(user);
+
+ _logger.LogInformation($"创建用户:{name},用户名:{userName}");
+ }
+ else
+ {
+ _logger.LogInformation($"Identity用户[{userName}]已存在,检查是否需要创建业务用户");
+
+ // 检查是否需要创建业务用户
+ var businessUser = await _userRepository.FindAsync(u => u.IdentityUserId == identityUser.Id);
+ if (businessUser == null)
+ {
+ // 创建系统用户
+ var user = new User(
+ _guidGenerator.Create(),
+ name,
+ identityUser.Id);
+
+ // 保存用户
+ await _userRepository.InsertAsync(user);
+
+ _logger.LogInformation($"为已存在的Identity用户创建业务用户:{name}");
+ }
+ }
+ }
+ }
+}
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Permissions/ProjectNamePermissionDefinitionProvider.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Permissions/ProjectNamePermissionDefinitionProvider.cs
index 4a170551b..2adf0a13f 100644
--- a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Permissions/ProjectNamePermissionDefinitionProvider.cs
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Permissions/ProjectNamePermissionDefinitionProvider.cs
@@ -13,6 +13,12 @@ public class ProjectNamePermissionDefinitionProvider : PermissionDefinitionProvi
group.AddPermission(
ProjectNamePermissions.ManageSettings,
L("Permission:ManageSettings"));
+
+ var userPermission = group.AddPermission(ProjectNamePermissions.User.Default, L("Permission:User"));
+ userPermission.AddChild(ProjectNamePermissions.User.Create, L("Permission:Create"));
+ userPermission.AddChild(ProjectNamePermissions.User.Update, L("Permission:Update"));
+ userPermission.AddChild(ProjectNamePermissions.User.Delete, L("Permission:Delete"));
+
}
private static LocalizableString L(string name)
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Permissions/ProjectNamePermissions.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Permissions/ProjectNamePermissions.cs
index 98a957ff9..fb03f41bc 100644
--- a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Permissions/ProjectNamePermissions.cs
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Permissions/ProjectNamePermissions.cs
@@ -5,4 +5,12 @@ public static class ProjectNamePermissions
public const string GroupName = "ProjectName";
public const string ManageSettings = GroupName + ".ManageSettings";
+
+ public class User
+ {
+ public const string Default = GroupName + ".User";
+ public const string Update = Default + ".Update";
+ public const string Create = Default + ".Create";
+ public const string Delete = Default + ".Delete";
+ }
}
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/CreateUpdateUserDto.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/CreateUpdateUserDto.cs
new file mode 100644
index 000000000..ae1e5e270
--- /dev/null
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/CreateUpdateUserDto.cs
@@ -0,0 +1,37 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+
+namespace PackageName.CompanyName.ProjectName.Users.Dtos
+{
+ [Serializable]
+ public class CreateUpdateUserDto
+ {
+ ///
+ /// 用户名称
+ ///
+ [Required(ErrorMessage = "用户名称不能为空")]
+ [StringLength(50, ErrorMessage = "用户名称长度不能超过50个字符")]
+ public string NickName { get; set; }
+
+ ///
+ /// 密码
+ ///
+ [StringLength(20, MinimumLength = 6, ErrorMessage = "密码长度必须在6-20个字符之间")]
+ public string Password { get; set; }
+
+ ///
+ /// 联系方式
+ ///
+ public string ContactInfo { get; set; }
+
+ ///
+ /// 职位
+ ///
+ public string Position { get; set; }
+
+ ///
+ /// 是否启用
+ ///
+ public bool IsActive { get; set; } = true;
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/UserDto.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/UserDto.cs
new file mode 100644
index 000000000..2c5dd4f3d
--- /dev/null
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/UserDto.cs
@@ -0,0 +1,39 @@
+using System;
+using Volo.Abp.Application.Dtos;
+
+namespace PackageName.CompanyName.ProjectName.Users.Dtos
+{
+ [Serializable]
+ public class UserDto : FullAuditedEntityDto
+ {
+ ///
+ /// 用户名称
+ ///
+ public string NickName { get; set; }
+
+ ///
+ /// Identity用户Id
+ ///
+ public Guid IdentityUserId { get; set; }
+
+ ///
+ /// 用户状态
+ ///
+ public bool IsActive { get; set; }
+
+ ///
+ /// 联系方式
+ ///
+ public string ContactInfo { get; set; }
+
+ ///
+ /// 职位
+ ///
+ public string Position { get; set; }
+
+ ///
+ /// 角色名称
+ ///
+ public string RoleNames { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/UserItemDto.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/UserItemDto.cs
new file mode 100644
index 000000000..92014ffbe
--- /dev/null
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/UserItemDto.cs
@@ -0,0 +1,39 @@
+using System;
+using Volo.Abp.Application.Dtos;
+
+namespace PackageName.CompanyName.ProjectName.Users.Dtos
+{
+ [Serializable]
+ public class UserItemDto : FullAuditedEntityDto
+ {
+ ///
+ /// 用户名称
+ ///
+ public string NickName { get; set; }
+
+ ///
+ /// Identity用户Id
+ ///
+ public Guid IdentityUserId { get; set; }
+
+ ///
+ /// 用户状态
+ ///
+ public bool IsActive { get; set; }
+
+ ///
+ /// 联系方式
+ ///
+ public string ContactInfo { get; set; }
+
+ ///
+ /// 职位
+ ///
+ public string Position { get; set; }
+
+ ///
+ /// 角色名称
+ ///
+ public string RoleNames { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/UserPagedAndSortedResultRequestDto.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/UserPagedAndSortedResultRequestDto.cs
new file mode 100644
index 000000000..9c7c26ee9
--- /dev/null
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/Dtos/UserPagedAndSortedResultRequestDto.cs
@@ -0,0 +1,14 @@
+using System;
+using Volo.Abp.Application.Dtos;
+
+namespace PackageName.CompanyName.ProjectName.Users.Dtos
+{
+ [Serializable]
+ public class UserPagedAndSortedResultRequestDto : PagedAndSortedResultRequestDto
+ {
+ ///
+ /// 用户名称
+ ///
+ public string NickName { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/IUserAppService.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/IUserAppService.cs
new file mode 100644
index 000000000..606d0ddc7
--- /dev/null
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application.Contracts/PackageName/CompanyName/ProjectName/Users/IUserAppService.cs
@@ -0,0 +1,55 @@
+using PackageName.CompanyName.ProjectName.Users.Dtos;
+using System;
+using System.Threading.Tasks;
+using Volo.Abp.Application.Dtos;
+using Volo.Abp.Application.Services;
+
+namespace PackageName.CompanyName.ProjectName.Users
+{
+ ///
+ /// 用户应用服务接口
+ ///
+ public interface IUserAppService :
+ IApplicationService
+ {
+ ///
+ /// 创建用户
+ ///
+ Task CreateAsync(CreateUpdateUserDto input);
+
+ ///
+ /// 更新用户
+ ///
+ Task UpdateAsync(Guid id, CreateUpdateUserDto input);
+
+ ///
+ /// 删除用户
+ ///
+ Task DeleteAsync(Guid id);
+
+ ///
+ /// 获取用户
+ ///
+ Task GetAsync(Guid id);
+
+ ///
+ /// 获取用户列表
+ ///
+ Task> GetListAsync(UserPagedAndSortedResultRequestDto input);
+
+ ///
+ /// 修改用户密码
+ ///
+ Task ChangePasswordAsync(Guid id, string currentPassword, string newPassword);
+
+ ///
+ /// 重置用户密码(管理员操作)
+ ///
+ Task ResetPasswordAsync(Guid id, string newPassword);
+
+ ///
+ /// 启用或禁用用户
+ ///
+ Task SetUserActiveStatusAsync(Guid id, bool isActive);
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName.CompanyName.ProjectName.Application.csproj b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName.CompanyName.ProjectName.Application.csproj
index b36251c2d..10f4f0ddc 100644
--- a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName.CompanyName.ProjectName.Application.csproj
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName.CompanyName.ProjectName.Application.csproj
@@ -14,6 +14,7 @@
+
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/ProjectNameApplicationMapperProfile.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/ProjectNameApplicationMapperProfile.cs
index 0ace9b456..2dd396079 100644
--- a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/ProjectNameApplicationMapperProfile.cs
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/ProjectNameApplicationMapperProfile.cs
@@ -1,4 +1,6 @@
using AutoMapper;
+using PackageName.CompanyName.ProjectName.Users;
+using PackageName.CompanyName.ProjectName.Users.Dtos;
namespace PackageName.CompanyName.ProjectName;
@@ -6,5 +8,13 @@ public class ProjectNameApplicationMapperProfile : Profile
{
public ProjectNameApplicationMapperProfile()
{
+ CreateMap()
+ .ForMember(d => d.IsActive, o => o.Ignore())
+ .ForMember(d => d.RoleNames, o => o.Ignore());
+ CreateMap()
+ .ForMember(d => d.IsActive, o => o.Ignore())
+ .ForMember(d => d.RoleNames, o => o.Ignore());
+ CreateMap(MemberList.None);
+
}
}
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/ProjectNameApplicationModule.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/ProjectNameApplicationModule.cs
index 6e1cf009f..d8704f2ac 100644
--- a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/ProjectNameApplicationModule.cs
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/ProjectNameApplicationModule.cs
@@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Application;
using Volo.Abp.Authorization;
using Volo.Abp.AutoMapper;
+using Volo.Abp.BackgroundJobs;
using Volo.Abp.Modularity;
namespace PackageName.CompanyName.ProjectName;
@@ -12,6 +13,7 @@ namespace PackageName.CompanyName.ProjectName;
typeof(AbpDddApplicationModule),
typeof(ProjectNameDomainModule),
typeof(ProjectNameApplicationContractsModule),
+ typeof(AbpBackgroundJobsModule),
typeof(AbpDynamicQueryableApplicationModule))]
public class ProjectNameApplicationModule : AbpModule
{
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/Users/UserAppService.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/Users/UserAppService.cs
new file mode 100644
index 000000000..ff2b0c031
--- /dev/null
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Application/PackageName/CompanyName/ProjectName/Users/UserAppService.cs
@@ -0,0 +1,211 @@
+using Microsoft.AspNetCore.Authorization;
+using PackageName.CompanyName.ProjectName.Permissions;
+using PackageName.CompanyName.ProjectName.Users.Dtos;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Dynamic.Core;
+using System.Threading.Tasks;
+using Volo.Abp;
+using Volo.Abp.Application.Dtos;
+using Volo.Abp.Application.Services;
+using Volo.Abp.Identity;
+
+namespace PackageName.CompanyName.ProjectName.Users
+{
+ ///
+ /// 用户
+ ///
+ public class UserAppService : ApplicationService, IUserAppService
+ {
+ private readonly IUserRepository _userRepository;
+ private readonly IUserManager _userManager;
+ private readonly IdentityUserManager _identityUserManager;
+
+ public UserAppService(
+ IUserRepository userRepository,
+ IUserManager userManager,
+ IdentityUserManager identityUserManager)
+ {
+ _userRepository = userRepository;
+ _userManager = userManager;
+ _identityUserManager = identityUserManager;
+ }
+
+ ///
+ /// 创建用户
+ ///
+ [Authorize(ProjectNamePermissions.User.Create)]
+ public async Task CreateAsync(CreateUpdateUserDto input)
+ {
+ // 参数检查和验证逻辑可以在这里添加
+ if (string.IsNullOrEmpty(input.NickName))
+ {
+ throw new UserFriendlyException("昵称不能为空");
+ }
+ if (string.IsNullOrEmpty(input.Password))
+ {
+ throw new UserFriendlyException("密码不能为空");
+ }
+
+ // 使用UserManager创建用户
+ var user = await _userManager.CreateAsync(
+ input.NickName,
+ input.Password,
+ input.ContactInfo,
+ input.Position,
+ input.IsActive);
+
+ // 返回DTO对象
+ return await MapToUserDtoAsync(user);
+ }
+
+ ///
+ /// 更新用户
+ ///
+ [Authorize(ProjectNamePermissions.User.Update)]
+ public async Task UpdateAsync(Guid id, CreateUpdateUserDto input)
+ {
+ // 使用UserManager更新用户基本信息
+ var user = await _userManager.UpdateAsync(
+ id,
+ input.NickName,
+ input.Password,
+ input.ContactInfo,
+ input.Position,
+ input.IsActive);
+
+ // 确保更新后的用户状态与DTO中的一致
+ if (user.IdentityUser != null)
+ {
+ bool currentIsActive = user.IdentityUser.LockoutEnd == null || user.IdentityUser.LockoutEnd < DateTimeOffset.Now;
+ if (currentIsActive != input.IsActive)
+ {
+ await _userManager.SetUserActiveStatusAsync(id, input.IsActive);
+ user = await _userManager.GetAsync(id);
+ }
+ }
+
+ // 返回DTO对象
+ return await MapToUserDtoAsync(user);
+ }
+
+ ///
+ /// 删除用户
+ ///
+ [Authorize(ProjectNamePermissions.User.Delete)]
+ public Task DeleteAsync(Guid id)
+ {
+ return _userManager.DeleteAsync(id);
+ }
+
+ ///
+ /// 获取用户
+ ///
+ [Authorize(ProjectNamePermissions.User.Default)]
+ public async Task GetAsync(Guid id)
+ {
+ var user = await _userManager.GetAsync(id);
+ return await MapToUserDtoAsync(user);
+ }
+
+ ///
+ /// 获取用户列表
+ ///
+ [Authorize(ProjectNamePermissions.User.Default)]
+ public async Task> GetListAsync(UserPagedAndSortedResultRequestDto input)
+ {
+ // 创建查询
+ var query = await CreateFilteredQueryAsync(input);
+
+ // 获取总记录数
+ var totalCount = await AsyncExecuter.CountAsync(query);
+
+ // 获取已排序和分页的查询结果
+ var users = await AsyncExecuter.ToListAsync(
+ query.OrderBy(input.Sorting ?? nameof(User.NickName))
+ .Skip(input.SkipCount)
+ .Take(input.MaxResultCount));
+
+ // 转换为DTO并返回
+ var userDtos = new List();
+ foreach (var user in users)
+ {
+ var userDto = ObjectMapper.Map(user);
+
+ // 填充角色信息
+ if (user.IdentityUser != null)
+ {
+ var roles = await _identityUserManager.GetRolesAsync(user.IdentityUser);
+ userDto.RoleNames = string.Join("、", roles);
+ userDto.IsActive = user.IdentityUser.LockoutEnd == null || user.IdentityUser.LockoutEnd < DateTimeOffset.Now;
+ }
+
+ userDtos.Add(userDto);
+ }
+
+ return new PagedResultDto(totalCount, userDtos);
+ }
+
+ ///
+ /// 修改用户密码
+ ///
+ [Authorize]
+ public async Task ChangePasswordAsync(Guid id, string currentPassword, string newPassword)
+ {
+ await _userManager.ChangePasswordAsync(id, currentPassword, newPassword);
+ }
+
+ ///
+ /// 重置用户密码(管理员操作)
+ ///
+ [Authorize(ProjectNamePermissions.User.Update)]
+ public async Task ResetPasswordAsync(Guid id, string newPassword)
+ {
+ await _userManager.ResetPasswordAsync(id, newPassword);
+ }
+
+ ///
+ /// 启用或禁用用户
+ ///
+ [Authorize(ProjectNamePermissions.User.Update)]
+ public async Task SetUserActiveStatusAsync(Guid id, bool isActive)
+ {
+ await _userManager.SetUserActiveStatusAsync(id, isActive);
+ }
+
+ ///
+ /// 创建基础查询,应用过滤条件
+ ///
+ protected async virtual Task> CreateFilteredQueryAsync(
+ UserPagedAndSortedResultRequestDto input)
+ {
+ // 获取基础查询,并加载相关实体
+ var query = await _userRepository.WithDetailsAsync(
+ x => x.IdentityUser);
+
+ // 应用过滤条件
+ return query
+ .WhereIf(!string.IsNullOrWhiteSpace(input.NickName),
+ x => x.NickName.Contains(input.NickName));
+ }
+
+ ///
+ /// 将User实体映射为UserDto,并填充权限信息
+ ///
+ private async Task MapToUserDtoAsync(User user)
+ {
+ var userDto = ObjectMapper.Map(user);
+
+ // 设置用户状态和角色信息
+ if (user.IdentityUser != null)
+ {
+ var roles = await _identityUserManager.GetRolesAsync(user.IdentityUser);
+ userDto.RoleNames = string.Join("、", roles);
+ userDto.IsActive = user.IdentityUser.LockoutEnd == null || user.IdentityUser.LockoutEnd < DateTimeOffset.Now;
+ }
+
+ return userDto;
+ }
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName.CompanyName.ProjectName.Domain.csproj b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName.CompanyName.ProjectName.Domain.csproj
index 160b74c5c..4158f0a15 100644
--- a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName.CompanyName.ProjectName.Domain.csproj
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName.CompanyName.ProjectName.Domain.csproj
@@ -14,10 +14,14 @@
+
+
+
+
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/ProjectNameDomainModule.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/ProjectNameDomainModule.cs
index 401aa849e..6ca9ae998 100644
--- a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/ProjectNameDomainModule.cs
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/ProjectNameDomainModule.cs
@@ -1,7 +1,9 @@
using LINGYUN.Abp.DataProtection;
+using LINGYUN.Abp.Identity;
using Microsoft.Extensions.DependencyInjection;
using PackageName.CompanyName.ProjectName.ObjectExtending;
using Volo.Abp.AutoMapper;
+using Volo.Abp.BackgroundWorkers.Hangfire;
using Volo.Abp.Domain.Entities.Events.Distributed;
using Volo.Abp.Modularity;
using Volo.Abp.ObjectExtending.Modularity;
@@ -12,6 +14,8 @@ namespace PackageName.CompanyName.ProjectName;
[DependsOn(
typeof(AbpAutoMapperModule),
typeof(AbpDataProtectionModule),
+ typeof(AbpIdentityDomainModule),
+ typeof(AbpBackgroundWorkersHangfireModule),
typeof(ProjectNameDomainSharedModule))]
public class ProjectNameDomainModule : AbpModule
{
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/TreeCodes/IHaveTreeCode.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/TreeCodes/IHaveTreeCode.cs
new file mode 100644
index 000000000..f1641be31
--- /dev/null
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/TreeCodes/IHaveTreeCode.cs
@@ -0,0 +1,21 @@
+using System;
+using Volo.Abp.Auditing;
+
+namespace PackageName.CompanyName.ProjectName.TreeCodes
+{
+ ///
+ /// 定义具有树形编码的实体接口
+ ///
+ public interface IHaveTreeCode : IHasCreationTime
+ {
+ ///
+ /// 树形编码
+ ///
+ string TreeCode { get; set; }
+
+ ///
+ /// 父级Id
+ ///
+ Guid? ParentId { get; set; }
+ }
+}
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/TreeCodes/ITreeCodeGenerator.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/TreeCodes/ITreeCodeGenerator.cs
new file mode 100644
index 000000000..0c3ce0754
--- /dev/null
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/TreeCodes/ITreeCodeGenerator.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Threading.Tasks;
+using Volo.Abp.Domain.Entities;
+using Volo.Abp.Domain.Repositories;
+
+namespace PackageName.CompanyName.ProjectName.TreeCodes
+{
+ ///
+ /// 树形编码生成器接口
+ ///
+ public interface ITreeCodeGenerator
+ {
+ ///
+ /// 生成树形编码
+ ///
+ /// 实体类型
+ /// 仓储
+ /// 父级Id
+ /// 生成的树形编码
+ Task GenerateAsync(
+ IRepository repository,
+ Guid? parentId)
+ where TEntity : class, IEntity, IHaveTreeCode;
+
+ ///
+ /// 更新节点及其所有子节点的TreeCode
+ ///
+ Task UpdateTreeCodesAsync(
+ IRepository repository,
+ Guid entityId)
+ where TEntity : class, IEntity, IHaveTreeCode;
+ }
+}
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/TreeCodes/TreeCodeGenerator.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/TreeCodes/TreeCodeGenerator.cs
new file mode 100644
index 000000000..2b08bc0f5
--- /dev/null
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/TreeCodes/TreeCodeGenerator.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.Domain.Entities;
+using Volo.Abp.Domain.Repositories;
+
+namespace PackageName.CompanyName.ProjectName.TreeCodes
+{
+ ///
+ /// 树形编码生成器
+ ///
+ public class TreeCodeGenerator : ITreeCodeGenerator, ISingletonDependency
+ {
+ ///
+ /// 生成树形编码
+ ///
+ /// 实体类型
+ /// 仓储
+ /// 父级Id
+ /// 生成的树形编码
+ public async virtual Task GenerateAsync(
+ IRepository repository,
+ Guid? parentId)
+ where TEntity : class, IEntity, IHaveTreeCode
+ {
+ if (!parentId.HasValue)
+ {
+ // 生成根节点编码
+ return await GenerateRootCodeAsync(repository);
+ }
+ else
+ {
+ // 生成子节点编码
+ return await GenerateChildCodeAsync(repository, parentId.Value);
+ }
+ }
+
+ private async Task GenerateRootCodeAsync(
+ IRepository repository)
+ where TEntity : class, IEntity, IHaveTreeCode
+ {
+ var query = await repository.GetQueryableAsync();
+ query = query.Where(e => e.ParentId == null).OrderByDescending(e => e.CreationTime);
+ var maxCodeEntity = await repository.AsyncExecuter.FirstOrDefaultAsync(query);
+
+ if (maxCodeEntity == null)
+ {
+ return "0001";
+ }
+
+ int maxCode = int.Parse(maxCodeEntity.TreeCode.Split('.').LastOrDefault("0"));
+ return (maxCode + 1).ToString("D4");
+ }
+
+ private async Task GenerateChildCodeAsync(
+ IRepository repository,
+ Guid parentId)
+ where TEntity : class, IEntity, IHaveTreeCode
+ {
+ var parent = await repository.GetAsync(parentId);
+ if (parent == null)
+ {
+ throw new EntityNotFoundException(typeof(TEntity), parentId);
+ }
+
+ var query = await repository.GetQueryableAsync();
+ query = query.Where(e => e.ParentId == parentId).OrderByDescending(e => e.CreationTime);
+ var maxCodeEntity = await repository.AsyncExecuter.FirstOrDefaultAsync(query);
+
+ string newCode;
+ if (maxCodeEntity == null)
+ {
+ newCode = "0001";
+ }
+ else
+ {
+ int maxCode = int.Parse(maxCodeEntity.TreeCode.Split('.').Last());
+ newCode = (maxCode + 1).ToString("D4");
+ }
+
+ // 构建完整的TreeCode: 父TreeCode.新编码
+ return $"{parent.TreeCode}.{newCode}";
+ }
+
+ ///
+ /// 更新节点及其所有子节点的TreeCode
+ ///
+ public async virtual Task UpdateTreeCodesAsync(
+ IRepository repository,
+ Guid entityId)
+ where TEntity : class, IEntity, IHaveTreeCode
+ {
+ var entity = await repository.GetAsync(entityId);
+ var query = await repository.GetQueryableAsync();
+ var children = await repository.AsyncExecuter.ToListAsync(
+ query.Where(e => e.ParentId == entityId));
+
+ foreach (var child in children)
+ {
+ // 获取子节点编码(TreeCode最后一部分)或生成新编码
+ string childCode = child.TreeCode.Contains('.')
+ ? child.TreeCode.Split('.').Last()
+ : child.TreeCode;
+
+ child.TreeCode = $"{entity.TreeCode}.{childCode}";
+ await repository.UpdateAsync(child);
+
+ // 递归更新子节点
+ await UpdateTreeCodesAsync(repository, child.Id);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/IUserManager.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/IUserManager.cs
new file mode 100644
index 000000000..7d15b7792
--- /dev/null
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/IUserManager.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Threading.Tasks;
+using Volo.Abp.Domain.Services;
+
+namespace PackageName.CompanyName.ProjectName.Users
+{
+ ///
+ /// 用户管理服务接口
+ ///
+ public interface IUserManager : IDomainService
+ {
+ ///
+ /// 创建新用户
+ ///
+ Task CreateAsync(
+ string nickName,
+ string password,
+ string contactInfo = null,
+ string position = null,
+ bool isActive = true
+ );
+
+ ///
+ /// 更新用户信息
+ ///
+ Task UpdateAsync(
+ Guid id,
+ string nickName,
+ string password,
+ string contactInfo = null,
+ string position = null,
+ bool isActive = true);
+
+ ///
+ /// 删除用户
+ ///
+ Task DeleteAsync(Guid id);
+
+ ///
+ /// 修改用户密码
+ ///
+ Task ChangePasswordAsync(Guid id, string currentPassword, string newPassword);
+
+ ///
+ /// 重置用户密码
+ ///
+ Task ResetPasswordAsync(Guid id, string newPassword);
+
+ ///
+ /// 获取用户信息
+ ///
+ Task GetAsync(Guid id);
+
+ ///
+ /// 根据Identity用户ID获取用户
+ ///
+ Task FindByIdentityUserIdAsync(Guid identityUserId);
+
+ ///
+ /// 根据用户昵称查找用户
+ ///
+ Task FindByNickNameAsync(string nickName);
+
+ ///
+ /// 禁用或启用用户
+ ///
+ Task SetUserActiveStatusAsync(Guid id, bool isActive);
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/IUserRepository.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/IUserRepository.cs
new file mode 100644
index 000000000..75fac813b
--- /dev/null
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/IUserRepository.cs
@@ -0,0 +1,9 @@
+using System;
+using Volo.Abp.Domain.Repositories;
+
+namespace PackageName.CompanyName.ProjectName.Users
+{
+ public interface IUserRepository : IRepository
+ {
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/User.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/User.cs
new file mode 100644
index 000000000..855fb5116
--- /dev/null
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/User.cs
@@ -0,0 +1,59 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using Volo.Abp.Domain.Entities.Auditing;
+using Volo.Abp.Identity;
+
+namespace PackageName.CompanyName.ProjectName.Users
+{
+ ///
+ /// 用户实体
+ ///
+ public class User : FullAuditedEntity
+ {
+ ///
+ /// 用户名称
+ ///
+ [MaxLength(50)]
+ public string NickName { get; set; }
+
+ ///
+ /// Identity用户Id
+ ///
+ public Guid IdentityUserId { get; set; }
+
+ ///
+ /// Identity用户
+ ///
+ public virtual IdentityUser IdentityUser { get; set; }
+
+ ///
+ /// 联系方式
+ ///
+ [MaxLength(50)]
+ public string ContactInfo { get; set; }
+
+ ///
+ /// 职位
+ ///
+ [MaxLength(50)]
+ public string Position { get; set; }
+
+ protected User()
+ {
+ }
+
+ public User(
+ Guid id,
+ string nickName,
+ Guid identityUserId,
+ string contactInfo = null,
+ string position = null
+ ) : base(id)
+ {
+ NickName = nickName;
+ IdentityUserId = identityUserId;
+ ContactInfo = contactInfo;
+ Position = position;
+ }
+ }
+}
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/UserManager.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/UserManager.cs
new file mode 100644
index 000000000..08ccdba57
--- /dev/null
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.Domain/PackageName/CompanyName/ProjectName/Users/UserManager.cs
@@ -0,0 +1,449 @@
+using Microsoft.Extensions.Logging;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Volo.Abp;
+using Volo.Abp.Domain.Services;
+using Volo.Abp.Identity;
+
+namespace PackageName.CompanyName.ProjectName.Users
+{
+ ///
+ /// 用户管理服务,用于处理用户的CRUD操作
+ ///
+ public class UserManager : DomainService, IUserManager
+ {
+ private readonly IUserRepository _userRepository;
+ private readonly IdentityUserManager _identityUserManager;
+ private readonly ILogger _logger;
+
+ public UserManager(
+ IUserRepository userRepository,
+ IdentityUserManager identityUserManager,
+ ILogger logger)
+ {
+ _userRepository = userRepository;
+ _identityUserManager = identityUserManager;
+ _logger = logger;
+ }
+
+ ///
+ /// 创建新用户
+ ///
+ /// 用户昵称
+ /// 用户密码
+ /// 联系方式
+ /// 职位
+ /// 是否启用
+ /// 创建的用户实体
+ public async Task CreateAsync(
+ string nickName,
+ string password,
+ string contactInfo = null,
+ string position = null,
+ bool isActive = true)
+ {
+ // 参数验证
+ if (string.IsNullOrWhiteSpace(nickName))
+ {
+ throw new UserFriendlyException("用户名不能为空");
+ }
+
+ // 密码校验
+ if (string.IsNullOrWhiteSpace(password) || password.Length < 6)
+ {
+ throw new UserFriendlyException("密码不能为空且长度不能少于6位");
+ }
+
+ // 检查用户昵称是否已存在
+ var existingUser = await _userRepository.FindAsync(u => u.NickName == nickName);
+ if (existingUser != null)
+ {
+ throw new UserFriendlyException($"昵称为 '{nickName}' 的用户已存在");
+ }
+
+ // 创建Identity用户
+ var identityUser = new IdentityUser(GuidGenerator.Create(), nickName, $"{nickName}@inspection.com");
+ var identityResult = await _identityUserManager.CreateAsync(identityUser, password);
+ if (!identityResult.Succeeded)
+ {
+ throw new UserFriendlyException("创建用户失败: " +
+ string.Join(", ", identityResult.Errors.Select(x => x.Description)));
+ }
+
+ // 设置用户状态
+ if (!isActive)
+ {
+ var lockoutResult = await _identityUserManager.SetLockoutEndDateAsync(identityUser, DateTimeOffset.MaxValue);
+ if (!lockoutResult.Succeeded)
+ {
+ throw new UserFriendlyException("设置用户状态失败: " +
+ string.Join(", ", lockoutResult.Errors.Select(e => e.Description)));
+ }
+ }
+ else
+ {
+ // 确保用户处于活动状态
+ await _identityUserManager.SetLockoutEnabledAsync(identityUser, false);
+ var lockoutResult = await _identityUserManager.SetLockoutEndDateAsync(identityUser, null);
+ if (!lockoutResult.Succeeded)
+ {
+ throw new UserFriendlyException("设置用户状态失败: " +
+ string.Join(", ", lockoutResult.Errors.Select(e => e.Description)));
+ }
+ }
+
+ // 创建业务用户
+ var user = new User(
+ GuidGenerator.Create(),
+ nickName,
+ identityUser.Id,
+ contactInfo,
+ position
+ );
+
+ // 保存用户
+ await _userRepository.InsertAsync(user, true);
+
+ _logger.LogInformation($"创建了新用户:{nickName},ID:{user.Id}");
+
+ return user;
+ }
+
+ ///
+ /// 更新用户信息
+ ///
+ /// 用户ID
+ /// 用户昵称
+ /// 用户密码(可选,如不修改则传入null)
+ /// 联系方式
+ /// 职位
+ /// 是否启用
+ /// 更新后的用户实体
+ public async Task UpdateAsync(
+ Guid id,
+ string nickName,
+ string password,
+ string contactInfo = null,
+ string position = null,
+ bool isActive = true)
+ {
+ // 最大重试次数
+ const int maxRetries = 3;
+ int retryCount = 0;
+
+ while (true)
+ {
+ try
+ {
+ // 每次尝试时重新获取最新的用户数据
+ var user = await _userRepository.GetAsync(id, false);
+ if (user == null)
+ {
+ throw new UserFriendlyException("用户不存在");
+ }
+
+ // 检查用户昵称是否已被其他用户使用
+ var existingUser = await _userRepository.FindAsync(u => u.NickName == nickName && u.Id != id);
+ if (existingUser != null)
+ {
+ throw new UserFriendlyException($"昵称为 '{nickName}' 的用户已存在");
+ }
+
+ // 更新Identity用户
+ var identityUser = await _identityUserManager.FindByIdAsync(user.IdentityUserId.ToString());
+ if (identityUser == null)
+ {
+ throw new UserFriendlyException("Identity用户不存在");
+ }
+
+ // 更新用户名
+ var usernameResult = await _identityUserManager.SetUserNameAsync(identityUser, nickName);
+ if (!usernameResult.Succeeded)
+ {
+ throw new UserFriendlyException("更新用户名失败: " +
+ string.Join(", ", usernameResult.Errors.Select(e => e.Description)));
+ }
+
+ // 更新电子邮件
+ var emailResult = await _identityUserManager.SetEmailAsync(identityUser, $"{nickName}@inspection.com");
+ if (!emailResult.Succeeded)
+ {
+ throw new UserFriendlyException("更新电子邮件失败: " +
+ string.Join(", ", emailResult.Errors.Select(e => e.Description)));
+ }
+
+ // 如果提供了新密码,则更新密码
+ if (!string.IsNullOrEmpty(password))
+ {
+ // 移除当前密码
+ await _identityUserManager.RemovePasswordAsync(identityUser);
+ // 设置新密码
+ var passwordResult = await _identityUserManager.AddPasswordAsync(identityUser, password);
+ if (!passwordResult.Succeeded)
+ {
+ throw new UserFriendlyException("更新密码失败: " +
+ string.Join(", ", passwordResult.Errors.Select(e => e.Description)));
+ }
+ }
+
+ // 设置用户状态
+ await SetUserActiveStatusAsync(id, isActive);
+
+ // 更新用户信息
+ user.NickName = nickName;
+ user.ContactInfo = contactInfo;
+ user.Position = position;
+
+ // 保存更新
+ await _userRepository.UpdateAsync(user, true);
+
+ _logger.LogInformation($"更新了用户信息:{nickName},ID:{user.Id}");
+
+ return user;
+ }
+ catch (Volo.Abp.Data.AbpDbConcurrencyException ex)
+ {
+ // 增加重试计数
+ retryCount++;
+
+ // 如果达到最大重试次数,则抛出用户友好的异常
+ if (retryCount >= maxRetries)
+ {
+ throw new UserFriendlyException(
+ "更新用户信息失败:数据已被其他用户修改。请刷新页面后重试。",
+ "409", ex.Message,
+ ex);
+ }
+
+ // 短暂延迟后重试
+ await Task.Delay(100 * retryCount); // 逐步增加延迟时间
+
+ // 记录重试信息
+ _logger.LogWarning($"检测到用户[{id}]更新时发生并发冲突,正在进行第{retryCount}次重试...");
+ }
+ }
+ }
+
+ ///
+ /// 删除用户
+ ///
+ /// 用户ID
+ /// 操作任务
+ public async Task DeleteAsync(Guid id)
+ {
+ // 最大重试次数
+ const int maxRetries = 3;
+ int retryCount = 0;
+
+ while (true)
+ {
+ try
+ {
+ // 每次尝试时重新获取最新的用户数据
+ var user = await _userRepository.GetAsync(id);
+ if (user == null)
+ {
+ _logger.LogWarning($"尝试删除不存在的用户,ID:{id}");
+ return; // 如果用户不存在,就不再继续处理
+ }
+
+ // 删除Identity用户
+ var identityUser = await _identityUserManager.FindByIdAsync(user.IdentityUserId.ToString());
+ if (identityUser != null)
+ {
+ var result = await _identityUserManager.DeleteAsync(identityUser);
+ if (!result.Succeeded)
+ {
+ throw new UserFriendlyException("删除Identity用户失败: " +
+ string.Join(", ", result.Errors.Select(e => e.Description)));
+ }
+ }
+
+ // 删除用户
+ await _userRepository.DeleteAsync(user);
+
+ _logger.LogInformation($"删除了用户,ID:{id}");
+ return;
+ }
+ catch (Volo.Abp.Data.AbpDbConcurrencyException ex)
+ {
+ // 增加重试计数
+ retryCount++;
+
+ // 如果达到最大重试次数,则抛出用户友好的异常
+ if (retryCount >= maxRetries)
+ {
+ throw new UserFriendlyException(
+ "删除用户失败:数据已被其他用户修改。请刷新页面后重试。",
+ "409", ex.Message,
+ ex);
+ }
+
+ // 短暂延迟后重试
+ await Task.Delay(100 * retryCount); // 逐步增加延迟时间
+
+ // 记录重试信息
+ _logger.LogWarning($"检测到用户[{id}]删除时发生并发冲突,正在进行第{retryCount}次重试...");
+ }
+ }
+ }
+
+ ///
+ /// 修改用户密码
+ ///
+ /// 用户ID
+ /// 当前密码
+ /// 新密码
+ /// 操作结果
+ public async Task ChangePasswordAsync(Guid id, string currentPassword, string newPassword)
+ {
+ if (string.IsNullOrEmpty(newPassword) || newPassword.Length < 6)
+ {
+ throw new UserFriendlyException("新密码不能为空且长度不能少于6位");
+ }
+
+ // 获取用户
+ var user = await _userRepository.GetAsync(id);
+ if (user == null)
+ {
+ throw new UserFriendlyException("用户不存在");
+ }
+
+ // 获取Identity用户
+ var identityUser = await _identityUserManager.FindByIdAsync(user.IdentityUserId.ToString());
+ if (identityUser == null)
+ {
+ throw new UserFriendlyException("Identity用户不存在");
+ }
+
+ // 修改密码
+ var result = await _identityUserManager.ChangePasswordAsync(identityUser, currentPassword, newPassword);
+ if (!result.Succeeded)
+ {
+ throw new UserFriendlyException("修改密码失败: " +
+ string.Join(", ", result.Errors.Select(e => e.Description)));
+ }
+
+ _logger.LogInformation($"用户[{user.NickName}]成功修改了密码");
+ }
+
+ ///
+ /// 重置用户密码(管理员操作)
+ ///
+ /// 用户ID
+ /// 新密码
+ /// 操作结果
+ public async Task ResetPasswordAsync(Guid id, string newPassword)
+ {
+ if (string.IsNullOrEmpty(newPassword) || newPassword.Length < 6)
+ {
+ throw new UserFriendlyException("新密码不能为空且长度不能少于6位");
+ }
+
+ // 获取用户
+ var user = await _userRepository.GetAsync(id);
+ if (user == null)
+ {
+ throw new UserFriendlyException("用户不存在");
+ }
+
+ // 获取Identity用户
+ var identityUser = await _identityUserManager.FindByIdAsync(user.IdentityUserId.ToString());
+ if (identityUser == null)
+ {
+ throw new UserFriendlyException("Identity用户不存在");
+ }
+
+ // 生成重置令牌
+ var token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityUser);
+
+ // 重置密码
+ var result = await _identityUserManager.ResetPasswordAsync(identityUser, token, newPassword);
+ if (!result.Succeeded)
+ {
+ throw new UserFriendlyException("重置密码失败: " +
+ string.Join(", ", result.Errors.Select(e => e.Description)));
+ }
+
+ _logger.LogInformation($"管理员重置了用户[{user.NickName}]的密码");
+ }
+
+ ///
+ /// 获取用户信息
+ ///
+ /// 用户ID
+ /// 用户实体
+ public async Task GetAsync(Guid id)
+ {
+ return await _userRepository.GetAsync(id);
+ }
+
+ ///
+ /// 根据Identity用户ID获取用户
+ ///
+ /// Identity用户ID
+ /// 用户实体,如果不存在则返回null
+ public async Task FindByIdentityUserIdAsync(Guid identityUserId)
+ {
+ return await _userRepository.FindAsync(u => u.IdentityUserId == identityUserId);
+ }
+
+ ///
+ /// 根据用户昵称查找用户
+ ///
+ /// 用户昵称
+ /// 用户实体,如果不存在则返回null
+ public async Task FindByNickNameAsync(string nickName)
+ {
+ return await _userRepository.FindAsync(u => u.NickName == nickName);
+ }
+
+ ///
+ /// 禁用或启用用户
+ ///
+ /// 用户ID
+ /// 是否启用
+ /// 操作任务
+ public async Task SetUserActiveStatusAsync(Guid id, bool isActive)
+ {
+ // 获取用户
+ var user = await _userRepository.GetAsync(id);
+ if (user == null)
+ {
+ throw new UserFriendlyException("用户不存在");
+ }
+
+ // 获取Identity用户
+ var identityUser = await _identityUserManager.FindByIdAsync(user.IdentityUserId.ToString());
+ if (identityUser == null)
+ {
+ throw new UserFriendlyException("Identity用户不存在");
+ }
+
+ // 设置用户状态
+ if (isActive)
+ {
+ // 启用用户
+ var result = await _identityUserManager.SetLockoutEndDateAsync(identityUser, null);
+ if (!result.Succeeded)
+ {
+ throw new UserFriendlyException($"启用用户失败: " +
+ string.Join(", ", result.Errors.Select(e => e.Description)));
+ }
+ }
+ else
+ {
+ // 禁用用户(设置永久锁定)
+ var result = await _identityUserManager.SetLockoutEndDateAsync(identityUser, DateTimeOffset.MaxValue);
+ if (!result.Succeeded)
+ {
+ throw new UserFriendlyException($"禁用用户失败: " +
+ string.Join(", ", result.Errors.Select(e => e.Description)));
+ }
+ }
+
+ _logger.LogInformation($"已{(isActive ? "启用" : "禁用")}用户[{user.NickName}]");
+ }
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName.CompanyName.ProjectName.EntityFrameworkCore.csproj b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName.CompanyName.ProjectName.EntityFrameworkCore.csproj
index a7904db7d..ada1145a0 100644
--- a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName.CompanyName.ProjectName.EntityFrameworkCore.csproj
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName.CompanyName.ProjectName.EntityFrameworkCore.csproj
@@ -28,6 +28,7 @@
+
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/IProjectNameDbContext.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/IProjectNameDbContext.cs
index c39c2923e..f3749036f 100644
--- a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/IProjectNameDbContext.cs
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/IProjectNameDbContext.cs
@@ -1,4 +1,6 @@
-using Volo.Abp.Data;
+using Microsoft.EntityFrameworkCore;
+using PackageName.CompanyName.ProjectName.Users;
+using Volo.Abp.Data;
using Volo.Abp.EntityFrameworkCore;
namespace PackageName.CompanyName.ProjectName.EntityFrameworkCore;
@@ -6,4 +8,5 @@ namespace PackageName.CompanyName.ProjectName.EntityFrameworkCore;
[ConnectionStringName(ProjectNameDbProperties.ConnectionStringName)]
public interface IProjectNameDbContext : IEfCoreDbContext
{
+ DbSet Users { get; }
}
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameDbContext.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameDbContext.cs
index 5cb2c015b..065418dc6 100644
--- a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameDbContext.cs
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameDbContext.cs
@@ -1,12 +1,18 @@
using LINGYUN.Abp.DataProtection.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
+using PackageName.CompanyName.ProjectName.Users;
using Volo.Abp.Data;
+using Volo.Abp.EntityFrameworkCore;
+using Volo.Abp.Identity.EntityFrameworkCore;
namespace PackageName.CompanyName.ProjectName.EntityFrameworkCore;
[ConnectionStringName(ProjectNameDbProperties.ConnectionStringName)]
-public class ProjectNameDbContext : AbpDataProtectionDbContext, IProjectNameDbContext
+// public class ProjectNameDbContext : AbpDataProtectionDbContext, IProjectNameDbContext
+public class ProjectNameDbContext : AbpDbContext, IProjectNameDbContext
{
+ public virtual DbSet Users { get; set; }
+
public ProjectNameDbContext(
DbContextOptions options) : base(options)
{
@@ -14,8 +20,9 @@ public class ProjectNameDbContext : AbpDataProtectionDbContext(b =>
+ {
+ b.ToTable(ProjectNameDbProperties.DbTablePrefix + "Users", ProjectNameDbProperties.DbSchema);
+ b.ConfigureByConvention();
+ b.Property(x => x.NickName).HasComment("用户名称");
+ b.Property(x => x.IdentityUserId).HasComment("Identity用户Id");
+
+ // 用户与IdentityUser的关系(一对一)
+ b.HasOne(x => x.IdentityUser)
+ .WithOne()
+ .HasForeignKey(u => u.IdentityUserId)
+ .IsRequired()
+ .OnDelete(DeleteBehavior.Cascade);
+ });
}
}
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameEntityFrameworkCoreModule.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameEntityFrameworkCoreModule.cs
index b18f43183..aac635d62 100644
--- a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameEntityFrameworkCoreModule.cs
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameEntityFrameworkCoreModule.cs
@@ -2,7 +2,9 @@
using LINGYUN.Abp.DataProtection.EntityFrameworkCore;
using LINGYUN.Abp.Saas.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
+using PackageName.CompanyName.ProjectName.Users;
using Volo.Abp.EntityFrameworkCore;
+using Volo.Abp.Identity.EntityFrameworkCore;
using Volo.Abp.Modularity;
#if MySQL
using Volo.Abp.EntityFrameworkCore.MySQL;
@@ -38,6 +40,7 @@ namespace PackageName.CompanyName.ProjectName.EntityFrameworkCore;
#elif PostgreSql
typeof(AbpEntityFrameworkCorePostgreSqlModule),
#endif
+ typeof(AbpIdentityEntityFrameworkCoreModule),
typeof(AbpSaasEntityFrameworkCoreModule))]
public class ProjectNameEntityFrameworkCoreModule : AbpModule
{
@@ -70,7 +73,8 @@ public class ProjectNameEntityFrameworkCoreModule : AbpModule
context.Services.AddAbpDbContext(options =>
{
- options.AddDefaultRepositories();
+ options.AddDefaultRepositories(true);
+ options.AddRepository();
});
}
}
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/Users/UserRepository.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/Users/UserRepository.cs
new file mode 100644
index 000000000..72d1575bf
--- /dev/null
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.EntityFrameworkCore/PackageName/CompanyName/ProjectName/Users/UserRepository.cs
@@ -0,0 +1,22 @@
+using Microsoft.EntityFrameworkCore;
+using PackageName.CompanyName.ProjectName.EntityFrameworkCore;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
+using Volo.Abp.EntityFrameworkCore;
+
+namespace PackageName.CompanyName.ProjectName.Users
+{
+ public class UserRepository : EfCoreRepository, IUserRepository
+ {
+ public UserRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider)
+ {
+ }
+
+ public override async Task> WithDetailsAsync()
+ {
+ return (await GetDbSetAsync()).Include(x => x.IdentityUser);
+ }
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.HttpApi/PackageName/CompanyName/ProjectName/Users/UserController.cs b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.HttpApi/PackageName/CompanyName/ProjectName/Users/UserController.cs
new file mode 100644
index 000000000..85d3f28f9
--- /dev/null
+++ b/aspnet-core/templates/aio/content/src/PackageName.CompanyName.ProjectName.HttpApi/PackageName/CompanyName/ProjectName/Users/UserController.cs
@@ -0,0 +1,134 @@
+using Microsoft.AspNetCore.Mvc;
+using PackageName.CompanyName.ProjectName.Users.Dtos;
+using System;
+using System.Threading.Tasks;
+using Volo.Abp;
+using Volo.Abp.Application.Dtos;
+
+namespace PackageName.CompanyName.ProjectName.Users
+{
+ ///
+ /// 用户管理控制器
+ ///
+ [RemoteService]
+ [Route("api/app/user")]
+ public class UserController : ProjectNameControllerBase
+ {
+ private readonly IUserAppService _userAppService;
+
+ public UserController(IUserAppService userAppService)
+ {
+ _userAppService = userAppService;
+ }
+
+ ///
+ /// 创建用户
+ ///
+ [HttpPost]
+ public async Task CreateAsync(CreateUpdateUserDto input)
+ {
+ return await _userAppService.CreateAsync(input);
+ }
+
+ ///
+ /// 更新用户
+ ///
+ [HttpPut("{id}")]
+ public async Task UpdateAsync(Guid id, CreateUpdateUserDto input)
+ {
+ return await _userAppService.UpdateAsync(id, input);
+ }
+
+ ///
+ /// 删除用户
+ ///
+ [HttpDelete("{id}")]
+ public async Task DeleteAsync(Guid id)
+ {
+ await _userAppService.DeleteAsync(id);
+ }
+
+ ///
+ /// 获取用户
+ ///
+ [HttpGet("{id}")]
+ public async Task GetAsync(Guid id)
+ {
+ return await _userAppService.GetAsync(id);
+ }
+
+ ///
+ /// 获取用户列表
+ ///
+ [HttpGet]
+ public async Task> GetListAsync(UserPagedAndSortedResultRequestDto input)
+ {
+ return await _userAppService.GetListAsync(input);
+ }
+
+ ///
+ /// 修改用户密码
+ ///
+ [HttpPost("{id}/change-password")]
+ public async Task ChangePasswordAsync(Guid id, [FromBody] ChangePasswordRequest request)
+ {
+ await _userAppService.ChangePasswordAsync(id, request.CurrentPassword, request.NewPassword);
+ }
+
+ ///
+ /// 重置用户密码(管理员操作)
+ ///
+ [HttpPost("{id}/reset-password")]
+ public async Task ResetPasswordAsync(Guid id, [FromBody] ResetPasswordRequest request)
+ {
+ await _userAppService.ResetPasswordAsync(id, request.NewPassword);
+ }
+
+ ///
+ /// 启用或禁用用户
+ ///
+ [HttpPost("{id}/set-active")]
+ public async Task SetUserActiveStatusAsync(Guid id, [FromBody] SetUserActiveRequest request)
+ {
+ await _userAppService.SetUserActiveStatusAsync(id, request.IsActive);
+ }
+ }
+
+ ///
+ /// 修改密码请求
+ ///
+ public class ChangePasswordRequest
+ {
+ ///
+ /// 当前密码
+ ///
+ public string CurrentPassword { get; set; }
+
+ ///
+ /// 新密码
+ ///
+ public string NewPassword { get; set; }
+ }
+
+ ///
+ /// 重置密码请求
+ ///
+ public class ResetPasswordRequest
+ {
+ ///
+ /// 新密码
+ ///
+ public string NewPassword { get; set; }
+ }
+
+ ///
+ /// 设置用户状态请求
+ ///
+ public class SetUserActiveRequest
+ {
+ ///
+ /// 是否启用
+ ///
+ public bool IsActive { get; set; }
+ }
+}
diff --git a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName.CompanyName.ProjectName.Application.Tests.csproj b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName.CompanyName.ProjectName.Application.Tests.csproj
index a661f6026..938edc57b 100644
--- a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName.CompanyName.ProjectName.Application.Tests.csproj
+++ b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName.CompanyName.ProjectName.Application.Tests.csproj
@@ -7,12 +7,17 @@
+
+
+
+
+
diff --git a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/DataSeeder/ProjectNameDataSeederTests.cs b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/DataSeeder/ProjectNameDataSeederTests.cs
new file mode 100644
index 000000000..dd8ff6727
--- /dev/null
+++ b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/DataSeeder/ProjectNameDataSeederTests.cs
@@ -0,0 +1,137 @@
+using Microsoft.Extensions.DependencyInjection;
+using PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore.DataSeeder;
+using PackageName.CompanyName.ProjectName.Users;
+using Shouldly;
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Volo.Abp.Data;
+using Volo.Abp.Domain.Repositories;
+using Volo.Abp.Identity;
+using Volo.Abp.Uow;
+using Xunit;
+
+namespace PackageName.CompanyName.ProjectName.DataSeeder
+{
+ ///
+ /// 数据种子初始化测试
+ ///
+ [Collection("Database")]
+ public class ProjectNameDataSeederTests : ProjectNameApplicationTestBase
+ {
+ private readonly IProjectNameDataSeeder _inspectionDataSeeder;
+ private readonly IRepository _userRepository;
+ private readonly IIdentityRoleRepository _identityRoleRepository;
+ private readonly IIdentityUserRepository _identityUserRepository;
+
+ public ProjectNameDataSeederTests()
+ {
+ _inspectionDataSeeder = GetRequiredService();
+ _userRepository = GetRequiredService>();
+ _identityRoleRepository = GetRequiredService();
+ _identityUserRepository = GetRequiredService();
+ }
+
+ [Fact]
+ public async Task Should_Seed_Data_Successfully()
+ {
+ // Arrange
+ var context = new DataSeedContext();
+
+ // Act
+ await _inspectionDataSeeder.SeedAsync(context);
+
+ // Assert - 使用单元工作方法包装所有数据库操作
+ await WithUnitOfWorkAsync(async () =>
+ {
+ // 测试角色
+ var roles = await _identityRoleRepository.GetListAsync();
+ roles.Count.ShouldBeGreaterThanOrEqualTo(7); // 至少应该有 7 个角色
+
+ var superAdminRole = await _identityRoleRepository.FindByNormalizedNameAsync("超级管理员".ToUpperInvariant());
+ superAdminRole.ShouldNotBeNull();
+ // 测试用户
+ var users = await _userRepository.GetListAsync();
+ users.Count.ShouldBeGreaterThanOrEqualTo(10); // 至少应该有 10 个用户
+
+ foreach (var user in users)
+ {
+ user.IdentityUserId.ShouldNotBe(Guid.Empty);
+
+ var identityUser = await _identityUserRepository.GetAsync(user.IdentityUserId);
+ identityUser.ShouldNotBeNull();
+ }
+
+ return true;
+ });
+ }
+
+ [Theory]
+ [InlineData("超级管理员")]
+ [InlineData("普通用户")]
+ public async Task Should_Create_Roles(string roleName)
+ {
+ // Arrange
+ var context = new DataSeedContext();
+ await _inspectionDataSeeder.SeedAsync(context);
+
+ // Act & Assert - 使用单元工作方法包装
+ await WithUnitOfWorkAsync(async () =>
+ {
+ var role = await _identityRoleRepository.FindByNormalizedNameAsync(roleName.ToUpperInvariant());
+ role.ShouldNotBeNull();
+ role.Name.ShouldBe(roleName);
+
+ return true;
+ });
+ }
+
+ [Theory]
+ [InlineData("testuser1")]
+ [InlineData("testuser2")]
+ public async Task Should_Create_Users(string nickName)
+ {
+ // Arrange
+ var context = new DataSeedContext();
+ await _inspectionDataSeeder.SeedAsync(context);
+
+ // Act & Assert - 使用单元工作方法包装
+ await WithUnitOfWorkAsync(async () =>
+ {
+ var users = await _userRepository.GetListAsync();
+ var user = users.FirstOrDefault(u => u.NickName == nickName);
+ user.ShouldNotBeNull();
+ user.NickName.ShouldBe(nickName);
+ user.IdentityUserId.ShouldNotBe(Guid.Empty);
+
+ var identityUser = await _identityUserRepository.GetAsync(user.IdentityUserId);
+ identityUser.ShouldNotBeNull();
+ identityUser.Name.ShouldBe(nickName);
+
+ return true;
+ });
+ }
+
+ // 添加单元工作方法
+ protected override Task WithUnitOfWorkAsync(Func> func)
+ {
+ return WithUnitOfWorkAsync(new AbpUnitOfWorkOptions(), func);
+ }
+
+ // 可选:添加重载方法以支持更多场景
+ protected async override Task WithUnitOfWorkAsync(AbpUnitOfWorkOptions options, Func> func)
+ {
+ using (var scope = ServiceProvider.CreateScope())
+ {
+ var uowManager = scope.ServiceProvider.GetRequiredService();
+
+ using (var uow = uowManager.Begin(options))
+ {
+ var result = await func();
+ await uow.CompleteAsync();
+ return result;
+ }
+ }
+ }
+ }
+}
diff --git a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/ProjectNameApplicationTestModule.cs b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/ProjectNameApplicationTestModule.cs
index 7e9fa658a..e9ac52aca 100644
--- a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/ProjectNameApplicationTestModule.cs
+++ b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/ProjectNameApplicationTestModule.cs
@@ -1,11 +1,65 @@
+using Hangfire;
+using Hangfire.MemoryStorage;
+using LINGYUN.Abp.Identity;
+using LINGYUN.Abp.Identity.Session;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore.DataSeeder;
+using PackageName.CompanyName.ProjectName.EntityFrameworkCore;
using Volo.Abp.Modularity;
+using Volo.Abp.PermissionManagement.Identity;
+using Volo.Abp.Security.Claims;
namespace PackageName.CompanyName.ProjectName;
[DependsOn(
typeof(ProjectNameDomainTestModule),
- typeof(ProjectNameApplicationModule)
+ typeof(ProjectNameApplicationModule),
+ typeof(AbpIdentityApplicationModule),
+ typeof(AbpPermissionManagementDomainIdentityModule),
+ typeof(ProjectNameEntityFrameworkCoreTestModule)
)]
public class ProjectNameApplicationTestModule : AbpModule
{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ // //设置ILogger为NullLogger
+ context.Services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
+ context.Services.AddTransient();
+ context.Services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance));
+ context.Services.AddTransient();
+ context.Services.AddTransient();
+
+ // 增加配置文件定义,在新建租户时需要
+ Configure(options =>
+ {
+ // 允许中文用户名
+ options.User.AllowedUserNameCharacters = null;
+ // 支持弱密码
+ options.Password.RequireDigit = false;
+ options.Password.RequiredLength = 1;
+ options.Password.RequireLowercase = false;
+ options.Password.RequireNonAlphanumeric = false;
+ options.Password.RequireUppercase = false;
+ // 添加默认的双因素令牌提供者配置
+ options.Tokens.ProviderMap.Add("Default", new TokenProviderDescriptor(typeof(EmailTokenProvider)));
+ });
+ Configure(options =>
+ {
+ options.IsDynamicClaimsEnabled = true;
+ });
+ Configure(options =>
+ {
+ options.IsCleanupEnabled = true;
+ });
+ // 配置Hangfire
+ context.Services.AddHangfire(config =>
+ {
+ config.UseMemoryStorage();
+ });
+ }
}
diff --git a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/TestFileProvider.cs b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/TestFileProvider.cs
new file mode 100644
index 000000000..c7a075cb2
--- /dev/null
+++ b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/TestFileProvider.cs
@@ -0,0 +1,64 @@
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Primitives;
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace PackageName.CompanyName.ProjectName;
+
+public class TestFileProvider : IFileProvider
+{
+ private readonly Dictionary _files;
+
+ public TestFileProvider()
+ {
+ _files = new Dictionary();
+ }
+
+ public IDirectoryContents GetDirectoryContents(string subpath)
+ {
+ return new NotFoundDirectoryContents();
+ }
+
+ public IFileInfo GetFileInfo(string subpath)
+ {
+ if (_files.TryGetValue(subpath, out var fileInfo))
+ {
+ return fileInfo;
+ }
+ return new NotFoundFileInfo(subpath);
+ }
+
+ public IChangeToken Watch(string filter)
+ {
+ return NullChangeToken.Singleton;
+ }
+
+ public void AddFile(string path, string contents)
+ {
+ _files[path] = new TestFileInfo(path, contents);
+ }
+}
+
+public class TestFileInfo : IFileInfo
+{
+ private readonly string _contents;
+
+ public TestFileInfo(string name, string contents)
+ {
+ Name = name;
+ _contents = contents;
+ }
+
+ public bool Exists => true;
+ public long Length => _contents.Length;
+ public string PhysicalPath => null;
+ public string Name { get; }
+ public DateTimeOffset LastModified => DateTimeOffset.UtcNow;
+ public bool IsDirectory => false;
+
+ public Stream CreateReadStream()
+ {
+ return new MemoryStream(System.Text.Encoding.UTF8.GetBytes(_contents));
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/TestHostEnvironment.cs b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/TestHostEnvironment.cs
new file mode 100644
index 000000000..ae140f85d
--- /dev/null
+++ b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/TestHostEnvironment.cs
@@ -0,0 +1,21 @@
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Hosting;
+using System;
+
+namespace PackageName.CompanyName.ProjectName;
+
+public class TestHostEnvironment : IHostEnvironment
+{
+ public TestHostEnvironment()
+ {
+ EnvironmentName = "Test";
+ ApplicationName = "TestApplication";
+ ContentRootPath = AppDomain.CurrentDomain.BaseDirectory;
+ ContentRootFileProvider = new PhysicalFileProvider(ContentRootPath);
+ }
+
+ public string EnvironmentName { get; set; }
+ public string ApplicationName { get; set; }
+ public string ContentRootPath { get; set; }
+ public IFileProvider ContentRootFileProvider { get; set; }
+}
\ No newline at end of file
diff --git a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/Users/UserAppServiceTests.cs b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/Users/UserAppServiceTests.cs
new file mode 100644
index 000000000..212d79fe2
--- /dev/null
+++ b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.Application.Tests/PackageName/CompanyName/ProjectName/Users/UserAppServiceTests.cs
@@ -0,0 +1,265 @@
+using PackageName.CompanyName.ProjectName.Users.Dtos;
+using Shouldly;
+using System;
+using System.Threading.Tasks;
+using Volo.Abp.Domain.Entities;
+using Volo.Abp.Validation;
+using Xunit;
+
+namespace PackageName.CompanyName.ProjectName.Users
+{
+ ///
+ /// UserAppService 的单元测试
+ ///
+ [Collection("Database")]
+ public class UserAppServiceTests : ProjectNameApplicationTestBase
+ {
+ private readonly IUserAppService _userAppService;
+ private readonly IUserManager _userManager;
+
+ public UserAppServiceTests()
+ {
+ _userAppService = GetRequiredService();
+ _userManager = GetRequiredService();
+ }
+
+ [Theory]
+ [InlineData("testuser1", "Test123456!", true)]
+ [InlineData("testuser2", "Test123456!", false)]
+ public async Task Should_Create_User(
+ string nickName,
+ string password,
+ bool isActive)
+ {
+ // Arrange
+ var input = new CreateUpdateUserDto
+ {
+ NickName = nickName,
+ Password = password,
+ IsActive = isActive
+ };
+
+ // Act
+ var result = await _userAppService.CreateAsync(input);
+
+ // Assert
+ result.ShouldNotBeNull();
+ result.NickName.ShouldBe(nickName);
+ result.IsActive.ShouldBe(isActive);
+ }
+
+ [Theory]
+ [InlineData("", "Test123456!", "用户名称不能为空")]
+ [InlineData("test", "123", "密码长度必须在6-20个字符之间")]
+ public async Task Should_Not_Create_User_With_Invalid_Input(string nickName, string password,
+ string expectedErrorMessage)
+ {
+ // Arrange
+ var input = new CreateUpdateUserDto
+ {
+ NickName = nickName,
+ Password = password
+ };
+
+ // Act & Assert
+ var exception = await Assert.ThrowsAsync(async () =>
+ {
+ await _userAppService.CreateAsync(input);
+ });
+
+ exception.ValidationErrors.ShouldContain(x => x.ErrorMessage.Contains(expectedErrorMessage));
+ }
+
+ [Fact]
+ public async Task Should_Get_User_List()
+ {
+ // Arrange
+ await CreateTestUserAsync("testuser1", "Test123456!");
+ await CreateTestUserAsync("testuser2", "Test123456!");
+
+ // Act
+ var result = await _userAppService.GetListAsync(
+ new UserPagedAndSortedResultRequestDto
+ {
+ MaxResultCount = 10,
+ SkipCount = 0,
+ Sorting = "NickName"
+ });
+
+ // Assert
+ result.ShouldNotBeNull();
+ result.TotalCount.ShouldBeGreaterThanOrEqualTo(2);
+ result.Items.ShouldContain(x => x.NickName == "testuser1");
+ result.Items.ShouldContain(x => x.NickName == "testuser2");
+ }
+
+ [Fact]
+ public async Task Should_Filter_Users_By_NickName()
+ {
+ // Arrange
+ await CreateTestUserAsync("testuser1", "Test123456!");
+ await CreateTestUserAsync("testuser2", "Test123456!");
+ await CreateTestUserAsync("otheruser", "Test123456!");
+
+ // Act
+ var result = await _userAppService.GetListAsync(
+ new UserPagedAndSortedResultRequestDto
+ {
+ MaxResultCount = 10,
+ SkipCount = 0,
+ Sorting = "NickName",
+ NickName = "testuser"
+ });
+
+ // Assert
+ result.ShouldNotBeNull();
+ result.TotalCount.ShouldBe(2);
+ result.Items.ShouldContain(x => x.NickName == "testuser1");
+ result.Items.ShouldContain(x => x.NickName == "testuser2");
+ result.Items.ShouldNotContain(x => x.NickName == "otheruser");
+ }
+
+ [Fact]
+ public async Task Should_Update_User()
+ {
+ // Arrange
+ var user = await CreateTestUserAsync("updatetest", "Test123456!");
+ var updateInput = new CreateUpdateUserDto
+ {
+ NickName = "updateduser",
+ Password = "NewPassword123!",
+ ContactInfo = "13800138000",
+ Position = "开发工程师",
+ IsActive = true
+ };
+
+ // Act
+ var result = await _userAppService.UpdateAsync(user.Id, updateInput);
+
+ // Assert
+ result.ShouldNotBeNull();
+ result.NickName.ShouldBe("updateduser");
+ result.ContactInfo.ShouldBe("13800138000");
+ result.Position.ShouldBe("开发工程师");
+
+ // 验证更新后的用户信息
+ var updatedUser = await _userAppService.GetAsync(user.Id);
+ updatedUser.NickName.ShouldBe("updateduser");
+ }
+
+ [Fact]
+ public async Task Should_Not_Update_Non_Existing_User()
+ {
+ // Arrange
+ var input = new CreateUpdateUserDto
+ {
+ NickName = "testuser",
+ Password = "Test123456!"
+ };
+
+ // Act & Assert
+ await Assert.ThrowsAsync(async () =>
+ {
+ await _userAppService.UpdateAsync(Guid.NewGuid(), input);
+ });
+ }
+
+ [Fact]
+ public async Task Should_Delete_User()
+ {
+ // Arrange
+ var user = await CreateTestUserAsync("deletetest", "Test123456!");
+
+ // Act
+ await _userAppService.DeleteAsync(user.Id);
+
+ // Assert - 尝试获取已删除的用户应该抛出异常
+ await Assert.ThrowsAsync(async () =>
+ {
+ await _userAppService.GetAsync(user.Id);
+ });
+ }
+
+ [Fact]
+ public async Task Should_Change_User_Password()
+ {
+ // Arrange
+ var user = await CreateTestUserAsync("passwordtest", "OldPassword123!");
+
+ // Act & Assert
+ await _userAppService.ChangePasswordAsync(user.Id, "OldPassword123!", "NewPassword123!");
+
+ // 尝试用新密码登录(这个需要集成测试才能完整测试)
+ // 这里我们只是验证方法执行不会抛出异常
+ }
+
+ [Fact]
+ public async Task Should_Reset_User_Password()
+ {
+ // Arrange
+ var user = await CreateTestUserAsync("resetpasswordtest", "OldPassword123!");
+
+ // Act & Assert
+ await _userAppService.ResetPasswordAsync(user.Id, "NewPassword123!");
+
+ // 同样,完整测试需要验证用户能用新密码登录,这需要集成测试
+ }
+
+ [Fact]
+ public async Task Should_Set_User_Active_Status()
+ {
+ // Arrange
+ var user = await CreateTestUserAsync("activestatustest", "Password123!");
+
+ // Act
+ await _userAppService.SetUserActiveStatusAsync(user.Id, false);
+ var disabledUser = await _userAppService.GetAsync(user.Id);
+
+ await _userAppService.SetUserActiveStatusAsync(user.Id, true);
+ var enabledUser = await _userAppService.GetAsync(user.Id);
+
+ // Assert
+ disabledUser.IsActive.ShouldBeFalse();
+ enabledUser.IsActive.ShouldBeTrue();
+ }
+
+ [Theory]
+ [InlineData("13900000000", "工程师")]
+ [InlineData("13800000000", "设计师")]
+ [InlineData(null, null)]
+ public async Task Should_Create_User_With_Optional_Fields(string contactInfo, string position)
+ {
+ // Arrange
+ var input = new CreateUpdateUserDto
+ {
+ NickName = $"user_{Guid.NewGuid():N}",
+ Password = "Test123456!",
+ ContactInfo = contactInfo,
+ Position = position
+ };
+
+ // Act
+ var result = await _userAppService.CreateAsync(input);
+
+ // Assert
+ result.ShouldNotBeNull();
+ result.ContactInfo.ShouldBe(contactInfo);
+ result.Position.ShouldBe(position);
+ }
+
+ private async Task CreateTestUserAsync(string nickName, string password)
+ {
+ return await WithUnitOfWorkAsync(async () =>
+ {
+ var input = new CreateUpdateUserDto
+ {
+ NickName = nickName,
+ Password = password,
+ IsActive = true
+ };
+
+ return await _userAppService.CreateAsync(input);
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.EntityFrameworkCore.Tests/PackageName.CompanyName.ProjectName.EntityFrameworkCore.Tests.csproj b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.EntityFrameworkCore.Tests/PackageName.CompanyName.ProjectName.EntityFrameworkCore.Tests.csproj
index 0a3e9f02c..944bbe75a 100644
--- a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.EntityFrameworkCore.Tests/PackageName.CompanyName.ProjectName.EntityFrameworkCore.Tests.csproj
+++ b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.EntityFrameworkCore.Tests/PackageName.CompanyName.ProjectName.EntityFrameworkCore.Tests.csproj
@@ -8,10 +8,14 @@
-
+
+
+
+
+
diff --git a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.EntityFrameworkCore.Tests/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameEntityFrameworkCoreTestModule.cs b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.EntityFrameworkCore.Tests/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameEntityFrameworkCoreTestModule.cs
index da8ce7c6c..b5fff765e 100644
--- a/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.EntityFrameworkCore.Tests/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameEntityFrameworkCoreTestModule.cs
+++ b/aspnet-core/templates/aio/content/tests/PackageName.CompanyName.ProjectName.EntityFrameworkCore.Tests/PackageName/CompanyName/ProjectName/EntityFrameworkCore/ProjectNameEntityFrameworkCoreTestModule.cs
@@ -1,38 +1,74 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
+using PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore;
using System;
+using Volo.Abp;
+using Volo.Abp.Data;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.Modularity;
+using Volo.Abp.Threading;
+using Volo.Abp.Timing;
using Volo.Abp.Uow;
namespace PackageName.CompanyName.ProjectName.EntityFrameworkCore;
[DependsOn(
typeof(ProjectNameTestBaseModule),
- typeof(ProjectNameEntityFrameworkCoreModule)
- )]
+ typeof(ProjectNameEntityFrameworkCoreModule),
+ typeof(SingleMigrationsEntityFrameworkCoreModule)
+)]
public class ProjectNameEntityFrameworkCoreTestModule : AbpModule
{
+ // 数据库配置
+ private const string DefaultPostgresConnectionString =
+ "Host=127.0.0.1;Port=5432;Database=test_db;User Id=postgres;Password=postgres;";
+
public override void ConfigureServices(ServiceConfigurationContext context)
{
- context.Services.AddEntityFrameworkInMemoryDatabase();
+ var connectionString = Environment.GetEnvironmentVariable("TEST_CONNECTION_STRING") ??
+ DefaultPostgresConnectionString;
- var databaseName = Guid.NewGuid().ToString();
+ // 配置数据库连接字符串
+ Configure(options =>
+ {
+ options.ConnectionStrings.Default = connectionString;
+ });
- Configure(options =>
+ Configure(options => { options.Kind = DateTimeKind.Utc; });
+ context.Services.AddAbpDbContext(options =>
{
- options.Configure(abpDbContextConfigurationContext =>
- {
- abpDbContextConfigurationContext.DbContextOptions.EnableDetailedErrors();
- abpDbContextConfigurationContext.DbContextOptions.EnableSensitiveDataLogging();
+ options.AddDefaultRepositories(true);
+ });
- abpDbContextConfigurationContext.DbContextOptions.UseInMemoryDatabase(databaseName);
- });
+ // 配置所有DbContext
+ Configure(options =>
+ {
+ AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
+ options.UseNpgsql();
});
Configure(options =>
{
- options.TransactionBehavior = UnitOfWorkTransactionBehavior.Disabled; //EF in-memory database does not support transactions
+ options.TransactionBehavior = UnitOfWorkTransactionBehavior.Disabled;
});
}
-}
+
+ public override void OnPreApplicationInitialization(ApplicationInitializationContext context)
+ {
+ var dbContext = context.ServiceProvider.GetRequiredService();
+ // 重置数据库
+ dbContext.Database.EnsureDeleted();
+ // // 创建数据库
+ dbContext.Database.EnsureCreated();
+ dbContext.Database.GenerateCreateScript();
+ // dbContext.Database.Migrate();
+
+ // 初始化种子数据
+ var dataSeeder = context.ServiceProvider.GetRequiredService();
+ AsyncHelper.RunSync(() => dataSeeder.SeedAsync());
+ }
+
+ public override void OnApplicationShutdown(ApplicationShutdownContext context)
+ {
+ }
+}
\ No newline at end of file