feat(basePay): 添加测试环境支持和新功能

- 为 transPreorder 和 relationClose 添加测试环境请求方法
- 更新所有模块导入路径,统一使用新的 common 包
- 重构多个结构体和服务命名以提高一致性
- 新增 labs 包整合基础支付相关功能模块
- 创建 SDK 入口文件便于统一管理和调用
- 在 consts 中增加 BASE_TEST_URL 常量定义
- 优化请求数据结构体字段注释和文档标签
- 移除冗余代码并调整部分函数签名以增强可读性
This commit is contained in:
maguodong
2025-10-21 14:34:42 +08:00
parent 7b9657a9e8
commit 54917f8985
50 changed files with 158 additions and 83 deletions
+117
View File
@@ -0,0 +1,117 @@
package common
import (
"context"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
ran "math/rand"
"time"
"github.com/black1552/base-common/utils"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/util/gconv"
)
// Client 拉卡拉SDK客户端
type Client[T any] struct {
Config *Config
response T
ctx context.Context
}
// NewClient 创建拉卡拉SDK客户端
func NewClient[T any](ctx context.Context, cfgJson string) *Client[T] {
var config *Config
err := gconv.Struct(cfgJson, &config)
if err != nil {
return nil
}
return &Client[T]{
Config: config,
ctx: ctx,
}
}
func (c *Client[T]) generateNonceStr() string {
const letterBytes = "abcdefghijklmnopqrstuvwxyz0123456789"
nonce := make([]byte, 12)
for i := range nonce {
nonce[i] = letterBytes[ran.Intn(len(letterBytes))]
}
return string(nonce)
}
// generateSign 生成签名
func (c *Client[T]) generateSign(request []byte) (string, error) {
// 生成随机字符串
nonceStr := c.generateNonceStr()
// 获取当前时间戳(秒)
timestamp := fmt.Sprintf("%d", time.Now().Unix())
// 构建待签名报文
signData := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n", c.Config.AppId, c.Config.SerialNo, timestamp, nonceStr, string(request))
// 计算签名
hashed := sha256.Sum256([]byte(signData))
privateKey, err := c.loadPrivateKey()
if err != nil {
return "", err
}
signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed[:])
if err != nil {
return "", err
}
signatureBase64 := base64.StdEncoding.EncodeToString(signature)
// 构建Authorization头
authorization := fmt.Sprintf("LKLAPI-SHA256withRSA appid=\"%s\",serial_no=\"%s\",timestamp=\"%s\",nonce_str=\"%s\",signature=\"%s\"",
c.Config.AppId, c.Config.SerialNo, timestamp, nonceStr, signatureBase64)
return authorization, nil
}
func (c *Client[T]) loadPrivateKey() (*rsa.PrivateKey, error) {
block, _ := pem.Decode(gfile.GetBytes(c.Config.PrivateKey))
if block == nil {
return nil, fmt.Errorf("无法解码私钥PEM数据")
}
// 解析PKCS#8格式私钥
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
return privateKey.(*rsa.PrivateKey), nil
}
// DoRequest 发送HTTP请求
func (c *Client[T]) DoRequest(url string, reqData interface{}) (*T, error) {
jsonStr, err := gjson.EncodeString(reqData)
if err != nil {
return nil, fmt.Errorf("请求参数转JSON字符串失败: %v", err)
}
reqJson, err := CleanJSON(jsonStr)
if err != nil {
return nil, fmt.Errorf("清理JSON空值失败: %v", err)
}
g.Log().Infof(c.ctx, "清理后的json: %v", string(reqJson))
auth, err := c.generateSign(reqJson)
if err != nil {
return nil, fmt.Errorf("生成签名失败: %v", err)
}
header := map[string]string{
"Authorization": auth,
"Content-Type": "application/json",
"Accept": "application/json",
}
// 设置其他必要的请求头
return utils.NewClient[T](reqJson, url, header).Post(c.ctx)
}
+118
View File
@@ -0,0 +1,118 @@
package common
import (
"encoding/json"
"reflect"
)
// CleanJSON 清理JSON字符串中的空值和0值字段
func CleanJSON(jsonStr string) ([]byte, error) {
var data interface{}
// 解析JSON字符串
if err := json.Unmarshal([]byte(jsonStr), &data); err != nil {
return nil, err
}
// 递归清理数据
cleaned := cleanData(data)
// 转换回JSON字符串
result, err := json.Marshal(cleaned)
if err != nil {
return nil, err
}
return result, nil
}
// cleanData 递归清理map或slice中的空值和0值
func cleanData(data interface{}) interface{} {
if data == nil {
return nil
}
v := reflect.ValueOf(data)
switch v.Kind() {
case reflect.Map:
cleanedMap := make(map[string]interface{})
for _, key := range v.MapKeys() {
val := v.MapIndex(key)
if val.Interface() != nil && !isEmptyValue(val) {
cleanedVal := cleanData(val.Interface())
// 只有清理后的值不为空才添加
if !isCleanedValueEmpty(cleanedVal) {
cleanedMap[key.String()] = cleanedVal
}
}
}
return cleanedMap
case reflect.Slice:
cleanedSlice := make([]interface{}, 0)
for i := 0; i < v.Len(); i++ {
item := v.Index(i)
if item.Interface() != nil && !isEmptyValue(item) {
cleanedItem := cleanData(item.Interface())
if !isCleanedValueEmpty(cleanedItem) {
cleanedSlice = append(cleanedSlice, cleanedItem)
}
}
}
// 如果slice为空,则返回nil而不是空slice
if len(cleanedSlice) == 0 {
return nil
}
return cleanedSlice
default:
return data
}
}
// isEmptyValue 检查值是否为空值或0值
func isEmptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.Interface, reflect.Ptr:
return v.IsNil()
case reflect.Slice, reflect.Map:
return v.Len() == 0
case reflect.String:
return v.String() == "" || v.String() == "0"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Bool:
return !v.Bool()
}
// 特殊处理nil值
if !v.IsValid() {
return true
}
return false
}
// isCleanedValueEmpty 检查清理后的值是否为空
func isCleanedValueEmpty(val interface{}) bool {
if val == nil {
return true
}
v := reflect.ValueOf(val)
switch v.Kind() {
case reflect.Map:
return v.Len() == 0
case reflect.Slice:
return v.Len() == 0
case reflect.String:
return val == ""
}
return false
}
+14
View File
@@ -0,0 +1,14 @@
package common
// Config 拉卡拉SDK配置
type Config struct {
PublicKey string `json:"public_key" dc:"公钥字符串"`
PrivateKey string `json:"private_key" dc:"私钥字符串"`
AppId string `json:"app_id" dc:"lakala应用ID"`
SerialNo string `json:"serial_no" dc:"序列号"`
SubAppId string `json:"sub_app_id" dc:"子应用ID 微信AppId"`
Version string `json:"version" dc:"lakala版本号"`
AccountType string `json:"account_type" dc:"账户类型"`
TransType string `json:"trans_type" dc:"交易类型"`
NotifyUrl string `json:"notify_url" dc:"回调地址"`
}