package chat import ( "context" "errors" "time" "gorm.io/gorm" "gorm.io/gorm/clause" "hfb_sys/backend/internal/model" ) // QrCodeStatus 二维码状态常量 const ( QrCodeStatusUnused = "unused" QrCodeStatusUsed = "used" QrCodeStatusDisabled = "disabled" ) var ( ErrQrCodeNotFound = errors.New("二维码不存在") ErrQrCodeCannotDelete = errors.New("已使用的二维码不能删除") ErrQrCodeCannotReenable = errors.New("已发放的二维码不能改回待用") ) // CreateQrCodeRequest 创建二维码请求 type CreateQrCodeRequest struct { ImageURL string `json:"image_url" binding:"required"` Note string `json:"note"` ExpiresAt *time.Time `json:"expires_at"` } // BatchCreateQrCodeRequest 批量创建二维码请求 type BatchCreateQrCodeRequest struct { Items []CreateQrCodeRequest `json:"items" binding:"required,min=1,max=20"` } // UpdateQrCodeRequest 更新二维码请求 type UpdateQrCodeRequest struct { ImageURL *string `json:"image_url"` Note *string `json:"note"` Status *string `json:"status"` ExpiresAt *time.Time `json:"expires_at"` ClearExpiresAt bool `json:"clear_expires_at"` } // QrCodeListRequest 列表查询请求 type QrCodeListRequest struct { Status string `form:"status"` Page int `form:"page"` Limit int `form:"limit"` } // QrCodeStats 二维码统计 type QrCodeStats struct { UnusedCount int64 `json:"unused_count"` UsedCount int64 `json:"used_count"` DisabledCount int64 `json:"disabled_count"` TotalCount int64 `json:"total_count"` } // CreateQrCode 创建二维码 func (r *Repository) CreateQrCode(ctx context.Context, adminID uint64, req CreateQrCodeRequest) (*model.ChatQrCode, error) { qrcode := model.ChatQrCode{ ImageURL: req.ImageURL, Status: QrCodeStatusUnused, CreatedBy: adminID, Note: req.Note, ExpiresAt: req.ExpiresAt, } // 如果未指定过期时间,默认7天后过期 if qrcode.ExpiresAt == nil { expires := time.Now().Add(7 * 24 * time.Hour) qrcode.ExpiresAt = &expires } if err := r.db.WithContext(ctx).Create(&qrcode).Error; err != nil { return nil, err } return &qrcode, nil } // BatchCreateQrCode 批量创建二维码 func (r *Repository) BatchCreateQrCode(ctx context.Context, adminID uint64, req BatchCreateQrCodeRequest) ([]model.ChatQrCode, error) { var qrcodes []model.ChatQrCode defaultExpires := time.Now().Add(7 * 24 * time.Hour) for _, item := range req.Items { qr := model.ChatQrCode{ ImageURL: item.ImageURL, Status: QrCodeStatusUnused, CreatedBy: adminID, Note: item.Note, ExpiresAt: item.ExpiresAt, } if qr.ExpiresAt == nil { qr.ExpiresAt = &defaultExpires } qrcodes = append(qrcodes, qr) } if err := r.db.WithContext(ctx).Create(&qrcodes).Error; err != nil { return nil, err } return qrcodes, nil } // ListQrCodes 列表查询二维码 func (r *Repository) ListQrCodes(ctx context.Context, req QrCodeListRequest) ([]model.ChatQrCode, int64, error) { if req.Page < 1 { req.Page = 1 } if req.Limit < 1 || req.Limit > 100 { req.Limit = 20 } query := r.db.WithContext(ctx).Model(&model.ChatQrCode{}) // 状态过滤 if req.Status != "" { query = query.Where("status = ?", req.Status) } // 统计总数 var total int64 if err := query.Count(&total).Error; err != nil { return nil, 0, err } // 查询列表 var qrcodes []model.ChatQrCode offset := (req.Page - 1) * req.Limit if err := query.Order("id DESC").Offset(offset).Limit(req.Limit).Find(&qrcodes).Error; err != nil { return nil, 0, err } return qrcodes, total, nil } // GetQrCodeStats 获取二维码统计信息 func (r *Repository) GetQrCodeStats(ctx context.Context) (*QrCodeStats, error) { stats := &QrCodeStats{} now := time.Now() if err := r.db.WithContext(ctx).Model(&model.ChatQrCode{}).Count(&stats.TotalCount).Error; err != nil { return nil, err } if err := r.db.WithContext(ctx). Model(&model.ChatQrCode{}). Where("status = ?", QrCodeStatusUsed). Count(&stats.UsedCount).Error; err != nil { return nil, err } if err := r.db.WithContext(ctx). Model(&model.ChatQrCode{}). Where("status = ?", QrCodeStatusDisabled). Count(&stats.DisabledCount).Error; err != nil { return nil, err } if err := r.db.WithContext(ctx). Model(&model.ChatQrCode{}). Where("status = ?", QrCodeStatusUnused). Where("expires_at IS NULL OR expires_at > ?", now). Count(&stats.UnusedCount).Error; err != nil { return nil, err } return stats, nil } // UpdateQrCode 更新二维码 func (r *Repository) UpdateQrCode(ctx context.Context, id uint64, req UpdateQrCodeRequest) error { var qrcode model.ChatQrCode if err := r.db.WithContext(ctx).First(&qrcode, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrQrCodeNotFound } return err } updates := make(map[string]interface{}) if req.ImageURL != nil { updates["image_url"] = *req.ImageURL } if req.Note != nil { updates["note"] = *req.Note } if req.Status != nil { // 校验状态值 if *req.Status != QrCodeStatusUnused && *req.Status != QrCodeStatusUsed && *req.Status != QrCodeStatusDisabled { return errors.New("无效的状态值") } if *req.Status == QrCodeStatusUnused && qrcodeWasIssued(qrcode) { return ErrQrCodeCannotReenable } updates["status"] = *req.Status } if req.ExpiresAt != nil { updates["expires_at"] = req.ExpiresAt } else if req.ClearExpiresAt { updates["expires_at"] = nil } if len(updates) == 0 { return nil } if err := r.db.WithContext(ctx).Model(&qrcode).Updates(updates).Error; err != nil { return err } return nil } func qrcodeWasIssued(qrcode model.ChatQrCode) bool { return qrcode.Status == QrCodeStatusUsed || qrcode.ConversationID != nil || qrcode.UsedAt != nil } // 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 { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrQrCodeNotFound } return err } // 已使用的不能删除 if qrcode.Status == QrCodeStatusUsed { return ErrQrCodeCannotDelete } return r.db.WithContext(ctx).Delete(&qrcode).Error } // fetchUnusedQrCode 获取一个未使用且未过期的二维码(带行锁) func (r *Repository) fetchUnusedQrCode(tx *gorm.DB) (*model.ChatQrCode, error) { var qrcode model.ChatQrCode now := time.Now() err := tx.Where("status = ?", QrCodeStatusUnused). Where("expires_at IS NULL OR expires_at > ?", now). Order("id ASC"). Limit(1). Clauses(clause.Locking{Strength: "UPDATE"}). First(&qrcode).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil // 无可用二维码,返回 nil 而非错误 } return nil, err } return &qrcode, nil } // markQrCodeAsUsed 标记二维码为已使用 func (r *Repository) markQrCodeAsUsed(tx *gorm.DB, qrcodeID uint64, conversationID uint64) error { now := time.Now() return tx.Model(&model.ChatQrCode{}). Where("id = ?", qrcodeID). Updates(map[string]interface{}{ "status": QrCodeStatusUsed, "conversation_id": conversationID, "used_at": now, }).Error }