This commit is contained in:
2025-04-27 12:17:18 +08:00
parent b60f6ffb3e
commit 2aea96db2c
128 changed files with 396 additions and 222 deletions

View File

@@ -0,0 +1,196 @@
package service
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"net/http"
"strconv"
"sync/atomic"
"time"
"tyc-server/app/main/api/internal/config"
"tyc-server/pkg/lzkit/lzUtils"
"github.com/smartwalle/alipay/v3"
)
type AliPayService struct {
config config.AlipayConfig
AlipayClient *alipay.Client
}
// NewAliPayService 是一个构造函数,用于初始化 AliPayService
func NewAliPayService(c config.Config) *AliPayService {
client, err := alipay.New(c.Alipay.AppID, c.Alipay.PrivateKey, c.Alipay.IsProduction)
if err != nil {
panic(fmt.Sprintf("创建支付宝客户端失败: %v", err))
}
// 加载支付宝公钥
err = client.LoadAliPayPublicKey(c.Alipay.AlipayPublicKey)
if err != nil {
panic(fmt.Sprintf("加载支付宝公钥失败: %v", err))
}
return &AliPayService{
config: c.Alipay,
AlipayClient: client,
}
}
func (a *AliPayService) CreateAlipayAppOrder(amount float64, subject string, outTradeNo string) (string, error) {
client := a.AlipayClient
totalAmount := lzUtils.ToAlipayAmount(amount)
// 构造移动支付请求
p := alipay.TradeAppPay{
Trade: alipay.Trade{
Subject: subject,
OutTradeNo: outTradeNo,
TotalAmount: totalAmount,
ProductCode: "QUICK_MSECURITY_PAY", // 移动端支付专用代码
NotifyURL: a.config.NotifyUrl, // 异步回调通知地址
},
}
// 获取APP支付字符串这里会签名
payStr, err := client.TradeAppPay(p)
if err != nil {
return "", fmt.Errorf("创建支付宝订单失败: %v", err)
}
return payStr, nil
}
// CreateAlipayH5Order 创建支付宝H5支付订单
func (a *AliPayService) CreateAlipayH5Order(amount float64, subject string, outTradeNo string, brand string) (string, error) {
var returnURL string
var notifyURL string
if brand == "tyc" {
returnURL = "https://www.tianyuancha.cn/report"
notifyURL = "https://www.tianyuancha.cn/api/v1/pay/alipay/callback"
} else {
returnURL = a.config.ReturnURL
notifyURL = a.config.NotifyUrl
}
client := a.AlipayClient
totalAmount := lzUtils.ToAlipayAmount(amount)
// 构造H5支付请求
p := alipay.TradeWapPay{
Trade: alipay.Trade{
Subject: subject,
OutTradeNo: outTradeNo,
TotalAmount: totalAmount,
ProductCode: "QUICK_WAP_PAY", // H5支付专用产品码
NotifyURL: notifyURL, // 异步回调通知地址
ReturnURL: returnURL,
},
}
// 获取H5支付请求字符串这里会签名
payUrl, err := client.TradeWapPay(p)
if err != nil {
return "", fmt.Errorf("创建支付宝H5订单失败: %v", err)
}
return payUrl.String(), nil
}
// CreateAlipayOrder 根据平台类型创建支付宝支付订单
func (a *AliPayService) CreateAlipayOrder(ctx context.Context, amount float64, subject string, outTradeNo string, brand string) (string, error) {
// 根据 ctx 中的 platform 判断平台
platform, platformOk := ctx.Value("platform").(string)
if !platformOk {
return "", fmt.Errorf("无的支付平台: %s", platform)
}
switch platform {
case "app":
// 调用App支付的创建方法
return a.CreateAlipayAppOrder(amount, subject, outTradeNo)
case "h5":
// 调用H5支付的创建方法并传入 returnUrl
return a.CreateAlipayH5Order(amount, subject, outTradeNo, brand)
default:
return "", fmt.Errorf("不支持的支付平台: %s", platform)
}
}
// AliRefund 发起支付宝退款
func (a *AliPayService) AliRefund(ctx context.Context, outTradeNo string, refundAmount float64) (*alipay.TradeRefundRsp, error) {
refund := alipay.TradeRefund{
OutTradeNo: outTradeNo,
RefundAmount: lzUtils.ToAlipayAmount(refundAmount),
OutRequestNo: fmt.Sprintf("%s-refund", outTradeNo),
}
// 发起退款请求
refundResp, err := a.AlipayClient.TradeRefund(ctx, refund)
if err != nil {
return nil, fmt.Errorf("支付宝退款请求错误:%v", err)
}
return refundResp, nil
}
// HandleAliPaymentNotification 支付宝支付回调
func (a *AliPayService) HandleAliPaymentNotification(r *http.Request) (*alipay.Notification, error) {
// 解析表单
err := r.ParseForm()
if err != nil {
return nil, fmt.Errorf("解析请求表单失败:%v", err)
}
// 解析并验证通知DecodeNotification 会自动验证签名
notification, err := a.AlipayClient.DecodeNotification(r.Form)
if err != nil {
return nil, fmt.Errorf("验证签名失败: %v", err)
}
return notification, nil
}
func (a *AliPayService) QueryOrderStatus(ctx context.Context, outTradeNo string) (*alipay.TradeQueryRsp, error) {
queryRequest := alipay.TradeQuery{
OutTradeNo: outTradeNo,
}
// 发起查询请求
resp, err := a.AlipayClient.TradeQuery(ctx, queryRequest)
if err != nil {
return nil, fmt.Errorf("查询支付宝订单失败: %v", err)
}
// 返回交易状态
if resp.IsSuccess() {
return resp, nil
}
return nil, fmt.Errorf("查询支付宝订单失败: %v", resp.SubMsg)
}
// 添加全局原子计数器
var alipayOrderCounter uint32 = 0
// GenerateOutTradeNo 生成唯一订单号的函数 - 优化版本
func (a *AliPayService) GenerateOutTradeNo() string {
// 获取当前时间戳(毫秒级)
timestamp := time.Now().UnixMilli()
timeStr := strconv.FormatInt(timestamp, 10)
// 原子递增计数器
counter := atomic.AddUint32(&alipayOrderCounter, 1)
// 生成4字节真随机数
randomBytes := make([]byte, 4)
_, err := rand.Read(randomBytes)
if err != nil {
// 如果随机数生成失败,回退到使用时间纳秒数据
randomBytes = []byte(strconv.FormatInt(time.Now().UnixNano()%1000000, 16))
}
randomHex := hex.EncodeToString(randomBytes)
// 组合所有部分: 前缀 + 时间戳 + 计数器 + 随机数
orderNo := fmt.Sprintf("%s%06x%s", timeStr[:10], counter%0xFFFFFF, randomHex[:6])
// 确保长度不超过32字符大多数支付平台的限制
if len(orderNo) > 32 {
orderNo = orderNo[:32]
}
return orderNo
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,169 @@
package service
import (
"context"
"crypto/ecdsa"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"io/ioutil"
"net/http"
"strconv"
"time"
"tyc-server/app/main/api/internal/config"
"github.com/golang-jwt/jwt/v4"
)
// ApplePayService 是 Apple IAP 支付服务的结构体
type ApplePayService struct {
config config.ApplepayConfig // 配置项
}
// NewApplePayService 是一个构造函数,用于初始化 ApplePayService
func NewApplePayService(c config.Config) *ApplePayService {
return &ApplePayService{
config: c.Applepay,
}
}
func (a *ApplePayService) GetIappayAppID(productName string) string {
return fmt.Sprintf("%s.%s", a.config.BundleID, productName)
}
// VerifyReceipt 验证苹果支付凭证
func (a *ApplePayService) VerifyReceipt(ctx context.Context, receipt string) (*AppleVerifyResponse, error) {
var reqUrl string
if a.config.Sandbox {
reqUrl = a.config.SandboxVerifyURL
} else {
reqUrl = a.config.ProductionVerifyURL
}
// 读取私钥
privateKey, err := loadPrivateKey(a.config.LoadPrivateKeyPath)
if err != nil {
return nil, fmt.Errorf("加载私钥失败:%v", err)
}
// 生成 JWT
token, err := generateJWT(privateKey, a.config.KeyID, a.config.IssuerID)
if err != nil {
return nil, fmt.Errorf("生成JWT失败%v", err)
}
// 构造查询参数
queryParams := fmt.Sprintf("?receipt-data=%s", receipt)
fullUrl := reqUrl + queryParams
// 构建 HTTP GET 请求
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullUrl, nil)
if err != nil {
return nil, fmt.Errorf("创建 HTTP 请求失败:%v", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
// 发送请求
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("请求苹果验证接口失败:%v", err)
}
defer resp.Body.Close()
// 解析响应
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应体失败:%v", err)
}
var verifyResponse AppleVerifyResponse
err = json.Unmarshal(body, &verifyResponse)
if err != nil {
return nil, fmt.Errorf("解析响应体失败:%v", err)
}
// 根据实际响应处理逻辑
if verifyResponse.Status != 0 {
return nil, fmt.Errorf("验证失败,状态码:%d", verifyResponse.Status)
}
return &verifyResponse, nil
}
func loadPrivateKey(path string) (*ecdsa.PrivateKey, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
block, _ := pem.Decode(data)
if block == nil || block.Type != "PRIVATE KEY" {
return nil, fmt.Errorf("无效的私钥数据")
}
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
ecdsaKey, ok := key.(*ecdsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("私钥类型错误")
}
return ecdsaKey, nil
}
func generateJWT(privateKey *ecdsa.PrivateKey, keyID, issuerID string) (string, error) {
now := time.Now()
claims := jwt.RegisteredClaims{
Issuer: issuerID,
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(1 * time.Hour)),
Audience: jwt.ClaimStrings{"appstoreconnect-v1"},
}
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
token.Header["kid"] = keyID
tokenString, err := token.SignedString(privateKey)
if err != nil {
return "", err
}
return tokenString, nil
}
// GenerateOutTradeNo 生成唯一订单号
func (a *ApplePayService) GenerateOutTradeNo() string {
length := 16
timestamp := time.Now().UnixNano()
timeStr := strconv.FormatInt(timestamp, 10)
randomPart := strconv.Itoa(int(timestamp % 1e6))
combined := timeStr + randomPart
if len(combined) >= length {
return combined[:length]
}
for len(combined) < length {
combined += strconv.Itoa(int(timestamp % 10))
}
return combined
}
// AppleVerifyResponse 定义苹果验证接口的响应结构
type AppleVerifyResponse struct {
Status int `json:"status"` // 验证状态码0 表示收据有效
Receipt *Receipt `json:"receipt"` // 收据信息
}
// Receipt 定义收据的精简结构
type Receipt struct {
BundleID string `json:"bundle_id"` // 应用的 Bundle ID
InApp []InAppItem `json:"in_app"` // 应用内购买记录
}
// InAppItem 定义单条交易记录
type InAppItem struct {
ProductID string `json:"product_id"` // 商品 ID
TransactionID string `json:"transaction_id"` // 交易 ID
PurchaseDate string `json:"purchase_date"` // 购买日期 (ISO 8601)
OriginalTransID string `json:"original_transaction_id"` // 原始交易 ID
}

