修改路径

This commit is contained in:
ayflying
2025-02-28 17:45:44 +08:00
parent 9f337df9de
commit 74a746bc47
29 changed files with 7 additions and 7 deletions

37
package/aycache/cache.go Normal file
View File

@@ -0,0 +1,37 @@
package aycache
import (
"github.com/ayflying/utility_go/package/aycache/drive"
"github.com/gogf/gf/v2/os/gcache"
)
type Mod struct {
client *gcache.Cache
}
//func NewV1(_name ...string) *cache.Mod {
// return pgk.Cache
//}
func New(_name ...string) gcache.Adapter {
var cacheAdapterObj gcache.Adapter
var name = "cache"
if len(_name) > 0 {
name = _name[0]
}
switch name {
case "cache":
cacheAdapterObj = NewAdapterMemory()
case "redis":
cacheAdapterObj = NewAdapterRedis()
case "file":
cacheAdapterObj = NewAdapterFile("runtime/cache")
case "es":
cacheAdapterObj = drive.NewAdapterElasticsearch([]string{"http://127.0.0.1:9200"})
}
//var client = gcache.New()
//client.SetAdapter(cacheAdapterObj)
return cacheAdapterObj
}

View File

@@ -0,0 +1,119 @@
package drive
import (
"context"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/os/gcache"
"time"
)
type AdapterElasticsearch struct {
//FilePath string
Addresses []string
}
func (a AdapterElasticsearch) Set(ctx context.Context, key interface{}, value interface{}, duration time.Duration) error {
//TODO implement me
panic("implement me")
}
func (a AdapterElasticsearch) SetMap(ctx context.Context, data map[interface{}]interface{}, duration time.Duration) error {
//TODO implement me
panic("implement me")
}
func (a AdapterElasticsearch) SetIfNotExist(ctx context.Context, key interface{}, value interface{}, duration time.Duration) (ok bool, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterElasticsearch) SetIfNotExistFunc(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (ok bool, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterElasticsearch) SetIfNotExistFuncLock(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (ok bool, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterElasticsearch) Get(ctx context.Context, key interface{}) (*gvar.Var, error) {
//TODO implement me
panic("implement me")
}
func (a AdapterElasticsearch) GetOrSet(ctx context.Context, key interface{}, value interface{}, duration time.Duration) (result *gvar.Var, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterElasticsearch) 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 AdapterElasticsearch) 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 AdapterElasticsearch) Contains(ctx context.Context, key interface{}) (bool, error) {
//TODO implement me
panic("implement me")
}
func (a AdapterElasticsearch) Size(ctx context.Context) (size int, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterElasticsearch) Data(ctx context.Context) (data map[interface{}]interface{}, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterElasticsearch) Keys(ctx context.Context) (keys []interface{}, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterElasticsearch) Values(ctx context.Context) (values []interface{}, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterElasticsearch) Update(ctx context.Context, key interface{}, value interface{}) (oldValue *gvar.Var, exist bool, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterElasticsearch) UpdateExpire(ctx context.Context, key interface{}, duration time.Duration) (oldDuration time.Duration, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterElasticsearch) GetExpire(ctx context.Context, key interface{}) (time.Duration, error) {
//TODO implement me
panic("implement me")
}
func (a AdapterElasticsearch) Remove(ctx context.Context, keys ...interface{}) (lastValue *gvar.Var, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterElasticsearch) Clear(ctx context.Context) error {
//TODO implement me
panic("implement me")
}
func (a AdapterElasticsearch) Close(ctx context.Context) error {
//TODO implement me
panic("implement me")
}
func NewAdapterElasticsearch(addresses []string) gcache.Adapter {
return &AdapterElasticsearch{
Addresses: addresses,
}
}

134
package/aycache/file.go Normal file
View File

@@ -0,0 +1,134 @@
package aycache
import (
"context"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/os/gcache"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/util/gconv"
"path"
"strings"
"time"
)
type AdapterFile struct {
FilePath string
}
func (a AdapterFile) Set(ctx context.Context, key interface{}, value interface{}, duration time.Duration) error {
//defer a.handleLruKey(ctx, key)
//expireTime := a.getInternalExpire(duration)
//a.data.Set(key, memoryDataItem{
// a: value,
// a: expireTime,
//})
//c.eventList.PushBack(&adapterMemoryEvent{
// k: key,
// e: expireTime,
//})
arr := strings.Split(":", gconv.String(key))
fileName := path.Join(arr...)
return gfile.PutBytes(fileName, gconv.Bytes(value))
}
func (a AdapterFile) SetMap(ctx context.Context, data map[interface{}]interface{}, duration time.Duration) 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) {
//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) {
//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) {
//TODO implement me
panic("implement me")
}
func (a AdapterFile) Get(ctx context.Context, key interface{}) (*gvar.Var, 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) {
//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) 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) 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 NewAdapterFile(filePath string) gcache.Adapter {
return &AdapterFile{
FilePath: filePath,
}
}

15
package/aycache/memory.go Normal file
View File

@@ -0,0 +1,15 @@
package aycache
import (
"github.com/gogf/gf/v2/os/gcache"
)
var adapterMemoryClient = gcache.New()
// NewAdapterMemory 创建并返回一个新的内存缓存对象。
func NewAdapterMemory() gcache.Adapter {
//if adapterMemoryClient == nil {
// adapterMemoryClient = gcache.New()
//}
return adapterMemoryClient
}

View File

@@ -0,0 +1,120 @@
package aycache
import (
"context"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/database/gredis"
"github.com/gogf/gf/v2/os/gcache"
"time"
)
// AdapterRedis is the gcache adapter implements using Redis server.
type AdapterMemcached struct {
//redis *gredis.Redis
//client
}
func (a AdapterMemcached) Set(ctx context.Context, key interface{}, value interface{}, duration time.Duration) error {
//TODO implement me
panic("implement me")
}
func (a AdapterMemcached) SetMap(ctx context.Context, data map[interface{}]interface{}, duration time.Duration) error {
//TODO implement me
panic("implement me")
}
func (a AdapterMemcached) SetIfNotExist(ctx context.Context, key interface{}, value interface{}, duration time.Duration) (ok bool, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterMemcached) SetIfNotExistFunc(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (ok bool, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterMemcached) SetIfNotExistFuncLock(ctx context.Context, key interface{}, f gcache.Func, duration time.Duration) (ok bool, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterMemcached) Get(ctx context.Context, key interface{}) (*gvar.Var, error) {
//TODO implement me
panic("implement me")
}
func (a AdapterMemcached) GetOrSet(ctx context.Context, key interface{}, value interface{}, duration time.Duration) (result *gvar.Var, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterMemcached) 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 AdapterMemcached) 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 AdapterMemcached) Contains(ctx context.Context, key interface{}) (bool, error) {
//TODO implement me
panic("implement me")
}
func (a AdapterMemcached) Size(ctx context.Context) (size int, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterMemcached) Data(ctx context.Context) (data map[interface{}]interface{}, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterMemcached) Keys(ctx context.Context) (keys []interface{}, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterMemcached) Values(ctx context.Context) (values []interface{}, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterMemcached) Update(ctx context.Context, key interface{}, value interface{}) (oldValue *gvar.Var, exist bool, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterMemcached) UpdateExpire(ctx context.Context, key interface{}, duration time.Duration) (oldDuration time.Duration, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterMemcached) GetExpire(ctx context.Context, key interface{}) (time.Duration, error) {
//TODO implement me
panic("implement me")
}
func (a AdapterMemcached) Remove(ctx context.Context, keys ...interface{}) (lastValue *gvar.Var, err error) {
//TODO implement me
panic("implement me")
}
func (a AdapterMemcached) Clear(ctx context.Context) error {
//TODO implement me
panic("implement me")
}
func (a AdapterMemcached) Close(ctx context.Context) error {
//TODO implement me
panic("implement me")
}
// NewAdapterRedis creates and returns a new memory cache object.
func NewAdapterMemcached(redis *gredis.Redis) gcache.Adapter {
return &AdapterMemcached{}
}

18
package/aycache/redis.go Normal file
View File

@@ -0,0 +1,18 @@
package aycache
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcache"
)
var adapterRedisClient gcache.Adapter
var adapterRedisCache = gcache.New()
func NewAdapterRedis() gcache.Adapter {
if adapterRedisClient == nil {
adapterRedisClient = gcache.NewAdapterRedis(g.Redis("default"))
adapterRedisCache.SetAdapter(adapterRedisClient)
}
return adapterRedisCache
}

View File

@@ -0,0 +1,101 @@
package elasticsearch
import (
"context"
"encoding/json"
"fmt"
"github.com/elastic/go-elasticsearch/v8"
)
var (
es *elasticsearch.TypedClient
)
type elastic struct {
client *elasticsearch.TypedClient
}
func New(name ...string) *elastic {
// ES 配置
cfg := elasticsearch.Config{
Addresses: []string{
"http://ay.cname.com:9200",
},
}
if es == nil {
var err error
es, err = elasticsearch.NewTypedClient(cfg)
if err != nil {
fmt.Printf("elasticsearch.NewTypedClient failed, err:%v\n", err)
return &elastic{}
}
}
return &elastic{
client: es,
}
}
// createIndex 创建索引
func (s *elastic) CreateIndex(name string) {
resp, err := s.client.Indices.
Create(name).
Do(context.Background())
if err != nil {
fmt.Printf("create index failed, err:%v\n", err)
return
}
fmt.Printf("index:%#v\n", resp.Index)
}
// indexDocument 索引文档
func (s *elastic) IndexDocument(name string, key string, data interface{}) {
// 添加文档
resp, err := s.client.Index(name).
Id(key).
Document(data).
Do(context.Background())
if err != nil {
fmt.Printf("indexing document failed, err:%v\n", err)
return
}
fmt.Printf("result:%#v\n", resp.Result)
}
// getDocument 获取文档
func (s *elastic) GetDocument(name string, id string) (res json.RawMessage) {
resp, err := s.client.Get(name, id).
Do(context.Background())
if err != nil {
fmt.Printf("get document by id failed, err:%v\n", err)
return
}
fmt.Printf("fileds:%s\n", resp.Source_)
res = resp.Source_
return
}
// updateDocument 更新文档
func (s *elastic) UpdateDocument(name string, key string, data interface{}) {
resp, err := s.client.Update(name, key).
Doc(data). // 使用结构体变量更新
Do(context.Background())
if err != nil {
fmt.Printf("update document failed, err:%v\n", err)
return
}
fmt.Printf("result:%v\n", resp.Result)
}
// deleteDocument 删除 document
func (s *elastic) DeleteDocument(name string, key string) {
resp, err := s.client.Delete(name, key).
Do(context.Background())
if err != nil {
fmt.Printf("delete document failed, err:%v\n", err)
return
}
fmt.Printf("result:%v\n", resp.Result)
}

53
package/excel/download.go Normal file
View File

@@ -0,0 +1,53 @@
package excel
import (
"context"
"fmt"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"github.com/xuri/excelize/v2"
)
func Data2Excel(ctx context.Context, data []map[string]interface{}) (file string) {
// 创建一个新的 Excel 文件
f := excelize.NewFile()
var sheetName = "Sheet1"
f.NewSheet(sheetName)
//// 准备数据
//data = []map[string]interface{}{
// {"Name": "Alice", "Age": 30, "City": "New York"},
// {"Name": "Bob", "Age": 25, "City": "Los Angeles"},
// {"Name": "Charlie", "Age": 35, "City": "Chicago"},
//}
//写入头部
var colIndex = 0
var headers []string
for header := range data[0] {
//追加头部名字
headers = append(headers, header)
cell, _ := excelize.CoordinatesToCellName(colIndex+1, 1) // 表头在第一行
f.SetCellValue(sheetName, cell, header)
colIndex++
}
// 写入数据
for rowIndex, record := range data {
for colIndex, header := range headers {
cell, _ := excelize.CoordinatesToCellName(colIndex+1, rowIndex+2) // 数据从第二行开始
f.SetCellValue(sheetName, cell, record[header]) // 通过表头获取数据
}
}
// 保存 Excel 文件
saveName := fmt.Sprintf("runtime/uploads/out_%v.xlsx", gtime.Now().Nanosecond())
if err := f.SaveAs(saveName); err != nil {
g.Log().Fatal(ctx, err)
}
//下载excel
//g.RequestFromCtx(ctx).Response.ServeFileDownload(saveName)
return saveName
}

179
package/excel/excel.go Normal file
View File

