package db import ( "database/sql" "encoding/json" "errors" "fmt" "math" "reflect" "strconv" "strings" "sync/atomic" "time" . "code.hoteas.com/golang/hotime/common" ) // md5 生成查询的 MD5 哈希(用于缓存) func (that *HoTimeDB) md5(query string, args ...interface{}) string { strByte, _ := json.Marshal(args) str := Md5(query + ":" + string(strByte)) return str } // Query 执行查询 SQL func (that *HoTimeDB) Query(query string, args ...interface{}) []Map { return that.queryWithRetry(query, false, args...) } // queryWithRetry 内部查询方法,支持重试标记 func (that *HoTimeDB) queryWithRetry(query string, retried bool, args ...interface{}) []Map { query, args = that.expandArrayPlaceholder(query, args) var sqlErr error defer func() { e := &DBError{Query: query, Data: args} e.SetError(sqlErr) that.lastError.Store(e) that.logSQL(query, args, sqlErr) }() var resl *sql.Rows db := that.DB if that.SlaveDB != nil { db = that.SlaveDB } if db == nil { sqlErr = errors.New("没有初始化数据库") return nil } processedArgs := that.processArgs(args) if that.testTx != nil { if that.testMu != nil { that.testMu.Lock() } resl, sqlErr = that.testTx.Query(query, processedArgs...) if sqlErr != nil { if that.txFailed != nil { *that.txFailed = true } if that.testMu != nil { that.testMu.Unlock() } return nil } result := that.Row(resl) if that.testMu != nil { that.testMu.Unlock() } return result } else if that.Tx != nil { resl, sqlErr = that.Tx.Query(query, processedArgs...) } else { resl, sqlErr = db.Query(query, processedArgs...) } if sqlErr != nil { if that.Tx != nil { if that.txFailed != nil { *that.txFailed = true } } if !retried && that.Tx == nil { if pingErr := db.Ping(); pingErr == nil { sqlErr = nil // 重置,让 retry 的 defer 覆盖 return that.queryWithRetry(query, true, args...) } } return nil } return that.Row(resl) } // Exec 执行非查询 SQL func (that *HoTimeDB) Exec(query string, args ...interface{}) (sql.Result, error) { return that.execWithRetry(query, false, args...) } // execWithRetry 内部执行方法,支持重试标记 func (that *HoTimeDB) execWithRetry(query string, retried bool, args ...interface{}) (sql.Result, error) { query, args = that.expandArrayPlaceholder(query, args) var sqlErr error var resl sql.Result defer func() { e := &DBError{Query: query, Data: args} e.SetError(sqlErr) that.lastError.Store(e) that.logSQL(query, args, sqlErr) }() if that.DB == nil { sqlErr = errors.New("没有初始化数据库") return nil, sqlErr } processedArgs := that.processArgs(args) if that.testTx != nil { if that.testMu != nil { that.testMu.Lock() } resl, sqlErr = that.testTx.Exec(query, processedArgs...) if that.testMu != nil { that.testMu.Unlock() } } else if that.Tx != nil { resl, sqlErr = that.Tx.Exec(query, processedArgs...) } else { resl, sqlErr = that.DB.Exec(query, processedArgs...) } if sqlErr != nil { if that.testTx != nil { if that.txFailed != nil { *that.txFailed = true } return resl, sqlErr } if that.Tx != nil { if that.txFailed != nil { *that.txFailed = true } return resl, sqlErr } if !retried { if pingErr := that.DB.Ping(); pingErr == nil { sqlErr = nil return that.execWithRetry(query, true, args...) } } return resl, sqlErr } return resl, sqlErr } // logSQL 统一 SQL 日志输出(仅此一处打印,消除重复) func (that *HoTimeDB) logSQL(query string, args []interface{}, err error) { if err != nil { msg := fmt.Sprintf("SQL: %s DATA: %v ERROR: %v", query, args, err) that.testLogBufMu.Lock() if that.testLogBuf != nil { that.testLogBuf.WriteString(msg + "\n") atomic.AddInt32(&that.testSQLErrCount, 1) } else if that.Log != nil { that.Log.Error().Msg(msg) } that.testLogBufMu.Unlock() return } // 无错误时只在 logLevel > 0 时打印 DEBUG 级别 if that.Log != nil && that.Log.GetLevel() > 0 { msg := fmt.Sprintf("SQL: %s DATA: %v", query, args) that.testLogBufMu.Lock() if that.testLogBuf != nil { that.testLogBuf.WriteString(msg + "\n") } else { that.Log.Debug().Msg(msg) } that.testLogBufMu.Unlock() } } // processArgs 处理参数中的 slice 类型 func (that *HoTimeDB) processArgs(args []interface{}) []interface{} { processedArgs := make([]interface{}, len(args)) copy(processedArgs, args) for key := range processedArgs { arg := processedArgs[key] if arg == nil { continue } argType := reflect.ValueOf(arg).Type().String() if strings.Contains(argType, "[]") || strings.Contains(argType, "Slice") { argLis := ObjToSlice(arg) // 将slice转为逗号分割字符串 argStr := "" for i := 0; i < len(argLis); i++ { if i == len(argLis)-1 { argStr += ObjToStr(argLis[i]) } else { argStr += ObjToStr(argLis[i]) + "," } } processedArgs[key] = argStr } } return processedArgs } // expandArrayPlaceholder 展开 IN (?) / NOT IN (?) 中的数组参数 // 自动识别 IN/NOT IN (?) 模式,当参数是数组时展开为多个 ? // // 示例: // // db.Query("SELECT * FROM user WHERE id IN (?)", []int{1, 2, 3}) // // 展开为: SELECT * FROM user WHERE id IN (?, ?, ?) 参数: [1, 2, 3] // // db.Query("SELECT * FROM user WHERE id IN (?)", []int{}) // // 展开为: SELECT * FROM user WHERE 1=0 参数: [] (空集合的IN永假) // // db.Query("SELECT * FROM user WHERE id NOT IN (?)", []int{}) // // 展开为: SELECT * FROM user WHERE 1=1 参数: [] (空集合的NOT IN永真) // // db.Query("SELECT * FROM user WHERE id = ?", 1) // // 保持不变: SELECT * FROM user WHERE id = ? 参数: [1] func (that *HoTimeDB) expandArrayPlaceholder(query string, args []interface{}) (string, []interface{}) { if len(args) == 0 || !strings.Contains(query, "?") { return query, args } // 检查是否有数组参数 hasArray := false for _, arg := range args { if arg == nil { continue } argType := reflect.ValueOf(arg).Type().String() if strings.Contains(argType, "[]") || strings.Contains(argType, "Slice") { hasArray = true break } } if !hasArray { return query, args } newArgs := make([]interface{}, 0, len(args)) result := strings.Builder{} argIndex := 0 for i := 0; i < len(query); i++ { if query[i] == '?' && argIndex < len(args) { arg := args[argIndex] argIndex++ if arg == nil { result.WriteByte('?') newArgs = append(newArgs, nil) continue } argType := reflect.ValueOf(arg).Type().String() if strings.Contains(argType, "[]") || strings.Contains(argType, "Slice") { // 是数组参数,检查是否在 IN (...) 或 NOT IN (...) 中 prevPart := result.String() prevUpper := strings.ToUpper(prevPart) // 查找最近的 NOT IN ( 模式 notInIndex := strings.LastIndex(prevUpper, " NOT IN (") notInIndex2 := strings.LastIndex(prevUpper, " NOT IN(") if notInIndex2 > notInIndex { notInIndex = notInIndex2 } // 查找最近的 IN ( 模式(但要排除 NOT IN 的情况) inIndex := strings.LastIndex(prevUpper, " IN (") inIndex2 := strings.LastIndex(prevUpper, " IN(") if inIndex2 > inIndex { inIndex = inIndex2 } // 判断是 NOT IN 还是 IN // 注意:" NOT IN (" 包含 " IN (",所以如果找到的 IN 位置在 NOT IN 范围内,应该优先判断为 NOT IN isNotIn := false matchIndex := -1 if notInIndex != -1 { // 检查 inIndex 是否在 notInIndex 范围内(即 NOT IN 的 IN 部分) // NOT IN ( 的 IN ( 部分从 notInIndex + 4 开始 if inIndex != -1 && inIndex >= notInIndex && inIndex <= notInIndex+5 { // inIndex 是 NOT IN 的一部分,使用 NOT IN isNotIn = true matchIndex = notInIndex } else if inIndex == -1 || notInIndex > inIndex { // 没有独立的 IN,或 NOT IN 在 IN 之后 isNotIn = true matchIndex = notInIndex } else { // 有独立的 IN 且在 NOT IN 之后 matchIndex = inIndex } } else if inIndex != -1 { matchIndex = inIndex } // 检查 IN ( 后面是否只有空格(即当前 ? 紧跟在 IN ( 后面) isInPattern := false if matchIndex != -1 { afterIn := prevPart[matchIndex:] // 找到 ( 的位置 parenIdx := strings.Index(afterIn, "(") if parenIdx != -1 { afterParen := strings.TrimSpace(afterIn[parenIdx+1:]) if afterParen == "" { isInPattern = true } } } if isInPattern { // 在 IN (...) 或 NOT IN (...) 模式中 argList := ObjToSlice(arg) if len(argList) == 0 { // 空数组处理:需要找到字段名的开始位置 // 往前找最近的 AND/OR/WHERE/(,以确定条件的开始位置 truncateIndex := matchIndex searchPart := prevUpper[:matchIndex] // 找最近的分隔符位置 andIdx := strings.LastIndex(searchPart, " AND ") orIdx := strings.LastIndex(searchPart, " OR ") whereIdx := strings.LastIndex(searchPart, " WHERE ") parenIdx := strings.LastIndex(searchPart, "(") // 取最靠后的分隔符 sepIndex := -1 sepLen := 0 if andIdx > sepIndex { sepIndex = andIdx sepLen = 5 // " AND " } if orIdx > sepIndex { sepIndex = orIdx sepLen = 4 // " OR " } if whereIdx > sepIndex { sepIndex = whereIdx sepLen = 7 // " WHERE " } if parenIdx > sepIndex { sepIndex = parenIdx sepLen = 1 // "(" } if sepIndex != -1 { truncateIndex = sepIndex + sepLen } result.Reset() result.WriteString(prevPart[:truncateIndex]) if isNotIn { // NOT IN 空集合 = 永真 result.WriteString(" 1=1 ") } else { // IN 空集合 = 永假 result.WriteString(" 1=0 ") } // 跳过后面的 ) for j := i + 1; j < len(query); j++ { if query[j] == ')' { i = j break } } } else if len(argList) == 1 { // 单元素数组 result.WriteByte('?') newArgs = append(newArgs, argList[0]) } else { // 多元素数组,展开为多个 ? for j := 0; j < len(argList); j++ { if j > 0 { result.WriteString(", ") } result.WriteByte('?') newArgs = append(newArgs, argList[j]) } } } else { // 不在 IN 模式中,保持原有行为(数组会被 processArgs 转为逗号字符串) result.WriteByte('?') newArgs = append(newArgs, arg) } } else { // 非数组参数 result.WriteByte('?') newArgs = append(newArgs, arg) } } else { result.WriteByte(query[i]) } } 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", "NUMBER": 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() 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) } } } 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] } if err := resl.Scan(b...); err != nil { dbErr := &DBError{} dbErr.SetError(err) that.lastError.Store(dbErr) return nil } for j := 0; j < colCount; j++ { colKey := strs[j] val := a[j] if val == nil { lis[colKey] = nil continue } switch v := val.(type) { case []byte: lis[colKey] = convertBytes(v, categories[j], scales[j]) case float64: lis[colKey] = fixFloatValue(v, categories[j], scales[j]) case float32: lis[colKey] = fixFloatValue(float64(v), categories[j], scales[j]) case time.Time: // DM8驱动直接返回time.Time,格式化为MySQL兼容字符串 lis[colKey] = v.Format("2006-01-02 15:04:05") default: lis[colKey] = val } } dest = append(dest, lis) } if err := resl.Err(); err != nil { dbErr := &DBError{} dbErr.SetError(err) that.lastError.Store(dbErr) } return dest }