From a36ce199ba963f7c8cfadfa64619fe43f4c75f90 Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Mon, 14 Jul 2025 21:54:53 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20implement=20backend=20chann?= =?UTF-8?q?el=20duplication=20&=20streamline=20frontend=20copy=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a dedicated backend endpoint to clone an existing channel (including its key) and replace all previous front-end cloning logic with a single API call. Backend • controller/channel.go – add CopyChannel: safely clone a channel, reset balance/usage, append name suffix, preserve key, create abilities, return new ID. – supports optional query params: `suffix`, `reset_balance`. • router/api-router.go – register POST /api/channel/copy/:id (secured by AdminAuth). • model interaction uses BatchInsertChannels to ensure transactional integrity. Frontend • ChannelsTable.js – simplify copySelectedChannel: call /api/channel/copy/{id} and refresh list. – remove complex field-manipulation & key-fetching logic. – improved error handling. Security & stability • All cloning done server-side; sensitive key never exposed to client. • Route inherits existing admin middleware. • Graceful JSON responses with detailed error messages. --- controller/channel.go | 49 +++++++++++++++++++++++ router/api-router.go | 1 + web/src/components/table/ChannelsTable.js | 20 ++------- 3 files changed, 54 insertions(+), 16 deletions(-) diff --git a/controller/channel.go b/controller/channel.go index ee6ddeba..126b7877 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -943,3 +943,52 @@ func GetTagModels(c *gin.Context) { }) return } + +// CopyChannel handles cloning an existing channel with its key. +// POST /api/channel/copy/:id +// Optional query params: +// suffix - string appended to the original name (default "_复制") +// reset_balance - bool, when true will reset balance & used_quota to 0 (default true) +func CopyChannel(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "message": "invalid id"}) + return + } + + suffix := c.DefaultQuery("suffix", "_复制") + resetBalance := true + if rbStr := c.DefaultQuery("reset_balance", "true"); rbStr != "" { + if v, err := strconv.ParseBool(rbStr); err == nil { + resetBalance = v + } + } + + // fetch original channel with key + origin, err := model.GetChannelById(id, true) + if err != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()}) + return + } + + // clone channel + clone := *origin // shallow copy is sufficient as we will overwrite primitives + clone.Id = 0 // let DB auto-generate + clone.CreatedTime = common.GetTimestamp() + clone.Name = origin.Name + suffix + clone.TestTime = 0 + clone.ResponseTime = 0 + if resetBalance { + clone.Balance = 0 + clone.UsedQuota = 0 + } + + // insert + if err := model.BatchInsertChannels([]model.Channel{clone}); err != nil { + c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()}) + return + } + + // success + c.JSON(http.StatusOK, gin.H{"success": true, "message": "", "data": gin.H{"id": clone.Id}}) +} diff --git a/router/api-router.go b/router/api-router.go index db4c3898..4bd2faff 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -115,6 +115,7 @@ func SetApiRouter(router *gin.Engine) { channelRoute.POST("/fetch_models", controller.FetchModels) channelRoute.POST("/batch/tag", controller.BatchSetChannelTag) channelRoute.GET("/tag/models", controller.GetTagModels) + channelRoute.POST("/copy/:id", controller.CopyChannel) } tokenRoute := apiRouter.Group("/token") tokenRoute.Use(middleware.UserAuth()) diff --git a/web/src/components/table/ChannelsTable.js b/web/src/components/table/ChannelsTable.js index 2582b950..2d409b09 100644 --- a/web/src/components/table/ChannelsTable.js +++ b/web/src/components/table/ChannelsTable.js @@ -964,28 +964,16 @@ const ChannelsTable = () => { }; const copySelectedChannel = async (record) => { - const channelToCopy = { ...record }; - channelToCopy.name += t('_复制'); - channelToCopy.created_time = null; - channelToCopy.balance = 0; - channelToCopy.used_quota = 0; - delete channelToCopy.test_time; - delete channelToCopy.response_time; - if (!channelToCopy) { - showError(t('渠道未找到,请刷新页面后重试。')); - return; - } try { - const newChannel = { ...channelToCopy, id: undefined }; - const response = await API.post('/api/channel/', newChannel); - if (response.data.success) { + const res = await API.post(`/api/channel/copy/${record.id}`); + if (res?.data?.success) { showSuccess(t('渠道复制成功')); await refresh(); } else { - showError(response.data.message); + showError(res?.data?.message || t('渠道复制失败')); } } catch (error) { - showError(t('渠道复制失败: ') + error.message); + showError(t('渠道复制失败: ') + (error?.response?.data?.message || error?.message || error)); } };