feat(distribution): 支持分站发布群固定二维码
主站分销配置可为每个站点绑定可复用企微码,号主在该站上传账号后发到对应发布群,不再消耗主站码池。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -38,6 +38,11 @@ func EnsureListingConversation(tx *gorm.DB, listing model.RentalListing, preferr
|
||||
return nil, err
|
||||
}
|
||||
|
||||
siteID, err := resolveListingDistributionSiteID(tx, listing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 建会话
|
||||
now := time.Now()
|
||||
conversation := model.ChatConversation{
|
||||
@@ -46,6 +51,9 @@ func EnsureListingConversation(tx *gorm.DB, listing model.RentalListing, preferr
|
||||
Title: listingConversationTitle(listing),
|
||||
Status: "active",
|
||||
}
|
||||
if siteID > 0 {
|
||||
conversation.DistributionSiteID = &siteID
|
||||
}
|
||||
if err := tx.Create(&conversation).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -88,17 +96,12 @@ func EnsureListingConversation(tx *gorm.DB, listing model.RentalListing, preferr
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 取二维码(带行锁)
|
||||
var qrcode *model.ChatQrCode
|
||||
var qrcodeImageURL string
|
||||
qrcodeObj, err := fetchUnusedQrCodeForListing(tx, listing.ID)
|
||||
// 4. 取二维码:分站优先用站点固定码(可复用);否则消耗对应码池。
|
||||
qrcode, qrcodeImageURL, err := resolveListingQrCode(tx, siteID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if qrcodeObj != nil {
|
||||
qrcode = qrcodeObj
|
||||
qrcodeImageURL = qrcode.ImageURL
|
||||
// 标记二维码为已使用
|
||||
if qrcode != nil {
|
||||
if err := markQrCodeAsUsed(tx, qrcode.ID, conversation.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -309,22 +312,60 @@ func fetchUnusedQrCode(tx *gorm.DB) (*model.ChatQrCode, error) {
|
||||
return fetchUnusedQrCodeForSite(tx, nil)
|
||||
}
|
||||
|
||||
func fetchUnusedQrCodeForListing(tx *gorm.DB, listingID uint64) (*model.ChatQrCode, error) {
|
||||
func resolveListingDistributionSiteID(tx *gorm.DB, listing model.RentalListing) (uint64, error) {
|
||||
if listing.DistributionSiteID != nil && *listing.DistributionSiteID > 0 {
|
||||
return *listing.DistributionSiteID, nil
|
||||
}
|
||||
if listing.ID == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
var relation model.DistributionSiteAccount
|
||||
// 仅匹配启用中的分销站点,真实 DB 错误直接返回,避免误回退到全局二维码池。
|
||||
err := tx.Table("distribution_site_accounts AS relation").
|
||||
Select("relation.*").
|
||||
Joins("JOIN distribution_sites AS site ON site.id = relation.site_id AND site.status = 'active'").
|
||||
Where("relation.listing_id = ? AND relation.display_status = 'enabled'", listingID).
|
||||
Where("relation.listing_id = ? AND relation.display_status = 'enabled'", listing.ID).
|
||||
Order("relation.id ASC").
|
||||
Limit(1).
|
||||
Scan(&relation).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return 0, err
|
||||
}
|
||||
if relation.ID != 0 {
|
||||
return fetchUnusedQrCodeForSite(tx, &relation.SiteID)
|
||||
return relation.SiteID, nil
|
||||
}
|
||||
return fetchUnusedQrCodeForSite(tx, nil)
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func resolveListingQrCode(tx *gorm.DB, siteID uint64) (*model.ChatQrCode, string, error) {
|
||||
if siteID > 0 {
|
||||
var site model.DistributionSite
|
||||
err := tx.Select("id", "listing_qrcode_url").First(&site, siteID).Error
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, "", err
|
||||
}
|
||||
if err == nil {
|
||||
if url := strings.TrimSpace(site.ListingQrcodeURL); url != "" {
|
||||
return nil, url, nil
|
||||
}
|
||||
}
|
||||
qrcode, err := fetchUnusedQrCodeForSite(tx, &siteID)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if qrcode != nil {
|
||||
return qrcode, qrcode.ImageURL, nil
|
||||
}
|
||||
return nil, "", nil
|
||||
}
|
||||
qrcode, err := fetchUnusedQrCodeForSite(tx, nil)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if qrcode != nil {
|
||||
return qrcode, qrcode.ImageURL, nil
|
||||
}
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
func fetchUnusedQrCodeForSite(tx *gorm.DB, siteID *uint64) (*model.ChatQrCode, error) {
|
||||
|
||||
@@ -479,3 +479,145 @@ func TestBatchCreateQrCodeDeliversPendingTask(t *testing.T) {
|
||||
t.Fatalf("未生成二维码图片消息: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureListingConversationUsesSiteFixedQrcode(t *testing.T) {
|
||||
db := setupQrCodeTestDB(t)
|
||||
site := model.DistributionSite{
|
||||
SiteCode: "fx_qr_fixed",
|
||||
SiteName: "固定码分站",
|
||||
Domain: "fixed-qr.example.com",
|
||||
Status: "active",
|
||||
ListingQrcodeURL: "/api/files/object?key=qrcode/site-fixed.png",
|
||||
}
|
||||
if err := db.Create(&site).Error; err != nil {
|
||||
t.Fatalf("创建分销站点失败: %v", err)
|
||||
}
|
||||
siteID := site.ID
|
||||
pool := []model.ChatQrCode{
|
||||
{ImageURL: "/api/files/object?key=qrcode/main-pool.png", Status: QrCodeStatusUnused},
|
||||
{ImageURL: "/api/files/object?key=qrcode/site-pool.png", Status: QrCodeStatusUnused, DistributionSiteID: &siteID},
|
||||
}
|
||||
if err := db.Create(&pool).Error; err != nil {
|
||||
t.Fatalf("创建码池失败: %v", err)
|
||||
}
|
||||
|
||||
listing := model.RentalListing{
|
||||
ID: 202,
|
||||
ListingNo: "L202609150001",
|
||||
OwnerID: 10,
|
||||
DistributionSiteID: &siteID,
|
||||
}
|
||||
var conv *model.ChatConversation
|
||||
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||
created, err := EnsureListingConversation(tx, listing, 0)
|
||||
conv = created
|
||||
return err
|
||||
}); err != nil {
|
||||
t.Fatalf("建发布群失败: %v", err)
|
||||
}
|
||||
if conv == nil || conv.DistributionSiteID == nil || *conv.DistributionSiteID != siteID {
|
||||
t.Fatalf("发布群站点 = %v, want %d", conv.DistributionSiteID, siteID)
|
||||
}
|
||||
|
||||
var message model.ChatMessage
|
||||
if err := db.Where("conversation_id = ? AND content_type = ?", conv.ID, "image").First(&message).Error; err != nil {
|
||||
t.Fatalf("未发送固定二维码: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(message.AttachmentURLS), "site-fixed.png") {
|
||||
t.Fatalf("二维码附件 = %s, want 分站固定码", message.AttachmentURLS)
|
||||
}
|
||||
|
||||
var unusedCount int64
|
||||
if err := db.Model(&model.ChatQrCode{}).Where("status = ?", QrCodeStatusUnused).Count(&unusedCount).Error; err != nil {
|
||||
t.Fatalf("统计码池失败: %v", err)
|
||||
}
|
||||
if unusedCount != 2 {
|
||||
t.Fatalf("固定码仍应保留码池库存, unused = %d, want 2", unusedCount)
|
||||
}
|
||||
|
||||
var taskCount int64
|
||||
if err := db.Model(&model.ChatQrCodeDeliveryTask{}).Count(&taskCount).Error; err != nil {
|
||||
t.Fatalf("统计补发任务失败: %v", err)
|
||||
}
|
||||
if taskCount != 0 {
|
||||
t.Fatalf("固定码场景不应创建补发任务, got %d", taskCount)
|
||||
}
|
||||
|
||||
listing2 := model.RentalListing{
|
||||
ID: 203,
|
||||
ListingNo: "L202609150002",
|
||||
OwnerID: 11,
|
||||
DistributionSiteID: &siteID,
|
||||
}
|
||||
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||
_, err := EnsureListingConversation(tx, listing2, 0)
|
||||
return err
|
||||
}); err != nil {
|
||||
t.Fatalf("第二次建发布群失败: %v", err)
|
||||
}
|
||||
if err := db.Model(&model.ChatQrCode{}).Where("status = ?", QrCodeStatusUnused).Count(&unusedCount).Error; err != nil {
|
||||
t.Fatalf("再次统计码池失败: %v", err)
|
||||
}
|
||||
if unusedCount != 2 {
|
||||
t.Fatalf("同一分站固定码应可复用, unused = %d, want 2", unusedCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureListingConversationSiteWithoutFixedQrUsesSitePool(t *testing.T) {
|
||||
db := setupQrCodeTestDB(t)
|
||||
site := model.DistributionSite{
|
||||
SiteCode: "fx_qr_pool",
|
||||
SiteName: "码池分站",
|
||||
Domain: "pool-qr.example.com",
|
||||
Status: "active",
|
||||
}
|
||||
if err := db.Create(&site).Error; err != nil {
|
||||
t.Fatalf("创建分销站点失败: %v", err)
|
||||
}
|
||||
siteID := site.ID
|
||||
pool := []model.ChatQrCode{
|
||||
{ImageURL: "/api/files/object?key=qrcode/main-pool.png", Status: QrCodeStatusUnused},
|
||||
{ImageURL: "/api/files/object?key=qrcode/site-pool.png", Status: QrCodeStatusUnused, DistributionSiteID: &siteID},
|
||||
}
|
||||
if err := db.Create(&pool).Error; err != nil {
|
||||
t.Fatalf("创建码池失败: %v", err)
|
||||
}
|
||||
|
||||
listing := model.RentalListing{
|
||||
ID: 204,
|
||||
ListingNo: "L202609150003",
|
||||
OwnerID: 12,
|
||||
DistributionSiteID: &siteID,
|
||||
}
|
||||
var conv *model.ChatConversation
|
||||
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||
created, err := EnsureListingConversation(tx, listing, 0)
|
||||
conv = created
|
||||
return err
|
||||
}); err != nil {
|
||||
t.Fatalf("建发布群失败: %v", err)
|
||||
}
|
||||
|
||||
var message model.ChatMessage
|
||||
if err := db.Where("conversation_id = ? AND content_type = ?", conv.ID, "image").First(&message).Error; err != nil {
|
||||
t.Fatalf("未发送分站池二维码: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(message.AttachmentURLS), "site-pool.png") {
|
||||
t.Fatalf("二维码附件 = %s, want 分站码池", message.AttachmentURLS)
|
||||
}
|
||||
|
||||
var siteQR model.ChatQrCode
|
||||
if err := db.First(&siteQR, pool[1].ID).Error; err != nil {
|
||||
t.Fatalf("查询分站码失败: %v", err)
|
||||
}
|
||||
if siteQR.Status != QrCodeStatusUsed {
|
||||
t.Fatalf("未配置固定码时应消耗分站码池, status = %q", siteQR.Status)
|
||||
}
|
||||
var mainQR model.ChatQrCode
|
||||
if err := db.First(&mainQR, pool[0].ID).Error; err != nil {
|
||||
t.Fatalf("查询主站码失败: %v", err)
|
||||
}
|
||||
if mainQR.Status != QrCodeStatusUnused {
|
||||
t.Fatalf("分站上架不得消耗主站码池, status = %q", mainQR.Status)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user