签入版本
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
# 4.3 后端分层架构
|
||||
|
||||
:::tip 概述
|
||||
后端五层:api / controller / service / logic / dao。分层不是形式主义——每层有明确的"允许/禁止"清单,组合起来构成依赖红线体系。本节逐层说明职责与边界。
|
||||
:::
|
||||
|
||||
## 分层与调用关系
|
||||
|
||||
```text
|
||||
api/v1(契约层)
|
||||
▲ 依赖
|
||||
controller(HTTP 薄层)
|
||||
▲ 依赖接口
|
||||
service(接口层,手维护)
|
||||
▲ 实现
|
||||
logic(业务层,ModuleSpec 基座 + 钩子 + 手写逃生舱)
|
||||
▲ 唯一入口
|
||||
dao(数据访问层,红线条件 + 全局钩子)
|
||||
▲
|
||||
model/entity · model/hooks · gdb
|
||||
```
|
||||
|
||||
侧路能力(横向复用):`utility/{cache,captcha,crypto,excel,ip2layer,jwt,response,ua,upload}` 与 `consts`(错误码/缓存键/上下文键/日志类型)。
|
||||
|
||||
## 各层职责与边界
|
||||
|
||||
### api/v1(契约层)
|
||||
|
||||
+ 全部 Req/Res 结构体,**单包 v1**,每模块一文件(snake_case)
|
||||
+ gmeta 注解声明 method/path/summary/permission/log/logType/public
|
||||
+ gvalid 标签声明校验规则;`dc` 标签供 OpenAPI 文档
|
||||
+ **禁止**:业务逻辑、数据库引用
|
||||
|
||||
### controller(HTTP 薄层)
|
||||
|
||||
+ 一行透传 `return service.Xxx().Method(ctx, req)`
|
||||
+ **禁止**:业务 if/for、import dao、手写响应信封、手写参数解析
|
||||
|
||||
### service(接口层,手维护)
|
||||
|
||||
+ 每模块一文件:接口 `IXxx` + 实例变量 + 取用函数 `Xxx()` + 注册函数 `RegisterXxx()`
|
||||
+ 未注册时取用 panic(启动即暴露接线错误)
|
||||
+ **禁止**:整体运行 `gf gen service`(会覆盖手工重组结构)
|
||||
|
||||
### logic(业务层)
|
||||
|
||||
+ 每模块一文件:`sXxx` 实现 + `xxxSpec` 声明 + `init()` 注册 + 标准委托 + 扩展手写
|
||||
+ 共享能力经 `base.go`(15 个导出上限)与 `module_spec.go` 基座
|
||||
+ 跨模块复用走同包包级函数(ResolveCityNames / LoadIdentity / ApplyDataScope / DictGetText…)
|
||||
+ **禁止**:反向 import task;绕过 dao;在钩子里做 HTTP/文件 IO
|
||||
|
||||
### dao(数据访问层)
|
||||
|
||||
+ 自定义模板产出:构造时挂 `hooks.Handler()`;`Ctx` 覆写注入红线条件
|
||||
+ **禁止**:绕过直连 `g.DB()`;手写 del_flag/tenant_id 条件;手改 dao/internal/
|
||||
|
||||
## 上下文键(consts/context_key.go)
|
||||
|
||||
JWT 中间件写入身份,全链路 `consts.GetXxx(ctx)` 读取:
|
||||
|
||||
| 写入方 | 键 |
|
||||
|--------|-----|
|
||||
| JWT 中间件 | UserID / TenantID / Username / Realname / TokenJTI / TokenExpire / Roles / Perms / DataScope / DeptID |
|
||||
| Response 中间件 | ClientIP / UserAgent / ReqMethod / ReqURL |
|
||||
|
||||
::: warning 两套租户读法(易错点)
|
||||
`CtxTenantID(ctx)` —— 原始值(0=平台),仅供租户设施(hooks/中间件)使用
|
||||
`GetTenantID(ctx)` —— 业务归属值(租户返回自身,超管返回 tenant.defaultId),**新增/导入/唯一校验统一用它**
|
||||
:::
|
||||
|
||||
数据范围读法:`GetDataScope(ctx)` 档位(1-全部;未注入回退全部);`GetDeptID(ctx)` 部门 ID(0=未分配)。
|
||||
|
||||
## 响应与错误码
|
||||
|
||||
统一信封(utility/response):
|
||||
|
||||
```text
|
||||
成功:{"code":0, "ok":true, "msg":"操作成功", "data":{...}}
|
||||
失败:{"code":1, "ok":false, "msg":"...", "data":{}}
|
||||
未认证:HTTP 401 + {"code":401,...}
|
||||
```
|
||||
|
||||
Fail 映射规则:gerror 码 == 401 原样保留;其余一律降级 code=1(前端 ResultEnum.ERROR 才弹 msg)。consts 中的细分错误码(1001 用户不存在…)主要用于日志与语义区分。
|
||||
|
||||
分页契约:`PageData[T]` = `{records, total, size, current, pages}`,records 为空输出 `[]` 而非 null;pageNo 默认 1,pageSize 默认 10 上限 500。
|
||||
|
||||
## 横切约定速查
|
||||
|
||||
```text
|
||||
* 主键:int 数据库自增;新增后 result.LastInsertId() 回读
|
||||
* 软删:del_flag 0 正常 / 1 删除;删除接口全部软删
|
||||
* 表前缀:goframe_;文件命名 snake_case 对齐 dao(前端文件仍驼峰)
|
||||
* 入参/出参 json 驼峰;唯一例外:登录/令牌接口返回
|
||||
access_token/refresh_token/expires_in/token_type(OAuth2 惯例,勿改)
|
||||
* 缓存键统一前缀 easygoadmin:
|
||||
* 权限码 sys:{module小驼峰}:{action};超管 ROLE_SUPER_ADMIN;前端通配 *:*:*
|
||||
* 审计列:id/create_user/create_time/update_user/update_time/del_flag(租户表另有 tenant_id)
|
||||
```
|
||||
@@ -0,0 +1,111 @@
|
||||
# 4.6 核心基座体系
|
||||
|
||||
:::tip 概述
|
||||
本项目没有传统"基类继承"——用**组合**替代继承:ModuleSpec 泛型基座(module_spec.go)承担 CRUD 流水线,base.go 共享能力层提供横切函数。本节解释两者的分工与设计纪律。
|
||||
:::
|
||||
|
||||
## 组合替代继承的架构
|
||||
|
||||
```text
|
||||
┌────────────────────────────────────────────────────┐
|
||||
│ 模块 logic 文件(example.go) │
|
||||
│ var exampleSpec = ModuleSpec[...]{ 声明差异 } │──▶ 委托
|
||||
│ func 方法() { exampleSpec.Page(...) } │
|
||||
│ func 扩展() { 手写(合法逃生舱)} │
|
||||
└───────────────────────┬────────────────────────────┘
|
||||
│ 复用
|
||||
┌───────────────────────▼────────────────────────────┐
|
||||
│ module_spec.go:ModuleSpec 泛型基座 │
|
||||
│ applyFilters / readTransform / saveTransform │
|
||||
│ checkUniqueRules / FillTextFields / buildTree │
|
||||
└───────────────────────┬────────────────────────────┘
|
||||
│ 调用
|
||||
┌───────────────────────▼────────────────────────────┐
|
||||
│ base.go 共享能力层(导出上限 15 个) │
|
||||
│ 文件转正 / 富文本 / 字典文本 / 唯一校验 / │
|
||||
│ 事务 / 分块查询 / 泛型树 / 临时目录清理 │
|
||||
└────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## base.go 能力清单(internal/logic/base.go)
|
||||
|
||||
| 能力 | 函数 | 一句话职责 |
|
||||
|------|------|-----------|
|
||||
| 文件字段 | SaveFileFields / SaveMultiFileFields | 批量转正(原地更新 map) |
|
||||
| | SaveFileField / SaveMultiFileField | 单值便捷版 |
|
||||
| 富文本 | LocalizeContentFields | 存库本地化([IMG_URL] 占位符化) |
|
||||
| | ExpandContentFields | 读取还原(**唯一还原出口**) |
|
||||
| 枚举文本 | DictGetText | 字典值 → 显示名(进程内缓存 300s) |
|
||||
| | SerializeMapOnRead | map 行补 {field}Text |
|
||||
| | LogTypeText | 操作日志类型 → 中文(静态表) |
|
||||
| 唯一校验 | CheckUnique | UX 快速报错(TOCTOU,真值靠 uk_* 索引) |
|
||||
| 事务 | WithTrans | 事务包裹(错误自动回滚) |
|
||||
| 分块 | ChunkQuery | 分块遍历(导出防 OOM,默认 500/页) |
|
||||
| 树 | BuildTree[T] | 泛型平铺转树 |
|
||||
| 清理 | CleanTemp | 清理上传临时日期目录 |
|
||||
|
||||
架构纪律(文件头约束,真实注释):
|
||||
|
||||
```text
|
||||
1) 按能力分区:文件字段 / 富文本 / 枚举文本 / 唯一校验 / 事务 / 分块查询 / 树
|
||||
2) 依赖方向单向:只允许依赖 dao / utility / consts,禁止引用模块文件
|
||||
3) 导出函数上限 15 个,超出按能力拆 internal/logic/base/ 子包
|
||||
4) CheckUnique 仅为 UX 快速报错(TOCTOU),并发唯一性靠 uk_* 索引
|
||||
5) [IMG_URL] 占位符是持久化契约,还原只能走 ExpandContentFields
|
||||
```
|
||||
|
||||
## ModuleSpec 基座流水线(module_spec.go)
|
||||
|
||||
```text
|
||||
Page applyFilters → Count → applyOrder.Page → structRows → readTransform
|
||||
→ AfterPageList
|
||||
List applyFilters → 排序 → req.limit 上限 → structRows → readTransform
|
||||
→ IsTree 时 buildTree(叶子 Children 预置 [] 而非 null)
|
||||
Detail Model.Where(id).One → Struct → readTransform → AfterDetail
|
||||
Add ToData → BeforeAdd → saveTransform(文件/富文本转正)→ checkUniqueRules
|
||||
→ Insert → LastInsertId → AfterAdd → 返回 id
|
||||
Update ToData → BeforeUpdate → saveTransform → checkUniqueRules(排除自身)
|
||||
→ Update(仅更新 data 中出现的键)→ AfterUpdate
|
||||
Delete BeforeDelete → Delete(钩子改写软删)→ AfterDelete
|
||||
BatchDelete BeforeBatchDelete → 逐条 BeforeDelete+软删 → AfterBatchDelete
|
||||
```
|
||||
|
||||
readTransform 顺序:FileFields 补 URL → MultiFileFields 补 URL → ContentFields 展开 [IMG_URL] → FillTextFields({field}Text)。
|
||||
|
||||
## 设计纪律(为什么基座不会腐化)
|
||||
|
||||
::: warning 基座三铁律
|
||||
1. **基座禁止"按模块分支"**——差异只进声明字段与钩子;写不进钩子 → 手写(合法逃生舱)。这条纪律保证基座永远不出现 `if module == "user"` 式代码。
|
||||
2. **钩子用具体函数类型**,req any 经 mustAs[T] 断言(失败=编程错误,panic 暴露而非带病运行)。
|
||||
3. **钩子内禁止 append、禁止 HTTP/文件 IO**——文件迁移统一在 saveTransform 完成。
|
||||
:::
|
||||
|
||||
applyFilters 固定顺序:`LikeFields → EqFields → KeywordFields → Filter → DataScope`;租户/软删隔离由 dao.Ctx **先于本链**注入(任何模块不可绕过)。
|
||||
|
||||
## ModuleSpec 声明字段全景
|
||||
|
||||
```text
|
||||
Model func(ctx) *gdb.Model 数据源(通常 dao.Xxx.Ctx)
|
||||
LikeFields []string 逐列 LIKE(值取 req 同名驼峰字段,空跳过)
|
||||
EqFields []string 等值(零值跳过)
|
||||
KeywordFields []string req.keyword 多列 OR(基座保留能力)
|
||||
Filter func(ctx, m, req) *gdb.Model 复杂条件逃生舱
|
||||
OrderBy / ListOrderBy / OrderableFields 排序(白名单控制前端排序)
|
||||
IsTree / ParentOf / ChildSetter 树形
|
||||
SerializeMaps map[字段]字典编码 {field}Text 填充
|
||||
FileFields / MultiFileFields / ContentFields / FileSaveDir 文件与富文本
|
||||
UniqueFields []UniqueRule{Field, Group} 唯一性预检(Group 分组唯一)
|
||||
DataScope DataScopeFunc 行级数据权限(ApplyDataScope 接线)
|
||||
ToData req → map[string]any 新增/修改共用转换
|
||||
钩子 Before/After × Add/Update/Delete/Batch + AfterPageList/AfterDetail
|
||||
```
|
||||
|
||||
## 与"基类方案"的对比
|
||||
|
||||
| 维度 | 本项目(泛型基座+组合) | 传统基类继承 |
|
||||
|------|----------------------|-------------|
|
||||
| 差异表达 | 声明字段 + 钩子 | 重写虚方法 |
|
||||
| 类型安全 | 泛型三类型参数编译期约束 | 常退化 any/map |
|
||||
| 复用粒度 | 函数级(base.go 自由组合) | 类级(受继承链束缚) |
|
||||
| 基座污染风险 | 三铁律禁止按模块分支 | 基类易堆积 if-else |
|
||||
| 逃生舱 | 手写方法(完全自由) | 多重继承/桥接方法 |
|
||||
@@ -0,0 +1,94 @@
|
||||
# 4.8 缓存架构设计
|
||||
|
||||
:::tip 概述
|
||||
缓存实现为 utility/cache 统一门面:Redis 优先,连接失败或运行期出错自动降级进程内存 gcache(UsingMemory() 可查)。三层缓存各司其职,Redis 键统一前缀 easygoadmin:。
|
||||
:::
|
||||
|
||||
## 缓存分层图
|
||||
|
||||
```text
|
||||
请求 ──▶ ① 进程内缓存(sync.Map,无 Redis 交互,重启即清)
|
||||
dictTextCache(300s) · reqFieldCache/textPlanCache(反射计划,永不过期)
|
||||
maskReCache(正则,永不过期)
|
||||
──▶ ② Redis(跨实例共享,TTL 自动回收)
|
||||
黑名单/在线/验证码/字典/租户摘要/身份/限流/任务锁
|
||||
──▶ ③ 数据库(唯一真相,dao.Ctx 全量过滤条件)
|
||||
|
||||
设计原则:② ③ 之间的一切缓存都必须有明确的失效点或短 TTL 兜底;
|
||||
① 仅缓存"计算计划/只读文本",不允许缓存业务状态。
|
||||
```
|
||||
|
||||
## Redis 键全集
|
||||
|
||||
| 键模式 | TTL | 写入方 | 读取方 |
|
||||
|--------|-----|--------|--------|
|
||||
| `easygoadmin:token:blacklist:{jti}` | 剩余有效期+60s | Logout/Refresh 轮换/RemoveToken/ForceLogout | JWT 中间件 Exists 判定 |
|
||||
| `easygoadmin:online:jti:set`(zset) | 成员随 info(score=过期 ts) | MarkOnline/MarkOffline | OnlineUser.Page(ZRangeByScore≥now) |
|
||||
| `easygoadmin:online:info:{jti}` | access 剩余+60s | MarkOnline/MarkOffline | OnlineUser.Page/Detail/ForceLogout |
|
||||
| `easygoadmin:captcha:{uuid}` | 300s | Captcha 生成 | Login/Verify(一次性) |
|
||||
| `easygoadmin:dict:item:{code}` | 3600s | dict.setCache/RefreshCache | dict_item 快捷接口/AliasList |
|
||||
| `easygoadmin:tenant:info:{id}` | 60s | Tenant 中间件/Login 路径 | Tenant 中间件/Login |
|
||||
| `easygoadmin:user:identity:{uid}` | 300s | JWT 中间件(回源 Loader) | JWT 中间件(Roles/Perms/DataScope/DeptId JSON) |
|
||||
| `easygoadmin:user:perms:{uid}` / `user:roles:{uid}` | 约定键(保留扩展) | — | 主路径用 identity 合并键,变更时一并清理 |
|
||||
| `ratelimit:{ip}:{path}:{窗口起点}` | window+5s | RateLimit incr | RateLimit |
|
||||
| `job:lock:{jobId}` | 300s 兜底 | jobTryLock(NX) | jobTryLock(执行完 defer Del) |
|
||||
|
||||
## 主动失效点清单
|
||||
|
||||
**身份缓存 `user:identity:{uid}` 删除时机**:
|
||||
|
||||
```text
|
||||
* role.SavePermission 保存角色权限 → clearRoleIdentityCache(roleId)
|
||||
→ 查该角色全部 user → 逐个 Del
|
||||
* role.Update 修改角色(含 data_scope 变更)→ AfterUpdate → 同上
|
||||
* user.AssignRoles 分配/调整角色 → clearIdentityCache(userId)
|
||||
(user.Update / user.Add 均经 AssignRoles——改部门 dept_id 亦在此清缓存)
|
||||
* 版本兼容:Identity 新增字段反序列化缺省为 0,读侧 GetDataScope 回退"全部"
|
||||
——升级无需强清存量缓存
|
||||
```
|
||||
|
||||
**其他**:租户摘要在 tenant 状态/过期变更时由 AfterUpdate clearCache 钩子清理;字典 Redis 缓存由 RefreshCache 主动重建(dict_item 变更不主动清,最长 1 小时延迟);黑名单靠 TTL 自然过期(过期即令牌本身已失效);验证码命中即删。
|
||||
|
||||
## 进程内缓存明细
|
||||
|
||||
| 缓存 | 位置 | 内容 | TTL |
|
||||
|------|------|------|-----|
|
||||
| dictTextCache | base.go | {code}:{value} → 显示名 | 300s |
|
||||
| reqFieldCache / textPlanCache / childrenIdxCache | module_spec.go | 反射索引与填充计划 | 永不过期(类型不变即安全) |
|
||||
| maskReCache | middleware/util.go | 脱敏正则 | 永不过期 |
|
||||
|
||||
## 内存降级的影响面
|
||||
|
||||
Redis 不可用时 `UsingMemory()=true`,以下能力**退化单机语义**:
|
||||
|
||||
```text
|
||||
限流计数 各实例各自计数(阈值实际翻倍)
|
||||
任务锁 job:lock 退化单机,多实例可能并发执行任务
|
||||
在线用户 只看本实例登录的会话
|
||||
JWT 黑名单 本实例签发的登出/强退不拦截其他实例
|
||||
验证码 单实例内有效
|
||||
字典缓存 各实例独立回源
|
||||
```
|
||||
|
||||
运维可通过 `GET /api/cache/info` 的 `mode` 字段确认当前模式(redis/memory)。
|
||||
|
||||
## TTL 设计原则(新增缓存遵循)
|
||||
|
||||
```text
|
||||
1) 状态判定类(黑名单/租户状态):TTL ≤ 业务有效期,且必须有主动失效点;
|
||||
宁可短 TTL 多回源,不可长 TTL 陈旧放行
|
||||
2) 展示文本类(字典/归属地):TTL 300s~3600s + 刷新接口,接受分钟级延迟
|
||||
3) 高频读聚合类(identity):合并为单键 JSON(一次回源装多个集合),
|
||||
TTL 300s,变更路径主动 Del
|
||||
4) 计数/锁类:键内嵌窗口起点或场景唯一 id,TTL 只做回收不参与语义
|
||||
5) 任何经 cache.Set 的值必须可 JSON 序列化;读侧 Unmarshal 失败视为未命中回源
|
||||
```
|
||||
|
||||
## 排障速查
|
||||
|
||||
```text
|
||||
键不存在/值陈旧 先查是否内存降级(redis-cli KEYS 'easygoadmin:*' 对比)
|
||||
权限不生效 DEL easygoadmin:user:identity:{uid}
|
||||
租户被禁仍可访问 等 60s 或 DEL easygoadmin:tenant:info:{id}
|
||||
多实例行为不一致 确认 Redis 可用(/api/cache/info mode 应为 redis)
|
||||
```
|
||||
@@ -0,0 +1,116 @@
|
||||
# 4.7 横切机制
|
||||
|
||||
:::tip 概述
|
||||
审计字段盖章、软删除改写、租户隔离三类横切逻辑不在任何业务代码中出现——它们统一在 gdb 模型钩子层(internal/model/hooks)执行。配合中间件链与 Bind 包装层,构成本项目的横切机制全景。
|
||||
:::
|
||||
|
||||
## 横切机制全景
|
||||
|
||||
```text
|
||||
请求横切(中间件链) 数据横切(模型钩子) 契约横切(Bind 包装层)
|
||||
CORS Insert 盖章 权限校验(gmeta permission)
|
||||
Response(panic/元信息) Update 盖章 参数解析(gvalid)
|
||||
RateLimit Delete → 软删 信封写出
|
||||
Demo del_flag=0 注入 OpenAPI 补注册
|
||||
JWT(身份) tenant_id 注入
|
||||
Tenant(状态)
|
||||
OperationLog(采集)
|
||||
```
|
||||
|
||||
## 全局钩子(internal/model/hooks/hooks.go)
|
||||
|
||||
钩子经 `dao.{X}.Ctx(ctx)` 构造模型时挂载(自定义模板 dao_index.tpl):
|
||||
|
||||
| 钩子 | 触发 | 行为 |
|
||||
|------|------|------|
|
||||
| Insert | 任意 Insert | 主键交数据库自增(不填 id);补 del_flag=0、create_time、create_user(ctx 用户名,回退真实姓名);租户级表自动携带 tenant_id |
|
||||
| Update | 任意 Update | 自动盖章 update_time / update_user(仅 map 数据生效) |
|
||||
| Delete | 任意 Delete | 物理删除一律改写为 `UPDATE del_flag=1` |
|
||||
|
||||
### 租户盖章规则(Insert)
|
||||
|
||||
```text
|
||||
租户用户插入未显式指定 tenant_id → 填自身(防逃逸)
|
||||
超管未显式指定 → 填 tenant.defaultId(默认 1)
|
||||
显式指定具体值 → 从显式值
|
||||
显式 0 → 建平台共享行
|
||||
```
|
||||
|
||||
### 查询条件注入(hooks.ApplyCtx)
|
||||
|
||||
```text
|
||||
所有表: WHERE del_flag = 0
|
||||
租户级表且非平台上下文:WHERE tenant_id IN (0, 当前租户)
|
||||
超管(ctx tenant_id=0):不过滤,全量可见
|
||||
```
|
||||
|
||||
## 租户级表清单(tenantTables,9 张)
|
||||
|
||||
```text
|
||||
goframe_user / role / dept / level / position / article / category / notice / link
|
||||
```
|
||||
|
||||
**新增租户级表必须同步修改 hooks.go 的 tenantTables**(代码生成器不自动维护此清单)。漏加的后果是租户数据互相可见。
|
||||
|
||||
## 两层行级过滤的叠加关系
|
||||
|
||||
```text
|
||||
第一层(硬规则,不可配置):租户隔离
|
||||
dao.Ctx / hooks.ApplyCtx 注入 tenant_id IN(0,自身)
|
||||
先于一切业务过滤,任何模块不可绕过
|
||||
|
||||
第二层(可配置):角色数据范围(data_scope 四档)
|
||||
ModuleSpec.DataScope 挂点,在 applyFilters 链末环叠加:
|
||||
Like → Eq → Keyword → Filter → DataScope
|
||||
共享工厂 ApplyDataScope(deptColumn),模块一行声明即生效
|
||||
```
|
||||
|
||||
详见 [5.8 多租户隔离](../core/tenant) 与 [5.9 数据权限](../core/data-scope)。
|
||||
|
||||
## 请求横切:中间件与包装层分工
|
||||
|
||||
```text
|
||||
中间件链(跨路由共性) Bind 包装层(路由级个性)
|
||||
认证状态(401) 权限码校验(403,读 gmeta)
|
||||
租户状态(1005/1006) 参数解析与校验(gvalid)
|
||||
限流(429)/演示(1015) 信封写出
|
||||
日志采集(不短路) OpenAPI 元数据
|
||||
```
|
||||
|
||||
权限校验不设独立中间件——因为它需要每路由不同的权限码,放在读路由元数据的包装层最自然(与日志注解同源,"注解即事实")。
|
||||
|
||||
## 日志横切(OperationLog)
|
||||
|
||||
```text
|
||||
gmeta log 标签 → 路由元数据 → 中间件组装 entry(脱敏 param/result)
|
||||
→ 有界通道 2048(满则丢弃告警,不阻塞业务)
|
||||
→ 单 worker 攒批(100 条或 1s flush)
|
||||
→ LogSink(operation_log 模块 init 注册)→ INSERT goframe_operation_log
|
||||
```
|
||||
|
||||
脱敏规则:param 中 password/oldPassword/newPassword/salt/access_token/refresh_token → `***`;result 截断 8000 字节(UTF-8 边界回退)。
|
||||
|
||||
## 配置横切:环境变量覆盖(utility/config)
|
||||
|
||||
```text
|
||||
启动入口 cmd.Run 最先执行 config.RegisterEnvOverride()
|
||||
→ 包装默认文件适配器为 env-first 适配器(gcfg.Adapter 官方扩展点)
|
||||
→ 全部 Get/MustGet 读取点零改动获得覆盖能力(含 gdb/gredis 经整节点读连接配置)
|
||||
```
|
||||
|
||||
```text
|
||||
优先级 GF_ 环境变量 > 配置文件(12-Factor;未设环境变量时行为不变)
|
||||
键名 配置路径大写下划线 + GF_ 前缀(database.default.link → GF_DATABASE_DEFAULT_LINK)
|
||||
空值 环境变量空串视为未设置,不清空文件有效值
|
||||
范围 仅覆盖配置文件中已存在的键(新增键先在 config.yaml 声明)
|
||||
```
|
||||
|
||||
典型场景:容器部署注入 `GF_DATABASE_DEFAULT_LINK` / `GF_FILE_UPLOADDIR` / `GF_JWT_SECRET`,镜像内 config.yaml 保持默认不动(同一镜像可部署多环境)。键名规则与 Docker 注入方式见 [2.5 配置文件说明](../quickstart/env-vars)。
|
||||
|
||||
## 横切开发纪律
|
||||
|
||||
+ 新增横切逻辑优先问:属于请求横切(中间件)/ 数据横切(钩子)/ 契约横切(Bind)/ 配置横切(utility/config)哪一层?不要散落在业务代码。
|
||||
+ 修改钩子必须全表回归——它影响 25 张表的所有写入。
|
||||
+ 钩子内禁止业务分支判断(如"某模块跳过盖章")——确有需要走逃生舱(显式值优先于自动盖章)。
|
||||
+ 缓存类横切(字典/身份/租户摘要)必须有明确失效点或短 TTL 兜底(见 [4.8 缓存架构设计](./cache))。
|
||||
+ 业务代码读配置一律用 `g.Cfg().Get/MustGet`,不要用 `GetWithEnv`(文件优先,环境变量永不生效)。
|
||||
@@ -0,0 +1,131 @@
|
||||
# 4.9 数据库设计
|
||||
|
||||
:::tip 概述
|
||||
25 张业务表、统一审计列、13 个唯一索引、9 张租户级表、注释枚举驱动字典——本节给出数据模型全景与 ER 关系。五方言定稿脚本位于 document/{mysql,postgresql,sqlserver,oracle,sqlite}/,MySQL 版为规范基准。
|
||||
:::
|
||||
|
||||
## ER 关系图(逻辑外键,无物理外键约束)
|
||||
|
||||
```text
|
||||
goframe_tenant ──< goframe_user >──┬──< goframe_user_role >── goframe_role
|
||||
│ │
|
||||
│ goframe_role_menu
|
||||
│ │
|
||||
├──< goframe_dept ▼
|
||||
├──< goframe_level goframe_menu
|
||||
├──< goframe_position ▲
|
||||
│ (user.level_id/position_id/dept_id 引用)
|
||||
│
|
||||
goframe_config ──< goframe_config_item
|
||||
goframe_dict ──< goframe_dict_item
|
||||
goframe_category(树 parent_id) ──< goframe_article(tenant_id, category_id)
|
||||
article: cover 单图 / images 多图 / content 富文本
|
||||
goframe_notice / goframe_link(tenant_id)
|
||||
goframe_city(树 pid + parent_code,全国行政区划,无租户)
|
||||
goframe_job ──< goframe_job_log(job_id, job_name 冗余)
|
||||
goframe_operation_log / goframe_login_log(独立日志表,无租户)
|
||||
goframe_file_template(全局文件模板,code 唯一)
|
||||
goframe_param(全局系统参数,code 唯一)
|
||||
goframe_example(案例演示表,导入导出/状态演示用)
|
||||
|
||||
">" 侧为多端;如 user >-- user_role --< role 为多对多。
|
||||
```
|
||||
|
||||
## 全表统一规范
|
||||
|
||||
```text
|
||||
表前缀 goframe_;存储引擎 InnoDB;字符集 utf8mb4。
|
||||
每张表统一审计列(25 表齐备):
|
||||
|
||||
id int NOT NULL AUTO_INCREMENT 主键(数据库自增,禁用雪花)
|
||||
create_user varchar(50) 创建人(ctx 用户名)
|
||||
create_time datetime DEFAULT (now()) 创建时间
|
||||
update_user varchar(50) 更新人
|
||||
update_time datetime DEFAULT (now()) 更新时间
|
||||
del_flag int DEFAULT 0 0-正常 1-已删除(软删)
|
||||
|
||||
租户级表另有 tenant_id int DEFAULT 0(0=平台级),共 9 张。
|
||||
删除一律软删(deleteHook 改写);查询一律经 dao.Ctx 过滤 del_flag=0。
|
||||
```
|
||||
|
||||
## 25 张表清单
|
||||
|
||||
| 表名 | 中文注释 | 域 | 租户级 | 特征列/说明 |
|
||||
|------|---------|-----|--------|------------|
|
||||
| goframe_article | 文章表 | content | 是 | category_id/cover/images(多图)/content(富文本)/status/click/sort |
|
||||
| goframe_category | 文章分类表 | content | 是 | 树形 parent_id、category_name/code/sort |
|
||||
| goframe_city | 城市行政区划表 | data | 否 | 树形 pid+parent_code、city_code/area_code/level、lng/lat/pinyin(大数据量表) |
|
||||
| goframe_config | 系统配置表 | data | 否 | name/code/sort |
|
||||
| goframe_config_item | 配置项表 | data | 否 | config_id/value/options/type/status/sort |
|
||||
| goframe_dept | 部门表 | system | 是 | 树形、删除有子级/用户守卫 |
|
||||
| goframe_dict | 数据字典表 | data | 否 | name/code;缓存 dict:item:{code} |
|
||||
| goframe_dict_item | 字典项表 | data | 否 | dict_id/name/value/sort |
|
||||
| goframe_example | 案例演示表 | tool | 否 | 七件套+导入导出+状态演示 |
|
||||
| goframe_file_template | 文件模板表 | tool | 否 | code 唯一;getTemplateByCode |
|
||||
| goframe_job | 定时任务表 | monitor | 否 | job_name/cron_expression/execute_policy/is_sync/url/params/status(0未发布/1运行/2暂停) |
|
||||
| goframe_job_log | 定时任务日志表 | monitor | 否 | job_id/status/耗时/exception_info |
|
||||
| goframe_level | 职级表 | system | 是 | 导入导出启用;user_count 展示 |
|
||||
| goframe_link | 友链表 | content | 是 | 无 status 列 |
|
||||
| goframe_login_log | 登录日志表 | monitor | 否 | log_type(1登录/2退出)/ip/location/os/browser/status |
|
||||
| goframe_menu | 菜单表 | system | 否 | 树形;type(0菜单/1权限节点)/permission/status/hide/sort |
|
||||
| goframe_notice | 通知公告表 | data | 是 | status |
|
||||
| goframe_operation_log | 操作日志表 | monitor | 否 | title/type(log_type枚举)/param/result/ip/location/consume_time/status/error |
|
||||
| goframe_param | 系统参数表 | data | 否 | code 唯一 |
|
||||
| goframe_position | 岗位表 | system | 是 | |
|
||||
| goframe_role | 角色表 | system | 是 | code/data_scope(四档行级数据权限) |
|
||||
| goframe_role_menu | 角色菜单关联表 | system | 否 | role_id+menu_id(无模块文件,归 role logic) |
|
||||
| goframe_tenant | 租户表 | system | 否 | code(T+ID)/status/expire_time/max_user_num |
|
||||
| goframe_user | 用户表 | system | 是 | username/password(bcrypt)/salt/mobile/email/avatar/dept_id/level_id/position_id/status/code |
|
||||
| goframe_user_role | 用户角色关联表 | system | 否 | user_id+role_id(无模块文件,归 user logic) |
|
||||
|
||||
## 唯一索引清单(13 个)
|
||||
|
||||
| 表 | 索引名 | 列 | 语义 |
|
||||
|----|--------|-----|------|
|
||||
| goframe_category | uk_tenant_name | (tenant_id, category_name, del_flag) | 租户内分类名唯一 |
|
||||
| goframe_config | uk_code | (code) | 全局配置编码唯一 |
|
||||
| goframe_dept | uk_tenant_name | (tenant_id, name, del_flag) | 租户内部门名唯一 |
|
||||
| goframe_dict | uk_code | (code) | 全局字典编码唯一 |
|
||||
| goframe_file_template | uk_code | (code) | 全局模板编码唯一 |
|
||||
| goframe_level | uk_tenant_name | (tenant_id, name, del_flag) | 租户内职级名唯一 |
|
||||
| goframe_param | uk_code | (code) | 全局参数编码唯一 |
|
||||
| goframe_position | uk_tenant_name | (tenant_id, name, del_flag) | 租户内岗位名唯一 |
|
||||
| goframe_role | uk_tenant_code | (tenant_id, code, del_flag) | 租户内角色编码唯一 |
|
||||
| goframe_tenant | uk_code | (code) | 租户编码全局唯一 |
|
||||
| goframe_user | uk_tenant_username | (tenant_id, username, del_flag) | 租户内用户名唯一 |
|
||||
| goframe_user | uk_tenant_mobile | (tenant_id, mobile, del_flag) | 租户内手机号唯一 |
|
||||
| goframe_user | uk_tenant_code | (tenant_id, code, del_flag) | 租户内用户编码唯一 |
|
||||
|
||||
要点:索引含 del_flag(软删行不占坑,与 dao.Ctx 恒过滤语义一致);含 tenant_id 的索引,模块唯一预检范围必须等于索引范围。
|
||||
|
||||
## 枚举列约定(列注释驱动字典与生成器)
|
||||
|
||||
```text
|
||||
status(账号/租户/字典项等) 1-正常 2-禁用
|
||||
article.status 0-正常 1-下架
|
||||
city.level 0-省份 1-城市 2-县区 3-街道 4-社区
|
||||
menu.type 0-菜单 1-权限节点
|
||||
job.status 0-未发布 1-运行中 2-暂停
|
||||
job.execute_policy 1-立即执行 2-执行一次 3-放弃执行
|
||||
job.is_sync 1-同步 0-异步
|
||||
role.data_scope 1-全部 2-本部门及以下 3-本部门 4-仅本人
|
||||
del_flag 0-正常 1-已删除
|
||||
login_log.log_type 1-登录 2-退出
|
||||
operation_log.type 见 consts/log_type.go(41 个值,静态映射不走字典)
|
||||
```
|
||||
|
||||
## 大表与性能注意
|
||||
|
||||
+ **goframe_city** 全国行政区划大表(AUTO_INCREMENT 已至 78 万+):查询走 parent_code/pid 索引路径(级联懒加载 getCityList),避免全表 List;导入/迁移注意批大小。
|
||||
+ **日志三表**(operation_log/login_log/job_log)只增不改,依赖 LogClean 任务按 `job.logRetentionDays`(默认 90 天)分批物理清理。
|
||||
+ **导出接口**统一 ChunkQuery 分批(500/页)防 OOM。
|
||||
|
||||
## 建表/变更工作流
|
||||
|
||||
```text
|
||||
1) 以 document/mysql/ 定稿为基准补 ALTER(含列注释枚举、uk_* 唯一索引、uk_/idx_ 命名规范)
|
||||
2) gf gen dao -c hack/config.yaml 刷新 dao/entity/do(service 手维护)
|
||||
3) 同步 document/ 其余四方言脚本(人工校对)
|
||||
4) 租户级表 → 修改 hooks.tenantTables
|
||||
5) go build ./... 重启;必要时 go run ./cmd/migrator 同步到其他环境库
|
||||
```
|
||||
@@ -0,0 +1,83 @@
|
||||
# 4.5 目录结构设计
|
||||
|
||||
:::tip 概述
|
||||
目录结构是架构决策的物理体现。本节解释"为什么这样放"——平铺单包、snake_case 命名、internal 边界、utility 无业务依赖等约定背后的原因。完整目录树见 [1.3 项目目录结构](../directory)。
|
||||
:::
|
||||
|
||||
## 后端顶层布局与设计意图
|
||||
|
||||
```text
|
||||
main.go 入口极薄:注册五库驱动 + Redis 适配器 → cmd.Run
|
||||
internal/ 私有包边界(Go internal 语义:外部不可 import)
|
||||
cmd/ 启动编排独立成包——把 main 的"做事"与"声明"分离
|
||||
api/v1/ 契约层放 internal 外?——不,本项目放顶层 api/,
|
||||
便于生成器与 gf 工具链定位,也表达"对外契约"语义
|
||||
controller/ logic/ service/ dao/ 四层各自单包平铺
|
||||
middleware/ 中间件 + wire.go 函数变量接线(不 import 业务包)
|
||||
migrate/ 迁移引擎独立——gdb 裸连,必须与 dao 体系隔离
|
||||
model/hooks/ 全局钩子独立于 entity/do(生成物与手写物分离)
|
||||
router/ router.go(注册)+ bind.go(反射)+ meta/(元数据表)
|
||||
task/ 定时任务实现——单向依赖 logic,cmd 空白导入触发
|
||||
templates/ 生成器模板独立于 internal(模板演进与代码解耦)
|
||||
utility/ 基础能力包——无业务依赖,可供任何层使用
|
||||
manifest/ 配置与数据(ip2region.xdb)
|
||||
resource/ 静态资源(SetServerRoot 提供 /upload/**、swagger 页面)
|
||||
hack/ 工具链配置(gf gen dao 模板、golden 回归基线)
|
||||
```
|
||||
|
||||
## 平铺 vs 分目录的取舍
|
||||
|
||||
| 维度 | 单包平铺(选择) | 按域分目录(弃用) |
|
||||
|------|----------------|------------------|
|
||||
| 跨模块复用 | 同包直接调用包级函数,零 import、无循环依赖风险 | 需 import,深组合易循环 |
|
||||
| 新增模块 | 4 个文件各就各位,无目录推断 | 多级目录间跳转 |
|
||||
| 生成器 | 目标路径唯一确定 | 需推断层级 |
|
||||
| init 注册 | 同包全量加载,无需 blank import | 每包需 blank import |
|
||||
| 代价 | 单包文件多(30+) | 包粒度细 |
|
||||
|
||||
Go 编译器对大包无压力,而"跨模块直接调用"的收益在中后台系统里极高(city 编解码、身份加载、字典文本被大量模块复用)。
|
||||
|
||||
## 文件命名规范
|
||||
|
||||
```text
|
||||
后端(对齐 dao 产物):表名去 goframe_ 前缀的 snake_case
|
||||
api/v1/config_item.go · internal/controller/config_item.go
|
||||
internal/logic/config_item.go · internal/service/config_item.go
|
||||
|
||||
前端:驼峰
|
||||
src/api/data/config.ts · src/views/data/configItem/
|
||||
|
||||
原因:后端文件名与 dao/entity/do 生成物一致,生成器与人工开发心智统一;
|
||||
前端沿用 JS 社区驼峰惯例,两边各自从俗。
|
||||
```
|
||||
|
||||
## internal 的边界价值
|
||||
|
||||
`internal/` 是 Go 编译器强制的私有边界——仓库外代码无法 import,防止外部项目把实现细节当公共 API 依赖。分层红线(controller 不 import dao 等)靠 code review + 生成器模板保证,internal 防的是"仓库外扩散"。
|
||||
|
||||
## utility 的纪律
|
||||
|
||||
```text
|
||||
utility/* 只依赖标准库与三方库,禁止 import internal(防反向耦合)
|
||||
|
||||
cache 统一缓存门面(Redis 优先、内存降级)
|
||||
captcha 验证码生成/校验
|
||||
crypto bcrypt + salt
|
||||
excel excelize 封装(ReadSheet/WriteSheet)
|
||||
ip2region xdb 归属地(懒加载 + 读锁)
|
||||
jwt HS256 双令牌 Sign/Parse
|
||||
response 统一信封写出
|
||||
ua OS/Browser 解析
|
||||
upload 两段式上传(SaveTemp/SaveFromTemp/GetFileUrl)
|
||||
```
|
||||
|
||||
## 前端目录设计
|
||||
|
||||
```text
|
||||
views 按"域"分目录(system/data/content/monitor/tool…)与后端模块域对应;
|
||||
api 同样按域分目录;
|
||||
components/store/utils/hooks 不按域——全局共享层;
|
||||
settings/styles/enums 全局配置面。
|
||||
```
|
||||
|
||||
views 域归属与生成器 genDomainMap 一致(user/role→system,config→data,article→content,operation_log→monitor,example→tool,默认 tool),生成器据此决定前端产出路径。
|
||||
@@ -0,0 +1,117 @@
|
||||
# 4.11 扩展性设计
|
||||
|
||||
:::tip 概述
|
||||
扩展性来自两方面:一是**标准变更的操作清单**(按变更类型给出 checklist,不漏步骤、不变异架构);二是**逃生舱挂点**(基座装不下的需求有明确的合法出口)。本节按变更类型组织。
|
||||
:::
|
||||
|
||||
## 变更类型 Checklist
|
||||
|
||||
### 1. 给现有模块新增一个接口
|
||||
|
||||
```text
|
||||
□ api/v1/{模块}.go:新增 {Mod}{Action}Req(内嵌 gmeta.Meta,path 带 /api 前缀、
|
||||
permission、log、logType)+ {Mod}{Action}Res
|
||||
□ controller:同名方法,签名 func (c *CXxx) Action(ctx, req) (res, err)
|
||||
□ service/{模块}.go:接口追加方法声明(手维护)
|
||||
□ logic:实现方法(基座可表达走 spec 钩子,复杂走手写)
|
||||
□ 权限节点同步:若 permission 为新码,需在菜单管理为角色勾选
|
||||
(或插入 goframe_menu type=1 节点)
|
||||
□ go build ./... && gofmt;重启;/api.json 核对新条目
|
||||
```
|
||||
|
||||
### 2. 给现有模块加字段
|
||||
|
||||
```text
|
||||
□ MySQL ALTER(列注释按枚举规范写"字段:1-x 2-y";需要唯一性则补 uk_*)
|
||||
□ gf gen dao -c hack/config.yaml(刷新 entity/do/dao internal;service 不动)
|
||||
□ api/v1:Req/Res/Item 结构体补字段(json 驼峰 + dc 注释)
|
||||
□ logic:ToData 补键;枚举补 SerializeMaps(字典编码 {模块}_{字段});
|
||||
文件字段补 FileFields+FileSaveDir;富文本补 ContentFields
|
||||
□ 前端:columns.ts / querySchemas.ts / edit.vue 同步
|
||||
□ document/ 五方言脚本同步;必要时附录表清单更新
|
||||
□ go build → 重启 → page/detail 黄金样本更新
|
||||
```
|
||||
|
||||
### 3. 新增模块
|
||||
|
||||
```text
|
||||
□ 设计表(统一审计列 + del_flag + uk_* + 注释枚举)→ DDL 落库
|
||||
□ 租户级?→ hooks.tenantTables 登记
|
||||
□ go run ./cmd/generator -tables "goframe_x|中文名"(或手动四件套)
|
||||
□ logic 调整:spec 声明(筛选/树形/字典/文件/唯一 Group)、扩展接口
|
||||
□ service 接口核对;router.go 注册行确认
|
||||
□ go build ./... → 重启 → 角色勾选新权限节点
|
||||
```
|
||||
|
||||
### 4. 新增中间件
|
||||
|
||||
```text
|
||||
□ internal/middleware/{name}.go:func(r *ghttp.Request),职责单一;
|
||||
读配置用 cfgBool/cfgInt;客户端 IP 用 realClientIP;响应用 utility/response
|
||||
□ 不得 import 业务包——需要业务数据时在 wire.go 增加 Loader 函数变量,
|
||||
由 logic init 注册(参照 IdentityLoader 模式)
|
||||
□ router.Register 的 s.Use 链按序插入(顺序即语义,评审明确插入点)
|
||||
□ public 路径行为:确认对 IsPublicPath 的放行策略
|
||||
```
|
||||
|
||||
### 5. 新增定时任务
|
||||
|
||||
```text
|
||||
□ internal/task/{task_name}.go + init RegisterJobHandler("TaskName", h)
|
||||
□ handler 签名 (ctx, params) (string, error);长任务分批+上限;禁 HTTP 于事务内
|
||||
□ 任务表配置:url="TaskName"、params JSON、cron(6 段或 # 5 段)、
|
||||
execute_policy/is_sync;发布(status=1)
|
||||
□ 验证:daemonStatus 挂载 + runOnce + job/log 成功记录
|
||||
□ go build 重启(init 注册发生在进程加载期)
|
||||
```
|
||||
|
||||
### 6. 自定义 / 扩展 dao
|
||||
|
||||
```text
|
||||
□ dao/{m}.go 尾部"Add your custom methods"区追加方法(统计/联查等)
|
||||
□ 修改模板本身:改 hack/tpl/dao_index.tpl 后 gf gen dao 重新生成
|
||||
(overwriteDao:false 保留手工扩展;新旧模板产物差异需人工合并)
|
||||
□ 红线:自定义方法也必须以 Ctx(ctx) 为查询入口;禁止在 dao 内做业务判断
|
||||
```
|
||||
|
||||
### 7. 配置变更
|
||||
|
||||
```text
|
||||
□ manifest/config/config.yaml 修改
|
||||
□ 多环境差异用 GF_ 环境变量覆盖,不改文件
|
||||
□ 新增配置键:注释说明 + 代码 cfgBool/cfgInt 带默认值读取(缺失不 panic)
|
||||
□ 影响限流/日志/上传/调度的键变更后跑对应回归
|
||||
```
|
||||
|
||||
## 逃生舱与扩展挂点
|
||||
|
||||
| 挂点 | 场景 | 说明 |
|
||||
|------|------|------|
|
||||
| BodyUnmarshaler | 请求体形态不规则(平铺 map 等) | Req 实现 `UnmarshalBody(r)` 跳过默认 Parse(自行做 gvalid) |
|
||||
| ModuleSpec.Filter | 复杂查询条件 | 不与 Like/Eq 重复声明 |
|
||||
| ModuleSpec.DataScope | 数据权限挂点 | 共享工厂 `ApplyDataScope(col)`,声明即生效 |
|
||||
| ModuleSpec 钩子装不下 | 基座禁止按模块分支 | 整个方法手写(合法逃生舱,如 job.Detail 因嵌入结构手写) |
|
||||
| logic 包级共享函数 | 跨模块复用 | 放具体模块文件顶部(同包可见);横切能力进 base.go(15 导出上限) |
|
||||
| RegisterJobHandler | 本地任务处理器 | 全局注册表(重名 panic) |
|
||||
| middleware wire | 业务与中间件解耦 | 三 Loader/Sink 函数变量 |
|
||||
|
||||
## 未启用项与前端遗留
|
||||
|
||||
| 项 | 现状 | 启用/处理要点 |
|
||||
|----|------|-------------|
|
||||
| 用户注册 | publicPaths 含 /api/register(路由未注册) | 启用时补建表逻辑 + 开放策略 + 租户归属规则 |
|
||||
| 数据范围自定义档 | 现四档纯列值(role.data_scope) | 第 5 档需部门关联表 + 角色表单部门树多选 |
|
||||
| 导出增强 | 当前导出返回临时文件 URL | 可加流式下载/异步导出 |
|
||||
| SendSms 任务 | task/send_sms.go 仅记日志 | 接短信通道时替换实现 |
|
||||
| 站内信/数据源/短信/OSS/WebSocket | 前端遗留死代码(后端桩已删除) | 不再调用即无影响 |
|
||||
|
||||
## 封板后的变更纪律
|
||||
|
||||
```text
|
||||
1) 一切接口行为以 API 接口清单 + /api.json 为准,改动即更新文档
|
||||
2) 表结构改动三同步:document/mysql/ 定稿 → 其余四方言脚本 → 迁移(跨环境时)
|
||||
3) 破坏性变更(改名/删字段/改枚举值)必须走版本化说明 + 前端同步 +
|
||||
黄金样本更新,禁止静默变更
|
||||
4) 架构红线复查:绕过 dao / 手写租户条件 / 反向依赖 task / 超 15 导出的 base /
|
||||
未登记 tenantTables——任何一项都应打回
|
||||
```
|
||||
@@ -0,0 +1,75 @@
|
||||
# 4.4 前端分层架构
|
||||
|
||||
:::tip 概述
|
||||
前端采用 Vue3 Composition API + TypeScript + Pinia + Element Plus,分层为:views(页面)→ components(公共组件)→ api(接口)→ utils/http(请求封装)→ store(状态)→ router(动态菜单)。本节给出分层视图与协作关系。
|
||||
:::
|
||||
|
||||
## 前端分层视图
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ views/{域}/{模块}/ 页面层 │
|
||||
│ index.vue / edit.vue / detail.vue │
|
||||
│ columns.ts(表格列) querySchemas.ts(筛选表单) │
|
||||
└──────────────┬──────────────────────────────────────────┘
|
||||
│ 组件/指令/组合式函数
|
||||
┌──────────────▼──────────────────────────────────────────┐
|
||||
│ components/ 公共组件层(25 类) │
|
||||
│ Table Form Select Upload Editor Excel ChinaArea … │
|
||||
│ directives/ 权限指令 v-permission 等 │
|
||||
│ hooks/ core / event / setting / web │
|
||||
└──────────────┬──────────────────────────────────────────┘
|
||||
│ 接口调用
|
||||
┌──────────────▼──────────────────────────────────────────┐
|
||||
│ api/{域}/{模块}.ts 接口层(url 对齐 gmeta path) │
|
||||
└──────────────┬──────────────────────────────────────────┘
|
||||
│ http.request
|
||||
┌──────────────▼──────────────────────────────────────────┐
|
||||
│ utils/http/axios 请求封装层 │
|
||||
│ baseURL=/api · JWT 注入 · 401 处理 · 信封解包 │
|
||||
└──────────────┬──────────────────────────────────────────┘
|
||||
│ 状态支撑
|
||||
┌──────────────▼──────────────────────────────────────────┐
|
||||
│ store/modules/ 状态层(Pinia) │
|
||||
│ user(令牌/身份) · asyncRoute(动态菜单)… │
|
||||
│ router/ 路由层 │
|
||||
│ constant.ts(常量路由) · generator-routers.ts(动态) │
|
||||
│ router-guards.ts(守卫) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 数据流(以列表页为例)
|
||||
|
||||
```text
|
||||
1. 路由守卫拦到未加载动态路由 → 调 /api/index/getMenus 拉菜单树 → 生成路由
|
||||
2. 进入页面 index.vue:
|
||||
querySchemas.ts 定义筛选表单 → 用户输入 → 拼 params
|
||||
3. 调 api getExamplePage(params)
|
||||
→ http 封装注入 Authorization: Bearer <access_token>
|
||||
→ 后端返回统一信封,拦截器解包出 data(records/total/…)
|
||||
4. columns.ts 驱动 Table 渲染;枚举列显示 {field}Text
|
||||
5. 增删改走 edit.vue 表单 → exampleAdd/exampleUpdate → 成功刷新列表
|
||||
```
|
||||
|
||||
## 关键机制索引
|
||||
|
||||
| 机制 | 位置 | 详见 |
|
||||
|------|------|------|
|
||||
| HTTP 封装(401 跳登录/令牌刷新) | utils/http/ | 9.4 |
|
||||
| 动态菜单路由 | router/generator-routers.ts | 9.5 |
|
||||
| 权限指令(按钮级) | directives/permission.ts | 9.7 |
|
||||
| 登录多形态(账号/扫码/手机) | views/login/ | 9.8 |
|
||||
| 三件套规范 | views/{域}/{模块}/ | 9.12 / 12.2 |
|
||||
| 主题与设置 | settings/ + styles/ | 9.14 |
|
||||
|
||||
## 与后端的契约对齐
|
||||
|
||||
```text
|
||||
api url(无 /api 前缀) ↔ gmeta path(/api 开头,无 /v1)
|
||||
querySchemas field ↔ XxxPageReq json 字段(驼峰)
|
||||
columns prop ↔ Item/DetailRes json 字段
|
||||
权限指令参数 ↔ gmeta permission 权限码
|
||||
登录读取 ↔ data.access_token(下划线例外)
|
||||
```
|
||||
|
||||
对齐是联调问题的高发区,联调自检清单见 [3.9 前端联调指引](../guide/frontend-integration)。
|
||||
@@ -0,0 +1,102 @@
|
||||
# 4.1 整体架构概览
|
||||
|
||||
:::tip 概述
|
||||
系统采用分层架构 + 声明式基座 + 全局钩子横切 + gmeta 反射注解的组合。本节给出总架构图、模块依赖方向(允许/禁止)与启动编排,是理解全部机制的地图。
|
||||
:::
|
||||
|
||||
## 系统总架构图
|
||||
|
||||
```text
|
||||
┌───────────────────────────── 客户端 ─────────────────────────────┐
|
||||
│ ui/(Vue3 + Element Plus,baseURL=/api) 其他 API 消费方 │
|
||||
└──────────────────────────────────┬─────────────────────────────────┘
|
||||
│ HTTP(Bearer JWT)
|
||||
╔══════════════════════════════════▼═════════════════════════════════╗
|
||||
║ ghttp Server(:8000) ║
|
||||
║ ┌────────────────── 中间件链(顺序执行)──────────────────┐ ║
|
||||
║ │ CORS → Response → RateLimit → Demo → JWT → Tenant │ ║
|
||||
║ │ → OperationLog │ ║
|
||||
║ └──────────────────────────┬──────────────────────────────┘ ║
|
||||
║ ┌──────────────────────────▼──────────────────────────────┐ ║
|
||||
║ │ Bind 反射包装层(gmeta):权限校验 → 参数解析 → 分发 │ ║
|
||||
║ │ 路由元数据表 meta(权限/日志 注解即事实,补注册 goai) │ ║
|
||||
║ └──────────────────────────┬──────────────────────────────┘ ║
|
||||
║ controller(薄)→ service 接口(手维护)→ logic(ModuleSpec 基座) ║
|
||||
║ │ 侧路:utility/{jwt,captcha,cache,upload,excel,ip2region…} ║
|
||||
║ │ 侧路:gcron 调度器(job daemon + CleanTemp) ║
|
||||
║ ┌────▼─────────────────────────────────────────────────────┐ ║
|
||||
║ │ dao.Xxx.Ctx(ctx) —— 唯一查询入口 │ ║
|
||||
║ │ hooks.ApplyCtx:del_flag=0 + 租户级表 tenant_id IN(0,x) │ ║
|
||||
║ │ hooks.Handler:Insert 盖章 / Update 盖章 / Delete→软删 │ ║
|
||||
║ └────┬─────────────────────────────────────────────────────┘ ║
|
||||
╚══════╪══════════════════════════════════════════════════════════════╝
|
||||
│ gdb(五驱动运行时切换)
|
||||
┌──────▼────────┐ ┌──────────────┐ ┌─────────────────────────────┐
|
||||
│ MySQL/PgSQL/… │ │ Redis(缓存) │ │ 磁盘 resource/upload + xdb │
|
||||
└───────────────┘ └──────────────┘ └─────────────────────────────┘
|
||||
```
|
||||
|
||||
## 模块依赖方向图(允许 / 禁止)
|
||||
|
||||
```text
|
||||
api/v1 ◀── controller ◀── logic ──▶ service(接口注册)
|
||||
│
|
||||
├──▶ dao ──▶ model/entity · model/hooks ──▶ gdb
|
||||
├──▶ utility/*(无业务依赖)
|
||||
└──▶ consts
|
||||
|
||||
internal/task ──▶ internal/logic ✅ 允许(任务复用 base 能力)
|
||||
internal/logic ──▶ internal/task ❌ 禁止(反向依赖)
|
||||
internal/middleware ──▶ 业务包 ❌ 禁止(经 wire.go 函数变量接线)
|
||||
internal/migrate ──▶ dao/logic ❌ 禁止(gdb 裸连,隔离钩子)
|
||||
base.go ──▶ 模块文件 ❌ 禁止(只依赖 dao/utility/consts)
|
||||
```
|
||||
|
||||
## 启动编排(internal/cmd)
|
||||
|
||||
```text
|
||||
main.go(注册五数据库驱动 + Redis 适配器 + blank import internal/logic)
|
||||
└─ internal/cmd.Run(ctx)
|
||||
├─ 1) utility/cache.Init Redis 探测,失败降级进程内存
|
||||
├─ 2) wireDependencies 校验中间件依赖是否已由业务包 init 接线
|
||||
├─ 3) g.Server("easygoadmin") + router.Register(s)
|
||||
│ 中间件链注册 + 404/500 状态处理器 + SetServerRoot("resource")
|
||||
│ + /api/health + /swagger → 逐个 Bind 全部控制器
|
||||
├─ 4) go DaemonInit(延时 2s) 定时任务调度器加载 status=1 任务
|
||||
├─ 5) gcron 注册 CleanTemp 每日 03:30 清理上传临时目录
|
||||
└─ 6) s.Run() 阻塞运行 HTTP 服务
|
||||
```
|
||||
|
||||
延时 2s 的原因:等 HTTP 监听就绪(任务可能是 HTTP 调自身接口)。
|
||||
|
||||
## 业务调用链(标准 CRUD)
|
||||
|
||||
```text
|
||||
Bind 包装(gmeta 权限/日志元数据)
|
||||
→ controller.CXxx(薄层,仅转发 service.Xxx())
|
||||
→ service.IXxx 接口(internal/service/{模块}.go,手维护)
|
||||
→ logic.sXxx 实现(internal/logic/{模块}.go,ModuleSpec 基座或手写)
|
||||
→ dao.Xxx.Ctx(ctx)(唯一查询入口:挂全局钩子 + 注入 del_flag/租户条件)
|
||||
→ gdb → 数据库
|
||||
```
|
||||
|
||||
## 五个关键设计决策
|
||||
|
||||
| 决策 | 理由 |
|
||||
|------|------|
|
||||
| **目录平铺**:api/controller/logic/service 均单包,每模块一文件 | 跨模块直接同包调用(如 user 调 ResolveCityNames 无需 import);生成器目标路径唯一 |
|
||||
| **service 接口手维护** | 接口即文档;禁止 gf gen service 整体覆盖 |
|
||||
| **logic 同包 init() 注册** | 无需 blank import,包内全量加载 |
|
||||
| **中间件不依赖业务层** | IdentityLoader/TenantLoader/LogSink 以函数变量接线(wire.go),cmd 启动期检查告警 |
|
||||
| **dao.Ctx 唯一查询入口** | 软删/租户/审计三条红线在钩子层强制执行,业务零感知 |
|
||||
|
||||
## 分层职责速查
|
||||
|
||||
| 层 | 位置 | 职责 | 禁止 |
|
||||
|----|------|------|------|
|
||||
| api/v1 | api/v1/{模块}.go | 请求响应结构体 + gmeta 注解 | 业务逻辑 |
|
||||
| controller | internal/controller/{模块}.go | 薄层透传 | 业务/dao/手写响应 |
|
||||
| service | internal/service/{模块}.go | 接口契约(手维护) | 实现 |
|
||||
| logic | internal/logic/{模块}.go | 业务实现(基座+钩子+手写) | 反向依赖 task |
|
||||
| dao | internal/dao/ | 查询入口(钩子+红线条件) | 绕过直连 DB |
|
||||
| hooks | internal/model/hooks/ | 审计盖章/软删/租户 | 业务判断 |
|
||||
@@ -0,0 +1,110 @@
|
||||
# 4.2 请求处理链路
|
||||
|
||||
:::tip 概述
|
||||
一个受保护的写请求依次穿过七层中间件与 Bind 包装层。理解这条链路是排查 401/403/429/演示拦截/日志缺失的钥匙。注册顺序即执行顺序(router.go s.Use)。
|
||||
:::
|
||||
|
||||
## 中间件链总览
|
||||
|
||||
```text
|
||||
CORS → Response → RateLimit → Demo → JWT → Tenant → OperationLog → 处理器
|
||||
(Bind 包装层做权限校验)
|
||||
```
|
||||
|
||||
| # | 中间件 | 短路行为 |
|
||||
|---|--------|---------|
|
||||
| 1 | CORS | 预检 204;头设置 |
|
||||
| 2 | Response | SVG 加固;元信息入 ctx;panic → 500 信封 |
|
||||
| 3 | RateLimit | 超限 code=429 |
|
||||
| 4 | Demo | `app.demo=true` 时写操作 code=1015 |
|
||||
| 5 | JWT | 无效/黑名单 code=401 |
|
||||
| 6 | Tenant | 租户禁用 1006 / 过期 1005 |
|
||||
| 7 | OperationLog | 不短路,异步采集 |
|
||||
| 8 | Bind 包装层 | 权限不足 code=403;参数校验失败 400 |
|
||||
|
||||
## 请求生命周期(12 步)
|
||||
|
||||
```text
|
||||
1 CORS 预检/头设置
|
||||
2 Response 元信息入 ctx;defer panic 兜底
|
||||
3 RateLimit 固定窗口计数,超限 429
|
||||
4 Demo 演示模式拦截写操作
|
||||
5 JWT 解析校验 token → 注入身份;黑名单 401
|
||||
6 Tenant 租户状态/过期校验(60s 缓存)
|
||||
7 OperationLog 记 start,Next()
|
||||
8 Bind 包装层 元数据入 ctx → HasPerm 权限校验(403 短路)→ Parse 参数 → 调 controller
|
||||
9 controller 转发 service 接口
|
||||
10 logic ModuleSpec 流水线 / 手写业务;dao.Ctx 查询(钩子已注入条件)
|
||||
11 response.Write 信封写出(code/body 记入 ctx)
|
||||
12 OperationLog 组装记录 → 异步通道 → 批量落库
|
||||
```
|
||||
|
||||
## 时序图(受保护写请求 + 日志异步落库)
|
||||
|
||||
```text
|
||||
Client 中间件链 Bind包装/业务 缓存 DB/日志
|
||||
│ POST /api/user/update │ │ │
|
||||
│───▶│ CORS 头/预检 │ │ │
|
||||
│ │ Response: 元信息入ctx, defer recover │ │
|
||||
│ │ RateLimit ──────────────────────────────────────▶ SET/GET 计数│
|
||||
│ │ Demo(app.demo=false 放行) │ │ │
|
||||
│ │ JWT: Parse+黑名单 ───────────────────────────────▶ Exists jti │
|
||||
│ │ 注入身份; Roles/Perms/DataScope/DeptId ──────▶ 命中缓存 │
|
||||
│ │ 未命中 → IdentityLoader 回源 ───────────────────────▶ user/role/menu
|
||||
│ │ Tenant: status/expire ───────────────────────────▶ tenant:info │
|
||||
│ │ OperationLog: start=now │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ ▼ ▼ HasPerm(403?短路) │ │
|
||||
│ │ Parse(req) → logic.Update │ │
|
||||
│ │ spec 流水线: BeforeUpdate → │ │
|
||||
│ │ saveTransform → 唯一预检 → ────────────────▶ UPDATE
|
||||
│ │ (hooks 盖章 update_time/user) │ │
|
||||
│ │ ▼ │ │ │
|
||||
│◀───│ WriteJson{code:0,...}(code/body 记 ctx) │ │
|
||||
│ │ OperationLog: 组装 entry(脱敏 param/result) │ │
|
||||
│ │ select{ logChan<-entry } 非阻塞 │ │
|
||||
│ │ worker: 攒批 100条/1s ──────────────────▶ INSERT operation_log
|
||||
```
|
||||
|
||||
异常分支:
|
||||
|
||||
```text
|
||||
* 任一中间件短路(429 / 演示拦截 / 401 / 403)→ 直接 WriteJson,后续不执行
|
||||
* panic → Response recover → 500 信封(已写出响应则仅记录日志)
|
||||
* logChan 满 → 丢弃并计数告警(每丢 100 条输出一次 Warning),绝不阻塞业务
|
||||
```
|
||||
|
||||
## 各层关键实现速查
|
||||
|
||||
| 层 | 实现位置 | 要点 |
|
||||
|----|---------|------|
|
||||
| CORS | middleware/cors.go | `cors.allowOrigin` 空或 `*` 走框架默认;具体来源显式回显凭证 |
|
||||
| Response | middleware/response.go | .svg 强制 attachment+nosniff(防存储型 XSS);realClientIP/UserAgent 入 ctx |
|
||||
| RateLimit | middleware/ratelimit.go | 固定窗口;键 `ratelimit:{ip}:{path}:{窗口起点}`;计数经 utility/cache 多实例共享;存储异常放行 |
|
||||
| Demo | middleware/demo.go | GET/HEAD/OPTIONS 与公开白名单放行;其余写操作拒绝 |
|
||||
| JWT | middleware/jwt.go | Parse(HS256/过期/签发者)→ 黑名单 Exists → 注入身份(缓存 300s,回源 LoadIdentity) |
|
||||
| Tenant | middleware/tenant.go | 缓存 60s;租户不存在按禁用处理;数据库基础错误放行(防存储抖动全站不可用) |
|
||||
| OperationLog | middleware/operation_log.go | 有界通道 2048 + 单 worker;批量 100 条或 1s flush;参数脱敏 password/salt/token → *** |
|
||||
| Bind | router/bind.go | 权限校验 + r.Parse(gvalid)+ 信封写出;BodyUnmarshaler 逃生舱 |
|
||||
|
||||
## 真实 IP 与受信代理
|
||||
|
||||
```text
|
||||
util.go realClientIP:
|
||||
直连来源不在 api.proxyIps(逗号分隔,精确 IP 与 CIDR)内 → 直接用 GetRemoteIp()
|
||||
在列表内 → 才信任 X-Forwarded-For 首段 / X-Real-IP
|
||||
```
|
||||
|
||||
防止伪造 XFF 绕过限流或污染日志。Nginx 反代部署务必把反代地址加入 `api.proxyIps`。
|
||||
|
||||
## 排障速查
|
||||
|
||||
| 症状 | 检查点 |
|
||||
|------|--------|
|
||||
| 401 | token 过期/黑名单(登出、轮换、强退都会拉黑 jti) |
|
||||
| 403 | gmeta permission ↔ 菜单节点 ↔ 角色勾选 ↔ 身份缓存(300s) |
|
||||
| 429 | 限流阈值;认证入口独立 authLimit=10 |
|
||||
| 1015 | app.demo=true 拦截写操作 |
|
||||
| 1005/1006 | 租户过期/禁用(Tenant 中间件,缓存 60s) |
|
||||
| 日志缺失 | operationLog.enable / log 标签空 / logQuery 关 / 通道满告警 |
|
||||
| panic 无堆栈 | server.errorStack=false;看服务端日志 |
|
||||
@@ -0,0 +1,96 @@
|
||||
# 4.10 安全架构设计
|
||||
|
||||
:::tip 概述
|
||||
安全设计按威胁面组织:每项威胁对应明确控制措施与代码落点。本节给出威胁→控制总表、四条安全红线、部署基线与已知边界(诚实声明),可直接用于安全评审自查。
|
||||
:::
|
||||
|
||||
## 威胁 → 控制总表
|
||||
|
||||
| 威胁面 | 控制措施 | 代码落点 |
|
||||
|--------|---------|---------|
|
||||
| 身份伪造 | HS256 签名校验 + iss/exp 强校验(限定 HS256 防算法混淆) | utility/jwt Parse |
|
||||
| 令牌重放(登出后) | jti 黑名单,TTL=剩余有效期 | `blacklist:{jti}` |
|
||||
| Refresh 重放 | 轮换:旧 refresh 入黑名单,已黑即拒(1008) | logic/auth Refresh |
|
||||
| 被踢会话复活 | 强退同时拉黑 access+refresh 双令牌 | online_user ForceLogout |
|
||||
| 口令窃取/泄露 | bcrypt(password+salt);日志脱敏(password/salt/token → ***) | utility/crypto; maskSecret |
|
||||
| 暴力破解 | 登录/验证码/令牌兑换独立严阈值 10/min/IP | rateLimit.authLimit |
|
||||
| 水平越权(跨租户) | dao.Ctx 注入 tenant_id IN(0,自身) | hooks.ApplyCtx |
|
||||
| 行级越权(部门/本人) | 角色 data_scope 四档叠加过滤(仅列表/导出;写操作由 permission 兜底) | logic/data_scope.go |
|
||||
| 垂直越权 | gmeta permission + HasPerm;超管放行 | Bind 包装层 |
|
||||
| SQL 注入 | 全链 gdb 参数化;无方言拼接红线 | dao/Where 参数化 |
|
||||
| 参数篡改 | gvalid 标签校验(Parse 统一触发) | api/v1 结构体标签 |
|
||||
| 存储型 XSS | .svg 强制 attachment+nosniff;富文本外链不本地化、占位符契约 | middleware/response; base.go |
|
||||
| SSRF | 富文本仅处理本站 URL,外站一律不抓取 | localizeOne 判定 |
|
||||
| CSRF | Bearer 头令牌(非 Cookie)+ CORS 白名单 | JWT 中间件/CORS |
|
||||
| IP 伪造 | 仅受信代理(api.proxyIps,支持 CIDR)的 XFF/X-Real-IP 生效 | realClientIP |
|
||||
| 限流绕过 | 计数经 Redis 跨实例共享;窗口起点对齐 | RateLimit incrCounter |
|
||||
| 越权写(演示站) | Demo 中间件拦截全部写操作 | middleware/demo |
|
||||
| 审计缺失 | 操作日志异步全量记录(脱敏+响应+耗时) | OperationLog |
|
||||
| 会话固定 | 无服务端 Cookie 会话,令牌全部服务端签发 | auth Login |
|
||||
| 敏感信息出栈 | GetUser 永不返回 password/salt;令牌原文仅在线用户详情(需权限) | index/online_user |
|
||||
|
||||
## 四条租户隔离红线(安全审计重点)
|
||||
|
||||
::: warning 红线清单
|
||||
1. 业务代码禁止 `g.DB()` 直查业务表(绕过钩子与过滤)
|
||||
2. 业务代码禁止手写 del_flag / tenant_id 条件(应由 dao.Ctx 注入)
|
||||
3. 新增租户级表必须登记 hooks.tenantTables(**漏登=跨租户泄漏**)
|
||||
4. 唯一预检范围必须等于唯一索引范围(否则 1062 漏报/误报)
|
||||
|
||||
另:平台级(超管)全量可见是设计预期,管理端点须收紧 permission。
|
||||
:::
|
||||
|
||||
## 文件上传安全链
|
||||
|
||||
```text
|
||||
大小限制 → 扩展名白名单 → 图片魔数嗅探(前 512 字节须 image/*)
|
||||
→ 随机文件名(防猜路径/覆盖)
|
||||
→ 临时/正式两段式(未过校验不进正式目录)
|
||||
→ 静态响应 svg 附件化 + nosniff
|
||||
→ 定期清理 temp(每日 03:30,tempExpireDays 默认 7 天)
|
||||
```
|
||||
|
||||
注意:fileExt 白名单含 js/css/sql 等——对公网用户提供上传时,建议按环境收紧 fileExt(配置层控制,无需改码)。
|
||||
|
||||
## 认证与授权安全细节
|
||||
|
||||
```text
|
||||
认证:
|
||||
* 令牌 HS256;密钥/签发者可配置;exp 必填
|
||||
* 双令牌分工:access 2h 只作身份;refresh 7d 仅 /api/oauth2/token 可用(typ 校验)
|
||||
* 验证码:一次性(命中即删)、TTL 300s、bypassCode 生产必须留空
|
||||
* 密码:bcrypt DefaultCost;外部 salt 防彩虹;旧 md5 数据被 IsBcrypt 拒绝(强制重置)
|
||||
|
||||
授权:
|
||||
* 权限点粒度到接口级(sys:module:action),按钮级由前端控制;
|
||||
新增接口必须声明 permission 或显式留空(留空=登录即可,评审需明确该选择)
|
||||
* 隐藏节点:menu hide=1 的权限节点不进 Perms——"禁用权限"用 status 而非 hide
|
||||
* 权限缓存一致性:保存权限/分配角色/改数据范围主动清 identity 缓存,最迟 300s 过期
|
||||
```
|
||||
|
||||
## 部署安全基线(生产)
|
||||
|
||||
```text
|
||||
1) 修改 jwt.secret 为高强度随机值且与环境隔离
|
||||
2) captcha.bypassCode=""; app.demo=false; app.errorMsg=false; errorStack=false
|
||||
3) database/redis 使用独立账号与内网地址;不暴露公网
|
||||
4) api.proxyIps 只列真实反代出口;未用反代时保持默认(含 127.0.0.1)
|
||||
5) file.fileExt 按业务收紧;uploadDir 权限最小化;domainUrl 走 CDN/域名隔离
|
||||
6) rateLimit 按压测调参(防误伤;authLimit 不建议调大)
|
||||
7) HTTPS 由前置代理终止;服务本身 HTTP 内网
|
||||
8) 定期核对 /api.json 与权限点清单(防新接口漏配 permission)
|
||||
```
|
||||
|
||||
## 已知边界与取舍(诚实声明)
|
||||
|
||||
| 项 | 边界 |
|
||||
|----|------|
|
||||
| CheckUnique | 先查后插的 UX 预检,存在 TOCTOU——并发唯一性以 uk_* 索引为最终防线 |
|
||||
| 数据权限 | 仅约束 Page/List/导出;Detail/Update/Delete 防线是 permission 节点 + 租户隔离 |
|
||||
| 接线范围 | 数据权限当前仅 user 模块接线(dept_id 列);其他模块行级可见性由租户隔离单层兜底 |
|
||||
| 部门子树 | deptSubtreeIds 每次全量拉部门表 BFS 无缓存——数百行内无感知,上万行需加短时缓存 |
|
||||
| 身份缓存窗口 | 300s 内直接改库的 dept_id/data_scope 不经 API 不会即时生效(API 路径均主动清缓存) |
|
||||
| 在线用户详情 | 返回令牌原文(便于运维排查),依赖 sys:onlineUser:detail 权限收口 |
|
||||
| 日志截断 | param/result 截断 8KB,超长报文不完整留存 |
|
||||
| 密码策略 | 无独立复杂度/定期更换强制(可按需在密码路径追加规则) |
|
||||
| 防重放 | jti 黑名单依赖 TTL 自然过期,无物理防重放窗口 |
|
||||
Reference in New Issue
Block a user