@@ -0,0 +1,179 @@
package excel
import (
"context"
"github.com/ayflying/excel2json"
"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/gtime"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
"path"
"strconv"
"strings"
"time"
)
var (
shadiao = []string{",", ":"}
)
type FileItem struct {
Name string `json:"name" dc:"配置文件名"`
Filename string `json:"filename" dc:"文件名"`
Tabs []string `json:"tabs" dc:"页签"`
Items []string `json:"items" dc:"道具字段"`
ItemsMap []string `json:"items_map" dc:"道具字段map格式"`
Slice map[string]string `json:"slice" dc:"切片"`
}
type Excel struct {
Header int //表头行数
Key int //key列
}
func New(header int, key int) *Excel {
return &Excel{
Header: header,
Key: key,
}
}
func (s *Excel) ExcelLoad(ctx context.Context, fileItem *FileItem, mainPath string) (runTime time.Duration) {
startTime := gtime.Now()
filepath := path.Join("manifest/game", fileItem.Name)
//如果filepath文件不存在跳过
if !gfile.Exists(path.Join(mainPath, fileItem.Filename)) {
return
}
//假设我们有一个命令行工具比如dirWindows环境下列出目录内容
var tempJson []interface{}
for k, v2 := range fileItem.Tabs {
sheet := v2
if k == 0 {
sheet = v2
}
//导出json
excel2json.Excel(path.Join(mainPath, fileItem.Filename),
filepath, s.Header, s.Key, sheet)
//如果配置了道具字段,则进行转换
//g.Log().Info(ctx, "当前任务表=%v,items=%v", v.Name, v.Items)
fileBytes := gfile.GetBytes(filepath)
arr, _ := gjson.DecodeToJson(fileBytes)
list := arr.Array()
//格式化item格式
if len(fileItem.Items) > 0 {
list = s.itemsFormat(list, fileItem.Items)
}
if len(fileItem.ItemsMap) > 0 {
list = s.itemsMapFormat(list, gconv.Strings(fileItem.ItemsMap))
}
//格式化切片修改
if len(fileItem.Slice) > 0 {
list = s.sliceFormat(list, fileItem.Slice)
}
//拼接json
tempJson = append(tempJson, list...)
fileBytes, _ = gjson.MarshalIndent(tempJson, "", "\t")
err := gfile.PutBytes(filepath, fileBytes)
if err != nil {
g.Log().Error(ctx, err)
}
}
runTime = gtime.Now().Sub(startTime)
return
}
func (s *Excel) itemsFormat(list []interface{}, Items []string) []interface{} {
for k2, v2 := range list {
for k3, v3 := range v2.(g.Map) {
if gstr.InArray(Items, k3) {
if _, ok := v3.(string); ok {
list[k2].(g.Map)[k3] = Spilt2Item(v3.(string))
} else {
g.Log().Errorf(gctx.New(), "当前类型断言失败:%v,list=%v", v3, v2)
}
}
}
}
return list
}
func (s *Excel) itemsMapFormat(list []interface{}, Items []string) []interface{} {
for k2, v2 := range list {
for k3, v3 := range v2.(g.Map) {
if gstr.InArray(Items, k3) {
if _, ok := v3.(string); ok {
get := Spilt2Item(v3.(string))
list[k2].(g.Map)[k3] = s.Items2Map(get)
} else {
g.Log().Errorf(gctx.New(), "当前类型断言失败:%v,list=%v", v3, v2)
}
}
}
}
return list
}
func (s *Excel) sliceFormat(list []interface{}, Slice map[string]string) []interface{} {
for s1, s2 := range Slice {
for k2, v2 := range list {
for k3, v3 := range v2.(g.Map) {
//判断是否存在
if s1 != k3 {
continue
}
if gconv.String(v3) == "" {
list[k2].(g.Map)[k3] = []string{}
continue
}
var parts []string
//断言是否成功
if get, ok := v3.(string); !ok {
//g.Log().Errorf(gctx.New(), "当前类型断言失败:%v", v3)
parts = []string{gconv.String(v3)}
} else {
for _, v := range shadiao {
get = strings.ReplaceAll(get, v, "|")
}
parts = strings.Split(get, "|") // 分割字符串
}
switch s2 {
case "int":
var temp = make([]int, len(parts))
for k, v := range parts {
temp[k], _ = strconv.Atoi(v)
}
list[k2].(g.Map)[k3] = temp
case "int64":
var temp = make([]int64, len(parts))
for k, v := range parts {
temp[k], _ = strconv.ParseInt(v, 10, 64)
}
case "float64":
var temp = make([]float64, len(parts))
for k, v := range parts {
temp[k], _ = strconv.ParseFloat(v, 64)
}
list[k2].(g.Map)[k3] = temp
default:
list[k2].(g.Map)[k3] = parts
}
}
}
}
return list
}

58
package/excel/tools.go Normal file
View File

@@ -0,0 +1,58 @@
package excel
import (
"github.com/xuri/excelize/v2"
"log"
"strconv"
"strings"
)
// Excel2Slice 读取excel文件导入为切片
func Excel2Slice(filePath string, _sheet ...string) [][]string {
excelObj, err := excelize.OpenFile(filePath)
if err != nil {
log.Fatalf("无法打开Excel文件: %v", err)
}
defer excelObj.Close()
var sheet string
if len(_sheet) == 0 {
sheet = excelObj.GetSheetList()[0]
} else {
sheet = _sheet[0]
}
res, err := excelObj.GetRows(sheet)
return res
}
// 字符串转道具类型
func Spilt2Item(str string) (result [][]int64) {
for _, v := range shadiao {
str = strings.ReplaceAll(str, v, "|")
//parts = append(parts, strings.Split(str, v)...) // 分割字符串
}
//var parts []string
parts := strings.Split(str, "|") // 分割字符串
if parts == nil {
parts = []string{str}
}
for i := 0; i < len(parts)-1; i += 2 {
num1, _ := strconv.ParseInt(parts[i], 10, 64)
num2, _ := strconv.ParseInt(parts[i+1], 10, 64)
pair := []int64{num1, num2}
result = append(result, pair)
}
return
}
// 道具格式转map
func (s *Excel) Items2Map(items [][]int64) (list map[int64]int64) {
list = make(map[int64]int64)
for _, v := range items {
list[v[0]] = v[1]
}
return
}

42
package/pay/alipay.go Normal file
View File

@@ -0,0 +1,42 @@
package pay
import (
"github.com/go-pay/gopay"
"github.com/go-pay/gopay/alipay"
"github.com/gogf/gf/v2/frame/g"
)
type AliPay struct {
Client *alipay.Client
}
func Alipay() *AliPay {
var pay = &AliPay{}
var err error
cfg, err := g.Cfg().Get(ctx, "pay.alipay")
cfgMap := cfg.MapStrStr()
appId := cfgMap["appid"]
privateKey := cfgMap["privateKey"]
isProd, _ := g.Cfg().Get(ctx, "pay.alipay.isProd")
// 初始化支付宝客户端
// appid应用ID
// privateKey应用私钥支持PKCS1和PKCS8
// isProd是否是正式环境沙箱环境请选择新版沙箱应用。
pay.Client, err = alipay.NewClient(appId, privateKey, isProd.Bool())
if err != nil {
g.Log().Error(ctx, err)
return nil
}
// 自定义配置http请求接收返回结果body大小默认 10MB
//pay.Client.SetBodySize() // 没有特殊需求,可忽略此配置
// 打开Debug开关输出日志默认关闭
pay.Client.DebugSwitch = gopay.DebugOn
pay.Client.SetCharset(alipay.UTF8). // 设置字符编码,不设置默认 utf-8
SetSignType(alipay.RSA2) // 设置签名类型,不设置默认 RSA2
return pay
}

156
package/pay/apple.go Normal file
View File

@@ -0,0 +1,156 @@
package pay
import (
"context"
"github.com/go-pay/gopay/apple"
"github.com/gogf/gf/v2/errors/gerror"
"strings"
"sync"
"time"
)
// ApplePay 苹果支付
// 这是一个用于处理苹果支付的结构体。
type ApplePay struct {
pass string // pass 是用于苹果支付过程中的密钥。
lock sync.RWMutex // lock 用于确保在并发访问或修改 pass 时的安全性。
}
// Init 是ApplePay类型的初始化函数。
//
// @Description: 对ApplePay对象进行初始化将传入的数据存储到对象中。
// @receiver p: ApplePay对象的指针用于接收初始化操作。
// @param data: 一个字节切片,包含需要初始化的数据。
func (p *ApplePay) Init(data []byte) {
p.lock.Lock() // 加锁以保证在多线程环境下的线程安全
defer p.lock.Unlock() // 确保在函数执行完毕退出时自动解锁,避免死锁
p.pass = string(data) // 将传入的字节切片数据转换为字符串并赋值给pass字段
}
// VerifyPay 验证苹果支付
//
// @Description: 验证苹果支付的收据信息,以确认支付的有效性。
// @receiver p *ApplePay: ApplePay对象用于执行验证支付的操作。
// @param userId uint64: 用户ID。
// @param OrderId string: 订单ID。
// @param package1 string: 付费产品的包装名称。
// @param subscriptionID string: 订阅ID。
// @param purchaseToken string: 购买令牌,用于苹果服务器的收据验证。
// @param isDebug bool: 是否为调试模式决定使用哪个验证URL。
// @param cb func(string) error: 回调函数用于处理验证成功后的产品ID。
// @return error: 返回错误信息,如果验证过程中出现错误,则返回相应的错误信息。
func (p *ApplePay) VerifyPay(userId uint64, OrderId, package1, subscriptionID, purchaseToken string, isDebug bool, cb func(string) error) error {
p.lock.RLock() // 加读锁,保证并发安全
defer p.lock.RUnlock() // 解读锁,确保函数执行完毕后释放锁
// 根据是否为调试模式选择验证URL
url := apple.UrlProd
if isDebug {
url = apple.UrlSandbox
}
// 向苹果服务器验证收据
info, err := apple.VerifyReceipt(context.Background(), url, p.pass, purchaseToken)
if err != nil {
// 如果验证失败,则返回错误
return err
}
// 检查收据验证的状态
if info.Status == 0 {
// 检查收据中是否包含内购信息
if len(info.Receipt.InApp) <= 0 {
return gerror.Wrap(err, "info.Receipt.InApp = 0")
}
// 调用回调函数处理商品ID
if err := cb(info.Receipt.InApp[0].ProductId); err != nil {
// 如果回调处理失败,则返回错误
return err
}
} else {
// 如果收据验证状态异常,则返回状态错误信息
return gerror.Wrapf(err, "status err = %v", info.Status)
}
return nil
}
// VerifyPayV1 验证苹果支付的交易
//
// @Description:
// @receiver p
// @param purchaseToken
// @param isDebug
// @param cb
// @return error
func (p *ApplePay) VerifyPayV1(purchaseToken string, isDebug bool, cb func(string, string) error) error {
p.lock.RLock() // 加读锁,确保并发安全
defer p.lock.RUnlock() // 结束时自动释放读锁
// 根据调试模式选择验证服务的URL
url := apple.UrlProd
if isDebug {
url = apple.UrlSandbox
}
// 向苹果服务器验证收据
info, err := apple.VerifyReceipt(context.Background(), url, p.pass, purchaseToken)
if err != nil {
// 验证失败,返回错误
return err
}
// 检查验证结果状态
if info.Status == 0 {
// 验证成功,检查收据中是否有内购信息
if len(info.Receipt.InApp) <= 0 {
// 收据中无内购信息,返回错误
return gerror.Wrap(err, "info.Receipt.InApp = 0")
}
// 调用回调函数,处理内购产品信息
if err := cb(info.Receipt.InApp[0].ProductId, info.Receipt.InApp[0].OriginalTransactionId); err != nil {
// 回调函数执行失败,返回错误
return gerror.Wrap(err, "回调函数执行失败")
}
} else {
// 验证结果状态异常,返回错误
return gerror.Wrapf(err, "status err = %v", info.Status)
}
// 验证成功返回nil
return nil
}
// VerifyPayTest 用于验证苹果支付的测试购买。
//
// @Description:
// @receiver p
// @param purchaseToken
// @return interface{}
// @return error
func (p *ApplePay) VerifyPayTest(purchaseToken string) (interface{}, error) {
// 使用沙箱环境的URL进行验证
url := apple.UrlSandbox
// 调用apple.VerifyReceipt进行收据验证
return apple.VerifyReceipt(context.Background(), url, p.pass, purchaseToken)
}
// GetTime 根据提供的 timer 字符串解析时间,格式为 "YYYY-MM-DD HH:MM:SS ZZZ",若解析失败则返回当前时间
//
// @Description: 根据指定格式解析时间字符串,如果解析失败或者格式不正确,则返回当前时间。
// @param timer 时间字符串,格式为 "YYYY-MM-DD HH:MM:SS ZZZ",其中 ZZZ 为时区标识。
// @return time.Time 解析得到的时间,若失败则返回当前时间。
func GetTime(timer string) time.Time {
// 将 timer 字符串按空格分割为年月日和时分秒两部分
ts := strings.Split(timer, "")
// 如果分割后的数组长度不为3则说明格式不正确返回当前时间
if len(ts) != 3 {
return time.Now()
}
// 尝试加载指定时区信息
location, err := time.LoadLocation(ts[2])
// 如果加载时区失败,则返回当前时间
if err != nil {
return time.Now()
}
// 使用指定时区解析时间字符串
t, err := time.ParseInLocation("2006-01-02 15:04:05 MST", ts[0]+" "+ts[1], location)
// 如果解析失败,则返回当前时间
if err != nil {
return time.Now()
}
// 将解析得到的时间转换为本地时间并返回
return t.In(time.Local)
}

668
package/pay/apple/apple.go Normal file
View File

@@ -0,0 +1,668 @@
package apple
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"sort"
"strings"
"time"
)
var (
MarshalErr = errors.New("marshal error")
UnmarshalErr = errors.New("unmarshal error")
)
const (
// is the URL when testing your app in the sandbox and while your application is in review
UrlSandbox = "https://sandbox.itunes.apple.com/verifyReceipt"
// is the URL when your app is live in the App Store
UrlProd = "https://buy.itunes.apple.com/verifyReceipt"
)
type RequestType string
const (
GET = "GET"
POST = "POST"
PUT = "PUT"
DELETE = "DELETE"
PATCH = "PATCH"
TypeJSON RequestType = "json"
TypeXML RequestType = "xml"
TypeUrlencoded RequestType = "urlencoded"
TypeForm RequestType = "form"
TypeFormData RequestType = "form-data"
TypeMultipartFormData RequestType = "multipart-form-data"
)
var types = map[RequestType]string{
TypeJSON: "application/json",
TypeXML: "application/xml",
TypeUrlencoded: "application/x-www-form-urlencoded",
TypeForm: "application/x-www-form-urlencoded",
TypeFormData: "application/x-www-form-urlencoded",
TypeMultipartFormData: "multipart/form-data",
}
type File struct {
Name string `json:"name"`
Content []byte `json:"content"`
}
type Client struct {
HttpClient *http.Client
Transport *http.Transport
Header http.Header
Timeout time.Duration
Host string
bodySize int // body size limit(MB), default is 10MB
url string
method string
requestType RequestType
FormString string
ContentType string
unmarshalType string
multipartBodyMap map[string]any
jsonByte []byte
err error
}
// VerifyRequest 校验请求体
// https://developer.apple.com/documentation/appstorereceipts/requestbody
type VerifyRequest struct {
// Receipt app解析出的票据信息
Receipt string `json:"receipt-data"`
// Password App的秘钥
Password string `json:"password"`
// ExcludeOldTranscations Set this value to true for the response to include only the latest renewal transaction for any subscriptions. Use this field only for app receipts that contain auto-renewable subscriptions.
ExcludeOldTranscations bool `json:"exclude-old-transactions"`
}
// VerifyResponse 校验响应体
// https://developer.apple.com/documentation/appstorereceipts/responsebody
type VerifyResponse struct {
// Environment is which the receipt was generated. Possible values: Sandbox, Production
Environment string `json:"environment"`
// IsRetryable is an indicator that an error occurred during the request. A value of 1 indicates a temporary issue; retry validation for this receipt at a later time. A value of 0 indicates an unresolvable issue; do not retry validation for this receipt. Only applicable to status codes 21100-21199.
IsRetryable bool `json:"is-retryable"`
// LatestReceipt The latest Base64 encoded app receipt. Only returned for receipts that contain auto-renewable subscriptions
LatestReceipt string `json:"latest_receipt,omitempty"`
// LatestReceiptInfo is an array that contains all in-app purchase transactions. This excludes transactions for consumable products that have been marked as finished by your app. Only returned for receipts that contain auto-renewable subscriptions.
LatestReceiptInfo []*LatestReceiptInfo `json:"latest_receipt_info,omitempty"`
// PendingRenewalInfo ,in the JSON file, an array where each element contains the pending renewal information for each auto-renewable subscription identified by the product_id. Only returned for app receipts that contain auto-renewable subscriptions.
PendingRenewalInfo []*PendingRenewalInfo `json:"pending_renewal_info,omitempty"`
// Receipt is a JSON representation of the receipt that was sent for verification.
Receipt *Receipt `json:"receipt,omitempty"`
// Status either 0 if the receipt is valid, or a status code if there is an error. The status code reflects the status of the app receipt as a whole. See status for possible status codes and descriptions.
// =0时就表示校验成功
Status int `json:"status"`
}
// LatestReceiptInfo
// https://developer.apple.com/documentation/appstorereceipts/responsebody/latest_receipt_info
type LatestReceiptInfo struct {
// The time Apple customer support canceled a transaction, in a date-time format similar to the ISO 8601. This field is only present for refunded transactions.
CancellationDate string `json:"cancellation_date"`
// The time Apple customer support canceled a transaction, or the time an auto-renewable subscription plan was upgraded, in UNIX epoch time format, in milliseconds. This field is only present for refunded transactions. Use this time format for processing dates.
// https://developer.apple.com/documentation/appstorereceipts/cancellation_date_ms
CancellationDateTimestamp string `json:"cancellation_date_ms"`
// The time Apple customer support canceled a transaction, in the Pacific Time zone. This field is only present for refunded transactions.
CancellationDatePST string `json:"cancellation_date_pst"`
// The reason for a refunded transaction. When a customer cancels a transaction, the App Store gives them a refund and provides a value for this key. A value of “1” indicates that the customer canceled their transaction due to an actual or perceived issue within your app. A value of “0” indicates that the transaction was canceled for another reason; for example, if the customer made the purchase accidentally.
// Possible values: 1, 0
CancellationReason string `json:"cancellation_reason"`
// The time a subscription expires or when it will renew, in a date-time format similar to the ISO 8601.
ExpiresDate string `json:"expires_date"`
// The time a subscription expires or when it will renew, in UNIX epoch time format, in milliseconds. Use this time format for processing dates.
// https://developer.apple.com/documentation/appstorereceipts/expires_date_ms
ExpiresDateTimestamp string `json:"expires_date_ms"`
// The time a subscription expires or when it will renew, in the Pacific Time zone.
ExpiresDatePST string `json:"expires_date_pst"`
// A value that indicates whether the user is the purchaser of the product, or is a family member with access to the product through Family Sharing.
// https://developer.apple.com/documentation/appstorereceipts/in_app_ownership_type
InAppOwnershipType string `json:"in_app_ownership_type"`
// An indicator of whether an auto-renewable subscription is in the introductory price period.
// Possible values: true, false
IsInIntroOfferPeriod string `json:"is_in_intro_offer_period"`
// An indicator of whether a subscription is in the free trial period.
// https://developer.apple.com/documentation/appstorereceipts/is_trial_period
IsTrialPeriod string `json:"is_trial_period"`
// An indicator that a subscription has been canceled due to an upgrade. This field is only present for upgrade transactions.
// Value: true
IsUpgraded string `json:"is_upgraded"`
// The reference name of a subscription offer that you configured in App Store Connect. This field is present when a customer redeemed a subscription offer code. For more information about offer codes
// https://help.apple.com/app-store-connect/#/dev6a098e4b1
// https://developer.apple.com/documentation/storekit/original_api_for_in-app_purchase/subscriptions_and_offers/implementing_offer_codes_in_your_app
OfferCodeRefName string `json:"offer_code_ref_name"`
// The time of the original app purchase, in a date-time format similar to ISO 8601.
OriginalPurchaseDate string `json:"original_purchase_date"`
// The time of the original app purchase, in UNIX epoch time format, in milliseconds. Use this time format for processing dates. For an auto-renewable subscription, this value indicates the date of the subscriptions initial purchase. The original purchase date applies to all product types and remains the same in all transactions for the same product ID. This value corresponds to the original transactions transactionDate property in StoreKit.
OriginalPurchaseDateTimestamp string `json:"original_purchase_date_ms"`
// The time of the original app purchase, in the Pacific Time zone.
OriginalPurchaseDatePST string `json:"original_purchase_date_pst"`
// The transaction identifier of the original purchase.
// https://developer.apple.com/documentation/appstorereceipts/original_transaction_id
OriginalTransactionId string `json:"original_transaction_id"`
// The unique identifier of the product purchased. You provide this value when creating the product in App Store Connect, and it corresponds to the productIdentifier property of the SKPayment object stored in the transactions payment property.
ProductId string `json:"product_id"`
// The identifier of the subscription offer redeemed by the user.
// https://developer.apple.com/documentation/appstorereceipts/promotional_offer_id
PromotionalOfferId string `json:"promotional_offer_id"`
// The time the App Store charged the users account for a purchased or restored product, or the time the App Store charged the users account for a subscription purchase or renewal after a lapse, in a date-time format similar to ISO 8601.
PurchaseDate string `json:"purchase_date"`
// For consumable, non-consumable, and non-renewing subscription products, the time the App Store charged the users account for a purchased or restored product, in the UNIX epoch time format, in milliseconds. For auto-renewable subscriptions, the time the App Store charged the users account for a subscription purchase or renewal after a lapse, in the UNIX epoch time format, in milliseconds. Use this time format for processing dates.
PurchaseDateTimestamp string `json:"purchase_date_ms"`
// The time the App Store charged the users account for a purchased or restored product, or the time the App Store charged the users account for a subscription purchase or renewal after a lapse, in the Pacific Time zone.
PurchaseDatePST string `json:"purchase_date_pst"`
// The number of consumable products purchased. This value corresponds to the quantity property of the SKPayment object stored in the transactions payment property. The value is usually “1” unless modified with a mutable payment. The maximum value is 10.
Quantity string `json:"quantity"`
// The identifier of the subscription group to which the subscription belongs. The value for this field is identical to the subscriptionGroupIdentifier property in SKProduct.
// https://developer.apple.com/documentation/storekit/skproduct/2981047-subscriptiongroupidentifier
SubscriptionGroupIdentifier string `json:"subscription_group_identifier"`
// A unique identifier for purchase events across devices, including subscription-renewal events. This value is the primary key for identifying subscription purchases.
WebOrderLineItemId string `json:"web_order_line_item_id"`
// A unique identifier for a transaction such as a purchase, restore, or renewal
TransactionId string `json:"transaction_id"`
// https://developer.apple.com/documentation/appstorereceipts/app_account_token
AppAccountToken string `json:"app_account_token"`
}
// PendingRenewalInfo
// https://developer.apple.com/documentation/appstorereceipts/responsebody/pending_renewal_info
type PendingRenewalInfo struct {
// The value for this key corresponds to the productIdentifier property of the product that the customers subscription renews.
AutoRenewProductId string `json:"auto_renew_product_id"`
// The current renewal status for the auto-renewable subscription.
// https://developer.apple.com/documentation/appstorereceipts/auto_renew_status
AutoRenewStatus string `json:"auto_renew_status"`
// The reason a subscription expired. This field is only present for a receipt that contains an expired auto-renewable subscription.
// https://developer.apple.com/documentation/appstorereceipts/expiration_intent
ExpirationIntent string `json:"expiration_intent"`
// The time at which the grace period for subscription renewals expires, in a date-time format similar to the ISO 8601.
GracePeriodExpiresDate string `json:"grace_period_expires_date"`
// The time at which the grace period for subscription renewals expires, in UNIX epoch time format, in milliseconds. This key is only present for apps that have Billing Grace Period enabled and when the user experiences a billing error at the time of renewal. Use this time format for processing dates.
GracePeriodExpiresDateTimestamp string `json:"grace_period_expires_date_ms"`
// The time at which the grace period for subscription renewals expires, in the Pacific Time zone.
GracePeriodExpiresDatePST string `json:"grace_period_expires_date_pst"`
// A flag that indicates Apple is attempting to renew an expired subscription automatically. This field is only present if an auto-renewable subscription is in the billing retry state.
// https://developer.apple.com/documentation/appstorereceipts/is_in_billing_retry_period
IsInBillingRetryPeriod string `json:"is_in_billing_retry_period"`
// The reference name of a subscription offer that you configured in App Store Connect. This field is present when a customer redeemed a subscription offer code
// https://developer.apple.com/documentation/appstorereceipts/offer_code_ref_name
OfferCodeRefName string `json:"offer_code_ref_name"`
// The transaction identifier of the original purchase.
OriginalTransactionId string `json:"original_transaction_id"`
// The price consent status for a subscription price increase. This field is only present if the customer was notified of the price increase. The default value is "0" and changes to "1" if the customer consents.
// Possible values: 1, 0
PriceConsentStatus string `json:"price_consent_status"`
// The unique identifier of the product purchased. You provide this value when creating the product in App Store Connect, and it corresponds to the productIdentifier property of the SKPayment object stored in the transaction's payment property.
// https://developer.apple.com/documentation/storekit/skpayment
ProductId string `json:"product_id"`
// The identifier of the promotional offer for an auto-renewable subscription that the user redeemed. You provide this value in the Promotional Offer Identifier field when you create the promotional offer in App Store Connect.
// https://developer.apple.com/documentation/appstorereceipts/promotional_offer_id
Promotionalofferid string `json:"promotional_offer_id"`
}
// Receipt is the decoded version of the encoded receipt data sent with the request to the App Store
// https://developer.apple.com/documentation/appstorereceipts/responsebody/receipt
type Receipt struct {
// See app_item_id.
AdamId int64 `json:"adam_id"`
// Generated by App Store Connect and used by the App Store to uniquely identify the app purchased. Apps are assigned this identifier only in production. Treat this value as a 64-bit long integer.
AppItemId int64 `json:"app_item_id"`
// The apps version number. The app's version number corresponds to the value of CFBundleVersion (in iOS) or CFBundleShortVersionString (in macOS) in the Info.plist. In production, this value is the current version of the app on the device based on the receipt_creation_date_ms. In the sandbox, the value is always "1.0".
ApplicationVersion string `json:"application_version"`
// The bundle identifier for the app to which the receipt belongs. You provide this string on App Store Connect. This corresponds to the value of CFBundleIdentifier in the Info.plist file of the app.
BundleId string `json:"bundle_id"`
// A unique identifier for the app download transaction.
DownloadId int64 `json:"download_id"`
// The time the receipt expires for apps purchased through the Volume Purchase Program, in a date-time format similar to the ISO 8601.
ExpirationDate string `json:"expiration_date"`
// The time the receipt expires for apps purchased through the Volume Purchase Program, in UNIX epoch time format, in milliseconds. If this key is not present for apps purchased through the Volume Purchase Program, the receipt does not expire. Use this time format for processing dates.
ExpirationDateTimestamp string `json:"expiration_date_ms"`
// The time the receipt expires for apps purchased through the Volume Purchase Program, in the Pacific Time zone.
ExpirationDatePST string `json:"expiration_date_pst"`
// An array that contains the in-app purchase receipt fields for all in-app purchase transactions.
InApp []*InApp `json:"in_app,omitempty"`
// The version of the app that the user originally purchased. This value does not change, and corresponds to the value of CFBundleVersion (in iOS) or CFBundleShortVersionString (in macOS) in the Info.plist file of the original purchase. In the sandbox environment, the value is always "1.0".
OriginalApplicationVersion string `json:"original_application_version"`
// The time of the original app purchase, in a date-time format similar to ISO 8601.
OriginalPurchaseDate string `json:"original_purchase_date"`
// The time of the original app purchase, in UNIX epoch time format, in milliseconds. Use this time format for processing dates.
OriginalPurchaseDateTimestamp string `json:"original_purchase_date_ms"`
// The time of the original app purchase, in the Pacific Time zone.
OriginalPurchaseDatePST string `json:"original_purchase_date_pst"`
// The time the user ordered the app available for pre-order, in a date-time format similar to ISO 8601.
PreorderDate string `json:"preorder_date"`
// The time the user ordered the app available for pre-order, in UNIX epoch time format, in milliseconds. This field is only present if the user pre-orders the app. Use this time format for processing dates.
PreorderDateTimestamp string `json:"preorder_date_ms"`
// The time the user ordered the app available for pre-order, in the Pacific Time zone.
PreorderDatePST string `json:"preorder_date_pst"`
// The time the App Store generated the receipt, in a date-time format similar to ISO 8601.
ReceiptCreationDate string `json:"receipt_creation_date"`
// The time the App Store generated the receipt, in UNIX epoch time format, in milliseconds. Use this time format for processing dates. This value does not change.
ReceiptCreationDateTimestamp string `json:"receipt_creation_date_ms"`
// The time the App Store generated the receipt, in the Pacific Time zone.
ReceiptCreationDatePST string `json:"receipt_creation_date_pst"`
// The type of receipt generated. The value corresponds to the environment in which the app or VPP purchase was made.
// Possible values: Production, ProductionVPP, ProductionSandbox, ProductionVPPSandbox
ReceiptType string `json:"receipt_type"`
// The time the request to the verifyReceipt endpoint was processed and the response was generated, in a date-time format similar to ISO 8601.
RequestDate string `json:"request_date"`
// The time the request to the verifyReceipt endpoint was processed and the response was generated, in UNIX epoch time format, in milliseconds. Use this time format for processing dates.
RequestDateTimestamp string `json:"request_date_ms"`
// The time the request to the verifyReceipt endpoint was processed and the response was generated, in the Pacific Time zone.
RequestDatePST string `json:"request_date_pst"`
// An arbitrary number that identifies a revision of your app. In the sandbox, this key's value is 0.
VersionExternalIdentifier int64 `json:"version_external_identifier"`
}
// InApp is the in-app purchase receipt fields for all in-app purchase transactions.
// https://developer.apple.com/documentation/appstorereceipts/responsebody/receipt/in_app
type InApp struct {
// The time the App Store refunded a transaction or revoked it from family sharing, in a date-time format similar to the ISO 8601. This field is present only for refunded or revoked transactions.
CancellationDate string `json:"cancellation_date"`
// The time the App Store refunded a transaction or revoked it from family sharing, in UNIX epoch time format, in milliseconds. This field is present only for refunded or revoked transactions. Use this time format for processing dates. The time the App Store refunded a transaction or revoked it from family sharing, in UNIX epoch time format, in milliseconds. This field is present only for refunded or revoked transactions. Use this time format for processing dates.
// https://developer.apple.com/documentation/appstorereceipts/cancellation_date_ms
CancellationDateTimestamp string `json:"cancellation_date_ms"`
// The time Apple customer support canceled a transaction, in the Pacific Time zone. This field is only present for refunded transactions.
CancellationDatePST string `json:"cancellation_date_pst"`
// The reason for a refunded transaction. When a customer cancels a transaction, the App Store gives them a refund and provides a value for this key. A value of “1” indicates that the customer canceled their transaction due to an actual or perceived issue within your app. A value of “0” indicates that the transaction was canceled for another reason; for example, if the customer made the purchase accidentally.
// Possible values: 1, 0
CancellationReason string `json:"cancellation_reason"`
// The time a subscription expires or when it will renew, in a date-time format similar to the ISO 8601.
ExpiresDate string `json:"expires_date"`
// The time a subscription expires or when it will renew, in UNIX epoch time format, in milliseconds. Use this time format for processing dates.
// https://developer.apple.com/documentation/appstorereceipts/expires_date_ms
ExpiresDateTimestamp string `json:"expires_date_ms"`
// The time a subscription expires or when it will renew, in the Pacific Time zone.
ExpiresDatePST string `json:"expires_date_pst"`
// An indicator of whether an auto-renewable subscription is in the introductory price period.
// https://developer.apple.com/documentation/appstorereceipts/is_in_intro_offer_period
IsInIntroOfferPeriod string `json:"is_in_intro_offer_period"`
// An indication of whether a subscription is in the free trial period.
// https://developer.apple.com/documentation/appstorereceipts/is_trial_period
IsTrialPeriod string `json:"is_trial_period"`
// The time of the original in-app purchase, in a date-time format similar to ISO 8601.
OriginalPurchaseDate string `json:"original_purchase_date"`
// The time of the original in-app purchase, in UNIX epoch time format, in milliseconds. For an auto-renewable subscription, this value indicates the date of the subscription's initial purchase. The original purchase date applies to all product types and remains the same in all transactions for the same product ID. This value corresponds to the original transactions transactionDate property in StoreKit. Use this time format for processing dates.
OriginalPurchaseDateTimestamp string `json:"original_purchase_date_ms"`
// The time of the original in-app purchase, in the Pacific Time zone.
OriginalPurchaseDatePST string `json:"original_purchase_date_pst"`
// The transaction identifier of the original purchase.
// https://developer.apple.com/documentation/appstorereceipts/original_transaction_id
OriginalTransactionId string `json:"original_transaction_id"`
// The unique identifier of the product purchased. You provide this value when creating the product in App Store Connect, and it corresponds to the productIdentifier property of the SKPayment object stored in the transaction's payment property.
ProductId string `json:"product_id"`
// The identifier of the subscription offer redeemed by the user.
// https://developer.apple.com/documentation/appstorereceipts/promotional_offer_id
PromotionalOfferId string `json:"promotional_offer_id"`
// The time the App Store charged the user's account for a purchased or restored product, or the time the App Store charged the users account for a subscription purchase or renewal after a lapse, in a date-time format similar to ISO 8601.
PurchaseDate string `json:"purchase_date"`
// For consumable, non-consumable, and non-renewing subscription products, the time the App Store charged the user's account for a purchased or restored product, in the UNIX epoch time format, in milliseconds. For auto-renewable subscriptions, the time the App Store charged the users account for a subscription purchase or renewal after a lapse, in the UNIX epoch time format, in milliseconds. Use this time format for processing dates.
PurchaseDateTimestamp string `json:"purchase_date_ms"`
// The time the App Store charged the user's account for a purchased or restored product, or the time the App Store charged the users account for a subscription purchase or renewal after a lapse, in the Pacific Time zone.
PurchaseDatePST string `json:"purchase_date_pst"`
// The number of consumable products purchased. This value corresponds to the quantity property of the SKPayment object stored in the transaction's payment property. The value is usually “1” unless modified with a mutable payment. The maximum value is 10.
Quantity string `json:"quantity"`
// A unique identifier for a transaction such as a purchase, restore, or renewal. See transaction_id for more information.
TransactionId string `json:"transaction_id"`
// A unique identifier for purchase events across devices, including subscription-renewal events. This value is the primary key for identifying subscription purchases.
WebOrderLineItemId string `json:"web_order_line_item_id"`
}
// VerifyReceipt 请求APP Store 校验支付请求,实际测试时发现这个文档介绍的返回信息只有那个status==0表示成功可以用其他的返回信息跟文档对不上
// url取 UrlProd 或 UrlSandbox
// pwd苹果APP秘钥https://help.apple.com/app-store-connect/#/devf341c0f01
// 文档https://developer.apple.com/documentation/appstorereceipts/verifyreceipt
func VerifyReceipt(ctx context.Context, url, pwd, receipt string) (rsp *VerifyResponse, err error) {
req := &VerifyRequest{Receipt: receipt, Password: pwd}
rsp = new(VerifyResponse)
_, err = NewClient().Type(TypeJSON).Post(url).SendStruct(req).EndStruct(ctx, rsp)
if err != nil {
return nil, err
}
return rsp, nil
}
// NewClient , default tls.Config{InsecureSkipVerify: true}
func NewClient() (client *Client) {
client = &Client{
HttpClient: &http.Client{
Timeout: 60 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
DisableKeepAlives: true,
Proxy: http.ProxyFromEnvironment,
},
},
Transport: nil,
Header: make(http.Header),
bodySize: 10, // default is 10MB
requestType: TypeJSON,
unmarshalType: string(TypeJSON),
}
return client
}
func (c *Client) Type(typeStr RequestType) (client *Client) {
if _, ok := types[typeStr]; ok {
c.requestType = typeStr
}
return c
}
func (c *Client) Post(url string) (client *Client) {
c.method = POST
c.url = url
return c
}
func (c *Client) SendStruct(v any) (client *Client) {
if v == nil {
return c
}
bs, err := json.Marshal(v)
if err != nil {
c.err = fmt.Errorf("[%w]: %v, value: %v", MarshalErr, err, v)
return c
}
switch c.requestType {
case TypeJSON:
c.jsonByte = bs
case TypeXML, TypeUrlencoded, TypeForm, TypeFormData:
body := make(map[string]any)
if err = json.Unmarshal(bs, &body); err != nil {
c.err = fmt.Errorf("[%w]: %v, bytes: %s", UnmarshalErr, err, string(bs))
return c
}
c.FormString = FormatURLParam(body)
}
return c
}
func FormatURLParam(body map[string]any) (urlParam string) {
var (
buf strings.Builder
keys []string
)
for k := range body {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
v, ok := body[k].(string)
if !ok {
v = convertToString(body[k])
}
if v != "" {
buf.WriteString(url.QueryEscape(k))
buf.WriteByte('=')
buf.WriteString(url.QueryEscape(v))
buf.WriteByte('&')
}
}
if buf.Len() <= 0 {
return ""
}
return buf.String()[:buf.Len()-1]
}
func convertToString(v any) (str string) {
if v == nil {
return ""
}
var (
bs []byte
err error
)
if bs, err = json.Marshal(v); err != nil {
return ""
}
str = string(bs)
return
}
func (c *Client) EndStruct(ctx context.Context, v any) (res *http.Response, err error) {
res, bs, err := c.EndBytes(ctx)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return res, fmt.Errorf("StatusCode(%d) != 200", res.StatusCode)
}
switch c.unmarshalType {
case string(TypeJSON):
err = json.Unmarshal(bs, &v)
if err != nil {
return nil, fmt.Errorf("[%w]: %v, bytes: %s", UnmarshalErr, err, string(bs))
}
return res, nil
case string(TypeXML):
err = xml.Unmarshal(bs, &v)
if err != nil {
return nil, fmt.Errorf("[%w]: %v, bytes: %s", UnmarshalErr, err, string(bs))
}
return res, nil
default:
return nil, errors.New("unmarshalType Type Wrong")
}
}
func (c *Client) EndBytes(ctx context.Context) (res *http.Response, bs []byte, err error) {
if c.err != nil {
return nil, nil, c.err
}
var (
body io.Reader
bw *multipart.Writer
)
// multipart-form-data
if c.requestType == TypeMultipartFormData {
body = &bytes.Buffer{}
bw = multipart.NewWriter(body.(io.Writer))
}
reqFunc := func() (err error) {
switch c.method {
case GET:
switch c.requestType {
case TypeJSON:
c.ContentType = types[TypeJSON]
case TypeForm, TypeFormData, TypeUrlencoded:
c.ContentType = types[TypeForm]
case TypeMultipartFormData:
c.ContentType = bw.FormDataContentType()
case TypeXML:
c.ContentType = types[TypeXML]
c.unmarshalType = string(TypeXML)
default:
return errors.New("Request type Error ")
}
case POST, PUT, DELETE, PATCH:
switch c.requestType {
case TypeJSON:
if c.jsonByte != nil {
body = strings.NewReader(string(c.jsonByte))
}
c.ContentType = types[TypeJSON]
case TypeForm, TypeFormData, TypeUrlencoded:
body = strings.NewReader(c.FormString)
c.ContentType = types[TypeForm]
case TypeMultipartFormData:
for k, v := range c.multipartBodyMap {
// file 参数
if file, ok := v.(*File); ok {
fw, err := bw.CreateFormFile(k, file.Name)
if err != nil {
return err
}
_, _ = fw.Write(file.Content)
continue
}
// text 参数
vs, ok2 := v.(string)
if ok2 {
_ = bw.WriteField(k, vs)
} else if ss := ConvertToString(v); ss != "" {
_ = bw.WriteField(k, ss)
}
}
_ = bw.Close()
c.ContentType = bw.FormDataContentType()
case TypeXML:
body = strings.NewReader(c.FormString)
c.ContentType = types[TypeXML]
c.unmarshalType = string(TypeXML)
default:
return errors.New("Request type Error ")
}
default:
return errors.New("Only support GET and POST and PUT and DELETE ")
}
req, err := http.NewRequestWithContext(ctx, c.method, c.url, body)
if err != nil {
return err
}
req.Header = c.Header
req.Header.Set("Content-Type", c.ContentType)
if c.Transport != nil {
c.HttpClient.Transport = c.Transport
}
if c.Host != "" {
req.Host = c.Host
}
if c.Timeout > 0 {
c.HttpClient.Timeout = c.Timeout
}
res, err = c.HttpClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
bs, err = io.ReadAll(io.LimitReader(res.Body, int64(c.bodySize<<20))) // default 10MB change the size you want
if err != nil {
return err
}
return nil
}
if err = reqFunc(); err != nil {
return nil, nil, err
}
return res, bs, nil
}
func ConvertToString(v any) (str string) {
if v == nil {
return ""
}
var (
bs []byte
err error
)
if bs, err = json.Marshal(v); err != nil {
return ""
}
str = string(bs)
return
}

