348 lines
10 KiB
Go
348 lines
10 KiB
Go
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
|
||
}
|