Skip to content

Latest commit

 

History

History
549 lines (415 loc) · 23.1 KB

File metadata and controls

549 lines (415 loc) · 23.1 KB

Graft 架构设计文档

1. 项目定位

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

2. 核心目标

项目要优先满足这几件事:

  • 新增一个后台模块时,接入流程尽量固定化
  • 平台内核与业务模块边界清晰
  • 模块之间可以通过稳定接口协作,而不是直接耦合实现
  • 前端页面风格统一,适合批量扩展标准中后台页面
  • 保持对 AI / MCP 辅助开发友好,便于快速生成和调整模块代码

明确不追求的目标:

  • 一开始就做运行时热插拔
  • 一开始就支持第三方动态分发模块市场
  • 一开始就做重量级微服务拆分

3. 技术选型

日志类别与高频诊断

运行时日志仍以 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 checkjust lintjust 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 不得各自维护第二套 blocking golangci-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 nolint is 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 testcd server && go test ./...
  • backend lint issue 如当前切片无法立即清理,只能通过 active tracking 文档中的 controlled exception 或 narrow temporary nolint 暂留,并记录来源、影响、保留原因、负责人或上下文与下一步清理条件。
  • 该质量基线默认治理 server 的手写代码;generated、third-party 与 migration 产物应通过显式配置与 豁免边界处理,而不是通过放宽手写代码规则来换取通过。

4. 总体架构

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

架构原则:

  • 核心运行时只提供公共能力,不承载具体业务
  • 业务全部通过编译期模块注册到平台
  • 模块通过公开接口交互,不直接依赖彼此内部实现
  • 前后端都按“模块化接入”组织

4.1 平台化扩展边界

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。


5. 后端分层设计

5.1 核心模块

后端核心建议分为这些包:

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

5.2 运行流程

应用执行顺序固定为:

先执行显式 schema / migration CLI 步骤,再启动应用:

  1. 加载配置
  2. 初始化数据库连接配置
  3. 执行 Atlas versioned migrations CLI

应用启动顺序固定为:

  1. 加载配置
  2. 初始化日志
  3. 初始化平台级 i18n 服务
  4. 初始化数据库
  5. 初始化 HTTP 服务
  6. 初始化核心注册中心与服务容器
  7. 注册模块
  8. 校验模块依赖并排序
  9. 执行模块 Register
  10. 冻结 i18n 等声明式注册面
  11. 执行模块 Boot
  12. 启动 HTTP 服务

关闭顺序反向执行,优先停止对外服务,再逐步释放模块资源与核心资源。


6. 模块系统设计

6.1 模块接口与 compile-time authority

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() 结果执行排序和生命周期编排。

6.2 生命周期语义

  • Register 负责声明和注册能力,不做长时间运行逻辑;模块面向用户的消息资源也应在这一阶段完成注册
  • Boot 负责真正启动运行时行为,例如事件监听、定时任务、后台任务;此时不得再追加新的 i18n 注册
  • Shutdown 负责资源回收和停止行为;模块应优先使用 module.Context.LifecycleContext 中注入的有界关闭上下文停止后台任务,而不是自行回退到 context.Background()

6.3 模块注册能力(历史 plugin 命名)

模块可以注册这些内容:

  • HTTP 路由
  • 菜单
  • 权限点
  • message bundles / message keys
  • 定时任务
  • 事件监听器
  • 对外公开的服务接口
  • 配置说明

6.4 依赖规则

模块依赖分两层:

  • 模块依赖:通过 compile-time ModuleSpec.Dependencies 声明
  • 服务依赖:通过容器解析公开接口

模块上下文与跨模块公开边界不直接暴露具体 ORM 句柄。

数据访问统一通过中性的 repository / store factory 边界进入,由核心运行时负责把底层 Ent client 装配到模块内部实现中。