191
package/pay/google.go Normal file
View File

@@ -0,0 +1,191 @@
package pay
import (
"context"
"github.com/ayflying/utility_go/package/pay/playstore"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
"google.golang.org/api/androidpublisher/v3"
)
var (
ctx = gctx.New()
)
// GooglePay 是一个处理Google支付的结构体。
type GooglePay struct {
c *playstore.Client
}
// Init 初始化GooglePay客户端。
// data: 初始化客户端所需的配置数据。
func (p *GooglePay) Init(data []byte) {
var err error
p.c, err = playstore.New(data)
if err != nil {
panic(err) // 如果初始化失败则panic。
}
}
// VerifyPay 验证用户的支付。
// userId: 用户ID。
// OrderId: 订单ID。
// package1: 应用包名。
// subscriptionID: 订阅ID。
// purchaseToken: 购买凭证。
// cb: 验证结果的回调函数,如果验证成功,会调用此函数。
// 返回值: 执行错误。
func (p *GooglePay) VerifyPay(userId int64, OrderId, package1, subscriptionID, purchaseToken string, cb func(string, string) error) error {
info, err := p.c.VerifyProduct(context.Background(), package1, subscriptionID, purchaseToken)
if err != nil {
return gerror.Cause(err) // 验证产品失败,返回错误。
}
if info.PurchaseState == 0 {
if err := cb(subscriptionID, info.OrderId); err != nil {
return gerror.Cause(err) // 调用回调函数失败,返回错误。
}
} else {
return nil // 验证结果不为购买状态直接返回nil。
}
return nil
}
// VerifyPayV1 是VerifyPay的另一个版本用于验证订阅支付。
// package1: 应用包名。
// subscriptionID: 订阅ID。
// purchaseToken: 购买凭证。
// cb: 验证结果的回调函数。
// 返回值: 执行错误。
func (p *GooglePay) VerifyPayV1(package1, subscriptionID, purchaseToken string, cb func(string, string) error) error {
//g.Log().Infof(ctx, "VerifyPayV1: package = %v subscriptionID = %v, purchaseToken = %v", package1, subscriptionID, purchaseToken)
info, err := p.c.VerifyProduct(context.Background(), package1, subscriptionID, purchaseToken)
if err != nil {
return gerror.Cause(err) // 验证产品失败,返回错误。
}
if info.PurchaseState == 0 {
if err := cb(subscriptionID, info.OrderId); err != nil {
return gerror.Cause(err) // 调用回调函数失败,返回错误。
}
} else {
return nil // 验证结果不为购买状态直接返回nil。
}
return nil
}
// VerifyPayV2 是VerifyPay的另一个版本支持不同类型产品的验证。
// types: 验证的产品类型。
// package1: 应用包名。
// subscriptionID: 订阅ID。
// purchaseToken: 购买凭证。
// cb: 验证结果的回调函数。
// 返回值: 执行错误。
func (p *GooglePay) VerifyPayV2(types int32, package1, subscriptionID, purchaseToken string, cb func(string, string) error) error {
//g.Log().Infof(ctx, "VerifyPayV1: package = %v subscriptionID = %v, purchaseToken = %v", package1, subscriptionID, purchaseToken)
switch types {
case 0:
info, err := p.c.VerifyProduct(context.Background(), package1, subscriptionID, purchaseToken)
if err != nil {
return gerror.Cause(err) // 验证产品失败,返回错误。
}
if info.PurchaseState == 0 {
if err := cb(subscriptionID, info.OrderId); err != nil {
return gerror.Cause(err) // 调用回调函数失败,返回错误。
}
}
case 1:
info, err := p.c.VerifySubscription(context.Background(), package1, subscriptionID, purchaseToken)
if err != nil {
return gerror.Cause(err) // 验证订阅失败,返回错误。
}
if len(info.OrderId) != 0 {
if err := cb(subscriptionID, info.OrderId); err != nil {
return gerror.Cause(err) // 调用回调函数失败,返回错误。
}
}
}
return nil
}
//func (p *GooglePay) VerifyPayTest(package1, subscriptionID, purchaseToken string) (*androidpublisher.ProductPurchase, error) {
// return p.c.VerifyProduct(context.Background(), package1, subscriptionID, purchaseToken)
//}
func (p *GooglePay) VerifySubscriptionTest(package1, subscriptionID, purchaseToken string) (interface{}, error) {
return p.c.VerifySubscription(context.Background(), package1, subscriptionID, purchaseToken)
}
// VerifySubSciption google 检查订阅是否有效
func (p *GooglePay) VerifySubSciption(package1, subscriptionID, purchaseToken string) (string, error) {
info, err := p.c.VerifySubscription(context.Background(), package1, subscriptionID, purchaseToken)
if err != nil {
return "", gerror.Cause(err)
}
if len(info.OrderId) != 0 {
return info.OrderId, nil
}
return "", nil
}
// 获取已撤销的购买列表
func (p *GooglePay) GetRevokedPurchaseList(package1 string) (res *androidpublisher.VoidedPurchasesListResponse, err error) {
res, err = p.c.Voidedpurchases(package1)
//return p.c.GetRevokedPurchaseList(context.Background(), package1)
return
}
// Acknowledge 确认购买应用内商品。
// Method: purchases.products.acknowledge y
func (p *GooglePay) Acknowledge(ctx context.Context, packageName, productID, token, developerPayload string) (err error) {
err = p.c.AcknowledgeProduct(ctx, packageName, productID, token, developerPayload)
return
}
// Consume 消费购买应用内商品。
func (p *GooglePay) Consume(ctx context.Context, packageName, productID, token string) (err error) {
err = p.c.ConsumeProduct(ctx, packageName, productID, token)
return
}
// 谷歌支付支付凭证校验V1
func (s *GooglePay) GooglePayTokenV1(token string) (err error) {
type PayOrderType struct {
Payload string `json:"Payload"`
Store string `json:"Store"`
TransactionID string `json:"TransactionID"`
}
type PayloadType struct {
Json string `json:"json"`
Signature string `json:"signature"`
SkuDetails []string `json:"skuDetails"`
}
type PayJson struct {
PackageName string `json:"packageName"`
ProductId string `json:"productId"`
PurchaseTime int64 `json:"purchaseTime"`
PurchaseState int `json:"purchaseState"`
PurchaseToken string `json:"purchaseToken"`
Quantity int `json:"quantity"`
Acknowledged bool `json:"acknowledged"`
OrderId string `json:"orderId"`
}
var data PayOrderType
gconv.Struct(token, &data)
var payload PayloadType
gconv.Struct(data.Payload, &payload)
var payJson PayJson
gconv.Struct(payload.Json, &payJson)
if gstr.Pos(payJson.OrderId, "GPA.") < 0 {
err = gerror.New("GPA验证失败")
return
}
if payJson.Quantity != 1 {
err = gerror.New("Quantity验证失败")
return
}
return
}

