签入脚本
This commit is contained in:
@@ -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 / 数据库)
|
||||
@@ -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 是否注册
|
||||
@@ -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 上生成或人工补注释。
|
||||
@@ -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)由后续人工处理
|
||||
@@ -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 必须可用。
|
||||
@@ -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)对齐。
|
||||
@@ -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 同步到其他环境库
|
||||
@@ -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)。
|
||||
@@ -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 条目)
|
||||
@@ -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——任何一项都应打回
|
||||
Reference in New Issue
Block a user