zhangyu131 hai 9 meses
achega
424df1b243
Modificáronse 21 ficheiros con 1075 adicións e 0 borrados
  1. 2 0
      .gitignore
  2. 45 0
      README.md
  3. 56 0
      go.mod
  4. 70 0
      lockx/lock.go
  5. 36 0
      utilx/compress.go
  6. 185 0
      utilx/convert.go
  7. 189 0
      utilx/copy.go
  8. 45 0
      utilx/createZzSign.go
  9. 58 0
      utilx/crypto.go
  10. 70 0
      utilx/excel.go
  11. 70 0
      utilx/excel_test.go
  12. 69 0
      utilx/ip.go
  13. 8 0
      utilx/map.go
  14. 23 0
      utilx/money.go
  15. 51 0
      utilx/msgDese.go
  16. 8 0
      utilx/number.go
  17. 25 0
      utilx/rand.go
  18. 21 0
      utilx/slice.go
  19. 3 0
      utilx/struct.go
  20. 30 0
      utilx/time.go
  21. 11 0
      utilx/utilx_test.go

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+.idea
+go.sum

+ 45 - 0
README.md

@@ -0,0 +1,45 @@
+# BackendCommon
+
+后端公共库
+
+## 私有库导入步骤
+
+#### 1.配置环境变量
+```
+go env -w GOINSECURE="git.gzquan.cn"
+go env -w GOPRIVATE="git.gzquan.cn"
+```
+
+#### 2.gitlab部署ssh公钥
+参考 [http://g.lenovo.com.cn/help/user/ssh.md](http://g.lenovo.com.cn/help/user/ssh.md)
+
+
+#### 3.如果提示还要username的话,git映射为ssh认证
+```
+git config --global --add url."git@git.gzquan.cn:".insteadOf "http://git.gzquan.cn/"
+```
+
+#### 4.导入
+```
+go get git.gzquan.cn/pkg/go-common
+```
+
+#### 5.指定包版本/commit
+
+项目迭代期间不必要每次提交都新建tag,代码commit到main分支即可,待版本迭代基本完成后新建新的tag版本。私有库指定commit版本,步骤:
+
+1.修改go.mod文件 修改后缀版本,改成tag版本号(如:v1.0.55)或commit hash (如:2fd581b1)
+
+```go
+require (
+    git.gzquan.cn/pkg/go-common 2fd581b1
+	github.com/Masterminds/squirrel v1.5.4
+	github.com/apolloconfig/agollo/v4 v4.1.1
+	...
+)
+```
+
+2.命令行执行
+```shell
+go mod tidy
+```

+ 56 - 0
go.mod

@@ -0,0 +1,56 @@
+module git.gzquan.cn/pkg/go-common
+
+go 1.18
+
+require (
+	github.com/xuri/excelize/v2 v2.8.1
+	github.com/zeromicro/go-zero v1.6.5
+)
+
+require (
+	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/cenkalti/backoff/v4 v4.2.1 // indirect
+	github.com/cespare/xxhash/v2 v2.2.0 // indirect
+	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+	github.com/fatih/color v1.16.0 // indirect
+	github.com/go-logr/logr v1.3.0 // indirect
+	github.com/go-logr/stdr v1.2.2 // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 // indirect
+	github.com/mattn/go-colorable v0.1.13 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
+	github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
+	github.com/openzipkin/zipkin-go v0.4.2 // indirect
+	github.com/pelletier/go-toml/v2 v2.2.2 // indirect
+	github.com/prometheus/client_golang v1.18.0 // indirect
+	github.com/prometheus/client_model v0.5.0 // indirect
+	github.com/prometheus/common v0.45.0 // indirect
+	github.com/prometheus/procfs v0.12.0 // indirect
+	github.com/redis/go-redis/v9 v9.4.0 // indirect
+	github.com/richardlehane/mscfb v1.0.4 // indirect
+	github.com/richardlehane/msoleps v1.0.3 // indirect
+	github.com/spaolacci/murmur3 v1.1.0 // indirect
+	github.com/xuri/efp v0.0.0-20231025114914-d1ff6096ae53 // indirect
+	github.com/xuri/nfp v0.0.0-20230919160717-d98342af3f05 // indirect
+	go.opentelemetry.io/otel v1.19.0 // indirect
+	go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect
+	go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.19.0 // indirect
+	go.opentelemetry.io/otel/exporters/zipkin v1.19.0 // indirect
+	go.opentelemetry.io/otel/metric v1.19.0 // indirect
+	go.opentelemetry.io/otel/sdk v1.19.0 // indirect
+	go.opentelemetry.io/otel/trace v1.19.0 // indirect
+	go.opentelemetry.io/proto/otlp v1.0.0 // indirect
+	go.uber.org/automaxprocs v1.5.3 // indirect
+	golang.org/x/crypto v0.23.0 // indirect
+	golang.org/x/net v0.25.0 // indirect
+	golang.org/x/sys v0.20.0 // indirect
+	golang.org/x/text v0.15.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
+	google.golang.org/grpc v1.63.2 // indirect
+	google.golang.org/protobuf v1.34.1 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
+)

+ 70 - 0
lockx/lock.go

@@ -0,0 +1,70 @@
+package lockx
+
+import (
+	"context"
+	"github.com/zeromicro/go-zero/core/stores/redis"
+	"time"
+)
+
+type DistributedLock struct {
+	client  *redis.Redis
+	key     string
+	seconds int
+}
+
+func NewDistributedLock(client *redis.Redis, key string, seconds int) *DistributedLock {
+	return &DistributedLock{
+		client:  client,
+		key:     key,
+		seconds: seconds,
+	}
+}
+
+// Lock 尝试获取分布式锁
+func (dl *DistributedLock) Lock() (bool, error) {
+	if ok, err := dl.client.SetnxEx(dl.key, "1", dl.seconds); err != nil {
+		return false, err
+	} else if ok {
+		return true, nil
+	}
+	return false, nil
+}
+
+// WaitLock 尝试获取分布式锁,获取不到等待
+func (dl *DistributedLock) WaitLock(ctx context.Context) error {
+	for {
+		select {
+		case <-ctx.Done():
+			// 如果Context被取消,则直接返回错误
+			return ctx.Err()
+		default:
+			// 尝试获取锁
+			result, err := dl.client.SetnxEx(dl.key, "1", dl.seconds)
+			if err != nil {
+				return err
+			}
+
+			if result {
+				// 获取到锁,直接返回
+				return nil
+			}
+
+			// 等待一段时间后重试
+			time.Sleep(time.Millisecond * 50)
+		}
+	}
+}
+
+// Unlock 释放分布式锁
+func (dl *DistributedLock) Unlock() error {
+	// 使用 Lua 脚本来保证原子性地删除锁(避免误删其他客户端的锁)
+	luaScript := `
+		if redis.call("get", KEYS[1]) == ARGV[1] then
+			return redis.call("del", KEYS[1])
+		else
+			return 0
+		end
+	`
+	_, err := dl.client.Eval(luaScript, []string{dl.key}, "1")
+	return err
+}

+ 36 - 0
utilx/compress.go

@@ -0,0 +1,36 @@
+package utilx
+
+import (
+	"bytes"
+	"compress/gzip"
+)
+
+//压gzip数据
+func GzipCompress(gzData string) (string, error) {
+	var b bytes.Buffer
+	gz := gzip.NewWriter(&b)
+	if _, err := gz.Write([]byte(gzData)); err != nil {
+		return "", err
+	}
+	if err := gz.Close(); err != nil {
+		return "", err
+	}
+
+	return b.String(), nil
+}
+
+// 解压gzip数据
+func GzipUnCompress(gzData string) (string, error) {
+	var b bytes.Buffer
+	gz, err := gzip.NewReader(bytes.NewBuffer([]byte(gzData)))
+	if err != nil {
+		return "", err
+	}
+	if _, err := b.ReadFrom(gz); err != nil {
+		return "", err
+	}
+	if err := gz.Close(); err != nil {
+		return "", err
+	}
+	return b.String(), nil
+}

+ 185 - 0
utilx/convert.go

@@ -0,0 +1,185 @@
+package utilx
+
+import (
+	"encoding/json"
+	"fmt"
+	"math"
+	"reflect"
+	"strconv"
+)
+
+// MustString 类型转换-返回string
+func MustString(v interface{}, defaultval ...string) string {
+	val, ok := TryString(v)
+	if ok {
+		return val
+	}
+	if len(defaultval) > 0 {
+		return defaultval[0]
+	}
+	return ""
+}
+
+// TryString 类型转换-转换为string
+func TryString(v interface{}) (string, bool) {
+	switch tv := v.(type) {
+	case string:
+		return tv, true
+	case []byte:
+		return string(tv), true
+	case int64:
+		return strconv.FormatInt(int64(tv), 10), true
+	case uint64:
+		return strconv.FormatUint(uint64(tv), 10), true
+	case int32:
+		return strconv.FormatInt(int64(tv), 10), true
+	case uint32:
+		return strconv.FormatUint(uint64(tv), 10), true
+	case int16:
+		return strconv.FormatInt(int64(tv), 10), true
+	case uint16:
+		return strconv.FormatUint(uint64(tv), 10), true
+	case int8:
+		return strconv.FormatInt(int64(tv), 10), true
+	case uint8:
+		return strconv.FormatUint(uint64(tv), 10), true
+	case float32:
+		return strconv.FormatFloat(float64(tv), 'f', -1, 64), true
+	case float64:
+		return strconv.FormatFloat(float64(tv), 'f', -1, 64), true
+	case int:
+		return strconv.Itoa(int(tv)), true
+	case json.Number:
+		return tv.String(), true
+	case bool:
+		if tv {
+			return "true", true
+		} else {
+			return "false", true
+		}
+	}
+	return "", false
+}
+
+// StructToMap 结构体转换为 map[string]interface{}
+func StructToMap(data interface{}) map[string]interface{} {
+	result := make(map[string]interface{})
+
+	if data == nil {
+		return result
+	}
+	val := reflect.ValueOf(data)
+	typ := val.Type()
+
+	for i := 0; i < val.NumField(); i++ {
+		field := typ.Field(i)
+		fieldName := field.Tag.Get("json") //优先读取json标签内容
+		if fieldName == "" {
+			fieldName = field.Name
+		}
+		fieldValue := val.Field(i).Interface()
+
+		result[fieldName] = fieldValue
+	}
+
+	return result
+}
+
+// MapToStruct map[string]interface{} 转换为结构体
+func MapToStruct(data map[string]interface{}, result interface{}) error {
+	val := reflect.ValueOf(result)
+	if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Struct {
+		return fmt.Errorf("result must be a pointer to a struct")
+	}
+
+	val = val.Elem()
+	typ := val.Type()
+
+	for i := 0; i < val.NumField(); i++ {
+		fieldName := typ.Field(i).Name
+		fieldValue, ok := data[fieldName]
+		if !ok {
+			// 如果 map 中没有结构体字段对应的键,跳过
+			continue
+		}
+
+		val.Field(i).Set(reflect.ValueOf(fieldValue))
+	}
+	return nil
+}
+
+// MustInt64 强制返回int64
+func MustInt64(v interface{}, defaultval ...int64) int64 {
+	var defaultValue int64 = 0
+	if len(defaultval) > 0 {
+		defaultValue = defaultval[0]
+	}
+	if v == nil {
+		return defaultValue
+	}
+	switch tv := v.(type) {
+	case float32:
+		if tv > float32(math.MaxInt64) {
+			return defaultValue
+		}
+		return int64(tv)
+	case float64:
+		if tv > float64(math.MaxInt64) {
+			return defaultValue
+		}
+		return int64(tv)
+	}
+	val, ok := TryInt64(v)
+	if ok {
+		return val
+	}
+	return defaultValue
+}
+
+// TryInt64 类型转换-int64
+func TryInt64(v interface{}) (int64, bool) {
+	if v == nil {
+		return -1, false
+	}
+	switch tv := v.(type) {
+	case []byte:
+		res, err := strconv.ParseInt(string(tv), 10, 0)
+		if err != nil {
+			return -1, false
+		}
+		return res, true
+	case string:
+		res, err := strconv.ParseInt(tv, 10, 0)
+		if err != nil {
+			return -1, false
+		}
+		return res, true
+	case int64:
+		return tv, true
+	case uint64:
+		if tv > uint64(math.MaxInt64) {
+			return -1, false
+		}
+		return int64(tv), true
+	case int32:
+		return int64(tv), true
+	case uint32:
+		return int64(tv), true
+	case int:
+		return int64(tv), true
+	case int16:
+		return int64(tv), true
+	case uint16:
+		return int64(tv), true
+	case int8:
+		return int64(tv), true
+	case uint8:
+		return int64(tv), true
+	case json.Number:
+		val, err := tv.Int64()
+		if err == nil {
+			return val, true
+		}
+	}
+	return -1, false
+}

+ 189 - 0
utilx/copy.go

@@ -0,0 +1,189 @@
+package utilx
+
+import (
+	"database/sql"
+	"errors"
+	"reflect"
+)
+
+// Copy 复制结构体内容
+func Copy(toValue interface{}, fromValue interface{}) (err error) {
+	var (
+		isSlice bool
+		amount  = 1
+		from    = indirect(reflect.ValueOf(fromValue))
+		to      = indirect(reflect.ValueOf(toValue))
+	)
+
+	if !to.CanAddr() {
+		return errors.New("copy to value is unaddressable")
+	}
+
+	// Return is from value is invalid
+	if !from.IsValid() {
+		return
+	}
+
+	fromType := indirectType(from.Type())
+	toType := indirectType(to.Type())
+
+	// Just set it if possible to assign
+	// And need to do copy anyway if the type is struct
+	if fromType.Kind() != reflect.Struct && from.Type().AssignableTo(to.Type()) {
+		to.Set(from)
+		return
+	}
+
+	if fromType.Kind() != reflect.Struct || toType.Kind() != reflect.Struct {
+		return
+	}
+
+	if to.Kind() == reflect.Slice {
+		isSlice = true
+		if from.Kind() == reflect.Slice {
+			amount = from.Len()
+		}
+	}
+
+	for i := 0; i < amount; i++ {
+		var dest, source reflect.Value
+
+		if isSlice {
+			// source
+			if from.Kind() == reflect.Slice {
+				source = indirect(from.Index(i))
+			} else {
+				source = indirect(from)
+			}
+			// dest
+			dest = indirect(reflect.New(toType).Elem())
+		} else {
+			source = indirect(from)
+			dest = indirect(to)
+		}
+
+		// check source
+		if source.IsValid() {
+			fromTypeFields := deepFields(fromType)
+			//fmt.Printf("%#v", fromTypeFields)
+			// Copy from field to field or method
+			for _, field := range fromTypeFields {
+				name := field.Name
+
+				if fromField := source.FieldByName(name); fromField.IsValid() {
+					// has field
+					if toField := dest.FieldByName(name); toField.IsValid() {
+						if toField.CanSet() {
+							if !set(toField, fromField) {
+								if err := Copy(toField.Addr().Interface(), fromField.Interface()); err != nil {
+									return err
+								}
+							}
+						}
+					} else {
+						// try to set to method
+						var toMethod reflect.Value
+						if dest.CanAddr() {
+							toMethod = dest.Addr().MethodByName(name)
+						} else {
+							toMethod = dest.MethodByName(name)
+						}
+
+						if toMethod.IsValid() && toMethod.Type().NumIn() == 1 && fromField.Type().AssignableTo(toMethod.Type().In(0)) {
+							toMethod.Call([]reflect.Value{fromField})
+						}
+					}
+				}
+			}
+
+			// Copy from method to field
+			for _, field := range deepFields(toType) {
+				name := field.Name
+
+				var fromMethod reflect.Value
+				if source.CanAddr() {
+					fromMethod = source.Addr().MethodByName(name)
+				} else {
+					fromMethod = source.MethodByName(name)
+				}
+
+				if fromMethod.IsValid() && fromMethod.Type().NumIn() == 0 && fromMethod.Type().NumOut() == 1 {
+					if toField := dest.FieldByName(name); toField.IsValid() && toField.CanSet() {
+						values := fromMethod.Call([]reflect.Value{})
+						if len(values) >= 1 {
+							set(toField, values[0])
+						}
+					}
+				}
+			}
+		}
+		if isSlice {
+			if dest.Addr().Type().AssignableTo(to.Type().Elem()) {
+				to.Set(reflect.Append(to, dest.Addr()))
+			} else if dest.Type().AssignableTo(to.Type().Elem()) {
+				to.Set(reflect.Append(to, dest))
+			}
+		}
+	}
+	return
+}
+
+func deepFields(reflectType reflect.Type) []reflect.StructField {
+	var fields []reflect.StructField
+
+	if reflectType = indirectType(reflectType); reflectType.Kind() == reflect.Struct {
+		for i := 0; i < reflectType.NumField(); i++ {
+			v := reflectType.Field(i)
+			if v.Anonymous {
+				fields = append(fields, deepFields(v.Type)...)
+			} else {
+				fields = append(fields, v)
+			}
+		}
+	}
+
+	return fields
+}
+
+func indirect(reflectValue reflect.Value) reflect.Value {
+	for reflectValue.Kind() == reflect.Ptr {
+		reflectValue = reflectValue.Elem()
+	}
+	return reflectValue
+}
+
+func indirectType(reflectType reflect.Type) reflect.Type {
+	for reflectType.Kind() == reflect.Ptr || reflectType.Kind() == reflect.Slice {
+		reflectType = reflectType.Elem()
+	}
+	return reflectType
+}
+
+func set(to, from reflect.Value) bool {
+	if from.IsValid() {
+		if to.Kind() == reflect.Ptr {
+			//set `to` to nil if from is nil
+			if from.Kind() == reflect.Ptr && from.IsNil() {
+				to.Set(reflect.Zero(to.Type()))
+				return true
+			} else if to.IsNil() {
+				to.Set(reflect.New(to.Type().Elem()))
+			}
+			to = to.Elem()
+		}
+
+		if from.Type().ConvertibleTo(to.Type()) {
+			to.Set(from.Convert(to.Type()))
+		} else if scanner, ok := to.Addr().Interface().(sql.Scanner); ok {
+			err := scanner.Scan(from.Interface())
+			if err != nil {
+				return false
+			}
+		} else if from.Kind() == reflect.Ptr {
+			return set(to, from.Elem())
+		} else {
+			return false
+		}
+	}
+	return true
+}

+ 45 - 0
utilx/createZzSign.go

@@ -0,0 +1,45 @@
+package utilx
+
+import (
+	"bytes"
+	"crypto/md5"
+	"encoding/hex"
+	"fmt"
+	"log"
+	"sort"
+)
+
+// 转转签名方法
+func CreateZzSign(params map[string]interface{}, secretKey string) string {
+	if len(params) == 0 {
+		log.Println("参数为空,签名: 空字符串")
+		return ""
+	}
+
+	// 将map转换为list,并按键排序
+	var keys []string
+	for k := range params {
+		keys = append(keys, k)
+	}
+	sort.Strings(keys)
+	var values []interface{}
+	for _, k := range keys {
+		values = append(values, params[k])
+	}
+
+	var buffer bytes.Buffer
+	for i := 0; i < len(keys); i++ {
+		buffer.WriteString(keys[i])
+		buffer.WriteString("=")
+		buffer.WriteString(fmt.Sprintf("%v", values[i]))
+		buffer.WriteString("&")
+	}
+	buffer.WriteString("key=")
+	buffer.WriteString(secretKey)
+	preEncryptString := buffer.String()
+	log.Println("排序后准备生成签名的字符串为:", preEncryptString)
+	md5Digest := md5.Sum([]byte(preEncryptString))
+	sign := hex.EncodeToString(md5Digest[:])
+	log.Println("preEncryptString:", preEncryptString, "==> sign:", sign)
+	return sign
+}

+ 58 - 0
utilx/crypto.go

@@ -0,0 +1,58 @@
+package utilx
+
+import (
+	"crypto/aes"
+	"crypto/md5"
+	"encoding/hex"
+)
+
+func Md5(input []byte) string {
+	hash := md5.New()
+	hash.Write(input)
+	return hex.EncodeToString(hash.Sum(nil))
+}
+
+//AesEncryptECB AES-EBC
+func AesEncryptECB(origData []byte, key []byte) (encrypted []byte) {
+	cipher, _ := aes.NewCipher(generateKey(key))
+	length := (len(origData) + aes.BlockSize) / aes.BlockSize
+	plain := make([]byte, length*aes.BlockSize)
+	copy(plain, origData)
+	pad := byte(len(plain) - len(origData))
+	for i := len(origData); i < len(plain); i++ {
+		plain[i] = pad
+	}
+	encrypted = make([]byte, len(plain))
+	// 分组分块加密
+	for bs, be := 0, cipher.BlockSize(); bs <= len(origData); bs, be = bs+cipher.BlockSize(), be+cipher.BlockSize() {
+		cipher.Encrypt(encrypted[bs:be], plain[bs:be])
+	}
+	return encrypted
+}
+
+//AesDecryptECB AES-EBC
+func AesDecryptECB(encrypted []byte, key []byte) (decrypted []byte) {
+	cipher, _ := aes.NewCipher(generateKey(key))
+	decrypted = make([]byte, len(encrypted))
+	//
+	for bs, be := 0, cipher.BlockSize(); bs < len(encrypted); bs, be = bs+cipher.BlockSize(), be+cipher.BlockSize() {
+		cipher.Decrypt(decrypted[bs:be], encrypted[bs:be])
+	}
+
+	trim := 0
+	if len(decrypted) > 0 {
+		trim = len(decrypted) - int(decrypted[len(decrypted)-1])
+	}
+
+	return decrypted[:trim]
+}
+func generateKey(key []byte) (genKey []byte) {
+	genKey = make([]byte, 16)
+	copy(genKey, key)
+	for i := 16; i < len(key); {
+		for j := 0; j < 16 && i < len(key); j, i = j+1, i+1 {
+			genKey[j] ^= key[i]
+		}
+	}
+	return genKey
+}

+ 70 - 0
utilx/excel.go

@@ -0,0 +1,70 @@
+package utilx
+
+import (
+	"fmt"
+	"github.com/xuri/excelize/v2"
+)
+
+func Xlsx(data [][]interface{}, sheetName string) (*excelize.File, error) {
+	f := excelize.NewFile()
+
+	index := 0
+	oldName := f.GetSheetName(index)
+	err := f.SetSheetName(oldName, sheetName)
+	if err != nil {
+		return nil, fmt.Errorf("SetSheetName failed: %s", err.Error())
+	}
+
+	// 遍历二维字符串数组并写入数据到Excel文件
+	for rowIdx, rowData := range data {
+		for colIdx, cellData := range rowData {
+			cell, err := excelize.CoordinatesToCellName(colIdx+1, rowIdx+1) // Excel索引从1开始
+			if err != nil {
+				return nil, fmt.Errorf("CoordinatesToCellName failed: %s", err.Error())
+			}
+			err = f.SetCellValue(sheetName, cell, cellData)
+			if err != nil {
+				return nil, fmt.Errorf("SetCellValue failed: %s", err.Error())
+			}
+		}
+	}
+
+	return f, nil
+}
+
+// XlsxV2 流式写入,节约内存 ps.生产内存限制512M
+func XlsxV2(data [][]interface{}, sheetName string) (*excelize.File, error) {
+	f := excelize.NewFile()
+	index := 0
+	oldName := f.GetSheetName(index)
+	err := f.SetSheetName(oldName, sheetName)
+
+	if err != nil {
+		return nil, fmt.Errorf("SetSheetName failed: %s", err.Error())
+	}
+
+	writer, err := f.NewStreamWriter(sheetName)
+	if err != nil {
+		return nil, fmt.Errorf("f.NewStreamWriter failed, err: %v", err)
+	}
+
+	// 遍历二维字符串数组并写入数据到Excel文件
+	for rowIdx, rowData := range data {
+		cell, err := excelize.CoordinatesToCellName(1, rowIdx+1) // Excel索引从1开始
+		if err != nil {
+			return nil, fmt.Errorf("CoordinatesToCellName failed: %s", err.Error())
+		}
+		err = writer.SetRow(cell, rowData)
+		if err != nil {
+			return nil, fmt.Errorf("writer.SetRow failed, err: %v", err)
+		}
+	}
+
+	err = writer.Flush()
+
+	if err != nil {
+		return nil, fmt.Errorf("writer.Flush failed, err: %v", err)
+	}
+
+	return f, nil
+}

+ 70 - 0
utilx/excel_test.go

@@ -0,0 +1,70 @@
+package utilx
+
+import (
+	"runtime"
+	"testing"
+)
+
+func getCurrentMemoryUsage() uint64 {
+	var m runtime.MemStats
+	runtime.ReadMemStats(&m)
+	return m.Alloc
+}
+
+func TestXlsx(t *testing.T) {
+	// 获取当前内存使用量
+	startMemory := getCurrentMemoryUsage()
+	t.Logf("Start memory: %d MB", startMemory/1024/1024)
+
+	data := make([][]interface{}, 0)
+
+	line := []interface{}{
+		"1",
+		"线上订单",
+		"XJ240429000560",
+		"huishoubao:1714383615112",
+		"-",
+		"iPhone 11",
+		"huishoubao:1.00元",
+		"-",
+		"",
+		"BD",
+		"-",
+		"2024-04-29 17:40:15",
+		"-",
+		"-",
+		"-",
+		"-",
+		"-",
+		"-",
+		"-",
+		"-",
+		"-",
+	}
+
+	for i := 0; i < 100000; i++ {
+		data = append(data, line)
+	}
+	dataMemory := getCurrentMemoryUsage()
+	t.Logf("Data memory:   %d MB", dataMemory/1024/1024)
+	file, err := XlsxV2(data, "订单列表.xlsx")
+
+	xlsxMemory := getCurrentMemoryUsage()
+	t.Logf("xlsx memory:   %d MB", xlsxMemory/1024/1024)
+	if err != nil {
+		t.Errorf("Xlsx failed, err: %v", err)
+		return
+	}
+
+	file.Path = "/Users/zhangyu/Downloads/xx.xlsx"
+
+	_ = file.Save()
+
+	// 再次获取内存使用量
+	endMemory := getCurrentMemoryUsage()
+	t.Logf("End memory:   %d MB", endMemory/1024/1024)
+
+	// 计算最终内存增长
+	memoryGrowth := endMemory - startMemory
+	t.Logf("Memory growth: %d MB", memoryGrowth/1024/1024)
+}

+ 69 - 0
utilx/ip.go

@@ -0,0 +1,69 @@
+package utilx
+
+import (
+	"net"
+	"net/http"
+	"strings"
+)
+
+// ClientIP 获取客户端ip
+func ClientIP(r *http.Request) string {
+	xForwardedFor := r.Header.Get("X-Forwarded-For")
+	ip := strings.TrimSpace(strings.Split(xForwardedFor, ",")[0])
+	if ip != "" {
+		return ip
+	}
+
+	ip = strings.TrimSpace(r.Header.Get("X-Real-Ip"))
+	if ip != "" {
+		return ip
+	}
+
+	if ip, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr)); err == nil {
+		return ip
+	}
+
+	return ""
+}
+
+func GetUserAgent(req *http.Request) string {
+	return req.Header.Get("User-Agent")
+}
+
+// 检测IP地址是否为内网IP
+func IpIsInternal(ip string) bool {
+	if ip == "" {
+		return false
+	}
+	rip := net.ParseIP(ip)
+	// 检查是否位于私有IP范围内
+	privateRanges := []string{
+		"10.0.0.0/8",
+		"172.16.0.0/12",
+		"192.168.0.0/16",
+		"169.254.0.0/16", // 链路本地
+		"127.0.0.0/8",    // 本地回环
+		"::1/128",        // IPv6 本地回环
+		"fe80::/10",      // IPv6 链路本地
+		"fc00::/7",
+	}
+
+	for _, r := range privateRanges {
+		_, network, _ := net.ParseCIDR(r)
+		if network.Contains(rip) {
+			return true
+		}
+	}
+
+	return false
+}
+
+// DirectClientIP 直接获取客户端ip
+func DirectClientIP(r *http.Request) string {
+
+	if ip, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr)); err == nil {
+		return ip
+	}
+
+	return ""
+}

