Go Gin Gorm Casbin权限管理实现 - 2. 使用Gorm存储Casbin权限配置以及`增删改查`
文章目录
- 0. 背景
- 1. 准备工作
- 2. 权限配置以及`增删改查`
- 2.1 策略和组使用规范
- 2.2 用户以及组关系的增删改查
- 2.2.1 获取所有用户以及关联的角色
- 2.2.2 角色组中添加用户
- 2.2.3 角色组中删除用户
- 2.3 角色组权限的`增删改查`
- 2.3.1 获取所有角色组权限
- 2.3.2 创建角色组权限
- 2.3.3 修改角色组权限
- 2.3.4 删除角色组权限
- 2.3.5 验证用户权限
- 3. 测试以及完整代码
- 3.1 casbin_service.go
- 3.2 casbin_service_test.go
- 3.3测试结果
- 4. 结语
0. 背景
Casbin是用于Golang项目的功能强大且高效的开源访问控制库。
强大通用也意味着概念和配置较多,具体到实际应用(以Gin Web框架开发)需要解决以下问题:
- 权限配置的存储,以及
增删改查- Gin框架的中间件如何实现
经过一番摸索实践出经验,计划分为三个章节,循序渐进的介绍使用方法
- Casbin概念介绍以及库使用
- 使用Gorm存储Casbin权限配置以及
增删改查- 实现Gin鉴权中间件
1. 准备工作
接上一章,略改进一下,将
model.conf文件内容存储到go字符串中,最终代码如下:
package mainimport ("log""github.com/casbin/casbin/v2""github.com/casbin/casbin/v2/model"gormadapter "github.com/casbin/gorm-adapter/v3""github.com/glebarez/sqlite""gorm.io/gorm"
)func main() {db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})if err != nil {panic("failed to connect database")}a, err := gormadapter.NewAdapterByDB(db)if err != nil {panic("new gorm adapter error: " + err.Error())}m, err := model.NewModelFromString(`[request_definition]
r = sub, obj, act[policy_definition]
p = sub, obj, act[role_definition]
g = _, _[policy_effect]
e = some(where (p.eft == allow))[matchers]
m = g(r.sub, p.sub) && keyMatch2(r.obj,p.obj) && r.act == p.act`)if err != nil {panic("new model error: " + err.Error())}e, err := casbin.NewEnforcer(m, a)if err != nil {panic("new casbin enforcer error: " + err.Error())}e.LoadPolicy()// 添加策略ok, err := e.AddPolicy("admin", "/api/user", "GET")log.Println("add admin /api/user GET: ", ok, err)ok, err = e.AddGroupingPolicy("leo", "admin")log.Println("add leo to admin group: ", ok, err)e.SavePolicy()ok, err = e.Enforce("leo", "/api/user", "GET")log.Println("leo GET /api/user :", ok, err)ok, err = e.Enforce("leo", "/api/user", "DELETE")log.Println("leo DELETE /api/user :", ok, err)
}
上述代码默认使用的
gormadapter.CasbinRule对应的Go结构和数据库表如下
type CasbinRule struct {ID uint `gorm:"primaryKey;autoIncrement"`Ptype string `gorm:"size:100"`V0 string `gorm:"size:100"`V1 string `gorm:"size:100"`V2 string `gorm:"size:100"`V3 string `gorm:"size:100"`V4 string `gorm:"size:100"`V5 string `gorm:"size:100"`
}

