Skip to content

支付交易 - 防止重复扣款 ​

概述 ​

支付系统是幂等性设计最重要的应用场景之一。重复扣款会导致严重的资损和用户投诉,因此必须通过严格的幂等性控制来保证资金安全。

核心挑战 ​

  1. 网络超时:支付请求超时后重试可能导致重复扣款
  2. 用户误操作:用户多次点击支付按钮
  3. 第三方支付回调:微信/支付宝可能多次通知同一笔支付
  4. 分布式事务:扣款、更新订单、发送通知需要保持一致性

架构设计 ​

客户端                    API Gateway               支付服务                  数据库
  |                           |                         |                        |
  |-- 发起支付 ------------->|                         |                        |
  |   (Idempotency-Key)      |                         |                        |
  |                           |-- 检查幂等键 ---------->|                        |
  |                           |                         |-- 查询 Redis ----------|
  |                           |                         |<-- 返回结果 -----------|
  |                           |                         |                        |
  |                           |<-- 已处理,直接返回 ------|                        |
  |<-- 返回支付结果 -----------|                         |                        |
  |                           |                         |                        |
  |-- 重试(相同Key) --------->|                         |                        |
  |                           |-- 检查幂等键 ---------->|                        |
  |                           |                         |-- 查询 Redis ----------|
  |                           |                         |<-- 返回缓存结果 -------|
  |                           |<-- 返回缓存结果 ---------|                        |
  |<-- 返回相同结果 -----------|                         |                        |

完整实现 ​

1. 数据库表设计(PostgreSQL) ​

sql
-- 支付记录表
CREATE TABLE payments (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    order_id UUID NOT NULL REFERENCES orders(id),
    user_id UUID NOT NULL REFERENCES users(id),
    
    -- 支付信息
    amount DECIMAL(10, 2) NOT NULL,
    currency VARCHAR(3) NOT NULL DEFAULT 'CNY',
    payment_method VARCHAR(20) NOT NULL, -- alipay, wechat, stripe
    
    -- 第三方支付信息
    third_party_payment_id VARCHAR(100), -- 支付宝/微信的交易号
    third_party_response JSONB,
    
    -- 状态
    status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending, success, failed, refunded
    error_message TEXT,
    
    -- 幂等性控制
    idempotency_key VARCHAR(64) UNIQUE NOT NULL,
    idempotency_locked_at TIMESTAMP WITH TIME ZONE,
    
    -- 时间戳
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    paid_at TIMESTAMP WITH TIME ZONE,
    
    -- 乐观锁
    version INTEGER NOT NULL DEFAULT 0
);

-- 索引
CREATE INDEX idx_payments_order_id ON payments(order_id);
CREATE INDEX idx_payments_user_id ON payments(user_id);
CREATE INDEX idx_payments_status ON payments(status);
CREATE INDEX idx_payments_idempotency_key ON payments(idempotency_key);
CREATE INDEX idx_payments_third_party_id ON payments(third_party_payment_id);

-- 支付流水表(记录每次尝试)
CREATE TABLE payment_attempts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    payment_id UUID NOT NULL REFERENCES payments(id),
    
    -- 尝试信息
    attempt_number INTEGER NOT NULL,
    request_data JSONB,
    response_data JSONB,
    
    -- 结果
    status VARCHAR(20) NOT NULL, -- success, failed, timeout
    error_code VARCHAR(50),
    error_message TEXT,
    
    -- 时间
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

CREATE INDEX idx_payment_attempts_payment_id ON payment_attempts(payment_id);

2. C# 实体类 ​

csharp
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

public class Payment
{
    [Key]
    public Guid Id { get; set; }
    
    [Required]
    public Guid OrderId { get; set; }
    public Order Order { get; set; }
    
    [Required]
    public Guid UserId { get; set; }
    public User User { get; set; }
    
    [Required]
    [Column(TypeName = "decimal(10,2)")]
    public decimal Amount { get; set; }
    
    [Required]
    [MaxLength(3)]
    public string Currency { get; set; } = "CNY";
    
    [Required]
    [MaxLength(20)]
    public string PaymentMethod { get; set; }
    
