Files
EasyGoAdmin_GoFrame_EleVue/document/wiki/01_系统架构设计.txt
T
2026-10-20 11:37:25 +08:00

269 lines
17 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
================================================================================
EasyGoAdmin 后端开发者文档 · 01 系统架构设计
================================================================================
--------------------------------------------------------------------------------
1. 分层与调用链
--------------------------------------------------------------------------------
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)
│ 中间件链:CORS → Response → RateLimit → Demo → JWT → Tenant
│ → OperationLog → 处理器
│ 处理器 = Bind 反射包装层(权限校验 → 参数解析 → controller →
│ service 接口 → logic 实现)
├─ 4) go DaemonInit(延时 2s) 定时任务调度器加载 status=1 任务
├─ 5) gcron 注册 CleanTemp 每日 03:30 清理上传临时目录
└─ 6) s.Run() 阻塞运行 HTTP 服务
业务调用链(标准 CRUD):
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/v1、controller、logic、service 均为单包,每模块一个
snake_case 文件;跨模块直接同包调用(如 user 直接调用包级函数
ResolveCityNames,无需 import)。
* service 接口手维护:internal/service/{模块}.go 声明接口 + Register 注册函数 +
包级取用函数(如 service.City())。禁止整体运行 gf gen service(会覆盖手工
重组的接口文件)。
* logic 同包 init() 注册:每个 logic 文件在 init() 里调用 service.RegisterXxx
完成依赖接线,无需 blank import,包内全量加载。
* 中间件不依赖业务层:身份/租户加载器与日志落地器以函数变量形式注册
(internal/middleware/wire.go),由 logic 的 init 注入(auth.go 的 init 注册
IdentityLoader=LoadIdentity、TenantLoader=LoadTenant;operation_log 的
LogSink 由日志模块注册)。cmd/wire.go 仅在启动期检查并告警缺失。
1.1 系统总架构图
┌───────────────────────────── 客户端 ─────────────────────────────┐
│ 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 │
└───────────────┘ └──────────────┘ └─────────────────────────────┘
1.2 模块依赖方向图(允许 / 禁止)
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)
--------------------------------------------------------------------------------
2. 启动编排详解(internal/cmd/cmd.go)
--------------------------------------------------------------------------------
cache.Init(ctx)
g.Redis() PING 失败 → memFallback=true,进程内 gcache 兜底;
运行期 Redis 出错同样自动切换(见 utility/cache)。
wireDependencies(ctx)
IdentityLoader / TenantLoader / LogSink 任一为 nil 时输出警告日志,
不阻断启动(缺失的表现分别是:不加载角色权限、跳过租户校验、日志不落库)。
router.Register(s)
注册中间件、404/500 状态处理器(统一 JSON)、SetServerRoot("resource")、
/api/health、/swagger,随后逐个 Bind 全部控制器。
调度器:time.Sleep(2s) 后 DaemonInit(等 HTTP 监听就绪,任务可能是 HTTP 调自身)。
上传清理:gcron.AddSingleton("0 30 3 * * *") 每日 03:30 调 logic.CleanTemp,
保留天数取 file.tempExpireDays(默认 7),条目名 upload_temp_clean。
--------------------------------------------------------------------------------
3. 路由注册与 gmeta 注解(internal/router)
--------------------------------------------------------------------------------
3.1 Bind 反射绑定(bind.go)
Bind(s, new(controller.CXxx)) 扫描控制器全部导出方法,方法签名必须为:
func (c *Xxx) Page(ctx context.Context, req *v1.XxxReq) (*v1.XxxRes, error)
对每个方法读取请求结构体内嵌 gmeta.Meta 的扩展标签:
gmeta.Meta `method:"get" path:"/api/user/page" summary:"用户分页"
permission:"sys:user:page" log:"用户管理-查询分页记录" logType:"query"`
gmeta.Meta `method:"post" path:"/api/login" summary:"登录" public:"true"`
标签语义:
method HTTP 方法(get/post/put/delete)
path 完整路由模板(含 /api 前缀;:id 为路由参数)
summary OpenAPI 摘要
permission 权限节点,形如 sys:{module小驼峰}:{action};空 = 登录即可
log 操作日志标题,形如 "{模块中文名}-{动作}";空 = 不记录
logType 日志类型字符串(query/add/update/delete/import/export/reset/…),
见 consts/log_type.go 的 LogTypeMap
public "true" = 免登录(仍需与 middleware/public.go 白名单一致)
绑定层为每个路由做的事:
1) 写入路由元数据表 meta.Register(rm),键 "METHOD /path"
2) 补注册进 gf goai(闭包路由 gf 不会自动收集)→ /api.json 原生 OpenAPI
3) 生成统一包装 handler:
- 路由元数据写入 ctx(操作日志中间件读取)
- 权限校验:rm.Permission != "" 且 consts.HasPerm(ctx, permission) 为假
→ 返回 code=403 信封(权限校验在包装层完成,不设独立 Permission 中间件)
- 参数解析:r.Parse(req)(路由参数+query+body,含 gvalid 校验);
实现 BodyUnmarshaler 接口的请求对象走自定义解析逃生舱
- 调用业务方法,错误 → response.Fail;nil 出参 → Ok(nil);否则 Ok(出参)
3.2 路由元数据表(meta/meta.go)
map["GET /api/user/page"] → RouteMeta{Method,Path,Summary,Permission,Log,LogType,Public}
读写锁保护;Lookup 供调试,All 供文档生成。权限校验与操作日志共用同一份元数据,
保证“注解即事实”。
3.3 路由注册表(router.go registerControllers)
按域顺序 Bind:auth → index → system7 → data7 → content3 → monitor7 →
tool4(含 CExample 代码生成器注册示例)。
新增模块时在最后一个 Bind 行后追加(代码生成器会自动写入该行,见 07 章)。
404/500 状态处理器:非 200 一律输出统一 JSON 信封,不返回 HTML。
3.4 URL 约定(与 architecture.md 的差异,务必注意)
* 路由 path 直接以 /api 开头,URL 中没有 /v1 版本段;
api/v1 只是 Go 包名,不代表 URL 版本。例:GET /api/user/page。
* 公开路由白名单(middleware/public.go,精确匹配):
/api/login、/api/register(白名单已放行,路由未注册)、/api/captcha、
/api/oauth2/token、/api/health
另放行 /api.json 与 /swagger*。
* 前端 baseURL 为 /api,模块路径与 gmeta path 逐条对齐。
--------------------------------------------------------------------------------
4. 统一响应与错误码
--------------------------------------------------------------------------------
4.1 响应信封(utility/response)
成功:{"code":0, "ok":true, "msg":"操作成功", "data":{...}}
失败:{"code":1, "ok":false, "msg":"...", "data":{}}
未认证:HTTP 401 + {"code":401,...}(前端据此跳登录页)
Fail() 映射规则:gerror 码 == 401 原样保留;其余一律降级为 code=1
(前端 ResultEnum.ERROR=1 才弹 msg)。因此 consts 里的细分错误码
(1001 用户不存在、1003 密码错误……)主要用于日志与语义区分。
写出响应时把 code/body 记入 ctx(respCodeKey/respBodyKey),
供 OperationLog 中间件判断成功/失败与记录响应内容。
4.2 分页契约
分页接口 data 为 PageData[T](api/v1/common.go NewPageData):
{"records":[...], "total":n, "size":pageSize, "current":pageNo, "pages":n}
records 为空输出 [] 而非 null。
PageReq:pageNo 默认 1;pageSize 默认 10、上限 500(GetPageSize 截断)。
批量入参统一 IdsReq{ids:[...]};新增返回统一 IdRes{id:n}。
4.3 业务错误码(consts/biz_err.go,gcode.Register 语义值)
1 CodeFailed 通用操作失败
400 CodeParamError 参数校验失败
401 CodeUnauthorized 登录已过期,请重新登录
403 CodeForbidden 没有该操作权限
429 CodeTooManyRequests 请求过于频繁
500 CodeServerError 服务器内部错误
1001 用户不存在 / 1002 账号禁用 / 1003 密码错误 / 1004 验证码错误
1005 租户过期 / 1006 租户禁用 / 1007 租户用户数上限 / 1008 Token 无效
1009 记录已存在 / 1010 记录不存在 / 1011 存在下级或关联
1012 原密码错误 / 1013 租户不存在 / 1014 记录已被引用 / 1015 演示环境禁止操作
--------------------------------------------------------------------------------
5. 数据访问与全局钩子(架构红线执行点)
--------------------------------------------------------------------------------
5.1 dao.Ctx 是唯一查询入口
dao/{模块}.go 由自定义模板(hack/tpl/dao_index.tpl)生成,做两件事:
1) 构造时 m.Hook(hooks.Handler()) —— 挂载 Insert/Update/Delete 全局钩子
2) Ctx 覆写 —— 进入 SQL 构建前注入条件:
所有表:del_flag = 0
租户级表且非平台上下文:tenant_id IN (0, 当前租户)
业务代码禁止绕过 dao 直连 g.DB(),禁止手写 del_flag/tenant_id 条件。
5.2 钩子行为(internal/model/hooks/hooks.go)
Insert:主键交数据库自增(int,不使用雪花);补 del_flag=0、create_time、
create_user(取 ctx 用户名,回退真实姓名);租户级表自动携带 tenant_id
(租户用户零值视为未指定→填自身;超管未指定→tenant.defaultId,
显式 0 可创建平台共享行)。
Update:自动盖章 update_time / update_user(仅 map 数据生效)。
Delete:物理删除一律改写为 UPDATE del_flag=1(软删)。
5.3 租户级表清单(tenantTables,共 9 张)
goframe_user / role / dept / level / position / article / category / notice / link
平台级(超管,ctx tenant_id=0)查询不过滤,全量可见。
5.4 上下文键(consts/context_key.go)
写入方:JWT 中间件写 UserID/TenantID/Username/Realname/TokenJTI/TokenExpire/
Roles/Perms/DataScope/DeptID(后两者来自身份缓存,见 03 章数据权限节);
Response 中间件写 ClientIP/UserAgent/ReqMethod/ReqURL。
读取方:全链路 consts.GetXxx(ctx)。
注意两套租户读法:
CtxTenantID(ctx) 原始值(0=平台),仅供租户设施使用
GetTenantID(ctx) 业务归属值(租户返回自身,超管返回 tenant.defaultId),
新增/导入/唯一校验统一用它
数据范围读法:
GetDataScope(ctx) 角色数据范围档位(1-全部;未注入视为全部)
GetDeptID(ctx) 当前用户部门ID(0=未分配)
5.5 行级数据权限叠加位置(两层独立)
第一层(硬规则,不可配置):租户隔离 —— dao.Ctx/hooks.ApplyCtx 注入
tenant_id IN(0,自身),先于一切业务过滤,任何模块不可绕过。
第二层(可配置):角色数据范围 —— ModuleSpec.DataScope 挂点,在
applyFilters 链末环(Like→Eq→Keyword→Filter→DataScope)叠加;
共享工厂 ApplyDataScope(deptColumn)(logic/data_scope.go),
模块一行声明即生效,机制详见 03 章第 6 节。
--------------------------------------------------------------------------------
6. 关键横切约定速查
--------------------------------------------------------------------------------
* 主键:int 数据库自增;新增后 result.LastInsertId() 回读。
* 软删:del_flag 0 正常 / 1 删除;删除接口全部软删。
* 表前缀:goframe_;文件命名 snake_case 对齐 dao(前端文件仍驼峰)。
* 入参/出参 json 驼峰;唯一例外:登录/令牌接口返回
access_token/refresh_token/expires_in/token_type(OAuth2 惯例,勿改)。
* 缓存键统一前缀 easygoadmin:(consts/cache_key.go)。
* 权限码 sys:{module小驼峰}:{action};超管角色编码 ROLE_SUPER_ADMIN,
前端通配权限 *:*:*。
* 每张表统一审计列:id/create_user/create_time/update_user/update_time/del_flag
(租户表另有 tenant_id)。
* 命名规范补充:api 与 controller 同样使用 snake_case 文件名
(与早期“按域分目录”设计不同,以现状为准)。