feat(admin): establish core data model

This commit is contained in:
QiuSW
2026-08-03 18:37:48 +08:00
parent 8be6d206dd
commit 87591e84f9
17 changed files with 1081 additions and 6 deletions
+80
View File
@@ -0,0 +1,80 @@
package domain
import (
"errors"
"time"
)
var ErrInvalidAuthorizationTransition = errors.New("invalid authorization status transition")
type AuthorizationStatus string
const (
AuthorizationStatusPendingDelivery AuthorizationStatus = "PENDING_DELIVERY"
AuthorizationStatusDelivered AuthorizationStatus = "DELIVERED"
AuthorizationStatusAcknowledged AuthorizationStatus = "ACKNOWLEDGED"
AuthorizationStatusExecuting AuthorizationStatus = "EXECUTING"
AuthorizationStatusFenced AuthorizationStatus = "FENCED"
AuthorizationStatusConsumed AuthorizationStatus = "CONSUMED"
AuthorizationStatusSuperseded AuthorizationStatus = "SUPERSEDED"
AuthorizationStatusExpired AuthorizationStatus = "EXPIRED"
)
type OrderAuthorization struct {
ID string
TaskID string
SpecTrialID string
Version int
GoodsID string
SKUColor string
SKUSize string
Quantity int
AuthorizedUnitPrice string
TotalPriceCap string
Note *string
Status AuthorizationStatus
CreatedBy string
CreatedAt time.Time
ExpiresAt time.Time
}
// CanTransitionTo 围栏后的授权只能消费,不能回到可领取或可过期状态,以防重复采购。
func (status AuthorizationStatus) CanTransitionTo(next AuthorizationStatus) bool {
_, allowed := authorizationTransitions[status][next]
return allowed
}
// TransitionAuthorization 返回下一状态;未定义的授权状态转移一律失败。
func TransitionAuthorization(current, next AuthorizationStatus) (AuthorizationStatus, error) {
if !current.CanTransitionTo(next) {
return current, ErrInvalidAuthorizationTransition
}
return next, nil
}
var authorizationTransitions = map[AuthorizationStatus]map[AuthorizationStatus]struct{}{
AuthorizationStatusPendingDelivery: {
AuthorizationStatusDelivered: {},
AuthorizationStatusSuperseded: {},
AuthorizationStatusExpired: {},
},
AuthorizationStatusDelivered: {
AuthorizationStatusAcknowledged: {},
AuthorizationStatusSuperseded: {},
AuthorizationStatusExpired: {},
},
AuthorizationStatusAcknowledged: {
AuthorizationStatusExecuting: {},
AuthorizationStatusSuperseded: {},
AuthorizationStatusExpired: {},
},
AuthorizationStatusExecuting: {
AuthorizationStatusFenced: {},
AuthorizationStatusSuperseded: {},
AuthorizationStatusExpired: {},
},
AuthorizationStatusFenced: {
AuthorizationStatusConsumed: {},
},
}
@@ -0,0 +1,54 @@
package domain_test
import (
"errors"
"testing"
"cmbuyer/admin/internal/domain"
)
func TestAuthorizationTransitions(t *testing.T) {
tests := []struct {
name string
current domain.AuthorizationStatus
next domain.AuthorizationStatus
allowed bool
}{
{"deliver", domain.AuthorizationStatusPendingDelivery, domain.AuthorizationStatusDelivered, true},
{"acknowledge", domain.AuthorizationStatusDelivered, domain.AuthorizationStatusAcknowledged, true},
{"execute", domain.AuthorizationStatusAcknowledged, domain.AuthorizationStatusExecuting, true},
{"fence", domain.AuthorizationStatusExecuting, domain.AuthorizationStatusFenced, true},
{"consume fenced authorization", domain.AuthorizationStatusFenced, domain.AuthorizationStatusConsumed, true},
{"expire pending delivery", domain.AuthorizationStatusPendingDelivery, domain.AuthorizationStatusExpired, true},
{"supersede pending delivery", domain.AuthorizationStatusPendingDelivery, domain.AuthorizationStatusSuperseded, true},
{"expire before fence", domain.AuthorizationStatusExecuting, domain.AuthorizationStatusExpired, true},
{"supersede before fence", domain.AuthorizationStatusDelivered, domain.AuthorizationStatusSuperseded, true},
{"fenced authorization cannot expire", domain.AuthorizationStatusFenced, domain.AuthorizationStatusExpired, false},
{"fenced authorization cannot be superseded", domain.AuthorizationStatusFenced, domain.AuthorizationStatusSuperseded, false},
{"fenced authorization cannot be delivered again", domain.AuthorizationStatusFenced, domain.AuthorizationStatusDelivered, false},
{"consumed authorization cannot restart", domain.AuthorizationStatusConsumed, domain.AuthorizationStatusDelivered, false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := test.current.CanTransitionTo(test.next); got != test.allowed {
t.Fatalf("CanTransitionTo(%s, %s) = %t, want %t", test.current, test.next, got, test.allowed)
}
result, err := domain.TransitionAuthorization(test.current, test.next)
if test.allowed {
if err != nil {
t.Fatalf("TransitionAuthorization(%s, %s): %v", test.current, test.next, err)
}
if result != test.next {
t.Fatalf("TransitionAuthorization(%s, %s) = %s, want %s", test.current, test.next, result, test.next)
}
return
}
if !errors.Is(err, domain.ErrInvalidAuthorizationTransition) {
t.Fatalf("TransitionAuthorization(%s, %s) error = %v, want ErrInvalidAuthorizationTransition", test.current, test.next, err)
}
})
}
}
+16
View File
@@ -0,0 +1,16 @@
package domain
import "time"
type SpecTrial struct {
ID string
TaskID string
Attempt int
ProductTitle string
SelectedColor string
SelectedSize string
UnitPrice string
TotalPrice string
EvidenceSHA256 string
CreatedAt time.Time
}
+56
View File
@@ -0,0 +1,56 @@
package domain
import (
"errors"
"time"
)
var ErrInvalidSubmissionTransition = errors.New("invalid submission status transition")
type SubmissionStatus string
const (
SubmissionStatusFenced SubmissionStatus = "FENCED"
SubmissionStatusSubmitted SubmissionStatus = "SUBMITTED"
SubmissionStatusReconciliationRequired SubmissionStatus = "RECONCILIATION_REQUIRED"
SubmissionStatusManualResolved SubmissionStatus = "MANUAL_RESOLVED"
)
type OrderSubmission struct {
ID string
TaskID string
AuthorizationID string
CommandID string
DryRunID string
Status SubmissionStatus
VerifiedUnitPrice string
QuantityRead int
ConfirmPageAmount string
CreatedAt time.Time
ResolvedAt *time.Time
}
// CanTransitionTo 只允许围栏记录向最终观察结果调和,拒绝回退以防触发第二次真实动作。
func (status SubmissionStatus) CanTransitionTo(next SubmissionStatus) bool {
_, allowed := submissionTransitions[status][next]
return allowed
}
// TransitionSubmission 返回下一状态;未定义的提交记录状态转移一律失败。
func TransitionSubmission(current, next SubmissionStatus) (SubmissionStatus, error) {
if !current.CanTransitionTo(next) {
return current, ErrInvalidSubmissionTransition
}
return next, nil
}
var submissionTransitions = map[SubmissionStatus]map[SubmissionStatus]struct{}{
SubmissionStatusFenced: {
SubmissionStatusSubmitted: {},
SubmissionStatusReconciliationRequired: {},
},
SubmissionStatusReconciliationRequired: {
SubmissionStatusManualResolved: {},
},
}
+47
View File
@@ -0,0 +1,47 @@
package domain_test
import (
"errors"
"testing"
"cmbuyer/admin/internal/domain"
)
func TestSubmissionTransitions(t *testing.T) {
tests := []struct {
name string
current domain.SubmissionStatus
next domain.SubmissionStatus
allowed bool
}{
{"submitted", domain.SubmissionStatusFenced, domain.SubmissionStatusSubmitted, true},
{"uncertain requires reconciliation", domain.SubmissionStatusFenced, domain.SubmissionStatusReconciliationRequired, true},
{"manual review resolves reconciliation", domain.SubmissionStatusReconciliationRequired, domain.SubmissionStatusManualResolved, true},
{"cannot reopen fenced submission", domain.SubmissionStatusSubmitted, domain.SubmissionStatusFenced, false},
{"submitted cannot require reconciliation", domain.SubmissionStatusSubmitted, domain.SubmissionStatusReconciliationRequired, false},
{"cannot skip reconciliation", domain.SubmissionStatusFenced, domain.SubmissionStatusManualResolved, false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := test.current.CanTransitionTo(test.next); got != test.allowed {
t.Fatalf("CanTransitionTo(%s, %s) = %t, want %t", test.current, test.next, got, test.allowed)
}
result, err := domain.TransitionSubmission(test.current, test.next)
if test.allowed {
if err != nil {
t.Fatalf("TransitionSubmission(%s, %s): %v", test.current, test.next, err)
}
if result != test.next {
t.Fatalf("TransitionSubmission(%s, %s) = %s, want %s", test.current, test.next, result, test.next)
}
return
}
if !errors.Is(err, domain.ErrInvalidSubmissionTransition) {
t.Fatalf("TransitionSubmission(%s, %s) error = %v, want ErrInvalidSubmissionTransition", test.current, test.next, err)
}
})
}
}
+102
View File
@@ -0,0 +1,102 @@
// Package domain 定义采购服务的业务实体与不依赖外部系统的状态规则。
package domain
import (
"errors"
"time"
)
var ErrInvalidTaskTransition = errors.New("invalid task status transition")
type TaskStatus string
const (
TaskStatusDraft TaskStatus = "DRAFT"
TaskStatusPending TaskStatus = "PENDING"
TaskStatusClaimed TaskStatus = "CLAIMED"
TaskStatusRunning TaskStatus = "RUNNING"
TaskStatusWaitingConfirmation TaskStatus = "WAITING_CONFIRMATION"
TaskStatusPendingRetrial TaskStatus = "PENDING_RETRIAL"
TaskStatusAuthorized TaskStatus = "AUTHORIZED"
TaskStatusOrdering TaskStatus = "ORDERING"
TaskStatusWaitingPayment TaskStatus = "WAITING_PAYMENT"
TaskStatusReconciliationRequired TaskStatus = "RECONCILIATION_REQUIRED"
TaskStatusNeedsManual TaskStatus = "NEEDS_MANUAL"
TaskStatusSucceeded TaskStatus = "SUCCEEDED"
TaskStatusCanceled TaskStatus = "CANCELED"
)
type Source string
const (
SourceManual Source = "MANUAL"
SourceExcel Source = "EXCEL"
SourceERP Source = "ERP"
)
type Task struct {
ID string
Source Source
SourceRef *string
Title string
GoodsID string
SKUColor string
SKUSize string
Quantity int
MaxTotalPrice string
ReferenceAssetID *string
Status TaskStatus
Version int
CreatedAt time.Time
UpdatedAt time.Time
}
// CanTransitionTo 只接受架构已定义的任务状态转移,未知状态或未列出的动作均拒绝。
func (status TaskStatus) CanTransitionTo(next TaskStatus) bool {
_, allowed := taskTransitions[status][next]
return allowed
}
// TransitionTask 返回下一状态;非法转移必须在写库前失败,不能由调用方猜测补救路径。
func TransitionTask(current, next TaskStatus) (TaskStatus, error) {
if !current.CanTransitionTo(next) {
return current, ErrInvalidTaskTransition
}
return next, nil
}
var taskTransitions = map[TaskStatus]map[TaskStatus]struct{}{
TaskStatusDraft: {
TaskStatusPending: {},
},
TaskStatusPending: {
TaskStatusClaimed: {},
},
TaskStatusPendingRetrial: {
TaskStatusClaimed: {},
},
TaskStatusClaimed: {
TaskStatusRunning: {},
TaskStatusPending: {},
},
TaskStatusRunning: {
TaskStatusWaitingConfirmation: {},
TaskStatusNeedsManual: {},
},
TaskStatusWaitingConfirmation: {
TaskStatusCanceled: {},
TaskStatusAuthorized: {},
},
TaskStatusAuthorized: {
TaskStatusOrdering: {},
},
TaskStatusOrdering: {
TaskStatusNeedsManual: {},
TaskStatusWaitingPayment: {},
TaskStatusReconciliationRequired: {},
},
TaskStatusWaitingPayment: {
TaskStatusSucceeded: {},
},
}
+62
View File
@@ -0,0 +1,62 @@
package domain_test
import (
"errors"
"testing"
"cmbuyer/admin/internal/domain"
)
func TestTaskTransitions(t *testing.T) {
tests := []struct {
name string
current domain.TaskStatus
next domain.TaskStatus
allowed bool
}{
{"start trial", domain.TaskStatusDraft, domain.TaskStatusPending, true},
{"claim trial", domain.TaskStatusPending, domain.TaskStatusClaimed, true},
{"claim retrial", domain.TaskStatusPendingRetrial, domain.TaskStatusClaimed, true},
{"start trial execution", domain.TaskStatusClaimed, domain.TaskStatusRunning, true},
{"release unstarted claim", domain.TaskStatusClaimed, domain.TaskStatusPending, true},
{"trial completes", domain.TaskStatusRunning, domain.TaskStatusWaitingConfirmation, true},
{"trial needs manual review", domain.TaskStatusRunning, domain.TaskStatusNeedsManual, true},
{"authorize confirmed trial", domain.TaskStatusWaitingConfirmation, domain.TaskStatusAuthorized, true},
{"reject confirmed trial", domain.TaskStatusWaitingConfirmation, domain.TaskStatusCanceled, true},
{"start authorized order leg", domain.TaskStatusAuthorized, domain.TaskStatusOrdering, true},
{"order reaches payment", domain.TaskStatusOrdering, domain.TaskStatusWaitingPayment, true},
{"order needs manual review before fence", domain.TaskStatusOrdering, domain.TaskStatusNeedsManual, true},
{"order needs reconciliation", domain.TaskStatusOrdering, domain.TaskStatusReconciliationRequired, true},
{"payment verified", domain.TaskStatusWaitingPayment, domain.TaskStatusSucceeded, true},
{"cannot skip trial", domain.TaskStatusDraft, domain.TaskStatusAuthorized, false},
{"trial cannot enter order leg", domain.TaskStatusRunning, domain.TaskStatusOrdering, false},
{"terminal task cannot restart", domain.TaskStatusSucceeded, domain.TaskStatusPending, false},
{"unknown status is rejected", domain.TaskStatus("UNKNOWN"), domain.TaskStatusPending, false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := test.current.CanTransitionTo(test.next); got != test.allowed {
t.Fatalf("CanTransitionTo(%s, %s) = %t, want %t", test.current, test.next, got, test.allowed)
}
result, err := domain.TransitionTask(test.current, test.next)
if test.allowed {
if err != nil {
t.Fatalf("TransitionTask(%s, %s): %v", test.current, test.next, err)
}
if result != test.next {
t.Fatalf("TransitionTask(%s, %s) = %s, want %s", test.current, test.next, result, test.next)
}
return
}
if !errors.Is(err, domain.ErrInvalidTaskTransition) {
t.Fatalf("TransitionTask(%s, %s) error = %v, want ErrInvalidTaskTransition", test.current, test.next, err)
}
if result != test.current {
t.Fatalf("TransitionTask(%s, %s) result = %s, want unchanged status", test.current, test.next, result)
}
})
}
}