+ 8 - 0
utilx/map.go

@@ -0,0 +1,8 @@
+package utilx
+
+import "reflect"
+
+func IsMap(i interface{}) bool {
+	t := reflect.TypeOf(i)
+	return t == reflect.TypeOf(make(map[string]interface{}))
+}

+ 23 - 0
utilx/money.go

@@ -0,0 +1,23 @@
+package utilx
+
+import "strconv"
+
+// Fen2Yuan 分转元
+func Fen2Yuan(amount int64) string {
+	result := Fen2Y(amount) + "元"
+	return result
+}
+
+// Fen2Y 分转元 返回 00.00  单位元
+func Fen2Y(amount int64) string {
+	amountInYuan := F2y(amount)
+
+	// 将结果转换为字符串格式,并添加单位元
+	result := strconv.FormatFloat(amountInYuan, 'f', 2, 64)
+	return result
+}
+
+// F2y 分转元
+func F2y(amount int64) float64 {
+	return float64(amount) / 100
+}

+ 51 - 0
utilx/msgDese.go

@@ -0,0 +1,51 @@
+package utilx
+
+// MaskPhone 手机号脱敏
+func MaskPhone(phone string) string {
+	if len(phone) < 8 {
+		return "****"
+	}
+	return phone[:3] + "****" + phone[7:]
+}
+
+// MaskPhone 手机号脱敏-中件替换8个 *
+func MaskPhoneEndFront(phone string) string {
+	if len(phone) == 11 {
+		return phone[:1] + "********" + phone[10:]
+	}
+	if len(phone) < 2 {
+		return "****"
+	}
+	phones := []rune(phone)
+	str := ""
+	for i := 0; i < len(phones)-2; i++ {
+		str = str + "*"
+	}
+	return string(phones[0]) + str + string(phones[len(phones)-1])
+}
+
+// MaskNameEnd 名称脱敏 保留最后一个字
+func MaskNameEnd(name string) string {
+	runes := []rune(name)
+	if len(runes) <= 1 {
+		return "**"
+	}
+	str := ""
+	for i := 0; i < len(runes)-1; i++ {
+		str = str + "*"
+	}
+	return str + string(runes[len(runes)-1])
+}
+
+// MaskNameFront 名称脱敏 保留最后一个字
+func MaskNameFront(name string) string {
+	runes := []rune(name)
+	if len(runes) <= 1 {
+		return "**"
+	}
+	str := ""
+	for i := 0; i < len(runes)-1; i++ {
+		str = str + "*"
+	}
+	return string(runes[0]) + str
+}

