架构与请求生命周期¶
x-admin 是标准的 Go 分层单体 + React SPA。理解这一页,后面的模块文档都只是这套骨架的具体填充。
1. 分层与依赖方向¶
cmd/server
└── bootstrap(wire 装配,唯一允许同时依赖各层实现)
├── router ── middleware
├── handler ── service(定义 repo 端口)
│ └── repository ── model
├── jobs(cron 调度)
└── pkg(错误码、响应、缓存、JWT、存储、Excel…)
| 层 | 目录 | 职责 | 禁止 |
|---|---|---|---|
| 入口 | cmd/server |
加载配置、启动 HTTP、优雅退出 | 写业务 |
| 装配 | internal/bootstrap |
创建基础设施、repo、service、handler、router | 写业务 |
| 路由 | internal/router |
声明全部端点与所需权限码;admin 内置,其他 API 面由 create 生成 | 写业务 |
| 中间件 | internal/middleware |
请求 ID、恢复、日志、CORS、审计、鉴权、限流 | 访问数据库 |
| 协议 | internal/handler |
参数绑定、校验入口、统一响应 | 直接访问 DB、写业务规则 |
| 业务 | internal/service |
业务规则、事务编排、权限约束、通知 | 依赖 Gin |
| 端口 | internal/service/ports.go |
定义仓储接口,供测试替换 | 依赖具体实现 |
| 数据 | internal/repository |
GORM 查询、分页、软删除、游标 | 写业务判断 |
| 模型 | internal/model |
结构体、GORM 标签、TableName、过滤器 | 写逻辑 |
| 基础库 | internal/pkg |
无业务依赖的通用能力 | 反向依赖 service |
依赖方向永远是 外层 → 内层:handler 依赖 service,service 依赖 ports.go 里的接口,repository 实现接口。service 不 import gin,repository 不知道 HTTP。
2. 依赖注入(wire)¶
- 各层用
wire.go里的ProviderSet声明构造函数(repository.NewUserRepo、service.NewUserService…)。 internal/bootstrap/providers.go负责把配置里的原语(端口、TTL、上限)转换成具名依赖,并承接带生命周期的构造(缓存关闭、任务 Boot、导出 worker)。wire_gen.go是生成物,不要手改;改了构造参数后运行make wire。- handler 依赖由
bootstrap汇总成router.Handlers,路由层只拿一个结构体。
新增依赖时的顺序:构造函数 → 加进对应 ProviderSet → make wire → 编译。具体示例见 新增业务模块。
3. 启动与关闭顺序¶
启动:
config.Load:默认值 → YAML →X_ADMIN_*环境变量 → 校验(端口、JWT 密钥、DSN、代理网段)。database.Open:建立 GORM 连接、设置连接池、Ping。database.Migrate(auto_migrate: true):执行内嵌的 golang-migrate 脚本与种子数据。- wire 装配:缓存(Redis / memory)→ 本地存储 → 各仓储 → 各服务 → handler → router。
- 生命周期动作:审计队列启动 worker、导出服务注册模块并
Boot()、定时任务Boot()把启用任务注册进 cron。 scheduler.Start(),HTTP 开始监听。
关闭(SIGINT/SIGTERM):
http.Server.Shutdown:停止接收新请求,等待存量请求,超时由server.shutdown_timeout控制。scheduler.Stop():停止 cron 循环,等待在途任务。- wire 注入的 cleanup:关闭导出队列、审计队列、缓存。
- 关闭数据库连接池。
依赖顺序保证不会出现「任务还在跑、数据库已关」的情况。
4. 一次请求的完整链路¶
以 PUT /admin/v1/users/12 为例:
client
│ Authorization: Bearer <access>
▼
RequestID 生成/透传 X-Request-ID,写入 context
Recovery panic → 500,记录堆栈
AccessLog 记录方法、路径、状态、耗时
CORS 按 cors.origins 放行预检
Audit 写操作复制请求体(敏感字段脱敏),异步落库
UploadSecurity 给 /uploads 加 CSP sandbox / nosniff
UploadAuth /uploads 专用鉴权(Cookie / Bearer / 分享令牌)
RateLimit(/admin/v1) 全局 100 次/分/IP
Auth 解析 JWT → 加载用户 → 校验 session_version 与会话
Require("system:user:update") 权限码判断,super_admin 直接放行
handler.Update 绑定 JSON → PathID → service.Update
service.Update 校验字段、部门/角色、越权与内置账号保护 → repo
repository 事务写 sys_users / sys_user_roles
handler response.OK → {code:0,message:"ok",data,request_id}
4.1 鉴权不是只验 JWT¶
middleware.Auth 每次请求都会:
- 校验 HS256 签名、issuer、过期时间。
- 用
uid重新从数据库加载用户(停用/删除立即生效)。 - 比对 token 里的
session_version与用户当前值(改密、重置密码、启停账号后旧 token 立即失效)。 - 检查
session:<uid>:<sid>是否仍存在(单设备撤销后 access token 立即失效)。 - 把
Principal{UserID, Username, SessionVersion, SessionID, Super, Permissions}写入 context。
因此角色/菜单改动无需重新登录即可生效,而安全变更会强制旧会话失效。
4.2 权限码¶
- 后台路由集中声明在
internal/router/admin.go,每个受保护路由都带middleware.Require("system:xxx:yyy")。 super_admin在Principal.Has中直接返回 true,不走权限集合。- 普通用户的权限码来自「启用角色 → 授权菜单 →
sys_menus.permission」。 - 前端
can()只决定按钮显隐,最终授权永远以后端Require为准。
详见 权限体系(RBAC)。
4.3 API 面¶
路由按 API 面拆分,不再把所有接口都写在 /admin/v1 下:
| 面 | 前缀 | 认证 |
|---|---|---|
| API 面 | 前缀 | 认证 |
| --- | --- | --- |
admin |
/admin/v1 |
后台 JWT + sys_users + 权限码 |
| 自定义面 | create api 指定 |
按需接入 JWT / API Key / 签名 |
框架默认只内置 admin。client、merchant、partner 等面由 go tool xadmin create api 生成,生成后写入 api.<name> 配置即可挂载。每个面在 Surface 中声明前缀、限流桶和认证中间件,公共挂载逻辑在 internal/router/surface.go。不同面使用不同 JWT issuer,token 不能跨面使用。接入方式见 多 API 模式,handler 脚手架用 代码生成。
5. 错误处理¶
业务错误统一用 internal/pkg/apperr:
return apperr.New(apperr.CodeConflict, "角色下仍有用户,无法删除")
return apperr.Wrap(err, apperr.CodeInternal, "文件保存失败")
response.Error把业务码映射为 HTTP 状态,并输出{code,message,request_id}。- 未知错误统一转
50001,不把数据库/文件系统细节返回给客户端。 - 5xx 会写结构化错误日志(含
request_id、方法、路径、原始错误),便于按请求 ID 追日志。 - 审计中间件读取
c.Errors,只记录可安全展示的错误摘要。
错误码表见 API 与错误码。
6. 异步与后台任务¶
| 机制 | 实现 | 关键点 |
|---|---|---|
| 操作审计 | LogService 有界队列(2048)+ 后台 worker |
队列满时丢弃而不是阻塞请求;关闭时等待剩余日志写完 |
| 异步导出 | ExportService worker 池 + 队列 |
游标分批生成 xlsx,完成后站内通知;详见 异步导出 |
| 定时任务 | jobs.Cron + JobService |
5 段 Cron、同名替换、SkipIfStillRunning、多副本缓存锁 |
| 站内通知 | 同步写入 sys_notifications |
通知失败不影响主流程(notify 辅助函数吞掉错误) |
| 会话/限流/验证码 | cache.Store(Redis / memory) |
键设计见各模块文档 |
7. 前后端边界¶
- 后端只返回 JSON:后台默认前缀
/admin/v1,自定义 API 面按生成时的前缀挂载;文件通过/uploads提供,但需要鉴权。 - 前端
api/client.ts负责令牌注入、401 单飞刷新、错误封装、上传与下载。 - 登录后
/auth/me返回用户、权限码、菜单树;前端据此生成导航,但页面路由是静态注册的(未知菜单路径不会渲染页面)。 - 前后端权限码必须一致:新增按钮权限时,后端
Require、种子菜单、前端can()三处都要改。
8. 扩展点速查¶
| 需求 | 扩展点 |
|---|---|
| 新增业务模块 | 参考 新增业务模块 |
| 新增后台任务 handler | provideJobExecutor 的 switch + 定时任务 |
| 新增导出模块 | ExportBuilders() + provideExportService |
| 替换文件存储为对象存储 | 实现 storage.Store,替换 provideStorage |
| 新增缓存实现 | 实现 cache.Store,替换 newCache |
| 新增 API 面/接入客户端认证 | 参考 多 API 模式、代码生成 |
| 新增认证体系(C 端) | 参考 前台用户认证 |
| 新增前端页面 | App.tsx 路由 + config/nav.ts + pages/ |