feat: 添加微信发货重试后台服务
- 新增 ShippingRetryBackgroundService 后台服务 - 每60秒检查 Redis 中失败的发货订单 - 支持最多10次重试,重试间隔30秒 - IRedisService 添加 GetKeysAsync 方法支持模式匹配 - 解决支付完成后立即发货导致'支付单不存在'的问题
This commit is contained in:
parent
ba0d0548d3
commit
d4c15c8feb
|
|
@ -106,6 +106,9 @@ try
|
||||||
// 注册福利屋开奖后台服务
|
// 注册福利屋开奖后台服务
|
||||||
builder.Services.AddHostedService<WelfareLotteryService>();
|
builder.Services.AddHostedService<WelfareLotteryService>();
|
||||||
|
|
||||||
|
// 注册微信发货重试后台服务
|
||||||
|
builder.Services.AddHostedService<HoneyBox.Core.Services.ShippingRetryBackgroundService>();
|
||||||
|
|
||||||
// 添加控制器
|
// 添加控制器
|
||||||
builder.Services.AddControllers(options =>
|
builder.Services.AddControllers(options =>
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -44,4 +44,11 @@ public interface IRedisService
|
||||||
/// 释放分布式锁
|
/// 释放分布式锁
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<bool> ReleaseLockAsync(string key, string value);
|
Task<bool> ReleaseLockAsync(string key, string value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据模式获取所有匹配的键
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pattern">匹配模式,如 "post_order:*"</param>
|
||||||
|
/// <returns>匹配的键列表</returns>
|
||||||
|
Task<IEnumerable<string>> GetKeysAsync(string pattern);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
@ -105,6 +105,26 @@ public class RedisService : IRedisService, IDisposable
|
||||||
return await _database.KeyDeleteAsync(key);
|
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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_connection?.Dispose();
|
_connection?.Dispose();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user