H-28: harden Shangwutong CID sync (#5)

* fix(shangwutong): close contact sync review gaps

* fix(shangwutong): harden CID sync boundaries

---------

Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
Rogee
2026-08-13 01:26:56 +08:00
committed by GitHub
co-authored by rogee
parent 0e9bf8dc14
commit d0995798f4
7 changed files with 133 additions and 41 deletions
@@ -37,7 +37,12 @@ func (h *ShangwutongConnectorHandler) UpdateContactMetadata(c *gin.Context) {
return
}
var request shangwutongContactMetadataRequest
if err := c.ShouldBindJSON(&request); err != nil || strings.TrimSpace(request.CID) == "" {
if err := c.ShouldBindJSON(&request); err != nil {
h.connectorError(c, http.StatusUnprocessableEntity, "invalid_contact_metadata", "cid is required", false)
return
}
request.CID = strings.TrimSpace(request.CID)
if request.CID == "" || len(request.CID) > 255 {
h.connectorError(c, http.StatusUnprocessableEntity, "invalid_contact_metadata", "cid is required", false)
return
}
@@ -48,13 +53,16 @@ func (h *ShangwutongConnectorHandler) UpdateContactMetadata(c *gin.Context) {
}
metadata := map[string]any{}
if len(contactInbox.ChannelMetadata) > 0 {
_ = json.Unmarshal(contactInbox.ChannelMetadata, &metadata)
if err := json.Unmarshal(contactInbox.ChannelMetadata, &metadata); err != nil {
h.connectorError(c, http.StatusInternalServerError, "contact_metadata_invalid", "stored contact metadata is invalid", true)
return
}
}
if metadata["cid"] == strings.TrimSpace(request.CID) {
if metadata["cid"] == request.CID {
c.JSON(http.StatusOK, gin.H{"updated": false})
return
}
metadata["cid"] = strings.TrimSpace(request.CID)
metadata["cid"] = request.CID
encoded, err := json.Marshal(metadata)
if err != nil {
h.connectorError(c, http.StatusInternalServerError, "contact_metadata_update_failed", "failed to encode contact metadata", true)
@@ -0,0 +1,58 @@
package service
import (
"context"
"encoding/json"
"testing"
"github.com/gochat/gochat/internal/channel"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/repository"
"github.com/gochat/gochat/internal/worker"
"github.com/stretchr/testify/require"
"gorm.io/datatypes"
)
func TestContactUpdateQueuesDurableShangwutongEvent(t *testing.T) {
db := setupServiceTestDB(t)
account := createTestAccount(t, db)
contact := &model.Contact{AccountID: account.ID, Name: "旧昵称"}
require.NoError(t, db.Create(contact).Error)
svc := NewContactService(repository.NewContactRepo(db), nil, repository.NewNoteRepo(db))
svc.SetWorkerPool(worker.NewWorkerPool(db))
_, err := svc.Update(context.Background(), account.ID, contact.ID, UpdateContactRequest{Name: "新昵称"})
require.NoError(t, err)
var job model.BackgroundJob
require.NoError(t, db.Where("job_type = ?", channel.TaskTypeEventDispatch).First(&job).Error)
var event channel.ChannelEvent
require.NoError(t, json.Unmarshal(job.Payload, &event))
require.Equal(t, channel.EventContactUpdated, event.Type)
require.Equal(t, contact.ID, event.ContactID)
}
func TestShangwutongContactListenerQueuesOnlyCIDBoundInboxes(t *testing.T) {
db := setupServiceTestDB(t)
account := createTestAccount(t, db)
contact := &model.Contact{AccountID: account.ID, Name: "昵称"}
require.NoError(t, db.Create(contact).Error)
swtInbox := &model.Inbox{AccountID: account.ID, Name: "SWT", ChannelType: "shangwutong", Enabled: true}
webInbox := &model.Inbox{AccountID: account.ID, Name: "Widget", ChannelType: "web_widget", Enabled: true}
require.NoError(t, db.Create(swtInbox).Error)
require.NoError(t, db.Create(webInbox).Error)
require.NoError(t, db.Create(&model.ContactInbox{ContactID: contact.ID, InboxID: swtInbox.ID, SourceID: "sid", ChannelMetadata: datatypes.JSON(`{"cid":"cid-1"}`)}).Error)
require.NoError(t, db.Create(&model.ContactInbox{ContactID: contact.ID, InboxID: webInbox.ID, SourceID: "widget"}).Error)
wp := worker.NewWorkerPool(db)
listener := NewShangwutongContactListener(db, wp)
require.NoError(t, listener.OnEvent(context.Background(), &channel.ChannelEvent{
Type: channel.EventContactUpdated, AccountID: account.ID, ContactID: contact.ID,
}))
var jobs []model.BackgroundJob
require.NoError(t, db.Where("job_type = ?", TaskTypeShangwutongWebhookDelivery).Find(&jobs).Error)
require.Len(t, jobs, 1)
var job shangwutongWebhookDeliveryJob
require.NoError(t, json.Unmarshal(jobs[0].Payload, &job))
require.Equal(t, "sid", job.SourceID)
require.Equal(t, "cid-1", job.CID)
}
@@ -8,7 +8,6 @@ import (
"encoding/json"
"errors"
"fmt"
"reflect"
"strconv"
"strings"
"time"
@@ -207,7 +206,6 @@ type PublicContactRequest struct {
PhoneNumber string
CustomAttributes map[string]any
AdditionalAttributes map[string]any
ChannelMetadata map[string]any
}
type PublicContactResponse struct {
@@ -780,11 +778,6 @@ func (s *WidgetService) PublicCreateContact(ctx context.Context, inboxIdentifier
if err != nil {
return nil, err
}
if setPublicContactInboxChannelMetadata(existingInbox, req.ChannelMetadata) {
if err := s.contactInboxRepo.Update(ctx, existingInbox); err != nil {
return nil, err
}
}
existingInbox.Contact = *contact
return &PublicContactResponse{ContactInbox: existingInbox, Contact: contact}, nil
}
@@ -808,7 +801,6 @@ func (s *WidgetService) PublicCreateContact(ctx context.Context, inboxIdentifier
HMACToken: hmacToken,
HMACVerified: req.IdentifierHash != "",
}
setPublicContactInboxChannelMetadata(contactInbox, req.ChannelMetadata)
if err := s.contactInboxRepo.Create(ctx, contactInbox); err != nil {
return nil, err
}
@@ -843,11 +835,6 @@ func (s *WidgetService) PublicUpdateContact(ctx context.Context, inboxIdentifier
if err != nil {
return nil, err
}
if setPublicContactInboxChannelMetadata(contactInbox, req.ChannelMetadata) {
if err := s.contactInboxRepo.Update(ctx, contactInbox); err != nil {
return nil, err
}
}
if req.IdentifierHash != "" && !contactInbox.HMACVerified {
contactInbox.HMACVerified = true
if err := s.contactInboxRepo.Update(ctx, contactInbox); err != nil {
@@ -857,26 +844,6 @@ func (s *WidgetService) PublicUpdateContact(ctx context.Context, inboxIdentifier
return &PublicContactResponse{ContactInbox: contactInbox, Contact: contact}, nil
}
func setPublicContactInboxChannelMetadata(contactInbox *model.ContactInbox, metadata map[string]any) bool {
if contactInbox == nil || len(metadata) == 0 {
return false
}
current := jsonMap(contactInbox.ChannelMetadata)
changed := false
for key, value := range metadata {
if key == "cid" {
continue
}
if !reflect.DeepEqual(current[key], value) {
current[key], changed = value, true
}
}
if changed {
contactInbox.ChannelMetadata = mustJSON(current)
}
return changed
}
func (s *WidgetService) PublicListConversations(ctx context.Context, inboxIdentifier, sourceID string) ([]model.Conversation, error) {
_, contactInbox, err := s.resolvePublicContactInbox(ctx, inboxIdentifier, sourceID)
if err != nil {