View File

@@ -0,0 +1,132 @@
// asynq_service.go
package service
import (
"encoding/json"
"time"
"tyc-server/app/main/api/internal/config"
"tyc-server/app/main/api/internal/types"
"github.com/hibiken/asynq"
"github.com/zeromicro/go-zero/core/logx"
)
type AsynqService struct {
client *asynq.Client
config config.Config
}
// NewAsynqService 创建并初始化 Asynq 客户端
func NewAsynqService(c config.Config) *AsynqService {
client := asynq.NewClient(asynq.RedisClientOpt{
Addr: c.CacheRedis[0].Host,
Password: c.CacheRedis[0].Pass,
})
return &AsynqService{client: client, config: c}
}
// Close 关闭 Asynq 客户端
func (s *AsynqService) Close() error {
return s.client.Close()
}
func (s *AsynqService) SendQueryTask(orderID int64) error {
// 准备任务的 payload
payload := types.MsgPaySuccessQueryPayload{
OrderID: orderID,
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
logx.Errorf("发送异步任务失败 (无法编码 payload): %v, 订单号: %d", err, orderID)
return err // 直接返回错误,避免继续执行
}
options := []asynq.Option{
asynq.MaxRetry(5), // 设置最大重试次数
}
// 创建任务
task := asynq.NewTask(types.MsgPaySuccessQuery, payloadBytes, options...)
// 将任务加入队列并获取任务信息
info, err := s.client.Enqueue(task)
if err != nil {
logx.Errorf("发送异步任务失败 (加入队列失败): %+v, 订单号: %d", err, orderID)
return err
}
// 记录成功日志,带上任务 ID 和队列信息
logx.Infof("发送异步任务成功任务ID: %s, 队列: %s, 订单号: %d", info.ID, info.Queue, orderID)
return nil
}
// SendCarMaintenanceQueryTaskWithBackoff 发送带有指数退避重试策略的车辆维保记录查询的异步任务
func (s *AsynqService) SendCarMaintenanceQueryTaskWithBackoff(orderID string, retryCount int, queryID int64) error {
// 准备任务的 payload
payload := types.MsgCarMaintenanceQueryPayload{
OrderID: orderID,
RetryCount: retryCount + 1, // 增加重试次数
QueryID: queryID, // 关联的query表记录ID
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
logx.Errorf("发送重试车辆维保记录查询任务失败 (无法编码 payload): %v, 订单号: %s", err, orderID)
return err
}
// 确定延迟时间
var delay time.Duration
if retryCount == 0 {
// 第一次执行,立即进行
delay = 0
} else {
// 非首次执行,使用指数退避策略
// 基础延迟为3秒每次重试后延迟时间翻倍最长不超过1小时
baseDelay := 3 * time.Second
maxDelay := 1 * time.Hour
delay = baseDelay
for i := 1; i < retryCount; i++ {
delay = delay * 2
if delay > maxDelay {
delay = maxDelay
break
}
}
}
// 保存延迟时间到payload
payload.DelayDuration = int64(delay)
payloadBytes, _ = json.Marshal(payload)
options := []asynq.Option{
asynq.MaxRetry(0), // 使用我们自己的重试逻辑不使用asynq的自动重试
}
// 如果有延迟,添加延迟选项
if delay > 0 {
options = append(options, asynq.ProcessIn(delay))
}
// 创建任务
task := asynq.NewTask(types.MsgCarMaintenanceQuery, payloadBytes, options...)
// 将任务加入队列并获取任务信息
info, err := s.client.Enqueue(task)
if err != nil {
logx.Errorf("发送重试车辆维保记录查询任务失败 (加入队列失败): %+v, 订单号: %s", err, orderID)
return err
}
// 记录成功日志,带上任务 ID 和队列信息
if delay == 0 {
logx.Infof("发送车辆维保记录查询任务成功立即执行任务ID: %s, 队列: %s, 订单号: %s, 查询ID: %d",
info.ID, info.Queue, orderID, queryID)
} else {
logx.Infof("发送重试车辆维保记录查询任务成功任务ID: %s, 队列: %s, 延迟: %v, 重试次数: %d, 订单号: %s, 查询ID: %d",
info.ID, info.Queue, delay, retryCount+1, orderID, queryID)
}
return nil
}

