Compare commits

...

10 Commits

Author SHA1 Message Date
7700baf127 feat:迁移到gitea
All checks were successful
Build and Deploy / deploy (push) Successful in 56s
2026-09-05 16:34:35 +08:00
4fcc0bf7a9 Merge remote-tracking branch 'origin/master' 2026-09-05 16:30:27 +08:00
zzh
b81432c0af feat: 查价结果返回销售产品说明
OrderPriceCheck 输出增加 ProductDescription,供前端报价卡展示线路简要说明。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-05 16:14:16 +08:00
zzh
25b4211b46 feat: 销售产品门户目录、线路排序与公告/到货调整
新增门户目录接口与 route_sort 字段迁移;完善公告创建与包裹到货相关逻辑。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-04 18:39:55 +08:00
zzh
eba1fa4867 feat: 注册绑定所属顾问与岗位职级字典种子
支持 AdvisorCode 写入 CustomerService=Account;补充 employee_position/employee_job_level 初始 SQL。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-03 12:45:13 +08:00
zzh
7e9a5d4ae0 feat: 用户资料支持企业微信客服接入链接
新增 user.WecomKfUrl 存取与迁移脚本,顾问接口同步返回该字段。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 19:09:34 +08:00
zzh
a70f6f2a87 feat: 接入 9610 汇总申报,字段按业务含义命名并对齐运单
含清单/报关单拉取、ITFX 转发配置、迁移脚本与拣货单号扩展。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 17:46:04 +08:00
zzh
78e06d01b9 feat: 工作台扩展代发/拣货统计与业务趋势接口
补充 Fulfill/Pickup 工作台统计 DTO 与查询实现,并扩展订单工作台聚合输出。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 10:59:10 +08:00
zzh
ce1349572d feat: extend receivable-payable APIs with summary, export, and filters
Add summary/export endpoints plus customer code, effective stage, time field, and payment status filtering for finance reconciliation.
2026-08-21 18:21:33 +08:00
zzh
2018e45a7d feat: 代发进度统计、申报HS分列与异常事件表迁移脚本
补充代发进度 Tab 计数、PrimarySkuCodes 与 Im/Ex HS 编码输出;新增 t_order_exception_event 幂等迁移(Handler*、ConcurrencyStamp)并同步 db.sql。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 17:06:16 +08:00
80 changed files with 4508 additions and 106 deletions

View File

