签入脚本

This commit is contained in:
小蚂蚁云
2026-10-20 11:37:25 +08:00
commit 7f5e8470af
909 changed files with 4045080 additions and 0 deletions
+215
View File
@@ -0,0 +1,215 @@
================================================================================
EasyGoAdmin_GoFrame_EleVue 后端开发者文档 · 00 总览与目录索引
================================================================================
文档版本:1.1(封板版,2026-10-17;v1.0 全量编写 → v1.1 补图示/数据模型/FAQ/
核心模块详解/缓存策略/测试质量/安全专题/变更管理,共 19 章)
适用范围:后端服务(GoFrame v2),前端 ui/ 仅在接口契约相关处提及
权威来源:本 wiki 以仓库最新代码为准;document/architecture.md 为早期设计
定稿,部分章节(目录分域、中间件顺序、/v1 版本段等)已被后续演进
取代,差异点在本文档各章中明确标注。
--------------------------------------------------------------------------------
一、项目定位
--------------------------------------------------------------------------------
企业级多租户后台管理基座,后端 GoFrame v2 + Element Plus 前端同仓库:
* 25 张业务表 → 27 个业务模块,全部平铺实现(auth/index + system7 +
data7 + content3 + monitor7 + tool4)
* JWT 双令牌认证 + RBAC 权限 + 多租户数据隔离 + 角色行级数据权限(data_scope)
* 代码生成器(HTTP + CLI 两种入口,一键产出前后端全套文件与菜单/字典/路由)
* 整库迁移器(mysql/pgsql/sqlite/mssql/oracle 五方言互迁)
* 常驻定时任务调度器(数据驱动、HTTP/本地处理器双形态、多实例执行锁)
* 在线用户 / 服务器监控 / 缓存监控三个无表监控模块
* 操作日志(中间件异步落库)+ 登录日志 + IP 归属地
--------------------------------------------------------------------------------
二、技术栈与版本(go.mod)
--------------------------------------------------------------------------------
Go 1.27.2
GoFrame github.com/gogf/gf/v2 v2.10.3(框架核心)
数据库驱动 contrib/drivers/{mysql,pgsql,mssql,oracle,sqlite}/v2 v2.10.3
(五驱动全部 blank import,运行时按配置切换)
Redis contrib/nosql/redis/v2 v2.10.3 + redis/go-redis/v9
JWT github.com/golang-jwt/jwt/v5 v5.3.1
密码 golang.org/x/crypto(bcrypt)
Excel github.com/xuri/excelize/v2 v2.11.0
IP 归属地 lionsoul2014/ip2region xdb(IPv4,manifest/data/ip2region.xdb)
图片处理 golang.org/x/image(验证码渲染)
前端 ui/(Vue3 + Element Plus,接口统一 /api/*)
--------------------------------------------------------------------------------
三、顶层目录结构
--------------------------------------------------------------------------------
main.go 服务入口:注册五库驱动 + Redis 适配器 → cmd.Run
Makefile run/build/test/vet/fmt/gen(gen-dao,gen-service)/clean
api/v1/ 全部请求/响应结构体(单包 v1;注意:URL 中并无 /v1 版本段,
路由 path 直接写作 /api/xxx,见 01 章“路由与注解”)
cmd/
generator/ 代码生成器 CLI(go run ./cmd/generator)
migrator/ 整库迁移 CLI(go run ./cmd/migrator)
internal/
cmd/ 启动编排:cache.Init → 依赖接线 → 路由注册 → 调度器 → 运行
consts/ 业务错误码 / 缓存键 / 上下文键 / 日志类型枚举
controller/ 控制器(单包 controller,薄层,仅转发 service)
dao/ 数据访问层(gf gen dao 产物 + 自定义扩展模板挂全局钩子)
internal/ dao 内部实现(自动生成,勿手改)
logic/ 业务逻辑(单包 logic,每模块一文件 snake_case,同包 init 注册)
middleware/ 7 个中间件 + 白名单 + 依赖接线(wire.go)
migrate/ 整库迁移引擎(内省/翻译/DDL 渲染/搬运/序列修复)
model/
entity/ 实体(gf gen dao 产物)
do/ 数据对象 DO(gf gen dao 产物)
hooks/ 全局模型钩子(审计盖章/软删改写/租户隔离执行点)
router/ 路由注册(router.go)+ gmeta 反射绑定(bind.go)+ 元数据表(meta/)
service/ 服务接口层(每模块一文件,手维护,勿整体运行 gf gen service)
task/ 定时任务处理器实现(init 自注册到 logic.RegisterJobHandler)
templates/ 代码生成器模板(后端 {{ }} 定界;ui/ui2 前端 [[ ]] 定界)
utility/ 基础能力包(cache/captcha/crypto/excel/ip2region/jwt/
response/ua/upload)
manifest/
config/config.yaml 运行时配置(唯一配置文件)
data/ip2region.xdb IP 归属地库
resource/
sql/ 五方言定稿 DDL(mysql 为规范基准,后四份待人工校对)
static/ 静态资源(swagger 页面、验证码字体等)
swagger/ 本地托管的 API 文档页面
upload/ 上传文件静态服务根(SetServerRoot("resource"))
hack/
config.yaml gf gen dao/service 配置(含自定义 dao_index 模板路径)
tpl/ 自定义 dao 扩展模板(挂载全局钩子 + Ctx 覆写)
golden/ after*/ 接口响应黄金样本(回归比对基线)
ui/ 前端工程(Element Plus)
document/
architecture.md 早期架构定稿(历史参考)
*.sql / 迁移日志 DDL 定稿与迁移实测记录
wiki/ 本开发者文档
--------------------------------------------------------------------------------
四、27 模块一览(文件名 = 表名去 goframe_ 前缀,snake_case)
--------------------------------------------------------------------------------
域 模块 表 路由前缀
-------- ------------- ---------------------- ----------------------
auth (无表) 写 goframe_login_log /api/login 等顶层
index (无表) 聚合 user/menu /api/index/*
system user goframe_user /api/user
role goframe_role(+role_menu)/api/role
menu goframe_menu /api/menu
dept goframe_dept /api/dept
level goframe_level /api/level
position goframe_position /api/position
tenant goframe_tenant /api/tenant
data config goframe_config /api/config
config_item goframe_config_item /api/config/item
param goframe_param /api/param
dict goframe_dict /api/dict
dict_item goframe_dict_item /api/dict/item
notice goframe_notice /api/notice
city goframe_city /api/city(别名 /api/area)
content article goframe_article /api/article
category goframe_category /api/category(树形)
link goframe_link /api/link
monitor operation_log goframe_operation_log /api/oper/log
login_log goframe_login_log /api/login/log
job goframe_job /api/job
job_log goframe_job_log /api/job/log
online_user (无表,Redis) /api/onlineUser
server_monitor (无表) /api/server
cache_monitor (无表,Redis INFO) /api/cache
tool example goframe_example /api/example
file_template goframe_file_template /api/file/template
generator (information_schema) /api/generator
upload (resource/upload) /api/upload
关联表不单列模块:user_role 归 user logic;role_menu 归 role logic。
--------------------------------------------------------------------------------
五、常用命令
--------------------------------------------------------------------------------
go build ./... 编译
go test ./... 测试
go vet ./... && gofmt -l . 静态检查与格式化检查
go run main.go 本地运行(:8000)
go build -o bin/admin-server.exe main.go 编译产物
gf gen dao -c hack/config.yaml 表结构变更后刷新 dao
(service 手维护,勿运行 gf gen service)
代码生成(详见 07 章):
go run ./cmd/generator -tables "goframe_order|订单表" # 一键生成
go run ./cmd/generator -tables "goframe_order|订单表" -preview # 仅预览
go run ./cmd/generator -tables "goframe_order|订单表" -batch # 批量统计
整库迁移(详见 08 章):
go run ./cmd/migrator -src-type=mysql ... -dst-type=pgsql ... [-drop-target-first]
重启(Windows,端口 8000,见 document/启动命令.txt):
netstat -ano | findstr ":8000" | findstr LISTENING
taskkill /F /PID <pid>
go build -o bin/admin-server.exe main.go && ./bin/admin-server.exe
--------------------------------------------------------------------------------
六、环境依赖
--------------------------------------------------------------------------------
MySQL 127.0.0.1:3306 root/root 库 easygoadmin.goframe.elevue
(库名含点;DDL 引用需反引号;定稿脚本 document/easygoadmin.goframe.elevue.sql)
Redis 127.0.0.1:6379 (不可用时自动降级进程内存,见 02/06 章)
测试账号 admin / admin123(超管,角色 ROLE_SUPER_ADMIN)
静态访问 /upload/** 由 SetServerRoot("resource") 提供
API 文档 GET /swagger(本地 Redoc);OpenAPI 原生 GET /api.json
健康检查 GET /api/health(免登录)
--------------------------------------------------------------------------------
七、文档索引
--------------------------------------------------------------------------------
00_总览与目录索引.txt 本文件
01_系统架构设计.txt 分层调用链、启动流程、路由绑定、统一响应、全局钩子
02_中间件与请求处理链.txt 7 中间件逐一说明、白名单、限流、演示模式
03_认证授权与多租户.txt JWT 双令牌、登录闭环、RBAC、数据权限(data_scope)、
租户隔离、在线用户登记
04_模块开发规范与CRUD基座.txt 七件套、gmeta 注解、ModuleSpec 声明式基座、字典文本
05_共享能力层与文件富文本.txt base.go 能力清单、上传两段式、[IMG_URL] 契约
06_监控体系与在线用户.txt 在线用户/服务器/缓存监控、操作日志、登录日志
07_代码生成器.txt HTTP+CLI、模板体系、副作用(菜单/字典/路由)、保护策略
08_数据库迁移.txt migrator 五阶段、方言渲染器、数据搬运、序列修复
09_定时任务调度.txt 调度器、执行策略、执行锁、task 包、内置任务
10_配置参考.txt config.yaml 全键注释、环境变量覆盖、部署要点与拓扑图
11_API接口清单.txt 全部路由(方法/路径/权限/日志)按模块清单
12_数据模型与表清单.txt 25 表清单、ER 关系图、13 个唯一索引、枚举约定、
租户表清单、大表注意、建表变更工作流
13_开发FAQ与排障.txt 按主题分组的 34 个常见问题(路由/认证/数据/文件/
任务/缓存/生成器/迁移/工程)
14_核心模块实现详解.txt index/role/menu/user/tenant/dict/city/config 等
高频模块的实现级细节(编码规则、缓存清理链、
开户事务、城市编解码等)
15_缓存键与失效策略.txt 缓存分层图、Redis 键全集(TTL/读写方)、主动失效
点清单、进程内缓存、TTL 设计原则
16_测试与质量保障.txt 静态门禁、黄金样本回归规范(录制参数/三类归因)、
运行期探针、测试补齐建议、变更回归映射、发布清单
17_安全设计专题.txt 威胁→控制总表、认证/授权/租户/文件安全链、
部署安全基线、已知边界与取舍
18_扩展开发与变更管理.txt 按变更类型的 checklist(新接口/字段/模块/中间件/
任务/dao/配置)、逃生舱挂点、前端遗留与未启用项清单、
封板变更纪律
--------------------------------------------------------------------------------
八、图示索引(ASCII 图分布在各章)
--------------------------------------------------------------------------------
01 §1.1 系统总架构图(分层 + 组件)
01 §1.2 模块依赖方向图(允许/禁止的依赖)
02 §12 请求处理时序图(中间件→业务→缓存/DB + 日志异步落库)
03 §3.1 登录时序图
03 §3.2 令牌生命周期状态图
03 §3.3 RBAC 授权链路图
04 §4.5 模块文件结构图(四件套职责)
04 §4.6 CRUD 流水线流程图(Page/Add/Update/Delete 落点)
05 §2.7 上传两段式流程图
05 §3.3 富文本 [IMG_URL] 转换流图
06 §1.5 在线会话生命周期图
07 §4.1 生成器 writeFile 落盘决策图
07 §4.2 生成器总体流水线图
08 §3.1 迁移五阶段流程图
08 §3.2 数据搬运循环图(主键游标分页)
09 §5.1 调度执行流程图
09 §5.2 任务状态机
10 §18.1 部署拓扑图
12 §2 ER 关系图(25 表逻辑关系)
15 §1 缓存分层图(进程内 / Redis / 数据库)
+268
View File
@@ -0,0 +1,268 @@
================================================================================
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 文件名
(与早期“按域分目录”设计不同,以现状为准)。
@@ -0,0 +1,217 @@
================================================================================
EasyGoAdmin 后端开发者文档 · 02 中间件与请求处理链
================================================================================
代码位置:internal/middleware/{cors,response,ratelimit,demo,jwt,tenant,
operation_log,public,util,wire,log}.go
注册顺序(router.go s.Use,顺序即执行顺序):
CORS → Response → RateLimit → Demo → JWT → Tenant → OperationLog → 处理器
(Bind 包装层做权限校验)
--------------------------------------------------------------------------------
1. CORS(cors.go)
--------------------------------------------------------------------------------
读取 cors.allowOrigin:
空或 "*" → r.Response.CORSDefault()(框架内置全放行策略)
具体来源 → 显式回显 Access-Control-Allow-Origin/Credentials/Headers/Methods,
OPTIONS 预检直接 204 返回。
Allow-Headers:Origin,Authorization,Content-Type,Accept,Token,X-Requested-With
Allow-Methods:GET,POST,PUT,DELETE,OPTIONS
--------------------------------------------------------------------------------
2. Response(response.go)—— 统一响应兜底
--------------------------------------------------------------------------------
职责一:SVG 静态文件加固。路径以 .svg 结尾时强制
Content-Disposition: attachment + X-Content-Type-Options: nosniff,
防止上传的 SVG 被浏览器同源内嵌执行(存储型 XSS 防线)。
职责二:请求元信息写入 ctx(realClientIP / User-Agent / Method / URL),
供登录日志与操作日志采集(consts.WithRequestMeta)。
职责三:panic 恢复。defer recover() 记录堆栈日志;若尚未写出响应则输出
500 统一信封(CodeServerError);已写出则只记录,避免双写。
--------------------------------------------------------------------------------
3. RateLimit(ratelimit.go)—— 固定窗口限流
--------------------------------------------------------------------------------
开关与阈值(manifest/config):
rateLimit.enable 默认 true
rateLimit.windowSeconds 窗口秒数,默认 60
rateLimit.limit 普通接口每窗口每 IP 每路径上限,默认 120(0=关闭普通限流)
rateLimit.authLimit 认证入口独立更严阈值,默认 10
规则:
* 仅限 /api 前缀路径;/api/health 显式豁免;/api.json、/swagger、静态资源
不带 /api/ 前缀自然不受限。
* 认证入口(/api/login、/api/captcha、/api/oauth2/token)用 authLimit。
* 键:ratelimit:{clientIP}:{path}:{窗口起点},窗口起点 = Unix/window*window
对齐;TTL = window+5s(仅用于回收,不影响窗口归属)。
* 计数经 utility/cache(Redis 优先、内存降级)→ 多实例共享计数。
* 计数存储异常时放行(限流不阻断业务);超限返回 code=429 信封。
* incrCounter:GET→SET 自增;不存在时 SET NX 抢占;并发下 NX 失败再读现值加一。
真实 IP 取值(util.go realClientIP):
直连来源不在 api.proxyIps(逗号分隔,支持精确 IP 与 CIDR)内 → 直接用
GetRemoteIp();在列表内才信任 X-Forwarded-For 首段 / X-Real-IP。
防止伪造 XFF 绕过限流或污染日志。
--------------------------------------------------------------------------------
4. Demo(demo.go)—— 演示模式
--------------------------------------------------------------------------------
配置 app.demo=true 时启用:
* GET/HEAD/OPTIONS 放行(演示站可浏览)
* 公开白名单路径放行(保证演示站可登录)
* 其余写操作(POST/PUT/PATCH/DELETE)一律拒绝:
code=1015 “演示环境,禁止操作!”
生产环境保持 app.demo=false。
--------------------------------------------------------------------------------
5. JWT(jwt.go)—— 身份认证
--------------------------------------------------------------------------------
流程:
1) IsPublicPath 命中 → 放行(登录/验证码/令牌兑换/健康检查/文档)
2) 取 token:Authorization: Bearer xxx(兼容小写与无前缀),
缺失时回退查询参数 ?token=(个别 URL 直传场景)
3) jwtutil.Parse:HS256 + 签名/过期/签发者(jwt.issuer)校验,
非 HMAC 或无效 → 401
4) 黑名单检查:cache.Exists(easygoadmin:token:blacklist:{jti}) → 401
(登出、令牌轮换、强制下线写入)
5) ctx 注入:UserID/TenantID/Username/Realname/TokenJTI/TokenExpire
6) 身份扩展(角色+权限+数据范围):优先读缓存 easygoadmin:user:identity:{userID}
(JSON,TTL 300s),未命中回源 IdentityLoader(= logic.LoadIdentity)并回写。
注入 ctx:Roles/Perms + DataScope(数据范围档位)/DeptID(部门ID),
后两者供数据权限过滤读取(见 03 章第 6 节)。
加载失败不阻断请求(告警日志),回退空身份(等价无权限;数据范围
GetDataScope 读 0 回退"全部"——无权限下列表本就不可达)。
loadIdentity 说明:userID<=0 直接跳过;IdentityLoader 为 nil(未接线)时
返回空身份。
--------------------------------------------------------------------------------
6. Tenant(tenant.go)—— 租户状态校验
--------------------------------------------------------------------------------
位于 JWT 之后:
* 公开路径放行
* ctx tenant_id=0(平台/超管)直接放行
* 其余加载租户摘要(缓存 easygoadmin:tenant:info:{id},TTL 60s,未命中
回源 TenantLoader = logic.LoadTenant):
- status != 1 → code=1006 租户已禁用
- expire_time 早于现在 → code=1005 租户已过期
* LoadTenant 语义:租户不存在 → 按禁用处理(失效关闭);数据库基础错误 →
返回 nil,中间件放行(避免存储抖动导致全站不可用)。
--------------------------------------------------------------------------------
7. OperationLog(operation_log.go)—— 操作日志采集
--------------------------------------------------------------------------------
开关:operationLog.enable(总开关);operationLog.logQuery(是否记录查询类)。
流程:请求前记 start → Next() → 从 ctx 取路由元数据(meta.CtxKey),
无 log 标签直接跳过 → logType=LogTypeMap[rm.LogType](query 且 logQuery=false
跳过)→ 组装 OperationLogEntry:
Title=log 标签 Type=logType Source=0
Method="GET /api/user/page"(路由模板) RequestMethod/URL=实际请求
Param:GET/DELETE 取 query JSON;写操作优先取 body(maskSecret 脱敏
password/oldPassword/newPassword/salt/access_token/refresh_token → ***)
Result:响应信封 JSON(截断 8000 字节,UTF-8 边界回退截断)
IP=realClientIP Location=ip2region OS/Browser=ua 解析
ConsumeTime=毫秒 Status:响应 code==0 → 0 正常,否则 1 异常(Error=msg)
CreateUser=ctx 用户名
异步落库:有界通道(2048)+ 单 worker;批量满 100 条或每 1s flush 一批,
逐条经 LogSink(= 业务侧注册的落库函数)写 goframe_operation_log;
落库失败仅记错误日志;通道满则丢弃(每丢 100 条告警一次),绝不阻塞业务。
--------------------------------------------------------------------------------
8. 免登录白名单(public.go)
--------------------------------------------------------------------------------
publicPaths 精确匹配:
/api/login、/api/register(白名单放行,路由未注册)、/api/captcha、/api/oauth2/token、/api/health
另放行 /api.json 与 /swagger 前缀(文档)。
三处须保持一致(改动时同步):
1) middleware/public.go 白名单
2) 各接口 gmeta 的 public:"true" 标注(auth.go)
3) 健康检查等 router.go 中的手工 BindHandler
注意:public 路由不带 /v1 版本段的历史约定已固化为“全部路由都不带 /v1”,
白名单与 gmeta path 均以 /api 直达。
--------------------------------------------------------------------------------
9. 依赖接线(wire.go)
--------------------------------------------------------------------------------
中间件层不 import 业务包,通过三个函数变量解耦:
var IdentityLoader func(ctx, userID) (*Identity, error) // 角色+权限
var TenantLoader func(ctx, tenantID) (*TenantInfo, error) // status/expire
var LogSink func(ctx, *OperationLogEntry) error // 日志落库
注册方(logic 各文件 init):
internal/logic/auth.go init: SetIdentityLoader(LoadIdentity)
SetTenantLoader(LoadTenant)
internal/logic/operation_log.go init: SetLogSink(SaveOperationLog)
(异步 worker 每条回调,写 goframe_operation_log)
Identity 结构:{Roles []string(角色编码), Perms []string(权限节点),
DataScope int(数据范围档位,多角色取最宽松;0=旧缓存缺字段,
读侧 GetDataScope 回退"全部"), DeptId int(所属部门ID)}
TenantInfo 结构:{ID, Status, ExpireTime}(热路径最小字段集)
cmd/wire.go 在启动时仅校验三者是否非 nil,缺失输出警告(不 panic)。
--------------------------------------------------------------------------------
10. 辅助工具(util.go / log.go)
--------------------------------------------------------------------------------
cfgBool/cfgInt 读配置,缺失回退默认值
gjsonEncode JSON 序列化(失败返回空串)
trunc 按字节截断并回退到 UTF-8 字符边界(防 MySQL 拒绝非法字节)
maskJSONField 正则替换 JSON 文本中指定字段值为 ***(正则缓存复用)
realClientIP 受信代理 + XFF/X-Real-IP
isTrustedProxy api.proxyIps 精确 IP / CIDR 匹配
requestLocation ip2region.Location(失败返回 "-")
glogWarn 中间件统一告警输出
--------------------------------------------------------------------------------
11. 请求生命周期(一个受保护写请求的完整路径)
--------------------------------------------------------------------------------
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 组装记录 → 异步通道 → 批量落库
12. 请求处理时序图(受保护写请求 + 操作日志异步落库)
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
短路/异常分支:
* 任一中间件短路(429 / 演示拦截 / 401 / 403)→ 直接 WriteJson,后续不执行
* panic → Response recover → 500 信封(已写出响应则仅记录日志)
* logChan 满 → 丢弃并计数告警(每丢 100 条输出一次 Warning)
@@ -0,0 +1,283 @@
================================================================================
EasyGoAdmin 后端开发者文档 · 03 认证授权与多租户
================================================================================
代码位置:internal/logic/auth.go、index.go、role.go、menu.go、tenant.go、
user.go、online_user.go、data_scope.go(数据权限);utility/jwt;
utility/captcha;utility/crypto
--------------------------------------------------------------------------------
1. JWT 双令牌(utility/jwt/jwt.go)
--------------------------------------------------------------------------------
算法 HS256;密钥 jwt.secret;签发者 jwt.issuer(默认 easygoadmin)。
Claims(golang-jwt/v5 + RegisteredClaims):
user_id / tenant_id / username / realname / typ(access|refresh)
RegisteredClaims:jti(随机)、iss、iat、exp(exp 必填,Parse 强校验)
有效期配置:
访问令牌 jwt.accessTokenExpire 默认 7200s(2h)
刷新令牌 代码读取 jwt.refreshExpire,默认 604800s(7d)
(config.yaml 中的 refreshTokenExpire 为同值说明项,运行时以
jwt.refreshExpire 为准,未配置走 7 天默认值)
Sign(ctx, tokenType, userID, tenantID, username, realname) → (token, claims, err)
Parse(ctx, tokenStr) → 校验 HMAC 方法(WithValidMethods HS256)、签名、
过期、签发者;失败统一返回 401 "Token无效或已失效"。
ExpireDuration(ctx, tokenType) → 有效期(黑名单 TTL 计算用)。
黑名单:easygoadmin:token:blacklist:{jti} = "1",TTL = 令牌剩余有效期。
写入时机:登出、refresh 轮换旧令牌、RemoveToken 指定撤销、在线用户强退
(访问+刷新一并拉黑)。
--------------------------------------------------------------------------------
2. 图形验证码(utility/captcha)
--------------------------------------------------------------------------------
GET /api/captcha → Captcha 生成(utility/captcha.Generate):
* 6 位字符(captcha.length,4~8),字符集排除易混淆字符
* 缓存 easygoadmin:captcha:{uuid},TTL captcha.expire(300s)
* 渲染 PNG(TTF 字体 resource/static/fonts/Vera.ttf + 干扰线/噪点),
字体缺失降级 SVG
* 返回 {key: uuid, captcha: "data:image/...;base64,..."}(前端直接展示)
Verify(ctx, key, code):一次性校验(命中即删除);配置 captcha.bypassCode
可免校验(仅开发调试,生产留空)。
--------------------------------------------------------------------------------
3. 登录闭环(logic/auth.go Login)
--------------------------------------------------------------------------------
POST /api/login(公开),入参 {username, password, tenantCode?, code?, key?}
流程(每一步失败均写登录日志 log_type=1 status=1):
1) 提供 key 时强制验证码校验(captcha.Verify)
2) findUser:username 全局查找;带 tenantCode 先按 code 定位租户再过滤;
多租户同名 → 优先平台租户(tenant_id=0),仍歧义 → 提示携带租户编码
3) 账号状态 status==1 校验(1002)
4) 租户状态与过期校验(mustLoadTenant,60s 缓存;1006/1005)
5) 密码:crypto.VerifyPassword(bcryptHash, password, salt)
—— bcrypt(password+salt) 双重加盐;IsBcrypt 识别旧 md5 数据(登录会拒绝,
需重置密码);salt 为空兼容直接比对
6) 签发双令牌(access + refresh,claims 含 user/tenant/username/realname)
7) 写登录日志成功(log_type=1 status=0,含 ip/location(os 归属地)/os/browser/
consume_time;code 参数脱敏为 ***)
8) MarkOnline 登记在线会话(见第 7 节)
返回(OAuth2 惯例命名,勿改驼峰):
{access_token, refresh_token, expires_in(秒), token_type:"Bearer"}
登录日志字段:username/log_type/source(0 后台)/method/request_method/url/param/
result(成功|失败)/ip/location/os/browser/consume_time/status/error。
3.1 登录时序图
Client Server/auth Redis DB
│ GET /api/captcha │ │ │
│────────────────────▶│ Generate ─────────▶│ SET captcha:uuid│
│◀── {key, 图片base64} │ │ │
│ POST /api/login {username,password,code,key,tenantCode?}
│────────────────────▶│ Verify ───────────▶│ GET+DEL captcha:uuid
│ │ findUser ──────────────────────────▶│ user(+tenant)
│ │ status==1? mustLoadTenant ────────▶│ tenant(60s缓存)
│ │ bcrypt(password+salt) 比对 │
│ │ Sign(access) + Sign(refresh) │
│ │ writeLoginLog(成功/失败) ──────────▶│ login_log
│ │ MarkOnline ───────▶│ ZAdd online:jti:set
│ │ │ SET online:info:{jti}
│◀── {access_token, refresh_token, expires_in, token_type} │
3.2 令牌生命周期状态图
Sign(access 2h) Refresh 轮换 / Logout / 强退
────────────────▶ 有效 ────────────────────────────────────────▶ 黑名单
▲ │ JWT中间件每请求: Parse + 黑名单Exists │
│ ▼ ▼
│ 到期(exp) ──前端──▶ POST /api/oauth2/token 自然过期(TTL
│ (refresh_token 模式) = 剩余有效期)自动清理
└── 旧 refresh 已拉黑则 1008;否则旧 refresh 入黑名单 + 签发新对
3.3 RBAC 授权链路图
请求 ──▶ JWT中间件 ──▶ LoadIdentity(userID)
│
user ─────────┤ dept_id → DeptId[](数据权限用)
user_role ────┤ role.code → Roles[]
│ role.data_scope → DataScope[](多角色取最宽松)
role_menu ────┤
menu(type=1, ┤ permission → Perms[]
status=0, │
hide=0) ────┘
▼
缓存 user:identity:{uid} JSON TTL 300s
▼
Bind 包装层 HasPerm("sys:user:page"):
permission=="" → 通过 │ Roles 含 ROLE_SUPER_ADMIN → 通过
│ Perms 精确匹配 → 通过 │ 否则 403
数据范围经 ctx(WithDataScope/WithDeptID)随行,
在 applyFilters 末环过滤(见第 6 节)
缓存失效:角色保存权限/改数据范围 / 用户分配角色/改部门 等主动 Del,过期兜底
--------------------------------------------------------------------------------
4. 令牌刷新与撤销
--------------------------------------------------------------------------------
POST /api/oauth2/token(公开)Refresh:
* grant_type=password 或无 refresh_token → 兼容密码模式,复用 Login
* refresh_token 模式:
- Parse 校验 + typ 必须为 refresh(1008)
- 旧 refresh 已在黑名单 → 1008(防重放)
- 旧 refresh 入黑名单(轮换)
- 签发新 access+refresh,MarkOnline 刷新在线登记
- 返回同登录格式四字段
GET /api/oauth2/check/token 登录可用;JWT 中间件通过即 {valid:true, userId}
DELETE /api/oauth2/remove/:token 登录可用;解析指定 token 的 jti 入黑名单
GET /api/logout 当前 jti 入黑名单 + MarkOffline + 登录日志(log_type=2)
--------------------------------------------------------------------------------
5. RBAC 权限模型
--------------------------------------------------------------------------------
表关系:user —(user_role)— role —(role_menu)— menu(type=1 权限节点)
身份加载 LoadIdentity(JWT 中间件调用,缓存 300s,键 user:identity:{uid}):
1) user 行回查 dept_id → DeptId(数据权限过滤用;单列查询)
2) user_role → roleIds → role.code 集合(Roles)
同一查询顺带读 role.data_scope:多角色取最宽松(数值 min,
1 最宽 4 最严;缺列值按"全部"参与比较)→ DataScope
3) role_menu → menuIds → menu 中 type=1 且 status=0 且 hide=0 的
permission 非空去重集合(Perms)
4) 超管/平台:DataScope 强制"全部"(过滤层另有 dataScopeBypass 双保险)
权限判定 consts.HasPerm(ctx, "sys:user:page"):
permission 为空 → 通过(仅登录即可)
Roles 含 ROLE_SUPER_ADMIN → 通过(超管全放行)
Perms 精确匹配 → 通过;否则 403。
前端识别超管通配权限 *:*:*(getUser 返回)放开全部按钮权限。
权限点声明:接口 gmeta permission 标签;生成器自动创建
sys:{module小驼峰}:{page|list|detail|add|update|delete|batchDelete|status|import|export}
节点并挂到模块菜单下。
角色管理扩展接口:
GET /api/role/menu/list/:roleId 查询角色已勾选菜单(sys:role:getPermission)
POST /api/role/menu/save 重写 role_menu + 清理相关用户身份缓存
(sys:role:savePermission)
菜单树 GetUserMenuTree:user→role→role_menu→menu 组装(index.GetMenus 消费);
超管返回全量菜单。
缓存失效:角色权限变更/改数据范围(roleSpec.AfterUpdate →
roleClearIdentityCache)/ 用户分配角色/用户改部门(user.Update →
AssignRoles → clearIdentityCache)后清理 user:identity:{uid}
(及 user:perms/user:roles 键),最迟 300s 自然过期兜底。
6. 角色数据权限(行级,role.data_scope,logic/data_scope.go)
与租户隔离(第 8 节,硬规则)独立叠加的第二层行级过滤,四档:
档位 含义 过滤条件(作用于挂接模块的列表行)
---- -------------- -----------------------------------------
1 全部 不过滤(租户隔离之内全量)
2 本部门及以下 dept_id IN (自身部门 + 全部子孙部门ID)
3 本部门 dept_id = 自身部门ID
4 仅本人 id = 自身用户ID
模块接线(一行声明):
userSpec = ModuleSpec[...]{ ..., DataScope: ApplyDataScope("dept_id") }
ApplyDataScope(deptColumn) 返回 DataScopeFunc 闭包,挂进 applyFilters
末环(Like→Eq→Keyword→Filter→DataScope),Page/List/导出自动生效。
目前仅 user 模块接线(全库仅 goframe_user 有 dept_id 列);
其他模块需要时声明同款闭包即可,无部门归属列的表可按需扩展
按创建人过滤的变体。
安全语义(与直觉不同的点):
* 多角色取最宽松(并集直觉),合并发生在 LoadIdentity 缓存层,
热路径零额外查询;
* 超管/平台完全绕过(IsPlatform 或 Roles 含 ROLE_SUPER_ADMIN,
dataScopeBypass 双判断);
* 未分配部门(dept_id=0)的用户挂档 2/3 时降级为"仅本人"
(防 dept_id=0 意外放大可见面);
* 仅作用于 Page/List/导出(applyFilters 链);Detail/Update/Delete
不过滤,由 permission 节点与租户隔离兜底(边界见 17 章第 8 节);
* 部门子树 deptSubtreeIds:dao.Dept.Ctx 拉全租户部门行(id,parent_id)
后 BFS(树小直查,无缓存;成热点可加短时缓存)。
配置链路:
role 表 data_scope 列(默认 1)→ 角色表单"数据范围"下拉(字典
role_data_scope 驱动,值 1/2/3/4)→ roleToData 归一(缺省回 1)
→ 角色 Update 后 AfterUpdate 清该角色全部用户身份缓存 → 生效。
--------------------------------------------------------------------------------
7. 个人中心(logic/index.go)
--------------------------------------------------------------------------------
GET /api/index/getUser 当前用户信息(超管返回通配权限 *:*:*)
GET /api/index/getMenus 当前用户菜单树(无参版,前端 common 菜单)
GET /api/index/getMenus/:id 同上(system/menu 页面调用)
PUT /api/index/updatePassword 旧密码校验(1012)+ bcrypt 新密码(事务外预计算)
PUT /api/index/updateProfile 更新个人信息(realname/avatar/mobile/email 等)
--------------------------------------------------------------------------------
8. 多租户
--------------------------------------------------------------------------------
7.1 数据模型
goframe_tenant:code(T+4位ID 规则由 Account 生成)、status(1 正常/2 禁用)、
expire_time(nil=永不过期)、max_user_num 等。
9 张租户级表带 tenant_id 列(见 01 章 5.3)。
7.2 读侧隔离
dao.Ctx(hooks.ApplyCtx):非平台上下文自动追加 tenant_id IN (0, 自身);
超管(ctx=0)不加过滤。写侧禁止手写该条件。
7.3 写侧归属
insertHook:租户用户插入未显式指定时自动填自身(防逃逸);超管未指定时填
tenant.defaultId(默认 1),显式 0 可建平台共享行,显式具体值从显式值。
业务取归属租户统一 consts.GetTenantID(ctx)。
7.4 请求级校验
Tenant 中间件(见 02 章)每请求校验 status/expire(60s 缓存);
登录路径 mustLoadTenant 同缓存复用。
7.5 租户开户(tenant.Account,POST /api/tenant/account)
创建租户 + 初始管理员:事务内建租户(code 生成)+ 建管理员用户 +
分配角色;max_user_num 上限校验(1007)在 user.Add 生效
(checkTenantUserMax)。事务纪律:bcrypt 等 CPU 计算在事务外预计算,
事务内不做 HTTP/文件 IO。
7.6 其他
tenant 用户列表:GET /api/tenant/users/:tenantId
租户状态变更后缓存 tenant:info:{id} 需清理(tenant 模块 clearCache 钩子)。
--------------------------------------------------------------------------------
9. 用户模块要点(logic/user.go)
--------------------------------------------------------------------------------
* 手写唯一校验:username/mobile 按“行现租户”检查(uk_tenant_username /
uk_tenant_mobile),Update 时以被改行的 tenant_id 为准(预检范围必须等于
唯一索引范围,详见 module_spec 章唯一校验说明)。
* Add:租户用户数上限校验;密码 bcrypt(事务外);genUserCode 生成用户编码;
AssignRoles 写 user_role。
* Update:AssignRoles 同步角色 + clearIdentityCache;城市编码经
ResolveCityCodes 存库、详情经 ResolveCityNames 还原。
* ResetPwd(PUT /api/user/resetPwd,sys:user:resetPwd):重置密码(bcrypt)。
* Import(POST /api/user/import):Excel(excelize)解析 → 逐行校验 →
批量插入(含角色/城市/密码默认);Export:ChunkQuery 分批防 OOM,
excel.WriteSheet 落临时文件返回 URL。
* 删除守卫:userDeleteGuard / 关联清理(user_role)。
--------------------------------------------------------------------------------
10. 在线登记(logic/online_user.go MarkOnline/MarkOffline)
--------------------------------------------------------------------------------
登录/刷新成功后登记:
zset easygoadmin:online:jti:set score=access 过期时间戳
详情键 easygoadmin:online:info:{jti} = JSON(含 userId、username、
realname、ip、location、os、browser、loginTime、access/refresh jti 与
起止时间戳、令牌原文(详情查看用)、tokenType、expiresIn),TTL=access
剩余有效期+60s。
登出/强退 MarkOffline:ZRem + Del。
在线用户模块的分页/详情/踢出见 06 章。
--------------------------------------------------------------------------------
11. 安全清单
--------------------------------------------------------------------------------
* 密码:bcrypt(password+salt),Cost=DefaultCost;旧 md5 数据被 IsBcrypt 拒绝
* 令牌:HS256 + jti 黑名单 + refresh 轮换(旧 refresh 拉黑防重放)
* 强退:访问+刷新令牌一并拉黑(防被踢会话经 refresh 复活)
* 限流:认证入口独立更严阈值(authLimit=10/分钟/IP)
* 脱敏:操作日志 param 中 password/salt/access_token/refresh_token → ***
* 代理:仅受信代理的 XFF 生效(api.proxyIps)
* 会话固定:登录信息全部由服务端签发 claims 承载,无 cookie 会话
@@ -0,0 +1,266 @@
================================================================================
EasyGoAdmin 后端开发者文档 · 04 模块开发规范与 CRUD 基座
================================================================================
代码位置:api/v1/{模块}.go、internal/controller/{模块}.go、
internal/logic/{模块}.go、internal/service/{模块}.go、
internal/logic/module_spec.go(ModuleSpec 基座)
--------------------------------------------------------------------------------
1. 新增模块四件套(平铺放置,同包 init 自动注册)
--------------------------------------------------------------------------------
api/v1/{模块}.go 全部 Req/Res 结构体 + gmeta 注解(单包 v1)
internal/controller/{模块}.go 控制器 CXxx,方法签名统一:
func (c *CXxx) Page(ctx, req *v1.XxxReq) (*v1.XxxRes, error)
internal/logic/{模块}.go 业务实现 sXxx + var xxxSpec = ModuleSpec[...]{...}
+ init(){ service.RegisterXxx(&sXxx{}) }
internal/service/{模块}.go 手维护接口 IXxx + RegisterXxx + 取用函数 Xxx()
路由:router.go registerControllers 追加 Bind(s, new(controller.CXxx))
(生成器自动写入;手动新增则自行追加在最后一个 Bind 行后)
dao 四件(dao/{m}.go、dao/internal/{m}.go、entity、do)由 gf gen dao 或
生成器产出。
文件命名:表名去 goframe_ 前缀的 snake_case(config_item.go / file_template.go);
前端文件仍用驼峰(fileTemplate.ts)。
--------------------------------------------------------------------------------
2. 标准七件套 + 扩展
--------------------------------------------------------------------------------
方法 HTTP 路由模板 说明
Page GET /api/{prefix}/page 分页(pageNo/pageSize + 筛选)
List GET /api/{prefix}/list 全量/树形(下拉、树选择)
Detail GET /api/{prefix}/detail/:id 详情
Add POST /api/{prefix}/add 新增,返回 {id}
Update PUT /api/{prefix}/update 修改(body 含 id)
Delete DELETE /api/{prefix}/delete/:id 软删(del_flag=1)
BatchDelete DELETE /api/{prefix}/batchDelete body {ids:[]}
Import POST /api/{prefix}/import multipart file(xls/xlsx)
Export GET /api/{prefix}/export 导出文件
Status PUT /api/{prefix}/status 状态切换(有 status 列的模块)
命名约定:结构体 {Mod}{Action}Req / {Mod}{Action}Res;controller 方法名 =
logic 方法名 = 上表方法名。日志标题 "{模块中文名}-{动作}",动作映射:
page=查询分页记录 list=查询列表(树形=查询树形列表) detail=查询详情
add=新增记录 update=修改记录 delete=删除记录 batchDelete=批量删除记录
import=导入数据 export=导出数据 status=设置状态 reset=重置。
--------------------------------------------------------------------------------
3. gmeta 注解完整示例
--------------------------------------------------------------------------------
type UserPageReq struct {
gmeta.Meta `method:"get" path:"/api/user/page" summary:"用户分页"
permission:"sys:user:page" log:"用户管理-查询分页记录" logType:"query"`
common.PageReq
Keyword string `json:"keyword" dc:"关键词"`
Status int `json:"status" dc:"状态"`
}
* permission 空 = 登录即可(如 /api/index/*、/api/job/daemonStatus、
字典别名 /api/dictionary/* 等)
* public:"true" 仅用于免登录路由,且必须同时出现在 middleware/public.go
* 入参 json 一律驼峰;路由参数 :id 由 gmeta path 声明、req 结构同名字段承接
--------------------------------------------------------------------------------
4. ModuleSpec 声明式 CRUD 基座(module_spec.go)
--------------------------------------------------------------------------------
泛型 ModuleSpec[Item, LItem, DRes]:
Item 分页行类型(Page/AfterPageList)
LItem 列表行类型(List/树形,可与 Item 相同)
DRes 详情类型
设计纪律:
1) 基座禁止“按模块分支”——差异只进声明字段与钩子;写不进钩子 → 手写(合法逃生舱)
2) 钩子用具体函数类型;req any 经 mustAs[T] 断言(失败=编程错误,panic)
3) 钩子内可改元素/data,禁止 append、禁止 HTTP/文件 IO(文件迁移统一在 saveTransform)
4) applyFilters 固定顺序:LikeFields → EqFields → KeywordFields → Filter → DataScope;
租户/软删隔离由 dao.Ctx 先于本链注入
5) ToData:Add=全量必填;Update=全量表单语义(前端 Detail 回显 → 全量 PUT)
6) 钩子错误记 stage 日志后原样上抛(保留 gerror 语义码)
4.1 声明字段
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 默认排序(如 "id DESC");ListOrderBy 列表排序;
OrderableFields 前端排序白名单(orderField/orderType 仅白名单内生效)
IsTree/ParentOf/ChildSetter 树形(List 末尾建树;缺 ParentOf fail-fast)
SerializeMaps map[字段]字典编码 {field}Text 填充
FileFields / MultiFileFields / ContentFields /
FileSaveDir 文件/富文本声明(写侧转正、读侧补 URL/还原占位符,见 05 章)
FieldLabels 唯一校验提示覆盖
UniqueFields []UniqueRule{Field, Group} 唯一性预检(Group 支持分组唯一,
如 Group:"tenant_id")
DataScope 行级数据权限(共享工厂 ApplyDataScope(deptColumn),
logic/data_scope.go;nil 跳过;仅 Page/List/导出生效;
模块一行声明即接线,机制见 03 章第 6 节,安全边界见 17 章)
钩子 BeforeAdd/AfterAdd/BeforeUpdate/AfterUpdate/BeforeDelete/
AfterDelete/BeforeBatchDelete/AfterBatchDelete/
AfterPageList(禁 append)/AfterDetail
ToData req → map[string]any(新增/修改共用转换)
4.2 基座流水线
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
MergeOld(data, id):把 data 中缺失/零值键用库中旧值补齐(全量覆盖辅助)
4.3 标准模块声明示例(节选自 job 模块)
var jobSpec = ModuleSpec[v1.JobItem, v1.JobItem, v1.JobDetailRes]{
Model: dao.Job.Ctx,
LikeFields: []string{"job_name", "job_alias"},
EqFields: []string{"status"},
OrderBy: "id DESC",
SerializeMaps: map[string]string{"status": "job_status", ...},
ToData: jobToData,
BeforeAdd: jobDefaultStatus,
BeforeUpdate: jobExistGuard,
AfterUpdate: jobRemount,
BeforeDelete: jobDeleteGuard,
}
业务方法三行委托:
func (s *sJob) Page(ctx, req) (*v1.JobPageRes, error) {
list, total, err := jobSpec.Page(ctx, req, req.GetPageNo(), req.GetPageSize())
...
return v1.NewPageData(..., derefSlice(list)), nil
}
4.4 唯一校验语义(重要)
UniqueFields 为“先查后插”的 UX 快速报错,存在 TOCTOU 竞态;并发唯一性以
数据库 uk_* 唯一索引为准(调用点必须保证对应索引存在)。
Group 语义:校验范围必须等于唯一索引范围——索引含 tenant_id 时必须
Group:"tenant_id",否则范围取窄漏报 1062、取宽跨租户误报。
手写检查(如 user 的 username/mobile)按“被改行现租户”校验,理由相同。
4.5 模块文件结构图(以 config_item 为例)
api/v1/config_item.go
├── type ConfigItemPageReq { gmeta.Meta(get /api/config/item/page …), PageReq, 筛选字段 }
├── type ConfigItemAddReq / UpdateReq / DeleteReq / BatchDeleteReq / …
└── type ConfigItemItem / ListItem / DetailRes / …Res
internal/controller/config_item.go
└── type CConfigItem struct{};方法转发 service.ConfigItem().Page(ctx, req)…
internal/service/config_item.go(手维护)
├── interface IConfigItem { Page/List/Detail/Add/Update/Delete/BatchDelete… }
├── var localConfigItem IConfigItem
├── func ConfigItem() IConfigItem ← controller 取用
└── func RegisterConfigItem(i IConfigItem) ← logic init 注册
internal/logic/config_item.go
├── type sConfigItem struct{}
├── var configItemSpec = ModuleSpec[…]{…} ← 差异全部进声明
├── init(){ service.RegisterConfigItem(&sConfigItem{}) }
└── 方法:三行委托基座 + 手写扩展接口(逃生舱)
internal/dao/config_item.go + dao/internal + entity + do ← 生成器/gf gen dao 产出
4.6 CRUD 流水线流程图(基座执行顺序,横切逻辑落点)
Page/List:
Model(ctx) ─▶ LikeFields ─▶ EqFields ─▶ KeywordFields ─▶ Filter ─▶ DataScope
└─ 前置:dao.Ctx 已注入 del_flag=0 + 租户 IN(0,自身)
─▶ Count ─▶ applyOrder(白名单) ─▶ .Page(pageNo,pageSize) ─▶ structRows[T]
─▶ readTransform:FileFields 补URL → MultiFileFields 补URL
→ ContentFields 展开[IMG_URL] → FillTextFields({field}Text)
─▶ AfterPageList(禁 append)/IsTree 时 List 走 buildTree
Add:
ToData(req) ─▶ BeforeAdd ─▶ saveTransform(temp→正式, 富文本占位符化)
─▶ checkUniqueRules(UniqueFields 预检,Group 支持分组唯一)
─▶ Insert(hooks: 盖章 create_*/del_flag/tenant_id,主键自增)
─▶ LastInsertId ─▶ AfterAdd ─▶ 返回 {id}
Update:
ToData(req) ─▶ BeforeUpdate(可用 MergeOld 补零值)─▶ saveTransform
─▶ checkUniqueRules(排除自身) ─▶ Update(仅更新 data 键,hooks 盖章)
─▶ AfterUpdate
Delete / BatchDelete:
BeforeDelete(守卫: 存在性/子级/引用) ─▶ Delete(deleteHook 改写 del_flag=1)
─▶ AfterDelete;批量为逐条循环 + Before/AfterBatchDelete
--------------------------------------------------------------------------------
5. 字典驱动的 {field}Text
--------------------------------------------------------------------------------
模块头部声明 SerializeMaps = map[字段]字典编码,编码规范
{模块snake}_{字段snake}(如 user_status、job_execute_policy)。
填充路径(二选一,机制同源):
* 基座 FillTextFields:结构体存在 {Field}Text string 字段即自动填充
(反射计划按类型缓存)
* map 场景 SerializeMapOnRead(ctx, row, maps):原地补 row[field+"Text"]
取值 DictGetText(ctx, code, value):查 dict→dict_item 的 name,
进程内缓存 300s(dictTextCache);未命中返回原值。
文案唯一真相在字典管理页(dict_item.name),修改最迟 5 分钟生效
(或调 GET /api/dict/refreshCache 刷新)。
例外:operation_log.type 存注解常量值,用静态表 LogTypeText
(consts.LogTypeNames),不走字典。
--------------------------------------------------------------------------------
6. 筛选对齐三件套(前端 ↔ 后端)
--------------------------------------------------------------------------------
前端 views/{域}/{模块}/querySchemas.ts 每个筛选输入后端对应:
字符串 Input(LikeFields)→ 逐列 LIKE,req 同名驼峰字段取值
Select/InputNumber(EqFields)→ 等值,零值跳过
关键词框(KeywordFields,可选)→ req.keyword 多列 OR
导出统一走 xxxSpec.applyFilters 与列表同套筛选(保证导出=所见)。
声明式之外的复杂条件写 Filter 钩子,不要重复 Like/Eq 已覆盖的条件。
--------------------------------------------------------------------------------
7. service 接口文件规范(手维护)
--------------------------------------------------------------------------------
internal/service/{模块}.go 结构:
type IXCity interface {
Page(ctx context.Context, req *v1.CityPageReq) (res *v1.CityPageRes, err error)
...
}
var localCity ICity
func City() ICity {
if localCity == nil { panic("implement not found ... forgot register?") }
return localCity
}
func RegisterCity(ic ICity) { localCity = ic }
logic 的 init() 调用 RegisterXxx;controller 通过 service.Xxx() 取用。
logic 方法签名变更时同步手工更新接口文件;禁止整体运行 gf gen service。
--------------------------------------------------------------------------------
8. 导入导出实现模式(user / level / example 启用)
--------------------------------------------------------------------------------
Import:POST multipart file(*ghttp.UploadFile + p:"file" 标签)
excel.ReadSheet 解析(首行表头,全单元格字符串,空行跳过)→
逐行映射/校验(唯一、上限、字典值)→ 批量 Insert(默认密码/角色/城市编码
等按模块规则处理)→ 返回导入统计。
Export:GET,与列表同筛选 → ChunkQuery 分批(默认 500/页,防 OOM)→
excel.WriteSheet 写临时文件 → 返回文件 URL(/upload/temp/...)。
日志:logType import/export;权限 sys:{module}:import/export。
--------------------------------------------------------------------------------
9. 跨模块共享的包级函数(logic 单包的红利)
--------------------------------------------------------------------------------
因全部 logic 同包,跨模块复用无需 import:
ResolveCityNames(ctx, codes, sep) / ResolveCityCodes(ctx, cityInfo)(city.go,
user 的 Add/Update/Import/Detail 已接入)
LoadIdentity / LoadTenant(auth.go,供中间件接线与登录路径复用)
ApplyDataScope(deptColumn)(data_scope.go,行级数据权限工厂,
spec 声明 DataScope 即接线,见 03 章第 6 节)
DictGetText / SerializeMapOnRead / FillTextFields(base.go / module_spec.go)
新增包级共享函数时注意 base.go 的“导出函数上限 15 个”纪律(超出拆子包)。
--------------------------------------------------------------------------------
10. 开发一个新模块的推荐步骤
--------------------------------------------------------------------------------
1) 设计表结构(统一审计列 + del_flag + uk_* 唯一索引 + 列注释枚举
"状态:1-正常 2-禁用"),执行 DDL
2) 用生成器生成全套文件(见 07 章),或手动按四件套模板编写
3) 调整 logic:声明 xxxSpec(筛选/树形/字典/文件字段/唯一规则)、补充扩展接口;
表含部门归属列且需行级可见性时补 DataScope: ApplyDataScope("dept_id")
4) 维护 service 接口文件与 router 注册行
5) go build ./... + gofmt + 重启;在菜单管理中为角色勾选新权限节点
@@ -0,0 +1,198 @@
================================================================================
EasyGoAdmin 后端开发者文档 · 05 共享能力层与文件/富文本处理
================================================================================
代码位置:internal/logic/base.go(共享能力层)、utility/upload、
internal/logic/upload.go(上传接口)、manifest/config 的 file.* 配置
--------------------------------------------------------------------------------
1. base.go 共享能力层总览(横切能力,组合式复用)
--------------------------------------------------------------------------------
架构纪律(文件头约束):
1) 按能力分区:文件字段 / 富文本 / 枚举文本 / 唯一校验 / 事务 / 分块查询 / 树
2) 依赖方向单向:只允许依赖 dao / utility / consts,禁止引用模块文件
3) 导出函数上限 15 个,超出按能力拆 internal/logic/base/ 子包
4) CheckUnique 仅为 UX 快速报错(TOCTOU),并发唯一性靠 uk_* 索引
5) [IMG_URL] 占位符是持久化契约,还原只能走 ExpandContentFields
能力清单:
SaveFileFields(ctx, data, fields, saveDir) 单文件字段批量转正(原地更新 map)
SaveMultiFileFields(ctx, data, fields, saveDir) 逗号多文件字段批量转正
SaveFileField(ctx, fileUrl, saveDir) 单值便捷版
SaveMultiFileField(ctx, fileUrls, saveDir) 逗号串便捷版
LocalizeContentFields(ctx, content, saveDir) 富文本存库本地化(占位符化)
ExpandContentFields(ctx, content) 富文本读取还原(唯一还原出口)
DictGetText(ctx, code, value) 字典值 → 显示名(进程内缓存 300s)
SerializeMapOnRead(ctx, data, fieldDict) map 行补 {field}Text
LogTypeText(v int) 操作日志类型 → 中文(静态表)
CheckUnique(ctx, m, column, value, excludeId, label) 唯一性快速校验
WithTrans(ctx, fn) 事务包裹(错误自动回滚)
ChunkQuery(ctx, build, size, fn) 分块遍历(导出防 OOM)
BuildTree[T](rows, idOf, pidOf, childrenOf, less) 泛型树构建
CleanTemp(ctx, expireDays) 清理上传临时日期目录
fillUserCount(column, list) 按外键统计引用数(内部,level/position 等复用)
注:ModuleSpec 基座(module_spec.go)在其上封装了 readTransform/saveTransform/
checkUniqueRules/FillTextFields,模块优先用基座声明,逃生舱才直接调 base 函数。
--------------------------------------------------------------------------------
2. 文件上传两段式(utility/upload + logic/upload.go)
--------------------------------------------------------------------------------
2.1 上传接口
POST /api/upload/uploadFile (perm sys:upload:file)multipart 字段名 file
POST /api/upload/uploadFiles (perm sys:upload:files)多文件
入参 type=image 时按图片规则校验,否则按普通文件。
2.2 SaveTemp(第一段:落临时目录)
校验顺序:
1) 大小 ≤ file.maxSize(MB,默认 10)
2) 扩展名白名单:file.fileExt(普通)/ file.imageExt(图片)
3) 读取前 512 字节:type=image 时做魔数嗅探(http.DetectContentType 须 image/*),
并据文件头嗅探 MIME(识别不出回退 application/octet-stream)
落盘:{file.uploadDir}/{file.tempDir}/{yyyymmdd}/{32位随机hex}.{ext}
返回 Result{Url(可直接预览的完整地址), Path(相对 uploadDir,入库形态),
Name, Size, Ext, Mime}。
2.3 SaveFromTemp(第二段:表单保存时转正)
入参为上传返回的 URL 或相对路径;处理规则:
* 空值 → ""
* 本站 URL(匹配 file.domainUrl 前缀)→ 剥离域名得相对路径
* 相对路径以 temp/ 开头 → 移动到 {saveDir}/{yyyymmdd}/{HHmmss}_{4字节hex}.{ext}
(os.Rename,跨卷回退“复制后删源”);返回新相对路径(入库形态)
* 已是正式相对路径 → 归一化后原样返回(幂等)
* 外站 http(s) URL → 原样返回(不处理外链)
SaveFromTempMulti:按逗号拆分逐个转正(如 article.images 多图字段)。
2.4 路径契约
数据库统一存“相对 uploadDir 的路径”(如 tenant/20260923/x.png);
对外访问地址由 GetFileUrl = file.domainUrl + "/" + 相对路径 生成
(domainUrl 为空时用 /upload/ 相对路径)。
静态访问:router SetServerRoot("resource") 提供 /upload/**;
Response 中间件对 .svg 强制 attachment + nosniff(防存储型 XSS)。
2.5 ModuleSpec 中的声明式用法
FileFields: []string{"avatar"} + FileSaveDir: "user"
写侧 saveTransform 自动 temp→正式;读侧 readTransform 自动 GetFileUrl 补域名。
MultiFileFields 处理逗号多文件;ContentFields 走富文本流程。
2.6 临时目录清理(运营闭环)
cmd.go 注册 gcron "0 30 3 * * *"(每日 03:30)→ logic.CleanTemp:
扫描 {uploadDir}/{tempDir} 下目录名为 yyyymmdd 且早于
now-file.tempExpireDays(默认 7 天,0=不清理)的日期目录整目录删除,
返回删除数量并记日志。
2.7 上传两段式流程图
阶段一(上传时,POST /api/upload/uploadFile):
浏览器 ──multipart──▶ SaveTemp
│ ① 大小 ≤ file.maxSize(MB)
│ ② 扩展名 ∈ file.fileExt / file.imageExt
│ ③ type=image:前512B 魔数嗅探须 image/*
▼
落盘 {uploadDir}/temp/{yyyymmdd}/{32位hex}.{ext}
▼
返回 {originalName, fileExtension, fileType, fileSize, fileName,
filePath:"temp/.../x.png"(入库形态), fileUrl:"{domainUrl}/temp/..."} → 前端预览
阶段二(表单保存时,spec.saveTransform / SaveFileField):
表单携带 temp URL ──▶ SaveFromTemp(url, saveDir="user")
│ 剥离 domainUrl → 相对路径
│ 以 temp/ 开头?──否──▶ 已正式/外站:归一化或原样返回
│ 是
▼
{uploadDir}/{saveDir}/{yyyymmdd}/{HHmmss}_{4字节hex}.{ext}(Rename,跨卷复制回退)
▼
库中写入相对路径 "user/20261017/153012_ab12.png"
▼
读侧 GetFileUrl:domainUrl + "/" + 相对路径 ──▶ 前端展示
生命周期:temp 文件由 CleanTemp(每日 03:30)按 tempExpireDays 清理;
未走阶段二的 temp 文件自然过期,不污染正式目录。
--------------------------------------------------------------------------------
3. 富文本 [IMG_URL] 持久化契约
--------------------------------------------------------------------------------
3.1 存库(LocalizeContentFields)
正文(如 article.content)入库前处理:
* 匹配范围:img/video/source/embed 的 src + a 的 href(带文件后缀)
* 本站 /temp/ 引用 → SaveFromTemp 转正后替换为 "[IMG_URL]/{相对路径}"
* 本站正式 /upload/ 引用 → 剥离访问基址(GetAccessBase)写 "[IMG_URL]{相对路径}"
* 外站 URL → 不处理(规避 SSRF/外链下载)
[IMG_URL] 是库内 content 字段格式的一部分:content 不供第三方系统直接消费。
3.2 读取(ExpandContentFields)
strings.ReplaceAll(content, "[IMG_URL]", GetAccessBase(ctx)),
是唯一还原出口,业务代码禁止手写字符串替换。
ModuleSpec 的 ContentFields 声明后读写两侧自动接入。
3.3 富文本转换流图
写侧 LocalizeContentFields(content, saveDir):
正文 HTML
│ 正则提取 img/video/source/embed[src] + a[href(带后缀)]
│ 逐 URL 判断:
│ 含 /temp/ → SaveFromTemp 转正 → "[IMG_URL]/user/.../x.png"
│ 本站正式路径 → 剥离 domainUrl → "[IMG_URL]/user/.../x.png"
│ 外站 http(s) → 跳过(不处理,规避 SSRF)
▼
库存 content(含 [IMG_URL] 占位符;此为持久化格式契约)
读侧 ExpandContentFields(content):
"[IMG_URL]" ──ReplaceAll──▶ domainUrl(访问基址)──▶ 前端富文本展示
唯一还原出口;ModuleSpec.ContentFields 声明后两侧自动接入。
--------------------------------------------------------------------------------
4. 事务(WithTrans)
--------------------------------------------------------------------------------
WithTrans(ctx, fn):g.DB().Begin → fn(gdb.WithTX(ctx, tx)) → 出错 Rollback,
否则 Commit。dao.Ctx(ctx) 在事务上下文内自动走事务连接。
纪律:事务内禁止 HTTP/文件 IO;bcrypt 等 CPU 密集操作在事务外预计算
(典型:租户开户 Account、用户 Add)。
--------------------------------------------------------------------------------
5. 分块查询(ChunkQuery)
--------------------------------------------------------------------------------
ChunkQuery(ctx, build, size, fn):build(page, size) 每页重建查询(gdb Model
不可复用执行),默认页大小 500;语义为快照——并发写入可能造成翻页漂移。
典型用途:用户导出(logic/user.go Export)分批读取 → 写 Excel,防大结果集 OOM。
--------------------------------------------------------------------------------
6. 树构建(BuildTree / ModuleSpec 树形)
--------------------------------------------------------------------------------
base.BuildTree[T]:泛型平铺转树,父节点不在集合内则挂根,可传排序函数。
ModuleSpec 路线:IsTree:true + ParentOf +(ChildSetter 或 DTO 的 Children 字段),
List 末尾自动建树,叶子 Children 预置空数组。DTO 必须含 Id 字段。
使用模块:menu / dept / category。
--------------------------------------------------------------------------------
7. Excel 导入导出(utility/excel)
--------------------------------------------------------------------------------
基于 xuri/excelize/v2,支持 xls/xlsx。
ReadSheet(file FileOpener):首行表头,全单元格转字符串(TrimSpace),
空行跳过;FileOpener 接口兼容 *ghttp.UploadFile 与 *multipart.FileHeader。
WriteSheet(dir, baseName, headers, rows [][]any):写文件返回 (fileName, filePath)。
--------------------------------------------------------------------------------
8. 其他 utility 能力
--------------------------------------------------------------------------------
utility/cache 统一缓存门面:Init 探测 Redis(失败/运行期出错自动降级 gcache
进程内存,UsingMemory() 可查);Set/Get/Del/Exists/SetNX。
注意:内存降级下多实例不共享(限流计数、任务锁退化为单机语义)。
utility/crypto GenerateSalt(8 字节随机 base64url 截 10 位)、
HashPassword/VerifyPassword(bcrypt(password+salt))、IsBcrypt。
utility/ua ParseOS / ParseBrowser(操作日志、登录日志、在线用户用)。
utility/ip2region xdb 懒加载(sync.Once)+ 读锁并发;City(ip) 优先市回退省,
内网/回环返回空;Location(ip) 供日志(失败显示 "-")。
路径配置 ip2region.path(默认 manifest/data/ip2region.xdb)。
utility/captcha 见 03 章。
utility/response 见 01 章。
--------------------------------------------------------------------------------
9. 常见问题排查
--------------------------------------------------------------------------------
上传图片 404:检查 file.domainUrl 与静态服务根(SetServerRoot("resource"))的
相对关系;库中存的是相对 uploadDir 路径,展示层必须经 GetFileUrl。
富文本图片不显示:确认读取侧走 Detail/List(基座自动 Expand),不要直接
Struct 输出;外链未本地化属预期(不处理外站 URL)。
temp 文件堆积:确认 CleanTemp 的 gcron 已注册(cmd.go),file.tempExpireDays>0。
归属地显示 "-":检查 ip2region.xdb 是否存在、api.proxyIps 是否包含反代地址
(否则日志 IP 是反代 IP)。
@@ -0,0 +1,142 @@
================================================================================
EasyGoAdmin 后端开发者文档 · 06 监控体系与在线用户
================================================================================
monitor 域共 7 个模块:operation_log、login_log、job、job_log(四张表)+
online_user、server_monitor、cache_monitor(三个无表模块)。
job/job_log 的调度机制见 09 章;本章聚焦监控与在线用户。
--------------------------------------------------------------------------------
1. 在线用户(logic/online_user.go,无表,数据源 Redis)
--------------------------------------------------------------------------------
1.1 数据结构(见 03 章第 10 节)
zset easygoadmin:online:jti:set score = 访问令牌过期时间戳
键 easygoadmin:online:info:{jti} JSON 详情(TTL = access 剩余期 + 60s)
onlineInfo 字段:jti/userId/username/realname/ip/location/oss/browser/
loginTime/issuedAt/expireAt/refreshJti/refreshIssuedAt/refreshExpireAt/
accessToken/refreshToken(令牌原文,仅详情查看返回)/tokenType/expiresIn
1.2 登记与注销
MarkOnline(登录成功、refresh 轮换后调用):写详情键 + ZAdd zset。
MarkOffline(登出、强退后调用):ZRem + Del。
过期令牌由 zset score 区间过滤自然淘汰(查询只取 score ≥ now 的成员)。
1.3 接口
GET /api/onlineUser/page perm sys:onlineUser:page
流程:ZRangeByScore(now..+∞) → 逐 jti 读详情 → username/ip 子串过滤
→ 按登录时间倒序 → 同用户多会话标注(SessionNo/Sessions 序号与总数)
→ 批量回填头像(按 userId 查 user.avatar,GetFileUrl 补域名,单用户只查一次)
→ 内存分页(NewPageData)。
GET /api/onlineUser/detail/:id perm sys:onlineUser:detail
按 jti 返回完整信息(含令牌原文)。
GET /api/onlineUser/forceLogout/:id perm sys:onlineUser:forceLogout
单会话强退:access jti 拉黑(TTL=剩余期+60s)+ refresh jti 一并拉黑
(防被踢会话经 refresh 换新令牌复活)+ MarkOffline。
GET /api/onlineUser/forceLogoutUser/:userId 同权限
踢出该用户全部设备:遍历 zset 匹配 userId,逐会话双令牌拉黑 + 注销。
1.4 无心跳/续期机制:会话有效期即 access 令牌有效期(2h);
前端 refresh 轮换会刷新在线登记(MarkOnline 覆盖写)。
1.5 在线会话生命周期图
登录/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 到期:无法再换新令牌
查询视图(Page)= zset(score≥now) × info 详情 × username/ip 过滤 × 内存分页
--------------------------------------------------------------------------------
2. 服务器监控(logic/server_monitor.go + server_monitor_windows/other.go)
--------------------------------------------------------------------------------
GET /api/server/info(登录即可,log "服务器监控-查询信息")
返回 ServerInfoRes:
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(挂载失败或数据被外部改动会出现差值)
--------------------------------------------------------------------------------
3. 缓存监控(logic/cache_monitor.go,无表)
--------------------------------------------------------------------------------
GET /api/cache/info(登录即可,log "缓存监控-查询信息")
内存降级(cache.UsingMemory())→ {mode:"memory", connected:false},仅返回模式标识。
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 返回已解析部分。
--------------------------------------------------------------------------------
4. 操作日志(表 goframe_operation_log,采集见 02 章)
--------------------------------------------------------------------------------
4.1 写入:中间件异步批量落库(LogSink),记录 Title/Type/Method/URL/Param(脱敏)/
Result(截断 8KB)/IP/Location/OS/Browser/ConsumeTime/Status/Error/CreateUser。
4.2 查询模块(/api/oper/log,仅 page/list/detail/delete/batchDelete,无增改):
perm sys:operLog:{page|list|detail|delete|batchDelete}。
4.3 log_type 固定枚举(consts/log_type.go,前端按此映射显示,数值勿改):
0=自动推断 1=新增 2=修改 3=删除 4=查询 5=导入 6=导出
7=审批 8=驳回 9=提交 10=撤回 11=关闭 12=开启 13=启用 14=禁用
15=上传 16=下载 17=发送 18=同步 19=代码生成 20=清空 21=还原
22=登录 23=登出 24=重置 25=分配 26=授权 27=执行 28=暂停 29=恢复
30=发布 31=下架 32=复制 33=移动 34=排序 35=锁定 36=解锁
37=归档 38=取消归档 39=绑定 40=解绑 99=其他
gmeta logType 字符串 → 枚举经 LogTypeMap;展示中文经 LogTypeNames
(logic 的 LogTypeText,该字段不走字典)。
4.4 清理:内置 LogClean 定时任务物理删除超保留期记录(见 09 章)。
--------------------------------------------------------------------------------
5. 登录日志(表 goframe_login_log,业务写入)
--------------------------------------------------------------------------------
写入方:logic/auth.go writeLoginLog——登录成功/失败(log_type=1,
失败 status=1 存 error,code 脱敏 ***)、退出(log_type=2)。
记录字段:username/log_type/source(0 后台)/method/request_method/url/param/
result(成功|失败)/ip/location(ip2region)/os/browser/consume_time/status/error。
查询模块:/api/login/log(page/list/detail/delete/batchDelete),
perm sys:loginLog:*;同样受 LogClean 清理。
--------------------------------------------------------------------------------
6. 任务日志(表 goframe_job_log)
--------------------------------------------------------------------------------
写入方:调度器 executeJob 每次执行后写入(create_user="daemon"):
job_id/job_name/job_group/job_trigger/job_message/cron_expression/status
(0 成功 1 失败)/start_time/end_time/consume_time(毫秒)/exception_info
查询模块:/api/job/log(page/list/detail/delete/batchDelete),perm sys:jobLog:*。
--------------------------------------------------------------------------------
7. 监控相关配置与运维入口
--------------------------------------------------------------------------------
operationLog.enable / operationLog.logQuery 操作日志开关(见 02 章)
job.logRetentionDays(默认 90) 三张日志表保留天数(LogClean 消费)
ip2region.path 归属地库路径
api.proxyIps 受信代理(影响 IP 与归属地准确性)
运维排障速查:
调度器异常 GET /api/job/daemonStatus(running/jobIds/startTime)
GET /api/server/info 的 Scheduler 字段(mounted vs expected)
缓存降级 GET /api/cache/info mode=memory(Redis 不可用,限流/锁退化单机)
在线人数 GET /api/onlineUser/page total
日志缺失 检查 operationLog.enable、通道满丢弃告警日志、LogSink 是否注册
+212
View File
@@ -0,0 +1,212 @@
================================================================================
EasyGoAdmin 后端开发者文档 · 07 代码生成器(tool 域 + CLI)
================================================================================
代码位置:internal/logic/generator.go(核心)、internal/logic/generator_side.go
(副作用:菜单/字典/路由)、cmd/generator/main.go(CLI)、templates/(模板)
--------------------------------------------------------------------------------
1. 双入口
--------------------------------------------------------------------------------
HTTP(/api/generator,管理页面“代码生成”):
GET /api/generator/page perm sys:generator:page 数据表清单(分页)
POST /api/generator/preview perm sys:generator:generator 预览(不落盘)
POST /api/generator/generate perm sys:generator:generator 一键生成
POST /api/generator/batchGenerate perm sys:generator:batchGenerator 批量生成
日志 logType=generate(19 代码生成)
CLI(仓库根目录执行,读取同一份 config.yaml):
go run ./cmd/generator -tables "goframe_order|订单表,goframe_sku|商品规格"
go run ./cmd/generator -tables "goframe_order|订单表" -preview # 仅预览清单
go run ./cmd/generator -tables "goframe_order|订单表" -batch # 逐表统计输出
-tables 格式:"表名|表注释"(注释可省略,|后内容被忽略),逗号分隔多表。
CLI 空白 import internal/logic 以注册全部服务,直接调用
service.Generator().Preview/Generate/BatchGenerate。
生成后手动执行 go build ./... 并重启服务。
--------------------------------------------------------------------------------
2. 表结构内省
--------------------------------------------------------------------------------
MySQL:查 information_schema.TABLES(表注释/引擎/行数/自增/排序规则/创建时间)
非 MySQL:gdb 元数据降级(仅表名,注释留空,模块名回退表名)。
字段元数据:g.DB().TableFields(tableName),按元数据 Index 还原物理列序。
类型映射(buildField,正则按 MySQL 类型串推断):
tinyint(1)→BooleanField int 族→IntegerField decimal/numeric→DecimalField
float/double→FloatField date→DateField datetime/timestamp→DateTimeField
blob/binary→BinaryField text 族→TextField varchar/char/enum→CharField
Go 类型:bigint→int64,decimal/float→float64,时间列→*gtime.Time,
int 族→int,其余 string;行取值方法 .Int()/.Float64()/.String()。
注释智能解析:
枚举:"状态:1-正常 2-禁用"(reChoice 支持 - — - = 分隔)→ Choices 集合,
标签类型 1→success 2→danger 3→warning 其余 info。
表单标签:cleanLabel 去掉枚举尾巴保留中文名。
图片字段:注释含 图片/头像/封面 或列名含 avatar/cover/image/icon/photo。
富文本:注释含 富文本 或 TextField 且列名含 content/body/editor。
字段集合划分(buildVars):
审计列排除:id 进列表(树形/分页需要 Id);create_user/create_time 仅标记
HasCreateUser/HasCreateTime;update_user/update_time/del_flag/tenant_id 不进
前后端字段;salt 不进表单。
列表字段:排除富文本/password/salt/BinaryField。
表单字段:全部业务列(BinaryField 除外)。
筛选字段:CharField 非图片非 URL 列(website/site/url/link 不作模糊条件);
带枚举的 Integer/Boolean → Select;*_id 外键 Integer → InputNumber。
显示字段(模糊搜索列)优先级:name/title/label/username/realname/nickname/
category_name/job_name/dept_name/role_name/position_name/level_name,
未命中取列表首列。
模块命名:moduleName = 表名去 goframe_ 前缀;Pascal → 结构体类名(ConfigItem);
camelLower → 前端模块名(configItem);权限前缀 sys:{camelLower};
路由前缀 = moduleName(snake,如 /api/config_item 实际以生成模板为准)。
域归属:genDomainMap(user/role/…→system,config/…→data,article/…→content,
operation_log/…→monitor,example/…→tool),未匹配默认 tool。
模板类型:有 parent_id/pid 列 → ui2(树形),否则 ui(普通列表)。
排序:有 sort 列 → "sort asc",否则 "id desc"。
--------------------------------------------------------------------------------
3. 产出文件(renderTable)
--------------------------------------------------------------------------------
后端八件套(Go 产物统一 gofmt,format.Source 失败即报错):
api/v1/{module}.go ← templates/api.tpl
internal/controller/{module}.go ← templates/controller.tpl
internal/logic/{module}.go ← templates/logic.tpl
internal/service/{module}.go ← templates/service.tpl
internal/dao/{module}.go ← templates/dao_index.tpl(挂钩子+Ctx)
internal/dao/internal/{module}.go ← templates/dao_internal.tpl
internal/model/entity/{module}.go ← templates/entity.tpl
internal/model/do/{module}.go ← templates/do.tpl
前端(模板定界符 [[ ]],规避 Vue 插值冲突):
ui 普通:src/api/{domain}/{moduleCamel}.ts、
src/views/{domain}/{moduleCamel}/{index.vue, edit.vue, detail.vue,
columns.ts, querySchemas.ts}
ui2 树形:src/api/{domain}/{moduleCamel}.ts、
src/views/{domain}/{moduleCamel}/{index.vue, edit.vue, detail.vue}
模板语法:Go text/template;后端模板默认 {{ }};templates/ui* 用 Delims("[[","]])。
模板函数:containsAny、isUrlField(URL 列名判定)。
--------------------------------------------------------------------------------
4. 落盘保护策略
--------------------------------------------------------------------------------
skipTables(整表跳过文件生成):仅关联表 role_menu、user_role——
跳过文件写入,但幂等副作用(菜单/字典/路由)照常执行。
writeFile 文件粒度四态:
fileWritten 目标不存在 → 新写入(计入 Generated)
fileExists 已存在且未开覆盖 → 跳过(计入 Skipped,保留手工代码)
fileOverwritten 已存在且开启覆盖 → 覆盖(计入 Overwritten)
fileFailed 写入/建目录失败(计入 Failed)
覆盖开关唯一入口:generator.overwrite(config.yaml;true=生成即覆盖,
false/未配置=缺失补生成、存在跳过)。
4.1 writeFile 落盘决策图
目标文件存在?
/ \
否 是
│ │
写入 generator.overwrite?
Generated / \
true false/缺省
│ │
覆盖写 跳过保留
Overwritten Skipped
\ /
任一失败 → Failed(MkdirAll/WriteFile 错误)
4.2 生成器总体流水线图
输入 tables[]("表名|注释")
│
▼
buildVars:TableFields 内省 → 类型/注释解析 → 字段分桶(列表/表单/筛选)
│ 命名:module/Pascal/camelLower;域归属;ui|ui2 模板类型
▼
skipTables? ──是(role_menu/user_role)──▶ 仅 buildVars(副作用用),跳过文件
│ 否
▼
renderTable:逐模板渲染(后端 {{ }} / 前端 [[ ]])→ go 产物 gofmt
│
▼
writeFile×N(决策图见 4.1)
│
▼
副作用(幂等):createMenu(+权限节点) → createDict(枚举字段) → registerRouter
│
▼
结果报告(Generated/Skipped/Overwritten/Failed + Menu/Dict/Route)
│
▼
提示:go build ./... 并重启服务
--------------------------------------------------------------------------------
5. 幂等副作用(generator_side.go)
--------------------------------------------------------------------------------
5.1 菜单 + 权限节点(createMenu)
主菜单:parent_id = generator.parentMenuId(默认 189),
path="/{domain}/{moduleCamel}",component=path+"/index",
type=0,sort=同级 MAX(sort)+5,note="代码生成器自动创建"。
按 path+type=0 查重 → 已存在直接返回(幂等)。
权限节点(type=1,挂在主菜单下,permission={PermissionPrefix}:{action}):
page/list/detail/add/update/delete/batchDelete 固定 7 个;
HasStatus 追加 :status;HasImport/HasExport 追加 :import/:export
(生成器默认二者均为 true)。sort 逐个 +5。
5.2 字典(createDict)
对每个带枚举注释的表单字段:code = {moduleCamel}_{camelField}
(如 user_status),按 code 查重 → 已存在跳过;
否则建 dict(name="{模块中文名}-{字段中文名}",note 自动创建标注)+
逐 Choice 建 dict_item(name=label,value,sort+5)。
结果逐项返回(已存在/创建成功/失败原因)。
5.3 路由注册(registerRouter)
目标文件 internal/router/router.go:
已含 new(controller.C{ModelClass}) → 返回“路由已存在”(幂等);
在最后一个 "\n\tBind(s, new(" 行之后插入
"\n\tBind(s, new(controller.C{ModelClass})) // 代码生成器注册"。
失败情形(返回消息,不中断):路由文件不存在、缺 controller 导入、
未找到锚点。
6. 生成器模板(templates/ 磁盘)与 file_template 表的区分(易混淆)
两套“模板”互不相干:
* templates/*.tpl(磁盘目录):代码生成器的渲染模板,随仓库版本管理,
generator.go 直接读取渲染;修改模板即改变生成产物风格。
* goframe_file_template 表 + /api/file/template 模块:业务功能(在线管理
文件/代码片段模板),提供 getTemplateByCode 查询;与代码生成器流水线
无调用关系。
修改生成产物改 templates/*.tpl;不要到 file_template 表里找生成器模板。
--------------------------------------------------------------------------------
7. 结果报告结构
--------------------------------------------------------------------------------
Generate(一键):Generated[] / Skipped[] / Overwritten[] / Failed[] /
Menu{Success,MenuId,Message} / Dict[]{Dict,Name,Items,Status} /
Route{Success,Message}
BatchGenerate:List[]{Table, Success, Generated, Skipped, Overwritten, Failed,
Message, Menu, Dict, Route}(逐表统计;Message="生成 N,跳过 N,覆盖 N,失败 N")
Preview:List[]{Path, Content}(渲染结果全文,不落盘)。
CLI 输出按上述分组打印(+ 生成 / - 跳过 / * 覆盖 / ! 失败),并提示
“下一步: go build ./... 并重启服务”。
--------------------------------------------------------------------------------
8. 与 gf gen dao 的分工
--------------------------------------------------------------------------------
gf gen dao -c hack/config.yaml 表结构变更后刷新已有模块 dao
(自定义模板 hack/tpl/dao_index.tpl:挂全局钩子 + Ctx 覆写;
overwriteDao:false 保留手工扩展)
生成器 新模块一键产出(dao 四件与 gf gen dao 同构,
生成后统一 gofmt,可直接编译)
gf gen service 禁止整体运行(service 接口为手工重组维护)
--------------------------------------------------------------------------------
9. 注意事项
--------------------------------------------------------------------------------
* generator.overwrite=true 会覆盖同名文件(包括手工修改过的)——批量覆盖前务必备份
或临时改回 false。
* 生成的 logic 是 ModuleSpec 基座风格的完整可用实现(含筛选/树形/字典/导入导出),
复杂业务需再手工扩展钩子。
* 新模块若为租户级表,需同步把表名加入 internal/model/hooks 的 tenantTables。
* 菜单创建后需在角色管理中为相关角色勾选权限节点方可访问。
* 非 MySQL 库生成时注释/枚举解析缺失,建议在 MySQL 上生成或人工补注释。
+207
View File
@@ -0,0 +1,207 @@
================================================================================
EasyGoAdmin 后端开发者文档 · 08 数据库迁移(整库迁移器)
================================================================================
代码位置:internal/migrate/{migrate,model,introspect,translate,ddl,copy,conn}.go
入口:cmd/migrator/main.go(纯 CLI,也可 go build 后用 migrator.exe)
--------------------------------------------------------------------------------
1. 定位与原则
--------------------------------------------------------------------------------
源库内省(表/列/索引/注释)→ 中间表示(IR)→ 目标方言 DDL → 分批搬运数据
→ 自增序列修复 → 逐表行数校验。
关键原则:
* 独立于业务基座:不依赖 dao/logic,gdb.New 动态连接源库与目标库,
裸读裸写,全程不触发全局钩子(租户隔离/软删/审计盖章),数据原样落库。
* 五方言目标渲染器:mysql / pgsql / sqlite / mssql / oracle。
* 无外键迁移(0 个外键),只迁表/列/二级索引/注释/数据。
* 首期实测链路 mysql→pgsql;mssql/oracle 按定稿方言约定实现,待人工校对
(resource/sql 后四份方言 DDL 同样待人工校对)。
--------------------------------------------------------------------------------
2. CLI 参数(cmd/migrator/main.go)
--------------------------------------------------------------------------------
源库(6 个):
-src-type mysql/pgsql/sqlite/mssql/oracle(默认 mysql)
-src-host 默认 127.0.0.1
-src-hostport 默认 3306
-src-database 库名(必填;sqlite 填数据库文件路径)
-src-username 默认 root
-src-password
目标库(6 个):
-dst-type 默认 pgsql
-dst-host 默认 127.0.0.1
-dst-hostport 默认 5432
-dst-database 库名(必填;sqlite 填文件路径,不存在时自动创建)
-dst-username 默认 postgres
-dst-password
迁移行为(3 个):
-drop-target-first 迁移前清空目标库全部表(不加则目标已有同名表时该表报错跳过)
-tables 只迁指定表(逗号分隔白名单,留空迁全部)
-batch-size 数据搬运批大小(默认 1000;Options 注释默认 500,CLI 传 1000)
示例(document/数据迁移命令.txt 有完整版):
go run ./cmd/migrator \
-src-type=mysql -src-host=127.0.0.1 -src-hostport=3306 \
-src-database=easygoadmin.goframe.elevue -src-username=root -src-password=root \
-dst-type=pgsql -dst-host=127.0.0.1 -dst-hostport=5432 \
-dst-database=easygoadmin.goframe.elevue -dst-username=postgres -dst-password=123456 \
-drop-target-first
--------------------------------------------------------------------------------
3. 主流程五阶段(migrate.Run)
--------------------------------------------------------------------------------
[1/5] 建目标库(ensureDatabase):
mysql(维护库空串→information_schema 检查,CREATE DATABASE utf8mb4)、
pgsql(维护库 postgres,CREATE DATABASE)、mssql(维护库 master)、
oracle/sqlite 无需预建。
[2/5] 建表结构:
逐表内省(introspectTable)→ 非清空模式下目标已有同名表则该表失败跳过
(建表语句为 IF NOT EXISTS,数据会追加,必须前置拦截)→ CreateTable →
逐个 CreateIndex。统计表数/索引数。
[3/5] 逐表迁移数据(copyTable,见第 5 节),打印每批进度。
[4/5] 重置自增序列(FixSequence):pgsql setval 到 MAX(id)+1;
mssql DBCC CHECKIDENT RESEED;mysql/sqlite/oracle 自动续接无需处理。
[5/5] 逐表行数校验:源/目标 COUNT 对比,输出 PASS/FAIL。
汇总 Report{TableTotal, TableOK, TableFailed, RowTotal, Failed[]};
存在失败表或行数不一致 → 返回错误、退出码 1。
3.1 五阶段流程图
┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐
│ 1/5 建目标库│──▶│ 2/5 建表结构│──▶│ 3/5 搬运数据│──▶│ 4/5 序列修复│──▶│ 5/5 行数校验│
│ ensureDB │ │ 内省→建表 │ │ 分批 INSERT│ │ setval等 │ │ COUNT 对比 │
│ (不存在才建)│ │ →建索引 │ │ 进度回调 │ │ │ │ PASS/FAIL │
└────────────┘ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘
drop-first: 无主键表报错跳过 pgsql/mssql 专项 不一致→
先 DropAllTables 其余方言免修 整体失败
非清空: 同名表拦截跳过
3.2 数据搬运循环图(doCopy,主键游标分页)
lastPK=nil
│
▼
┌──────────────────────────────────────────┐
│ SELECT 列集 FROM src.table │
│ WHERE pk > lastPK ORDER BY pk LIMIT N │◀──────┐
└──────────────┬───────────────────────────┘ │
行数=0?─ 是 ─▶ 返回 total │
否 │
▼ │
batch=[{列:值(字节转字符串)}…],lastPK=末行 pk ────┘
│
▼
目标写入:通用方言 → 参数化多行 INSERT
mssql → 内联字面量 INSERT(自增列:事务内 IDENTITY_INSERT ON/OFF)
│
▼
total += len(batch);onBatch 进度回调;len(batch)<N → 结束
--------------------------------------------------------------------------------
4. 源库内省与中间表示
--------------------------------------------------------------------------------
表清单:db.Tables + 白名单过滤 + 按名排序(结果可复现)。
列:TableFields 按 Index 还原物理列序;类型经 normalizeType 归一为中间类型
(20 种:bool/tinyint/smallint/int/bigint/float/double/decimal/varchar/char/
text/tinytext/mediumtext/longtext/date/time/datetime/blob/json/enum/set/other),
同时解析长度/精度/枚举值;剥离 unsigned 与 pgsql 时区修饰;
tinyint(1) 惯例归为 bool;uuid→char(36)。
默认值 cleanDefault:pgsql 剥 `'x'::character varying` 类型转换、
current_timestamp() 等函数归一;其余方言原样。
自增识别 isAutoInc:extra 含 auto_increment 或 identity。
索引:MySQL SHOW INDEX / pgsql pg_indexes(解析 indexdef 括号内列清单,
跳过表达式索引部分);主键并入列元数据 IsPK,其余为二级索引;
其余三方言二级索引降级跳过。
索引命名统一 normalizeIndexName:唯一 uk_ / 普通 idx_ + 表名 + 列名,
超 63 字符截断(迁移后全库一种命名规则)。
IR 结构(model.go):
TableMeta{Name, Comment, Columns[]ColumnMeta, Indexes[]IndexMeta}
ColumnMeta{Name, Type(中间类型), Length, Scale, EnumVals, Nullable, Default,
AutoInc, Comment, IsPK}
IndexMeta{Unique, Name, Columns}
--------------------------------------------------------------------------------
5. 数据搬运(copy.go)
--------------------------------------------------------------------------------
无主键表直接报错(“请先补主键”)——分页搬运依赖主键排序。
核心循环 doCopy:
源端按主键排序快照分页(WHERE pk > lastPK ORDER BY pk LIMIT batchSize),
目标端多行 INSERT;每批回调进度;字节切片值转字符串(convertValue)。
方言差异:
通用方言(mysql/pgsql/sqlite/oracle):gdb 参数化多行 Insert
(dst.Model(table).Data(batch).Insert())。
mssql 专项:内联字面量 INSERT(buildInsertSQL + inlineValue)绕过 2100
参数占位符上限;含自增列时每批独立小事务 SET IDENTITY_INSERT ON → INSERT
→ OFF → COMMIT(事务粒度=单批,避免整表单事务撑爆事务日志,同时钉住连接)。
inlineValue:nil→NULL,字符串/字节转义单引号,数值原样,bool→1/0,
时间格式 "2006-01-02 15:04:05"。
源端标识符引用 quoteAll 按源方言(mysql 反引号 / mssql 方括号 / 其余双引号)。
--------------------------------------------------------------------------------
6. 目标方言 DDL 渲染(ddl.go)
--------------------------------------------------------------------------------
Renderer 接口:Quote / Column / CreateTable / CreateIndex / DropAllTables /
FixSequence;NewRenderer(dialect) 工厂。
引号:mysql 反引号;pgsql/sqlite/oracle 双引号;mssql 方括号。
默认值 defaultLiteral:时间函数按方言归一(mssql→GETDATE(),oracle→SYSDATE,
其余→CURRENT_TIMESTAMP);数值原样;NULL 保留;其余加引号转义。
各方言要点:
mysql AUTO_INCREMENT + NOT NULL;列/表注释 COMMENT 内联;
DropAllTables 先 SET FOREIGN_KEY_CHECKS=0;FixSequence 免责。
pgsql GENERATED BY DEFAULT AS IDENTITY;表/列注释 COMMENT ON 独立语句;
索引 CREATE [UNIQUE] INDEX IF NOT EXISTS;
DropAllTables CASCADE;类型映射:int→INTEGER、datetime→TIMESTAMP、
blob→BYTEA、json→JSONB、enum/set→VARCHAR(255)(pgsql 无内建枚举,
值域由应用层保证);
FixSequence:SELECT setval(pg_get_serial_sequence(...), MAX(id)+1, false)。
sqlite INTEGER PRIMARY KEY AUTOINCREMENT(单列主键列级;复合主键表级约束);
类型极简:整数族/时间→INTEGER,浮点小数→REAL,blob→BLOB,其余 TEXT;
无表/列注释;DropAllTables 关 PRAGMA foreign_keys 再删;免修序。
mssql IDENTITY(1,1);类型走 NVARCHAR/NCHAR(Unicode)、datetime→DATETIME2、
blob→VARBINARY(MAX)、text/json/enum→NVARCHAR(MAX);
建表 IF OBJECT_ID(...) IS NULL CREATE TABLE;注释走扩展属性
sp_addextendedproperty MS_Description;
索引 IF NOT EXISTS(sys.indexes) 保护;FixSequence DBCC CHECKIDENT RESEED。
oracle IDENTITY;NUMBER 族(int→NUMBER(10)、bigint→NUMBER(19))、
NVARCHAR2/NCLOB/BLOB、时间→DATE/TIMESTAMP;
注释 COMMENT ON;无 IF NOT EXISTS(由编排层预检索引存在性,当前
实现直接执行);IDENTITY 缓存自动续接,FixSequence 免责。
--------------------------------------------------------------------------------
7. 连接管理(conn.go)
--------------------------------------------------------------------------------
openConn:gdb.New(gdb.ConfigNode{Name:"migrate_src|migrate_dst", Type, Link})
注册独立命名节点,不触碰 config.yaml 的默认连接;Query "SELECT 1" 探活。
buildLink 按方言组装(mysql 带 charset/parseTime/loc,pgsql sslmode=disable,
mssql encrypt=disable,oracle 简洁串,sqlite sqlite::@file(路径))。
--------------------------------------------------------------------------------
8. 与 resource/sql 的关系
--------------------------------------------------------------------------------
resource/sql/{mysql,pgsql,sqlite,mssql,oracle}.sql 为“定稿 DDL”
(mysql 为规范基准,人工导入维护);迁移器是“运行期动态等价物”。
两者独立演进:新表/新索引需两边同步(DDL 脚本手工维护;迁移器自动跟随
源库结构)。document/*.sql 与 resource/sql 之间存在已知 DDL 漂移
(如部分 uk_code 唯一索引缺失),以 resource/sql 定稿为准。
--------------------------------------------------------------------------------
9. 实测记录与已知限制
--------------------------------------------------------------------------------
document/ 下保留实测日志:数据迁移PostgreSQL版本日志.txt、
数据迁移SqlServer版本日志.txt、数据迁移Sqlite版本日志.txt、
数据迁移命令.txt(参数说明与示例)。
根目录 migrator.exe 为编译产物。
限制清单:
* 无外键/视图/存储过程/触发器迁移
* 无主键表拒绝搬运
* mssql/oracle 源端索引内省未实现(作为源库时二级索引会丢)
* 逐表串行搬运(无并发),超大库耗时线性增长
* -drop-target-first 清空的是目标库“全部表”(含非本项目表),慎用
* Oracle 方言差异(如自增回读 LastInsertId)由后续人工处理
+183
View File
@@ -0,0 +1,183 @@
================================================================================
EasyGoAdmin 后端开发者文档 · 09 定时任务调度
================================================================================
代码位置:internal/logic/daemon.go(调度器)、internal/logic/job_handler.go
(本地处理器注册表)、internal/logic/job.go(/api/job 业务)、
internal/logic/job_log.go(任务日志)、internal/task/(任务实现)
表:goframe_job(任务定义)、goframe_job_log(执行日志)
--------------------------------------------------------------------------------
1. 架构总览
--------------------------------------------------------------------------------
数据驱动的常驻调度器:
启动(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 注册。
--------------------------------------------------------------------------------
2. 任务执行目标双形态(executeJob)
--------------------------------------------------------------------------------
读取任务行的 url 字段分派:
http:// 或 https:// 开头 → HTTP 调用目标地址:
params 为空 GET,非空 POST(Content-Type application/json,超时 30s);
非 200 或网络错误 → 失败(exception 记录 HTTP 状态与响应前 1000 字符)。
其余取值 → 视为本地处理方法名,从 jobHandlers 注册表查找:
签名 func(ctx context.Context, params string) (string, error)
params 为任务参数(JSON 字符串,可为空);返回摘要写入任务日志 message。
未注册 → 失败“未找到本地任务处理方法”。
执行防护:
* recover() 捕获 panic(不外泄影响调度器)
* force=false 时校验 status=1,否则从调度器摘除(数据被外部改动的自愈)
* 任务行不存在 → 摘除
--------------------------------------------------------------------------------
3. 执行策略与执行方式
--------------------------------------------------------------------------------
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 分钟)。
内置清理任务使用 6 段:"0 30 3 * * *" = 每日 03:30。
--------------------------------------------------------------------------------
4. 本地处理器注册表(job_handler.go + internal/task)
--------------------------------------------------------------------------------
logic.RegisterJobHandler(name, h):注册名全局唯一,重复注册进程直接 panic
(暴露配置错误)。注册名与任务记录 url 字段值一一对应(数据驱动查找)。
internal/task 包约定:
每任务一个文件(snake_case),init 中注册;
处理方法签名 func(ctx, params) (string, error),返回值写任务日志摘要;
不在 HTTP 启动链路内,由 internal/cmd 空白导入触发加载。
内置任务清单:
HelloWorld(hello_world.go) 示例任务,仅打日志,用于验证调度链路
SendSms(send_sms.go) 仅记录执行参数日志,不对接短信通道
LogClean(log_clean.go) 日志清理(见第 6 节)
--------------------------------------------------------------------------------
5. /api/job 管理接口(logic/job.go)
--------------------------------------------------------------------------------
七件套(page/list/detail/add/update/delete/batchDelete,perm sys:job:*)
+ 控制接口:
GET /api/job/runOnce/:id sys:job:execute 立即执行一次(异步
go executeJob(force=true),未发布任务也可执行)
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=gcron 条目数、jobIds、startTime)
业务钩子(ModuleSpec 声明):
BeforeAdd 新增固定 status=0(未发布;发布走 Status/Resume)
AfterUpdate 运行中任务先摘除再按新配置重挂;挂载失败降级 status=2 并报
“Cron表达式无效”
BeforeDelete 存在性检查 + 摘除调度
status 语义:0 未发布 / 1 运行中 / 2 暂停。
枚举字典:job_status、job_execute_policy、job_is_sync(SerializeMaps 驱动
{field}Text 显示)。
5.1 调度执行流程图
启动(cmd, 延时2s) ─▶ DaemonInit(sync.Once)
│ SELECT * FROM goframe_job WHERE status=1
▼
逐条 addJobToScheduler(id, cron, policy, isSync)
│ policy=1 → gcron.Add (可并发,无锁)
│ policy≠1 → gcron.AddSingleton (单例)+ 执行锁
▼
tick 触发 ─▶ is_sync=1? ──是──▶ 同步执行 run()
│否
▼ go run(context.Background())
run(): policy≠1 → jobTryLock SET NX job:lock:{id}
│ 抢锁失败 → 放弃本次(Debug 日志)
▼ 抢到(defer 释放)
executeJob(id, force=false)
├─ 任务不存在/status≠1 → 摘除调度,返回
├─ url=http(s) → gclient GET/POST(30s 超时,params 为 body)
├─ url=方法名 → jobHandlers 查找执行(task 包注册)
└─ recover panic → 写日志
dao.JobLog.Insert(status/message/耗时/异常,create_user=daemon)
5.2 任务状态机
Add(固定置0)
│
▼
┌─────────┐ Status(1) / Resume ┌─────────┐
│ 0 未发布 │─────────────────────▶│ 1 运行中 │◀──┐
└─────────┘ └────┬────┘ │
▲ Status(2)/Pause │ │ Resume
│ ─────────────────────▶│ │
│ 摘除调度 ▼ │
│ ┌─────────┐ │
│ Status(0) │ 2 暂停 │───┘
│◀─────────────────────────── └─────────┘
│ 摘除调度
Delete:软删(del_flag=1) + 摘除调度
运行中 Update:摘除 → 按新 cron 重挂(失败降级置 2 并报 Cron 无效)
RunOnce:不看状态,异步 force 执行一次(不改状态)
--------------------------------------------------------------------------------
6. 内置任务 LogClean(task/log_clean.go)
--------------------------------------------------------------------------------
注册名 "LogClean";每日 02:30 由任务表配置驱动(内置任务数据在初始化菜单/
字典数据中配置;url 字段值 = LogClean)。
参数:{"retentionDays": n} 可覆盖配置 job.logRetentionDays(默认 90),
参数为 0/缺失取配置值,配置也无效则回退 90。
行为:分批物理删除三张日志表(goframe_operation_log / login_log / job_log)
中 create_time 早于 now-retentionDays 的记录:
单批 5000 行(logCleanBatch),循环至不足一批;
单表单次任务上限 200000 行(logCleanMaxTotal,防长事务与 runaway)。
摘要返回各表清理条数,写入任务日志 message。
--------------------------------------------------------------------------------
7. 与进程内 gcron 的其他条目
--------------------------------------------------------------------------------
调度器与内置清理共用 gcron,但命名空间隔离:
业务任务条目键 job:{id}(server/info 的 Scheduler.mounted 只统计 "job:" 前缀)
上传清理条目 upload_temp_clean(cmd.go 注册,每日 03:30 logic.CleanTemp,
天数取 file.tempExpireDays)
DaemonStatus 的 running 为 gcron 全部条目数(含非任务条目),
Scheduler.mounted 按 "job:" 前缀过滤——两处口径不同,排障时注意。
--------------------------------------------------------------------------------
8. 开发一个新定时任务的步骤
--------------------------------------------------------------------------------
1) internal/task/{任务名}.go 实现 handler:func(ctx, params) (string, error)
2) init() 中 logic.RegisterJobHandler("TaskName", handler)
(注册名=任务表 url 字段值;http(s) 地址则无需本地处理器)
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 超时,长任务应改为本地处理器。
--------------------------------------------------------------------------------
9. 多实例部署语义
--------------------------------------------------------------------------------
单实例:gcron 各自挂载,正常运行。
多实例 + Redis:策略 2/3 经 job:lock:{id} 全局互斥(同一时刻至多一个实例
在执行该任务);仍会重复“触发”(各实例各自 tick),但未抢到锁的实例直接放弃。
多实例 + 内存降级:锁退化为单机语义,可能出现跨实例并发执行——生产环境
Redis 必须可用。
+194
View File
@@ -0,0 +1,194 @@
================================================================================
EasyGoAdmin 后端开发者文档 · 10 配置参考与部署
================================================================================
配置文件:manifest/config/config.yaml(唯一运行时配置)
环境变量覆盖:GF_GENV_ 前缀 + 下划线路径,如 GF_GENV_DATABASE_DEFAULT_LINK
--------------------------------------------------------------------------------
1. server 服务端
--------------------------------------------------------------------------------
address: ":8000" 监听地址
openapiPath: "/api.json" OpenAPI 文档(gf 原生生成,bind.go 补注册闭包路由)
swaggerPath: "" Swagger UI 路径置空——内置页依赖境外 CDN,
改为本地托管 resource/swagger/(/swagger 入口)
dumpRouterMap: false 启动打印路由表
errorStack: true 响应携带错误堆栈(生产可关)
graceful: true 优雅重启
--------------------------------------------------------------------------------
2. logger 日志
--------------------------------------------------------------------------------
level: "all" stdout: true path: "log"
--------------------------------------------------------------------------------
3. database 数据库(五方言可切换)
--------------------------------------------------------------------------------
default:
type: "mysql" 运行时驱动:mysql/pgsql/mssql/oracle/sqlite
link: "mysql:root:root@tcp(127.0.0.1:3306)/easygoadmin.goframe.elevue?charset=utf8mb4&parseTime=true&loc=Local"
debug: false SQL 调试日志(调试期开,生产关)
其他方言示例(config 内注释):
pgsql: "pgsql:user:password@tcp(127.0.0.1:5432)/easygoadmin?sslmode=disable"
mssql: "mssql:sa:Password1@tcp(127.0.0.1:1433)/easygoadmin?encrypt=disable"
oracle: "oracle:user:password@tcp(127.0.0.1:1521)/XE"
sqlite: "sqlite::@file(easygoadmin.db)"
切换只需改 type+link(五驱动已在 main.go 全部注册)。
注意库名含点(easygoadmin.goframe.elevue),DDL 引用需反引号。
--------------------------------------------------------------------------------
4. redis 缓存
--------------------------------------------------------------------------------
default: address 127.0.0.1:6379 db 0 pass "" idle 10 maxActive 50 timeout 300
用途:JWT 黑名单、验证码、字典缓存、身份/租户摘要缓存、限流计数、
在线用户 zset、任务执行锁。
不可用时自动降级进程内存(utility/cache.Init + 运行期自动切换);
降级影响:多实例不共享(限流/锁/在线用户退化单机语义)。
--------------------------------------------------------------------------------
5. jwt 双令牌
--------------------------------------------------------------------------------
secret: 签名密钥(生产必须修改!)
issuer: "easygoadmin"
accessTokenExpire: 7200 访问令牌(秒)
refreshTokenExpire: 604800 刷新令牌(说明项)
※ 运行时刷新令牌有效期代码读取 jwt.refreshExpire,未配置回退 168h(=604800s,
与 refreshTokenExpire 同值)。如需自定义请配置 jwt.refreshExpire。
--------------------------------------------------------------------------------
6. captcha 图形验证码
--------------------------------------------------------------------------------
expire: 300 bypassCode: "618618"(万能验证码,仅开发调试,生产留空!)
length: 6(4~8) chars: 排除 0O/1lI 的字符集
fontPath: resource/static/fonts/Vera.ttf(缺失降级 SVG)
imageWidth/Height/FontSize/noiseLines/noiseDots:渲染参数
--------------------------------------------------------------------------------
7. rateLimit 限流
--------------------------------------------------------------------------------
enable: true limit: 120 windowSeconds: 60 authLimit: 10
语义:每窗口每 IP 每路径;authLimit 仅对 /api/login、/api/captcha、
/api/oauth2/token;limit=0 关闭普通限流;仅 /api 前缀参与。
--------------------------------------------------------------------------------
8. operationLog 操作日志
--------------------------------------------------------------------------------
enable: true 总开关
logQuery: true 是否记录查询类(logType=query)操作
--------------------------------------------------------------------------------
9. job 定时任务
--------------------------------------------------------------------------------
logRetentionDays: 90 三张日志表保留天数(LogClean 消费,任务参数
retentionDays 可覆盖)
--------------------------------------------------------------------------------
10. tenant 租户
--------------------------------------------------------------------------------
defaultId: 1 超管新增租户级数据时的默认归属租户(显式指定时从显式值)
--------------------------------------------------------------------------------
11. app 应用
--------------------------------------------------------------------------------
timezone: "Asia/Shanghai"
demo: false 演示模式:true 时写操作一律拒绝(登录等公开接口放行)
errorMsg: true 返回详细错误信息(生产建议 false)
--------------------------------------------------------------------------------
12. api
--------------------------------------------------------------------------------
camelSnakeConvert: true 全链路驼峰,恒开
proxyIps: "127.0.0.1,::1,101.226.0.0/15,..." 受信代理(逗号分隔,支持 CIDR);
仅直连来源命中时才信任 X-Forwarded-For / X-Real-IP(限流计数、日志 IP、
归属地解析均依赖此处准确性)
--------------------------------------------------------------------------------
13. cors
--------------------------------------------------------------------------------
allowOrigin: "*" 空或 * 用框架内置策略;具体来源显式回显 + 预检 204
--------------------------------------------------------------------------------
14. file 文件上传
--------------------------------------------------------------------------------
uploadDir: 上传物理根目录(绝对路径,按环境调整)
domainUrl: 文件访问域名(如 http://file.goframe.elevue;为空用 /upload/ 相对路径)
tempDir: "temp"
imageExt: gif,jpg,jpeg,png,bmp,webp,svg
fileExt: 上述 + doc/docx/xls/xlsx/ppt/pptx/pdf/zip/rar/txt/csv/mp3/mp4/sql/js/css
maxSize: 10(MB)
tempExpireDays: 7 临时目录保留天数(0=不清理;CleanTemp 每日 03:30 执行)
--------------------------------------------------------------------------------
15. generator 代码生成器
--------------------------------------------------------------------------------
parentMenuId: 189 新模块菜单挂载的父级菜单 ID
overwrite: false 覆盖开关:true 生成即覆盖已存在文件;
false/缺省 = 缺失补生成、存在跳过
--------------------------------------------------------------------------------
16. ip2region 归属地
--------------------------------------------------------------------------------
path: "manifest/data/ip2region.xdb" IPv4 xdb 文件
--------------------------------------------------------------------------------
17. gf gen 配置(hack/config.yaml,开发工具链)
--------------------------------------------------------------------------------
gfcli.gen.dao:
link / removePrefix goframe_ / group default / jsonCase CamelLower /
descriptionTag true / overwriteDao false /
tplDaoIndexPath hack/tpl/dao_index.tpl(自定义:挂全局钩子 + Ctx 覆写)/
path internal、daoPath dao、doPath model/do、entityPath model/entity
gfcli.gen.service:
srcFolder internal/logic → dstFolder internal/service;
clear: false(gf v2.10.3 Windows 下 clear 逻辑路径分隔符不匹配会误删,
故关闭;且 service 本就手维护,勿整体运行)
--------------------------------------------------------------------------------
18. 部署要点
--------------------------------------------------------------------------------
18.1 部署拓扑图
┌────────────┐
│ 浏览器/ui │
└─────┬──────┘
│ /api/*(JWT)
反向代理(可选,需列入 api.proxyIps)
nginx/… ──────────────▶┌──────────────────────────────┐
│ admin-server :8000 │
│ 中间件链 + Bind + logic │
│ gcron(job daemon + 03:30 │
│ 上传清理) │
└───┬──────────┬──────────┬─────┘
│ │ │
┌─────────▼──┐ ┌─────▼─────┐ ┌──▼──────────────┐
│ MySQL/PG… │ │ Redis │ │ 磁盘 │
│ goframe_* │ │ 缓存/锁/ │ │ file.uploadDir │
│ 25 张表 │ │ 在线/限流 │ │ manifest/data/xdb│
└────────────┘ └───────────┘ │ log/ │
└─────────────────┘
多实例:应用可横向扩展;Redis 保证 限流计数/任务执行锁/在线用户 跨实例一致
(Redis 降级内存时以上全部退化为单机语义)
构建:go build -o bin/admin-server.exe main.go(Windows);Linux 同理去 .exe
启动:./bin/admin-server.exe(默认 :8000)
重启(本机):见 document/启动命令.txt——先按端口找 PID taskkill,再编译启动
依赖检查清单:
1) MySQL 可连(database.default.link)
2) Redis 可连(不可用自动降级但多实例语义受损)
3) manifest/data/ip2region.xdb 存在
4) file.uploadDir 目录存在且可写;file.domainUrl 可对外访问
5) jwt.secret 修改为环境专属值;captcha.bypassCode 生产留空;app.demo=false;
app.errorMsg=false(生产)
6) generator.parentMenuId 按环境菜单树确认
静态资源:SetServerRoot("resource") 对外暴露 resource/(含 /upload、/swagger、字体)
健康检查:GET /api/health → {"code":0,"data":{"status":"up"}}(免登录,不限流)
API 文档:GET /swagger(本地 Redoc 页);GET /api.json(OpenAPI JSON)
日志:控制台 + log/ 目录(logger.path)
--------------------------------------------------------------------------------
19. 质量与回归
--------------------------------------------------------------------------------
静态检查:go vet ./... && gofmt -l .
测试:go test ./...(命令见 Makefile test 目标)
接口回归基线:hack/golden/ 与 hack/after* 目录存放接口响应黄金样本
(如 article_page.json、level_list.json 等),用于比对接口行为回归;
比对时注意分页等动态参数需带录制参数(如 pageSize=3)对齐。
+173
View File
@@ -0,0 +1,173 @@
================================================================================
EasyGoAdmin 后端开发者文档 · 11 API 接口清单
================================================================================
来源:api/v1/*.go 的 gmeta 注解全量提取(与运行时路由一致)。
说明:
* 全部路由无 /v1 版本段,统一 /api 前缀。
* “权限”列为空表示登录即可(permission 标签为空);公开表示免登录
(须与 middleware/public.go 白名单一致)。
* 每个接口的操作日志标题/类型见 gmeta log/logType(下文按模块给出规律:
七件套日志标题 = "{模块中文名}-查询分页记录/查询列表/查询详情/新增记录/
修改记录/删除记录/批量删除记录/导入数据/导出数据/设置状态")。
* 完整 OpenAPI:GET /api.json;在线调试:GET /swagger。
--------------------------------------------------------------------------------
auth 认证(公开/顶层)
--------------------------------------------------------------------------------
POST /api/login 公开 登录(写登录日志)
GET /api/logout 登出(拉黑 jti)
GET /api/captcha 公开 图形验证码
POST /api/oauth2/token 公开 刷新令牌/密码模式兑换(轮换)
GET /api/oauth2/check/token 校验令牌有效性
DELETE /api/oauth2/remove/:token 撤销指定令牌(踢下线)
(POST /api/register 在白名单中放行,路由未注册——启用时补实现)
--------------------------------------------------------------------------------
index 个人中心(登录即可)
--------------------------------------------------------------------------------
GET /api/index/getUser 当前用户信息(超管返回 *:*:*)
GET /api/index/getMenus 当前用户菜单树
GET /api/index/getMenus/:id 同上(menu 页面调用)
PUT /api/index/updatePassword 修改个人密码(旧密码校验)
PUT /api/index/updateProfile 更新个人信息
--------------------------------------------------------------------------------
system 域
--------------------------------------------------------------------------------
user(/api/user,perm sys:user:*)
GET /api/user/page sys:user:page
GET /api/user/list sys:user:list
GET /api/user/detail/:id sys:user:detail
POST /api/user/add sys:user:add
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
DELETE /api/user/batchDelete sys:user:batchDelete
POST /api/user/import sys:user:import Excel 导入(logType import)
GET /api/user/export sys:user:export Excel 导出(logType export)
GET /api/user/document/:id sys:user:detail 打印信息
role(/api/role)
七件套 sys:role:{page|list|detail|add|update|delete|batchDelete}
GET /api/role/menu/list/:roleId sys:role:getPermission 查询角色菜单
POST /api/role/menu/save sys:role:savePermission 保存权限
报文字段:Add/Update 含 dataScope(1-全部 2-本部门及以下 3-本部门 4-仅本人,
gvalid in:1,2,3,4 校验,缺省归一 1);Page/Detail 返回 dataScope +
dataScopeText(字典 role_data_scope 驱动)。机制见 03 章第 6 节。
menu(/api/menu,List 返回树形)
七件套 sys:menu:{page|list|detail|add|update|delete|batchDelete}
dept(/api/dept,List 返回树形;删除有子级/用户守卫)
七件套 sys:dept:*
level(/api/level,含状态与导入导出)
七件套 + PUT /api/level/status(sys:level:status)
POST /api/level/import sys:level:import
GET /api/level/export sys:level:export
position(/api/position)
七件套 + PUT /api/position/status(sys:position:status)
tenant(/api/tenant)
七件套 + PUT /api/tenant/status(sys:tenant:status)
POST /api/tenant/account sys:tenant:add 创建租户账号(开户)
GET /api/tenant/users/:tenantId sys:tenant:users 查询租户用户
--------------------------------------------------------------------------------
data 域
--------------------------------------------------------------------------------
config(/api/config)
七件套 sys:config:*
GET /api/config/web/index sys:configWeb:data 获取网站配置
POST /api/config/web/save sys:configWeb:save 保存网站配置
config_item(/api/config/item) 七件套 sys:configitem:*
param(/api/param) 七件套 sys:param:*
dict(/api/dict)
七件套 sys:dict:*(注:无 /dict/list,列表经 page/别名)
GET /api/dict/refreshCache sys:dict:cache 刷新字典缓存
GET /api/dictionary/list 登录即可 字典列表别名(前端 dictionary.ts)
GET /api/dictionary/info 登录即可 字典详情别名
dict_item(/api/dict/item)
七件套 sys:dictitem:*
GET /api/dict/item/getDictItemList/:code 登录即可 按编码查字典项(缓存)
notice(/api/notice)
七件套 + PUT /api/notice/status(sys:notice:status)
city(/api/city,别名 /api/area)
七件套 sys:city:*
GET /api/city/getCityList/:parentCode 登录即可 下级城市(级联懒加载)
GET /api/area/getParent 登录即可 省份列表
GET /api/area/findByParentCode 登录即可 按父编码查城市
--------------------------------------------------------------------------------
content 域
--------------------------------------------------------------------------------
article(/api/article)
七件套 + PUT /api/article/status(sys:article:status)
category(/api/category,List 返回树形)
七件套 sys:category:*
link(/api/link)
七件套 sys:link:*(无 status)
--------------------------------------------------------------------------------
monitor 域
--------------------------------------------------------------------------------
operation_log(/api/oper/log,仅查询与删除)
GET /page /list /detail/:id、DELETE /delete/:id /batchDelete sys:operLog:*
login_log(/api/login/log)同上五件 sys:loginLog:*
job_log(/api/job/log)同上五件 sys:jobLog:*
job(/api/job)
七件套 sys:job:*
GET /api/job/runOnce/:id sys:job:execute 立即执行(异步)
GET /api/job/pause/:id sys:job:pause
GET /api/job/resume/:id sys:job:resume
POST /api/job/status sys:job:update
GET /api/job/daemonStatus 登录即可 调度器状态
online_user(/api/onlineUser,无表)
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
server_monitor(无表)
GET /api/server/info 登录即可 服务器监控信息
cache_monitor(无表)
GET /api/cache/info 登录即可 缓存监控信息
--------------------------------------------------------------------------------
tool 域
--------------------------------------------------------------------------------
example(/api/example,导入导出演示)
七件套 + PUT /api/example/status(sys:example:status)
POST /api/example/import sys:example:import
GET /api/example/export sys:example:export
file_template(/api/file/template)
七件套 sys:fileTemplate:*
GET /api/file/template/getTemplateByCode/:code 登录即可 按编码查模板
generator(/api/generator)
GET /api/generator/page sys:generator:page 数据表清单
POST /api/generator/preview sys:generator:generator 预览代码
POST /api/generator/generate sys:generator:generator 一键生成
POST /api/generator/batchGenerate sys:generator:batchGenerator 批量生成
upload(/api/upload)
POST /api/upload/uploadFile sys:upload:file 上传(multipart file)
POST /api/upload/uploadFiles sys:upload:files 批量上传
--------------------------------------------------------------------------------
基础设施(router.go 手工注册)
--------------------------------------------------------------------------------
GET /api/health 公开 健康检查 {"status":"up"}
GET /swagger 公开 本地 API 文档页(重定向 Redoc)
GET /api.json 公开 OpenAPI JSON
GET /upload/** 静态文件(SetServerRoot resource)
--------------------------------------------------------------------------------
权限码总规律
--------------------------------------------------------------------------------
sys:{模块小驼峰}:{动作};动作与方法名一致:page/list/detail/add/update/delete/
batchDelete/status/import/export/resetPwd/execute/pause/resume/savePermission/
getPermission/cache/users/save 等。
特例:配置网站 sys:configWeb:data|save;生成器 sys:generator:generator|
batchGenerator;上传 sys:upload:file|files。
超管(ROLE_SUPER_ADMIN)绕过全部权限校验。
@@ -0,0 +1,174 @@
================================================================================
EasyGoAdmin 后端开发者文档 · 12 数据模型与表清单
================================================================================
来源:resource/sql/mysql.sql(规范基准,25 张表)+ internal/model/hooks +
internal/model/entity。方言 DDL(pgsql/sqlite/mssql/oracle)对齐此基准转换,
后四份待人工校对。
--------------------------------------------------------------------------------
1. 全表统一规范
--------------------------------------------------------------------------------
表前缀 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 张(见第 3 节)。
删除一律软删(deleteHook 改写);查询一律经 dao.Ctx 过滤 del_flag=0。
--------------------------------------------------------------------------------
2. ER 关系图(逻辑外键,无物理外键约束)
--------------------------------------------------------------------------------
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 为多对多。
--------------------------------------------------------------------------------
3. 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(无模块文件)
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(无模块文件)
--------------------------------------------------------------------------------
4. 唯一索引清单(13 个,resource/sql 定稿;并发唯一性的最终保证)
--------------------------------------------------------------------------------
表 索引名 列 语义
-------------- ------------------- ------------------------------------------ ------------
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 恒过滤 del_flag=0 语义一致)。
* 含 tenant_id 的索引,模块唯一预检(UniqueFields/手写检查)范围必须等于
索引范围(Group:"tenant_id" 或按行现租户),否则漏报 1062 或跨租户误报。
* document/easygoadmin.goframe.elevue.sql 导入脚本与定稿存在已知 DDL 漂移
(部分 uk_code 缺失),以 resource/sql 为准,需人工补齐。
--------------------------------------------------------------------------------
5. 枚举列约定(列注释驱动字典与生成器)
--------------------------------------------------------------------------------
注释写法 "字段名:1-正常 2-禁用"(或 = 分隔),生成器解析为前端选项并自动
建字典(编码 {模块驼峰}_{字段驼峰})。常见枚举:
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-仅本人
(默认 1;字典 role_data_scope 已随定稿种子
写入 dict/dict_item,机制见 03 章第 6 节)
del_flag 0-正常 1-已删除
login_log.log_type 1-登录 2-退出
operation_log.type 见 consts/log_type.go(41 个值,静态映射不走字典)
--------------------------------------------------------------------------------
6. 租户级表与平台级表(hooks.tenantTables,共 9 张)
--------------------------------------------------------------------------------
租户级(tenant_id 自动过滤/盖章):
goframe_user / role / dept / level / position / article / category / notice / link
平台级(无租户过滤):
tenant / menu / city / config / config_item / param / dict / dict_item /
file_template / job / job_log / operation_log / login_log / example /
user_role / role_menu
新增租户级表必须同步修改 internal/model/hooks/hooks.go 的 tenantTables
(代码生成器不自动维护此清单)。
--------------------------------------------------------------------------------
7. 大表与性能注意
--------------------------------------------------------------------------------
goframe_city 为全国行政区划大表(AUTO_INCREMENT 已至 78 万+):
查询走 parent_code/pid 索引路径(级联懒加载 getCityList),
避免全表 List;导入/迁移时注意批大小。
日志三表(operation_log/login_log/job_log)只增不改,依赖 LogClean 任务
按 job.logRetentionDays(默认 90 天)分批物理清理。
导出接口统一 ChunkQuery 分批(500/页)防 OOM。
--------------------------------------------------------------------------------
8. 建表/变更工作流
--------------------------------------------------------------------------------
1) 以 mysql.sql 为基准补 ALTER(含列注释枚举、uk_* 唯一索引、索引命名
uk_/idx_ 规范)
2) gf gen dao -c hack/config.yaml 刷新 dao/entity/do(service 手维护)
3) 同步 resource/sql 其余四份方言 DDL(人工校对)
4) 租户级表 → 修改 hooks.tenantTables
5) go build ./... 重启;必要时 go run ./cmd/migrator 同步到其他环境库
+194
View File
@@ -0,0 +1,194 @@
================================================================================
EasyGoAdmin 后端开发者文档 · 13 开发 FAQ 与排障
================================================================================
按主题分组的常见问题,结论均可回溯到代码位置(括号内为文件)。
--------------------------------------------------------------------------------
A. 接口与路由
--------------------------------------------------------------------------------
Q1: 为什么接口 404?项目文档里不是说有 /v1 吗?
A: 实际路由无 /v1 版本段(api/v1 只是包名)。全部 gmeta path 直接以 /api
开头(如 GET /api/user/page)。新增接口后必须:重启服务;若为新模块还需
router.go 中 Bind(controller.CXxx)(internal/router/router.go)。
确认路由是否注册可开 server.dumpRouterMap 或查 GET /api.json。
Q2: 返回 403 "没有该操作权限"?
A: gmeta permission 非空且当前用户 Perms 不含该节点(Bind 包装层判定)。
处理:角色管理 → 勾选对应菜单下权限节点 → 保存(会清身份缓存)。
超管(ROLE_SUPER_ADMIN)不受限。permission 留空 = 登录即可。
Q3: 返回 401 但 token 刚签发?
A: 依次排查:① Authorization 头格式(Bearer + 空格);② jwt.secret 是否
多实例/重启后不一致;③ 令牌是否已入黑名单(登出/轮换/强退,
blacklist:token:{jti});④ access 已过期(默认 2h)应先走
/api/oauth2/token 刷新。
Q4: 演示环境提示“演示环境,禁止操作!”?
A: app.demo=true 时写操作全部拒绝(middleware/demo.go)。生产改 false。
Q5: 偶发 429 “请求过于频繁”?
A: 固定窗口限流:普通 /api 120 次/分钟/IP/路径;登录/验证码/令牌兑换
10 次/分钟/IP(rateLimit.*)。反向代理后如全部请求变成同一 IP,
检查 api.proxyIps 是否包含代理地址(否则 XFF 不被信任)。
Q6: 在线调试文档在哪里?
A: GET /swagger(本地 Redoc,不依赖境外 CDN);GET /api.json(OpenAPI JSON,
含全部补注册的闭包路由)。
--------------------------------------------------------------------------------
B. 认证与权限
--------------------------------------------------------------------------------
Q7: 验证码总是失败?
A: ① 校验为一次性(命中即删),前端须每次先取 /api/captcha;② Redis 不可用
降级内存后多实例不共享(验证码与请求可能落在不同实例);③ 开发可用
captcha.bypassCode 万能码,生产必须留空。
Q8: 修改了角色权限但用户仍被拒绝(或仍放行)?
A: 身份缓存 user:identity:{uid} TTL 300s。保存角色权限/分配角色会主动清缓存
(role/user 模块);手工改库不会——等 5 分钟或删除对应缓存键。
Q9: 忘记密码/重置密码?
A: 超管走 PUT /api/user/resetPwd;本人走 PUT /api/index/updatePassword
(需旧密码)。密码存储 bcrypt(password+salt);旧 md5 数据登录会被
IsBcrypt 拒绝,需重置。
Q10: 登录报“该用户名存在于多个租户,请携带租户编码登录”?
A: 未指定 tenantCode 时全局查 username,多租户同名优先平台租户(tenant_id=0),
仍歧义则要求携带 tenantCode(logic/auth.go findUser)。
--------------------------------------------------------------------------------
C. 数据与租户
--------------------------------------------------------------------------------
Q11: Update 报 1062 Duplicate entry(如 uk_tenant_mobile)?
A: 唯一预检范围必须等于唯一索引范围。索引含 tenant_id + del_flag,预检须按
“被改行的现租户”检查(user 模块已按行现租户实现;spec 用
UniqueRule{Field, Group:"tenant_id"})。注意:直接 UPDATE 全表数据/迁移
后预检与索引不一致是最常见根因。
Q12: 租户用户看到了别的租户数据?
A: 排查是否绕过 dao.Ctx 直连 g.DB()(钩子条件不生效);排查该表是否登记进
hooks.tenantTables(新增租户级表必须手工加入,共 9 张)。
Q13: 超管新增数据落到哪个租户?
A: 未显式指定时 hooks.insertHook 填 tenant.defaultId(默认 1);显式传 0
可创建平台共享行。业务取归属租户用 consts.GetTenantID(超管=默认租户)。
Q14: 数据被“删除”了但数据库里还有?
A: 删除接口全部是软删(del_flag=1,deleteHook 改写)。物理清除走 LogClean
仅限三张日志表;业务表恢复需手工 UPDATE del_flag=0。
Q15: 新加了表/列,代码里读不到?
A: gf gen dao -c hack/config.yaml 刷新 dao/entity/do → go build → 重启。
service 接口文件手工维护,勿运行 gf gen service。
--------------------------------------------------------------------------------
D. 文件与富文本
--------------------------------------------------------------------------------
Q16: 上传成功但页面图片 404?
A: ① 库中存相对 uploadDir 路径,展示必须经 GetFileUrl(基座 readTransform
自动处理,手写查询勿直接输出原值);② file.domainUrl 与实际可访问地址
一致性;③ 静态服务根为 resource/(SetServerRoot),确认 uploadDir 位于
resource/upload 或已做目录映射。
Q17: 富文本图片路径是 [IMG_URL]/...?
A: 这是持久化契约(存库格式),读取侧必须经 Detail/List(基座自动
ExpandContentFields)或手工调用 ExpandContentFields;禁止业务直接读
content 原值给第三方。
Q18: temp 目录越来越大?
A: CleanTemp 由 cmd.go 的 gcron 每日 03:30 触发,删除 temp 下超过
file.tempExpireDays(默认 7 天)的日期目录;确认配置非 0 且服务在运行。
Q19: 上传被拒绝(类型/大小)?
A: 扩展名白名单 file.fileExt / file.imageExt;大小 file.maxSize(MB);
图片另做魔数嗅探。.svg 可上传但响应强制 attachment+nosniff(防 XSS)。
--------------------------------------------------------------------------------
E. 定时任务
--------------------------------------------------------------------------------
Q20: 任务到点不执行?
A: ① 任务 status 必须=1(运行中);② cron 为 gcron 6 段(秒 分 时 日 月 周),
可用 # 前缀写标准 5 段("# */5 * * * *");③ 调度器启动延时 2s 且仅
DaemonInit 一次——改库后需 Resume/Status 或重启;④ 查
GET /api/job/daemonStatus 与 /api/server/info 的 Scheduler
(mounted vs expected 差值=挂载失败)。
Q21: 任务执行失败(任务日志 exception)?
A: url 为 http(s):检查可达性、params 是否为合法 JSON(作为 POST body)、
30s 超时;url 为本地方法名:须在 internal/task 注册且进程已加载
(RegisterJobHandler 重复注册会 panic)。多实例下策略 2/3 依赖 Redis 锁。
Q22: 如何新增内置任务?
A: 见 09 章第 8 节:task 包新文件 + init 注册 + 任务表配置 url=注册名 +
go build 重启。
--------------------------------------------------------------------------------
F. 缓存与降级
--------------------------------------------------------------------------------
Q23: Redis 挂了会怎样?
A: utility/cache 自动降级进程内存(启动探测 + 运行期自动切换,Warning 日志)。
功能可用,但限流计数/任务锁/在线用户/黑名单/验证码退化为单机语义,
多实例部署会不一致。/api/cache/info 与 /api/server/info 可查当前模式。
Q24: 字典改了文案没生效?
A: DictGetText 进程内缓存 300s;可调 GET /api/dict/refreshCache 主动刷新
(清 dict:item:{code} 键),或等自然过期。
--------------------------------------------------------------------------------
G. 代码生成器
--------------------------------------------------------------------------------
Q25: 生成器把我的手工修改覆盖了?
A: generator.overwrite=true 时生成即覆盖。恢复手段:git 还原;或改回
false 仅补缺失文件。批量覆盖前务必备份。
Q26: 生成后编译报错/路由没生效?
A: ① go build ./...(生成的 go 文件需编译);② 确认 router.go 已插入
Bind 行(生成器自动写入,幂等);③ 重启服务;④ 菜单/权限已建,
需给角色勾选权限节点。
Q27: 非 MySQL 库生成效果差?
A: 表注释/枚举解析依赖 information_schema(MySQL)。建议在 MySQL 上执行
生成,或人工补齐注释后重新生成。
--------------------------------------------------------------------------------
H. 数据库迁移
--------------------------------------------------------------------------------
Q28: 迁移报“目标库已存在同名表”?
A: 非 -drop-target-first 模式下同名表拦截(建表为 IF NOT EXISTS,数据会
追加,必须前置拦截)。加 -drop-target-first 会清空目标库全部表,慎用。
Q29: 迁移报“表 xxx 无主键,无法分页搬运”?
A: 搬运按主键游标分页,无主键表先补主键。
Q30: pgsql 迁移后插入主键冲突?
A: 序列修复(setval MAX(id)+1)在迁移末段执行;若手工导数据未走迁移器,
需手工修序。mssql 用 DBCC CHECKIDENT;mysql/sqlite/oracle 自动续接。
--------------------------------------------------------------------------------
I. 工程与质量
--------------------------------------------------------------------------------
Q31: 标准命令有哪些?
A: go build ./... | go test ./... | go vet ./... && gofmt -l .
| gf gen dao -c hack/config.yaml | make run/build/test/vet/fmt。
Q32: 接口回归怎么比对?
A: hack/golden/ 与 hack/after* 存有响应黄金样本;比对时带录制参数
(如 pageSize=3)对齐动态值后再 DIFF。
Q33: 日志在哪里?
A: 控制台 + log/ 目录(logger.path)。操作日志通道满丢弃会有
“操作日志通道已满”Warning;中间件依赖缺失启动时有 Warning 提示。
Q34: Windows 重启服务的快捷方式?
A: 见 document/启动命令.txt:按端口找 PID → taskkill → 编译 → 启动。
Q35: 用户列表数据权限(data_scope)不生效/过滤范围不对?
A: 逐项排查(机制见 03 章第 6 节):
1) 角色 data_scope 值是否正确(SELECT data_scope FROM goframe_role);
2) 用户是否挂了多角色(多角色取最宽松——任一角色为"全部"即不过滤);
3) 身份缓存陈旧:DEL easygoadmin:user:identity:{uid} 或重登
(正常经角色/用户编辑接口变更会主动清);
4) 直接改库 dept_id 不经 API 不清缓存(最长 300s 延迟);
5) 未分配部门(dept_id=0)挂部门档会降级为"仅本人"——属安全兜底设计;
6) 超管/ROLE_SUPER_ADMIN 永远绕过,测试过滤效果需用普通账号;
7) 模块未声明 DataScope 挂点则完全不过滤(当前仅 user 模块接线)。
@@ -0,0 +1,185 @@
================================================================================
EasyGoAdmin 后端开发者文档 · 14 核心模块实现详解
================================================================================
对高频改动/机制代表性最强的模块,给出实现级细节(其余标准 CRUD 模块遵循
04 章 ModuleSpec 模式,不再逐个展开)。代码位置均在 internal/logic/。
--------------------------------------------------------------------------------
1. index 个人中心(index.go)
--------------------------------------------------------------------------------
1.1 GetUser(GET /api/index/getUser)
数据来源与安全边界:
* 用户行经 dao.User.Ctx(自动过滤软删/租户);password/salt 永不返回。
* Roles/Perms 优先取 JWT 中间件注入的 ctx(与鉴权同源),为空时回源
LoadIdentity;超管强制 perms=["*:*:*"](前端据此放开全部按钮)。
* 头像经 upload.GetFileUrl 补域名;城市编码四列
(province/city/district/street_code)组装为 city[] 数组;city_info
为空时用 ResolveCityNames 按编码回填名称。
1.2 GetMenus / GetMenusById(两条路由共用 getMenuTree)
* 仅取 type=0(目录/菜单)且 status=0(启用)且 hide=0 的节点——
按钮权限节点不进菜单树,由 GetUser.permissions 下发。
* 超管全量;其余 user→user_role→role_role_menu→menu.id IN(...) 授权过滤;
无角色返回空树([] 非 null)。
* 挂树规则:父节点不在授权集合内(未授权/顶级)一律挂根,避免菜单丢失;
排序 sort,id(根级再按 sort+id 兜底稳定排序)。
1.3 UpdatePassword
只取 id/password/salt 三列 → 旧密码 IsBcrypt+VerifyPassword 校验(1012)→
事务外 GenerateSalt+HashPassword → Update 覆盖 password/salt
(update_user/time 由 hooks 盖章)。改密后未强制下线(令牌仍有效至过期),
如需立即失效可配合在线用户强退。
1.4 UpdateProfile
头像 SaveFileField(avatar, "user") 临时转正;城市优先由编码数组经
ResolveCityNames 解析名称覆盖 city_info;四列编码经 cityCodeAt(req.City, i)
取值写入;不触达 username/password/tenant 等敏感列。
--------------------------------------------------------------------------------
2. role 角色(role.go)
--------------------------------------------------------------------------------
2.1 MenuList(GET /api/role/menu/list/:roleId)
返回全量菜单树(type 0/1 均含,供权限勾选界面展示目录+菜单+按钮三层),
每节点带 Checked=role_menu 是否含该 id(checkedSet 预构建)。
2.2 SavePermission(POST /api/role/menu/save)
语义为“全量重写”:先 Delete 该角色全部 role_menu(软删)→ 逐个 Insert
req.MenuIds → clearRoleIdentityCache(roleId)。
缓存清理链:按 roleId 查受影响 user_id 列表 → 逐个 Del
easygoadmin:user:identity:{uid}(见 15 章缓存清单)。
2.3 data_scope 字段链路(数据范围,机制总览见 03 章第 6 节):
roleToData:Add/Update 均带 data_scope,入参 ≤0 归一为 1(全部);
roleSpec.SerializeMaps = {"data_scope": "role_data_scope"} →
列表行 dataScopeText 由字典填充;
AfterUpdate(roleClearCache)在数据范围变更后失效该角色全部用户
身份缓存——下一次请求 LoadIdentity 重取 min(data_scope) 即生效。
2.4 其他:删除守卫(存在用户引用则拒绝,1014/1011 语义)。
已知偏差(如实记录):role 的 spec 唯一预检当前声明为
UniqueFields{Field:"code"}(未带 Group),而索引 uk_tenant_code 是
“租户内唯一”——预检范围偏宽,超管为不同租户创建相同角色编码时会被
误报“已存在”(数据库层不会冲突)。修复方向:补 Group:"tenant_id",
使预检范围等于索引范围(见 04 章 4.4 与 13 章 Q11)。
--------------------------------------------------------------------------------
3. menu 菜单(menu.go)
--------------------------------------------------------------------------------
* List 树形(IsTree+ParentOf);删除守卫 menuDeleteGuard:存在子菜单拒绝(1011)。
* 关联清理:删除菜单时同步清理 role_menu 引用(menuCleanRelations),
批量删除同理——防止悬挂授权。
* 父级校验 menuParentGuard:parentId 不得指向自身或后代(防环)。
* type 语义:0=菜单/目录(进菜单树),1=权限节点(进 Perms 校验);
status 0=启用;hide 0=显示(hide=1 不进菜单树但权限仍生效需注意——
getMenuTree 过滤 hide=0,LoadIdentity 过滤 hide=0,故 hide=1 权限节点
不生效,配置时勿用)。
--------------------------------------------------------------------------------
4. user 用户(user.go,机制最全的模块)
--------------------------------------------------------------------------------
4.1 唯一校验(手写,按行现租户)
usernameExists / mobileExists 按 (username|mobile, tenant_id, 排除自身) 查;
Update 时先读被改行的 tenant_id 再校验——校验范围必须等于
uk_tenant_username / uk_tenant_mobile 索引范围(含 del_flag 由 dao.Ctx
恒过滤保证)。历史教训:用 GetTenantID(超管=默认租户)会漏报 1062。
4.2 AssignRoles(userId, roleIds)
全量重写 user_role(先删后插)→ clearIdentityCache(userId)。
Add/Update 均经此方法同步角色。
4.3 checkTenantUserMax(tenantId)
统计租户现有用户数 vs tenant.max_user_num,超限返回 1007。
4.4 genUserCode(tenantId)
前缀=租户 code(平台/超管为 "SYS"),序号=该租户现有用户数+1,
格式 "{prefix}-%06d"(如 T0001-000001)。
4.5 Import(POST /api/user/import)
excel.ReadSheet → 表头映射 → 逐行:唯一校验/租户上限/默认密码 bcrypt
(预计算)/角色与城市解析 → 批量插入 → 返回导入成功/失败统计。
城市列经 ResolveCityCodes(名称串) 转编码入库。
4.6 Export:与列表同筛选(userFilter + DataScope,applyFilters 同链)
→ ChunkQuery 500/页 → excel.WriteSheet(导出可见集=列表可见集)。
4.7 userExpandNames:列表行批量补 dept/level/position/role 名称与城市文本
(namesById/dictName 批查防 N+1)。
4.8 DataScope 接线(数据权限,机制总览见 03 章第 6 节):
userSpec 声明 DataScope: ApplyDataScope("dept_id")——user 是当前
唯一接线模块(全库仅 user 表有 dept_id 列)。Update 改 dept_id 后
AssignRoles 无条件 clearIdentityCache,部门变更即时生效(无需等 300s)。
--------------------------------------------------------------------------------
5. tenant 租户(tenant.go)
--------------------------------------------------------------------------------
5.1 编码规则 T%04d(自增 ID 回填)
code 为 NOT NULL + 唯一键,插入时无法预知 ID → 先写临时占位编码
(T%010d(纳秒取模)),Insert 后按 LastInsertId 修正为 "T"+4位ID。
Add 与 Account 均遵循此模式;显式传 code 时查重后直接使用不回填。
5.2 Account(POST /api/tenant/account)开户事务
bcrypt 在事务外预计算(短事务纪律)→ WithTrans 内三段原子写:
① Insert 租户(status=1,hooks 填审计列)
② 修正 code=T%04d(tenantId)
③ 以 tenantCtx(WithTenantID(txCtx, tenantId))Insert 初始管理员:
code="{code}-000001"(首用户序号),tenant_id 显式传入,password/salt
为预计算值
返回 {tenantId, code, userId}。角色分配未含在开户事务内(按需另行调用)。
5.3 tenantFilter / fillUserCount / clearCache
列表为平台专属视图(超管可见全部,带每租户用户数统计);
状态/过期变更后 Del tenant:info:{id}(Tenant 中间件 60s 摘要缓存)。
--------------------------------------------------------------------------------
6. dict / dict_item 字典(dict.go、dict_item.go)
--------------------------------------------------------------------------------
6.1 两级缓存
Redis 层:easygoadmin:dict:item:{code} = 字典项 JSON 数组,TTL 3600s
(setCache;getCache 命中直接返回,供前端下拉等高频读取)。
进程层:base.go dictTextCache({code}:{value}→name),TTL 300s
(DictGetText 消费,供 {field}Text 填充)。
6.2 RefreshCache(GET /api/dict/refreshCache)
重建 Redis 层:可按 code 单字典或全量重查 dict_item → 按字典编码分组
→ 逐 code setCache。注意:只刷 Redis 层,进程内 dictTextCache 仍按 300s
自然过期(重启/多实例各自过期)。
6.3 别名路由(/api/dictionary/list|info,登录即可)供前端通用字典组件;
dict_item 快捷路由 getDictItemList/:code 走 Redis 缓存。
6.4 dict_item 无 list 标准接口(页面用 page);删除字典项不主动清缓存
(最长 1 小时延迟),关键场景调 RefreshCache。
--------------------------------------------------------------------------------
7. city 城市(city.go)
--------------------------------------------------------------------------------
7.1 双向编解码(internal/logic 包级函数,user/index 模块复用)
ResolveCityNames(ctx, codes[]string, separator) string
area_code IN(...) 批查 → 按入参顺序拼名称串(空编码跳过)。
ResolveCityCodes(ctx, cityInfo string) [4]string
名称串按逗号拆 4 级,逐级 parent 约束消歧:
省:pid=0 AND name=? → 市:parent_code=省area_code AND name=?
→ 区 → 街道,逐级依赖上级结果;任一级失败后续留空。
例:"江苏省,南京市,建邺区" → [320000000000, 320100000000, 320105000000, ""]
7.2 查询接口:ByParent(parent_code)/GetCityList(:parentCode) 级联懒加载、
AreaProvince(pid=0 省份列表),均登录即可(前端级联选择器高频调用)。
7.3 大表注意:勿全表 List;筛选走 area_code/parent_code/pid。
--------------------------------------------------------------------------------
8. config 配置(config.go)与 param 参数
--------------------------------------------------------------------------------
* config 为“配置分组”,config_item 为键值明细(config_id 关联,value/options/type)。
* 网站设置页专用:GET /api/config/web/index 聚合读取、POST /api/config/web/save
批量保存(perm sys:configWeb:*),save 为覆盖式写入。
* param 为全局键值参数(code 唯一),与 config_item 的区别:param 面向
运行参数快速读写,config 面向结构化表单配置。
--------------------------------------------------------------------------------
9. 模块机制代表速查
--------------------------------------------------------------------------------
树形模块 menu / dept / category(IsTree+ParentOf+删除子级守卫)
导入导出模块 user / level / example(excel + ChunkQuery)
文件/富文本模块 article(cover 单图、images 多图、content 富文本)、user(avatar)、
tenant(logo)、example
无表模块 auth / index / online_user / server_monitor / cache_monitor /
generator / upload
控制密集模块 job(调度挂载)、role(权限缓存)、tenant(开户事务)、
user(唯一/上限/角色)、dict(两级缓存)
--------------------------------------------------------------------------------
10. 唯一预检覆盖现状(已知偏差汇总)
--------------------------------------------------------------------------------
当前 spec 声明了 UniqueFields 的模块仅两处:
role {Field:"code"}(无 Group —— 预检范围偏宽,见第 2.3 节偏差说明)
param {Field:"code"}(与全局 uk_code 一致,无偏差)
user(username/mobile/code)为手写检查(按行现租户,与索引对齐);
tenant.Add 手写 code 查重;config/dict/file_template 的 Add 走 CheckUnique
(code 全局,三处调用点在各自 Add);三者的 Update 未做预检——改 code 撞
uk_code 时以数据库 1062 兜底。
未做预检、依赖数据库 uk_* 兜底的模块:dept / level / position / category
(uk_tenant_name)——重复名称提交会收到 1062 裸错误提示。
修复方向:为四个模块补 UniqueFields{Field:"name", Group:"tenant_id"}
(user 名称类除外,已手写),并按 04 章 4.4 保证预检范围=索引范围。
@@ -0,0 +1,106 @@
================================================================================
EasyGoAdmin 后端开发者文档 · 15 缓存键与失效策略
================================================================================
缓存实现:utility/cache 统一门面(Redis 优先,失败/运行期出错自动降级进程
内存 gcache,UsingMemory() 可查)。Redis 键统一前缀 easygoadmin:(consts/
cache_key.go)。降级影响:多实例不共享(限流计数、任务锁、在线用户、黑名单、
验证码、字典全部退化单机语义)。
--------------------------------------------------------------------------------
1. 缓存分层图
--------------------------------------------------------------------------------
请求 ──▶ ① 进程内缓存(sync.Map,无 Redis 交互,重启即清)
dictTextCache(300s) · reqFieldCache/textPlanCache(反射计划,永不过期)
maskReCache(正则,永不过期)
──▶ ② Redis(跨实例共享,TTL 自动回收)
黑名单/在线/验证码/字典/租户摘要/身份/限流/任务锁
──▶ ③ 数据库(唯一真相,dao.Ctx 全量过滤条件)
设计原则:② ③ 之间的一切缓存都必须有明确的失效点或短 TTL 兜底;
① 仅缓存“计算计划/只读文本”,不允许缓存业务状态。
--------------------------------------------------------------------------------
2. Redis 键全集(键模式 / TTL / 写入方 / 读取方 / 失效策略)
--------------------------------------------------------------------------------
键模式 TTL 写入 读取
------------------------------------ -------- ------------ --------------
easygoadmin:token:blacklist:{jti} 剩余有效期 Logout/Refresh JWT 中间件
+0(+60) 轮换/RemoveToken Exists 判定
ForceLogout
easygoadmin:online:jti:set (zset) 成员随info MarkOnline OnlineUser.Page
(score=过期 ts)MarkOffline (ZRangeByScore≥now)
easygoadmin:online:info:{jti} access剩余 MarkOnline OnlineUser.Page/
+60s MarkOffline Detail/ForceLogout
easygoadmin:captcha:{uuid} 300s Captcha 生成 Login/Verify(一次性)
easygoadmin:dict:item:{code} 3600s dict.setCache/ dict_item 快捷接口/
(Refresh RefreshCache AliasList 等
Cache 主动刷)
easygoadmin:tenant:info:{id} 60s Tenant中间件/ Tenant 中间件/
(自然过期) Login 路径 Login
easygoadmin:user:identity:{uid} 300s JWT 中间件 JWT 中间件
(回源Loader) (LoadIdentity 结果 JSON:
Roles/Perms/DataScope/DeptId;
旧缓存缺新字段时读侧回退安全值)
easygoadmin:user:perms:{uid} —(约定键,当前主路径用 identity 合并键,
保留扩展;变更时一并清理)
easygoadmin:user:roles:{uid} —(同上)
ratelimit:{ip}:{path}:{窗口起点} window+5s RateLimit incr RateLimit
job:lock:{jobId} 300s 兜底 jobTryLock(NX) jobTryLock
(执行完即 Del)
--------------------------------------------------------------------------------
3. 主动失效点清单(代码级)
--------------------------------------------------------------------------------
身份缓存 user:identity:{uid} 删除时机:
* role.SavePermission 保存角色权限 → clearRoleIdentityCache(roleId)
→ 查该角色全部 user → 逐个 Del
* role.Update 修改角色(含 data_scope 数据范围变更)→ AfterUpdate
钩子 roleClearCache → 同上链路逐个 Del
* user.AssignRoles 分配/调整角色 → clearIdentityCache(userId)
(user.Update / user.Add 均经 AssignRoles——改部门 dept_id 亦在此
路径清缓存,数据权限部门维度变更即时生效)
* user 修改密码 / 禁用等(部分路径主动清;其余靠 300s 过期兜底)
* 版本兼容:Identity 新增字段(DataScope/DeptId)反序列化缺省为 0,
读侧 GetDataScope 回退"全部"——升级无需强清存量缓存
租户摘要 tenant:info:{id} 删除时机:
* tenant 状态/过期时间变更(tenant 模块 AfterUpdate clearCache 钩子)
字典 Redis 缓存 dict:item:{code}:
* dict.RefreshCache 主动全量/单编码重建;dict_item 变更不主动清
(最长 1 小时延迟——关键场景调刷新接口)
黑名单:不主动删,靠 TTL=令牌剩余有效期自然过期(过期即令牌本身已失效)。
任务锁 job:lock:{id}:执行结束 defer Del;崩溃场景靠 300s TTL 兜底。
验证码:Verify 命中即删(一次性)。
--------------------------------------------------------------------------------
4. 进程内缓存明细
--------------------------------------------------------------------------------
dictTextCache(base.go)key={code}:{value} → 显示名,TTL 300s。
读多写少的 {field}Text 填充热路径。修改字典文案最迟 5 分钟生效;
RefreshCache 不清此层(各实例自然过期)。
reqFieldCache / textPlanCache / childrenIdxCache(module_spec.go)
反射索引与填充计划,按 reflect.Type 缓存,进程生命周期——只缓存“结构计划”
不缓存数据,类型不变即安全。
maskReCache(middleware/util.go)脱敏正则,按字段名缓存。
--------------------------------------------------------------------------------
5. TTL 设计原则(新增缓存时遵循)
--------------------------------------------------------------------------------
1) 状态判定类(黑名单/租户状态):TTL ≤ 业务有效期,且必须有主动失效点;
宁可短 TTL 多回源,不可长 TTL 陈旧放行。
2) 展示文本类(字典/归属地):TTL 300s~3600s + 刷新接口,接受分钟级延迟。
3) 高频读聚合类(identity):合并为单键 JSON(一次回源装多个集合),
TTL 300s,变更路径主动 Del。
4) 计数/锁类:键内嵌窗口起点或场景唯一 id,TTL 只做回收不参与语义
(限流窗口、执行锁均如此)。
5) 任何经 cache.Set 的值必须可 JSON 序列化为字符串;读侧 Unmarshal 失败
视为未命中回源。
--------------------------------------------------------------------------------
6. 排障
--------------------------------------------------------------------------------
键不存在/值陈旧:先查是否内存降级(redis-cli KEYS 'easygoadmin:*' 对比);
权限不生效:DEL easygoadmin:user:identity:{uid};
租户被禁仍可访问:等 60s 或 DEL easygoadmin:tenant:info:{id};
多实例行为不一致:确认 Redis 可用(/api/cache/info mode 应为 redis)。
+103
View File
@@ -0,0 +1,103 @@
================================================================================
EasyGoAdmin 后端开发者文档 · 16 测试与质量保障
================================================================================
--------------------------------------------------------------------------------
1. 现状(封板时点,如实说明)
--------------------------------------------------------------------------------
仓库当前不含 *_test.go 单元/集成测试文件(find 全仓为 0)。
质量保障依赖以下四层手段;测试补齐方向见第 5 节(待补齐建议,未实施)。
--------------------------------------------------------------------------------
2. 静态质量门禁(每次提交前必跑)
--------------------------------------------------------------------------------
go build ./... 编译(生成器产物、service 接口同步的第一道闸)
go vet ./... 静态分析
gofmt -l . 格式检查(无输出=通过;生成器产物已统一 gofmt)
Windows 无 make 时直接执行上述命令;有 make 时:
make vet / make fmt / make build / make test(test 目前为空跑)
--------------------------------------------------------------------------------
3. 接口响应黄金样本回归(hack/golden 体系)
--------------------------------------------------------------------------------
3.1 目录与用途
hack/golden/ 34 个响应 JSON —— 当前回归基线(比对基准)
hack/after_all/ 31 个响应 JSON —— 全量接口终态快照(覆盖各域 page/list/
detail/user_detail 等,含 tag_* 等扩展样本)
hack/before_c/ 12 个 —— c 轮改造前快照(对照组)
hack/after_b/ 5 个 —— b 轮改造后样本
hack/after_c/ 12 个 —— c 轮改造后样本
hack/after/ 12 个 —— 早期轮次样本
命名规律:{模块}_{动作}.json(如 user_page.json、level_list.json、
city_list_pc.json 带场景后缀)。
3.2 比对流程(规范)
1) 录制/回放均须带录制参数对齐:分页接口固定 pageSize=3 等小值,
否则 total/list 长度随数据漂移造成大量伪 DIFF。
2) 请求以管理员身份(admin)发起,保证数据集稳定;筛选条件固定。
3) DIFF 归因三分类后再决定处理:
A. 预期变更(本次改动引起)→ 更新对应 golden 样本并说明
B. 动态值漂移(id 递增、时间戳、consume_time、ip/location)→
归一化后比较,不更新
C. 非预期回归 → 修代码,禁止改样本
4) 样本与代码同库提交,review 时随 diff 一并审阅。
--------------------------------------------------------------------------------
4. 运行期可观测性(替代部分测试的兜底)
--------------------------------------------------------------------------------
启动自检:cmd/wireDependencies 校验 IdentityLoader/TenantLoader/LogSink
接线(缺失启动 Warning);cache.Init 探测 Redis;调度器 DaemonInit
统计挂载数。
运行探针:
GET /api/health 存活(免登录、不限流)
GET /api/server/info 运行时/磁盘/DB/缓存/调度器健康(mounted≥expected)
GET /api/cache/info 缓存模式与命中率
GET /api/job/daemonStatus 调度条目
GET /api.json 路由全集(与 gmeta 注解一致性自查)
日志信号:
启动期依赖缺失 Warning;Redis 降级 Warning;操作日志通道满丢弃计数 Warning;
调度器任务挂载失败 Warning;panic 堆栈 Error(Response recover)。
--------------------------------------------------------------------------------
5. 测试补齐建议(当前未实施,给出优先级)
--------------------------------------------------------------------------------
P0 hooks 集成测试(sqlite 内存库):
Insert 盖章(审计列/租户填充/平台默认租户)、Update 盖章、
Delete 软删改写、ApplyCtx 过滤(del_flag + 租户 IN)。
P0 ModuleSpec 流水线测试:Like/Eq/Keyword 零值跳过、Keyword OR 参数化、
树形挂根、FillTextFields 反射计划、唯一规则 Group 语义。
P1 migrate 渲染器单测:五方言 Column/CreateTable/CreateIndex 金样对比
(纯字符串函数,无需数据库);normalizeType/parsePgIndexCols 解析用例。
P1 utility 单测:crypto(bcrypt 往返)、excel(ReadSheet/WriteSheet tmpfs)、
upload(SaveFromTemp 幂等与外站跳过)、captcha.Verify 一次性、
ua/ip2region 解析。
P2 auth 集成:登录→refresh 轮换→黑名单失效→强退双拉黑闭环(需 Redis 或
内存降级模式)。
建议目录:internal/**/**_test.go 与包同置;sqlite 用例通过
database.default.type=sqlite 临时配置注入;MySQL 集成用例以环境变量门控
(无环境时 t.Skip)。
--------------------------------------------------------------------------------
6. 变更回归最小集(改动映射到必查项)
--------------------------------------------------------------------------------
改动类型 最小回归
------------------------ --------------------------------------------------
中间件/认证 login→带token请求→401路径→refresh→logout 黑名单
权限/菜单 getMenus/getUser/403 接口/角色保存后即时生效
某模块 CRUD 该模块 page/detail golden 比对 + 唯一冲突路径
hooks/dao 相邻两租户数据隔离抽查 + 软删后 list 不可见
生成器 -preview 清单核对 → 生成 → go build → 新路由 200
调度/任务 daemonStatus 挂载数 + 手动 runOnce + job/log 状态
迁移器 行数校验 PASS + 随机抽行值比对 + 序列插入测试
配置 启动日志 + health + 对应功能开关路径
--------------------------------------------------------------------------------
7. 发布前检查清单
--------------------------------------------------------------------------------
1) go build ./... && go vet ./... && gofmt -l .(无输出)
2) 关键接口黄金样本回归(带录制参数)
3) 配置核对:jwt.secret / captcha.bypassCode 空 / app.demo=false /
app.errorMsg=false / file.uploadDir 与 domainUrl / rateLimit 生产值
4) /api/health、/api/server/info(调度器 mounted=expected、缓存 mode=redis)
5) 登录→菜单加载→任一 CRUD 冒烟;操作日志页有新记录
6) LogClean/CleanTemp 两个内置调度已挂载(daemonStatus/gcron 条目)
+124
View File
@@ -0,0 +1,124 @@
================================================================================
EasyGoAdmin 后端开发者文档 · 17 安全设计专题
================================================================================
按威胁面组织,每项给出控制措施与代码落点,便于安全评审与自查。
--------------------------------------------------------------------------------
1. 威胁 → 控制总表
--------------------------------------------------------------------------------
威胁面 控制措施 代码落点
------------------ -------------------------------------- ------------------
身份伪造 HS256 签名校验+iss/exp 强校验 utility/jwt Parse
令牌重放(登出后) jti 黑名单,TTL=剩余有效期 blacklist:token:{jti}
Refresh 重放 轮换:旧 refresh 入黑名单,已黑即拒 logic/auth Refresh
被踢会话复活 强退同时拉黑 access+refresh 双令牌 online_user ForceLogout*
口令窃取/泄露 bcrypt(password+salt);日志脱敏 utility/crypto;
access_token/refresh_token/password 不落日志 middleware maskSecret
暴力破解 登录/验证码/令牌兑换独立严阈值 10/min/IP rateLimit.authLimit
水平越权(跨租户) dao.Ctx 注入 tenant_id IN(0,自身) hooks.ApplyCtx
行级越权(部门/本人) 角色 data_scope 四档叠加过滤 logic/data_scope.go
(仅列表/导出;写操作由 permission 兜底) ApplyDataScope
垂直越权 gmeta permission + HasPerm;超管放行 Bind 包装层
注入(SQL) 全链 gdb 参数化;无方言拼接红线 dao/Where 参数化
参数篡改 gvalid 标签校验(Parse 统一触发) api/v1 结构体标签
存储型 XSS .svg 强制 attachment+nosniff middleware/response
富文本外链不本地化、占位符契约 base.go Localize
SSRF 富文本仅处理本站 URL,外站一律不抓取 localizeOne 判定
CSRF Bearer 头令牌(非 Cookie)+ CORS 白名单 JWT 中间件/CORS
IP 伪造 仅受信代理(api.proxyIps,支持 CIDR) realClientIP
的 XFF/X-Real-IP 生效
限流绕过 计数经 Redis 跨实例共享;窗口起点对齐 RateLimit incrCounter
越权写(演示站) Demo 中间件拦截全部写操作 middleware/demo
审计缺失 操作日志异步全量记录(脱敏+响应+耗时) OperationLog
会话固定 无服务端 Cookie 会话,令牌全部服务端签发 auth Login
敏感信息出栈 GetUser 永不返回 password/salt;详情页
令牌原文仅在线用户详情(需权限) index/online_user
--------------------------------------------------------------------------------
2. 认证安全细节
--------------------------------------------------------------------------------
* 令牌:HS256,密钥与签发者可配置(jwt.secret/issuer);Parse 限定
WithValidMethods(HS256) 防算法混淆;exp 必填。
* 双令牌分工:access 2h 只作身份;refresh 7d 且仅 /api/oauth2/token 可用
(typ 校验,access 不能当 refresh 用)。
* 验证码:一次性(命中即删)、TTL 300s、bypassCode 生产必须留空。
* 密码:bcrypt DefaultCost;外部 salt 防彩虹;旧 md5 数据被 IsBcrypt 拒绝
(强制重置升级)。
* 万能码/调试口:仅 captcha.bypassCode 一处,发布检查清单强制清空。
--------------------------------------------------------------------------------
3. 授权安全细节
--------------------------------------------------------------------------------
* 权限点粒度:到接口级(sys:module:action),按钮级由前端按 permissions 控制;
后端不做按钮级校验——新增接口必须同时声明 permission 或显式留空(留空
= 登录即可,评审需明确该选择)。
* 数据权限(行级):租户隔离(硬规则,不可配置)+ 角色 data_scope(可配置,
两层独立叠加)。data_scope 四档:1-全部 2-本部门及以下 3-本部门 4-仅本人;
多角色取最宽松;超管/平台绕过;未分配部门用户挂部门档降级为仅本人;
仅作用于挂接 ApplyDataScope 的模块之 Page/List/导出(applyFilters 链),
Detail/Update/Delete 不过滤,由 permission 节点与租户隔离兜底。
* 权限缓存一致性:保存权限/分配角色/角色改数据范围主动清 identity 缓存,
最迟 300s 过期。
* 隐藏节点:menu hide=1 的权限节点不进 Perms(LoadIdentity 过滤 hide=0),
不能用 hide 表达“禁用权限”——用 status。
--------------------------------------------------------------------------------
4. 租户隔离红线(安全审计重点)
--------------------------------------------------------------------------------
1) 业务代码禁止 g.DB() 直查业务表(绕过钩子与过滤);
2) 业务代码禁止手写 del_flag / tenant_id 条件(应由 dao.Ctx 注入);
3) 新增租户级表必须登记 hooks.tenantTables(漏登=跨租户泄漏);
4) 唯一预检范围必须等于唯一索引范围(否则 1062 漏报/误报);
5) 平台级(超管)全量可见是设计预期,管理端点须收紧 permission。
--------------------------------------------------------------------------------
5. 文件上传安全链
--------------------------------------------------------------------------------
大小限制 → 扩展名白名单 → 图片魔数嗅探 → 随机文件名(防猜路径/覆盖)
→ 临时/正式两段式(未过校验不进正式目录)→ 静态响应 svg 附件化+nosniff
→ 定期清理 temp。注意:fileExt 白名单含 js/css/sql 等——如对公网用户提供
上传,建议按环境收紧 fileExt(配置层控制,无需改码)。
--------------------------------------------------------------------------------
6. 日志与审计合规
--------------------------------------------------------------------------------
* 操作日志:登录后全部带 log 标签接口的操作均可追溯(操作人/参数/响应/IP/
归属地/UA/耗时/结果);密码与令牌字段脱敏。
* 登录日志:成功/失败/退出全记录,失败含原因。
* 日志保留:job.logRetentionDays(默认 90 天)物理清理——合规要求更长
保留期时调大配置。
* 审计字段:create_user/update_user 由 hooks 强制盖章(取登录名),
业务不可伪造(经 ctx 注入;无 ctx 的后台任务写 daemon/空)。
--------------------------------------------------------------------------------
7. 部署安全基线(生产)
--------------------------------------------------------------------------------
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)
--------------------------------------------------------------------------------
8. 已知边界与取舍(诚实声明)
--------------------------------------------------------------------------------
* CheckUnique 为先查后插的 UX 预检,存在 TOCTOU——并发唯一性以 uk_* 索引
为最终防线(捕获 1062 翻译提示)。
* 数据权限(data_scope)仅约束 Page/List/导出;Detail/Update/Delete
不做行级过滤——越权改删他人记录的防线是接口 permission 节点 + 租户隔离。
管理端点授予权限时应评估该角色是否应拥有写权限。
* 数据权限当前仅 user 模块接线(dept_id 列);其他模块行级可见性由
租户隔离单层兜底。
* 部门子树查询(deptSubtreeIds)每次全量拉部门表 BFS,无缓存——部门表
数百行内无感知,成千上万行时需加短时缓存。
* 身份缓存(300s)窗口内,直接改库的 dept_id/data_scope 不经 API 不会
即时生效(API 路径均主动清缓存,见 15 章)。
* 在线用户详情返回令牌原文(便于运维排查),依赖 sys:onlineUser:detail
权限收口。
* 操作日志 param 截断 8KB、响应截断 8KB,超长报文不完整留存。
* 无独立的密码复杂度策略/定期更换策略(业务层未强制,可按需在 index/
user 密码路径追加规则)。
* 无物理防重放窗口(jti 黑名单依赖 TTL;时钟偏移极小场景不受影响)。
@@ -0,0 +1,128 @@
================================================================================
EasyGoAdmin 后端开发者文档 · 18 扩展开发与变更管理
================================================================================
按“变更类型”给出标准操作清单(checklist),保证不漏步骤、不变异架构。
--------------------------------------------------------------------------------
1. 给现有模块新增一个接口
--------------------------------------------------------------------------------
□ 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 核对新条目
□ 变更回归(16 章第 6 节对应行)
--------------------------------------------------------------------------------
2. 给现有模块加字段
--------------------------------------------------------------------------------
□ 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 同步(生成器模块可 -preview
对照模板手改)
□ resource/sql 五份方言 DDL 同步;必要时 12 章清单更新
□ go build → 重启 → page/detail 黄金样本更新
--------------------------------------------------------------------------------
3. 新增模块
--------------------------------------------------------------------------------
□ 设计表(统一审计列 + del_flag + uk_* + 注释枚举)→ DDL 落库
□ 租户级?→ hooks.tenantTables 登记(12 章第 6 节)
□ go run ./cmd/generator -tables "goframe_x|中文名"(或手动四件套)
□ logic 调整:spec 声明(筛选/树形/字典/文件/唯一 Group)、扩展接口
□ service 接口核对(生成器已产出,按需扩展);router.go 注册行确认
□ go build ./... → 重启 → 角色勾选新权限节点
□ 00 章模块表、11 章接口清单、12 章表清单同步更新(文档纪律)
--------------------------------------------------------------------------------
4. 新增中间件
--------------------------------------------------------------------------------
□ 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 的放行策略
□ 02 章与 00 章链路描述同步更新
--------------------------------------------------------------------------------
5. 新增定时任务
--------------------------------------------------------------------------------
□ internal/task/{task_name}.go + init RegisterJobHandler("TaskName", h)
□ handler 签名 (ctx, params) (string, error);长任务分批+上限;禁 HTTP 于
事务内;可复用 base(WithTrans/ChunkQuery)
□ 任务表配置:url="TaskName"、params JSON、cron(6 段或 # 5 段)、
execute_policy/is_sync;发布(status=1)
□ 验证:daemonStatus 挂载 + runOnce + job/log 成功记录
□ go build 重启(init 注册发生在进程加载期)
--------------------------------------------------------------------------------
6. 自定义 / 扩展 dao
--------------------------------------------------------------------------------
□ 生成的 dao/{m}.go(自定义模板 hack/tpl/dao_index.tpl 产物)可安全手改:
挂钩子 + Ctx 覆写已在生成模板内;在文件尾部“Add your custom methods”
区追加方法(如统计/联查),保持只依赖 dao 内部与 gdb
□ 修改模板本身:改 hack/tpl/dao_index.tpl 后 gf gen dao 重新生成全部
(overwriteDao:false 保留手工扩展文件——注意新旧模板产物差异需人工合并)
□ 红线:自定义方法也必须以 Ctx(ctx) 为查询入口(否则丢失租户/软删过滤);
禁止在 dao 内做业务判断
--------------------------------------------------------------------------------
7. 逃生舱与扩展挂点
--------------------------------------------------------------------------------
BodyUnmarshaler 请求体形态不规则(平铺 map 等)时,Req 实现
UnmarshalBody(r) 跳过默认 Parse(自行做 gvalid)
ModuleSpec.Filter 复杂查询条件(不与 Like/Eq 重复)
ModuleSpec.DataScope 数据权限挂点(已接通):共享工厂 ApplyDataScope(col)
位于 logic/data_scope.go;模块声明 DataScope 即生效,
作用于 Page/List/导出;档位见 17 章授权安全细节
ModuleSpec 钩子写不进 基座禁止按模块分支——写不进钩子就整个方法手写
(合法逃生舱,如 job.Detail 因嵌入结构手写)
logic 包级共享函数 跨模块复用放具体模块文件顶部(同包可见);
横切能力进 base.go(导出上限 15 个,超出拆子包)
RegisterJobHandler 本地任务处理器全局注册表(重名 panic)
middleware wire 三 Loader/Sink 函数变量,业务与中间件解耦
--------------------------------------------------------------------------------
8. 配置变更
--------------------------------------------------------------------------------
□ manifest/config/config.yaml 修改(键义见 10 章)
□ 多环境差异用 GF_GENV_ 环境变量覆盖(如 GF_GENV_DATABASE_DEFAULT_LINK),
不改文件
□ 新增配置键:config.yaml 注释说明 + 代码 cfgBool/cfgInt 带默认值读取
(缺失不 panic)+ 10 章文档同步
□ 影响限流/日志/上传/调度的键变更后跑对应回归(16 章第 6 节)
--------------------------------------------------------------------------------
9. 未启用项与前端遗留
--------------------------------------------------------------------------------
项 现状 启用/处理要点
---------------- --------------------------------------- -----------------
用户注册 publicPaths 含 /api/register 启用时补建表逻辑+
(路由未注册) 开放策略+租户归属规则
数据范围自定义档 现四档纯列值(role.data_scope) 第5档+部门关联表+
角色表单部门树多选
导出增强 当前导出返回临时文件 URL 可加流式下载/异步导出
SendSms 任务 task/send_sms.go 仅记日志 接短信通道时替换实现
站内信/数据源 前端废弃功能(sendMsg.vue、 后端不实现;前端调用
dataSource.ts) 点为遗留死代码
短信/OSS/WebSocket 后端桩接口已删除 前端遗留调用为废弃代码,
不再调用即无影响
注:角色数据权限(四档)已实现,见 17 章授权安全细节与 logic/data_scope.go。
--------------------------------------------------------------------------------
10. 封板后的变更纪律
--------------------------------------------------------------------------------
1) 一切接口行为以 11 章清单 + /api.json 为准,改动即更新文档
2) 表结构改动三同步:MySQL 定稿 → resource/sql 四方言 → 迁移(如跨环境)
3) 破坏性变更(改名/删字段/改枚举值)必须走版本化说明 + 前端同步 + 黄金
样本更新,禁止静默变更
4) 架构红线复查:绕过 dao/手写租户条件/反向依赖 task/超 15 导出的 base/
未登记 tenantTables——任何一项都应打回