feat: 实现微信小程序发货信息录入功能

- IWechatService 添加 UploadShippingInfoAsync 接口
- WechatService 实现调用微信 upload_shipping_info API
- PaymentNotifyService 支付成功后自动调用发货接口
- 发货失败时保存到 Redis 等待重试(3天过期)
- 添加 WechatShippingRequest/WechatShippingResult 模型
This commit is contained in:
zpc 2026-02-10 14:03:00 +08:00
parent 113247a1e3
commit 27613ab5b2
6 changed files with 379 additions and 19 deletions

View File

@ -30,10 +30,6 @@
</view>
<view class="" style="width: 100%; margin: 44rpx 32rpx;">
<image :src="$img1('my/ic_alipay.png')" style="width: 320rpx; height: 96rpx;" mode=""></image>
</view>
<view class="center"
style="width: 686rpx; height: 92rpx; margin: 46rpx auto 0; background-color: #333333; border-radius: 16rpx;"
@click="pay" :class="{ 'btn-active': isPaying }">

View File

@ -45,6 +45,14 @@ public interface IWechatService
/// <param name="width">二维码宽度默认430</param>
/// <returns>小程序码图片字节数组失败返回null</returns>
Task<byte[]?> GetWxaCodeUnlimitAsync(string scene, string? page = null, int width = 430);
/// <summary>
/// 上传微信小程序发货信息
/// 调用微信API upload_shipping_info 将订单标记为已发货
/// </summary>
/// <param name="request">发货信息请求</param>
/// <returns>发货结果</returns>
Task<WechatShippingResult> UploadShippingInfoAsync(WechatShippingRequest request);
}
/// <summary>

View File

