add utils to pkg
CI / changes (push) Successful in 1s
CI / docker-bff (push) Successful in 1s
CI / docker-product (push) Failing after 1s

This commit is contained in:
2026-08-07 09:55:22 +08:00
parent 95d4c7bdc2
commit 10424d23d8
21 changed files with 410 additions and 1081 deletions
+676
View File
@@ -0,0 +1,676 @@
package utils
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"math"
"math/rand"
"reflect"
"regexp"
"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 // 保证全局只初始化一次,并发安全
)
func IsMobile(mobile string) bool {
// 手机号验证正则表达式(支持国内手机号)
phoneRegex := regexp.MustCompile(`^1[3-9]\d{9}$`)
return phoneRegex.MatchString(mobile)
}
// 初始化随机数生成器(内部方法,外部无需调用)
func initRandGenerator() {
// 创建随机数源
source := rand.NewSource(time.Now().UnixNano())
// 基于源创建生成器
randGen = rand.New(source)
}
// GetRandNum 工程化生成四位随机数(0000-9999)
func GetRandNum(number int) int {
// 首次调用时初始化,后续调用直接复用,无重复初始化开销
once.Do(initRandGenerator)
// 生成0-9999随机数,格式化补零为4位
return randGen.Intn(number)
}
func UniqueId(typeStr string, id int) string {
// 格式化补零:%04d 表示数字占4位,不足左侧补0
return fmt.Sprintf("%s%010d%s%s", typeStr, id, time.Now().Format("20060102150405"), fmt.Sprintf("%04d", GetRandNum(10000)))
}
// GenerateNickname 生成风格1:形容词+名词+随机数字(例:帅气小猫668)
func GenerateNickname() string {
var (
// 形容词(修饰昵称)
adjectives = []string{
"帅气", "可爱", "温柔", "霸气", "呆萌", "高冷", "暖心", "潇洒",
"元气", "佛系", "硬核", "软萌", "酷飒", "慵懒", "清爽",
}
// 名词(核心昵称)
nouns = []string{
"小猫", "小狗", "星辰", "大海", "清风", "明月", "云朵", "小鹿",
"枫叶", "晚风", "暖阳", "星辰", "鲸鱼", "兔子", "小熊",
}
)
adj := adjectives[rand.Intn(len(adjectives))]
noun := nouns[rand.Intn(len(nouns))]
var numStr strings.Builder
for i := 0; i < 3; i++ {
numStr.WriteByte(byte('0' + rand.Intn(10)))
}
num := numStr.String() // 2位随机数字
return fmt.Sprintf("%s%s%s", adj, noun, num)
}
// 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 := json.Marshal(data) // 将 map 转换为 JSON 字节切片
if err != nil {
Logger.Error("Error marshalling map to JSON:", err)
return err
}
err = json.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 := json.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 := json.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
}