Go: refactor dao and entity (#17771)

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
This commit is contained in:
Jin Hai
2026-08-04 14:18:03 +08:00
committed by GitHub
parent 197b142cef
commit 85c40d87a7
5 changed files with 205 additions and 162 deletions

View File

@@ -0,0 +1,148 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package dao
import (
"context"
"errors"
"ragflow/internal/entity"
"gorm.io/gorm"
)
// API4ConversationDAO API for conversation data access object
type API4ConversationDAO struct{}
// NewAPI4ConversationDAO create API4Conversation DAO
func NewAPI4ConversationDAO() *API4ConversationDAO {
return &API4ConversationDAO{}
}
// ConversationStatsRow is one daily aggregate row for api_4_conversation.
type ConversationStatsRow struct {
Dt string `gorm:"column:dt"`
PV int64 `gorm:"column:pv"`
UV int64 `gorm:"column:uv"`
Tokens float64 `gorm:"column:tokens"`
Duration float64 `gorm:"column:duration"`
Round float64 `gorm:"column:round"`
ThumbUp int64 `gorm:"column:thumb_up"`
}
// Create inserts a new api_4_conversation row. The caller is responsible
// for setting ID, DialogID, UserID and the BaseModel time fields; the
// DAO does not assign defaults because session creation paths in the
// Python agent API generate an uuid + tenant timestamp and rely on the
// round-trip shape being byte-identical.
func (dao *API4ConversationDAO) Create(ctx context.Context, db *gorm.DB, conv *entity.API4Conversation) error {
if conv == nil {
return errors.New("api4 conversation: nil row")
}
return db.WithContext(ctx).Create(conv).Error
}
// Update writes back an existing api_4_conversation row. The bot
// completion path calls this with the updated Message JSON after each
// turn so multi-turn chatbot sessions carry prior history into the next
// LLM call. Matches the Python conversation_service.update pattern at
// api/db/services/conversation_service.py:236 (async_iframe_completion).
func (dao *API4ConversationDAO) Update(ctx context.Context, db *gorm.DB, conv *entity.API4Conversation) error {
if conv == nil {
return errors.New("api4 conversation: nil row")
}
if conv.ID == "" {
return errors.New("api4 conversation: empty id")
}
return db.WithContext(ctx).Save(conv).Error
}
// Stats returns daily conversation aggregates for a tenant.
func (dao *API4ConversationDAO) Stats(ctx context.Context, db *gorm.DB, tenantID, fromDate, toDate string, source *string) ([]ConversationStatsRow, error) {
var rows []ConversationStatsRow
dateExpr := "DATE_FORMAT(a.create_date, '%Y-%m-%d 00:00:00')"
query := db.WithContext(ctx).Table("api_4_conversation AS a").
Select(`
DATE_FORMAT(a.create_date, '%Y-%m-%d 00:00:00') AS dt,
COUNT(a.id) AS pv,
COUNT(DISTINCT a.user_id) AS uv,
COALESCE(SUM(a.tokens), 0) AS tokens,
COALESCE(SUM(a.duration), 0) AS duration,
COALESCE(AVG(a.round), 0) AS round,
COALESCE(SUM(a.thumb_up), 0) AS thumb_up
`).
Joins("JOIN dialog AS d ON a.dialog_id = d.id AND d.tenant_id = ?", tenantID).
Where("a.create_date >= ? AND a.create_date <= ?", fromDate, toDate)
if source == nil {
query = query.Where("a.source IS NULL")
} else {
query = query.Where("a.source = ?", *source)
}
err := query.Group(dateExpr).
Order(dateExpr).
Scan(&rows).Error
return rows, err
}
func (dao *API4ConversationDAO) GetBySessionID(ctx context.Context, db *gorm.DB, sessionID, agentID string) (*entity.API4Conversation, error) {
var result entity.API4Conversation
tx := db.WithContext(ctx).Where("id = ? AND dialog_id = ?", sessionID, agentID).Find(&result)
if tx.Error != nil {
return nil, tx.Error
}
if tx.RowsAffected == 0 {
return nil, nil
}
return &result, nil
}
// GetByID returns a conversation without requiring the caller to know its
// agent. It is used when the session itself is the authorization resource.
func (dao *API4ConversationDAO) GetByID(ctx context.Context, db *gorm.DB, id string) (*entity.API4Conversation, error) {
var result entity.API4Conversation
tx := db.WithContext(ctx).Where("id = ?", id).Find(&result)
if tx.Error != nil {
return nil, tx.Error
}
if tx.RowsAffected == 0 {
return nil, nil
}
return &result, nil
}
// ListIDsByAgentID lists conversation IDs for one agent.
func (dao *API4ConversationDAO) ListIDsByAgentID(ctx context.Context, db *gorm.DB, agentID string) ([]string, error) {
var ids []string
err := db.WithContext(ctx).Model(&entity.API4Conversation{}).Where("dialog_id = ?", agentID).Pluck("id", &ids).Error
return ids, err
}
// DeleteBySessionIDAndAgentID deletes API4Conversations by sessionID and agentID
func (dao *API4ConversationDAO) DeleteBySessionIDAndAgentID(ctx context.Context, db *gorm.DB, sessionID, agentID string) (int64, error) {
result := db.WithContext(ctx).Where("id = ? AND dialog_id = ?", sessionID, agentID).Delete(&entity.API4Conversation{})
return result.RowsAffected, result.Error
}
// DeleteByDialogIDs deletes API4Conversations by dialog IDs (hard delete)
func (dao *API4ConversationDAO) DeleteByDialogIDs(ctx context.Context, db *gorm.DB, dialogIDs []string) (int64, error) {
if len(dialogIDs) == 0 {
return 0, nil
}
result := db.WithContext(ctx).Where("dialog_id IN ?", dialogIDs).Delete(&entity.API4Conversation{})
return result.RowsAffected, result.Error
}

View File

@@ -18,8 +18,6 @@ package dao
import (
"context"
"errors"
"ragflow/internal/entity"
"gorm.io/gorm"
@@ -47,16 +45,19 @@ func (dao *APITokenDAO) GetByTenantID(ctx context.Context, db *gorm.DB, tenantID
// DeleteByTenantID deletes all API tokens by tenant ID (hard delete)
func (dao *APITokenDAO) DeleteByTenantID(ctx context.Context, db *gorm.DB, tenantID string) (int64, error) {
result := db.WithContext(ctx).Unscoped().Where("tenant_id = ?", tenantID).Delete(&entity.APIToken{})
result := db.WithContext(ctx).Where("tenant_id = ?", tenantID).Delete(&entity.APIToken{})
return result.RowsAffected, result.Error
}
// GetUserByAPIToken gets user by API token
func (dao *APITokenDAO) GetUserByAPIToken(ctx context.Context, db *gorm.DB, token string) (*entity.APIToken, error) {
// GetByAPIToken gets user by API token
func (dao *APITokenDAO) GetByAPIToken(ctx context.Context, db *gorm.DB, token string) (*entity.APIToken, error) {
var apiToken entity.APIToken
err := db.WithContext(ctx).Where("token = ?", token).First(&apiToken).Error
if err != nil {
return nil, err
tx := db.WithContext(ctx).Where("token = ?", token).Find(&apiToken)
if tx.Error != nil {
return nil, tx.Error
}
if tx.RowsAffected == 0 {
return nil, nil
}
return &apiToken, nil
}
@@ -74,135 +75,12 @@ func (dao *APITokenDAO) DeleteByDialogIDs(ctx context.Context, db *gorm.DB, dial
if len(dialogIDs) == 0 {
return 0, nil
}
result := db.WithContext(ctx).Unscoped().Where("dialog_id IN ?", dialogIDs).Delete(&entity.APIToken{})
result := db.WithContext(ctx).Where("dialog_id IN ?", dialogIDs).Delete(&entity.APIToken{})
return result.RowsAffected, result.Error
}
// DeleteByTenantIDAndToken deletes a specific API token by tenant ID and token value
func (dao *APITokenDAO) DeleteByTenantIDAndToken(ctx context.Context, db *gorm.DB, tenantID, token string) (int64, error) {
result := db.WithContext(ctx).Unscoped().Where("tenant_id = ? AND token = ?", tenantID, token).Delete(&entity.APIToken{})
return result.RowsAffected, result.Error
}
// API4ConversationDAO API for conversation data access object
type API4ConversationDAO struct{}
// NewAPI4ConversationDAO create API4Conversation DAO
func NewAPI4ConversationDAO() *API4ConversationDAO {
return &API4ConversationDAO{}
}
// ConversationStatsRow is one daily aggregate row for api_4_conversation.
type ConversationStatsRow struct {
Dt string `gorm:"column:dt"`
PV int64 `gorm:"column:pv"`
UV int64 `gorm:"column:uv"`
Tokens float64 `gorm:"column:tokens"`
Duration float64 `gorm:"column:duration"`
Round float64 `gorm:"column:round"`
ThumbUp int64 `gorm:"column:thumb_up"`
}
// Create inserts a new api_4_conversation row. The caller is responsible
// for setting ID, DialogID, UserID and the BaseModel time fields; the
// DAO does not assign defaults because session creation paths in the
// Python agent API generate an uuid + tenant timestamp and rely on the
// round-trip shape being byte-identical.
func (dao *API4ConversationDAO) Create(ctx context.Context, db *gorm.DB, conv *entity.API4Conversation) error {
if conv == nil {
return errors.New("api4 conversation: nil row")
}
return db.WithContext(ctx).Create(conv).Error
}
// Update writes back an existing api_4_conversation row. The bot
// completion path calls this with the updated Message JSON after each
// turn so multi-turn chatbot sessions carry prior history into the next
// LLM call. Matches the Python conversation_service.update pattern at
// api/db/services/conversation_service.py:236 (async_iframe_completion).
func (dao *API4ConversationDAO) Update(ctx context.Context, db *gorm.DB, conv *entity.API4Conversation) error {
if conv == nil {
return errors.New("api4 conversation: nil row")
}
if conv.ID == "" {
return errors.New("api4 conversation: empty id")
}
return db.WithContext(ctx).Save(conv).Error
}
// Stats returns daily conversation aggregates for a tenant.
func (dao *API4ConversationDAO) Stats(ctx context.Context, db *gorm.DB, tenantID, fromDate, toDate string, source *string) ([]ConversationStatsRow, error) {
var rows []ConversationStatsRow
dateExpr := "DATE_FORMAT(a.create_date, '%Y-%m-%d 00:00:00')"
query := db.WithContext(ctx).Table("api_4_conversation AS a").
Select(`
DATE_FORMAT(a.create_date, '%Y-%m-%d 00:00:00') AS dt,
COUNT(a.id) AS pv,
COUNT(DISTINCT a.user_id) AS uv,
COALESCE(SUM(a.tokens), 0) AS tokens,
COALESCE(SUM(a.duration), 0) AS duration,
COALESCE(AVG(a.round), 0) AS round,
COALESCE(SUM(a.thumb_up), 0) AS thumb_up
`).
Joins("JOIN dialog AS d ON a.dialog_id = d.id AND d.tenant_id = ?", tenantID).
Where("a.create_date >= ? AND a.create_date <= ?", fromDate, toDate)
if source == nil {
query = query.Where("a.source IS NULL")
} else {
query = query.Where("a.source = ?", *source)
}
err := query.Group(dateExpr).
Order(dateExpr).
Scan(&rows).Error
return rows, err
}
func (dao *API4ConversationDAO) GetBySessionID(ctx context.Context, db *gorm.DB, sessionID, agentID string) (*entity.API4Conversation, error) {
var result entity.API4Conversation
tx := db.WithContext(ctx).Where("id = ? AND dialog_id = ?", sessionID, agentID).Find(&result)
if tx.Error != nil {
return nil, tx.Error
}
if tx.RowsAffected == 0 {
return nil, nil
}
return &result, nil
}
// GetByID returns a conversation without requiring the caller to know its
// agent. It is used when the session itself is the authorization resource.
func (dao *API4ConversationDAO) GetByID(ctx context.Context, db *gorm.DB, id string) (*entity.API4Conversation, error) {
var result entity.API4Conversation
tx := db.WithContext(ctx).Where("id = ?", id).Find(&result)
if tx.Error != nil {
return nil, tx.Error
}
if tx.RowsAffected == 0 {
return nil, nil
}
return &result, nil
}
// ListIDsByAgentID lists conversation IDs for one agent.
func (dao *API4ConversationDAO) ListIDsByAgentID(ctx context.Context, db *gorm.DB, agentID string) ([]string, error) {
var ids []string
err := db.WithContext(ctx).Model(&entity.API4Conversation{}).Where("dialog_id = ?", agentID).Pluck("id", &ids).Error
return ids, err
}
// DeleteBySessionIDAndAgentID deletes API4Conversations by sessionID and agentID
func (dao *API4ConversationDAO) DeleteBySessionIDAndAgentID(ctx context.Context, db *gorm.DB, sessionID, agentID string) (int64, error) {
result := db.WithContext(ctx).Where("id = ? AND dialog_id = ?", sessionID, agentID).Delete(&entity.API4Conversation{})
return result.RowsAffected, result.Error
}
// DeleteByDialogIDs deletes API4Conversations by dialog IDs (hard delete)
func (dao *API4ConversationDAO) DeleteByDialogIDs(ctx context.Context, db *gorm.DB, dialogIDs []string) (int64, error) {
if len(dialogIDs) == 0 {
return 0, nil
}
result := db.WithContext(ctx).Unscoped().Where("dialog_id IN ?", dialogIDs).Delete(&entity.API4Conversation{})
result := db.WithContext(ctx).Where("tenant_id = ? AND token = ?", tenantID, token).Delete(&entity.APIToken{})
return result.RowsAffected, result.Error
}

View File

@@ -0,0 +1,44 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package entity
import "encoding/json"
// API4Conversation API for conversation model
type API4Conversation struct {
ID string `gorm:"column:id;primaryKey;size:32" json:"id"`
Name *string `gorm:"column:name;size:255" json:"name,omitempty"`
DialogID string `gorm:"column:dialog_id;size:32;not null;index" json:"dialog_id"`
UserID string `gorm:"column:user_id;size:255;not null;index" json:"user_id"`
ExpUserID *string `gorm:"column:exp_user_id;size:255;index" json:"exp_user_id,omitempty"`
Message json.RawMessage `gorm:"column:message;type:longtext" json:"message,omitempty"`
Reference json.RawMessage `gorm:"column:reference;type:longtext" json:"reference,omitempty"`
Tokens int `gorm:"column:tokens" json:"tokens"`
Source *string `gorm:"column:source;size:16" json:"source,omitempty"`
DSL JSONMap `gorm:"column:dsl;type:longtext" json:"dsl,omitempty"`
Duration float64 `gorm:"column:duration" json:"duration"`
Round int `gorm:"column:round" json:"round"`
ThumbUp int `gorm:"column:thumb_up" json:"thumb_up"`
Errors *string `gorm:"column:errors;type:text" json:"errors,omitempty"`
VersionTitle *string `gorm:"column:version_title;size:255" json:"version_title,omitempty"`
BaseModel
}
// TableName specify table name
func (API4Conversation) TableName() string {
return "api_4_conversation"
}

View File

@@ -16,8 +16,6 @@
package entity
import "encoding/json"
// APIToken API token model
type APIToken struct {
TenantID string `gorm:"column:tenant_id;size:32;not null;primaryKey" json:"tenant_id"`
@@ -32,28 +30,3 @@ type APIToken struct {
func (APIToken) TableName() string {
return "api_token"
}
// API4Conversation API for conversation model
type API4Conversation struct {
ID string `gorm:"column:id;primaryKey;size:32" json:"id"`
Name *string `gorm:"column:name;size:255" json:"name,omitempty"`
DialogID string `gorm:"column:dialog_id;size:32;not null;index" json:"dialog_id"`
UserID string `gorm:"column:user_id;size:255;not null;index" json:"user_id"`
ExpUserID *string `gorm:"column:exp_user_id;size:255;index" json:"exp_user_id,omitempty"`
Message json.RawMessage `gorm:"column:message;type:longtext" json:"message,omitempty"`
Reference json.RawMessage `gorm:"column:reference;type:longtext" json:"reference,omitempty"`
Tokens int `gorm:"column:tokens" json:"tokens"`
Source *string `gorm:"column:source;size:16" json:"source,omitempty"`
DSL JSONMap `gorm:"column:dsl;type:longtext" json:"dsl,omitempty"`
Duration float64 `gorm:"column:duration" json:"duration"`
Round int `gorm:"column:round" json:"round"`
ThumbUp int `gorm:"column:thumb_up" json:"thumb_up"`
Errors *string `gorm:"column:errors;type:text" json:"errors,omitempty"`
VersionTitle *string `gorm:"column:version_title;size:255" json:"version_title,omitempty"`
BaseModel
}
// TableName specify table name
func (API4Conversation) TableName() string {
return "api_4_conversation"
}

View File

@@ -958,8 +958,8 @@ func (s *UserService) GetUserByAPIToken(ctx context.Context, authorization strin
// Query API token from database
apiTokenDAO := dao.NewAPITokenDAO()
userToken, err := apiTokenDAO.GetUserByAPIToken(ctx, dao.DB, token)
if err != nil {
userToken, err := apiTokenDAO.GetByAPIToken(ctx, dao.DB, token)
if err != nil || userToken == nil {
return nil, common.CodeUnauthorized, fmt.Errorf("invalid access token")
}