using System.Linq.Expressions; using Fake.Application; using Fake.Domain.Exceptions; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using MiniExcelLibs; using Oms.Application.Dtos.Recharge; using Oms.Application.Dtos.Shared; using Oms.Application.Shared; using Oms.Domain.CustomersAggregate.Repositories; using Oms.Domain.RechargeAggregate.Entities; using Oms.Domain.RechargeAggregate.Enums; using Oms.Domain.RechargeAggregate.Repositories; namespace Oms.Application.Services.Recharge; /// /// 充值相关 API 服务。 /// [ApiExplorerSettings(GroupName = "OMS-Recharge")] public class RechargeService : ApplicationService { private IRechargeRecordRepository RechargeRecordRepository => LazyServiceProvider.GetRequiredService(); private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService(); private IWebHostEnvironment WebHostEnvironment => LazyServiceProvider.GetRequiredService(); /// /// 提交充值申请。 /// /// 入参:multipart 表单,截图字段 rechargeScreenshot。 /// 充值记录结果。 [HttpPost("/recharge/recharge/submit")] [Consumes("multipart/form-data")] public async Task SubmitAsync([FromForm] SubmitRechargeFormInput input) { var customer = await OmsSessionHelper.ResolveSessionCustomerAsync( CustomerRepository, CurrentUser, input.CustomerId); if (input.RechargeAmount < 0.01M) { throw new BusinessException("充值金额不能小于 0.01"); } var screenshotPath = await OmsFileUploadHelper.SaveRechargeScreenshotAsync( input.RechargeScreenshot, WebHostEnvironment); var entity = new RechargeRecord { CustomerId = customer.Id, SerialNumber = await GenerateSerialNumberAsync(), RechargeTime = DateTime.Now, PayType = input.PayType, RechargeAmount = input.RechargeAmount, RechargeScreenshot = screenshotPath, RechargeStatus = RechargeStatus.Pending, Remark = input.Remark }; var savedEntity = await RechargeRecordRepository.InsertAsync(entity); return ObjectMapper.Map(savedEntity); } /// /// 审核通过充值记录。 /// /// 入参:充值审核通过参数。 /// 无返回体,仅完成审核通过充值记录。 /// /// 核心业务逻辑: /// 1. 查询并校验目标对象及当前状态。 /// 2. 执行业务状态流转或敏感字段更新。 /// 3. 写入审计字段并保存结果。 /// [HttpPost("/recharge/recharge-record/approve")] public async Task ApproveAsync(ApproveRechargeInput input) { var record = await GetRechargeRecordOrThrowAsync(input.Id); if (record.RechargeStatus != RechargeStatus.Pending) { throw new BusinessException("只有待审核记录才能审核通过"); } var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == record.CustomerId); if (customer == null) { throw new BusinessException("客户不存在"); } var now = DateTime.Now; record.RechargeStatus = RechargeStatus.Approved; record.Remark = input.Remark; record.AuditTime = now; record.AuditBy = CurrentUser.UserName; customer.Balance += record.RechargeAmount; await RechargeRecordRepository.UpdateAsync(record); await CustomerRepository.UpdateAsync(customer); } /// /// 驳回充值记录。 /// /// 入参:充值审核驳回参数。 /// 无返回体,仅完成驳回充值记录。 /// /// 核心业务逻辑: /// 1. 查询并校验目标对象及当前状态。 /// 2. 执行业务状态流转或敏感字段更新。 /// 3. 写入审计字段并保存结果。 /// [HttpPost("/recharge/recharge-record/reject")] public async Task RejectAsync(RejectRechargeInput input) { var record = await GetRechargeRecordOrThrowAsync(input.Id); if (record.RechargeStatus != RechargeStatus.Pending) { throw new BusinessException("只有待审核记录才能驳回"); } record.RechargeStatus = RechargeStatus.Rejected; record.Remark = input.Remark; record.AuditTime = DateTime.Now; record.AuditBy = CurrentUser.UserName; await RechargeRecordRepository.UpdateAsync(record); } /// /// 查询充值记录列表。 /// /// 入参:充值记录分页查询条件。 /// 充值记录分页结果,包含列表数据和总记录数。 /// /// 核心业务逻辑: /// 1. 根据查询条件构造筛选规则。 /// 2. 查询目标数据并补齐关联展示字段。 /// 3. 按接口约定返回列表结果。 /// [HttpGet("/recharge/recharge-record/list")] public async Task> GetPaginatedListAsync([FromQuery] RechargeRecordPaginatedInput input) { var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync( CustomerRepository, CurrentUser, input.CustomerId); var predicate = BuildPaginatedPredicate(input, customerId); var pageIndex = OmsDocHelper.NormalizePage(input.Page); var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize); var entities = await RechargeRecordRepository.GetPagedListAsync( predicate, pageIndex, pageSize, new Dictionary { [nameof(RechargeRecord.RechargeTime)] = false, [nameof(RechargeRecord.Id)] = false }); var totalCount = await RechargeRecordRepository.CountAsync(predicate); var items = new List(entities.Count); var customerIds = entities.Select(x => x.CustomerId).Distinct().ToArray(); var customers = customerIds.Length == 0 ? [] : await CustomerRepository.GetListAsync(x => customerIds.Contains(x.Id)); var customerMap = customers.ToDictionary(x => x.Id); // 列表返回时补齐客户信息,避免前端为了展示字段额外发起关联查询。 foreach (var entity in entities) { var output = ObjectMapper.Map(entity); output.PayTypeName = GetPayTypeName(entity.PayType); output.RechargeStatusName = GetRechargeStatusName(entity.RechargeStatus); if (customerMap.TryGetValue(entity.CustomerId, out var customer)) { output.CustomerCode = customer.CustomerCode; output.CustomerPhone = customer.Phone; } output.UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime); items.Add(output); } return new OmsPageResult { Items = items, Total = totalCount }; } /// /// 导出充值记录。 /// /// 入参:充值记录导出筛选条件。 /// 文件流响应。 /// /// 核心业务逻辑: /// 1. 根据筛选条件查询待导出数据或定位模板资源。 /// 2. 生成文件内容或组装下载响应。 /// 3. 以文件形式返回结果。 /// [HttpGet("/recharge/export")] public async Task ExportAsync([FromQuery] RechargeExportInput input) { var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync( CustomerRepository, CurrentUser, input.CustomerId); var records = await RechargeRecordRepository.GetListAsync(BuildExportPredicate(input, customerId)); var customerIds = records.Select(x => x.CustomerId).Distinct().ToArray(); var customers = customerIds.Length == 0 ? [] : await CustomerRepository.GetListAsync(x => customerIds.Contains(x.Id)); var customerMap = customers.ToDictionary(x => x.Id); var exportRows = records.Select(record => new { record.SerialNumber, record.CustomerId, CustomerCode = customerMap.TryGetValue(record.CustomerId, out var customer) ? customer.CustomerCode : null, CustomerPhone = customerMap.TryGetValue(record.CustomerId, out customer) ? customer.Phone : null, record.RechargeTime, record.PayType, PayTypeName = GetPayTypeName(record.PayType), record.RechargeAmount, record.RechargeScreenshot, record.RechargeStatus, RechargeStatusName = GetRechargeStatusName(record.RechargeStatus), record.Remark, record.AuditTime, record.AuditBy, record.CreateTime, UpdateTime = OmsDocHelper.NullIfDefault(record.UpdateTime) }).ToList(); await using var stream = new MemoryStream(); await MiniExcel.SaveAsAsync(stream, exportRows); stream.Position = 0; return new FileContentResult(stream.ToArray(), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") { FileDownloadName = $"recharge-records-{DateTime.Now:yyyyMMddHHmmss}.xlsx" }; } /// /// 将充值分页请求对象转换为仓储可执行的查询表达式。 /// /// 入参:充值分页查询条件对象。 /// 入参:客户 ID,空值表示不过滤。 /// 出参:充值记录查询表达式。 private Expression> BuildPaginatedPredicate( RechargeRecordPaginatedInput input, int? customerId) { var serialNumber = input.SerialNumber?.Trim(); return BuildRechargePredicate(customerId, input.RechargeStatus, serialNumber, input.StartTime, input.EndTime); } private async Task GetRechargeRecordOrThrowAsync(int id) { var record = await RechargeRecordRepository.FirstOrDefaultAsync(x => x.Id == id); if (record == null) { throw new BusinessException("充值记录不存在"); } return record; } private async Task GenerateSerialNumberAsync() { for (var i = 0; i < 5; i++) { var serialNumber = $"RECHARGE{DateTime.Now:yyyyMMddHHmmss}{Random.Shared.Next(100000, 999999)}"; if (!await RechargeRecordRepository.ExistsSerialNumberAsync(serialNumber)) { return serialNumber; } } throw new BusinessException("充值流水号生成失败"); } private static string GetPayTypeName(PayType payType) { return payType switch { PayType.Wechat => "微信", PayType.Alipay => "支付宝", _ => payType.ToString() }; } private static string GetRechargeStatusName(RechargeStatus rechargeStatus) { return rechargeStatus switch { RechargeStatus.Pending => "待审核", RechargeStatus.Approved => "审核通过", RechargeStatus.Rejected => "审核驳回", _ => rechargeStatus.ToString() }; } /// /// 将充值导出请求对象转换为仓储可执行的查询表达式。 /// /// 入参:充值导出筛选条件对象。 /// 入参:客户 ID,空值表示不过滤。 /// 出参:充值记录查询表达式。 private Expression> BuildExportPredicate(RechargeExportInput input, int? customerId) { return BuildRechargePredicate(customerId, null, null, input.StartTime, input.EndTime); } /// /// 统一构造充值记录查询条件。 /// /// 入参:客户 ID,空值表示不过滤。 /// 入参:充值状态,空值表示不过滤。 /// 入参:充值流水号关键词,空值表示不过滤。 /// 入参:充值时间开始值。 /// 入参:充值时间结束值。 /// 出参:可直接交给 SqlSugar 仓储执行的充值记录筛选表达式。 /// /// 核心业务逻辑: /// 1. 将可选筛选条件先转换成布尔开关和值; /// 2. 根据时间区间是否存在拆分表达式; /// 3. 避免在 SQL 翻译阶段混用 Nullable 时间判空和比较,降低生成异常 SQL 的风险。 /// private static Expression> BuildRechargePredicate( int? customerId, RechargeStatus? rechargeStatus, string? serialNumber, DateTime? startTime, DateTime? endTime) { var hasCustomerFilter = customerId.HasValue; var customerIdValue = customerId.GetValueOrDefault(); var hasStatusFilter = rechargeStatus.HasValue; var rechargeStatusValue = rechargeStatus.GetValueOrDefault(); var hasSerialNumberFilter = !string.IsNullOrWhiteSpace(serialNumber); // 显式拆开时间分支,保证 ORM 只翻译纯比较表达式。 if (startTime.HasValue && endTime.HasValue) { var start = startTime.Value; var end = endTime.Value; return record => (!hasCustomerFilter || record.CustomerId == customerIdValue) && (!hasStatusFilter || record.RechargeStatus == rechargeStatusValue) && (!hasSerialNumberFilter || record.SerialNumber.Contains(serialNumber!)) && record.RechargeTime >= start && record.RechargeTime <= end; } if (startTime.HasValue) { var start = startTime.Value; return record => (!hasCustomerFilter || record.CustomerId == customerIdValue) && (!hasStatusFilter || record.RechargeStatus == rechargeStatusValue) && (!hasSerialNumberFilter || record.SerialNumber.Contains(serialNumber!)) && record.RechargeTime >= start; } if (endTime.HasValue) { var end = endTime.Value; return record => (!hasCustomerFilter || record.CustomerId == customerIdValue) && (!hasStatusFilter || record.RechargeStatus == rechargeStatusValue) && (!hasSerialNumberFilter || record.SerialNumber.Contains(serialNumber!)) && record.RechargeTime <= end; } return record => (!hasCustomerFilter || record.CustomerId == customerIdValue) && (!hasStatusFilter || record.RechargeStatus == rechargeStatusValue) && (!hasSerialNumberFilter || record.SerialNumber.Contains(serialNumber!)); } }