Files
order/service/payment.go
2025-12-10 13:51:09 +08:00

348 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"context"
"errors"
"fmt"
"math/rand"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"order/consts"
"order/dao"
"order/model/dto"
"order/model/entity"
)
type payment struct{}
// Payment 支付服务
var Payment = new(payment)
// PayOrder 支付订单
func (s *payment) PayOrder(ctx context.Context, req *dto.PayOrderReq) (*dto.PayOrderResp, error) {
// 1. 参数验证
if req.TenantID == "" || req.OrderNo == "" || req.PayMethod == "" || req.PayType == "" {
return nil, errors.New("必填参数不能为空")
}
// 2. 查询订单
order, status, err := dao.Order.GetOrderByNo(ctx, req.TenantID, req.OrderNo)
if err != nil {
return nil, fmt.Errorf("获取订单失败: %w", err)
}
if order == nil {
return nil, errors.New("订单不存在")
}
// 3. 验证订单状态(只有待支付订单可以支付)
if status != consts.OrderStatusPending {
return nil, fmt.Errorf("订单状态不正确,当前状态: %s", status)
}
pendingOrder, ok := order.(*entity.OrderPending)
if !ok {
return nil, errors.New("订单类型错误")
}
// 4. 获取支付配置
paymentConfig, err := dao.PaymentConfig.GetByTenantAndMethod(ctx, req.TenantID, req.PayMethod)
if err != nil {
return nil, fmt.Errorf("获取支付配置失败: %w", err)
}
if paymentConfig == nil {
return nil, errors.New("支付配置不存在")
}
// 5. 调用第三方支付接口
payResp, err := s.callThirdPartyPayment(ctx, paymentConfig, req, pendingOrder)
if err != nil {
return nil, fmt.Errorf("调用支付接口失败: %w", err)
}
// 6. 创建支付记录
paymentRecord := &entity.PaymentRecord{
TenantID: req.TenantID,
OrderID: pendingOrder.ID,
OrderNo: req.OrderNo,
PayMethod: req.PayMethod,
PayType: req.PayType,
Amount: pendingOrder.PayAmount,
OutTradeNo: payResp.OutTradeNo,
Status: "pending",
}
if err := dao.PaymentRecord.Create(ctx, paymentRecord); err != nil {
return nil, fmt.Errorf("创建支付记录失败: %w", err)
}
// 7. 更新订单支付信息
payInfo := entity.PayInfo{
OutTradeNo: payResp.OutTradeNo,
QRCode: payResp.QRCode,
PayURL: payResp.PayURL,
PrepayID: payResp.PrepayID,
JSAPIParams: payResp.JSAPIParams,
APPParams: payResp.APPParams,
}
if err := dao.Order.UpdatePayInfo(ctx, req.TenantID, req.OrderNo, payInfo); err != nil {
return nil, fmt.Errorf("更新订单支付信息失败: %w", err)
}
// 8. 返回支付信息
resp := &dto.PayOrderResp{
OrderNo: req.OrderNo,
QRCode: payResp.QRCode,
PayURL: payResp.PayURL,
PrepayID: payResp.PrepayID,
JSAPIParams: payResp.JSAPIParams,
APPParams: payResp.APPParams,
}
return resp, nil
}
// callThirdPartyPayment 调用第三方支付接口
func (s *payment) callThirdPartyPayment(ctx context.Context, config *entity.PaymentConfig, req *dto.PayOrderReq, order *entity.OrderPending) (*PaymentResponse, error) {
// 这里应该是实际的第三方支付接口调用
// 为了演示,我们返回模拟数据
outTradeNo := s.generateOutTradeNo(req.TenantID)
payResp := &PaymentResponse{
OutTradeNo: outTradeNo,
}
// 根据支付类型返回不同的支付参数
switch req.PayType {
case "native":
// 扫码支付
payResp.QRCode = fmt.Sprintf("https://api.example.com/qrcode/%s", outTradeNo)
case "jsapi":
// JSAPI支付
payResp.PrepayID = fmt.Sprintf("wxprepay_%s", outTradeNo)
payResp.JSAPIParams = fmt.Sprintf(`{"appId":"%s","timeStamp":"%d","nonceStr":"%s","package":"prepay_id=%s","signType":"MD5","paySign":"signature"}`,
config.AppID, time.Now().Unix(), s.generateNonceStr(), payResp.PrepayID)
case "app":
// APP支付
payResp.APPParams = fmt.Sprintf(`{"appid":"%s","partnerid":"%s","prepayid":"%s","package":"Sign=WXPay","noncestr":"%s","timestamp":"%d","sign":"signature"}`,
config.AppID, config.MchID, payResp.PrepayID, s.generateNonceStr(), time.Now().Unix())
case "h5":
// H5支付
payResp.PayURL = fmt.Sprintf("https://api.example.com/h5pay/%s", outTradeNo)
}
return payResp, nil
}
// PaymentResponse 支付响应
type PaymentResponse struct {
OutTradeNo string `json:"out_trade_no"` // 商户订单号
QRCode string `json:"qrcode"` // 支付二维码
PayURL string `json:"pay_url"` // 支付链接
PrepayID string `json:"prepay_id"` // 预支付ID
JSAPIParams string `json:"jsapi_params"` // JSAPI参数
APPParams string `json:"app_params"` // APP参数
}
// generateOutTradeNo 生成商户订单号
func (s *payment) generateOutTradeNo(tenantID string) string {
timestamp := time.Now().Format("20060102150405")
random := rand.Intn(10000)
return fmt.Sprintf("%s%s%04d", tenantID, timestamp, random)
}
// generateNonceStr 生成随机字符串
func (s *payment) generateNonceStr() string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, 16)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
return string(b)
}
// HandlePaymentNotify 处理支付回调
func (s *payment) HandlePaymentNotify(ctx context.Context, req *dto.PaymentNotifyReq) error {
// 1. 验证回调签名
if !s.verifyNotifySignature(req) {
return errors.New("签名验证失败")
}
// 2. 查询支付记录
paymentRecord, err := dao.PaymentRecord.GetByOrderNo(ctx, req.TenantID, req.OrderNo)
if err != nil {
return fmt.Errorf("查询支付记录失败: %w", err)
}
if paymentRecord == nil {
return errors.New("支付记录不存在")
}
// 3. 更新支付记录状态
if err := dao.PaymentRecord.UpdateStatus(ctx, paymentRecord.ID.Hex(), req.Status, req.TransactionID, req.TradeNo); err != nil {
return fmt.Errorf("更新支付记录失败: %w", err)
}
// 4. 如果支付成功,更新订单状态
if req.Status == "success" {
updateData := bson.M{
"paid_at": time.Now(),
"transaction_id": req.TransactionID,
"trade_no": req.TradeNo,
"payment_channel": req.PayMethod,
}
if err := dao.Order.MoveOrderToStatus(ctx, consts.OrderStatusPending, consts.OrderStatusPaid, req.TenantID, req.OrderNo, updateData); err != nil {
return fmt.Errorf("更新订单状态失败: %w", err)
}
}
return nil
}
// verifyNotifySignature 验证回调签名
func (s *payment) verifyNotifySignature(req *dto.PaymentNotifyReq) bool {
// 这里应该是实际的签名验证逻辑
// 为了演示我们总是返回true
return true
}
// RefundOrder 退款
func (s *payment) RefundOrder(ctx context.Context, req *dto.RefundOrderReq) (*dto.RefundOrderResp, error) {
// 1. 参数验证
if req.TenantID == "" || req.OrderNo == "" || req.RefundAmount <= 0 {
return nil, errors.New("必填参数不能为空")
}
// 2. 查询订单
order, status, err := dao.Order.GetOrderByNo(ctx, req.TenantID, req.OrderNo)
if err != nil {
return nil, fmt.Errorf("获取订单失败: %w", err)
}
if order == nil {
return nil, errors.New("订单不存在")
}
// 3. 验证订单状态(只有已支付订单可以退款)
if status != consts.OrderStatusPaid {
return nil, fmt.Errorf("订单状态不正确,当前状态: %s", status)
}
paidOrder, ok := order.(*entity.OrderPaid)
if !ok {
return nil, errors.New("订单类型错误")
}
// 4. 验证退款金额
if req.RefundAmount > paidOrder.PayAmount {
return nil, errors.New("退款金额不能超过支付金额")
}
// 5. 调用第三方退款接口
refundResp, err := s.callThirdPartyRefund(ctx, req, paidOrder)
if err != nil {
return nil, fmt.Errorf("调用退款接口失败: %w", err)
}
// 6. 创建退款记录
refundRecord := &entity.RefundRecord{
TenantID: req.TenantID,
OrderID: paidOrder.ID,
OrderNo: req.OrderNo,
RefundNo: refundResp.RefundNo,
RefundAmount: req.RefundAmount,
Reason: req.Reason,
Status: "pending",
}
if err := dao.RefundRecord.Create(ctx, refundRecord); err != nil {
return nil, fmt.Errorf("创建退款记录失败: %w", err)
}
// 7. 如果是全额退款,更新订单状态
if req.RefundAmount == paidOrder.PayAmount {
updateData := bson.M{
"refund_reason": req.Reason,
}
if err := dao.Order.MoveOrderToStatus(ctx, consts.OrderStatusPaid, consts.OrderStatusRefunded, req.TenantID, req.OrderNo, updateData); err != nil {
return nil, fmt.Errorf("更新订单状态失败: %w", err)
}
}
// 8. 返回退款结果
resp := &dto.RefundOrderResp{
RefundNo: refundResp.RefundNo,
RefundID: refundResp.RefundID,
RefundAmount: req.RefundAmount,
}
return resp, nil
}
// callThirdPartyRefund 调用第三方退款接口
func (s *payment) callThirdPartyRefund(ctx context.Context, req *dto.RefundOrderReq, order *entity.OrderPaid) (*RefundResponse, error) {
// 这里应该是实际的第三方退款接口调用
// 为了演示,我们返回模拟数据
refundNo := s.generateRefundNo(req.TenantID)
refundID := fmt.Sprintf("refund_%s", refundNo)
return &RefundResponse{
RefundNo: refundNo,
RefundID: refundID,
}, nil
}
// RefundResponse 退款响应
type RefundResponse struct {
RefundNo string `json:"refund_no"` // 退款单号
RefundID string `json:"refund_id"` // 退款ID
}
// generateRefundNo 生成退款单号
func (s *payment) generateRefundNo(tenantID string) string {
timestamp := time.Now().Format("20060102150405")
random := rand.Intn(10000)
return fmt.Sprintf("R%s%s%04d", tenantID, timestamp, random)
}
// HandleRefundNotify 处理退款回调
func (s *payment) HandleRefundNotify(ctx context.Context, req *dto.RefundNotifyReq) error {
// 1. 验证回调签名
if !s.verifyRefundNotifySignature(req) {
return errors.New("签名验证失败")
}
// 2. 查询退款记录
refundRecord, err := dao.RefundRecord.GetByRefundNo(ctx, req.TenantID, req.RefundNo)
if err != nil {
return fmt.Errorf("查询退款记录失败: %w", err)
}
if refundRecord == nil {
return errors.New("退款记录不存在")
}
// 3. 更新退款记录状态
if err := dao.RefundRecord.UpdateRefundStatus(ctx, refundRecord.ID.Hex(), req.Status, req.RefundID); err != nil {
return fmt.Errorf("更新退款记录失败: %w", err)
}
return nil
}
// verifyRefundNotifySignature 验证退款回调签名
func (s *payment) verifyRefundNotifySignature(req *dto.RefundNotifyReq) bool {
// 这里应该是实际的签名验证逻辑
// 为了演示我们总是返回true
return true
}