Files
2026-08-17 13:19:15 +08:00

264 lines
6.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package common
import (
"database/sql"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"math"
"os"
"strings"
)
// RoundInt 浮点数量(克)× 单价(分)等金额计算的四舍五入到整数分
func RoundInt(f float64) int64 {
return int64(math.Round(f))
}
// IsNotFound GoFrame Scan/One 无匹配行时返回 sql.ErrNoRows
// dao 层统一归一为「无记录」(返回 nil 实体),不作为系统错误向上抛
func IsNotFound(err error) bool {
return err != nil && errors.Is(err, sql.ErrNoRows)
}
// ImageFileToBase64 reads an image file and returns a data:image/...;base64 string.
func ImageFileToBase64(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", err
}
ext := strings.ToLower(pathExt(path))
mime := "image/png"
switch ext {
case ".jpg", ".jpeg":
mime = "image/jpeg"
case ".gif":
mime = "image/gif"
case ".webp":
mime = "image/webp"
}
return "data:" + mime + ";base64," + base64.StdEncoding.EncodeToString(data), nil
}
// pathExt extracts the extension from a path.
func pathExt(path string) string {
for i := len(path) - 1; i >= 0 && path[i] != '/'; i-- {
if path[i] == '.' {
return path[i:]
}
}
return ""
}
// BuildSchemaRequest validates input values against a JSON schema definition,
// fills in default values for missing optional fields,
// and returns the result matching the schema's nested structure.
//
// The schema format follows test.json convention:
//
// {
// "section": {
// "field_name": {
// "type": "string|integer|number|boolean|array|object",
// "required": true|false,
// "default": value,
// "enum": [...],
// "min": number,
// "max": number,
// "max_chars": number,
// "min_items": number,
// "max_items": number
// }
// }
// }
//
// input is a flat map like {"prompt": "hello", "duration": 5}.
// Fields not present in input but with a "default" in the schema are filled automatically.
// Nodes without "type" are treated as grouping sections and recursed into.
// When validate is false, required/type/range/enum checks are skipped (only structure + defaults).
func BuildSchemaRequest(schema map[string]any, input map[string]any, validate bool) (map[string]any, error) {
result := make(map[string]any)
for key, val := range schema {
fieldDef, ok := val.(map[string]any)
if !ok {
result[key] = val
continue
}
if _, hasType := fieldDef["type"]; hasType {
processed, err := processField(key, fieldDef, input, validate)
if err != nil {
return nil, err
}
if processed != nil {
result[key] = processed
}
continue
}
nested, err := BuildSchemaRequest(fieldDef, input, validate)
if err != nil {
return nil, err
}
if len(nested) > 0 {
result[key] = nested
}
}
return result, nil
}
func processField(name string, def map[string]any, input map[string]any, validate bool) (any, error) {
fieldType, _ := def["type"].(string)
required, _ := def["required"].(bool)
rawVal, exists := input[name]
if !exists {
if validate && required {
return nil, fmt.Errorf("%s", def["description"])
}
if dflt, ok := def["default"]; ok {
return convertDefault(dflt, fieldType), nil
}
return nil, nil
}
if !validate {
return rawVal, nil
}
switch fieldType {
case "string":
s, ok := rawVal.(string)
if !ok {
return nil, fmt.Errorf("'%s' must be a string", name)
}
if maxChars, ok := def["max_chars"].(float64); ok && len([]rune(s)) > int(maxChars) {
return nil, fmt.Errorf("'%s' exceeds max length of %d", name, int(maxChars))
}
if enum, ok := def["enum"].([]any); ok && len(enum) > 0 {
if !containsValue(enum, s) {
return nil, fmt.Errorf("'%s' must be one of %v", name, enum)
}
}
return s, nil
case "integer":
v, err := toInt(rawVal)
if err != nil {
return nil, fmt.Errorf("'%s' must be an integer", name)
}
if minVal, ok := def["min"].(float64); ok && v < int(minVal) {
return nil, fmt.Errorf("'%s' must be >= %d", name, int(minVal))
}
if maxVal, ok := def["max"].(float64); ok && v > int(maxVal) {
return nil, fmt.Errorf("'%s' must be <= %d", name, int(maxVal))
}
return v, nil
case "number":
v, ok := rawVal.(float64)
if !ok {
if iv, err := toInt(rawVal); err == nil {
v = float64(iv)
} else {
return nil, fmt.Errorf("'%s' must be a number", name)
}
}
if minVal, ok := def["min"].(float64); ok && v < minVal {
return nil, fmt.Errorf("'%s' must be >= %v", name, minVal)
}
if maxVal, ok := def["max"].(float64); ok && v > maxVal {
return nil, fmt.Errorf("'%s' must be <= %v", name, maxVal)
}
return v, nil
case "boolean":
_, ok := rawVal.(bool)
if !ok {
return nil, fmt.Errorf("'%s' must be a boolean", name)
}
return rawVal, nil
case "array":
arr, ok := rawVal.([]any)
if !ok {
return nil, fmt.Errorf("'%s' must be an array", name)
}
if minItems, ok := def["min_items"].(float64); ok && len(arr) < int(minItems) {
return nil, fmt.Errorf("'%s' must have at least %d items", name, int(minItems))
}
if maxItems, ok := def["max_items"].(float64); ok && len(arr) > int(maxItems) {
return nil, fmt.Errorf("'%s' must have at most %d items", name, int(maxItems))
}
if itemsDef, ok := def["items"].(map[string]any); ok {
items, err := processArrayItems(arr, itemsDef, validate)
if err != nil {
return nil, fmt.Errorf("'%s': %w", name, err)
}
return items, nil
}
return arr, nil
}
return rawVal, nil
}
func processArrayItems(arr []any, itemsDef map[string]any, validate bool) ([]any, error) {
itemType, _ := itemsDef["type"].(string)
if itemType != "object" {
return arr, nil
}
props, _ := itemsDef["properties"].(map[string]any)
if props == nil {
return arr, nil
}
result := make([]any, len(arr))
for i, item := range arr {
itemMap, ok := item.(map[string]any)
if !ok {
result[i] = item
continue
}
processed, err := BuildSchemaRequest(props, itemMap, validate)
if err != nil {
return nil, fmt.Errorf("item[%d]: %w", i, err)
}
result[i] = processed
}
return result, nil
}
func toInt(v any) (int, error) {
switch val := v.(type) {
case float64:
return int(val), nil
case int:
return val, nil
case int64:
return int(val), nil
case json.Number:
n, err := val.Int64()
return int(n), err
default:
return 0, fmt.Errorf("cannot convert %T to int", v)
}
}
func convertDefault(dflt any, fieldType string) any {
if fieldType == "integer" {
if f, ok := dflt.(float64); ok {
return int(f)
}
}
return dflt
}
func containsValue(arr []any, val any) bool {
for _, v := range arr {
if v == val {
return true
}
}
return false
}