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 }