功能管理:基于 ABP 的 Feature Management 实现动态开关

发布于:2025-06-04 ⋅ 阅读:(23) ⋅ 点赞:(0)

🚀 功能管理:基于 ABP 的 Feature Management 实现动态开关



📚 一、背景分析

在 SaaS 场景中,业务常常要求:

  • 🌐 不同租户具备不同功能开关
  • 🔀 根据套餐或版本灰度发布新功能
  • 🎯 控制功能粒度细化、灵活

ABP 的 Feature Management 模块内建对租户、主机、版本多级别支持,配合后台 UI 可视化管理界面,让开发者无需侵入业务逻辑即可控制功能可用性和灰度发布。


🧩 二、核心功能设计

2.1 定义 Feature 常量与分组

// 文件:MyApp.Domain.Shared/Feature/MyAppFeatures.cs
namespace MyApp.FeatureManagement
{
    /// <summary>
    /// 功能标识常量
    /// </summary>
    public static class MyAppFeatures
    {
        // 布尔型开关:控制是否启用 PDF 报表功能
        public const string EnablePdfReport = "MyApp.EnablePdfReport";

        // 数值型限额:导出功能限额
        public const string ExportLimit = "MyApp.ExportLimit";
    }
}

2.2 实现 FeatureDefinitionProvider 🛠️

将以下类放在 MyApp.Domain.Shared 或其他 ABP 扫描范围内的项目中,框架会自动发现并加载。

// 文件:MyApp.Domain.Shared/Feature/MyAppFeatureDefinitionProvider.cs
using Volo.Abp.FeatureManagement;
using Volo.Abp.FeatureManagement.Definitions;
using Volo.Abp.Localization;
using Volo.Abp.Validation.StringValues;

namespace MyApp.FeatureManagement
{
    /// <summary>
    /// 定义 MyApp 相关的 Feature
    /// </summary>
    public class MyAppFeatureDefinitionProvider : FeatureDefinitionProvider
    {
        public override void Define(IFeatureDefinitionContext context)
        {
            // 将所有 MyApp 下的 Feature 放到同一分组
            var group = context.AddGroup(
                "MyApp",
                LocalizableString.Create<MyAppResource>("MyApp")
            );

            // 布尔型开关,仅允许 true/false
            group.AddFeature(
                MyAppFeatures.EnablePdfReport,
                defaultValue: "false",
                displayName: LocalizableString.Create<MyAppResource>("EnablePdfReport"),
                valueType: new ToggleStringValueType()
            );

            // 自由文本型,通过 NumericValueValidator 限制数值范围 1~10000
            group.AddFeature(
                MyAppFeatures.ExportLimit,
                defaultValue: "100",
                displayName: LocalizableString.Create<MyAppResource>("ExportLimit"),
                valueType: new FreeTextStringValueType(
                    new NumericValueValidator(1, 10000)
                )
            );
        }
    }
}

ℹ️ 说明

  • 在 ABP v9.1.3 中,只要将 FeatureDefinitionProvider 放在被框架扫描的项目(如 Domain.Shared),就会自动注册。
  • 若需要集中管理加载顺序,可在应用模块中显式通过 AbpFeatureManagementOptions.DefinitionProviders.Add<>() 注册。

2.3 注册到模块 ⚙️

如果您希望显式手动注册 FeatureDefinitionProvider,可在模块中添加以下配置;否则框架会自动扫描加载,无需再写这段。

// 文件:MyApp.Application/MyAppApplicationModule.cs
using Volo.Abp.Modules;
using Volo.Abp.FeatureManagement;

namespace MyApp
{
    [DependsOn(
        typeof(MyAppDomainSharedModule),
        typeof(AbpFeatureManagementDomainModule)
    )]
    public class MyAppApplicationModule : AbpModule
    {
        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            Configure<AbpFeatureManagementOptions>(options =>
            {
                // 可选:显式添加定义提供者
                // options.DefinitionProviders.Add<MyAppFeatureDefinitionProvider>();
            });
        }
    }
}
2.3.1 ABP 特性注册流程图
启动应用
框架扫描 FeatureDefinitionProvider
将特性信息加载到数据库
启动完成,UI 与 API 可用