+ 8 - 0
utilx/number.go

@@ -0,0 +1,8 @@
+package utilx
+
+import "strconv"
+
+func IsNumeric(str string) bool {
+	_, err := strconv.ParseFloat(str, 64)
+	return err == nil
+}

+ 25 - 0
utilx/rand.go

@@ -0,0 +1,25 @@
+package utilx
+
+import (
+	"crypto/rand"
+	"math/big"
+)
+
+const (
+	charsetStr = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+)
+
+// RandStr 生成指定长度的随机字符串
+func RandStr(length int) (string, error) {
+	charsetLength := big.NewInt(int64(len(charsetStr)))
+
+	randomString := make([]byte, length)
+	for i := 0; i < length; i++ {
+		randomIndex, err := rand.Int(rand.Reader, charsetLength)
+		if err != nil {
+			return "", err
+		}
+		randomString[i] = charsetStr[randomIndex.Int64()]
+	}
+	return string(randomString), nil
+}

+ 21 - 0
utilx/slice.go

@@ -0,0 +1,21 @@
+package utilx
+
+// InSliceStr 判断slice中是否存在某元素,仅支持小切片
+func InSliceStr(val string, s []string) bool {
+	for _, v := range s {
+		if val == v {
+			return true
+		}
+	}
+	return false
+}
+
+// InSliceInt64 判断slice中是否存在某元素,仅支持小切片
+func InSliceInt64(val int64, s []int64) bool {
+	for _, v := range s {
+		if val == v {
+			return true
+		}
+	}
+	return false
+}

+ 3 - 0
utilx/struct.go

@@ -0,0 +1,3 @@
+package utilx
+
+type Empty struct{}

+ 30 - 0
utilx/time.go

@@ -0,0 +1,30 @@
+package utilx
+
+import (
+	"database/sql"
+	"time"
+)
+
+const TimeLayout = "2006-01-02 15:04:05"
+
+// ParseTime 文本转时间
+func ParseTime(str string) time.Time {
+	t, _ := TryParseTime(str)
+	return t
+}
+
+func TryParseTime(str string) (time.Time, error) {
+	return time.ParseInLocation(TimeLayout, str, time.Local)
+}
+
+// NullTimeFormat 格式化sql.NullTime
+func NullTimeFormat(t sql.NullTime) string {
+	v, _ := t.Value()
+	if v == nil {
+		return ""
+	}
+	if ts, ok := v.(time.Time); ok {
+		return ts.Format(TimeLayout)
+	}
+	return ""
+}

+ 11 - 0
utilx/utilx_test.go

@@ -0,0 +1,11 @@
+package utilx
+
+import (
+	"fmt"
+	"testing"
+)
+
+func TestName(t *testing.T) {
+	fmt.Println(MaskPhoneEndFront("13132190650"))
+
+}