第七章:数据验证与异常处理
7.1 数据验证概述
在 Web 应用开发中,数据验证是保障系统安全性和数据完整性的关键环节。Furion 框架提供了多层次的数据验证机制,包括模型验证、自定义验证、FluentValidation 集成等,同时还提供了统一的异常处理方案,确保 API 返回友好且一致的错误信息。
Furion 数据验证的核心特性:
| 特性 | 说明 |
|---|---|
| DataAnnotations 验证 | 基于特性标注的声明式验证 |
| FluentValidation 集成 | 支持流式验证规则定义 |
| 自定义验证特性 | 支持自定义验证逻辑 |
| 验证过滤器 | 全局统一验证处理 |
| 友好异常 | 结构化异常返回 |
| 全局异常处理 | 统一异常拦截与处理 |
7.2 DataAnnotations 验证
7.2.1 内置验证特性
.NET 提供了丰富的数据注解验证特性,Furion 完全支持并进行了增强:
| 验证特性 | 说明 | 示例 |
|---|---|---|
[Required] |
必填验证 | [Required(ErrorMessage = "不能为空")] |
[StringLength] |
字符串长度验证 | [StringLength(50, MinimumLength = 2)] |
[MaxLength] |
最大长度 | [MaxLength(100)] |
[MinLength] |
最小长度 | [MinLength(6)] |
[Range] |
数值范围验证 | [Range(0, 150)] |
[EmailAddress] |
邮箱格式验证 | [EmailAddress] |
[Phone] |
手机号格式验证 | [Phone] |
[Url] |
URL 格式验证 | [Url] |
[RegularExpression] |
正则表达式验证 | [RegularExpression(@"^\d{6}$")] |
[Compare] |
属性比较验证 | [Compare("Password")] |
[CreditCard] |
信用卡号验证 | [CreditCard] |
7.2.2 验证模型示例
using System.ComponentModel.DataAnnotations;
/// <summary>
/// 用户注册输入模型
/// </summary>
public class UserRegisterInput
{
/// <summary>
/// 用户名
/// </summary>
[Required(ErrorMessage = "用户名不能为空")]
[StringLength(50, MinimumLength = 2, ErrorMessage = "用户名长度必须在2-50个字符之间")]
[RegularExpression(@"^[a-zA-Z0-9_]+$", ErrorMessage = "用户名只能包含字母、数字和下划线")]
public string UserName { get; set; }
/// <summary>
/// 邮箱
/// </summary>
[Required(ErrorMessage = "邮箱不能为空")]
[EmailAddress(ErrorMessage = "邮箱格式不正确")]
[MaxLength(100, ErrorMessage = "邮箱长度不能超过100个字符")]
public string Email { get; set; }
/// <summary>
/// 密码
/// </summary>
[Required(ErrorMessage = "密码不能为空")]
[MinLength(6, ErrorMessage = "密码长度不能少于6个字符")]
[MaxLength(20, ErrorMessage = "密码长度不能超过20个字符")]
public string Password { get; set; }
/// <summary>
/// 确认密码
/// </summary>
[Required(ErrorMessage = "确认密码不能为空")]
[Compare("Password", ErrorMessage = "两次密码输入不一致")]
public string ConfirmPassword { get; set; }
/// <summary>
/// 年龄
/// </summary>
[Range(1, 150, ErrorMessage = "年龄必须在1-150之间")]
public int Age { get; set; }
/// <summary>
/// 手机号
/// </summary>
[Phone(ErrorMessage = "手机号格式不正确")]
[RegularExpression(@"^1[3-9]\d{9}$", ErrorMessage = "请输入正确的手机号")]
public string PhoneNumber { get; set; }
}
7.2.3 在控制器中使用验证
using Furion.DynamicApiController;
using Furion.DataValidation;
public class UserService : IDynamicApiController
{
// Furion 自动进行模型验证,验证失败会自动返回 422 状态码
public async Task<string> Register(UserRegisterInput input)
{
// 如果执行到这里,说明验证已通过
// 处理业务逻辑...
return "注册成功";
}
// 手动触发验证
public async Task<string> ManualValidate(UserRegisterInput input)
{
// 手动验证
var (isValid, validationResults) = input.TryValidate();
if (!isValid)
{
var errors = validationResults
.Select(v => v.ErrorMessage)
.ToList();
throw new AppFriendlyException("参数验证失败:" + string.Join("; ", errors));
}
return "验证通过";
}
}
7.3 FluentValidation 集成
7.3.1 安装与配置
# 安装 FluentValidation 包
dotnet add package FluentValidation
dotnet add package FluentValidation.AspNetCore
在 Program.cs 中注册:
using FluentValidation;
using FluentValidation.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
// 注册 FluentValidation
builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
7.3.2 创建验证器
using FluentValidation;
/// <summary>
/// 订单创建验证器
/// </summary>
public class CreateOrderInputValidator : AbstractValidator<CreateOrderInput>
{
public CreateOrderInputValidator()
{
// 订单编号验证
RuleFor(x => x.OrderNo)
.NotEmpty().WithMessage("订单编号不能为空")
.MaximumLength(32).WithMessage("订单编号长度不能超过32个字符")
.Matches(@"^ORD\d+$").WithMessage("订单编号格式不正确,应以ORD开头");
// 商品名称验证
RuleFor(x => x.ProductName)
.NotEmpty().WithMessage("商品名称不能为空")
.Length(2, 200).WithMessage("商品名称长度应在2-200个字符之间");
// 金额验证
RuleFor(x => x.Amount)
.GreaterThan(0).WithMessage("订单金额必须大于0")
.LessThanOrEqualTo(999999.99m).WithMessage("订单金额不能超过999999.99");
// 数量验证
RuleFor(x => x.Quantity)
.InclusiveBetween(1, 9999).WithMessage("商品数量必须在1-9999之间");
// 收货地址验证
RuleFor(x => x.Address)
.NotEmpty().WithMessage("收货地址不能为空")
.MaximumLength(500).WithMessage("收货地址长度不能超过500个字符");
// 条件验证:当需要发票时,发票抬头必填
RuleFor(x => x.InvoiceTitle)
.NotEmpty().WithMessage("发票抬头不能为空")
.When(x => x.NeedInvoice);
// 集合验证
RuleFor(x => x.Items)
.NotEmpty().WithMessage("订单明细不能为空")
.Must(items => items.Count <= 100).WithMessage("订单明细数量不能超过100条");
// 子项验证
RuleForEach(x => x.Items).SetValidator(new OrderItemValidator());
}
}
/// <summary>
/// 订单明细验证器
/// </summary>
public class OrderItemValidator : AbstractValidator<OrderItemInput>
{
public OrderItemValidator()
{
RuleFor(x => x.ProductId)
.GreaterThan(0).WithMessage("商品ID无效");
RuleFor(x => x.UnitPrice)
.GreaterThan(0).WithMessage("单价必须大于0");
RuleFor(x => x.Quantity)
.InclusiveBetween(1, 999).WithMessage("数量必须在1-999之间");
}
}
/// <summary>
/// 订单创建输入模型
/// </summary>
public class CreateOrderInput
{
public string OrderNo { get; set; }
public string ProductName { get; set; }
public decimal Amount { get; set; }
public int Quantity { get; set; }
public string Address { get; set; }
public bool NeedInvoice { get; set; }
public string InvoiceTitle { get; set; }
public List<OrderItemInput> Items { get; set; }
}
public class OrderItemInput
{
public int ProductId { get; set; }
public decimal UnitPrice { get; set; }
public int Quantity { get; set; }
}
7.3.3 异步验证
public class UserRegisterValidator : AbstractValidator<UserRegisterInput>
{
private readonly IRepository<User> _userRepository;
public UserRegisterValidator(IRepository<User> userRepository)
{
_userRepository = userRepository;
RuleFor(x => x.UserName)
.NotEmpty().WithMessage("用户名不能为空")
.MustAsync(BeUniqueUserName).WithMessage("用户名已存在");
RuleFor(x => x.Email)
.NotEmpty().WithMessage("邮箱不能为空")
.EmailAddress().WithMessage("邮箱格式不正确")
.MustAsync(BeUniqueEmail).WithMessage("邮箱已被注册");
}
private async Task<bool> BeUniqueUserName(string userName, CancellationToken cancellationToken)
{
return !await _userRepository.AnyAsync(u => u.UserName == userName);
}
private async Task<bool> BeUniqueEmail(string email, CancellationToken cancellationToken)
{
return !await _userRepository.AnyAsync(u => u.Email == email);
}
}
7.4 自定义验证特性
7.4.1 创建自定义验证特性
using System.ComponentModel.DataAnnotations;
/// <summary>
/// 身份证号码验证特性
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class IdCardAttribute : ValidationAttribute
{
public IdCardAttribute() : base("身份证号码格式不正确")
{
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null) return ValidationResult.Success;
var idCard = value.ToString();
// 18位身份证号码正则
var regex = new System.Text.RegularExpressions.Regex(
@"^\d{17}[\dXx]$");
if (!regex.IsMatch(idCard))
{
return new ValidationResult(ErrorMessage);
}
// 校验码验证
if (!ValidateCheckCode(idCard))
{
return new ValidationResult("身份证号码校验码错误");
}
return ValidationResult.Success;
}
private bool ValidateCheckCode(string idCard)
{
int[] weights = { 7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2 };
char[] checkCodes = { '1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2' };
int sum = 0;
for (int i = 0; i < 17; i++)
{
sum += (idCard[i] - '0') * weights[i];
}
char expectedCheck = checkCodes[sum % 11];
return char.ToUpper(idCard[17]) == expectedCheck;
}
}
/// <summary>
/// 中国手机号验证特性
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class ChinaPhoneAttribute : ValidationAttribute
{
public ChinaPhoneAttribute() : base("手机号码格式不正确")
{
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (value == null) return ValidationResult.Success;
var phone = value.ToString();
var regex = new System.Text.RegularExpressions.Regex(@"^1[3-9]\d{9}$");
return regex.IsMatch(phone)
? ValidationResult.Success
: new ValidationResult(ErrorMessage);
}
}
// 使用自定义验证特性
public class PersonInput
{
[Required(ErrorMessage = "姓名不能为空")]
public string Name { get; set; }
[IdCard]
public string IdCardNo { get; set; }
[ChinaPhone]
public string Phone { get; set; }
}
7.4.2 跨属性验证
/// <summary>
/// 日期范围验证特性
/// </summary>
public class DateRangeAttribute : ValidationAttribute
{
private readonly string _startDateProperty;
public DateRangeAttribute(string startDateProperty)
{
_startDateProperty = startDateProperty;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var endDate = (DateTime?)value;
var startDateProp = validationContext.ObjectType.GetProperty(_startDateProperty);
if (startDateProp == null)
return new ValidationResult($"未找到属性 {_startDateProperty}");
var startDate = (DateTime?)startDateProp.GetValue(validationContext.ObjectInstance);
if (startDate.HasValue && endDate.HasValue && endDate < startDate)
{
return new ValidationResult("结束日期不能早于开始日期");
}
return ValidationResult.Success;
}
}
// 使用示例
public class QueryInput
{
public DateTime? StartDate { get; set; }
[DateRange("StartDate")]
public DateTime? EndDate { get; set; }
}
7.5 验证过滤器
7.5.1 全局验证过滤器
using Furion.DataValidation;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
/// <summary>
/// 全局数据验证过滤器
/// </summary>
public class GlobalValidationFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
var errors = context.ModelState
.Where(e => e.Value.Errors.Count > 0)
.Select(e => new
{
Field = e.Key,
Messages = e.Value.Errors.Select(err => err.ErrorMessage).ToList()
})
.ToList();
context.Result = new JsonResult(new
{
Code = 422,
Message = "数据验证失败",
Errors = errors
})
{
StatusCode = 422
};
}
}
public void OnActionExecuted(ActionExecutedContext context) { }
}
// 注册全局验证过滤器
builder.Services.AddControllers(options =>
{
options.Filters.Add<GlobalValidationFilter>();
});
7.5.2 Furion 内置验证配置
// 在 Program.cs 中配置 Furion 数据验证
builder.Services.AddControllers()
.AddDataValidation(); // 启用 Furion 数据验证
// 也可以配置验证选项
builder.Services.AddControllers()
.AddDataValidation(options =>
{
// 全局启用验证
options.GlobalEnabled = true;
// 是否在验证失败时抛出异常
options.SuppressModelStateInvalidFilter = false;
});
7.6 全局异常处理
7.6.1 IGlobalExceptionHandler
Furion 提供了 IGlobalExceptionHandler 接口用于全局异常处理:
using Furion.FriendlyException;
/// <summary>
/// 全局异常处理器
/// </summary>
public class GlobalExceptionHandler : IGlobalExceptionHandler
{
private readonly ILogger<GlobalExceptionHandler> _logger;
public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
{
_logger = logger;
}
public Task OnExceptionAsync(ExceptionContext context)
{
// 记录异常日志
_logger.LogError(context.Exception, "全局异常捕获:{Message}", context.Exception.Message);
// 设置返回结果
var result = new
{
Code = 500,
Message = "服务器内部错误,请联系管理员",
Detail = context.Exception.Message
};
context.Result = new JsonResult(result)
{
StatusCode = StatusCodes.Status500InternalServerError
};
context.ExceptionHandled = true;
return Task.CompletedTask;
}
}
// 注册全局异常处理器
builder.Services.AddSingleton<IGlobalExceptionHandler, GlobalExceptionHandler>();
7.6.2 自定义异常处理中间件
/// <summary>
/// 异常处理中间件
/// </summary>
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (AppFriendlyException ex)
{
_logger.LogWarning(ex, "业务异常:{Message}", ex.Message);
await HandleFriendlyExceptionAsync(context, ex);
}
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, "未授权访问:{Message}", ex.Message);
await HandleUnauthorizedExceptionAsync(context, ex);
}
catch (Exception ex)
{
_logger.LogError(ex, "未处理的异常:{Message}", ex.Message);
await HandleUnknownExceptionAsync(context, ex);
}
}
private static async Task HandleFriendlyExceptionAsync(HttpContext context, AppFriendlyException ex)
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
context.Response.ContentType = "application/json";
var response = new
{
Code = ex.ErrorCode ?? 400,
Message = ex.Message
};
await context.Response.WriteAsJsonAsync(response);
}
private static async Task HandleUnauthorizedExceptionAsync(HttpContext context, UnauthorizedAccessException ex)
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Response.ContentType = "application/json";
var response = new
{
Code = 401,
Message = "未授权访问"
};
await context.Response.WriteAsJsonAsync(response);
}
private static async Task HandleUnknownExceptionAsync(HttpContext context, Exception ex)
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
context.Response.ContentType = "application/json";
var response = new
{
Code = 500,
Message = "服务器内部错误"
};
await context.Response.WriteAsJsonAsync(response);
}
}
// 在 Program.cs 中注册中间件
app.UseMiddleware<ExceptionHandlingMiddleware>();
7.7 友好异常
7.7.1 AppFriendlyException
Furion 提供了 AppFriendlyException 友好异常类,可以将业务异常转化为友好的错误信息:
using Furion.FriendlyException;
public class OrderService : IDynamicApiController
{
private readonly IRepository<Order> _orderRepo;
private readonly IRepository<User> _userRepo;
public OrderService(IRepository<Order> orderRepo, IRepository<User> userRepo)
{
_orderRepo = orderRepo;
_userRepo = userRepo;
}
public async Task<Order> CreateOrder(CreateOrderInput input)
{
// 使用 Oops.Oh 抛出友好异常
var user = await _userRepo.FindAsync(input.UserId);
if (user == null)
{
throw Oops.Oh("用户不存在");
}
// 使用错误码
if (input.Amount <= 0)
{
throw Oops.Oh(ErrorCodes.OrderAmountInvalid);
}
// 带格式化参数的异常
var existOrder = await _orderRepo.AnyAsync(o => o.OrderNo == input.OrderNo);
if (existOrder)
{
throw Oops.Oh("订单号 {0} 已存在", input.OrderNo);
}
var order = new Order
{
OrderNo = input.OrderNo,
Amount = input.Amount,
UserId = input.UserId,
Status = OrderStatus.Pending
};
var result = await _orderRepo.InsertNowAsync(order);
return result.Entity;
}
public async Task CancelOrder(int orderId)
{
var order = await _orderRepo.FindAsync(orderId)
?? throw Oops.Oh("订单不存在");
if (order.Status == OrderStatus.Completed)
{
throw Oops.Oh("已完成的订单不能取消");
}
if (order.Status == OrderStatus.Cancelled)
{
throw Oops.Oh("订单已经被取消");
}
order.Status = OrderStatus.Cancelled;
await _orderRepo.UpdateAsync(order);
await _orderRepo.SaveNowAsync();
}
}
7.7.2 错误码定义
using Furion.FriendlyException;
/// <summary>
/// 错误码枚举
/// </summary>
[ErrorCodeType]
public enum ErrorCodes
{
// 通用错误 (1000-1999)
[ErrorCodeItemMetadata("操作失败")]
OperationFailed = 1000,
[ErrorCodeItemMetadata("参数无效")]
InvalidParameter = 1001,
[ErrorCodeItemMetadata("数据不存在")]
DataNotFound = 1002,
[ErrorCodeItemMetadata("数据已存在")]
DataAlreadyExists = 1003,
// 用户相关错误 (2000-2999)
[ErrorCodeItemMetadata("用户不存在")]
UserNotFound = 2000,
[ErrorCodeItemMetadata("用户名已存在")]
UserNameExists = 2001,
[ErrorCodeItemMetadata("密码错误")]
PasswordError = 2002,
[ErrorCodeItemMetadata("账号已被禁用")]
AccountDisabled = 2003,
// 订单相关错误 (3000-3999)
[ErrorCodeItemMetadata("订单金额无效")]
OrderAmountInvalid = 3000,
[ErrorCodeItemMetadata("订单状态不允许此操作")]
OrderStatusInvalid = 3001,
[ErrorCodeItemMetadata("库存不足")]
InsufficientStock = 3002
}
// 使用错误码抛出异常
public class UserService : IDynamicApiController
{
private readonly IRepository<User> _userRepo;
public UserService(IRepository<User> userRepo)
{
_userRepo = userRepo;
}
public async Task<User> GetUser(int id)
{
var user = await _userRepo.FindAsync(id);
if (user == null)
{
throw Oops.Oh(ErrorCodes.UserNotFound);
}
return user;
}
public async Task Login(string userName, string password)
{
var user = await _userRepo.FirstOrDefaultAsync(u => u.UserName == userName);
if (user == null) throw Oops.Oh(ErrorCodes.UserNotFound);
if (user.IsDisabled) throw Oops.Oh(ErrorCodes.AccountDisabled);
// 密码验证...
}
}
7.8 异常日志记录
using Furion.FriendlyException;
using Microsoft.Extensions.Logging;
/// <summary>
/// 异常日志订阅器
/// </summary>
public class ExceptionLogSubscriber : IGlobalExceptionHandler
{
private readonly ILogger<ExceptionLogSubscriber> _logger;
public ExceptionLogSubscriber(ILogger<ExceptionLogSubscriber> logger)
{
_logger = logger;
}
public Task OnExceptionAsync(ExceptionContext context)
{
var exception = context.Exception;
// 根据异常类型使用不同级别记录日志
switch (exception)
{
case AppFriendlyException friendlyEx:
// 业务异常记录为 Warning
_logger.LogWarning("业务异常 | 错误码: {ErrorCode} | 消息: {Message}",
friendlyEx.ErrorCode, friendlyEx.Message);
break;
case UnauthorizedAccessException:
// 权限异常记录为 Warning
_logger.LogWarning("权限异常 | 路径: {Path} | 消息: {Message}",
context.HttpContext.Request.Path, exception.Message);
break;
default:
// 系统异常记录为 Error(包含完整堆栈信息)
_logger.LogError(exception,
"系统异常 | 路径: {Path} | 方法: {Method} | 消息: {Message}",
context.HttpContext.Request.Path,
context.HttpContext.Request.Method,
exception.Message);
break;
}
return Task.CompletedTask;
}
}
7.9 HTTP状态码管理
7.9.1 统一响应格式
/// <summary>
/// 统一API响应模型
/// </summary>
public class ApiResult<T>
{
/// <summary>
/// 状态码
/// </summary>
public int Code { get; set; }
/// <summary>
/// 消息
/// </summary>
public string Message { get; set; }
/// <summary>
/// 数据
/// </summary>
public T Data { get; set; }
/// <summary>
/// 时间戳
/// </summary>
public long Timestamp { get; set; } = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
public static ApiResult<T> Success(T data, string message = "操作成功")
{
return new ApiResult<T> { Code = 200, Message = message, Data = data };
}
public static ApiResult<T> Fail(string message, int code = 400)
{
return new ApiResult<T> { Code = code, Message = message };
}
}
/// <summary>
/// 统一响应包装过滤器
/// </summary>
public class ApiResultFilter : IAsyncResultFilter
{
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
if (context.Result is ObjectResult objectResult)
{
// 如果已经是 ApiResult 类型,则不再包装
if (objectResult.Value is not null &&
objectResult.Value.GetType().IsGenericType &&
objectResult.Value.GetType().GetGenericTypeDefinition() == typeof(ApiResult<>))
{
await next();
return;
}
// 包装为统一格式
var statusCode = objectResult.StatusCode ?? StatusCodes.Status200OK;
objectResult.Value = new
{
Code = statusCode,
Message = statusCode == 200 ? "操作成功" : "操作失败",
Data = objectResult.Value,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
}
await next();
}
}
7.9.2 常用HTTP状态码映射
| HTTP状态码 | 说明 | 使用场景 |
|---|---|---|
| 200 | 成功 | 正常请求响应 |
| 201 | 已创建 | 资源创建成功 |
| 204 | 无内容 | 删除成功 |
| 400 | 错误请求 | 客户端参数错误 |
| 401 | 未授权 | 未提供认证信息或认证失败 |
| 403 | 禁止访问 | 无权限访问 |
| 404 | 未找到 | 资源不存在 |
| 409 | 冲突 | 资源冲突(如重复创建) |
| 422 | 参数验证失败 | 数据验证不通过 |
| 500 | 服务器错误 | 系统内部异常 |
7.10 模型验证最佳实践
7.10.1 分层验证策略
// 1. 表示层 - 使用 DataAnnotations 进行基本验证
public class UpdateProductInput
{
[Required(ErrorMessage = "商品ID不能为空")]
public int Id { get; set; }
[Required(ErrorMessage = "商品名称不能为空")]
[MaxLength(200)]
public string Name { get; set; }
[Range(0.01, double.MaxValue, ErrorMessage = "价格必须大于0")]
public decimal Price { get; set; }
}
// 2. 业务层 - 使用 FluentValidation 进行复杂业务验证
public class UpdateProductValidator : AbstractValidator<UpdateProductInput>
{
private readonly IRepository<Product> _productRepo;
public UpdateProductValidator(IRepository<Product> productRepo)
{
_productRepo = productRepo;
RuleFor(x => x.Name)
.MustAsync(async (input, name, cancellation) =>
{
// 排除自身的名称唯一性检查
return !await _productRepo.AnyAsync(
p => p.Name == name && p.Id != input.Id);
})
.WithMessage("商品名称已存在");
}
}
// 3. 服务层 - 进行业务规则验证
public class ProductService : IDynamicApiController
{
private readonly IRepository<Product> _productRepo;
public ProductService(IRepository<Product> productRepo)
{
_productRepo = productRepo;
}
public async Task UpdateProduct(UpdateProductInput input)
{
var product = await _productRepo.FindAsync(input.Id)
?? throw Oops.Oh("商品不存在");
// 业务规则验证
if (product.Status == ProductStatus.OffShelf)
{
throw Oops.Oh("已下架的商品不能修改");
}
product.Name = input.Name;
product.Price = input.Price;
await _productRepo.UpdateAsync(product);
await _productRepo.SaveNowAsync();
}
}
7.11 业务异常与系统异常区分
在实际开发中,需要明确区分业务异常和系统异常,它们的处理方式不同:
| 类别 | 说明 | 处理方式 | HTTP状态码 |
|---|---|---|---|
| 业务异常 | 业务规则不满足导致的异常 | 返回友好提示信息给用户 | 400/422 |
| 参数异常 | 输入参数不合法 | 返回验证错误详情 | 400/422 |
| 认证异常 | 未登录或登录过期 | 引导用户重新登录 | 401 |
| 授权异常 | 无权限操作 | 提示权限不足 | 403 |
| 系统异常 | 程序 Bug 或系统故障 | 记录详细日志,返回通用错误信息 | 500 |
/// <summary>
/// 业务异常基类
/// </summary>
public class BusinessException : AppFriendlyException
{
public BusinessException(string message) : base(message)
{
}
public BusinessException(string message, object errorCode) : base(message, errorCode)
{
}
}
/// <summary>
/// 未找到异常
/// </summary>
public class NotFoundException : BusinessException
{
public NotFoundException(string entityName, object id)
: base($"{entityName}(ID: {id})不存在")
{
}
}
/// <summary>
/// 权限不足异常
/// </summary>
public class ForbiddenException : BusinessException
{
public ForbiddenException(string message = "您没有权限执行此操作")
: base(message)
{
}
}
// 使用示例
public class UserManageService : IDynamicApiController
{
private readonly IRepository<User> _userRepo;
public UserManageService(IRepository<User> userRepo)
{
_userRepo = userRepo;
}
public async Task DeleteUser(int id)
{
var user = await _userRepo.FindAsync(id)
?? throw new NotFoundException("用户", id);
if (user.UserName == "admin")
{
throw new ForbiddenException("不允许删除管理员账号");
}
await _userRepo.DeleteAsync(user);
await _userRepo.SaveNowAsync();
}
}