@ -0,0 +1,34 @@
# 复制到 admin .gitea/workflows/deploy.yml
name: Build and Deploy
on:
push:
branches: [master, main]
workflow_dispatch:
env:
CSPROJ: src/SimpleAdmin.Api/SimpleAdmin.Api.csproj
IMAGE_NAME: admin_api
DEPLOY_NAME: admin-api
CONTAINER_NAME: admin-api
TCR_REGISTRY: ccr.ccs.tencentyun.com
TCR_NAMESPACE: ideship
jobs:
deploy:
runs-on: k3s-host
steps:
- name: Build and deploy
run: |
set -euxo pipefail
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
TAG="${{ gitea.sha }}"
IMG="${{ env.TCR_REGISTRY }}/${{ env.TCR_NAMESPACE }}/${{ env.IMAGE_NAME }}:${TAG}"
REPO="${{ gitea.repository }}"
test -n "$REPO" && [[ "$REPO" == */* ]]
echo "REPO=$REPO TAG=$TAG host=$(hostname)"
cd /data/k3s
./scripts/ci-publish-container.sh "$REPO" "${{ env.CSPROJ }}" \
"${{ env.IMAGE_NAME }}" "$TAG" "$TAG"
./scripts/ci-deploy-api.sh "${{ env.DEPLOY_NAME }}" \
"${{ env.CONTAINER_NAME }}" "$IMG"

View File

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="xiaolipro.cn" value="https://baget.xiaolipro.cn/v3/index.json" />
<clear />
<add key="ideship" value="https://baget.ideship.com/v3/index.json" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources>
</configuration>

View File

@ -15,6 +15,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "items", "items", "{BE5132AD
oms表结构与接口设计20260427.md = oms表结构与接口设计20260427.md
db.sql = db.sql
Directory.Build.props = Directory.Build.props
.gitea\workflows\deploy.yml = .gitea\workflows\deploy.yml
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleAdmin.Api", "src\SimpleAdmin.Api\SimpleAdmin.Api.csproj", "{229ED167-9ECC-4736-8DFB-522EEA8FF680}"

97
db.sql
View File

@ -34,6 +34,8 @@ DROP TABLE IF EXISTS `t_kb_category`;
DROP TABLE IF EXISTS `t_mcp_tool`;
DROP TABLE IF EXISTS `t_pickup_order_line`;
DROP TABLE IF EXISTS `t_pickup_order`;
DROP TABLE IF EXISTS `t_9610_order_item`;
DROP TABLE IF EXISTS `t_9610_order`;
DROP TABLE IF EXISTS `t_ai_skill`;
DROP TABLE IF EXISTS `t_ai_provider`;
DROP TABLE IF EXISTS `t_xhs_ai_provider`;
@ -490,6 +492,7 @@ CREATE TABLE `t_order_exception_event` (
`ClaimReason` VARCHAR(512) NULL COMMENT '理赔原因',
`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 '更新人',
@ -506,6 +509,7 @@ CREATE TABLE `t_sales_product` (
`MinTimeliness` INT NULL COMMENT '最小时效(天)',
`MaxTimeliness` INT NULL COMMENT '最大时效(天)',
`TradeTerms` VARCHAR(16) NULL COMMENT '贸易条款:DDP/DDU',
`RouteSort` INT NOT NULL DEFAULT 0 COMMENT '官网线路排序,越小越靠前',
`AdditionalServices` VARCHAR(512) NULL COMMENT '可选附加服务字典code JSON数组',
`CustomerWaybill` VARCHAR(32) NULL COMMENT '系统面单/客户面单',
`BlacklistAreaId` INT NULL COMMENT '黑名单区ID',
@ -527,6 +531,7 @@ CREATE TABLE `t_sales_product` (
PRIMARY KEY (`Id`),
UNIQUE KEY `uk_sp_code` (`ProductCode`),
KEY `idx_sp_group_active` (`ProductGroup`, `IsActive`),
KEY `idx_sp_route_sort` (`RouteSort`, `IsActive`),
KEY `idx_sp_scheme` (`Scheme`),
KEY `idx_sp_weight_rule` (`WeightRuleId`),
KEY `idx_sp_weight_rounding` (`WeightRoundingId`)
@ -1919,6 +1924,98 @@ CREATE TABLE `t_pickup_order_line` (
KEY `idx_pickup_line_header` (`PickupOrderId`, `IsDeleted`)
) COMMENT='揽收单关联物流订单(1揽收:N物流)';
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汇总申报单';
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申报商品明细';
CREATE TABLE `t_kb_category` (
`Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
`Code` VARCHAR(64) NOT NULL COMMENT '稳定键,如 pricing',

View File

@ -0,0 +1,80 @@
-- 员工档案「岗位 / 职级」字典初始数据(幂等:已存在同 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
);

View File

@ -0,0 +1,36 @@
-- t_sales_product:官网线路展示排序(幂等;列已存在则跳过)
-- 数值越小越靠前;默认 0
SET @db := DATABASE();
SET @col := (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db
AND TABLE_NAME = 't_sales_product'
AND COLUMN_NAME = 'RouteSort'
);
SET @sql := IF(
@col = 0,
'ALTER TABLE `t_sales_product` ADD COLUMN `RouteSort` INT NOT NULL DEFAULT 0 COMMENT ''官网线路排序,越小越靠前'' AFTER `TradeTerms`',
'SELECT ''skip RouteSort'' AS msg'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @idx := (
SELECT COUNT(*)
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db
AND TABLE_NAME = 't_sales_product'
AND INDEX_NAME = 'idx_sp_route_sort'
);
SET @sql_idx := IF(
@idx = 0,
'ALTER TABLE `t_sales_product` ADD KEY `idx_sp_route_sort` (`RouteSort`, `IsActive`)',
'SELECT ''skip idx_sp_route_sort'' AS msg'
);
PREPARE stmt_idx FROM @sql_idx;
EXECUTE stmt_idx;
DEALLOCATE PREPARE stmt_idx;

View File

@ -0,0 +1,81 @@
-- 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;

View File

@ -0,0 +1,118 @@
-- 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;

View File

@ -0,0 +1,170 @@
-- 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;

View File

@ -0,0 +1,122 @@
-- 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;

View File

@ -0,0 +1,90 @@
-- 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;

View File

@ -0,0 +1,118 @@
-- 订单异常事件表:补齐 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;

View File

@ -1,14 +1,58 @@
-- 代发订单:t_order 履约类型字段(逐条执行;若列已存在请跳过对应 ALTER)
-- 代发订单:t_order 履约类型字段(幂等;列/索引已存在则自动跳过)
-- MySQL 5.7+ / 8.0
ALTER TABLE `t_order`
ADD COLUMN `FulfillmentType` VARCHAR(16) NULL DEFAULT 'direct' COMMENT '履约类型 direct/warehouse' AFTER `AdditionalServices`;
SET @db := DATABASE();
ALTER TABLE `t_order`
ADD COLUMN `FulfillmentSource` VARCHAR(32) NULL COMMENT '代发来源 storeFulfill/postBind' AFTER `FulfillmentType`;
-- 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;
ALTER TABLE `t_order`
ADD KEY `idx_ord_fulfillment_customer` (`FulfillmentType`, `CustomerId`, `OrderStatus`);
-- 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

View File

@ -0,0 +1,49 @@
-- 揽收明细:补齐运单号/客户单号快照(幂等;列已存在则跳过)
-- 对应实体 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` = '');

View File

@ -0,0 +1,20 @@
-- 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;

View File

@ -40,6 +40,11 @@ public class SalesProductDetailOutput
/// </summary>
public string? TradeTerms { get; set; }
/// <summary>
/// 官网线路排序,数值越小越靠前。
/// </summary>
public int RouteSort { get; set; }
/// <summary>
/// 可选附加服务字典 code JSON 数组。
/// </summary>

View File

@ -35,6 +35,11 @@ public class SalesProductListOutput
/// </summary>
public int? MaxTimeliness { get; set; }
/// <summary>
/// 官网线路排序,数值越小越靠前。
/// </summary>
public int RouteSort { get; set; }
/// <summary>
/// 可选附加服务字典 code JSON 数组。
/// </summary>

View File

@ -0,0 +1,84 @@
namespace Oms.Application.Dtos.Channels;
/// <summary>
/// 官网线路目录出参(公开字段)。
/// </summary>
public class SalesProductPortalCatalogOutput
{
/// <summary>
/// 分组列表(已排除国内揽收,且仅含有产品的分组)。
/// </summary>
public List<SalesProductPortalGroupOutput> Groups { get; set; } = [];
/// <summary>
/// 启用中的销售产品(已排除国内揽收)。
/// </summary>
public List<SalesProductPortalItemOutput> Items { get; set; } = [];
}
/// <summary>
/// 官网线路分组。
/// </summary>
public class SalesProductPortalGroupOutput
{
/// <summary>
/// 分组编码(字典 code)。
/// </summary>
public string Code { get; set; } = string.Empty;
/// <summary>
/// 分组名称。
/// </summary>
public string Name { get; set; } = string.Empty;
}
/// <summary>
/// 官网线路产品卡片字段。
/// </summary>
public class SalesProductPortalItemOutput
{
/// <summary>
/// 主键。
/// </summary>
public int Id { get; set; }
/// <summary>
/// 产品名称。
/// </summary>
public string ProductName { get; set; } = string.Empty;
/// <summary>
/// 产品分组编码。
/// </summary>
public string? ProductGroup { get; set; }
/// <summary>
/// 产品分组名称。
/// </summary>
public string ProductGroupName { get; set; } = string.Empty;
/// <summary>
/// 最小时效(天)。
/// </summary>
public int? MinTimeliness { get; set; }
/// <summary>
/// 最大时效(天)。
/// </summary>
public int? MaxTimeliness { get; set; }
/// <summary>
/// 贸易条款(DDP/DDU)。
/// </summary>
public string? TradeTerms { get; set; }
/// <summary>
/// 官网线路排序,数值越小越靠前。
/// </summary>
public int RouteSort { get; set; }
/// <summary>
/// 产品说明。
/// </summary>
public string? ProductDescription { get; set; }
}

View File

@ -42,6 +42,11 @@ public class SalesProductSaveInput
/// </summary>
public string? TradeTerms { get; set; }
/// <summary>
/// 官网线路排序,数值越小越靠前。
/// </summary>
public int RouteSort { get; set; }
/// <summary>
/// 可选附加服务字典 code JSON 数组。
/// </summary>

View File

@ -118,4 +118,10 @@ public class CreateCustomerInput
[StringLength(16)]
public string? InviteCode { get; set; }
/// <summary>
/// 顾问邀请码(所属顾问绑定,值为员工 Account;仅 CustomerService 为空时生效)。
/// </summary>
[StringLength(64)]
public string? AdvisorCode { get; set; }
}

View File

@ -19,4 +19,9 @@ public class CustomerServiceAdvisorOutput
/// 微信二维码(URL 或 Base64)。
/// </summary>
public string? WechatQrCode { get; set; }
/// <summary>
/// 企业微信客服接入链接。
/// </summary>
public string? WecomKfUrl { get; set; }
}

View File

@ -32,12 +32,32 @@ public class ReceivablePayablePaginatedInput
public int? CustomerId { get; set; }
/// <summary>
/// 订单创建时间起。
/// 客户编码关键词(模糊匹配)。
/// </summary>
public string? CustomerCode { get; set; }
/// <summary>
/// 仅保留各运单费用项的当前生效阶段。
/// </summary>
public bool EffectiveStageOnly { get; set; }
/// <summary>
/// 订单创建时间起(timeField=createTime 时生效,兼容旧参数)。
/// </summary>
public DateTime? StartTime { get; set; }
/// <summary>
/// 订单创建时间止。
/// 订单创建时间止(timeField=createTime 时生效,兼容旧参数)。
/// </summary>
public DateTime? EndTime { get; set; }
/// <summary>
/// 时间筛选字段:createTime / receiveTime / shipTime / payTime。
/// </summary>
public string? TimeField { get; set; }
/// <summary>
/// 支付状态:0 未支付,1 已支付。
/// </summary>
public int? IsPaid { get; set; }
}

View File

@ -5,6 +5,8 @@ namespace Oms.Application.Dtos.Finance;
/// </summary>
public class ReceivablePayablePaginatedOutput
{
public int OrderId { get; set; }
public string ServiceNo { get; set; } = string.Empty;
public string? ClientNo { get; set; }
@ -37,6 +39,8 @@ public class ReceivablePayablePaginatedOutput
public string CostItem { get; set; } = string.Empty;
public string? CostItemName { get; set; }
public decimal Amount { get; set; }
public string Currency { get; set; } = string.Empty;

View File

@ -0,0 +1,30 @@
namespace Oms.Application.Dtos.Finance;
/// <summary>
/// 应收应付汇总出参。
/// </summary>
public class ReceivablePayableSummaryOutput
{
/// <summary>
/// 匹配筛选条件的费用明细总行数。
/// </summary>
public int TotalLines { get; set; }
/// <summary>
/// 按费用类型 + 币种汇总。
/// </summary>
public List<ReceivablePayableSummaryItemOutput> Items { get; set; } = [];
}
public class ReceivablePayableSummaryItemOutput
{
public string FeeType { get; set; } = string.Empty;
public string? FeeTypeName { get; set; }
public string Currency { get; set; } = string.Empty;
public decimal TotalAmount { get; set; }
public int LineCount { get; set; }
}

View File

@ -0,0 +1,313 @@
using System.ComponentModel.DataAnnotations;
namespace Oms.Application.Dtos.Orders;
public class Customs9610ListInput
{
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 30;
public int? CustomerId { get; set; }
/// <summary>
/// pending / declared;空=全部。
/// </summary>
public string? Status { get; set; }
public string? Keyword { get; set; }
public DateTime? CreateTimeStart { get; set; }
public DateTime? CreateTimeEnd { get; set; }
}
public class Customs9610IdInput
{
[Required]
public int Id { get; set; }
}
public class Customs9610IdsInput
{
[Required]
[MinLength(1)]
public List<int> Ids { get; set; } = [];
}
public class Customs9610StatusCountsOutput
{
public int Total { get; set; }
public int Pending { get; set; }
public int Declared { get; set; }
}
public class Customs9610ItemInput
{
public int? Id { get; set; }
public int? OrderDeclarationId { get; set; }
[Required]
[StringLength(32)]
public string Sku { get; set; } = string.Empty;
[Required]
[StringLength(250)]
public string CnName { get; set; } = string.Empty;
[StringLength(10)]
public string? ExHsCode { get; set; }
[StringLength(255)]
public string? DeclareElement { get; set; }
[Required]
public decimal UnitPrice { get; set; }
[Required]
public decimal Quantity { get; set; }
public decimal? Freight { get; set; }
public int SortNo { get; set; }
}
public class Customs9610UpdateInput
{
[Required]
public int Id { get; set; }
[Required]
[StringLength(16)]
public string PlatformCode { get; set; } = string.Empty;
[StringLength(18)]
public string? EcomCorpCode { get; set; }
[StringLength(100)]
public string? EcomCorpName { get; set; }
[StringLength(18)]
public string? ProducerCode { get; set; }
[StringLength(100)]
public string? ProducerName { get; set; }
[Required]
[StringLength(48)]
public string ClientNo { get; set; } = string.Empty;
[Required]
[StringLength(48)]
public string ServiceNo { get; set; } = string.Empty;
[Required]
[StringLength(1)]
public string BusinessType { get; set; } = "B";
[StringLength(10)]
public string? Region { get; set; }
[StringLength(2)]
public string? AgentDeclare { get; set; }
[StringLength(18)]
public string? ActualSellerCode { get; set; }
[StringLength(1)]
public string? PreRecordFlag { get; set; }
public bool IsSummarySimple { get; set; }
[Required]
public decimal GrossWeight { get; set; }
[Required]
[StringLength(4)]
public string Currency { get; set; } = "CNY";
[Required]
[StringLength(4)]
public string DestCountry { get; set; } = string.Empty;
public decimal? Freight { get; set; }
[StringLength(32)]
public string? ShipmentNo { get; set; }
[MinLength(1)]
public List<Customs9610ItemInput> Items { get; set; } = [];
}
public class Customs9610ItemOutput
{
public int Id { get; set; }
public int? OrderDeclarationId { get; set; }
public string Sku { get; set; } = string.Empty;
public string CnName { get; set; } = string.Empty;
public string? ExHsCode { get; set; }
public string? DeclareElement { get; set; }
public string? Barcode { get; set; }
public string? DestCountry { get; set; }
public string? ItemCurrency { get; set; }
public decimal UnitPrice { get; set; }
public decimal? Amount { get; set; }
public decimal Quantity { get; set; }
public decimal? LegalQty { get; set; }
public string? Unit { get; set; }
public string? LegalUnit { get; set; }
public decimal? Freight { get; set; }
public int SortNo { get; set; }
}
public class Customs9610OrderOutput
{
public int Id { get; set; }
public int OrderId { get; set; }
public string? ServiceNo { get; set; }
public int CustomerId { get; set; }
public string? CustomerName { get; set; }
public string Status { get; set; } = string.Empty;
public string PlatformCode { get; set; } = string.Empty;
public string? EcomCorpCode { get; set; }
public string? EcomCorpName { get; set; }
public string? ProducerCode { get; set; }
public string? ProducerName { get; set; }
public string ClientNo { get; set; } = string.Empty;
public string BusinessType { get; set; } = "B";
public string? Region { get; set; }
public string? AgentDeclare { get; set; }
public string? ActualSellerCode { get; set; }
public string? PreRecordFlag { get; set; }
public bool IsSummarySimple { get; set; }
public decimal GrossWeight { get; set; }
public string Currency { get; set; } = "CNY";
public string DestCountry { get; set; } = string.Empty;
public decimal? Freight { get; set; }
public string? FreightCurrency { get; set; }
public int? FreightMark { get; set; }
public int? PackageType { get; set; }
public int? PackageQty { get; set; }
public string? ShipmentNo { get; set; }
public string? CustomsOrderNo { get; set; }
public string? DownstreamRefNo { get; set; }
public string? EnterpriseBizNo { get; set; }
public string? ManifestNo { get; set; }
public string? ClearanceTime { get; set; }
public string? CustomsInboundTime { get; set; }
public string? CustomsState { get; set; }
public string? DeclareCustomsCode { get; set; }
public string? PlatformCorpCode { get; set; }
public string? PlatformName { get; set; }
public string? LogisticsCorpCode { get; set; }
public string? LogisticsCorpName { get; set; }
public string? IeMark { get; set; }
public string? ExportPortCode { get; set; }
public string? ExportDate { get; set; }
public string? DeclareAgentCode { get; set; }
public string? DeclareAgentName { get; set; }
public string? TradeModeCode { get; set; }
public string? TransportMode { get; set; }
public DateTime? DeclareTime { get; set; }
public DateTime? ManifestSyncTime { get; set; }
public string? FailReason { get; set; }
public DateTime CreateTime { get; set; }
public DateTime? UpdateTime { get; set; }
public List<Customs9610ItemOutput>? Items { get; set; }
}
public class Customs9610DeclareResult
{
public int Id { get; set; }
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
public string? CustomsOrderNo { get; set; }
public string? Status { get; set; }
public int? HttpStatusCode { get; set; }
public string? ResponseBody { get; set; }
}
public class Customs9610DeclareBatchOutput
{
public int SuccessCount { get; set; }
public int FailCount { get; set; }
public List<Customs9610DeclareResult> Items { get; set; } = [];
}

View File

@ -0,0 +1,19 @@
namespace Oms.Application.Dtos.Orders;
/// <summary>
/// 工作台代发订单统计。
/// </summary>
public class FulfillWorkplaceStatsOutput
{
/// <summary>待绑定包裹/库存。</summary>
public int PendingBind { get; set; }
/// <summary>国内快递在途。</summary>
public int DomesticInTransit { get; set; }
/// <summary>已到仓,待仓库拣货。</summary>
public int ArrivedPendingPick { get; set; }
/// <summary>已出库(拣货扣减完成)。</summary>
public int OutboundDone { get; set; }
}

View File

@ -23,6 +23,13 @@ public class OrderExportInput
[Newtonsoft.Json.JsonProperty("fulfillmentType")]
public string? FulfillmentType { get; set; }
/// <summary>
/// 代发进度筛选(pendingBind / domesticInTransit / arrivedPendingPick / outboundDone)。
/// </summary>
[Newtonsoft.Json.JsonProperty("fulfillProgress")]
[global::System.Text.Json.Serialization.JsonPropertyName("fulfillProgress")]
public string? FulfillProgress { get; set; }
/// <summary>
/// 关键词。
/// </summary>

View File

@ -51,10 +51,20 @@ public class OrderItemOutput
public decimal? Weight { get; set; }
/// <summary>
/// HS 编码。
/// HS 编码(兼容旧字段,优先出口编码)。
/// </summary>
public string? HsCode { get; set; }
/// <summary>
/// 进口 HS 编码。
/// </summary>
public string? ImHsCode { get; set; }
/// <summary>
/// 出口 HS 编码。
/// </summary>
public string? ExHsCode { get; set; }
/// <summary>
/// SKU 图片。
/// </summary>

View File

@ -175,6 +175,11 @@ public class OrderOutput
/// </summary>
public string? PrimaryBinCode { get; set; }
/// <summary>
/// 绑定 SKU 编码汇总(逗号分隔,列表展示/拣货筛选用)。
/// </summary>
public string? PrimarySkuCodes { get; set; }
/// <summary>
/// 关联揽收单 Id。
/// </summary>

View File

@ -57,6 +57,11 @@ public class OrderPriceCheckOutput
/// </summary>
public string? ProductGroupName { get; set; }
/// <summary>
/// 产品说明(线路简要说明,对客展示)。
/// </summary>
public string? ProductDescription { get; set; }
/// <summary>
/// 体积重量。
/// </summary>

View File

@ -20,4 +20,10 @@ public class OrderStatusCountsOutput
/// </summary>
public Dictionary<string, OrderHandleStatusCountsOutput> HandleCounts { get; set; } =
new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// 代发进度分组数量(fulfillmentType=warehouse 时返回)。
/// </summary>
public Dictionary<string, int> FulfillProgressCounts { get; set; } =
new(StringComparer.OrdinalIgnoreCase);
}

View File

@ -21,4 +21,9 @@ public class OrderUploadInput
/// 默认发件地址 ID(地址管理中的发件人地址)。
/// </summary>
public int? SenderAddressId { get; set; }
/// <summary>
/// 客户主键(管理端代客下单时必传)。
/// </summary>
public int? CustomerId { get; set; }
}

View File

@ -0,0 +1,43 @@
namespace Oms.Application.Dtos.Orders;
/// <summary>
/// 工作台业务线趋势(运单 / 代发 / 揽收新增量)。
/// </summary>
public class OrderWorkplaceBusinessTrendOutput
{
/// <summary>
/// 维度:day / week / month。
/// </summary>
public string Dimension { get; set; } = "day";
/// <summary>
/// 趋势点(按时间正序)。
/// </summary>
public List<OrderWorkplaceBusinessTrendPointOutput> Points { get; set; } = [];
}
/// <summary>
/// 单个时间桶的三类业务量。
/// </summary>
public class OrderWorkplaceBusinessTrendPointOutput
{
/// <summary>
/// 展示标签。
/// </summary>
public string Label { get; set; } = string.Empty;
/// <summary>
/// 运单新增(非仓内代发)。
/// </summary>
public int WaybillCount { get; set; }
/// <summary>
/// 代发订单新增(仓内履约)。
/// </summary>
public int FulfillCount { get; set; }
/// <summary>
/// 揽收单新增。
/// </summary>
public int PickupCount { get; set; }
}

View File

@ -64,4 +64,14 @@ public class OrderWorkplaceStatsOutput
/// 店铺订单待处理数量(履约状态 pending)。
/// </summary>
public int StoreOrderPending { get; set; }
/// <summary>
/// 代发订单统计(仓内履约)。
/// </summary>
public FulfillWorkplaceStatsOutput Fulfill { get; set; } = new();
/// <summary>
/// 揽收订单统计。
/// </summary>
public PickupWorkplaceStatsOutput Pickup { get; set; } = new();
}

View File

@ -100,9 +100,24 @@ public class PickupOrderListInput
public string? Keyword { get; set; }
public int? WarehouseId { get; set; }
public DateTime? CreateTimeStart { get; set; }
public DateTime? CreateTimeEnd { get; set; }
public DateTime? ExpectPickupTimeStart { get; set; }
public DateTime? ExpectPickupTimeEnd { get; set; }
}
public class PickupOrderStatusCountsOutput
{
public int Total { get; set; }
public int PendingPayment { get; set; }
public Dictionary<string, int> Counts { get; set; } = new(StringComparer.OrdinalIgnoreCase);
}
public class PickupOrderConfirmFreightInput
@ -267,6 +282,8 @@ public class PickupOrderOutput
public DateTime? CreateTime { get; set; }
public DateTime? UpdateTime { get; set; }
public List<PickupOrderLineOutput> Orders { get; set; } = [];
public List<PickupOrderRejectItem> Rejected { get; set; } = [];

View File

@ -0,0 +1,19 @@
namespace Oms.Application.Dtos.Orders;
/// <summary>
/// 工作台揽收订单统计。
/// </summary>
public class PickupWorkplaceStatsOutput
{
/// <summary>待支付(有应收且未支付,不含取消/失败)。</summary>
public int PendingPayment { get; set; }
/// <summary>草稿,待确认下发。</summary>
public int PendingSchedule { get; set; }
/// <summary>已预约/已派单,待取件。</summary>
public int PendingPickup { get; set; }
/// <summary>已取件,待到仓。</summary>
public int PendingInbound { get; set; }
}

View File

@ -11,6 +11,11 @@ public class UserProfileData
public string? WechatId { get; set; }
public string? WechatQrCode { get; set; }
/// <summary>
/// 企业微信客服接入链接(微信外接入)。
/// </summary>
public string? WecomKfUrl { get; set; }
}
public class OmsUserCreateInput : UserCreateDto
@ -22,6 +27,8 @@ public class OmsUserCreateInput : UserCreateDto
public string? WechatId { get; set; }
public string? WechatQrCode { get; set; }
public string? WecomKfUrl { get; set; }
}
public class OmsUserUpdateInput : UserUpdateDto
@ -31,6 +38,8 @@ public class OmsUserUpdateInput : UserUpdateDto
public string? WechatId { get; set; }
public string? WechatQrCode { get; set; }
public string? WecomKfUrl { get; set; }
}
public class OmsUserDetailDto : UserDto
@ -42,4 +51,6 @@ public class OmsUserDetailDto : UserDto
public string? WechatId { get; set; }
public string? WechatQrCode { get; set; }
public string? WecomKfUrl { get; set; }
}

View File

@ -32,14 +32,13 @@ public class NoticeCreateInput
public byte IsPopup { get; set; }
/// <summary>
/// 接收对象类型:user/customer。
/// 接收对象类型:user/customer。公告全体广播时可空(后端按 user 占位)。
/// </summary>
[Required]
[StringLength(16)]
public string ReceiverType { get; set; } = string.Empty;
/// <summary>
/// 接收主体 ID 列表。
/// 接收主体 ID 列表。公告传空表示全体生效;站内信必填。
/// </summary>
public List<int> ReceiverIds { get; set; } = [];
}

View File

@ -579,6 +579,7 @@ public class WarehouseParcelArrivalListInput : WarehousePageInput
public class WarehouseParcelArrivalCreateInput
{
/// <summary>已废弃:到仓按仓库+快递单号定位包裹,客户归属取自包裹。</summary>
public int? CustomerId { get; set; }
[Required]

View File

@ -21,4 +21,15 @@ public interface IItfxForwardClient
string appSecret,
object body,
CancellationToken cancellationToken = default);
/// <summary>
/// 通用 ForwardWithSubscribe(standardCode 可配,如 9610 申报暂定码)。
/// </summary>
Task<ItfxForwardResult> ForwardWithSubscribeAsync(
string standardCode,
string supplierCode,
string appKey,
string appSecret,
object body,
CancellationToken cancellationToken = default);
}

View File

@ -21,7 +21,24 @@ internal sealed class ItfxForwardClient(
WriteIndented = true
};
public async Task<ItfxForwardResult> ForwardCreateOrderOneAsync(
public Task<ItfxForwardResult> ForwardCreateOrderOneAsync(
string supplierCode,
string appKey,
string appSecret,
object body,
CancellationToken cancellationToken = default)
{
return ForwardWithSubscribeAsync(
CreateOrderOneStandardCode,
supplierCode,
appKey,
appSecret,
body,
cancellationToken);
}
public async Task<ItfxForwardResult> ForwardWithSubscribeAsync(
string standardCode,
string supplierCode,
string appKey,
string appSecret,
@ -34,9 +51,19 @@ internal sealed class ItfxForwardClient(
throw new InvalidOperationException("Itfx BaseUrl not configured");
}
if (string.IsNullOrWhiteSpace(standardCode))
{
throw new InvalidOperationException("Itfx standardCode is empty");
}
if (string.IsNullOrWhiteSpace(supplierCode))
{
throw new InvalidOperationException("Itfx supplierCode is empty");
}
var root = $"{settings.BaseUrl.Trim().TrimEnd('/')}/InterfaceForward/ForwardWithSubscribe";
var url =
$"{root}?standardCode={CreateOrderOneStandardCode}&supplierCode={Uri.EscapeDataString(supplierCode)}";
$"{root}?standardCode={Uri.EscapeDataString(standardCode.Trim())}&supplierCode={Uri.EscapeDataString(supplierCode.Trim())}";
var requestJson = JsonSerializer.Serialize(body, JsonOptions);
using var request = new HttpRequestMessage(HttpMethod.Post, url);

View File

@ -13,4 +13,24 @@ public class ItfxGatewayOptions
public string BaseUrl { get; set; } = "http://api.nanafa.com/itfx";
public int TimeoutSeconds { get; set; } = 30;
/// <summary>
/// 9610 汇总申报上游标准码(暂定,可在配置覆盖)。
/// </summary>
public string Customs9610StandardCode { get; set; } = "customs9610Declare";
/// <summary>
/// 9610 获取清单数据上游标准码(暂定)。
/// </summary>
public string Customs9610ManifestStandardCode { get; set; } = "customs9610Manifest";
/// <summary>
/// 9610 获取报关单上游标准码(暂定)。
/// </summary>
public string Customs9610DeclarationFormStandardCode { get; set; } = "customs9610DeclarationForm";
/// <summary>
/// 9610 申报默认 supplierCode;空则回退订单 DownstreamSupplierCode。
/// </summary>
public string? Customs9610SupplierCode { get; set; }
}

View File

@ -30,6 +30,7 @@ public class CustomerServiceAdvisorResolver(FakeRbacDbContext dbContext) : IScop
Name = row.Name,
WechatId = row.WechatId,
WechatQrCode = row.WechatQrCode,
WecomKfUrl = row.WecomKfUrl,
};
}
@ -44,7 +45,8 @@ public class CustomerServiceAdvisorResolver(FakeRbacDbContext dbContext) : IScop
SELECT
u.`Name` AS `Name`,
u.`WechatId` AS `WechatId`,
u.`WechatQrCode` AS `WechatQrCode`
u.`WechatQrCode` AS `WechatQrCode`,
u.`WecomKfUrl` AS `WecomKfUrl`
FROM `user` u
WHERE u.`IsDeleted` = 0
AND u.`userid` = {0}
@ -62,7 +64,8 @@ public class CustomerServiceAdvisorResolver(FakeRbacDbContext dbContext) : IScop
SELECT
u.`Name` AS `Name`,
u.`WechatId` AS `WechatId`,
u.`WechatQrCode` AS `WechatQrCode`
u.`WechatQrCode` AS `WechatQrCode`,
u.`WecomKfUrl` AS `WecomKfUrl`
FROM `user` u
WHERE u.`IsDeleted` = 0
AND (u.`Name` = {0} OR u.`Account` = {0})
@ -82,5 +85,7 @@ public class CustomerServiceAdvisorResolver(FakeRbacDbContext dbContext) : IScop
public string? WechatId { get; set; }
public string? WechatQrCode { get; set; }
public string? WecomKfUrl { get; set; }
}
}

View File

@ -32,6 +32,7 @@ internal static class OmsUserProfileMapper
detail.EmployeeNo = profile.EmployeeNo;
detail.WechatId = profile.WechatId;
detail.WechatQrCode = profile.WechatQrCode;
detail.WecomKfUrl = profile.WecomKfUrl;
return detail;
}
}

View File

@ -20,7 +20,8 @@ public class UserProfileRepository(FakeRbacDbContext dbContext) : IScopedDepende
u.`Phone` AS `Phone`,
u.`EmployeeNo` AS `EmployeeNo`,
u.`WechatId` AS `WechatId`,
u.`WechatQrCode` AS `WechatQrCode`
u.`WechatQrCode` AS `WechatQrCode`,
u.`WecomKfUrl` AS `WecomKfUrl`
FROM `user` u
WHERE u.`Id` = {0} AND u.`IsDeleted` = 0
LIMIT 1
@ -41,6 +42,7 @@ public class UserProfileRepository(FakeRbacDbContext dbContext) : IScopedDepende
EmployeeNo = row.EmployeeNo,
WechatId = row.WechatId,
WechatQrCode = row.WechatQrCode,
WecomKfUrl = row.WecomKfUrl,
};
}
@ -60,7 +62,8 @@ public class UserProfileRepository(FakeRbacDbContext dbContext) : IScopedDepende
u.`Phone` AS `Phone`,
u.`EmployeeNo` AS `EmployeeNo`,
u.`WechatId` AS `WechatId`,
u.`WechatQrCode` AS `WechatQrCode`
u.`WechatQrCode` AS `WechatQrCode`,
u.`WecomKfUrl` AS `WecomKfUrl`
FROM `user` u
WHERE u.`Id` IN ({inClause}) AND u.`IsDeleted` = 0
""";
@ -74,6 +77,7 @@ public class UserProfileRepository(FakeRbacDbContext dbContext) : IScopedDepende
EmployeeNo = x.EmployeeNo,
WechatId = x.WechatId,
WechatQrCode = x.WechatQrCode,
WecomKfUrl = x.WecomKfUrl,
});
}
@ -85,7 +89,8 @@ public class UserProfileRepository(FakeRbacDbContext dbContext) : IScopedDepende
`Phone` = {1},
`EmployeeNo` = {2},
`WechatId` = {3},
`WechatQrCode` = {4}
`WechatQrCode` = {4},
`WecomKfUrl` = {5}
WHERE `Id` = {0} AND `IsDeleted` = 0
""";
@ -98,6 +103,7 @@ public class UserProfileRepository(FakeRbacDbContext dbContext) : IScopedDepende
(object?)NormalizeOptional(profile.EmployeeNo),
(object?)NormalizeOptional(profile.WechatId),
(object?)NormalizeOptional(profile.WechatQrCode),
(object?)NormalizeOptional(profile.WecomKfUrl),
],
cancellationToken);
}
@ -112,7 +118,8 @@ public class UserProfileRepository(FakeRbacDbContext dbContext) : IScopedDepende
SET
`Phone` = {1},
`WechatId` = {2},
`WechatQrCode` = {3}
`WechatQrCode` = {3},
`WecomKfUrl` = {4}
WHERE `Id` = {0} AND `IsDeleted` = 0
""";
@ -123,6 +130,7 @@ public class UserProfileRepository(FakeRbacDbContext dbContext) : IScopedDepende
(object?)NormalizeOptional(profile.Phone),
(object?)NormalizeOptional(profile.WechatId),
(object?)NormalizeOptional(profile.WechatQrCode),
(object?)NormalizeOptional(profile.WecomKfUrl),
],
cancellationToken);
}
@ -208,6 +216,8 @@ public class UserProfileRepository(FakeRbacDbContext dbContext) : IScopedDepende
public string? WechatId { get; set; }
public string? WechatQrCode { get; set; }
public string? WecomKfUrl { get; set; }
}
private sealed class UserProfileListRow
@ -221,6 +231,8 @@ public class UserProfileRepository(FakeRbacDbContext dbContext) : IScopedDepende
public string? WechatId { get; set; }
public string? WechatQrCode { get; set; }
public string? WecomKfUrl { get; set; }
}
private sealed class EmployeeNoRow

View File

@ -211,6 +211,7 @@ public partial class SalesProductService
MinTimeliness = product.MinTimeliness,
MaxTimeliness = product.MaxTimeliness,
TradeTerms = product.TradeTerms,
RouteSort = product.RouteSort,
AdditionalServices = product.AdditionalServices,
CustomerWaybill = product.CustomerWaybill,
LabelTemplateId = product.LabelTemplateId,
@ -221,7 +222,8 @@ public partial class SalesProductService
WeightRoundingId = product.WeightRoundingId,
ProductDescription = product.ProductDescription,
PreStorageScript = product.PreStorageScript,
CustomerGuideMarkdown = product.CustomerGuideMarkdown,
// 对客说明不常变动,且有独立保存入口;规则发布快照跳过,避免配置发布被其 diff 干扰
CustomerGuideMarkdown = null,
IsActive = product.IsActive,
Scheme = product.Scheme
};
@ -333,6 +335,7 @@ public partial class SalesProductService
entity.MinTimeliness = input.MinTimeliness;
entity.MaxTimeliness = input.MaxTimeliness;
entity.TradeTerms = NormalizeTradeTerms(input.TradeTerms);
entity.RouteSort = input.RouteSort < 0 ? 0 : input.RouteSort;
entity.AdditionalServices = NormalizeAdditionalServices(input.AdditionalServices);
entity.CustomerWaybill = input.CustomerWaybill;
entity.LabelTemplateId = input.LabelTemplateId;
@ -344,7 +347,7 @@ public partial class SalesProductService
entity.WeightRoundingId = input.WeightRoundingId;
entity.ProductDescription = input.ProductDescription;
entity.PreStorageScript = input.PreStorageScript;
entity.CustomerGuideMarkdown = input.CustomerGuideMarkdown;
// 对客说明仅通过 /customer-guide/save 维护,产品配置保存/发布不覆盖
entity.IsActive = input.IsActive;
entity.Scheme = string.IsNullOrWhiteSpace(input.Scheme) ? AllocationSchemeCodes.CostLowest : input.Scheme;
}
@ -444,6 +447,7 @@ public partial class SalesProductService
ProductGroup = entity.ProductGroup,
MinTimeliness = entity.MinTimeliness,
MaxTimeliness = entity.MaxTimeliness,
RouteSort = entity.RouteSort,
AdditionalServices = entity.AdditionalServices,
CustomerWaybill = entity.CustomerWaybill,
LabelTemplateId = entity.LabelTemplateId,
@ -473,6 +477,7 @@ public partial class SalesProductService
MinTimeliness = entity.MinTimeliness,
MaxTimeliness = entity.MaxTimeliness,
TradeTerms = entity.TradeTerms,
RouteSort = entity.RouteSort,
AdditionalServices = entity.AdditionalServices,
CustomerWaybill = entity.CustomerWaybill,
LabelTemplateId = entity.LabelTemplateId,

View File

@ -0,0 +1,130 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Channels;
using Oms.Domain.Shared.Enums;
using Oms.Domain.SystemAggregate.Repositories;
namespace Oms.Application.Services.Channels;
public partial class SalesProductService
{
private const string SalesProductGroupDictType = "sales_product_group";
private const string UngroupedProductGroupCode = "_ungrouped";
private const string DomesticPickupGroupNameKeyword = "国内揽收";
private IDictRepository DictRepository =>
LazyServiceProvider.GetRequiredService<IDictRepository>();
/// <summary>
/// 官网线路目录(匿名):仅启用产品,排除「国内揽收」分组。
/// </summary>
[AllowAnonymous]
[HttpGet("/channel/sales-product/portal/catalog")]
public async Task<SalesProductPortalCatalogOutput> GetPortalCatalogAsync()
{
var products = await SalesProductRepository.GetListAsync(x => x.IsActive == EnableStatus.Enabled);
var dictRows = await DictRepository.GetListAsync(x => x.DictType == SalesProductGroupDictType);
var domesticPickupGroupCodes = dictRows
.Where(x => !string.IsNullOrWhiteSpace(x.DictName)
&& x.DictName.Trim().Contains(DomesticPickupGroupNameKeyword))
.Select(x => (x.DictCode ?? string.Empty).Trim())
.Where(code => !string.IsNullOrWhiteSpace(code))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var groupNameMap = dictRows
.Where(x => !string.IsNullOrWhiteSpace(x.DictCode))
.GroupBy(x => x.DictCode!.Trim(), StringComparer.OrdinalIgnoreCase)
.ToDictionary(
g => g.Key,
g =>
{
var name = g.First().DictName?.Trim();
return string.IsNullOrWhiteSpace(name) ? g.Key : name;
},
StringComparer.OrdinalIgnoreCase);
groupNameMap[UngroupedProductGroupCode] = "未分组";
var filtered = products
.Where(p =>
{
var groupCode = string.IsNullOrWhiteSpace(p.ProductGroup)
? UngroupedProductGroupCode
: p.ProductGroup.Trim();
return !domesticPickupGroupCodes.Contains(groupCode);
})
.OrderBy(p => p.RouteSort)
.ThenBy(p => p.MinTimeliness ?? int.MaxValue)
.ThenBy(p => p.ProductName, StringComparer.OrdinalIgnoreCase)
.ThenBy(p => p.Id)
.ToList();
var usedGroupCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var items = filtered.Select(p =>
{
var groupCode = string.IsNullOrWhiteSpace(p.ProductGroup)
? UngroupedProductGroupCode
: p.ProductGroup.Trim();
usedGroupCodes.Add(groupCode);
groupNameMap.TryGetValue(groupCode, out var groupName);
return new SalesProductPortalItemOutput
{
Id = p.Id,
ProductName = p.ProductName,
ProductGroup = groupCode == UngroupedProductGroupCode ? null : groupCode,
ProductGroupName = string.IsNullOrWhiteSpace(groupName) ? groupCode : groupName,
MinTimeliness = p.MinTimeliness,
MaxTimeliness = p.MaxTimeliness,
TradeTerms = string.IsNullOrWhiteSpace(p.TradeTerms) ? null : p.TradeTerms.Trim().ToUpperInvariant(),
RouteSort = p.RouteSort,
ProductDescription = p.ProductDescription
};
}).ToList();
var groups = new List<SalesProductPortalGroupOutput>();
var consumed = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var dict in dictRows.OrderBy(x => x.DictSort).ThenBy(x => x.DictCode))
{
var code = dict.DictCode?.Trim();
if (string.IsNullOrWhiteSpace(code)
|| domesticPickupGroupCodes.Contains(code)
|| !usedGroupCodes.Contains(code)
|| !consumed.Add(code))
{
continue;
}
groups.Add(new SalesProductPortalGroupOutput
{
Code = code,
Name = string.IsNullOrWhiteSpace(dict.DictName) ? code : dict.DictName.Trim()
});
}
if (usedGroupCodes.Contains(UngroupedProductGroupCode) && consumed.Add(UngroupedProductGroupCode))
{
groups.Add(new SalesProductPortalGroupOutput
{
Code = UngroupedProductGroupCode,
Name = groupNameMap[UngroupedProductGroupCode]
});
}
foreach (var code in usedGroupCodes.OrderBy(x => x, StringComparer.OrdinalIgnoreCase))
{
if (!consumed.Add(code)) continue;
groupNameMap.TryGetValue(code, out var name);
groups.Add(new SalesProductPortalGroupOutput
{
Code = code,
Name = string.IsNullOrWhiteSpace(name) ? code : name
});
}
return new SalesProductPortalCatalogOutput
{
Groups = groups,
Items = items
};
}
}

View File

@ -233,6 +233,10 @@ public class CustomerService : ApplicationService
input.InviteCode,
ReferralCodeRepository,
CustomerRepository);
await StaffAdvisorBinder.BindAdvisorOnCreateAsync(
entity,
input.AdvisorCode,
RbacDbContext);
await CustomerRepository.InsertAsync(entity);
try
{

View File

@ -1,46 +1,49 @@
using Fake.Application;
using Fake.Users;
using Microsoft.AspNetCore.Mvc;
using MiniExcelLibs;
using Oms.Application.DataPerm;
using Oms.Application.Dtos.Finance;
using Oms.Application.Dtos.Shared;
using Oms.Application.Services.Orders;
using Oms.Application.Shared;
using Oms.Domain.CustomersAggregate.Repositories;
using Oms.Domain.OrdersAggregate;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Repositories;
using Oms.Domain.SystemAggregate.Repositories;
namespace Oms.Application.Services.Finance;
/// <summary>
/// 财务相关 API<EFBFBD>。
/// </summary>
/// 财务相关 API。
/// </summary>
[ApiExplorerSettings(GroupName = "OMS-Finance")]
public class FinanceService : ApplicationService
{
private const string FeeItemDictType = "fee_item";
private const int ExportMaxRows = 50000;
private IOrderRepository OrderRepository =>
LazyServiceProvider.GetRequiredService<IOrderRepository>();
private ICustomerRepository CustomerRepository =>
LazyServiceProvider.GetRequiredService<ICustomerRepository>();
private IDictRepository DictRepository =>
LazyServiceProvider.GetRequiredService<IDictRepository>();
private EffectiveScopeBuilder DataScopeBuilder =>
LazyServiceProvider.GetRequiredService<EffectiveScopeBuilder>();
/// <summary>
/// 分页查询应收应付(订单费用联查订单)<EFBFBD>。
/// 分页查询应收应付(订单费用联查订单)。
/// </summary>
[HttpGet("/finance/receivable-payable/list")]
public async Task<OmsPageResult<ReceivablePayablePaginatedOutput>> GetReceivablePayableListAsync(
[FromQuery] ReceivablePayablePaginatedInput input)
{
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
input.CustomerId);
var filter = BuildFilter(input, customerId);
await ApplyInternalOrderDataScopeAsync(filter);
var filter = await BuildScopedFilterAsync(input);
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
var (rows, total) = await OrderRepository.GetReceivablePayablePagedListAsync(
@ -48,6 +51,102 @@ public class FinanceService : ApplicationService
pageSize,
filter);
var items = await MapReceivablePayableRowsAsync(rows);
return new OmsPageResult<ReceivablePayablePaginatedOutput>
{
Items = items,
Total = total
};
}
/// <summary>
/// 应收应付汇总(当前筛选条件下全量费用明细)。
/// </summary>
[HttpGet("/finance/receivable-payable/summary")]
public async Task<ReceivablePayableSummaryOutput> GetReceivablePayableSummaryAsync(
[FromQuery] ReceivablePayablePaginatedInput input)
{
var filter = await BuildScopedFilterAsync(input);
var rows = await OrderRepository.GetReceivablePayableSummaryAsync(filter);
var items = rows
.Select(row => new ReceivablePayableSummaryItemOutput
{
FeeType = row.FeeType,
FeeTypeName = GetFeeTypeName(row.FeeType),
Currency = row.Currency,
TotalAmount = row.TotalAmount,
LineCount = row.LineCount
})
.OrderBy(x => x.FeeType)
.ThenBy(x => x.Currency)
.ToList();
return new ReceivablePayableSummaryOutput
{
TotalLines = items.Sum(x => x.LineCount),
Items = items
};
}
/// <summary>
/// 导出应收应付 Excel。
/// </summary>
[HttpGet("/finance/receivable-payable/export")]
public async Task<FileContentResult> ExportReceivablePayableAsync(
[FromQuery] ReceivablePayablePaginatedInput input)
{
var filter = await BuildScopedFilterAsync(input);
var rows = await OrderRepository.GetReceivablePayableExportListAsync(filter, ExportMaxRows);
var items = await MapReceivablePayableRowsAsync(rows);
var exportRows = items.Select(item => new
{
运单号 = item.ServiceNo,
客户单号 = item.ClientNo,
跟踪号 = item.TrackNo,
客户编码 = item.CustomerCode,
费用类型 = item.FeeTypeName,
费用项 = item.CostItemName ?? item.CostItem,
金额 = item.Amount,
币种 = item.Currency,
计费阶段 = item.ChargingStageName,
订单状态 = item.OrderStatusName,
支付状态 = item.IsPaid == 1 ? "已支付" : "未支付",
产品编码 = item.ProductCode,
渠道编码 = item.ChannelCode,
目的国 = item.ConsigneeCountryName,
下游服务商 = item.DownstreamProvider,
价格编号 = item.PriceNo,
价格生效时间 = item.PriceEffectiveTime,
支付时间 = item.PayTime,
收货时间 = item.ReceiveTime,
发货时间 = item.ShipTime
}).ToList();
await using var stream = new MemoryStream();
await MiniExcel.SaveAsAsync(stream, exportRows);
stream.Position = 0;
return new FileContentResult(stream.ToArray(),
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
{
FileDownloadName = $"receivable-payable-{DateTime.Now:yyyyMMddHHmmss}.xlsx"
};
}
private async Task<OrderReceivablePayableFilter> BuildScopedFilterAsync(
ReceivablePayablePaginatedInput input)
{
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
input.CustomerId);
var filter = BuildFilter(input, customerId);
await ApplyInternalOrderDataScopeAsync(filter);
return filter;
}
private async Task<List<ReceivablePayablePaginatedOutput>> MapReceivablePayableRowsAsync(
List<OrderReceivablePayableRow> rows)
{
var customerIds = rows
.Select(x => x.Order.CustomerId)
.Where(x => x.HasValue)
@ -58,8 +157,9 @@ public class FinanceService : ApplicationService
? []
: await CustomerRepository.GetListAsync(x => customerIds.Contains(x.Id));
var customerMap = customers.ToDictionary(x => x.Id);
var feeItemNameMap = await BuildFeeItemNameMapAsync();
var items = rows.Select(row =>
return rows.Select(row =>
{
string? customerCode = null;
if (row.Order.CustomerId.HasValue
@ -68,8 +168,12 @@ public class FinanceService : ApplicationService
customerCode = customer.CustomerCode;
}
var costItem = row.Fee.CostItem;
feeItemNameMap.TryGetValue(costItem, out var costItemName);
return new ReceivablePayablePaginatedOutput
{
OrderId = row.Order.Id,
ServiceNo = row.Order.ServiceNo,
ClientNo = row.Order.ClientNo,
TrackNo = row.Order.TrackNo,
@ -85,7 +189,8 @@ public class FinanceService : ApplicationService
ConsigneeCountryName = row.Order.ConsigneeCountryName,
FeeType = row.Fee.Type,
FeeTypeName = GetFeeTypeName(row.Fee.Type),
CostItem = row.Fee.CostItem,
CostItem = costItem,
CostItemName = costItemName ?? ParcelBillingCalculator.ResolveFeeDisplayName(costItem, null),
Amount = row.Fee.Amount,
Currency = row.Fee.Currency,
DownstreamProvider = row.Fee.DownstreamProvider,
@ -95,27 +200,59 @@ public class FinanceService : ApplicationService
ChargingStageName = GetChargingStageName(row.Fee.ChargingStage)
};
}).ToList();
}
return new OmsPageResult<ReceivablePayablePaginatedOutput>
{
Items = items,
Total = total
};
private async Task<Dictionary<string, string>> BuildFeeItemNameMapAsync()
{
var dictRows = await DictRepository.GetListAsync(x => x.DictType == FeeItemDictType);
return dictRows
.Where(x => !string.IsNullOrWhiteSpace(x.DictCode))
.GroupBy(x => x.DictCode.Trim(), StringComparer.OrdinalIgnoreCase)
.ToDictionary(
g => g.Key,
g => g.First().DictName?.Trim() ?? g.Key,
StringComparer.OrdinalIgnoreCase);
}
private static OrderReceivablePayableFilter BuildFilter(
ReceivablePayablePaginatedInput input,
int? customerId) =>
new()
int? customerId)
{
var timeField = NormalizeTimeField(input.TimeField);
var useCreateTime = timeField == "createTime";
return new()
{
Keyword = input.Keyword,
CustomerId = customerId,
CreateTimeStart = input.StartTime,
CreateTimeEnd = input.EndTime,
CustomerCode = input.CustomerCode,
CreateTimeStart = useCreateTime ? input.StartTime : null,
CreateTimeEnd = useCreateTime ? input.EndTime : null,
TimeField = timeField,
TimeStart = input.StartTime,
TimeEnd = input.EndTime,
FeeType = input.FeeType,
CostItem = input.CostItem,
ChargingStage = input.ChargingStage
ChargingStage = input.ChargingStage,
EffectiveStageOnly = input.EffectiveStageOnly,
IsPaid = NormalizeIsPaid(input.IsPaid)
};
}
private static int? NormalizeIsPaid(int? isPaid)
{
if (!isPaid.HasValue) return null;
return isPaid.Value == 1 ? 1 : isPaid.Value == 0 ? 0 : null;
}
private static string NormalizeTimeField(string? timeField)
{
var raw = timeField?.Trim();
return raw switch
{
"receiveTime" or "shipTime" or "payTime" => raw,
_ => "createTime"
};
}
private async Task ApplyInternalOrderDataScopeAsync(OrderReceivablePayableFilter filter)
{

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,184 @@
using Fake.DependencyInjection;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories;
namespace Oms.Application.Services.Orders;
/// <summary>
/// 运单创建时按 ExportCustomsMode=9610Summary 写入/刷新 t_9610_order。
/// </summary>
public class Customs9610OrderWriter : IScopedDependency
{
private readonly ICustoms9610OrderRepository _headerRepository;
private readonly ICustoms9610OrderItemRepository _itemRepository;
private readonly IOrderRepository _orderRepository;
public Customs9610OrderWriter(
ICustoms9610OrderRepository headerRepository,
ICustoms9610OrderItemRepository itemRepository,
IOrderRepository orderRepository)
{
_headerRepository = headerRepository;
_itemRepository = itemRepository;
_orderRepository = orderRepository;
}
public async Task UpsertFromOrderAsync(Order order, CancellationToken cancellationToken = default)
{
if (order.Id <= 0)
{
return;
}
var isSummary = string.Equals(
order.ExportCustomsMode,
ExportCustomsModeCodes.Mode9610Summary,
StringComparison.OrdinalIgnoreCase);
var existing = await _headerRepository.GetByOrderIdAsync(order.Id, cancellationToken);
if (!isSummary)
{
if (existing != null
&& string.Equals(existing.Status, Customs9610StatusCodes.Pending, StringComparison.OrdinalIgnoreCase))
{
await _headerRepository.HardDeleteItemsByHeaderIdAsync(existing.Id, cancellationToken);
await _headerRepository.HardDeleteAsync(existing, cancellationToken);
}
return;
}
var declarations = await _orderRepository.GetDeclarationsByOrderIdsAsync([order.Id], cancellationToken);
var weights = await _orderRepository.GetWeightsByOrderIdsAsync([order.Id], cancellationToken);
var grossWeight = ResolveGrossWeight(weights);
var serviceNo = order.ServiceNo?.Trim() ?? string.Empty;
var clientNo = FirstNonEmpty(order.ClientNo, order.ServiceNo) ?? string.Empty;
if (existing != null
&& string.Equals(existing.Status, Customs9610StatusCodes.Declared, StringComparison.OrdinalIgnoreCase))
{
// 已申报:仅同步冗余检索字段,不覆盖申报内容
existing.ServiceNo = string.IsNullOrWhiteSpace(existing.ServiceNo) ? serviceNo : existing.ServiceNo;
existing.UpdateTime = DateTime.Now;
await _headerRepository.UpdateAsync(existing);
return;
}
Customs9610Order header;
if (existing == null)
{
header = new Customs9610Order
{
OrderId = order.Id,
CustomerId = order.CustomerId ?? 0,
Status = Customs9610StatusCodes.Pending,
BusinessType = Customs9610FormTypeCodes.SummaryB,
PreRecordFlag = "1",
IsSummarySimple = 0,
CreateTime = DateTime.Now,
};
}
else
{
header = existing;
header.IsDeleted = false;
}
header.ServiceNo = serviceNo;
header.CustomerId = order.CustomerId ?? header.CustomerId;
header.ClientNo = clientNo;
header.BusinessType = string.IsNullOrWhiteSpace(header.BusinessType)
? Customs9610FormTypeCodes.SummaryB
: header.BusinessType;
if (string.IsNullOrWhiteSpace(header.PreRecordFlag)
&& string.Equals(header.BusinessType, Customs9610FormTypeCodes.SummaryB, StringComparison.OrdinalIgnoreCase))
{
header.PreRecordFlag = "1";
}
header.GrossWeight = grossWeight;
header.Currency = FirstNonEmpty(order.DeclaredCurrency, header.Currency, "CNY")!;
header.DestCountry = FirstNonEmpty(order.ConsigneeCountry, header.DestCountry) ?? string.Empty;
header.UpdateTime = DateTime.Now;
if (existing == null)
{
header = await _headerRepository.InsertAsync(header);
}
else
{
await _headerRepository.UpdateAsync(header);
}
await _headerRepository.HardDeleteItemsByHeaderIdAsync(header.Id, cancellationToken);
var sort = 0;
foreach (var d in declarations.OrderBy(x => x.Id))
{
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);
await _itemRepository.InsertAsync(new Customs9610OrderItem
{
Customs9610OrderId = header.Id,
OrderDeclarationId = d.Id,
Sku = Truncate(sku, 32) ?? string.Empty,
CnName = Truncate(itemName, 250) ?? string.Empty,
ExHsCode = Truncate(FirstNonEmpty(d.ExHsCode, d.HsCode, d.ImHsCode), 10),
DeclareElement = Truncate(declareElement, 255),
UnitPrice = d.UnitPrice ?? 0,
Quantity = d.Quantity ?? 0,
Amount = d.Amount,
Unit = Truncate(d.Unit, 3),
Freight = null,
SortNo = sort++,
CreateTime = DateTime.Now,
});
}
}
private static decimal ResolveGrossWeight(IReadOnlyList<OrderWeight> weights)
{
var forecast = weights.FirstOrDefault(x =>
string.Equals(x.Type, OrderWeightTypeCodes.Forecast, StringComparison.OrdinalIgnoreCase));
var row = forecast ?? weights.FirstOrDefault();
if (row == null)
{
return 0;
}
var w = row.ActualWeight ?? row.ChargeableWeight ?? 0;
return w > 0 ? w : 0;
}
private static string? FirstNonEmpty(params string?[] values)
{
foreach (var v in values)
{
if (!string.IsNullOrWhiteSpace(v))
{
return v.Trim();
}
}
return null;
}
private static string? JoinNonEmpty(string sep, params string?[] values)
{
var parts = values.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x!.Trim()).ToList();
return parts.Count == 0 ? null : string.Join(sep, parts);
}
private static string? Truncate(string? value, int max)
{
if (string.IsNullOrEmpty(value))
{
return value;
}
return value.Length <= max ? value : value[..max];
}
}

View File

@ -187,6 +187,7 @@ public partial class OrderService
bindSummary?.LineCount ?? 0,
bindSummary?.HasInTransitParcel ?? false),
PrimaryBinCode = bindSummary?.PrimaryBinCode,
PrimarySkuCodes = bindSummary?.PrimarySkuCodes,
PickupOrderId = order.PickupOrderId,
PickupNo = order.PickupOrderId is > 0
&& pickupNoMap.TryGetValue(order.PickupOrderId.Value, out var pickupNo)
@ -280,6 +281,8 @@ public partial class OrderService
public bool HasInTransitParcel { get; init; }
public string? PrimaryBinCode { get; init; }
public string? PrimarySkuCodes { get; init; }
}
private async Task<Dictionary<int, StockBindListSummary>> LoadStockBindListSummariesAsync(int[] orderIds)
@ -342,6 +345,12 @@ public partial class OrderService
.Where(code => !string.IsNullOrWhiteSpace(code))
.OrderBy(code => code, StringComparer.OrdinalIgnoreCase)
.FirstOrDefault();
var skuCodes = rows
.Select(row => row.SkuCode?.Trim())
.Where(code => !string.IsNullOrWhiteSpace(code))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(code => code, StringComparer.OrdinalIgnoreCase)
.ToArray();
return new StockBindListSummary
{
BindStatus = bindStatus,
@ -349,7 +358,8 @@ public partial class OrderService
QtyTotal = rows.Sum(x => x.BindQty),
TrackingNosText = trackingNos.Length == 0 ? null : string.Join(", ", trackingNos),
HasInTransitParcel = hasInTransitParcel,
PrimaryBinCode = primaryBinCode
PrimaryBinCode = primaryBinCode,
PrimarySkuCodes = skuCodes.Length == 0 ? null : string.Join(", ", skuCodes),
};
});
}
@ -533,7 +543,7 @@ public partial class OrderService
var template = await OrderTemplateRepository.FirstOrDefaultAsync(x => x.Id == input.TemplateId)
?? throw new BusinessException("模板不存在");
var effectiveCustomer = await ResolveEffectiveCustomerAsync();
var effectiveCustomer = await ResolveEffectiveCustomerAsync(input.CustomerId);
EnsureTemplateAccessible(template, effectiveCustomer.Id);
OrderTemplateImportMapping.EnsureTemplateReadyForImport(template);
@ -732,6 +742,8 @@ public partial class OrderService
Amount = x.Amount,
Weight = x.Weight,
HsCode = x.ExHsCode ?? x.ImHsCode ?? x.HsCode,
ImHsCode = x.ImHsCode ?? x.HsCode,
ExHsCode = x.ExHsCode ?? x.HsCode,
SkuImage = x.SkuImage,
Category = x.Category,
Brand = x.Brand,

View File

@ -139,6 +139,7 @@ public partial class OrderService
// 客户主动「先保存为草稿」:落库草稿后直接返回,不跑预报计费。
if (input.SaveAsDraft)
{
await TryUpsertCustoms9610Async(order);
return await BuildParcelCreateOutputAsync(order);
}
@ -154,6 +155,7 @@ public partial class OrderService
await ApplyChargeableWeightAsync(product, snapshotBinding.Rules, forecast);
SyncWeightRows(weights, forecast);
await OrderRepository.ReplaceWeightsAsync(order.Id, weights);
await TryUpsertCustoms9610Async(order);
var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId);
var priceTierKey = await ResolvePriceTierKeyAsync(order.CustomerId);
@ -235,6 +237,20 @@ public partial class OrderService
/// </summary>
private async Task RollbackFailedParcelCreateAsync(Order order)
{
try
{
var writer = LazyServiceProvider.GetRequiredService<Customs9610OrderWriter>();
// 强制清掉可能已写入的 pending 9610(ExportCustomsMode 临时置空触发删除逻辑)
var mode = order.ExportCustomsMode;
order.ExportCustomsMode = null;
await writer.UpsertFromOrderAsync(order);
order.ExportCustomsMode = mode;
}
catch
{
// ignore
}
order.OrderStatus = OrderStatusCodes.Deleted;
order.PredeclareFailReason = string.Empty;
order.UpdateTime = DateTime.Now;
@ -242,6 +258,19 @@ public partial class OrderService
await OrderRepository.DeleteAsync(order);
}
private async Task TryUpsertCustoms9610Async(Order order)
{
try
{
await LazyServiceProvider.GetRequiredService<Customs9610OrderWriter>()
.UpsertFromOrderAsync(order);
}
catch
{
// 9610 同步失败不阻断运单创建
}
}
/// <summary>
/// 已有草稿重新预报失败时,在业务文案末尾附加机器可读标记,供前端区分。
/// 格式:原因 [[omsDraft:{"draftSaved":true,"orderId":1,"serviceNo":"IDE..."}]]

View File

@ -276,6 +276,9 @@ public partial class OrderService
TradeTerms = string.IsNullOrWhiteSpace(product.TradeTerms) ? null : product.TradeTerms.Trim().ToUpperInvariant(),
ProductGroup = groupCode,
ProductGroupName = groupName,
ProductDescription = string.IsNullOrWhiteSpace(product.ProductDescription)
? null
: product.ProductDescription.Trim(),
VolumetricWeight = quote.VolumetricWeight,
ChargeableWeight = quote.ChargeableWeight,
CostFormula = summary.CostFormula,

View File

@ -8,6 +8,7 @@ using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories;
using Oms.Domain.ChannelsAggregate.Entities;
using Oms.Domain.WarehouseAggregate.Consts;
using Oms.Domain.WarehouseAggregate.Entities;
using Oms.Domain.WarehouseAggregate.Repositories;
@ -279,6 +280,12 @@ public partial class OrderService
var order = await GetOrderOrThrowAsync(input.OrderId);
var binds = await OrderStockBindRepository.GetListAsync(x =>
x.OrderId == order.Id && x.Status == OrderStockBindStatusCodes.Bound);
if (binds.Count == 0)
{
throw new BusinessException("当前订单无已绑定明细,无法确认扣减");
}
await EnsureStockBindReadyForDeductAsync(binds);
foreach (var bind in binds)
{
await DeductBoundLineAsync(bind);
@ -906,4 +913,37 @@ public partial class OrderService
order.UpdateTime = DateTime.Now;
await OrderRepository.UpdateAsync(order);
}
private async Task EnsureStockBindReadyForDeductAsync(IReadOnlyList<OrderStockBind> binds)
{
var parcelIds = binds
.Where(x => x.ParcelId is > 0)
.Select(x => x.ParcelId!.Value)
.Distinct()
.ToArray();
if (parcelIds.Length == 0)
{
return;
}
var parcels = await StockBindParcelRepository.GetListAsync(x => parcelIds.Contains(x.Id));
var inTransitNos = parcels
.Where(x => string.Equals(
x.LifecycleStatus,
WarehouseParcelLifecycleCodes.InTransit,
StringComparison.OrdinalIgnoreCase))
.Select(x => x.TrackingNo?.Trim())
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
if (inTransitNos.Length == 0)
{
return;
}
var sample = inTransitNos[0];
throw new BusinessException(inTransitNos.Length == 1
? $"包裹国内在途({sample}),请先到仓确认后再标记已打包"
: $"存在 {inTransitNos.Length} 个国内在途包裹,请先到仓确认后再标记已打包");
}
}

View File

@ -65,6 +65,25 @@ public partial class OrderService
: await SalesStoreOrderRepository.CountAsync(x =>
x.FulfillStatus == SalesStoreFulfillStatusCodes.Pending);
var fulfillFilter = new OrderListFilter
{
CustomerId = filter.CustomerId,
FulfillmentType = FulfillmentTypeCodes.Warehouse,
ScopeOrganizationIds = filter.ScopeOrganizationIds,
ScopeSecretLevels = filter.ScopeSecretLevels,
ScopeDataScope = filter.ScopeDataScope,
ScopePersonalOwnerMapId = filter.ScopePersonalOwnerMapId,
ScopePersonalOwnerAccount = filter.ScopePersonalOwnerAccount,
ScopeOwnerMapIds = filter.ScopeOwnerMapIds,
ScopeBizFilters = filter.ScopeBizFilters,
ScopeCustomFilters = filter.ScopeCustomFilters,
};
var fulfillCounts = await OrderRepository.GetFulfillProgressCountsByFilterAsync(fulfillFilter);
int FulfillCount(string key) =>
fulfillCounts.TryGetValue(key, out var value) ? value : 0;
var pickupCounts = await PickupOrderRepository.GetWorkplaceCountsAsync(filter.CustomerId);
return new OrderWorkplaceStatsOutput
{
Total = counts.Values.Sum(),
@ -79,6 +98,20 @@ public partial class OrderService
Claimed = GetCount(OrderStatusCodes.Claimed),
StoreCount = storeCount,
StoreOrderPending = storeOrderPending,
Fulfill = new FulfillWorkplaceStatsOutput
{
PendingBind = FulfillCount(FulfillmentProgressCodes.PendingBind),
DomesticInTransit = FulfillCount(FulfillmentProgressCodes.DomesticInTransit),
ArrivedPendingPick = FulfillCount(FulfillmentProgressCodes.ArrivedPendingPick),
OutboundDone = FulfillCount(FulfillmentProgressCodes.OutboundDone),
},
Pickup = new PickupWorkplaceStatsOutput
{
PendingPayment = pickupCounts.PendingPayment,
PendingSchedule = pickupCounts.PendingSchedule,
PendingPickup = pickupCounts.PendingPickup,
PendingInbound = pickupCounts.PendingInbound,
},
};
}
@ -198,6 +231,79 @@ public partial class OrderService
};
}
/// <summary>
/// 工作台业务线趋势(运单 / 代发 / 揽收新增量)。
/// </summary>
[HttpGet("/order/workplace-business-trend")]
public async Task<OrderWorkplaceBusinessTrendOutput> GetWorkplaceBusinessTrendAsync(
[FromQuery] string? dimension = "day",
[FromQuery] int count = 7)
{
var dim = (dimension ?? "day").Trim().ToLowerInvariant();
if (dim is not ("day" or "week" or "month"))
{
dim = "day";
}
count = dim switch
{
"week" => Math.Clamp(count, 1, 12),
"month" => Math.Clamp(count, 1, 12),
_ => Math.Clamp(count, 1, 31),
};
var filter = await BuildScopedListFilterAsync();
var fulfillFilter = CloneScopedFilter(filter);
fulfillFilter.FulfillmentType = FulfillmentTypeCodes.Warehouse;
var buckets = BuildTrendBuckets(dim, count);
var points = new List<OrderWorkplaceBusinessTrendPointOutput>(buckets.Count);
foreach (var bucket in buckets)
{
var totalOrders = await OrderRepository.CountByFilterAndCreateTimeRangeAsync(
filter,
bucket.Start,
bucket.EndExclusive);
var fulfillOrders = await OrderRepository.CountByFilterAndCreateTimeRangeAsync(
fulfillFilter,
bucket.Start,
bucket.EndExclusive);
var pickupOrders = await PickupOrderRepository.CountByCreateTimeRangeAsync(
filter.CustomerId,
bucket.Start,
bucket.EndExclusive);
points.Add(new OrderWorkplaceBusinessTrendPointOutput
{
Label = bucket.Label,
WaybillCount = Math.Max(0, totalOrders - fulfillOrders),
FulfillCount = fulfillOrders,
PickupCount = pickupOrders,
});
}
return new OrderWorkplaceBusinessTrendOutput
{
Dimension = dim,
Points = points,
};
}
private static OrderListFilter CloneScopedFilter(OrderListFilter filter) =>
new()
{
CustomerId = filter.CustomerId,
ScopeOrganizationIds = filter.ScopeOrganizationIds,
ScopeSecretLevels = filter.ScopeSecretLevels,
ScopeDataScope = filter.ScopeDataScope,
ScopePersonalOwnerMapId = filter.ScopePersonalOwnerMapId,
ScopePersonalOwnerAccount = filter.ScopePersonalOwnerAccount,
ScopeOwnerMapIds = filter.ScopeOwnerMapIds,
ScopeBizFilters = filter.ScopeBizFilters,
ScopeCustomFilters = filter.ScopeCustomFilters,
};
private async Task<OrderListFilter> BuildScopedListFilterAsync()
{
var customerId = await OmsSessionHelper.ResolveListCustomerIdAsync(

View File

@ -93,12 +93,18 @@ public partial class OrderService : ApplicationService
Processed = x.Value.Processed
},
StringComparer.OrdinalIgnoreCase);
return new OrderStatusCountsOutput
var output = new OrderStatusCountsOutput
{
Total = rows.Sum(x => x.Count),
Counts = counts,
HandleCounts = handleCounts
};
if (string.Equals(filter.FulfillmentType, FulfillmentTypeCodes.Warehouse, StringComparison.OrdinalIgnoreCase))
{
output.FulfillProgressCounts = await OrderRepository.GetFulfillProgressCountsByFilterAsync(filter);
}
return output;
}
/// <summary>
@ -626,6 +632,7 @@ public partial class OrderService : ApplicationService
Status = input.Status,
OrderSource = input.OrderSource,
FulfillmentType = NormalizeFulfillmentTypeFilter(input.FulfillmentType),
FulfillProgress = NormalizeFulfillProgressFilter(input.FulfillProgress),
Keyword = NormalizeOrderKeyword(input.Keyword),
CreateTimeStart = input.CreateTimeStart,
CreateTimeEnd = input.CreateTimeEnd,

View File

@ -413,8 +413,11 @@ public class PickupOrderService : ApplicationService
(customerId == null || x.CustomerId == customerId.Value) &&
(status == null || x.Status == status) &&
(payStatus == null || x.PayStatus == payStatus) &&
(input.WarehouseId == null || input.WarehouseId <= 0 || x.WarehouseId == input.WarehouseId.Value) &&
(input.CreateTimeStart == null || x.CreateTime >= input.CreateTimeStart) &&
(input.CreateTimeEnd == null || x.CreateTime <= input.CreateTimeEnd) &&
(input.ExpectPickupTimeStart == null || x.ExpectPickupTime >= input.ExpectPickupTimeStart) &&
(input.ExpectPickupTimeEnd == null || x.ExpectPickupTime <= input.ExpectPickupTimeEnd) &&
(keyword == null || x.PickupNo.Contains(keyword) ||
(x.CarrierWaybillNo != null && x.CarrierWaybillNo.Contains(keyword)) ||
(x.SenderName != null && x.SenderName.Contains(keyword))));
@ -431,6 +434,38 @@ public class PickupOrderService : ApplicationService
return new OmsPageResult<PickupOrderOutput> { Items = outputs, Total = total };
}
/// <summary>
/// 揽收单状态计数(Tab 徽章)。
/// </summary>
[HttpGet("/order/pickup-order/status-counts")]
public async Task<PickupOrderStatusCountsOutput> GetStatusCountsAsync([FromQuery] int? customerId)
{
var resolvedCustomerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
customerId);
var all = await PickupOrderRepository.GetListAsync(x =>
resolvedCustomerId == null || x.CustomerId == resolvedCustomerId.Value);
var terminal = new[] { PickupOrderStatusCodes.Cancelled, PickupOrderStatusCodes.Failed };
var pendingPayment = all.Count(x =>
!terminal.Contains(x.Status) &&
x.PayStatus != PickupPayStatusCodes.Paid &&
x.ReceivableAmount is > 0);
var counts = all
.GroupBy(x => x.Status ?? string.Empty, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.Count(), StringComparer.OrdinalIgnoreCase);
return new PickupOrderStatusCountsOutput
{
Total = all.Count,
PendingPayment = pendingPayment,
Counts = counts
};
}
/// <summary>
/// 揽收单详情。
/// </summary>
@ -930,7 +965,8 @@ public class PickupOrderService : ApplicationService
SettlementStatus = entity.SettlementStatus,
SettlementBatchNo = entity.SettlementBatchNo,
Remark = entity.Remark,
CreateTime = entity.CreateTime
CreateTime = entity.CreateTime,
UpdateTime = entity.UpdateTime
};
if (!includeOrders)

View File

@ -109,6 +109,7 @@ public class OmsUserService(
EmployeeNo = employeeNo,
WechatId = input.WechatId,
WechatQrCode = input.WechatQrCode,
WecomKfUrl = input.WecomKfUrl,
},
cancellationToken);
@ -145,6 +146,7 @@ public class OmsUserService(
Phone = input.Phone,
WechatId = input.WechatId,
WechatQrCode = input.WechatQrCode,
WecomKfUrl = input.WecomKfUrl,
},
cancellationToken);

View File

@ -130,14 +130,15 @@ public class NoticeService : ApplicationService
{
var noticeType = NormalizeNoticeType(input.NoticeType);
ValidateNoticeText(noticeType, input.Title, input.Content);
var receiverIds = await NormalizeAndValidateReceiverIdsAsync(input.ReceiverType, input.ReceiverIds);
var receiverType = ResolveReceiverType(noticeType, input.ReceiverType);
var receiverIds = await NormalizeAndValidateReceiverIdsAsync(noticeType, receiverType, input.ReceiverIds);
await NoticeRepository.InsertAsync(new Notice
{
NoticeType = noticeType,
Title = NormalizeTitle(input.Title),
Content = input.Content.Trim(),
IsPopup = noticeType == NoticeTypeMessage ? (byte)0 : NormalizePopupValue(input.IsPopup),
ReceiverType = NormalizeReceiverType(input.ReceiverType),
ReceiverType = receiverType,
ReceiverIds = OmsDocHelper.SerializeJson(receiverIds) ?? "[]"
});
}
@ -151,20 +152,27 @@ public class NoticeService : ApplicationService
var entity = await GetNoticeOrThrowAsync(input.Id);
var noticeType = NormalizeNoticeType(input.NoticeType);
ValidateNoticeText(noticeType, input.Title, input.Content);
var receiverIds = await NormalizeAndValidateReceiverIdsAsync(input.ReceiverType, input.ReceiverIds);
var receiverType = ResolveReceiverType(noticeType, input.ReceiverType);
var receiverIds = await NormalizeAndValidateReceiverIdsAsync(noticeType, receiverType, input.ReceiverIds);
entity.NoticeType = noticeType;
entity.Title = NormalizeTitle(input.Title);
entity.Content = input.Content.Trim();
entity.IsPopup = noticeType == NoticeTypeMessage ? (byte)0 : NormalizePopupValue(input.IsPopup);
entity.ReceiverType = NormalizeReceiverType(input.ReceiverType);
entity.ReceiverType = receiverType;
entity.ReceiverIds = OmsDocHelper.SerializeJson(receiverIds) ?? "[]";
await NoticeRepository.UpdateAsync(entity);
var existsReads = await NoticeReadRepository.GetListAsync(x => x.NoticeId == entity.Id);
var receiverIdSet = receiverIds.ToHashSet();
foreach (var read in existsReads.Where(x => x.ReceiverType != ReceiverTypeUser || !receiverIdSet.Contains(x.ReceiverId)))
// 全体广播不清理已读;定向接收时移除已不在名单内的已读记录
if (receiverIds.Count > 0)
{
await NoticeReadRepository.DeleteAsync(read);
var existsReads = await NoticeReadRepository.GetListAsync(x => x.NoticeId == entity.Id);
var receiverIdSet = receiverIds.ToHashSet();
foreach (var read in existsReads.Where(x =>
!string.Equals(x.ReceiverType, receiverType, StringComparison.OrdinalIgnoreCase) ||
!receiverIdSet.Contains(x.ReceiverId)))
{
await NoticeReadRepository.DeleteAsync(read);
}
}
}
@ -186,14 +194,17 @@ public class NoticeService : ApplicationService
{
var notice = await GetNoticeOrThrowAsync(input.Id);
var receiverIds = OmsDocHelper.DeserializeJson<List<int>>(notice.ReceiverIds) ?? [];
var currentReceiverType = NormalizeReceiverType(notice.ReceiverType);
var currentReceiverType = OmsSessionHelper.IsCustomerLogin(CurrentUser)
? ReceiverTypeCustomer
: ReceiverTypeUser;
var currentReceiverId = CurrentUser.GetId<int>();
if (currentReceiverId <= 0)
{
throw new BusinessException("当前登录主体无效");
}
if (!receiverIds.Contains(currentReceiverId))
// 空接收人 = 全体广播;否则校验当前主体在定向名单内
if (receiverIds.Count > 0 && !receiverIds.Contains(currentReceiverId))
{
throw new BusinessException("当前主体不在通知接收范围内");
}
@ -355,11 +366,15 @@ public class NoticeService : ApplicationService
(string.IsNullOrWhiteSpace(keyword) ||
x.Title.Contains(keyword) ||
x.Content.Contains(keyword)) &&
x.ReceiverType == expectedReceiverType &&
(x.ReceiverIds == $"[{receiverIdToken}]" ||
x.ReceiverIds.StartsWith($"[{receiverIdToken},") ||
x.ReceiverIds.Contains($",{receiverIdToken},") ||
x.ReceiverIds.EndsWith($",{receiverIdToken}]")) &&
(
// 空接收人列表 = 全体广播(员工/客户均可见)
x.ReceiverIds == "[]" ||
(x.ReceiverType == expectedReceiverType &&
(x.ReceiverIds == $"[{receiverIdToken}]" ||
x.ReceiverIds.StartsWith($"[{receiverIdToken},") ||
x.ReceiverIds.Contains($",{receiverIdToken},") ||
x.ReceiverIds.EndsWith($",{receiverIdToken}]")))
) &&
(excluded.Count == 0 || !excluded.Contains(x.Id));
}
@ -374,19 +389,29 @@ public class NoticeService : ApplicationService
return entity;
}
private async Task<List<int>> NormalizeAndValidateReceiverIdsAsync(string receiverType, IEnumerable<int>? receiverIds)
private async Task<List<int>> NormalizeAndValidateReceiverIdsAsync(
string noticeType,
string receiverType,
IEnumerable<int>? receiverIds)
{
var normalizedReceiverType = NormalizeReceiverType(receiverType);
var selectionIds = (receiverIds ?? [])
.Where(x => x > 0)
.Distinct()
.ToList();
// 公告默认全体生效,允许空接收人;站内信必须指定接收人
if (selectionIds.Count == 0)
{
if (noticeType == NoticeTypeNotice)
{
return [];
}
throw new BusinessException("接收人不能为空");
}
var normalizedReceiverType = NormalizeReceiverType(receiverType);
if (normalizedReceiverType == ReceiverTypeCustomer)
{
var customers = await CustomerRepository.GetListAsync(x =>
@ -420,6 +445,19 @@ public class NoticeService : ApplicationService
return selectionIds;
}
/// <summary>
/// 公告全体广播时接收类型仅作占位;站内信必须明确 user/customer。
/// </summary>
private static string ResolveReceiverType(string noticeType, string? receiverType)
{
if (noticeType == NoticeTypeNotice && string.IsNullOrWhiteSpace(receiverType))
{
return ReceiverTypeUser;
}
return NormalizeReceiverType(receiverType ?? string.Empty);
}
private async Task<List<int>> QueryValidUserIdsAsync(IReadOnlyCollection<int> userIds)
{
if (userIds.Count == 0)
@ -456,6 +494,7 @@ public class NoticeService : ApplicationService
List<int> receiverSelectionIds)
{
var receiverIds = OmsDocHelper.DeserializeJson<List<int>>(entity.ReceiverIds) ?? [];
var isBroadcast = receiverIds.Count == 0;
return new NoticeOutput
{
Id = entity.Id,
@ -465,12 +504,14 @@ public class NoticeService : ApplicationService
Content = entity.Content,
IsPopup = entity.IsPopup,
ReceiverType = entity.ReceiverType,
ReceiverTypeName = entity.ReceiverType == ReceiverTypeCustomer ? "客户" : "用户",
ReceiverTypeName = isBroadcast
? "全部"
: entity.ReceiverType == ReceiverTypeCustomer ? "客户" : "用户",
ReceiverIds = receiverIds,
ReceiverSelectionIds = receiverSelectionIds,
ReadCount = readCount,
IsRead = isRead,
ReceiverCount = receiverIds.Distinct().Count(),
ReceiverCount = isBroadcast ? 0 : receiverIds.Distinct().Count(),
CreateTime = entity.CreateTime,
UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime)
};

View File

@ -96,7 +96,10 @@ public partial class WarehouseService
[HttpPost("/warehouse/parcel-arrival/create")]
public async Task<WarehouseParcelArrivalOutput> CreateParcelArrivalAsync(WarehouseParcelArrivalCreateInput input)
{
var customerId = await ResolveMutationCustomerIdAsync(input.CustomerId);
// 到仓按仓库 + 快递单号定位包裹;客户归属取自包裹本身,管理端无需先选客户。
// 客户门户会话仍会限制只能到自己的包裹。
var sessionCustomerId = await ResolveListCustomerIdAsync(null);
var (hasCustomerFilter, customerIdValue) = CustomerFilter(sessionCustomerId);
if (input.WarehouseId <= 0)
{
throw new BusinessException("仓库ID不能为空");
@ -111,15 +114,21 @@ public partial class WarehouseService
var warehouse = await WarehouseAddressRepository.FirstOrDefaultAsync(x => x.Id == input.WarehouseId)
?? throw new BusinessException("仓库不存在");
var parcel = await ParcelRepository.FirstOrDefaultAsync(x =>
x.CustomerId == customerId
&& x.WarehouseId == input.WarehouseId
&& x.TrackingNo == trackingNo);
if (parcel == null)
var parcels = await ParcelRepository.GetListAsync(x =>
x.WarehouseId == input.WarehouseId
&& x.TrackingNo == trackingNo
&& (!hasCustomerFilter || x.CustomerId == customerIdValue));
if (parcels.Count == 0)
{
throw new BusinessException("未找到对应包裹,请确认快递单号与仓库");
}
if (parcels.Count > 1)
{
throw new BusinessException("同一仓库下存在多条相同快递单号的包裹,请先在「我的包裹」核对后再到仓");
}
var parcel = parcels[0];
if (parcel.LifecycleStatus == WarehouseParcelLifecycleCodes.Stored)
{
throw new BusinessException("包裹已上架,无法再到仓");
@ -131,7 +140,7 @@ public partial class WarehouseService
var entity = new CParcelArrival
{
CustomerId = customerId,
CustomerId = parcel.CustomerId,
WarehouseId = input.WarehouseId,
ParcelId = parcel.Id,
TrackingNo = trackingNo

View File

@ -0,0 +1,67 @@
using Fake.Rbac.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Oms.Domain.CustomersAggregate.Entities;
namespace Oms.Application.Shared;
/// <summary>
/// 顾问邀请绑定:注册时把所属顾问(CustomerService)写成员工登录账号。
/// </summary>
public static class StaffAdvisorBinder
{
/// <summary>
/// 仅在 CustomerService 为空时绑定;advisorCode 为员工 Account(来自 ?advisor=)。
/// 无效账号静默跳过,不阻断注册。
/// </summary>
public static async Task BindAdvisorOnCreateAsync(
Customer entity,
string? advisorCode,
FakeRbacDbContext rbacDbContext,
CancellationToken cancellationToken = default)
{
if (!string.IsNullOrWhiteSpace(entity.CustomerService))
{
return;
}
var key = advisorCode?.Trim();
if (string.IsNullOrWhiteSpace(key))
{
return;
}
var account = await ResolveStaffAccountAsync(rbacDbContext, key, cancellationToken);
if (string.IsNullOrWhiteSpace(account))
{
return;
}
entity.CustomerService = account;
}
private static async Task<string?> ResolveStaffAccountAsync(
FakeRbacDbContext rbacDbContext,
string key,
CancellationToken cancellationToken)
{
const string sql = """
SELECT u.`Account` AS `Account`
FROM `user` u
WHERE u.`IsDeleted` = 0
AND u.`Account` = {0}
ORDER BY u.`CreateTime` ASC
LIMIT 1
""";
var row = await rbacDbContext.Database
.SqlQueryRaw<AccountRow>(sql, key)
.FirstOrDefaultAsync(cancellationToken);
return string.IsNullOrWhiteSpace(row?.Account) ? null : row.Account.Trim();
}
private sealed class AccountRow
{
public string? Account { get; set; }
}
}

View File

@ -22,6 +22,11 @@ public class SalesProduct : OmsAuditedAggregateRoot
/// </summary>
public string? TradeTerms { get; set; }
/// <summary>
/// 官网线路排序,数值越小越靠前。
/// </summary>
public int RouteSort { get; set; }
/// <summary>
/// 可选附加服务字典 code JSON 数组,如 ["insurance","remoteFee"]。
/// </summary>

View File

@ -0,0 +1,15 @@
namespace Oms.Domain.OrdersAggregate.Consts;
public static class Customs9610StatusCodes
{
public const string Pending = "pending";
public const string Declared = "declared";
public static readonly string[] All = [Pending, Declared];
}
public static class Customs9610FormTypeCodes
{
public const string SimpleA = "A";
public const string SummaryB = "B";
}

View File

@ -0,0 +1,164 @@
using Oms.Domain.Shared;
using SqlSugar;
namespace Oms.Domain.OrdersAggregate.Entities;
/// <summary>
/// 9610 汇总申报单(1 运单 : 0..1)。
/// </summary>
[SugarTable("t_9610_order")]
public class Customs9610Order : OmsAuditedAggregateRoot
{
public int OrderId { get; set; }
[SugarColumn(Length = 64, IsNullable = true)]
public string? ServiceNo { get; set; }
public int CustomerId { get; set; }
[SugarColumn(Length = 16)]
public string Status { get; set; } = "pending";
/// <summary>电商平台。</summary>
[SugarColumn(Length = 16)]
public string PlatformCode { get; set; } = string.Empty;
/// <summary>电商企业代码。</summary>
[SugarColumn(Length = 18, IsNullable = true)]
public string? EcomCorpCode { get; set; }
/// <summary>电商企业名称。</summary>
[SugarColumn(Length = 100, IsNullable = true)]
public string? EcomCorpName { get; set; }
/// <summary>生产销售单位代码。</summary>
[SugarColumn(Length = 18, IsNullable = true)]
public string? ProducerCode { get; set; }
/// <summary>生产销售单位。</summary>
[SugarColumn(Length = 100, IsNullable = true)]
public string? ProducerName { get; set; }
[SugarColumn(Length = 48)]
public string ClientNo { get; set; } = string.Empty;
/// <summary>业务类型:A 简化 / B 汇总。</summary>
[SugarColumn(Length = 1)]
public string BusinessType { get; set; } = "B";
/// <summary>区域 SZ/other(A 必填)。</summary>
[SugarColumn(Length = 10, IsNullable = true)]
public string? Region { get; set; }
/// <summary>是否代理申报:是/否(A 必填)。</summary>
[SugarColumn(Length = 2, IsNullable = true)]
public string? AgentDeclare { get; set; }
/// <summary>实际卖家信用代码(A 可填)。</summary>
[SugarColumn(Length = 18, IsNullable = true)]
public string? ActualSellerCode { get; set; }
/// <summary>汇总预录:0 已预录 / 1 未预录(B 必填)。</summary>
[SugarColumn(Length = 1, IsNullable = true)]
public string? PreRecordFlag { get; set; }
/// <summary>是否汇总简化申报。</summary>
public byte IsSummarySimple { get; set; }
public decimal GrossWeight { get; set; }
[SugarColumn(Length = 4)]
public string Currency { get; set; } = "CNY";
/// <summary>目的国。</summary>
[SugarColumn(Length = 4)]
public string DestCountry { get; set; } = string.Empty;
[SugarColumn(DecimalDigits = 5, IsNullable = true)]
public decimal? Freight { get; set; }
[SugarColumn(Length = 4, IsNullable = true)]
public string? FreightCurrency { get; set; }
[SugarColumn(IsNullable = true)]
public int? FreightMark { get; set; }
[SugarColumn(IsNullable = true)]
public int? PackageType { get; set; }
[SugarColumn(IsNullable = true)]
public int? PackageQty { get; set; }
/// <summary>出货单号。</summary>
[SugarColumn(Length = 32, IsNullable = true)]
public string? ShipmentNo { get; set; }
/// <summary>海关订单号(申报回写)。</summary>
[SugarColumn(Length = 32, IsNullable = true)]
public string? CustomsOrderNo { get; set; }
[SugarColumn(Length = 32, IsNullable = true)]
public string? DownstreamRefNo { get; set; }
[SugarColumn(Length = 48, IsNullable = true)]
public string? EnterpriseBizNo { get; set; }
[SugarColumn(Length = 48, IsNullable = true)]
public string? ManifestNo { get; set; }
[SugarColumn(Length = 48, IsNullable = true)]
public string? ClearanceTime { get; set; }
[SugarColumn(Length = 48, IsNullable = true)]
public string? CustomsInboundTime { get; set; }
[SugarColumn(Length = 4, IsNullable = true)]
public string? CustomsState { get; set; }
[SugarColumn(Length = 4, IsNullable = true)]
public string? DeclareCustomsCode { get; set; }
/// <summary>电商平台代码(清单回写)。</summary>
[SugarColumn(Length = 18, IsNullable = true)]
public string? PlatformCorpCode { get; set; }
/// <summary>电商平台名称(清单回写)。</summary>
[SugarColumn(Length = 100, IsNullable = true)]
public string? PlatformName { get; set; }
[SugarColumn(Length = 18, IsNullable = true)]
public string? LogisticsCorpCode { get; set; }
[SugarColumn(Length = 100, IsNullable = true)]
public string? LogisticsCorpName { get; set; }
[SugarColumn(Length = 4, IsNullable = true)]
public string? IeMark { get; set; }
[SugarColumn(Length = 4, IsNullable = true)]
public string? ExportPortCode { get; set; }
[SugarColumn(Length = 32, IsNullable = true)]
public string? ExportDate { get; set; }
[SugarColumn(Length = 18, IsNullable = true)]
public string? DeclareAgentCode { get; set; }
[SugarColumn(Length = 100, IsNullable = true)]
public string? DeclareAgentName { get; set; }
[SugarColumn(Length = 4, IsNullable = true)]
public string? TradeModeCode { get; set; }
[SugarColumn(Length = 4, IsNullable = true)]
public string? TransportMode { get; set; }
[SugarColumn(IsNullable = true)]
public DateTime? DeclareTime { get; set; }
[SugarColumn(IsNullable = true)]
public DateTime? ManifestSyncTime { get; set; }
[SugarColumn(Length = 512, IsNullable = true)]
public string? FailReason { get; set; }
}

View File

@ -0,0 +1,59 @@
using Oms.Domain.Shared;
using SqlSugar;
namespace Oms.Domain.OrdersAggregate.Entities;
/// <summary>
/// 9610 申报商品明细(字段名对齐 t_order_declaration;海关专属另列)。
/// </summary>
[SugarTable("t_9610_order_item")]
public class Customs9610OrderItem : OmsAuditedAggregateRoot
{
public int Customs9610OrderId { get; set; }
[SugarColumn(IsNullable = true)]
public int? OrderDeclarationId { get; set; }
[SugarColumn(Length = 32)]
public string Sku { get; set; } = string.Empty;
[SugarColumn(Length = 250)]
public string CnName { get; set; } = string.Empty;
[SugarColumn(Length = 10, IsNullable = true)]
public string? ExHsCode { get; set; }
/// <summary>申报要素。</summary>
[SugarColumn(Length = 255, IsNullable = true)]
public string? DeclareElement { get; set; }
[SugarColumn(Length = 255, IsNullable = true)]
public string? Barcode { get; set; }
[SugarColumn(Length = 255, IsNullable = true)]
public string? DestCountry { get; set; }
[SugarColumn(Length = 3, IsNullable = true)]
public string? ItemCurrency { get; set; }
public decimal UnitPrice { get; set; }
[SugarColumn(DecimalDigits = 5, IsNullable = true)]
public decimal? Amount { get; set; }
public decimal Quantity { get; set; }
[SugarColumn(DecimalDigits = 5, IsNullable = true)]
public decimal? LegalQty { get; set; }
[SugarColumn(Length = 3, IsNullable = true)]
public string? Unit { get; set; }
[SugarColumn(Length = 3, IsNullable = true)]
public string? LegalUnit { get; set; }
[SugarColumn(DecimalDigits = 4, IsNullable = true)]
public decimal? Freight { get; set; }
public int SortNo { get; set; }
}

View File

@ -19,4 +19,34 @@ public class OrderReceivablePayableFilter : OrderListFilter
/// 计费阶段:forecast / inbound / outbound。
/// </summary>
public string? ChargingStage { get; set; }
/// <summary>
/// 客户编码关键词(模糊匹配)。
/// </summary>
public string? CustomerCode { get; set; }
/// <summary>
/// 仅保留各运单费用项的当前生效阶段(出库 &gt; 入库 &gt; 预报)。
/// </summary>
public bool EffectiveStageOnly { get; set; }
/// <summary>
/// 时间筛选字段:createTime / receiveTime / shipTime / payTime。
/// </summary>
public string? TimeField { get; set; }
/// <summary>
/// 时间区间起(与 TimeField 对应字段)。
/// </summary>
public DateTime? TimeStart { get; set; }
/// <summary>
/// 时间区间止(与 TimeField 对应字段)。
/// </summary>
public DateTime? TimeEnd { get; set; }
/// <summary>
/// 支付状态:0 未支付,1 已支付;null 表示全部。
/// </summary>
public int? IsPaid { get; set; }
}

View File

@ -0,0 +1,15 @@
namespace Oms.Domain.OrdersAggregate;
/// <summary>
/// 应收应付汇总行(按费用类型 + 币种)。
/// </summary>
public class OrderReceivablePayableSummaryRow
{
public string FeeType { get; set; } = string.Empty;
public string Currency { get; set; } = string.Empty;
public decimal TotalAmount { get; set; }
public int LineCount { get; set; }
}

View File

@ -0,0 +1,24 @@
using Fake.Domain.Repositories;
using Oms.Domain.OrdersAggregate.Entities;
namespace Oms.Domain.OrdersAggregate.Repositories;
public interface ICustoms9610OrderRepository : IRepository<Customs9610Order>
{
Task<Customs9610Order?> GetByOrderIdAsync(int orderId, CancellationToken cancellationToken = default);
Task HardDeleteItemsByHeaderIdAsync(int customs9610OrderId, CancellationToken cancellationToken = default);
Task HardDeleteAsync(Customs9610Order entity, CancellationToken cancellationToken = default);
}
public interface ICustoms9610OrderItemRepository : IRepository<Customs9610OrderItem>
{
Task<List<Customs9610OrderItem>> GetByHeaderIdAsync(
int customs9610OrderId,
CancellationToken cancellationToken = default);
Task<List<Customs9610OrderItem>> GetByHeaderIdsAsync(
IReadOnlyCollection<int> headerIds,
CancellationToken cancellationToken = default);
}

View File

@ -76,6 +76,15 @@ public interface IOrderRepository : IRepository<Order>
OrderReceivablePayableFilter filter,
CancellationToken cancellationToken = default);
Task<List<OrderReceivablePayableSummaryRow>> GetReceivablePayableSummaryAsync(
OrderReceivablePayableFilter filter,
CancellationToken cancellationToken = default);
Task<List<OrderReceivablePayableRow>> GetReceivablePayableExportListAsync(
OrderReceivablePayableFilter filter,
int maxRows,
CancellationToken cancellationToken = default);
Task<List<OrderStatusCountRow>> GetStatusCountsAsync(int? customerId, CancellationToken cancellationToken = default);
Task<List<OrderStatusCountRow>> GetStatusCountsByFilterAsync(
@ -86,6 +95,10 @@ public interface IOrderRepository : IRepository<Order>
OrderListFilter filter,
CancellationToken cancellationToken = default);
Task<Dictionary<string, int>> GetFulfillProgressCountsByFilterAsync(
OrderListFilter filter,
CancellationToken cancellationToken = default);
Task<int> CountByFilterAndCreateTimeRangeAsync(
OrderListFilter filter,
DateTime startInclusive,

View File

@ -8,6 +8,27 @@ public interface IPickupOrderRepository : IRepository<PickupOrder>
Task<PickupOrder?> GetByPickupNoAsync(string pickupNo, CancellationToken cancellationToken = default);
Task<PickupOrder?> GetOwnedAsync(int id, int customerId, CancellationToken cancellationToken = default);
Task<PickupWorkplaceCountRow> GetWorkplaceCountsAsync(
int? customerId,
CancellationToken cancellationToken = default);
Task<int> CountByCreateTimeRangeAsync(
int? customerId,
DateTime startInclusive,
DateTime endExclusive,
CancellationToken cancellationToken = default);
}
public sealed class PickupWorkplaceCountRow
{
public int PendingPayment { get; set; }
public int PendingSchedule { get; set; }
public int PendingPickup { get; set; }
public int PendingInbound { get; set; }
}
public interface IPickupOrderLineRepository : IRepository<PickupOrderLine>

View File

@ -0,0 +1,56 @@
using Fake.DependencyInjection;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories;
using Oms.SqlSugarCore.Repositories;
namespace Oms.SqlSugarCore.Orders;
public class Customs9610OrderRepository : OmsSqlSugarRepository<Customs9610Order>, ICustoms9610OrderRepository, IScopedDependency
{
public Task<Customs9610Order?> GetByOrderIdAsync(int orderId, CancellationToken cancellationToken = default)
{
return FirstOrDefaultAsync(x => x.OrderId == orderId, cancellationToken);
}
public async Task HardDeleteItemsByHeaderIdAsync(int customs9610OrderId, CancellationToken cancellationToken = default)
{
var db = await GetDbContextAsync(cancellationToken);
await db.Deleteable<Customs9610OrderItem>()
.Where(x => x.Customs9610OrderId == customs9610OrderId)
.ExecuteCommandAsync(cancellationToken);
}
public async Task HardDeleteAsync(Customs9610Order entity, CancellationToken cancellationToken = default)
{
var db = await GetDbContextAsync(cancellationToken);
await db.Deleteable<Customs9610Order>()
.Where(x => x.Id == entity.Id)
.ExecuteCommandAsync(cancellationToken);
}
}
public class Customs9610OrderItemRepository
: OmsSqlSugarRepository<Customs9610OrderItem>,
ICustoms9610OrderItemRepository,
IScopedDependency
{
public Task<List<Customs9610OrderItem>> GetByHeaderIdAsync(
int customs9610OrderId,
CancellationToken cancellationToken = default)
{
return GetListAsync(x => x.Customs9610OrderId == customs9610OrderId, cancellationToken: cancellationToken);
}
public async Task<List<Customs9610OrderItem>> GetByHeaderIdsAsync(
IReadOnlyCollection<int> headerIds,
CancellationToken cancellationToken = default)
{
if (headerIds == null || headerIds.Count == 0)
{
return [];
}
var ids = headerIds.Distinct().ToList();
return await GetListAsync(x => ids.Contains(x.Customs9610OrderId), cancellationToken: cancellationToken);
}
}

View File

@ -1,4 +1,5 @@
using Fake.DependencyInjection;
using Oms.Domain.CustomersAggregate.Entities;
using Oms.Domain.OrdersAggregate;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
@ -1088,8 +1089,81 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
CancellationToken cancellationToken = default)
{
var db = await GetDbContextAsync(cancellationToken);
var orderIdsQuery = ApplyListFilter(db.Queryable<Order>(), filter).Select(x => x.Id);
var feeQuery = await BuildReceivablePayableFeeQueryAsync(db, filter, cancellationToken);
var total = await feeQuery.CountAsync(cancellationToken);
var fees = await feeQuery
.OrderBy(fee => fee.CreateTime, OrderByType.Desc)
.OrderBy(fee => fee.Id, OrderByType.Desc)
.ToPageListAsync(pageIndex, pageSize, cancellationToken);
return (await MapReceivablePayableRowsAsync(db, fees, cancellationToken), total);
}
public async Task<List<OrderReceivablePayableSummaryRow>> GetReceivablePayableSummaryAsync(
OrderReceivablePayableFilter filter,
CancellationToken cancellationToken = default)
{
var db = await GetDbContextAsync(cancellationToken);
var feeQuery = await BuildReceivablePayableFeeQueryAsync(db, filter, cancellationToken);
return await feeQuery
.GroupBy(fee => new { fee.Type, fee.Currency })
.Select(g => new OrderReceivablePayableSummaryRow
{
FeeType = g.Type,
Currency = g.Currency,
TotalAmount = SqlFunc.AggregateSum(g.Amount),
LineCount = SqlFunc.AggregateCount(g.Id)
})
.ToListAsync(cancellationToken);
}
public async Task<List<OrderReceivablePayableRow>> GetReceivablePayableExportListAsync(
OrderReceivablePayableFilter filter,
int maxRows,
CancellationToken cancellationToken = default)
{
if (maxRows <= 0)
{
return [];
}
var db = await GetDbContextAsync(cancellationToken);
var feeQuery = await BuildReceivablePayableFeeQueryAsync(db, filter, cancellationToken);
var fees = await feeQuery
.OrderBy(fee => fee.CreateTime, OrderByType.Desc)
.OrderBy(fee => fee.Id, OrderByType.Desc)
.Take(maxRows)
.ToListAsync(cancellationToken);
return await MapReceivablePayableRowsAsync(db, fees, cancellationToken);
}
private static async Task<ISugarQueryable<OrderFee>> BuildReceivablePayableFeeQueryAsync(
ISqlSugarClient db,
OrderReceivablePayableFilter filter,
CancellationToken cancellationToken = default)
{
var orderQuery = ApplyListFilter(db.Queryable<Order>(), filter);
var customerCode = filter.CustomerCode?.Trim();
if (!string.IsNullOrWhiteSpace(customerCode))
{
orderQuery = orderQuery.Where(o =>
SqlFunc.Subqueryable<Customer>()
.Where(c => c.Id == o.CustomerId && c.CustomerCode.Contains(customerCode))
.Any());
}
orderQuery = ApplyReceivablePayableTimeFilter(orderQuery, filter);
if (filter.IsPaid is 0 or 1)
{
var paidFlag = (byte)filter.IsPaid.Value;
orderQuery = orderQuery.Where(o => o.IsPaid == paidFlag);
}
var orderIdsQuery = orderQuery.Select(x => x.Id);
var feeQuery = db.Queryable<OrderFee>()
.Where(fee => !fee.IsDeleted)
.In(fee => fee.OrderId, orderIdsQuery);
@ -1104,7 +1178,7 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
var costItem = filter.CostItem?.Trim();
if (!string.IsNullOrWhiteSpace(costItem))
{
feeQuery = feeQuery.Where(fee => fee.CostItem.Contains(costItem));
feeQuery = feeQuery.Where(fee => fee.CostItem == costItem || fee.CostItem.Contains(costItem));
}
var chargingStage = filter.ChargingStage?.Trim();
@ -1114,15 +1188,85 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
feeQuery = feeQuery.Where(fee => fee.ChargingStage == chargingStage);
}
var total = await feeQuery.CountAsync(cancellationToken);
var fees = await feeQuery
.OrderBy(fee => fee.CreateTime, OrderByType.Desc)
.OrderBy(fee => fee.Id, OrderByType.Desc)
.ToPageListAsync(pageIndex, pageSize, cancellationToken);
if (!filter.EffectiveStageOnly)
{
return feeQuery;
}
var candidates = await feeQuery
.Select(f => new { f.Id, f.OrderId, f.Type, f.CostItem, f.ChargingStage })
.ToListAsync(cancellationToken);
if (candidates.Count == 0)
{
return feeQuery.Where(_ => false);
}
var winningIds = candidates
.GroupBy(f => (f.OrderId, f.Type, f.CostItem))
.Select(g => g
.OrderByDescending(f => GetChargingStageRank(f.ChargingStage))
.ThenByDescending(f => f.Id)
.First()
.Id)
.ToArray();
return feeQuery.Where(f => winningIds.Contains(f.Id));
}
private static int GetChargingStageRank(string? stage) =>
stage switch
{
OrderFeeChargingStageCodes.Outbound => 3,
OrderFeeChargingStageCodes.Inbound => 2,
OrderFeeChargingStageCodes.Forecast => 1,
_ => 0
};
private static ISugarQueryable<Order> ApplyReceivablePayableTimeFilter(
ISugarQueryable<Order> query,
OrderReceivablePayableFilter filter)
{
var timeField = filter.TimeField?.Trim();
if (string.IsNullOrWhiteSpace(timeField) || string.Equals(timeField, "createTime", StringComparison.OrdinalIgnoreCase))
{
return query;
}
if (filter.TimeStart.HasValue)
{
var start = filter.TimeStart.Value;
query = timeField switch
{
"receiveTime" => query.Where(o => o.ReceiveTime != null && o.ReceiveTime >= start),
"shipTime" => query.Where(o => o.ShipTime != null && o.ShipTime >= start),
"payTime" => query.Where(o => o.PayTime != null && o.PayTime >= start),
_ => query
};
}
if (filter.TimeEnd.HasValue)
{
var end = filter.TimeEnd.Value;
query = timeField switch
{
"receiveTime" => query.Where(o => o.ReceiveTime != null && o.ReceiveTime <= end),
"shipTime" => query.Where(o => o.ShipTime != null && o.ShipTime <= end),
"payTime" => query.Where(o => o.PayTime != null && o.PayTime <= end),
_ => query
};
}
return query;
}
private static async Task<List<OrderReceivablePayableRow>> MapReceivablePayableRowsAsync(
ISqlSugarClient db,
List<OrderFee> fees,
CancellationToken cancellationToken)
{
if (fees.Count == 0)
{
return ([], total);
return [];
}
var orderIds = fees.Select(x => x.OrderId).Distinct().ToArray();
@ -1131,7 +1275,7 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
.ToListAsync(cancellationToken);
var orderMap = orders.ToDictionary(x => x.Id);
var items = fees
return fees
.Where(fee => orderMap.ContainsKey(fee.OrderId))
.Select(fee => new OrderReceivablePayableRow
{
@ -1139,8 +1283,6 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
Order = orderMap[fee.OrderId]
})
.ToList();
return (items, total);
}
public async Task<List<OrderStatusCountRow>> GetStatusCountsAsync(int? customerId, CancellationToken cancellationToken = default)
@ -1212,6 +1354,56 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
return result;
}
public async Task<Dictionary<string, int>> GetFulfillProgressCountsByFilterAsync(
OrderListFilter filter,
CancellationToken cancellationToken = default)
{
var db = await GetDbContextAsync(cancellationToken);
var baseFilter = new OrderListFilter
{
Keyword = filter.Keyword,
CreateTimeStart = filter.CreateTimeStart,
CreateTimeEnd = filter.CreateTimeEnd,
CustomerId = filter.CustomerId,
WarehouseId = filter.WarehouseId,
FulfillmentType = filter.FulfillmentType,
StockBindStatus = filter.StockBindStatus,
OrderSource = filter.OrderSource,
FieldFilters = filter.FieldFilters,
SortField = filter.SortField,
SortOrder = filter.SortOrder,
ScopeOrganizationIds = filter.ScopeOrganizationIds,
ScopeSecretLevels = filter.ScopeSecretLevels,
ScopeDataScope = filter.ScopeDataScope,
ScopePersonalOwnerMapId = filter.ScopePersonalOwnerMapId,
ScopePersonalOwnerAccount = filter.ScopePersonalOwnerAccount,
ScopeOwnerMapIds = filter.ScopeOwnerMapIds,
ScopeBizFilters = filter.ScopeBizFilters,
ScopeCustomFilters = filter.ScopeCustomFilters,
};
async Task<int> CountAsync(string? fulfillProgress)
{
var query = ApplyListFilter(db.Queryable<Order>(), baseFilter);
if (!string.IsNullOrWhiteSpace(fulfillProgress))
{
query = ApplyFulfillProgressFilter(query, fulfillProgress);
}
return await query.CountAsync(cancellationToken);
}
var result = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)
{
["all"] = await CountAsync(null),
[FulfillmentProgressCodes.PendingBind] = await CountAsync(FulfillmentProgressCodes.PendingBind),
[FulfillmentProgressCodes.DomesticInTransit] = await CountAsync(FulfillmentProgressCodes.DomesticInTransit),
[FulfillmentProgressCodes.ArrivedPendingPick] = await CountAsync(FulfillmentProgressCodes.ArrivedPendingPick),
[FulfillmentProgressCodes.OutboundDone] = await CountAsync(FulfillmentProgressCodes.OutboundDone),
};
return result;
}
public async Task<int> CountByFilterAndCreateTimeRangeAsync(
OrderListFilter filter,
DateTime startInclusive,

View File

@ -1,4 +1,5 @@
using Fake.DependencyInjection;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories;
using Oms.SqlSugarCore.Repositories;
@ -17,6 +18,63 @@ public class PickupOrderRepository : OmsSqlSugarRepository<PickupOrder>, IPickup
{
return FirstOrDefaultAsync(x => x.Id == id && x.CustomerId == customerId, cancellationToken);
}
public async Task<PickupWorkplaceCountRow> GetWorkplaceCountsAsync(
int? customerId,
CancellationToken cancellationToken = default)
{
var db = await GetDbContextAsync(cancellationToken);
var query = db.Queryable<PickupOrder>();
if (customerId is > 0)
{
query = query.Where(x => x.CustomerId == customerId.Value);
}
var terminal = new[] { PickupOrderStatusCodes.Cancelled, PickupOrderStatusCodes.Failed };
var pendingPayment = await query
.Where(x => !terminal.Contains(x.Status))
.Where(x => x.PayStatus != PickupPayStatusCodes.Paid)
.Where(x => x.ReceivableAmount != null && x.ReceivableAmount > 0)
.CountAsync(cancellationToken);
var pendingSchedule = await query
.Where(x => x.Status == PickupOrderStatusCodes.Draft)
.CountAsync(cancellationToken);
var pendingPickup = await query
.Where(x => x.Status == PickupOrderStatusCodes.Scheduled || x.Status == PickupOrderStatusCodes.Dispatched)
.CountAsync(cancellationToken);
var pendingInbound = await query
.Where(x => x.Status == PickupOrderStatusCodes.Picked)
.CountAsync(cancellationToken);
return new PickupWorkplaceCountRow
{
PendingPayment = pendingPayment,
PendingSchedule = pendingSchedule,
PendingPickup = pendingPickup,
PendingInbound = pendingInbound,
};
}
public async Task<int> CountByCreateTimeRangeAsync(
int? customerId,
DateTime startInclusive,
DateTime endExclusive,
CancellationToken cancellationToken = default)
{
var db = await GetDbContextAsync(cancellationToken);
var query = db.Queryable<PickupOrder>()
.Where(x => x.CreateTime >= startInclusive && x.CreateTime < endExclusive);
if (customerId is > 0)
{
query = query.Where(x => x.CustomerId == customerId.Value);
}
return await query.CountAsync(cancellationToken);
}
}
public class PickupOrderLineRepository : OmsSqlSugarRepository<PickupOrderLine>, IPickupOrderLineRepository, IScopedDependency

View File

@ -531,12 +531,32 @@ public partial class AuthService(
.FirstOrDefaultAsync(cancellationToken);
}
var permissions = roles
.SelectMany(x => x.Permissions.Select(p => p.PermissionCode))
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
var menus = await BuildMenusByPermissionsAsync(permissions, cancellationToken);
var isAdminRole = roles.Any(x =>
string.Equals(x.Code, "Admin", StringComparison.OrdinalIgnoreCase));
List<string> permissions;
List<MenuTreeDto> menus;
if (isAdminRole)
{
// Admin:默认全菜单/全权限码,无需在角色管理里逐项勾选
var allMenus = await GetAllActiveMenusCachedAsync(cancellationToken);
permissions = allMenus
.Select(x => x.PermissionCode)
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct(StringComparer.OrdinalIgnoreCase)
.Select(x => x!)
.ToList();
menus = BuildMenuTree(allMenus);
}
else
{
permissions = roles
.SelectMany(x => x.Permissions.Select(p => p.PermissionCode))
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
menus = await BuildMenusByPermissionsAsync(permissions, cancellationToken);
}
var dto = new UserInfoDto
{
@ -784,8 +804,12 @@ public partial class AuthService(
}
}
var menuDtos = allMenus
.Where(x => selectedMenuIds.Contains(x.Id))
return BuildMenuTree(allMenus.Where(x => selectedMenuIds.Contains(x.Id)));
}
private static List<MenuTreeDto> BuildMenuTree(IEnumerable<Fake.Rbac.Domain.MenuAggregate.Menu> menus)
{
var menuDtos = menus
.OrderBy(x => x.Order)
.ThenBy(x => x.CreateTime)
.Select(x => new MenuTreeDto

View File

@ -1,17 +1,28 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0.303 AS base
ARG BUILD_CONFIGURATION=Release
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . /src
ARG NUGET_IDESHIP=https://baget.ideship.com/v3/index.json
COPY ["NuGet.Config", "Directory.Build.props", "./"]
RUN sed -i \
-e "s|https://baget.ideship.com/v3/index.json|${NUGET_IDESHIP}|g" \
-e "s|http://baget.ideship.com/v3/index.json|${NUGET_IDESHIP}|g" \
-e "s|https://baget.xiaolipro.cn/v3/index.json|${NUGET_IDESHIP}|g" \
NuGet.Config \
&& cat NuGet.Config
COPY ["src/Oms.Domain/Oms.Domain.csproj", "src/Oms.Domain/"]
COPY ["src/Oms.SqlSugarCore/Oms.SqlSugarCore.csproj", "src/Oms.SqlSugarCore/"]
COPY ["src/Oms.Application/Oms.Application.csproj", "src/Oms.Application/"]
COPY ["src/SimpleAdmin.Api/SimpleAdmin.Api.csproj", "src/SimpleAdmin.Api/"]
RUN dotnet restore "src/SimpleAdmin.Api/SimpleAdmin.Api.csproj"
WORKDIR "/src/src/SimpleAdmin.Api"
RUN dotnet build "SimpleAdmin.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM base AS publish
COPY . .
WORKDIR /src/src/SimpleAdmin.Api
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "SimpleAdmin.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
RUN dotnet publish "SimpleAdmin.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false --no-restore
# 使用运行时镜像
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=publish /app/publish .
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "SimpleAdmin.Api.dll"]

View File

@ -7,8 +7,11 @@
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<EnableSdkContainerSupport>true</EnableSdkContainerSupport>
<ContainerBaseImage>mcr.microsoft.com/dotnet/aspnet:8.0</ContainerBaseImage>
<ContainerWorkingDirectory>/app</ContainerWorkingDirectory>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Fake.Autofac" Version="$(FakePackageVersion)" />
<PackageReference Include="Fake.Consul" Version="$(FakePackageVersion)" />

View File

@ -96,6 +96,11 @@ public class WeChatCustomerRegisterInput
/// </summary>
public string? InviteCode { get; set; }
/// <summary>
/// 顾问邀请码(可选)。来自 ?advisor=,值为员工登录账号;注册时写入所属顾问 CustomerService。
/// </summary>
public string? AdvisorCode { get; set; }
[Required]
public WeChatProfileInput Wechat { get; set; } = new();
}

View File

@ -164,6 +164,11 @@ public class WeChatCustomerService(
{
entity.ReferralChannel = "wechat";
}
await StaffAdvisorBinder.BindAdvisorOnCreateAsync(
entity,
input.AdvisorCode,
rbacDbContext,
cancellationToken);
await customerRepository.InsertAsync(entity);
try

View File

@ -64,7 +64,10 @@
},
"Itfx": {
"BaseUrl": "http://api.nanafa.com/itfx",
"TimeoutSeconds": 30
"TimeoutSeconds": 30,
"Customs9610StandardCode": "customs9610Declare",
"Customs9610ManifestStandardCode": "customs9610Manifest",
"Customs9610DeclarationFormStandardCode": "customs9610DeclarationForm"
},
"PlatformShop": {
"FrontendBaseUrl": "http://localhost:5173",