package repository import ( "context" "strings" "haixun-backend/internal/library/clock" app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" "haixun-backend/internal/model/member/domain/entity" domrepo "haixun-backend/internal/model/member/domain/repository" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) type mongoRepository struct { collection *mongo.Collection } func NewMongoRepository(db *mongo.Database) domrepo.Repository { if db == nil { return &mongoRepository{} } return &mongoRepository{collection: db.Collection(entity.CollectionName)} } func (r *mongoRepository) EnsureIndexes(ctx context.Context) error { if r.collection == nil { return nil } _, err := r.collection.Indexes().CreateMany(ctx, []mongo.IndexModel{ {Keys: bson.D{{Key: "tenant_id", Value: 1}, {Key: "uid", Value: 1}}, Options: options.Index().SetUnique(true)}, {Keys: bson.D{{Key: "tenant_id", Value: 1}, {Key: "email", Value: 1}}, Options: options.Index().SetUnique(true)}, }) return err } func (r *mongoRepository) Create(ctx context.Context, member *entity.Member) (*entity.Member, error) { if r.collection == nil { return nil, app.For(code.Member).DBUnavailable("Mongo is not configured") } now := clock.NowUnixNano() member.Email = normalizeEmail(member.Email) member.CreateAt = now member.UpdateAt = now if member.ID.IsZero() { member.ID = primitive.NewObjectID() } if member.Status == "" { member.Status = entity.StatusOpen } if member.Origin == "" { member.Origin = entity.OriginNative } _, err := r.collection.InsertOne(ctx, member) if mongo.IsDuplicateKeyError(err) { return nil, app.For(code.Member).ResConflict("member already exists") } if err != nil { return nil, err } return member, nil } func (r *mongoRepository) List(ctx context.Context, tenantID string, page, pageSize int64) ([]*entity.Member, int64, int64, int64, error) { if r.collection == nil { return nil, 0, page, pageSize, app.For(code.Member).DBUnavailable("Mongo is not configured") } if tenantID == "" { return nil, 0, page, pageSize, app.For(code.Member).InputMissingRequired("tenant_id is required") } page, pageSize = normalizePagination(page, pageSize) filter := bson.M{"tenant_id": tenantID} total, err := r.collection.CountDocuments(ctx, filter) if err != nil { return nil, 0, page, pageSize, err } cursor, err := r.collection.Find(ctx, filter, options.Find().SetSort(bson.D{{Key: "create_at", Value: -1}}).SetSkip((page-1)*pageSize).SetLimit(pageSize)) if err != nil { return nil, 0, page, pageSize, err } defer cursor.Close(ctx) items := make([]*entity.Member, 0) for cursor.Next(ctx) { var item entity.Member if err := cursor.Decode(&item); err != nil { return nil, 0, page, pageSize, err } items = append(items, &item) } if err := cursor.Err(); err != nil { return nil, 0, page, pageSize, err } return items, total, page, pageSize, nil } func (r *mongoRepository) FindByUID(ctx context.Context, tenantID, uid string) (*entity.Member, error) { return r.findOne(ctx, bson.M{"tenant_id": tenantID, "uid": uid}) } func (r *mongoRepository) FindByEmail(ctx context.Context, tenantID, email string) (*entity.Member, error) { return r.findOne(ctx, bson.M{"tenant_id": tenantID, "email": normalizeEmail(email)}) } func (r *mongoRepository) SetActiveThreadsAccountID(ctx context.Context, tenantID, uid, accountID string) error { if r.collection == nil { return app.For(code.Member).DBUnavailable("Mongo is not configured") } if tenantID == "" || uid == "" { return app.For(code.Member).InputMissingRequired("tenant_id and uid are required") } res, err := r.collection.UpdateOne( ctx, bson.M{"tenant_id": tenantID, "uid": uid}, bson.M{"$set": bson.M{"active_threads_account_id": strings.TrimSpace(accountID), "update_at": clock.NowUnixNano()}}, ) if err != nil { return err } if res.MatchedCount == 0 { return app.For(code.Member).ResNotFound("member not found") } return nil } func (r *mongoRepository) SetRoles(ctx context.Context, tenantID, uid string, roles []string) error { if r.collection == nil { return app.For(code.Member).DBUnavailable("Mongo is not configured") } if tenantID == "" || uid == "" { return app.For(code.Member).InputMissingRequired("tenant_id and uid are required") } res, err := r.collection.UpdateOne( ctx, bson.M{"tenant_id": tenantID, "uid": uid}, bson.M{"$set": bson.M{"roles": roles, "update_at": clock.NowUnixNano()}}, ) if err != nil { return err } if res.MatchedCount == 0 { return app.For(code.Member).ResNotFound("member not found") } return nil } func (r *mongoRepository) UpdateProfile(ctx context.Context, tenantID, uid string, update domrepo.ProfileUpdate) (*entity.Member, error) { if r.collection == nil { return nil, app.For(code.Member).DBUnavailable("Mongo is not configured") } set := bson.M{"update_at": clock.NowUnixNano()} if update.DisplayName != nil { set["display_name"] = *update.DisplayName } if update.Avatar != nil { set["avatar"] = *update.Avatar } if update.Language != nil { set["language"] = *update.Language } if update.Currency != nil { set["currency"] = *update.Currency } if update.Phone != nil { set["phone"] = *update.Phone } if update.EmailVerified != nil { set["email_verified"] = *update.EmailVerified } if update.Status != nil { set["status"] = *update.Status } if update.BusinessEmail != nil { set["business_email"] = *update.BusinessEmail } if update.BusinessEmailVerified != nil { set["business_email_verified"] = *update.BusinessEmailVerified } if update.BusinessPhone != nil { set["business_phone"] = *update.BusinessPhone } if update.BusinessPhoneVerified != nil { set["business_phone_verified"] = *update.BusinessPhoneVerified } var out entity.Member err := r.collection.FindOneAndUpdate( ctx, bson.M{"tenant_id": tenantID, "uid": uid}, bson.M{"$set": set}, options.FindOneAndUpdate().SetReturnDocument(options.After), ).Decode(&out) if err == mongo.ErrNoDocuments { return nil, app.For(code.Member).ResNotFound("member not found") } return &out, err } func (r *mongoRepository) UpdatePassword(ctx context.Context, tenantID, uid, passwordHash string) error { if r.collection == nil { return app.For(code.Member).DBUnavailable("Mongo is not configured") } if tenantID == "" || uid == "" { return app.For(code.Member).InputMissingRequired("tenant_id and uid are required") } res, err := r.collection.UpdateOne( ctx, bson.M{"tenant_id": tenantID, "uid": uid}, bson.M{"$set": bson.M{"password_hash": passwordHash, "update_at": clock.NowUnixNano()}}, ) if err != nil { return err } if res.MatchedCount == 0 { return app.For(code.Member).ResNotFound("member not found") } return nil } func (r *mongoRepository) ConfirmEmailVerification(ctx context.Context, tenantID, uid, verifyCode string) (*entity.Member, error) { if r.collection == nil { return nil, app.For(code.Member).DBUnavailable("Mongo is not configured") } member, err := r.FindByUID(ctx, tenantID, uid) if err != nil { return nil, err } if member.EmailVerified { return member, nil } normalizedCode := strings.TrimSpace(verifyCode) if normalizedCode == "" || member.EmailVerifyCode == "" || member.EmailVerifyCode != normalizedCode { return nil, app.For(code.Auth).AuthUnauthorized("invalid verification code") } if member.EmailVerifyExpiresAt > 0 && clock.NowUnixNano() > member.EmailVerifyExpiresAt { return nil, app.For(code.Auth).AuthUnauthorized("verification code expired") } res, err := r.collection.UpdateOne( ctx, bson.M{"tenant_id": tenantID, "uid": uid, "email_verify_code": member.EmailVerifyCode}, bson.M{ "$set": bson.M{ "email_verified": true, "update_at": clock.NowUnixNano(), }, "$unset": bson.M{ "email_verify_code": "", "email_verify_expires_at": "", }, }, ) if err != nil { return nil, err } if res.MatchedCount == 0 { return nil, app.For(code.Auth).AuthUnauthorized("invalid verification code") } return r.FindByUID(ctx, tenantID, uid) } func (r *mongoRepository) SetEmailVerificationCode(ctx context.Context, tenantID, uid, verifyCode string, expiresAt int64) error { if r.collection == nil { return app.For(code.Member).DBUnavailable("Mongo is not configured") } if tenantID == "" || uid == "" { return app.For(code.Member).InputMissingRequired("tenant_id and uid are required") } res, err := r.collection.UpdateOne( ctx, bson.M{"tenant_id": tenantID, "uid": uid}, bson.M{"$set": bson.M{"email_verify_code": verifyCode, "email_verify_expires_at": expiresAt, "email_verified": false, "update_at": clock.NowUnixNano()}}, ) if err != nil { return err } if res.MatchedCount == 0 { return app.For(code.Member).ResNotFound("member not found") } return nil } func (r *mongoRepository) findOne(ctx context.Context, filter bson.M) (*entity.Member, error) { if r.collection == nil { return nil, app.For(code.Member).DBUnavailable("Mongo is not configured") } var out entity.Member err := r.collection.FindOne(ctx, filter).Decode(&out) if err == mongo.ErrNoDocuments { return nil, app.For(code.Member).ResNotFound("member not found") } if err != nil { return nil, err } return &out, nil } func normalizeEmail(email string) string { return strings.ToLower(strings.TrimSpace(email)) } func normalizePagination(page, pageSize int64) (int64, int64) { if page < 1 { page = 1 } if pageSize < 1 { pageSize = 20 } if pageSize > 100 { pageSize = 100 } return page, pageSize }