first commit
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
package async
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime/debug"
|
||||
"sundynix-go/global"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type AsyncTask func(ctx context.Context)
|
||||
|
||||
type namedTask struct {
|
||||
name string
|
||||
fn AsyncTask
|
||||
}
|
||||
|
||||
type TaskRunner struct {
|
||||
mu sync.Mutex
|
||||
tasks []namedTask
|
||||
}
|
||||
|
||||
// Add 添加任务
|
||||
func (tr *TaskRunner) Add(name string, task AsyncTask) {
|
||||
if task == nil {
|
||||
return
|
||||
}
|
||||
tr.mu.Lock()
|
||||
defer tr.mu.Unlock()
|
||||
tr.tasks = append(tr.tasks, namedTask{name: name, fn: task})
|
||||
}
|
||||
|
||||
// RunAll 安全执行
|
||||
func (tr *TaskRunner) RunAll() {
|
||||
tr.mu.Lock()
|
||||
todoTasks := tr.tasks
|
||||
tr.tasks = nil
|
||||
tr.mu.Unlock()
|
||||
|
||||
for _, task := range todoTasks {
|
||||
t := task
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// 使用全局 Zap 记录结构化日志
|
||||
// 这里的 global.Logger 替换为你实际的全局变量名
|
||||
global.Logger.Error("异步任务异常崩溃",
|
||||
zap.String("task_name", t.name),
|
||||
zap.Any("panic_info", r),
|
||||
zap.String("stack", string(debug.Stack())),
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
// 异步任务执行,设置独立的超时控制
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
t.fn(ctx)
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sundynix-go/global"
|
||||
"sundynix-go/model/system/request"
|
||||
"sundynix-go/utils/timer"
|
||||
"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, _ := timer.ParseDuration(global.Config.JWT.BufferTime)
|
||||
ep, _ := timer.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
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package captcha
|
||||
|
||||
import "github.com/mojocn/base64Captcha"
|
||||
|
||||
var CaptchaStore = base64Captcha.DefaultMemStore
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package timer
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
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)
|
||||
case totalMinutes < 1:
|
||||
return "刚刚"
|
||||
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
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package wechat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"sundynix-go/global"
|
||||
"sundynix-go/pkg/httpclient"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// GetMiniAccessToken 获取小程序的access_token
|
||||
func GetMiniAccessToken() string {
|
||||
ak, err := global.Redis.Get(context.Background(), "zeeq_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
|
||||
myHttpClient := httpclient.GetClient()
|
||||
resp, err := myHttpClient.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(), "zeeq_mini_access_token", ak, time.Duration(ex)*time.Second) //秒
|
||||
} else if err != nil {
|
||||
log.Fatalf("Error getting access token from Redis: %s", err)
|
||||
}
|
||||
return ak
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package wechat
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sundynix-go/global"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// WeChatCommonResponse 微信通用响应
|
||||
type WeChatCommonResponse struct {
|
||||
Errcode int `json:"errcode"`
|
||||
Errmsg string `json:"errmsg"`
|
||||
}
|
||||
|
||||
// MsgSecCheckDetail 文本检测结果详情
|
||||
type MsgSecCheckDetail struct {
|
||||
Strategy string `json:"strategy"`
|
||||
Errcode int `json:"errcode"`
|
||||
Suggest string `json:"suggest"`
|
||||
Label int `json:"label"`
|
||||
Prob int `json:"prob"`
|
||||
}
|
||||
|
||||
// MsgSecCheckResult 文本检测结果
|
||||
type MsgSecCheckResult struct {
|
||||
Suggest string `json:"suggest"`
|
||||
Label int `json:"label"`
|
||||
}
|
||||
|
||||
// MsgSecCheckResponse 文本检测响应
|
||||
type MsgSecCheckResponse struct {
|
||||
WeChatCommonResponse
|
||||
Result MsgSecCheckResult `json:"result"`
|
||||
Detail []MsgSecCheckDetail `json:"detail"`
|
||||
}
|
||||
|
||||
// MediaCheckAsyncResponse 媒体检测异步响应
|
||||
type MediaCheckAsyncResponse struct {
|
||||
WeChatCommonResponse
|
||||
TraceId string `json:"trace_id"`
|
||||
}
|
||||
|
||||
// MsgSecCheck 文本内容安全识别
|
||||
// content: 需检测的文本内容
|
||||
// openid: 用户的openid(用户需在近两小时访问过小程序)
|
||||
// return: true-通过, false-不通过
|
||||
func MsgSecCheck(content string, openid string) bool {
|
||||
// 2024-05-15: 微信api调整
|
||||
// https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/sec-center/sec-check/msgSecCheck.html
|
||||
accessToken := GetMiniAccessToken()
|
||||
url := "https://api.weixin.qq.com/wxa/msg_sec_check?access_token=" + accessToken
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"version": 2,
|
||||
"openid": openid,
|
||||
"scene": 2, // 2-资料说明
|
||||
"content": content,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
global.Logger.Error("MsgSecCheck json marshal error", zap.Error(err))
|
||||
return false
|
||||
}
|
||||
|
||||
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
global.Logger.Error("MsgSecCheck http post error", zap.Error(err))
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
global.Logger.Error("MsgSecCheck read body error", zap.Error(err))
|
||||
return false
|
||||
}
|
||||
|
||||
var response MsgSecCheckResponse
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
global.Logger.Error("MsgSecCheck json unmarshal error", zap.Error(err))
|
||||
return false
|
||||
}
|
||||
|
||||
if response.Errcode != 0 {
|
||||
global.Logger.Error("MsgSecCheck api error", zap.Int("errcode", response.Errcode), zap.String("errmsg", response.Errmsg))
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查 result.suggest
|
||||
if response.Result.Suggest == "pass" {
|
||||
return true
|
||||
}
|
||||
|
||||
global.Logger.Warn("MsgSecCheck risky content",
|
||||
zap.String("content", content),
|
||||
zap.String("suggest", response.Result.Suggest),
|
||||
zap.Int("label", response.Result.Label),
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
// MediaCheckAsync 多媒体内容安全识别(异步)
|
||||
// mediaUrl: 需检测的多媒体url
|
||||
// mediaType: 1:音频; 2:图片
|
||||
// openid: 用户的openid
|
||||
// return: trace_id, error
|
||||
func MediaCheckAsync(mediaUrl string, mediaType int, openid string) (string, error) {
|
||||
accessToken := GetMiniAccessToken()
|
||||
url := "https://api.weixin.qq.com/wxa/media_check_async?access_token=" + accessToken
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"media_url": mediaUrl,
|
||||
"media_type": mediaType,
|
||||
"version": 2,
|
||||
"openid": openid,
|
||||
"scene": 2,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var response MediaCheckAsyncResponse
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if response.Errcode != 0 {
|
||||
global.Logger.Error("MediaCheckAsync api error", zap.Int("errcode", response.Errcode), zap.String("errmsg", response.Errmsg))
|
||||
return "", fmt.Errorf("errcode: %d, errmsg: %s", response.Errcode, response.Errmsg)
|
||||
}
|
||||
|
||||
if response.TraceId != "" {
|
||||
return response.TraceId, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no trace_id returned")
|
||||
}
|
||||
Reference in New Issue
Block a user