461 lines
15 KiB
Go
461 lines
15 KiB
Go
package rchgutil
|
|
|
|
import (
|
|
"91porn-server/common/constant/redisconst"
|
|
"bytes"
|
|
"context"
|
|
"crypto/md5"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"91porn-server/app/appg"
|
|
"91porn-server/common/httputil"
|
|
"91porn-server/common/log"
|
|
"91porn-server/common/stderr"
|
|
|
|
"github.com/vmihailenco/msgpack/v5"
|
|
"golang.org/x/sync/singleflight"
|
|
)
|
|
|
|
var payTypeCacheGroup singleflight.Group
|
|
|
|
type GainPayTypeReq struct {
|
|
Time string `json:"time" bson:"time"` // 时间戳,最大长度50,必填
|
|
Sign string `json:"sign" bson:"sign"` // 签名,最大长度100,必填
|
|
MercID string `json:"mercId" bson:"mercId"` // 商户ID,最大长度50,必填
|
|
Money []string `json:"money" bson:"money"` // 金额数组,必填
|
|
Mode string `json:"mode,omitempty"` // 支付模式,兼容商户版本迭代,传sdk表示支持sdk模式,可选
|
|
Payload string `json:"payload,omitempty"` // 备用参数:1-不匹配赔付渠道 2-空单 3-兑换划转,可选
|
|
|
|
}
|
|
|
|
type AllPayType struct {
|
|
Money string `json:"money" bson:"money"` // 金额
|
|
Types []mercPayTypeInfo `json:"types" bson:"types"` // 支付类型
|
|
}
|
|
|
|
type mercPayTypeInfo struct {
|
|
Name string `json:"name" bson:"name"` // 支付类型名称
|
|
Type string `json:"type" bson:"type"` // 支付类型
|
|
}
|
|
|
|
type GainPayTypeResp struct {
|
|
Code int `json:"code" bson:"code"` // code码
|
|
Info string `json:"info" bson:"info"` // code码信息
|
|
Err string `json:"err" bson:"err"` // 错误信息
|
|
Msg []AllPayType `json:"msg" bson:"msg"` // 响应数据
|
|
Tip string `json:"tip" bson:"tip"` // 备注
|
|
}
|
|
|
|
// GetPayTypeFromCache 获取支付通道并缓存
|
|
func (in *GainPayTypeReq) GetPayTypeFromCache() (bc []AllPayType, err error) {
|
|
// 金额排序
|
|
sort.Strings(in.Money)
|
|
|
|
redisKey := fmt.Sprintf("paycenter-channels:%s", strings.Join(in.Money, "-"))
|
|
value, err, _ := payTypeCacheGroup.Do(redisKey, func() (interface{}, error) {
|
|
str, cacheErr := appg.Redis.Get(redisKey)
|
|
if cacheErr != nil {
|
|
log.Warn(fmt.Sprintf("[paycenter]缓存获取支付渠道列表信息异常:%v, moneys: %v", cacheErr, in.Money))
|
|
} else if str != nil {
|
|
cached, decodeErr := decodeCachedPayTypes(*str)
|
|
if decodeErr == nil {
|
|
return cached, nil
|
|
}
|
|
log.Warn(fmt.Sprintf("[paycenter]解析支付渠道缓存数据异常:%v", decodeErr))
|
|
}
|
|
|
|
fetched, fetchErr := in.GetPayType()
|
|
if fetchErr != nil {
|
|
log.Warn(fmt.Sprintf("[paycenter]GetPayType获取支付渠道错误:%v", fetchErr))
|
|
return nil, fetchErr
|
|
}
|
|
|
|
jsonBytes, encodeErr := json.Marshal(fetched)
|
|
if encodeErr != nil {
|
|
log.Warn(fmt.Sprintf("[paycenter]序列化支付渠道缓存异常:%v", encodeErr))
|
|
return fetched, nil
|
|
}
|
|
if cacheErr = appg.Redis.Set(redisKey, string(jsonBytes), redisconst.DataCachExpire); cacheErr != nil {
|
|
log.Warn(fmt.Sprintf("[paycenter]保存支付渠道缓存异常:%v", cacheErr))
|
|
}
|
|
return fetched, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bc, ok := value.([]AllPayType)
|
|
if !ok {
|
|
return nil, fmt.Errorf("[paycenter] unexpected cached pay type: %T", value)
|
|
}
|
|
return bc, nil
|
|
}
|
|
|
|
func decodeCachedPayTypes(raw string) ([]AllPayType, error) {
|
|
var payTypes []AllPayType
|
|
jsonErr := json.Unmarshal([]byte(raw), &payTypes)
|
|
if jsonErr == nil {
|
|
return payTypes, nil
|
|
}
|
|
|
|
// 兼容历史上可能已经写入 Redis 的 MsgPack 数据;新数据统一使用 JSON。
|
|
msgpackErr := msgpack.Unmarshal([]byte(raw), &payTypes)
|
|
if msgpackErr == nil {
|
|
return payTypes, nil
|
|
}
|
|
return nil, fmt.Errorf("json decode: %v; msgpack decode: %v", jsonErr, msgpackErr)
|
|
}
|
|
|
|
// GetPayType 从支付中心获取充值金额对应的充值方式
|
|
func (in *GainPayTypeReq) GetPayType() ([]AllPayType, error) {
|
|
res := GainPayTypeResp{}
|
|
code, err := httputil.DefaultClientPostJsonWithResp(&res, in.GetURL(), nil, in.GetBody())
|
|
if err != nil {
|
|
log.Error(fmt.Sprintf("param:%+v,get pay type in bill center failed err:%v", in, err))
|
|
return nil, errors.New(fmt.Sprintf("bill GetPayType requset fail \nerror:%+v\n\ndata:%+v;", err, in))
|
|
}
|
|
if code != http.StatusOK {
|
|
log.Error(fmt.Sprintf("code:%v,get pay type response status exception", code))
|
|
return nil, errors.New(fmt.Sprintf("bill GetPayType response status exception;\nstatus %+v;", code))
|
|
}
|
|
if res.Code != 200 {
|
|
log.Error(fmt.Sprintf("bill GetPayType be rejected;\n param:%+v, err:%+v:", in, res))
|
|
return nil, errors.New("bill GetPayType be rejected")
|
|
}
|
|
bytes, err := json.Marshal(res.Msg)
|
|
if err != nil {
|
|
log.Error(fmt.Sprintf("bill payUrl urlDecode fail error:%+v;", err))
|
|
return nil, errors.New("getPayType json Marshal error")
|
|
}
|
|
var payTypes []AllPayType
|
|
if err = json.Unmarshal(bytes, &payTypes); err != nil {
|
|
log.Error(fmt.Sprintf("bill payUrl urlDecode fail error:%+v;", err))
|
|
return nil, errors.New("bill payUrl urlDecode fail error")
|
|
}
|
|
log.Debug("bill GetPayType success", log.Any("payTypeCount", len(payTypes)))
|
|
return payTypes, nil
|
|
}
|
|
|
|
// GetBody 获取请求body
|
|
func (in *GainPayTypeReq) GetBody() []byte {
|
|
in.MercID = appg.Conf.PayCenter.MercID
|
|
in.Time = Generate13DigitString()
|
|
in.Sign = in.GenerateSign()
|
|
data, err := json.Marshal(in)
|
|
if err != nil {
|
|
log.Info(fmt.Sprintf("bill GetBody json.Marshal is fail error:%+v/data:%+v", err, in))
|
|
return nil
|
|
}
|
|
return data
|
|
}
|
|
|
|
func Generate13DigitString() string {
|
|
// 生成13位时间戳(毫秒级)
|
|
timestamp := time.Now().UnixMilli()
|
|
return strconv.FormatInt(timestamp, 10)
|
|
}
|
|
|
|
// generateMD5 生成MD5哈希
|
|
func generateMD5(text string) string {
|
|
hash := md5.Sum([]byte(text))
|
|
return hex.EncodeToString(hash[:])
|
|
}
|
|
|
|
// GenerateSign 生成加密
|
|
func (in *GainPayTypeReq) GenerateSign() string {
|
|
// 默认签名方式1: md5(value+value)
|
|
signStr := in.MercID + strings.Join(in.Money, "") + appg.Conf.PayCenter.MercSecret
|
|
sign := generateMD5(signStr)
|
|
return sign
|
|
}
|
|
|
|
// GetURL 获取请求地址
|
|
func (in *GainPayTypeReq) GetURL() string {
|
|
return appg.Conf.PayCenter.ApiUrl + "/api/shark/getPayType"
|
|
}
|
|
|
|
type Recharge struct {
|
|
Time string `json:"time" bson:"time"` // 时间戳,最大长度50,必填
|
|
Sign string `json:"sign" bson:"sign"` // 签名,最大长度100,必填
|
|
MercID string `json:"mercId" bson:"mercId"` // 商户ID,最大长度50,必填
|
|
Type string `json:"type" bson:"type"` // 支付类型,必填
|
|
Money string `json:"money" bson:"money"` // 金额,必填
|
|
TradeNo string `json:"tradeNo" bson:"tradeNo"` // 交易号,最大长度50,必填
|
|
NotifyUrl string `json:"notifyUrl" bson:"notifyUrl"` // 通知URL,最大长度150,必填
|
|
CancelNotifyUrl string `json:"cancelNotifyUrl,omitempty"` // 退款URL,最大长度150,非必填
|
|
Info PayInfo `json:"info" bson:"info"` // 支付信息对象,必填
|
|
SignType string `json:"signType" bson:"signType"` // 加密方式:1-md5(value+value) 2-md5(key1=value1|key2=value2@@secret),可选
|
|
Mode string `json:"mode" bson:"mode"` // 支付模式,兼容商户版本迭代,传sdk表示支持sdk模式,可选
|
|
Payload string `json:"payload" bson:"payload"` // 备用参数:1-不匹配赔付渠道 2-空单 3-兑换划转,可选
|
|
}
|
|
|
|
// PayInfo 支付信息子结构体
|
|
type PayInfo struct {
|
|
App string `json:"app" json:"app"` // 产品,最大长度15,必填
|
|
PlayerId string `json:"playerId" bson:"playerId"` // 玩家ID,最大长度50,必填
|
|
PlayerIp string `json:"playerIp" bson:"playerIp"` // 玩家IP,最大长度100,必填
|
|
DeviceType string `json:"deviceType" bson:"deviceType"` // 设备类型,必填
|
|
DeviceId string `json:"deviceId" bson:"deviceId"` // 设备ID,最大长度100,必填
|
|
Name string `json:"name" bson:"name"` // 玩家姓名
|
|
Tel string `json:"tel" bson:"tel"` // 玩家⼿机号
|
|
AlipayAct string `json:"alipayAct" bson:"alipayAct"` // 玩家支付宝账号
|
|
}
|
|
|
|
type PayResp struct {
|
|
Code stderr.Code `json:"code" bson:"code"` // 状态
|
|
OID string `json:"oid" bson:"oid"` // 订单号
|
|
PayUrl string `json:"payUrl" bson:"payUrl"` // 支付链接
|
|
Mode string `json:"mode" bson:"mode"` // 模式
|
|
Rebate string `json:"rebate" bson:"rebate"` // 通道费率
|
|
Sign string `json:"sign" bson:"sign"` // 加密
|
|
}
|
|
|
|
type RechargeResp struct {
|
|
Code int `json:"code" bson:"code"` // code码
|
|
Info string `json:"info" bson:"info"` // code码信息
|
|
Err string `json:"err" bson:"err"` // 错误信息
|
|
Msg PayResp `json:"msg" bson:"msg"` // 响应数据
|
|
Tip string `json:"tip" bson:"tip"` // 备注
|
|
}
|
|
|
|
type RechargeCallbackResp struct {
|
|
Code int `json:"code" binding:"required"` // 编码
|
|
MercID string `json:"mercId" binding:"required"` // 商户编号
|
|
OID string `json:"oid" binding:"required"` // 支付平台订单号
|
|
PayMoney string `json:"payMoney" binding:"required"` // 订单到账实际金额
|
|
TradeNo string `json:"tradeNo" binding:"required"` // 商户订单号
|
|
Sign string `json:"sign" binding:"required"` // 签名
|
|
}
|
|
|
|
type RefundCallbackResp struct {
|
|
Code int `json:"code" bson:"code"` // 编码
|
|
MercID string `json:"mercId" bson:"mercId"` // 商户编号
|
|
OID string `json:"oid" bson:"oid"` // 支付平台订单号
|
|
TradeNo string `json:"tradeNo" bson:"tradeNo"` // 商户订单号
|
|
Sign string `json:"sign" bson:"sign"` // 签名
|
|
}
|
|
|
|
// ToPayNew 获取请求body
|
|
func (in *Recharge) ToPayNew(ctx context.Context) (res PayResp, err error) {
|
|
defer func() {
|
|
log.InfoX(ctx, "bill ToPayNew request info end 2", log.Any("transNo", in.TradeNo), log.Any("res", res))
|
|
if err := recover(); err != nil {
|
|
log.ErrorX(ctx, "bill ToPayNew recover", log.Any("Rchg", in), log.Any("panic", err))
|
|
}
|
|
}()
|
|
in.fill()
|
|
startS := time.Now().UnixNano()
|
|
|
|
var rechargeResp RechargeResp
|
|
code, err := httputil.DefaultClientPostJsonWithResp(&rechargeResp, in.GetURL(), nil, in.GetBody())
|
|
log.InfoX(ctx, "ToPayNew POST resp ", log.Any("code", code), log.Any("transNo", in.TradeNo),
|
|
log.Any("topay-bill-time cost", time.Now().UnixNano()-startS))
|
|
if err != nil {
|
|
log.ErrorX(ctx, "bill ToPayNew post failed", log.Any("transNo", in.TradeNo), log.E(err))
|
|
err = PayErrPostFailure
|
|
return
|
|
}
|
|
if code != http.StatusOK {
|
|
log.ErrorX(ctx, "bill ToPayNew bad http statusCode", log.Any("transNo", in.TradeNo), log.Any("statusCode", code))
|
|
err = PayErrResponseCode
|
|
return
|
|
}
|
|
if rechargeResp.Err != "" {
|
|
log.ErrorX(ctx, "bill ToPayNew has been rejected", log.Any("transNo", in.TradeNo), log.Any("res err", rechargeResp.Err))
|
|
err = PayErrBeRejected
|
|
return
|
|
}
|
|
log.InfoX(ctx, "bill ToPayNew request info end", log.Any("transNo", in.TradeNo), log.Any("res", res))
|
|
return rechargeResp.Msg, nil
|
|
}
|
|
|
|
// fill 组装数据
|
|
func (in *Recharge) fill() {
|
|
in.MercID = in.GetMercID()
|
|
in.Time = Generate13DigitString()
|
|
in.TradeNo = RChgIDAssemble(in.TradeNo)
|
|
in.Info.App = appg.Conf.PayCenter.AppName
|
|
in.NotifyUrl = in.GetNotifyURL()
|
|
in.CancelNotifyUrl = in.GetCancelNotifyURL()
|
|
in.SignType = "1"
|
|
in.Sign = in.sign()
|
|
}
|
|
|
|
// GetNotifyURL 获取回调地址
|
|
func (in *Recharge) GetNotifyURL() string {
|
|
return appg.Conf.PayCenter.CallbackUrl + "/3rd/defray/callback/pay_center"
|
|
}
|
|
|
|
// GetCancelNotifyURL 获取退款回调地址
|
|
func (in *Recharge) GetCancelNotifyURL() string {
|
|
return appg.Conf.PayCenter.CallbackUrl + "/3rd/defray/callback/refund"
|
|
}
|
|
|
|
// sign 签名
|
|
func (in *Recharge) sign() (sign string) {
|
|
var buf bytes.Buffer
|
|
buf.WriteString(in.MercID)
|
|
buf.WriteString(in.Money)
|
|
buf.WriteString(in.NotifyUrl)
|
|
buf.WriteString(in.TradeNo)
|
|
buf.WriteString(in.Type)
|
|
buf.WriteString(in.GetAppSecret())
|
|
md5Ctx := md5.New()
|
|
md5Ctx.Write(buf.Bytes())
|
|
cipherStr := md5Ctx.Sum(nil)
|
|
return hex.EncodeToString(cipherStr)
|
|
}
|
|
|
|
// GetBody 获取请求body
|
|
func (in *Recharge) GetBody() []byte {
|
|
jsonStr, err := json.Marshal(in)
|
|
if err != nil {
|
|
log.Info(fmt.Sprintf("DaBaiSha GetBody json.Marshal is fail error:%+v/data:%+v", err, in))
|
|
}
|
|
sstr := string(jsonStr)
|
|
log.Info(fmt.Sprintf("==================%+v", sstr))
|
|
return jsonStr
|
|
}
|
|
|
|
// GetURL 获取请求地址
|
|
func (in *Recharge) GetURL() string {
|
|
return appg.Conf.PayCenter.ApiUrl + "/api/shark/topay"
|
|
}
|
|
|
|
// GetBody 获取请求body
|
|
//func (in *Recharge) Notify() (RchgBack, error) {
|
|
// return in, nil
|
|
//}
|
|
|
|
// Success 获取请求body
|
|
func (in *Recharge) Success() string {
|
|
return "success"
|
|
}
|
|
|
|
func (in *Recharge) GetAppSecret() string {
|
|
return appg.Conf.PayCenter.MercSecret
|
|
}
|
|
|
|
// GetMercID 获取商户编号
|
|
func (in *Recharge) GetMercID() string {
|
|
return appg.Conf.PayCenter.MercID
|
|
}
|
|
|
|
func FenToYuan(price int64) string {
|
|
return strconv.FormatInt(price/100, 10)
|
|
}
|
|
|
|
func YuanToFen(money string) int64 {
|
|
// 去除前后空格
|
|
money = strings.TrimSpace(money)
|
|
if money == "" {
|
|
return 0
|
|
}
|
|
|
|
// 检查是否为负数
|
|
isNegative := false
|
|
if strings.HasPrefix(money, "-") {
|
|
isNegative = true
|
|
money = money[1:]
|
|
}
|
|
|
|
// 按小数点分割
|
|
parts := strings.Split(money, ".")
|
|
|
|
switch len(parts) {
|
|
case 1:
|
|
// 只有整数部分,如 "123", "456"
|
|
yuan, err := strconv.ParseInt(parts[0], 10, 64)
|
|
if err != nil {
|
|
log.Warn(fmt.Sprintf("invalid integer part: %v", err))
|
|
return 0
|
|
}
|
|
result := yuan * 100
|
|
if isNegative {
|
|
result = -result
|
|
}
|
|
return result
|
|
|
|
case 2:
|
|
// 有整数和小数部分,如 "123.45", "78.9"
|
|
yuan, err := strconv.ParseInt(parts[0], 10, 64)
|
|
if err != nil {
|
|
log.Warn(fmt.Sprintf("invalid integer part: %v", err))
|
|
return 0
|
|
}
|
|
|
|
// 处理小数部分
|
|
decimalPart := parts[1]
|
|
if len(decimalPart) > 2 {
|
|
// 如果小数部分超过2位,进行四舍五入或截断
|
|
// 这里选择截断,也可以根据需要改为四舍五入
|
|
decimalPart = decimalPart[:2]
|
|
} else if len(decimalPart) == 1 {
|
|
// 如果只有1位小数,补零
|
|
decimalPart += "0"
|
|
}
|
|
|
|
fen, err := strconv.ParseInt(decimalPart, 10, 64)
|
|
if err != nil {
|
|
log.Warn(fmt.Sprintf("invalid decimal part: %v", err))
|
|
return 0
|
|
}
|
|
|
|
result := yuan*100 + fen
|
|
if isNegative {
|
|
result = -result
|
|
}
|
|
return result
|
|
default:
|
|
log.Warn(fmt.Sprintf("invalid money format: %s", money))
|
|
return 0
|
|
}
|
|
}
|
|
|
|
// GetQueryURL 获取请求地址
|
|
func (in *Recharge) GetQueryURL() string {
|
|
return appg.Conf.PayCenter.ApiUrl + "/api/shark/order/queryOrder"
|
|
}
|
|
|
|
type RechargeQueryResp struct {
|
|
Code int `json:"code" bson:"code"` // 响应码
|
|
Msg QueryOrderInfo `json:"msg" bson:"msg"` // 订单信息
|
|
}
|
|
|
|
type QueryOrderInfo struct {
|
|
MercID string `json:"mercID"`
|
|
TradeNo string `json:"tradeNo"`
|
|
Money float32 `json:"money"`
|
|
PayMoney float32 `json:"payMoney"`
|
|
PayTime string `json:"payTime"`
|
|
PayStatus string `json:"payStatus"`
|
|
NotifyStatus string `json:"notifyStatus"`
|
|
}
|
|
|
|
// QueryOrder 订单状态查询发起
|
|
func (g *Recharge) QueryOrder() (msg QueryOrderInfo, err error) {
|
|
var res RechargeQueryResp
|
|
body := make(map[string]string)
|
|
body["mercId"] = g.GetMercID()
|
|
body["tradeNo"] = g.TradeNo
|
|
code, err := httputil.DefaultClientGetWithResp(&res, g.GetQueryURL(), nil, body)
|
|
if err != nil || code != 200 {
|
|
log.Error(fmt.Sprintf("pay center QueryOrder http.Get fail error:%+v:", err))
|
|
return
|
|
}
|
|
if res.Code != 200 {
|
|
err = errors.New("pay center QueryOrder http query fail")
|
|
return
|
|
}
|
|
msg = res.Msg
|
|
return res.Msg, nil
|
|
}
|