duidui_fiber/internal/question/dao/paper_dao_mongo.go
2026-03-27 10:34:03 +08:00

450 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}