seed.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. // RBAC 种子生成器(AC-14)——**从路由表生成,不手写清单**。
  2. //
  3. // ⚠ 两条已验证的结论,改这里之前先读:
  4. //
  5. // 1. casbin 路径必须用 Fiber 的 `:param`,不能用 `*`。
  6. // keyMatch2 里 `*` 会展开成 `.*`(**跨 `/` 匹配**),`/admin/activities/*`
  7. // 会把 `POST /admin/activities/1/sign` 也放行 = **提权**。
  8. // 反证见 TestRBACSeedEnforcedByCasbin:`teacher POST /admin/activities/9/sign` 必须为 false。
  9. // ⚠ 既有 /org 种子用的是 `*`,是否存在同样的跨段放行**尚未核实**,已登记待 S3 核。
  10. //
  11. // 2. 种子从路由表生成、不要手写清单。
  12. // 本批 40 端点,第二批还有 20 个;手写必漏,症状是"接口从 404 变成 403",比 404 更难查。
  13. //
  14. // 依据:unified/03-migration/academic-domain-design.md AC-14 行("种子幂等 + 与路由表 1:1 对齐")/
  15. // `01-architecture/casbin-rbac-design.md` §6(casbin p 规则种子写法:`dom=*`、`keyMatch2` 路径、
  16. // `regexMatch` 方法、`INSERT IGNORE` 幂等)/ `02-data-model/core-model-ddl.sql`(三张表的既有种子格式)。
  17. //
  18. // ---------------------------------------------------------------------------
  19. // 为什么必须"从路由表生成"
  20. // ---------------------------------------------------------------------------
  21. //
  22. // 本任务只挂**第一批 40 个**端点,后面还有第二批(44~63)。手写种子清单在第二批时**一定会漏**,
  23. // 而症状极其隐蔽:接口从 **404** 变成 **403(1003)**——404 一眼看出"没挂",
  24. // 403 看起来像"权限配错",排查者会去查 RBAC 矩阵/角色/token,最后才发现是种子少了一行。
  25. //
  26. // 因此三张表全部由 `Route` 派生:
  27. //
  28. // casbin_rule ← Route.Roles × Route.Path × Route.Method(**1:1** 可反查)
  29. // t_permission ← 每个端点一个 button 权限点(code 由 method+path 推导)
  30. // t_role_permission← Route.Roles × 该端点的权限点 code
  31. //
  32. // ---------------------------------------------------------------------------
  33. // 幂等(重复执行不产生重复行)
  34. // ---------------------------------------------------------------------------
  35. //
  36. // 复用既有 DDL 的 `INSERT IGNORE` 写法(MySQL),依赖三张表已有的唯一键:
  37. //
  38. // casbin_rule UNIQUE uk_policy (ptype,v0,v1,v2,v3)
  39. // t_permission PRIMARY KEY (code)
  40. // t_role_permission UNIQUE uk_role_permission (role_code,permission_code)
  41. //
  42. // SQLite(单测库)没有 `INSERT IGNORE`,改写为 `INSERT OR IGNORE`(同语义、同依赖唯一键)。
  43. // 两种方言都**不做"先查后插"**:先查后插在并发下会插重(两次都查不到),
  44. // 而唯一键 + IGNORE 是数据库级的原子保证。
  45. //
  46. // ---------------------------------------------------------------------------
  47. // 与既有 `/auth`、`/org` 种子的关系(不另起一套)
  48. // ---------------------------------------------------------------------------
  49. //
  50. // 写法完全同构:`ptype=p` / `v0=角色码` / `v1=*`(不区分机构)/ `v2=path` / `v3=method 正则`,
  51. // 同一张 `casbin_rule`、同一套 model.conf,不新增表、不新增列、不改 model。
  52. //
  53. // **唯一的路径写法差异(有意,且更安全)**:既有种子把 `:id` 段写成 `*`
  54. // (如 `/org/institutions/*`),本生成器**原样保留 Fiber 的 `:param`** 写法
  55. // (如 `/admin/activities/:id`)。原因:
  56. // 1. `keyMatch2` 原生支持 `:param`(转成 `[^/]+`),语义与 Fiber 完全一致,无需翻译;
  57. // 2. `*` 在 keyMatch2 里是 `.*`(**跨 `/` 匹配**),`/admin/activities/*` 会连带命中
  58. // `/admin/activities/1/sign`(POST)等本不该给该角色的端点 —— 那是一条**提权**路径。
  59. // 保留 `:param` 后规则与路由表逐字符可对齐,`git diff` 就能审出"这条策略给了谁"。
  60. // 3. 生成器不做"路径翻译"这一步,就不会出现"翻译函数写错、某类路径全放行"的静默缺陷。
  61. package handler
  62. import (
  63. "context"
  64. "fmt"
  65. "sort"
  66. "strings"
  67. "gorm.io/gorm"
  68. )
  69. // CasbinRule `casbin_rule` 的一行(ptype + v0..v3,对齐 `infra/rbac` 的 `ruleRow`)。
  70. //
  71. // 字段语义(casbin model.conf `p = sub, dom, obj, act`):
  72. // - Ptype:策略类型(本生成器恒 `p`;`g` 段留空供将来人级授权);
  73. // - V0:`sub` = 角色码;
  74. // - V1:`dom` = 机构 ID;本批恒 `*`(不区分机构,与既有 /org 种子一致);
  75. // - V2:`obj` = 请求 path(keyMatch2,保留下 Fiber 的 `:param` 写法);
  76. // - V3:`act` = HTTP method 正则(同 path 多 method 合并为 `GET|POST`)。
  77. type CasbinRule struct {
  78. Ptype string
  79. V0 string
  80. V1 string
  81. V2 string
  82. V3 string
  83. }
  84. // PermissionSeed `t_permission` 的一行(权限点字典;本批只生成 `button` 型)。
  85. //
  86. // 为什么只生成 `button` 不生成 `menu`:菜单树是**UI 结构**(`parent_code`/`path`/`icon`/`sort`
  87. // 描述的是"页面怎么摆",与"后端有哪些接口"不是同一件事,路由表推不出层级与图标。
  88. // 硬造 menu 会把菜单树写坏(`/auth/issue` 的 menuTree 直接受影响)。菜单项由前端/产品排期单独提。
  89. type PermissionSeed struct {
  90. Code string
  91. Type string
  92. Name string
  93. ParentCode string
  94. Path string
  95. Icon string
  96. Sort int
  97. Status string
  98. }
  99. // RolePermissionSeed `t_role_permission` 的一行(角色 × 权限点矩阵)。
  100. type RolePermissionSeed struct {
  101. RoleCode string
  102. PermissionCode string
  103. }
  104. // RBACSeed 一次生成的三张表种子(顺序稳定,便于 diff 与单测快照)。
  105. type RBACSeed struct {
  106. // CasbinRules 接口强制策略(`casbin_rule`,第②层)。
  107. CasbinRules []CasbinRule
  108. // Permissions 权限点字典(`t_permission`,第①层展示用)。
  109. Permissions []PermissionSeed
  110. // RolePermissions 角色矩阵(`t_role_permission`)。
  111. RolePermissions []RolePermissionSeed
  112. }
  113. // SeedResult 种子落库结果(幂等判据:第二次执行时 `Inserted*` 全为 0)。
  114. type SeedResult struct {
  115. // CasbinRules 本次生成的策略行数(不是插入行数)。
  116. CasbinRules int
  117. // Permissions 本次生成的权限点行数。
  118. Permissions int
  119. // RolePermissions 本次生成的角色矩阵行数。
  120. RolePermissions int
  121. // InsertedCasbinRules 本次**实际插入**的策略行数(重复行被唯一键忽略,不计入)。
  122. InsertedCasbinRules int64
  123. // InsertedPermissions 本次实际插入的权限点行数。
  124. InsertedPermissions int64
  125. // InsertedRolePermissions 本次实际插入的角色矩阵行数。
  126. InsertedRolePermissions int64
  127. }
  128. // BuildRBACSeed 从路由表生成三张表种子。
  129. //
  130. // 入参:routes []Route(生产传 `Registration.Routes`,保证"挂什么就种什么")。
  131. // 出参:
  132. // - *RBACSeed:三张表的行(顺序稳定);
  133. // - error:权限点 code 冲突(两个端点推导出同一 code)。**冲突必须报错而不是去重**:
  134. // 去重会让某个端点的权限点在矩阵里"看起来配了、实际没有",是典型的静默失效。
  135. func BuildRBACSeed(routes []Route) (*RBACSeed, error) {
  136. seed := &RBACSeed{
  137. CasbinRules: make([]CasbinRule, 0, len(routes)*2),
  138. Permissions: make([]PermissionSeed, 0, len(routes)),
  139. RolePermissions: make([]RolePermissionSeed, 0, len(routes)*2),
  140. }
  141. codes := make(map[string]int, len(routes))
  142. rolePerms := make(map[string]struct{}, len(routes)*2)
  143. casbin := make(map[casbinKey]map[string]struct{}, len(routes))
  144. order := make([]casbinKey, 0, len(routes))
  145. for _, r := range routes {
  146. // ---- t_permission(每个端点一个 button 权限点)
  147. code := permissionCodeFor(r.Method, r.Path)
  148. if prev, ok := codes[code]; ok {
  149. return nil, fmt.Errorf(
  150. "academic seed: 端点 %d 与端点 %d 推导出同一权限点 code %q(端点路径 %s %s / %s)——"+
  151. "请检查 permissionCodeFor 的推导规则,不要用去重掩盖",
  152. prev, r.No, code, r.Method, r.Path, r.Name)
  153. }
  154. codes[code] = r.No
  155. seed.Permissions = append(seed.Permissions, PermissionSeed{
  156. Code: code, Type: "button", Name: r.Name,
  157. ParentCode: "", Path: "", Icon: "",
  158. Sort: r.No, Status: "active",
  159. })
  160. // ---- t_role_permission(角色 × 本端点权限点)
  161. for _, role := range r.Roles {
  162. key := role + "\x00" + code
  163. if _, dup := rolePerms[key]; dup {
  164. continue
  165. }
  166. rolePerms[key] = struct{}{}
  167. seed.RolePermissions = append(seed.RolePermissions, RolePermissionSeed{
  168. RoleCode: role, PermissionCode: code,
  169. })
  170. }
  171. // ---- casbin_rule(角色 × path,多 method 合并)
  172. for _, role := range r.Roles {
  173. key := casbinKey{role: role, path: r.Path}
  174. if _, ok := casbin[key]; !ok {
  175. casbin[key] = make(map[string]struct{}, 2)
  176. order = append(order, key)
  177. }
  178. casbin[key][r.Method] = struct{}{}
  179. }
  180. }
  181. // 按"首次出现顺序"输出(顺序稳定:注册顺序即契约端点顺序),method 按固定序合并。
  182. for _, key := range order {
  183. methods := make([]string, 0, len(casbin[key]))
  184. for m := range casbin[key] {
  185. methods = append(methods, m)
  186. }
  187. sort.Slice(methods, func(i, j int) bool { return methodRank(methods[i]) < methodRank(methods[j]) })
  188. seed.CasbinRules = append(seed.CasbinRules, CasbinRule{
  189. Ptype: "p", V0: key.role, V1: "*", V2: key.path, V3: strings.Join(methods, "|"),
  190. })
  191. }
  192. return seed, nil
  193. }
  194. // casbinKey casbin 规则的去重键(角色 + path;method 合并到 v3)。
  195. type casbinKey struct {
  196. role string
  197. path string
  198. }
  199. // methodRank method 的稳定排序序(让 `GET|POST` 这样的正则可复现,避免 map 遍历顺序抖动)。
  200. func methodRank(method string) int {
  201. switch method {
  202. case "GET":
  203. return 0
  204. case "POST":
  205. return 1
  206. case "PUT":
  207. return 2
  208. case "PATCH":
  209. return 3
  210. case "DELETE":
  211. return 4
  212. default:
  213. return 9
  214. }
  215. }
  216. // permissionCodeFor 从 `method + path` 推导三段式权限点码 `academic.<资源>.<动作>`。
  217. //
  218. // 推导规则(**纯函数、无表驱动**:第二批端点接进来时零改动):
  219. //
  220. // 资源 = 路径去掉一级段(`/admin`|`/student`)与参数段后,字面段用 `_` 连接;
  221. // 若"路径尾是 `/:param/<动词>`",该动词**不计入资源**(它是动作)。
  222. // 动作 = GET 且路径含参数段 → `get`;GET 无参数段 → `list`;
  223. // 非 GET 且尾是 `/:param/<动词>` → 该动词(连字符转下划线);
  224. // 其余 → POST `create` / PUT `update` / DELETE `delete`。
  225. //
  226. // 样例(可对照交付报告的 40 行注册表逐条核):
  227. //
  228. // GET /admin/finance/accounts/:userId → academic.finance_accounts.get
  229. // POST /admin/finance/refunds/custom → academic.finance_refunds_custom.create
  230. // POST /admin/finance/requests/:id/approve → academic.finance_requests.approve
  231. // GET /admin/activities/discounts/usable → academic.activities_discounts_usable.list
  232. // POST /admin/activities/:id/cancel-sign → academic.activities.cancel_sign
  233. // GET /student/bill → academic.bill.list
  234. // PUT /admin/divide-into/members/:userId/quota → academic.divide_into_members.quota
  235. //
  236. // ⚠ **本规则是本任务推导出来的,契约里没有 academic 的权限点码词表**(现有字典只有
  237. // `org.*` / `role.*`)。若产品/前端已有约定词表,只改本函数一处即可全量重算——
  238. // 这正是"从路由表生成"的价值:不存在"改了 40 行漏了 3 行"。
  239. func permissionCodeFor(method, path string) string {
  240. segs := pathSegments(path)
  241. // 去一级段(/admin 或 /student;将来 /pay 同理)。
  242. if len(segs) > 0 {
  243. segs = segs[1:]
  244. }
  245. if len(segs) == 0 {
  246. // 理论上不会出现(本批所有端点都至少两段),兜底成一个稳定的 code。
  247. return "academic.root." + strings.ToLower(method)
  248. }
  249. hasParam := false
  250. for _, seg := range segs {
  251. if isParamSegment(seg) {
  252. hasParam = true
  253. break
  254. }
  255. }
  256. // 尾形态 `/:param/<动词>`:最后两段为"参数 + 字面段"。
  257. tailVerb := ""
  258. if n := len(segs); n >= 2 && isParamSegment(segs[n-2]) && !isParamSegment(segs[n-1]) {
  259. tailVerb = normalizeSegment(segs[n-1])
  260. }
  261. literals := make([]string, 0, len(segs))
  262. for _, seg := range segs {
  263. if isParamSegment(seg) {
  264. continue
  265. }
  266. literals = append(literals, normalizeSegment(seg))
  267. }
  268. action := ""
  269. switch method {
  270. case "GET":
  271. if hasParam {
  272. action = "get"
  273. } else {
  274. action = "list"
  275. }
  276. default:
  277. if tailVerb != "" {
  278. action = tailVerb
  279. // 尾动词是动作,不进资源段(否则会出现 `..._approve.approve`)。
  280. literals = literals[:len(literals)-1]
  281. } else {
  282. switch method {
  283. case "POST":
  284. action = "create"
  285. case "PUT":
  286. action = "update"
  287. case "DELETE":
  288. action = "delete"
  289. default:
  290. action = strings.ToLower(method)
  291. }
  292. }
  293. }
  294. return "academic." + strings.Join(literals, "_") + "." + action
  295. }
  296. // pathSegments 切分路径为段(丢弃前导空段)。
  297. func pathSegments(path string) []string {
  298. trimmed := strings.Trim(path, "/")
  299. if trimmed == "" {
  300. return nil
  301. }
  302. return strings.Split(trimmed, "/")
  303. }
  304. // isParamSegment 判断是否参数段(Fiber 的 `:name` 与可选 `:name?` 均算)。
  305. func isParamSegment(seg string) bool { return strings.HasPrefix(seg, ":") }
  306. // normalizeSegment 归一化字面段:连字符转下划线(`refund-by-activity` → `refund_by_activity`)。
  307. func normalizeSegment(seg string) string { return strings.ReplaceAll(seg, "-", "_") }
  308. // SQL 渲染:与既有 `core-model-ddl.sql` 的种子写法逐字对齐(`INSERT IGNORE INTO ... VALUES`)。
  309. // CasbinRulesSQL 渲染 `casbin_rule` 的 INSERT 语句。
  310. //
  311. // 入参:dialect `mysql` / `sqlite`(决定 `INSERT IGNORE` 还是 `INSERT OR IGNORE`)。
  312. // 出参:单条多值 INSERT(无行时返回空串)。
  313. func (s *RBACSeed) CasbinRulesSQL(dialect string) (string, error) {
  314. if len(s.CasbinRules) == 0 {
  315. return "", nil
  316. }
  317. prefix, err := insertIgnorePrefix("casbin_rule", "`ptype`, `v0`, `v1`, `v2`, `v3`", dialect)
  318. if err != nil {
  319. return "", err
  320. }
  321. rows := make([]string, 0, len(s.CasbinRules))
  322. for _, r := range s.CasbinRules {
  323. rows = append(rows, fmt.Sprintf("(%s)", strings.Join([]string{
  324. sqlString(r.Ptype), sqlString(r.V0), sqlString(r.V1), sqlString(r.V2), sqlString(r.V3),
  325. }, ", ")))
  326. }
  327. return prefix + "\n " + strings.Join(rows, ",\n ") + ";", nil
  328. }
  329. // PermissionsSQL 渲染 `t_permission` 的 INSERT 语句。
  330. func (s *RBACSeed) PermissionsSQL(dialect string) (string, error) {
  331. if len(s.Permissions) == 0 {
  332. return "", nil
  333. }
  334. prefix, err := insertIgnorePrefix("t_permission",
  335. "`code`, `type`, `name`, `parent_code`, `path`, `icon`, `sort`, `status`", dialect)
  336. if err != nil {
  337. return "", err
  338. }
  339. rows := make([]string, 0, len(s.Permissions))
  340. for _, p := range s.Permissions {
  341. parent := "NULL"
  342. if p.ParentCode != "" {
  343. parent = sqlString(p.ParentCode)
  344. }
  345. path := "NULL"
  346. if p.Path != "" {
  347. path = sqlString(p.Path)
  348. }
  349. icon := "NULL"
  350. if p.Icon != "" {
  351. icon = sqlString(p.Icon)
  352. }
  353. rows = append(rows, fmt.Sprintf("(%s)",
  354. strings.Join([]string{
  355. sqlString(p.Code), sqlString(p.Type), sqlString(p.Name),
  356. parent, path, icon, fmt.Sprintf("%d", p.Sort), sqlString(p.Status),
  357. }, ", ")))
  358. }
  359. return prefix + "\n " + strings.Join(rows, ",\n ") + ";", nil
  360. }
  361. // RolePermissionsSQL 渲染 `t_role_permission` 的 INSERT 语句。
  362. func (s *RBACSeed) RolePermissionsSQL(dialect string) (string, error) {
  363. if len(s.RolePermissions) == 0 {
  364. return "", nil
  365. }
  366. prefix, err := insertIgnorePrefix("t_role_permission", "`role_code`, `permission_code`", dialect)
  367. if err != nil {
  368. return "", err
  369. }
  370. rows := make([]string, 0, len(s.RolePermissions))
  371. for _, rp := range s.RolePermissions {
  372. rows = append(rows, fmt.Sprintf("(%s, %s)", sqlString(rp.RoleCode), sqlString(rp.PermissionCode)))
  373. }
  374. return prefix + "\n " + strings.Join(rows, ",\n ") + ";", nil
  375. }
  376. // SQL 渲染三张表的完整种子脚本(可直接追加进 `core-model-ddl.sql`,逐行可 diff)。
  377. func (s *RBACSeed) SQL(dialect string) (string, error) {
  378. casbin, err := s.CasbinRulesSQL(dialect)
  379. if err != nil {
  380. return "", err
  381. }
  382. perms, err := s.PermissionsSQL(dialect)
  383. if err != nil {
  384. return "", err
  385. }
  386. roles, err := s.RolePermissionsSQL(dialect)
  387. if err != nil {
  388. return "", err
  389. }
  390. parts := []string{
  391. "-- academic 域 RBAC 种子(AC-14,**由路由表生成,勿手改**:改路由后重新生成)",
  392. "-- casbin_rule:第②层接口强制(角色 × path × method;dom=* 不区分机构;path 保留 Fiber 的 :param)",
  393. casbin,
  394. "-- t_permission:button 型权限点(菜单树是 UI 结构,不由路由表推导)",
  395. perms,
  396. "-- t_role_permission:角色 × 权限点矩阵",
  397. roles,
  398. }
  399. out := make([]string, 0, len(parts))
  400. for _, p := range parts {
  401. if strings.TrimSpace(p) == "" {
  402. continue
  403. }
  404. out = append(out, p)
  405. }
  406. return strings.Join(out, "\n") + "\n", nil
  407. }
  408. // insertIgnorePrefix 生成各方言的 `INSERT IGNORE` 前缀。
  409. func insertIgnorePrefix(table, columns, dialect string) (string, error) {
  410. switch dialect {
  411. case "mysql":
  412. return "INSERT IGNORE INTO `" + table + "` (" + columns + ") VALUES", nil
  413. case "sqlite":
  414. return "INSERT OR IGNORE INTO `" + table + "` (" + columns + ") VALUES", nil
  415. default:
  416. return "", fmt.Errorf("academic seed: 不支持的方言 %q(仅 mysql/sqlite)", dialect)
  417. }
  418. }
  419. // sqlString 把字符串渲染为 SQL 字面量(单引号转义;本生成器的输入来自路由表,仍做转义以防将来注错)。
  420. func sqlString(s string) string {
  421. return "'" + strings.ReplaceAll(s, "'", "''") + "'"
  422. }
  423. // SeedRBAC 生成并落库 RBAC 种子(**幂等**:重复执行由唯一键 + IGNORE 吸收)。
  424. //
  425. // 入参:
  426. // - ctx context.Context:执行上下文;
  427. // - db *gorm.DB:数据库句柄(方言决定 IGNORE 关键字;仅支持 mysql/sqlite);
  428. // - routes []Route:路由表(生产传 `Registration.Routes`——**挂什么就种什么**)。
  429. //
  430. // 出参:
  431. // - *SeedResult:生成行数 + 实际插入行数(`Inserted*` 全 0 即"已是最新",可直接用于幂等断言);
  432. // - error:权限点 code 冲突 / 方言不支持 / 执行失败。
  433. //
  434. // 装配位置:`cmd/api` 装配层在 `UNIFIED_AUTO_MIGRATE=1`(dev 建表守卫)时调用;
  435. // 生产按"DDL 先行"纪律用本函数渲染的 SQL(见 `SQL`)随迁移脚本执行,不在启动时写权限表。
  436. func SeedRBAC(ctx context.Context, db *gorm.DB, routes []Route) (*SeedResult, error) {
  437. if db == nil {
  438. return nil, fmt.Errorf("academic seed: DB 未注入")
  439. }
  440. seed, err := BuildRBACSeed(routes)
  441. if err != nil {
  442. return nil, err
  443. }
  444. dialect := db.Dialector.Name()
  445. res := &SeedResult{
  446. CasbinRules: len(seed.CasbinRules),
  447. Permissions: len(seed.Permissions),
  448. RolePermissions: len(seed.RolePermissions),
  449. }
  450. if sqlText, serr := seed.CasbinRulesSQL(dialect); serr != nil {
  451. return nil, serr
  452. } else if sqlText != "" {
  453. tx := db.WithContext(ctx).Exec(sqlText)
  454. if tx.Error != nil {
  455. return nil, fmt.Errorf("academic seed: 写 casbin_rule 失败: %w", tx.Error)
  456. }
  457. res.InsertedCasbinRules = tx.RowsAffected
  458. }
  459. if sqlText, serr := seed.PermissionsSQL(dialect); serr != nil {
  460. return nil, serr
  461. } else if sqlText != "" {
  462. tx := db.WithContext(ctx).Exec(sqlText)
  463. if tx.Error != nil {
  464. return nil, fmt.Errorf("academic seed: 写 t_permission 失败: %w", tx.Error)
  465. }
  466. res.InsertedPermissions = tx.RowsAffected
  467. }
  468. if sqlText, serr := seed.RolePermissionsSQL(dialect); serr != nil {
  469. return nil, serr
  470. } else if sqlText != "" {
  471. tx := db.WithContext(ctx).Exec(sqlText)
  472. if tx.Error != nil {
  473. return nil, fmt.Errorf("academic seed: 写 t_role_permission 失败: %w", tx.Error)
  474. }
  475. res.InsertedRolePermissions = tx.RowsAffected
  476. }
  477. return res, nil
  478. }