3. 应用服务层
3.1 数据库应用服务
文件位置 : backend/application/memory/database.go
go
func (d *DatabaseApplicationService) AddDatabase(ctx context.Context, req *table.AddDatabaseRequest) (*table.SingleDatabaseResponse, error) {
// 1. 用户身份验证
uid := ctxutil.GetUIDFromCtx(ctx)
if uid == nil {
return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "session required"))
}
if *uid != req.CreatorID {
return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "creator id is invalid"))
}
// 2. 表名验证
if req.GetTableName() == "database" {
return nil, errorx.New(errno.ErrMemoryDatabaseNameInvalid)
}
// 3. 空间权限验证
spaces, err := crossuser.DefaultSVC().GetUserSpaceList(ctx, *uid)
if err != nil {
return nil, err
}
if len(spaces) == 0 || spaces[0].ID != req.SpaceID {
return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "space id is invalid"))
}
// 4. 调用领域服务创建数据库
res, err := d.DomainSVC.CreateDatabase(ctx, convertAddDatabase(req))
if err != nil {
return nil, err
}
// 5. 发布资源创建事件
databaseRes := res.Database
var ptrAppID *int64
if databaseRes.AppID != 0 {
ptrAppID = ptr.Of(databaseRes.AppID)
}
err = d.eventbus.PublishResources(ctx, &searchEntity.ResourceDomainEvent{
OpType: searchEntity.Created,
Resource: &searchEntity.ResourceDocument{
ResType: resCommon.ResType_Database,
ResID: databaseRes.ID,
Name: &databaseRes.TableName,
APPID: ptrAppID,
SpaceID: &databaseRes.SpaceID,
OwnerID: &databaseRes.CreatorID,
PublishStatus: ptr.Of(resCommon.PublishStatus_Published),
CreateTimeMS: ptr.Of(databaseRes.CreatedAtMs),
UpdateTimeMS: ptr.Of(databaseRes.UpdatedAtMs),
},
})
if err != nil {
return nil, fmt.Errorf("publish resource failed, err=%w", err)
}
// 6. 转换并返回结果
return ConvertDatabaseRes(databaseRes), nil
}
3.2 应用服务特点
数据库应用服务的主要特点:
- 多重身份验证: 验证会话和创建者ID一致性
- 表名验证: 防止使用保留字"database"作为表名
- 空间权限验证: 确保用户有权限在指定空间创建数据库
- 请求转换: 通过convertAddDatabase函数将API层请求转换为领域层请求
- 事件发布: 创建成功后发布资源创建事件,用于更新搜索索引
- 结果转换: 通过ConvertDatabaseRes函数将领域层结果转换为API层响应
3.3 事件驱动架构
数据库创建完成后,系统会发布资源创建事件:
go
// 发布创建事件
err = k.eventBus.PublishResources(ctx, &resourceEntity.ResourceDomainEvent{
OpType: resourceEntity.Created,
Resource: &resourceEntity.ResourceDocument{
ResType: resource.ResType_Database,
ResID: domainResp.DatabaseID,
Name: ptr.Of(req.Name),
ResSubType: ptr.Of(int32(req.FormatType)),
SpaceID: ptr.Of(req.SpaceID),
APPID: ptrAppID,
OwnerID: ptr.Of(*uid),
PublishStatus: ptr.Of(resource.PublishStatus_Published),
PublishTimeMS: ptr.Of(domainResp.CreatedAtMs),
CreateTimeMS: ptr.Of(domainResp.CreatedAtMs),
UpdateTimeMS: ptr.Of(domainResp.CreatedAtMs),
},
})
这个事件会被搜索服务监听,用于更新搜索索引,确保新创建的知识库能够被搜索到。
4. 领域服务层
4.1 数据库领域服务
文件位置 : backend/domain/memory/database/service/database.go
数据库领域服务接口定义:
go
type Database interface {
CreateDatabase(ctx context.Context, req *CreateDatabaseRequest) (*CreateDatabaseResponse, error)
UpdateDatabase(ctx context.Context, req *UpdateDatabaseRequest) (*UpdateDatabaseResponse, error)
DeleteDatabase(ctx context.Context, req *DeleteDatabaseRequest) error
MGetDatabase(ctx context.Context, req *MGetDatabaseRequest) (*MGetDatabaseResponse, error)
ListDatabase(ctx context.Context, req *ListDatabaseRequest) (*ListDatabaseResponse, error)
GetDraftDatabaseByOnlineID(ctx context.Context, req *GetDraftDatabaseByOnlineIDRequest) (*GetDraftDatabaseByOnlineIDResponse, error)
DeleteDatabaseByAppID(ctx context.Context, req *DeleteDatabaseByAppIDRequest) (*DeleteDatabaseByAppIDResponse, error)
GetAllDatabaseByAppID(ctx context.Context, req *GetAllDatabaseByAppIDRequest) (*GetAllDatabaseByAppIDResponse, error)
GetDatabaseTemplate(ctx context.Context, req *GetDatabaseTemplateRequest) (*GetDatabaseTemplateResponse, error)
GetDatabaseTableSchema(ctx context.Context, req *GetDatabaseTableSchemaRequest) (*GetDatabaseTableSchemaResponse, error)
ValidateDatabaseTableSchema(ctx context.Context, req *ValidateDatabaseTableSchemaRequest) (*ValidateDatabaseTableSchemaResponse, error)
SubmitDatabaseInsertTask(ctx context.Context, req *SubmitDatabaseInsertTaskRequest) error
GetDatabaseFileProgressData(ctx context.Context, req *GetDatabaseFileProgressDataRequest) (*GetDatabaseFileProgressDataResponse, error)
AddDatabaseRecord(ctx context.Context, req *AddDatabaseRecordRequest) error
UpdateDatabaseRecord(ctx context.Context, req *UpdateDatabaseRecordRequest) error
DeleteDatabaseRecord(ctx context.Context, req *DeleteDatabaseRecordRequest) error
ListDatabaseRecord(ctx context.Context, req *ListDatabaseRecordRequest) (*ListDatabaseRecordResponse, error)
ExecuteSQL(ctx context.Context, req *ExecuteSQLRequest) (*ExecuteSQLResponse, error)
BindDatabase(ctx context.Context, req *BindDatabaseToAgentRequest) error
UnBindDatabase(ctx context.Context, req *UnBindDatabaseToAgentRequest) error
MGetDatabaseByAgentID(ctx context.Context, req *MGetDatabaseByAgentIDRequest) (*MGetDatabaseByAgentIDResponse, error)
MGetRelationsByAgentID(ctx context.Context, req *MGetRelationsByAgentIDRequest) (*MGetRelationsByAgentIDResponse, error)
PublishDatabase(ctx context.Context, req *PublishDatabaseRequest) (*PublishDatabaseResponse, error)
}
type CreateDatabaseRequest struct {
Database *entity.Database
}
type CreateDatabaseResponse struct {
Database *entity.Database
}
type UpdateDatabaseRequest struct {
Database *entity.Database
}
type UpdateDatabaseResponse struct {
Database *entity.Database
}
5. 数据访问层
数据访问层负责数据库相关数据的持久化操作,采用Repository模式和DAO模式相结合的设计,确保数据访问的一致性和可维护性。
5.1 数据库Repository接口定义
文件位置 : backend/domain/memory/database/repository/repository.go
数据库数据访问层采用Repository模式,提供统一的数据访问接口。由于系统采用了草稿-线上分离的设计,提供了两个主要的Repository接口:DraftDAO和OnlineDAO,分别负责草稿态和线上态的数据库操作:
go
// DraftDAO 草稿态数据库操作接口
type DraftDAO interface {
Get(ctx context.Context, id int64) (*entity.Database, error)
List(ctx context.Context, filter *entity.DatabaseFilter, page *entity.Pagination, orderBy []*database.OrderBy) ([]*entity.Database, int64, error)
MGet(ctx context.Context, ids []int64) ([]*entity.Database, error)
CreateWithTX(ctx context.Context, tx *query.QueryTx, database *entity.Database, draftID, onlineID int64, physicalTableName string) (*entity.Database, error)
UpdateWithTX(ctx context.Context, tx *query.QueryTx, database *entity.Database) (*entity.Database, error)
DeleteWithTX(ctx context.Context, tx *query.QueryTx, id int64) error
BatchDeleteWithTX(ctx context.Context, tx *query.QueryTx, ids []int64) error
}
// OnlineDAO 线上态数据库操作接口
type OnlineDAO interface {
Get(ctx context.Context, id int64) (*entity.Database, error)
MGet(ctx context.Context, ids []int64) ([]*entity.Database, error)
List(ctx context.Context, filter *entity.DatabaseFilter, page *entity.Pagination, orderBy []*database.OrderBy) ([]*entity.Database, int64, error)
UpdateWithTX(ctx context.Context, tx *query.QueryTx, database *entity.Database) (*entity.Database, error)
CreateWithTX(ctx context.Context, tx *query.QueryTx, database *entity.Database, draftID, onlineID int64, physicalTableName string) (*entity.Database, error)
DeleteWithTX(ctx context.Context, tx *query.QueryTx, id int64) error
BatchDeleteWithTX(ctx context.Context, tx *query.QueryTx, ids []int64) error
}
// 工厂方法
func NewDraftDatabaseDAO(db *gorm.DB, idGen idgen.IDGenerator) DraftDAO {
return dal.NewDraftDatabaseDAO(db, idGen)
}
func NewOnlineDatabaseDAO(db *gorm.DB, idGen idgen.IDGenerator) OnlineDAO {
return dal.NewOnlineDatabaseDAO(db, idGen)
}
创建方法特点
- 事务支持: 所有写操作都支持事务,确保数据一致性
- 类型安全: 使用强类型参数,避免运行时错误
- 错误处理: 详细的错误信息,便于问题定位
- 批量操作: 支持批量创建,提高性能
5.2 数据访问对象(DAO)
文件位置 : backend/domain/memory/database/internal/dal/
数据库的DAO实现采用了单例模式和工厂方法模式,分别为草稿态和线上态的数据库提供了独立的DAO实现:DraftImpl和OlineImpl。
go
// DraftImpl 草稿态数据库DAO实现
type DraftImpl struct {
IDGen idgen.IDGenerator
query *query.Query
}
func NewDraftDatabaseDAO(db *gorm.DB, idGen idgen.IDGenerator) *DraftImpl {
draftOnce.Do(func() {
singletonDraft = &DraftImpl{
IDGen: idGen,
query: query.Use(db),
}
})
return singletonDraft
}
// OlineImpl 线上态数据库DAO实现
type OlineImpl struct {
IDGen idgen.IDGenerator
query *query.Query
}
func NewOnlineDatabaseDAO(db *gorm.DB, idGen idgen.IDGenerator) *OlineImpl {
onlineOnce.Do(func() {
singletonOnline = &OlineImpl{
IDGen: idGen,
query: query.Use(db),
}
})
return singletonOnline
}
// CreateWithTX 创建数据库记录(事务支持)
func (d *DraftImpl) CreateWithTX(ctx context.Context, tx *query.QueryTx, database *entity.Database,
draftID, onlineID int64, physicalTableName string) (*entity.Database, error) {
now := time.Now().UnixMilli()
draftInfo := &model.DraftDatabaseInfo{
ID: draftID,
AppID: database.AppID,
SpaceID: database.SpaceID,
RelatedOnlineID: onlineID,
IsVisible: 1, // visible by default
PromptDisabled: func() int32 {
if database.PromptDisabled {
return 1
} else {
return 0
}
}(),
TableName_: database.TableName,
TableDesc: database.TableDesc,
TableField: database.FieldList,
CreatorID: database.CreatorID,
IconURI: database.IconURI,
PhysicalTableName: physicalTableName,
RwMode: int64(database.RwMode),
CreatedAt: now,
UpdatedAt: now,
}
table := tx.DraftDatabaseInfo
err := table.WithContext(ctx).Create(draftInfo)
if err != nil {
return nil, err
}
database.CreatedAtMs = now
database.UpdatedAtMs = now
return database, nil
}
// Get 获取单个数据库记录
func (d *DraftImpl) Get(ctx context.Context, id int64) (*entity.Database, error) {
res := d.query.DraftDatabaseInfo
info, err := res.WithContext(ctx).Where(res.ID.Eq(id)).First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errorx.New(errno.ErrMemoryDatabaseNotFoundCode)
}
return nil, fmt.Errorf("query draft database failed: %v", err)
}
// 构建返回的数据库对象
db := &entity.Database{
ID: info.ID,
SpaceID: info.SpaceID,
CreatorID: info.CreatorID,
IconURI: info.IconURI,
AppID: info.AppID,
IsVisible: info.IsVisible == 1,
PromptDisabled: info.PromptDisabled == 1,
TableName: info.TableName_,
TableDesc: info.TableDesc,
FieldList: info.TableField,
Status: table.BotTableStatus_Online,
ActualTableName: info.PhysicalTableName,
RwMode: table.BotTableRWMode(info.RwMode),
OnlineID: &info.RelatedOnlineID,
DraftID: &info.ID,
}
return db, nil
}
// List 列表查询数据库记录
func (o *OlineImpl) List(ctx context.Context, filter *entity.DatabaseFilter,
page *entity.Pagination, orderBy []*database.OrderBy) ([]*entity.Database, int64, error) {
res := o.query.OnlineDatabaseInfo
// 构建基本查询
q := res.WithContext(ctx)
// 添加过滤条件
if filter != nil {
if filter.CreatorID != nil {
q = q.Where(res.CreatorID.Eq(*filter.CreatorID))
}
if filter.SpaceID != nil {
q = q.Where(res.SpaceID.Eq(*filter.SpaceID))
}
if filter.AppID != nil {
q = q.Where(res.AppID.Eq(*filter.AppID))
}
if filter.TableName != nil {
q = q.Where(res.TableName_.Like("%" + *filter.TableName + "%"))
}
q = q.Where(res.IsVisible.Eq(1))
}
// 计数
count, err := q.Count()
if err != nil {
return nil, 0, fmt.Errorf("count online database failed: %v", err)
}
// 分页处理
limit := int64(50) // 默认
if page != nil && page.Limit > 0 {
limit = int64(page.Limit)
}
offset := 0
if page != nil && page.Offset > 0 {
offset = page.Offset
}
// 排序处理
if len(orderBy) > 0 {
for _, order := range orderBy {
switch order.Field {
case "created_at":
if order.Direction == table.SortDirection_Desc {
q = q.Order(res.CreatedAt.Desc())
} else {
q = q.Order(res.CreatedAt)
}
case "updated_at":
if order.Direction == table.SortDirection_Desc {
q = q.Order(res.UpdatedAt.Desc())
} else {
q = q.Order(res.UpdatedAt)
}
default:
q = q.Order(res.CreatedAt.Desc())
}
}
} else {
q = q.Order(res.CreatedAt.Desc())
}
// 查询数据
records, err := q.Limit(int(limit)).Offset(offset).Find()
if err != nil {
return nil, 0, fmt.Errorf("list online database failed: %v", err)
}
// 转换为实体对象
databases := make([]*entity.Database, 0, len(records))
for _, info := range records {
d := &entity.Database{
ID: info.ID,
SpaceID: info.SpaceID,
CreatorID: info.CreatorID,
IconURI: info.IconURI,
AppID: info.AppID,
DraftID: &info.RelatedDraftID,
OnlineID: &info.ID,
IsVisible: info.IsVisible == 1,
PromptDisabled: info.PromptDisabled == 1,
TableName: info.TableName_,
TableDesc: info.TableDesc,
FieldList: info.TableField,
Status: table.BotTableStatus_Online,
ActualTableName: info.PhysicalTableName,
RwMode: table.BotTableRWMode(info.RwMode),
TableType: ptr.Of(table.TableType_OnlineTable),
}
databases = append(databases, d)
}
return databases, count, nil
}
数据库操作特点
- 单例模式: DAO实现采用单例模式,确保全局唯一实例
- 事务支持 : 所有写操作都通过
WithTX
方法提供事务支持,确保数据一致性 - 双重状态管理: 分别管理草稿态和线上态的数据,支持版本控制和发布流程
- 分页与排序: List方法支持灵活的分页和排序功能
- 错误处理: 提供详细的错误信息和适当的错误码映射
- 过滤条件: 支持多维度的过滤条件,满足不同查询需求
5.3 完整数据访问流程
数据库创建涉及的数据表:
- draft_database_info: 草稿态数据库信息表
- online_database_info: 线上态数据库信息表
- physical tables: 实际存储数据的物理表
数据访问流程:
- 领域服务层接收请求并进行业务校验
- 生成唯一的草稿ID和线上ID
- 创建物理表(如果是新表)
- 开启数据库事务
- 通过DraftDAO创建草稿记录
- 通过OnlineDAO创建线上记录
- 提交事务
- 发布数据库创建事件
特点说明:
- 数据库采用了物理表与元数据表分离的设计
- 草稿态和线上态数据同时创建,确保数据一致性
- 事务贯穿整个创建流程,保证原子性操作
5.4 数据模型定义
文件位置 : backend/domain/memory/database/internal/dal/model/
数据库系统使用了两个主要的数据模型来分别表示草稿态和线上态的数据库信息:
go
// DraftDatabaseInfo 草稿态数据库信息模型
type DraftDatabaseInfo struct {
ID int64 `gorm:"column:id;primaryKey" json:"id"`
AppID int64 `gorm:"column:app_id" json:"app_id"`
SpaceID int64 `gorm:"column:space_id;not null" json:"space_id"`
RelatedOnlineID int64 `gorm:"column:related_online_id" json:"related_online_id"`
IsVisible int32 `gorm:"column:is_visible;default:1" json:"is_visible"`
PromptDisabled int32 `gorm:"column:prompt_disabled;default:0" json:"prompt_disabled"`
TableName_ string `gorm:"column:table_name;not null;size:100" json:"table_name"`
TableDesc string `gorm:"column:table_desc;size:500" json:"table_desc"`
TableField []*database.FieldItem `gorm:"column:table_field;type:json" json:"table_field"`
CreatorID int64 `gorm:"column:creator_id;not null" json:"creator_id"`
IconURI string `gorm:"column:icon_uri;size:500" json:"icon_uri"`
PhysicalTableName string `gorm:"column:physical_table_name;size:255" json:"physical_table_name"`
RwMode int64 `gorm:"column:rw_mode;default:0" json:"rw_mode"`
CreatedAt int64 `gorm:"column:created_at;autoCreateTime:milli" json:"created_at"`
UpdatedAt int64 `gorm:"column:updated_at;autoUpdateTime:milli" json:"updated_at"`
}
// OnlineDatabaseInfo 线上态数据库信息模型
type OnlineDatabaseInfo struct {
ID int64 `gorm:"column:id;primaryKey" json:"id"`
AppID int64 `gorm:"column:app_id" json:"app_id"`
SpaceID int64 `gorm:"column:space_id;not null" json:"space_id"`
RelatedDraftID int64 `gorm:"column:related_draft_id" json:"related_draft_id"`
IsVisible int32 `gorm:"column:is_visible;default:1" json:"is_visible"`
PromptDisabled int32 `gorm:"column:prompt_disabled;default:0" json:"prompt_disabled"`
TableName_ string `gorm:"column:table_name;not null;size:100" json:"table_name"`
TableDesc string `gorm:"column:table_desc;size:500" json:"table_desc"`
TableField []*database.FieldItem `gorm:"column:table_field;type:json" json:"table_field"`
CreatorID int64 `gorm:"column:creator_id;not null" json:"creator_id"`
IconURI string `gorm:"column:icon_uri;size:500" json:"icon_uri"`
PhysicalTableName string `gorm:"column:physical_table_name;size:255" json:"physical_table_name"`
RwMode int64 `gorm:"column:rw_mode;default:0" json:"rw_mode"`
CreatedAt int64 `gorm:"column:created_at;autoCreateTime:milli" json:"created_at"`
UpdatedAt int64 `gorm:"column:updated_at;autoUpdateTime:milli" json:"updated_at"`
}
### 数据模型的核心特点
1. **双重状态管理**:通过DraftDatabaseInfo和OnlineDatabaseInfo两个模型分别管理数据库的草稿态和线上态,实现了开发与生产环境的分离
- 草稿态用于数据库的创建、编辑和预览
- 线上态包含最终发布的数据库配置,提供稳定的数据访问服务
2. **字段关系**:两个模型之间通过RelatedDraftID和RelatedOnlineID字段相互关联,确保草稿与线上版本的对应关系
3. **物理表与元数据分离**:通过PhysicalTableName字段记录实际存储数据的物理表名,实现了元数据管理与数据存储的解耦
4. **字段定义**:使用TableField字段(JSON类型)存储表的字段定义信息,支持灵活的表结构配置
5. **访问控制**:通过RwMode字段控制数据库的读写模式,IsVisible和PromptDisabled控制数据库的可见性和提示功能
6. **时间戳管理**:自动维护CreatedAt和UpdatedAt时间戳,记录数据库的创建和更新时间
5.5 查询生成器
文件位置 : backend/domain/memory/database/internal/dal/query/
数据库系统使用GORM生成的查询接口,为草稿态和线上态数据库提供类型安全的查询方法:
go
// 线上态数据库查询接口
type IOnlineDatabaseInfoDo interface {
gen.SubQuery
Debug() IOnlineDatabaseInfoDo
WithContext(ctx context.Context) IOnlineDatabaseInfoDo
WithResult(fc func(tx gen.Dao)) gen.ResultInfo
ReplaceDB(db *gorm.DB)
ReadDB() IOnlineDatabaseInfoDo
WriteDB() IOnlineDatabaseInfoDo
As(alias string) gen.Dao
Session(config *gorm.Session) IOnlineDatabaseInfoDo
Columns(cols ...field.Expr) gen.Columns
Clauses(conds ...clause.Expression) IOnlineDatabaseInfoDo
Not(conds ...gen.Condition) IOnlineDatabaseInfoDo
Or(conds ...gen.Condition) IOnlineDatabaseInfoDo
Select(conds ...field.Expr) IOnlineDatabaseInfoDo
Where(conds ...gen.Condition) IOnlineDatabaseInfoDo
Order(conds ...field.Expr) IOnlineDatabaseInfoDo
Distinct(cols ...field.Expr) IOnlineDatabaseInfoDo
Omit(cols ...field.Expr) IOnlineDatabaseInfoDo
Join(table schema.Tabler, on ...field.Expr) IOnlineDatabaseInfoDo
LeftJoin(table schema.Tabler, on ...field.Expr) IOnlineDatabaseInfoDo
RightJoin(table schema.Tabler, on ...field.Expr) IOnlineDatabaseInfoDo
Group(cols ...field.Expr) IOnlineDatabaseInfoDo
Having(conds ...gen.Condition) IOnlineDatabaseInfoDo
Limit(limit int) IOnlineDatabaseInfoDo
Offset(offset int) IOnlineDatabaseInfoDo
Count() (count int64, err error)
Scopes(funcs ...func(gen.Dao) gen.Dao) IOnlineDatabaseInfoDo
Unscoped() IOnlineDatabaseInfoDo
Create(values ...*model.OnlineDatabaseInfo) error
CreateInBatches(values []*model.OnlineDatabaseInfo, batchSize int) error
Save(values ...*model.OnlineDatabaseInfo) error
First() (*model.OnlineDatabaseInfo, error)
Take() (*model.OnlineDatabaseInfo, error)
Last() (*model.OnlineDatabaseInfo, error)
Find() ([]*model.OnlineDatabaseInfo, error)
// 更多查询方法...
}
// 草稿态数据库查询接口类似,命名为IDraftDatabaseInfoDo