186 lines
13 KiB
Plaintext
186 lines
13 KiB
Plaintext
================================================================================
|
||
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 保证预检范围=索引范围。
|