feat 初始化
This commit is contained in:
6
utils/admin_info.go
Normal file
6
utils/admin_info.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package utils
|
||||
|
||||
type AdminInfo struct {
|
||||
Usn int64 `redis:"usn"`
|
||||
RoleCode string `redis:"roleCode"`
|
||||
}
|
||||
13
utils/channel.go
Normal file
13
utils/channel.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.hlsq.asia/alex/proto/proto/ss/sspb"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetChannelName 获得频道名称
|
||||
func GetChannelName(t sspb.ChannelType, sn int64) string {
|
||||
s := sspb.ChannelType_name[int32(t)]
|
||||
return fmt.Sprintf("%v-%v", strings.TrimPrefix(s, "CHANNEL_TYPE_"), sn)
|
||||
}
|
||||
@@ -9,8 +9,13 @@ import (
|
||||
)
|
||||
|
||||
type WrapErrors struct {
|
||||
message string
|
||||
trace []string
|
||||
original error
|
||||
message string
|
||||
trace []string
|
||||
}
|
||||
|
||||
func (e *WrapErrors) Original() error {
|
||||
return e.original
|
||||
}
|
||||
|
||||
func (e *WrapErrors) Error() string {
|
||||
@@ -30,7 +35,9 @@ func ErrorsWrap(err error, message ...string) error {
|
||||
}
|
||||
var e *WrapErrors
|
||||
if !errors.As(err, &e) {
|
||||
e = &WrapErrors{}
|
||||
e = &WrapErrors{
|
||||
original: err,
|
||||
}
|
||||
}
|
||||
if len(message) > 0 {
|
||||
e.message = fmt.Sprintf("%v: %v", message[0], err.Error())
|
||||
@@ -47,7 +54,9 @@ func ErrorsWrapF(err error, message string, a ...any) error {
|
||||
}
|
||||
var e *WrapErrors
|
||||
if !errors.As(err, &e) {
|
||||
e = &WrapErrors{}
|
||||
e = &WrapErrors{
|
||||
original: err,
|
||||
}
|
||||
}
|
||||
e.message = fmt.Sprintf("%v: %v", fmt.Sprintf(message, a...), err.Error())
|
||||
e.trace = append(e.trace, getErrorPath())
|
||||
|
||||
32
utils/jwt.go
32
utils/jwt.go
@@ -37,3 +37,35 @@ func ParseToken(tokenString string, secret string) (*Claims, error) {
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
type PhoneClaims struct {
|
||||
Phone string `json:"phone"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func GenPhoneToken(phone string, secret string, expires time.Duration) (string, error) {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, PhoneClaims{
|
||||
Phone: phone,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(expires)),
|
||||
},
|
||||
})
|
||||
return token.SignedString([]byte(secret))
|
||||
}
|
||||
|
||||
func ParsePhoneToken(tokenString string, secret string) (*PhoneClaims, error) {
|
||||
if tokenString == "" {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
claims := &PhoneClaims{}
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(_ *jwt.Token) (interface{}, error) {
|
||||
return []byte(secret), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !token.Valid {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
82
utils/locator/locator.go
Normal file
82
utils/locator/locator.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package locator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.hlsq.asia/alex/service-common/db/redis"
|
||||
"git.hlsq.asia/alex/service-common/log"
|
||||
)
|
||||
|
||||
// 玩家定位器
|
||||
|
||||
const (
|
||||
KeyGatewayInfo = "gateway:info:%v"
|
||||
HFieldInfoGatewaySID = "gateway_sid_%v"
|
||||
)
|
||||
|
||||
var (
|
||||
scriptBindGatewaySha1 string
|
||||
scriptBindGateway = `
|
||||
local gateway_key = KEYS[1]
|
||||
local field_key = ARGV[1]
|
||||
local new_sid = ARGV[2]
|
||||
|
||||
local existing_sid = redis.call("HGET", gateway_key, field_key)
|
||||
if existing_sid then
|
||||
return 0
|
||||
end
|
||||
|
||||
redis.call("HSET", gateway_key, field_key, new_sid)
|
||||
return 1
|
||||
`
|
||||
)
|
||||
|
||||
func NewLocator(ctx context.Context, usn int64, platform int32) *Locator {
|
||||
return &Locator{
|
||||
ctx: ctx,
|
||||
usn: usn,
|
||||
platform: platform,
|
||||
}
|
||||
}
|
||||
|
||||
type Locator struct {
|
||||
ctx context.Context
|
||||
usn int64
|
||||
platform int32
|
||||
}
|
||||
|
||||
// BindGateway 绑定网关
|
||||
func (l *Locator) BindGateway(gatewaySID int64) bool {
|
||||
if scriptBindGatewaySha1 == "" {
|
||||
scriptBindGatewaySha1 = redis.GetClient().ScriptLoad(l.ctx, scriptBindGateway).Val()
|
||||
}
|
||||
|
||||
result, err := redis.GetClient().EvalSha(
|
||||
l.ctx,
|
||||
scriptBindGatewaySha1,
|
||||
[]string{fmt.Sprintf(KeyGatewayInfo, l.usn)},
|
||||
fmt.Sprintf(HFieldInfoGatewaySID, l.platform),
|
||||
gatewaySID,
|
||||
).Int64()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if result == 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// UnbindGateway 解绑网关
|
||||
func (l *Locator) UnbindGateway() {
|
||||
err := redis.GetClient().HDel(l.ctx, fmt.Sprintf(KeyGatewayInfo, l.usn), fmt.Sprintf(HFieldInfoGatewaySID, l.platform)).Err()
|
||||
if err != nil {
|
||||
log.Errorf("UnbindGateway Failed %v-%v, err: %v", l.usn, l.platform, err)
|
||||
}
|
||||
}
|
||||
|
||||
// LocateGateway 定位网关
|
||||
func (l *Locator) LocateGateway() int64 {
|
||||
sid, _ := redis.GetClient().HGet(l.ctx, fmt.Sprintf(KeyGatewayInfo, l.usn), fmt.Sprintf(HFieldInfoGatewaySID, l.platform)).Int64()
|
||||
return sid
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/shopspring/decimal"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
)
|
||||
@@ -26,3 +27,86 @@ func StringToInt64(s string) int64 {
|
||||
func Int64ToString(i int64) string {
|
||||
return strconv.FormatInt(i, 10)
|
||||
}
|
||||
|
||||
// AddFloat64 对浮点数求和
|
||||
func AddFloat64(places int32, number ...float64) float64 {
|
||||
if len(number) == 0 {
|
||||
return 0
|
||||
}
|
||||
result := decimal.NewFromFloat(0)
|
||||
for _, n := range number {
|
||||
result = result.Add(decimal.NewFromFloat(n))
|
||||
}
|
||||
return result.Round(places).InexactFloat64()
|
||||
}
|
||||
|
||||
// SubFloat64 对浮点数求差
|
||||
func SubFloat64(places int32, number ...float64) float64 {
|
||||
if len(number) == 0 {
|
||||
return 0
|
||||
}
|
||||
result := decimal.NewFromFloat(number[0])
|
||||
for i := 1; i < len(number); i++ {
|
||||
result = result.Sub(decimal.NewFromFloat(number[i]))
|
||||
}
|
||||
return result.Round(places).InexactFloat64()
|
||||
}
|
||||
|
||||
// MulFloat64 对浮点数求积
|
||||
func MulFloat64(places int32, number ...float64) float64 {
|
||||
if len(number) == 0 {
|
||||
return 0
|
||||
}
|
||||
result := decimal.NewFromFloat(1)
|
||||
for _, n := range number {
|
||||
result = result.Mul(decimal.NewFromFloat(n))
|
||||
}
|
||||
return result.Round(places).InexactFloat64()
|
||||
}
|
||||
|
||||
// DivFloat64 对浮点数求商
|
||||
func DivFloat64(places int32, number ...float64) float64 {
|
||||
if len(number) == 0 {
|
||||
return 0
|
||||
}
|
||||
result := decimal.NewFromFloat(number[0])
|
||||
for i := 1; i < len(number); i++ {
|
||||
if number[i] == 0 {
|
||||
return 0 // 避免除以零
|
||||
}
|
||||
result = result.Div(decimal.NewFromFloat(number[i]))
|
||||
}
|
||||
return result.Round(places).InexactFloat64()
|
||||
}
|
||||
|
||||
// MaxInt32 返回两个 int32 中的较大值
|
||||
func MaxInt32(a, b int32) int32 {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// MinInt32 返回两个 int32 中的较小值
|
||||
func MinInt32(a, b int32) int32 {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// MaxFloat64 返回两个 float64 中的较大值
|
||||
func MaxFloat64(a, b float64) float64 {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// MinFloat64 返回两个 float64 中的较小值
|
||||
func MinFloat64(a, b float64) float64 {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
62
utils/page.go
Normal file
62
utils/page.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"git.hlsq.asia/alex/proto/proto/rs/rscommon"
|
||||
"gorm.io/gen/field"
|
||||
)
|
||||
|
||||
func PageGetOffSet(page *rscommon.PageArgs) int {
|
||||
if page == nil || page.PageSize <= 0 {
|
||||
return 0
|
||||
}
|
||||
return int((page.PageIndex - 1) * page.PageSize)
|
||||
}
|
||||
|
||||
func CalcTotalPage(totalCount, pageSize int32) int32 {
|
||||
if pageSize <= 0 {
|
||||
return 0
|
||||
}
|
||||
return int32(math.Ceil(float64(totalCount) / float64(pageSize)))
|
||||
}
|
||||
|
||||
func BuildPageInfoReply(page *rscommon.PageArgs, totalCount int32) *rscommon.PageInfoReply {
|
||||
if page == nil {
|
||||
page = &rscommon.PageArgs{PageIndex: 1, PageSize: 10}
|
||||
}
|
||||
if page.PageSize <= 0 {
|
||||
page.PageSize = 10
|
||||
}
|
||||
if page.PageIndex <= 0 {
|
||||
page.PageIndex = 1
|
||||
}
|
||||
if totalCount <= 0 {
|
||||
totalCount = 0
|
||||
return &rscommon.PageInfoReply{
|
||||
PageIndex: page.PageIndex,
|
||||
PageSize: page.PageSize,
|
||||
TotalCount: totalCount,
|
||||
}
|
||||
}
|
||||
return &rscommon.PageInfoReply{
|
||||
PageIndex: page.PageIndex,
|
||||
PageSize: page.PageSize,
|
||||
TotalCount: totalCount,
|
||||
}
|
||||
}
|
||||
|
||||
func GetOrderInfo(orderList []*rscommon.OrderInfo, getField func(field string) field.OrderExpr) []field.Expr {
|
||||
orders := make([]field.Expr, 0, len(orderList))
|
||||
for _, order := range orderList {
|
||||
expr := getField(order.Field)
|
||||
if expr != nil {
|
||||
if order.Asc {
|
||||
orders = append(orders, expr.Asc())
|
||||
} else {
|
||||
orders = append(orders, expr.Desc())
|
||||
}
|
||||
}
|
||||
}
|
||||
return orders
|
||||
}
|
||||
33
utils/pool_object.go
Normal file
33
utils/pool_object.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type ObjectPool[T any] struct {
|
||||
pool sync.Pool
|
||||
}
|
||||
|
||||
func NewObjectPool[T any]() *ObjectPool[T] {
|
||||
return &ObjectPool[T]{
|
||||
pool: sync.Pool{
|
||||
New: func() any {
|
||||
return new(T)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ObjectPool[T]) Get() *T {
|
||||
return p.pool.Get().(*T)
|
||||
}
|
||||
|
||||
func (p *ObjectPool[T]) Put(obj *T) {
|
||||
*obj = zero[T]()
|
||||
p.pool.Put(obj)
|
||||
}
|
||||
|
||||
func zero[T any]() T {
|
||||
var z T
|
||||
return z
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"git.hlsq.asia/alex/service-common/log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@@ -27,6 +28,11 @@ func WorkerPool(tasks []interface{}, taskFunc func(interface{}), maxWorkers ...i
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
log.Errorf("WorkerPool panic: %v", e)
|
||||
}
|
||||
}()
|
||||
for task := range jobs {
|
||||
taskFunc(task)
|
||||
}
|
||||
19
utils/qr.go
Normal file
19
utils/qr.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
|
||||
"github.com/skip2/go-qrcode"
|
||||
)
|
||||
|
||||
func GetQRCodeIO(content string, size ...int) string {
|
||||
s := 260 // 写死尺寸 260*260
|
||||
if len(size) > 0 {
|
||||
s = size[0]
|
||||
}
|
||||
png, err := qrcode.Encode(content, qrcode.High, s)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return "data:image/png;base64," + base64.StdEncoding.EncodeToString(png)
|
||||
}
|
||||
59
utils/replay.go
Normal file
59
utils/replay.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"git.hlsq.asia/alex/service-common/db/redis"
|
||||
"git.hlsq.asia/alex/service-common/net/http/httpresp"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
replayTTL = 2 * time.Minute // 防重放缓存有效期
|
||||
lockTTL = 10 * time.Second // 分布式锁有效期
|
||||
)
|
||||
|
||||
// WrapPreventReplay 防止重放请求
|
||||
func WrapPreventReplay[T any](ctx context.Context, usn int64, fun func() (T, error)) (T, error) {
|
||||
var zero T
|
||||
key := ""
|
||||
if md, ok := metadata.FromIncomingContext(ctx); ok {
|
||||
ridArr := md.Get("Request-Id")
|
||||
if len(ridArr) == 0 || ridArr[0] == "" {
|
||||
return fun()
|
||||
}
|
||||
key = fmt.Sprintf("cache:replay:%v:%v", usn, ridArr[0])
|
||||
} else {
|
||||
return fun()
|
||||
}
|
||||
|
||||
// 先查缓存,命中直接返回
|
||||
if val := getReplayCache[T](ctx, key); val != nil {
|
||||
return *val, nil
|
||||
}
|
||||
|
||||
// 尝试抢分布式锁,抢到就执行,没抢到报错
|
||||
lockKey := fmt.Sprintf("%s:lock", key)
|
||||
locked, err := redis.GetClient().SetNX(ctx, lockKey, "1", lockTTL).Result()
|
||||
if err == nil && locked {
|
||||
defer redis.GetClient().Del(ctx, lockKey)
|
||||
result, err := fun()
|
||||
// 这里报错了是没有缓存的
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
redis.GetCacheClient().Set(ctx, key, result, replayTTL)
|
||||
return result, nil
|
||||
}
|
||||
return zero, httpresp.RequestTooFast
|
||||
}
|
||||
|
||||
// getReplayCache 从 Redis 读取重放缓存
|
||||
func getReplayCache[T any](ctx context.Context, key string) *T {
|
||||
var dst T
|
||||
if redis.GetCacheClient().Get(ctx, key, &dst) {
|
||||
return &dst
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package utils
|
||||
import (
|
||||
"context"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type UserSession struct {
|
||||
@@ -17,6 +19,17 @@ func (us *UserSession) GetUsnKey() string {
|
||||
return "usn"
|
||||
}
|
||||
|
||||
func (us *UserSession) GetAccessTokenKey() string {
|
||||
return "at"
|
||||
}
|
||||
|
||||
func (us *UserSession) GetRefreshTokenKey() string {
|
||||
return "rt"
|
||||
}
|
||||
|
||||
// Deprecated: 请使用 ShouldGetUsn 替代,示例:
|
||||
// usn := utils.ShouldGetUsn(ctx)
|
||||
// 不需要判断 usn 是否为 0,只有不鉴权接口的 usn 才可能是 0。
|
||||
func ShouldBindUsn(ctx context.Context, usn *int64) bool {
|
||||
if md, ok := metadata.FromIncomingContext(ctx); ok {
|
||||
usnArr := md.Get("X-Usn")
|
||||
@@ -27,3 +40,30 @@ func ShouldBindUsn(ctx context.Context, usn *int64) bool {
|
||||
}
|
||||
return *usn != 0
|
||||
}
|
||||
|
||||
func ShouldGetUsn(ctx context.Context) int64 {
|
||||
if md, ok := metadata.FromIncomingContext(ctx); ok {
|
||||
usnArr := md.Get("X-Usn")
|
||||
if len(usnArr) == 0 || usnArr[0] == "" {
|
||||
return 0
|
||||
}
|
||||
return StringToInt64(usnArr[0])
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func GetPlatformAndUsn(token string) (int32, int64, bool) {
|
||||
str := strings.Split(token, ":")
|
||||
if len(str) != 3 {
|
||||
return 0, 0, false
|
||||
}
|
||||
platform, err := strconv.Atoi(str[0])
|
||||
if err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
usn, err := strconv.Atoi(str[1])
|
||||
if err != nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
return int32(platform), int64(usn), true
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/bwmarrin/snowflake"
|
||||
"github.com/sony/sonyflake/v2"
|
||||
"log"
|
||||
)
|
||||
|
||||
var instance *snowflake.Node
|
||||
var instance *sonyflake.Sonyflake
|
||||
|
||||
func InitSnowflake(node int64) *snowflake.Node {
|
||||
n, err := snowflake.NewNode(node)
|
||||
func InitSnowflake(node int64) *sonyflake.Sonyflake {
|
||||
n, err := sonyflake.New(sonyflake.Settings{
|
||||
BitsMachineID: 10,
|
||||
MachineID: func() (int, error) {
|
||||
return int(node), nil
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("create snowflake's node failed: %v", err)
|
||||
}
|
||||
@@ -16,9 +21,10 @@ func InitSnowflake(node int64) *snowflake.Node {
|
||||
return instance
|
||||
}
|
||||
|
||||
func SnowflakeInstance() *snowflake.Node {
|
||||
func SnowflakeInt64() int64 {
|
||||
if instance == nil {
|
||||
log.Fatal("snowflake's not init")
|
||||
}
|
||||
return instance
|
||||
id, _ := instance.NextID()
|
||||
return id
|
||||
}
|
||||
|
||||
49
utils/string.go
Normal file
49
utils/string.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
// GetMD5Encode 获取Md5编码
|
||||
func GetMD5Encode(data string) string {
|
||||
h := md5.New()
|
||||
h.Write([]byte(data))
|
||||
return hex.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
const (
|
||||
StringChars1 = "0123456789"
|
||||
StringChars2 = "abcdefghijklmnopqrstuvwxyz"
|
||||
StringChars3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
StringChars4 = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
StringChars5 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
StringChars6 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
StringChars7 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
)
|
||||
|
||||
// RandomString 获取随机字符串
|
||||
func RandomString(chars string, length int8) string {
|
||||
result := make([]byte, length)
|
||||
for i := int8(0); i < length; i++ {
|
||||
result[i] = chars[rand.Intn(len(chars))]
|
||||
if i == 0 && result[i] == '0' {
|
||||
i--
|
||||
}
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// GetDisplayWidth 获取字符串长度
|
||||
func GetDisplayWidth(text string) int {
|
||||
width := 0
|
||||
for _, r := range text {
|
||||
if r <= 0x7F {
|
||||
width += 1 // ASCII,半角
|
||||
} else {
|
||||
width += 2 // CJK、日文假名等,全角
|
||||
}
|
||||
}
|
||||
return width
|
||||
}
|
||||
26
utils/time.go
Normal file
26
utils/time.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CalcDaysDiff 计算两个日期相差多少天
|
||||
func CalcDaysDiff(one, two time.Time) int32 {
|
||||
oneZero := time.Date(
|
||||
one.Year(),
|
||||
one.Month(),
|
||||
one.Day(),
|
||||
0, 0, 0, 0,
|
||||
time.Local,
|
||||
)
|
||||
twoZero := time.Date(
|
||||
two.Year(),
|
||||
two.Month(),
|
||||
two.Day(),
|
||||
0, 0, 0, 0,
|
||||
time.Local,
|
||||
)
|
||||
// 计算相差天数
|
||||
return int32(math.Abs(oneZero.Sub(twoZero).Hours() / 24))
|
||||
}
|
||||
Reference in New Issue
Block a user