    [MaxLength(100)]
    public string? ThirdPartyPaymentId { get; set; }
    
    public JsonDocument? ThirdPartyResponse { get; set; }
    
    [Required]
    [MaxLength(20)]
    public string Status { get; set; } = "pending";
    
    public string? ErrorMessage { get; set; }
    
    [Required]
    [MaxLength(64)]
    public string IdempotencyKey { get; set; }
    
    public DateTime? IdempotencyLockedAt { get; set; }
    
    public DateTime CreatedAt { get; set; }
    public DateTime UpdatedAt { get; set; }
    public DateTime? PaidAt { get; set; }
    
    [ConcurrencyCheck]
    public int Version { get; set; }
    
    // 导航属性
    public ICollection<PaymentAttempt> Attempts { get; set; } = new List<PaymentAttempt>();
}

public class PaymentAttempt
{
    [Key]
    public Guid Id { get; set; }
    
    [Required]
    public Guid PaymentId { get; set; }
    public Payment Payment { get; set; }
    
    [Required]
    public int AttemptNumber { get; set; }
    
    public JsonDocument? RequestData { get; set; }
    public JsonDocument? ResponseData { get; set; }
    
    [Required]
    [MaxLength(20)]
    public string Status { get; set; }
    
    [MaxLength(50)]
    public string? ErrorCode { get; set; }
    
    public string? ErrorMessage { get; set; }
    
    public DateTime CreatedAt { get; set; }
}

3. 支付服务实现 ​

csharp
public class PaymentService
{
    private readonly AppDbContext _dbContext;
    private readonly IIdempotencyTokenService _idempotencyService;
    private readonly IPaymentGateway _paymentGateway; // 第三方支付网关
    private readonly ILogger<PaymentService> _logger;
    
    public PaymentService(
        AppDbContext dbContext,
        IIdempotencyTokenService idempotencyService,
        IPaymentGateway paymentGateway,
        ILogger<PaymentService> logger)
    {
        _dbContext = dbContext;
        _idempotencyService = idempotencyService;
        _paymentGateway = paymentGateway;
        _logger = logger;
    }
    
    /// <summary>
    /// 创建支付(带幂等性保证)
    /// </summary>
    public async Task<Result<Payment>> CreatePaymentAsync(
        Guid orderId, 
        decimal amount, 
        string paymentMethod,
        string idempotencyKey)
    {
        // 验证幂等键
        if (string.IsNullOrWhiteSpace(idempotencyKey))
        {
            return Result<Payment>.Failure("Idempotency key is required");
        }
        
        await using var transaction = await _dbContext.Database.BeginTransactionAsync();
        
        try
        {
            // 检查是否已存在相同的幂等键
            var existingPayment = await _dbContext.Payments
                .FirstOrDefaultAsync(p => p.IdempotencyKey == idempotencyKey);
            
            if (existingPayment != null)
            {
                _logger.LogInformation("Payment already exists for key: {Key}", idempotencyKey);
                return Result<Payment>.Success(existingPayment);
            }
            
            // 验证订单
            var order = await _dbContext.Orders.FindAsync(orderId);
            if (order == null)
            {
                return Result<Payment>.Failure("Order not found");
            }
            
            if (order.Status != "pending")
            {
                return Result<Payment>.Failure("Order is not in pending status");
            }
            
            // 验证金额
            if (amount != order.TotalAmount)
            {
                return Result<Payment>.Failure("Payment amount does not match order total");
            }
            
            // 创建支付记录
            var payment = new Payment
            {
                Id = Guid.NewGuid(),
                OrderId = orderId,
                UserId = order.UserId,
                Amount = amount,
                Currency = "CNY",
                PaymentMethod = paymentMethod,
                Status = "pending",
                IdempotencyKey = idempotencyKey,
                CreatedAt = DateTime.UtcNow,
                UpdatedAt = DateTime.UtcNow,
                Version = 0
            };
            
            _dbContext.Payments.Add(payment);
            await _dbContext.SaveChangesAsync();
            
            await transaction.CommitAsync();
            
            _logger.LogInformation("Payment created: {PaymentId}, Key: {Key}", 
                payment.Id, idempotencyKey);
            
            return Result<Payment>.Success(payment);
        }
        catch (Exception ex)
        {
            await transaction.RollbackAsync();
            _logger.LogError(ex, "Failed to create payment for order: {OrderId}", orderId);
            return Result<Payment>.Failure("Failed to create payment");
        }
    }
    
