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 }