View File

@@ -0,0 +1,88 @@
package service
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"tyc-server/app/main/api/internal/config"
"github.com/tidwall/gjson"
)
type TianjuService struct {
config config.TianjuConfig
}
func NewTianjuService(c config.Config) *TianjuService {
return &TianjuService{
config: c.TianjuConfig,
}
}
func (t *TianjuService) Request(apiPath string, params map[string]interface{}) ([]byte, error) {
// 确保params中包含key参数
reqParams := make(map[string]interface{})
// 复制用户参数
for k, v := range params {
reqParams[k] = v
}
// 如果未提供key则使用配置中的ApiKey
if _, ok := reqParams["key"]; !ok {
reqParams["key"] = t.config.ApiKey
}
// 构建完整的URL假设BaseURL已包含https://前缀
fullURL := fmt.Sprintf("%s/%s/index", strings.TrimRight(t.config.BaseURL, "/"), apiPath)
// 构建表单数据
formData := url.Values{}
for k, v := range reqParams {
// 将不同类型的值转换为字符串
switch val := v.(type) {
case string:
formData.Add(k, val)
case int, int64, float64:
formData.Add(k, fmt.Sprintf("%v", val))
default:
// 对于复杂类型转为JSON字符串
jsonBytes, err := json.Marshal(val)
if err != nil {
return nil, fmt.Errorf("参数值序列化失败: %w", err)
}
formData.Add(k, string(jsonBytes))
}
}
// 发起HTTP请求 - 使用表单数据
resp, err := http.PostForm(fullURL, formData)
if err != nil {
return nil, fmt.Errorf("发送请求失败: %w", err)
}
defer resp.Body.Close()
// 读取响应体
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %w", err)
}
// 检查响应状态码
code := gjson.GetBytes(body, "code").Int()
if code != 200 {
msg := gjson.GetBytes(body, "msg").String()
return nil, fmt.Errorf("天聚请求失败: 状态码 %d, 信息: %s", code, msg)
}
// 获取结果数据
result := gjson.GetBytes(body, "result")
if !result.Exists() {
return nil, fmt.Errorf("天聚请求result为空: %s", string(body))
}
return []byte(result.Raw), nil
}

