From d4c15c8feb0c7ad0192fe047456803371b976f8d Mon Sep 17 00:00:00 2001 From: zpc Date: Tue, 10 Feb 2026 16:45:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=BE=AE=E4=BF=A1?= =?UTF-8?q?=E5=8F=91=E8=B4=A7=E9=87=8D=E8=AF=95=E5=90=8E=E5=8F=B0=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ShippingRetryBackgroundService 后台服务 - 每60秒检查 Redis 中失败的发货订单 - 支持最多10次重试,重试间隔30秒 - IRedisService 添加 GetKeysAsync 方法支持模式匹配 - 解决支付完成后立即发货导致'支付单不存在'的问题 --- server/HoneyBox/src/HoneyBox.Api/Program.cs | 3 + .../HoneyBox.Core/Interfaces/IRedisService.cs | 7 + .../ShippingRetryBackgroundService.cs | 175 ++++++++++++++++++ .../Cache/RedisService.cs | 20 ++ 4 files changed, 205 insertions(+) create mode 100644 server/HoneyBox/src/HoneyBox.Core/Services/ShippingRetryBackgroundService.cs diff --git a/server/HoneyBox/src/HoneyBox.Api/Program.cs b/server/HoneyBox/src/HoneyBox.Api/Program.cs index 65235475..42284349 100644 --- a/server/HoneyBox/src/HoneyBox.Api/Program.cs +++ b/server/HoneyBox/src/HoneyBox.Api/Program.cs @@ -106,6 +106,9 @@ try // 注册福利屋开奖后台服务 builder.Services.AddHostedService(); + // 注册微信发货重试后台服务 + builder.Services.AddHostedService(); + // 添加控制器 builder.Services.AddControllers(options => { diff --git a/server/HoneyBox/src/HoneyBox.Core/Interfaces/IRedisService.cs b/server/HoneyBox/src/HoneyBox.Core/Interfaces/IRedisService.cs index a8d521fd..cca80349 100644 --- a/server/HoneyBox/src/HoneyBox.Core/Interfaces/IRedisService.cs +++ b/server/HoneyBox/src/HoneyBox.Core/Interfaces/IRedisService.cs @@ -44,4 +44,11 @@ public interface IRedisService /// 释放分布式锁 /// Task ReleaseLockAsync(string key, string value); + + /// + /// 根据模式获取所有匹配的键 + /// + /// 匹配模式,如 "post_order:*" + /// 匹配的键列表 + Task> GetKeysAsync(string pattern); } diff --git a/server/HoneyBox/src/HoneyBox.Core/Services/ShippingRetryBackgroundService.cs b/server/HoneyBox/src/HoneyBox.Core/Services/ShippingRetryBackgroundService.cs new file mode 100644 index 00000000..f2bd4c16 --- /dev/null +++ b/server/HoneyBox/src/HoneyBox.Core/Services/ShippingRetryBackgroundService.cs @@ -0,0 +1,175 @@ +using System.Text.Json; +using HoneyBox.Core.Interfaces; +using HoneyBox.Model.Models.Auth; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace HoneyBox.Core.Services; + +/// +/// 微信发货重试后台服务 +/// 定期检查 Redis 中失败的发货订单并重试 +/// +public class ShippingRetryBackgroundService : BackgroundService +{ + private readonly IRedisService _redisService; + private readonly IWechatService _wechatService; + private readonly ILogger _logger; + + private const int MaxRetryCount = 10; // 最大重试次数 + private const int RetryIntervalSeconds = 30; // 重试间隔(秒) + private const int CheckIntervalSeconds = 60; // 检查间隔(秒) + private const string RetryKeyPattern = "post_order:*"; + + public ShippingRetryBackgroundService( + IRedisService redisService, + IWechatService wechatService, + ILogger logger) + { + _redisService = redisService; + _wechatService = wechatService; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("发货重试后台服务已启动"); + + while (!stoppingToken.IsCancellationRequested) + { + try + { + await ProcessFailedShippingOrdersAsync(stoppingToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "处理发货重试时发生异常"); + } + + // 等待下一次检查 + await Task.Delay(TimeSpan.FromSeconds(CheckIntervalSeconds), stoppingToken); + } + + _logger.LogInformation("发货重试后台服务已停止"); + } + + private async Task ProcessFailedShippingOrdersAsync(CancellationToken stoppingToken) + { + // 获取所有发货失败的订单键 + var failedOrderKeys = await _redisService.GetKeysAsync(RetryKeyPattern); + + if (failedOrderKeys == null || !failedOrderKeys.Any()) + { + return; + } + + _logger.LogDebug("发现 {Count} 个待重试的发货订单", failedOrderKeys.Count()); + + var nowTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + + foreach (var key in failedOrderKeys) + { + if (stoppingToken.IsCancellationRequested) + break; + + try + { + await ProcessSingleOrderAsync(key, nowTime); + } + catch (Exception ex) + { + _logger.LogError(ex, "处理发货重试订单失败: Key={Key}", key); + } + } + } + + private async Task ProcessSingleOrderAsync(string key, long nowTime) + { + // 获取订单数据 + var orderDataJson = await _redisService.GetStringAsync(key); + if (string.IsNullOrEmpty(orderDataJson)) + { + return; + } + + ShippingRetryData? orderData; + try + { + orderData = JsonSerializer.Deserialize(orderDataJson); + } + catch + { + // 数据格式错误,删除此键 + await _redisService.DeleteAsync(key); + return; + } + + if (orderData == null) + { + await _redisService.DeleteAsync(key); + return; + } + + // 检查重试次数 + if (orderData.retry_count >= MaxRetryCount) + { + _logger.LogWarning("订单 {OrderNo} 超过最大重试次数({MaxRetry}),已移除", + orderData.order_num, MaxRetryCount); + await _redisService.DeleteAsync(key); + return; + } + + // 检查是否达到重试间隔 + if ((nowTime - orderData.last_retry_time) < RetryIntervalSeconds) + { + return; + } + + // 尝试重新发货 + _logger.LogInformation("开始重试发货: OrderNo={OrderNo}, RetryCount={RetryCount}", + orderData.order_num, orderData.retry_count + 1); + + var request = new WechatShippingRequest + { + OpenId = orderData.openid, + OrderNo = orderData.order_num + }; + + var result = await _wechatService.UploadShippingInfoAsync(request); + + if (result.Success) + { + // 发货成功,删除 Redis 记录 + await _redisService.DeleteAsync(key); + _logger.LogInformation("订单 {OrderNo} 重试发货成功,已从重试队列移除", orderData.order_num); + } + else + { + // 发货失败,更新重试信息 + orderData.retry_count += 1; + orderData.last_retry_time = nowTime; + orderData.error_code = result.ErrorCode; + orderData.error_msg = result.ErrorMessage ?? "unknown"; + + var updatedJson = JsonSerializer.Serialize(orderData); + await _redisService.SetStringAsync(key, updatedJson, TimeSpan.FromDays(3)); + + _logger.LogWarning("订单 {OrderNo} 第 {RetryCount} 次重试发货失败: {ErrorMsg}", + orderData.order_num, orderData.retry_count, result.ErrorMessage); + } + } +} + +/// +/// 发货重试数据结构(与 Redis 存储格式一致) +/// +internal class ShippingRetryData +{ + public string openid { get; set; } = string.Empty; + public string order_num { get; set; } = string.Empty; + public int error_code { get; set; } + public string error_msg { get; set; } = string.Empty; + public int retry_count { get; set; } + public long last_retry_time { get; set; } + public long create_time { get; set; } +} diff --git a/server/HoneyBox/src/HoneyBox.Infrastructure/Cache/RedisService.cs b/server/HoneyBox/src/HoneyBox.Infrastructure/Cache/RedisService.cs index e25fc7e5..b21a2162 100644 --- a/server/HoneyBox/src/HoneyBox.Infrastructure/Cache/RedisService.cs +++ b/server/HoneyBox/src/HoneyBox.Infrastructure/Cache/RedisService.cs @@ -105,6 +105,26 @@ public class RedisService : IRedisService, IDisposable return await _database.KeyDeleteAsync(key); } + public async Task> GetKeysAsync(string pattern) + { + if (_connection == null || !_isConnected) + return Enumerable.Empty(); + + var keys = new List(); + var endpoints = _connection.GetEndPoints(); + + foreach (var endpoint in endpoints) + { + var server = _connection.GetServer(endpoint); + await foreach (var key in server.KeysAsync(pattern: pattern)) + { + keys.Add(key.ToString()); + } + } + + return keys; + } + public void Dispose() { _connection?.Dispose();