2.4 使用 [RequiresFeature] 控制访问 🔒

以下示例演示如何在 Controller 上使用 [RequiresFeature],仅当租户启用对应功能时才允许访问;否则返回 HTTP 403。

// 文件:MyApp.Web/Controllers/ReportController.cs
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.Features;

namespace MyApp.Web.Controllers
{
    [Route("api/report")]
    public class ReportController : AbpController
    {
        // 仅当租户启用 EnablePdfReport 时,才能访问此接口;否则返回 403
        [RequiresFeature(MyAppFeatures.EnablePdfReport)]
        [HttpGet("pdf")]
        public async Task<IActionResult> GeneratePdfReportAsync()
        {
            // 报表生成逻辑
            await Task.CompletedTask;
            return Ok("📄 PDF 报表已生成");
        }
    }
}

📝

  • [RequiresFeature] 仅在被依赖注入容器管理的 Controller 或 ApplicationService 上生效;若在普通类或非 DI 管理的类方法上使用,则不会触发拦截 citeturn1search2。
  • 被拦截后会返回 HTTP 403,且消息中会说明“Feature 未启用”。

2.5 后台 UI 支持 🖥️

在 Web 模块中,需要在模块定义类上添加对 Feature 管理相关模块的依赖,以便后台 UI 能正确加载并渲染功能管理页面。

// 文件:MyApp.Web/MyAppWebModule.cs
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.Modularity;
using Volo.Abp.FeatureManagement;
using Volo.Abp.FeatureManagement.Web;

namespace MyApp.Web
{
    [DependsOn(
        typeof(MyAppApplicationModule),
        typeof(AbpAspNetCoreMvcModule),
        typeof(AbpFeatureManagementApplicationModule),
        typeof(AbpFeatureManagementHttpApiModule),
        typeof(AbpFeatureManagementWebModule)
    )]
    public class MyAppWebModule : AbpModule
    {
        // 如果想手动注册 Provider,可在 ConfigureServices 中补充
        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            // 可选:显式添加 MyAppFeatureDefinitionProvider
            // Configure<AbpFeatureManagementOptions>(options =>
            // {
            //     options.DefinitionProviders.Add<MyAppFeatureDefinitionProvider>();
            // });
        }
    }
}
2.5.1 React 前端路由示例
// 文件:src/routes.tsx(React 示例)
import { FeatureManagement } from '@abp/feature-management'; // React 官方包
import { AuthGuard } from '@abp/abp-ui-react';
import HomePage from './pages/HomePage';

export const routes = [
  {
    path: '/',
    element: <HomePage />,
  },
  {
    path: '/feature-management',
    element: <FeatureManagement />,
    // 只有拥有 AbpFeatureManagement.FeatureManagement.Default 权限才可访问
    canActivate: [AuthGuard],
    data: { requiredPolicy: 'AbpFeatureManagement.FeatureManagement.Default' },
  },
  // …其他路由
];

🔔 提示

  1. React 端需安装 @abp/feature-management@abp/react-components
  2. requiredPolicy 必须与后端在 PermissionDefinitionProvider 中定义的权限名称一致,否则会导致页面或菜单无法显示 citeturn1search2。
2.5.2 Angular 前端路由示例
// 文件:app/app-routing.module.ts(Angular 示例)
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { FeatureManagementComponent } from '@abp/ng.feature-management';
import { NgxPermissionsGuard } from 'ngx-permissions';

