Browse Source
1. 创建ICurrentUserService和CurrentUserService来获取当前用户信息 2. 修改ProtocolVersion和CellularDevice实体的Create/Update方法,添加createdBy/updatedBy参数 3. 更新所有相关CommandHandler,使用当前用户服务设置审计字段 4. 修复实体配置中的DateTime类型为timestamp with time zone 5. 确保所有CommandHandler调用UnitOfWork.SaveChangesAsync() 6. 修复C#参数顺序错误,将必需参数放在可选参数之前 7. 移除默认值'system',当无法获取用户ID时抛出异常feature/x1-owen-debug
24 changed files with 555 additions and 26 deletions
@ -0,0 +1,151 @@ |
|||||
|
# DDD 设计原则修复总结 |
||||
|
|
||||
|
## 问题描述 |
||||
|
|
||||
|
在原始的代码实现中,存在一个重要的DDD设计原则违反问题: |
||||
|
|
||||
|
### 问题分析 |
||||
|
1. **仓储层职责不清**:`ProtocolVersionRepository`、`CellularDeviceRepository` 等仓储的 `AddAsync` 方法只是将实体添加到 EF Core 的 ChangeTracker 中,但**没有调用 `SaveChangesAsync`** |
||||
|
2. **数据持久化缺失**:在 CommandHandler 中,没有显式调用 `UnitOfWork.SaveChangesAsync()`,导致数据可能没有被实际保存到数据库中 |
||||
|
3. **事务管理不当**:违反了DDD中关于事务管理的最佳实践 |
||||
|
|
||||
|
### 违反的DDD原则 |
||||
|
- **单一职责原则**:仓储层应该只负责数据访问,不应该自动保存 |
||||
|
- **事务边界**:事务管理和保存操作应该在应用层(CommandHandler)中通过 UnitOfWork 来管理 |
||||
|
- **业务操作的原子性**:确保业务操作的原子性和一致性 |
||||
|
|
||||
|
## 修复方案 |
||||
|
|
||||
|
### 1. 修复的CommandHandler |
||||
|
|
||||
|
#### ProtocolVersion相关 |
||||
|
- ✅ `CreateProtocolVersionCommandHandler` |
||||
|
- ✅ `UpdateProtocolVersionCommandHandler` |
||||
|
- ✅ `DeleteProtocolVersionCommandHandler` |
||||
|
|
||||
|
#### Device相关 |
||||
|
- ✅ `CreateDeviceCommandHandler` |
||||
|
- ✅ `UpdateDeviceCommandHandler` |
||||
|
- ✅ `DeleteDeviceCommandHandler` |
||||
|
|
||||
|
#### Permission相关 |
||||
|
- ✅ `CreatePermissionCommandHandler` |
||||
|
|
||||
|
### 2. 修复内容 |
||||
|
|
||||
|
每个CommandHandler都进行了以下修改: |
||||
|
|
||||
|
```csharp |
||||
|
// 1. 添加IUnitOfWork依赖 |
||||
|
private readonly IUnitOfWork _unitOfWork; |
||||
|
|
||||
|
// 2. 在构造函数中注入IUnitOfWork |
||||
|
public CommandHandler( |
||||
|
IRepository repository, |
||||
|
ILogger<CommandHandler> logger, |
||||
|
IUnitOfWork unitOfWork) // 新增 |
||||
|
{ |
||||
|
_repository = repository; |
||||
|
_logger = logger; |
||||
|
_unitOfWork = unitOfWork; // 新增 |
||||
|
} |
||||
|
|
||||
|
// 3. 在业务操作后调用SaveChangesAsync |
||||
|
await _repository.AddAsync(entity, cancellationToken); |
||||
|
await _unitOfWork.SaveChangesAsync(cancellationToken); // 新增 |
||||
|
``` |
||||
|
|
||||
|
## DDD设计原则说明 |
||||
|
|
||||
|
### 1. 仓储层职责 |
||||
|
- **仓储层**:只负责数据访问和查询,不负责事务管理 |
||||
|
- **应用层**:负责业务逻辑和事务管理 |
||||
|
|
||||
|
### 2. 工作单元模式 |
||||
|
- **UnitOfWork**:统一管理数据库事务和持久化操作 |
||||
|
- **事务边界**:在CommandHandler中明确事务边界 |
||||
|
- **原子性**:确保业务操作的原子性 |
||||
|
|
||||
|
### 3. 分层架构 |
||||
|
``` |
||||
|
应用层 (Application) |
||||
|
├── CommandHandler (业务逻辑 + 事务管理) |
||||
|
└── UnitOfWork (事务管理) |
||||
|
|
||||
|
领域层 (Domain) |
||||
|
├── Repository Interface (数据访问契约) |
||||
|
└── Entity (业务实体) |
||||
|
|
||||
|
基础设施层 (Infrastructure) |
||||
|
├── Repository Implementation (数据访问实现) |
||||
|
└── UnitOfWork Implementation (事务实现) |
||||
|
``` |
||||
|
|
||||
|
## 修复后的优势 |
||||
|
|
||||
|
### 1. 数据一致性 |
||||
|
- 确保所有数据操作都被正确持久化 |
||||
|
- 避免数据丢失和不一致问题 |
||||
|
|
||||
|
### 2. 事务管理 |
||||
|
- 明确的事务边界 |
||||
|
- 支持复杂业务操作的事务管理 |
||||
|
- 异常时自动回滚 |
||||
|
|
||||
|
### 3. 代码可维护性 |
||||
|
- 清晰的职责分离 |
||||
|
- 符合DDD设计原则 |
||||
|
- 便于测试和调试 |
||||
|
|
||||
|
### 4. 性能优化 |
||||
|
- 批量保存操作 |
||||
|
- 减少不必要的数据库往返 |
||||
|
- 更好的内存管理 |
||||
|
|
||||
|
## 最佳实践建议 |
||||
|
|
||||
|
### 1. 事务管理 |
||||
|
```csharp |
||||
|
// 推荐:在CommandHandler中使用UnitOfWork |
||||
|
await _unitOfWork.SaveChangesAsync(cancellationToken); |
||||
|
|
||||
|
// 复杂事务场景 |
||||
|
await _unitOfWork.ExecuteTransactionAsync(async () => |
||||
|
{ |
||||
|
// 多个业务操作 |
||||
|
await _repository1.AddAsync(entity1); |
||||
|
await _repository2.UpdateAsync(entity2); |
||||
|
}, cancellationToken); |
||||
|
``` |
||||
|
|
||||
|
### 2. 异常处理 |
||||
|
```csharp |
||||
|
try |
||||
|
{ |
||||
|
await _repository.AddAsync(entity, cancellationToken); |
||||
|
await _unitOfWork.SaveChangesAsync(cancellationToken); |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
// 记录日志,返回错误响应 |
||||
|
_logger.LogError(ex, "操作失败"); |
||||
|
return OperationResult<T>.CreateFailure("操作失败"); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### 3. 依赖注入 |
||||
|
```csharp |
||||
|
// 确保在DI容器中正确注册 |
||||
|
services.AddScoped<IUnitOfWork, UnitOfWork>(); |
||||
|
services.AddScoped<IRepository, Repository>(); |
||||
|
``` |
||||
|
|
||||
|
## 总结 |
||||
|
|
||||
|
这次修复确保了: |
||||
|
1. **数据持久化**:所有数据操作都被正确保存到数据库 |
||||
|
2. **DDD合规性**:严格遵循DDD设计原则和最佳实践 |
||||
|
3. **事务安全**:正确的事务管理和异常处理 |
||||
|
4. **代码质量**:清晰的职责分离和可维护的代码结构 |
||||
|
|
||||
|
这种修复方式为整个系统提供了坚实的数据访问基础,确保了业务操作的可靠性和一致性。 |
@ -0,0 +1,163 @@ |
|||||
|
# PostgreSQL DateTime 时间戳问题修复总结 |
||||
|
|
||||
|
## 问题描述 |
||||
|
|
||||
|
在PostgreSQL数据库中,当使用`timestamp with time zone`类型时,只支持UTC时间的DateTime值。错误信息: |
||||
|
|
||||
|
``` |
||||
|
Cannot write DateTime with Kind=Unspecified to PostgreSQL type 'timestamp with time zone', only UTC is supported. |
||||
|
``` |
||||
|
|
||||
|
## 问题分析 |
||||
|
|
||||
|
### 1. 根本原因 |
||||
|
- PostgreSQL的`timestamp with time zone`类型要求DateTime的Kind必须是UTC |
||||
|
- 代码中虽然使用了`DateTime.UtcNow`,但实体配置中没有明确指定数据库列类型 |
||||
|
- 响应类中使用了`DateTime.UtcNow`而不是从实体中获取的时间 |
||||
|
|
||||
|
### 2. 问题位置 |
||||
|
1. **实体配置类**:缺少`.HasColumnType("timestamp with time zone")`配置 |
||||
|
2. **响应构建**:使用了`DateTime.UtcNow`而不是实体的时间字段 |
||||
|
|
||||
|
## 修复方案 |
||||
|
|
||||
|
### 1. 修复的实体配置类 |
||||
|
|
||||
|
#### Device相关 |
||||
|
- ✅ `ProtocolVersionConfiguration` |
||||
|
- ✅ `CellularDeviceConfiguration` |
||||
|
|
||||
|
#### Identity相关 |
||||
|
- ✅ `AppRoleConfiguration` |
||||
|
- ✅ `AppUserConfiguration` |
||||
|
|
||||
|
#### 其他 |
||||
|
- ✅ `BaseEntityConfiguration` |
||||
|
- ✅ `PermissionConfiguration` |
||||
|
- ✅ `LoginLogConfiguration` |
||||
|
|
||||
|
### 2. 修复的CommandHandler响应 |
||||
|
|
||||
|
#### ProtocolVersion相关 |
||||
|
- ✅ `UpdateProtocolVersionCommandHandler`:使用`existingProtocolVersion.UpdatedAt` |
||||
|
|
||||
|
#### Device相关 |
||||
|
- ✅ `UpdateDeviceCommandHandler`:使用`existingDevice.UpdatedAt` |
||||
|
|
||||
|
### 3. 修复内容 |
||||
|
|
||||
|
#### 实体配置类修复 |
||||
|
```csharp |
||||
|
// 修复前 |
||||
|
builder.Property(e => e.CreatedAt) |
||||
|
.IsRequired() |
||||
|
.HasComment("创建时间"); |
||||
|
|
||||
|
// 修复后 |
||||
|
builder.Property(e => e.CreatedAt) |
||||
|
.IsRequired() |
||||
|
.HasColumnType("timestamp with time zone") |
||||
|
.HasComment("创建时间"); |
||||
|
``` |
||||
|
|
||||
|
#### 响应构建修复 |
||||
|
```csharp |
||||
|
// 修复前 |
||||
|
UpdatedAt = DateTime.UtcNow |
||||
|
|
||||
|
// 修复后 |
||||
|
UpdatedAt = existingEntity.UpdatedAt |
||||
|
``` |
||||
|
|
||||
|
## 修复的字段类型 |
||||
|
|
||||
|
### 1. 时间戳字段 |
||||
|
- `CreatedAt` / `CreatedTime`:创建时间 |
||||
|
- `UpdatedAt` / `ModifiedTime`:更新时间 |
||||
|
- `LastLoginTime`:最后登录时间 |
||||
|
- `LoginTime`:登录时间 |
||||
|
- `ReleaseDate`:发布日期 |
||||
|
|
||||
|
### 2. 数据库类型 |
||||
|
- PostgreSQL: `timestamp with time zone` |
||||
|
- 确保所有DateTime字段都使用UTC时间 |
||||
|
|
||||
|
## 最佳实践 |
||||
|
|
||||
|
### 1. 时间处理原则 |
||||
|
```csharp |
||||
|
// ✅ 正确:使用UTC时间 |
||||
|
DateTime.UtcNow |
||||
|
|
||||
|
// ❌ 错误:使用本地时间 |
||||
|
DateTime.Now |
||||
|
|
||||
|
// ❌ 错误:使用未指定Kind的时间 |
||||
|
new DateTime(2024, 1, 1) |
||||
|
``` |
||||
|
|
||||
|
### 2. 实体配置 |
||||
|
```csharp |
||||
|
// ✅ 正确:明确指定PostgreSQL时间戳类型 |
||||
|
builder.Property(e => e.CreatedAt) |
||||
|
.HasColumnType("timestamp with time zone"); |
||||
|
|
||||
|
// ❌ 错误:依赖默认类型推断 |
||||
|
builder.Property(e => e.CreatedAt); |
||||
|
``` |
||||
|
|
||||
|
### 3. 响应构建 |
||||
|
```csharp |
||||
|
// ✅ 正确:使用实体的时间字段 |
||||
|
response.UpdatedAt = entity.UpdatedAt; |
||||
|
|
||||
|
// ❌ 错误:重新生成时间 |
||||
|
response.UpdatedAt = DateTime.UtcNow; |
||||
|
``` |
||||
|
|
||||
|
## 数据库迁移 |
||||
|
|
||||
|
### 1. 现有数据库 |
||||
|
如果数据库中已有数据,需要确保: |
||||
|
- 所有DateTime字段都存储为UTC时间 |
||||
|
- 列类型为`timestamp with time zone` |
||||
|
|
||||
|
### 2. 新数据库 |
||||
|
- 实体配置会自动生成正确的列类型 |
||||
|
- 所有时间字段都会使用UTC时间 |
||||
|
|
||||
|
## 验证方法 |
||||
|
|
||||
|
### 1. 代码验证 |
||||
|
```csharp |
||||
|
// 检查DateTime.Kind |
||||
|
if (entity.CreatedAt.Kind != DateTimeKind.Utc) |
||||
|
{ |
||||
|
throw new InvalidOperationException("DateTime must be UTC"); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### 2. 数据库验证 |
||||
|
```sql |
||||
|
-- 检查列类型 |
||||
|
SELECT column_name, data_type |
||||
|
FROM information_schema.columns |
||||
|
WHERE table_name = 'your_table' |
||||
|
AND column_name LIKE '%time%'; |
||||
|
|
||||
|
-- 检查时间值 |
||||
|
SELECT created_at, updated_at |
||||
|
FROM your_table |
||||
|
LIMIT 5; |
||||
|
``` |
||||
|
|
||||
|
## 总结 |
||||
|
|
||||
|
这次修复确保了: |
||||
|
|
||||
|
1. **数据类型一致性**:所有DateTime字段都使用PostgreSQL的`timestamp with time zone`类型 |
||||
|
2. **时间标准统一**:所有时间都使用UTC标准 |
||||
|
3. **配置明确性**:实体配置中明确指定了数据库列类型 |
||||
|
4. **响应准确性**:响应中使用实体的实际时间而不是重新生成的时间 |
||||
|
|
||||
|
这种修复方式为整个系统提供了统一的时间处理标准,确保了与PostgreSQL数据库的完全兼容性。 |
@ -0,0 +1,33 @@ |
|||||
|
namespace CellularManagement.Domain.Services; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 当前用户服务接口
|
||||
|
/// 用于获取当前请求的用户信息
|
||||
|
/// </summary>
|
||||
|
public interface ICurrentUserService |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 获取当前用户ID
|
||||
|
/// </summary>
|
||||
|
string? GetCurrentUserId(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 获取当前用户名
|
||||
|
/// </summary>
|
||||
|
string? GetCurrentUserName(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 获取当前用户邮箱
|
||||
|
/// </summary>
|
||||
|
string? GetCurrentUserEmail(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 检查当前用户是否已认证
|
||||
|
/// </summary>
|
||||
|
bool IsAuthenticated(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 获取当前用户的所有声明
|
||||
|
/// </summary>
|
||||
|
IEnumerable<System.Security.Claims.Claim> GetCurrentUserClaims(); |
||||
|
} |
@ -0,0 +1,63 @@ |
|||||
|
using Microsoft.AspNetCore.Http; |
||||
|
using System.Security.Claims; |
||||
|
using CellularManagement.Domain.Services; |
||||
|
|
||||
|
namespace CellularManagement.Infrastructure.Services; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 当前用户服务实现类
|
||||
|
/// 从HttpContext中获取当前用户信息
|
||||
|
/// </summary>
|
||||
|
public class CurrentUserService : ICurrentUserService |
||||
|
{ |
||||
|
private readonly IHttpContextAccessor _httpContextAccessor; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 初始化当前用户服务
|
||||
|
/// </summary>
|
||||
|
/// <param name="httpContextAccessor">HTTP上下文访问器</param>
|
||||
|
public CurrentUserService(IHttpContextAccessor httpContextAccessor) |
||||
|
{ |
||||
|
_httpContextAccessor = httpContextAccessor; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 获取当前用户ID
|
||||
|
/// </summary>
|
||||
|
public string? GetCurrentUserId() |
||||
|
{ |
||||
|
return _httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 获取当前用户名
|
||||
|
/// </summary>
|
||||
|
public string? GetCurrentUserName() |
||||
|
{ |
||||
|
return _httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.Name)?.Value; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 获取当前用户邮箱
|
||||
|
/// </summary>
|
||||
|
public string? GetCurrentUserEmail() |
||||
|
{ |
||||
|
return _httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.Email)?.Value; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 检查当前用户是否已认证
|
||||
|
/// </summary>
|
||||
|
public bool IsAuthenticated() |
||||
|
{ |
||||
|
return _httpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated == true; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 获取当前用户的所有声明
|
||||
|
/// </summary>
|
||||
|
public IEnumerable<Claim> GetCurrentUserClaims() |
||||
|
{ |
||||
|
return _httpContextAccessor.HttpContext?.User?.Claims ?? Enumerable.Empty<Claim>(); |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue