using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; using System.ComponentModel.DataAnnotations; using TakeoutSaaS.Application.App.Billings.Commands; using TakeoutSaaS.Application.App.Billings.Dto; using TakeoutSaaS.Application.App.Billings.Queries; using TakeoutSaaS.Module.Authorization.Attributes; using TakeoutSaaS.Shared.Abstractions.Results; using TakeoutSaaS.Shared.Web.Api; namespace TakeoutSaaS.AdminApi.Controllers; /// /// 账单管理。 /// [ApiVersion("1.0")] [Authorize] [Route("api/admin/v{version:apiVersion}/billings")] public sealed class BillingsController(IMediator mediator) : BaseApiController { /// /// 分页查询账单列表。 /// /// 账单分页结果。 [HttpGet] [PermissionAuthorize("bill:read")] [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] public async Task>> GetList([FromQuery] GetBillingListQuery query, CancellationToken cancellationToken) { // 1. 查询账单列表 var result = await mediator.Send(query, cancellationToken); // 2. 返回分页结果 return ApiResponse>.Ok(result); } /// /// 获取账单详情。 /// /// 账单 ID。 /// 取消标记。 /// 账单详情。 [HttpGet("{id:long}")] [PermissionAuthorize("bill:read")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> GetDetail(long id, CancellationToken cancellationToken) { // 1. 查询账单详情(若不存在则抛出业务异常,由全局异常处理转换为 404) var result = await mediator.Send(new GetBillingDetailQuery { BillingId = id }, cancellationToken); // 2. 返回详情 return ApiResponse.Ok(result); } /// /// 手动创建账单。 /// /// 创建账单命令。 /// 取消标记。 /// 创建的账单信息。 [HttpPost] [PermissionAuthorize("bill:create")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task> Create([FromBody, Required] CreateBillingCommand command, CancellationToken cancellationToken) { // 1. 创建账单 var result = await mediator.Send(command, cancellationToken); // 2. 返回创建结果 return ApiResponse.Ok(result); } /// /// 更新账单状态。 /// /// 账单 ID。 /// 更新状态命令。 /// 取消标记。 /// 更新结果。 [HttpPut("{id:long}/status")] [PermissionAuthorize("bill:update")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> UpdateStatus(long id, [FromBody, Required] UpdateBillingStatusCommand command, CancellationToken cancellationToken) { // 1. 绑定账单标识 command = command with { BillingId = id }; // 2. 更新账单状态(若不存在则抛出业务异常,由全局异常处理转换为 404) await mediator.Send(command, cancellationToken); // 3. 返回成功结果 return ApiResponse.Ok(null); } /// /// 取消账单。 /// /// 账单 ID。 /// 取消原因(可选)。 /// 取消标记。 /// 取消结果。 [HttpDelete("{id:long}")] [PermissionAuthorize("bill:delete")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> Cancel(long id, [FromQuery] string? reason, CancellationToken cancellationToken) { // 1. 取消账单(取消原因支持可选) await mediator.Send(new CancelBillingCommand { BillingId = id, Reason = reason ?? string.Empty }, cancellationToken); // 2. 返回成功结果 return ApiResponse.Ok(null); } /// /// 获取账单支付记录。 /// /// 账单 ID。 /// 取消标记。 /// 支付记录列表。 [HttpGet("{id:long}/payments")] [PermissionAuthorize("bill:read")] [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] public async Task>> GetPayments(long id, CancellationToken cancellationToken) { // 1. 查询支付记录 var result = await mediator.Send(new GetBillingPaymentsQuery { BillingId = id }, cancellationToken); // 2. 返回列表 return ApiResponse>.Ok(result); } /// /// 记录支付(线下支付确认)。 /// /// 账单 ID。 /// 记录支付命令。 /// 取消标记。 /// 支付记录信息。 [HttpPost("{id:long}/payments")] [PermissionAuthorize("bill:pay")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> RecordPayment(long id, [FromBody, Required] RecordPaymentCommand command, CancellationToken cancellationToken) { // 1. 绑定账单标识 command = command with { BillingId = id }; // 2. 记录支付 var result = await mediator.Send(command, cancellationToken); // 3. 返回支付记录 return ApiResponse.Ok(result); } /// /// 一键确认收款(记录支付 + 立即审核通过)。 /// /// 账单 ID。 /// 确认收款命令。 /// 取消标记。 /// 确认后的支付记录。 [HttpPost("{id:long}/payments/confirm")] [PermissionAuthorize("bill:pay")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> ConfirmPayment(long id, [FromBody, Required] ConfirmPaymentCommand command, CancellationToken cancellationToken) { // 1. 绑定账单标识 command = command with { BillingId = id }; // 2. 一键确认收款(含:写入 VerifiedBy/VerifiedAt,并同步更新账单已收金额/状态) var result = await mediator.Send(command, cancellationToken); // 3. 返回结果 return ApiResponse.Ok(result); } /// /// 审核支付记录。 /// /// 支付记录 ID。 /// 审核参数。 /// 取消标记。 /// 审核后的支付记录。 [HttpPut("payments/{paymentId:long}/verify")] [PermissionAuthorize("bill:update")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> VerifyPayment(long paymentId, [FromBody, Required] VerifyPaymentCommand command, CancellationToken cancellationToken) { // 1. 绑定支付记录标识 command = command with { PaymentId = paymentId }; // 2. 审核支付记录 var result = await mediator.Send(command, cancellationToken); // 3. 返回审核结果 return ApiResponse.Ok(result); } /// /// 批量更新账单状态。 /// /// 批量更新命令。 /// 取消标记。 /// 更新条数。 [HttpPost("batch/status")] [PermissionAuthorize("bill:update")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task> BatchUpdateStatus([FromBody, Required] BatchUpdateStatusCommand command, CancellationToken cancellationToken) { // 1. 执行批量更新 var affected = await mediator.Send(command, cancellationToken); // 2. 返回更新条数 return ApiResponse.Ok(affected); } /// /// 导出账单(Excel/PDF/CSV)。 /// /// 导出请求。 /// 取消标记。 /// 导出文件。 [HttpPost("export")] [PermissionAuthorize("bill:read")] [Produces("application/octet-stream")] [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] public async Task Export([FromBody, Required] ExportBillingsQuery query, CancellationToken cancellationToken) { // 1. 执行导出 var bytes = await mediator.Send(query, cancellationToken); // 2. 解析格式并生成文件名 var extension = ResolveExportFileExtension(query.Format); var fileName = $"billings_{DateTime.UtcNow:yyyyMMdd_HHmmss}.{extension}"; // 3. 显式写入 Content-Disposition,确保浏览器以附件形式下载 Response.Headers[HeaderNames.ContentDisposition] = new ContentDispositionHeaderValue("attachment") { FileName = fileName, FileNameStar = fileName }.ToString(); // 4. 返回二进制流(统一 octet-stream,避免被默认 JSON Produces 影响) return File(bytes, "application/octet-stream"); } /// /// 获取账单统计数据。 /// /// 统计查询参数。 /// 取消标记。 /// 统计结果。 [HttpGet("statistics")] [PermissionAuthorize("bill:read")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] public async Task> Statistics([FromQuery] GetBillingStatisticsQuery query, CancellationToken cancellationToken) { // 1. 查询统计数据 var result = await mediator.Send(query, cancellationToken); // 2. 返回统计结果 return ApiResponse.Ok(result); } /// /// 获取逾期账单列表。 /// /// 逾期列表查询参数。 /// 取消标记。 /// 逾期账单分页结果。 [HttpGet("overdue")] [PermissionAuthorize("bill:read")] [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] public async Task>> Overdue([FromQuery] GetOverdueBillingsQuery query, CancellationToken cancellationToken) { // 1. 查询逾期账单分页列表 var result = await mediator.Send(query, cancellationToken); // 2. 返回分页结果 return ApiResponse>.Ok(result); } private static string ResolveExportFileExtension(string? format) { // 1. 归一化导出格式 var normalized = (format ?? string.Empty).Trim(); // 2. 映射扩展名 return normalized.ToUpperInvariant() switch { "PDF" => "pdf", "CSV" => "csv", _ => "xlsx" }; } }