From 3ec7c9be9d2694f1a37fc1cf156fc3d77955c083 Mon Sep 17 00:00:00 2001 From: DurianPankek Date: Tue, 7 Apr 2026 19:38:16 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(export):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=AF=BC=E5=87=BA=E6=97=B6=E9=97=B4=E6=97=B6=E5=8C=BA?= =?UTF-8?q?=E8=AF=AF=E5=81=8F=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/app/methods_file.go | 26 +++++++--- internal/app/methods_file_export_test.go | 63 ++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/internal/app/methods_file.go b/internal/app/methods_file.go index 3d02edf..2f97cc1 100644 --- a/internal/app/methods_file.go +++ b/internal/app/methods_file.go @@ -614,13 +614,23 @@ func parseTemporalString(raw string) (time.Time, bool) { return time.Time{}, false } - layouts := []string{ + layoutsWithZone := []string{ "2006-01-02 15:04:05.999999999 -0700 MST", "2006-01-02 15:04:05 -0700 MST", "2006-01-02 15:04:05.999999999 -0700", "2006-01-02 15:04:05 -0700", time.RFC3339Nano, time.RFC3339, + } + + for _, layout := range layoutsWithZone { + parsed, err := time.Parse(layout, text) + if err == nil { + return parsed, true + } + } + + layoutsWithoutZone := []string{ "2006-01-02 15:04:05.999999999", "2006-01-02 15:04:05", "2006-01-02", @@ -628,8 +638,8 @@ func parseTemporalString(raw string) (time.Time, bool) { "15:04:05", } - for _, layout := range layouts { - parsed, err := time.Parse(layout, text) + for _, layout := range layoutsWithoutZone { + parsed, err := time.ParseInLocation(layout, text, time.Local) if err == nil { return parsed, true } @@ -2208,9 +2218,9 @@ func formatExportCellText(val interface{}) string { return text default: text := fmt.Sprintf("%v", val) - // 字符串型日期时间值(如 RFC3339 "2026-03-10T17:01:55+08:00")格式化为本地时区 yyyy-MM-dd HH:mm:ss + // 字符串型日期时间值(如 RFC3339 "2026-03-10T17:01:55+08:00")统一格式化为 yyyy-MM-dd HH:mm:ss if parsed, ok := parseTemporalString(text); ok { - return parsed.Local().Format("2006-01-02 15:04:05") + return parsed.Format("2006-01-02 15:04:05") } return text } @@ -2223,15 +2233,15 @@ func normalizeExportJSONValue(val interface{}) interface{} { switch v := val.(type) { case time.Time: - return v.Local().Format("2006-01-02 15:04:05") + return v.Format("2006-01-02 15:04:05") case *time.Time: if v == nil { return nil } - return v.Local().Format("2006-01-02 15:04:05") + return v.Format("2006-01-02 15:04:05") case string: if parsed, ok := parseTemporalString(v); ok { - return parsed.Local().Format("2006-01-02 15:04:05") + return parsed.Format("2006-01-02 15:04:05") } return v case float32: diff --git a/internal/app/methods_file_export_test.go b/internal/app/methods_file_export_test.go index 5ddaf9c..549d8b2 100644 --- a/internal/app/methods_file_export_test.go +++ b/internal/app/methods_file_export_test.go @@ -141,6 +141,69 @@ func TestWriteRowsToFile_JSON_NumberKeepPlainText(t *testing.T) { } } +func TestNormalizeExportJSONValue_LocalDateTimeString_NoTimezoneShift(t *testing.T) { + originalLocal := time.Local + time.Local = time.FixedZone("UTC+8", 8*60*60) + defer func() { time.Local = originalLocal }() + + got := normalizeExportJSONValue("2026-04-07 18:44:32") + if got != "2026-04-07 18:44:32" { + t.Fatalf("本地无时区字符串不应发生时区偏移,want=%q got=%v", "2026-04-07 18:44:32", got) + } +} + +func TestFormatExportCellText_TimeValue_KeepWallClock(t *testing.T) { + originalLocal := time.Local + time.Local = time.FixedZone("UTC+8", 8*60*60) + defer func() { time.Local = originalLocal }() + + utc := time.Date(2026, 4, 7, 10, 44, 32, 0, time.UTC) + got := formatExportCellText(utc) + if got != "2026-04-07 10:44:32" { + t.Fatalf("time.Time 导出应保持原始钟表时间,want=%q got=%q", "2026-04-07 10:44:32", got) + } +} + +func TestParseTemporalString_LocalDateTime_NoTimezoneShift(t *testing.T) { + originalLocal := time.Local + time.Local = time.FixedZone("UTC+8", 8*60*60) + defer func() { time.Local = originalLocal }() + + parsed, ok := parseTemporalString("2026-04-07 18:44:32") + if !ok { + t.Fatal("parseTemporalString 应成功解析本地日期时间") + } + if parsed.Local().Format("2006-01-02 15:04:05") != "2026-04-07 18:44:32" { + t.Fatalf("无时区时间解析后不应发生偏移,got=%q", parsed.Local().Format("2006-01-02 15:04:05")) + } +} + +func TestParseTemporalString_RFC3339_KeepWallClock(t *testing.T) { + originalLocal := time.Local + time.Local = time.FixedZone("UTC+8", 8*60*60) + defer func() { time.Local = originalLocal }() + + parsed, ok := parseTemporalString("2026-04-07T10:44:32Z") + if !ok { + t.Fatal("parseTemporalString 应成功解析 RFC3339") + } + if parsed.Format("2006-01-02 15:04:05") != "2026-04-07 10:44:32" { + t.Fatalf("RFC3339 解析后应保持原始钟表时间,got=%q", parsed.Format("2006-01-02 15:04:05")) + } +} + +func TestNormalizeExportJSONValue_TimeValue_KeepWallClock(t *testing.T) { + originalLocal := time.Local + time.Local = time.FixedZone("UTC+8", 8*60*60) + defer func() { time.Local = originalLocal }() + + utc := time.Date(2026, 4, 7, 18, 44, 32, 0, time.UTC) + got := normalizeExportJSONValue(utc) + if got != "2026-04-07 18:44:32" { + t.Fatalf("JSON 导出 time.Time 应保持原始钟表时间,want=%q got=%v", "2026-04-07 18:44:32", got) + } +} + func TestQueryDataForExport_UsesMinimumTimeout(t *testing.T) { fake := &fakeExportQueryDB{ data: []map[string]interface{}{{"v": 1}},