大家好,我是 JavaPub。
最近又重新折腾了一下我自己的开源后台项目 ShiyuAdmin。
项目地址:
https://github.com/Rodert/ShiyuAdmin
这次主要做的一件事情,是给项目补上了 SQL Server 支持。

如果只是站在“能不能连接 SQL Server”的角度来看,这件事情其实非常简单。
Go 里面加一个驱动:
gorm.io/driver/sqlserver
然后:
gorm.Open(sqlserver.Open(dsn))
理论上数据库就连上了。
但对于一个已经存在用户、角色、菜单、权限、日志、数据管理、系统监控等模块的后台脚手架来说,真正的问题并不是:
怎么连接 SQL Server?
而是:
怎么在不改业务代码的情况下,让同一套后台同时跑在 PostgreSQL、MySQL、SQLite 和 SQL Server 上?
这就是这次改造比较有意思的地方。
今天就借这个机会,聊聊现在 ShiyuAdmin 的整体架构,以及 SQL Server 是怎么接进去的。
一、先看一下 ShiyuAdmin 是什么
ShiyuAdmin 本身定位并不是一个具体业务系统。
它更像是一套:
Go + React 的通用后台管理脚手架。
目前后端主要使用:
Go
Gin
Gorm
Viper
JWT
Redis
前端则是:
React
TypeScript
Umi Max
Ant Design Pro
ECharts
项目采用标准的前后端分离模式。后端负责 API、权限、数据库和业务逻辑,前端负责管理后台 UI。
项目现在主要包含这些基础能力:
用户管理
角色管理
菜单管理
部门管理
RBAC 权限
JWT 登录认证
动态菜单
超级管理员
操作日志
Redis 管理
系统监控
数据库管理
数据仪表盘
这些东西本质上都是后台系统绕不开的公共能力。
所以我做这个项目一直有一个思路:
新项目不要每次从登录、权限、用户、菜单重新写。
真正做业务的时候,在这个脚手架上继续增加自己的业务模块就可以了。
二、整体架构其实很清晰
现在 ShiyuAdmin 后端目录大概是这样的:
backend/shiyu-admin-backend
├── cmd
│ └── server
├── configs
├── internal
│ ├── api
│ ├── bootstrap
│ ├── config
│ ├── middleware
│ ├── model
│ ├── repository
│ ├── server
│ └── service
├── migrations
├── pkg
│ ├── database
│ ├── redis
│ ├── jwtutil
│ └── logger
└── sql
如果把它抽象一下,大致可以理解成:
React / Ant Design Pro
│
│ HTTP
▼
Gin Router
│
Middleware Layer
JWT / RBAC / Log / Trace
│
▼
API Layer
│
▼
Service Layer
│
▼
Repository Layer
│
▼
GORM
│
┌──────────────┼──────────────┐
▼ ▼ ▼
PostgreSQL MySQL SQL Server
│
SQLite
这个结构看起来并不复杂。
但对后台脚手架来说,其实我反而比较喜欢这种架构。
因为:
后台管理系统最大的敌人不是代码少,而是业务越来越多以后所有东西搅在一起。
三、第一层:API 层只负责 HTTP
比如用户管理接口,API 层应该解决的是:
接收参数
参数校验
调用 Service
返回 JSON
HTTP 状态处理
而不应该在 API Handler 里面直接写:
db.Where(...)
db.Create(...)
db.Delete(...)
否则随着业务越来越多,很快就会出现这种代码:
Handler
├── HTTP
├── SQL
├── 权限
├── 业务判断
├── 数据转换
└── 日志
最后一个接口可能几百行。
所以 ShiyuAdmin 里面真正的业务逻辑主要继续往下走:
API
↓
Service
↓
Repository
四、Service 才是业务核心
Service 层做什么?
举个用户管理的例子。
比如:
创建用户
它并不仅仅对应:
INSERT INTO sys_users
还可能涉及:
检查用户名
检查用户编码
密码 BCrypt 加密
部门是否合法
分配角色
数据权限
记录日志
所以这些东西应该属于 Service。
Service 不应该过于关心:
底层到底是 PostgreSQL
还是 MySQL
还是 SQL Server
这也是这次接 SQL Server 能比较顺利的关键原因之一。
五、Repository 才真正负责数据库
再向下一层就是 Repository。
现在 Server 初始化的时候,会统一创建各种 Repository:
authRepo = repoDB.NewAuthRepository(db)
userRepo = repoDB.NewUserRepository(db)
roleRepo = repoDB.NewRoleRepository(db)
menuRepo = repoDB.NewMenuRepository(db)
deptRepo = repoDB.NewDeptRepository(db)
userRoleRepo = repoDB.NewUserRoleRepository(db)
roleMenuRepo = repoDB.NewRoleMenuRepository(db)
roleDeptRepo = repoDB.NewRoleDeptRepository(db)
operationLogRepo = repoDB.NewOperationLogRepository(db)
dbMetaRepo = repoDB.NewDBMetaRepository(db)
然后再注入对应的 Service。
于是就形成了:
User API
↓
User Service
↓
User Repository
↓
GORM
这时候有一个非常重要的好处出现了。
上面的:
API
Service
Repository
基本不用知道底下是什么数据库。
这就是多数据库架构最关键的一点。
六、SQL Server 到底是怎么接进来的?
这次 SQL Server 接入最核心的位置,其实在:
pkg/database/database.go
数据库统一从:
database.Connect(cfg)
进去。
然后根据:
cfg.Database.Driver
决定加载哪个 GORM Dialector。
现在结构大概就是:
switch cfg.Database.Driver {
case "postgres", "postgresql":
dialector = postgres.Open(dsn)
case "mysql":
dialector = mysql.Open(dsn)
case "sqlite":
dialector = sqlite.Open(...)
case "sqlserver", "mssql":
dialector = sqlserver.Open(dsn)
default:
return nil, errors.Errorf(
"unsupported database driver: %s",
cfg.Database.Driver,
)
}
也就是说:
SQL Server 并没有入侵业务层。
它只是在数据库基础设施层新增了一个 Dialector。
于是上层仍然拿到同样的:
*gorm.DB
后面:
Repository
Service
API
基本不需要发生变化。
我认为这才是一套通用后台应该有的数据库接入方式。
七、为什么我没有到处判断数据库类型
一种比较容易出现的写法是:
if driver == "mysql" {
}
if driver == "postgres" {
}
if driver == "sqlserver" {
}
然后慢慢演变成:
if sqlserver {
...
} else if mysql {
...
} else {
...
}
如果这些判断出现在 Repository、Service 甚至 API 层,整个项目很快就废了。
因为增加一个数据库意味着:
改连接层
改 Repository
改 Service
改 Handler
改初始化
而现在 ShiyuAdmin 的原则更接近:
database.Connect
│
┌─────────────┼─────────────┐
│ │ │
PostgreSQL MySQL SQL Server
│ │ │
└─────────────┼─────────────┘
│
*gorm.DB
│
Repository
│
Service
│
API
数据库差异尽量截止在:
Database / Repository
这一层。
八、SQL Server 的 DSN 也单独处理了
PostgreSQL 的连接方式一般类似:
host=
user=
password=
dbname=
port=
sslmode=
MySQL 又是另外一套:
user:password@tcp(host:port)/database
SQL Server 则使用:
sqlserver://username:password@host:1433
所以项目里没有强行让所有数据库拼同一种 DSN。
SQL Server 使用 URL 形式构造:
dsnURL := &url.URL{
Scheme: "sqlserver",
User: url.UserPassword(
cfg.Database.Username,
cfg.Database.Password,
),
Host: net.JoinHostPort(
cfg.Database.Host,
fmt.Sprintf("%d", cfg.Database.Port),
),
}
然后增加:
query.Set("database", cfg.Database.Database)
以及加密配置:
query.Set("encrypt", cfg.Database.SSLMode)
最后:
sqlserver.Open(dsnURL.String())
这样做还有个好处。
账号密码如果带一些特殊字符,不需要自己疯狂处理:
@
:
/
#
?
URL 编码可以减少很多 DSN 拼接问题。
九、数据库连接池还是统一管理
不管使用哪一种数据库,最终都会得到:
sqlDB, err := db.DB()
然后统一设置连接池:
sqlDB.SetMaxOpenConns(50)
sqlDB.SetMaxIdleConns(10)
sqlDB.SetConnMaxLifetime(30 * time.Minute)
所以现在:
PostgreSQL
MySQL
SQL Server
SQLite
在应用层看到的都是:
GORM
↓
database/sql
↓
数据库驱动
这也意味着后续如果需要做:
连接池配置化
连接超时
慢 SQL
Metrics
数据库健康检查
都可以继续统一放到 Database 层,而不是污染业务代码。
十、配置层也完全独立
SQL Server 已经有单独的配置:
configs/config.sqlserver.yaml
例如:
database:
driver: "sqlserver"
host: "shiyu-sqlserver"
port: 1433
username: "sa"
password: "******"
database: "shiyu_admin_scaffold"
ssl_mode: "disable"
也就是说切换数据库本质变成:
换配置
而不是改业务代码
这点非常重要。
理想情况下,你甚至可以:
CONFIG_FILE=configs/config.sqlserver.yaml
启动 SQL Server 环境。
换一个配置:
CONFIG_FILE=configs/config.mysql.yaml
就是 MySQL。
整个应用架构不变。
十一、Docker Compose 也拆成了四套数据库模式
目前项目已经不只是代码层支持多数据库,部署层也开始分离。
现在分别提供:
docker-compose.yml
docker-compose.mysql.yml
docker-compose.sqlserver.yml
docker-compose.sqlite.yml
对应:
| 数据库 | 使用场景 |
|---|---|
| PostgreSQL | 默认生产环境 |
| MySQL 8.4 | 已有 MySQL 基础设施 |
| SQL Server 2022 | 企业 SQL Server 环境 |
| SQLite | 本地、演示、轻量部署 |
SQL Server 可以直接:
docker compose -f docker-compose.sqlserver.yml up -d
启动。
这一点我觉得比“代码里支持 SQL Server”更重要。
因为一个开源脚手架真正要降低的是:
从 Git Clone 到系统跑起来之间的成本。
十二、为什么我要支持 SQL Server?
很多做互联网项目的同学可能会问:
都 2026 年了,为什么还专门接 SQL Server?
原因其实很现实。
如果只看互联网创业项目:
MySQL
PostgreSQL
确实已经占据了绝大多数场景。
但是到了:
传统企业
制造业
ERP
医院
学校
政府项目
.NET 老系统
内部管理系统
SQL Server 依然非常常见。
尤其很多企业已经存在大量:
SQL Server
Windows Server
.NET
ERP
OA
MES
基础设施。
你不可能跟甲方说:
为了用我的后台框架,请你们先把数据库换成 PostgreSQL。
这显然不现实。
所以对于一套“通用后台脚手架”来说,多数据库支持其实不是炫技。
而是在解决一个非常现实的问题:
降低接入现有企业基础设施的成本。
十三、GORM 在这里的价值就体现出来了
很多人会讨论:
到底要不要 ORM?
如果是一个非常极致的高性能服务,我可能会倾向于:
sqlc
原生 SQL
手写 DAO
但 ShiyuAdmin 的定位不一样。
它是:
通用中后台脚手架。
这里更重要的目标是:
快速开发
CRUD 效率
代码一致性
多数据库适配
维护成本
GORM 在这个场景里其实非常合适。
比如启动的时候项目统一执行:
db.AutoMigrate(
&entity.User{
},
&entity.Role{
},
&entity.Menu{
},
&entity.Dept{
},
&entity.UserRole{
},
&entity.RoleMenu{
},
&entity.RoleDept{
},
&entity.OperationLog{
},
...
)
核心 RBAC 表可以直接根据 Entity 初始化。
所以:
同一套 Entity
↓
GORM
↓
PostgreSQL / MySQL / SQL Server / SQLite
这让多数据库维护成本低了非常多。
十四、RBAC 其实也是整个项目最核心的一层
数据库之外,我觉得 ShiyuAdmin 另一个比较核心的设计就是 RBAC。
目前整体关系可以理解成:
User
│
▼
UserRole
│
▼
Role
│
├────────► RoleMenu ────────► Menu
│
└────────► RoleDept ────────► Dept
也就是:
用户
↓
角色
↓
菜单 / 权限
同时角色还可以继续关联:
部门
数据范围
这也是为什么系统不仅可以做到:
这个用户能不能看到菜单
还可以继续向:
这个用户能不能调用 API
这个角色可以看哪些部门
这个角色可以操作哪些数据
扩展。
项目的 JWT 中还单独加入了:
IsSuperAdmin bool
超级管理员可以直接绕过普通 RBAC 权限检查。普通账号则继续根据用户、角色、菜单关系计算权限。
这个设计对于后台系统很实用。
因为一定要给运维留一条:
“最高权限救场通道”。
十五、应用启动过程其实也体现了架构
现在启动过程大概是:
main.go
↓
加载 Config
↓
server.Run()
↓
初始化 Logger
↓
初始化 Gin
↓
database.Connect()
↓
AutoMigrate()
↓
初始化默认管理员
↓
初始化 RBAC
↓
初始化 Redis
↓
创建 Repository
↓
创建 Service
↓
注册 API
↓
启动 HTTP Server
Server 层实际上扮演的是一个:
依赖组装器。
数据库、Repository、Service 都在这里完成组合。
所以业务代码本身不会到处:
gorm.Open(...)
redis.NewClient(...)
所有基础设施集中初始化。
这一点对于后面做:
Mock
单元测试
替换数据库
增加缓存
增加 MQ
都会比较舒服。
十六、这次 SQL Server 改造,我最满意的并不是“支持 SQL Server”
如果只是:
✔ SQL Server
其实没有什么技术含量。
真正让我觉得这次改造有价值的是:
SQL Server 加进来以后,原来的 API、Service、RBAC 和大部分 Repository 并没有因此被破坏。
换句话说:
增加一个基础设施
没有演变成:
重写一遍业务系统。
这其实就是架构设计真正有价值的地方。
好的架构不是:
看起来用了多少设计模式。
而是:
当需求发生变化的时候,需要改多少地方。
十七、现在这套架构可以继续怎么演进?
接下来其实还有几个方向可以继续完善。
1. 数据库能力矩阵
不同数据库并不是百分百一样。
以后可以做一份明确的:
PostgreSQL ✅
MySQL ✅
SQL Server ✅
SQLite ✅
然后针对:
表注释
字段类型
分页
JSON
全文索引
事务
DDL
元数据查询
分别测试兼容性。
2. 数据库差异继续下沉
未来如果出现:
SQL Server 特有 SQL
PostgreSQL 特有 SQL
MySQL 特有 SQL
不要让它进入 Service。
而应该继续下沉到:
Repository
DBMetaRepository
Dialect Adapter
例如:
DBMetaRepository
│
├── PostgreSQL Metadata
├── MySQL Metadata
└── SQL Server Metadata
这样数据管理、表结构查看这些功能才能真正做到跨数据库。
3. 连接池配置化
现在连接池参数是统一配置的:
MaxOpenConns = 50
MaxIdleConns = 10
ConnMaxLifetime = 30min
未来最好进一步配置化:
database:
max_open_conns: 50
max_idle_conns: 10
conn_max_lifetime: 30m
因为不同部署规模的数据库并不应该使用完全相同的参数。
4. 把多数据库加入 CI
这是我认为非常重要的一步。
以后提交代码的时候最好直接跑:
PostgreSQL Test
MySQL Test
SQL Server Test
SQLite Test
否则:
“理论兼容”
和:
“每次 Commit 都验证兼容”
完全是两回事。
十八、最后
ShiyuAdmin 从一开始我就没有想把它做成一个特别重的“大而全框架”。
我的目标一直比较简单:
把后台项目里重复率最高的那 60%~80% 基础工作提前做好。
例如:
登录
JWT
RBAC
用户
角色
菜单
部门
日志
Redis
监控
数据库
Docker
前端后台
这些东西一旦稳定下来,以后真正开发业务系统的时候,就可以直接:
写业务。
而不是每开一个新项目:
第 1 天:写登录
第 2 天:写用户
第 3 天:写角色
第 4 天:写菜单
第 5 天:写权限
……
这次加入 SQL Server,也是沿着这个方向继续走。
现在底层已经可以覆盖:
PostgreSQL
MySQL
SQL Server
SQLite
我觉得对于一套 Go 后台脚手架来说,数据库层基本已经有了一个比较完整的雏形。SQL Server 2022 也已经有独立的 Docker Compose 部署方案。
后面我还会继续完善:
多数据库兼容
数据权限
代码生成
文件管理
系统监控
部署能力
自动化测试
如果你平时也在做:
Go
Gin
GORM
React
企业后台
RBAC
SQL Server
可以看看这个项目。
services:
shiyu-sqlserver:
image: mcr.microsoft.com/mssql/server:2022-latest
platform: linux/amd64
container_name: shiyu-sqlserver
restart: unless-stopped
environment:
ACCEPT_EULA: "Y"
MSSQL_PID: "Developer"
MSSQL_SA_PASSWORD: "ShiyuAdmin@123"
ports:
- "11433:1433"
volumes:
- shiyu-sqlserver-data:/var/opt/mssql
healthcheck:
test: ["CMD-SHELL", "timeout 2 bash -c '</dev/tcp/127.0.0.1/1433'"]
interval: 10s
timeout: 5s
retries: 15
start_period: 30s
networks: [shiyu-network]
shiyu-sqlserver-init:
image: mcr.microsoft.com/mssql-tools18:latest
platform: linux/amd64
container_name: shiyu-sqlserver-init
restart: "no"
environment:
MSSQL_SA_PASSWORD: "ShiyuAdmin@123"
depends_on:
shiyu-sqlserver:
condition: service_healthy
command:
- /bin/bash
- -c
- >
/opt/mssql-tools18/bin/sqlcmd -S shiyu-sqlserver -U sa -P "$$MSSQL_SA_PASSWORD" -C
-Q "IF DB_ID(N'shiyu_admin_scaffold') IS NULL CREATE DATABASE [shiyu_admin_scaffold]"
networks: [shiyu-network]
shiyu-redis:
image: redis:7
container_name: shiyu-redis
restart: unless-stopped
volumes:
- shiyu-redis-data:/data
command: redis-server --appendonly yes
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks: [shiyu-network]
shiyu-app:
image: ${SHIYU_IMAGE:-ghcr.io/rodert/shiyuadmin:latest}
container_name: shiyu-app
restart: unless-stopped
environment:
TZ: Asia/Shanghai
CONFIG_FILE: configs/config.sqlserver.yaml
ports:
- "18000:80"
depends_on:
shiyu-sqlserver-init:
condition: service_completed_successfully
shiyu-redis:
condition: service_healthy
networks: [shiyu-network]
networks:
shiyu-network:
driver: bridge
volumes:
shiyu-sqlserver-data:
shiyu-redis-data:

我是王仕宇
GitHub:
https://github.com/Rodert/ShiyuAdmin
也欢迎一起提 Issue、PR,或者把它直接拿去作为自己项目的基础骨架。
一起成为勇猛精进的人类。