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