二维码自动补发

This commit is contained in:
yml2213
2026-06-19 17:08:57 +08:00
parent 5ff1ea40b0
commit 076ac6098e
7 changed files with 303 additions and 43 deletions
+15
View File
@@ -76,6 +76,21 @@ func (ChatQrCode) TableName() string {
return "chat_qrcode_pool"
}
type ChatQrCodeDeliveryTask struct {
ID uint64 `gorm:"primaryKey" json:"id"`
ConversationID uint64 `gorm:"column:conversation_id;not null;uniqueIndex" json:"conversation_id"`
Status string `gorm:"size:16;not null;default:'pending';index" json:"status"`
QrCodeID *uint64 `gorm:"column:qrcode_id;index" json:"qrcode_id"`
ErrorMessage string `gorm:"column:error_message;size:255;not null;default:''" json:"error_message"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
SentAt *time.Time `gorm:"column:sent_at" json:"sent_at"`
}
func (ChatQrCodeDeliveryTask) TableName() string {
return "chat_qrcode_delivery_tasks"
}
type ChatSupportGroup struct {
ID uint64 `gorm:"primaryKey" json:"id"`
Code string `gorm:"size:64;not null;uniqueIndex" json:"code"`
+18 -4
View File
@@ -112,7 +112,7 @@ func EnsureListingConversation(tx *gorm.DB, listing model.RentalListing, preferr
// 6. 发二维码图片(如果有)
if qrcodeImageURL != "" {
if err := sendQrCodeImage(tx, conversation.ID, qrcodeImageURL); err != nil {
if _, err := sendQrCodeImage(tx, conversation.ID, qrcodeImageURL); err != nil {
return nil, err
}
} else {
@@ -121,6 +121,9 @@ func EnsureListingConversation(tx *gorm.DB, listing model.RentalListing, preferr
if err := sendSystemMessage(tx, conversation.ID, noQrTip); err != nil {
return nil, err
}
if err := createQrCodeDeliveryTask(tx, conversation.ID); err != nil {
return nil, err
}
}
// 7. 库存预警检查
@@ -262,7 +265,7 @@ func sendSystemMessage(tx *gorm.DB, conversationID uint64, content string) error
return updateConversationLastMessage(tx, conversationID, &message)
}
func sendQrCodeImage(tx *gorm.DB, conversationID uint64, imageURL string) error {
func sendQrCodeImage(tx *gorm.DB, conversationID uint64, imageURL string) (*model.ChatMessage, error) {
content := "👇 请扫码加入企业微信群"
message := model.ChatMessage{
@@ -275,11 +278,14 @@ func sendQrCodeImage(tx *gorm.DB, conversationID uint64, imageURL string) error
}
if err := tx.Create(&message).Error; err != nil {
return err
return nil, err
}
// 更新会话的最后消息
return updateConversationLastMessage(tx, conversationID, &message)
if err := updateConversationLastMessage(tx, conversationID, &message); err != nil {
return nil, err
}
return &message, nil
}
func updateConversationLastMessage(tx *gorm.DB, conversationID uint64, message *model.ChatMessage) error {
@@ -326,6 +332,14 @@ func markQrCodeAsUsed(tx *gorm.DB, qrcodeID uint64, conversationID uint64) error
}).Error
}
func createQrCodeDeliveryTask(tx *gorm.DB, conversationID uint64) error {
task := model.ChatQrCodeDeliveryTask{
ConversationID: conversationID,
Status: "pending",
}
return tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&task).Error
}
// pushAlert 事务提交后需要发送的外部推送。
type pushAlert struct {
providers []push.Provider
+129 -28
View File
@@ -8,6 +8,7 @@ import (
"gorm.io/gorm"
"gorm.io/gorm/clause"
"hfb_sys/backend/internal/model"
"hfb_sys/backend/internal/modules/chathub"
)
// QrCodeStatus 二维码状态常量
@@ -83,24 +84,35 @@ type QrCodeOCRConfig struct {
// CreateQrCode 创建二维码
func (r *Repository) CreateQrCode(ctx context.Context, adminID uint64, req CreateQrCodeRequest) (*model.ChatQrCode, error) {
qrcode := model.ChatQrCode{
ImageURL: req.ImageURL,
GroupName: req.GroupName,
Status: QrCodeStatusUnused,
CreatedBy: adminID,
Note: req.Note,
ExpiresAt: req.ExpiresAt,
}
var qrcode model.ChatQrCode
var deliveredMessages []model.ChatMessage
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
qrcode = model.ChatQrCode{
ImageURL: req.ImageURL,
GroupName: req.GroupName,
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
}
// 如果未指定过期时间,默认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 {
if err := tx.Create(&qrcode).Error; err != nil {
return err
}
var err error
deliveredMessages, err = deliverPendingQrCodes(tx)
return err
})
if err != nil {
return nil, err
}
r.notifyDeliveredQrCodeMessages(deliveredMessages)
return &qrcode, nil
}
@@ -108,26 +120,36 @@ func (r *Repository) CreateQrCode(ctx context.Context, adminID uint64, req Creat
// BatchCreateQrCode 批量创建二维码
func (r *Repository) BatchCreateQrCode(ctx context.Context, adminID uint64, req BatchCreateQrCodeRequest) ([]model.ChatQrCode, error) {
var qrcodes []model.ChatQrCode
var deliveredMessages []model.ChatMessage
defaultExpires := time.Now().Add(7 * 24 * time.Hour)
for _, item := range req.Items {
qr := model.ChatQrCode{
ImageURL: item.ImageURL,
GroupName: item.GroupName,
Status: QrCodeStatusUnused,
CreatedBy: adminID,
Note: item.Note,
ExpiresAt: item.ExpiresAt,
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
for _, item := range req.Items {
qr := model.ChatQrCode{
ImageURL: item.ImageURL,
GroupName: item.GroupName,
Status: QrCodeStatusUnused,
CreatedBy: adminID,
Note: item.Note,
ExpiresAt: item.ExpiresAt,
}
if qr.ExpiresAt == nil {
qr.ExpiresAt = &defaultExpires
}
qrcodes = append(qrcodes, qr)
}
if qr.ExpiresAt == nil {
qr.ExpiresAt = &defaultExpires
}
qrcodes = append(qrcodes, qr)
}
if err := r.db.WithContext(ctx).Create(&qrcodes).Error; err != nil {
if err := tx.Create(&qrcodes).Error; err != nil {
return err
}
var err error
deliveredMessages, err = deliverPendingQrCodes(tx)
return err
})
if err != nil {
return nil, err
}
r.notifyDeliveredQrCodeMessages(deliveredMessages)
return qrcodes, nil
}
@@ -342,3 +364,82 @@ func (r *Repository) markQrCodeAsUsed(tx *gorm.DB, qrcodeID uint64, conversation
"used_at": now,
}).Error
}
func deliverPendingQrCodes(tx *gorm.DB) ([]model.ChatMessage, error) {
deliveredMessages := make([]model.ChatMessage, 0)
for {
var task model.ChatQrCodeDeliveryTask
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("status = ?", "pending").
Order("created_at ASC, id ASC").
First(&task).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return deliveredMessages, nil
}
return nil, err
}
qrcode, err := fetchUnusedQrCode(tx)
if err != nil {
return nil, err
}
if qrcode == nil {
return deliveredMessages, nil
}
if err := markQrCodeAsUsed(tx, qrcode.ID, task.ConversationID); err != nil {
return nil, err
}
message, err := sendQrCodeImage(tx, task.ConversationID, qrcode.ImageURL)
if err != nil {
now := time.Now()
if updateErr := tx.Model(&task).Updates(map[string]interface{}{
"status": "failed",
"qrcode_id": qrcode.ID,
"error_message": err.Error(),
"updated_at": now,
}).Error; updateErr != nil {
return nil, updateErr
}
continue
}
deliveredMessages = append(deliveredMessages, *message)
now := time.Now()
if err := tx.Model(&task).Updates(map[string]interface{}{
"status": "sent",
"qrcode_id": qrcode.ID,
"error_message": "",
"sent_at": now,
"updated_at": now,
}).Error; err != nil {
return nil, err
}
}
}
func (r *Repository) notifyDeliveredQrCodeMessages(messages []model.ChatMessage) {
if r.hub == nil {
return
}
for _, msg := range messages {
event := &chathub.ChatEvent{
Type: "new_message",
ConversationID: msg.ConversationID,
Message: &chathub.MessageData{
ID: msg.ID,
ConversationID: msg.ConversationID,
SenderType: msg.SenderType,
SenderID: msg.SenderID,
SenderRole: msg.SenderRole,
SenderName: "系统",
ContentType: msg.ContentType,
Content: msg.Content,
AttachmentURLS: decodeStringList(msg.AttachmentURLS),
CreatedAt: msg.CreatedAt.Format(time.RFC3339),
},
}
r.hub.NotifyConversation(msg.ConversationID, event)
r.hub.NotifyAllAdmins(event)
}
}
+96 -1
View File
@@ -21,7 +21,19 @@ func setupQrCodeTestDB(t *testing.T) *gorm.DB {
if err != nil {
t.Fatalf("无法创建测试数据库: %v", err)
}
if err := db.AutoMigrate(&model.ChatQrCode{}, &model.ChatConversation{}, &model.SystemConfig{}); err != nil {
if err := db.AutoMigrate(
&model.ChatQrCode{},
&model.ChatConversation{},
&model.ChatParticipant{},
&model.ChatMessage{},
&model.ChatQrCodeDeliveryTask{},
&model.ChatSupportGroup{},
&model.ChatSupportGroupMember{},
&model.AdminUser{},
&model.AdminUserRole{},
&model.Role{},
&model.SystemConfig{},
); err != nil {
t.Fatalf("数据库迁移失败: %v", err)
}
return db
@@ -218,3 +230,86 @@ func TestUpdateQrCodeGroupNameAndRenameFlag(t *testing.T) {
t.Fatal("企微改名标记未保存")
}
}
func TestEnsureListingConversationCreatesQrCodeDeliveryTaskWhenStockEmpty(t *testing.T) {
db := setupQrCodeTestDB(t)
listing := model.RentalListing{
ID: 101,
ListingNo: "L202606260001",
OwnerID: 10,
}
if err := db.Transaction(func(tx *gorm.DB) error {
_, err := EnsureListingConversation(tx, listing, 0)
return err
}); err != nil {
t.Fatalf("建发布群失败: %v", err)
}
var task model.ChatQrCodeDeliveryTask
if err := db.First(&task).Error; err != nil {
t.Fatalf("未创建补发任务: %v", err)
}
if task.Status != "pending" {
t.Fatalf("补发任务状态 = %q, want pending", task.Status)
}
}
func TestBatchCreateQrCodeDeliversPendingTask(t *testing.T) {
db := setupQrCodeTestDB(t)
repo := NewRepository(db, nil)
conversation := model.ChatConversation{
Title: "账号群 L202606260002",
Type: ConversationTypeListingGroup,
Status: "active",
}
if err := db.Create(&conversation).Error; err != nil {
t.Fatalf("创建群聊失败: %v", err)
}
task := model.ChatQrCodeDeliveryTask{
ConversationID: conversation.ID,
Status: "pending",
}
if err := db.Create(&task).Error; err != nil {
t.Fatalf("创建补发任务失败: %v", err)
}
_, err := repo.BatchCreateQrCode(t.Context(), 1, BatchCreateQrCodeRequest{
Items: []CreateQrCodeRequest{
{ImageURL: "/api/files/object?key=qrcode/replenish.png", GroupName: "补发群"},
},
})
if err != nil {
t.Fatalf("批量创建二维码失败: %v", err)
}
var savedTask model.ChatQrCodeDeliveryTask
if err := db.First(&savedTask, task.ID).Error; err != nil {
t.Fatalf("查询补发任务失败: %v", err)
}
if savedTask.Status != "sent" {
t.Fatalf("补发任务状态 = %q, want sent", savedTask.Status)
}
if savedTask.QrCodeID == nil {
t.Fatal("补发任务未记录二维码 ID")
}
if savedTask.SentAt == nil {
t.Fatal("补发任务未记录 sent_at")
}
var qrcode model.ChatQrCode
if err := db.First(&qrcode, *savedTask.QrCodeID).Error; err != nil {
t.Fatalf("查询二维码失败: %v", err)
}
if qrcode.Status != QrCodeStatusUsed {
t.Fatalf("二维码状态 = %q, want used", qrcode.Status)
}
if qrcode.ConversationID == nil || *qrcode.ConversationID != conversation.ID {
t.Fatalf("二维码绑定会话 = %v, want %d", qrcode.ConversationID, conversation.ID)
}
var message model.ChatMessage
if err := db.Where("conversation_id = ? AND content_type = ?", conversation.ID, "image").First(&message).Error; err != nil {
t.Fatalf("未生成二维码图片消息: %v", err)
}
}