View File

@@ -0,0 +1,218 @@
package service
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"tyc-server/app/main/api/internal/config"
"tyc-server/pkg/lzkit/crypto"
"github.com/tidwall/gjson"
)
type VerificationService struct {
c config.Config
westDexService *WestDexService
apiRequestService *ApiRequestService
}
func NewVerificationService(c config.Config, westDexService *WestDexService, apiRequestService *ApiRequestService) *VerificationService {
return &VerificationService{
c: c,
westDexService: westDexService,
apiRequestService: apiRequestService,
}
}
// 二要素
type TwoFactorVerificationRequest struct {
Name string
IDCard string
}
type TwoFactorVerificationResp struct {
Msg string `json:"msg"`
Success bool `json:"success"`
Code int `json:"code"`
Data *TwoFactorVerificationData `json:"data"` //
}
type TwoFactorVerificationData struct {
Birthday string `json:"birthday"`
Result int `json:"result"`
Address string `json:"address"`
OrderNo string `json:"orderNo"`
Sex string `json:"sex"`
Desc string `json:"desc"`
}
// 三要素
type ThreeFactorVerificationRequest struct {
Name string
IDCard string
Mobile string
}
// VerificationResult 定义校验结果结构体
type VerificationResult struct {
Passed bool
Err error
}
// ValidationError 定义校验错误类型
type ValidationError struct {
Message string
}
func (e *ValidationError) Error() string {
return e.Message
}
func (r *VerificationService) TwoFactorVerification(request TwoFactorVerificationRequest) (*VerificationResult, error) {
appCode := r.c.Ali.Code
requestUrl := "https://kzidcardv1.market.alicloudapi.com/api-mall/api/id_card/check"
// 构造查询参数
data := url.Values{}
data.Add("name", request.Name)
data.Add("idcard", request.IDCard)
req, err := http.NewRequest(http.MethodPost, requestUrl, strings.NewReader(data.Encode()))
if err != nil {
return nil, fmt.Errorf("创建请求失败: %+v", err)
}
req.Header.Set("Authorization", "APPCODE "+appCode)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("请求失败: %+v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("请求失败, 状态码: %d", resp.StatusCode)
}
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("响应体读取失败:%v", err)
}
var twoFactorVerificationResp TwoFactorVerificationResp
err = json.Unmarshal(respBody, &twoFactorVerificationResp)
if err != nil {
return nil, fmt.Errorf("二要素解析错误: %v", err)
}
if !twoFactorVerificationResp.Success {
return &VerificationResult{
Passed: false,
Err: &ValidationError{Message: "请输入有效的身份证号码"},
}, nil
}
if twoFactorVerificationResp.Code != 200 {
return &VerificationResult{
Passed: false,
Err: &ValidationError{Message: twoFactorVerificationResp.Msg},
}, nil
}
if twoFactorVerificationResp.Data.Result == 1 {
return &VerificationResult{
Passed: false,
Err: &ValidationError{Message: "姓名与身份证不一致"},
}, nil
}
return &VerificationResult{Passed: true, Err: nil}, nil
}
func (r *VerificationService) TwoFactorVerificationWest(ctx context.Context, request TwoFactorVerificationRequest) (*VerificationResult, error) {
params := map[string]interface{}{
"name": request.Name,
"id_card": request.IDCard,
}
marshal, err := json.Marshal(params)
if err != nil {
return nil, fmt.Errorf("二要素参数创建错误: %v", err)
}
resp, err := r.apiRequestService.ProcessLayoutIdcardRequest(ctx, marshal)
if err != nil {
return nil, fmt.Errorf("请求失败: %v", err)
}
respStr := string(resp.Data)
if respStr != "0" {
return &VerificationResult{
Passed: false,
Err: &ValidationError{Message: "姓名与身份证不一致"},
}, nil
}
return &VerificationResult{Passed: true, Err: nil}, nil
}
func (r *VerificationService) ThreeFactorVerification(request ThreeFactorVerificationRequest) (*VerificationResult, error) {
westName, err := crypto.WestDexEncrypt(request.Name, r.c.WestConfig.Key)
if err != nil {
return nil, err
}
westIDCard, err := crypto.WestDexEncrypt(request.IDCard, r.c.WestConfig.Key)
if err != nil {
return nil, err
}
westPhone, err := crypto.WestDexEncrypt(request.Mobile, r.c.WestConfig.Key)
if err != nil {
return nil, err
}
threeElementsReq := map[string]interface{}{
"data": map[string]interface{}{
"name": westName,
"idNo": westIDCard,
"phone": westPhone,
},
}
resp, err := r.westDexService.CallAPI("G15BJ02", threeElementsReq)
if err != nil {
return nil, err
}
dataResult := gjson.GetBytes(resp, "data.code")
if !dataResult.Exists() {
return nil, fmt.Errorf("code 字段不存在")
}
code := dataResult.Int()
switch code {
case 1000:
case 1002:
return &VerificationResult{
Passed: false,
Err: &ValidationError{Message: "姓名、证件号、手机号信息不一致"},
}, nil
case 1003:
return &VerificationResult{
Passed: false,
Err: &ValidationError{Message: "姓名、证件号、手机号信息不一致"},
}, nil
case 1004:
return &VerificationResult{
Passed: false,
Err: &ValidationError{Message: "姓名不正确"},
}, nil
case 1005:
return &VerificationResult{
Passed: false,
Err: &ValidationError{Message: "证件号码不正确"},
}, nil
default:
dataResultMsg := gjson.GetBytes(resp, "data.msg")
if !dataResultMsg.Exists() {
return nil, fmt.Errorf("msg字段不存在")
}
return nil, fmt.Errorf("三要素核验错误状态响应: %s", dataResultMsg.String())
}
return &VerificationResult{Passed: true, Err: nil}, nil
}

