first commit

This commit is contained in:
Yangtao
2025-11-18 17:48:20 +08:00
commit 6e56cab848
196 changed files with 65809 additions and 0 deletions

7
pkg/tools/float_util.go Normal file
View File

@ -0,0 +1,7 @@
package tools
import "math"
func Float64EqualsZero(data float64) bool {
return math.Abs(data-0) < 1e-5
}

105
pkg/tools/jwt.go Normal file
View File

@ -0,0 +1,105 @@
package tools
import (
"crypto/ecdsa"
"errors"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
const (
HeaderNonce = "X-NONCE"
HeaderSign = "X-S-SIGN"
TypeResource = "RESOURCE" // 资源权限
TypeInterface = "INTERFACE" // 接口权限
TypeData = "DATA" // 数据权限
HeaderUserId = "X-USER-ID"
HeaderScopeId = "X-SCOPE-ID"
HeaderScopeCode = "X-SCOPE"
HeaderUsername = "X-USERNAME"
HeaderNickname = "X-NICKNAME"
HeaderClient = "X-CLIENT"
ClientAdminCompany = "ADMIN_COMPANY"
ClientAdminAppUser = "APP_USER"
ClientAdminAppDriver = "APP_DRIVER"
)
const (
iss = "anxpp.com"
secret = `-----BEGIN ECD PRIVATE KEY-----
MHcCAQEEIDjrRKvb5a6Klcqi38w5vQMZZluja1DTOG+UFrh3hRxfoAoGCCqGSM49AwEHoUQDQgAEt6K29uBUrZmM4Sdyw/w9d2EUk1kiV8YM+tmGkuVyXQ+qFcbl7f4V1UMbKzXsqmyCPxgFBaeN61/nxdi99Ds4bw==
-----END ECD PRIVATE KEY-----`
pk = `-----BEGIN ECD PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEt6K29uBUrZmM4Sdyw/w9d2EUk1kiV8YM+tmGkuVyXQ+qFcbl7f4V1UMbKzXsqmyCPxgFBaeN61/nxdi99Ds4bw==
-----END ECD PUBLIC KEY-----`
)
type Claims struct {
Username string `json:"un,omitempty"`
Nickname string `json:"nn,omitempty"`
Client string `json:"cli,omitempty"`
jwt.RegisteredClaims
}
func (c Claims) NeedRefresh(d time.Duration) bool {
return c.ExpiresAt.Before(time.Now().Add(d))
}
func GenerateToken(client, uid, username, nickname string, exp, nbf, iat time.Time, aud []string) (tokenStr string, e error) {
token := jwt.NewWithClaims(jwt.SigningMethodES256, &Claims{
Username: username,
Nickname: nickname,
Client: client,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: iss, // 令牌发行者
Subject: uid, // 统一用户ID
Audience: aud, // 受众
ExpiresAt: &jwt.NumericDate{Time: exp}, // 过期时间
NotBefore: &jwt.NumericDate{Time: nbf}, // 启用时间
IssuedAt: &jwt.NumericDate{Time: iat}, // 发布时间
ID: JwtID(), // jwt ID
},
})
var ecdsaKey *ecdsa.PrivateKey
if ecdsaKey, e = jwt.ParseECPrivateKeyFromPEM([]byte(secret)); e != nil {
panic(e)
}
tokenStr, e = token.SignedString(ecdsaKey)
return
}
func VerifyToken(tokenStr string) (access bool, claims Claims, e error) {
var ecdsaKeyPub *ecdsa.PublicKey
if ecdsaKeyPub, e = jwt.ParseECPublicKeyFromPEM([]byte(pk)); e != nil {
return
}
var token *jwt.Token
if token, e = jwt.ParseWithClaims(tokenStr, &claims, func(token *jwt.Token) (interface{}, error) {
return ecdsaKeyPub, nil
}); e != nil {
return
}
access = token.Valid
return
}
func JwtID() string {
u4 := uuid.New()
return strings.ReplaceAll(u4.String(), "-", "")
}
func ParseAuthToken(jwt string) (access bool, claims Claims, e error) {
// jwt := c.GetHeader("Authorization")
fields := strings.Fields(jwt)
if len(fields) < 2 {
e = errors.New("wrong token")
return
}
return VerifyToken(fields[1])
}

