first commit
This commit is contained in:
7
pkg/tools/float_util.go
Normal file
7
pkg/tools/float_util.go
Normal 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
105
pkg/tools/jwt.go
Normal 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
69
pkg/tools/jwt_test.go
Normal 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
26
pkg/tools/list.go
Normal 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
107
pkg/tools/password.go
Normal 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
34
pkg/tools/path.go
Normal 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)
|
||||
}
|
||||
182
pkg/tools/picture/thumb_task.go
Normal file
182
pkg/tools/picture/thumb_task.go
Normal 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)))
|
||||
}
|
||||
57
pkg/tools/picture/thumb_task_test.go
Normal file
57
pkg/tools/picture/thumb_task_test.go
Normal 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
26
pkg/tools/safe.go
Normal 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
332
pkg/tools/strings.go
Normal 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
19
pkg/tools/strings_test.go
Normal 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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user