diff --git a/go-backend/internal/http/handler/handler.go b/go-backend/internal/http/handler/handler.go index c9262d7..bb2f65d 100644 --- a/go-backend/internal/http/handler/handler.go +++ b/go-backend/internal/http/handler/handler.go @@ -138,6 +138,7 @@ func (h *Handler) Register(mux *http.ServeMux) { mux.HandleFunc("/api/v1/user/update", h.userUpdate) mux.HandleFunc("/api/v1/user/delete", h.userDelete) mux.HandleFunc("/api/v1/user/reset", h.userResetFlow) + mux.HandleFunc("/api/v1/user/batch-set-max-conn", h.userBatchSetMaxConn) mux.HandleFunc("/api/v1/user/quota/reset", h.userQuotaReset) mux.HandleFunc("/api/v1/user/groups", h.userGroups) mux.HandleFunc("/api/v1/config/get", h.getConfigByName) diff --git a/go-backend/internal/http/handler/mutations.go b/go-backend/internal/http/handler/mutations.go index 2c5f112..efe9fb3 100644 --- a/go-backend/internal/http/handler/mutations.go +++ b/go-backend/internal/http/handler/mutations.go @@ -214,6 +214,26 @@ func (h *Handler) userUpdate(w http.ResponseWriter, r *http.Request) { response.WriteJSON(w, response.OKEmpty()) } +func (h *Handler) userBatchSetMaxConn(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + response.WriteJSON(w, response.ErrDefault("请求失败")) + return + } + var req struct { + UserIDs []int64 `json:"userIds"` + MaxConn int `json:"maxConn"` + } + if err := decodeJSON(r.Body, &req); err != nil || len(req.UserIDs) == 0 { + response.WriteJSON(w, response.ErrDefault("请求参数错误")) + return + } + s, f := h.repo.BatchUpdateUserMaxConn(req.UserIDs, req.MaxConn) + response.WriteJSON(w, response.OK(map[string]interface{}{ + "success": s, + "failed": f, + })) +} + func (h *Handler) userGroups(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("请求失败")) diff --git a/go-backend/internal/http/middleware/auth.go b/go-backend/internal/http/middleware/auth.go index e739ac7..bc195c0 100644 --- a/go-backend/internal/http/middleware/auth.go +++ b/go-backend/internal/http/middleware/auth.go @@ -137,7 +137,7 @@ func requiresAdmin(path string) bool { } switch path { - case "/api/v1/user/create", "/api/v1/user/list", "/api/v1/user/update", "/api/v1/user/delete", "/api/v1/user/reset": + case "/api/v1/user/create", "/api/v1/user/list", "/api/v1/user/update", "/api/v1/user/delete", "/api/v1/user/reset", "/api/v1/user/batch-set-max-conn": return true case "/api/v1/config/update", "/api/v1/config/update-single": return true diff --git a/go-backend/internal/store/repo/repository_mutations.go b/go-backend/internal/store/repo/repository_mutations.go index 979d187..8567d82 100644 --- a/go-backend/internal/store/repo/repository_mutations.go +++ b/go-backend/internal/store/repo/repository_mutations.go @@ -1320,6 +1320,23 @@ func (r *Repository) BatchUpdateForwardStatus(ids []int64, status int) (int, int return s, f } +func (r *Repository) BatchUpdateUserMaxConn(ids []int64, maxConn int) (int, int) { + if r == nil || r.db == nil { + return 0, len(ids) + } + s := 0 + f := 0 + now := time.Now().UnixMilli() + for _, id := range ids { + if err := r.db.Model(&model.User{}).Where("id = ?", id).Updates(map[string]interface{}{"max_conn": maxConn, "updated_time": now}).Error; err != nil { + f++ + } else { + s++ + } + } + return s, f +} + func (r *Repository) CreateTunnelTx(tx *gorm.DB, name string, trafficRatio float64, typeVal int, flow int64, now int64, status int, inIP interface{}, inx int, ipPreference string) (int64, error) { inIPVal := nullStringFromInterface(inIP) tunnel := model.Tunnel{ diff --git a/vite-frontend/src/api/index.ts b/vite-frontend/src/api/index.ts index 1145736..215ebcd 100644 --- a/vite-frontend/src/api/index.ts +++ b/vite-frontend/src/api/index.ts @@ -240,6 +240,8 @@ export const resetUserFlow = (data: { id: number; type: number }) => Network.post("/user/reset", data); export const resetUserQuota = (data: UserQuotaResetPayload) => Network.post("/user/quota/reset", data); +export const batchSetUserMaxConn = (data: { userIds: number[]; maxConn: number }) => + Network.post("/user/batch-set-max-conn", data); export const getUserGroups = (id: number) => Network.post("/user/groups", { id }); diff --git a/vite-frontend/src/pages/user.tsx b/vite-frontend/src/pages/user.tsx index 38b782a..4bf12f0 100644 --- a/vite-frontend/src/pages/user.tsx +++ b/vite-frontend/src/pages/user.tsx @@ -58,6 +58,7 @@ import { getSpeedLimitList, resetUserFlow, resetUserQuota, + batchSetUserMaxConn, getUserGroupList, getUserGroups, getMonitorPermissionList, @@ -201,6 +202,10 @@ export default function UserPage() { size: 10, total: 0, }); + const [selectedUserIds, setSelectedUserIds] = useState>(new Set()); + const [isBatchMaxConnModalOpen, setIsBatchMaxConnModalOpen] = useState(false); + const [batchMaxConnValue, setBatchMaxConnValue] = useState(0); + const [batchMaxConnLoading, setBatchMaxConnLoading] = useState(false); const searchDebounceRef = useRef | null>(null); // 用户表单相关状态 @@ -904,6 +909,60 @@ export default function UserPage() { } }; + const handleBatchSetMaxConn = async () => { + if (selectedUserIds.size === 0) { + toast.error("请先选择用户"); + return; + } + + setBatchMaxConnLoading(true); + try { + const response = await batchSetUserMaxConn({ + userIds: Array.from(selectedUserIds), + maxConn: batchMaxConnValue, + }); + + if (response.code === 0) { + const data = response.data as { success: number; failed: number } | null; + const { success = 0, failed = 0 } = data || {}; + if (failed === 0) { + toast.success(`成功设置 ${success} 个用户的连接数限制`); + } else { + toast.success(`成功 ${success} 个,失败 ${failed} 个`); + } + setIsBatchMaxConnModalOpen(false); + setSelectedUserIds(new Set()); + await loadUsers(searchKeyword); + } else { + toast.error(response.msg || "批量设置失败"); + } + } catch { + toast.error("批量设置失败"); + } finally { + setBatchMaxConnLoading(false); + } + }; + + const toggleSelectUser = (userId: number) => { + setSelectedUserIds((prev) => { + const next = new Set(prev); + if (next.has(userId)) { + next.delete(userId); + } else { + next.add(userId); + } + return next; + }); + }; + + const toggleSelectAll = () => { + if (selectedUserIds.size === users.length) { + setSelectedUserIds(new Set()); + } else { + setSelectedUserIds(new Set(users.map((u) => u.id))); + } + }; + // 隧道流量重置相关函数 const handleResetTunnelFlow = (userTunnel: UserTunnel) => { setTunnelToReset(userTunnel); @@ -1046,6 +1105,19 @@ export default function UserPage() {
+ {selectedUserIds.size > 0 && ( + + )} + + + + ); }