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
@@ -26,6 +26,9 @@ func (h *adminHandlers) createFreightSync(ctx *gin.Context) {
var request struct {
Mode string `json:"mode"`
OrderNumber string `json:"order_number"`
CreatedFrom string `json:"created_from"`
CreatedTo string `json:"created_to"`
SyncToNow bool `json:"sync_to_now"`
}
if err := decodeJSON(ctx, &request); err != nil {
writePublicError(
@@ -38,26 +41,66 @@ func (h *adminHandlers) createFreightSync(ctx *gin.Context) {
)
return
}
if request.Mode != domain.FreightSyncOrderNumber {
var result usecase.CreateFreightSyncResult
var err error
switch request.Mode {
case domain.FreightSyncOrderNumber:
if strings.TrimSpace(request.CreatedFrom) != "" ||
strings.TrimSpace(request.CreatedTo) != "" ||
request.SyncToNow {
writePublicError(
ctx,
http.StatusUnprocessableEntity,
"FREIGHT_SYNC_INVALID",
"freight sync request is invalid",
false,
fieldDetails("mode", "ORDER_NUMBER cannot include date parameters"),
)
return
}
result, err = h.services.Freight.CreateOrderSync(
ctx.Request.Context(),
usecase.CreateFreightSyncCommand{
CreatorSubject: localAdminSubject,
ActorUserID: adminActorUserID(ctx),
IdempotencyKey: ctx.GetHeader("Idempotency-Key"),
OrderNumber: request.OrderNumber,
},
)
case domain.FreightSyncCreatedRange:
if strings.TrimSpace(request.OrderNumber) != "" {
writePublicError(
ctx,
http.StatusUnprocessableEntity,
"FREIGHT_SYNC_INVALID",
"freight sync request is invalid",
false,
fieldDetails("mode", "CREATED_RANGE cannot include order_number"),
)
return
}
result, err = h.services.Freight.CreateDateSync(
ctx.Request.Context(),
usecase.CreateFreightDateSyncCommand{
CreatorSubject: localAdminSubject,
ActorUserID: adminActorUserID(ctx),
IdempotencyKey: ctx.GetHeader("Idempotency-Key"),
CreatedFrom: request.CreatedFrom,
CreatedTo: request.CreatedTo,
SyncToNow: request.SyncToNow,
},
)
default:
writePublicError(
ctx,
http.StatusUnprocessableEntity,
"FREIGHT_SYNC_MODE_INVALID",
"freight sync mode is not supported",
false,
fieldDetails("mode", "must be ORDER_NUMBER"),
fieldDetails("mode", "must be ORDER_NUMBER or CREATED_RANGE"),
)
return
}
result, err := h.services.Freight.CreateOrderSync(
ctx.Request.Context(),
usecase.CreateFreightSyncCommand{
CreatorSubject: localAdminSubject,
ActorUserID: adminActorUserID(ctx),
IdempotencyKey: ctx.GetHeader("Idempotency-Key"),
OrderNumber: request.OrderNumber,
},
)
if err != nil {
writeUsecaseError(ctx, err)
return
@@ -69,6 +112,28 @@ func (h *adminHandlers) createFreightSync(ctx *gin.Context) {
})
}
func (h *adminHandlers) freightSyncWatermark(ctx *gin.Context) {
watermark, err := h.services.Freight.GetWatermark(
ctx.Request.Context(),
localAdminSubject,
)
if err != nil {
writeUsecaseError(ctx, err)
return
}
ctx.Header("Cache-Control", "no-store")
if watermark == nil {
ctx.JSON(http.StatusOK, gin.H{"watermark": nil})
return
}
ctx.JSON(http.StatusOK, gin.H{"watermark": gin.H{
"source_system": watermark.SourceSystem,
"last_successful_to": formatTime(watermark.LastSuccessfulTo),
"last_successful_run_id": watermark.LastSuccessfulRunID,
"updated_at": formatTime(watermark.UpdatedAt),
}})
}
func (h *adminHandlers) freightSyncDetail(ctx *gin.Context) {
run, err := h.services.Freight.GetSync(
ctx.Request.Context(),
@@ -175,19 +240,29 @@ func (h *adminHandlers) freightOrderDetail(ctx *gin.Context) {
func freightSyncResponse(run domain.FreightSyncRun) gin.H {
return gin.H{
"id": run.ID,
"mode": run.Mode,
"query_sha256": run.QuerySHA256,
"status": run.Status,
"error_code": run.ErrorCode,
"order_count": run.OrderCount,
"item_count": run.ItemCount,
"created_at": formatTime(run.CreatedAt),
"started_at": formatOptionalTime(run.StartedAt),
"finished_at": formatOptionalTime(run.FinishedAt),
"id": run.ID,
"mode": run.Mode,
"created_from": nullableResponseString(run.CreatedFrom),
"created_to": nullableResponseString(run.CreatedTo),
"watermark_through": formatOptionalTime(run.WatermarkThrough),
"query_sha256": run.QuerySHA256,
"status": run.Status,
"error_code": run.ErrorCode,
"order_count": run.OrderCount,
"item_count": run.ItemCount,
"created_at": formatTime(run.CreatedAt),
"started_at": formatOptionalTime(run.StartedAt),
"finished_at": formatOptionalTime(run.FinishedAt),
}
}
func nullableResponseString(value string) any {
if value == "" {
return nil
}
return value
}
func freightOrderResponse(order domain.FreightOrder) gin.H {
return gin.H{
"id": order.ID,