    /// <summary>
    /// 处理支付(调用第三方支付网关)
    /// </summary>
    public async Task<Result<PaymentResult>> ProcessPaymentAsync(Guid paymentId)
    {
        await using var transaction = await _dbContext.Database.BeginTransactionAsync();
        
        try
        {
            // 获取支付记录(带锁)
            var payment = await _dbContext.Payments
                .FirstOrDefaultAsync(p => p.Id == paymentId);
            
            if (payment == null)
            {
                return Result<PaymentResult>.Failure("Payment not found");
            }
            
            // 检查状态
            if (payment.Status == "success")
            {
                _logger.LogWarning("Payment already completed: {PaymentId}", paymentId);
                return Result<PaymentResult>.Success(new PaymentResult
                {
                    Success = true,
                    PaymentId = payment.Id,
                    ThirdPartyPaymentId = payment.ThirdPartyPaymentId
                });
            }
            
            if (payment.Status != "pending")
            {
                return Result<PaymentResult>.Failure($"Payment is in {payment.Status} status");
            }
            
            // 更新状态为 processing
            payment.Status = "processing";
            payment.UpdatedAt = DateTime.UtcNow;
            await _dbContext.SaveChangesAsync();
            
            // 记录尝试
            var attempt = new PaymentAttempt
            {
                Id = Guid.NewGuid(),
                PaymentId = payment.Id,
                AttemptNumber = payment.Attempts.Count + 1,
                Status = "processing",
                CreatedAt = DateTime.UtcNow
            };
            
            _dbContext.PaymentAttempts.Add(attempt);
            await _dbContext.SaveChangesAsync();
            
            // 调用第三方支付网关
            PaymentGatewayResponse gatewayResponse;
            try
            {
                gatewayResponse = await _paymentGateway.ChargeAsync(new PaymentChargeRequest
                {
                    Amount = payment.Amount,
                    Currency = payment.Currency,
                    PaymentMethod = payment.PaymentMethod,
                    OrderId = payment.OrderId,
                    IdempotencyKey = payment.IdempotencyKey // 传递幂等键给第三方
                });
            }
            catch (Exception ex)
            {
                // 网关调用失败
                attempt.Status = "failed";
                attempt.ErrorMessage = ex.Message;
                await _dbContext.SaveChangesAsync();
                
                throw; // 抛出异常让上层处理重试
            }
            
            // 更新尝试记录
            attempt.Status = gatewayResponse.Success ? "success" : "failed";
            attempt.ResponseData = gatewayResponse.RawResponse;
            attempt.ErrorCode = gatewayResponse.ErrorCode;
            attempt.ErrorMessage = gatewayResponse.ErrorMessage;
            
            // 更新支付记录
            if (gatewayResponse.Success)
            {
                payment.Status = "success";
                payment.ThirdPartyPaymentId = gatewayResponse.TransactionId;
                payment.ThirdPartyResponse = gatewayResponse.RawResponse;
                payment.PaidAt = DateTime.UtcNow;
                
                // 更新订单状态
                var order = await _dbContext.Orders.FindAsync(payment.OrderId);
                if (order != null)
                {
                    order.Status = "paid";
                    order.PaidAt = DateTime.UtcNow;
                }
                
                _logger.LogInformation("Payment succeeded: {PaymentId}, Transaction: {TxnId}",
                    payment.Id, gatewayResponse.TransactionId);
            }
            else
            {
                payment.Status = "failed";
                payment.ErrorMessage = gatewayResponse.ErrorMessage;
                
                _logger.LogWarning("Payment failed: {PaymentId}, Error: {Error}",
                    payment.Id, gatewayResponse.ErrorMessage);
            }
            
            payment.UpdatedAt = DateTime.UtcNow;
            payment.Version++;
            
            await _dbContext.SaveChangesAsync();
            await transaction.CommitAsync();
            
            return Result<PaymentResult>.Success(new PaymentResult
            {
                Success = gatewayResponse.Success,
                PaymentId = payment.Id,
                ThirdPartyPaymentId = gatewayResponse.TransactionId,
                ErrorMessage = gatewayResponse.ErrorMessage
            });
        }
        catch (Exception ex)
        {
            await transaction.RollbackAsync();
            _logger.LogError(ex, "Failed to process payment: {PaymentId}", paymentId);
            return Result<PaymentResult>.Failure("Payment processing failed");
        }
    }
    
