feat(db): add return_match table for #338
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
This commit is contained in:
@@ -79,6 +79,7 @@ func MigratedModels() []any {
|
||||
&models.PDDProductReplacement{},
|
||||
&models.PDDProductReplacementItem{},
|
||||
&models.PDDProductReplacementWorkerLease{},
|
||||
&models.ReturnMatch{},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user