View File

@@ -0,0 +1,260 @@
package service
import (
"context"
"fmt"
"net/http"
"strconv"
"time"
"tyc-server/app/main/api/internal/config"
"tyc-server/app/main/model"
"tyc-server/common/ctxdata"
"tyc-server/pkg/lzkit/lzUtils"
"github.com/wechatpay-apiv3/wechatpay-go/core"
"github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers"
"github.com/wechatpay-apiv3/wechatpay-go/core/downloader"
"github.com/wechatpay-apiv3/wechatpay-go/core/notify"
"github.com/wechatpay-apiv3/wechatpay-go/core/option"
"github.com/wechatpay-apiv3/wechatpay-go/services/payments"
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/app"
"github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi"
"github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic"
"github.com/wechatpay-apiv3/wechatpay-go/utils"
"github.com/zeromicro/go-zero/core/logx"
)
const (
TradeStateSuccess = "SUCCESS" // 支付成功
TradeStateRefund = "REFUND" // 转入退款
TradeStateNotPay = "NOTPAY" // 未支付
TradeStateClosed = "CLOSED" // 已关闭
TradeStateRevoked = "REVOKED" // 已撤销(付款码支付)
TradeStateUserPaying = "USERPAYING" // 用户支付中(付款码支付)
TradeStatePayError = "PAYERROR" // 支付失败(其他原因,如银行返回失败)
)
type WechatPayService struct {
config config.WxpayConfig
wechatClient *core.Client
notifyHandler *notify.Handler
userAuthModel model.UserAuthModel
}
// NewWechatPayService 初始化微信支付服务
func NewWechatPayService(c config.Config, userAuthModel model.UserAuthModel) *WechatPayService {
// 从配置中加载商户信息
mchID := c.Wxpay.MchID
mchCertificateSerialNumber := c.Wxpay.MchCertificateSerialNumber
mchAPIv3Key := c.Wxpay.MchApiv3Key
// 从文件中加载商户私钥
mchPrivateKey, err := utils.LoadPrivateKeyWithPath(c.Wxpay.MchPrivateKeyPath)
if err != nil {
logx.Errorf("加载商户私钥失败: %v", err)
panic(fmt.Sprintf("初始化失败,服务停止: %v", err)) // 记录错误并停止程序
}
// 使用商户私钥和其他参数初始化微信支付客户端
opts := []core.ClientOption{
option.WithWechatPayAutoAuthCipher(mchID, mchCertificateSerialNumber, mchPrivateKey, mchAPIv3Key),
}
client, err := core.NewClient(context.Background(), opts...)
if err != nil {
logx.Errorf("创建微信支付客户端失败: %v", err)
panic(fmt.Sprintf("初始化失败,服务停止: %v", err)) // 记录错误并停止程序
}
// 在初始化时获取证书访问器并创建 notifyHandler
certificateVisitor := downloader.MgrInstance().GetCertificateVisitor(mchID)
notifyHandler, err := notify.NewRSANotifyHandler(mchAPIv3Key, verifiers.NewSHA256WithRSAVerifier(certificateVisitor))
if err != nil {
logx.Errorf("获取证书访问器失败: %v", err)
panic(fmt.Sprintf("初始化失败,服务停止: %v", err)) // 记录错误并停止程序
}
return &WechatPayService{
config: c.Wxpay,
wechatClient: client,
notifyHandler: notifyHandler,
userAuthModel: userAuthModel,
}
}
// CreateWechatAppOrder 创建微信APP支付订单
func (w *WechatPayService) CreateWechatAppOrder(ctx context.Context, amount float64, description string, outTradeNo string) (string, error) {
totalAmount := lzUtils.ToWechatAmount(amount)
// 构建支付请求参数
payRequest := app.PrepayRequest{
Appid: core.String(w.config.AppID),
Mchid: core.String(w.config.MchID),
Description: core.String(description),
OutTradeNo: core.String(outTradeNo),
NotifyUrl: core.String(w.config.NotifyUrl),
Amount: &app.Amount{
Total: core.Int64(totalAmount),
},
}
// 初始化 AppApiService
svc := app.AppApiService{Client: w.wechatClient}
// 发起预支付请求
resp, result, err := svc.Prepay(ctx, payRequest)
if err != nil {
return "", fmt.Errorf("微信支付订单创建失败: %v, 状态码: %d", err, result.Response.StatusCode)
}
// 返回预支付交易会话标识
return *resp.PrepayId, nil
}
// CreateWechatMiniProgramOrder 创建微信小程序支付订单
func (w *WechatPayService) CreateWechatMiniProgramOrder(ctx context.Context, amount float64, description string, outTradeNo string, openid string) (interface{}, error) {
totalAmount := lzUtils.ToWechatAmount(amount)
// 构建支付请求参数
payRequest := jsapi.PrepayRequest{
Appid: core.String(w.config.AppID),
Mchid: core.String(w.config.MchID),
Description: core.String(description),
OutTradeNo: core.String(outTradeNo),
NotifyUrl: core.String(w.config.NotifyUrl),
Amount: &jsapi.Amount{
Total: core.Int64(totalAmount),
},
Payer: &jsapi.Payer{
Openid: core.String(openid), // 用户的 OpenID通过前端传入
}}
// 初始化 AppApiService
svc := jsapi.JsapiApiService{Client: w.wechatClient}
// 发起预支付请求
resp, result, err := svc.PrepayWithRequestPayment(ctx, payRequest)
if err != nil {
return "", fmt.Errorf("微信支付订单创建失败: %v, 状态码: %d", err, result.Response.StatusCode)
}
// 返回预支付交易会话标识
return resp, nil
}
// CreateWechatOrder 创建微信支付订单(集成 APP、H5、小程序
func (w *WechatPayService) CreateWechatOrder(ctx context.Context, amount float64, description string, outTradeNo string) (interface{}, error) {
// 根据 ctx 中的 platform 判断平台
platform := ctx.Value("platform").(string)
var prepayData interface{}
var err error
switch platform {
case "mp-weixin":
userID, getUidErr := ctxdata.GetUidFromCtx(ctx)
if getUidErr != nil {
return "", fmt.Errorf("获取用户信息失败: %s", getUidErr)
}
userAuthModel, findUserAuthErr := w.userAuthModel.FindOneByUserIdAuthType(ctx, userID, "wx_mini")
if findUserAuthErr != nil {
return "", fmt.Errorf("获取用户认证信息失败: %s", findUserAuthErr)
}
// 如果是小程序平台,调用小程序支付订单创建
prepayData, err = w.CreateWechatMiniProgramOrder(ctx, amount, description, outTradeNo, userAuthModel.AuthKey)
case "app":
// 如果是 APP 平台,调用 APP 支付订单创建
prepayData, err = w.CreateWechatAppOrder(ctx, amount, description, outTradeNo)
default:
return "", fmt.Errorf("不支持的支付平台: %s", platform)
}
// 如果创建支付订单失败,返回错误
if err != nil {
return "", fmt.Errorf("支付订单创建失败: %v", err)
}
// 返回预支付ID
return prepayData, nil
}
// HandleWechatPayNotification 处理微信支付回调
func (w *WechatPayService) HandleWechatPayNotification(ctx context.Context, req *http.Request) (*payments.Transaction, error) {
transaction := new(payments.Transaction)
_, err := w.notifyHandler.ParseNotifyRequest(ctx, req, transaction)
if err != nil {
return nil, fmt.Errorf("微信支付通知处理失败: %v", err)
}
// 返回交易信息
return transaction, nil
}
// HandleRefundNotification 处理微信退款回调
func (w *WechatPayService) HandleRefundNotification(ctx context.Context, req *http.Request) (*refunddomestic.Refund, error) {
refund := new(refunddomestic.Refund)
_, err := w.notifyHandler.ParseNotifyRequest(ctx, req, refund)
if err != nil {
return nil, fmt.Errorf("微信退款回调通知处理失败: %v", err)
}
return refund, nil
}
// QueryOrderStatus 主动查询订单状态
func (w *WechatPayService) QueryOrderStatus(ctx context.Context, transactionID string) (*payments.Transaction, error) {
svc := jsapi.JsapiApiService{Client: w.wechatClient}
// 调用 QueryOrderById 方法查询订单状态
resp, result, err := svc.QueryOrderById(ctx, jsapi.QueryOrderByIdRequest{
TransactionId: core.String(transactionID),
Mchid: core.String(w.config.MchID),
})
if err != nil {
return nil, fmt.Errorf("订单查询失败: %v, 状态码: %d", err, result.Response.StatusCode)
}
return resp, nil
}
// WeChatRefund 申请微信退款
func (w *WechatPayService) WeChatRefund(ctx context.Context, outTradeNo string, refundAmount float64, totalAmount float64) error {
// 生成唯一的退款单号
outRefundNo := fmt.Sprintf("%s-refund", outTradeNo)
// 初始化退款服务
svc := refunddomestic.RefundsApiService{Client: w.wechatClient}
// 创建退款请求
resp, result, err := svc.Create(ctx, refunddomestic.CreateRequest{
OutTradeNo: core.String(outTradeNo),
OutRefundNo: core.String(outRefundNo),
NotifyUrl: core.String(w.config.RefundNotifyUrl),
Amount: &refunddomestic.AmountReq{
Currency: core.String("CNY"),
Refund: core.Int64(lzUtils.ToWechatAmount(refundAmount)),
Total: core.Int64(lzUtils.ToWechatAmount(totalAmount)),
},
})
if err != nil {
return fmt.Errorf("微信订单申请退款错误: %v", err)
}
// 打印退款结果
logx.Infof("退款申请成功,状态码=%d退款单号=%s微信退款单号=%s", result.Response.StatusCode, *resp.OutRefundNo, *resp.RefundId)
return nil
}
// GenerateOutTradeNo 生成唯一订单号
func (w *WechatPayService) GenerateOutTradeNo() string {
length := 16
timestamp := time.Now().UnixNano()
timeStr := strconv.FormatInt(timestamp, 10)
randomPart := strconv.Itoa(int(timestamp % 1e6))
combined := timeStr + randomPart
if len(combined) >= length {
return combined[:length]
}
for len(combined) < length {
combined += strconv.Itoa(int(timestamp % 10))
}
return combined
}

