支付.
This commit is contained in:
parent
b22add232d
commit
635c708487
2
.vscode/settings.json
vendored
Normal file
2
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
{
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/png" href="/logo.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>相宜相亲 - 后台管理系统</title>
|
||||
</head>
|
||||
|
|
|
|||
BIN
admin/public/logo.png
Normal file
BIN
admin/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
4
admin/public/logo.svg
Normal file
4
admin/public/logo.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
|
||||
<circle cx="50" cy="50" r="45" fill="#409eff"/>
|
||||
<text x="50" y="60" font-size="40" fill="white" text-anchor="middle" font-family="Arial, sans-serif" font-weight="bold">相</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 278 B |
BIN
admin/src/assets/images/logo.png
Normal file
BIN
admin/src/assets/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
|
|
@ -72,7 +72,7 @@ function getFullPath(parent: RouteRecordRaw, child?: RouteRecordRaw): string {
|
|||
<div class="sidebar-logo">
|
||||
<img
|
||||
v-if="!isCollapsed"
|
||||
src="@/assets/images/logo.svg"
|
||||
src="@/assets/images/logo.png"
|
||||
alt="Logo"
|
||||
class="logo-img"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ async function handleLogin() {
|
|||
<!-- Logo和标题 -->
|
||||
<div class="login-header">
|
||||
<img
|
||||
src="@/assets/images/logo.svg"
|
||||
src="@/assets/images/logo.png"
|
||||
alt="Logo"
|
||||
class="logo"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export * from './interact'
|
|||
export * from './chat'
|
||||
export * from './member'
|
||||
export * from './order'
|
||||
export * from './pay'
|
||||
|
||||
// 默认导出所有模块
|
||||
import request from './request'
|
||||
|
|
@ -22,6 +23,7 @@ import interact from './interact'
|
|||
import chat from './chat'
|
||||
import member from './member'
|
||||
import order from './order'
|
||||
import pay from './pay'
|
||||
|
||||
export default {
|
||||
request,
|
||||
|
|
@ -32,5 +34,6 @@ export default {
|
|||
interact,
|
||||
chat,
|
||||
member,
|
||||
order
|
||||
order,
|
||||
pay
|
||||
}
|
||||
|
|
|
|||
120
miniapp/api/pay.js
Normal file
120
miniapp/api/pay.js
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
/**
|
||||
* 支付接口模块
|
||||
*/
|
||||
|
||||
import { get, post } from './request'
|
||||
|
||||
/**
|
||||
* 获取会员等级配置
|
||||
* @returns {Promise<Object>} 会员等级列表
|
||||
*/
|
||||
export function getMemberTiers() {
|
||||
return get('/pay/memberTiers')
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建会员订单
|
||||
* @param {number} memberLevel - 会员等级:1永久会员 2诚意会员 3家庭版会员
|
||||
* @returns {Promise<Object>} 订单信息和支付参数
|
||||
*/
|
||||
export function createMemberOrder(memberLevel) {
|
||||
return post('/pay/createOrder', { memberLevel })
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询订单详情
|
||||
* @param {string} orderNo - 订单号
|
||||
* @returns {Promise<Object>} 订单详情
|
||||
*/
|
||||
export function getOrderDetail(orderNo) {
|
||||
return get(`/pay/order/${orderNo}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询订单支付状态(主动查询微信)
|
||||
* @param {string} orderNo - 订单号
|
||||
* @returns {Promise<Object>} 订单详情
|
||||
*/
|
||||
export function queryOrderStatus(orderNo) {
|
||||
return get(`/pay/order/${orderNo}/status`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订单
|
||||
* @param {string} orderNo - 订单号
|
||||
* @returns {Promise<Object>} 操作结果
|
||||
*/
|
||||
export function cancelOrder(orderNo) {
|
||||
return post(`/pay/order/${orderNo}/cancel`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 调起微信支付
|
||||
* @param {Object} paymentParams - 支付参数
|
||||
* @returns {Promise<Object>} 支付结果
|
||||
*/
|
||||
export function requestPayment(paymentParams) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.requestPayment({
|
||||
timeStamp: paymentParams.timeStamp,
|
||||
nonceStr: paymentParams.nonceStr,
|
||||
package: paymentParams.package,
|
||||
signType: paymentParams.signType || 'RSA',
|
||||
paySign: paymentParams.paySign,
|
||||
success: (res) => {
|
||||
resolve({ success: true, data: res })
|
||||
},
|
||||
fail: (err) => {
|
||||
if (err.errMsg.includes('cancel')) {
|
||||
resolve({ success: false, cancelled: true, message: '用户取消支付' })
|
||||
} else {
|
||||
reject(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 购买会员完整流程
|
||||
* @param {number} memberLevel - 会员等级
|
||||
* @returns {Promise<Object>} 支付结果
|
||||
*/
|
||||
export async function purchaseMember(memberLevel) {
|
||||
try {
|
||||
// 1. 创建订单
|
||||
const orderRes = await createMemberOrder(memberLevel)
|
||||
if (orderRes.code !== 0) {
|
||||
return { success: false, message: orderRes.message || '创建订单失败' }
|
||||
}
|
||||
|
||||
const { orderNo, paymentParams } = orderRes.data
|
||||
|
||||
// 2. 调起支付
|
||||
const payRes = await requestPayment(paymentParams)
|
||||
if (!payRes.success) {
|
||||
return payRes
|
||||
}
|
||||
|
||||
// 3. 查询支付结果
|
||||
const statusRes = await queryOrderStatus(orderNo)
|
||||
if (statusRes.code === 0 && statusRes.data.status === 2) {
|
||||
return { success: true, orderNo, message: '支付成功' }
|
||||
}
|
||||
|
||||
return { success: true, orderNo, message: '支付处理中' }
|
||||
} catch (error) {
|
||||
console.error('购买会员失败:', error)
|
||||
return { success: false, message: error.message || '支付失败' }
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getMemberTiers,
|
||||
createMemberOrder,
|
||||
getOrderDetail,
|
||||
queryOrderStatus,
|
||||
cancelOrder,
|
||||
requestPayment,
|
||||
purchaseMember
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ const ENV = {
|
|||
}
|
||||
|
||||
// 当前环境 - 开发时使用 development,打包时改为 production
|
||||
const CURRENT_ENV = 'development'
|
||||
const CURRENT_ENV = 'production'
|
||||
|
||||
// 导出配置
|
||||
export const config = {
|
||||
|
|
|
|||
187
server/src/XiangYi.AppApi/Controllers/PayController.cs
Normal file
187
server/src/XiangYi.AppApi/Controllers/PayController.cs
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using XiangYi.Application.DTOs.Requests;
|
||||
using XiangYi.Application.DTOs.Responses;
|
||||
using XiangYi.Application.Interfaces;
|
||||
using XiangYi.Core.Constants;
|
||||
using XiangYi.Infrastructure.Payment;
|
||||
|
||||
namespace XiangYi.AppApi.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// 支付控制器
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/app/[controller]")]
|
||||
[Authorize]
|
||||
public class PayController : ControllerBase
|
||||
{
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly IWeChatPayService _weChatPayService;
|
||||
private readonly ILogger<PayController> _logger;
|
||||
|
||||
public PayController(
|
||||
IPaymentService paymentService,
|
||||
IWeChatPayService weChatPayService,
|
||||
ILogger<PayController> logger)
|
||||
{
|
||||
_paymentService = paymentService;
|
||||
_weChatPayService = weChatPayService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取会员等级配置
|
||||
/// </summary>
|
||||
[HttpGet("memberTiers")]
|
||||
[AllowAnonymous]
|
||||
public async Task<ApiResponse<List<MemberTierResponse>>> GetMemberTiers()
|
||||
{
|
||||
var result = await _paymentService.GetMemberTiersAsync();
|
||||
return ApiResponse<List<MemberTierResponse>>.Success(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建会员订单
|
||||
/// </summary>
|
||||
[HttpPost("createOrder")]
|
||||
public async Task<ApiResponse<CreateOrderResponse>> CreateOrder([FromBody] CreateMemberOrderRequest request)
|
||||
{
|
||||
if (request.MemberLevel < 1 || request.MemberLevel > 3)
|
||||
{
|
||||
return ApiResponse<CreateOrderResponse>.Error(ErrorCodes.InvalidParameter, "无效的会员等级");
|
||||
}
|
||||
|
||||
var userId = GetCurrentUserId();
|
||||
var result = await _paymentService.CreateMemberOrderAsync(userId, request);
|
||||
return ApiResponse<CreateOrderResponse>.Success(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询订单详情
|
||||
/// </summary>
|
||||
[HttpGet("order/{orderNo}")]
|
||||
public async Task<ApiResponse<OrderDetailResponse>> GetOrderDetail(string orderNo)
|
||||
{
|
||||
var userId = GetCurrentUserId();
|
||||
var result = await _paymentService.GetOrderDetailAsync(userId, orderNo);
|
||||
if (result == null)
|
||||
{
|
||||
return ApiResponse<OrderDetailResponse>.Error(ErrorCodes.OrderNotFound, "订单不存在");
|
||||
}
|
||||
return ApiResponse<OrderDetailResponse>.Success(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询订单支付状态(主动查询微信)
|
||||
/// </summary>
|
||||
[HttpGet("order/{orderNo}/status")]
|
||||
public async Task<ApiResponse<OrderDetailResponse>> QueryOrderStatus(string orderNo)
|
||||
{
|
||||
var userId = GetCurrentUserId();
|
||||
var result = await _paymentService.QueryOrderPayStatusAsync(userId, orderNo);
|
||||
if (result == null)
|
||||
{
|
||||
return ApiResponse<OrderDetailResponse>.Error(ErrorCodes.OrderNotFound, "订单不存在");
|
||||
}
|
||||
return ApiResponse<OrderDetailResponse>.Success(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消订单
|
||||
/// </summary>
|
||||
[HttpPost("order/{orderNo}/cancel")]
|
||||
public async Task<ApiResponse> CancelOrder(string orderNo)
|
||||
{
|
||||
var userId = GetCurrentUserId();
|
||||
await _paymentService.CancelOrderAsync(userId, orderNo);
|
||||
return ApiResponse.Success();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 微信支付回调通知
|
||||
/// </summary>
|
||||
[HttpPost("notify")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> WeChatPayNotify()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 读取请求体
|
||||
using var reader = new StreamReader(Request.Body);
|
||||
var body = await reader.ReadToEndAsync();
|
||||
|
||||
_logger.LogInformation("收到微信支付回调: {Body}", body);
|
||||
|
||||
// 获取签名相关头信息
|
||||
var timestamp = Request.Headers["Wechatpay-Timestamp"].FirstOrDefault() ?? "";
|
||||
var nonce = Request.Headers["Wechatpay-Nonce"].FirstOrDefault() ?? "";
|
||||
var signature = Request.Headers["Wechatpay-Signature"].FirstOrDefault() ?? "";
|
||||
var serial = Request.Headers["Wechatpay-Serial"].FirstOrDefault() ?? "";
|
||||
|
||||
// 验证签名
|
||||
if (!_weChatPayService.VerifySignature(timestamp, nonce, body, signature, serial))
|
||||
{
|
||||
_logger.LogWarning("微信支付回调签名验证失败");
|
||||
return new JsonResult(new { code = "FAIL", message = "签名验证失败" }) { StatusCode = 401 };
|
||||
}
|
||||
|
||||
// 解析回调数据
|
||||
var notifyData = JsonSerializer.Deserialize<WeChatPayNotifyRequest>(body, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
|
||||
if (notifyData == null || notifyData.Event_type != "TRANSACTION.SUCCESS")
|
||||
{
|
||||
_logger.LogWarning("微信支付回调事件类型不是支付成功: {EventType}", notifyData?.Event_type);
|
||||
return Ok(new { code = "SUCCESS", message = "OK" });
|
||||
}
|
||||
|
||||
// 解密回调数据
|
||||
var decryptedData = _weChatPayService.DecryptNotifyData(
|
||||
notifyData.Resource.Ciphertext,
|
||||
notifyData.Resource.Nonce,
|
||||
notifyData.Resource.Associated_data);
|
||||
|
||||
_logger.LogInformation("解密后的支付数据: {Data}", decryptedData);
|
||||
|
||||
// 解析支付结果
|
||||
using var doc = JsonDocument.Parse(decryptedData);
|
||||
var root = doc.RootElement;
|
||||
|
||||
var orderNo = root.GetProperty("out_trade_no").GetString()!;
|
||||
var transactionId = root.GetProperty("transaction_id").GetString()!;
|
||||
var successTimeStr = root.GetProperty("success_time").GetString()!;
|
||||
var successTime = DateTime.Parse(successTimeStr);
|
||||
|
||||
// 处理支付成功
|
||||
var success = await _paymentService.HandlePaymentSuccessAsync(orderNo, transactionId, successTime);
|
||||
|
||||
if (success)
|
||||
{
|
||||
return Ok(new { code = "SUCCESS", message = "OK" });
|
||||
}
|
||||
else
|
||||
{
|
||||
return new JsonResult(new { code = "FAIL", message = "处理失败" }) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "处理微信支付回调异常");
|
||||
return new JsonResult(new { code = "FAIL", message = ex.Message }) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户ID
|
||||
/// </summary>
|
||||
private long GetCurrentUserId()
|
||||
{
|
||||
var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
return long.TryParse(userIdClaim, out var userId) ? userId : 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -39,6 +39,17 @@
|
|||
"AppSecret": "fe3b5aa5715820cd66af3d42d55efad6"
|
||||
}
|
||||
},
|
||||
"WeChatPay": {
|
||||
"AppId": "wx21b4110b18b31831",
|
||||
"MchId": "1737943225",
|
||||
"ApiV3Key": "1230uaPcnzdh3lkxjcoiddUBXddWkpx2",
|
||||
"CertSerialNo": "429F8544BF89D61B1A986432776A5C7E5C4B1DAA",
|
||||
"PrivateKeyPath": "apiclient_key.pem",
|
||||
"CertPath": "apiclient_cert.pem",
|
||||
"PlatformCertPath": "pub_key.pem",
|
||||
"PlatformCertSerialNo": "PUB_KEY_ID_0117379432252026012200382382002003",
|
||||
"NotifyUrl": "https://app.zpc-xy.com/xyqj/api/pay/notify"
|
||||
},
|
||||
"Storage": {
|
||||
"Provider": "TencentCos",
|
||||
"Local": {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
namespace XiangYi.Application.DTOs.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// 创建会员订单请求
|
||||
/// </summary>
|
||||
public class CreateMemberOrderRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 会员等级:1永久会员 2诚意会员 3家庭版会员
|
||||
/// </summary>
|
||||
public int MemberLevel { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 微信支付回调请求
|
||||
/// </summary>
|
||||
public class WeChatPayNotifyRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 通知ID
|
||||
/// </summary>
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 通知创建时间
|
||||
/// </summary>
|
||||
public string Create_time { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 通知类型
|
||||
/// </summary>
|
||||
public string Event_type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 通知数据类型
|
||||
/// </summary>
|
||||
public string Resource_type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 通知数据
|
||||
/// </summary>
|
||||
public WeChatPayNotifyResource Resource { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 回调摘要
|
||||
/// </summary>
|
||||
public string Summary { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 微信支付回调资源数据
|
||||
/// </summary>
|
||||
public class WeChatPayNotifyResource
|
||||
{
|
||||
/// <summary>
|
||||
/// 加密算法类型
|
||||
/// </summary>
|
||||
public string Algorithm { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 数据密文
|
||||
/// </summary>
|
||||
public string Ciphertext { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 附加数据
|
||||
/// </summary>
|
||||
public string Associated_data { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 原始类型
|
||||
/// </summary>
|
||||
public string Original_type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 随机串
|
||||
/// </summary>
|
||||
public string Nonce { get; set; } = string.Empty;
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
namespace XiangYi.Application.DTOs.Responses;
|
||||
|
||||
/// <summary>
|
||||
/// 创建订单响应
|
||||
/// </summary>
|
||||
public class CreateOrderResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 订单号
|
||||
/// </summary>
|
||||
public string OrderNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 订单金额(元)
|
||||
/// </summary>
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 小程序调起支付参数
|
||||
/// </summary>
|
||||
public PaymentParams? PaymentParams { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 小程序支付参数
|
||||
/// </summary>
|
||||
public class PaymentParams
|
||||
{
|
||||
/// <summary>
|
||||
/// 时间戳
|
||||
/// </summary>
|
||||
public string TimeStamp { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 随机字符串
|
||||
/// </summary>
|
||||
public string NonceStr { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 订单详情扩展字符串
|
||||
/// </summary>
|
||||
public string Package { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 签名方式
|
||||
/// </summary>
|
||||
public string SignType { get; set; } = "RSA";
|
||||
|
||||
/// <summary>
|
||||
/// 签名
|
||||
/// </summary>
|
||||
public string PaySign { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
// OrderDetailResponse 和 MemberTierResponse 已在 OrderResponses.cs 和 MemberResponses.cs 中定义
|
||||
|
|
@ -47,6 +47,7 @@ public static class ServiceCollectionExtensions
|
|||
services.AddScoped<ISearchService, SearchService>();
|
||||
services.AddScoped<ISensitiveWordService, SensitiveWordService>();
|
||||
services.AddScoped<ISystemConfigService, SystemConfigService>();
|
||||
services.AddScoped<IPaymentService, PaymentService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
|
|
|||
53
server/src/XiangYi.Application/Interfaces/IPaymentService.cs
Normal file
53
server/src/XiangYi.Application/Interfaces/IPaymentService.cs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
using XiangYi.Application.DTOs.Requests;
|
||||
using XiangYi.Application.DTOs.Responses;
|
||||
|
||||
namespace XiangYi.Application.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// 支付服务接口
|
||||
/// </summary>
|
||||
public interface IPaymentService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取会员等级配置列表
|
||||
/// </summary>
|
||||
Task<List<MemberTierResponse>> GetMemberTiersAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 创建会员订单
|
||||
/// </summary>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <param name="request">创建订单请求</param>
|
||||
/// <returns>订单信息和支付参数</returns>
|
||||
Task<CreateOrderResponse> CreateMemberOrderAsync(long userId, CreateMemberOrderRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// 处理微信支付回调
|
||||
/// </summary>
|
||||
/// <param name="orderNo">商户订单号</param>
|
||||
/// <param name="transactionId">微信支付订单号</param>
|
||||
/// <param name="payTime">支付时间</param>
|
||||
/// <returns>是否处理成功</returns>
|
||||
Task<bool> HandlePaymentSuccessAsync(string orderNo, string transactionId, DateTime payTime);
|
||||
|
||||
/// <summary>
|
||||
/// 查询订单详情
|
||||
/// </summary>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <param name="orderNo">订单号</param>
|
||||
Task<OrderDetailResponse?> GetOrderDetailAsync(long userId, string orderNo);
|
||||
|
||||
/// <summary>
|
||||
/// 查询订单支付状态(主动查询微信)
|
||||
/// </summary>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <param name="orderNo">订单号</param>
|
||||
Task<OrderDetailResponse?> QueryOrderPayStatusAsync(long userId, string orderNo);
|
||||
|
||||
/// <summary>
|
||||
/// 取消订单
|
||||
/// </summary>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <param name="orderNo">订单号</param>
|
||||
Task<bool> CancelOrderAsync(long userId, string orderNo);
|
||||
}
|
||||
318
server/src/XiangYi.Application/Services/PaymentService.cs
Normal file
318
server/src/XiangYi.Application/Services/PaymentService.cs
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using XiangYi.Application.DTOs.Requests;
|
||||
using XiangYi.Application.DTOs.Responses;
|
||||
using XiangYi.Application.Interfaces;
|
||||
using XiangYi.Core.Constants;
|
||||
using XiangYi.Core.Entities.Biz;
|
||||
using XiangYi.Core.Exceptions;
|
||||
using XiangYi.Core.Interfaces;
|
||||
using XiangYi.Infrastructure.Payment;
|
||||
|
||||
namespace XiangYi.Application.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 支付服务实现
|
||||
/// </summary>
|
||||
public class PaymentService : IPaymentService
|
||||
{
|
||||
private readonly IRepository<Order> _orderRepository;
|
||||
private readonly IRepository<Member> _memberRepository;
|
||||
private readonly IRepository<MemberTierConfig> _tierConfigRepository;
|
||||
private readonly IRepository<User> _userRepository;
|
||||
private readonly IWeChatPayService _weChatPayService;
|
||||
private readonly ILogger<PaymentService> _logger;
|
||||
|
||||
public PaymentService(
|
||||
IRepository<Order> orderRepository,
|
||||
IRepository<Member> memberRepository,
|
||||
IRepository<MemberTierConfig> tierConfigRepository,
|
||||
IRepository<User> userRepository,
|
||||
IWeChatPayService weChatPayService,
|
||||
ILogger<PaymentService> logger)
|
||||
{
|
||||
_orderRepository = orderRepository;
|
||||
_memberRepository = memberRepository;
|
||||
_tierConfigRepository = tierConfigRepository;
|
||||
_userRepository = userRepository;
|
||||
_weChatPayService = weChatPayService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<List<MemberTierResponse>> GetMemberTiersAsync()
|
||||
{
|
||||
var tiers = await _tierConfigRepository.GetListAsync(t => t.Status == 1);
|
||||
return tiers.OrderBy(t => t.Sort).Select(t => new MemberTierResponse
|
||||
{
|
||||
Level = t.Level,
|
||||
Name = t.Name,
|
||||
Badge = t.Badge,
|
||||
Price = t.Price,
|
||||
OriginalPrice = t.OriginalPrice,
|
||||
Discount = t.Discount,
|
||||
BenefitsImage = t.BenefitsImage
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CreateOrderResponse> CreateMemberOrderAsync(long userId, CreateMemberOrderRequest request)
|
||||
{
|
||||
// 获取会员等级配置
|
||||
var tierConfigs = await _tierConfigRepository.GetListAsync(t => t.Level == request.MemberLevel && t.Status == 1);
|
||||
var tierConfig = tierConfigs.FirstOrDefault();
|
||||
if (tierConfig == null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.InvalidParameter, "无效的会员等级");
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
var user = await _userRepository.GetByIdAsync(userId);
|
||||
if (user == null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.UserNotFound, "用户不存在");
|
||||
}
|
||||
|
||||
// 生成订单号
|
||||
var orderNo = GenerateOrderNo();
|
||||
|
||||
// 创建订单
|
||||
var order = new Order
|
||||
{
|
||||
OrderNo = orderNo,
|
||||
UserId = userId,
|
||||
OrderType = 1, // 会员订单
|
||||
ProductName = tierConfig.Name,
|
||||
Amount = tierConfig.Price,
|
||||
PayAmount = tierConfig.Price,
|
||||
Status = 1, // 待支付
|
||||
ExpireTime = DateTime.Now.AddMinutes(30), // 30分钟过期
|
||||
CreateTime = DateTime.Now,
|
||||
UpdateTime = DateTime.Now
|
||||
};
|
||||
|
||||
await _orderRepository.AddAsync(order);
|
||||
_logger.LogInformation("创建会员订单: OrderNo={OrderNo}, UserId={UserId}, Amount={Amount}",
|
||||
orderNo, userId, tierConfig.Price);
|
||||
|
||||
// 调用微信支付创建预支付订单
|
||||
var amountInFen = (int)(tierConfig.Price * 100); // 转换为分
|
||||
var payResult = await _weChatPayService.CreateJsApiOrderAsync(
|
||||
orderNo,
|
||||
amountInFen,
|
||||
tierConfig.Name,
|
||||
user.OpenId ?? "");
|
||||
|
||||
if (!payResult.Success)
|
||||
{
|
||||
_logger.LogError("创建微信支付订单失败: {Error}", payResult.ErrorMessage);
|
||||
throw new BusinessException(ErrorCodes.PaymentFailed, payResult.ErrorMessage ?? "创建支付订单失败");
|
||||
}
|
||||
|
||||
return new CreateOrderResponse
|
||||
{
|
||||
OrderNo = orderNo,
|
||||
Amount = tierConfig.Price,
|
||||
PaymentParams = new PaymentParams
|
||||
{
|
||||
TimeStamp = payResult.TimeStamp!,
|
||||
NonceStr = payResult.NonceStr!,
|
||||
Package = payResult.Package!,
|
||||
SignType = payResult.SignType!,
|
||||
PaySign = payResult.PaySign!
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> HandlePaymentSuccessAsync(string orderNo, string transactionId, DateTime payTime)
|
||||
{
|
||||
// 查询订单
|
||||
var orders = await _orderRepository.GetListAsync(o => o.OrderNo == orderNo);
|
||||
var order = orders.FirstOrDefault();
|
||||
if (order == null)
|
||||
{
|
||||
_logger.LogWarning("支付回调订单不存在: {OrderNo}", orderNo);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查订单状态,避免重复处理
|
||||
if (order.Status == 2)
|
||||
{
|
||||
_logger.LogInformation("订单已支付,跳过处理: {OrderNo}", orderNo);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 更新订单状态
|
||||
order.Status = 2; // 已支付
|
||||
order.PayType = 1; // 微信支付
|
||||
order.PayTime = payTime;
|
||||
order.TransactionId = transactionId;
|
||||
order.UpdateTime = DateTime.Now;
|
||||
await _orderRepository.UpdateAsync(order);
|
||||
|
||||
_logger.LogInformation("订单支付成功: OrderNo={OrderNo}, TransactionId={TransactionId}", orderNo, transactionId);
|
||||
|
||||
// 处理会员开通
|
||||
if (order.OrderType == 1)
|
||||
{
|
||||
await ActivateMembershipAsync(order);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 激活会员
|
||||
/// </summary>
|
||||
private async Task ActivateMembershipAsync(Order order)
|
||||
{
|
||||
// 获取会员等级配置
|
||||
var tierConfigs = await _tierConfigRepository.GetListAsync(t => t.Name == order.ProductName);
|
||||
var tierConfig = tierConfigs.FirstOrDefault();
|
||||
var memberLevel = tierConfig?.Level ?? 1;
|
||||
|
||||
// 检查是否已有会员记录
|
||||
var existingMembers = await _memberRepository.GetListAsync(m => m.UserId == order.UserId && m.Status == 1);
|
||||
var existingMember = existingMembers.FirstOrDefault();
|
||||
|
||||
DateTime? expireTime = memberLevel switch
|
||||
{
|
||||
1 => null, // 永久会员
|
||||
2 => DateTime.Now.AddMonths(1), // 诚意会员1个月
|
||||
3 => DateTime.Now.AddYears(1), // 家庭版1年
|
||||
_ => DateTime.Now.AddMonths(1)
|
||||
};
|
||||
|
||||
if (existingMember != null)
|
||||
{
|
||||
// 更新现有会员
|
||||
existingMember.MemberLevel = memberLevel;
|
||||
existingMember.OrderId = order.Id;
|
||||
existingMember.StartTime = DateTime.Now;
|
||||
existingMember.ExpireTime = expireTime;
|
||||
existingMember.UpdateTime = DateTime.Now;
|
||||
await _memberRepository.UpdateAsync(existingMember);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 创建新会员记录
|
||||
var member = new Member
|
||||
{
|
||||
UserId = order.UserId,
|
||||
MemberLevel = memberLevel,
|
||||
OrderId = order.Id,
|
||||
StartTime = DateTime.Now,
|
||||
ExpireTime = expireTime,
|
||||
Status = 1,
|
||||
CreateTime = DateTime.Now,
|
||||
UpdateTime = DateTime.Now
|
||||
};
|
||||
await _memberRepository.AddAsync(member);
|
||||
}
|
||||
|
||||
// 更新用户会员状态
|
||||
var user = await _userRepository.GetByIdAsync(order.UserId);
|
||||
if (user != null)
|
||||
{
|
||||
user.IsMember = true;
|
||||
user.MemberLevel = memberLevel;
|
||||
user.MemberExpireTime = expireTime;
|
||||
user.UpdateTime = DateTime.Now;
|
||||
await _userRepository.UpdateAsync(user);
|
||||
}
|
||||
|
||||
_logger.LogInformation("会员开通成功: UserId={UserId}, Level={Level}, ExpireTime={ExpireTime}",
|
||||
order.UserId, memberLevel, expireTime);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<OrderDetailResponse?> GetOrderDetailAsync(long userId, string orderNo)
|
||||
{
|
||||
var orders = await _orderRepository.GetListAsync(o => o.OrderNo == orderNo && o.UserId == userId);
|
||||
var order = orders.FirstOrDefault();
|
||||
if (order == null) return null;
|
||||
|
||||
return MapToOrderDetail(order);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<OrderDetailResponse?> QueryOrderPayStatusAsync(long userId, string orderNo)
|
||||
{
|
||||
var orders = await _orderRepository.GetListAsync(o => o.OrderNo == orderNo && o.UserId == userId);
|
||||
var order = orders.FirstOrDefault();
|
||||
if (order == null) return null;
|
||||
|
||||
// 如果订单已支付,直接返回
|
||||
if (order.Status == 2)
|
||||
{
|
||||
return MapToOrderDetail(order);
|
||||
}
|
||||
|
||||
// 主动查询微信支付状态
|
||||
var queryResult = await _weChatPayService.QueryOrderAsync(orderNo);
|
||||
if (queryResult != null && queryResult.TradeState == "SUCCESS")
|
||||
{
|
||||
// 支付成功,更新订单
|
||||
await HandlePaymentSuccessAsync(orderNo, queryResult.TransactionId!, queryResult.SuccessTime ?? DateTime.Now);
|
||||
|
||||
// 重新查询订单
|
||||
orders = await _orderRepository.GetListAsync(o => o.OrderNo == orderNo);
|
||||
order = orders.FirstOrDefault();
|
||||
}
|
||||
|
||||
return order != null ? MapToOrderDetail(order) : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> CancelOrderAsync(long userId, string orderNo)
|
||||
{
|
||||
var orders = await _orderRepository.GetListAsync(o => o.OrderNo == orderNo && o.UserId == userId);
|
||||
var order = orders.FirstOrDefault();
|
||||
if (order == null) return false;
|
||||
|
||||
// 只能取消待支付的订单
|
||||
if (order.Status != 1)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.InvalidOperation, "只能取消待支付的订单");
|
||||
}
|
||||
|
||||
// 关闭微信支付订单
|
||||
await _weChatPayService.CloseOrderAsync(orderNo);
|
||||
|
||||
// 更新订单状态
|
||||
order.Status = 3; // 已取消
|
||||
order.UpdateTime = DateTime.Now;
|
||||
await _orderRepository.UpdateAsync(order);
|
||||
|
||||
_logger.LogInformation("订单已取消: OrderNo={OrderNo}", orderNo);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成订单号
|
||||
/// </summary>
|
||||
private static string GenerateOrderNo()
|
||||
{
|
||||
return $"XY{DateTime.Now:yyyyMMddHHmmss}{Random.Shared.Next(1000, 9999)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 映射订单详情
|
||||
/// </summary>
|
||||
private static OrderDetailResponse MapToOrderDetail(Order order)
|
||||
{
|
||||
return new OrderDetailResponse
|
||||
{
|
||||
OrderId = order.Id,
|
||||
OrderNo = order.OrderNo,
|
||||
OrderType = order.OrderType,
|
||||
ProductName = order.ProductName,
|
||||
Amount = order.Amount,
|
||||
PayAmount = order.PayAmount,
|
||||
Status = order.Status,
|
||||
PayTime = order.PayTime,
|
||||
CreateTime = order.CreateTime
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -58,6 +58,8 @@ public static class ErrorCodes
|
|||
public const int CannotDeleteSelf = 40030;
|
||||
public const int PermissionNotFound = 40031;
|
||||
public const int OrderCannotDelete = 40032;
|
||||
public const int PaymentFailed = 40033;
|
||||
public const int InvalidOperation = 40034;
|
||||
#endregion
|
||||
|
||||
#region 第三方服务错误 50000-59999
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using XiangYi.Infrastructure.Cache;
|
||||
using XiangYi.Infrastructure.Payment;
|
||||
using XiangYi.Infrastructure.RealName;
|
||||
using XiangYi.Infrastructure.Sms;
|
||||
using XiangYi.Infrastructure.Storage;
|
||||
|
|
@ -24,6 +25,10 @@ public static class InfrastructureExtensions
|
|||
{
|
||||
client.Timeout = TimeSpan.FromSeconds(30);
|
||||
});
|
||||
services.AddHttpClient("WeChatPay", client =>
|
||||
{
|
||||
client.Timeout = TimeSpan.FromSeconds(30);
|
||||
});
|
||||
|
||||
// Storage - 根据配置选择存储提供者
|
||||
services.Configure<StorageOptions>(configuration.GetSection("Storage"));
|
||||
|
|
@ -53,6 +58,10 @@ public static class InfrastructureExtensions
|
|||
services.Configure<WeChatOptions>(configuration.GetSection("WeChat"));
|
||||
services.AddScoped<IWeChatService, WeChatService>();
|
||||
|
||||
// WeChat Pay
|
||||
services.Configure<Payment.WeChatPayOptions>(configuration.GetSection("WeChatPay"));
|
||||
services.AddScoped<IWeChatPayService, WeChatPayService>();
|
||||
|
||||
// Redis Cache (使用专门的扩展方法)
|
||||
services.AddRedisCache(configuration);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,98 @@
|
|||
namespace XiangYi.Infrastructure.Payment;
|
||||
|
||||
/// <summary>
|
||||
/// 微信支付服务接口
|
||||
/// </summary>
|
||||
public interface IWeChatPayService
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建JSAPI支付订单(小程序支付)
|
||||
/// </summary>
|
||||
/// <param name="orderNo">商户订单号</param>
|
||||
/// <param name="amount">金额(分)</param>
|
||||
/// <param name="description">商品描述</param>
|
||||
/// <param name="openId">用户OpenId</param>
|
||||
/// <returns>小程序调起支付所需参数</returns>
|
||||
Task<WeChatPayResult> CreateJsApiOrderAsync(string orderNo, int amount, string description, string openId);
|
||||
|
||||
/// <summary>
|
||||
/// 查询订单
|
||||
/// </summary>
|
||||
/// <param name="orderNo">商户订单号</param>
|
||||
/// <returns>订单信息</returns>
|
||||
Task<WeChatPayQueryResult?> QueryOrderAsync(string orderNo);
|
||||
|
||||
/// <summary>
|
||||
/// 关闭订单
|
||||
/// </summary>
|
||||
/// <param name="orderNo">商户订单号</param>
|
||||
Task<bool> CloseOrderAsync(string orderNo);
|
||||
|
||||
/// <summary>
|
||||
/// 验证回调签名
|
||||
/// </summary>
|
||||
/// <param name="timestamp">时间戳</param>
|
||||
/// <param name="nonce">随机串</param>
|
||||
/// <param name="body">请求体</param>
|
||||
/// <param name="signature">签名</param>
|
||||
/// <param name="serial">证书序列号</param>
|
||||
/// <returns>是否验证通过</returns>
|
||||
bool VerifySignature(string timestamp, string nonce, string body, string signature, string serial);
|
||||
|
||||
/// <summary>
|
||||
/// 解密回调通知数据
|
||||
/// </summary>
|
||||
/// <param name="ciphertext">密文</param>
|
||||
/// <param name="nonce">随机串</param>
|
||||
/// <param name="associatedData">附加数据</param>
|
||||
/// <returns>解密后的JSON字符串</returns>
|
||||
string DecryptNotifyData(string ciphertext, string nonce, string associatedData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 微信支付结果(小程序调起支付参数)
|
||||
/// </summary>
|
||||
public class WeChatPayResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
public string? PrepayId { get; set; }
|
||||
|
||||
// 小程序调起支付所需参数
|
||||
public string? TimeStamp { get; set; }
|
||||
public string? NonceStr { get; set; }
|
||||
public string? Package { get; set; }
|
||||
public string? SignType { get; set; }
|
||||
public string? PaySign { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 微信支付查询结果
|
||||
/// </summary>
|
||||
public class WeChatPayQueryResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 交易状态:SUCCESS—支付成功, REFUND—转入退款, NOTPAY—未支付, CLOSED—已关闭, PAYERROR—支付失败
|
||||
/// </summary>
|
||||
public string TradeState { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 微信支付订单号
|
||||
/// </summary>
|
||||
public string? TransactionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商户订单号
|
||||
/// </summary>
|
||||
public string? OutTradeNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 支付完成时间
|
||||
/// </summary>
|
||||
public DateTime? SuccessTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单金额(分)
|
||||
/// </summary>
|
||||
public int? Amount { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
namespace XiangYi.Infrastructure.Payment;
|
||||
|
||||
/// <summary>
|
||||
/// 微信支付配置选项
|
||||
/// </summary>
|
||||
public class WeChatPayOptions
|
||||
{
|
||||
public const string SectionName = "WeChatPay";
|
||||
|
||||
/// <summary>
|
||||
/// 小程序AppId
|
||||
/// </summary>
|
||||
public string AppId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 商户号
|
||||
/// </summary>
|
||||
public string MchId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// APIv3密钥
|
||||
/// </summary>
|
||||
public string ApiV3Key { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 商户证书序列号
|
||||
/// </summary>
|
||||
public string CertSerialNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 商户私钥文件路径
|
||||
/// </summary>
|
||||
public string PrivateKeyPath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 商户证书文件路径
|
||||
/// </summary>
|
||||
public string CertPath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 平台公钥文件路径
|
||||
/// </summary>
|
||||
public string PlatformCertPath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 平台公钥序列号
|
||||
/// </summary>
|
||||
public string PlatformCertSerialNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 支付回调通知地址
|
||||
/// </summary>
|
||||
public string NotifyUrl { get; set; } = string.Empty;
|
||||
}
|
||||
295
server/src/XiangYi.Infrastructure/Payment/WeChatPayService.cs
Normal file
295
server/src/XiangYi.Infrastructure/Payment/WeChatPayService.cs
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace XiangYi.Infrastructure.Payment;
|
||||
|
||||
/// <summary>
|
||||
/// 微信支付服务实现
|
||||
/// </summary>
|
||||
public class WeChatPayService : IWeChatPayService
|
||||
{
|
||||
private readonly WeChatPayOptions _options;
|
||||
private readonly ILogger<WeChatPayService> _logger;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly RSA _privateKey;
|
||||
private readonly RSA? _platformPublicKey;
|
||||
|
||||
private const string BaseUrl = "https://api.mch.weixin.qq.com";
|
||||
|
||||
public WeChatPayService(
|
||||
IOptions<WeChatPayOptions> options,
|
||||
ILogger<WeChatPayService> logger,
|
||||
IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_options = options.Value;
|
||||
_logger = logger;
|
||||
_httpClient = httpClientFactory.CreateClient("WeChatPay");
|
||||
|
||||
// 加载商户私钥
|
||||
var privateKeyPath = Path.Combine(AppContext.BaseDirectory, _options.PrivateKeyPath);
|
||||
if (File.Exists(privateKeyPath))
|
||||
{
|
||||
var privateKeyPem = File.ReadAllText(privateKeyPath);
|
||||
_privateKey = RSA.Create();
|
||||
_privateKey.ImportFromPem(privateKeyPem);
|
||||
}
|
||||
else
|
||||
{
|
||||
_privateKey = RSA.Create();
|
||||
_logger.LogWarning("商户私钥文件不存在: {Path}", privateKeyPath);
|
||||
}
|
||||
|
||||
// 加载平台公钥(用于验证回调签名)
|
||||
var platformCertPath = Path.Combine(AppContext.BaseDirectory, _options.PlatformCertPath);
|
||||
if (File.Exists(platformCertPath))
|
||||
{
|
||||
var publicKeyPem = File.ReadAllText(platformCertPath);
|
||||
_platformPublicKey = RSA.Create();
|
||||
_platformPublicKey.ImportFromPem(publicKeyPem);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<WeChatPayResult> CreateJsApiOrderAsync(string orderNo, int amount, string description, string openId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = "/v3/pay/transactions/jsapi";
|
||||
var requestBody = new
|
||||
{
|
||||
appid = _options.AppId,
|
||||
mchid = _options.MchId,
|
||||
description = description,
|
||||
out_trade_no = orderNo,
|
||||
notify_url = _options.NotifyUrl,
|
||||
amount = new
|
||||
{
|
||||
total = amount,
|
||||
currency = "CNY"
|
||||
},
|
||||
payer = new
|
||||
{
|
||||
openid = openId
|
||||
}
|
||||
};
|
||||
|
||||
var jsonBody = JsonSerializer.Serialize(requestBody);
|
||||
var response = await SendRequestAsync("POST", url, jsonBody);
|
||||
|
||||
if (response == null)
|
||||
{
|
||||
return new WeChatPayResult { Success = false, ErrorMessage = "请求微信支付失败" };
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(response);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (root.TryGetProperty("prepay_id", out var prepayIdElement))
|
||||
{
|
||||
var prepayId = prepayIdElement.GetString()!;
|
||||
return GeneratePayParams(prepayId);
|
||||
}
|
||||
|
||||
var errorMsg = root.TryGetProperty("message", out var msgElement)
|
||||
? msgElement.GetString()
|
||||
: "未知错误";
|
||||
|
||||
_logger.LogError("创建微信支付订单失败: {Error}", errorMsg);
|
||||
return new WeChatPayResult { Success = false, ErrorMessage = errorMsg };
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "创建微信支付订单异常");
|
||||
return new WeChatPayResult { Success = false, ErrorMessage = ex.Message };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<WeChatPayQueryResult?> QueryOrderAsync(string orderNo)
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = $"/v3/pay/transactions/out-trade-no/{orderNo}?mchid={_options.MchId}";
|
||||
var response = await SendRequestAsync("GET", url, null);
|
||||
|
||||
if (response == null) return null;
|
||||
|
||||
using var doc = JsonDocument.Parse(response);
|
||||
var root = doc.RootElement;
|
||||
|
||||
var result = new WeChatPayQueryResult
|
||||
{
|
||||
TradeState = root.GetProperty("trade_state").GetString() ?? "",
|
||||
OutTradeNo = root.TryGetProperty("out_trade_no", out var outTradeNo) ? outTradeNo.GetString() : null,
|
||||
TransactionId = root.TryGetProperty("transaction_id", out var transId) ? transId.GetString() : null
|
||||
};
|
||||
|
||||
if (root.TryGetProperty("success_time", out var successTime))
|
||||
{
|
||||
result.SuccessTime = DateTime.Parse(successTime.GetString()!);
|
||||
}
|
||||
|
||||
if (root.TryGetProperty("amount", out var amountObj) &&
|
||||
amountObj.TryGetProperty("total", out var total))
|
||||
{
|
||||
result.Amount = total.GetInt32();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "查询微信支付订单异常: {OrderNo}", orderNo);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> CloseOrderAsync(string orderNo)
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = $"/v3/pay/transactions/out-trade-no/{orderNo}/close";
|
||||
var requestBody = new { mchid = _options.MchId };
|
||||
var jsonBody = JsonSerializer.Serialize(requestBody);
|
||||
|
||||
var response = await SendRequestAsync("POST", url, jsonBody);
|
||||
return true; // 关闭订单成功返回204,无响应体
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "关闭微信支付订单异常: {OrderNo}", orderNo);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool VerifySignature(string timestamp, string nonce, string body, string signature, string serial)
|
||||
{
|
||||
if (_platformPublicKey == null)
|
||||
{
|
||||
_logger.LogWarning("平台公钥未配置,跳过签名验证");
|
||||
return true; // 如果没有配置平台公钥,跳过验证
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var message = $"{timestamp}\n{nonce}\n{body}\n";
|
||||
var signatureBytes = Convert.FromBase64String(signature);
|
||||
var messageBytes = Encoding.UTF8.GetBytes(message);
|
||||
|
||||
return _platformPublicKey.VerifyData(
|
||||
messageBytes,
|
||||
signatureBytes,
|
||||
HashAlgorithmName.SHA256,
|
||||
RSASignaturePadding.Pkcs1);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "验证微信支付签名异常");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string DecryptNotifyData(string ciphertext, string nonce, string associatedData)
|
||||
{
|
||||
try
|
||||
{
|
||||
var key = Encoding.UTF8.GetBytes(_options.ApiV3Key);
|
||||
var nonceBytes = Encoding.UTF8.GetBytes(nonce);
|
||||
var ciphertextBytes = Convert.FromBase64String(ciphertext);
|
||||
var associatedDataBytes = Encoding.UTF8.GetBytes(associatedData);
|
||||
|
||||
// AES-256-GCM 解密
|
||||
using var aesGcm = new AesGcm(key, 16);
|
||||
|
||||
// 密文最后16字节是认证标签
|
||||
var tagSize = 16;
|
||||
var actualCiphertext = ciphertextBytes[..^tagSize];
|
||||
var tag = ciphertextBytes[^tagSize..];
|
||||
|
||||
var plaintext = new byte[actualCiphertext.Length];
|
||||
aesGcm.Decrypt(nonceBytes, actualCiphertext, tag, plaintext, associatedDataBytes);
|
||||
|
||||
return Encoding.UTF8.GetString(plaintext);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "解密微信支付回调数据异常");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成小程序调起支付所需参数
|
||||
/// </summary>
|
||||
private WeChatPayResult GeneratePayParams(string prepayId)
|
||||
{
|
||||
var timeStamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
|
||||
var nonceStr = Guid.NewGuid().ToString("N");
|
||||
var package = $"prepay_id={prepayId}";
|
||||
|
||||
// 签名字符串
|
||||
var message = $"{_options.AppId}\n{timeStamp}\n{nonceStr}\n{package}\n";
|
||||
var messageBytes = Encoding.UTF8.GetBytes(message);
|
||||
var signatureBytes = _privateKey.SignData(messageBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
var paySign = Convert.ToBase64String(signatureBytes);
|
||||
|
||||
return new WeChatPayResult
|
||||
{
|
||||
Success = true,
|
||||
PrepayId = prepayId,
|
||||
TimeStamp = timeStamp,
|
||||
NonceStr = nonceStr,
|
||||
Package = package,
|
||||
SignType = "RSA",
|
||||
PaySign = paySign
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送HTTP请求到微信支付API
|
||||
/// </summary>
|
||||
private async Task<string?> SendRequestAsync(string method, string url, string? body)
|
||||
{
|
||||
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
|
||||
var nonceStr = Guid.NewGuid().ToString("N");
|
||||
|
||||
// 构造签名串
|
||||
var signMessage = $"{method}\n{url}\n{timestamp}\n{nonceStr}\n{body ?? ""}\n";
|
||||
var signatureBytes = _privateKey.SignData(
|
||||
Encoding.UTF8.GetBytes(signMessage),
|
||||
HashAlgorithmName.SHA256,
|
||||
RSASignaturePadding.Pkcs1);
|
||||
var signature = Convert.ToBase64String(signatureBytes);
|
||||
|
||||
// 构造Authorization头
|
||||
var authorization = $"WECHATPAY2-SHA256-RSA2048 mchid=\"{_options.MchId}\",nonce_str=\"{nonceStr}\",timestamp=\"{timestamp}\",serial_no=\"{_options.CertSerialNo}\",signature=\"{signature}\"";
|
||||
|
||||
var request = new HttpRequestMessage(new HttpMethod(method), BaseUrl + url);
|
||||
request.Headers.Add("Authorization", authorization);
|
||||
request.Headers.Add("Accept", "application/json");
|
||||
request.Headers.Add("User-Agent", "XiangYi/1.0");
|
||||
|
||||
if (!string.IsNullOrEmpty(body))
|
||||
{
|
||||
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
|
||||
}
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
|
||||
if (!response.IsSuccessStatusCode && response.StatusCode != System.Net.HttpStatusCode.NoContent)
|
||||
{
|
||||
_logger.LogError("微信支付API请求失败: {StatusCode} {Body}", response.StatusCode, responseBody);
|
||||
return responseBody; // 返回错误信息
|
||||
}
|
||||
|
||||
return responseBody;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user