Files
huangguo_server/common/rchgutil/recharge.go
T
2026-09-15 22:11:05 +08:00

444 lines
14 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 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
}