feat: 实现发货运费支付功能

- IWechatService: 添加 CreatePayOrderAsync 方法和相关类型
- WechatService: 实现 Web 支付订单创建,生成订单号并记录到 OrderNotify
- WarehouseService: 发货时调用支付服务生成支付参数
- SendResultDto: 修改 Res 属性支持 Web 支付格式
- ServiceModule: 更新依赖注入配置
This commit is contained in:
gpu 2026-01-24 11:30:13 +08:00
parent 7f941f1ddc
commit 4fba70467d
5 changed files with 300 additions and 10 deletions

View File

@ -1,4 +1,5 @@
using HoneyBox.Model.Models.Auth;
using HoneyBox.Model.Models.Payment;
namespace HoneyBox.Core.Interfaces;
@ -27,4 +28,105 @@ public interface IWechatService
/// <param name="appId">小程序AppId可选不传则使用默认配置</param>
/// <returns>access_token失败返回null</returns>
Task<string?> GetAccessTokenAsync(string? appId = null);
/// <summary>
/// 创建支付订单Web支付方式
/// </summary>
/// <param name="request">支付请求</param>
/// <returns>支付结果</returns>
Task<CreatePayResult> CreatePayOrderAsync(CreatePayRequest request);
}
/// <summary>
/// 创建支付请求
/// </summary>
public class CreatePayRequest
{
/// <summary>
/// 用户ID
/// </summary>
public int UserId { get; set; }
/// <summary>
/// 用户OpenId
/// </summary>
public string OpenId { get; set; } = string.Empty;
/// <summary>
/// 支付金额(元)
/// </summary>
public decimal Price { get; set; }
/// <summary>
/// 商品标题
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// 附加数据(订单类型)
/// </summary>
public string Attach { get; set; } = string.Empty;
/// <summary>
/// 订单号前缀
/// </summary>
public string Prefix { get; set; } = "MH_";
}
/// <summary>
/// 创建支付结果
/// </summary>
public class CreatePayResult
{
/// <summary>
/// 状态1=成功0=失败
/// </summary>
public int Status { get; set; }
/// <summary>
/// 错误消息
/// </summary>
public string? Message { get; set; }
/// <summary>
/// 订单号
/// </summary>
public string OrderNo { get; set; } = string.Empty;
/// <summary>
/// 支付参数(返回给前端)
/// </summary>
public WebPayParams? Res { get; set; }
}
/// <summary>
/// Web支付参数
/// </summary>
public class WebPayParams
{
/// <summary>
/// 支付数据
/// </summary>
public WebPayData? Data { get; set; }
/// <summary>
/// 请求支付URL
/// </summary>
public string RequestPay { get; set; } = string.Empty;
/// <summary>
/// 提示信息
/// </summary>
public string Tips { get; set; } = string.Empty;
}
/// <summary>
/// Web支付数据
/// </summary>
public class WebPayData
{
/// <summary>
/// 订单号
/// </summary>
public string OrderNum { get; set; } = string.Empty;
}

View File

