浏览代码

feat: add OrderRepository interface, MemoryRepo with CAS, and concurrent tests

b 1 月之前
父节点
当前提交
626ea45cc5
共有 3 个文件被更改,包括 217 次插入0 次删除
  1. 57 0
      repository/memory.go
  2. 151 0
      repository/memory_test.go
  3. 9 0
      repository/order.go

+ 57 - 0
repository/memory.go

@@ -0,0 +1,57 @@
+package repository
+
+import (
+	"sync"
+	"time"
+
+	"job-cheng-xing/model"
+)
+
+type MemoryRepo struct {
+	mu     sync.RWMutex
+	orders map[string]*model.Order
+}
+
+func NewMemoryRepo() *MemoryRepo {
+	return &MemoryRepo{orders: make(map[string]*model.Order)}
+}
+
+func (r *MemoryRepo) Create(order *model.Order) error {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	if _, exists := r.orders[order.ID]; exists {
+		return model.NewAppError("conflict", "订单已存在", 409)
+	}
+	clone := *order
+	r.orders[order.ID] = &clone
+	return nil
+}
+
+func (r *MemoryRepo) FindByID(id string) (*model.Order, error) {
+	r.mu.RLock()
+	defer r.mu.RUnlock()
+	order, ok := r.orders[id]
+	if !ok {
+		return nil, model.ErrNotFound
+	}
+	clone := *order
+	return &clone, nil
+}
+
+func (r *MemoryRepo) Update(id string, oldStatus model.OrderStatus, newOrder *model.Order) error {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+	current, ok := r.orders[id]
+	if !ok {
+		return model.ErrNotFound
+	}
+	if current.Status != oldStatus {
+		return model.ErrStatusConflict
+	}
+	current.Status = newOrder.Status
+	if newOrder.ProviderID != nil {
+		current.ProviderID = newOrder.ProviderID
+	}
+	current.UpdatedAt = time.Now()
+	return nil
+}

+ 151 - 0
repository/memory_test.go

@@ -0,0 +1,151 @@
+package repository
+
+import (
+	"sync"
+	"testing"
+	"time"
+
+	"job-cheng-xing/model"
+)
+
+func strPtr(s string) *string { return &s }
+
+func TestMemoryRepo_CreateAndFind(t *testing.T) {
+	repo := NewMemoryRepo()
+	order := &model.Order{
+		ID:          "ord_001",
+		Status:      model.StatusPending,
+		ServiceTime: time.Now(),
+		Duration:    120,
+		Address:     "北京朝阳",
+		CreatedAt:   time.Now(),
+		UpdatedAt:   time.Now(),
+	}
+
+	err := repo.Create(order)
+	if err != nil {
+		t.Fatalf("Create failed: %v", err)
+	}
+
+	found, err := repo.FindByID("ord_001")
+	if err != nil {
+		t.Fatalf("FindByID failed: %v", err)
+	}
+	if found.ID != "ord_001" {
+		t.Errorf("expected ord_001, got %s", found.ID)
+	}
+}
+
+func TestMemoryRepo_FindByID_NotFound(t *testing.T) {
+	repo := NewMemoryRepo()
+	_, err := repo.FindByID("nonexistent")
+	if err != model.ErrNotFound {
+		t.Errorf("expected ErrNotFound, got %v", err)
+	}
+}
+
+func TestMemoryRepo_Update_CAS_Success(t *testing.T) {
+	repo := NewMemoryRepo()
+	order := &model.Order{
+		ID:          "ord_001",
+		Status:      model.StatusPending,
+		ServiceTime: time.Now(),
+		Duration:    120,
+		Address:     "北京朝阳",
+		CreatedAt:   time.Now(),
+		UpdatedAt:   time.Now(),
+	}
+	repo.Create(order)
+
+	newOrder := &model.Order{
+		Status:     model.StatusAccepted,
+		ProviderID: strPtr("prov_001"),
+		UpdatedAt:  time.Now(),
+	}
+	err := repo.Update("ord_001", model.StatusPending, newOrder)
+	if err != nil {
+		t.Fatalf("Update failed: %v", err)
+	}
+
+	found, _ := repo.FindByID("ord_001")
+	if found.Status != model.StatusAccepted {
+		t.Errorf("expected accepted, got %s", found.Status)
+	}
+	if *found.ProviderID != "prov_001" {
+		t.Errorf("expected prov_001, got %s", *found.ProviderID)
+	}
+}
+
+func TestMemoryRepo_Update_CAS_Fail_WrongStatus(t *testing.T) {
+	repo := NewMemoryRepo()
+	order := &model.Order{
+		ID:     "ord_001",
+		Status: model.StatusPending,
+	}
+	repo.Create(order)
+
+	newOrder := &model.Order{Status: model.StatusCanceled}
+	err := repo.Update("ord_001", model.StatusAccepted, newOrder)
+	if err != model.ErrStatusConflict {
+		t.Errorf("expected ErrStatusConflict, got %v", err)
+	}
+}
+
+func TestMemoryRepo_ConcurrentAccept(t *testing.T) {
+	repo := NewMemoryRepo()
+	order := &model.Order{
+		ID:          "ord_001",
+		Status:      model.StatusPending,
+		ServiceTime: time.Now(),
+		Duration:    120,
+		Address:     "北京朝阳",
+		CreatedAt:   time.Now(),
+		UpdatedAt:   time.Now(),
+	}
+	repo.Create(order)
+
+	const numGoroutines = 50
+	var wg sync.WaitGroup
+	results := make(chan error, numGoroutines)
+
+	for i := 0; i < numGoroutines; i++ {
+		wg.Add(1)
+		go func(idx int) {
+			defer wg.Done()
+			pid := "prov_" + string(rune('a'+idx%26)) + string(rune('0'+idx/26))
+			newOrder := &model.Order{
+				Status:     model.StatusAccepted,
+				ProviderID: &pid,
+				UpdatedAt:  time.Now(),
+			}
+			results <- repo.Update("ord_001", model.StatusPending, newOrder)
+		}(i)
+	}
+	wg.Wait()
+	close(results)
+
+	successCount := 0
+	failCount := 0
+	for err := range results {
+		if err == nil {
+			successCount++
+		} else if err == model.ErrStatusConflict {
+			failCount++
+		} else {
+			t.Errorf("unexpected error: %v", err)
+		}
+	}
+
+	if successCount != 1 {
+		t.Errorf("expected exactly 1 success, got %d", successCount)
+	}
+	if failCount != numGoroutines-1 {
+		t.Errorf("expected %d failures, got %d", numGoroutines-1, failCount)
+	}
+
+	found, _ := repo.FindByID("ord_001")
+	if found.ProviderID == nil {
+		t.Fatal("expected ProviderID to be set")
+	}
+	t.Logf("winner provider: %s", *found.ProviderID)
+}

+ 9 - 0
repository/order.go

@@ -0,0 +1,9 @@
+package repository
+
+import "job-cheng-xing/model"
+
+type OrderRepository interface {
+	Create(order *model.Order) error
+	FindByID(id string) (*model.Order, error)
+	Update(id string, oldStatus model.OrderStatus, newOrder *model.Order) error
+}