345 lines
9.2 KiB
Go
345 lines
9.2 KiB
Go
package dao
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"time"
|
||
|
||
"dd_fiber_api/internal/question"
|
||
"dd_fiber_api/pkg/database"
|
||
|
||
"go.mongodb.org/mongo-driver/bson"
|
||
"go.mongodb.org/mongo-driver/mongo"
|
||
"go.mongodb.org/mongo-driver/mongo/options"
|
||
)
|
||
|
||
// QuestionDAOMongo MongoDB 实现的题目数据访问对象
|
||
type QuestionDAOMongo struct {
|
||
client *database.MongoDBClient
|
||
collection *mongo.Collection
|
||
}
|
||
|
||
// NewQuestionDAOMongo 创建 MongoDB 题目 DAO
|
||
func NewQuestionDAOMongo(client *database.MongoDBClient) *QuestionDAOMongo {
|
||
return &QuestionDAOMongo{
|
||
client: client,
|
||
collection: client.Collection("questions"),
|
||
}
|
||
}
|
||
|
||
// QuestionDocument MongoDB 文档结构
|
||
type QuestionDocument struct {
|
||
ID string `bson:"_id" json:"id"`
|
||
Type int32 `bson:"type" json:"type"`
|
||
Name string `bson:"name,omitempty" json:"name,omitempty"` // 题目名称(可选)
|
||
Source string `bson:"source,omitempty" json:"source,omitempty"` // 题目出处(可选)
|
||
MaterialID string `bson:"material_id,omitempty" json:"material_id,omitempty"` // 关联材料ID(引用)
|
||
Content string `bson:"content" json:"content"`
|
||
Options []string `bson:"options" json:"options"`
|
||
Answer string `bson:"answer" json:"answer"`
|
||
Explanation string `bson:"explanation" json:"explanation"`
|
||
KnowledgeTreeIDs []string `bson:"knowledge_tree_ids" json:"knowledge_tree_ids"` // 关联的知识树ID列表(替代原来的tags)
|
||
CreatedAt int64 `bson:"created_at" json:"created_at"`
|
||
UpdatedAt int64 `bson:"updated_at" json:"updated_at"`
|
||
DeletedAt *int64 `bson:"deleted_at,omitempty" json:"deleted_at,omitempty"`
|
||
}
|
||
|
||
// Create 创建题目
|
||
func (dao *QuestionDAOMongo) Create(question *question.Question) error {
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer cancel()
|
||
|
||
// 确保 KnowledgeTreeIDs 不为 nil(即使是空数组也要写入)
|
||
knowledgeTreeIDs := question.KnowledgeTreeIDs
|
||
if knowledgeTreeIDs == nil {
|
||
knowledgeTreeIDs = []string{}
|
||
}
|
||
|
||
doc := &QuestionDocument{
|
||
ID: question.ID,
|
||
Type: int32(question.Type),
|
||
Name: question.Name,
|
||
Source: question.Source,
|
||
MaterialID: question.MaterialID,
|
||
Content: question.Content,
|
||
Options: question.Options,
|
||
Answer: question.Answer,
|
||
Explanation: question.Explanation,
|
||
KnowledgeTreeIDs: knowledgeTreeIDs,
|
||
CreatedAt: question.CreatedAt,
|
||
UpdatedAt: question.UpdatedAt,
|
||
}
|
||
|
||
_, err := dao.collection.InsertOne(ctx, doc)
|
||
if err != nil {
|
||
return fmt.Errorf("插入题目失败: %v", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetByID 根据ID获取题目
|
||
func (dao *QuestionDAOMongo) GetByID(id string) (*question.Question, error) {
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer cancel()
|
||
|
||
var doc QuestionDocument
|
||
filter := bson.M{
|
||
"_id": id,
|
||
"deleted_at": bson.M{"$exists": false},
|
||
}
|
||
|
||
err := dao.collection.FindOne(ctx, filter).Decode(&doc)
|
||
if err != nil {
|
||
if err == mongo.ErrNoDocuments {
|
||
return nil, fmt.Errorf("题目不存在")
|
||
}
|
||
return nil, fmt.Errorf("查询题目失败: %v", err)
|
||
}
|
||
|
||
questionObj := dao.documentToQuestion(&doc)
|
||
return questionObj, nil
|
||
}
|
||
|
||
// Search 搜索题目
|
||
func (dao *QuestionDAOMongo) Search(query string, qType question.QuestionType, knowledgeTreeIds []string, page, pageSize int32) ([]*question.Question, int32, error) {
|
||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||
defer cancel()
|
||
|
||
// 构建查询条件
|
||
filter := bson.M{
|
||
"deleted_at": bson.M{"$exists": false},
|
||
}
|
||
|
||
// 全文搜索(支持名称、内容、出处)
|
||
if query != "" {
|
||
filter["$or"] = []bson.M{
|
||
{"name": bson.M{"$regex": query, "$options": "i"}},
|
||
{"content": bson.M{"$regex": query, "$options": "i"}},
|
||
{"source": bson.M{"$regex": query, "$options": "i"}},
|
||
}
|
||
}
|
||
|
||
// 题目类型过滤
|
||
if qType != question.QuestionTypeUnspecified {
|
||
filter["type"] = int32(qType)
|
||
}
|
||
|
||
// 知识树过滤
|
||
if len(knowledgeTreeIds) > 0 {
|
||
filter["knowledge_tree_ids"] = bson.M{"$in": knowledgeTreeIds}
|
||
}
|
||
|
||
// 查询总数
|
||
total, err := dao.collection.CountDocuments(ctx, filter)
|
||
if err != nil {
|
||
return nil, 0, fmt.Errorf("查询总数失败: %v", err)
|
||
}
|
||
|
||
// 查询数据
|
||
skip := int64((page - 1) * pageSize)
|
||
limit := int64(pageSize)
|
||
|
||
opts := options.Find().
|
||
SetSkip(skip).
|
||
SetLimit(limit).
|
||
SetSort(bson.M{"created_at": -1})
|
||
|
||
cursor, err := dao.collection.Find(ctx, filter, opts)
|
||
if err != nil {
|
||
return nil, 0, fmt.Errorf("查询题目失败: %v", err)
|
||
}
|
||
defer cursor.Close(ctx)
|
||
|
||
var docs []QuestionDocument
|
||
if err := cursor.All(ctx, &docs); err != nil {
|
||
return nil, 0, fmt.Errorf("解析题目数据失败: %v", err)
|
||
}
|
||
|
||
questions := make([]*question.Question, len(docs))
|
||
for i, doc := range docs {
|
||
questions[i] = dao.documentToQuestion(&doc)
|
||
}
|
||
|
||
return questions, int32(total), nil
|
||
}
|
||
|
||
// Update 更新题目
|
||
func (dao *QuestionDAOMongo) Update(question *question.Question) error {
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer cancel()
|
||
|
||
// 确保 KnowledgeTreeIDs 不为 nil(即使是空数组也要写入)
|
||
knowledgeTreeIDs := question.KnowledgeTreeIDs
|
||
if knowledgeTreeIDs == nil {
|
||
knowledgeTreeIDs = []string{}
|
||
}
|
||
|
||
filter := bson.M{"_id": question.ID}
|
||
update := bson.M{
|
||
"$set": bson.M{
|
||
"type": int32(question.Type),
|
||
"name": question.Name,
|
||
"source": question.Source,
|
||
"material_id": question.MaterialID,
|
||
"content": question.Content,
|
||
"options": question.Options,
|
||
"answer": question.Answer,
|
||
"explanation": question.Explanation,
|
||
"knowledge_tree_ids": knowledgeTreeIDs,
|
||
"updated_at": question.UpdatedAt,
|
||
},
|
||
}
|
||
|
||
result, err := dao.collection.UpdateOne(ctx, filter, update)
|
||
if err != nil {
|
||
return fmt.Errorf("更新题目失败: %v", err)
|
||
}
|
||
if result.MatchedCount == 0 {
|
||
return fmt.Errorf("题目不存在")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// Delete 删除题目(软删除)
|
||
func (dao *QuestionDAOMongo) Delete(id string) error {
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer cancel()
|
||
|
||
filter := bson.M{"_id": id}
|
||
now := time.Now().Unix()
|
||
update := bson.M{
|
||
"$set": bson.M{
|
||
"deleted_at": now,
|
||
"updated_at": now,
|
||
},
|
||
}
|
||
|
||
result, err := dao.collection.UpdateOne(ctx, filter, update)
|
||
if err != nil {
|
||
return fmt.Errorf("删除题目失败: %v", err)
|
||
}
|
||
if result.MatchedCount == 0 {
|
||
return fmt.Errorf("题目不存在")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// documentToQuestion 将 MongoDB 文档转换为 Question 对象
|
||
func (dao *QuestionDAOMongo) documentToQuestion(doc *QuestionDocument) *question.Question {
|
||
// 确保 KnowledgeTreeIDs 不为 nil
|
||
knowledgeTreeIDs := doc.KnowledgeTreeIDs
|
||
if knowledgeTreeIDs == nil {
|
||
knowledgeTreeIDs = []string{}
|
||
}
|
||
|
||
return &question.Question{
|
||
ID: doc.ID,
|
||
Type: question.QuestionType(doc.Type),
|
||
Name: doc.Name,
|
||
Source: doc.Source,
|
||
MaterialID: doc.MaterialID,
|
||
Content: doc.Content,
|
||
Options: doc.Options,
|
||
Answer: doc.Answer,
|
||
Explanation: doc.Explanation,
|
||
KnowledgeTreeIDs: knowledgeTreeIDs,
|
||
CreatedAt: doc.CreatedAt,
|
||
UpdatedAt: doc.UpdatedAt,
|
||
}
|
||
}
|
||
|
||
// BatchDelete 批量删除题目
|
||
func (dao *QuestionDAOMongo) BatchDelete(ids []string) (int, []string, error) {
|
||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||
defer cancel()
|
||
|
||
var deleted int
|
||
var failed []string
|
||
now := time.Now().Unix()
|
||
|
||
for _, id := range ids {
|
||
filter := bson.M{"_id": id, "deleted_at": bson.M{"$exists": false}}
|
||
update := bson.M{
|
||
"$set": bson.M{
|
||
"deleted_at": now,
|
||
"updated_at": now,
|
||
},
|
||
}
|
||
|
||
result, err := dao.collection.UpdateOne(ctx, filter, update)
|
||
if err != nil {
|
||
failed = append(failed, id)
|
||
continue
|
||
}
|
||
if result.MatchedCount > 0 {
|
||
deleted++
|
||
} else {
|
||
failed = append(failed, id)
|
||
}
|
||
}
|
||
|
||
return deleted, failed, nil
|
||
}
|
||
|
||
// CreateIndexes 创建索引(在应用启动时调用)
|
||
func (dao *QuestionDAOMongo) CreateIndexes() error {
|
||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||
defer cancel()
|
||
|
||
// 先尝试删除旧的 text_index(如果存在且包含 title 字段)
|
||
// 忽略删除错误,因为索引可能不存在
|
||
_, _ = dao.collection.Indexes().DropOne(ctx, "text_index")
|
||
|
||
indexes := []mongo.IndexModel{
|
||
{
|
||
Keys: bson.D{
|
||
{Key: "type", Value: 1},
|
||
{Key: "created_at", Value: -1},
|
||
},
|
||
},
|
||
{
|
||
Keys: bson.D{
|
||
{Key: "knowledge_tree_ids", Value: 1},
|
||
},
|
||
},
|
||
{
|
||
Keys: bson.D{
|
||
{Key: "name", Value: 1},
|
||
},
|
||
},
|
||
{
|
||
Keys: bson.D{
|
||
{Key: "source", Value: 1},
|
||
},
|
||
},
|
||
{
|
||
Keys: bson.D{
|
||
{Key: "material_id", Value: 1},
|
||
},
|
||
},
|
||
{
|
||
Keys: bson.D{
|
||
{Key: "content", Value: "text"},
|
||
{Key: "name", Value: "text"},
|
||
{Key: "source", Value: "text"},
|
||
},
|
||
Options: options.Index().SetName("text_index"),
|
||
},
|
||
{
|
||
Keys: bson.D{
|
||
{Key: "created_at", Value: -1},
|
||
},
|
||
},
|
||
}
|
||
|
||
_, err := dao.collection.Indexes().CreateMany(ctx, indexes)
|
||
if err != nil {
|
||
return fmt.Errorf("创建索引失败: %v", err)
|
||
}
|
||
|
||
return nil
|
||
}
|