committed by
GitHub
49 changed files with 2924 additions and 836 deletions
@ -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 <repository-url> |
|||
cd <repository-path>/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 |
|||
<Version>8.3.0</Version> |
|||
```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. |
|||
|
|||
@ -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 <repository-url> |
|||
cd <repository-path>/aspnet-core/templates/content |
|||
``` |
|||
### 第二步:配置测试环境 |
|||
|
|||
2. 修改版本号 |
|||
编辑项目文件更新版本号: |
|||
- 微服务模板:`../PackageName.CompanyName.ProjectName.csproj` |
|||
- 单体应用模板:`../PackageName.CompanyName.ProjectName.AIO.csproj` |
|||
如需修改 `ProjectNameEntityFrameworkCoreTestModule.cs` 中的连接字符串: |
|||
|
|||
```xml |
|||
<Version>8.3.0</Version> |
|||
```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 发布地址和密钥 |
|||
- 发布前建议进行完整测试 |
|||
当从此模板创建新项目时,您将指定这些值,它们将在整个解决方案中进行替换。 |
|||
|
|||
@ -0,0 +1,15 @@ |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Data; |
|||
|
|||
namespace PackageName.CompanyName.ProjectName.AIO.EntityFrameworkCore.DataSeeder |
|||
{ |
|||
public interface IProjectNameDataSeeder |
|||
{ |
|||
/// <summary>
|
|||
/// 初始化数据
|
|||
/// </summary>
|
|||
/// <param name="context">数据种子上下文</param>
|
|||
/// <returns>任务</returns>
|
|||
Task SeedAsync(DataSeedContext context); |
|||
} |
|||
} |
|||
@ -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<ProjectNameDataSeeder> _logger; |
|||
private readonly IRepository<User, Guid> _userRepository; |
|||
private readonly IdentityUserManager _identityUserManager; |
|||
private readonly IdentityRoleManager _identityRoleManager; |
|||
private readonly IUnitOfWorkManager _unitOfWorkManager; |
|||
|
|||
/// <summary>
|
|||
/// 构造函数
|
|||
/// </summary>
|
|||
public ProjectNameDataSeeder( |
|||
ICurrentTenant currentTenant, |
|||
IGuidGenerator guidGenerator, |
|||
ILogger<ProjectNameDataSeeder> logger, |
|||
IRepository<User, Guid> userRepository, |
|||
IdentityUserManager identityUserManager, |
|||
IdentityRoleManager identityRoleManager, |
|||
IUnitOfWorkManager unitOfWorkManager) |
|||
{ |
|||
_currentTenant = currentTenant; |
|||
_guidGenerator = guidGenerator; |
|||
_logger = logger; |
|||
_userRepository = userRepository; |
|||
_identityUserManager = identityUserManager; |
|||
_identityRoleManager = identityRoleManager; |
|||
_unitOfWorkManager = unitOfWorkManager; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 初始化数据
|
|||
/// </summary>
|
|||
/// <param name="context">数据种子上下文</param>
|
|||
/// <returns>任务</returns>
|
|||
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("数据初始化完成"); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 初始化角色数据
|
|||
/// </summary>
|
|||
private async Task SeedRolesAsync() |
|||
{ |
|||
// 超级管理员
|
|||
await CreateRoleIfNotExistsAsync( |
|||
"超级管理员", |
|||
"系统超级管理员,拥有所有权限"); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 创建角色(如果不存在)
|
|||
/// </summary>
|
|||
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}"); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 初始化用户数据
|
|||
/// </summary>
|
|||
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", "超级管理员"); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 创建用户(如果不存在)
|
|||
/// </summary>
|
|||
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}"); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
using System; |
|||
using System.ComponentModel.DataAnnotations; |
|||
|
|||
namespace PackageName.CompanyName.ProjectName.Users.Dtos |
|||
{ |
|||
[Serializable] |
|||
public class CreateUpdateUserDto |
|||
{ |
|||
/// <summary>
|
|||
/// 用户名称
|
|||
/// </summary>
|
|||
[Required(ErrorMessage = "用户名称不能为空")] |
|||
[StringLength(50, ErrorMessage = "用户名称长度不能超过50个字符")] |
|||
public string NickName { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 密码
|
|||
/// </summary>
|
|||
[StringLength(20, MinimumLength = 6, ErrorMessage = "密码长度必须在6-20个字符之间")] |
|||
public string Password { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 联系方式
|
|||
/// </summary>
|
|||
public string ContactInfo { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 职位
|
|||
/// </summary>
|
|||
public string Position { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 是否启用
|
|||
/// </summary>
|
|||
public bool IsActive { get; set; } = true; |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace PackageName.CompanyName.ProjectName.Users.Dtos |
|||
{ |
|||
[Serializable] |
|||
public class UserDto : FullAuditedEntityDto<Guid> |
|||
{ |
|||
/// <summary>
|
|||
/// 用户名称
|
|||
/// </summary>
|
|||
public string NickName { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Identity用户Id
|
|||
/// </summary>
|
|||
public Guid IdentityUserId { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 用户状态
|
|||
/// </summary>
|
|||
public bool IsActive { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 联系方式
|
|||
/// </summary>
|
|||
public string ContactInfo { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 职位
|
|||
/// </summary>
|
|||
public string Position { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 角色名称
|
|||
/// </summary>
|
|||
public string RoleNames { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace PackageName.CompanyName.ProjectName.Users.Dtos |
|||
{ |
|||
[Serializable] |
|||
public class UserItemDto : FullAuditedEntityDto<Guid> |
|||
{ |
|||
/// <summary>
|
|||
/// 用户名称
|
|||
/// </summary>
|
|||
public string NickName { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Identity用户Id
|
|||
/// </summary>
|
|||
public Guid IdentityUserId { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 用户状态
|
|||
/// </summary>
|
|||
public bool IsActive { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 联系方式
|
|||
/// </summary>
|
|||
public string ContactInfo { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 职位
|
|||
/// </summary>
|
|||
public string Position { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 角色名称
|
|||
/// </summary>
|
|||
public string RoleNames { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using System; |
|||
using Volo.Abp.Application.Dtos; |
|||
|
|||
namespace PackageName.CompanyName.ProjectName.Users.Dtos |
|||
{ |
|||
[Serializable] |
|||
public class UserPagedAndSortedResultRequestDto : PagedAndSortedResultRequestDto |
|||
{ |
|||
/// <summary>
|
|||
/// 用户名称
|
|||
/// </summary>
|
|||
public string NickName { get; set; } |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// 用户应用服务接口
|
|||
/// </summary>
|
|||
public interface IUserAppService : |
|||
IApplicationService |
|||
{ |
|||
/// <summary>
|
|||
/// 创建用户
|
|||
/// </summary>
|
|||
Task<UserDto> CreateAsync(CreateUpdateUserDto input); |
|||
|
|||
/// <summary>
|
|||
/// 更新用户
|
|||
/// </summary>
|
|||
Task<UserDto> UpdateAsync(Guid id, CreateUpdateUserDto input); |
|||
|
|||
/// <summary>
|
|||
/// 删除用户
|
|||
/// </summary>
|
|||
Task DeleteAsync(Guid id); |
|||
|
|||
/// <summary>
|
|||
/// 获取用户
|
|||
/// </summary>
|
|||
Task<UserDto> GetAsync(Guid id); |
|||
|
|||
/// <summary>
|
|||
/// 获取用户列表
|
|||
/// </summary>
|
|||
Task<PagedResultDto<UserItemDto>> GetListAsync(UserPagedAndSortedResultRequestDto input); |
|||
|
|||
/// <summary>
|
|||
/// 修改用户密码
|
|||
/// </summary>
|
|||
Task ChangePasswordAsync(Guid id, string currentPassword, string newPassword); |
|||
|
|||
/// <summary>
|
|||
/// 重置用户密码(管理员操作)
|
|||
/// </summary>
|
|||
Task ResetPasswordAsync(Guid id, string newPassword); |
|||
|
|||
/// <summary>
|
|||
/// 启用或禁用用户
|
|||
/// </summary>
|
|||
Task SetUserActiveStatusAsync(Guid id, bool isActive); |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// 用户
|
|||
/// </summary>
|
|||
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; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 创建用户
|
|||
/// </summary>
|
|||
[Authorize(ProjectNamePermissions.User.Create)] |
|||
public async Task<UserDto> 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); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 更新用户
|
|||
/// </summary>
|
|||
[Authorize(ProjectNamePermissions.User.Update)] |
|||
public async Task<UserDto> 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); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 删除用户
|
|||
/// </summary>
|
|||
[Authorize(ProjectNamePermissions.User.Delete)] |
|||
public Task DeleteAsync(Guid id) |
|||
{ |
|||
return _userManager.DeleteAsync(id); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取用户
|
|||
/// </summary>
|
|||
[Authorize(ProjectNamePermissions.User.Default)] |
|||
public async Task<UserDto> GetAsync(Guid id) |
|||
{ |
|||
var user = await _userManager.GetAsync(id); |
|||
return await MapToUserDtoAsync(user); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取用户列表
|
|||
/// </summary>
|
|||
[Authorize(ProjectNamePermissions.User.Default)] |
|||
public async Task<PagedResultDto<UserItemDto>> 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<UserItemDto>(); |
|||
foreach (var user in users) |
|||
{ |
|||
var userDto = ObjectMapper.Map<User, UserItemDto>(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<UserItemDto>(totalCount, userDtos); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 修改用户密码
|
|||
/// </summary>
|
|||
[Authorize] |
|||
public async Task ChangePasswordAsync(Guid id, string currentPassword, string newPassword) |
|||
{ |
|||
await _userManager.ChangePasswordAsync(id, currentPassword, newPassword); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 重置用户密码(管理员操作)
|
|||
/// </summary>
|
|||
[Authorize(ProjectNamePermissions.User.Update)] |
|||
public async Task ResetPasswordAsync(Guid id, string newPassword) |
|||
{ |
|||
await _userManager.ResetPasswordAsync(id, newPassword); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 启用或禁用用户
|
|||
/// </summary>
|
|||
[Authorize(ProjectNamePermissions.User.Update)] |
|||
public async Task SetUserActiveStatusAsync(Guid id, bool isActive) |
|||
{ |
|||
await _userManager.SetUserActiveStatusAsync(id, isActive); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 创建基础查询,应用过滤条件
|
|||
/// </summary>
|
|||
protected async virtual Task<IQueryable<User>> CreateFilteredQueryAsync( |
|||
UserPagedAndSortedResultRequestDto input) |
|||
{ |
|||
// 获取基础查询,并加载相关实体
|
|||
var query = await _userRepository.WithDetailsAsync( |
|||
x => x.IdentityUser); |
|||
|
|||
// 应用过滤条件
|
|||
return query |
|||
.WhereIf(!string.IsNullOrWhiteSpace(input.NickName), |
|||
x => x.NickName.Contains(input.NickName)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 将User实体映射为UserDto,并填充权限信息
|
|||
/// </summary>
|
|||
private async Task<UserDto> MapToUserDtoAsync(User user) |
|||
{ |
|||
var userDto = ObjectMapper.Map<User, UserDto>(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; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
using System; |
|||
using Volo.Abp.Auditing; |
|||
|
|||
namespace PackageName.CompanyName.ProjectName.TreeCodes |
|||
{ |
|||
/// <summary>
|
|||
/// 定义具有树形编码的实体接口
|
|||
/// </summary>
|
|||
public interface IHaveTreeCode : IHasCreationTime |
|||
{ |
|||
/// <summary>
|
|||
/// 树形编码
|
|||
/// </summary>
|
|||
string TreeCode { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 父级Id
|
|||
/// </summary>
|
|||
Guid? ParentId { get; set; } |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// 树形编码生成器接口
|
|||
/// </summary>
|
|||
public interface ITreeCodeGenerator |
|||
{ |
|||
/// <summary>
|
|||
/// 生成树形编码
|
|||
/// </summary>
|
|||
/// <typeparam name="TEntity">实体类型</typeparam>
|
|||
/// <param name="repository">仓储</param>
|
|||
/// <param name="parentId">父级Id</param>
|
|||
/// <returns>生成的树形编码</returns>
|
|||
Task<string> GenerateAsync<TEntity>( |
|||
IRepository<TEntity, Guid> repository, |
|||
Guid? parentId) |
|||
where TEntity : class, IEntity<Guid>, IHaveTreeCode; |
|||
|
|||
/// <summary>
|
|||
/// 更新节点及其所有子节点的TreeCode
|
|||
/// </summary>
|
|||
Task UpdateTreeCodesAsync<TEntity>( |
|||
IRepository<TEntity, Guid> repository, |
|||
Guid entityId) |
|||
where TEntity : class, IEntity<Guid>, IHaveTreeCode; |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// 树形编码生成器
|
|||
/// </summary>
|
|||
public class TreeCodeGenerator : ITreeCodeGenerator, ISingletonDependency |
|||
{ |
|||
/// <summary>
|
|||
/// 生成树形编码
|
|||
/// </summary>
|
|||
/// <typeparam name="TEntity">实体类型</typeparam>
|
|||
/// <param name="repository">仓储</param>
|
|||
/// <param name="parentId">父级Id</param>
|
|||
/// <returns>生成的树形编码</returns>
|
|||
public async virtual Task<string> GenerateAsync<TEntity>( |
|||
IRepository<TEntity, Guid> repository, |
|||
Guid? parentId) |
|||
where TEntity : class, IEntity<Guid>, IHaveTreeCode |
|||
{ |
|||
if (!parentId.HasValue) |
|||
{ |
|||
// 生成根节点编码
|
|||
return await GenerateRootCodeAsync(repository); |
|||
} |
|||
else |
|||
{ |
|||
// 生成子节点编码
|
|||
return await GenerateChildCodeAsync(repository, parentId.Value); |
|||
} |
|||
} |
|||
|
|||
private async Task<string> GenerateRootCodeAsync<TEntity>( |
|||
IRepository<TEntity, Guid> repository) |
|||
where TEntity : class, IEntity<Guid>, 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<string> GenerateChildCodeAsync<TEntity>( |
|||
IRepository<TEntity, Guid> repository, |
|||
Guid parentId) |
|||
where TEntity : class, IEntity<Guid>, 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}"; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 更新节点及其所有子节点的TreeCode
|
|||
/// </summary>
|
|||
public async virtual Task UpdateTreeCodesAsync<TEntity>( |
|||
IRepository<TEntity, Guid> repository, |
|||
Guid entityId) |
|||
where TEntity : class, IEntity<Guid>, 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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,69 @@ |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.Domain.Services; |
|||
|
|||
namespace PackageName.CompanyName.ProjectName.Users |
|||
{ |
|||
/// <summary>
|
|||
/// 用户管理服务接口
|
|||
/// </summary>
|
|||
public interface IUserManager : IDomainService |
|||
{ |
|||
/// <summary>
|
|||
/// 创建新用户
|
|||
/// </summary>
|
|||
Task<User> CreateAsync( |
|||
string nickName, |
|||
string password, |
|||
string contactInfo = null, |
|||
string position = null, |
|||
bool isActive = true |
|||
); |
|||
|
|||
/// <summary>
|
|||
/// 更新用户信息
|
|||
/// </summary>
|
|||
Task<User> UpdateAsync( |
|||
Guid id, |
|||
string nickName, |
|||
string password, |
|||
string contactInfo = null, |
|||
string position = null, |
|||
bool isActive = true); |
|||
|
|||
/// <summary>
|
|||
/// 删除用户
|
|||
/// </summary>
|
|||
Task DeleteAsync(Guid id); |
|||
|
|||
/// <summary>
|
|||
/// 修改用户密码
|
|||
/// </summary>
|
|||
Task ChangePasswordAsync(Guid id, string currentPassword, string newPassword); |
|||
|
|||
/// <summary>
|
|||
/// 重置用户密码
|
|||
/// </summary>
|
|||
Task ResetPasswordAsync(Guid id, string newPassword); |
|||
|
|||
/// <summary>
|
|||
/// 获取用户信息
|
|||
/// </summary>
|
|||
Task<User> GetAsync(Guid id); |
|||
|
|||
/// <summary>
|
|||
/// 根据Identity用户ID获取用户
|
|||
/// </summary>
|
|||
Task<User> FindByIdentityUserIdAsync(Guid identityUserId); |
|||
|
|||
/// <summary>
|
|||
/// 根据用户昵称查找用户
|
|||
/// </summary>
|
|||
Task<User> FindByNickNameAsync(string nickName); |
|||
|
|||
/// <summary>
|
|||
/// 禁用或启用用户
|
|||
/// </summary>
|
|||
Task SetUserActiveStatusAsync(Guid id, bool isActive); |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using System; |
|||
using Volo.Abp.Domain.Repositories; |
|||
|
|||
namespace PackageName.CompanyName.ProjectName.Users |
|||
{ |
|||
public interface IUserRepository : IRepository<User, Guid> |
|||
{ |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// 用户实体
|
|||
/// </summary>
|
|||
public class User : FullAuditedEntity<Guid> |
|||
{ |
|||
/// <summary>
|
|||
/// 用户名称
|
|||
/// </summary>
|
|||
[MaxLength(50)] |
|||
public string NickName { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Identity用户Id
|
|||
/// </summary>
|
|||
public Guid IdentityUserId { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Identity用户
|
|||
/// </summary>
|
|||
public virtual IdentityUser IdentityUser { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 联系方式
|
|||
/// </summary>
|
|||
[MaxLength(50)] |
|||
public string ContactInfo { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 职位
|
|||
/// </summary>
|
|||
[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; |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// 用户管理服务,用于处理用户的CRUD操作
|
|||
/// </summary>
|
|||
public class UserManager : DomainService, IUserManager |
|||
{ |
|||
private readonly IUserRepository _userRepository; |
|||
private readonly IdentityUserManager _identityUserManager; |
|||
private readonly ILogger<UserManager> _logger; |
|||
|
|||
public UserManager( |
|||
IUserRepository userRepository, |
|||
IdentityUserManager identityUserManager, |
|||
ILogger<UserManager> logger) |
|||
{ |
|||
_userRepository = userRepository; |
|||
_identityUserManager = identityUserManager; |
|||
_logger = logger; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 创建新用户
|
|||
/// </summary>
|
|||
/// <param name="nickName">用户昵称</param>
|
|||
/// <param name="password">用户密码</param>
|
|||
/// <param name="contactInfo">联系方式</param>
|
|||
/// <param name="position">职位</param>
|
|||
/// <param name="isActive">是否启用</param>
|
|||
/// <returns>创建的用户实体</returns>
|
|||
public async Task<User> 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; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 更新用户信息
|
|||
/// </summary>
|
|||
/// <param name="id">用户ID</param>
|
|||
/// <param name="nickName">用户昵称</param>
|
|||
/// <param name="password">用户密码(可选,如不修改则传入null)</param>
|
|||
/// <param name="contactInfo">联系方式</param>
|
|||
/// <param name="position">职位</param>
|
|||
/// <param name="isActive">是否启用</param>
|
|||
/// <returns>更新后的用户实体</returns>
|
|||
public async Task<User> 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}次重试..."); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 删除用户
|
|||
/// </summary>
|
|||
/// <param name="id">用户ID</param>
|
|||
/// <returns>操作任务</returns>
|
|||
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}次重试..."); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 修改用户密码
|
|||
/// </summary>
|
|||
/// <param name="id">用户ID</param>
|
|||
/// <param name="currentPassword">当前密码</param>
|
|||
/// <param name="newPassword">新密码</param>
|
|||
/// <returns>操作结果</returns>
|
|||
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}]成功修改了密码"); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 重置用户密码(管理员操作)
|
|||
/// </summary>
|
|||
/// <param name="id">用户ID</param>
|
|||
/// <param name="newPassword">新密码</param>
|
|||
/// <returns>操作结果</returns>
|
|||
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}]的密码"); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取用户信息
|
|||
/// </summary>
|
|||
/// <param name="id">用户ID</param>
|
|||
/// <returns>用户实体</returns>
|
|||
public async Task<User> GetAsync(Guid id) |
|||
{ |
|||
return await _userRepository.GetAsync(id); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 根据Identity用户ID获取用户
|
|||
/// </summary>
|
|||
/// <param name="identityUserId">Identity用户ID</param>
|
|||
/// <returns>用户实体,如果不存在则返回null</returns>
|
|||
public async Task<User> FindByIdentityUserIdAsync(Guid identityUserId) |
|||
{ |
|||
return await _userRepository.FindAsync(u => u.IdentityUserId == identityUserId); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 根据用户昵称查找用户
|
|||
/// </summary>
|
|||
/// <param name="nickName">用户昵称</param>
|
|||
/// <returns>用户实体,如果不存在则返回null</returns>
|
|||
public async Task<User> FindByNickNameAsync(string nickName) |
|||
{ |
|||
return await _userRepository.FindAsync(u => u.NickName == nickName); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 禁用或启用用户
|
|||
/// </summary>
|
|||
/// <param name="id">用户ID</param>
|
|||
/// <param name="isActive">是否启用</param>
|
|||
/// <returns>操作任务</returns>
|
|||
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}]"); |
|||
} |
|||
} |
|||
} |
|||
@ -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<ProjectNameDbContext, User, Guid>, IUserRepository |
|||
{ |
|||
public UserRepository(IDbContextProvider<ProjectNameDbContext> dbContextProvider) : base(dbContextProvider) |
|||
{ |
|||
} |
|||
|
|||
public override async Task<IQueryable<User>> WithDetailsAsync() |
|||
{ |
|||
return (await GetDbSetAsync()).Include(x => x.IdentityUser); |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// 用户管理控制器
|
|||
/// </summary>
|
|||
[RemoteService] |
|||
[Route("api/app/user")] |
|||
public class UserController : ProjectNameControllerBase |
|||
{ |
|||
private readonly IUserAppService _userAppService; |
|||
|
|||
public UserController(IUserAppService userAppService) |
|||
{ |
|||
_userAppService = userAppService; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 创建用户
|
|||
/// </summary>
|
|||
[HttpPost] |
|||
public async Task<UserDto> CreateAsync(CreateUpdateUserDto input) |
|||
{ |
|||
return await _userAppService.CreateAsync(input); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 更新用户
|
|||
/// </summary>
|
|||
[HttpPut("{id}")] |
|||
public async Task<UserDto> UpdateAsync(Guid id, CreateUpdateUserDto input) |
|||
{ |
|||
return await _userAppService.UpdateAsync(id, input); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 删除用户
|
|||
/// </summary>
|
|||
[HttpDelete("{id}")] |
|||
public async Task DeleteAsync(Guid id) |
|||
{ |
|||
await _userAppService.DeleteAsync(id); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取用户
|
|||
/// </summary>
|
|||
[HttpGet("{id}")] |
|||
public async Task<UserDto> GetAsync(Guid id) |
|||
{ |
|||
return await _userAppService.GetAsync(id); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 获取用户列表
|
|||
/// </summary>
|
|||
[HttpGet] |
|||
public async Task<PagedResultDto<UserItemDto>> GetListAsync(UserPagedAndSortedResultRequestDto input) |
|||
{ |
|||
return await _userAppService.GetListAsync(input); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 修改用户密码
|
|||
/// </summary>
|
|||
[HttpPost("{id}/change-password")] |
|||
public async Task ChangePasswordAsync(Guid id, [FromBody] ChangePasswordRequest request) |
|||
{ |
|||
await _userAppService.ChangePasswordAsync(id, request.CurrentPassword, request.NewPassword); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 重置用户密码(管理员操作)
|
|||
/// </summary>
|
|||
[HttpPost("{id}/reset-password")] |
|||
public async Task ResetPasswordAsync(Guid id, [FromBody] ResetPasswordRequest request) |
|||
{ |
|||
await _userAppService.ResetPasswordAsync(id, request.NewPassword); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 启用或禁用用户
|
|||
/// </summary>
|
|||
[HttpPost("{id}/set-active")] |
|||
public async Task SetUserActiveStatusAsync(Guid id, [FromBody] SetUserActiveRequest request) |
|||
{ |
|||
await _userAppService.SetUserActiveStatusAsync(id, request.IsActive); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 修改密码请求
|
|||
/// </summary>
|
|||
public class ChangePasswordRequest |
|||
{ |
|||
/// <summary>
|
|||
/// 当前密码
|
|||
/// </summary>
|
|||
public string CurrentPassword { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// 新密码
|
|||
/// </summary>
|
|||
public string NewPassword { get; set; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 重置密码请求
|
|||
/// </summary>
|
|||
public class ResetPasswordRequest |
|||
{ |
|||
/// <summary>
|
|||
/// 新密码
|
|||
/// </summary>
|
|||
public string NewPassword { get; set; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 设置用户状态请求
|
|||
/// </summary>
|
|||
public class SetUserActiveRequest |
|||
{ |
|||
/// <summary>
|
|||
/// 是否启用
|
|||
/// </summary>
|
|||
public bool IsActive { get; set; } |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// 数据种子初始化测试
|
|||
/// </summary>
|
|||
[Collection("Database")] |
|||
public class ProjectNameDataSeederTests : ProjectNameApplicationTestBase |
|||
{ |
|||
private readonly IProjectNameDataSeeder _inspectionDataSeeder; |
|||
private readonly IRepository<User, Guid> _userRepository; |
|||
private readonly IIdentityRoleRepository _identityRoleRepository; |
|||
private readonly IIdentityUserRepository _identityUserRepository; |
|||
|
|||
public ProjectNameDataSeederTests() |
|||
{ |
|||
_inspectionDataSeeder = GetRequiredService<IProjectNameDataSeeder>(); |
|||
_userRepository = GetRequiredService<IRepository<User, Guid>>(); |
|||
_identityRoleRepository = GetRequiredService<IIdentityRoleRepository>(); |
|||
_identityUserRepository = GetRequiredService<IIdentityUserRepository>(); |
|||
} |
|||
|
|||
[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<TResult> WithUnitOfWorkAsync<TResult>(Func<Task<TResult>> func) |
|||
{ |
|||
return WithUnitOfWorkAsync(new AbpUnitOfWorkOptions(), func); |
|||
} |
|||
|
|||
// 可选:添加重载方法以支持更多场景
|
|||
protected async override Task<TResult> WithUnitOfWorkAsync<TResult>(AbpUnitOfWorkOptions options, Func<Task<TResult>> func) |
|||
{ |
|||
using (var scope = ServiceProvider.CreateScope()) |
|||
{ |
|||
var uowManager = scope.ServiceProvider.GetRequiredService<IUnitOfWorkManager>(); |
|||
|
|||
using (var uow = uowManager.Begin(options)) |
|||
{ |
|||
var result = await func(); |
|||
await uow.CompleteAsync(); |
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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<IProjectNameDataSeeder, ProjectNameDataSeeder>(); |
|||
context.Services.AddLogging(builder => builder.AddProvider(NullLoggerProvider.Instance)); |
|||
context.Services.AddTransient<IHostEnvironment, TestHostEnvironment>(); |
|||
context.Services.AddTransient<IFileProvider, TestFileProvider>(); |
|||
|
|||
// 增加配置文件定义,在新建租户时需要
|
|||
Configure<IdentityOptions>(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<IdentityUser>))); |
|||
}); |
|||
Configure<AbpClaimsPrincipalFactoryOptions>(options => |
|||
{ |
|||
options.IsDynamicClaimsEnabled = true; |
|||
}); |
|||
Configure<IdentitySessionCleanupOptions>(options => |
|||
{ |
|||
options.IsCleanupEnabled = true; |
|||
}); |
|||
// 配置Hangfire
|
|||
context.Services.AddHangfire(config => |
|||
{ |
|||
config.UseMemoryStorage(); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
@ -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<string, IFileInfo> _files; |
|||
|
|||
public TestFileProvider() |
|||
{ |
|||
_files = new Dictionary<string, IFileInfo>(); |
|||
} |
|||
|
|||
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)); |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// UserAppService 的单元测试
|
|||
/// </summary>
|
|||
[Collection("Database")] |
|||
public class UserAppServiceTests : ProjectNameApplicationTestBase |
|||
{ |
|||
private readonly IUserAppService _userAppService; |
|||
private readonly IUserManager _userManager; |
|||
|
|||
public UserAppServiceTests() |
|||
{ |
|||
_userAppService = GetRequiredService<IUserAppService>(); |
|||
_userManager = GetRequiredService<IUserManager>(); |
|||
} |
|||
|
|||
[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<AbpValidationException>(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<EntityNotFoundException>(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<EntityNotFoundException>(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<UserDto> CreateTestUserAsync(string nickName, string password) |
|||
{ |
|||
return await WithUnitOfWorkAsync(async () => |
|||
{ |
|||
var input = new CreateUpdateUserDto |
|||
{ |
|||
NickName = nickName, |
|||
Password = password, |
|||
IsActive = true |
|||
}; |
|||
|
|||
return await _userAppService.CreateAsync(input); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -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<AbpDbConnectionOptions>(options => |
|||
{ |
|||
options.ConnectionStrings.Default = connectionString; |
|||
}); |
|||
|
|||
Configure<AbpDbContextOptions>(options => |
|||
Configure<AbpClockOptions>(options => { options.Kind = DateTimeKind.Utc; }); |
|||
context.Services.AddAbpDbContext<SingleMigrationsDbContext>(options => |
|||
{ |
|||
options.Configure(abpDbContextConfigurationContext => |
|||
{ |
|||
abpDbContextConfigurationContext.DbContextOptions.EnableDetailedErrors(); |
|||
abpDbContextConfigurationContext.DbContextOptions.EnableSensitiveDataLogging(); |
|||
options.AddDefaultRepositories(true); |
|||
}); |
|||
|
|||
abpDbContextConfigurationContext.DbContextOptions.UseInMemoryDatabase(databaseName); |
|||
}); |
|||
// 配置所有DbContext
|
|||
Configure<AbpDbContextOptions>(options => |
|||
{ |
|||
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); |
|||
options.UseNpgsql(); |
|||
}); |
|||
|
|||
Configure<AbpUnitOfWorkDefaultOptions>(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<SingleMigrationsDbContext>(); |
|||
// 重置数据库
|
|||
dbContext.Database.EnsureDeleted(); |
|||
// // 创建数据库
|
|||
dbContext.Database.EnsureCreated(); |
|||
dbContext.Database.GenerateCreateScript(); |
|||
// dbContext.Database.Migrate();
|
|||
|
|||
// 初始化种子数据
|
|||
var dataSeeder = context.ServiceProvider.GetRequiredService<IDataSeeder>(); |
|||
AsyncHelper.RunSync(() => dataSeeder.SeedAsync()); |
|||
} |
|||
|
|||
public override void OnApplicationShutdown(ApplicationShutdownContext context) |
|||
{ |
|||
} |
|||
} |
|||
Loading…
Reference in new issue