|
|
@@ -0,0 +1,233 @@ |
|
|
|
|
|
package controller |
|
|
|
|
|
|
|
|
|
|
|
import ( |
|
|
|
|
|
"net/http" |
|
|
|
|
|
"strconv" |
|
|
|
|
|
|
|
|
|
|
|
"github.com/QuantumNous/new-api/model" |
|
|
|
|
|
user_migration "github.com/QuantumNous/new-api/service/user_migration" |
|
|
|
|
|
"github.com/QuantumNous/new-api/setting/system_setting" |
|
|
|
|
|
"github.com/gin-gonic/gin" |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
func CreateUserMigrationBatch(c *gin.Context) { |
|
|
|
|
|
var req struct { |
|
|
|
|
|
Name string `json:"name" binding:"required"` |
|
|
|
|
|
SourceRegion string `json:"source_region" binding:"required"` |
|
|
|
|
|
TargetRegion string `json:"target_region" binding:"required"` |
|
|
|
|
|
} |
|
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil { |
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": err.Error()}) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
operatorID, _ := c.Get("id") |
|
|
|
|
|
batch := &model.UserMigrationBatch{ |
|
|
|
|
|
Name: req.Name, |
|
|
|
|
|
SourceRegion: req.SourceRegion, |
|
|
|
|
|
TargetRegion: req.TargetRegion, |
|
|
|
|
|
OperatorId: operatorID.(int), |
|
|
|
|
|
} |
|
|
|
|
|
if err := model.CreateUserMigrationBatch(batch); err != nil { |
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()}) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": batch}) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func ListUserMigrationBatches(c *gin.Context) { |
|
|
|
|
|
var batches []model.UserMigrationBatch |
|
|
|
|
|
if err := model.DB.Order("id desc").Find(&batches).Error; err != nil { |
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()}) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": batches}) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func GetUserMigrationBatch(c *gin.Context) { |
|
|
|
|
|
batchID, _ := strconv.Atoi(c.Param("id")) |
|
|
|
|
|
|
|
|
|
|
|
var batch model.UserMigrationBatch |
|
|
|
|
|
if err := model.DB.First(&batch, batchID).Error; err != nil { |
|
|
|
|
|
c.JSON(http.StatusNotFound, gin.H{"success": false, "error": "batch not found"}) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "0")) |
|
|
|
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "50")) |
|
|
|
|
|
if pageSize <= 0 { |
|
|
|
|
|
pageSize = 50 |
|
|
|
|
|
} |
|
|
|
|
|
if pageSize > 200 { |
|
|
|
|
|
pageSize = 200 |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
var items []model.UserMigrationItem |
|
|
|
|
|
var total int64 |
|
|
|
|
|
model.DB.Model(&model.UserMigrationItem{}).Where("batch_id = ?", batchID).Count(&total) |
|
|
|
|
|
model.DB.Where("batch_id = ?", batchID).Order("id asc").Offset(page * pageSize).Limit(pageSize).Find(&items) |
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{ |
|
|
|
|
|
"success": true, |
|
|
|
|
|
"data": batch, |
|
|
|
|
|
"items": items, |
|
|
|
|
|
"total": total, |
|
|
|
|
|
}) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func ScanUserMigrationBatch(c *gin.Context) { |
|
|
|
|
|
batchID, _ := strconv.Atoi(c.Param("id")) |
|
|
|
|
|
locked, err := model.TryLockBatchForScan(batchID) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()}) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
if !locked { |
|
|
|
|
|
c.JSON(http.StatusConflict, gin.H{"success": false, "error": "batch is already being scanned or not in a scannable state"}) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
svc := newMigrationService() |
|
|
|
|
|
go func() { |
|
|
|
|
|
if err := svc.ScanBatch(batchID); err != nil { |
|
|
|
|
|
model.DB.Model(&model.UserMigrationBatch{}).Where("id = ?", batchID). |
|
|
|
|
|
Update("status", model.UserMigrationBatchStatusFailed) |
|
|
|
|
|
} |
|
|
|
|
|
}() |
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusAccepted, gin.H{"success": true, "message": "scan started"}) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func ResolveUserMigrationItem(c *gin.Context) { |
|
|
|
|
|
itemID, _ := strconv.Atoi(c.Param("id")) |
|
|
|
|
|
|
|
|
|
|
|
var item model.UserMigrationItem |
|
|
|
|
|
if err := model.DB.Select("id", "batch_id").First(&item, itemID).Error; err != nil { |
|
|
|
|
|
c.JSON(http.StatusNotFound, gin.H{"success": false, "error": "item not found"}) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
var req struct { |
|
|
|
|
|
ResolutionStrategy string `json:"resolution_strategy" binding:"required"` |
|
|
|
|
|
TargetUserId int `json:"target_user_id"` |
|
|
|
|
|
} |
|
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil { |
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": err.Error()}) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
validStrategies := map[string]bool{ |
|
|
|
|
|
model.UserMigrationStrategyCreateNew: true, |
|
|
|
|
|
model.UserMigrationStrategyMergeExisting: true, |
|
|
|
|
|
model.UserMigrationStrategySkip: true, |
|
|
|
|
|
} |
|
|
|
|
|
if !validStrategies[req.ResolutionStrategy] { |
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{ |
|
|
|
|
|
"success": false, |
|
|
|
|
|
"error": "invalid resolution_strategy: must be create_new, merge_into_existing, or skip", |
|
|
|
|
|
}) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
if req.ResolutionStrategy == model.UserMigrationStrategyMergeExisting && req.TargetUserId == 0 { |
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{ |
|
|
|
|
|
"success": false, |
|
|
|
|
|
"error": "merge_into_existing requires target_user_id", |
|
|
|
|
|
}) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
updates := map[string]any{ |
|
|
|
|
|
"resolution_strategy": req.ResolutionStrategy, |
|
|
|
|
|
"status": model.UserMigrationItemStatusReady, |
|
|
|
|
|
} |
|
|
|
|
|
if req.TargetUserId != 0 { |
|
|
|
|
|
updates["target_user_id"] = req.TargetUserId |
|
|
|
|
|
} |
|
|
|
|
|
if err := model.DB.Model(&model.UserMigrationItem{}).Where("id = ?", itemID).Updates(updates).Error; err != nil { |
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()}) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
if err := model.RefreshUserMigrationBatchStats(item.BatchId); err != nil { |
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()}) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true}) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func ExecuteUserMigrationBatch(c *gin.Context) { |
|
|
|
|
|
batchID, _ := strconv.Atoi(c.Param("id")) |
|
|
|
|
|
locked, err := model.TryLockBatchForExecution(batchID) |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()}) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
if !locked { |
|
|
|
|
|
c.JSON(http.StatusConflict, gin.H{"success": false, "error": "batch is already running or not in an executable state"}) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
svc := newMigrationService() |
|
|
|
|
|
go func() { |
|
|
|
|
|
_ = svc.ExecuteBatch(batchID) |
|
|
|
|
|
}() |
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusAccepted, gin.H{"success": true, "message": "execution started"}) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func RetryUserMigrationBatch(c *gin.Context) { |
|
|
|
|
|
batchID, _ := strconv.Atoi(c.Param("id")) |
|
|
|
|
|
|
|
|
|
|
|
model.DB.Model(&model.UserMigrationItem{}). |
|
|
|
|
|
Where("batch_id = ? AND status = ?", batchID, model.UserMigrationItemStatusFailed). |
|
|
|
|
|
Updates(map[string]any{"status": model.UserMigrationItemStatusReady, "error_message": ""}) |
|
|
|
|
|
|
|
|
|
|
|
locked, err := model.TryLockBatchForExecution(batchID) |
|
|
|
|
|
if err != nil || !locked { |
|
|
|
|
|
c.JSON(http.StatusConflict, gin.H{"success": false, "error": "batch cannot be retried now"}) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
svc := newMigrationService() |
|
|
|
|
|
go func() { |
|
|
|
|
|
_ = svc.ExecuteBatch(batchID) |
|
|
|
|
|
}() |
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusAccepted, gin.H{"success": true, "message": "retry started"}) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func VerifyUserMigrationBatch(c *gin.Context) { |
|
|
|
|
|
batchID, _ := strconv.Atoi(c.Param("id")) |
|
|
|
|
|
|
|
|
|
|
|
var items []model.UserMigrationItem |
|
|
|
|
|
model.DB.Where("batch_id = ? AND status = ?", batchID, model.UserMigrationItemStatusMigrated).Find(&items) |
|
|
|
|
|
|
|
|
|
|
|
svc := newMigrationService() |
|
|
|
|
|
results := make([]map[string]any, 0, len(items)) |
|
|
|
|
|
for _, item := range items { |
|
|
|
|
|
result, err := svc.VerifyItem(&item) |
|
|
|
|
|
entry := map[string]any{ |
|
|
|
|
|
"source_user_id": item.SourceUserId, |
|
|
|
|
|
"target_user_id": item.TargetUserId, |
|
|
|
|
|
} |
|
|
|
|
|
if err != nil { |
|
|
|
|
|
entry["error"] = err.Error() |
|
|
|
|
|
} else { |
|
|
|
|
|
entry["result"] = result |
|
|
|
|
|
} |
|
|
|
|
|
results = append(results, entry) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": results}) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
func newMigrationService() *user_migration.Service { |
|
|
|
|
|
settings := system_setting.GetRegionSyncSettings() |
|
|
|
|
|
endpoint := settings.MasterEndpoint |
|
|
|
|
|
if settings.IsMaster && len(settings.SlaveEndpoints) > 0 { |
|
|
|
|
|
endpoint = settings.SlaveEndpoints[0] |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
client := user_migration.NewClient(endpoint, settings.SyncApiKey) |
|
|
|
|
|
return user_migration.NewService(client) |
|
|
|
|
|
} |