69
pkg/tools/jwt_test.go Normal file
View File

@ -0,0 +1,69 @@
package tools
import (
"testing"
"time"
)
func TestGenerateToken(t *testing.T) {
type args struct {
client string
uid string
username string
nickname string
exp time.Time
nbf time.Time
iat time.Time
aud []string
}
var now = time.Now()
var loginExpiration = 60 * 24 * 30 // 30天
tests := []struct {
name string
args args
wantTokenStr string
wantErr bool
}{
// 28a08f58496f11eeb95f0242ac110004
{
name: "test01",
args: struct {
client string
uid string
username string
nickname string
exp time.Time
nbf time.Time
iat time.Time
aud []string
}{
client: "APP_USER",
uid: "28a08f58496f11eeb95f0242ac110004",
username: "driver001",
nickname: "driver001",
exp: now.Add(time.Minute * time.Duration(loginExpiration)),
nbf: now,
iat: now,
aud: []string{},
},
wantTokenStr: "",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotTokenStr, err := GenerateToken(tt.args.client, tt.args.uid, tt.args.username, tt.args.nickname, tt.args.exp, tt.args.nbf, tt.args.iat, tt.args.aud)
if (err != nil) != tt.wantErr {
t.Errorf("GenerateToken() error = %v, wantErr %v", err, tt.wantErr)
return
}
if gotTokenStr != tt.wantTokenStr {
t.Errorf("GenerateToken() = %v, want %v", gotTokenStr, tt.wantTokenStr)
}
})
}
}
func TestUuid(t *testing.T) {
println(Uuid())
}

26
pkg/tools/list.go Normal file
View File

@ -0,0 +1,26 @@
package tools
func ListToMap[K comparable, S any](list []S, keyFunc func(S) K) map[K]S {
if len(list) == 0 {
return make(map[K]S)
}
result := make(map[K]S, len(list))
for _, item := range list {
key := keyFunc(item)
result[key] = item
}
return result
}
func SliceToMapList[K comparable, V any](slice []V, keyFunc func(V) K) map[K][]V {
result := make(map[K][]V)
for _, v := range slice {
key := keyFunc(v)
if _, ok := result[key]; !ok {
result[key] = make([]V, 0)
result[key] = append(result[key], v)
} else {
result[key] = append(result[key], v)
}
}
return result
}

107
pkg/tools/password.go Normal file
View File

