37 KiB
Mokee Gateway 系统接入完整指南
版本:2026-06-13 | 适用于所有需要通过 Mokee Gateway 统一登录的业务系统
目录
- 架构概览
- 接入流程
- 前端接入(完整代码)
- 后端接入(完整代码)
- API 接口参考(完整请求/响应结构)
- 数据库初始化
- 创建管理员账号
- 创建管理员账号
- UserChip Widget 一键接入
- 对接测试清单
一、架构概览
┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ 业务前端 │────▶│ Mokee Gateway │────▶│ 业务后端 │
│ (Vue/React) │ │ gw.server.zgitm.com │ │ (任意语言) │
│ │ │ │ │ │
│ 携带 Token │ │ 验证 Token │ │ 从请求头读取: │
│ + X-System-Code │ 识别系统编码 │ │ X-User-Id │
│ │ │ 转发请求 │ │ X-User-Name │
│ │ │ 注入用户信息头 │ │ X-System-Code │
└──────────────┘ └──────────────────┘ └──────────────────┘
流程
- 用户访问业务前端 → 无 Token → 跳转统一登录页
- 登录成功 → 获得 JWT Token → 回跳到业务前端
- 业务前端所有 API 请求 → 网关 (携带 Authorization + X-System-Code)
- 网关验证 Token → 注入用户信息请求头 → 转发到业务后端
- 业务后端从请求头读取用户信息 → 执行业务逻辑
三条关键规则
规则一:前端只调网关 — 业务前端 baseURL 配置为网关地址,不能直接调业务后端。
规则二:后端必须开跨域 (CORS) — 业务后端需要允许来自网关域名的跨域请求。
规则三:业务后端不再做登录/鉴权 — Token 验证由网关完成,业务后端只需从请求头读取用户信息。
二、接入流程
- 在网关管理后台注册业务系统 → 获得
systemCode和docToken - 在网关数据库执行初始化 SQL → 创建菜单和角色
- 前端接入:配置 Axios 实例 + 路由守卫
- 后端接入:配置 CORS + UserContext 中间件
- 创建管理员账号 → 登录管理后台维护本系统数据
- 对接测试 → 完成验收
三、前端接入(完整代码)
3.1 安装依赖
npm install axios
3.2 环境变量
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)
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)
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 即可。
// 从 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:
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 中全局注册:
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')
业务代码中使用:
<template>
<!-- 只有拥有 system:user:add 权限的用户才能看到"新增"按钮 -->
<el-button type="primary" v-permission="'system:user:add'">新增用户</el-button>
<el-button v-permission="'system:user:edit'">编辑</el-button>
<el-button type="danger" v-permission="'system:user:delete'">删除</el-button>
</template>
原理:后端返回的
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 工具类
// ====== UserContext.java ======
import java.util.*;
public class UserContext {
private static final ThreadLocal<Map<String, String>> CTX = new ThreadLocal<>();
public static void set(Map<String, String> user) { CTX.set(user); }
public static Map<String, String> 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<String, String> 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<Order> list() {
String userId = UserContext.getUserId(); // "123"
String userName = UserContext.getUserName(); // "张三"
String systemCode = UserContext.getSystemCode(); // "msg"
return orderService.findByUser(userId, systemCode);
}
}
4.3 Python — Flask 中间件
# ====== 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 — 中间件
// ====== 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 配置
@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<T>
{
"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):
{
"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):
{
"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<MenuRouterVO> | 树形菜单路由列表(结构见下表) |
permission |
List<String> | 该系统所有权限标识(用于前端 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<SystemVO>):
[
{
"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
-- 系统编码: {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 创建一个应用系统管理员账号,该管理员只能看到自己绑定系统的数据:
-- ============================================
-- 创建应用系统管理员账号
-- 角色编码: 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(最方便):
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:
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中写<script src="...user-chip.js">会报这个错。原因是 Widget 脚本内部打包了 Vue 3.x,引用了process.env.NODE_ENV,浏览器没有 Node.js 的process全局变量。正确做法是下面的三步接入。
步骤 1:在 index.html 中引入 Widget 样式
<!-- index.html -->
<head>
<link rel="stylesheet" href="https://web.gw.zgitm.com/widgets/user-chip.css" />
</head>
⚠️ 注意:不要在这里引入 JS 脚本,不要在 body 中放占位 div。
步骤 2:在 TopBar 组件中放置占位 div
<!-- src/layouts/components/TopBar.vue -->
<template>
<header class="topbar">
<div class="topbar-left">...</div>
<div class="topbar-right">
<!-- Mokee Gateway UserChip Widget 占位 -->
<div id="mokee-user-chip"
data-api-base="https://gw.server.zgitm.com"
data-cookie-domain=".zgitm.com"></div>
</div>
</header>
</template>
<style scoped>
#mokee-user-chip { flex-shrink: 0; }
</style>
⚠️ 占位 div 必须在 Vue 组件模板中,这样 Vue mount 后 DOM 中才存在该元素。放在
index.html的<body>中无效。
步骤 3:在 main.js 中挂载后动态加载 Widget 脚本
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')
// ⚠️ 关键:必须在 mount 之后加载,且需要 process polyfill
window.process = { env: {} }
const widgetScript = document.createElement('script')
widgetScript.src = 'https://web.gw.zgitm.com/widgets/user-chip.js'
document.body.appendChild(widgetScript)
加载原理
index.html 加载
↓
<link rel="stylesheet" href="user-chip.css"> ← 样式就绪
↓
<script type="module" src="main.js"> ← Vue 入口
↓
createApp → mount('#app') ← Vue 渲染 TopBar,#mokee-user-chip 出现在 DOM
↓
window.process = { env: {} } ← polyfill(防止 process is not defined)
↓
appendChild(<script src="user-chip.js">) ← Widget 脚本执行,找到 #mokee-user-chip 并渲染
组件功能
- 未登录时显示「🔑 登录」按钮,点击跳转统一登录页
- 已登录时显示用户头像 + 用户名,点击弹出下拉菜单
下拉菜单(仅两项)
| 菜单项 | 行为 |
|---|---|
| 🔄 切换系统 | 跳转到系统选择页 |
| 🚪 退出登录 | 清除 Token 并跳转登录页 |
配置属性
| 属性 | 必填 | 默认值 | 说明 |
|---|---|---|---|
data-api-base |
是 | 无 | 网关 API 地址,生产环境为 https://gw.server.zgitm.com |
data-login-url |
否 | 自动推断 | 统一登录页地址,无需配置 |
data-select-url |
否 | 自动推断 | 切换系统页地址,无需配置 |
data-cookie-domain |
否 | 空(仅当前域名) | Cookie 跨域共享域名,跨子域时填 .zgitm.com |
跨子域共享 Token
如果外部系统和网关在不同子域(如 app.zgitm.com 和 web.gw.zgitm.com),需配置 Cookie domain:
<div id="mokee-user-chip" data-cookie-domain=".zgitm.com" ...></div>
排查清单
| 问题 | 检查项 |
|---|---|
| Widget 不显示 | #mokee-user-chip 是否在 Vue 模板中(非 index.html) |
process is not defined |
window.process = { env: {} } 是否在 Widget 脚本加载前执行 |
| 登录状态不识别 | data-cookie-domain=".zgitm.com" 是否配置 |
| 样式错乱 | user-chip.css 是否在 index.html 中正确引入 |
CDN 资源地址
- 样式:
https://web.gw.zgitm.com/widgets/user-chip.css - 脚本:
https://web.gw.zgitm.com/widgets/user-chip.js
九、对接测试清单
- 访问业务前端首页 → 自动跳转到统一登录页
- 使用测试账号登录 → 成功进入业务前端
- 调用
GET /zgapi/v1/auth/user/info→ 返回用户信息 - 调用
GET /zgapi/v1/auth/menu/router?systemCode=xxx→ 返回菜单树 + 权限列表 - 业务后端 Controller 中 UserContext.getUserId() 能获取到用户ID
- 退出登录后访问业务页面 → 重新跳转登录页
- Widget 组件正常显示头像 + 下拉菜单
附录:相关地址
| 服务 | 地址 |
|---|---|
| 统一登录页 | https://web.gw.zgitm.com/login |
| 系统选择页 | https://web.gw.zgitm.com/select |
| API 网关 | https://gw.server.zgitm.com |
| 管理后台 | https://web.gw.zgitm.com/admin |
| 文件服务 | https://file.wg.zgitm.com |
| Widget JS | https://web.gw.zgitm.com/widgets/user-chip.js |
| Widget CSS | https://web.gw.zgitm.com/widgets/user-chip.css |