使用情境:
- 付費選擇 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.
讀這些程式,需要在腦袋清楚的情況下,不然是很容易邏輯打結。