From 039acea0affab1121ab5d17c4c7dcee4f5696f4f Mon Sep 17 00:00:00 2001 From: ayflying Date: Tue, 5 Aug 2025 18:19:42 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=8D=8E=E4=B8=BA=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package/pay/huawei/consts.go | 32 ++++++ package/pay/huawei/huawei.go | 159 +++++++++++++++++++++++++++++ package/pay/huawei/model.go | 49 +++++++++ package/pay/huawei/notification.go | 107 +++++++++++++++++++ package/pay/vivo/sign.go | 5 +- 5 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 package/pay/huawei/consts.go create mode 100644 package/pay/huawei/huawei.go create mode 100644 package/pay/huawei/model.go create mode 100644 package/pay/huawei/notification.go diff --git a/package/pay/huawei/consts.go b/package/pay/huawei/consts.go new file mode 100644 index 0000000..1aba07d --- /dev/null +++ b/package/pay/huawei/consts.go @@ -0,0 +1,32 @@ +package huawei + +import ( + "net/http" + "time" +) + +const ( + AuthTokenUrl = "https://oauth-api.cloud.huawei.com/rest.php?nsp_fmt=JSON&nsp_svc=huawei.oauth2.user.getTokenInfo" + OrderUrl = "https://orders-drcn.iap.hicloud.com/applications/purchases/tokens/verify" + LocationShanghai = "Asia/Shanghai" + + RSA = "RSA" + RSA2 = "RSA2" + + OrderResponseOk = "0" + PurchaseStateOk = 0 +) + +func getOrderUrl(accountFlag int) string { + if accountFlag == 1 { + // site for telecom carrier + return "https://orders-at-dre.iap.dbankcloud.com" + } 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} diff --git a/package/pay/huawei/huawei.go b/package/pay/huawei/huawei.go new file mode 100644 index 0000000..7e30ecb --- /dev/null +++ b/package/pay/huawei/huawei.go @@ -0,0 +1,159 @@ +package huawei + +import ( + "bytes" + "crypto" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "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: 建议根据华为支付文档解析响应数据(如检查code是否为0表示成功) + log.Printf("%s", bodyBytes) +} + +// VerifyToken 验证回调订单 +//您可以调用本接口向华为应用内支付服务器校验支付结果中的购买令牌,确认支付结果的准确性。 +func (p *Pay) VerifyToken(purchaseToken, productId string, accountFlag int) { + bodyMap := map[string]string{"purchaseToken": purchaseToken, "productId": productId} + url := getOrderUrl(accountFlag) + "/applications/purchases/tokens/verify" + bodyBytes, err := p.SendRequest(url, bodyMap) + if err != nil { + log.Printf("err is %s", err) + } + // TODO: display the response as string in console, you can replace it with your business logic. + log.Printf("%s", bodyBytes) +} + +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 { + 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(p.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 +} diff --git a/package/pay/huawei/model.go b/package/pay/huawei/model.go new file mode 100644 index 0000000..4685f62 --- /dev/null +++ b/package/pay/huawei/model.go @@ -0,0 +1,49 @@ +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"` +} diff --git a/package/pay/huawei/notification.go b/package/pay/huawei/notification.go new file mode 100644 index 0000000..804c6cc --- /dev/null +++ b/package/pay/huawei/notification.go @@ -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 +} diff --git a/package/pay/vivo/sign.go b/package/pay/vivo/sign.go index cd0c481..a05b5cb 100644 --- a/package/pay/vivo/sign.go +++ b/package/pay/vivo/sign.go @@ -1,7 +1,9 @@ package vivo import ( + "context" "errors" + "github.com/ayflying/utility_go/package/pay/common" "github.com/gogf/gf/v2/crypto/gmd5" "github.com/gogf/gf/v2/frame/g" "github.com/gogf/gf/v2/util/gconv" @@ -10,7 +12,8 @@ import ( "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"] delete(bm, "signature") delete(bm, "signMethod")