Compare commits

...

19 Commits

Author SHA1 Message Date
ayflying
50fe34e1c1 增加s3文件重命名方法 2025-07-22 10:38:08 +08:00
ayflying
25c00d5072 gameact持久化保存区分跟新与添加 2025-07-21 19:20:46 +08:00
ayflying
ee32c8b83d 更新支持配置代理地址配置 2025-07-11 14:07:56 +08:00
ayflying
12a193fdee 增加oppo支付回调库 2025-07-09 11:01:38 +08:00
ayflying
ca55880beb 小米支付的回调结构体放入库 2025-07-09 10:58:27 +08:00
ayflying
ee9e399a81 分离小米支付与taptap支付到库里面 2025-07-08 16:30:44 +08:00
ayflying
ed9b0dd248 日志上传批量上传接口修改 2025-07-02 14:52:57 +08:00
ayflying
02edbdae8d 修改config模板的导入方法 2025-06-13 11:14:30 +08:00
ayflying
5e75094c16 s3使用自动模式 2025-06-11 19:11:50 +08:00
ayflying
140317e2df 判断s3服务器提供商 2025-06-11 12:17:01 +08:00
ayflying
8e2301eb34 为了兼容七牛云接口,s3强制使用路径风格 2025-06-11 11:02:21 +08:00
ayflying
c673da89cf taptap支付回调接口 2025-05-29 19:19:38 +08:00
ayflying
005cf11b2a 增加taptap支付 2025-05-29 11:25:06 +08:00
ayflying
8195d633a1 修改文件缓存的缓存文件清理 2025-05-23 10:38:26 +08:00
ayflying
4741a0bd93 增加file驱动 2025-05-22 18:11:26 +08:00
ayflying
f22c9df605 分离websocket模块 2025-05-20 10:43:10 +08:00
ayflying
82cffdfe29 没有配置数据库,不执行计划任务,预防报错 2025-05-19 11:31:39 +08:00
ayflying
9da1a3bf25 读取配置有问题 2025-05-14 17:52:41 +08:00
ayflying
2cb005e8ed 未读取到配置报错提示 2025-05-14 16:41:53 +08:00
24 changed files with 862 additions and 120 deletions

View File

