20261008-ide1.0发布 #1
42
db.sql
42
db.sql
@ -233,7 +233,7 @@ CREATE TABLE `t_order_file` (
|
||||
CREATE TABLE `t_customer_sender_address` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '归属客户 t_customer.Id',
|
||||
`AddressType` VARCHAR(16) NOT NULL DEFAULT 'sender' COMMENT '地址类型:sender发件/return退件/receiver收件',
|
||||
`AddressType` VARCHAR(16) NOT NULL DEFAULT 'sender' COMMENT '地址类型:sender国内发件/return国内退件/pickup国内揽收/receiver海外收件',
|
||||
`AddressName` VARCHAR(64) NULL COMMENT '地址别名',
|
||||
`SenderName` VARCHAR(50) NOT NULL COMMENT '联系人姓名',
|
||||
`Company` VARCHAR(255) NULL COMMENT '公司',
|
||||
@ -261,6 +261,30 @@ CREATE TABLE `t_customer_sender_address` (
|
||||
KEY `idx_csa_customer_default` (`CustomerId`, `AddressType`, `IsDefault`)
|
||||
) COMMENT='客户地址簿(发件/退件/收件)';
|
||||
|
||||
CREATE TABLE `t_customer_export_qualification` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`CustomerId` INT NOT NULL COMMENT '归属客户 t_customer.Id',
|
||||
`CompanyName` VARCHAR(200) NOT NULL COMMENT '企业名称',
|
||||
`SocialCreditCode` VARCHAR(64) NOT NULL COMMENT '企业社会信用代码',
|
||||
`DomesticConsignor` VARCHAR(200) NOT NULL COMMENT '境内发货人',
|
||||
`DomesticConsignorCustomsCode` VARCHAR(64) NULL COMMENT '境内发货人海关代码',
|
||||
`ProducerName` VARCHAR(200) NOT NULL COMMENT '生产销售单位',
|
||||
`ProducerCustomsCode` VARCHAR(64) NULL COMMENT '生产销售单位海关代码',
|
||||
`CustomsPort` VARCHAR(100) NULL COMMENT '备案海关港口地',
|
||||
`IsDefault` TINYINT NOT NULL DEFAULT 0 COMMENT '是否默认:0-否,1-是(同客户仅一条)',
|
||||
`Remark` VARCHAR(512) NULL COMMENT '备注',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳',
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`UpdateUserId` INT NULL COMMENT '更新人',
|
||||
`UpdateTime` DATETIME NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`Id`),
|
||||
KEY `idx_ceq_customer` (`CustomerId`, `IsDeleted`, `Id`),
|
||||
KEY `idx_ceq_credit` (`SocialCreditCode`, `IsDeleted`),
|
||||
KEY `idx_ceq_customer_default` (`CustomerId`, `IsDefault`)
|
||||
) COMMENT='客户出口资质备案';
|
||||
|
||||
CREATE TABLE `t_order_template` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增,步长 1',
|
||||
`Name` VARCHAR(64) NOT NULL COMMENT '模板名称',
|
||||
@ -403,7 +427,8 @@ CREATE TABLE `t_order_declaration` (
|
||||
`HsCode` VARCHAR(50) NULL COMMENT 'HsCode',
|
||||
`ImHsCode` VARCHAR(50) NULL COMMENT '进口HsCode',
|
||||
`ExHsCode` VARCHAR(50) NULL COMMENT '出口HsCode',
|
||||
`SkuImage` VARCHAR(255) NULL COMMENT '图片',
|
||||
`DeclareElement` VARCHAR(255) NULL COMMENT '出口申报要素',
|
||||
`SkuImage` VARCHAR(512) NULL COMMENT '图片(存 storage key 或短 URL)',
|
||||
`Category` VARCHAR(20) NULL COMMENT '类别',
|
||||
`Brand` VARCHAR(50) NULL COMMENT '品牌',
|
||||
`BrandType` VARCHAR(50) NULL COMMENT '品牌类型',
|
||||
@ -595,7 +620,8 @@ CREATE TABLE `t_sales_product_allocation_provider` (
|
||||
`ProviderName` VARCHAR(128) NOT NULL COMMENT '服务商名称',
|
||||
`ServiceChannelId` INT NULL COMMENT '服务渠道ID',
|
||||
`Weight` INT NOT NULL COMMENT '权重(评分用)',
|
||||
`CustomerIds` JSON NULL COMMENT '指定客户ID数组,空表示全部',
|
||||
`CustomerIds` JSON NULL COMMENT '指定客户ID数组,空表示全部(兼容旧配置)',
|
||||
`MatchCondition` TEXT NULL COMMENT '排货判断条件(中文公式),空表示全部订单可用',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
@ -1350,6 +1376,7 @@ CREATE TABLE `c_sku` (
|
||||
`UnitPrice` DECIMAL(12,4) NULL COMMENT '单价',
|
||||
`ExHsCode` VARCHAR(64) NULL COMMENT '出口HSCode',
|
||||
`ImHsCode` VARCHAR(64) NULL COMMENT '进口HSCode',
|
||||
`DeclareElement` VARCHAR(255) NULL COMMENT '出口申报要素',
|
||||
`WeightG` DECIMAL(10,2) NULL COMMENT '重量(g)',
|
||||
`LengthCm` DECIMAL(10,2) NULL COMMENT '长(cm)',
|
||||
`WidthCm` DECIMAL(10,2) NULL COMMENT '宽(cm)',
|
||||
@ -1850,7 +1877,8 @@ CREATE TABLE `t_pickup_order` (
|
||||
`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',
|
||||
`Direction` VARCHAR(16) NOT NULL DEFAULT 'forward' COMMENT 'forward正向(客户→仓)/reverse逆向(仓→客户)',
|
||||
`SenderName` VARCHAR(50) NOT NULL DEFAULT '' COMMENT '取件联系人快照',
|
||||
`SenderCompany` VARCHAR(255) NULL COMMENT '公司快照',
|
||||
`SenderCountry` INT NULL COMMENT '国家 t_country.Id 快照',
|
||||
@ -1899,6 +1927,7 @@ CREATE TABLE `t_pickup_order` (
|
||||
UNIQUE KEY `uk_pickup_no` (`PickupNo`),
|
||||
UNIQUE KEY `uk_pickup_request_id` (`RequestId`),
|
||||
KEY `idx_pickup_customer_status` (`CustomerId`, `Status`, `CreateTime`),
|
||||
KEY `idx_pickup_direction_status` (`Direction`, `Status`, `CreateTime`),
|
||||
KEY `idx_pickup_warehouse_time` (`WarehouseId`, `ExpectPickupTime`),
|
||||
KEY `idx_pickup_pay` (`PayStatus`),
|
||||
KEY `idx_pickup_settle` (`SettlementStatus`),
|
||||
@ -1974,7 +2003,7 @@ CREATE TABLE `t_9610_order` (
|
||||
`ManifestSyncTime` DATETIME NULL COMMENT '清单数据最近同步时间',
|
||||
`FailReason` VARCHAR(512) NULL COMMENT '最近失败原因',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除',
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳',
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT '' COMMENT '并发戳',
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`UpdateUserId` INT NULL COMMENT '更新人',
|
||||
@ -1995,7 +2024,7 @@ CREATE TABLE `t_9610_order_item` (
|
||||
`Sku` VARCHAR(32) NOT NULL DEFAULT '' COMMENT 'SKU(对齐 t_order_declaration.Sku)',
|
||||
`CnName` VARCHAR(250) NOT NULL DEFAULT '' COMMENT '中文品名(对齐 t_order_declaration.CnName)',
|
||||
`ExHsCode` VARCHAR(10) NULL COMMENT '出口海关编码(对齐 t_order_declaration.ExHsCode)',
|
||||
`DeclareElement` VARCHAR(255) NULL COMMENT '申报要素',
|
||||
`DeclareElement` VARCHAR(255) NULL COMMENT '出口申报要素',
|
||||
`Barcode` VARCHAR(255) NULL COMMENT '条形码',
|
||||
`DestCountry` VARCHAR(255) NULL COMMENT '目的国(地区)代码',
|
||||
`ItemCurrency` VARCHAR(3) NULL COMMENT '币制',
|
||||
@ -2008,6 +2037,7 @@ CREATE TABLE `t_9610_order_item` (
|
||||
`Freight` DECIMAL(12,4) NULL COMMENT '行运费',
|
||||
`SortNo` INT NOT NULL DEFAULT 0 COMMENT '排序',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除',
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT '' COMMENT '并发戳',
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`UpdateUserId` INT NULL COMMENT '更新人',
|
||||
|
||||
21
sql/migrate_allocation_match_condition.sql
Normal file
21
sql/migrate_allocation_match_condition.sql
Normal file
@ -0,0 +1,21 @@
|
||||
-- 存量库:排货规则服务商表增加判断条件 MatchCondition
|
||||
-- 可重复执行;仅在列不存在时 ADD。
|
||||
-- 目标表:t_sales_product_allocation_provider
|
||||
|
||||
SET @db := DATABASE();
|
||||
|
||||
SET @exists := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_sales_product_allocation_provider'
|
||||
AND COLUMN_NAME = 'MatchCondition'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@exists = 0,
|
||||
'ALTER TABLE `t_sales_product_allocation_provider` ADD COLUMN `MatchCondition` TEXT NULL COMMENT ''排货判断条件(中文公式),空表示全部订单可用'' AFTER `CustomerIds`',
|
||||
'SELECT ''MatchCondition already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
98
sql/migrate_customer_export_qualification.sql
Normal file
98
sql/migrate_customer_export_qualification.sql
Normal file
@ -0,0 +1,98 @@
|
||||
-- 存量库:客户出口资质备案表(含 IsDefault / Remark)
|
||||
-- 可重复执行。
|
||||
-- 目标表:t_customer_export_qualification
|
||||
|
||||
SET @db := DATABASE();
|
||||
|
||||
-- 1) 表不存在则整表创建
|
||||
SET @exists := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_customer_export_qualification'
|
||||
);
|
||||
|
||||
SET @sql := IF(
|
||||
@exists = 0,
|
||||
'CREATE TABLE `t_customer_export_qualification` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT ''主键'',
|
||||
`CustomerId` INT NOT NULL COMMENT ''归属客户 t_customer.Id'',
|
||||
`CompanyName` VARCHAR(200) NOT NULL COMMENT ''企业名称'',
|
||||
`SocialCreditCode` VARCHAR(64) NOT NULL COMMENT ''企业社会信用代码'',
|
||||
`DomesticConsignor` VARCHAR(200) NOT NULL COMMENT ''境内发货人'',
|
||||
`DomesticConsignorCustomsCode` VARCHAR(64) NULL COMMENT ''境内发货人海关代码'',
|
||||
`ProducerName` VARCHAR(200) NOT NULL COMMENT ''生产销售单位'',
|
||||
`ProducerCustomsCode` VARCHAR(64) NULL COMMENT ''生产销售单位海关代码'',
|
||||
`CustomsPort` VARCHAR(100) NULL COMMENT ''备案海关港口地'',
|
||||
`IsDefault` TINYINT NOT NULL DEFAULT 0 COMMENT ''是否默认:0-否,1-是(同客户仅一条)'',
|
||||
`Remark` VARCHAR(512) NULL COMMENT ''备注'',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT ''软删除:0-否,1-是'',
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT ''并发戳'',
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT ''创建人'',
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ''创建时间'',
|
||||
`UpdateUserId` INT NULL COMMENT ''更新人'',
|
||||
`UpdateTime` DATETIME NULL COMMENT ''更新时间'',
|
||||
PRIMARY KEY (`Id`),
|
||||
KEY `idx_ceq_customer` (`CustomerId`, `IsDeleted`, `Id`),
|
||||
KEY `idx_ceq_credit` (`SocialCreditCode`, `IsDeleted`),
|
||||
KEY `idx_ceq_customer_default` (`CustomerId`, `IsDefault`)
|
||||
) COMMENT=''客户出口资质备案''',
|
||||
'SELECT ''t_customer_export_qualification already exists'' AS info'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 2) 已建表但缺 IsDefault 时补列
|
||||
SET @col := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_customer_export_qualification'
|
||||
AND COLUMN_NAME = 'IsDefault'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@col = 0,
|
||||
'ALTER TABLE `t_customer_export_qualification`
|
||||
ADD COLUMN `IsDefault` TINYINT NOT NULL DEFAULT 0 COMMENT ''是否默认:0-否,1-是(同客户仅一条)'' AFTER `CustomsPort`',
|
||||
'SELECT ''IsDefault already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 3) 已建表但缺 Remark 时补列
|
||||
SET @col := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_customer_export_qualification'
|
||||
AND COLUMN_NAME = 'Remark'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@col = 0,
|
||||
'ALTER TABLE `t_customer_export_qualification`
|
||||
ADD COLUMN `Remark` VARCHAR(512) NULL COMMENT ''备注'' AFTER `IsDefault`',
|
||||
'SELECT ''Remark already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 4) 默认索引
|
||||
SET @idx := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_customer_export_qualification'
|
||||
AND INDEX_NAME = 'idx_ceq_customer_default'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@idx = 0,
|
||||
'ALTER TABLE `t_customer_export_qualification` ADD KEY `idx_ceq_customer_default` (`CustomerId`, `IsDefault`)',
|
||||
'SELECT ''idx_ceq_customer_default already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
38
sql/migrate_declare_element.sql
Normal file
38
sql/migrate_declare_element.sql
Normal file
@ -0,0 +1,38 @@
|
||||
-- 存量库:SKU / 订单申报增加「出口申报要素」DeclareElement
|
||||
-- 可重复执行;仅在列不存在时 ADD。
|
||||
|
||||
SET @db := DATABASE();
|
||||
|
||||
-- c_sku.DeclareElement
|
||||
SET @exists := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 'c_sku'
|
||||
AND COLUMN_NAME = 'DeclareElement'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@exists = 0,
|
||||
'ALTER TABLE `c_sku` ADD COLUMN `DeclareElement` VARCHAR(255) NULL COMMENT ''出口申报要素'' AFTER `ImHsCode`',
|
||||
'SELECT ''c_sku.DeclareElement already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- t_order_declaration.DeclareElement
|
||||
SET @exists := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_order_declaration'
|
||||
AND COLUMN_NAME = 'DeclareElement'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@exists = 0,
|
||||
'ALTER TABLE `t_order_declaration` ADD COLUMN `DeclareElement` VARCHAR(255) NULL COMMENT ''出口申报要素'' AFTER `ExHsCode`',
|
||||
'SELECT ''t_order_declaration.DeclareElement already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@ -1,80 +0,0 @@
|
||||
-- 员工档案「岗位 / 职级」字典初始数据(幂等:已存在同 DictType+DictCode 则跳过)
|
||||
-- dictType: employee_position / employee_job_level
|
||||
-- MySQL 5.7+ / 8.0
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- ========== 岗位 employee_position ==========
|
||||
INSERT INTO `t_dict_type` (`DictType`, `Remark`, `TypeSort`, `IsDeleted`, `CreateUserId`)
|
||||
SELECT 'employee_position', '员工岗位', 0, 0, 0
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `t_dict_type` WHERE `DictType` = 'employee_position' AND `IsDeleted` = 0
|
||||
);
|
||||
|
||||
INSERT INTO `t_dict` (`DictType`, `DictSort`, `DictCode`, `DictName`, `IsDeleted`, `CreateUserId`)
|
||||
SELECT v.`DictType`, v.`DictSort`, v.`DictCode`, v.`DictName`, 0, 0
|
||||
FROM (
|
||||
SELECT 'employee_position' AS DictType, 1 AS DictSort, 'generalManager' AS DictCode, '总经理' AS DictName
|
||||
UNION ALL SELECT 'employee_position', 2, 'deputyGeneralManager', '副总经理'
|
||||
UNION ALL SELECT 'employee_position', 3, 'logisticsAdvisor', '物流顾问'
|
||||
UNION ALL SELECT 'employee_position', 4, 'csManager', '客服主管'
|
||||
UNION ALL SELECT 'employee_position', 5, 'csSpecialist', '客服专员'
|
||||
UNION ALL SELECT 'employee_position', 6, 'opsManager', '运营经理'
|
||||
UNION ALL SELECT 'employee_position', 7, 'opsSpecialist', '运营专员'
|
||||
UNION ALL SELECT 'employee_position', 8, 'financeManager', '财务经理'
|
||||
UNION ALL SELECT 'employee_position', 9, 'financeSpecialist', '财务专员'
|
||||
UNION ALL SELECT 'employee_position', 10, 'warehouseManager', '仓储主管'
|
||||
UNION ALL SELECT 'employee_position', 11, 'warehouseClerk', '仓管员'
|
||||
UNION ALL SELECT 'employee_position', 12, 'customsSpecialist', '关务专员'
|
||||
UNION ALL SELECT 'employee_position', 13, 'productManager', '产品经理'
|
||||
UNION ALL SELECT 'employee_position', 14, 'techEngineer', '技术工程师'
|
||||
UNION ALL SELECT 'employee_position', 15, 'hrAdmin', '人事行政'
|
||||
UNION ALL SELECT 'employee_position', 16, 'marketingSpecialist', '市场专员'
|
||||
UNION ALL SELECT 'employee_position', 17, 'dispatcher', '调度员'
|
||||
) AS v
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM `t_dict` d
|
||||
WHERE d.`DictType` = v.`DictType`
|
||||
AND d.`DictCode` = v.`DictCode`
|
||||
AND d.`IsDeleted` = 0
|
||||
);
|
||||
|
||||
UPDATE `t_dict`
|
||||
SET `IsDeleted` = 1,
|
||||
`UpdateTime` = NOW()
|
||||
WHERE `DictType` = 'employee_position'
|
||||
AND `DictCode` IN (
|
||||
'salesDirector',
|
||||
'salesManager',
|
||||
'salesSpecialist',
|
||||
'logisticsAdvisorDirector',
|
||||
'logisticsAdvisorManager'
|
||||
)
|
||||
AND `IsDeleted` = 0;
|
||||
|
||||
-- ========== 职级 employee_job_level(一级~五级) ==========
|
||||
INSERT INTO `t_dict_type` (`DictType`, `Remark`, `TypeSort`, `IsDeleted`, `CreateUserId`)
|
||||
SELECT 'employee_job_level', '员工职级', 0, 0, 0
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `t_dict_type` WHERE `DictType` = 'employee_job_level' AND `IsDeleted` = 0
|
||||
);
|
||||
|
||||
INSERT INTO `t_dict` (`DictType`, `DictSort`, `DictCode`, `DictName`, `IsDeleted`, `CreateUserId`)
|
||||
SELECT v.`DictType`, v.`DictSort`, v.`DictCode`, v.`DictName`, 0, 0
|
||||
FROM (
|
||||
SELECT 'employee_job_level' AS DictType, 1 AS DictSort, '1' AS DictCode, '一级' AS DictName
|
||||
UNION ALL SELECT 'employee_job_level', 2, '2', '二级'
|
||||
UNION ALL SELECT 'employee_job_level', 3, '3', '三级'
|
||||
UNION ALL SELECT 'employee_job_level', 4, '4', '四级'
|
||||
UNION ALL SELECT 'employee_job_level', 5, '5', '五级'
|
||||
) AS v
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM `t_dict` d
|
||||
WHERE d.`DictType` = v.`DictType`
|
||||
AND d.`DictCode` = v.`DictCode`
|
||||
AND d.`IsDeleted` = 0
|
||||
);
|
||||
45
sql/migrate_issue_template_handler.sql
Normal file
45
sql/migrate_issue_template_handler.sql
Normal file
@ -0,0 +1,45 @@
|
||||
-- 存量库:为问题模板补充处理人字段(与 db.sql / IssueTemplate 实体对齐)
|
||||
-- 可重复执行;仅在列不存在时 ADD。
|
||||
-- 目标表:t_issue_template
|
||||
|
||||
SET @db := DATABASE();
|
||||
|
||||
-- HandlerType
|
||||
SET @exists := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_issue_template'
|
||||
AND COLUMN_NAME = 'HandlerType'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@exists = 0,
|
||||
'ALTER TABLE `t_issue_template` ADD COLUMN `HandlerType` VARCHAR(16) NULL COMMENT ''处理人类型:customer=订单所属客户,user=指定用户'' AFTER `NotifyMethod`',
|
||||
'SELECT ''HandlerType already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- HandlerUserId
|
||||
SET @exists := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_issue_template'
|
||||
AND COLUMN_NAME = 'HandlerUserId'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@exists = 0,
|
||||
'ALTER TABLE `t_issue_template` ADD COLUMN `HandlerUserId` INT NULL COMMENT ''指定处理人用户Id(HandlerType=user 时必填)'' AFTER `HandlerType`',
|
||||
'SELECT ''HandlerUserId already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 历史数据默认按「指定用户」语义;无处理人时可在页面再编辑
|
||||
UPDATE `t_issue_template`
|
||||
SET `HandlerType` = 'user'
|
||||
WHERE (`HandlerType` IS NULL OR `HandlerType` = '')
|
||||
AND `IsDeleted` = 0;
|
||||
16
sql/migrate_pickup_direction_and_address_type.sql
Normal file
16
sql/migrate_pickup_direction_and_address_type.sql
Normal file
@ -0,0 +1,16 @@
|
||||
-- 揽收方向 + 客户地址类型扩展(国内揽收)
|
||||
-- 正向 forward:客户揽收地址 → 仓库
|
||||
-- 逆向 reverse:仓库 → 客户揽收地址
|
||||
|
||||
ALTER TABLE `t_pickup_order`
|
||||
ADD COLUMN `Direction` VARCHAR(16) NOT NULL DEFAULT 'forward'
|
||||
COMMENT 'forward正向(客户→仓)/reverse逆向(仓→客户)'
|
||||
AFTER `SenderAddressId`;
|
||||
|
||||
ALTER TABLE `t_pickup_order`
|
||||
ADD KEY `idx_pickup_direction_status` (`Direction`, `Status`, `CreateTime`);
|
||||
|
||||
-- 地址类型注释更新(列已存在,仅说明)
|
||||
ALTER TABLE `t_customer_sender_address`
|
||||
MODIFY COLUMN `AddressType` VARCHAR(16) NOT NULL DEFAULT 'sender'
|
||||
COMMENT 'sender国内发件/return国内退件/pickup国内揽收/receiver海外收件';
|
||||
@ -1,36 +0,0 @@
|
||||
-- t_sales_product:官网线路展示排序(幂等;列已存在则跳过)
|
||||
-- 数值越小越靠前;默认 0
|
||||
|
||||
SET @db := DATABASE();
|
||||
|
||||
SET @col := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_sales_product'
|
||||
AND COLUMN_NAME = 'RouteSort'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@col = 0,
|
||||
'ALTER TABLE `t_sales_product` ADD COLUMN `RouteSort` INT NOT NULL DEFAULT 0 COMMENT ''官网线路排序,越小越靠前'' AFTER `TradeTerms`',
|
||||
'SELECT ''skip RouteSort'' AS msg'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @idx := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_sales_product'
|
||||
AND INDEX_NAME = 'idx_sp_route_sort'
|
||||
);
|
||||
SET @sql_idx := IF(
|
||||
@idx = 0,
|
||||
'ALTER TABLE `t_sales_product` ADD KEY `idx_sp_route_sort` (`RouteSort`, `IsActive`)',
|
||||
'SELECT ''skip idx_sp_route_sort'' AS msg'
|
||||
);
|
||||
PREPARE stmt_idx FROM @sql_idx;
|
||||
EXECUTE stmt_idx;
|
||||
DEALLOCATE PREPARE stmt_idx;
|
||||
@ -1,81 +0,0 @@
|
||||
-- 9610 商品明细命名对齐 t_order_declaration(幂等)
|
||||
-- ItemNo→Sku, ItemName→CnName, Gcode→ExHsCode, Price→UnitPrice, Qty→Quantity,
|
||||
-- TotalAmount→Amount, DeclareUnit→Unit
|
||||
-- 海关专属保留:Gmodel / Barcode / DestCountry / ItemCurrency / LegalQty / LegalUnit / Freight / SortNo
|
||||
|
||||
SET @db := DATABASE();
|
||||
|
||||
-- ItemNo → Sku
|
||||
SET @hasOld := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='ItemNo');
|
||||
SET @hasNew := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='Sku');
|
||||
SET @sql := IF(@hasOld>0 AND @hasNew=0,
|
||||
'ALTER TABLE `t_9610_order_item` CHANGE COLUMN `ItemNo` `Sku` VARCHAR(32) NOT NULL DEFAULT '''' COMMENT ''SKU(对齐 t_order_declaration.Sku / 上游 itemNo)''',
|
||||
'SELECT ''skip ItemNo->Sku'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ItemName → CnName
|
||||
SET @hasOld := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='ItemName');
|
||||
SET @hasNew := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='CnName');
|
||||
SET @sql := IF(@hasOld>0 AND @hasNew=0,
|
||||
'ALTER TABLE `t_9610_order_item` CHANGE COLUMN `ItemName` `CnName` VARCHAR(250) NOT NULL DEFAULT '''' COMMENT ''中文品名(对齐 t_order_declaration.CnName / 上游 itemName)''',
|
||||
'SELECT ''skip ItemName->CnName'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Gcode → ExHsCode
|
||||
SET @hasOld := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='Gcode');
|
||||
SET @hasNew := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='ExHsCode');
|
||||
SET @sql := IF(@hasOld>0 AND @hasNew=0,
|
||||
'ALTER TABLE `t_9610_order_item` CHANGE COLUMN `Gcode` `ExHsCode` VARCHAR(10) NULL COMMENT ''出口海关编码(对齐 t_order_declaration.ExHsCode / 上游 gcode)''',
|
||||
'SELECT ''skip Gcode->ExHsCode'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Price → UnitPrice
|
||||
SET @hasOld := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='Price');
|
||||
SET @hasNew := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='UnitPrice');
|
||||
SET @sql := IF(@hasOld>0 AND @hasNew=0,
|
||||
'ALTER TABLE `t_9610_order_item` CHANGE COLUMN `Price` `UnitPrice` DECIMAL(19,5) NOT NULL DEFAULT 0 COMMENT ''单价(对齐 t_order_declaration.UnitPrice)''',
|
||||
'SELECT ''skip Price->UnitPrice'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- TotalAmount → Amount(可能尚未存在 TotalAmount)
|
||||
SET @hasOld := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='TotalAmount');
|
||||
SET @hasNew := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='Amount');
|
||||
SET @sql := IF(@hasOld>0 AND @hasNew=0,
|
||||
'ALTER TABLE `t_9610_order_item` CHANGE COLUMN `TotalAmount` `Amount` DECIMAL(19,5) NULL COMMENT ''总价(对齐 t_order_declaration.Amount / 上游 totalPrice)''',
|
||||
'SELECT ''skip TotalAmount->Amount'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @hasNew := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='Amount');
|
||||
SET @sql := IF(@hasNew=0,
|
||||
'ALTER TABLE `t_9610_order_item` ADD COLUMN `Amount` DECIMAL(19,5) NULL COMMENT ''总价(对齐 t_order_declaration.Amount)'' AFTER `UnitPrice`',
|
||||
'SELECT ''skip add Amount'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Qty → Quantity
|
||||
SET @hasOld := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='Qty');
|
||||
SET @hasNew := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='Quantity');
|
||||
SET @sql := IF(@hasOld>0 AND @hasNew=0,
|
||||
'ALTER TABLE `t_9610_order_item` CHANGE COLUMN `Qty` `Quantity` DECIMAL(19,5) NOT NULL DEFAULT 0 COMMENT ''数量(对齐 t_order_declaration.Quantity,精度保留海关小数)''',
|
||||
'SELECT ''skip Qty->Quantity'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- DeclareUnit → Unit
|
||||
SET @hasOld := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='DeclareUnit');
|
||||
SET @hasNew := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='Unit');
|
||||
SET @sql := IF(@hasOld>0 AND @hasNew=0,
|
||||
'ALTER TABLE `t_9610_order_item` CHANGE COLUMN `DeclareUnit` `Unit` VARCHAR(3) NULL COMMENT ''计量单位(对齐 t_order_declaration.Unit / 上游 unit)''',
|
||||
'SELECT ''skip DeclareUnit->Unit'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @hasNew := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='Unit');
|
||||
SET @sql := IF(@hasNew=0,
|
||||
'ALTER TABLE `t_9610_order_item` ADD COLUMN `Unit` VARCHAR(3) NULL COMMENT ''计量单位(对齐 t_order_declaration.Unit)'' AFTER `LegalQty`',
|
||||
'SELECT ''skip add Unit'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Gmodel 注释:海关申报要素(由运单 Model/Specifications/Attributes/Material 拼接)
|
||||
SET @has := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='Gmodel');
|
||||
SET @sql := IF(@has>0,
|
||||
'ALTER TABLE `t_9610_order_item` MODIFY COLUMN `Gmodel` VARCHAR(255) NULL COMMENT ''申报要素(海关;由运单型号/规格等拼接,上游 gmodel)''',
|
||||
'SELECT ''skip Gmodel comment'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
@ -1,118 +0,0 @@
|
||||
-- 9610 单号命名对齐运单 t_order(幂等)
|
||||
-- ReferenceNo → ClientNo(客户单号)
|
||||
-- LogisticsNo 并入 ServiceNo(运单号)后删除 LogisticsNo
|
||||
|
||||
SET @db := DATABASE();
|
||||
|
||||
-- 1) ReferenceNo → ClientNo
|
||||
SET @hasRef := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='ReferenceNo'
|
||||
);
|
||||
SET @hasClient := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='ClientNo'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@hasRef > 0 AND @hasClient = 0,
|
||||
'ALTER TABLE `t_9610_order` CHANGE COLUMN `ReferenceNo` `ClientNo` VARCHAR(48) NOT NULL DEFAULT '''' COMMENT ''客户单号(对齐 t_order.ClientNo / 上游 referenceNo)''',
|
||||
'SELECT ''skip ReferenceNo->ClientNo'' AS msg'
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 索引名顺带调整(旧 idx_9610_ref → idx_9610_client_no)
|
||||
SET @idxRef := (
|
||||
SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND INDEX_NAME='idx_9610_ref'
|
||||
);
|
||||
SET @idxClient := (
|
||||
SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND INDEX_NAME='idx_9610_client_no'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@idxRef > 0 AND @idxClient = 0,
|
||||
'ALTER TABLE `t_9610_order` RENAME INDEX `idx_9610_ref` TO `idx_9610_client_no`',
|
||||
'SELECT ''skip rename idx_9610_ref'' AS msg'
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @idxClient := (
|
||||
SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND INDEX_NAME='idx_9610_client_no'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@idxClient = 0,
|
||||
'ALTER TABLE `t_9610_order` ADD KEY `idx_9610_client_no` (`ClientNo`)',
|
||||
'SELECT ''skip add idx_9610_client_no'' AS msg'
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 2) LogisticsNo → 回填 ServiceNo,再删列
|
||||
SET @hasLog := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='LogisticsNo'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@hasLog > 0,
|
||||
'UPDATE `t_9610_order` SET `ServiceNo` = NULLIF(TRIM(`LogisticsNo`), '''') WHERE (`ServiceNo` IS NULL OR TRIM(`ServiceNo`) = '''') AND `LogisticsNo` IS NOT NULL AND TRIM(`LogisticsNo`) <> ''''',
|
||||
'SELECT ''skip backfill ServiceNo'' AS msg'
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @idxLog := (
|
||||
SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND INDEX_NAME='idx_9610_logistics'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@idxLog > 0,
|
||||
'ALTER TABLE `t_9610_order` DROP INDEX `idx_9610_logistics`',
|
||||
'SELECT ''skip drop idx_9610_logistics'' AS msg'
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @hasLog := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='LogisticsNo'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@hasLog > 0,
|
||||
'ALTER TABLE `t_9610_order` DROP COLUMN `LogisticsNo`',
|
||||
'SELECT ''skip drop LogisticsNo'' AS msg'
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ServiceNo 索引(检索)
|
||||
SET @idxSvc := (
|
||||
SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND INDEX_NAME='idx_9610_service_no'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@idxSvc = 0,
|
||||
'ALTER TABLE `t_9610_order` ADD KEY `idx_9610_service_no` (`ServiceNo`)',
|
||||
'SELECT ''skip idx_9610_service_no'' AS msg'
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- OrderNo 注释对齐:海关下游订单编号(非运单 DownstreamServiceNo)
|
||||
SET @hasOrderNo := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='OrderNo'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@hasOrderNo > 0,
|
||||
'ALTER TABLE `t_9610_order` MODIFY COLUMN `OrderNo` VARCHAR(32) NULL COMMENT ''海关下游订单编号(申报回写,非 t_order.DownstreamServiceNo)''',
|
||||
'SELECT ''skip OrderNo comment'' AS msg'
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- DownstreamRefNo 注释
|
||||
SET @hasDr := (
|
||||
SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='DownstreamRefNo'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@hasDr > 0,
|
||||
'ALTER TABLE `t_9610_order` MODIFY COLUMN `DownstreamRefNo` VARCHAR(32) NULL COMMENT ''申报回写的 referenceNo 快照''',
|
||||
'SELECT ''skip DownstreamRefNo comment'' AS msg'
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
@ -1,170 +0,0 @@
|
||||
-- 9610:补齐「获取清单数据」回写字段(幂等;列已存在则跳过)
|
||||
-- 字段名为 OMS 自有命名,与上游 CEB 字段对照见注释
|
||||
|
||||
SET @db := DATABASE();
|
||||
|
||||
-- ========== t_9610_order ==========
|
||||
|
||||
-- EnterpriseBizNo ← copNo 企业唯一编号
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='EnterpriseBizNo');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order` ADD COLUMN `EnterpriseBizNo` VARCHAR(48) NULL COMMENT ''企业唯一编号(上游 copNo)'' AFTER `DownstreamRefNo`', 'SELECT ''skip EnterpriseBizNo'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ManifestNo ← invtNo 清单编号
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='ManifestNo');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order` ADD COLUMN `ManifestNo` VARCHAR(48) NULL COMMENT ''清单编号(上游 invtNo)'' AFTER `EnterpriseBizNo`', 'SELECT ''skip ManifestNo'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ClearanceTime ← clearanceTime 结关日期(原文,格式不固定)
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='ClearanceTime');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order` ADD COLUMN `ClearanceTime` VARCHAR(48) NULL COMMENT ''结关日期原文'' AFTER `ManifestNo`', 'SELECT ''skip ClearanceTime'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- CustomsInboundTime ← warehouseTime 海关入库时间
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='CustomsInboundTime');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order` ADD COLUMN `CustomsInboundTime` VARCHAR(48) NULL COMMENT ''海关入库时间原文'' AFTER `ClearanceTime`', 'SELECT ''skip CustomsInboundTime'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- CustomsState ← customsStatus 海关状态
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='CustomsState');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order` ADD COLUMN `CustomsState` VARCHAR(4) NULL COMMENT ''海关状态'' AFTER `CustomsInboundTime`', 'SELECT ''skip CustomsState'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- DeclareCustomsCode ← customsCode 申报地海关
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='DeclareCustomsCode');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order` ADD COLUMN `DeclareCustomsCode` VARCHAR(4) NULL COMMENT ''申报地海关代码'' AFTER `CustomsState`', 'SELECT ''skip DeclareCustomsCode'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- EbpCode ← ebpCode
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='EbpCode');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order` ADD COLUMN `EbpCode` VARCHAR(18) NULL COMMENT ''电商平台代码'' AFTER `DeclareCustomsCode`', 'SELECT ''skip EbpCode'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- EbpName ← ebpName
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='EbpName');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order` ADD COLUMN `EbpName` VARCHAR(100) NULL COMMENT ''电商平台名称'' AFTER `EbpCode`', 'SELECT ''skip EbpName'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- LogisticsCorpCode ← logisticsCode
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='LogisticsCorpCode');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order` ADD COLUMN `LogisticsCorpCode` VARCHAR(18) NULL COMMENT ''物流企业代码'' AFTER `EbpName`', 'SELECT ''skip LogisticsCorpCode'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- LogisticsCorpName ← logisticsName
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='LogisticsCorpName');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order` ADD COLUMN `LogisticsCorpName` VARCHAR(100) NULL COMMENT ''物流企业名称'' AFTER `LogisticsCorpCode`', 'SELECT ''skip LogisticsCorpName'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- IeMark ← ieFlag I/E
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='IeMark');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order` ADD COLUMN `IeMark` VARCHAR(4) NULL COMMENT ''进出口标记 I/E'' AFTER `LogisticsCorpName`', 'SELECT ''skip IeMark'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ExportPortCode ← portCode
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='ExportPortCode');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order` ADD COLUMN `ExportPortCode` VARCHAR(4) NULL COMMENT ''出口口岸代码'' AFTER `IeMark`', 'SELECT ''skip ExportPortCode'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ExportDate ← ieDate
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='ExportDate');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order` ADD COLUMN `ExportDate` VARCHAR(32) NULL COMMENT ''出口日期原文'' AFTER `ExportPortCode`', 'SELECT ''skip ExportDate'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- DeclareAgentCode ← agentCode
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='DeclareAgentCode');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order` ADD COLUMN `DeclareAgentCode` VARCHAR(18) NULL COMMENT ''申报企业代码'' AFTER `ExportDate`', 'SELECT ''skip DeclareAgentCode'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- DeclareAgentName ← agentName
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='DeclareAgentName');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order` ADD COLUMN `DeclareAgentName` VARCHAR(100) NULL COMMENT ''申报企业名称'' AFTER `DeclareAgentCode`', 'SELECT ''skip DeclareAgentName'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- TradeModeCode ← tradeMode
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='TradeModeCode');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order` ADD COLUMN `TradeModeCode` VARCHAR(4) NULL COMMENT ''贸易方式代码'' AFTER `DeclareAgentName`', 'SELECT ''skip TradeModeCode'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- TransportMode ← trafMode
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='TransportMode');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order` ADD COLUMN `TransportMode` VARCHAR(4) NULL COMMENT ''运输方式代码'' AFTER `TradeModeCode`', 'SELECT ''skip TransportMode'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- FreightCurrency ← fCurrency
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='FreightCurrency');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order` ADD COLUMN `FreightCurrency` VARCHAR(4) NULL COMMENT ''运费币制'' AFTER `Freight`', 'SELECT ''skip FreightCurrency'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- FreightMark ← fFlag 1率/2单价/3总价
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='FreightMark');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order` ADD COLUMN `FreightMark` INT NULL COMMENT ''运费标志 1率2单价3总价'' AFTER `FreightCurrency`', 'SELECT ''skip FreightMark'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- PackageType ← wrapType
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='PackageType');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order` ADD COLUMN `PackageType` INT NULL COMMENT ''包装种类代码'' AFTER `FreightMark`', 'SELECT ''skip PackageType'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- PackageQty ← packNo
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='PackageQty');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order` ADD COLUMN `PackageQty` INT NULL COMMENT ''件数'' AFTER `PackageType`', 'SELECT ''skip PackageQty'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ManifestSyncTime 最近一次清单回写时间
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='ManifestSyncTime');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order` ADD COLUMN `ManifestSyncTime` DATETIME NULL COMMENT ''清单数据最近同步时间'' AFTER `DeclareTime`', 'SELECT ''skip ManifestSyncTime'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 运费精度对齐上游 DECIMAL(19,5)
|
||||
SET @colType := (
|
||||
SELECT COLUMN_TYPE FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='Freight'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@colType IS NOT NULL AND @colType NOT LIKE 'decimal(19,5)%',
|
||||
'ALTER TABLE `t_9610_order` MODIFY COLUMN `Freight` DECIMAL(19,5) NULL COMMENT ''运费''',
|
||||
'SELECT ''skip Freight precision'' AS msg'
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ManifestNo 索引(幂等)
|
||||
SET @idx := (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND INDEX_NAME='idx_9610_manifest');
|
||||
SET @sql := IF(@idx=0, 'ALTER TABLE `t_9610_order` ADD KEY `idx_9610_manifest` (`ManifestNo`)', 'SELECT ''skip idx_9610_manifest'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ========== t_9610_order_item ==========
|
||||
|
||||
-- Barcode ← barCode
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='Barcode');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order_item` ADD COLUMN `Barcode` VARCHAR(255) NULL COMMENT ''条形码'' AFTER `Gmodel`', 'SELECT ''skip Barcode'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- DestCountry ← country 目的国
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='DestCountry');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order_item` ADD COLUMN `DestCountry` VARCHAR(255) NULL COMMENT ''目的国(地区)代码'' AFTER `Barcode`', 'SELECT ''skip DestCountry'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ItemCurrency ← currency
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='ItemCurrency');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order_item` ADD COLUMN `ItemCurrency` VARCHAR(3) NULL COMMENT ''币制'' AFTER `DestCountry`', 'SELECT ''skip ItemCurrency'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- LegalQty ← qty1 法定数量
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='LegalQty');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order_item` ADD COLUMN `LegalQty` DECIMAL(19,5) NULL COMMENT ''法定数量'' AFTER `Quantity`', 'SELECT ''skip LegalQty'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Unit ← unit(对齐运单;旧名 DeclareUnit 由 align_item_fields 迁移)
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='Unit');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order_item` ADD COLUMN `Unit` VARCHAR(3) NULL COMMENT ''计量单位'' AFTER `LegalQty`', 'SELECT ''skip Unit'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- LegalUnit ← unit1
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='LegalUnit');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order_item` ADD COLUMN `LegalUnit` VARCHAR(3) NULL COMMENT ''法定计量单位'' AFTER `Unit`', 'SELECT ''skip LegalUnit'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- Amount ← totalPrice
|
||||
SET @col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='Amount');
|
||||
SET @sql := IF(@col=0, 'ALTER TABLE `t_9610_order_item` ADD COLUMN `Amount` DECIMAL(19,5) NULL COMMENT ''总价'' AFTER `UnitPrice`', 'SELECT ''skip Amount'' AS msg');
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
@ -1,122 +0,0 @@
|
||||
-- 9610 汇总申报单(ExportCustomsMode=9610Summary)
|
||||
-- 幂等:表已存在则跳过
|
||||
|
||||
SET @db := DATABASE();
|
||||
|
||||
-- t_9610_order
|
||||
SET @exists := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_9610_order'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@exists = 0,
|
||||
'CREATE TABLE `t_9610_order` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT ''主键'',
|
||||
`OrderId` INT NOT NULL COMMENT ''关联运单 t_order.Id'',
|
||||
`ServiceNo` VARCHAR(64) NULL COMMENT ''运单号冗余'',
|
||||
`CustomerId` INT NOT NULL DEFAULT 0 COMMENT ''客户Id'',
|
||||
`Status` VARCHAR(16) NOT NULL DEFAULT ''pending'' COMMENT ''pending待申报 / declared已申报'',
|
||||
`PlatformCode` VARCHAR(16) NOT NULL DEFAULT '''' COMMENT ''电商平台'',
|
||||
`EcomCorpCode` VARCHAR(18) NULL COMMENT ''电商企业代码'',
|
||||
`EcomCorpName` VARCHAR(100) NULL COMMENT ''电商企业名称'',
|
||||
`ProducerCode` VARCHAR(18) NULL COMMENT ''生产销售单位代码'',
|
||||
`ProducerName` VARCHAR(100) NULL COMMENT ''生产销售单位'',
|
||||
`ClientNo` VARCHAR(48) NOT NULL DEFAULT '''' COMMENT ''客户单号(对齐 t_order.ClientNo)'',
|
||||
`BusinessType` CHAR(1) NOT NULL DEFAULT ''B'' COMMENT ''业务类型 A简化/B汇总'',
|
||||
`Region` VARCHAR(10) NULL COMMENT ''区域 SZ/other'',
|
||||
`AgentDeclare` VARCHAR(2) NULL COMMENT ''是否代理申报 是/否'',
|
||||
`ActualSellerCode` VARCHAR(18) NULL COMMENT ''实际卖家信用代码'',
|
||||
`PreRecordFlag` CHAR(1) NULL COMMENT ''汇总预录 0已预录 1未预录'',
|
||||
`IsSummarySimple` TINYINT NOT NULL DEFAULT 0 COMMENT ''是否汇总简化申报'',
|
||||
`GrossWeight` DECIMAL(19,5) NOT NULL DEFAULT 0 COMMENT ''毛重KG'',
|
||||
`Currency` VARCHAR(4) NOT NULL DEFAULT ''CNY'' COMMENT ''币制'',
|
||||
`DestCountry` VARCHAR(4) NOT NULL DEFAULT '''' COMMENT ''目的国'',
|
||||
`Freight` DECIMAL(19,5) NULL COMMENT ''运费'',
|
||||
`FreightCurrency` VARCHAR(4) NULL COMMENT ''运费币制'',
|
||||
`FreightMark` INT NULL COMMENT ''运费标志 1率2单价3总价'',
|
||||
`PackageType` INT NULL COMMENT ''包装种类代码'',
|
||||
`PackageQty` INT NULL COMMENT ''件数'',
|
||||
`ShipmentNo` VARCHAR(32) NULL COMMENT ''出货单号'',
|
||||
`CustomsOrderNo` VARCHAR(32) NULL COMMENT ''海关订单号(申报回写)'',
|
||||
`DownstreamRefNo` VARCHAR(32) NULL COMMENT ''申报回写的 referenceNo 快照'',
|
||||
`EnterpriseBizNo` VARCHAR(48) NULL COMMENT ''企业唯一编号(上游 copNo)'',
|
||||
`ManifestNo` VARCHAR(48) NULL COMMENT ''清单编号(上游 invtNo)'',
|
||||
`ClearanceTime` VARCHAR(48) NULL COMMENT ''结关日期原文'',
|
||||
`CustomsInboundTime` VARCHAR(48) NULL COMMENT ''海关入库时间原文'',
|
||||
`CustomsState` VARCHAR(4) NULL COMMENT ''海关状态'',
|
||||
`DeclareCustomsCode` VARCHAR(4) NULL COMMENT ''申报地海关代码'',
|
||||
`PlatformCorpCode` VARCHAR(18) NULL COMMENT ''电商平台代码'',
|
||||
`PlatformName` VARCHAR(100) NULL COMMENT ''电商平台名称'',
|
||||
`LogisticsCorpCode` VARCHAR(18) NULL COMMENT ''物流企业代码'',
|
||||
`LogisticsCorpName` VARCHAR(100) NULL COMMENT ''物流企业名称'',
|
||||
`IeMark` VARCHAR(4) NULL COMMENT ''进出口标记 I/E'',
|
||||
`ExportPortCode` VARCHAR(4) NULL COMMENT ''出口口岸代码'',
|
||||
`ExportDate` VARCHAR(32) NULL COMMENT ''出口日期原文'',
|
||||
`DeclareAgentCode` VARCHAR(18) NULL COMMENT ''申报企业代码'',
|
||||
`DeclareAgentName` VARCHAR(100) NULL COMMENT ''申报企业名称'',
|
||||
`TradeModeCode` VARCHAR(4) NULL COMMENT ''贸易方式代码'',
|
||||
`TransportMode` VARCHAR(4) NULL COMMENT ''运输方式代码'',
|
||||
`DeclareTime` DATETIME NULL COMMENT ''申报成功时间'',
|
||||
`ManifestSyncTime` DATETIME NULL COMMENT ''清单数据最近同步时间'',
|
||||
`FailReason` VARCHAR(512) NULL COMMENT ''最近失败原因'',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT ''软删除'',
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT ''并发戳'',
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT ''创建人'',
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ''创建时间'',
|
||||
`UpdateUserId` INT NULL COMMENT ''更新人'',
|
||||
`UpdateTime` DATETIME NULL COMMENT ''更新时间'',
|
||||
PRIMARY KEY (`Id`),
|
||||
UNIQUE KEY `uk_9610_order_id` (`OrderId`),
|
||||
KEY `idx_9610_status_ctime` (`Status`, `CreateTime`),
|
||||
KEY `idx_9610_service_no` (`ServiceNo`),
|
||||
KEY `idx_9610_client_no` (`ClientNo`),
|
||||
KEY `idx_9610_customer` (`CustomerId`, `CreateTime`),
|
||||
KEY `idx_9610_manifest` (`ManifestNo`)
|
||||
) COMMENT=''9610汇总申报单''',
|
||||
'SELECT ''skip t_9610_order'' AS msg'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- t_9610_order_item
|
||||
SET @exists := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_9610_order_item'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@exists = 0,
|
||||
'CREATE TABLE `t_9610_order_item` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT COMMENT ''主键'',
|
||||
`Customs9610OrderId` INT NOT NULL COMMENT ''t_9610_order.Id'',
|
||||
`OrderDeclarationId` INT NULL COMMENT ''溯源 t_order_declaration.Id'',
|
||||
`Sku` VARCHAR(32) NOT NULL DEFAULT '''' COMMENT ''SKU(对齐 t_order_declaration.Sku)'',
|
||||
`CnName` VARCHAR(250) NOT NULL DEFAULT '''' COMMENT ''中文品名(对齐 t_order_declaration.CnName)'',
|
||||
`ExHsCode` VARCHAR(10) NULL COMMENT ''出口海关编码(对齐 t_order_declaration.ExHsCode)'',
|
||||
`DeclareElement` VARCHAR(255) NULL COMMENT ''申报要素'',
|
||||
`Barcode` VARCHAR(255) NULL COMMENT ''条形码'',
|
||||
`DestCountry` VARCHAR(255) NULL COMMENT ''目的国(地区)代码'',
|
||||
`ItemCurrency` VARCHAR(3) NULL COMMENT ''币制'',
|
||||
`UnitPrice` DECIMAL(19,5) NOT NULL DEFAULT 0 COMMENT ''单价(对齐 t_order_declaration.UnitPrice)'',
|
||||
`Amount` DECIMAL(19,5) NULL COMMENT ''总价(对齐 t_order_declaration.Amount)'',
|
||||
`Quantity` DECIMAL(19,5) NOT NULL DEFAULT 0 COMMENT ''数量(对齐 t_order_declaration.Quantity)'',
|
||||
`LegalQty` DECIMAL(19,5) NULL COMMENT ''法定数量'',
|
||||
`Unit` VARCHAR(3) NULL COMMENT ''计量单位(对齐 t_order_declaration.Unit)'',
|
||||
`LegalUnit` VARCHAR(3) NULL COMMENT ''法定计量单位'',
|
||||
`Freight` DECIMAL(12,4) NULL COMMENT ''行运费'',
|
||||
`SortNo` INT NOT NULL DEFAULT 0 COMMENT ''排序'',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT ''软删除'',
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT ''创建人'',
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ''创建时间'',
|
||||
`UpdateUserId` INT NULL COMMENT ''更新人'',
|
||||
`UpdateTime` DATETIME NULL COMMENT ''更新时间'',
|
||||
PRIMARY KEY (`Id`),
|
||||
KEY `idx_9610_item_header` (`Customs9610OrderId`, `IsDeleted`)
|
||||
) COMMENT=''9610申报商品明细''',
|
||||
'SELECT ''skip t_9610_order_item'' AS msg'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@ -1,90 +0,0 @@
|
||||
-- 9610 其余字段按业务含义重命名(幂等 CHANGE)
|
||||
-- 申报头:Ebp→PlatformCode, Ebc*→EcomCorp*, Owner*→Producer*, FormType→BusinessType,
|
||||
-- IsAgent→AgentDeclare, ActBuyer→ActualSellerCode, SoLabelFlag→PreRecordFlag,
|
||||
-- IsSimple→IsSummarySimple, Country→DestCountry, BlNo→ShipmentNo,
|
||||
-- EbpCode/EbpName→PlatformCorpCode/PlatformName, OrderNo→CustomsOrderNo
|
||||
-- 明细:Gmodel→DeclareElement
|
||||
|
||||
SET @db := DATABASE();
|
||||
|
||||
-- helper pattern repeated
|
||||
|
||||
SET @o:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='Ebp');
|
||||
SET @n:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='PlatformCode');
|
||||
SET @sql:=IF(@o>0 AND @n=0,'ALTER TABLE `t_9610_order` CHANGE COLUMN `Ebp` `PlatformCode` VARCHAR(16) NOT NULL DEFAULT '''' COMMENT ''电商平台''','SELECT 1');
|
||||
PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
SET @o:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='EbcCode');
|
||||
SET @n:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='EcomCorpCode');
|
||||
SET @sql:=IF(@o>0 AND @n=0,'ALTER TABLE `t_9610_order` CHANGE COLUMN `EbcCode` `EcomCorpCode` VARCHAR(18) NULL COMMENT ''电商企业代码''','SELECT 1');
|
||||
PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
SET @o:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='EbcName');
|
||||
SET @n:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='EcomCorpName');
|
||||
SET @sql:=IF(@o>0 AND @n=0,'ALTER TABLE `t_9610_order` CHANGE COLUMN `EbcName` `EcomCorpName` VARCHAR(100) NULL COMMENT ''电商企业名称''','SELECT 1');
|
||||
PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
SET @o:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='OwnerCode');
|
||||
SET @n:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='ProducerCode');
|
||||
SET @sql:=IF(@o>0 AND @n=0,'ALTER TABLE `t_9610_order` CHANGE COLUMN `OwnerCode` `ProducerCode` VARCHAR(18) NULL COMMENT ''生产销售单位代码''','SELECT 1');
|
||||
PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
SET @o:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='OwnerName');
|
||||
SET @n:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='ProducerName');
|
||||
SET @sql:=IF(@o>0 AND @n=0,'ALTER TABLE `t_9610_order` CHANGE COLUMN `OwnerName` `ProducerName` VARCHAR(100) NULL COMMENT ''生产销售单位''','SELECT 1');
|
||||
PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
SET @o:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='FormType');
|
||||
SET @n:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='BusinessType');
|
||||
SET @sql:=IF(@o>0 AND @n=0,'ALTER TABLE `t_9610_order` CHANGE COLUMN `FormType` `BusinessType` CHAR(1) NOT NULL DEFAULT ''B'' COMMENT ''业务类型 A简化/B汇总''','SELECT 1');
|
||||
PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
SET @o:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='IsAgent');
|
||||
SET @n:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='AgentDeclare');
|
||||
SET @sql:=IF(@o>0 AND @n=0,'ALTER TABLE `t_9610_order` CHANGE COLUMN `IsAgent` `AgentDeclare` VARCHAR(2) NULL COMMENT ''是否代理申报 是/否''','SELECT 1');
|
||||
PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
SET @o:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='ActBuyer');
|
||||
SET @n:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='ActualSellerCode');
|
||||
SET @sql:=IF(@o>0 AND @n=0,'ALTER TABLE `t_9610_order` CHANGE COLUMN `ActBuyer` `ActualSellerCode` VARCHAR(18) NULL COMMENT ''实际卖家信用代码''','SELECT 1');
|
||||
PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
SET @o:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='SoLabelFlag');
|
||||
SET @n:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='PreRecordFlag');
|
||||
SET @sql:=IF(@o>0 AND @n=0,'ALTER TABLE `t_9610_order` CHANGE COLUMN `SoLabelFlag` `PreRecordFlag` CHAR(1) NULL COMMENT ''汇总预录 0已预录/1未预录''','SELECT 1');
|
||||
PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
SET @o:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='IsSimple');
|
||||
SET @n:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='IsSummarySimple');
|
||||
SET @sql:=IF(@o>0 AND @n=0,'ALTER TABLE `t_9610_order` CHANGE COLUMN `IsSimple` `IsSummarySimple` TINYINT NOT NULL DEFAULT 0 COMMENT ''是否汇总简化申报''','SELECT 1');
|
||||
PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
SET @o:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='Country');
|
||||
SET @n:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='DestCountry');
|
||||
SET @sql:=IF(@o>0 AND @n=0,'ALTER TABLE `t_9610_order` CHANGE COLUMN `Country` `DestCountry` VARCHAR(4) NOT NULL DEFAULT '''' COMMENT ''目的国''','SELECT 1');
|
||||
PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
SET @o:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='BlNo');
|
||||
SET @n:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='ShipmentNo');
|
||||
SET @sql:=IF(@o>0 AND @n=0,'ALTER TABLE `t_9610_order` CHANGE COLUMN `BlNo` `ShipmentNo` VARCHAR(32) NULL COMMENT ''出货单号''','SELECT 1');
|
||||
PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
SET @o:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='OrderNo');
|
||||
SET @n:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='CustomsOrderNo');
|
||||
SET @sql:=IF(@o>0 AND @n=0,'ALTER TABLE `t_9610_order` CHANGE COLUMN `OrderNo` `CustomsOrderNo` VARCHAR(32) NULL COMMENT ''海关订单号''','SELECT 1');
|
||||
PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
SET @o:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='EbpCode');
|
||||
SET @n:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='PlatformCorpCode');
|
||||
SET @sql:=IF(@o>0 AND @n=0,'ALTER TABLE `t_9610_order` CHANGE COLUMN `EbpCode` `PlatformCorpCode` VARCHAR(18) NULL COMMENT ''电商平台代码''','SELECT 1');
|
||||
PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
SET @o:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='EbpName');
|
||||
SET @n:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order' AND COLUMN_NAME='PlatformName');
|
||||
SET @sql:=IF(@o>0 AND @n=0,'ALTER TABLE `t_9610_order` CHANGE COLUMN `EbpName` `PlatformName` VARCHAR(100) NULL COMMENT ''电商平台名称''','SELECT 1');
|
||||
PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
|
||||
SET @o:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='Gmodel');
|
||||
SET @n:=(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=@db AND TABLE_NAME='t_9610_order_item' AND COLUMN_NAME='DeclareElement');
|
||||
SET @sql:=IF(@o>0 AND @n=0,'ALTER TABLE `t_9610_order_item` CHANGE COLUMN `Gmodel` `DeclareElement` VARCHAR(255) NULL COMMENT ''申报要素''','SELECT 1');
|
||||
PREPARE s FROM @sql; EXECUTE s; DEALLOCATE PREPARE s;
|
||||
@ -1,118 +0,0 @@
|
||||
-- 订单异常事件表:补齐 Handler* / ConcurrencyStamp(幂等;列已存在则自动跳过)
|
||||
-- 修复:Unknown column 'HandlerNames' / 'ConcurrencyStamp' in 'field list'
|
||||
-- MySQL 5.7+ / 8.0
|
||||
|
||||
SET @db := DATABASE();
|
||||
|
||||
-- HandlerNames
|
||||
SET @col := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_order_exception_event'
|
||||
AND COLUMN_NAME = 'HandlerNames'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@col = 0,
|
||||
'ALTER TABLE `t_order_exception_event` ADD COLUMN `HandlerNames` VARCHAR(256) NULL COMMENT ''处理人快照''',
|
||||
'SELECT ''skip HandlerNames'' AS msg'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- HandlerType
|
||||
SET @col := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_order_exception_event'
|
||||
AND COLUMN_NAME = 'HandlerType'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@col = 0,
|
||||
'ALTER TABLE `t_order_exception_event` ADD COLUMN `HandlerType` VARCHAR(16) NULL COMMENT ''处理人类型''',
|
||||
'SELECT ''skip HandlerType'' AS msg'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- HandlerUserId
|
||||
SET @col := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_order_exception_event'
|
||||
AND COLUMN_NAME = 'HandlerUserId'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@col = 0,
|
||||
'ALTER TABLE `t_order_exception_event` ADD COLUMN `HandlerUserId` INT NULL COMMENT ''处理人用户Id(customer 时为订单客户账号)''',
|
||||
'SELECT ''skip HandlerUserId'' AS msg'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ConcurrencyStamp(AggregateRoot 并发戳;避免 DEFAULT(UUID()) 触发 1674 复制不安全)
|
||||
SET @col := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_order_exception_event'
|
||||
AND COLUMN_NAME = 'ConcurrencyStamp'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@col = 0,
|
||||
'ALTER TABLE `t_order_exception_event` ADD COLUMN `ConcurrencyStamp` CHAR(36) NULL COMMENT ''并发戳''',
|
||||
'SELECT ''skip ConcurrencyStamp add'' AS msg'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_order_exception_event'
|
||||
AND COLUMN_NAME = 'ConcurrencyStamp'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@col > 0,
|
||||
'UPDATE `t_order_exception_event`
|
||||
SET `ConcurrencyStamp` = LOWER(CONCAT(
|
||||
LPAD(HEX(`Id`), 8, ''0''),
|
||||
''-0000-4000-8000-'',
|
||||
LPAD(HEX(`Id` * 9973), 12, ''0'')
|
||||
))
|
||||
WHERE `ConcurrencyStamp` IS NULL OR `ConcurrencyStamp` = ''''',
|
||||
'SELECT ''skip ConcurrencyStamp backfill'' AS msg'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @col := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_order_exception_event'
|
||||
AND COLUMN_NAME = 'ConcurrencyStamp'
|
||||
);
|
||||
SET @isNullable := (
|
||||
SELECT IFNULL(MAX(CASE WHEN IS_NULLABLE = 'YES' THEN 1 ELSE 0 END), 0)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_order_exception_event'
|
||||
AND COLUMN_NAME = 'ConcurrencyStamp'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@col > 0 AND @isNullable = 1,
|
||||
'ALTER TABLE `t_order_exception_event` MODIFY COLUMN `ConcurrencyStamp` CHAR(36) NOT NULL COMMENT ''并发戳''',
|
||||
'SELECT ''skip ConcurrencyStamp not null'' AS msg'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@ -1,70 +0,0 @@
|
||||
-- 代发订单:t_order 履约类型字段(幂等;列/索引已存在则自动跳过)
|
||||
-- MySQL 5.7+ / 8.0
|
||||
|
||||
SET @db := DATABASE();
|
||||
|
||||
-- FulfillmentType
|
||||
SET @col := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_order'
|
||||
AND COLUMN_NAME = 'FulfillmentType'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@col = 0,
|
||||
'ALTER TABLE `t_order` ADD COLUMN `FulfillmentType` VARCHAR(16) NULL DEFAULT ''direct'' COMMENT ''履约类型 direct/warehouse'' AFTER `AdditionalServices`',
|
||||
'SELECT ''skip FulfillmentType'' AS msg'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- FulfillmentSource
|
||||
SET @col := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_order'
|
||||
AND COLUMN_NAME = 'FulfillmentSource'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@col = 0,
|
||||
'ALTER TABLE `t_order` ADD COLUMN `FulfillmentSource` VARCHAR(32) NULL COMMENT ''代发来源 storeFulfill/postBind'' AFTER `FulfillmentType`',
|
||||
'SELECT ''skip FulfillmentSource'' AS msg'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- idx_ord_fulfillment_customer
|
||||
SET @idx := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_order'
|
||||
AND INDEX_NAME = 'idx_ord_fulfillment_customer'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@idx = 0,
|
||||
'ALTER TABLE `t_order` ADD KEY `idx_ord_fulfillment_customer` (`FulfillmentType`, `CustomerId`, `OrderStatus`)',
|
||||
'SELECT ''skip idx_ord_fulfillment_customer'' AS msg'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 历史:已有绑定行的订单标记为 warehouse
|
||||
UPDATE `t_order` o
|
||||
INNER JOIN (
|
||||
SELECT DISTINCT b.`OrderId`
|
||||
FROM `t_order_stock_bind` b
|
||||
WHERE b.`IsDeleted` = 0
|
||||
AND b.`Status` IN ('bound', 'deducted')
|
||||
) x ON x.`OrderId` = o.`Id`
|
||||
SET o.`FulfillmentType` = 'warehouse';
|
||||
|
||||
-- 其余空值兜底为 direct
|
||||
UPDATE `t_order`
|
||||
SET `FulfillmentType` = 'direct'
|
||||
WHERE `FulfillmentType` IS NULL OR `FulfillmentType` = '';
|
||||
@ -1,49 +0,0 @@
|
||||
-- 揽收明细:补齐运单号/客户单号快照(幂等;列已存在则跳过)
|
||||
-- 对应实体 PickupOrderLine.ServiceNo / ClientNo
|
||||
-- 报错 Unknown column 'ServiceNo' in 'field list' 时执行本脚本
|
||||
-- MySQL 5.7+ / 8.0
|
||||
|
||||
SET @db := DATABASE();
|
||||
|
||||
-- ServiceNo
|
||||
SET @col := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_pickup_order_line'
|
||||
AND COLUMN_NAME = 'ServiceNo'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@col = 0,
|
||||
'ALTER TABLE `t_pickup_order_line` ADD COLUMN `ServiceNo` VARCHAR(64) NULL COMMENT ''运单号快照(便于列表)'' AFTER `OrderId`',
|
||||
'SELECT ''skip ServiceNo'' AS msg'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ClientNo
|
||||
SET @col := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_pickup_order_line'
|
||||
AND COLUMN_NAME = 'ClientNo'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@col = 0,
|
||||
'ALTER TABLE `t_pickup_order_line` ADD COLUMN `ClientNo` VARCHAR(64) NULL COMMENT ''客户单号快照'' AFTER `ServiceNo`',
|
||||
'SELECT ''skip ClientNo'' AS msg'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 存量行从订单表回填快照(仅空值)
|
||||
UPDATE `t_pickup_order_line` pl
|
||||
INNER JOIN `t_order` o ON o.`Id` = pl.`OrderId`
|
||||
SET
|
||||
pl.`ServiceNo` = IFNULL(NULLIF(pl.`ServiceNo`, ''), o.`ServiceNo`),
|
||||
pl.`ClientNo` = IFNULL(NULLIF(pl.`ClientNo`, ''), o.`ClientNo`)
|
||||
WHERE pl.`IsDeleted` = 0
|
||||
AND (pl.`ServiceNo` IS NULL OR pl.`ServiceNo` = '' OR pl.`ClientNo` IS NULL OR pl.`ClientNo` = '');
|
||||
@ -1,20 +0,0 @@
|
||||
-- user 表:企业微信客服接入链接(幂等;列已存在则跳过)
|
||||
-- MySQL 5.7+ / 8.0
|
||||
|
||||
SET @db := DATABASE();
|
||||
|
||||
SET @col := (
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 'user'
|
||||
AND COLUMN_NAME = 'WecomKfUrl'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@col = 0,
|
||||
'ALTER TABLE `user` ADD COLUMN `WecomKfUrl` VARCHAR(512) NULL COMMENT ''企业微信客服接入链接''',
|
||||
'SELECT ''skip WecomKfUrl'' AS msg'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@ -31,7 +31,12 @@ public class SalesProductAllocationProviderInput
|
||||
public int Weight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 客户主键集合。
|
||||
/// 客户主键集合(兼容旧配置;新配置请用 MatchCondition)。
|
||||
/// </summary>
|
||||
public List<int> CustomerIds { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 排货判断条件(中文公式);空表示全部订单可用。
|
||||
/// </summary>
|
||||
public string? MatchCondition { get; set; }
|
||||
}
|
||||
|
||||
@ -41,10 +41,15 @@ public class SalesProductAllocationProviderOutput
|
||||
public int Weight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 客户主键集合。
|
||||
/// 客户主键集合(兼容旧配置)。
|
||||
/// </summary>
|
||||
public List<int> CustomerIds { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 排货判断条件(中文公式);空表示全部订单可用。
|
||||
/// </summary>
|
||||
public string? MatchCondition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间。
|
||||
/// </summary>
|
||||
|
||||
@ -41,10 +41,15 @@ public class SalesProductAllocationProviderSnapshotDto
|
||||
public int Weight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 指定客户主键集合,空表示全部。
|
||||
/// 指定客户主键集合,空表示全部(兼容旧快照)。
|
||||
/// </summary>
|
||||
public List<int> CustomerIds { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 排货判断条件(中文公式);空表示全部订单可用。
|
||||
/// </summary>
|
||||
public string? MatchCondition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务渠道编码(发布快照固化)。
|
||||
/// </summary>
|
||||
|
||||
@ -0,0 +1,109 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Oms.Application.Dtos.Customers;
|
||||
|
||||
/// <summary>
|
||||
/// 出口资质备案分页查询。
|
||||
/// </summary>
|
||||
public class CustomerExportQualificationPaginatedInput
|
||||
{
|
||||
[Newtonsoft.Json.JsonProperty("page")]
|
||||
public int Page { get; set; } = 1;
|
||||
|
||||
[Newtonsoft.Json.JsonProperty("pageSize")]
|
||||
public int PageSize { get; set; } = 50;
|
||||
|
||||
/// <summary>
|
||||
/// 客户 ID(管理端可选;不传则查全部)。
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonProperty("customerId")]
|
||||
public int? CustomerId { get; set; }
|
||||
|
||||
[Newtonsoft.Json.JsonProperty("keyword")]
|
||||
public string? Keyword { get; set; }
|
||||
|
||||
[Newtonsoft.Json.JsonProperty("onlyDefault")]
|
||||
public bool OnlyDefault { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增出口资质备案。
|
||||
/// </summary>
|
||||
public class CreateCustomerExportQualificationInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 客户 ID(管理端代操作必传;客户门户可省略)。
|
||||
/// </summary>
|
||||
public int? CustomerId { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(200)]
|
||||
public string CompanyName { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[StringLength(64)]
|
||||
public string SocialCreditCode { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[StringLength(200)]
|
||||
public string DomesticConsignor { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(64)]
|
||||
public string? DomesticConsignorCustomsCode { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(200)]
|
||||
public string ProducerName { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(64)]
|
||||
public string? ProducerCustomsCode { get; set; }
|
||||
|
||||
[StringLength(100)]
|
||||
public string? CustomsPort { get; set; }
|
||||
|
||||
public bool IsDefault { get; set; }
|
||||
|
||||
[StringLength(512)]
|
||||
public string? Remark { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新出口资质备案。
|
||||
/// </summary>
|
||||
public class UpdateCustomerExportQualificationInput : CreateCustomerExportQualificationInput
|
||||
{
|
||||
[Required]
|
||||
public int Id { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 出口资质备案出参。
|
||||
/// </summary>
|
||||
public class CustomerExportQualificationOutput
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int CustomerId { get; set; }
|
||||
|
||||
public string CompanyName { get; set; } = string.Empty;
|
||||
|
||||
public string SocialCreditCode { get; set; } = string.Empty;
|
||||
|
||||
public string DomesticConsignor { get; set; } = string.Empty;
|
||||
|
||||
public string? DomesticConsignorCustomsCode { get; set; }
|
||||
|
||||
public string ProducerName { get; set; } = string.Empty;
|
||||
|
||||
public string? ProducerCustomsCode { get; set; }
|
||||
|
||||
public string? CustomsPort { get; set; }
|
||||
|
||||
public bool IsDefault { get; set; }
|
||||
|
||||
public string? Remark { get; set; }
|
||||
|
||||
public DateTime? CreateTime { get; set; }
|
||||
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
}
|
||||
@ -187,6 +187,11 @@ public class OrderPickupCreateInput
|
||||
[Required]
|
||||
public int SenderAddressId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 揽收方向:forward / reverse。默认正向。
|
||||
/// </summary>
|
||||
public string? Direction { get; set; }
|
||||
|
||||
[Required]
|
||||
public DateTime PickupTime { get; set; }
|
||||
|
||||
|
||||
@ -301,6 +301,14 @@ public class Customs9610DeclareResult
|
||||
public int? HttpStatusCode { get; set; }
|
||||
|
||||
public string? ResponseBody { get; set; }
|
||||
|
||||
public string? Url { get; set; }
|
||||
|
||||
public string? RequestJson { get; set; }
|
||||
|
||||
public string? ServiceNo { get; set; }
|
||||
|
||||
public string? ClientNo { get; set; }
|
||||
}
|
||||
|
||||
public class Customs9610DeclareBatchOutput
|
||||
|
||||
57
src/Oms.Application/Dtos/Orders/ItfxCreateOrderOneBody.cs
Normal file
57
src/Oms.Application/Dtos/Orders/ItfxCreateOrderOneBody.cs
Normal file
@ -0,0 +1,57 @@
|
||||
namespace Oms.Application.Dtos.Orders;
|
||||
|
||||
/// <summary>
|
||||
/// itfx createOrderOne(ForwardWithSubscribe)标准请求体。
|
||||
/// 仅包含下游接口字段,不含 OMS 内部扩展参数。
|
||||
/// </summary>
|
||||
public class ItfxCreateOrderOneBody
|
||||
{
|
||||
public string? ClientNo { get; set; }
|
||||
|
||||
public string? TrackNo { get; set; }
|
||||
|
||||
public string? ServiceNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 下游产品代码:取服务渠道「预报渠道 code」(ForecastCode)。
|
||||
/// </summary>
|
||||
public string? ProductCode { get; set; }
|
||||
|
||||
public string? CargoAttribute { get; set; }
|
||||
|
||||
public int? WarehouseId { get; set; }
|
||||
|
||||
public int? CustomerId { get; set; }
|
||||
|
||||
public decimal? Length { get; set; }
|
||||
|
||||
public decimal? Width { get; set; }
|
||||
|
||||
public decimal? Height { get; set; }
|
||||
|
||||
public decimal? ActualWeight { get; set; }
|
||||
|
||||
public decimal? VolumetricWeight { get; set; }
|
||||
|
||||
public decimal? ChargeableWeight { get; set; }
|
||||
|
||||
public decimal? Cubic { get; set; }
|
||||
|
||||
public bool IsCod { get; set; }
|
||||
|
||||
public string? CodCurrency { get; set; }
|
||||
|
||||
public string? CodSum { get; set; }
|
||||
|
||||
public string? DeclaredCurrency { get; set; }
|
||||
|
||||
public string? DeclaredSum { get; set; }
|
||||
|
||||
public string? Quantity { get; set; }
|
||||
|
||||
public OrderParcelReceiverInput? Receiver { get; set; }
|
||||
|
||||
public OrderParcelSenderInput? Sender { get; set; }
|
||||
|
||||
public List<OrderParcelItemInput> Items { get; set; } = [];
|
||||
}
|
||||
@ -36,6 +36,15 @@ public class OrderAllocationTraceOutput
|
||||
|
||||
public DateTime? TraceTime { get; set; }
|
||||
|
||||
/// <summary>排货池行数。</summary>
|
||||
public int PoolCount { get; set; }
|
||||
|
||||
/// <summary>判断条件通过的行数。</summary>
|
||||
public int ConditionPassCount { get; set; }
|
||||
|
||||
/// <summary>最终进入比选的候选数。</summary>
|
||||
public int CandidateCount { get; set; }
|
||||
|
||||
public List<OrderAllocationCandidateOutput> Candidates { get; set; } = [];
|
||||
|
||||
public List<OrderAllocationSkippedOutput> Skipped { get; set; } = [];
|
||||
@ -71,6 +80,12 @@ public class OrderAllocationSkippedOutput
|
||||
|
||||
public string? ChannelName { get; set; }
|
||||
|
||||
/// <summary>跳过阶段:condition / channel / buyPrice / customer。</summary>
|
||||
public string? Stage { get; set; }
|
||||
|
||||
/// <summary>当时评价的判断条件摘要(截断)。</summary>
|
||||
public string? MatchCondition { get; set; }
|
||||
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ public class OrderDownstreamCreateOneBodyOutput
|
||||
|
||||
public string SupplierCode { get; set; } = string.Empty;
|
||||
|
||||
public OrderParcelCreateInput Body { get; set; } = new();
|
||||
public ItfxCreateOrderOneBody Body { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Oms.Application.Dtos.Orders;
|
||||
|
||||
/// <summary>
|
||||
/// 对账件重尺(downstream)Excel 导入入参。
|
||||
/// </summary>
|
||||
public class OrderDownstreamWeightImportInput
|
||||
{
|
||||
/// <summary>
|
||||
/// Excel 文件。
|
||||
/// </summary>
|
||||
public IFormFile File { get; set; } = null!;
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
namespace Oms.Application.Dtos.Orders;
|
||||
|
||||
/// <summary>
|
||||
/// 对账件重尺导入结果。
|
||||
/// </summary>
|
||||
public class OrderDownstreamWeightImportOutput
|
||||
{
|
||||
public int SuccessCount { get; set; }
|
||||
|
||||
public int FailCount { get; set; }
|
||||
|
||||
public List<OrderDownstreamWeightImportFailRow> Failures { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导入失败行。
|
||||
/// </summary>
|
||||
public class OrderDownstreamWeightImportFailRow
|
||||
{
|
||||
public int RowNumber { get; set; }
|
||||
|
||||
public string? WaybillNo { get; set; }
|
||||
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
}
|
||||
@ -65,6 +65,11 @@ public class OrderItemOutput
|
||||
/// </summary>
|
||||
public string? ExHsCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 出口申报要素。
|
||||
/// </summary>
|
||||
public string? DeclareElement { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// SKU 图片。
|
||||
/// </summary>
|
||||
|
||||
@ -170,6 +170,11 @@ public class OrderParcelCreateInput
|
||||
/// </summary>
|
||||
public DateTime? ExpectPickupTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 揽收方向:forward / reverse(上门揽收挂载揽收单时使用)。
|
||||
/// </summary>
|
||||
public string? PickupDirection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 承运商编码(可后补)。
|
||||
/// </summary>
|
||||
|
||||
@ -60,6 +60,11 @@ public class OrderParcelItemInput
|
||||
/// </summary>
|
||||
public string? ExHsCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 出口申报要素。
|
||||
/// </summary>
|
||||
public string? DeclareElement { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// SKU 图片。
|
||||
/// </summary>
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Oms.Application.Dtos.Orders;
|
||||
|
||||
/// <summary>
|
||||
@ -20,3 +22,37 @@ public class OrderPrintOutput
|
||||
/// </summary>
|
||||
public string? LabelGenerateError { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量获取面单打印地址入参。
|
||||
/// </summary>
|
||||
public class OrderPrintBatchInput
|
||||
{
|
||||
[Required]
|
||||
[MinLength(1)]
|
||||
public List<int> OrderIds { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量获取面单打印地址单条结果。
|
||||
/// </summary>
|
||||
public class OrderPrintBatchItemOutput
|
||||
{
|
||||
public int OrderId { get; set; }
|
||||
|
||||
public bool Success { get; set; }
|
||||
|
||||
public string PrintUrl { get; set; } = string.Empty;
|
||||
|
||||
public string? LabelGenerateStatus { get; set; }
|
||||
|
||||
public string? Error { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量获取面单打印地址出参。
|
||||
/// </summary>
|
||||
public class OrderPrintBatchOutput
|
||||
{
|
||||
public List<OrderPrintBatchItemOutput> Items { get; set; } = [];
|
||||
}
|
||||
|
||||
@ -14,6 +14,11 @@ public class PickupOrderCreateInput
|
||||
[Required]
|
||||
public int SenderAddressId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 揽收方向:forward 正向(客户地址→仓库)/ reverse 逆向(仓库→客户地址)。默认正向。
|
||||
/// </summary>
|
||||
public string? Direction { get; set; }
|
||||
|
||||
[Required]
|
||||
public int WarehouseId { get; set; }
|
||||
|
||||
@ -102,6 +107,11 @@ public class PickupOrderListInput
|
||||
|
||||
public int? WarehouseId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 揽收方向:forward / reverse。
|
||||
/// </summary>
|
||||
public string? Direction { get; set; }
|
||||
|
||||
public DateTime? CreateTimeStart { get; set; }
|
||||
|
||||
public DateTime? CreateTimeEnd { get; set; }
|
||||
@ -208,6 +218,11 @@ public class PickupOrderOutput
|
||||
|
||||
public int SenderAddressId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 揽收方向:forward / reverse。
|
||||
/// </summary>
|
||||
public string Direction { get; set; } = "forward";
|
||||
|
||||
public string SenderName { get; set; } = string.Empty;
|
||||
|
||||
public string? SenderPhone { get; set; }
|
||||
|
||||
@ -42,6 +42,11 @@ public class WarehouseSkuOutput
|
||||
|
||||
public string? ImHsCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 出口申报要素。
|
||||
/// </summary>
|
||||
public string? DeclareElement { get; set; }
|
||||
|
||||
public decimal? WeightG { get; set; }
|
||||
|
||||
public decimal? LengthCm { get; set; }
|
||||
@ -81,6 +86,11 @@ public class WarehouseSkuSaveInput
|
||||
|
||||
public string? ImHsCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 出口申报要素。
|
||||
/// </summary>
|
||||
public string? DeclareElement { get; set; }
|
||||
|
||||
public decimal? WeightG { get; set; }
|
||||
|
||||
public decimal? LengthCm { get; set; }
|
||||
@ -335,6 +345,15 @@ public class WarehouseInboundSummaryOutput
|
||||
public int Completed { get; set; }
|
||||
}
|
||||
|
||||
public class WarehouseParcelSummaryOutput
|
||||
{
|
||||
public int InTransit { get; set; }
|
||||
|
||||
public int Arrived { get; set; }
|
||||
|
||||
public int Stored { get; set; }
|
||||
}
|
||||
|
||||
// --- Parcel ---
|
||||
|
||||
public class WarehouseParcelListInput : WarehousePageInput
|
||||
|
||||
@ -32,4 +32,14 @@ public interface IItfxForwardClient
|
||||
string appSecret,
|
||||
object body,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 向指定绝对 URL 发起 ForwardWithSubscribe(用于写死网关地址的场景)。
|
||||
/// </summary>
|
||||
Task<ItfxForwardResult> ForwardToAbsoluteUrlAsync(
|
||||
string absoluteUrl,
|
||||
string appKey,
|
||||
string appSecret,
|
||||
object body,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@ -3,13 +3,12 @@ using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Fake.DependencyInjection;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Oms.Application.Integrations.Itfx;
|
||||
|
||||
internal sealed class ItfxForwardClient(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IOptions<ItfxGatewayOptions> options,
|
||||
IItfxGatewayConfigProvider configProvider,
|
||||
IHttpContextAccessor httpContextAccessor) : IItfxForwardClient, IScopedDependency
|
||||
{
|
||||
public const string HttpClientName = nameof(ItfxForwardClient);
|
||||
@ -18,6 +17,7 @@ internal sealed class ItfxForwardClient(
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
@ -45,10 +45,10 @@ internal sealed class ItfxForwardClient(
|
||||
object body,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var settings = options.Value;
|
||||
var settings = await configProvider.GetAsync(cancellationToken);
|
||||
if (string.IsNullOrWhiteSpace(settings.BaseUrl))
|
||||
{
|
||||
throw new InvalidOperationException("Itfx BaseUrl not configured");
|
||||
throw new InvalidOperationException("API 中心服务地址未配置,请在「其他配置」中启用 api_center");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(standardCode))
|
||||
@ -64,6 +64,23 @@ internal sealed class ItfxForwardClient(
|
||||
var root = $"{settings.BaseUrl.Trim().TrimEnd('/')}/InterfaceForward/ForwardWithSubscribe";
|
||||
var url =
|
||||
$"{root}?standardCode={Uri.EscapeDataString(standardCode.Trim())}&supplierCode={Uri.EscapeDataString(supplierCode.Trim())}";
|
||||
return await ForwardToAbsoluteUrlAsync(url, appKey, appSecret, body, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<ItfxForwardResult> ForwardToAbsoluteUrlAsync(
|
||||
string absoluteUrl,
|
||||
string appKey,
|
||||
string appSecret,
|
||||
object body,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(absoluteUrl))
|
||||
{
|
||||
throw new InvalidOperationException("Itfx absoluteUrl is empty");
|
||||
}
|
||||
|
||||
var settings = await configProvider.GetAsync(cancellationToken);
|
||||
var url = absoluteUrl.Trim();
|
||||
var requestJson = JsonSerializer.Serialize(body, JsonOptions);
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, url);
|
||||
|
||||
@ -0,0 +1,57 @@
|
||||
using Fake.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Oms.Domain.SystemAggregate.Consts;
|
||||
using Oms.Domain.SystemAggregate.Repositories;
|
||||
|
||||
namespace Oms.Application.Integrations.Itfx;
|
||||
|
||||
/// <summary>
|
||||
/// itfx / API 中心运行时配置(优先「其他配置」api_center,回退 appsettings Oms:Itfx)。
|
||||
/// </summary>
|
||||
public interface IItfxGatewayConfigProvider
|
||||
{
|
||||
Task<ItfxGatewayOptions> GetAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
internal sealed class ItfxGatewayConfigProvider(
|
||||
ISystemOtherConfigRepository repository,
|
||||
IOptions<ItfxGatewayOptions> options) : IItfxGatewayConfigProvider, IScopedDependency
|
||||
{
|
||||
public async Task<ItfxGatewayOptions> GetAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var fallback = options.Value;
|
||||
var entity = await repository.GetByConfigCodeAsync(SystemOtherConfigCodes.ApiCenter, cancellationToken);
|
||||
if (entity == null || !entity.Enabled)
|
||||
{
|
||||
return Clone(fallback);
|
||||
}
|
||||
|
||||
return new ItfxGatewayOptions
|
||||
{
|
||||
BaseUrl = Coalesce(entity.BaseUrl, fallback.BaseUrl),
|
||||
AppKey = Coalesce(entity.AppKey, fallback.AppKey),
|
||||
AppSecret = Coalesce(entity.AppSecret, fallback.AppSecret),
|
||||
TimeoutSeconds = entity.TimeoutSeconds > 0 ? entity.TimeoutSeconds : fallback.TimeoutSeconds,
|
||||
Customs9610StandardCode = fallback.Customs9610StandardCode,
|
||||
Customs9610ManifestStandardCode = fallback.Customs9610ManifestStandardCode,
|
||||
Customs9610DeclarationFormStandardCode = fallback.Customs9610DeclarationFormStandardCode,
|
||||
Customs9610SupplierCode = fallback.Customs9610SupplierCode
|
||||
};
|
||||
}
|
||||
|
||||
private static ItfxGatewayOptions Clone(ItfxGatewayOptions src) =>
|
||||
new()
|
||||
{
|
||||
BaseUrl = src.BaseUrl,
|
||||
AppKey = src.AppKey,
|
||||
AppSecret = src.AppSecret,
|
||||
TimeoutSeconds = src.TimeoutSeconds,
|
||||
Customs9610StandardCode = src.Customs9610StandardCode,
|
||||
Customs9610ManifestStandardCode = src.Customs9610ManifestStandardCode,
|
||||
Customs9610DeclarationFormStandardCode = src.Customs9610DeclarationFormStandardCode,
|
||||
Customs9610SupplierCode = src.Customs9610SupplierCode
|
||||
};
|
||||
|
||||
private static string Coalesce(string? primary, string fallback) =>
|
||||
string.IsNullOrWhiteSpace(primary) ? fallback : primary.Trim();
|
||||
}
|
||||
@ -12,6 +12,16 @@ public class ItfxGatewayOptions
|
||||
/// </summary>
|
||||
public string BaseUrl { get; set; } = "https://api.ideship.com/itfx";
|
||||
|
||||
/// <summary>
|
||||
/// 默认应用 AppKey(其他配置 api_center;GetInfo 无 Secret 时回退)。
|
||||
/// </summary>
|
||||
public string AppKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 默认应用 AppSecret(其他配置 api_center;GetInfo 无 Secret 时回退)。
|
||||
/// </summary>
|
||||
public string AppSecret { get; set; } = string.Empty;
|
||||
|
||||
public int TimeoutSeconds { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -3,13 +3,12 @@ using System.Text.Json;
|
||||
using Fake.DependencyInjection;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Oms.Application.Integrations.Itfx;
|
||||
|
||||
internal sealed class ItfxMetadataClient(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IOptions<ItfxGatewayOptions> options,
|
||||
IItfxGatewayConfigProvider configProvider,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<ItfxMetadataClient> logger) : IItfxMetadataClient, IScopedDependency
|
||||
{
|
||||
@ -67,7 +66,7 @@ internal sealed class ItfxMetadataClient(
|
||||
CancellationToken cancellationToken)
|
||||
where T : class
|
||||
{
|
||||
var settings = options.Value;
|
||||
var settings = await configProvider.GetAsync(cancellationToken);
|
||||
if (string.IsNullOrWhiteSpace(settings.BaseUrl))
|
||||
{
|
||||
logger.LogDebug("Itfx BaseUrl not configured, skip {Url}", relativeUrl);
|
||||
|
||||
@ -10,6 +10,7 @@ public static class ItfxServiceExtensions
|
||||
services.Configure<ItfxGatewayOptions>(configuration.GetSection(ItfxGatewayOptions.SectionName));
|
||||
services.AddHttpClient(ItfxMetadataClient.HttpClientName);
|
||||
services.AddHttpClient(ItfxForwardClient.HttpClientName);
|
||||
services.AddScoped<IItfxGatewayConfigProvider, ItfxGatewayConfigProvider>();
|
||||
services.AddScoped<IItfxMetadataClient, ItfxMetadataClient>();
|
||||
services.AddScoped<IItfxForwardClient, ItfxForwardClient>();
|
||||
return services;
|
||||
|
||||
@ -326,7 +326,18 @@ public partial class SalesProductService
|
||||
: (await GetAllocationCoreAsync(product.Id)).Providers
|
||||
.Select(PublishSnapshotSlimmer.SlimAllocationProvider)
|
||||
.ToList();
|
||||
var filtered = AllocationResolver.FilterProviders(providers, customerId: null);
|
||||
var filtered = AllocationResolver.FilterProvidersDetailed(
|
||||
providers,
|
||||
customerId: null,
|
||||
OrderRuleEvaluationContext.FromOrderOnly(new Order
|
||||
{
|
||||
ProductCode = product.ProductCode,
|
||||
CargoAttribute = cargoAttribute,
|
||||
WarehouseId = warehouseId,
|
||||
DeclaredSum = declaredSum,
|
||||
DeclaredCurrency = declaredCurrency,
|
||||
IsRemote = isRemote ? (byte)1 : (byte)0
|
||||
})).Passed;
|
||||
var provider = filtered.OrderBy(x => x.SerialNumber).FirstOrDefault(x => x.ServiceChannelId is > 0);
|
||||
if (provider?.ServiceChannelId is not > 0)
|
||||
{
|
||||
|
||||
@ -123,10 +123,11 @@ public partial class SalesProductService
|
||||
ProviderName = provider.ProviderName,
|
||||
ServiceChannelId = provider.ServiceChannelId,
|
||||
ServiceChannelName = provider.ServiceChannelId.HasValue && channelMap.TryGetValue(provider.ServiceChannelId.Value, out var name) ? name : null,
|
||||
Weight = provider.Weight,
|
||||
CustomerIds = OmsDocHelper.DeserializeJson<List<int>>(provider.CustomerIds) ?? [],
|
||||
CreateTime = provider.CreateTime,
|
||||
UpdateTime = OmsDocHelper.NullIfDefault(provider.UpdateTime)
|
||||
Weight = provider.Weight,
|
||||
CustomerIds = OmsDocHelper.DeserializeJson<List<int>>(provider.CustomerIds) ?? [],
|
||||
MatchCondition = provider.MatchCondition,
|
||||
CreateTime = provider.CreateTime,
|
||||
UpdateTime = OmsDocHelper.NullIfDefault(provider.UpdateTime)
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
@ -319,7 +320,8 @@ public partial class SalesProductService
|
||||
ProviderName = provider.ProviderName,
|
||||
ServiceChannelId = provider.ServiceChannelId,
|
||||
Weight = provider.Weight,
|
||||
CustomerIds = OmsDocHelper.SerializeJson(provider.CustomerIds)
|
||||
CustomerIds = OmsDocHelper.SerializeJson(provider.CustomerIds),
|
||||
MatchCondition = provider.MatchCondition
|
||||
}).ToList();
|
||||
await SalesProductRepository.ReplaceAllocationProvidersAsync(input.SalesProductId, providers);
|
||||
}
|
||||
|
||||
@ -311,7 +311,8 @@ public partial class SalesProductService : ApplicationService
|
||||
ProviderName = provider.ProviderName,
|
||||
ServiceChannelId = provider.ServiceChannelId,
|
||||
Weight = provider.Weight,
|
||||
CustomerIds = OmsDocHelper.SerializeJson(provider.CustomerIds)
|
||||
CustomerIds = OmsDocHelper.SerializeJson(provider.CustomerIds),
|
||||
MatchCondition = provider.MatchCondition
|
||||
}).ToList();
|
||||
await SalesProductRepository.ReplaceAllocationProvidersAsync(input.SalesProductId, providers);
|
||||
|
||||
|
||||
@ -64,6 +64,21 @@ public partial class ServiceChannelService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新时若目标名称被软删记录占用,则改名释放唯一键,避免 uk_scg_name 冲突。
|
||||
/// </summary>
|
||||
private async Task FreeSoftDeletedNameIfNeededAsync(string name, int currentId)
|
||||
{
|
||||
var conflict = await ServiceChannelGroupRepository.GetByNameIncludingDeletedAsync(name);
|
||||
if (conflict == null || !conflict.IsDeleted || conflict.Id == currentId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
conflict.Name = $"{name}__deleted_{conflict.Id}";
|
||||
await ServiceChannelGroupRepository.UpdateAsync(conflict);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验渠道编码唯一性。
|
||||
/// </summary>
|
||||
|
||||
@ -128,9 +128,21 @@ public partial class ServiceChannelService : ApplicationService
|
||||
public async Task CreateGroupAsync(ServiceChannelGroupSaveInput input)
|
||||
{
|
||||
await ValidateGroupInputAsync(input, null);
|
||||
var name = input.Name.Trim();
|
||||
var deleted = await ServiceChannelGroupRepository.GetByNameIncludingDeletedAsync(name);
|
||||
if (deleted != null && deleted.IsDeleted)
|
||||
{
|
||||
deleted.IsDeleted = false;
|
||||
deleted.Name = name;
|
||||
deleted.AppId = input.AppId;
|
||||
deleted.ApiServiceProviderId = input.ApiServiceProviderId;
|
||||
await ServiceChannelGroupRepository.UpdateAsync(deleted);
|
||||
return;
|
||||
}
|
||||
|
||||
await ServiceChannelGroupRepository.InsertAsync(new ServiceChannelGroup
|
||||
{
|
||||
Name = input.Name.Trim(),
|
||||
Name = name,
|
||||
AppId = input.AppId,
|
||||
ApiServiceProviderId = input.ApiServiceProviderId
|
||||
});
|
||||
@ -146,7 +158,9 @@ public partial class ServiceChannelService : ApplicationService
|
||||
{
|
||||
var group = await GetGroupOrThrowAsync(input.Id);
|
||||
await ValidateGroupInputAsync(input, group.Id);
|
||||
group.Name = input.Name.Trim();
|
||||
var name = input.Name.Trim();
|
||||
await FreeSoftDeletedNameIfNeededAsync(name, group.Id);
|
||||
group.Name = name;
|
||||
group.AppId = input.AppId;
|
||||
group.ApiServiceProviderId = input.ApiServiceProviderId;
|
||||
await ServiceChannelGroupRepository.UpdateAsync(group);
|
||||
|
||||
@ -0,0 +1,239 @@
|
||||
using System.Linq.Expressions;
|
||||
using Fake.Application;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Oms.Application.Dtos.Customers;
|
||||
using Oms.Application.Dtos.Shared;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Domain.CustomersAggregate.Entities;
|
||||
using Oms.Domain.CustomersAggregate.Repositories;
|
||||
|
||||
namespace Oms.Application.Services.Customers;
|
||||
|
||||
/// <summary>
|
||||
/// 客户出口资质备案 API。
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(GroupName = "OMS-Customer")]
|
||||
public class CustomerExportQualificationService : ApplicationService
|
||||
{
|
||||
private ICustomerRepository CustomerRepository =>
|
||||
LazyServiceProvider.GetRequiredService<ICustomerRepository>();
|
||||
|
||||
private ICustomerExportQualificationRepository ExportQualificationRepository =>
|
||||
LazyServiceProvider.GetRequiredService<ICustomerExportQualificationRepository>();
|
||||
|
||||
/// <summary>
|
||||
/// 出口资质备案分页列表。
|
||||
/// 管理端未传 customerId 时查询全部;客户门户仅查自身。
|
||||
/// </summary>
|
||||
[HttpPost("/customer/export-qualification/list")]
|
||||
public async Task<OmsPageResult<CustomerExportQualificationOutput>> GetListAsync(
|
||||
[FromBody] CustomerExportQualificationPaginatedInput input)
|
||||
{
|
||||
var customerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync(
|
||||
CustomerRepository,
|
||||
CurrentUser,
|
||||
input.CustomerId);
|
||||
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
|
||||
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
|
||||
var predicate = BuildPredicate(customerId, input);
|
||||
|
||||
var (entities, total) = await RepositoryPageHelper.GetPageAsync(
|
||||
ExportQualificationRepository,
|
||||
predicate,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
q => q.OrderByDescending(x => x.IsDefault).ThenByDescending(x => x.Id));
|
||||
|
||||
return new OmsPageResult<CustomerExportQualificationOutput>
|
||||
{
|
||||
Items = entities.Select(MapOutput).ToList(),
|
||||
Total = total
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 出口资质备案详情。
|
||||
/// </summary>
|
||||
[HttpGet("/customer/export-qualification/detail")]
|
||||
public async Task<CustomerExportQualificationOutput> GetDetailAsync(
|
||||
[FromQuery] int id,
|
||||
[FromQuery] int? customerId = null)
|
||||
{
|
||||
var entity = await GetOwnedOrThrowAsync(id, customerId);
|
||||
return MapOutput(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增出口资质备案。
|
||||
/// </summary>
|
||||
[HttpPost("/customer/export-qualification/create")]
|
||||
public async Task<object> CreateAsync([FromBody] CreateCustomerExportQualificationInput input)
|
||||
{
|
||||
var customerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
|
||||
CustomerRepository,
|
||||
CurrentUser,
|
||||
input.CustomerId);
|
||||
ValidateInput(input);
|
||||
|
||||
if (input.IsDefault)
|
||||
{
|
||||
await ExportQualificationRepository.ClearDefaultAsync(customerId);
|
||||
}
|
||||
|
||||
var entity = new CustomerExportQualification
|
||||
{
|
||||
CustomerId = customerId,
|
||||
CompanyName = input.CompanyName.Trim(),
|
||||
SocialCreditCode = input.SocialCreditCode.Trim(),
|
||||
DomesticConsignor = input.DomesticConsignor.Trim(),
|
||||
DomesticConsignorCustomsCode = TrimOrNull(input.DomesticConsignorCustomsCode),
|
||||
ProducerName = input.ProducerName.Trim(),
|
||||
ProducerCustomsCode = TrimOrNull(input.ProducerCustomsCode),
|
||||
CustomsPort = TrimOrNull(input.CustomsPort),
|
||||
IsDefault = input.IsDefault ? (byte)1 : (byte)0,
|
||||
Remark = TrimOrNull(input.Remark)
|
||||
};
|
||||
|
||||
entity = await ExportQualificationRepository.InsertAsync(entity);
|
||||
return new { id = entity.Id };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新出口资质备案。
|
||||
/// </summary>
|
||||
[HttpPost("/customer/export-qualification/update")]
|
||||
public async Task UpdateAsync([FromBody] UpdateCustomerExportQualificationInput input)
|
||||
{
|
||||
var entity = await GetOwnedOrThrowAsync(input.Id, input.CustomerId);
|
||||
ValidateInput(input);
|
||||
|
||||
if (input.IsDefault)
|
||||
{
|
||||
await ExportQualificationRepository.ClearDefaultAsync(entity.CustomerId, entity.Id);
|
||||
}
|
||||
|
||||
entity.CompanyName = input.CompanyName.Trim();
|
||||
entity.SocialCreditCode = input.SocialCreditCode.Trim();
|
||||
entity.DomesticConsignor = input.DomesticConsignor.Trim();
|
||||
entity.DomesticConsignorCustomsCode = TrimOrNull(input.DomesticConsignorCustomsCode);
|
||||
entity.ProducerName = input.ProducerName.Trim();
|
||||
entity.ProducerCustomsCode = TrimOrNull(input.ProducerCustomsCode);
|
||||
entity.CustomsPort = TrimOrNull(input.CustomsPort);
|
||||
entity.IsDefault = input.IsDefault ? (byte)1 : (byte)0;
|
||||
entity.Remark = TrimOrNull(input.Remark);
|
||||
|
||||
await ExportQualificationRepository.UpdateAsync(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除出口资质备案(软删)。
|
||||
/// </summary>
|
||||
[HttpPost("/customer/export-qualification/delete")]
|
||||
public async Task DeleteAsync([FromBody] IdInput input)
|
||||
{
|
||||
var entity = await GetOwnedOrThrowAsync(input.Id, null);
|
||||
await ExportQualificationRepository.DeleteAsync(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设为默认出口资质备案(同客户仅一条)。
|
||||
/// </summary>
|
||||
[HttpPost("/customer/export-qualification/set-default")]
|
||||
public async Task SetDefaultAsync([FromBody] IdInput input)
|
||||
{
|
||||
var entity = await GetOwnedOrThrowAsync(input.Id, null);
|
||||
await ExportQualificationRepository.ClearDefaultAsync(entity.CustomerId, entity.Id);
|
||||
entity.IsDefault = 1;
|
||||
await ExportQualificationRepository.UpdateAsync(entity);
|
||||
}
|
||||
|
||||
private async Task<CustomerExportQualification> GetOwnedOrThrowAsync(int id, int? requestedCustomerId)
|
||||
{
|
||||
var customerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync(
|
||||
CustomerRepository,
|
||||
CurrentUser,
|
||||
requestedCustomerId);
|
||||
var (hasCustomerFilter, customerIdValue) = WarehouseCustomerHelper.ToCustomerFilter(customerId);
|
||||
|
||||
var entity = hasCustomerFilter
|
||||
? await ExportQualificationRepository.GetOwnedAsync(id, customerIdValue)
|
||||
: await ExportQualificationRepository.FirstOrDefaultAsync(x => x.Id == id);
|
||||
if (entity == null)
|
||||
{
|
||||
throw new BusinessException("出口资质备案不存在");
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
private static void ValidateInput(CreateCustomerExportQualificationInput input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input.CompanyName))
|
||||
{
|
||||
throw new BusinessException("企业名称不能为空");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input.SocialCreditCode))
|
||||
{
|
||||
throw new BusinessException("企业社会信用代码不能为空");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input.DomesticConsignor))
|
||||
{
|
||||
throw new BusinessException("境内发货人不能为空");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input.ProducerName))
|
||||
{
|
||||
throw new BusinessException("生产销售单位不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
private static Expression<Func<CustomerExportQualification, bool>> BuildPredicate(
|
||||
int? customerId,
|
||||
CustomerExportQualificationPaginatedInput input)
|
||||
{
|
||||
var (hasCustomerFilter, customerIdValue) = WarehouseCustomerHelper.ToCustomerFilter(customerId);
|
||||
var keyword = input.Keyword?.Trim();
|
||||
var hasKeyword = !string.IsNullOrWhiteSpace(keyword);
|
||||
var onlyDefault = input.OnlyDefault;
|
||||
|
||||
return x =>
|
||||
(!hasCustomerFilter || x.CustomerId == customerIdValue)
|
||||
&& (!onlyDefault || x.IsDefault == 1)
|
||||
&& (!hasKeyword
|
||||
|| x.CompanyName.Contains(keyword!)
|
||||
|| x.SocialCreditCode.Contains(keyword!)
|
||||
|| x.DomesticConsignor.Contains(keyword!)
|
||||
|| (x.DomesticConsignorCustomsCode != null && x.DomesticConsignorCustomsCode.Contains(keyword!))
|
||||
|| x.ProducerName.Contains(keyword!)
|
||||
|| (x.ProducerCustomsCode != null && x.ProducerCustomsCode.Contains(keyword!))
|
||||
|| (x.CustomsPort != null && x.CustomsPort.Contains(keyword!))
|
||||
|| (x.Remark != null && x.Remark.Contains(keyword!)));
|
||||
}
|
||||
|
||||
private static CustomerExportQualificationOutput MapOutput(CustomerExportQualification entity)
|
||||
{
|
||||
return new CustomerExportQualificationOutput
|
||||
{
|
||||
Id = entity.Id,
|
||||
CustomerId = entity.CustomerId,
|
||||
CompanyName = entity.CompanyName,
|
||||
SocialCreditCode = entity.SocialCreditCode,
|
||||
DomesticConsignor = entity.DomesticConsignor,
|
||||
DomesticConsignorCustomsCode = entity.DomesticConsignorCustomsCode,
|
||||
ProducerName = entity.ProducerName,
|
||||
ProducerCustomsCode = entity.ProducerCustomsCode,
|
||||
CustomsPort = entity.CustomsPort,
|
||||
IsDefault = entity.IsDefault > 0,
|
||||
Remark = entity.Remark,
|
||||
CreateTime = entity.CreateTime,
|
||||
UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime)
|
||||
};
|
||||
}
|
||||
|
||||
private static string? TrimOrNull(string? value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,8 @@ using Oms.Application.Integrations.Itfx;
|
||||
using Oms.Application.Integrations.SmallBagTrack;
|
||||
using Oms.Application.Messaging;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Application.Storage;
|
||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||
using Oms.Domain.OrdersAggregate.Consts;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
using Oms.Domain.OrdersAggregate.Repositories;
|
||||
@ -16,6 +18,8 @@ namespace Oms.Application.Services.Orders.Batch;
|
||||
public sealed class OrderBatchActionExecutor(
|
||||
IOrderRepository orderRepository,
|
||||
ICountryRepository countryRepository,
|
||||
IServiceChannelRepository serviceChannelRepository,
|
||||
IFileStorage fileStorage,
|
||||
ISmallBagTrackOrderClient smallBagTrackOrderClient,
|
||||
ISmallBagTrackUpsertRequestBuilder smallBagTrackUpsertRequestBuilder,
|
||||
OrderDownstreamCredentialResolver downstreamCredentialResolver,
|
||||
@ -143,12 +147,12 @@ public sealed class OrderBatchActionExecutor(
|
||||
|
||||
try
|
||||
{
|
||||
OrderParcelCreateInput body;
|
||||
ItfxCreateOrderOneBody body;
|
||||
Order order;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(message.PayloadJson))
|
||||
{
|
||||
body = JsonConvert.DeserializeObject<OrderParcelCreateInput>(message.PayloadJson!)
|
||||
body = JsonConvert.DeserializeObject<ItfxCreateOrderOneBody>(message.PayloadJson!)
|
||||
?? throw new BusinessException("下游预报消息体无效");
|
||||
|
||||
// 凭证解析仍需订单上的渠道/下游绑定字段;有 payload 时只查主表一次
|
||||
@ -176,6 +180,8 @@ public sealed class OrderBatchActionExecutor(
|
||||
bundle.Declarations,
|
||||
bundle.Weights,
|
||||
countryRepository,
|
||||
serviceChannelRepository,
|
||||
fileStorage,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,8 @@ using Oms.Application.Dtos.Shared;
|
||||
using Oms.Application.Integrations.SmallBagTrack;
|
||||
using Oms.Application.Messaging;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Application.Storage;
|
||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||
using Oms.Domain.OrdersAggregate.Consts;
|
||||
using System.Linq.Expressions;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
@ -23,6 +25,8 @@ public sealed class OrderBatchJobCoordinator(
|
||||
IOrderBatchJobRepository batchJobRepository,
|
||||
IOrderRepository orderRepository,
|
||||
ICountryRepository countryRepository,
|
||||
IServiceChannelRepository serviceChannelRepository,
|
||||
IFileStorage fileStorage,
|
||||
IOrderBatchJobPublisher publisher,
|
||||
OrderBatchJobProcessor batchJobProcessor,
|
||||
IOptions<OrderBatchRabbitMqOptions> batchOptions,
|
||||
@ -649,6 +653,8 @@ public sealed class OrderBatchJobCoordinator(
|
||||
bundle.Declarations,
|
||||
bundle.Weights,
|
||||
countryRepository,
|
||||
serviceChannelRepository,
|
||||
fileStorage,
|
||||
cancellationToken);
|
||||
result[orderId] = JsonConvert.SerializeObject(body);
|
||||
}
|
||||
|
||||
@ -242,17 +242,138 @@ public class Customs9610OrderService : ApplicationService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量获取报关单(调 API 中心上游,标准码暂定)。
|
||||
/// 预报下游(写死 create9610Order / BH 网关地址)。
|
||||
/// </summary>
|
||||
[HttpPost("/order/customs-9610/fetch-declaration-form-batch")]
|
||||
public async Task<Customs9610DeclareBatchOutput> FetchDeclarationFormBatchAsync([FromBody] Customs9610IdsInput input)
|
||||
{
|
||||
return await FetchDocumentBatchAsync(
|
||||
input,
|
||||
settings => string.IsNullOrWhiteSpace(settings.Customs9610DeclarationFormStandardCode)
|
||||
? "customs9610DeclarationForm"
|
||||
: settings.Customs9610DeclarationFormStandardCode.Trim(),
|
||||
"获取报关单");
|
||||
var results = new List<Customs9610DeclareResult>();
|
||||
foreach (var id in input.Ids.Where(x => x > 0).Distinct())
|
||||
{
|
||||
try
|
||||
{
|
||||
results.Add(await DownstreamForwardOneAsync(id));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
results.Add(new Customs9610DeclareResult
|
||||
{
|
||||
Id = id,
|
||||
Success = false,
|
||||
Message = string.IsNullOrWhiteSpace(ex.Message) ? "预报下游失败" : ex.Message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new Customs9610DeclareBatchOutput
|
||||
{
|
||||
SuccessCount = results.Count(x => x.Success),
|
||||
FailCount = results.Count(x => !x.Success),
|
||||
Items = results,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写死的 9610 预报下游地址:create9610Order + supplierCode=BH。
|
||||
/// </summary>
|
||||
private const string Create9610OrderForwardUrl =
|
||||
"https://api.ideship.com/itfx/InterfaceForward/ForwardWithSubscribe?standardCode=create9610Order&supplierCode=BH";
|
||||
|
||||
private async Task<Customs9610DeclareResult> DownstreamForwardOneAsync(int id)
|
||||
{
|
||||
var header = await GetOwnedOrThrowAsync(id);
|
||||
if (!string.Equals(header.Status, Customs9610StatusCodes.Pending, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new BusinessException("仅待申报单据可预报下游");
|
||||
}
|
||||
|
||||
var items = await ItemRepository.GetByHeaderIdAsync(header.Id);
|
||||
if (items.Count == 0)
|
||||
{
|
||||
throw new BusinessException("申报商品不能为空");
|
||||
}
|
||||
|
||||
ValidateHeaderForDeclare(header);
|
||||
|
||||
var order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == header.OrderId)
|
||||
?? throw new BusinessException("关联运单不存在");
|
||||
|
||||
var creds = await CredentialResolver.ResolveAsync(order);
|
||||
var body = BuildDeclareBody(header, items);
|
||||
ItfxForwardResult forward;
|
||||
try
|
||||
{
|
||||
forward = await ItfxForwardClient.ForwardToAbsoluteUrlAsync(
|
||||
Create9610OrderForwardUrl,
|
||||
creds.AppKey,
|
||||
creds.AppSecret,
|
||||
body);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
header.FailReason = Truncate(ex.Message, 512);
|
||||
header.UpdateTime = DateTime.Now;
|
||||
await HeaderRepository.UpdateAsync(header);
|
||||
return new Customs9610DeclareResult
|
||||
{
|
||||
Id = header.Id,
|
||||
Success = false,
|
||||
Message = header.FailReason ?? "调用 API 中心失败",
|
||||
Status = header.Status,
|
||||
ServiceNo = header.ServiceNo,
|
||||
ClientNo = header.ClientNo,
|
||||
Url = Create9610OrderForwardUrl,
|
||||
};
|
||||
}
|
||||
|
||||
if (!forward.Success)
|
||||
{
|
||||
var reason = ItfxForwardResultEvaluator.BuildFailReason(
|
||||
forward.StatusCode,
|
||||
forward.ResponseBody,
|
||||
null,
|
||||
null);
|
||||
header.FailReason = Truncate(reason, 512);
|
||||
header.UpdateTime = DateTime.Now;
|
||||
await HeaderRepository.UpdateAsync(header);
|
||||
return new Customs9610DeclareResult
|
||||
{
|
||||
Id = header.Id,
|
||||
Success = false,
|
||||
Message = header.FailReason ?? "预报下游失败",
|
||||
Status = header.Status,
|
||||
ServiceNo = header.ServiceNo,
|
||||
ClientNo = header.ClientNo,
|
||||
HttpStatusCode = forward.StatusCode,
|
||||
ResponseBody = forward.ResponseBody,
|
||||
Url = forward.Url,
|
||||
RequestJson = forward.RequestJson,
|
||||
};
|
||||
}
|
||||
|
||||
TryParseDeclareResponse(forward.ResponseBody, out var customsOrderNo, out var downstreamRef);
|
||||
header.Status = Customs9610StatusCodes.Declared;
|
||||
header.CustomsOrderNo = Truncate(customsOrderNo, 32) ?? header.CustomsOrderNo;
|
||||
header.DownstreamRefNo = Truncate(downstreamRef, 32) ?? header.DownstreamRefNo;
|
||||
header.DeclareTime = DateTime.Now;
|
||||
header.FailReason = null;
|
||||
header.UpdateTime = DateTime.Now;
|
||||
await HeaderRepository.UpdateAsync(header);
|
||||
|
||||
return new Customs9610DeclareResult
|
||||
{
|
||||
Id = header.Id,
|
||||
Success = true,
|
||||
Message = "预报下游成功",
|
||||
CustomsOrderNo = header.CustomsOrderNo,
|
||||
Status = header.Status,
|
||||
ServiceNo = header.ServiceNo,
|
||||
ClientNo = header.ClientNo,
|
||||
HttpStatusCode = forward.StatusCode,
|
||||
ResponseBody = forward.ResponseBody,
|
||||
Url = forward.Url,
|
||||
RequestJson = forward.RequestJson,
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<Customs9610DeclareResult> FetchManifestOneAsync(int id)
|
||||
|
||||
@ -119,7 +119,9 @@ public class Customs9610OrderWriter : IScopedDependency
|
||||
{
|
||||
var itemName = FirstNonEmpty(d.CnName, d.EnName) ?? string.Empty;
|
||||
var sku = FirstNonEmpty(d.Sku, $"ITEM{sort + 1}")!;
|
||||
var declareElement = JoinNonEmpty(" ", d.Model, d.Specifications, d.Attributes, d.Material);
|
||||
var declareElement = FirstNonEmpty(
|
||||
d.DeclareElement,
|
||||
JoinNonEmpty(" ", d.Model, d.Specifications, d.Attributes, d.Material));
|
||||
await _itemRepository.InsertAsync(new Customs9610OrderItem
|
||||
{
|
||||
Customs9610OrderId = header.Id,
|
||||
|
||||
@ -198,6 +198,7 @@ public static class AutoTriggerFormulaMeta
|
||||
yield return ("HS编码", "t_order_declaration", "HsCode", FormulaValueType.String, false);
|
||||
yield return ("进口HS编码", "t_order_declaration", "ImHsCode", FormulaValueType.String, false);
|
||||
yield return ("出口HS编码", "t_order_declaration", "ExHsCode", FormulaValueType.String, false);
|
||||
yield return ("出口申报要素", "t_order_declaration", "DeclareElement", FormulaValueType.String, false);
|
||||
yield return ("申报类别", "t_order_declaration", "Category", FormulaValueType.String, false);
|
||||
yield return ("品牌", "t_order_declaration", "Brand", FormulaValueType.String, false);
|
||||
yield return ("品牌类型", "t_order_declaration", "BrandType", FormulaValueType.String, false);
|
||||
|
||||
@ -35,6 +35,37 @@ public sealed class OrderFileManager(
|
||||
return string.IsNullOrWhiteSpace(file?.FileUrl) ? null : fileStorage.GetPublicUrl(file.FileUrl.Trim());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量取系统面单公开地址(已存在才返回)。
|
||||
/// </summary>
|
||||
public async Task<Dictionary<int, string>> GetSystemLabelUrlsAsync(
|
||||
IReadOnlyCollection<int> orderIds,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = new Dictionary<int, string>();
|
||||
var ids = orderIds?.Where(x => x > 0).Distinct().ToList() ?? [];
|
||||
if (ids.Count == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var files = await orderFileRepository.GetByOrderIdsAndTypeAsync(
|
||||
ids,
|
||||
OrderFileTypeCodes.SystemLabel,
|
||||
cancellationToken);
|
||||
foreach (var file in files)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(file.FileUrl) || result.ContainsKey(file.OrderId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result[file.OrderId] = fileStorage.GetPublicUrl(file.FileUrl.Trim());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<OrderFileGroupedOutput> GetGroupedAsync(
|
||||
int orderId,
|
||||
bool customerView,
|
||||
|
||||
@ -82,7 +82,7 @@ public partial class OrderService
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<(Domain.OrdersAggregate.Entities.Order order, OrderParcelCreateInput body)> LoadDownstreamCreateOneContextAsync(
|
||||
private async Task<(Domain.OrdersAggregate.Entities.Order order, ItfxCreateOrderOneBody body)> LoadDownstreamCreateOneContextAsync(
|
||||
int orderId)
|
||||
{
|
||||
var bundles = await OrderRepository.GetDownstreamCreateOneBundlesAsync([orderId]);
|
||||
@ -97,7 +97,9 @@ public partial class OrderService
|
||||
bundle.Recipient,
|
||||
bundle.Declarations,
|
||||
bundle.Weights,
|
||||
CountryRepository);
|
||||
CountryRepository,
|
||||
ServiceChannelRepository,
|
||||
FileStorage);
|
||||
|
||||
return (bundle.Order, body);
|
||||
}
|
||||
|
||||
@ -743,7 +743,7 @@ public partial class OrderService
|
||||
};
|
||||
}
|
||||
|
||||
private static OrderItemOutput MapDeclaration(OrderDeclaration x)
|
||||
private OrderItemOutput MapDeclaration(OrderDeclaration x)
|
||||
{
|
||||
return new OrderItemOutput
|
||||
{
|
||||
@ -759,7 +759,8 @@ public partial class OrderService
|
||||
HsCode = x.ExHsCode ?? x.ImHsCode ?? x.HsCode,
|
||||
ImHsCode = x.ImHsCode ?? x.HsCode,
|
||||
ExHsCode = x.ExHsCode ?? x.HsCode,
|
||||
SkuImage = x.SkuImage,
|
||||
DeclareElement = x.DeclareElement,
|
||||
SkuImage = FileStorage.GetPublicUrl(x.SkuImage),
|
||||
Category = x.Category,
|
||||
Brand = x.Brand,
|
||||
BrandType = x.BrandType,
|
||||
|
||||
@ -256,11 +256,85 @@ public partial class OrderService
|
||||
public async Task<OrderOutput> RerunInboundBillingAsync(OrderActionInput input)
|
||||
{
|
||||
var order = await GetOrderOrThrowAsync(input.Id);
|
||||
EnsureReceivableMutable(order);
|
||||
await ProcessInboundBillingForBatchAsync(order.Id);
|
||||
order = await GetOrderOrThrowAsync(input.Id);
|
||||
return (await MapOrderOutputsAsync([order])).First();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载对账件重尺(downstream)导入模板。导入后只重算应付,不改应收。
|
||||
/// </summary>
|
||||
[HttpGet("/order/weight/downstream-import-template")]
|
||||
public async Task<FileContentResult> DownloadDownstreamWeightImportTemplateAsync()
|
||||
{
|
||||
var bytes = await OrderDownstreamWeightImportHelper.BuildTemplateBytesAsync();
|
||||
return new FileContentResult(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||
{
|
||||
FileDownloadName = OrderDownstreamWeightImportHelper.TemplateFileName
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导入对账件重尺:写入 Type=downstream,锁定当前渠道重算应付;不改应收。
|
||||
/// </summary>
|
||||
[HttpPost("/order/weight/downstream-import")]
|
||||
public async Task<OrderDownstreamWeightImportOutput> ImportDownstreamWeightsAsync(
|
||||
[FromForm] OrderDownstreamWeightImportInput input)
|
||||
{
|
||||
if (input.File == null || input.File.Length == 0)
|
||||
{
|
||||
throw new BusinessException("请上传 Excel 文件");
|
||||
}
|
||||
|
||||
await using var stream = input.File.OpenReadStream();
|
||||
List<(int RowNumber, OrderDownstreamWeightImportRow Row)> parsedRows;
|
||||
try
|
||||
{
|
||||
parsedRows = await OrderDownstreamWeightImportHelper.ParseImportRowsAsync(stream);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new BusinessException($"Excel 解析失败:{ex.Message}");
|
||||
}
|
||||
|
||||
if (parsedRows.Count == 0)
|
||||
{
|
||||
throw new BusinessException("Excel 中无有效数据行");
|
||||
}
|
||||
|
||||
if (parsedRows.Count > OrderDownstreamWeightImportHelper.MaxRows)
|
||||
{
|
||||
throw new BusinessException(
|
||||
$"单次最多导入 {OrderDownstreamWeightImportHelper.MaxRows} 行,当前 {parsedRows.Count} 行");
|
||||
}
|
||||
|
||||
var output = new OrderDownstreamWeightImportOutput();
|
||||
foreach (var (rowNumber, row) in parsedRows)
|
||||
{
|
||||
try
|
||||
{
|
||||
OrderDownstreamWeightImportHelper.ValidateRow(row);
|
||||
await ApplyDownstreamWeightAndRecalculatePayableAsync(row);
|
||||
output.SuccessCount++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
output.FailCount++;
|
||||
output.Failures.Add(new OrderDownstreamWeightImportFailRow
|
||||
{
|
||||
RowNumber = rowNumber,
|
||||
WaybillNo = row.WaybillNo,
|
||||
Reason = ex is BusinessException or InvalidOperationException
|
||||
? ex.Message
|
||||
: $"导入失败:{ex.Message}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询排货过程:优先返回快照;无快照或 live=true 时按当前规则现算(不落库)。
|
||||
/// </summary>
|
||||
@ -432,7 +506,11 @@ public partial class OrderService
|
||||
|
||||
var binding = await ResolveSnapshotBindingAsync(product, DateTime.Now);
|
||||
var providers = await ResolveAllocationProvidersAsync(product, binding.Rules);
|
||||
var filtered = AllocationResolver.FilterProviders(providers, order.CustomerId);
|
||||
var ruleContext = await OrderAutoTriggerService.BuildPostCommitContextAsync(order);
|
||||
var filtered = AllocationResolver.FilterProvidersDetailed(
|
||||
providers,
|
||||
order.CustomerId,
|
||||
ruleContext).Passed;
|
||||
var channelIds = new HashSet<int>();
|
||||
foreach (var provider in filtered)
|
||||
{
|
||||
@ -701,7 +779,121 @@ public partial class OrderService
|
||||
|
||||
private async Task RecalculatePayableCoreAsync(Order order)
|
||||
{
|
||||
await ReallocateCoreAsync(order, scheme: null, serviceChannelId: null, enforceReallocateGate: false);
|
||||
// 已有渠道时锁定渠道只重算金额,避免「重算应付」误换渠道清下游单号。
|
||||
await ReallocateCoreAsync(
|
||||
order,
|
||||
scheme: null,
|
||||
serviceChannelId: null,
|
||||
enforceReallocateGate: false,
|
||||
lockCurrentChannel: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入对账件重尺并锁定当前渠道重算应付(不改应收)。
|
||||
/// </summary>
|
||||
private async Task ApplyDownstreamWeightAndRecalculatePayableAsync(OrderDownstreamWeightImportRow row)
|
||||
{
|
||||
var order = await OrderRepository.GetByWaybillNoAsync(row.WaybillNo!.Trim());
|
||||
if (order == null)
|
||||
{
|
||||
throw new BusinessException("未找到对应订单,请检查单号");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(order.ChannelCode))
|
||||
{
|
||||
throw new BusinessException("订单未确认服务渠道,无法按对账重量重算应付");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(order.ProductCode))
|
||||
{
|
||||
throw new BusinessException("订单未绑定销售产品,无法重算应付");
|
||||
}
|
||||
|
||||
var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim());
|
||||
if (product == null)
|
||||
{
|
||||
throw new BusinessException("销售产品不存在");
|
||||
}
|
||||
|
||||
var weights = (await OrderRepository.GetWeightsByOrderIdsAsync([order.Id]))
|
||||
.OrderBy(x => x.Id)
|
||||
.ToList();
|
||||
var downstream = weights.FirstOrDefault(x =>
|
||||
string.Equals(x.Type, OrderWeightTypeCodes.Downstream, StringComparison.OrdinalIgnoreCase));
|
||||
if (downstream == null)
|
||||
{
|
||||
throw new BusinessException("订单缺少下游件重尺记录");
|
||||
}
|
||||
|
||||
// 尺寸可选:未填则沿用下游已有,仍缺则回落收货→发货→预报。
|
||||
var length = row.Length ?? downstream.Length;
|
||||
var width = row.Width ?? downstream.Width;
|
||||
var height = row.Height ?? downstream.Height;
|
||||
if (length is not > 0 || width is not > 0 || height is not > 0)
|
||||
{
|
||||
FillDimensionsFromFallback(weights, ref length, ref width, ref height);
|
||||
}
|
||||
|
||||
if (length is not > 0 || width is not > 0 || height is not > 0)
|
||||
{
|
||||
throw new BusinessException("件重尺尺寸不完整,请在 Excel 填写长/宽/高,或先完善订单收货/发货尺寸");
|
||||
}
|
||||
|
||||
ApplyMeasuredDimensions(downstream, length.Value, width.Value, height.Value, row.ActualWeight!.Value);
|
||||
|
||||
var snapshotBinding = await ResolveSnapshotBindingFromOrderAsync(product, order);
|
||||
await ApplyChargeableWeightAsync(product, snapshotBinding.Rules, downstream);
|
||||
await OrderRepository.ReplaceWeightsAsync(order.Id, weights);
|
||||
|
||||
await ReallocateCoreAsync(
|
||||
order,
|
||||
scheme: null,
|
||||
serviceChannelId: null,
|
||||
enforceReallocateGate: false,
|
||||
lockCurrentChannel: true,
|
||||
preferDownstreamWeight: true);
|
||||
}
|
||||
|
||||
private static void FillDimensionsFromFallback(
|
||||
IReadOnlyList<OrderWeight> weights,
|
||||
ref decimal? length,
|
||||
ref decimal? width,
|
||||
ref decimal? height)
|
||||
{
|
||||
foreach (var type in new[]
|
||||
{
|
||||
OrderWeightTypeCodes.Receive,
|
||||
OrderWeightTypeCodes.Ship,
|
||||
OrderWeightTypeCodes.Forecast
|
||||
})
|
||||
{
|
||||
var src = weights.FirstOrDefault(x =>
|
||||
string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase));
|
||||
if (src == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (length is not > 0)
|
||||
{
|
||||
length = src.Length;
|
||||
}
|
||||
|
||||
if (width is not > 0)
|
||||
{
|
||||
width = src.Width;
|
||||
}
|
||||
|
||||
if (height is not > 0)
|
||||
{
|
||||
height = src.Height;
|
||||
}
|
||||
|
||||
if (length is > 0 && width is > 0 && height is > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -711,7 +903,9 @@ public partial class OrderService
|
||||
Order order,
|
||||
string? scheme,
|
||||
int? serviceChannelId,
|
||||
bool enforceReallocateGate = true)
|
||||
bool enforceReallocateGate = true,
|
||||
bool lockCurrentChannel = false,
|
||||
bool preferDownstreamWeight = false)
|
||||
{
|
||||
string? schemeOverride = null;
|
||||
int? forceChannelId = null;
|
||||
@ -723,6 +917,18 @@ public partial class OrderService
|
||||
? AllocationSchemeCodes.Designated
|
||||
: normalizedScheme;
|
||||
}
|
||||
else if (lockCurrentChannel && !string.IsNullOrWhiteSpace(order.ChannelCode))
|
||||
{
|
||||
var channel = await ServiceChannelRepository.FirstOrDefaultAsync(x =>
|
||||
x.ChannelCode == order.ChannelCode.Trim());
|
||||
if (channel == null)
|
||||
{
|
||||
throw new BusinessException("订单服务渠道不存在,无法重算应付");
|
||||
}
|
||||
|
||||
forceChannelId = channel.Id;
|
||||
schemeOverride = AllocationSchemeCodes.Designated;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(order.ProductCode))
|
||||
{
|
||||
@ -736,13 +942,18 @@ public partial class OrderService
|
||||
}
|
||||
|
||||
var weights = await OrderRepository.GetWeightsByOrderIdsAsync([order.Id]);
|
||||
var weightRow = ResolvePayableWeightRow(order, weights);
|
||||
var weightRow = preferDownstreamWeight
|
||||
? weights.FirstOrDefault(x =>
|
||||
string.Equals(x.Type, OrderWeightTypeCodes.Downstream, StringComparison.OrdinalIgnoreCase))
|
||||
?? throw new BusinessException("缺少件重尺类型 downstream")
|
||||
: ResolvePayableWeightRow(order, weights);
|
||||
if (!weightRow.ChargeableWeight.HasValue || weightRow.ChargeableWeight <= 0)
|
||||
{
|
||||
throw new BusinessException("计费重无效,请先完善件重尺");
|
||||
}
|
||||
|
||||
// 重新排货:产品规则/分区与渠道买价均取当前有效发布,避免沿用下单时旧快照。
|
||||
// 对账重算 / 普通重算应付:沿用订单快照绑定(分区等),买价仍由排货评估取当前有效发布。
|
||||
var snapshotBinding = enforceReallocateGate
|
||||
? await ResolveSnapshotBindingAsync(product, DateTime.Now)
|
||||
: await ResolveSnapshotBindingFromOrderAsync(product, order);
|
||||
@ -753,6 +964,11 @@ public partial class OrderService
|
||||
snapshotBinding);
|
||||
|
||||
var (_, chargingStage) = ResolveReceivableBillingStage(order);
|
||||
// 对账件重尺导入:费用阶段记 outbound(终稿成本),与入库应收阶段解耦。
|
||||
if (preferDownstreamWeight)
|
||||
{
|
||||
chargingStage = OrderFeeChargingStageCodes.Outbound;
|
||||
}
|
||||
|
||||
await ConfirmServiceChannelAndPayableAsync(
|
||||
order,
|
||||
@ -823,11 +1039,12 @@ public partial class OrderService
|
||||
var weightType = chargingStage switch
|
||||
{
|
||||
OrderFeeChargingStageCodes.Inbound => OrderWeightTypeCodes.Receive,
|
||||
_ => OrderWeightTypeCodes.Downstream
|
||||
OrderFeeChargingStageCodes.Outbound => OrderWeightTypeCodes.Ship,
|
||||
_ => OrderWeightTypeCodes.Forecast
|
||||
};
|
||||
|
||||
return weights.FirstOrDefault(x => x.Type == weightType)
|
||||
?? weights.FirstOrDefault(x => x.Type == OrderWeightTypeCodes.Downstream)
|
||||
return weights.FirstOrDefault(x =>
|
||||
string.Equals(x.Type, weightType, StringComparison.OrdinalIgnoreCase))
|
||||
?? throw new BusinessException($"缺少件重尺类型 {weightType}");
|
||||
}
|
||||
|
||||
@ -894,7 +1111,7 @@ public partial class OrderService
|
||||
_ => (OrderWeightTypeCodes.Forecast, OrderFeeChargingStageCodes.Forecast)
|
||||
};
|
||||
|
||||
private static OrderDeclaration MapDeclarationInput(int orderId, OrderParcelItemInput x)
|
||||
private OrderDeclaration MapDeclarationInput(int orderId, OrderParcelItemInput x)
|
||||
{
|
||||
return new OrderDeclaration
|
||||
{
|
||||
@ -911,7 +1128,8 @@ public partial class OrderService
|
||||
HsCode = x.ExHsCode ?? x.ImHsCode,
|
||||
ImHsCode = x.ImHsCode,
|
||||
ExHsCode = x.ExHsCode ?? x.ImHsCode,
|
||||
SkuImage = x.SkuImage,
|
||||
DeclareElement = x.DeclareElement,
|
||||
SkuImage = FileStorage.ToStorageKey(x.SkuImage),
|
||||
Category = x.Category,
|
||||
Brand = x.Brand,
|
||||
BrandType = x.BrandType,
|
||||
|
||||
@ -774,7 +774,8 @@ public partial class OrderService
|
||||
{
|
||||
if (replaceExistingReceivable)
|
||||
{
|
||||
await OrderRepository.DeleteReceivableFeesByStageAsync(order.Id, chargingStage);
|
||||
// 有效应收只保留一套:清空全部阶段后再写入当前阶段,避免预报+入库叠算。
|
||||
await OrderRepository.DeleteReceivableFeesAsync(order.Id);
|
||||
}
|
||||
|
||||
await OrderRepository.UpsertFeesAsync(order.Id, fees);
|
||||
@ -860,6 +861,8 @@ public partial class OrderService
|
||||
public IReadOnlyList<string> ZoneKeys { get; init; } = [];
|
||||
public List<AllocationResolver.ChannelCandidate> Candidates { get; init; } = [];
|
||||
public List<OrderAllocationSkippedOutput> Skipped { get; init; } = [];
|
||||
public int PoolCount { get; init; }
|
||||
public int ConditionPassCount { get; init; }
|
||||
public AllocationResolver.ChannelCandidate? Selected { get; set; }
|
||||
public string? FailReason { get; set; }
|
||||
}
|
||||
@ -874,10 +877,18 @@ public partial class OrderService
|
||||
string? schemeOverride,
|
||||
int? forceServiceChannelId)
|
||||
{
|
||||
var skipped = new List<OrderAllocationSkippedOutput>();
|
||||
var warehouseName = await ResolveWarehouseNameAsync(order.WarehouseId);
|
||||
var providers = await ResolveAllocationProvidersAsync(product, snapshotBinding.Rules);
|
||||
var filtered = AllocationResolver.FilterProviders(providers, order.CustomerId);
|
||||
// 判断条件需要客户名、件重尺等,与自动触发共用完整上下文
|
||||
var ruleContext = await OrderAutoTriggerService.BuildPostCommitContextAsync(order);
|
||||
var filterResult = AllocationResolver.FilterProvidersDetailed(
|
||||
providers,
|
||||
order.CustomerId,
|
||||
ruleContext);
|
||||
var skipped = filterResult.Skipped.ToList();
|
||||
var filtered = filterResult.Passed;
|
||||
var poolCount = filterResult.PoolCount;
|
||||
var conditionPassCount = filterResult.ConditionPassCount;
|
||||
|
||||
var productScheme = snapshotBinding.Rules.Product?.Scheme
|
||||
?? product.Scheme
|
||||
@ -899,52 +910,68 @@ public partial class OrderService
|
||||
? AllocationSchemeCodes.CostLowest
|
||||
: storedScheme;
|
||||
|
||||
AllocationEvaluation BaseEval(string? failReason = null) => new()
|
||||
{
|
||||
StoredScheme = storedScheme,
|
||||
SelectScheme = selectScheme,
|
||||
SchemeSource = schemeSource,
|
||||
ChargeableWeight = chargeableWeight,
|
||||
ChargingStage = chargingStage,
|
||||
ZoneKeys = zoneKeys,
|
||||
Skipped = skipped,
|
||||
PoolCount = poolCount,
|
||||
ConditionPassCount = conditionPassCount,
|
||||
FailReason = failReason
|
||||
};
|
||||
|
||||
if (forceServiceChannelId is > 0)
|
||||
{
|
||||
var before = filtered;
|
||||
filtered = filtered
|
||||
var designated = filtered
|
||||
.Where(x => x.ServiceChannelId == forceServiceChannelId.Value)
|
||||
.ToList();
|
||||
if (filtered.Count == 0)
|
||||
if (designated.Count == 0)
|
||||
{
|
||||
foreach (var p in before.Where(x => x.ServiceChannelId != forceServiceChannelId))
|
||||
var poolRow = providers.FirstOrDefault(x => x.ServiceChannelId == forceServiceChannelId.Value);
|
||||
var failReason = poolRow == null
|
||||
? "指定渠道不在该销售产品排货池"
|
||||
: "指定渠道不满足判断条件";
|
||||
if (poolRow != null
|
||||
&& skipped.All(x => x.ServiceChannelId != forceServiceChannelId.Value))
|
||||
{
|
||||
// 非指定渠道不记跳过;指定不在池内单独失败
|
||||
skipped.Add(new OrderAllocationSkippedOutput
|
||||
{
|
||||
ServiceChannelId = forceServiceChannelId.Value,
|
||||
ChannelCode = poolRow.ChannelCode,
|
||||
ChannelName = poolRow.ServiceChannelName ?? poolRow.ProviderName,
|
||||
Stage = AllocationResolver.SkipStageCondition,
|
||||
MatchCondition = AllocationResolver.TruncateMatchCondition(poolRow.MatchCondition),
|
||||
Reason = failReason
|
||||
});
|
||||
}
|
||||
|
||||
return new AllocationEvaluation
|
||||
{
|
||||
StoredScheme = storedScheme,
|
||||
SelectScheme = selectScheme,
|
||||
SchemeSource = schemeSource,
|
||||
ChargeableWeight = chargeableWeight,
|
||||
ChargingStage = chargingStage,
|
||||
ZoneKeys = zoneKeys,
|
||||
Skipped = skipped,
|
||||
FailReason = "指定渠道不在该销售产品排货池或当前客户不可用"
|
||||
};
|
||||
return BaseEval(failReason);
|
||||
}
|
||||
|
||||
filtered = designated;
|
||||
}
|
||||
|
||||
if (filtered.Count == 0)
|
||||
{
|
||||
return new AllocationEvaluation
|
||||
{
|
||||
StoredScheme = storedScheme,
|
||||
SelectScheme = selectScheme,
|
||||
SchemeSource = schemeSource,
|
||||
ChargeableWeight = chargeableWeight,
|
||||
ChargingStage = chargingStage,
|
||||
ZoneKeys = zoneKeys,
|
||||
Skipped = skipped,
|
||||
FailReason = "排货规则未匹配到可用服务渠道"
|
||||
};
|
||||
var conditionMiss = skipped.Count(x =>
|
||||
x.Stage is AllocationResolver.SkipStageCondition or AllocationResolver.SkipStageCustomer);
|
||||
var failReason = poolCount == 0
|
||||
? "排货规则未配置可用服务渠道"
|
||||
: conditionMiss > 0
|
||||
? $"排货规则未匹配到可用服务渠道(条件未命中 {conditionMiss}/{poolCount})"
|
||||
: "排货规则未匹配到可用服务渠道";
|
||||
return BaseEval(failReason);
|
||||
}
|
||||
|
||||
// 应付买价取「当前」有效发布(PublishTime<=Now),不用下单时间,避免重新排货/入库漏掉后续发布的买价。
|
||||
var bindTime = DateTime.Now;
|
||||
var candidates = new List<AllocationResolver.ChannelCandidate>();
|
||||
var recipient = (await OrderRepository.GetRecipientsByOrderIdsAsync([order.Id])).FirstOrDefault();
|
||||
var recipient = ruleContext.Recipient
|
||||
?? (await OrderRepository.GetRecipientsByOrderIdsAsync([order.Id])).FirstOrDefault();
|
||||
var cargoAttribute = order.CargoAttribute ?? CargoAttributeCodes.General;
|
||||
|
||||
foreach (var provider in filtered)
|
||||
@ -956,6 +983,8 @@ public partial class OrderService
|
||||
skipped.Add(new OrderAllocationSkippedOutput
|
||||
{
|
||||
ServiceChannelId = channelId,
|
||||
Stage = AllocationResolver.SkipStageChannel,
|
||||
MatchCondition = AllocationResolver.TruncateMatchCondition(provider.MatchCondition),
|
||||
Reason = "服务渠道不存在"
|
||||
});
|
||||
continue;
|
||||
@ -968,6 +997,8 @@ public partial class OrderService
|
||||
ServiceChannelId = channel.Id,
|
||||
ChannelCode = channel.ChannelCode,
|
||||
ChannelName = channel.ChannelName,
|
||||
Stage = AllocationResolver.SkipStageChannel,
|
||||
MatchCondition = AllocationResolver.TruncateMatchCondition(provider.MatchCondition),
|
||||
Reason = "服务渠道未启用"
|
||||
});
|
||||
continue;
|
||||
@ -1016,6 +1047,8 @@ public partial class OrderService
|
||||
ServiceChannelId = channel.Id,
|
||||
ChannelCode = channel.ChannelCode,
|
||||
ChannelName = channel.ChannelName,
|
||||
Stage = AllocationResolver.SkipStageBuyPrice,
|
||||
MatchCondition = AllocationResolver.TruncateMatchCondition(provider.MatchCondition),
|
||||
Reason = string.IsNullOrWhiteSpace(miss)
|
||||
? $"未匹配到有效渠道买价(版本 {channelPriceBinding.PriceVersion})"
|
||||
: $"未匹配到有效渠道买价(版本 {channelPriceBinding.PriceVersion}):{miss}"
|
||||
@ -1034,7 +1067,17 @@ public partial class OrderService
|
||||
|
||||
if (candidates.Count == 0)
|
||||
{
|
||||
var detail = skipped.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x.Reason))?.Reason;
|
||||
var buyPriceMiss = skipped.Count(x => x.Stage == AllocationResolver.SkipStageBuyPrice);
|
||||
var detail = skipped.FirstOrDefault(x =>
|
||||
x.Stage == AllocationResolver.SkipStageBuyPrice
|
||||
&& !string.IsNullOrWhiteSpace(x.Reason))?.Reason;
|
||||
var failReason = chargeableWeight <= 0
|
||||
? "计费重无效,无法计算应付费用"
|
||||
: buyPriceMiss > 0
|
||||
? (string.IsNullOrWhiteSpace(detail)
|
||||
? $"排货规则未匹配到有效渠道买价(买价未命中 {buyPriceMiss})"
|
||||
: $"排货规则未匹配到有效渠道买价;{detail}")
|
||||
: "排货规则未匹配到有效渠道买价";
|
||||
return new AllocationEvaluation
|
||||
{
|
||||
StoredScheme = storedScheme,
|
||||
@ -1045,11 +1088,9 @@ public partial class OrderService
|
||||
ZoneKeys = zoneKeys,
|
||||
Candidates = candidates,
|
||||
Skipped = skipped,
|
||||
FailReason = chargeableWeight <= 0
|
||||
? "计费重无效,无法计算应付费用"
|
||||
: string.IsNullOrWhiteSpace(detail)
|
||||
? "排货规则未匹配到有效渠道买价"
|
||||
: $"排货规则未匹配到有效渠道买价;{detail}"
|
||||
PoolCount = poolCount,
|
||||
ConditionPassCount = conditionPassCount,
|
||||
FailReason = failReason
|
||||
};
|
||||
}
|
||||
|
||||
@ -1064,6 +1105,8 @@ public partial class OrderService
|
||||
ZoneKeys = zoneKeys,
|
||||
Candidates = candidates,
|
||||
Skipped = skipped,
|
||||
PoolCount = poolCount,
|
||||
ConditionPassCount = conditionPassCount,
|
||||
Selected = selected
|
||||
};
|
||||
}
|
||||
@ -1126,6 +1169,9 @@ public partial class OrderService
|
||||
SelectedChannelName = evaluation.Selected?.Channel.ChannelName,
|
||||
FailReason = evaluation.FailReason,
|
||||
TraceTime = DateTime.Now,
|
||||
PoolCount = evaluation.PoolCount,
|
||||
ConditionPassCount = evaluation.ConditionPassCount,
|
||||
CandidateCount = candidates.Count,
|
||||
Candidates = candidates,
|
||||
Skipped = evaluation.Skipped
|
||||
};
|
||||
@ -1254,7 +1300,8 @@ public partial class OrderService
|
||||
ServiceChannelId = provider.ServiceChannelId,
|
||||
ServiceChannelName = provider.ServiceChannelId.HasValue && channelMap.TryGetValue(provider.ServiceChannelId.Value, out var name) ? name : null,
|
||||
Weight = provider.Weight,
|
||||
CustomerIds = OmsDocHelper.DeserializeJson<List<int>>(provider.CustomerIds) ?? []
|
||||
CustomerIds = OmsDocHelper.DeserializeJson<List<int>>(provider.CustomerIds) ?? [],
|
||||
MatchCondition = provider.MatchCondition
|
||||
})).ToList();
|
||||
await AllocationProviderSnapshotEnricher.EnrichAsync(providers);
|
||||
}
|
||||
@ -1965,7 +2012,8 @@ public partial class OrderService
|
||||
HsCode = x.ExHsCode ?? x.ImHsCode,
|
||||
ImHsCode = x.ImHsCode,
|
||||
ExHsCode = x.ExHsCode,
|
||||
SkuImage = x.SkuImage,
|
||||
DeclareElement = x.DeclareElement,
|
||||
SkuImage = FileStorage.ToStorageKey(x.SkuImage),
|
||||
Category = x.Category,
|
||||
Brand = x.Brand,
|
||||
BrandType = x.BrandType,
|
||||
|
||||
@ -22,7 +22,7 @@ public partial class OrderService
|
||||
|
||||
if (input.SenderAddressId <= 0)
|
||||
{
|
||||
throw new BusinessException("发件地址不能为空");
|
||||
throw new BusinessException("揽收地址不能为空");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input.ProductCode))
|
||||
@ -55,15 +55,15 @@ public partial class OrderService
|
||||
var senderAddress = await CustomerSenderAddressRepository.GetOwnedAsync(input.SenderAddressId, customerId);
|
||||
if (senderAddress == null)
|
||||
{
|
||||
throw new BusinessException("发件地址不存在");
|
||||
throw new BusinessException("揽收地址不存在");
|
||||
}
|
||||
|
||||
var addressType = string.IsNullOrWhiteSpace(senderAddress.AddressType)
|
||||
? CustomerAddressTypeCodes.Sender
|
||||
: senderAddress.AddressType;
|
||||
if (!string.Equals(addressType, CustomerAddressTypeCodes.Sender, StringComparison.OrdinalIgnoreCase))
|
||||
if (!CustomerAddressTypeCodes.IsPickupEligible(addressType))
|
||||
{
|
||||
throw new BusinessException("揽收订单仅可选择发件地址");
|
||||
throw new BusinessException("揽收订单请选择「国内揽收」类型地址");
|
||||
}
|
||||
|
||||
var warehouse = await WarehouseAddressRepository.FirstOrDefaultAsync(x => x.Id == input.WarehouseId);
|
||||
|
||||
@ -318,6 +318,89 @@ public partial class OrderService : ApplicationService
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量获取打印面单地址(同步返回,避免前端逐单请求)。
|
||||
/// </summary>
|
||||
/// <param name="input">入参:订单 ID 列表。</param>
|
||||
/// <returns>各订单打印结果。</returns>
|
||||
[HttpPost("/order/print/batch")]
|
||||
public async Task<OrderPrintBatchOutput> PrintBatchAsync([FromBody] OrderPrintBatchInput input)
|
||||
{
|
||||
var orderIds = (input.OrderIds ?? []).Where(x => x > 0).Distinct().ToList();
|
||||
if (orderIds.Count == 0)
|
||||
{
|
||||
throw new BusinessException("请选择要打印的订单");
|
||||
}
|
||||
|
||||
const int maxBatchSize = 100;
|
||||
if (orderIds.Count > maxBatchSize)
|
||||
{
|
||||
throw new BusinessException($"单次最多获取 {maxBatchSize} 张面单");
|
||||
}
|
||||
|
||||
var output = new OrderPrintBatchOutput
|
||||
{
|
||||
Items = new List<OrderPrintBatchItemOutput>(orderIds.Count)
|
||||
};
|
||||
|
||||
// 已有系统面单一律批量查出,避免 N 次附件查询
|
||||
var existingUrls = await OrderFileManager.GetSystemLabelUrlsAsync(orderIds);
|
||||
var pendingIds = new List<int>();
|
||||
foreach (var orderId in orderIds)
|
||||
{
|
||||
if (existingUrls.TryGetValue(orderId, out var url) && !string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
output.Items.Add(new OrderPrintBatchItemOutput
|
||||
{
|
||||
OrderId = orderId,
|
||||
Success = true,
|
||||
PrintUrl = url,
|
||||
LabelGenerateStatus = LabelGenerateStatusCodes.Ready
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
pendingIds.Add(orderId);
|
||||
}
|
||||
}
|
||||
|
||||
// 未就绪的走单票打印逻辑(含即时补生成),单条失败不影响其它
|
||||
foreach (var orderId in pendingIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var print = await PrintAsync(new OrderActionInput { Id = orderId });
|
||||
var ok = !string.IsNullOrWhiteSpace(print.PrintUrl);
|
||||
output.Items.Add(new OrderPrintBatchItemOutput
|
||||
{
|
||||
OrderId = orderId,
|
||||
Success = ok,
|
||||
PrintUrl = print.PrintUrl ?? string.Empty,
|
||||
LabelGenerateStatus = print.LabelGenerateStatus,
|
||||
Error = ok ? null : (print.LabelGenerateError ?? "未获取到打印文件")
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
output.Items.Add(new OrderPrintBatchItemOutput
|
||||
{
|
||||
OrderId = orderId,
|
||||
Success = false,
|
||||
Error = ex is BusinessException be ? be.Message : ex.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 保持与入参顺序一致
|
||||
var byId = output.Items.ToDictionary(x => x.OrderId);
|
||||
output.Items = orderIds
|
||||
.Select(id => byId.TryGetValue(id, out var item)
|
||||
? item
|
||||
: new OrderPrintBatchItemOutput { OrderId = id, Success = false, Error = "未处理" })
|
||||
.ToList();
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导出订单。
|
||||
/// </summary>
|
||||
@ -459,7 +542,7 @@ public partial class OrderService : ApplicationService
|
||||
|
||||
if (input.SenderAddressId is not > 0)
|
||||
{
|
||||
output.PickupCreateError = "上门揽收需选择发件地址";
|
||||
output.PickupCreateError = "上门揽收需选择揽收地址";
|
||||
return;
|
||||
}
|
||||
|
||||
@ -482,6 +565,7 @@ public partial class OrderService : ApplicationService
|
||||
{
|
||||
CustomerId = input.CustomerId,
|
||||
SenderAddressId = input.SenderAddressId.Value,
|
||||
Direction = PickupDirectionCodes.NormalizeOrDefault(input.PickupDirection),
|
||||
WarehouseId = input.WarehouseId.Value,
|
||||
CarrierCode = input.CarrierCode,
|
||||
CarrierService = input.CarrierService,
|
||||
|
||||
@ -23,13 +23,71 @@ public static class PickupOrderParcelBuilder
|
||||
? $"PK{DateTime.Now:yyyyMMddHHmmss}{Random.Shared.Next(1000, 9999)}"
|
||||
: input.ClientNo.Trim();
|
||||
|
||||
var receiverName = string.IsNullOrWhiteSpace(warehouse.Recipient)
|
||||
var warehouseContact = string.IsNullOrWhiteSpace(warehouse.Recipient)
|
||||
? warehouse.WarehouseName
|
||||
: warehouse.Recipient!.Trim();
|
||||
var street = string.IsNullOrWhiteSpace(warehouse.ShippingAddress)
|
||||
var warehouseStreet = string.IsNullOrWhiteSpace(warehouse.ShippingAddress)
|
||||
? warehouse.WarehouseName
|
||||
: warehouse.ShippingAddress!.Trim();
|
||||
|
||||
var isReverse = string.Equals(
|
||||
PickupDirectionCodes.NormalizeOrDefault(input.Direction),
|
||||
PickupDirectionCodes.Reverse,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// 正向:客户揽收地址 → 仓库;逆向:仓库 → 客户揽收地址
|
||||
var sender = isReverse
|
||||
? new OrderParcelSenderInput
|
||||
{
|
||||
SenderName = warehouseContact,
|
||||
Phone = warehouse.ContactPhone,
|
||||
CountryCode = "CN",
|
||||
StreetAddress1 = warehouseStreet,
|
||||
ZipCode = "000000"
|
||||
}
|
||||
: new OrderParcelSenderInput
|
||||
{
|
||||
SenderName = senderAddress.SenderName,
|
||||
Company = senderAddress.Company,
|
||||
Phone = senderAddress.Phone,
|
||||
TelePhone = senderAddress.TelePhone,
|
||||
Email = senderAddress.Email,
|
||||
CountryCode = senderCountryCode ?? "CN",
|
||||
Province = senderAddress.Province,
|
||||
City = senderAddress.City,
|
||||
ZipCode = senderAddress.ZipCode,
|
||||
StreetAddress1 = senderAddress.StreetAddress1,
|
||||
StreetAddress2 = senderAddress.StreetAddress2,
|
||||
StreetAddress3 = senderAddress.StreetAddress3,
|
||||
HouseNumber = senderAddress.HouseNumber
|
||||
};
|
||||
|
||||
var receiver = isReverse
|
||||
? new OrderParcelReceiverInput
|
||||
{
|
||||
RecipientsName = senderAddress.SenderName,
|
||||
CompanyName = senderAddress.Company,
|
||||
Phone = senderAddress.Phone,
|
||||
TelePhone = senderAddress.TelePhone,
|
||||
Email = senderAddress.Email,
|
||||
CountryCode = senderCountryCode ?? "CN",
|
||||
Province = senderAddress.Province,
|
||||
City = senderAddress.City,
|
||||
ZipCode = senderAddress.ZipCode ?? "000000",
|
||||
StreetAddress1 = senderAddress.StreetAddress1,
|
||||
StreetAddress2 = senderAddress.StreetAddress2,
|
||||
StreetAddress3 = senderAddress.StreetAddress3,
|
||||
StreetNumber = senderAddress.HouseNumber
|
||||
}
|
||||
: new OrderParcelReceiverInput
|
||||
{
|
||||
RecipientsName = warehouseContact,
|
||||
Phone = warehouse.ContactPhone,
|
||||
CountryCode = "CN",
|
||||
ZipCode = "000000",
|
||||
StreetAddress1 = warehouseStreet
|
||||
};
|
||||
|
||||
return new OrderParcelCreateInput
|
||||
{
|
||||
RequestId = string.IsNullOrWhiteSpace(input.RequestId) ? null : input.RequestId.Trim(),
|
||||
@ -47,30 +105,12 @@ public static class PickupOrderParcelBuilder
|
||||
DeclaredCurrency = "CNY",
|
||||
DeclaredSum = 1,
|
||||
Quantity = 1,
|
||||
Sender = new OrderParcelSenderInput
|
||||
{
|
||||
SenderName = senderAddress.SenderName,
|
||||
Company = senderAddress.Company,
|
||||
Phone = senderAddress.Phone,
|
||||
TelePhone = senderAddress.TelePhone,
|
||||
Email = senderAddress.Email,
|
||||
CountryCode = senderCountryCode ?? "CN",
|
||||
Province = senderAddress.Province,
|
||||
City = senderAddress.City,
|
||||
ZipCode = senderAddress.ZipCode,
|
||||
StreetAddress1 = senderAddress.StreetAddress1,
|
||||
StreetAddress2 = senderAddress.StreetAddress2,
|
||||
StreetAddress3 = senderAddress.StreetAddress3,
|
||||
HouseNumber = senderAddress.HouseNumber
|
||||
},
|
||||
Receiver = new OrderParcelReceiverInput
|
||||
{
|
||||
RecipientsName = receiverName,
|
||||
Phone = warehouse.ContactPhone,
|
||||
CountryCode = "CN",
|
||||
ZipCode = "000000",
|
||||
StreetAddress1 = street
|
||||
},
|
||||
InboundMethod = OrderInboundMethodCodes.DoorPickup,
|
||||
SenderAddressId = input.SenderAddressId,
|
||||
ExpectPickupTime = input.PickupTime,
|
||||
PickupDirection = PickupDirectionCodes.NormalizeOrDefault(input.Direction),
|
||||
Sender = sender,
|
||||
Receiver = receiver,
|
||||
Items =
|
||||
[
|
||||
new OrderParcelItemInput
|
||||
|
||||
@ -2,7 +2,9 @@ using Fake.Application;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Oms.Application.Dtos.Orders;
|
||||
using Oms.Application.Dtos.Shared;
|
||||
using Oms.Application.Integrations.Itfx;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||
using Oms.Domain.CustomersAggregate.Repositories;
|
||||
using Oms.Domain.OrdersAggregate.Consts;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
@ -48,6 +50,21 @@ public class PickupOrderService : ApplicationService
|
||||
private ICountryRepository CountryRepository =>
|
||||
LazyServiceProvider.GetRequiredService<ICountryRepository>();
|
||||
|
||||
private ISalesProductRepository SalesProductRepository =>
|
||||
LazyServiceProvider.GetRequiredService<ISalesProductRepository>();
|
||||
|
||||
private IServiceChannelRepository ServiceChannelRepository =>
|
||||
LazyServiceProvider.GetRequiredService<IServiceChannelRepository>();
|
||||
|
||||
private IServiceChannelGroupRepository ServiceChannelGroupRepository =>
|
||||
LazyServiceProvider.GetRequiredService<IServiceChannelGroupRepository>();
|
||||
|
||||
private IItfxForwardClient ItfxForwardClient =>
|
||||
LazyServiceProvider.GetRequiredService<IItfxForwardClient>();
|
||||
|
||||
private IItfxMetadataClient ItfxMetadataClient =>
|
||||
LazyServiceProvider.GetRequiredService<IItfxMetadataClient>();
|
||||
|
||||
/// <summary>
|
||||
/// 创建揽收单并挂载物流单(原子:全部校验通过才落库)。
|
||||
/// </summary>
|
||||
@ -63,7 +80,7 @@ public class PickupOrderService : ApplicationService
|
||||
|
||||
if (input.SenderAddressId <= 0)
|
||||
{
|
||||
throw new BusinessException("发件地址不能为空");
|
||||
throw new BusinessException("揽收地址不能为空");
|
||||
}
|
||||
|
||||
if (input.WarehouseId <= 0)
|
||||
@ -76,6 +93,8 @@ public class PickupOrderService : ApplicationService
|
||||
throw new BusinessException("期望揽收时间不能为空");
|
||||
}
|
||||
|
||||
var direction = PickupDirectionCodes.NormalizeOrDefault(input.Direction);
|
||||
|
||||
var orderIds = (input.OrderIds ?? []).Where(x => x > 0).Distinct().ToList();
|
||||
if (orderIds.Count == 0)
|
||||
{
|
||||
@ -85,15 +104,15 @@ public class PickupOrderService : ApplicationService
|
||||
var senderAddress = await CustomerSenderAddressRepository.GetOwnedAsync(input.SenderAddressId, customerId);
|
||||
if (senderAddress == null)
|
||||
{
|
||||
throw new BusinessException("发件地址不存在");
|
||||
throw new BusinessException("揽收地址不存在");
|
||||
}
|
||||
|
||||
var addressType = string.IsNullOrWhiteSpace(senderAddress.AddressType)
|
||||
? CustomerAddressTypeCodes.Sender
|
||||
: senderAddress.AddressType;
|
||||
if (!string.Equals(addressType, CustomerAddressTypeCodes.Sender, StringComparison.OrdinalIgnoreCase))
|
||||
if (!CustomerAddressTypeCodes.IsPickupEligible(addressType))
|
||||
{
|
||||
throw new BusinessException("揽收单仅可选择发件地址");
|
||||
throw new BusinessException("揽收单请选择「国内揽收」类型地址");
|
||||
}
|
||||
|
||||
var warehouse = await WarehouseAddressRepository.FirstOrDefaultAsync(x => x.Id == input.WarehouseId);
|
||||
@ -198,6 +217,7 @@ public class PickupOrderService : ApplicationService
|
||||
OrganizationId = orgId,
|
||||
SecretLevel = secretLevel,
|
||||
SenderAddressId = senderAddress.Id,
|
||||
Direction = direction,
|
||||
SenderName = senderAddress.SenderName ?? string.Empty,
|
||||
SenderCompany = senderAddress.Company,
|
||||
SenderCountry = senderAddress.Country,
|
||||
@ -408,9 +428,13 @@ public class PickupOrderService : ApplicationService
|
||||
var keyword = NormalizeOptional(input.Keyword);
|
||||
var status = NormalizeOptional(input.Status);
|
||||
var payStatus = NormalizeOptional(input.PayStatus);
|
||||
var direction = string.IsNullOrWhiteSpace(input.Direction)
|
||||
? null
|
||||
: PickupDirectionCodes.NormalizeOrDefault(input.Direction);
|
||||
|
||||
var all = await PickupOrderRepository.GetListAsync(x =>
|
||||
(customerId == null || x.CustomerId == customerId.Value) &&
|
||||
(direction == null || x.Direction == direction) &&
|
||||
(status == null || x.Status == status) &&
|
||||
(payStatus == null || x.PayStatus == payStatus) &&
|
||||
(input.WarehouseId == null || input.WarehouseId <= 0 || x.WarehouseId == input.WarehouseId.Value) &&
|
||||
@ -438,15 +462,22 @@ public class PickupOrderService : ApplicationService
|
||||
/// 揽收单状态计数(Tab 徽章)。
|
||||
/// </summary>
|
||||
[HttpGet("/order/pickup-order/status-counts")]
|
||||
public async Task<PickupOrderStatusCountsOutput> GetStatusCountsAsync([FromQuery] int? customerId)
|
||||
public async Task<PickupOrderStatusCountsOutput> GetStatusCountsAsync(
|
||||
[FromQuery] int? customerId,
|
||||
[FromQuery] string? direction = null)
|
||||
{
|
||||
var resolvedCustomerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync(
|
||||
CustomerRepository,
|
||||
CurrentUser,
|
||||
customerId);
|
||||
|
||||
var directionFilter = string.IsNullOrWhiteSpace(direction)
|
||||
? null
|
||||
: PickupDirectionCodes.NormalizeOrDefault(direction);
|
||||
|
||||
var all = await PickupOrderRepository.GetListAsync(x =>
|
||||
resolvedCustomerId == null || x.CustomerId == resolvedCustomerId.Value);
|
||||
(resolvedCustomerId == null || x.CustomerId == resolvedCustomerId.Value) &&
|
||||
(directionFilter == null || x.Direction == directionFilter));
|
||||
|
||||
var terminal = new[] { PickupOrderStatusCodes.Cancelled, PickupOrderStatusCodes.Failed };
|
||||
var pendingPayment = all.Count(x =>
|
||||
@ -888,6 +919,157 @@ public class PickupOrderService : ApplicationService
|
||||
await PickupOrderRepository.UpdateAsync(pickup);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 揽收单预报下游:createOrderOne 请求体全部从 t_pickup_order 组装(不读关联物流单)。
|
||||
/// </summary>
|
||||
[HttpPost("/order/pickup-order/downstream-forward")]
|
||||
public async Task<OrderDownstreamForwardResult> DownstreamForwardAsync([FromBody] PickupOrderIdInput input)
|
||||
{
|
||||
var pickup = await GetOwnedPickupOrThrowAsync(input.Id);
|
||||
if (string.IsNullOrWhiteSpace(pickup.CarrierService))
|
||||
{
|
||||
throw new BusinessException("请先选择承运服务后再预报下游");
|
||||
}
|
||||
|
||||
var warehouse = await WarehouseAddressRepository.FirstOrDefaultAsync(x => x.Id == pickup.WarehouseId);
|
||||
if (warehouse == null)
|
||||
{
|
||||
throw new BusinessException("仓库不存在");
|
||||
}
|
||||
|
||||
var (body, _) = await PickupOrderDownstreamCreateOneBodyBuilder.BuildAsync(
|
||||
pickup,
|
||||
warehouse,
|
||||
SalesProductRepository,
|
||||
ServiceChannelRepository);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(body.ProductCode))
|
||||
{
|
||||
throw new BusinessException("无法解析下游 productCode,请检查承运服务对应渠道的预报编码");
|
||||
}
|
||||
|
||||
var creds = await ResolvePickupDownstreamCredentialsAsync(pickup);
|
||||
var forward = await ItfxForwardClient.ForwardCreateOrderOneAsync(
|
||||
creds.SupplierCode,
|
||||
creds.AppKey,
|
||||
creds.AppSecret,
|
||||
body);
|
||||
|
||||
if (forward.Success
|
||||
&& ItfxCreateOrderOneDataParser.TryParse(forward.ResponseBody, out var data)
|
||||
&& data != null)
|
||||
{
|
||||
var changed = false;
|
||||
var waybill = !string.IsNullOrWhiteSpace(data.TrackNo)
|
||||
? data.TrackNo.Trim()
|
||||
: data.ServiceNo?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(waybill)
|
||||
&& !string.Equals(pickup.CarrierWaybillNo, waybill, StringComparison.Ordinal))
|
||||
{
|
||||
pickup.CarrierWaybillNo = waybill;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(pickup.CarrierCode)
|
||||
&& !string.IsNullOrWhiteSpace(creds.SupplierCode))
|
||||
{
|
||||
pickup.CarrierCode = creds.SupplierCode;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
await PickupOrderRepository.UpdateAsync(pickup);
|
||||
}
|
||||
}
|
||||
|
||||
return new OrderDownstreamForwardResult
|
||||
{
|
||||
Success = forward.Success,
|
||||
SupplierCode = creds.SupplierCode,
|
||||
Url = forward.Url,
|
||||
RequestJson = forward.RequestJson,
|
||||
StatusCode = forward.StatusCode,
|
||||
ResponseBody = forward.ResponseBody,
|
||||
DownstreamServiceNo = pickup.CarrierWaybillNo,
|
||||
TrackNo = pickup.CarrierWaybillNo,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 凭证:优先揽收单 CarrierCode 作为 supplierCode;AppKey/Secret 仍按承运服务→渠道分组解析。
|
||||
/// </summary>
|
||||
private async Task<OrderDownstreamCredentials> ResolvePickupDownstreamCredentialsAsync(PickupOrder pickup)
|
||||
{
|
||||
var salesProductCode = pickup.CarrierService!.Trim();
|
||||
var product = await SalesProductRepository.GetByProductCodeAsync(salesProductCode);
|
||||
if (product == null)
|
||||
{
|
||||
throw new BusinessException($"未找到承运服务对应销售产品「{salesProductCode}」");
|
||||
}
|
||||
|
||||
var providers = await SalesProductRepository.GetAllocationProvidersAsync(product.Id);
|
||||
var channelId = providers
|
||||
.Where(x => x.ServiceChannelId is > 0)
|
||||
.OrderBy(x => x.SerialNumber)
|
||||
.Select(x => x.ServiceChannelId!.Value)
|
||||
.FirstOrDefault();
|
||||
if (channelId <= 0)
|
||||
{
|
||||
throw new BusinessException($"销售产品「{salesProductCode}」未配置服务渠道");
|
||||
}
|
||||
|
||||
var channel = await ServiceChannelRepository.FirstOrDefaultAsync(x => x.Id == channelId);
|
||||
if (channel == null)
|
||||
{
|
||||
throw new BusinessException("承运服务关联的服务渠道不存在");
|
||||
}
|
||||
|
||||
if (!channel.GroupId.HasValue || channel.GroupId.Value <= 0)
|
||||
{
|
||||
throw new BusinessException($"渠道「{channel.ChannelCode}」未关联服务商分组");
|
||||
}
|
||||
|
||||
var group = await ServiceChannelGroupRepository.FirstOrDefaultAsync(x => x.Id == channel.GroupId.Value);
|
||||
if (group == null)
|
||||
{
|
||||
throw new BusinessException("服务商分组不存在或已删除");
|
||||
}
|
||||
|
||||
if (group.AppId <= 0 || group.ApiServiceProviderId <= 0)
|
||||
{
|
||||
throw new BusinessException($"服务商「{group.Name}」未配置应用或 API 服务商");
|
||||
}
|
||||
|
||||
var appInfo = await ItfxMetadataClient.GetAppAsync(group.AppId);
|
||||
var appKey = appInfo?.AppKey?.Trim();
|
||||
var appSecret = appInfo?.AppSecret?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(appKey) || string.IsNullOrWhiteSpace(appSecret))
|
||||
{
|
||||
throw new BusinessException(
|
||||
"无法获取应用 AppKey/AppSecret,请检查服务商应用配置或「其他配置-API中心服务」");
|
||||
}
|
||||
|
||||
var supplierCode = pickup.CarrierCode?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(supplierCode))
|
||||
{
|
||||
var sp = await ItfxMetadataClient.GetServiceProviderAsync(group.ApiServiceProviderId);
|
||||
if (sp == null || string.IsNullOrWhiteSpace(sp.Code))
|
||||
{
|
||||
throw new BusinessException("API 服务商编码(supplierCode)未配置");
|
||||
}
|
||||
|
||||
supplierCode = sp.Code.Trim();
|
||||
}
|
||||
|
||||
return new OrderDownstreamCredentials
|
||||
{
|
||||
SupplierCode = supplierCode,
|
||||
AppKey = appKey,
|
||||
AppSecret = appSecret
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<string> GeneratePickupNoAsync()
|
||||
{
|
||||
for (var i = 0; i < 8; i++)
|
||||
@ -929,6 +1111,7 @@ public class PickupOrderService : ApplicationService
|
||||
OrganizationId = entity.OrganizationId,
|
||||
SecretLevel = entity.SecretLevel,
|
||||
SenderAddressId = entity.SenderAddressId,
|
||||
Direction = PickupDirectionCodes.NormalizeOrDefault(entity.Direction),
|
||||
SenderName = entity.SenderName,
|
||||
SenderPhone = entity.SenderPhone,
|
||||
SenderTelePhone = entity.SenderTelePhone,
|
||||
|
||||
@ -171,6 +171,23 @@ public class SystemOtherConfigService : ApplicationService
|
||||
throw new BusinessException("轨迹服务 AppSecret 不能为空");
|
||||
}
|
||||
}
|
||||
else if (string.Equals(configCode, SystemOtherConfigCodes.ApiCenter, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input.BaseUrl))
|
||||
{
|
||||
throw new BusinessException("API 中心服务地址不能为空");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(input.AppKey))
|
||||
{
|
||||
throw new BusinessException("API 中心应用 AppKey 不能为空");
|
||||
}
|
||||
|
||||
if (excludeId == null && ShouldKeepExistingSecret(input.AppSecret))
|
||||
{
|
||||
throw new BusinessException("API 中心应用 AppSecret 不能为空");
|
||||
}
|
||||
}
|
||||
else if (string.Equals(configCode, SystemOtherConfigCodes.WeChatMp, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input.AppKey))
|
||||
|
||||
@ -79,6 +79,43 @@ public partial class WarehouseService
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询包裹生命周期状态汇总。
|
||||
/// </summary>
|
||||
/// <param name="customerId">客户 ID(可选)。</param>
|
||||
/// <param name="warehouseId">仓库 ID(可选)。</param>
|
||||
/// <returns>各状态数量汇总。</returns>
|
||||
[HttpGet("/warehouse/parcel/summary")]
|
||||
public async Task<WarehouseParcelSummaryOutput> GetParcelSummaryAsync(
|
||||
[FromQuery] int? customerId = null,
|
||||
[FromQuery] int? warehouseId = null)
|
||||
{
|
||||
var resolvedCustomerId = await ResolveListCustomerIdAsync(customerId);
|
||||
var (hasCustomerFilter, customerIdValue) = CustomerFilter(resolvedCustomerId);
|
||||
var hasWarehouseFilter = warehouseId.HasValue;
|
||||
var warehouseIdValue = warehouseId.GetValueOrDefault();
|
||||
|
||||
var inTransit = await ParcelRepository.CountAsync(x =>
|
||||
(!hasCustomerFilter || x.CustomerId == customerIdValue)
|
||||
&& (!hasWarehouseFilter || x.WarehouseId == warehouseIdValue)
|
||||
&& x.LifecycleStatus == WarehouseParcelLifecycleCodes.InTransit);
|
||||
var arrived = await ParcelRepository.CountAsync(x =>
|
||||
(!hasCustomerFilter || x.CustomerId == customerIdValue)
|
||||
&& (!hasWarehouseFilter || x.WarehouseId == warehouseIdValue)
|
||||
&& x.LifecycleStatus == WarehouseParcelLifecycleCodes.Arrived);
|
||||
var stored = await ParcelRepository.CountAsync(x =>
|
||||
(!hasCustomerFilter || x.CustomerId == customerIdValue)
|
||||
&& (!hasWarehouseFilter || x.WarehouseId == warehouseIdValue)
|
||||
&& x.LifecycleStatus == WarehouseParcelLifecycleCodes.Stored);
|
||||
|
||||
return new WarehouseParcelSummaryOutput
|
||||
{
|
||||
InTransit = inTransit,
|
||||
Arrived = arrived,
|
||||
Stored = stored
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 规范化列表状态筛选:空 / all 表示不按状态过滤;已知编码统一为规范值。
|
||||
/// </summary>
|
||||
@ -209,9 +246,9 @@ public partial class WarehouseService
|
||||
/// </summary>
|
||||
[HttpPost("/warehouse/parcel/upload-image")]
|
||||
[Consumes("multipart/form-data")]
|
||||
public async Task<object> UploadParcelImageAsync(IFormFile file)
|
||||
public async Task<object> UploadParcelImageAsync(IFormFile file, [FromQuery] int? customerId = null)
|
||||
{
|
||||
await ResolveMutationCustomerIdAsync(null);
|
||||
await ResolveMutationCustomerIdAsync(customerId);
|
||||
var key = await OmsFileUploadHelper.SaveWarehouseParcelImageAsync(file, FileStorage);
|
||||
var url = FileStorage.GetPublicUrl(key);
|
||||
return new { key, url, fileUrl = key };
|
||||
|
||||
@ -122,6 +122,7 @@ public partial class WarehouseService
|
||||
entity.UnitPrice = input.UnitPrice;
|
||||
entity.ExHsCode = string.IsNullOrWhiteSpace(input.ExHsCode) ? null : input.ExHsCode.Trim();
|
||||
entity.ImHsCode = string.IsNullOrWhiteSpace(input.ImHsCode) ? null : input.ImHsCode.Trim();
|
||||
entity.DeclareElement = string.IsNullOrWhiteSpace(input.DeclareElement) ? null : input.DeclareElement.Trim();
|
||||
entity.WeightG = input.WeightG;
|
||||
entity.LengthCm = input.LengthCm;
|
||||
entity.WidthCm = input.WidthCm;
|
||||
@ -164,9 +165,9 @@ public partial class WarehouseService
|
||||
/// </summary>
|
||||
[HttpPost("/warehouse/sku/upload-image")]
|
||||
[Consumes("multipart/form-data")]
|
||||
public async Task<object> UploadSkuImageAsync(IFormFile file)
|
||||
public async Task<object> UploadSkuImageAsync(IFormFile file, [FromQuery] int? customerId = null)
|
||||
{
|
||||
await ResolveMutationCustomerIdAsync(null);
|
||||
await ResolveMutationCustomerIdAsync(customerId);
|
||||
var key = await OmsFileUploadHelper.SaveWarehouseSkuImageAsync(file, FileStorage);
|
||||
var url = FileStorage.GetPublicUrl(key);
|
||||
return new { key, url, fileUrl = key };
|
||||
@ -231,6 +232,7 @@ public partial class WarehouseService
|
||||
UnitPrice = entity.UnitPrice,
|
||||
ExHsCode = entity.ExHsCode,
|
||||
ImHsCode = entity.ImHsCode,
|
||||
DeclareElement = entity.DeclareElement,
|
||||
WeightG = entity.WeightG,
|
||||
LengthCm = entity.LengthCm,
|
||||
WidthCm = entity.WidthCm,
|
||||
@ -252,6 +254,7 @@ public partial class WarehouseService
|
||||
UnitPrice = entity.UnitPrice,
|
||||
ExHsCode = entity.ExHsCode,
|
||||
ImHsCode = entity.ImHsCode,
|
||||
DeclareElement = entity.DeclareElement,
|
||||
WeightG = entity.WeightG,
|
||||
LengthCm = entity.LengthCm,
|
||||
WidthCm = entity.WidthCm,
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
using Oms.Application.Dtos.Channels;
|
||||
using Oms.Application.Dtos.Orders;
|
||||
using Oms.Application.Services.Orders.Issues;
|
||||
using Oms.Domain.ChannelsAggregate.Consts;
|
||||
using Oms.Domain.ChannelsAggregate.Entities;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
@ -10,6 +12,13 @@ namespace Oms.Application.Shared;
|
||||
/// </summary>
|
||||
public static class AllocationResolver
|
||||
{
|
||||
public const string SkipStageCondition = "condition";
|
||||
public const string SkipStageCustomer = "customer";
|
||||
public const string SkipStageChannel = "channel";
|
||||
public const string SkipStageBuyPrice = "buyPrice";
|
||||
|
||||
private const int MatchConditionTraceMaxLen = 200;
|
||||
|
||||
public sealed class ChannelCandidate
|
||||
{
|
||||
public SalesProductAllocationProviderSnapshotDto Provider { get; init; } = null!;
|
||||
@ -22,16 +31,140 @@ public static class AllocationResolver
|
||||
public IReadOnlyList<OrderFee> PayableFees { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed class FilterProvidersResult
|
||||
{
|
||||
public List<SalesProductAllocationProviderSnapshotDto> Passed { get; init; } = [];
|
||||
|
||||
public List<OrderAllocationSkippedOutput> Skipped { get; init; } = [];
|
||||
|
||||
public int PoolCount { get; init; }
|
||||
|
||||
public int ConditionPassCount { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧调用:仅按客户名单过滤(无判断条件上下文时)。
|
||||
/// </summary>
|
||||
public static List<SalesProductAllocationProviderSnapshotDto> FilterProviders(
|
||||
IEnumerable<SalesProductAllocationProviderSnapshotDto> providers,
|
||||
int? customerId) =>
|
||||
providers
|
||||
FilterProvidersDetailed(providers, customerId, context: null).Passed;
|
||||
|
||||
/// <summary>
|
||||
/// 先按判断条件(或兼容客户名单)筛出可用渠道;空条件恒真。
|
||||
/// </summary>
|
||||
public static FilterProvidersResult FilterProvidersDetailed(
|
||||
IEnumerable<SalesProductAllocationProviderSnapshotDto> providers,
|
||||
int? customerId,
|
||||
OrderRuleEvaluationContext? context)
|
||||
{
|
||||
var pool = providers
|
||||
.Where(x => x.ServiceChannelId.HasValue && x.ServiceChannelId > 0)
|
||||
.Where(x => x.CustomerIds.Count == 0
|
||||
|| (customerId.HasValue && x.CustomerIds.Contains(customerId.Value)))
|
||||
.OrderBy(x => x.SerialNumber)
|
||||
.ToList();
|
||||
|
||||
var passed = new List<SalesProductAllocationProviderSnapshotDto>();
|
||||
var skipped = new List<OrderAllocationSkippedOutput>();
|
||||
|
||||
foreach (var provider in pool)
|
||||
{
|
||||
var match = EvaluateProviderMatch(provider, customerId, context);
|
||||
if (match.Pass)
|
||||
{
|
||||
passed.Add(provider);
|
||||
continue;
|
||||
}
|
||||
|
||||
skipped.Add(new OrderAllocationSkippedOutput
|
||||
{
|
||||
ServiceChannelId = provider.ServiceChannelId,
|
||||
ChannelCode = provider.ChannelCode,
|
||||
ChannelName = provider.ServiceChannelName ?? provider.ProviderName,
|
||||
Stage = match.Stage,
|
||||
MatchCondition = TruncateMatchCondition(provider.MatchCondition),
|
||||
Reason = match.Reason
|
||||
});
|
||||
}
|
||||
|
||||
// 同一渠道多行命中时保留权重更高、序号更小的一行
|
||||
var deduped = DeduplicateByChannel(passed);
|
||||
|
||||
return new FilterProvidersResult
|
||||
{
|
||||
Passed = deduped,
|
||||
Skipped = skipped,
|
||||
PoolCount = pool.Count,
|
||||
ConditionPassCount = passed.Count
|
||||
};
|
||||
}
|
||||
|
||||
private static List<SalesProductAllocationProviderSnapshotDto> DeduplicateByChannel(
|
||||
List<SalesProductAllocationProviderSnapshotDto> passed)
|
||||
{
|
||||
return passed
|
||||
.GroupBy(x => x.ServiceChannelId!.Value)
|
||||
.Select(g => g
|
||||
.OrderByDescending(x => x.Weight)
|
||||
.ThenBy(x => x.SerialNumber)
|
||||
.First())
|
||||
.OrderBy(x => x.SerialNumber)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private readonly record struct MatchOutcome(bool Pass, string Stage, string Reason);
|
||||
|
||||
private static MatchOutcome EvaluateProviderMatch(
|
||||
SalesProductAllocationProviderSnapshotDto provider,
|
||||
int? customerId,
|
||||
OrderRuleEvaluationContext? context)
|
||||
{
|
||||
var formula = provider.MatchCondition?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(formula)
|
||||
&& !string.Equals(formula, "无", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
return new MatchOutcome(false, SkipStageCondition, "判断条件缺少订单上下文");
|
||||
}
|
||||
|
||||
if (!ChineseFormulaEvaluator.TryEvaluate(formula, context, out var hit))
|
||||
{
|
||||
return new MatchOutcome(false, SkipStageCondition, "判断条件公式无效");
|
||||
}
|
||||
|
||||
if (!hit)
|
||||
{
|
||||
return new MatchOutcome(false, SkipStageCondition, "判断条件未命中");
|
||||
}
|
||||
|
||||
return new MatchOutcome(true, SkipStageCondition, string.Empty);
|
||||
}
|
||||
|
||||
// 无公式:兼容旧 CustomerIds 名单
|
||||
if (provider.CustomerIds.Count > 0)
|
||||
{
|
||||
if (customerId.HasValue && provider.CustomerIds.Contains(customerId.Value))
|
||||
{
|
||||
return new MatchOutcome(true, SkipStageCustomer, string.Empty);
|
||||
}
|
||||
|
||||
return new MatchOutcome(false, SkipStageCustomer, "客户不在名单内");
|
||||
}
|
||||
|
||||
return new MatchOutcome(true, SkipStageCondition, string.Empty);
|
||||
}
|
||||
|
||||
public static string TruncateMatchCondition(string? condition)
|
||||
{
|
||||
var text = condition?.Trim() ?? string.Empty;
|
||||
if (text.Length <= MatchConditionTraceMaxLen)
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
return text[..MatchConditionTraceMaxLen] + "…";
|
||||
}
|
||||
|
||||
public static ServiceChannel SelectChannel(string? scheme, IReadOnlyList<ChannelCandidate> candidates) =>
|
||||
SelectCandidate(scheme, candidates).Channel;
|
||||
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
using Oms.Application.Dtos.Orders;
|
||||
using Oms.Application.Storage;
|
||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||
using Oms.Domain.OrdersAggregate.Consts;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
using Oms.Domain.SystemAggregate.Repositories;
|
||||
@ -6,17 +8,19 @@ using Oms.Domain.SystemAggregate.Repositories;
|
||||
namespace Oms.Application.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// 从订单表及关联子表组装 createOrderOne 标准请求体。
|
||||
/// 从订单表及关联子表组装 itfx createOrderOne 标准请求体。
|
||||
/// </summary>
|
||||
public static class OrderDownstreamCreateOneBodyBuilder
|
||||
{
|
||||
public static async Task<OrderParcelCreateInput> BuildAsync(
|
||||
public static async Task<ItfxCreateOrderOneBody> BuildAsync(
|
||||
Order order,
|
||||
OrderSender? sender,
|
||||
OrderRecipient? recipient,
|
||||
IReadOnlyList<OrderDeclaration> declarations,
|
||||
IReadOnlyList<OrderWeight> weights,
|
||||
ICountryRepository countryRepository,
|
||||
IServiceChannelRepository serviceChannelRepository,
|
||||
IFileStorage? fileStorage = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var countryIds = new List<int>();
|
||||
@ -39,16 +43,18 @@ public static class OrderDownstreamCreateOneBodyBuilder
|
||||
string.Equals(x.Type, OrderWeightTypeCodes.Forecast, StringComparison.OrdinalIgnoreCase))
|
||||
?? weights.FirstOrDefault();
|
||||
|
||||
return new OrderParcelCreateInput
|
||||
var productCode = await ResolveForecastProductCodeAsync(
|
||||
order,
|
||||
serviceChannelRepository,
|
||||
cancellationToken);
|
||||
|
||||
return new ItfxCreateOrderOneBody
|
||||
{
|
||||
RequestId = order.RequestId,
|
||||
ClientNo = order.ClientNo,
|
||||
TrackNo = order.TrackNo,
|
||||
ServiceNo = order.ServiceNo,
|
||||
ProductCode = order.ProductCode,
|
||||
ChannelCode = order.ChannelCode,
|
||||
ProductCode = productCode,
|
||||
CargoAttribute = order.CargoAttribute,
|
||||
ExportCustomsMode = order.ExportCustomsMode,
|
||||
WarehouseId = order.WarehouseId,
|
||||
CustomerId = order.CustomerId,
|
||||
Length = forecast?.Length,
|
||||
@ -60,16 +66,45 @@ public static class OrderDownstreamCreateOneBodyBuilder
|
||||
Cubic = forecast?.Cubic,
|
||||
IsCod = order.IsCod > 0,
|
||||
CodCurrency = order.CodCurrency,
|
||||
CodSum = order.CodSum,
|
||||
CodSum = FormatDecimal(order.CodSum),
|
||||
DeclaredCurrency = order.DeclaredCurrency,
|
||||
DeclaredSum = order.DeclaredSum,
|
||||
Quantity = order.Quantity,
|
||||
DeclaredSum = FormatDecimal(order.DeclaredSum),
|
||||
Quantity = order.Quantity?.ToString(),
|
||||
Receiver = MapReceiver(recipient, order.ConsigneeCountry, idToCode),
|
||||
Sender = MapSender(sender, idToCode),
|
||||
Items = declarations.Select(MapItem).ToList()
|
||||
Items = declarations.Select(x => MapItem(x, fileStorage)).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// createOrderOne.productCode = 渠道「预报渠道 code」:
|
||||
/// 优先订单固化的 ChannelForecastCode,否则 live 查 t_service_channel.ForecastCode。
|
||||
/// </summary>
|
||||
private static async Task<string?> ResolveForecastProductCodeAsync(
|
||||
Order order,
|
||||
IServiceChannelRepository serviceChannelRepository,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var snapshotted = order.ChannelForecastCode?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(snapshotted))
|
||||
{
|
||||
return snapshotted;
|
||||
}
|
||||
|
||||
var channelCode = order.ChannelCode?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(channelCode))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var channel = await serviceChannelRepository.GetByChannelCodeAsync(channelCode, cancellationToken);
|
||||
var forecastCode = channel?.ForecastCode?.Trim();
|
||||
return string.IsNullOrWhiteSpace(forecastCode) ? null : forecastCode;
|
||||
}
|
||||
|
||||
private static string? FormatDecimal(decimal? value) =>
|
||||
value.HasValue ? value.Value.ToString(System.Globalization.CultureInfo.InvariantCulture) : null;
|
||||
|
||||
private static string? ResolveCountryIso(int? countryId, string? orderConsigneeCountry, IReadOnlyDictionary<int, string> idToCode)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(orderConsigneeCountry))
|
||||
@ -81,9 +116,9 @@ public static class OrderDownstreamCreateOneBodyBuilder
|
||||
}
|
||||
}
|
||||
|
||||
if (countryId is > 0 && idToCode.TryGetValue(countryId.Value, out var code) && !string.IsNullOrWhiteSpace(code))
|
||||
if (countryId is > 0 && idToCode.TryGetValue(countryId.Value, out var iso) && !string.IsNullOrWhiteSpace(iso))
|
||||
{
|
||||
return code.Trim().ToUpperInvariant();
|
||||
return iso.Trim().ToUpperInvariant();
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -150,7 +185,7 @@ public static class OrderDownstreamCreateOneBodyBuilder
|
||||
};
|
||||
}
|
||||
|
||||
private static OrderParcelItemInput MapItem(OrderDeclaration x) =>
|
||||
private static OrderParcelItemInput MapItem(OrderDeclaration x, IFileStorage? fileStorage) =>
|
||||
new()
|
||||
{
|
||||
CnName = x.CnName,
|
||||
@ -164,7 +199,8 @@ public static class OrderDownstreamCreateOneBodyBuilder
|
||||
Weight = x.Weight,
|
||||
ImHsCode = x.ImHsCode,
|
||||
ExHsCode = x.ExHsCode ?? x.HsCode,
|
||||
SkuImage = x.SkuImage,
|
||||
DeclareElement = x.DeclareElement,
|
||||
SkuImage = fileStorage?.GetPublicUrl(x.SkuImage) ?? x.SkuImage,
|
||||
Category = x.Category,
|
||||
Brand = x.Brand,
|
||||
BrandType = x.BrandType,
|
||||
|
||||
@ -17,12 +17,15 @@ public sealed class OrderDownstreamCredentials
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析订单下游 itfx 凭证(优先订单绑定字段,回退渠道分组)。
|
||||
/// 解析订单下游 itfx 凭证。
|
||||
/// 优先级:服务渠道规则快照(整套同源)→ 订单绑定字段 → 渠道分组实时查询。
|
||||
/// 快照命中时 SupplierCode/AppKey/AppSecret 必须同出快照,禁止与订单字段混拼。
|
||||
/// </summary>
|
||||
public class OrderDownstreamCredentialResolver(
|
||||
IServiceChannelRepository serviceChannelRepository,
|
||||
IServiceChannelGroupRepository serviceChannelGroupRepository,
|
||||
IItfxMetadataClient itfxMetadataClient) : IScopedDependency
|
||||
IItfxMetadataClient itfxMetadataClient,
|
||||
IItfxGatewayConfigProvider itfxGatewayConfigProvider) : IScopedDependency
|
||||
{
|
||||
public async Task<OrderDownstreamCredentials> ResolveAsync(Order order, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@ -40,7 +43,19 @@ public class OrderDownstreamCredentialResolver(
|
||||
var appSecret = app?.AppSecret?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(appSecret))
|
||||
{
|
||||
throw new BusinessException("应用 AppSecret 未配置");
|
||||
appSecret = await TryResolveAppSecretFromApiCenterAsync(order.DownstreamAppKey, cancellationToken);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(appSecret))
|
||||
{
|
||||
if (app == null)
|
||||
{
|
||||
throw new BusinessException(
|
||||
"无法从 API 中心获取应用信息,请检查「其他配置-API中心服务」地址/AppKey/AppSecret,以及下游应用是否存在");
|
||||
}
|
||||
|
||||
throw new BusinessException(
|
||||
"应用 AppSecret 未配置,请在「其他配置-API中心服务」填写与下游一致的 AppKey/AppSecret");
|
||||
}
|
||||
|
||||
return new OrderDownstreamCredentials
|
||||
@ -84,9 +99,28 @@ public class OrderDownstreamCredentialResolver(
|
||||
}
|
||||
|
||||
var appInfo = await itfxMetadataClient.GetAppAsync(group.AppId, cancellationToken);
|
||||
if (appInfo == null || string.IsNullOrWhiteSpace(appInfo.AppKey) || string.IsNullOrWhiteSpace(appInfo.AppSecret))
|
||||
var resolvedAppKey = appInfo?.AppKey?.Trim();
|
||||
var resolvedAppSecret = appInfo?.AppSecret?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(resolvedAppKey) || string.IsNullOrWhiteSpace(resolvedAppSecret))
|
||||
{
|
||||
throw new BusinessException("应用 AppKey / AppSecret 未配置");
|
||||
var fromCenter = await TryResolveCredentialsFromApiCenterAsync(cancellationToken);
|
||||
if (fromCenter != null)
|
||||
{
|
||||
resolvedAppKey = string.IsNullOrWhiteSpace(resolvedAppKey) ? fromCenter.AppKey : resolvedAppKey;
|
||||
resolvedAppSecret = string.IsNullOrWhiteSpace(resolvedAppSecret) ? fromCenter.AppSecret : resolvedAppSecret;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(resolvedAppKey) || string.IsNullOrWhiteSpace(resolvedAppSecret))
|
||||
{
|
||||
if (appInfo == null)
|
||||
{
|
||||
throw new BusinessException(
|
||||
"无法从 API 中心获取应用信息,请检查「其他配置-API中心服务」地址/AppKey/AppSecret,以及服务商应用是否存在");
|
||||
}
|
||||
|
||||
throw new BusinessException(
|
||||
"应用 AppKey / AppSecret 未配置,请在「其他配置-API中心服务」填写应用凭证");
|
||||
}
|
||||
|
||||
var sp = await itfxMetadataClient.GetServiceProviderAsync(group.ApiServiceProviderId, cancellationToken);
|
||||
@ -98,8 +132,42 @@ public class OrderDownstreamCredentialResolver(
|
||||
return new OrderDownstreamCredentials
|
||||
{
|
||||
SupplierCode = sp.Code.Trim(),
|
||||
AppKey = appInfo.AppKey.Trim(),
|
||||
AppSecret = appInfo.AppSecret.Trim()
|
||||
AppKey = resolvedAppKey,
|
||||
AppSecret = resolvedAppSecret
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<string?> TryResolveAppSecretFromApiCenterAsync(
|
||||
string expectedAppKey,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var cfg = await itfxGatewayConfigProvider.GetAsync(cancellationToken);
|
||||
if (string.IsNullOrWhiteSpace(cfg.AppKey) || string.IsNullOrWhiteSpace(cfg.AppSecret))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!string.Equals(cfg.AppKey.Trim(), expectedAppKey.Trim(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return cfg.AppSecret.Trim();
|
||||
}
|
||||
|
||||
private async Task<OrderDownstreamCredentials?> TryResolveCredentialsFromApiCenterAsync(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var cfg = await itfxGatewayConfigProvider.GetAsync(cancellationToken);
|
||||
if (string.IsNullOrWhiteSpace(cfg.AppKey) || string.IsNullOrWhiteSpace(cfg.AppSecret))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new OrderDownstreamCredentials
|
||||
{
|
||||
AppKey = cfg.AppKey.Trim(),
|
||||
AppSecret = cfg.AppSecret.Trim()
|
||||
};
|
||||
}
|
||||
|
||||
@ -147,23 +215,20 @@ public class OrderDownstreamCredentialResolver(
|
||||
return null;
|
||||
}
|
||||
|
||||
// 整套同源:禁止用订单 SupplierCode/AppKey 拼快照 AppSecret,避免两边发布不同步时混用。
|
||||
var appSecret = downstream.AppSecret?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(appSecret))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var appKey = !string.IsNullOrWhiteSpace(order.DownstreamAppKey)
|
||||
? order.DownstreamAppKey.Trim()
|
||||
: downstream.AppKey?.Trim();
|
||||
var appKey = downstream.AppKey?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(appKey))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var supplierCode = !string.IsNullOrWhiteSpace(order.DownstreamSupplierCode)
|
||||
? order.DownstreamSupplierCode.Trim()
|
||||
: downstream.SupplierCode?.Trim();
|
||||
var supplierCode = downstream.SupplierCode?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(supplierCode))
|
||||
{
|
||||
return null;
|
||||
|
||||
251
src/Oms.Application/Shared/OrderDownstreamWeightImportHelper.cs
Normal file
251
src/Oms.Application/Shared/OrderDownstreamWeightImportHelper.cs
Normal file
@ -0,0 +1,251 @@
|
||||
using System.Globalization;
|
||||
using MiniExcelLibs;
|
||||
using Oms.Application.Dtos.Orders;
|
||||
|
||||
namespace Oms.Application.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// 对账件重尺(downstream)Excel 模板与解析。
|
||||
/// 仅写入下游对账重量,触发应付重算;不改应收。
|
||||
/// </summary>
|
||||
public static class OrderDownstreamWeightImportHelper
|
||||
{
|
||||
public const int MaxRows = 2000;
|
||||
|
||||
private static readonly Dictionary<string, string> HeaderAliases = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["运单号"] = "WaybillNo",
|
||||
["单号"] = "WaybillNo",
|
||||
["服务商单号"] = "WaybillNo",
|
||||
["WaybillNo"] = "WaybillNo",
|
||||
["waybillNo"] = "WaybillNo",
|
||||
["ServiceNo"] = "WaybillNo",
|
||||
["serviceNo"] = "WaybillNo",
|
||||
["长"] = "Length",
|
||||
["长度"] = "Length",
|
||||
["Length"] = "Length",
|
||||
["length"] = "Length",
|
||||
["宽"] = "Width",
|
||||
["宽度"] = "Width",
|
||||
["Width"] = "Width",
|
||||
["width"] = "Width",
|
||||
["高"] = "Height",
|
||||
["高度"] = "Height",
|
||||
["Height"] = "Height",
|
||||
["height"] = "Height",
|
||||
["实重"] = "ActualWeight",
|
||||
["重量"] = "ActualWeight",
|
||||
["ActualWeight"] = "ActualWeight",
|
||||
["actualWeight"] = "ActualWeight",
|
||||
["Weight"] = "ActualWeight",
|
||||
["weight"] = "ActualWeight",
|
||||
};
|
||||
|
||||
public static async Task<byte[]> BuildTemplateBytesAsync()
|
||||
{
|
||||
var rows = new List<Dictionary<string, object?>>
|
||||
{
|
||||
new()
|
||||
{
|
||||
["运单号"] = "OMS202601010001",
|
||||
["长"] = 30,
|
||||
["宽"] = 20,
|
||||
["高"] = 10,
|
||||
["实重"] = 1.25
|
||||
}
|
||||
};
|
||||
|
||||
await using var stream = new MemoryStream();
|
||||
await MiniExcel.SaveAsAsync(stream, rows);
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
public static string TemplateFileName => "对账件重尺导入模板.xlsx";
|
||||
|
||||
public static async Task<List<(int RowNumber, OrderDownstreamWeightImportRow Row)>> ParseImportRowsAsync(
|
||||
Stream fileStream)
|
||||
{
|
||||
var rows = await MiniExcel.QueryAsync(fileStream, useHeaderRow: true);
|
||||
var result = new List<(int RowNumber, OrderDownstreamWeightImportRow Row)>();
|
||||
var rowNumber = 1;
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
rowNumber++;
|
||||
if (row is not IDictionary<string, object> rowDict)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsEmptyRow(rowDict))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
result.Add((rowNumber, ParseRow(rowDict)));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
result.Add((rowNumber, new OrderDownstreamWeightImportRow
|
||||
{
|
||||
WaybillNo = TryPeekWaybillNo(rowDict),
|
||||
ParseError = ex.Message
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void ValidateRow(OrderDownstreamWeightImportRow row)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(row.ParseError))
|
||||
{
|
||||
throw new InvalidOperationException(row.ParseError);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(row.WaybillNo))
|
||||
{
|
||||
throw new InvalidOperationException("运单号不能为空");
|
||||
}
|
||||
|
||||
row.WaybillNo = row.WaybillNo.Trim();
|
||||
if (row.WaybillNo.Length > 64)
|
||||
{
|
||||
throw new InvalidOperationException("运单号不可超过64个字符");
|
||||
}
|
||||
|
||||
if (!row.ActualWeight.HasValue || row.ActualWeight.Value <= 0)
|
||||
{
|
||||
throw new InvalidOperationException("实重必须大于0");
|
||||
}
|
||||
|
||||
ValidateOptionalDimension(row.Length, "长");
|
||||
ValidateOptionalDimension(row.Width, "宽");
|
||||
ValidateOptionalDimension(row.Height, "高");
|
||||
|
||||
var hasAnyDim = row.Length.HasValue || row.Width.HasValue || row.Height.HasValue;
|
||||
var hasAllDim = row.Length.HasValue && row.Width.HasValue && row.Height.HasValue;
|
||||
if (hasAnyDim && !hasAllDim)
|
||||
{
|
||||
throw new InvalidOperationException("长/宽/高需同时填写或全部留空(留空则沿用订单已有尺寸)");
|
||||
}
|
||||
}
|
||||
|
||||
private static string? TryPeekWaybillNo(IDictionary<string, object> rowDict)
|
||||
{
|
||||
foreach (var (header, value) in rowDict)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(header))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (HeaderAliases.TryGetValue(header.Trim(), out var field)
|
||||
&& string.Equals(field, "WaybillNo", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return NormalizeCell(value);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void ValidateOptionalDimension(decimal? value, string label)
|
||||
{
|
||||
if (!value.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.Value <= 0)
|
||||
{
|
||||
throw new InvalidOperationException($"{label}必须大于0");
|
||||
}
|
||||
}
|
||||
|
||||
private static OrderDownstreamWeightImportRow ParseRow(IDictionary<string, object> rowDict)
|
||||
{
|
||||
var mapped = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var (header, value) in rowDict)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(header))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!HeaderAliases.TryGetValue(header.Trim(), out var field))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
mapped[field] = NormalizeCell(value);
|
||||
}
|
||||
|
||||
return new OrderDownstreamWeightImportRow
|
||||
{
|
||||
WaybillNo = mapped.GetValueOrDefault("WaybillNo"),
|
||||
Length = ParseDecimal(mapped.GetValueOrDefault("Length")),
|
||||
Width = ParseDecimal(mapped.GetValueOrDefault("Width")),
|
||||
Height = ParseDecimal(mapped.GetValueOrDefault("Height")),
|
||||
ActualWeight = ParseDecimal(mapped.GetValueOrDefault("ActualWeight"))
|
||||
};
|
||||
}
|
||||
|
||||
private static decimal? ParseDecimal(string? text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (decimal.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
if (decimal.TryParse(text, NumberStyles.Any, CultureInfo.GetCultureInfo("zh-CN"), out value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"数值格式无效:{text}");
|
||||
}
|
||||
|
||||
private static string? NormalizeCell(object? value)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var text = Convert.ToString(value, CultureInfo.InvariantCulture)?.Trim();
|
||||
return string.IsNullOrWhiteSpace(text) ? null : text;
|
||||
}
|
||||
|
||||
private static bool IsEmptyRow(IDictionary<string, object> rowDict) =>
|
||||
rowDict.Values.All(value => string.IsNullOrWhiteSpace(NormalizeCell(value)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对账件重尺导入行。
|
||||
/// </summary>
|
||||
public sealed class OrderDownstreamWeightImportRow
|
||||
{
|
||||
public string? WaybillNo { get; set; }
|
||||
|
||||
public decimal? Length { get; set; }
|
||||
|
||||
public decimal? Width { get; set; }
|
||||
|
||||
public decimal? Height { get; set; }
|
||||
|
||||
public decimal? ActualWeight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Excel 单元格解析错误(校验阶段直接抛出)。
|
||||
/// </summary>
|
||||
public string? ParseError { get; set; }
|
||||
}
|
||||
@ -0,0 +1,190 @@
|
||||
using Oms.Application.Dtos.Orders;
|
||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||
using Oms.Domain.OrdersAggregate.Consts;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
using Oms.Domain.SystemAggregate.Entities;
|
||||
|
||||
namespace Oms.Application.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// 从揽收单表组装 itfx createOrderOne 标准请求体(不读关联物流单)。
|
||||
/// </summary>
|
||||
public static class PickupOrderDownstreamCreateOneBodyBuilder
|
||||
{
|
||||
public static async Task<(ItfxCreateOrderOneBody Body, string? ForecastProductCode)> BuildAsync(
|
||||
PickupOrder pickup,
|
||||
WarehouseAddress warehouse,
|
||||
ISalesProductRepository salesProductRepository,
|
||||
IServiceChannelRepository serviceChannelRepository,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var productCode = await ResolveForecastProductCodeAsync(
|
||||
pickup.CarrierService,
|
||||
salesProductRepository,
|
||||
serviceChannelRepository,
|
||||
cancellationToken);
|
||||
|
||||
var isReverse = string.Equals(
|
||||
PickupDirectionCodes.NormalizeOrDefault(pickup.Direction),
|
||||
PickupDirectionCodes.Reverse,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var customerSide = MapCustomerSide(pickup);
|
||||
var warehouseSide = MapWarehouseSide(warehouse);
|
||||
|
||||
var weight = pickup.TotalActualWeight ?? pickup.ChargeableWeight;
|
||||
var qty = pickup.ParcelCount > 0 ? pickup.ParcelCount : 1;
|
||||
var cargoName = string.IsNullOrWhiteSpace(pickup.Remark)
|
||||
? "上门揽收包裹"
|
||||
: pickup.Remark.Trim();
|
||||
|
||||
return (new ItfxCreateOrderOneBody
|
||||
{
|
||||
ClientNo = pickup.PickupNo,
|
||||
ServiceNo = pickup.PickupNo,
|
||||
TrackNo = pickup.CarrierWaybillNo,
|
||||
ProductCode = productCode,
|
||||
CargoAttribute = CargoAttributeCodes.General,
|
||||
WarehouseId = pickup.WarehouseId,
|
||||
CustomerId = pickup.CustomerId,
|
||||
Length = 1,
|
||||
Width = 1,
|
||||
Height = 1,
|
||||
ActualWeight = weight,
|
||||
ChargeableWeight = pickup.ChargeableWeight ?? weight,
|
||||
IsCod = false,
|
||||
DeclaredCurrency = pickup.Currency,
|
||||
DeclaredSum = FormatDecimal(pickup.ReceivableAmount ?? pickup.FreightAmount ?? 1),
|
||||
Quantity = qty.ToString(),
|
||||
Sender = isReverse ? warehouseSide.Sender : customerSide.Sender,
|
||||
Receiver = isReverse ? customerSide.Receiver : warehouseSide.Receiver,
|
||||
Items =
|
||||
[
|
||||
new OrderParcelItemInput
|
||||
{
|
||||
CnName = cargoName,
|
||||
EnName = cargoName,
|
||||
UnitPrice = 1,
|
||||
UnitWeight = weight,
|
||||
Quantity = qty,
|
||||
Amount = 1,
|
||||
Weight = weight
|
||||
}
|
||||
]
|
||||
}, productCode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// createOrderOne.productCode:承运服务(销售产品)对应渠道的 ForecastCode,缺省回退 CarrierService。
|
||||
/// </summary>
|
||||
public static async Task<string?> ResolveForecastProductCodeAsync(
|
||||
string? carrierService,
|
||||
ISalesProductRepository salesProductRepository,
|
||||
IServiceChannelRepository serviceChannelRepository,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var salesProductCode = carrierService?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(salesProductCode))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var product = await salesProductRepository.GetByProductCodeAsync(salesProductCode, cancellationToken);
|
||||
if (product == null)
|
||||
{
|
||||
return salesProductCode;
|
||||
}
|
||||
|
||||
var providers = await salesProductRepository.GetAllocationProvidersAsync(product.Id, cancellationToken);
|
||||
var channelId = providers
|
||||
.Where(x => x.ServiceChannelId is > 0)
|
||||
.OrderBy(x => x.SerialNumber)
|
||||
.Select(x => x.ServiceChannelId!.Value)
|
||||
.FirstOrDefault();
|
||||
if (channelId <= 0)
|
||||
{
|
||||
return salesProductCode;
|
||||
}
|
||||
|
||||
var channel = await serviceChannelRepository.FirstOrDefaultAsync(x => x.Id == channelId, cancellationToken);
|
||||
var forecastCode = channel?.ForecastCode?.Trim();
|
||||
return string.IsNullOrWhiteSpace(forecastCode) ? salesProductCode : forecastCode;
|
||||
}
|
||||
|
||||
private static string? FormatDecimal(decimal? value) =>
|
||||
value.HasValue ? value.Value.ToString(System.Globalization.CultureInfo.InvariantCulture) : null;
|
||||
|
||||
private static (OrderParcelSenderInput Sender, OrderParcelReceiverInput Receiver) MapCustomerSide(PickupOrder pickup)
|
||||
{
|
||||
var sender = new OrderParcelSenderInput
|
||||
{
|
||||
SenderName = pickup.SenderName,
|
||||
Company = pickup.SenderCompany,
|
||||
Phone = pickup.SenderPhone,
|
||||
TelePhone = pickup.SenderTelePhone,
|
||||
Email = pickup.SenderEmail,
|
||||
CountryCode = string.IsNullOrWhiteSpace(pickup.SenderCountryCode)
|
||||
? "CN"
|
||||
: pickup.SenderCountryCode.Trim().ToUpperInvariant(),
|
||||
Province = pickup.SenderProvince,
|
||||
City = pickup.SenderCity,
|
||||
ZipCode = pickup.SenderZipCode,
|
||||
StreetAddress1 = pickup.SenderStreetAddress1,
|
||||
StreetAddress2 = pickup.SenderStreetAddress2,
|
||||
StreetAddress3 = pickup.SenderStreetAddress3,
|
||||
HouseNumber = pickup.SenderHouseNumber
|
||||
};
|
||||
|
||||
var receiver = new OrderParcelReceiverInput
|
||||
{
|
||||
RecipientsName = pickup.SenderName,
|
||||
CompanyName = pickup.SenderCompany,
|
||||
Phone = pickup.SenderPhone,
|
||||
TelePhone = pickup.SenderTelePhone,
|
||||
Email = pickup.SenderEmail,
|
||||
CountryCode = string.IsNullOrWhiteSpace(pickup.SenderCountryCode)
|
||||
? "CN"
|
||||
: pickup.SenderCountryCode.Trim().ToUpperInvariant(),
|
||||
Province = pickup.SenderProvince,
|
||||
City = pickup.SenderCity,
|
||||
ZipCode = string.IsNullOrWhiteSpace(pickup.SenderZipCode) ? "000000" : pickup.SenderZipCode,
|
||||
StreetAddress1 = pickup.SenderStreetAddress1,
|
||||
StreetAddress2 = pickup.SenderStreetAddress2,
|
||||
StreetAddress3 = pickup.SenderStreetAddress3,
|
||||
StreetNumber = pickup.SenderHouseNumber
|
||||
};
|
||||
|
||||
return (sender, receiver);
|
||||
}
|
||||
|
||||
private static (OrderParcelSenderInput Sender, OrderParcelReceiverInput Receiver) MapWarehouseSide(
|
||||
WarehouseAddress warehouse)
|
||||
{
|
||||
var contact = string.IsNullOrWhiteSpace(warehouse.Recipient)
|
||||
? warehouse.WarehouseName
|
||||
: warehouse.Recipient!.Trim();
|
||||
var street = string.IsNullOrWhiteSpace(warehouse.ShippingAddress)
|
||||
? warehouse.WarehouseName
|
||||
: warehouse.ShippingAddress!.Trim();
|
||||
|
||||
var sender = new OrderParcelSenderInput
|
||||
{
|
||||
SenderName = contact,
|
||||
Phone = warehouse.ContactPhone,
|
||||
CountryCode = "CN",
|
||||
StreetAddress1 = street,
|
||||
ZipCode = "000000"
|
||||
};
|
||||
|
||||
var receiver = new OrderParcelReceiverInput
|
||||
{
|
||||
RecipientsName = contact,
|
||||
Phone = warehouse.ContactPhone,
|
||||
CountryCode = "CN",
|
||||
ZipCode = "000000",
|
||||
StreetAddress1 = street
|
||||
};
|
||||
|
||||
return (sender, receiver);
|
||||
}
|
||||
}
|
||||
@ -67,7 +67,8 @@ public static class PublishSnapshotSlimmer
|
||||
ServiceChannelId = provider.ServiceChannelId,
|
||||
ServiceChannelName = provider.ServiceChannelName,
|
||||
Weight = provider.Weight,
|
||||
CustomerIds = provider.CustomerIds
|
||||
CustomerIds = provider.CustomerIds,
|
||||
MatchCondition = provider.MatchCondition
|
||||
};
|
||||
|
||||
public static WeightRuleSnapshotRuleDto SlimWeightRule(WeightRuleInfo entity) =>
|
||||
|
||||
@ -18,4 +18,10 @@ public class SalesProductAllocationProvider : OmsAuditedEntity
|
||||
|
||||
[SugarColumn(ColumnDataType = "json", IsNullable = true)]
|
||||
public string? CustomerIds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排货判断条件(中文公式);空表示全部订单可用。
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDataType = "text", IsNullable = true)]
|
||||
public string? MatchCondition { get; set; }
|
||||
}
|
||||
|
||||
@ -6,4 +6,9 @@ namespace Oms.Domain.ChannelsAggregate.Repositories;
|
||||
public interface IServiceChannelGroupRepository : IRepository<ServiceChannelGroup>
|
||||
{
|
||||
Task<ServiceChannelGroup?> GetByNameAsync(string name, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 按名称查询分组(含软删除),用于唯一名冲突时恢复或释放名称。
|
||||
/// </summary>
|
||||
Task<ServiceChannelGroup?> GetByNameIncludingDeletedAsync(string name, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@ -0,0 +1,58 @@
|
||||
using Oms.Domain.Shared;
|
||||
using SqlSugar;
|
||||
|
||||
namespace Oms.Domain.CustomersAggregate.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 客户出口资质备案。
|
||||
/// </summary>
|
||||
[SugarTable("t_customer_export_qualification")]
|
||||
public class CustomerExportQualification : OmsAuditedAggregateRoot
|
||||
{
|
||||
public int CustomerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 企业名称。
|
||||
/// </summary>
|
||||
public string CompanyName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 企业社会信用代码。
|
||||
/// </summary>
|
||||
public string SocialCreditCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 境内发货人。
|
||||
/// </summary>
|
||||
public string DomesticConsignor { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 境内发货人海关代码。
|
||||
/// </summary>
|
||||
public string? DomesticConsignorCustomsCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 生产销售单位。
|
||||
/// </summary>
|
||||
public string ProducerName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 生产销售单位海关代码。
|
||||
/// </summary>
|
||||
public string? ProducerCustomsCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备案海关港口地。
|
||||
/// </summary>
|
||||
public string? CustomsPort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否默认:同客户仅一条为默认。
|
||||
/// </summary>
|
||||
public byte IsDefault { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注。
|
||||
/// </summary>
|
||||
public string? Remark { get; set; }
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
using Fake.Domain.Repositories;
|
||||
using Oms.Domain.CustomersAggregate.Entities;
|
||||
|
||||
namespace Oms.Domain.CustomersAggregate.Repositories;
|
||||
|
||||
public interface ICustomerExportQualificationRepository : IRepository<CustomerExportQualification>
|
||||
{
|
||||
Task<CustomerExportQualification?> GetOwnedAsync(
|
||||
int id,
|
||||
int customerId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task ClearDefaultAsync(
|
||||
int customerId,
|
||||
int? excludeId = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@ -1,18 +1,27 @@
|
||||
namespace Oms.Domain.OrdersAggregate.Consts;
|
||||
|
||||
/// <summary>
|
||||
/// 客户地址类型:发件 / 退件 / 收件。
|
||||
/// 客户地址类型:国内发件 / 国内退件 / 国内揽收 / 海外收件。
|
||||
/// </summary>
|
||||
public static class CustomerAddressTypeCodes
|
||||
{
|
||||
/// <summary>国内发件(国际件发件人地址)</summary>
|
||||
public const string Sender = "sender";
|
||||
|
||||
/// <summary>国内退件</summary>
|
||||
public const string Return = "return";
|
||||
|
||||
/// <summary>国内揽收(上门取件地址)</summary>
|
||||
public const string Pickup = "pickup";
|
||||
|
||||
/// <summary>海外收件</summary>
|
||||
public const string Receiver = "receiver";
|
||||
|
||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
Sender,
|
||||
Return,
|
||||
Pickup,
|
||||
Receiver
|
||||
};
|
||||
|
||||
@ -21,7 +30,7 @@ public static class CustomerAddressTypeCodes
|
||||
var code = string.IsNullOrWhiteSpace(addressType) ? Sender : addressType.Trim();
|
||||
if (!All.Contains(code))
|
||||
{
|
||||
throw new ArgumentException("地址类型不合法,仅支持 sender/return/receiver");
|
||||
throw new ArgumentException("地址类型不合法,仅支持 sender/return/pickup/receiver");
|
||||
}
|
||||
|
||||
return All.First(x => string.Equals(x, code, StringComparison.OrdinalIgnoreCase));
|
||||
@ -31,10 +40,19 @@ public static class CustomerAddressTypeCodes
|
||||
{
|
||||
return addressType?.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
Sender => "发件",
|
||||
Return => "退件",
|
||||
Receiver => "收件",
|
||||
Sender => "国内发件",
|
||||
Return => "国内退件",
|
||||
Pickup => "国内揽收",
|
||||
Receiver => "海外收件",
|
||||
_ => addressType ?? string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>是否可作为揽收单客户地址(国内揽收;兼容历史发件地址)。</summary>
|
||||
public static bool IsPickupEligible(string? addressType)
|
||||
{
|
||||
var code = string.IsNullOrWhiteSpace(addressType) ? Sender : addressType.Trim();
|
||||
return string.Equals(code, Pickup, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(code, Sender, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
namespace Oms.Domain.OrdersAggregate.Consts;
|
||||
|
||||
/// <summary>
|
||||
/// 揽收方向:正向(客户地址→仓库)/ 逆向(仓库→客户地址)。
|
||||
/// </summary>
|
||||
public static class PickupDirectionCodes
|
||||
{
|
||||
/// <summary>正向:客户揽收地址 → 仓库</summary>
|
||||
public const string Forward = "forward";
|
||||
|
||||
/// <summary>逆向:仓库 → 客户揽收地址</summary>
|
||||
public const string Reverse = "reverse";
|
||||
|
||||
public static readonly HashSet<string> All = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
Forward,
|
||||
Reverse
|
||||
};
|
||||
|
||||
public static string NormalizeOrDefault(string? direction)
|
||||
{
|
||||
var code = string.IsNullOrWhiteSpace(direction) ? Forward : direction.Trim();
|
||||
if (!All.Contains(code))
|
||||
{
|
||||
return Forward;
|
||||
}
|
||||
|
||||
return All.First(x => string.Equals(x, code, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public static string GetDisplayName(string? direction)
|
||||
{
|
||||
return NormalizeOrDefault(direction) switch
|
||||
{
|
||||
Reverse => "逆向",
|
||||
_ => "正向"
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -32,6 +32,11 @@ public class OrderDeclaration : OmsAuditedEntity
|
||||
|
||||
public string? ExHsCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 出口申报要素。
|
||||
/// </summary>
|
||||
public string? DeclareElement { get; set; }
|
||||
|
||||
public string? SkuImage { get; set; }
|
||||
|
||||
public string? Category { get; set; }
|
||||
|
||||
@ -24,6 +24,12 @@ public class PickupOrder : OmsAuditedAggregateRoot
|
||||
|
||||
public int SenderAddressId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 揽收方向:forward 正向(客户→仓)/ reverse 逆向(仓→客户)。
|
||||
/// </summary>
|
||||
[SugarColumn(Length = 16)]
|
||||
public string Direction { get; set; } = "forward";
|
||||
|
||||
[SugarColumn(Length = 50)]
|
||||
public string SenderName { get; set; } = string.Empty;
|
||||
|
||||
|
||||
@ -17,6 +17,14 @@ public interface IOrderFileRepository : IRepository<OrderFile>
|
||||
string fileType,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 按订单 ID 集合 + 附件类型批量查询。
|
||||
/// </summary>
|
||||
Task<List<OrderFile>> GetByOrderIdsAndTypeAsync(
|
||||
IReadOnlyCollection<int> orderIds,
|
||||
string fileType,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task SoftDeleteByOrderAndTypeAsync(
|
||||
int orderId,
|
||||
string fileType,
|
||||
|
||||
@ -49,4 +49,9 @@ public static class SystemOtherConfigCodes
|
||||
/// 全局默认面单模板(AppKey=模板 Id,Enabled 控制是否生效)。
|
||||
/// </summary>
|
||||
public const string LabelDefaultTemplate = "label_default_template";
|
||||
|
||||
/// <summary>
|
||||
/// API 中心(接口通 / itfx)网关:BaseUrl=itfx 根地址,AppKey/AppSecret=默认应用凭证,TimeoutSeconds=超时。
|
||||
/// </summary>
|
||||
public const string ApiCenter = "api_center";
|
||||
}
|
||||
|
||||
@ -22,6 +22,11 @@ public class CSku : OmsAuditedAggregateRoot
|
||||
|
||||
public string? ImHsCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 出口申报要素。
|
||||
/// </summary>
|
||||
public string? DeclareElement { get; set; }
|
||||
|
||||
public decimal? WeightG { get; set; }
|
||||
|
||||
public decimal? LengthCm { get; set; }
|
||||
|
||||
@ -2,6 +2,7 @@ using Fake.DependencyInjection;
|
||||
using Oms.Domain.ChannelsAggregate.Entities;
|
||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||
using Oms.SqlSugarCore.Repositories;
|
||||
using SqlSugar;
|
||||
|
||||
namespace Oms.SqlSugarCore.Channels;
|
||||
|
||||
@ -11,4 +12,17 @@ public class ServiceChannelGroupRepository : OmsSqlSugarRepository<ServiceChanne
|
||||
{
|
||||
return FirstOrDefaultAsync(x => x.Name == name, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<ServiceChannelGroup?> GetByNameIncludingDeletedAsync(
|
||||
string name,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var db = await GetDbContextAsync(cancellationToken);
|
||||
var rows = await db.Queryable<ServiceChannelGroup>()
|
||||
.ClearFilter()
|
||||
.Where(x => x.Name == name)
|
||||
.Take(1)
|
||||
.ToListAsync(cancellationToken);
|
||||
return rows.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
using Fake.DependencyInjection;
|
||||
using Oms.Domain.CustomersAggregate.Entities;
|
||||
using Oms.Domain.CustomersAggregate.Repositories;
|
||||
using Oms.SqlSugarCore.Repositories;
|
||||
|
||||
namespace Oms.SqlSugarCore.Customers;
|
||||
|
||||
public class CustomerExportQualificationRepository
|
||||
: OmsSqlSugarRepository<CustomerExportQualification>,
|
||||
ICustomerExportQualificationRepository,
|
||||
IScopedDependency
|
||||
{
|
||||
public Task<CustomerExportQualification?> GetOwnedAsync(
|
||||
int id,
|
||||
int customerId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return FirstOrDefaultAsync(x => x.Id == id && x.CustomerId == customerId, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task ClearDefaultAsync(
|
||||
int customerId,
|
||||
int? excludeId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var hasExcludeId = excludeId.HasValue;
|
||||
var excludeIdValue = excludeId.GetValueOrDefault();
|
||||
var list = await GetListAsync(
|
||||
x => x.CustomerId == customerId
|
||||
&& x.IsDefault == 1
|
||||
&& (!hasExcludeId || x.Id != excludeIdValue),
|
||||
cancellationToken: cancellationToken);
|
||||
if (list.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var item in list)
|
||||
{
|
||||
item.IsDefault = 0;
|
||||
await UpdateAsync(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -31,6 +31,22 @@ public class OrderFileRepository
|
||||
x => x.OrderId == orderId && x.FileType == fileType,
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
public Task<List<OrderFile>> GetByOrderIdsAndTypeAsync(
|
||||
IReadOnlyCollection<int> orderIds,
|
||||
string fileType,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var ids = orderIds?.Where(x => x > 0).Distinct().ToList() ?? [];
|
||||
if (ids.Count == 0)
|
||||
{
|
||||
return Task.FromResult(new List<OrderFile>());
|
||||
}
|
||||
|
||||
return GetListAsync(
|
||||
x => ids.Contains(x.OrderId) && x.FileType == fileType,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
public async Task SoftDeleteByOrderAndTypeAsync(
|
||||
int orderId,
|
||||
string fileType,
|
||||
|
||||
@ -996,6 +996,7 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
|
||||
fee.OrderId = orderId;
|
||||
var existingList = await db.Queryable<OrderFee>()
|
||||
.Where(x => x.OrderId == orderId
|
||||
&& !x.IsDeleted
|
||||
&& x.Type == fee.Type
|
||||
&& x.CostItem == fee.CostItem
|
||||
&& x.ChargingStage == fee.ChargingStage)
|
||||
@ -1004,6 +1005,7 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
|
||||
var existing = existingList.FirstOrDefault();
|
||||
if (existing == null)
|
||||
{
|
||||
fee.IsDeleted = false;
|
||||
await db.Insertable(fee).ExecuteCommandAsync(cancellationToken);
|
||||
continue;
|
||||
}
|
||||
@ -1013,6 +1015,7 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
|
||||
existing.PriceNo = fee.PriceNo;
|
||||
existing.PriceEffectiveTime = fee.PriceEffectiveTime;
|
||||
existing.DownstreamProvider = fee.DownstreamProvider;
|
||||
existing.IsDeleted = false;
|
||||
existing.UpdateTime = DateTime.Now;
|
||||
await db.Updateable(existing).ExecuteCommandAsync(cancellationToken);
|
||||
}
|
||||
@ -1021,36 +1024,46 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
|
||||
public async Task DeleteReceivableFeesAsync(int orderId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var db = await GetDbContextAsync(cancellationToken);
|
||||
await db.Deleteable<OrderFee>()
|
||||
.Where(x => x.OrderId == orderId && x.Type == OrderFeeTypeCodes.Receivable)
|
||||
await db.Updateable<OrderFee>()
|
||||
.SetColumns(x => new OrderFee { IsDeleted = true, UpdateTime = DateTime.Now })
|
||||
.Where(x => x.OrderId == orderId
|
||||
&& x.Type == OrderFeeTypeCodes.Receivable
|
||||
&& !x.IsDeleted)
|
||||
.ExecuteCommandAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DeleteReceivableFeesByStageAsync(int orderId, string chargingStage, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var db = await GetDbContextAsync(cancellationToken);
|
||||
await db.Deleteable<OrderFee>()
|
||||
await db.Updateable<OrderFee>()
|
||||
.SetColumns(x => new OrderFee { IsDeleted = true, UpdateTime = DateTime.Now })
|
||||
.Where(x => x.OrderId == orderId
|
||||
&& x.Type == OrderFeeTypeCodes.Receivable
|
||||
&& x.ChargingStage == chargingStage)
|
||||
&& x.ChargingStage == chargingStage
|
||||
&& !x.IsDeleted)
|
||||
.ExecuteCommandAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DeletePayableFeesAsync(int orderId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var db = await GetDbContextAsync(cancellationToken);
|
||||
await db.Deleteable<OrderFee>()
|
||||
.Where(x => x.OrderId == orderId && x.Type == OrderFeeTypeCodes.Payable)
|
||||
await db.Updateable<OrderFee>()
|
||||
.SetColumns(x => new OrderFee { IsDeleted = true, UpdateTime = DateTime.Now })
|
||||
.Where(x => x.OrderId == orderId
|
||||
&& x.Type == OrderFeeTypeCodes.Payable
|
||||
&& !x.IsDeleted)
|
||||
.ExecuteCommandAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DeletePayableFeesByStageAsync(int orderId, string chargingStage, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var db = await GetDbContextAsync(cancellationToken);
|
||||
await db.Deleteable<OrderFee>()
|
||||
await db.Updateable<OrderFee>()
|
||||
.SetColumns(x => new OrderFee { IsDeleted = true, UpdateTime = DateTime.Now })
|
||||
.Where(x => x.OrderId == orderId
|
||||
&& x.Type == OrderFeeTypeCodes.Payable
|
||||
&& x.ChargingStage == chargingStage)
|
||||
&& x.ChargingStage == chargingStage
|
||||
&& !x.IsDeleted)
|
||||
.ExecuteCommandAsync(cancellationToken);
|
||||
}
|
||||
|
||||
@ -1078,7 +1091,7 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
|
||||
|
||||
var db = await GetDbContextAsync(cancellationToken);
|
||||
return await db.Queryable<OrderFee>()
|
||||
.Where(x => ids.Contains(x.OrderId))
|
||||
.Where(x => ids.Contains(x.OrderId) && !x.IsDeleted)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
using FluentAssertions;
|
||||
using Oms.Application.Dtos.Channels;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Application.Services.Orders.Issues;
|
||||
using Oms.Domain.ChannelsAggregate.Consts;
|
||||
using Oms.Domain.ChannelsAggregate.Entities;
|
||||
using Oms.Domain.CustomersAggregate.Entities;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
|
||||
namespace Oms.Tests;
|
||||
|
||||
@ -23,6 +26,159 @@ public class AllocationResolverTests
|
||||
filtered.Select(x => x.ServiceChannelId).Should().BeEquivalentTo([1, 2]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FilterProvidersDetailed_EmptyCondition_ShouldPass()
|
||||
{
|
||||
var providers = new List<SalesProductAllocationProviderSnapshotDto>
|
||||
{
|
||||
new() { SerialNumber = 1, ServiceChannelId = 1, MatchCondition = null },
|
||||
new() { SerialNumber = 2, ServiceChannelId = 2, MatchCondition = " " }
|
||||
};
|
||||
|
||||
var result = AllocationResolver.FilterProvidersDetailed(providers, 1, Context());
|
||||
result.Passed.Should().HaveCount(2);
|
||||
result.Skipped.Should().BeEmpty();
|
||||
result.PoolCount.Should().Be(2);
|
||||
result.ConditionPassCount.Should().Be(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FilterProvidersDetailed_CargoAttribute_ShouldFilter()
|
||||
{
|
||||
var providers = new List<SalesProductAllocationProviderSnapshotDto>
|
||||
{
|
||||
new()
|
||||
{
|
||||
SerialNumber = 1,
|
||||
ServiceChannelId = 1,
|
||||
ChannelCode = "SENS",
|
||||
MatchCondition = "货物属性包含('sensitive')"
|
||||
},
|
||||
new()
|
||||
{
|
||||
SerialNumber = 2,
|
||||
ServiceChannelId = 2,
|
||||
ChannelCode = "GEN",
|
||||
MatchCondition = "货物属性包含('general')"
|
||||
}
|
||||
};
|
||||
|
||||
var result = AllocationResolver.FilterProvidersDetailed(
|
||||
providers,
|
||||
customerId: null,
|
||||
Context(cargoAttribute: "sensitive"));
|
||||
|
||||
result.Passed.Should().HaveCount(1);
|
||||
result.Passed[0].ServiceChannelId.Should().Be(1);
|
||||
result.Skipped.Should().HaveCount(1);
|
||||
result.Skipped[0].Stage.Should().Be(AllocationResolver.SkipStageCondition);
|
||||
result.Skipped[0].Reason.Should().Be("判断条件未命中");
|
||||
result.Skipped[0].MatchCondition.Should().Contain("货物属性包含");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FilterProvidersDetailed_CustomerContains_ShouldUseCustomerEntity()
|
||||
{
|
||||
var providers = new List<SalesProductAllocationProviderSnapshotDto>
|
||||
{
|
||||
new()
|
||||
{
|
||||
SerialNumber = 1,
|
||||
ServiceChannelId = 1,
|
||||
MatchCondition = "客户包含('阿里')"
|
||||
},
|
||||
new()
|
||||
{
|
||||
SerialNumber = 2,
|
||||
ServiceChannelId = 2,
|
||||
MatchCondition = "客户包含('京东')"
|
||||
}
|
||||
};
|
||||
|
||||
var result = AllocationResolver.FilterProvidersDetailed(
|
||||
providers,
|
||||
customerId: 9,
|
||||
Context(customerName: "阿里国际站"));
|
||||
|
||||
result.Passed.Should().HaveCount(1);
|
||||
result.Passed[0].ServiceChannelId.Should().Be(1);
|
||||
result.Skipped[0].Reason.Should().Be("判断条件未命中");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FilterProvidersDetailed_InvalidFormula_ShouldSkip()
|
||||
{
|
||||
var providers = new List<SalesProductAllocationProviderSnapshotDto>
|
||||
{
|
||||
new()
|
||||
{
|
||||
SerialNumber = 1,
|
||||
ServiceChannelId = 1,
|
||||
MatchCondition = "实重()>"
|
||||
}
|
||||
};
|
||||
|
||||
var result = AllocationResolver.FilterProvidersDetailed(providers, null, Context());
|
||||
result.Passed.Should().BeEmpty();
|
||||
result.Skipped.Should().HaveCount(1);
|
||||
result.Skipped[0].Reason.Should().Be("判断条件公式无效");
|
||||
result.PoolCount.Should().Be(1);
|
||||
result.ConditionPassCount.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FilterProvidersDetailed_LegacyCustomerIds_WhenNoFormula()
|
||||
{
|
||||
var providers = new List<SalesProductAllocationProviderSnapshotDto>
|
||||
{
|
||||
new() { SerialNumber = 1, ServiceChannelId = 1, CustomerIds = [10] },
|
||||
new() { SerialNumber = 2, ServiceChannelId = 2, CustomerIds = [99] }
|
||||
};
|
||||
|
||||
var result = AllocationResolver.FilterProvidersDetailed(providers, 10, Context());
|
||||
result.Passed.Should().HaveCount(1);
|
||||
result.Passed[0].ServiceChannelId.Should().Be(1);
|
||||
result.Skipped[0].Stage.Should().Be(AllocationResolver.SkipStageCustomer);
|
||||
result.Skipped[0].Reason.Should().Be("客户不在名单内");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FilterProvidersDetailed_MatchCondition_IgnoresLegacyCustomerIds()
|
||||
{
|
||||
var providers = new List<SalesProductAllocationProviderSnapshotDto>
|
||||
{
|
||||
new()
|
||||
{
|
||||
SerialNumber = 1,
|
||||
ServiceChannelId = 1,
|
||||
CustomerIds = [99],
|
||||
MatchCondition = "货物属性包含('general')"
|
||||
}
|
||||
};
|
||||
|
||||
var result = AllocationResolver.FilterProvidersDetailed(
|
||||
providers,
|
||||
customerId: 1,
|
||||
Context(cargoAttribute: "general"));
|
||||
|
||||
result.Passed.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FilterProvidersDetailed_DeduplicateSameChannel_KeepsHigherWeight()
|
||||
{
|
||||
var providers = new List<SalesProductAllocationProviderSnapshotDto>
|
||||
{
|
||||
new() { SerialNumber = 1, ServiceChannelId = 7, Weight = 10, MatchCondition = "" },
|
||||
new() { SerialNumber = 2, ServiceChannelId = 7, Weight = 30, MatchCondition = "" }
|
||||
};
|
||||
|
||||
var result = AllocationResolver.FilterProvidersDetailed(providers, null, Context());
|
||||
result.ConditionPassCount.Should().Be(2);
|
||||
result.Passed.Should().HaveCount(1);
|
||||
result.Passed[0].Weight.Should().Be(30);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectChannel_CostLowest_ShouldPickCheapest()
|
||||
{
|
||||
@ -56,6 +212,20 @@ public class AllocationResolverTests
|
||||
channel.Id.Should().Be(2);
|
||||
}
|
||||
|
||||
private static OrderRuleEvaluationContext Context(
|
||||
string cargoAttribute = "general",
|
||||
string? customerName = null) =>
|
||||
OrderRuleEvaluationContext.FromOrder(
|
||||
new Order { CargoAttribute = cargoAttribute },
|
||||
string.IsNullOrWhiteSpace(customerName)
|
||||
? null
|
||||
: new Customer { CustomerName = customerName },
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
|
||||
private static List<AllocationResolver.ChannelCandidate> BuildCandidates(
|
||||
params (int channelId, decimal cost, int minTime, int weight)[] rows)
|
||||
{
|
||||
|
||||
@ -6,6 +6,8 @@ using Moq;
|
||||
using Oms.Application.Dtos.Orders;
|
||||
using Oms.Application.Messaging;
|
||||
using Oms.Application.Services.Orders.Batch;
|
||||
using Oms.Application.Storage;
|
||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||
using Oms.Domain.OrdersAggregate.Consts;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
using Oms.Domain.OrdersAggregate.Repositories;
|
||||
@ -18,6 +20,8 @@ public class OrderBatchJobCoordinatorGovernanceTests
|
||||
private readonly Mock<IOrderBatchJobRepository> _batchJobRepository = new(MockBehavior.Strict);
|
||||
private readonly Mock<IOrderRepository> _orderRepository = new(MockBehavior.Loose);
|
||||
private readonly Mock<ICountryRepository> _countryRepository = new(MockBehavior.Loose);
|
||||
private readonly Mock<IServiceChannelRepository> _serviceChannelRepository = new(MockBehavior.Loose);
|
||||
private readonly Mock<IFileStorage> _fileStorage = new(MockBehavior.Loose);
|
||||
private readonly Mock<IOrderBatchJobPublisher> _publisher = new(MockBehavior.Strict);
|
||||
private readonly OrderBatchJobCoordinator _coordinator;
|
||||
|
||||
@ -36,6 +40,8 @@ public class OrderBatchJobCoordinatorGovernanceTests
|
||||
_batchJobRepository.Object,
|
||||
_orderRepository.Object,
|
||||
_countryRepository.Object,
|
||||
_serviceChannelRepository.Object,
|
||||
_fileStorage.Object,
|
||||
_publisher.Object,
|
||||
processor,
|
||||
batchOptions,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user