333 lines
7.5 KiB
Go
333 lines
7.5 KiB
Go
package tools
|
||
|
||
import (
|
||
"crypto/md5"
|
||
"encoding/hex"
|
||
"errors"
|
||
"fmt"
|
||
"math/rand"
|
||
"regexp"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
"unicode/utf8"
|
||
|
||
"github.com/google/uuid"
|
||
"github.com/shopspring/decimal"
|
||
)
|
||
|
||
const (
|
||
defaultSaltLen = 4
|
||
)
|
||
|
||
func Uuid() string {
|
||
u7, _ := uuid.NewV7()
|
||
return strings.ReplaceAll(u7.String(), "-", "")
|
||
}
|
||
|
||
func SafePStr(p *string) string {
|
||
if p == nil {
|
||
return ""
|
||
}
|
||
return *p
|
||
}
|
||
|
||
func SafeI32(p *int32) int32 {
|
||
if p == nil {
|
||
return 0
|
||
}
|
||
return *p
|
||
}
|
||
|
||
func StrFromInt(i int) string {
|
||
return strconv.Itoa(i)
|
||
}
|
||
|
||
func StrFromInt32(i int32) string {
|
||
return strconv.Itoa(int(i))
|
||
}
|
||
func StrFromInt64(i int64) string {
|
||
return strconv.FormatInt(i, 10)
|
||
}
|
||
|
||
func StrFromFloat32(f float32) string {
|
||
return strconv.FormatFloat(float64(f), 'f', -1, 32)
|
||
}
|
||
|
||
func StrFromFloat64(f float64) string {
|
||
return strconv.FormatFloat(f, 'f', -1, 64)
|
||
}
|
||
|
||
func MD5(str string) string {
|
||
h := md5.New()
|
||
h.Write([]byte(str))
|
||
return hex.EncodeToString(h.Sum(nil))
|
||
}
|
||
|
||
func MD5WithSalt(str, salt string) string {
|
||
return MD5(str + salt)
|
||
}
|
||
|
||
func RandSalt() string {
|
||
return RandSaltN(defaultSaltLen)
|
||
}
|
||
|
||
func RandSaltN(n uint8) string {
|
||
if n == 0 {
|
||
return ""
|
||
}
|
||
var ans = make([]byte, n)
|
||
ans[0] = byte(rand.Intn(9)+1) + '0'
|
||
for i := byte(1); i < n; i++ {
|
||
ans[i] = byte(rand.Intn(10)) + '0'
|
||
}
|
||
return string(ans)
|
||
}
|
||
|
||
func StrToInt(str string) int {
|
||
return int(StrToFloat(str))
|
||
}
|
||
func StrToInt64(str string) int64 {
|
||
return int64(StrToFloat(str))
|
||
}
|
||
|
||
func StrToInt32(str string) int32 {
|
||
return int32(StrToFloat(str))
|
||
}
|
||
func StrToFloat(str string) float64 {
|
||
if s, err := strconv.ParseFloat(str, 64); err != nil {
|
||
return 0
|
||
} else {
|
||
return s
|
||
}
|
||
}
|
||
|
||
// GetWeekEnd 根据年份和ISO周数结束日期(周日)
|
||
func GetWeekEnd(year, week int) (endWeek time.Time) {
|
||
// 1月4日至少是该年的第一周
|
||
// 这是计算ISO周的一个参考点
|
||
t := time.Date(year, time.January, 4, 0, 0, 0, 0, time.UTC)
|
||
|
||
// 找到这一天所在的周一是哪一天
|
||
// ISO周中周一是一周的第一天
|
||
weekday := t.Weekday()
|
||
if weekday == time.Sunday {
|
||
weekday = 7 // 调整周日为7,以便正确计算偏移量
|
||
}
|
||
// 计算到周一的偏移量
|
||
offset := int(time.Monday - weekday)
|
||
if offset > 0 {
|
||
offset -= 7
|
||
}
|
||
t = t.AddDate(0, 0, offset)
|
||
|
||
// 计算目标周的起始日期(加上周数-1周)
|
||
t = t.AddDate(0, 0, (week-1)*7)
|
||
|
||
return t.AddDate(0, 0, 6)
|
||
}
|
||
|
||
// GetWeekRange 根据年份和ISO周数获取该周的开始日期(周一)和结束日期(周日)
|
||
func GetWeekRange(year, week int) (start, end string) {
|
||
// 1月4日至少是该年的第一周
|
||
// 这是计算ISO周的一个参考点
|
||
t := time.Date(year, time.January, 4, 0, 0, 0, 0, time.UTC)
|
||
|
||
// 找到这一天所在的周一是哪一天
|
||
// ISO周中周一是一周的第一天
|
||
weekday := t.Weekday()
|
||
if weekday == time.Sunday {
|
||
weekday = 7 // 调整周日为7,以便正确计算偏移量
|
||
}
|
||
// 计算到周一的偏移量
|
||
offset := int(time.Monday - weekday)
|
||
if offset > 0 {
|
||
offset -= 7
|
||
}
|
||
t = t.AddDate(0, 0, offset)
|
||
|
||
// 计算目标周的起始日期(加上周数-1周)
|
||
t = t.AddDate(0, 0, (week-1)*7)
|
||
|
||
// 开始日期是周一
|
||
start = t.Format("2006-01-02")
|
||
|
||
// 结束日期是周日(加6天)
|
||
end = t.AddDate(0, 0, 6).Format("2006-01-02")
|
||
|
||
return
|
||
}
|
||
func DaysBetween(date1, date2 time.Time) int {
|
||
// 只比较日期部分,忽略时间
|
||
y1, m1, d1 := date1.Date()
|
||
y2, m2, d2 := date2.Date()
|
||
date1 = time.Date(y1, m1, d1, 0, 0, 0, 0, date1.Location())
|
||
date2 = time.Date(y2, m2, d2, 0, 0, 0, 0, date2.Location())
|
||
// 计算时间差(纳秒)
|
||
diff := date1.Sub(date2)
|
||
if diff < 0 {
|
||
diff = -diff
|
||
}
|
||
|
||
// 转换为天数
|
||
return int(diff.Hours() / 24)
|
||
}
|
||
|
||
func StrToDate(str string) time.Time {
|
||
layout := "2006-01-02"
|
||
t, _ := time.ParseInLocation(layout, str, time.Local)
|
||
return t
|
||
}
|
||
func StrToDateTime(str string) time.Time {
|
||
layout := "2006-01-02 15:04:05"
|
||
t, _ := time.ParseInLocation(layout, str, time.Local)
|
||
return t
|
||
}
|
||
func SecMobile(src string) string {
|
||
start, end := 3, 7
|
||
bytes := []rune(src)
|
||
for start < end && start < len(bytes) {
|
||
bytes[start] = '*'
|
||
start++
|
||
}
|
||
return string(bytes)
|
||
}
|
||
|
||
const (
|
||
lenMin = 4
|
||
lenMax = 6
|
||
defaultTimeout = time.Minute * 5
|
||
)
|
||
|
||
func RandomCode() string {
|
||
code, _ := RandomCodeByLen(lenMin)
|
||
return code
|
||
}
|
||
|
||
func RandomCodeWithTimeout() (code string, validTo time.Time) {
|
||
code, _ = RandomCodeByLen(lenMin)
|
||
validTo = time.Now().Add(defaultTimeout)
|
||
return
|
||
}
|
||
|
||
func RandomCodeByLen(len int) (code string, e error) {
|
||
if len < lenMin || len > lenMax {
|
||
e = errors.New("只能生成4~6位验证码")
|
||
return
|
||
}
|
||
switch len {
|
||
case 4:
|
||
code = fmt.Sprintf("%4d", r.Intn(8999)+1000)
|
||
case 5:
|
||
code = fmt.Sprintf("%5d", r.Intn(89999)+10000)
|
||
case 6:
|
||
code = fmt.Sprintf("%6d", r.Intn(899999)+100000)
|
||
}
|
||
return
|
||
}
|
||
|
||
func MaskIDCardFlexible(id string) string {
|
||
length := utf8.RuneCountInString(id)
|
||
runes := []rune(id)
|
||
switch length {
|
||
case 18:
|
||
// 18位:前6后4
|
||
return string(runes[:6]) + "**********" + string(runes[16:])
|
||
case 15:
|
||
// 15位:前6后3(老身份证)
|
||
return string(runes[:6]) + "******" + string(runes[12:])
|
||
case 0:
|
||
return ""
|
||
default:
|
||
return "****" // 或返回原值 / 错误提示
|
||
}
|
||
}
|
||
|
||
// 判断decimal是否是0.5的倍数
|
||
func IsHalfMultiple(d decimal.Decimal) bool {
|
||
// 乘以2后检查是否为整数
|
||
multiplied := d.Mul(decimal.NewFromInt(2))
|
||
// 将整数部分转换为decimal类型后再比较
|
||
intPart := decimal.NewFromInt(multiplied.IntPart())
|
||
return multiplied.Equal(intPart)
|
||
}
|
||
|
||
// IsChineseIDCard 检查输入是否为中国大陆合法的 18 位身份证号码
|
||
func IsChineseIDCard(id string) bool {
|
||
// 1. 检查长度
|
||
if utf8.RuneCountInString(id) != 18 {
|
||
return false
|
||
}
|
||
|
||
// 2. 使用正则表达式校验格式
|
||
// 第1-17位:数字;第18位:数字或X(大小写均可)
|
||
matched, err := regexp.MatchString(`^\d{17}[\dXx]$`, id)
|
||
if err != nil || !matched {
|
||
return false
|
||
}
|
||
|
||
// 3. 校验最后一位校验码
|
||
return validateCheckDigit(id)
|
||
}
|
||
|
||
// validateCheckDigit 验证身份证最后一位校验码是否正确
|
||
func validateCheckDigit(id string) bool {
|
||
// 权重因子
|
||
factor := []int{7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2}
|
||
// 校验码对照表
|
||
checkCode := []byte{'1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'}
|
||
|
||
sum := 0
|
||
for i := 0; i < 17; i++ {
|
||
if id[i] < '0' || id[i] > '9' {
|
||
return false
|
||
}
|
||
sum += int(id[i]-'0') * factor[i]
|
||
}
|
||
// 计算校验码
|
||
mod := sum % 11
|
||
expected := checkCode[mod]
|
||
|
||
// 比较最后一位(忽略大小写)
|
||
lastChar := id[17]
|
||
if lastChar >= 'a' && lastChar <= 'z' {
|
||
lastChar -= 32 // 转大写
|
||
}
|
||
if lastChar >= 'A' && lastChar <= 'Z' {
|
||
lastChar += 32 // 转小写再比?不,直接比 'X'
|
||
}
|
||
return lastChar == expected || (lastChar == 'x' && expected == 'X')
|
||
}
|
||
|
||
func IsMultipleOfHalf(d decimal.Decimal) bool {
|
||
// 乘以 2
|
||
times2 := d.Mul(decimal.NewFromInt(2))
|
||
|
||
// 检查是否为整数(小数位为 0)
|
||
return times2.Equal(times2.Truncate(0))
|
||
}
|
||
func NumToChinese(n int) string {
|
||
chineseDigits := []string{"零", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十"}
|
||
return chineseDigits[n]
|
||
}
|
||
|
||
// 将切片按每组10个元素拆分
|
||
// 参数:原切片、每组元素数量(此处固定为10)
|
||
// 返回:拆分后的二维切片
|
||
func PaginateList[T any](slice []T, groupSize int) [][]T {
|
||
var groups [][]T
|
||
length := len(slice)
|
||
|
||
// 循环截取,每次取 groupSize 个元素
|
||
for i := 0; i < length; i += groupSize {
|
||
end := i + groupSize
|
||
// 处理最后一组可能不足 groupSize 的情况
|
||
if end > length {
|
||
end = length
|
||
}
|
||
// 截取当前组并添加到结果中
|
||
groups = append(groups, slice[i:end])
|
||
}
|
||
return groups
|
||
}
|