const routes: Routes = [
  {
    path: '',
    children: [
      {
        path: 'feature-management',
        component: FeatureManagementComponent,
        canActivate: [NgxPermissionsGuard],
        data: { permissions: { only: 'AbpFeatureManagement.FeatureManagement.Default' } },
      },
      // …其他路由
    ],
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

🔔 提示

  1. Angular 端需安装 @abp/ng.feature-managementngx-permissions 等依赖。
  2. permissions.only 必须与后端在 PermissionDefinitionProvider 中定义的权限名称一致,否则会导致页面或菜单无法显示 citeturn1search2。

🔍 三、实战示例

3.1 📘 场景一:PDF 报表开关

  • Tenant A:将 EnablePdfReport 设为 true
  • Tenant B:将 EnablePdfReport 保持 false

访问 /api/report/pdf 接口时:

  • 🎉 Tenant A 能够正常生成并返回报表
  • 🚫 Tenant B 则会被拦截并返回 HTTP 403

3.2 📕 场景二:导出限额控制

ApplicationService 中注入 IFeatureChecker 并获取数值型特性值,示例如下:

// 文件:MyApp.Application/ReportAppService.cs
using Volo.Abp.Application.Services;
using Volo.Abp.Features;
using Volo.Abp.Validation;
using Volo.Abp;
using Microsoft.Extensions.Logging;

namespace MyApp
{
    public class ReportAppService : ApplicationService
    {
        private readonly IFeatureChecker _featureChecker;
        private readonly ILogger<ReportAppService> _logger;

        public ReportAppService(
            IFeatureChecker featureChecker,
            ILogger<ReportAppService> logger)
        {
            _featureChecker = featureChecker;
            _logger = logger;
        }

        public async Task ExportAsync(int count)
        {
            int limit;
            try
            {
                // 泛型方法会自动将字符串值转换为 int,若非法则抛出 AbpValidationException
                limit = await _featureChecker.GetAsync<int>(MyAppFeatures.ExportLimit);
            }
            catch (AbpValidationException ex)
            {
                _logger.LogWarning(ex, "🚧 ExportLimit 非法,使用默认值 100。");
                limit = 100;
            }

            if (count > limit)
            {
                throw new BusinessException("❌ 超出导出限额");
            }

            // 导出逻辑
            await Task.CompletedTask;
        }
    }
}

ℹ️ 说明

  1. 若直接使用 GetOrNullAsync 返回 string,则需手动 int.TryParse 并处理异常;推荐使用泛型 GetAsync<int> 搭配 NumericValueValidator,由框架自动校验 citeturn1search2。
  2. 业务高峰期可通过 Feature 界面临时调整限额,无需重启服务,响应快速。

🔧 四、扩展内容

4.1 🌐 本地化资源支持

将资源文件放在 MyApp.Domain.Shared/Localization/MyAppResource.xml,示例如下:

<!-- 文件:MyApp.Domain.Shared/Localization/MyAppResource.xml -->
<localization xmlns="https://docs.abp.io/en/abp/latest/Localization/Model">
  <texts>
    <text name="MyApp" value="我的应用" />
    <text name="EnablePdfReport" value="启用 PDF 报表" />
    <text name="ExportLimit" value="导出限额" />
    <text name="Permission:FeatureManagement" value="功能管理" />
    <text name="Permission:FeatureManagement:Default" value="访问功能管理界面" />
  </texts>
</localization>

若需要多语言支持,可在同目录下添加 MyAppResource.en.xmlMyAppResource.zh-CN.xml 等对应文件;确保项目已在模块中启用本地化:

// 文件:MyApp.Domain.Shared/MyAppDomainSharedModule.cs
using Volo.Abp.Localization;
using Volo.Abp.Modularity;

namespace MyApp
{
    public class MyAppDomainSharedModule : AbpModule
    {
        public override void ConfigureServices\ServiceConfigurationContext context)
        {
            Configure<AbpLocalizationOptions>(options =>
            {
                options.Resources
                    .Get<MyAppResource>()
                    .AddBaseTypes(typeof(AbpValidationResource))
                    .AddVirtualJson("/Localization/MyApp");
            });
        }
    }
}

⚠️ 注意

  1. 目录结构必须与 AddVirtualJson("/Localization/MyApp") 中的路径保持一致。
  2. 若本地化资源文件放在不同位置,需要同步修改 AddVirtualJson 的参数。

4.2 🖥️ UI 模块接入说明

  • React 端

    1. 安装依赖:
      npm install @abp/feature-management @abp/abp-ui-react
      
    2. routes.tsx 中添加如下路由:
      import { FeatureManagement } from '@abp/feature-management';
      import { AuthGuard } from '@abp/abp-ui-react';
      import HomePage from './pages/HomePage';
      
      export const routes = [
        {
          path: '/',
          element: <HomePage />,
        },
        {
          path: '/feature-management',
          element: <FeatureManagement />,
          canActivate: [AuthGuard],
          data: { requiredPolicy: 'AbpFeatureManagement.FeatureManagement.Default' },
        },
        // …其他路由
      ];
      
  • Angular 端

    1. 安装依赖:
      npm install @abp/ng.feature-management ngx-permissions
      
    2. app-routing.module.ts 中添加如下路由:
      import { FeatureManagementComponent } from '@abp/ng.feature-management';
      import { NgxPermissionsGuard } from 'ngx-permissions';
      
      const routes: Routes = [
        // …其他路由
        {
          path: 'feature-management',
          component: FeatureManagementComponent,
          canActivate: [NgxPermissionsGuard],
          data: { permissions: { only: 'AbpFeatureManagement.FeatureManagement.Default' } },
        },
      ];
      

💡 提示

  • React 与 Angular 示例要区分清楚,避免包名或组件名混淆。
  • 确保前端依赖包版本与后端 ABP 版本兼容。

4.3 🎨 灰度流程图

EnablePdfReport == true
EnablePdfReport == false
ExportLimit 返回数值
count <= limit
count > limit
客户端请求 API
IFeatureChecker 检查功能
租户配置
正常执行业务逻辑(生成报表)
返回 403 Forbidden
根据限额判断是否可导出
继续执行导出
抛出 BusinessException

4.4 🧪 单元测试

在测试项目中,先通过 MyAppTestBase(ABP 提供的测试基类)或构造函数注入获取所需仓储与服务实例。例如:

// 文件:MyApp.Tests/FeatureManagementTests.cs
using Volo.Abp.FeatureManagement;
using Volo.Abp.Features;
using Volo.Abp.Testing;
using Xunit;

namespace MyApp.Tests
{
    public class FeatureManagementTests : MyAppTestBase
    {
        private readonly IFeatureValueRepository _featureValueRepository;
        private readonly ReportAppService _reportAppService;

        public FeatureManagementTests()
        {
            // 通过基类方法解析依赖
            _featureValueRepository = GetRequiredService<IFeatureValueRepository>();
            _reportAppService = GetRequiredService<ReportAppService>();
        }

        [Fact]
        public async Task GeneratePdfReport_ShouldThrow_WhenFeatureDisabled()
        {
            // 准备租户上下文,假设测试基类已创建默认租户
            var tenantId = CurrentTenant.Id ?? 1;

            using (CurrentTenant.Change(tenantId))
            {
                // 插入特性值:禁用 PDF 报表
                await _featureValueRepository.InsertAsync(new FeatureValue
                {
                    Name = MyAppFeatures.EnablePdfReport,
                    ProviderName = FeatureValueProviderName.Tenant, // ABP v9 中定义的常量
                    ProviderKey = tenantId.ToString(),
                    Value = "false"
                });
            }

            await Assert.ThrowsAsync<AbpAuthorizationException>(async () =>
            {
                await _reportAppService.GeneratePdfReportAsync();
            });
        }
    }
}

ℹ️ 说明

  1. 测试类继承自 MyAppTestBase 后,可以直接使用 GetRequiredService<T>() 获取 IFeatureValueRepositoryReportAppService 等。
  2. 确保测试环境中至少存在一个租户,否则 CurrentTenant.Id 可能为空。可以在测试初始化时创建一个租户并切换上下文。
  3. FeatureValueProviderName.Tenant 是 ABP v9 中提供的常量;也可直接使用 "Tenant"


网站公告

今日签到

点亮在社区的每一天
去签到