Ver Fonte

✨ feat: implement backend channel duplication & streamline frontend copy flow

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.
t0ng7u há 7 meses atrás
pai
commit
a36ce199ba
3 ficheiros alterados com 54 adições e 16 exclusões
  1. 49 0
      controller/channel.go
  2. 1 0
      router/api-router.go
  3. 4 16
      web/src/components/table/ChannelsTable.js

+ 49 - 0
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}})
+}

+ 1 - 0
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())

+ 4 - 16
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));
     }
   };