Skip to content

Commit 2f35e8c

Browse files
committed
feat: 对接dial、实例排序
1 parent 8b567aa commit 2f35e8c

File tree

6 files changed

+184
-30
lines changed

6 files changed

+184
-30
lines changed

internal/api/tunnel.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ func SetupTunnelRoutes(rg *gin.RouterGroup, tunnelService *tunnel.Service, sseMa
6262
rg.POST("/tunnels/create_by_url", tunnelHandler.HandleQuickCreateTunnel)
6363
rg.POST("/tunnels/quick-batch", tunnelHandler.HandleQuickBatchCreateTunnel)
6464
rg.POST("/tunnels/template", tunnelHandler.HandleTemplateCreate)
65+
rg.POST("/tunnels/sorts", tunnelHandler.HandleUpdateTunnelsSorts)
6566
rg.PATCH("/tunnels", tunnelHandler.HandlePatchTunnels)
6667
rg.PATCH("/tunnels/:id", tunnelHandler.HandlePatchTunnels)
6768
rg.PATCH("/tunnels/:id/attributes", tunnelHandler.HandlePatchTunnelAttributes)
@@ -831,6 +832,8 @@ func (h *TunnelHandler) HandleGetTunnelDetails(c *gin.Context) {
831832
"tlsMode": tunnel.TLSMode,
832833
"commandLine": tunnel.CommandLine,
833834
"configLine": tunnel.ConfigLine,
835+
"sorts": tunnel.Sorts,
836+
"dial": tunnel.Dial,
834837

835838
// endpoint 改为对象形式
836839
"endpoint": map[string]interface{}{
@@ -3215,3 +3218,19 @@ func (h *TunnelHandler) HandleUpdateInstanceTags(c *gin.Context) {
32153218
"data": result,
32163219
})
32173220
}
3221+
3222+
// HandleUpdateTunnelsSorts 批量更新隧道排序 (POST /api/tunnels/sorts)
3223+
func (h *TunnelHandler) HandleUpdateTunnelsSorts(c *gin.Context) {
3224+
var req tunnel.UpdateTunnelsSortsRequest
3225+
if err := c.ShouldBindJSON(&req); err != nil {
3226+
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误: " + err.Error()})
3227+
return
3228+
}
3229+
3230+
if err := h.tunnelService.UpdateTunnelsSorts(&req); err != nil {
3231+
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新排序失败: " + err.Error()})
3232+
return
3233+
}
3234+
3235+
c.JSON(http.StatusOK, gin.H{"success": true, "message": "排序已保存"})
3236+
}

internal/models/models.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,12 @@ type Tunnel struct {
9797
Tags *map[string]string `json:"tags,omitempty" gorm:"type:text;serializer:json;column:tags"`
9898

9999
// 配置行 (存储Config字段内容)
100-
ConfigLine *string `json:"configLine,omitempty" gorm:"type:text;column:config_line"`
101-
Peer *Peer `json:"peer,omitempty" gorm:"type:text;serializer:json;column:peer"`
100+
ConfigLine *string `json:"configLine,omitempty" gorm:"type:text;column:config_line"`
101+
Peer *Peer `json:"peer,omitempty" gorm:"type:text;serializer:json;column:peer"`
102+
Dial *string `json:"dial,omitempty" gorm:"type:text;column:dial"` //出站源IP地址
103+
104+
Sorts int64 `json:"sorts" gorm:"type:int;column:sorts;default:0"`
105+
102106
CreatedAt time.Time `json:"createdAt" gorm:"autoCreateTime;index;column:created_at"`
103107
UpdatedAt time.Time `json:"updatedAt" gorm:"autoUpdateTime;column:updated_at"`
104108
LastEventTime NullTime `json:"lastEventTime,omitempty" gorm:"column:last_event_time;type:datetime"`

internal/nodepass/parse.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type TunnelConfig struct {
3232
Slot string
3333
Proxy string // proxy protocol 支持 (0|1)
3434
Quic string // proxy protocol 支持 (0|1)
35+
Dial string // proxy protocol 支持 (0|1)
3536
}
3637

3738
// ParseTunnelURL 解析隧道实例 URL 并返回 Tunnel 模型
@@ -228,6 +229,9 @@ func ParseTunnelURL(rawURL string) *models.Tunnel {
228229
case "noudp":
229230
// UDP支持控制 (0=启用, 1=禁用)
230231
noUDP = &val
232+
case "diap":
233+
// UDP支持控制 (0=启用, 1=禁用)
234+
tunnel.Dial = &val
231235
case "quic":
232236
// QUIC控制 (0=启用, 1=禁用)
233237
switch val {
@@ -303,6 +307,7 @@ func TunnelToMap(tunnel *models.Tunnel) map[string]interface{} {
303307
"proxy_protocol": tunnel.ProxyProtocol,
304308
"config_line": tunnel.ConfigLine,
305309
"listen_type": tunnel.ListenType,
310+
"sorts": tunnel.Sorts,
306311
}
307312

308313
if tunnel.CertPath != nil {
@@ -348,6 +353,9 @@ func TunnelToMap(tunnel *models.Tunnel) map[string]interface{} {
348353
if tunnel.ConfigLine != nil {
349354
updates["config_line"] = tunnel.ConfigLine
350355
}
356+
if tunnel.Dial != nil {
357+
updates["dial"] = tunnel.Dial
358+
}
351359
if tunnel.ExtendTargetAddress != nil {
352360
if extendAddrJSON, err := json.Marshal(tunnel.ExtendTargetAddress); err == nil {
353361
updates["extend_target_address"] = string(extendAddrJSON)
@@ -424,6 +432,7 @@ func ParseTunnelConfig(rawURL string) *TunnelConfig {
424432
cfg.Slot = query.Get("slot")
425433
cfg.Proxy = query.Get("proxy")
426434
cfg.Quic = query.Get("quic")
435+
cfg.Dial = query.Get("dial")
427436
noTCP := query.Get("notcp")
428437
noUDP := query.Get("noudp")
429438

@@ -548,6 +557,9 @@ func (c *TunnelConfig) BuildTunnelConfigURL() string {
548557
if c.Quic != "" {
549558
queryParams = append(queryParams, fmt.Sprintf("quic=%s", c.Quic))
550559
}
560+
if c.Dial != "" {
561+
queryParams = append(queryParams, fmt.Sprintf("dial=%s", c.Dial))
562+
}
551563

552564
// 根据listenType生成notcp和noudp参数
553565
if c.ListenType != "" {
@@ -721,6 +733,9 @@ func BuildTunnelURLs(tunnel models.Tunnel) string {
721733
if tunnel.Slot != nil {
722734
queryParams = append(queryParams, fmt.Sprintf("slot=%d", *tunnel.Slot))
723735
}
736+
if tunnel.Dial != nil {
737+
queryParams = append(queryParams, fmt.Sprintf("dial=%s", *tunnel.Dial))
738+
}
724739
if tunnel.Quic != nil && protocol == "server" {
725740
quicVal := "0"
726741
if *tunnel.Quic {

internal/tunnel/models.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,3 +226,14 @@ type TunnelResponse struct {
226226
Tunnel interface{} `json:"tunnel,omitempty"`
227227
TunnelIDs []int64 `json:"tunnel_ids,omitempty"` // 创建的隧道ID列表
228228
}
229+
230+
// TunnelSortItem 隧道排序项
231+
type TunnelSortItem struct {
232+
ID int64 `json:"id" binding:"required"`
233+
Sorts int64 `json:"sorts" binding:"required"`
234+
}
235+
236+
// UpdateTunnelsSortsRequest 更新隧道排序请求
237+
type UpdateTunnelsSortsRequest struct {
238+
Tunnels []TunnelSortItem `json:"tunnels" binding:"required,min=1"`
239+
}

internal/tunnel/service.go

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func (s *Service) GetTunnels() ([]TunnelWithStats, error) {
5656
COALESCE(e.ver, '') as version
5757
FROM tunnels t
5858
LEFT JOIN endpoints e ON t.endpoint_id = e.id
59-
ORDER BY t.created_at DESC
59+
ORDER BY t.sorts DESC, t.id DESC
6060
`
6161

6262
rows, err := sqlDB.Query(query)
@@ -214,6 +214,10 @@ func (s *Service) CreateTunnel(req CreateTunnelRequest) (*Tunnel, error) {
214214
var tunnelID int64
215215

216216
if err == gorm.ErrRecordNotFound {
217+
// 查询当前最大 sorts 值并 +1(自动设置排序)
218+
var maxSorts int64
219+
tx.Model(&models.Tunnel{}).Select("COALESCE(MAX(sorts), -1)").Scan(&maxSorts)
220+
217221
// 创建新记录
218222
newTunnel := models.Tunnel{
219223
InstanceID: &response.ID,
@@ -229,10 +233,13 @@ func (s *Service) CreateTunnel(req CreateTunnelRequest) (*Tunnel, error) {
229233
CommandLine: commandLine,
230234
Restart: &req.Restart,
231235
Status: models.TunnelStatusRunning,
236+
Sorts: maxSorts + 1,
232237
CreatedAt: now,
233238
UpdatedAt: now,
234239
}
235240

241+
log.Infof("[API] 新隧道自动设置 sorts=%d", newTunnel.Sorts)
242+
236243
// 处理可选字段
237244
if req.CertPath != "" {
238245
newTunnel.CertPath = &req.CertPath
@@ -2431,24 +2438,24 @@ func (s *Service) GetTunnelsWithPagination(params TunnelQueryParams) (*TunnelLis
24312438
if params.SortBy != "" {
24322439
switch params.SortBy {
24332440
case "name":
2434-
orderClause = fmt.Sprintf(" ORDER BY t.name %s", params.SortOrder)
2441+
orderClause = fmt.Sprintf(" ORDER BY t.name %s, t.sorts DESC, t.id DESC", params.SortOrder)
24352442
case "created_at":
2436-
orderClause = fmt.Sprintf(" ORDER BY t.created_at %s", params.SortOrder)
2443+
orderClause = fmt.Sprintf(" ORDER BY t.created_at %s, t.sorts DESC, t.id DESC", params.SortOrder)
24372444
case "status":
2438-
orderClause = fmt.Sprintf(" ORDER BY t.status %s", params.SortOrder)
2445+
orderClause = fmt.Sprintf(" ORDER BY t.status %s, t.sorts DESC, t.id DESC", params.SortOrder)
24392446
case "tunnelAddress":
2440-
orderClause = fmt.Sprintf(" ORDER BY t.tunnel_address %s, t.tunnel_port %s", params.SortOrder, params.SortOrder)
2447+
orderClause = fmt.Sprintf(" ORDER BY t.tunnel_address %s, t.tunnel_port %s, t.sorts DESC, t.id DESC", params.SortOrder, params.SortOrder)
24412448
case "targetAddress":
2442-
orderClause = fmt.Sprintf(" ORDER BY t.target_address %s, t.target_port %s", params.SortOrder, params.SortOrder)
2449+
orderClause = fmt.Sprintf(" ORDER BY t.target_address %s, t.target_port %s, t.sorts DESC, t.id DESC", params.SortOrder, params.SortOrder)
24432450
case "type":
2444-
orderClause = fmt.Sprintf(" ORDER BY t.type %s", params.SortOrder)
2451+
orderClause = fmt.Sprintf(" ORDER BY t.type %s, t.sorts DESC, t.id DESC", params.SortOrder)
24452452
case "updated_at":
2446-
orderClause = fmt.Sprintf(" ORDER BY t.updated_at %s", params.SortOrder)
2453+
orderClause = fmt.Sprintf(" ORDER BY t.updated_at %s, t.sorts DESC, t.id DESC", params.SortOrder)
24472454
default:
2448-
orderClause = " ORDER BY t.created_at DESC"
2455+
orderClause = " ORDER BY t.sorts DESC, t.id DESC"
24492456
}
24502457
} else {
2451-
orderClause = " ORDER BY t.created_at DESC"
2458+
orderClause = " ORDER BY t.sorts DESC, t.id DESC"
24522459
}
24532460

24542461
// 构建分页
@@ -2763,6 +2770,58 @@ func (s *Service) getEndpointWithGroup(endpointID int) (struct {
27632770
return endpoint, nil
27642771
}
27652772

2773+
// UpdateTunnelsSorts 批量更新隧道排序(优化版:使用 CASE WHEN 单条 SQL)
2774+
func (s *Service) UpdateTunnelsSorts(req *UpdateTunnelsSortsRequest) error {
2775+
if len(req.Tunnels) == 0 {
2776+
return nil
2777+
}
2778+
2779+
// 开启事务
2780+
tx := s.db.Begin()
2781+
if tx.Error != nil {
2782+
return fmt.Errorf("开启事务失败: %w", tx.Error)
2783+
}
2784+
defer func() {
2785+
if r := recover(); r != nil {
2786+
tx.Rollback()
2787+
}
2788+
}()
2789+
2790+
// 构建批量更新 SQL(使用 CASE WHEN)
2791+
// UPDATE tunnels SET sorts = CASE id
2792+
// WHEN 1 THEN 10
2793+
// WHEN 2 THEN 9
2794+
// ELSE sorts
2795+
// END
2796+
// WHERE id IN (1, 2, ...)
2797+
2798+
var caseSQL string
2799+
var ids []int64
2800+
var args []interface{}
2801+
2802+
for _, item := range req.Tunnels {
2803+
caseSQL += " WHEN ? THEN ?"
2804+
args = append(args, item.ID, item.Sorts)
2805+
ids = append(ids, item.ID)
2806+
}
2807+
2808+
sql := fmt.Sprintf("UPDATE tunnels SET sorts = CASE id %s ELSE sorts END WHERE id IN (?)", caseSQL)
2809+
2810+
// 执行批量更新
2811+
if err := tx.Exec(sql, append(args, ids)...).Error; err != nil {
2812+
tx.Rollback()
2813+
return fmt.Errorf("批量更新隧道排序失败: %w", err)
2814+
}
2815+
2816+
// 提交事务
2817+
if err := tx.Commit().Error; err != nil {
2818+
return fmt.Errorf("提交事务失败: %w", err)
2819+
}
2820+
2821+
log.Infof("[Tunnel] 批量更新 %d 个隧道的排序成功", len(req.Tunnels))
2822+
return nil
2823+
}
2824+
27662825
// getTunnelsByIDs 根据ID列表获取隧道信息
27672826
func (s *Service) getTunnelsByIDs(tunnelIDs []int) ([]models.Tunnel, error) {
27682827
var tunnels []models.Tunnel

web/src/components/tunnels/simple-create-tunnel-modal.tsx

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ export default function SimpleCreateTunnelModal({
150150
loadBalancingIPs: "", // 负载均衡IP地址,一行一个
151151
extendTargetAddresses: "", // 扩展目标地址,一行一个
152152
quic: "", // QUIC 支持:启用/关闭
153+
dial: "",// Dial
154+
sorts: "",
153155
});
154156

155157
// 当打开时加载端点,并在 edit 时从API获取隧道详情
@@ -216,6 +218,8 @@ export default function SimpleCreateTunnelModal({
216218
: "false"
217219
: "",
218220
loadBalancingIPs: tunnel.loadBalancingIPs || "",
221+
sorts: tunnel.sorts || "",
222+
dial: tunnel.dial || "",
219223
// 扩展目标地址和监听类型
220224
listenType: tunnel.listenType || "ALL",
221225
extendTargetAddresses: tunnel.extendTargetAddress
@@ -868,7 +872,7 @@ export default function SimpleCreateTunnelModal({
868872
}}
869873
>
870874
<div className="space-y-2">
871-
<div className={`grid grid-cols-${((isClientType && formData.mode === 2) || isServerType) ? 3 : 1} gap-2`}
875+
<div className={`grid grid-cols-${((isClientType && formData.mode === 2) || isServerType) ? 3 : 3} gap-2`}
872876
>
873877

874878
{isShowClientPoolMin && (
@@ -882,12 +886,38 @@ export default function SimpleCreateTunnelModal({
882886
onValueChange={(v) => handleField("min", v ? String(v) : "")}
883887
/>
884888
{proxyProtocolSelect}
889+
<Input
890+
label="权重(越大越前)"
891+
placeholder="0"
892+
type="number"
893+
value={formData.sorts}
894+
onValueChange={(v) => handleField("sorts", v ? String(v) : "")}
895+
/>
896+
<Input
897+
label="Dial"
898+
placeholder="出站源IP地址"
899+
value={formData.dial}
900+
onValueChange={(v) => handleField("dial", v ? String(v) : "")}
901+
/>
885902
</>
886903
)}
887904
{isClientType &&
888905
formData.mode === 1 && (
889906
<>
890907
{proxyProtocolSelect}
908+
<Input
909+
label="权重(越大越前)"
910+
placeholder="0"
911+
type="number"
912+
value={formData.sorts}
913+
onValueChange={(v) => handleField("sorts", v ? String(v) : "")}
914+
/>
915+
<Input
916+
label="Dial"
917+
placeholder="出站源IP地址"
918+
value={formData.dial}
919+
onValueChange={(v) => handleField("dial", v ? String(v) : "")}
920+
/>
891921
</>
892922
)}
893923
{isServerType && (
@@ -901,6 +931,7 @@ export default function SimpleCreateTunnelModal({
901931
onValueChange={(v) => handleField("max", v ? String(v) : "")}
902932
/>
903933
{proxyProtocolSelect}
934+
904935
</>
905936
)}
906937
</div>
@@ -934,8 +965,40 @@ export default function SimpleCreateTunnelModal({
934965
value={formData.slot}
935966
onValueChange={(v) => handleField("slot", v ? String(v) : "")}
936967
/>
968+
{/* 启用 QUIC */}
969+
{formData.type == 'server' &&
970+
<>
971+
<Select
972+
label="启用 QUIC"
973+
selectedKeys={
974+
formData.quic ? [formData.quic] : ["false"]
975+
}
976+
onSelectionChange={(keys) => {
977+
const selectedKey = Array.from(keys)[0] as string;
978+
handleField("quic", selectedKey);
979+
}}
980+
>
981+
<SelectItem key="false">关闭</SelectItem>
982+
<SelectItem key="true">启用</SelectItem>
983+
</Select>
984+
<Input
985+
label="权重(越大越前)"
986+
placeholder="0"
987+
type="number"
988+
value={formData.sorts}
989+
onValueChange={(v) => handleField("sorts", v ? String(v) : "")}
990+
/>
991+
<Input
992+
label="Dial"
993+
placeholder="出站源IP地址"
994+
value={formData.dial}
995+
onValueChange={(v) => handleField("dial", v ? String(v) : "")}
996+
/>
997+
</>
998+
}
937999
</div>
9381000

1001+
9391002
{/* 扩展目标地址 */}
9401003
<div className="flex items-start gap-2">
9411004
{isEnableLoadBalancing && (
@@ -959,23 +1022,6 @@ export default function SimpleCreateTunnelModal({
9591022
/>
9601023
)}
9611024
</div>
962-
963-
{/* 启用 QUIC */}
964-
{formData.type=='server'&& <div>
965-
<Select
966-
label="启用 QUIC"
967-
selectedKeys={
968-
formData.quic ? [formData.quic] : ["false"]
969-
}
970-
onSelectionChange={(keys) => {
971-
const selectedKey = Array.from(keys)[0] as string;
972-
handleField("quic", selectedKey);
973-
}}
974-
>
975-
<SelectItem key="false">关闭</SelectItem>
976-
<SelectItem key="true">启用</SelectItem>
977-
</Select>
978-
</div>}
9791025
</div>
9801026
</motion.div>
9811027
)}

0 commit comments

Comments
 (0)