444 lines
14 KiB
Go
444 lines
14 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
|
||
|
||
// GainPayTypeReq 按金额查询支付通道(账单中心 /api/recharge/payType)
|
||
type GainPayTypeReq struct {
|
||
Money []string // 金额数组(元),必填
|
||
DevType string // 设备类型 android/ios,非android按ios处理
|
||
DistrictCode string // 用户渠道码,支付中心据此绑定可用支付渠道
|
||
}
|
||
|
||
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:%s:%s", billDevType(in.DevType), in.DistrictCode, 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) {
|
||
moneys := make([]int64, 0, len(in.Money))
|
||
for _, m := range in.Money {
|
||
moneys = append(moneys, YuanToFen(m))
|
||
}
|
||
rchgTypes, err := GetPayTypeByDistrict(context.Background(), moneys, in.DevType, 0, 0, in.DistrictCode)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
payTypes := make([]AllPayType, 0, len(rchgTypes))
|
||
for _, r := range rchgTypes {
|
||
payTypes = append(payTypes, r.ToAllPayType())
|
||
}
|
||
return payTypes, nil
|
||
}
|
||
|
||
// ToAllPayType 转换为按金额分组的支付类型列表
|
||
func (r RchgType) ToAllPayType() AllPayType {
|
||
res := AllPayType{
|
||
Money: fmt.Sprintf("%d.%02d", r.Money/100, r.Money%100),
|
||
Types: make([]mercPayTypeInfo, 0),
|
||
}
|
||
enabled := []struct {
|
||
ok bool
|
||
t string
|
||
}{
|
||
{r.Alipay, Alipay},
|
||
{r.Wechat, Wechat},
|
||
{r.Union, "union"},
|
||
{r.QuickUnion, "quickUnion"},
|
||
{r.USDT, "usdt"},
|
||
{r.DaiChong, "daichong"},
|
||
}
|
||
for _, e := range enabled {
|
||
if e.ok {
|
||
res.Types = append(res.Types, mercPayTypeInfo{Name: e.t, Type: e.t})
|
||
}
|
||
}
|
||
return res
|
||
}
|
||
|
||
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[:])
|
||
}
|
||
|
||
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
|
||
}
|