View File

@@ -0,0 +1,40 @@
package playstore
// GetStatus 获取产品的状态,例如产品是否处于活跃状态。
//
// 返回值 EProductStatus 代表产品状态。
// 可能的状态包括:
//
// ProductStatus_Unspecified // 未指定状态。
// ProductStatus_active // 产品已发布且在商店中处于活跃状态。
// ProductStatus_inactive // 产品未发布,因此在商店中处于非活跃状态。
func (iap InAppProduct) GetStatus() EProductStatus {
return EProductStatus(iap.AndroidPublisherInAppProduct.Status)
}
// GetSubscriptionPeriod 获取订阅的周期。
//
// 返回值 ESubscriptionPeriod 代表订阅周期。
// 可能的周期包括:
//
// SubscriptionPeriod_Invalid : 无效的订阅(可能是消耗品)。
// SubscriptionPeriod_OneWeek (一周)。
// SubscriptionPeriod_OneMonth (一个月)。
// SubscriptionPeriod_ThreeMonths (三个月)。
// SubscriptionPeriod_SixMonths (六个月)。
// SubscriptionPeriod_OneYear (一年)。
func (iap InAppProduct) GetSubscriptionPeriod() ESubscriptionPeriod {
return ESubscriptionPeriod(iap.AndroidPublisherInAppProduct.SubscriptionPeriod)
}
// GetPurchaseType 获取产品的购买类型。
//
// 返回值 EPurchaseType 代表产品的购买类型。
// 可能的类型包括:
//
// EPurchaseType_Unspecified (未指定购买类型)。
// EPurchaseType_ManagedUser 可以被单次或多次购买(消耗品、非消耗品)。
// EPurchaseType_Subscription (应用内产品,具有周期性消费)。
func (iap InAppProduct) GetPurchaseType() EPurchaseType {
return EPurchaseType(iap.AndroidPublisherInAppProduct.PurchaseType)
}

View File

@@ -0,0 +1,34 @@
package playstore
// EProductStatus 定义了产品的状态,例如产品是否处于活跃状态。
type EProductStatus string
// 定义了产品可能的状态常量。
const (
ProductStatus_Unspecified EProductStatus = "statusUnspecified" // 未指定状态。
ProductStatus_active EProductStatus = "active" // 产品已发布且在商店中处于活跃状态。
ProductStatus_inactive EProductStatus = "inactive" // 产品未发布,因此在商店中处于非活跃状态。
)
// ESubscriptionPeriod 定义了订阅的周期。
type ESubscriptionPeriod string
// 定义了订阅可能的周期常量。
const (
SubscriptionPeriod_Invalid ESubscriptionPeriod = "" // 无效的订阅(可能是消耗品)。
SubscriptionPeriod_OneWeek ESubscriptionPeriod = "P1W" // 一周。
SubscriptionPeriod_OneMonth ESubscriptionPeriod = "P1M" // 一个月。
SubscriptionPeriod_ThreeMonths ESubscriptionPeriod = "P3M" // 三个月。
SubscriptionPeriod_SixMonths ESubscriptionPeriod = "P6M" // 六个月。
SubscriptionPeriod_OneYear ESubscriptionPeriod = "P1Y" // 一年。
)
// EPurchaseType 定义了产品的购买类型,例如周期性订阅。
type EPurchaseType string
// 定义了产品可能的购买类型常量。
const (
EPurchaseType_Unspecified EPurchaseType = "purchaseTypeUnspecified" // 未指定购买类型。
EPurchaseType_ManagedUser EPurchaseType = "managedUser" // 默认的产品类型 - 可以单次或多次购买(消耗品、非消耗品)。
EPurchaseType_Subscription EPurchaseType = "subscription" // 应用内具有周期性的产品。
)

View File

@@ -0,0 +1,80 @@
package playstore
import (
"context"
"google.golang.org/api/androidpublisher/v3"
)
// IABProduct 接口定义了商品服务的基本操作。
type IABProduct interface {
// VerifyProduct 验证指定的内购产品购买信息。
// ctx: 上下文,用于控制请求的取消、超时等。
// packageName: 应用包名。
// productId: 内购商品ID。
// purchaseToken: 购买凭证。
// 返回经过验证的购买信息和可能的错误。
VerifyProduct(context.Context, string, string, string) (*androidpublisher.ProductPurchase, error)
// AcknowledgeProduct 确认指定的内购产品的购买。
// ctx: 上下文。
// packageName: 应用包名。
// productId: 内购商品ID。
// purchaseToken: 购买凭证。
// orderId: 订单ID。
// 返回可能发生的错误。
AcknowledgeProduct(context.Context, string, string, string, string) error
}
// IABSubscription 接口定义了订阅服务的基本操作。
type IABSubscription interface {
// AcknowledgeSubscription 确认指定订阅的购买。
// ctx: 上下文。
// packageName: 应用包名。
// subscriptionId: 订阅ID。
// purchaseToken: 购买凭证。
// acknowledgeRequest: 确认请求参数。
// 返回可能发生的错误。
AcknowledgeSubscription(context.Context, string, string, string, *androidpublisher.SubscriptionPurchasesAcknowledgeRequest) error
// VerifySubscription 验证指定订阅的购买信息。
// ctx: 上下文。
// packageName: 应用包名。
// subscriptionId: 订阅ID。
// purchaseToken: 购买凭证。
// 返回经过验证的订阅购买信息和可能的错误。
VerifySubscription(context.Context, string, string, string) (*androidpublisher.SubscriptionPurchase, error)
// CancelSubscription 取消指定的订阅。
// ctx: 上下文。
// packageName: 应用包名。
// subscriptionId: 订阅ID。
// purchaseToken: 购买凭证。
// 返回可能发生的错误。
CancelSubscription(context.Context, string, string, string) error
// RefundSubscription 为指定的订阅办理退款。
// ctx: 上下文。
// packageName: 应用包名。
// subscriptionId: 订阅ID。
// purchaseToken: 购买凭证。
// 返回可能发生的错误。
RefundSubscription(context.Context, string, string, string) error
// RevokeSubscription 撤销指定的订阅。
// ctx: 上下文。
// packageName: 应用包名。
// subscriptionId: 订阅ID。
// purchaseToken: 购买凭证。
// 返回可能发生的错误。
RevokeSubscription(context.Context, string, string, string) error
}
// Client 结构体实现了 IABSubscription 接口,提供了具体的操作实现。
type Client struct {
service *androidpublisher.Service
}
// InAppProduct 结构体封装了 androidpublisher.InAppProduct并提供了一些辅助方法。
type InAppProduct struct {
AndroidPublisherInAppProduct *androidpublisher.InAppProduct
}