@ -16,15 +16,18 @@ public class WarehouseService : IWarehouseService
private readonly HoneyBoxDbContext _dbContext;
private readonly ILogger<WarehouseService> _logger;
private readonly ILogisticsService _logisticsService;
private readonly IWechatService _wechatService;
public WarehouseService(
HoneyBoxDbContext dbContext,
ILogger<WarehouseService> logger,
ILogisticsService logisticsService)
ILogisticsService logisticsService,
IWechatService wechatService)
{
_dbContext = dbContext;
_logger = logger;
_logisticsService = logisticsService;
_wechatService = wechatService;
}
#region
@ -1099,13 +1102,55 @@ public class WarehouseService : IWarehouseService
SendResultDto result;
if (freePost > count && postMoney > 0)
{
// 需要支付运费,返回支付参数
// 注意:实际支付参数需要调用微信支付服务生成
// 需要支付运费,调用微信支付服务生成支付参数
// 获取用户信息
var user = await _dbContext.Users.FindAsync(userId);
if (user == null)
{
throw new InvalidOperationException("用户不存在");
}
var payRequest = new CreatePayRequest
{
UserId = userId,
OpenId = user.OpenId,
Price = actualFreight,
Title = $"背包发货{count}件",
Attach = "order_list_send",
Prefix = "FH_"
};
var payResult = await _wechatService.CreatePayOrderAsync(payRequest);
if (payResult.Status != 1)
{
throw new InvalidOperationException(payResult.Message ?? "创建支付订单失败");
}
// 更新发货记录的订单号为支付订单号
sendRecord.SendNum = payResult.OrderNo;
await _dbContext.SaveChangesAsync();
// 同时更新 OrderItems 的 SendNum
await _dbContext.OrderItems
.Where(o => o.SendNum == sendNum && o.UserId == userId)
.ExecuteUpdateAsync(s => s
.SetProperty(o => o.SendNum, payResult.OrderNo)
.SetProperty(o => o.UpdatedAt, DateTime.UtcNow));
result = new SendResultDto
{
Status = 1, // 需要支付
OrderNo = sendNum,
Res = null // 实际项目中需要调用WechatService生成支付参数
OrderNo = payResult.OrderNo,
Res = payResult.Res != null ? new WebPayResultDto
{
Data = new WebPayDataDto
{
OrderNum = payResult.OrderNo
},
RequestPay = payResult.Res.RequestPay,
Tips = payResult.Res.Tips
} : null
};
}
else

View File

@ -1,5 +1,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.Payment;
using Microsoft.Extensions.Logging;
@ -17,6 +19,7 @@ public class WechatService : IWechatService
private readonly WechatSettings _wechatSettings;
private readonly WechatPaySettings _wechatPaySettings;
private readonly IRedisService _redisService;
private readonly HoneyBoxDbContext _dbContext;
// 微信API端点
private const string WechatCodeToSessionUrl = "https://api.weixin.qq.com/sns/jscode2session";
@ -31,13 +34,15 @@ public class WechatService : IWechatService
ILogger<WechatService> logger,
WechatSettings wechatSettings,
IOptions<WechatPaySettings> wechatPaySettings,
IRedisService redisService)
IRedisService redisService,
HoneyBoxDbContext dbContext)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_wechatSettings = wechatSettings ?? throw new ArgumentNullException(nameof(wechatSettings));
_wechatPaySettings = wechatPaySettings?.Value ?? throw new ArgumentNullException(nameof(wechatPaySettings));
_redisService = redisService ?? throw new ArgumentNullException(nameof(redisService));
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
}
/// <summary>
@ -394,4 +399,104 @@ public class WechatService : IWechatService
_logger.LogWarning("未找到AppId {AppId} 的配置使用默认AppSecret", appId);
return _wechatSettings.AppSecret;
}
/// <summary>
/// 创建支付订单Web支付方式
/// </summary>
public async Task<CreatePayResult> CreatePayOrderAsync(CreatePayRequest request)
{
try
{
_logger.LogInformation("[创建支付订单] 开始处理: UserId={UserId}, Price={Price}, Title={Title}, Attach={Attach}",
request.UserId, request.Price, request.Title, request.Attach);
// 如果金额为0直接返回成功免费订单
if (request.Price <= 0)
{
var freeOrderNo = GenerateOrderNo(request.Prefix, "MON", "H5", "ZFB");
_logger.LogInformation("[创建支付订单] 免费订单,直接返回成功: OrderNo={OrderNo}", freeOrderNo);
return new CreatePayResult
{
Status = 1,
OrderNo = freeOrderNo,
Res = null
};
}
// 生成订单号
var orderNo = GenerateOrderNo(request.Prefix, "ZFA", "H5", "ZFB");
// 截取标题最多30个字符
var title = request.Title.Length > 30 ? request.Title.Substring(0, 30) : request.Title;
// 构建扩展数据
var extend = new Dictionary<string, string>
{
{ "orderType", request.Attach }
};
var extendStr = JsonSerializer.Serialize(extend);
// 创建支付通知记录
var orderNotify = new OrderNotify
{
OrderNo = orderNo,
NotifyUrl = string.Empty,
NonceStr = string.Empty,
PayTime = DateTime.UtcNow,
PayAmount = request.Price,
Status = 0, // 待处理
RetryCount = 0,
Attach = request.Attach,
OpenId = request.OpenId,
Extend = extendStr,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
_dbContext.OrderNotifies.Add(orderNotify);
await _dbContext.SaveChangesAsync();
_logger.LogInformation("[创建支付订单] 订单记录已创建: OrderNo={OrderNo}, NotifyId={NotifyId}",
orderNo, orderNotify.Id);
// 构建Web支付参数
var webPayParams = new WebPayParams
{
Data = new WebPayData
{
OrderNum = orderNo
},
RequestPay = "/api/send_web_pay_order",
Tips = "您即将进入客服聊天界面完成支付也可前往「我的」页面下载官方APP享受更便捷的购物及充值服务。"
};
return new CreatePayResult
{
Status = 1,
OrderNo = orderNo,
Res = webPayParams
};
}
catch (Exception ex)
{
_logger.LogError(ex, "[创建支付订单] 创建失败: UserId={UserId}", request.UserId);
return new CreatePayResult
{
Status = 0,
Message = "创建支付订单失败",
OrderNo = string.Empty
};
}
}
/// <summary>
/// 生成订单号
/// 格式:前缀(3位) + 商户前缀(3位) + 项目前缀(2位) + 支付类型(3位) + 时间戳 + 随机数
/// </summary>
private string GenerateOrderNo(string prefix, string merchantPrefix, string projectPrefix, string payType)
{
var timestamp = DateTime.UtcNow.ToString("yyyyMMddHHmmss");
var random = new Random().Next(1000, 9999);
return $"{prefix}{merchantPrefix}{projectPrefix}{payType}{timestamp}{random}";
}
}

