Files
EasyGoAdmin_GoFrame_EleVue/document/wiki/14_核心模块实现详解.txt
T
2026-10-20 11:37:25 +08:00

186 lines
13 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
================================================================================
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 保证预检范围=索引范围。