fix: 修复账单时间显示为UTC时区问题,改为本地时间

- 新增 LocalTime 自定义类型,JSON序列化输出本地时间格式
- 修改 CleanedBill.Time 字段类型为 LocalTime
- 更新 parseTime 函数返回 LocalTime 类型
- 前端添加 formatDateTime 工具函数(兼容处理)
- 版本号更新至 1.0.2
This commit is contained in:
2026-01-11 21:40:27 +08:00
parent d49d9afb3a
commit 8a9de1b328
9 changed files with 107 additions and 14 deletions

View File

@@ -1,7 +1,7 @@
# BillAI 服务器配置文件
# 应用版本
version: "1.0.0"
version: "1.0.2"
# 服务配置
server:

View File

@@ -90,7 +90,7 @@ func CreateManualBills(c *gin.Context) {
BillType: "manual",
TransactionID: "", // 手动账单不设置交易ID
MerchantOrderNo: "",
Time: t,
Time: model.LocalTime(t),
Category: input.Category,
Merchant: input.Merchant,
Description: input.Description,

View File

@@ -3,9 +3,70 @@ package model
import (
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/bsontype"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// LocalTime 自定义时间类型JSON 序列化时输出本地时间格式
type LocalTime time.Time
// MarshalJSON 实现 json.Marshaler 接口,输出本地时间格式
func (t LocalTime) MarshalJSON() ([]byte, error) {
tt := time.Time(t)
if tt.IsZero() {
return []byte(`""`), nil
}
// 输出格式: "2006-01-02 15:04:05"
return []byte(`"` + tt.Local().Format("2006-01-02 15:04:05") + `"`), nil
}
// UnmarshalJSON 实现 json.Unmarshaler 接口
func (t *LocalTime) UnmarshalJSON(data []byte) error {
s := string(data)
if s == `""` || s == "null" {
*t = LocalTime(time.Time{})
return nil
}
// 去掉引号
s = s[1 : len(s)-1]
// 尝试多种格式解析
formats := []string{
"2006-01-02 15:04:05",
"2006-01-02T15:04:05Z07:00",
"2006-01-02T15:04:05Z",
"2006-01-02",
}
for _, format := range formats {
if parsed, err := time.ParseInLocation(format, s, time.Local); err == nil {
*t = LocalTime(parsed)
return nil
}
}
return nil
}
// MarshalBSONValue 实现 bson.ValueMarshaler 接口
func (t LocalTime) MarshalBSONValue() (bsontype.Type, []byte, error) {
return bson.MarshalValue(time.Time(t))
}
// UnmarshalBSONValue 实现 bson.ValueUnmarshaler 接口
func (t *LocalTime) UnmarshalBSONValue(btype bsontype.Type, data []byte) error {
var tt time.Time
if err := bson.UnmarshalValue(btype, data, &tt); err != nil {
return err
}
*t = LocalTime(tt)
return nil
}
// Time 返回标准 time.Time
func (t LocalTime) Time() time.Time {
return time.Time(t)
}
// RawBill 原始账单记录(存储上传的原始数据)
type RawBill struct {
ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
@@ -23,7 +84,7 @@ type CleanedBill struct {
BillType string `bson:"bill_type" json:"bill_type"` // 账单类型: alipay/wechat
TransactionID string `bson:"transaction_id" json:"transaction_id"` // 交易订单号(用于去重)
MerchantOrderNo string `bson:"merchant_order_no" json:"merchant_order_no"` // 商家订单号(用于去重)
Time time.Time `bson:"time" json:"time"` // 交易时间
Time LocalTime `bson:"time" json:"time"` // 交易时间(本地时间格式)
Category string `bson:"category" json:"category"` // 交易分类
Merchant string `bson:"merchant" json:"merchant"` // 交易对方
Description string `bson:"description" json:"description"` // 商品说明

View File

@@ -157,7 +157,7 @@ func (r *Repository) CheckCleanedDuplicate(bill *model.CleanedBill) (bool, error
} else {
// 回退到 时间+金额+商户 组合判断
filter = bson.M{
"time": bill.Time,
"time": bill.Time.Time(), // 转换为 time.Time 用于 MongoDB 查询
"amount": bill.Amount,
"merchant": bill.Merchant,
}

View File

@@ -34,7 +34,7 @@ func checkDuplicate(ctx context.Context, bill *model.CleanedBill) bool {
} else {
// 回退到 时间+金额+商户 组合判断
filter = bson.M{
"time": bill.Time,
"time": bill.Time.Time(), // 转换为 time.Time 用于 MongoDB 查询
"amount": bill.Amount,
"merchant": bill.Merchant,
}
@@ -489,11 +489,11 @@ func saveCleanedBillsFromJSON(filePath, billType, sourceFile, uploadBatch string
}
// parseTime 解析时间字符串
// 使用本地时区解析,避免 UTC 时区问题
func parseTime(s string) time.Time {
// 使用本地时区解析,返回 model.LocalTime 类型
func parseTime(s string) model.LocalTime {
s = strings.TrimSpace(s)
if s == "" {
return time.Time{}
return model.LocalTime(time.Time{})
}
// 尝试多种时间格式(使用本地时区)
@@ -508,11 +508,11 @@ func parseTime(s string) time.Time {
for _, format := range formats {
if t, err := time.ParseInLocation(format, s, time.Local); err == nil {
return t
return model.LocalTime(t)
}
}
return time.Time{}
return model.LocalTime(time.Time{})
}
// parseAmount 解析金额字符串