feat: remarks

This commit is contained in:
xiaolipro 2026-04-15 16:50:57 +08:00
parent 91a8ccdecb
commit b912c5b159
8 changed files with 164 additions and 1 deletions

View File

@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AClient_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fc7d66b24252bb52da52d1262d99fd654ffc18aedde818f147c3daf6aee3e63_003FClient_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIAggregateRoot_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6015bad29db142679ead208ae4ecd55c7800_003F0e_003F66feb813_003FIAggregateRoot_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIAggregateRoot_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6015bad29db142679ead208ae4ecd55c7800_003F0e_003F66feb813_003FIAggregateRoot_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIAggregateRoot_00601_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6015bad29db142679ead208ae4ecd55c7800_003F9a_003Fea4337c7_003FIAggregateRoot_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIAggregateRoot_00601_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6015bad29db142679ead208ae4ecd55c7800_003F9a_003Fea4337c7_003FIAggregateRoot_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIRepository_00601_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6015bad29db142679ead208ae4ecd55c7800_003F85_003Fa6db724e_003FIRepository_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIRepository_00601_002Ecs_002Fl_003AC_0021_003FUsers_003FBSI_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F6015bad29db142679ead208ae4ecd55c7800_003F85_003Fa6db724e_003FIRepository_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>

View File

@ -1,12 +1,27 @@
namespace Oms.Application.Dtos.Orders; namespace Oms.Application.Dtos.Orders;
/// <summary>
/// 订单导出入参。
/// </summary>
public class OrderExportInput public class OrderExportInput
{ {
/// <summary>
/// 订单状态,传 <c>all</c> 或空值时表示不过滤。
/// </summary>
public string? Status { get; set; } public string? Status { get; set; }
/// <summary>
/// 关键词,可匹配运单号、客户单号、跟踪号。
/// </summary>
public string? Keyword { get; set; } public string? Keyword { get; set; }
/// <summary>
/// 创建时间起始值。
/// </summary>
public DateTime? CreateTimeStart { get; set; } public DateTime? CreateTimeStart { get; set; }
/// <summary>
/// 创建时间结束值。
/// </summary>
public DateTime? CreateTimeEnd { get; set; } public DateTime? CreateTimeEnd { get; set; }
} }

View File

@ -1,16 +1,37 @@
namespace Oms.Application.Dtos.Orders; namespace Oms.Application.Dtos.Orders;
/// <summary>
/// 订单列表分页查询入参。
/// </summary>
public class OrderPaginatedInput public class OrderPaginatedInput
{ {
/// <summary>
/// 页码,从 1 开始。
/// </summary>
public int Page { get; set; } = 1; public int Page { get; set; } = 1;
/// <summary>
/// 每页条数。
/// </summary>
public int PageSize { get; set; } = 20; public int PageSize { get; set; } = 20;
/// <summary>
/// 订单状态,传 <c>all</c> 或空值时表示不过滤。
/// </summary>
public string? Status { get; set; } public string? Status { get; set; }
/// <summary>
/// 关键词,可匹配运单号、客户单号、跟踪号。
/// </summary>
public string? Keyword { get; set; } public string? Keyword { get; set; }
/// <summary>
/// 创建时间起始值。
/// </summary>
public DateTime? CreateTimeStart { get; set; } public DateTime? CreateTimeStart { get; set; }
/// <summary>
/// 创建时间结束值。
/// </summary>
public DateTime? CreateTimeEnd { get; set; } public DateTime? CreateTimeEnd { get; set; }
} }

View File

@ -1,10 +1,22 @@
namespace Oms.Application.Dtos.Recharge; namespace Oms.Application.Dtos.Recharge;
/// <summary>
/// 充值记录导出入参。
/// </summary>
public class RechargeExportInput public class RechargeExportInput
{ {
/// <summary>
/// 客户 ID,空值表示不过滤。
/// </summary>
public long? CustomerId { get; set; } public long? CustomerId { get; set; }
/// <summary>
/// 充值时间起始值。
/// </summary>
public DateTime? StartTime { get; set; } public DateTime? StartTime { get; set; }
/// <summary>
/// 充值时间结束值。
/// </summary>
public DateTime? EndTime { get; set; } public DateTime? EndTime { get; set; }
} }

