diff --git a/backend/internal/modules/chat/handler_qrcode.go b/backend/internal/modules/chat/handler_qrcode.go index 75c5df8..259a01b 100644 --- a/backend/internal/modules/chat/handler_qrcode.go +++ b/backend/internal/modules/chat/handler_qrcode.go @@ -44,6 +44,23 @@ func (h *Handler) BatchCreateQrCodeHandler(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"data": qrcodes}) } +// BatchDeleteQrCodeHandler 批量删除二维码 +func (h *Handler) BatchDeleteQrCodeHandler(c *gin.Context) { + var req BatchDeleteQrCodeRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + deleted, err := h.service.repo.BatchDeleteQrCodes(c.Request.Context(), req.IDs) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"data": gin.H{"deleted": deleted}}) +} + // ListQrCodesHandler 列表查询二维码 func (h *Handler) ListQrCodesHandler(c *gin.Context) { var req QrCodeListRequest @@ -157,10 +174,6 @@ func (h *Handler) DeleteQrCodeHandler(c *gin.Context) { c.JSON(http.StatusNotFound, gin.H{"error": "二维码不存在"}) return } - if err == ErrQrCodeCannotDelete { - c.JSON(http.StatusBadRequest, gin.H{"error": "已使用的二维码不能删除"}) - return - } c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } diff --git a/backend/internal/modules/chat/qrcode.go b/backend/internal/modules/chat/qrcode.go index e3d7489..016108d 100644 --- a/backend/internal/modules/chat/qrcode.go +++ b/backend/internal/modules/chat/qrcode.go @@ -26,7 +26,6 @@ const ( var ( ErrQrCodeNotFound = errors.New("二维码不存在") - ErrQrCodeCannotDelete = errors.New("已使用的二维码不能删除") ErrQrCodeCannotReenable = errors.New("已发放的二维码不能改回待用") ) @@ -43,6 +42,11 @@ type BatchCreateQrCodeRequest struct { Items []CreateQrCodeRequest `json:"items" binding:"required,min=1,max=20"` } +// BatchDeleteQrCodeRequest 批量删除二维码请求 +type BatchDeleteQrCodeRequest struct { + IDs []uint64 `json:"ids" binding:"required,min=1,max=100"` +} + // UpdateQrCodeRequest 更新二维码请求 type UpdateQrCodeRequest struct { ImageURL *string `json:"image_url"` @@ -313,7 +317,7 @@ func qrcodeWasIssued(qrcode model.ChatQrCode) bool { return qrcode.Status == QrCodeStatusUsed || qrcode.ConversationID != nil || qrcode.UsedAt != nil } -// DeleteQrCode 删除二维码(仅未使用的可删除) +// DeleteQrCode 删除二维码 func (r *Repository) DeleteQrCode(ctx context.Context, id uint64) error { var qrcode model.ChatQrCode if err := r.db.WithContext(ctx).First(&qrcode, id).Error; err != nil { @@ -323,12 +327,20 @@ func (r *Repository) DeleteQrCode(ctx context.Context, id uint64) error { return err } - // 已使用的不能删除 - if qrcode.Status == QrCodeStatusUsed { - return ErrQrCodeCannotDelete + return r.db.WithContext(ctx).Delete(&qrcode).Error +} + +// BatchDeleteQrCodes 批量删除二维码 +func (r *Repository) BatchDeleteQrCodes(ctx context.Context, ids []uint64) (int64, error) { + if len(ids) == 0 { + return 0, nil } - return r.db.WithContext(ctx).Delete(&qrcode).Error + result := r.db.WithContext(ctx).Where("id IN ?", ids).Delete(&model.ChatQrCode{}) + if result.Error != nil { + return 0, result.Error + } + return result.RowsAffected, nil } // fetchUnusedQrCode 获取一个未使用且未过期的二维码(带行锁) diff --git a/backend/internal/modules/chat/qrcode_test.go b/backend/internal/modules/chat/qrcode_test.go index 9e2ae31..9a3a810 100644 --- a/backend/internal/modules/chat/qrcode_test.go +++ b/backend/internal/modules/chat/qrcode_test.go @@ -231,6 +231,80 @@ func TestUpdateQrCodeGroupNameAndRenameFlag(t *testing.T) { } } +func TestDeleteQrCodeAllowsIssuedQrCode(t *testing.T) { + db := setupQrCodeTestDB(t) + repo := NewRepository(db, nil) + now := time.Date(2026, 6, 19, 17, 8, 0, 0, time.UTC) + conversationID := uint64(1003) + qrcode := model.ChatQrCode{ + ImageURL: "/api/files/object?key=qrcode/issued.png", + Status: QrCodeStatusUsed, + ConversationID: &conversationID, + UsedAt: &now, + CreatedBy: 1, + } + if err := db.Create(&qrcode).Error; err != nil { + t.Fatalf("创建二维码失败: %v", err) + } + + if err := repo.DeleteQrCode(t.Context(), qrcode.ID); err != nil { + t.Fatalf("删除已发放二维码失败: %v", err) + } + + var count int64 + if err := db.Model(&model.ChatQrCode{}).Where("id = ?", qrcode.ID).Count(&count).Error; err != nil { + t.Fatalf("统计二维码失败: %v", err) + } + if count != 0 { + t.Fatalf("二维码数量 = %d, want 0", count) + } +} + +func TestBatchDeleteQrCodes(t *testing.T) { + db := setupQrCodeTestDB(t) + repo := NewRepository(db, nil) + now := time.Date(2026, 6, 19, 17, 8, 0, 0, time.UTC) + conversationID := uint64(1004) + qrcodes := []model.ChatQrCode{ + { + ImageURL: "/api/files/object?key=qrcode/unused.png", + Status: QrCodeStatusUnused, + CreatedBy: 1, + }, + { + ImageURL: "/api/files/object?key=qrcode/used.png", + Status: QrCodeStatusUsed, + ConversationID: &conversationID, + UsedAt: &now, + CreatedBy: 1, + }, + { + ImageURL: "/api/files/object?key=qrcode/keep.png", + Status: QrCodeStatusDisabled, + CreatedBy: 1, + }, + } + if err := db.Create(&qrcodes).Error; err != nil { + t.Fatalf("创建二维码失败: %v", err) + } + + deleted, err := repo.BatchDeleteQrCodes(t.Context(), []uint64{qrcodes[0].ID, qrcodes[1].ID}) + if err != nil { + t.Fatalf("批量删除二维码失败: %v", err) + } + if deleted != 2 { + t.Fatalf("删除数量 = %d, want 2", deleted) + } + + var remaining []model.ChatQrCode + if err := db.Order("id ASC").Find(&remaining).Error; err != nil { + t.Fatalf("查询剩余二维码失败: %v", err) + } + if len(remaining) != 1 || remaining[0].ID != qrcodes[2].ID { + t.Fatalf("剩余二维码 = %+v, want id %d", remaining, qrcodes[2].ID) + } +} + func TestEnsureListingConversationCreatesQrCodeDeliveryTaskWhenStockEmpty(t *testing.T) { db := setupQrCodeTestDB(t) listing := model.RentalListing{ diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index bd3ab44..9bf1098 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -593,6 +593,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { // 二维码池管理 adminRoutes.POST("/chats/qrcodes", requirePerm("chat:manage"), chatHandler.CreateQrCodeHandler) adminRoutes.POST("/chats/qrcodes/batch", requirePerm("chat:manage"), chatHandler.BatchCreateQrCodeHandler) + adminRoutes.POST("/chats/qrcodes/batch-delete", requirePerm("chat:manage"), chatHandler.BatchDeleteQrCodeHandler) adminRoutes.GET("/chats/qrcodes", requirePerm("chat:view"), chatHandler.ListQrCodesHandler) adminRoutes.GET("/chats/qrcodes/stats", requirePerm("chat:view"), chatHandler.GetQrCodeStatsHandler) adminRoutes.POST("/chats/qrcodes/ocr-group-name", requirePerm("chat:manage"), chatHandler.RecognizeQrCodeGroupNameHandler) diff --git a/frontend/src/features/admin/api/adminQrCodes.ts b/frontend/src/features/admin/api/adminQrCodes.ts index 5803b75..17b208e 100644 --- a/frontend/src/features/admin/api/adminQrCodes.ts +++ b/frontend/src/features/admin/api/adminQrCodes.ts @@ -55,6 +55,10 @@ export interface UpdateQrCodePayload { clear_expires_at?: boolean } +export interface BatchDeleteQrCodeResult { + deleted: number +} + export async function fetchQrCodes(query: QrCodeListQuery = {}) { const { data } = await apiClient.get>>( '/admin/chats/qrcodes', @@ -111,3 +115,11 @@ export async function deleteQrCode(id: number) { const { data } = await apiClient.delete>(`/admin/chats/qrcodes/${id}`) return data.data } + +export async function batchDeleteQrCodes(ids: number[]) { + const { data } = await apiClient.post>( + '/admin/chats/qrcodes/batch-delete', + { ids } + ) + return data.data +} diff --git a/frontend/src/features/admin/views/AdminQrCodePoolView.vue b/frontend/src/features/admin/views/AdminQrCodePoolView.vue index a78e90f..d8c4037 100644 --- a/frontend/src/features/admin/views/AdminQrCodePoolView.vue +++ b/frontend/src/features/admin/views/AdminQrCodePoolView.vue @@ -1,10 +1,19 @@ @@ -414,6 +458,15 @@ onMounted(reloadAll) 筛选
+ 已选 {{ selectedCount }} 条 + + 批量删除 + 刷新 添加二维码
@@ -422,7 +475,15 @@ onMounted(reloadAll) - + +