@ -2,6 +2,7 @@ using System.Text.Json;
using HoneyBox.Core.Interfaces;
using HoneyBox.Model.Data;
using HoneyBox.Model.Entities;
using HoneyBox.Model.Models.Auth;
using HoneyBox.Model.Models.Lottery;
using HoneyBox.Model.Models.Mall;
using HoneyBox.Model.Models.Payment;
@ -21,6 +22,8 @@ public class PaymentNotifyService : IPaymentNotifyService
private readonly IWechatPayConfigService _wechatPayConfigService;
private readonly IPaymentService _paymentService;
private readonly ILotteryEngine _lotteryEngine;
private readonly IWechatService _wechatService;
private readonly IRedisService _redisService;
private readonly ILogger<PaymentNotifyService> _logger;
/// <summary>
@ -51,6 +54,8 @@ public class PaymentNotifyService : IPaymentNotifyService
IWechatPayConfigService wechatPayConfigService,
IPaymentService paymentService,
ILotteryEngine lotteryEngine,
IWechatService wechatService,
IRedisService redisService,
ILogger<PaymentNotifyService> logger)
{
_dbContext = dbContext;
@ -59,6 +64,8 @@ public class PaymentNotifyService : IPaymentNotifyService
_wechatPayConfigService = wechatPayConfigService;
_paymentService = paymentService;
_lotteryEngine = lotteryEngine;
_wechatService = wechatService;
_redisService = redisService;
_logger = logger;
}
@ -418,22 +425,24 @@ public class PaymentNotifyService : IPaymentNotifyService
// 记录支付流水
await RecordPaymentAsync(user.Id, orderNo, notifyData.TotalFee / 100m, attach);
bool processResult;
// 根据attach类型路由处理
// attach格式: order_{type} 或 infinite_{type} 或固定字符串
if (attach == OrderAttachType.UserRecharge)
{
// 余额充值
return await ProcessRechargeOrderAsync(orderNo);
processResult = await ProcessRechargeOrderAsync(orderNo);
}
else if (attach == OrderAttachType.OrderListSend)
{
// 发货运费订单
return await ProcessShippingFeeOrderAsync(orderNo);
processResult = await ProcessShippingFeeOrderAsync(orderNo);
}
else if (attach == OrderAttachType.OrderProduct)
{
// 钻石商品订单
return await ProcessDiamondOrderAsync(orderNo, user.Id);
processResult = await ProcessDiamondOrderAsync(orderNo, user.Id);
}
else if (attach.StartsWith("order_"))
{
@ -445,39 +454,53 @@ public class PaymentNotifyService : IPaymentNotifyService
// 类型 4 是抽卡机
if (orderType == 4)
{
return await ProcessCardExtractorOrderByOrderNoAsync(orderNo);
processResult = await ProcessCardExtractorOrderByOrderNoAsync(orderNo);
}
// 其他类型按一番赏处理
return await ProcessLotteryOrderByOrderNoAsync(orderNo);
}
// 如果不是数字格式,尝试匹配旧格式
if (LotteryOrderTypes.Contains(attach))
else
{
return await ProcessLotteryOrderByOrderNoAsync(orderNo);
// 其他类型按一番赏处理
processResult = await ProcessLotteryOrderByOrderNoAsync(orderNo);
}
}
else if (LotteryOrderTypes.Contains(attach))
{
// 如果不是数字格式,尝试匹配旧格式
processResult = await ProcessLotteryOrderByOrderNoAsync(orderNo);
}
else
{
_logger.LogWarning("未知的order_类型: Attach={Attach}, OrderNo={OrderNo}", attach, orderNo);
return false;
}
}
else if (attach.StartsWith("infinite_"))
{
// 无限赏类订单 (infinite_2, infinite_7, infinite_8, infinite_9 等)
return await ProcessInfiniteOrderByOrderNoAsync(orderNo);
processResult = await ProcessInfiniteOrderByOrderNoAsync(orderNo);
}
else if (LotteryOrderTypes.Contains(attach))
{
// 兼容旧格式 order_yfs, order_lts 等
return await ProcessLotteryOrderByOrderNoAsync(orderNo);
processResult = await ProcessLotteryOrderByOrderNoAsync(orderNo);
}
else if (InfiniteOrderTypes.Contains(attach))
{
// 兼容旧格式 order_wxs, order_fbs 等
return await ProcessInfiniteOrderByOrderNoAsync(orderNo);
processResult = await ProcessInfiniteOrderByOrderNoAsync(orderNo);
}
else
{
_logger.LogWarning("未知的订单类型: Attach={Attach}, OrderNo={OrderNo}", attach, orderNo);
return false;
}
// 处理成功后,调用微信发货接口
if (processResult && !string.IsNullOrEmpty(notifyData.OpenId))
{
await UploadWechatShippingInfoAsync(notifyData.OpenId, orderNo);
}
return processResult;
}
catch (Exception ex)
{
@ -1407,5 +1430,76 @@ public class PaymentNotifyService : IPaymentNotifyService
}
}
/// <summary>
/// 上传微信发货信息
/// 支付成功后调用微信接口将订单标记为已发货
/// </summary>
private async Task UploadWechatShippingInfoAsync(string openId, string orderNo)
{
try
{
var request = new WechatShippingRequest
{
OpenId = openId,
OrderNo = orderNo
};
var result = await _wechatService.UploadShippingInfoAsync(request);
if (result.Success)
{
_logger.LogInformation("微信发货成功: OrderNo={OrderNo}", orderNo);
}
else
{
_logger.LogWarning("微信发货失败: OrderNo={OrderNo}, ErrCode={ErrCode}, ErrMsg={ErrMsg}",
orderNo, result.ErrorCode, result.ErrorMessage);
// 发货失败存入Redis等待重试
if (result.NeedRetry)
{
await SaveShippingRetryInfoAsync(openId, orderNo, result.ErrorCode, result.ErrorMessage);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "调用微信发货接口异常: OrderNo={OrderNo}", orderNo);
// 异常情况也存入Redis等待重试
await SaveShippingRetryInfoAsync(openId, orderNo, -1, ex.Message);
}
}
/// <summary>
/// 保存发货重试信息到Redis
/// </summary>
private async Task SaveShippingRetryInfoAsync(string openId, string orderNo, int errorCode, string? errorMessage)
{
try
{
var key = $"post_order:{orderNo}";
var retryData = new
{
openid = openId,
order_num = orderNo,
error_code = errorCode,
error_msg = errorMessage ?? "unknown",
retry_count = 0,
last_retry_time = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
create_time = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
var json = JsonSerializer.Serialize(retryData);
// 设置过期时间为3天
await _redisService.SetStringAsync(key, json, TimeSpan.FromDays(3));
_logger.LogInformation("发货重试信息已保存到Redis: OrderNo={OrderNo}, Key={Key}", orderNo, key);
}
catch (Exception ex)
{
_logger.LogError(ex, "保存发货重试信息失败: OrderNo={OrderNo}", orderNo);
}
}
#endregion
}

View File

@ -947,4 +947,201 @@ public class WechatService : IWechatService
var random = new Random().Next(1000, 9999);
return $"{prefix}{merchantPrefix}{projectPrefix}{payType}{timestamp}{random}";
}
/// <summary>
/// 上传微信小程序发货信息
/// 调用微信API upload_shipping_info 将订单标记为已发货
/// </summary>
public async Task<WechatShippingResult> UploadShippingInfoAsync(WechatShippingRequest request)
{
const string uploadShippingUrl = "https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info";
try
{
// 1. 参数验证
if (string.IsNullOrEmpty(request.OpenId))
{
return new WechatShippingResult
{
Success = false,
ErrorCode = -1,
ErrorMessage = "OpenId不能为空"
};
}
if (string.IsNullOrEmpty(request.OrderNo))
{
return new WechatShippingResult
{
Success = false,
ErrorCode = -1,
ErrorMessage = "订单号不能为空"
};
}
// 2. 获取商户配置(根据订单号匹配)
var merchantConfig = await GetMerchantConfigByOrderNoAsync(request.OrderNo);
if (merchantConfig == null)
{
_logger.LogWarning("发货失败:未找到订单对应的商户配置, OrderNo={OrderNo}", request.OrderNo);
return new WechatShippingResult
{
Success = false,
ErrorCode = -1,
ErrorMessage = "未找到商户配置",
NeedRetry = true
};
}
var mchId = request.MchId ?? merchantConfig.MchId;
var appId = request.AppId ?? merchantConfig.AppId;
// 3. 获取access_token
var accessToken = await GetAccessTokenAsync(appId);
if (string.IsNullOrEmpty(accessToken))
{
_logger.LogWarning("发货失败获取access_token失败, OrderNo={OrderNo}", request.OrderNo);
return new WechatShippingResult
{
Success = false,
ErrorCode = -1,
ErrorMessage = "获取access_token失败",
NeedRetry = true
};
}
// 4. 构建请求参数
var itemDesc = request.ItemDesc ?? GetDefaultItemDesc(request.OrderNo);
var uploadTime = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss") + "+08:00";
var requestBody = new
{
order_key = new
{
order_number_type = 1, // 使用商户订单号
mchid = mchId,
out_trade_no = request.OrderNo
},
logistics_type = request.LogisticsType, // 4=虚拟商品
delivery_mode = 1, // 1=统一发货
shipping_list = new[]
{
new { item_desc = itemDesc }
},
upload_time = uploadTime,
payer = new
{
openid = request.OpenId
}
};
var jsonContent = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
});
_logger.LogInformation("调用微信发货接口: OrderNo={OrderNo}, MchId={MchId}, OpenId={OpenId}",
request.OrderNo, mchId, request.OpenId);
// 5. 发送请求
var url = $"{uploadShippingUrl}?access_token={accessToken}";
var content = new StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(url, content);
var responseContent = await response.Content.ReadAsStringAsync();
_logger.LogDebug("微信发货接口响应: {Response}", responseContent);
// 6. 解析响应
using var jsonDoc = JsonDocument.Parse(responseContent);
var root = jsonDoc.RootElement;
var errCode = root.TryGetProperty("errcode", out var errCodeProp) ? errCodeProp.GetInt32() : -1;
var errMsg = root.TryGetProperty("errmsg", out var errMsgProp) ? errMsgProp.GetString() : "未知错误";
if (errCode == 0)
{
_logger.LogInformation("微信发货成功: OrderNo={OrderNo}", request.OrderNo);
return new WechatShippingResult
{
Success = true,
ErrorCode = 0,
ErrorMessage = "ok"
};
}
else
{
_logger.LogWarning("微信发货失败: OrderNo={OrderNo}, ErrCode={ErrCode}, ErrMsg={ErrMsg}",
request.OrderNo, errCode, errMsg);
return new WechatShippingResult
{
Success = false,
ErrorCode = errCode,
ErrorMessage = errMsg,
NeedRetry = ShouldRetry(errCode)
};
}
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "微信发货HTTP请求异常: OrderNo={OrderNo}", request.OrderNo);
return new WechatShippingResult
{
Success = false,
ErrorCode = -1,
ErrorMessage = $"HTTP请求异常: {ex.Message}",
NeedRetry = true
};
}
catch (Exception ex)
{
_logger.LogError(ex, "微信发货异常: OrderNo={OrderNo}", request.OrderNo);
return new WechatShippingResult
{
Success = false,
ErrorCode = -1,
ErrorMessage = $"发货异常: {ex.Message}",
NeedRetry = true
};
}
}
/// <summary>
/// 根据订单号获取商户配置
/// </summary>
private async Task<WechatPayMerchantConfig?> GetMerchantConfigByOrderNoAsync(string orderNo)
{
// 直接使用现有的 GetMerchantConfigAsync 方法获取默认商户配置
// 该方法已经从数据库读取配置并支持缓存
return await GetMerchantConfigAsync();
}
/// <summary>
/// 获取默认商品描述
/// </summary>
private static string GetDefaultItemDesc(string orderNo)
{
// 发货订单FH_开头使用不同的描述
if (orderNo.StartsWith("FH_", StringComparison.OrdinalIgnoreCase))
{
return "本单购买的商品正在打包,请联系客服获取物流信息";
}
return "本单购买商品已发放至[小程序盒柜]";
}
/// <summary>
/// 判断是否需要重试
/// </summary>
private static bool ShouldRetry(int errCode)
{
// 以下错误码可以重试
// 40001: access_token无效可能过期
// -1: 系统繁忙
return errCode switch
{
40001 => true, // access_token无效
-1 => true, // 系统繁忙
42001 => true, // access_token过期
_ => false
};
}
}

