diff --git a/db.sql b/db.sql
index 595997a..32e2179 100644
--- a/db.sql
+++ b/db.sql
@@ -1489,6 +1489,17 @@ CREATE TABLE `t_sales_store_order` (
`PlatformCode` VARCHAR(32) NOT NULL COMMENT '平台编码',
`PlatformOrderNo` VARCHAR(64) NOT NULL COMMENT '平台订单号',
`OrderCountry` VARCHAR(8) NULL COMMENT '订单国家',
+ `RecipientName` VARCHAR(64) NULL COMMENT '收件人姓名',
+ `RecipientCompany` VARCHAR(128) NULL COMMENT '收件人公司',
+ `RecipientPhone` VARCHAR(32) NULL COMMENT '收件人手机',
+ `RecipientTelePhone` VARCHAR(32) NULL COMMENT '收件人电话',
+ `RecipientEmail` VARCHAR(64) NULL COMMENT '收件人邮箱',
+ `RecipientCountry` VARCHAR(8) NULL COMMENT '收件国家码',
+ `RecipientProvince` VARCHAR(64) NULL COMMENT '收件省/州',
+ `RecipientCity` VARCHAR(64) NULL COMMENT '收件城市',
+ `RecipientZipCode` VARCHAR(32) NULL COMMENT '收件邮编',
+ `RecipientAddress1` VARCHAR(512) NULL COMMENT '收件地址1',
+ `RecipientAddress2` VARCHAR(512) NULL COMMENT '收件地址2',
`ItemCount` INT NULL COMMENT '商品行数',
`TotalAmount` DECIMAL(18,4) NULL COMMENT '订单总额',
`Currency` VARCHAR(8) NULL COMMENT '币种',
diff --git a/sql/fix_sales_store_concurrency_stamp.sql b/sql/fix_sales_store_concurrency_stamp.sql
new file mode 100644
index 0000000..f8b503b
--- /dev/null
+++ b/sql/fix_sales_store_concurrency_stamp.sql
@@ -0,0 +1,41 @@
+-- 修复店铺相关表 ConcurrencyStamp 无法绑定到 Guid 的问题
+-- 典型报错:ConcurrencyStamp绑定到SalesStoreOrder失败 / SalesStoreOrder ConcurrencyStamp bind error
+--
+-- 原因:补列时若写入空串 '',SqlSugar 无法把 '' 转成 System.Guid
+-- 用法:mysql -u root -p oms < sql/fix_sales_store_concurrency_stamp.sql
+
+SET NAMES utf8mb4;
+
+UPDATE `t_sales_store`
+SET `ConcurrencyStamp` = UUID()
+WHERE `ConcurrencyStamp` IS NULL
+ OR TRIM(`ConcurrencyStamp`) = ''
+ OR LENGTH(TRIM(`ConcurrencyStamp`)) <> 36;
+
+UPDATE `t_sales_store_order`
+SET `ConcurrencyStamp` = UUID()
+WHERE `ConcurrencyStamp` IS NULL
+ OR TRIM(`ConcurrencyStamp`) = ''
+ OR LENGTH(TRIM(`ConcurrencyStamp`)) <> 36;
+
+UPDATE `t_sales_store_order_line`
+SET `ConcurrencyStamp` = UUID()
+WHERE `ConcurrencyStamp` IS NULL
+ OR TRIM(`ConcurrencyStamp`) = ''
+ OR LENGTH(TRIM(`ConcurrencyStamp`)) <> 36;
+
+-- 抽查
+SELECT 't_sales_store' AS tbl,
+ SUM(CASE WHEN TRIM(IFNULL(ConcurrencyStamp,'')) = '' OR LENGTH(TRIM(ConcurrencyStamp)) <> 36 THEN 1 ELSE 0 END) AS bad_cnt,
+ COUNT(*) AS total
+FROM `t_sales_store`
+UNION ALL
+SELECT 't_sales_store_order',
+ SUM(CASE WHEN TRIM(IFNULL(ConcurrencyStamp,'')) = '' OR LENGTH(TRIM(ConcurrencyStamp)) <> 36 THEN 1 ELSE 0 END),
+ COUNT(*)
+FROM `t_sales_store_order`
+UNION ALL
+SELECT 't_sales_store_order_line',
+ SUM(CASE WHEN TRIM(IFNULL(ConcurrencyStamp,'')) = '' OR LENGTH(TRIM(ConcurrencyStamp)) <> 36 THEN 1 ELSE 0 END),
+ COUNT(*)
+FROM `t_sales_store_order_line`;
diff --git a/sql/migrate_t_sales_store_order_recipient.sql b/sql/migrate_t_sales_store_order_recipient.sql
new file mode 100644
index 0000000..588025f
--- /dev/null
+++ b/sql/migrate_t_sales_store_order_recipient.sql
@@ -0,0 +1,32 @@
+-- 店铺订单补收件人字段(可重复执行)
+-- 用法:mysql -u root -p oms < sql/migrate_t_sales_store_order_recipient.sql
+
+SET NAMES utf8mb4;
+
+DROP PROCEDURE IF EXISTS sp_add_store_order_recipient_cols;
+DELIMITER $$
+CREATE PROCEDURE sp_add_store_order_recipient_cols()
+BEGIN
+ DECLARE col_exists INT DEFAULT 0;
+
+ SELECT COUNT(*) INTO col_exists FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't_sales_store_order' AND COLUMN_NAME = 'RecipientName';
+ IF col_exists = 0 THEN
+ ALTER TABLE `t_sales_store_order`
+ ADD COLUMN `RecipientName` VARCHAR(64) NULL COMMENT '收件人姓名' AFTER `OrderCountry`,
+ ADD COLUMN `RecipientCompany` VARCHAR(128) NULL COMMENT '收件人公司' AFTER `RecipientName`,
+ ADD COLUMN `RecipientPhone` VARCHAR(32) NULL COMMENT '收件人手机' AFTER `RecipientCompany`,
+ ADD COLUMN `RecipientTelePhone` VARCHAR(32) NULL COMMENT '收件人电话' AFTER `RecipientPhone`,
+ ADD COLUMN `RecipientEmail` VARCHAR(64) NULL COMMENT '收件人邮箱' AFTER `RecipientTelePhone`,
+ ADD COLUMN `RecipientCountry` VARCHAR(8) NULL COMMENT '收件国家码' AFTER `RecipientEmail`,
+ ADD COLUMN `RecipientProvince` VARCHAR(64) NULL COMMENT '收件省/州' AFTER `RecipientCountry`,
+ ADD COLUMN `RecipientCity` VARCHAR(64) NULL COMMENT '收件城市' AFTER `RecipientProvince`,
+ ADD COLUMN `RecipientZipCode` VARCHAR(32) NULL COMMENT '收件邮编' AFTER `RecipientCity`,
+ ADD COLUMN `RecipientAddress1` VARCHAR(512) NULL COMMENT '收件地址1' AFTER `RecipientZipCode`,
+ ADD COLUMN `RecipientAddress2` VARCHAR(512) NULL COMMENT '收件地址2' AFTER `RecipientAddress1`;
+ END IF;
+END$$
+DELIMITER ;
+
+CALL sp_add_store_order_recipient_cols();
+DROP PROCEDURE IF EXISTS sp_add_store_order_recipient_cols;
diff --git a/sql/seed_pickup_order_customer9.sql b/sql/seed_pickup_order_customer9.sql
new file mode 100644
index 0000000..07388df
--- /dev/null
+++ b/sql/seed_pickup_order_customer9.sql
@@ -0,0 +1,306 @@
+-- 揽收订单种子(CustomerId = 9,OrderSource = pickup)
+-- 可重复执行:已存在则跳过
+-- 用法:mysql -u root -p oms < sql/seed_pickup_order_customer9.sql
+--
+-- 依赖:t_customer.Id=9 存在;仓库/销售产品尽量取库内已有,否则用占位编码
+
+SET NAMES utf8mb4;
+
+SET @customerId := 9;
+
+-- ---------------------------------------------------------------------------
+-- 0. 修复 t_order 脏 ConcurrencyStamp(空串无法绑 Guid)
+-- ---------------------------------------------------------------------------
+UPDATE `t_order`
+SET `ConcurrencyStamp` = UUID()
+WHERE `ConcurrencyStamp` IS NULL
+ OR TRIM(`ConcurrencyStamp`) = ''
+ OR LENGTH(TRIM(`ConcurrencyStamp`)) <> 36;
+
+-- ---------------------------------------------------------------------------
+-- 1. 发件地址簿(揽收创建依赖)
+-- ---------------------------------------------------------------------------
+INSERT INTO `t_customer_sender_address` (
+ `CustomerId`, `AddressType`, `AddressName`, `SenderName`, `Company`,
+ `Province`, `City`, `ZipCode`, `StreetAddress1`, `Phone`,
+ `IsDefault`, `IsDeleted`, `ConcurrencyStamp`, `CreateUserId`, `CreateTime`
+)
+SELECT @customerId, 'sender', '种子-默认发件', '张三', '种子发件公司',
+ '广东省', '深圳市', '518000', '南山区科技园种子路 1 号', '13800000009',
+ 1, 0, UUID(), 0, NOW()
+WHERE NOT EXISTS (
+ SELECT 1 FROM `t_customer_sender_address`
+ WHERE `CustomerId` = @customerId
+ AND `AddressType` = 'sender'
+ AND `AddressName` = '种子-默认发件'
+ AND `IsDeleted` = 0
+);
+
+SET @senderAddressId := (
+ SELECT `Id` FROM `t_customer_sender_address`
+ WHERE `CustomerId` = @customerId
+ AND `AddressType` = 'sender'
+ AND `AddressName` = '种子-默认发件'
+ AND `IsDeleted` = 0
+ ORDER BY `Id`
+ LIMIT 1
+);
+
+-- 仓库:优先取已有;没有则插入一条种子仓
+INSERT INTO `t_warehouse_address` (
+ `WarehouseName`, `WarehouseType`, `Recipient`, `ContactPhone`, `ShippingAddress`,
+ `IsDeleted`, `ConcurrencyStamp`, `CreateUserId`, `CreateTime`
+)
+SELECT '种子-深圳仓', 'main', '仓管员', '0755-80000009', '广东省深圳市南山区仓库大道 9 号',
+ 0, UUID(), 0, NOW()
+WHERE NOT EXISTS (
+ SELECT 1 FROM `t_warehouse_address` WHERE `IsDeleted` = 0 LIMIT 1
+)
+AND NOT EXISTS (
+ SELECT 1 FROM `t_warehouse_address` WHERE `WarehouseName` = '种子-深圳仓' AND `IsDeleted` = 0
+);
+
+SET @warehouseId := (
+ SELECT `Id` FROM `t_warehouse_address`
+ WHERE `IsDeleted` = 0
+ ORDER BY CASE WHEN `WarehouseName` = '种子-深圳仓' THEN 0 ELSE 1 END, `Id`
+ LIMIT 1
+);
+
+SET @productCode := (
+ SELECT `ProductCode` FROM `t_sales_product`
+ WHERE `IsDeleted` = 0
+ ORDER BY `Id`
+ LIMIT 1
+);
+SET @productCode := IFNULL(NULLIF(TRIM(@productCode), ''), 'SEED-PICKUP-PRODUCT');
+
+-- ---------------------------------------------------------------------------
+-- 2. 揽收订单 t_order(若干状态)
+-- ---------------------------------------------------------------------------
+
+-- draft
+INSERT INTO `t_order` (
+ `RequestId`, `ServiceNo`, `ClientNo`, `ProductCode`, `ChannelCode`,
+ `ConsigneeCountry`, `ConsigneeCountryName`, `CargoAttribute`,
+ `OrderStatus`, `HandleStatus`, `WarehouseId`, `CustomerId`,
+ `OrderSource`, `PickupTime`, `CustomerSenderAddressId`,
+ `DeclaredCurrency`, `DeclaredSum`, `Quantity`,
+ `IsDeleted`, `ConcurrencyStamp`, `CreateUserId`, `CreateTime`
+)
+SELECT
+ 'SEED9-PK-REQ-001', 'SEED9-PK-001', 'PK-CLIENT-001', @productCode, @productCode,
+ 'CN', '中国', 'general',
+ 'draft', 'pending', @warehouseId, @customerId,
+ 'pickup', DATE_ADD(NOW(), INTERVAL 1 DAY), @senderAddressId,
+ 'CNY', 1.0000, 1,
+ 0, UUID(), 0, DATE_SUB(NOW(), INTERVAL 2 DAY)
+WHERE @warehouseId IS NOT NULL
+ AND NOT EXISTS (SELECT 1 FROM `t_order` WHERE `ServiceNo` = 'SEED9-PK-001');
+
+-- pendingPayment
+INSERT INTO `t_order` (
+ `RequestId`, `ServiceNo`, `ClientNo`, `ProductCode`, `ChannelCode`,
+ `ConsigneeCountry`, `ConsigneeCountryName`, `CargoAttribute`,
+ `OrderStatus`, `HandleStatus`, `WarehouseId`, `CustomerId`,
+ `OrderSource`, `PickupTime`, `CustomerSenderAddressId`,
+ `DeclaredCurrency`, `DeclaredSum`, `Quantity`,
+ `IsDeleted`, `ConcurrencyStamp`, `CreateUserId`, `CreateTime`
+)
+SELECT
+ 'SEED9-PK-REQ-002', 'SEED9-PK-002', 'PK-CLIENT-002', @productCode, @productCode,
+ 'CN', '中国', 'general',
+ 'pendingPayment', 'pending', @warehouseId, @customerId,
+ 'pickup', DATE_ADD(NOW(), INTERVAL 2 DAY), @senderAddressId,
+ 'CNY', 1.0000, 1,
+ 0, UUID(), 0, DATE_SUB(NOW(), INTERVAL 1 DAY)
+WHERE @warehouseId IS NOT NULL
+ AND NOT EXISTS (SELECT 1 FROM `t_order` WHERE `ServiceNo` = 'SEED9-PK-002');
+
+-- pendingShip
+INSERT INTO `t_order` (
+ `RequestId`, `ServiceNo`, `ClientNo`, `ProductCode`, `ChannelCode`,
+ `ConsigneeCountry`, `ConsigneeCountryName`, `CargoAttribute`,
+ `OrderStatus`, `HandleStatus`, `WarehouseId`, `CustomerId`,
+ `OrderSource`, `PickupTime`, `CustomerSenderAddressId`,
+ `DeclaredCurrency`, `DeclaredSum`, `Quantity`,
+ `IsPaid`, `PayTime`,
+ `IsDeleted`, `ConcurrencyStamp`, `CreateUserId`, `CreateTime`
+)
+SELECT
+ 'SEED9-PK-REQ-003', 'SEED9-PK-003', 'PK-CLIENT-003', @productCode, @productCode,
+ 'CN', '中国', 'electronics',
+ 'pendingShip', 'pending', @warehouseId, @customerId,
+ 'pickup', DATE_ADD(NOW(), INTERVAL 3 DAY), @senderAddressId,
+ 'CNY', 1.0000, 1,
+ 1, DATE_SUB(NOW(), INTERVAL 6 HOUR),
+ 0, UUID(), 0, DATE_SUB(NOW(), INTERVAL 12 HOUR)
+WHERE @warehouseId IS NOT NULL
+ AND NOT EXISTS (SELECT 1 FROM `t_order` WHERE `ServiceNo` = 'SEED9-PK-003');
+
+-- received
+INSERT INTO `t_order` (
+ `RequestId`, `ServiceNo`, `ClientNo`, `ProductCode`, `ChannelCode`,
+ `ConsigneeCountry`, `ConsigneeCountryName`, `CargoAttribute`,
+ `OrderStatus`, `HandleStatus`, `WarehouseId`, `CustomerId`,
+ `OrderSource`, `PickupTime`, `CustomerSenderAddressId`,
+ `DeclaredCurrency`, `DeclaredSum`, `Quantity`,
+ `ReceiveTime`,
+ `IsDeleted`, `ConcurrencyStamp`, `CreateUserId`, `CreateTime`
+)
+SELECT
+ 'SEED9-PK-REQ-004', 'SEED9-PK-004', 'PK-CLIENT-004', @productCode, @productCode,
+ 'CN', '中国', 'general',
+ 'received', 'processed', @warehouseId, @customerId,
+ 'pickup', DATE_SUB(NOW(), INTERVAL 1 DAY), @senderAddressId,
+ 'CNY', 1.0000, 1,
+ DATE_SUB(NOW(), INTERVAL 8 HOUR),
+ 0, UUID(), 0, DATE_SUB(NOW(), INTERVAL 3 DAY)
+WHERE @warehouseId IS NOT NULL
+ AND NOT EXISTS (SELECT 1 FROM `t_order` WHERE `ServiceNo` = 'SEED9-PK-004');
+
+-- pendingOnline
+INSERT INTO `t_order` (
+ `RequestId`, `ServiceNo`, `ClientNo`, `ProductCode`, `ChannelCode`,
+ `ConsigneeCountry`, `ConsigneeCountryName`, `CargoAttribute`,
+ `OrderStatus`, `HandleStatus`, `WarehouseId`, `CustomerId`,
+ `OrderSource`, `PickupTime`, `CustomerSenderAddressId`,
+ `DeclaredCurrency`, `DeclaredSum`, `Quantity`,
+ `IsPaid`, `PayTime`, `ShipTime`,
+ `IsDeleted`, `ConcurrencyStamp`, `CreateUserId`, `CreateTime`
+)
+SELECT
+ 'SEED9-PK-REQ-005', 'SEED9-PK-005', 'PK-CLIENT-005', @productCode, @productCode,
+ 'CN', '中国', 'general',
+ 'pendingOnline', 'processed', @warehouseId, @customerId,
+ 'pickup', DATE_SUB(NOW(), INTERVAL 2 DAY), @senderAddressId,
+ 'CNY', 1.0000, 1,
+ 1, DATE_SUB(NOW(), INTERVAL 2 DAY), DATE_SUB(NOW(), INTERVAL 1 DAY),
+ 0, UUID(), 0, DATE_SUB(NOW(), INTERVAL 4 DAY)
+WHERE @warehouseId IS NOT NULL
+ AND NOT EXISTS (SELECT 1 FROM `t_order` WHERE `ServiceNo` = 'SEED9-PK-005');
+
+-- ---------------------------------------------------------------------------
+-- 3. 子表:发件人 / 收件人 / 申报 / 件重尺
+-- ---------------------------------------------------------------------------
+
+-- 发件人
+INSERT INTO `t_order_sender` (
+ `OrderId`, `SenderName`, `Company`, `Province`, `City`, `ZipCode`,
+ `StreetAddress1`, `Phone`, `IsDeleted`, `CreateUserId`, `CreateTime`
+)
+SELECT o.`Id`, '张三', '种子发件公司', '广东省', '深圳市', '518000',
+ '南山区科技园种子路 1 号', '13800000009', 0, 0, NOW()
+FROM `t_order` o
+WHERE o.`ServiceNo` IN ('SEED9-PK-001','SEED9-PK-002','SEED9-PK-003','SEED9-PK-004','SEED9-PK-005')
+ AND o.`CustomerId` = @customerId
+ AND o.`OrderSource` = 'pickup'
+ AND NOT EXISTS (SELECT 1 FROM `t_order_sender` s WHERE s.`OrderId` = o.`Id` AND s.`IsDeleted` = 0);
+
+-- 收件人(仓库)
+INSERT INTO `t_order_recipient` (
+ `OrderId`, `RecipientsName`, `Phone`, `ZipCode`, `StreetAddress1`,
+ `IsDeleted`, `CreateUserId`, `CreateTime`
+)
+SELECT o.`Id`,
+ IFNULL(NULLIF(TRIM(w.`Recipient`), ''), IFNULL(w.`WarehouseName`, '种子仓')),
+ w.`ContactPhone`,
+ '000000',
+ IFNULL(NULLIF(TRIM(w.`ShippingAddress`), ''), IFNULL(w.`WarehouseName`, '种子仓地址')),
+ 0, 0, NOW()
+FROM `t_order` o
+LEFT JOIN `t_warehouse_address` w ON w.`Id` = o.`WarehouseId`
+WHERE o.`ServiceNo` IN ('SEED9-PK-001','SEED9-PK-002','SEED9-PK-003','SEED9-PK-004','SEED9-PK-005')
+ AND o.`CustomerId` = @customerId
+ AND o.`OrderSource` = 'pickup'
+ AND NOT EXISTS (SELECT 1 FROM `t_order_recipient` r WHERE r.`OrderId` = o.`Id` AND r.`IsDeleted` = 0);
+
+-- 申报行(货物名称)
+INSERT INTO `t_order_declaration` (
+ `OrderId`, `CnName`, `EnName`, `UnitPrice`, `UnitWeight`, `Quantity`, `Amount`, `Weight`,
+ `IsDeleted`, `CreateUserId`, `CreateTime`
+)
+SELECT o.`Id`,
+ CASE o.`ServiceNo`
+ WHEN 'SEED9-PK-001' THEN '日用百货样品'
+ WHEN 'SEED9-PK-002' THEN '服装配件'
+ WHEN 'SEED9-PK-003' THEN '蓝牙耳机'
+ WHEN 'SEED9-PK-004' THEN '家居收纳盒'
+ ELSE '办公文具套装'
+ END,
+ CASE o.`ServiceNo`
+ WHEN 'SEED9-PK-001' THEN 'Daily Sample Goods'
+ WHEN 'SEED9-PK-002' THEN 'Apparel Accessories'
+ WHEN 'SEED9-PK-003' THEN 'Bluetooth Earbuds'
+ WHEN 'SEED9-PK-004' THEN 'Storage Box'
+ ELSE 'Stationery Set'
+ END,
+ 1.00,
+ CASE o.`ServiceNo`
+ WHEN 'SEED9-PK-001' THEN 0.500
+ WHEN 'SEED9-PK-002' THEN 1.200
+ WHEN 'SEED9-PK-003' THEN 0.350
+ WHEN 'SEED9-PK-004' THEN 2.000
+ ELSE 0.800
+ END,
+ 1,
+ 1.00,
+ CASE o.`ServiceNo`
+ WHEN 'SEED9-PK-001' THEN 0.500
+ WHEN 'SEED9-PK-002' THEN 1.200
+ WHEN 'SEED9-PK-003' THEN 0.350
+ WHEN 'SEED9-PK-004' THEN 2.000
+ ELSE 0.800
+ END,
+ 0, 0, NOW()
+FROM `t_order` o
+WHERE o.`ServiceNo` IN ('SEED9-PK-001','SEED9-PK-002','SEED9-PK-003','SEED9-PK-004','SEED9-PK-005')
+ AND o.`CustomerId` = @customerId
+ AND o.`OrderSource` = 'pickup'
+ AND NOT EXISTS (SELECT 1 FROM `t_order_declaration` d WHERE d.`OrderId` = o.`Id` AND d.`IsDeleted` = 0);
+
+-- 预报件重尺
+INSERT INTO `t_order_weight` (
+ `OrderId`, `Type`, `Length`, `Width`, `Height`, `ActualWeight`, `VolumetricWeight`, `ChargeableWeight`,
+ `IsDeleted`, `CreateUserId`, `CreateTime`
+)
+SELECT o.`Id`, 'forecast', 1.00, 1.00, 1.00,
+ CASE o.`ServiceNo`
+ WHEN 'SEED9-PK-001' THEN 0.500
+ WHEN 'SEED9-PK-002' THEN 1.200
+ WHEN 'SEED9-PK-003' THEN 0.350
+ WHEN 'SEED9-PK-004' THEN 2.000
+ ELSE 0.800
+ END,
+ 0.001,
+ CASE o.`ServiceNo`
+ WHEN 'SEED9-PK-001' THEN 0.500
+ WHEN 'SEED9-PK-002' THEN 1.200
+ WHEN 'SEED9-PK-003' THEN 0.350
+ WHEN 'SEED9-PK-004' THEN 2.000
+ ELSE 0.800
+ END,
+ 0, 0, NOW()
+FROM `t_order` o
+WHERE o.`ServiceNo` IN ('SEED9-PK-001','SEED9-PK-002','SEED9-PK-003','SEED9-PK-004','SEED9-PK-005')
+ AND o.`CustomerId` = @customerId
+ AND o.`OrderSource` = 'pickup'
+ AND NOT EXISTS (
+ SELECT 1 FROM `t_order_weight` w
+ WHERE w.`OrderId` = o.`Id` AND w.`Type` = 'forecast' AND w.`IsDeleted` = 0
+ );
+
+-- ---------------------------------------------------------------------------
+-- 4. 摘要
+-- ---------------------------------------------------------------------------
+SELECT 'pickup_orders' AS tbl, COUNT(*) AS cnt
+FROM `t_order`
+WHERE `CustomerId` = @customerId AND `OrderSource` = 'pickup' AND `IsDeleted` = 0
+UNION ALL
+SELECT 'sender_address', COUNT(*)
+FROM `t_customer_sender_address`
+WHERE `CustomerId` = @customerId AND `AddressType` = 'sender' AND `IsDeleted` = 0
+UNION ALL
+SELECT 'warehouse_used', IF(@warehouseId IS NULL, 0, 1)
+UNION ALL
+SELECT 'product_code', IF(@productCode IS NULL OR @productCode = '', 0, 1);
diff --git a/sql/seed_sales_store_customer9.sql b/sql/seed_sales_store_customer9.sql
new file mode 100644
index 0000000..58b9257
--- /dev/null
+++ b/sql/seed_sales_store_customer9.sql
@@ -0,0 +1,510 @@
+-- 店铺授权 + 店铺订单种子(CustomerId = 9)
+-- 可重复执行:已存在则跳过
+-- 用法:
+-- 1) mysql -u root -p oms < sql/migrate_t_sales_store_order_recipient.sql
+-- 2) mysql -u root -p oms < sql/seed_sales_store_customer9.sql
+--
+-- 注意:ConcurrencyStamp 必须是合法 Guid 字符串;空串会导致
+-- 「ConcurrencyStamp绑定到SalesStoreOrder失败」
+
+SET NAMES utf8mb4;
+
+-- 先修好已有脏数据(空串 / 非 36 位)
+UPDATE `t_sales_store`
+SET `ConcurrencyStamp` = UUID()
+WHERE `ConcurrencyStamp` IS NULL OR TRIM(`ConcurrencyStamp`) = '' OR LENGTH(TRIM(`ConcurrencyStamp`)) <> 36;
+UPDATE `t_sales_store_order`
+SET `ConcurrencyStamp` = UUID()
+WHERE `ConcurrencyStamp` IS NULL OR TRIM(`ConcurrencyStamp`) = '' OR LENGTH(TRIM(`ConcurrencyStamp`)) <> 36;
+UPDATE `t_sales_store_order_line`
+SET `ConcurrencyStamp` = UUID()
+WHERE `ConcurrencyStamp` IS NULL OR TRIM(`ConcurrencyStamp`) = '' OR LENGTH(TRIM(`ConcurrencyStamp`)) <> 36;
+
+-- ---------------------------------------------------------------------------
+-- 1. 店铺授权 t_sales_store(4 个平台,覆盖 active/expired)
+-- ---------------------------------------------------------------------------
+INSERT INTO `t_sales_store` (
+ `CustomerId`, `PlatformCode`, `StoreNickname`, `PlatformStoreId`, `PlatformAccount`,
+ `SiteCountry`, `SellerId`, `AuthStatus`, `AuthTime`, `TokenExpireTime`, `RefreshExpireTime`,
+ `AccessToken`, `RefreshToken`, `IsAutoFulfill`, `Remark`, `IsDeleted`, `ConcurrencyStamp`, `CreateUserId`, `CreateTime`
+)
+SELECT 9, 'shopee', '种子-Shopee旗舰店', 'SEED9-SHOPEE-001', 'seed9.shopee@demo.com',
+ 'SG', 'SELLER-SHOPEE-9001', 'active',
+ DATE_SUB(NOW(), INTERVAL 30 DAY), DATE_ADD(NOW(), INTERVAL 60 DAY), DATE_ADD(NOW(), INTERVAL 180 DAY),
+ 'seed_access_token_shopee_9', 'seed_refresh_token_shopee_9', 1, 'CustomerId=9 种子店铺', 0, UUID(), 0, NOW()
+WHERE NOT EXISTS (
+ SELECT 1 FROM `t_sales_store`
+ WHERE `CustomerId` = 9 AND `PlatformStoreId` = 'SEED9-SHOPEE-001' AND `IsDeleted` = 0
+);
+
+INSERT INTO `t_sales_store` (
+ `CustomerId`, `PlatformCode`, `StoreNickname`, `PlatformStoreId`, `PlatformAccount`,
+ `SiteCountry`, `SellerId`, `AuthStatus`, `AuthTime`, `TokenExpireTime`, `RefreshExpireTime`,
+ `AccessToken`, `RefreshToken`, `IsAutoFulfill`, `Remark`, `IsDeleted`, `ConcurrencyStamp`, `CreateUserId`, `CreateTime`
+)
+SELECT 9, 'lazada', '种子-Lazada马来店', 'SEED9-LAZADA-001', 'seed9.lazada@demo.com',
+ 'MY', 'SELLER-LAZADA-9002', 'active',
+ DATE_SUB(NOW(), INTERVAL 14 DAY), DATE_ADD(NOW(), INTERVAL 45 DAY), DATE_ADD(NOW(), INTERVAL 120 DAY),
+ 'seed_access_token_lazada_9', 'seed_refresh_token_lazada_9', 0, 'CustomerId=9 种子店铺', 0, UUID(), 0, NOW()
+WHERE NOT EXISTS (
+ SELECT 1 FROM `t_sales_store`
+ WHERE `CustomerId` = 9 AND `PlatformStoreId` = 'SEED9-LAZADA-001' AND `IsDeleted` = 0
+);
+
+INSERT INTO `t_sales_store` (
+ `CustomerId`, `PlatformCode`, `StoreNickname`, `PlatformStoreId`, `PlatformAccount`,
+ `SiteCountry`, `SellerId`, `AuthStatus`, `AuthTime`, `TokenExpireTime`, `RefreshExpireTime`,
+ `AccessToken`, `RefreshToken`, `IsAutoFulfill`, `Remark`, `IsDeleted`, `ConcurrencyStamp`, `CreateUserId`, `CreateTime`
+)
+SELECT 9, 'tiktok', '种子-TikTok跨境店', 'SEED9-TIKTOK-001', 'seed9.tiktok@demo.com',
+ 'US', 'SELLER-TIKTOK-9003', 'active',
+ DATE_SUB(NOW(), INTERVAL 7 DAY), DATE_ADD(NOW(), INTERVAL 30 DAY), DATE_ADD(NOW(), INTERVAL 90 DAY),
+ 'seed_access_token_tiktok_9', 'seed_refresh_token_tiktok_9', 1, 'CustomerId=9 种子店铺', 0, UUID(), 0, NOW()
+WHERE NOT EXISTS (
+ SELECT 1 FROM `t_sales_store`
+ WHERE `CustomerId` = 9 AND `PlatformStoreId` = 'SEED9-TIKTOK-001' AND `IsDeleted` = 0
+);
+
+INSERT INTO `t_sales_store` (
+ `CustomerId`, `PlatformCode`, `StoreNickname`, `PlatformStoreId`, `PlatformAccount`,
+ `SiteCountry`, `SellerId`, `AuthStatus`, `AuthTime`, `TokenExpireTime`, `RefreshExpireTime`,
+ `AccessToken`, `RefreshToken`, `IsAutoFulfill`, `Remark`, `IsDeleted`, `ConcurrencyStamp`, `CreateUserId`, `CreateTime`
+)
+SELECT 9, 'temu', '种子-Temu美区店(已过期)', 'SEED9-TEMU-001', 'seed9.temu@demo.com',
+ 'US', 'SELLER-TEMU-9004', 'expired',
+ DATE_SUB(NOW(), INTERVAL 90 DAY), DATE_SUB(NOW(), INTERVAL 5 DAY), DATE_SUB(NOW(), INTERVAL 1 DAY),
+ 'seed_access_token_temu_9_expired', 'seed_refresh_token_temu_9_expired', 0, 'CustomerId=9 种子店铺-授权过期', 0, UUID(), 0, NOW()
+WHERE NOT EXISTS (
+ SELECT 1 FROM `t_sales_store`
+ WHERE `CustomerId` = 9 AND `PlatformStoreId` = 'SEED9-TEMU-001' AND `IsDeleted` = 0
+);
+
+-- ---------------------------------------------------------------------------
+-- 2. 店铺订单 t_sales_store_order(覆盖各履约状态)
+-- ---------------------------------------------------------------------------
+
+-- pending
+INSERT INTO `t_sales_store_order` (
+ `CustomerId`, `StoreId`, `PlatformCode`, `PlatformOrderNo`, `OrderCountry`,
+ `ItemCount`, `TotalAmount`, `Currency`, `PlatformOrderTime`, `FulfillStatus`,
+ `PlatformStatus`, `PlatformStatusLabel`, `IsMultiLabel`, `IsSuspended`,
+ `LastSyncTime`, `IsDeleted`, `CreateUserId`, `CreateTime`
+)
+SELECT 9, s.`Id`, 'shopee', 'SEED9-ORD-SHOPEE-P001', 'SG',
+ 2, 45.8000, 'SGD', DATE_SUB(NOW(), INTERVAL 2 HOUR), 'pending',
+ 'UNPAID', '待付款', 0, 0,
+ NOW(), 0, 0, NOW()
+FROM `t_sales_store` s
+WHERE s.`CustomerId` = 9 AND s.`PlatformStoreId` = 'SEED9-SHOPEE-001' AND s.`IsDeleted` = 0
+ AND NOT EXISTS (
+ SELECT 1 FROM `t_sales_store_order` o
+ WHERE o.`StoreId` = s.`Id` AND o.`PlatformOrderNo` = 'SEED9-ORD-SHOPEE-P001' AND o.`IsDeleted` = 0
+ );
+
+-- toShip
+INSERT INTO `t_sales_store_order` (
+ `CustomerId`, `StoreId`, `PlatformCode`, `PlatformOrderNo`, `OrderCountry`,
+ `ItemCount`, `TotalAmount`, `Currency`, `PlatformOrderTime`, `FulfillStatus`,
+ `PlatformStatus`, `PlatformStatusLabel`, `IsMultiLabel`, `CustomTags`, `IsSuspended`,
+ `LastSyncTime`, `IsDeleted`, `CreateUserId`, `CreateTime`
+)
+SELECT 9, s.`Id`, 'shopee', 'SEED9-ORD-SHOPEE-T001', 'SG',
+ 1, 28.5000, 'SGD', DATE_SUB(NOW(), INTERVAL 1 DAY), 'toShip',
+ 'READY_TO_SHIP', '待发货', 0, '急单', 0,
+ NOW(), 0, 0, NOW()
+FROM `t_sales_store` s
+WHERE s.`CustomerId` = 9 AND s.`PlatformStoreId` = 'SEED9-SHOPEE-001' AND s.`IsDeleted` = 0
+ AND NOT EXISTS (
+ SELECT 1 FROM `t_sales_store_order` o
+ WHERE o.`StoreId` = s.`Id` AND o.`PlatformOrderNo` = 'SEED9-ORD-SHOPEE-T001' AND o.`IsDeleted` = 0
+ );
+
+-- shipped
+INSERT INTO `t_sales_store_order` (
+ `CustomerId`, `StoreId`, `PlatformCode`, `PlatformOrderNo`, `OrderCountry`,
+ `ItemCount`, `TotalAmount`, `Currency`, `PlatformOrderTime`, `FulfillStatus`,
+ `PlatformStatus`, `PlatformStatusLabel`, `ShippingLabelNo`, `ShippingLabelUrl`,
+ `IsMultiLabel`, `IsSuspended`, `LastSyncTime`, `IsDeleted`, `CreateUserId`, `CreateTime`
+)
+SELECT 9, s.`Id`, 'lazada', 'SEED9-ORD-LAZADA-S001', 'MY',
+ 3, 129.9000, 'MYR', DATE_SUB(NOW(), INTERVAL 3 DAY), 'shipped',
+ 'SHIPPED', '已发货', 'LZD-LABEL-90001', 'https://example.com/labels/lzd-90001.pdf',
+ 0, 0, NOW(), 0, 0, NOW()
+FROM `t_sales_store` s
+WHERE s.`CustomerId` = 9 AND s.`PlatformStoreId` = 'SEED9-LAZADA-001' AND s.`IsDeleted` = 0
+ AND NOT EXISTS (
+ SELECT 1 FROM `t_sales_store_order` o
+ WHERE o.`StoreId` = s.`Id` AND o.`PlatformOrderNo` = 'SEED9-ORD-LAZADA-S001' AND o.`IsDeleted` = 0
+ );
+
+-- completed
+INSERT INTO `t_sales_store_order` (
+ `CustomerId`, `StoreId`, `PlatformCode`, `PlatformOrderNo`, `OrderCountry`,
+ `ItemCount`, `TotalAmount`, `Currency`, `PlatformOrderTime`, `FulfillStatus`,
+ `PlatformStatus`, `PlatformStatusLabel`, `ShippingLabelNo`,
+ `IsMultiLabel`, `IsSuspended`, `LastSyncTime`, `IsDeleted`, `CreateUserId`, `CreateTime`
+)
+SELECT 9, s.`Id`, 'tiktok', 'SEED9-ORD-TIKTOK-C001', 'US',
+ 1, 19.9900, 'USD', DATE_SUB(NOW(), INTERVAL 10 DAY), 'completed',
+ 'COMPLETED', '已完成', 'TT-LABEL-90002',
+ 0, 0, NOW(), 0, 0, NOW()
+FROM `t_sales_store` s
+WHERE s.`CustomerId` = 9 AND s.`PlatformStoreId` = 'SEED9-TIKTOK-001' AND s.`IsDeleted` = 0
+ AND NOT EXISTS (
+ SELECT 1 FROM `t_sales_store_order` o
+ WHERE o.`StoreId` = s.`Id` AND o.`PlatformOrderNo` = 'SEED9-ORD-TIKTOK-C001' AND o.`IsDeleted` = 0
+ );
+
+-- canceled
+INSERT INTO `t_sales_store_order` (
+ `CustomerId`, `StoreId`, `PlatformCode`, `PlatformOrderNo`, `OrderCountry`,
+ `ItemCount`, `TotalAmount`, `Currency`, `PlatformOrderTime`, `FulfillStatus`,
+ `PlatformStatus`, `PlatformStatusLabel`, `IsMultiLabel`, `IsSuspended`,
+ `LastSyncTime`, `IsDeleted`, `CreateUserId`, `CreateTime`
+)
+SELECT 9, s.`Id`, 'tiktok', 'SEED9-ORD-TIKTOK-X001', 'US',
+ 2, 35.5000, 'USD', DATE_SUB(NOW(), INTERVAL 5 DAY), 'canceled',
+ 'CANCELLED', '已取消', 0, 0,
+ NOW(), 0, 0, NOW()
+FROM `t_sales_store` s
+WHERE s.`CustomerId` = 9 AND s.`PlatformStoreId` = 'SEED9-TIKTOK-001' AND s.`IsDeleted` = 0
+ AND NOT EXISTS (
+ SELECT 1 FROM `t_sales_store_order` o
+ WHERE o.`StoreId` = s.`Id` AND o.`PlatformOrderNo` = 'SEED9-ORD-TIKTOK-X001' AND o.`IsDeleted` = 0
+ );
+
+-- suspended
+INSERT INTO `t_sales_store_order` (
+ `CustomerId`, `StoreId`, `PlatformCode`, `PlatformOrderNo`, `OrderCountry`,
+ `ItemCount`, `TotalAmount`, `Currency`, `PlatformOrderTime`, `FulfillStatus`,
+ `PlatformStatus`, `PlatformStatusLabel`, `IsMultiLabel`, `CustomTags`, `IsSuspended`,
+ `LastSyncTime`, `IsDeleted`, `CreateUserId`, `CreateTime`
+)
+SELECT 9, s.`Id`, 'lazada', 'SEED9-ORD-LAZADA-H001', 'MY',
+ 1, 59.0000, 'MYR', DATE_SUB(NOW(), INTERVAL 4 DAY), 'suspended',
+ 'PENDING', '待处理', 0, '缺货搁置', 1,
+ NOW(), 0, 0, NOW()
+FROM `t_sales_store` s
+WHERE s.`CustomerId` = 9 AND s.`PlatformStoreId` = 'SEED9-LAZADA-001' AND s.`IsDeleted` = 0
+ AND NOT EXISTS (
+ SELECT 1 FROM `t_sales_store_order` o
+ WHERE o.`StoreId` = s.`Id` AND o.`PlatformOrderNo` = 'SEED9-ORD-LAZADA-H001' AND o.`IsDeleted` = 0
+ );
+
+-- toShip(多件)
+INSERT INTO `t_sales_store_order` (
+ `CustomerId`, `StoreId`, `PlatformCode`, `PlatformOrderNo`, `OrderCountry`,
+ `ItemCount`, `TotalAmount`, `Currency`, `PlatformOrderTime`, `FulfillStatus`,
+ `PlatformStatus`, `PlatformStatusLabel`, `IsMultiLabel`, `IsSuspended`,
+ `LastSyncTime`, `IsDeleted`, `CreateUserId`, `CreateTime`
+)
+SELECT 9, s.`Id`, 'shopee', 'SEED9-ORD-SHOPEE-T002', 'PH',
+ 2, 88.0000, 'PHP', DATE_SUB(NOW(), INTERVAL 6 HOUR), 'toShip',
+ 'READY_TO_SHIP', '待发货', 1, 0,
+ NOW(), 0, 0, NOW()
+FROM `t_sales_store` s
+WHERE s.`CustomerId` = 9 AND s.`PlatformStoreId` = 'SEED9-SHOPEE-001' AND s.`IsDeleted` = 0
+ AND NOT EXISTS (
+ SELECT 1 FROM `t_sales_store_order` o
+ WHERE o.`StoreId` = s.`Id` AND o.`PlatformOrderNo` = 'SEED9-ORD-SHOPEE-T002' AND o.`IsDeleted` = 0
+ );
+
+-- ---------------------------------------------------------------------------
+-- 3. 订单明细 t_sales_store_order_line
+-- ---------------------------------------------------------------------------
+
+-- SEED9-ORD-SHOPEE-P001 两行
+INSERT INTO `t_sales_store_order_line` (
+ `StoreOrderId`, `PlatformLineId`, `PlatformSku`, `ProductName`, `ProductImageUrl`,
+ `VariantText`, `UnitPrice`, `Quantity`, `Currency`, `IsShipExcluded`, `IsDeleted`, `CreateUserId`, `CreateTime`
+)
+SELECT o.`Id`, 'LINE-P001-1', 'SKU-PHONE-CASE-01', '硅胶手机壳', 'https://example.com/img/phone-case.jpg',
+ '颜色:黑色;型号:iPhone15', 18.9000, 1, 'SGD', 0, 0, 0, NOW()
+FROM `t_sales_store_order` o
+WHERE o.`CustomerId` = 9 AND o.`PlatformOrderNo` = 'SEED9-ORD-SHOPEE-P001' AND o.`IsDeleted` = 0
+ AND NOT EXISTS (
+ SELECT 1 FROM `t_sales_store_order_line` l
+ WHERE l.`StoreOrderId` = o.`Id` AND l.`PlatformLineId` = 'LINE-P001-1' AND l.`IsDeleted` = 0
+ );
+
+INSERT INTO `t_sales_store_order_line` (
+ `StoreOrderId`, `PlatformLineId`, `PlatformSku`, `ProductName`, `ProductImageUrl`,
+ `VariantText`, `UnitPrice`, `Quantity`, `Currency`, `IsShipExcluded`, `IsDeleted`, `CreateUserId`, `CreateTime`
+)
+SELECT o.`Id`, 'LINE-P001-2', 'SKU-CABLE-USB-C', 'USB-C 数据线 1m', 'https://example.com/img/usb-c.jpg',
+ '长度:1m', 26.9000, 1, 'SGD', 0, 0, 0, NOW()
+FROM `t_sales_store_order` o
+WHERE o.`CustomerId` = 9 AND o.`PlatformOrderNo` = 'SEED9-ORD-SHOPEE-P001' AND o.`IsDeleted` = 0
+ AND NOT EXISTS (
+ SELECT 1 FROM `t_sales_store_order_line` l
+ WHERE l.`StoreOrderId` = o.`Id` AND l.`PlatformLineId` = 'LINE-P001-2' AND l.`IsDeleted` = 0
+ );
+
+-- SEED9-ORD-SHOPEE-T001
+INSERT INTO `t_sales_store_order_line` (
+ `StoreOrderId`, `PlatformLineId`, `PlatformSku`, `ProductName`, `ProductImageUrl`,
+ `VariantText`, `UnitPrice`, `Quantity`, `Currency`, `IsShipExcluded`, `IsDeleted`, `CreateUserId`, `CreateTime`
+)
+SELECT o.`Id`, 'LINE-T001-1', 'SKU-EARBUD-PRO', '无线蓝牙耳机 Pro', 'https://example.com/img/earbud.jpg',
+ '颜色:白色', 28.5000, 1, 'SGD', 0, 0, 0, NOW()
+FROM `t_sales_store_order` o
+WHERE o.`CustomerId` = 9 AND o.`PlatformOrderNo` = 'SEED9-ORD-SHOPEE-T001' AND o.`IsDeleted` = 0
+ AND NOT EXISTS (
+ SELECT 1 FROM `t_sales_store_order_line` l
+ WHERE l.`StoreOrderId` = o.`Id` AND l.`PlatformLineId` = 'LINE-T001-1' AND l.`IsDeleted` = 0
+ );
+
+-- SEED9-ORD-LAZADA-S001 三行
+INSERT INTO `t_sales_store_order_line` (
+ `StoreOrderId`, `PlatformLineId`, `PlatformSku`, `ProductName`, `ProductImageUrl`,
+ `VariantText`, `UnitPrice`, `Quantity`, `Currency`, `IsShipExcluded`, `IsDeleted`, `CreateUserId`, `CreateTime`
+)
+SELECT o.`Id`, 'LINE-S001-1', 'SKU-TSHIRT-M', '纯棉短袖 T 恤', 'https://example.com/img/tshirt.jpg',
+ '尺码:M;颜色:藏青', 29.9000, 2, 'MYR', 0, 0, 0, NOW()
+FROM `t_sales_store_order` o
+WHERE o.`CustomerId` = 9 AND o.`PlatformOrderNo` = 'SEED9-ORD-LAZADA-S001' AND o.`IsDeleted` = 0
+ AND NOT EXISTS (
+ SELECT 1 FROM `t_sales_store_order_line` l
+ WHERE l.`StoreOrderId` = o.`Id` AND l.`PlatformLineId` = 'LINE-S001-1' AND l.`IsDeleted` = 0
+ );
+
+INSERT INTO `t_sales_store_order_line` (
+ `StoreOrderId`, `PlatformLineId`, `PlatformSku`, `ProductName`, `ProductImageUrl`,
+ `VariantText`, `UnitPrice`, `Quantity`, `Currency`, `IsShipExcluded`, `IsDeleted`, `CreateUserId`, `CreateTime`
+)
+SELECT o.`Id`, 'LINE-S001-2', 'SKU-SOCK-PACK', '运动短袜 3 双装', 'https://example.com/img/socks.jpg',
+ '颜色:混色', 19.9000, 1, 'MYR', 0, 0, 0, NOW()
+FROM `t_sales_store_order` o
+WHERE o.`CustomerId` = 9 AND o.`PlatformOrderNo` = 'SEED9-ORD-LAZADA-S001' AND o.`IsDeleted` = 0
+ AND NOT EXISTS (
+ SELECT 1 FROM `t_sales_store_order_line` l
+ WHERE l.`StoreOrderId` = o.`Id` AND l.`PlatformLineId` = 'LINE-S001-2' AND l.`IsDeleted` = 0
+ );
+
+INSERT INTO `t_sales_store_order_line` (
+ `StoreOrderId`, `PlatformLineId`, `PlatformSku`, `ProductName`, `ProductImageUrl`,
+ `VariantText`, `UnitPrice`, `Quantity`, `Currency`, `IsShipExcluded`, `IsDeleted`, `CreateUserId`, `CreateTime`
+)
+SELECT o.`Id`, 'LINE-S001-3', 'SKU-CAP-BLK', '棒球帽', 'https://example.com/img/cap.jpg',
+ '颜色:黑色', 50.2000, 1, 'MYR', 0, 0, 0, NOW()
+FROM `t_sales_store_order` o
+WHERE o.`CustomerId` = 9 AND o.`PlatformOrderNo` = 'SEED9-ORD-LAZADA-S001' AND o.`IsDeleted` = 0
+ AND NOT EXISTS (
+ SELECT 1 FROM `t_sales_store_order_line` l
+ WHERE l.`StoreOrderId` = o.`Id` AND l.`PlatformLineId` = 'LINE-S001-3' AND l.`IsDeleted` = 0
+ );
+
+-- SEED9-ORD-TIKTOK-C001
+INSERT INTO `t_sales_store_order_line` (
+ `StoreOrderId`, `PlatformLineId`, `PlatformSku`, `ProductName`, `ProductImageUrl`,
+ `VariantText`, `UnitPrice`, `Quantity`, `Currency`, `IsShipExcluded`, `IsDeleted`, `CreateUserId`, `CreateTime`
+)
+SELECT o.`Id`, 'LINE-C001-1', 'SKU-LED-STRIP', 'LED 灯带 5m', 'https://example.com/img/led.jpg',
+ '色温:暖白', 19.9900, 1, 'USD', 0, 0, 0, NOW()
+FROM `t_sales_store_order` o
+WHERE o.`CustomerId` = 9 AND o.`PlatformOrderNo` = 'SEED9-ORD-TIKTOK-C001' AND o.`IsDeleted` = 0
+ AND NOT EXISTS (
+ SELECT 1 FROM `t_sales_store_order_line` l
+ WHERE l.`StoreOrderId` = o.`Id` AND l.`PlatformLineId` = 'LINE-C001-1' AND l.`IsDeleted` = 0
+ );
+
+-- SEED9-ORD-TIKTOK-X001
+INSERT INTO `t_sales_store_order_line` (
+ `StoreOrderId`, `PlatformLineId`, `PlatformSku`, `ProductName`, `ProductImageUrl`,
+ `VariantText`, `UnitPrice`, `Quantity`, `Currency`, `IsShipExcluded`, `IsDeleted`, `CreateUserId`, `CreateTime`
+)
+SELECT o.`Id`, 'LINE-X001-1', 'SKU-MOUSE-PAD', '加大鼠标垫', 'https://example.com/img/mousepad.jpg',
+ '尺寸:900x400', 15.5000, 1, 'USD', 0, 0, 0, NOW()
+FROM `t_sales_store_order` o
+WHERE o.`CustomerId` = 9 AND o.`PlatformOrderNo` = 'SEED9-ORD-TIKTOK-X001' AND o.`IsDeleted` = 0
+ AND NOT EXISTS (
+ SELECT 1 FROM `t_sales_store_order_line` l
+ WHERE l.`StoreOrderId` = o.`Id` AND l.`PlatformLineId` = 'LINE-X001-1' AND l.`IsDeleted` = 0
+ );
+
+INSERT INTO `t_sales_store_order_line` (
+ `StoreOrderId`, `PlatformLineId`, `PlatformSku`, `ProductName`, `ProductImageUrl`,
+ `VariantText`, `UnitPrice`, `Quantity`, `Currency`, `IsShipExcluded`, `IsDeleted`, `CreateUserId`, `CreateTime`
+)
+SELECT o.`Id`, 'LINE-X001-2', 'SKU-KEYCAP-SET', '键帽套装 104 键', 'https://example.com/img/keycap.jpg',
+ '风格:布丁', 20.0000, 1, 'USD', 1, 0, 0, NOW()
+FROM `t_sales_store_order` o
+WHERE o.`CustomerId` = 9 AND o.`PlatformOrderNo` = 'SEED9-ORD-TIKTOK-X001' AND o.`IsDeleted` = 0
+ AND NOT EXISTS (
+ SELECT 1 FROM `t_sales_store_order_line` l
+ WHERE l.`StoreOrderId` = o.`Id` AND l.`PlatformLineId` = 'LINE-X001-2' AND l.`IsDeleted` = 0
+ );
+
+-- SEED9-ORD-LAZADA-H001
+INSERT INTO `t_sales_store_order_line` (
+ `StoreOrderId`, `PlatformLineId`, `PlatformSku`, `ProductName`, `ProductImageUrl`,
+ `VariantText`, `UnitPrice`, `Quantity`, `Currency`, `IsShipExcluded`, `IsDeleted`, `CreateUserId`, `CreateTime`
+)
+SELECT o.`Id`, 'LINE-H001-1', 'SKU-POWERBANK-20K', '移动电源 20000mAh', 'https://example.com/img/powerbank.jpg',
+ '颜色:灰色', 59.0000, 1, 'MYR', 0, 0, 0, NOW()
+FROM `t_sales_store_order` o
+WHERE o.`CustomerId` = 9 AND o.`PlatformOrderNo` = 'SEED9-ORD-LAZADA-H001' AND o.`IsDeleted` = 0
+ AND NOT EXISTS (
+ SELECT 1 FROM `t_sales_store_order_line` l
+ WHERE l.`StoreOrderId` = o.`Id` AND l.`PlatformLineId` = 'LINE-H001-1' AND l.`IsDeleted` = 0
+ );
+
+-- SEED9-ORD-SHOPEE-T002 两行
+INSERT INTO `t_sales_store_order_line` (
+ `StoreOrderId`, `PlatformLineId`, `PlatformSku`, `ProductName`, `ProductImageUrl`,
+ `VariantText`, `UnitPrice`, `Quantity`, `Currency`, `IsShipExcluded`, `IsDeleted`, `CreateUserId`, `CreateTime`
+)
+SELECT o.`Id`, 'LINE-T002-1', 'SKU-BAG-TOTE', '帆布托特包', 'https://example.com/img/tote.jpg',
+ '颜色:米白', 48.0000, 1, 'PHP', 0, 0, 0, NOW()
+FROM `t_sales_store_order` o
+WHERE o.`CustomerId` = 9 AND o.`PlatformOrderNo` = 'SEED9-ORD-SHOPEE-T002' AND o.`IsDeleted` = 0
+ AND NOT EXISTS (
+ SELECT 1 FROM `t_sales_store_order_line` l
+ WHERE l.`StoreOrderId` = o.`Id` AND l.`PlatformLineId` = 'LINE-T002-1' AND l.`IsDeleted` = 0
+ );
+
+INSERT INTO `t_sales_store_order_line` (
+ `StoreOrderId`, `PlatformLineId`, `PlatformSku`, `ProductName`, `ProductImageUrl`,
+ `VariantText`, `UnitPrice`, `Quantity`, `Currency`, `IsShipExcluded`, `IsDeleted`, `CreateUserId`, `CreateTime`
+)
+SELECT o.`Id`, 'LINE-T002-2', 'SKU-WALLET-MINI', '迷你卡包', 'https://example.com/img/wallet.jpg',
+ '颜色:棕色', 40.0000, 1, 'PHP', 0, 0, 0, NOW()
+FROM `t_sales_store_order` o
+WHERE o.`CustomerId` = 9 AND o.`PlatformOrderNo` = 'SEED9-ORD-SHOPEE-T002' AND o.`IsDeleted` = 0
+ AND NOT EXISTS (
+ SELECT 1 FROM `t_sales_store_order_line` l
+ WHERE l.`StoreOrderId` = o.`Id` AND l.`PlatformLineId` = 'LINE-T002-2' AND l.`IsDeleted` = 0
+ );
+
+-- ---------------------------------------------------------------------------
+-- 4. 补齐收件人(需先执行 migrate_t_sales_store_order_recipient.sql)
+-- ---------------------------------------------------------------------------
+UPDATE `t_sales_store_order` SET
+ `RecipientName` = 'Tan Wei Ming',
+ `RecipientCompany` = NULL,
+ `RecipientPhone` = '+65-91234567',
+ `RecipientEmail` = 'tan.wm@example.com',
+ `RecipientCountry` = 'SG',
+ `RecipientProvince` = 'Singapore',
+ `RecipientCity` = 'Singapore',
+ `RecipientZipCode` = '018956',
+ `RecipientAddress1` = '10 Bayfront Avenue, Marina Bay Sands',
+ `RecipientAddress2` = 'Tower 1 #12-01',
+ `PlatformExtraJson` = JSON_OBJECT(
+ 'recipient', JSON_OBJECT(
+ 'recipientsName', 'Tan Wei Ming',
+ 'phone', '+65-91234567',
+ 'email', 'tan.wm@example.com',
+ 'countryCode', 'SG',
+ 'province', 'Singapore',
+ 'city', 'Singapore',
+ 'zipCode', '018956',
+ 'streetAddress1', '10 Bayfront Avenue, Marina Bay Sands',
+ 'streetAddress2', 'Tower 1 #12-01'
+ )
+ )
+WHERE `CustomerId` = 9 AND `PlatformOrderNo` = 'SEED9-ORD-SHOPEE-P001' AND `IsDeleted` = 0
+ AND (`RecipientName` IS NULL OR `RecipientName` = '');
+
+UPDATE `t_sales_store_order` SET
+ `RecipientName` = 'Lim Jia Hui',
+ `RecipientPhone` = '+65-98887766',
+ `RecipientCountry` = 'SG',
+ `RecipientProvince` = 'Singapore',
+ `RecipientCity` = 'Singapore',
+ `RecipientZipCode` = '238801',
+ `RecipientAddress1` = '313 Orchard Road',
+ `PlatformExtraJson` = JSON_OBJECT(
+ 'recipient', JSON_OBJECT(
+ 'recipientsName', 'Lim Jia Hui',
+ 'phone', '+65-98887766',
+ 'countryCode', 'SG',
+ 'city', 'Singapore',
+ 'zipCode', '238801',
+ 'streetAddress1', '313 Orchard Road'
+ )
+ )
+WHERE `CustomerId` = 9 AND `PlatformOrderNo` = 'SEED9-ORD-SHOPEE-T001' AND `IsDeleted` = 0
+ AND (`RecipientName` IS NULL OR `RecipientName` = '');
+
+UPDATE `t_sales_store_order` SET
+ `RecipientName` = 'Ahmad Faizal',
+ `RecipientPhone` = '+60-12-3456789',
+ `RecipientCountry` = 'MY',
+ `RecipientProvince` = 'Selangor',
+ `RecipientCity` = 'Petaling Jaya',
+ `RecipientZipCode` = '47800',
+ `RecipientAddress1` = 'No. 18, Jalan SS2/24',
+ `PlatformExtraJson` = JSON_OBJECT(
+ 'recipient', JSON_OBJECT(
+ 'recipientsName', 'Ahmad Faizal',
+ 'phone', '+60-12-3456789',
+ 'countryCode', 'MY',
+ 'province', 'Selangor',
+ 'city', 'Petaling Jaya',
+ 'zipCode', '47800',
+ 'streetAddress1', 'No. 18, Jalan SS2/24'
+ )
+ )
+WHERE `CustomerId` = 9 AND `PlatformOrderNo` IN ('SEED9-ORD-LAZADA-S001', 'SEED9-ORD-LAZADA-H001') AND `IsDeleted` = 0
+ AND (`RecipientName` IS NULL OR `RecipientName` = '');
+
+UPDATE `t_sales_store_order` SET
+ `RecipientName` = 'John Smith',
+ `RecipientPhone` = '+1-415-555-0198',
+ `RecipientEmail` = 'john.smith@example.com',
+ `RecipientCountry` = 'US',
+ `RecipientProvince` = 'CA',
+ `RecipientCity` = 'San Francisco',
+ `RecipientZipCode` = '94105',
+ `RecipientAddress1` = '1 Market St, Suite 300',
+ `PlatformExtraJson` = JSON_OBJECT(
+ 'recipient', JSON_OBJECT(
+ 'recipientsName', 'John Smith',
+ 'phone', '+1-415-555-0198',
+ 'email', 'john.smith@example.com',
+ 'countryCode', 'US',
+ 'province', 'CA',
+ 'city', 'San Francisco',
+ 'zipCode', '94105',
+ 'streetAddress1', '1 Market St, Suite 300'
+ )
+ )
+WHERE `CustomerId` = 9 AND `PlatformOrderNo` IN ('SEED9-ORD-TIKTOK-C001', 'SEED9-ORD-TIKTOK-X001') AND `IsDeleted` = 0
+ AND (`RecipientName` IS NULL OR `RecipientName` = '');
+
+UPDATE `t_sales_store_order` SET
+ `RecipientName` = 'Maria Santos',
+ `RecipientPhone` = '+63-917-123-4567',
+ `RecipientCountry` = 'PH',
+ `RecipientProvince` = 'Metro Manila',
+ `RecipientCity` = 'Makati',
+ `RecipientZipCode` = '1226',
+ `RecipientAddress1` = 'Ayala Avenue, Glorietta 4',
+ `PlatformExtraJson` = JSON_OBJECT(
+ 'recipient', JSON_OBJECT(
+ 'recipientsName', 'Maria Santos',
+ 'phone', '+63-917-123-4567',
+ 'countryCode', 'PH',
+ 'province', 'Metro Manila',
+ 'city', 'Makati',
+ 'zipCode', '1226',
+ 'streetAddress1', 'Ayala Avenue, Glorietta 4'
+ )
+ )
+WHERE `CustomerId` = 9 AND `PlatformOrderNo` = 'SEED9-ORD-SHOPEE-T002' AND `IsDeleted` = 0
+ AND (`RecipientName` IS NULL OR `RecipientName` = '');
+
+-- 校验摘要(执行后可看)
+SELECT 't_sales_store' AS tbl, COUNT(*) AS cnt FROM `t_sales_store` WHERE `CustomerId` = 9 AND `IsDeleted` = 0
+UNION ALL
+SELECT 't_sales_store_order', COUNT(*) FROM `t_sales_store_order` WHERE `CustomerId` = 9 AND `IsDeleted` = 0
+UNION ALL
+SELECT 't_sales_store_order_with_recipient', COUNT(*)
+FROM `t_sales_store_order`
+WHERE `CustomerId` = 9 AND `IsDeleted` = 0 AND `RecipientName` IS NOT NULL AND `RecipientName` <> ''
+UNION ALL
+SELECT 't_sales_store_order_line', COUNT(*)
+FROM `t_sales_store_order_line` l
+INNER JOIN `t_sales_store_order` o ON o.`Id` = l.`StoreOrderId`
+WHERE o.`CustomerId` = 9 AND o.`IsDeleted` = 0 AND l.`IsDeleted` = 0;
diff --git a/src/Oms.Application/Dtos/Orders/OrderStockBindDtos.cs b/src/Oms.Application/Dtos/Orders/OrderStockBindDtos.cs
index 0d02cde..6c7d390 100644
--- a/src/Oms.Application/Dtos/Orders/OrderStockBindDtos.cs
+++ b/src/Oms.Application/Dtos/Orders/OrderStockBindDtos.cs
@@ -55,11 +55,16 @@ public class OrderStockBindItemOutput
public class OrderStockBindCandidatesInput
{
- [Required]
+ ///
+ /// 物流单 Id。创建前草稿选仓时可传 0,此时须传 ,客户取会话。
+ ///
public int OrderId { get; set; }
public int? WarehouseId { get; set; }
+ /// 草稿模式(无物流单)时可选,管理端指定客户。
+ public int? CustomerId { get; set; }
+
public string? Keyword { get; set; }
/// 包裹候选:空/all=全部生命周期。
diff --git a/src/Oms.Application/Dtos/PlatformStore/PlatformStoreDtos.cs b/src/Oms.Application/Dtos/PlatformStore/PlatformStoreDtos.cs
index 2cc2f38..36baf5b 100644
--- a/src/Oms.Application/Dtos/PlatformStore/PlatformStoreDtos.cs
+++ b/src/Oms.Application/Dtos/PlatformStore/PlatformStoreDtos.cs
@@ -204,6 +204,28 @@ public class PlatformStoreOrderOutput
public string? OrderCountry { get; set; }
+ public string? RecipientName { get; set; }
+
+ public string? RecipientCompany { get; set; }
+
+ public string? RecipientPhone { get; set; }
+
+ public string? RecipientTelePhone { get; set; }
+
+ public string? RecipientEmail { get; set; }
+
+ public string? RecipientCountry { get; set; }
+
+ public string? RecipientProvince { get; set; }
+
+ public string? RecipientCity { get; set; }
+
+ public string? RecipientZipCode { get; set; }
+
+ public string? RecipientAddress1 { get; set; }
+
+ public string? RecipientAddress2 { get; set; }
+
public int? ItemCount { get; set; }
public decimal? TotalAmount { get; set; }
@@ -333,6 +355,54 @@ public class PlatformStoreOrderMatchInventoryOutput
public string? ClientNo { get; set; }
}
+public class PlatformStoreOrderBatchCreateLogisticsInput
+{
+ [Required]
+ public string ProductCode { get; set; } = string.Empty;
+
+ public int? WarehouseId { get; set; }
+
+ public int? CustomerId { get; set; }
+
+ [Required]
+ public List Items { get; set; } = [];
+}
+
+public class PlatformStoreOrderBatchCreateFulfillInput
+{
+ [Required]
+ public string ProductCode { get; set; } = string.Empty;
+
+ [Required]
+ public int WarehouseId { get; set; }
+
+ public int? CustomerId { get; set; }
+
+ [Required]
+ public List Items { get; set; } = [];
+}
+
+public class PlatformStoreOrderBatchCreateItemInput
+{
+ [Required]
+ public int StoreOrderId { get; set; }
+
+ public OrderParcelReceiverInput? Receiver { get; set; }
+
+ public List BindItems { get; set; } = [];
+}
+
+public class PlatformStoreOrderBatchCreateLogisticsOutput
+{
+ public string BatchNo { get; set; } = string.Empty;
+
+ public int SuccessCount { get; set; }
+
+ public int FailCount { get; set; }
+
+ public List Items { get; set; } = [];
+}
+
public class PlatformStoreOrderToggleSuspendInput
{
[Required]
diff --git a/src/Oms.Application/Services/Orders/OrderService.BatchCreate.cs b/src/Oms.Application/Services/Orders/OrderService.BatchCreate.cs
index 1059450..510e863 100644
--- a/src/Oms.Application/Services/Orders/OrderService.BatchCreate.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.BatchCreate.cs
@@ -78,6 +78,24 @@ public partial class OrderService
int operatorUserId,
CancellationToken cancellationToken = default)
{
- return await CreateParcelCoreAsync(input, operatorUserId);
+ return await CreateParcelForBatchAsync(input, operatorUserId, orderSource: null, cancellationToken);
+ }
+
+ ///
+ /// 批量 Worker 创建小包订单(可指定 )。
+ ///
+ public async Task CreateParcelForBatchAsync(
+ OrderParcelCreateInput input,
+ int operatorUserId,
+ string? orderSource,
+ CancellationToken cancellationToken = default)
+ {
+ ParcelCreateContext? createContext = null;
+ if (!string.IsNullOrWhiteSpace(orderSource))
+ {
+ createContext = new ParcelCreateContext { OrderSource = orderSource.Trim() };
+ }
+
+ return await CreateParcelCoreAsync(input, operatorUserId, createContext);
}
}
diff --git a/src/Oms.Application/Services/Orders/OrderService.StockBind.cs b/src/Oms.Application/Services/Orders/OrderService.StockBind.cs
index f67978f..50e475c 100644
--- a/src/Oms.Application/Services/Orders/OrderService.StockBind.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.StockBind.cs
@@ -49,10 +49,7 @@ public partial class OrderService
public async Task> GetStockBindInventoryCandidatesAsync(
[FromBody] OrderStockBindCandidatesInput input)
{
- var order = await GetOrderOrThrowAsync(input.OrderId);
- EnsureOrderCanBindStock(order);
- var warehouseId = ResolveBindWarehouseId(order, input.WarehouseId);
- var customerId = await ResolveStockBindCustomerIdAsync(order);
+ var (warehouseId, customerId) = await ResolveCandidatesContextAsync(input);
var inventories = await StockBindInventoryRepository.GetListAsync(x =>
x.CustomerId == customerId
@@ -108,10 +105,7 @@ public partial class OrderService
public async Task> GetStockBindParcelCandidatesAsync(
[FromBody] OrderStockBindCandidatesInput input)
{
- var order = await GetOrderOrThrowAsync(input.OrderId);
- EnsureOrderCanBindStock(order);
- var warehouseId = ResolveBindWarehouseId(order, input.WarehouseId);
- var customerId = await ResolveStockBindCustomerIdAsync(order);
+ var (warehouseId, customerId) = await ResolveCandidatesContextAsync(input);
var lifecycle = input.LifecycleStatus?.Trim();
var filterLifecycle = !string.IsNullOrWhiteSpace(lifecycle)
@@ -631,6 +625,34 @@ public partial class OrderService
}
}
+ private async Task<(int WarehouseId, int CustomerId)> ResolveCandidatesContextAsync(OrderStockBindCandidatesInput input)
+ {
+ if (input.OrderId > 0)
+ {
+ var order = await GetOrderOrThrowAsync(input.OrderId);
+ EnsureOrderCanBindStock(order);
+ var warehouseId = ResolveBindWarehouseId(order, input.WarehouseId);
+ var customerId = await ResolveStockBindCustomerIdAsync(order);
+ return (warehouseId, customerId);
+ }
+
+ if (input.WarehouseId is not > 0)
+ {
+ throw new BusinessException("请指定发货仓库");
+ }
+
+ var sessionCustomerId = await OmsSessionHelper.ResolveListCustomerIdAsync(
+ CustomerRepository,
+ CurrentUser,
+ input.CustomerId);
+ if (sessionCustomerId is not > 0)
+ {
+ throw new BusinessException("请指定客户");
+ }
+
+ return (input.WarehouseId.Value, sessionCustomerId.Value);
+ }
+
private static int RequireOrderCustomerId(Order order)
{
if (!order.CustomerId.HasValue || order.CustomerId.Value <= 0)
diff --git a/src/Oms.Application/Services/Orders/PlatformStoreOrderFulfillmentService.cs b/src/Oms.Application/Services/Orders/PlatformStoreOrderFulfillmentService.cs
index 4d9931f..e8e06de 100644
--- a/src/Oms.Application/Services/Orders/PlatformStoreOrderFulfillmentService.cs
+++ b/src/Oms.Application/Services/Orders/PlatformStoreOrderFulfillmentService.cs
@@ -318,6 +318,7 @@ internal sealed class PlatformStoreOrderFulfillmentService(
entity.ShippingLabelUrl = pulled.ShippingLabelUrl ?? entity.ShippingLabelUrl;
entity.PlatformExtraJson = pulled.ExtraJson ?? entity.PlatformExtraJson;
entity.LastSyncTime = now;
+ PlatformStoreOrderParcelBuilder.TryFillRecipientFields(entity, pulled.ExtraJson);
if (entity.Id > 0)
{
diff --git a/src/Oms.Application/Services/Orders/PlatformStoreOrderParcelBuilder.cs b/src/Oms.Application/Services/Orders/PlatformStoreOrderParcelBuilder.cs
index e912d37..c4828d8 100644
--- a/src/Oms.Application/Services/Orders/PlatformStoreOrderParcelBuilder.cs
+++ b/src/Oms.Application/Services/Orders/PlatformStoreOrderParcelBuilder.cs
@@ -59,6 +59,99 @@ public static class PlatformStoreOrderParcelBuilder
};
}
+ ///
+ /// 按平台商品行组装小包入参(不要求库内 SKU 映射;用于创建物流 / 物流&代发)。
+ ///
+ public static OrderParcelCreateInput BuildFromPlatformLines(
+ SalesStoreOrder storeOrder,
+ IReadOnlyCollection lines,
+ WarehouseAddress? warehouse,
+ string productCode,
+ int? warehouseId,
+ int customerId,
+ OrderParcelReceiverInput? receiverOverride = null,
+ string? trackNoOverride = null)
+ {
+ var receiver = receiverOverride ?? ParseReceiver(storeOrder);
+ ValidateReceiver(receiver);
+
+ var declarationLines = BuildDeclarationLinesFromPlatform(lines);
+ var totalWeightKg = Math.Max(
+ declarationLines.Sum(x => x.Weight ?? 0m),
+ MinActualWeightKg);
+
+ return new OrderParcelCreateInput
+ {
+ RequestId = $"platform-store-order:{storeOrder.Id}",
+ ClientNo = storeOrder.PlatformOrderNo,
+ TrackNo = string.IsNullOrWhiteSpace(trackNoOverride) ? storeOrder.ShippingLabelNo : trackNoOverride?.Trim(),
+ ProductCode = productCode,
+ WarehouseId = warehouseId,
+ CustomerId = customerId,
+ Length = DefaultLengthCm,
+ Width = DefaultWidthCm,
+ Height = DefaultHeightCm,
+ ActualWeight = totalWeightKg,
+ ChargeableWeight = totalWeightKg,
+ CargoAttribute = CargoAttributeCodes.General,
+ DeclaredCurrency = string.IsNullOrWhiteSpace(storeOrder.Currency) ? "USD" : storeOrder.Currency.Trim(),
+ DeclaredSum = storeOrder.TotalAmount,
+ Quantity = 1,
+ Receiver = receiver,
+ Sender = warehouse != null ? BuildSender(warehouse) : BuildFallbackSender(storeOrder),
+ Items = declarationLines
+ };
+ }
+
+ private static List BuildDeclarationLinesFromPlatform(
+ IReadOnlyCollection lines)
+ {
+ var items = new List();
+ foreach (var line in lines.Where(x => !x.IsShipExcluded))
+ {
+ var cnName = string.IsNullOrWhiteSpace(line.ProductName)
+ ? (string.IsNullOrWhiteSpace(line.PlatformSku) ? "Platform Item" : line.PlatformSku.Trim())
+ : line.ProductName.Trim();
+ var sku = string.IsNullOrWhiteSpace(line.PlatformSku)
+ ? (cnName.Length > 64 ? cnName[..64] : cnName)
+ : line.PlatformSku.Trim();
+ var unitPrice = line.UnitPrice is > 0 ? line.UnitPrice.Value : 1m;
+ var qty = Math.Max(1, line.Quantity);
+
+ items.Add(new OrderParcelItemInput
+ {
+ CnName = cnName,
+ EnName = cnName,
+ Sku = sku,
+ Unit = "PCS",
+ UnitPrice = unitPrice,
+ UnitWeight = MinActualWeightKg,
+ Quantity = qty,
+ Amount = unitPrice * qty,
+ Weight = MinActualWeightKg * qty
+ });
+ }
+
+ if (items.Count == 0)
+ {
+ throw new BusinessException("没有可申报的商品行");
+ }
+
+ return items;
+ }
+
+ private static OrderParcelSenderInput BuildFallbackSender(SalesStoreOrder storeOrder)
+ {
+ return new OrderParcelSenderInput
+ {
+ SenderName = string.IsNullOrWhiteSpace(storeOrder.PlatformOrderNo)
+ ? "Platform Seller"
+ : $"Store#{storeOrder.StoreId}",
+ StreetAddress1 = "TBD",
+ CountryCode = storeOrder.OrderCountry
+ };
+ }
+
private static List BuildDeclarationLines(
IReadOnlyCollection lines,
IReadOnlyDictionary skuMap)
@@ -155,6 +248,27 @@ public static class PlatformStoreOrderParcelBuilder
private static OrderParcelReceiverInput? ParseReceiver(SalesStoreOrder storeOrder)
{
+ if (!string.IsNullOrWhiteSpace(storeOrder.RecipientName)
+ && !string.IsNullOrWhiteSpace(storeOrder.RecipientCountry ?? storeOrder.OrderCountry)
+ && !string.IsNullOrWhiteSpace(storeOrder.RecipientZipCode)
+ && !string.IsNullOrWhiteSpace(storeOrder.RecipientAddress1))
+ {
+ return new OrderParcelReceiverInput
+ {
+ RecipientsName = storeOrder.RecipientName.Trim(),
+ CompanyName = storeOrder.RecipientCompany,
+ Phone = storeOrder.RecipientPhone,
+ TelePhone = storeOrder.RecipientTelePhone,
+ Email = storeOrder.RecipientEmail,
+ CountryCode = (storeOrder.RecipientCountry ?? storeOrder.OrderCountry)!.Trim(),
+ Province = storeOrder.RecipientProvince,
+ City = storeOrder.RecipientCity,
+ StreetAddress1 = storeOrder.RecipientAddress1.Trim(),
+ StreetAddress2 = storeOrder.RecipientAddress2,
+ ZipCode = storeOrder.RecipientZipCode.Trim()
+ };
+ }
+
if (!string.IsNullOrWhiteSpace(storeOrder.PlatformExtraJson))
{
try
@@ -260,4 +374,59 @@ public static class PlatformStoreOrderParcelBuilder
throw new BusinessException("收件人信息不完整,请同步平台订单或在匹配时补充收件人");
}
}
+
+ ///
+ /// 从 PlatformExtraJson / ExtraJson 解析收件人并写入结构化字段(已有值不覆盖)。
+ ///
+ public static void TryFillRecipientFields(SalesStoreOrder order, string? extraJson = null)
+ {
+ if (!string.IsNullOrWhiteSpace(order.RecipientName)
+ && !string.IsNullOrWhiteSpace(order.RecipientAddress1)
+ && !string.IsNullOrWhiteSpace(order.RecipientZipCode)
+ && !string.IsNullOrWhiteSpace(order.RecipientCountry ?? order.OrderCountry))
+ {
+ return;
+ }
+
+ var json = !string.IsNullOrWhiteSpace(extraJson) ? extraJson : order.PlatformExtraJson;
+ if (string.IsNullOrWhiteSpace(json))
+ {
+ return;
+ }
+
+ try
+ {
+ var node = JsonNode.Parse(json);
+ if (node == null)
+ {
+ return;
+ }
+
+ var parsed = ParseReceiverNode(node);
+ if (parsed == null)
+ {
+ return;
+ }
+
+ order.RecipientName ??= parsed.RecipientsName;
+ order.RecipientCompany ??= parsed.CompanyName;
+ order.RecipientPhone ??= parsed.Phone;
+ order.RecipientTelePhone ??= parsed.TelePhone;
+ order.RecipientEmail ??= parsed.Email;
+ order.RecipientCountry ??= parsed.CountryCode;
+ order.RecipientProvince ??= parsed.Province;
+ order.RecipientCity ??= parsed.City;
+ order.RecipientZipCode ??= parsed.ZipCode;
+ order.RecipientAddress1 ??= parsed.StreetAddress1;
+ order.RecipientAddress2 ??= parsed.StreetAddress2;
+ if (string.IsNullOrWhiteSpace(order.OrderCountry) && !string.IsNullOrWhiteSpace(parsed.CountryCode))
+ {
+ order.OrderCountry = parsed.CountryCode;
+ }
+ }
+ catch (JsonException)
+ {
+ // ignore
+ }
+ }
}
diff --git a/src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs b/src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs
index 45f11ee..dc38a53 100644
--- a/src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs
+++ b/src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs
@@ -11,6 +11,7 @@ using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories;
using Oms.Domain.Shared.Enums;
+using Oms.Domain.SystemAggregate.Entities;
using Oms.Domain.SystemAggregate.Repositories;
using Oms.Domain.WarehouseAggregate.Consts;
using Oms.Domain.WarehouseAggregate.Repositories;
@@ -372,6 +373,253 @@ public class PlatformStoreOrderService : ApplicationService
return output;
}
+ ///
+ /// 批量创建物流订单(不扣库存、不要求 SKU 映射)。
+ ///
+ [HttpPost("/order/platform-store-order/batch-create-logistics")]
+ public async Task BatchCreateLogisticsAsync(
+ PlatformStoreOrderBatchCreateLogisticsInput input)
+ {
+ return await BatchCreateLogisticsInternalAsync(
+ input.CustomerId,
+ input.ProductCode,
+ input.WarehouseId,
+ input.Items,
+ requireBind: false);
+ }
+
+ ///
+ /// 批量创建物流订单并绑定包裹/库存(冻结,不即时扣实物)。
+ ///
+ [HttpPost("/order/platform-store-order/batch-create-fulfill")]
+ public async Task BatchCreateFulfillAsync(
+ PlatformStoreOrderBatchCreateFulfillInput input)
+ {
+ if (input.WarehouseId <= 0)
+ {
+ throw new BusinessException("请指定发货仓库");
+ }
+
+ return await BatchCreateLogisticsInternalAsync(
+ input.CustomerId,
+ input.ProductCode,
+ input.WarehouseId,
+ input.Items,
+ requireBind: true);
+ }
+
+ private async Task BatchCreateLogisticsInternalAsync(
+ int? customerIdInput,
+ string? productCode,
+ int? warehouseId,
+ List items,
+ bool requireBind)
+ {
+ if (items == null || items.Count == 0)
+ {
+ throw new BusinessException("请选择要创建的店铺订单");
+ }
+
+ if (items.Count > 50)
+ {
+ throw new BusinessException("单次最多创建 50 单");
+ }
+
+ if (string.IsNullOrWhiteSpace(productCode))
+ {
+ throw new BusinessException("请选择运输方式");
+ }
+
+ var customerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
+ CustomerRepository,
+ CurrentUser,
+ customerIdInput);
+ var resolvedProductCode = await ResolveProductCodeRequiredAsync(productCode);
+ var batchNo = $"PSL{DateTime.Now:yyyyMMddHHmmss}";
+ var output = new PlatformStoreOrderBatchCreateLogisticsOutput { BatchNo = batchNo };
+
+ foreach (var itemInput in items)
+ {
+ var storeOrderId = itemInput.StoreOrderId;
+ var storeOrder = await StoreOrderRepository.FirstOrDefaultAsync(x =>
+ x.Id == storeOrderId && x.CustomerId == customerId);
+ var item = new PlatformStoreOrderBatchMatchItemOutput
+ {
+ Id = storeOrderId,
+ PlatformOrderNo = storeOrder?.PlatformOrderNo ?? storeOrderId.ToString()
+ };
+
+ try
+ {
+ if (requireBind && (itemInput.BindItems == null || itemInput.BindItems.Count == 0))
+ {
+ throw new BusinessException("请先绑定包裹或库存");
+ }
+
+ var result = await CreateLogisticsCoreAsync(
+ customerId,
+ storeOrderId,
+ warehouseId,
+ resolvedProductCode,
+ itemInput.Receiver,
+ requireBind ? itemInput.BindItems : null);
+ item.Success = true;
+ item.Message = requireBind
+ ? $"已创建物流单 {result.ServiceNo} 并绑定"
+ : $"已创建物流单 {result.ServiceNo}";
+ item.LinkedOrderId = result.OrderId;
+ item.ServiceNo = result.ServiceNo;
+ output.SuccessCount++;
+ }
+ catch (BusinessException ex)
+ {
+ item.Success = false;
+ item.Message = ex.Message;
+ output.FailCount++;
+ }
+ catch (Exception ex)
+ {
+ item.Success = false;
+ item.Message = ex.Message;
+ output.FailCount++;
+ }
+
+ output.Items.Add(item);
+ }
+
+ return output;
+ }
+
+ private async Task CreateLogisticsCoreAsync(
+ int customerId,
+ int storeOrderId,
+ int? warehouseId,
+ string productCode,
+ OrderParcelReceiverInput? receiver,
+ List? bindItems)
+ {
+ var order = await GetOrderOrThrowAsync(customerId, storeOrderId);
+ await EnsureMatchableAsync(order);
+
+ var lines = await StoreOrderLineRepository.GetListAsync(x => x.StoreOrderId == order.Id && !x.IsShipExcluded);
+ if (lines.Count == 0)
+ {
+ throw new BusinessException("没有可发货的商品行");
+ }
+
+ var resolvedWarehouseId = warehouseId ?? order.WarehouseId;
+ WarehouseAddress? warehouse = null;
+ if (resolvedWarehouseId is > 0)
+ {
+ warehouse = await WarehouseAddressRepository.FirstOrDefaultAsync(x => x.Id == resolvedWarehouseId.Value)
+ ?? throw new BusinessException("仓库不存在");
+ }
+
+ if (bindItems is { Count: > 0 } && resolvedWarehouseId is not > 0)
+ {
+ throw new BusinessException("请指定发货仓库");
+ }
+
+ var parcelInput = PlatformStoreOrderParcelBuilder.BuildFromPlatformLines(
+ order,
+ lines,
+ warehouse,
+ productCode,
+ resolvedWarehouseId,
+ customerId,
+ receiver);
+
+ var parcelOutput = await OrderService.CreateParcelForBatchAsync(
+ parcelInput,
+ CurrentUser.GetId(),
+ OrderSourceCodes.Platform);
+
+ order.WarehouseId = resolvedWarehouseId ?? order.WarehouseId;
+ order.LinkedOrderId = parcelOutput.OrderId;
+ order.FulfillStatus = SalesStoreFulfillStatusCodes.ToShip;
+ order.LastSyncTime = DateTime.Now;
+ if (!string.IsNullOrWhiteSpace(parcelOutput.TrackNo))
+ {
+ order.ShippingLabelNo = parcelOutput.TrackNo;
+ }
+
+ await StoreOrderRepository.UpdateAsync(order);
+
+ if (bindItems is not { Count: > 0 })
+ {
+ return parcelOutput;
+ }
+
+ try
+ {
+ // 确保物流单带仓库,绑定校验依赖 WarehouseId
+ if (resolvedWarehouseId is > 0)
+ {
+ var logisticsOrder = await OrderRepository.FirstOrDefaultAsync(x => x.Id == parcelOutput.OrderId);
+ if (logisticsOrder != null && logisticsOrder.WarehouseId is not > 0)
+ {
+ logisticsOrder.WarehouseId = resolvedWarehouseId;
+ await OrderRepository.UpdateAsync(logisticsOrder);
+ }
+ }
+
+ await OrderService.SaveStockBindAsync(new OrderStockBindSaveInput
+ {
+ OrderId = parcelOutput.OrderId,
+ Items = bindItems
+ });
+ return parcelOutput;
+ }
+ catch (Exception)
+ {
+ await RollbackCreatedLogisticsAsync(order, parcelOutput.OrderId);
+ throw;
+ }
+ }
+
+ private async Task RollbackCreatedLogisticsAsync(SalesStoreOrder storeOrder, int logisticsOrderId)
+ {
+ try
+ {
+ await OrderService.ClearStockBindAsync(new OrderStockBindOrderInput { OrderId = logisticsOrderId });
+ }
+ catch
+ {
+ // 可能尚未写入绑定
+ }
+
+ storeOrder.LinkedOrderId = null;
+ storeOrder.FulfillStatus = SalesStoreFulfillStatusCodes.Pending;
+ storeOrder.LastSyncTime = DateTime.Now;
+ await StoreOrderRepository.UpdateAsync(storeOrder);
+
+ var logisticsOrder = await OrderRepository.FirstOrDefaultAsync(x => x.Id == logisticsOrderId);
+ if (logisticsOrder == null)
+ {
+ return;
+ }
+
+ logisticsOrder.OrderStatus = OrderStatusCodes.Deleted;
+ if (!string.IsNullOrWhiteSpace(logisticsOrder.RequestId))
+ {
+ logisticsOrder.RequestId = $"{logisticsOrder.RequestId}:rolled-back:{DateTime.Now.Ticks}";
+ }
+
+ await OrderRepository.UpdateAsync(logisticsOrder);
+ }
+
+ private async Task ResolveProductCodeRequiredAsync(string productCode)
+ {
+ var normalized = productCode.Trim();
+ var product = await SalesProductRepository.GetByProductCodeAsync(normalized);
+ if (product == null || product.IsActive != EnableStatus.Enabled)
+ {
+ throw new BusinessException("销售产品不存在或未启用");
+ }
+
+ return normalized;
+ }
+
private async Task MatchInventoryCoreAsync(
int customerId,
int storeOrderId,
@@ -727,6 +975,17 @@ public class PlatformStoreOrderService : ApplicationService
PlatformCode = order.PlatformCode,
PlatformOrderNo = order.PlatformOrderNo,
OrderCountry = order.OrderCountry,
+ RecipientName = order.RecipientName,
+ RecipientCompany = order.RecipientCompany,
+ RecipientPhone = order.RecipientPhone,
+ RecipientTelePhone = order.RecipientTelePhone,
+ RecipientEmail = order.RecipientEmail,
+ RecipientCountry = order.RecipientCountry,
+ RecipientProvince = order.RecipientProvince,
+ RecipientCity = order.RecipientCity,
+ RecipientZipCode = order.RecipientZipCode,
+ RecipientAddress1 = order.RecipientAddress1,
+ RecipientAddress2 = order.RecipientAddress2,
ItemCount = order.ItemCount,
TotalAmount = order.TotalAmount,
Currency = order.Currency,
diff --git a/src/Oms.Domain/OrdersAggregate/Entities/SalesStoreOrder.cs b/src/Oms.Domain/OrdersAggregate/Entities/SalesStoreOrder.cs
index cbc2f9d..a297d48 100644
--- a/src/Oms.Domain/OrdersAggregate/Entities/SalesStoreOrder.cs
+++ b/src/Oms.Domain/OrdersAggregate/Entities/SalesStoreOrder.cs
@@ -16,6 +16,28 @@ public class SalesStoreOrder : OmsAuditedAggregateRoot
public string? OrderCountry { get; set; }
+ public string? RecipientName { get; set; }
+
+ public string? RecipientCompany { get; set; }
+
+ public string? RecipientPhone { get; set; }
+
+ public string? RecipientTelePhone { get; set; }
+
+ public string? RecipientEmail { get; set; }
+
+ public string? RecipientCountry { get; set; }
+
+ public string? RecipientProvince { get; set; }
+
+ public string? RecipientCity { get; set; }
+
+ public string? RecipientZipCode { get; set; }
+
+ public string? RecipientAddress1 { get; set; }
+
+ public string? RecipientAddress2 { get; set; }
+
public int? ItemCount { get; set; }
public decimal? TotalAmount { get; set; }