feat(t224): add incremental freight sync

This commit is contained in:
QiuSW
2026-07-29 00:26:05 +08:00
parent 433db45100
commit c69879650e
35 changed files with 2085 additions and 156 deletions
@@ -1,7 +1,10 @@
package usecase
import (
"context"
"errors"
"testing"
"time"
"cmroubao/backend-api/internal/domain"
)
@@ -52,6 +55,125 @@ func TestFreightNormalizationRejectsConflictingIdentityAndInvalidTime(
}
}
func TestFreightDateQuerySplitsIntoSevenDayWindows(t *testing.T) {
source := &recordingDateSource{}
service := &FreightService{source: source}
result, err := service.queryCreatedRange(
context.Background(),
domain.FreightSyncRun{
Mode: domain.FreightSyncCreatedRange,
CreatedFrom: "2026-07-01",
CreatedTo: "2026-07-15",
},
)
if err != nil {
t.Fatalf("queryCreatedRange() error = %v", err)
}
want := [][2]string{
{"2026-07-01", "2026-07-07"},
{"2026-07-08", "2026-07-14"},
{"2026-07-15", "2026-07-15"},
}
if len(source.calls) != len(want) {
t.Fatalf("calls = %#v", source.calls)
}
for index := range want {
if source.calls[index] != want[index] {
t.Fatalf("call %d = %#v, want %#v", index, source.calls[index], want[index])
}
}
if result.Query.CreatedFrom == nil ||
*result.Query.CreatedFrom != "2026-07-01" ||
result.Query.CreatedTo == nil ||
*result.Query.CreatedTo != "2026-07-15" {
t.Fatalf("merged query = %+v", result.Query)
}
}
func TestFreightDateQueryStopsOnMiddleWindowFailure(t *testing.T) {
source := &recordingDateSource{failOnCall: 2}
service := &FreightService{source: source}
_, err := service.queryCreatedRange(
context.Background(),
domain.FreightSyncRun{
Mode: domain.FreightSyncCreatedRange,
CreatedFrom: "2026-07-01",
CreatedTo: "2026-07-15",
},
)
if !errors.Is(err, errDateSourceFailure) || len(source.calls) != 2 {
t.Fatalf("queryCreatedRange() error/calls = %v / %#v", err, source.calls)
}
}
func TestCreateFreightSyncToNowUsesWatermarkOverlapDay(t *testing.T) {
watermarkTime := time.Date(2026, 7, 25, 16, 5, 0, 0, time.UTC)
repository := &dateCaptureRepository{
watermark: &domain.FreightSyncWatermark{
CreatorSubject: "local-admin",
SourceSystem: domain.FreightSourceShunyunbao,
LastSuccessfulTo: watermarkTime,
},
}
service, err := NewFreightService(
repository,
&recordingDateSource{},
fakeClock{},
&sequenceIDs{},
time.Minute,
)
if err != nil {
t.Fatalf("NewFreightService() error = %v", err)
}
result, err := service.CreateDateSync(
context.Background(),
CreateFreightDateSyncCommand{
CreatorSubject: "local-admin",
ActorUserID: "00000000-0000-4000-8000-000000000099",
IdempotencyKey: "sync-to-now",
SyncToNow: true,
},
)
if err != nil {
t.Fatalf("CreateDateSync() error = %v", err)
}
if result.Run.CreatedFrom != "2026-07-25" ||
result.Run.CreatedTo != "2026-07-26" ||
result.Run.WatermarkThrough == nil ||
!result.Run.WatermarkThrough.Equal(fakeClock{}.Now()) {
t.Fatalf("sync-to-now run = %+v", result.Run)
}
}
func TestCreateFreightManualRangeRejectsMoreThanSevenDays(t *testing.T) {
service, _ := NewFreightService(
&dateCaptureRepository{},
&recordingDateSource{},
fakeClock{},
&sequenceIDs{},
time.Minute,
)
_, err := service.CreateDateSync(
context.Background(),
CreateFreightDateSyncCommand{
CreatorSubject: "local-admin",
ActorUserID: "00000000-0000-4000-8000-000000000099",
IdempotencyKey: "too-wide",
CreatedFrom: "2026-07-19",
CreatedTo: "2026-07-26",
},
)
assertUsecaseError(t, err, ErrorKindInvalid, "FREIGHT_SYNC_INVALID")
}
func validFreightSource() domain.FreightSourceBatch {
shop := "测试店铺"
sourceCreatedAt := "2026-07-28 08:00:00"
@@ -86,3 +208,122 @@ func validFreightSource() domain.FreightSourceBatch {
}},
}
}
var errDateSourceFailure = errors.New("date source failed")
type recordingDateSource struct {
calls [][2]string
failOnCall int
}
func (source *recordingDateSource) QueryOrder(
context.Context,
string,
) (domain.FreightSourceBatch, error) {
return validFreightSource(), nil
}
func (source *recordingDateSource) QueryCreatedRange(
_ context.Context,
createdFrom, createdTo string,
) (domain.FreightSourceBatch, error) {
source.calls = append(source.calls, [2]string{createdFrom, createdTo})
if source.failOnCall == len(source.calls) {
return domain.FreightSourceBatch{}, errDateSourceFailure
}
return domain.FreightSourceBatch{
SchemaVersion: 1,
Query: domain.FreightSourceQuery{
Mode: domain.FreightSyncCreatedRange,
CreatedFrom: &createdFrom,
CreatedTo: &createdTo,
},
Orders: []domain.FreightSourceOrder{},
}, nil
}
type dateCaptureRepository struct {
watermark *domain.FreightSyncWatermark
}
func (repository *dateCaptureRepository) CreateFreightSync(
_ context.Context,
run domain.FreightSyncRun,
_, _ string,
) (domain.FreightSyncRun, bool, error) {
return run, false, nil
}
func (*dateCaptureRepository) StartFreightSync(
context.Context,
string,
time.Time,
) error {
return nil
}
func (*dateCaptureRepository) CompleteFreightSync(
context.Context,
domain.FreightSyncRun,
domain.FreightImportBatch,
time.Time,
) error {
return nil
}
func (*dateCaptureRepository) CompleteFreightDateSync(
context.Context,
domain.FreightSyncRun,
domain.FreightImportBatch,
time.Time,
time.Time,
) error {
return nil
}
func (*dateCaptureRepository) FailFreightSync(
context.Context,
string,
string,
time.Time,
) error {
return nil
}
func (*dateCaptureRepository) RecoverFreightSyncs(
context.Context,
time.Time,
) (int64, error) {
return 0, nil
}
func (*dateCaptureRepository) GetFreightSync(
context.Context,
string,
string,
) (domain.FreightSyncRun, error) {
return domain.FreightSyncRun{}, nil
}
func (repository *dateCaptureRepository) GetFreightSyncWatermark(
context.Context,
string,
) (*domain.FreightSyncWatermark, error) {
return repository.watermark, nil
}
func (*dateCaptureRepository) ListFreightOrders(
context.Context,
string,
int,
) ([]domain.FreightOrder, error) {
return nil, nil
}
func (*dateCaptureRepository) GetFreightOrder(
context.Context,
string,
string,
) (domain.FreightOrderDetail, error) {
return domain.FreightOrderDetail{}, nil
}