签入版本
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
# 6.7 文章管理
|
||||
|
||||
:::tip 概述
|
||||
文章模块(goframe_article,租户级表)是文件/富文本字段的代表模块:cover 单图、images 多图、content 富文本([IMG_URL] 持久化契约),配合分类关联与状态/点击量/排序。
|
||||
:::
|
||||
|
||||
## 接口清单
|
||||
|
||||
```text
|
||||
七件套 + PUT /api/article/status(sys:article:status)
|
||||
sys:article:{page|list|detail|add|update|delete|batchDelete|status}
|
||||
```
|
||||
|
||||
## 表特征列
|
||||
|
||||
```text
|
||||
category_id 所属分类(逻辑外键 → goframe_category)
|
||||
cover 封面单图(FileFields)
|
||||
images 多图(MultiFileFields,逗号分隔)
|
||||
content 富文本(ContentFields,[IMG_URL] 占位符契约)
|
||||
status 0-正常 1-下架(注释枚举)
|
||||
click 点击量
|
||||
sort 排序
|
||||
```
|
||||
|
||||
## 声明式文件/富文本处理(spec 模式)
|
||||
|
||||
```go
|
||||
// articleSpec 关键声明(模式示意)
|
||||
var articleSpec = ModuleSpec[...]{ ...,
|
||||
FileFields: []string{"cover"}, // 单图
|
||||
MultiFileFields: []string{"images"}, // 逗号多图
|
||||
ContentFields: []string{"content"}, // 富文本
|
||||
FileSaveDir: "article", // 转正目录
|
||||
}
|
||||
```
|
||||
|
||||
基座自动完成:
|
||||
|
||||
```text
|
||||
写侧(saveTransform):
|
||||
cover/images:temp 路径 → SaveFromTemp 转正到 article/{日期}/
|
||||
content:LocalizeContentFields —— 本站 /temp/ 引用转正、
|
||||
本站正式引用剥离域名 → "[IMG_URL]/相对路径"
|
||||
外站 URL 不处理(防 SSRF)
|
||||
|
||||
读侧(readTransform):
|
||||
cover/images:GetFileUrl 补域名(domainUrl 或 /upload/)
|
||||
content:ExpandContentFields —— "[IMG_URL]" 还原为访问基址
|
||||
```
|
||||
|
||||
详见 [7.3 富文本处理](../tools/rich-text) 与 [7.1 文件上传全流程](../tools/upload)。
|
||||
|
||||
## 分类关联
|
||||
|
||||
```text
|
||||
article.category_id → category.id(树形模块,见 6.10 分类管理)
|
||||
列表可按分类筛选(EqFields);分类删除有引用守卫意识(避免文章悬挂)
|
||||
```
|
||||
|
||||
## 状态与展示
|
||||
|
||||
```text
|
||||
status 0-正常 1-下架:前端列表按状态过滤展示
|
||||
SerializeMaps: status → article_status(字典驱动 {field}Text)
|
||||
click 点击量:详情阅读累加(业务自定义)
|
||||
sort 排序:OrderBy "sort asc" 或前端排序白名单
|
||||
```
|
||||
|
||||
## 管理建议
|
||||
|
||||
+ 富文本图片会被自动本地化(temp → 正式目录)——编辑器直传的图片安全落库
|
||||
+ 外站图片链接保持原样不抓取(设计边界:防 SSRF)
|
||||
+ content 字段含 [IMG_URL] 占位符是库内格式契约——content 不供第三方系统直接消费,读取必须走 Detail/List(基座自动还原)
|
||||
+ cover/images 字段入库为相对 uploadDir 路径,展示层经 GetFileUrl(基座已处理,勿手拼)
|
||||
@@ -0,0 +1,66 @@
|
||||
# 6.18 缓存监控
|
||||
|
||||
:::tip 概述
|
||||
缓存监控是无表模块:解析 Redis INFO 返回内存、客户端、命中率、键空间等指标;内存降级时返回 mode=memory 标识。路由 GET /api/cache/info(登录即可)。
|
||||
:::
|
||||
|
||||
## 接口
|
||||
|
||||
```text
|
||||
GET /api/cache/info 登录即可,log "缓存监控-查询信息"
|
||||
```
|
||||
|
||||
## 返回内容
|
||||
|
||||
```text
|
||||
内存降级(cache.UsingMemory()):
|
||||
→ {mode:"memory", connected:false}(仅返回模式标识,无 INFO 指标)
|
||||
|
||||
Redis 模式:依次 INFO server/memory/stats/clients/keyspace + DBSIZE,
|
||||
解析(parseRedisInfo:# 注释行跳过,k:v 映射):
|
||||
version / uptime / usedMemory / usedMemoryPeak / maxMemory / fragRatio /
|
||||
connectedClients / expiredKeys / evictedKeys / totalCommands / totalConns /
|
||||
hits / misses / hitRate(hits/(hits+misses),两位小数)/
|
||||
keys(DBSIZE)/ keySpace[](每 db 的 keys/expires)
|
||||
|
||||
任一 INFO 失败 → connected=false 返回已解析部分
|
||||
```
|
||||
|
||||
## 关键指标解读
|
||||
|
||||
| 指标 | 健康参考 | 异常含义 |
|
||||
|------|---------|---------|
|
||||
| mode | redis(生产必须) | memory = 已降级,多实例共享能力全失效 |
|
||||
| hitRate | > 0.8 | 偏低 → 键 TTL 过短或缓存未命中业务路径 |
|
||||
| usedMemory | 远小于 maxMemory | 接近 maxmemory → 需扩内存或清理 |
|
||||
| fragRatio | < 1.5 | 偏高 → 内存碎片(考虑碎片整理) |
|
||||
| connectedClients | 稳定 | 持续增长 → 连接泄漏 |
|
||||
| expiredKeys / evictedKeys | 有自然过期 | evictedKeys 持续增长 → maxmemory 淘汰(容量不足) |
|
||||
|
||||
## 与整体架构的关系
|
||||
|
||||
```text
|
||||
缓存分层:进程内(dictTextCache 等)→ Redis(跨实例共享)→ 数据库(唯一真相)
|
||||
本接口观测的是 Redis 层;进程内层不可远程观测(重启即清)
|
||||
|
||||
降级影响面(mode=memory 时):
|
||||
限流计数 / 任务锁 / 在线用户 / JWT 黑名单 / 验证码 / 字典缓存 全部退化单机语义
|
||||
```
|
||||
|
||||
## 运维用法
|
||||
|
||||
```text
|
||||
日常巡检:mode 必须为 redis(生产红线)
|
||||
容量规划:usedMemory/maxMemory 趋势 + evictedKeys 增速
|
||||
命中率治理:hitRate 持续偏低时排查 TTL 策略(见 4.8 缓存架构设计 TTL 原则)
|
||||
连接治理:connectedClients 对照 redis maxActive(连接池配置)
|
||||
```
|
||||
|
||||
## 排障
|
||||
|
||||
| 症状 | 处理 |
|
||||
|------|------|
|
||||
| mode=memory | Redis 不可用——查地址/密码/网络;恢复后自动回 redis(运行期探测) |
|
||||
| connected=false 但 mode=redis | INFO 命令失败(权限/超时);返回已解析部分 |
|
||||
| hitRate=0 | 全部未命中——检查是否有业务直连 g.Redis 绕过 cache 门面 |
|
||||
| 键数量异常多 | redis-cli KEYS 'easygoadmin:*' 抽查;对照键全集(4.8 缓存架构设计) |
|
||||
@@ -0,0 +1,60 @@
|
||||
# 6.10 分类管理
|
||||
|
||||
:::tip 概述
|
||||
分类模块(goframe_category,租户级表)管理文章/商品等树形分类:ModuleSpec 树形声明(IsTree + ParentOf)、租户内名称唯一、删除守卫。是三个树形模块(menu/dept/category)之一。
|
||||
:::
|
||||
|
||||
## 接口清单
|
||||
|
||||
```text
|
||||
七件套:sys:category:{page|list|detail|add|update|delete|batchDelete}
|
||||
List 返回树形(IsTree + ParentOf)
|
||||
```
|
||||
|
||||
## 树形声明(spec 模式)
|
||||
|
||||
```go
|
||||
// categorySpec 树形声明(模式示意)
|
||||
var categorySpec = ModuleSpec[...]{ ...,
|
||||
IsTree: true,
|
||||
ParentOf: func(item *v1.CategoryItem) int { return item.ParentId },
|
||||
// ChildSetter 或 DTO 的 Children 字段(叶子预置 [] 而非 null)
|
||||
}
|
||||
```
|
||||
|
||||
基座 List 末尾自动建树;DTO 必须含 Id 字段;缺 ParentOf 时 fail-fast(启动即报)。
|
||||
|
||||
## 模块特征
|
||||
|
||||
| 特征 | 实现 |
|
||||
|------|------|
|
||||
| 树形 | IsTree + ParentOf,List 建树(叶子 Children 预置空数组) |
|
||||
| 唯一性 | 租户内分类名唯一(uk_tenant_name);未声明预检,1062 兜底 |
|
||||
| 排序 | sort 列(有 sort 列时生成器默认 "sort asc") |
|
||||
| 引用方 | article.category_id(文章归属分类) |
|
||||
|
||||
## 树构建的两条路线
|
||||
|
||||
```text
|
||||
路线一(后端建树,本模块采用):
|
||||
spec.IsTree=true → List 返回树形 JSON → 前端直接渲染树
|
||||
|
||||
路线二(前端建树):
|
||||
后端返回平铺列表 → 前端 buildTree 组装
|
||||
(example 模块即此模式:"data 直接为数组,树形结构由前端 buildTree 构建")
|
||||
|
||||
选择:菜单/部门/分类走后端建树(权限/守卫与树结构耦合);
|
||||
简单展示树可走前端建树(后端更轻)。
|
||||
```
|
||||
|
||||
## 删除守卫与引用
|
||||
|
||||
+ 删除分类前检查子分类(树形守卫,1011 语义)
|
||||
+ 删除被文章引用的分类前,先调整文章归属(避免悬挂引用)
|
||||
+ 批量删除逐条守卫
|
||||
|
||||
## 管理建议
|
||||
|
||||
+ 分类层级控制在 3 层内(文章分类场景足够;更深考虑是否该拆多套分类)
|
||||
+ sort 控制同级排序(间隔 5/10 留出插入空间,生成器同款策略)
|
||||
+ 租户内独立分类体系;跨租户共享分类不在设计内(平台共享行 tenant_id=0 为扩展方向)
|
||||
@@ -0,0 +1,72 @@
|
||||
# 6.9 行政区划管理
|
||||
|
||||
:::tip 概述
|
||||
行政区划模块(goframe_city,平台级大表)管理全国省/市/区/街道/社区数据:树形双键(pid + parent_code)、级联懒加载查询、双向编解码包级函数(user/index 模块复用)。78 万+ 行,查询必须走索引路径。
|
||||
:::
|
||||
|
||||
## 接口清单
|
||||
|
||||
| 方法 | 路由 | 权限 | 说明 |
|
||||
|------|------|------|------|
|
||||
| GET | /api/city/page | sys:city:page | 分页 |
|
||||
| GET | /api/city/list | sys:city:list | 列表 |
|
||||
| GET | /api/city/detail/:id | sys:city:detail | 详情 |
|
||||
| POST | /api/city/add | sys:city:add | 新增 |
|
||||
| PUT | /api/city/update | sys:city:update | 修改 |
|
||||
| DELETE | /api/city/delete/:id | sys:city:delete | 软删 |
|
||||
| DELETE | /api/city/batchDelete | sys:city:batchDelete | 批量删除 |
|
||||
| GET | /api/city/getCityList/:parentCode | 登录即可 | 下级城市(级联懒加载) |
|
||||
| GET | /api/area/getParent | 登录即可 | 省份列表(别名路由) |
|
||||
| GET | /api/area/findByParentCode | 登录即可 | 按父编码查城市(别名) |
|
||||
|
||||
七件套权限 sys:city:*;三个查询别名登录即可(前端级联选择器高频调用)。
|
||||
|
||||
## 表特征
|
||||
|
||||
```text
|
||||
树形双键:pid(父 ID)+ parent_code(父编码)——级联查询走 parent_code 索引
|
||||
area_code 区划编码(唯一业务键,编解码主键)
|
||||
level 0-省份 1-城市 2-县区 3-街道 4-社区(注释枚举)
|
||||
lng/lat/pinyin 经纬度与拼音(检索辅助)
|
||||
平台级表(无租户):全国统一数据
|
||||
```
|
||||
|
||||
## 大表性能纪律(78 万+ 行)
|
||||
|
||||
::: warning 大表注意
|
||||
+ **勿全表 List**——查询走 area_code / parent_code / pid 索引路径
|
||||
+ 级联懒加载 `getCityList/:parentCode` 只取单级子集
|
||||
+ 导入/迁移注意批大小(与整库迁移器 batch-size 同理)
|
||||
:::
|
||||
|
||||
## 双向编解码(包级共享函数)
|
||||
|
||||
```text
|
||||
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, ""]
|
||||
```
|
||||
|
||||
因全部 logic 同包,user/index 直接调用无需 import。user 模块 Add/Update/Import/Detail 已接入:**存库用编码(四列),展示用名称**。
|
||||
|
||||
## 与用户模块的协作流
|
||||
|
||||
```text
|
||||
写侧:前端名称串 → ResolveCityCodes → 四列编码入库
|
||||
(user 表 province/city/district/street_code)
|
||||
读侧:Detail/List → ResolveCityNames(编码) → 名称串还原
|
||||
(city_info 为空时按编码回填)
|
||||
```
|
||||
|
||||
## 管理建议
|
||||
|
||||
+ 区划数据以国家统计口径为准,导入前核对 area_code 唯一性
|
||||
+ 业务代码消费区划一律走 getCityList 级联(勿全量拉取)
|
||||
+ level 枚举新增档位(如街道/社区细分)需同步字典与前端级联深度
|
||||
@@ -0,0 +1,51 @@
|
||||
# 6.8 公告与链接管理
|
||||
|
||||
:::tip 概述
|
||||
内容域还有两个租户级模块:通知公告(goframe_notice)与友情链接(goframe_link)。公告带状态开关,友链无状态列——两者均为标准 CRUD 的典型代表。
|
||||
:::
|
||||
|
||||
## 通知公告(goframe_notice,/api/notice)
|
||||
|
||||
```text
|
||||
七件套 + PUT /api/notice/status(sys:notice:status)
|
||||
sys:notice:{page|list|detail|add|update|delete|batchDelete|status}
|
||||
```
|
||||
|
||||
| 特征 | 说明 |
|
||||
|------|------|
|
||||
| status | 1-正常 2-禁用(注释枚举,字典驱动 {field}Text) |
|
||||
| 租户级 | 各租户独立公告 |
|
||||
| 文件字段 | 支持附件(FileFields 模式) |
|
||||
| 富文本 | 内容字段可按 ContentFields 声明(如启用富文本编辑) |
|
||||
|
||||
## 友情链接(goframe_link,/api/link)
|
||||
|
||||
```text
|
||||
七件套(无 status 接口)
|
||||
sys:link:{page|list|detail|add|update|delete|batchDelete}
|
||||
```
|
||||
|
||||
| 特征 | 说明 |
|
||||
|------|------|
|
||||
| 无 status 列 | 表结构无状态字段,不声明 Status 接口 |
|
||||
| 租户级 | 各租户独立友链 |
|
||||
| URL 字段 | website/site/url 等列不作模糊筛选列(生成器 isUrlField 判定) |
|
||||
|
||||
## 标准 CRUD 模式的教学价值
|
||||
|
||||
两个模块都是"最小差异"的标准形态——新模块开发可直接对照:
|
||||
|
||||
```text
|
||||
差异只体现在:
|
||||
* SerializeMaps(status → notice_status)
|
||||
* LikeFields/EqFields 筛选声明
|
||||
* FileFields/ContentFields(如有文件/富文本列)
|
||||
* 有无 Status 接口(看表有无 status 列)
|
||||
其余七件套结构与 example 模块完全一致(见第 3 章)。
|
||||
```
|
||||
|
||||
## 管理建议
|
||||
|
||||
+ 公告状态用于"草稿/下架"语义(禁用后前端不展示)
|
||||
+ 友链删除无守卫(软删可恢复);批量删除支持勾选操作
|
||||
+ 两者均为租户级——平台统一公告场景需在租户维度分别维护(或扩展平台共享行 tenant_id=0,需业务确认)
|
||||
@@ -0,0 +1,43 @@
|
||||
# 6.4 部门管理
|
||||
|
||||
:::tip 概述
|
||||
部门模块(goframe_dept,租户级表)管理组织架构:树形结构、删除有子级/用户守卫、租户内名称唯一。部门是数据权限(本部门及以下/本部门)的组织基础。
|
||||
:::
|
||||
|
||||
## 接口清单
|
||||
|
||||
```text
|
||||
七件套:sys:dept:{page|list|detail|add|update|delete|batchDelete}
|
||||
List 返回树形(IsTree + ParentOf)
|
||||
```
|
||||
|
||||
## 模块特征
|
||||
|
||||
| 特征 | 实现 |
|
||||
|------|------|
|
||||
| 树形 | IsTree + ParentOf,List 自动建树 |
|
||||
| 删除守卫 | 存在子部门拒绝(1011);存在用户引用拒绝(1014 语义) |
|
||||
| 唯一性 | 租户内部门名唯一(uk_tenant_name) |
|
||||
| 数据权限 | dept_id 是 data_scope 档 2/3 的过滤维度 |
|
||||
| 子树查询 | data_scope.go deptSubtreeIds:拉全租户部门行后 BFS |
|
||||
|
||||
## 与其他模块的关系
|
||||
|
||||
```text
|
||||
user.dept_id → dept.id 用户归属部门(用户表单部门选择)
|
||||
data_scope 档 2/3 按部门维度过滤可见数据(见 5.9 数据权限)
|
||||
deptSubtreeIds(deptId) 档 2 "本部门及以下"的子孙部门集合计算
|
||||
(每次全量拉部门表 BFS,树小无缓存;
|
||||
部门表数百行内无感知,上万行需加短时缓存)
|
||||
```
|
||||
|
||||
## 唯一性说明
|
||||
|
||||
建表有 `uk_tenant_name (tenant_id, name, del_flag)` 唯一索引,但 dept 模块未声明 UniqueFields 预检——重复名称提交会收到数据库 1062 裸错误。修复方向:补 `UniqueFields{Field:"name", Group:"tenant_id"}`(预检范围=索引范围)。
|
||||
|
||||
## 管理建议
|
||||
|
||||
+ 部门层级建议控制在 5 层内(子树 BFS 与前端展示友好)
|
||||
+ 删除部门前先处理子部门与挂载用户(守卫会拦截)
|
||||
+ 未分配部门(dept_id=0)的用户挂数据权限档 2/3 时降级为"仅本人"(防可见面意外放大)
|
||||
+ 部门调整后,受影响用户的数据权限即时生效(user.Update 清身份缓存路径覆盖 dept_id 变更)
|
||||
@@ -0,0 +1,80 @@
|
||||
# 6.19 案例演示模块
|
||||
|
||||
:::tip 概述
|
||||
案例演示模块(goframe_example,tool 域)是官方完整样板:七件套 + 状态切换 + Excel 导入导出 + 文件字段 + 字典填充,前端五件套齐备。第 3 章模块开发实战全程以它为参照。
|
||||
:::
|
||||
|
||||
## 接口清单
|
||||
|
||||
```text
|
||||
七件套 + PUT /api/example/status(sys:example:status)
|
||||
+ POST /api/example/import(sys:example:import)
|
||||
+ GET /api/example/export(sys:example:export)
|
||||
sys:example:{page|list|detail|add|update|delete|batchDelete|status|import|export}
|
||||
```
|
||||
|
||||
## 后端声明(真实代码,摘自 internal/logic/example.go)
|
||||
|
||||
```go
|
||||
// exampleSerializeMaps 枚举字段到字典编码的映射
|
||||
// {field}Text 由基座自动填充
|
||||
var exampleSerializeMaps = map[string]string{
|
||||
"status": "example_status",
|
||||
}
|
||||
|
||||
// exampleSpec 案例演示模块声明
|
||||
//
|
||||
// 采用声明式实现:查询、输出与规则均在声明中定义,方法仅做委托
|
||||
var exampleSpec = ModuleSpec[v1.ExampleItem, v1.ExampleItem, v1.ExampleDetailRes]{
|
||||
Model: dao.Example.Ctx,
|
||||
LikeFields: []string{"name"},
|
||||
EqFields: []string{"type", "status"},
|
||||
OrderBy: "sort, id",
|
||||
SerializeMaps: exampleSerializeMaps,
|
||||
FileFields: []string{
|
||||
"avatar",
|
||||
},
|
||||
FileSaveDir: "example",
|
||||
ToData: exampleToData,
|
||||
}
|
||||
```
|
||||
|
||||
覆盖的基座能力:Like/Eq 筛选、默认排序、字典文本填充、单文件字段两段式、全量表单转换。
|
||||
|
||||
## 扩展方法(手写示范)
|
||||
|
||||
+ **Status**:非标准方法手写实现(dao 直接 Update 单列)
|
||||
+ **Import**:excel.ReadSheet 逐行校验写入,返回成功/失败统计与逐行原因
|
||||
+ **Export**:`exampleSpec.applyFilters` 与列表同套筛选 → DictGetText 枚举转文本 → excel.WriteSheet
|
||||
|
||||
完整代码见 [3.10 完整代码清单](../guide/code-list)。
|
||||
|
||||
## 前端五件套(ui/src/views/tool/example/)
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| index.vue | 列表页(分页表格 + querySchemas 筛选 + 增删改入口) |
|
||||
| edit.vue | 新增/编辑弹窗表单 |
|
||||
| detail.vue | 详情视图 |
|
||||
| columns.ts | 表格列配置(图片列 ElAvatar 渲染示范) |
|
||||
| querySchemas.ts | 筛选表单配置(Input/Select 示范) |
|
||||
|
||||
接口模块:`ui/src/api/tool/example.ts`(getExamplePage/exampleAdd…)。
|
||||
|
||||
## 学习路径建议
|
||||
|
||||
```text
|
||||
1. 跑通页面:登录 → 工具 → 案例演示 → 增删改查/导入导出全流程点一遍
|
||||
2. 对照后端:api/v1/example.go(gmeta+gvalid)→ logic/example.go(spec+委托)
|
||||
→ controller/service(透传/接口)→ dao/example.go(钩子+Ctx)
|
||||
3. 改一个字段:加 remark 列 → 四处补齐(3.3 改字段实战)
|
||||
4. 仿写新模块:以 example 为骨架手动写一遍,或用生成器产出后对照差异
|
||||
```
|
||||
|
||||
## 生成器视角
|
||||
|
||||
example 也是生成器产物的示范形态(ModuleSpec 基座风格的完整可用实现)。生成器对含枚举注释/图片列的表会产出同样的模式:
|
||||
|
||||
```sh
|
||||
go run ./cmd/generator -tables "goframe_example|案例演示表" # 产出与 example 同构的全套文件
|
||||
```
|
||||
@@ -0,0 +1,48 @@
|
||||
# 6.14 文件模板管理
|
||||
|
||||
:::tip 概述
|
||||
文件模板模块(goframe_file_template,平台级表)在线管理文件/代码片段模板:code 全局唯一、按编码查询(getTemplateByCode)。注意与代码生成器的磁盘模板(templates/*.tpl)是两套互不相干的体系。
|
||||
:::
|
||||
|
||||
## 接口清单
|
||||
|
||||
```text
|
||||
七件套:sys:fileTemplate:{page|list|detail|add|update|delete|batchDelete}
|
||||
GET /api/file/template/getTemplateByCode/:code 登录即可 按编码查模板
|
||||
```
|
||||
|
||||
## 模块特征
|
||||
|
||||
| 特征 | 实现 |
|
||||
|------|------|
|
||||
| 唯一性 | code 全局唯一(uk_code);Add 走 CheckUnique 预检 |
|
||||
| 平台级 | 无租户隔离(全局模板库) |
|
||||
| 快捷查询 | getTemplateByCode(登录即可,供业务按编码取模板内容) |
|
||||
| 内容形态 | 模板正文(文本/HTML/代码片段) |
|
||||
|
||||
## 易混淆点:两套"模板"
|
||||
|
||||
::: warning 两套模板互不相干
|
||||
| | templates/*.tpl(磁盘目录) | goframe_file_template 表 |
|
||||
|--|--|--|
|
||||
| 定位 | **代码生成器的渲染模板** | 业务功能(在线管理文件/代码片段模板) |
|
||||
| 消费方 | generator.go 直接读取渲染 | /api/file/template 模块 + getTemplateByCode |
|
||||
| 版本管理 | 随仓库版本管理 | 数据库数据 |
|
||||
| 修改影响 | 改变生成产物风格 | 影响业务取到的模板内容 |
|
||||
|
||||
修改生成产物改 `templates/*.tpl`;不要到 file_template 表里找生成器模板。两者无调用关系。
|
||||
:::
|
||||
|
||||
## 使用场景
|
||||
|
||||
```text
|
||||
* 系统内需要下发"格式文本"的场景:导出模板、打印模板、消息模板、代码片段
|
||||
* 业务按编码取模板:GET /api/file/template/getTemplateByCode/{code}
|
||||
* 在线编辑维护:模板管理页 CRUD(无需发版)
|
||||
```
|
||||
|
||||
## 管理建议
|
||||
|
||||
+ code 命名语义化(如 export_header / print_invoice),全局唯一
|
||||
+ 模板内容变更即时生效(无缓存层,按编码实时查询)
|
||||
+ 平台级模块——租户差异化模板需扩展(或按 code 前缀约定租户命名空间)
|
||||
@@ -0,0 +1,79 @@
|
||||
# 6.15 首页与个人中心
|
||||
|
||||
:::tip 概述
|
||||
index 模块(无表,聚合 user/menu)提供个人中心五接口:当前用户信息(超管返回通配权限)、菜单树、修改密码、更新个人信息。路由前缀 /api/index,登录即可(无 permission 节点)。
|
||||
:::
|
||||
|
||||
## 接口清单
|
||||
|
||||
| 方法 | 路由 | 权限 | 说明 |
|
||||
|------|------|------|------|
|
||||
| GET | /api/index/getUser | 登录即可 | 当前用户信息(超管返回 `*:*:*`) |
|
||||
| GET | /api/index/getMenus | 登录即可 | 当前用户菜单树(前端 common 菜单调用) |
|
||||
| GET | /api/index/getMenus/:id | 登录即可 | 同上(system/menu 页面调用) |
|
||||
| PUT | /api/index/updatePassword | 登录即可 | 修改个人密码(旧密码校验) |
|
||||
| PUT | /api/index/updateProfile | 登录即可 | 更新个人信息 |
|
||||
|
||||
## GetUser(当前用户信息)
|
||||
|
||||
```text
|
||||
数据来源与安全边界:
|
||||
* 用户行经 dao.User.Ctx(自动过滤软删/租户);password/salt 永不返回
|
||||
* Roles/Perms 优先取 JWT 中间件注入的 ctx(与鉴权同源),为空时回源 LoadIdentity
|
||||
* 超管强制 perms=["*:*:*"](前端据此放开全部按钮)
|
||||
* 头像经 upload.GetFileUrl 补域名
|
||||
* 城市编码四列(province/city/district/street_code)组装为 city[] 数组;
|
||||
city_info 为空时用 ResolveCityNames 按编码回填名称
|
||||
```
|
||||
|
||||
## GetMenus(菜单树)
|
||||
|
||||
```text
|
||||
两条路由共用 getMenuTree:
|
||||
* 仅取 type=0(目录/菜单)且 status=0 且 hide=0 的节点
|
||||
——按钮权限节点不进菜单树,由 GetUser.permissions 下发
|
||||
* 超管全量;其余 user→user_role→role_menu→menu.id IN(...) 授权过滤
|
||||
* 无角色返回空树([] 非 null)
|
||||
* 挂树规则:父节点不在授权集合内(未授权/顶级)一律挂根,避免菜单丢失
|
||||
* 排序 sort,id(根级再按 sort+id 兜底稳定排序)
|
||||
```
|
||||
|
||||
## UpdatePassword(修改密码)
|
||||
|
||||
```text
|
||||
流程:
|
||||
1) 只取 id/password/salt 三列
|
||||
2) 旧密码 IsBcrypt + VerifyPassword 校验(错误 1012 原密码错误)
|
||||
3) 事务外 GenerateSalt + HashPassword(CPU 密集预计算)
|
||||
4) Update 覆盖 password/salt(update_user/time 由 hooks 盖章)
|
||||
|
||||
注意:改密后未强制下线(令牌仍有效至过期);
|
||||
如需立即失效可配合在线用户强退(见 6.16 在线用户监控)
|
||||
```
|
||||
|
||||
## UpdateProfile(更新个人信息)
|
||||
|
||||
```text
|
||||
流程:
|
||||
* 头像 SaveFileField(avatar, "user") 临时转正
|
||||
* 城市优先由编码数组经 ResolveCityNames 解析名称覆盖 city_info
|
||||
* 四列编码经 cityCodeAt(req.City, i) 取值写入
|
||||
安全边界:不触达 username/password/tenant 等敏感列
|
||||
```
|
||||
|
||||
## 前端消费
|
||||
|
||||
```text
|
||||
登录后首页 dashboard:调 getUser(头像/名称/权限)+ getMenus(侧边菜单)
|
||||
路由守卫:动态路由由 getMenus 生成(见 9.5 路由与动态菜单)
|
||||
个人中心页:updateProfile(资料)/ updatePassword(改密)
|
||||
```
|
||||
|
||||
## 排障
|
||||
|
||||
| 症状 | 原因 |
|
||||
|------|------|
|
||||
| 菜单为空 | 无角色或角色未勾选菜单;超管应返回全量 |
|
||||
| getUser 权限缺失 | 身份缓存异常;超管应返回 `*:*:*` |
|
||||
| 改密后旧令牌仍可用 | 设计预期(不强制下线);配合在线用户强退 |
|
||||
| 头像不显示 | upload.GetFileUrl 域名配置(file.domainUrl)与静态服务根 |
|
||||
@@ -0,0 +1,132 @@
|
||||
# 6.11 定时任务
|
||||
|
||||
:::tip 概述
|
||||
定时任务是数据驱动的常驻调度器:任务定义在 goframe_job 表,启动时加载 status=1 任务挂载 gcron,执行目标分双形态(HTTP 调用 / 本地处理器),三种执行策略叠加多实例执行锁,结果落 goframe_job_log。
|
||||
:::
|
||||
|
||||
## 架构总览
|
||||
|
||||
```text
|
||||
启动(cmd.go 延时 2s)→ DaemonInit 加载 goframe_job 中 status=1 的任务
|
||||
→ 按 cron_expression 挂载 gcron → tick 触发 executeJob
|
||||
→ 执行目标分派(HTTP 调用 或 本地处理器)→ 结果写 goframe_job_log
|
||||
|
||||
职责分层:
|
||||
调度基础设施(挂载/摘除/锁/执行/落日志)在 internal/logic 的 job 模块
|
||||
任务业务实现在 internal/task 包(单向依赖 task→logic,logic 禁止反向 import task)
|
||||
由 internal/cmd 空白导入 internal/task 触发 init 注册
|
||||
```
|
||||
|
||||
## 执行目标双形态(executeJob 读 url 字段分派)
|
||||
|
||||
```text
|
||||
url 为 http:// 或 https:// 开头 → HTTP 调用目标地址:
|
||||
params 为空 GET,非空 POST(application/json,超时 30s)
|
||||
非 200 或网络错误 → 失败(exception 记录状态与响应前 1000 字符)
|
||||
|
||||
url 其余取值 → 本地处理器方法名,从 jobHandlers 注册表查找:
|
||||
签名 func(ctx context.Context, params string) (string, error)
|
||||
返回摘要写入任务日志 message
|
||||
未注册 → 失败"未找到本地任务处理方法"
|
||||
```
|
||||
|
||||
## 执行策略与执行方式
|
||||
|
||||
```text
|
||||
execute_policy(列值):
|
||||
1 立即执行 允许并发:gcron.Add,tick 即启动新执行;不加执行锁
|
||||
2 执行一次 单例调度(gcron.AddSingleton),上次未结束跳过本次;叠加执行锁
|
||||
3 放弃执行 并发 tick 照常触发,但抢不到执行锁则放弃本次
|
||||
|
||||
is_sync(列值):
|
||||
1 同步 在调度协程内执行(阻塞本条目下一 tick 的派发)
|
||||
0 异步 go 协程执行(context.Background())
|
||||
|
||||
执行锁 jobTryLock(策略 2/3 叠加):
|
||||
键 job:lock:{id},utility/cache SET NX(TTL 300s 兜底——实例崩溃未释放自动过期;
|
||||
正常路径 defer 立即释放)。多实例部署共享同一把锁(Redis 模式防跨实例并发;
|
||||
内存降级时退化单机语义)
|
||||
|
||||
cron 表达式:gf gcron 格式,默认 6 段(秒 分 时 日 月 周);
|
||||
# 前缀表示标准 5 段忽略秒(如 "# */5 * * * *" = 每 5 分钟)
|
||||
```
|
||||
|
||||
## 调度流程与状态机
|
||||
|
||||
```text
|
||||
Add(固定置0)
|
||||
│
|
||||
▼
|
||||
┌─────────┐ Status(1) / Resume ┌─────────┐
|
||||
│ 0 未发布 │─────────────────────▶│ 1 运行中 │◀──┐
|
||||
└─────────┘ └────┬────┘ │
|
||||
▲ Status(2)/Pause │ │ Resume
|
||||
│ ─────────────────────▶│ │
|
||||
│ 摘除调度 ▼ │
|
||||
│ ┌─────────┐ │
|
||||
│ Status(0) │ 2 暂停 │───┘
|
||||
│◀─────────────────────────── └─────────┘
|
||||
│ 摘除调度
|
||||
Delete:软删 + 摘除调度
|
||||
运行中 Update:摘除 → 按新 cron 重挂(失败降级置 2 并报 Cron 无效)
|
||||
RunOnce:不看状态,异步 force 执行一次(不改状态)
|
||||
```
|
||||
|
||||
## 管理接口(/api/job)
|
||||
|
||||
```text
|
||||
七件套 sys:job:*
|
||||
GET /api/job/runOnce/:id sys:job:execute 立即执行(异步,未发布也可执行)
|
||||
GET /api/job/pause/:id sys:job:pause 摘除调度 + status=2
|
||||
GET /api/job/resume/:id sys:job:resume 重新挂载 + status=1
|
||||
POST /api/job/status sys:job:update 设状态(1→挂载,其他→摘除)
|
||||
GET /api/job/daemonStatus 登录即可 调度器状态(running/jobIds/startTime)
|
||||
|
||||
业务钩子:BeforeAdd 固定 status=0;AfterUpdate 运行中先摘除再重挂;
|
||||
BeforeDelete 存在性检查 + 摘除调度
|
||||
字典:job_status / job_execute_policy / job_is_sync(SerializeMaps 驱动 {field}Text)
|
||||
```
|
||||
|
||||
## 本地处理器注册(internal/task)
|
||||
|
||||
```text
|
||||
logic.RegisterJobHandler(name, h):注册名全局唯一,重复注册进程 panic
|
||||
注册名与任务记录 url 字段值一一对应(数据驱动查找)
|
||||
|
||||
internal/task 包约定:
|
||||
每任务一个文件(snake_case),init 中注册
|
||||
处理方法签名 func(ctx, params) (string, error),返回值写任务日志摘要
|
||||
由 internal/cmd 空白导入触发加载(不在 HTTP 启动链路内)
|
||||
|
||||
内置任务:
|
||||
HelloWorld(hello_world.go) 示例,仅打日志(验证调度链路)
|
||||
SendSms(send_sms.go) 仅记录执行参数日志(不对接短信通道)
|
||||
LogClean(log_clean.go) 日志清理:每日 02:30 分批物理删除三张日志表
|
||||
参数 {"retentionDays": n} 可覆盖 job.logRetentionDays(默认 90)
|
||||
单批 5000 行循环至不足一批;单表单次任务上限 200000 行(防长事务/runaway)
|
||||
```
|
||||
|
||||
## 新增任务开发步骤
|
||||
|
||||
```text
|
||||
1) internal/task/{任务名}.go 实现 handler:func(ctx, params) (string, error)
|
||||
2) init() 中 logic.RegisterJobHandler("TaskName", handler)
|
||||
3) 任务管理页新增记录:url 填 "TaskName",params 填 JSON,
|
||||
cron_expression 填 6 段或 # 5 段,execute_policy/is_sync 按需
|
||||
4) Status 置为运行中(发布)→ 调度器挂载;观察 job/log 执行日志
|
||||
5) go build ./... 并重启(init 注册发生在进程加载期)
|
||||
|
||||
注意:handler 可复用 base 共享能力(WithTrans/ChunkQuery)与 dao;
|
||||
长任务用分批+上限;HTTP 目标任务自带 30s 超时,长任务应改本地处理器
|
||||
```
|
||||
|
||||
## 多实例部署语义
|
||||
|
||||
```text
|
||||
单实例:gcron 各自挂载,正常运行
|
||||
多实例 + Redis:策略 2/3 经 job:lock:{id} 全局互斥(同一时刻至多一个实例执行)
|
||||
——仍会重复"触发"(各实例各自 tick),未抢到锁的实例直接放弃
|
||||
多实例 + 内存降级:锁退化单机语义,可能跨实例并发执行——生产 Redis 必须可用
|
||||
```
|
||||
|
||||
排障口径差异:`daemonStatus.running` 是 gcron 全部条目数(含 CleanTemp 等非任务条目);`server/info` 的 `Scheduler.mounted` 只统计 "job:" 前缀条目。
|
||||
@@ -0,0 +1,54 @@
|
||||
# 6.5 职级管理
|
||||
|
||||
:::tip 概述
|
||||
职级模块(goframe_level,租户级表)管理用户职级属性:标准 CRUD + 状态切换 + Excel 导入导出(与 user/example 并列的三个导入导出示范模块之一),列表带 user_count 引用统计。
|
||||
:::
|
||||
|
||||
## 接口清单
|
||||
|
||||
| 方法 | 路由 | 权限 | 说明 |
|
||||
|------|------|------|------|
|
||||
| GET | /api/level/page | sys:level:page | 分页 |
|
||||
| GET | /api/level/list | sys:level:list | 列表 |
|
||||
| GET | /api/level/detail/:id | sys:level:detail | 详情 |
|
||||
| POST | /api/level/add | sys:level:add | 新增 |
|
||||
| PUT | /api/level/update | sys:level:update | 修改 |
|
||||
| PUT | /api/level/status | sys:level:status | 状态切换 |
|
||||
| DELETE | /api/level/delete/:id | sys:level:delete | 软删 |
|
||||
| DELETE | /api/level/batchDelete | sys:level:batchDelete | 批量删除 |
|
||||
| POST | /api/level/import | sys:level:import | Excel 导入 |
|
||||
| GET | /api/level/export | sys:level:export | Excel 导出 |
|
||||
|
||||
## 模块特征
|
||||
|
||||
| 特征 | 实现 |
|
||||
|------|------|
|
||||
| 导入导出 | excel.ReadSheet / WriteSheet + ChunkQuery 分批导出 |
|
||||
| 引用统计 | fillUserCount:按 level_id 统计引用用户数(base.go 共享能力,level/position 复用) |
|
||||
| 唯一性 | 租户内名称唯一(uk_tenant_name);未声明预检,1062 兜底 |
|
||||
| 字典 | status → 字典驱动 {field}Text |
|
||||
|
||||
## 与其他模块的关系
|
||||
|
||||
```text
|
||||
user.level_id → level.id 用户归属职级(用户表单职级选择)
|
||||
fillUserCount 列表/详情展示 user_count(该职级下用户数)
|
||||
—— 删除前可据此判断是否被引用
|
||||
```
|
||||
|
||||
## 导入导出模式(与 user/example 同款)
|
||||
|
||||
```text
|
||||
Import:multipart file(*ghttp.UploadFile + p:"file")
|
||||
→ excel.ReadSheet(首行表头,全单元格字符串,空行跳过)
|
||||
→ 逐行映射/校验(唯一、字典值)→ 批量 Insert → 返回导入统计
|
||||
|
||||
Export:与列表同筛选 → ChunkQuery 分批(默认 500/页,防 OOM)
|
||||
→ excel.WriteSheet 写临时文件 → 返回文件 URL(/upload/temp/...)
|
||||
```
|
||||
|
||||
## 管理建议
|
||||
|
||||
+ 职级是租户级数据——各租户独立维护自己的职级体系
|
||||
+ 删除前查 user_count 引用;有引用先调整用户职级
|
||||
+ 导入模板与导出列保持一致(表头=表单字段名);批量导入注意租户内唯一名称
|
||||
@@ -0,0 +1,67 @@
|
||||
# 6.3 菜单管理
|
||||
|
||||
:::tip 概述
|
||||
菜单模块(goframe_menu,平台级表)统一承载两类节点:type=0 目录/菜单(进前端菜单树)与 type=1 权限节点(进后端 Perms 校验)。树形结构、防环校验、关联清理齐备。
|
||||
:::
|
||||
|
||||
## 接口清单
|
||||
|
||||
```text
|
||||
七件套:sys:menu:{page|list|detail|add|update|delete|batchDelete}
|
||||
List 返回树形(IsTree + ParentOf)
|
||||
```
|
||||
|
||||
## type 语义(关键概念)
|
||||
|
||||
| type | 名称 | 消费方 |
|
||||
|------|------|--------|
|
||||
| 0 | 菜单/目录 | 前端菜单树(getMenuTree 过滤 type=0 且 status=0 且 hide=0) |
|
||||
| 1 | 权限节点 | 后端 Perms 校验(LoadIdentity 过滤 type=1 且 status=0 且 hide=0) |
|
||||
|
||||
```text
|
||||
menu 行的关键列:
|
||||
parent_id 父级(树形)
|
||||
type 0-菜单 1-权限节点(注释枚举)
|
||||
permission 权限码(sys:user:page 形式;type=1 必填)
|
||||
status 0-启用(禁用节点不进树也不进 Perms)
|
||||
hide 0-显示(hide=1 不进菜单树)
|
||||
sort 排序
|
||||
```
|
||||
|
||||
::: warning hide 与 status 的区别(易错)
|
||||
hide=1 的权限节点**不生效**:getMenuTree 与 LoadIdentity 都过滤 hide=0,hide=1 节点既不进菜单树也不进 Perms。"禁用权限"必须用 status,不能用 hide。
|
||||
:::
|
||||
|
||||
## 树形与守卫
|
||||
|
||||
```text
|
||||
List 树形:spec 声明 IsTree + ParentOf(DTO 含 Id/Children,叶子 Children 预置 [])
|
||||
|
||||
删除守卫 menuDeleteGuard:存在子菜单拒绝(1011)
|
||||
关联清理 menuCleanRelations:删除菜单时同步清理 role_menu 引用
|
||||
(批量删除同理)——防止悬挂授权
|
||||
父级校验 menuParentGuard:parentId 不得指向自身或后代(防环)
|
||||
```
|
||||
|
||||
## 菜单树的消费(index.GetMenus)
|
||||
|
||||
```text
|
||||
getMenuTree:
|
||||
1) 仅取 type=0 且 status=0 且 hide=0 的节点(按钮权限不进菜单树,
|
||||
由 GetUser.permissions 下发)
|
||||
2) 超管全量;其余 user→user_role→role_menu→menu.id IN(...) 授权过滤
|
||||
3) 无角色返回空树([] 非 null)
|
||||
4) 挂树规则:父节点不在授权集合内(未授权/顶级)一律挂根,避免菜单丢失
|
||||
5) 排序 sort,id(稳定排序)
|
||||
```
|
||||
|
||||
## 与生成器的关系
|
||||
|
||||
代码生成器 createMenu 副作用自动创建:主菜单(type=0,挂 generator.parentMenuId 下)+ 权限节点(type=1,7 固定 + status/import/export 按需)。按 path+type 查重幂等。详见 [10.8 实战案例](../generator/demo)。
|
||||
|
||||
## 管理建议
|
||||
|
||||
+ 权限节点命名与 gmeta permission 严格一致(sys:{module小驼峰}:{action})
|
||||
+ 一个模块一个主菜单 + N 个权限节点,权限节点挂主菜单下
|
||||
+ 禁用节点用 status=2(保留数据可恢复);删除走软删 + 关联清理
|
||||
+ hide 只用于"菜单树隐藏但(当前实现下)不建议用于权限节点"——见上方警告
|
||||
@@ -0,0 +1,82 @@
|
||||
# 6.16 在线用户监控
|
||||
|
||||
:::tip 概述
|
||||
在线用户是无表模块(数据源 Redis):登录/刷新登记会话,分页查看在线列表,强退踢出(access+refresh 双令牌拉黑,防被踢会话经刷新复活)。路由前缀 /api/onlineUser。
|
||||
:::
|
||||
|
||||
## 数据结构(Redis)
|
||||
|
||||
```text
|
||||
zset easygoadmin:online:jti:set score = 访问令牌过期时间戳
|
||||
键 easygoadmin:online:info:{jti} JSON 详情(TTL = access 剩余期 + 60s)
|
||||
|
||||
onlineInfo 字段:
|
||||
jti / userId / username / realname / ip / location / os / browser /
|
||||
loginTime / issuedAt / expireAt / refreshJti / refreshIssuedAt / refreshExpireAt /
|
||||
accessToken / refreshToken(令牌原文,仅详情查看返回)/ tokenType / expiresIn
|
||||
```
|
||||
|
||||
## 会话生命周期
|
||||
|
||||
```text
|
||||
登录/refresh 成功
|
||||
│ MarkOnline
|
||||
▼
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ zset easygoadmin:online:jti:set │ score=access过期ts
|
||||
│ 键 easygoadmin:online:info:{jti} = JSON │ TTL=access剩余+60s
|
||||
└──────────────────┬──────────────────────────┘
|
||||
页面访问(有效期内)│ │登出 Logout / 强退 ForceLogout
|
||||
(token 有效,│ ▼ MarkOffline + access/refresh 双拉黑
|
||||
JWT 放行) │ zset 移除 + info 删除 → 会话消失
|
||||
▼
|
||||
access 到期:zset score < now 被区间过滤(列表不可见);
|
||||
info 键 TTL 到期自动清除
|
||||
refresh 到期:无法再换新令牌
|
||||
|
||||
无心跳/续期机制:会话有效期即 access 令牌有效期(2h);
|
||||
前端 refresh 轮换会刷新在线登记(MarkOnline 覆盖写)
|
||||
```
|
||||
|
||||
## 接口清单
|
||||
|
||||
| 方法 | 路由 | 权限 | 说明 |
|
||||
|------|------|------|------|
|
||||
| GET | /api/onlineUser/page | sys:onlineUser:page | 在线会话分页 |
|
||||
| GET | /api/onlineUser/detail/:id | sys:onlineUser:detail | 会话详情(含令牌原文) |
|
||||
| GET | /api/onlineUser/forceLogout/:id | sys:onlineUser:forceLogout | 单会话强退 |
|
||||
| GET | /api/onlineUser/forceLogoutUser/:userId | sys:onlineUser:forceLogout | 踢出该用户全部设备 |
|
||||
|
||||
## Page 流程
|
||||
|
||||
```text
|
||||
ZRangeByScore(now..+∞) → 逐 jti 读详情 → username/ip 子串过滤
|
||||
→ 按登录时间倒序 → 同用户多会话标注(SessionNo/Sessions 序号与总数)
|
||||
→ 批量回填头像(按 userId 查 user.avatar,GetFileUrl 补域名,单用户只查一次)
|
||||
→ 内存分页(NewPageData)
|
||||
```
|
||||
|
||||
## 强退的双令牌拉黑(安全关键)
|
||||
|
||||
```text
|
||||
单会话强退(forceLogout/:id):
|
||||
1) access jti 拉黑(TTL=剩余期+60s)
|
||||
2) refresh jti 一并拉黑——防被踢会话经 refresh 换新令牌复活
|
||||
3) MarkOffline(zset 移除 + info 删除)
|
||||
|
||||
踢全部设备(forceLogoutUser/:userId):
|
||||
遍历 zset 匹配 userId → 逐会话双令牌拉黑 + 注销
|
||||
```
|
||||
|
||||
::: tip 为什么双拉黑
|
||||
只拉黑 access 的话,被踢用户的 refresh_token 仍能兑换新令牌对重新"复活"。access+refresh 一并拉黑才能真正踢出。
|
||||
:::
|
||||
|
||||
## 排障
|
||||
|
||||
| 症状 | 原因 |
|
||||
|------|------|
|
||||
| 在线列表为空但有人在线 | Redis 内存降级(各实例独立);或会话已过 access 有效期 |
|
||||
| 强退后对方仍能请求 | 该实例内存降级(黑名单不跨实例);等窗口或查 /api/cache/info |
|
||||
| 会话数偏多 | access 2h 有效期内多次登录=多会话(同用户多设备标注序号) |
|
||||
| 详情泄露令牌原文 | 设计如此(运维排查用),依赖 sys:onlineUser:detail 权限收口 |
|
||||
@@ -0,0 +1,36 @@
|
||||
# 6.12 参数管理
|
||||
|
||||
:::tip 概述
|
||||
参数模块(goframe_param,平台级表)管理系统级扁平键值参数:code 全局唯一(uk_code),标准七件套 CRUD。机制说明见 [5.13 系统参数管理](../core/param),本节给出模块视角。
|
||||
:::
|
||||
|
||||
## 接口清单
|
||||
|
||||
```text
|
||||
七件套:sys:param:{page|list|detail|add|update|delete|batchDelete}
|
||||
```
|
||||
|
||||
## 模块要点
|
||||
|
||||
| 要点 | 实现 |
|
||||
|------|------|
|
||||
| 唯一性 | UniqueFields{Field:"code"}(与全局 uk_code 一致,无偏差)——Add 前预检 |
|
||||
| 平台级 | 无租户隔离(全局参数) |
|
||||
| 消费方式 | 业务按 code 查询取值(低频直接查库) |
|
||||
| 选择判据 | 简单键值对 → param;结构化表单配置 → config;需重启 → config.yaml |
|
||||
|
||||
## 与 config 模块的对照
|
||||
|
||||
| 维度 | param | config + config_item |
|
||||
|------|-------|---------------------|
|
||||
| 形态 | 扁平 code→value | 分组 + 键值明细(type/options 结构化) |
|
||||
| 适用 | 运行参数快速读写 | 网站配置等表单化场景 |
|
||||
| 接口 | 七件套 | 七件套 + web/index + web/save |
|
||||
| 唯一预检 | 有(spec UniqueFields) | Add 走 CheckUnique(Update 1062 兜底) |
|
||||
|
||||
## 管理建议
|
||||
|
||||
+ code 命名 snake_case 语义化(default_password / page_size / user_code_prefix)
|
||||
+ 删除是软删(可恢复),但删除前确认无业务引用(param 无引用守卫)
|
||||
+ 高频消费场景(每请求读)建议调用方自建缓存(param 模块无缓存层)
|
||||
+ 新增参数后在代码中以常量引用 code 字符串,避免拼写漂移
|
||||
@@ -0,0 +1,41 @@
|
||||
# 6.6 岗位管理
|
||||
|
||||
:::tip 概述
|
||||
岗位模块(goframe_position,租户级表)管理用户岗位属性:标准 CRUD + 状态切换 + 引用统计。与职级模块同构(同为用户属性维护模块)。
|
||||
:::
|
||||
|
||||
## 接口清单
|
||||
|
||||
```text
|
||||
七件套 + PUT /api/position/status(sys:position:status)
|
||||
sys:position:{page|list|detail|add|update|delete|batchDelete|status}
|
||||
```
|
||||
|
||||
## 模块特征
|
||||
|
||||
| 特征 | 实现 |
|
||||
|------|------|
|
||||
| 状态切换 | status(1-正常 2-禁用,字典驱动 {field}Text) |
|
||||
| 引用统计 | fillUserCount:按 position_id 统计引用用户数 |
|
||||
| 唯一性 | 租户内名称唯一(uk_tenant_name);未声明预检,1062 兜底 |
|
||||
| 租户级 | 各租户独立维护 |
|
||||
|
||||
## 与其他模块的关系
|
||||
|
||||
```text
|
||||
user.position_id → position.id 用户归属岗位(用户表单岗位选择)
|
||||
fillUserCount 列表展示 user_count(该岗位下用户数)
|
||||
```
|
||||
|
||||
## 与职级模块的差异
|
||||
|
||||
| 维度 | 岗位 position | 职级 level |
|
||||
|------|--------------|-----------|
|
||||
| 导入导出 | 无 | 有(import/export) |
|
||||
| 其余 | 同构(CRUD + status + 引用统计) | 同构 |
|
||||
|
||||
## 管理建议
|
||||
|
||||
+ 岗位与职级的分工:岗位偏"职责"(如前端工程师),职级偏"层级"(如 P7)
|
||||
+ 删除前查 user_count 引用;有引用先调整用户岗位
|
||||
+ 岗位编码/名称变更即时生效(无缓存层,用户展示经 userExpandNames 批查回填)
|
||||
@@ -0,0 +1,68 @@
|
||||
# 6.2 角色管理
|
||||
|
||||
:::tip 概述
|
||||
角色模块(goframe_role,租户级表)承载 RBAC 分配与数据权限四档:MenuList 返回带勾选状态的全量菜单树,SavePermission 全量重写角色菜单并清理身份缓存,data_scope 经字典驱动展示。
|
||||
:::
|
||||
|
||||
## 接口清单
|
||||
|
||||
| 方法 | 路由 | 权限 | 说明 |
|
||||
|------|------|------|------|
|
||||
| GET | /api/role/page | sys:role:page | 分页(含 dataScope + dataScopeText) |
|
||||
| GET | /api/role/list | sys:role:list | 列表 |
|
||||
| GET | /api/role/detail/:id | sys:role:detail | 详情 |
|
||||
| POST | /api/role/add | sys:role:add | 新增(data_scope 缺省归一 1) |
|
||||
| PUT | /api/role/update | sys:role:update | 修改 |
|
||||
| DELETE | /api/role/delete/:id | sys:role:delete | 软删(存在用户引用则拒绝) |
|
||||
| DELETE | /api/role/batchDelete | sys:role:batchDelete | 批量删除 |
|
||||
| GET | /api/role/menu/list/:roleId | sys:role:getPermission | 查询角色菜单(带勾选状态) |
|
||||
| POST | /api/role/menu/save | sys:role:savePermission | 保存权限(全量重写) |
|
||||
|
||||
## 权限勾选(MenuList / SavePermission)
|
||||
|
||||
```text
|
||||
MenuList(GET /api/role/menu/list/:roleId):
|
||||
返回全量菜单树(type 0/1 均含,供勾选界面展示目录+菜单+按钮三层),
|
||||
每节点带 Checked = role_menu 是否含该 id(checkedSet 预构建)
|
||||
|
||||
SavePermission(POST /api/role/menu/save)——语义为"全量重写":
|
||||
1) 先 Delete 该角色全部 role_menu(软删)
|
||||
2) 逐个 Insert req.MenuIds
|
||||
3) clearRoleIdentityCache(roleId)
|
||||
缓存清理链:按 roleId 查受影响 user_id 列表 → 逐个 Del
|
||||
easygoadmin:user:identity:{uid}
|
||||
```
|
||||
|
||||
## data_scope 数据范围链路
|
||||
|
||||
```text
|
||||
roleToData:Add/Update 均带 data_scope,入参 ≤0 归一为 1(全部),
|
||||
gvalid in:1,2,3,4 校验
|
||||
roleSpec.SerializeMaps = {"data_scope": "role_data_scope"}
|
||||
→ 列表行 dataScopeText 由字典填充
|
||||
AfterUpdate(roleClearCache):数据范围变更后失效该角色全部用户身份缓存
|
||||
→ 下一次请求 LoadIdentity 重取 min(data_scope) 即生效
|
||||
```
|
||||
|
||||
四档语义与安全边界见 [5.9 数据权限](../core/data-scope)。
|
||||
|
||||
## 删除守卫
|
||||
|
||||
存在用户引用则拒绝删除(1014/1011 语义)——先给用户换角色,再删空角色。
|
||||
|
||||
## 已知偏差(如实记录)
|
||||
|
||||
::: warning 唯一预检范围偏差
|
||||
role 的 spec 唯一预检当前声明为 `UniqueFields{Field:"code"}`(未带 Group),而索引 `uk_tenant_code` 是"租户内唯一"——预检范围偏宽:超管为不同租户创建相同角色编码时会被误报"已存在"(数据库层不会冲突)。
|
||||
|
||||
修复方向:补 `Group:"tenant_id"`,使预检范围等于索引范围。
|
||||
:::
|
||||
|
||||
## 排障
|
||||
|
||||
| 症状 | 原因 |
|
||||
|------|------|
|
||||
| 保存权限后用户仍 403 | 身份缓存未清(SavePermission 应触发清理链);DEL `easygoadmin:user:identity:{uid}` |
|
||||
| 改 data_scope 不生效 | 同上——AfterUpdate 应清该角色全部用户缓存 |
|
||||
| 删除角色报"存在关联" | 有用户挂该角色,先在用户管理换角色 |
|
||||
| 不同租户建同名角色编码被拒 | 上述已知偏差(预检缺 Group) |
|
||||
@@ -0,0 +1,76 @@
|
||||
# 6.17 服务器监控
|
||||
|
||||
:::tip 概述
|
||||
服务器监控是无表模块:单接口聚合返回运行时、内存、磁盘、数据库连通、缓存模式与调度器健康。磁盘读取按平台分文件实现(windows/other build tags)。路由 GET /api/server/info(登录即可)。
|
||||
:::
|
||||
|
||||
## 接口
|
||||
|
||||
```text
|
||||
GET /api/server/info 登录即可,log "服务器监控-查询信息"
|
||||
权限:无 permission 节点(登录即可)
|
||||
```
|
||||
|
||||
## 返回结构(ServerInfoRes)
|
||||
|
||||
```text
|
||||
Server hostname / goVersion / arch / numCPU / goroutines / uptime
|
||||
(procStart 为包加载时刻)
|
||||
Mem allocMB / totalAllocMB / sysMB / heapAllocMB / heapSysMB /
|
||||
heapObjects / gccycles(runtime.ReadMemStats,MB 保留两位小数)
|
||||
Disk 平台分支实现:
|
||||
windows(//go:build windows):GetDiskFreeSpaceEx 读工作目录所在盘符
|
||||
(wd[:3] 如 "E:\"),返回 totalGB/freeGB/usedPercent
|
||||
other(!windows):syscall.Statfs
|
||||
Db SELECT VERSION() 计时(pingMs)+ 版本号;
|
||||
数据库不通不视为整体失败(版本留空,err 置 nil 返回)
|
||||
Cache cache.UsingMemory() → {mode:"memory", connected:false}
|
||||
否则 Redis PING → {mode:"redis", connected:true/false}
|
||||
Scheduler 调度器健康:统计 gcron 中 "job:" 前缀条目数(mounted)与
|
||||
goframe_job status=1 计数(expected);enabled=DaemonInit 已执行;
|
||||
healthy = mounted >= expected(挂载失败或数据被外部改动会出现差值)
|
||||
```
|
||||
|
||||
## 平台分支实现(文件组织示例)
|
||||
|
||||
```text
|
||||
internal/logic/server_monitor.go 公共组装
|
||||
internal/logic/server_monitor_windows.go //go:build windows —— GetDiskFreeSpaceEx
|
||||
internal/logic/server_monitor_other.go //go:build !windows —— syscall.Statfs
|
||||
|
||||
Go build tags 实现"同函数名、按平台选实现"——编译期裁剪,零运行时分支
|
||||
```
|
||||
|
||||
## 调度器健康判定(运维关键)
|
||||
|
||||
```text
|
||||
mounted = gcron 中 "job:" 前缀条目数(实际挂载)
|
||||
expected = goframe_job status=1 计数(应该挂载)
|
||||
healthy = mounted >= expected
|
||||
|
||||
差值排查:
|
||||
mounted < expected 部分任务挂载失败(cron 表达式无效?)
|
||||
或任务数据被外部改动(直接改库没走 API)
|
||||
mounted > expected 摘除遗漏(API 路径应先摘除再处理)
|
||||
注意口径:daemonStatus.running 是 gcron 全部条目数(含 CleanTemp 等非任务条目),
|
||||
Scheduler.mounted 只统计 "job:" 前缀——两处口径不同
|
||||
```
|
||||
|
||||
## 运维用法
|
||||
|
||||
```text
|
||||
日常巡检:GET /api/server/info 一眼看全(CPU 核数/内存/磁盘余量/DB/缓存/调度)
|
||||
磁盘告警阈值:usedPercent > 85% 关注
|
||||
内存观察:heapObjects 持续增长 + gccycles 低 → 疑似泄漏
|
||||
调度异常:healthy=false → 逐条核对任务 cron 与 status
|
||||
```
|
||||
|
||||
## 排障
|
||||
|
||||
| 症状 | 原因 |
|
||||
|------|------|
|
||||
| Disk 数据异常 | windows 读的是工作目录所在盘符(wd[:3])——服务在哪个盘就读哪个盘 |
|
||||
| Db 版本为空 | 数据库不通(不视为整体失败的设计);查 database 配置 |
|
||||
| Cache mode=memory | Redis 不可用已降级(限流/锁/黑名单退化单机语义) |
|
||||
| Scheduler healthy=false | 见上方差值排查;对照 /api/job/daemonStatus |
|
||||
| uptime 很长但内存高 | procStart 是包加载时刻——优雅重启才会重置 |
|
||||
@@ -0,0 +1,66 @@
|
||||
# 6.13 租户管理
|
||||
|
||||
:::tip 概述
|
||||
租户模块(goframe_tenant,平台级表)管理多租户生命周期:状态/过期、用户上限、事务化开户(Account,code 按 T+ID 规则回填)。隔离机制详见 [5.8 多租户隔离](../core/tenant)。
|
||||
:::
|
||||
|
||||
## 接口清单
|
||||
|
||||
| 方法 | 路由 | 权限 | 说明 |
|
||||
|------|------|------|------|
|
||||
| GET | /api/tenant/page | sys:tenant:page | 分页(平台专属视图,带用户数统计) |
|
||||
| GET | /api/tenant/list | sys:tenant:list | 列表 |
|
||||
| GET | /api/tenant/detail/:id | sys:tenant:detail | 详情 |
|
||||
| POST | /api/tenant/add | sys:tenant:add | 新增 |
|
||||
| PUT | /api/tenant/update | sys:tenant:update | 修改 |
|
||||
| PUT | /api/tenant/status | sys:tenant:status | 状态切换 |
|
||||
| DELETE | /api/tenant/delete/:id | sys:tenant:delete | 软删 |
|
||||
| DELETE | /api/tenant/batchDelete | sys:tenant:batchDelete | 批量删除 |
|
||||
| POST | /api/tenant/account | sys:tenant:add | 创建租户账号(开户) |
|
||||
| GET | /api/tenant/users/:tenantId | sys:tenant:users | 查询租户用户 |
|
||||
|
||||
## 编码规则(T%04d,自增 ID 回填)
|
||||
|
||||
```text
|
||||
code 为 NOT NULL + 唯一键,插入时无法预知 ID →
|
||||
1) 先写临时占位编码(T%010d,纳秒取模防撞)
|
||||
2) Insert 后按 LastInsertId 修正为 "T"+4 位ID(如 T0001)
|
||||
Add 与 Account 均遵循此模式;显式传 code 时查重后直接使用不回填
|
||||
```
|
||||
|
||||
## 开户事务(tenant.Account,真实流程)
|
||||
|
||||
```text
|
||||
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}
|
||||
|
||||
角色分配未含在开户事务内(按需另行调用)
|
||||
事务纪律:事务内不做 HTTP/文件 IO,bcrypt 等 CPU 计算事务外预计算
|
||||
```
|
||||
|
||||
## 状态与缓存
|
||||
|
||||
```text
|
||||
tenantFilter:列表为平台专属视图(超管可见全部,带每租户用户数统计)
|
||||
fillUserCount:每租户用户数(对照 max_user_num 判断容量)
|
||||
clearCache:状态/过期变更后 Del tenant:info:{id}
|
||||
(Tenant 中间件 60s 摘要缓存——变更后最多 60s 生效,Del 后即时生效)
|
||||
```
|
||||
|
||||
## 关联约束
|
||||
|
||||
+ 用户数上限:user.Add 的 checkTenantUserMax 对照 max_user_num(1007)
|
||||
+ 租户用户列表:GET /api/tenant/users/:tenantId
|
||||
+ 租户删除前应清空/迁移该租户用户与业务数据(软删租户不级联)
|
||||
|
||||
## 管理建议
|
||||
|
||||
+ expire_time 为 nil 表示永不过期;到期前 30 天关注续期
|
||||
+ max_user_num 控制容量;调大即时生效(user.Add 实时校验)
|
||||
+ 禁用租户(status=2)立即拦截该租户全部用户请求(等 60s 缓存或 Del 即时)
|
||||
+ 开户后记得为初始管理员分配角色(Account 不含角色分配)
|
||||
@@ -0,0 +1,90 @@
|
||||
# 6.1 用户管理
|
||||
|
||||
:::tip 概述
|
||||
用户模块(goframe_user,租户级表)是机制最全的模块:唯一校验(按行现租户)、租户用户上限、角色全量重写、城市编解码、Excel 导入导出、数据权限接线、批量名称回填。路由前缀 /api/user。
|
||||
:::
|
||||
|
||||
## 接口清单
|
||||
|
||||
| 方法 | 路由 | 权限 | 说明 |
|
||||
|------|------|------|------|
|
||||
| GET | /api/user/page | sys:user:page | 分页(关键词/状态/部门/角色/职级/岗位筛选) |
|
||||
| GET | /api/user/list | sys:user:list | 全量列表(下拉选择用) |
|
||||
| GET | /api/user/detail/:id | sys:user:detail | 详情(含角色 ID 数组与四级区划编码) |
|
||||
| POST | /api/user/add | sys:user:add | 新增(返回新记录 ID) |
|
||||
| PUT | /api/user/update | sys:user:update | 修改(密码留空不改) |
|
||||
| PUT | /api/user/status | sys:user:status | 状态切换 |
|
||||
| PUT | /api/user/resetPwd | sys:user:resetPwd | 重置密码(logType=reset) |
|
||||
| DELETE | /api/user/delete/:id | sys:user:delete | 软删(清理 user_role) |
|
||||
| DELETE | /api/user/batchDelete | sys:user:batchDelete | 批量删除 |
|
||||
| POST | /api/user/import | sys:user:import | Excel 导入 |
|
||||
| GET | /api/user/export | sys:user:export | Excel 导出 |
|
||||
| GET | /api/user/document/:id | sys:user:detail | 打印信息 |
|
||||
|
||||
## 核心机制
|
||||
|
||||
### 唯一校验(手写,按行现租户)
|
||||
|
||||
```text
|
||||
usernameExists / mobileExists 按 (username|mobile, tenant_id, 排除自身) 查;
|
||||
Update 时先读被改行的 tenant_id 再校验——校验范围必须等于
|
||||
uk_tenant_username / uk_tenant_mobile 索引范围(含 del_flag 由 dao.Ctx 恒过滤保证)。
|
||||
|
||||
历史教训:用 GetTenantID(超管=默认租户)会漏报 1062——必须按行现租户。
|
||||
```
|
||||
|
||||
### 角色同步(AssignRoles)
|
||||
|
||||
```text
|
||||
AssignRoles(userId, roleIds):全量重写 user_role(先删后插)
|
||||
→ clearIdentityCache(userId)
|
||||
Add/Update 均经此方法;Update 改 dept_id 也在该路径清缓存——部门变更即时生效
|
||||
(数据权限无需等 300s 缓存过期)
|
||||
```
|
||||
|
||||
### 租户用户上限(checkTenantUserMax)
|
||||
|
||||
统计租户现有用户数 vs tenant.max_user_num,超限返回 1007。在 user.Add 生效。
|
||||
|
||||
### 用户编码(genUserCode)
|
||||
|
||||
```text
|
||||
前缀=租户 code(平台/超管为 "SYS"),序号=该租户现有用户数+1
|
||||
格式 "{prefix}-%06d",如 T0001-000001
|
||||
```
|
||||
|
||||
### 数据权限接线
|
||||
|
||||
userSpec 声明 `DataScope: ApplyDataScope("dept_id")`——user 是当前唯一接线模块(全库仅 user 表有 dept_id 列)。
|
||||
|
||||
### 列表名称回填(userExpandNames)
|
||||
|
||||
列表行批量补 dept/level/position/role 名称与城市文本(namesById/dictName 批查防 N+1)。
|
||||
|
||||
## 导入导出
|
||||
|
||||
```text
|
||||
Import(POST /api/user/import):
|
||||
excel.ReadSheet → 表头映射 → 逐行:唯一校验 / 租户上限 / 默认密码 bcrypt
|
||||
(事务外预计算)/ 角色与城市解析 → 批量插入 → 返回成功/失败统计
|
||||
城市列经 ResolveCityCodes(名称串) 转编码入库
|
||||
|
||||
Export(GET /api/user/export):
|
||||
与列表同筛选(userFilter + DataScope,applyFilters 同链)
|
||||
→ ChunkQuery 500/页 → excel.WriteSheet
|
||||
导出可见集 = 列表可见集(导出=所见)
|
||||
```
|
||||
|
||||
## 城市编解码接入
|
||||
|
||||
Add/Update/Import/Detail 均接入包级函数:写侧 `ResolveCityCodes`(名称串 → 四级编码入库),读侧 `ResolveCityNames`(编码 → 名称还原)。机制见 [6.9 行政区划管理](./city)。
|
||||
|
||||
## 字段与安全边界
|
||||
|
||||
```text
|
||||
password/salt:bcrypt 双重加盐;接口永不返回
|
||||
密码留空 = 不修改(Update 语义)
|
||||
删除守卫:userDeleteGuard / 关联清理(user_role)
|
||||
SerializeMaps:status → user_status({field}Text 字典填充)
|
||||
FileFields: avatar + FileSaveDir: "user"(两段式上传自动转正)
|
||||
```
|
||||
Reference in New Issue
Block a user