================================================================================ EasyGoAdmin 后端开发者文档 · 14 核心模块实现详解 ================================================================================ 对高频改动/机制代表性最强的模块,给出实现级细节(其余标准 CRUD 模块遵循 04 章 ModuleSpec 模式,不再逐个展开)。代码位置均在 internal/logic/。 -------------------------------------------------------------------------------- 1. index 个人中心(index.go) -------------------------------------------------------------------------------- 1.1 GetUser(GET /api/index/getUser) 数据来源与安全边界: * 用户行经 dao.User.Ctx(自动过滤软删/租户);password/salt 永不返回。 * Roles/Perms 优先取 JWT 中间件注入的 ctx(与鉴权同源),为空时回源 LoadIdentity;超管强制 perms=["*:*:*"](前端据此放开全部按钮)。 * 头像经 upload.GetFileUrl 补域名;城市编码四列 (province/city/district/street_code)组装为 city[] 数组;city_info 为空时用 ResolveCityNames 按编码回填名称。 1.2 GetMenus / GetMenusById(两条路由共用 getMenuTree) * 仅取 type=0(目录/菜单)且 status=0(启用)且 hide=0 的节点—— 按钮权限节点不进菜单树,由 GetUser.permissions 下发。 * 超管全量;其余 user→user_role→role_role_menu→menu.id IN(...) 授权过滤; 无角色返回空树([] 非 null)。 * 挂树规则:父节点不在授权集合内(未授权/顶级)一律挂根,避免菜单丢失; 排序 sort,id(根级再按 sort+id 兜底稳定排序)。 1.3 UpdatePassword 只取 id/password/salt 三列 → 旧密码 IsBcrypt+VerifyPassword 校验(1012)→ 事务外 GenerateSalt+HashPassword → Update 覆盖 password/salt (update_user/time 由 hooks 盖章)。改密后未强制下线(令牌仍有效至过期), 如需立即失效可配合在线用户强退。 1.4 UpdateProfile 头像 SaveFileField(avatar, "user") 临时转正;城市优先由编码数组经 ResolveCityNames 解析名称覆盖 city_info;四列编码经 cityCodeAt(req.City, i) 取值写入;不触达 username/password/tenant 等敏感列。 -------------------------------------------------------------------------------- 2. role 角色(role.go) -------------------------------------------------------------------------------- 2.1 MenuList(GET /api/role/menu/list/:roleId) 返回全量菜单树(type 0/1 均含,供权限勾选界面展示目录+菜单+按钮三层), 每节点带 Checked=role_menu 是否含该 id(checkedSet 预构建)。 2.2 SavePermission(POST /api/role/menu/save) 语义为“全量重写”:先 Delete 该角色全部 role_menu(软删)→ 逐个 Insert req.MenuIds → clearRoleIdentityCache(roleId)。 缓存清理链:按 roleId 查受影响 user_id 列表 → 逐个 Del easygoadmin:user:identity:{uid}(见 15 章缓存清单)。 2.3 data_scope 字段链路(数据范围,机制总览见 03 章第 6 节): roleToData:Add/Update 均带 data_scope,入参 ≤0 归一为 1(全部); roleSpec.SerializeMaps = {"data_scope": "role_data_scope"} → 列表行 dataScopeText 由字典填充; AfterUpdate(roleClearCache)在数据范围变更后失效该角色全部用户 身份缓存——下一次请求 LoadIdentity 重取 min(data_scope) 即生效。 2.4 其他:删除守卫(存在用户引用则拒绝,1014/1011 语义)。 已知偏差(如实记录):role 的 spec 唯一预检当前声明为 UniqueFields{Field:"code"}(未带 Group),而索引 uk_tenant_code 是 “租户内唯一”——预检范围偏宽,超管为不同租户创建相同角色编码时会被 误报“已存在”(数据库层不会冲突)。修复方向:补 Group:"tenant_id", 使预检范围等于索引范围(见 04 章 4.4 与 13 章 Q11)。 -------------------------------------------------------------------------------- 3. menu 菜单(menu.go) -------------------------------------------------------------------------------- * List 树形(IsTree+ParentOf);删除守卫 menuDeleteGuard:存在子菜单拒绝(1011)。 * 关联清理:删除菜单时同步清理 role_menu 引用(menuCleanRelations), 批量删除同理——防止悬挂授权。 * 父级校验 menuParentGuard:parentId 不得指向自身或后代(防环)。 * type 语义:0=菜单/目录(进菜单树),1=权限节点(进 Perms 校验); status 0=启用;hide 0=显示(hide=1 不进菜单树但权限仍生效需注意—— getMenuTree 过滤 hide=0,LoadIdentity 过滤 hide=0,故 hide=1 权限节点 不生效,配置时勿用)。 -------------------------------------------------------------------------------- 4. user 用户(user.go,机制最全的模块) -------------------------------------------------------------------------------- 4.1 唯一校验(手写,按行现租户) usernameExists / mobileExists 按 (username|mobile, tenant_id, 排除自身) 查; Update 时先读被改行的 tenant_id 再校验——校验范围必须等于 uk_tenant_username / uk_tenant_mobile 索引范围(含 del_flag 由 dao.Ctx 恒过滤保证)。历史教训:用 GetTenantID(超管=默认租户)会漏报 1062。 4.2 AssignRoles(userId, roleIds) 全量重写 user_role(先删后插)→ clearIdentityCache(userId)。 Add/Update 均经此方法同步角色。 4.3 checkTenantUserMax(tenantId) 统计租户现有用户数 vs tenant.max_user_num,超限返回 1007。 4.4 genUserCode(tenantId) 前缀=租户 code(平台/超管为 "SYS"),序号=该租户现有用户数+1, 格式 "{prefix}-%06d"(如 T0001-000001)。 4.5 Import(POST /api/user/import) excel.ReadSheet → 表头映射 → 逐行:唯一校验/租户上限/默认密码 bcrypt (预计算)/角色与城市解析 → 批量插入 → 返回导入成功/失败统计。 城市列经 ResolveCityCodes(名称串) 转编码入库。 4.6 Export:与列表同筛选(userFilter + DataScope,applyFilters 同链) → ChunkQuery 500/页 → excel.WriteSheet(导出可见集=列表可见集)。 4.7 userExpandNames:列表行批量补 dept/level/position/role 名称与城市文本 (namesById/dictName 批查防 N+1)。 4.8 DataScope 接线(数据权限,机制总览见 03 章第 6 节): userSpec 声明 DataScope: ApplyDataScope("dept_id")——user 是当前 唯一接线模块(全库仅 user 表有 dept_id 列)。Update 改 dept_id 后 AssignRoles 无条件 clearIdentityCache,部门变更即时生效(无需等 300s)。 -------------------------------------------------------------------------------- 5. tenant 租户(tenant.go) -------------------------------------------------------------------------------- 5.1 编码规则 T%04d(自增 ID 回填) code 为 NOT NULL + 唯一键,插入时无法预知 ID → 先写临时占位编码 (T%010d(纳秒取模)),Insert 后按 LastInsertId 修正为 "T"+4位ID。 Add 与 Account 均遵循此模式;显式传 code 时查重后直接使用不回填。 5.2 Account(POST /api/tenant/account)开户事务 bcrypt 在事务外预计算(短事务纪律)→ WithTrans 内三段原子写: ① Insert 租户(status=1,hooks 填审计列) ② 修正 code=T%04d(tenantId) ③ 以 tenantCtx(WithTenantID(txCtx, tenantId))Insert 初始管理员: code="{code}-000001"(首用户序号),tenant_id 显式传入,password/salt 为预计算值 返回 {tenantId, code, userId}。角色分配未含在开户事务内(按需另行调用)。 5.3 tenantFilter / fillUserCount / clearCache 列表为平台专属视图(超管可见全部,带每租户用户数统计); 状态/过期变更后 Del tenant:info:{id}(Tenant 中间件 60s 摘要缓存)。 -------------------------------------------------------------------------------- 6. dict / dict_item 字典(dict.go、dict_item.go) -------------------------------------------------------------------------------- 6.1 两级缓存 Redis 层:easygoadmin:dict:item:{code} = 字典项 JSON 数组,TTL 3600s (setCache;getCache 命中直接返回,供前端下拉等高频读取)。 进程层:base.go dictTextCache({code}:{value}→name),TTL 300s (DictGetText 消费,供 {field}Text 填充)。 6.2 RefreshCache(GET /api/dict/refreshCache) 重建 Redis 层:可按 code 单字典或全量重查 dict_item → 按字典编码分组 → 逐 code setCache。注意:只刷 Redis 层,进程内 dictTextCache 仍按 300s 自然过期(重启/多实例各自过期)。 6.3 别名路由(/api/dictionary/list|info,登录即可)供前端通用字典组件; dict_item 快捷路由 getDictItemList/:code 走 Redis 缓存。 6.4 dict_item 无 list 标准接口(页面用 page);删除字典项不主动清缓存 (最长 1 小时延迟),关键场景调 RefreshCache。 -------------------------------------------------------------------------------- 7. city 城市(city.go) -------------------------------------------------------------------------------- 7.1 双向编解码(internal/logic 包级函数,user/index 模块复用) ResolveCityNames(ctx, codes[]string, separator) string area_code IN(...) 批查 → 按入参顺序拼名称串(空编码跳过)。 ResolveCityCodes(ctx, cityInfo string) [4]string 名称串按逗号拆 4 级,逐级 parent 约束消歧: 省:pid=0 AND name=? → 市:parent_code=省area_code AND name=? → 区 → 街道,逐级依赖上级结果;任一级失败后续留空。 例:"江苏省,南京市,建邺区" → [320000000000, 320100000000, 320105000000, ""] 7.2 查询接口:ByParent(parent_code)/GetCityList(:parentCode) 级联懒加载、 AreaProvince(pid=0 省份列表),均登录即可(前端级联选择器高频调用)。 7.3 大表注意:勿全表 List;筛选走 area_code/parent_code/pid。 -------------------------------------------------------------------------------- 8. config 配置(config.go)与 param 参数 -------------------------------------------------------------------------------- * config 为“配置分组”,config_item 为键值明细(config_id 关联,value/options/type)。 * 网站设置页专用:GET /api/config/web/index 聚合读取、POST /api/config/web/save 批量保存(perm sys:configWeb:*),save 为覆盖式写入。 * param 为全局键值参数(code 唯一),与 config_item 的区别:param 面向 运行参数快速读写,config 面向结构化表单配置。 -------------------------------------------------------------------------------- 9. 模块机制代表速查 -------------------------------------------------------------------------------- 树形模块 menu / dept / category(IsTree+ParentOf+删除子级守卫) 导入导出模块 user / level / example(excel + ChunkQuery) 文件/富文本模块 article(cover 单图、images 多图、content 富文本)、user(avatar)、 tenant(logo)、example 无表模块 auth / index / online_user / server_monitor / cache_monitor / generator / upload 控制密集模块 job(调度挂载)、role(权限缓存)、tenant(开户事务)、 user(唯一/上限/角色)、dict(两级缓存) -------------------------------------------------------------------------------- 10. 唯一预检覆盖现状(已知偏差汇总) -------------------------------------------------------------------------------- 当前 spec 声明了 UniqueFields 的模块仅两处: role {Field:"code"}(无 Group —— 预检范围偏宽,见第 2.3 节偏差说明) param {Field:"code"}(与全局 uk_code 一致,无偏差) user(username/mobile/code)为手写检查(按行现租户,与索引对齐); tenant.Add 手写 code 查重;config/dict/file_template 的 Add 走 CheckUnique (code 全局,三处调用点在各自 Add);三者的 Update 未做预检——改 code 撞 uk_code 时以数据库 1062 兜底。 未做预检、依赖数据库 uk_* 兜底的模块:dept / level / position / category (uk_tenant_name)——重复名称提交会收到 1062 裸错误提示。 修复方向:为四个模块补 UniqueFields{Field:"name", Group:"tenant_id"} (user 名称类除外,已手写),并按 04 章 4.4 保证预检范围=索引范围。