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

345 lines
9.2 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"
"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
}