@ -0,0 +1,107 @@
package tools
import (
"crypto/ecdsa"
"crypto/elliptic"
randcrypto "crypto/rand"
"crypto/x509"
"encoding/pem"
"fmt"
"math/rand"
"time"
)
const (
NUmStr = "0123456789"
CharStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
SpecStr = "+=-@#~,.[]()!%^*$"
)
var (
r = rand.New(rand.NewSource(time.Now().UnixNano()))
)
func GeneratePasswd(level string, length int32) string {
//var (
// length int = 16
// charset = "advance"
//)
//初始化密码切片
var passwd = make([]byte, length, length)
//源字符串
var sourceStr string
//判断字符类型,如果是数字
switch level {
case "num":
sourceStr = NUmStr
case "char":
sourceStr = level
case "mix":
sourceStr = fmt.Sprintf("%s%s", NUmStr, CharStr)
case "advance":
sourceStr = fmt.Sprintf("%s%s%s", NUmStr, CharStr, SpecStr)
default:
sourceStr = NUmStr
}
//遍历生成一个随机index索引,
for i := range passwd {
index := r.Intn(len(sourceStr))
passwd[i] = sourceStr[index]
}
return string(passwd)
}
type PassLevel string
const (
High PassLevel = "advance"
Mid PassLevel = "mix"
Low PassLevel = "char"
Danger PassLevel = "num"
nUmStr = "0123456789"
charStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
specStr = "+=-@#~,.[]()!%^*$"
)
func GeneratePwd(level PassLevel, length int32) string {
var passwd = make([]byte, length, length)
var sourceStr string
switch level {
case Danger:
sourceStr = nUmStr
case Low:
sourceStr = charStr
case Mid:
sourceStr = fmt.Sprintf("%s%s", nUmStr, charStr)
case High:
sourceStr = fmt.Sprintf("%s%s%s", nUmStr, charStr, specStr)
default:
sourceStr = fmt.Sprintf("%s%s", nUmStr, charStr)
}
for i := range passwd {
index := r.Intn(len(sourceStr))
passwd[i] = sourceStr[index]
}
return string(passwd)
}
func GenerateECDSAKeyPem() (primaryKey, publicKey []byte, e error) {
var (
key *ecdsa.PrivateKey
sec []byte
pk []byte
)
if key, e = ecdsa.GenerateKey(elliptic.P256(), randcrypto.Reader); e != nil {
return
}
if sec, e = x509.MarshalECPrivateKey(key); e != nil {
return
}
if pk, e = x509.MarshalPKIXPublicKey(key.Public()); e != nil {
return
}
primaryKey = pem.EncodeToMemory(&pem.Block{Type: "ECD PRIVATE KEY", Bytes: sec})
publicKey = pem.EncodeToMemory(&pem.Block{Type: "ECD PUBLIC KEY", Bytes: pk})
return
}

34
pkg/tools/path.go Normal file
View File

@ -0,0 +1,34 @@
package tools
import (
"github.com/spf13/viper"
"os"
"path"
"path/filepath"
)
const (
urlPre = "/api/v1/attachment"
)
func Mkdir(basePath string, folderName string) string {
folderPath := filepath.Join(basePath, folderName)
_ = os.MkdirAll(folderPath, os.ModePerm)
return folderPath
}
func BasePath() string {
return viper.GetString("attachment.path")
}
func UrlBasePath() string {
return "/static"
}
func URLPath(filePath string) string {
return filepath.Join("/", UrlBasePath(), filePath)
}
func AttachmentUrl(relative string) string {
return path.Join(urlPre, relative)
}

View File