View File

@ -29,7 +29,8 @@ public class ServiceModule : Module
var wechatSettings = c.Resolve<WechatSettings>();
var wechatPaySettings = c.Resolve<Microsoft.Extensions.Options.IOptions<WechatPaySettings>>();
var redisService = c.Resolve<IRedisService>();
return new WechatService(httpClientFactory.CreateClient(), logger, wechatSettings, wechatPaySettings, redisService);
var dbContext = c.Resolve<HoneyBoxDbContext>();
return new WechatService(httpClientFactory.CreateClient(), logger, wechatSettings, wechatPaySettings, redisService, dbContext);
}).As<IWechatService>().InstancePerLifetimeScope();
// 注册 IP 地理位置服务
@ -218,7 +219,8 @@ public class ServiceModule : Module
var dbContext = c.Resolve<HoneyBoxDbContext>();
var logger = c.Resolve<ILogger<WarehouseService>>();
var logisticsService = c.Resolve<ILogisticsService>();
return new WarehouseService(dbContext, logger, logisticsService);
var wechatService = c.Resolve<IWechatService>();
return new WarehouseService(dbContext, logger, logisticsService, wechatService);
}).As<IWarehouseService>().InstancePerLifetimeScope();
// ========== 支付系统服务注册 ==========

View File

@ -473,10 +473,46 @@ public class SendResultDto
public string OrderNo { get; set; } = string.Empty;
/// <summary>
/// 微信支付参数(需要支付运费时返回)
/// 支付参数(需要支付运费时返回支持Web支付和微信原生支付
/// </summary>
[JsonPropertyName("res")]
public WechatPayParamsDto? Res { get; set; }
public WebPayResultDto? Res { get; set; }
}
/// <summary>
/// Web支付结果DTO兼容前端支付流程
/// </summary>
public class WebPayResultDto
{
/// <summary>
/// 支付数据(包含订单号)
/// </summary>
[JsonPropertyName("data")]
public WebPayDataDto? Data { get; set; }
/// <summary>
/// 支付请求URL
/// </summary>
[JsonPropertyName("requestPay")]
public string RequestPay { get; set; } = string.Empty;
/// <summary>
/// 提示信息
/// </summary>
[JsonPropertyName("tips")]
public string Tips { get; set; } = string.Empty;
}
/// <summary>
/// Web支付数据DTO
/// </summary>
public class WebPayDataDto
{
/// <summary>
/// 订单号
/// </summary>
[JsonPropertyName("order_num")]
public string OrderNum { get; set; } = string.Empty;
}
/// <summary>