admin/src/Oms.Application/Services/Recharge/RechargeService.cs
2026-06-25 19:59:06 +08:00

385 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/// <summary>
/// 充值相关 API 服务。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Recharge")]
public class RechargeService : ApplicationService
{
private IRechargeRecordRepository RechargeRecordRepository => LazyServiceProvider.GetRequiredService<IRechargeRecordRepository>();
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>();
private IWebHostEnvironment WebHostEnvironment => LazyServiceProvider.GetRequiredService<IWebHostEnvironment>();
/// <summary>
/// 提交充值申请。
/// </summary>
/// <param name="input">入参:multipart 表单,截图字段 rechargeScreenshot。</param>
/// <returns>充值记录结果。</returns>
[HttpPost("/recharge/recharge/submit")]
[Consumes("multipart/form-data")]
public async Task<RechargeRecordOutput> 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<RechargeRecord, RechargeRecordOutput>(savedEntity);
}
/// <summary>
/// 审核通过充值记录。
/// </summary>
/// <param name="input">入参:充值审核通过参数。</param>
/// <returns>无返回体,仅完成审核通过充值记录。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 查询并校验目标对象及当前状态。
/// 2. 执行业务状态流转或敏感字段更新。
/// 3. 写入审计字段并保存结果。
/// </remarks>
[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);
}
/// <summary>
/// 驳回充值记录。
/// </summary>
/// <param name="input">入参:充值审核驳回参数。</param>
/// <returns>无返回体,仅完成驳回充值记录。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 查询并校验目标对象及当前状态。
/// 2. 执行业务状态流转或敏感字段更新。
/// 3. 写入审计字段并保存结果。
/// </remarks>
[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);
}
/// <summary>
/// 查询充值记录列表。
/// </summary>
/// <param name="input">入参:充值记录分页查询条件。</param>
/// <returns>充值记录分页结果,包含列表数据和总记录数。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 根据查询条件构造筛选规则。
/// 2. 查询目标数据并补齐关联展示字段。
/// 3. 按接口约定返回列表结果。
/// </remarks>
[HttpGet("/recharge/recharge-record/list")]
public async Task<OmsPageResult<RechargeRecordPaginatedOutput>> 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<string, bool> { [nameof(RechargeRecord.RechargeTime)] = false, [nameof(RechargeRecord.Id)] = false });
var totalCount = await RechargeRecordRepository.CountAsync(predicate);
var items = new List<RechargeRecordPaginatedOutput>(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<RechargeRecord, RechargeRecordPaginatedOutput>(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<RechargeRecordPaginatedOutput>
{
Items = items,
Total = totalCount
};
}
/// <summary>
/// 导出充值记录。
/// </summary>
/// <param name="input">入参:充值记录导出筛选条件。</param>
/// <returns>文件流响应。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 根据筛选条件查询待导出数据或定位模板资源。
/// 2. 生成文件内容或组装下载响应。
/// 3. 以文件形式返回结果。
/// </remarks>
[HttpGet("/recharge/export")]
public async Task<FileContentResult> 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"
};
}
/// <summary>
/// 将充值分页请求对象转换为仓储可执行的查询表达式。
/// </summary>
/// <param name="input">入参:充值分页查询条件对象。</param>
/// <param name="customerId">入参:客户 ID,空值表示不过滤。</param>
/// <returns>出参:充值记录查询表达式。</returns>
private Expression<Func<RechargeRecord, bool>> BuildPaginatedPredicate(
RechargeRecordPaginatedInput input,
int? customerId)
{
var serialNumber = input.SerialNumber?.Trim();
return BuildRechargePredicate(customerId, input.RechargeStatus, serialNumber, input.StartTime, input.EndTime);
}
private async Task<RechargeRecord> GetRechargeRecordOrThrowAsync(int id)
{
var record = await RechargeRecordRepository.FirstOrDefaultAsync(x => x.Id == id);
if (record == null)
{
throw new BusinessException("充值记录不存在");
}
return record;
}
private async Task<string> 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()
};
}
/// <summary>
/// 将充值导出请求对象转换为仓储可执行的查询表达式。
/// </summary>
/// <param name="input">入参:充值导出筛选条件对象。</param>
/// <param name="customerId">入参:客户 ID,空值表示不过滤。</param>
/// <returns>出参:充值记录查询表达式。</returns>
private Expression<Func<RechargeRecord, bool>> BuildExportPredicate(RechargeExportInput input, int? customerId)
{
return BuildRechargePredicate(customerId, null, null, input.StartTime, input.EndTime);
}
/// <summary>
/// 统一构造充值记录查询条件。
/// </summary>
/// <param name="customerId">入参:客户 ID,空值表示不过滤。</param>
/// <param name="rechargeStatus">入参:充值状态,空值表示不过滤。</param>
/// <param name="serialNumber">入参:充值流水号关键词,空值表示不过滤。</param>
/// <param name="startTime">入参:充值时间开始值。</param>
/// <param name="endTime">入参:充值时间结束值。</param>
/// <returns>出参:可直接交给 SqlSugar 仓储执行的充值记录筛选表达式。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 将可选筛选条件先转换成布尔开关和值;
/// 2. 根据时间区间是否存在拆分表达式;
/// 3. 避免在 SQL 翻译阶段混用 Nullable 时间判空和比较,降低生成异常 SQL 的风险。
/// </remarks>
private static Expression<Func<RechargeRecord, bool>> 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!));
}
}