init: initial commit

This commit is contained in:
Blizzard
2026-02-06 14:44:06 +08:00
commit 3115b58cb2
133 changed files with 25889 additions and 0 deletions
+130
View File
@@ -0,0 +1,130 @@
package auth
import (
"net"
"strings"
"sundynix-go/global"
"sundynix-go/model/system"
systemReq "sundynix-go/model/system/request"
"github.com/gin-gonic/gin"
)
// GetLoginToken 获取登录token
func GetLoginToken(user system.Login) (token string, claims systemReq.CustomClaims, err error) {
j := NewJWT()
claims = j.CreateClaims(systemReq.BaseClaims{
Account: user.GetAccount(),
ID: user.GetUserId(),
})
token, err = j.CreateToken(claims)
return
}
// GetToken 从请求头中获取JWT token,并确保其有效性
//
// 参数:
// - c: *gin.Context, Gin框架的上下文对象,用于获取请求信息和设置响应。
//
// 返回值:
// - string: 获取到的JWT token,如果获取失败则返回空字符串。
func GetToken(c *gin.Context) string {
// 从请求头中获取Authorization字段的值
token := c.Request.Header.Get("Authorization")
prefix := strings.HasPrefix(token, "Bearer ")
if prefix {
token = strings.TrimPrefix(token, "Bearer ")
}
// 返回获取到的token
return token
}
// ClearToken 清除Cookie中的token
func ClearToken(c *gin.Context) {
host, _, err := net.SplitHostPort(c.Request.Host)
if err != nil {
host = c.Request.Host
}
if net.ParseIP(host) != nil {
c.SetCookie("sundynix-token", "", -1, "/", "", false, false)
} else {
c.SetCookie("sundynix-token", "", -1, "/", host, false, false)
}
}
// GetUserInfo 从 gin.Context 中获取用户信息,并返回 CustomClaims 类型的指针。
// 该函数首先尝试从上下文中获取已存在的 claims,如果不存在,则调用 GetClaims 函数获取 claims。
// 如果获取 claims 失败,则返回 nil。
//
// 参数:
// - c: *gin.Context, gin 框架的上下文对象,用于获取请求相关的信息。
//
// 返回值:
// - *systemReq.CustomClaims: 返回用户的自定义 claims 信息,如果获取失败则返回 nil。
func GetUserInfo(c *gin.Context) *systemReq.CustomClaims {
// 尝试从上下文中获取已存在的 claims
if claims, exists := c.Get("claims"); !exists {
// 如果 claims 不存在,则调用 GetClaims 函数获取 claims
if cl, err := GetClaims(c); err != nil {
// 如果获取 claims 失败,返回 nil
return nil
} else {
// 成功获取 claims,返回 claims
return cl
}
} else {
// 如果 claims 存在,将其转换为 CustomClaims 类型并返回
waitUse := claims.(*systemReq.CustomClaims)
return waitUse
}
}
// GetUserId 从 gin.Context 中获取用户 ID,并返回 uint 类型的 ID。
// 该函数首先尝试从上下文中获取已存在的 claims,如果不存在,则调用 GetClaims 函数获取 claims。
// 如果获取 claims 失败,则返回 0。
//
// 参数:
// - c: *gin.Context, gin 框架的上下文对象,用于获取请求相关的信息。
//
// 返回值:
// - uint: 返回用户的 ID,如果获取失败则返回 0。
func GetUserId(c *gin.Context) string {
if claims, exists := c.Get("claims"); !exists {
if cl, err := GetClaims(c); err != nil {
return "0"
} else {
return cl.BaseClaims.ID
}
} else {
waitUse := claims.(*systemReq.CustomClaims)
return waitUse.BaseClaims.ID
}
}
// GetClaims 从 Gin 上下文中提取并解析 JWT 令牌,返回自定义的 claims 信息。
// 该函数首先从请求头中获取 JWT 令牌,然后使用 JWT 解析器解析令牌并返回 claims。
// 如果解析过程中发生错误,函数会记录错误日志并返回错误信息。
//
// 参数:
// - c: *gin.Context, Gin 上下文对象,用于获取请求头中的 JWT 令牌。
//
// 返回值:
// - *systemReq.CustomClaims: 解析后的自定义 claims 信息。
// - error: 解析过程中发生的错误,如果解析成功则为 nil。
func GetClaims(c *gin.Context) (*systemReq.CustomClaims, error) {
// 从 Gin 上下文中获取 JWT 令牌
token := GetToken(c)
// 创建新的 JWT 解析器
j := NewJWT()
// 解析 JWT 令牌并获取 claims 信息
claims, err := j.ParseToken(token)
if err != nil {
// 如果解析失败,记录错误日志
global.Logger.Error("获取用户信息失败,请检查请求头是否存在x-token且claims是否为规定结构")
}
// 返回解析后的 claims 信息和可能的错误
return claims, err
}
+89
View File
@@ -0,0 +1,89 @@
package auth
import (
"errors"
"sundynix-go/global"
"sundynix-go/model/system/request"
"sundynix-go/utils"
"time"
"github.com/golang-jwt/jwt/v5"
)
type JWT struct {
SigningKey []byte
}
var (
TokenValid = errors.New("未知错误")
TokenExpired = errors.New("token已过期")
TokenNotValidYet = errors.New("token尚未激活")
TokenMalformed = errors.New("这不是一个token")
TokenSignatureInvalid = errors.New("无效签名")
TokenInvalid = errors.New("无法处理此token")
)
// NewJWT 初始化JWT
func NewJWT() *JWT {
return &JWT{
SigningKey: []byte("gin-blog-key"),
}
}
// CreateClaims 创建Claims
func (j *JWT) CreateClaims(baseClaims request.BaseClaims) request.CustomClaims {
bf, _ := utils.ParseDuration(global.Config.JWT.BufferTime)
ep, _ := utils.ParseDuration(global.Config.JWT.ExpiresTime)
claims := request.CustomClaims{
BaseClaims: baseClaims,
BufferTime: int64(bf / time.Second), // 缓冲时间1天 缓冲时间内会获得新的token刷新令牌 此时一个用户会存在两个有效令牌 但是前端只留一个 另一个会丢失
RegisteredClaims: jwt.RegisteredClaims{
Audience: jwt.ClaimStrings{"sundynix"},
NotBefore: jwt.NewNumericDate(time.Now().Add(-1000)), // 签名生效时间
ExpiresAt: jwt.NewNumericDate(time.Now().Add(ep)), // 过期时间 7天 配置文件
Issuer: global.Config.JWT.Issuer,
},
}
return claims
}
// CreateToken 创建一个token
func (j *JWT) CreateToken(claims request.CustomClaims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(j.SigningKey)
}
// RefreshToken 刷新token
func (j *JWT) RefreshToken(oldTokenString string, claims request.CustomClaims) (string, error) {
v, err, _ := global.ConcurrencyControl.Do("JWT:"+oldTokenString, func() (interface{}, error) {
return j.CreateToken(claims)
})
return v.(string), err
}
// ParseToken 解析token
func (j *JWT) ParseToken(tokenString string) (*request.CustomClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &request.CustomClaims{}, func(token *jwt.Token) (i interface{}, e error) {
return j.SigningKey, nil
})
if err != nil {
switch {
case errors.Is(err, jwt.ErrTokenExpired):
return nil, TokenExpired
case errors.Is(err, jwt.ErrTokenNotValidYet):
return nil, TokenNotValidYet
case errors.Is(err, jwt.ErrTokenMalformed):
return nil, TokenMalformed
case errors.Is(err, jwt.ErrTokenSignatureInvalid):
return nil, TokenSignatureInvalid
default:
return nil, TokenInvalid
}
}
if token != nil {
if claims, ok := token.Claims.(*request.CustomClaims); ok && token.Valid {
return claims, nil
}
}
return nil, TokenInvalid
}
+5
View File
@@ -0,0 +1,5 @@
package captcha
import "github.com/mojocn/base64Captcha"
var CaptchaStore = base64Captcha.DefaultMemStore
+20
View File
@@ -0,0 +1,20 @@
package utils
import (
"errors"
"os"
)
func PathExist(path string) (bool, error) {
stat, err := os.Stat(path)
if err == nil {
if stat.IsDir() {
return true, nil
}
return false, errors.New("存在同名文件")
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
+32
View File
@@ -0,0 +1,32 @@
package utils
import (
"crypto/md5"
"encoding/hex"
"golang.org/x/crypto/bcrypt"
)
// BcryptHash 使用 bcrypt 对密码进行加密
func BcryptHash(password string) string {
bytes, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes)
}
// BcryptCheck 对比明文密码和数据库的哈希值
func BcryptCheck(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: MD5V
//@description: md5加密
//@param: str []byte
//@return: string
func MD5V(str []byte, b ...byte) string {
h := md5.New()
h.Write(str)
return hex.EncodeToString(h.Sum(b))
}
+111
View File
@@ -0,0 +1,111 @@
package utils
import (
"fmt"
"log"
"sundynix-go/utils/location"
"sundynix-go/utils/timer"
"sundynix-go/utils/uniqueid"
"testing"
"time"
)
func TestHashPwd(t *testing.T) {
hash := BcryptHash("sundynix")
fmt.Println(hash) // $2a$10$QC/zkQ/ohPmvjF/goDyicu7cHgAEj8gHg6OTDHWhbYQMHHn4dwxX2
//check := BcryptCheck("sundynix", "$2a$10$QC/zkQ/ohPmvjF/goDyicu7cHgAEj8gHg6OTDHWhbYQMHHn4dwxX2")
//fmt.Println(check)
}
func TestUuid(t *testing.T) {
id := uniqueid.GenerateId()
fmt.Println(id)
}
func TestTimePeriod(t *testing.T) {
str := "2025-11-19 10:29:20.597"
parse, _ := time.ParseInLocation("2006-01-02 15:04:05.999999999", str, time.Local)
interval := timer.TimeInterval(parse)
fmt.Println(interval)
}
func TestPoint2Code(t *testing.T) {
//latitude := float32(39.90882)
//longitude := float32(116.39748)
longitude := float32(102.74837)
latitude := float32(25.02847)
// 调用逆地理编码
entity, err := location.Point2code(longitude, latitude)
if err != nil {
log.Fatalf("获取地址失败: %v", err)
}
// 打印结果
// 打印结果
fmt.Println("=== 解析结果 ===")
fmt.Println("格式化地址:", entity.Address)
fmt.Println("省份:", entity.AddressComponent.Province.String())
fmt.Println("城市:", entity.AddressComponent.City.String())
fmt.Println("区县:", entity.AddressComponent.District.String())
fmt.Println("乡镇:", entity.AddressComponent.Township)
fmt.Println("街道:", entity.AddressComponent.Street)
fmt.Println("门牌号:", entity.AddressComponent.StreetNumber.Number)
fmt.Println("门牌号所属街道:", entity.AddressComponent.StreetNumber.Street)
fmt.Println("行政区划编码:", entity.AddressComponent.Adcode)
fmt.Println("国家:", entity.AddressComponent.Country)
}
func TestWeather(t *testing.T) {
adcode := "532325"
// 可选:extensions="all" 查询实时+未来3天预报
weatherResp, err := location.GetWeather(adcode, "base")
if err != nil {
log.Fatalf("查询天气失败: %v", err)
}
// 打印实时天气
fmt.Println("=== 实时天气 ===")
live := weatherResp.Lives[0] // 实时天气数组仅1条数据
fmt.Printf("省份:%s\n", live.Province)
fmt.Printf("城市:%s\n", live.City)
fmt.Printf("行政区划编码:%s\n", live.Adcode)
fmt.Printf("天气:%s\n", live.Weather)
fmt.Printf("实时气温:%s℃\n", live.Temperature)
fmt.Printf("风向:%s\n", live.WindDirection)
fmt.Printf("风力:%s级\n", live.WindPower)
fmt.Printf("湿度:%s%%\n", live.Humidity)
fmt.Printf("数据更新时间:%s\n", live.ReportTime)
// 若查询的是all(实时+预报),打印预报数据
// if len(weatherResp.Forecasts) > 0 {
// fmt.Println("\n=== 未来3天预报 ===")
// forecast := weatherResp.Forecasts[0]
// for _, day := range forecast.Casts {
// fmt.Printf("\n日期:%s(星期%s\n", day.Date, day.Week)
// fmt.Printf("白天天气:%s,温度:%s℃\n", day.DayWeather, day.DayTemp)
// fmt.Printf("夜间天气:%s,温度:%s℃\n", day.NightWeather, day.NightTemp)
// fmt.Printf("白天风向/风力:%s/%s级\n", day.DayWindDir, day.DayWindPower)
// }
// }
}
func TestGenOrderNo(t *testing.T) {
no := uniqueid.GenOrderNo()
fmt.Println(no)
}
func TestTime(t *testing.T) {
milliTimestamp := time.Now().UnixMilli()
microTimestamp := time.Now().UnixMicro()
fmt.Printf("毫秒级时间戳: %d\n", milliTimestamp)
fmt.Printf("微秒级时间戳: %d\n", microTimestamp)
}
func TestGetZeroTime(t *testing.T) {
zeroTime := timer.GetZeroTime()
fmt.Printf("当天零点: %v\n", zeroTime)
fmt.Printf("时间戳(秒): %v\n", zeroTime.Unix())
}
+30
View File
@@ -0,0 +1,30 @@
package utils
import (
"strconv"
"strings"
"time"
)
// ParseDuration 解析时间
func ParseDuration(d string) (time.Duration, error) {
d = strings.TrimSpace(d)
dr, err := time.ParseDuration(d)
if err == nil {
return dr, nil
}
if strings.Contains(d, "d") {
index := strings.Index(d, "d")
hour, _ := strconv.Atoi(d[:index])
dr = time.Hour * 24 * time.Duration(hour)
ndr, err := time.ParseDuration(d[index+1:])
if err != nil {
return dr, nil
}
return dr + ndr, nil
}
dv, err := strconv.ParseInt(d, 10, 64)
return time.Duration(dv), err
}
+168
View File
@@ -0,0 +1,168 @@
package location
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"time"
)
// StringOrArray ########################### 核心:兼容字符串/数组的自定义类型 ###########################
type StringOrArray string
// UnmarshalJSON 处理字符串/数组两种格式
func (s *StringOrArray) UnmarshalJSON(data []byte) error {
// 尝试解析为纯字符串
var str string
if err := json.Unmarshal(data, &str); err == nil {
*s = StringOrArray(str)
return nil
}
// 尝试解析为字符串数组
var strArr []string
if err := json.Unmarshal(data, &strArr); err != nil {
return fmt.Errorf("字段解析失败: %v, 原始数据: %s", err, string(data))
}
// 数组取第一个元素(高德返回的数组通常仅1个元素)
if len(strArr) > 0 {
*s = StringOrArray(strArr[0])
} else {
*s = StringOrArray("")
}
return nil
}
// String 转为普通字符串,方便使用
func (s StringOrArray) String() string {
return string(s)
}
// ########################### 严格对齐高德API的结构体 ###########################
// AMapEntity 最终返回的实体(对应Java的AMapEntity
type AMapEntity struct {
AddressComponent AddressComponent `json:"addresscomponent"`
Address StringOrArray `json:"formatted_address"` // 改为兼容类型
}
// StreetNumber 门牌号嵌套对象(高德固定返回对象)
type StreetNumber struct {
Number StringOrArray `json:"number"` // 门牌号
Location StringOrArray `json:"location"` // 经纬度
Direction StringOrArray `json:"direction"` // 方向
Distance StringOrArray `json:"distance"` // 距离
Street StringOrArray `json:"street"` // 所属街道
}
// AddressComponent 地址组件(全量兼容)
type AddressComponent struct {
Province StringOrArray `json:"province"` // 省(字符串/数组)
City StringOrArray `json:"city"` // 市(字符串/数组)
District StringOrArray `json:"district"` // 区(字符串/数组)
Township StringOrArray `json:"township"` // 乡镇(纯字符串)
Street StringOrArray `json:"street"` // 街道(纯字符串)
StreetNumber StreetNumber `json:"streetnumber"` // 门牌号(对象)
Adcode StringOrArray `json:"adcode"` // 行政区划编码(纯字符串)
Country StringOrArray `json:"country"` // 国家(纯字符串)
CountryCode StringOrArray `json:"countrycode"` // 国家编码(纯字符串)
}
// AMapResponse 高德API顶层响应
type AMapResponse struct {
Regeocode Regeocode `json:"regeocode"` // 核心数据
Status string `json:"status"` // 1=成功,0=失败
Info string `json:"info"` // 错误信息
Infocode string `json:"infocode"` // 错误码(精准定位问题)
}
// Regeocode 逆地理编码核心数据
type Regeocode struct {
FormattedAddress StringOrArray `json:"formatted_address"` // 改为兼容类型
AddressComponent AddressComponent `json:"addresscomponent"` // 地址组件
}
// ########################### 配置常量 ###########################
const (
appKey = "1b8dd8848bcc062ff1f2cec6db683673" // 替换为你的有效Key
amapApiUrl = "https://restapi.amap.com/v3/geocode/regeo"
)
// Point2code 经纬度转地址(完整错误处理+兼容)
func Point2code(longitude, latitude float32) (*AMapEntity, error) {
// 1. 基础参数校验
if longitude == 0 || latitude == 0 {
return nil, errors.New("经纬度不能为0")
}
// 2. 构建请求URL
baseURL, err := url.Parse(amapApiUrl)
if err != nil {
log.Printf("URL解析失败: %v", err)
return nil, err
}
// 设置查询参数
params := url.Values{}
params.Set("key", appKey)
params.Set("location", fmt.Sprintf("%f,%f", longitude, latitude))
baseURL.RawQuery = params.Encode()
// 3. 全局HTTP客户端(建议单例复用)
client := &http.Client{Timeout: 10 * time.Second}
// 4. 发送GET请求
req, err := http.NewRequest(http.MethodGet, baseURL.String(), nil)
if err != nil {
log.Printf("请求构建失败: %v", err)
return nil, err
}
resp, err := client.Do(req)
if err != nil {
log.Printf("请求发送失败: %v", err)
return nil, err
}
defer resp.Body.Close() // 强制关闭响应体,避免内存泄漏
// 5. 校验HTTP状态码
if resp.StatusCode != http.StatusOK {
errMsg := fmt.Sprintf("HTTP请求失败,状态码: %d", resp.StatusCode)
log.Println(errMsg)
return nil, errors.New(errMsg)
}
// 6. 读取响应体
bodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Printf("响应体读取失败: %v", err)
return nil, err
}
// 7. 解析JSON响应
var amapResp AMapResponse
if err := json.Unmarshal(bodyBytes, &amapResp); err != nil {
log.Printf("JSON解析失败: %v, 响应内容: %s", err, string(bodyBytes))
return nil, err
}
// 8. 校验高德API业务状态
if amapResp.Status != "1" {
errMsg := fmt.Sprintf("高德API返回失败: status=%s, info=%s, infocode=%s",
amapResp.Status, amapResp.Info, amapResp.Infocode)
log.Println(errMsg)
return nil, errors.New(errMsg)
}
// 9. 构造最终返回实体
result := &AMapEntity{
AddressComponent: amapResp.Regeocode.AddressComponent,
Address: amapResp.Regeocode.FormattedAddress,
}
return result, nil
}
+186
View File
@@ -0,0 +1,186 @@
package location
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"time"
)
// ########################### 1. 结构体定义(对齐高德天气API返回格式) ###########################
// WeatherResponse 高德天气API顶层响应结构体
type WeatherResponse struct {
Status string `json:"status"` // 1=成功,0=失败
Info string `json:"info"` // 错误信息
Infocode string `json:"infocode"` // 错误码
Lives []LiveWeather `json:"lives"` // 实时天气(数组,仅1条数据)
Forecasts []Forecast `json:"forecasts"` // 天气预报(数组,仅1条数据)
}
// LiveWeather 实时天气结构体
type LiveWeather struct {
Province string `json:"province"` // 省份
City string `json:"city"` // 城市
Adcode string `json:"adcode"` // 行政区划编码
Weather string `json:"weather"` // 天气现象(如晴、阴)
Temperature string `json:"temperature"` // 实时气温(℃)
WindDirection string `json:"winddirection"` // 风向(如东、西南)
WindPower string `json:"windpower"` // 风力(如3级)
Humidity string `json:"humidity"` // 湿度(%
ReportTime string `json:"reporttime"` // 数据更新时间
}
// Forecast 天气预报顶层结构体(包含多日预报)
type Forecast struct {
Province string `json:"province"` // 省份
City string `json:"city"` // 城市
Adcode string `json:"adcode"` // 行政区划编码
ReportTime string `json:"reporttime"` // 预报发布时间
Casts []ForecastDay `json:"casts"` // 每日预报(未来3天)
}
// ForecastDay 单日天气预报
type ForecastDay struct {
Date string `json:"date"` // 日期(yyyy-MM-dd
Week string `json:"week"` // 星期(1=周一,7=周日)
DayWeather string `json:"dayweather"` // 白天天气
NightWeather string `json:"nightweather"` // 夜间天气
DayTemp string `json:"daytemp"` // 白天温度
NightTemp string `json:"nighttemp"` // 夜间温度
DayWindDir string `json:"daywinddir"` // 白天风向
NightWindDir string `json:"nightwinddir"` // 夜间风向
DayWindPower string `json:"daywindpower"` // 白天风力
NightWindPower string `json:"nightwindpower"` // 夜间风力
}
// ########################### 2. 配置常量 ###########################
const (
amapWeatherApi = "https://restapi.amap.com/v3/weather/weatherInfo"
)
// GetWeather 根据行政区划编码查询天气
// adcode: 行政区划编码(如110101=北京市东城区)
// extensions: 查询类型(base=实时,all=实时+预报)
func GetWeather(adcode string, extensions string) (*WeatherResponse, error) {
// 1. 参数校验
if adcode == "" {
return nil, errors.New("行政区划编码adcode不能为空")
}
if extensions == "" {
extensions = "base" // 默认查实时天气
}
if extensions != "base" && extensions != "all" {
return nil, errors.New("extensions仅支持base(实时)或all(实时+预报)")
}
// 2. 构建请求URL和参数
baseURL, err := url.Parse(amapWeatherApi)
if err != nil {
log.Printf("解析基础URL失败: %v", err)
return nil, err
}
// 设置查询参数
params := url.Values{}
params.Set("key", appKey)
params.Set("city", adcode) // 核心参数:行政区划编码
params.Set("extensions", extensions)
params.Set("output", "json") // 固定返回JSON格式
baseURL.RawQuery = params.Encode()
// 3. 发送HTTP请求(全局客户端,复用连接)
client := &http.Client{
Timeout: 10 * time.Second, // 10秒超时
}
req, err := http.NewRequest(http.MethodGet, baseURL.String(), nil)
if err != nil {
log.Printf("构建请求失败: %v", err)
return nil, err
}
resp, err := client.Do(req)
if err != nil {
log.Printf("发送请求失败: %v", err)
return nil, err
}
defer resp.Body.Close() // 强制关闭响应体
// 4. 检查HTTP状态码
if resp.StatusCode != http.StatusOK {
errMsg := fmt.Sprintf("请求失败,状态码: %d", resp.StatusCode)
log.Println(errMsg)
return nil, errors.New(errMsg)
}
// 5. 读取响应体
bodyBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Printf("读取响应体失败: %v", err)
return nil, err
}
// 6. 解析JSON响应
var weatherResp WeatherResponse
if err := json.Unmarshal(bodyBytes, &weatherResp); err != nil {
log.Printf("解析JSON失败: %v, 响应内容: %s", err, string(bodyBytes))
return nil, err
}
// 7. 检查API业务状态
if weatherResp.Status != "1" {
errMsg := fmt.Sprintf("高德天气API返回失败: status=%s, info=%s, infocode=%s",
weatherResp.Status, weatherResp.Info, weatherResp.Infocode)
log.Println(errMsg)
return nil, errors.New(errMsg)
}
// 8. 校验返回数据非空
if extensions == "base" && len(weatherResp.Lives) == 0 {
return nil, errors.New("未查询到实时天气数据")
}
if extensions == "all" && (len(weatherResp.Lives) == 0 || len(weatherResp.Forecasts) == 0) {
return nil, errors.New("未查询到天气数据(实时/预报)")
}
return &weatherResp, nil
}
// ########################### 4. 测试示例 ###########################
func main() {
// 示例1:查询北京市东城区(adcode=110101)实时天气
adcode := "110101"
// 可选:extensions="all" 查询实时+未来3天预报
weatherResp, err := GetWeather(adcode, "base")
if err != nil {
log.Fatalf("查询天气失败: %v", err)
}
// 打印实时天气
fmt.Println("=== 实时天气 ===")
live := weatherResp.Lives[0] // 实时天气数组仅1条数据
fmt.Printf("省份:%s\n", live.Province)
fmt.Printf("城市:%s\n", live.City)
fmt.Printf("行政区划编码:%s\n", live.Adcode)
fmt.Printf("天气:%s\n", live.Weather)
fmt.Printf("实时气温:%s℃\n", live.Temperature)
fmt.Printf("风向:%s\n", live.WindDirection)
fmt.Printf("风力:%s级\n", live.WindPower)
fmt.Printf("湿度:%s%%\n", live.Humidity)
fmt.Printf("数据更新时间:%s\n", live.ReportTime)
// 若查询的是all(实时+预报),打印预报数据
// if len(weatherResp.Forecasts) > 0 {
// fmt.Println("\n=== 未来3天预报 ===")
// forecast := weatherResp.Forecasts[0]
// for _, day := range forecast.Casts {
// fmt.Printf("\n日期:%s(星期%s\n", day.Date, day.Week)
// fmt.Printf("白天天气:%s,温度:%s℃\n", day.DayWeather, day.DayTemp)
// fmt.Printf("夜间天气:%s,温度:%s℃\n", day.NightWeather, day.NightTemp)
// fmt.Printf("白天风向/风力:%s/%s级\n", day.DayWindDir, day.DayWindPower)
// }
// }
}
+46
View File
@@ -0,0 +1,46 @@
package timer
import (
"fmt"
"time"
)
// TimeInterval 计算两个时间的间隔,超过24小时返回天数,否则返回几小时前
func TimeInterval(targetTime time.Time) string {
// 1. 将当前时间和目标时间统一转换为本地时区(time.Local)
now := time.Now().In(time.Local) // 当前时间转为本地时区
target := targetTime.In(time.Local) // 目标时间转为本地时区
// 2. 计算时间差(取绝对值,避免因目标时间在未来导致负数)
var diff time.Duration
if now.After(target) {
diff = now.Sub(target)
} else {
diff = target.Sub(now)
}
// 3. 转换为总小时数(取整数部分,自动截断小数)
// 转换为总分钟数(取整数部分,自动截断秒数)
totalMinutes := int(diff.Minutes())
// 按不同阈值返回对应格式
switch {
case totalMinutes >= 24*60: // 24小时 = 1440分钟
days := totalMinutes / (24 * 60)
return fmt.Sprintf("%d天前", days)
case totalMinutes >= 60: // 1小时 = 60分钟
hours := totalMinutes / 60
return fmt.Sprintf("%d小时前", hours)
default: // 不足1小时
return fmt.Sprintf("%d分钟前", totalMinutes)
}
}
// GetZeroTime 获取当天零点时间
func GetZeroTime() time.Time {
now := time.Now()
// 2. 使用当天的年月日,将时分秒纳秒设为0,并保留原时区(Location)
zeroTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
return zeroTime
}
+219
View File
@@ -0,0 +1,219 @@
package timer
import (
"sync"
"github.com/robfig/cron/v3"
)
type Timer interface {
FindCronList() map[string]*taskManager
AddTaskByFuncWithSecond(cronName string, spec string, fun func(), taskName string, options ...cron.Option) (cron.EntryID, error) // 添加Task Func以秒的形式加入
AddTaskByJobWithSecond(cronName string, spec string, job interface{ Run() }, taskName string, options ...cron.Option) (cron.EntryID, error) // 添加Task Job以秒的形式加入
AddTaskByFunc(cronName string, spec string, task func(), taskName string, options ...cron.Option) (cron.EntryID, error) // 添加Task Func加入
AddTaskByJob(cronName string, spec string, job interface{ Run() }, taskName string, options ...cron.Option) (cron.EntryID, error) // 添加Task Job加入
FindCron(cronName string) (*taskManager, bool) //获取对应taskName的cron 可能会为空
StartCron(cronName string) // 启动对应cron
StopCron(cronName string) // 停止对应cron
FindTask(cronName string, taskName string) (*task, bool) //获取对应taskName的task
RemoveTask(cronName string, id int) //删除对应taskName的task
RemoveTaskByName(cronName string, taskName string) //删除对应taskName的task
Clear(cronName string) //清空对应cronName的task
Close() // 关闭所有定时任务
}
type task struct {
EntryId cron.EntryID
Spec string
TaskName string
}
type taskManager struct {
corn *cron.Cron
tasks map[cron.EntryID]*task
}
// timer 定时任务管理
type timer struct {
cronList map[string]*taskManager
sync.Mutex
}
// AddTaskByFuncWithSecond 通过函数的方法使用WithSeconds添加任务
func (t *timer) AddTaskByFuncWithSecond(cronName string, spec string, fun func(), taskName string, option ...cron.Option) (cron.EntryID, error) {
t.Lock()
defer t.Unlock()
option = append(option, cron.WithSeconds())
if _, ok := t.cronList[cronName]; !ok {
tasks := make(map[cron.EntryID]*task)
t.cronList[cronName] = &taskManager{
corn: cron.New(option...),
tasks: tasks,
}
}
id, err := t.cronList[cronName].corn.AddFunc(spec, fun)
t.cronList[cronName].corn.Start()
t.cronList[cronName].tasks[id] = &task{
EntryId: id,
Spec: spec,
TaskName: taskName,
}
return id, err
}
// AddTaskByFunc 通过函数的方法添加任务
func (t *timer) AddTaskByFunc(cronName string, spec string, fun func(), taskName string, option ...cron.Option) (cron.EntryID, error) {
t.Lock()
defer t.Unlock()
if _, ok := t.cronList[cronName]; !ok {
tasks := make(map[cron.EntryID]*task)
t.cronList[cronName] = &taskManager{
corn: cron.New(option...),
tasks: tasks,
}
}
id, err := t.cronList[cronName].corn.AddFunc(spec, fun)
t.cronList[cronName].corn.Start()
t.cronList[cronName].tasks[id] = &task{
EntryId: id,
Spec: spec,
TaskName: taskName,
}
return id, err
}
// AddTaskByJobWithSecond 通过Job的方法使用WithSeconds添加任务
func (t *timer) AddTaskByJobWithSecond(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error) {
t.Lock()
defer t.Unlock()
option = append(option, cron.WithSeconds())
if _, ok := t.cronList[cronName]; !ok {
tasks := make(map[cron.EntryID]*task)
t.cronList[cronName] = &taskManager{
corn: cron.New(option...),
tasks: tasks,
}
}
id, err := t.cronList[cronName].corn.AddJob(spec, job)
t.cronList[cronName].corn.Start()
t.cronList[cronName].tasks[id] = &task{
EntryId: id,
Spec: spec,
TaskName: taskName,
}
return id, err
}
// AddTaskByJob 通过Job的方法添加任务
func (t *timer) AddTaskByJob(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error) {
t.Lock()
defer t.Unlock()
if _, ok := t.cronList[cronName]; !ok {
tasks := make(map[cron.EntryID]*task)
t.cronList[cronName] = &taskManager{
corn: cron.New(option...),
tasks: tasks,
}
}
id, err := t.cronList[cronName].corn.AddJob(spec, job)
t.cronList[cronName].corn.Start()
t.cronList[cronName].tasks[id] = &task{
EntryId: id,
Spec: spec,
TaskName: taskName,
}
return id, err
}
// FindCron 获取对应cronName的cron 可能会为空
func (t *timer) FindCron(cronName string) (*taskManager, bool) {
t.Lock()
defer t.Unlock()
v, ok := t.cronList[cronName]
return v, ok
}
// FindTask 获取对应taskName的task
func (t *timer) FindTask(cronName string, taskName string) (*task, bool) {
t.Lock()
defer t.Unlock()
v, ok := t.cronList[cronName]
if !ok {
return nil, false
}
for _, t2 := range v.tasks {
if t2.TaskName == taskName {
return t2, true
}
}
return nil, false
}
// FindCronList 获取所有cron
func (t *timer) FindCronList() map[string]*taskManager {
t.Lock()
defer t.Unlock()
return t.cronList
}
// StartCron 启动对应cron
func (t *timer) StartCron(cronName string) {
t.Lock()
defer t.Unlock()
if v, ok := t.cronList[cronName]; ok {
v.corn.Start()
}
}
// StopCron 停止对应cron
func (t *timer) StopCron(cronName string) {
t.Lock()
defer t.Unlock()
if v, ok := t.cronList[cronName]; ok {
v.corn.Stop()
}
}
// RemoveTask 从cronName 删除指定任务
func (t *timer) RemoveTask(cronName string, id int) {
t.Lock()
defer t.Unlock()
if v, ok := t.cronList[cronName]; ok {
v.corn.Remove(cron.EntryID(id))
delete(v.tasks, cron.EntryID(id))
}
}
// RemoveTaskByName 从cronName 删除指定任务
func (t *timer) RemoveTaskByName(cronName string, taskName string) {
fTask, ok := t.FindTask(cronName, taskName)
if !ok {
return
}
t.RemoveTask(cronName, int(fTask.EntryId))
}
// Clear 清空对应cronName的task
func (t *timer) Clear(cronName string) {
t.Lock()
defer t.Unlock()
if v, ok := t.cronList[cronName]; ok {
v.corn.Stop()
delete(t.cronList, cronName)
}
}
// Close 关闭所有定时任务 释放资源
func (t *timer) Close() {
t.Lock()
defer t.Unlock()
for _, v := range t.cronList {
v.corn.Stop()
}
}
// NewTimerTask 创建定时任务
func NewTimerTask() Timer {
return &timer{
cronList: make(map[string]*taskManager),
}
}
+41
View File
@@ -0,0 +1,41 @@
package uniqueid
import (
"crypto/rand"
"fmt"
"math/big"
"strings"
"github.com/google/uuid"
)
func GenerateId() string {
uuidV1, err := uuid.NewUUID()
if err != nil {
panic(err)
}
return uuidV1.String()
}
func GenerateName() string {
str := uuid.New().String()
//生成一个用户名 比如花友u278bb 中文后的字符随机生成 不可重复 取str的前六位
return "花友" + str[6:12]
}
// GenerateRandomCode 生成邀请码
func GenerateRandomCode(length int) string {
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
var sb strings.Builder
for i := 0; i < length; i++ {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
sb.WriteByte(charset[n.Int64()])
}
return sb.String()
}
// GenCodeKey 生成邀请码的key
func GenCodeKey(userId string) string {
return fmt.Sprintf("code:%s", userId)
}
+32
View File
@@ -0,0 +1,32 @@
package uniqueid
import (
"fmt"
"time"
)
func GenOrderNo() string {
/*
支付宝订单号示例:1217752501201407033233368028
分析可能格式:
- 前14位:时间戳(年月日时分秒)20250112 151420
- 中间6位:商户/业务标识
- 最后8位:随机数或序列号
*/
now := time.Now()
// 时间部分:年月日时分秒
timePart := now.Format("20060102150405") // 14位
// 业务标识:机器ID + 进程ID + 随机数
machineID := 1
pid := now.Nanosecond() % 1000
businessPart := fmt.Sprintf("%02d%03d%01d", machineID, pid, now.Second()%10) // 6位
// 随机部分:纳秒取模
random1 := fmt.Sprintf("%04d", now.Nanosecond()%10000)
random2 := fmt.Sprintf("%04d", (now.Nanosecond()/10000)%10000)
return timePart + businessPart + random1 + random2
}
+106
View File
@@ -0,0 +1,106 @@
package upload
import (
"bytes"
"context"
"errors"
"io"
"mime/multipart"
"path/filepath"
"strconv"
"strings"
"sundynix-go/global"
"sundynix-go/utils"
"time"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"go.uber.org/zap"
)
var MinioClient *Minio // 优化性能,但是不支持动态配置
type Minio struct {
Client *minio.Client
bucket string
}
func GetMinio(endpoint, accessKey, secretKey, bucketName string, useSSL bool) (*Minio, error) {
if MinioClient != nil {
return MinioClient, nil
}
// Initialize minio client object.
minioClient, err := minio.New(endpoint, &minio.Options{
Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
Secure: useSSL, // Set to true if using https
})
if err != nil {
return nil, err
}
// 创建bucket
err = minioClient.MakeBucket(context.Background(), bucketName, minio.MakeBucketOptions{})
if err != nil {
// 判断是否已经存在
exists, errBucketExists := minioClient.BucketExists(context.Background(), bucketName)
if errBucketExists == nil && exists {
global.Logger.Info("Bucket already exists")
} else {
return nil, err
}
}
MinioClient = &Minio{
Client: minioClient,
bucket: bucketName,
}
return MinioClient, nil
}
func (m *Minio) UploadFile(file *multipart.FileHeader) (filePathres, key string, uploadErr error) {
f, openErr := file.Open()
// mutipart.File to os.File
if openErr != nil {
global.Logger.Error("function file.Open() Failed", zap.Any("err", openErr.Error()))
return "", "", errors.New("function file.Open() Failed, err:" + openErr.Error())
}
buffer := bytes.Buffer{}
_, err := io.Copy(&buffer, f)
if err != nil {
global.Logger.Error("读取文件失败", zap.Any("err", err.Error()))
return "", "", errors.New("读取文件失败, err:" + err.Error())
}
f.Close() // 创建文件 defer 关闭
//对文件名进行加密存储
ext := filepath.Ext(file.Filename)
filename := utils.MD5V([]byte(strings.TrimSuffix(file.Filename, ext))) + ext
timestamp := time.Now().UnixMicro()
timestr := strconv.FormatInt(timestamp, 10)
if global.Config.Minio.BasePath == "" {
filePathres = "uploads/" + time.Now().Format("2006-01-02") + "/" + timestr + "-" + filename // uploads/2025-09-17/xxxx.png
} else {
filePathres = global.Config.Minio.BasePath + "/" + time.Now().Format("2006-01-02") + "/" + timestr + "-" + filename
}
// 设置超时10分钟
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*10)
defer cancel()
//大文件自动切换为分片上传
info, err := m.Client.PutObject(ctx, global.Config.Minio.BucketName, filePathres, &buffer, file.Size, minio.PutObjectOptions{
ContentType: "application/octet-stream",
})
if err != nil {
global.Logger.Error("上传文件到minio失败", zap.Any("err", err.Error()))
return "", "", errors.New("上传文件到minio失败, err:" + err.Error())
}
//http://127.0.0.1:9000/planting-fun/uploads/2025-09-17/211476f3837fc7acbaebf0f901c1bd68.png
return global.Config.Minio.BucketUrl + "/" + info.Key, filePathres, nil
}
func (m *Minio) DeleteFile(key string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
err := m.Client.RemoveObject(ctx, m.bucket, key, minio.RemoveObjectOptions{})
return err
}
+31
View File
@@ -0,0 +1,31 @@
package upload
import (
"fmt"
"mime/multipart"
"sundynix-go/global"
)
// Oss 对象存储接口
type Oss interface {
UploadFile(file *multipart.FileHeader) (string, string, error)
DeleteFile(key string) error
}
// OssInstance 实例化oos方法
func OssInstance() Oss {
switch global.Config.System.OssType {
case "local":
fmt.Println("local")
case "tencent-cos":
return &TencentCOS{}
case "minio":
minioClient, err := GetMinio(global.Config.Minio.Endpoint, global.Config.Minio.AccessKeyId, global.Config.Minio.AccessKeySecret, global.Config.Minio.BucketName, global.Config.Minio.UseSSL)
if err != nil {
global.Logger.Warn("minio初始化失败,请检查minio可用性或安全配置:" + err.Error())
panic("minio初始化失败,请检查minio可用性或安全配置")
}
return minioClient
}
return nil
}
+60
View File
@@ -0,0 +1,60 @@
package upload
import (
"context"
"errors"
"fmt"
"mime/multipart"
"net/http"
"net/url"
"sundynix-go/global"
"time"
"github.com/tencentyun/cos-go-sdk-v5"
"go.uber.org/zap"
)
type TencentCOS struct{}
// NewClient 创建一个腾讯云COS客户端
func NewClient() *cos.Client {
urlStr, _ := url.Parse("https://" + global.Config.TencentCOS.Bucket + ".cos." + global.Config.TencentCOS.Region + ".myqcloud.com")
baseURL := &cos.BaseURL{BucketURL: urlStr}
client := cos.NewClient(baseURL, &http.Client{
Transport: &cos.AuthorizationTransport{
SecretID: global.Config.TencentCOS.SecretID,
SecretKey: global.Config.TencentCOS.SecretKey,
},
})
return client
}
// UploadFile upload file to COS
func (*TencentCOS) UploadFile(file *multipart.FileHeader) (string, string, error) {
client := NewClient()
f, openError := file.Open()
if openError != nil {
global.Logger.Error("function file.Open() failed", zap.Any("err", openError.Error()))
return "", "", errors.New("function file.Open() failed, err:" + openError.Error())
}
defer f.Close() // 创建文件 defer 关闭
fileKey := fmt.Sprintf("%d%s", time.Now().Unix(), file.Filename)
_, err := client.Object.Put(context.Background(), global.Config.TencentCOS.PathPrefix+"/"+fileKey, f, nil)
if err != nil {
panic(err)
}
return global.Config.TencentCOS.BaseURL + "/" + global.Config.TencentCOS.PathPrefix + "/" + fileKey, fileKey, nil
}
// DeleteFile delete file form COS
func (*TencentCOS) DeleteFile(key string) error {
client := NewClient()
name := global.Config.TencentCOS.PathPrefix + "/" + key
_, err := client.Object.Delete(context.Background(), name)
if err != nil {
global.Logger.Error("function bucketManager.Delete() failed", zap.Any("err", err.Error()))
return errors.New("function bucketManager.Delete() failed, err:" + err.Error())
}
return nil
}
+294
View File
@@ -0,0 +1,294 @@
package utils
import (
"errors"
"reflect"
"regexp"
"strconv"
"strings"
)
type Rules map[string][]string
type RulesMap map[string]Rules
var CustomizeMap = make(map[string]Rules)
//@author: [piexlmax](https://github.com/piexlmax)
//@function: RegisterRule
//@description: 注册自定义规则方案建议在路由初始化层即注册
//@param: key string, rule Rules
//@return: err error
func RegisterRule(key string, rule Rules) (err error) {
if CustomizeMap[key] != nil {
return errors.New(key + "已注册,无法重复注册")
} else {
CustomizeMap[key] = rule
return nil
}
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: NotEmpty
//@description: 非空 不能为其对应类型的0值
//@return: string
func NotEmpty() string {
return "notEmpty"
}
// @author: [zooqkl](https://github.com/zooqkl)
// @function: RegexpMatch
// @description: 正则校验 校验输入项是否满足正则表达式
// @param: rule string
// @return: string
func RegexpMatch(rule string) string {
return "regexp=" + rule
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: Lt
//@description: 小于入参(<) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
//@param: mark string
//@return: string
func Lt(mark string) string {
return "lt=" + mark
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: Le
//@description: 小于等于入参(<=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
//@param: mark string
//@return: string
func Le(mark string) string {
return "le=" + mark
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: Eq
//@description: 等于入参(==) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
//@param: mark string
//@return: string
func Eq(mark string) string {
return "eq=" + mark
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: Ne
//@description: 不等于入参(!=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
//@param: mark string
//@return: string
func Ne(mark string) string {
return "ne=" + mark
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: Ge
//@description: 大于等于入参(>=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
//@param: mark string
//@return: string
func Ge(mark string) string {
return "ge=" + mark
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: Gt
//@description: 大于入参(>) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较
//@param: mark string
//@return: string
func Gt(mark string) string {
return "gt=" + mark
}
//
//@author: [piexlmax](https://github.com/piexlmax)
//@function: Verify
//@description: 校验方法
//@param: st interface{}, roleMap Rules(入参实例,规则map)
//@return: err error
func Verify(st interface{}, roleMap Rules) (err error) {
compareMap := map[string]bool{
"lt": true,
"le": true,
"eq": true,
"ne": true,
"ge": true,
"gt": true,
}
typ := reflect.TypeOf(st)
val := reflect.ValueOf(st) // 获取reflect.Type类型
kd := val.Kind() // 获取到st对应的类别
if kd != reflect.Struct {
return errors.New("expect struct")
}
num := val.NumField()
// 遍历结构体的所有字段
for i := 0; i < num; i++ {
tagVal := typ.Field(i)
val := val.Field(i)
if tagVal.Type.Kind() == reflect.Struct {
if err = Verify(val.Interface(), roleMap); err != nil {
return err
}
}
if len(roleMap[tagVal.Name]) > 0 {
for _, v := range roleMap[tagVal.Name] {
switch {
case v == "notEmpty":
if isBlank(val) {
return errors.New(tagVal.Name + "值不能为空")
}
case strings.Split(v, "=")[0] == "regexp":
if !regexpMatch(strings.Split(v, "=")[1], val.String()) {
return errors.New(tagVal.Name + "格式校验不通过")
}
case compareMap[strings.Split(v, "=")[0]]:
if !compareVerify(val, v) {
return errors.New(tagVal.Name + "长度或值不在合法范围," + v)
}
}
}
}
}
return nil
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: compareVerify
//@description: 长度和数字的校验方法 根据类型自动校验
//@param: value reflect.Value, VerifyStr string
//@return: bool
func compareVerify(value reflect.Value, VerifyStr string) bool {
switch value.Kind() {
case reflect.String:
return compare(len([]rune(value.String())), VerifyStr)
case reflect.Slice, reflect.Array:
return compare(value.Len(), VerifyStr)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return compare(value.Uint(), VerifyStr)
case reflect.Float32, reflect.Float64:
return compare(value.Float(), VerifyStr)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return compare(value.Int(), VerifyStr)
default:
return false
}
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: isBlank
//@description: 非空校验
//@param: value reflect.Value
//@return: bool
func isBlank(value reflect.Value) bool {
switch value.Kind() {
case reflect.String, reflect.Slice:
return value.Len() == 0
case reflect.Bool:
return !value.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return value.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return value.Uint() == 0
case reflect.Float32, reflect.Float64:
return value.Float() == 0
case reflect.Interface, reflect.Ptr:
return value.IsNil()
}
return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface())
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: compare
//@description: 比较函数
//@param: value interface{}, VerifyStr string
//@return: bool
func compare(value interface{}, VerifyStr string) bool {
VerifyStrArr := strings.Split(VerifyStr, "=")
val := reflect.ValueOf(value)
switch val.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
VInt, VErr := strconv.ParseInt(VerifyStrArr[1], 10, 64)
if VErr != nil {
return false
}
switch {
case VerifyStrArr[0] == "lt":
return val.Int() < VInt
case VerifyStrArr[0] == "le":
return val.Int() <= VInt
case VerifyStrArr[0] == "eq":
return val.Int() == VInt
case VerifyStrArr[0] == "ne":
return val.Int() != VInt
case VerifyStrArr[0] == "ge":
return val.Int() >= VInt
case VerifyStrArr[0] == "gt":
return val.Int() > VInt
default:
return false
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
VInt, VErr := strconv.Atoi(VerifyStrArr[1])
if VErr != nil {
return false
}
switch {
case VerifyStrArr[0] == "lt":
return val.Uint() < uint64(VInt)
case VerifyStrArr[0] == "le":
return val.Uint() <= uint64(VInt)
case VerifyStrArr[0] == "eq":
return val.Uint() == uint64(VInt)
case VerifyStrArr[0] == "ne":
return val.Uint() != uint64(VInt)
case VerifyStrArr[0] == "ge":
return val.Uint() >= uint64(VInt)
case VerifyStrArr[0] == "gt":
return val.Uint() > uint64(VInt)
default:
return false
}
case reflect.Float32, reflect.Float64:
VFloat, VErr := strconv.ParseFloat(VerifyStrArr[1], 64)
if VErr != nil {
return false
}
switch {
case VerifyStrArr[0] == "lt":
return val.Float() < VFloat
case VerifyStrArr[0] == "le":
return val.Float() <= VFloat
case VerifyStrArr[0] == "eq":
return val.Float() == VFloat
case VerifyStrArr[0] == "ne":
return val.Float() != VFloat
case VerifyStrArr[0] == "ge":
return val.Float() >= VFloat
case VerifyStrArr[0] == "gt":
return val.Float() > VFloat
default:
return false
}
default:
return false
}
}
func regexpMatch(rule, matchStr string) bool {
return regexp.MustCompile(rule).MatchString(matchStr)
}
+54
View File
@@ -0,0 +1,54 @@
package wechat
import (
"context"
"encoding/json"
"errors"
"io"
"log"
"net/http"
"sundynix-go/global"
"time"
"github.com/redis/go-redis/v9"
)
// GetMiniAccessToken 获取小程序的access_token
func GetMiniAccessToken() string {
ak, err := global.Redis.Get(context.Background(), "mini_access_token").Result()
if errors.Is(err, redis.Nil) {
// 从微信服务器获取
//重新从微信服务器获取
url := "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + global.Config.MiniProgram.AppId + "&secret=" + global.Config.MiniProgram.AppSecret
resp, err := http.Get(url)
if err != nil {
log.Fatalf("Error making GET request: %s", err)
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("Error closing response body: %s", err)
}
}(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatalf("Error reading response body: %s", err)
}
res := string(body)
var data map[string]interface{}
err = json.Unmarshal([]byte(res), &data)
if err != nil {
log.Fatalf("Error unmarshalling JSON: %s", err)
}
ak = data["access_token"].(string)
ex := data["expires_in"].(float64)
global.Redis.Set(context.Background(), "mini_access_token", ak, time.Duration(ex)*time.Second) //秒
} else if err != nil {
log.Fatalf("Error getting access token from Redis: %s", err)
} else {
return ak
}
return ak
}
+39
View File
@@ -0,0 +1,39 @@
package wechat
import (
"context"
"fmt"
"sundynix-go/global"
"github.com/wechatpay-apiv3/wechatpay-go/core"
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
"github.com/wechatpay-apiv3/wechatpay-go/utils"
)
// GetWxPayClient 初始化微信支付客户端
func GetWxPayClient() (*core.Client, error) {
//2.加载私钥
mchPrivateKey, err := utils.LoadPrivateKeyWithPath(global.Config.WechatPay.PrivateKeyPath)
if err != nil {
return nil, err
}
mchPublicKey, err := utils.LoadPublicKeyWithPath(global.Config.WechatPay.PublicKeyPath)
if err != nil {
return nil, err
}
ctx := context.Background()
// 3. 创建客户端配置 使用商户私钥等初始化 client,并使它具有自动定时获取微信支付平台证书的能力
opts := []core.ClientOption{
option.WithWechatPayPublicKeyAuthCipher(global.Config.WechatPay.MchId,
global.Config.WechatPay.MchCertificateSerialNumber,
mchPrivateKey, global.Config.WechatPay.PublicKeyId, mchPublicKey), // 自动处理签名/验签
}
client, err := core.NewClient(ctx, opts...)
if err != nil {
fmt.Printf("new wechat pay client err:%s", err)
}
return client, err
}