add member_status (#4)
Co-authored-by: daniel.w <daniel.w@intteam.net> Reviewed-on: #4
This commit is contained in:
parent
83a57e750f
commit
6015f1d7d1
|
@ -117,6 +117,15 @@ issues:
|
||||||
- gocognit
|
- gocognit
|
||||||
- contextcheck
|
- contextcheck
|
||||||
|
|
||||||
|
exclude-dirs:
|
||||||
|
- internal/model
|
||||||
|
- internal/mock
|
||||||
|
|
||||||
|
exclude-files:
|
||||||
|
- .*_test.go
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
linters-settings:
|
linters-settings:
|
||||||
gci:
|
gci:
|
||||||
sections:
|
sections:
|
||||||
|
|
1
Makefile
1
Makefile
|
@ -16,6 +16,7 @@ test: # 進行測試
|
||||||
fmt: # 格式優化
|
fmt: # 格式優化
|
||||||
$(GOFMT) -w $(GOFILES)
|
$(GOFMT) -w $(GOFILES)
|
||||||
goimports -w ./
|
goimports -w ./
|
||||||
|
golangci-lint run
|
||||||
|
|
||||||
.PHONY: gen-rpc
|
.PHONY: gen-rpc
|
||||||
gen-rpc: # 建立 rpc code
|
gen-rpc: # 建立 rpc code
|
||||||
|
|
4
go.mod
4
go.mod
|
@ -4,10 +4,11 @@ go 1.22.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
code.30cm.net/digimon/library-go/errors v1.0.1
|
code.30cm.net/digimon/library-go/errors v1.0.1
|
||||||
|
code.30cm.net/digimon/library-go/utils/invited_code v1.0.2
|
||||||
code.30cm.net/digimon/library-go/validator v1.0.0
|
code.30cm.net/digimon/library-go/validator v1.0.0
|
||||||
code.30cm.net/wanderland/library-go/errors v1.0.1
|
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/zeromicro/go-zero v1.7.0
|
github.com/zeromicro/go-zero v1.7.0
|
||||||
go.uber.org/mock v0.4.0
|
go.uber.org/mock v0.4.0
|
||||||
google.golang.org/grpc v1.65.0
|
google.golang.org/grpc v1.65.0
|
||||||
|
@ -53,6 +54,7 @@ require (
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/openzipkin/zipkin-go v0.4.3 // indirect
|
github.com/openzipkin/zipkin-go v0.4.3 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_golang v1.19.1 // indirect
|
github.com/prometheus/client_golang v1.19.1 // indirect
|
||||||
github.com/prometheus/client_model v0.5.0 // indirect
|
github.com/prometheus/client_model v0.5.0 // indirect
|
||||||
github.com/prometheus/common v0.48.0 // indirect
|
github.com/prometheus/common v0.48.0 // indirect
|
||||||
|
|
|
@ -3,8 +3,8 @@ package domain
|
||||||
import (
|
import (
|
||||||
mts "app-cloudep-permission-server/internal/lib/metric"
|
mts "app-cloudep-permission-server/internal/lib/metric"
|
||||||
|
|
||||||
ers "code.30cm.net/wanderland/library-go/errors"
|
ers "code.30cm.net/digimon/library-go/errors"
|
||||||
"code.30cm.net/wanderland/library-go/errors/code"
|
"code.30cm.net/digimon/library-go/errors/code"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 12 represents Scope
|
// 12 represents Scope
|
||||||
|
@ -32,52 +32,58 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
// TokenUnexpectedSigningErr 30001 Token 簽名錯誤
|
// TokenUnexpectedSigningErr 30001 Token 簽名錯誤
|
||||||
func TokenUnexpectedSigningErr(msg string) *ers.Err {
|
func TokenUnexpectedSigningErr(msg string) *ers.LibError {
|
||||||
mts.AppErrorMetrics.AddFailure("token", "token_unexpected_sign")
|
mts.AppErrorMetrics.AddFailure("token", "token_unexpected_sign")
|
||||||
|
|
||||||
return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenUnexpectedSigningErrorCode, msg)
|
return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenUnexpectedSigningErrorCode, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenTokenValidateErr 30002 Token 驗證錯誤
|
// TokenTokenValidateErr 30002 Token 驗證錯誤
|
||||||
func TokenTokenValidateErr(msg string) *ers.Err {
|
func TokenTokenValidateErr(msg string) *ers.LibError {
|
||||||
mts.AppErrorMetrics.AddFailure("token", "token_validate_ilegal")
|
mts.AppErrorMetrics.AddFailure("token", "token_validate_ilegal")
|
||||||
|
|
||||||
return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenValidateErrorCode, msg)
|
return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenValidateErrorCode, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenClaimError 30003 Token 驗證錯誤
|
// TokenClaimError 30003 Token 驗證錯誤
|
||||||
func TokenClaimError(msg string) *ers.Err {
|
func TokenClaimError(msg string) *ers.LibError {
|
||||||
mts.AppErrorMetrics.AddFailure("token", "token_claim_error")
|
mts.AppErrorMetrics.AddFailure("token", "token_claim_error")
|
||||||
|
|
||||||
return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenClaimErrorCode, msg)
|
return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenClaimErrorCode, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RedisDelError 30020 Redis 刪除錯誤
|
// RedisDelError 30020 Redis 刪除錯誤
|
||||||
func RedisDelError(msg string) *ers.Err {
|
func RedisDelError(msg string) *ers.LibError {
|
||||||
// 看需要建立哪些 Metrics
|
// 看需要建立哪些 Metrics
|
||||||
mts.AppErrorMetrics.AddFailure("redis", "del_error")
|
mts.AppErrorMetrics.AddFailure("redis", "del_error")
|
||||||
|
|
||||||
return ers.NewErr(code.CloudEPPermission, code.CatDB, RedisDelErrorCode, msg)
|
return ers.NewErr(code.CloudEPPermission, code.CatDB, RedisDelErrorCode, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RedisPipLineError 30021 Redis PipLine 錯誤
|
// RedisPipLineError 30021 Redis PipLine 錯誤
|
||||||
func RedisPipLineError(msg string) *ers.Err {
|
func RedisPipLineError(msg string) *ers.LibError {
|
||||||
// 看需要建立哪些 Metrics
|
// 看需要建立哪些 Metrics
|
||||||
mts.AppErrorMetrics.AddFailure("redis", "pip_line_error")
|
mts.AppErrorMetrics.AddFailure("redis", "pip_line_error")
|
||||||
|
|
||||||
return ers.NewErr(code.CloudEPPermission, code.CatInput, RedisPipLineErrorCode, msg)
|
return ers.NewErr(code.CloudEPPermission, code.CatInput, RedisPipLineErrorCode, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RedisError 30022 Redis 錯誤
|
// RedisError 30022 Redis 錯誤
|
||||||
func RedisError(msg string) *ers.Err {
|
func RedisError(msg string) *ers.LibError {
|
||||||
// 看需要建立哪些 Metrics
|
// 看需要建立哪些 Metrics
|
||||||
mts.AppErrorMetrics.AddFailure("redis", "error")
|
mts.AppErrorMetrics.AddFailure("redis", "error")
|
||||||
|
|
||||||
return ers.NewErr(code.CloudEPPermission, code.CatInput, RedisErrorCode, msg)
|
return ers.NewErr(code.CloudEPPermission, code.CatInput, RedisErrorCode, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PermissionNotFoundError 30030 權限錯誤
|
// PermissionNotFoundError 30030 權限錯誤
|
||||||
func PermissionNotFoundError(msg string) *ers.Err {
|
func PermissionNotFoundError(msg string) *ers.LibError {
|
||||||
// 看需要建立哪些 Metrics
|
// 看需要建立哪些 Metrics
|
||||||
return ers.NewErr(code.CloudEPPermission, code.Forbidden, PermissionNotFoundCode, msg)
|
return ers.NewErr(code.CloudEPPermission, code.Forbidden, PermissionNotFoundCode, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PermissionGetDataError 30031 解析權限時錯誤
|
// PermissionGetDataError 30031 解析權限時錯誤
|
||||||
func PermissionGetDataError(msg string) *ers.Err {
|
func PermissionGetDataError(msg string) *ers.LibError {
|
||||||
// 看需要建立哪些 Metrics
|
// 看需要建立哪些 Metrics
|
||||||
return ers.NewErr(code.CloudEPPermission, code.InvalidFormat, PermissionGetDataErrorCode, msg)
|
return ers.NewErr(code.CloudEPPermission, code.InvalidFormat, PermissionGetDataErrorCode, msg)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,18 +23,21 @@ const (
|
||||||
// TokenUnexpectedSigningErr 30001 Token 簽名錯誤
|
// TokenUnexpectedSigningErr 30001 Token 簽名錯誤
|
||||||
func TokenUnexpectedSigningErr(msg string) *ers.LibError {
|
func TokenUnexpectedSigningErr(msg string) *ers.LibError {
|
||||||
mts.AppErrorMetrics.AddFailure("token", "token_unexpected_sign")
|
mts.AppErrorMetrics.AddFailure("token", "token_unexpected_sign")
|
||||||
|
|
||||||
return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenUnexpectedSigningErrorCode, msg)
|
return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenUnexpectedSigningErrorCode, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenTokenValidateErr 30002 Token 驗證錯誤
|
// TokenTokenValidateErr 30002 Token 驗證錯誤
|
||||||
func TokenTokenValidateErr(msg string) *ers.LibError {
|
func TokenTokenValidateErr(msg string) *ers.LibError {
|
||||||
mts.AppErrorMetrics.AddFailure("token", "token_validate_ilegal")
|
mts.AppErrorMetrics.AddFailure("token", "token_validate_ilegal")
|
||||||
|
|
||||||
return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenValidateErrorCode, msg)
|
return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenValidateErrorCode, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenClaimError 30003 Token 驗證錯誤
|
// TokenClaimError 30003 Token 驗證錯誤
|
||||||
func TokenClaimError(msg string) *ers.LibError {
|
func TokenClaimError(msg string) *ers.LibError {
|
||||||
mts.AppErrorMetrics.AddFailure("token", "token_claim_error")
|
mts.AppErrorMetrics.AddFailure("token", "token_claim_error")
|
||||||
|
|
||||||
return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenClaimErrorCode, msg)
|
return ers.NewErr(code.CloudEPPermission, code.CatInput, TokenClaimErrorCode, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +45,7 @@ func TokenClaimError(msg string) *ers.LibError {
|
||||||
func RedisDelError(msg string) *ers.LibError {
|
func RedisDelError(msg string) *ers.LibError {
|
||||||
// 看需要建立哪些 Metrics
|
// 看需要建立哪些 Metrics
|
||||||
mts.AppErrorMetrics.AddFailure("redis", "del_error")
|
mts.AppErrorMetrics.AddFailure("redis", "del_error")
|
||||||
|
|
||||||
return ers.NewErr(code.CloudEPPermission, code.CatDB, RedisDelErrorCode, msg)
|
return ers.NewErr(code.CloudEPPermission, code.CatDB, RedisDelErrorCode, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +53,7 @@ func RedisDelError(msg string) *ers.LibError {
|
||||||
func RedisPipLineError(msg string) *ers.LibError {
|
func RedisPipLineError(msg string) *ers.LibError {
|
||||||
// 看需要建立哪些 Metrics
|
// 看需要建立哪些 Metrics
|
||||||
mts.AppErrorMetrics.AddFailure("redis", "pip_line_error")
|
mts.AppErrorMetrics.AddFailure("redis", "pip_line_error")
|
||||||
|
|
||||||
return ers.NewErr(code.CloudEPPermission, code.CatInput, RedisPipLineErrorCode, msg)
|
return ers.NewErr(code.CloudEPPermission, code.CatInput, RedisPipLineErrorCode, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,5 +61,6 @@ func RedisPipLineError(msg string) *ers.LibError {
|
||||||
func RedisError(msg string) *ers.LibError {
|
func RedisError(msg string) *ers.LibError {
|
||||||
// 看需要建立哪些 Metrics
|
// 看需要建立哪些 Metrics
|
||||||
mts.AppErrorMetrics.AddFailure("redis", "error")
|
mts.AppErrorMetrics.AddFailure("redis", "error")
|
||||||
|
|
||||||
return ers.NewErr(code.CloudEPPermission, code.CatInput, RedisErrorCode, msg)
|
return ers.NewErr(code.CloudEPPermission, code.CatInput, RedisErrorCode, msg)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// MemberOnlineStatusRepository 會員上限狀態,使用Bitmap
|
||||||
|
type MemberOnlineStatusRepository interface {
|
||||||
|
SetMemberOnline(ctx context.Context, uid string) (bool, error)
|
||||||
|
SetMemberOffline(ctx context.Context, uid string) (bool, error)
|
||||||
|
IsMemberOnline(ctx context.Context, uid string) (bool, error)
|
||||||
|
QueryMemberOnlineList(ctx context.Context, uids []string) ([]MemberOnlineStatusResp, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MemberOnlineStatusResp struct {
|
||||||
|
UID string
|
||||||
|
Status bool
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package usecase
|
||||||
|
|
||||||
|
type BitMapUseCase interface {
|
||||||
|
// SetTrue 設定該 Bit 狀態為 true
|
||||||
|
SetTrue(bitPos uint32)
|
||||||
|
// SetFalse 設定該Bit 狀態為 false
|
||||||
|
SetFalse(bitPos uint32)
|
||||||
|
// IsTrue 確認是否為真
|
||||||
|
IsTrue(bitPos uint32) bool
|
||||||
|
// Reset 重設 BitMap
|
||||||
|
Reset()
|
||||||
|
// ByteSize 最大 Byte 數
|
||||||
|
ByteSize() int
|
||||||
|
// BitSize 最大 Byte * 8
|
||||||
|
BitSize() int
|
||||||
|
}
|
|
@ -1,38 +1,33 @@
|
||||||
package usecase
|
package usecase
|
||||||
|
|
||||||
import (
|
// // PermissionTreeManager 定義一組操作權限樹的接口
|
||||||
"ark-permission/internal/domain"
|
// // 這個名稱說明它是專門負責管理和操作權限樹的管理器
|
||||||
"ark-permission/internal/entity"
|
// type PermissionTreeManager interface {
|
||||||
)
|
// // AddPermission 將一個新的權限節點插入到樹中
|
||||||
|
// // key 是父節點的ID,value 是要插入的 Permission 資料
|
||||||
// PermissionTreeManager 定義一組操作權限樹的接口
|
// // 此方法應該能處理節點是否存在於父節點下的情況
|
||||||
// 這個名稱說明它是專門負責管理和操作權限樹的管理器
|
// AddPermission(parentID int64, permission entity.Permission) error
|
||||||
type PermissionTreeManager interface {
|
// // FindPermissionByID 根據權限 ID 查詢樹中的某個節點
|
||||||
// AddPermission 將一個新的權限節點插入到樹中
|
// // 如果節點存在,返回對應的 Permission 資料,否則返回 nil
|
||||||
// key 是父節點的ID,value 是要插入的 Permission 資料
|
// FindPermissionByID(permissionID int64) (*Permission, error)
|
||||||
// 此方法應該能處理節點是否存在於父節點下的情況
|
// // GetAllParentPermissionIDs 根據傳入的 permissions 列表
|
||||||
AddPermission(parentID int64, permission entity.Permission) error
|
// // 找出每個權限的完整父節點權限 ID 路徑
|
||||||
// FindPermissionByID 根據權限 ID 查詢樹中的某個節點
|
// // 例如,如果 B 的父權限是 A,並且給了 B 權限,則返回 A 和 B 的權限 ID
|
||||||
// 如果節點存在,返回對應的 Permission 資料,否則返回 nil
|
// GetAllParentPermissionIDs(permissions domain.Permissions) ([]int64, error)
|
||||||
FindPermissionByID(permissionID int64) (*Permission, error)
|
// // GetAllParentPermissionStatuses 返回給定權限下的所有完整父節點權限狀態
|
||||||
// GetAllParentPermissionIDs 根據傳入的 permissions 列表
|
// // 例如,若給 B 權限,該方法將返回所有與 B 相關的父權限的狀態
|
||||||
// 找出每個權限的完整父節點權限 ID 路徑
|
// GetAllParentPermissionStatuses(permissions domain.Permissions) (domain.Permissions, error)
|
||||||
// 例如,如果 B 的父權限是 A,並且給了 B 權限,則返回 A 和 B 的權限 ID
|
// // GetRolePermissionTree 根據角色權限找出所有父節點和子節點權限狀態
|
||||||
GetAllParentPermissionIDs(permissions domain.Permissions) ([]int64, error)
|
// // 角色權限是傳入的一個列表,該方法會根據每個角色的權限,返回所有相關的權限狀態
|
||||||
// GetAllParentPermissionStatuses 返回給定權限下的所有完整父節點權限狀態
|
// GetRolePermissionTree(rolePermissions []entity.RolePermission) domain.Permissions
|
||||||
// 例如,若給 B 權限,該方法將返回所有與 B 相關的父權限的狀態
|
// }
|
||||||
GetAllParentPermissionStatuses(permissions domain.Permissions) (domain.Permissions, error)
|
//
|
||||||
// GetRolePermissionTree 根據角色權限找出所有父節點和子節點權限狀態
|
// type Permission struct {
|
||||||
// 角色權限是傳入的一個列表,該方法會根據每個角色的權限,返回所有相關的權限狀態
|
// ID int64 `json:"-"`
|
||||||
GetRolePermissionTree(rolePermissions []entity.RolePermission) domain.Permissions
|
// Name string `json:"name"`
|
||||||
}
|
// HTTPMethod string `json:"http_method"`
|
||||||
|
// HTTPPath string `json:"http_path"`
|
||||||
type Permission struct {
|
// Parent *Permission `json:"-"`
|
||||||
ID int64 `json:"-"`
|
// Children []*Permission `json:"children"`
|
||||||
Name string `json:"name"`
|
// PathIDs []int64 `json:"-"` // full path id
|
||||||
HTTPMethod string `json:"http_method"`
|
// }
|
||||||
HTTPPath string `json:"http_path"`
|
|
||||||
Parent *Permission `json:"-"`
|
|
||||||
Children []*Permission `json:"children"`
|
|
||||||
PathIDs []int64 `json:"-"` // full path id
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"app-cloudep-permission-server/internal/domain/repository"
|
||||||
|
"code.30cm.net/digimon/library-go/utils/invited_code"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MemberOnlineStatusRepositoryParam struct {
|
||||||
|
Store *redis.Redis `name:"redis"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type memberOnlineStatusRepository struct {
|
||||||
|
store *redis.Redis
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 UID 計算 Bitmap 的 offset
|
||||||
|
func (t *memberOnlineStatusRepository) uidToOffset(uid string) (int64, error) {
|
||||||
|
converter := invited_code.MustConverter(10, invited_code.DefaultCodeLen, invited_code.ConvertTable)
|
||||||
|
|
||||||
|
// 將 UID 轉換為整數型,並減去基礎 UID (1000000)
|
||||||
|
uidInt, err := converter.DecodeFromCode(uid)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("invalid UID: %w", err)
|
||||||
|
}
|
||||||
|
// 以 1000000 作為基準
|
||||||
|
baseUID := invited_code.InitAutoId
|
||||||
|
if uidInt < int64(baseUID) {
|
||||||
|
return 0, fmt.Errorf("UID smaller than base: %d", baseUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return uidInt - int64(baseUID), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *memberOnlineStatusRepository) SetMemberOnline(ctx context.Context, uid string) (bool, error) {
|
||||||
|
offset, err := t.uidToOffset(uid)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 SET BIT 設置對應的位為 1
|
||||||
|
_, err = t.store.SetBit("member_status", offset, 1)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to set member online: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *memberOnlineStatusRepository) SetMemberOffline(ctx context.Context, uid string) (bool, error) {
|
||||||
|
offset, err := t.uidToOffset(uid)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 SET BIT 設置對應的位為 1
|
||||||
|
_, err = t.store.SetBit("member_status", offset, 0)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to set member offline: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *memberOnlineStatusRepository) IsMemberOnline(ctx context.Context, uid string) (bool, error) {
|
||||||
|
offset, err := t.uidToOffset(uid)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 GET BIT 獲取對應的位,1 表示在線,0 表示離線
|
||||||
|
status, err := t.store.GetBit("member_status", offset)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to get member status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return status == 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *memberOnlineStatusRepository) QueryMemberOnlineList(ctx context.Context, uids []string) ([]repository.MemberOnlineStatusResp, error) {
|
||||||
|
statusMap := make([]repository.MemberOnlineStatusResp, 0, len(uids))
|
||||||
|
|
||||||
|
// 遍歷所有用戶的 UID,查詢他們的在線狀態
|
||||||
|
for _, uid := range uids {
|
||||||
|
isOnline, err := t.IsMemberOnline(ctx, uid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query member status for UID %s: %w", uid, err)
|
||||||
|
}
|
||||||
|
statusMap = append(statusMap, repository.MemberOnlineStatusResp{
|
||||||
|
UID: uid,
|
||||||
|
Status: isOnline,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMemberOnlineStatusRepository(param MemberOnlineStatusRepositoryParam) repository.MemberOnlineStatusRepository {
|
||||||
|
return &memberOnlineStatusRepository{
|
||||||
|
store: param.Store,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
package usecase
|
||||||
|
|
||||||
|
// Bitmap 基礎結構
|
||||||
|
// Bitmap 是一個位圖結構,使用 byte slice 來表示大量的位(bit)。
|
||||||
|
// 每個 byte 由 8 個位組成,因此可以高效地管理大量的開關狀態(true/false)。
|
||||||
|
// Bitmap 的優點在於它能節省空間,尤其是在需要大量布爾值的場合。
|
||||||
|
// 缺點是,如果需要動態擴充 Bitmap 的大小,會導致效率下降,因為需要重新分配和移動內存。
|
||||||
|
// 因此,最好在初始化時就規劃好所需的位數大小,避免在之後頻繁擴充。
|
||||||
|
|
||||||
|
type Bitmap []byte
|
||||||
|
|
||||||
|
// MakeBitmapWithBitSize 通過指定的 bit 數創建一個新的 Bitmap。
|
||||||
|
// 參數 nBits 表示所需的位(bit)數。
|
||||||
|
// 如果指定的位數少於 64,則默認將 Bitmap 初始化為 64 位(這是最低的限制)。
|
||||||
|
// 此外,位數(nBits)會被自動調整為 8 的倍數,以適配 byte 的長度(每 8 位為一個 byte)。
|
||||||
|
// 返回值是一個 Bitmap(byte slice),其大小根據位數確定。
|
||||||
|
func MakeBitmapWithBitSize(nBits int) Bitmap {
|
||||||
|
// 如果指定的位數少於 64,則設置為 64 位(8 個 byte)
|
||||||
|
if nBits < 64 {
|
||||||
|
nBits = 64
|
||||||
|
}
|
||||||
|
// 計算需要的 byte 數,確保每 8 位為一個 byte,並調整 nBits 以達到 8 的倍數
|
||||||
|
return MustBitMap((nBits + 7) / 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustBitMap 根據指定的 byte 數創建一個 Bitmap(byte slice)。
|
||||||
|
// 參數 nBytes 表示所需的 byte 數。
|
||||||
|
// 返回值是一個長度為 nBytes 的 Bitmap。
|
||||||
|
func MustBitMap(nBytes int) Bitmap {
|
||||||
|
// 使用 make 函數創建一個 byte slice,大小為 nBytes。
|
||||||
|
return make([]byte, nBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTrue 設置指定位置的 bit 為 true(1)。
|
||||||
|
// 參數 bitPos 是需要設置的位的位置(以 0 為基準的位索引)。
|
||||||
|
// 這個操作會找到該 bit 所在的 byte,然後通過位運算將該位置的 bit 設置為 1。
|
||||||
|
func (b Bitmap) SetTrue(bitPos uint32) {
|
||||||
|
// |= 是一種位運算的複合賦值運算符,表示將左邊的變數與右邊的值進行 位或運算(bitwise OR),並將結果賦值
|
||||||
|
b[bitPos/8] |= 1 << (bitPos % 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFalse 設置指定位置的 bit 為 false(0)。
|
||||||
|
// 參數 bitPos 是需要設置的位的位置(以 0 為基準的位索引)。
|
||||||
|
// 這個操作會找到該 bit 所在的 byte,然後通過位運算將該位置的 bit 設置為 0。
|
||||||
|
func (b Bitmap) SetFalse(bitPos uint32) {
|
||||||
|
// 取出對應 byte,使用位與和取反運算將對應的 bit 設置為 0
|
||||||
|
// 假設我們有以下情況:
|
||||||
|
|
||||||
|
// • b[1](即 b[bitPos/8])是 10101111(十進制 175)。
|
||||||
|
// • bitPos = 10,也就是我們想清除第 10 位的值。
|
||||||
|
//
|
||||||
|
// 操作步驟:
|
||||||
|
//
|
||||||
|
// 1. bitPos/8 = 1:所以我們要修改 b[1] 這個 byte。
|
||||||
|
// 2. bitPos % 8 = 2:表示我們要清除的位是這個 byte 中的第 3 位(從右數起第 3 位)。
|
||||||
|
// 3. 1 << (bitPos % 8) = 1 << 2 = 00000100:生成位掩碼 00000100。
|
||||||
|
// 4. 取反:^(1 << 2) = ^00000100 = 11111011,這樣的掩碼表示除了第 3 位,其他位都是 1。
|
||||||
|
// 5. 位與運算:10101111 & 11111011 = 10101011,結果將第 3 位清除,其餘位保持不變。即,b[1] 變成了 10101011(十進制 171)。
|
||||||
|
// &= 是一種 位與運算的複合賦值運算符,表示將左邊的變數與右邊的值進行 位與運算(bitwise AND),然後將結果賦值給左邊的變數。
|
||||||
|
b[bitPos/8] &= ^(1 << (bitPos % 8))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTrue 檢查指定位置的 bit 是否為 true(1)。
|
||||||
|
// 參數 bitPos 是要檢查的位的位置(以 0 為基準的位索引)。
|
||||||
|
// 如果該 bit 是 1,則返回 true;否則返回 false。
|
||||||
|
func (b Bitmap) IsTrue(bitPos uint32) bool {
|
||||||
|
/*
|
||||||
|
這一行程式碼 b[bitPos/8]&(1<<(bitPos%8)) != 0 是用來檢查 指定位(bit) 是否為 true(1),
|
||||||
|
它的核心是位運算。讓我們逐步拆解並解釋這一行程式碼:
|
||||||
|
|
||||||
|
1. 背景知識:
|
||||||
|
|
||||||
|
• 位運算 是在二進制層面操作數字。每個 byte 有 8 個位(bit),所以位圖結構是以 byte 來表示位的集合。
|
||||||
|
• b 是一個 Bitmap 結構,也就是 []byte,即 byte 的切片。
|
||||||
|
• bitPos 是一個 uint32 類型的變數,表示我們想要檢查的位(bit)在整個位圖中的索引(位置)。
|
||||||
|
3. 完整流程舉例:
|
||||||
|
假設:
|
||||||
|
• b = []byte{0b10101010, 0b01010101} (即二進制表示的兩個 byte,分別是 10101010 和 01010101)。
|
||||||
|
• bitPos = 10(我們要檢查第 10 位是否為 1)。
|
||||||
|
操作順序:
|
||||||
|
|
||||||
|
1. 計算 bitPos/8 = 10/8 = 1,所以我們要檢查的是第二個 byte:0b01010101。
|
||||||
|
2. 計算 bitPos%8 = 10%8 = 2,所以我們要檢查的是該 byte 中的第 3 位(從右數起第 3 位)。
|
||||||
|
3. 位移:1 << 2 = 00000100。
|
||||||
|
4. 位與:0b01010101 & 0b00000100 = 0b00000100(因為該 byte 的第 3 位是 1,結果不等於 0)。
|
||||||
|
5. 判斷結果:結果不等於 0,因此第 10 位是 1(true)。
|
||||||
|
|
||||||
|
4. 總結:
|
||||||
|
|
||||||
|
• b[bitPos/8]&(1<<(bitPos%8)) != 0 是一個經典的位操作,用來檢查位圖中某一個位是否為 1。
|
||||||
|
• bitPos/8 找到對應的 byte,bitPos % 8 找到該位在這個 byte 中的具體位置。
|
||||||
|
• 最後的位與運算和比較用來確定該位的狀態是 true(1)還是 false(0)。
|
||||||
|
*/
|
||||||
|
return b[bitPos/8]&(1<<(bitPos%8)) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset 重置 Bitmap,使所有的 bit 都設置為 false(0)。
|
||||||
|
// 這個操作會將整個 Bitmap 的所有 byte 都設置為 0,從而達到重置的效果。
|
||||||
|
func (b Bitmap) Reset() {
|
||||||
|
// 迭代 Bitmap 中的每個 byte,並將其設置為 0
|
||||||
|
for i := range b {
|
||||||
|
b[i] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByteSize 返回 Bitmap 的 byte 長度。
|
||||||
|
// 這個函數返回 Bitmap 目前占用的 byte 數量,該值等於 Bitmap 的長度(slice 的長度)。
|
||||||
|
func (b Bitmap) ByteSize() int {
|
||||||
|
return len(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BitSize 返回 Bitmap 的位(bit)長度。
|
||||||
|
// 這個函數通過將 byte 長度乘以 8 來計算 Bitmap 中的總位數。
|
||||||
|
func (b Bitmap) BitSize() int {
|
||||||
|
// 每個 byte 包含 8 個 bit,因此將 byte 長度乘以 8
|
||||||
|
return len(b) * 8
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
package usecase
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// 基準測試 SetTrue 函數,測試在不同大小的 Bitmap 上設置位元為 true 的效能
|
||||||
|
func BenchmarkBitmapSetTrue(b *testing.B) {
|
||||||
|
// 以 10^6 位元作為基準測試的 Bitmap 大小
|
||||||
|
bitmap := MakeBitmapWithBitSize(1000000)
|
||||||
|
b.ResetTimer() // 重設計時器,排除初始化的時間
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
bitmap.SetTrue(uint32(i % 1000000)) // 設置位元為 true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基準測試 SetFalse 函數,測試在不同大小的 Bitmap 上清除位元為 false 的效能
|
||||||
|
func BenchmarkBitmapSetFalse(b *testing.B) {
|
||||||
|
// 以 10^6 位元作為基準測試的 Bitmap 大小
|
||||||
|
bitmap := MakeBitmapWithBitSize(1000000)
|
||||||
|
b.ResetTimer() // 重設計時器,排除初始化的時間
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
bitmap.SetFalse(uint32(i % 1000000)) // 清除位元為 false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基準測試 IsTrue 函數,測試在不同大小的 Bitmap 上檢查位元狀態的效能
|
||||||
|
func BenchmarkBitmapIsTrue(b *testing.B) {
|
||||||
|
// 以 10^6 位元作為基準測試的 Bitmap 大小
|
||||||
|
bitmap := MakeBitmapWithBitSize(1000000)
|
||||||
|
b.ResetTimer() // 重設計時器,排除初始化的時間
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = bitmap.IsTrue(uint32(i % 1000000)) // 檢查位元是否為 true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基準測試 Reset 函數,測試重置不同大小的 Bitmap 的效能
|
||||||
|
func BenchmarkBitmapReset(b *testing.B) {
|
||||||
|
// 以 10^6 位元作為基準測試的 Bitmap 大小
|
||||||
|
bitmap := MakeBitmapWithBitSize(1000000)
|
||||||
|
b.ResetTimer() // 重設計時器,排除初始化的時間
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
bitmap.Reset() // 重置 Bitmap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基準測試 BitSize 函數,測試返回位圖的 bit 長度的效能
|
||||||
|
func BenchmarkBitmapBitSize(b *testing.B) {
|
||||||
|
// 以 10^6 位元作為基準測試的 Bitmap 大小
|
||||||
|
bitmap := MakeBitmapWithBitSize(1000000)
|
||||||
|
b.ResetTimer() // 重設計時器,排除初始化的時間
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = bitmap.BitSize(0) // 測試返回位圖的 bit 長度
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基準測試 ByteSize 函數,測試返回位圖的 byte 長度的效能
|
||||||
|
func BenchmarkBitmapByteSize(b *testing.B) {
|
||||||
|
// 以 10^6 位元作為基準測試的 Bitmap 大小
|
||||||
|
bitmap := MakeBitmapWithBitSize(1000000)
|
||||||
|
b.ResetTimer() // 重設計時器,排除初始化的時間
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = bitmap.ByteSize(0) // 測試返回位圖的 byte 長度
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package usecase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBitmap_SetTrueAndIsTrue(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
bitPos uint32
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{"Set bit 0 to true", 0, true},
|
||||||
|
{"Set bit 1 to true", 1, true},
|
||||||
|
{"Set bit 63 to true", 63, true},
|
||||||
|
{"Set bit 64 to true", 64, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
bitmap := MakeBitmapWithBitSize(128) // 初始化一個 128 位的 Bitmap
|
||||||
|
bitmap.SetTrue(tt.bitPos)
|
||||||
|
result := bitmap.IsTrue(tt.bitPos)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitmap_SetFalse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
bitPos uint32
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{"Set bit 0 to false", 0, false},
|
||||||
|
{"Set bit 1 to false", 1, false},
|
||||||
|
{"Set bit 63 to false", 63, false},
|
||||||
|
{"Set bit 64 to false", 64, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
bitmap := MakeBitmapWithBitSize(128) // 初始化一個 128 位的 Bitmap
|
||||||
|
bitmap.SetTrue(tt.bitPos) // 先設置該 bit 為 true
|
||||||
|
bitmap.SetFalse(tt.bitPos) // 然後設置該 bit 為 false
|
||||||
|
result := bitmap.IsTrue(tt.bitPos)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitmap_Reset(t *testing.T) {
|
||||||
|
bitmap := MakeBitmapWithBitSize(128) // 初始化一個 128 位的 Bitmap
|
||||||
|
bitmap.SetTrue(0)
|
||||||
|
bitmap.SetTrue(64)
|
||||||
|
|
||||||
|
// 確認 bit 0 和 bit 64 是 true
|
||||||
|
assert.True(t, bitmap.IsTrue(0))
|
||||||
|
assert.True(t, bitmap.IsTrue(64))
|
||||||
|
|
||||||
|
bitmap.Reset() // 重置位圖
|
||||||
|
|
||||||
|
// 確認所有的位都已經重置為 false
|
||||||
|
assert.False(t, bitmap.IsTrue(0))
|
||||||
|
assert.False(t, bitmap.IsTrue(64))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitmap_ByteSize(t *testing.T) {
|
||||||
|
bitmap := MakeBitmapWithBitSize(64) // 初始化一個 64 位的 Bitmap
|
||||||
|
assert.Equal(t, 8, bitmap.ByteSize(0)) // 64 位應該佔用 8 個 byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitmap_BitSize(t *testing.T) {
|
||||||
|
bitmap := MakeBitmapWithBitSize(128) // 初始化一個 128 位的 Bitmap
|
||||||
|
assert.Equal(t, 128, bitmap.BitSize(0)) // 128 位應該有 128 個 bit
|
||||||
|
}
|
Loading…
Reference in New Issue