View File

@@ -0,0 +1,62 @@
package playstore
// SubscriptionNotificationType 定义了订阅通知的类型。
type SubscriptionNotificationType int
// 预定义的订阅通知类型。
const (
SubscriptionNotificationTypeRecovered SubscriptionNotificationType = iota + 1 // 订阅已恢复
SubscriptionNotificationTypeRenewed // 订阅已续订
SubscriptionNotificationTypeCanceled // 订阅已取消
SubscriptionNotificationTypePurchased // 订阅已购买
SubscriptionNotificationTypeAccountHold // 订阅账户暂停
SubscriptionNotificationTypeGracePeriod // 宽限期通知
SubscriptionNotificationTypeRestarted // 订阅已重新开始
SubscriptionNotificationTypePriceChangeConfirmed // 订阅价格变更已确认
SubscriptionNotificationTypeDeferred // 订阅延迟
SubscriptionNotificationTypePaused // 订阅已暂停
SubscriptionNotificationTypePauseScheduleChanged // 暂停计划已更改
SubscriptionNotificationTypeRevoked // 订阅已撤销
SubscriptionNotificationTypeExpired // 订阅已过期
)
// OneTimeProductNotificationType 定义了一次性产品通知的类型。
type OneTimeProductNotificationType int
// 预定义的一次性产品通知类型。
const (
OneTimeProductNotificationTypePurchased OneTimeProductNotificationType = iota + 1 // 一次性产品已购买
OneTimeProductNotificationTypeCanceled // 一次性产品已取消
)
// DeveloperNotification 是通过 Pub/Sub 主题发送给开发者的通知。
// 详细描述请参见https://developer.android.com/google/play/billing/rtdn-reference#json_specification
type DeveloperNotification struct {
Version string `json:"version"` // 版本号
PackageName string `json:"packageName"` // 应用包名
EventTimeMillis string `json:"eventTimeMillis"` // 事件发生时间(毫秒)
SubscriptionNotification SubscriptionNotification `json:"subscriptionNotification,omitempty"` // 订阅通知
OneTimeProductNotification OneTimeProductNotification `json:"oneTimeProductNotification,omitempty"` // 一次性产品通知
TestNotification TestNotification `json:"testNotification,omitempty"` // 测试通知
}
// SubscriptionNotification 包含订阅状态通知类型、token 和订阅ID用于通过Google Android Publisher API确认状态。
type SubscriptionNotification struct {
Version string `json:"version"` // 版本号
NotificationType SubscriptionNotificationType `json:"notificationType,omitempty"` // 通知类型
PurchaseToken string `json:"purchaseToken,omitempty"` // 购买token
SubscriptionID string `json:"subscriptionId,omitempty"` // 订阅ID
}
// OneTimeProductNotification 包含一次性产品状态通知类型、token 和产品IDSKU用于通过Google Android Publisher API确认状态。
type OneTimeProductNotification struct {
Version string `json:"version"` // 版本号
NotificationType OneTimeProductNotificationType `json:"notificationType,omitempty"` // 通知类型
PurchaseToken string `json:"purchaseToken,omitempty"` // 购买token
SKU string `json:"sku,omitempty"` // 产品IDSKU
}
// TestNotification 是仅通过Google Play开发者控制台发送的测试发布通知。
type TestNotification struct {
Version string `json:"version"` // 版本号
}

View File

@@ -0,0 +1,118 @@
package playstore
import (
"context"
"crypto"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"encoding/base64"
"fmt"
"net/http"
"time"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"google.golang.org/api/androidpublisher/v3"
"google.golang.org/api/option"
)
// New 创建并返回一个包含访问androidpublisher API所需凭证的http客户端。
//
// @Description: 通过提供的JSON密钥创建一个配置好的Client实例可用于与Google Play Store API交互。
// @param jsonKey 用于构建JWT配置的JSON密钥字节切片。
// @return *Client 返回初始化好的Client实例。
// @return error 如果在创建过程中遇到任何错误则返回非nil的error。
func New(jsonKey []byte) (*Client, error) {
// 设置http客户端超时时间为10秒
c := &http.Client{Timeout: 10 * time.Second}
// 为context设置HTTP客户端以便在OAuth2流程中使用
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, c)
// 使用JSON密钥和所需范围配置JWT
conf, err := google.JWTConfigFromJSON(jsonKey, androidpublisher.AndroidpublisherScope)
if err != nil {
return nil, err
}
// 验证JWT配置是否正确并获取访问令牌
val := conf.Client(ctx).Transport.(*oauth2.Transport)
_, err = val.Source.Token()
if err != nil {
return nil, err
}
// 使用配置的HTTP客户端初始化androidpublisher服务
service, err := androidpublisher.NewService(ctx, option.WithHTTPClient(conf.Client(ctx)))
if err != nil {
return nil, err
}
// 返回初始化好的Client实例
return &Client{service}, err
}
// NewWithClient returns http client which includes the custom http client.
// 使用自定义的http客户端创建并返回一个包含访问androidpublisher API所需凭证的http客户端。
func NewWithClient(jsonKey []byte, cli *http.Client) (*Client, error) {
if cli == nil {
return nil, fmt.Errorf("client is nil")
}
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, cli)
conf, err := google.JWTConfigFromJSON(jsonKey, androidpublisher.AndroidpublisherScope)
if err != nil {
return nil, err
}
service, err := androidpublisher.NewService(ctx, option.WithHTTPClient(conf.Client(ctx)))
if err != nil {
return nil, err
}
return &Client{service}, err
}
// VerifySignature 验证应用内购买的签名。
// 您需要为您的 Android 应用的内购准备公钥,可在 https://play.google.com/apps/publish/ 上完成。
// 参数:
//
// base64EncodedPublicKey string - 经过 Base64 编码的公钥字符串。
// receipt []byte - 购买收据的字节数据。
// signature string - 购买收据的签名字符串。
//
// 返回值:
//
// isValid bool - 标识签名是否验证成功。
// err error - 验证过程中遇到的错误。
func VerifySignature(base64EncodedPublicKey string, receipt []byte, signature string) (isValid bool, err error) {
// 准备公钥
decodedPublicKey, err := base64.StdEncoding.DecodeString(base64EncodedPublicKey)
if err != nil {
return false, fmt.Errorf("failed to decode public key")
}
publicKeyInterface, err := x509.ParsePKIXPublicKey(decodedPublicKey)
if err != nil {
return false, fmt.Errorf("failed to parse public key")
}
publicKey, _ := publicKeyInterface.(*rsa.PublicKey)
// 从收据生成哈希值
hasher := sha1.New()
hasher.Write(receipt)
hashedReceipt := hasher.Sum(nil)
// 解码签名
decodedSignature, err := base64.StdEncoding.DecodeString(signature)
if err != nil {
return false, fmt.Errorf("failed to decode signature")
}
// 验证签名
if err := rsa.VerifyPKCS1v15(publicKey, crypto.SHA1, hashedReceipt, decodedSignature); err != nil {
return false, nil
}
return true, nil
}

View File

@@ -0,0 +1,64 @@
package playstore
import (
"context"
"google.golang.org/api/androidpublisher/v3"
)
// VerifyProduct 验证产品状态
//
// 参数:
// - ctx: 上下文,用于控制请求的生命周期。
// - packageName: 应用的包名(例如,'com.some.thing')。
// - productID: 内购产品的SKU例如'com.some.thing.inapp1')。
// - token: 用户购买内购产品时设备上提供的令牌。
//
// 返回值:
// - *androidpublisher.ProductPurchase: 验证购买后的详细信息。
// - error: 执行过程中出现的错误。
func (c *Client) VerifyProduct(ctx context.Context, packageName string, productID string, token string) (*androidpublisher.ProductPurchase, error) {
ps := androidpublisher.NewPurchasesProductsService(c.service)
result, err := ps.Get(packageName, productID, token).Context(ctx).Do()
return result, err
}
// AcknowledgeProduct 确认内购商品购买
//
// 注意此函数必须在购买后的约24小时内对所有购买调用否则购买将被自动撤销。
//
// 参数:
// - ctx: 上下文,用于控制请求的生命周期。
// - packageName: 应用的包名(例如,'com.some.thing')。
// - productId: 内购产品的SKU例如'com.some.thing.inapp1')。
// - token: 用户购买内购产品时设备上提供的令牌。
// - developerPayload: 开发者自定义信息。
//
// 返回值:
// - error: 执行过程中出现的错误。
func (c *Client) AcknowledgeProduct(ctx context.Context, packageName, productID, token, developerPayload string) error {
ps := androidpublisher.NewPurchasesProductsService(c.service)
acknowledgeRequest := &androidpublisher.ProductPurchasesAcknowledgeRequest{DeveloperPayload: developerPayload}
err := ps.Acknowledge(packageName, productID, token, acknowledgeRequest).Context(ctx).Do()
return err
}
// ConsumeProduct 消费购买应用内商品。
func (c *Client) ConsumeProduct(ctx context.Context, packageName, productID, token string) error {
ps := androidpublisher.NewPurchasesProductsService(c.service)
//acknowledgeRequest := &androidpublisher.PurchasesProductsConsumeCall{DeveloperPayload: developerPayload}
//err := ps.Consume(packageName, productID, token).Context(ctx).Do()
_, err := ps.Get(packageName, productID, token).Context(ctx).Do()
return err
}
// Voidedpurchases 获取已撤销的购买列表
//
// 参数:
// - packageName: 应用的包名(例如,'com.some.thing')。
//
// 返回值:
// - *androidpublisher.VoidedPurchasesListResponse: 已撤销购买的列表响应。
// - error: 执行过程中出现的错误。
func (c *Client) Voidedpurchases(packageName string) (*androidpublisher.VoidedPurchasesListResponse, error) {
return androidpublisher.NewPurchasesVoidedpurchasesService(c.service).List(packageName).Do()
}

View File

@@ -0,0 +1,63 @@
package playstore
import (
"context"
"google.golang.org/api/androidpublisher/v3"
)
// AcknowledgeSubscription acknowledges a subscription purchase.
// 功能:确认订阅购买。
// 参数packageName应用包名subscriptionID订阅IDtoken购买令牌req确认请求对象
// 实现使用PurchasesSubscriptionsService服务的Acknowledge方法来确认指定订阅。
func (c *Client) AcknowledgeSubscription(ctx context.Context, packageName string, subscriptionID string, token string,
req *androidpublisher.SubscriptionPurchasesAcknowledgeRequest) error {
ps := androidpublisher.NewPurchasesSubscriptionsService(c.service)
err := ps.Acknowledge(packageName, subscriptionID, token, req).Context(ctx).Do()
return err
}
// VerifySubscription verifies subscription status
// 功能:验证订阅状态。
// 参数packageName应用包名subscriptionID订阅IDtoken购买令牌
// 实现使用PurchasesSubscriptionsService的Get方法来获取订阅的当前状态。
// 返回值SubscriptionPurchase对象包含订阅详情。
func (c *Client) VerifySubscription(ctx context.Context, packageName string, subscriptionID string, token string) (*androidpublisher.SubscriptionPurchase, error) {
ps := androidpublisher.NewPurchasesSubscriptionsService(c.service)
result, err := ps.Get(packageName, subscriptionID, token).Context(ctx).Do()
return result, err
}
// CancelSubscription cancels a user's subscription purchase.
// 功能:取消用户的订阅购买。
// 参数packageName应用包名subscriptionID订阅IDtoken购买令牌
// 实现使用PurchasesSubscriptionsService的Cancel方法来取消订阅。
func (c *Client) CancelSubscription(ctx context.Context, packageName string, subscriptionID string, token string) error {
ps := androidpublisher.NewPurchasesSubscriptionsService(c.service)
err := ps.Cancel(packageName, subscriptionID, token).Context(ctx).Do()
return err
}
// RefundSubscription refunds a user's subscription purchase, but the subscription remains valid
// until its expiration time and it will continue to recur.
// 功能:退款用户的订阅购买,但订阅在到期前仍有效,并且会继续递延。
// 参数packageName应用包名subscriptionID订阅IDtoken购买令牌
// 实现使用PurchasesSubscriptionsService的Refund方法来退款但不取消订阅。
func (c *Client) RefundSubscription(ctx context.Context, packageName string, subscriptionID string, token string) error {
ps := androidpublisher.NewPurchasesSubscriptionsService(c.service)
err := ps.Refund(packageName, subscriptionID, token).Context(ctx).Do()
return err
}
// RevokeSubscription refunds and immediately revokes a user's subscription purchase.
// Access to the subscription will be terminated immediately and it will stop recurring.
// 功能:退款并立即撤销用户的订阅购买。订阅将立即终止,并停止递延。
// 参数packageName应用包名subscriptionID订阅IDtoken购买令牌
// 实现使用PurchasesSubscriptionsService的Revoke方法来退款并撤销订阅。
func (c *Client) RevokeSubscription(ctx context.Context, packageName string, subscriptionID string, token string) error {
ps := androidpublisher.NewPurchasesSubscriptionsService(c.service)
err := ps.Revoke(packageName, subscriptionID, token).Context(ctx).Do()
return err
}

