package dao import ( "context" "fmt" "log" "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" ) // PaperDAOMongo MongoDB 实现的试卷数据访问对象 type PaperDAOMongo struct { client *database.MongoDBClient collection *mongo.Collection questionDAO QuestionDAOInterface // 用于获取题目详情 } // NewPaperDAOMongo 创建 MongoDB 试卷 DAO func NewPaperDAOMongo(client *database.MongoDBClient, questionDAO QuestionDAOInterface) *PaperDAOMongo { return &PaperDAOMongo{ client: client, collection: client.Collection("papers"), questionDAO: questionDAO, } } // PaperDocument MongoDB 文档结构 // 使用引用方式:存储题目ID数组,而不是嵌入题目数据 type PaperDocument struct { ID string `bson:"_id" json:"id"` Title string `bson:"title" json:"title"` Description string `bson:"description" json:"description"` Source string `bson:"source,omitempty" json:"source,omitempty"` // 题目出处(可选) QuestionIDs []string `bson:"question_ids" json:"question_ids"` // 引用:题目ID数组 MaterialIDs []string `bson:"material_ids,omitempty" json:"material_ids,omitempty"` // 关联的材料ID列表(可选,用于主观题组卷) 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 *PaperDAOMongo) Create(paper *question.Paper) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // 确保 MaterialIDs 不为 nil materialIDs := paper.MaterialIDs if materialIDs == nil { materialIDs = []string{} } doc := &PaperDocument{ ID: paper.ID, Title: paper.Title, Description: paper.Description, Source: paper.Source, QuestionIDs: paper.QuestionIDs, MaterialIDs: materialIDs, CreatedAt: paper.CreatedAt, UpdatedAt: paper.UpdatedAt, } _, err := dao.collection.InsertOne(ctx, doc) if err != nil { return fmt.Errorf("插入试卷失败: %v", err) } return nil } // GetByID 根据ID获取试卷 func (dao *PaperDAOMongo) GetByID(id string) (*question.Paper, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() var doc PaperDocument filter := bson.M{ "_id": id, "deleted_at": bson.M{"$exists": false}, } // 先检查是否存在(包括已删除的) var allDoc PaperDocument allFilter := bson.M{"_id": id} err := dao.collection.FindOne(ctx, allFilter).Decode(&allDoc) if err == nil { // 如果找到了,检查是否被删除 if allDoc.DeletedAt != nil { log.Printf("试卷 %s 存在但已被软删除 (deleted_at: %d)", id, *allDoc.DeletedAt) return nil, fmt.Errorf("试卷不存在") } } err = dao.collection.FindOne(ctx, filter).Decode(&doc) if err != nil { if err == mongo.ErrNoDocuments { log.Printf("试卷 %s 在数据库中不存在", id) return nil, fmt.Errorf("试卷不存在") } return nil, fmt.Errorf("查询试卷失败: %v", err) } return dao.documentToPaper(&doc), nil } // GetWithQuestions 获取试卷及题目详情 func (dao *PaperDAOMongo) GetWithQuestions(id string) (*question.Paper, error) { paper, err := dao.GetByID(id) if err != nil { return nil, err } // 通过引用获取题目详情 if len(paper.QuestionIDs) > 0 && dao.questionDAO != nil { var questions []*question.QuestionInfo for _, questionID := range paper.QuestionIDs { q, err := dao.questionDAO.GetByID(questionID) if err != nil { // 如果题目不存在,跳过 continue } questionInfo := &question.QuestionInfo{ ID: q.ID, Type: q.Type, Content: q.Content, Answer: q.Answer, Explanation: q.Explanation, Options: q.Options, KnowledgeTreeIDs: q.KnowledgeTreeIDs, MaterialID: q.MaterialID, } questions = append(questions, questionInfo) } paper.Questions = questions } return paper, nil } // Search 搜索试卷 func (dao *PaperDAOMongo) Search(query string, page, pageSize int32) ([]*question.Paper, 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{ {"title": bson.M{"$regex": query, "$options": "i"}}, {"description": bson.M{"$regex": query, "$options": "i"}}, } } // 查询总数 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 []PaperDocument if err := cursor.All(ctx, &docs); err != nil { return nil, 0, fmt.Errorf("解析试卷数据失败: %v", err) } papers := make([]*question.Paper, len(docs)) for i, doc := range docs { papers[i] = dao.documentToPaper(&doc) } return papers, int32(total), nil } // Update 更新试卷 func (dao *PaperDAOMongo) Update(paper *question.Paper) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // 确保 MaterialIDs 不为 nil materialIDs := paper.MaterialIDs if materialIDs == nil { materialIDs = []string{} } // 确保 QuestionIDs 不为 nil questionIDs := paper.QuestionIDs if questionIDs == nil { questionIDs = []string{} } filter := bson.M{ "_id": paper.ID, "deleted_at": bson.M{"$exists": false}, } update := bson.M{ "$set": bson.M{ "title": paper.Title, "description": paper.Description, "source": paper.Source, "question_ids": questionIDs, "material_ids": materialIDs, "updated_at": paper.UpdatedAt, }, } result, err := dao.collection.UpdateOne(ctx, filter, update) if err == nil { log.Printf("DAO Update - 更新结果: MatchedCount=%d, ModifiedCount=%d", result.MatchedCount, result.ModifiedCount) } if err != nil { return fmt.Errorf("更新试卷失败: %v", err) } if result.MatchedCount == 0 { return fmt.Errorf("试卷不存在或已被删除") } return nil } // Delete 删除试卷(软删除) func (dao *PaperDAOMongo) Delete(id string) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() filter := bson.M{ "_id": id, "deleted_at": bson.M{"$exists": false}, } 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 } // BatchDelete 批量删除试卷 func (dao *PaperDAOMongo) 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 } // AddQuestions 添加题目到试卷(使用引用方式) func (dao *PaperDAOMongo) AddQuestions(paperID string, questionIDs []string) (int, []string, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // 获取当前试卷 paper, err := dao.GetByID(paperID) if err != nil { return 0, questionIDs, err } // 合并题目ID(去重) existingIDs := make(map[string]bool) for _, id := range paper.QuestionIDs { existingIDs[id] = true } var added int var failed []string var newIDs []string for _, questionID := range questionIDs { if existingIDs[questionID] { // 已存在,跳过 continue } newIDs = append(newIDs, questionID) added++ } if len(newIDs) > 0 { // 更新试卷的题目ID数组 allIDs := append(paper.QuestionIDs, newIDs...) filter := bson.M{"_id": paperID} update := bson.M{ "$set": bson.M{ "question_ids": allIDs, "updated_at": question.GetCurrentTimestamp(), }, } _, err := dao.collection.UpdateOne(ctx, filter, update) if err != nil { return 0, questionIDs, fmt.Errorf("添加题目失败: %v", err) } } return added, failed, nil } // RemoveQuestions 从试卷移除题目(使用引用方式) func (dao *PaperDAOMongo) RemoveQuestions(paperID string, questionIDs []string) (int, []string, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // 获取当前试卷 paper, err := dao.GetByID(paperID) if err != nil { return 0, questionIDs, err } // 构建要移除的ID集合 removeSet := make(map[string]bool) for _, id := range questionIDs { removeSet[id] = true } // 过滤出保留的题目ID var remainingIDs []string for _, id := range paper.QuestionIDs { if !removeSet[id] { remainingIDs = append(remainingIDs, id) } } removed := len(paper.QuestionIDs) - len(remainingIDs) // 更新试卷 filter := bson.M{"_id": paperID} update := bson.M{ "$set": bson.M{ "question_ids": remainingIDs, "updated_at": question.GetCurrentTimestamp(), }, } _, err = dao.collection.UpdateOne(ctx, filter, update) if err != nil { return 0, questionIDs, fmt.Errorf("移除题目失败: %v", err) } return removed, []string{}, nil } // documentToPaper 将 MongoDB 文档转换为 Paper 对象 func (dao *PaperDAOMongo) documentToPaper(doc *PaperDocument) *question.Paper { return &question.Paper{ ID: doc.ID, Title: doc.Title, Description: doc.Description, Source: doc.Source, QuestionIDs: doc.QuestionIDs, MaterialIDs: doc.MaterialIDs, CreatedAt: doc.CreatedAt, UpdatedAt: doc.UpdatedAt, } } // CreateIndexes 创建索引 func (dao *PaperDAOMongo) CreateIndexes() error { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() indexes := []mongo.IndexModel{ { Keys: bson.D{ {Key: "title", Value: 1}, }, }, { Keys: bson.D{ {Key: "question_ids", Value: 1}, }, }, { Keys: bson.D{ {Key: "created_at", Value: -1}, }, }, { Keys: bson.D{ {Key: "title", Value: "text"}, {Key: "description", Value: "text"}, }, Options: options.Index().SetName("text_index"), }, } _, err := dao.collection.Indexes().CreateMany(ctx, indexes) if err != nil { return fmt.Errorf("创建索引失败: %v", err) } return nil }