叠甲
本文由 Cursor AI 辅助生成,正在学习 go 中,同时这也是我开始阅读的第一份开源项目的源代码,有很多设计思想值得学习,不懂的地方有很多,如有错误欢迎指正,共同努力。
前情提要
成功用 docker compose 启动了项目,了解数据库表设计,新建了 go 工程的目录。
Day2
今日目标:从 0 学习并搭建 coze 同款 go 后端框架的用户接口层,并且成功启动项目。
DDD思想的四层架构分析
(User Interface Layer)
backend/api/"] --> B["应用服务层
(Application Service Layer)
backend/application/"] B --> C["领域层
(Domain Layer)
backend/domain/"] C --> D["基础设施层
(Infrastructure Layer)
backend/infra/"] A1["API Handler"] --> A A2["Router"] --> A A3["Middleware"] --> A A4["Model/DTO"] --> A B1["Application Services"] --> B B2["Event Bus"] --> B B3["Service Orchestration"] --> B C1["Domain Services"] --> C C2["Domain Entities"] --> C C3["Repository Interfaces"] --> C C4["Domain Events"] --> C D1["Repository Implementations"] --> D D2["External Service Clients"] --> D D3["Database Access"] --> D D4["Cache/Storage"] --> D
1. 用户接口层 (User Interface Layer) - backend/api/
职责: 处理HTTP请求,数据传输对象(DTO)转换,路由管理主要组件:
- Handler: 处理具体的HTTP请求逻辑
- Router: 路由配置和注册
- Middleware: 认证、日志、CORS等横切关注点
- Model: API请求/响应的数据模型
2.应用服务层 (Application Service Layer) - backend/application/
职责: 业务流程编排,跨领域协调,事务管理主要组件:
- Application Services: 具体的应用服务实现
- Service Components: 服务依赖组件
- Event Bus: 事件总线处理
分层设计:
- Basic Services: 仅依赖基础设施的基础服务
- Primary Services: 依赖基础服务的主要业务服务
- Complex Services: 依赖主要服务的复杂业务服务
3.领域层 (Domain Layer) - backend/domain/
职责: 核心业务逻辑,领域实体,业务规则领域划分:
- user/ - 用户管理领域
- workflow/ - 工作流领域
- conversation/ - 对话领域
- knowledge/ - 知识库领域
- plugin/ - 插件领域
- agent/ - 智能体领域
- 等多个业务领域...
每个领域的标准结构:
text
domain/user/
├── entity/ # 领域实体
├── service/ # 领域服务
├── repository/ # 仓储接口
└── internal/ # 内部实现
4.基础设施层 (Infrastructure Layer) - backend/infra/
职责: 技术实现,外部系统集成,数据持久化主要组件:
- Contract: 基础设施接口定义
- Impl: 具体技术实现
技术栈:
- 数据库: MySQL (使用GORM)
- 缓存: Redis
- 存储: TOS/S3/MinIO
- 搜索: Elasticsearch
- 消息队列: Event Bus
- 模型管理: Model Manager
special. 跨领域协作 - backend/crossdomain/
昨天粗略了解了一下 DDD 的设计思想,看到有一点原则就是禁止跨领域调用,领域的依赖都是自上而下的,但是项目中却有跨领域的实现,问了一下 Cursor,他是这么回答的:
工作流领域"] PD["Plugin Domain
插件领域"] KD["Knowledge Domain
知识库领域"] DD["Database Domain
数据库领域"] AD["Agent Domain
智能体领域"] WFD -.->|"直接依赖?
违反DDD原则"| PD WFD -.->|"直接依赖?
违反DDD原则"| KD WFD -.->|"直接依赖?
违反DDD原则"| DD WFD -.->|"直接依赖?
违反DDD原则"| AD end subgraph "跨领域解决方案" CC["Cross-Domain Contract
跨领域契约"] CI["Cross-Domain Implementation
跨领域实现"] WFD --> CC CC --> CI CI --> PD CI --> KD CI --> DD CI --> AD end
🤔 为什么会有跨领域的情况?
1️⃣ 业务复杂性驱动
在Coze Studio这样的AI平台中,业务场景天然具有跨领域特性:典型场景:
- 工作流执行:一个工作流可能需要调用插件、检索知识库、查询数据库、调用智能体
- 智能体对话:智能体需要使用插件工具、访问知识库、记录对话历史
- 应用构建:应用可能集成多个领域的能力
2️⃣ DDD原则约束
严格的DDD原则要求:
- 领域独立性:每个领域应该独立,不直接依赖其他领域
- 防腐层:避免领域间的直接耦合
- 单一职责:每个领域只关注自己的核心业务
问题:如果工作流领域直接依赖插件领域,会导致:
go
// ❌ 违反DDD原则的做法
import "github.com/coze-dev/coze-studio/backend/domain/plugin/service"
type WorkflowService struct {
pluginService plugin.PluginService // 直接依赖其他领域
}
3️⃣ 跨领域解决方案的设计
项目采用了契约-实现分离的跨领域模式:
- 契约层 (Contract)
定义跨领域接口,不包含具体实现:
go
// cross_plugin.go
type PluginService interface {
MGetVersionPlugins(ctx context.Context, versionPlugins []model.VersionPlugin) (plugins []*model.PluginInfo, err error)
ExecuteTool(ctx context.Context, req *model.ExecuteToolRequest, opts ...model.ExecuteToolOpt) (resp *model.ExecuteToolResponse, err error)
// ... 其他方法
}
var defaultSVC PluginService // 全局单例
- 实现层 (Implementation)
桥接具体的领域服务:
go
// plugin.go
type impl struct {
DomainSVC plugin.PluginService // 包装真实的领域服务
}
func InitDomainService(c plugin.PluginService) crossplugin.PluginService {
defaultSVC = &impl{
DomainSVC: c,
}
return defaultSVC
}
4️⃣ 架构优势
这种跨领域设计带来了多个优势:
- 🔒 解耦合
- 工作流领域不直接依赖插件领域
- 通过接口契约进行交互
- 便于单元测试和模拟
- 🔄 可扩展
- 新增领域时,只需定义新的跨领域契约
- 现有领域无需修改
- 🎯 职责清晰
- 每个领域专注自己的核心业务
- 跨领域逻辑统一管理
- 🧪 可测试
- 可以轻松模拟跨领域依赖
- 单元测试更加独立
总结
跨领域的存在是因为:
- 业务需求:AI平台的复杂业务场景天然需要多领域协作
- 架构原则:严格遵循DDD原则,避免领域间直接耦合
- 工程实践:通过契约-实现分离,实现了既满足业务需求又保持架构清洁的解决方案
项目框架分析
- web框架:cloudwego-hertz
- rpc框架:grpc、thrift
- ai 编排框架:cloudwego-eino
- orm 框架:gorm
搭建hertz框架
text
cloudwego-hertz 官网:https://www.cloudwego.io/zh/docs/hertz/overview/
- 初始化 go 环境,go 版本选择 1.24 跟 coze-studio 保持一致
- 根据官网文档安装hertz依赖
- hertz 框架提供了代码生成的能力,coze-studio中的 idl 目录就是当前项目的 thrift 文件
由于以前没有接触过 IDL,做个笔记记录
1. 什么是 IDL
IDL(Interface Definition Language)是一种接口定义语言,用于:
- 定义服务接口(方法签名)
- 定义数据结构(请求/响应格式)
- 支持跨语言的服务通信
- 实现代码自动生成
2.Thrift IDL
这个项目使用的是 Apache Thrift IDL
thrift
// 定义数据结构
struct Base {
1: string LogID = "",
2: string Caller = "",
3: string Addr = "",
// ...
}
// 定义响应结构
struct BaseResp {
1: string StatusMessage = "",
2: i32 StatusCode = 0,
// ...
}
//服务定义
service MessageService {
message.GetMessageListResponse GetMessageList(1: message.GetMessageListRequest request)
message.DeleteMessageResponse DeleteMessage(1: message.DeleteMessageRequest request)
// ...
}
3. 项目中的 IDL 组织结构
text
idl/
├── api.thrift # 主入口文件,聚合所有服务
├── base.thrift # 基础数据结构
├── conversation/ # 对话相关服务
│ ├── message_service.thrift
│ ├── conversation_service.thrift
│ └── ...
├── intelligence/ # AI 智能体服务
├── plugin/ # 插件服务
├── upload/ # 文件上传服务
└── ... # 其他业务模块
DDD-用户接口层:使用 hz 进行代码生成
经过 Cursor 的分析,参考 hertz 的文档,以及 coze-studio 目录下的.hz 文件,可以自己写出这段生成代码
shell
hz new --handler_dir=api/handler --model_dir=api/model --router_dir=api/router -idl idl/api.thrift -module github.com/<your-name>/coze-studio --enable_extends true
这里要注意,一定要开启
enable_extends
,不然只会根据 api.thrift 中的 service 生成空壳 router 文件,因为 idl/api.thrift 文件采用了service IntelligenceService extends intelligence.IntelligenceService {}
extends 的写法,在这里踩坑了好久,最后在 hz 的文档中看到了enable_extends
的属性,开启后成功解决。
生成的文件目录结构:
text
.
├── handler
│ └── coze
│ ├── agent_run_service.go
│ ├── bot_open_api_service.go
│ ├── conversation_service.go
│ ├── database_service.go
│ ├── developer_api_service.go
│ ├── intelligence_service.go
│ ├── knowledge_service.go
│ ├── memory_service.go
│ ├── message_service.go
│ ├── open_apiauth_service.go
│ ├── passport_service.go
│ ├── playground_service.go
│ ├── plugin_develop_service.go
│ ├── public_product_service.go
│ ├── resource_service.go
│ ├── upload_service.go
│ └── workflow_service.go
├── model
│ ├── base
│ │ └── base.go
│ ├── common
│ │ └── common.go
│ ├── conversation
│ │ ├── agentrun
│ │ │ └── agentrun_service.go
│ │ ├── common
│ │ │ └── common.go
│ │ ├── conversation
│ │ │ ├── conversation.go
│ │ │ └── conversation_service.go
│ │ ├── message
│ │ │ ├── message.go
│ │ │ └── message_service.go
│ │ └── run
│ │ └── run.go
│ ├── coze
│ │ └── api.go
│ ├── database
│ │ └── database.go
│ ├── file
│ │ └── upload
│ │ └── upload.go
│ ├── flow
│ │ ├── dataengine
│ │ │ └── dataset
│ │ │ ├── common.go
│ │ │ ├── dataset.go
│ │ │ ├── document.go
│ │ │ ├── flow_dataengine_dataset.go
│ │ │ ├── review.go
│ │ │ └── slice.go
│ │ ├── devops
│ │ │ └── debugger
│ │ │ ├── coze
│ │ │ │ └── flow.devops.debugger.coze.go
│ │ │ └── domain
│ │ │ ├── infra
│ │ │ │ └── infra.go
│ │ │ └── testcase
│ │ │ └── testcase.go
│ │ └── marketplace
│ │ ├── marketplace_common
│ │ │ └── marketplace_common.go
│ │ ├── product_common
│ │ │ └── product_common.go
│ │ └── product_public_api
│ │ └── public_api.go
│ ├── intelligence
│ │ ├── common
│ │ │ ├── common_struct.go
│ │ │ └── intelligence_common_struct.go
│ │ ├── intelligence.go
│ │ └── search.go
│ ├── knowledge
│ │ └── document
│ │ └── kdocument.go
│ ├── kvmemory
│ │ └── kvmemory.go
│ ├── ocean
│ │ └── cloud
│ │ ├── bot_common
│ │ │ └── bot_common.go
│ │ ├── bot_open_api
│ │ │ └── bot_open_api.go
│ │ ├── developer_api
│ │ │ └── developer_api.go
│ │ ├── memory
│ │ │ └── ocean_cloud_memory.go
│ │ ├── playground
│ │ │ ├── playground.go
│ │ │ ├── prompt_resource.go
│ │ │ └── shortcut_command.go
│ │ ├── plugin_develop
│ │ │ └── plugin_develop.go
│ │ └── workflow
│ │ ├── ocean_cloud_workflow.go
│ │ ├── trace.go
│ │ └── workflow.go
│ ├── passport
│ │ └── passport.go
│ ├── permission
│ │ └── openapiauth
│ │ ├── openapiauth.go
│ │ └── openapiauth_service.go
│ ├── plugin_develop_common
│ │ └── plugin_develop_common.go
│ ├── project
│ │ └── project.go
│ ├── project_memory
│ │ └── project_memory.go
│ ├── publish
│ │ └── publish.go
│ ├── resource
│ │ ├── common
│ │ │ └── resource_common.go
│ │ └── resource.go
│ ├── table
│ │ └── table.go
│ ├── task
│ │ └── task.go
│ └── task_struct
│ └── task_struct.go
└── router
├── coze
│ ├── api.go
│ └── middleware.go
└── register.go
57 directories, 73 files
这样就很方便的帮我们生成了 DDD 四层架构的第一层,用户接口层。
到这一步我们的项目代码其实就可以运行起来了,替换一下main.go
中的默认注册路由,替换成/api/router/register.go 中的GeneratedRegister
go
// Code generated by hertz generator.
package main
import (
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/liliagrisina/coze-studio/api/router"
)
func main() {
h := server.Default()
router.GeneratedRegister(h)
h.Spin()
}
执行go run main.go
成功启动我的第一个 hertz 框架项目。