2. 权限配置以及增删改查
以上准备知识,仅了解和casbin基本操作以及如何配合gorm存储到DB中,还需要完善权限,以及提供
增删改查操作
2.1 策略和组使用规范
casbin的policy十分灵活,具体到自己业务使用中,我这里按以下两条规则使用,基本能满足业务需求
- 所有策略只针对角色组设置
- 用户关联到组(一个用户可以有多个组)
如下,两个角色组
admin和user组,admin组能查询和删除用户,user组只能查询用户
| ptype | v0 | v1 | v2 | v3 | v4 | v5 |
|---|---|---|---|---|---|---|
| p | admin | /api/user | GET | |||
| p | admin | /api/user | DELETE | |||
| p | user | /api/user | GET | |||
| … | … | … | ||||
| g | leo | admin | ||||
| g | leo2 | user |
基础代码如下,下一节给
CasbinService添加增删改查功能
package mainimport ("github.com/casbin/casbin/v2""github.com/casbin/casbin/v2/model"gormadapter "github.com/casbin/gorm-adapter/v3""gorm.io/gorm"
)type CasbinService struct {enforcer *casbin.Enforceradapter *gormadapter.Adapter
}func NewCasbinService(db *gorm.DB) (*CasbinService, error) {a, err := gormadapter.NewAdapterByDB(db)if err != nil {return nil, err}m, err := model.NewModelFromString(`[request_definition]r = sub, obj, act[policy_definition]p = sub, obj, act[role_definition]g = _, _[policy_effect]e = some(where (p.eft == allow))[matchers]m = g(r.sub, p.sub) && keyMatch2(r.obj,p.obj) && r.act == p.act`)if err != nil {return nil, err}e, err := casbin.NewEnforcer(m, a)if err != nil {return nil, err}return &CasbinService{adapter: a, enforcer: e}, nil
}
2.2 用户以及组关系的增删改查
2.2.1 获取所有用户以及关联的角色
根据
2.1中示范数据,预期输出
[{"username": "leo","roleNames": ["admin"]
}, {"username": "leo2","roleNames": ["user"]
}]
获取所有用户以及关联的角色代码片段
type User struct {UserName stringRoleNames []string
}// 获取所有用户以及关联的角色
func (c *CasbinService) GetUsers() (users []User) {p := c.enforcer.GetGroupingPolicy()usernameUser := make(map[string]*User, 0)for _, _p := range p {username, usergroup := _p[0], _p[1]if v, ok := usernameUser[username]; ok {usernameUser[username].RoleNames = append(v.RoleNames, usergroup)} else {usernameUser[username] = &User{UserName: username, RoleNames: []string{usergroup}}}}for _, v := range usernameUser {users = append(users, *v)}return
}
2.2.2 角色组中添加用户
// 角色组中添加用户, 没有组默认创建
func (c *CasbinService) UpdateUserRole(username, rolename string) error {_, err := c.enforcer.AddGroupingPolicy(username, rolename)return err
}
2.2.3 角色组中删除用户
// 角色组中删除用户
func (c *CasbinService) DeleteUserRole(username, rolename string) error {_, err := c.enforcer.RemoveGroupingPolicy(username, rolename)return err
}
2.3 角色组权限的增删改查
2.3.1 获取所有角色组权限
根据
2.1中示范数据,预期输出形式
[{"roleName": "admin","url": "/api/user","method": "DELETE"
}, {"roleName": "admin","url": "/api/user","method": "GET"
}, {"roleName": "user","url": "/api/user","method": "GET"
}]
获取所有角色组权限
```go
// (RoleName, Url, Method) 对应于 `CasbinRule` 表中的 (v0, v1, v2)
type RolePolicy struct {RoleName string `gorm:"column:v0"`Url string `gorm:"column:v1"`Method string `gorm:"column:v2"`
}// 获取所有角色组权限
func (c *CasbinService) GetRolePolicy() (roles []RolePolicy, err error) {err = c.adapter.GetDb().Model(&gormadapter.CasbinRule{}).Where("ptype = 'p'").Find(&roles).Errorif err != nil {return nil, err}return
}
2.3.2 创建角色组权限
// 创建角色组权限, 已有的会忽略
func (c *CasbinService) CreateRolePolicy(r RolePolicy) error {// 不直接操作数据库,利用enforcer简化操作err := c.enforcer.LoadPolicy()if err != nil {return err}_, err = c.enforcer.AddPolicy(r.RoleName, r.Url, r.Method)if err != nil {return err}return c.enforcer.SavePolicy()
}
2.3.3 修改角色组权限
// 修改角色组权限
func (c *CasbinService) UpdateRolePolicy(old, new RolePolicy) error {_, err := c.enforcer.UpdatePolicy([]string{old.RoleName, old.Url, old.Method},[]string{new.RoleName, new.Url, new.Method})if err != nil {return err}return c.enforcer.SavePolicy()
}
2.3.4 删除角色组权限
// 删除角色组权限
func (c *CasbinService) DeleteRolePolicy(r RolePolicy) error {_, err := c.enforcer.RemovePolicy(r.RoleName, r.Url, r.Method)if err != nil {return err}return c.enforcer.SavePolicy()
}
2.3.5 验证用户权限
// 验证用户权限
func (c *CasbinService) CanAccess(username, url, method string) (ok bool, err error) {return c.enforcer.Enforce(username, url, method)
}
3. 测试以及完整代码
3.1 casbin_service.go
package mainimport ("github.com/casbin/casbin/v2""github.com/casbin/casbin/v2/model"gormadapter "github.com/casbin/gorm-adapter/v3""gorm.io/gorm"
)/*
按如下约定:1. 所有策略只针对角色组设置2. 用户关联到组(一个用户可以有多个组)
+-------+-------+-----------+--------+----+----+----+
| ptype | v0 | v1 | v2 | v3 | v4 | v5 |
+-------+-------+-----------+--------+----+----+----+
| p | admin | /api/user | GET | | | |
+-------+-------+-----------+--------+----+----+----+
| p | admin | /api/user | DELETE | | | |
+-------+-------+-----------+--------+----+----+----+
| p | user | /api/user | GET | | | |
+-------+-------+-----------+--------+----+----+----+
| ... | ... | ... | | | | |
+-------+-------+-----------+--------+----+----+----+
| g | leo | admin | | | | |
+-------+-------+-----------+--------+----+----+----+
| g | leo2 | admin | | | | |
+-------+-------+-----------+--------+----+----+----+
| g | leo3 | user | | | | |
+-------+-------+-----------+--------+----+----+----+
*/
type CasbinService struct {enforcer *casbin.Enforceradapter *gormadapter.Adapter
}func NewCasbinService(db *gorm.DB) (*CasbinService, error) {a, err := gormadapter.NewAdapterByDB(db)if err != nil {return nil, err}m, err := model.NewModelFromString(`[request_definition]r = sub, obj, act[policy_definition]p = sub, obj, act[role_definition]g = _, _[policy_effect]e = some(where (p.eft == allow))[matchers]m = g(r.sub, p.sub) && keyMatch2(r.obj,p.obj) && r.act == p.act`)if err != nil {return nil, err}e, err := casbin.NewEnforcer(m, a)if err != nil {return nil, err}return &CasbinService{adapter: a, enforcer: e}, nil
}// (RoleName, Url, Method) 对应于 `CasbinRule` 表中的 (v0, v1, v2)
type RolePolicy struct {RoleName string `gorm:"column:v0"`Url string `gorm:"column:v1"`Method string `gorm:"column:v2"`
}// 获取所有角色组权限
func (c *CasbinService) GetRolePolicy() (roles []RolePolicy, err error) {err = c.adapter.GetDb().Model(&gormadapter.CasbinRule{}).Where("ptype = 'p'").Find(&roles).Errorif err != nil {return nil, err}return
}// 创建角色组权限, 已有的会忽略
func (c *CasbinService) CreateRolePolicy(r RolePolicy) error {// 不直接操作数据库,利用enforcer简化操作err := c.enforcer.LoadPolicy()if err != nil {return err}_, err = c.enforcer.AddPolicy(r.RoleName, r.Url, r.Method)if err != nil {return err}return c.enforcer.SavePolicy()
}// 修改角色组权限
func (c *CasbinService) UpdateRolePolicy(old, new RolePolicy) error {_, err := c.enforcer.UpdatePolicy([]string{old.RoleName, old.Url, old.Method},[]string{new.RoleName, new.Url, new.Method})if err != nil {return err}return c.enforcer.SavePolicy()
}// 删除角色组权限
func (c *CasbinService) DeleteRolePolicy(r RolePolicy) error {_, err := c.enforcer.RemovePolicy(r.RoleName, r.Url, r.Method)if err != nil {return err}return c.enforcer.SavePolicy()
}type User struct {UserName stringRoleNames []string
}// 获取所有用户以及关联的角色
func (c *CasbinService) GetUsers() (users []User) {p := c.enforcer.GetGroupingPolicy()usernameUser := make(map[string]*User, 0)for _, _p := range p {username, usergroup := _p[0], _p[1]if v, ok := usernameUser[username]; ok {usernameUser[username].RoleNames = append(v.RoleNames, usergroup)} else {usernameUser[username] = &User{UserName: username, RoleNames: []string{usergroup}}}}for _, v := range usernameUser {users = append(users, *v)}return
}// 角色组中添加用户, 没有组默认创建
func (c *CasbinService) UpdateUserRole(username, rolename string) error {_, err := c.enforcer.AddGroupingPolicy(username, rolename)return err
}// 角色组中删除用户
func (c *CasbinService) DeleteUserRole(username, rolename string) error {_, err := c.enforcer.RemoveGroupingPolicy(username, rolename)return err
}// 验证用户权限
func (c *CasbinService) CanAccess(username, url, method string) (ok bool, err error) {return c.enforcer.Enforce(username, url, method)
}
3.2 casbin_service_test.go
package mainimport ("testing""github.com/glebarez/sqlite""gorm.io/gorm"
)func TestCasbinService(t *testing.T) {db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})if err != nil {panic("failed to connect database")}s, err := NewCasbinService(db)if err != nil {t.Fatalf("new service error: %v", err)}// 创建个用户,分别关联到`admin`和`user`组, `leo2`既是`admin`组又是`user`组t.Logf("create user: leo with group: admin result: %v", s.UpdateUserRole("leo", "admin"))t.Logf("create user: leo with group: admin result: %v", s.UpdateUserRole("leo2", "admin"))t.Logf("create user: leo with group: admin result: %v", s.UpdateUserRole("leo2", "user"))t.Log()t.Logf("users is: %v\n", s.GetUsers())// 针对`admin`和`user`组创建三条策略t.Log("create admin /api/user GET: ", s.CreateRolePolicy(RolePolicy{RoleName: "admin", Url: "/api/user", Method: "GET"}))t.Log("create admin /api/user DELETE: ", s.CreateRolePolicy(RolePolicy{RoleName: "admin", Url: "/api/user", Method: "DELETE"}))t.Log("create user /api/user GET: ", s.CreateRolePolicy(RolePolicy{RoleName: "user", Url: "/api/user", Method: "GET"}))t.Log("all policy is: ")t.Log(s.GetRolePolicy())t.Log("delete admin /api/user GET", s.DeleteRolePolicy(RolePolicy{RoleName: "admin", Url: "/api/user", Method: "GET"}))
}
3.3测试结果


4. 结语
基本的权限模型设计以及操作函数均已设计正常, 下一章开始结合
gin框架设计一个中间件, 实现casbin权限验证
相关文章:
Go Gin Gorm Casbin权限管理实现 - 2. 使用Gorm存储Casbin权限配置以及`增删改查`
文章目录 0. 背景1. 准备工作2. 权限配置以及增删改查2.1 策略和组使用规范2.2 用户以及组关系的增删改查2.2.1 获取所有用户以及关联的角色2.2.2 角色组中添加用户2.2.3 角色组中删除用户 2.3 角色组权限的增删改查2.3.1 获取所有角色组权限2.3.2 创建角色组权限2.3.3 修改角色…...
DNDC模型的温室气体排放分析
DNDC(Denitrification-Decomposition,反硝化-分解模型)是目前国际上最为成功的模拟生物地球化学循环的模型之一,自开发以来,经过不断完善和改进,从模拟简单的农田生态系统发展成为可以模拟几乎所有陆地生态…...
vue、全局前置守卫
需求:在使用商城app的时候,游客(没有登录的用户)可以看到商品信息,当游客点击添加购物车的时候,我们需要把游客“拦”到登录页面,登陆后,才可以添加商品。 游客只可以看得到部分页面…...
OpenWRT、Yocto 、Buildroot和Ubuntu有什么区别
OpenWRT: 用途:OpenWRT 是一个专注于路由器和嵌入式网络设备的Linux发行版。它提供了一个优化的Linux环境,旨在将网络设备变成功能丰富、高度可定制的路由器。 包管理器:OpenWRT 使用 opkg 包管理器,它是一个轻量级的…...
数据挖掘(3)特征化
从数据分析角度,DM分为两类,描述式数据挖掘,预测式数据挖掘。描述式数据挖掘是以简介概要的方式描述数据,并提供数据的一般性质。预测式数据挖掘分析数据建立模型并试图预测新数据集的行为。 DM的分类: 描述式DM&#…...
【RabbitMQ 实战】08 集群原理剖析
上一节,我们用docker-compose搭建了一个RabbitMQ集群,这一节我们来分析一下集群的原理 一、基础概念 1.1 元数据 前面我们有介绍到 RabbitMQ 内部有各种基础构件,包括队列、交换器、绑定、虚拟主机等,他们组成了 AMQP 协议消息…...
2023年 2月3月 工作经历
2月 #pragma make_public(type) 托管C导出传统C类,另一个托管C项目使用不了。传统C类make_public后,就可以使用了。对模板类无效,比如:std::string。 C#线程绑定CPU 我的方案: 假定我们想把 CPU0 设置成专有CPU。 定…...
selenium京东商城爬取
该项目主要参考与:http://c.biancheng.net/python_spider/selenium-case.html 你看完上述项目内容之后,会发现京东登录是一个比较坑的点,selenium控制浏览器没有登录京东,导致我们自动爬取网页被重定向到京东登录注册页面。 因此,我们要单独…...
用pandas处理数据时,使变量能够在不同的Notebook会话页面进行传递,魔法命令%store
【需求来源】 在使用pandas时,有的时候我想将.ipynb文件分开写 其中一个写清洗数据代码另外一个写数据可视化代码 【解决方案】 但是会涉及到变量转移问题,这个时候我通常使用的方法是: 1、在清洗完数据后导出到本地 2、在文件后面增加当…...
选择适合户外篷房企业的企业云盘解决方案
“户外篷房企业用什么企业云盘好?Zoho WorkDrive企业网盘可以帮助户外篷房企业实现文档统一管理、提高工作效率、加强团队协作,并且支持各种文件类型的预览和编辑。” S公司是一家注重管理规范的大型户外篷房企业,已经有10余年的经验。作为设…...
松鼠搜索算法(SSA)(含MATLAB代码)
先做一个声明:文章是由我的个人公众号中的推送直接复制粘贴而来,因此对智能优化算法感兴趣的朋友,可关注我的个人公众号:启发式算法讨论。我会不定期在公众号里分享不同的智能优化算法,经典的,或者是近几年…...
折半+dp之限制转状态+状压:CF1767E
https://vjudge.net/problem/CodeForces-1767E/origin 首先40,必然折半。然后怎么做? 分析性质。每次可以走1步or2步,等价什么?等价任意相邻2个必选一个!然后就可以建图 这个图是个限制图,我们折半后可以…...
如何写出优质代码
(本文转载自其他博主但是个人忘记了出处) 优质代码是什么? 优质代码是指那些易于理解、易于维护、可读性强、结构清晰、没有冗余、运行效率高、可复用性强、稳定性好、可扩展性强的代码。 这类代码不仅能够准确执行预期功能,同时也便于其他开发者理解…...
ChatGLM2-6B的通透解析:从FlashAttention、Multi-Query Attention到GLM2的微调、源码解读
前言 本文最初和第一代ChatGLM-6B的内容汇总在一块,但为了阐述清楚FlashAttention、Multi-Query Attention等相关的原理,以及GLM2的微调、源码解读等内容,导致之前那篇文章越写越长,故特把ChatGLM2相关的内容独立抽取出来成本文 …...
3D人脸生成的论文
一、TECA 1、论文信息 2、开源情况:comming soon TECA: Text-Guided Generation and Editing of Compositional 3D AvatarsGiven a text description, our method produces a compositional 3D avatar consisting of a mesh-based face and body and NeRF-based ha…...
解决问题:可以用什么方式实现自动化部署
自动化部署可以使用多种工具来实现: 脚本编写:可以使用 Bash、Python 等编写脚本来实现自动化部署。例如,可以使用 Bash 脚本来自动安装、配置和启动应用程序。 配置管理工具:像 Ansible、Puppet、Chef、Salt 等配置管理工具可以…...
【数据结构】链表栈
目录: 链表栈 1. 链式栈的实现2. 链表栈的创建3. 压栈4. 弹栈 链表栈 栈的主要表示方式有两种,一种是顺序表示,另一种是链式表示。本文主要介绍链式表示的栈。 链栈实际上和单链表差别不大,唯一区别就在于只需要对链表限定从头…...
Android笔记:Android 组件化方案探索与思考
组件化项目,通过gradle脚本,实现module在编译期隔离,运行期按需加载,实现组件间解耦,高效单独调试。 先来一张效果图 组件化初衷 APP版本不断的迭代,新功能的不断增加,业务也会变的越来越复杂…...
MeterSphere v2.10.X-lts 双节点HA部署方案
一、MeterSphere高可用部署架构及服务器配置 1.1 服务器信息 序号应用名称操作系统要求配置要求描述1负载均衡器CentOS 7.X /RedHat 7.X2C,4G,200GB部署Nginx,实现负载路由。 部署NFS服务器。2MeterSphere应用节点1CentOS 7.X /RedHat 7.X8C,16GB,200G…...
Java进阶篇--网络编程
目录 计算机网络体系结构 什么是网络协议? 为什么要对网络协议分层? 网络通信协议 TCP/IP 协议族 应用层 运输层 网络层 数据链路层 物理层 TCP/IP 协议族 TCP的三次握手四次挥手 TCP报文的头部结构 三次握手 四次挥手 …...
浏览器访问 AWS ECS 上部署的 Docker 容器(监听 80 端口)
✅ 一、ECS 服务配置 Dockerfile 确保监听 80 端口 EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]或 EXPOSE 80 CMD ["python3", "-m", "http.server", "80"]任务定义(Task Definition&…...
谷歌浏览器插件
项目中有时候会用到插件 sync-cookie-extension1.0.0:开发环境同步测试 cookie 至 localhost,便于本地请求服务携带 cookie 参考地址:https://juejin.cn/post/7139354571712757767 里面有源码下载下来,加在到扩展即可使用FeHelp…...
Vue3 + Element Plus + TypeScript中el-transfer穿梭框组件使用详解及示例
使用详解 Element Plus 的 el-transfer 组件是一个强大的穿梭框组件,常用于在两个集合之间进行数据转移,如权限分配、数据选择等场景。下面我将详细介绍其用法并提供一个完整示例。 核心特性与用法 基本属性 v-model:绑定右侧列表的值&…...
【Web 进阶篇】优雅的接口设计:统一响应、全局异常处理与参数校验
系列回顾: 在上一篇中,我们成功地为应用集成了数据库,并使用 Spring Data JPA 实现了基本的 CRUD API。我们的应用现在能“记忆”数据了!但是,如果你仔细审视那些 API,会发现它们还很“粗糙”:有…...
Spring数据访问模块设计
前面我们已经完成了IoC和web模块的设计,聪明的码友立马就知道了,该到数据访问模块了,要不就这俩玩个6啊,查库势在必行,至此,它来了。 一、核心设计理念 1、痛点在哪 应用离不开数据(数据库、No…...
算法岗面试经验分享-大模型篇
文章目录 A 基础语言模型A.1 TransformerA.2 Bert B 大语言模型结构B.1 GPTB.2 LLamaB.3 ChatGLMB.4 Qwen C 大语言模型微调C.1 Fine-tuningC.2 Adapter-tuningC.3 Prefix-tuningC.4 P-tuningC.5 LoRA A 基础语言模型 A.1 Transformer (1)资源 论文&a…...
QT3D学习笔记——圆台、圆锥
类名作用Qt3DWindow3D渲染窗口容器QEntity场景中的实体(对象或容器)QCamera控制观察视角QPointLight点光源QConeMesh圆锥几何网格QTransform控制实体的位置/旋转/缩放QPhongMaterialPhong光照材质(定义颜色、反光等)QFirstPersonC…...
Netty从入门到进阶(二)
二、Netty入门 1. 概述 1.1 Netty是什么 Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients. Netty是一个异步的、基于事件驱动的网络应用框架,用于…...
三分算法与DeepSeek辅助证明是单峰函数
前置 单峰函数有唯一的最大值,最大值左侧的数值严格单调递增,最大值右侧的数值严格单调递减。 单谷函数有唯一的最小值,最小值左侧的数值严格单调递减,最小值右侧的数值严格单调递增。 三分的本质 三分和二分一样都是通过不断缩…...
MySQL 主从同步异常处理
阅读原文:https://www.xiaozaoshu.top/articles/mysql-m-s-update-pk MySQL 做双主,遇到的这个错误: Could not execute Update_rows event on table ... Error_code: 1032是 MySQL 主从复制时的经典错误之一,通常表示ÿ…...
