採用 SSM (Spring + Spring MVC + MyBatis) + Freemarker 模板引擎,前端權限樹使用 zTree 插件。核心流程:
-- 模塊表:遞歸樹結構
CREATE TABLE modules (
id INT PRIMARY KEY AUTO_INCREMENT,
module VARCHAR(30) COMMENT '模塊名稱',
pid INT COMMENT '父級ID',
level INT COMMENT '層級:0-一級,1-二級,2-三級'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 用戶表:權限碼存儲格式 "一級碼,二級碼;二級碼,三級碼;三級碼..."
CREATE TABLE users (
user_code VARCHAR(10) PRIMARY KEY COMMENT '用戶代碼',
user_name VARCHAR(40),
user_password VARCHAR(100),
auth_code TEXT COMMENT '權限碼:層級間分號分隔,同層級逗號分隔'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
權限碼示例:1,2,3;4,5,6;7,8,9 表示擁有一級模塊 1/2/3、二級 4/5/6、三級 7/8/9。
@RequestMapping("getUserRightMaskById")
@ResponseBody
public String getUserRightMaskById(HttpSession session, String id, String substoreid) {
// 1. 判斷版本(完整版/簡易版)查詢不同模塊集
List<Map> versions = setupService.getHotelHotelVersions(substoreid);
String version = (String) versions.get(0).get("versions");
Map<String, Object> treeMap = rightMaskService.getUserRightMaskOnTree(substoreid, id, version);
return JSONObject.toJSONString(Map.of("datas", treeMap), SerializerFeature.WriteMapNullValue);
}
@RequestMapping("updateUserRightMaskByAjax")
@ResponseBody
public String updateUserRightMaskByAjax(String rootId, String midId, String minId, String userId) {
// 前端傳遞三層級勾選的 ID 字符串,Service 拼接保存
rightMaskService.updateUserRightMask(rootId, midId, minId, userId);
return "1"; // 成功
}
public Map<String, Object> getUserRightMaskOnTree(String substoreid, String userId, String version) {
// 1. 獲取用戶現有權限碼
Map<String, Object> userMask = iRightMaskDao.getUserRightMaskBySubAndId(substoreid, userId);
// 2. 查詢各層級模塊列表
List<Map> level1 = "complete".equals(version) ?
iRightMaskDao.getRightMaskOnHotelOne() : iRightMaskDao.getRightMaskOnTavernOne();
List<Map> level2 = ...; // level3 同理
// 3. 遞歸構建 zTree 節點
List<Map> result = new ArrayList<>();
buildTree(result, level1, level2, level3, userMask);
return Map.of("data", result);
}
private void buildTree(List<Map> result, List<Map> l1, List<Map> l2, List<Map> l3, Map<String, Object> auth) {
for (Map m1 : l1) {
Map<String, Object> node1 = Map.of("id", m1.get("id"), "name", m1.get("module"));
node1.put("checked", hasPermission(l1, auth, m1));
List<Map> children2 = new ArrayList<>();
for (Map m2 : l2) if (m2.get("pid").equals(m1.get("id"))) {
Map<String, Object> node2 = Map.of("id", m2.get("id"), "name", m2.get("module"));
node2.put("checked", hasPermission(l2, auth, m2));
List<Map> children3 = new ArrayList<>();
for (Map m3 : l3) if (m3.get("pid").equals(m2.get("id"))) {
Map<String, Object> node3 = Map.of("id", m3.get("id"), "name", m3.get("module"));
node3.put("checked", hasPermission(l3, auth, m3));
children3.add(node3);
}
node2.put("children", children3);
children2.add(node2);
}
node1.put("children", children2);
result.add(node1);
}
}
private boolean hasPermission(List<Map> list, Map auth, Map node) {
String code = (String) auth.get("auth_code");
if (code == null) return false;
String[] levels = code.split(";");
// 根據節點所在層級解析對應層級碼
// 此處簡化:實際需傳入層級索引
return Arrays.stream(levels[level].split(",")).anyMatch(id -> id.equals(node.get("id")));
}
<select id="getRightMaskOnHotelOne" resultType="map">
select id, module, pid, level from modules where level = 0
</select>
<select id="getUserRightMaskBySubAndId" resultType="map">
select auth_code from users where user_code = #{userId}
</select>
// setUserauthOnTree.html
$.post("${ctx}/Setup/getUserRightMaskById", {id: "${userId}"}, function(res) {
zTreeObj = $.fn.zTree.init($("#treeDemo"), setting, res.datas.data);
zTreeObj.expandAll(true);
}, "json");
var setting = {
check: { enable: true, chkStyle: "checkbox", chkboxType: { "Y": "ps", "N": "ps" } },
callback: { onCheck: zTreeOnCheck }
};
function editModle() {
var ids = { rootId: "", midId: "", minId: "" };
var nodes = zTreeObj.getCheckedNodes();
nodes.forEach(n => {
if (n.level == 0) ids.rootId += "," + n.id;
else if (n.level == 1) ids.midId += "," + n.id;
else if (n.level == 2) ids.minId += "," + n.id;
});
// 去除首個逗號後提交
$.post("${ctx}/Setup/updateUserRightMaskByAjax", { ...ids, userId: "${userId}" }, ...);
}
<bean id="freeMarkerViewResolver" class="...FreeMarkerViewResolver">
<property name="attributesMap">
<map>
<entry key="menucall"><bean class="com.leike.util.MenuFunction"/></entry>
</map>
</property>
</bean>
public class MenuFunction implements TemplateMethodModel {
public Object exec(List args) {
int level = Integer.parseInt(args.get(0).toString());
int moduleId = Integer.parseInt(args.get(1).toString());
HttpSession session = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes())
.getRequest().getSession();
Info info = (Info) session.getAttribute("info");
String authCode = info.getUser().getAuthCode(); // "1,2;3,4;5,6"
String[] levels = authCode.split(";");
String[] modules = levels[level].split(",");
return Arrays.stream(modules).anyMatch(m -> m.equals(String.valueOf(moduleId)));
}
}
<#if menucall(1, 122)>
<li><a href="#" id="booknew"><i class="glyphicon"></i>預訂</a></li>
</#if>
| 層面 | 設計決策 | 優缺點 |
|---|---|---|
| 權限存儲 | 字符串編碼(分號/逗號分隔) | 簡單、無需關聯表;但查詢/統計困難,長度受限 |
| 樹構建 | Service 層多層循環組裝 Map | 直觀、靈活;大數據量時性能一般,可改用遞歸 CTE 或緩存 |
| 前端交互 | zTree checkbox + 三狀態聯動 | 用戶體驗好;需注意大量節點渲染性能(分頁/虛擬滾動) |
| 頁面控制 | Freemarker 指令讀取 Session | 服務端渲染安全;但模板邏輯與後端耦合,現代 SPA 應改為前端路由守衛 |
Set),登錄時加載,頁面檢查 O(1)。/api/permissions 返回權限標識集合,前端路由守衛/按鈕級 v-if 控制。@RequiresPermission("order:create") 統一攔截。本方案展示了傳統 SSM + 服務端渲染架構下完整的權限控制實現:從數據庫編碼存儲、樹形結構動態構建、前端可視化授權、到模板引擎級別的顯隱控制。雖然技術棧略顯老舊,但核心思想(權限編碼、樹構建、多層校驗)在現代微服務、前後端分離架構中依然具有參考價值。