From 01be7e72515ff311af7946753641dc8227bfa32d Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Wed, 29 Jul 2026 14:21:57 +0800 Subject: [PATCH] fix(t238): accept ERP minute timestamps --- .../transport/httpapi/admin_handlers_test.go | 2 +- .../internal/usecase/freight_service.go | 2 + .../internal/usecase/freight_service_test.go | 70 +++++++++++++++++++ docs/api.md | 3 + docs/current-state.md | 8 +-- docs/tasks/T-238.md | 19 +++-- 6 files changed, 92 insertions(+), 12 deletions(-) diff --git a/backend-api/internal/transport/httpapi/admin_handlers_test.go b/backend-api/internal/transport/httpapi/admin_handlers_test.go index 60fe2c2..6b7306e 100644 --- a/backend-api/internal/transport/httpapi/admin_handlers_test.go +++ b/backend-api/internal/transport/httpapi/admin_handlers_test.go @@ -1260,7 +1260,7 @@ func (staticFreightSource) QueryOrder( string, ) (domain.FreightSourceBatch, error) { shop := "测试店铺" - created := "2026-07-28 08:00:00" + created := "2026-07-28 08:00" quantityOne := 1 quantityTwo := 2 return domain.FreightSourceBatch{ diff --git a/backend-api/internal/usecase/freight_service.go b/backend-api/internal/usecase/freight_service.go index 139924d..619954d 100644 --- a/backend-api/internal/usecase/freight_service.go +++ b/backend-api/internal/usecase/freight_service.go @@ -924,6 +924,8 @@ func parseERPTime(value *string) (*time.Time, error) { time.RFC3339Nano, "2006-01-02 15:04:05", "2006-01-02T15:04:05", + "2006-01-02 15:04", + "2006-01-02T15:04", } for _, layout := range layouts { var parsed time.Time diff --git a/backend-api/internal/usecase/freight_service_test.go b/backend-api/internal/usecase/freight_service_test.go index 0f8a626..805a5a3 100644 --- a/backend-api/internal/usecase/freight_service_test.go +++ b/backend-api/internal/usecase/freight_service_test.go @@ -56,6 +56,76 @@ func TestFreightNormalizationRejectsConflictingIdentityAndInvalidTime( } } +func TestParseERPTimeAcceptsConfirmedMinuteAndSecondPrecision(t *testing.T) { + shanghaiExpected := time.Date(2026, 7, 29, 4, 34, 0, 0, time.UTC) + shanghaiSecondExpected := time.Date( + 2026, + 7, + 29, + 4, + 34, + 56, + 0, + time.UTC, + ) + testCases := []struct { + name string + value string + expected time.Time + }{ + {"space minute", "2026-07-29 12:34", shanghaiExpected}, + {"T minute", "2026-07-29T12:34", shanghaiExpected}, + {"space second", "2026-07-29 12:34:56", shanghaiSecondExpected}, + {"T second", "2026-07-29T12:34:56", shanghaiSecondExpected}, + { + "RFC3339", + "2026-07-29T12:34:56+08:00", + shanghaiSecondExpected, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + actual, err := parseERPTime(&testCase.value) + if err != nil || actual == nil || !actual.Equal(testCase.expected) { + t.Fatalf( + "parseERPTime(%q) = %v, %v; want %v", + testCase.value, + actual, + err, + testCase.expected, + ) + } + }) + } +} + +func TestParseERPTimeRejectsUnknownMinuteFormats(t *testing.T) { + for _, value := range []string{ + "2026-02-30 12:34", + "2026-07-29 12", + "2026-07-29 12:34 extra", + "29/07/2026 12:34", + } { + if actual, err := parseERPTime(&value); err == nil || actual != nil { + t.Fatalf("parseERPTime(%q) = %v, %v", value, actual, err) + } + } +} + +func TestFreightNormalizationAcceptsDetailMinutePrecisionTime(t *testing.T) { + source := validFreightSource() + value := "2026-07-29 12:34" + source.Orders[0].SourceCreatedAt = &value + service := &FreightService{ids: &sequenceIDs{}} + result, err := service.normalize(source) + expected := time.Date(2026, 7, 29, 4, 34, 0, 0, time.UTC) + if err != nil || len(result.Orders) != 1 || + result.Orders[0].SourceCreatedAt == nil || + !result.Orders[0].SourceCreatedAt.Equal(expected) { + t.Fatalf("normalize minute time = %+v, %v", result.Orders, err) + } +} + func TestFreightSourceErrorCodesAreSourceNeutral(t *testing.T) { cases := []struct { err error diff --git a/docs/api.md b/docs/api.md index f485c23..a1c94f8 100644 --- a/docs/api.md +++ b/docs/api.md @@ -298,6 +298,9 @@ T-222 返回货运头、全部当前商品明细和 revision/hash 状态。T-223 T-227 的 Go source 使用当前内存会话,按 `listTotal -> list 分页 -> listByStock` 查询; 完整单号由请求同步调用,日期范围由后台 worker 调用。每次先校验会话;列表最多 100 条、 每页 20 条,详情每批最多 100 个外部 stock ID。 +顺运宝详情 `created` 已确认可能只有分钟精度;无时区的 +`yyyy-MM-dd HH:mm[:ss]` 或 `yyyy-MM-ddTHH:mm[:ss]` 均严格按 Asia/Shanghai 解释并转 UTC, +RFC3339 保留其显式时区。其他未知时间格式继续返回 `ERP_RESPONSE_INVALID`。 未配置、未登录、找不到货运单、响应协议错误和暂时不可用分别落为 `ERP_NOT_CONFIGURED`、`ERP_SESSION_REQUIRED`、`ERP_FREIGHT_NOT_FOUND`、 `ERP_RESPONSE_INVALID` 和 `ERP_UNAVAILABLE`,不返回 ERP 原始错误 body。 diff --git a/docs/current-state.md b/docs/current-state.md index 7874366..8a5d451 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -5,7 +5,7 @@ ## 当前快照 - 日期:2026-07-29 -- 阶段:T-238 已规划顺运宝详情分钟精度时间兼容,待实现 +- 阶段:T-238 已完成顺运宝详情分钟精度时间兼容 - Git:当前分支为 `main`;T-001 至 T-004、T-101 至 T-104、T-201 至 T-219 均按文档提交、实现提交的顺序纳入历史 - 生产代码:`android-buyer/` 已接入 Roubao Android 源码 @@ -18,8 +18,8 @@ 同步记录、货运头和全部明细,canonical hash 控制 revision,Admin 已有 `/freight`、 `/freight/import`、`/freight/{id}` 与对应 JSON API。T-237 已将完整单号改为 55 秒 同步响应,成功后直接进入可见货运列表;超时/取消使用独立 cleanup context 写 - `FAILED`,日期范围仍后台异步。HTTP `WriteTimeout` 为 70 秒。T-238 已定位真实详情 - `created` 为分钟精度,而当前 parser 只接受带秒格式,待兼容后复测。 + `FAILED`,日期范围仍后台异步。HTTP `WriteTimeout` 为 70 秒。T-238 已兼容真实详情 + `created` 的分钟精度,仍按 Asia/Shanghai 严格解析并转 UTC。 - ERP Go 迁移:T-225 已用脱敏 fixture 固定 `internal/platform/shunyunbao` 的 header、 单号/日期查询、分页、详情批量和字段 allowlist,并使货运用例依赖来源中立错误。T-226 已增加受锁保护的 Go 内存 Cookie jar、验证码 ticket、登录和用户校验,以及 ADMIN 的 @@ -41,7 +41,7 @@ - Android Studio:未安装;`winget` 静默安装卡住后已终止,不阻塞命令行构建 - 测试:T-219 Android Debug/Release 单元测试与构建和根 `init.ps1` 通过; Debug APK `1.4.16 (21)` 已覆盖安装到 PKG110 -- 后端测试:T-226 至 T-237 已运行 `go test ./...`、`go test -race ./...`、`go vet ./...` +- 后端测试:T-226 至 T-238 已运行 `go test ./...`、`go test -race ./...`、`go vet ./...` 和三个 Go 入口构建;T-227 增加 Go source 的伪 ERP 会话预检、完整单号、日期分页去重、 详情 allowlist 和稳定错误码覆盖;根 `init.ps1` 的 Android 测试/Debug APK 与 Go 标准 验证也通过,均未访问真实 ERP; diff --git a/docs/tasks/T-238.md b/docs/tasks/T-238.md index 6068874..d1d8c66 100644 --- a/docs/tasks/T-238.md +++ b/docs/tasks/T-238.md @@ -4,7 +4,7 @@ title: 兼容顺运宝详情分钟精度时间 phase: 2 deps: - T-237 -status: PLANNED +status: DONE created: 2026-07-29 context_ref: 8d5b88f work_branch: null @@ -14,6 +14,7 @@ write_paths: - docs/current-state.md - backend-api/internal/usecase/freight_service.go - backend-api/internal/usecase/freight_service_test.go + - backend-api/internal/transport/httpapi/admin_handlers_test.go --- ## 问题 / 背景 @@ -43,12 +44,12 @@ write_paths: ## 验收要点 -- [ ] 详情 `yyyy-MM-dd HH:mm` 可规范化并按 Asia/Shanghai 得到正确 UTC 时间。 -- [ ] `yyyy-MM-ddTHH:mm` 同样可解析;既有 RFC3339 和带秒格式不回归。 -- [ ] 非法日期和未知格式仍返回错误,不放宽为模糊解析。 -- [ ] 完整单号同步不再因已确认的 16 位详情时间返回 `ERP_RESPONSE_INVALID`。 -- [ ] 两份真实响应 JSON 保持未跟踪,不进入 Git。 -- [ ] 标准 Go 测试、race、vet 和三个入口构建通过。 +- [x] 详情 `yyyy-MM-dd HH:mm` 可规范化并按 Asia/Shanghai 得到正确 UTC 时间。 +- [x] `yyyy-MM-ddTHH:mm` 同样可解析;既有 RFC3339 和带秒格式不回归。 +- [x] 非法日期和未知格式仍返回错误,不放宽为模糊解析。 +- [x] 完整单号同步不再因已确认的 16 位详情时间返回 `ERP_RESPONSE_INVALID`。 +- [x] 两份真实响应 JSON 保持未跟踪,不进入 Git。 +- [x] 标准 Go 测试、race、vet 和三个入口构建通过。 ## 边界 @@ -62,3 +63,7 @@ write_paths: - 2026-07-29:创建任务。以只输出字段名、类型、长度和结构断言的方式检查两份本机响应; 排除 envelope、list、detail id、details、数量、SKU 回退和字段长度,定位为详情 `created` 分钟精度与用例层严格 layout 不一致。 +- 2026-07-29:严格增加空格/`T` 分隔的分钟 precision layout,保持 Asia/Shanghai 和 UTC + 规范化语义。脱敏单元测试覆盖分钟、秒、RFC3339 和未知格式,Admin/Gin/SQLite 完整单号 + 集成 fixture 改用分钟精度并返回 `201/SUCCEEDED`。标准 Go 测试、race、vet 及三个入口 + 构建均通过;两份真实响应 JSON 未暂存。