深度探索.NET Feature Management功能開關的魔法

来源:https://www.cnblogs.com/ruipeng/p/18098211
-Advertisement-
Play Games

前言 .NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地添加、移除和管理功能。使用 Feature Management,開發人員可以根據不同用戶、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功 ...


前言

.NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地添加、移除和管理功能。使用 Feature Management,開發人員可以根據不同用戶、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功能,並根據需要快速調整和部署新功能。 Feature Management 還提供了一些方便的工具和 API,幫助開發人員更輕鬆地實現功能管理和控制。

安裝

  • .Net CLI
dotnet add package Microsoft.FeatureManagement.AspNetCore --version 4.0.0-preview2
  • Package Manager
NuGet\Install-Package Microsoft.FeatureManagement.AspNetCore -Version 4.0.0-preview2

或者 Vs Nuget 包管理 管理工具安裝等

依賴註入

.Net 功能管理器是通過框架的本機配置系統配置的,簡單來說只要是.Net 的配置系統支持的數據源都可以用做功能管理(FeatureManagement)的配置源

.NET 中的配置是使用一個或多個配置提供程式執行的。 配置提供程式使用各種配置源從鍵值對讀取配置數據:

  • 設置文件,例如 appsettings.json
  • 環境變數
  • Azure Key Vault
  • Azure 應用配置
  • 命令行參數
  • 已安裝或已創建的自定義提供程式
  • 目錄文件
  • 記憶體中的 .NET 對象
  • 第三方提供程式

.NET 中的配置提供程式

依賴註入:

service.AddFeatureManagement();

預設情況下,功能管理器從 .NET appsettings.json配置數據的 FeatureManagement Section 來獲取數據

  // Define feature flags in config file
  "FeatureManagement": {
    "sayHello": true, // On feature
    "todo": false // Off feature
  }

當然也可以自定義 Section

service.AddFeatureManagement(builder.Configuration.GetSection("CustomFeatureManagement"));
  // Define feature flags in config file
  "CustomFeatureManagement": {
    "sayHello": true, // On feature
    "todo": false // Off feature
  }

功能開關註冊成 Scoped

AddFeatureManagement 方法將特性管理服務作為單例添加到應用程式中,但有些情況下可能需要將特性管理服務添加為Scoped(作用域服務)。例如,我們可能希望使用 Scoped 以獲取上下文信息的功能過濾器。在這種情況下,應該使用 AddScopedFeatureManagement 方法, 這將確保功能管理服務(包括功能過濾器)被添加為 Scoped 服務。

//功能管理註冊 Scoped 作用域
service.AddScopedFeatureManagement();

功能管理的基本形式是檢查功能標誌是否已啟用,然後根據結果執行操作。這通過 IFeatureManagerIsEnabledAsync 方法來實現。

對我們上面的 FeatureManager 的配置來做一個驗證

  • sayhello 功能開關標誌測試
app.MapGet("/sayHello", async Task<IResult> ([FromServices] IFeatureManager manager, string name) =>
{
    if (await manager.IsEnabledAsync("sayHello"))
    {
        return TypedResults.Ok($"hello {name}");
    }
    return TypedResults.NotFound();

}).WithSummary("sayHello");

調用介面查看一下結果,在配置中我們的sayHello設置為true

image

狀態碼為 200,返回信息"hello Ruipeng",符合預期,功能開啟正常。

  • todo 功能開關標誌測試
app.MapGet("/todo", async Task<IResult> ([FromServices] IFeatureManager manager) =>
{
    if (await manager.IsEnabledAsync("todo"))
    {
        return TypedResults.Ok($"todo is enabled !");
    }
    return TypedResults.NotFound();

}).WithSummary("todo");

調用介面查看一下結果,狀態碼 404,返回信息 Not Found,符合預期,功能未開啟。

image

上面的示例簡單講解了一下功能開關的使用,接下來深入瞭解功能開關的配置

功能開關的定義

功能開關的標誌由兩部分組成:名稱和用於啟用功能的過濾器列表。

功能過濾器(Feature filters)定義了功能應何時啟用的場景。在評估特性是開啟還是關閉時,會遍歷其功能過濾器列表,直到其中一個過濾器決定啟用該特性。如果一個過濾器都沒有標識改功能應該開啟,那此功能標誌是關閉的狀態。

