java 實作有繼承特性的資費方案

使用情境:

  • 付費選擇 Basic 方案用戶, 可以使用30個元件。
  • 付費選擇 Plus 方案用戶, 可以使用Basic 中的所以元件與Plus 可以額外使用的 30個元件,全部可以使用 60個元件。
  • 付費選擇 Enterprise 方案用戶, 可以使用Plus 中的所以元件與Enterprise 可以額外使用的 30個元件,全部可以使用 60個元件。

在 Basic 用戶增加與刪除元件,都會影響到 Plus 與 Enterprise 用戶。

規則就上面這些,我們實作需要用到3個 table 分別敘述如下:

  • plan, 資費安案
  • plan_component, 每個方案, 各自的元件對映表。
  • plan_component_extend, 每個方案, 在繼承狀態下的元件對映表。

Database schema

java source code:

if (!this.isTableExist("plan"))
{
    q = "CREATE TABLE plan ( id smallint AUTO_INCREMENT PRIMARY KEY, plan_name varchar(512), description varchar(2048), tag varchar(4096), extend_plan_id int)  ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
    this.execute(q);

    // default plan.
    q = "INSERT INTO plan(plan_name, description, tag, extend_plan_id) VALUES ('Basic','For all projects','#public', null);";
    this.execute(q);
    q = "INSERT INTO plan(plan_name, description, tag, extend_plan_id) VALUES ('Plus','For advanced projects','#public', 1);";
    this.execute(q);
    q = "INSERT INTO plan(plan_name, description, tag, extend_plan_id) VALUES ('Enterprise','For enterprise-grade projects, include best solutions.','#public', 2);";
    this.execute(q);
}

if (!this.isTableExist("plan_component")) {
    q = "CREATE TABLE plan_component( id INT AUTO_INCREMENT PRIMARY KEY, plan_id int not null, component_id int not null)  ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
    this.execute(q);

    q = "CREATE INDEX plan_components_plan on plan_component(plan_id);";
    this.execute(q);

    q = "CREATE UNIQUE INDEX plan_component_unique on plan_component(plan_id, component_id);";
    this.execute(q);
}

if (!this.isTableExist("plan_component_extend")) {
    q = "CREATE TABLE plan_component_extend( id INT AUTO_INCREMENT PRIMARY KEY, plan_id int not null, component_id int not null)  ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;";
    this.execute(q);

    q = "CREATE INDEX plan_components_plan_extend on plan_component_extend(plan_id);";
    this.execute(q);

    q = "CREATE UNIQUE INDEX plan_component_extend_unique on plan_component_extend(plan_id, component_id);";
    this.execute(q);
}

說明:

  • 預設有3 個 plan: Basic, Plus, Enterprise,
  • Plus 裡的 extend_plan_id 為 Basic,
  • Enterprise 裡的 extend_plan_id 為 Plus,

Database 元件

針對 3 個 Table 都建立一個 Object 來用存取 table:

  • Plan Table 就沒什麼好講的, 對 Plan 做新增/修改/刪除.
  • plan_component_extend Table, 直接繼承 plan_component table,
  • plan_component table 使用的原始碼如下:
public boolean resetPermisionListByPlanId(long plan_id)
{
    boolean ret = false;
    String sql = "";
    sql = "delete from " + this.tablename + " where plan_id=" + plan_id;
    this.execute(sql);
    ret = true;
    return ret;
}

public boolean resetPermisionListByComponentId(long component_id)
{
    boolean ret = false;
    String sql = "";
    sql = "delete from " + this.tablename + " where component_id=" + component_id;
    this.execute(sql);
    ret = true;
    return ret;
}

public long addPermisionList(long plan_id, long component_id)
{
    String sql = "";

    // allow to duplicate.

    if (this.dbms == DBMS.MySQL)
    {
        sql = "INSERT IGNORE INTO " + this.tablename;
    }
    else
    {
        sql = "INSERT INTO " + this.tablename;
    }

    sql += " (plan_id, component_id) values(" + plan_id + "," + component_id + ");";
    long db_id = executeGetKeys(sql);
    return db_id;
}

public void delete_match(long plan_id, long component_id)
{
    String sql;
    sql = "DELETE";
    sql += " FROM " + this.tablename;
    sql += " WHERE plan_id=" + plan_id;
    sql += " AND component_id=" + component_id;
    this.execute(sql);
}