View File

@@ -0,0 +1,199 @@
package service
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strconv"
"time"
"tyc-server/app/main/api/internal/config"
"tyc-server/pkg/lzkit/crypto"
"github.com/pkg/errors"
)
type WestResp struct {
Message string `json:"message"`
Code string `json:"code"`
Data string `json:"data"`
ID string `json:"id"`
ErrorCode *int `json:"error_code"`
Reason string `json:"reason"`
}
type G05HZ01WestResp struct {
Message string `json:"message"`
Code string `json:"code"`
Data json.RawMessage `json:"data"`
ID string `json:"id"`
ErrorCode *int `json:"error_code"`
Reason string `json:"reason"`
}
type WestDexService struct {
config config.WestConfig
}
// NewWestDexService 是一个构造函数,用于初始化 WestDexService
func NewWestDexService(c config.Config) *WestDexService {
return &WestDexService{
config: c.WestConfig,
}
}
// CallAPI 调用西部数据的 API
func (w *WestDexService) CallAPI(code string, reqData map[string]interface{}) (resp []byte, err error) {
// 生成当前的13位时间戳
timestamp := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)
// 构造请求URL
reqUrl := fmt.Sprintf("%s/%s/%s?timestamp=%s", w.config.Url, w.config.SecretId, code, timestamp)
jsonData, marshalErr := json.Marshal(reqData)
if marshalErr != nil {
return nil, marshalErr
}
// 创建HTTP POST请求
req, newRequestErr := http.NewRequest("POST", reqUrl, bytes.NewBuffer(jsonData))
if newRequestErr != nil {
return nil, newRequestErr
}
// 设置请求头
req.Header.Set("Content-Type", "application/json")
// 发送请求
client := &http.Client{}
httpResp, clientDoErr := client.Do(req)
if clientDoErr != nil {
return nil, clientDoErr
}
defer func(Body io.ReadCloser) {
closeErr := Body.Close()
if closeErr != nil {
}
}(httpResp.Body)
// 检查请求是否成功
if httpResp.StatusCode == 200 {
// 读取响应体
bodyBytes, ReadErr := io.ReadAll(httpResp.Body)
if ReadErr != nil {
return nil, ReadErr
}
// 手动调用 json.Unmarshal 触发自定义的 UnmarshalJSON 方法
var westDexResp WestResp
UnmarshalErr := json.Unmarshal(bodyBytes, &westDexResp)
if UnmarshalErr != nil {
return nil, UnmarshalErr
}
if westDexResp.Code != "00000" && westDexResp.Code != "0" {
if westDexResp.Data == "" {
return nil, errors.New(westDexResp.Message)
}
decryptedData, DecryptErr := crypto.WestDexDecrypt(westDexResp.Data, w.config.Key)
if DecryptErr != nil {
return nil, DecryptErr
}
return decryptedData, errors.New(westDexResp.Message)
}
if westDexResp.Data == "" {
return nil, errors.New(westDexResp.Message)
}
// 解密响应数据
decryptedData, DecryptErr := crypto.WestDexDecrypt(westDexResp.Data, w.config.Key)
if DecryptErr != nil {
return nil, DecryptErr
}
// 输出解密后的数据
log.Println(string(decryptedData))
return decryptedData, nil
}
return nil, fmt.Errorf("西部请求失败Code: %d", httpResp.StatusCode)
}
// CallAPI 调用西部数据的 API
func (w *WestDexService) G05HZ01CallAPI(code string, reqData map[string]interface{}) (resp []byte, err error) {
// 生成当前的13位时间戳
timestamp := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)
// 构造请求URL
reqUrl := fmt.Sprintf("%s/%s/%s?timestamp=%s", w.config.Url, w.config.SecretSecondId, code, timestamp)
jsonData, marshalErr := json.Marshal(reqData)
if marshalErr != nil {
return nil, marshalErr
}
// 创建HTTP POST请求
req, newRequestErr := http.NewRequest("POST", reqUrl, bytes.NewBuffer(jsonData))
if newRequestErr != nil {
return nil, newRequestErr
}
// 设置请求头
req.Header.Set("Content-Type", "application/json")
// 发送请求
client := &http.Client{}
httpResp, clientDoErr := client.Do(req)
if clientDoErr != nil {
return nil, clientDoErr
}
defer func(Body io.ReadCloser) {
closeErr := Body.Close()
if closeErr != nil {
}
}(httpResp.Body)
// 检查请求是否成功
if httpResp.StatusCode == 200 {
// 读取响应体
bodyBytes, ReadErr := io.ReadAll(httpResp.Body)
if ReadErr != nil {
return nil, ReadErr
}
// 手动调用 json.Unmarshal 触发自定义的 UnmarshalJSON 方法
var westDexResp G05HZ01WestResp
UnmarshalErr := json.Unmarshal(bodyBytes, &westDexResp)
if UnmarshalErr != nil {
return nil, UnmarshalErr
}
if westDexResp.Code != "0000" {
if westDexResp.Data == nil {
return nil, errors.New(westDexResp.Message)
} else {
return westDexResp.Data, errors.New(string(westDexResp.Data))
}
}
if westDexResp.Data == nil {
return nil, errors.New(westDexResp.Message)
}
return westDexResp.Data, nil
}
return nil, fmt.Errorf("西部请求失败Code: %d", httpResp.StatusCode)
}
func (w *WestDexService) Encrypt(data string) string {
encryptedValue, err := crypto.WestDexEncrypt(data, w.config.Key)
if err != nil {
panic("WestDexEncrypt error: " + err.Error())
}
return encryptedValue
}
// GetDateRange 返回今天到明天的日期范围,格式为 "yyyyMMdd-yyyyMMdd"
func (w *WestDexService) GetDateRange() string {
today := time.Now().Format("20060102") // 获取今天的日期
tomorrow := time.Now().Add(24 * time.Hour).Format("20060102") // 获取明天的日期
return fmt.Sprintf("%s-%s", today, tomorrow) // 拼接日期范围并返回
}

