Compare commits

..

5 Commits

Author SHA1 Message Date
ayflying
0628882533 缓存key删除失败不报错 2025-08-20 15:13:01 +08:00
ayflying
f68655eee6 修改持久化act的异常报错 2025-08-14 14:11:26 +08:00
ayflying
d8491f0aba 修复ip库无法显示的问题 2025-08-07 11:17:04 +08:00
ayflying
999f1f6a84 完成华为支付的验单 2025-08-06 10:26:08 +08:00
ayflying
039acea0af 增加华为支付模块 2025-08-05 18:19:42 +08:00
8 changed files with 412 additions and 35 deletions

View File

@@ -3,6 +3,10 @@ package gameAct
import ( import (
"context" "context"
"fmt" "fmt"
"strconv"
"strings"
"time"
"github.com/ayflying/utility_go/internal/model/do" "github.com/ayflying/utility_go/internal/model/do"
"github.com/ayflying/utility_go/internal/model/entity" "github.com/ayflying/utility_go/internal/model/entity"
"github.com/ayflying/utility_go/pkg" "github.com/ayflying/utility_go/pkg"
@@ -13,9 +17,6 @@ import (
"github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/util/gconv" "github.com/gogf/gf/v2/util/gconv"
"strconv"
"strings"
"time"
) )
var ( var (
@@ -206,37 +207,29 @@ func (s *sGameAct) Save(ctx context.Context, actId int) (err error) {
g.Log().Error(ctx, err2) g.Log().Error(ctx, err2)
return return
} }
////获取多少个数据,删除不是当前修改的数据
//count, _ := g.Model(Name).Where(do.GameAct{
// Uid: v.Uid,
// ActId: v.ActId,
//}).Count()
//if count > 1 {
// g.Model(Name).Where(do.GameAct{
// Uid: v.Uid,
// ActId: v.ActId,
// }).WhereNot("updated_at", v.UpdatedAt).Delete()
//}
} }
//dbRes, err2 := g.Model(Name).Batch(50).Data(add).Update()
update = make([]*entity.GameAct, 0) update = make([]*entity.GameAct, 0)
var count int64
if len(add) > 0 {
dbRes, err2 := g.Model(Name).Batch(50).Data(add).Save() dbRes, err2 := g.Model(Name).Batch(50).Data(add).Save()
add = make([]*entity.GameAct, 0) add = make([]*entity.GameAct, 0)
if err2 != nil { err = err2
if err != nil {
g.Log().Error(ctx, err2) g.Log().Error(ctx, err2)
return return
} }
count, _ = dbRes.RowsAffected()
}
for _, v := range delKey { for _, v := range delKey {
_, err2 = g.Redis().Del(ctx, v) _, err = g.Redis().Del(ctx, v)
if err2 != nil { if err != nil {
g.Log().Error(ctx, err2) g.Log().Error(ctx, err)
return
} }
} }
delKey = make([]string, 0) delKey = make([]string, 0)
count, _ := dbRes.RowsAffected()
g.Log().Debugf(ctx, "当前 %v 写入数据库: %v 条", actId, count) g.Log().Debugf(ctx, "当前 %v 写入数据库: %v 条", actId, count)
} }

View File