public boolean delete(long plan_permission_id)
{
    boolean ret = false;

    long plan_id = 0;
    long component_id = 0;
    JSONObject plan_permission_json = this.getRowJsonByPk("" + plan_permission_id);
    if(plan_permission_json != null)
    {
        plan_id = plan_permission_json.getLong("plan_id");
        component_id = plan_permission_json.getLong("component_id");
    }

    // delete mapping.
    this.deleteByPk(plan_permission_id + "");

    // sync to extend table.
    if(plan_id > 0 && component_id > 0)
    {
        PlanPermissionExtendTable dbo_plan_permission_extend = new PlanPermissionExtendTable();
        dbo_plan_permission_extend.delete_match(plan_id, component_id);

        push_permission(plan_id);
    }

    ret = true;
    return ret;
}

public void push_as_permission(long plan_id, long as_plan_id)
{
    if(plan_id > 0)
    {
        String sql;
        sql = "SELECT component_id";
        sql += " FROM plan_component_extend";
        sql += " WHERE plan_id=" + plan_id;

        PlanPermissionExtendTable dbo_plan_permission_extend = new PlanPermissionExtendTable();

        JSONArray data_list_obj = null;
        data_list_obj = executeJson(sql);
        if(data_list_obj != null)
        {
            for(int i = 0; i < data_list_obj.length(); i++)
            {
                long component_id = data_list_obj.getJSONObject(i).getLong("component_id");
                dbo_plan_permission_extend.addPermisionList(as_plan_id, component_id);
            }
        }
    }
}


public void push_permission(long plan_id)
{
    if(plan_id > 0)
    {
        String sql;
        sql = "SELECT component_id";
        sql += " FROM " + this.tablename;
        sql += " WHERE plan_id=" + plan_id;

        PlanPermissionExtendTable dbo_plan_permission_extend = new PlanPermissionExtendTable();

        JSONArray data_list_obj = null;
        data_list_obj = executeJson(sql);
        if(data_list_obj != null)
        {
            for(int i = 0; i < data_list_obj.length(); i++)
            {
                long component_id = data_list_obj.getJSONObject(i).getLong("component_id");
                dbo_plan_permission_extend.addPermisionList(plan_id, component_id);
            }
        }

        sql = "SELECT id";
        sql += " FROM plan";
        sql += " WHERE extend_plan_id=" + plan_id;
        data_list_obj = executeJson(sql);
        if(data_list_obj != null)
        {
            for(int i = 0; i < data_list_obj.length(); i++)
            {
                long child_plan_id = data_list_obj.getJSONObject(i).getLong("id");
                if(child_plan_id > 0)
                {
                    dbo_plan_permission_extend.resetPermisionListByPlanId(child_plan_id);
                    push_as_permission(plan_id, child_plan_id);

                    push_permission(child_plan_id);
                }
            }
        }
    }
}

說明:

在權限的同步上, 有很多個解法, 可以從「爸爸去找小孩」, 也可以從「小孩去找爸爸」進行資料的同步。

以 Enterprise 在內容有異動的情況下, 不會影響到 Plus / Basic, 所以在權限同步的選擇上, 是用parent 去找 child 會簡單一些. Enterprise 算是 grand child, 所以不需要在這個情況下去同步權限。

以小孩找爸爸的解法也可以, 先把 _extend table 裡的 Enterprise 都清掉, 再把 Enterprise 的 component 先倒一次到 _extend table, 接著, 再把 _extend table 裡 Enterprise 的上一層的 parent (Plus的) 資料都另存為 Enterprise。這一個解法也很簡單。但並不是上面的程式碼裡的邏輯。

上面的程式碼裡的邏輯說明:

  • Step 1: 在 plan_compoent 刪除後, 同時也要去 _extend table 刪除。
  • Step 2: 在 plan_compoent 刪除或新增後,呼叫 push_permission(plan_id); 對整個 plan_id 的權限全部列出來, 複製到 _extend table 做一次同步。
  • Step 3: 檢查目前 plan_id 下有沒有掛 child plan, 有話全部讀出來。
  • Step 4: 刪除 child plan_id 在 _extend table 裡的值,因為需要重新同步。
  • Step 5: 把目前 plan 的權限, 先 save as 為 child plan 在 _extend table.
  • Step 6: 對 child plan 做 push_permission(child_plan_id), 這個產生一個 recursive loop 在上面的 step 2.

讀這些程式,需要在腦袋清楚的情況下,不然是很容易邏輯打結。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *