package repository import ( "backend/pkg/library/mongo" "backend/pkg/permission/domain" "backend/pkg/permission/domain/entity" "backend/pkg/permission/domain/repository" "context" "errors" "fmt" "time" "github.com/zeromicro/go-zero/core/stores/cache" "github.com/zeromicro/go-zero/core/stores/mon" "go.mongodb.org/mongo-driver/v2/bson" mongodriver "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" ) type RoleRepositoryParam struct { Conf *mongo.Conf CacheConf cache.CacheConf DBOpts []mon.Option CacheOpts []cache.Option } type RoleRepository struct { DB mongo.DocumentDBWithCacheUseCase } func NewRoleRepository(param RoleRepositoryParam) repository.RoleRepository { e := entity.Role{} documentDB, err := mongo.MustDocumentDBWithCache( param.Conf, e.CollectionName(), param.CacheConf, param.DBOpts, param.CacheOpts, ) if err != nil { panic(err) } return &RoleRepository{ DB: documentDB, } } // Create 建立角色 func (repo *RoleRepository) Create(ctx context.Context, role *entity.Role) error { if role.ID.IsZero() { role.ID = bson.NewObjectID() } // 設定時間戳記 now := time.Now().Unix() role.CreateTime = now role.UpdateTime = now _, err := repo.DB.GetClient().InsertOne(ctx, role) if err != nil { return err } // 清除相關快取 repo.clearRoleCache(ctx, role) return nil } // Update 更新角色 func (repo *RoleRepository) Update(ctx context.Context, role *entity.Role) error { // 更新時間戳記 role.UpdateTime = time.Now().Unix() filter := bson.M{"_id": role.ID} update := bson.M{"$set": role} _, err := repo.DB.GetClient().UpdateOne(ctx, filter, update) if err != nil { return err } // 清除相關快取 repo.clearRoleCache(ctx, role) return nil } // Delete 刪除角色 (軟刪除) func (repo *RoleRepository) Delete(ctx context.Context, uid string) error { now := time.Now().Unix() filter := bson.M{"uid": uid} update := bson.M{ "$set": bson.M{ "status": domain.RecordDeleted, "update_time": now, }, } _, err := repo.DB.GetClient().UpdateOne(ctx, filter, update) if err != nil { return err } // 清除快取 rk := domain.GetRoleUIDRedisKey(uid) _ = repo.DB.DelCache(ctx, rk) return nil } // Get 取得單一角色 (by ID) func (repo *RoleRepository) Get(ctx context.Context, id int64) (*entity.Role, error) { var data entity.Role rk := domain.GetRoleIDRedisKey(id) // 將 int64 ID 轉換為 ObjectID (假設 ID 可以轉換為 hex) // 注意:這裡可能需要根據實際的 ID 生成策略調整 oid := bson.NewObjectIDFromTimestamp(time.Unix(id, 0)) err := repo.DB.FindOne(ctx, rk, &data, bson.M{"_id": oid}) switch { case err == nil: return &data, nil case errors.Is(err, mon.ErrNotFound): return nil, ErrNotFound default: return nil, err } } // GetByUID 取得單一角色 (by UID) func (repo *RoleRepository) GetByUID(ctx context.Context, uid string) (*entity.Role, error) { var data entity.Role rk := domain.GetRoleUIDRedisKey(uid) err := repo.DB.FindOne(ctx, rk, &data, bson.M{"uid": uid}) switch { case err == nil: return &data, nil case errors.Is(err, mon.ErrNotFound): return nil, ErrNotFound default: return nil, err } } // GetByUIDs 批量取得角色 (by UIDs) func (repo *RoleRepository) GetByUIDs(ctx context.Context, uids []string) ([]*entity.Role, error) { var data []*entity.Role filter := bson.M{ "uid": bson.M{"$in": uids}, } err := repo.DB.GetClient().Find(ctx, &data, filter) if err != nil { if errors.Is(err, mon.ErrNotFound) { return nil, ErrNotFound } return nil, err } return data, nil } // List 列出所有角色 func (repo *RoleRepository) List(ctx context.Context, filter repository.RoleFilter) ([]*entity.Role, error) { var data []*entity.Role // 建立查詢條件 bsonFilter := bson.M{} // 如果有指定 ClientID if filter.ClientID > 0 { bsonFilter["client_id"] = filter.ClientID } // 如果有指定 UID if filter.UID != "" { bsonFilter["uid"] = filter.UID } // 如果有指定 Name (模糊搜尋) if filter.Name != "" { bsonFilter["name"] = bson.M{"$regex": filter.Name, "$options": "i"} } // 如果有指定狀態 if filter.Status != nil { bsonFilter["status"] = *filter.Status } err := repo.DB.GetClient().Find(ctx, &data, bsonFilter) if err != nil { if errors.Is(err, mon.ErrNotFound) { return []*entity.Role{}, nil } return nil, err } return data, nil } // Page 分頁查詢角色 func (repo *RoleRepository) Page(ctx context.Context, filter repository.RoleFilter, page, size int) ([]*entity.Role, int64, error) { var data []*entity.Role // 建立查詢條件 bsonFilter := bson.M{} // 如果有指定 ClientID if filter.ClientID > 0 { bsonFilter["client_id"] = filter.ClientID } // 如果有指定 UID if filter.UID != "" { bsonFilter["uid"] = filter.UID } // 如果有指定 Name (模糊搜尋) if filter.Name != "" { bsonFilter["name"] = bson.M{"$regex": filter.Name, "$options": "i"} } // 如果有指定狀態 if filter.Status != nil { bsonFilter["status"] = *filter.Status } // 計算總數 total, err := repo.DB.GetClient().CountDocuments(ctx, bsonFilter) if err != nil { return nil, 0, err } // 如果沒有資料,直接返回 if total == 0 { return []*entity.Role{}, 0, nil } // 計算分頁參數 skip := int64((page - 1) * size) limit := int64(size) // 查詢資料 findOptions := options.Find(). SetSkip(skip). SetLimit(limit). SetSort(bson.M{"create_time": -1}) // 依建立時間降序排列 err = repo.DB.GetClient().Find(ctx, &data, bsonFilter, findOptions) if err != nil { return nil, 0, err } return data, total, nil } // Exists 檢查角色是否存在 func (repo *RoleRepository) Exists(ctx context.Context, uid string) (bool, error) { count, err := repo.DB.GetClient().CountDocuments(ctx, bson.M{"uid": uid}) if err != nil { return false, err } return count > 0, nil } // clearRoleCache 清除角色相關快取 func (repo *RoleRepository) clearRoleCache(ctx context.Context, role *entity.Role) { // 清除 UID 快取 if role.UID != "" { rk := domain.GetRoleUIDRedisKey(role.UID) _ = repo.DB.DelCache(ctx, rk) } } // NextID 取得下一個角色 ID // 使用 MongoDB 的 findOneAndUpdate 原子操作來生成自增 ID func (repo *RoleRepository) NextID(ctx context.Context) (int64, error) { // 使用一個特殊的文檔來存儲計數器 filter := bson.M{"_id": "role_counter"} update := bson.M{ "$inc": bson.M{"seq": 1}, } var result struct { ID string `bson:"_id"` Seq int64 `bson:"seq"` } opts := options.FindOneAndUpdate(). SetUpsert(true). SetReturnDocument(options.After) err := repo.DB.GetClient().FindOneAndUpdate(ctx, &result, filter, update, opts) if err != nil { return 0, fmt.Errorf("failed to generate next ID: %w", err) } return result.Seq, nil } // Index20251009002UP 建立 Role 集合的索引 // 這個函數應該在應用啟動時或數據庫遷移時執行一次 func (repo *RoleRepository) Index20251009002UP(ctx context.Context) (*mongodriver.Cursor, error) { // 1. 唯一索引:角色 UID 必須唯一 // 等價於 db.role.createIndex({"uid": 1}, {unique: true}) repo.DB.PopulateIndex(ctx, "uid", 1, true) // 2. 複合唯一索引:同一個 Client 下角色名稱必須唯一 // 等價於 db.role.createIndex({"client_id": 1, "name": 1}, {unique: true}) repo.DB.PopulateMultiIndex(ctx, []string{"client_id", "name"}, []int32{1, 1}, true) // 3. 查詢索引:按 Client ID 查詢 // 等價於 db.role.createIndex({"client_id": 1}) repo.DB.PopulateIndex(ctx, "client_id", 1, false) // 4. 查詢索引:按狀態查詢 // 等價於 db.role.createIndex({"status": 1}) repo.DB.PopulateIndex(ctx, "status", 1, false) // 5. 複合索引:按 Client ID 和狀態查詢(常用組合) // 等價於 db.role.createIndex({"client_id": 1, "status": 1}) repo.DB.PopulateMultiIndex(ctx, []string{"client_id", "status"}, []int32{1, 1}, false) // 6. 文本索引:支持角色名稱模糊搜索 // 注意:如果已經有文本索引,這個可能會衝突,可以根據需要調整 // repo.DB.PopulateIndex(ctx, "name", 1, false) // 7. 時間戳索引:用於排序和時間範圍查詢 // 等價於 db.role.createIndex({"create_time": 1}) repo.DB.PopulateIndex(ctx, "create_time", 1, false) // 8. 時間戳索引:用於更新時間排序 // 等價於 db.role.createIndex({"update_time": -1}) repo.DB.PopulateIndex(ctx, "update_time", -1, false) // 返回所有索引列表 return repo.DB.GetClient().Indexes().List(ctx) }