View File

@@ -0,0 +1,33 @@
package playstore
import "context"
// GetProduct 获取应用内商品信息,该商品可以是管理型商品或订阅。
//
// - packageName: 应用的包名。
// - productID: 应用内商品的唯一标识符SKU
//
// 返回值为InAppProduct类型的商品信息和可能出现的错误。
func (c *Client) GetProduct(ctx context.Context, packageName string, productID string) (*InAppProduct, error) {
// 通过Google Play 商店API获取指定商品的信息
var iap, err = c.service.Inappproducts.Get(packageName, productID).Context(ctx).Do()
return &InAppProduct{iap}, err
}
// ConvertRegionPrices 将商品的价格区域配置转换为指定货币单位。
//
// - ctx: 上下文,用于控制请求的取消、超时等。
// - packageName: 应用的包名。
// - productID: 应用内商品的唯一标识符。
// - inAppProduct: 需要转换价格区域的InAppProduct对象。
//
// 返回转换后的InAppProduct对象和可能出现的错误。
//
// 注:此函数暂未实现。
//func (c *Client) ConvertRegionPrices(ctx context.Context, packageName string, productID string, inAppProduct InAppProduct) (*InAppProduct, error) {
// // TODO: 实现商品价格区域转换逻辑
// // c.service.
//
// // 返回未实现的错误
// return &InAppProduct{iap}, err
//}

99
package/pay/wechat.go Normal file
View File

@@ -0,0 +1,99 @@
package pay
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"fmt"
"github.com/go-pay/crypto/xpem"
"github.com/go-pay/gopay"
"github.com/go-pay/gopay/wechat/v3"
"github.com/go-pay/util/convert"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gfile"
"strings"
)
var (
// ctx = gctx.New()
)
// GooglePay 是一个处理Google支付的结构体。
type WechatPay struct {
Client *wechat.ClientV3
PrivateKey string
}
func Wechat() *WechatPay {
var pay = &WechatPay{}
var err error
cfg, _ := g.Cfg().Get(ctx, "pay.wechat")
cfgMap := cfg.MapStrStr()
MchId := cfgMap["mchid"]
SerialNo := cfgMap["serialNo"]
APIv3Key := cfgMap["apiV3Key"]
PrivateKey := gfile.GetContents("manifest/pay/apiclient_key.pem")
//PrivateKey := cfgMap["privateKey"]
// NewClientV3 初始化微信客户端 v3
// mchid商户ID 或者服务商模式的 sp_mchid
// serialNo商户证书的证书序列号
// apiV3KeyapiV3Key商户平台获取
// privateKey私钥 apiclient_key.pem 读取后的内容
pay.Client, err = wechat.NewClientV3(MchId, SerialNo, APIv3Key, PrivateKey)
if err != nil {
g.Log().Error(ctx, err)
return nil
}
err = pay.Client.AutoVerifySign()
if err != nil {
g.Log().Error(ctx, err)
return nil
}
return pay
}
// v3 鉴权请求Header
func (c *WechatPay) Authorization(appid string, timestamp int64, nonceStr string, prepay_id string) (string, error) {
//var (
// jb = ""
// timestamp = time.Now().Unix()
// nonceStr = util.RandomString(32)
//)
//if bm != nil {
// jb = bm.JsonBody()
//}
//path = strings.TrimSuffix(path, "?")
ts := convert.Int642String(timestamp)
_str := strings.Join([]string{appid, ts, nonceStr, prepay_id}, "\n") + "\n"
//_str := appid + "\n" + timestamp + "\n" + nonceStr + "\n" + jb + "\n"
sign, err := c.rsaSign(_str)
if err != nil {
return "", err
}
return sign, nil
}
func (c *WechatPay) rsaSign(str string) (string, error) {
//if c.privateKey == nil {
// return "", errors.New("privateKey can't be nil")
//}
privateKey := gfile.GetContents("manifest/pay/apiclient_key.pem")
priKey, err := xpem.DecodePrivateKey([]byte(privateKey))
h := sha256.New()
h.Write([]byte(str))
result, err := rsa.SignPKCS1v15(rand.Reader, priKey, crypto.SHA256, h.Sum(nil))
if err != nil {
return gopay.NULL, fmt.Errorf("[%w]: %+v", gopay.SignatureErr, err)
}
return base64.StdEncoding.EncodeToString(result), nil
}

51
package/rand/rand.go Normal file
View File

@@ -0,0 +1,51 @@
package utility
import (
"github.com/ayflying/utility_go/tools"
"math/rand"
"time"
)
// rands 结构体用于封装 rand.Rand 实例,以提供随机数生成功能。
// 该结构体目前不包含锁,因此在多线程环境下使用时应注意同步问题。
type rands struct {
r *rand.Rand
// lock sync.Mutex
}
// Rand 是一个全局的 rands 实例,用于在整个程序中生成随机数。
// 它使用当前时间的毫秒值作为随机源,以确保每次程序运行时都能获得不同的随机数序列。
var Rand = rands{
r: rand.New(rand.NewSource(time.Now().UnixMilli())),
}
// RandByArrInt 函数从一个整数数组中按权重选择一个索引,并返回该索引。
// 权重是数组中相应元素的值。该函数通过计算累积和来确定选择的索引。
// 参数 v 是一个泛型参数,限制为实现了 Number 接口的类型。
// 返回值是一个整数,表示在数组中的索引。
func RandByArrInt[v tools.Number](s []v) int {
sv := 0
for i := range s {
sv += int(s[i])
}
r := Rand.Intn(sv)
var all v
for i := range s {
all += s[i]
if all > v(r) {
return i
}
}
return 0
}
// Intn 方法通过给定的整数 i 生成一个 0 到 i-1 之间的随机数。
// 如果 i 为0则会触发 panic。
// 参数 i 是一个整数,表示生成随机数的上限(不包含)。
// 返回值 ret 是一个在 0 到 i-1 范围内的随机整数。
func (r rands) Intn(i int) (ret int) {
if i == 0 {
panic(1)
}
return rand.Intn(i)
}

392
package/rank/rank.go Normal file
View File

@@ -0,0 +1,392 @@
package rank
import (
"fmt"
"time"
"github.com/gogf/gf/v2/database/gredis"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
)
var (
ctx = gctx.New()
)
type Mod struct {
}
type F64CountRank struct {
name string // 排行榜名
updateTs string // 更新时间key
}
type Data struct {
Id int64
Score int64
Rank int32
UpdateTs int64
}
func New() *Mod {
return &Mod{}
}
func (s *Mod) Load() {
}
// CreateF64CountRank 创建一个排行榜实例
// 参数:
//
// name: 排行榜的名称,通常代表一个赛季
//
// 返回值:
//
// *F64CountRank: 返回一个指向新创建的F64CountRank实例的指针
func (s *Mod) CreateF64CountRank(name string) *F64CountRank {
// 初始化F64CountRank实例的name和updateTs字段
// name字段用于标识排行榜的名称格式为"rank:<name>:score"
// updateTs字段用于标识排行榜的更新时间格式为"rank:<name>:updateTs"
return &F64CountRank{
name: fmt.Sprintf("rank:%s:score", name),
updateTs: fmt.Sprintf("rank:%s:updateTs", name),
}
}
// IncrScore 对指定ID的分数进行增加并返回增加后的当前分数。
// 该方法首先更新成员的更新时间戳,然后增加成员的分数。
//
// 参数:
//
// id - 要操作的成员ID。
// score - 要增加的分数。
//
// 返回值:
//
// curScore - 增加分数后的当前分数。
// err - 操作过程中可能发生的错误。
//
// IncrScore 先改redis再改cache
//
// @Description:
// @receiver r
// @param id
// @param score
// @return curScore
// @return err
func (r *F64CountRank) IncrScore(id int64, score int64) (curScore float64, err error) {
// 记录当前时间戳,用于更新成员的最新活动时间。
now := time.Now().UnixMilli()
// 将成员的更新时间戳加入到Redis的有序集合中确保成员的排序依据是最新的活动时间。
_, err = g.Redis().ZAdd(ctx, r.updateTs, &gredis.ZAddOption{}, gredis.ZAddMember{
Score: float64(now),
Member: id,
})
// 增加成员的分数,并返回增加后的当前分数。
curScore, err = g.Redis().ZIncrBy(ctx, r.name, float64(score), id)
//如果分数小于0则删除
if curScore <= 0 {
err = r.DelScore(id)
}
return
}
// todo暂时未使用
func (r *F64CountRank) GetCount() {
count, _ := g.Redis().ZCard(ctx, r.name)
if count > 9999 {
//删除超过9999的数据
g.Redis().ZRemRangeByRank(ctx, r.name, 0, -9999)
}
}
// Delete 删除当前排行榜
// 该方法通过删除Redis中与排行榜相关的键来清除排行榜信息
func (r *F64CountRank) Delete() {
// 删除排行榜数据键
_, err := g.Redis().Del(ctx, r.name)
if err != nil {
// 如果删除失败,记录错误日志
g.Log().Error(ctx, "排行榜删除失败:%v", err)
}
// 删除排行榜更新时间键
_, err = g.Redis().Del(ctx, r.updateTs)
if err != nil {
// 如果删除失败,记录错误日志
g.Log().Error(ctx, "排行榜删除失败:%v", err)
}
}
// DelScore 删除当前分数
//
// 该方法从更新时间有序集合和排名有序集合中移除指定的id。
// 这通常用于从排行榜中删除一个条目,同时确保其在更新时间集合中的对应记录也被清除。
//
// @Description: 从更新时间和排名集合中移除指定id
// @receiver r 接收者为F64CountRank类型的实例
// @param id 要从集合中移除的条目的ID
// @return err 可能发生的错误如果操作成功err为nil
func (r *F64CountRank) DelScore(id int64) (err error) {
// 从更新时间集合中移除id
_, err = g.Redis().ZRem(ctx, r.updateTs, id)
// 从排名集合中移除id
_, err = g.Redis().ZRem(ctx, r.name, id)
return
}
// DelByRank 根据排名范围删除元素。
// 该方法使用了Redis的有序集合数据结构通过ZRange和ZRemRangeByRank命令来实现。
// 参数start和stop定义了要删除的排名范围从start到stop包括start和stop
// 返回可能的错误。
func (r *F64CountRank) DelByRank(start int64, stop int64) (err error) {
// 初始化一个空的int64切片用于存储指定排名范围内的元素。
var members []int64
// 使用Redis的ZRange命令获取指定排名范围内的元素。
// 选项Rev设置为true表示按照分数从高到低的顺序返回元素。
get, err := g.Redis().ZRange(ctx, r.name, start, stop,
gredis.ZRangeOption{
Rev: true,
})
// 使用Scan方法将获取到的元素扫描到members切片中。
err = get.Scan(&members)
// 如果扫描过程中出现错误,直接返回错误。
if err != nil {
return
}
// 遍历members切片对于每个元素使用ZRem命令从更新时间集合中删除对应的成员。
for _, member := range members {
_, err = g.Redis().ZRem(ctx, r.updateTs, member)
// 忽略ZRem操作的错误因为即使元素不存在ZRem也不会返回错误。
}
// 使用ZRemRangeByRank命令从有序集合中删除指定排名范围内的元素。
_, err = g.Redis().ZRemRangeByRank(ctx, r.name, start, stop)
// 返回可能的错误。
return
}
// updateScore 更新给定ID的分数值。
//
// 参数:
//
// id - 需要更新分数的实体ID。
// score - 新的分数值。
//
// 返回值:
//
// error - 更新过程中可能出现的错误。
//
// 该方法首先记录当前时间作为更新时间戳,然后将新的分数值添加到排名系统中。
// 使用Redis的ZAdd方法来确保操作的原子性和一致性。
// UpdateScore 更新分数
//
// @Description:
// @receiver r
// @param id
// @param score
// @return err
func (r *F64CountRank) UpdateScore(id int64, score int64) (err error) {
// 获取当前时间戳,以毫秒为单位。
now := time.Now().UnixMilli()
// 向更新时间戳的有序集合中添加新的成员和分数成员为id分数为当前时间戳。
_, err = g.Redis().ZAdd(ctx, r.updateTs, &gredis.ZAddOption{}, gredis.ZAddMember{
Score: float64(now),
Member: id,
})
// 向排名的有序集合中添加新的成员和分数成员为id分数为传入的score。
_, err = g.Redis().ZAdd(ctx, r.name, &gredis.ZAddOption{}, gredis.ZAddMember{
Score: float64(score),
Member: id,
})
return
}
//// GetRankInfosV1 获取0~count跳记录
//func (r *F64CountRank) getRankInfosV1(offset, count int) (list []*RankInfo, err error) {
// /*
// 找到maxRank的玩家的分数
// 根据分数拿到所有分数大于等于minScore玩家
// 将这些玩家进行排序
// 返回maxRank条目
// */
// var (
// minScore int64 // 最低分
// maxScore int64
// //zl []redis2.Z
// zl []gredis.ZAddMember
// length int
// )
// // 拉取所有玩家的更新时间戳
// zl, err = g.Redis().ZRemRangeByScore(ctx,r.updateTs, strconv.Itoa(0), strconv.Itoa(-1))//ZRemRangeByScore(ctx, r.updateTs, strconv.Itoa(0), strconv.Itoa(-1))
// //zl, err = rdbV1.ZRangeWithScores(ctx, r.updateTs, 0, -1).Result()
// if err != nil {
// g.Log().Errorf(ctx, "redis err:%v", err)
// return
// }
// if len(zl) == 0 {
// //logs.Infof("empty list")
// return
// }
// tsl := make(map[int64]int64, len(zl))
// for _, z := range zl {
// id := gconv.Int64(z.Member) //pgk.InterfaceToNumber[uint64](z.Member)
// tsl[id] = int64(z.Score)
// }
//
// // 找到maxRank的玩家的分数
// zl, err = rdbV1.ZRevRangeByScoreWithScores(ctx, r.name, &redis2.ZRangeBy{
// Min: "0",
// Max: strconv.Itoa(math.MaxInt),
// Offset: 0,
// Count: int64(count),
// }).Result()
// if err != nil {
// g.Log().Errorf(ctx, "redis err:%v", err)
// return
// }
// if len(zl) == 0 {
// g.Log().Info(ctx, "empty list")
// return
// }
// minScore = int64(zl[len(zl)-1].Score)
// maxScore = int64(zl[0].Score)
// // 根据分数拿到所有分数大于等于minScore玩家
// zl, err = rdbV1.ZRevRangeByScoreWithScores(ctx, r.name, &redis2.ZRangeBy{
// Min: fmt.Sprintf("%v", minScore),
// Max: fmt.Sprintf("%v", maxScore),
// }).Result()
// if err != nil {
// g.Log().Errorf(ctx, "redis err:%v", err)
// return
// }
// if len(zl) == 0 {
// g.Log().Info(ctx, "empty list")
// return
// }
// //如果开始已经大于等于总长度,就返回空
// if offset >= len(zl) {
// return
// }
// list = make([]*RankInfo, len(zl))
// for i, z := range zl {
// id := gconv.Int64(z.Member)
// list[i] = &RankInfo{
// Id: id,
// Score: int64(z.Score),
// UpdateTs: tsl[id],
// }
// }
// // 将这些玩家进行排序
// sort.Slice(list, func(i, j int) bool {
// if list[i].Score != list[j].Score {
// return list[i].Score > list[j].Score
// } else {
// return list[i].UpdateTs < list[j].UpdateTs
// }
// })
// length = len(list)
// if length > count {
// length = count
// }
// for i := range list {
// info := list[i]
// info.Rank = i + 1
// }
//
// list = list[offset:length]
// return
//}
// GetRankInfosNotTs 获取0~count跳记录 不根据更新时间来
// 该方法使用ZRange命令从Redis中获取指定范围的排名信息不考虑更新时间
// 参数:
//
// offset - 获取记录的起始偏移量
// count - 获取记录的数量
//
// 返回值:
//
// list - 排名信息列表
// err - 错误信息,如果执行过程中遇到错误
func (r *F64CountRank) GetRankInfosNotTs(offset, count int) (list []*Data, err error) {
// 初始化存储成员ID的切片
var members []int64
// 使用Redis的ZRange命令获取指定范围的成员ID
// 参数Rev设为true以从高分到低分获取成员
get, err := g.Redis().ZRange(ctx, r.name, int64(offset), int64(count),
gredis.ZRangeOption{
Rev: true,
}) //.ScanSlice(&members)
// 将获取的结果扫描到members切片中
err = get.Scan(&members)
// 如果发生错误,记录日志并返回
if err != nil {
//logs.Withf("redis err:%v", err)
return
}
// 根据获取的成员ID数量初始化排名信息列表
list = make([]*Data, len(members))
for i := range members {
// 获取当前成员ID
id := members[i]
// 使用成员ID获取排名信息不考虑更新时间
list[i] = r.GetIdRankNotTs(id)
}
// 返回排名信息列表和可能的错误
return
}
// GetIdRankNotTs 获取指定id的当前排名
// 该方法从Redis的有序集合中查询指定id的分数和排名信息不考虑时间戳
// 参数:
//
// id - 需要查询排名的id
//
// 返回值:
//
// rankInfo - 包含id的分数和排名信息的指针如果没有找到则返回nil
func (r *F64CountRank) GetIdRankNotTs(id int64) (rankInfo *Data) {
// 初始化rankInfo结构体设置id其他字段将通过查询填充
rankInfo = &Data{Id: id}
// 查询有序集合中指定id的分数
score, err := g.Redis().ZScore(ctx, r.name, id)
if err != nil {
// 如果发生错误直接返回rankInfo为初始化状态Id已设置其他字段为零值
return
}
// 将分数转换为int64类型并更新rankInfo
rankInfo.Score = int64(score)
// 如果分数为0直接返回表示该id的分数为0没有进一步查询排名的必要
if score == 0 {
return
}
// 查询有序集合中指定id的排名
rank, err := g.Redis().ZRevRank(ctx, r.name, id)
if err != nil {
// 如果发生错误直接返回rankInfo中仅分数有效排名信息未更新
return
}
// 更新rankInfo中的排名信息排名从0开始所以需要加1以符合人类的计数习惯
rankInfo.Rank = int32(rank) + 1
// 返回包含完整排名信息的rankInfo指针
return rankInfo
}

