znlgis 博客

GIS开发与技术分享

第十六章:规范化接口与统一返回

一、规范化接口概述

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.csStartup.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&amp;pageSize=20&amp;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 框架中规范化接口与统一返回的完整方案。统一的返回格式不仅能提升团队协作效率,还能为前端开发提供一致的数据处理体验。在下一章中,我们将深入学习中间件与过滤器的使用。