package db import ( "encoding/hex" "encoding/json" "fmt" "math/big" "reflect" "strconv" "strings" "time" "unicode" "unicode/utf8" mssql "github.com/microsoft/go-mssqldb" ) const ( jsMaxSafeInteger int64 = 9007199254740991 jsMinSafeInteger int64 = -9007199254740991 jsMaxSafeUint uint64 = 9007199254740991 ) var ( jsMaxSafeBigInt = big.NewInt(jsMaxSafeInteger) jsMinSafeBigInt = big.NewInt(jsMinSafeInteger) ) // normalizeQueryValue normalizes driver-returned values for UI/JSON transport. // 当前主要处理 []byte:如果是可读文本则转为 string,否则转为十六进制字符串,避免前端出现“空白值”。 func normalizeQueryValue(v interface{}) interface{} { return normalizeQueryValueWithDBType(v, "") } func normalizeQueryValueWithDBType(v interface{}, databaseTypeName string) interface{} { if tm, ok := v.(time.Time); ok { return normalizeTemporalValueForDisplay(tm, databaseTypeName) } if b, ok := v.([]byte); ok { return bytesToDisplayValue(b, databaseTypeName) } return normalizeCompositeQueryValue(v) } func normalizeTemporalValueForDisplay(value time.Time, databaseTypeName string) interface{} { if value.IsZero() { if zeroValue, ok := zeroTemporalDisplayValue(databaseTypeName); ok { return zeroValue } } return value.Format(time.RFC3339Nano) } func zeroTemporalDisplayValue(databaseTypeName string) (string, bool) { typeName := strings.ToUpper(strings.TrimSpace(databaseTypeName)) if typeName == "" { return "0000-00-00 00:00:00", true } switch { case strings.Contains(typeName, "TIMESTAMP") || strings.Contains(typeName, "DATETIME"): return "0000-00-00 00:00:00", true case typeName == "DATE" || typeName == "NEWDATE": return "0000-00-00", true case strings.Contains(typeName, "TIME"): return "00:00:00", true case strings.Contains(typeName, "YEAR"): return "0000", true default: return "", false } } func normalizeCompositeQueryValue(v interface{}) interface{} { if v == nil { return nil } switch typed := v.(type) { case []interface{}: items := make([]interface{}, len(typed)) for i, item := range typed { items[i] = normalizeQueryValue(item) } return items case map[string]interface{}: out := make(map[string]interface{}, len(typed)) for key, value := range typed { out[key] = normalizeQueryValue(value) } return out case json.Number: return normalizeJSONNumberForJS(typed) } rv := reflect.ValueOf(v) switch rv.Kind() { case reflect.Pointer: if rv.IsNil() { return nil } return normalizeQueryValue(rv.Elem().Interface()) case reflect.Map: if rv.IsNil() { return nil } out := make(map[string]interface{}, rv.Len()) iter := rv.MapRange() for iter.Next() { out[mapKeyToString(iter.Key().Interface())] = normalizeQueryValue(iter.Value().Interface()) } return out case reflect.Slice, reflect.Array: // []byte 在上层已单独处理,这里保留对其它切片/数组的递归规整。 if rv.Kind() == reflect.Slice && rv.IsNil() { return nil } size := rv.Len() items := make([]interface{}, size) for i := 0; i < size; i++ { items[i] = normalizeQueryValue(rv.Index(i).Interface()) } return items case reflect.Struct: // 部分驱动(如 Kingbase)会返回复杂结构体值,直接透传会导致前端渲染和比较开销激增。 // 统一降级为可读字符串,避免对象深层序列化触发 UI 卡顿。 if tm, ok := v.(time.Time); ok { return normalizeTemporalValueForDisplay(tm, "") } if stringer, ok := v.(fmt.Stringer); ok { return stringer.String() } return fmt.Sprintf("%v", v) default: return normalizeUnsafeIntegerForJS(rv, v) } } func normalizeJSONNumberForJS(n json.Number) interface{} { text := strings.TrimSpace(n.String()) if text == "" { return "" } if integer, ok := parseJSONInteger(text); ok { if integer.Cmp(jsMaxSafeBigInt) > 0 || integer.Cmp(jsMinSafeBigInt) < 0 { return text } return integer.Int64() } if f, err := n.Float64(); err == nil { return f } return text } func parseJSONInteger(text string) (*big.Int, bool) { if text == "" { return nil, false } start := 0 if text[0] == '+' || text[0] == '-' { if len(text) == 1 { return nil, false } start = 1 } for i := start; i < len(text); i++ { if text[i] < '0' || text[i] > '9' { return nil, false } } value, ok := new(big.Int).SetString(text, 10) if !ok { return nil, false } return value, true } func mapKeyToString(key interface{}) string { if key == nil { return "null" } if s, ok := key.(string); ok { return s } return fmt.Sprintf("%v", key) } func bytesToDisplayValue(b []byte, databaseTypeName string) interface{} { if b == nil { return nil } if len(b) == 0 { return "" } dbType := strings.ToUpper(strings.TrimSpace(databaseTypeName)) if isSQLServerUniqueIdentifierType(dbType) { var guid mssql.UniqueIdentifier if err := guid.Scan(b); err == nil { return guid.String() } } if isBitLikeDBType(dbType) { if u, ok := bytesToUint64(b); ok { // JS number precision is limited; keep large bitmasks as string. if u <= jsMaxSafeUint { return int64(u) } return fmt.Sprintf("%d", u) } } if utf8.Valid(b) { s := string(b) if isMostlyPrintable(s) { return s } } // Fallback: some drivers return BIT(1) as []byte{0} / []byte{1} without type info. if dbType == "" && len(b) == 1 && (b[0] == 0 || b[0] == 1) { return int64(b[0]) } return bytesToReadableString(b) } func bytesToReadableString(b []byte) interface{} { if b == nil { return nil } if len(b) == 0 { return "" } return "0x" + hex.EncodeToString(b) } func isSQLServerUniqueIdentifierType(typeName string) bool { return typeName == "UNIQUEIDENTIFIER" } func isBitLikeDBType(typeName string) bool { if typeName == "" { return false } switch typeName { case "BIT", "VARBIT": return true default: } return strings.HasPrefix(typeName, "BIT") } func bytesToUint64(b []byte) (uint64, bool) { if len(b) == 0 || len(b) > 8 { return 0, false } var u uint64 for _, v := range b { u = (u << 8) | uint64(v) } return u, true } func normalizeUnsafeIntegerForJS(rv reflect.Value, original interface{}) interface{} { switch rv.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: n := rv.Int() if n > jsMaxSafeInteger || n < jsMinSafeInteger { return strconv.FormatInt(n, 10) } return original case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: u := rv.Uint() if u > jsMaxSafeUint { return strconv.FormatUint(u, 10) } return original default: return original } } func isMostlyPrintable(s string) bool { if s == "" { return true } total := 0 printable := 0 for _, r := range s { total++ switch r { case '\n', '\r', '\t': printable++ continue default: } if unicode.IsPrint(r) { printable++ } } // 允许少量不可见字符,避免把正常文本误判为二进制。 return printable*100 >= total*90 }