diff --git a/sql/20260803_pickup_order.sql b/sql/20260803_pickup_order.sql index 6e710b1..62d1cbe 100644 --- a/sql/20260803_pickup_order.sql +++ b/sql/20260803_pickup_order.sql @@ -1,21 +1,31 @@ -- 揽收单(独立履约/结算)+ 与物流单 1:N 关联;物流单冗余到仓方式与揽收 FK。 +-- 对齐现表公共字段:ConcurrencyStamp / OrganizationId / SecretLevel;发件快照对齐 t_order_sender。 -- 文档:前端 docs/揽收单与物流单串联方案.md -- 执行前请备份。 +-- 若表已按旧版脚本建好,请改跑 sql/20260803_pickup_order_align_fields.sql CREATE TABLE IF NOT EXISTS `t_pickup_order` ( - `Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键', + `Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增', `PickupNo` VARCHAR(32) NOT NULL COMMENT '揽收单号', + `RequestId` VARCHAR(64) NULL COMMENT '请求幂等ID', `CustomerId` INT NOT NULL COMMENT '客户Id', + `OrganizationId` CHAR(36) NULL COMMENT '数据归属组织', + `SecretLevel` TINYINT NOT NULL DEFAULT 2 COMMENT '数据密级 1-4', `SenderAddressId` INT NOT NULL COMMENT '发件地址簿Id', `SenderName` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '取件联系人快照', - `SenderPhone` VARCHAR(32) NULL COMMENT '取件电话快照', `SenderCompany` VARCHAR(255) NULL COMMENT '公司快照', + `SenderCountry` INT NULL COMMENT '国家 t_country.Id 快照', `SenderCountryCode` VARCHAR(8) NULL COMMENT '国家码快照', `SenderProvince` VARCHAR(50) NULL COMMENT '省快照', `SenderCity` VARCHAR(50) NULL COMMENT '市快照', `SenderZipCode` VARCHAR(50) NULL COMMENT '邮编快照', `SenderStreetAddress1` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '地址1快照', `SenderStreetAddress2` VARCHAR(500) NULL COMMENT '地址2快照', + `SenderStreetAddress3` VARCHAR(500) NULL COMMENT '地址3快照', + `SenderHouseNumber` VARCHAR(50) NULL COMMENT '门牌号快照', + `SenderPhone` VARCHAR(50) NULL COMMENT '手机快照', + `SenderTelePhone` VARCHAR(50) NULL COMMENT '电话快照', + `SenderEmail` VARCHAR(60) NULL COMMENT '邮箱快照', `WarehouseId` INT NOT NULL COMMENT '送到仓库Id', `CarrierCode` VARCHAR(64) NULL COMMENT '承运商编码(draft可空)', `CarrierService` VARCHAR(64) NULL COMMENT '承运服务(draft可空)', @@ -40,25 +50,32 @@ CREATE TABLE IF NOT EXISTS `t_pickup_order` ( `SettlementBatchNo` VARCHAR(64) NULL COMMENT '结算批次号', `SettledAt` DATETIME NULL COMMENT '结算时间', `Remark` VARCHAR(512) NULL COMMENT '备注', - `IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除', + `IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是', + -- CREATE 可用 DEFAULT (UUID());若环境同样禁非确定默认值,可去掉 DEFAULT,由应用写入 + `ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳', `CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人', `CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `UpdateUserId` INT NULL COMMENT '更新人', `UpdateTime` DATETIME NULL COMMENT '更新时间', PRIMARY KEY (`Id`), UNIQUE KEY `uk_pickup_no` (`PickupNo`), + UNIQUE KEY `uk_pickup_request_id` (`RequestId`), KEY `idx_pickup_customer_status` (`CustomerId`, `Status`, `CreateTime`), KEY `idx_pickup_warehouse_time` (`WarehouseId`, `ExpectPickupTime`), KEY `idx_pickup_pay` (`PayStatus`), - KEY `idx_pickup_settle` (`SettlementStatus`) + KEY `idx_pickup_settle` (`SettlementStatus`), + KEY `idx_pickup_org_secret_ctime` (`OrganizationId`, `SecretLevel`, `CreateTime`) ) COMMENT='揽收单(上门取件履约+对客收款+承运结算)'; CREATE TABLE IF NOT EXISTS `t_pickup_order_line` ( - `Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键', + `Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增', `PickupOrderId` INT NOT NULL COMMENT '揽收单Id', `OrderId` INT NOT NULL COMMENT '物流订单Id', + `ServiceNo` VARCHAR(64) NULL COMMENT '运单号快照(便于列表)', + `ClientNo` VARCHAR(64) NULL COMMENT '客户单号快照', `SortNo` INT NOT NULL DEFAULT 0 COMMENT '展示序', - `IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除', + `IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是', + `ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳', `CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人', `CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `UpdateUserId` INT NULL COMMENT '更新人', diff --git a/sql/20260803_pickup_order_align_fields.sql b/sql/20260803_pickup_order_align_fields.sql new file mode 100644 index 0000000..638fce1 --- /dev/null +++ b/sql/20260803_pickup_order_align_fields.sql @@ -0,0 +1,53 @@ +-- 对齐已创建的 t_pickup_order / t_pickup_order_line 与现表公共字段。 +-- 适用于已执行旧版 20260803_pickup_order.sql 的环境;列已存在时对应语句会报错,可跳过。 +-- 执行前请备份。 +-- +-- 注意:ConcurrencyStamp 勿写 DEFAULT (UUID()),在开启复制/GTID 时 ALTER 会报 1674。 +-- 正确做法:先可空列 → UPDATE 回填 → 再改为 NOT NULL。 + +-- ===== t_pickup_order ===== +ALTER TABLE `t_pickup_order` + ADD COLUMN `RequestId` VARCHAR(64) NULL COMMENT '请求幂等ID' AFTER `PickupNo`, + ADD COLUMN `OrganizationId` CHAR(36) NULL COMMENT '数据归属组织' AFTER `CustomerId`, + ADD COLUMN `SecretLevel` TINYINT NOT NULL DEFAULT 2 COMMENT '数据密级 1-4' AFTER `OrganizationId`; + +ALTER TABLE `t_pickup_order` + ADD COLUMN `SenderCountry` INT NULL COMMENT '国家 t_country.Id 快照' AFTER `SenderCompany`, + ADD COLUMN `SenderStreetAddress3` VARCHAR(500) NULL COMMENT '地址3快照' AFTER `SenderStreetAddress2`, + ADD COLUMN `SenderHouseNumber` VARCHAR(50) NULL COMMENT '门牌号快照' AFTER `SenderStreetAddress3`, + ADD COLUMN `SenderTelePhone` VARCHAR(50) NULL COMMENT '电话快照' AFTER `SenderPhone`, + ADD COLUMN `SenderEmail` VARCHAR(60) NULL COMMENT '邮箱快照' AFTER `SenderTelePhone`; + +-- 手机字段长度与现表对齐(若已是 50 可忽略报错) +ALTER TABLE `t_pickup_order` + MODIFY COLUMN `SenderPhone` VARCHAR(50) NULL COMMENT '手机快照'; + +-- ConcurrencyStamp(复制安全写法) +ALTER TABLE `t_pickup_order` + ADD COLUMN `ConcurrencyStamp` CHAR(36) NULL COMMENT '并发戳' AFTER `IsDeleted`; + +UPDATE `t_pickup_order` +SET `ConcurrencyStamp` = UUID() +WHERE `ConcurrencyStamp` IS NULL OR `ConcurrencyStamp` = ''; + +ALTER TABLE `t_pickup_order` + MODIFY COLUMN `ConcurrencyStamp` CHAR(36) NOT NULL COMMENT '并发戳'; + +ALTER TABLE `t_pickup_order` + ADD UNIQUE KEY `uk_pickup_request_id` (`RequestId`), + ADD KEY `idx_pickup_org_secret_ctime` (`OrganizationId`, `SecretLevel`, `CreateTime`); + +-- ===== t_pickup_order_line ===== +ALTER TABLE `t_pickup_order_line` + ADD COLUMN `ServiceNo` VARCHAR(64) NULL COMMENT '运单号快照(便于列表)' AFTER `OrderId`, + ADD COLUMN `ClientNo` VARCHAR(64) NULL COMMENT '客户单号快照' AFTER `ServiceNo`; + +ALTER TABLE `t_pickup_order_line` + ADD COLUMN `ConcurrencyStamp` CHAR(36) NULL COMMENT '并发戳' AFTER `IsDeleted`; + +UPDATE `t_pickup_order_line` +SET `ConcurrencyStamp` = UUID() +WHERE `ConcurrencyStamp` IS NULL OR `ConcurrencyStamp` = ''; + +ALTER TABLE `t_pickup_order_line` + MODIFY COLUMN `ConcurrencyStamp` CHAR(36) NOT NULL COMMENT '并发戳'; diff --git a/sql/20260803_pickup_order_concurrency_stamp.sql b/sql/20260803_pickup_order_concurrency_stamp.sql new file mode 100644 index 0000000..06e2d44 --- /dev/null +++ b/sql/20260803_pickup_order_concurrency_stamp.sql @@ -0,0 +1,53 @@ +-- 幂等补 ConcurrencyStamp:已存在则跳过 ADD,只做回填/NOT NULL。 +-- 执行前请备份。 + +-- 先看现状(可选) +SELECT TABLE_NAME, COLUMN_NAME, IS_NULLABLE, COLUMN_TYPE +FROM information_schema.COLUMNS +WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME IN ('t_pickup_order', 't_pickup_order_line') + AND COLUMN_NAME = 'ConcurrencyStamp'; + +-- ===== t_pickup_order ===== +SET @sql := ( + SELECT IF( + EXISTS( + SELECT 1 FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 't_pickup_order' + AND COLUMN_NAME = 'ConcurrencyStamp' + ), + 'SELECT ''t_pickup_order.ConcurrencyStamp already exists'' AS info', + 'ALTER TABLE `t_pickup_order` ADD COLUMN `ConcurrencyStamp` CHAR(36) NULL COMMENT ''并发戳'' AFTER `IsDeleted`' + ) +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +UPDATE `t_pickup_order` +SET `ConcurrencyStamp` = UUID() +WHERE `ConcurrencyStamp` IS NULL OR `ConcurrencyStamp` = ''; + +ALTER TABLE `t_pickup_order` + MODIFY COLUMN `ConcurrencyStamp` CHAR(36) NOT NULL COMMENT '并发戳'; + +-- ===== t_pickup_order_line ===== +SET @sql := ( + SELECT IF( + EXISTS( + SELECT 1 FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 't_pickup_order_line' + AND COLUMN_NAME = 'ConcurrencyStamp' + ), + 'SELECT ''t_pickup_order_line.ConcurrencyStamp already exists'' AS info', + 'ALTER TABLE `t_pickup_order_line` ADD COLUMN `ConcurrencyStamp` CHAR(36) NULL COMMENT ''并发戳'' AFTER `IsDeleted`' + ) +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +UPDATE `t_pickup_order_line` +SET `ConcurrencyStamp` = UUID() +WHERE `ConcurrencyStamp` IS NULL OR `ConcurrencyStamp` = ''; + +ALTER TABLE `t_pickup_order_line` + MODIFY COLUMN `ConcurrencyStamp` CHAR(36) NOT NULL COMMENT '并发戳'; diff --git a/sql/20260804_issue_template_handler.sql b/sql/20260804_issue_template_handler.sql new file mode 100644 index 0000000..624bf3a --- /dev/null +++ b/sql/20260804_issue_template_handler.sql @@ -0,0 +1,9 @@ +-- 问题模板:处理人类型 + 指定用户 +ALTER TABLE `t_issue_template` + ADD COLUMN `HandlerType` VARCHAR(16) NULL COMMENT '处理人类型:customer=订单所属客户 user=指定用户' AFTER `NotifyMethod`, + ADD COLUMN `HandlerUserId` INT NULL COMMENT '指定处理人用户Id(HandlerType=user 时必填)' AFTER `HandlerType`; + +-- 问题事件:处理人快照(运行时解析) +ALTER TABLE `t_order_exception_event` + ADD COLUMN `HandlerType` VARCHAR(16) NULL COMMENT '处理人类型' AFTER `HandlerNames`, + ADD COLUMN `HandlerUserId` INT NULL COMMENT '处理人用户Id(customer 时为订单客户账号)' AFTER `HandlerType`; diff --git a/sql/20260804_order_exception_event_handler.sql b/sql/20260804_order_exception_event_handler.sql new file mode 100644 index 0000000..48ee74a --- /dev/null +++ b/sql/20260804_order_exception_event_handler.sql @@ -0,0 +1,3 @@ +-- 问题处理:事件级处理人快照(订单可有多条问题记录) +ALTER TABLE `t_order_exception_event` + ADD COLUMN `HandlerNames` VARCHAR(256) NULL COMMENT '处理人快照' AFTER `NotifyMethod`; diff --git a/sql/20260804_order_status_drop_received.sql b/sql/20260804_order_status_drop_received.sql new file mode 100644 index 0000000..4b875b3 --- /dev/null +++ b/sql/20260804_order_status_drop_received.sql @@ -0,0 +1,14 @@ +-- 废弃订单状态 received(已收货):入库后直接进 pendingAllocation / pendingPayment,不再停留 received +-- 历史数据:已入库的归待排货;若已有应付且未支付则归待支付 + +UPDATE `t_order` +SET + `OrderStatus` = CASE + WHEN IFNULL(`IsPaid`, 0) > 0 THEN 'pendingShip' + WHEN IFNULL(`PayableAmount`, 0) > 0 + AND (`AllocationPendingReason` IS NULL OR TRIM(`AllocationPendingReason`) = '') + THEN 'pendingPayment' + ELSE 'pendingAllocation' + END, + `UpdateTime` = NOW() +WHERE `OrderStatus` = 'received'; diff --git a/src/Oms.Application/Dtos/Channels/IssueTemplateOutput.cs b/src/Oms.Application/Dtos/Channels/IssueTemplateOutput.cs index aa30bb5..c9879a4 100644 --- a/src/Oms.Application/Dtos/Channels/IssueTemplateOutput.cs +++ b/src/Oms.Application/Dtos/Channels/IssueTemplateOutput.cs @@ -23,6 +23,14 @@ public class IssueTemplateOutput public List NotifyMethods { get; set; } = []; + public string? HandlerType { get; set; } + + public string? HandlerTypeName { get; set; } + + public int? HandlerUserId { get; set; } + + public string? HandlerUserName { get; set; } + public byte Status { get; set; } public DateTime CreateTime { get; set; } diff --git a/src/Oms.Application/Dtos/Channels/IssueTemplateSaveInput.cs b/src/Oms.Application/Dtos/Channels/IssueTemplateSaveInput.cs index 9ae17c6..49d5600 100644 --- a/src/Oms.Application/Dtos/Channels/IssueTemplateSaveInput.cs +++ b/src/Oms.Application/Dtos/Channels/IssueTemplateSaveInput.cs @@ -19,5 +19,9 @@ public class IssueTemplateSaveInput public string? NotifyMethod { get; set; } + public string? HandlerType { get; set; } + + public int? HandlerUserId { get; set; } + public EnableStatus Status { get; set; } = EnableStatus.Enabled; } diff --git a/src/Oms.Application/Dtos/Orders/OrderAllocationTraceDtos.cs b/src/Oms.Application/Dtos/Orders/OrderAllocationTraceDtos.cs index e1dc427..e431cc8 100644 --- a/src/Oms.Application/Dtos/Orders/OrderAllocationTraceDtos.cs +++ b/src/Oms.Application/Dtos/Orders/OrderAllocationTraceDtos.cs @@ -21,6 +21,9 @@ public class OrderAllocationTraceOutput public decimal? ChargeableWeight { get; set; } + /// 订单解析出的价格分区键(用于对照买价明细分区)。 + public List ZoneKeys { get; set; } = []; + public string? ChargingStage { get; set; } public string? ChargingStageName { get; set; } diff --git a/src/Oms.Application/Dtos/Orders/OrderExceptionDtos.cs b/src/Oms.Application/Dtos/Orders/OrderExceptionDtos.cs index 2f442a5..eddc1be 100644 --- a/src/Oms.Application/Dtos/Orders/OrderExceptionDtos.cs +++ b/src/Oms.Application/Dtos/Orders/OrderExceptionDtos.cs @@ -29,6 +29,67 @@ public class OrderMarkProblematicInput public string? Content { get; set; } public string? Remark { get; set; } + + public string? HandlerNames { get; set; } +} + +public class OrderExceptionEventProcessInput +{ + public int Id { get; set; } + + public string? Remark { get; set; } +} + +public class OrderExceptionEventOutput +{ + public int Id { get; set; } + + public int OrderId { get; set; } + + public string EventType { get; set; } = string.Empty; + + public string? IssueType { get; set; } + + public string? IssueTypeName { get; set; } + + public string? Title { get; set; } + + public string? Content { get; set; } + + public string? NotifyMethod { get; set; } + + public string? NotifyMethodNames { get; set; } + + public string? HandlerNames { get; set; } + + public string? HandlerType { get; set; } + + public string? HandlerTypeName { get; set; } + + public int? HandlerUserId { get; set; } + + public string? HandleStatus { get; set; } + + public string? HandleStatusName { get; set; } + + public string? CreateBy { get; set; } + + public DateTime CreateTime { get; set; } + + public string? ProcessedBy { get; set; } + + public DateTime? ProcessedTime { get; set; } + + public string? Remark { get; set; } +} + +public class OrderExceptionEventListOutput +{ + public List Items { get; set; } = []; + + public int PendingCount { get; set; } + + public int ProcessedCount { get; set; } } public class OrderMarkClaimedInput diff --git a/src/Oms.Application/Dtos/Orders/OrderOutput.cs b/src/Oms.Application/Dtos/Orders/OrderOutput.cs index e681eff..972c8d6 100644 --- a/src/Oms.Application/Dtos/Orders/OrderOutput.cs +++ b/src/Oms.Application/Dtos/Orders/OrderOutput.cs @@ -136,10 +136,15 @@ public class OrderOutput public int? PickupOrderId { get; set; } /// - /// 关联揽收单号。 + /// 关联揽收系统单号(OMS PickupNo)。 /// public string? PickupNo { get; set; } + /// + /// 国内承运商回执单号(揽收承运商运单号)。 + /// + public string? CarrierWaybillNo { get; set; } + /// /// 幂等请求标识。 /// diff --git a/src/Oms.Application/Dtos/Orders/OrderParcelCreateOutput.cs b/src/Oms.Application/Dtos/Orders/OrderParcelCreateOutput.cs index 6e9b014..0f7f1fe 100644 --- a/src/Oms.Application/Dtos/Orders/OrderParcelCreateOutput.cs +++ b/src/Oms.Application/Dtos/Orders/OrderParcelCreateOutput.cs @@ -50,6 +50,11 @@ public class OrderParcelCreateOutput /// public int? PickupOrderId { get; set; } + /// + /// 国内承运运单号(上门揽收时可选)。 + /// + public string? CarrierWaybillNo { get; set; } + /// /// 揽收单创建失败原因(运单已成功时仅作提示)。 /// diff --git a/src/Oms.Application/Dtos/Orders/OrderReallocateDtos.cs b/src/Oms.Application/Dtos/Orders/OrderReallocateDtos.cs index ea58b9c..09c0e56 100644 --- a/src/Oms.Application/Dtos/Orders/OrderReallocateDtos.cs +++ b/src/Oms.Application/Dtos/Orders/OrderReallocateDtos.cs @@ -38,3 +38,22 @@ public class OrderReallocateBatchInput public int? ServiceChannelId { get; set; } } + +/// +/// 查询指定渠道可选项(按订单排货池交集)。 +/// +public class OrderReallocateChannelOptionsInput +{ + [Required] + [MinLength(1)] + public List OrderIds { get; set; } = []; +} + +public class OrderReallocateChannelOptionOutput +{ + public int ServiceChannelId { get; set; } + + public string ChannelCode { get; set; } = string.Empty; + + public string? ChannelName { get; set; } +} diff --git a/src/Oms.Application/Dtos/Orders/PickupOrderDtos.cs b/src/Oms.Application/Dtos/Orders/PickupOrderDtos.cs index 4eadb95..663377c 100644 --- a/src/Oms.Application/Dtos/Orders/PickupOrderDtos.cs +++ b/src/Oms.Application/Dtos/Orders/PickupOrderDtos.cs @@ -6,6 +6,11 @@ public class PickupOrderCreateInput { public int? CustomerId { get; set; } + /// + /// 请求幂等键(可选)。 + /// + public string? RequestId { get; set; } + [Required] public int SenderAddressId { get; set; } @@ -38,6 +43,39 @@ public class PickupOrderIdInput public int Id { get; set; } } +public class PickupOrderIdsInput +{ + [Required] + [MinLength(1)] + public List Ids { get; set; } = []; +} + +public class PickupOrderBatchPayItemOutput +{ + public int Id { get; set; } + + public string? PickupNo { get; set; } + + public bool Success { get; set; } + + public string Message { get; set; } = string.Empty; + + public decimal? Amount { get; set; } +} + +public class PickupOrderBatchPayOutput +{ + public int SuccessCount { get; set; } + + public int FailCount { get; set; } + + public decimal PaidAmount { get; set; } + + public string Message { get; set; } = string.Empty; + + public List Items { get; set; } = []; +} + public class PickupOrderOrdersInput { [Required] @@ -85,10 +123,21 @@ public class PickupOrderScheduleInput [Required] public int Id { get; set; } - [Required] - public string CarrierCode { get; set; } = string.Empty; + /// + /// 兼容旧字段,已不再作为预约必填;承运服务改用销售产品编码。 + /// + public string? CarrierCode { get; set; } - public string? CarrierService { get; set; } + /// + /// 承运服务:国内揽收分组销售产品编码。 + /// + [Required] + public string CarrierService { get; set; } = string.Empty; + + /// + /// 国内承运商回执单号(手工录入或对接返回)。 + /// + public string? CarrierWaybillNo { get; set; } public DateTime? ExpectPickupTime { get; set; } } @@ -132,24 +181,46 @@ public class PickupOrderOutput public string PickupNo { get; set; } = string.Empty; + public string? RequestId { get; set; } + public int CustomerId { get; set; } public string? CustomerName { get; set; } + public string? OrganizationId { get; set; } + + public int SecretLevel { get; set; } + public int SenderAddressId { get; set; } public string SenderName { get; set; } = string.Empty; public string? SenderPhone { get; set; } + public string? SenderTelePhone { get; set; } + + public string? SenderEmail { get; set; } + public string? SenderCompany { get; set; } + public int? SenderCountry { get; set; } + + public string? SenderCountryCode { get; set; } + public string? SenderProvince { get; set; } public string? SenderCity { get; set; } + public string? SenderZipCode { get; set; } + public string? SenderStreetAddress1 { get; set; } + public string? SenderStreetAddress2 { get; set; } + + public string? SenderStreetAddress3 { get; set; } + + public string? SenderHouseNumber { get; set; } + public int WarehouseId { get; set; } public string? WarehouseName { get; set; } diff --git a/src/Oms.Application/Services/Channels/IssueTemplateService.cs b/src/Oms.Application/Services/Channels/IssueTemplateService.cs index 07d4d57..8e4da90 100644 --- a/src/Oms.Application/Services/Channels/IssueTemplateService.cs +++ b/src/Oms.Application/Services/Channels/IssueTemplateService.cs @@ -24,6 +24,9 @@ public class IssueTemplateService : ApplicationService private DataPermRepository DataPermRepository => LazyServiceProvider.GetRequiredService(); + private OmsRbacUserDisplayNameResolver UserDisplayNameResolver => + LazyServiceProvider.GetRequiredService(); + /// /// 分页查询问题模板列表。 /// @@ -40,7 +43,8 @@ public class IssueTemplateService : ApplicationService new Dictionary { [nameof(IssueTemplate.UpdateTime)] = false, [nameof(IssueTemplate.Id)] = false }); var total = await IssueTemplateRepository.CountAsync(predicate); var orgNameMap = await ResolveOrganizationNameMapAsync(entities); - var items = entities.Select(x => MapOutput(x, orgNameMap)).ToList(); + var handlerUserNameMap = await ResolveHandlerUserNameMapAsync(entities); + var items = entities.Select(x => MapOutput(x, orgNameMap, handlerUserNameMap)).ToList(); return new OmsPageResult { Items = items, Total = total }; } @@ -52,7 +56,8 @@ public class IssueTemplateService : ApplicationService { var entity = await GetEntityOrThrowAsync(id); var orgNameMap = await ResolveOrganizationNameMapAsync([entity]); - return MapOutput(entity, orgNameMap); + var handlerUserNameMap = await ResolveHandlerUserNameMapAsync([entity]); + return MapOutput(entity, orgNameMap, handlerUserNameMap); } /// @@ -69,6 +74,8 @@ public class IssueTemplateService : ApplicationService Title = normalized.Title, Content = normalized.Content, NotifyMethod = normalized.NotifyMethod, + HandlerType = normalized.HandlerType, + HandlerUserId = normalized.HandlerUserId, Status = normalized.Status }; await IssueTemplateRepository.InsertAsync(entity); @@ -87,6 +94,8 @@ public class IssueTemplateService : ApplicationService entity.Title = normalized.Title; entity.Content = normalized.Content; entity.NotifyMethod = normalized.NotifyMethod; + entity.HandlerType = normalized.HandlerType; + entity.HandlerUserId = normalized.HandlerUserId; entity.Status = normalized.Status; await IssueTemplateRepository.UpdateAsync(entity); } @@ -212,6 +221,38 @@ public class IssueTemplateService : ApplicationService throw new BusinessException("请至少选择一种通知方式"); } + var handlerType = input.HandlerType?.Trim().ToLowerInvariant(); + if (string.IsNullOrWhiteSpace(handlerType)) + { + handlerType = IssueHandlerTypeCodes.User; + } + + if (!IssueHandlerTypeCodes.All.Contains(handlerType)) + { + throw new BusinessException("处理人类型不合法"); + } + + int? handlerUserId = null; + if (IssueHandlerTypeCodes.IsUser(handlerType)) + { + if (input.HandlerUserId is not int userId || userId <= 0) + { + throw new BusinessException("请选择指定处理人"); + } + + handlerUserId = userId; + } + else if (IssueHandlerTypeCodes.IsCustomer(handlerType)) + { + var methods = IssueNotifyMethodCodes.ParseList(notifyMethod); + if (methods.Any(x => !string.Equals(x, IssueNotifyMethodCodes.InApp, StringComparison.OrdinalIgnoreCase))) + { + throw new BusinessException("处理人为客户时,目前仅支持站内信通知"); + } + + notifyMethod = IssueNotifyMethodCodes.InApp; + } + var organizationId = NormalizeOrganizationId(input.OrganizationId); if (!string.IsNullOrWhiteSpace(organizationId)) { @@ -238,6 +279,8 @@ public class IssueTemplateService : ApplicationService Title = title, Content = content, NotifyMethod = notifyMethod, + HandlerType = handlerType, + HandlerUserId = handlerUserId, Status = input.Status }; } @@ -280,9 +323,20 @@ public class IssueTemplateService : ApplicationService : await DataPermRepository.GetOrganizationNamesAsync(orgIds); } + private async Task> ResolveHandlerUserNameMapAsync( + IReadOnlyCollection entities) + { + var userIds = entities + .Where(x => IssueHandlerTypeCodes.IsUser(x.HandlerType) && x.HandlerUserId is > 0) + .Select(x => x.HandlerUserId!.Value) + .Distinct(); + return await UserDisplayNameResolver.ResolveManyAsync(userIds); + } + private static IssueTemplateOutput MapOutput( IssueTemplate entity, - IReadOnlyDictionary orgNameMap) + IReadOnlyDictionary orgNameMap, + IReadOnlyDictionary handlerUserNameMap) { string? organizationName = null; if (!string.IsNullOrWhiteSpace(entity.OrganizationId) @@ -307,6 +361,12 @@ public class IssueTemplateService : ApplicationService Content = entity.Content, NotifyMethod = entity.NotifyMethod, NotifyMethods = IssueNotifyMethodCodes.ParseList(entity.NotifyMethod), + HandlerType = entity.HandlerType, + HandlerTypeName = IssueHandlerTypeCodes.GetDisplayName(entity.HandlerType), + HandlerUserId = entity.HandlerUserId, + HandlerUserName = entity.HandlerUserId is int handlerUserId && handlerUserId > 0 + ? OmsRbacUserDisplayNameResolver.FormatUserId(handlerUserId, handlerUserNameMap) + : null, Status = (byte)entity.Status, CreateTime = entity.CreateTime, UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime) @@ -325,6 +385,10 @@ public class IssueTemplateService : ApplicationService public string NotifyMethod { get; set; } = string.Empty; + public string HandlerType { get; set; } = IssueHandlerTypeCodes.User; + + public int? HandlerUserId { get; set; } + public EnableStatus Status { get; set; } } } diff --git a/src/Oms.Application/Services/Orders/Issues/AutoTriggerRuntimeService.cs b/src/Oms.Application/Services/Orders/Issues/AutoTriggerRuntimeService.cs index 7af917c..e8276d1 100644 --- a/src/Oms.Application/Services/Orders/Issues/AutoTriggerRuntimeService.cs +++ b/src/Oms.Application/Services/Orders/Issues/AutoTriggerRuntimeService.cs @@ -134,6 +134,7 @@ public class AutoTriggerRuntimeService( IssuePlaceholderRenderer.Render(content, context), IssueNotifyMethodCodes.InApp, operatorUserId, + null, AutoTriggerConditionMatcher.ParseNotifierUserIds(trigger.Notifier)); return; } diff --git a/src/Oms.Application/Services/Orders/Issues/IssueHandlerResolver.cs b/src/Oms.Application/Services/Orders/Issues/IssueHandlerResolver.cs new file mode 100644 index 0000000..32575a5 --- /dev/null +++ b/src/Oms.Application/Services/Orders/Issues/IssueHandlerResolver.cs @@ -0,0 +1,101 @@ +using Fake.Application; +using Fake.DependencyInjection; +using Oms.Application.Shared; +using Oms.Domain.ChannelsAggregate.Consts; +using Oms.Domain.ChannelsAggregate.Entities; +using Oms.Domain.CustomersAggregate.Repositories; +using Oms.Domain.OrdersAggregate.Entities; + +namespace Oms.Application.Services.Orders.Issues; + +public sealed class IssueHandlerResolution +{ + public string? HandlerType { get; set; } + + public int? HandlerUserId { get; set; } + + public string? HandlerNames { get; set; } + + public IReadOnlyList NotifyUserIds { get; set; } = []; +} + +public class IssueHandlerResolver( + ICustomerRepository customerRepository, + OmsRbacUserDisplayNameResolver userDisplayNameResolver) : IScopedDependency +{ + public async Task ResolveAsync( + Order order, + IssueTemplate? template, + string? manualHandlerNames = null) + { + if (template != null && !string.IsNullOrWhiteSpace(template.HandlerType)) + { + if (IssueHandlerTypeCodes.IsCustomer(template.HandlerType)) + { + return await ResolveCustomerHandlerAsync(order); + } + + if (IssueHandlerTypeCodes.IsUser(template.HandlerType)) + { + return await ResolveUserHandlerAsync(template.HandlerUserId); + } + } + + var names = manualHandlerNames?.Trim(); + return new IssueHandlerResolution + { + HandlerNames = string.IsNullOrWhiteSpace(names) ? null : names, + NotifyUserIds = [] + }; + } + + private async Task ResolveCustomerHandlerAsync(Order order) + { + if (order.CustomerId is not int customerId || customerId <= 0) + { + throw new BusinessException("订单未关联客户,无法指定客户为处理人"); + } + + var customer = await customerRepository.FirstOrDefaultAsync(x => x.Id == customerId); + if (customer == null) + { + throw new BusinessException("订单客户不存在,无法指定客户为处理人"); + } + + if (customer.UserId <= 0) + { + throw new BusinessException("客户未绑定登录账号,无法通知客户处理"); + } + + var displayName = string.IsNullOrWhiteSpace(customer.CustomerName) + ? $"客户#{customer.Id}" + : customer.CustomerName.Trim(); + + return new IssueHandlerResolution + { + HandlerType = IssueHandlerTypeCodes.Customer, + HandlerUserId = customer.UserId, + HandlerNames = displayName, + NotifyUserIds = [customer.UserId] + }; + } + + private async Task ResolveUserHandlerAsync(int? handlerUserId) + { + if (handlerUserId is not int userId || userId <= 0) + { + throw new BusinessException("问题模板未配置指定处理人"); + } + + var nameMap = await userDisplayNameResolver.ResolveManyAsync([userId]); + var displayName = OmsRbacUserDisplayNameResolver.FormatUserId(userId, nameMap); + + return new IssueHandlerResolution + { + HandlerType = IssueHandlerTypeCodes.User, + HandlerUserId = userId, + HandlerNames = displayName, + NotifyUserIds = [userId] + }; + } +} diff --git a/src/Oms.Application/Services/Orders/Issues/OrderExceptionWorkflowService.cs b/src/Oms.Application/Services/Orders/Issues/OrderExceptionWorkflowService.cs index d8b41ac..374b18a 100644 --- a/src/Oms.Application/Services/Orders/Issues/OrderExceptionWorkflowService.cs +++ b/src/Oms.Application/Services/Orders/Issues/OrderExceptionWorkflowService.cs @@ -1,3 +1,4 @@ +using Fake.Application; using Fake.DependencyInjection; using Oms.Domain.ChannelsAggregate.Consts; using Oms.Domain.ChannelsAggregate.Entities; @@ -21,6 +22,8 @@ public sealed class ProblematicMarkRequest public string? NotifyMethod { get; set; } + public string? HandlerNames { get; set; } + public string? Remark { get; set; } public IEnumerable? ExtraReceiverUserIds { get; set; } @@ -30,6 +33,7 @@ public class OrderExceptionWorkflowService( IOrderRepository orderRepository, IOrderExceptionEventRepository orderExceptionEventRepository, IIssueTemplateRepository issueTemplateRepository, + IssueHandlerResolver issueHandlerResolver, OrderIssueNotificationService issueNotificationService) : IScopedDependency { public async Task TryMarkProblematicAsync( @@ -43,13 +47,40 @@ public class OrderExceptionWorkflowService( return OrderExceptionStatusRules.GetForbiddenReason(order.OrderStatus); } + IssueTemplate? template = null; + if (request.IssueTemplateId is int templateId && templateId > 0) + { + template = await issueTemplateRepository.FirstOrDefaultAsync(x => x.Id == templateId); + } + + IssueHandlerResolution handlerResolution; + try + { + handlerResolution = await issueHandlerResolver.ResolveAsync(order, template, request.HandlerNames); + } + catch (BusinessException ex) + { + return ex.Message; + } + var context = IssuePlaceholderContext.FromOrder(order, request.IssueType, request.Title); var renderedTitle = IssuePlaceholderRenderer.Render(request.Title, context); var renderedContent = IssuePlaceholderRenderer.Render(request.Content, context); + var notifyMethod = request.NotifyMethod; + if (IssueHandlerTypeCodes.IsCustomer(handlerResolution.HandlerType)) + { + notifyMethod = IssueNotifyMethodCodes.InApp; + } + var fromStatus = order.OrderStatus; - order.PreviousOrderStatus = fromStatus; - order.OrderStatus = OrderStatusCodes.Problematic; + var isAppend = string.Equals(fromStatus, OrderStatusCodes.Problematic, StringComparison.OrdinalIgnoreCase); + if (!isAppend) + { + order.PreviousOrderStatus = fromStatus; + order.OrderStatus = OrderStatusCodes.Problematic; + } + order.HandleStatus = OrderHandleStatusCodes.Pending; order.IssueType = request.IssueType; order.IssueTitle = renderedTitle; @@ -76,18 +107,25 @@ public class OrderExceptionWorkflowService( Title = renderedTitle, Content = renderedContent, IssueTemplateId = request.IssueTemplateId, - NotifyMethod = request.NotifyMethod, + NotifyMethod = notifyMethod, + HandlerNames = handlerResolution.HandlerNames, + HandlerType = handlerResolution.HandlerType, + HandlerUserId = handlerResolution.HandlerUserId, Remark = request.Remark }); - if (sendNotification) + if (sendNotification && !string.IsNullOrWhiteSpace(notifyMethod)) { + var notifyUserIds = handlerResolution.NotifyUserIds.Count > 0 + ? handlerResolution.NotifyUserIds + : null; await issueNotificationService.DispatchAsync( order, renderedTitle, renderedContent, - request.NotifyMethod, + notifyMethod, operatorUserId, + notifyUserIds, request.ExtraReceiverUserIds); } diff --git a/src/Oms.Application/Services/Orders/Issues/OrderIssueNotificationService.cs b/src/Oms.Application/Services/Orders/Issues/OrderIssueNotificationService.cs index e7730d0..7732dcd 100644 --- a/src/Oms.Application/Services/Orders/Issues/OrderIssueNotificationService.cs +++ b/src/Oms.Application/Services/Orders/Issues/OrderIssueNotificationService.cs @@ -29,6 +29,7 @@ public class OrderIssueNotificationService( string content, string? notifyMethodCsv, int operatorUserId, + IEnumerable? primaryReceiverUserIds = null, IEnumerable? extraReceiverUserIds = null) { var methods = IssueNotifyMethodCodes.ParseList(notifyMethodCsv); @@ -50,7 +51,7 @@ public class OrderIssueNotificationService( switch (method) { case IssueNotifyMethodCodes.InApp: - await SendInAppAsync(order, customer, title, content, extraReceiverUserIds); + await SendInAppAsync(order, customer, title, content, primaryReceiverUserIds, extraReceiverUserIds); break; case IssueNotifyMethodCodes.Email: await SendEmailAsync(order, customer, title, content, operatorUserId); @@ -81,19 +82,44 @@ public class OrderIssueNotificationService( Customer? customer, string title, string content, + IEnumerable? primaryReceiverUserIds, IEnumerable? extraReceiverUserIds) { var receiverIds = new HashSet(); - if (customer is { UserId: > 0 }) + if (primaryReceiverUserIds != null) { - receiverIds.Add(customer.UserId); + foreach (var userId in primaryReceiverUserIds) + { + if (userId > 0) + { + receiverIds.Add(userId); + } + } } - foreach (var userId in extraReceiverUserIds ?? []) + if (receiverIds.Count == 0) { - if (userId > 0) + if (customer is { UserId: > 0 }) { - receiverIds.Add(userId); + receiverIds.Add(customer.UserId); + } + + foreach (var userId in extraReceiverUserIds ?? []) + { + if (userId > 0) + { + receiverIds.Add(userId); + } + } + } + else + { + foreach (var userId in extraReceiverUserIds ?? []) + { + if (userId > 0) + { + receiverIds.Add(userId); + } } } diff --git a/src/Oms.Application/Services/Orders/OrderService.Exception.cs b/src/Oms.Application/Services/Orders/OrderService.Exception.cs index a0cf383..bb48203 100644 --- a/src/Oms.Application/Services/Orders/OrderService.Exception.cs +++ b/src/Oms.Application/Services/Orders/OrderService.Exception.cs @@ -3,6 +3,7 @@ using Fake.Domain.Exceptions; using Microsoft.AspNetCore.Mvc; using Oms.Application.Dtos.Orders; using Oms.Application.Services.Orders.Issues; +using Oms.Application.Shared; using Oms.Domain.ChannelsAggregate.Consts; using Oms.Domain.ChannelsAggregate.Entities; using Oms.Domain.ChannelsAggregate.Repositories; @@ -40,6 +41,7 @@ public partial class OrderService Content = payload.Content, IssueTemplateId = payload.IssueTemplateId, NotifyMethod = payload.NotifyMethod, + HandlerNames = payload.HandlerNames, Remark = payload.Remark }; @@ -115,6 +117,154 @@ public partial class OrderService input.Remark?.Trim()); } + /// + /// 订单问题处理记录列表(mark_problematic 事件,支持一单多条)。 + /// + [HttpGet("/order/exception-events/list")] + public async Task GetExceptionEventListAsync([FromQuery] int orderId) + { + if (orderId <= 0) + { + throw new BusinessException("订单Id无效"); + } + + await GetOrderOrThrowAsync(orderId); + var events = await OrderExceptionEventRepository.GetListAsync(x => + x.OrderId == orderId && x.EventType == OrderExceptionEventTypeCodes.MarkProblematic); + events = events.OrderByDescending(x => x.CreateTime).ThenByDescending(x => x.Id).ToList(); + + var userIds = events + .SelectMany(x => new int?[] { x.CreateUserId, x.UpdateUserId, x.HandlerUserId }) + .Where(x => x is > 0) + .Select(x => x!.Value) + .Distinct(); + var auditUserNameMap = await UserDisplayNameResolver.ResolveManyAsync(userIds); + + var items = events.Select(evt => MapExceptionEventOutput(evt, auditUserNameMap)).ToList(); + return new OrderExceptionEventListOutput + { + Items = items, + PendingCount = items.Count(x => OrderHandleStatusCodes.IsPending(x.HandleStatus)), + ProcessedCount = items.Count(x => OrderHandleStatusCodes.IsProcessed(x.HandleStatus)) + }; + } + + /// + /// 单条问题标记已处理。 + /// + [HttpPost("/order/exception-event/process")] + public async Task ProcessExceptionEventAsync([FromBody] OrderExceptionEventProcessInput input) + { + if (input.Id <= 0) + { + throw new BusinessException("问题记录Id无效"); + } + + var evt = await OrderExceptionEventRepository.FirstOrDefaultAsync(x => x.Id == input.Id); + if (evt == null || !string.Equals(evt.EventType, OrderExceptionEventTypeCodes.MarkProblematic, StringComparison.OrdinalIgnoreCase)) + { + throw new BusinessException("问题记录不存在"); + } + + await GetOrderOrThrowAsync(evt.OrderId); + if (OrderHandleStatusCodes.IsProcessed(evt.ToHandleStatus)) + { + throw new BusinessException("该问题已处理"); + } + + var remark = input.Remark?.Trim(); + if (!string.IsNullOrWhiteSpace(remark) && remark.Length > 512) + { + throw new BusinessException("备注长度不能超过512"); + } + + var operatorUserId = CurrentUser.GetId(); + evt.ToHandleStatus = OrderHandleStatusCodes.Processed; + evt.UpdateUserId = operatorUserId; + evt.UpdateTime = DateTime.Now; + if (!string.IsNullOrWhiteSpace(remark)) + { + evt.Remark = remark; + } + + await OrderExceptionEventRepository.UpdateAsync(evt); + await OrderExceptionEventRepository.InsertAsync(new OrderExceptionEvent + { + OrderId = evt.OrderId, + EventType = OrderExceptionEventTypeCodes.HandleProcessed, + FromStatus = OrderStatusCodes.Problematic, + ToStatus = OrderStatusCodes.Problematic, + FromHandleStatus = OrderHandleStatusCodes.Pending, + ToHandleStatus = OrderHandleStatusCodes.Processed, + IssueType = evt.IssueType, + Title = evt.Title, + Content = evt.Content, + Remark = remark + }); + + await SyncOrderHandleStatusFromEventsAsync(evt.OrderId); + } + + private async Task SyncOrderHandleStatusFromEventsAsync(int orderId) + { + var order = await GetOrderOrThrowAsync(orderId); + if (order.OrderStatus is not (OrderStatusCodes.Problematic or OrderStatusCodes.Claimed)) + { + return; + } + + var issues = await OrderExceptionEventRepository.GetListAsync(x => + x.OrderId == orderId && x.EventType == OrderExceptionEventTypeCodes.MarkProblematic); + if (issues.Count == 0) + { + return; + } + + var anyPending = issues.Any(x => OrderHandleStatusCodes.IsPending(x.ToHandleStatus)); + order.HandleStatus = anyPending ? OrderHandleStatusCodes.Pending : OrderHandleStatusCodes.Processed; + order.UpdateTime = DateTime.Now; + await OrderRepository.UpdateAsync(order); + } + + private static OrderExceptionEventOutput MapExceptionEventOutput( + OrderExceptionEvent evt, + IReadOnlyDictionary auditUserNameMap) + { + var notifyCodes = IssueNotifyMethodCodes.ParseList(evt.NotifyMethod); + var notifyNames = notifyCodes.Count == 0 + ? string.Empty + : string.Join('、', notifyCodes.Select(IssueNotifyMethodCodes.GetDisplayName)); + + return new OrderExceptionEventOutput + { + Id = evt.Id, + OrderId = evt.OrderId, + EventType = evt.EventType, + IssueType = evt.IssueType, + IssueTypeName = IssueTypeCodes.GetDisplayName(evt.IssueType), + Title = evt.Title, + Content = evt.Content, + NotifyMethod = evt.NotifyMethod, + NotifyMethodNames = notifyNames, + HandlerNames = evt.HandlerNames + ?? (evt.HandlerUserId is int handlerUserId && handlerUserId > 0 + ? OmsRbacUserDisplayNameResolver.FormatUserId(handlerUserId, auditUserNameMap) + : null), + HandlerType = evt.HandlerType, + HandlerTypeName = IssueHandlerTypeCodes.GetDisplayName(evt.HandlerType), + HandlerUserId = evt.HandlerUserId, + HandleStatus = evt.ToHandleStatus, + HandleStatusName = OrderHandleStatusCodes.GetDisplayName(evt.ToHandleStatus), + CreateBy = OmsRbacUserDisplayNameResolver.FormatUserId(evt.CreateUserId, auditUserNameMap), + CreateTime = evt.CreateTime, + ProcessedBy = OrderHandleStatusCodes.IsProcessed(evt.ToHandleStatus) + ? OmsRbacUserDisplayNameResolver.FormatUserId(evt.UpdateUserId, auditUserNameMap) + : null, + ProcessedTime = OrderHandleStatusCodes.IsProcessed(evt.ToHandleStatus) ? evt.UpdateTime : null, + Remark = evt.Remark + }; + } + private async Task ExecuteProblematicBatchAsync( IReadOnlyCollection orderIds, ProblematicMarkRequest request) @@ -271,6 +421,12 @@ public partial class OrderService throw new BusinessException("备注长度不能超过512"); } + var handlerNames = input.HandlerNames?.Trim(); + if (!string.IsNullOrWhiteSpace(handlerNames) && handlerNames.Length > 256) + { + throw new BusinessException("处理人长度不能超过256"); + } + return new ResolvedProblematicPayload { IssueType = issueType, @@ -278,6 +434,7 @@ public partial class OrderService Content = content, IssueTemplateId = template?.Id, NotifyMethod = template == null ? null : IssueNotifyMethodCodes.Normalize(template.NotifyMethod), + HandlerNames = string.IsNullOrWhiteSpace(handlerNames) ? null : handlerNames, Remark = remark }; } @@ -399,6 +556,8 @@ public partial class OrderService public string? NotifyMethod { get; set; } + public string? HandlerNames { get; set; } + public string? Remark { get; set; } } diff --git a/src/Oms.Application/Services/Orders/OrderService.Helpers.cs b/src/Oms.Application/Services/Orders/OrderService.Helpers.cs index e2bfe80..4831e33 100644 --- a/src/Oms.Application/Services/Orders/OrderService.Helpers.cs +++ b/src/Oms.Application/Services/Orders/OrderService.Helpers.cs @@ -98,6 +98,9 @@ public partial class OrderService ? [] : await PickupOrderRepository.GetListAsync(x => pickupOrderIds.Contains(x.Id)); var pickupNoMap = pickupOrders.ToDictionary(x => x.Id, x => x.PickupNo); + var carrierWaybillNoMap = pickupOrders + .Where(x => !string.IsNullOrWhiteSpace(x.CarrierWaybillNo)) + .ToDictionary(x => x.Id, x => x.CarrierWaybillNo!); var outputs = orders.Select(order => { @@ -165,6 +168,10 @@ public partial class OrderService && pickupNoMap.TryGetValue(order.PickupOrderId.Value, out var pickupNo) ? pickupNo : null, + CarrierWaybillNo = order.PickupOrderId is > 0 + && carrierWaybillNoMap.TryGetValue(order.PickupOrderId.Value, out var carrierWaybillNo) + ? carrierWaybillNo + : null, RequestId = order.RequestId, ExportCustomsMode = order.ExportCustomsMode, PredeclareFailReason = string.IsNullOrWhiteSpace(order.PredeclareFailReason) diff --git a/src/Oms.Application/Services/Orders/OrderService.OrderEdit.cs b/src/Oms.Application/Services/Orders/OrderService.OrderEdit.cs index 3350981..f7ebcab 100644 --- a/src/Oms.Application/Services/Orders/OrderService.OrderEdit.cs +++ b/src/Oms.Application/Services/Orders/OrderService.OrderEdit.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using Oms.Application.Dtos.Channels; using Oms.Application.Dtos.Orders; +using Oms.Application.Dtos.Shared; using Oms.Application.Shared; using Oms.Domain.ChannelsAggregate.Consts; using Oms.Domain.ChannelsAggregate.Entities; @@ -328,7 +329,8 @@ public partial class OrderService var weights = await OrderRepository.GetWeightsByOrderIdsAsync([order.Id]); var weightRow = ResolvePayableWeightRow(order, weights); var chargeable = weightRow.ChargeableWeight ?? 0; - var snapshotBinding = await ResolveSnapshotBindingFromOrderAsync(product, order); + // 现算与重新排货一致:规则分区 + 渠道买价均取当前有效发布。 + var snapshotBinding = await ResolveSnapshotBindingAsync(product, DateTime.Now); var billing = await ResolveOrderBillingContextAsync( order, product, @@ -393,6 +395,91 @@ public partial class OrderService } } + /// + /// 指定渠道可选项:取选中订单销售产品排货池(含客户过滤)的交集。 + /// + [HttpPost("/order/fee/reallocate-channel-options")] + public async Task> GetReallocateChannelOptionsAsync( + [FromBody] OrderReallocateChannelOptionsInput input) + { + var orderIds = (input.OrderIds ?? []).Where(x => x > 0).Distinct().ToList(); + if (orderIds.Count == 0) + { + return new OmsItemsResult { Items = [] }; + } + + var orders = await OrderRepository.GetListAsync(x => orderIds.Contains(x.Id)); + if (orders.Count == 0) + { + return new OmsItemsResult { Items = [] }; + } + + HashSet? intersection = null; + var channelMeta = new Dictionary(); + + foreach (var order in orders) + { + if (string.IsNullOrWhiteSpace(order.ProductCode)) + { + return new OmsItemsResult { Items = [] }; + } + + var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim()); + if (product == null) + { + return new OmsItemsResult { Items = [] }; + } + + var binding = await ResolveSnapshotBindingAsync(product, DateTime.Now); + var providers = await ResolveAllocationProvidersAsync(product, binding.Rules); + var filtered = AllocationResolver.FilterProviders(providers, order.CustomerId); + var channelIds = new HashSet(); + foreach (var provider in filtered) + { + if (provider.ServiceChannelId is not > 0) + { + continue; + } + + var channelId = provider.ServiceChannelId.Value; + if (!channelMeta.TryGetValue(channelId, out _)) + { + var channel = await ServiceChannelRepository.FirstOrDefaultAsync(x => x.Id == channelId); + if (channel == null || channel.IsActive != EnableStatus.Enabled) + { + continue; + } + + channelMeta[channelId] = (channel.ChannelCode, channel.ChannelName); + } + + channelIds.Add(channelId); + } + + intersection = intersection == null + ? channelIds + : intersection.Intersect(channelIds).ToHashSet(); + + if (intersection.Count == 0) + { + break; + } + } + + var items = (intersection ?? []) + .Where(id => channelMeta.ContainsKey(id)) + .Select(id => new OrderReallocateChannelOptionOutput + { + ServiceChannelId = id, + ChannelCode = channelMeta[id].Code, + ChannelName = channelMeta[id].Name + }) + .OrderBy(x => x.ChannelCode, StringComparer.OrdinalIgnoreCase) + .ToList(); + + return new OmsItemsResult { Items = items }; + } + /// /// 单票重新排货:仅重写应付与服务渠道,不动应收。 /// @@ -646,7 +733,10 @@ public partial class OrderService throw new BusinessException("计费重无效,请先完善件重尺"); } - var snapshotBinding = await ResolveSnapshotBindingFromOrderAsync(product, order); + // 重新排货:产品规则/分区与渠道买价均取当前有效发布,避免沿用下单时旧快照。 + var snapshotBinding = enforceReallocateGate + ? await ResolveSnapshotBindingAsync(product, DateTime.Now) + : await ResolveSnapshotBindingFromOrderAsync(product, order); var billing = await ResolveOrderBillingContextAsync( order, product, @@ -787,7 +877,7 @@ public partial class OrderService private static (string WeightType, string ChargingStage) ResolveReceivableBillingStage(Order order) => order.OrderStatus switch { - OrderStatusCodes.Received or OrderStatusCodes.PendingAllocation or OrderStatusCodes.PendingPayment or OrderStatusCodes.PendingShip + OrderStatusCodes.PendingAllocation or OrderStatusCodes.PendingPayment or OrderStatusCodes.PendingShip => (OrderWeightTypeCodes.Receive, OrderFeeChargingStageCodes.Inbound), OrderStatusCodes.PendingOnline or OrderStatusCodes.Shipping or OrderStatusCodes.DeliverySuccess or OrderStatusCodes.DeliveryFailed diff --git a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs index 798d9ff..3ffd512 100644 --- a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs +++ b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs @@ -793,11 +793,28 @@ public partial class OrderService throw new BusinessException(evaluation.FailReason); } + var previousChannelCode = order.ChannelCode?.Trim(); + var newChannelCode = selectedCandidate.Channel.ChannelCode?.Trim() ?? string.Empty; + var channelChanged = !string.Equals( + previousChannelCode, + newChannelCode, + StringComparison.OrdinalIgnoreCase); + order.ChannelCode = selectedCandidate.Channel.ChannelCode; AllocationProviderSnapshotEnricher.ApplyDownstreamFieldsToOrder( order, selectedCandidate.Provider, selectedCandidate.Channel); + + // 换渠道后旧下游单号/跟踪号失效,清空以免「新渠道+旧服务商单号」。 + if (channelChanged + && (!string.IsNullOrWhiteSpace(order.DownstreamServiceNo) + || !string.IsNullOrWhiteSpace(order.TrackNo))) + { + order.DownstreamServiceNo = null; + order.TrackNo = null; + } + // 有效应付只保留本阶段一套,避免 forecast+inbound 叠行虚增成本。 await OrderRepository.DeletePayableFeesAsync(order.Id); await OrderRepository.UpsertFeesAsync(order.Id, payableFees); @@ -811,6 +828,7 @@ public partial class OrderService public string SchemeSource { get; init; } = "productDefault"; public decimal ChargeableWeight { get; init; } public string ChargingStage { get; init; } = string.Empty; + public IReadOnlyList ZoneKeys { get; init; } = []; public List Candidates { get; init; } = []; public List Skipped { get; init; } = []; public AllocationResolver.ChannelCandidate? Selected { get; set; } @@ -872,6 +890,7 @@ public partial class OrderService SchemeSource = schemeSource, ChargeableWeight = chargeableWeight, ChargingStage = chargingStage, + ZoneKeys = zoneKeys, Skipped = skipped, FailReason = "指定渠道不在该销售产品排货池或当前客户不可用" }; @@ -887,14 +906,17 @@ public partial class OrderService SchemeSource = schemeSource, ChargeableWeight = chargeableWeight, ChargingStage = chargingStage, + ZoneKeys = zoneKeys, Skipped = skipped, FailReason = "排货规则未匹配到可用服务渠道" }; } - var bindTime = order.CreateTime == default ? DateTime.Now : order.CreateTime; + // 应付买价取「当前」有效发布(PublishTime<=Now),不用下单时间,避免重新排货/入库漏掉后续发布的买价。 + var bindTime = DateTime.Now; var candidates = new List(); var recipient = (await OrderRepository.GetRecipientsByOrderIdsAsync([order.Id])).FirstOrDefault(); + var cargoAttribute = order.CargoAttribute ?? CargoAttributeCodes.General; foreach (var provider in filtered) { @@ -942,7 +964,7 @@ public partial class OrderService channelPriceBinding.PriceEffectiveTime, chargeableWeight, zoneKeys, - order.CargoAttribute ?? CargoAttributeCodes.General, + cargoAttribute, chargingStage, order.WarehouseId, warehouseName, @@ -952,12 +974,22 @@ public partial class OrderService channelRemoteZoneKeys); if (previewFees.Count == 0) { + var miss = ParcelBillingCalculator.ExplainPayablePriceMiss( + channelPriceBinding.Prices, + chargeableWeight, + zoneKeys, + cargoAttribute, + order.WarehouseId, + warehouseName, + channelRemoteZoneKeys); skipped.Add(new OrderAllocationSkippedOutput { ServiceChannelId = channel.Id, ChannelCode = channel.ChannelCode, ChannelName = channel.ChannelName, - Reason = "未匹配到有效渠道买价" + Reason = string.IsNullOrWhiteSpace(miss) + ? $"未匹配到有效渠道买价(版本 {channelPriceBinding.PriceVersion})" + : $"未匹配到有效渠道买价(版本 {channelPriceBinding.PriceVersion}):{miss}" }); continue; } @@ -973,6 +1005,7 @@ public partial class OrderService if (candidates.Count == 0) { + var detail = skipped.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.Reason))?.Reason; return new AllocationEvaluation { StoredScheme = storedScheme, @@ -980,11 +1013,14 @@ public partial class OrderService SchemeSource = schemeSource, ChargeableWeight = chargeableWeight, ChargingStage = chargingStage, + ZoneKeys = zoneKeys, Candidates = candidates, Skipped = skipped, FailReason = chargeableWeight <= 0 ? "计费重无效,无法计算应付费用" - : "排货规则未匹配到有效渠道买价" + : string.IsNullOrWhiteSpace(detail) + ? "排货规则未匹配到有效渠道买价" + : $"排货规则未匹配到有效渠道买价;{detail}" }; } @@ -996,6 +1032,7 @@ public partial class OrderService SchemeSource = schemeSource, ChargeableWeight = chargeableWeight, ChargingStage = chargingStage, + ZoneKeys = zoneKeys, Candidates = candidates, Skipped = skipped, Selected = selected @@ -1053,6 +1090,7 @@ public partial class OrderService SchemeSource = evaluation.SchemeSource, SchemeSourceName = ResolveAllocationSchemeSourceName(evaluation.SchemeSource), ChargeableWeight = evaluation.ChargeableWeight, + ZoneKeys = evaluation.ZoneKeys?.ToList() ?? [], ChargingStage = evaluation.ChargingStage, ChargingStageName = ResolveChargingStageName(evaluation.ChargingStage), SelectedChannelCode = selectedChannelCode ?? evaluation.Selected?.Channel.ChannelCode, diff --git a/src/Oms.Application/Services/Orders/OrderService.cs b/src/Oms.Application/Services/Orders/OrderService.cs index cecb10b..c9a3e28 100644 --- a/src/Oms.Application/Services/Orders/OrderService.cs +++ b/src/Oms.Application/Services/Orders/OrderService.cs @@ -125,7 +125,7 @@ public partial class OrderService : ApplicationService throw new BusinessException("订单已支付"); } - if (order.OrderStatus is not (OrderStatusCodes.PendingPayment or OrderStatusCodes.Received)) + if (order.OrderStatus is not OrderStatusCodes.PendingPayment) { throw new BusinessException("当前订单状态不可支付"); } @@ -475,6 +475,7 @@ public partial class OrderService : ApplicationService }); output.PickupNo = pickup.PickupNo; output.PickupOrderId = pickup.Id; + output.CarrierWaybillNo = pickup.CarrierWaybillNo; } catch (Exception ex) { diff --git a/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs b/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs index d833543..186d60b 100644 --- a/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs +++ b/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs @@ -628,6 +628,128 @@ internal static class ParcelBillingCalculator return string.Join(";", reasons); } + /// + /// 买价未命中时,按价格项输出具体原因(分区/货物类型/重量区间等)。 + /// + public static string ExplainPayablePriceMiss( + IReadOnlyList prices, + decimal chargeableWeight, + IReadOnlyList zoneKeys, + string cargoAttribute, + int? warehouseId = null, + string? warehouseName = null, + IReadOnlyList? remoteZoneKeys = null) + { + var cargoLabel = GetCargoAttributeLabel(cargoAttribute); + var warehouseLabel = FormatWarehouseLabel(warehouseId, warehouseName); + var enabledPrices = prices.Where(x => EnableStatusRules.IsEnabled(x.Status)).ToList(); + if (enabledPrices.Count == 0) + { + return "买价中无启用的价格项"; + } + + var reasons = new List(); + foreach (var price in enabledPrices) + { + var label = !string.IsNullOrWhiteSpace(price.PriceName) ? price.PriceName! : price.CostItem; + var details = price.Details ?? []; + if (details.Count == 0) + { + reasons.Add($"【{label}】价格项Id={price.Id}:无明细行"); + continue; + } + + var zoneMatchedDetails = details.Where(detail => + { + var factors = ParseFactorDictionary(detail.FactorValuesJson); + return ZoneMatches(factors, zoneKeys); + }).ToList(); + + if (zoneMatchedDetails.Count == 0) + { + var detailZones = details + .Select(detail => + { + var factors = ParseFactorDictionary(detail.FactorValuesJson); + return TryGetZoneFactorText(factors, out var zone) ? zone.Trim() : null; + }) + .Where(zone => !string.IsNullOrWhiteSpace(zone)) + .Distinct(StringComparer.OrdinalIgnoreCase); + reasons.Add( + $"【{label}】分区不匹配,明细分区=[{string.Join(", ", detailZones)}]," + + $"订单分区键=[{string.Join(", ", zoneKeys)}]"); + continue; + } + + var cargoMatchedDetails = zoneMatchedDetails.Where(detail => + { + var factors = ParseFactorDictionary(detail.FactorValuesJson); + return MatchCargoTypeFactor(factors, cargoAttribute); + }).ToList(); + + if (cargoMatchedDetails.Count == 0) + { + reasons.Add($"【{label}】货物类型不匹配,当前={cargoLabel}"); + continue; + } + + var warehouseMatchedDetails = cargoMatchedDetails.Where(detail => + { + var factors = ParseFactorDictionary(detail.FactorValuesJson); + return MatchWarehouseFactor(factors, warehouseId, warehouseName); + }).ToList(); + + if (warehouseMatchedDetails.Count == 0) + { + reasons.Add($"【{label}】仓库不匹配,当前={warehouseLabel}"); + continue; + } + + var remoteMatchedDetails = warehouseMatchedDetails.Where(detail => + { + var factors = ParseFactorDictionary(detail.FactorValuesJson); + return RemoteZoneMatches(factors, remoteZoneKeys); + }).ToList(); + + if (remoteMatchedDetails.Count == 0) + { + reasons.Add($"【{label}】偏远分区不匹配"); + continue; + } + + var weightMatched = false; + var intervals = new List(); + foreach (var detail in remoteMatchedDetails) + { + var factors = ParseFactorDictionary(detail.FactorValuesJson); + if (!TryGetWeightFactorText(factors, out var weightText)) + { + weightMatched = true; + break; + } + + intervals.Add(weightText); + if (TryParseWeightInterval(weightText, out var rangeStart, out var rangeEnd) + && WeightInLeftOpenRightClosedInterval(chargeableWeight, rangeStart, rangeEnd)) + { + weightMatched = true; + break; + } + } + + if (!weightMatched) + { + var intervalText = intervals.Count > 0 + ? string.Join(", ", intervals.Distinct(StringComparer.OrdinalIgnoreCase)) + : "无有效重量区间"; + reasons.Add( + $"【{label}】计费重 {chargeableWeight}kg 未落入重量区间 [{intervalText}]"); + } + } + + return string.Join(";", reasons); + } + public static List CalculateForecastReceivableFees( SalesProductPricePublishSnapshotDto priceSnapshot, string priceVersion, diff --git a/src/Oms.Application/Services/Orders/PickupOrderService.cs b/src/Oms.Application/Services/Orders/PickupOrderService.cs index 1018de7..6e6bba1 100644 --- a/src/Oms.Application/Services/Orders/PickupOrderService.cs +++ b/src/Oms.Application/Services/Orders/PickupOrderService.cs @@ -156,6 +156,20 @@ public class PickupOrderService : ApplicationService senderCountryCode = country?.CountryCode; } + if (!string.IsNullOrWhiteSpace(input.RequestId)) + { + var existedByRequest = await PickupOrderRepository.FirstOrDefaultAsync(x => x.RequestId == input.RequestId.Trim()); + if (existedByRequest != null) + { + if (existedByRequest.CustomerId != customerId) + { + throw new BusinessException("幂等请求已存在"); + } + + return await MapOutputAsync(existedByRequest, includeOrders: true); + } + } + var weights = await OrderRepository.GetWeightsByOrderIdsAsync(accepted.Select(x => x.Id)); var weightSum = weights .GroupBy(x => x.OrderId) @@ -165,21 +179,39 @@ public class PickupOrderService : ApplicationService var freight = input.FreightAmount; var receivable = freight; + var orgId = accepted + .Select(x => x.OrganizationId) + .FirstOrDefault(x => !string.IsNullOrWhiteSpace(x)); + var secretLevel = accepted + .Select(x => x.SecretLevel) + .FirstOrDefault(x => x > 0); + if (secretLevel <= 0) + { + secretLevel = 2; + } var entity = new PickupOrder { PickupNo = await GeneratePickupNoAsync(), + RequestId = NormalizeOptional(input.RequestId), CustomerId = customerId, + OrganizationId = orgId, + SecretLevel = secretLevel, SenderAddressId = senderAddress.Id, SenderName = senderAddress.SenderName ?? string.Empty, - SenderPhone = senderAddress.Phone, SenderCompany = senderAddress.Company, + SenderCountry = senderAddress.Country, SenderCountryCode = senderCountryCode, SenderProvince = senderAddress.Province, SenderCity = senderAddress.City, SenderZipCode = senderAddress.ZipCode, SenderStreetAddress1 = senderAddress.StreetAddress1 ?? string.Empty, SenderStreetAddress2 = senderAddress.StreetAddress2, + SenderStreetAddress3 = senderAddress.StreetAddress3, + SenderHouseNumber = senderAddress.HouseNumber, + SenderPhone = senderAddress.Phone, + SenderTelePhone = senderAddress.TelePhone, + SenderEmail = senderAddress.Email, WarehouseId = input.WarehouseId, CarrierCode = NormalizeOptional(input.CarrierCode), CarrierService = NormalizeOptional(input.CarrierService), @@ -206,6 +238,8 @@ public class PickupOrderService : ApplicationService { PickupOrderId = entity.Id, OrderId = order.Id, + ServiceNo = order.ServiceNo, + ClientNo = order.ClientNo, SortNo = sort++ }); @@ -287,6 +321,8 @@ public class PickupOrderService : ApplicationService { PickupOrderId = pickup.Id, OrderId = order.Id, + ServiceNo = order.ServiceNo, + ClientNo = order.ClientNo, SortNo = sort++ }); order.PickupOrderId = pickup.Id; @@ -451,7 +487,66 @@ public class PickupOrderService : ApplicationService [HttpPost("/order/pickup-order/pay")] public async Task PayAsync([FromBody] PickupOrderIdInput input) { - var pickup = await GetOwnedPickupOrThrowAsync(input.Id); + var pickup = await PayCoreAsync(input.Id); + return await MapOutputAsync(pickup, includeOrders: true); + } + + /// + /// 批量支付揽收运费(逐单扣余额,部分失败不回滚已成功项)。 + /// + [HttpPost("/order/pickup-order/pay-batch")] + public async Task PayBatchAsync([FromBody] PickupOrderIdsInput input) + { + var ids = (input.Ids ?? []).Where(x => x > 0).Distinct().ToList(); + if (ids.Count == 0) + { + throw new BusinessException("请至少选择一张揽收单"); + } + + if (ids.Count > 100) + { + throw new BusinessException("单次最多支付 100 张揽收单"); + } + + var output = new PickupOrderBatchPayOutput(); + foreach (var id in ids) + { + try + { + var pickup = await PayCoreAsync(id); + var amount = pickup.ReceivableAmount ?? pickup.FreightAmount ?? 0; + output.SuccessCount++; + output.PaidAmount += amount; + output.Items.Add(new PickupOrderBatchPayItemOutput + { + Id = pickup.Id, + PickupNo = pickup.PickupNo, + Success = true, + Message = "支付成功", + Amount = amount + }); + } + catch (Exception ex) + { + output.FailCount++; + var pickupNo = await TryGetPickupNoAsync(id); + output.Items.Add(new PickupOrderBatchPayItemOutput + { + Id = id, + PickupNo = pickupNo, + Success = false, + Message = ex.Message + }); + } + } + + output.Message = $"支付完成:成功 {output.SuccessCount},失败 {output.FailCount}"; + return output; + } + + private async Task PayCoreAsync(int id) + { + var pickup = await GetOwnedPickupOrThrowAsync(id); if (pickup.PayStatus == PickupPayStatusCodes.Paid) { throw new BusinessException("揽收单已支付"); @@ -485,7 +580,13 @@ public class PickupOrderService : ApplicationService pickup.PayStatus = PickupPayStatusCodes.Paid; pickup.PayTime = DateTime.Now; await PickupOrderRepository.UpdateAsync(pickup); - return await MapOutputAsync(pickup, includeOrders: true); + return pickup; + } + + private async Task TryGetPickupNoAsync(int id) + { + var pickup = await PickupOrderRepository.FirstOrDefaultAsync(x => x.Id == id); + return pickup?.PickupNo; } /// @@ -500,10 +601,10 @@ public class PickupOrderService : ApplicationService throw new BusinessException("当前状态不可预约下发"); } - var carrier = NormalizeOptional(input.CarrierCode); - if (carrier == null) + var carrierService = NormalizeOptional(input.CarrierService); + if (carrierService == null) { - throw new BusinessException("承运商不能为空"); + throw new BusinessException("承运服务不能为空"); } if (string.Equals(pickup.PayMode, PickupPayModeCodes.Prepaid, StringComparison.OrdinalIgnoreCase) @@ -518,8 +619,14 @@ public class PickupOrderService : ApplicationService throw new BusinessException("请先确认揽收运费"); } - pickup.CarrierCode = carrier; - pickup.CarrierService = NormalizeOptional(input.CarrierService); + pickup.CarrierService = carrierService; + pickup.CarrierCode = NormalizeOptional(input.CarrierCode); + var carrierWaybillNo = NormalizeOptional(input.CarrierWaybillNo); + if (carrierWaybillNo != null) + { + pickup.CarrierWaybillNo = carrierWaybillNo; + } + if (input.ExpectPickupTime.HasValue && input.ExpectPickupTime.Value != default) { pickup.ExpectPickupTime = input.ExpectPickupTime.Value; @@ -781,15 +888,26 @@ public class PickupOrderService : ApplicationService { Id = entity.Id, PickupNo = entity.PickupNo, + RequestId = entity.RequestId, CustomerId = entity.CustomerId, CustomerName = customerName, + OrganizationId = entity.OrganizationId, + SecretLevel = entity.SecretLevel, SenderAddressId = entity.SenderAddressId, SenderName = entity.SenderName, SenderPhone = entity.SenderPhone, + SenderTelePhone = entity.SenderTelePhone, + SenderEmail = entity.SenderEmail, SenderCompany = entity.SenderCompany, + SenderCountry = entity.SenderCountry, + SenderCountryCode = entity.SenderCountryCode, SenderProvince = entity.SenderProvince, SenderCity = entity.SenderCity, + SenderZipCode = entity.SenderZipCode, SenderStreetAddress1 = entity.SenderStreetAddress1, + SenderStreetAddress2 = entity.SenderStreetAddress2, + SenderStreetAddress3 = entity.SenderStreetAddress3, + SenderHouseNumber = entity.SenderHouseNumber, WarehouseId = entity.WarehouseId, WarehouseName = warehouseName, CarrierCode = entity.CarrierCode, diff --git a/src/Oms.Application/Shared/OmsDocHelper.cs b/src/Oms.Application/Shared/OmsDocHelper.cs index e7a6315..ddcd2a3 100644 --- a/src/Oms.Application/Shared/OmsDocHelper.cs +++ b/src/Oms.Application/Shared/OmsDocHelper.cs @@ -199,7 +199,6 @@ public static class OmsDocHelper { OrderStatusCodes.Draft => "草稿", OrderStatusCodes.Predeclared => "已预报", - OrderStatusCodes.Received => "已收货", OrderStatusCodes.PendingAllocation => "待排货", OrderStatusCodes.PendingPayment => "待支付", OrderStatusCodes.PendingShip => "待发货", diff --git a/src/Oms.Domain/ChannelsAggregate/Consts/IssueHandlerTypeCodes.cs b/src/Oms.Domain/ChannelsAggregate/Consts/IssueHandlerTypeCodes.cs new file mode 100644 index 0000000..17c97b8 --- /dev/null +++ b/src/Oms.Domain/ChannelsAggregate/Consts/IssueHandlerTypeCodes.cs @@ -0,0 +1,27 @@ +namespace Oms.Domain.ChannelsAggregate.Consts; + +public static class IssueHandlerTypeCodes +{ + public const string Customer = "customer"; + + public const string User = "user"; + + public static readonly HashSet All = new(StringComparer.OrdinalIgnoreCase) + { + Customer, + User + }; + + public static string GetDisplayName(string? code) => code?.Trim().ToLowerInvariant() switch + { + Customer => "客户", + User => "指定用户", + _ => "—" + }; + + public static bool IsCustomer(string? code) => + string.Equals(code?.Trim(), Customer, StringComparison.OrdinalIgnoreCase); + + public static bool IsUser(string? code) => + string.Equals(code?.Trim(), User, StringComparison.OrdinalIgnoreCase); +} diff --git a/src/Oms.Domain/ChannelsAggregate/Entities/IssueTemplate.cs b/src/Oms.Domain/ChannelsAggregate/Entities/IssueTemplate.cs index 4a07876..8890c17 100644 --- a/src/Oms.Domain/ChannelsAggregate/Entities/IssueTemplate.cs +++ b/src/Oms.Domain/ChannelsAggregate/Entities/IssueTemplate.cs @@ -17,5 +17,11 @@ public class IssueTemplate : OmsAuditedAggregateRoot public string NotifyMethod { get; set; } = string.Empty; + /// 处理人类型:customer=订单所属客户,user=指定用户。 + public string? HandlerType { get; set; } + + /// 指定处理人用户 Id(HandlerType=user 时)。 + public int? HandlerUserId { get; set; } + public EnableStatus Status { get; set; } = EnableStatus.Enabled; } diff --git a/src/Oms.Domain/OrdersAggregate/Consts/OrderExceptionStatusRules.cs b/src/Oms.Domain/OrdersAggregate/Consts/OrderExceptionStatusRules.cs index b188712..b866189 100644 --- a/src/Oms.Domain/OrdersAggregate/Consts/OrderExceptionStatusRules.cs +++ b/src/Oms.Domain/OrdersAggregate/Consts/OrderExceptionStatusRules.cs @@ -6,7 +6,6 @@ public static class OrderExceptionStatusRules { OrderStatusCodes.Draft, OrderStatusCodes.Deleted, - OrderStatusCodes.Problematic, OrderStatusCodes.Claimed }; @@ -18,7 +17,6 @@ public static class OrderExceptionStatusRules { OrderStatusCodes.Draft => "草稿订单不可标记", OrderStatusCodes.Deleted => "已删除订单不可标记", - OrderStatusCodes.Problematic => "已是问题件", OrderStatusCodes.Claimed => "已是理赔件", _ => "订单状态不允许标记" }; diff --git a/src/Oms.Domain/OrdersAggregate/Consts/OrderStatusCodes.cs b/src/Oms.Domain/OrdersAggregate/Consts/OrderStatusCodes.cs index 6debda2..070a647 100644 --- a/src/Oms.Domain/OrdersAggregate/Consts/OrderStatusCodes.cs +++ b/src/Oms.Domain/OrdersAggregate/Consts/OrderStatusCodes.cs @@ -4,7 +4,6 @@ public static class OrderStatusCodes { public const string Draft = "draft"; public const string Predeclared = "predeclared"; - public const string Received = "received"; public const string PendingAllocation = "pendingAllocation"; public const string PendingPayment = "pendingPayment"; public const string PendingShip = "pendingShip"; diff --git a/src/Oms.Domain/OrdersAggregate/Entities/OrderExceptionEvent.cs b/src/Oms.Domain/OrdersAggregate/Entities/OrderExceptionEvent.cs index 8f368ba..be4a1e0 100644 --- a/src/Oms.Domain/OrdersAggregate/Entities/OrderExceptionEvent.cs +++ b/src/Oms.Domain/OrdersAggregate/Entities/OrderExceptionEvent.cs @@ -28,6 +28,12 @@ public class OrderExceptionEvent : OmsAuditedAggregateRoot public string? NotifyMethod { get; set; } + public string? HandlerNames { get; set; } + + public string? HandlerType { get; set; } + + public int? HandlerUserId { get; set; } + public string? ClaimType { get; set; } public decimal? ClaimAmount { get; set; } diff --git a/src/Oms.Domain/OrdersAggregate/Entities/PickupOrder.cs b/src/Oms.Domain/OrdersAggregate/Entities/PickupOrder.cs index a65c6ab..f758144 100644 --- a/src/Oms.Domain/OrdersAggregate/Entities/PickupOrder.cs +++ b/src/Oms.Domain/OrdersAggregate/Entities/PickupOrder.cs @@ -12,19 +12,27 @@ public class PickupOrder : OmsAuditedAggregateRoot [SugarColumn(Length = 32)] public string PickupNo { get; set; } = string.Empty; + [SugarColumn(Length = 64, IsNullable = true)] + public string? RequestId { get; set; } + public int CustomerId { get; set; } + [SugarColumn(Length = 36, IsNullable = true)] + public string? OrganizationId { get; set; } + + public int SecretLevel { get; set; } = 2; + public int SenderAddressId { get; set; } [SugarColumn(Length = 50)] public string SenderName { get; set; } = string.Empty; - [SugarColumn(Length = 32, IsNullable = true)] - public string? SenderPhone { get; set; } - [SugarColumn(Length = 255, IsNullable = true)] public string? SenderCompany { get; set; } + [SugarColumn(IsNullable = true)] + public int? SenderCountry { get; set; } + [SugarColumn(Length = 8, IsNullable = true)] public string? SenderCountryCode { get; set; } @@ -43,6 +51,21 @@ public class PickupOrder : OmsAuditedAggregateRoot [SugarColumn(Length = 500, IsNullable = true)] public string? SenderStreetAddress2 { get; set; } + [SugarColumn(Length = 500, IsNullable = true)] + public string? SenderStreetAddress3 { get; set; } + + [SugarColumn(Length = 50, IsNullable = true)] + public string? SenderHouseNumber { get; set; } + + [SugarColumn(Length = 50, IsNullable = true)] + public string? SenderPhone { get; set; } + + [SugarColumn(Length = 50, IsNullable = true)] + public string? SenderTelePhone { get; set; } + + [SugarColumn(Length = 60, IsNullable = true)] + public string? SenderEmail { get; set; } + public int WarehouseId { get; set; } [SugarColumn(Length = 64, IsNullable = true)] diff --git a/src/Oms.Domain/OrdersAggregate/Entities/PickupOrderLine.cs b/src/Oms.Domain/OrdersAggregate/Entities/PickupOrderLine.cs index 3c06b86..f19450d 100644 --- a/src/Oms.Domain/OrdersAggregate/Entities/PickupOrderLine.cs +++ b/src/Oms.Domain/OrdersAggregate/Entities/PickupOrderLine.cs @@ -13,5 +13,11 @@ public class PickupOrderLine : OmsAuditedAggregateRoot public int OrderId { get; set; } + [SugarColumn(Length = 64, IsNullable = true)] + public string? ServiceNo { get; set; } + + [SugarColumn(Length = 64, IsNullable = true)] + public string? ClientNo { get; set; } + public int SortNo { get; set; } }