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 方法优先显式接收 workspaceID、userID 和请求上下文需要的数据。兼容旧调用的默认工作区包装方法不能用于新 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、响应解包或工作区头逻辑。