    /// <summary>
    /// 处理第三方支付回调( webhook )
    /// </summary>
    public async Task HandlePaymentCallbackAsync(PaymentCallback callback)
    {
        await using var transaction = await _dbContext.Database.BeginTransactionAsync();
        
        try
        {
            // 根据第三方交易 ID 查找支付记录
            var payment = await _dbContext.Payments
                .FirstOrDefaultAsync(p => p.ThirdPartyPaymentId == callback.TransactionId);
            
            if (payment == null)
            {
                // 尝试根据订单 ID 查找
                payment = await _dbContext.Payments
                    .FirstOrDefaultAsync(p => p.OrderId == callback.OrderId && p.Status == "pending");
                
                if (payment == null)
                {
                    _logger.LogWarning("Payment not found for callback: {TxnId}", callback.TransactionId);
                    return;
                }
            }
            
            // 幂等性检查:如果已经处理过,直接返回
            if (payment.Status == "success")
            {
                _logger.LogInformation("Payment callback already processed: {PaymentId}", payment.Id);
                return;
            }
            
            // 使用乐观锁更新
            payment.Status = callback.Success ? "success" : "failed";
            payment.ThirdPartyResponse = callback.RawData;
            payment.UpdatedAt = DateTime.UtcNow;
            payment.Version++;
            
            if (callback.Success)
            {
                payment.PaidAt = callback.PaidAt ?? DateTime.UtcNow;
                
                // 更新订单状态
                var order = await _dbContext.Orders.FindAsync(payment.OrderId);
                if (order != null)
                {
                    order.Status = "paid";
                    order.PaidAt = payment.PaidAt;
                }
            }
            
            await _dbContext.SaveChangesAsync();
            await transaction.CommitAsync();
            
            _logger.LogInformation("Payment callback processed: {PaymentId}, Status: {Status}",
                payment.Id, payment.Status);
        }
        catch (Exception ex)
        {
            await transaction.RollbackAsync();
            _logger.LogError(ex, "Failed to process payment callback: {TxnId}", callback.TransactionId);
            throw;
        }
    }
}

public class PaymentResult
{
    public bool Success { get; set; }
    public Guid PaymentId { get; set; }
    public string? ThirdPartyPaymentId { get; set; }
    public string? ErrorMessage { get; set; }
}

4. 控制器实现 ​

csharp
[ApiController]
[Route("api/[controller]")]
public class PaymentsController : ControllerBase
{
    private readonly PaymentService _paymentService;
    
    public PaymentsController(PaymentService paymentService)
    {
        _paymentService = paymentService;
    }
    
    /// <summary>
    /// 创建支付
    /// </summary>
    [HttpPost]
    public async Task<ActionResult<Payment>> CreatePayment(
        [FromBody] CreatePaymentRequest request,
        [FromHeader(Name = "Idempotency-Key")] string idempotencyKey)
    {
        if (string.IsNullOrWhiteSpace(idempotencyKey))
        {
            return BadRequest(new { error = "Idempotency-Key header is required" });
        }
        
        var result = await _paymentService.CreatePaymentAsync(
            request.OrderId,
            request.Amount,
            request.PaymentMethod,
            idempotencyKey);
        
        if (!result.IsSuccess)
        {
            return BadRequest(new { error = result.Error });
        }
        
        return CreatedAtAction(nameof(GetPayment), new { id = result.Data.Id }, result.Data);
    }
    
