You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
475 lines
19 KiB
475 lines
19 KiB
package service
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
|
|
"laic-backend/cache"
|
|
"laic-backend/client"
|
|
"laic-backend/common"
|
|
"laic-backend/logger"
|
|
"laic-backend/model"
|
|
"laic-backend/tool"
|
|
"laic-backend/vo"
|
|
)
|
|
|
|
type PaymentService struct {
|
|
agpay *client.AGPayClient
|
|
notifyURL string
|
|
}
|
|
|
|
type trafficCreditAudit struct {
|
|
UserID int64
|
|
OrderID int64
|
|
LedgerID int64
|
|
AmountBytes int64
|
|
BalanceBefore int64
|
|
BalanceAfter int64
|
|
}
|
|
|
|
var DefaultPaymentService = &PaymentService{}
|
|
|
|
func InitAGPay(conf common.AGPay) error {
|
|
if conf.BaseURL == "" && conf.NotifyURL == "" {
|
|
return nil
|
|
}
|
|
instance, err := client.NewAGPayClient(conf.BaseURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if conf.NotifyURL == "" {
|
|
return errors.New("AGPay notify URL is required")
|
|
}
|
|
DefaultPaymentService.agpay = instance
|
|
DefaultPaymentService.notifyURL = conf.NotifyURL
|
|
return nil
|
|
}
|
|
|
|
func (s *PaymentService) CreateWechatPayment(userID int64, req *vo.PaymentCreateReq) (*vo.PaymentCreateVO, *common.BusiError) {
|
|
if s.agpay == nil {
|
|
return nil, common.ErrPaymentDisabled
|
|
}
|
|
|
|
transaction, created, busiErr := s.getOrCreateTransaction(userID, req.BusinessType, req.BusinessOrderID)
|
|
if busiErr != nil {
|
|
return nil, busiErr
|
|
}
|
|
if transaction.Status == "paid" {
|
|
DefaultOperationLogService.RecordEvent(userID, "充值与履约", "创建支付交易", fmt.Sprintf("transactionId=%d businessType=%s businessOrderId=%d status=paid", transaction.ID, req.BusinessType, req.BusinessOrderID), "failed", "user_api")
|
|
return nil, common.ErrPaymentAlreadyPaid
|
|
}
|
|
if !created {
|
|
if transaction.Status == "cancelled" || transaction.Status == "closed" || transaction.Status == "failed" {
|
|
DefaultOperationLogService.RecordEvent(userID, "充值与履约", "复用支付交易", fmt.Sprintf("transactionId=%d businessType=%s businessOrderId=%d status=%s", transaction.ID, transaction.BusinessType, transaction.BusinessOrderID, transaction.Status), "failed", "user_api")
|
|
return nil, common.ErrPaymentOrderConflict
|
|
}
|
|
if isUsableProviderPayload(transaction.ProviderPayload) {
|
|
DefaultOperationLogService.RecordEvent(userID, "充值与履约", "复用支付交易", fmt.Sprintf("transactionId=%d businessType=%s businessOrderId=%d status=%s", transaction.ID, transaction.BusinessType, transaction.BusinessOrderID, transaction.Status), "success", "user_api")
|
|
return paymentCreateVO(transaction, transaction.ProviderPayload), nil
|
|
}
|
|
// Older versions stored raw PNG bytes as UTF-8. The stale payload is
|
|
// replaced below by requesting a fresh QR code for this transaction.
|
|
}
|
|
|
|
payload, err := s.agpay.CreateWechatQRCode(client.AGPayCreateRequest{
|
|
TotalFeeFen: transaction.TotalFeeFen, MerchantOrder: transaction.MerchantOrderNo, NotifyURL: s.notifyURL,
|
|
})
|
|
if err != nil {
|
|
logger.WARN("创建 AGPay 微信支付请求失败,交易保留待确认", transaction.ID, err)
|
|
return nil, common.ErrPaymentOrderConflict
|
|
}
|
|
payloadText := encodeProviderPayload(payload)
|
|
if !isUsableProviderPayload(payloadText) {
|
|
logger.WARN("AGPay 返回了无法展示的支付二维码", transaction.ID)
|
|
return nil, common.ErrPaymentOrderConflict
|
|
}
|
|
if err := common.DB.Model(&model.PaymentTransaction{}).Where("id = ? AND status = ?", transaction.ID, "processing").Updates(map[string]any{
|
|
"provider_payload": payloadText, "requested_at": time.Now(), "updated_at": time.Now(),
|
|
}).Error; err != nil {
|
|
logger.ERROR("保存 AGPay 支付响应失败", err)
|
|
return nil, common.ErrInternal
|
|
}
|
|
transaction.ProviderPayload = payloadText
|
|
DefaultOperationLogService.RecordEvent(userID, "充值与履约", "创建支付交易", fmt.Sprintf("transactionId=%d businessType=%s businessOrderId=%d feeFen=%d", transaction.ID, transaction.BusinessType, transaction.BusinessOrderID, transaction.TotalFeeFen), "success", "user_api")
|
|
|
|
return paymentCreateVO(transaction, payloadText), nil
|
|
}
|
|
|
|
func (s *PaymentService) GetTransaction(userID, transactionID int64) (*vo.PaymentTransactionVO, *common.BusiError) {
|
|
var transaction model.PaymentTransaction
|
|
if err := common.DB.Where("id = ? AND user_id = ?", transactionID, userID).First(&transaction).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, common.ErrPaymentTransactionNotFound
|
|
}
|
|
return nil, common.ErrInternal
|
|
}
|
|
return paymentTransactionVO(&transaction), nil
|
|
}
|
|
|
|
// encodeProviderPayload converts AGPay's binary QR image into a JSON-safe data
|
|
// URI. JSON responses remain unchanged for compatibility with other gateways.
|
|
func encodeProviderPayload(payload []byte) string {
|
|
if len(payload) == 0 {
|
|
return ""
|
|
}
|
|
if json.Valid(payload) {
|
|
return string(payload)
|
|
}
|
|
contentType := http.DetectContentType(payload)
|
|
return "data:" + contentType + ";base64," + base64.StdEncoding.EncodeToString(payload)
|
|
}
|
|
|
|
func isUsableProviderPayload(payload string) bool {
|
|
if payload == "" || strings.ContainsRune(payload, '\ufffd') {
|
|
return false
|
|
}
|
|
if json.Valid([]byte(payload)) {
|
|
return true
|
|
}
|
|
if !strings.HasPrefix(payload, "data:image/") {
|
|
return false
|
|
}
|
|
comma := strings.IndexByte(payload, ',')
|
|
if comma < 0 || !strings.HasSuffix(payload[:comma], ";base64") {
|
|
return false
|
|
}
|
|
decoded, err := base64.StdEncoding.DecodeString(payload[comma+1:])
|
|
return err == nil && len(decoded) > 0 && strings.HasPrefix(http.DetectContentType(decoded), "image/")
|
|
}
|
|
|
|
func (s *PaymentService) markPaymentRequestFailed(transactionID int64, businessType string, businessOrderID int64) {
|
|
if err := common.DB.Transaction(func(tx *gorm.DB) error {
|
|
if err := tx.Model(&model.PaymentTransaction{}).Where("id = ? AND status = ?", transactionID, "processing").Updates(map[string]any{"status": "failed", "updated_at": time.Now()}).Error; err != nil {
|
|
return err
|
|
}
|
|
switch businessType {
|
|
case "traffic_order":
|
|
return tx.Model(&model.TrafficOrder{}).Where("id = ? AND pay_status = ?", businessOrderID, "processing").Updates(map[string]any{"pay_status": "unpaid", "payment_provider": "", "updated_at": time.Now()}).Error
|
|
case "sim_recharge_order":
|
|
return tx.Model(&model.SimRechargeOrder{}).Where("id = ? AND payment_status = ?", businessOrderID, "processing").Updates(map[string]any{"payment_status": "unpaid", "updated_at": time.Now()}).Error
|
|
default:
|
|
return common.ErrParam
|
|
}
|
|
}); err != nil {
|
|
logger.ERROR("回滚支付请求状态失败", err)
|
|
}
|
|
}
|
|
|
|
func (s *PaymentService) getOrCreateTransaction(userID int64, businessType string, businessOrderID int64) (*model.PaymentTransaction, bool, *common.BusiError) {
|
|
var transaction model.PaymentTransaction
|
|
var created bool
|
|
var busiErr *common.BusiError
|
|
err := common.DB.Transaction(func(tx *gorm.DB) error {
|
|
amount, status, err := paymentBusinessOrder(tx, userID, businessType, businessOrderID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
existingResult := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
Where("user_id = ? AND business_type = ? AND business_order_id = ? AND provider = ?", userID, businessType, businessOrderID, "agpay").
|
|
Limit(1).Find(&transaction)
|
|
if existingResult.Error != nil {
|
|
return existingResult.Error
|
|
}
|
|
if existingResult.RowsAffected > 0 {
|
|
return nil
|
|
}
|
|
if amount <= 0 {
|
|
busiErr = common.ErrParam
|
|
return busiErr
|
|
}
|
|
if status == "paid" {
|
|
busiErr = common.ErrPaymentAlreadyPaid
|
|
return busiErr
|
|
}
|
|
if status != "unpaid" && status != "processing" {
|
|
busiErr = common.ErrPaymentOrderConflict
|
|
return busiErr
|
|
}
|
|
|
|
id, err := tool.NextID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
now := time.Now()
|
|
transaction = model.PaymentTransaction{
|
|
ID: id, UserID: userID, BusinessType: businessType, BusinessOrderID: businessOrderID,
|
|
Provider: "agpay", MerchantOrderNo: "agpay-" + strconv.FormatInt(id, 10),
|
|
TotalFeeFen: amount, Status: "processing", CreatedAt: now, UpdatedAt: now,
|
|
}
|
|
if err := tx.Create(&transaction).Error; err != nil {
|
|
return err
|
|
}
|
|
created = true
|
|
return updatePaymentBusinessStatus(tx, businessType, businessOrderID, "processing")
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, false, common.ErrOrderNotFound
|
|
}
|
|
if busiErr != nil {
|
|
return nil, false, busiErr
|
|
}
|
|
logger.ERROR("创建支付交易失败", err)
|
|
return nil, false, common.ErrInternal
|
|
}
|
|
return &transaction, created, nil
|
|
}
|
|
|
|
func paymentBusinessOrder(tx *gorm.DB, userID int64, businessType string, businessOrderID int64) (int64, string, error) {
|
|
switch businessType {
|
|
case "traffic_order":
|
|
var order model.TrafficOrder
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND user_id = ?", businessOrderID, userID).First(&order).Error; err != nil {
|
|
return 0, "", err
|
|
}
|
|
return order.TotalFeeFen, order.PayStatus, nil
|
|
case "sim_recharge_order":
|
|
var order model.SimRechargeOrder
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND user_id = ?", businessOrderID, userID).First(&order).Error; err != nil {
|
|
return 0, "", err
|
|
}
|
|
return order.TotalFeeFen, order.PaymentStatus, nil
|
|
default:
|
|
return 0, "", common.ErrParam
|
|
}
|
|
}
|
|
|
|
func updatePaymentBusinessStatus(tx *gorm.DB, businessType string, businessOrderID int64, status string) error {
|
|
var result *gorm.DB
|
|
switch businessType {
|
|
case "traffic_order":
|
|
result = tx.Model(&model.TrafficOrder{}).Where("id = ? AND pay_status = ?", businessOrderID, "unpaid").Updates(map[string]any{"pay_status": status, "payment_provider": "agpay", "updated_at": time.Now()})
|
|
case "sim_recharge_order":
|
|
result = tx.Model(&model.SimRechargeOrder{}).Where("id = ? AND payment_status = ?", businessOrderID, "unpaid").Updates(map[string]any{"payment_status": status, "updated_at": time.Now()})
|
|
default:
|
|
return common.ErrParam
|
|
}
|
|
if result.Error != nil {
|
|
return result.Error
|
|
}
|
|
if result.RowsAffected != 1 {
|
|
return common.ErrPaymentOrderConflict
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func paymentCreateVO(transaction *model.PaymentTransaction, payload string) *vo.PaymentCreateVO {
|
|
return &vo.PaymentCreateVO{
|
|
TransactionID: transaction.ID, BusinessType: transaction.BusinessType, BusinessOrderID: transaction.BusinessOrderID,
|
|
Provider: transaction.Provider, MerchantOrderNo: transaction.MerchantOrderNo, TotalFeeFen: transaction.TotalFeeFen,
|
|
Status: transaction.Status, ProviderPayload: payload, CallbackEnabled: false,
|
|
}
|
|
}
|
|
|
|
func paymentTransactionVO(transaction *model.PaymentTransaction) *vo.PaymentTransactionVO {
|
|
return &vo.PaymentTransactionVO{
|
|
ID: transaction.ID, BusinessType: transaction.BusinessType, BusinessOrderID: transaction.BusinessOrderID,
|
|
Provider: transaction.Provider, MerchantOrderNo: transaction.MerchantOrderNo, TotalFeeFen: transaction.TotalFeeFen,
|
|
Status: transaction.Status, CreatedAt: transaction.CreatedAt, PaidAt: transaction.PaidAt,
|
|
}
|
|
}
|
|
|
|
func (s *PaymentService) ConfirmAGPayCallback(tradeNo string, payload []byte) *common.BusiError {
|
|
if tradeNo == "" || len(tradeNo) > 128 {
|
|
return common.ErrParam
|
|
}
|
|
|
|
now := time.Now()
|
|
sum := sha256.Sum256(payload)
|
|
payloadHash := hex.EncodeToString(sum[:])
|
|
creditedUserID := int64(0)
|
|
var trafficCredit *trafficCreditAudit
|
|
var simOrderToSubmit *model.SimRechargeOrder
|
|
callbackAudit := ""
|
|
var callbackTransaction model.PaymentTransaction
|
|
var busiErr *common.BusiError
|
|
err := common.DB.Transaction(func(tx *gorm.DB) error {
|
|
var transaction model.PaymentTransaction
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("provider = ? AND merchant_order_no = ?", "agpay", tradeNo).First(&transaction).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
busiErr = common.ErrPaymentTransactionNotFound
|
|
return busiErr
|
|
}
|
|
return err
|
|
}
|
|
|
|
var event model.PaymentCallbackEvent
|
|
eventResult := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("provider = ? AND event_key = ?", "agpay", tradeNo).Limit(1).Find(&event)
|
|
if eventResult.Error != nil {
|
|
return eventResult.Error
|
|
}
|
|
if eventResult.RowsAffected > 0 {
|
|
callbackTransaction = transaction
|
|
if event.PayloadHash != payloadHash {
|
|
callbackAudit = "conflict"
|
|
busiErr = common.ErrPaymentOrderConflict
|
|
return busiErr
|
|
}
|
|
callbackAudit = "duplicate"
|
|
callbackTransaction = transaction
|
|
return nil
|
|
}
|
|
id, err := tool.NextID()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
event = model.PaymentCallbackEvent{ID: id, Provider: "agpay", EventKey: tradeNo, PayloadHash: payloadHash, Verified: true, CreatedAt: now}
|
|
if err := tx.Create(&event).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
callbackTransaction = transaction
|
|
if transaction.Status == "paid" {
|
|
callbackAudit = "duplicate"
|
|
return tx.Model(&event).Updates(map[string]any{"processed_at": now, "result_code": "duplicate"}).Error
|
|
}
|
|
if transaction.Status == "cancelled" || transaction.Status == "closed" {
|
|
callbackAudit = transaction.Status
|
|
return tx.Model(&event).Updates(map[string]any{"processed_at": now, "result_code": transaction.Status}).Error
|
|
}
|
|
if transaction.Status != "processing" {
|
|
callbackAudit = "conflict"
|
|
busiErr = common.ErrPaymentOrderConflict
|
|
return busiErr
|
|
}
|
|
result := tx.Model(&transaction).Where("id = ? AND status = ?", transaction.ID, "processing").Updates(map[string]any{
|
|
"status": "paid", "paid_at": now, "updated_at": now,
|
|
})
|
|
if result.Error != nil {
|
|
return result.Error
|
|
}
|
|
if result.RowsAffected != 1 {
|
|
callbackAudit = "conflict"
|
|
busiErr = common.ErrPaymentOrderConflict
|
|
return busiErr
|
|
}
|
|
switch transaction.BusinessType {
|
|
case "traffic_order":
|
|
trafficCredit, err = creditPaidTrafficOrder(tx, transaction.BusinessOrderID, transaction.UserID, now)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
creditedUserID = trafficCredit.UserID
|
|
case "sim_recharge_order":
|
|
var order model.SimRechargeOrder
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND user_id = ?", transaction.BusinessOrderID, transaction.UserID).First(&order).Error; err != nil {
|
|
return err
|
|
}
|
|
result := tx.Model(&model.SimRechargeOrder{}).Where("id = ? AND user_id = ? AND payment_status = ? AND fulfillment_status = ?", order.ID, transaction.UserID, "processing", "pending").Updates(map[string]any{
|
|
"payment_status": "paid", "paid_at": now, "fulfillment_status": "submitting", "attempt_count": 1,
|
|
"fulfillment_started_at": now, "fulfillment_message": "续费提交处理中;如状态未更新,请联系管理员。",
|
|
"fulfillment_lease_token": nil, "fulfillment_lease_until": nil, "next_attempt_at": nil,
|
|
"last_error_code": "", "last_error_message": "", "updated_at": now,
|
|
})
|
|
if result.Error != nil {
|
|
return result.Error
|
|
}
|
|
if result.RowsAffected != 1 {
|
|
callbackAudit = "conflict"
|
|
busiErr = common.ErrPaymentOrderConflict
|
|
return busiErr
|
|
}
|
|
order.PaymentStatus = "paid"
|
|
order.FulfillmentStatus = "submitting"
|
|
order.AttemptCount = 1
|
|
order.PaidAt = &now
|
|
order.FulfillmentStartedAt = &now
|
|
order.FulfillmentMessage = "续费提交处理中;如状态未更新,请联系管理员。"
|
|
simOrderToSubmit = &order
|
|
default:
|
|
callbackAudit = "conflict"
|
|
busiErr = common.ErrPaymentOrderConflict
|
|
return busiErr
|
|
}
|
|
callbackAudit = "paid"
|
|
return tx.Model(&event).Updates(map[string]any{"processed_at": now, "result_code": "paid"}).Error
|
|
})
|
|
if err != nil {
|
|
if callbackAudit == "conflict" && callbackTransaction.ID != 0 {
|
|
DefaultOperationLogService.RecordEvent(callbackTransaction.UserID, "充值与履约", "AGPay回调冲突", fmt.Sprintf("transactionId=%d businessType=%s businessOrderId=%d", callbackTransaction.ID, callbackTransaction.BusinessType, callbackTransaction.BusinessOrderID), "failed", "agpay_callback")
|
|
}
|
|
if busiErr != nil {
|
|
return busiErr
|
|
}
|
|
logger.ERROR("确认 AGPay 支付回调失败", err)
|
|
return common.ErrInternal
|
|
}
|
|
if callbackAudit != "" {
|
|
action := "确认AGPay支付回调"
|
|
if callbackAudit == "duplicate" {
|
|
action = "忽略重复AGPay回调"
|
|
} else if callbackAudit == "cancelled" || callbackAudit == "closed" {
|
|
action = "忽略迟到AGPay回调"
|
|
}
|
|
DefaultOperationLogService.RecordEvent(callbackTransaction.UserID, "充值与履约", action, fmt.Sprintf("transactionId=%d businessType=%s businessOrderId=%d result=%s", callbackTransaction.ID, callbackTransaction.BusinessType, callbackTransaction.BusinessOrderID, callbackAudit), "success", "agpay_callback")
|
|
}
|
|
if trafficCredit != nil {
|
|
DefaultOperationLogService.RecordEvent(trafficCredit.UserID, "充值与履约", "流量充值入账", fmt.Sprintf("orderId=%d ledgerId=%d amountBytes=%d balanceBefore=%d balanceAfter=%d", trafficCredit.OrderID, trafficCredit.LedgerID, trafficCredit.AmountBytes, trafficCredit.BalanceBefore, trafficCredit.BalanceAfter), "success", "agpay_callback")
|
|
}
|
|
if creditedUserID != 0 {
|
|
if err := common.Delete(cache.TrafficKeyOf(creditedUserID)); err != nil {
|
|
logger.WARN("删除支付入账余额缓存失败", err)
|
|
}
|
|
}
|
|
if simOrderToSubmit != nil {
|
|
DefaultOperationLogService.RecordEvent(simOrderToSubmit.UserID, "充值与履约", "启动SIM续费履约", fmt.Sprintf("orderId=%d attempt=1", simOrderToSubmit.ID), "success", "agpay_callback")
|
|
DefaultSimRechargeService.SubmitPaidOrderOnce(simOrderToSubmit)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func creditPaidTrafficOrder(tx *gorm.DB, orderID, expectedUserID int64, now time.Time) (*trafficCreditAudit, error) {
|
|
var order model.TrafficOrder
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ? AND user_id = ?", orderID, expectedUserID).First(&order).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
if order.PayStatus == "paid" && order.CreditStatus == "credited" {
|
|
return nil, nil
|
|
}
|
|
if order.PayStatus != "processing" || order.CreditStatus != "pending" || order.AmountBytes <= 0 {
|
|
return nil, common.ErrPaymentOrderConflict
|
|
}
|
|
var user model.User
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Select("id", "traffic_balance").First(&user, order.UserID).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
ledgerID := mustID()
|
|
if ledgerID == 0 {
|
|
return nil, errors.New("generate traffic credit ledger ID")
|
|
}
|
|
after := user.TrafficBalance + order.AmountBytes
|
|
if err := tx.Model(&user).UpdateColumn("traffic_balance", after).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
idempotencyKey := "payment:" + strconv.FormatInt(order.ID, 10)
|
|
if err := tx.Create(&model.TrafficLedger{
|
|
ID: ledgerID, AccountType: "user", AccountID: order.UserID, Direction: "credit", AmountBytes: order.AmountBytes,
|
|
BalanceBefore: user.TrafficBalance, BalanceAfter: after, SourceType: "traffic_order", SourceID: strconv.FormatInt(order.ID, 10),
|
|
IdempotencyKey: idempotencyKey, CreatedAt: now,
|
|
}).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
result := tx.Model(&model.TrafficOrder{}).Where("id = ? AND user_id = ? AND pay_status = ? AND credit_status = ?", order.ID, expectedUserID, "processing", "pending").Updates(map[string]any{
|
|
"pay_status": "paid", "payment_provider": "agpay",
|
|
"credit_status": "credited", "credit_ledger_id": ledgerID, "payment_idempotency_key": idempotencyKey,
|
|
"paid_at": now, "updated_at": now,
|
|
})
|
|
if result.Error != nil {
|
|
return nil, result.Error
|
|
}
|
|
if result.RowsAffected != 1 {
|
|
return nil, common.ErrPaymentOrderConflict
|
|
}
|
|
return &trafficCreditAudit{UserID: order.UserID, OrderID: order.ID, LedgerID: ledgerID, AmountBytes: order.AmountBytes, BalanceBefore: user.TrafficBalance, BalanceAfter: after}, nil
|
|
}
|
|
|