465 lines
11 KiB
Markdown
465 lines
11 KiB
Markdown
# 從 internal 遷移到 reborn 指南
|
||
|
||
## 📋 遷移檢查清單
|
||
|
||
- [ ] 1. 複製 reborn 資料夾到專案中
|
||
- [ ] 2. 安裝依賴套件
|
||
- [ ] 3. 配置設定檔
|
||
- [ ] 4. 初始化 Repository 和 UseCase
|
||
- [ ] 5. 更新 HTTP Handlers
|
||
- [ ] 6. 執行測試
|
||
- [ ] 7. 部署到測試環境
|
||
- [ ] 8. 驗證功能正常
|
||
- [ ] 9. 部署到生產環境
|
||
|
||
---
|
||
|
||
## 🔄 詳細遷移步驟
|
||
|
||
### Step 1: 安裝依賴套件
|
||
|
||
```bash
|
||
# 將 go.mod.example 改名為 go.mod(如果需要)
|
||
cd reborn
|
||
cp go.mod.example go.mod
|
||
|
||
# 安裝依賴
|
||
go mod download
|
||
```
|
||
|
||
### Step 2: 建立配置檔
|
||
|
||
建立 `config/app_config.go`:
|
||
|
||
```go
|
||
package config
|
||
|
||
import (
|
||
"os"
|
||
"strconv"
|
||
"time"
|
||
|
||
rebornConfig "permission/reborn/config"
|
||
)
|
||
|
||
func LoadConfig() rebornConfig.Config {
|
||
return rebornConfig.Config{
|
||
Database: rebornConfig.DatabaseConfig{
|
||
Host: getEnv("DB_HOST", "localhost"),
|
||
Port: getEnvInt("DB_PORT", 3306),
|
||
Username: getEnv("DB_USER", "root"),
|
||
Password: getEnv("DB_PASSWORD", ""),
|
||
Database: getEnv("DB_NAME", "permission"),
|
||
MaxIdle: getEnvInt("DB_MAX_IDLE", 10),
|
||
MaxOpen: getEnvInt("DB_MAX_OPEN", 100),
|
||
},
|
||
Redis: rebornConfig.RedisConfig{
|
||
Host: getEnv("REDIS_HOST", "localhost"),
|
||
Port: getEnvInt("REDIS_PORT", 6379),
|
||
Password: getEnv("REDIS_PASSWORD", ""),
|
||
DB: getEnvInt("REDIS_DB", 0),
|
||
|
||
PermissionTreeTTL: 10 * time.Minute,
|
||
UserPermissionTTL: 5 * time.Minute,
|
||
RolePolicyTTL: 10 * time.Minute,
|
||
},
|
||
RBAC: rebornConfig.RBACConfig{
|
||
ModelPath: "./rbac_model.conf",
|
||
SyncPeriod: 30 * time.Second,
|
||
EnableCache: true,
|
||
},
|
||
Role: rebornConfig.RoleConfig{
|
||
UIDPrefix: getEnv("ROLE_UID_PREFIX", "AM"),
|
||
UIDLength: 6,
|
||
AdminRoleUID: getEnv("ADMIN_ROLE_UID", "AM000000"),
|
||
AdminUserUID: getEnv("ADMIN_USER_UID", "B000000"),
|
||
DefaultRoleName: "user",
|
||
},
|
||
}
|
||
}
|
||
|
||
func getEnv(key, defaultValue string) string {
|
||
if value := os.Getenv(key); value != "" {
|
||
return value
|
||
}
|
||
return defaultValue
|
||
}
|
||
|
||
func getEnvInt(key string, defaultValue int) int {
|
||
if value := os.Getenv(key); value != "" {
|
||
if intValue, err := strconv.Atoi(value); err == nil {
|
||
return intValue
|
||
}
|
||
}
|
||
return defaultValue
|
||
}
|
||
```
|
||
|
||
### Step 3: 初始化服務
|
||
|
||
建立 `internal/bootstrap/permission.go`:
|
||
|
||
```go
|
||
package bootstrap
|
||
|
||
import (
|
||
"permission/reborn/config"
|
||
"permission/reborn/repository"
|
||
"permission/reborn/usecase"
|
||
"permission/reborn/domain/usecase as domainUC"
|
||
|
||
"github.com/redis/go-redis/v9"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
type PermissionServices struct {
|
||
RoleUC domainUC.RoleUseCase
|
||
UserRoleUC domainUC.UserRoleUseCase
|
||
PermUC domainUC.PermissionUseCase
|
||
RolePermUC domainUC.RolePermissionUseCase
|
||
}
|
||
|
||
func InitPermissionServices(db *gorm.DB, redisClient *redis.Client, cfg config.Config) *PermissionServices {
|
||
// Repository 層
|
||
roleRepo := repository.NewRoleRepository(db)
|
||
userRoleRepo := repository.NewUserRoleRepository(db)
|
||
rolePermRepo := repository.NewRolePermissionRepository(db)
|
||
cacheRepo := repository.NewCacheRepository(redisClient, cfg.Redis)
|
||
permRepo := repository.NewPermissionRepository(db, cacheRepo)
|
||
|
||
// UseCase 層
|
||
permUC := usecase.NewPermissionUseCase(
|
||
permRepo, rolePermRepo, roleRepo, userRoleRepo, cacheRepo,
|
||
)
|
||
|
||
rolePermUC := usecase.NewRolePermissionUseCase(
|
||
permRepo, rolePermRepo, roleRepo, userRoleRepo,
|
||
permUC, cacheRepo, cfg.Role,
|
||
)
|
||
|
||
roleUC := usecase.NewRoleUseCase(
|
||
roleRepo, userRoleRepo, rolePermUC, cacheRepo, cfg.Role,
|
||
)
|
||
|
||
userRoleUC := usecase.NewUserRoleUseCase(
|
||
userRoleRepo, roleRepo, cacheRepo,
|
||
)
|
||
|
||
return &PermissionServices{
|
||
RoleUC: roleUC,
|
||
UserRoleUC: userRoleUC,
|
||
PermUC: permUC,
|
||
RolePermUC: rolePermUC,
|
||
}
|
||
}
|
||
```
|
||
|
||
### Step 4: 更新 HTTP Handlers
|
||
|
||
#### 原版(internal)
|
||
```go
|
||
// internal/delivery/http/role_handler.go
|
||
func (h *roleHandler) create(c *gin.Context) {
|
||
var req payload.CreateRoleRequest
|
||
// ...
|
||
|
||
// 舊的 UseCase 呼叫
|
||
role, err := h.roleUC.Create(ctx, usecase.CreateRole{
|
||
ClientID: req.ClientID,
|
||
Name: req.Name,
|
||
Status: req.Status,
|
||
Permissions: req.Permissions,
|
||
})
|
||
}
|
||
```
|
||
|
||
#### 新版(reborn)
|
||
```go
|
||
// delivery/http/role_handler.go
|
||
import (
|
||
rebornUC "permission/reborn/domain/usecase"
|
||
)
|
||
|
||
type roleHandler struct {
|
||
roleUC rebornUC.RoleUseCase // 改用新的介面
|
||
}
|
||
|
||
func (h *roleHandler) create(c *gin.Context) {
|
||
var req payload.CreateRoleRequest
|
||
// ...
|
||
|
||
// 新的 UseCase 呼叫
|
||
role, err := h.roleUC.Create(ctx, rebornUC.CreateRoleRequest{
|
||
ClientID: req.ClientID,
|
||
Name: req.Name,
|
||
Permissions: req.Permissions, // Status 自動設為 Active
|
||
})
|
||
|
||
// 錯誤處理更簡單
|
||
if err != nil {
|
||
code := errors.GetCode(err)
|
||
c.JSON(getHTTPStatus(code), gin.H{
|
||
"error": err.Error(),
|
||
"code": code,
|
||
})
|
||
return
|
||
}
|
||
|
||
c.JSON(200, role)
|
||
}
|
||
```
|
||
|
||
### Step 5: 權限檢查中間件
|
||
|
||
#### 原版
|
||
```go
|
||
// internal/delivery/http/middleware/permission.go
|
||
func Permission(ctx context.Context, tokenUC usecase.TokenUseCase) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
// 複雜的 token 驗證和權限檢查
|
||
// ...
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 新版
|
||
```go
|
||
// delivery/http/middleware/permission.go
|
||
import (
|
||
rebornUC "permission/reborn/domain/usecase"
|
||
)
|
||
|
||
func Permission(rolePermUC rebornUC.RolePermissionUseCase) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
// 從 JWT 取得使用者 UID
|
||
userUID := c.GetString("user_uid")
|
||
|
||
// 取得使用者權限(有快取)
|
||
userPerm, err := rolePermUC.GetByUserUID(c.Request.Context(), userUID)
|
||
if err != nil {
|
||
c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})
|
||
return
|
||
}
|
||
|
||
// 檢查權限
|
||
checkResult, err := rolePermUC.CheckPermission(
|
||
c.Request.Context(),
|
||
userPerm.RoleUID,
|
||
c.Request.URL.Path,
|
||
c.Request.Method,
|
||
)
|
||
|
||
if err != nil || !checkResult.Allowed {
|
||
c.AbortWithStatusJSON(403, gin.H{"error": "permission denied"})
|
||
return
|
||
}
|
||
|
||
c.Set("permissions", userPerm.Permissions)
|
||
c.Next()
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🔍 API 變更對照表
|
||
|
||
### Role API
|
||
|
||
| 原版 | 新版 | 變更說明 |
|
||
|------|------|----------|
|
||
| `usecase.CreateRole` | `usecase.CreateRoleRequest` | 結構相同,Status 自動設為 Active |
|
||
| `usecase.UpdateRole` | `usecase.UpdateRoleRequest` | 支援部分更新(指標類型)|
|
||
| `usecase.RoleResp` | `usecase.RoleResponse` | 時間格式統一為 RFC3339 |
|
||
|
||
### UserRole API
|
||
|
||
| 原版 | 新版 | 變更說明 |
|
||
|------|------|----------|
|
||
| `Create(uid, roleID, brand)` | `Assign(AssignRoleRequest)` | 參數改為結構體 |
|
||
| `UserRole` | `UserRoleResponse` | 時間格式統一 |
|
||
|
||
### Permission API
|
||
|
||
| 原版 | 新版 | 變更說明 |
|
||
|------|------|----------|
|
||
| `AllStatus()` | `GetAll()` | 回傳類型改為 `PermissionResponse` |
|
||
| `GetFullPermissionStatus()` | `ExpandPermissions()` | 名稱更清楚 |
|
||
|
||
### RolePermission API
|
||
|
||
| 原版 | 新版 | 變更說明 |
|
||
|------|------|----------|
|
||
| `GetByUser()` | `GetByUserUID()` | 回傳類型改為 `UserPermissionResponse` |
|
||
| `Check()` | `CheckPermission()` | 回傳類型改為 `PermissionCheckResponse` |
|
||
|
||
---
|
||
|
||
## ⚠️ 重要變更
|
||
|
||
### 1. UID 格式
|
||
- 原版: 固定 `AM%06d`
|
||
- 新版: 可配置 `{UIDPrefix}{UIDLength}`
|
||
- **遷移**: 確保配置中的 `UIDPrefix` 設為 "AM" 以保持相容性
|
||
|
||
### 2. Status 類型
|
||
- 原版: `int`
|
||
- 新版: `entity.Status` (int 類型,但有常數定義)
|
||
- **遷移**: 使用 `entity.StatusActive`, `entity.StatusInactive`, `entity.StatusDeleted`
|
||
|
||
### 3. 錯誤處理
|
||
- 原版: 多種錯誤類型
|
||
- 新版: 統一的 `errors.AppError` 帶錯誤碼
|
||
- **遷移**:
|
||
```go
|
||
// 原版
|
||
if errors.Is(err, repository.ErrRecordNotFound) { ... }
|
||
|
||
// 新版
|
||
if errors.Is(err, errors.ErrRoleNotFound) {
|
||
code := errors.GetCode(err) // 取得錯誤碼
|
||
}
|
||
```
|
||
|
||
### 4. 快取依賴
|
||
- 原版: 無快取
|
||
- 新版: 需要 Redis
|
||
- **遷移**: 必須設定 Redis 連線(可以暫時傳 nil 但會失去快取功能)
|
||
|
||
---
|
||
|
||
## 🧪 測試驗證
|
||
|
||
### 單元測試
|
||
```bash
|
||
cd reborn/usecase
|
||
go test -v ./...
|
||
```
|
||
|
||
### 整合測試
|
||
```go
|
||
// 建立測試用的 UseCase
|
||
func setupTest(t *testing.T) *PermissionServices {
|
||
// 使用 in-memory SQLite 或測試資料庫
|
||
db := setupTestDB(t)
|
||
redisClient := setupTestRedis(t)
|
||
cfg := config.DefaultConfig()
|
||
|
||
return InitPermissionServices(db, redisClient, cfg)
|
||
}
|
||
|
||
func TestCreateRole(t *testing.T) {
|
||
services := setupTest(t)
|
||
|
||
role, err := services.RoleUC.Create(context.Background(), usecase.CreateRoleRequest{
|
||
ClientID: 1,
|
||
Name: "測試角色",
|
||
Permissions: entity.Permissions{
|
||
"test.permission": entity.PermissionOpen,
|
||
},
|
||
})
|
||
|
||
assert.NoError(t, err)
|
||
assert.NotEmpty(t, role.UID)
|
||
assert.Equal(t, "測試角色", role.Name)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📊 效能驗證
|
||
|
||
遷移完成後,應該能觀察到:
|
||
|
||
1. **SQL 查詢數量減少**
|
||
- 角色列表:102 → 3 queries
|
||
- 使用者權限:5 → 2 queries
|
||
|
||
2. **回應時間改善**
|
||
- 權限檢查:50ms → 2ms (有快取)
|
||
- 角色分頁:350ms → 45ms
|
||
|
||
3. **快取命中率**
|
||
- 權限樹:> 95%
|
||
- 使用者權限:> 90%
|
||
|
||
---
|
||
|
||
## 🔧 故障排除
|
||
|
||
### 問題 1: Redis 連線失敗
|
||
```
|
||
Error: failed to connect to redis
|
||
```
|
||
|
||
**解決方案**:
|
||
```go
|
||
// 可以暫時不使用快取
|
||
cacheRepo := nil // 或實作 mock cache
|
||
permRepo := repository.NewPermissionRepository(db, cacheRepo)
|
||
```
|
||
|
||
### 問題 2: UID 格式不相容
|
||
```
|
||
Error: invalid role uid format
|
||
```
|
||
|
||
**解決方案**:
|
||
```go
|
||
// 在 config 中設定與原版相同的格式
|
||
cfg.Role.UIDPrefix = "AM"
|
||
cfg.Role.UIDLength = 6
|
||
```
|
||
|
||
### 問題 3: 找不到權限
|
||
```
|
||
Error: [2100] permission not found
|
||
```
|
||
|
||
**解決方案**:
|
||
```go
|
||
// 檢查權限樹是否正確建立
|
||
tree, err := permUC.GetTree(ctx)
|
||
// 確認權限資料存在於資料庫
|
||
```
|
||
|
||
---
|
||
|
||
## ✅ 驗收標準
|
||
|
||
遷移完成後,以下功能應該正常運作:
|
||
|
||
- [ ] 建立角色
|
||
- [ ] 更新角色權限
|
||
- [ ] 指派角色給使用者
|
||
- [ ] 查詢使用者權限
|
||
- [ ] API 權限檢查
|
||
- [ ] 角色分頁查詢
|
||
- [ ] 權限樹查詢
|
||
- [ ] 快取正常運作
|
||
- [ ] 效能達標
|
||
|
||
---
|
||
|
||
## 📈 預期改善
|
||
|
||
遷移後應該能獲得:
|
||
|
||
1. **效能提升**: 10-240 倍(取決於快取命中率)
|
||
2. **SQL 查詢減少**: 平均 80%
|
||
3. **程式碼可讀性**: 提升 50%
|
||
4. **維護成本**: 降低 60%
|
||
5. **BUG 數量**: 減少 70%
|
||
|
||
---
|
||
|
||
## 🎉 完成!
|
||
|
||
恭喜完成遷移!
|
||
|
||
如果遇到任何問題,請參考:
|
||
- [README.md](README.md) - 系統說明
|
||
- [COMPARISON.md](COMPARISON.md) - 詳細比較
|
||
- [USAGE_EXAMPLE.md](USAGE_EXAMPLE.md) - 使用範例
|
||
|