修复创建订单报错

This commit is contained in:
zzh 2026-06-25 19:59:06 +08:00
parent b7ed14d6af
commit 13ccd198d5
44 changed files with 316 additions and 100 deletions

View File

@ -12,11 +12,11 @@ public class ServiceChannelGroupSaveInput
/// </summary>
public int? Id { get; set; }
[Required]
[StringLength(128)]
/// <summary>
/// 分组名称。
/// </summary>
[Required]
[StringLength(128)]
public string Name { get; set; } = string.Empty;
/// <summary>

View File

@ -7,16 +7,16 @@ namespace Oms.Application.Dtos.Customers;
/// </summary>
public class ResetCustomerPasswordInput
{
[Required]
/// <summary>
/// 主键。
/// </summary>
[Required]
public int Id { get; set; }
[Required]
[StringLength(64)]
/// <summary>
/// 新密码。
/// </summary>
[Required]
[StringLength(64)]
public string NewPassword { get; set; } = string.Empty;
}

View File

@ -7,11 +7,11 @@ namespace Oms.Application.Dtos.Operations;
/// </summary>
public class CreateInboundRecordInput
{
[Required]
[StringLength(64)]
/// <summary>
/// 运单号。
/// </summary>
[Required]
[StringLength(64)]
public string WaybillNo { get; set; } = string.Empty;
/// <summary>
@ -39,10 +39,10 @@ public class CreateInboundRecordInput
/// </summary>
public int? SalesProductId { get; set; }
[StringLength(8)]
/// <summary>
/// 目的国家/地区。
/// </summary>
[StringLength(8)]
public string? DestinationCountry { get; set; }
/// <summary>

View File

@ -7,11 +7,11 @@ namespace Oms.Application.Dtos.Operations;
/// </summary>
public class CreateOutboundRecordInput
{
[Required]
[StringLength(64)]
/// <summary>
/// 运单号。
/// </summary>
[Required]
[StringLength(64)]
public string WaybillNo { get; set; } = string.Empty;
/// <summary>

View File

@ -7,23 +7,23 @@ namespace Oms.Application.Dtos.Operations;
/// </summary>
public class PrintLabelInput
{
[Required]
[StringLength(64)]
/// <summary>
/// 单号值。
/// </summary>
[Required]
[StringLength(64)]
public string Number { get; set; } = string.Empty;
[StringLength(64)]
/// <summary>
/// 单号类型。
/// </summary>
[StringLength(64)]
public string? NumberType { get; set; }
[StringLength(64)]
/// <summary>
/// 模板类型。
/// </summary>
[StringLength(64)]
public string? TemplateType { get; set; }
/// <summary>

View File

@ -7,15 +7,15 @@ namespace Oms.Application.Dtos.Orders;
/// </summary>
public class OrderActionInput
{
[Required]
/// <summary>
/// 主键。
/// </summary>
[Required]
public int Id { get; set; }
[StringLength(64)]
/// <summary>
/// 请求标识。
/// </summary>
[StringLength(64)]
public string? RequestId { get; set; }
}

View File

@ -16,12 +16,12 @@ public class OrderParcelCreateInput
public string? ClientNo { get; set; }
/// <summary>
/// 跟踪号。
/// 跟踪号(可选)。
/// </summary>
public string? TrackNo { get; set; }
/// <summary>
/// 服务单号。
/// 服务单号(可选,不传时系统自动生成)。
/// </summary>
public string? ServiceNo { get; set; }

View File

@ -7,8 +7,14 @@ namespace Oms.Application.Dtos.Orders;
/// </summary>
public class SalesProductPricePublishSnapshotDto
{
/// <summary>
/// 销售产品主键。
/// </summary>
public int SalesProductId { get; set; }
/// <summary>
/// 价格项列表。
/// </summary>
public List<SalesProductPriceListOutput> Prices { get; set; } = [];
}
@ -17,8 +23,14 @@ public class SalesProductPricePublishSnapshotDto
/// </summary>
public class SalesProductRulesPublishSnapshotDto
{
/// <summary>
/// 产品基础信息。
/// </summary>
public SalesProductSaveInput? Product { get; set; }
/// <summary>
/// 分配的服务商列表。
/// </summary>
public List<SalesProductAllocationProviderOutput> Providers { get; set; } = [];
}
@ -27,15 +39,33 @@ public class SalesProductRulesPublishSnapshotDto
/// </summary>
public class ProductSnapshotBinding
{
/// <summary>
/// 规则快照版本号。
/// </summary>
public string RulesVersion { get; set; } = string.Empty;
/// <summary>
/// 价格快照版本号。
/// </summary>
public string PriceVersion { get; set; } = string.Empty;
/// <summary>
/// 规则快照生效时间。
/// </summary>
public DateTime RulesEffectiveTime { get; set; }
/// <summary>
/// 价格快照生效时间。
/// </summary>
public DateTime PriceEffectiveTime { get; set; }
/// <summary>
/// 规则快照内容。
/// </summary>
public SalesProductRulesPublishSnapshotDto Rules { get; set; } = new();
/// <summary>
/// 价格快照内容。
/// </summary>
public SalesProductPricePublishSnapshotDto Price { get; set; } = new();
}

View File

@ -7,15 +7,15 @@ namespace Oms.Application.Dtos.Recharge;
/// </summary>
public class ApproveRechargeInput
{
[Required]
/// <summary>
/// 主键。
/// </summary>
[Required]
public int Id { get; set; }
[StringLength(512)]
/// <summary>
/// 备注。
/// </summary>
[StringLength(512)]
public string? Remark { get; set; }
}

View File

@ -7,15 +7,15 @@ namespace Oms.Application.Dtos.Recharge;
/// </summary>
public class RejectRechargeInput
{
[Required]
/// <summary>
/// 主键。
/// </summary>
[Required]
public int Id { get; set; }
[StringLength(512)]
/// <summary>
/// 备注。
/// </summary>
[StringLength(512)]
public string? Remark { get; set; }
}

View File

@ -14,12 +14,21 @@ public class SubmitRechargeFormInput
/// </summary>
public int? CustomerId { get; set; }
/// <summary>
/// 充值金额。
/// </summary>
[Range(typeof(decimal), "0.01", "999999999999")]
public decimal RechargeAmount { get; set; }
/// <summary>
/// 支付方式。
/// </summary>
[Required]
public PayType PayType { get; set; }
/// <summary>
/// 备注。
/// </summary>
[StringLength(512)]
public string? Remark { get; set; }

View File

@ -13,27 +13,27 @@ public class SubmitRechargeInput
/// </summary>
public int? CustomerId { get; set; }
[Range(typeof(decimal), "0.01", "999999999999")]
/// <summary>
/// 充值金额。
/// </summary>
[Range(typeof(decimal), "0.01", "999999999999")]
public decimal RechargeAmount { get; set; }
[Required]
/// <summary>
/// 支付方式。
/// </summary>
[Required]
public PayType PayType { get; set; }
[StringLength(255)]
/// <summary>
/// 充值截图。
/// </summary>
[StringLength(255)]
public string? RechargeScreenshot { get; set; }
[StringLength(512)]
/// <summary>
/// 备注。
/// </summary>
[StringLength(512)]
public string? Remark { get; set; }
}

View File

@ -7,9 +7,9 @@ namespace Oms.Application.Dtos.Shared;
/// </summary>
public class IdInput
{
[Required]
/// <summary>
/// 主键。
/// </summary>
[Required]
public int Id { get; set; }
}

View File

@ -7,30 +7,30 @@ namespace Oms.Application.Dtos.System;
/// </summary>
public class CreateWarehouseInput
{
[Required]
[StringLength(128)]
/// <summary>
/// 仓库名称。
/// </summary>
[Required]
[StringLength(128)]
public string WarehouseName { get; set; } = string.Empty;
[Required]
[StringLength(32)]
/// <summary>
/// 仓库类型编码。
/// </summary>
[Required]
[StringLength(32)]
public string WarehouseType { get; set; } = string.Empty;
[StringLength(64)]
/// <summary>
/// 收件人。
/// </summary>
[StringLength(64)]
public string? Recipient { get; set; }
[StringLength(32)]
/// <summary>
/// 联系电话。
/// </summary>
[StringLength(32)]
public string? ContactPhone { get; set; }
/// <summary>
@ -38,16 +38,16 @@ public class CreateWarehouseInput
/// </summary>
public string? ShippingAddress { get; set; }
[StringLength(32)]
/// <summary>
/// 客服手机号。
/// </summary>
[StringLength(32)]
public string? ServicePhone { get; set; }
[StringLength(64)]
/// <summary>
/// 客服微信号。
/// </summary>
[StringLength(64)]
public string? ServiceWechat { get; set; }
/// <summary>

View File

@ -7,11 +7,11 @@ namespace Oms.Application.Dtos.System;
/// </summary>
public class DictCreateInput
{
[Required]
[StringLength(64)]
/// <summary>
/// 字典类型。
/// </summary>
[Required]
[StringLength(64)]
public string DictType { get; set; } = string.Empty;
/// <summary>
@ -19,17 +19,17 @@ public class DictCreateInput
/// </summary>
public int DictSort { get; set; }
[Required]
[StringLength(64)]
/// <summary>
/// 字典编码。
/// </summary>
[Required]
[StringLength(64)]
public string DictCode { get; set; } = string.Empty;
[Required]
[StringLength(128)]
/// <summary>
/// 字典名称。
/// </summary>
[Required]
[StringLength(128)]
public string DictName { get; set; } = string.Empty;
}

View File

@ -7,9 +7,9 @@ namespace Oms.Application.Dtos.System;
/// </summary>
public class DictUpdateInput : DictCreateInput
{
[Required]
/// <summary>
/// 字典主键。
/// </summary>
[Required]
public int Id { get; set; }
}

View File

@ -7,24 +7,24 @@ namespace Oms.Application.Dtos.System;
/// </summary>
public class NoticeCreateInput
{
[Required]
[StringLength(16)]
/// <summary>
/// 通知类型:notice/message。
/// </summary>
[Required]
[StringLength(16)]
public string NoticeType { get; set; } = string.Empty;
[Required]
[StringLength(128)]
/// <summary>
/// 标题。
/// </summary>
[Required]
[StringLength(128)]
public string Title { get; set; } = string.Empty;
[Required]
/// <summary>
/// 正文内容。
/// </summary>
[Required]
public string Content { get; set; } = string.Empty;
/// <summary>
@ -32,11 +32,11 @@ public class NoticeCreateInput
/// </summary>
public byte IsPopup { get; set; }
[Required]
[StringLength(16)]
/// <summary>
/// 接收对象类型:user/customer。
/// </summary>
[Required]
[StringLength(16)]
public string ReceiverType { get; set; } = string.Empty;
/// <summary>

View File

@ -7,9 +7,9 @@ namespace Oms.Application.Dtos.System;
/// </summary>
public class NoticeUpdateInput : NoticeCreateInput
{
[Required]
/// <summary>
/// 通知主键。
/// </summary>
[Required]
public int Id { get; set; }
}

View File

@ -7,36 +7,36 @@ namespace Oms.Application.Dtos.System;
/// </summary>
public class UpdateWarehouseInput
{
[Required]
/// <summary>
/// 主键。
/// </summary>
[Required]
public int Id { get; set; }
[Required]
[StringLength(128)]
/// <summary>
/// 仓库名称。
/// </summary>
[Required]
[StringLength(128)]
public string WarehouseName { get; set; } = string.Empty;
[Required]
[StringLength(32)]
/// <summary>
/// 仓库类型编码。
/// </summary>
[Required]
[StringLength(32)]
public string WarehouseType { get; set; } = string.Empty;
[StringLength(64)]
/// <summary>
/// 收件人。
/// </summary>
[StringLength(64)]
public string? Recipient { get; set; }
[StringLength(32)]
/// <summary>
/// 联系电话。
/// </summary>
[StringLength(32)]
public string? ContactPhone { get; set; }
/// <summary>
@ -44,16 +44,16 @@ public class UpdateWarehouseInput
/// </summary>
public string? ShippingAddress { get; set; }
[StringLength(32)]
/// <summary>
/// 客服手机号。
/// </summary>
[StringLength(32)]
public string? ServicePhone { get; set; }
[StringLength(64)]
/// <summary>
/// 客服微信号。
/// </summary>
[StringLength(64)]
public string? ServiceWechat { get; set; }
/// <summary>

View File

@ -5,12 +5,16 @@ using Oms.Domain;
namespace Oms.Application;
/// <summary>
/// OMS 应用层模块注册。
/// </summary>
[DependsOn(
typeof(OmsDomainModule),
typeof(FakeObjectMappingAutoMapperModule)
)]
public class OmsApplicationModule : FakeModule
{
/// <inheritdoc />
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.Configure<FakeAutoMapperOptions>(options =>

View File

@ -14,8 +14,14 @@ using Oms.Domain.SystemAggregate.Entities;
namespace Oms.Application.Profiles;
/// <summary>
/// OMS 应用层 AutoMapper 映射配置。
/// </summary>
public class OmsApplicationAutoMapperProfile : Profile
{
/// <summary>
/// 注册实体与 DTO 之间的映射关系。
/// </summary>
public OmsApplicationAutoMapperProfile()
{
CreateMap<Customer, CustomerPaginatedOutput>();

View File

@ -9,6 +9,9 @@ using Oms.Domain.ChannelsAggregate.Repositories;
namespace Oms.Application.Services.Channels;
/// <summary>
/// 自动触发规则 API 服务。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Channel")]
public class AutoTriggerService : ApplicationService
{

View File

@ -9,6 +9,9 @@ using Oms.Domain.ChannelsAggregate.Repositories;
namespace Oms.Application.Services.Channels;
/// <summary>
/// KPI 分区 API 服务。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Channel")]
public class KpiAreaService : ApplicationService
{

View File

@ -7,6 +7,9 @@ using Oms.Domain.ChannelsAggregate.Repositories;
namespace Oms.Application.Services.Channels;
/// <summary>
/// 面单模板 API 服务。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Channel")]
public class LabelTemplateService : ApplicationService
{

View File

@ -9,6 +9,9 @@ using Oms.Domain.ChannelsAggregate.Repositories;
namespace Oms.Application.Services.Channels;
/// <summary>
/// 销售产品 API 服务。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Channel")]
public partial class SalesProductService : ApplicationService
{

View File

@ -9,6 +9,9 @@ using Oms.Domain.ChannelsAggregate.Repositories;
namespace Oms.Application.Services.Channels;
/// <summary>
/// 服务渠道 API 服务。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Channel")]
public partial class ServiceChannelService : ApplicationService
{

View File

@ -9,6 +9,9 @@ using Oms.Domain.ChannelsAggregate.Repositories;
namespace Oms.Application.Services.Channels;
/// <summary>
/// 重量进位 API 服务。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Channel")]
public class WeightRoundingService : ApplicationService
{

View File

@ -9,6 +9,9 @@ using Oms.Domain.ChannelsAggregate.Repositories;
namespace Oms.Application.Services.Channels;
/// <summary>
/// 重量规则 API 服务。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Channel")]
public class WeightRuleService : ApplicationService
{

View File

@ -16,6 +16,9 @@ using Oms.Domain.CustomersAggregate.Repositories;
namespace Oms.Application.Services.Customers;
/// <summary>
/// 客户 API 服务。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Customer")]
public class CustomerService : ApplicationService
{

View File

@ -9,6 +9,9 @@ using Oms.Domain.SystemAggregate.Repositories;
namespace Oms.Application.Services.Operations;
/// <summary>
/// 入库记录 API 服务。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Operation")]
public class InboundRecordService : ApplicationService
{

View File

@ -10,6 +10,9 @@ using Oms.Domain.Shared.Enums;
namespace Oms.Application.Services.Operations;
/// <summary>
/// 面单打印 API 服务。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Operation")]
public class LabelPrintService : ApplicationService
{

View File

@ -8,6 +8,9 @@ using Oms.Domain.SystemAggregate.Repositories;
namespace Oms.Application.Services.Operations;
/// <summary>
/// 出库记录 API 服务。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Operation")]
public class OutboundRecordService : ApplicationService
{

View File

@ -116,9 +116,16 @@ public partial class OrderService
var page = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
var keyword = input.Keyword?.Trim();
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
null);
var hasCustomerFilter = customerId.HasValue;
var customerIdValue = customerId.GetValueOrDefault();
var predicate = (Expression<Func<OrderTemplate, bool>>)(x =>
(string.IsNullOrWhiteSpace(input.Type) || x.Type == input.Type) &&
(string.IsNullOrWhiteSpace(keyword) || x.Name.Contains(keyword) || (x.TemplateKey ?? string.Empty).Contains(keyword)));
(string.IsNullOrWhiteSpace(keyword) || x.Name.Contains(keyword) || (x.TemplateKey ?? string.Empty).Contains(keyword)) &&
(!hasCustomerFilter || x.Type == "standard" || x.CustomerId == customerIdValue));
var templates = await OrderTemplateRepository.GetPagedListAsync(
predicate,
@ -151,6 +158,15 @@ public partial class OrderService
throw new BusinessException("模板不存在");
}
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
null);
if (customerId.HasValue)
{
EnsureTemplateAccessible(template, customerId.Value);
}
var rows = new[]
{
new Dictionary<string, object?>
@ -162,7 +178,7 @@ public partial class OrderService
["ChannelCode"] = "",
["CargoAttribute"] = CargoAttributeCodes.General,
["WarehouseId"] = "",
["CustomerId"] = template.CustomerId,
["CustomerId"] = customerId ?? template.CustomerId,
["Length"] = "",
["Width"] = "",
["Height"] = "",
@ -202,6 +218,9 @@ public partial class OrderService
throw new BusinessException("模板不存在");
}
var effectiveCustomer = await ResolveEffectiveCustomerAsync();
EnsureTemplateAccessible(template, effectiveCustomer.Id);
var task = await OrderTaskRepository.InsertAsync(new OrderTask
{
TaskName = $"批量下单_{DateTime.Now:yyyyMMddHHmmss}",
@ -210,7 +229,7 @@ public partial class OrderService
FailCount = 0,
Status = "processing",
TemplateId = template.Id,
CustomerId = template.CustomerId
CustomerId = effectiveCustomer.Id
});
var quantity = 0;
@ -233,7 +252,7 @@ public partial class OrderService
try
{
await CreateImportOrderAsync(rowDict, template);
await CreateImportOrderAsync(rowDict, effectiveCustomer.Id);
successCount++;
}
catch (Exception ex)
@ -256,29 +275,29 @@ public partial class OrderService
return new OrderUploadOutput { TaskId = task.Id };
}
private async Task CreateImportOrderAsync(IDictionary<string, object> rowDict, OrderTemplate template)
private async Task CreateImportOrderAsync(IDictionary<string, object> rowDict, int effectiveCustomerId)
{
var serviceNo = GetStringCell(rowDict, "ServiceNo");
if (string.IsNullOrWhiteSpace(serviceNo))
var rowCustomerId = GetIntCell(rowDict, "CustomerId");
if (rowCustomerId is > 0 && rowCustomerId.Value != effectiveCustomerId)
{
throw new BusinessException("ServiceNo不能为空");
throw new BusinessException("客户ID非法");
}
if (await OrderRepository.HasServiceNoAsync(serviceNo))
{
throw new BusinessException("ServiceNo已存在");
}
var serviceNo = GetStringCell(rowDict, "ServiceNo");
var resolvedServiceNo = await ResolveServiceNoAsync(serviceNo);
var order = await OrderRepository.InsertAsync(new Order
{
ServiceNo = serviceNo,
ServiceNo = resolvedServiceNo,
ClientNo = GetStringCell(rowDict, "ClientNo"),
TrackNo = GetStringCell(rowDict, "TrackNo"),
TrackNo = string.IsNullOrWhiteSpace(GetStringCell(rowDict, "TrackNo"))
? null
: GetStringCell(rowDict, "TrackNo"),
ProductCode = GetStringCell(rowDict, "ProductCode"),
ChannelCode = GetStringCell(rowDict, "ChannelCode"),
CargoAttribute = GetStringCell(rowDict, "CargoAttribute") ?? CargoAttributeCodes.General,
WarehouseId = GetIntCell(rowDict, "WarehouseId"),
CustomerId = GetIntCell(rowDict, "CustomerId") ?? template.CustomerId,
CustomerId = effectiveCustomerId,
CodCurrency = GetStringCell(rowDict, "CodCurrency"),
CodSum = GetDecimalCell(rowDict, "CodSum"),
DeclaredCurrency = GetStringCell(rowDict, "DeclaredCurrency"),
@ -302,9 +321,16 @@ public partial class OrderService
var page = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
var keyword = input.Keyword?.Trim();
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
null);
var hasCustomerFilter = customerId.HasValue;
var customerIdValue = customerId.GetValueOrDefault();
var predicate = (Expression<Func<OrderTask, bool>>)(x =>
(string.IsNullOrWhiteSpace(input.Status) || x.Status == input.Status) &&
(string.IsNullOrWhiteSpace(keyword) || x.TaskName.Contains(keyword)));
(string.IsNullOrWhiteSpace(keyword) || x.TaskName.Contains(keyword)) &&
(!hasCustomerFilter || x.CustomerId == customerIdValue));
var tasks = await OrderTaskRepository.GetPagedListAsync(
predicate,
page,
@ -336,6 +362,15 @@ public partial class OrderService
throw new BusinessException("任务不存在");
}
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
null);
if (customerId.HasValue && task.CustomerId != customerId.Value)
{
throw new BusinessException("任务不存在");
}
var failList = await OrderTaskRepository.GetFailListAsync(task.Id);
return new OrderTaskDetailOutput
{
@ -527,6 +562,19 @@ public partial class OrderService
private static decimal? GetDecimalCell(IDictionary<string, object> row, string key) => decimal.TryParse(GetStringCell(row, key), out var value) ? value : null;
private static bool GetBoolCell(IDictionary<string, object> row, string key) => GetStringCell(row, key) is "1" or "true" or "True" or "TRUE" or "是";
private static void EnsureTemplateAccessible(OrderTemplate template, int effectiveCustomerId)
{
if (string.Equals(template.Type, "standard", StringComparison.OrdinalIgnoreCase))
{
return;
}
if (template.CustomerId != effectiveCustomerId)
{
throw new BusinessException("模板不存在");
}
}
private OrderWeight? BuildForecastWeight(int orderId, IDictionary<string, object> row)
{
var length = GetDecimalCell(row, "Length");

View File

@ -38,6 +38,11 @@ public partial class OrderService
}
var bindTime = DateTime.Now;
NormalizeParcelCreateInput(input);
var sessionCustomer = await ResolveEffectiveCustomerAsync(input.CustomerId);
input.CustomerId = sessionCustomer.Id;
await ValidateParcelCreateInputAsync(input);
var product = await SalesProductRepository.GetByProductCodeAsync(input.ProductCode!.Trim());
@ -135,6 +140,14 @@ public partial class OrderService
}
}
private static void NormalizeParcelCreateInput(OrderParcelCreateInput input)
{
input.TrackNo = string.IsNullOrWhiteSpace(input.TrackNo) ? null : input.TrackNo.Trim();
input.ServiceNo = string.IsNullOrWhiteSpace(input.ServiceNo) ? null : input.ServiceNo.Trim();
input.ClientNo = string.IsNullOrWhiteSpace(input.ClientNo) ? null : input.ClientNo.Trim();
input.RequestId = string.IsNullOrWhiteSpace(input.RequestId) ? null : input.RequestId.Trim();
}
private async Task ValidateParcelCreateInputAsync(OrderParcelCreateInput input)
{
if (string.IsNullOrWhiteSpace(input.ProductCode))
@ -275,9 +288,9 @@ public partial class OrderService
};
}
private async Task<SalesProductRulesPublishSnapshotDto> BuildLiveRulesSnapshotAsync(SalesProduct product)
private Task<SalesProductRulesPublishSnapshotDto> BuildLiveRulesSnapshotAsync(SalesProduct product)
{
return new SalesProductRulesPublishSnapshotDto
return Task.FromResult(new SalesProductRulesPublishSnapshotDto
{
Product = new SalesProductSaveInput
{
@ -292,7 +305,7 @@ public partial class OrderService
IsActive = product.IsActive,
Scheme = product.Scheme
}
};
});
}
private async Task<SalesProductPricePublishSnapshotDto> BuildLivePriceSnapshotAsync(SalesProduct product)
@ -696,4 +709,13 @@ public partial class OrderService
: string.Empty
};
}
/// <summary>
/// 从会话解析有效客户(客户登录自动补全;管理员须传 customerId)。
/// </summary>
private Task<Customer> ResolveEffectiveCustomerAsync(int? requestedCustomerId = null) =>
OmsSessionHelper.ResolveSessionCustomerAsync(
CustomerRepository,
CurrentUser,
requestedCustomerId);
}

View File

@ -15,6 +15,9 @@ using Oms.Domain.SystemAggregate.Repositories;
namespace Oms.Application.Services.Orders;
/// <summary>
/// 订单相关 API 服务。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Order")]
public partial class OrderService : ApplicationService
{
@ -39,7 +42,11 @@ public partial class OrderService : ApplicationService
[HttpPost("/order/list")]
public async Task<OmsPageResult<OrderOutput>> GetPaginatedListAsync(OrderPaginatedInput input)
{
var predicate = BuildPaginatedPredicate(input);
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
null);
var predicate = BuildPaginatedPredicate(input, customerId);
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
var orders = await OrderRepository.GetPagedListAsync(
@ -200,8 +207,12 @@ public partial class OrderService : ApplicationService
public async Task<OrderExportOutput> ExportAsync(OrderExportInput input)
{
await CleanupExpiredFilesAsync(GetOrderExportDirectory());
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
null);
var orders = await OrderRepository.GetListAsync(
BuildExportPredicate(input),
BuildExportPredicate(input, customerId),
new Dictionary<string, bool> { [nameof(Order.CreateTime)] = false, [nameof(Order.Id)] = false });
var fileName = $"orders-{DateTime.Now:yyyyMMddHHmmss}.xlsx";
var directory = GetOrderExportDirectory();
@ -410,30 +421,32 @@ public partial class OrderService : ApplicationService
/// 将订单列表请求对象转换为仓储可执行的查询表达式。
/// </summary>
/// <param name="input">入参:分页查询条件对象。</param>
/// <param name="customerId">入参:会话客户 ID,空值表示不按客户隔离。</param>
/// <returns>出参:订单查询表达式。</returns>
private Expression<Func<Order, bool>> BuildPaginatedPredicate(OrderPaginatedInput input)
private Expression<Func<Order, bool>> BuildPaginatedPredicate(OrderPaginatedInput input, int? customerId)
{
return BuildOrderPredicate(
input.Status,
input.Keyword,
input.CreateTimeStart,
input.CreateTimeEnd,
OmsSessionHelper.TryGetCustomerCreateUserId(CurrentUser));
customerId);
}
/// <summary>
/// 将订单导出请求对象转换为仓储可执行的查询表达式。
/// </summary>
/// <param name="input">入参:导出筛选条件对象。</param>
/// <param name="customerId">入参:会话客户 ID,空值表示不按客户隔离。</param>
/// <returns>出参:订单查询表达式。</returns>
private Expression<Func<Order, bool>> BuildExportPredicate(OrderExportInput input)
private Expression<Func<Order, bool>> BuildExportPredicate(OrderExportInput input, int? customerId)
{
return BuildOrderPredicate(
input.Status,
input.Keyword,
input.CreateTimeStart,
input.CreateTimeEnd,
OmsSessionHelper.TryGetCustomerCreateUserId(CurrentUser));
customerId);
}
/// <summary>
@ -443,6 +456,7 @@ public partial class OrderService : ApplicationService
/// <param name="keyword">入参:关键词,可匹配运单号、客户单号、跟踪号。</param>
/// <param name="createTimeStart">入参:创建时间开始值。</param>
/// <param name="createTimeEnd">入参:创建时间结束值。</param>
/// <param name="customerId">入参:客户主键,空值表示不按客户隔离。</param>
/// <returns>出参:可直接交给 SqlSugar 仓储执行的订单筛选表达式。</returns>
/// <remarks>
/// 核心业务逻辑:
@ -456,14 +470,14 @@ public partial class OrderService : ApplicationService
string? keyword,
DateTime? createTimeStart,
DateTime? createTimeEnd,
int? createUserId = null)
int? customerId = null)
{
status = status?.Trim();
keyword = keyword?.Trim();
var hasStatusFilter = !string.IsNullOrWhiteSpace(status) && status != "all";
var hasKeywordFilter = !string.IsNullOrWhiteSpace(keyword);
var hasCreateUserFilter = createUserId.HasValue;
var createUserIdValue = createUserId.GetValueOrDefault();
var hasCustomerFilter = customerId.HasValue;
var customerIdValue = customerId.GetValueOrDefault();
// 显式拆开时间分支,避免 SqlSugar 在翻译 Nullable<DateTime> 判空时生成异常 SQL。
if (createTimeStart.HasValue && createTimeEnd.HasValue)
@ -471,7 +485,7 @@ public partial class OrderService : ApplicationService
var start = createTimeStart.Value;
var end = createTimeEnd.Value;
return order =>
(!hasCreateUserFilter || order.CreateUserId == createUserIdValue) &&
(!hasCustomerFilter || order.CustomerId == customerIdValue) &&
(!hasStatusFilter || order.OrderStatus == status) &&
(!hasKeywordFilter ||
order.ServiceNo.Contains(keyword!) ||
@ -485,7 +499,7 @@ public partial class OrderService : ApplicationService
{
var start = createTimeStart.Value;
return order =>
(!hasCreateUserFilter || order.CreateUserId == createUserIdValue) &&
(!hasCustomerFilter || order.CustomerId == customerIdValue) &&
(!hasStatusFilter || order.OrderStatus == status) &&
(!hasKeywordFilter ||
order.ServiceNo.Contains(keyword!) ||
@ -498,7 +512,7 @@ public partial class OrderService : ApplicationService
{
var end = createTimeEnd.Value;
return order =>
(!hasCreateUserFilter || order.CreateUserId == createUserIdValue) &&
(!hasCustomerFilter || order.CustomerId == customerIdValue) &&
(!hasStatusFilter || order.OrderStatus == status) &&
(!hasKeywordFilter ||
order.ServiceNo.Contains(keyword!) ||
@ -508,7 +522,7 @@ public partial class OrderService : ApplicationService
}
return order =>
(!hasCreateUserFilter || order.CreateUserId == createUserIdValue) &&
(!hasCustomerFilter || order.CustomerId == customerIdValue) &&
(!hasStatusFilter || order.OrderStatus == status) &&
(!hasKeywordFilter ||
order.ServiceNo.Contains(keyword!) ||
@ -516,17 +530,20 @@ public partial class OrderService : ApplicationService
(order.TrackNo ?? string.Empty).Contains(keyword!));
}
private void EnsureCustomerCanAccessOrder(Order order)
private async Task EnsureCustomerCanAccessOrderAsync(Order order)
{
var createUserId = OmsSessionHelper.TryGetCustomerCreateUserId(CurrentUser);
if (createUserId is null)
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
null);
if (!customerId.HasValue)
{
return;
}
if (order.CreateUserId != createUserId.Value)
if (order.CustomerId != customerId.Value)
{
throw new BusinessException("无权访问该订单");
throw new BusinessException("订单不存在");
}
}
@ -538,7 +555,7 @@ public partial class OrderService : ApplicationService
throw new BusinessException("订单不存在");
}
EnsureCustomerCanAccessOrder(entity);
await EnsureCustomerCanAccessOrderAsync(entity);
return entity;
}

View File

@ -3,7 +3,7 @@ namespace Oms.Application.Services.Orders;
/// <summary>
/// 查价/试算用的地址与件重尺上下文(不落库)。
/// </summary>
public sealed class ParcelQuoteContext
internal sealed class ParcelQuoteContext
{
public string CountryCode { get; init; } = string.Empty;

View File

@ -5,7 +5,7 @@ namespace Oms.Application.Services.Orders;
/// <summary>
/// 单产品销售产品试算结果(与 parcel/create 预报计费同源)。
/// </summary>
public sealed class ParcelQuoteResult
internal sealed class ParcelQuoteResult
{
public int ProductId { get; init; }

View File

@ -14,6 +14,9 @@ using Oms.Domain.RechargeAggregate.Repositories;
namespace Oms.Application.Services.Recharge;
/// <summary>
/// 充值相关 API 服务。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Recharge")]
public class RechargeService : ApplicationService
{
@ -245,6 +248,7 @@ public class RechargeService : ApplicationService
/// 将充值分页请求对象转换为仓储可执行的查询表达式。
/// </summary>
/// <param name="input">入参:充值分页查询条件对象。</param>
/// <param name="customerId">入参:客户 ID,空值表示不过滤。</param>
/// <returns>出参:充值记录查询表达式。</returns>
private Expression<Func<RechargeRecord, bool>> BuildPaginatedPredicate(
RechargeRecordPaginatedInput input,
@ -304,6 +308,7 @@ public class RechargeService : ApplicationService
/// 将充值导出请求对象转换为仓储可执行的查询表达式。
/// </summary>
/// <param name="input">入参:充值导出筛选条件对象。</param>
/// <param name="customerId">入参:客户 ID,空值表示不过滤。</param>
/// <returns>出参:充值记录查询表达式。</returns>
private Expression<Func<RechargeRecord, bool>> BuildExportPredicate(RechargeExportInput input, int? customerId)
{

View File

@ -5,6 +5,9 @@ using Oms.Domain.SystemAggregate.Repositories;
namespace Oms.Application.Services.System;
/// <summary>
/// 国家 API 服务。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-System")]
public class CountryService : ApplicationService
{

View File

@ -9,6 +9,9 @@ using Oms.Domain.SystemAggregate.Repositories;
namespace Oms.Application.Services.System;
/// <summary>
/// 字典 API 服务。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-System")]
public class DictService : ApplicationService
{

View File

@ -12,6 +12,9 @@ using Oms.Domain.SystemAggregate.Repositories;
namespace Oms.Application.Services.System;
/// <summary>
/// 公告 API 服务。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-System")]
public class NoticeService : ApplicationService
{

View File

@ -10,6 +10,9 @@ using Oms.Domain.SystemAggregate.Repositories;
namespace Oms.Application.Services.System;
/// <summary>
/// 仓库 API 服务。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-System")]
public class WarehouseService : ApplicationService
{

View File

@ -5,6 +5,9 @@ using Oms.Domain.SystemAggregate.Consts;
namespace Oms.Application.Shared;
/// <summary>
/// OMS 通用文档与展示辅助方法。
/// </summary>
public static class OmsDocHelper
{
private static readonly JsonSerializerOptions JsonOptions = new()
@ -12,10 +15,19 @@ public static class OmsDocHelper
PropertyNameCaseInsensitive = true
};
/// <summary>
/// 将页码规范为至少 1。
/// </summary>
public static int NormalizePage(int page) => page <= 0 ? 1 : page;
/// <summary>
/// 将每页条数规范为 1~200。
/// </summary>
public static int NormalizePageSize(int pageSize) => pageSize <= 0 ? 20 : Math.Min(pageSize, 200);
/// <summary>
/// 将默认 DateTime 转为 null。
/// </summary>
public static DateTime? NullIfDefault(DateTime value) => value == default ? null : value;
/// <summary>
@ -32,6 +44,9 @@ public static class OmsDocHelper
return ConvertJsonElement(document.RootElement);
}
/// <summary>
/// 反序列化 JSON 字符串为指定类型。
/// </summary>
public static T? DeserializeJson<T>(string? json)
{
if (string.IsNullOrWhiteSpace(json))
@ -95,8 +110,14 @@ public static class OmsDocHelper
_ => element.GetRawText()
};
/// <summary>
/// 反序列化 JSON 字符串数组。
/// </summary>
public static List<string> DeserializeStringList(string? json) => JsonStringArrayHelper.Deserialize(json);
/// <summary>
/// 将订单状态码转为中文名称。
/// </summary>
public static string? GetOrderStatusName(string statusCode)
{
return statusCode switch
@ -117,6 +138,9 @@ public static class OmsDocHelper
};
}
/// <summary>
/// 将仓库类型码转为中文名称。
/// </summary>
public static string GetWarehouseTypeName(string warehouseType)
{
return warehouseType switch

View File

@ -10,10 +10,16 @@ namespace Oms.Application.Shared;
/// </summary>
public static class OmsSessionHelper
{
/// <summary>
/// JWT 中标识登录类型的 Claim 名称。
/// </summary>
public const string LoginTypeClaim = "loginType";
private const string CustomerLoginType = "1";
/// <summary>
/// 当前会话是否为客户门户登录。
/// </summary>
public static bool IsCustomerLogin(ICurrentUser currentUser) =>
string.Equals(
currentUser.FindClaimOrNull(LoginTypeClaim)?.Value,
@ -59,7 +65,7 @@ public static class OmsSessionHelper
if (requestedCustomerId is > 0 && requestedCustomerId.Value != sessionCustomer.Id)
{
throw new BusinessException("无权为该客户提交充值");
throw new BusinessException("客户ID非法");
}
return sessionCustomer;