feat: 添加微信发货重试后台服务

- 新增 ShippingRetryBackgroundService 后台服务
- 每60秒检查 Redis 中失败的发货订单
- 支持最多10次重试,重试间隔30秒
- IRedisService 添加 GetKeysAsync 方法支持模式匹配
- 解决支付完成后立即发货导致'支付单不存在'的问题
This commit is contained in:
zpc 2026-02-10 16:45:06 +08:00
parent ba0d0548d3
commit d4c15c8feb
4 changed files with 205 additions and 0 deletions

View File

@ -106,6 +106,9 @@ try
// 注册福利屋开奖后台服务
builder.Services.AddHostedService<WelfareLotteryService>();
// 注册微信发货重试后台服务
builder.Services.AddHostedService<HoneyBox.Core.Services.ShippingRetryBackgroundService>();
// 添加控制器
builder.Services.AddControllers(options =>
{

View File

@ -44,4 +44,11 @@ public interface IRedisService
/// 释放分布式锁
/// </summary>
Task<bool> ReleaseLockAsync(string key, string value);
/// <summary>
/// 根据模式获取所有匹配的键
/// </summary>
/// <param name="pattern">匹配模式,如 "post_order:*"</param>
/// <returns>匹配的键列表</returns>
Task<IEnumerable<string>> GetKeysAsync(string pattern);
}

View File

@ -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;
/// <summary>
/// 微信发货重试后台服务
/// 定期检查 Redis 中失败的发货订单并重试
/// </summary>
public class ShippingRetryBackgroundService : BackgroundService
{
private readonly IRedisService _redisService;
private readonly IWechatService _wechatService;
private readonly ILogger<ShippingRetryBackgroundService> _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<ShippingRetryBackgroundService> 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<ShippingRetryData>(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);
}
}
}
/// <summary>
/// 发货重试数据结构(与 Redis 存储格式一致)
/// </summary>
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; }
}

View File

@ -105,6 +105,26 @@ public class RedisService : IRedisService, IDisposable
return await _database.KeyDeleteAsync(key);
}
public async Task<IEnumerable<string>> GetKeysAsync(string pattern)
{
if (_connection == null || !_isConnected)
return Enumerable.Empty<string>();
var keys = new List<string>();
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();