內置過濾器

  • AlwaysOn: 總是開啟
  • PercentageFilter:根據百分比隨機啟用/禁用功能。這個過濾器允許您基於一個百分比值來決定功能被啟用的概率,提供了一種簡單而靈活的機制來控制特性的曝光範圍。
  • TimeWindowFilter:在預定義的時間視窗內啟用特性。這個過濾器允許您指定特性的開始和結束時間,確保特性只在特定的時間段內可用。這對於限時活動或測試場景非常有用。
  • TargetingFilter:(這個主要是在Azure 用為目標受眾啟用功能的分階段推出針對特定用戶或用戶組啟用特性。這個過濾器允許您根據用戶屬性或標識來啟用特性,例如基於用戶 ID、角色、地區等。此外,對於此過濾器,您還可以設置一個百分比值,以進一步控制特性在目標用戶中的啟用概率。

詳細信息可以參考註冊功能篩選器 Docs

過濾器的配置指南

需要註意的是在功能標誌名稱中禁止使用冒號:,這是為了遵循一定的命名規範,避免與現有的或未來的功能管理系統產生衝突或造成解析錯誤。在定義功能標誌名稱時,請確保使用合法和合適的字元組合,以確保系統的穩定性和可維護性。
功能使用 EnabledFor 屬性來定義它們的功能過濾器


AlwaysOn 過濾器

  // Define feature flags in config file
  "FeatureManagement": {
    //始終啟用該功能
    "featureAlwaysOn": {
      "EnabledFor": [
        {
          "Name": "AlwaysOn"
        }
      ]
    }
  }
app.MapGet("/featureAlwaysOn", async Task<IResult> (IFeatureManager manager) =>
{
    if (await manager.IsEnabledAsync("featureAlwaysOn"))
    {
        return TypedResults.Ok($"featureAlwaysOn is enabled !");
    }
    return TypedResults.NotFound();
}).WithSummary("featureAlwaysOn");

調用介面查看測試結果,返回 200,符合預期

image


TimeWindow 過濾器

  "FeatureManagement": {
    "featureTimeWindow": {
      "EnabledFor": [
        {
          "Name": "TimeWindow",
          "Parameters": {
            "Start": "2024-03-26 13:30:00",
            "End": "2024-03-27 13:30:00"
          }
        }
      ]
    }
  }

指定了一個名為 TimeWindow 的功能過濾器。這是一個可配置的功能過濾,具有 Parameters 屬性,配置了功能活動的開始和結束時間 。

app.MapGet("/featureTimeWindow", async Task<IResult> (IFeatureManager manager) =>
{
    if (await manager.IsEnabledAsync("featureTimeWindow"))
    {
        return TypedResults.Ok($"featureTimeWindow is enabled !");
    }
    return TypedResults.NotFound();
}).WithSummary("TimeWindow 過濾器測試");

調用介面測試:返回 200 符合預期

image


Percentage 過濾器
百分比過濾器(Percentage Filter)它根據指定的百分比值隨機啟用或禁用某個特性。這種過濾器允許您控制特性的曝光率,以便在不同的用戶群體中測試特性的效果,或者在逐步推廣新特性時控制其影響範圍。

  "FeatureManagement": {
    "featurePercentage": {
      "EnabledFor": [
        {
          "Name": "Percentage",
          "Parameters": {
            "Value": "50"
          }
        }
      ]
    }
  },

app.MapGet("/featurePercentage", async Task<IResult> (IFeatureManager manager) =>
{
    if (await manager.IsEnabledAsync("featurePercentage"))
    {
        return TypedResults.Ok($"featurePercentage is enabled !");
    }
    return TypedResults.NotFound();
}).WithSummary("Percentage 過濾器測試");

連續測兩次

第一次測試結果: 返回 200
image

第二次測試結果:返回 404
image

通過測試結果可以看出有百分之五十的幾率成功,符合預期。

RequirementType

功能標誌的 RequirementType 屬性用於確定在評估功能狀態時,過濾器應該使用任何(Any)還是全部(All)邏輯。如果未指定 RequirementType,則預設值為 Any

  • Any 表示只需一個過濾器評估為 true,特性就會被啟用。
  • All 表示每個過濾器都必須評估為 true,特性才會被啟用。
    RequirementTypeAll 會改變遍歷方式。首先,如果沒有過濾器,則功能將被禁用。然後,遍歷特性過濾器,直到其中一個過濾器決定應將功能禁用。如果沒有過濾器指示應禁用功能,則該功能將被視為已啟用。
  "FeatureManagement": {
    "featureRequirementTypeAll": {
      "RequirementType": "All",
      "EnabledFor": [
        {
          "Name": "TimeWindow",
          "Parameters": {
            "Start": "2024-03-27 13:00:00",
            "End": "2024-05-01 13:00:00"
          }
        },
        {
          "Name": "Percentage",
          "Parameters": {
            "Value": "50"
          }
        }
      ]
    }
  },
app.MapGet("/featureRequirementTypeAll", async Task<IResult> (IFeatureManager manager) =>
{
    if (await manager.IsEnabledAsync("featureRequirementTypeAll"))
    {
        return TypedResults.Ok($"featureRequirementTypeAll is enabled !");
    }
    return TypedResults.NotFound();
}).WithSummary("RequirementTypeAll 多過濾器測試");

上面的實例設置為 all 之後此功能標誌的過濾器列表必須全部符合要求才能調用成功。

比如上面我設置的開始日期是2024-03-27 13:00:00當前時間小於這個日期
image

無論調用幾次還是還是 404,結果符合我們的預期。

自定義過濾器

要實現一個功能過濾器,必須要實現的是一個IFeatureFilter介面,介面包含了一個EvaluateAsync的方法。當功能標誌指定啟用該過濾器時,將調用 EvaluateAsync方法,如果方法返回的是true,則表示應該啟用功能。

定義一個中間件介面只對某個用戶組做開放,這個場景在 C 端的產品上比較常見,比如說部分功能的內測。

[FilterAlias("AuthenticatedGroup")]
public class AuthenticatedGroupFilter : IFeatureFilter, IFeatureFilterMetadata, IFilterParametersBinder
{
    public object BindParameters(IConfiguration parameters)
    {
        return parameters.Get<GroupSetting>() ?? new GroupSetting();
    }

    public Task<bool> EvaluateAsync(FeatureFilterEvaluationContext featureFilterContext)
    {
        GroupSetting filterSettings = ((GroupSetting)featureFilterContext.Settings) ?? ((GroupSetting)BindParameters(featureFilterContext.Parameters));
        // 假設您有一個方法來檢查用戶是否已通過身份驗證
        // 例如,這可能是一個從身份驗證服務或中間件中獲得的屬性或方法
        bool isAuthenticated = IsGroupAuthenticated(filterSettings);
        return Task.FromResult(isAuthenticated);
    }


    private bool IsGroupAuthenticated(GroupSetting groupSetting)
    {
        // 在這裡編寫您的身份驗證檢查邏輯
        // 這可能涉及到檢查HTTP請求的上下文、會話狀態、令牌等
        // 具體的實現將取決於您使用的身份驗證機制

        // 示例:返回一個硬編碼的值,表示用戶是否已通過身份驗證
        // 在實際應用中,您應該實現實際的檢查邏輯
        return true; // 或者 false,取決於用戶是否已通過身份驗證
    }
}

FilterAlias是定義過濾器的別名,我們在配置文件中指定時需要用別名,IFeatureFilter介面返回的信息決定功能是否啟用,IFeatureFilterMetadata是一個空的標記介面,用於評估功能狀態的特征過濾器的標記介面,IFilterParametersBinder 介面用於參數綁定。

  • json 配置
  "FeatureManagement": {
    "featureAuthencatedGroup": {
      "EnabledFor": [
        {
          "Name": "AuthenticatedGroup",
          "Parameters": {
            "Groups": [ "AdminGroup", "GroupOne" ]
          }
        }
      ]
    }
  }
  • 依賴註入
services.AddFeatureManagement()
    .AddFeatureFilter<AuthenticatedGroupFilter>();

調用 AddFeatureFilter 方法可把自定義的過濾器註冊到功能管理器中。

app.MapGet("/featureAuthencatedGroup", async Task<IResult> (IFeatureManager manager) =>
{
    if (await manager.IsEnabledAsync("featureAuthencatedGroup"))
    {
        return TypedResults.Ok($"featureAuthencatedGroup is enabled !");
    }
    return TypedResults.NotFound();
}).WithSummary("AuthencatedGroup 自定義過濾器測試");

測試一下,返回 200 ,符合預期
image

一個小 tips;如果多個過濾器有同一個別名是,可以用命名空間加別名的方式來定義唯一一個過濾器,例如,Microsoft.Percentage 是一個完全限定的別名,它明確指出了 Percentage過濾器位於 Microsoft 命名空間下

自定義開啟中間件

  "FeatureManagement": {
    "featureMiddleWare": {
      "EnabledFor": [
        {
          "Name": "Percentage",
          "Parameters": {
            "Value": "50"
          }
        }
      ]
    }
  }

自定義中間件

public class FeatureMiddleWare(RequestDelegate next)
{
    public async Task Invoke(HttpContext context)
    {
        Console.WriteLine("FeatureMiddleWare管道執行之前~");
        await next(context);
        Console.WriteLine("FeatureMiddleWare管道執行之後~");
    }
}

添加擴展方法

//測試中間件的功能開啟
app.UseMiddlewareForFeature<FeatureMiddleWare>("featureMiddleWare");

隨便調用一個介面測試一下,可以看到管道根據百分比觸發成功
image

通過上述調用,應用程式添加了一個中間件組件,只有在特性“featureMiddleWare”被啟用時才會出現在請求管道中。如果在運行時啟用/禁用特性,中間件管道可以動態更改。

這是建立在基於特性對整個應用程式進行分支的更通用能力之上。

app.UseForFeature(featureName, appBuilder =>
{
appBuilder.UseMiddleware<T>();
});

MinimalApis 集成

在我們的 MVC 或者 Razor Pages 中有如下方案來啟用功能的開關,不過多介紹大家可以官方瀏覽學習。

FeatureManagement-Dotnet

services.AddMvc(o =>
{
    o.Filters.AddForFeature<SomeMvcFilter>("FeatureX");
});
[FeatureGate("FeatureX")]
public class IndexModel : PageModel
{
    public void OnGet()
    {
    }
}

MinimalAps 中可以利用 endpoint filter來簡化公功能的開關,

  • 第一步創建最小 Api 的基類,所有的 MinimalApis 過濾器都要繼承它
public abstract class FeatureFlagEndpointFilter(IFeatureManager featureManager) : IEndpointFilter
{
    protected abstract string FeatureFlag { get; }

    private readonly IFeatureManager _featureManager = featureManager;

    public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        var isEnabled = await _featureManager.IsEnabledAsync(FeatureFlag);
        if (!isEnabled)
        {
            return TypedResults.NotFound();
        }
        return await next(context);
    }
}
  • 定義目標 Json 配置
  "FeatureManagement": {
    "featureUserApi": {
      "EnabledFor": [
        {
          "Name": "Percentage",
          "Parameters": {
            "Value": "50"
          }
        }
      ]
    }
  • 定義最小 Api 過濾器
public class UserApiFeatureFilter(IFeatureManager featureManager) : FeatureFlagEndpointFilter(featureManager)
{
    protected override string FeatureFlag => "featureUserApi";
}

  • 定義 Api 介面測試
//最小Api分組功能添加
{
    var userGroup = app.MapGroup("User").WithTags("User").AddEndpointFilter<UserApiFeatureFilter>(); ;

    userGroup.MapGet("/featureUserApi", IResult (IFeatureManager manager) =>
    {
        return TypedResults.Ok($"featureUserApi is enabled !");

    }).WithSummary("featureUserApi 最小Api過濾器測試");
}

調用測試,可以看出我們配置的百分比過濾器成功。

image

通過對 IEndpointFilter 的封裝藉助最小 ApiMapGroup 可以對一組相關的 Api 進行功能管理,簡化了我們一個個 Api 註冊。

最後

在本文中,我們深入探討了.NET Feature Management 庫的安裝、配置和使用方法,以及如何利用功能開關來動態管理應用程式的功能。以下是關鍵點的總結和提煉:

  • 安裝與依賴註入:通過.NET CLINuGet Package Manager 安裝等方式 Microsoft.FeatureManagement.AspNetCore 庫,併在應用程式中添加功能管理服務的依賴註入。

  • 功能定義與配置:通過.NET 的配置系統,在 appsettings.json 中定義功能標誌,指定功能的啟用和禁用狀態,以及可選的功能過濾器配置。

  • 自定義功能過濾器:實現 IFeatureFilter 介面來定義自定義功能過濾器,根據特定條件決定功能是否啟用,例如基於用戶組、時間視窗或百分比等條件。

  • 功能開關的使用:利用 IFeatureManagerIsEnabledAsync 方法檢查功能是否啟用,根據不同的功能狀態執行相應的邏輯,實現功能的動態控制。

  • RequirementType 設置:可以通過 RequirementType 屬性指定功能過濾器的邏輯要求,是 Any 還是 All,決定多個過濾器的組合邏輯。

  • 自定義中間件的動態切換:通過自定義功能過濾器和中間件,可以根據功能狀態動態調整請求管道,實現功能開關對中間件的控制。

  • 最小 API 集成:在 Minimal APIs 中,利用 IEndpointFilter 介面來簡化功能開關的應用,將功能管理應用到最小 API 的端點上,實現對一組相關 API 的功能管理。

通過以上總結和提煉,您可以更好地瞭解和應用.NET Feature Management 庫,實現靈活的功能管理和動態控制應用程式的功能。

有條件的富哥可以體驗一下在 Azure 應用程式配置中管理功能標誌

更多詳細的內容請瀏覽FeatureManagement-Dotnet

本文測試完整源代碼

本文來自博客園,作者:董瑞鵬,轉載請註明原文鏈接:https://www.cnblogs.com/ruipeng/p/18098211


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文介紹瞭如何快速搭建一個基於大型語言模型(LLM)的混元聊天應用。強調了開發速度的重要性,並指出了使用Streamlit這一工具的優勢,特別是對於不熟悉前端代碼的開發者來說,Streamlit提供了一種快速構建聊天應用的方法。 ...
  • 前言 aardio中有些經常使用的庫,換個項目總需要複製一下,還不便於修改。雖然可以直接把它放到aardio\lib目錄下,也是不便於共用給其他人使用。 最近偶然翻到編輯器里的工具->開發環境->擴展庫發佈工具,就想著可以像官方一樣,發佈自己的擴展庫,也便於分享給大家使用,最好能像官方擴展庫一樣線上 ...
  • 隨著汽車的普及和使用頻率的增加,車輛的維修保養成為了車主們經常需要面對的問題。為了提供更好的服務,挖數據平臺提供了一個維修保養記錄統計介面,讓用戶可以方便地查詢車輛的保養記錄和維修記錄。本文將對該介面進行詳細解析,並介紹其使用方法和應用場景。 首先,我們來看一下該介面的具體功能。該介面可以查詢車輛的 ...
  • 在使用Django等框架來操作MySQL時,實際上底層還是通過Python來操作的,首先需要安裝一個驅動程式,在Python3中,驅動程式有多種選擇,比如有pymysql以及mysqlclient等。使用pip命令安裝mysqlclient失敗應如何解決? 安裝的python版本說明 機器同時安裝了 ...
  • Csharper中的表達式樹 這節課來瞭解一下表示式樹是什麼? 在C#中,表達式樹是一種數據結構,它可以表示一些代碼塊,如Lambda表達式或查詢表達式。表達式樹使你能夠查看和操作數據,就像你可以查看和操作代碼一樣。它們通常用於創建動態查詢和解析表達式。 一、認識表達式樹 為什麼要這樣說?它和委托有 ...
  • 一、前言 這是一篇搭建許可權管理系統的系列文章。 隨著網路的發展,信息安全對應任何企業來說都越發的重要,而本系列文章將和大家一起一步一步搭建一個全新的許可權管理系統。 說明:由於搭建一個全新的項目過於繁瑣,所有作者將挑選核心代碼和核心思路進行分享。 二、技術選擇 三、開始設計 1、自主搭建vue前端和. ...
  • 在實際使用中,由於涉及到不同編程語言之間互相調用,導致C++ 中的OpenCV與C#中的OpenCvSharp 圖像數據在不同編程語言之間難以有效傳遞。在本文中我們將結合OpenCvSharp源碼實現原理,探究兩種數據之間的通信方式。 ...
  • 在 WPF 應用程式中,拖放操作是實現用戶交互的重要組成部分。通過拖放操作,用戶可以輕鬆地將數據從一個位置移動到另一個位置,或者將控制項從一個容器移動到另一個容器。然而,WPF 中預設的拖放操作可能並不是那麼好用。為瞭解決這個問題,我們可以自定義一個 Panel 來實現更簡單的拖拽操作。 自定義 Pa ...
一周排行
    -Advertisement-
    Play Games
  • 不廢話,直接代碼 private Stack<Action> actionStack = new Stack<Action>(); private void SetCellValues() { var worksheet = Globals.ThisAddIn.Application.ActiveS ...
  • OpenAPI 規範是用於描述 HTTP API 的標準。該標準允許開發人員定義 API 的形狀,這些 API 可以插入到客戶端生成器、伺服器生成器、測試工具、文檔等中。儘管該標準具有普遍性和普遍性,但 ASP.NET Core 在框架內預設不提供對 OpenAPI 的支持。 當前 ASP.NET ...
  • @DateTimeFormat 和 @JsonFormat 是 Spring 和 Jackson 中用於處理日期時間格式的註解,它們有不同的作用: @DateTimeFormat @DateTimeFormat 是 Spring 框架提供的註解,用於指定字元串如何轉換為日期時間類型,以及如何格式化日 ...
  • 一、背景說明 1.1 效果演示 用python開發的爬蟲採集軟體,可自動抓取抖音評論數據,並且含二級評論! 為什麼有了源碼還開發界面軟體呢?方便不懂編程代碼的小白用戶使用,無需安裝python、無需懂代碼,雙擊打開即用! 軟體界面截圖: 爬取結果截圖: 以上。 1.2 演示視頻 軟體運行演示視頻:見 ...
  • SpringBoot筆記 SpringBoot文檔 官網: https://spring.io/projects/spring-boot 學習文檔: https://docs.spring.io/spring-boot/docs/current/reference/html/ 線上API: http ...
  • 作為後端工程師,多數情況都是給別人提供介面,寫的好不好使你得重視起來。 最近我手頭一些活,需要和外部公司對接,我們需要提供一個介面文檔,這樣可以節省雙方時間、也可以防止後續扯皮。這是就要考驗我的介面是否規範化。 1. 介面名稱清晰、明確 顧名思義,介面是做什麼的,是否準確、清晰?讓使用這一眼就能知道 ...
  • 本文介紹基於Python語言,遍歷文件夾並從中找到文件名稱符合我們需求的多個.txt格式文本文件,並從上述每一個文本文件中,找到我們需要的指定數據,最後得到所有文本文件中我們需要的數據的合集的方法~ ...
  • Java JUC&多線程 基礎完整版 目錄Java JUC&多線程 基礎完整版1、 多線程的第一種啟動方式之繼承Thread類2、多線程的第二種啟動方式之實現Runnable介面3、多線程的第三種實現方式之實現Callable介面4、多線的常用成員方法5、線程的優先順序6、守護線程7、線程的讓出8、線 ...
  • 實時識別關鍵詞是一種能夠將搜索結果提升至新的高度的API介面。它可以幫助我們更有效地分析文本,並提取出關鍵詞,以便進行進一步的處理和分析。 該介面是挖數據平臺提供的,有三種模式:精確模式、全模式和搜索引擎模式。不同的模式在分詞的方式上有所不同,適用於不同的場景。 首先是精確模式。這種模式會儘量將句子 ...
  • 1 為啥要折騰搭建一個專屬圖床? 技術大佬寫博客都用 md 格式,要在多平臺發佈,圖片就得有外鏈 後續如博客遷移,國內博客網站如掘金,簡書,語雀等都做了防盜鏈,圖片無法遷移 2 為啥選擇CloudFlare R2 跳轉:https://dash.cloudflare.com/ 有白嫖額度 免費 CD ...