package repository import ( "context" "strings" app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" "haixun-backend/internal/model/content_ops/domain/entity" domrepo "haixun-backend/internal/model/content_ops/domain/repository" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) type mongoRepository struct { topics *mongo.Collection plans *mongo.Collection feedback *mongo.Collection sources *mongo.Collection chunks *mongo.Collection formulas *mongo.Collection } func NewMongoRepository(db *mongo.Database) domrepo.Repository { if db == nil { return &mongoRepository{} } return &mongoRepository{ topics: db.Collection(entity.TopicCandidateCollectionName), plans: db.Collection(entity.ContentPlanCollectionName), feedback: db.Collection(entity.FeedbackEventCollectionName), sources: db.Collection(entity.KnowledgeSourceCollectionName), chunks: db.Collection(entity.KnowledgeChunkCollectionName), formulas: db.Collection(entity.FormulaPoolCollectionName), } } func (r *mongoRepository) EnsureIndexes(ctx context.Context) error { if r.topics == nil || r.plans == nil || r.feedback == nil || r.sources == nil || r.chunks == nil || r.formulas == nil { return nil } if _, err := r.topics.Indexes().CreateMany(ctx, []mongo.IndexModel{{Keys: bson.D{{Key: "tenant_id", Value: 1}, {Key: "owner_uid", Value: 1}, {Key: "persona_id", Value: 1}, {Key: "create_at", Value: -1}}}}); err != nil { return err } if _, err := r.plans.Indexes().CreateMany(ctx, []mongo.IndexModel{{Keys: bson.D{{Key: "tenant_id", Value: 1}, {Key: "owner_uid", Value: 1}, {Key: "persona_id", Value: 1}, {Key: "create_at", Value: -1}}}, {Keys: bson.D{{Key: "topic_candidate_id", Value: 1}}}}); err != nil { return err } _, err := r.feedback.Indexes().CreateMany(ctx, []mongo.IndexModel{{Keys: bson.D{{Key: "tenant_id", Value: 1}, {Key: "owner_uid", Value: 1}, {Key: "persona_id", Value: 1}, {Key: "create_at", Value: -1}}}, {Keys: bson.D{{Key: "content_plan_id", Value: 1}}}, {Keys: bson.D{{Key: "draft_id", Value: 1}}}}) if err != nil { return err } if _, err := r.sources.Indexes().CreateMany(ctx, []mongo.IndexModel{{Keys: bson.D{{Key: "tenant_id", Value: 1}, {Key: "owner_uid", Value: 1}, {Key: "persona_id", Value: 1}, {Key: "create_at", Value: -1}}}}); err != nil { return err } if _, err := r.chunks.Indexes().CreateMany(ctx, []mongo.IndexModel{{Keys: bson.D{{Key: "tenant_id", Value: 1}, {Key: "owner_uid", Value: 1}, {Key: "persona_id", Value: 1}, {Key: "create_at", Value: -1}}}, {Keys: bson.D{{Key: "source_id", Value: 1}}}}); err != nil { return err } _, err = r.formulas.Indexes().CreateMany(ctx, []mongo.IndexModel{{Keys: bson.D{{Key: "tenant_id", Value: 1}, {Key: "owner_uid", Value: 1}, {Key: "persona_id", Value: 1}, {Key: "type", Value: 1}}}}) return err } func personaFilter(tenantID, ownerUID, personaID string) bson.M { return bson.M{"tenant_id": strings.TrimSpace(tenantID), "owner_uid": strings.TrimSpace(ownerUID), "persona_id": strings.TrimSpace(personaID)} } func normalizeLimit(limit int) int64 { if limit <= 0 { limit = 50 } if limit > 200 { limit = 200 } return int64(limit) } func (r *mongoRepository) CreateTopicCandidate(ctx context.Context, item *entity.TopicCandidate) error { if r.topics == nil { return app.For(code.Persona).DBUnavailable("Mongo is not configured") } _, err := r.topics.InsertOne(ctx, item) return err } func (r *mongoRepository) ListTopicCandidates(ctx context.Context, tenantID, ownerUID, personaID string, limit int) ([]entity.TopicCandidate, error) { if r.topics == nil { return nil, app.For(code.Persona).DBUnavailable("Mongo is not configured") } cur, err := r.topics.Find(ctx, personaFilter(tenantID, ownerUID, personaID), options.Find().SetSort(bson.D{{Key: "create_at", Value: -1}}).SetLimit(normalizeLimit(limit))) if err != nil { return nil, err } defer cur.Close(ctx) var out []entity.TopicCandidate return out, cur.All(ctx, &out) } func (r *mongoRepository) CreateContentPlan(ctx context.Context, item *entity.ContentPlan) error { if r.plans == nil { return app.For(code.Persona).DBUnavailable("Mongo is not configured") } _, err := r.plans.InsertOne(ctx, item) return err } func (r *mongoRepository) UpdateContentPlan(ctx context.Context, tenantID, ownerUID, personaID, id string, patch map[string]any) (*entity.ContentPlan, error) { if r.plans == nil { return nil, app.For(code.Persona).DBUnavailable("Mongo is not configured") } filter := personaFilter(tenantID, ownerUID, personaID) filter["_id"] = strings.TrimSpace(id) var out entity.ContentPlan err := r.plans.FindOneAndUpdate(ctx, filter, bson.M{"$set": patch}, options.FindOneAndUpdate().SetReturnDocument(options.After)).Decode(&out) if err == mongo.ErrNoDocuments { return nil, app.For(code.Persona).ResNotFound("找不到內容計畫") } return &out, err } func (r *mongoRepository) GetContentPlan(ctx context.Context, tenantID, ownerUID, personaID, id string) (*entity.ContentPlan, error) { if r.plans == nil { return nil, app.For(code.Persona).DBUnavailable("Mongo is not configured") } filter := personaFilter(tenantID, ownerUID, personaID) filter["_id"] = strings.TrimSpace(id) var out entity.ContentPlan err := r.plans.FindOne(ctx, filter).Decode(&out) if err == mongo.ErrNoDocuments { return nil, app.For(code.Persona).ResNotFound("找不到內容計畫") } return &out, err } func (r *mongoRepository) ListContentPlans(ctx context.Context, tenantID, ownerUID, personaID string, limit int) ([]entity.ContentPlan, error) { if r.plans == nil { return nil, app.For(code.Persona).DBUnavailable("Mongo is not configured") } cur, err := r.plans.Find(ctx, personaFilter(tenantID, ownerUID, personaID), options.Find().SetSort(bson.D{{Key: "create_at", Value: -1}}).SetLimit(normalizeLimit(limit))) if err != nil { return nil, err } defer cur.Close(ctx) var out []entity.ContentPlan return out, cur.All(ctx, &out) } func (r *mongoRepository) CreateFeedbackEvent(ctx context.Context, item *entity.FeedbackEvent) error { if r.feedback == nil { return app.For(code.Persona).DBUnavailable("Mongo is not configured") } _, err := r.feedback.InsertOne(ctx, item) return err } func (r *mongoRepository) ListFeedbackEvents(ctx context.Context, tenantID, ownerUID, personaID string, limit int) ([]entity.FeedbackEvent, error) { if r.feedback == nil { return nil, app.For(code.Persona).DBUnavailable("Mongo is not configured") } cur, err := r.feedback.Find(ctx, personaFilter(tenantID, ownerUID, personaID), options.Find().SetSort(bson.D{{Key: "create_at", Value: -1}}).SetLimit(normalizeLimit(limit))) if err != nil { return nil, err } defer cur.Close(ctx) var out []entity.FeedbackEvent return out, cur.All(ctx, &out) } func (r *mongoRepository) CreateKnowledgeSource(ctx context.Context, item *entity.KnowledgeSource, chunks []*entity.KnowledgeChunk) error { if r.sources == nil || r.chunks == nil { return app.For(code.Persona).DBUnavailable("Mongo is not configured") } if _, err := r.sources.InsertOne(ctx, item); err != nil { return err } if len(chunks) == 0 { return nil } docs := make([]any, 0, len(chunks)) for _, c := range chunks { docs = append(docs, c) } _, err := r.chunks.InsertMany(ctx, docs) return err } func (r *mongoRepository) ListKnowledgeSources(ctx context.Context, tenantID, ownerUID, personaID string, limit int) ([]entity.KnowledgeSource, error) { if r.sources == nil { return nil, app.For(code.Persona).DBUnavailable("Mongo is not configured") } cur, err := r.sources.Find(ctx, personaFilter(tenantID, ownerUID, personaID), options.Find().SetSort(bson.D{{Key: "create_at", Value: -1}}).SetLimit(normalizeLimit(limit))) if err != nil { return nil, err } defer cur.Close(ctx) var out []entity.KnowledgeSource return out, cur.All(ctx, &out) } func (r *mongoRepository) ListKnowledgeChunks(ctx context.Context, tenantID, ownerUID, personaID string, limit int) ([]entity.KnowledgeChunk, error) { if r.chunks == nil { return nil, app.For(code.Persona).DBUnavailable("Mongo is not configured") } cur, err := r.chunks.Find(ctx, personaFilter(tenantID, ownerUID, personaID), options.Find().SetSort(bson.D{{Key: "create_at", Value: -1}}).SetLimit(normalizeLimit(limit))) if err != nil { return nil, err } defer cur.Close(ctx) var out []entity.KnowledgeChunk return out, cur.All(ctx, &out) } func (r *mongoRepository) CreateFormulaPool(ctx context.Context, item *entity.FormulaPool) error { if r.formulas == nil { return app.For(code.Persona).DBUnavailable("Mongo is not configured") } _, err := r.formulas.InsertOne(ctx, item) return err } func (r *mongoRepository) ListFormulaPools(ctx context.Context, tenantID, ownerUID, personaID string, limit int) ([]entity.FormulaPool, error) { if r.formulas == nil { return nil, app.For(code.Persona).DBUnavailable("Mongo is not configured") } cur, err := r.formulas.Find(ctx, personaFilter(tenantID, ownerUID, personaID), options.Find().SetSort(bson.D{{Key: "weight", Value: -1}, {Key: "create_at", Value: -1}}).SetLimit(normalizeLimit(limit))) if err != nil { return nil, err } defer cur.Close(ctx) var out []entity.FormulaPool return out, cur.All(ctx, &out) }