637 lines
15 KiB
Go
637 lines
15 KiB
Go
package utils
|
||
|
||
import (
|
||
"crypto/md5"
|
||
"encoding/hex"
|
||
"errors"
|
||
"fmt"
|
||
"math"
|
||
"math/rand"
|
||
"reflect"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
jsoniter "github.com/json-iterator/go"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/xuri/excelize/v2"
|
||
)
|
||
|
||
var (
|
||
randGen *rand.Rand
|
||
once sync.Once // 保证全局只初始化一次,并发安全
|
||
)
|
||
|
||
// MD5Encrypt 封装MD5加密函数:输入字符串,返回32位小写MD5哈希值
|
||
func MD5Encrypt(str string) string {
|
||
// 1. 创建MD5哈希对象
|
||
hash := md5.New()
|
||
// 2. 写入需要加密的字节流(MD5处理的是字节,需将字符串转[]byte)
|
||
hash.Write([]byte(str))
|
||
// 3. 计算哈希值(返回[]byte类型的二进制结果)
|
||
sum := hash.Sum(nil)
|
||
// 4. 将二进制结果转为16进制字符串(32位小写)
|
||
return hex.EncodeToString(sum)
|
||
}
|
||
|
||
func GetEncrypt(key string, nums ...int8) string {
|
||
original := GetConfigString(key)
|
||
str := original
|
||
//TODO 后台切换到新版时开启
|
||
//for _, num := range nums {
|
||
// str = str[num:] + str[:num]
|
||
//}
|
||
return str
|
||
}
|
||
|
||
func InArray[K int | int8 | uint8 | int64 | string, V any](val K, arr map[K]V) bool {
|
||
_, ok := arr[val]
|
||
return ok
|
||
}
|
||
|
||
// ConvertTo 通用类型转换方法
|
||
// 入参:val 任意类型的值
|
||
// 出参:T 指定类型的结果,error 转换失败原因(成功时为nil)
|
||
// 支持类型:int/int64/uint/string/float64/bool 所有基础类型双向转换
|
||
func ConvertTo[T int | int64 | uint | string | float64 | bool](val interface{}) (T, error) {
|
||
// 定义返回值的零值,用于转换失败时返回
|
||
var zero T
|
||
|
||
// 入参为nil,直接返回错误
|
||
if val == nil {
|
||
return zero, errors.New("convert failed: val is nil")
|
||
}
|
||
|
||
// 核心:类型断言+匹配,实现任意类型转指定类型T
|
||
switch t := val.(type) {
|
||
// 入参类型与返回类型一致,直接返回
|
||
case T:
|
||
return t, nil
|
||
|
||
// 入参为int,转换为目标类型T
|
||
case int:
|
||
return convertIntTo[T](t)
|
||
// 入参为int64,转换为目标类型T
|
||
case int64:
|
||
return convertInt64To[T](t)
|
||
// 入参为string,转换为目标类型T
|
||
case string:
|
||
return convertStringTo[T](t)
|
||
// 入参为float64,转换为目标类型T
|
||
case float64:
|
||
return convertFloat64To[T](t)
|
||
// 入参为bool,转换为目标类型T
|
||
case bool:
|
||
return convertBoolTo[T](t)
|
||
// 入参为uint,转换为目标类型T
|
||
case uint:
|
||
return convertUintTo[T](t)
|
||
|
||
// 不支持的入参类型
|
||
default:
|
||
return zero, fmt.Errorf("convert failed: unsupport input type %T, target type %T", val, zero)
|
||
}
|
||
}
|
||
|
||
// ------------------------------ 基础类型转换子方法 ------------------------------
|
||
// convertIntTo int转指定类型T
|
||
func convertIntTo[T int | int64 | uint | string | float64 | bool](val int) (T, error) {
|
||
var zero T
|
||
switch any(zero).(type) {
|
||
case int:
|
||
return any(val).(T), nil
|
||
case int64:
|
||
return any(int64(val)).(T), nil
|
||
case uint:
|
||
return any(uint(val)).(T), nil
|
||
case string:
|
||
return any(strconv.Itoa(val)).(T), nil
|
||
case float64:
|
||
return any(float64(val)).(T), nil
|
||
case bool:
|
||
return any(val != 0).(T), nil
|
||
default:
|
||
return zero, fmt.Errorf("unsupport convert int to %T", zero)
|
||
}
|
||
}
|
||
|
||
// convertInt64To int64转指定类型T
|
||
func convertInt64To[T int | int64 | uint | string | float64 | bool](val int64) (T, error) {
|
||
var zero T
|
||
switch any(zero).(type) {
|
||
case int:
|
||
return any(int(val)).(T), nil
|
||
case int64:
|
||
return any(val).(T), nil
|
||
case uint:
|
||
return any(uint(val)).(T), nil
|
||
case string:
|
||
return any(strconv.FormatInt(val, 10)).(T), nil
|
||
case float64:
|
||
return any(float64(val)).(T), nil
|
||
case bool:
|
||
return any(val != 0).(T), nil
|
||
default:
|
||
return zero, fmt.Errorf("unsupport convert int64 to %T", zero)
|
||
}
|
||
}
|
||
|
||
// convertUintTo uint转指定类型T
|
||
func convertUintTo[T int | int64 | uint | string | float64 | bool](val uint) (T, error) {
|
||
var zero T
|
||
switch any(zero).(type) {
|
||
case int:
|
||
return any(int(val)).(T), nil
|
||
case int64:
|
||
return any(int64(val)).(T), nil
|
||
case uint:
|
||
return any(val).(T), nil
|
||
case string:
|
||
return any(strconv.FormatUint(uint64(val), 10)).(T), nil
|
||
case float64:
|
||
return any(float64(val)).(T), nil
|
||
case bool:
|
||
return any(val != 0).(T), nil
|
||
default:
|
||
return zero, fmt.Errorf("unsupport convert uint to %T", zero)
|
||
}
|
||
}
|
||
|
||
// convertFloat64To float64转指定类型T(注意:转整型会截断小数)
|
||
func convertFloat64To[T int | int64 | uint | string | float64 | bool](val float64) (T, error) {
|
||
var zero T
|
||
switch any(zero).(type) {
|
||
case int:
|
||
return any(int(val)).(T), nil
|
||
case int64:
|
||
return any(int64(val)).(T), nil
|
||
case uint:
|
||
return any(uint(val)).(T), nil
|
||
case string:
|
||
return any(strconv.FormatFloat(val, 'f', -1, 64)).(T), nil
|
||
case float64:
|
||
return any(val).(T), nil
|
||
case bool:
|
||
return any(val != 0).(T), nil
|
||
default:
|
||
return zero, fmt.Errorf("unsupport convert float64 to %T", zero)
|
||
}
|
||
}
|
||
|
||
// convertBoolTo bool转指定类型T(true→1/"true",false→0/"false")
|
||
func convertBoolTo[T int | int64 | uint | string | float64 | bool](val bool) (T, error) {
|
||
var zero T
|
||
var intVal int = 0
|
||
var strVal string = "false"
|
||
if val {
|
||
intVal = 1
|
||
strVal = "true"
|
||
}
|
||
switch any(zero).(type) {
|
||
case int:
|
||
return any(intVal).(T), nil
|
||
case int64:
|
||
return any(int64(intVal)).(T), nil
|
||
case uint:
|
||
return any(uint(intVal)).(T), nil
|
||
case string:
|
||
return any(strVal).(T), nil
|
||
case float64:
|
||
return any(float64(intVal)).(T), nil
|
||
case bool:
|
||
return any(val).(T), nil
|
||
default:
|
||
return zero, fmt.Errorf("unsupport convert bool to %T", zero)
|
||
}
|
||
}
|
||
|
||
// convertStringTo string转指定类型T(需保证字符串格式合法,否则转换失败)
|
||
func convertStringTo[T int | int64 | uint | string | float64 | bool](val string) (T, error) {
|
||
var zero T
|
||
switch any(zero).(type) {
|
||
case int:
|
||
num, err := strconv.Atoi(val)
|
||
return any(num).(T), err
|
||
case int64:
|
||
num, err := strconv.ParseInt(val, 10, 64)
|
||
return any(num).(T), err
|
||
case uint:
|
||
num, err := strconv.ParseUint(val, 10, 64)
|
||
return any(uint(num)).(T), err
|
||
case string:
|
||
return any(val).(T), nil
|
||
case float64:
|
||
num, err := strconv.ParseFloat(val, 64)
|
||
return any(num).(T), err
|
||
case bool:
|
||
b, err := strconv.ParseBool(val)
|
||
return any(b).(T), err
|
||
default:
|
||
return zero, fmt.Errorf("unsupport convert string to %T", zero)
|
||
}
|
||
}
|
||
|
||
func StringToFloat64(val string) float64 {
|
||
n, _ := convertStringTo[float64](strings.TrimSpace(val))
|
||
return n
|
||
}
|
||
|
||
func RoundMoney(amount float64) float64 {
|
||
return math.Round(amount*100) / 100
|
||
}
|
||
|
||
func Decrypt(str string) (string, Error) {
|
||
str, err := Crypto{}.AESDecryptECB(str)
|
||
if err != nil {
|
||
Logger.Error("decrypt str error", str, err)
|
||
return str, err
|
||
}
|
||
|
||
// 去掉乱码
|
||
str = strings.TrimRight(str, "\x00\x01\x02\x03\x04\x05\x06\x07\x08")
|
||
|
||
return str, nil
|
||
}
|
||
|
||
func DecryptMobile(mobile string) string {
|
||
|
||
mobile, err := Decrypt(mobile)
|
||
if err != nil {
|
||
Logger.Error("decrypt mobile error", mobile, err)
|
||
return mobile
|
||
}
|
||
|
||
if len(mobile) < 11 {
|
||
return mobile
|
||
}
|
||
|
||
return mobile[:3] + "****" + mobile[7:11]
|
||
}
|
||
|
||
// 接收 [startDate, endDate](格式 "YYYY-MM-DD"),
|
||
// 返回 [2006-01-02 00:00:00, 2006-01-02 23:59:59]
|
||
func DateRangeWithTime(dates []string) [2]string {
|
||
var result [2]string
|
||
if len(dates) != 2 {
|
||
return result
|
||
}
|
||
|
||
layout := "2006-01-02"
|
||
startDate, err1 := time.Parse(layout, dates[0])
|
||
endDate, err2 := time.Parse(layout, dates[1])
|
||
if err1 != nil || err2 != nil {
|
||
// 出错就原样返回前端输入
|
||
result[0] = dates[0]
|
||
result[1] = dates[1]
|
||
return result
|
||
}
|
||
|
||
result[0] = startDate.Format("2006-01-02") + " 00:00:00"
|
||
result[1] = endDate.Format("2006-01-02") + " 23:59:59"
|
||
|
||
return result
|
||
}
|
||
|
||
// CalcAge 根据出生日期计算年龄
|
||
func CalcAgeStr(birthday string) string {
|
||
|
||
if birthday == "" {
|
||
return ""
|
||
}
|
||
|
||
layout := "2006-01-02"
|
||
birth, err := time.Parse(layout, birthday)
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
|
||
now := time.Now()
|
||
age := now.Year() - birth.Year()
|
||
|
||
if now.Month() < birth.Month() ||
|
||
(now.Month() == birth.Month() && now.Day() < birth.Day()) {
|
||
age--
|
||
}
|
||
|
||
if age < 0 {
|
||
return ""
|
||
}
|
||
|
||
return strconv.Itoa(age)
|
||
}
|
||
|
||
func MaskData(data string, first, end uint8) string {
|
||
// 2. 截取前3位 + **** + 后4位
|
||
// Go字符串按字节截取,数字字符单字节,直接切片即可
|
||
return data[:first] + "****" + data[end:]
|
||
}
|
||
|
||
func RandString(num uint8) string {
|
||
chars := "23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ"
|
||
rand.Seed(time.Now().UnixNano())
|
||
|
||
res := make([]byte, num)
|
||
for i := range res {
|
||
res[i] = chars[rand.Intn(len(chars))]
|
||
}
|
||
return string(res)
|
||
}
|
||
|
||
// ExcelColumn 定义导出的列:表头 + 对应结构体字段
|
||
type ExcelColumn struct {
|
||
Header string // 表头显示名称
|
||
Field string // 结构体字段名(必须大写)
|
||
Width float64 //宽
|
||
IsImage bool //是否是图片
|
||
}
|
||
|
||
func ExcelExport(c *gin.Context, fileName string, columns []ExcelColumn, dataList interface{}) error {
|
||
f := excelize.NewFile()
|
||
sheet := "Sheet1"
|
||
f.SetSheetName("Sheet1", sheet)
|
||
|
||
// 自动换行样式
|
||
wrapStyle, _ := f.NewStyle(&excelize.Style{
|
||
Alignment: &excelize.Alignment{
|
||
WrapText: true,
|
||
Vertical: "top",
|
||
},
|
||
})
|
||
|
||
// 写入表头
|
||
for i, col := range columns {
|
||
colName, _ := excelize.ColumnNumberToName(i + 1)
|
||
_ = f.SetCellValue(sheet, colName+"1", col.Header)
|
||
}
|
||
|
||
items := reflect.ValueOf(dataList)
|
||
if items.Kind() != reflect.Slice {
|
||
return f.Write(c.Writer)
|
||
}
|
||
|
||
// 遍历行
|
||
for rowIdx := 0; rowIdx < items.Len(); rowIdx++ {
|
||
rowNum := rowIdx + 2
|
||
item := items.Index(rowIdx).Interface()
|
||
val := reflect.ValueOf(item)
|
||
|
||
if val.Kind() == reflect.Ptr {
|
||
val = val.Elem()
|
||
}
|
||
|
||
// 遍历列
|
||
for colIdx, col := range columns {
|
||
fieldVal := getFieldValue(val, col.Field)
|
||
if !fieldVal.IsValid() {
|
||
continue
|
||
}
|
||
|
||
cellName, _ := excelize.ColumnNumberToName(colIdx + 1)
|
||
cellPos := cellName + strconv.Itoa(rowNum)
|
||
|
||
// ======================================
|
||
// 图片URL → 改为【点击查看图片】链接
|
||
// ======================================
|
||
if col.IsImage {
|
||
imgURL := toStr(fieldVal.Interface())
|
||
if imgURL != "" {
|
||
// 设置超链接(显示文字:点击查看图片,真正链接:imgURL)
|
||
_ = f.SetCellHyperLink(sheet, cellPos, imgURL, "External")
|
||
// 单元格显示文字
|
||
_ = f.SetCellValue(sheet, cellPos, "点击查看")
|
||
}
|
||
continue
|
||
}
|
||
|
||
// ======================================
|
||
// 文本/数组 自动换行
|
||
// ======================================
|
||
var cellValue interface{}
|
||
kind := fieldVal.Kind()
|
||
if kind == reflect.Slice || kind == reflect.Array {
|
||
var lines []string
|
||
for i := 0; i < fieldVal.Len(); i++ {
|
||
lines = append(lines, toStr(fieldVal.Index(i).Interface()))
|
||
}
|
||
cellValue = strings.Join(lines, "\n")
|
||
} else {
|
||
cellValue = fieldVal.Interface()
|
||
}
|
||
|
||
_ = f.SetCellValue(sheet, cellPos, cellValue)
|
||
_ = f.SetCellStyle(sheet, cellPos, cellPos, wrapStyle)
|
||
}
|
||
|
||
_ = f.SetRowHeight(sheet, rowNum, 30)
|
||
}
|
||
|
||
// 设置列宽
|
||
for colIdx, col := range columns {
|
||
if col.Width <= 0 {
|
||
continue
|
||
}
|
||
colName, _ := excelize.ColumnNumberToName(colIdx + 1)
|
||
_ = f.SetColWidth(sheet, colName, colName, col.Width)
|
||
}
|
||
|
||
// 下载
|
||
c.Header("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||
c.Header("Content-Disposition", "attachment; filename="+fileName+".xlsx")
|
||
return f.Write(c.Writer)
|
||
}
|
||
|
||
// 嵌套字段取值
|
||
func getFieldValue(val reflect.Value, field string) reflect.Value {
|
||
fields := strings.Split(field, ".")
|
||
current := val
|
||
for _, f := range fields {
|
||
if current.Kind() == reflect.Ptr {
|
||
current = current.Elem()
|
||
}
|
||
if !current.IsValid() {
|
||
return reflect.Value{}
|
||
}
|
||
current = current.FieldByName(f)
|
||
}
|
||
return current
|
||
}
|
||
|
||
// 通用转字符串
|
||
func toStr(v interface{}) string {
|
||
if v == nil {
|
||
return ""
|
||
}
|
||
return strings.TrimSpace(fmt.Sprintf("%v", v))
|
||
}
|
||
|
||
// Unique 去重函数:传入切片,返回去重后的新切片
|
||
func Unique(s []string) []string {
|
||
tempMap := make(map[string]bool) // 用map记录是否存在
|
||
res := []string{}
|
||
|
||
for _, item := range s {
|
||
if !tempMap[item] { // 不存在才添加
|
||
tempMap[item] = true
|
||
res = append(res, item)
|
||
}
|
||
}
|
||
return res
|
||
}
|
||
|
||
// MapToStruct 将 map 转换为 struct(支持 map[string]interface{} 转任意 struct)
|
||
// 参数:
|
||
//
|
||
// data: 源 map(key 为字符串,value 为任意类型)
|
||
// target: 目标 struct 指针(必须传指针,否则无法赋值
|
||
func MapToStruct(data map[string]interface{}, target interface{}) error {
|
||
jsonData, err := jsoniter.Marshal(data) // 将 map 转换为 JSON 字节切片
|
||
if err != nil {
|
||
Logger.Error("Error marshalling map to JSON:", err)
|
||
return err
|
||
}
|
||
err = jsoniter.Unmarshal(jsonData, &target) // 将 JSON 字节切片解析到结构体中
|
||
if err != nil {
|
||
Logger.Error("Error unmarshalling JSON:", err)
|
||
return err
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func JsonStringToStruct(jsonStr string, v interface{}) bool {
|
||
// 1. 校验入参:确保v是指针类型(否则json.Unmarshal无法工作)
|
||
if v == nil {
|
||
Logger.Error("传入的结构体指针不能为nil")
|
||
return false
|
||
}
|
||
|
||
// 2. 将JSON字符串转为字节数组,执行解析
|
||
err := jsoniter.Unmarshal([]byte(jsonStr), v)
|
||
if err != nil {
|
||
Logger.Error("JSON解析失败: ", err, jsonStr)
|
||
return false
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
func TimeToStartAndEnd(timeString string, day int) (string, string) {
|
||
if len(timeString) > 0 {
|
||
tmpStr := strings.Split(timeString, " - ")
|
||
if len(tmpStr) > 1 {
|
||
startTime := TimeStart(day, tmpStr[0])
|
||
endTime := TimeEnd(day, tmpStr[1])
|
||
if len(startTime) > 0 && len(endTime) > 0 {
|
||
return startTime, endTime
|
||
}
|
||
}
|
||
}
|
||
|
||
return "", ""
|
||
}
|
||
|
||
func StructToJson(data interface{}) string {
|
||
bytes, err := jsoniter.Marshal(data)
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
return string(bytes)
|
||
}
|
||
|
||
func GetStringByMap(m map[string]interface{}, key string) string {
|
||
if v, ok := m[key].(string); ok {
|
||
return v
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func BuildImageURL(data interface{}) interface{} {
|
||
switch v := data.(type) {
|
||
|
||
case map[string]interface{}:
|
||
for key, val := range v {
|
||
v[key] = BuildImageURL(val)
|
||
}
|
||
return v
|
||
|
||
case []interface{}:
|
||
for i, val := range v {
|
||
v[i] = BuildImageURL(val)
|
||
}
|
||
return v
|
||
|
||
case string:
|
||
if v == "" {
|
||
return v
|
||
}
|
||
base := GetConfigString("upload.image_base_url")
|
||
return fmt.Sprintf("%s/%s", strings.TrimRight(base, "/"), strings.TrimLeft(v, "/"))
|
||
|
||
default:
|
||
return v
|
||
}
|
||
}
|
||
|
||
func ParseIntArray(value string) ([]int, error) {
|
||
|
||
var result []int
|
||
|
||
value = strings.TrimSpace(value)
|
||
|
||
if value == "" {
|
||
return result, nil
|
||
}
|
||
|
||
err := jsoniter.Unmarshal(
|
||
[]byte(value),
|
||
&result,
|
||
)
|
||
|
||
return result, err
|
||
}
|
||
|
||
func ParseStringArray(value string) ([]string, error) {
|
||
|
||
var result []string
|
||
|
||
value = strings.TrimSpace(value)
|
||
|
||
if value == "" {
|
||
return result, nil
|
||
}
|
||
|
||
err := jsoniter.Unmarshal(
|
||
[]byte(value),
|
||
&result,
|
||
)
|
||
|
||
return result, err
|
||
}
|
||
func UniqueStr(arr []string) []string {
|
||
m := make(map[string]struct{})
|
||
res := make([]string, 0, len(arr))
|
||
for _, v := range arr {
|
||
// 修正这里:用ok接收是否存在
|
||
if _, ok := m[v]; !ok {
|
||
m[v] = struct{}{}
|
||
res = append(res, v)
|
||
}
|
||
}
|
||
return res
|
||
}
|
||
|
||
// Float 泛型约束,只允许 float32 / float64
|
||
type Float interface {
|
||
~float32 | ~float64
|
||
}
|
||
|
||
// RoundFloat 四舍五入保留n位小数,泛型版本
|
||
func RoundFloat[T Float](val T) T {
|
||
shift := math.Pow10(NumberTwo)
|
||
f := float64(val)
|
||
res := math.Round(f*shift) / shift
|
||
return T(res)
|
||
}
|