微信扫码
添加专属顾问
我要投稿
Golang微服务架构如何结合DDD应对复杂AI业务?本文以千万级教育App为例,详解从理论到实践的完整方案。 核心内容: 1. DDD战略设计与战术设计核心概念解析 2. 基于DDD的Golang微服务目录结构设计 3. 教育平台各子域的具体实现方案
目前在开发的AI应用,业务比较复杂,用户量在千万级别,业务复杂度也在不断增长,我们构建了一整套完善的微服务架构体系,同时也在践行领域驱动设计(Domain-Driven Design,DDD),DDD作为一种软件设计方法论,为我们提供了应对复杂业务场景的有效解决方案。
DDD强调以业务领域为核心,通过深入理解业务逻辑来指导软件架构设计,从而构建出真正贴合业务需求的系统。
在Golang生态中,凭借其简洁的语法、强大的并发特性和丰富的工具链,Go为DDD的落地实践提供了理想的技术土壤。
本文就以一个大型C端教育App为例,深入探讨如何在Golang微服务架构中实践DDD思想,从理论概念到具体实现,全面展示DDD在真实项目中的应用价值。
领域(Domain):业务所涉及的整个问题空间。在教育平台中,包括用户管理、内容管理、学习评估、支付结算等多个子领域。
子域(Subdomain):将复杂领域分解为更小、更聚焦的业务区域。可分为核心域(Core Domain)、支撑域(Supporting Domain)和通用域(Generic Domain)。
限界上下文(Bounded Context):明确定义的边界,在这个边界内,领域模型具有特定的含义。不同上下文中的同一概念可能有不同的定义和行为。
上下文映射(Context Mapping):描述不同限界上下文之间的关系和集成方式,包括共享内核、客户方-供应方、防腐层等模式。
实体(Entity):具有唯一标识且生命周期较长的领域对象,其标识在整个生命周期内保持不变。
值对象(Value Object):没有唯一标识,通过属性值来区分的不可变对象,主要用于描述实体的特征。
聚合(Aggregate):一组相关实体和值对象的集合,通过聚合根统一管理,确保业务规则的一致性。
领域服务(Domain Service):不属于特定实体或值对象的业务逻辑,通常涉及多个聚合的协调操作。
仓储(Repository):提供类似集合的接口来访问聚合,封装数据访问的技术细节。
领域事件(Domain Event):领域中发生的重要业务事件,用于实现不同聚合间的松耦合通信。
基于DDD思想,我们设计了相当清晰的目录结构,体现了分层架构和领域边界(已列举出核心目录结构):
education-platform/
├── api/ # API定义层
│ ├── user/v1/ # 用户服务API
│ │ ├── user.proto # 用户相关接口定义
│ │ ├── user_grpc.pb.go # gRPC代码生成
│ │ └── user_http.pb.go # HTTP转换代码
│ ├── content/v1/ # 内容服务API
│ │ ├── content.proto # 内容管理接口
│ │ ├── content_grpc.pb.go # gRPC服务代码
│ │ └── content_http.pb.go # HTTP路由代码
│ ├── assessment/v1/ # 评估服务API
│ │ ├── assessment.proto # 评估相关接口
│ │ ├── assessment_grpc.pb.go
│ │ └── assessment_http.pb.go
│ ├── payment/v1/ # 支付服务API
│ │ ├── payment.proto # 支付接口定义
│ │ ├── payment_grpc.pb.go
│ │ └── payment_http.pb.go
│ └── session/v1/ # 会话服务API
│ ├── session.proto # 会话管理接口
│ ├── session_grpc.pb.go
│ └── session_http.pb.go
├── internal/ # 内部实现
│ ├── biz/ # 业务逻辑层(领域层)
│ │ ├── user.go # 用户领域服务
│ │ ├── content.go # 内容领域服务
│ │ ├── assessment.go # 评估领域服务
│ │ ├── ai_dialogue.go # AI对话领域服务
│ │ └── domain/ # 领域模型
│ │ ├── user/ # 用户聚合
│ │ │ ├── entity.go # 用户实体
│ │ │ ├── value_object.go # 值对象
│ │ │ └── repository.go # 仓储接口
│ │ ├── content/ # 内容聚合
│ │ └── assessment/ # 评估聚合
│ ├── data/ # 数据访问层
│ │ ├── user.go # 用户数据访问实现
│ │ ├── content.go # 内容数据访问实现
│ │ ├── model/ # 数据模型
│ │ └── services/ # 外部服务客户端
│ ├── service/ # 应用服务层
│ │ ├── user.go # 用户应用服务
│ │ ├── content.go # 内容应用服务
│ │ └── assessment.go # 评估应用服务
│ ├── server/ # 服务器配置
│ │ ├── http.go # HTTP服务器
│ │ ├── grpc.go # gRPC服务器
│ │ └── middleware/ # 中间件
│ └── conf/ # 配置定义
├── cmd/ # 应用入口
│ ├── education-platform/ # 主服务入口
│ └── education-platform-job/ # 任务服务入口,Cron任务目录
├── configs/ # 配置文件(配置中心)
└── pkg/ # 共享包
├── domain/ # 领域基础设施
├── utils/ # 工具类
└── middleware/ # 公共中间件
目录严格遵循了DDD的分层架构原则,确保了代码的高内聚、低耦合。
下面的代码里几乎每段、每行都有注释,方便大家理解。
Protocol Buffers作为服务间通信的契约,定义了限界上下文之间的接口边界。在设计proto文件时,我们严格按照业务领域来组织服务和消息结构,每个proto文件代表一个特定的业务上下文。
// api/user/v1/user.proto
syntax = "proto3";
package api.user.v1;
import "google/api/annotations.proto";
import "validate/validate.proto";
import "google/protobuf/empty.proto";
option go_package = "education-platform/api/user/v1;v1";
service User {
// 用户注册
rpc Register(RegisterUserReq) returns (RegisterUserResp) {
option (google.api.http) = {
post: "/api/v1/user/register"
body: "*"
};
}
// 获取用户档案
rpc GetProfile(GetUserProfileReq) returns (GetUserProfileResp) {
option (google.api.http) = {
get: "/api/v1/user/profile"
};
}
// 更新用户档案
rpc UpdateProfile(UpdateUserProfileReq) returns (UpdateUserProfileResp) {
option (google.api.http) = {
post: "/api/v1/user/profile"
body: "*"
};
}
// 获取用户学习进度
rpc GetLearningProgress(GetLearningProgressReq) returns (GetLearningProgressResp) {
option (google.api.http) = {
get: "/api/v1/user/learning_progress"
};
}
}
message RegisterUserReq {
string name = 1 [(validate.rules).string = {min_len: 1, max_len: 50}];
string email = 2 [(validate.rules).string.email = true];
string phone = 3 [(validate.rules).string.pattern = "^1[3-9]\\d{9}$"];
int32 grade_id = 4 [(validate.rules).int32 = {gte: 1, lte: 12}];
string avatar = 5;
}
message RegisterUserResp {
string user_id = 1;
string name = 2;
string email = 3;
int32 grade_id = 4;
string created_at = 5;
}
message GetUserProfileReq {
string user_id = 1 [(validate.rules).string.min_len = 1];
}
message GetUserProfileResp {
string user_id = 1;
string name = 2;
string email = 3;
string phone = 4;
string avatar = 5;
int32 grade_id = 6;
string grade_name = 7;
repeated string permissions = 8;
}
// api/content/v1/content.proto
syntax = "proto3";
package api.content.v1;
import "google/api/annotations.proto";
import "validate/validate.proto";
import "google/protobuf/empty.proto";
option go_package = "education-platform/api/content/v1;v1";
service Content {
// 内容搜索
rpc SearchContent(SearchContentReq) returns (SearchContentResp) {
option (google.api.http) = {
post: "/api/v1/content/search"
body: "*"
};
}
// 获取推荐内容
rpc GetRecommendations(GetRecommendationsReq) returns (GetRecommendationsResp) {
option (google.api.http) = {
get: "/api/v1/content/recommendations"
};
}
// 内容详情
rpc GetContentDetail(GetContentDetailReq) returns (GetContentDetailResp) {
option (google.api.http) = {
get: "/api/v1/content/detail"
};
}
}
message SearchContentReq {
string query = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];
int32 subject_id = 2 [(validate.rules).int32.gte = 0];
int32 grade_id = 3 [(validate.rules).int32.gte = 0];
repeated string content_types = 4;
int32 page = 5 [(validate.rules).int32.gte = 1];
int32 limit = 6 [(validate.rules).int32 = {gte: 1, lte: 100}];
}
message ContentItem {
string content_id = 1;
string title = 2;
string description = 3;
string content_type = 4;
int32 subject_id = 5;
int32 grade_id = 6;
int32 difficulty = 7;
repeated string tags = 8;
string created_at = 9;
}
message SearchContentResp {
repeated ContentItem items = 1;
int32 total = 2;
int32 page = 3;
int32 limit = 4;
}
实体是DDD中最核心的概念之一,它代表了业务中具有唯一标识和生命周期的对象。
在设计实体时,我们需要特别注意业务规则的封装,确保实体的不变性约束。值对象则用于描述实体的属性特征,它们是不可变的,通过值相等性来判断是否相同。
在Golang中,我们通过结构体和方法来实现实体和值对象,并通过包级别的可见性来控制访问权限。
// internal/biz/domain/user/entity.go
package user
import (
"errors"
"time"
)
// User 用户实体
type User struct {
id UserID // 用户唯一标识
profile *UserProfile // 用户档案(值对象)
grade *Grade // 年级信息(值对象)
permissions []Permission // 权限列表
createdAt time.Time
updatedAt time.Time
}
// UserID 用户标识值对象
type UserID struct {
value string
}
func NewUserID(value string) (*UserID, error) {
if value == "" {
returnnil, errors.New("用户ID不能为空")
}
return &UserID{value: value}, nil
}
func (id UserID) String() string {
return id.value
}
// UserProfile 用户档案值对象
type UserProfile struct {
name string
email string
phone string
avatar string
}
func NewUserProfile(name, email, phone, avatar string) (*UserProfile, error) {
if name == "" {
returnnil, errors.New("用户姓名不能为空")
}
// 其他验证逻辑...
return &UserProfile{
name: name,
email: email,
phone: phone,
avatar: avatar,
}, nil
}
// 业务方法
func (u *User) UpdateProfile(profile *UserProfile) error {
if profile == nil {
return errors.New("用户档案不能为空")
}
u.profile = profile
u.updatedAt = time.Now()
returnnil
}
func (u *User) UpgradeGrade(newGrade *Grade) error {
if newGrade == nil {
return errors.New("年级信息不能为空")
}
// 业务规则:只能升级到更高年级
if u.grade != nil && newGrade.Level() <= u.grade.Level() {
return errors.New("只能升级到更高年级")
}
u.grade = newGrade
u.updatedAt = time.Now()
returnnil
}
聚合是DDD中最重要的战术模式之一,它定义了数据一致性的边界。聚合内的对象必须作为一个整体来修改。聚合根是聚合的入口点,外部只能通过聚合根来访问聚合内的其他对象。
这也是我们常说的biz层(业务逻辑层的定义)。
// internal/biz/domain/session/aggregate.go
package session
import (
"errors"
"time"
)
// LearningSession 学习会话聚合根
type LearningSession struct {
id SessionID
userID UserID
subject Subject
messages []*Message
status SessionStatus
metadata *SessionMetadata
createdAt time.Time
updatedAt time.Time
}
// AddMessage 添加消息(聚合内的业务规则)
func (s *LearningSession) AddMessage(content string, msgType MessageType) (*Message, error) {
if s.status == SessionStatusClosed {
returnnil, errors.New("已关闭的会话不能添加消息")
}
// 检查消息频率限制
if err := s.checkMessageRateLimit(); err != nil {
returnnil, err
}
message := NewMessage(content, msgType, s.userID)
s.messages = append(s.messages, message)
s.updatedAt = time.Now()
// 发布领域事件
s.publishEvent(NewMessageAddedEvent(s.id, message.ID()))
return message, nil
}
// checkMessageRateLimit 检查消息频率限制(业务规则)
func (s *LearningSession) checkMessageRateLimit() error {
now := time.Now()
recentMessages := 0
for _, msg := range s.messages {
if now.Sub(msg.CreatedAt()) < time.Minute {
recentMessages++
}
}
if recentMessages >= 10 {
return errors.New("发送消息过于频繁,请稍后再试")
}
returnnil
}
// CloseSession 关闭会话
func (s *LearningSession) CloseSession() error {
if s.status == SessionStatusClosed {
return errors.New("会话已经关闭")
}
s.status = SessionStatusClosed
s.updatedAt = time.Now()
// 发布会话关闭事件
s.publishEvent(NewSessionClosedEvent(s.id, s.userID))
returnnil
}
领域服务通常涉及多个聚合的协调操作。领域服务是无状态的,它接收领域对象作为参数,执行业务逻辑,并返回结果或修改传入的对象。
在实现领域服务时,我们需要确保服务的职责单一,避免服务变得过于庞大。同时,领域服务应该表达清晰的业务概念,使代码更具可读性。
biz层,对上层供Service层调用。
// internal/biz/ai_dialogue.go
package biz
import (
"context"
"fmt"
)
// AIDialogueService AI对话领域服务
type AIDialogueService struct {
sessionRepo SessionRepository
userRepo UserRepository
contentRepo ContentRepository
aiModelService AIModelService
securityService SecurityService
log Logger
}
// ProcessUserQuery 处理用户查询(领域服务方法)
func (s *AIDialogueService) ProcessUserQuery(
ctx context.Context,
userID UserID,
query string,
) (*DialogueResult, error) {
// 1. 获取用户信息和权限
user, err := s.userRepo.GetByID(ctx, userID)
if err != nil {
returnnil, fmt.Errorf("获取用户信息失败: %w", err)
}
// 2. 内容安全检查
if !s.securityService.IsContentSafe(ctx, query) {
returnnil, errors.New("输入内容包含敏感信息")
}
// 3. 意图识别和分类
intention, err := s.classifyUserIntention(ctx, query, user)
if err != nil {
returnnil, fmt.Errorf("意图识别失败: %w", err)
}
// 4. 根据意图选择处理策略
processor := s.selectProcessor(intention)
// 5. 执行处理逻辑
result, err := processor.Process(ctx, &ProcessRequest{
UserID: userID,
Query: query,
Intention: intention,
User: user,
})
if err != nil {
returnnil, fmt.Errorf("处理用户查询失败: %w", err)
}
return result, nil
}
// classifyUserIntention 意图识别(领域逻辑)
func (s *AIDialogueService) classifyUserIntention(
ctx context.Context,
query string,
user *User,
) (*UserIntention, error) {
// 基于用户历史行为和当前输入进行意图分析
history, err := s.sessionRepo.GetUserRecentSessions(ctx, user.ID(), 5)
if err != nil {
s.log.Warnf("获取用户历史会话失败: %v", err)
}
// 调用AI模型进行意图识别
intentionResult, err := s.aiModelService.ClassifyIntention(ctx, &IntentionRequest{
Query: query,
History: history,
UserContext: &UserContext{
Grade: user.Grade(),
Subject: user.PreferredSubject(),
},
})
if err != nil {
returnnil, err
}
return NewUserIntention(intentionResult), nil
}
仓储模式是DDD中连接领域模型和数据持久化的桥梁。仓储接口在领域层(biz层)定义,具体实现在基础设施层(data层)。
这种设计遵循了依赖倒置原则,使得领域层不依赖于具体的数据访问技术。
在实现仓储时,我们需要注意聚合的完整性加载和保存,确保聚合边界内的数据一致性。同时,仓储应该隐藏数据访问的复杂性,为领域层提供简洁的接口(增删改查等)。
// internal/biz/user.go - 仓储接口定义(在biz层)
package biz
import"context"
// UserRepository 用户仓储接口(领域层定义)
type UserRepository interface {
GetByID(ctx context.Context, id UserID) (*User, error)
GetByEmail(ctx context.Context, email string) (*User, error)
Save(ctx context.Context, user *User) error
Delete(ctx context.Context, id UserID) error
FindByGrade(ctx context.Context, grade Grade) ([]*User, error)
}
// ContentRepository 内容仓储接口
type ContentRepository interface {
GetContentByType(ctx context.Context, contentType ContentType) ([]*Content, error)
SaveContent(ctx context.Context, content *Content) error
SearchContent(ctx context.Context, query *ContentQuery) (*ContentSearchResult, error)
}
数据层的仓储实现需要处理领域对象与持久化对象之间的转换,这是一个关键的技术细节。
我们使用了适配器模式来实现这种转换,确保领域模型的纯净性不受数据库模式的影响。
在实现仓储时,我们还需要考虑性能优化,如批量操作、缓存策略等。
同时,错误处理也很重要,需要将底层的技术错误转换为领域层能够理解的业务错误。
// internal/data/user.go - 仓储实现(在data层)
package data
import (
"context"
"database/sql/driver"
"encoding/json"
"gorm.io/gorm"
"github.com/jinzhu/copier"
"education-platform/internal/biz"
)
type userRepo struct {
data *Data
}
func NewUserRepo(data *Data) biz.UserRepository {
return &userRepo{data: data}
}
// UserPO 用户持久化对象
type UserPO struct {
ID string `gorm:"primaryKey"`
Name string `gorm:"not null"`
Email string `gorm:"unique;not null"`
Phone string
Avatar string
GradeID int32
Profile JSON `gorm:"type:json"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt
}
// JSON 自定义JSON类型
type JSON map[string]interface{}
func (j JSON) Value() (driver.Value, error) {
return json.Marshal(j)
}
func (j *JSON) Scan(value interface{}) error {
bytes, ok := value.([]byte)
if !ok {
return errors.New("类型断言失败")
}
return json.Unmarshal(bytes, j)
}
// GetByID 根据ID获取用户
func (r *userRepo) GetByID(ctx context.Context, id biz.UserID) (*biz.User, error) {
var userPO UserPO
err := r.data.db.WithContext(ctx).Where("id = ?", id.String()).First(&userPO).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
returnnil, biz.ErrUserNotFound
}
returnnil, err
}
return r.poToDomain(&userPO)
}
// Save 保存用户
func (r *userRepo) Save(ctx context.Context, user *biz.User) error {
userPO := r.domainToPO(user)
return r.data.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// 保存用户基本信息
if err := tx.Save(userPO).Error; err != nil {
return err
}
// 处理关联数据
if err := r.saveUserPermissions(tx, user); err != nil {
return err
}
returnnil
})
}
// poToDomain 持久化对象转领域对象
func (r *userRepo) poToDomain(po *UserPO) (*biz.User, error) {
userID, err := biz.NewUserID(po.ID)
if err != nil {
returnnil, err
}
profile, err := biz.NewUserProfile(po.Name, po.Email, po.Phone, po.Avatar)
if err != nil {
returnnil, err
}
grade, err := biz.NewGrade(po.GradeID)
if err != nil {
returnnil, err
}
return biz.NewUser(userID, profile, grade), nil
}
应用服务层是DDD架构中的重要组成部分,它作为外部接口和领域逻辑的协调者,负责编排用例的执行流程。
应用服务本身不包含业务逻辑,而是通过调用领域对象和领域服务来完成业务操作。
在Golang中,应用服务通常对应gRPC或HTTP的服务实现,它需要处理参数验证、错误转换、事务管理等技术细节。使用copier.Copy进行对象转换是一种高效的方式,可以减少手动映射的代码量。
// internal/service/user.go
package service
import (
"context"
"github.com/jinzhu/copier"
pb "education-platform/api/user/v1"
"education-platform/internal/biz"
)
type UserService struct {
pb.UnimplementedUserServer
userUsecase *biz.UserUsecase
contentUsecase *biz.ContentUsecase
log Logger
}
func NewUserService(
userUsecase *biz.UserUsecase,
contentUsecase *biz.ContentUsecase,
logger Logger,
) *UserService {
return &UserService{
userUsecase: userUsecase,
contentUsecase: contentUsecase,
log: NewHelper(logger),
}
}
// GetUserProfile 获取用户档案
func (s *UserService) GetProfile(
ctx context.Context,
req *pb.GetUserProfileReq,
) (*pb.GetUserProfileResp, error) {
// 参数验证
if req.UserId == "" {
returnnil, pb.ErrorInvalidParameter("用户ID不能为空")
}
// 调用用例层
var bizReq biz.GetUserProfileRequest
if err := copier.Copy(&bizReq, req); err != nil {
s.log.WithContext(ctx).Errorf("用户档案请求参数转换错误: %v", err)
returnnil, pb.ErrorParamValidator("参数转换错误").WithCause(err)
}
profile, err := s.userUsecase.GetUserProfile(ctx, &bizReq)
if err != nil {
s.log.WithContext(ctx).Errorf("获取用户档案失败: %v", err)
returnnil, pb.ErrorInternalError("获取用户档案失败").WithCause(err)
}
// 转换为响应对象
var resp pb.GetUserProfileResp
if err := copier.Copy(&resp, profile); err != nil {
s.log.WithContext(ctx).Errorf("用户档案响应转换错误: %v", err)
returnnil, pb.ErrorInternalError("响应转换错误").WithCause(err)
}
return &resp, nil
}
// UpdateProfile 更新用户档案
func (s *UserService) UpdateProfile(
ctx context.Context,
req *pb.UpdateUserProfileReq,
) (*pb.UpdateUserProfileResp, error) {
// 使用copier进行参数转换
var bizReq biz.UpdateUserProfileRequest
if err := copier.Copy(&bizReq, req); err != nil {
s.log.WithContext(ctx).Errorf("更新用户档案请求参数转换错误: %v", err)
returnnil, pb.ErrorParamValidator("参数转换错误").WithCause(err)
}
// 调用用例层
result, err := s.userUsecase.UpdateUserProfile(ctx, &bizReq)
if err != nil {
s.log.WithContext(ctx).Errorf("更新用户档案失败: %v", err)
returnnil, pb.ErrorInternalError("更新用户档案失败").WithCause(err)
}
// 转换响应
var resp pb.UpdateUserProfileResp
if err := copier.Copy(&resp, result); err != nil {
s.log.WithContext(ctx).Errorf("更新用户档案响应转换错误: %v", err)
returnnil, pb.ErrorInternalError("响应转换错误").WithCause(err)
}
return &resp, nil
}
// Register 用户注册
func (s *UserService) Register(
ctx context.Context,
req *pb.RegisterUserReq,
) (*pb.RegisterUserResp, error) {
var bizReq biz.RegisterUserRequest
if err := copier.Copy(&bizReq, req); err != nil {
s.log.WithContext(ctx).Errorf("用户注册请求参数转换错误: %v", err)
returnnil, pb.ErrorParamValidator("参数转换错误").WithCause(err)
}
user, err := s.userUsecase.RegisterUser(ctx, &bizReq)
if err != nil {
s.log.WithContext(ctx).Errorf("用户注册失败: %v", err)
returnnil, pb.ErrorInternalError("用户注册失败").WithCause(err)
}
var resp pb.RegisterUserResp
if err := copier.Copy(&resp, user); err != nil {
s.log.WithContext(ctx).Errorf("用户注册响应转换错误: %v", err)
returnnil, pb.ErrorInternalError("响应转换错误").WithCause(err)
}
return &resp, nil
}
数据访问层负责实现仓储接口,处理与外部数据源的交互。
在微服务架构中,数据访问不仅包括数据库操作,还包括对其他微服务的调用。
使用copier.Copy进行对象转换可以大大简化代码,减少手动映射的错误(项目中大量使用)。
在实现数据访问层时,我们需要注意错误处理、性能优化、事务管理等方面。同时,要确保数据访问层的实现不会泄露到领域层,保持架构的清洁性。
// internal/data/content.go
package data
import (
"context"
"github.com/jinzhu/copier"
contentV1 "education-platform/api/content/v1"
"education-platform/internal/biz"
)
type contentRepo struct {
data *Data
client contentV1.ContentClient
}
func NewContentRepo(data *Data, client contentV1.ContentClient) biz.ContentRepository {
return &contentRepo{
data: data,
client: client,
}
}
// SearchContent 内容搜索
func (r *contentRepo) SearchContent(
ctx context.Context,
query *biz.ContentQuery,
) (*biz.ContentSearchResult, error) {
// 业务对象转换为外部服务请求
var req contentV1.SearchContentReq
if err := copier.Copy(&req, query); err != nil {
returnnil, fmt.Errorf("搜索请求参数转换错误: %w", err)
}
// 调用外部内容服务
resp, err := r.client.SearchContent(ctx, &req)
if err != nil {
returnnil, fmt.Errorf("调用内容搜索服务失败: %w", err)
}
// 外部响应转换为业务对象
var result biz.ContentSearchResult
if err := copier.Copy(&result, resp); err != nil {
returnnil, fmt.Errorf("搜索响应转换错误: %w", err)
}
return &result, nil
}
// GetRecommendations 获取推荐内容
func (r *contentRepo) GetRecommendations(
ctx context.Context,
userID biz.UserID,
preferences *biz.UserPreferences,
) ([]*biz.Content, error) {
// 构建推荐请求
req := &contentV1.GetRecommendationsReq{
UserId: userID.String(),
GradeId: int32(preferences.Grade().ID()),
SubjectId: int32(preferences.Subject().ID()),
Limit: preferences.RecommendationLimit(),
}
resp, err := r.client.GetRecommendations(ctx, req)
if err != nil {
returnnil, fmt.Errorf("获取内容推荐失败: %w", err)
}
// 批量转换内容项
var contents []*biz.Content
for _, item := range resp.Items {
var content biz.Content
if err := copier.Copy(&content, item); err != nil {
r.data.log.Warnf("转换内容项失败: %v", err)
continue
}
contents = append(contents, &content)
}
return contents, nil
}
// SaveContent 保存内容
func (r *contentRepo) SaveContent(ctx context.Context, content *biz.Content) error {
// 领域对象转换为数据库模型
var contentPO ContentPO
if err := copier.Copy(&contentPO, content); err != nil {
return fmt.Errorf("内容对象转换错误: %w", err)
}
// 保存到数据库
err := r.data.db.WithContext(ctx).Save(&contentPO).Error
if err != nil {
return fmt.Errorf("保存内容失败: %w", err)
}
returnnil
}
领域事件是实现聚合间松耦合通信的重要机制,它表示领域中发生的重要业务事件。
事件的设计应该反映业务语言,事件名称应该使用过去时态,表示已经发生的事情。在实现事件机制时,我们需要考虑事件的持久化、事件的顺序性、事件处理的幂等性等问题。
事件总线作为事件的分发中心,需要保证事件的可靠传递和处理。在微服务架构中,事件机制还可以用于实现最终一致性。
// internal/biz/domain/events.go
package biz
import (
"context"
"time"
)
// DomainEvent 领域事件接口
type DomainEvent interface {
EventID() string
EventType() string
AggregateID() string
OccurredOn() time.Time
EventData() interface{}
}
// UserRegisteredEvent 用户注册事件
type UserRegisteredEvent struct {
eventID string
userID UserID
profile *UserProfile
occurredOn time.Time
}
func NewUserRegisteredEvent(userID UserID, profile *UserProfile) *UserRegisteredEvent {
return &UserRegisteredEvent{
eventID: generateEventID(),
userID: userID,
profile: profile,
occurredOn: time.Now(),
}
}
func (e *UserRegisteredEvent) EventID() string { return e.eventID }
func (e *UserRegisteredEvent) EventType() string { return"UserRegistered" }
func (e *UserRegisteredEvent) AggregateID() string { return e.userID.String() }
func (e *UserRegisteredEvent) OccurredOn() time.Time { return e.occurredOn }
func (e *UserRegisteredEvent) EventData() interface{} { return e.profile }
// EventBus 事件总线
type EventBus interface {
Publish(ctx context.Context, event DomainEvent) error
Subscribe(eventType string, handler EventHandler) error
}
// EventHandler 事件处理器
type EventHandler interface {
Handle(ctx context.Context, event DomainEvent) error
}
// UserRegisteredEventHandler 用户注册事件处理器
type UserRegisteredEventHandler struct {
contentService *ContentService
emailService *EmailService
}
func (h *UserRegisteredEventHandler) Handle(ctx context.Context, event DomainEvent) error {
userEvent, ok := event.(*UserRegisteredEvent)
if !ok {
return errors.New("事件类型错误")
}
// 为新用户初始化学习内容
err := h.contentService.InitializeUserContent(ctx, userEvent.userID)
if err != nil {
return fmt.Errorf("初始化用户内容失败: %w", err)
}
// 发送欢迎邮件
err = h.emailService.SendWelcomeEmail(ctx, userEvent.profile.Email())
if err != nil {
// 邮件发送失败不影响主流程
log.Warnf("发送欢迎邮件失败: %v", err)
}
returnnil
}
用例层代表了应用程序的业务流程,它协调领域对象来完成特定的业务任务。用例层应该保持薄薄的一层,主要负责流程编排、事务管理、权限检查等。在设计用例时,我们需要从用户的角度思考,每个用例应该对应一个完整的业务操作。
用例层还负责发布领域事件,实现不同聚合间的协调。
在Golang中,用例通常以Usecase结构体的方法形式实现,每个方法代表一个具体的业务用例。
// internal/biz/user.go
package biz
import (
"context"
"fmt"
)
// UserUsecase 用户用例
type UserUsecase struct {
userRepo UserRepository
sessionRepo SessionRepository
eventBus EventBus
securityService SecurityService
log Logger
}
func NewUserUsecase(
userRepo UserRepository,
sessionRepo SessionRepository,
eventBus EventBus,
securityService SecurityService,
logger Logger,
) *UserUsecase {
return &UserUsecase{
userRepo: userRepo,
sessionRepo: sessionRepo,
eventBus: eventBus,
securityService: securityService,
log: NewHelper(logger),
}
}
// RegisterUser 用户注册用例
func (uc *UserUsecase) RegisterUser(
ctx context.Context,
req *RegisterUserRequest,
) (*User, error) {
// 1. 参数验证
if err := req.Validate(); err != nil {
returnnil, fmt.Errorf("参数验证失败: %w", err)
}
// 2. 业务规则检查
existingUser, err := uc.userRepo.GetByEmail(ctx, req.Email)
if err == nil && existingUser != nil {
returnnil, ErrEmailAlreadyExists
}
// 3. 创建用户领域对象
userID, err := NewUserID(generateUserID())
if err != nil {
returnnil, err
}
profile, err := NewUserProfile(req.Name, req.Email, req.Phone, req.Avatar)
if err != nil {
returnnil, fmt.Errorf("创建用户档案失败: %w", err)
}
grade, err := NewGrade(req.GradeID)
if err != nil {
returnnil, fmt.Errorf("年级信息错误: %w", err)
}
user := NewUser(userID, profile, grade)
// 4. 持久化用户
err = uc.userRepo.Save(ctx, user)
if err != nil {
returnnil, fmt.Errorf("保存用户失败: %w", err)
}
// 5. 发布领域事件
event := NewUserRegisteredEvent(userID, profile)
err = uc.eventBus.Publish(ctx, event)
if err != nil {
uc.log.Warnf("发布用户注册事件失败: %v", err)
}
return user, nil
}
// StartLearningSession 开始学习会话
func (uc *UserUsecase) StartLearningSession(
ctx context.Context,
userID UserID,
subject Subject,
) (*LearningSession, error) {
// 1. 验证用户权限
user, err := uc.userRepo.GetByID(ctx, userID)
if err != nil {
returnnil, fmt.Errorf("获取用户信息失败: %w", err)
}
if !user.HasPermissionForSubject(subject) {
returnnil, ErrInsufficientPermission
}
// 2. 检查并发会话限制
activeSessions, err := uc.sessionRepo.GetActiveSessionsByUser(ctx, userID)
if err != nil {
returnnil, err
}
iflen(activeSessions) >= MaxConcurrentSessions {
returnnil, ErrTooManyConcurrentSessions
}
// 3. 创建学习会话聚合
sessionID := NewSessionID(generateSessionID())
metadata := NewSessionMetadata(user.Grade(), subject)
session := NewLearningSession(sessionID, userID, subject, metadata)
// 4. 保存会话
err = uc.sessionRepo.Save(ctx, session)
if err != nil {
returnnil, fmt.Errorf("保存学习会话失败: %w", err)
}
return session, nil
}
防腐层(Anti-Corruption Layer)是DDD中用于保护领域模型免受外部系统影响的重要模式(也就是data层里调用其他三方接口或微服务的方法集合)。
在微服务架构中,防腐层特别重要,因为它确保了外部服务的变化不会直接影响到我们的核心业务逻辑。
在处理外部服务调用时,我们需要进行数据格式转换、错误处理、超时控制等。防腐层还可以实现服务降级、缓存等功能,提升系统的可靠性。
// internal/data/services/content_service.go
package services
import (
"context"
"github.com/jinzhu/copier"
contentV1 "education-platform/api/content/v1"
"education-platform/internal/biz"
)
// ContentServiceAdapter 内容服务适配器(防腐层)
type ContentServiceAdapter struct {
client contentV1.ContentClient
log Logger
}
func NewContentServiceAdapter(
client contentV1.ContentClient,
logger Logger,
) *ContentServiceAdapter {
return &ContentServiceAdapter{
client: client,
log: NewHelper(logger),
}
}
// GetContentRecommendations 获取内容推荐(防腐层方法)
func (a *ContentServiceAdapter) GetContentRecommendations(
ctx context.Context,
userID biz.UserID,
preferences *biz.UserPreferences,
) ([]*biz.Content, error) {
// 将领域对象转换为外部服务请求
var req contentV1.GetRecommendationsReq
if err := copier.Copy(&req, &struct {
UserId string
GradeId int32
SubjectId int32
Limit int32
}{
UserId: userID.String(),
GradeId: int32(preferences.Grade().ID()),
SubjectId: int32(preferences.Subject().ID()),
Limit: preferences.RecommendationLimit(),
}); err != nil {
returnnil, fmt.Errorf("推荐请求参数转换错误: %w", err)
}
resp, err := a.client.GetRecommendations(ctx, &req)
if err != nil {
a.log.WithContext(ctx).Errorf("获取内容推荐失败: %v", err)
returnnil, err
}
// 将外部服务响应转换为领域对象
var contents []*biz.Content
for _, item := range resp.Items {
var content biz.Content
if err := copier.Copy(&content, item); err != nil {
a.log.WithContext(ctx).Warnf("转换内容项失败: %v", err)
continue
}
contents = append(contents, &content)
}
return contents, nil
}
// SearchContent 内容搜索防腐层
func (a *ContentServiceAdapter) SearchContent(
ctx context.Context,
query *biz.ContentSearchQuery,
) (*biz.ContentSearchResult, error) {
var req contentV1.SearchContentReq
if err := copier.Copy(&req, query); err != nil {
returnnil, fmt.Errorf("搜索请求转换错误: %w", err)
}
resp, err := a.client.SearchContent(ctx, &req)
if err != nil {
returnnil, fmt.Errorf("内容搜索失败: %w", err)
}
var result biz.ContentSearchResult
if err := copier.Copy(&result, resp); err != nil {
returnnil, fmt.Errorf("搜索结果转换错误: %w", err)
}
return &result, nil
}
在处理复杂的业务流程时,领域服务发挥着关键作用。它们协调多个聚合的操作,实现跨聚合的业务逻辑。
流程编排需要考虑事务边界、错误处理、补偿机制等方面。在AI对话场景中,我们需要处理意图识别、内容匹配、安全审核等多个步骤,每个步骤都可能涉及不同的领域对象和外部服务。通过合理的流程设计,我们可以确保业务逻辑的正确执行和系统的可靠性。
// internal/biz/ai_dialogue.go
package biz
import (
"context"
"fmt"
)
// DialogueProcessor 对话处理器(领域服务)
type DialogueProcessor struct {
intentionClassifier *IntentionClassifier
contentMatcher *ContentMatcher
responseGenerator *ResponseGenerator
securityAuditor *SecurityAuditor
sessionManager *SessionManager
}
// ProcessDialogue 处理对话流程
func (p *DialogueProcessor) ProcessDialogue(
ctx context.Context,
req *DialogueRequest,
) (*DialogueResponse, error) {
// 1. 获取或创建会话
session, err := p.sessionManager.GetOrCreateSession(ctx, req.UserID, req.SessionID)
if err != nil {
returnnil, fmt.Errorf("会话管理失败: %w", err)
}
// 2. 添加用户消息到会话
userMessage, err := session.AddUserMessage(req.Query)
if err != nil {
returnnil, fmt.Errorf("添加用户消息失败: %w", err)
}
// 3. 意图识别
intention, err := p.intentionClassifier.Classify(ctx, &ClassificationRequest{
Query: req.Query,
History: session.GetRecentMessages(5),
User: req.User,
})
if err != nil {
returnnil, fmt.Errorf("意图识别失败: %w", err)
}
// 4. 内容匹配和处理
var response *DialogueResponse
switch intention.Type() {
case IntentionTypeContentSearch:
response, err = p.handleContentSearch(ctx, session, intention)
case IntentionTypeQuestionAnswering:
response, err = p.handleQuestionAnswering(ctx, session, intention)
case IntentionTypeWritingAssistance:
response, err = p.handleWritingAssistance(ctx, session, intention)
default:
response, err = p.handleGeneralChat(ctx, session, intention)
}
if err != nil {
returnnil, fmt.Errorf("处理对话失败: %w", err)
}
// 5. 安全审核
auditResult, err := p.securityAuditor.AuditContent(ctx, response.Content)
if err != nil {
returnnil, fmt.Errorf("安全审核失败: %w", err)
}
if !auditResult.IsPass() {
return p.buildSecurityErrorResponse(auditResult), nil
}
// 6. 添加AI响应到会话
aiMessage, err := session.AddAIMessage(response.Content)
if err != nil {
returnnil, fmt.Errorf("添加AI消息失败: %w", err)
}
// 7. 保存会话状态
err = p.sessionManager.SaveSession(ctx, session)
if err != nil {
returnnil, fmt.Errorf("保存会话状态失败: %w", err)
}
// 8. 构建响应
return &DialogueResponse{
SessionID: session.ID(),
MessageID: aiMessage.ID(),
Content: response.Content,
Intention: intention,
Metadata: response.Metadata,
}, nil
}
// handleContentSearch 处理内容搜索意图
func (p *DialogueProcessor) handleContentSearch(
ctx context.Context,
session *LearningSession,
intention *UserIntention,
) (*DialogueResponse, error) {
// 提取搜索参数
searchParams := intention.ExtractSearchParameters()
// 执行内容搜索
searchResult, err := p.contentMatcher.Search(ctx, &ContentSearchRequest{
Query: searchParams.Query,
Subject: session.Subject(),
Grade: session.User().Grade(),
Filters: searchParams.Filters,
})
if err != nil {
returnnil, err
}
// 生成响应内容
content, err := p.responseGenerator.GenerateSearchResponse(ctx, searchResult)
if err != nil {
returnnil, err
}
return &DialogueResponse{
Content: content,
Type: ResponseTypeContentList,
Metadata: searchResult.Metadata,
}, nil
}
依赖注入是现代软件架构中的重要模式,它有助于实现控制反转和依赖倒置原则。
Google Wire是一个编译时依赖注入工具,它通过代码生成来创建依赖关系图,避免了运行时反射的性能开销。
在DDD架构中,Wire帮助我们正确地组装各层的依赖关系,确保仓储接口、领域服务、用例等组件能够正确地协作。通过Provider函数和ProviderSet,我们可以清晰地定义每一层的依赖关系,使得系统的组装过程变得透明和可控。
// cmd/education-platform/wire.go
//go:build wireinject
// +build wireinject
package main
import (
"github.com/google/wire"
"education-platform/internal/biz"
"education-platform/internal/data"
"education-platform/internal/service"
"education-platform/internal/server"
"education-platform/internal/conf"
)
// wireApp 应用程序依赖注入
func wireApp(*conf.Server, *conf.Data, *conf.Biz, logger.Logger) (*kratos.App, func(), error) {
panic(wire.Build(
// 数据层
data.ProviderSet,
// 业务层
biz.ProviderSet,
// 服务层
service.ProviderSet,
// 服务器层
server.ProviderSet,
// 应用构建
newApp,
))
}
// internal/biz/provider.go
package biz
import"github.com/google/wire"
// ProviderSet 业务层依赖注入集合
var ProviderSet = wire.NewSet(
// 用例
NewUserUsecase,
NewContentUsecase,
NewAssessmentUsecase,
NewAIDialogueUsecase,
// 领域服务
NewDialogueProcessor,
NewContentMatcher,
NewSecurityAuditor,
// 事件相关
NewEventBus,
NewUserRegisteredEventHandler,
// 绑定接口
wire.Bind(new(UserRepository), new(*data.UserRepo)),
wire.Bind(new(ContentRepository), new(*data.ContentRepo)),
wire.Bind(new(AssessmentRepository), new(*data.AssessmentRepo)),
)
通过以上12点的分解,我们基本上聊透了DDD的实践。
DDD作为一种成熟的软件设计方法论,在我们的微服务架构中展现出了强大的实用价值。基于DDD,我们不仅构建了一个技术先进的系统,更建立了一套可持续发展的软件工程实践体系。
清晰的业务表达、稳定的架构基础、高效的团队协作。这些收获不仅体现在技术层面,更重要的是支撑了我们业务稳定健康的发展。
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费POC验证,效果达标后再合作。零风险落地应用大模型,已交付160+中大型企业
2025-08-20
基于Daft实现百万级文本Embedding
2025-08-20
你的供应链还在“裸奔”吗?这份AI转型蓝图,AI产品经理看完都收藏
2025-08-20
解构1688 AI黑盒:从用户交互到技术实现,五大功能全链路拆解
2025-08-20
AutoGLM 2.0 发布:给AI配个手机,自己点|附API
2025-08-20
DeepSeek V3.1 悄然上线:128k上下文、代码能力直逼Claude,价格却只有1/65!
2025-08-20
DeepSeek v3.1 到底有多强?与 Claude Code 一起实测!
2025-08-20
业务流程复杂,单Agent独木难支?ShareFlow:让AI能力“集”中生智
2025-08-20
从Prompt到Context:为什么Think Tool是形式化的必然?
2025-05-29
2025-05-23
2025-06-01
2025-06-21
2025-06-07
2025-06-12
2025-06-13
2025-06-19
2025-05-28
2025-07-29
2025-08-20
2025-08-19
2025-08-19
2025-08-18
2025-08-18
2025-08-18
2025-08-15
2025-08-14