feat: 顺运宝支持指定日期同步 (#53)
This commit is contained in:
+97
-14
@@ -32,6 +32,49 @@ import (
|
||||
|
||||
const dateLayout = "2006-01-02"
|
||||
|
||||
// SybSyncOptions 表示一次同步的日期选择。From/To 都为空是日常自动增量;
|
||||
// 两者都有值是操作员明确发起的指定日期补同步。
|
||||
//
|
||||
// `[必须]` 不允许只填一端。把“半个范围”悄悄退化成自动增量,会让操作员
|
||||
// 以为补拉了指定日期,实际却跑了另一段数据。
|
||||
type SybSyncOptions struct {
|
||||
From string
|
||||
To string
|
||||
}
|
||||
|
||||
// IsSpecified 判断这是不是指定日期同步。
|
||||
func (o SybSyncOptions) IsSpecified() bool {
|
||||
return strings.TrimSpace(o.From) != "" || strings.TrimSpace(o.To) != ""
|
||||
}
|
||||
|
||||
// NewSybSyncOptions 清理并校验浏览器提交的日期范围。
|
||||
// 日期按顺运宝服务端 UTC+8 解释,起止两天都包含。
|
||||
func NewSybSyncOptions(fromRaw, toRaw string, now time.Time) (SybSyncOptions, error) {
|
||||
from := strings.TrimSpace(fromRaw)
|
||||
to := strings.TrimSpace(toRaw)
|
||||
if from == "" && to == "" {
|
||||
return SybSyncOptions{}, nil
|
||||
}
|
||||
if from == "" || to == "" {
|
||||
return SybSyncOptions{}, fmt.Errorf("开始日期和结束日期必须同时填写")
|
||||
}
|
||||
fromDate, err := time.Parse(dateLayout, from)
|
||||
if err != nil || fromDate.Format(dateLayout) != from {
|
||||
return SybSyncOptions{}, fmt.Errorf("开始日期格式不正确,请使用 YYYY-MM-DD")
|
||||
}
|
||||
toDate, err := time.Parse(dateLayout, to)
|
||||
if err != nil || toDate.Format(dateLayout) != to {
|
||||
return SybSyncOptions{}, fmt.Errorf("结束日期格式不正确,请使用 YYYY-MM-DD")
|
||||
}
|
||||
if fromDate.After(toDate) {
|
||||
return SybSyncOptions{}, fmt.Errorf("开始日期不能晚于结束日期")
|
||||
}
|
||||
if to > dateOf(now) {
|
||||
return SybSyncOptions{}, fmt.Errorf("结束日期不能晚于顺运宝服务端今天(%s)", dateOf(now))
|
||||
}
|
||||
return SybSyncOptions{From: from, To: to}, nil
|
||||
}
|
||||
|
||||
// sybLocation 是顺运宝服务端的时区。
|
||||
//
|
||||
// `[必须]` 日期范围筛的是服务端的 created,而它是 UTC+8——实测
|
||||
@@ -57,6 +100,12 @@ func dateOf(t time.Time) string {
|
||||
return t.In(sybLocation).Format(dateLayout)
|
||||
}
|
||||
|
||||
// SybToday 返回顺运宝服务端时区下的今天,供页面 date 输入的 max/default
|
||||
// 使用。页面和同步校验共用这一处,避免本机时区不同导致显示和校验差一天。
|
||||
func SybToday(now time.Time) string {
|
||||
return dateOf(now)
|
||||
}
|
||||
|
||||
// syncDateRange 算出这次同步该拉哪个日期范围。
|
||||
//
|
||||
// `[必须]` 增量必须从"上次同步日期当天"重新拉,不是第二天——created
|
||||
@@ -97,8 +146,9 @@ type SkipNote struct {
|
||||
// SyncReport 是一次同步的结果,供状态条显示。
|
||||
type SyncReport struct {
|
||||
From, To string
|
||||
StockCount int // 拉到的货运单数
|
||||
DetailCount int // 落库的商品明细行数(不含跳过的)
|
||||
Specified bool // true 表示操作员发起的指定日期补同步
|
||||
StockCount int // 拉到的货运单数
|
||||
DetailCount int // 落库的商品明细行数(不含跳过的)
|
||||
Created int
|
||||
Updated int
|
||||
SkippedZero int // quantity <= 0 被跳过的条数
|
||||
@@ -116,7 +166,11 @@ func (r SyncReport) Summary() string {
|
||||
if r.Err != nil {
|
||||
return "同步失败:" + r.Err.Error()
|
||||
}
|
||||
msg := fmt.Sprintf("同步完成:日期范围 %s ~ %s,货运单 %d 张,商品明细 %d 条(新增 %d,更新 %d,跳过 %d)",
|
||||
prefix := "同步完成"
|
||||
if r.Specified {
|
||||
prefix = "指定日期同步完成"
|
||||
}
|
||||
msg := fmt.Sprintf("%s:日期范围 %s ~ %s,货运单 %d 张,商品明细 %d 条(新增 %d,更新 %d,跳过 %d)", prefix,
|
||||
r.From, r.To, r.StockCount, r.DetailCount, r.Created, r.Updated, r.SkippedZero)
|
||||
if len(r.Notes) > 0 {
|
||||
var reasons []string
|
||||
@@ -222,7 +276,15 @@ func roundYuanToCent(yuan float64) int64 {
|
||||
// 如果会话恰好失效,会从 syb.Client 的调用里冒出 syb.ErrSessionInvalid,
|
||||
// 和其它错误一样按"中途失败"处理:不更新 last_synced_at,把原因写进报告。
|
||||
func RunSybSync(ctx context.Context, db *sql.DB, client *syb.Client, cfg config.SybConfig, now time.Time) SyncReport {
|
||||
report := SyncReport{StartedAt: now}
|
||||
return RunSybSyncWithOptions(ctx, db, client, cfg, now, SybSyncOptions{})
|
||||
}
|
||||
|
||||
// RunSybSyncWithOptions 执行自动增量或指定日期同步。
|
||||
//
|
||||
// 指定日期只有完整覆盖“本来应该自动同步的范围”时才推进 last_synced_at。
|
||||
// 局部历史补拉只 upsert 数据、不动游标,否则会让未覆盖的订单永久漏掉。
|
||||
func RunSybSyncWithOptions(ctx context.Context, db *sql.DB, client *syb.Client, cfg config.SybConfig, now time.Time, options SybSyncOptions) SyncReport {
|
||||
report := SyncReport{StartedAt: now, Specified: options.IsSpecified()}
|
||||
|
||||
pageSize := cfg.PageSize
|
||||
if pageSize <= 0 {
|
||||
@@ -240,11 +302,28 @@ func RunSybSync(ctx context.Context, db *sql.DB, client *syb.Client, cfg config.
|
||||
return report
|
||||
}
|
||||
|
||||
from, to, err := syncDateRange(lastSyncedAt, cfg.SyncFrom, now)
|
||||
if err != nil {
|
||||
report.Err = err
|
||||
report.FinishedAt = time.Now().UTC()
|
||||
return report
|
||||
from, to := options.From, options.To
|
||||
advanceCursor := !options.IsSpecified()
|
||||
if options.IsSpecified() {
|
||||
validated, validateErr := NewSybSyncOptions(options.From, options.To, now)
|
||||
if validateErr != nil {
|
||||
report.Err = validateErr
|
||||
report.FinishedAt = time.Now().UTC()
|
||||
return report
|
||||
}
|
||||
from, to = validated.From, validated.To
|
||||
|
||||
// 算得出自动范围时,只有完整覆盖它才允许推进游标。首次同步没有
|
||||
// sync_from 时仍允许补拉,但不能凭局部范围建立一个不可靠的游标。
|
||||
autoFrom, autoTo, rangeErr := syncDateRange(lastSyncedAt, cfg.SyncFrom, now)
|
||||
advanceCursor = rangeErr == nil && from <= autoFrom && to >= autoTo
|
||||
} else {
|
||||
from, to, err = syncDateRange(lastSyncedAt, cfg.SyncFrom, now)
|
||||
if err != nil {
|
||||
report.Err = err
|
||||
report.FinishedAt = time.Now().UTC()
|
||||
return report
|
||||
}
|
||||
}
|
||||
report.From, report.To = from, to
|
||||
|
||||
@@ -263,8 +342,10 @@ func RunSybSync(ctx context.Context, db *sql.DB, client *syb.Client, cfg config.
|
||||
}
|
||||
if total == 0 {
|
||||
report.FinishedAt = time.Now().UTC()
|
||||
if err := repository.SetSybLastSyncedAt(db, model.NowISO()); err != nil {
|
||||
report.Err = fmt.Errorf("更新同步进度失败: %w", err)
|
||||
if advanceCursor {
|
||||
if err := repository.SetSybLastSyncedAt(db, model.NowISO()); err != nil {
|
||||
report.Err = fmt.Errorf("更新同步进度失败: %w", err)
|
||||
}
|
||||
}
|
||||
return report
|
||||
}
|
||||
@@ -329,9 +410,11 @@ func RunSybSync(ctx context.Context, db *sql.DB, client *syb.Client, cfg config.
|
||||
}
|
||||
|
||||
// ④ 全部成功,才更新 last_synced_at。
|
||||
if err := repository.SetSybLastSyncedAt(db, model.NowISO()); err != nil {
|
||||
report.Err = fmt.Errorf("同步数据已全部写入,但更新同步进度失败,"+
|
||||
"下次同步会重新拉这个日期范围(不会漏,但会重复拉一次): %w", err)
|
||||
if advanceCursor {
|
||||
if err := repository.SetSybLastSyncedAt(db, model.NowISO()); err != nil {
|
||||
report.Err = fmt.Errorf("同步数据已全部写入,但更新同步进度失败,"+
|
||||
"下次同步会重新拉这个日期范围(不会漏,但会重复拉一次): %w", err)
|
||||
}
|
||||
}
|
||||
report.FinishedAt = time.Now().UTC()
|
||||
return report
|
||||
|
||||
Reference in New Issue
Block a user