This commit is contained in:
18631081161 2026-01-22 19:12:22 +08:00
parent b22add232d
commit 635c708487
22 changed files with 1296 additions and 5 deletions

2
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,2 @@
{
}

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

4
admin/public/logo.svg Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -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"
>

View File

@ -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"
>

View File

@ -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
View 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
}

View File

@ -23,7 +23,7 @@ const ENV = {
}
// 当前环境 - 开发时使用 development打包时改为 production
const CURRENT_ENV = 'development'
const CURRENT_ENV = 'production'
// 导出配置
export const config = {

View 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;
}
}

View File

@ -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": {

View File

@ -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;
}

View File

@ -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 中定义

View File

@ -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;
}

View 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);
}

View 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
};
}
}

View File

@ -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

View File

@ -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);

View File

@ -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; }
}

View File

@ -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;
}

View 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;
}
}