Skip to content

Commit 8b567aa

Browse files
committed
feat: 服务卡片拖拽顺序
1 parent eac12c7 commit 8b567aa

File tree

11 files changed

+772
-179
lines changed

11 files changed

+772
-179
lines changed

internal/api/services.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ func SetupServicesRoutes(rg *gin.RouterGroup, servicesService *services.ServiceI
2727
rg.GET("/services/:sid", servicesHandler.GetServiceByID)
2828
rg.GET("/services/available-instances", servicesHandler.GetAvailableInstances)
2929
rg.POST("/services/assemble", servicesHandler.AssembleService)
30+
rg.POST("/services/sorts", servicesHandler.UpdateServicesSorts)
3031

3132
// 服务操作路由
3233
rg.POST("/services/:sid/start", servicesHandler.StartService)
@@ -218,3 +219,22 @@ func (h *ServicesHandler) SyncService(c *gin.Context) {
218219
"message": "同步服务成功",
219220
})
220221
}
222+
223+
// UpdateServicesSorts 批量更新服务排序
224+
func (h *ServicesHandler) UpdateServicesSorts(c *gin.Context) {
225+
var req services.UpdateServicesSortsRequest
226+
if err := c.ShouldBindJSON(&req); err != nil {
227+
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误: " + err.Error()})
228+
return
229+
}
230+
231+
if err := h.servicesService.UpdateServicesSorts(&req); err != nil {
232+
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新排序失败: " + err.Error()})
233+
return
234+
}
235+
236+
c.JSON(http.StatusOK, gin.H{
237+
"success": true,
238+
"message": "排序已保存",
239+
})
240+
}

internal/services/models.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,14 @@ type AssembleServiceRequest struct {
4141
ClientInstanceId string `json:"clientInstanceId" binding:"required"`
4242
ServerInstanceId *string `json:"serverInstanceId,omitempty"`
4343
}
44+
45+
// ServiceSortItem 服务排序项
46+
type ServiceSortItem struct {
47+
Sid string `json:"sid" binding:"required"`
48+
Sorts int64 `json:"sorts" binding:"required"`
49+
}
50+
51+
// UpdateServicesSortsRequest 更新服务排序请求
52+
type UpdateServicesSortsRequest struct {
53+
Services []ServiceSortItem `json:"services" binding:"required,min=1"`
54+
}

internal/services/service.go

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ func NewService(db *gorm.DB, tunnelService *tunnel.Service, sseManager *sse.Mana
2727
}
2828
}
2929

30-
// GetServices 获取所有服务
30+
// GetServices 获取所有服务(按 sorts 降序排序,更大的值排在前面)
3131
func (s *ServiceImpl) GetServices() ([]*models.Services, error) {
3232
var services []*models.Services
33-
err := s.db.Order("sorts").Find(&services).Error
33+
err := s.db.Order("sorts DESC").Find(&services).Error
3434
return services, err
3535
}
3636

@@ -581,3 +581,55 @@ func (s *ServiceImpl) syncServiceFromTunnel(sid, serviceType, instanceID string,
581581

582582
return nil
583583
}
584+
585+
// UpdateServicesSorts 批量更新服务排序(优化版:使用 CASE WHEN 单条 SQL)
586+
func (s *ServiceImpl) UpdateServicesSorts(req *UpdateServicesSortsRequest) error {
587+
if len(req.Services) == 0 {
588+
return nil
589+
}
590+
591+
// 开启事务
592+
tx := s.db.Begin()
593+
if tx.Error != nil {
594+
return fmt.Errorf("开启事务失败: %w", tx.Error)
595+
}
596+
defer func() {
597+
if r := recover(); r != nil {
598+
tx.Rollback()
599+
}
600+
}()
601+
602+
// 构建批量更新 SQL(使用 CASE WHEN)
603+
// UPDATE services SET sorts = CASE sid
604+
// WHEN 'service-1' THEN 0
605+
// WHEN 'service-2' THEN 1
606+
// ELSE sorts
607+
// END
608+
// WHERE sid IN ('service-1', 'service-2', ...)
609+
610+
var caseSQL string
611+
var sids []string
612+
var args []interface{}
613+
614+
for _, item := range req.Services {
615+
caseSQL += " WHEN ? THEN ?"
616+
args = append(args, item.Sid, item.Sorts)
617+
sids = append(sids, item.Sid)
618+
}
619+
620+
sql := fmt.Sprintf("UPDATE services SET sorts = CASE sid %s ELSE sorts END WHERE sid IN (?)", caseSQL)
621+
622+
// 执行批量更新
623+
if err := tx.Exec(sql, append(args, sids)...).Error; err != nil {
624+
tx.Rollback()
625+
return fmt.Errorf("批量更新服务排序失败: %w", err)
626+
}
627+
628+
// 提交事务
629+
if err := tx.Commit().Error; err != nil {
630+
return fmt.Errorf("提交事务失败: %w", err)
631+
}
632+
633+
log.Infof("[Service] 批量更新 %d 个服务的排序成功", len(req.Services))
634+
return nil
635+
}

internal/sse/service.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,18 @@ func (s *Service) upsertService(instanceID string, tunnel *models.Tunnel) {
729729
updateColumns = []string{"alias", "client_instance_id", "client_endpoint_id", "exit_host", "exit_port", "total_rx", "total_tx"}
730730
}
731731
}
732+
// 检查服务是否已存在,如果不存在则自动设置 sorts
733+
var existingService models.Services
734+
isNewService := s.db.Where("sid = ? AND type = ?", *peer.SID, *peer.Type).First(&existingService).Error != nil
735+
736+
if isNewService {
737+
// 新服务:查询当前最大 sorts 值并 +1
738+
var maxSorts int64
739+
s.db.Model(&models.Services{}).Select("COALESCE(MAX(sorts), -1)").Scan(&maxSorts)
740+
service.Sorts = maxSorts + 1
741+
log.Infof("新服务 SID=%s, Type=%s, 自动设置 sorts=%d", *peer.SID, *peer.Type, service.Sorts)
742+
}
743+
732744
// 使用 OnConflict 处理插入或更新
733745
if err := s.db.Clauses(clause.OnConflict{
734746
Columns: []clause.Column{

web/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
"preview": "vite preview"
1212
},
1313
"dependencies": {
14+
"@dnd-kit/core": "^6.3.1",
15+
"@dnd-kit/sortable": "^10.0.0",
16+
"@dnd-kit/utilities": "^3.2.2",
1417
"@fortawesome/fontawesome-svg-core": "^6.5.0",
1518
"@fortawesome/free-brands-svg-icons": "^6.5.0",
1619
"@fortawesome/free-solid-svg-icons": "^6.5.0",

web/pnpm-lock.yaml

Lines changed: 56 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)