@@ -1,9 +1,9 @@
package config
import (
"github.com/ayflying/utility_go"
"github.com/gogf/gf/v2/util/gutil"
"sync"
"github.com/ayflying/utility_go/pkg"
"github.com/gogf/gf/v2/util/gutil"
"sync"
)
type {cfg}Cfg struct {
@@ -23,7 +23,7 @@ func (c *{mod}Mod) Load(_cfg ...string) {
c.lock.Lock()
defer c.lock.Unlock()
c.cfgArr = make([]*{cfg}Cfg, 0)
data, err := utility_go.Config.GetFile("{file}")
data, err := pkg.Config().GetFile("{file}")
err = data.Scan(&c.cfgArr)
if err != nil {
panic(err)

View File

@@ -124,11 +124,11 @@ var (
FileUrl: url[v.S3],
})
if err != nil {
Proxy := g.Cfg().MustGet(ctx, "update_proxy", "http://192.168.50.170:10808").String()
g.Log().Debugf(ctx, "切换代理进行上传:err=%v", err)
get, err = client.Proxy("http://192.168.50.114:10808").
Post(ctx, address+"/callback/update", &UpdateReq{
FileUrl: url[v.S3],
})
get, err = client.Proxy(Proxy).Post(ctx, address+"/callback/update", &UpdateReq{
FileUrl: url[v.S3],
})
}
if err != nil {
g.Log().Error(ctx, err)

View File

@@ -27,26 +27,26 @@ type Cfg struct {
// Deprecated : pkg.Config().GetDbFile(name)
func (c *Cfg) GetDbFile(name string) (res *g.Var, err error) {
pkg.Config().GetDbFile(name)
res, err = pkg.Config().GetDbFile(name)
return
}
// Deprecated : pkg.Config().GetFile(name, obj...)
func (c *Cfg) GetFile(filename string, obj ...Load) (jsonObj *gjson.Json, err error) {
pkg.Config().GetFile(filename)
jsonObj, err = pkg.Config().GetFile(filename)
return
}
// getUrlFile 获取远程配置
// Deprecated : pkg.Config().GetUrlFile(name)
func (c *Cfg) GetUrlFile(name string) (jsonObj *gjson.Json, err error) {
pkg.Config().GetUrlFile(name)
jsonObj, err = pkg.Config().GetUrlFile(name)
return
}
// Deprecated : pkg.Config().GetApollo(name, obj)
func (c *Cfg) GetApollo(name string, obj Load) (jsonObj *gjson.Json, err error) {
pkg.Config().GetApollo(name, obj)
jsonObj, err = pkg.Config().GetApollo(name, obj)
return
}

4
go.mod
View File

@@ -4,7 +4,7 @@ go 1.24.0
require (
github.com/apolloconfig/agollo/v4 v4.4.0
github.com/ayflying/excel2json v1.1.7
github.com/ayflying/excel2json v1.1.9
github.com/elastic/go-elasticsearch/v8 v8.18.0
github.com/go-pay/crypto v0.0.1
github.com/go-pay/gopay v1.5.109
@@ -12,7 +12,6 @@ require (
github.com/goccy/go-json v0.10.5
github.com/gogf/gf/contrib/config/apollo/v2 v2.9.0
github.com/gogf/gf/v2 v2.9.0
github.com/gorilla/websocket v1.5.3
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20241220152942-06eb5c6e8230
github.com/minio/minio-go/v7 v7.0.91
github.com/prometheus/client_golang v1.22.0
@@ -45,6 +44,7 @@ require (
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/gax-go/v2 v2.0.5 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/grokify/html-strip-tags-go v0.1.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect

4
go.sum
View File

@@ -50,8 +50,8 @@ github.com/apolloconfig/agollo/v4 v4.4.0/go.mod h1:6WjI68IzqMk/Y6ghMtrj5AX6Uewo2
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/ayflying/excel2json v1.1.7 h1:gp4Ls6hNXd1EuT2MEiDQ6yPLaSKIFWkGyaNajMTc59c=
github.com/ayflying/excel2json v1.1.7/go.mod h1:GjXYP2cDzeTgQxssQ3ba8QRa9JsScAeJMwCrZ7QTlQo=
github.com/ayflying/excel2json v1.1.9 h1:AKaUFc/FNQXV/53/VSgp3J+2d4PMWhVpTDRj/yx3VOs=
github.com/ayflying/excel2json v1.1.9/go.mod h1:GjXYP2cDzeTgQxssQ3ba8QRa9JsScAeJMwCrZ7QTlQo=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=

View File

@@ -134,7 +134,8 @@ func (s *sGameAct) Save(ctx context.Context, actId int) (err error) {
//循环获取缓存数据
err = tools.Redis.RedisScanV2(cacheKey, func(keys []string) (err error) {
var add []interface{}
var add = make([]*entity.GameAct, 0)
var update = make([]*entity.GameAct, 0)
var delKey []string
for _, cacheKey = range keys {
result := strings.Split(cacheKey, ":")
@@ -176,25 +177,51 @@ func (s *sGameAct) Save(ctx context.Context, actId int) (err error) {
}
actionData := cacheGet.String()
if data == nil {
//data =
add = append(add, &do.GameAct{
add = append(add, &entity.GameAct{
ActId: actId,
Uid: uid,
Action: actionData,
})
} else {
//覆盖数据
data.ActId = actId
data.Uid = uid
data.Action = actionData
add = append(add, data)
update = append(update, data)
}
//最后删除key
delKey = append(delKey, cacheKey)
}
//批量写入数据库
if len(add) > 0 {
dbRes, err2 := g.Model(Name).Batch(30).Data(add).Save()
add = make([]interface{}, 0)
if len(delKey) > 0 {
for _, v := range update {
v.UpdatedAt = gtime.Now()
_, err2 := g.Model(Name).Where(do.GameAct{
Uid: v.Uid,
ActId: v.ActId,
UpdatedAt: v.UpdatedAt,
}).Data(v).Update()
if err2 != nil {
g.Log().Error(ctx, err2)
return
}
////获取多少个数据,删除不是当前修改的数据
//count, _ := g.Model(Name).Where(do.GameAct{
// Uid: v.Uid,
// ActId: v.ActId,
//}).Count()
//if count > 1 {
// g.Model(Name).Where(do.GameAct{
// Uid: v.Uid,
// ActId: v.ActId,
// }).WhereNot("updated_at", v.UpdatedAt).Delete()
//}
}
//dbRes, err2 := g.Model(Name).Batch(50).Data(add).Update()
update = make([]*entity.GameAct, 0)
dbRes, err2 := g.Model(Name).Batch(50).Data(add).Save()
add = make([]*entity.GameAct, 0)
if err2 != nil {
g.Log().Error(ctx, err2)
return

View File

@@ -136,6 +136,10 @@ func (s *sSystemCron) AddCronV2(typ v1.CronType, _func func(context.Context) err
// @receiver s
// @return err
func (s *sSystemCron) StartCron() (err error) {
//如果没有数据库配置,跳过计划任务执行
if g.Cfg().MustGet(gctx.New(), "database") == nil {
return
}
//预防重复启动
if startTime != nil {
return

5
modules/websocket/go.mod Normal file
View File

@@ -0,0 +1,5 @@
module github.com/ayflying/utility_go/modules/websocket
go 1.24.0
require github.com/ayflying/utility_go v1.2.26

2
modules/websocket/go.sum Normal file
View File

@@ -0,0 +1,2 @@
github.com/ayflying/utility_go v1.2.26/go.mod h1:y6qwRUmYk73cH/dTRSzj2mnaQdtrAfFNH4wRFewxlIc=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=

13
package/pay/oppo/model.go Normal file
View File

@@ -0,0 +1,13 @@
package oppo
// OPPO支付回调参数结构体
type PayCallback struct {
NotifyId string `json:"notifyId" dc:"回调通知单号以GC开头必填示例:GC20230314657000"`
PartnerOrder string `json:"partnerOrder" dc:"开发者订单号,必填,示例:123456"`
ProductName string `json:"productName" dc:"商品名称,必填,示例:10元宝"`
ProductDesc string `json:"productDesc" dc:"商品描述,必填,示例:10元宝等于1元"`
Price int64 `json:"price" dc:"商品价格,单位为分,需要游戏服务端做验证,必填,示例:100"`
Count int `json:"count" dc:"商品数量一般为1必填示例:1"`
Attach string `json:"attach" dc:"请求支付时上传的附加参数,可能为空,选填"`
Sign string `json:"sign" dc:"OPPO服务端签名需要游戏服务端做验证必填"`
}

119
package/pay/oppo/oppo.go Normal file
View File

@@ -0,0 +1,119 @@
package oppo
import (
"context"
"crypto"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"net/http"
"strings"
)
// 跟充值平台通信的加密key
//const PUBLIC_KEY = `dfsdfs`
type OppoType struct {
PublicKey string `json:"public_key"`
}
func New(PublicKey string) *OppoType {
return &OppoType{
PublicKey: PublicKey,
}
}
func (p *OppoType) Verify(ctx context.Context, data map[string]string) error {
// 解析请求参数
for k, v := range data {
if v == "" || v == "0" {
delete(data, k)
}
}
//data["notifyId"] = getParam(r, "notifyId")
//data["partnerOrder"] = getParam(r, "partnerOrder")
//data["productName"] = getParam(r, "productName")
//data["productDesc"] = getParam(r, "productDesc")
//data["price"] = getParam(r, "price")
//data["count"] = getParam(r, "count")
//data["attach"] = getParam(r, "attach")
//data["sign"] = getParam(r, "sign")
// 验证签名
result, err := p.rsaVerify(data)
if err != nil {
//http.Error(w, "Verification error: "+err.Error(), http.StatusInternalServerError)
g.Log().Errorf(ctx, "Verification error: %v", err.Error())
return err
}
if result {
// TODO::验证成功,处理后续逻辑
//fmt.Fprint(w, "Verification successful")
//g.Log().Errorf(ctx, "Verification error: %v", err.Error())
} else {
// TODO::验证失败,处理后续逻辑
//http.Error(w, "Verification failed", http.StatusBadRequest)
g.Log().Error(ctx, "Verification failed")
err = gerror.New("Verification failed")
}
return nil
}
func (p *OppoType) getParam(r *http.Request, paramName string) string {
r.ParseForm()
if value := r.FormValue(paramName); value != "" {
return strings.TrimSpace(value)
}
return ""
}
func (p *OppoType) rsaVerify(contents map[string]string) (bool, error) {
// 构建待签名字符串
strContents := fmt.Sprintf("notifyId=%s&partnerOrder=%s&productName=%s&productDesc=%s&price=%s&count=%s&attach=%s",
contents["notifyId"], contents["partnerOrder"], contents["productName"],
contents["productDesc"], contents["price"], contents["count"], contents["attach"])
// 解析公钥
publicKey := p.PublicKey
pemData := []byte("-----BEGIN PUBLIC KEY-----\n" +
strings.ReplaceAll(publicKey, " ", "\n") +
"\n-----END PUBLIC KEY-----")
block, _ := pem.Decode(pemData)
if block == nil {
return false, fmt.Errorf("failed to decode PEM block")
}
pubInterface, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return false, err
}
pubKey, ok := pubInterface.(*rsa.PublicKey)
if !ok {
return false, fmt.Errorf("public key is not an RSA public key")
}
// 解码签名
signature, err := base64.StdEncoding.DecodeString(contents["sign"])
if err != nil {
return false, err
}
// 计算内容的哈希值
hash := sha1.New()
hash.Write([]byte(strContents))
hashed := hash.Sum(nil)
// 验证签名
err = rsa.VerifyPKCS1v15(pubKey, crypto.SHA1, hashed, signature)
return err == nil, err
}

103
package/pay/taptap/sign.go Normal file
View File

@@ -0,0 +1,103 @@
package taptap
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"net/http"
"sort"
"strings"
)
type pTapTap struct {
Secret string `json:"secret" dc:"秘钥"`
//OrderId string `json:"order_id" dc:"订单唯一 ID"`
ClientId string `json:"client_id" dc:"应用的 Client ID"`
}
func New(clientId string, secret string) *pTapTap {
return &pTapTap{
Secret: secret,
ClientId: clientId,
//OrderId: orderId,
}
}
// Sign signs the request.
func (p *pTapTap) Sign(req *http.Request, secret string) (string, error) {
//获取请求参数
//req := g.RequestFromCtx(ctx).Request
return Sign(req, secret)
}
//func (p *pTapTap) SignOld(ctx context.Context, method, url string, token string, data any) (sign string, ts int64, nonce string, err error) {
// //secret := p.Secret
// //ts = gtime.Now().Unix()
// //nonce = grand.S(5)
// //header := http.Header{
// // "Content-Type": {"Content-Type: application/json; charset=utf-8"},
// // "X-Tap-Ts": {strconv.FormatInt(ts, 10)},
// // "X-Tap-Nonce": {nonce},
// //}
// //if method == "POST" {
// // header.Set("Content-Type", "application/json; charset=utf-8")
// //}
// ////ctx := context.Background()
// //request := g.RequestFromCtx(ctx).Request
// //body, _ := json.Marshal(data)
// ////req, err := http.NewRequestWithContext(ctx, method, url, strings.NewReader(string(body)))
// //req.Header = header
// //sign, err = Sign(req, secret)
// //if err != nil {
// // panic(err)
// //}
// //req.Header.Set("X-Tap-Sign", sign)
// //return
//}
// Sign signs the request.
func Sign(req *http.Request, secret string) (string, error) {
methodPart := req.Method
urlPathAndQueryPart := req.URL.RequestURI()
headersPart, err := getHeadersPart(req.Header)
if err != nil {
return "", err
}
bodyPart, err := io.ReadAll(req.Body)
if err != nil {
return "", err
}
signParts := methodPart + "\n" + urlPathAndQueryPart + "\n" + headersPart + "\n" + string(bodyPart) + "\n"
fmt.Println(signParts)
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(signParts))
rawSign := h.Sum(nil)
sign := base64.StdEncoding.EncodeToString(rawSign)
return sign, nil
}
// getHeadersPart returns the headers part of the request.
func getHeadersPart(header http.Header) (string, error) {
var headerKeys []string
for k, v := range header {
k = strings.ToLower(k)
if !strings.HasPrefix(k, "x-tap-") {
continue
}
if k == "x-tap-sign" {
continue
}
if len(v) > 1 {
return "", fmt.Errorf("invalid header, %q has multiple values", k)
}
headerKeys = append(headerKeys, k)
}
sort.Strings(headerKeys)
headers := make([]string, 0, len(headerKeys))
for _, k := range headerKeys {
headers = append(headers, fmt.Sprintf("%s:%s", k, header.Get(k)))
}
return strings.Join(headers, "\n"), nil
}

View File

@@ -0,0 +1,129 @@
package taptap
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/gclient"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/grand"
"io"
"net/http"
)
type GetPayData struct {
Data struct {
Order struct {
Order
} `json:"order"`
} `json:"data"`
Success bool `json:"success"`
}
type Order struct {
OrderID string `json:"order_id"` // 订单唯一 ID
PurchaseToken string `json:"purchase_token"` // 用于订单核销的 token
ClientID string `json:"client_id"` // 应用的 Client ID
OpenID string `json:"open_id"` // 用户的开放平台 ID
UserRegion string `json:"user_region"` // 用户地区
GoodsOpenID string `json:"goods_open_id"` // 商品唯一 ID
GoodsName string `json:"goods_name"` // 商品名称
Status PaymentStatus `json:"status"` // 订单状态
Amount string `json:"amount"` // 金额(本币金额 x 1,000,000
Currency string `json:"currency"` // 币种
CreateTime string `json:"create_time"` // 创建时间
PayTime string `json:"pay_time"` // 支付时间
Extra string `json:"extra"` // 商户自定义数据,如角色信息等,长度不超过 255 UTF-8 字符
}
type PaymentStatus string
const (
ChargePending PaymentStatus = "charge.pending" // 待支付
ChargeSucceeded PaymentStatus = "charge.succeeded" //支付成功
ChargeConfirmed PaymentStatus = "charge.confirmed" //已核销
ChargeOverdue PaymentStatus = "charge.overdue" //支付超时关闭
RefundPending PaymentStatus = "refund.pending" //退款中
RefundSucceeded PaymentStatus = "refund.succeeded" //退款成功
RefundFailed PaymentStatus = "refund.failed" //退款失败
RefundRejected PaymentStatus = "refund.rejected" //退款被拒绝
)
// 查询订单信息
func (p *pTapTap) Info(ctx context.Context, order string) (getPayData *GetPayData, err error) {
url := fmt.Sprintf("https://cloud-payment.tapapis.cn/order/v1/info?client_id=%v&order_id=%v", p.ClientId, order)
getPayData, err = p.get(ctx, url)
if err != nil {
return
}
return
}
// 验证并核销订单
func (p *pTapTap) Verify(ctx context.Context, req any) (getPayData *GetPayData, err error) {
url := fmt.Sprintf("https://cloud-payment.tapapis.cn/order/v1/verify?client_id=%v", p.ClientId)
getPayData, err = p.get(ctx, url, req)
if err != nil {
return
}
return
}
func (p *pTapTap) get(ctx context.Context, url string, _data ...any) (getPayData *GetPayData, err error) {
var _get *gclient.Response
var header = map[string]string{
"Content-Type": "Content-Type: application/json; charset=utf-8",
"X-Tap-Nonce": grand.S(6),
"X-Tap-Ts": gtime.Now().TimestampStr(),
}
ctx2 := context.Background()
var method = "GET"
if len(_data) > 0 {
method = "POST"
}
//temp := []byte(`{"event_type":"charge.succeeded","order":{"order_id":"1790288650833465345","purchase_token":"rT2Et9p0cfzq4fwjrTsGSacq0jQExFDqf5gTy1alp+Y=","client_id":"o6nD4iNavjQj75zPQk","open_id":"4+Axcl2RFgXbt6MZwdh++w==","user_region":"US","goods_open_id":"com.goods.open_id","goods_name":"TestGoodsName","status":"charge.succeeded","amount":"19000000000","currency":"USD","create_time":"1716168000","pay_time":"1716168000","extra":"1111111111111111111"}}`)
var body io.Reader
if len(_data) > 0 {
body = bytes.NewBuffer(gjson.MustEncode(_data[0]))
} else {
body = bytes.NewBuffer([]byte{})
}
req, _ := http.NewRequestWithContext(ctx2, method, url, body)
for k, v := range header {
req.Header.Set(k, v)
}
sign, err2 := p.Sign(req, p.Secret)
if err2 != nil {
err = err2
return
}
req.Header.Set("X-Tap-Sign", sign)
header["X-Tap-Sign"] = sign
if len(_data) == 0 {
_get, err = g.Client().Header(header).ContentJson().Get(gctx.New(), url)
} else {
_get, err = g.Client().Header(header).ContentJson().Post(gctx.New(), url, _data[0])
}
if err != nil {
return
}
getPayData = &GetPayData{}
resData := _get.ReadAll()
g.Dump(resData)
if err = json.Unmarshal(resData, &getPayData); err != nil {
return
}
if !getPayData.Success {
err = errors.New(string(resData))
}
return
}

View File

@@ -0,0 +1,63 @@
package xiaomi
import (
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"fmt"
"sort"
"strings"
)
// SignatureHelper 签名辅助类
type SignatureHelper struct{}
// hmacSHA1 计算HMAC-SHA1哈希值
func hmacSHA1(data, key string) string {
h := hmac.New(sha1.New, []byte(key))
h.Write([]byte(data))
return hex.EncodeToString(h.Sum(nil))
}
// Sign 计算hmac-sha1签名
func (m *MiPay) Sign(params map[string]string, secretKey string) string {
if _, ok := params["signature"]; ok {
delete(params, "signature")
}
for k, v := range params {
if v == "" || v == "0" {
delete(params, k)
}
}
sortString := m.buildSortString(params)
signature := hmacSHA1(sortString, secretKey)
return signature
}
// VerifySignature 验证签名
func (m *MiPay) VerifySignature(params map[string]string, signature, secretKey string) bool {
tmpSign := m.Sign(params, secretKey)
return tmpSign == signature
}
// buildSortString 构造排序字符串
func (m *MiPay) buildSortString(params map[string]string) string {
if len(params) == 0 {
return ""
}
// 按键排序
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
// 构建排序字符串
var fields []string
for _, k := range keys {
fields = append(fields, fmt.Sprintf("%s=%s", k, params[k]))
}
return strings.Join(fields, "&")
}

View File

@@ -0,0 +1,20 @@
package xiaomi
import "github.com/gogf/gf/v2/os/gtime"
type PayCallback struct {
AppID string `json:"appId" dc:"游戏ID" required:"true"`
CPOrderID string `json:"cpOrderId" dc:"开发商订单ID" required:"true"`
CPUserInfo string `json:"cpUserInfo" dc:"开发商透传信息" required:"false"`
OrderConsumeType int `json:"orderConsumeType" dc:"订单类型10普通订单 11直充直消订单" required:"false"`
OrderID string `json:"orderId" dc:"游戏平台订单ID" required:"true"`
OrderStatus string `json:"orderStatus" dc:"订单状态TRADE_SUCCESS代表成功" required:"true"`
PayFee int `json:"payFee" dc:"支付金额单位为分即0.01米币。请务必使用payFee字段值与游戏发起订单金额做校验确保订单金额一致性" required:"true"`
PayTime *gtime.Time `json:"payTime" dc:"支付时间格式yyyy-MM-dd HH:mm:ss" required:"true"`
ProductCode string `json:"productCode" dc:"商品代码" required:"true"`
ProductCount int `json:"productCount" dc:"商品数量" required:"true"`
ProductName string `json:"productName" dc:"商品名称" required:"true"`
UID string `json:"uid" dc:"用户ID" required:"true"`
PartnerGiftConsume int64 `json:"partnerGiftConsume" dc:"使用游戏券金额如果订单使用游戏券则有long型如果有则参与签名" required:"false"`
Signature string `json:"signature" dc:"签名,签名方法见后面说明" required:"true"`
}

View File

@@ -0,0 +1,29 @@
package xiaomi
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
)
// Config 小米支付配置信息
type Config struct {
AppID string `json:"app_id"` // 应用ID
AppSecret string `json:"app_secret"` // 应用密钥
//PrivateKey string // 商户私钥(如需证书)
//MIAPIURL string // 小米支付API基础地址
//IsSandbox bool // 是否沙箱环境
}
// Miipay 小米支付客户端
type MiPay struct {
config *Config
}
func New() *MiPay {
_cfg, _ := g.Cfg().Get(gctx.New(), "pay.xiaomi")
var cfg *Config
_cfg.Scan(&cfg)
return &MiPay{
config: cfg,
}
}

View File

@@ -270,7 +270,7 @@ func (s *Mod) GetPath(url string) (filePath string) {
return url[len(get+bucketName)+1:]
}
// 复制文件
// CopyObject 复制文件
func (s *Mod) CopyObject(bucketName string, dstStr string, srcStr string) (err error) {
// 原始文件
@@ -288,3 +288,15 @@ func (s *Mod) CopyObject(bucketName string, dstStr string, srcStr string) (err e
_, err = s.client.CopyObject(ctx, dst, src)
return
}
// Rename 重命名文件
func (s *Mod) Rename(bucketName string, name string, newName string) (err error) {
// 复制文件到新的名称
err = s.CopyObject(bucketName, name, newName)
if err != nil {
return
}
// 删除原始文件
err = s.RemoveObject(bucketName, name)
return
}

View File

@@ -3,121 +3,270 @@ package drive
import (
"context"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/crypto/gmd5"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/os/gcache"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv"
"path"
"strconv"
"strings"
"sync"
"time"
)
var (
fileIndex = "index.txt"
)
type FileIndex struct {
File string `json:"file"`
Duration time.Duration `json:"duration"`
}
type FileData struct {
Data interface{} `json:"data"`
Time int64 `json:"time"`
}
type AdapterFile struct {
FilePath string
Lock sync.Mutex
}
func (a AdapterFile) Set(ctx context.Context, key interface{}, value interface{}, duration time.Duration) error {
arr := strings.Split(":", gconv.String(key))
fileName := path.Join(arr...)
return gfile.PutBytes(fileName, gconv.Bytes(value))
func (a *AdapterFile) Key2Name(key interface{}) string {
md5Str, _ := gmd5.Encrypt(key)
fileName := path.Join(md5Str[0:2], md5Str[2:4], md5Str[4:6], md5Str[16:])
fileNameAll := path.Join(a.FilePath, fileName)
return fileNameAll
}
func (a AdapterFile) SetMap(ctx context.Context, data map[interface{}]interface{}, duration time.Duration) error {
func (a *AdapterFile) AddIndex(FileName interface{}, duration time.Duration) {
var isEdit bool
var setTime int64
if duration == 0 {
setTime = 0
} else {
setTime = gtime.Now().Add(duration).Unix()
}
saveArr := []string{
gconv.String(FileName),
strconv.FormatInt(setTime, 10),
}
saveStr := strings.Join(saveArr, "|")
gfile.ReadLines(fileIndex, func(text string) (err error) {
arr := strings.Split(text, "|")
if arr[0] == FileName {
isEdit = true
gfile.ReplaceFile(text, saveStr, fileIndex)
return
}
return
})
if isEdit {
return
}
gfile.PutContentsAppend(fileIndex, saveStr+"\n")
}
func (a *AdapterFile) DelIndex(FileName interface{}) {
//var save bool
gfile.ReadLines(fileIndex, func(text string) (err error) {
arr := strings.Split(text, "|")
if arr[0] == FileName {
//save = true
err = gfile.ReplaceFile(text+"\n", "", fileIndex)
return
}
return
})
}
func (a *AdapterFile) Set(ctx context.Context, key interface{}, value interface{}, duration time.Duration) (err error) {
fileNameAll := a.Key2Name(key)
var send = &FileData{
Data: value,
Time: gtime.Now().Add(duration).Unix(),
}
err = gfile.PutBytes(fileNameAll, gjson.MustEncode(send))
if err != nil {
return
}
a.AddIndex(key, duration)
return
}
func (a *AdapterFile) SetMap(ctx context.Context, data map[interface{}]interface{}, duration time.Duration) (err error) {
for k, v := range data {
//fileNameAll := a.Key2Name(k)
//var send = &FileData{
// Data: v,
// Time: gtime.Now().Add(duration).Unix(),
//}
//
//err = gfile.PutBytes(fileNameAll, gconv.Bytes(send))
a.Set(ctx, k, v, duration)
}
return
}
func (a *AdapterFile) SetIfNotExist(ctx context.Context, key interface{}, value interface{}, duration time.Duration) (ok bool, err error) {
err = a.Set(ctx, key, value, duration)
return
}
func (a *AdapterFile) SetIfNotExistFunc(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (ok bool, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterFile) SetIfNotExist(ctx context.Context, key interface{}, value interface{}, duration time.Duration) (ok bool, err error) {
func (a *AdapterFile) SetIfNotExistFuncLock(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (ok bool, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterFile) SetIfNotExistFunc(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (ok bool, err error) {
func (a *AdapterFile) Get(ctx context.Context, key interface{}) (res *gvar.Var, err error) {
var data *FileData
name := a.Key2Name(key)
if !gfile.IsFile(name) {
return
}
file := gfile.GetBytes(name)
gjson.DecodeTo(file, &data)
if data.Time < time.Now().Unix() {
a.Remove(ctx, key)
return
}
res = gvar.New(data.Data)
return
}
func (a *AdapterFile) GetOrSet(ctx context.Context, key interface{}, value interface{}, duration time.Duration) (result *gvar.Var, err error) {
a.Set(ctx, key, value, duration)
result, err = a.Get(ctx, key)
return
}
func (a *AdapterFile) GetOrSetFunc(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (result *gvar.Var, err error) {
value := f
a.Set(ctx, key, value, duration)
result, err = a.Get(ctx, key)
return
}
func (a *AdapterFile) GetOrSetFuncLock(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (result *gvar.Var, err error) {
a.Lock.Lock()
defer a.Lock.Unlock()
result, err = a.GetOrSetFunc(ctx, key, f, duration)
return
}
func (a *AdapterFile) Contains(ctx context.Context, key interface{}) (bool, error) {
return gfile.IsFile(a.FilePath), nil
}
func (a *AdapterFile) Size(ctx context.Context) (size int, err error) {
size = int(gfile.Size(a.FilePath))
return
}
func (a *AdapterFile) Data(ctx context.Context) (data map[interface{}]interface{}, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterFile) SetIfNotExistFuncLock(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (ok bool, err error) {
func (a *AdapterFile) Keys(ctx context.Context) (keys []interface{}, err error) {
keys = make([]interface{}, 0)
gfile.ReadLines(fileIndex, func(text string) (err error) {
arr := strings.Split(text, "|")
keys = append(keys, arr[0])
return
})
return
}
func (a *AdapterFile) Values(ctx context.Context) (values []interface{}, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterFile) Get(ctx context.Context, key interface{}) (*gvar.Var, error) {
func (a *AdapterFile) Update(ctx context.Context, key interface{}, value interface{}) (oldValue *gvar.Var, exist bool, err error) {
fileNameAll := a.Key2Name(key)
getFile := gfile.GetBytes(fileNameAll)
var data *FileData
gconv.Scan(getFile, &data)
oldValue.Set(data.Data)
var send = &FileData{
Data: value,
Time: data.Time,
}
err = gfile.PutBytes(fileNameAll, gconv.Bytes(send))
if err != nil {
return
}
a.AddIndex(key, time.Duration(data.Time-gtime.Now().Unix()))
return
}
func (a *AdapterFile) UpdateExpire(ctx context.Context, key interface{}, duration time.Duration) (oldDuration time.Duration, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterFile) GetOrSet(ctx context.Context, key interface{}, value interface{}, duration time.Duration) (result *gvar.Var, err error) {
func (a *AdapterFile) GetExpire(ctx context.Context, key interface{}) (time.Duration, error) {
//TODO implement me
panic("implement me")
}
func (a AdapterFile) GetOrSetFunc(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (result *gvar.Var, err error) {
//TODO implement me
panic("implement me")
func (a *AdapterFile) Remove(ctx context.Context, keys ...interface{}) (lastValue *gvar.Var, err error) {
for _, v := range keys {
fileNameAll := a.Key2Name(v)
lastValue, err = a.Get(ctx, fileNameAll)
err = gfile.RemoveFile(fileNameAll)
//删除索引文件
a.DelIndex(v)
}
return nil, nil
}
func (a AdapterFile) GetOrSetFuncLock(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (result *gvar.Var, err error) {
//TODO implement me
panic("implement me")
func (a *AdapterFile) Clear(ctx context.Context) error {
return gfile.RemoveAll(a.FilePath)
}
func (a AdapterFile) Contains(ctx context.Context, key interface{}) (bool, error) {
//TODO implement me
panic("implement me")
}
func (a AdapterFile) Size(ctx context.Context) (size int, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterFile) Data(ctx context.Context) (data map[interface{}]interface{}, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterFile) Keys(ctx context.Context) (keys []interface{}, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterFile) Values(ctx context.Context) (values []interface{}, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterFile) Update(ctx context.Context, key interface{}, value interface{}) (oldValue *gvar.Var, exist bool, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterFile) UpdateExpire(ctx context.Context, key interface{}, duration time.Duration) (oldDuration time.Duration, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterFile) GetExpire(ctx context.Context, key interface{}) (time.Duration, error) {
//TODO implement me
panic("implement me")
}
func (a AdapterFile) Remove(ctx context.Context, keys ...interface{}) (lastValue *gvar.Var, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterFile) Clear(ctx context.Context) error {
//TODO implement me
panic("implement me")
}
func (a AdapterFile) Close(ctx context.Context) error {
//TODO implement me
panic("implement me")
func (a *AdapterFile) Close(ctx context.Context) error {
return nil
}
func NewAdapterFile(filePath string) gcache.Adapter {
return &AdapterFile{
fileIndex = path.Join(filePath, fileIndex)
//清空过期文件目录
delKeys := make([]interface{}, 0)
gfile.ReadLines(fileIndex, func(text string) error {
arr := strings.Split(text, "|")
if gtime.Now().Unix() > gconv.Int64(arr[1]) {
delKeys = append(delKeys, arr[0])
}
return nil
})
obj := &AdapterFile{
FilePath: filePath,
}
if len(delKeys) > 0 {
obj.Remove(gctx.New(), delKeys...)
}
return obj
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/os/gres"
"github.com/gogf/gf/v2/text/gstr"
@@ -98,6 +99,9 @@ func (c *Cfg) GetFile(filename string, _pathStr ...string) (jsonObj *gjson.Json,
}
}
}
if bytes == nil {
g.Log().Errorf(gctx.New(), "未读取到配置文件:%v", filePath)
}
// 解析 JSON 内容并返回结果
jsonObj, err = gjson.DecodeToJson(bytes)

View File

@@ -12,6 +12,7 @@ import (
"github.com/elastic/go-elasticsearch/v8/typedapi/types"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/util/gconv"
)
var (
@@ -23,6 +24,13 @@ type Elastic struct {
name string
}
type elkBulk struct {
Index struct {
Index string `json:"_index"`
Id string `json:"_id"`
} `json:"index"`
}
func NewV1(name string) *Elastic {
var cfg elasticsearch.Config
_cfg := g.Cfg().MustGetWithEnv(gctx.New(), "elasticsearch")
@@ -52,7 +60,7 @@ func NewV1(name string) *Elastic {
// fmt.Printf("index:%#v\n", resp.Index)
//}
// Set 索引文档
// Set 添加文档索引文档
func (s *Elastic) Set(ctx context.Context, key string, data interface{}) (err error) {
// 添加文档
_, err = s.client.Index(s.name).Id(key).Document(data).Do(ctx)
@@ -61,11 +69,20 @@ func (s *Elastic) Set(ctx context.Context, key string, data interface{}) (err er
// SetBulk 批量添加文档
func (s *Elastic) SetBulk(ctx context.Context, data []any) (err error) {
var save *bulk.Request
save = &bulk.Request{
data,
var save bulk.Request
save = make(bulk.Request, 0)
for _, v := range data {
val := gconv.Map(v)
var saveIndex = elkBulk{}
saveIndex.Index.Index = s.name
if _, ok := val["uuid"]; ok {
saveIndex.Index.Id = val["uuid"].(string)
}
save = append(save, saveIndex)
save = append(save, v)
}
s.client.Bulk().Index(s.name).Request(save).Do(ctx)
//save = data
_, err = s.client.Bulk().Index(s.name).Request(&save).Do(ctx)
return
}

View File

@@ -8,7 +8,7 @@ import (
"github.com/ayflying/utility_go/pkg/notice"
"github.com/ayflying/utility_go/pkg/rank"
"github.com/ayflying/utility_go/pkg/s3"
"github.com/ayflying/utility_go/pkg/websocket"
//"github.com/ayflying/utility_go/pkg/websocket"
"github.com/gogf/gf/v2/os/gcache"
)
@@ -32,9 +32,9 @@ func Rank() *rank.Mod {
return rank.New()
}
func Websocket() *websocket.SocketV1 {
return websocket.NewV1()
}
//func Websocket() *websocket.SocketV1 {
// return websocket.NewV1()
//}
func Config() *config.Cfg {
return config.NewV1()

View File

@@ -24,13 +24,14 @@ var (
// DataType 定义了 S3 配置的数据结构,用于存储访问 S3 所需的各种信息
type DataType struct {
AccessKey string `json:"access_key"` // 访问 S3 的密钥 ID
SecretKey string `json:"secret_key"` // 访问 S3 的密钥
Address string `json:"address"` // S3 服务的地址
Ssl bool `json:"ssl"` // 是否使用 SSL 加密连接
Url string `json:"url"` // S3 服务的访问 URL
BucketName string `json:"bucket_name"` // 默认存储桶名称
AccessKey string `json:"access_key"` // 访问 S3 的密钥 ID
SecretKey string `json:"secret_key"` // 访问 S3 的密钥
Address string `json:"address"` // S3 服务的地址
Ssl bool `json:"ssl"` // 是否使用 SSL 加密连接
Url string `json:"url"` // S3 服务的访问 URL
BucketName string `json:"bucket_name"` // 默认存储桶名称
BucketNameCdn string `json:"bucket_name_cdn"` // CDN 存储桶名称
Provider string `json:"provider"` // S3 服务的提供方
}
// Mod 定义了 S3 模块的结构体,包含一个 S3 客户端实例和配置信息
@@ -64,6 +65,7 @@ func New(_name ...string) *Mod {
&minio.Options{
Creds: credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""),
Secure: cfg.Ssl,
//BucketLookup: minio.BucketLookupPath,
},
)
if err != nil {
@@ -133,11 +135,13 @@ func (s *Mod) ListBuckets() []minio.BucketInfo {
func (s *Mod) PutObject(f io.Reader, name string, bucketName string, _size ...int64) (res minio.UploadInfo, err error) {
// 初始化文件大小为 -1表示将读取文件至结束
var size = int64(-1)
if len(_size) > 0 {
size = _size[0]
}
//if len(_size) > 0 {
// size = _size[0]
//}
// 调用 S3 客户端上传文件,设置内容类型为 "application/octet-stream"
res, err = s.client.PutObject(ctx, bucketName, name, f, size, minio.PutObjectOptions{ContentType: "application/octet-stream"})
res, err = s.client.PutObject(ctx, bucketName, name, f, size, minio.PutObjectOptions{
//ContentType: "application/octet-stream",
})
if err != nil {
// 记录上传错误日志
g.Log().Error(ctx, err)
@@ -146,8 +150,22 @@ func (s *Mod) PutObject(f io.Reader, name string, bucketName string, _size ...in
}
// RemoveObject 从指定存储桶中删除指定名称的文件
// Deprecation: to新方法 RemoveObjectV2
func (s *Mod) RemoveObject(name string, bucketName string) (err error) {
opts := minio.RemoveObjectOptions{
ForceDelete: true,
//GovernanceBypass: true,
//VersionID: "myversionid",
}
// 调用 S3 客户端删除文件
err = s.client.RemoveObject(ctx, bucketName, name, opts)
return
}
// RemoveObjectV2 从指定存储桶中删除指定名称的文件
func (s *Mod) RemoveObjectV2(bucketName string, name string) (err error) {
opts := minio.RemoveObjectOptions{
ForceDelete: true,
//GovernanceBypass: true,
//VersionID: "myversionid",
}
@@ -187,9 +205,15 @@ func (s *Mod) GetUrl(filePath string, defaultFile ...string) (url string) {
filePath = defaultFile[0]
}
if s.cfg.Ssl {
url = get + filePath
} else {
//switch s.cfg.Provider {
//case "qiniu":
// url = get + path.Join(bucketName, filePath)
//default:
// url = get + filePath
//}
url = get + filePath
if !s.cfg.Ssl {
url = get + path.Join(bucketName, filePath)
}
@@ -205,21 +229,43 @@ func (s *Mod) GetPath(url string) (filePath string) {
}
// CopyObject 在指定存储桶内复制文件
// 支持指定源文件和目标文件路径
// bucketName 存储桶名称
// dstStr 目标文件路径
// srcStr 源文件路径
// 返回操作过程中可能出现的错误
func (s *Mod) CopyObject(bucketName string, dstStr string, srcStr string) (err error) {
// 定义目标文件选项
// 定义目标文件的复制选项,包含存储桶名称和目标文件路径
var dst = minio.CopyDestOptions{
Bucket: bucketName,
Object: dstStr,
}
// 定义源文件选项
// 定义源文件的复制选项,包含存储桶名称和源文件路径
var src = minio.CopySrcOptions{
Bucket: bucketName,
Object: srcStr,
}
// 调用 S3 客户端复制文件
// 调用 S3 客户端的 CopyObject 方法,将源文件复制到目标位置
// 忽略返回的复制信息,仅关注是否发生错误
_, err = s.client.CopyObject(ctx, dst, src)
return
}
// Rename 重命名文件
func (s *Mod) Rename(bucketName string, oldName string, newName string) (err error) {
// 复制文件到新的名称
g.Log().Debugf(nil, "仓库=%v,rename %s to %s", bucketName, oldName, newName)
err = s.CopyObject(bucketName, newName, oldName)
if err != nil {
g.Log().Error(ctx, err)
return
}
// 删除原始文件
err = s.RemoveObjectV2(bucketName, oldName)
if err != nil {
g.Log().Error(ctx, err)
return
}
return
}