Initial commit

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 13:57:10 +08:00
co-authored by Claude Opus 5
commit 8679200f41
1897 changed files with 257900 additions and 0 deletions
+460
View File
@@ -0,0 +1,460 @@
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
}