using System.Text; using System.Text.Json; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.WebUtilities; using Tiku.Api.Contracts; using Tiku.Api.Security; using Tiku.Application.Commerce; using Tiku.Application.Security; using Tiku.Application.Tenancy; using Tiku.Domain.Commerce; namespace Tiku.Api.Controllers; [ApiController] [Tags("学生端-交易")] [Authorize(Policy = TikuPolicies.CurrentTenantMember)] [RequireSaasFeature(SaasFeatureCatalog.StudentStore)] [Produces("application/json")] [Route("api/student/commerce")] public sealed class CommerceController( ICommerceService commerceService, ICommerceAdminService commerceAdminService, ICurrentUser currentUser, ITenantContext currentTenant, ITenantContextInitializer tenantInitializer, ITenantDirectory tenantDirectory) : ControllerBase { [HttpPost("orders")] [EndpointSummary("创建学生端订单")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> CreateOrder( CreateCommerceOrderDto request, CancellationToken cancellationToken) { return Ok(await commerceService.CreateOrderAsync( ResolveActor(), request.ToCommand(), cancellationToken)); } [HttpGet("orders")] [EndpointSummary("查询当前用户订单")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> Orders( [FromQuery] CommerceOrderQueryDto query, CancellationToken cancellationToken) { return Ok(await commerceService.GetOrdersAsync( ResolveActor(), new CommerceOrderQuery(query.Limit, query.Status), cancellationToken)); } [HttpGet("orders/{orderNo}")] [EndpointSummary("查询当前用户订单详情")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> Order( string orderNo, CancellationToken cancellationToken) { return Ok(await commerceService.GetOrderAsync( ResolveActor(), orderNo, cancellationToken)); } [HttpPost("payments")] [EndpointSummary("创建订单支付")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> CreatePayment( CreateCommercePaymentDto request, CancellationToken cancellationToken) { return Ok(await commerceService.CreatePaymentAsync( ResolveActor(), request.ToCommand(), cancellationToken)); } [HttpGet("entitlements/current")] [EndpointSummary("查询当前用户权益")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> CurrentEntitlement( CancellationToken cancellationToken) { return Ok(await commerceService.GetCurrentEntitlementAsync( ResolveActor(), cancellationToken)); } [HttpPost("coupons/claim")] [EndpointSummary("领取优惠券")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> ClaimCoupon( ClaimCommerceCouponDto request, CancellationToken cancellationToken) { return Ok(await commerceService.ClaimCouponAsync( ResolveActor(), request.ToCommand(), cancellationToken)); } [HttpGet("coupons")] [EndpointSummary("查询当前用户优惠券")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> Coupons( [FromQuery] CommerceCouponQueryDto query, CancellationToken cancellationToken) { return Ok(await commerceService.GetCouponsAsync( ResolveActor(), query.ToQuery(), cancellationToken)); } [HttpPost("coupons/check")] [EndpointSummary("校验优惠券并预览订单金额")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> CheckCoupon( CheckCommerceCouponDto request, CancellationToken cancellationToken) { return Ok(await commerceService.CheckCouponAsync( ResolveActor(), request.ToCommand(), cancellationToken)); } [AllowAnonymous] [HttpPost("payments/notify/wechat-pay")] [EndpointSummary("微信支付回调")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> WechatPayNotify( [FromQuery] string? tenantCode, CancellationToken cancellationToken) { return Ok(await ProcessNotificationAsync( await ResolveNotificationTenantAsync(tenantCode, cancellationToken), PaymentProviders.WechatPay, cancellationToken)); } [AllowAnonymous] [HttpPost("payments/notify/alipay")] [EndpointSummary("支付宝支付回调")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> AlipayNotify( [FromQuery] string? tenantCode, CancellationToken cancellationToken) { return Ok(await ProcessNotificationAsync( await ResolveNotificationTenantAsync(tenantCode, cancellationToken), PaymentProviders.Alipay, cancellationToken)); } [AllowAnonymous] [HttpPost("refunds/notify/wechat_pay")] [EndpointSummary("微信退款结果回调")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> WechatRefundNotify( [FromQuery] string? tenantCode, RefundNotificationDto request, CancellationToken cancellationToken) { return Ok(await commerceAdminService.ProcessRefundNotificationAsync( await ResolveNotificationTenantAsync(tenantCode, cancellationToken), request.ToCommand(PaymentProviders.WechatPay), cancellationToken)); } [AllowAnonymous] [HttpPost("refunds/notify/alipay")] [EndpointSummary("支付宝退款结果回调")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> AlipayRefundNotify( [FromQuery] string? tenantCode, RefundNotificationDto request, CancellationToken cancellationToken) { return Ok(await commerceAdminService.ProcessRefundNotificationAsync( await ResolveNotificationTenantAsync(tenantCode, cancellationToken), request.ToCommand(PaymentProviders.Alipay), cancellationToken)); } private async Task ResolveNotificationTenantAsync( string? tenantCode, CancellationToken cancellationToken) { if (currentTenant.TenantId.HasValue) return currentTenant.TenantId.Value; if (string.IsNullOrWhiteSpace(tenantCode)) throw new CommerceException("Tenant code is required for payment notification.", "tenant_required"); var tenant = await tenantDirectory.FindByCodeAsync(tenantCode.Trim(), cancellationToken) ?? throw new CommerceException("Tenant was not found.", "tenant_not_found"); tenantInitializer.Initialize(tenant.TenantId, tenant.TenantCode, TenantResolutionSource.TenantCode); return tenant.TenantId; } private CommerceActor ResolveActor() { if (currentTenant.TenantId is null || currentUser.UserId is null) throw new CommerceException("Current commerce actor was not resolved.", "commerce_access_denied"); return new CommerceActor(currentTenant.TenantId.Value, currentUser.UserId.Value); } private async Task ProcessNotificationAsync( Guid tenantId, string provider, CancellationToken cancellationToken) { if (tenantId == Guid.Empty) throw new CommerceException("Tenant id is required for payment notification.", "tenant_required"); var rawBody = await ReadRawBodyAsync(Request, cancellationToken); using var body = ParseNotificationBody(Request, rawBody, cancellationToken); var headers = Request.Headers.ToDictionary( pair => pair.Key, pair => pair.Value.ToString(), StringComparer.OrdinalIgnoreCase); return await commerceService.ProcessPaymentNotificationAsync( tenantId, provider, headers, rawBody, body.RootElement.Clone(), cancellationToken); } private static async Task ReadRawBodyAsync(HttpRequest request, CancellationToken cancellationToken) { using var reader = new StreamReader( request.Body, Encoding.UTF8, false, leaveOpen: false); return await reader.ReadToEndAsync(cancellationToken); } private static JsonDocument ParseNotificationBody( HttpRequest request, string rawBody, CancellationToken cancellationToken) { if (request.HasFormContentType) { _ = cancellationToken; var form = QueryHelpers.ParseQuery(rawBody); var values = form.ToDictionary( pair => pair.Key, pair => pair.Value.ToString(), StringComparer.Ordinal); return JsonDocument.Parse(JsonSerializer.Serialize(values)); } if (string.IsNullOrWhiteSpace(rawBody)) return JsonDocument.Parse("{}"); return JsonDocument.Parse(rawBody); } }