@ -0,0 +1,182 @@
package picture
import (
"context"
//cache "ServiceAll/modules/company/internal/client/redis"
"servicebase/pkg/datasource"
"servicebase/pkg/log"
"servicebase/pkg/repo"
"servicebase/pkg/tools"
"fmt"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/disintegration/imaging"
"github.com/pkg/errors"
"github.com/spf13/viper"
"go.uber.org/zap"
)
const (
CurIndexName = "CUR_INDEX_DATA_ATTACHMENT"
CommonTypeImage = "IMAGE"
CommonTypeUnknown = "UNCLASSIFIED"
pathThumb = "thumb"
FileTypeThumb = "jpg"
)
var typeOfCommonType map[string]string
func init() {
typeOfCommonType = map[string]string{
"jpg": CommonTypeImage,
"png": CommonTypeImage,
"jfif": CommonTypeImage,
"jpeg": CommonTypeImage,
"bmp": CommonTypeImage,
"webp": CommonTypeImage,
"icon": CommonTypeImage,
}
}
func ParseCommonType(_type string) string {
if v, ok := typeOfCommonType[_type]; ok {
return v
}
return CommonTypeUnknown
}
func StartThumbTask() {
const lines = 10
const dur = time.Second * 10
for {
if cnt := ThumbTask(lines); cnt != lines {
time.Sleep(dur)
}
}
}
// ThumbTask 图片缩略图任务
// 定期扫描附件表,将未生成压缩图的图片生成压缩图
func ThumbTask(lines int) (cnt int) {
var (
pid int64
e error
ctx = context.Background()
w, h uint = 0, 100
)
if pid, e = curIndexPID(); e != nil {
log.Error("ThumbTask init error", zap.String("error", e.Error()), zap.Int64("pid", pid))
return
}
var list []struct {
Pid int64
ID string
CommonType string
FilePath string
FileType string
}
if e = datasource.DB.WithContext(ctx).
Raw("select * from data_attachment where pid>=? and common_type=? and file_thumb_path=? order by pid limit ?", pid, CommonTypeImage, "", lines).
Find(&list).Error; e != nil {
log.Error("ThumbTask select error", zap.String("error", e.Error()), zap.Int64("pid", pid))
return
}
// todo with WaitGroup
//userCache := cache.User()
for _, item := range list {
if e = DoThumb(item.FilePath, item.ID, item.CommonType, item.FileType, w, h); e != nil {
log.Error("ThumbTask thumb error",
zap.String("error", e.Error()),
zap.Int64("pid", pid),
zap.String("path", item.FilePath),
zap.String("file", item.ID),
zap.String("type", item.FileType))
return
}
//userCache.Set(ctx, CurIndexName, item.Pid+1, 0)
cnt++
}
return
}
// 获取开始生成缩略图的索引
func curIndexPID() (pid int64, e error) {
// 取当前已压缩的位置
//ctx := context.Background()
//userCache := cache.User()
//if pid, _ = userCache.Get(ctx, CurIndexName).Int64(); pid == 0 {
// // 当前不存在,从数据库统计
// if e = datasource.DB.WithContext(ctx).
// Raw("select pid from data_attachment where common_type=? and file_thumb_path=? order by pid limit 1", CommonTypeImage, "").
// First(&pid).Error; e != nil {
// return
// }
// userCache.Set(ctx, CurIndexName, pid, 0)
//}
return
}
func DoThumb(path, id, commonType, _type string, w, h uint) error {
if w == 0 && h == 0 {
return errors.New("尺寸错误")
}
commonType = strings.ToLower(commonType)
// 得到后缀和
image, err := imaging.Open(filepath.Join(viper.GetString("attachment.path"), path))
if err != nil {
return err
}
var suffix []byte
if w > 0 {
suffix = append(suffix, 'w')
suffix = append(suffix, strconv.Itoa(int(w))...)
}
if h > 0 {
suffix = append(suffix, 'h')
suffix = append(suffix, strconv.Itoa(int(h))...)
}
_ = tools.Mkdir(filepath.Join(viper.GetString("attachment.path"), commonType), pathThumb)
thumbFilename := filepath.Join(commonType, pathThumb, fmt.Sprintf("%s.%s.%s", id, suffix, FileTypeThumb))
targetImage := imaging.Resize(image, int(w), int(h), imaging.Lanczos)
return repo.Q.Transaction(func(tx *repo.Query) error {
if _, e := tx.DataAttach.WithContext(context.Background()).
Select(tx.DataAttach.FileThumbPath, tx.DataAttach.FileThumbType).
Where(tx.DataAttach.ID.Eq(id)).
UpdateSimple(
tx.DataAttach.FileThumbPath.Value(thumbFilename),
tx.DataAttach.FileThumbType.Value(FileTypeThumb),
); e != nil {
return e
}
return imaging.Save(targetImage, filepath.Join(filepath.Join(viper.GetString("attachment.path"), thumbFilename)))
})
}
func Do(path, id, commonType, _type string, w, h uint) (string, error) {
if w == 0 && h == 0 {
return "", errors.New("尺寸错误")
}
commonType = strings.ToLower(commonType)
// 得到后缀和
image, err := imaging.Open(filepath.Join(viper.GetString("attachment.path"), path))
if err != nil {
return "", err
}
var suffix []byte
if w > 0 {
suffix = append(suffix, 'w')
suffix = append(suffix, strconv.Itoa(int(w))...)
}
if h > 0 {
suffix = append(suffix, 'h')
suffix = append(suffix, strconv.Itoa(int(h))...)
}
_ = tools.Mkdir(filepath.Join(viper.GetString("attachment.path"), commonType), pathThumb)
thumbFilename := filepath.Join(commonType, pathThumb, fmt.Sprintf("%s.%s.%s", id, suffix, FileTypeThumb))
targetImage := imaging.Resize(image, int(w), int(h), imaging.Lanczos)
return thumbFilename, imaging.Save(targetImage, filepath.Join(filepath.Join(viper.GetString("attachment.path"), thumbFilename)))
}

