From 27613ab5b296e96153a93ddca4b2f16f697f7028 Mon Sep 17 00:00:00 2001 From: zpc Date: Tue, 10 Feb 2026 14:03:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=BE=AE=E4=BF=A1?= =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=E5=8F=91=E8=B4=A7=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=E5=BD=95=E5=85=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IWechatService 添加 UploadShippingInfoAsync 接口 - WechatService 实现调用微信 upload_shipping_info API - PaymentNotifyService 支付成功后自动调用发货接口 - 发货失败时保存到 Redis 等待重试(3天过期) - 添加 WechatShippingRequest/WechatShippingResult 模型 --- honey_box/pages/user/recharge-page.vue | 4 - .../Interfaces/IWechatService.cs | 8 + .../Services/PaymentNotifyService.cs | 122 +++++++++-- .../HoneyBox.Core/Services/WechatService.cs | 197 ++++++++++++++++++ .../Modules/ServiceModule.cs | 4 +- .../Models/Auth/WechatAuthResult.cs | 63 ++++++ 6 files changed, 379 insertions(+), 19 deletions(-) diff --git a/honey_box/pages/user/recharge-page.vue b/honey_box/pages/user/recharge-page.vue index 52280973..34ff3acc 100644 --- a/honey_box/pages/user/recharge-page.vue +++ b/honey_box/pages/user/recharge-page.vue @@ -30,10 +30,6 @@ - - - - diff --git a/server/HoneyBox/src/HoneyBox.Core/Interfaces/IWechatService.cs b/server/HoneyBox/src/HoneyBox.Core/Interfaces/IWechatService.cs index 6c0ece11..f4388b26 100644 --- a/server/HoneyBox/src/HoneyBox.Core/Interfaces/IWechatService.cs +++ b/server/HoneyBox/src/HoneyBox.Core/Interfaces/IWechatService.cs @@ -45,6 +45,14 @@ public interface IWechatService /// 二维码宽度,默认430 /// 小程序码图片字节数组,失败返回null Task GetWxaCodeUnlimitAsync(string scene, string? page = null, int width = 430); + + /// + /// 上传微信小程序发货信息 + /// 调用微信API upload_shipping_info 将订单标记为已发货 + /// + /// 发货信息请求 + /// 发货结果 + Task UploadShippingInfoAsync(WechatShippingRequest request); } /// diff --git a/server/HoneyBox/src/HoneyBox.Core/Services/PaymentNotifyService.cs b/server/HoneyBox/src/HoneyBox.Core/Services/PaymentNotifyService.cs index af4296f6..34df3037 100644 --- a/server/HoneyBox/src/HoneyBox.Core/Services/PaymentNotifyService.cs +++ b/server/HoneyBox/src/HoneyBox.Core/Services/PaymentNotifyService.cs @@ -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 _logger; /// @@ -51,6 +54,8 @@ public class PaymentNotifyService : IPaymentNotifyService IWechatPayConfigService wechatPayConfigService, IPaymentService paymentService, ILotteryEngine lotteryEngine, + IWechatService wechatService, + IRedisService redisService, ILogger 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); + } + else + { + // 其他类型按一番赏处理 + processResult = await ProcessLotteryOrderByOrderNoAsync(orderNo); } - // 其他类型按一番赏处理 - return await ProcessLotteryOrderByOrderNoAsync(orderNo); } - // 如果不是数字格式,尝试匹配旧格式 - if (LotteryOrderTypes.Contains(attach)) + else if (LotteryOrderTypes.Contains(attach)) { - return await ProcessLotteryOrderByOrderNoAsync(orderNo); + // 如果不是数字格式,尝试匹配旧格式 + processResult = await ProcessLotteryOrderByOrderNoAsync(orderNo); + } + else + { + _logger.LogWarning("未知的order_类型: Attach={Attach}, OrderNo={OrderNo}", attach, orderNo); + return false; } - _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 } } + /// + /// 上传微信发货信息 + /// 支付成功后调用微信接口将订单标记为已发货 + /// + 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); + } + } + + /// + /// 保存发货重试信息到Redis + /// + 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 } diff --git a/server/HoneyBox/src/HoneyBox.Core/Services/WechatService.cs b/server/HoneyBox/src/HoneyBox.Core/Services/WechatService.cs index 712a4a99..ba9e3f02 100644 --- a/server/HoneyBox/src/HoneyBox.Core/Services/WechatService.cs +++ b/server/HoneyBox/src/HoneyBox.Core/Services/WechatService.cs @@ -947,4 +947,201 @@ public class WechatService : IWechatService var random = new Random().Next(1000, 9999); return $"{prefix}{merchantPrefix}{projectPrefix}{payType}{timestamp}{random}"; } + + /// + /// 上传微信小程序发货信息 + /// 调用微信API upload_shipping_info 将订单标记为已发货 + /// + public async Task 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 + }; + } + } + + /// + /// 根据订单号获取商户配置 + /// + private async Task GetMerchantConfigByOrderNoAsync(string orderNo) + { + // 直接使用现有的 GetMerchantConfigAsync 方法获取默认商户配置 + // 该方法已经从数据库读取配置并支持缓存 + return await GetMerchantConfigAsync(); + } + + /// + /// 获取默认商品描述 + /// + private static string GetDefaultItemDesc(string orderNo) + { + // 发货订单(FH_开头)使用不同的描述 + if (orderNo.StartsWith("FH_", StringComparison.OrdinalIgnoreCase)) + { + return "本单购买的商品正在打包,请联系客服获取物流信息"; + } + return "本单购买商品已发放至[小程序盒柜]"; + } + + /// + /// 判断是否需要重试 + /// + private static bool ShouldRetry(int errCode) + { + // 以下错误码可以重试 + // 40001: access_token无效(可能过期) + // -1: 系统繁忙 + return errCode switch + { + 40001 => true, // access_token无效 + -1 => true, // 系统繁忙 + 42001 => true, // access_token过期 + _ => false + }; + } } diff --git a/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/ServiceModule.cs b/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/ServiceModule.cs index 83420546..48d90849 100644 --- a/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/ServiceModule.cs +++ b/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/ServiceModule.cs @@ -288,8 +288,10 @@ public class ServiceModule : Module var wechatPayConfigService = c.Resolve(); var paymentService = c.Resolve(); var lotteryEngine = c.Resolve(); + var wechatService = c.Resolve(); + var redisService = c.Resolve(); var logger = c.Resolve>(); - return new PaymentNotifyService(dbContext, wechatPayService, wechatPayV3Service, wechatPayConfigService, paymentService, lotteryEngine, logger); + return new PaymentNotifyService(dbContext, wechatPayService, wechatPayV3Service, wechatPayConfigService, paymentService, lotteryEngine, wechatService, redisService, logger); }).As().InstancePerLifetimeScope(); // 注册充值服务 diff --git a/server/HoneyBox/src/HoneyBox.Model/Models/Auth/WechatAuthResult.cs b/server/HoneyBox/src/HoneyBox.Model/Models/Auth/WechatAuthResult.cs index a9ed424d..35986382 100644 --- a/server/HoneyBox/src/HoneyBox.Model/Models/Auth/WechatAuthResult.cs +++ b/server/HoneyBox/src/HoneyBox.Model/Models/Auth/WechatAuthResult.cs @@ -46,3 +46,66 @@ public class WechatMobileResult /// public string? ErrorMessage { get; set; } } + +/// +/// 微信小程序发货信息请求 +/// +public class WechatShippingRequest +{ + /// + /// 用户OpenId + /// + public string OpenId { get; set; } = string.Empty; + + /// + /// 商户订单号 + /// + public string OrderNo { get; set; } = string.Empty; + + /// + /// 商户号(可选,不传则根据订单号自动匹配) + /// + public string? MchId { get; set; } + + /// + /// 小程序AppId(可选,不传则使用默认配置) + /// + public string? AppId { get; set; } + + /// + /// 商品描述(可选,默认为"本单购买商品已发放至[小程序盒柜]") + /// + public string? ItemDesc { get; set; } + + /// + /// 物流类型:1=实体物流,2=同城配送,3=虚拟商品,4=用户自提 + /// 默认为4(虚拟商品场景) + /// + public int LogisticsType { get; set; } = 4; +} + +/// +/// 微信小程序发货信息结果 +/// +public class WechatShippingResult +{ + /// + /// 是否成功 + /// + public bool Success { get; set; } + + /// + /// 错误码(0表示成功) + /// + public int ErrorCode { get; set; } + + /// + /// 错误信息 + /// + public string? ErrorMessage { get; set; } + + /// + /// 是否需要重试(发货失败时为true) + /// + public bool NeedRetry { get; set; } +}