View File

@ -2,19 +2,43 @@ using Oms.Domain.RechargeAggregate.Enums;
namespace Oms.Application.Dtos.Recharge; namespace Oms.Application.Dtos.Recharge;
/// <summary>
/// 充值记录分页查询入参。
/// </summary>
public class RechargeRecordPaginatedInput public class RechargeRecordPaginatedInput
{ {
/// <summary>
/// 页码,从 1 开始。
/// </summary>
public int Page { get; set; } = 1; public int Page { get; set; } = 1;
/// <summary>
/// 每页条数。
/// </summary>
public int PageSize { get; set; } = 20; public int PageSize { get; set; } = 20;
/// <summary>
/// 客户 ID,空值表示不过滤。
/// </summary>
public long? CustomerId { get; set; } public long? CustomerId { get; set; }
/// <summary>
/// 充值流水号,支持模糊匹配。
/// </summary>
public string? SerialNumber { get; set; } public string? SerialNumber { get; set; }
/// <summary>
/// 充值审核状态,空值表示不过滤。
/// </summary>
public RechargeStatus? RechargeStatus { get; set; } public RechargeStatus? RechargeStatus { get; set; }
/// <summary>
/// 充值时间起始值。
/// </summary>
public DateTime? StartTime { get; set; } public DateTime? StartTime { get; set; }
/// <summary>
/// 充值时间结束值。
/// </summary>
public DateTime? EndTime { get; set; } public DateTime? EndTime { get; set; }
} }

View File

