refactor(app): 优化 URL 处理和 JSON 解析逻辑

- 更新应用程序处理程序中的 URL 赋值逻辑,确保静态文件使用原始路径
- 修改缓存数据库的 JSON 解析方法,使用 JsonToObj 函数替代 json.Unmarshal,提升代码可读性和性能
- 在 Map 和 Slice 类型中新增获取四舍五入浮点数的方法,增强数据处理能力
- 在 Obj 类型中添加四舍五入功能,支持精度控制
- 改进数据库查询结果的处理逻辑,确保数据类型的准确性和一致性
- 优化日志格式设置,增强日志信息的可读性
This commit is contained in:
2026-03-10 23:44:41 +08:00
parent bf9bb5807d
commit fab7931d3c
15 changed files with 4116 additions and 147 deletions
@@ -0,0 +1,331 @@
---
name: JSON Path 查询支持
overview: 在现有 Dialect 接口和 WHERE 条件解析引擎上扩展,使 MySQL 和 SQLite 支持类似 PgSQL 的 `data["a"]["b"]["c"]` JSON 路径查询与操作语法,所有三种数据库共享统一的上层 API。
todos:
- id: dialect-json-interface
content: 在 db/dialect.go 的 Dialect 接口新增 JSONExtract / JSONSet / JSONContains / JSONArrayLength 四个方法
status: pending
- id: dialect-json-mysql
content: 实现 MySQLDialect 的三个 JSON 方法,处理 JSON_EXTRACT / JSON_SET / JSON_UNQUOTE 等
status: pending
- id: dialect-json-sqlite
content: 实现 SQLiteDialect 的三个 JSON 方法,使用小写 json_extract / json_set
status: pending
- id: dialect-json-pgsql
content: "实现 PostgreSQLDialect 的三个 JSON 方法,使用 -> / #>> / jsonb_set 语法"
status: pending
- id: where-json-parse
content: 新增 parseJSONPathKey()/parseJSONPathFromName() 工具函数,同时支持 col["key"] 和 col['key'] 两种引号格式,以及数字索引 [0],解析出列名、[]string 路径键、操作符后缀
status: pending
- id: where-json-cond
content: 在 db/where.go 的 varCond() 开头最优先调用 parseJSONPathKey,分发到 jsonPathCond()jsonPathCond 对现有操作符复用比较逻辑,新增 [?]/[!?]/[@]/[!@]/[len]/[len>] 等分支
status: pending
- id: crud-json-update
content: 在 db/crud.go 的 Update() SET 构建循环中检测 JSON 路径列名生成 JSONSet 表达式,在 Select() 的 Slice 字段循环中检测 JSON 路径生成 JSONExtract 表达式(修复两处 ProcessColumnNoPrefix 误处理)
status: pending
isProject: false
---
# MySQL/SQLite JSON 路径查询支持方案
## 背景与现状
现有 ORM 已有 `Dialect` 接口抽象三种数据库差异,`where.go``[operator]` 后缀语法解析条件。扩展的核心思路:**在解析 key 时识别 JSON 路径记法,委托给 Dialect 生成对应函数调用**。
## 三种数据库 JSON 能力深度对比
### 1. JSON 值提取(WHERE 比较场景)
| | MySQL 5.7+ | SQLite 3.9+ | PostgreSQL |
| ---------- | ------------------------------------------------------- | ------------------------------ | ----------------- |
| 单层提取(返回文本) | `JSON_UNQUOTE(JSON_EXTRACT(col,'$.a'))``col->>'$.a'` | `json_extract(col,'$.a')` | `col->>'a'` |
| 多层嵌套(返回文本) | `JSON_UNQUOTE(JSON_EXTRACT(col,'$.a.b'))` | `json_extract(col,'$.a.b')` | `col#>>'{a,b}'` |
| 数组索引 | `JSON_EXTRACT(col,'$.arr[0]')` | `json_extract(col,'$.arr[0]')` | `col#>>'{arr,0}'` |
| 路径格式 | `$.a.b` | `$.a.b`(与MySQL相同) | `{a,b}`#>> 方式) |
**关键差异**MySQL 的 `JSON_EXTRACT` 对字符串返回值带外层双引号(如 `"北京"` 而非 `北京`),与普通字符串 `= ?` 比较会失败,**必须包一层 `JSON_UNQUOTE()`**。SQLite 和 PostgreSQL`->>`/`#>>`)则直接返回文本,无此问题。
### 2. JSON 局部更新(UPDATE SET 场景)
| | MySQL 5.7+ | SQLite 3.9+ | PostgreSQL |
| ----- | ------------------------- | ------------------------- | ------------------------------------------ |
| 语法 | `JSON_SET(col,'$.a.b',?)` | `json_set(col,'$.a.b',?)` | `jsonb_set(col,'{a,b}',to_jsonb(?::text))` |
| 路径格式 | `$.a.b` | `$.a.b`(与MySQL相同) | `'{a,b}'` 数组字面量 |
| 值的类型 | 直接绑定参数 | 直接绑定参数 | 字符串需 `to_jsonb(?::text)`,数字可 `?::jsonb` |
| 列类型限制 | 无(TEXT 也行) | 无(TEXT 列) | **必须是 `jsonb` 列**`json` 列无 `jsonb_set` |
**关键差异**MySQL 与 SQLite 的语法几乎完全一致(函数名大小写不同而已)。PostgreSQL 只有 `jsonb_set`,要求列类型为 `jsonb`,且字符串值必须转为 JSON 格式(`to_jsonb(?::text)``'"value"'::jsonb`)。
### 3. JSON 数组包含(`[@]` 操作符)
| | MySQL 5.7+ | SQLite 3.9+ | PostgreSQL |
| ------ | --------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------ |
| SQL 形式 | `JSON_CONTAINS(JSON_EXTRACT(col,'$.path'),JSON_QUOTE(?))` | `EXISTS(SELECT 1 FROM json_each(col,'$.path') WHERE value=?)` | `col#>'{path}' @> to_jsonb(?::text)` |
| 结构类型 | 函数调用(内联) | **EXISTS 子查询**(结构不同) | 中缀操作符(内联) |
**最大兼容难点**:SQLite 没有内联的包含函数,必须生成 EXISTS 子查询,SQL 结构与另外两种完全不同。`JSONContains()` 方法在 SQLite 下返回的是一段 `EXISTS(...)` 表达式。
### 4. 路径格式统一转换规则
ORM 内部统一使用 `[]string{"a","b","c"}` 表示路径键(数字索引用字符串 `"0"` 表示),各 Dialect 自行转换:
```
内部路径: ["address","city"]
→ MySQL/SQLite: $.address.city
→ PostgreSQL: {address,city}
内部路径: ["tags","0"](数组索引)
→ MySQL/SQLite: $.tags[0]
→ PostgreSQL: {tags,0}
```
## 统一 API 语法设计
### Key 写法(单引号/双引号两种都支持)
```go
// 双引号风格(JSON 标准,推荐,需用反引号字符串)
common.Map{ `profile["addr"]["city"]`: "北京" }
// 单引号风格(普通字符串也能写)
common.Map{ "profile['addr']['city']": "北京" }
```
两种写法解析结果完全一致,内部统一转为 `$.addr.city`
### 操作符设计:复用现有 + 新增 JSON 专属
**所有现有操作符均可接在 JSON 路径后直接使用**`=` / `>` / `<` / `>=` / `<=` / `!=` / `LIKE` / `IN` / `NOT IN` / `BETWEEN` / `IS NULL`):
```go
db.Select("user", common.Map{
`profile["age"]`: 18, // = 18
`profile["age"][>]`: 18, // > 18
`profile["age"][<>]`: []int{18, 30}, // BETWEEN
`profile["name"][~]`: "张", // LIKE %张%
`profile["tags"][0]`: "vip", // 数组第一个元素 = vip
`profile["addr"]["city"]`: "北京", // 多级嵌套
`profile["score"]`: nil, // IS NULL
})
```
**新增 JSON 专属操作符**(现有体系无对应):
| 操作符 | 含义 | MySQL | SQLite | PgSQL |
| --------- | ----------- | -------------------------------- | ------------------------ | ------------------------- |
| `[?]` | 路径存在(非NULL) | `IS NOT NULL` | `IS NOT NULL` | `? ?` |
| `[!?]` | 路径不存在 | `IS NULL` | `IS NULL` | `NOT (? ?)` |
| `[@]` | 数组/对象包含某值 | `JSON_CONTAINS(col,?,'$.path')` | `json_each` 子查询 | `col @> ?::jsonb` |
| `[!@]` | 不包含 | `NOT JSON_CONTAINS(...)` | 子查询 NOT EXISTS | `NOT (col @> ?)` |
| `[len]` | 数组长度 = ? | `JSON_LENGTH(JSON_EXTRACT(...))` | `json_array_length(...)` | `jsonb_array_length(...)` |
| `[len>]` | 数组长度 > ? | 同上 | 同上 | 同上 |
| `[len<]` | 数组长度 < ? | 同上 | 同上 | 同上 |
| `[len>=]` | 数组长度 >= ? | 同上 | 同上 | 同上 |
| `[len<=]` | 数组长度 <= ? | 同上 | 同上 | 同上 |
```go
// 新操作符使用示例
db.Select("user", common.Map{
`profile["vip"][?]`: nil, // vip 字段存在
`profile["tags"][@]`: "admin", // tags 数组包含 "admin"
`profile["friends"][len>]`: 5, // friends 数组长度 > 5
`profile["items"][len]`: 3, // items 数组长度 = 3
})
// UPDATE 局部更新 JSON 字段
db.Update("user", common.Map{
`profile["age"]`: 20,
`profile["addr"]["city"]`: "上海",
}, common.Map{"id": 1})
```
## 与现有代码的兼容性分析
### 三处具体冲突点
**冲突 1 — WHERE `varCond` 的 `[...]` 分支**`[db/where.go:225](db/where.go)`
现有逻辑:只要 key 含 `[` 且末尾是 `]` 就进分支,取末尾 3/4 字符匹配已知操作符。
```
profile["age"] → 末尾3字符 e"] → 无匹配 → handleDefaultCondition
→ ProcessColumn("profile[\"age\"]") → 被加引号 → WRONG SQL
profile["age"][>] → 末尾3字符 [>] → MATCHstrips → ProcessColumn("profile[\"age\"]")
→ 被加引号 → WRONG SQL
profile["age"][@] → 末尾3字符 [@] → 无匹配(新操作符) → handleDefaultCondition
→ ProcessColumn("profile[\"age\"][@]") → WRONG SQL
```
**冲突 2 — UPDATE `ProcessColumnNoPrefix` 调用**`[db/crud.go:629](db/crud.go)`
```go
query += processor.ProcessColumnNoPrefix(k) + "=" + vstr // 第629行
```
`k = profile["age"]``QuoteIdentifier` 加引号 → ``profile["age"]`=?` → 无效 SQL。
**冲突 3 — SELECT slice 字段**`[db/crud.go:109](db/crud.go)`
无 `.` 且无 `AS` 的字段走 `ProcessColumnNoPrefix``profile["age"]` 被当成普通列名加引号。
### 已有格式不受影响的验证
- `profile[>]` — 末尾 `[>]` 匹配现有 switch → 先 strips 再调 `ProcessColumn("profile")` → **正常**
- `user.name` — `ProcessColumn` 有 `.` 分支 → **正常**
- ``user`.name` — `stripQuotes` 后同上 → **正常**
- `"name,age"` 字符串字段 — `ProcessFieldList` 的正则不会错误匹配无 `.` 的字段 → **正常**
- `Slice{"name","age"}` — 无 JSON 路径字符 → **正常**
---
## 实现方案
### 核心设计:`parseJSONPathKey` 最先拦截
区分 JSON 路径括号和操作符括号的关键:**括号内容**
- JSON 路径段:`["xxx"]`、`['xxx']`、`[0]`(引号字符串或纯数字)
- 操作符段:`[>]`、`[@]`、`[len>]`(无引号,含符号字符)
**第一步:新增路径工具函数(`[db/where.go](db/where.go)` 顶部或独立 `jsonpath.go`**
```go
// parseJSONPathKey 解析 JSON 路径 key,识别规则:
// ^(\w[\w.]*) 列名
// ((?:\["[^"]*"\]|\['[^']*'\]|\[\d+\])+) JSON 路径段(双引号/单引号/数字索引)
// (\[.*\])?$ 可选尾部操作符 [>] [@] [len>] 等
//
// 示例:
// profile["age"][>] → col=profile, keys=["age"], op="[>]", ok=true
// profile['addr']['city'] → col=profile, keys=["addr","city"], op="", ok=true
// profile["tags"][0][@] → col=profile, keys=["tags","0"], op="[@]", ok=true
// profile[>] → ok=false(操作符括号,非 JSON 路径)
func parseJSONPathKey(k string) (col string, pathKeys []string, opSuffix string, ok bool)
```
**第二步:`varCond()` 开头最先调用**`[db/where.go](db/where.go)`
```go
func (that *HoTimeDB) varCond(k string, v interface{}) (string, []interface{}) {
// ★ 最优先:JSON 路径检测,完全绕过现有 [...] 分支
if col, pathKeys, opSuffix, ok := parseJSONPathKey(k); ok {
return that.jsonPathCond(col, pathKeys, opSuffix, v)
}
// 以下全部保持不变 ↓
...
}
```
`jsonPathCond` 根据 opSuffix 分发:
| opSuffix | 动作 |
| --------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| `""` / 现有操作符(`[>]`/`[<]`/`[>=]`/`[<=]`/`[!]`/`[~]`/`[!~]`/`[~!]`/`[<>]`/`[><]`/`[#]` 等) | 调用 `dialect.JSONExtract(col, pathKeys)` 得到提取表达式,替换原来的列名,复用现有比较逻辑 |
| `[?]` | `JSONExtract(...) IS NOT NULL` |
| `[!?]` | `JSONExtract(...) IS NULL` |
| `[@]` | `dialect.JSONContains(col, pathKeys, "?")` |
| `[!@]` | `NOT` + JSONContains |
| `[len]` | `dialect.JSONArrayLength(col, pathKeys)` + `=?` |
| `[len>]` / `[len<]` / `[len>=]` / `[len<=]` | JSONArrayLength + 对应比较符 |
**第三步:扩展 `Dialect` 接口(`[db/dialect.go](db/dialect.go)`**
新增 4 个方法,内部路径统一用 `[]string`(数字索引用字符串 `"0"`):
```go
// MySQL: JSON_UNQUOTE(JSON_EXTRACT(`col`, '$.a.b'))
// SQLite: json_extract("col", '$.a.b')
// PgSQL: "col"#>>'{a,b}'
JSONExtract(quotedColumn string, pathKeys []string) string
// MySQL: JSON_SET(`col`, '$.a.b', ?)
// SQLite: json_set("col", '$.a.b', ?)
// PgSQL: jsonb_set("col", '{a,b}', to_jsonb($N::text)) -- isStr=true
// jsonb_set("col", '{a,b}', $N::jsonb) -- isStr=false
JSONSet(quotedColumn string, pathKeys []string, placeholder string, isStr bool) string
// MySQL: JSON_CONTAINS(JSON_EXTRACT(`col`,'$.path'), JSON_QUOTE(?))
// SQLite: EXISTS (SELECT 1 FROM json_each("col", '$.path') WHERE value=?)
// PgSQL: "col"#>'{path}' @> to_jsonb($N::text)
JSONContains(quotedColumn string, pathKeys []string, placeholder string) string
// MySQL: JSON_LENGTH(JSON_EXTRACT(`col`, '$.path'))
// SQLite: json_array_length("col", '$.path')
// PgSQL: jsonb_array_length("col"#>'{path}')
JSONArrayLength(quotedColumn string, pathKeys []string) string
```
**第四步:修复 UPDATE SET`[db/crud.go:621](db/crud.go)`**
在 `[#]` 检查之后、`ProcessColumnNoPrefix` 之前增加 JSON 路径检测:
```go
} else if col, pathKeys, ok := parseJSONPathFromName(k); ok {
quotedCol := processor.ProcessColumnNoPrefix(col)
isStr := v != nil && reflect.TypeOf(v).Kind() == reflect.String
ph := that.Dialect.Placeholder(len(qs) + 1)
query += quotedCol + "=" + that.Dialect.JSONSet(quotedCol, pathKeys, ph, isStr)
qs = append(qs, v)
```
**第五步:修复 SELECT slice 字段(`[db/crud.go:101](db/crud.go)`**
在 `Slice` 字段循环中,对每个字段先做 JSON 路径检测:
```go
if col, pathKeys, ok := parseJSONPathFromName(stripAlias(k)); ok {
alias := extractAlias(k)
quotedCol := processor.ProcessColumnNoPrefix(col)
query += " " + that.Dialect.JSONExtract(quotedCol, pathKeys) + alias + " "
} else if strings.Contains(k, " AS ") || strings.Contains(k, ".") {
query += " " + processor.ProcessFieldList(k) + " "
} else {
query += " " + processor.ProcessColumnNoPrefix(k) + " "
}
```
字符串字段(如 `"profile[\"age\"] AS age, name"`)暂不处理 JSON 路径,用户可用原有原始 SQL 写法。
## 支持的能力范围
- **WHERE 条件**:复用全部现有 14 种操作符(`=` / `!=` / `>` / `<` / `>=` / `<=` / `LIKE` / `IN` / `NOT IN` / `BETWEEN` / `IS NULL` 等)+ 新增 `[?]` / `[!?]` / `[@]` / `[!@]` / `[len]` / `[len>]` / `[len<]` / `[len>=]` / `[len<=]`
- **UPDATE SET**JSON 路径局部更新,原列其他字段保持不变
- **SELECT slice 字段**`Slice{profile["age"] AS score}` 支持 JSON 路径
- **SELECT 字符串字段**:暂不处理,用户写原始 SQL 或 Slice 形式
- **数组索引**`["arr"][0]` → `$.arr[0]`
- **多级嵌套**:任意深度
- **引号兼容**`["key"]` 和 `['key']` 两种写法完全等效
- **原有格式零影响**`user.name` / ``user`.name` / `"name,age"` / `Slice{"name","age"}` 全部不变
## 兼容性注意事项
| 级别 | 问题 | 处理方式 |
| --- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| 高 | **MySQL 必须 JSON_UNQUOTE**`JSON_EXTRACT` 返回字符串含外层引号,直接 `= ?` 失败 | `MySQLDialect.JSONExtract()` 统一包 `JSON_UNQUOTE(JSON_EXTRACT(...))` |
| 高 | **PostgreSQL 只支持 `jsonb` 列**`jsonb_set` 不支持 `json` 类型列 | 文档约定:使用 JSON 路径操作的列需建为 `jsonb` 类型;或调用方在列名后加 `::jsonb` 强转 |
| 高 | **SQLite `[@]` 生成 EXISTS 子查询**:结构与 MySQL/PgSQL 完全不同 | `SQLiteDialect.JSONContains()` 返回 `EXISTS(SELECT 1 FROM json_each(...) WHERE value=?)` 片段,在 `jsonPathCond` 里直接拼入 WHERE |
| 中 | **PgSQL 更新时值的类型转换**:字符串需 `to_jsonb(?::text)`,数值/布尔用 `?::jsonb` | `JSONSet()` 接受 `isStr bool` 参数,由 `jsonPathCond` 根据 Go 值类型传入 |
| 中 | **MySQL 版本**JSON 函数需 5.7+`JSON_OVERLAPS` 需 8.0+ | 先实现 5.7 兼容的基础操作符,`[!@]` 用 `NOT JSON_CONTAINS` 实现 |
| 低 | **SQLite 版本**:需 3.9+`go-sqlite3` 默认已启用 json1 扩展 | 无需特殊处理 |
| 低 | **路径不存在返回 NULL**:三种数据库行为一致 | 调用方自行处理 NULL 判断,或用 `[?]` 操作符先做存在性检查 |
## 涉及文件
- `[db/dialect.go](db/dialect.go)` — 接口扩展 + 三种方言实现(主要改动)
- `[db/where.go](db/where.go)` — 新增 JSON 路径 key 检测与条件生成
- `[db/crud.go](db/crud.go)` — UPDATE SET 子句 JSON 路径支持
@@ -0,0 +1,305 @@
---
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\xbc.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 | 销量方案需要 | 新增函数 |
@@ -0,0 +1,311 @@
---
name: Row方法方案审查与修正
overview: 修改 4 个文件:common/objtoobj.go(新增 JsonToObj + 增强 ObjToMap/ObjToSlice)、common/map.go(增强 JsonToMap)、db/query.goRow 重写)、cache/cache_db.go(用 JsonToObj 替换 json.Unmarshal)。
todos:
- id: add-json-safe
content: 在 objtoobj.go 新增 JsonToObj(公共) + fixJsonNumbers(私有),增强 ObjToMap/ObjToSlice 的 4 处 json.Unmarshal
status: completed
- id: fix-json-to-map
content: 增强 map.go 的 JsonToMap,使用 UseNumber + fixJsonNumbers
status: completed
- id: fix-row-method
content: 重写 db/query.go 的 Row() + 新增辅助函数 + 更新 import
status: completed
- id: fix-cache-db
content: cache/cache_db.go 的 3 处 json.Unmarshal 改用 JsonToObj
status: completed
isProject: false
---
# Row() + JSON 反序列化类型保留修正方案
## 修改文件一览
| 文件 | 改动 |
| ------------------ | --------------------------------------------------------------------------------- |
| common/objtoobj.go | 新增 `JsonToObj` + `fixJsonNumbers`;增强 `ObjToMap``ObjToSlice` 的 4 处 json.Unmarshal |
| common/map.go | 增强 `Map.JsonToMap` |
| db/query.go | 重写 Row() + 新增辅助函数 + 更新 import |
| cache/cache_db.go | 3 处 json.Unmarshal 改用 `JsonToObj` |
---
## 文件 1common/objtoobj.go
### 新增 JsonToObj 和 fixJsonNumbers
在文件 `StrToMap` 之前(约第 378 行前)新增,需要在 import 中添加 `"bytes"`
```go
// JsonToObj 将 JSON 字符串反序列化为 interface{},保留数字原始类型。
// 标准 json.Unmarshal 会把所有 JSON 数字变成 float64,本函数保留整数为 int64、小数为 float64。
// JsonToObj(`{"id":2,"price":1.20}`) → Map{"id": int64(2), "price": float64(1.2)}
// JsonToObj(`[1, 2.5, "a"]`) → Slice{int64(1), float64(2.5), "a"}
func JsonToObj(jsonStr string) (interface{}, error) {
dec := json.NewDecoder(bytes.NewReader([]byte(jsonStr)))
dec.UseNumber()
var result interface{}
if err := dec.Decode(&result); err != nil {
return nil, err
}
return fixJsonNumbers(result), nil
}
// fixJsonNumbers 递归将 json.Number 转为 int64 或 float64
func fixJsonNumbers(v interface{}) interface{} {
switch val := v.(type) {
case json.Number:
if n, err := val.Int64(); err == nil {
return n
}
if f, err := val.Float64(); err == nil {
return f
}
return string(val)
case map[string]interface{}:
for k, item := range val {
val[k] = fixJsonNumbers(item)
}
return val
case []interface{}:
for i, item := range val {
val[i] = fixJsonNumbers(item)
}
return val
default:
return val
}
}
```
### 增强 ObjToMap2 处 json.Unmarshal
**位置 1** -- string case(约第 27-31 行):
```go
// 修改前:
case string:
v = Map{}
e := json.Unmarshal([]byte(obj.(string)), &v)
if e != nil {
err = errors.New("没有合适的转换对象!" + e.Error())
v = nil
}
// 修改后:
case string:
result, e := JsonToObj(val)
if e != nil {
err = errors.New("没有合适的转换对象!" + e.Error())
v = nil
} else if m, ok := result.(map[string]interface{}); ok {
v = m
} else {
err = errors.New("没有合适的转换对象!")
v = nil
}
```
**位置 2** -- default case(约第 34-44 行):
```go
// 修改前:
default:
data, err := json.Marshal(obj)
if err != nil { ... }
v = Map{}
e := json.Unmarshal(data, &v)
if e != nil { ... }
// 修改后:
default:
data, e2 := json.Marshal(obj)
if e2 != nil {
err = errors.New("没有合适的转换对象!" + e2.Error())
v = nil
} else {
result, e3 := JsonToObj(string(data))
if e3 != nil {
err = errors.New("没有合适的转换对象!" + e3.Error())
v = nil
} else if m, ok := result.(map[string]interface{}); ok {
v = m
} else {
err = errors.New("没有合适的转换对象!")
v = nil
}
}
```
### 增强 ObjToSlice2 处 json.Unmarshal
**位置 1** -- string case(约第 82-83 行):
```go
// 修改前:
case string:
v = Slice{}
err = json.Unmarshal([]byte(obj.(string)), &v)
// 修改后:
case string:
result, e := JsonToObj(val)
if e != nil {
err = e
} else if s, ok := result.([]interface{}); ok {
v = s
} else {
err = errors.New("没有合适的转换对象!")
}
```
**位置 2** -- default case(约第 86-91 行):
```go
// 修改前:
default:
v = Slice{}
var data []byte
data, err = json.Marshal(obj)
err = json.Unmarshal(data, &v)
// 修改后:
default:
data, e2 := json.Marshal(obj)
if e2 != nil {
err = e2
} else {
result, e3 := JsonToObj(string(data))
if e3 != nil {
err = e3
} else if s, ok := result.([]interface{}); ok {
v = s
} else {
err = errors.New("没有合适的转换对象!")
}
}
```
---
## 文件 2common/map.go
### 增强 JsonToMap(第 183-189 行)
需要在 import 中添加 `"bytes"`
```go
// 修改前:
func (that Map) JsonToMap(jsonStr string, err ...*Error) {
e := json.Unmarshal([]byte(jsonStr), &that)
if e != nil && len(err) != 0 {
err[0].SetError(e)
}
}
// 修改后:
func (that Map) JsonToMap(jsonStr string, err ...*Error) {
dec := json.NewDecoder(bytes.NewReader([]byte(jsonStr)))
dec.UseNumber()
e := dec.Decode(&that)
if e != nil {
if len(err) != 0 {
err[0].SetError(e)
}
return
}
for k, v := range that {
that[k] = fixJsonNumbers(v)
}
}
```
这里 `fixJsonNumbers` 定义在 `objtoobj.go` 中,同属 `common` 包,可以直接调用。
影响链路:
- `StrToMap(s)` → 调用 `JsonToMap(s)`**自动修复**
- `Map.JsonToMap(s)`**直接修复**
---
## 文件 3db/query.go -- Row() 重写
(内容与上一版相同,此处只列关键点)
### import 变更
移除 `"reflect"`,新增 `"math"``"strconv"`
### 新增辅助函数
`dbTypeUnknown/dbTypeInteger/dbTypeFloat/dbTypeDecimal` 常量 + `classifyDBType` + `getDecimalScale` + `getFloatScale` + `fixFloatValue` + `roundFloat` + `convertBytes`
覆盖 MySQL/SQLite/PostgreSQL 三种数据库的类型名。
### Row() 方法替换(第 362-391 行)
- `defer resl.Close()` + `resl.Err()` 检查
- `ColumnTypes()` 预分类列类型
- type switch 替代 reflect`[]byte``convertBytes``float64``fixFloatValue``float32``fixFloatValue`,其余不变
---
## 文件 4cache/cache_db.go -- DB 缓存
3 处 `json.Unmarshal` 改用 `JsonToObj`(来自 common 包,已通过 `. "code.hoteas.com/golang/hotime/common"` 导入):
**位置 1** -- `getLegacy()` 约第 359-363 行:
```go
// 修改前:
var data interface{}
err := json.Unmarshal([]byte(valueStr), &data)
// 修改后:
data, err := JsonToObj(valueStr)
```
**位置 2** -- `get()` 约第 411-416 行:
```go
// 修改前:
var data interface{}
err := json.Unmarshal([]byte(valueStr), &data)
// 修改后:
data, err := JsonToObj(valueStr)
```
**位置 3** -- `CachesGet()` 约第 594-598 行:
```go
// 修改前:
var data interface{}
err := json.Unmarshal([]byte(valueStr), &data)
// 修改后:
data, err := JsonToObj(valueStr)
```
不需要新增 import`JsonToObj` 在 common 包,cache_db.go 已 import)。
---
## 修正效果总结
| 数据路径 | 修正前 | 修正后 |
| ---------------------- | ------------------------------------- | ------------------------------------------------------ |
| DB → Row() → Map | float32/float64 原样存入,[]byte 全转 string | 按列类型精确转换:INT→int64, DECIMAL→roundFloat, FLOAT→fixFloat |
| DB 缓存 → json.Unmarshal | 所有数字→float64 | JsonToObj:整数→int64, 小数→float64 |
| ObjToMap(string) | json.Unmarshal 数字全→float64 | JsonToObj:保留 int64/float64 |
| ObjToSlice(string) | json.Unmarshal 数字全→float64 | JsonToObj:保留 int64/float64 |
| StrToMap(string) | JsonToMap → json.Unmarshal | JsonToMap → UseNumber + fixJsonNumbers |
| Map.JsonToMap(string) | json.Unmarshal | UseNumber + fixJsonNumbers |
| Redis 缓存 | ObjToStr → string → ObjToMap | ObjToMap 已增强,**自动修复** |
Redis 缓存路径:`set``ObjToStr``get` 返回 string → 调用方用 `ObjToMap` 转回 Map。由于 `ObjToMap` 的 string 分支现在使用 `JsonToObj`,Redis 缓存也**自动获得类型保留**,无需单独改 cache_redis.go。
+3 -3
View File
@@ -411,8 +411,8 @@ func (that *Application) handler(w http.ResponseWriter, req *http.Request) {
return
}
//url赋值
path := that.Config.GetString("tpt") + tempHandlerStr
//url赋值,静态文件必须使用原始路径(Linux文件系统大小写敏感)
path := that.Config.GetString("tpt") + context.HandlerStr
//判断是否为默认
if path[len(path)-1] == '/' {
@@ -448,7 +448,7 @@ func (that *Application) handler(w http.ResponseWriter, req *http.Request) {
t := strings.LastIndex(path, ".")
if t != -1 {
tt := path[t:]
tt := strings.ToLower(path[t:])
if MimeMaps[tt] != "" {
+3 -6
View File
@@ -357,8 +357,7 @@ func (that *CacheDb) getLegacy(key string) interface{} {
return nil
}
var data interface{}
err := json.Unmarshal([]byte(valueStr), &data)
data, err := JsonToObj(valueStr)
if err != nil {
return nil
}
@@ -409,8 +408,7 @@ func (that *CacheDb) get(key string) interface{} {
// 直接解析 value,不再需要 {"data": value} 包装
valueStr := cached.GetString("value")
if valueStr != "" {
var data interface{}
err := json.Unmarshal([]byte(valueStr), &data)
data, err := JsonToObj(valueStr)
if err == nil {
return data
}
@@ -592,8 +590,7 @@ func (that *CacheDb) CachesGet(keys []string) Map {
for _, cached := range cachedList {
valueStr := cached.GetString("value")
if valueStr != "" {
var data interface{}
err := json.Unmarshal([]byte(valueStr), &data)
data, err := JsonToObj(valueStr)
if err == nil {
result[cached.GetString("key")] = data
} else {
+17 -4
View File
@@ -1,6 +1,7 @@
package common
import (
"bytes"
"encoding/json"
"errors"
"reflect"
@@ -181,9 +182,21 @@ func (that Map) ToJsonString() string {
}
func (that Map) JsonToMap(jsonStr string, err ...*Error) {
e := json.Unmarshal([]byte(jsonStr), &that)
if e != nil && len(err) != 0 {
err[0].SetError(e)
dec := json.NewDecoder(bytes.NewReader([]byte(jsonStr)))
dec.UseNumber()
e := dec.Decode(&that)
if e != nil {
if len(err) != 0 {
err[0].SetError(e)
}
return
}
for k, v := range that {
that[k] = fixJsonNumbers(v)
}
}
// GetRoundFloat64 获取四舍五入到 precision 位小数的 float64
func (that Map) GetRoundFloat64(key string, precision int, err ...*Error) float64 {
return ObjToRoundFloat64((that)[key], precision, err...)
}
+5
View File
@@ -101,3 +101,8 @@ func (that *Obj) ToCeilInt(err ...*Error) int {
return v
}
// ToRoundFloat64 获取四舍五入到 precision 位小数的 float64
func (that *Obj) ToRoundFloat64(precision int, err ...*Error) float64 {
return ObjToRoundFloat64(that.Data, precision, err...)
}
+190 -119
View File
@@ -1,6 +1,7 @@
package common
import (
"bytes"
"encoding/json"
"errors"
"math"
@@ -18,31 +19,40 @@ func ObjToMap(obj interface{}, e ...*Error) Map {
v = nil
err = errors.New("没有合适的转换对象!")
} else {
switch obj.(type) {
switch val := obj.(type) {
case Map:
v = obj.(Map)
v = val
case map[string]interface{}:
v = obj.(map[string]interface{})
v = Map(val)
case string:
v = Map{}
e := json.Unmarshal([]byte(obj.(string)), &v)
if e != nil {
err = errors.New("没有合适的转换对象!" + e.Error())
result, e2 := JsonToObj(val)
if e2 != nil {
err = errors.New("没有合适的转换对象!" + e2.Error())
v = nil
} else if m, ok := result.(map[string]interface{}); ok {
v = m
} else {
err = errors.New("没有合适的转换对象!")
v = nil
}
default:
data, err := json.Marshal(obj)
var data []byte
data, err = json.Marshal(obj)
if err != nil {
err = errors.New("没有合适的转换对象!" + err.Error())
v = nil
break
}
v = Map{}
e := json.Unmarshal(data, &v)
if e != nil {
err = errors.New("没有合适的转换对象!" + e.Error())
result, e2 := JsonToObj(string(data))
if e2 != nil {
err = errors.New("没有合适的转换对象!" + e2.Error())
v = nil
} else if m, ok := result.(map[string]interface{}); ok {
v = m
} else {
err = errors.New("没有合适的转换对象!")
v = nil
}
}
}
if len(e) != 0 {
@@ -53,7 +63,7 @@ func ObjToMap(obj interface{}, e ...*Error) Map {
func ObjToMapArray(obj interface{}, e ...*Error) []Map {
s := ObjToSlice(obj, e...)
res := []Map{}
res := make([]Map, 0, len(s))
for i := 0; i < len(s); i++ {
res = append(res, s.GetMap(i))
}
@@ -69,25 +79,39 @@ func ObjToSlice(obj interface{}, e ...*Error) Slice {
v = nil
err = errors.New("没有合适的转换对象!")
} else {
switch obj.(type) {
switch val := obj.(type) {
case Slice:
v = obj.(Slice)
v = val
case []interface{}:
v = obj.([]interface{})
v = Slice(val)
case []string:
v = Slice{}
for i := 0; i < len(obj.([]string)); i++ {
v = append(v, obj.([]string)[i])
v = make(Slice, len(val))
for i, s := range val {
v[i] = s
}
case string:
v = Slice{}
err = json.Unmarshal([]byte(obj.(string)), &v)
result, e2 := JsonToObj(val)
if e2 != nil {
err = e2
} else if s, ok := result.([]interface{}); ok {
v = s
} else {
err = errors.New("没有合适的转换对象!")
}
default:
v = Slice{}
var data []byte
data, err = json.Marshal(obj)
err = json.Unmarshal(data, &v)
if err != nil {
break
}
result, e2 := JsonToObj(string(data))
if e2 != nil {
err = e2
} else if s, ok := result.([]interface{}); ok {
v = s
} else {
err = errors.New("没有合适的转换对象!")
}
}
}
@@ -104,22 +128,24 @@ func ObjToTime(obj interface{}, e ...*Error) *time.Time {
//字符串类型,只支持标准mysql datetime格式
if tInt == 0 {
tStr := ObjToStr(obj)
timeNewStr := ""
timeNewStrs := strings.Split(tStr, "-")
for _, v := range timeNewStrs {
if v == "" {
continue
}
if len(v) == 1 {
v = "0" + v
}
if timeNewStr == "" {
timeNewStr = v
continue
}
timeNewStr = timeNewStr + "-" + v
// 先分离日期和时间部分,再对日期各段补零,避免时间中的数字被误处理
datePart := tStr
timePart := ""
if idx := strings.Index(tStr, " "); idx > 0 {
datePart = tStr[:idx]
timePart = tStr[idx:] // 含前导空格
}
tStr = timeNewStr
dateSegs := strings.Split(datePart, "-")
for i, seg := range dateSegs {
if seg == "" {
continue
}
if len(seg) == 1 {
dateSegs[i] = "0" + seg
}
}
tStr = strings.Join(dateSegs, "-") + timePart
if len(tStr) > 18 {
t, e := time.Parse("2006-01-02 15:04:05", tStr)
if e == nil {
@@ -146,31 +172,30 @@ func ObjToTime(obj interface{}, e ...*Error) *time.Time {
return &t
}
}
}
// 缓存字符串,避免重复调用
tIntStr := ObjToStr(tInt)
tIntLen := len(tIntStr)
//纳秒级别
if len(ObjToStr(tInt)) > 16 {
//t := time.Time{}.Add(time.Nanosecond * time.Duration(tInt))
if tIntLen > 16 {
t := time.UnixMicro(tInt / 1000)
return &t
//微秒级别
} else if len(ObjToStr(tInt)) > 13 {
//t := time.Time{}.Add(time.Microsecond * time.Duration(tInt))
} else if tIntLen > 13 {
t := time.UnixMicro(tInt)
return &t
//毫秒级别
} else if len(ObjToStr(tInt)) > 10 {
//t := time.Time{}.Add(time.Millisecond * time.Duration(tInt))
} else if tIntLen > 10 {
t := time.UnixMilli(tInt)
return &t
//秒级别
} else if len(ObjToStr(tInt)) > 9 {
//t := time.Time{}.Add(time.Second * time.Duration(tInt))
} else if tIntLen > 9 {
t := time.Unix(tInt, 0)
return &t
} else if len(ObjToStr(tInt)) > 3 {
t, e := time.Parse("2006", ObjToStr(tInt))
} else if tIntLen > 3 {
t, e := time.Parse("2006", tIntStr)
if e == nil {
return &t
}
@@ -184,37 +209,34 @@ func ObjToFloat64(obj interface{}, e ...*Error) float64 {
v := float64(0)
if obj == nil {
err = errors.New("没有合适的转换对象!")
} else {
switch obj.(type) {
switch val := obj.(type) {
case int:
v = float64(obj.(int))
v = float64(val)
case int64:
v = float64(obj.(int64))
v = float64(val)
case string:
value, e := strconv.ParseFloat(obj.(string), 64)
if e != nil {
v = float64(0)
err = e
value, e2 := strconv.ParseFloat(val, 64)
if e2 != nil {
err = e2
} else {
v = value
}
case float64:
v = obj.(float64)
v = val
case float32:
v = float64(obj.(float32))
case uint8:
value, e := strconv.ParseFloat(obj.(string), 64)
if e != nil {
v = float64(0)
err = e
// 通过 string 中转避免 float32→float64 精度膨胀(如 float32(1.2) 直接转得 1.200000047...
v, _ = strconv.ParseFloat(strconv.FormatFloat(float64(val), 'f', -1, 32), 64)
case []byte:
// 数据库 driver 有时以 []byte 返回数值字符串
value, e2 := strconv.ParseFloat(string(val), 64)
if e2 != nil {
err = e2
} else {
v = value
}
default:
v = float64(0)
err = errors.New("没有合适的转换对象!")
}
}
@@ -234,25 +256,33 @@ func ObjToFloat64(obj interface{}, e ...*Error) float64 {
return v
}
// ObjToRoundFloat64 将 obj 转为 float64 后四舍五入到 precision 位小数。
// precision < 0 时不做舍入,直接返回原始 float64。
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
}
// 向上取整
func ObjToCeilInt64(obj interface{}, e ...*Error) int64 {
f := ObjToCeilFloat64(obj, e...)
return ObjToInt64(math.Ceil(f))
return int64(f)
}
// 向上取整
func ObjToCeilFloat64(obj interface{}, e ...*Error) float64 {
f := ObjToFloat64(obj, e...)
return math.Ceil(f)
}
// 向上取整
func ObjToCeilInt(obj interface{}, e ...*Error) int {
f := ObjToCeilFloat64(obj, e...)
return ObjToInt(f)
}
func ObjToInt64(obj interface{}, e ...*Error) int64 {
@@ -260,36 +290,43 @@ func ObjToInt64(obj interface{}, e ...*Error) int64 {
v := int64(0)
if obj == nil {
err = errors.New("没有合适的转换对象!")
} else {
switch obj.(type) {
switch val := obj.(type) {
case int:
v = int64(obj.(int))
v = int64(val)
case int64:
v = obj.(int64)
v = val
case string:
value, e := StrToInt(obj.(string))
if e != nil {
v = int64(0)
err = e
value, e2 := StrToInt(val)
if e2 != nil {
// fallback:处理 "1.20" 这类带小数点的整数字符串
if fv, fe := strconv.ParseFloat(val, 64); fe == nil {
v = int64(fv)
} else {
err = e2
}
} else {
v = int64(value)
}
case uint8:
value, e := StrToInt(obj.(string))
if e != nil {
v = int64(0)
err = e
case []byte:
// 数据库 driver 有时以 []byte 返回整数字符串
value, e2 := StrToInt(string(val))
if e2 != nil {
// fallback:处理 "1.20" 这类带小数点的整数字符串
if fv, fe := strconv.ParseFloat(string(val), 64); fe == nil {
v = int64(fv)
} else {
err = e2
}
} else {
v = int64(value)
}
case float64:
v = int64(obj.(float64))
v = int64(val)
case float32:
v = int64(obj.(float32))
v = int64(val)
default:
v = int64(0)
err = errors.New("没有合适的转换对象!")
}
}
@@ -309,18 +346,19 @@ func ObjToBool(obj interface{}, e ...*Error) bool {
v := false
if obj == nil {
err = errors.New("没有合适的转换对象!")
} else {
switch obj.(type) {
switch val := obj.(type) {
case bool:
v = obj.(bool)
v = val
default:
toInt := ObjToInt(obj)
if toInt != 0 {
var intErr Error
toInt := ObjToInt(obj, &intErr)
if intErr.GetError() != nil {
err = errors.New("没有合适的转换对象!")
} else if toInt != 0 {
v = true
}
err = errors.New("没有合适的转换对象!")
}
}
if len(e) != 0 {
@@ -330,55 +368,92 @@ func ObjToBool(obj interface{}, e ...*Error) bool {
}
func ObjToStr(obj interface{}) string {
// fmt.Println(reflect.ValueOf(obj).Type().String() )
str := ""
if obj == nil {
return str
}
switch obj.(type) {
switch val := obj.(type) {
case int:
str = strconv.Itoa(obj.(int))
str = strconv.Itoa(val)
case uint8:
str = obj.(string)
str = strconv.Itoa(int(val))
case int64:
str = strconv.FormatInt(obj.(int64), 10)
str = strconv.FormatInt(val, 10)
case []byte:
str = string(obj.([]byte))
str = string(val)
case string:
str = obj.(string)
str = val
case float64:
str = strconv.FormatFloat(obj.(float64), 'f', -1, 64)
str = strconv.FormatFloat(val, 'f', -1, 64)
case float32:
str = strconv.FormatFloat(float64(val), 'f', -1, 32)
case bool:
str = strconv.FormatBool(val)
default:
strbte, err := json.MarshalIndent(obj, "", "\t")
if err == nil {
str = string(strbte)
}
}
return str
}
// 转换为Map
func StrToMap(string string) Map {
data := Map{}
data.JsonToMap(string)
// JsonToObj 将 JSON 字符串反序列化为 interface{},保留数字原始类型。
// 标准 json.Unmarshal 会把所有 JSON 数字变成 float64,本函数保留整数为 int64、小数为 float64。
//
// JsonToObj(`{"id":2,"price":1.20}`) → map{"id": int64(2), "price": float64(1.2)}
// JsonToObj(`[1, 2.5, "a"]`) → []interface{}{int64(1), float64(2.5), "a"}
func JsonToObj(jsonStr string) (interface{}, error) {
dec := json.NewDecoder(bytes.NewReader([]byte(jsonStr)))
dec.UseNumber()
var result interface{}
if err := dec.Decode(&result); err != nil {
return nil, err
}
return fixJsonNumbers(result), nil
}
// fixJsonNumbers 递归将 json.Number 转为 int64 或 float64
func fixJsonNumbers(v interface{}) interface{} {
switch val := v.(type) {
case json.Number:
if n, err := val.Int64(); err == nil {
return n
}
if f, err := val.Float64(); err == nil {
return f
}
return string(val)
case map[string]interface{}:
for k, item := range val {
val[k] = fixJsonNumbers(item)
}
return val
case []interface{}:
for i, item := range val {
val[i] = fixJsonNumbers(item)
}
return val
default:
return val
}
}
// 转换为Map
func StrToMap(s string) Map {
data := Map{}
data.JsonToMap(s)
return data
}
// 转换为Slice
func StrToSlice(string string) Slice {
data := ObjToSlice(string)
return data
func StrToSlice(s string) Slice {
return ObjToSlice(s)
}
// 字符串数组: a1,a2,a3转["a1","a2","a3"]
func StrArrayToJsonStr(a string) string {
if len(a) > 2 {
if a[0] == ',' {
a = Substr(a, 1, len(a)-1)
@@ -386,7 +461,6 @@ func StrArrayToJsonStr(a string) string {
if a[len(a)-1] == ',' {
a = Substr(a, 0, len(a)-1)
}
//a = strings.Replace(a, ",", `,`, -1)
a = `[` + a + `]`
} else {
a = "[]"
@@ -394,13 +468,11 @@ func StrArrayToJsonStr(a string) string {
return a
}
// 字符串数组: a1,a2,a3转["a1","a2","a3"]
// 字符串数组: ["a1","a2","a3"]转,a1,a2,a3,
func JsonStrToStrArray(a string) string {
//a = strings.Replace(a, `"`, "", -1)
if len(a) != 0 {
a = Substr(a, 1, len(a)-2)
}
return "," + a + ","
}
@@ -408,5 +480,4 @@ func JsonStrToStrArray(a string) string {
func StrToInt(s string) (int, error) {
i, err := strconv.Atoi(s)
return i, err
}
+917
View File
@@ -0,0 +1,917 @@
package common
import (
"math"
"testing"
"time"
)
// ─────────────────────────────────────────────
// 辅助:检查 Error 是否携带错误
// ─────────────────────────────────────────────
func hasErr(e *Error) bool {
return e.GetError() != nil
}
func newErr() *Error {
return &Error{}
}
// ─────────────────────────────────────────────
// ObjToMap
// ─────────────────────────────────────────────
func TestObjToMap(t *testing.T) {
t.Run("nil returns nil with error", func(t *testing.T) {
e := newErr()
v := ObjToMap(nil, e)
if v != nil || !hasErr(e) {
t.Errorf("nil: got %v err=%v", v, e.GetError())
}
})
t.Run("Map passthrough", func(t *testing.T) {
input := Map{"a": 1}
v := ObjToMap(input)
if v == nil || v["a"] != 1 {
t.Errorf("Map passthrough failed: %v", v)
}
})
t.Run("map[string]interface{} cast", func(t *testing.T) {
input := map[string]interface{}{"b": 2}
v := ObjToMap(input)
if v == nil || v["b"] != 2 {
t.Errorf("map[string]interface{} cast failed: %v", v)
}
})
t.Run("valid JSON string - integer preserved as int64", func(t *testing.T) {
v := ObjToMap(`{"x":42}`)
if v == nil || v["x"] != int64(42) {
t.Errorf("JSON string: got %v (type %T), want int64(42)", v["x"], v["x"])
}
})
t.Run("invalid JSON string returns nil with error", func(t *testing.T) {
e := newErr()
v := ObjToMap(`not json`, e)
if v != nil || !hasErr(e) {
t.Errorf("invalid JSON: got %v err=%v", v, e.GetError())
}
})
t.Run("struct default branch marshal+unmarshal", func(t *testing.T) {
type S struct{ Name string }
v := ObjToMap(S{Name: "hello"})
if v == nil || v["Name"] != "hello" {
t.Errorf("struct default: %v", v)
}
})
t.Run("marshal fail (channel) returns nil with error", func(t *testing.T) {
e := newErr()
ch := make(chan int)
v := ObjToMap(ch, e)
if v != nil || !hasErr(e) {
t.Errorf("channel marshal: got %v err=%v", v, e.GetError())
}
})
t.Run("default marshal OK but unmarshal to map fails (int)", func(t *testing.T) {
// json.Marshal(123) = "123", which can't unmarshal to Map
e := newErr()
v := ObjToMap(123, e)
if v != nil || !hasErr(e) {
t.Errorf("int default: got %v err=%v", v, e.GetError())
}
})
}
// ─────────────────────────────────────────────
// ObjToMapArray
// ─────────────────────────────────────────────
func TestObjToMapArray(t *testing.T) {
t.Run("nil returns empty", func(t *testing.T) {
e := newErr()
v := ObjToMapArray(nil, e)
if len(v) != 0 || !hasErr(e) {
t.Errorf("nil: got %v err=%v", v, e.GetError())
}
})
t.Run("JSON array of maps", func(t *testing.T) {
v := ObjToMapArray(`[{"a":1},{"b":2}]`)
if len(v) != 2 {
t.Errorf("expected 2 elements, got %d", len(v))
}
})
}
// ─────────────────────────────────────────────
// ObjToSlice
// ─────────────────────────────────────────────
func TestObjToSlice(t *testing.T) {
t.Run("nil returns nil with error", func(t *testing.T) {
e := newErr()
v := ObjToSlice(nil, e)
if v != nil || !hasErr(e) {
t.Errorf("nil: got %v err=%v", v, e.GetError())
}
})
t.Run("Slice passthrough", func(t *testing.T) {
input := Slice{1, 2, 3}
v := ObjToSlice(input)
if len(v) != 3 {
t.Errorf("Slice passthrough: %v", v)
}
})
t.Run("[]interface{} cast", func(t *testing.T) {
input := []interface{}{4, 5}
v := ObjToSlice(input)
if len(v) != 2 || v[0] != 4 {
t.Errorf("[]interface{} cast: %v", v)
}
})
t.Run("[]string conversion", func(t *testing.T) {
input := []string{"a", "b", "c"}
v := ObjToSlice(input)
if len(v) != 3 || v[0] != "a" {
t.Errorf("[]string: %v", v)
}
})
t.Run("valid JSON array string", func(t *testing.T) {
v := ObjToSlice(`[1,2,3]`)
if len(v) != 3 {
t.Errorf("JSON array: %v", v)
}
})
t.Run("invalid JSON string returns error", func(t *testing.T) {
e := newErr()
v := ObjToSlice(`not json`, e)
if !hasErr(e) {
t.Errorf("invalid JSON: got %v err=%v", v, e.GetError())
}
})
t.Run("default: []int marshals to JSON array", func(t *testing.T) {
v := ObjToSlice([]int{10, 20, 30})
if len(v) != 3 {
t.Errorf("[]int default: %v", v)
}
})
t.Run("default: marshal fail (channel) returns error", func(t *testing.T) {
e := newErr()
ch := make(chan int)
ObjToSlice(ch, e)
if !hasErr(e) {
t.Errorf("channel marshal: expected error")
}
})
}
// ─────────────────────────────────────────────
// ObjToTime
// ─────────────────────────────────────────────
func TestObjToTime(t *testing.T) {
mustParse := func(layout, s string) time.Time {
tt, err := time.Parse(layout, s)
if err != nil {
panic(err)
}
return tt
}
t.Run("full datetime string", func(t *testing.T) {
ts := ObjToTime("2006-01-02 15:04:05")
want := mustParse("2006-01-02 15:04:05", "2006-01-02 15:04:05")
if ts == nil || !ts.Equal(want) {
t.Errorf("full datetime: %v", ts)
}
})
t.Run("single digit day+time (bug fix)", func(t *testing.T) {
ts := ObjToTime("2006-1-2 15:04:05")
want := mustParse("2006-01-02 15:04:05", "2006-01-02 15:04:05")
if ts == nil || !ts.Equal(want) {
t.Errorf("single digit day+time: got %v, want %v", ts, want)
}
})
t.Run("datetime without seconds", func(t *testing.T) {
ts := ObjToTime("2006-01-02 15:04")
want := mustParse("2006-01-02 15:04", "2006-01-02 15:04")
if ts == nil || !ts.Equal(want) {
t.Errorf("datetime without seconds: %v", ts)
}
})
t.Run("date+hour only", func(t *testing.T) {
ts := ObjToTime("2006-01-02 15")
want := mustParse("2006-01-02 15", "2006-01-02 15")
if ts == nil || !ts.Equal(want) {
t.Errorf("date+hour: %v", ts)
}
})
t.Run("date only", func(t *testing.T) {
ts := ObjToTime("2006-01-02")
want := mustParse("2006-01-02", "2006-01-02")
if ts == nil || !ts.Equal(want) {
t.Errorf("date only: %v", ts)
}
})
t.Run("month only", func(t *testing.T) {
ts := ObjToTime("2006-01")
want := mustParse("2006-01", "2006-01")
if ts == nil || !ts.Equal(want) {
t.Errorf("month only: %v", ts)
}
})
t.Run("invalid string returns nil", func(t *testing.T) {
ts := ObjToTime("not-a-date")
if ts != nil {
t.Errorf("invalid string: expected nil, got %v", ts)
}
})
t.Run("unix second timestamp (10 digits)", func(t *testing.T) {
ts := ObjToTime(int64(1700000000)) // 10 digits
if ts == nil {
t.Error("second timestamp: got nil")
}
want := time.Unix(1700000000, 0)
if !ts.Equal(want) {
t.Errorf("second: %v vs %v", ts, want)
}
})
t.Run("unix millisecond timestamp (13 digits)", func(t *testing.T) {
ts := ObjToTime(int64(1700000000000)) // 13 digits
if ts == nil {
t.Error("millisecond timestamp: got nil")
}
want := time.UnixMilli(1700000000000)
if !ts.Equal(want) {
t.Errorf("millisecond: %v vs %v", ts, want)
}
})
t.Run("unix microsecond timestamp (16 digits)", func(t *testing.T) {
ts := ObjToTime(int64(1700000000000000)) // 16 digits
if ts == nil {
t.Error("microsecond timestamp: got nil")
}
want := time.UnixMicro(1700000000000000)
if !ts.Equal(want) {
t.Errorf("microsecond: %v vs %v", ts, want)
}
})
t.Run("nanosecond timestamp (19 digits)", func(t *testing.T) {
ts := ObjToTime(int64(1700000000000000000)) // 19 digits -> nano branch
if ts == nil {
t.Error("nanosecond timestamp: got nil")
}
want := time.UnixMicro(1700000000000000000 / 1000)
if !ts.Equal(want) {
t.Errorf("nanosecond: %v vs %v", ts, want)
}
})
t.Run("year int (4 digits)", func(t *testing.T) {
ts := ObjToTime(int64(2024))
if ts == nil {
t.Error("year int: got nil")
}
want := mustParse("2006", "2024")
if !ts.Equal(want) {
t.Errorf("year: %v vs %v", ts, want)
}
})
t.Run("small int returns nil", func(t *testing.T) {
ts := ObjToTime(int64(1))
if ts != nil {
t.Errorf("small int: expected nil, got %v", ts)
}
})
t.Run("nil returns nil", func(t *testing.T) {
ts := ObjToTime(nil)
if ts != nil {
t.Errorf("nil: expected nil, got %v", ts)
}
})
}
// ─────────────────────────────────────────────
// ObjToFloat64
// ─────────────────────────────────────────────
func TestObjToFloat64(t *testing.T) {
cases := []struct {
name string
input interface{}
want float64
wantErr bool
}{
{"nil", nil, 0, true},
{"int", 42, 42.0, false},
{"int64", int64(100), 100.0, false},
{"string valid", "3.14", 3.14, false},
{"string invalid", "abc", 0, true},
{"float64", float64(1.5), 1.5, false},
// Fix4: float32 通过 strconv 转换,消除精度膨胀噪声
{"float32 no precision inflation", float32(1.2), 1.2, false},
{"[]byte valid", []byte("2.71"), 2.71, false},
{"[]byte invalid", []byte("xyz"), 0, true},
{"unknown type", struct{}{}, 0, true},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
e := newErr()
got := ObjToFloat64(c.input, e)
if math.Abs(got-c.want) > 1e-9 {
t.Errorf("value: got %v, want %v", got, c.want)
}
if hasErr(e) != c.wantErr {
t.Errorf("error: got hasErr=%v, wantErr=%v (err=%v)", hasErr(e), c.wantErr, e.GetError())
}
})
}
t.Run("NaN is caught and zeroed", func(t *testing.T) {
e := newErr()
v := ObjToFloat64(math.NaN(), e)
if v != 0 || !hasErr(e) {
t.Errorf("NaN: got %v err=%v", v, e.GetError())
}
})
t.Run("Inf is caught and zeroed", func(t *testing.T) {
e := newErr()
v := ObjToFloat64(math.Inf(1), e)
if v != 0 || !hasErr(e) {
t.Errorf("Inf: got %v err=%v", v, e.GetError())
}
})
t.Run("-Inf is caught and zeroed", func(t *testing.T) {
e := newErr()
v := ObjToFloat64(math.Inf(-1), e)
if v != 0 || !hasErr(e) {
t.Errorf("-Inf: got %v err=%v", v, e.GetError())
}
})
}
// ─────────────────────────────────────────────
// ObjToRoundFloat64
// ─────────────────────────────────────────────
func TestObjToRoundFloat64(t *testing.T) {
t.Run("precision 2 rounds correctly", func(t *testing.T) {
v := ObjToRoundFloat64(3.5000001, 2)
if v != 3.5 {
t.Errorf("got %v, want 3.5", v)
}
})
t.Run("precision 0 rounds to integer", func(t *testing.T) {
v := ObjToRoundFloat64(2.7, 0)
if v != 3.0 {
t.Errorf("got %v, want 3.0", v)
}
})
t.Run("precision -1 returns raw float", func(t *testing.T) {
v := ObjToRoundFloat64(1.23456789, -1)
if math.Abs(v-1.23456789) > 1e-9 {
t.Errorf("got %v, want ~1.23456789", v)
}
})
t.Run("string input with float noise", func(t *testing.T) {
v := ObjToRoundFloat64("3.15000001", 2)
if v != 3.15 {
t.Errorf("got %v, want 3.15", v)
}
})
}
// ─────────────────────────────────────────────
// ObjToCeil*
// ─────────────────────────────────────────────
func TestObjToCeil(t *testing.T) {
t.Run("ObjToCeilInt64 rounds up", func(t *testing.T) {
v := ObjToCeilInt64(1.1)
if v != 2 {
t.Errorf("got %d, want 2", v)
}
})
t.Run("ObjToCeilInt64 already integer", func(t *testing.T) {
v := ObjToCeilInt64(float64(3))
if v != 3 {
t.Errorf("got %d, want 3", v)
}
})
t.Run("ObjToCeilInt64 no double ceil (bug fix)", func(t *testing.T) {
// 3.0 -> Ceil(3.0)=3.0 -> int64(3), NOT Ceil(3.0) again
v := ObjToCeilInt64(float64(3))
if v != 3 {
t.Errorf("double ceil: got %d, want 3", v)
}
})
t.Run("ObjToCeilFloat64", func(t *testing.T) {
v := ObjToCeilFloat64(1.1)
if v != 2.0 {
t.Errorf("got %v, want 2.0", v)
}
})
t.Run("ObjToCeilInt", func(t *testing.T) {
v := ObjToCeilInt(2.3)
if v != 3 {
t.Errorf("got %d, want 3", v)
}
})
}
// ─────────────────────────────────────────────
// ObjToInt64
// ─────────────────────────────────────────────
func TestObjToInt64(t *testing.T) {
cases := []struct {
name string
input interface{}
want int64
wantErr bool
}{
{"nil", nil, 0, true},
{"int", 7, 7, false},
{"int64", int64(99), 99, false},
{"string valid int", "42", 42, false},
// Fix3 fallback: "1.20" 原来报错,现在截断得 1
{"string float-like", "1.20", 1, false},
{"string invalid", "abc", 0, true},
{"[]byte valid int", []byte("123"), 123, false},
// Fix3 fallback: []byte "2.70" 截断得 2
{"[]byte float-like", []byte("2.70"), 2, false},
{"[]byte invalid", []byte("xyz"), 0, true},
{"float64 truncate", float64(5.9), 5, false},
{"float32 truncate", float32(3.1), 3, false},
{"unknown type", struct{}{}, 0, true},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
e := newErr()
got := ObjToInt64(c.input, e)
if got != c.want {
t.Errorf("value: got %d, want %d", got, c.want)
}
if hasErr(e) != c.wantErr {
t.Errorf("error: got hasErr=%v, wantErr=%v (err=%v)", hasErr(e), c.wantErr, e.GetError())
}
})
}
}
// ─────────────────────────────────────────────
// ObjToInt
// ─────────────────────────────────────────────
func TestObjToInt(t *testing.T) {
v := ObjToInt(10)
if v != 10 {
t.Errorf("got %d, want 10", v)
}
}
// ─────────────────────────────────────────────
// ObjToBool
// ─────────────────────────────────────────────
func TestObjToBool(t *testing.T) {
t.Run("nil returns false with error", func(t *testing.T) {
e := newErr()
v := ObjToBool(nil, e)
if v || !hasErr(e) {
t.Errorf("nil: got %v err=%v", v, e.GetError())
}
})
t.Run("bool true", func(t *testing.T) {
e := newErr()
v := ObjToBool(true, e)
if !v || hasErr(e) {
t.Errorf("bool true: got %v err=%v", v, e.GetError())
}
})
t.Run("bool false", func(t *testing.T) {
e := newErr()
v := ObjToBool(false, e)
if v || hasErr(e) {
t.Errorf("bool false: got %v err=%v", v, e.GetError())
}
})
t.Run("int 1 -> true, no error (bug fix)", func(t *testing.T) {
e := newErr()
v := ObjToBool(1, e)
if !v || hasErr(e) {
t.Errorf("int 1: got v=%v err=%v", v, e.GetError())
}
})
t.Run("int 0 -> false, no error", func(t *testing.T) {
e := newErr()
v := ObjToBool(0, e)
if v || hasErr(e) {
t.Errorf("int 0: got v=%v err=%v", v, e.GetError())
}
})
t.Run("unknown type returns false with error", func(t *testing.T) {
e := newErr()
v := ObjToBool(struct{}{}, e)
if v || !hasErr(e) {
t.Errorf("struct: got v=%v err=%v", v, e.GetError())
}
})
}
// ─────────────────────────────────────────────
// ObjToStr
// ─────────────────────────────────────────────
func TestObjToStr(t *testing.T) {
cases := []struct {
name string
input interface{}
want string
}{
{"nil", nil, ""},
{"int", 42, "42"},
{"uint8", uint8(65), "65"},
{"int64", int64(1234567890), "1234567890"},
{"[]byte", []byte("hello"), "hello"},
{"string", "world", "world"},
{"float64", float64(3.14), "3.14"},
{"float32", float32(2.5), "2.5"},
{"bool true", true, "true"},
{"bool false", false, "false"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := ObjToStr(c.input)
if got != c.want {
t.Errorf("got %q, want %q", got, c.want)
}
})
}
t.Run("default (struct) marshals to JSON", func(t *testing.T) {
type S struct{ X int }
got := ObjToStr(S{X: 1})
if got == "" {
t.Error("struct: expected non-empty JSON string")
}
})
}
// ─────────────────────────────────────────────
// StrToMap
// ─────────────────────────────────────────────
func TestStrToMap(t *testing.T) {
t.Run("valid JSON", func(t *testing.T) {
v := StrToMap(`{"k":"v"}`)
if v["k"] != "v" {
t.Errorf("StrToMap: %v", v)
}
})
t.Run("invalid JSON returns empty map", func(t *testing.T) {
v := StrToMap(`not json`)
if v == nil {
t.Error("StrToMap invalid: expected empty map, got nil")
}
})
}
// ─────────────────────────────────────────────
// StrToSlice
// ─────────────────────────────────────────────
func TestStrToSlice(t *testing.T) {
v := StrToSlice(`[1,2,3]`)
if len(v) != 3 {
t.Errorf("StrToSlice: expected 3 elements, got %d", len(v))
}
}
// ─────────────────────────────────────────────
// StrArrayToJsonStr
// ─────────────────────────────────────────────
func TestStrArrayToJsonStr(t *testing.T) {
cases := []struct {
name string
input string
want string
}{
{"empty returns []", "", "[]"},
{"short (<=2) returns []", "a", "[]"},
{"no commas", `"a","b"`, `["a","b"]`},
{"leading comma stripped", `,"a","b"`, `["a","b"]`},
{"trailing comma stripped", `"a","b",`, `["a","b"]`},
{"both commas stripped", `,"a","b",`, `["a","b"]`},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := StrArrayToJsonStr(c.input)
if got != c.want {
t.Errorf("got %q, want %q", got, c.want)
}
})
}
}
// ─────────────────────────────────────────────
// JsonStrToStrArray
// ─────────────────────────────────────────────
func TestJsonStrToStrArray(t *testing.T) {
t.Run("non-empty strips brackets", func(t *testing.T) {
got := JsonStrToStrArray(`["a","b"]`)
if got != `,"a","b",` {
t.Errorf("got %q", got)
}
})
t.Run("empty string", func(t *testing.T) {
got := JsonStrToStrArray("")
if got != ",," {
t.Errorf("got %q", got)
}
})
}
// ─────────────────────────────────────────────
// StrToInt
// ─────────────────────────────────────────────
func TestStrToInt(t *testing.T) {
t.Run("valid", func(t *testing.T) {
v, err := StrToInt("123")
if v != 123 || err != nil {
t.Errorf("got %d %v", v, err)
}
})
t.Run("invalid", func(t *testing.T) {
_, err := StrToInt("abc")
if err == nil {
t.Error("expected error")
}
})
}
// ─────────────────────────────────────────────
// JsonToObj
// ─────────────────────────────────────────────
func TestJsonToObj(t *testing.T) {
t.Run("integer preserved as int64", func(t *testing.T) {
v, err := JsonToObj(`{"id":2}`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
m, ok := v.(map[string]interface{})
if !ok {
t.Fatalf("expected map, got %T", v)
}
if m["id"] != int64(2) {
t.Errorf("got %v (type %T), want int64(2)", m["id"], m["id"])
}
})
t.Run("decimal preserved as float64 with correct precision", func(t *testing.T) {
v, err := JsonToObj(`{"price":1.20}`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
m := v.(map[string]interface{})
price, ok := m["price"].(float64)
if !ok {
t.Fatalf("expected float64, got %T", m["price"])
}
if price != 1.2 {
t.Errorf("got %v, want 1.2", price)
}
})
t.Run("large integer preserved as int64", func(t *testing.T) {
v, err := JsonToObj(`{"num":9999999}`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
m := v.(map[string]interface{})
if m["num"] != int64(9999999) {
t.Errorf("got %v (type %T), want int64(9999999)", m["num"], m["num"])
}
})
t.Run("array with mixed types", func(t *testing.T) {
v, err := JsonToObj(`[1, 2.5, "hello", true]`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
arr := v.([]interface{})
if len(arr) != 4 {
t.Fatalf("expected 4 elements, got %d", len(arr))
}
if arr[0] != int64(1) {
t.Errorf("arr[0]: got %v (%T), want int64(1)", arr[0], arr[0])
}
if arr[1] != float64(2.5) {
t.Errorf("arr[1]: got %v (%T), want float64(2.5)", arr[1], arr[1])
}
if arr[2] != "hello" {
t.Errorf("arr[2]: got %v, want hello", arr[2])
}
if arr[3] != true {
t.Errorf("arr[3]: got %v, want true", arr[3])
}
})
t.Run("nested object integers preserved", func(t *testing.T) {
v, err := JsonToObj(`{"order":{"id":5,"num":3,"price":1.20}}`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
m := v.(map[string]interface{})
order := m["order"].(map[string]interface{})
if order["id"] != int64(5) {
t.Errorf("nested id: got %v (%T), want int64(5)", order["id"], order["id"])
}
if order["num"] != int64(3) {
t.Errorf("nested num: got %v (%T), want int64(3)", order["num"], order["num"])
}
if order["price"] != float64(1.2) {
t.Errorf("nested price: got %v, want 1.2", order["price"])
}
})
t.Run("invalid JSON returns error", func(t *testing.T) {
_, err := JsonToObj(`not json`)
if err == nil {
t.Error("expected error for invalid JSON")
}
})
t.Run("empty object", func(t *testing.T) {
v, err := JsonToObj(`{}`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
m, ok := v.(map[string]interface{})
if !ok || len(m) != 0 {
t.Errorf("expected empty map, got %v", v)
}
})
t.Run("null value", func(t *testing.T) {
v, err := JsonToObj(`{"x":null}`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
m := v.(map[string]interface{})
if m["x"] != nil {
t.Errorf("expected nil, got %v", m["x"])
}
})
t.Run("zero integer", func(t *testing.T) {
v, err := JsonToObj(`{"count":0}`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
m := v.(map[string]interface{})
if m["count"] != int64(0) {
t.Errorf("got %v (%T), want int64(0)", m["count"], m["count"])
}
})
}
// ─────────────────────────────────────────────
// ObjToMap 数字类型保留(新行为)
// ─────────────────────────────────────────────
func TestObjToMapNumberPreservation(t *testing.T) {
t.Run("JSON integer -> int64", func(t *testing.T) {
v := ObjToMap(`{"id":2,"num":0}`)
if v["id"] != int64(2) {
t.Errorf("id: got %v (%T), want int64(2)", v["id"], v["id"])
}
if v["num"] != int64(0) {
t.Errorf("num: got %v (%T), want int64(0)", v["num"], v["num"])
}
})
t.Run("JSON decimal -> float64", func(t *testing.T) {
v := ObjToMap(`{"price":1.20,"rate":0.5}`)
if v["price"] != float64(1.2) {
t.Errorf("price: got %v (%T), want float64(1.2)", v["price"], v["price"])
}
if v["rate"] != float64(0.5) {
t.Errorf("rate: got %v (%T), want float64(0.5)", v["rate"], v["rate"])
}
})
t.Run("struct with int field -> int64", func(t *testing.T) {
type S struct {
Count int `json:"count"`
Name string `json:"name"`
}
v := ObjToMap(S{Count: 3, Name: "test"})
if v == nil {
t.Fatal("got nil")
}
if v["count"] != int64(3) {
t.Errorf("count: got %v (%T), want int64(3)", v["count"], v["count"])
}
})
}
// ─────────────────────────────────────────────
// ObjToSlice 数字类型保留(新行为)
// ─────────────────────────────────────────────
func TestObjToSliceNumberPreservation(t *testing.T) {
t.Run("JSON integer array -> int64 elements", func(t *testing.T) {
v := ObjToSlice(`[1, 2, 3]`)
if len(v) != 3 {
t.Fatalf("expected 3, got %d", len(v))
}
if v[0] != int64(1) {
t.Errorf("v[0]: got %v (%T), want int64(1)", v[0], v[0])
}
})
t.Run("JSON mixed array - integer and float", func(t *testing.T) {
v := ObjToSlice(`[2, 1.5, "hello"]`)
if v[0] != int64(2) {
t.Errorf("v[0]: got %v (%T), want int64(2)", v[0], v[0])
}
if v[1] != float64(1.5) {
t.Errorf("v[1]: got %v (%T), want float64(1.5)", v[1], v[1])
}
})
}
// ─────────────────────────────────────────────
// Map.JsonToMap 数字类型保留(新行为)
// ─────────────────────────────────────────────
func TestMapJsonToMapNumberPreservation(t *testing.T) {
t.Run("integer -> int64", func(t *testing.T) {
m := Map{}
m.JsonToMap(`{"id":5,"count":0}`)
if m["id"] != int64(5) {
t.Errorf("id: got %v (%T), want int64(5)", m["id"], m["id"])
}
if m["count"] != int64(0) {
t.Errorf("count: got %v (%T), want int64(0)", m["count"], m["count"])
}
})
t.Run("decimal -> float64", func(t *testing.T) {
m := Map{}
m.JsonToMap(`{"price":1.20}`)
if m["price"] != float64(1.2) {
t.Errorf("price: got %v (%T), want float64(1.2)", m["price"], m["price"])
}
})
t.Run("StrToMap integer -> int64", func(t *testing.T) {
m := StrToMap(`{"total":100,"amount":1.50}`)
if m["total"] != int64(100) {
t.Errorf("total: got %v (%T), want int64(100)", m["total"], m["total"])
}
if m["amount"] != float64(1.5) {
t.Errorf("amount: got %v (%T), want float64(1.5)", m["amount"], m["amount"])
}
})
t.Run("invalid JSON sets error", func(t *testing.T) {
e := newErr()
m := Map{}
m.JsonToMap(`not json`, e)
if !hasErr(e) {
t.Error("expected error for invalid JSON")
}
})
}
+5
View File
@@ -63,6 +63,11 @@ func (that Slice) GetFloat64(key int, err ...*Error) float64 {
return v
}
// GetRoundFloat64 获取四舍五入到 precision 位小数的 float64
func (that Slice) GetRoundFloat64(key int, precision int, err ...*Error) float64 {
return ObjToRoundFloat64((that)[key], precision, err...)
}
func (that Slice) GetSlice(key int, err ...*Error) Slice {
v := ObjToSlice((that)[key], err...)
return v
+344
View File
@@ -0,0 +1,344 @@
mode: set
code.hoteas.com/golang/hotime/common/context_base.go:12.42,14.20 1 0
code.hoteas.com/golang/hotime/common/context_base.go:14.20,16.3 1 0
code.hoteas.com/golang/hotime/common/context_base.go:17.2,17.17 1 0
code.hoteas.com/golang/hotime/common/error.go:15.37,19.2 3 1
code.hoteas.com/golang/hotime/common/error.go:21.40,26.38 4 1
code.hoteas.com/golang/hotime/common/error.go:26.38,28.3 1 0
code.hoteas.com/golang/hotime/common/func.go:29.41,30.19 1 0
code.hoteas.com/golang/hotime/common/func.go:30.19,32.3 1 0
code.hoteas.com/golang/hotime/common/func.go:34.2,37.39 3 0
code.hoteas.com/golang/hotime/common/func.go:41.54,42.18 1 0
code.hoteas.com/golang/hotime/common/func.go:42.18,44.3 1 0
code.hoteas.com/golang/hotime/common/func.go:45.2,46.18 2 0
code.hoteas.com/golang/hotime/common/func.go:46.18,48.3 1 0
code.hoteas.com/golang/hotime/common/func.go:50.2,50.12 1 0
code.hoteas.com/golang/hotime/common/func.go:51.9,52.29 1 0
code.hoteas.com/golang/hotime/common/func.go:53.9,54.32 1 0
code.hoteas.com/golang/hotime/common/func.go:55.9,56.35 1 0
code.hoteas.com/golang/hotime/common/func.go:57.9,58.38 1 0
code.hoteas.com/golang/hotime/common/func.go:59.9,60.41 1 0
code.hoteas.com/golang/hotime/common/func.go:61.10,62.27 1 0
code.hoteas.com/golang/hotime/common/func.go:63.10,64.33 1 0
code.hoteas.com/golang/hotime/common/func.go:65.10,66.36 1 0
code.hoteas.com/golang/hotime/common/func.go:67.10,68.27 1 0
code.hoteas.com/golang/hotime/common/func.go:69.10,70.30 1 0
code.hoteas.com/golang/hotime/common/func.go:72.2,72.40 1 0
code.hoteas.com/golang/hotime/common/func.go:77.46,78.16 1 0
code.hoteas.com/golang/hotime/common/func.go:78.16,81.3 2 0
code.hoteas.com/golang/hotime/common/func.go:82.2,83.19 2 0
code.hoteas.com/golang/hotime/common/func.go:83.19,85.3 1 0
code.hoteas.com/golang/hotime/common/func.go:86.2,86.19 1 0
code.hoteas.com/golang/hotime/common/func.go:86.19,88.3 1 0
code.hoteas.com/golang/hotime/common/func.go:89.2,89.22 1 0
code.hoteas.com/golang/hotime/common/func.go:89.22,91.3 1 0
code.hoteas.com/golang/hotime/common/func.go:92.2,92.31 1 0
code.hoteas.com/golang/hotime/common/func.go:92.31,93.32 1 0
code.hoteas.com/golang/hotime/common/func.go:93.32,94.24 1 0
code.hoteas.com/golang/hotime/common/func.go:94.24,96.5 1 0
code.hoteas.com/golang/hotime/common/func.go:96.10,98.24 2 0
code.hoteas.com/golang/hotime/common/func.go:98.24,100.6 1 0
code.hoteas.com/golang/hotime/common/func.go:101.5,101.26 1 0
code.hoteas.com/golang/hotime/common/func.go:101.26,103.6 1 0
code.hoteas.com/golang/hotime/common/func.go:104.5,104.22 1 0
code.hoteas.com/golang/hotime/common/func.go:109.2,109.26 1 0
code.hoteas.com/golang/hotime/common/func.go:113.55,118.15 4 1
code.hoteas.com/golang/hotime/common/func.go:118.15,120.3 1 0
code.hoteas.com/golang/hotime/common/func.go:121.2,123.17 2 1
code.hoteas.com/golang/hotime/common/func.go:123.17,125.3 1 0
code.hoteas.com/golang/hotime/common/func.go:127.2,127.15 1 1
code.hoteas.com/golang/hotime/common/func.go:127.15,129.3 1 0
code.hoteas.com/golang/hotime/common/func.go:130.2,130.16 1 1
code.hoteas.com/golang/hotime/common/func.go:130.16,132.3 1 0
code.hoteas.com/golang/hotime/common/func.go:133.2,133.13 1 1
code.hoteas.com/golang/hotime/common/func.go:133.13,135.3 1 0
code.hoteas.com/golang/hotime/common/func.go:136.2,136.14 1 1
code.hoteas.com/golang/hotime/common/func.go:136.14,138.3 1 0
code.hoteas.com/golang/hotime/common/func.go:140.2,140.30 1 1
code.hoteas.com/golang/hotime/common/func.go:145.40,148.35 3 0
code.hoteas.com/golang/hotime/common/func.go:148.35,150.3 1 0
code.hoteas.com/golang/hotime/common/func.go:152.2,154.42 2 0
code.hoteas.com/golang/hotime/common/func.go:154.42,156.14 2 0
code.hoteas.com/golang/hotime/common/func.go:156.14,158.22 2 0
code.hoteas.com/golang/hotime/common/func.go:158.22,161.18 3 0
code.hoteas.com/golang/hotime/common/func.go:161.18,162.11 1 0
code.hoteas.com/golang/hotime/common/func.go:165.4,165.13 1 0
code.hoteas.com/golang/hotime/common/func.go:165.13,167.5 1 0
code.hoteas.com/golang/hotime/common/func.go:171.2,171.11 1 0
code.hoteas.com/golang/hotime/common/func.go:175.29,180.2 4 0
code.hoteas.com/golang/hotime/common/func.go:183.26,185.29 2 0
code.hoteas.com/golang/hotime/common/func.go:185.29,187.3 1 0
code.hoteas.com/golang/hotime/common/func.go:188.2,188.22 1 0
code.hoteas.com/golang/hotime/common/func.go:190.23,193.26 3 0
code.hoteas.com/golang/hotime/common/func.go:193.26,195.28 2 0
code.hoteas.com/golang/hotime/common/func.go:195.28,198.9 2 0
code.hoteas.com/golang/hotime/common/func.go:200.3,200.14 1 0
code.hoteas.com/golang/hotime/common/func.go:203.2,203.10 1 0
code.hoteas.com/golang/hotime/common/func.go:208.36,211.18 2 0
code.hoteas.com/golang/hotime/common/func.go:211.18,213.3 1 0
code.hoteas.com/golang/hotime/common/func.go:215.2,215.6 1 0
code.hoteas.com/golang/hotime/common/func.go:215.6,217.19 2 0
code.hoteas.com/golang/hotime/common/func.go:217.19,218.9 1 0
code.hoteas.com/golang/hotime/common/func.go:221.2,221.12 1 0
code.hoteas.com/golang/hotime/common/func.go:260.49,261.37 1 0
code.hoteas.com/golang/hotime/common/func.go:261.37,263.30 2 0
code.hoteas.com/golang/hotime/common/func.go:263.30,265.4 1 0
code.hoteas.com/golang/hotime/common/func.go:267.3,267.16 1 0
code.hoteas.com/golang/hotime/common/func.go:268.8,268.56 1 0
code.hoteas.com/golang/hotime/common/func.go:268.56,270.32 2 0
code.hoteas.com/golang/hotime/common/func.go:270.32,272.4 1 0
code.hoteas.com/golang/hotime/common/func.go:274.3,274.18 1 0
code.hoteas.com/golang/hotime/common/func.go:275.8,275.63 1 0
code.hoteas.com/golang/hotime/common/func.go:275.63,277.30 2 0
code.hoteas.com/golang/hotime/common/func.go:277.30,279.4 1 0
code.hoteas.com/golang/hotime/common/func.go:281.3,281.16 1 0
code.hoteas.com/golang/hotime/common/func.go:283.8,283.48 1 0
code.hoteas.com/golang/hotime/common/func.go:283.48,285.32 2 0
code.hoteas.com/golang/hotime/common/func.go:285.32,287.4 1 0
code.hoteas.com/golang/hotime/common/func.go:289.3,289.18 1 0
code.hoteas.com/golang/hotime/common/func.go:292.2,292.14 1 0
code.hoteas.com/golang/hotime/common/func.go:319.38,322.2 2 0
code.hoteas.com/golang/hotime/common/map.go:15.61,17.19 1 0
code.hoteas.com/golang/hotime/common/map.go:17.19,19.3 1 0
code.hoteas.com/golang/hotime/common/map.go:20.2,20.30 1 0
code.hoteas.com/golang/hotime/common/map.go:24.33,27.2 1 0
code.hoteas.com/golang/hotime/common/map.go:30.52,35.2 1 0
code.hoteas.com/golang/hotime/common/map.go:38.36,41.2 1 0
code.hoteas.com/golang/hotime/common/map.go:44.55,49.2 2 0
code.hoteas.com/golang/hotime/common/map.go:52.59,56.2 2 0
code.hoteas.com/golang/hotime/common/map.go:59.63,63.2 2 0
code.hoteas.com/golang/hotime/common/map.go:66.59,70.2 2 0
code.hoteas.com/golang/hotime/common/map.go:73.67,77.2 2 0
code.hoteas.com/golang/hotime/common/map.go:80.63,86.2 2 0
code.hoteas.com/golang/hotime/common/map.go:88.59,95.2 2 0
code.hoteas.com/golang/hotime/common/map.go:96.57,103.2 2 0
code.hoteas.com/golang/hotime/common/map.go:105.63,110.2 2 0
code.hoteas.com/golang/hotime/common/map.go:112.80,115.27 2 0
code.hoteas.com/golang/hotime/common/map.go:115.27,118.3 1 0
code.hoteas.com/golang/hotime/common/map.go:119.2,120.27 2 0
code.hoteas.com/golang/hotime/common/map.go:120.27,122.9 2 0
code.hoteas.com/golang/hotime/common/map.go:122.9,124.4 1 0
code.hoteas.com/golang/hotime/common/map.go:129.55,136.2 2 0
code.hoteas.com/golang/hotime/common/map.go:138.60,140.30 1 0
code.hoteas.com/golang/hotime/common/map.go:140.30,142.3 1 0
code.hoteas.com/golang/hotime/common/map.go:143.2,145.19 2 0
code.hoteas.com/golang/hotime/common/map.go:145.19,148.3 1 0
code.hoteas.com/golang/hotime/common/map.go:149.2,149.12 1 0
code.hoteas.com/golang/hotime/common/map.go:153.44,156.25 2 0
code.hoteas.com/golang/hotime/common/map.go:156.25,159.22 3 0
code.hoteas.com/golang/hotime/common/map.go:159.22,160.12 1 0
code.hoteas.com/golang/hotime/common/map.go:162.3,162.31 1 0
code.hoteas.com/golang/hotime/common/map.go:163.14,164.33 1 0
code.hoteas.com/golang/hotime/common/map.go:165.16,166.47 1 0
code.hoteas.com/golang/hotime/common/map.go:167.18,168.49 1 0
code.hoteas.com/golang/hotime/common/map.go:169.17,170.48 1 0
code.hoteas.com/golang/hotime/common/map.go:171.22,172.32 1 0
code.hoteas.com/golang/hotime/common/map.go:178.39,181.2 1 0
code.hoteas.com/golang/hotime/common/map.go:183.58,185.31 2 1
code.hoteas.com/golang/hotime/common/map.go:185.31,187.3 1 0
code.hoteas.com/golang/hotime/common/map.go:192.83,194.2 1 0
code.hoteas.com/golang/hotime/common/obj.go:12.40,14.2 1 0
code.hoteas.com/golang/hotime/common/obj.go:16.42,17.19 1 0
code.hoteas.com/golang/hotime/common/obj.go:17.19,19.3 1 0
code.hoteas.com/golang/hotime/common/obj.go:20.2,20.41 1 0
code.hoteas.com/golang/hotime/common/obj.go:23.50,24.19 1 0
code.hoteas.com/golang/hotime/common/obj.go:24.19,26.3 1 0
code.hoteas.com/golang/hotime/common/obj.go:27.2,27.42 1 0
code.hoteas.com/golang/hotime/common/obj.go:30.46,31.19 1 0
code.hoteas.com/golang/hotime/common/obj.go:31.19,33.3 1 0
code.hoteas.com/golang/hotime/common/obj.go:34.2,34.43 1 0
code.hoteas.com/golang/hotime/common/obj.go:37.50,38.19 1 0
code.hoteas.com/golang/hotime/common/obj.go:38.19,40.3 1 0
code.hoteas.com/golang/hotime/common/obj.go:41.2,41.45 1 0
code.hoteas.com/golang/hotime/common/obj.go:45.55,46.19 1 0
code.hoteas.com/golang/hotime/common/obj.go:46.19,48.3 1 0
code.hoteas.com/golang/hotime/common/obj.go:49.2,50.10 2 0
code.hoteas.com/golang/hotime/common/obj.go:54.33,57.2 1 0
code.hoteas.com/golang/hotime/common/obj.go:59.42,60.19 1 0
code.hoteas.com/golang/hotime/common/obj.go:60.19,62.3 1 0
code.hoteas.com/golang/hotime/common/obj.go:63.2,63.41 1 0
code.hoteas.com/golang/hotime/common/obj.go:66.46,67.19 1 0
code.hoteas.com/golang/hotime/common/obj.go:67.19,69.3 1 0
code.hoteas.com/golang/hotime/common/obj.go:70.2,70.43 1 0
code.hoteas.com/golang/hotime/common/obj.go:73.49,74.19 1 0
code.hoteas.com/golang/hotime/common/obj.go:74.19,76.3 1 0
code.hoteas.com/golang/hotime/common/obj.go:77.2,77.46 1 0
code.hoteas.com/golang/hotime/common/obj.go:80.38,83.2 1 0
code.hoteas.com/golang/hotime/common/obj.go:86.51,87.19 1 0
code.hoteas.com/golang/hotime/common/obj.go:87.19,89.3 1 0
code.hoteas.com/golang/hotime/common/obj.go:90.2,91.10 2 0
code.hoteas.com/golang/hotime/common/obj.go:96.47,97.19 1 0
code.hoteas.com/golang/hotime/common/obj.go:97.19,99.3 1 0
code.hoteas.com/golang/hotime/common/obj.go:100.2,101.10 2 0
code.hoteas.com/golang/hotime/common/obj.go:106.71,108.2 1 0
code.hoteas.com/golang/hotime/common/objtoobj.go:13.49,17.16 3 1
code.hoteas.com/golang/hotime/common/objtoobj.go:17.16,20.3 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:20.8,21.28 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:22.12,23.11 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:24.31,25.16 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:26.15,28.56 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:28.56,31.5 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:32.11,35.18 3 1
code.hoteas.com/golang/hotime/common/objtoobj.go:35.18,38.10 3 1
code.hoteas.com/golang/hotime/common/objtoobj.go:40.4,41.49 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:41.49,44.5 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:47.2,47.17 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:47.17,49.3 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:50.2,50.10 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:53.56,56.30 3 1
code.hoteas.com/golang/hotime/common/objtoobj.go:56.30,58.3 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:59.2,59.12 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:63.53,67.16 3 1
code.hoteas.com/golang/hotime/common/objtoobj.go:67.16,70.3 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:70.8,71.28 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:72.14,73.11 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:74.22,75.18 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:76.17,78.26 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:78.26,80.5 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:81.15,83.41 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:84.11,88.18 4 1
code.hoteas.com/golang/hotime/common/objtoobj.go:88.18,89.10 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:91.4,91.34 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:95.2,95.17 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:95.17,97.3 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:99.2,99.10 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:102.57,106.15 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:106.15,111.47 4 1
code.hoteas.com/golang/hotime/common/objtoobj.go:111.47,114.4 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:115.3,116.32 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:116.32,117.17 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:117.17,118.13 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:120.4,120.21 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:120.21,122.5 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:124.3,126.21 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:126.21,128.16 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:128.16,130.5 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:131.9,131.28 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:131.28,133.16 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:133.16,135.5 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:136.9,136.28 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:136.28,138.16 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:138.16,140.5 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:141.9,141.27 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:141.27,143.16 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:143.16,145.5 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:146.9,146.27 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:146.27,148.16 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:148.16,150.5 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:155.2,159.18 3 1
code.hoteas.com/golang/hotime/common/objtoobj.go:159.18,163.3 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:163.8,163.25 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:163.25,167.3 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:167.8,167.25 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:167.25,171.3 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:171.8,171.24 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:171.24,174.3 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:174.8,174.24 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:174.24,176.15 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:176.15,178.4 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:181.2,181.12 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:184.57,188.16 3 1
code.hoteas.com/golang/hotime/common/objtoobj.go:188.16,190.3 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:190.8,191.28 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:192.12,193.20 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:194.14,195.20 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:196.15,198.17 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:198.17,200.5 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:200.10,202.5 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:203.16,204.11 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:205.16,206.20 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:207.15,210.17 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:210.17,212.5 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:212.10,214.5 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:215.11,216.54 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:220.2,220.19 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:220.19,223.3 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:224.2,224.22 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:224.22,227.3 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:228.2,228.17 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:228.17,230.3 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:232.2,232.10 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:237.77,239.19 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:239.19,241.3 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:242.2,243.32 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:247.57,250.2 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:253.61,256.2 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:259.53,262.2 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:264.53,268.16 3 1
code.hoteas.com/golang/hotime/common/objtoobj.go:268.16,270.3 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:270.8,271.28 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:272.12,273.18 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:274.14,275.11 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:276.15,278.17 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:278.17,280.5 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:280.10,282.5 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:283.15,286.17 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:286.17,288.5 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:288.10,290.5 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:291.16,292.18 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:293.16,294.18 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:295.11,296.54 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:299.2,299.17 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:299.17,301.3 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:302.2,302.10 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:305.49,308.2 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:310.51,314.16 3 1
code.hoteas.com/golang/hotime/common/objtoobj.go:314.16,316.3 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:316.8,317.28 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:318.13,319.11 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:320.11,323.32 3 1
code.hoteas.com/golang/hotime/common/objtoobj.go:323.32,325.5 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:325.10,325.25 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:325.25,327.5 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:330.2,330.17 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:330.17,332.3 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:333.2,333.10 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:336.39,338.16 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:338.16,340.3 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:341.2,341.27 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:342.11,343.26 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:344.13,345.31 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:346.13,347.35 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:348.14,349.20 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:350.14,351.12 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:352.15,353.46 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:354.15,355.55 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:356.12,357.32 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:358.10,360.17 2 1
code.hoteas.com/golang/hotime/common/objtoobj.go:360.17,362.4 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:365.2,365.12 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:369.29,373.2 3 1
code.hoteas.com/golang/hotime/common/objtoobj.go:376.33,378.2 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:381.41,382.16 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:382.16,383.18 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:383.18,385.4 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:386.3,386.25 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:386.25,388.4 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:389.3,389.20 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:390.8,392.3 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:393.2,393.10 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:397.41,398.17 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:398.17,400.3 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:401.2,401.22 1 1
code.hoteas.com/golang/hotime/common/objtoobj.go:405.38,408.2 2 1
code.hoteas.com/golang/hotime/common/slice.go:11.60,12.19 1 0
code.hoteas.com/golang/hotime/common/slice.go:12.19,14.3 1 0
code.hoteas.com/golang/hotime/common/slice.go:15.2,15.30 1 0
code.hoteas.com/golang/hotime/common/slice.go:18.62,23.2 2 0
code.hoteas.com/golang/hotime/common/slice.go:26.54,29.2 2 0
code.hoteas.com/golang/hotime/common/slice.go:32.58,36.2 2 0
code.hoteas.com/golang/hotime/common/slice.go:39.62,43.2 2 0
code.hoteas.com/golang/hotime/common/slice.go:46.58,50.2 2 0
code.hoteas.com/golang/hotime/common/slice.go:53.66,57.2 2 0
code.hoteas.com/golang/hotime/common/slice.go:60.62,64.2 2 0
code.hoteas.com/golang/hotime/common/slice.go:67.82,69.2 1 0
code.hoteas.com/golang/hotime/common/slice.go:71.58,74.2 2 0
code.hoteas.com/golang/hotime/common/slice.go:76.56,83.2 2 0
code.hoteas.com/golang/hotime/common/slice.go:85.54,89.2 2 1
code.hoteas.com/golang/hotime/common/slice.go:91.59,93.21 1 0
code.hoteas.com/golang/hotime/common/slice.go:93.21,95.3 1 0
code.hoteas.com/golang/hotime/common/slice.go:96.2,97.19 2 0
code.hoteas.com/golang/hotime/common/slice.go:97.19,99.3 1 0
code.hoteas.com/golang/hotime/common/slice.go:100.2,100.12 1 0
code.hoteas.com/golang/hotime/common/slice.go:103.51,105.2 1 0
code.hoteas.com/golang/hotime/common/slice.go:107.41,109.2 1 0
+1257
View File
File diff suppressed because it is too large Load Diff
+177 -14
View File
@@ -4,7 +4,9 @@ import (
"database/sql"
"encoding/json"
"errors"
"math"
"reflect"
"strconv"
"strings"
. "code.hoteas.com/golang/hotime/common"
@@ -358,34 +360,195 @@ func (that *HoTimeDB) expandArrayPlaceholder(query string, args []interface{}) (
return result.String(), newArgs
}
const (
dbTypeUnknown = iota
dbTypeInteger
dbTypeFloat
dbTypeDecimal
)
// classifyDBType 将数据库类型名分类,覆盖 MySQL/SQLite/PostgreSQL
func classifyDBType(typeName string) int {
t := strings.ToUpper(strings.TrimSpace(typeName))
t = strings.TrimSuffix(strings.TrimSuffix(t, " UNSIGNED"), " SIGNED")
t = strings.TrimSpace(t)
base := t
if idx := strings.IndexByte(t, '('); idx > 0 {
base = t[:idx]
}
switch base {
case "INT", "INTEGER", "BIGINT", "TINYINT", "SMALLINT", "MEDIUMINT",
"INT2", "INT4", "INT8",
"SERIAL", "BIGSERIAL", "SMALLSERIAL",
"YEAR", "BOOL", "BOOLEAN", "OID":
return dbTypeInteger
case "DECIMAL", "NUMERIC", "NEWDECIMAL":
return dbTypeDecimal
case "FLOAT", "DOUBLE", "REAL", "FLOAT4", "FLOAT8", "DOUBLE PRECISION":
return dbTypeFloat
}
if strings.Contains(base, "INT") {
return dbTypeInteger
}
if strings.Contains(base, "DECIMAL") || strings.Contains(base, "NUMERIC") {
return dbTypeDecimal
}
if strings.Contains(base, "DOUBLE") || strings.Contains(base, "FLOAT") {
return dbTypeFloat
}
return dbTypeUnknown
}
// getDecimalScale 提取 DECIMAL/NUMERIC 精度,失败返回 -1
func getDecimalScale(colType *sql.ColumnType, typeName string) int {
_, scale, ok := colType.DecimalSize()
if ok && scale >= 0 {
return int(scale)
}
// SQLite 不支持 DecimalSize,从类型名解析 "DECIMAL(10,2)" → 2
upper := strings.ToUpper(typeName)
if idx := strings.Index(upper, ","); idx > 0 {
rest := upper[idx+1:]
if end := strings.IndexByte(rest, ')'); end > 0 {
if s, err := strconv.Atoi(strings.TrimSpace(rest[:end])); err == nil {
return s
}
}
}
return -1
}
// getFloatScale 提取 FLOAT/DOUBLE 显式精度,无显式精度返回 -1 表示不截断
func getFloatScale(colType *sql.ColumnType, typeName string) int {
_, scale, ok := colType.DecimalSize()
if ok && scale > 0 && scale < 20 {
return int(scale)
}
return -1
}
// fixFloatValue 对 float64 值做类型感知精度修正
func fixFloatValue(f float64, category int, scale int) interface{} {
if math.IsNaN(f) || math.IsInf(f, 0) {
return float64(0)
}
switch category {
case dbTypeInteger:
return int64(math.Round(f))
case dbTypeDecimal:
return roundFloat(f, scale)
case dbTypeFloat:
return roundFloat(f, scale)
default:
return f
}
}
// roundFloat 使用 strconv 精确舍入,避免 f*pow 中间值产生精度误差
func roundFloat(f float64, scale int) interface{} {
if math.IsNaN(f) || math.IsInf(f, 0) {
return float64(0)
}
if scale == 0 {
// DECIMAL(10,0) 明确是整数
return math.Round(f)
}
if scale < 0 {
// 精度未知,不修正,原样返回
return f
}
s := strconv.FormatFloat(f, 'f', scale, 64)
v, err := strconv.ParseFloat(s, 64)
if err != nil {
return f
}
return v
}
// convertBytes 对 []byte 类型的列做类型感知解析
// MySQL 文本协议和 PG 的 DECIMAL/NUMERIC 以 []byte 返回,需要按列类型解析为正确 Go 类型
func convertBytes(raw []byte, category int, scale int) interface{} {
s := string(raw)
switch category {
case dbTypeInteger:
if n, err := strconv.ParseInt(s, 10, 64); err == nil {
return n
}
if f, err := strconv.ParseFloat(s, 64); err == nil {
return int64(math.Round(f))
}
return s
case dbTypeDecimal:
if f, err := strconv.ParseFloat(s, 64); err == nil {
return roundFloat(f, scale)
}
return s
case dbTypeFloat:
if f, err := strconv.ParseFloat(s, 64); err == nil {
return roundFloat(f, scale)
}
return s
default:
return s
}
}
// Row 数据库数据解析
func (that *HoTimeDB) Row(resl *sql.Rows) []Map {
defer resl.Close()
dest := make([]Map, 0)
strs, _ := resl.Columns()
colTypes, _ := resl.ColumnTypes()
for i := 0; resl.Next(); i++ {
lis := make(Map, 0)
a := make([]interface{}, len(strs))
colCount := len(strs)
categories := make([]int, colCount)
scales := make([]int, colCount)
if colTypes != nil {
for j := 0; j < len(colTypes) && j < colCount; j++ {
typeName := colTypes[j].DatabaseTypeName()
categories[j] = classifyDBType(typeName)
switch categories[j] {
case dbTypeDecimal:
scales[j] = getDecimalScale(colTypes[j], typeName)
case dbTypeFloat:
scales[j] = getFloatScale(colTypes[j], typeName)
}
}
}
b := make([]interface{}, len(a))
for j := 0; j < len(a); j++ {
for resl.Next() {
lis := make(Map, colCount)
a := make([]interface{}, colCount)
b := make([]interface{}, colCount)
for j := 0; j < colCount; j++ {
b[j] = &a[j]
}
err := resl.Scan(b...)
if err != nil {
if err := resl.Scan(b...); err != nil {
that.LastErr.SetError(err)
return nil
}
for j := 0; j < len(a); j++ {
if a[j] != nil && reflect.ValueOf(a[j]).Type().String() == "[]uint8" {
lis[strs[j]] = string(a[j].([]byte))
} else {
lis[strs[j]] = a[j] // 取实际类型
for j := 0; j < colCount; j++ {
val := a[j]
if val == nil {
lis[strs[j]] = nil
continue
}
switch v := val.(type) {
case []byte:
lis[strs[j]] = convertBytes(v, categories[j], scales[j])
case float64:
lis[strs[j]] = fixFloatValue(v, categories[j], scales[j])
case float32:
lis[strs[j]] = fixFloatValue(float64(v), categories[j], scales[j])
default:
lis[strs[j]] = val
}
}
dest = append(dest, lis)
}
if err := resl.Err(); err != nil {
that.LastErr.SetError(err)
}
return dest
}
+245
View File
@@ -0,0 +1,245 @@
package db
import (
"math"
"testing"
)
// ─────────────────────────────────────────────
// classifyDBType
// ─────────────────────────────────────────────
func TestClassifyDBType(t *testing.T) {
cases := []struct {
typeName string
want int
}{
// MySQL 整数类型
{"INT", dbTypeInteger},
{"TINYINT", dbTypeInteger},
{"SMALLINT", dbTypeInteger},
{"MEDIUMINT", dbTypeInteger},
{"BIGINT", dbTypeInteger},
{"INT UNSIGNED", dbTypeInteger},
{"BIGINT UNSIGNED", dbTypeInteger},
{"TINYINT(1)", dbTypeInteger},
{"YEAR", dbTypeInteger},
{"BOOL", dbTypeInteger},
{"BOOLEAN", dbTypeInteger},
// PostgreSQL 整数类型
{"INT2", dbTypeInteger},
{"INT4", dbTypeInteger},
{"INT8", dbTypeInteger},
{"SERIAL", dbTypeInteger},
{"BIGSERIAL", dbTypeInteger},
{"SMALLSERIAL", dbTypeInteger},
{"OID", dbTypeInteger},
// DECIMAL / NUMERIC
{"DECIMAL", dbTypeDecimal},
{"DECIMAL(10,2)", dbTypeDecimal},
{"NUMERIC", dbTypeDecimal},
{"NUMERIC(8,4)", dbTypeDecimal},
{"NEWDECIMAL", dbTypeDecimal},
// FLOAT / DOUBLE
{"FLOAT", dbTypeFloat},
{"DOUBLE", dbTypeFloat},
{"REAL", dbTypeFloat},
{"FLOAT4", dbTypeFloat},
{"FLOAT8", dbTypeFloat},
{"DOUBLE PRECISION", dbTypeFloat},
// 未知类型
{"VARCHAR", dbTypeUnknown},
{"TEXT", dbTypeUnknown},
{"DATETIME", dbTypeUnknown},
{"TIMESTAMP", dbTypeUnknown},
{"BLOB", dbTypeUnknown},
{"", dbTypeUnknown},
}
for _, c := range cases {
t.Run(c.typeName, func(t *testing.T) {
got := classifyDBType(c.typeName)
if got != c.want {
t.Errorf("classifyDBType(%q) = %d, want %d", c.typeName, got, c.want)
}
})
}
}
// ─────────────────────────────────────────────
// fixFloatValue
// ─────────────────────────────────────────────
func TestFixFloatValue(t *testing.T) {
t.Run("integer category: round to int64", func(t *testing.T) {
// 这是核心场景:float64 精度噪声导致整数变为 1.9999...
got := fixFloatValue(1.9999999999999998, dbTypeInteger, 0)
if got != int64(2) {
t.Errorf("got %v (%T), want int64(2)", got, got)
}
})
t.Run("integer category: 2.0000000000001 rounds to 2", func(t *testing.T) {
got := fixFloatValue(2.0000000000001, dbTypeInteger, 0)
if got != int64(2) {
t.Errorf("got %v (%T), want int64(2)", got, got)
}
})
t.Run("decimal category: scale=2 roundtrip precision", func(t *testing.T) {
got := fixFloatValue(1.1999999999999999, dbTypeDecimal, 2)
f, ok := got.(float64)
if !ok {
t.Fatalf("expected float64, got %T", got)
}
if f != 1.2 {
t.Errorf("got %v, want 1.2", f)
}
})
t.Run("decimal category: scale=2, 1.20000000001 -> 1.2", func(t *testing.T) {
got := fixFloatValue(1.20000000001, dbTypeDecimal, 2)
f := got.(float64)
if f != 1.2 {
t.Errorf("got %v, want 1.2", f)
}
})
t.Run("decimal category: scale=-1 (unknown), no modification", func(t *testing.T) {
got := fixFloatValue(1.23456789, dbTypeDecimal, -1)
if got != 1.23456789 {
t.Errorf("got %v, want 1.23456789", got)
}
})
t.Run("decimal category: scale=0, round to whole number", func(t *testing.T) {
got := fixFloatValue(2.7, dbTypeDecimal, 0)
if got != float64(3) {
t.Errorf("got %v (%T), want float64(3)", got, got)
}
})
t.Run("float category: scale=-1 no modification", func(t *testing.T) {
got := fixFloatValue(3.14159265, dbTypeFloat, -1)
if got != 3.14159265 {
t.Errorf("got %v, want 3.14159265", got)
}
})
t.Run("unknown category: passthrough", func(t *testing.T) {
got := fixFloatValue(99.9, dbTypeUnknown, -1)
if got != 99.9 {
t.Errorf("got %v, want 99.9", got)
}
})
t.Run("NaN -> 0", func(t *testing.T) {
got := fixFloatValue(math.NaN(), dbTypeInteger, 0)
if got != float64(0) {
t.Errorf("got %v, want 0", got)
}
})
t.Run("+Inf -> 0", func(t *testing.T) {
got := fixFloatValue(math.Inf(1), dbTypeDecimal, 2)
if got != float64(0) {
t.Errorf("got %v, want 0", got)
}
})
}
// ─────────────────────────────────────────────
// roundFloat
// ─────────────────────────────────────────────
func TestRoundFloat(t *testing.T) {
t.Run("scale=2: 1.19999... -> 1.2", func(t *testing.T) {
got := roundFloat(1.1999999999999999, 2)
if got != float64(1.2) {
t.Errorf("got %v, want 1.2", got)
}
})
t.Run("scale=2: 1.20000...1 -> 1.2", func(t *testing.T) {
got := roundFloat(1.20000000001, 2)
if got != float64(1.2) {
t.Errorf("got %v, want 1.2", got)
}
})
t.Run("scale=0: round to integer float", func(t *testing.T) {
got := roundFloat(2.5, 0)
if got != float64(3) {
t.Errorf("got %v, want 3.0", got)
}
})
t.Run("scale=-1: passthrough unchanged", func(t *testing.T) {
got := roundFloat(1.23456789, -1)
if got != 1.23456789 {
t.Errorf("got %v, want 1.23456789", got)
}
})
t.Run("NaN -> 0", func(t *testing.T) {
got := roundFloat(math.NaN(), 2)
if got != float64(0) {
t.Errorf("got %v, want 0", got)
}
})
}
// ─────────────────────────────────────────────
// convertBytes
// ─────────────────────────────────────────────
func TestConvertBytes(t *testing.T) {
t.Run("integer category: '2' -> int64(2)", func(t *testing.T) {
got := convertBytes([]byte("2"), dbTypeInteger, 0)
if got != int64(2) {
t.Errorf("got %v (%T), want int64(2)", got, got)
}
})
t.Run("integer category: '2.00' -> int64(2)", func(t *testing.T) {
got := convertBytes([]byte("2.00"), dbTypeInteger, 0)
if got != int64(2) {
t.Errorf("got %v (%T), want int64(2)", got, got)
}
})
t.Run("decimal category: '1.20' scale=2 -> float64(1.2)", func(t *testing.T) {
got := convertBytes([]byte("1.20"), dbTypeDecimal, 2)
if got != float64(1.2) {
t.Errorf("got %v (%T), want float64(1.2)", got, got)
}
})
t.Run("decimal category: '0.00' scale=2 -> float64(0)", func(t *testing.T) {
got := convertBytes([]byte("0.00"), dbTypeDecimal, 2)
if got != float64(0) {
t.Errorf("got %v (%T), want float64(0)", got, got)
}
})
t.Run("float category: '3.14' scale=-1 -> float64(3.14)", func(t *testing.T) {
got := convertBytes([]byte("3.14"), dbTypeFloat, -1)
if got != float64(3.14) {
t.Errorf("got %v (%T), want float64(3.14)", got, got)
}
})
t.Run("unknown category: passthrough as string", func(t *testing.T) {
got := convertBytes([]byte("hello"), dbTypeUnknown, 0)
if got != "hello" {
t.Errorf("got %v (%T), want 'hello'", got, got)
}
})
t.Run("integer category: invalid bytes -> string fallback", func(t *testing.T) {
got := convertBytes([]byte("abc"), dbTypeInteger, 0)
if got != "abc" {
t.Errorf("got %v (%T), want 'abc'", got, got)
}
})
}
+6 -1
View File
@@ -20,7 +20,12 @@ func GetLog(path string, showCodeLine bool) *log.Logger {
}
loger := log.New()
loger.SetFormatter(&log.TextFormatter{})
loger.SetFormatter(&log.TextFormatter{
ForceColors: true,
FullTimestamp: true,
TimestampFormat: "2006-01-02 15:04:05",
DisableLevelTruncation: true,
})
loger.AddHook(&hook)
return loger
}