fix(t238): accept ERP minute timestamps

This commit is contained in:
QiuSW
2026-07-29 14:21:57 +08:00
parent 15b1101941
commit 01be7e7251
6 changed files with 92 additions and 12 deletions
@@ -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{
@@ -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
@@ -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
+3
View File
@@ -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。
+4 -4
View File
@@ -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;
+12 -7
View File
@@ -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 未暂存。