From 3516fb7edc347d73a0e61fc7969abde9ab8a80fd Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Thu, 24 Sep 2026 11:48:33 +0800 Subject: [PATCH] feat(db): add return_match table for #338 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New model models.ReturnMatch backing the SYB↔yeeke return matching feature: matched/confirmed/cancelled status, nullable ActiveSYBProductID/ActiveYeekeReturnItemID columns (same pattern as YeekeSyncRun.ActiveSlot) each carrying a unique index so only one active match can occupy either side at a time; cancel clears both to free the slot for a rematch. Registered in migrations.MigratedModels(). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F --- server/app/goauto/migrations/migrate.go | 1 + server/app/goauto/models/return_match.go | 71 ++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 server/app/goauto/models/return_match.go diff --git a/server/app/goauto/migrations/migrate.go b/server/app/goauto/migrations/migrate.go index e9aeb32..e3fdde8 100644 --- a/server/app/goauto/migrations/migrate.go +++ b/server/app/goauto/migrations/migrate.go @@ -79,6 +79,7 @@ func MigratedModels() []any { &models.PDDProductReplacement{}, &models.PDDProductReplacementItem{}, &models.PDDProductReplacementWorkerLease{}, + &models.ReturnMatch{}, } } diff --git a/server/app/goauto/models/return_match.go b/server/app/goauto/models/return_match.go new file mode 100644 index 0000000..baab66c --- /dev/null +++ b/server/app/goauto/models/return_match.go @@ -0,0 +1,71 @@ +package models + +import "time" + +// #338: ReturnMatch links one syb_product row to one yeeke_return_item row so +// the return can be reused instead of buying the item again on PDD. A match +// is "active" (still blocking purchase creation and occupying the return) +// while Status is matched or confirmed; cancelling clears both Active* +// columns so the unique index frees up both sides for a future rematch +// (issue #338, business rule: 取消匹配后再次点击「匹配退货」若配回同一对,允许). +// +// ActiveSYBProductID/ActiveYeekeReturnItemID follow the same nullable-unique- +// column pattern already used by YeekeSyncRun.ActiveSlot in this codebase: +// MySQL unique indexes allow any number of NULL rows, so only the currently +// active match for a given side occupies the slot, and a concurrent second +// insert for the same side fails the unique constraint instead of double- +// booking it. +const ( + ReturnMatchStatusMatched = "matched" + ReturnMatchStatusConfirmed = "confirmed" + ReturnMatchStatusCancelled = "cancelled" +) + +type ReturnMatch struct { + ID uint64 `json:"id" gorm:"primaryKey;autoIncrement"` + + SYBProductID uint64 `json:"sybProductId" gorm:"not null;index"` + YeekeReturnItemID uint64 `json:"yeekeReturnItemId" gorm:"not null;index"` + + // ActiveSYBProductID/ActiveYeekeReturnItemID mirror the ID columns above + // while Status is matched/confirmed, and are cleared to NULL on cancel. + ActiveSYBProductID *uint64 `json:"-" gorm:"uniqueIndex:ux_return_match_active_syb"` + ActiveYeekeReturnItemID *uint64 `json:"-" gorm:"uniqueIndex:ux_return_match_active_return"` + + Status string `json:"status" gorm:"size:16;not null;index;check:ck_return_match_status,status IN ('matched','confirmed','cancelled')"` + + // Snapshots of the normalized spec text on both sides at match time, kept + // for audit and for the compare screen even if the source rows change + // later. + SYBSpecText string `json:"sybSpecText" gorm:"size:255;not null;default:''"` + YeekeSpecText string `json:"yeekeSpecText" gorm:"size:255;not null;default:''"` + NormalizedKey string `json:"normalizedKey" gorm:"size:255;not null;default:'';index"` + + // PreviousProcessStage records what the SYB product's computed process + // stage was immediately before this match, so cancel can report/restore + // context; the actual restore is a pure recomputation (no stage is + // stored on syb_product), this is for audit/display only. + PreviousProcessStage string `json:"previousProcessStage" gorm:"size:32;not null;default:''"` + + DestroyDeadlineSnapshot *time.Time `json:"destroyDeadlineSnapshot,omitempty"` + + Remark string `json:"remark" gorm:"size:500;not null;default:''"` + + MatchedBy string `json:"matchedBy" gorm:"size:64;not null;default:''"` + MatchedAt time.Time `json:"matchedAt"` + ConfirmedBy string `json:"confirmedBy,omitempty" gorm:"size:64;not null;default:''"` + ConfirmedAt *time.Time `json:"confirmedAt,omitempty"` + CancelledBy string `json:"cancelledBy,omitempty" gorm:"size:64;not null;default:''"` + CancelledAt *time.Time `json:"cancelledAt,omitempty"` + + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +func (ReturnMatch) TableName() string { return "return_match" } + +// IsActive reports whether the match currently blocks purchase creation and +// occupies its return item. +func (m ReturnMatch) IsActive() bool { + return m.Status == ReturnMatchStatusMatched || m.Status == ReturnMatchStatusConfirmed +}