    /// <summary>
    /// 处理支付
    /// </summary>
    [HttpPost("{id}/process")]
    public async Task<ActionResult<PaymentResult>> ProcessPayment(Guid id)
    {
        var result = await _paymentService.ProcessPaymentAsync(id);
        
        if (!result.IsSuccess)
        {
            return BadRequest(new { error = result.Error });
        }
        
        return Ok(result.Data);
    }
    
    /// <summary>
    /// 查询支付状态
    /// </summary>
    [HttpGet("{id}")]
    public async Task<ActionResult<Payment>> GetPayment(Guid id)
    {
        var payment = await _dbContext.Payments.FindAsync(id);
        
        if (payment == null)
        {
            return NotFound();
        }
        
        return Ok(payment);
    }
    
    /// <summary>
    /// 支付回调(webhook)
    /// </summary>
    [HttpPost("callback/{provider}")]
    public async Task<IActionResult> PaymentCallback(string provider, [FromBody] JsonElement callback)
    {
        try
        {
            var paymentCallback = ParseCallback(provider, callback);
            await _paymentService.HandlePaymentCallbackAsync(paymentCallback);
            
            return Ok(new { status = "success" });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to process payment callback from {Provider}", provider);
            return StatusCode(500, new { status = "error" });
        }
    }
}

客户端最佳实践 ​

csharp
public class PaymentClient
{
    private readonly HttpClient _httpClient;
    
    public async Task<Payment> CreatePaymentWithRetry(
        CreatePaymentRequest request, 
        int maxRetries = 3)
    {
        // 生成幂等键(基于业务数据)
        var idempotencyKey = GenerateIdempotencyKey(request);
        
        for (int i = 0; i < maxRetries; i++)
        {
            try
            {
                var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/api/payments");
                httpRequest.Headers.Add("Idempotency-Key", idempotencyKey);
                httpRequest.Content = JsonContent.Create(request);
                
                var response = await _httpClient.SendAsync(httpRequest);
                response.EnsureSuccessStatusCode();
                
                return await response.Content.ReadFromJsonAsync<Payment>();
            }
            catch (HttpRequestException ex) when (i < maxRetries - 1)
            {
                _logger.LogWarning(ex, "Payment attempt {Attempt} failed, retrying...", i + 1);
                
                // 指数退避
                await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i)));
            }
        }
        
        throw new Exception("Payment failed after retries");
    }
    
    private string GenerateIdempotencyKey(CreatePaymentRequest request)
    {
        // 基于业务数据生成确定性幂等键
        var data = $"{request.OrderId}_{request.Amount}_{DateTime.UtcNow:yyyyMMdd}";
        using var sha256 = SHA256.Create();
        var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(data));
        return Convert.ToHexString(hash)[..32].ToLowerInvariant();
    }
}

监控告警 ​

csharp
public class PaymentMetrics
{
    private readonly Counter<long> _paymentsTotal;
    private readonly Counter<long> _paymentsSucceeded;
    private readonly Counter<long> _paymentsFailed;
    private readonly Counter<long> _duplicatePayments;
    private readonly Histogram<double> _paymentProcessingTime;
    
    public void RecordPayment(bool success, bool isDuplicate, double processingTimeMs)
    {
        _paymentsTotal.Add(1);
        
        if (success)
        {
            _paymentsSucceeded.Add(1);
        }
        else
        {
            _paymentsFailed.Add(1);
        }
        
        if (isDuplicate)
        {
            _duplicatePayments.Add(1);
        }
        
        _paymentProcessingTime.Record(processingTimeMs);
    }
}

总结 ​

支付系统的幂等性设计要点:

✅ 多层防护:幂等键 + 唯一索引 + 乐观锁
✅ 事务控制:确保数据一致性
✅ 详细日志:记录每次尝试和回调
✅ 监控告警:及时发现异常
✅ 客户端配合:生成确定性幂等键

⚠️ 注意事项:

  • 幂等键要基于业务数据生成
  • 第三方支付也要传递幂等键
  • Webhook 回调要做幂等检查
  • 定期清理过期数据

Released under the MIT License.