feat: 后端限速实现 + 前端i18n全面国际化 + 安全与代码质量修复
后端: - 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 后端任务管理
This commit is contained in:
1 parent
021d012478
commit
91974fc30f
26 files changed
+1977
-377
No files matched your search
@@ -0,0 +1,238 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user