二维码自动补发
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user