Graft 是一个基于 Go 构建的组合式后台平台。
它的目标不是单一业务后台,而是提供一套可扩展的运行时内核,让新功能可以按模块方式快速接入,包括:
- 用户、认证与授权治理
- RBAC 权限
- 审计日志
- 定时任务
- 存储管理
- Docker / SSH / 运维能力
- 后续其它业务模块
平台核心只负责基础设施与扩展机制,业务能力全部通过编译期模块组合。
当前架构定位固定为:
Module-Oriented Modular Monolith- compile-time module wiring
- minimal module lifecycle
- runtime feature enablement rather than runtime plugin platform
术语说明:
server/plugins/*、internal/plugin*只作为历史迁移证据保留在归档或追踪材料中;当前 live 物理路径已迁到server/modules/*、internal/module*与internal/moduleapi/*- 在当前仓库语义里,它们表示 backend business modules,而不是 runtime plugin platform
项目要优先满足这几件事:
- 新增一个后台模块时,接入流程尽量固定化
- 平台内核与业务模块边界清晰
- 模块之间可以通过稳定接口协作,而不是直接耦合实现
- 前端页面风格统一,适合批量扩展标准中后台页面
- 保持对 AI / MCP 辅助开发友好,便于快速生成和调整模块代码
明确不追求的目标:
- 一开始就做运行时热插拔
- 一开始就支持第三方动态分发模块市场
- 一开始就做重量级微服务拆分
运行时日志仍以 core 注入的 *zap.Logger 为唯一 DI 基线。server/internal/logger 额外拥有 typed category registry
和薄 CategoryLogger facade,用于有界的高频诊断;它不是新的日志框架,也不改变 module 依赖方向。类别过滤在 Zap core
中执行,确保禁用类别不会进入 output 或下游 sink。TRACE 是低于 DEBUG 的 process-output-only 诊断级别,App Log
持久化边界仍由 AppLogger 独立拥有。
| 分类 | 技术 |
|---|---|
| 语言 | Go 1.26.x |
| Web 框架 | Gin |
| 配置管理 | Viper |
| 日志 | Zap |
| ORM | Ent |
| 数据库 | PostgreSQL |
| 缓存 / 基础 KV | Redis |
| 权限 | Casbin |
| 认证 | JWT |
| CLI | Cobra |
| 数据迁移 | Atlas versioned migrations |
| 定时任务 | robfig/cron |
| 配置格式 | 部署与启动配置由环境变量承载;管理员运行时策略由 System Config 承载;提交 .env.example 作为本地与 Docker 部署模板 |
| 分类 | 技术 |
|---|---|
| 框架 | Vue 3 |
| 语言 | TypeScript |
| 构建工具 | Vite |
| UI 组件库 | TDesign Vue Next |
| 状态管理 | Pinia |
| 路由 | Vue Router |
| HTTP 请求 | Axios |
| 样式辅助 | UnoCSS |
| 图标 | Iconify |
前端不切 React,原因不是 React 不可用,而是当前项目目标是“快速扩展标准中后台”,Vue 3 + TDesign 更贴近这个目标。
平台级本地化采用“先留口子、后扩语言”的策略:
server/internal/i18n继续作为server唯一平台级 i18n facade,负责 locale 解析、message key 查找与回退能力web在应用壳层提供 locale 状态、消息查找与请求透传能力;壳层负责聚合应用级与modules/**的消息源,而不是把 locale 真相限制为单一宿主目录- 当前阶段先补强自管实现的 registry / namespace / duplicate-key / freeze 语义,不在此时切换为
go-i18n或引入第二套 i18n facade - 当前 locale 范围只收敛到
zh-CN/en-US,默认语言与回退语言保持zh-CN - 首阶段不要求把全部页面与历史文案迁移为多语言资源,但新公共错误响应、模块消息注册与后续模块接入应优先使用稳定 key
server 的完成态校验统一收敛到一个显式 Go CLI 入口:graft validate backend。
仓库根 Justfile 可以作为 contributor-facing 的 optional developer entrypoint / convenience layer,帮助首次上手或常见本地工作流收口到统一命令名;但它只是包装现有 authoritative commands,不是新的 startup authority、validation truth、runtime contract 或 CI contract。涉及 backend 完成态时,just check、just lint、just generate 等 recipe 只能包装既有 Go CLI、脚本与前端命令,不能替代 graft validate backend 本身的 authority。
后端质量基线规则如下:
- 固定使用
golangci-lint v2.12.2作为统一 backend lint runner,不允许在本地、agent 或 CI 中使用latest或临时版本漂移。 - 首次运行 backend validation 前,先通过
golangci-lint --version确认 runner 可用且实际版本为v2.12.2;命令缺失或版本不匹配时,使用 Go toolchain 安装固定版本:go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2。同时确保$(go env GOPATH)/bin(或显式配置的GOBIN)在PATH中,并再次从版本命令输出确认v2.12.2后再执行验证;这只提供 lint runner,不改变graft validate backend作为唯一 blocking lint gate 的入口。 graft validate backend --stage lint是唯一允许作为 backend blocking lint gate 的入口;agent、本地开发与 CI 不得各自维护第二套 blockinggolangci-lint run命令或 shell 编排。- backend blocking lint gate is changed-file scoped against the resolved base branch.
- changed-file scoped means whole-file enforcement on files changed relative to the resolved base branch via
--new-from-rev=<merge-base> --whole-files. - untouched files are not blocking gate failures.
- touching a historical hotspot file means the touched file must satisfy the current lint gate, unless a narrowly
documented temporary
nolintis justified. - full-repo lint is audit-only backlog scanning.
- new code must not expand the lint backlog.
- 后端完成态质量链顺序固定为:
migration version gate -> graft validate backend --stage lint -> go test (smallest direct scope) -> go build ./cmd/graft -> graft validate smoke when needed。 - 当任务修改任一 live Ent schema 路径、任一 live Ent 生成入口、Atlas/Ent 迁移生成相关配置,或任何会影响 Ent
schema 语义的手写代码时,完成态还必须对受影响的 Ent 包补跑对应
go generate,把生成结果纳入同一变更范围;若同时涉及数据库结构变更,还必须通过仓库现有显式迁移流程生成或更新迁移文件,并至少验证受影响 Ent 包的最小直接go test与cd server && go test ./...。 - backend lint issue 如当前切片无法立即清理,只能通过 active tracking 文档中的 controlled exception 或 narrow
temporary
nolint暂留,并记录来源、影响、保留原因、负责人或上下文与下一步清理条件。 - 该质量基线默认治理
server的手写代码;generated、third-party 与 migration 产物应通过显式配置与 豁免边界处理,而不是通过放宽手写代码规则来换取通过。
graft
├─ core runtime
│ ├─ config
│ ├─ logger
│ ├─ database
│ ├─ http server
│ ├─ schema / migration assets + CLI
│ ├─ event bus
│ ├─ permission registry
│ ├─ menu registry
│ ├─ cron registry
│ ├─ service container
│ └─ module manager
│
├─ business modules
│ ├─ auth
│ ├─ user
│ ├─ rbac
│ ├─ audit
│ ├─ scheduler
│ ├─ storage
│ ├─ docker
│ ├─ ssh
│ └─ more...
│
├─ admin web
│
└─ cli
架构原则:
- 核心运行时只提供公共能力,不承载具体业务
- 业务全部通过编译期模块注册到平台
- 模块通过公开接口交互,不直接依赖彼此内部实现
- 前后端都按“模块化接入”组织
server 的目标文件组织、Provider/Adapter/Integration 区分、独立 Agent、Runner 和 conformance fixture 以
项目文件组织与扩展点设计.md 为详细 authority,并由 ADR-025 锁定迁移原则。
该设计是渐进目标,不代表当前目录已经完成迁移。现有 server/internal/**、server/modules/** 和
server/runner/compose/** 继续作为 live/current 或 legacy-frozen 路径;新代码必须先完成边界判定并遵循目标落点。
稳定依赖方向为:
module / application -> moduleapi capability or external port -> provider implementation -> adapter / SDK
Provider 是平台运行能力的实现,例如 Docker Runtime、Kubernetes Deployment、Vault Secret 或 BuildKit Builder。 GitHub、GitLab、Jira、Slack 等连接外部业务系统的能力属于 Integration,不得被泛化为 Runtime Provider。
当前仍保持 compile-time registration、显式 Runtime 生命周期、单一 Task/Submission 执行边界和轻量 DI;不引入 runtime plugin loading、Provider marketplace、第二套 service locator 或 Provider 自有任务状态机。
运行时资源 ownership、capability visibility 与 composition unit 的跨边界规则由 运行时组合与资源治理设计.md 统一约束。它要求长期资源具有可追踪 creator、owner 和 disposer, 但不改变 Module、Task Runtime、Agent、Provider 或 realtime 的既有 authority,也不引入动态 Context hierarchy 或 HMR。
后端核心建议分为这些包:
cmd/graft/
internal/
app/ # 应用启动、装配、生命周期
container/ # 轻量 DI / 服务注册
config/ # 配置读取与默认值
contract/ # 平台级 typed contract 与稳定契约归属
i18n/ # 平台级 locale 解析与消息回退
logger/ # 日志初始化
database/ # DB 初始化与事务封装
http/ # Gin server、router、中间件
eventbus/ # 进程内事件总线
permission/ # 权限点注册
menu/ # 菜单注册
cron/ # 定时任务注册
module/ # 当前模块契约与管理器;历史 `plugin` 命名仅作迁移证据
moduleapi/ # 跨模块稳定接口定义
modules/ # 当前 business modules 目录
auth/ # 认证、token/session/cookie、登录与受限会话治理
user/ # 用户资料、用户管理与管理员用户资源治理
rbac/ # 角色、权限、resource/action 元数据与授权能力
上面的现有 Core/Module 目录是当前实现视图。未来新增外部扩展按以下目标分类记录,不在本阶段创建空目录:
server/providers/<kind>/<name>/**:compile-time Provider 实现;server/agents/<name>/**:独立部署的 Builder/Runtime Agent;server/integrations/**:外部业务系统集成;deployments/**:仓库级部署拓扑;tests/conformance/**:Provider、Agent、Runner 的一致性 fixture。
internal/moduleapi 仍然是跨模块业务 capability 的稳定边界;未来的 internal/ports 只允许承载经过明确
评审的外部能力端口,不能替代 moduleapi 或演变为 common/shared/utils。
应用执行顺序固定为:
先执行显式 schema / migration CLI 步骤,再启动应用:
- 加载配置
- 初始化数据库连接配置
- 执行 Atlas versioned migrations CLI
应用启动顺序固定为:
- 加载配置
- 初始化日志
- 初始化平台级 i18n 服务
- 初始化数据库
- 初始化 HTTP 服务
- 初始化核心注册中心与服务容器
- 注册模块
- 校验模块依赖并排序
- 执行模块
Register - 冻结 i18n 等声明式注册面
- 执行模块
Boot - 启动 HTTP 服务
关闭顺序反向执行,优先停止对外服务,再逐步释放模块资源与核心资源。
type ModuleLifecycle interface {
Register(ctx *module.Context) error
Boot(ctx *module.Context) error
Shutdown(ctx *module.Context) error
}
type ModuleSpec struct {
ID string
Dependencies []string
Builder module.Builder
}
type Module interface {
ModuleLifecycle
Name() string
DependsOn() []string
}模块名与模块依赖的 canonical authority 来自 compile-time ModuleSpec,而不是运行时模块实例再维护第二份 Name() /
Version() / DependsOn() 元数据镜像。
运行时只要求业务模块实现最小生命周期契约;core runtime 通过 compile-time ModuleSpec 包装得到 Module 视图,再按
稳定的 Name() / DependsOn() 结果执行排序和生命周期编排。
Register负责声明和注册能力,不做长时间运行逻辑;模块面向用户的消息资源也应在这一阶段完成注册Boot负责真正启动运行时行为,例如事件监听、定时任务、后台任务;此时不得再追加新的 i18n 注册Shutdown负责资源回收和停止行为;模块应优先使用module.Context.LifecycleContext中注入的有界关闭上下文停止后台任务,而不是自行回退到context.Background()
模块可以注册这些内容:
- HTTP 路由
- 菜单
- 权限点
- message bundles / message keys
- 定时任务
- 事件监听器
- 对外公开的服务接口
- 配置说明
模块依赖分两层:
- 模块依赖:通过 compile-time
ModuleSpec.Dependencies声明 - 服务依赖:通过容器解析公开接口
模块上下文与跨模块公开边界不直接暴露具体 ORM 句柄。
数据访问统一通过中性的 repository / store factory 边界进入,由核心运行时负责把底层 Ent client 装配到模块内部实现中。
模块上下文还应显式提供平台统一的日志与 i18n 能力:
- 日志统一复用 core 创建的
Zaplogger,不允许模块各自维护分裂的日志框架 - i18n 能力统一经由
server/internal/i18nfacade 暴露;模块不直接依赖未来可能替换的底层实现细节 - 模块面向用户返回的稳定错误响应应优先输出
message_key + localized message,而不是只拼接硬编码字符串
运行时管理器必须支持:
- 依赖拓扑排序
- 缺失依赖报错
- 循环依赖报错
- 重复模块报错
平台级实时推送能力属于 core runtime,不属于某一个业务模块私有实现。
固定分层如下:
server/internal/realtime负责统一 topic、发布订阅 hub、WebSocket / SSE gateway、ticket 消费和连接生命周期server/modules/<name>/...负责把模块自有事件、日志或资源快照发布到 canonical topicweb先通过普通 HTTP 鉴权接口申请单次 realtime ticket,再通过统一 WebSocket 或 SSE gateway 订阅 topic
当前 canonical topic 形态固定为:
container.stats:<id>container.logs:<id>container.events:<id>audit.eventssystem.eventsplatform.update.operations:<operationID>
规则:
- 模块不得再各自发明第二套 WebSocket 或 SSE 入口用于普通订阅式事件推送;只允许向其 canonical topic 发布受控 payload
- WebSocket 与 SSE 共用 topic、ticket、权限和资源范围语义。WebSocket 用于双向会话,SSE 用于服务端单向状态流;两者都必须由
server/internal/realtimegateway 消费一次性 ticket - 需要持续采样的能力,例如容器资源统计,应在模块
Boot阶段启动后台 collector,再向 realtime topic 发布结果 - request-time one-shot 采样不是实时资源展示的 authority,不再作为长期主路径
最终结论:
- 引入轻量 DI / 服务注册
- 不引入重量级 IoC 容器
- 不使用基于反射的自动装配、包扫描、tag 注入
- 保持 Go 风格的显式构造与显式依赖传递
这样做的原因:
- 模块化平台需要统一注册与解析入口
- 但仓库不希望牺牲可读性和可调试性去换取隐式魔法
- 对 AI 辅助开发而言,显式 wiring 比隐藏框架行为更稳定
判断标准:
- 新模块能清楚声明依赖并完成接入
- 运行时依赖错误能直接定位到注册或解析路径
- 平台不会因为容器抽象过重而模糊
core与module的边界
前后端协作要围绕统一的扩展路径组织,而不是各自发散演进。
一个新能力的最小接入链路应保持显式:
- 后端模块
- 权限点
- 菜单元信息
- API
- 前端模块
- 页面入口
这条链路必须可追踪、可验证、可复用。
前端不应发明一套脱离后端模块语义的独立权限体系;后端也不应只暴露接口而不提供菜单和权限元信息。
当前 MVP 收敛阶段采用:
- 后端主导真实契约稳定
- 前端围绕真实契约接入与壳层收敛
当前阶段的 web 不是后台业务页面扩展阶段,而是基础壳层与真实契约接通阶段。
契约治理补充约束:
- 高风险共享契约的真值统一遵循 契约治理与魔法值治理规范
permission code、error code、message key、event name、route name/path、storage key、header与其它跨边界稳定值,不得继续以散落字面量作为长期真值- 当前阶段优先建立 canonical 归属、typed contract 边界、lifecycle 与 compatibility 规则,再逐步补 drift detection、hook 与 CI
- 同一语义只允许一个 canonical contract;兼容期 alias 可以存在,但不能演变为第二套长期真值
当前认证治理补充约束:
-
auth、user、rbac、resource的职责边界固定如下:auth回答“你是谁”,拥有登录、token、session、cookie、bootstrap、受限会话与自助改密生命周期user不等于auth,只拥有用户资料与用户管理能力rbac回答“你能做什么”,拥有 role、permission、user-role 与授权判定resource只表达“你要访问什么”的被保护对象概念边界;当前阶段不拆独立模块,继续由rbac承载 resource/action/permission metadata
-
auth不拥有resource、permission、role -
rbac不拥有token、session、cookie、login -
OpenAPI spec covered 不等于 runtime migrated;typed contract 已覆盖
auth接口时,也不能据此认定运行时 ownership 已完成迁移 -
默认管理员账号固定为
graft -
graft-admin只允许作为默认管理员初始化时写入的例外密码,不能作为通用可设置密码 -
首次改密状态必须由后端持久化字段表达,例如
must_change_password -
前端不得通过用户名或默认密码猜测是否需要改密,只能消费后端
login/bootstrap返回的稳定状态 -
普通自助改密固定使用
POST /auth/change-password,必须显式提交current_password + new_password -
首次强制改密固定使用
POST /auth/complete-required-password-change,只接收new_password,不复用普通改密接口的旧密码语义 -
must_change_password=true的受限会话只能访问bootstrap、logout与complete-required-password-change;其余已登录接口由后端直接返回403 -
当前 MVP 的阻断策略由
web负责受限态页面与导航收敛,由server负责受限会话白名单强制约束 -
默认管理员不能是“只能登录但看不到后台菜单”的空账号;最小管理员角色/权限绑定属于同一闭环要求
这一阶段前端优先完成:
- 登录
- token refresh
- 当前用户获取
- 动态菜单
- 动态路由
- permission guard
- API client
- 错误处理
- starter 壳层收敛
这一阶段不以页面数量、CRUD 数量或 UI 丰富程度衡量进度,而以真实契约是否接通衡量进度。
第一阶段优先稳定扩展路径,而不是追求能力面。
当前收敛阶段优先顺序:
- 核心运行时
- 模块管理与 DI
- 菜单、权限、迁移、定时任务注册中心
user、auth与rbac
当前阶段后端模块边界补充冻结为:
-
auth 拆分执行顺序固定为:
- Phase 0:只提交计划与边界文档
- Phase 1:后端 auth 模块骨架 + moduleapi capability
- Phase 2:迁移 token/session/cookie/refresh store
- Phase 3:迁移
/auth/*路由 - Phase 4:前端
modules/auth骨架 + api/types 迁移 - Phase 5:前端 auth store 与页面迁移
- Phase 6:清理兼容 re-export / user 残留 auth 逻辑
-
auth作为独立模块收口:login / refresh / logout / bootstrapchange-password / complete-required-password-change- refresh token、refresh session、cookie、rotation/revoke、restricted-session
-
user收口:- 用户资料读取
- 用户 CRUD / status / delete
- 管理员
reset-password - 按用户维度的 session 治理入口若保留在
user,只能作为调用authcapability 的管理入口,不再直连 auth 持久化
-
rbac继续收口:- role / permission / user-role
- resource/action/permission metadata
- authorizer 与 access snapshot 组装依赖的授权能力
event busaudit与scheduler- 前端后台壳与真实后端契约接入
以下方向明确延后:
- Docker / SSH / 运维模块
- 第三方模块分发
- 热插拔
- 复杂工作流
- 大规模 CRUD 页面扩展
- 高级权限系统
- 中台化能力扩展
补充约束:
event bus采用自研最小进程内实现,目标是模块解耦,不是分布式消息系统audit采用业务贴合型自研最小实现,不引入复杂审计框架scheduler以robfig/cron为底层,但业务模块只能依赖仓库内封装接口- 当前
web禁止继续按 mock 优先、静态菜单优先或演示页优先的方式扩展
如果这套架构是成功的,那么新增一个模块时应该能按固定路径推进:
- 在
server中确定它是core还是module - 在模块侧定义公开服务、权限、菜单、路由和迁移
- 在
web中按menu + route + page + api + permission接入 - 用最小验证证明该链路可编译、可访问、可扩展
在当前 MVP 收敛阶段,还应满足:
- 后端最小闭环的目标边界是
auth + user + rbac + audit + scheduler event bus、审计、调度能力都保持在最小边界,没有提前演化成平台化大工程- 前端已经围绕真实登录、刷新、当前用户、菜单、路由、权限与错误处理链路完成接通
- 默认管理员初始化、首次登录强制改密与
login/bootstrap契约的分工边界保持显式:默认密码只用于初始化例外路径,首次改密真值由后端持久化,普通改密与首次强制改密使用不同接口,登录后阻断由web受限态和server受限会话白名单共同保证 server的完成态默认要经过统一 backend quality chain,不能继续以“只要go test或go build通过就算完成”的松散口径收尾
如果新增模块仍然需要频繁修改核心抽象、引入隐藏耦合,或者让前后端边界变得含糊,说明架构设计仍需调整。