update
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
// Package types 提供可复用的基础类型。
|
||||
package types
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
// TimeFormat 是时间字段返回给前端的标准格式(Y-m-d H:i:s)。
|
||||
const TimeFormat = "2006-01-02 15:04:05"
|
||||
|
||||
// Time 是对 time.Time 的封装:JSON 序列化输出 TimeFormat 格式,
|
||||
// 同时实现 driver.Valuer / sql.Scanner 以及与 GORM 的 GormDataTypeInterface,
|
||||
// 使其既能作为模型字段被 GORM 自动维护(CreatedAt/UpdatedAt),又能以
|
||||
// "2006-01-02 15:04:05" 字符串形式返回给前端。
|
||||
type Time struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
// NewTime 基于 time.Time 构造 Time。
|
||||
func NewTime(t time.Time) Time { return Time{Time: t} }
|
||||
|
||||
// Now 返回当前时间的 Time 封装。
|
||||
func Now() Time { return Time{Time: time.Now()} }
|
||||
|
||||
// GormDataType 让 GORM 将该字段识别为时间类型,从而启用自动时间戳。
|
||||
func (Time) GormDataType() schema.DataType { return schema.Time }
|
||||
|
||||
func (t Time) MarshalJSON() ([]byte, error) {
|
||||
if t.Time.IsZero() {
|
||||
return []byte(`""`), nil
|
||||
}
|
||||
return []byte(`"` + t.Time.Format(TimeFormat) + `"`), nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON 解析前端传入的时间字符串,兼容 TimeFormat 与 RFC3339。
|
||||
func (t *Time) UnmarshalJSON(b []byte) error {
|
||||
s := strings.Trim(string(b), `"`)
|
||||
if s == "" || s == "null" {
|
||||
t.Time = time.Time{}
|
||||
return nil
|
||||
}
|
||||
parsed, err := time.ParseInLocation(TimeFormat, s, time.Local)
|
||||
if err != nil {
|
||||
if parsed, err = time.Parse(time.RFC3339, s); err != nil {
|
||||
return fmt.Errorf("types.Time: 解析 %q 失败: %w", s, err)
|
||||
}
|
||||
}
|
||||
t.Time = parsed
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value 实现 driver.Valuer,写入数据库时返回底层 time.Time。
|
||||
func (t Time) Value() (driver.Value, error) {
|
||||
if t.Time.IsZero() {
|
||||
return nil, nil
|
||||
}
|
||||
return t.Time, nil
|
||||
}
|
||||
|
||||
// Scan 实现 sql.Scanner,从数据库读取时还原为 time.Time。
|
||||
func (t *Time) Scan(src interface{}) error {
|
||||
if src == nil {
|
||||
t.Time = time.Time{}
|
||||
return nil
|
||||
}
|
||||
switch v := src.(type) {
|
||||
case time.Time:
|
||||
t.Time = v
|
||||
case *time.Time:
|
||||
if v == nil {
|
||||
t.Time = time.Time{}
|
||||
} else {
|
||||
t.Time = *v
|
||||
}
|
||||
case []byte:
|
||||
parsed, err := time.ParseInLocation(TimeFormat, string(v), time.Local)
|
||||
if err != nil {
|
||||
return fmt.Errorf("types.Time: 扫描 []byte %q 失败: %w", string(v), err)
|
||||
}
|
||||
t.Time = parsed
|
||||
case string:
|
||||
parsed, err := time.ParseInLocation(TimeFormat, v, time.Local)
|
||||
if err != nil {
|
||||
return fmt.Errorf("types.Time: 扫描 string %q 失败: %w", v, err)
|
||||
}
|
||||
t.Time = parsed
|
||||
default:
|
||||
return fmt.Errorf("types.Time: 不支持的扫描类型 %T", src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// String 返回 TimeFormat 格式的字符串表示。
|
||||
func (t Time) String() string {
|
||||
if t.Time.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return t.Time.Format(TimeFormat)
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
func TestTime_MarshalJSON(t *testing.T) {
|
||||
t.Run("正常时间输出 Y-m-d H:i:s", func(t *testing.T) {
|
||||
tm := NewTime(time.Date(2026, 10, 26, 15, 4, 5, 0, time.UTC))
|
||||
b, err := json.Marshal(tm)
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, `"2026-10-26 15:04:05"`, string(b))
|
||||
})
|
||||
|
||||
t.Run("零值输出空字符串", func(t *testing.T) {
|
||||
b, err := json.Marshal(Time{})
|
||||
require.NoError(t, err)
|
||||
assert.JSONEq(t, `""`, string(b))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTime_UnmarshalJSON(t *testing.T) {
|
||||
t.Run("Y-m-d H:i:s", func(t *testing.T) {
|
||||
var tm Time
|
||||
require.NoError(t, json.Unmarshal([]byte(`"2026-10-26 15:04:05"`), &tm))
|
||||
assert.Equal(t, time.Date(2026, 10, 26, 15, 4, 5, 0, time.Local), tm.Time)
|
||||
})
|
||||
|
||||
t.Run("RFC3339 兼容", func(t *testing.T) {
|
||||
var tm Time
|
||||
require.NoError(t, json.Unmarshal([]byte(`"2026-10-26T15:04:05Z"`), &tm))
|
||||
assert.Equal(t, 2026, tm.Year())
|
||||
assert.Equal(t, time.October, tm.Month())
|
||||
})
|
||||
|
||||
t.Run("空字符串与 null", func(t *testing.T) {
|
||||
for _, in := range []string{`""`, `null`} {
|
||||
var tm Time
|
||||
require.NoError(t, json.Unmarshal([]byte(in), &tm))
|
||||
assert.True(t, tm.IsZero())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("非法格式报错", func(t *testing.T) {
|
||||
var tm Time
|
||||
assert.Error(t, json.Unmarshal([]byte(`"not-a-time"`), &tm))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTime_Scan(t *testing.T) {
|
||||
src := time.Date(2026, 10, 26, 15, 4, 5, 0, time.UTC)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
in interface{}
|
||||
want time.Time
|
||||
}{
|
||||
{"time.Time", src, src},
|
||||
{"*time.Time", &src, src},
|
||||
{"[]byte", []byte("2026-10-26 15:04:05"), time.Date(2026, 10, 26, 15, 4, 5, 0, time.Local)},
|
||||
{"string", "2026-10-26 15:04:05", time.Date(2026, 10, 26, 15, 4, 5, 0, time.Local)},
|
||||
{"nil", nil, time.Time{}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
var tm Time
|
||||
require.NoError(t, tm.Scan(c.in))
|
||||
if c.want.IsZero() {
|
||||
assert.True(t, tm.IsZero())
|
||||
return
|
||||
}
|
||||
assert.True(t, c.want.Equal(tm.Time), "got %v want %v", tm.Time, c.want)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("不支持的类型报错", func(t *testing.T) {
|
||||
var tm Time
|
||||
assert.Error(t, tm.Scan(123))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTime_Value(t *testing.T) {
|
||||
src := time.Date(2026, 10, 26, 15, 4, 5, 0, time.UTC)
|
||||
v, err := NewTime(src).Value()
|
||||
require.NoError(t, err)
|
||||
got, ok := v.(time.Time)
|
||||
require.True(t, ok)
|
||||
assert.True(t, src.Equal(got))
|
||||
|
||||
// 零值返回 nil。
|
||||
v, err = Time{}.Value()
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, v)
|
||||
}
|
||||
|
||||
func TestTime_GormDataType(t *testing.T) {
|
||||
assert.Equal(t, schema.Time, Time{}.GormDataType())
|
||||
}
|
||||
|
||||
// 确保 Time 满足相关接口,编译期校验。
|
||||
var (
|
||||
_ driver.Valuer = Time{}
|
||||
_ interface{ Scan(interface{}) error } = (*Time)(nil)
|
||||
_ json.Marshaler = Time{}
|
||||
_ json.Unmarshaler = (*Time)(nil)
|
||||
)
|
||||
Reference in New Issue
Block a user