@ -25,6 +25,17 @@ public partial class OrderService : ApplicationService
private ISalesProductRepository SalesProductRepository => LazyServiceProvider.GetRequiredService<ISalesProductRepository>(); private ISalesProductRepository SalesProductRepository => LazyServiceProvider.GetRequiredService<ISalesProductRepository>();
private IWebHostEnvironment WebHostEnvironment => LazyServiceProvider.GetRequiredService<IWebHostEnvironment>(); private IWebHostEnvironment WebHostEnvironment => LazyServiceProvider.GetRequiredService<IWebHostEnvironment>();
/// <summary>
/// 分页查询订单列表。
/// </summary>
/// <param name="input">入参:分页信息、订单状态、关键词、创建时间起止。</param>
/// <returns>出参:订单分页结果,包含当前页订单集合和总记录数。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 统一归一化页码和页大小;
/// 2. 根据状态、关键词、创建时间区间构造筛选表达式;
/// 3. 查询分页数据后补齐订单展示所需的映射字段。
/// </remarks>
[HttpPost("/order/list")] [HttpPost("/order/list")]
public async Task<OmsPageResult<OrderOutput>> GetPaginatedListAsync(OrderPaginatedInput input) public async Task<OmsPageResult<OrderOutput>> GetPaginatedListAsync(OrderPaginatedInput input)
{ {
@ -97,6 +108,14 @@ public partial class OrderService : ApplicationService
return new OrderPrintOutput { PrintUrl = printUrl }; return new OrderPrintOutput { PrintUrl = printUrl };
} }
/// <summary>
/// 导出订单列表。
/// </summary>
/// <param name="input">入参:订单状态、关键词、创建时间起止。</param>
/// <returns>出参:导出文件下载地址及失效时间。</returns>
/// <remarks>
/// 核心业务逻辑:先清理过期文件,再按筛选条件查询订单,最后生成 Excel 并返回下载信息。
/// </remarks>
[HttpPost("/order/export")] [HttpPost("/order/export")]
public async Task<OrderExportOutput> ExportAsync(OrderExportInput input) public async Task<OrderExportOutput> ExportAsync(OrderExportInput input)
{ {
@ -159,16 +178,41 @@ public partial class OrderService : ApplicationService
return await CreateParcelCoreAsync(input); return await CreateParcelCoreAsync(input);
} }
/// <summary>
/// 将订单列表请求对象转换为仓储可执行的查询表达式。
/// </summary>
/// <param name="input">入参:分页查询条件对象。</param>
/// <returns>出参:订单查询表达式。</returns>
private Expression<Func<Order, bool>> BuildPaginatedPredicate(OrderPaginatedInput input) private Expression<Func<Order, bool>> BuildPaginatedPredicate(OrderPaginatedInput input)
{ {
return BuildOrderPredicate(input.Status, input.Keyword, input.CreateTimeStart, input.CreateTimeEnd); return BuildOrderPredicate(input.Status, input.Keyword, input.CreateTimeStart, input.CreateTimeEnd);
} }
/// <summary>
/// 将订单导出请求对象转换为仓储可执行的查询表达式。
/// </summary>
/// <param name="input">入参:导出筛选条件对象。</param>
/// <returns>出参:订单查询表达式。</returns>
private Expression<Func<Order, bool>> BuildExportPredicate(OrderExportInput input) private Expression<Func<Order, bool>> BuildExportPredicate(OrderExportInput input)
{ {
return BuildOrderPredicate(input.Status, input.Keyword, input.CreateTimeStart, input.CreateTimeEnd); return BuildOrderPredicate(input.Status, input.Keyword, input.CreateTimeStart, input.CreateTimeEnd);
} }
/// <summary>
/// 统一构造订单查询条件。
/// </summary>
/// <param name="status">入参:订单状态,传入 <c>all</c> 或空值时表示不按状态过滤。</param>
/// <param name="keyword">入参:关键词,可匹配运单号、客户单号、跟踪号。</param>
/// <param name="createTimeStart">入参:创建时间开始值。</param>
/// <param name="createTimeEnd">入参:创建时间结束值。</param>
/// <returns>出参:可直接交给 SqlSugar 仓储执行的订单筛选表达式。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 先把状态和关键词做统一清洗;
/// 2. 再按时间区间是否存在拆分成不同表达式;
/// 3. 避免把 <see cref="Nullable{T}.HasValue"/> / <see cref="Nullable{T}.Value"/> 放进可翻译 SQL 的表达式中,
/// 从而规避 MySQL 下时间条件被错误翻译的问题。
/// </remarks>
private static Expression<Func<Order, bool>> BuildOrderPredicate( private static Expression<Func<Order, bool>> BuildOrderPredicate(
string? status, string? status,
string? keyword, string? keyword,
@ -180,6 +224,7 @@ public partial class OrderService : ApplicationService
var hasStatusFilter = !string.IsNullOrWhiteSpace(status) && status != "all"; var hasStatusFilter = !string.IsNullOrWhiteSpace(status) && status != "all";
var hasKeywordFilter = !string.IsNullOrWhiteSpace(keyword); var hasKeywordFilter = !string.IsNullOrWhiteSpace(keyword);
// 显式拆开时间分支,避免 SqlSugar 在翻译 Nullable<DateTime> 判空时生成异常 SQL。
if (createTimeStart.HasValue && createTimeEnd.HasValue) if (createTimeStart.HasValue && createTimeEnd.HasValue)
{ {
var start = createTimeStart.Value; var start = createTimeStart.Value;

View File

@ -106,6 +106,17 @@ public class RechargeService : ApplicationService
await RechargeRecordRepository.UpdateAsync(record); await RechargeRecordRepository.UpdateAsync(record);
} }
/// <summary>
/// 分页查询充值记录列表。
/// </summary>
/// <param name="input">入参:分页信息、客户、充值流水号、审核状态、充值时间区间。</param>
/// <returns>出参:充值记录分页结果,包含列表数据和总记录数。</returns>
/// <remarks>
/// 核心业务逻辑:
/// 1. 先根据筛选条件查询充值记录;
/// 2. 再批量补齐客户编码和手机号等展示字段;
/// 3. 最后返回前端可直接渲染的分页结果。
/// </remarks>
[HttpGet("/recharge/recharge-record/list")] [HttpGet("/recharge/recharge-record/list")]
public async Task<OmsPageResult<RechargeRecordPaginatedOutput>> GetPaginatedListAsync([FromQuery] RechargeRecordPaginatedInput input) public async Task<OmsPageResult<RechargeRecordPaginatedOutput>> GetPaginatedListAsync([FromQuery] RechargeRecordPaginatedInput input)
{ {
@ -127,6 +138,7 @@ public class RechargeService : ApplicationService
: await CustomerRepository.GetListAsync(x => !x.IsDeleted && customerIds.Contains(x.Id)); : await CustomerRepository.GetListAsync(x => !x.IsDeleted && customerIds.Contains(x.Id));
var customerMap = customers.ToDictionary(x => x.Id); var customerMap = customers.ToDictionary(x => x.Id);
// 列表返回时补齐客户信息,避免前端为了展示字段额外发起关联查询。
foreach (var entity in entities) foreach (var entity in entities)
{ {
var output = ObjectMapper.Map<RechargeRecord, RechargeRecordPaginatedOutput>(entity); var output = ObjectMapper.Map<RechargeRecord, RechargeRecordPaginatedOutput>(entity);
@ -149,6 +161,14 @@ public class RechargeService : ApplicationService
}; };
} }
/// <summary>
/// 导出充值记录。
/// </summary>
/// <param name="input">入参:客户和充值时间区间。</param>
/// <returns>出参:Excel 文件内容。</returns>
/// <remarks>
/// 核心业务逻辑:根据导出条件查询充值记录,补齐客户展示字段后直接生成 Excel 文件流。
/// </remarks>
[HttpGet("/recharge/export")] [HttpGet("/recharge/export")]
public async Task<FileContentResult> ExportAsync([FromQuery] RechargeExportInput input) public async Task<FileContentResult> ExportAsync([FromQuery] RechargeExportInput input)
{ {
@ -189,6 +209,11 @@ public class RechargeService : ApplicationService
}; };
} }
/// <summary>
/// 将充值分页请求对象转换为仓储可执行的查询表达式。
/// </summary>
/// <param name="input">入参:充值分页查询条件对象。</param>
/// <returns>出参:充值记录查询表达式。</returns>
private Expression<Func<RechargeRecord, bool>> BuildPaginatedPredicate(RechargeRecordPaginatedInput input) private Expression<Func<RechargeRecord, bool>> BuildPaginatedPredicate(RechargeRecordPaginatedInput input)
{ {
var serialNumber = input.SerialNumber?.Trim(); var serialNumber = input.SerialNumber?.Trim();
@ -241,11 +266,31 @@ public class RechargeService : ApplicationService
}; };
} }
/// <summary>
/// 将充值导出请求对象转换为仓储可执行的查询表达式。
/// </summary>
/// <param name="input">入参:充值导出筛选条件对象。</param>
/// <returns>出参:充值记录查询表达式。</returns>
private Expression<Func<RechargeRecord, bool>> BuildExportPredicate(RechargeExportInput input) private Expression<Func<RechargeRecord, bool>> BuildExportPredicate(RechargeExportInput input)
{ {
return BuildRechargePredicate(input.CustomerId, null, null, input.StartTime, input.EndTime); return BuildRechargePredicate(input.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( private static Expression<Func<RechargeRecord, bool>> BuildRechargePredicate(
long? customerId, long? customerId,
RechargeStatus? rechargeStatus, RechargeStatus? rechargeStatus,
@ -259,6 +304,7 @@ public class RechargeService : ApplicationService
var rechargeStatusValue = rechargeStatus.GetValueOrDefault(); var rechargeStatusValue = rechargeStatus.GetValueOrDefault();
var hasSerialNumberFilter = !string.IsNullOrWhiteSpace(serialNumber); var hasSerialNumberFilter = !string.IsNullOrWhiteSpace(serialNumber);
// 显式拆开时间分支,保证 ORM 只翻译纯比较表达式。
if (startTime.HasValue && endTime.HasValue) if (startTime.HasValue && endTime.HasValue)
{ {
var start = startTime.Value; var start = startTime.Value;

View File

@ -4,7 +4,6 @@
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>e3d506cf-f5ce-4ba5-bc8e-ab2c179f3034</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup> </PropertyGroup>