AI 生成 Vue3 后台后状态乱了?Pinia 双树和 Go 权限联动这样查

📅 2026/7/20 7:14:09
AI 生成 Vue3 后台后状态乱了?Pinia 双树和 Go 权限联动这样查
Cursor 或 Agent 生成 Vue3 后台时最容易让人误判的一点是页面能跑不等于状态是干净的。登录态、菜单树、按钮权限、通知、WebSocket、用户资料如果被分散到两套 Pinia 目录里短期看只是“偶尔刷新丢菜单”上线后就会变成权限错乱A 角色看到了 B 角色的菜单按钮隐藏了但接口还能打刷新后前端状态和 Go 后端返回的权限又对不上。这篇不讨论 AI 会不会替代前端也不写工具清单。只看一个能落地的排查问题AI 生成或重构 Vue3 管理后台后怎么检查 Pinia 状态树、动态路由和 Go 后台 RBAC 是否还在同一套规则里。先判断是不是“状态树分叉”很多后台项目会有类似目录src/ store/ modules/ user.ts permission.ts app.ts stores/ backend/ chat.ts notification.ts websocket.ts这不一定马上报错。真正的问题是两个目录可能各自维护登录态、菜单、权限点或连接状态。AI 重构时尤其容易这样做它看到老目录就沿用老目录看到新模块又新增一套模块最后页面上能 import 成功但状态来源已经不唯一。可以先用下面几个命令做静态检查# 1. 找出项目里所有 Pinia store 定义 rg defineStore\( src # 2. 找出谁在读取菜单和按钮权限 rg menu|menus|permission|permissions|button|buttons src/store src/stores src/router src/views # 3. 找出 localStorage / sessionStorage 里是否保存了重复权限字段 rg localStorage|sessionStorage src如果结果里同时出现 src/store/modules/permission.ts 和 src/stores/backend/*就要继续查这些模块是否互相隔离。后台系统里最怕的不是多几个文件而是同一个“当前用户权限”被拆成两份。一个常见故障菜单刷新后变了接口权限没变假设后端给当前用户返回这样的菜单和接口权限{ userId: 1001, roleKeys: [operator], menus: [ {name: UserList, path: /system/user, type: menu}, {name: UserQuery, path: GET /admin/user/list, type: api} ], buttons: [system:user:query] }前端如果只拿 menus 生成动态路由却把 buttons 放到另一套 store按钮权限就容易和路由权限脱节。页面上可能看不到“删除用户”按钮但某个旧组件还从旧 store 里读到了 system:user:delete或者接口层没有用后端 RBAC 再拦一次。上线前至少跑这三类验证# 菜单接口当前角色只能看到用户列表 curl -s http://127.0.0.1:8000/admin/user/menu \ -H Authorization: Bearer operator-token | jq .data.menus # 允许的查询接口应返回 200 curl -i http://127.0.0.1:8000/admin/user/list \ -H Authorization: Bearer operator-token # 不允许的删除接口必须返回 403 curl -i -X DELETE http://127.0.0.1:8000/admin/user/delete?id1001 \ -H Authorization: Bearer operator-token如果第三个请求返回 200前端 store 再整齐也没用。按钮隐藏只能改善交互不能替代后端接口鉴权。Pinia 侧怎么查每个权限字段只能有一个来源我通常会把检查点压到很具体别只看“页面有没有报错”。// src/store/modules/permission.ts import { defineStore } from pinia import { getUserMenus } from /api/system/permission export const usePermissionStore defineStore(permission, { state: () ({ routes: [] as AppRouteRecordRaw[], buttons: [] as string[], apiPerms: [] as string[], loaded: false }), actions: { async loadPermission() { const res await getUserMenus() this.routes buildRoutes(res.data.menus) this.buttons res.data.buttons || [] this.apiPerms res.data.apiPerms || [] this.loaded true }, hasButton(code: string) { return this.buttons.includes(code) }, hasApi(perm: string) { return this.apiPerms.includes(perm) } } })关键不是这段代码长什么样而是它要成为唯一入口。组件里不要再到处写const buttons JSON.parse(localStorage.getItem(buttons) || []) const menus userStore.userInfo.menus const oldPerms backendStore.permissionList这种写法短期最省事也最容易把 AI 生成的补丁变成隐患。建议用搜索把这些旧入口删干净rg permissionList|userInfo\.menus|localStorage\.getItem\(buttons\)|backendStore src每命中一处就确认它是不是仍在参与权限判断。不是的话删掉是的话迁到统一 store。Go 后端也要给前端一个稳定契约前端状态能不能稳定取决于后端接口返回是否稳定。不要让菜单接口今天返回 buttonCodes明天返回 permissions后天又让前端从角色详情里拼。一个比较清楚的返回结构可以这样定type UserPermissionRes struct { UserId uint64 json:userId RoleKeys []string json:roleKeys Menus []MenuItem json:menus Buttons []string json:buttons ApiPerms []string json:apiPerms } type MenuItem struct { Name string json:name Path string json:path ParentId uint64 json:parentId Type string json:type // menu / button / api }接口层再用同一套 ApiPerms 做校验func PermissionMiddleware(r *ghttp.Request) { user : contexts.GetUser(r.Context()) if user nil { r.Response.WriteStatusExit(401) return } perm : strings.ToUpper(r.Method) r.URL.Path ok, err : service.Permission().UserCanAccess(r.Context(), user.Id, perm) if err ! nil { g.Log().Warningf(r.Context(), permission check failed: %v, err) r.Response.WriteStatusExit(500) return } if !ok { r.Response.WriteStatusExit(403) return } r.Middleware.Next() }这样前端只负责“显示什么”后端负责“能不能做”。两边用同一份权限语义但不要互相替代。数据库里也能查出不一致如果怀疑菜单、按钮、接口没有绑在一起可以直接从库里查。下面用一组中性表名示例-- 查某个角色拥有的菜单和按钮 SELECT r.role_key, m.id, m.title, m.type, m.permission_code, m.api_perm FROM admin_role r JOIN admin_role_menu rm ON rm.role_id r.id JOIN admin_menu m ON m.id rm.menu_id WHERE r.role_key operator ORDER BY m.parent_id, m.id; -- 查有接口权限字段但没有挂给任何角色的动作 SELECT m.id, m.title, m.api_perm FROM admin_menu m LEFT JOIN admin_role_menu rm ON rm.menu_id m.id WHERE m.api_perm AND rm.menu_id IS NULL; -- 查按钮权限存在但缺少后端接口映射的记录 SELECT id, title, permission_code FROM admin_menu WHERE type button AND (api_perm IS NULL OR api_perm );这三条 SQL 很适合放进上线前检查。尤其是第三条按钮权限如果没有接口映射前端很可能“看起来管住了”后端却没有真正拦住。AI 重构后再跑一次构建和权限冒烟状态树分叉经常不是 TypeScript 第一时间报错而是在运行时暴露。可以把检查步骤固定下来# 前端类型和构建 pnpm typecheck pnpm build # 后端测试或最小启动 go test ./... go run main.go # 权限冒烟普通角色不能访问管理员动作 curl -i -X DELETE http://127.0.0.1:8000/admin/user/delete?id1 \ -H Authorization: Bearer operator-token日志里至少要能看到权限被拒绝的原因而不是只有一个 500WARN permission denied user_id1001 roleoperator permDELETE /admin/user/delete HTTP/1.1 403 Forbidden如果这里打出 500说明权限异常和业务异常混在一起了。后面排查会很痛苦。什么时候该用代码生成器约束 AIAI 适合补页面、补字段、补测试草稿但后台项目里的确定性最好别全靠提示词。表结构、接口路径、菜单权限、按钮编码、前端 store 入口这些东西越早固定后面越少返工。XYGo Admin 最近一次提交里做过一个很典型的维护动作把 stores/backend 下的 chat、notification、websocket 迁到统一的 store/modules同时修前端 TypeScript 配置。这类改动不炫但它说明一个现实问题后台项目跑久以后真正消耗时间的往往不是再生成一个 CRUD而是把状态、路由、权限、类型这些边界收回来。项目源码可以看这里GitHub - z312193608/xygo-admin: Open-source admin framework built with GoFrame v2 and Vue 3, featuring RBAC, full-stack CRUD code generation, MySQL/PostgreSQL, plugins, and single-binary deployment. · GitHub。我的建议是让 AI 写代码之前先把生成规则和验收表写清楚。比如1. 新模块必须使用 src/store/modules 下的 Pinia store 2. 菜单、按钮、接口权限必须来自同一个权限接口 3. 前端隐藏按钮后后端 DELETE/POST 接口仍要返回 403 4. 新增字段后必须同步表单、列表、查询条件和导入导出 5. 构建、类型检查、curl 权限冒烟全部通过后才合并这比“帮我生成一个后台管理模块”有效得多。AI 可以很快把文件铺出来但能不能上线最后还是看这些边界有没有被验过。再补一层字段同步检查状态树收拢以后还要看 CRUD 二次生成有没有把字段同步完整。AI 很容易只改表单不改列表只改前端类型不改 Go 入参只改数据库字段不补导入导出。结果就是页面能保存搜索条件却查不到或者导入模板里还是旧字段。可以从数据库开始对字段做一次对账。假设新增了 mobile、status、remark 这几个字段先确认表结构里真实存在SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name admin_user AND column_name IN (mobile, status, remark) ORDER BY ordinal_position;再检查后端请求结构体有没有同步rg type .*Req struct|Mobile|Status|Remark internal api app前端也要查四个地方列表列、表单项、搜索条件、导入导出字段。不要只看页面上有没有输入框。rg mobile|status|remark src/views src/api src/types如果你用的是生成器二次同步按钮或命令应该至少更新这些文件api 请求类型 Go 入参/出参结构体 service/logic 保存逻辑 Vue 列表列配置 Vue 表单字段 搜索条件 导入导出模板 权限按钮编码这里最容易出错的是“旧字段还在参与搜索”。比如前端发了 phone后端实际只认 mobile接口不一定报错只是查不到数据。上线前用一条请求把它打出来curl -G http://127.0.0.1:8000/admin/user/list \ -H Authorization: Bearer admin-token \ --data-urlencode mobile13800000000 \ --data-urlencode status1日志里应该能看到最终 SQL 使用的是新字段SELECT * FROM admin_user WHERE mobile ? AND status ? LIMIT 10如果日志里还是 phone ?说明生成后的某一层没有同步。这个问题和 AI 关系不大手写也会犯AI 只是把文件生成得更快让这种不一致更不容易被肉眼发现。发布前检查表最后给一份可以直接复制的检查表[ ] 项目里只有一套 Pinia 权限入口 [ ] 动态路由、按钮权限、接口权限来自同一个后端响应 [ ] localStorage 里没有旧权限字段继续参与判断 [ ] 普通角色访问管理员接口返回 403 [ ] SQL 能查出未绑定角色的接口权限 [ ] 新增字段后列表、表单、搜索、导入导出同步更新 [ ] pnpm typecheck / pnpm build / go test ./... 通过 [ ] 权限拒绝日志能定位 user、role 和 perm如果你正在用 Cursor 或 Agent 生成 GoFrame Vue3 后台建议先跑这张表。页面生成得快是好事但后台系统真正怕的是“看起来能用权限和状态其实各走各的”。