3430bdec19
- 在 ApiCase 中添加失败原因记录,提升测试结果的可读性 - 更新 CoverageReport 结构,增加总用例、通过用例和失败用例计数 - 优化 PrintCoverage 方法,支持详细输出未通过接口及其失败原因 - 更新 Swagger 生成逻辑,包含失败原因字段,增强调试信息 - 改进 SQL 错误计数逻辑,确保在测试模式下准确记录 SQL 错误
616 lines
16 KiB
Go
616 lines
16 KiB
Go
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
|
|
}
|