2025-02-24 13:59:58 +08:00

615 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"context"
"crypto/rsa"
"encoding/json"
"errors"
"f5"
"main/constant"
"main/mt"
"q5"
"time"
"github.com/wechatpay-apiv3/wechatpay-go/core"
)
type TokenInfo struct {
GameId int64 `json:"gameid"`
Token string `json:"token"`
Expire int64 `json:"expire"`
}
type expireSession struct {
SessionTime int64
Time int64
}
type mediaidInfo struct {
mediaId string
time int64
}
type wxpay struct {
gamesGoods q5.ConcurrentMap[int64, map[int64]GoodsInfo] //[gameid, [goodsid]goodsinfo]
accessTokens q5.ConcurrentMap[int64, TokenInfo] //[gameid, TokenInfo]
expireInfo q5.ConcurrentMap[string, *expireSession] //[accountid, expireSession]
mediaInfo q5.ConcurrentMap[int64, mediaidInfo] //[gameid, mediainfo]
refreshflag bool
ctx context.Context
client *core.Client
payhtmlstr string
mchpubkey *rsa.PublicKey
}
type WxQuery struct {
AccessToken string `json:"access_token"`
Signature string `json:"signature"`
SigMethod string `json:"sig_method"`
PaySig string `json:"pay_sig"`
}
type WxBalanceRsp struct {
ErrCode int32 `json:"errcode"`
ErrMsg string `json:"errmsg"`
Balance int64 `json:"balance"`
GiftBalance int64 `json:"present_balance"`
SumSave int64 `json:"sum_save"`
SumGift int64 `json:"sum_present"`
SumBalance int64 `json:"sum_balance"`
SumCost int64 `json:"sum_cost"`
FirstSave bool `json:"first_save"`
}
type WxPayRsp struct {
ErrCode int32 `json:"errcode"`
ErrMsg string `json:"errmsg"`
BillNo string `json:"bill_no"`
Balance int64 `json:"balance"`
UsedGift int64 `json:"used_present_amount"`
}
type WxQueryOrderRsp struct {
ErrCode int32 `json:"errcode"`
ErrMsg string `json:"errmsg"`
ProductId string `json:"product_id"` //道具id
PayState int32 `json:"pay_state"` //1:未支付;2:已支付
DeliverState int32 `json:"deliver_state"` //1:未发货;2:已发货
PayTime int64 `json:"pay_finish_time"` //支付完成时间
TrandNo string `json:"out_trade_no"` //用户订单号
MchOrderNo string `json:"mch_order_no"` //微信支付商户单
Trans string `json:"transaction_id"` //微信支付订单号
}
type WxPurchaseNotify struct {
ToUserName string `json:"ToUserName"` //小游戏原始ID
FromUserName string `json:"FromUserName"` //该事件消息的openid道具发货场景固定为微信官方的openid
CreateTime int64 `json:"CreateTime"` //消息发送时间
MsgType string `json:"MsgType"` //消息类型道具发货场景固定为event
Event string `json:"Event"` //事件类型 商城道具场景固定为minigame_h5_goods_deliver_notify 道具直购游戏内场景固定为minigame_game_pay_goods_deliver_notify
MiniGame struct {
Payload string `json:"Payload"` // 携带的具体内容格式为json具体内容如下表格Payload因为这里需要对消息内容统一签名所以统一把消息内容设计成json格式
PayEventSig string `json:"PayEventSig"` //见https://docs.qq.com/doc/DVVZZdHFsYkttYmxlPayEventSig
IsMock bool `json:"IsMock"` //True: 模拟测试推送 False真实推送
} `json:"MiniGame"` //道具直购发货参数
}
type WxPayload struct {
OpenId string `json:"OpenId"` //接收道具的玩家openid
Env int32 `json:"Env"` //环境配置 0现网环境也叫正式环境 1沙箱环境
OutTradeNo string `json:"OutTradeNo"` // 订单号
GoodsInfo struct {
ProductId string `json:"ProductId"` //游戏道具id标识
Quantity int64 `json:"Quantity"` //购买道具数量
ZoneId string `json:"ZoneId"` //分区
OrigPrice int64 `json:"OrigPrice"` //物品原始价格 (单位:分)
ActualPrice int64 `json:"ActualPrice"` //物品实际支付价格(单位:分)
Attach string `json:"Attach"` //透传数据
OrderSource int64 `json:"OrderSource"` // 1 游戏内 2 商城下单 3 商城测试下单
} `json:"GoodsInfo"` //发货道具
WeChatPayInfo struct {
MchOrderNo string `json:"MchOrderNo"` // 微信支付商户单号
TransactionId string `json:"TransactionId"` // 交易单号(微信支付订单号)
} `json:"WeChatPayInfo"` //微信支付信息(仅微信支付渠道)
}
type GoodsInfo struct {
Name string
Count int64
Price int64 //
}
func (wp *wxpay) init() {
wp.gamesGoods = q5.ConcurrentMap[int64, map[int64]GoodsInfo]{}
wp.accessTokens = q5.ConcurrentMap[int64, TokenInfo]{}
wp.expireInfo = q5.ConcurrentMap[string, *expireSession]{}
mt.Table.Wxconfig.Traverse(func(w *mt.Wxconfig) bool {
filename := "../res/gamegoods/" + q5.SafeToString(w.GetGameid()) + ".json"
str, err := f5.ReadJsonFile(filename)
if err != nil {
return true
}
data := []map[string]interface{}{}
if json.Unmarshal([]byte(str), &data) != nil {
f5.GetSysLog().Error("parse [%s] error.", filename)
} else {
gamegoods := map[int64]GoodsInfo{}
for _, data := range data {
gamegoods[q5.SafeToInt64(data["id"])] = GoodsInfo{
Name: q5.SafeToString(data["name"]),
Count: q5.SafeToInt64(data["count"]),
Price: q5.SafeToInt64(data["price"]) * 100,
}
}
wp.gamesGoods.Store(w.GetGameid(), gamegoods)
wp.accessTokens.Store(w.GetGameid(), TokenInfo{})
}
return true
})
go wp.checkExpireInfo()
wp.refreshflag = true
go wp.checkAccessToken()
wp.initMch()
}
func (wp *wxpay) unInit() {
}
func (wp *wxpay) freshAccessToken(gameid int64) (token string) {
cfg := mt.Table.Wxconfig.GetById(gameid)
params := map[string]string{
"grant_type": "client_credential",
"appid": cfg.GetAppid(),
"secret": cfg.GetAppsecret(),
}
urls := mt.Table.Config.GetWxUrl()
queryuri := "/cgi-bin/token"
for _, wxhost := range urls {
url := "https://" + wxhost + queryuri
sendok := false
f5.GetHttpCliMgr().SendGoStyleRequest(
url,
params,
func(hcr f5.HttpCliResponse) {
if hcr.GetErr() != nil {
return
}
rspObj := struct {
Token string `json:"access_token"`
Expire int64 `json:"expires_in"`
ErrCode int64 `json:"errcode"`
ErrMsg string `json:"errmsg"`
}{}
sendok = true
f5.GetSysLog().Debug("wx get access token rsp:%s", hcr.GetRawData())
if json.Unmarshal([]byte(hcr.GetRawData()), &rspObj) != nil {
return
}
if rspObj.ErrCode == 0 {
tokenitem, _ := wp.accessTokens.Load(gameid)
tokenitem.GameId = gameid
tokenitem.Token = rspObj.Token
tokenitem.Expire = rspObj.Expire + f5.GetApp().GetRealSeconds()
wp.accessTokens.Store(gameid, *tokenitem)
}
})
if sendok {
break
}
}
return token
}
func (wp *wxpay) getAccessToken(gameid int64) (token string) {
cfg, exist := wp.accessTokens.Load(gameid)
nowTime := f5.GetApp().GetRealSeconds()
if exist {
if cfg.Expire > nowTime+6 {
if cfg.Expire < nowTime+30 && !wp.refreshflag {
wp.refreshflag = true
}
return cfg.Token
}
if !wp.refreshflag {
wp.refreshflag = true
}
return ""
}
return ""
}
func (wp *wxpay) GetGoodsCount(gameid int64, goodsid int64) (count int64, err error) {
goods, ok := wp.gamesGoods.Load(gameid)
if !ok {
return 0, errors.New("no game")
}
info, ok := (*goods)[goodsid]
if !ok {
return 0, errors.New("no goods")
}
return info.Count, nil
}
func (wp *wxpay) GetGoodsName(gameid int64, goodsid int64) (name string, err error) {
goods, ok := wp.gamesGoods.Load(gameid)
if !ok {
return "", errors.New("no game")
}
info, ok := (*goods)[goodsid]
if !ok {
return "", errors.New("no goods")
}
return info.Name, nil
}
func (wp *wxpay) QueryBalance(openid string, gameid int64, userip string, sessionkey string) (balance int64, wxerrcode int32, err error) {
cfg := mt.Table.Wxconfig.GetById(gameid)
postbody := struct {
OpenId string `json:"openid"`
OfferId string `json:"offer_id"`
Ts int64 `json:"ts"`
ZoneId string `json:"zone_id"`
Env int32 `json:"env"`
UserIp string `json:"user_ip"`
}{
OpenId: openid,
OfferId: cfg.GetOfferid(),
Ts: f5.GetApp().GetRealSeconds(),
ZoneId: cfg.GetZoneid(),
UserIp: userip,
}
if !f5.IsOnlineEnv() {
postbody.Env = 1
}
poststr := q5.EncodeJson(postbody)
queryuri := "/wxa/game/getbalance"
query := WxQuery{
AccessToken: wp.getAccessToken(gameid),
Signature: wp.GenSHA256Signature(poststr, sessionkey),
SigMethod: "hmac_sha256",
PaySig: wp.GenSHA256Signature(queryuri+"&"+poststr, cfg.GetAppkey()),
}
params := map[string]string{}
data, _ := json.Marshal(query)
json.Unmarshal(data, &params)
sendRequest := false
urls := mt.Table.Config.GetWxUrl()
for _, wxhost := range urls {
url := "https://" + wxhost + queryuri
f5.GetHttpCliMgr().SendGoStylePost(
url,
params,
"Content-Type: application/json",
poststr,
func(rsp f5.HttpCliResponse) {
if rsp.GetErr() != nil {
return
}
sendRequest = true
rspJson := WxBalanceRsp{}
f5.GetSysLog().Debug("wx balance rsp:%s", rsp.GetRawData())
err = q5.DecodeJson(rsp.GetRawData(), &rspJson)
if err != nil {
return
}
wxerrcode = rspJson.ErrCode
switch rspJson.ErrCode {
case constant.WX_ERRCODE_OK:
balance = rspJson.Balance
case constant.WX_ERRCODE_BUSY:
sendRequest = false
default:
f5.GetSysLog().Info("err msg:%s", rspJson.ErrMsg)
wp.checkErrorCode(rspJson.ErrCode)
}
})
if sendRequest {
break
}
}
return balance, wxerrcode, err
}
func (wp *wxpay) QueryPay(openid string, gameid int64, userip string, sessionkey string, amount int32, billno string) (wxerrcode int32) {
cfg := mt.Table.Wxconfig.GetById(gameid)
postbody := struct {
OpenId string `json:"openid"`
OfferId string `json:"offer_id"`
Ts int64 `json:"ts"`
ZoneId string `json:"zone_id"`
Env int32 `json:"env"`
UserIp string `json:"user_ip"`
Amount int32 `json:"amount"`
BillNo string `json:"bill_no"`
}{
OpenId: openid,
OfferId: cfg.GetOfferid(),
Ts: f5.GetApp().GetRealSeconds(),
ZoneId: cfg.GetZoneid(),
UserIp: userip,
Amount: amount,
BillNo: billno,
}
if !f5.IsOnlineEnv() {
postbody.Env = 1
}
poststr := q5.EncodeJson(postbody)
queryuri := "/wxa/game/pay"
query := WxQuery{
AccessToken: wp.getAccessToken(gameid),
Signature: wp.GenSHA256Signature(poststr, sessionkey),
SigMethod: "hmac_sha256",
PaySig: wp.GenSHA256Signature(queryuri+"&"+poststr, cfg.GetAppkey()),
}
params := map[string]string{}
data, _ := json.Marshal(query)
json.Unmarshal(data, &params)
sendRequest := false
urls := mt.Table.Config.GetWxUrl()
for _, wxhost := range urls {
url := "https://" + wxhost + queryuri
f5.GetHttpCliMgr().SendGoStylePost(
url,
params,
"Content-Type: application/json",
poststr,
func(rsp f5.HttpCliResponse) {
if rsp.GetErr() != nil {
return
}
sendRequest = true
rspJson := WxPayRsp{}
f5.GetSysLog().Debug("wx pay rsp:%s", rsp.GetRawData())
err := q5.DecodeJson(rsp.GetRawData(), &rspJson)
if err != nil {
return
}
wxerrcode = rspJson.ErrCode
switch rspJson.ErrCode {
case constant.WX_ERRCODE_OK:
case constant.WX_ERRCODE_BUSY:
sendRequest = false
default:
f5.GetSysLog().Info("err msg:%s", rspJson.ErrMsg)
wp.checkErrorCode(rspJson.ErrCode)
}
})
if sendRequest {
break
}
}
return wxerrcode
}
func (wp *wxpay) QueryPurchase(openid string, gameid int64, userip string, sessionkey string, amount int32, tradeno string) (wxerrcode int32) {
cfg := mt.Table.Wxconfig.GetById(gameid)
postbody := struct {
OpenId string `json:"openid"`
Ts int64 `json:"ts"`
Env int32 `json:"env"`
TradeNo string `json:"out_trade_no"`
BizId int32 `json:"biz_id"`
OfferId string `json:"offer_id"`
}{
OpenId: openid,
Ts: f5.GetApp().GetRealSeconds(),
BizId: 2, //1代币 2道具直购
TradeNo: tradeno,
OfferId: cfg.GetOfferid(),
}
if !f5.IsOnlineEnv() {
postbody.Env = 1
}
poststr := q5.EncodeJson(postbody)
queryuri := "/wxa/game/queryorderinfo"
query := WxQuery{
AccessToken: wp.getAccessToken(gameid),
Signature: wp.GenSHA256Signature(poststr, sessionkey),
SigMethod: "hmac_sha256",
PaySig: wp.GenSHA256Signature(queryuri+"&"+poststr, cfg.GetAppkey()),
}
params := map[string]string{}
data, _ := json.Marshal(query)
json.Unmarshal(data, &params)
sendRequest := false
urls := mt.Table.Config.GetWxUrl()
for _, wxhost := range urls {
url := "https://" + wxhost + queryuri
f5.GetHttpCliMgr().SendGoStylePost(
url,
params,
"Content-Type: application/json",
poststr,
func(rsp f5.HttpCliResponse) {
if rsp.GetErr() != nil {
return
}
sendRequest = true
rspJson := WxQueryOrderRsp{}
f5.GetSysLog().Debug("wx query order rsp:%s", rsp.GetRawData())
err := q5.DecodeJson(rsp.GetRawData(), &rspJson)
if err != nil {
return
}
wxerrcode = rspJson.ErrCode
switch rspJson.ErrCode {
case constant.WX_ERRCODE_OK:
case constant.WX_ERRCODE_BUSY:
sendRequest = false
default:
f5.GetSysLog().Info("err msg:%s", rspJson.ErrMsg)
wp.checkErrorCode(rspJson.ErrCode)
}
})
if sendRequest {
break
}
}
return wxerrcode
}
func (wp *wxpay) AddExpireInfo(accountid string, sessiontime int64) {
info := expireSession{
SessionTime: sessiontime,
Time: f5.GetApp().GetRealSeconds(),
}
wp.expireInfo.Store(accountid, &info)
}
func (wp *wxpay) CheckExpireCache(accountid string, expire int64) bool {
info, exist := wp.expireInfo.Load(accountid)
if !exist {
return false
}
if (*info).SessionTime < expire {
wp.expireInfo.Delete(accountid)
return false
}
return true
}
func (wp *wxpay) GetAccessTokenList(data *[]TokenInfo) {
wp.accessTokens.Range(func(key int64, value TokenInfo) bool {
*data = append(*data, value)
return true
})
}
func (wp *wxpay) checkExpireInfo() {
for {
time.Sleep(time.Minute * 30)
nowTime := f5.GetApp().GetRealSeconds()
deletelist := map[string]int64{}
wp.expireInfo.Range(func(accountid string, value *expireSession) bool {
if value.Time+3600 < nowTime {
deletelist[accountid] = value.Time
}
return true
})
for accountid := range deletelist {
wp.expireInfo.Delete(accountid)
}
}
}
func (wp *wxpay) checkAccessToken() {
for {
time.Sleep(time.Second)
if f5.IsOnlineEnv() {
wp.gamesGoods.Range(func(gameid int64, value map[int64]GoodsInfo) bool {
cfg, _ := wp.accessTokens.Load(gameid)
if wp.refreshflag || (cfg != nil && cfg.Expire <= f5.GetApp().GetRealSeconds()) {
wp.freshAccessToken(gameid)
}
return true
})
} else {
if !wp.refreshflag {
wp.accessTokens.Range(func(key int64, value TokenInfo) bool {
if value.Expire <= f5.GetApp().GetRealSeconds() {
wp.refreshflag = true
return false
}
return true
})
if !wp.refreshflag {
continue
}
}
url := "https://payservice.kingsome.cn/api/service/refresh"
nowtimestr := q5.SafeToString(f5.GetApp().GetRealSeconds())
params := map[string]string{
"time": nowtimestr,
"sign": wp.GenSHA256Signature(nowtimestr+constant.GLOBAL_SALT, constant.GLOBAL_SALT),
}
f5.GetHttpCliMgr().SendGoStyleRequest(
url,
params,
func(hcr f5.HttpCliResponse) {
if hcr.GetErr() != nil {
return
}
rspObj := struct {
Data []TokenInfo `json:"data"`
ErrCode int64 `json:"errcode"`
ErrMsg string `json:"errmsg"`
}{}
f5.GetSysLog().Debug("get online payservice rsp:%s", hcr.GetRawData())
if json.Unmarshal([]byte(hcr.GetRawData()), &rspObj) != nil {
return
}
if rspObj.ErrCode == 0 {
for _, dataitem := range rspObj.Data {
wp.accessTokens.Store(dataitem.GameId, dataitem)
}
}
})
}
wp.refreshflag = false
}
}
func (wp *wxpay) checkErrorCode(errcode int32) {
if errcode == constant.WX_ERRCODE_EXPIRE_ACCESSTOKEN ||
errcode == constant.WX_ERRCODE_MISSING_ACCESSTOKEN ||
errcode == constant.WX_ERRCODE_UNAUTH_ACCESSTOKEN {
if !wp.refreshflag {
wp.refreshflag = true
}
}
}