本章节详细介绍 Fastdotnet 后端开发的核心概念和最佳实践,包括实体设计、仓储模式、服务层、控制器等。
📖 本章内容
- 实体设计 - BaseEntity 和 AuditableEntity
- 仓储模式 - IRepository 接口详解
- 服务层 - IBaseService 业务逻辑
- 工作单元 - IUnitOfWork 事务管理
- 控制器 - GenericDtoControllerBase 快速开发
- DTO 设计 - 数据传输对象规范
- 依赖注入 - 服务注册与使用
- 核心服务 - 缓存、日志、用户信息 ⭐
- 最佳实践 - 代码规范和性能优化
🏗️ 实体设计
Fastdotnet 提供了两个实体基类,所有业务实体都应该继承它们。
1. BaseEntity - 基础实体
文件位置: Fastdotnet.Core.Dtos.Base.BaseEntity
public class BaseEntity : IBaseEntity, ISoftDelete
{
/// <summary>
/// 主键ID (string 类型,支持 GUID/雪花算法)
/// </summary>
[SugarColumn(ColumnName = "id", IsPrimaryKey = true)]
public string Id { get; set; }
/// <summary>
/// 创建时间 (自动填充)
/// </summary>
[SplitField]
[SugarColumn(ColumnName = "created_at")]
public DateTime CreatedAt { get; set; } = DateTime.Now;
/// <summary>
/// 更新时间 (自动填充)
/// </summary>
[SugarColumn(ColumnName = "updated_at", IsNullable = true)]
public DateTime? UpdatedAt { get; set; }
/// <summary>
/// 是否删除 (软删除标记)
/// </summary>
[SugarColumn(ColumnName = "is_deleted")]
public bool IsDeleted { get; set; } = false;
/// <summary>
/// 删除时间 (软删除时自动填充)
/// </summary>
[SugarColumn(ColumnName = "deleted_at", IsNullable = true)]
public DateTime? DeletedAt { get; set; }
}特性说明:
| 字段 | 类型 | 说明 | 自动填充 |
|---|---|---|---|
Id | string | 主键,支持 GUID/雪花ID | ❌ 需手动设置 |
CreatedAt | DateTime | 创建时间 | ✅ 是 |
UpdatedAt | DateTime? | 更新时间 | ✅ 是 |
IsDeleted | bool | 软删除标记 | ✅ 是 |
DeletedAt | DateTime? | 删除时间 | ✅ 是 |
使用示例:
using Fastdotnet.Core.Dtos.Base;
using SqlSugar;
namespace MyPlugin.Entities
{
/// <summary>
/// 任务实体
/// </summary>
[SugarTable("my_tasks", "任务表")]
public class TaskItem : BaseEntity
{
/// <summary>
/// 任务标题
/// </summary>
[SugarColumn(Length = 200, IsNullable = false)]
public string Title { get; set; }
/// <summary>
/// 任务描述
/// </summary>
[SugarColumn(Length = 1000, IsNullable = true)]
public string? Description { get; set; }
/// <summary>
/// 任务状态:0-待办,1-进行中,2-已完成
/// </summary>
public int Status { get; set; } = 0;
}
}2. AuditableEntity - 审计实体
文件位置: Fastdotnet.Core.Dtos.Base.AuditableEntity
在 BaseEntity 基础上增加了操作人信息:
public class AuditableEntity : BaseEntity, IAuditableEntity
{
/// <summary>
/// 创建人ID (自动填充当前用户ID)
/// </summary>
[SugarColumn(ColumnName = "created_by", IsNullable = true)]
public string? CreatedBy { get; set; }
/// <summary>
/// 更新人ID (自动填充当前用户ID)
/// </summary>
[SugarColumn(ColumnName = "updated_by", IsNullable = true)]
public string? UpdatedBy { get; set; }
/// <summary>
/// 删除人ID (自动填充当前用户ID)
/// </summary>
[SugarColumn(ColumnName = "deleted_by", IsNullable = true)]
public string? DeletedBy { get; set; }
}何时使用 AuditableEntity?
- ✅ 需要追踪谁创建了数据
- ✅ 需要追踪谁修改了数据
- ✅ 需要追踪谁删除了数据
- ✅ 业务数据(订单、文章、评论等)
何时使用 BaseEntity?
- ✅ 不需要追踪操作人
- ✅ 配置数据、字典数据
- ✅ 系统内部使用的数据
使用示例:
/// <summary>
/// 订单实体(需要审计)
/// </summary>
[SugarTable("my_orders", "订单表")]
public class Order : AuditableEntity
{
[SugarColumn(Length = 50)]
public string OrderNo { get; set; }
public decimal Amount { get; set; }
public int Status { get; set; }
}
// 创建订单时,CreatedBy 会自动填充为当前登录用户ID
var order = new Order
{
OrderNo = "ORD20240101001",
Amount = 99.99m,
Status = 0
};
await _repository.InsertAsync(order);
// order.CreatedBy 自动设置为当前用户ID📦 仓储模式
Fastdotnet 提供了强大的通用仓储接口 IRepository<T>;,封装了常用的 CRUD 操作。
1. IRepository 接口概览
文件位置: Fastdotnet.Core.IService.IRepository
public interface IRepository<T, TKey>; where T : BaseEntity, new()
{
// 查询操作
Task<T>; GetByIdAsync(TKey id);
Task<List<T>;>; GetAllAsync();
Task<List<T>;>; GetListAsync(Expression<Func<T, bool>;>; where);
Task<T>; GetFirstAsync(Expression<Func<T, bool>;>; where);
Task<PageResult<T>;>; GetPageAsync(...);
Task<bool>; ExistsAsync(Expression<Func<T, bool>;>; where);
// 插入操作
Task<T>; InsertAsync(T entity);
Task<List<T>;>; InsertRangeAsync(List<T>; entities);
// 更新操作
Task UpdateAsync(T entity);
Task UpdateRangeAsync(List<T>; entities);
// 删除操作
Task DeleteAsync(TKey id);
Task DeleteAsync(T entity);
Task DeleteRangeAsync(List<T>; entities);
// 高级查询
Task<List<TResult>;>; GetListAsync<TResult>;(
Expression<Func<T, bool>;>; where,
Expression<Func<T, TResult>;>; select);
}2. 查询操作详解
① 根据 ID 查询
// 单个查询
var task = await _repository.GetByIdAsync("task_id_123");
// 如果不存在返回 null
if (task == null)
{
throw new BusinessException("任务不存在");
}② 条件查询
// 查询所有未完成的任务
var tasks = await _repository.GetListAsync(t => t.Status == 0);
// 复杂条件
var tasks = await _repository.GetListAsync(t =>
t.Status == 0 &&
t.CreatedAt > DateTime.Now.AddDays(-7));③ 投影查询(只查询需要的字段)
// 只查询 ID 和标题,提高性能
var taskSummaries = await _repository.GetListAsync(
t => t.Status == 0,
t => new { t.Id, t.Title }
);④ 分页查询
// 基本分页
var pageResult = await _repository.GetPageAsync(
pageIndex: 1,
pageSize: 10,
orderByExpression: t => t.CreatedAt,
orderByType: OrderByType.Desc
);
// 带条件的分页
var pageResult = await _repository.GetPageAsync(
whereExpression: t => t.Status == 0,
pageIndex: 1,
pageSize: 10,
orderByExpression: t => t.CreatedAt,
orderByType: OrderByType.Desc
);
// 返回结果
Console.WriteLine($"总记录数: {pageResult.Total}");
Console.WriteLine($"总页数: {pageResult.Pages}");
Console.WriteLine($"当前页数据: {pageResult.Items.Count}");⑤ 判断是否存在
// 检查任务标题是否已存在
bool exists = await _repository.ExistsAsync(t => t.Title == "测试任务");
if (exists)
{
throw new BusinessException("任务标题已存在");
}3. 插入操作
// 单个插入
var task = new TaskItem
{
Id = Guid.NewGuid().ToString(),
Title = "新任务",
Status = 0
};
await _repository.InsertAsync(task);
// 批量插入
var tasks = new List<TaskItem>
{
new TaskItem { Id = "1", Title = "任务1" },
new TaskItem { Id = "2", Title = "任务2" },
new TaskItem { Id = "3", Title = "任务3" }
};
await _repository.InsertRangeAsync(tasks);4. 更新操作
// 单个更新
var task = await _repository.GetByIdAsync("task_id");
task.Status = 1;
task.Title = "更新后的标题";
await _repository.UpdateAsync(task);
// 批量更新
var tasks = await _repository.GetListAsync(t => t.Status == 0);
foreach (var task in tasks)
{
task.Status = 1;
}
await _repository.UpdateRangeAsync(tasks);5. 删除操作(软删除)
// 单个删除(软删除)
await _repository.DeleteAsync("task_id");
// 实际上是将 IsDeleted 设置为 true,DeletedAt 设置为当前时间
// 批量删除
var tasks = await _repository.GetListAsync(t => t.Status == 2);
await _repository.DeleteRangeAsync(tasks);**注意:**物理删除需要直接调用 SqlSugar 的 Delete 方法,请谨慎使用。
🔧 服务层
服务层负责业务逻辑处理,通过依赖注入使用仓储。
1. IBaseService 接口
文件位置: Fastdotnet.Core.IService.IBaseService
public interface IBaseService<T>; where T : BaseEntity, new()
{
IRepository<T>; Repository { get; }
// 继承 IRepository 的所有方法
// 可以添加自定义的业务方法
}2. 创建服务类
using Fastdotnet.Core.IService;
using MyPlugin.Entities;
namespace MyPlugin.Services
{
public interface ITaskService : IBaseService<TaskItem>
{
// 自定义业务方法
Task CompleteTaskAsync(string taskId);
Task<List<TaskItem>;>; GetOverdueTasksAsync();
}
public class TaskService : BaseService<TaskItem>;, ITaskService
{
private readonly ICurrentUser _currentUser;
public TaskService(
IRepository<TaskItem>; repository,
ICurrentUser currentUser)
: base(repository)
{
_currentUser = currentUser;
}
/// <summary>
/// 完成任务
/// </summary>
public async Task CompleteTaskAsync(string taskId)
{
var task = await Repository.GetByIdAsync(taskId);
if (task == null)
{
throw new BusinessException("任务不存在");
}
// 权限检查:只能完成自己的任务
if (task.CreatedBy != _currentUser.Id)
{
throw new BusinessException("无权操作此任务");
}
task.Status = 2; // 已完成
await Repository.UpdateAsync(task);
}
/// <summary>
/// 获取逾期任务
/// </summary>
public async Task<List<TaskItem>;>; GetOverdueTasksAsync()
{
return await Repository.GetListAsync(t =>
t.Status != 2 && // 未完成
t.DueDate < DateTime.Now // 已逾期
);
}
}
}3. 注册服务
在插件的 ConfigureServices 方法中注册:
public override void ConfigureServices(ContainerBuilder builder)
{
// 注册服务
builder.RegisterType<TaskService>()
.As<ITaskService>()
.InstancePerLifetimeScope();
}💾 工作单元(事务管理)
1. IUnitOfWork 接口
文件位置: Fastdotnet.Core.IService.IUnitOfWork
public interface IUnitOfWork : IDisposable
{
/// <summary>
/// 开始事务
/// </summary>
void BeginTransaction();
/// <summary>
/// 提交事务
/// </summary>
Task CommitTransactionAsync();
/// <summary>
/// 回滚事务
/// </summary>
Task RollbackTransactionAsync();
}2. 使用事务
public class OrderService : BaseService<Order>;, IOrderService
{
private readonly IUnitOfWork _unitOfWork;
private readonly IRepository<OrderItem>; _orderItemRepository;
public OrderService(
IRepository<Order>; repository,
IRepository<OrderItem>; orderItemRepository,
IUnitOfWork unitOfWork)
: base(repository)
{
_orderItemRepository = orderItemRepository;
_unitOfWork = unitOfWork;
}
/// <summary>
/// 创建订单(包含订单项)
/// </summary>
public async Task<string> CreateOrderAsync(CreateOrderDto dto)
{
_unitOfWork.BeginTransaction();
try
{
// 1. 创建订单
var order = new Order
{
Id = Guid.NewGuid().ToString(),
OrderNo = GenerateOrderNo(),
Amount = dto.Items.Sum(i => i.Price * i.Quantity),
Status = 0
};
await Repository.InsertAsync(order);
// 2. 创建订单项
var orderItems = dto.Items.Select(item => new OrderItem
{
Id = Guid.NewGuid().ToString(),
OrderId = order.Id,
ProductId = item.ProductId,
Quantity = item.Quantity,
Price = item.Price
}).ToList();
await _orderItemRepository.InsertRangeAsync(orderItems);
// 3. 提交事务
await _unitOfWork.CommitTransactionAsync();
return order.Id;
}
catch (Exception ex)
{
// 4. 回滚事务
await _unitOfWork.RollbackTransactionAsync();
throw new BusinessException($"创建订单失败: {ex.Message}");
}
}
}🎮 控制器
Fastdotnet 提供了强大的基类控制器,可以快速实现 CRUD API。
1. GenericDtoControllerBase
文件位置: Fastdotnet.Core.Controllers.GenericDtoControllerBase
这是最常用的控制器基类,提供了完整的 CRUD API。
① 最简单的用法
using Fastdotnet.Core.Controllers;
using Fastdotnet.Core.IService;
using MyPlugin.Entities;
namespace MyPlugin.Controllers
{
[Route("api/my-plugin/[controller]")]
public class TaskController : GenericDtoControllerBase<TaskItem>
{
public TaskController(IBaseService<TaskItem>; service)
: base(service)
{
}
// 自动继承以下 API:
// GET /api/my-plugin/task - 分页列表
// GET /api/my-plugin/task/{id} - 详情
// POST /api/my-plugin/task - 创建
// PUT /api/my-plugin/task/{id} - 更新
// DELETE /api/my-plugin/task/{id} - 删除(软删除)
// POST /api/my-plugin/task/batch-delete - 批量删除
// POST /api/my-plugin/task/search - 高级搜索
}
}② 添加自定义 API
[Route("api/my-plugin/[controller]")]
public class TaskController : GenericDtoControllerBase<TaskItem>
{
private readonly ITaskService _taskService;
public TaskController(IBaseService<TaskItem>; service, ITaskService taskService)
: base(service)
{
_taskService = taskService;
}
/// <summary>
/// 完成任务
/// </summary>
[HttpPost("{id}/complete")]
public async Task<IActionResult> CompleteTask(string id)
{
await _taskService.CompleteTaskAsync(id);
return Ok(new { success = true });
}
/// <summary>
/// 获取逾期任务
/// </summary>
[HttpGet("overdue")]
public async Task<IActionResult> GetOverdueTasks()
{
var tasks = await _taskService.GetOverdueTasksAsync();
return Ok(tasks);
}
}2. AppGenericDtoControllerBase
用于应用端的控制器,功能与 GenericDtoControllerBase 类似,但针对应用端做了优化。
[Route("api/my-plugin/[controller]")]
[ApiUsageScope(ApiUsageScopeEnum.AppOnly)] // 仅应用端可访问
public class AppTaskController : AppGenericDtoControllerBase<TaskItem>
{
public AppTaskController(IBaseService<TaskItem>; service)
: base(service)
{
}
}3. 高级搜索功能
GenericDtoControllerBase 内置了强大的动态搜索功能:
POST /api/my-plugin/task/search
Content-Type: application/json
{
"pageIndex": 1,
"pageSize": 10,
"conditions": [
{
"propertyName": "Status",
"operator": "Equal",
"value": 0
},
{
"propertyName": "Title",
"operator": "Contains",
"value": "测试"
},
{
"propertyName": "CreatedAt",
"operator": "Between",
"value": "2024-01-01",
"value2": "2024-12-31"
}
],
"logic": "And",
"orderBy": "CreatedAt",
"orderType": "Desc"
}支持的操作符:
| 操作符 | 说明 | 示例 |
|---|---|---|
Equal | 等于 | Status = 0 |
NotEqual | 不等于 | Status != 0 |
GreaterThan | 大于 | Price >; 100 |
LessThan | 小于 | Price < 100 |
Contains | 包含 | Title LIKE '%测试%' |
StartsWith | 开头 | Title LIKE '测试%' |
In | 在列表中 | Status IN (0, 1) |
Between | 范围 | CreatedAt BETWEEN ... AND ... |
📋 DTO 设计
1. DTO 命名规范
// 创建 DTO
public class CreateTaskDto
{
public string Title { get; set; }
public string? Description { get; set; }
public DateTime? DueDate { get; set; }
}
// 更新 DTO
public class UpdateTaskDto
{
public string Title { get; set; }
public int Status { get; set; }
}
// 响应 DTO
public class TaskDto
{
public string Id { get; set; }
public string Title { get; set; }
public int Status { get; set; }
public string CreatedBy { get; set; }
public DateTime CreatedAt { get; set; }
}2. 使用 AutoMapper 映射
// 在 Mappings 目录创建映射配置
public class TaskMappingProfile : Profile
{
public TaskMappingProfile()
{
CreateMap<TaskItem, TaskDto>();
CreateMap<CreateTaskDto, TaskItem>();
CreateMap<UpdateTaskDto, TaskItem>()
.ForAllMembers(opts => opts.Condition((src, dest, srcMember) => srcMember != null));
}
}💉 依赖注入
1. 服务注册
在插件入口类中注册服务:
public class MyPluginPlugin : PluginBase
{
public override void ConfigureServices(ContainerBuilder builder)
{
// 注册仓储(自动注册,无需手动)
// 注册服务
builder.RegisterType<TaskService>()
.As<ITaskService>()
.InstancePerLifetimeScope();
// 注册单例服务
builder.RegisterType<CacheService>()
.As<ICacheService>()
.SingleInstance();
}
}2. 构造函数注入
public class TaskController : ControllerBase
{
private readonly ITaskService _taskService;
private readonly ICurrentUser _currentUser;
private readonly ILogger<TaskController> _logger;
public TaskController(
ITaskService taskService,
ICurrentUser currentUser,
ILogger<TaskController> logger)
{
_taskService = taskService;
_currentUser = currentUser;
_logger = logger;
}
}🔧 核心服务
Fastdotnet 提供了一系列核心服务,简化常见开发任务。
1. 混合缓存服务 (IHybridCacheService)
文件位置: Fastdotnet.Core.IService.IHybridCacheService
特性说明
- 双层架构: 本地缓存 + 分布式缓存(Redis)
- 标签管理: 支持按标签批量清除缓存
- 过期策略: 灵活的过期时间配置
- 高性能: 自动优化缓存命中率
基本用法
public class ProductService : BaseService<Product>;, IProductService
{
private readonly IHybridCacheService _cache;
public ProductService(
IRepository<Product>; repository,
IHybridCacheService cache)
: base(repository)
{
_cache = cache;
}
/// <summary>
/// 获取产品详情(带缓存)
/// </summary>
public async Task<ProductDto> GetProductAsync(string id)
{
var cacheKey = $"product:{id}";
// GetOrCreate: 如果缓存存在则返回,否则执行 factory 并缓存结果
return await _cache.GetOrCreateAsync(
cacheKey,
async () =>
{
// 从数据库查询
var product = await Repository.GetByIdAsync(id);
return Mapper.Map<ProductDto>(product);
},
options: new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(30)
},
tags: new[] { "products", $"product:{id}" }
);
}
}设置缓存
// 简单设置
await _cache.SetAsync("key", value);
// 带过期时间
await _cache.SetAsync(
"key",
value,
new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromHours(1)
}
);
// 带标签
await _cache.SetAsync(
"key",
value,
tags: new[] { "tag1", "tag2" }
);获取缓存
// 获取缓存(不存在返回默认值)
var value = await _cache.GetAsync<MyType>("key");
if (value == null)
{
// 缓存不存在
}清除缓存
// 清除单个缓存
await _cache.RemoveAsync("key");
// 根据标签批量清除(强烈推荐)
await _cache.RemoveByTagAsync(new[] { "products" });
// 这会清除所有标记为 "products" 的缓存项实际应用场景
场景 1: 字典数据缓存
public async Task<List<DictionaryItem>> GetDictionariesAsync(string type)
{
return await _cache.GetOrCreateAsync(
$"dict:{type}",
async () => await _dictRepository.GetListAsync(d => d.Type == type),
tags: new[] { "dictionaries", $"dict:{type}" }
);
}
// 更新字典后清除缓存
public async Task UpdateDictionaryAsync(DictionaryItem item)
{
await _dictRepository.UpdateAsync(item);
await _cache.RemoveByTagAsync(new[] { $"dict:{item.Type}" });
}场景 2: 用户权限缓存
public async Task<List<string>> GetUserPermissionsAsync(string userId)
{
return await _cache.GetOrCreateAsync(
$"user:permissions:{userId}",
async () => await LoadPermissionsFromDb(userId),
new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromHours(2)
},
tags: new[] { $"user:{userId}", "permissions" }
);
}
// 权限变更后清除
public async Task UpdateUserRoleAsync(string userId, string roleId)
{
// ... 更新逻辑
// 清除该用户的所有缓存
await _cache.RemoveByTagAsync(new[] { $"user:{userId}" });
}场景 3: 统计数据缓存
public async Task<OrderStatistics> GetOrderStatisticsAsync(DateTime date)
{
return await _cache.GetOrCreateAsync(
$"stats:orders:{date:yyyy-MM-dd}",
async () => await CalculateStatistics(date),
new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromHours(1)
}
);
}最佳实践
✅ 推荐:
- 使用标签管理缓存,便于批量清除
- 为不同类型的缓存设置合理的过期时间
- 字典、配置等不常变数据可以缓存较长时间
- 用户相关数据使用用户ID作为标签
❌ 避免:
- 缓存大量临时数据
- 忘记清除过期缓存
- 不使用标签导致无法批量清除
- 缓存敏感信息(密码、Token等)控制器方法缓存特性(⭐ 推荐)
Fastdotnet 提供了两个强大的缓存特性,可以在控制器层面自动缓存 API 响应结果,无需手动编写缓存逻辑。
1. CacheResultAttribute - 方法结果缓存
在控制器方法上添加 [CacheResult] 特性,自动缓存方法的返回结果:
[Route("api/products")]
public class ProductController : ControllerBase
{
private readonly IProductService _productService;
public ProductController(IProductService productService)
{
_productService = productService;
}
/// <summary>
/// 获取产品列表(自动缓存 5 分钟)
/// </summary>
[HttpGet]
[CacheResult(ExpirationSeconds = 300)] // 缓存 5 分钟
public async Task<IActionResult> GetList()
{
var products = await _productService.GetAllAsync();
return Ok(products);
}
/// <summary>
/// 获取产品详情(自动缓存 30 分钟)
/// </summary>
[HttpGet("{id}")]
[CacheResult(ExpirationSeconds = 1800)] // 缓存 30 分钟
public async Task<IActionResult> GetById(string id)
{
var product = await _productService.GetByIdAsync(id);
return Ok(product);
}
}特性参数:
| 参数 | 类型 | 说明 | 默认值 |
|---|---|---|---|
KeyPrefix | string | 自定义缓存键前缀 | 自动生成 |
ExpirationSeconds | int | 缓存过期时间(秒) | 使用配置文件默认值 |
Enabled | bool | 是否启用缓存 | true |
工作原理:
- 自动生成缓存键:根据控制器类型、方法名和参数生成唯一缓存键
- 智能参数签名:对请求参数进行哈希处理,确保相同参数的请求命中缓存
- 自动序列化/反序列化:使用 Newtonsoft.Json 序列化和反序列化缓存数据
- 支持标签管理:配合
CacheTagAttribute实现批量清除
2. CacheTagAttribute - 缓存标签
为缓存项添加标签,便于批量清除相关缓存:
[Route("api/products")]
[CacheTag("products")] // 控制器级别标签
public class ProductController : ControllerBase
{
/// <summary>
/// 获取产品列表
/// </summary>
[HttpGet]
[CacheResult(ExpirationSeconds = 300)]
[CacheTag("product-list")] // 方法级别标签
public async Task<IActionResult> GetList()
{
// ...
}
/// <summary>
/// 创建产品(清除相关缓存)
/// </summary>
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateProductDto dto)
{
var product = await _productService.CreateAsync(dto);
// 清除所有标记为 "products" 的缓存
var cacheService = HttpContext.RequestServices.GetService<IHybridCacheService>();
await cacheService.RemoveByTagAsync(new[] { "products" });
return Ok(product);
}
}使用场景:
场景 1: 字典数据缓存
[Route("api/dictionaries")]
public class DictionaryController : ControllerBase
{
[HttpGet("{type}")]
[CacheResult(ExpirationSeconds = 3600)] // 缓存 1 小时
[CacheTag("dictionaries")]
public async Task<IActionResult> GetByType(string type)
{
var items = await _dictService.GetByTypeAsync(type);
return Ok(items);
}
[HttpPost]
public async Task<IActionResult> Create([FromBody] DictionaryItem item)
{
await _dictService.CreateAsync(item);
// 清除所有字典缓存
var cacheService = HttpContext.RequestServices.GetService<IHybridCacheService>();
await cacheService.RemoveByTagAsync(new[] { "dictionaries" });
return Ok();
}
}场景 2: 用户权限缓存
[Route("api/users")]
public class UserController : ControllerBase
{
[HttpGet("{userId}/permissions")]
[CacheResult(ExpirationSeconds = 7200)] // 缓存 2 小时
[CacheTag("permissions")]
public async Task<IActionResult> GetPermissions(string userId)
{
var permissions = await _userService.GetPermissionsAsync(userId);
return Ok(permissions);
}
[HttpPost("{userId}/role")]
public async Task<IActionResult> UpdateRole(string userId, [FromBody] UpdateRoleDto dto)
{
await _userService.UpdateRoleAsync(userId, dto.RoleId);
// 清除该用户的权限缓存
var cacheService = HttpContext.RequestServices.GetService<IHybridCacheService>();
await cacheService.RemoveByTagAsync(new[] { $"user:{userId}" });
return Ok();
}
}场景 3: 统计数据缓存
[Route("api/statistics")]
public class StatisticsController : ControllerBase
{
[HttpGet("orders/{date}")]
[CacheResult(ExpirationSeconds = 3600)] // 缓存 1 小时
[CacheTag("statistics")]
public async Task<IActionResult> GetOrderStatistics(DateTime date)
{
var stats = await _statsService.GetOrderStatisticsAsync(date);
return Ok(stats);
}
}最佳实践:
✅ 推荐:
- 为只读 GET 接口添加缓存特性
- 使用有意义的标签名称,便于管理和清除
- 根据数据更新频率设置合理的过期时间
- 在数据变更时及时清除相关缓存
❌ 避免:
- 不要对 POST/PUT/DELETE 等非幂等操作使用缓存
- 不要缓存包含敏感信息的响应
- 不要忘记在数据变更时清除缓存
- 不要设置过长的过期时间导致数据不一致2. 日志服务 (ILogService)
文件位置: Fastdotnet.Core.IService.ILogService
日志类型
| 日志类型 | 方法 | 用途 |
|---|---|---|
| 操作日志 | AddOperationLogAsync | 记录用户操作(增删改查) |
| 异常日志 | AddExceptionLogAsync | 记录系统异常 |
| 调试日志 | AddDebugLogAsync | 开发调试信息 |
操作日志
public class OrderController : GenericDtoControllerBase<Order>
{
private readonly ILogService _logService;
private readonly ICurrentUser _currentUser;
public OrderController(
IBaseService<Order>; service,
ILogService logService,
ICurrentUser currentUser)
: base(service)
{
_logService = logService;
_currentUser = currentUser;
}
[HttpPost]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderDto dto)
{
try
{
var order = await _service.InsertAsync(Mapper.Map<Order>(dto));
// 记录操作日志
await _logService.AddOperationLogAsync(new OperationLog
{
UserId = _currentUser.Id,
UserName = _currentUser.UserName,
Operation = "创建订单",
Module = "订单管理",
Method = "POST",
Url = "/api/orders",
IpAddress = HttpContext.Connection.RemoteIpAddress?.ToString(),
RequestData = JsonSerializer.Serialize(dto),
ResponseData = $"{{\"orderId\":\"{order.Id}\"}}",
Status = 200,
Duration = 0, // 可以计算耗时
CreatedAt = DateTime.Now
});
return Ok(order);
}
catch (Exception ex)
{
// 记录异常日志
await _logService.AddExceptionLogAsync(new ExceptionLog
{
UserId = _currentUser.Id,
UserName = _currentUser.UserName,
ExceptionType = ex.GetType().Name,
Message = ex.Message,
StackTrace = ex.StackTrace,
Url = "/api/orders",
Method = "POST",
RequestData = JsonSerializer.Serialize(dto),
CreatedAt = DateTime.Now
});
throw;
}
}
}异步 vs 同步
// 异步(推荐)
await _logService.AddOperationLogAsync(log);
// 同步(适用于无法使用 async 的场景)
_logService.AddOperationLog(log);使用中间件自动记录
Fastdotnet 提供了操作日志中间件,可以自动记录所有 API 请求:
// 在 Program.cs 中配置
app.UseOperationLogMiddleware();3. 当前用户服务 (ICurrentUser)
文件位置: Fastdotnet.Core.IService.ICurrentUser
接口定义
public interface ICurrentUser
{
/// <summary>
/// 是否已认证
/// </summary>
bool IsAuthenticated { get; }
/// <summary>
/// 用户ID
/// </summary>
string? Id { get; }
/// <summary>
/// 用户名
/// </summary>
string UserName { get; }
/// <summary>
/// 用户类型:Admin 或 App
/// </summary>
string UserType { get; }
/// <summary>
/// 是否为超级管理员
/// </summary>
bool IsSuperAdmin { get; }
}基本用法
public class TaskService : BaseService<TaskItem>;, ITaskService
{
private readonly ICurrentUser _currentUser;
public TaskService(
IRepository<TaskItem>; repository,
ICurrentUser currentUser)
: base(repository)
{
_currentUser = currentUser;
}
public async Task CreateTaskAsync(CreateTaskDto dto)
{
var task = new TaskItem
{
Id = Guid.NewGuid().ToString(),
Title = dto.Title,
CreatedBy = _currentUser.Id, // 自动填充创建人
// ... 其他字段
};
await Repository.InsertAsync(task);
}
public async Task DeleteTaskAsync(string taskId)
{
var task = await Repository.GetByIdAsync(taskId);
if (task == null)
{
throw new BusinessException("任务不存在");
}
// 权限检查:只能删除自己的任务(超级管理员除外)
if (!_currentUser.IsSuperAdmin && task.CreatedBy != _currentUser.Id)
{
throw new BusinessException("无权删除此任务");
}
await Repository.DeleteAsync(taskId);
}
}判断用户类型
public async Task<object> GetUserInfoAsync()
{
if (!_currentUser.IsAuthenticated)
{
throw new BusinessException("用户未登录");
}
return new
{
Id = _currentUser.Id,
UserName = _currentUser.UserName,
UserType = _currentUser.UserType, // "Admin" or "App"
IsSuperAdmin = _currentUser.IsSuperAdmin
};
}在控制器中使用
[HttpGet("my-tasks")]
public async Task<IActionResult> GetMyTasks()
{
// 只查询当前用户的任务
var tasks = await _repository.GetListAsync(
t => t.CreatedBy == _currentUser.Id
);
return Ok(tasks);
}4. 用户引用填充服务 (IUserRefFiller)
文件位置: Fastdotnet.Core.IService.Sys.IUserRefFiller
功能说明
将 DTO 中的用户ID自动转换为用户姓名,避免手动查询。
UserRefDto 结构
public class UserRefDto
{
/// <summary>
/// 用户ID
/// </summary>
public string Id { get; set; } = string.Empty;
/// <summary>
/// 用户姓名
/// </summary>
public string Name { get; set; } = "未知用户";
}使用示例
步骤 1: 在 DTO 中添加 UserRefDto 属性
public class TaskDto
{
public string Id { get; set; }
public string Title { get; set; }
// 原始用户ID
public string CreatedBy { get; set; }
// 用户引用信息(自动填充)
public UserRefDto CreatedByUser { get; set; }
public string UpdatedBy { get; set; }
public UserRefDto UpdatedByUser { get; set; }
}步骤 2: 在控制器中调用 FillNamesAsync
[HttpGet]
public async Task<IActionResult> GetTasks()
{
var tasks = await _service.GetListAsync(t => true);
var dtos = Mapper.Map<List<TaskDto>>(tasks);
// 自动填充用户姓名
await _userRefFiller.FillNamesAsync(
dtos,
SystemCategory.Admin, // 用户类型:Admin 或 App
dto => dto.CreatedByUser, // 指定要填充的字段
dto => dto.UpdatedByUser
);
return Ok(dtos);
}步骤 3: 返回结果
[
{
"id": "task_001",
"title": "测试任务",
"createdBy": "user_123",
"createdByUser": {
"id": "user_123",
"name": "张三"
},
"updatedBy": "user_456",
"updatedByUser": {
"id": "user_456",
"name": "李四"
}
}
]批量填充
// 分页查询后填充
var pageResult = await _repository.GetPageAsync(1, 10);
var dtos = Mapper.Map<List<TaskDto>>(pageResult.Items);
// 批量填充(性能优化:一次性查询所有用户)
await _userRefFiller.FillNamesAsync(
dtos,
SystemCategory.Admin,
dto => dto.CreatedByUser
);多个用户字段
public class OrderDto
{
public string Id { get; set; }
public string CreatedBy { get; set; }
public UserRefDto CreatedByUser { get; set; }
public string ApprovedBy { get; set; }
public UserRefDto ApprovedByUser { get; set; }
public string CancelledBy { get; set; }
public UserRefDto CancelledByUser { get; set; }
}
// 一次性填充所有用户字段
await _userRefFiller.FillNamesAsync(
orders,
SystemCategory.Admin,
dto => dto.CreatedByUser,
dto => dto.ApprovedByUser,
dto => dto.CancelledByUser
);优势
✅ 传统方式(N+1 查询问题):
foreach (var task in tasks)
{
var user = await _userRepository.GetByIdAsync(task.CreatedBy); // N次查询
task.CreatedByName = user.Name;
}
✅ 使用 IUserRefFiller(优化为 1 次查询):
await _userRefFiller.FillNamesAsync(tasks, SystemCategory.Admin, t => t.CreatedByUser);
// 内部实现:
// 1. 收集所有 UserID
// 2. 一次性查询所有用户
// 3. 映射到对应的 DTO5. 数据脱敏服务 ⭐
文件位置:
Fastdotnet.Core.Attributes.SensitiveDataAttributeFastdotnet.Core.Extensions.AutoMapperSensitiveDataExtensions
功能说明
在数据传输过程中自动对敏感信息进行脱敏处理,保护用户隐私和数据安全。
SensitiveDataAttribute
通过特性标记需要脱敏的字段,支持多种预定义的数据类型:
public enum SensitiveDataType
{
Phone, // 手机号
Email, // 邮箱
IdCard, // 身份证
BankCard, // 银行卡
Name, // 姓名
Custom // 自定义规则
}使用示例
步骤 1: 在 DTO 中标记敏感字段
public class UserDto
{
public string Id { get; set; }
[SensitiveData(SensitiveDataType.Name)]
public string Name { get; set; }
[SensitiveData(SensitiveDataType.Phone)]
public string Phone { get; set; }
[SensitiveData(SensitiveDataType.Email)]
public string Email { get; set; }
[SensitiveData(SensitiveDataType.IdCard)]
public string IdCard { get; set; }
[SensitiveData(SensitiveDataType.BankCard)]
public string BankCard { get; set; }
// 自定义脱敏规则
[SensitiveData(SensitiveDataType.Custom,
CustomPattern = @"\d{4}(\d{4})\d{4}",
CustomReplacement = "****$1****")]
public string CustomField { get; set; }
}步骤 2: 在 AutoMapper 映射中启用脱敏
public class UserProfile : Profile
{
public UserProfile()
{
CreateMap<User, UserDto>()
.MaskSensitiveData<User, UserDto>(); // 启用脱敏
}
}步骤 3: 返回结果
{
"id": "user_123",
"name": "张*", // 姓名脱敏
"phone": "138****5678", // 手机号脱敏
"email": "ex***le@domain.com", // 邮箱脱敏
"idCard": "110101********1234", // 身份证脱敏
"bankCard": "622202******7890" // 银行卡脱敏
}脱敏规则
| 数据类型 | 示例 | 脱敏后 |
|---|---|---|
| Phone | 13812345678 | 138****5678 |
| example@domain.com | ex***le@domain.com | |
| IdCard | 110101199001011234 | 110101********1234 |
| BankCard | 6222021234567890 | 622202******7890 |
| Name | 张三 | 张* |
自定义脱敏参数
对于更精细的脱敏需求,可以使用自定义参数:
public class CustomUserDto
{
[SensitiveData(SensitiveDataType.Phone,
PrefixKeep = 2, // 保留前缀2位
SuffixKeep = 2, // 保留后缀2位
MaskChar = '#')] // 使用 # 作为脱敏字符
public string Phone { get; set; }
[SensitiveData(SensitiveDataType.Name,
PrefixKeep = 1, // 保留前缀1位
SuffixKeep = 0, // 不保留后缀
MaskChar = '*',
MaskLength = 2)] // 脱敏部分长度为2
public string Name { get; set; }
}自定义脱敏规则
使用正则表达式定义完全自定义的脱敏规则:
public class CustomFieldDto
{
[SensitiveData(SensitiveDataType.Custom,
CustomPattern = @"^(.{3}).*(.{4})$", // 匹配前后固定字符
CustomReplacement = "$1****$2")] // 替换为保留前后字符,中间用****
public string Field { get; set; }
}在服务层使用
public class UserService : BaseService<User>;, IUserService
{
public async Task<UserDto> GetUserAsync(string id)
{
var user = await Repository.GetByIdAsync(id);
// 自动应用脱敏规则
var dto = Mapper.Map<UserDto>(user);
return dto;
}
public async Task<List<UserDto>> GetUsersAsync()
{
var users = await Repository.GetListAsync(_ => true);
// 批量脱敏
var dtos = users.Select(user => Mapper.Map<UserDto>(user)).ToList();
return dtos;
}
}优势
✅ 集中式管理:通过特性统一配置脱敏规则
✅ 类型安全:编译时检查脱敏配置
✅ 自动应用:映射时自动执行脱敏
✅ 灵活配置:支持自定义参数和规则
✅ 性能良好:脱敏操作在映射阶段完成
❌ 避免手动脱敏:
// 不推荐
userDto.Phone = user.Phone.Substring(0, 3) + "****" + user.Phone.Substring(7);最佳实践
✅ 推荐:
- 在 DTO 层标记敏感字段
- 使用预定义的数据类型
- 在 AutoMapper 配置中启用脱敏
- 定期审查敏感字段标记
⚠️ 注意:
- 仅对需要对外暴露的敏感数据进行脱敏
- 不要在数据库层进行脱敏存储
- 脱敏仅用于数据传输,原始数据保持完整💡 最佳实践
1. 实体设计规范
✅ 推荐:
- 继承 BaseEntity 或 AuditableEntity
- 使用 [SugarColumn] 明确指定列名
- 字符串字段指定 Length
- 可空字段使用 nullable 类型
- 添加清晰的注释
❌ 避免:
- 不继承基类
- 使用魔法数字
- 忽略索引优化2. 查询优化
✅ 推荐:
// 只查询需要的字段
var titles = await _repository.GetListAsync(
t => t.Status == 0,
t => t.Title
);
// 使用分页
var page = await _repository.GetPageAsync(1, 10);
// 添加索引
[SugarIndex("idx_status", nameof(Status), OrderByType.Asc)]
❌ 避免:
// 查询所有字段
var all = await _repository.GetAllAsync();
// 不分页查询大量数据
var all = await _repository.GetListAsync(_ => true);3. 事务使用
✅ 推荐:
- 只在必要时使用事务
- 保持事务简短
- 及时提交或回滚
- 使用 try-catch 处理异常
❌ 避免:
- 在事务中进行耗时操作
- 嵌套事务
- 忘记回滚4. 错误处理
✅ 推荐:
try
{
await _service.DoSomething();
}
catch (BusinessException ex)
{
return BadRequest(new { error = ex.Message });
}
catch (Exception ex)
{
_logger.LogError(ex, "操作失败");
return StatusCode(500, new { error = "Internal server error" });
}
❌ 避免:
catch (Exception ex)
{
// 吞掉异常
}❓ 常见问题
Q: 如何选择 BaseEntity 还是 AuditableEntity?
A:
- 需要追踪操作人 → AuditableEntity
- 不需要追踪 → BaseEntity
Q: 软删除的数据如何查询?
A:
// 默认查询会过滤已删除的数据
var tasks = await _repository.GetListAsync(t => t.Status == 0);
// 查询包括已删除的数据
var allTasks = await _db.Queryable<TaskItem>()
.IgnoreQueryFilters() // 忽略全局过滤器
.ToListAsync();Q: 如何实现复杂的业务逻辑?
A:
- 在服务层实现业务逻辑
- 使用工作单元管理事务
- 在控制器中调用服务
- 保持控制器轻薄
Q: 如何优化查询性能?
A:
- 使用投影查询(只查需要的字段)
- 添加合适的数据库索引
- 使用分页避免大数据量
- 缓存常用数据
🔗 相关链接
- PluginA演示插件 - 完整示例
- 前端开发 - 前后端联调
- 插件开发 - 插件后端开发