简介:GoFly快速开发后台管理系统框架是一套面向中后台系统开发者的前后端分离解决方案,基于Go语言与Vue.js技术栈构建,集成总管理系统admin端与业务管理系统business端,并支持SAAS多账号数据分离,适合需要快速搭建云服务或软件服务平台的技术团队,尤其在医疗信息化场景中具备应用价值。压缩包共211个文件,约3.27MB,以141个Go源码文件为核心,辅以14个TypeScript、8个Vue组件、3个SQL脚本及多个yml配置、html页面与zip依赖包,另附说明文档与附赠资源,便于安装配置与二次开发。目前已有181人学习关注。资源涵盖框架核心代码、数据库脚本、配置示例与生成代码工具,读者可据此理解SAAS多租户数据隔离的实现思路,掌握admin与business双端协作的目录结构,并参考医疗行业相关模块进行系统扩展与排错实践。
1. GoFly 中后台框架到底解决了什么问题:从 admin 端到 business 端的双端拆分
很多团队做中后台系统时,第一版往往是一个 admin 端打天下:用户管理、角色权限、业务数据、报表统计全塞在一个工程里。等到第二个客户、第三个客户进来,要求数据隔离、要求独立配置、要求按套餐区分功能,代码就开始失控了。GoFly 这个框架的核心思路,就是把「总管理系统 admin 端」和「业务管理系统 business 端」拆成两个独立进程,admin 端负责租户开通、套餐配置、全局字典和系统级监控,business 端负责具体业务逻辑和租户内的数据操作,两者通过共享的数据库和一套租户标识串联起来。它基于 Go 做后端、Vue 做前端,前后端分离,天然支持 SAAS 多账号数据分离。适合谁?适合正在做医疗、餐饮、教育这类需要多租户隔离的中后台团队,尤其是那些已经感受到「单 admin 端撑不住多客户」的开发者。这一章先把双端架构和 SAAS 数据分离的边界讲清楚,后面再落到具体跑通步骤。
2. 双端架构的选型理由与最小可跑通环境
2.1 为什么 admin 端和 business 端必须拆成两个服务
单服务做 SAAS 最直接的坑是权限模型会打架。admin 端的超级管理员需要跨租户查看所有数据,business 端的租户管理员只能看自己租户内的数据。如果放在同一个进程里,你会在每个接口里写if isSuperAdmin { ... } else { ... },时间一长,漏掉一个判断就是越权漏洞。拆成两个服务后,admin 端只连「平台库」,business 端只连「租户库」或带租户过滤的同一套表,权限边界在进程级别就隔开了。
另一个理由是部署弹性。admin 端访问量低但权限高,business 端访问量随租户数量线性增长。拆开后,business 端可以水平扩展多个实例,admin 端保持单实例或双实例热备即可。GoFly 的常见做法是 admin 端和 business 端共用一套 Go module 里的 model 和 service 基础层,但各自有独立的 main 入口和路由注册文件。
从技术栈看,Go 侧一般用 Gin 或 GoFrame 做 HTTP 层,Vue 侧用 Vue3 + Vite + Pinia + Element Plus。数据库常见 MySQL 8,缓存 Redis,权限用 JWT + Casbin 或框架自带的 RBAC。这些不是 GoFly 强制的,但属于这个方向里最稳的组合。
2.2 本地跑通 admin 端和 business 端的最小命令
先确认 Go 和 Node 环境。Go 建议 1.20 以上,Node 建议 18 LTS。下面是一套通用的初始化流程,具体仓库结构以你拿到的代码为准,但命令顺序和参数含义是通用的。
# 1. 确认基础环境 go version # 期望 go1.20+ node -v # 期望 v18.x npm -v # 期望 9.x # 2. 后端依赖拉取(在项目根目录) go mod tidy # 拉取 go.mod 里声明的所有依赖 go mod download # 预下载到本地模块缓存 # 3. 前端依赖安装(admin 端和 business 端各一次) cd web/admin && npm install cd ../business && npm install # 4. 数据库初始化:先建库,再导入 SQL mysql -u root -p -e "CREATE DATABASE gofly_admin DEFAULT CHARSET utf8mb4;" mysql -u root -p -e "CREATE DATABASE gofly_business DEFAULT CHARSET utf8mb4;" mysql -u root -p gofly_admin < deploy/sql/admin.sql mysql -u root -p gofly_business < deploy/sql/business.sql逻辑说明:go mod tidy会补全缺失依赖并删除未使用的;npm install在两个前端目录分别执行,因为 admin 端和 business 端的依赖清单是独立的。数据库分两个库是 SAAS 数据分离的物理基础,admin 库存平台级配置,business 库存租户业务数据。
参数说明:DEFAULT CHARSET utf8mb4必须显式指定,否则中文和 emoji 会出问题。SQL 文件路径按实际项目调整,常见放在deploy/sql或docs/sql下。导入顺序不能反,admin 库里的租户表要先于 business 库的业务表存在。
2.3 配置文件里必须改的四个参数
后端启动前,配置文件(常见为config.yaml或manifest/config/config.yaml)里有四个参数不改一定翻车。
| 参数 | 含义 | 常见错误值 | 正确做法 |
|---|---|---|---|
database.admin.dsn | admin 库连接串 | 指向 business 库 | 指向 gofly_admin |
database.business.dsn | business 库连接串 | 与 admin 相同 | 指向 gofly_business |
jwt.secret | 双端共享的签名密钥 | 两端不一致 | 两端填同一个值 |
tenant.mode | 租户隔离模式 | 留空 | 填column或database |
jwt.secret两端必须一致,因为 admin 端签发的 token 有时需要被 business 端校验(比如单点登录场景)。tenant.mode决定数据分离是「同库加 tenant_id 列」还是「一租户一库」,医疗类项目因为合规要求常见用database模式,普通项目用column模式性能更好。
启动命令:
# 启动 admin 端(默认 8080) go run cmd/admin/main.go # 另开终端启动 business 端(默认 8081) go run cmd/business/main.go # 前端分别启动 cd web/admin && npm run dev # 默认 5173 cd web/business && npm run dev # 默认 5174两个后端端口不能冲突,两个前端 dev server 端口也不能冲突。Vite 的代理配置里要把/api/admin转发到 8080,/api/business转发到 8081。
3. SAAS 多账号数据分离的落地方式与租户上下文传递
3.1 两种数据分离模式的取舍
SAAS 数据分离常见三种模式:独立数据库、共享数据库独立 schema、共享表加 tenant_id 列。GoFly 这类框架通常支持前两种或第一种加第三种。医疗项目因为数据敏感,一般选独立数据库;普通中后台选共享表加 tenant_id 列,成本低、运维简单。
共享表模式的核心是每个业务表都有tenant_id字段,所有查询必须带这个条件。ORM 层可以用钩子自动注入,但钩子不是万能的,手写 SQL 的地方必须自己加。独立数据库模式则是每个租户一个库,连接池按租户切换,好处是隔离彻底,坏处是租户多了连接数爆炸,需要连接池复用策略。
选型建议:租户数少于 50 且数据敏感,用独立数据库;租户数多且数据敏感度一般,用共享表加 tenant_id。混合模式也有,核心表独立库,日志表共享,但复杂度高,不建议第一版就上。
3.2 租户上下文从登录到查询的完整链路
租户上下文传递是 SAAS 最容易出 bug 的地方。完整链路是:用户登录时,后端根据用户名查出所属 tenant_id,签发 JWT 时把 tenant_id 写进 claims;后续请求经过中间件解析 JWT,把 tenant_id 塞进context.Context;service 层从 context 取出 tenant_id,拼进查询条件。
// 中间件:从 JWT 解析 tenant_id 并注入 context func TenantMiddleware() gin.HandlerFunc { return func(c *gin.Context) { token := c.GetHeader("Authorization") claims, err := ParseToken(token) if err != nil { c.AbortWithStatusJSON(401, gin.H{"msg": "token invalid"}) return } // 关键:tenant_id 必须从 claims 取,不能从请求参数取 tenantID := claims.TenantID if tenantID == 0 { c.AbortWithStatusJSON(403, gin.H{"msg": "tenant missing"}) return } ctx := context.WithValue(c.Request.Context(), "tenant_id", tenantID) c.Request = c.Request.WithContext(ctx) c.Next() } }逻辑说明:tenant_id 只能从服务端签发的 token 里取,绝不能信任前端传的 tenant_id 参数,否则租户 A 可以伪造请求查租户 B 的数据。这是 SAAS 系统最基础的安全底线。
参数说明:claims.TenantID的类型要和数据库tenant_id字段一致,常见是 int64。context.WithValue的 key 建议用自定义类型避免冲突,这里用字符串是为了演示简洁。
3.3 业务查询里 tenant_id 的注入与校验
service 层拿到 tenant_id 后,查询必须显式带上。以 GORM 为例:
func GetPatientList(ctx context.Context, page, size int) ([]Patient, int64, error) { tenantID, ok := ctx.Value("tenant_id").(int64) if !ok || tenantID == 0 { return nil, 0, errors.New("tenant_id missing in context") } var list []Patient var total int64 db := global.DB.Model(&Patient{}).Where("tenant_id = ?", tenantID) db.Count(&total) err := db.Offset((page - 1) * size).Limit(size).Find(&list).Error return list, total, err }逻辑说明:先校验 context 里的 tenant_id 存在且非零,再拼查询条件。Count和Find用同一个db会话,避免条件丢失。分页参数 page 从 1 开始,offset 计算是(page-1)*size。
参数说明:tenant_id字段建议加索引,联合索引(tenant_id, created_at)对列表查询最有效。如果用了软删除,GORM 会自动加deleted_at IS NULL,不用手写。
提示:任何绕过 service 层直接写 SQL 的地方,都要人工检查 tenant_id 条件。这是 SAAS 项目 code review 的必查项。
4. 避坑与排查:双端部署和 SAAS 隔离里最容易翻车的五件事
4.1 现象:business 端能登录但查不到任何数据
原因:JWT 里没有 tenant_id,或者中间件没生效。常见于 admin 端和 business 端用了不同的 jwt.secret,token 解析失败后走了匿名分支。
解决:先确认两端jwt.secret完全一致,再在中间件里打日志确认 claims 内容。如果 claims 里确实没有 tenant_id,检查登录接口签发 token 时有没有把用户的 tenant_id 写进去。
4.2 现象:租户 A 能看到租户 B 的数据
原因:某个查询漏了 tenant_id 条件,或者用了Unscoped()绕过了 GORM 的默认作用域。也可能是缓存 key 没带 tenant_id,导致跨租户命中同一份缓存。
解决:全局搜索所有Find、First、Scan调用,逐个确认 tenant_id 条件。缓存 key 统一加tenant:{id}:前缀。建议写一个单元测试,用两个租户的数据交叉查询,断言结果不重叠。
4.3 现象:admin 端改了套餐配置,business 端不生效
原因:admin 端和 business 端各自缓存了套餐配置,admin 端更新后没有通知 business 端刷新。或者两端读的不是同一张表。
解决:套餐配置这类低频变更数据,建议 business 端每次请求都从库里读,或者用 Redis 发布订阅做失效通知。不要用本地内存缓存,多实例下必然不一致。
4.4 现象:独立数据库模式下新增租户后连接超时
原因:每个租户一个库,连接池没有上限,租户多了之后 MySQL 的 max_connections 被打满。
解决:给租户连接池设上限,比如每个租户最多 5 个连接,用 LRU 策略回收空闲租户的连接。或者改用共享表加 tenant_id 模式,从根上避免连接数问题。
4.5 现象:前端切换租户后页面数据没变
原因:Vue 的 Pinia store 里缓存了上一个租户的数据,切换租户后没有清空。或者 axios 的请求头里还带着旧 token。
解决:切换租户时强制刷新页面,或者手动重置所有 store 并重新拉取用户信息。axios 拦截器里统一从 store 取 token,不要从 localStorage 直接读,避免多标签页不同步。
5. 用一套最小验证脚本确认 SAAS 隔离真的生效
最后一章给一个可复现的验证方法。不要靠肉眼看页面,写一个脚本,用两个租户的账号分别登录,交叉请求对方的数据,断言返回为空或 403。这个脚本跑通,SAAS 隔离才算真正落地。
#!/bin/bash # 用法:./verify_tenant.sh BASE="http://localhost:8081/api/business" # 租户 A 登录 TOKEN_A=$(curl -s -X POST "$BASE/login" \ -H "Content-Type: application/json" \ -d '{"username":"userA","password":"passA"}' | jq -r '.data.token') # 租户 B 登录 TOKEN_B=$(curl -s -X POST "$BASE/login" \ -H "Content-Type: application/json" \ -d '{"username":"userB","password":"passB"}' | jq -r '.data.token') # 用 A 的 token 查列表,记录第一条数据的 id ID_A=$(curl -s "$BASE/patient/list?page=1&size=1" \ -H "Authorization: $TOKEN_A" | jq -r '.data.list[0].id') # 用 B 的 token 去查 A 的那条数据,期望 403 或空 RESULT=$(curl -s "$BASE/patient/detail?id=$ID_A" \ -H "Authorization: $TOKEN_B" | jq -r '.code') if [ "$RESULT" = "403" ] || [ "$RESULT" = "404" ]; then echo "PASS: tenant isolation works" else echo "FAIL: tenant B can access tenant A data, code=$RESULT" fi逻辑说明:脚本先拿两个租户的 token,再用 A 的 token 取一条数据 id,最后用 B 的 token 去访问这条数据。如果隔离生效,应该返回 403 或 404;如果返回 200,说明隔离有漏洞。
参数说明:jq需要提前安装。BASE地址按实际部署调整。code字段是后端统一响应格式里的状态码,常见 200 成功、403 无权限、404 不存在。如果你的项目响应格式不同,改 jq 的取值路径即可。
这个脚本建议加到 CI 里,每次改完权限相关代码都跑一遍。我自己踩过的坑是:某次重构把中间件顺序调了,tenant 中间件跑在鉴权中间件后面,导致 tenant_id 一直是空的,所有租户看到的是全量数据。那个 bug 上线两天才发现,血泪经验就是——隔离验证不能靠人点页面,必须自动化。希望帮到你。
本文还有配套的精品资源,点击获取