@@ -2,16 +2,17 @@ package gameKv
import ( import (
"fmt" "fmt"
"strconv"
"strings"
"sync"
"time"
"github.com/ayflying/utility_go/pkg" "github.com/ayflying/utility_go/pkg"
"github.com/ayflying/utility_go/service" "github.com/ayflying/utility_go/service"
"github.com/ayflying/utility_go/tools" "github.com/ayflying/utility_go/tools"
"github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gtime" "github.com/gogf/gf/v2/os/gtime"
"strconv"
"strings"
"sync"
"time"
) )
var ( var (
@@ -107,8 +108,7 @@ func (s *sGameKv) SavesV1() (err error) {
for _, v := range delKey { for _, v := range delKey {
_, err2 = g.Redis().Del(ctx, v) _, err2 = g.Redis().Del(ctx, v)
if err2 != nil { if err2 != nil {
g.Log().Errorf(ctx, "删除存档错误%v,err=%v", v, err2) g.Log().Errorf(ctx, "删除存档失败%v,err=%v", v, err2)
return
} }
} }

View File

@@ -1,13 +1,14 @@
package ip2region package ip2region
import ( import (
"net"
"strings"
"github.com/ayflying/utility_go/service" "github.com/ayflying/utility_go/service"
"github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx" "github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/os/gfile"
"github.com/lionsoul2014/ip2region/binding/golang/xdb" "github.com/lionsoul2014/ip2region/binding/golang/xdb"
"net"
"strings"
) )
var ( var (
@@ -81,7 +82,7 @@ func (s *sIp2region) Load() {
func (s *sIp2region) GetIp(ip string) (res []string) { func (s *sIp2region) GetIp(ip string) (res []string) {
//初始化加载 //初始化加载
if s.searcher != nil { if s.searcher == nil {
s.Load() s.Load()
} }

View File

@@ -0,0 +1,25 @@
package huawei
import (
"net/http"
"time"
)
const (
TokenUrl = "https://oauth-login.cloud.huawei.com/oauth2/v3/token"
)
func getOrderUrl(accountFlag int) string {
if accountFlag == 1 {
// site for telecom carrier
//return "https://orders-at-dre.iap.dbankcloud.com"
return "https://orders-drcn.iap.cloud.huawei.com.cn"
} else {
// TODO: replace the (ip:port) to the real one
return "http://exampleserver/_mockserver_"
}
}
// default http client with 5 seconds timeout
var RequestHttpClient = http.Client{Timeout: time.Second * 5}

View File

@@ -0,0 +1,166 @@
package huawei
import (
"bytes"
"crypto"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"io"
"log"
"net/http"
"net/url"
)
type Pay struct {
ClientSecret string `json:"client_secret"`
ClientId string `json:"client_id"`
//TokenUrl string `json:"token_url"`
ApplicationPublicKey string `json:"application_public_key"`
}
func New(cfg *Pay) *Pay {
return cfg
}
// ConfirmPurchase 发货后确认购买接口(华为支付)消耗商品
// 功能:通知华为支付平台当前订单已完成发货,触发支付完成流程(需在商品实际发货后调用)
// 参数说明:
// purchaseToken: 华为支付返回的购买令牌(唯一标识一笔具体的购买交易,由客户端支付成功后返回)
// productId: 应用内商品的唯一标识需与客户端发起支付时使用的productId一致
// accountFlag: 账户标识(用于区分不同账户体系/环境如0-普通用户、1-企业用户,具体值由业务定义)
func (p *Pay) ConfirmPurchase(purchaseToken, productId string, accountFlag int) {
// 构造请求体参数包含购买令牌和产品ID
bodyMap := map[string]string{
"purchaseToken": purchaseToken, // 华为支付返回的购买凭证
"productId": productId, // 对应应用内商品的唯一标识
}
url := getOrderUrl(accountFlag) + "/applications/v2/purchases/confirm"
bodyBytes, err := p.SendRequest(url, bodyMap)
if err != nil {
// 请求失败时记录错误日志(实际业务中建议增加重试或异常处理逻辑)
log.Printf("err is %s", err)
}
// 打印响应结果(实际业务中需替换为具体处理逻辑,如更新订单状态、校验响应数据等)
// TODO: 建议根据华为支付文档解析响应数据如检查responseCode是否为0表示成功
log.Printf("%s", bodyBytes)
}
// VerifyToken 验证回调订单
//您可以调用本接口向华为应用内支付服务器校验支付结果中的购买令牌,确认支付结果的准确性。
func (p *Pay) VerifyToken(purchaseToken, productId string, accountFlag int) (res *PurchaseTokenData, err error) {
bodyMap := map[string]string{"purchaseToken": purchaseToken, "productId": productId}
url := getOrderUrl(accountFlag) + "/applications/purchases/tokens/verify"
bodyBytes, err := p.SendRequest(url, bodyMap)
if err != nil {
g.Log().Error(gctx.New(), "err is %s", err)
}
var data *VerifyTokenRes
err = gjson.DecodeTo(bodyBytes, &data)
err = gjson.DecodeTo(data.PurchaseTokenData, &res)
return
}
func (p *Pay) SendRequest(url string, bodyMap map[string]string) (string, error) {
authHeaderString, err := p.BuildAuthorization()
if err != nil {
return "", err
}
bodyString, err := json.Marshal(bodyMap)
if err != nil {
return "", err
}
req, err := http.NewRequest("POST", url, bytes.NewReader(bodyString))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json; charset=UTF-8")
req.Header.Set("Authorization", authHeaderString)
response, err := RequestHttpClient.Do(req)
defer response.Body.Close()
bodyBytes, err := io.ReadAll(response.Body)
//req, err := g.Client().Header(g.MapStrStr{
// "Content-Type": "application/json; charset=UTF-8",
// "Authorization": authHeaderString,
//}).Post(gctx.New(), url, bodyString)
//defer req.Close()
//var bodyBytes = req.ReadAll()
if err != nil {
return "", err
}
return string(bodyBytes), nil
}
func (p *Pay) VerifyRsaSign(content string, sign string, publicKey string) error {
//publicKey = common.FormatPublicKey(publicKey)
publicKeyByte, err := base64.StdEncoding.DecodeString(publicKey)
if err != nil {
return err
}
pub, err := x509.ParsePKIXPublicKey(publicKeyByte)
if err != nil {
return err
}
hashed := sha256.Sum256([]byte(content))
signature, err := base64.StdEncoding.DecodeString(sign)
if err != nil {
return err
}
return rsa.VerifyPKCS1v15(pub.(*rsa.PublicKey), crypto.SHA256, hashed[:], signature)
}
func (p *Pay) GetAppAt() (string, error) {
//demoConfig := GetDefaultConfig()
urlValue := url.Values{
"grant_type": {"client_credentials"},
"client_secret": {p.ClientSecret},
"client_id": {p.ClientId},
}
resp, err := RequestHttpClient.PostForm(TokenUrl, urlValue)
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
//post := g.MapStrStr{
// "grant_type": "client_credentials",
// "client_secret": p.ClientSecret,
// "client_id": p.ClientId,
//}
//resp, err := g.Client().PostForm(gctx.New(), p.TokenUrl, post)
//if err != nil {
// return "", err
//}
//resp.Close()
//bodyBytes := resp.ReadAll()
if err != nil {
return "", err
}
var atResponse AtResponse
json.Unmarshal(bodyBytes, &atResponse)
if atResponse.AccessToken != "" {
return atResponse.AccessToken, nil
} else {
return "", errors.New("Get token fail, " + string(bodyBytes))
}
}
func (p *Pay) BuildAuthorization() (string, error) {
appAt, err := p.GetAppAt()
if err != nil {
return "", err
}
oriString := fmt.Sprintf("APPAT:%s", appAt)
var authString = base64.StdEncoding.EncodeToString([]byte(oriString))
var authHeaderString = fmt.Sprintf("Basic %s", authString)
return authHeaderString, nil
}

View File

@@ -0,0 +1,82 @@
package huawei
type CallbackType struct {
Version string `json:"version"`
NotifyTime int64 `json:"notifyTime"`
EventType string `json:"eventType"`
ApplicationId string `json:"applicationId"`
OrderNotification *OrderNotification `json:"orderNotification"`
SubNotification *SubNotification `json:"subNotification"`
}
type OrderNotification struct {
Version string `json:"version" dc:"通知版本v2"`
NotificationType int `json:"notificationType" dc:"通知事件的类型取值如下1支付成功 2退款成功"`
PurchaseToken string `json:"purchaseToken" dc:"待下发商品的购买Token"`
ProductId string `json:"productId" dc:"商品ID"`
}
type SubNotification struct {
StatusUpdateNotification *StatusUpdateNotification `json:"statusUpdateNotification" dc:"通知消息"`
NotificationSignature string `json:"notificationSignature" dc:"对statusUpdateNotification字段的签名字符串签名算法为signatureAlgorithm表示的签名算法。"`
Version string `json:"version" dc:"通知版本v2"`
SignatureAlgorithm string `json:"signatureAlgorithm" dc:"签名算法。"`
}
// StatusUpdateNotification 订阅状态更新通知
type StatusUpdateNotification struct {
Environment string `json:"environment" dc:"发送通知的环境。PROD正式环境Sandbox沙盒测试"`
NotificationType int `json:"notificationType" dc:"通知事件的类型,具体定义需参考相关文档说明"`
SubscriptionID string `json:"subscriptionId" dc:"订阅ID"`
CancellationDate int64 `json:"cancellationDate" dc:"撤销订阅时间或退款时间UTC时间戳以毫秒为单位仅在notificationType取值为CANCEL的场景下会传入"`
OrderID string `json:"orderId" dc:"订单ID唯一标识一笔需要收费的收据由华为应用内支付服务器在创建订单以及订阅型商品续费时生成。每一笔新的收据都会使用不同的orderId。通知类型为NEW_RENEWAL_PREF时不存在"`
LatestReceipt string `json:"latestReceipt" dc:"最近的一笔收据的token仅在notificationType取值为INITIAL_BUY 、RENEWAL或INTERACTIVE_RENEWAL并且续期成功情况下传入"`
LatestReceiptInfo string `json:"latestReceiptInfo" dc:"最近的一笔收据JSON字符串格式包含的参数请参见InappPurchaseDetails在notificationType取值为CANCEL时无值"`
LatestReceiptInfoSignature string `json:"latestReceiptInfoSignature" dc:"对latestReceiptInfo的签名字符串签名算法为statusUpdateNotification中的signatureAlgorithm。服务器在收到签名字符串后需要参见对返回结果验签使用IAP公钥对latestReceiptInfo的JSON字符串进行验签。公钥获取请参见查询支付服务信息"`
LatestExpiredReceipt string `json:"latestExpiredReceipt" dc:"最近的一笔过期收据的token"`
LatestExpiredReceiptInfo string `json:"latestExpiredReceiptInfo" dc:"最近的一笔过期收据JSON字符串格式在notificationType取值为RENEWAL或INTERACTIVE_RENEWAL时有值"`
LatestExpiredReceiptInfoSignature string `json:"latestExpiredReceiptInfoSignature" dc:"对latestExpiredReceiptInfo的签名字符串签名算法为statusUpdateNotification中的signatureAlgorithm。服务器在收到签名字符串后需要参见对返回结果验签使用IAP公钥对latestExpiredReceiptInfo的JSON字符串进行验签。公钥获取请参见查询支付服务信息"`
AutoRenewStatus int `json:"autoRenewStatus" dc:"续期状态。取值说明1当前周期到期后正常续期0用户已终止续期"`
RefundPayOrderId string `json:"refundPayOrderId" dc:"退款交易号在notificationType取值为CANCEL时有值"`
ProductID string `json:"productId" dc:"订阅型商品ID"`
ApplicationID string `json:"applicationId" dc:"应用ID"`
ExpirationIntent int `json:"expirationIntent" dc:"超期原因仅在notificationType为RENEWAL或INTERACTIVE_RENEWAL时并且续期失败情况下有值"`
PurchaseToken string `json:"purchaseToken" dc:"订阅token与上述订阅ID字段subscriptionId对应。"`
}
type AtResponse struct {
AccessToken string `json:"access_token"`
}
type VerifyTokenRes struct {
ResponseCode string `json:"responseCode"`
PurchaseTokenData string `json:"purchaseTokenData"`
DataSignature string `json:"dataSignature"`
SignatureAlgorithm string `json:"signatureAlgorithm"`
}
type PurchaseTokenData struct {
AutoRenewing bool `json:"autoRenewing" dc:"表示订阅是否自动续费"`
OrderId string `json:"orderId" dc:"订单ID唯一标识一笔订单"`
PackageName string `json:"packageName" dc:"应用的包名"`
ApplicationId int `json:"applicationId" dc:"应用ID以整数形式表示"`
ApplicationIdString string `json:"applicationIdString" dc:"应用ID的字符串形式"`
Kind int `json:"kind" dc:"购买类型的某种标识,具体含义可能取决于业务逻辑"`
ProductId string `json:"productId" dc:"商品ID用于标识购买的商品"`
ProductName string `json:"productName" dc:"商品名称"`
PurchaseTime int64 `json:"purchaseTime" dc:"购买时间,可能是某种特定格式的时间表示"`
PurchaseTimeMillis int64 `json:"purchaseTimeMillis" dc:"购买时间,以毫秒为单位的时间戳"`
PurchaseState int `json:"purchaseState" dc:"购买状态,不同的整数值代表不同的状态,具体含义取决于业务逻辑"`
DeveloperPayload string `json:"developerPayload" dc:"开发者自定义负载数据"`
PurchaseToken string `json:"purchaseToken" dc:"购买令牌"`
ResponseCode string `json:"responseCode" dc:"响应代码,用于表示购买操作的响应结果"`
ConsumptionState int `json:"consumptionState" dc:"消费状态,不同的整数值代表不同的消费状态,具体含义取决于业务逻辑"`
Confirmed int `json:"confirmed" dc:"确认状态,不同的整数值代表不同的确认情况,具体含义取决于业务逻辑"`
PurchaseType int `json:"purchaseType" dc:"购买类型,不同的整数值代表不同的购买类型,具体含义取决于业务逻辑"`
Currency string `json:"currency" dc:"货币类型"`
Price int `json:"price" dc:"商品价格"`
Country string `json:"country" dc:"购买所在国家"`
PayOrderId string `json:"payOrderId" dc:"支付订单ID"`
PayType string `json:"payType" dc:"支付类型"`
SdkChannel string `json:"sdkChannel" dc:"SDK渠道"`
}

View File

@@ -0,0 +1,107 @@
package huawei
/*
* Copyright 2020. Huawei Technologies Co., Ltd. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
//import "encoding/json"
const (
INITIAL_BUY = 0
CANCEL = 1
RENEWAL = 2
INTERACTIVE_RENEWAL = 3
NEW_RENEWAL_PREF = 4
RENEWAL_STOPPED = 5
RENEWAL_RESTORED = 6
RENEWAL_RECURRING = 7
ON_HOLD = 9
PAUSED = 10
PAUSE_PLAN_CHANGED = 11
PRICE_CHANGE_CONFIRMED = 12
DEFERRED = 13
)
//type NotificationServer struct {
//}
//
//var NotificationDemo = &NotificationServer{}
//type NotificationRequest struct {
// StatusUpdateNotification string `json:"statusUpdateNotification"`
// NotificationSignature string `json:"notifycationSignature"`
//}
//
//type NotificationResponse struct {
// ErrorCode string `json:"errorCode"`
// ErrorMsg string `json:"errorMsg"`
//}
//type StatusUpdateNotification struct {
// Environment string `json:"environment"`
// NotificationType int `json:"notificationType"`
// SubscriptionID string `json:"subscriptionId"`
// CancellationDate int64 `json:"cancellationDate"`
// OrderID string `json:"orderId"`
// LatestReceipt string `json:"latestReceipt"`
// LatestReceiptInfo string `json:"latestReceiptInfo"`
// LatestReceiptInfoSignature string `json:"latestReceiptInfoSignature"`
// LatestExpiredReceipt string `json:"latestExpiredReceipt"`
// LatestExpiredReceiptInfo string `json:"latestExpiredReceiptInfo"`
// LatestExpiredReceiptInfoSignature string `json:"latestExpiredReceiptInfoSignature"`
// AutoRenewStatus int `json:"autoRenewStatus"`
// RefundPayOrderId string `json:"refundPayOrderId"`
// ProductID string `json:"productId"`
// ApplicationID string `json:"applicationId"`
// ExpirationIntent int `json:"expirationIntent"`
//}
func (p *Pay) DealNotification(information string) (err error) {
//var request PayCallback
//err = json.Unmarshal([]byte(information), &request)
//if err != nil {
// return
//}
//err = p.VerifyRsaSign(request.StatusUpdateNotification, request.NotificationSignature, DefaultConfig.ApplicationPublicKey)
//if err != nil {
// return
//}
//
//var info = request.StatusUpdateNotification
////json.Unmarshal([]byte(request.StatusUpdateNotification), &info)
//switch notificationType := info.NotificationType; notificationType {
//case INITIAL_BUY:
//case CANCEL:
//case RENEWAL:
//case INTERACTIVE_RENEWAL:
//case NEW_RENEWAL_PREF:
//case RENEWAL_STOPPED:
//case RENEWAL_RESTORED:
//case RENEWAL_RECURRING:
//case ON_HOLD:
//case PAUSED:
//case PAUSE_PLAN_CHANGED:
//case PRICE_CHANGE_CONFIRMED:
//case DEFERRED:
//default:
//}
//
////response := NotificationResponse{ErrorCode: "0"}
////return &response, nil
//return
return
}

View File

@@ -1,7 +1,9 @@
package vivo package vivo
import ( import (
"context"
"errors" "errors"
"github.com/ayflying/utility_go/package/pay/common"
"github.com/gogf/gf/v2/crypto/gmd5" "github.com/gogf/gf/v2/crypto/gmd5"
"github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv" "github.com/gogf/gf/v2/util/gconv"
@@ -10,7 +12,8 @@ import (
"strings" "strings"
) )
func (p *Pay) VerifySign(bm g.Map, key string) bool { func (p *Pay) VerifySign(ctx context.Context, key string) bool {
bm, _ := common.ParseNotifyToBodyMap(g.RequestFromCtx(ctx).Request)
signature := bm["signature"] signature := bm["signature"]
delete(bm, "signature") delete(bm, "signature")
delete(bm, "signMethod") delete(bm, "signMethod")