6.4 代码组织与分层约定

代码组织与分层约定

项目按 HTTP 边界、业务规则和持久化职责分层。新增代码应放到承担该职责的包中,不通过跨层直接调用换取短期方便。

后端目录

app/
├── controller/   # HTTP 输入、权限、错误映射和响应
├── dto/          # JSON/Query 请求与响应类型
├── middleware/   # 跨请求认证、工作区和审计
├── service/      # 业务规则、事务和跨资源编排
├── dao/          # GORM 查询与写入
├── model/        # 持久化模型与领域常量
└── constants/    # 通用错误码
config/autoload/  # 默认配置、路由和全局中间件
pkg/helper/       # 容器服务的兼容访问入口
pkg/service/      # 数据库、缓存、迁移、发号器等基础设施
migrations/      # Goose SQL/Go 迁移

依赖方向

Controller → DTO / Middleware / Service
Service    → DTO / Model / DAO / Infrastructure interfaces
DAO        → Model / GORM
Model      → 标准库与必要的值对象依赖

DAO 不调用 Controller,Service 不渲染 HTTP 响应,Model 不从全局容器读取数据库。需要基础服务时,现有 Service 通过 interfaces.HelperInterface 或容器解析出的接口使用。

Controller 约定

  • 使用 RouterContextInterface,先做角色检查,再绑定参数。
  • Path ID 使用 strconv.ParseUint 并在失败时返回 400。
  • 将“资源不存在”“冲突”“无权限”分别映射为 404、409、403。
  • 成功响应使用 Success/SuccessWithMessage,不要手写另一套 JSON 包装。
  • Controller 不开启事务,也不直接写复杂 GORM 查询。
var req dto.TagRequest
if err := c.ShouldBindJSON(&req); err != nil {
    ctrl.Error(c, constants.ErrCodeBadRequest, "请求参数错误: "+err.Error())
    return
}

DTO 与校验

  • JSON 和 Query 字段使用 snake_case。
  • 必填、长度、枚举和数字范围优先放在 binding 标签。
  • 需要区分“未提供”和显式零值时使用指针,例如 *bool*int
  • 时间统一使用 time.Time/*time.Time,日期 Query 明确 YYYY-MM-DD
  • 响应 DTO 不返回密码哈希、完整 Token、密文 Secret 或内部 URL hash。

Service 与事务

Service 负责:

  • 工作区内资源关联校验;
  • 状态机与删除前置条件;
  • URL 安全扫描、重复策略和分流规则;
  • 多 DAO 写入的事务边界;
  • 缓存失效和可控异步任务。

Service 方法优先显式接收 workspaceIDuserID 和请求上下文需要的数据。兼容旧调用的默认工作区包装方法不能用于新 API 路径。

DAO 与工作区隔离

所有工作区资源的查询条件都包含 workspace_id

err := db.
    Where("id = ? AND workspace_id = ? AND deleted_at IS NULL", id, workspaceID).
    First(&item).Error

列表方法明确 offset、limit、筛选和稳定排序。动态列名、表名或排序表达式必须从服务端白名单选择,不能直接拼接用户输入。

基础设施和配置

新配置项在 config/autoload 中提供默认值,并通过点分键读取;环境变量会自动映射为大写下划线。需要新容器依赖时实现 Assembly 并声明 DependsOn,需要启动生命周期时实现 Server,保持装配顺序可解释。

前后端协作

后端路由和 DTO 确定后,在 admin-webui/apps/web-antd/src/api 添加同字段的 TypeScript 类型和请求函数。页面只依赖 API 层,不复制 URL、响应解包或工作区头逻辑。

相关章节:系统架构添加新功能管理端开发