init: web_hgcg_server 初始化提交
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
package payCenter
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Conf 与 girl 的 newPayCenter 配置保持一致,App 和 Web 各自加载本进程配置。
|
||||
type Conf struct {
|
||||
AppName string `yaml:"appName"`
|
||||
CallbackUrl string `yaml:"callbackUrl"`
|
||||
ApiUrl string `yaml:"apiUrl"`
|
||||
MercID string `yaml:"mercID"`
|
||||
MercSecret string `yaml:"mercSecret"`
|
||||
}
|
||||
|
||||
var payCenterConf Conf
|
||||
|
||||
func Init(c Conf) {
|
||||
c.ApiUrl = strings.TrimRight(strings.TrimSpace(c.ApiUrl), "/")
|
||||
c.CallbackUrl = strings.TrimRight(strings.TrimSpace(c.CallbackUrl), "/")
|
||||
payCenterConf = c
|
||||
}
|
||||
|
||||
// validateConfig 仅在支付请求时校验,配置暂未填写不影响其他业务启动。
|
||||
func validateConfig(requireCallback bool) error {
|
||||
if payCenterConf.MercID == "" || payCenterConf.MercSecret == "" || !validHTTPURL(payCenterConf.ApiUrl) {
|
||||
return errors.New("新支付中心配置不完整:请配置 newPayCenter.apiUrl、mercID、mercSecret")
|
||||
}
|
||||
if requireCallback && (payCenterConf.AppName == "" || !validHTTPURL(payCenterConf.CallbackUrl)) {
|
||||
return errors.New("新支付中心配置不完整:请配置 newPayCenter.appName、callbackUrl")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validHTTPURL(value string) bool {
|
||||
u, err := url.Parse(value)
|
||||
return err == nil && (u.Scheme == "http" || u.Scheme == "https") && u.Host != "" && u.User == nil && u.RawQuery == "" && u.Fragment == ""
|
||||
}
|
||||
|
||||
func Generate13DigitString() string {
|
||||
return strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
}
|
||||
|
||||
func md5Sign(value string) string {
|
||||
hash := md5.Sum([]byte(value))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
func VerifySign(sign, value string) bool {
|
||||
return subtle.ConstantTimeCompare([]byte(md5Sign(value)), []byte(sign)) == 1
|
||||
}
|
||||
|
||||
func RChgIDDisassemble(transNo string) string {
|
||||
parts := strings.Split(transNo, "-")
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package payCenter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"videoweb/pub/common/log"
|
||||
)
|
||||
|
||||
// FenToYuan 用整数拆分分/元,避免浮点运算改变下单金额及签名原文。
|
||||
func FenToYuan(price int64) string {
|
||||
amount := big.NewInt(price)
|
||||
negative := amount.Sign() < 0
|
||||
amount.Abs(amount)
|
||||
yuan := new(big.Int)
|
||||
fen := new(big.Int)
|
||||
yuan.QuoRem(amount, big.NewInt(100), fen)
|
||||
result := yuan.String()
|
||||
if fen.Sign() != 0 {
|
||||
result += fmt.Sprintf(".%02d", fen.Int64())
|
||||
}
|
||||
if negative {
|
||||
return "-" + result
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func YuanToFen(ctx context.Context, money string) int64 {
|
||||
result, err := ParseYuanToFen(money)
|
||||
if err != nil {
|
||||
log.Error("【新支付中心金额转换】元转分失败", log.E(err))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ParseYuanToFen 只接受十进制金额和最多两位小数,拒绝指数、内嵌符号和溢出。
|
||||
func ParseYuanToFen(money string) (int64, error) {
|
||||
money = strings.TrimSpace(money)
|
||||
if money == "" {
|
||||
return 0, errors.New("money is empty")
|
||||
}
|
||||
|
||||
negative := strings.HasPrefix(money, "-")
|
||||
if negative {
|
||||
money = money[1:]
|
||||
if money == "" {
|
||||
return 0, errors.New("money is empty")
|
||||
}
|
||||
}
|
||||
parts := strings.Split(money, ".")
|
||||
if len(parts) > 2 {
|
||||
return 0, fmt.Errorf("invalid money format %q", money)
|
||||
}
|
||||
if strings.ReplaceAll(money, ".", "") == "" {
|
||||
return 0, errors.New("money has no digits")
|
||||
}
|
||||
for _, part := range parts {
|
||||
for _, digit := range part {
|
||||
if digit < '0' || digit > '9' {
|
||||
return 0, errors.New("money must contain only decimal digits")
|
||||
}
|
||||
}
|
||||
}
|
||||
if parts[0] == "" {
|
||||
parts[0] = "0"
|
||||
}
|
||||
|
||||
yuan, ok := new(big.Int).SetString(parts[0], 10)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("invalid yuan amount %q", money)
|
||||
}
|
||||
|
||||
decimalPart := "00"
|
||||
if len(parts) == 2 {
|
||||
decimalPart = parts[1]
|
||||
if len(decimalPart) > 2 {
|
||||
return 0, fmt.Errorf("money has more than two decimal places: %q", money)
|
||||
}
|
||||
if len(decimalPart) == 0 {
|
||||
decimalPart = "00"
|
||||
} else if len(decimalPart) == 1 {
|
||||
decimalPart += "0"
|
||||
}
|
||||
}
|
||||
cents, err := strconv.ParseInt(decimalPart, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid fen amount %q: %w", money, err)
|
||||
}
|
||||
|
||||
total := new(big.Int).Mul(yuan, big.NewInt(100))
|
||||
total.Add(total, big.NewInt(cents))
|
||||
if negative {
|
||||
total.Neg(total)
|
||||
}
|
||||
if !total.IsInt64() {
|
||||
return 0, fmt.Errorf("money overflows int64 fen: %q", money)
|
||||
}
|
||||
return total.Int64(), nil
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package payCenter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"videoweb/pub/common/httputil"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
)
|
||||
|
||||
// PayTypeInfo 支付中心返回的支付方式,名称优先使用本地配置。
|
||||
type PayTypeInfo struct {
|
||||
Type string
|
||||
TypeName string
|
||||
}
|
||||
|
||||
const payCenterErrorBodyLogLimit = 4096
|
||||
|
||||
type GainPayTypeReq struct {
|
||||
Ctx context.Context `json:"-"`
|
||||
Time string `json:"time"`
|
||||
Sign string `json:"sign"`
|
||||
MercID string `json:"mercId"`
|
||||
Money []string `json:"money"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
Payload string `json:"payload,omitempty"`
|
||||
}
|
||||
|
||||
type AllPayType struct {
|
||||
Money string `json:"money"`
|
||||
Types []mercPayTypeInfo `json:"types"`
|
||||
}
|
||||
|
||||
type mercPayTypeInfo struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type GainPayTypeResp struct {
|
||||
Code int `json:"code"`
|
||||
Info string `json:"info"`
|
||||
Err string `json:"err"`
|
||||
Msg []AllPayType `json:"msg"`
|
||||
Tip string `json:"tip"`
|
||||
}
|
||||
|
||||
type AllPayTypeMgr []AllPayType
|
||||
|
||||
// GetPayType 保留支付中心返回的全部支付类型和名称,不再限定为支付宝/微信。
|
||||
func (a AllPayTypeMgr) GetPayType(money int64) []PayTypeInfo {
|
||||
result := make([]PayTypeInfo, 0)
|
||||
for _, item := range a {
|
||||
itemMoney, err := ParseYuanToFen(item.Money)
|
||||
if err != nil || itemMoney != money {
|
||||
continue
|
||||
}
|
||||
for _, payType := range item.Types {
|
||||
result = append(result, PayTypeInfo{
|
||||
Type: payType.Type,
|
||||
TypeName: payTypeDisplayName(payType.Type, payType.Name),
|
||||
})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func NewPayTypeReq(ctx context.Context, moneys []int64) *GainPayTypeReq {
|
||||
moneyStrings := make([]string, 0, len(moneys))
|
||||
for _, money := range moneys {
|
||||
moneyStrings = append(moneyStrings, FenToYuan(money))
|
||||
}
|
||||
return &GainPayTypeReq{Ctx: ctx, Money: moneyStrings}
|
||||
}
|
||||
|
||||
func (in *GainPayTypeReq) GetPayType() (AllPayTypeMgr, error) {
|
||||
if in == nil {
|
||||
return nil, errors.New("pay center get pay type: nil request")
|
||||
}
|
||||
if err := validateConfig(false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(in.Money) == 0 {
|
||||
return AllPayTypeMgr{}, nil
|
||||
}
|
||||
return in.getPayTypeWithCache()
|
||||
}
|
||||
|
||||
// GetPayChannelByMoney 供会员卡、金币、钻石和促销入口共用,统一转换业务错误。
|
||||
func GetPayChannelByMoney(ctx context.Context, moneys []int64) (AllPayTypeMgr, *srvdef.SrvErr) {
|
||||
channels, err := NewPayTypeReq(ctx, moneys).GetPayType()
|
||||
if err != nil {
|
||||
log.Error("【新支付中心 /api/shark/getPayType】查询支付通道失败", log.Any("amounts", moneys), log.E(err))
|
||||
return nil, srvdef.New(srvdef.Err, "支付通道暂不可用", err.Error())
|
||||
}
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func (in *GainPayTypeReq) getPayTypeFromProvider() (AllPayTypeMgr, error) {
|
||||
in.fill()
|
||||
resp, err := httputil.NewCtx(in.Ctx).POSTJson(in.GetURL(), nil, in)
|
||||
if err != nil {
|
||||
log.Error("【新支付中心 /api/shark/getPayType】请求失败", log.E(err))
|
||||
return nil, fmt.Errorf("get pay type: %w", err)
|
||||
}
|
||||
defer resp.Body.Body.Close()
|
||||
|
||||
body, err := resp.Body.ReadAll()
|
||||
if err != nil {
|
||||
log.Error("【新支付中心 /api/shark/getPayType】读取响应失败",
|
||||
log.Any("httpStatus", resp.StatusCode),
|
||||
log.Any("contentType", resp.Body.Header.Get("Content-Type")),
|
||||
log.E(err),
|
||||
)
|
||||
return nil, fmt.Errorf("get pay type: read response: %w", err)
|
||||
}
|
||||
bodyForLog := body
|
||||
truncated := false
|
||||
if len(bodyForLog) > payCenterErrorBodyLogLimit {
|
||||
bodyForLog = bodyForLog[:payCenterErrorBodyLogLimit]
|
||||
truncated = true
|
||||
}
|
||||
|
||||
responseMeta := func() string {
|
||||
return fmt.Sprintf("HTTP status %d, content-type %q, body %q, body-truncated %t",
|
||||
resp.StatusCode,
|
||||
resp.Body.Header.Get("Content-Type"),
|
||||
string(bodyForLog),
|
||||
truncated,
|
||||
)
|
||||
}
|
||||
logInvalidResponse := func(message string, responseErr error) {
|
||||
fields := []log.Field{
|
||||
log.Any("httpStatus", resp.StatusCode),
|
||||
log.Any("contentType", resp.Body.Header.Get("Content-Type")),
|
||||
log.Any("responseBody", string(bodyForLog)),
|
||||
log.Any("bodyTruncated", truncated),
|
||||
}
|
||||
if responseErr != nil {
|
||||
fields = append(fields, log.E(responseErr))
|
||||
}
|
||||
log.Error(message, fields...)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
logInvalidResponse("【新支付中心 /api/shark/getPayType】HTTP状态异常", nil)
|
||||
return nil, fmt.Errorf("get pay type: %s", responseMeta())
|
||||
}
|
||||
|
||||
var response GainPayTypeResp
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
logInvalidResponse("【新支付中心 /api/shark/getPayType】响应JSON解析失败", err)
|
||||
return nil, fmt.Errorf("get pay type: decode response: %w; %s", err, responseMeta())
|
||||
}
|
||||
if response.Code != http.StatusOK || response.Err != "" {
|
||||
logInvalidResponse("【新支付中心 /api/shark/getPayType】支付中心返回失败", nil)
|
||||
return nil, fmt.Errorf("get pay type: provider code %d; %s", response.Code, responseMeta())
|
||||
}
|
||||
return AllPayTypeMgr(response.Msg), nil
|
||||
}
|
||||
|
||||
func (in *GainPayTypeReq) fill() {
|
||||
in.MercID = payCenterConf.MercID
|
||||
in.Time = Generate13DigitString()
|
||||
in.Sign = md5Sign(in.MercID + strings.Join(in.Money, "") + payCenterConf.MercSecret)
|
||||
}
|
||||
|
||||
func (in *GainPayTypeReq) GetURL() string {
|
||||
return payCenterConf.ApiUrl + "/api/shark/getPayType"
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package payCenter
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/redis"
|
||||
|
||||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
|
||||
var payTypeRequests singleflight.Group
|
||||
|
||||
// 通道按商户、支付中心和金额组合缓存一分钟;缓存异常直接查询支付中心。
|
||||
const payTypesCacheExpire = time.Minute
|
||||
|
||||
func (in *GainPayTypeReq) getPayTypeWithCache() (AllPayTypeMgr, error) {
|
||||
if in.Ctx == nil || redis.Client() == nil {
|
||||
return in.getPayTypeFromProvider()
|
||||
}
|
||||
|
||||
cacheKey := in.payTypeCacheKey()
|
||||
if payTypes, found := loadCachedPayTypes(in, cacheKey); found {
|
||||
return payTypes, nil
|
||||
}
|
||||
|
||||
resultCh := payTypeRequests.DoChan(cacheKey, func() (any, error) {
|
||||
if payTypes, found := loadCachedPayTypes(in, cacheKey); found {
|
||||
return payTypes, nil
|
||||
}
|
||||
|
||||
payTypes, err := in.getPayTypeFromProvider()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
storeCachedPayTypes(in, cacheKey, payTypes)
|
||||
return payTypes, nil
|
||||
})
|
||||
|
||||
select {
|
||||
case <-in.Ctx.Done():
|
||||
return nil, in.Ctx.Err()
|
||||
case result := <-resultCh:
|
||||
if result.Err != nil {
|
||||
if result.Shared {
|
||||
return in.getPayTypeFromProvider()
|
||||
}
|
||||
return nil, result.Err
|
||||
}
|
||||
payTypes, ok := result.Val.(AllPayTypeMgr)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("pay center cached pay type has unexpected result type %T", result.Val)
|
||||
}
|
||||
return payTypes, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (in *GainPayTypeReq) payTypeCacheKey() string {
|
||||
moneys := slices.Clone(in.Money)
|
||||
sort.Strings(moneys)
|
||||
moneys = slices.Compact(moneys)
|
||||
|
||||
fingerprint := strings.Join([]string{
|
||||
payCenterConf.ApiUrl,
|
||||
payCenterConf.MercID,
|
||||
in.Mode,
|
||||
in.Payload,
|
||||
strings.Join(moneys, ","),
|
||||
}, "\x00")
|
||||
digest := sha256.Sum256([]byte(fingerprint))
|
||||
return "videoweb:newpaycenter:pay-types:v1:" + hex.EncodeToString(digest[:])
|
||||
}
|
||||
|
||||
func loadCachedPayTypes(in *GainPayTypeReq, cacheKey string) (AllPayTypeMgr, bool) {
|
||||
payload, err := redis.Get(cacheKey)
|
||||
if err != nil {
|
||||
log.Error("【新支付中心 /api/shark/getPayType】读取通道缓存失败", log.E(err))
|
||||
return nil, false
|
||||
}
|
||||
if payload == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var payTypes AllPayTypeMgr
|
||||
if err := json.Unmarshal([]byte(*payload), &payTypes); err != nil {
|
||||
log.Error("【新支付中心 /api/shark/getPayType】解析通道缓存失败", log.E(err))
|
||||
return nil, false
|
||||
}
|
||||
return payTypes, true
|
||||
}
|
||||
|
||||
func storeCachedPayTypes(in *GainPayTypeReq, cacheKey string, payTypes AllPayTypeMgr) {
|
||||
payload, err := json.Marshal(payTypes)
|
||||
if err != nil {
|
||||
log.Error("【新支付中心 /api/shark/getPayType】编码通道缓存失败", log.E(err))
|
||||
return
|
||||
}
|
||||
if err := redis.Set(cacheKey, payload, payTypesCacheExpire); err != nil {
|
||||
log.Error("【新支付中心 /api/shark/getPayType】写入通道缓存失败", log.E(err))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package payCenter
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var payTypeNames struct {
|
||||
sync.RWMutex
|
||||
alipay string
|
||||
wechat string
|
||||
}
|
||||
|
||||
// SetPayTypeNames 保留 KS 后台已配置的支付名称;未配置时采用新支付中心返回的名称。
|
||||
func SetPayTypeNames(alipay, wechat string) {
|
||||
payTypeNames.Lock()
|
||||
defer payTypeNames.Unlock()
|
||||
payTypeNames.alipay = strings.TrimSpace(alipay)
|
||||
payTypeNames.wechat = strings.TrimSpace(wechat)
|
||||
}
|
||||
|
||||
func payTypeDisplayName(kind, providerName string) string {
|
||||
payTypeNames.RLock()
|
||||
defer payTypeNames.RUnlock()
|
||||
if kind == "alipay" && payTypeNames.alipay != "" {
|
||||
return payTypeNames.alipay
|
||||
}
|
||||
if kind == "wechat" && payTypeNames.wechat != "" {
|
||||
return payTypeNames.wechat
|
||||
}
|
||||
return providerName
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package payCenter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"videoweb/pub/common/httputil"
|
||||
"videoweb/pub/common/log"
|
||||
)
|
||||
|
||||
type RechargeQueryResp struct {
|
||||
Code int `json:"code"`
|
||||
Msg QueryOrderInfo `json:"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"`
|
||||
}
|
||||
|
||||
func (in *Recharge) GetQueryURL() string {
|
||||
return payCenterConf.ApiUrl + "/api/shark/order/queryOrder"
|
||||
}
|
||||
|
||||
func (in *Recharge) QueryOrder(ctx context.Context) (QueryOrderInfo, error) {
|
||||
if in == nil {
|
||||
return QueryOrderInfo{}, errors.New("query pay center order: nil recharge")
|
||||
}
|
||||
if err := validateConfig(false); err != nil {
|
||||
return QueryOrderInfo{}, err
|
||||
}
|
||||
if in.TradeNo == "" {
|
||||
return QueryOrderInfo{}, errors.New("新支付中心查单缺少订单号")
|
||||
}
|
||||
var response RechargeQueryResp
|
||||
body := map[string]string{
|
||||
"mercId": in.GetMercID(),
|
||||
"tradeNo": in.TradeNo,
|
||||
}
|
||||
code, err := httputil.NewCtx(ctx).GetWithJResp(&response, in.GetQueryURL(), nil, body)
|
||||
if err != nil {
|
||||
log.Error("【新支付中心 /api/shark/order/queryOrder】查单失败", log.Any("tradeNo", in.TradeNo), log.E(err))
|
||||
return QueryOrderInfo{}, fmt.Errorf("query pay center order: %w", err)
|
||||
}
|
||||
if code != 200 {
|
||||
return QueryOrderInfo{}, fmt.Errorf("query pay center order: HTTP status %d", code)
|
||||
}
|
||||
if response.Code != 200 {
|
||||
return QueryOrderInfo{}, fmt.Errorf("query pay center order: provider code %d", response.Code)
|
||||
}
|
||||
return response.Msg, nil
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package payCenter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"videoweb/pub/common/httputil"
|
||||
"videoweb/pub/common/log"
|
||||
"videoweb/pub/common/srvdef"
|
||||
)
|
||||
|
||||
// Recharge 使用新支付中心的元字符串金额和商户 MD5 签名协议。
|
||||
type Recharge struct {
|
||||
Ctx context.Context `json:"-"`
|
||||
Time string `json:"time"`
|
||||
Sign string `json:"sign"`
|
||||
MercID string `json:"mercId"`
|
||||
Type string `json:"type"`
|
||||
Money string `json:"money"`
|
||||
TradeNo string `json:"tradeNo"`
|
||||
NotifyUrl string `json:"notifyUrl"`
|
||||
CancelNotifyUrl string `json:"cancelNotifyUrl,omitempty"`
|
||||
Info PayInfo `json:"info"`
|
||||
SignType string `json:"signType"`
|
||||
Mode string `json:"mode"`
|
||||
Payload string `json:"payload"`
|
||||
}
|
||||
|
||||
type PayInfo struct {
|
||||
App string `json:"app"`
|
||||
PlayerId string `json:"playerId"`
|
||||
PlayerIp string `json:"playerIp"`
|
||||
DeviceType string `json:"deviceType"`
|
||||
DeviceId string `json:"deviceId"`
|
||||
Name string `json:"name"`
|
||||
Tel string `json:"tel"`
|
||||
AlipayAct string `json:"alipayAct"`
|
||||
}
|
||||
|
||||
type PayResp struct {
|
||||
Code srvdef.CodeType `json:"code"`
|
||||
OID string `json:"oid"`
|
||||
PayUrl string `json:"payUrl"`
|
||||
Mode string `json:"mode"`
|
||||
Rebate string `json:"rebate"`
|
||||
Sign string `json:"sign"`
|
||||
}
|
||||
|
||||
type RechargeResp struct {
|
||||
Code int `json:"code"`
|
||||
Info string `json:"info"`
|
||||
Err string `json:"err"`
|
||||
Msg PayResp `json:"msg"`
|
||||
Tip string `json:"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"`
|
||||
MercID string `json:"mercId"`
|
||||
OID string `json:"oid"`
|
||||
TradeNo string `json:"tradeNo"`
|
||||
Sign string `json:"sign"`
|
||||
}
|
||||
|
||||
type rechargePostFunc func(context.Context, *Recharge, *RechargeResp) (int, error)
|
||||
|
||||
func (in *Recharge) ToPayNew() (PayResp, error) {
|
||||
return in.toPayNew(func(ctx context.Context, request *Recharge, response *RechargeResp) (int, error) {
|
||||
return httputil.NewCtx(ctx).POSTJsonWithJResp(response, request.GetURL(), nil, request)
|
||||
})
|
||||
}
|
||||
|
||||
func (in *Recharge) toPayNew(post rechargePostFunc) (result PayResp, err error) {
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
result = PayResp{}
|
||||
err = fmt.Errorf("pay center request panic: %v", recovered)
|
||||
if in != nil {
|
||||
log.Error("【新支付中心 /api/shark/topay】请求异常", log.Any("tradeNo", in.TradeNo), log.E(err))
|
||||
}
|
||||
}
|
||||
if in != nil {
|
||||
if err != nil {
|
||||
log.Error("【新支付中心 /api/shark/topay】下单失败", log.Any("tradeNo", in.TradeNo), log.E(err))
|
||||
} else {
|
||||
log.Info("【新支付中心 /api/shark/topay】下单成功", log.Any("tradeNo", in.TradeNo), log.Any("oid", result.OID))
|
||||
}
|
||||
}
|
||||
}()
|
||||
if in == nil {
|
||||
return result, errors.New("pay center request: nil recharge")
|
||||
}
|
||||
if post == nil {
|
||||
return result, errors.New("pay center request: nil poster")
|
||||
}
|
||||
if err := validateConfig(true); err != nil {
|
||||
return result, err
|
||||
}
|
||||
amount, err := ParseYuanToFen(in.Money)
|
||||
if err != nil || amount <= 0 || in.TradeNo == "" || in.Type == "" {
|
||||
return result, errors.New("新支付中心下单参数错误:订单号、支付方式和正数金额不能为空")
|
||||
}
|
||||
in.fill()
|
||||
|
||||
var response RechargeResp
|
||||
code, requestErr := post(in.Ctx, in, &response)
|
||||
if requestErr != nil {
|
||||
return result, fmt.Errorf("post pay center order: %w", requestErr)
|
||||
}
|
||||
if code != http.StatusOK {
|
||||
return result, fmt.Errorf("pay center returned HTTP status %d", code)
|
||||
}
|
||||
if response.Code != http.StatusOK {
|
||||
return result, fmt.Errorf("pay center returned provider code %d", response.Code)
|
||||
}
|
||||
if response.Err != "" {
|
||||
return result, fmt.Errorf("pay center rejected order: %s", response.Err)
|
||||
}
|
||||
// 外层成功但没有订单号/支付链接不能落为处理中订单。
|
||||
if response.Msg.OID == "" || response.Msg.PayUrl == "" {
|
||||
return result, errors.New("新支付中心下单结果缺少订单号或支付链接")
|
||||
}
|
||||
return response.Msg, nil
|
||||
}
|
||||
|
||||
func (in *Recharge) fill() {
|
||||
in.MercID = in.GetMercID()
|
||||
in.Time = Generate13DigitString()
|
||||
in.Info.App = payCenterConf.AppName
|
||||
in.NotifyUrl = in.GetNotifyURL()
|
||||
in.SignType = "1"
|
||||
in.Sign = in.sign()
|
||||
}
|
||||
|
||||
func (in *Recharge) GetNotifyURL() string {
|
||||
return payCenterConf.CallbackUrl + "/api/web/third/newpaycenter/recharge/callback"
|
||||
}
|
||||
|
||||
func (in *Recharge) GetCancelNotifyURL() string {
|
||||
return payCenterConf.CallbackUrl + "/api/web/third/newpaycenter/refund/callback"
|
||||
}
|
||||
|
||||
func (in *Recharge) sign() string {
|
||||
// 字段顺序必须与 girl 及支付中心约定一致,金额使用原始元字符串。
|
||||
var body bytes.Buffer
|
||||
body.WriteString(in.MercID)
|
||||
body.WriteString(in.Money)
|
||||
body.WriteString(in.NotifyUrl)
|
||||
body.WriteString(in.TradeNo)
|
||||
body.WriteString(in.Type)
|
||||
body.WriteString(in.GetAppSecret())
|
||||
return md5Sign(body.String())
|
||||
}
|
||||
|
||||
func (in *Recharge) GetURL() string {
|
||||
return payCenterConf.ApiUrl + "/api/shark/topay"
|
||||
}
|
||||
|
||||
func (in *Recharge) Success() string {
|
||||
return "success"
|
||||
}
|
||||
|
||||
func (in *Recharge) GetAppSecret() string {
|
||||
return payCenterConf.MercSecret
|
||||
}
|
||||
|
||||
func (in *Recharge) GetMercID() string {
|
||||
return payCenterConf.MercID
|
||||
}
|
||||
Reference in New Issue
Block a user