View File

@ -0,0 +1,57 @@
package picture
import (
"servicebase/pkg/datasource"
"servicebase/pkg/log"
"fmt"
"testing"
"github.com/disintegration/imaging"
"github.com/spf13/viper"
)
func init() {
viper.Set("db.connectString", "zhzl:Zhzl_2022@tcp(cq.anxpp.com:3306)/data_front?charset=utf8mb4&parseTime=True&loc=Local")
log.Init()
datasource.InitMySQl()
}
func TestThumbTask(t *testing.T) {
path := "../../../data/IMAGE/d4d7c746cce146a190cf97154c6f07c1.jfif"
tsrc, err := imaging.Open(path)
if err != nil {
println(err)
return
}
// 以下质量由低到高
//dstImage := imaging.Resize(tsrc, 0, 100, imaging.NearestNeighbor)
//err = imaging.Save(dstImage, fmt.Sprintf("%s%s.%s", "../../../data/IMAGE/", "NearestNeighbor", "jpg"))
//if err != nil {
// println(err.Error())
//}
//dstImage = imaging.Resize(tsrc, 0, 100, imaging.Linear)
//err = imaging.Save(dstImage, fmt.Sprintf("%s%s.%s", "../../../data/IMAGE/", "Linear", "jpg"))
//if err != nil {
// println(err.Error())
//}
//dstImage = imaging.Resize(tsrc, 0, 100, imaging.CatmullRom)
//err = imaging.Save(dstImage, fmt.Sprintf("%s%s.%s", "../../../data/IMAGE/", "CatmullRom", "jpg"))
//if err != nil {
// println(err.Error())
//}
dstImage := imaging.Resize(tsrc, 0, 100, imaging.Lanczos)
err = imaging.Save(dstImage, fmt.Sprintf("%s%s.%s", "../../../data/IMAGE/", "Lanczos", "jpg"))
if err != nil {
println(err.Error())
}
dstImage = imaging.Resize(tsrc, 0, 100, imaging.Lanczos)
err = imaging.Save(dstImage, fmt.Sprintf("%s%s.%s", "../../../data/IMAGE/", "Lanczos", "png"))
if err != nil {
println(err.Error())
}
}
func Test_curIndexPID(t *testing.T) {
gotPid, err := curIndexPID()
println(gotPid, err)
}

26
pkg/tools/safe.go Normal file
View File

@ -0,0 +1,26 @@
package tools
import (
"runtime/debug"
"servicebase/pkg/log"
)
func Recover(cleanups ...func()) {
for _, cleanup := range cleanups {
cleanup()
}
if p := recover(); p != nil {
log.ErrorF("occur panic: [%+v], stack info [%s]", p, debug.Stack())
}
}
func RunSafe(fn func()) {
defer Recover()
fn()
}
func GoSafe(fn func()) {
go RunSafe(fn)
}

332
pkg/tools/strings.go Normal file
View File

@ -0,0 +1,332 @@
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
}

19
pkg/tools/strings_test.go Normal file
View File

@ -0,0 +1,19 @@
package tools
import "testing"
func TestMD5(t *testing.T) {
str, salt := "123", "456"
println(MD5(str))
println(MD5(str + salt))
println(MD5WithSalt(str, salt))
}
func TestRandSaltN(t *testing.T) {
for i := 1; i < 10; i++ {
println(RandSalt())
for j := 0; j < 10; j++ {
println(RandSaltN(uint8(i)))
}
}
}