开始增加支付-1
This commit is contained in:
@@ -0,0 +1,355 @@
|
||||
package leshua
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/config"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrConfigIncomplete = errors.New("leshua payment config incomplete")
|
||||
ErrUnsupportedSign = errors.New("unsupported leshua sign type")
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
cfg config.LeshuaPaymentConfig
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
type CreatePaymentRequest struct {
|
||||
ThirdOrderID string
|
||||
AmountCent int64
|
||||
PayWay string
|
||||
JSPayFlag string
|
||||
NotifyURL string
|
||||
JumpURL string
|
||||
ClientIP string
|
||||
Body string
|
||||
Attach string
|
||||
}
|
||||
|
||||
type CreatePaymentResponse struct {
|
||||
RespCode string
|
||||
ResultCode string
|
||||
ErrorCode string
|
||||
ErrorMessage string
|
||||
MerchantID string
|
||||
ThirdOrderID string
|
||||
ProviderOrderID string
|
||||
PayWay string
|
||||
TDCode string
|
||||
JSPayURL string
|
||||
JSPayInfo string
|
||||
Raw map[string]string
|
||||
}
|
||||
|
||||
type QueryPaymentResponse struct {
|
||||
RespCode string
|
||||
ResultCode string
|
||||
ErrorCode string
|
||||
ErrorMessage string
|
||||
MerchantID string
|
||||
ThirdOrderID string
|
||||
ProviderOrderID string
|
||||
Status string
|
||||
Amount string
|
||||
PayWay string
|
||||
PayTime string
|
||||
Raw map[string]string
|
||||
}
|
||||
|
||||
func NewClient(cfg config.LeshuaPaymentConfig) *Client {
|
||||
return &Client{
|
||||
cfg: cfg,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) CreatePayment(ctx context.Context, req CreatePaymentRequest) (*CreatePaymentResponse, map[string]string, error) {
|
||||
if err := c.validate(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
payWay := firstNonEmpty(req.PayWay, c.cfg.PayWay, "ZFBZF")
|
||||
jsPayFlag := firstNonEmpty(req.JSPayFlag, c.cfg.JSPayFlag, "2")
|
||||
params := map[string]string{
|
||||
"service": "get_tdcode",
|
||||
"merchant_id": c.cfg.MerchantID,
|
||||
"third_order_id": req.ThirdOrderID,
|
||||
"amount": fmt.Sprintf("%d", req.AmountCent),
|
||||
"pay_way": payWay,
|
||||
"jspay_flag": jsPayFlag,
|
||||
"nonce_str": Nonce(32),
|
||||
"body": sanitizeText(req.Body, 128),
|
||||
"attach": sanitizeText(req.Attach, 64),
|
||||
}
|
||||
if c.cfg.SignType != "" && !strings.EqualFold(c.cfg.SignType, "MD5") {
|
||||
params["sign_type"] = c.cfg.SignType
|
||||
}
|
||||
if req.NotifyURL != "" {
|
||||
params["notify_url"] = req.NotifyURL
|
||||
}
|
||||
if req.JumpURL != "" {
|
||||
params["jump_url"] = req.JumpURL
|
||||
}
|
||||
if req.ClientIP != "" {
|
||||
params["client_ip"] = req.ClientIP
|
||||
}
|
||||
params["sign"] = Sign(params, c.cfg.SignKey, SignOptions{})
|
||||
raw, err := c.post(ctx, params)
|
||||
if err != nil {
|
||||
return nil, params, err
|
||||
}
|
||||
resp := &CreatePaymentResponse{
|
||||
RespCode: raw["resp_code"],
|
||||
ResultCode: raw["result_code"],
|
||||
ErrorCode: raw["error_code"],
|
||||
ErrorMessage: firstNonEmpty(raw["error_msg"], raw["resp_msg"]),
|
||||
MerchantID: raw["merchant_id"],
|
||||
ThirdOrderID: raw["third_order_id"],
|
||||
ProviderOrderID: raw["leshua_order_id"],
|
||||
PayWay: raw["pay_way"],
|
||||
TDCode: raw["td_code"],
|
||||
JSPayURL: raw["jspay_url"],
|
||||
JSPayInfo: raw["jspay_info"],
|
||||
Raw: raw,
|
||||
}
|
||||
return resp, params, nil
|
||||
}
|
||||
|
||||
func (c *Client) QueryPayment(ctx context.Context, thirdOrderID, providerOrderID string) (*QueryPaymentResponse, error) {
|
||||
if err := c.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
params := map[string]string{
|
||||
"service": "query_status",
|
||||
"merchant_id": c.cfg.MerchantID,
|
||||
"nonce_str": Nonce(32),
|
||||
}
|
||||
if providerOrderID != "" {
|
||||
params["leshua_order_id"] = providerOrderID
|
||||
} else {
|
||||
params["third_order_id"] = thirdOrderID
|
||||
}
|
||||
if c.cfg.SignType != "" && !strings.EqualFold(c.cfg.SignType, "MD5") {
|
||||
params["sign_type"] = c.cfg.SignType
|
||||
}
|
||||
params["sign"] = Sign(params, c.cfg.SignKey, SignOptions{})
|
||||
raw, err := c.post(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &QueryPaymentResponse{
|
||||
RespCode: raw["resp_code"],
|
||||
ResultCode: raw["result_code"],
|
||||
ErrorCode: raw["error_code"],
|
||||
ErrorMessage: firstNonEmpty(raw["error_msg"], raw["resp_msg"]),
|
||||
MerchantID: raw["merchant_id"],
|
||||
ThirdOrderID: raw["third_order_id"],
|
||||
ProviderOrderID: raw["leshua_order_id"],
|
||||
Status: raw["status"],
|
||||
Amount: raw["amount"],
|
||||
PayWay: raw["pay_way"],
|
||||
PayTime: raw["pay_time"],
|
||||
Raw: raw,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) VerifyNotify(params map[string]string) bool {
|
||||
key := firstNonEmpty(c.cfg.NotifyKey, c.cfg.SignKey)
|
||||
if key == "" {
|
||||
return false
|
||||
}
|
||||
got := strings.ToUpper(params["sign"])
|
||||
if got == "" {
|
||||
return false
|
||||
}
|
||||
expected := Sign(params, key, SignOptions{
|
||||
IncludeEmpty: true,
|
||||
ExcludeKeys: []string{"error_code", "sign"},
|
||||
})
|
||||
return got == expected
|
||||
}
|
||||
|
||||
func (c *Client) validate() error {
|
||||
if c.cfg.GatewayURL == "" || c.cfg.MerchantID == "" || c.cfg.SignKey == "" {
|
||||
return ErrConfigIncomplete
|
||||
}
|
||||
if c.cfg.SignType != "" && !strings.EqualFold(c.cfg.SignType, "MD5") {
|
||||
return ErrUnsupportedSign
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) post(ctx context.Context, params map[string]string) (map[string]string, error) {
|
||||
values := url.Values{}
|
||||
for key, value := range params {
|
||||
values.Set(key, value)
|
||||
}
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.cfg.GatewayURL, strings.NewReader(values.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
httpResp, err := c.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer httpResp.Body.Close()
|
||||
body, err := io.ReadAll(io.LimitReader(httpResp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("leshua http status %d: %s", httpResp.StatusCode, string(body))
|
||||
}
|
||||
return ParsePayload(body)
|
||||
}
|
||||
|
||||
type SignOptions struct {
|
||||
IncludeEmpty bool
|
||||
ExcludeKeys []string
|
||||
}
|
||||
|
||||
func Sign(params map[string]string, key string, opts SignOptions) string {
|
||||
excluded := map[string]bool{}
|
||||
for _, item := range opts.ExcludeKeys {
|
||||
excluded[item] = true
|
||||
}
|
||||
if len(opts.ExcludeKeys) == 0 {
|
||||
excluded["sign"] = true
|
||||
}
|
||||
keys := make([]string, 0, len(params))
|
||||
for name, value := range params {
|
||||
if excluded[name] {
|
||||
continue
|
||||
}
|
||||
if !opts.IncludeEmpty && value == "" {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, name)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
parts := make([]string, 0, len(keys)+1)
|
||||
for _, name := range keys {
|
||||
parts = append(parts, name+"="+params[name])
|
||||
}
|
||||
parts = append(parts, "key="+key)
|
||||
sum := md5.Sum([]byte(strings.Join(parts, "&")))
|
||||
return strings.ToUpper(hex.EncodeToString(sum[:]))
|
||||
}
|
||||
|
||||
func ParsePayload(body []byte) (map[string]string, error) {
|
||||
trimmed := bytes.TrimSpace(body)
|
||||
if len(trimmed) == 0 {
|
||||
return map[string]string{}, nil
|
||||
}
|
||||
if trimmed[0] == '<' {
|
||||
return parseXMLPayload(trimmed)
|
||||
}
|
||||
if trimmed[0] == '{' {
|
||||
var raw map[string]any
|
||||
if err := json.Unmarshal(trimmed, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := map[string]string{}
|
||||
for key, value := range raw {
|
||||
out[key] = fmt.Sprint(value)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
values, err := url.ParseQuery(string(trimmed))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := map[string]string{}
|
||||
for key, item := range values {
|
||||
if len(item) > 0 {
|
||||
out[key] = item[0]
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, fmt.Errorf("unsupported leshua payload: %s", string(trimmed))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func parseXMLPayload(body []byte) (map[string]string, error) {
|
||||
decoder := xml.NewDecoder(bytes.NewReader(body))
|
||||
out := map[string]string{}
|
||||
var current string
|
||||
for {
|
||||
token, err := decoder.Token()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch item := token.(type) {
|
||||
case xml.StartElement:
|
||||
current = item.Name.Local
|
||||
case xml.CharData:
|
||||
value := strings.TrimSpace(string(item))
|
||||
if current != "" && current != "xml" && value != "" {
|
||||
out[current] = value
|
||||
}
|
||||
case xml.EndElement:
|
||||
current = ""
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func Nonce(length int) string {
|
||||
const alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
if length <= 0 {
|
||||
length = 32
|
||||
}
|
||||
buf := make([]byte, length)
|
||||
random := make([]byte, length)
|
||||
if _, err := rand.Read(random); err != nil {
|
||||
for i := range buf {
|
||||
buf[i] = alphabet[int(time.Now().UnixNano())%len(alphabet)]
|
||||
}
|
||||
return string(buf)
|
||||
}
|
||||
for i, item := range random {
|
||||
buf[i] = alphabet[int(item)%len(alphabet)]
|
||||
}
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func sanitizeText(value string, max int) string {
|
||||
value = strings.ReplaceAll(value, "\n", " ")
|
||||
value = strings.ReplaceAll(value, "\r", " ")
|
||||
value = strings.TrimSpace(value)
|
||||
if max > 0 && len([]rune(value)) > max {
|
||||
return string([]rune(value)[:max])
|
||||
}
|
||||
return value
|
||||
}
|
||||
Reference in New Issue
Block a user