模块上下文还应显式提供平台统一的日志与 i18n 能力:

  • 日志统一复用 core 创建的 Zap logger,不允许模块各自维护分裂的日志框架
  • i18n 能力统一经由 server/internal/i18n facade 暴露;模块不直接依赖未来可能替换的底层实现细节
  • 模块面向用户返回的稳定错误响应应优先输出 message_key + localized message,而不是只拼接硬编码字符串

运行时管理器必须支持:

  • 依赖拓扑排序
  • 缺失依赖报错
  • 循环依赖报错
  • 重复模块报错

6.5 Realtime 与后台采样边界

平台级实时推送能力属于 core runtime,不属于某一个业务模块私有实现。

固定分层如下:

  • server/internal/realtime 负责统一 topic、发布订阅 hub、WebSocket / SSE gateway、ticket 消费和连接生命周期
  • server/modules/<name>/... 负责把模块自有事件、日志或资源快照发布到 canonical topic
  • web 先通过普通 HTTP 鉴权接口申请单次 realtime ticket,再通过统一 WebSocket 或 SSE gateway 订阅 topic

当前 canonical topic 形态固定为:

  • container.stats:<id>
  • container.logs:<id>
  • container.events:<id>
  • audit.events
  • system.events
  • platform.update.operations:<operationID>

规则:

  • 模块不得再各自发明第二套 WebSocket 或 SSE 入口用于普通订阅式事件推送;只允许向其 canonical topic 发布受控 payload
  • WebSocket 与 SSE 共用 topic、ticket、权限和资源范围语义。WebSocket 用于双向会话,SSE 用于服务端单向状态流;两者都必须由 server/internal/realtime gateway 消费一次性 ticket
  • 需要持续采样的能力,例如容器资源统计,应在模块 Boot 阶段启动后台 collector,再向 realtime topic 发布结果
  • request-time one-shot 采样不是实时资源展示的 authority,不再作为长期主路径

7. 依赖注入与 IoC 决策

最终结论:

  • 引入轻量 DI / 服务注册
  • 不引入重量级 IoC 容器
  • 不使用基于反射的自动装配、包扫描、tag 注入
  • 保持 Go 风格的显式构造与显式依赖传递

这样做的原因:

  • 模块化平台需要统一注册与解析入口
  • 但仓库不希望牺牲可读性和可调试性去换取隐式魔法
  • 对 AI 辅助开发而言,显式 wiring 比隐藏框架行为更稳定

判断标准:

  • 新模块能清楚声明依赖并完成接入
  • 运行时依赖错误能直接定位到注册或解析路径
  • 平台不会因为容器抽象过重而模糊 coremodule 的边界

8. 前端与后端协作边界

前后端协作要围绕统一的扩展路径组织,而不是各自发散演进。

一个新能力的最小接入链路应保持显式:

  • 后端模块
  • 权限点
  • 菜单元信息
  • API
  • 前端模块
  • 页面入口

这条链路必须可追踪、可验证、可复用。

前端不应发明一套脱离后端模块语义的独立权限体系;后端也不应只暴露接口而不提供菜单和权限元信息。

当前 MVP 收敛阶段采用:

  • 后端主导真实契约稳定
  • 前端围绕真实契约接入与壳层收敛

当前阶段的 web 不是后台业务页面扩展阶段,而是基础壳层与真实契约接通阶段。

契约治理补充约束:

  • 高风险共享契约的真值统一遵循 契约治理与魔法值治理规范
  • permission codeerror codemessage keyevent nameroute name/pathstorage keyheader 与其它跨边界稳定值,不得继续以散落字面量作为长期真值
  • 当前阶段优先建立 canonical 归属、typed contract 边界、lifecycle 与 compatibility 规则,再逐步补 drift detection、hook 与 CI
  • 同一语义只允许一个 canonical contract;兼容期 alias 可以存在,但不能演变为第二套长期真值

