第十六章:规范化接口与统一返回
一、规范化接口概述
1.1 为什么需要统一的 API 返回格式
在实际开发中,前后端分离架构已经成为主流。如果每个接口都按照自己的方式返回数据,前端开发人员将面临巨大的解析负担。统一的 API 返回格式能够带来以下好处:
- 一致性:所有接口遵循相同的数据结构,降低前端对接成本
- 可维护性:统一的错误处理和状态码映射,便于排查问题
- 可扩展性:预留扩展字段,适应业务增长需求
- 文档友好:Swagger 文档能够清晰展示统一的返回结构
1.2 常见的返回格式问题
| 问题类型 | 描述 | 影响 |
|---|---|---|
| 返回结构不统一 | 有的接口返回 { data: ... },有的直接返回数据 |
前端解析困难 |
| 错误码不规范 | 使用自定义错误码与 HTTP 状态码混用 | 错误处理混乱 |
| 缺少时间戳 | 无法追踪请求时间 | 调试排查困难 |
| 异常信息暴露 | 直接返回堆栈信息给前端 | 安全隐患 |
| 分页格式不统一 | 不同接口的分页字段名称不一致 | 通用组件难以封装 |
1.3 Furion 的解决方案
Furion 框架提供了完整的规范化接口解决方案,通过 IUnifyResultProvider 接口和内置的规范化中间件,可以轻松实现统一返回格式:
// Furion 规范化返回的基本使用
services.AddControllers()
.AddUnifyResult(); // 一行代码启用规范化返回
二、统一返回模型设计
2.1 RESTfulResult<T> 结构体
Furion 中推荐使用如下统一返回模型:
/// <summary>
/// RESTful 风格统一返回结果
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
public class RESTfulResult<T>
{
/// <summary>
/// 状态码
/// </summary>
public int? Code { get; set; }
/// <summary>
/// 是否成功
/// </summary>
public bool Success { get; set; }
/// <summary>
/// 返回数据
/// </summary>
public T Data { get; set; }
/// <summary>
/// 提示信息
/// </summary>
public string Message { get; set; }
/// <summary>
/// 附加数据
/// </summary>
public object Extras { get; set; }
/// <summary>
/// 时间戳
/// </summary>
public long Timestamp { get; set; }
}
2.2 各字段含义详解
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| Code | int? | 是 | HTTP 状态码或自定义业务状态码 |
| Success | bool | 是 | 请求是否成功,true 表示成功 |
| Data | T | 否 | 返回的业务数据,泛型支持任意类型 |
| Message | string | 否 | 返回的提示消息,成功或失败信息 |
| Extras | object | 否 | 额外的附加数据,如调试信息、链路追踪ID等 |
| Timestamp | long | 是 | 响应时间戳(Unix 毫秒级) |
2.3 成功与失败返回示例
成功返回的 JSON 结构:
{
"code": 200,
"success": true,
"data": {
"id": 1,
"name": "张三",
"email": "zhangsan@example.com"
},
"message": "操作成功",
"extras": null,
"timestamp": 1700000000000
}
失败返回的 JSON 结构:
{
"code": 400,
"success": false,
"data": null,
"message": "用户名不能为空",
"extras": {
"traceId": "abc-123-def-456"
},
"timestamp": 1700000000000
}
三、Furion 规范化结果提供器
3.1 IUnifyResultProvider 接口
Furion 通过 IUnifyResultProvider 接口定义了规范化结果的行为契约:
using Furion.UnifyResult;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
/// <summary>
/// 自定义规范化结果提供器
/// </summary>
[UnifyModel(typeof(RESTfulResult<>))]
public class CustomUnifyResultProvider : IUnifyResultProvider
{
/// <summary>
/// 成功返回处理
/// </summary>
public IActionResult OnSucceeded(ActionExecutedContext context, object data)
{
return new JsonResult(new RESTfulResult<object>
{
Code = StatusCodes.Status200OK,
Success = true,
Data = data,
Message = "操作成功",
Extras = UnifyContext.Take(),
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
});
}
/// <summary>
/// 验证失败返回处理
/// </summary>
public IActionResult OnValidateFailed(ActionExecutingContext context,
ValidationMetadata metadata)
{
return new JsonResult(new RESTfulResult<object>
{
Code = StatusCodes.Status400BadRequest,
Success = false,
Data = null,
Message = metadata.Message,
Extras = metadata.ValidationResult,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
});
}
/// <summary>
/// 异常返回处理
/// </summary>
public IActionResult OnException(ExceptionContext context, ExceptionMetadata metadata)
{
return new JsonResult(new RESTfulResult<object>
{
Code = metadata.StatusCode,
Success = false,
Data = null,
Message = metadata.Errors.ToString(),
Extras = null,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
});
}
/// <summary>
/// 状态码拦截处理(如 401、403、404 等)
/// </summary>
public async Task OnResponseStatusCodes(HttpContext context,
int statusCode, UnifyResultSettingsOptions unifyResultSettings)
{
switch (statusCode)
{
case StatusCodes.Status401Unauthorized:
await context.Response.WriteAsJsonAsync(new RESTfulResult<object>
{
Code = statusCode,
Success = false,
Data = null,
Message = "未经授权,请先登录",
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
});
break;
case StatusCodes.Status403Forbidden:
await context.Response.WriteAsJsonAsync(new RESTfulResult<object>
{
Code = statusCode,
Success = false,
Data = null,
Message = "权限不足,拒绝访问",
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
});
break;
case StatusCodes.Status404NotFound:
await context.Response.WriteAsJsonAsync(new RESTfulResult<object>
{
Code = statusCode,
Success = false,
Data = null,
Message = "资源不存在",
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
});
break;
}
}
}
3.2 注册规范化结果服务
在 Program.cs 或 Startup.cs 中注册:
var builder = WebApplication.CreateBuilder(args);
// 添加控制器并启用规范化结果
builder.Services.AddControllers()
.AddUnifyResult<CustomUnifyResultProvider>();
// 或使用默认的规范化结果提供器
builder.Services.AddControllers()
.AddUnifyResult();
var app = builder.Build();
// 启用规范化结果状态码拦截中间件
app.UseUnifyResultStatusCodes();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
四、成功返回与失败返回
4.1 控制器中的使用
using Furion.DynamicApiController;
using Microsoft.AspNetCore.Mvc;
/// <summary>
/// 用户管理接口
/// </summary>
[ApiDescriptionSettings(Name = "User")]
public class UserAppService : IDynamicApiController
{
private readonly IUserRepository _userRepository;
public UserAppService(IUserRepository userRepository)
{
_userRepository = userRepository;
}
/// <summary>
/// 获取用户详情 - 成功返回
/// </summary>
/// <param name="id">用户ID</param>
/// <returns>用户信息</returns>
public async Task<UserDto> GetUser(int id)
{
var user = await _userRepository.FindAsync(id);
if (user == null)
{
throw Oops.Oh("用户不存在");
}
return user.Adapt<UserDto>();
}
/// <summary>
/// 创建用户 - 返回新建的用户
/// </summary>
public async Task<UserDto> CreateUser(CreateUserInput input)
{
var user = input.Adapt<User>();
var newUser = await _userRepository.InsertAsync(user);
return newUser.Entity.Adapt<UserDto>();
}
/// <summary>
/// 删除用户 - 无数据返回
/// </summary>
public async Task DeleteUser(int id)
{
var user = await _userRepository.FindAsync(id);
if (user == null)
{
throw Oops.Oh("用户不存在");
}
await _userRepository.DeleteAsync(user);
}
}
4.2 手动控制返回结果
在某些场景下需要手动控制返回结果:
/// <summary>
/// 手动设置返回附加数据
/// </summary>
public async Task<UserDto> GetUserWithExtras(int id)
{
var user = await _userRepository.FindAsync(id);
// 设置附加数据,会被 Extras 字段捕获
UnifyContext.Fill(new
{
RequestTime = DateTime.Now,
ServerNode = "Node-01"
});
return user.Adapt<UserDto>();
}
/// <summary>
/// 跳过规范化处理,直接返回原始数据
/// </summary>
[NonUnify] // 标记不进行规范化处理
public IActionResult DownloadFile(string fileName)
{
var fileBytes = System.IO.File.ReadAllBytes($"uploads/{fileName}");
return File(fileBytes, "application/octet-stream", fileName);
}
五、异常统一处理与返回格式
5.1 使用 Oops 抛出业务异常
Furion 提供了友好的异常抛出方式:
/// <summary>
/// 业务异常抛出示例
/// </summary>
public class OrderAppService : IDynamicApiController
{
/// <summary>
/// 创建订单
/// </summary>
public async Task<OrderDto> CreateOrder(CreateOrderInput input)
{
// 简单异常
if (input.Amount <= 0)
{
throw Oops.Oh("订单金额必须大于零");
}
// 带错误码的异常
var product = await _productRepository.FindAsync(input.ProductId);
if (product == null)
{
throw Oops.Oh(ErrorCodes.ProductNotFound);
}
// 库存检查
if (product.Stock < input.Quantity)
{
throw Oops.Oh("库存不足,当前库存:{0},请求数量:{1}",
product.Stock, input.Quantity);
}
// 正常业务逻辑
var order = new Order
{
ProductId = input.ProductId,
Quantity = input.Quantity,
Amount = product.Price * input.Quantity
};
await _orderRepository.InsertAsync(order);
return order.Adapt<OrderDto>();
}
}
5.2 定义错误码枚举
using Furion.FriendlyException;
/// <summary>
/// 业务错误码定义
/// </summary>
[ErrorCodeType]
public enum ErrorCodes
{
/// <summary>
/// 用户不存在
/// </summary>
[ErrorCodeItemMetadata("用户不存在")]
UserNotFound,
/// <summary>
/// 商品不存在
/// </summary>
[ErrorCodeItemMetadata("商品不存在,商品ID:{0}")]
ProductNotFound,
/// <summary>
/// 库存不足
/// </summary>
[ErrorCodeItemMetadata("库存不足,当前库存:{0}")]
InsufficientStock,
/// <summary>
/// 订单已取消
/// </summary>
[ErrorCodeItemMetadata("订单已取消,无法重复操作")]
OrderCancelled,
/// <summary>
/// 权限不足
/// </summary>
[ErrorCodeItemMetadata("您没有权限执行此操作")]
PermissionDenied
}
5.3 全局异常处理配置
// 在 Program.cs 中配置
builder.Services.AddControllers()
.AddUnifyResult()
.AddFriendlyException(options =>
{
// 配置是否隐藏异常堆栈信息(生产环境建议设置为 true)
options.HideErrorCode = false;
// 自定义异常处理
options.GlobalExceptionHandler = (context, exception) =>
{
// 记录异常日志
var logger = context.RequestServices
.GetRequiredService<ILogger<Program>>();
logger.LogError(exception, "全局异常捕获");
};
});
六、数据验证失败的统一返回
6.1 数据验证与规范化结合
using System.ComponentModel.DataAnnotations;
using Furion.DataValidation;
/// <summary>
/// 创建用户输入模型
/// </summary>
public class CreateUserInput
{
/// <summary>
/// 用户名
/// </summary>
[Required(ErrorMessage = "用户名不能为空")]
[MinLength(2, ErrorMessage = "用户名至少2个字符")]
[MaxLength(20, ErrorMessage = "用户名最多20个字符")]
public string UserName { get; set; }
/// <summary>
/// 邮箱
/// </summary>
[Required(ErrorMessage = "邮箱不能为空")]
[EmailAddress(ErrorMessage = "邮箱格式不正确")]
public string Email { get; set; }
/// <summary>
/// 手机号
/// </summary>
[Required(ErrorMessage = "手机号不能为空")]
[DataValidation(ValidationTypes.PhoneNumber, ErrorMessage = "手机号格式不正确")]
public string Phone { get; set; }
/// <summary>
/// 年龄
/// </summary>
[Range(1, 150, ErrorMessage = "年龄必须在1-150之间")]
public int Age { get; set; }
}
验证失败时的统一返回格式:
{
"code": 400,
"success": false,
"data": null,
"message": "数据验证失败",
"extras": {
"UserName": ["用户名不能为空"],
"Email": ["邮箱格式不正确"]
},
"timestamp": 1700000000000
}
6.2 自定义验证特性与统一返回
/// <summary>
/// 自定义验证特性 - 身份证号码
/// </summary>
public class IdCardAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
if (value == null) return ValidationResult.Success;
var idCard = value.ToString();
if (idCard.Length != 18)
{
return new ValidationResult("身份证号码必须为18位");
}
// 验证校验位逻辑
if (!ValidateCheckDigit(idCard))
{
return new ValidationResult("身份证号码校验位不正确");
}
return ValidationResult.Success;
}
private bool ValidateCheckDigit(string idCard)
{
int[] weights = { 7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2 };
char[] checkChars = { '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];
}
return char.ToUpper(idCard[17]) == checkChars[sum % 11];
}
}
七、自定义状态码映射
7.1 业务状态码与 HTTP 状态码映射
/// <summary>
/// 自定义状态码映射
/// </summary>
public class CustomStatusCodeMapper
{
/// <summary>
/// 业务错误码到 HTTP 状态码的映射表
/// </summary>
private static readonly Dictionary<int, int> StatusCodeMap = new()
{
{ 1001, StatusCodes.Status400BadRequest }, // 参数错误
{ 1002, StatusCodes.Status404NotFound }, // 资源不存在
{ 1003, StatusCodes.Status409Conflict }, // 资源冲突
{ 2001, StatusCodes.Status401Unauthorized }, // 未认证
{ 2002, StatusCodes.Status403Forbidden }, // 未授权
{ 3001, StatusCodes.Status500InternalServerError }, // 服务器错误
{ 3002, StatusCodes.Status503ServiceUnavailable }, // 服务不可用
};
public static int GetHttpStatusCode(int businessCode)
{
return StatusCodeMap.TryGetValue(businessCode, out var httpCode)
? httpCode
: StatusCodes.Status200OK;
}
}
7.2 状态码枚举定义
/// <summary>
/// 业务状态码定义
/// </summary>
public enum BusinessCode
{
/// <summary>
/// 操作成功
/// </summary>
Success = 0,
/// <summary>
/// 操作失败
/// </summary>
Fail = -1,
/// <summary>
/// 参数验证失败
/// </summary>
ValidationError = 1001,
/// <summary>
/// 资源不存在
/// </summary>
NotFound = 1002,
/// <summary>
/// 未登录
/// </summary>
Unauthorized = 2001,
/// <summary>
/// 权限不足
/// </summary>
Forbidden = 2002,
/// <summary>
/// 服务器内部错误
/// </summary>
ServerError = 3001,
/// <summary>
/// 频率限制
/// </summary>
RateLimited = 4001
}
八、Swagger 集成与规范化文档
8.1 Swagger 配置规范化返回类型
// Program.cs 中配置 Swagger 支持规范化返回
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "我的 API",
Version = "v1",
Description = "基于 Furion 的规范化接口文档"
});
// 添加通用响应描述
options.OperationFilter<UnifyResultResponseFilter>();
});
/// <summary>
/// Swagger 规范化返回响应过滤器
/// </summary>
public class UnifyResultResponseFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
// 检查是否标记了 NonUnify
var hasNonUnify = context.MethodInfo
.GetCustomAttribute<NonUnifyAttribute>() != null;
if (hasNonUnify) return;
// 添加统一的错误响应描述
operation.Responses.TryAdd("400", new OpenApiResponse
{
Description = "请求参数验证失败"
});
operation.Responses.TryAdd("401", new OpenApiResponse
{
Description = "未授权,请先登录"
});
operation.Responses.TryAdd("500", new OpenApiResponse
{
Description = "服务器内部错误"
});
}
}
8.2 接口文档注释规范
/// <summary>
/// 用户管理接口
/// </summary>
[ApiDescriptionSettings("User", Name = "User", Order = 100)]
public class UserAppService : IDynamicApiController
{
/// <summary>
/// 获取用户列表
/// </summary>
/// <remarks>
/// 支持分页查询和关键字搜索。
///
/// 请求示例:
///
/// GET /api/user/list?page=1&pageSize=20&keyword=张三
///
/// </remarks>
/// <param name="input">查询参数</param>
/// <returns>分页用户列表</returns>
/// <response code="200">返回用户列表</response>
/// <response code="401">未授权</response>
[ProducesResponseType(typeof(RESTfulResult<PagedList<UserDto>>), 200)]
public async Task<PagedList<UserDto>> GetUserList([FromQuery] UserQueryInput input)
{
return await _userRepository.AsQueryable()
.WhereIf(!string.IsNullOrEmpty(input.Keyword),
u => u.UserName.Contains(input.Keyword))
.OrderByDescending(u => u.CreatedTime)
.ToPagedListAsync(input.Page, input.PageSize);
}
}
九、分页数据统一返回
9.1 分页模型定义
/// <summary>
/// 分页请求基类
/// </summary>
public class PagedQueryInput
{
/// <summary>
/// 当前页码(从1开始)
/// </summary>
[Range(1, int.MaxValue, ErrorMessage = "页码必须大于0")]
public int Page { get; set; } = 1;
/// <summary>
/// 每页数量
/// </summary>
[Range(1, 100, ErrorMessage = "每页数量必须在1-100之间")]
public int PageSize { get; set; } = 20;
/// <summary>
/// 搜索关键字
/// </summary>
public string Keyword { get; set; }
}
/// <summary>
/// 分页返回结果
/// </summary>
public class PagedResult<T>
{
/// <summary>
/// 当前页数据
/// </summary>
public List<T> Items { get; set; }
/// <summary>
/// 总记录数
/// </summary>
public int TotalCount { get; set; }
/// <summary>
/// 总页数
/// </summary>
public int TotalPages { get; set; }
/// <summary>
/// 当前页码
/// </summary>
public int Page { get; set; }
/// <summary>
/// 每页数量
/// </summary>
public int PageSize { get; set; }
/// <summary>
/// 是否有上一页
/// </summary>
public bool HasPrevPage => Page > 1;
/// <summary>
/// 是否有下一页
/// </summary>
public bool HasNextPage => Page < TotalPages;
}
9.2 分页数据统一返回格式
{
"code": 200,
"success": true,
"data": {
"items": [
{ "id": 1, "name": "张三" },
{ "id": 2, "name": "李四" }
],
"totalCount": 100,
"totalPages": 5,
"page": 1,
"pageSize": 20,
"hasPrevPage": false,
"hasNextPage": true
},
"message": "操作成功",
"extras": null,
"timestamp": 1700000000000
}
9.3 Furion 分页扩展
/// <summary>
/// 分页查询扩展方法
/// </summary>
public static class PagedExtensions
{
/// <summary>
/// 转换为统一分页结果
/// </summary>
public static async Task<PagedResult<TResult>> ToPagedResultAsync<TSource, TResult>(
this IQueryable<TSource> query,
int page,
int pageSize,
Expression<Func<TSource, TResult>> selector)
{
var totalCount = await query.CountAsync();
var items = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.Select(selector)
.ToListAsync();
return new PagedResult<TResult>
{
Items = items,
TotalCount = totalCount,
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize),
Page = page,
PageSize = pageSize
};
}
}
十、RESTful API 设计规范
10.1 资源命名规范
| 规范 | 正确示例 | 错误示例 | 说明 |
|---|---|---|---|
| 使用名词复数 | /api/users |
/api/getUsers |
资源使用名词表示 |
| 使用小写字母 | /api/users |
/api/Users |
URL 路径使用小写 |
| 使用连字符分隔 | /api/user-roles |
/api/userRoles |
多个单词用连字符 |
| 嵌套资源 | /api/users/1/orders |
/api/getUserOrders?id=1 |
体现资源层级关系 |
| 避免深层嵌套 | /api/orders?userId=1 |
/api/users/1/orders/2/items/3 |
最多两层嵌套 |
10.2 HTTP 方法语义
| HTTP 方法 | 用途 | 幂等性 | 示例 |
|---|---|---|---|
| GET | 获取资源 | 是 | GET /api/users/1 |
| POST | 创建资源 | 否 | POST /api/users |
| PUT | 全量更新 | 是 | PUT /api/users/1 |
| PATCH | 部分更新 | 否 | PATCH /api/users/1 |
| DELETE | 删除资源 | 是 | DELETE /api/users/1 |
10.3 Furion 动态 API 中的 RESTful 实现
/// <summary>
/// 遵循 RESTful 规范的动态 API
/// </summary>
[ApiDescriptionSettings(Name = "User")]
public class UserAppService : IDynamicApiController
{
private readonly IRepository<User> _repository;
public UserAppService(IRepository<User> repository)
{
_repository = repository;
}
/// <summary>
/// GET /api/user - 获取用户列表
/// </summary>
public async Task<PagedResult<UserDto>> GetList([FromQuery] UserQueryInput input)
{
return await _repository.AsQueryable()
.WhereIf(!string.IsNullOrEmpty(input.Keyword),
u => u.UserName.Contains(input.Keyword))
.ToPagedResultAsync(input.Page, input.PageSize, u => u.Adapt<UserDto>());
}
/// <summary>
/// GET /api/user/{id} - 获取用户详情
/// </summary>
public async Task<UserDto> Get(int id)
{
var user = await _repository.FindOrDefaultAsync(id)
?? throw Oops.Oh(ErrorCodes.UserNotFound);
return user.Adapt<UserDto>();
}
/// <summary>
/// POST /api/user - 创建用户
/// </summary>
public async Task<UserDto> Post(CreateUserInput input)
{
var user = input.Adapt<User>();
var newUser = await _repository.InsertAsync(user);
return newUser.Entity.Adapt<UserDto>();
}
/// <summary>
/// PUT /api/user - 更新用户
/// </summary>
public async Task Put(UpdateUserInput input)
{
var user = await _repository.FindOrDefaultAsync(input.Id)
?? throw Oops.Oh(ErrorCodes.UserNotFound);
input.Adapt(user);
await _repository.UpdateAsync(user);
}
/// <summary>
/// DELETE /api/user/{id} - 删除用户
/// </summary>
public async Task Delete(int id)
{
var user = await _repository.FindOrDefaultAsync(id)
?? throw Oops.Oh(ErrorCodes.UserNotFound);
await _repository.DeleteAsync(user);
}
}
十一、接口版本控制
11.1 URL 路径版本控制
/// <summary>
/// V1 版本用户接口
/// </summary>
[ApiDescriptionSettings(Name = "User", Version = "v1")]
public class UserV1AppService : IDynamicApiController
{
/// <summary>
/// GET /api/v1/user/{id}
/// </summary>
public async Task<UserDto> Get(int id)
{
// V1 版本逻辑
return new UserDto { Id = id, Name = "V1用户" };
}
}
/// <summary>
/// V2 版本用户接口(增强版)
/// </summary>
[ApiDescriptionSettings(Name = "User", Version = "v2")]
public class UserV2AppService : IDynamicApiController
{
/// <summary>
/// GET /api/v2/user/{id}
/// </summary>
public async Task<UserDetailDto> Get(int id)
{
// V2 版本逻辑,返回更多字段
return new UserDetailDto
{
Id = id,
Name = "V2用户",
Department = "技术部",
LastLoginTime = DateTime.Now
};
}
}
11.2 请求头版本控制
// 配置基于 Header 的版本控制
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
options.ApiVersionReader = new HeaderApiVersionReader("X-Api-Version");
});
十二、规范化接口最佳实践
12.1 最佳实践清单
| 实践项 | 建议 | 原因 |
|---|---|---|
| 统一返回格式 | 所有接口使用 RESTfulResult | 前端易于统一处理 |
| 错误码管理 | 使用枚举集中管理 | 便于维护和文档化 |
| 异常处理 | 使用 Oops.Oh() | 自动转为规范化返回 |
| 数据验证 | 使用 DataAnnotation | 验证失败自动规范化 |
| 文件下载 | 使用 [NonUnify] | 非 JSON 响应跳过规范化 |
| 分页查询 | 统一分页模型 | 前端分页组件通用 |
| 版本控制 | URL 路径方式 | 最直观、最易于管理 |
| 文档维护 | XML 注释 + Swagger | 接口自文档化 |
12.2 完整项目配置示例
// Program.cs - 完整的规范化接口配置
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddControllers()
.AddUnifyResult<CustomUnifyResultProvider>()
.AddFriendlyException()
.AddDataValidation()
.AddJsonOptions(options =>
{
// 配置 JSON 序列化选项
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.JsonSerializerOptions.DefaultIgnoreCondition =
JsonIgnoreCondition.WhenWritingNull;
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
options.JsonSerializerOptions.Converters
.Add(new DateTimeJsonConverter("yyyy-MM-dd HH:mm:ss"));
});
// Swagger 配置
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "企业管理系统 API",
Version = "v1",
Description = "基于 Furion 框架的企业管理系统接口文档",
Contact = new OpenApiContact
{
Name = "技术团队",
Email = "dev@example.com"
}
});
// 加载 XML 注释文件
var xmlFiles = Directory.GetFiles(AppContext.BaseDirectory, "*.xml");
foreach (var xmlFile in xmlFiles)
{
options.IncludeXmlComments(xmlFile, true);
}
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseUnifyResultStatusCodes();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
12.3 DateTime 自定义 JSON 转换器
/// <summary>
/// DateTime 自定义 JSON 转换器
/// </summary>
public class DateTimeJsonConverter : JsonConverter<DateTime>
{
private readonly string _format;
public DateTimeJsonConverter(string format = "yyyy-MM-dd HH:mm:ss")
{
_format = format;
}
public override DateTime Read(ref Utf8JsonReader reader,
Type typeToConvert, JsonSerializerOptions options)
{
return DateTime.Parse(reader.GetString()!);
}
public override void Write(Utf8JsonWriter writer,
DateTime value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString(_format));
}
}
通过本章的学习,你应该已经掌握了 Furion 框架中规范化接口与统一返回的完整方案。统一的返回格式不仅能提升团队协作效率,还能为前端开发提供一致的数据处理体验。在下一章中,我们将深入学习中间件与过滤器的使用。