View File

@@ -0,0 +1,193 @@
package service
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"tyc-server/app/main/api/internal/config"
"github.com/tidwall/gjson"
)
type YushanService struct {
config config.YushanConfig
}
func NewYushanService(c config.Config) *YushanService {
return &YushanService{
config: c.YushanConfig,
}
}
var (
// ErrEmptyResult 表示查询结果为空
ErrEmptyResult = errors.New("查询结果为空")
)
func (y *YushanService) request(prodID string, params map[string]interface{}) ([]byte, error) {
// 获取当前时间戳
unixMilliseconds := time.Now().UnixNano() / int64(time.Millisecond)
// 生成请求序列号
requestSN, _ := y.GenerateRandomString()
// 构建请求数据
reqData := map[string]interface{}{
"prod_id": prodID,
"req_time": unixMilliseconds,
"request_sn": requestSN,
"req_data": params,
}
// 将请求数据转换为 JSON 字节数组
messageBytes, err := json.Marshal(reqData)
if err != nil {
return nil, err
}
// 获取 API 密钥
key, err := hex.DecodeString(y.config.ApiKey)
if err != nil {
return nil, err
}
// 使用 AES CBC 加密请求数据
cipherText := y.AES_CBC_Encrypt(messageBytes, key)
// 将加密后的数据编码为 Base64 字符串
content := base64.StdEncoding.EncodeToString(cipherText)
// 发起 HTTP 请求
client := &http.Client{}
req, err := http.NewRequest("POST", y.config.Url, strings.NewReader(content))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("ACCT_ID", y.config.AcctID)
// 执行请求
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// 读取响应体
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var respData []byte
if IsJSON(string(body)) {
respData = body
} else {
sDec, err := base64.StdEncoding.DecodeString(string(body))
if err != nil {
return nil, err
}
respData = y.AES_CBC_Decrypt(sDec, key)
}
retCode := gjson.GetBytes(respData, "retcode").String()
if retCode == "100000" {
// retcode 为 100000表示查询为空
return json.RawMessage("{}"), ErrEmptyResult
} else if retCode == "000000" || retCode == "000001" || retCode == "000002" || retCode == "000003" {
// retcode 为 000000表示有数据返回 retdata
retData := gjson.GetBytes(respData, "retdata")
if !retData.Exists() {
return respData, fmt.Errorf("羽山请求retdata为空: %s", string(respData))
}
return []byte(retData.Raw), nil
} else {
return respData, fmt.Errorf("羽山请求未知的状态码: %s", string(respData))
}
}
// 判断字符串是否为 JSON 格式
func IsJSON(s string) bool {
var js interface{}
return json.Unmarshal([]byte(s), &js) == nil
}
// GenerateRandomString 生成一个32位的随机字符串订单号
func (y *YushanService) GenerateRandomString() (string, error) {
// 创建一个16字节的数组
bytes := make([]byte, 16)
// 读取随机字节到数组中
if _, err := rand.Read(bytes); err != nil {
return "", err
}
// 将字节数组编码为16进制字符串
return hex.EncodeToString(bytes), nil
}
// AEC加密CBC模式
func (y *YushanService) AES_CBC_Encrypt(plainText []byte, key []byte) []byte {
//指定加密算法返回一个AES算法的Block接口对象
block, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
//进行填充
plainText = Padding(plainText, block.BlockSize())
//指定初始向量vi,长度和block的块尺寸一致
iv := []byte("0000000000000000")
//指定分组模式返回一个BlockMode接口对象
blockMode := cipher.NewCBCEncrypter(block, iv)
//加密连续数据库
cipherText := make([]byte, len(plainText))
blockMode.CryptBlocks(cipherText, plainText)
//返回base64密文
return cipherText
}
// AEC解密CBC模式
func (y *YushanService) AES_CBC_Decrypt(cipherText []byte, key []byte) []byte {
//指定解密算法返回一个AES算法的Block接口对象
block, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
//指定初始化向量IV,和加密的一致
iv := []byte("0000000000000000")
//指定分组模式返回一个BlockMode接口对象
blockMode := cipher.NewCBCDecrypter(block, iv)
//解密
plainText := make([]byte, len(cipherText))
blockMode.CryptBlocks(plainText, cipherText)
//删除填充
plainText = UnPadding(plainText)
return plainText
} // 对明文进行填充
func Padding(plainText []byte, blockSize int) []byte {
//计算要填充的长度
n := blockSize - len(plainText)%blockSize
//对原来的明文填充n个n
temp := bytes.Repeat([]byte{byte(n)}, n)
plainText = append(plainText, temp...)
return plainText
}
// 对密文删除填充
func UnPadding(cipherText []byte) []byte {
//取出密文最后一个字节end
end := cipherText[len(cipherText)-1]
//删除填充
cipherText = cipherText[:len(cipherText)-int(end)]
return cipherText
}