289
package/s3/s3.go Normal file
View File

@@ -0,0 +1,289 @@
package s3
import (
"fmt"
"io"
"log"
"net/url"
"path"
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcache"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/util/gconv"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
var (
//client *minio.Client
ctx = gctx.New()
)
type DataType struct {
AccessKey string `json:"access_key"`
SecretKey string `json:"secret_key"`
Address string `json:"address"`
Ssl bool `json:"ssl"`
Url string `json:"url"`
BucketName string `json:"bucket_name"`
BucketNameCdn string `json:"bucket_name_cdn"`
}
type Mod struct {
client *minio.Client
cfg DataType
}
func New(_name ...string) *Mod {
var name string
if len(_name) > 0 {
name = _name[0]
} else {
getName, _ := g.Cfg().Get(ctx, "s3.type")
name = getName.String()
}
get, err := g.Cfg().Get(ctx, "s3."+name)
if err != nil {
panic(err.Error())
}
var cfg DataType
get.Scan(&cfg)
// 使用minio-go创建S3客户端
obj, err := minio.New(
cfg.Address,
&minio.Options{
Creds: credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""),
Secure: cfg.Ssl,
},
)
if err != nil {
log.Fatalln(err)
}
mod := &Mod{
client: obj,
cfg: cfg,
}
return mod
}
//func (s *Mod) Load() {
// //导入配置
// get, err := g.Cfg().Get(ctx, "s3.type")
// cfgType := get.String()
// if cfgType == "" {
// cfgType = "default"
// }
//
// cfgData, err := g.Cfg().Get(ctx, "s3."+cfgType)
// if cfgData.IsEmpty() {
// panic("当前配置中未配置s3" + cfgType)
// }
//
// get, err = g.Cfg().Get(ctx, "s3."+cfgType)
// err = get.Scan(&Cfg)
//
// // 使用minio-go创建S3客户端
// obj, err := minio.New(
// Cfg.Address,
// &minio.Options{
// Creds: credentials.NewStaticV4(Cfg.AccessKey, Cfg.SecretKey, ""),
// Secure: Cfg.Ssl,
// },
// )
// if err != nil {
// log.Fatalln(err)
// }
//
// client = obj
//}
//
//func (s *Mod) S3(name string) {
// get, err := g.Cfg().Get(ctx, "s3."+name)
// if err != nil {
// panic(err)
// }
// get.Scan(&Cfg)
//
// // 使用minio-go创建S3客户端
// obj, err := minio.New(
// Cfg.Address,
// &minio.Options{
// Creds: credentials.NewStaticV4(Cfg.AccessKey, Cfg.SecretKey, ""),
// Secure: Cfg.Ssl,
// },
// )
// if err != nil {
// log.Fatalln(err)
// }
//
// client = obj
//
//}
// GetCfg 获取配置
func (s *Mod) GetCfg() *DataType {
return &s.cfg
}
// GetFileUrl 生成指向S3存储桶中指定文件的预签名URL
//
// @Description: 生成一个具有有限有效期的预签名URL可用于访问S3存储桶中的文件。
// @receiver s: S3的实例用于执行S3操作。
// @param name: 要获取预签名URL的文件名。
// @param bucketName: 文件所在的存储桶名称。
// @return presignedURL: 生成的预签名URL可用于访问文件。
// @return err: 在获取预签名URL过程中遇到的任何错误。
func (s *Mod) GetFileUrl(name string, bucketName string, _expires ...time.Duration) (presignedURL *url.URL, err error) {
// 设置预签名URL的有效期为1小时
expires := time.Hour * 1
if len(_expires) > 0 {
expires = _expires[0]
}
cacheKey := fmt.Sprintf("s3:%v:%v", name, bucketName)
get, _ := gcache.Get(ctx, cacheKey)
//g.Dump(get.Vars())
if !get.IsEmpty() {
err = gconv.Struct(get.Val(), &presignedURL)
//presignedURL =
return
}
//expires := time.Duration(604800)
// 调用s3().PresignedGetObject方法生成预签名URL
presignedURL, err = s.client.PresignedGetObject(ctx, bucketName, name, expires, nil)
err = gcache.Set(ctx, cacheKey, presignedURL, expires)
return
}
// PutFileUrl 生成一个用于上传文件到指定bucket的预签名URL
//
// @Description:
// @receiver s
// @param name 文件名
// @param bucketName 存储桶名称
// @return presignedURL 预签名的URL用于上传文件
// @return err 错误信息如果在生成预签名URL时发生错误
func (s *Mod) PutFileUrl(name string, bucketName string) (presignedURL *url.URL, err error) {
// 设置预签名URL的有效期
//expires := time.Now().Add(time.Minute * 30).Unix() // 例如有效期30分钟
//expires2 := time.Duration(expires)
expires := time.Minute * 10
// 生成预签名URL
presignedURL, err = s.client.PresignedPutObject(ctx, bucketName, name, expires)
return
}
// 获取储存桶列表
func (s *Mod) ListBuckets() []minio.BucketInfo {
buckets, err := s.client.ListBuckets(ctx)
//g.Dump(buckets)
if err != nil {
//fmt.Println(err)
return nil
}
return buckets
}
// PutObject 上传文件到指定的存储桶中。
//
// @Description: 上传一个文件到指定的存储桶。
// @receiver s *Mod: 表示调用此方法的Mod实例。
// @param f io.Reader: 文件的读取器,用于读取待上传的文件内容。
// @param name string: 待上传文件的名称。
// @param bucketName string: 存储桶的名称。
// @param _size ...int64: 可选参数,指定上传文件的大小。如果未提供,则默认为-1表示读取文件直到EOF。
// @return res minio.UploadInfo: 上传成功后返回的上传信息。
// @return err error: 如果上传过程中出现错误,则返回错误信息。
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]
}
// 调用client的PutObject方法上传文件并设置内容类型为"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)
}
return
}
// RemoveObject 删除文件
func (s *Mod) RemoveObject(name string, bucketName string) (err error) {
opts := minio.RemoveObjectOptions{
//GovernanceBypass: true,
//VersionID: "myversionid",
}
err = s.client.RemoveObject(ctx, bucketName, name, opts)
return
}
// ListObjects 文件列表
func (s *Mod) ListObjects(bucketName string, prefix string) (res <-chan minio.ObjectInfo, err error) {
res = s.client.ListObjects(ctx, bucketName, minio.ListObjectsOptions{
Prefix: prefix,
})
return
}
// SetBucketPolicy 设置bucket或对象前缀的访问权限
func (s *Mod) SetBucketPolicy(bucketName string, prefix string) (err error) {
policy := `{"Version": "2012-10-17","Statement": [{"Action": ["s3:GetObject"],"Effect": "Allow","Principal": {"AWS": ["*"]},"Resource": ["arn:aws:s3:::my-bucketname/*"],"Sid": ""}]}`
err = s.client.SetBucketPolicy(ctx, bucketName, policy)
return
}
// GetUrl 获取文件访问地址
func (s *Mod) GetUrl(filePath string, defaultFile ...string) (url string) {
bucketName := s.cfg.BucketNameCdn
get := s.cfg.Url
//如果没有图片,返回默认的图片地址
if filePath == "" && len(defaultFile) > 0 {
filePath = defaultFile[0]
}
if s.cfg.Ssl {
url = get + filePath
} else {
url = get + path.Join(bucketName, filePath)
}
return
}
func (s *Mod) GetPath(url string) (filePath string) {
bucketName := s.cfg.BucketNameCdn
get := s.cfg.Url
return url[len(get+bucketName)+1:]
}
// 复制文件
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,
}
_, err = s.client.CopyObject(ctx, dst, src)
return
}