From 093d86040fc03950dcd0d8205cfd8635915f3281 Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Sun, 22 Jun 2025 16:35:30 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20feat(token):=20implemen?= =?UTF-8?q?t=20batch=20token=20deletion=20API=20&=20front-end=20integratio?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Back-end • model/token.go • Add `BatchDeleteTokens(ids []int, userId int)` – transactional DB removal + async Redis cache cleanup. • controller/token.go • Introduce `TokenBatch` DTO and `DeleteTokenBatch` handler calling the model layer; returns amount deleted. • router/api-router.go • Register `POST /api/token/batch` route (user-scoped). • Front-end (TokensTable.js) • Replace per-token deletion loops with single request to `/api/token/batch`. • Display dynamic i18n message: “Deleted {{count}} tokens!”. • Add modal confirmation: • Title “Batch delete token”. • Content “Are you sure you want to delete the selected {{count}} tokens?”. • UI/UX tweaks • Responsive button group (flex-wrap, mobile line-break). • Clear `selectedKeys` after refresh / successful deletion to avoid ghost selections. • i18n • Ensure placeholder style matches translation keys (`{{count}}`). This commit delivers efficient, scalable token management and an improved user experience across devices. --- controller/token.go | 29 ++++++++++++++ model/token.go | 34 ++++++++++++++++ router/api-router.go | 1 + web/src/components/table/TokensTable.js | 53 +++++++++++++++++++++++-- web/src/i18n/locales/en.json | 6 +++ 5 files changed, 120 insertions(+), 3 deletions(-) diff --git a/controller/token.go b/controller/token.go index c57552c0..173fc22e 100644 --- a/controller/token.go +++ b/controller/token.go @@ -258,3 +258,32 @@ func UpdateToken(c *gin.Context) { }) return } + +type TokenBatch struct { + Ids []int `json:"ids"` +} + +func DeleteTokenBatch(c *gin.Context) { + tokenBatch := TokenBatch{} + if err := c.ShouldBindJSON(&tokenBatch); err != nil || len(tokenBatch.Ids) == 0 { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "参数错误", + }) + return + } + userId := c.GetInt("id") + count, err := model.BatchDeleteTokens(tokenBatch.Ids, userId) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + "data": count, + }) +} diff --git a/model/token.go b/model/token.go index 2ed2c09a..7e68f185 100644 --- a/model/token.go +++ b/model/token.go @@ -327,3 +327,37 @@ func CountUserTokens(userId int) (int64, error) { err := DB.Model(&Token{}).Where("user_id = ?", userId).Count(&total).Error return total, err } + +// BatchDeleteTokens 删除指定用户的一组令牌,返回成功删除数量 +func BatchDeleteTokens(ids []int, userId int) (int, error) { + if len(ids) == 0 { + return 0, errors.New("ids 不能为空!") + } + + tx := DB.Begin() + + var tokens []Token + if err := tx.Where("user_id = ? AND id IN (?)", userId, ids).Find(&tokens).Error; err != nil { + tx.Rollback() + return 0, err + } + + if err := tx.Where("user_id = ? AND id IN (?)", userId, ids).Delete(&Token{}).Error; err != nil { + tx.Rollback() + return 0, err + } + + if err := tx.Commit().Error; err != nil { + return 0, err + } + + if common.RedisEnabled { + gopool.Go(func() { + for _, t := range tokens { + _ = cacheDeleteToken(t.Key) + } + }) + } + + return len(tokens), nil +} diff --git a/router/api-router.go b/router/api-router.go index badfa7bf..db4c3898 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -125,6 +125,7 @@ func SetApiRouter(router *gin.Engine) { tokenRoute.POST("/", controller.AddToken) tokenRoute.PUT("/", controller.UpdateToken) tokenRoute.DELETE("/:id", controller.DeleteToken) + tokenRoute.POST("/batch", controller.DeleteTokenBatch) } redemptionRoute := apiRouter.Group("/redemption") redemptionRoute.Use(middleware.AdminAuth()) diff --git a/web/src/components/table/TokensTable.js b/web/src/components/table/TokensTable.js index bc6c7607..d49a344d 100644 --- a/web/src/components/table/TokensTable.js +++ b/web/src/components/table/TokensTable.js @@ -435,6 +435,7 @@ const TokensTable = () => { const refresh = async () => { await loadTokens(1); + setSelectedKeys([]); }; const copyText = async (text) => { @@ -583,6 +584,29 @@ const TokensTable = () => { } }; + const batchDeleteTokens = async () => { + if (selectedKeys.length === 0) { + showError(t('请先选择要删除的令牌!')); + return; + } + setLoading(true); + try { + const ids = selectedKeys.map((token) => token.id); + const res = await API.post('/api/token/batch', { ids }); + if (res?.data?.success) { + const count = res.data.data || 0; + showSuccess(t('已删除 {{count}} 个令牌!', { count })); + await refresh(); + } else { + showError(res?.data?.message || t('删除失败')); + } + } catch (error) { + showError(error.message); + } finally { + setLoading(false); + } + }; + const renderHeader = () => (
@@ -595,12 +619,12 @@ const TokensTable = () => {
-
+
+
+