# Mokee Gateway 系统接入完整指南 > 版本:2026-06-13 | 适用于所有需要通过 Mokee Gateway 统一登录的业务系统 --- ## 目录 1. [架构概览](#一架构概览) 2. [接入流程](#二接入流程) 3. [前端接入(完整代码)](#三前端接入完整代码) 4. [后端接入(完整代码)](#四后端接入完整代码) 5. [API 接口参考(完整请求/响应结构)](#五api-接口参考) 6. [数据库初始化](#六数据库初始化) 7. [创建管理员账号](#七创建网关管理员账号) 8. [创建管理员账号](#七创建网关管理员账号) 9. [UserChip Widget 一键接入](#八usership-widget-一键接入) 10. [对接测试清单](#九对接测试清单) --- ## 一、架构概览 ``` ┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ 业务前端 │────▶│ Mokee Gateway │────▶│ 业务后端 │ │ (Vue/React) │ │ gw.server.zgitm.com │ │ (任意语言) │ │ │ │ │ │ │ │ 携带 Token │ │ 验证 Token │ │ 从请求头读取: │ │ + X-System-Code │ 识别系统编码 │ │ X-User-Id │ │ │ │ 转发请求 │ │ X-User-Name │ │ │ │ 注入用户信息头 │ │ X-System-Code │ └──────────────┘ └──────────────────┘ └──────────────────┘ ``` ### 流程 1. 用户访问业务前端 → 无 Token → 跳转统一登录页 2. 登录成功 → 获得 JWT Token → 回跳到业务前端 3. 业务前端所有 API 请求 → 网关 (携带 Authorization + X-System-Code) 4. 网关验证 Token → 注入用户信息请求头 → 转发到业务后端 5. 业务后端从请求头读取用户信息 → 执行业务逻辑 ### 三条关键规则 **规则一:前端只调网关** — 业务前端 baseURL 配置为网关地址,不能直接调业务后端。 **规则二:后端必须开跨域 (CORS)** — 业务后端需要允许来自网关域名的跨域请求。 **规则三:业务后端不再做登录/鉴权** — Token 验证由网关完成,业务后端只需从请求头读取用户信息。 --- ## 二、接入流程 1. 在网关管理后台注册业务系统 → 获得 `systemCode` 和 `docToken` 2. 在网关数据库执行初始化 SQL → 创建菜单和角色 3. 前端接入:配置 Axios 实例 + 路由守卫 4. 后端接入:配置 CORS + UserContext 中间件 5. 创建管理员账号 → 登录管理后台维护本系统数据 6. 对接测试 → 完成验收 --- ## 三、前端接入(完整代码) ### 3.1 安装依赖 ```bash npm install axios ``` ### 3.2 环境变量 ```env VITE_GATEWAY_URL = https://gw.server.zgitm.com VITE_LOGIN_URL = https://web.gw.zgitm.com/login VITE_SYSTEM_CODE = YOUR_SYSTEM_CODE ``` | 变量 | 说明 | 从哪里获取 | |------|------|-----------| | `VITE_GATEWAY_URL` | 网关 API 地址 | 固定值,见附录地址表 | | `VITE_LOGIN_URL` | 统一登录页地址 | 固定值,见附录地址表 | | `VITE_SYSTEM_CODE` | 你的业务系统编码 | 在网关管理后台注册系统后获得(联系平台管理员) | ### 3.3 Axios 实例 (src/utils/request.ts) ```typescript import axios from 'axios' import { ElMessage } from 'element-plus' const SYSTEM_CODE = import.meta.env.VITE_SYSTEM_CODE const request = axios.create({ baseURL: import.meta.env.VITE_GATEWAY_URL, timeout: 30000, }) // 从 Cookie 读取 Token(登录后网关写入的跨域 Cookie) function getToken(): string | null { const match = document.cookie.match(/(?:^|;\s*)token=([^;]*)/) return match ? decodeURIComponent(match[1]) : null } // 请求拦截器:携带 Token 和系统编码 request.interceptors.request.use(config => { const token = getToken() if (token) { config.headers.Authorization = `Bearer ${token}` } // 关键:网关根据此头识别业务系统 config.headers['X-System-Code'] = SYSTEM_CODE return config }) // 响应拦截器:401 自动跳转登录 request.interceptors.response.use( res => res.data.code === 200 ? res.data : Promise.reject(res.data), err => { if (err.response?.status === 401) { window.location.href = `${import.meta.env.VITE_LOGIN_URL}?systemCode=${SYSTEM_CODE}` } return Promise.reject(err) } ) export default request ``` ### 3.4 路由守卫 (src/router/index.ts) ```typescript import { createRouter, createWebHistory } from 'vue-router' const SYSTEM_CODE = import.meta.env.VITE_SYSTEM_CODE const LOGIN_URL = import.meta.env.VITE_LOGIN_URL const router = createRouter({ history: createWebHistory(), routes: [ /* 你的路由 */ ], }) router.beforeEach((to, _from, next) => { const token = document.cookie.match(/(?:^|;\s*)token=([^;]*)/)?.[1] if (!token && to.path !== '/login') { // 无 Token → 跳统一登录页 window.location.href = `${LOGIN_URL}?systemCode=${SYSTEM_CODE}` return } next() }) export default router ``` ### 3.5 登录成功回调处理 用户在统一登录页登录成功后,网关将 Token 写入 **Cookie**(domain: `.zgitm.com`,跨子域共享),然后跳回系统注册时填写的 `frontUrl`(即你的业务前端首页地址)。业务前端无需自行实现登录接口,只需从 Cookie 中读取 Token 即可。 ```typescript // 从 Cookie 读取 Token function getToken(): string | null { const match = document.cookie.match(/(?:^|;\s*)token=([^;]*)/) return match ? decodeURIComponent(match[1]) : null } // App.vue onMounted 或 main.ts const token = getToken() if (token) { // 用户已登录,正常渲染应用 // 如需获取用户信息,调用网关 API: const res = await request.get('/zgapi/v1/auth/user/info') console.log('当前用户:', res.data) } else { // 无 Token → 跳统一登录页 window.location.href = `${import.meta.env.VITE_LOGIN_URL}?systemCode=${SYSTEM_CODE}` } ``` #### 多系统场景说明 一个用户可能绑定了多个业务系统。跳转登录页时在 URL 中带上 `systemCode` 参数,登录页会自动选择对应系统: ``` https://web.gw.zgitm.com/login?systemCode=YOUR_SYSTEM_CODE ``` 这就是为什么路由守卫和 Axios 拦截器中跳转登录时都要拼 `?systemCode=${SYSTEM_CODE}`——网关登录页根据这个参数知道用户要从哪个系统登录,登录成功后直接回跳到该系统的 `frontUrl`。 > **💡 Token 存储说明**:Token 存在 Cookie 中(而非 localStorage),因为 Cookie 支持跨子域共享(`.zgitm.com`),统一登录、管理后台、文件服务等不同子站都能读取到同一个 Token。Cookie 有效期 2 小时,与 Token 过期时间一致。 ### 3.6 按钮权限控制(v-permission 指令) 用户信息接口(`/zgapi/v1/auth/user/info`)会返回 `permissions` 字段(按钮权限标识列表),前端据此控制按钮的显示/隐藏。你需要注册一个 `v-permission` 自定义指令: **src/directives/permission.ts:** ```typescript import type { Directive } from 'vue' // 从 Cookie 读取 Token,解析出 payload 中的 permissions // 或从 Vuex/Pinia store 中读取(取决于你的状态管理方案) function getPermissions(): string[] { const token = document.cookie.match(/(?:^|;\s*)token=([^;]*)/)?.[1] if (!token) return [] try { // JWT payload 在中间那段(Base64) const payload = JSON.parse(atob(token.split('.')[1])) return payload.permissions ?? [] } catch { return [] } } export const permission: Directive = { mounted(el, binding) { const permissions = getPermissions() const required = binding.value as string // 如果用户没有该权限,隐藏按钮 if (!permissions.includes(required)) { el.parentNode?.removeChild(el) } }, } ``` **src/main.ts 中全局注册:** ```typescript import { createApp } from 'vue' import App from './App.vue' import { permission } from './directives/permission' const app = createApp(App) app.directive('permission', permission) app.mount('#app') ``` **业务代码中使用:** ```html ``` > **原理**:后端返回的 `permissions` 就是你在 `sys_menu` 表里配置的 `permission` 字段值(menu_type=3 的按钮记录)。它们通过角色绑定到用户,登录时返回。前端 `v-permission` 指令检查当前用户是否有该权限,没有就从 DOM 中移除元素。 --- ## 四、后端接入(完整代码) ### 4.1 网关转发时注入的请求头 | 请求头名称 | 数据类型 | 说明 | |-----------|---------|------| | `X-User-Id` | String | 当前登录用户的唯一ID | | `X-User-Name` | String | 用户真实姓名 | | `X-User-Account` | String | 用户登录账号 | | `X-User-Email` | String | 用户邮箱 | | `X-User-Post` | String | 用户岗位 | | `X-User-Roles` | JSON | 用户角色列表 `[{roleId, roleName, roleCode}]` | | `X-System-Id` | String | 当前业务系统的数据库ID | | `X-System-Code` | String | 当前业务系统的唯一编码 | | `X-System-Name` | String | 当前业务系统的显示名称 | > **重要**:这些请求头由网关自动注入,业务后端可以信任这些值。不需要再验证 Token 或查询用户信息。 ### 4.2 Java — UserContext 工具类 ```java // ====== UserContext.java ====== import java.util.*; public class UserContext { private static final ThreadLocal> CTX = new ThreadLocal<>(); public static void set(Map user) { CTX.set(user); } public static Map get() { return CTX.get(); } public static void clear() { CTX.remove(); } // 快捷方法 public static String getUserId() { return get().get("X-User-Id"); } public static String getUserName() { return get().get("X-User-Name"); } public static String getUserAccount(){ return get().get("X-User-Account"); } public static String getSystemCode() { return get().get("X-System-Code"); } public static String getSystemId() { return get().get("X-System-Id"); } } // ====== UserContextFilter.java ====== import jakarta.servlet.*; import jakarta.servlet.http.HttpServletRequest; import org.springframework.stereotype.Component; import java.io.IOException; import java.util.*; @Component public class UserContextFilter implements Filter { private static final String[] HEADERS = { "X-User-Id", "X-User-Name", "X-User-Account", "X-User-Email", "X-User-Post", "X-User-Roles", "X-System-Id", "X-System-Code", "X-System-Name" }; @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; Map user = new HashMap<>(); for (String h : HEADERS) { String v = request.getHeader(h); if (v != null) user.put(h, v); } UserContext.set(user); try { chain.doFilter(req, res); } finally { UserContext.clear(); } } } // ====== 业务代码中使用 ====== @RestController public class OrderController { @GetMapping("/api/orders") public List list() { String userId = UserContext.getUserId(); // "123" String userName = UserContext.getUserName(); // "张三" String systemCode = UserContext.getSystemCode(); // "msg" return orderService.findByUser(userId, systemCode); } } ``` ### 4.3 Python — Flask 中间件 ```python # ====== middleware.py ====== from flask import request, g USER_HEADERS = [ 'X-User-Id', 'X-User-Name', 'X-User-Account', 'X-User-Email', 'X-User-Post', 'X-User-Roles', 'X-System-Id', 'X-System-Code', 'X-System-Name', ] def user_context_middleware(): """将网关注入的用户信息存到 Flask g 对象""" for h in USER_HEADERS: val = request.headers.get(h) if val: setattr(g, h.replace('-', '_').lower(), val) # 注册到 app # app.before_request(user_context_middleware) # ====== 业务代码中使用 ====== from flask import g @app.route('/api/orders') def list_orders(): user_id = g.x_user_id # "123" user_name = g.x_user_name # "张三" system_code = g.x_system_code # "msg" return order_service.query(user_id, system_code) ``` ### 4.4 Go — 中间件 ```go // ====== middleware.go ====== package middleware import ( "context" "net/http" ) type contextKey string func UserContext(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() for _, h := range []string{ "X-User-Id", "X-User-Name", "X-User-Account", "X-User-Email", "X-User-Roles", "X-System-Id", "X-System-Code", } { if v := r.Header.Get(h); v != "" { ctx = context.WithValue(ctx, contextKey(h), v) } } next.ServeHTTP(w, r.WithContext(ctx)) }) } // 快捷函数 func GetUserID(ctx context.Context) string { return ctx.Value(contextKey("X-User-Id")).(string) } func GetSystemCode(ctx context.Context) string { return ctx.Value(contextKey("X-System-Code")).(string) } // ====== main.go: 注册中间件 ====== // http.Handle("/api/", middleware.UserContext(yourHandler)) // ====== 业务代码 ====== func listOrders(w http.ResponseWriter, r *http.Request) { userID := middleware.GetUserID(r.Context()) // "123" systemCode := middleware.GetSystemCode(r.Context()) // "msg" // ... } ``` ### 4.5 Java CORS 配置 ```java @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("*") .allowedHeaders("*") .allowCredentials(true); } } ``` > **注意**:`permissions` 字段**仅在当前系统为网关(gateway)时返回**。业务系统调用此接口时 `permissions` 为 `null`,业务系统应通过 §5.2 菜单路由接口获取自身系统的权限列表。 --- ## 五、API 接口参考 > 所有接口统一通过网关地址访问:`https://gw.server.zgitm.com` ### 统一响应格式 `Result` ```json { "code": 200, "msg": "操作成功", "data": {}, "timestamp": 1718294400000 } ``` | code 值 | 含义 | HTTP 状态码 | 说明 | |---------|------|------------|------| | `200` | 成功 | 200 | 业务正常返回 | | `401` | 未登录/Token 过期 | 401 | 前端自动跳转登录页 | | `403` | 无权限 | 403 | Token 有效但用户无此接口权限 | | `500` | 服务器错误 | 500 | 后端异常,查看网关日志 | > **💡 注意**:`code` 和 HTTP 状态码是**两套独立的值**。通常它们保持一致,但判断业务结果应看响应体的 `code` 字段而不是 HTTP status code。响应拦截器里 `res.data.code === 200` 判断成功就是这个道理。 ### 接口速查表 | 方法 | 路径 | 说明 | |------|------|------| | GET | `/zgapi/v1/auth/user/info` | 获取当前登录用户信息 | | GET | `/zgapi/v1/auth/menu/router?systemCode=xxx` | 获取树形菜单路由 + 系统所有权限标识 | | GET | `/zgapi/v1/auth/system/list` | 获取可切换的业务系统列表 | | PUT | `/zgapi/v1/auth/password/update?oldPwd=xxx&newPwd=xxx` | 修改当前用户密码 | --- ### 5.1 获取当前用户信息 ``` GET /zgapi/v1/auth/user/info Authorization: Bearer {token} ``` **响应 data (UserInfoVO):** ```json { "userId": "a1b2c3d4e5f6789012345678", "username": "zhangsan", "realName": "张三", "email": "zhangsan@example.com", "phone": "13800138000", "avatar": "https://file.wg.zgitm.com/api/download/xxx?token=...", "post": "高级工程师", "createdAt": "2025-01-15T10:30:00", "lastLoginTime": "2026-06-13T09:15:30" } ``` | 字段 | 类型 | 说明 | |------|------|------| | `userId` | String | 用户UUID | | `username` | String | 用户名 | | `realName` | String | 真实姓名 | | `email` | String | 邮箱 | | `phone` | String | 手机号 | | `avatar` | String | 头像URL(可能来自文件服务,每次都需刷新 Token) | | `post` | String | 岗位 | | `createdAt` | String | 注册时间 (ISO 8601) | | `lastLoginTime` | String | 最后登录时间 | --- ### 5.2 获取系统菜单路由 ``` GET /zgapi/v1/auth/menu/router?systemCode=YOUR_CODE Authorization: Bearer {token} ``` **响应 data (MenuRouterResponse):** ```json { "menu": [ { "id": "menu-001", "menuName": "系统管理", "menuType": 1, "icon": "Setting", "permission": null, "path": null, "name": "系统管理", "component": "Layout", "sortOrder": 1, "status": 1, "meta": { "title": "系统管理", "icon": "Setting" }, "children": [ { "id": "menu-002", "menuName": "用户管理", "menuType": 2, "icon": "User", "permission": "system:user:list", "path": "/system/user", "name": "用户管理", "component": "system/user/index", "sortOrder": 1, "status": 1, "meta": { "title": "用户管理", "icon": "User" }, "children": null }, { "id": "menu-003", "menuName": "角色管理", "menuType": 2, "icon": "Avatar", "permission": "system:role:list", "path": "/system/role", "name": "角色管理", "component": "system/role/index", "sortOrder": 2, "status": 1, "meta": { "title": "角色管理", "icon": "Avatar" }, "children": null } ] } ], "permission": [ "system:user:list", "system:user:add", "system:user:edit", "system:user:delete", "system:role:list", "system:role:add", "system:role:edit", "system:role:delete" ] } ``` | 顶层字段 | 类型 | 说明 | |------|------|------| | `menu` | List\ | 树形菜单路由列表(结构见下表) | | `permission` | List\ | 该系统所有权限标识(用于前端 v-permission 指令控制按钮显隐) | **menu[] 中每个菜单节点的字段说明:** |------|------|------| | `id` | String | 菜单ID | | `menuName` | String | 菜单显示名称 | | `menuType` | Integer | **1=目录**(折叠分组,path=null,component="Layout")
**2=菜单**(页面,有path和component)
**3=按钮**(权限点,不在菜单显示) | | `icon` | String | 图标名称 | | `permission` | String | 权限标识(如 `system:user:list`) | | `path` | String | 前端路由路径(目录为 null) | | `name` | String | 路由名称 | | `component` | String | Vue 组件路径 | | `sortOrder` | Integer | 排序号(越小越靠前) | | `status` | Integer | 0=隐藏, 1=显示 | | `meta` | Object | `{title, icon}` 路由元数据 | | `children` | List | 子菜单(叶子节点为 **null**) | > **注意**:菜单按用户角色过滤,不同角色看到的菜单不同。叶子节点 `children` 为 `null`(非空数组),前端据此判断是否显示展开箭头。 --- ### 5.3 获取可切换系统列表 ``` GET /zgapi/v1/auth/system/list Authorization: Bearer {token} ``` **响应 data (List\):** ```json [ { "systemId": "sys-001", "systemCode": "erp", "systemName": "ERP系统", "frontUrl": "https://erp.example.com", "icon": "el-icon-s-shop", "description": "企业资源计划管理系统" } ] ``` | 字段 | 类型 | 说明 | |------|------|------| | `systemId` | String | 系统UUID | | `systemCode` | String | 系统编码 | | `systemName` | String | 系统名称 | | `frontUrl` | String | 前端入口地址 | | `icon` | String | 系统图标 | | `description` | String | 系统描述 | > 列表中不包含网关系统本身(systemCode="gateway"),用户无绑定系统时返回空数组 `[]`。 --- ### 5.4 修改当前用户密码 ``` PUT /zgapi/v1/auth/password/update?oldPwd={旧密码}&newPwd={新密码} Authorization: Bearer {token} ``` | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | `oldPwd` | String | 是 | 旧密码(Query参数) | | `newPwd` | String | 是 | 新密码(Query参数,至少6位) | 响应:`{"code":200,"msg":"密码修改成功","data":null}`。 需验证旧密码正确后才能修改,修改后需重新登录。 --- ## 六、数据库初始化 在网关数据库执行以下 SQL,为你的业务系统创建默认菜单和角色。 ### 6.1 涉及的表 #### sys_menu(菜单/权限表) 菜单采用**三级结构**:目录 → 页面菜单 → 按钮权限。后端返回菜单树时按角色过滤,用户只能看到自己角色绑定的菜单。 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `id` | VARCHAR(64) | 是 | UUID 主键,用 `REPLACE(UUID(),'-','')` 生成 | | `system_id` | VARCHAR(64) | 是 | 所属业务系统的 ID,从 `sys_system` 表查出 | | `parent_id` | VARCHAR(64) | 是 | 父菜单 ID,顶级菜单填 `'0'`,子菜单填父菜单的 ID | | `menu_name` | VARCHAR(64) | 是 | 菜单显示名称(如"用户管理"、"新增用户") | | `menu_path` | VARCHAR(256) | 否 | 前端路由路径。**目录**填 `/xxx`,**页面菜单**填 `/xxx/yyy`,**按钮**填 `NULL` | | `component` | VARCHAR(256) | 否 | Vue 组件路径。**目录**固定填 `'Layout'`,**页面菜单**填 `'xxx/yyy/index'`,**按钮**填 `NULL` | | `icon` | VARCHAR(128) | 否 | 图标名称(Element Plus 图标)。**目录/菜单**填写,**按钮**填空串 `''` | | `menu_type` | TINYINT | 是 | **1=目录**(侧边栏折叠分组,不对应具体页面)
**2=页面菜单**(具体页面,可点击跳转)
**3=按钮**(页面内的操作按钮,不显示在菜单树上,仅用于后端权限校验) | | `permission` | VARCHAR(128) | 否 | 权限标识符,命名规范:`{模块}:{资源}:{操作}`。**目录**填 `NULL`,**页面菜单**填 `xxx:yyy:list`,**按钮**填 `xxx:yyy:add/edit/delete` 等 | | `sort_order` | INT | 是 | 排序号,数字越小越靠前。同级菜单按此字段升序排列 | | `status` | TINYINT | 是 | 0=隐藏(不在菜单树中返回),1=显示 | **permission 命名规范示例**: | 资源 | 页面菜单 (list) | 新增按钮 (add) | 编辑按钮 (edit) | 删除按钮 (delete) | |------|----------------|---------------|----------------|----------------| | 用户 | `system:user:list` | `system:user:add` | `system:user:edit` | `system:user:delete` | | 角色 | `system:role:list` | `system:role:add` | `system:role:edit` | `system:role:delete` | | 订单 | `biz:order:list` | `biz:order:add` | `biz:order:edit` | `biz:order:delete` | > 接入方可以自定义 permission 标识,只要和前端 `v-permission` 指令、后端 `@RequirePermission` 注解保持一致即可。 #### sys_role(角色表) | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `id` | VARCHAR(64) | 是 | UUID 主键 | | `system_id` | VARCHAR(64) | 是 | 所属业务系统 ID | | `role_name` | VARCHAR(64) | 是 | 角色显示名称(如"管理员"、"普通用户") | | `role_code` | VARCHAR(64) | 是 | 角色编码(唯一标识,如 `admin`、`user`),同一系统内不可重复 | | `description` | VARCHAR(256) | 否 | 角色描述 | | `status` | TINYINT | 是 | 0=禁用,1=启用 | #### sys_role_menu(角色菜单关联表) | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `id` | VARCHAR(64) | 是 | UUID 主键 | | `role_id` | VARCHAR(64) | 是 | 角色 ID | | `menu_id` | VARCHAR(64) | 是 | 菜单 ID(含按钮权限) | > 一条 `sys_role_menu` 记录表示:该角色拥有该菜单/按钮的权限。菜单接口返回时会根据当前用户的角色自动过滤。 ### 6.2 初始化 SQL ```sql -- ============================================ -- {系统名称} 菜单 & 角色初始化 SQL -- 系统编码: {systemCode} -- ============================================ -- 0. 获取系统 ID SET @system_id = (SELECT id FROM sys_system WHERE system_code = '{systemCode}' LIMIT 1); -- 1. 创建顶级菜单目录(menu_type=1,permission 为 NULL) -- 用变量持有父菜单 UUID,后续子菜单直接引用变量名,无需手动查 ID -- 目录仅作为侧边栏折叠分组,不绑定具体页面,不需要权限标识 SET @menu_sys = REPLACE(UUID(),'-',''); SET @menu_biz = REPLACE(UUID(),'-',''); INSERT INTO sys_menu (id, system_id, parent_id, menu_name, menu_path, component, icon, menu_type, sort_order, status, permission) VALUES (@menu_sys, @system_id, '0', '系统管理', '/system', 'Layout', 'Setting', 1, 1, 1, NULL), (@menu_biz, @system_id, '0', '业务管理', '/biz', 'Layout', 'Document', 1, 2, 1, NULL); -- 2. 创建页面菜单(menu_type=2,permission 为页面访问权限标识) -- parent_id 直接用上面定义的变量,MySQL 变量在同一个会话内全局可见 -- 命名规范:{模块}:{资源}:list INSERT INTO sys_menu (id, system_id, parent_id, menu_name, menu_path, component, icon, menu_type, sort_order, status, permission) VALUES (REPLACE(UUID(),'-',''), @system_id, @menu_sys, '用户管理', '/system/user', 'system/user/index', 'User', 2, 1, 1, 'system:user:list'), (REPLACE(UUID(),'-',''), @system_id, @menu_sys, '角色管理', '/system/role', 'system/role/index', 'Avatar', 2, 2, 1, 'system:role:list'), (REPLACE(UUID(),'-',''), @system_id, @menu_biz, '订单管理', '/biz/order', 'biz/order/index', 'Tickets', 2, 1, 1, 'biz:order:list'); -- 3. 创建按钮权限(menu_type=3,permission 为按钮操作权限标识) -- 按钮的 parent_id 统一挂在对应页面的父菜单目录下(@menu_sys 或 @menu_biz) -- 按钮不显示在菜单树中,仅用于后端权限校验,path/component/icon 均为 NULL/空串 -- 命名规范:{模块}:{资源}:{操作}(add/edit/delete) INSERT INTO sys_menu (id, system_id, parent_id, menu_name, menu_path, component, icon, menu_type, sort_order, status, permission) VALUES -- 用户管理按钮 (REPLACE(UUID(),'-',''), @system_id, @menu_sys, '新增用户', NULL, NULL, '', 3, 1, 1, 'system:user:add'), (REPLACE(UUID(),'-',''), @system_id, @menu_sys, '编辑用户', NULL, NULL, '', 3, 2, 1, 'system:user:edit'), (REPLACE(UUID(),'-',''), @system_id, @menu_sys, '删除用户', NULL, NULL, '', 3, 3, 1, 'system:user:delete'), -- 角色管理按钮 (REPLACE(UUID(),'-',''), @system_id, @menu_sys, '新增角色', NULL, NULL, '', 3, 1, 1, 'system:role:add'), (REPLACE(UUID(),'-',''), @system_id, @menu_sys, '编辑角色', NULL, NULL, '', 3, 2, 1, 'system:role:edit'), (REPLACE(UUID(),'-',''), @system_id, @menu_sys, '删除角色', NULL, NULL, '', 3, 3, 1, 'system:role:delete'), -- 订单管理按钮 (REPLACE(UUID(),'-',''), @system_id, @menu_biz, '新增订单', NULL, NULL, '', 3, 1, 1, 'biz:order:add'), (REPLACE(UUID(),'-',''), @system_id, @menu_biz, '编辑订单', NULL, NULL, '', 3, 2, 1, 'biz:order:edit'), (REPLACE(UUID(),'-',''), @system_id, @menu_biz, '删除订单', NULL, NULL, '', 3, 3, 1, 'biz:order:delete'); -- 4. 创建角色(用变量持有角色 UUID,供后续步骤 5 分配用户时引用) SET @role_admin = REPLACE(UUID(),'-',''); SET @role_user = REPLACE(UUID(),'-',''); INSERT INTO sys_role (id, system_id, role_name, role_code, description, status) VALUES (@role_admin, @system_id, '管理员', 'admin', '拥有全部权限', 1), (@role_user, @system_id, '普通用户', 'user', '拥有基本权限', 1); -- 5. 将本系统所有菜单(含按钮权限)分配给管理员角色 INSERT INTO sys_role_menu (id, role_id, menu_id) SELECT REPLACE(UUID(),'-',''), @role_admin, m.id FROM sys_menu m WHERE m.system_id = @system_id; -- 6. (可选)为普通用户分配菜单——根据实际需求手动给 user 角色分配菜单 -- INSERT INTO sys_role_menu (id, role_id, menu_id) -- SELECT REPLACE(UUID(),'-',''), @role_user, m.id -- FROM sys_menu m WHERE m.system_id = @system_id -- AND m.menu_type != 3; -- 普通用户只分配页面菜单,不给按钮权限 ``` > **💡 普通用户 (user) 角色**:初始化 SQL 只给管理员分配了菜单,普通用户角色默认没有任何菜单权限。这是因为不同系统对普通用户的权限要求不同,无法预置。接入后请登录管理后台,在「角色管理」中手动为 `user` 角色分配菜单。或者取消上面第 6 步的注释,给普通用户分配所有页面菜单(不含按钮)。 --- ## 七、创建网关管理员账号 执行以下 SQL 创建一个应用系统管理员账号,该管理员只能看到自己绑定系统的数据: ```sql -- ============================================ -- 创建应用系统管理员账号 -- 角色编码: gateway-app-admin (只能看到自己绑定系统的数据) -- ============================================ -- 1. 创建管理员用户(密码 'admin123' 的 BCrypt 密文,建议自行生成替换) INSERT INTO sys_user (id, username, password, real_name, email, phone, status, is_deleted) SELECT REPLACE(UUID(),'-',''), 'admin_{systemCode}', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', '{系统名称}管理员', 'admin@{systemCode}.com', NULL, 1, 0 FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_user WHERE username = 'admin_{systemCode}'); -- 2. 绑定管理员到网关系统 (gateway,用于登录管理后台) INSERT INTO sys_user_system (id, user_id, system_id) SELECT REPLACE(UUID(),'-',''), u.id, s.id FROM sys_user u, sys_system s WHERE u.username = 'admin_{systemCode}' AND s.system_code = 'gateway' AND NOT EXISTS ( SELECT 1 FROM sys_user_system us WHERE us.user_id = u.id AND us.system_id = s.id ); -- 3. 分配 gateway-app-admin 角色(管理后台权限,只能看到自己绑定系统的数据) INSERT INTO sys_user_role (id, user_id, role_id) SELECT REPLACE(UUID(),'-',''), u.id, r.id FROM sys_user u, sys_role r, sys_system s WHERE u.username = 'admin_{systemCode}' AND r.role_code = 'gateway-app-admin' AND r.system_id = s.id AND s.system_code = 'gateway' AND NOT EXISTS ( SELECT 1 FROM sys_user_role ur WHERE ur.user_id = u.id AND ur.role_id = r.id ); -- 4. 绑定管理员到自己的业务系统(用于操作本系统的用户/角色/菜单) INSERT INTO sys_user_system (id, user_id, system_id) SELECT REPLACE(UUID(),'-',''), u.id, s.id FROM sys_user u, sys_system s WHERE u.username = 'admin_{systemCode}' AND s.system_code = '{systemCode}' AND NOT EXISTS ( SELECT 1 FROM sys_user_system us WHERE us.user_id = u.id AND us.system_id = s.id ); -- 5. 分配本系统的管理员角色(admin,拥有本系统全部菜单权限) INSERT INTO sys_user_role (id, user_id, role_id) SELECT REPLACE(UUID(),'-',''), u.id, r.id FROM sys_user u, sys_role r, sys_system s WHERE u.username = 'admin_{systemCode}' AND r.role_code = 'admin' AND r.system_id = s.id AND s.system_code = '{systemCode}' AND NOT EXISTS ( SELECT 1 FROM sys_user_role ur WHERE ur.user_id = u.id AND ur.role_id = r.id ); ``` > ⚠️ **如何生成 BCrypt 密文**: > > **Python(最方便)**: > ```bash > python -c "import bcrypt; print(bcrypt.hashpw(b'your_password', bcrypt.gensalt()).decode())" > ``` > 如果没装 bcrypt 库:`pip install bcrypt`(Windows 用 `pip install bcrypt`,Linux 用 `pip3 install bcrypt`)。 > > **Java**: > ```java > String hash = BCrypt.hashpw("your_password", BCrypt.gensalt()); > ``` > > **在线工具**(仅测试环境):https://www.bcryptcalculator.com > > 如果已创建过用户,跳过步骤 1,只执行步骤 2-5。 --- ## 八、UserChip Widget 一键接入 在页面右上角添加用户头像和下拉菜单,引入网关提供的 UserChip Widget,按以下三步操作。 > 🚫 **常见错误:`process is not defined`** > 直接在 `index.html` 中写 `