From 25b4211b46dbd1b1d392c9e2638a6835710309a0 Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Fri, 4 Sep 2026 18:39:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=94=80=E5=94=AE=E4=BA=A7=E5=93=81?= =?UTF-8?q?=E9=97=A8=E6=88=B7=E7=9B=AE=E5=BD=95=E3=80=81=E7=BA=BF=E8=B7=AF?= =?UTF-8?q?=E6=8E=92=E5=BA=8F=E4=B8=8E=E5=85=AC=E5=91=8A/=E5=88=B0?= =?UTF-8?q?=E8=B4=A7=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增门户目录接口与 route_sort 字段迁移;完善公告创建与包裹到货相关逻辑。 Co-authored-by: Cursor --- db.sql | 2 + sql/migrate_sales_product_route_sort.sql | 36 +++++ .../Dtos/Channels/SalesProductDetailOutput.cs | 5 + .../Dtos/Channels/SalesProductListOutput.cs | 5 + .../SalesProductPortalCatalogOutput.cs | 84 +++++++++++ .../Dtos/Channels/SalesProductSaveInput.cs | 5 + .../Dtos/System/NoticeCreateInput.cs | 5 +- .../Dtos/Warehouse/WarehouseDtos.cs | 1 + .../Channels/SalesProductService.Helpers.cs | 9 +- .../Channels/SalesProductService.Portal.cs | 130 ++++++++++++++++++ .../Services/System/NoticeService.cs | 79 ++++++++--- .../WarehouseService.ParcelArrival.cs | 23 +++- .../Entities/SalesProduct.cs | 5 + 13 files changed, 358 insertions(+), 31 deletions(-) create mode 100644 sql/migrate_sales_product_route_sort.sql create mode 100644 src/Oms.Application/Dtos/Channels/SalesProductPortalCatalogOutput.cs create mode 100644 src/Oms.Application/Services/Channels/SalesProductService.Portal.cs diff --git a/db.sql b/db.sql index 052f048..7ca48f4 100644 --- a/db.sql +++ b/db.sql @@ -509,6 +509,7 @@ CREATE TABLE `t_sales_product` ( `MinTimeliness` INT NULL COMMENT '最小时效(天)', `MaxTimeliness` INT NULL COMMENT '最大时效(天)', `TradeTerms` VARCHAR(16) NULL COMMENT '贸易条款:DDP/DDU', + `RouteSort` INT NOT NULL DEFAULT 0 COMMENT '官网线路排序,越小越靠前', `AdditionalServices` VARCHAR(512) NULL COMMENT '可选附加服务字典code JSON数组', `CustomerWaybill` VARCHAR(32) NULL COMMENT '系统面单/客户面单', `BlacklistAreaId` INT NULL COMMENT '黑名单区ID', @@ -530,6 +531,7 @@ CREATE TABLE `t_sales_product` ( PRIMARY KEY (`Id`), UNIQUE KEY `uk_sp_code` (`ProductCode`), KEY `idx_sp_group_active` (`ProductGroup`, `IsActive`), + KEY `idx_sp_route_sort` (`RouteSort`, `IsActive`), KEY `idx_sp_scheme` (`Scheme`), KEY `idx_sp_weight_rule` (`WeightRuleId`), KEY `idx_sp_weight_rounding` (`WeightRoundingId`) diff --git a/sql/migrate_sales_product_route_sort.sql b/sql/migrate_sales_product_route_sort.sql new file mode 100644 index 0000000..54d6fd1 --- /dev/null +++ b/sql/migrate_sales_product_route_sort.sql @@ -0,0 +1,36 @@ +-- t_sales_product:官网线路展示排序(幂等;列已存在则跳过) +-- 数值越小越靠前;默认 0 + +SET @db := DATABASE(); + +SET @col := ( + SELECT COUNT(*) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db + AND TABLE_NAME = 't_sales_product' + AND COLUMN_NAME = 'RouteSort' +); +SET @sql := IF( + @col = 0, + 'ALTER TABLE `t_sales_product` ADD COLUMN `RouteSort` INT NOT NULL DEFAULT 0 COMMENT ''官网线路排序,越小越靠前'' AFTER `TradeTerms`', + 'SELECT ''skip RouteSort'' AS msg' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @idx := ( + SELECT COUNT(*) + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = @db + AND TABLE_NAME = 't_sales_product' + AND INDEX_NAME = 'idx_sp_route_sort' +); +SET @sql_idx := IF( + @idx = 0, + 'ALTER TABLE `t_sales_product` ADD KEY `idx_sp_route_sort` (`RouteSort`, `IsActive`)', + 'SELECT ''skip idx_sp_route_sort'' AS msg' +); +PREPARE stmt_idx FROM @sql_idx; +EXECUTE stmt_idx; +DEALLOCATE PREPARE stmt_idx; diff --git a/src/Oms.Application/Dtos/Channels/SalesProductDetailOutput.cs b/src/Oms.Application/Dtos/Channels/SalesProductDetailOutput.cs index 992482d..2ddcfa8 100644 --- a/src/Oms.Application/Dtos/Channels/SalesProductDetailOutput.cs +++ b/src/Oms.Application/Dtos/Channels/SalesProductDetailOutput.cs @@ -40,6 +40,11 @@ public class SalesProductDetailOutput /// public string? TradeTerms { get; set; } + /// + /// 官网线路排序,数值越小越靠前。 + /// + public int RouteSort { get; set; } + /// /// 可选附加服务字典 code JSON 数组。 /// diff --git a/src/Oms.Application/Dtos/Channels/SalesProductListOutput.cs b/src/Oms.Application/Dtos/Channels/SalesProductListOutput.cs index 9fd6049..3743ba9 100644 --- a/src/Oms.Application/Dtos/Channels/SalesProductListOutput.cs +++ b/src/Oms.Application/Dtos/Channels/SalesProductListOutput.cs @@ -35,6 +35,11 @@ public class SalesProductListOutput /// public int? MaxTimeliness { get; set; } + /// + /// 官网线路排序,数值越小越靠前。 + /// + public int RouteSort { get; set; } + /// /// 可选附加服务字典 code JSON 数组。 /// diff --git a/src/Oms.Application/Dtos/Channels/SalesProductPortalCatalogOutput.cs b/src/Oms.Application/Dtos/Channels/SalesProductPortalCatalogOutput.cs new file mode 100644 index 0000000..fff802a --- /dev/null +++ b/src/Oms.Application/Dtos/Channels/SalesProductPortalCatalogOutput.cs @@ -0,0 +1,84 @@ +namespace Oms.Application.Dtos.Channels; + +/// +/// 官网线路目录出参(公开字段)。 +/// +public class SalesProductPortalCatalogOutput +{ + /// + /// 分组列表(已排除国内揽收,且仅含有产品的分组)。 + /// + public List Groups { get; set; } = []; + + /// + /// 启用中的销售产品(已排除国内揽收)。 + /// + public List Items { get; set; } = []; +} + +/// +/// 官网线路分组。 +/// +public class SalesProductPortalGroupOutput +{ + /// + /// 分组编码(字典 code)。 + /// + public string Code { get; set; } = string.Empty; + + /// + /// 分组名称。 + /// + public string Name { get; set; } = string.Empty; +} + +/// +/// 官网线路产品卡片字段。 +/// +public class SalesProductPortalItemOutput +{ + /// + /// 主键。 + /// + public int Id { get; set; } + + /// + /// 产品名称。 + /// + public string ProductName { get; set; } = string.Empty; + + /// + /// 产品分组编码。 + /// + public string? ProductGroup { get; set; } + + /// + /// 产品分组名称。 + /// + public string ProductGroupName { get; set; } = string.Empty; + + /// + /// 最小时效(天)。 + /// + public int? MinTimeliness { get; set; } + + /// + /// 最大时效(天)。 + /// + public int? MaxTimeliness { get; set; } + + /// + /// 贸易条款(DDP/DDU)。 + /// + public string? TradeTerms { get; set; } + + /// + /// 官网线路排序,数值越小越靠前。 + /// + public int RouteSort { get; set; } + + /// + /// 产品说明。 + /// + public string? ProductDescription { get; set; } +} diff --git a/src/Oms.Application/Dtos/Channels/SalesProductSaveInput.cs b/src/Oms.Application/Dtos/Channels/SalesProductSaveInput.cs index aa59bf7..7cb0f1f 100644 --- a/src/Oms.Application/Dtos/Channels/SalesProductSaveInput.cs +++ b/src/Oms.Application/Dtos/Channels/SalesProductSaveInput.cs @@ -42,6 +42,11 @@ public class SalesProductSaveInput /// public string? TradeTerms { get; set; } + /// + /// 官网线路排序,数值越小越靠前。 + /// + public int RouteSort { get; set; } + /// /// 可选附加服务字典 code JSON 数组。 /// diff --git a/src/Oms.Application/Dtos/System/NoticeCreateInput.cs b/src/Oms.Application/Dtos/System/NoticeCreateInput.cs index c9742ac..e31841b 100644 --- a/src/Oms.Application/Dtos/System/NoticeCreateInput.cs +++ b/src/Oms.Application/Dtos/System/NoticeCreateInput.cs @@ -32,14 +32,13 @@ public class NoticeCreateInput public byte IsPopup { get; set; } /// - /// 接收对象类型:user/customer。 + /// 接收对象类型:user/customer。公告全体广播时可空(后端按 user 占位)。 /// - [Required] [StringLength(16)] public string ReceiverType { get; set; } = string.Empty; /// - /// 接收主体 ID 列表。 + /// 接收主体 ID 列表。公告传空表示全体生效;站内信必填。 /// public List ReceiverIds { get; set; } = []; } diff --git a/src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs b/src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs index 7930f38..85f3df5 100644 --- a/src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs +++ b/src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs @@ -579,6 +579,7 @@ public class WarehouseParcelArrivalListInput : WarehousePageInput public class WarehouseParcelArrivalCreateInput { + /// 已废弃:到仓按仓库+快递单号定位包裹,客户归属取自包裹。 public int? CustomerId { get; set; } [Required] diff --git a/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs b/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs index e5b097c..ecf7284 100644 --- a/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs +++ b/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs @@ -211,6 +211,7 @@ public partial class SalesProductService MinTimeliness = product.MinTimeliness, MaxTimeliness = product.MaxTimeliness, TradeTerms = product.TradeTerms, + RouteSort = product.RouteSort, AdditionalServices = product.AdditionalServices, CustomerWaybill = product.CustomerWaybill, LabelTemplateId = product.LabelTemplateId, @@ -221,7 +222,8 @@ public partial class SalesProductService WeightRoundingId = product.WeightRoundingId, ProductDescription = product.ProductDescription, PreStorageScript = product.PreStorageScript, - CustomerGuideMarkdown = product.CustomerGuideMarkdown, + // 对客说明不常变动,且有独立保存入口;规则发布快照跳过,避免配置发布被其 diff 干扰 + CustomerGuideMarkdown = null, IsActive = product.IsActive, Scheme = product.Scheme }; @@ -333,6 +335,7 @@ public partial class SalesProductService entity.MinTimeliness = input.MinTimeliness; entity.MaxTimeliness = input.MaxTimeliness; entity.TradeTerms = NormalizeTradeTerms(input.TradeTerms); + entity.RouteSort = input.RouteSort < 0 ? 0 : input.RouteSort; entity.AdditionalServices = NormalizeAdditionalServices(input.AdditionalServices); entity.CustomerWaybill = input.CustomerWaybill; entity.LabelTemplateId = input.LabelTemplateId; @@ -344,7 +347,7 @@ public partial class SalesProductService entity.WeightRoundingId = input.WeightRoundingId; entity.ProductDescription = input.ProductDescription; entity.PreStorageScript = input.PreStorageScript; - entity.CustomerGuideMarkdown = input.CustomerGuideMarkdown; + // 对客说明仅通过 /customer-guide/save 维护,产品配置保存/发布不覆盖 entity.IsActive = input.IsActive; entity.Scheme = string.IsNullOrWhiteSpace(input.Scheme) ? AllocationSchemeCodes.CostLowest : input.Scheme; } @@ -444,6 +447,7 @@ public partial class SalesProductService ProductGroup = entity.ProductGroup, MinTimeliness = entity.MinTimeliness, MaxTimeliness = entity.MaxTimeliness, + RouteSort = entity.RouteSort, AdditionalServices = entity.AdditionalServices, CustomerWaybill = entity.CustomerWaybill, LabelTemplateId = entity.LabelTemplateId, @@ -473,6 +477,7 @@ public partial class SalesProductService MinTimeliness = entity.MinTimeliness, MaxTimeliness = entity.MaxTimeliness, TradeTerms = entity.TradeTerms, + RouteSort = entity.RouteSort, AdditionalServices = entity.AdditionalServices, CustomerWaybill = entity.CustomerWaybill, LabelTemplateId = entity.LabelTemplateId, diff --git a/src/Oms.Application/Services/Channels/SalesProductService.Portal.cs b/src/Oms.Application/Services/Channels/SalesProductService.Portal.cs new file mode 100644 index 0000000..12c0ed9 --- /dev/null +++ b/src/Oms.Application/Services/Channels/SalesProductService.Portal.cs @@ -0,0 +1,130 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Oms.Application.Dtos.Channels; +using Oms.Domain.Shared.Enums; +using Oms.Domain.SystemAggregate.Repositories; + +namespace Oms.Application.Services.Channels; + +public partial class SalesProductService +{ + private const string SalesProductGroupDictType = "sales_product_group"; + private const string UngroupedProductGroupCode = "_ungrouped"; + private const string DomesticPickupGroupNameKeyword = "国内揽收"; + + private IDictRepository DictRepository => + LazyServiceProvider.GetRequiredService(); + + /// + /// 官网线路目录(匿名):仅启用产品,排除「国内揽收」分组。 + /// + [AllowAnonymous] + [HttpGet("/channel/sales-product/portal/catalog")] + public async Task GetPortalCatalogAsync() + { + var products = await SalesProductRepository.GetListAsync(x => x.IsActive == EnableStatus.Enabled); + var dictRows = await DictRepository.GetListAsync(x => x.DictType == SalesProductGroupDictType); + + var domesticPickupGroupCodes = dictRows + .Where(x => !string.IsNullOrWhiteSpace(x.DictName) + && x.DictName.Trim().Contains(DomesticPickupGroupNameKeyword)) + .Select(x => (x.DictCode ?? string.Empty).Trim()) + .Where(code => !string.IsNullOrWhiteSpace(code)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + var groupNameMap = dictRows + .Where(x => !string.IsNullOrWhiteSpace(x.DictCode)) + .GroupBy(x => x.DictCode!.Trim(), StringComparer.OrdinalIgnoreCase) + .ToDictionary( + g => g.Key, + g => + { + var name = g.First().DictName?.Trim(); + return string.IsNullOrWhiteSpace(name) ? g.Key : name; + }, + StringComparer.OrdinalIgnoreCase); + groupNameMap[UngroupedProductGroupCode] = "未分组"; + + var filtered = products + .Where(p => + { + var groupCode = string.IsNullOrWhiteSpace(p.ProductGroup) + ? UngroupedProductGroupCode + : p.ProductGroup.Trim(); + return !domesticPickupGroupCodes.Contains(groupCode); + }) + .OrderBy(p => p.RouteSort) + .ThenBy(p => p.MinTimeliness ?? int.MaxValue) + .ThenBy(p => p.ProductName, StringComparer.OrdinalIgnoreCase) + .ThenBy(p => p.Id) + .ToList(); + + var usedGroupCodes = new HashSet(StringComparer.OrdinalIgnoreCase); + var items = filtered.Select(p => + { + var groupCode = string.IsNullOrWhiteSpace(p.ProductGroup) + ? UngroupedProductGroupCode + : p.ProductGroup.Trim(); + usedGroupCodes.Add(groupCode); + groupNameMap.TryGetValue(groupCode, out var groupName); + return new SalesProductPortalItemOutput + { + Id = p.Id, + ProductName = p.ProductName, + ProductGroup = groupCode == UngroupedProductGroupCode ? null : groupCode, + ProductGroupName = string.IsNullOrWhiteSpace(groupName) ? groupCode : groupName, + MinTimeliness = p.MinTimeliness, + MaxTimeliness = p.MaxTimeliness, + TradeTerms = string.IsNullOrWhiteSpace(p.TradeTerms) ? null : p.TradeTerms.Trim().ToUpperInvariant(), + RouteSort = p.RouteSort, + ProductDescription = p.ProductDescription + }; + }).ToList(); + + var groups = new List(); + var consumed = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var dict in dictRows.OrderBy(x => x.DictSort).ThenBy(x => x.DictCode)) + { + var code = dict.DictCode?.Trim(); + if (string.IsNullOrWhiteSpace(code) + || domesticPickupGroupCodes.Contains(code) + || !usedGroupCodes.Contains(code) + || !consumed.Add(code)) + { + continue; + } + + groups.Add(new SalesProductPortalGroupOutput + { + Code = code, + Name = string.IsNullOrWhiteSpace(dict.DictName) ? code : dict.DictName.Trim() + }); + } + + if (usedGroupCodes.Contains(UngroupedProductGroupCode) && consumed.Add(UngroupedProductGroupCode)) + { + groups.Add(new SalesProductPortalGroupOutput + { + Code = UngroupedProductGroupCode, + Name = groupNameMap[UngroupedProductGroupCode] + }); + } + + foreach (var code in usedGroupCodes.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) + { + if (!consumed.Add(code)) continue; + groupNameMap.TryGetValue(code, out var name); + groups.Add(new SalesProductPortalGroupOutput + { + Code = code, + Name = string.IsNullOrWhiteSpace(name) ? code : name + }); + } + + return new SalesProductPortalCatalogOutput + { + Groups = groups, + Items = items + }; + } +} diff --git a/src/Oms.Application/Services/System/NoticeService.cs b/src/Oms.Application/Services/System/NoticeService.cs index 6406e53..d804de4 100644 --- a/src/Oms.Application/Services/System/NoticeService.cs +++ b/src/Oms.Application/Services/System/NoticeService.cs @@ -130,14 +130,15 @@ public class NoticeService : ApplicationService { var noticeType = NormalizeNoticeType(input.NoticeType); ValidateNoticeText(noticeType, input.Title, input.Content); - var receiverIds = await NormalizeAndValidateReceiverIdsAsync(input.ReceiverType, input.ReceiverIds); + var receiverType = ResolveReceiverType(noticeType, input.ReceiverType); + var receiverIds = await NormalizeAndValidateReceiverIdsAsync(noticeType, receiverType, input.ReceiverIds); await NoticeRepository.InsertAsync(new Notice { NoticeType = noticeType, Title = NormalizeTitle(input.Title), Content = input.Content.Trim(), IsPopup = noticeType == NoticeTypeMessage ? (byte)0 : NormalizePopupValue(input.IsPopup), - ReceiverType = NormalizeReceiverType(input.ReceiverType), + ReceiverType = receiverType, ReceiverIds = OmsDocHelper.SerializeJson(receiverIds) ?? "[]" }); } @@ -151,20 +152,27 @@ public class NoticeService : ApplicationService var entity = await GetNoticeOrThrowAsync(input.Id); var noticeType = NormalizeNoticeType(input.NoticeType); ValidateNoticeText(noticeType, input.Title, input.Content); - var receiverIds = await NormalizeAndValidateReceiverIdsAsync(input.ReceiverType, input.ReceiverIds); + var receiverType = ResolveReceiverType(noticeType, input.ReceiverType); + var receiverIds = await NormalizeAndValidateReceiverIdsAsync(noticeType, receiverType, input.ReceiverIds); entity.NoticeType = noticeType; entity.Title = NormalizeTitle(input.Title); entity.Content = input.Content.Trim(); entity.IsPopup = noticeType == NoticeTypeMessage ? (byte)0 : NormalizePopupValue(input.IsPopup); - entity.ReceiverType = NormalizeReceiverType(input.ReceiverType); + entity.ReceiverType = receiverType; entity.ReceiverIds = OmsDocHelper.SerializeJson(receiverIds) ?? "[]"; await NoticeRepository.UpdateAsync(entity); - var existsReads = await NoticeReadRepository.GetListAsync(x => x.NoticeId == entity.Id); - var receiverIdSet = receiverIds.ToHashSet(); - foreach (var read in existsReads.Where(x => x.ReceiverType != ReceiverTypeUser || !receiverIdSet.Contains(x.ReceiverId))) + // 全体广播不清理已读;定向接收时移除已不在名单内的已读记录 + if (receiverIds.Count > 0) { - await NoticeReadRepository.DeleteAsync(read); + var existsReads = await NoticeReadRepository.GetListAsync(x => x.NoticeId == entity.Id); + var receiverIdSet = receiverIds.ToHashSet(); + foreach (var read in existsReads.Where(x => + !string.Equals(x.ReceiverType, receiverType, StringComparison.OrdinalIgnoreCase) || + !receiverIdSet.Contains(x.ReceiverId))) + { + await NoticeReadRepository.DeleteAsync(read); + } } } @@ -186,14 +194,17 @@ public class NoticeService : ApplicationService { var notice = await GetNoticeOrThrowAsync(input.Id); var receiverIds = OmsDocHelper.DeserializeJson>(notice.ReceiverIds) ?? []; - var currentReceiverType = NormalizeReceiverType(notice.ReceiverType); + var currentReceiverType = OmsSessionHelper.IsCustomerLogin(CurrentUser) + ? ReceiverTypeCustomer + : ReceiverTypeUser; var currentReceiverId = CurrentUser.GetId(); if (currentReceiverId <= 0) { throw new BusinessException("当前登录主体无效"); } - if (!receiverIds.Contains(currentReceiverId)) + // 空接收人 = 全体广播;否则校验当前主体在定向名单内 + if (receiverIds.Count > 0 && !receiverIds.Contains(currentReceiverId)) { throw new BusinessException("当前主体不在通知接收范围内"); } @@ -355,11 +366,15 @@ public class NoticeService : ApplicationService (string.IsNullOrWhiteSpace(keyword) || x.Title.Contains(keyword) || x.Content.Contains(keyword)) && - x.ReceiverType == expectedReceiverType && - (x.ReceiverIds == $"[{receiverIdToken}]" || - x.ReceiverIds.StartsWith($"[{receiverIdToken},") || - x.ReceiverIds.Contains($",{receiverIdToken},") || - x.ReceiverIds.EndsWith($",{receiverIdToken}]")) && + ( + // 空接收人列表 = 全体广播(员工/客户均可见) + x.ReceiverIds == "[]" || + (x.ReceiverType == expectedReceiverType && + (x.ReceiverIds == $"[{receiverIdToken}]" || + x.ReceiverIds.StartsWith($"[{receiverIdToken},") || + x.ReceiverIds.Contains($",{receiverIdToken},") || + x.ReceiverIds.EndsWith($",{receiverIdToken}]"))) + ) && (excluded.Count == 0 || !excluded.Contains(x.Id)); } @@ -374,19 +389,29 @@ public class NoticeService : ApplicationService return entity; } - private async Task> NormalizeAndValidateReceiverIdsAsync(string receiverType, IEnumerable? receiverIds) + private async Task> NormalizeAndValidateReceiverIdsAsync( + string noticeType, + string receiverType, + IEnumerable? receiverIds) { - var normalizedReceiverType = NormalizeReceiverType(receiverType); var selectionIds = (receiverIds ?? []) .Where(x => x > 0) .Distinct() .ToList(); + // 公告默认全体生效,允许空接收人;站内信必须指定接收人 if (selectionIds.Count == 0) { + if (noticeType == NoticeTypeNotice) + { + return []; + } + throw new BusinessException("接收人不能为空"); } + var normalizedReceiverType = NormalizeReceiverType(receiverType); + if (normalizedReceiverType == ReceiverTypeCustomer) { var customers = await CustomerRepository.GetListAsync(x => @@ -420,6 +445,19 @@ public class NoticeService : ApplicationService return selectionIds; } + /// + /// 公告全体广播时接收类型仅作占位;站内信必须明确 user/customer。 + /// + private static string ResolveReceiverType(string noticeType, string? receiverType) + { + if (noticeType == NoticeTypeNotice && string.IsNullOrWhiteSpace(receiverType)) + { + return ReceiverTypeUser; + } + + return NormalizeReceiverType(receiverType ?? string.Empty); + } + private async Task> QueryValidUserIdsAsync(IReadOnlyCollection userIds) { if (userIds.Count == 0) @@ -456,6 +494,7 @@ public class NoticeService : ApplicationService List receiverSelectionIds) { var receiverIds = OmsDocHelper.DeserializeJson>(entity.ReceiverIds) ?? []; + var isBroadcast = receiverIds.Count == 0; return new NoticeOutput { Id = entity.Id, @@ -465,12 +504,14 @@ public class NoticeService : ApplicationService Content = entity.Content, IsPopup = entity.IsPopup, ReceiverType = entity.ReceiverType, - ReceiverTypeName = entity.ReceiverType == ReceiverTypeCustomer ? "客户" : "用户", + ReceiverTypeName = isBroadcast + ? "全部" + : entity.ReceiverType == ReceiverTypeCustomer ? "客户" : "用户", ReceiverIds = receiverIds, ReceiverSelectionIds = receiverSelectionIds, ReadCount = readCount, IsRead = isRead, - ReceiverCount = receiverIds.Distinct().Count(), + ReceiverCount = isBroadcast ? 0 : receiverIds.Distinct().Count(), CreateTime = entity.CreateTime, UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime) }; diff --git a/src/Oms.Application/Services/Warehouse/WarehouseService.ParcelArrival.cs b/src/Oms.Application/Services/Warehouse/WarehouseService.ParcelArrival.cs index 74a2d27..91d24be 100644 --- a/src/Oms.Application/Services/Warehouse/WarehouseService.ParcelArrival.cs +++ b/src/Oms.Application/Services/Warehouse/WarehouseService.ParcelArrival.cs @@ -96,7 +96,10 @@ public partial class WarehouseService [HttpPost("/warehouse/parcel-arrival/create")] public async Task CreateParcelArrivalAsync(WarehouseParcelArrivalCreateInput input) { - var customerId = await ResolveMutationCustomerIdAsync(input.CustomerId); + // 到仓按仓库 + 快递单号定位包裹;客户归属取自包裹本身,管理端无需先选客户。 + // 客户门户会话仍会限制只能到自己的包裹。 + var sessionCustomerId = await ResolveListCustomerIdAsync(null); + var (hasCustomerFilter, customerIdValue) = CustomerFilter(sessionCustomerId); if (input.WarehouseId <= 0) { throw new BusinessException("仓库ID不能为空"); @@ -111,15 +114,21 @@ public partial class WarehouseService var warehouse = await WarehouseAddressRepository.FirstOrDefaultAsync(x => x.Id == input.WarehouseId) ?? throw new BusinessException("仓库不存在"); - var parcel = await ParcelRepository.FirstOrDefaultAsync(x => - x.CustomerId == customerId - && x.WarehouseId == input.WarehouseId - && x.TrackingNo == trackingNo); - if (parcel == null) + var parcels = await ParcelRepository.GetListAsync(x => + x.WarehouseId == input.WarehouseId + && x.TrackingNo == trackingNo + && (!hasCustomerFilter || x.CustomerId == customerIdValue)); + if (parcels.Count == 0) { throw new BusinessException("未找到对应包裹,请确认快递单号与仓库"); } + if (parcels.Count > 1) + { + throw new BusinessException("同一仓库下存在多条相同快递单号的包裹,请先在「我的包裹」核对后再到仓"); + } + + var parcel = parcels[0]; if (parcel.LifecycleStatus == WarehouseParcelLifecycleCodes.Stored) { throw new BusinessException("包裹已上架,无法再到仓"); @@ -131,7 +140,7 @@ public partial class WarehouseService var entity = new CParcelArrival { - CustomerId = customerId, + CustomerId = parcel.CustomerId, WarehouseId = input.WarehouseId, ParcelId = parcel.Id, TrackingNo = trackingNo diff --git a/src/Oms.Domain/ChannelsAggregate/Entities/SalesProduct.cs b/src/Oms.Domain/ChannelsAggregate/Entities/SalesProduct.cs index ba63d85..a6a65ec 100644 --- a/src/Oms.Domain/ChannelsAggregate/Entities/SalesProduct.cs +++ b/src/Oms.Domain/ChannelsAggregate/Entities/SalesProduct.cs @@ -22,6 +22,11 @@ public class SalesProduct : OmsAuditedAggregateRoot /// public string? TradeTerms { get; set; } + /// + /// 官网线路排序,数值越小越靠前。 + /// + public int RouteSort { get; set; } + /// /// 可选附加服务字典 code JSON 数组,如 ["insurance","remoteFee"]。 ///