refactor(app): 优化 URL 处理和 JSON 解析逻辑
- 更新应用程序处理程序中的 URL 赋值逻辑,确保静态文件使用原始路径 - 修改缓存数据库的 JSON 解析方法,使用 JsonToObj 函数替代 json.Unmarshal,提升代码可读性和性能 - 在 Map 和 Slice 类型中新增获取四舍五入浮点数的方法,增强数据处理能力 - 在 Obj 类型中添加四舍五入功能,支持精度控制 - 改进数据库查询结果的处理逻辑,确保数据类型的准确性和一致性 - 优化日志格式设置,增强日志信息的可读性
This commit is contained in:
@@ -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字符 [>] → MATCH,strips → 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)
|
||||
|
||||
---
|
||||
|
||||
## 一、严重 Bug(uint8 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.go(Row 重写)、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` |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 文件 1:common/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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 增强 ObjToMap(2 处 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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 增强 ObjToSlice(2 处 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("没有合适的转换对象!")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 文件 2:common/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)` → **直接修复**
|
||||
|
||||
---
|
||||
|
||||
## 文件 3:db/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`,其余不变
|
||||
|
||||
---
|
||||
|
||||
## 文件 4:cache/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
@@ -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] != "" {
|
||||
|
||||
|
||||
Vendored
+3
-6
@@ -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
@@ -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...)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
+177
-14
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user