后端: - trafficmgr: 实现 tc htb 限速(ApplyRateLimit/RemoveRateLimit),自动推导接口名 - 各协议服务: 代码质量改进与错误处理增强 前端: - protocols.js: 170个硬编码中文字符串全部转为 i18n key,中英文双语翻译 - 提取 fmtBytes() 到 utils/format.js 公共模块(消除 4 处重复) - router.js: 添加路由级权限守卫,非管理员无法直接 URL 访问管理页 - Tunnels.vue: SSE 日志流改用 fetch+ReadableStream,token 放 Authorization header - VPNUsers.vue: 修复 catch 块中文硬编码 fallback - 新增 TaskProgress 组件、taskmgr 后端任务管理
239 lines
5.9 KiB
Go
239 lines
5.9 KiB
Go
package api
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"vpn-platform/backend/internal/model"
|
|
"vpn-platform/backend/internal/service"
|
|
"vpn-platform/backend/internal/store"
|
|
"vpn-platform/backend/internal/taskmgr"
|
|
)
|
|
|
|
// batchRequest 批量操作请求体
|
|
type batchRequest struct {
|
|
TunnelIDs []uint `json:"tunnel_ids" binding:"required,min=1"`
|
|
}
|
|
|
|
// batchTunnels 批量启动/停止/删除隧道
|
|
func (s *Server) batchTunnels(action taskmgr.TaskAction) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
var req batchRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
// 验证所有隧道存在
|
|
var tunnels []model.Tunnel
|
|
if err := store.DB.Where("id IN ?", req.TunnelIDs).Find(&tunnels).Error; err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询隧道失败"})
|
|
return
|
|
}
|
|
if len(tunnels) != len(req.TunnelIDs) {
|
|
found := map[uint]bool{}
|
|
for _, t := range tunnels {
|
|
found[t.ID] = true
|
|
}
|
|
var missing []uint
|
|
for _, id := range req.TunnelIDs {
|
|
if !found[id] {
|
|
missing = append(missing, id)
|
|
}
|
|
}
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("隧道不存在: %v", missing)})
|
|
return
|
|
}
|
|
|
|
// 构建名称映射
|
|
names := map[uint]string{}
|
|
for _, t := range tunnels {
|
|
names[t.ID] = t.Name
|
|
}
|
|
|
|
// 创建任务
|
|
taskID := s.taskMgr.Create(action, req.TunnelIDs, names)
|
|
username := c.GetString("username")
|
|
|
|
// 并发执行
|
|
go func() {
|
|
var wg sync.WaitGroup
|
|
sem := make(chan struct{}, 5) // 最多 5 个并发
|
|
|
|
for _, tid := range req.TunnelIDs {
|
|
wg.Add(1)
|
|
sem <- struct{}{}
|
|
go func(tunnelID uint) {
|
|
defer wg.Done()
|
|
defer func() { <-sem }()
|
|
|
|
s.taskMgr.MarkRunning(taskID, tunnelID)
|
|
err := s.executeBatchAction(action, tunnelID, username)
|
|
if err != nil {
|
|
s.taskMgr.MarkFailed(taskID, tunnelID, err)
|
|
} else {
|
|
s.taskMgr.MarkDone(taskID, tunnelID)
|
|
}
|
|
}(tid)
|
|
}
|
|
wg.Wait()
|
|
}()
|
|
|
|
c.JSON(http.StatusAccepted, gin.H{"task_id": taskID})
|
|
}
|
|
}
|
|
|
|
// executeBatchAction 执行单个隧道的操作
|
|
func (s *Server) executeBatchAction(action taskmgr.TaskAction, tunnelID uint, username string) error {
|
|
var t model.Tunnel
|
|
if err := store.DB.First(&t, tunnelID).Error; err != nil {
|
|
return fmt.Errorf("隧道不存在")
|
|
}
|
|
|
|
mu := lockTunnel(t.ID)
|
|
defer mu.Unlock()
|
|
|
|
switch action {
|
|
case taskmgr.ActionStart:
|
|
if t.ExpireAt != nil && time.Now().After(*t.ExpireAt) {
|
|
return fmt.Errorf("专线已到期")
|
|
}
|
|
p, err := s.mgr.Get(t.Protocol)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if t.State == "running" {
|
|
return nil // 已经运行中,视为成功
|
|
}
|
|
if err := p.Start(&t); err != nil {
|
|
t.State = "error"
|
|
t.LastError = err.Error()
|
|
store.DB.Save(&t)
|
|
return err
|
|
}
|
|
t.State = "running"
|
|
t.LastError = ""
|
|
store.DB.Save(&t)
|
|
store.Audit(username, "batch_start", t.Name, "")
|
|
|
|
case taskmgr.ActionStop:
|
|
p, err := s.mgr.Get(t.Protocol)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if t.State == "stopped" {
|
|
return nil
|
|
}
|
|
if err := p.Stop(&t); err != nil {
|
|
return err
|
|
}
|
|
t.State = "stopped"
|
|
store.DB.Save(&t)
|
|
store.Audit(username, "batch_stop", t.Name, "")
|
|
|
|
case taskmgr.ActionDelete:
|
|
if p, err := s.mgr.Get(t.Protocol); err == nil {
|
|
_ = p.Delete(&t)
|
|
}
|
|
if err := service.RemoveInstance(&t); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("清理实例文件失败: %w", err)
|
|
}
|
|
s.mgr.RemoveStatus(t.ID)
|
|
store.DB.Delete(&t)
|
|
store.Audit(username, "batch_delete", t.Name, "")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getTask 查询单个任务状态
|
|
func (s *Server) getTask(c *gin.Context) {
|
|
id := c.Param("id")
|
|
t, ok := s.taskMgr.Get(id)
|
|
if !ok {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "任务不存在"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, t)
|
|
}
|
|
|
|
// listTasks 列出最近任务
|
|
func (s *Server) listTasks(c *gin.Context) {
|
|
tasks := s.taskMgr.List()
|
|
c.JSON(http.StatusOK, tasks)
|
|
}
|
|
|
|
// ensureID 确保 ID 有效(从 batchCreateUsers 模式参考)
|
|
func parseTunnelIDs(c *gin.Context) ([]uint, error) {
|
|
var req batchRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, id := range req.TunnelIDs {
|
|
if id == 0 {
|
|
return nil, fmt.Errorf("无效的隧道 ID: %d", id)
|
|
}
|
|
}
|
|
return req.TunnelIDs, nil
|
|
}
|
|
|
|
// batchTrafficPolicy 批量设置流量策略
|
|
type batchPolicyRequest struct {
|
|
TunnelIDs []uint `json:"tunnel_ids" binding:"required,min=1"`
|
|
RateLimitMbps int `json:"rate_limit_mbps"`
|
|
QuotaBytes int64 `json:"quota_bytes"`
|
|
ActionOnExceed string `json:"action_on_exceed"`
|
|
ResetCycle string `json:"reset_cycle"`
|
|
}
|
|
|
|
func (s *Server) batchSetPolicy(c *gin.Context) {
|
|
var req batchPolicyRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
success := 0
|
|
var errors []string
|
|
for _, tid := range req.TunnelIDs {
|
|
var t model.Tunnel
|
|
if err := store.DB.First(&t, tid).Error; err != nil {
|
|
errors = append(errors, fmt.Sprintf("隧道 %d 不存在", tid))
|
|
continue
|
|
}
|
|
var policy model.TrafficPolicy
|
|
if err := store.DB.Where("tunnel_id = ?", tid).First(&policy).Error; err != nil {
|
|
policy.TunnelID = tid
|
|
}
|
|
if req.RateLimitMbps > 0 {
|
|
policy.RateLimitMbps = req.RateLimitMbps
|
|
}
|
|
if req.QuotaBytes > 0 {
|
|
policy.QuotaBytes = req.QuotaBytes
|
|
}
|
|
if req.ActionOnExceed != "" {
|
|
policy.ActionOnExceed = req.ActionOnExceed
|
|
}
|
|
if req.ResetCycle != "" {
|
|
policy.ResetCycle = req.ResetCycle
|
|
}
|
|
if err := store.DB.Save(&policy).Error; err != nil {
|
|
errors = append(errors, fmt.Sprintf("隧道 %d 策略保存失败: %s", t.Name, err.Error()))
|
|
continue
|
|
}
|
|
success++
|
|
store.Audit(c.GetString("username"), "batch_policy", t.Name, strconv.Itoa(req.RateLimitMbps)+"Mbps")
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": success,
|
|
"errors": errors,
|
|
})
|
|
}
|