feat: 完善揽收单与问题件处理能力
补齐揽收单字段/并发戳与问题件处理人配置,扩展异常工作流与下单同步揽收出参;移除已收货状态并补充相关 SQL 脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c02cb21049
commit
b2355104cf
@ -1,21 +1,31 @@
|
|||||||
-- 揽收单(独立履约/结算)+ 与物流单 1:N 关联;物流单冗余到仓方式与揽收 FK。
|
-- 揽收单(独立履约/结算)+ 与物流单 1:N 关联;物流单冗余到仓方式与揽收 FK。
|
||||||
|
-- 对齐现表公共字段:ConcurrencyStamp / OrganizationId / SecretLevel;发件快照对齐 t_order_sender。
|
||||||
-- 文档:前端 docs/揽收单与物流单串联方案.md
|
-- 文档:前端 docs/揽收单与物流单串联方案.md
|
||||||
-- 执行前请备份。
|
-- 执行前请备份。
|
||||||
|
-- 若表已按旧版脚本建好,请改跑 sql/20260803_pickup_order_align_fields.sql
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `t_pickup_order` (
|
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 '揽收单号',
|
`PickupNo` VARCHAR(32) NOT NULL COMMENT '揽收单号',
|
||||||
|
`RequestId` VARCHAR(64) NULL COMMENT '请求幂等ID',
|
||||||
`CustomerId` INT NOT 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',
|
`SenderAddressId` INT NOT NULL COMMENT '发件地址簿Id',
|
||||||
`SenderName` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '取件联系人快照',
|
`SenderName` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '取件联系人快照',
|
||||||
`SenderPhone` VARCHAR(32) NULL COMMENT '取件电话快照',
|
|
||||||
`SenderCompany` VARCHAR(255) NULL COMMENT '公司快照',
|
`SenderCompany` VARCHAR(255) NULL COMMENT '公司快照',
|
||||||
|
`SenderCountry` INT NULL COMMENT '国家 t_country.Id 快照',
|
||||||
`SenderCountryCode` VARCHAR(8) NULL COMMENT '国家码快照',
|
`SenderCountryCode` VARCHAR(8) NULL COMMENT '国家码快照',
|
||||||
`SenderProvince` VARCHAR(50) NULL COMMENT '省快照',
|
`SenderProvince` VARCHAR(50) NULL COMMENT '省快照',
|
||||||
`SenderCity` VARCHAR(50) NULL COMMENT '市快照',
|
`SenderCity` VARCHAR(50) NULL COMMENT '市快照',
|
||||||
`SenderZipCode` VARCHAR(50) NULL COMMENT '邮编快照',
|
`SenderZipCode` VARCHAR(50) NULL COMMENT '邮编快照',
|
||||||
`SenderStreetAddress1` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '地址1快照',
|
`SenderStreetAddress1` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '地址1快照',
|
||||||
`SenderStreetAddress2` VARCHAR(500) NULL COMMENT '地址2快照',
|
`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',
|
`WarehouseId` INT NOT NULL COMMENT '送到仓库Id',
|
||||||
`CarrierCode` VARCHAR(64) NULL COMMENT '承运商编码(draft可空)',
|
`CarrierCode` VARCHAR(64) NULL COMMENT '承运商编码(draft可空)',
|
||||||
`CarrierService` 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 '结算批次号',
|
`SettlementBatchNo` VARCHAR(64) NULL COMMENT '结算批次号',
|
||||||
`SettledAt` DATETIME NULL COMMENT '结算时间',
|
`SettledAt` DATETIME NULL COMMENT '结算时间',
|
||||||
`Remark` VARCHAR(512) 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 '创建人',
|
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
`UpdateUserId` INT NULL COMMENT '更新人',
|
`UpdateUserId` INT NULL COMMENT '更新人',
|
||||||
`UpdateTime` DATETIME NULL COMMENT '更新时间',
|
`UpdateTime` DATETIME NULL COMMENT '更新时间',
|
||||||
PRIMARY KEY (`Id`),
|
PRIMARY KEY (`Id`),
|
||||||
UNIQUE KEY `uk_pickup_no` (`PickupNo`),
|
UNIQUE KEY `uk_pickup_no` (`PickupNo`),
|
||||||
|
UNIQUE KEY `uk_pickup_request_id` (`RequestId`),
|
||||||
KEY `idx_pickup_customer_status` (`CustomerId`, `Status`, `CreateTime`),
|
KEY `idx_pickup_customer_status` (`CustomerId`, `Status`, `CreateTime`),
|
||||||
KEY `idx_pickup_warehouse_time` (`WarehouseId`, `ExpectPickupTime`),
|
KEY `idx_pickup_warehouse_time` (`WarehouseId`, `ExpectPickupTime`),
|
||||||
KEY `idx_pickup_pay` (`PayStatus`),
|
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='揽收单(上门取件履约+对客收款+承运结算)';
|
) COMMENT='揽收单(上门取件履约+对客收款+承运结算)';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `t_pickup_order_line` (
|
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',
|
`PickupOrderId` INT NOT NULL COMMENT '揽收单Id',
|
||||||
`OrderId` 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 '展示序',
|
`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 '创建人',
|
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
`UpdateUserId` INT NULL COMMENT '更新人',
|
`UpdateUserId` INT NULL COMMENT '更新人',
|
||||||
|
|||||||
53
sql/20260803_pickup_order_align_fields.sql
Normal file
53
sql/20260803_pickup_order_align_fields.sql
Normal file
@ -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 '并发戳';
|
||||||
53
sql/20260803_pickup_order_concurrency_stamp.sql
Normal file
53
sql/20260803_pickup_order_concurrency_stamp.sql
Normal file
@ -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 '并发戳';
|
||||||
9
sql/20260804_issue_template_handler.sql
Normal file
9
sql/20260804_issue_template_handler.sql
Normal file
@ -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`;
|
||||||
3
sql/20260804_order_exception_event_handler.sql
Normal file
3
sql/20260804_order_exception_event_handler.sql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
-- 问题处理:事件级处理人快照(订单可有多条问题记录)
|
||||||
|
ALTER TABLE `t_order_exception_event`
|
||||||
|
ADD COLUMN `HandlerNames` VARCHAR(256) NULL COMMENT '处理人快照' AFTER `NotifyMethod`;
|
||||||
14
sql/20260804_order_status_drop_received.sql
Normal file
14
sql/20260804_order_status_drop_received.sql
Normal file
@ -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';
|
||||||
@ -23,6 +23,14 @@ public class IssueTemplateOutput
|
|||||||
|
|
||||||
public List<string> NotifyMethods { get; set; } = [];
|
public List<string> 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 byte Status { get; set; }
|
||||||
|
|
||||||
public DateTime CreateTime { get; set; }
|
public DateTime CreateTime { get; set; }
|
||||||
|
|||||||
@ -19,5 +19,9 @@ public class IssueTemplateSaveInput
|
|||||||
|
|
||||||
public string? NotifyMethod { get; set; }
|
public string? NotifyMethod { get; set; }
|
||||||
|
|
||||||
|
public string? HandlerType { get; set; }
|
||||||
|
|
||||||
|
public int? HandlerUserId { get; set; }
|
||||||
|
|
||||||
public EnableStatus Status { get; set; } = EnableStatus.Enabled;
|
public EnableStatus Status { get; set; } = EnableStatus.Enabled;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,9 @@ public class OrderAllocationTraceOutput
|
|||||||
|
|
||||||
public decimal? ChargeableWeight { get; set; }
|
public decimal? ChargeableWeight { get; set; }
|
||||||
|
|
||||||
|
/// <summary>订单解析出的价格分区键(用于对照买价明细分区)。</summary>
|
||||||
|
public List<string> ZoneKeys { get; set; } = [];
|
||||||
|
|
||||||
public string? ChargingStage { get; set; }
|
public string? ChargingStage { get; set; }
|
||||||
|
|
||||||
public string? ChargingStageName { get; set; }
|
public string? ChargingStageName { get; set; }
|
||||||
|
|||||||
@ -29,6 +29,67 @@ public class OrderMarkProblematicInput
|
|||||||
public string? Content { get; set; }
|
public string? Content { get; set; }
|
||||||
|
|
||||||
public string? Remark { 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<OrderExceptionEventOutput> Items { get; set; } = [];
|
||||||
|
|
||||||
|
public int PendingCount { get; set; }
|
||||||
|
|
||||||
|
public int ProcessedCount { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class OrderMarkClaimedInput
|
public class OrderMarkClaimedInput
|
||||||
|
|||||||
@ -136,10 +136,15 @@ public class OrderOutput
|
|||||||
public int? PickupOrderId { get; set; }
|
public int? PickupOrderId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 关联揽收单号。
|
/// 关联揽收系统单号(OMS PickupNo)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? PickupNo { get; set; }
|
public string? PickupNo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 国内承运商回执单号(揽收承运商运单号)。
|
||||||
|
/// </summary>
|
||||||
|
public string? CarrierWaybillNo { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 幂等请求标识。
|
/// 幂等请求标识。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -50,6 +50,11 @@ public class OrderParcelCreateOutput
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int? PickupOrderId { get; set; }
|
public int? PickupOrderId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 国内承运运单号(上门揽收时可选)。
|
||||||
|
/// </summary>
|
||||||
|
public string? CarrierWaybillNo { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 揽收单创建失败原因(运单已成功时仅作提示)。
|
/// 揽收单创建失败原因(运单已成功时仅作提示)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -38,3 +38,22 @@ public class OrderReallocateBatchInput
|
|||||||
|
|
||||||
public int? ServiceChannelId { get; set; }
|
public int? ServiceChannelId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询指定渠道可选项(按订单排货池交集)。
|
||||||
|
/// </summary>
|
||||||
|
public class OrderReallocateChannelOptionsInput
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[MinLength(1)]
|
||||||
|
public List<int> OrderIds { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OrderReallocateChannelOptionOutput
|
||||||
|
{
|
||||||
|
public int ServiceChannelId { get; set; }
|
||||||
|
|
||||||
|
public string ChannelCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string? ChannelName { get; set; }
|
||||||
|
}
|
||||||
|
|||||||
@ -6,6 +6,11 @@ public class PickupOrderCreateInput
|
|||||||
{
|
{
|
||||||
public int? CustomerId { get; set; }
|
public int? CustomerId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 请求幂等键(可选)。
|
||||||
|
/// </summary>
|
||||||
|
public string? RequestId { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public int SenderAddressId { get; set; }
|
public int SenderAddressId { get; set; }
|
||||||
|
|
||||||
@ -38,6 +43,39 @@ public class PickupOrderIdInput
|
|||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class PickupOrderIdsInput
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[MinLength(1)]
|
||||||
|
public List<int> 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<PickupOrderBatchPayItemOutput> Items { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
public class PickupOrderOrdersInput
|
public class PickupOrderOrdersInput
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
@ -85,10 +123,21 @@ public class PickupOrderScheduleInput
|
|||||||
[Required]
|
[Required]
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
[Required]
|
/// <summary>
|
||||||
public string CarrierCode { get; set; } = string.Empty;
|
/// 兼容旧字段,已不再作为预约必填;承运服务改用销售产品编码。
|
||||||
|
/// </summary>
|
||||||
|
public string? CarrierCode { get; set; }
|
||||||
|
|
||||||
public string? CarrierService { get; set; }
|
/// <summary>
|
||||||
|
/// 承运服务:国内揽收分组销售产品编码。
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public string CarrierService { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 国内承运商回执单号(手工录入或对接返回)。
|
||||||
|
/// </summary>
|
||||||
|
public string? CarrierWaybillNo { get; set; }
|
||||||
|
|
||||||
public DateTime? ExpectPickupTime { get; set; }
|
public DateTime? ExpectPickupTime { get; set; }
|
||||||
}
|
}
|
||||||
@ -132,24 +181,46 @@ public class PickupOrderOutput
|
|||||||
|
|
||||||
public string PickupNo { get; set; } = string.Empty;
|
public string PickupNo { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string? RequestId { get; set; }
|
||||||
|
|
||||||
public int CustomerId { get; set; }
|
public int CustomerId { get; set; }
|
||||||
|
|
||||||
public string? CustomerName { get; set; }
|
public string? CustomerName { get; set; }
|
||||||
|
|
||||||
|
public string? OrganizationId { get; set; }
|
||||||
|
|
||||||
|
public int SecretLevel { get; set; }
|
||||||
|
|
||||||
public int SenderAddressId { get; set; }
|
public int SenderAddressId { get; set; }
|
||||||
|
|
||||||
public string SenderName { get; set; } = string.Empty;
|
public string SenderName { get; set; } = string.Empty;
|
||||||
|
|
||||||
public string? SenderPhone { get; set; }
|
public string? SenderPhone { get; set; }
|
||||||
|
|
||||||
|
public string? SenderTelePhone { get; set; }
|
||||||
|
|
||||||
|
public string? SenderEmail { get; set; }
|
||||||
|
|
||||||
public string? SenderCompany { get; set; }
|
public string? SenderCompany { get; set; }
|
||||||
|
|
||||||
|
public int? SenderCountry { get; set; }
|
||||||
|
|
||||||
|
public string? SenderCountryCode { get; set; }
|
||||||
|
|
||||||
public string? SenderProvince { get; set; }
|
public string? SenderProvince { get; set; }
|
||||||
|
|
||||||
public string? SenderCity { get; set; }
|
public string? SenderCity { get; set; }
|
||||||
|
|
||||||
|
public string? SenderZipCode { get; set; }
|
||||||
|
|
||||||
public string? SenderStreetAddress1 { 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 int WarehouseId { get; set; }
|
||||||
|
|
||||||
public string? WarehouseName { get; set; }
|
public string? WarehouseName { get; set; }
|
||||||
|
|||||||
@ -24,6 +24,9 @@ public class IssueTemplateService : ApplicationService
|
|||||||
private DataPermRepository DataPermRepository =>
|
private DataPermRepository DataPermRepository =>
|
||||||
LazyServiceProvider.GetRequiredService<DataPermRepository>();
|
LazyServiceProvider.GetRequiredService<DataPermRepository>();
|
||||||
|
|
||||||
|
private OmsRbacUserDisplayNameResolver UserDisplayNameResolver =>
|
||||||
|
LazyServiceProvider.GetRequiredService<OmsRbacUserDisplayNameResolver>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 分页查询问题模板列表。
|
/// 分页查询问题模板列表。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -40,7 +43,8 @@ public class IssueTemplateService : ApplicationService
|
|||||||
new Dictionary<string, bool> { [nameof(IssueTemplate.UpdateTime)] = false, [nameof(IssueTemplate.Id)] = false });
|
new Dictionary<string, bool> { [nameof(IssueTemplate.UpdateTime)] = false, [nameof(IssueTemplate.Id)] = false });
|
||||||
var total = await IssueTemplateRepository.CountAsync(predicate);
|
var total = await IssueTemplateRepository.CountAsync(predicate);
|
||||||
var orgNameMap = await ResolveOrganizationNameMapAsync(entities);
|
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<IssueTemplateOutput> { Items = items, Total = total };
|
return new OmsPageResult<IssueTemplateOutput> { Items = items, Total = total };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +56,8 @@ public class IssueTemplateService : ApplicationService
|
|||||||
{
|
{
|
||||||
var entity = await GetEntityOrThrowAsync(id);
|
var entity = await GetEntityOrThrowAsync(id);
|
||||||
var orgNameMap = await ResolveOrganizationNameMapAsync([entity]);
|
var orgNameMap = await ResolveOrganizationNameMapAsync([entity]);
|
||||||
return MapOutput(entity, orgNameMap);
|
var handlerUserNameMap = await ResolveHandlerUserNameMapAsync([entity]);
|
||||||
|
return MapOutput(entity, orgNameMap, handlerUserNameMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -69,6 +74,8 @@ public class IssueTemplateService : ApplicationService
|
|||||||
Title = normalized.Title,
|
Title = normalized.Title,
|
||||||
Content = normalized.Content,
|
Content = normalized.Content,
|
||||||
NotifyMethod = normalized.NotifyMethod,
|
NotifyMethod = normalized.NotifyMethod,
|
||||||
|
HandlerType = normalized.HandlerType,
|
||||||
|
HandlerUserId = normalized.HandlerUserId,
|
||||||
Status = normalized.Status
|
Status = normalized.Status
|
||||||
};
|
};
|
||||||
await IssueTemplateRepository.InsertAsync(entity);
|
await IssueTemplateRepository.InsertAsync(entity);
|
||||||
@ -87,6 +94,8 @@ public class IssueTemplateService : ApplicationService
|
|||||||
entity.Title = normalized.Title;
|
entity.Title = normalized.Title;
|
||||||
entity.Content = normalized.Content;
|
entity.Content = normalized.Content;
|
||||||
entity.NotifyMethod = normalized.NotifyMethod;
|
entity.NotifyMethod = normalized.NotifyMethod;
|
||||||
|
entity.HandlerType = normalized.HandlerType;
|
||||||
|
entity.HandlerUserId = normalized.HandlerUserId;
|
||||||
entity.Status = normalized.Status;
|
entity.Status = normalized.Status;
|
||||||
await IssueTemplateRepository.UpdateAsync(entity);
|
await IssueTemplateRepository.UpdateAsync(entity);
|
||||||
}
|
}
|
||||||
@ -212,6 +221,38 @@ public class IssueTemplateService : ApplicationService
|
|||||||
throw new BusinessException("请至少选择一种通知方式");
|
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);
|
var organizationId = NormalizeOrganizationId(input.OrganizationId);
|
||||||
if (!string.IsNullOrWhiteSpace(organizationId))
|
if (!string.IsNullOrWhiteSpace(organizationId))
|
||||||
{
|
{
|
||||||
@ -238,6 +279,8 @@ public class IssueTemplateService : ApplicationService
|
|||||||
Title = title,
|
Title = title,
|
||||||
Content = content,
|
Content = content,
|
||||||
NotifyMethod = notifyMethod,
|
NotifyMethod = notifyMethod,
|
||||||
|
HandlerType = handlerType,
|
||||||
|
HandlerUserId = handlerUserId,
|
||||||
Status = input.Status
|
Status = input.Status
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -280,9 +323,20 @@ public class IssueTemplateService : ApplicationService
|
|||||||
: await DataPermRepository.GetOrganizationNamesAsync(orgIds);
|
: await DataPermRepository.GetOrganizationNamesAsync(orgIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<IReadOnlyDictionary<int, string>> ResolveHandlerUserNameMapAsync(
|
||||||
|
IReadOnlyCollection<IssueTemplate> 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(
|
private static IssueTemplateOutput MapOutput(
|
||||||
IssueTemplate entity,
|
IssueTemplate entity,
|
||||||
IReadOnlyDictionary<Guid, string> orgNameMap)
|
IReadOnlyDictionary<Guid, string> orgNameMap,
|
||||||
|
IReadOnlyDictionary<int, string> handlerUserNameMap)
|
||||||
{
|
{
|
||||||
string? organizationName = null;
|
string? organizationName = null;
|
||||||
if (!string.IsNullOrWhiteSpace(entity.OrganizationId)
|
if (!string.IsNullOrWhiteSpace(entity.OrganizationId)
|
||||||
@ -307,6 +361,12 @@ public class IssueTemplateService : ApplicationService
|
|||||||
Content = entity.Content,
|
Content = entity.Content,
|
||||||
NotifyMethod = entity.NotifyMethod,
|
NotifyMethod = entity.NotifyMethod,
|
||||||
NotifyMethods = IssueNotifyMethodCodes.ParseList(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,
|
Status = (byte)entity.Status,
|
||||||
CreateTime = entity.CreateTime,
|
CreateTime = entity.CreateTime,
|
||||||
UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime)
|
UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime)
|
||||||
@ -325,6 +385,10 @@ public class IssueTemplateService : ApplicationService
|
|||||||
|
|
||||||
public string NotifyMethod { get; set; } = string.Empty;
|
public string NotifyMethod { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string HandlerType { get; set; } = IssueHandlerTypeCodes.User;
|
||||||
|
|
||||||
|
public int? HandlerUserId { get; set; }
|
||||||
|
|
||||||
public EnableStatus Status { get; set; }
|
public EnableStatus Status { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -134,6 +134,7 @@ public class AutoTriggerRuntimeService(
|
|||||||
IssuePlaceholderRenderer.Render(content, context),
|
IssuePlaceholderRenderer.Render(content, context),
|
||||||
IssueNotifyMethodCodes.InApp,
|
IssueNotifyMethodCodes.InApp,
|
||||||
operatorUserId,
|
operatorUserId,
|
||||||
|
null,
|
||||||
AutoTriggerConditionMatcher.ParseNotifierUserIds(trigger.Notifier));
|
AutoTriggerConditionMatcher.ParseNotifierUserIds(trigger.Notifier));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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<int> NotifyUserIds { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IssueHandlerResolver(
|
||||||
|
ICustomerRepository customerRepository,
|
||||||
|
OmsRbacUserDisplayNameResolver userDisplayNameResolver) : IScopedDependency
|
||||||
|
{
|
||||||
|
public async Task<IssueHandlerResolution> 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<IssueHandlerResolution> 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<IssueHandlerResolution> 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]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
using Fake.Application;
|
||||||
using Fake.DependencyInjection;
|
using Fake.DependencyInjection;
|
||||||
using Oms.Domain.ChannelsAggregate.Consts;
|
using Oms.Domain.ChannelsAggregate.Consts;
|
||||||
using Oms.Domain.ChannelsAggregate.Entities;
|
using Oms.Domain.ChannelsAggregate.Entities;
|
||||||
@ -21,6 +22,8 @@ public sealed class ProblematicMarkRequest
|
|||||||
|
|
||||||
public string? NotifyMethod { get; set; }
|
public string? NotifyMethod { get; set; }
|
||||||
|
|
||||||
|
public string? HandlerNames { get; set; }
|
||||||
|
|
||||||
public string? Remark { get; set; }
|
public string? Remark { get; set; }
|
||||||
|
|
||||||
public IEnumerable<int>? ExtraReceiverUserIds { get; set; }
|
public IEnumerable<int>? ExtraReceiverUserIds { get; set; }
|
||||||
@ -30,6 +33,7 @@ public class OrderExceptionWorkflowService(
|
|||||||
IOrderRepository orderRepository,
|
IOrderRepository orderRepository,
|
||||||
IOrderExceptionEventRepository orderExceptionEventRepository,
|
IOrderExceptionEventRepository orderExceptionEventRepository,
|
||||||
IIssueTemplateRepository issueTemplateRepository,
|
IIssueTemplateRepository issueTemplateRepository,
|
||||||
|
IssueHandlerResolver issueHandlerResolver,
|
||||||
OrderIssueNotificationService issueNotificationService) : IScopedDependency
|
OrderIssueNotificationService issueNotificationService) : IScopedDependency
|
||||||
{
|
{
|
||||||
public async Task<string?> TryMarkProblematicAsync(
|
public async Task<string?> TryMarkProblematicAsync(
|
||||||
@ -43,13 +47,40 @@ public class OrderExceptionWorkflowService(
|
|||||||
return OrderExceptionStatusRules.GetForbiddenReason(order.OrderStatus);
|
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 context = IssuePlaceholderContext.FromOrder(order, request.IssueType, request.Title);
|
||||||
var renderedTitle = IssuePlaceholderRenderer.Render(request.Title, context);
|
var renderedTitle = IssuePlaceholderRenderer.Render(request.Title, context);
|
||||||
var renderedContent = IssuePlaceholderRenderer.Render(request.Content, context);
|
var renderedContent = IssuePlaceholderRenderer.Render(request.Content, context);
|
||||||
|
|
||||||
|
var notifyMethod = request.NotifyMethod;
|
||||||
|
if (IssueHandlerTypeCodes.IsCustomer(handlerResolution.HandlerType))
|
||||||
|
{
|
||||||
|
notifyMethod = IssueNotifyMethodCodes.InApp;
|
||||||
|
}
|
||||||
|
|
||||||
var fromStatus = order.OrderStatus;
|
var fromStatus = order.OrderStatus;
|
||||||
order.PreviousOrderStatus = fromStatus;
|
var isAppend = string.Equals(fromStatus, OrderStatusCodes.Problematic, StringComparison.OrdinalIgnoreCase);
|
||||||
order.OrderStatus = OrderStatusCodes.Problematic;
|
if (!isAppend)
|
||||||
|
{
|
||||||
|
order.PreviousOrderStatus = fromStatus;
|
||||||
|
order.OrderStatus = OrderStatusCodes.Problematic;
|
||||||
|
}
|
||||||
|
|
||||||
order.HandleStatus = OrderHandleStatusCodes.Pending;
|
order.HandleStatus = OrderHandleStatusCodes.Pending;
|
||||||
order.IssueType = request.IssueType;
|
order.IssueType = request.IssueType;
|
||||||
order.IssueTitle = renderedTitle;
|
order.IssueTitle = renderedTitle;
|
||||||
@ -76,18 +107,25 @@ public class OrderExceptionWorkflowService(
|
|||||||
Title = renderedTitle,
|
Title = renderedTitle,
|
||||||
Content = renderedContent,
|
Content = renderedContent,
|
||||||
IssueTemplateId = request.IssueTemplateId,
|
IssueTemplateId = request.IssueTemplateId,
|
||||||
NotifyMethod = request.NotifyMethod,
|
NotifyMethod = notifyMethod,
|
||||||
|
HandlerNames = handlerResolution.HandlerNames,
|
||||||
|
HandlerType = handlerResolution.HandlerType,
|
||||||
|
HandlerUserId = handlerResolution.HandlerUserId,
|
||||||
Remark = request.Remark
|
Remark = request.Remark
|
||||||
});
|
});
|
||||||
|
|
||||||
if (sendNotification)
|
if (sendNotification && !string.IsNullOrWhiteSpace(notifyMethod))
|
||||||
{
|
{
|
||||||
|
var notifyUserIds = handlerResolution.NotifyUserIds.Count > 0
|
||||||
|
? handlerResolution.NotifyUserIds
|
||||||
|
: null;
|
||||||
await issueNotificationService.DispatchAsync(
|
await issueNotificationService.DispatchAsync(
|
||||||
order,
|
order,
|
||||||
renderedTitle,
|
renderedTitle,
|
||||||
renderedContent,
|
renderedContent,
|
||||||
request.NotifyMethod,
|
notifyMethod,
|
||||||
operatorUserId,
|
operatorUserId,
|
||||||
|
notifyUserIds,
|
||||||
request.ExtraReceiverUserIds);
|
request.ExtraReceiverUserIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -29,6 +29,7 @@ public class OrderIssueNotificationService(
|
|||||||
string content,
|
string content,
|
||||||
string? notifyMethodCsv,
|
string? notifyMethodCsv,
|
||||||
int operatorUserId,
|
int operatorUserId,
|
||||||
|
IEnumerable<int>? primaryReceiverUserIds = null,
|
||||||
IEnumerable<int>? extraReceiverUserIds = null)
|
IEnumerable<int>? extraReceiverUserIds = null)
|
||||||
{
|
{
|
||||||
var methods = IssueNotifyMethodCodes.ParseList(notifyMethodCsv);
|
var methods = IssueNotifyMethodCodes.ParseList(notifyMethodCsv);
|
||||||
@ -50,7 +51,7 @@ public class OrderIssueNotificationService(
|
|||||||
switch (method)
|
switch (method)
|
||||||
{
|
{
|
||||||
case IssueNotifyMethodCodes.InApp:
|
case IssueNotifyMethodCodes.InApp:
|
||||||
await SendInAppAsync(order, customer, title, content, extraReceiverUserIds);
|
await SendInAppAsync(order, customer, title, content, primaryReceiverUserIds, extraReceiverUserIds);
|
||||||
break;
|
break;
|
||||||
case IssueNotifyMethodCodes.Email:
|
case IssueNotifyMethodCodes.Email:
|
||||||
await SendEmailAsync(order, customer, title, content, operatorUserId);
|
await SendEmailAsync(order, customer, title, content, operatorUserId);
|
||||||
@ -81,19 +82,44 @@ public class OrderIssueNotificationService(
|
|||||||
Customer? customer,
|
Customer? customer,
|
||||||
string title,
|
string title,
|
||||||
string content,
|
string content,
|
||||||
|
IEnumerable<int>? primaryReceiverUserIds,
|
||||||
IEnumerable<int>? extraReceiverUserIds)
|
IEnumerable<int>? extraReceiverUserIds)
|
||||||
{
|
{
|
||||||
var receiverIds = new HashSet<int>();
|
var receiverIds = new HashSet<int>();
|
||||||
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ using Fake.Domain.Exceptions;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Oms.Application.Dtos.Orders;
|
using Oms.Application.Dtos.Orders;
|
||||||
using Oms.Application.Services.Orders.Issues;
|
using Oms.Application.Services.Orders.Issues;
|
||||||
|
using Oms.Application.Shared;
|
||||||
using Oms.Domain.ChannelsAggregate.Consts;
|
using Oms.Domain.ChannelsAggregate.Consts;
|
||||||
using Oms.Domain.ChannelsAggregate.Entities;
|
using Oms.Domain.ChannelsAggregate.Entities;
|
||||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||||
@ -40,6 +41,7 @@ public partial class OrderService
|
|||||||
Content = payload.Content,
|
Content = payload.Content,
|
||||||
IssueTemplateId = payload.IssueTemplateId,
|
IssueTemplateId = payload.IssueTemplateId,
|
||||||
NotifyMethod = payload.NotifyMethod,
|
NotifyMethod = payload.NotifyMethod,
|
||||||
|
HandlerNames = payload.HandlerNames,
|
||||||
Remark = payload.Remark
|
Remark = payload.Remark
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -115,6 +117,154 @@ public partial class OrderService
|
|||||||
input.Remark?.Trim());
|
input.Remark?.Trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 订单问题处理记录列表(mark_problematic 事件,支持一单多条)。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("/order/exception-events/list")]
|
||||||
|
public async Task<OrderExceptionEventListOutput> 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))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单条问题标记已处理。
|
||||||
|
/// </summary>
|
||||||
|
[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<int>();
|
||||||
|
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<int, string> 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<OrderBatchActionResultOutput> ExecuteProblematicBatchAsync(
|
private async Task<OrderBatchActionResultOutput> ExecuteProblematicBatchAsync(
|
||||||
IReadOnlyCollection<int> orderIds,
|
IReadOnlyCollection<int> orderIds,
|
||||||
ProblematicMarkRequest request)
|
ProblematicMarkRequest request)
|
||||||
@ -271,6 +421,12 @@ public partial class OrderService
|
|||||||
throw new BusinessException("备注长度不能超过512");
|
throw new BusinessException("备注长度不能超过512");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var handlerNames = input.HandlerNames?.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(handlerNames) && handlerNames.Length > 256)
|
||||||
|
{
|
||||||
|
throw new BusinessException("处理人长度不能超过256");
|
||||||
|
}
|
||||||
|
|
||||||
return new ResolvedProblematicPayload
|
return new ResolvedProblematicPayload
|
||||||
{
|
{
|
||||||
IssueType = issueType,
|
IssueType = issueType,
|
||||||
@ -278,6 +434,7 @@ public partial class OrderService
|
|||||||
Content = content,
|
Content = content,
|
||||||
IssueTemplateId = template?.Id,
|
IssueTemplateId = template?.Id,
|
||||||
NotifyMethod = template == null ? null : IssueNotifyMethodCodes.Normalize(template.NotifyMethod),
|
NotifyMethod = template == null ? null : IssueNotifyMethodCodes.Normalize(template.NotifyMethod),
|
||||||
|
HandlerNames = string.IsNullOrWhiteSpace(handlerNames) ? null : handlerNames,
|
||||||
Remark = remark
|
Remark = remark
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -399,6 +556,8 @@ public partial class OrderService
|
|||||||
|
|
||||||
public string? NotifyMethod { get; set; }
|
public string? NotifyMethod { get; set; }
|
||||||
|
|
||||||
|
public string? HandlerNames { get; set; }
|
||||||
|
|
||||||
public string? Remark { get; set; }
|
public string? Remark { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -98,6 +98,9 @@ public partial class OrderService
|
|||||||
? []
|
? []
|
||||||
: await PickupOrderRepository.GetListAsync(x => pickupOrderIds.Contains(x.Id));
|
: await PickupOrderRepository.GetListAsync(x => pickupOrderIds.Contains(x.Id));
|
||||||
var pickupNoMap = pickupOrders.ToDictionary(x => x.Id, x => x.PickupNo);
|
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 =>
|
var outputs = orders.Select(order =>
|
||||||
{
|
{
|
||||||
@ -165,6 +168,10 @@ public partial class OrderService
|
|||||||
&& pickupNoMap.TryGetValue(order.PickupOrderId.Value, out var pickupNo)
|
&& pickupNoMap.TryGetValue(order.PickupOrderId.Value, out var pickupNo)
|
||||||
? pickupNo
|
? pickupNo
|
||||||
: null,
|
: null,
|
||||||
|
CarrierWaybillNo = order.PickupOrderId is > 0
|
||||||
|
&& carrierWaybillNoMap.TryGetValue(order.PickupOrderId.Value, out var carrierWaybillNo)
|
||||||
|
? carrierWaybillNo
|
||||||
|
: null,
|
||||||
RequestId = order.RequestId,
|
RequestId = order.RequestId,
|
||||||
ExportCustomsMode = order.ExportCustomsMode,
|
ExportCustomsMode = order.ExportCustomsMode,
|
||||||
PredeclareFailReason = string.IsNullOrWhiteSpace(order.PredeclareFailReason)
|
PredeclareFailReason = string.IsNullOrWhiteSpace(order.PredeclareFailReason)
|
||||||
|
|||||||
@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Oms.Application.Dtos.Channels;
|
using Oms.Application.Dtos.Channels;
|
||||||
using Oms.Application.Dtos.Orders;
|
using Oms.Application.Dtos.Orders;
|
||||||
|
using Oms.Application.Dtos.Shared;
|
||||||
using Oms.Application.Shared;
|
using Oms.Application.Shared;
|
||||||
using Oms.Domain.ChannelsAggregate.Consts;
|
using Oms.Domain.ChannelsAggregate.Consts;
|
||||||
using Oms.Domain.ChannelsAggregate.Entities;
|
using Oms.Domain.ChannelsAggregate.Entities;
|
||||||
@ -328,7 +329,8 @@ public partial class OrderService
|
|||||||
var weights = await OrderRepository.GetWeightsByOrderIdsAsync([order.Id]);
|
var weights = await OrderRepository.GetWeightsByOrderIdsAsync([order.Id]);
|
||||||
var weightRow = ResolvePayableWeightRow(order, weights);
|
var weightRow = ResolvePayableWeightRow(order, weights);
|
||||||
var chargeable = weightRow.ChargeableWeight ?? 0;
|
var chargeable = weightRow.ChargeableWeight ?? 0;
|
||||||
var snapshotBinding = await ResolveSnapshotBindingFromOrderAsync(product, order);
|
// 现算与重新排货一致:规则分区 + 渠道买价均取当前有效发布。
|
||||||
|
var snapshotBinding = await ResolveSnapshotBindingAsync(product, DateTime.Now);
|
||||||
var billing = await ResolveOrderBillingContextAsync(
|
var billing = await ResolveOrderBillingContextAsync(
|
||||||
order,
|
order,
|
||||||
product,
|
product,
|
||||||
@ -393,6 +395,91 @@ public partial class OrderService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 指定渠道可选项:取选中订单销售产品排货池(含客户过滤)的交集。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("/order/fee/reallocate-channel-options")]
|
||||||
|
public async Task<OmsItemsResult<OrderReallocateChannelOptionOutput>> GetReallocateChannelOptionsAsync(
|
||||||
|
[FromBody] OrderReallocateChannelOptionsInput input)
|
||||||
|
{
|
||||||
|
var orderIds = (input.OrderIds ?? []).Where(x => x > 0).Distinct().ToList();
|
||||||
|
if (orderIds.Count == 0)
|
||||||
|
{
|
||||||
|
return new OmsItemsResult<OrderReallocateChannelOptionOutput> { Items = [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
var orders = await OrderRepository.GetListAsync(x => orderIds.Contains(x.Id));
|
||||||
|
if (orders.Count == 0)
|
||||||
|
{
|
||||||
|
return new OmsItemsResult<OrderReallocateChannelOptionOutput> { Items = [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
HashSet<int>? intersection = null;
|
||||||
|
var channelMeta = new Dictionary<int, (string Code, string? Name)>();
|
||||||
|
|
||||||
|
foreach (var order in orders)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(order.ProductCode))
|
||||||
|
{
|
||||||
|
return new OmsItemsResult<OrderReallocateChannelOptionOutput> { Items = [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim());
|
||||||
|
if (product == null)
|
||||||
|
{
|
||||||
|
return new OmsItemsResult<OrderReallocateChannelOptionOutput> { 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<int>();
|
||||||
|
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<OrderReallocateChannelOptionOutput> { Items = items };
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 单票重新排货:仅重写应付与服务渠道,不动应收。
|
/// 单票重新排货:仅重写应付与服务渠道,不动应收。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -646,7 +733,10 @@ public partial class OrderService
|
|||||||
throw new BusinessException("计费重无效,请先完善件重尺");
|
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(
|
var billing = await ResolveOrderBillingContextAsync(
|
||||||
order,
|
order,
|
||||||
product,
|
product,
|
||||||
@ -787,7 +877,7 @@ public partial class OrderService
|
|||||||
private static (string WeightType, string ChargingStage) ResolveReceivableBillingStage(Order order) =>
|
private static (string WeightType, string ChargingStage) ResolveReceivableBillingStage(Order order) =>
|
||||||
order.OrderStatus switch
|
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),
|
=> (OrderWeightTypeCodes.Receive, OrderFeeChargingStageCodes.Inbound),
|
||||||
OrderStatusCodes.PendingOnline or OrderStatusCodes.Shipping
|
OrderStatusCodes.PendingOnline or OrderStatusCodes.Shipping
|
||||||
or OrderStatusCodes.DeliverySuccess or OrderStatusCodes.DeliveryFailed
|
or OrderStatusCodes.DeliverySuccess or OrderStatusCodes.DeliveryFailed
|
||||||
|
|||||||
@ -793,11 +793,28 @@ public partial class OrderService
|
|||||||
throw new BusinessException(evaluation.FailReason);
|
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;
|
order.ChannelCode = selectedCandidate.Channel.ChannelCode;
|
||||||
AllocationProviderSnapshotEnricher.ApplyDownstreamFieldsToOrder(
|
AllocationProviderSnapshotEnricher.ApplyDownstreamFieldsToOrder(
|
||||||
order,
|
order,
|
||||||
selectedCandidate.Provider,
|
selectedCandidate.Provider,
|
||||||
selectedCandidate.Channel);
|
selectedCandidate.Channel);
|
||||||
|
|
||||||
|
// 换渠道后旧下游单号/跟踪号失效,清空以免「新渠道+旧服务商单号」。
|
||||||
|
if (channelChanged
|
||||||
|
&& (!string.IsNullOrWhiteSpace(order.DownstreamServiceNo)
|
||||||
|
|| !string.IsNullOrWhiteSpace(order.TrackNo)))
|
||||||
|
{
|
||||||
|
order.DownstreamServiceNo = null;
|
||||||
|
order.TrackNo = null;
|
||||||
|
}
|
||||||
|
|
||||||
// 有效应付只保留本阶段一套,避免 forecast+inbound 叠行虚增成本。
|
// 有效应付只保留本阶段一套,避免 forecast+inbound 叠行虚增成本。
|
||||||
await OrderRepository.DeletePayableFeesAsync(order.Id);
|
await OrderRepository.DeletePayableFeesAsync(order.Id);
|
||||||
await OrderRepository.UpsertFeesAsync(order.Id, payableFees);
|
await OrderRepository.UpsertFeesAsync(order.Id, payableFees);
|
||||||
@ -811,6 +828,7 @@ public partial class OrderService
|
|||||||
public string SchemeSource { get; init; } = "productDefault";
|
public string SchemeSource { get; init; } = "productDefault";
|
||||||
public decimal ChargeableWeight { get; init; }
|
public decimal ChargeableWeight { get; init; }
|
||||||
public string ChargingStage { get; init; } = string.Empty;
|
public string ChargingStage { get; init; } = string.Empty;
|
||||||
|
public IReadOnlyList<string> ZoneKeys { get; init; } = [];
|
||||||
public List<AllocationResolver.ChannelCandidate> Candidates { get; init; } = [];
|
public List<AllocationResolver.ChannelCandidate> Candidates { get; init; } = [];
|
||||||
public List<OrderAllocationSkippedOutput> Skipped { get; init; } = [];
|
public List<OrderAllocationSkippedOutput> Skipped { get; init; } = [];
|
||||||
public AllocationResolver.ChannelCandidate? Selected { get; set; }
|
public AllocationResolver.ChannelCandidate? Selected { get; set; }
|
||||||
@ -872,6 +890,7 @@ public partial class OrderService
|
|||||||
SchemeSource = schemeSource,
|
SchemeSource = schemeSource,
|
||||||
ChargeableWeight = chargeableWeight,
|
ChargeableWeight = chargeableWeight,
|
||||||
ChargingStage = chargingStage,
|
ChargingStage = chargingStage,
|
||||||
|
ZoneKeys = zoneKeys,
|
||||||
Skipped = skipped,
|
Skipped = skipped,
|
||||||
FailReason = "指定渠道不在该销售产品排货池或当前客户不可用"
|
FailReason = "指定渠道不在该销售产品排货池或当前客户不可用"
|
||||||
};
|
};
|
||||||
@ -887,14 +906,17 @@ public partial class OrderService
|
|||||||
SchemeSource = schemeSource,
|
SchemeSource = schemeSource,
|
||||||
ChargeableWeight = chargeableWeight,
|
ChargeableWeight = chargeableWeight,
|
||||||
ChargingStage = chargingStage,
|
ChargingStage = chargingStage,
|
||||||
|
ZoneKeys = zoneKeys,
|
||||||
Skipped = skipped,
|
Skipped = skipped,
|
||||||
FailReason = "排货规则未匹配到可用服务渠道"
|
FailReason = "排货规则未匹配到可用服务渠道"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var bindTime = order.CreateTime == default ? DateTime.Now : order.CreateTime;
|
// 应付买价取「当前」有效发布(PublishTime<=Now),不用下单时间,避免重新排货/入库漏掉后续发布的买价。
|
||||||
|
var bindTime = DateTime.Now;
|
||||||
var candidates = new List<AllocationResolver.ChannelCandidate>();
|
var candidates = new List<AllocationResolver.ChannelCandidate>();
|
||||||
var recipient = (await OrderRepository.GetRecipientsByOrderIdsAsync([order.Id])).FirstOrDefault();
|
var recipient = (await OrderRepository.GetRecipientsByOrderIdsAsync([order.Id])).FirstOrDefault();
|
||||||
|
var cargoAttribute = order.CargoAttribute ?? CargoAttributeCodes.General;
|
||||||
|
|
||||||
foreach (var provider in filtered)
|
foreach (var provider in filtered)
|
||||||
{
|
{
|
||||||
@ -942,7 +964,7 @@ public partial class OrderService
|
|||||||
channelPriceBinding.PriceEffectiveTime,
|
channelPriceBinding.PriceEffectiveTime,
|
||||||
chargeableWeight,
|
chargeableWeight,
|
||||||
zoneKeys,
|
zoneKeys,
|
||||||
order.CargoAttribute ?? CargoAttributeCodes.General,
|
cargoAttribute,
|
||||||
chargingStage,
|
chargingStage,
|
||||||
order.WarehouseId,
|
order.WarehouseId,
|
||||||
warehouseName,
|
warehouseName,
|
||||||
@ -952,12 +974,22 @@ public partial class OrderService
|
|||||||
channelRemoteZoneKeys);
|
channelRemoteZoneKeys);
|
||||||
if (previewFees.Count == 0)
|
if (previewFees.Count == 0)
|
||||||
{
|
{
|
||||||
|
var miss = ParcelBillingCalculator.ExplainPayablePriceMiss(
|
||||||
|
channelPriceBinding.Prices,
|
||||||
|
chargeableWeight,
|
||||||
|
zoneKeys,
|
||||||
|
cargoAttribute,
|
||||||
|
order.WarehouseId,
|
||||||
|
warehouseName,
|
||||||
|
channelRemoteZoneKeys);
|
||||||
skipped.Add(new OrderAllocationSkippedOutput
|
skipped.Add(new OrderAllocationSkippedOutput
|
||||||
{
|
{
|
||||||
ServiceChannelId = channel.Id,
|
ServiceChannelId = channel.Id,
|
||||||
ChannelCode = channel.ChannelCode,
|
ChannelCode = channel.ChannelCode,
|
||||||
ChannelName = channel.ChannelName,
|
ChannelName = channel.ChannelName,
|
||||||
Reason = "未匹配到有效渠道买价"
|
Reason = string.IsNullOrWhiteSpace(miss)
|
||||||
|
? $"未匹配到有效渠道买价(版本 {channelPriceBinding.PriceVersion})"
|
||||||
|
: $"未匹配到有效渠道买价(版本 {channelPriceBinding.PriceVersion}):{miss}"
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -973,6 +1005,7 @@ public partial class OrderService
|
|||||||
|
|
||||||
if (candidates.Count == 0)
|
if (candidates.Count == 0)
|
||||||
{
|
{
|
||||||
|
var detail = skipped.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.Reason))?.Reason;
|
||||||
return new AllocationEvaluation
|
return new AllocationEvaluation
|
||||||
{
|
{
|
||||||
StoredScheme = storedScheme,
|
StoredScheme = storedScheme,
|
||||||
@ -980,11 +1013,14 @@ public partial class OrderService
|
|||||||
SchemeSource = schemeSource,
|
SchemeSource = schemeSource,
|
||||||
ChargeableWeight = chargeableWeight,
|
ChargeableWeight = chargeableWeight,
|
||||||
ChargingStage = chargingStage,
|
ChargingStage = chargingStage,
|
||||||
|
ZoneKeys = zoneKeys,
|
||||||
Candidates = candidates,
|
Candidates = candidates,
|
||||||
Skipped = skipped,
|
Skipped = skipped,
|
||||||
FailReason = chargeableWeight <= 0
|
FailReason = chargeableWeight <= 0
|
||||||
? "计费重无效,无法计算应付费用"
|
? "计费重无效,无法计算应付费用"
|
||||||
: "排货规则未匹配到有效渠道买价"
|
: string.IsNullOrWhiteSpace(detail)
|
||||||
|
? "排货规则未匹配到有效渠道买价"
|
||||||
|
: $"排货规则未匹配到有效渠道买价;{detail}"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -996,6 +1032,7 @@ public partial class OrderService
|
|||||||
SchemeSource = schemeSource,
|
SchemeSource = schemeSource,
|
||||||
ChargeableWeight = chargeableWeight,
|
ChargeableWeight = chargeableWeight,
|
||||||
ChargingStage = chargingStage,
|
ChargingStage = chargingStage,
|
||||||
|
ZoneKeys = zoneKeys,
|
||||||
Candidates = candidates,
|
Candidates = candidates,
|
||||||
Skipped = skipped,
|
Skipped = skipped,
|
||||||
Selected = selected
|
Selected = selected
|
||||||
@ -1053,6 +1090,7 @@ public partial class OrderService
|
|||||||
SchemeSource = evaluation.SchemeSource,
|
SchemeSource = evaluation.SchemeSource,
|
||||||
SchemeSourceName = ResolveAllocationSchemeSourceName(evaluation.SchemeSource),
|
SchemeSourceName = ResolveAllocationSchemeSourceName(evaluation.SchemeSource),
|
||||||
ChargeableWeight = evaluation.ChargeableWeight,
|
ChargeableWeight = evaluation.ChargeableWeight,
|
||||||
|
ZoneKeys = evaluation.ZoneKeys?.ToList() ?? [],
|
||||||
ChargingStage = evaluation.ChargingStage,
|
ChargingStage = evaluation.ChargingStage,
|
||||||
ChargingStageName = ResolveChargingStageName(evaluation.ChargingStage),
|
ChargingStageName = ResolveChargingStageName(evaluation.ChargingStage),
|
||||||
SelectedChannelCode = selectedChannelCode ?? evaluation.Selected?.Channel.ChannelCode,
|
SelectedChannelCode = selectedChannelCode ?? evaluation.Selected?.Channel.ChannelCode,
|
||||||
|
|||||||
@ -125,7 +125,7 @@ public partial class OrderService : ApplicationService
|
|||||||
throw new BusinessException("订单已支付");
|
throw new BusinessException("订单已支付");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (order.OrderStatus is not (OrderStatusCodes.PendingPayment or OrderStatusCodes.Received))
|
if (order.OrderStatus is not OrderStatusCodes.PendingPayment)
|
||||||
{
|
{
|
||||||
throw new BusinessException("当前订单状态不可支付");
|
throw new BusinessException("当前订单状态不可支付");
|
||||||
}
|
}
|
||||||
@ -475,6 +475,7 @@ public partial class OrderService : ApplicationService
|
|||||||
});
|
});
|
||||||
output.PickupNo = pickup.PickupNo;
|
output.PickupNo = pickup.PickupNo;
|
||||||
output.PickupOrderId = pickup.Id;
|
output.PickupOrderId = pickup.Id;
|
||||||
|
output.CarrierWaybillNo = pickup.CarrierWaybillNo;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -628,6 +628,128 @@ internal static class ParcelBillingCalculator
|
|||||||
return string.Join(";", reasons);
|
return string.Join(";", reasons);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 买价未命中时,按价格项输出具体原因(分区/货物类型/重量区间等)。
|
||||||
|
/// </summary>
|
||||||
|
public static string ExplainPayablePriceMiss(
|
||||||
|
IReadOnlyList<ServiceChannelPriceListOutput> prices,
|
||||||
|
decimal chargeableWeight,
|
||||||
|
IReadOnlyList<string> zoneKeys,
|
||||||
|
string cargoAttribute,
|
||||||
|
int? warehouseId = null,
|
||||||
|
string? warehouseName = null,
|
||||||
|
IReadOnlyList<string>? 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<string>();
|
||||||
|
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<string>();
|
||||||
|
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<OrderFee> CalculateForecastReceivableFees(
|
public static List<OrderFee> CalculateForecastReceivableFees(
|
||||||
SalesProductPricePublishSnapshotDto priceSnapshot,
|
SalesProductPricePublishSnapshotDto priceSnapshot,
|
||||||
string priceVersion,
|
string priceVersion,
|
||||||
|
|||||||
@ -156,6 +156,20 @@ public class PickupOrderService : ApplicationService
|
|||||||
senderCountryCode = country?.CountryCode;
|
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 weights = await OrderRepository.GetWeightsByOrderIdsAsync(accepted.Select(x => x.Id));
|
||||||
var weightSum = weights
|
var weightSum = weights
|
||||||
.GroupBy(x => x.OrderId)
|
.GroupBy(x => x.OrderId)
|
||||||
@ -165,21 +179,39 @@ public class PickupOrderService : ApplicationService
|
|||||||
|
|
||||||
var freight = input.FreightAmount;
|
var freight = input.FreightAmount;
|
||||||
var receivable = freight;
|
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
|
var entity = new PickupOrder
|
||||||
{
|
{
|
||||||
PickupNo = await GeneratePickupNoAsync(),
|
PickupNo = await GeneratePickupNoAsync(),
|
||||||
|
RequestId = NormalizeOptional(input.RequestId),
|
||||||
CustomerId = customerId,
|
CustomerId = customerId,
|
||||||
|
OrganizationId = orgId,
|
||||||
|
SecretLevel = secretLevel,
|
||||||
SenderAddressId = senderAddress.Id,
|
SenderAddressId = senderAddress.Id,
|
||||||
SenderName = senderAddress.SenderName ?? string.Empty,
|
SenderName = senderAddress.SenderName ?? string.Empty,
|
||||||
SenderPhone = senderAddress.Phone,
|
|
||||||
SenderCompany = senderAddress.Company,
|
SenderCompany = senderAddress.Company,
|
||||||
|
SenderCountry = senderAddress.Country,
|
||||||
SenderCountryCode = senderCountryCode,
|
SenderCountryCode = senderCountryCode,
|
||||||
SenderProvince = senderAddress.Province,
|
SenderProvince = senderAddress.Province,
|
||||||
SenderCity = senderAddress.City,
|
SenderCity = senderAddress.City,
|
||||||
SenderZipCode = senderAddress.ZipCode,
|
SenderZipCode = senderAddress.ZipCode,
|
||||||
SenderStreetAddress1 = senderAddress.StreetAddress1 ?? string.Empty,
|
SenderStreetAddress1 = senderAddress.StreetAddress1 ?? string.Empty,
|
||||||
SenderStreetAddress2 = senderAddress.StreetAddress2,
|
SenderStreetAddress2 = senderAddress.StreetAddress2,
|
||||||
|
SenderStreetAddress3 = senderAddress.StreetAddress3,
|
||||||
|
SenderHouseNumber = senderAddress.HouseNumber,
|
||||||
|
SenderPhone = senderAddress.Phone,
|
||||||
|
SenderTelePhone = senderAddress.TelePhone,
|
||||||
|
SenderEmail = senderAddress.Email,
|
||||||
WarehouseId = input.WarehouseId,
|
WarehouseId = input.WarehouseId,
|
||||||
CarrierCode = NormalizeOptional(input.CarrierCode),
|
CarrierCode = NormalizeOptional(input.CarrierCode),
|
||||||
CarrierService = NormalizeOptional(input.CarrierService),
|
CarrierService = NormalizeOptional(input.CarrierService),
|
||||||
@ -206,6 +238,8 @@ public class PickupOrderService : ApplicationService
|
|||||||
{
|
{
|
||||||
PickupOrderId = entity.Id,
|
PickupOrderId = entity.Id,
|
||||||
OrderId = order.Id,
|
OrderId = order.Id,
|
||||||
|
ServiceNo = order.ServiceNo,
|
||||||
|
ClientNo = order.ClientNo,
|
||||||
SortNo = sort++
|
SortNo = sort++
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -287,6 +321,8 @@ public class PickupOrderService : ApplicationService
|
|||||||
{
|
{
|
||||||
PickupOrderId = pickup.Id,
|
PickupOrderId = pickup.Id,
|
||||||
OrderId = order.Id,
|
OrderId = order.Id,
|
||||||
|
ServiceNo = order.ServiceNo,
|
||||||
|
ClientNo = order.ClientNo,
|
||||||
SortNo = sort++
|
SortNo = sort++
|
||||||
});
|
});
|
||||||
order.PickupOrderId = pickup.Id;
|
order.PickupOrderId = pickup.Id;
|
||||||
@ -451,7 +487,66 @@ public class PickupOrderService : ApplicationService
|
|||||||
[HttpPost("/order/pickup-order/pay")]
|
[HttpPost("/order/pickup-order/pay")]
|
||||||
public async Task<PickupOrderOutput> PayAsync([FromBody] PickupOrderIdInput input)
|
public async Task<PickupOrderOutput> PayAsync([FromBody] PickupOrderIdInput input)
|
||||||
{
|
{
|
||||||
var pickup = await GetOwnedPickupOrThrowAsync(input.Id);
|
var pickup = await PayCoreAsync(input.Id);
|
||||||
|
return await MapOutputAsync(pickup, includeOrders: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量支付揽收运费(逐单扣余额,部分失败不回滚已成功项)。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("/order/pickup-order/pay-batch")]
|
||||||
|
public async Task<PickupOrderBatchPayOutput> 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<PickupOrder> PayCoreAsync(int id)
|
||||||
|
{
|
||||||
|
var pickup = await GetOwnedPickupOrThrowAsync(id);
|
||||||
if (pickup.PayStatus == PickupPayStatusCodes.Paid)
|
if (pickup.PayStatus == PickupPayStatusCodes.Paid)
|
||||||
{
|
{
|
||||||
throw new BusinessException("揽收单已支付");
|
throw new BusinessException("揽收单已支付");
|
||||||
@ -485,7 +580,13 @@ public class PickupOrderService : ApplicationService
|
|||||||
pickup.PayStatus = PickupPayStatusCodes.Paid;
|
pickup.PayStatus = PickupPayStatusCodes.Paid;
|
||||||
pickup.PayTime = DateTime.Now;
|
pickup.PayTime = DateTime.Now;
|
||||||
await PickupOrderRepository.UpdateAsync(pickup);
|
await PickupOrderRepository.UpdateAsync(pickup);
|
||||||
return await MapOutputAsync(pickup, includeOrders: true);
|
return pickup;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string?> TryGetPickupNoAsync(int id)
|
||||||
|
{
|
||||||
|
var pickup = await PickupOrderRepository.FirstOrDefaultAsync(x => x.Id == id);
|
||||||
|
return pickup?.PickupNo;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -500,10 +601,10 @@ public class PickupOrderService : ApplicationService
|
|||||||
throw new BusinessException("当前状态不可预约下发");
|
throw new BusinessException("当前状态不可预约下发");
|
||||||
}
|
}
|
||||||
|
|
||||||
var carrier = NormalizeOptional(input.CarrierCode);
|
var carrierService = NormalizeOptional(input.CarrierService);
|
||||||
if (carrier == null)
|
if (carrierService == null)
|
||||||
{
|
{
|
||||||
throw new BusinessException("承运商不能为空");
|
throw new BusinessException("承运服务不能为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(pickup.PayMode, PickupPayModeCodes.Prepaid, StringComparison.OrdinalIgnoreCase)
|
if (string.Equals(pickup.PayMode, PickupPayModeCodes.Prepaid, StringComparison.OrdinalIgnoreCase)
|
||||||
@ -518,8 +619,14 @@ public class PickupOrderService : ApplicationService
|
|||||||
throw new BusinessException("请先确认揽收运费");
|
throw new BusinessException("请先确认揽收运费");
|
||||||
}
|
}
|
||||||
|
|
||||||
pickup.CarrierCode = carrier;
|
pickup.CarrierService = carrierService;
|
||||||
pickup.CarrierService = NormalizeOptional(input.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)
|
if (input.ExpectPickupTime.HasValue && input.ExpectPickupTime.Value != default)
|
||||||
{
|
{
|
||||||
pickup.ExpectPickupTime = input.ExpectPickupTime.Value;
|
pickup.ExpectPickupTime = input.ExpectPickupTime.Value;
|
||||||
@ -781,15 +888,26 @@ public class PickupOrderService : ApplicationService
|
|||||||
{
|
{
|
||||||
Id = entity.Id,
|
Id = entity.Id,
|
||||||
PickupNo = entity.PickupNo,
|
PickupNo = entity.PickupNo,
|
||||||
|
RequestId = entity.RequestId,
|
||||||
CustomerId = entity.CustomerId,
|
CustomerId = entity.CustomerId,
|
||||||
CustomerName = customerName,
|
CustomerName = customerName,
|
||||||
|
OrganizationId = entity.OrganizationId,
|
||||||
|
SecretLevel = entity.SecretLevel,
|
||||||
SenderAddressId = entity.SenderAddressId,
|
SenderAddressId = entity.SenderAddressId,
|
||||||
SenderName = entity.SenderName,
|
SenderName = entity.SenderName,
|
||||||
SenderPhone = entity.SenderPhone,
|
SenderPhone = entity.SenderPhone,
|
||||||
|
SenderTelePhone = entity.SenderTelePhone,
|
||||||
|
SenderEmail = entity.SenderEmail,
|
||||||
SenderCompany = entity.SenderCompany,
|
SenderCompany = entity.SenderCompany,
|
||||||
|
SenderCountry = entity.SenderCountry,
|
||||||
|
SenderCountryCode = entity.SenderCountryCode,
|
||||||
SenderProvince = entity.SenderProvince,
|
SenderProvince = entity.SenderProvince,
|
||||||
SenderCity = entity.SenderCity,
|
SenderCity = entity.SenderCity,
|
||||||
|
SenderZipCode = entity.SenderZipCode,
|
||||||
SenderStreetAddress1 = entity.SenderStreetAddress1,
|
SenderStreetAddress1 = entity.SenderStreetAddress1,
|
||||||
|
SenderStreetAddress2 = entity.SenderStreetAddress2,
|
||||||
|
SenderStreetAddress3 = entity.SenderStreetAddress3,
|
||||||
|
SenderHouseNumber = entity.SenderHouseNumber,
|
||||||
WarehouseId = entity.WarehouseId,
|
WarehouseId = entity.WarehouseId,
|
||||||
WarehouseName = warehouseName,
|
WarehouseName = warehouseName,
|
||||||
CarrierCode = entity.CarrierCode,
|
CarrierCode = entity.CarrierCode,
|
||||||
|
|||||||
@ -199,7 +199,6 @@ public static class OmsDocHelper
|
|||||||
{
|
{
|
||||||
OrderStatusCodes.Draft => "草稿",
|
OrderStatusCodes.Draft => "草稿",
|
||||||
OrderStatusCodes.Predeclared => "已预报",
|
OrderStatusCodes.Predeclared => "已预报",
|
||||||
OrderStatusCodes.Received => "已收货",
|
|
||||||
OrderStatusCodes.PendingAllocation => "待排货",
|
OrderStatusCodes.PendingAllocation => "待排货",
|
||||||
OrderStatusCodes.PendingPayment => "待支付",
|
OrderStatusCodes.PendingPayment => "待支付",
|
||||||
OrderStatusCodes.PendingShip => "待发货",
|
OrderStatusCodes.PendingShip => "待发货",
|
||||||
|
|||||||
@ -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<string> 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);
|
||||||
|
}
|
||||||
@ -17,5 +17,11 @@ public class IssueTemplate : OmsAuditedAggregateRoot
|
|||||||
|
|
||||||
public string NotifyMethod { get; set; } = string.Empty;
|
public string NotifyMethod { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>处理人类型:customer=订单所属客户,user=指定用户。</summary>
|
||||||
|
public string? HandlerType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>指定处理人用户 Id(HandlerType=user 时)。</summary>
|
||||||
|
public int? HandlerUserId { get; set; }
|
||||||
|
|
||||||
public EnableStatus Status { get; set; } = EnableStatus.Enabled;
|
public EnableStatus Status { get; set; } = EnableStatus.Enabled;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,6 @@ public static class OrderExceptionStatusRules
|
|||||||
{
|
{
|
||||||
OrderStatusCodes.Draft,
|
OrderStatusCodes.Draft,
|
||||||
OrderStatusCodes.Deleted,
|
OrderStatusCodes.Deleted,
|
||||||
OrderStatusCodes.Problematic,
|
|
||||||
OrderStatusCodes.Claimed
|
OrderStatusCodes.Claimed
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -18,7 +17,6 @@ public static class OrderExceptionStatusRules
|
|||||||
{
|
{
|
||||||
OrderStatusCodes.Draft => "草稿订单不可标记",
|
OrderStatusCodes.Draft => "草稿订单不可标记",
|
||||||
OrderStatusCodes.Deleted => "已删除订单不可标记",
|
OrderStatusCodes.Deleted => "已删除订单不可标记",
|
||||||
OrderStatusCodes.Problematic => "已是问题件",
|
|
||||||
OrderStatusCodes.Claimed => "已是理赔件",
|
OrderStatusCodes.Claimed => "已是理赔件",
|
||||||
_ => "订单状态不允许标记"
|
_ => "订单状态不允许标记"
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,7 +4,6 @@ public static class OrderStatusCodes
|
|||||||
{
|
{
|
||||||
public const string Draft = "draft";
|
public const string Draft = "draft";
|
||||||
public const string Predeclared = "predeclared";
|
public const string Predeclared = "predeclared";
|
||||||
public const string Received = "received";
|
|
||||||
public const string PendingAllocation = "pendingAllocation";
|
public const string PendingAllocation = "pendingAllocation";
|
||||||
public const string PendingPayment = "pendingPayment";
|
public const string PendingPayment = "pendingPayment";
|
||||||
public const string PendingShip = "pendingShip";
|
public const string PendingShip = "pendingShip";
|
||||||
|
|||||||
@ -28,6 +28,12 @@ public class OrderExceptionEvent : OmsAuditedAggregateRoot
|
|||||||
|
|
||||||
public string? NotifyMethod { get; set; }
|
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 string? ClaimType { get; set; }
|
||||||
|
|
||||||
public decimal? ClaimAmount { get; set; }
|
public decimal? ClaimAmount { get; set; }
|
||||||
|
|||||||
@ -12,19 +12,27 @@ public class PickupOrder : OmsAuditedAggregateRoot
|
|||||||
[SugarColumn(Length = 32)]
|
[SugarColumn(Length = 32)]
|
||||||
public string PickupNo { get; set; } = string.Empty;
|
public string PickupNo { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[SugarColumn(Length = 64, IsNullable = true)]
|
||||||
|
public string? RequestId { get; set; }
|
||||||
|
|
||||||
public int CustomerId { 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; }
|
public int SenderAddressId { get; set; }
|
||||||
|
|
||||||
[SugarColumn(Length = 50)]
|
[SugarColumn(Length = 50)]
|
||||||
public string SenderName { get; set; } = string.Empty;
|
public string SenderName { get; set; } = string.Empty;
|
||||||
|
|
||||||
[SugarColumn(Length = 32, IsNullable = true)]
|
|
||||||
public string? SenderPhone { get; set; }
|
|
||||||
|
|
||||||
[SugarColumn(Length = 255, IsNullable = true)]
|
[SugarColumn(Length = 255, IsNullable = true)]
|
||||||
public string? SenderCompany { get; set; }
|
public string? SenderCompany { get; set; }
|
||||||
|
|
||||||
|
[SugarColumn(IsNullable = true)]
|
||||||
|
public int? SenderCountry { get; set; }
|
||||||
|
|
||||||
[SugarColumn(Length = 8, IsNullable = true)]
|
[SugarColumn(Length = 8, IsNullable = true)]
|
||||||
public string? SenderCountryCode { get; set; }
|
public string? SenderCountryCode { get; set; }
|
||||||
|
|
||||||
@ -43,6 +51,21 @@ public class PickupOrder : OmsAuditedAggregateRoot
|
|||||||
[SugarColumn(Length = 500, IsNullable = true)]
|
[SugarColumn(Length = 500, IsNullable = true)]
|
||||||
public string? SenderStreetAddress2 { get; set; }
|
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; }
|
public int WarehouseId { get; set; }
|
||||||
|
|
||||||
[SugarColumn(Length = 64, IsNullable = true)]
|
[SugarColumn(Length = 64, IsNullable = true)]
|
||||||
|
|||||||
@ -13,5 +13,11 @@ public class PickupOrderLine : OmsAuditedAggregateRoot
|
|||||||
|
|
||||||
public int OrderId { get; set; }
|
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; }
|
public int SortNo { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user