444 lines
11 KiB
Go
444 lines
11 KiB
Go
package pkg
|
||
|
||
import (
|
||
"ai_scheduler/internal/entitys"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"math/rand"
|
||
"net/url"
|
||
"reflect"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
func JsonStringIgonErr(data interface{}) string {
|
||
return string(JsonByteIgonErr(data))
|
||
}
|
||
|
||
func JsonByteIgonErr(data interface{}) []byte {
|
||
dataByte, _ := json.Marshal(data)
|
||
return dataByte
|
||
}
|
||
|
||
// IsChannelClosed 检查给定的 channel 是否已经关闭
|
||
// 参数 ch: 要检查的 channel,类型为 chan entitys.ResponseData
|
||
// 返回值: bool 类型,true 表示 channel 已关闭,false 表示未关闭
|
||
func IsChannelClosed(ch chan entitys.ResponseData) bool {
|
||
select {
|
||
case _, ok := <-ch: // 尝试从 channel 中读取数据
|
||
return !ok // 如果 ok=false,说明 channel 已关闭
|
||
default: // 如果 channel 暂时无数据可读(但不一定关闭)
|
||
return false // channel 未关闭(但可能有数据未读取)
|
||
}
|
||
}
|
||
|
||
// ValidateImageURL 验证图片 URL 是否有效
|
||
func ValidateImageURL(rawURL string) error {
|
||
// 1. 基础格式验证
|
||
parsed, err := url.Parse(rawURL)
|
||
if err != nil {
|
||
return fmt.Errorf("未知的图片格式: %v", err)
|
||
}
|
||
|
||
// 2. 检查协议是否为 http/https
|
||
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
||
return errors.New("必须是http/https结构")
|
||
}
|
||
|
||
// 3. 检查是否有空的主机名
|
||
if parsed.Host == "" {
|
||
return errors.New("未知的url地址")
|
||
}
|
||
|
||
// 4. 检查路径是否为空
|
||
if strings.TrimSpace(parsed.Path) == "" {
|
||
return errors.New("url为空")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// hexEncode 将 src 的二进制数据编码为十六进制字符串,写入 dst,返回写入长度
|
||
func HexEncode(src, dst []byte) int {
|
||
const hextable = "0123456789abcdef"
|
||
for i := 0; i < len(src); i++ {
|
||
dst[i*2] = hextable[src[i]>>4]
|
||
dst[i*2+1] = hextable[src[i]&0xf]
|
||
}
|
||
return len(src) * 2
|
||
}
|
||
|
||
// Ter 三目运算 Ter(true, 1, 2)
|
||
func Ter[T any](cond bool, a, b T) T {
|
||
if cond {
|
||
return a
|
||
}
|
||
return b
|
||
}
|
||
|
||
// StringToSlice [num,num]转slice
|
||
func StringToSlice(s string) ([]int, error) {
|
||
// 1. 去掉两端的方括号
|
||
trimmed := strings.Trim(s, "[]")
|
||
|
||
// 2. 按逗号分割
|
||
parts := strings.Split(trimmed, ",")
|
||
|
||
// 3. 转换为 []int
|
||
result := make([]int, 0, len(parts))
|
||
for _, part := range parts {
|
||
num, err := strconv.Atoi(strings.TrimSpace(part))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
result = append(result, num)
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// Difference 差集
|
||
func Difference[T comparable](a, b []T) []T {
|
||
// 创建 b 的映射(T 必须是可比较的类型)
|
||
bMap := make(map[T]struct{}, len(b))
|
||
for _, item := range b {
|
||
bMap[item] = struct{}{}
|
||
}
|
||
|
||
var diff []T // 修正为 []T 而非 []int
|
||
for _, item := range a {
|
||
if _, found := bMap[item]; !found {
|
||
diff = append(diff, item)
|
||
}
|
||
}
|
||
return diff
|
||
}
|
||
|
||
// SliceStringToInt []string=>[]int
|
||
func SliceStringToInt(strSlice []string) []int {
|
||
numSlice := make([]int, len(strSlice))
|
||
for i, str := range strSlice {
|
||
num, err := strconv.Atoi(str)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
numSlice[i] = num
|
||
}
|
||
return numSlice
|
||
}
|
||
|
||
// SliceIntToString []int=>[]string
|
||
func SliceIntToString(slice []int) []string {
|
||
strSlice := make([]string, len(slice)) // len=cap=len(slice)
|
||
for i, num := range slice {
|
||
strSlice[i] = strconv.Itoa(num) // 直接赋值,无 append
|
||
}
|
||
return strSlice
|
||
}
|
||
|
||
// SafeReplace 替换字符串中的 %s,并自动转义特殊字符(如 ")
|
||
/**
|
||
* SafeReplace 函数用于安全地替换模板字符串中的占位符
|
||
* @param template 原始模板字符串
|
||
* @param replaceTag 要被替换的占位符(如 "%s")
|
||
* @param replacements 可变参数,用于替换占位符的字符串
|
||
* @return 返回替换后的字符串和可能的错误
|
||
*/
|
||
func SafeReplace(template string, replaceTag string, replacements ...string) (string, error) {
|
||
// 如果没有提供替换参数,直接返回原始模板
|
||
if len(replacements) == 0 {
|
||
return template, nil
|
||
}
|
||
|
||
// 检查模板中 %s 的数量是否匹配替换参数
|
||
expectedReplacements := strings.Count(template, replaceTag)
|
||
if expectedReplacements != len(replacements) {
|
||
return "", fmt.Errorf("模板需要 %d 个替换参数,但提供了 %d 个", expectedReplacements, len(replacements))
|
||
}
|
||
|
||
// 逐个替换 %s,并转义特殊字符
|
||
for _, rep := range replacements {
|
||
// 转义特殊字符(如 ", \, \n 等)
|
||
escaped := strconv.Quote(rep)
|
||
// 去掉 strconv.Quote 添加的额外引号
|
||
escaped = escaped[1 : len(escaped)-1]
|
||
template = strings.Replace(template, replaceTag, escaped, 1)
|
||
}
|
||
|
||
return template, nil
|
||
}
|
||
|
||
// 配置选项
|
||
type URLValuesOptions struct {
|
||
ArrayFormat string // 数组格式:"brackets" -> name[], "indices" -> name[0], "repeat" -> name=value1&name=value2
|
||
TimeFormat string // 时间格式
|
||
}
|
||
|
||
var defaultOptions = URLValuesOptions{
|
||
ArrayFormat: "brackets", // 默认使用括号格式
|
||
TimeFormat: time.DateTime,
|
||
}
|
||
|
||
// StructToURLValues 将结构体转换为 url.Values
|
||
func StructToURLValues(input interface{}, options ...URLValuesOptions) (url.Values, error) {
|
||
opts := defaultOptions
|
||
if len(options) > 0 {
|
||
opts = options[0]
|
||
}
|
||
|
||
values := url.Values{}
|
||
|
||
if input == nil {
|
||
return values, nil
|
||
}
|
||
|
||
v := reflect.ValueOf(input)
|
||
t := reflect.TypeOf(input)
|
||
|
||
// 如果是指针,获取其指向的值
|
||
if v.Kind() == reflect.Ptr {
|
||
if v.IsNil() {
|
||
return values, nil
|
||
}
|
||
v = v.Elem()
|
||
t = t.Elem()
|
||
}
|
||
|
||
// 确保是结构体类型
|
||
if v.Kind() != reflect.Struct {
|
||
return nil, fmt.Errorf("input must be a struct or pointer to struct")
|
||
}
|
||
|
||
// 遍历结构体字段
|
||
for i := 0; i < v.NumField(); i++ {
|
||
field := t.Field(i)
|
||
fieldValue := v.Field(i)
|
||
|
||
// 跳过非导出字段
|
||
if !field.IsExported() {
|
||
continue
|
||
}
|
||
|
||
// 解析 JSON 标签(也可以支持 form 标签)
|
||
tag := field.Tag.Get("json")
|
||
fieldName, omitempty := parseJSONTag(tag)
|
||
if fieldName == "-" {
|
||
continue // 忽略该字段
|
||
}
|
||
if fieldName == "" {
|
||
fieldName = field.Name
|
||
}
|
||
|
||
// 处理指针类型
|
||
if fieldValue.Kind() == reflect.Ptr {
|
||
if fieldValue.IsNil() {
|
||
if omitempty {
|
||
continue
|
||
}
|
||
// 可以为 nil 指针添加空值
|
||
values.Set(fieldName, "")
|
||
continue
|
||
}
|
||
fieldValue = fieldValue.Elem()
|
||
}
|
||
|
||
// 处理切片/数组
|
||
if fieldValue.Kind() == reflect.Slice || fieldValue.Kind() == reflect.Array {
|
||
if fieldValue.Len() == 0 && omitempty {
|
||
continue
|
||
}
|
||
|
||
// 将切片转换为 URL 参数
|
||
err := addSliceToValues(values, fieldName, fieldValue, opts)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
continue
|
||
}
|
||
|
||
// 检查是否需要忽略空值
|
||
if omitempty && isEmptyValue(fieldValue) {
|
||
continue
|
||
}
|
||
|
||
// 转换单个值
|
||
str, err := valueToString(fieldValue, opts)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
values.Set(fieldName, str)
|
||
}
|
||
|
||
return values, nil
|
||
}
|
||
|
||
// 解析 JSON 标签
|
||
func parseJSONTag(tag string) (fieldName string, omitempty bool) {
|
||
if tag == "" {
|
||
return "", false
|
||
}
|
||
|
||
parts := strings.Split(tag, ",")
|
||
fieldName = parts[0]
|
||
|
||
if len(parts) > 1 {
|
||
for _, part := range parts[1:] {
|
||
if part == "omitempty" {
|
||
omitempty = true
|
||
}
|
||
}
|
||
}
|
||
|
||
return fieldName, omitempty
|
||
}
|
||
|
||
// 添加切片到 values
|
||
func addSliceToValues(values url.Values, fieldName string, slice reflect.Value, opts URLValuesOptions) error {
|
||
length := slice.Len()
|
||
if length == 0 {
|
||
return nil
|
||
}
|
||
|
||
switch opts.ArrayFormat {
|
||
case "brackets":
|
||
// 格式:field[]=value1&field[]=value2
|
||
for i := 0; i < length; i++ {
|
||
item := slice.Index(i)
|
||
str, err := valueToString(item, opts)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
values.Add(fieldName, str)
|
||
}
|
||
|
||
case "indices":
|
||
// 格式:field[0]=value1&field[1]=value2
|
||
for i := 0; i < length; i++ {
|
||
item := slice.Index(i)
|
||
str, err := valueToString(item, opts)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
values.Set(fmt.Sprintf("%s[%d]", fieldName, i), str)
|
||
}
|
||
|
||
case "repeat":
|
||
// 格式:field=value1&field=value2
|
||
for i := 0; i < length; i++ {
|
||
item := slice.Index(i)
|
||
str, err := valueToString(item, opts)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
values.Add(fieldName, str)
|
||
}
|
||
|
||
default:
|
||
// 默认使用 brackets 格式
|
||
for i := 0; i < length; i++ {
|
||
item := slice.Index(i)
|
||
str, err := valueToString(item, opts)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
values.Add(fieldName+"[]", str)
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// 将值转换为字符串
|
||
func valueToString(v reflect.Value, opts URLValuesOptions) (string, error) {
|
||
if !v.IsValid() {
|
||
return "", nil
|
||
}
|
||
|
||
// 处理不同类型
|
||
switch v.Kind() {
|
||
case reflect.String:
|
||
return v.String(), nil
|
||
|
||
case reflect.Bool:
|
||
return strconv.FormatBool(v.Bool()), nil
|
||
|
||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||
return strconv.FormatInt(v.Int(), 10), nil
|
||
|
||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||
return strconv.FormatUint(v.Uint(), 10), nil
|
||
|
||
case reflect.Float32, reflect.Float64:
|
||
return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil
|
||
|
||
case reflect.Struct:
|
||
// 特殊处理 time.Time
|
||
if t, ok := v.Interface().(time.Time); ok {
|
||
return t.Format(opts.TimeFormat), nil
|
||
}
|
||
// 其他结构体递归处理
|
||
// 这里可以扩展为递归处理嵌套结构体
|
||
|
||
default:
|
||
// 默认使用 fmt 的字符串表示
|
||
return fmt.Sprintf("%v", v.Interface()), nil
|
||
}
|
||
|
||
return fmt.Sprintf("%v", v.Interface()), nil
|
||
}
|
||
|
||
// 检查值是否为空
|
||
func isEmptyValue(v reflect.Value) bool {
|
||
switch v.Kind() {
|
||
case reflect.String:
|
||
return v.String() == ""
|
||
case reflect.Bool:
|
||
return false
|
||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||
return v.Int() == 0
|
||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||
return v.Uint() == 0
|
||
case reflect.Float32, reflect.Float64:
|
||
return v.Float() == 0
|
||
case reflect.Slice, reflect.Array, reflect.Map:
|
||
return v.Len() == 0
|
||
case reflect.Ptr, reflect.Interface:
|
||
return v.IsNil()
|
||
case reflect.Struct:
|
||
if t, ok := v.Interface().(time.Time); ok {
|
||
return t.IsZero()
|
||
}
|
||
return false
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 方便函数:直接生成查询字符串
|
||
func StructToQueryString(input interface{}, options ...URLValuesOptions) (string, error) {
|
||
values, err := StructToURLValues(input, options...)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
return values.Encode(), nil
|
||
}
|
||
|
||
const (
|
||
letterBytes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" // 62个字符
|
||
)
|
||
|
||
// RandomString 生成随机字符串,包含 0-9, a-z, A-Z
|
||
// length: 要生成的字符串长度
|
||
func RandomString(length int) string {
|
||
// 使用 crypto/rand 替代 math/rand(更安全,适用于密码学场景)
|
||
// 但如果不需要高安全性,math/rand 更快
|
||
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||
|
||
result := make([]byte, length)
|
||
for i := range result {
|
||
result[i] = letterBytes[rng.Intn(len(letterBytes))]
|
||
}
|
||
return string(result)
|
||
}
|