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。
|
||||
Reference in New Issue
Block a user