✨ 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.
This commit is contained in:
@@ -943,3 +943,52 @@ func GetTagModels(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
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}})
|
||||||
|
}
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
channelRoute.POST("/fetch_models", controller.FetchModels)
|
channelRoute.POST("/fetch_models", controller.FetchModels)
|
||||||
channelRoute.POST("/batch/tag", controller.BatchSetChannelTag)
|
channelRoute.POST("/batch/tag", controller.BatchSetChannelTag)
|
||||||
channelRoute.GET("/tag/models", controller.GetTagModels)
|
channelRoute.GET("/tag/models", controller.GetTagModels)
|
||||||
|
channelRoute.POST("/copy/:id", controller.CopyChannel)
|
||||||
}
|
}
|
||||||
tokenRoute := apiRouter.Group("/token")
|
tokenRoute := apiRouter.Group("/token")
|
||||||
tokenRoute.Use(middleware.UserAuth())
|
tokenRoute.Use(middleware.UserAuth())
|
||||||
|
|||||||
@@ -964,28 +964,16 @@ const ChannelsTable = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const copySelectedChannel = async (record) => {
|
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 {
|
try {
|
||||||
const newChannel = { ...channelToCopy, id: undefined };
|
const res = await API.post(`/api/channel/copy/${record.id}`);
|
||||||
const response = await API.post('/api/channel/', newChannel);
|
if (res?.data?.success) {
|
||||||
if (response.data.success) {
|
|
||||||
showSuccess(t('渠道复制成功'));
|
showSuccess(t('渠道复制成功'));
|
||||||
await refresh();
|
await refresh();
|
||||||
} else {
|
} else {
|
||||||
showError(response.data.message);
|
showError(res?.data?.message || t('渠道复制失败'));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(t('渠道复制失败: ') + error.message);
|
showError(t('渠道复制失败: ') + (error?.response?.data?.message || error?.message || error));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user