Files
2026-10-20 21:53:26 +08:00

99 lines
4.1 KiB
Markdown
Raw Permalink 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.
# 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)
```