当前认证治理补充约束:

  • authuserrbacresource 的职责边界固定如下:

    • auth 回答“你是谁”,拥有登录、token、session、cookie、bootstrap、受限会话与自助改密生命周期
    • user 不等于 auth,只拥有用户资料与用户管理能力
    • rbac 回答“你能做什么”,拥有 role、permission、user-role 与授权判定
    • resource 只表达“你要访问什么”的被保护对象概念边界;当前阶段不拆独立模块,继续由 rbac 承载 resource/action/permission metadata
  • auth 不拥有 resourcepermissionrole

  • rbac 不拥有 tokensessioncookielogin

  • 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 的受限会话只能访问 bootstraplogoutcomplete-required-password-change;其余已登录接口由后端直接返回 403

  • 当前 MVP 的阻断策略由 web 负责受限态页面与导航收敛,由 server 负责受限会话白名单强制约束

  • 默认管理员不能是“只能登录但看不到后台菜单”的空账号;最小管理员角色/权限绑定属于同一闭环要求

这一阶段前端优先完成:

  • 登录
  • token refresh
  • 当前用户获取
  • 动态菜单
  • 动态路由
  • permission guard
  • API client
  • 错误处理
  • starter 壳层收敛

这一阶段不以页面数量、CRUD 数量或 UI 丰富程度衡量进度,而以真实契约是否接通衡量进度。


9. 第一阶段优先级

第一阶段优先稳定扩展路径,而不是追求能力面。

当前收敛阶段优先顺序:

  1. 核心运行时
  2. 模块管理与 DI
  3. 菜单、权限、迁移、定时任务注册中心
  4. userauthrbac

当前阶段后端模块边界补充冻结为:

  • 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 / bootstrap
    • change-password / complete-required-password-change
    • refresh token、refresh session、cookie、rotation/revoke、restricted-session
  • user 收口:

    • 用户资料读取
    • 用户 CRUD / status / delete
    • 管理员 reset-password
    • 按用户维度的 session 治理入口若保留在 user,只能作为调用 auth capability 的管理入口,不再直连 auth 持久化
  • rbac 继续收口:

    • role / permission / user-role
    • resource/action/permission metadata
    • authorizer 与 access snapshot 组装依赖的授权能力
  1. event bus
  2. auditscheduler
  3. 前端后台壳与真实后端契约接入

以下方向明确延后:

  • Docker / SSH / 运维模块
  • 第三方模块分发
  • 热插拔
  • 复杂工作流
  • 大规模 CRUD 页面扩展
  • 高级权限系统
  • 中台化能力扩展

补充约束:

  • event bus 采用自研最小进程内实现,目标是模块解耦,不是分布式消息系统
  • audit 采用业务贴合型自研最小实现,不引入复杂审计框架
  • schedulerrobfig/cron 为底层,但业务模块只能依赖仓库内封装接口
  • 当前 web 禁止继续按 mock 优先、静态菜单优先或演示页优先的方式扩展

10. 成功标准

如果这套架构是成功的,那么新增一个模块时应该能按固定路径推进:

  1. server 中确定它是 core 还是 module
  2. 在模块侧定义公开服务、权限、菜单、路由和迁移
  3. web 中按 menu + route + page + api + permission 接入
  4. 用最小验证证明该链路可编译、可访问、可扩展

在当前 MVP 收敛阶段,还应满足:

  • 后端最小闭环的目标边界是 auth + user + rbac + audit + scheduler
  • event bus、审计、调度能力都保持在最小边界,没有提前演化成平台化大工程
  • 前端已经围绕真实登录、刷新、当前用户、菜单、路由、权限与错误处理链路完成接通
  • 默认管理员初始化、首次登录强制改密与 login/bootstrap 契约的分工边界保持显式:默认密码只用于初始化例外路径,首次改密真值由后端持久化,普通改密与首次强制改密使用不同接口,登录后阻断由 web 受限态和 server 受限会话白名单共同保证
  • server 的完成态默认要经过统一 backend quality chain,不能继续以“只要 go testgo build 通过就算完成”的松散口径收尾

如果新增模块仍然需要频繁修改核心抽象、引入隐藏耦合,或者让前后端边界变得含糊,说明架构设计仍需调整。