385 lines
16 KiB
C#
385 lines
16 KiB
C#
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!));
|
||
}
|
||
}
|