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"]。
///