Files
hotime/.cursor/plans/objtoobj_问题修正_a5e6aae7.plan.md
T
hoteas 6b689f3a1b chore(logging): 更新日志重定向与捕获功能
- 在 .gitignore 中添加调试日志文件的忽略规则,避免不必要的调试信息被提交
- 修改 application.go 中的 stdout 重定向逻辑,使用 log.CaptureStream 以支持更灵活的日志捕获
- 更新 README 文档,增加对日志重定向功能的说明
2026-07-13 07:45:51 +08:00

306 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: objtoobj 问题修正
overview: 对 objtoobj.go 中发现的 3 个运行时 panic bug、2 个错误处理 bug、多处性能问题进行修正,同时新增销量方案所需的 ObjToRoundFloat64 函数。
todos:
- id: fix-uint8-panics
content: 修复 ObjToFloat64、ObjToInt64、ObjToStr 三处 uint8 分支的 panic bug
status: completed
- id: fix-objtomap-shadow
content: 修复 ObjToMap default 分支变量遮蔽和赋值逻辑错误
status: completed
- id: fix-objtoslice-default
content: 修复 ObjToSlice default 分支 Marshal 错误被覆盖的问题
status: completed
- id: fix-objtobool-error
content: 修复 ObjToBool 成功转换时仍报错的问题
status: completed
- id: fix-objtotime-padding
content: 修复 ObjToTime 单位数日+时间组合时补零失败的 bug
status: completed
- id: fix-objtoceilint64-double
content: 修复 ObjToCeilInt64 双重 Ceil 和多余 ObjToInt64 调用
status: completed
- id: perf-type-switch-idiom
content: 所有函数改用 switch v := obj.(type) 惯用写法,消除重复类型断言
status: completed
- id: perf-objtotime
content: ObjToTime 中 ObjToStr(tInt) 只调用一次并缓存
status: completed
- id: perf-objtoslice-string
content: ObjToSlice []string 分支类型断言只做一次
status: completed
- id: perf-objtostr-types
content: ObjToStr 补充 float32、bool 等常见类型的直接转换
status: completed
- id: perf-objtomaparray-cap
content: ObjToMapArray 预分配切片容量
status: completed
- id: add-roundfloat64
content: 新增 ObjToRoundFloat64,并在 map.go、obj.go 中新增对应 Get/To 方法
status: completed
isProject: false
---
# objtoobj.go 问题审查与修正计划
修改文件:[common/objtoobj.go](d:\work\hotimev1.5\common\objtoobj.go)
---
## 一、严重 Buguint8 vs []byte 类型混淆,可能 panic
开发者原意是处理数据库 driver 返回的 `[]uint8`(即 `[]byte`),但 Go 中 `uint8`(单字节)和 `[]uint8`/`[]byte`(字节数组)是完全不同的类型,type switch 不会互相匹配。
当前 `Row()` 方法(query.go:380)已经用 reflect 将 `[]uint8` 转为 `string`,所以正常 DB 流程不会触发这些分支。但其他非 DB 数据源可能传入 `[]byte`,此时会走 `default` 分支而非预期的 `uint8` 分支。
### Bug 1: `ObjToFloat64` — `case uint8` 应为 `case []byte`
```208:215:d:\work\hotimev1.5\common\objtoobj.go
case uint8:
value, e := strconv.ParseFloat(obj.(string), 64)
```
**修正**:改为 `case []byte`,用 `string(v)` 转换后再 ParseFloat
```go
case []byte:
value, e := strconv.ParseFloat(string(obj.([]byte)), 64)
if e != nil {
v = float64(0)
err = e
} else {
v = value
}
```
### Bug 2: `ObjToInt64` — `case uint8` 应为 `case []byte`
```279:286:d:\work\hotimev1.5\common\objtoobj.go
case uint8:
value, e := StrToInt(obj.(string))
```
**修正**:改为 `case []byte`,用 `string(v)` 转换后再 StrToInt
```go
case []byte:
value, e := StrToInt(string(obj.([]byte)))
if e != nil {
v = int64(0)
err = e
} else {
v = int64(value)
}
```
### Bug 3: `ObjToStr` — `case uint8` 应改为正确处理单字节
```341:342:d:\work\hotimev1.5\common\objtoobj.go
case uint8:
str = obj.(string)
```
`ObjToStr` 已有 `case []byte` 分支正确处理字节数组。这里的 `case uint8` 应正确处理单字节值:
**修正**:改为 `str = strconv.Itoa(int(obj.(uint8)))`
> 这三处 bug 目前未爆发,因为 `Row()` 已将 `[]uint8` 转为 `string`。但修正后能覆盖非 DB 数据源直接传入 `[]byte` 的场景,更加健壮。
---
## 二、逻辑 Bug
### Bug 4: `ObjToMap` default 分支 — 变量遮蔽导致错误丢失
```33:44:d:\work\hotimev1.5\common\objtoobj.go
default:
data, err := json.Marshal(obj)
if err != nil {
err = errors.New("没有合适的转换对象!" + err.Error())
v = nil
}
v = Map{}
e := json.Unmarshal(data, &v)
```
第 34 行的 `data, err := json.Marshal(obj)` 使用了 `:=`,创建了一个**新的局部变量 `err`**,遮蔽了第 14 行声明的外部 `err`。这意味着:
- Marshal 失败时,外部 `err` 仍然是 nil,错误信息丢失
- 第 38 行设 `v = nil`,但第 39 行又立刻把 `v` 重新赋值为 `Map{}`,然后用坏数据尝试 Unmarshal
**修正**:用 `=` 代替 `:=`,并在 Marshal 失败时 break。
### Bug 5: `ObjToSlice` default 分支 — Marshal 错误被覆盖
```86:91:d:\work\hotimev1.5\common\objtoobj.go
default:
v = Slice{}
var data []byte
data, err = json.Marshal(obj)
err = json.Unmarshal(data, &v)
```
第 89 行 `err = json.Marshal(obj)` 即使失败,第 90 行也立刻用 `err = json.Unmarshal(data, &v)` 覆盖了前一个错误。当 Marshal 失败时 `data` 为空,Unmarshal 也会失败,但丢失了原始的错误信息。
**修正**Marshal 失败时直接 break,不再尝试 Unmarshal。
### Bug 6: `ObjToBool` — 成功转换时也报错
```307:330:d:\work\hotimev1.5\common\objtoobj.go
default:
toInt := ObjToInt(obj)
if toInt != 0 {
v = true
}
err = errors.New("没有合适的转换对象!")
```
当传入 `1`int 类型)时,`ObjToInt` 成功返回 1`v = true`,但仍然设置了 `err`。逻辑上这不应该报错。
**修正**:只在 `ObjToInt` 也无法转换时才报错。
---
### Bug 7: `ObjToTime` 日期补零逻辑缺陷
```108:121:d:\work\hotimev1.5\common\objtoobj.go
timeNewStrs := strings.Split(tStr, "-")
for _, v := range timeNewStrs {
if len(v) == 1 {
v = "0" + v
}
}
```
按 `"-"` 分割 `"2006-1-2 15:04:05"` 得到 `["2006", "1", "2 15:04:05"]`。第三段 `"2 15:04:05"` 长度 > 1 不会补零,最终得到 `"2006-01-2 15:04:05"``time.Parse` 会失败。
**修正**:分割前先按空格分离日期和时间部分,分别处理后再拼接。或者改用 `strings.TrimLeft` 对每段的纯日期部分补零。
### Bug 8: `ObjToCeilInt64` 双重 Ceil + 多余 ObjToInt64
```238:241:d:\work\hotimev1.5\common\objtoobj.go
func ObjToCeilInt64(obj interface{}, e ...*Error) int64 {
f := ObjToCeilFloat64(obj, e...) // 内部已 math.Ceil
return ObjToInt64(math.Ceil(f)) // 又 Ceil + 走完整 type switch
}
```
**修正**:改为 `return int64(f)`,因为 `ObjToCeilFloat64` 已经返回了 Ceil 后的值。
---
## 三、性能问题
### P1: `ObjToTime` 重复调用 `ObjToStr(tInt)` 多达 5 次
```152:177:d:\work\hotimev1.5\common\objtoobj.go
if len(ObjToStr(tInt)) > 16 {
// ...
} else if len(ObjToStr(tInt)) > 13 {
// ...
} else if len(ObjToStr(tInt)) > 10 {
// ...
} else if len(ObjToStr(tInt)) > 9 {
// ...
} else if len(ObjToStr(tInt)) > 3 {
t, e := time.Parse("2006", ObjToStr(tInt))
```
每次 `ObjToStr(tInt)` 都会执行 `strconv.FormatInt` + 分配新字符串。应在循环外只调用一次,存入变量复用。
### P2: `ObjToSlice` 的 `[]string` 分支重复类型断言
```78:81:d:\work\hotimev1.5\common\objtoobj.go
case []string:
v = Slice{}
for i := 0; i < len(obj.([]string)); i++ {
v = append(v, obj.([]string)[i])
}
```
循环中每次 `obj.([]string)` 都做了类型断言。应先断言一次存入变量:`ss := obj.([]string)`。
### P3: `ObjToStr` 缺少 `float32`、`bool` 等常见类型直接转换
`float32`、`bool`、`int32` 等类型会走 `default` 分支用 `json.MarshalIndent`,比直接转换慢 100 倍以上。
**修正**:补充常见类型的 case:
```go
case float32:
str = strconv.FormatFloat(float64(obj.(float32)), 'f', -1, 32)
case bool:
str = strconv.FormatBool(obj.(bool))
```
### P4: `ObjToStr` default 分支使用 `json.MarshalIndent`
`MarshalIndent` 比 `Marshal` 慢 20-30%,且产生更大的输出。但由于此改动可能影响现有依赖格式化输出的调用方,标记为**可选优化**。
### P5: 所有函数应改用 `switch v := obj.(type)` 惯用写法
当前每个 case 都重复做类型断言(如 `obj.(int)`),改为 Go 惯用写法可消除所有冗余断言:
```go
// 改前
switch obj.(type) {
case int:
v = int64(obj.(int))
// 改后
switch val := obj.(type) {
case int:
v = int64(val)
```
### P6: `ObjToMapArray` 未预分配切片容量
`res := []Map{}` 应改为 `res := make([]Map, 0, len(s))`。
### P7: `StrToMap` / `StrToSlice` 参数名 `string` 遮蔽内置类型
参数名应改为 `s` 或 `jsonStr`,避免遮蔽 Go 内置 `string` 类型。
---
## 四、结合销量方案需新增的函数
根据[销量支持小数方案](d:\work\your-app.cursor\plans\销量支持小数方案_92094ec9.plan.md)第二步要求,需在本文件新增 `ObjToRoundFloat64`
```go
func ObjToRoundFloat64(obj interface{}, precision int, e ...*Error) float64 {
f := ObjToFloat64(obj, e...)
if precision < 0 {
return f
}
pow := math.Pow10(precision)
return math.Round(f*pow) / pow
}
```
同时需在 [common/map.go](d:\work\hotimev1.5\common\map.go) 新增 `GetRoundFloat64`,在 [common/obj.go](d:\work\hotimev1.5\common\obj.go) 新增 `ToRoundFloat64`。
---
## 五、修正摘要
| # | 类型 | 位置 | 问题 | 修正 |
| --- | ---- | ------------------- | ------------------------- | ---------------------------- |
| 1 | 类型混淆 | ObjToFloat64 uint8 | case uint8 应为 case []byte | 改为 case []byte + string() 解析 |
| 2 | 类型混淆 | ObjToInt64 uint8 | case uint8 应为 case []byte | 改为 case []byte + string() 解析 |
| 3 | 类型混淆 | ObjToStr uint8 | uint8 断言为 string 会 panic | 改为 strconv.Itoa(int(...)) |
| 4 | 逻辑 | ObjToMap default | := 遮蔽外部 err,v 赋值逻辑错乱 | 用 = 并加 break |
| 5 | 逻辑 | ObjToSlice default | Marshal 错误被 Unmarshal 覆盖 | 失败时 break |
| 6 | 逻辑 | ObjToBool default | 成功转换也报错 | 转换成功不报错 |
| 7 | 逻辑 | ObjToTime 日期补零 | 单位数日+时间时补不到零 | 分离日期时间部分再补零 |
| 8 | 逻辑 | ObjToCeilInt64 | 双重 Ceil + 多余 type switch | 改为 int64(f) |
| 9 | 性能 | ObjToTime | ObjToStr 重复调用 5 次 | 缓存结果 |
| 10 | 性能 | ObjToSlice []string | 循环内重复类型断言 | 断言一次复用 |
| 11 | 性能 | 所有函数 | 未用 switch v := obj.(type) | 改用惯用写法消除冗余断言 |
| 12 | 性能 | ObjToStr | 缺少 float32/bool 等常见类型 | 补充直接转换 case |
| 13 | 性能 | ObjToMapArray | 切片未预分配容量 | make([]Map, 0, len(s)) |
| 14 | 规范 | StrToMap/StrToSlice | 参数名 string 遮蔽内置类型 | 改为 s 或 jsonStr |
| 15 | 新增 | ObjToRoundFloat64 | 销量方案需要 | 新增函数 |