View File

@ -288,8 +288,10 @@ public class ServiceModule : Module
var wechatPayConfigService = c.Resolve<IWechatPayConfigService>();
var paymentService = c.Resolve<IPaymentService>();
var lotteryEngine = c.Resolve<ILotteryEngine>();
var wechatService = c.Resolve<IWechatService>();
var redisService = c.Resolve<IRedisService>();
var logger = c.Resolve<ILogger<PaymentNotifyService>>();
return new PaymentNotifyService(dbContext, wechatPayService, wechatPayV3Service, wechatPayConfigService, paymentService, lotteryEngine, logger);
return new PaymentNotifyService(dbContext, wechatPayService, wechatPayV3Service, wechatPayConfigService, paymentService, lotteryEngine, wechatService, redisService, logger);
}).As<IPaymentNotifyService>().InstancePerLifetimeScope();
// 注册充值服务

View File

@ -46,3 +46,66 @@ public class WechatMobileResult
/// </summary>
public string? ErrorMessage { get; set; }
}
/// <summary>
/// 微信小程序发货信息请求
/// </summary>
public class WechatShippingRequest
{
/// <summary>
/// 用户OpenId
/// </summary>
public string OpenId { get; set; } = string.Empty;
/// <summary>
/// 商户订单号
/// </summary>
public string OrderNo { get; set; } = string.Empty;
/// <summary>
/// 商户号(可选,不传则根据订单号自动匹配)
/// </summary>
public string? MchId { get; set; }
/// <summary>
/// 小程序AppId可选不传则使用默认配置
/// </summary>
public string? AppId { get; set; }
/// <summary>
/// 商品描述(可选,默认为"本单购买商品已发放至[小程序盒柜]"
/// </summary>
public string? ItemDesc { get; set; }
/// <summary>
/// 物流类型1=实体物流2=同城配送3=虚拟商品4=用户自提
/// 默认为4虚拟商品场景
/// </summary>
public int LogisticsType { get; set; } = 4;
}
/// <summary>
/// 微信小程序发货信息结果
/// </summary>
public class WechatShippingResult
{
/// <summary>
/// 是否成功
/// </summary>
public bool Success { get; set; }
/// <summary>
/// 错误码0表示成功
/// </summary>
public int ErrorCode { get; set; }
/// <summary>
/// 错误信息
/// </summary>
public string? ErrorMessage { get; set; }
/// <summary>
/// 是否需要重试发货失败时为true
/// </summary>
public bool NeedRetry { get; set; }
}