450 lines
11 KiB
Go
450 lines
11 KiB
Go
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
|
||
}
|