Compare commits
No commits in common. "master" and "mq" have entirely different histories.
@ -40,10 +40,6 @@ jobs:
|
||||
IMG="${{ env.TCR_REGISTRY }}/${{ env.TCR_NAMESPACE }}/${{ env.IMAGE_NAME }}:${TAG}"
|
||||
echo "TARGET=$TARGET REF=$REF REPO=$REPO TAG=$TAG host=$(hostname)"
|
||||
|
||||
# FakePackageVersion bump 后私服元数据可能仍被 NuGet http-cache 命中旧索引
|
||||
echo "==> clear NuGet http-cache (Fake packages / BaGet)"
|
||||
dotnet nuget locals http-cache --clear
|
||||
|
||||
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
|
||||
unset NAMESPACE
|
||||
cd /data/k3s
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
<Project>
|
||||
<Import Project="Fake.Local.props" Condition="Exists('Fake.Local.props')" />
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- All Fake packages -->
|
||||
<!-- 清本地nuget缓存:dotnet nuget locals -c http-cache -->
|
||||
<FakePackageVersion>8.0.5.29</FakePackageVersion>
|
||||
<FakePackageVersion>8.0.5.19</FakePackageVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
<Project>
|
||||
<!--
|
||||
UseLocalFake=true:每个 Fake.* PackageReference 换成本地工程。
|
||||
NuGet 资产全部排除,避免同名 1.0.0.0 程序集盖掉本地输出。
|
||||
-->
|
||||
<ItemGroup Condition="'$(UseLocalFake)' == 'true'">
|
||||
<PackageReference Update="Fake.*" ExcludeAssets="all" PrivateAssets="all" />
|
||||
<_LocalFakeCandidate Include="@(PackageReference -> '$(FakeRepoRoot)\src\%(Identity)\%(Identity).csproj')" />
|
||||
<_LocalFakeCandidate Include="@(PackageReference -> '$(FakeRepoRoot)\modules\rbac\%(Identity)\%(Identity).csproj')" />
|
||||
<_LocalFakeCandidate Include="@(PackageReference -> '$(FakeRepoRoot)\modules\file-management\%(Identity)\%(Identity).csproj')" />
|
||||
<!-- 变换会带上 PackageReference 的 ExcludeAssets/PrivateAssets,必须清掉,否则传递引用进不了编译。 -->
|
||||
<ProjectReference Include="@(_LocalFakeCandidate)">
|
||||
<ExcludeAssets></ExcludeAssets>
|
||||
<PrivateAssets></PrivateAssets>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="DropMissingLocalFakeProjects"
|
||||
BeforeTargets="AssignProjectConfiguration"
|
||||
Condition="'$(UseLocalFake)' == 'true'">
|
||||
<ItemGroup>
|
||||
<_MissingLocalFake Include="@(_LocalFakeCandidate)" Condition="!Exists('%(Identity)')" />
|
||||
<ProjectReference Remove="@(_MissingLocalFake)" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
</Project>
|
||||
@ -1,10 +0,0 @@
|
||||
<Project>
|
||||
<!--
|
||||
UseLocalFake=true:所有 Fake.* 包改为引用 $(FakeRepoRoot) 下的本地工程。
|
||||
UseLocalFake=false:全部走 NuGet(FakePackageVersion)。
|
||||
-->
|
||||
<PropertyGroup>
|
||||
<FakeRepoRoot Condition="'$(FakeRepoRoot)' == ''">C:\Users\BSI\RiderProjects\fake2</FakeRepoRoot>
|
||||
<UseLocalFake Condition="'$(UseLocalFake)' == ''">false</UseLocalFake>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
57
README.md
57
README.md
@ -122,63 +122,6 @@ SimpleAdmin.Api/
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 开发规范(强制)
|
||||
|
||||
更完整的分层 / 路由 / DTO 约定见 `开发复刻说明.md`、`design.md`。下列为新增代码与合入共享分支的硬性标准,评审与上传前必须满足。
|
||||
|
||||
### 1. 写接口必须写注释
|
||||
|
||||
对外暴露的 Application API(`ApplicationService` 上的 `[HttpGet]` / `[HttpPost]` 等方法)**必须**有 XML / 中文注释,至少说明:
|
||||
|
||||
- 接口用途(做什么)
|
||||
- 关键入参含义与约束(可写在方法注释或 DTO 属性注释上)
|
||||
- 关键返回语义或业务失败场景(如抛 `BusinessException` 的条件)
|
||||
|
||||
DTO、领域常量、非显而易见的私有辅助方法也应补注释,避免「只有路由名能猜业务」。
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// 批量创建仓储代发订单:建单并绑定库存/包裹,产品须为分组 wms。
|
||||
/// </summary>
|
||||
/// <param name="input">仓库、产品编码与店铺订单绑定明细。</param>
|
||||
[HttpPost("/order/platform-store-order/batch-create-fulfill-wms")]
|
||||
public async Task<PlatformStoreOrderBatchCreateLogisticsOutput> BatchCreateFulfillWmsAsync(
|
||||
PlatformStoreOrderBatchCreateFulfillInput input)
|
||||
```
|
||||
|
||||
### 2. 写 SQL 必须走 ORM(SqlSugar)
|
||||
|
||||
业务读写、列表筛选、联表、聚合、更新**一律通过 SqlSugar / Repository**,禁止在 Application / Domain 服务里手写拼装 SQL 字符串或 `Ado.SqlQuery` 作为常规实现。
|
||||
|
||||
| 允许 | 禁止(业务代码) |
|
||||
|------|------------------|
|
||||
| `Repository` / `ISugarQueryable` / 表达式 `Where` | 服务内大段 `"SELECT ... " +` 拼接 |
|
||||
| 实体 + 仓储封装复杂查询 | 绕过仓储直接 ADO 执行业务 SQL |
|
||||
| `sql/*.sql` 仅作建库 / 存量 DDL 迁移 | 用脚本替代日常业务 CRUD |
|
||||
|
||||
存量 DDL、字典初始化等脚本放在 `sql/`(说明见 `sql/README.md`),与运行时 ORM 查询分离;**运行时查询与写入必须 ORM**。
|
||||
|
||||
新增增量迁移文件命名:`migrate_<主题>_YYYYMMDD.sql`(日期在末尾,例如 `migrate_xxx_20261016.sql`)。详细约定见 `sql/README.md`。
|
||||
|
||||
### 3. 上传代码前先同步远端
|
||||
|
||||
`test`、`master` 等共享分支会有其他人提交。**推送前必须先拉取远端,再上传**,避免覆盖或漏掉别人的提交。
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git pull --ff-only origin <当前分支>
|
||||
git push origin <当前分支>
|
||||
```
|
||||
|
||||
- 工作区有未提交改动时,先暂存(stash)或提交,再同步,同步完成后再恢复。
|
||||
- 本地已有未推送提交、远端也有新提交时,先 rebase 或 merge 并解决冲突,确认与远端对齐后再 `push`。
|
||||
- 禁止在未同步远端的情况下直接 `push` 共享分支。
|
||||
|
||||
### 4. 相关约定速查
|
||||
|
||||
- **禁止**新建库级唯一索引(`UNIQUE KEY`):见 `.cursor/rules/no-unique-indexes.mdc`,业务唯一性在 Application 校验。
|
||||
- 路由显式声明;分页统一 `totalCount` + `items`;勿把 Entity 直接返回前端。
|
||||
|
||||
## Fake 框架特性
|
||||
|
||||
### 自动 API 生成
|
||||
|
||||
@ -16,7 +16,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "items", "items", "{BE5132AD
|
||||
db.sql = db.sql
|
||||
Directory.Build.props = Directory.Build.props
|
||||
.gitea\workflows\deploy.yml = .gitea\workflows\deploy.yml
|
||||
Fake.Local.props = Fake.Local.props
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleAdmin.Api", "src\SimpleAdmin.Api\SimpleAdmin.Api.csproj", "{229ED167-9ECC-4736-8DFB-522EEA8FF680}"
|
||||
|
||||
8
db.sql
8
db.sql
@ -432,9 +432,6 @@ CREATE TABLE `t_order_declaration` (
|
||||
`ImHsCode` VARCHAR(50) NULL COMMENT '进口HsCode',
|
||||
`ExHsCode` VARCHAR(50) NULL COMMENT '出口HsCode',
|
||||
`DeclareElement` VARCHAR(255) NULL COMMENT '出口申报要素',
|
||||
`ProductId` VARCHAR(100) NULL COMMENT '产品标识编码',
|
||||
`NonStdProductId` VARCHAR(100) NULL COMMENT '非标产品标识编码',
|
||||
`StdProductId` VARCHAR(100) NULL COMMENT '标准产品标识编码',
|
||||
`SkuImage` VARCHAR(512) NULL COMMENT '图片(存 storage key 或短 URL)',
|
||||
`Category` VARCHAR(20) NULL COMMENT '类别',
|
||||
`Brand` VARCHAR(50) NULL COMMENT '品牌',
|
||||
@ -1223,7 +1220,7 @@ CREATE TABLE `t_notice` (
|
||||
`Content` TEXT NOT NULL COMMENT '通知内容',
|
||||
`IsPopup` TINYINT NOT NULL DEFAULT 0 COMMENT '是否弹窗:0-否,1-是',
|
||||
`ReceiverType` VARCHAR(16) NOT NULL COMMENT '接收对象:user/customer',
|
||||
`ReceiverIds` TEXT NOT NULL COMMENT '接收对象ID列表(JSON数组字符串)',
|
||||
`ReceiverIds` JSON NOT NULL COMMENT '接收对象ID列表(JSON数组)',
|
||||
`IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是',
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳',
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||
@ -1413,9 +1410,6 @@ CREATE TABLE `c_sku` (
|
||||
`ExHsCode` VARCHAR(64) NULL COMMENT '出口HSCode',
|
||||
`ImHsCode` VARCHAR(64) NULL COMMENT '进口HSCode',
|
||||
`DeclareElement` VARCHAR(255) NULL COMMENT '出口申报要素',
|
||||
`ProductId` VARCHAR(100) NULL COMMENT '产品标识编码',
|
||||
`NonStdProductId` VARCHAR(100) NULL COMMENT '非标产品标识编码',
|
||||
`StdProductId` VARCHAR(100) NULL COMMENT '标准产品标识编码',
|
||||
`WeightG` DECIMAL(10,2) NULL COMMENT '重量(g)',
|
||||
`LengthCm` DECIMAL(10,2) NULL COMMENT '长(cm)',
|
||||
`WidthCm` DECIMAL(10,2) NULL COMMENT '宽(cm)',
|
||||
|
||||
@ -2,18 +2,6 @@
|
||||
|
||||
OMS / 共用库的建表脚本已统一到仓库根目录 `db.sql`。
|
||||
|
||||
## 职责边界
|
||||
|
||||
本目录**只放 DDL / 存量迁移 / 字典种子**等库结构与一次性数据脚本。
|
||||
|
||||
- **运行时业务查询与写入必须走 SqlSugar ORM(仓储)**,不要在 Application 里用手写 SQL 替代 CRUD。
|
||||
- 完整编码标准见仓库根目录 `README.md` →「开发规范」。
|
||||
|
||||
## 使用说明
|
||||
|
||||
- 新环境:直接执行 `db.sql`(会 DROP 后重建,勿在有生产数据的库上执行)
|
||||
- 已有库:用 diff 对比 `db.sql` 与现库结构后手工 ALTER;本目录 `migrate_all.sql` 为存量增量迁移合集(可重复执行段已做列/表存在判断;揽收 Direction / FeePush 为裸 ALTER,已执行过请跳过)
|
||||
- 数据权限表(`user_org_perm` 等)若 RBAC 与 OMS 分库,请只在 RBAC 库执行 `db.sql` 末尾对应段落
|
||||
- 单次增量迁移命名:`migrate_<主题>_YYYYMMDD.sql`(日期放在文件名**末尾**,如 `migrate_sales_store_order_provider_fulfillment_20261016.sql`)
|
||||
- 尽量可重复执行(先判断列/表/字典是否已存在)
|
||||
- **后续新增脚本一律带日期后缀**;历史未带日期的脚本不强制改名
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
-- 删除创单后置步骤表(已不再使用)。可重复执行。
|
||||
|
||||
SET @db := DATABASE();
|
||||
|
||||
SET @exists := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.TABLES
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_order_side_effect'
|
||||
);
|
||||
|
||||
SET @sql := IF(
|
||||
@exists > 0,
|
||||
'DROP TABLE `t_order_side_effect`',
|
||||
'SELECT ''t_order_side_effect already dropped'' AS info'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@ -1,105 +0,0 @@
|
||||
-- 渠道代理与返佣(首版轻量)。可重复执行。
|
||||
-- 含:t_agent、t_agent_commission_detail;t_customer / t_order 扩展 AgentId。
|
||||
-- AggregateRoot 需要 ConcurrencyStamp。
|
||||
|
||||
SET @db := DATABASE();
|
||||
|
||||
-- ---------- t_agent ----------
|
||||
CREATE TABLE IF NOT EXISTS `t_agent` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT,
|
||||
`UserId` INT NOT NULL COMMENT 'RBAC user.userid / JWT mapId',
|
||||
`AgentCode` VARCHAR(32) NOT NULL COMMENT '登录账号+注册链接编码',
|
||||
`AgentName` VARCHAR(64) NOT NULL COMMENT '显示名',
|
||||
`Phone` VARCHAR(32) NULL,
|
||||
`Email` VARCHAR(128) NULL,
|
||||
`Password` VARCHAR(64) NOT NULL COMMENT 'MD5 密码',
|
||||
`Status` TINYINT NOT NULL DEFAULT 2 COMMENT '1停用 2启用',
|
||||
`BaseType` VARCHAR(32) NOT NULL DEFAULT 'freight' COMMENT 'freight/receivable',
|
||||
`CommissionRate` DECIMAL(18,6) NOT NULL DEFAULT 0 COMMENT '返佣比例',
|
||||
`SalesProductIds` VARCHAR(512) NULL COMMENT '可选销售产品Id列表 JSON/[1,2] 或逗号;空=不限',
|
||||
`Remark` VARCHAR(512) NULL,
|
||||
`IsDeleted` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NOT NULL DEFAULT 0,
|
||||
`UpdateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
-- 不用 DEFAULT(UUID()):statement 复制下会报 1674;运行时由 ORM 写入
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT '' COMMENT '并发戳',
|
||||
PRIMARY KEY (`Id`),
|
||||
KEY `ix_agent_code` (`AgentCode`),
|
||||
KEY `ix_agent_userid` (`UserId`)
|
||||
) COMMENT='渠道代理主数据';
|
||||
|
||||
-- 已建表补 ConcurrencyStamp(可重复)
|
||||
SET @exists := (
|
||||
SELECT COUNT(1) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_agent' AND COLUMN_NAME = 'ConcurrencyStamp'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@exists = 0,
|
||||
'ALTER TABLE `t_agent` ADD COLUMN `ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT '''' COMMENT ''并发戳'' AFTER `UpdateTime`',
|
||||
'SELECT ''t_agent.ConcurrencyStamp already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ---------- t_agent_commission_detail ----------
|
||||
CREATE TABLE IF NOT EXISTS `t_agent_commission_detail` (
|
||||
`Id` INT NOT NULL AUTO_INCREMENT,
|
||||
`AgentId` INT NOT NULL,
|
||||
`OrderId` INT NOT NULL,
|
||||
`CustomerId` INT NOT NULL,
|
||||
`SalesProductId` INT NULL,
|
||||
`BaseType` VARCHAR(32) NOT NULL,
|
||||
`BaseAmount` DECIMAL(18,4) NOT NULL DEFAULT 0,
|
||||
`CommissionRate` DECIMAL(18,6) NOT NULL DEFAULT 0,
|
||||
`CommissionAmount` DECIMAL(18,4) NOT NULL DEFAULT 0,
|
||||
`Status` VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT 'pending/paid',
|
||||
`TriggerOrderStatus` VARCHAR(32) NOT NULL,
|
||||
`PaidTime` DATETIME NULL,
|
||||
`Remark` VARCHAR(512) NULL,
|
||||
`IsDeleted` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`CreateUserId` INT NOT NULL DEFAULT 0,
|
||||
`CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`UpdateUserId` INT NOT NULL DEFAULT 0,
|
||||
`UpdateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT '' COMMENT '并发戳',
|
||||
PRIMARY KEY (`Id`),
|
||||
KEY `ix_agent_comm_agent` (`AgentId`),
|
||||
KEY `ix_agent_comm_order` (`OrderId`),
|
||||
KEY `ix_agent_comm_status` (`Status`)
|
||||
) COMMENT='代理返佣明细(首版无余额入账)';
|
||||
|
||||
SET @exists := (
|
||||
SELECT COUNT(1) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_agent_commission_detail' AND COLUMN_NAME = 'ConcurrencyStamp'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@exists = 0,
|
||||
'ALTER TABLE `t_agent_commission_detail` ADD COLUMN `ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT '''' COMMENT ''并发戳'' AFTER `UpdateTime`',
|
||||
'SELECT ''t_agent_commission_detail.ConcurrencyStamp already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ---------- t_customer.AgentId ----------
|
||||
SET @exists := (
|
||||
SELECT COUNT(1) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_customer' AND COLUMN_NAME = 'AgentId'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@exists = 0,
|
||||
'ALTER TABLE `t_customer` ADD COLUMN `AgentId` INT NULL COMMENT ''所属渠道代理'' AFTER `CommissionBalance`, ADD COLUMN `AgentBindTime` DATETIME NULL COMMENT ''代理绑定时间'' AFTER `AgentId`, ADD KEY `ix_customer_agent` (`AgentId`)',
|
||||
'SELECT ''t_customer.AgentId already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- ---------- t_order.AgentId ----------
|
||||
SET @exists := (
|
||||
SELECT COUNT(1) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 't_order' AND COLUMN_NAME = 'AgentId'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@exists = 0,
|
||||
'ALTER TABLE `t_order` ADD COLUMN `AgentId` INT NULL COMMENT ''下单时代理快照'' AFTER `CustomerId`, ADD KEY `ix_order_agent` (`AgentId`)',
|
||||
'SELECT ''t_order.AgentId already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
@ -1,55 +0,0 @@
|
||||
-- c_sku:新增产品标识编码 / 非标产品标识编码 / 标准产品标识编码(对齐订单申报明细)。
|
||||
-- 可重复执行。
|
||||
|
||||
SET @db := DATABASE();
|
||||
|
||||
-- c_sku.ProductId
|
||||
SET @exists := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 'c_sku'
|
||||
AND COLUMN_NAME = 'ProductId'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@exists = 0,
|
||||
'ALTER TABLE `c_sku` ADD COLUMN `ProductId` VARCHAR(100) NULL COMMENT ''产品标识编码'' AFTER `DeclareElement`',
|
||||
'SELECT ''c_sku.ProductId already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- c_sku.NonStdProductId
|
||||
SET @exists := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 'c_sku'
|
||||
AND COLUMN_NAME = 'NonStdProductId'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@exists = 0,
|
||||
'ALTER TABLE `c_sku` ADD COLUMN `NonStdProductId` VARCHAR(100) NULL COMMENT ''非标产品标识编码'' AFTER `ProductId`',
|
||||
'SELECT ''c_sku.NonStdProductId already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- c_sku.StdProductId
|
||||
SET @exists := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 'c_sku'
|
||||
AND COLUMN_NAME = 'StdProductId'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@exists = 0,
|
||||
'ALTER TABLE `c_sku` ADD COLUMN `StdProductId` VARCHAR(100) NULL COMMENT ''标准产品标识编码'' AFTER `NonStdProductId`',
|
||||
'SELECT ''c_sku.StdProductId already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@ -1,34 +0,0 @@
|
||||
-- 费用项字典:营销抵扣(支付时写入的负数应收行)。可重复执行。
|
||||
-- coupon_discount 优惠券抵扣、member_discount 会员折扣;由 /order/pay 自动生成,禁止手工添加。
|
||||
|
||||
-- 确保字典类型存在
|
||||
INSERT INTO `t_dict_type` (`DictType`, `Remark`, `TypeSort`, `IsDeleted`, `CreateUserId`)
|
||||
SELECT 'fee_item', '费用项', 0, 0, 0
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `t_dict_type`
|
||||
WHERE `DictType` = 'fee_item' AND `IsDeleted` = 0
|
||||
);
|
||||
|
||||
-- 曾被软删的同 code 项直接恢复
|
||||
UPDATE `t_dict`
|
||||
SET `IsDeleted` = 0, `UpdateTime` = NOW()
|
||||
WHERE `DictType` = 'fee_item'
|
||||
AND `DictCode` IN ('coupon_discount', 'member_discount')
|
||||
AND `IsDeleted` = 1;
|
||||
|
||||
INSERT INTO `t_dict` (`DictType`, `DictSort`, `DictCode`, `DictName`, `IsDeleted`, `CreateUserId`)
|
||||
SELECT 'fee_item', 900, 'coupon_discount', '优惠券抵扣', 0, 0
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `t_dict`
|
||||
WHERE `DictType` = 'fee_item' AND `DictCode` = 'coupon_discount'
|
||||
);
|
||||
|
||||
INSERT INTO `t_dict` (`DictType`, `DictSort`, `DictCode`, `DictName`, `IsDeleted`, `CreateUserId`)
|
||||
SELECT 'fee_item', 901, 'member_discount', '会员折扣', 0, 0
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `t_dict`
|
||||
WHERE `DictType` = 'fee_item' AND `DictCode` = 'member_discount'
|
||||
);
|
||||
@ -1,25 +0,0 @@
|
||||
-- =============================================================================
|
||||
-- migrate_notice_receiver_ids_text.sql
|
||||
-- t_notice.ReceiverIds:JSON → TEXT,保证广播([])与定向名单可用字符串条件查询
|
||||
-- 可重复执行
|
||||
-- =============================================================================
|
||||
SET @db := DATABASE();
|
||||
|
||||
SET @col_type := (
|
||||
SELECT DATA_TYPE
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_notice'
|
||||
AND COLUMN_NAME = 'ReceiverIds'
|
||||
LIMIT 1
|
||||
);
|
||||
|
||||
SET @sql := IF(
|
||||
@col_type IS NOT NULL AND LOWER(IFNULL(@col_type, '')) = 'json',
|
||||
'ALTER TABLE `t_notice`
|
||||
MODIFY COLUMN `ReceiverIds` TEXT NOT NULL COMMENT ''接收对象ID列表(JSON数组字符串)''',
|
||||
'SELECT ''t_notice.ReceiverIds already text or missing'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@ -1,55 +0,0 @@
|
||||
-- 申报明细:新增产品标识编码 / 非标产品标识编码 / 标准产品标识编码。
|
||||
-- 可重复执行。
|
||||
|
||||
SET @db := DATABASE();
|
||||
|
||||
-- t_order_declaration.ProductId
|
||||
SET @exists := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_order_declaration'
|
||||
AND COLUMN_NAME = 'ProductId'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@exists = 0,
|
||||
'ALTER TABLE `t_order_declaration` ADD COLUMN `ProductId` VARCHAR(100) NULL COMMENT ''产品标识编码'' AFTER `DeclareElement`',
|
||||
'SELECT ''t_order_declaration.ProductId already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- t_order_declaration.NonStdProductId
|
||||
SET @exists := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_order_declaration'
|
||||
AND COLUMN_NAME = 'NonStdProductId'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@exists = 0,
|
||||
'ALTER TABLE `t_order_declaration` ADD COLUMN `NonStdProductId` VARCHAR(100) NULL COMMENT ''非标产品标识编码'' AFTER `ProductId`',
|
||||
'SELECT ''t_order_declaration.NonStdProductId already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- t_order_declaration.StdProductId
|
||||
SET @exists := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_order_declaration'
|
||||
AND COLUMN_NAME = 'StdProductId'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@exists = 0,
|
||||
'ALTER TABLE `t_order_declaration` ADD COLUMN `StdProductId` VARCHAR(100) NULL COMMENT ''标准产品标识编码'' AFTER `NonStdProductId`',
|
||||
'SELECT ''t_order_declaration.StdProductId already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@ -1,24 +0,0 @@
|
||||
-- 销售产品分组字典:仓储代发(wms)。可重复执行。
|
||||
-- 用于店铺订单「创建代发订单」选品过滤;销售产品 ProductGroup 填 wms。
|
||||
|
||||
SET @db := DATABASE();
|
||||
|
||||
-- 确保字典类型存在
|
||||
INSERT INTO `t_dict_type` (`DictType`, `Remark`, `TypeSort`, `IsDeleted`, `CreateUserId`)
|
||||
SELECT 'sales_product_group', '销售产品分组', 0, 0, 0
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `t_dict_type`
|
||||
WHERE `DictType` = 'sales_product_group' AND `IsDeleted` = 0
|
||||
);
|
||||
|
||||
-- 写入 wms 字典项
|
||||
INSERT INTO `t_dict` (`DictType`, `DictSort`, `DictCode`, `DictName`, `IsDeleted`, `CreateUserId`)
|
||||
SELECT 'sales_product_group', 90, 'wms', '仓储代发', 0, 0
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM `t_dict`
|
||||
WHERE `DictType` = 'sales_product_group'
|
||||
AND `DictCode` = 'wms'
|
||||
AND `IsDeleted` = 0
|
||||
);
|
||||
@ -1,89 +0,0 @@
|
||||
-- 店铺订单:落库 shipping_provider / fulfillment_type;删除多面单、自定义标签、拒发字段。
|
||||
-- 可重复执行。
|
||||
|
||||
SET @db := DATABASE();
|
||||
|
||||
-- t_sales_store_order.ShippingProvider
|
||||
SET @exists := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_sales_store_order'
|
||||
AND COLUMN_NAME = 'ShippingProvider'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@exists = 0,
|
||||
'ALTER TABLE `t_sales_store_order` ADD COLUMN `ShippingProvider` VARCHAR(128) NULL COMMENT ''平台物流商'' AFTER `ShippingLabelUrl`',
|
||||
'SELECT ''t_sales_store_order.ShippingProvider already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- t_sales_store_order.FulfillmentType
|
||||
SET @exists := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_sales_store_order'
|
||||
AND COLUMN_NAME = 'FulfillmentType'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@exists = 0,
|
||||
'ALTER TABLE `t_sales_store_order` ADD COLUMN `FulfillmentType` VARCHAR(64) NULL COMMENT ''平台履约类型'' AFTER `ShippingProvider`',
|
||||
'SELECT ''t_sales_store_order.FulfillmentType already exists'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- drop IsMultiLabel
|
||||
SET @exists := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_sales_store_order'
|
||||
AND COLUMN_NAME = 'IsMultiLabel'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@exists > 0,
|
||||
'ALTER TABLE `t_sales_store_order` DROP COLUMN `IsMultiLabel`',
|
||||
'SELECT ''t_sales_store_order.IsMultiLabel already dropped'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- drop CustomTags
|
||||
SET @exists := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_sales_store_order'
|
||||
AND COLUMN_NAME = 'CustomTags'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@exists > 0,
|
||||
'ALTER TABLE `t_sales_store_order` DROP COLUMN `CustomTags`',
|
||||
'SELECT ''t_sales_store_order.CustomTags already dropped'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- drop line IsShipExcluded
|
||||
SET @exists := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 't_sales_store_order_line'
|
||||
AND COLUMN_NAME = 'IsShipExcluded'
|
||||
);
|
||||
SET @sql := IF(
|
||||
@exists > 0,
|
||||
'ALTER TABLE `t_sales_store_order_line` DROP COLUMN `IsShipExcluded`',
|
||||
'SELECT ''t_sales_store_order_line.IsShipExcluded already dropped'' AS info'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@ -1,24 +0,0 @@
|
||||
-- 后台用户启用/禁用(可重复执行)。
|
||||
-- 表名:fk_user(Fake RBAC)。默认启用;禁用后拒绝账号密码登录与仓配 Token 换取。
|
||||
|
||||
SET @db := DATABASE();
|
||||
|
||||
SET @exists := (
|
||||
SELECT COUNT(1)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @db
|
||||
AND TABLE_NAME = 'fk_user'
|
||||
AND COLUMN_NAME = 'IsEnabled'
|
||||
);
|
||||
|
||||
SET @sql := IF(
|
||||
@exists = 0,
|
||||
'ALTER TABLE `fk_user` ADD COLUMN `IsEnabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT ''是否启用:1启用 0禁用'' AFTER `IsDeleted`',
|
||||
'SELECT ''fk_user.IsEnabled already exists'' AS info'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
UPDATE `fk_user` SET `IsEnabled` = 1 WHERE `IsEnabled` IS NULL;
|
||||
@ -1,6 +1,5 @@
|
||||
using Fake.DependencyInjection;
|
||||
using Fake.Rbac.Infrastructure;
|
||||
using Fake.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Oms.Application.Dtos.DataPerm;
|
||||
|
||||
@ -12,7 +11,7 @@ public class DataPermRepository(FakeRbacDbContext dbContext) : IScopedDependency
|
||||
|
||||
public async Task<RbacUserSnapshot?> GetUserSnapshotAsync(Guid userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = $@"
|
||||
const string sql = """
|
||||
SELECT
|
||||
u.`Id` AS `Id`,
|
||||
u.`userid` AS `MapId`,
|
||||
@ -20,25 +19,26 @@ public class DataPermRepository(FakeRbacDbContext dbContext) : IScopedDependency
|
||||
u.`Name` AS `Name`,
|
||||
u.`OrganizationId` AS `OrganizationId`,
|
||||
o.`Name` AS `OrganizationName`
|
||||
FROM `{FakeTables.User}` u
|
||||
LEFT JOIN `{FakeTables.Organization}` o ON o.`Id` = u.`OrganizationId` AND o.`IsDeleted` = 0
|
||||
WHERE u.`Id` = {{0}} AND u.`IsDeleted` = 0
|
||||
FROM `user` u
|
||||
LEFT JOIN `organization` o ON o.`Id` = u.`OrganizationId` AND o.`IsDeleted` = 0
|
||||
WHERE u.`Id` = {0} AND u.`IsDeleted` = 0
|
||||
LIMIT 1
|
||||
";
|
||||
""";
|
||||
|
||||
return await Db.Database.SqlQueryRaw<RbacUserSnapshot>(sql, userId.ToString())
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> IsPureCustomerUserAsync(Guid userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = $@"
|
||||
const string sql = """
|
||||
SELECT COUNT(*) AS `Value`
|
||||
FROM `{FakeTables.UserRole}` ur
|
||||
INNER JOIN `{FakeTables.Role}` r ON r.`Id` = ur.`RoleId` AND r.`IsDeleted` = 0
|
||||
WHERE ur.`UserId` = {{0}}
|
||||
FROM `user_role` ur
|
||||
INNER JOIN `role` r ON r.`Id` = ur.`RoleId` AND r.`IsDeleted` = 0
|
||||
WHERE ur.`UserId` = {0}
|
||||
AND r.`Code` <> 'Customer'
|
||||
AND r.`Code` NOT LIKE 'CustomerSub_%'
|
||||
";
|
||||
""";
|
||||
|
||||
var nonCustomerRoleCount = await Db.Database.SqlQueryRaw<int>(sql, userId.ToString())
|
||||
.FirstAsync(cancellationToken);
|
||||
@ -47,11 +47,12 @@ public class DataPermRepository(FakeRbacDbContext dbContext) : IScopedDependency
|
||||
return false;
|
||||
}
|
||||
|
||||
var hasRoleSql = $@"
|
||||
const string hasRoleSql = """
|
||||
SELECT COUNT(*) AS `Value`
|
||||
FROM `{FakeTables.UserRole}` ur
|
||||
WHERE ur.`UserId` = {{0}}
|
||||
";
|
||||
FROM `user_role` ur
|
||||
WHERE ur.`UserId` = {0}
|
||||
""";
|
||||
|
||||
var roleCount = await Db.Database.SqlQueryRaw<int>(hasRoleSql, userId.ToString())
|
||||
.FirstAsync(cancellationToken);
|
||||
return roleCount > 0;
|
||||
@ -59,13 +60,13 @@ public class DataPermRepository(FakeRbacDbContext dbContext) : IScopedDependency
|
||||
|
||||
public async Task<bool> IsAdminUserAsync(Guid userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = $@"
|
||||
const string sql = """
|
||||
SELECT COUNT(*) AS `Value`
|
||||
FROM `{FakeTables.UserRole}` ur
|
||||
INNER JOIN `{FakeTables.Role}` r ON r.`Id` = ur.`RoleId` AND r.`IsDeleted` = 0
|
||||
WHERE ur.`UserId` = {{0}}
|
||||
FROM `user_role` ur
|
||||
INNER JOIN `role` r ON r.`Id` = ur.`RoleId` AND r.`IsDeleted` = 0
|
||||
WHERE ur.`UserId` = {0}
|
||||
AND r.`Code` = 'Admin'
|
||||
";
|
||||
""";
|
||||
|
||||
var count = await Db.Database.SqlQueryRaw<int>(sql, userId.ToString()).FirstAsync(cancellationToken);
|
||||
return count > 0;
|
||||
@ -73,22 +74,23 @@ public class DataPermRepository(FakeRbacDbContext dbContext) : IScopedDependency
|
||||
|
||||
public async Task<List<Guid>> GetUserRoleIdsAsync(Guid userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = $@"
|
||||
const string sql = """
|
||||
SELECT ur.`RoleId` AS `Value`
|
||||
FROM `{FakeTables.UserRole}` ur
|
||||
WHERE ur.`UserId` = {{0}}
|
||||
";
|
||||
FROM `user_role` ur
|
||||
WHERE ur.`UserId` = {0}
|
||||
""";
|
||||
|
||||
return await Db.Database.SqlQueryRaw<Guid>(sql, userId.ToString())
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<Guid>> GetUserIdsByRoleAsync(Guid roleId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = $@"
|
||||
const string sql = """
|
||||
SELECT ur.`UserId` AS `Value`
|
||||
FROM `{FakeTables.UserRole}` ur
|
||||
WHERE ur.`RoleId` = {{0}}
|
||||
";
|
||||
FROM `user_role` ur
|
||||
WHERE ur.`RoleId` = {0}
|
||||
""";
|
||||
|
||||
return await Db.Database.SqlQueryRaw<Guid>(sql, roleId.ToString())
|
||||
.ToListAsync(cancellationToken);
|
||||
@ -113,6 +115,7 @@ public class DataPermRepository(FakeRbacDbContext dbContext) : IScopedDependency
|
||||
WHERE p.`IsDeleted` = 0
|
||||
AND p.`RoleId` IN ({inClause})
|
||||
""";
|
||||
|
||||
return await Db.Database.SqlQueryRaw<OrgPermRow>(sql).ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
@ -382,19 +385,19 @@ public class DataPermRepository(FakeRbacDbContext dbContext) : IScopedDependency
|
||||
|
||||
public async Task<List<Guid>> GetOrganizationSubtreeIdsAsync(Guid rootOrganizationId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = $@"
|
||||
const string sql = """
|
||||
WITH RECURSIVE org_tree AS (
|
||||
SELECT o.`Id`
|
||||
FROM `{FakeTables.Organization}` o
|
||||
WHERE o.`Id` = {{0}} AND o.`IsDeleted` = 0
|
||||
FROM `organization` o
|
||||
WHERE o.`Id` = {0} AND o.`IsDeleted` = 0
|
||||
UNION ALL
|
||||
SELECT c.`Id`
|
||||
FROM `{FakeTables.Organization}` c
|
||||
FROM `organization` c
|
||||
INNER JOIN org_tree t ON c.`ParentId` = t.`Id`
|
||||
WHERE c.`IsDeleted` = 0
|
||||
)
|
||||
SELECT `Id` AS `Value` FROM org_tree
|
||||
";
|
||||
""";
|
||||
|
||||
return await Db.Database.SqlQueryRaw<Guid>(sql, rootOrganizationId.ToString())
|
||||
.ToListAsync(cancellationToken);
|
||||
@ -411,24 +414,25 @@ public class DataPermRepository(FakeRbacDbContext dbContext) : IScopedDependency
|
||||
}
|
||||
|
||||
var inClause = string.Join(",", ids.Select(x => $"'{x}'"));
|
||||
var sql = $@"
|
||||
var sql = $"""
|
||||
SELECT o.`Id` AS `Id`, o.`Name` AS `Name`
|
||||
FROM `{FakeTables.Organization}` o
|
||||
FROM `organization` o
|
||||
WHERE o.`IsDeleted` = 0
|
||||
AND o.`Id` IN ({inClause})
|
||||
";
|
||||
""";
|
||||
|
||||
var rows = await Db.Database.SqlQueryRaw<OrganizationNameRow>(sql).ToListAsync(cancellationToken);
|
||||
return rows.ToDictionary(x => x.Id, x => x.Name ?? x.Id.ToString());
|
||||
}
|
||||
|
||||
public async Task<bool> OrganizationExistsAndEnabledAsync(Guid organizationId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = $@"
|
||||
const string sql = """
|
||||
SELECT COUNT(*) AS `Value`
|
||||
FROM `{FakeTables.Organization}` o
|
||||
WHERE o.`Id` = {{0}}
|
||||
FROM `organization` o
|
||||
WHERE o.`Id` = {0}
|
||||
AND o.`IsDeleted` = 0
|
||||
";
|
||||
""";
|
||||
|
||||
var count = await Db.Database.SqlQueryRaw<int>(sql, organizationId.ToString()).FirstAsync(cancellationToken);
|
||||
return count > 0;
|
||||
@ -446,12 +450,13 @@ public class DataPermRepository(FakeRbacDbContext dbContext) : IScopedDependency
|
||||
|
||||
if (dataScope == DataPermConstants.DataScopeTeam)
|
||||
{
|
||||
var sql = $@"
|
||||
const string sql = """
|
||||
SELECT u.`userid` AS `Value`
|
||||
FROM `{FakeTables.User}` u
|
||||
FROM `user` u
|
||||
WHERE u.`IsDeleted` = 0
|
||||
AND u.`OrganizationId` = {{0}}
|
||||
";
|
||||
AND u.`OrganizationId` = {0}
|
||||
""";
|
||||
|
||||
return await Db.Database.SqlQueryRaw<int>(sql, organizationId.ToString())
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
@ -465,12 +470,12 @@ public class DataPermRepository(FakeRbacDbContext dbContext) : IScopedDependency
|
||||
}
|
||||
|
||||
var inClause = string.Join(",", orgIds.Select(x => $"'{x}'"));
|
||||
var sql = $@"
|
||||
var sql = $"""
|
||||
SELECT u.`userid` AS `Value`
|
||||
FROM `{FakeTables.User}` u
|
||||
FROM `user` u
|
||||
WHERE u.`IsDeleted` = 0
|
||||
AND u.`OrganizationId` IN ({inClause})
|
||||
";
|
||||
""";
|
||||
|
||||
return await Db.Database.SqlQueryRaw<int>(sql).ToListAsync(cancellationToken);
|
||||
}
|
||||
@ -480,7 +485,7 @@ public class DataPermRepository(FakeRbacDbContext dbContext) : IScopedDependency
|
||||
|
||||
public async Task<List<DataResourceMetaDto>> GetDataResourcesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = $"""
|
||||
const string sql = """
|
||||
SELECT
|
||||
r.`Code` AS `Code`,
|
||||
r.`Name` AS `Name`,
|
||||
|
||||
@ -2,7 +2,6 @@ using System.Text.Json;
|
||||
using Fake.Application;
|
||||
using Fake.DependencyInjection;
|
||||
using Fake.Rbac.Infrastructure;
|
||||
using Fake.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using Oms.Application.Dtos.DataPerm;
|
||||
@ -16,28 +15,26 @@ public class DataPermStore(FakeRbacDbContext dbContext, DataPermContextCache cac
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
// EF Core 原始 SQL 参数不接受 DBNull;object[] 元素类型为 object,运行时仍可传 null
|
||||
private static object SqlParam(object? value) => value ?? null!;
|
||||
// EF Core 原始 SQL 参数不接受 DBNull,空值直接传 null
|
||||
private static object? SqlParam(object? value) => value;
|
||||
|
||||
public async Task<UserDataPermDto> GetUserDataPermAsync(Guid userId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var userSql = $@"
|
||||
var user = await dbContext.Database.SqlQueryRaw<UserHeaderRow>("""
|
||||
SELECT
|
||||
u.`Id` AS `UserId`,
|
||||
u.`Name` AS `UserName`,
|
||||
u.`OrganizationId` AS `OrganizationId`,
|
||||
o.`Name` AS `OrganizationName`
|
||||
FROM `{FakeTables.User}` u
|
||||
LEFT JOIN `{FakeTables.Organization}` o ON o.`Id` = u.`OrganizationId` AND o.`IsDeleted` = 0
|
||||
WHERE u.`Id` = {{0}} AND u.`IsDeleted` = 0
|
||||
FROM `user` u
|
||||
LEFT JOIN `organization` o ON o.`Id` = u.`OrganizationId` AND o.`IsDeleted` = 0
|
||||
WHERE u.`Id` = {0} AND u.`IsDeleted` = 0
|
||||
LIMIT 1
|
||||
";
|
||||
var user = await dbContext.Database.SqlQueryRaw<UserHeaderRow>(userSql, userId.ToString())
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
""", userId.ToString()).FirstOrDefaultAsync(cancellationToken)
|
||||
?? throw new BusinessException("用户不存在");
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var orgSql = $@"
|
||||
var orgRows = await dbContext.Database.SqlQueryRaw<UserOrgPermRow>("""
|
||||
SELECT
|
||||
p.`Id` AS `Id`,
|
||||
p.`OrganizationId` AS `OrganizationId`,
|
||||
@ -47,11 +44,9 @@ public class DataPermStore(FakeRbacDbContext dbContext, DataPermContextCache cac
|
||||
p.`ExpireTime` AS `ExpireTime`,
|
||||
p.`IsTemporary` AS `IsTemporary`
|
||||
FROM `user_org_perm` p
|
||||
LEFT JOIN `{FakeTables.Organization}` o ON o.`Id` = p.`OrganizationId` AND o.`IsDeleted` = 0
|
||||
WHERE p.`IsDeleted` = 0 AND p.`UserId` = {{0}}
|
||||
";
|
||||
var orgRows = await dbContext.Database.SqlQueryRaw<UserOrgPermRow>(orgSql, userId.ToString())
|
||||
.ToListAsync(cancellationToken);
|
||||
LEFT JOIN `organization` o ON o.`Id` = p.`OrganizationId` AND o.`IsDeleted` = 0
|
||||
WHERE p.`IsDeleted` = 0 AND p.`UserId` = {0}
|
||||
""", userId.ToString()).ToListAsync(cancellationToken);
|
||||
|
||||
var scopeRows = await dbContext.Database.SqlQueryRaw<UserDataScopeRow>("""
|
||||
SELECT p.`Id` AS `Id`, p.`DataScope` AS `DataScope`, p.`DataResourceCode` AS `DataResourceCode`, p.`ExpireTime` AS `ExpireTime`
|
||||
@ -132,24 +127,20 @@ public class DataPermStore(FakeRbacDbContext dbContext, DataPermContextCache cac
|
||||
|
||||
public async Task<RoleDataPermDto> GetRoleDataPermAsync(Guid roleId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var roleSql = $@"
|
||||
var role = await dbContext.Database.SqlQueryRaw<RoleHeaderRow>("""
|
||||
SELECT r.`Id` AS `RoleId`, r.`Name` AS `RoleName`
|
||||
FROM `{FakeTables.Role}` r
|
||||
WHERE r.`Id` = {{0}} AND r.`IsDeleted` = 0
|
||||
FROM `role` r
|
||||
WHERE r.`Id` = {0} AND r.`IsDeleted` = 0
|
||||
LIMIT 1
|
||||
";
|
||||
var role = await dbContext.Database.SqlQueryRaw<RoleHeaderRow>(roleSql, roleId.ToString())
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
""", roleId.ToString()).FirstOrDefaultAsync(cancellationToken)
|
||||
?? throw new BusinessException("角色不存在");
|
||||
|
||||
var orgSql = $@"
|
||||
var orgRows = await dbContext.Database.SqlQueryRaw<RoleOrgPermRow>("""
|
||||
SELECT p.`Id` AS `Id`, p.`OrganizationId` AS `OrganizationId`, o.`Name` AS `OrganizationName`, p.`OrgScope` AS `OrgScope`
|
||||
FROM `role_org_perm` p
|
||||
LEFT JOIN `{FakeTables.Organization}` o ON o.`Id` = p.`OrganizationId` AND o.`IsDeleted` = 0
|
||||
WHERE p.`IsDeleted` = 0 AND p.`RoleId` = {{0}}
|
||||
";
|
||||
var orgRows = await dbContext.Database.SqlQueryRaw<RoleOrgPermRow>(orgSql, roleId.ToString())
|
||||
.ToListAsync(cancellationToken);
|
||||
LEFT JOIN `organization` o ON o.`Id` = p.`OrganizationId` AND o.`IsDeleted` = 0
|
||||
WHERE p.`IsDeleted` = 0 AND p.`RoleId` = {0}
|
||||
""", roleId.ToString()).ToListAsync(cancellationToken);
|
||||
|
||||
var scopeRows = await dbContext.Database.SqlQueryRaw<RoleDataScopeRow>("""
|
||||
SELECT p.`Id` AS `Id`, p.`DataScope` AS `DataScope`, p.`DataResourceCode` AS `DataResourceCode`
|
||||
@ -529,7 +520,7 @@ public class DataPermStore(FakeRbacDbContext dbContext, DataPermContextCache cac
|
||||
|
||||
public async Task<List<FieldMaskItemDto>> GetFieldMasksAsync(string dataResourceCode, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sql = """
|
||||
const string sql = """
|
||||
SELECT m.`FieldName` AS `FieldName`, m.`SecretLevel` AS `SecretLevel`, m.`MaskType` AS `MaskType`, m.`MaskPattern` AS `MaskPattern`
|
||||
FROM `data_perm_field_mask` m
|
||||
WHERE m.`IsDeleted` = 0 AND m.`IsEnabled` = 1 AND m.`DataResourceCode` = {0}
|
||||
@ -649,7 +640,7 @@ public class DataPermStore(FakeRbacDbContext dbContext, DataPermContextCache cac
|
||||
|
||||
public async Task<DataPermTemplateDetailDto> GetTemplateDetailAsync(Guid templateId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var headerSql = """
|
||||
const string headerSql = """
|
||||
SELECT t.`Id` AS `Id`, t.`Name` AS `Name`, t.`Code` AS `Code`, t.`Description` AS `Description`,
|
||||
t.`RoleCode` AS `RoleCode`, t.`IsBuiltIn` AS `IsBuiltIn`
|
||||
FROM `data_perm_template` t
|
||||
@ -661,7 +652,7 @@ public class DataPermStore(FakeRbacDbContext dbContext, DataPermContextCache cac
|
||||
.FirstOrDefaultAsync(cancellationToken)
|
||||
?? throw new BusinessException("模板不存在");
|
||||
|
||||
var itemSql = """
|
||||
const string itemSql = """
|
||||
SELECT i.`Id` AS `Id`, i.`DimensionType` AS `DimensionType`, CAST(i.`ItemContent` AS CHAR) AS `ItemContentJson`, i.`SortOrder` AS `SortOrder`
|
||||
FROM `data_perm_template_item` i
|
||||
WHERE i.`TemplateId` = {0} AND i.`IsDeleted` = 0
|
||||
|
||||
@ -1,362 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Oms.Domain.Shared.Enums;
|
||||
|
||||
namespace Oms.Application.Dtos.Agents;
|
||||
|
||||
public class AgentListInput
|
||||
{
|
||||
public int Page { get; set; } = 1;
|
||||
|
||||
public int PageSize { get; set; } = 50;
|
||||
|
||||
public string? Keyword { get; set; }
|
||||
|
||||
public EnableStatus? Status { get; set; }
|
||||
}
|
||||
|
||||
public class AgentOutput
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string AgentCode { get; set; } = string.Empty;
|
||||
|
||||
public string AgentName { get; set; } = string.Empty;
|
||||
|
||||
public string? Phone { get; set; }
|
||||
|
||||
public string? Email { get; set; }
|
||||
|
||||
public EnableStatus Status { get; set; }
|
||||
|
||||
public string BaseType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>基数类型展示名(运费/应收)。</summary>
|
||||
public string? BaseTypeName { get; set; }
|
||||
|
||||
public decimal CommissionRate { get; set; }
|
||||
|
||||
public string? SalesProductIds { get; set; }
|
||||
|
||||
/// <summary>适用销售产品名称;空表示不限产品。</summary>
|
||||
public List<string> SalesProductNames { get; set; } = [];
|
||||
|
||||
public string? Remark { get; set; }
|
||||
|
||||
public DateTime CreateTime { get; set; }
|
||||
}
|
||||
|
||||
public class AgentCreateInput
|
||||
{
|
||||
[Required]
|
||||
[StringLength(32)]
|
||||
public string AgentCode { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[StringLength(64)]
|
||||
public string AgentName { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(32)]
|
||||
public string? Phone { get; set; }
|
||||
|
||||
[StringLength(128)]
|
||||
public string? Email { get; set; }
|
||||
|
||||
[Required]
|
||||
[MinLength(6)]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>freight / receivable。</summary>
|
||||
[Required]
|
||||
[StringLength(32)]
|
||||
public string BaseType { get; set; } = "freight";
|
||||
|
||||
[Range(0, 1)]
|
||||
public decimal CommissionRate { get; set; }
|
||||
|
||||
/// <summary>可选销售产品 Id,JSON 或逗号分隔;空=不限。</summary>
|
||||
[StringLength(512)]
|
||||
public string? SalesProductIds { get; set; }
|
||||
|
||||
[StringLength(512)]
|
||||
public string? Remark { get; set; }
|
||||
}
|
||||
|
||||
public class AgentUpdateInput
|
||||
{
|
||||
[Required]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(64)]
|
||||
public string AgentName { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(32)]
|
||||
public string? Phone { get; set; }
|
||||
|
||||
[StringLength(128)]
|
||||
public string? Email { get; set; }
|
||||
|
||||
/// <summary>不传则不改密码。</summary>
|
||||
[MinLength(6)]
|
||||
public string? Password { get; set; }
|
||||
|
||||
public EnableStatus Status { get; set; } = EnableStatus.Enabled;
|
||||
|
||||
[Required]
|
||||
[StringLength(32)]
|
||||
public string BaseType { get; set; } = "freight";
|
||||
|
||||
[Range(0, 1)]
|
||||
public decimal CommissionRate { get; set; }
|
||||
|
||||
[StringLength(512)]
|
||||
public string? SalesProductIds { get; set; }
|
||||
|
||||
[StringLength(512)]
|
||||
public string? Remark { get; set; }
|
||||
}
|
||||
|
||||
public class AgentRebindCustomerInput
|
||||
{
|
||||
[Required]
|
||||
public int CustomerId { get; set; }
|
||||
|
||||
/// <summary>空则解绑。</summary>
|
||||
public int? AgentId { get; set; }
|
||||
}
|
||||
|
||||
public class AgentCommissionListInput
|
||||
{
|
||||
public int Page { get; set; } = 1;
|
||||
|
||||
public int PageSize { get; set; } = 50;
|
||||
|
||||
public int? AgentId { get; set; }
|
||||
|
||||
public int? OrderId { get; set; }
|
||||
|
||||
public string? Status { get; set; }
|
||||
}
|
||||
|
||||
public class AgentCommissionDetailOutput
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int AgentId { get; set; }
|
||||
|
||||
public string? AgentCode { get; set; }
|
||||
|
||||
public string? AgentName { get; set; }
|
||||
|
||||
public int OrderId { get; set; }
|
||||
|
||||
public string? ServiceNo { get; set; }
|
||||
|
||||
public int CustomerId { get; set; }
|
||||
|
||||
public string? CustomerCode { get; set; }
|
||||
|
||||
public string? CustomerName { get; set; }
|
||||
|
||||
public int? SalesProductId { get; set; }
|
||||
|
||||
public string BaseType { get; set; } = string.Empty;
|
||||
|
||||
public decimal BaseAmount { get; set; }
|
||||
|
||||
public decimal CommissionRate { get; set; }
|
||||
|
||||
public decimal CommissionAmount { get; set; }
|
||||
|
||||
public string Status { get; set; } = string.Empty;
|
||||
|
||||
public string TriggerOrderStatus { get; set; } = string.Empty;
|
||||
|
||||
public DateTime? PaidTime { get; set; }
|
||||
|
||||
public string? Remark { get; set; }
|
||||
|
||||
public DateTime CreateTime { get; set; }
|
||||
}
|
||||
|
||||
public class AgentCommissionMarkPaidInput
|
||||
{
|
||||
[Required]
|
||||
public List<int> Ids { get; set; } = [];
|
||||
|
||||
[StringLength(512)]
|
||||
public string? Remark { get; set; }
|
||||
}
|
||||
|
||||
public class AgentPortalCustomerOutput
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string CustomerCode { get; set; } = string.Empty;
|
||||
|
||||
public string CustomerName { get; set; } = string.Empty;
|
||||
|
||||
public string? Phone { get; set; }
|
||||
|
||||
public DateTime? AgentBindTime { get; set; }
|
||||
|
||||
public DateTime CreateTime { get; set; }
|
||||
|
||||
/// <summary>最近下单时间(含名下子客户)。</summary>
|
||||
public DateTime? LastOrderTime { get; set; }
|
||||
|
||||
/// <summary>近 30 天运单数(含名下子客户,不含已删除)。</summary>
|
||||
public int OrderCount30d { get; set; }
|
||||
|
||||
/// <summary>待付款运单数(含名下子客户)。</summary>
|
||||
public int PendingPaymentCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>代理门户返佣明细分页筛选。</summary>
|
||||
public class AgentPortalCommissionListInput
|
||||
{
|
||||
public int Page { get; set; } = 1;
|
||||
|
||||
public int PageSize { get; set; } = 50;
|
||||
|
||||
/// <summary>pending / paid;空=全部。</summary>
|
||||
public string? Status { get; set; }
|
||||
|
||||
/// <summary>运单号关键字。</summary>
|
||||
public string? Keyword { get; set; }
|
||||
|
||||
public DateTime? CreateTimeStart { get; set; }
|
||||
|
||||
public DateTime? CreateTimeEnd { get; set; }
|
||||
}
|
||||
|
||||
public class AgentPortalOrderOutput
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string ServiceNo { get; set; } = string.Empty;
|
||||
|
||||
public string? TrackNo { get; set; }
|
||||
|
||||
public int? CustomerId { get; set; }
|
||||
|
||||
public string? CustomerCode { get; set; }
|
||||
|
||||
public string? CustomerName { get; set; }
|
||||
|
||||
public string? ProductCode { get; set; }
|
||||
|
||||
public string? OrderStatus { get; set; }
|
||||
|
||||
public string? ConsigneeCountryName { get; set; }
|
||||
|
||||
public decimal? ReceivableSubtotal { get; set; }
|
||||
|
||||
public decimal? PayableAmount { get; set; }
|
||||
|
||||
public DateTime CreateTime { get; set; }
|
||||
}
|
||||
|
||||
public class AgentPortalPageInput
|
||||
{
|
||||
public int Page { get; set; } = 1;
|
||||
|
||||
public int PageSize { get; set; } = 50;
|
||||
|
||||
public string? Keyword { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>匿名校验代理编号(客户注册归因)。</summary>
|
||||
public class AgentValidateCodeOutput
|
||||
{
|
||||
public bool Valid { get; set; }
|
||||
|
||||
public string? AgentCode { get; set; }
|
||||
|
||||
/// <summary>脱敏代理名称,供注册页展示。</summary>
|
||||
public string? AgentNameMasked { get; set; }
|
||||
|
||||
public string? Message { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 代理工作台统计概览。
|
||||
/// </summary>
|
||||
public class AgentWorkplaceStatsOutput
|
||||
{
|
||||
/// <summary>名下主客户数。</summary>
|
||||
public int CustomerCount { get; set; }
|
||||
|
||||
/// <summary>今日代客下单数。</summary>
|
||||
public int TodayCreated { get; set; }
|
||||
|
||||
/// <summary>昨日代客下单数。</summary>
|
||||
public int YesterdayCreated { get; set; }
|
||||
|
||||
/// <summary>较昨日新增百分比。</summary>
|
||||
public decimal? DayOverDayGrowthPercent { get; set; }
|
||||
|
||||
/// <summary>待付款运单数。</summary>
|
||||
public int PendingPayment { get; set; }
|
||||
|
||||
/// <summary>运输中运单数。</summary>
|
||||
public int Shipping { get; set; }
|
||||
|
||||
/// <summary>问题件运单数。</summary>
|
||||
public int Problematic { get; set; }
|
||||
|
||||
/// <summary>派送成功运单数(全量状态桶)。</summary>
|
||||
public int DeliverySuccess { get; set; }
|
||||
|
||||
/// <summary>预计返佣笔数(未锁定)。</summary>
|
||||
public int EstimatedCommissionCount { get; set; }
|
||||
|
||||
/// <summary>预计返佣金额(未锁定,不计入应打款)。</summary>
|
||||
public decimal EstimatedCommissionAmount { get; set; }
|
||||
|
||||
/// <summary>待结算返佣笔数。</summary>
|
||||
public int PendingCommissionCount { get; set; }
|
||||
|
||||
/// <summary>待结算返佣金额。</summary>
|
||||
public decimal PendingCommissionAmount { get; set; }
|
||||
|
||||
/// <summary>已打款返佣金额(累计)。</summary>
|
||||
public decimal PaidCommissionAmount { get; set; }
|
||||
|
||||
/// <summary>本周新增绑定主客户数(按 AgentBindTime)。</summary>
|
||||
public int WeekNewCustomers { get; set; }
|
||||
|
||||
/// <summary>派送失败运单数。</summary>
|
||||
public int DeliveryFailed { get; set; }
|
||||
|
||||
/// <summary>按 OrderStatus 分组数量。</summary>
|
||||
public Dictionary<string, int> Counts { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 代理工作台趋势点。
|
||||
/// </summary>
|
||||
public class AgentWorkplaceTrendPoint
|
||||
{
|
||||
public string Label { get; set; } = string.Empty;
|
||||
|
||||
public DateTime Start { get; set; }
|
||||
|
||||
public DateTime End { get; set; }
|
||||
|
||||
/// <summary>区间内代客下单数。</summary>
|
||||
public int OrderCount { get; set; }
|
||||
|
||||
/// <summary>区间内计提返佣金额。</summary>
|
||||
public decimal CommissionAmount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 代理工作台趋势。
|
||||
/// </summary>
|
||||
public class AgentWorkplaceTrendOutput
|
||||
{
|
||||
public string Dimension { get; set; } = "day";
|
||||
|
||||
public List<AgentWorkplaceTrendPoint> Points { get; set; } = [];
|
||||
}
|
||||
@ -16,9 +16,6 @@ public class AiProviderOutput
|
||||
|
||||
public string? ApiKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 默认模型名(历史字段名 ApiSecret,实际不当密钥脱敏)。
|
||||
/// </summary>
|
||||
public string? ApiSecret { get; set; }
|
||||
|
||||
public bool IsDefault { get; set; }
|
||||
@ -42,9 +39,6 @@ public class AiProviderSaveInput
|
||||
|
||||
public string? ApiKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 默认文字模型 ID;空表示保持原值。
|
||||
/// </summary>
|
||||
public string? ApiSecret { get; set; }
|
||||
|
||||
public bool IsDefault { get; set; }
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Oms.Application.Dtos.Channels;
|
||||
|
||||
/// <summary>
|
||||
/// 自动触发规则 Excel 导入入参。
|
||||
/// </summary>
|
||||
public class AutoTriggerImportInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 导入模式:incremental=增量;full=全量(先清空再导入)。
|
||||
/// </summary>
|
||||
public string ImportMode { get; set; } = ImportModes.Incremental;
|
||||
|
||||
/// <summary>
|
||||
/// Excel 文件。
|
||||
/// </summary>
|
||||
public IFormFile File { get; set; } = null!;
|
||||
|
||||
public static class ImportModes
|
||||
{
|
||||
public const string Incremental = "incremental";
|
||||
public const string Full = "full";
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
namespace Oms.Application.Dtos.Channels;
|
||||
|
||||
/// <summary>
|
||||
/// 自动触发规则 Excel 导入结果。
|
||||
/// </summary>
|
||||
public class AutoTriggerImportOutput
|
||||
{
|
||||
public int SuccessCount { get; set; }
|
||||
|
||||
public int SkipCount { get; set; }
|
||||
|
||||
public int FailCount { get; set; }
|
||||
|
||||
public List<AutoTriggerImportFailRow> Failures { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导入失败行。
|
||||
/// </summary>
|
||||
public class AutoTriggerImportFailRow
|
||||
{
|
||||
public int RowNumber { get; set; }
|
||||
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
}
|
||||
@ -19,49 +19,4 @@ public class KpiAreaInfoListInput
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
public int PageSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 国家/地区(匹配国家名称或国家代码,模糊)。
|
||||
/// </summary>
|
||||
public string? Country { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 匹配维度(如 zip/state/city/country,模糊)。
|
||||
/// </summary>
|
||||
public string? MatchDim { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 匹配方式(如 prefix/exact/range/all,模糊)。
|
||||
/// </summary>
|
||||
public string? MatchMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 州/省(模糊)。
|
||||
/// </summary>
|
||||
public string? Province { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 城市(模糊)。
|
||||
/// </summary>
|
||||
public string? City { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 起始邮编(模糊)。
|
||||
/// </summary>
|
||||
public string? LeftPostcode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 截止邮编(模糊)。
|
||||
/// </summary>
|
||||
public string? RightPostcode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序字段:leftPostcode / rightPostcode;空则按 Id 降序。
|
||||
/// </summary>
|
||||
public string? SortField { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序方向:asc / desc。
|
||||
/// </summary>
|
||||
public string? SortOrder { get; set; }
|
||||
}
|
||||
|
||||
@ -124,10 +124,4 @@ public class CreateCustomerInput
|
||||
[StringLength(64)]
|
||||
public string? AdvisorCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 渠道代理编号(注册绑定;有值时不走 C2C 邀请码)。
|
||||
/// </summary>
|
||||
[StringLength(32)]
|
||||
public string? AgentCode { get; set; }
|
||||
|
||||
}
|
||||
|
||||
@ -78,7 +78,7 @@ public class CustomerOutput
|
||||
public string? Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 销售人员(遗留字段,管理端已改用 AgentId)。
|
||||
/// 销售人员。
|
||||
/// </summary>
|
||||
public string? Salesman { get; set; }
|
||||
|
||||
@ -87,15 +87,6 @@ public class CustomerOutput
|
||||
/// </summary>
|
||||
public string? CustomerService { get; set; }
|
||||
|
||||
/// <summary>绑定渠道代理 Id。</summary>
|
||||
public int? AgentId { get; set; }
|
||||
|
||||
/// <summary>绑定代理编号。</summary>
|
||||
public string? AgentCode { get; set; }
|
||||
|
||||
/// <summary>绑定代理名称。</summary>
|
||||
public string? AgentName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 客户分组。
|
||||
/// </summary>
|
||||
|
||||
@ -33,7 +33,7 @@ public class CustomerPaginatedInput
|
||||
public CustomerGroup? CustomerGroup { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 销售人员(遗留筛选)。
|
||||
/// 销售人员。
|
||||
/// </summary>
|
||||
public string? Salesman { get; set; }
|
||||
|
||||
@ -42,9 +42,6 @@ public class CustomerPaginatedInput
|
||||
/// </summary>
|
||||
public string? CustomerService { get; set; }
|
||||
|
||||
/// <summary>按绑定代理 Id 筛选。</summary>
|
||||
public int? AgentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序字段:createTime / updateTime / customerCode / customerName / balance。
|
||||
/// </summary>
|
||||
|
||||
@ -78,7 +78,7 @@ public class CustomerPaginatedOutput
|
||||
public string? Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 销售人员(遗留字段,管理端已改用 AgentId)。
|
||||
/// 销售人员。
|
||||
/// </summary>
|
||||
public string? Salesman { get; set; }
|
||||
|
||||
@ -87,15 +87,6 @@ public class CustomerPaginatedOutput
|
||||
/// </summary>
|
||||
public string? CustomerService { get; set; }
|
||||
|
||||
/// <summary>绑定渠道代理 Id。</summary>
|
||||
public int? AgentId { get; set; }
|
||||
|
||||
/// <summary>绑定代理编号。</summary>
|
||||
public string? AgentCode { get; set; }
|
||||
|
||||
/// <summary>绑定代理名称。</summary>
|
||||
public string? AgentName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 客户分组。
|
||||
/// </summary>
|
||||
|
||||
@ -15,64 +15,67 @@ public class UpdateCustomerInput
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 客户编号;不传或空则保留原值(编辑页编号只读,允许局部更新省略)。
|
||||
/// 客户编号。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(32)]
|
||||
public string? CustomerCode { get; set; }
|
||||
public string CustomerCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 客户名称;不传或空则保留原值。
|
||||
/// 客户名称。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(128)]
|
||||
public string? CustomerName { get; set; }
|
||||
public string CustomerName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 手机号;不传或空则保留原值。
|
||||
/// 手机号。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(16)]
|
||||
public string? Phone { get; set; }
|
||||
public string Phone { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 邮箱;不传则保留原值,传空串可清空。
|
||||
/// 邮箱。
|
||||
/// </summary>
|
||||
[EmailAddress]
|
||||
[StringLength(64)]
|
||||
public string? Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 微信头像地址;不传则保留原值,传空串可清空。
|
||||
/// 微信头像地址。
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
public string? WechatAvatar { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 微信昵称;不传则保留原值,传空串可清空。
|
||||
/// 微信昵称。
|
||||
/// </summary>
|
||||
[StringLength(64)]
|
||||
public string? WechatNickname { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 账户余额;不传则保留原值。
|
||||
/// 账户余额。
|
||||
/// </summary>
|
||||
[Range(typeof(decimal), "0", "999999999999")]
|
||||
public decimal? Balance { get; set; }
|
||||
public decimal Balance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 总额度;不传则保留原值。
|
||||
/// 总额度。
|
||||
/// </summary>
|
||||
[Range(typeof(decimal), "0", "999999999999")]
|
||||
public decimal? TotalQuota { get; set; }
|
||||
public decimal TotalQuota { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已用额度;不传则保留原值。
|
||||
/// 已用额度。
|
||||
/// </summary>
|
||||
[Range(typeof(decimal), "0", "999999999999")]
|
||||
public decimal? UsedQuota { get; set; }
|
||||
public decimal UsedQuota { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态;不传则保留原值。
|
||||
/// 状态。
|
||||
/// </summary>
|
||||
public CustomerStatus? Status { get; set; }
|
||||
public CustomerStatus Status { get; set; } = CustomerStatus.Normal;
|
||||
|
||||
/// <summary>
|
||||
/// 备注。
|
||||
@ -81,7 +84,7 @@ public class UpdateCustomerInput
|
||||
public string? Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 销售人员(遗留;新流程请用 AgentCode)。
|
||||
/// 销售人员。
|
||||
/// </summary>
|
||||
[StringLength(32)]
|
||||
public string? Salesman { get; set; }
|
||||
@ -92,12 +95,6 @@ public class UpdateCustomerInput
|
||||
[StringLength(32)]
|
||||
public string? CustomerService { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 渠道代理编号;传空串解绑;不传则保留原绑定。仅主客户可改。
|
||||
/// </summary>
|
||||
[StringLength(32)]
|
||||
public string? AgentCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 客户分组(遗留字段,计价优先 MemberLevelId)。
|
||||
/// </summary>
|
||||
|
||||
@ -10,32 +10,12 @@ public class MonitorOverviewOutput
|
||||
public MonitorCustomerStatsOutput Customer { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 经营指标(订单量/营收/毛利)按日周月维度。
|
||||
/// </summary>
|
||||
public MonitorOpsStatsOutput Ops { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 实时积压队列(不随维度变化)。
|
||||
/// </summary>
|
||||
public MonitorQueueStatsOutput Queue { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 订单趋势(随 dimension 变化;兼容字段名 OrderTrend7d 在 day 时等同本字段)。
|
||||
/// </summary>
|
||||
public List<MonitorTrendPointOutput> OrderTrend { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 近 7 日新增订单趋势(兼容旧前端;dimension=day 时与 OrderTrend 相同)。
|
||||
/// 近 7 日新增订单趋势。
|
||||
/// </summary>
|
||||
public List<MonitorTrendPointOutput> OrderTrend7d { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 营收/毛利趋势(随 dimension 变化)。
|
||||
/// </summary>
|
||||
public List<MonitorRevenueTrendPointOutput> RevenueTrend { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 近 7 日营收/毛利趋势(兼容旧前端;dimension=day 时与 RevenueTrend 相同)。
|
||||
/// 近 7 日营收/毛利趋势。
|
||||
/// </summary>
|
||||
public List<MonitorRevenueTrendPointOutput> RevenueTrend7d { get; set; } = [];
|
||||
|
||||
@ -60,47 +40,6 @@ public class MonitorOverviewOutput
|
||||
public MonitorTrackRatesOutput TrackRates { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 经营 KPI:订单量 / 营收 / 毛利(随日周月)。
|
||||
/// </summary>
|
||||
public class MonitorOpsStatsOutput
|
||||
{
|
||||
/// <summary>day / week / month</summary>
|
||||
public string Dimension { get; set; } = "day";
|
||||
|
||||
/// <summary>当前周期新建订单数。</summary>
|
||||
public int OrderCount { get; set; }
|
||||
|
||||
/// <summary>订单量环比增长百分比。</summary>
|
||||
public decimal? OrderCountGrowthPercent { get; set; }
|
||||
|
||||
/// <summary>当前周期应收(已付)营收。</summary>
|
||||
public decimal Revenue { get; set; }
|
||||
|
||||
/// <summary>营收环比增长百分比。</summary>
|
||||
public decimal? RevenueGrowthPercent { get; set; }
|
||||
|
||||
/// <summary>当前周期毛利 = 应收 - 应付。</summary>
|
||||
public decimal GrossProfit { get; set; }
|
||||
|
||||
/// <summary>毛利环比增长百分比。</summary>
|
||||
public decimal? GrossProfitGrowthPercent { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实时积压快照(不受日周月影响)。
|
||||
/// </summary>
|
||||
public class MonitorQueueStatsOutput
|
||||
{
|
||||
public int PendingPayment { get; set; }
|
||||
|
||||
public int PendingOnline { get; set; }
|
||||
|
||||
public int PendingSign { get; set; }
|
||||
|
||||
public int Problematic { get; set; }
|
||||
}
|
||||
|
||||
public class MonitorOrderStatsOutput
|
||||
{
|
||||
public int TodayCreated { get; set; }
|
||||
|
||||
@ -1,108 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Oms.Application.Dtos.OpenApi;
|
||||
|
||||
/// <summary>
|
||||
/// 开放接口获取 Token 入参。
|
||||
/// </summary>
|
||||
public class OpenApiGetTokenInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户名(OMS 后台账号)。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(64)]
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 密码。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(128)]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开放接口简易结果(件重尺上传等)。
|
||||
/// </summary>
|
||||
public class OpenApiSimpleResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
|
||||
public string Msg { get; set; } = string.Empty;
|
||||
|
||||
public static OpenApiSimpleResult Ok(string msg = "success") =>
|
||||
new() { Success = true, Msg = msg };
|
||||
|
||||
public static OpenApiSimpleResult Fail(string msg) =>
|
||||
new() { Success = false, Msg = msg };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仓配上传件重尺入参(入库接口可忽略 mode,固定走入库扫描「入库」逻辑)。
|
||||
/// </summary>
|
||||
public class OpenApiUploadWeightInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 订单号(服务单号 / 跟踪号 / 下游单号,同入库扫描扫码框)。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(64)]
|
||||
public string OrderNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 收货实重(同入库扫描「实重」)。
|
||||
/// </summary>
|
||||
public decimal Weight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 长(同入库扫描尺寸)。
|
||||
/// </summary>
|
||||
public decimal Length { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 宽。
|
||||
/// </summary>
|
||||
public decimal Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 高。
|
||||
/// </summary>
|
||||
public decimal Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模式:0=入库(同「入库」按钮),1=出库(同「出库」按钮)。调用 /inbound 时可省略。
|
||||
/// </summary>
|
||||
public int Mode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 重量单位:KG(默认)/ G / LB。
|
||||
/// </summary>
|
||||
[StringLength(8)]
|
||||
public string? WeightUnit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 长度单位:CM(默认)/ MM / IN / M。
|
||||
/// </summary>
|
||||
[StringLength(8)]
|
||||
public string? LengthUnit { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 仓配上传红外图片入参(落订单附件 infraredImage,转存 COS)。
|
||||
/// </summary>
|
||||
public class OpenApiUploadImageInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 订单号(服务单号 / 跟踪号 / 下游单号)。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(64)]
|
||||
public string OrderNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 图片 Base64(可带 data:image/...;base64, 前缀;jpg/png/gif/webp/bmp/pdf)。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string ImageBase64 { get; set; } = string.Empty;
|
||||
}
|
||||
@ -319,68 +319,3 @@ public class Customs9610DeclareBatchOutput
|
||||
|
||||
public List<Customs9610DeclareResult> Items { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从已有运单批量创建 9610 汇总申报单入参。
|
||||
/// </summary>
|
||||
public class Customs9610CreateFromOrdersInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 运单 Id 列表(t_order.Id)。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MinLength(1)]
|
||||
public List<int> OrderIds { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 出口资质备案 Id;为空时按各运单客户的默认备案。
|
||||
/// </summary>
|
||||
public int? ExportQualificationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 电商平台编码(e-commerce_platform 字典),最长 16。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(16)]
|
||||
public string PlatformCode { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单个运单创建 9610 申报单结果。
|
||||
/// </summary>
|
||||
public class Customs9610CreateFromOrderResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 运单 Id。
|
||||
/// </summary>
|
||||
public int OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 运单号。
|
||||
/// </summary>
|
||||
public string? ServiceNo { get; set; }
|
||||
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 失败原因;成功为空。
|
||||
/// </summary>
|
||||
public string? Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 生成的 9610 申报单 Id。
|
||||
/// </summary>
|
||||
public int? Customs9610OrderId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量创建 9610 申报单结果。
|
||||
/// </summary>
|
||||
public class Customs9610CreateFromOrdersOutput
|
||||
{
|
||||
public int SuccessCount { get; set; }
|
||||
|
||||
public int FailCount { get; set; }
|
||||
|
||||
public List<Customs9610CreateFromOrderResult> Items { get; set; } = [];
|
||||
}
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
namespace Oms.Application.Dtos.Orders;
|
||||
|
||||
/// <summary>
|
||||
/// itfx getFile(ForwardWithSubscribe)标准请求体。
|
||||
/// </summary>
|
||||
public class ItfxGetFileBody
|
||||
{
|
||||
/// <summary>
|
||||
/// 客户单号。
|
||||
/// </summary>
|
||||
public string? ClientNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 跟踪号。
|
||||
/// </summary>
|
||||
public string? TrackNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务商单号。
|
||||
/// </summary>
|
||||
public string? ServiceNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件类型:下游面单=21,末端面单=31,POD=180。
|
||||
/// </summary>
|
||||
public int Type { get; set; }
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Oms.Application.Dtos.Orders;
|
||||
|
||||
/// <summary>
|
||||
/// 出库后状态校正入参:仅允许在待上网 / 运输中 / 派送成功 / 派送失败之间变更;派送成功只进不出。
|
||||
/// </summary>
|
||||
public class OrderChangePostShipStatusInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 订单主键。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 目标订单状态(pendingOnline / shipping / deliverySuccess / deliveryFailed)。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(32)]
|
||||
public string TargetStatus { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 变更备注(必填,落操作日志)。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(512)]
|
||||
public string Remark { get; set; } = string.Empty;
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Oms.Application.Dtos.Orders;
|
||||
|
||||
/// <summary>
|
||||
/// 修改订单销售产品入参。
|
||||
/// </summary>
|
||||
public class OrderChangeProductInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 订单主键。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 新销售产品编码。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(64)]
|
||||
public string ProductCode { get; set; } = string.Empty;
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
using Oms.Application.Dtos.Orders;
|
||||
|
||||
namespace Oms.Application.Dtos.Orders;
|
||||
|
||||
/// <summary>
|
||||
@ -41,56 +43,3 @@ public class OrderDownstreamForwardResult
|
||||
/// </summary>
|
||||
public string? TrackNo { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下游 getFile 入参。
|
||||
/// </summary>
|
||||
public class OrderDownstreamGetFileInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 订单 Id。
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件类型:下游面单=21,末端面单=31,POD=180。
|
||||
/// </summary>
|
||||
public int Type { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下游 getFile 转发结果。
|
||||
/// </summary>
|
||||
public class OrderDownstreamGetFileResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
|
||||
public string SupplierCode { get; set; } = string.Empty;
|
||||
|
||||
public int Type { get; set; }
|
||||
|
||||
public string? FileType { get; set; }
|
||||
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
public string RequestJson { get; set; } = string.Empty;
|
||||
|
||||
public int? StatusCode { get; set; }
|
||||
|
||||
public string ResponseBody { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 转存到自有 COS 后的公开地址。
|
||||
/// </summary>
|
||||
public string? StoredFileUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 回写后的服务商单号(Data.serviceNo)。
|
||||
/// </summary>
|
||||
public string? DownstreamServiceNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 回写后的跟踪号(Data.trackNo)。
|
||||
/// </summary>
|
||||
public string? TrackNo { get; set; }
|
||||
}
|
||||
|
||||
@ -20,11 +20,6 @@ public class OrderFeeOutput
|
||||
/// </summary>
|
||||
public string CostItem { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 费用项显示名:仅营销抵扣行(优惠券抵扣/会员折扣)由后端给出,其余为空由前端按 fee_item 字典解析。
|
||||
/// </summary>
|
||||
public string? CostItemName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 金额。
|
||||
/// </summary>
|
||||
|
||||
@ -70,21 +70,6 @@ public class OrderItemOutput
|
||||
/// </summary>
|
||||
public string? DeclareElement { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 产品标识编码。
|
||||
/// </summary>
|
||||
public string? ProductId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 非标产品标识编码。
|
||||
/// </summary>
|
||||
public string? NonStdProductId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标准产品标识编码。
|
||||
/// </summary>
|
||||
public string? StdProductId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// SKU 图片。
|
||||
/// </summary>
|
||||
|
||||
@ -368,31 +368,6 @@ public class OrderOutput
|
||||
/// </summary>
|
||||
public decimal? ForecastPayableAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 营销快照:折前应收小计。
|
||||
/// </summary>
|
||||
public decimal? ReceivableSubtotal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 营销快照:会员折扣金额(与券互斥)。
|
||||
/// </summary>
|
||||
public decimal LevelDiscountAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 营销快照:优惠券抵扣金额。
|
||||
/// </summary>
|
||||
public decimal CouponDiscountAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 营销快照:客户实付金额(支付扣款口径;区别于应付成本)。
|
||||
/// </summary>
|
||||
public decimal? ActualPayAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 营销快照:使用的优惠券实例 Id。
|
||||
/// </summary>
|
||||
public int? CouponInstanceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单绑定的销售产品规则发布版本。
|
||||
/// </summary>
|
||||
|
||||
@ -1,6 +1,3 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Oms.Application.Dtos.Orders;
|
||||
|
||||
/// <summary>
|
||||
@ -68,27 +65,6 @@ public class OrderParcelItemInput
|
||||
/// </summary>
|
||||
public string? DeclareElement { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 产品标识编码(选填)。预报下游始终序列化,无值时为 null。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
|
||||
public string? ProductId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 非标产品标识编码(选填)。预报下游始终序列化,无值时为 null。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
|
||||
public string? NonStdProductId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标准产品标识编码(选填)。预报下游始终序列化,无值时为 null。
|
||||
/// </summary>
|
||||
[StringLength(100)]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
|
||||
public string? StdProductId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// SKU 图片。
|
||||
/// </summary>
|
||||
|
||||
@ -1,81 +0,0 @@
|
||||
using Oms.Application.Dtos.Marketing;
|
||||
|
||||
namespace Oms.Application.Dtos.Orders;
|
||||
|
||||
/// <summary>
|
||||
/// 订单支付入参(支付时选券)。
|
||||
/// </summary>
|
||||
public class OrderPayInput : OrderActionInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 选用的优惠券实例 Id:null 沿用订单已绑定的券(不可用则不使用);0 不使用券;大于 0 使用指定券。
|
||||
/// </summary>
|
||||
public int? CouponInstanceId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 订单支付预览入参。
|
||||
/// </summary>
|
||||
public class OrderPayPreviewInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 订单主键。
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 试算用券:null 自动推荐(优先已绑定券,否则选优惠最大且优于会员折的券);0 不使用券;大于 0 使用指定券。
|
||||
/// </summary>
|
||||
public int? CouponInstanceId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 订单支付预览:应付拆解与可选券。
|
||||
/// </summary>
|
||||
public class OrderPayPreviewOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 订单主键。
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 应收小计(折前)。
|
||||
/// </summary>
|
||||
public decimal ReceivableSubtotal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 基础运费(券门槛与抵扣口径)。
|
||||
/// </summary>
|
||||
public decimal BaseFreightAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 会员折扣金额(与券互斥,用券时为 0)。
|
||||
/// </summary>
|
||||
public decimal LevelDiscountAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 优惠券抵扣金额。
|
||||
/// </summary>
|
||||
public decimal CouponDiscountAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实际应付金额(支付扣款金额)。
|
||||
/// </summary>
|
||||
public decimal PayableAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 会员等级名称。
|
||||
/// </summary>
|
||||
public string? MemberLevelName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 本次试算实际生效的券实例 Id;为空表示未用券。
|
||||
/// </summary>
|
||||
public int? AppliedCouponInstanceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前客户可选券(CanUse=false 时附不可用原因,含「优惠不高于会员折扣」)。
|
||||
/// </summary>
|
||||
public List<CouponAvailableOutput> AvailableCoupons { get; set; } = [];
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
namespace Oms.Application.Dtos.Orders;
|
||||
|
||||
/// <summary>
|
||||
/// 查价表单下拉选项(国家 / 发货仓 / 货物属性),可供访客匿名加载。
|
||||
/// </summary>
|
||||
public class OrderPriceCheckFormOptionsOutput
|
||||
{
|
||||
/// <summary>目的国家列表。</summary>
|
||||
public List<OrderPriceCheckCountryOptionOutput> Countries { get; set; } = [];
|
||||
|
||||
/// <summary>发货仓列表(仅 id + 名称,不含地址电话)。</summary>
|
||||
public List<OrderPriceCheckWarehouseOptionOutput> Warehouses { get; set; } = [];
|
||||
|
||||
/// <summary>货物属性(字典 cargo_attribute)。</summary>
|
||||
public List<OrderPriceCheckCargoOptionOutput> CargoAttributes { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>查价目的国家选项。</summary>
|
||||
public class OrderPriceCheckCountryOptionOutput
|
||||
{
|
||||
/// <summary>ISO 二字码。</summary>
|
||||
public string CountryCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>中文名。</summary>
|
||||
public string ChineseName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>英文名。</summary>
|
||||
public string EnglishName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>查价发货仓选项。</summary>
|
||||
public class OrderPriceCheckWarehouseOptionOutput
|
||||
{
|
||||
/// <summary>仓库主键。</summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>仓库名称。</summary>
|
||||
public string WarehouseName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>查价货物属性选项。</summary>
|
||||
public class OrderPriceCheckCargoOptionOutput
|
||||
{
|
||||
/// <summary>字典编码。</summary>
|
||||
public string DictCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>字典名称。</summary>
|
||||
public string DictName { get; set; } = string.Empty;
|
||||
}
|
||||
@ -25,12 +25,6 @@ public class OrderUpdateInput
|
||||
[StringLength(64)]
|
||||
public string? TrackNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务商单号。
|
||||
/// </summary>
|
||||
[StringLength(64)]
|
||||
public string? DownstreamServiceNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 渠道编码。
|
||||
/// </summary>
|
||||
|
||||
@ -11,9 +11,6 @@ public class PlatformStorePageInput
|
||||
|
||||
public int? CustomerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 平台编码:tiktok / temu / shopee / lazada;空或 all 表示全部。
|
||||
/// </summary>
|
||||
public string? PlatformCode { get; set; }
|
||||
|
||||
public string? StoreNickname { get; set; }
|
||||
@ -150,6 +147,11 @@ public class PlatformStoreOrderListInput : PlatformStorePageInput
|
||||
{
|
||||
public int? StoreId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 平台编码:tiktok / temu / shopee / lazada;空或 all 表示全部。
|
||||
/// </summary>
|
||||
public string? PlatformCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 履约状态:pending / toShip / shipped / canceled / suspended / completed;空或 all 表示全部。
|
||||
/// </summary>
|
||||
@ -180,6 +182,8 @@ public class PlatformStoreOrderLineOutput
|
||||
|
||||
public string? Currency { get; set; }
|
||||
|
||||
public bool IsShipExcluded { get; set; }
|
||||
|
||||
public int? MappedSkuId { get; set; }
|
||||
|
||||
public string? MappedSkuCode { get; set; }
|
||||
@ -243,11 +247,9 @@ public class PlatformStoreOrderOutput
|
||||
|
||||
public string? ShippingLabelUrl { get; set; }
|
||||
|
||||
/// <summary>平台物流商。</summary>
|
||||
public string? ShippingProvider { get; set; }
|
||||
public bool IsMultiLabel { get; set; }
|
||||
|
||||
/// <summary>平台履约类型。</summary>
|
||||
public string? FulfillmentType { get; set; }
|
||||
public string? CustomTags { get; set; }
|
||||
|
||||
public bool IsSuspended { get; set; }
|
||||
|
||||
@ -364,60 +366,6 @@ public class PlatformStoreOrderMatchInventoryOutput
|
||||
public string? ClientNo { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 店铺订单创建运单时填写的运单基本信息(对齐下单页基本信息)。
|
||||
/// </summary>
|
||||
public class PlatformStoreOrderParcelBasicInput
|
||||
{
|
||||
/// <summary>客户单号;空则沿用平台订单号。</summary>
|
||||
public string? ClientNo { get; set; }
|
||||
|
||||
/// <summary>货物属性:general / electronics / sensitive。</summary>
|
||||
public string? CargoAttribute { get; set; }
|
||||
|
||||
/// <summary>实重(kg)。</summary>
|
||||
public decimal? ActualWeight { get; set; }
|
||||
|
||||
/// <summary>包裹数量。</summary>
|
||||
public int? Quantity { get; set; }
|
||||
|
||||
/// <summary>长(cm)。</summary>
|
||||
public decimal? Length { get; set; }
|
||||
|
||||
/// <summary>宽(cm)。</summary>
|
||||
public decimal? Width { get; set; }
|
||||
|
||||
/// <summary>高(cm)。</summary>
|
||||
public decimal? Height { get; set; }
|
||||
|
||||
/// <summary>出口报关方式。</summary>
|
||||
public string? ExportCustomsMode { get; set; }
|
||||
|
||||
/// <summary>出口资质备案 Id(9610Summary 时必填)。</summary>
|
||||
public int? ExportQualificationId { get; set; }
|
||||
|
||||
/// <summary>电商平台编码(9610Summary 时必填)。</summary>
|
||||
public string? PlatformCode { get; set; }
|
||||
|
||||
/// <summary>到仓方式:selfShip / doorPickup。</summary>
|
||||
public string? InboundMethod { get; set; }
|
||||
|
||||
/// <summary>上门揽收发件地址簿 Id。</summary>
|
||||
public int? SenderAddressId { get; set; }
|
||||
|
||||
/// <summary>期望揽收时间。</summary>
|
||||
public DateTime? ExpectPickupTime { get; set; }
|
||||
|
||||
/// <summary>揽收方向。</summary>
|
||||
public string? PickupDirection { get; set; }
|
||||
|
||||
/// <summary>承运商编码。</summary>
|
||||
public string? CarrierCode { get; set; }
|
||||
|
||||
/// <summary>承运服务。</summary>
|
||||
public string? CarrierService { get; set; }
|
||||
}
|
||||
|
||||
public class PlatformStoreOrderBatchCreateLogisticsInput
|
||||
{
|
||||
[Required]
|
||||
@ -427,9 +375,6 @@ public class PlatformStoreOrderBatchCreateLogisticsInput
|
||||
|
||||
public int? CustomerId { get; set; }
|
||||
|
||||
/// <summary>运单基本信息(创建运单/代发运单必填)。</summary>
|
||||
public PlatformStoreOrderParcelBasicInput? ParcelBasic { get; set; }
|
||||
|
||||
[Required]
|
||||
public List<PlatformStoreOrderBatchCreateItemInput> Items { get; set; } = [];
|
||||
}
|
||||
@ -444,9 +389,6 @@ public class PlatformStoreOrderBatchCreateFulfillInput
|
||||
|
||||
public int? CustomerId { get; set; }
|
||||
|
||||
/// <summary>运单基本信息(创建运单/代发运单必填)。</summary>
|
||||
public PlatformStoreOrderParcelBasicInput? ParcelBasic { get; set; }
|
||||
|
||||
[Required]
|
||||
public List<PlatformStoreOrderBatchCreateItemInput> Items { get; set; } = [];
|
||||
}
|
||||
@ -456,9 +398,6 @@ public class PlatformStoreOrderBatchCreateItemInput
|
||||
[Required]
|
||||
public int StoreOrderId { get; set; }
|
||||
|
||||
/// <summary>覆盖本单客户单号;空则用 ParcelBasic.ClientNo 或平台订单号。</summary>
|
||||
public string? ClientNo { get; set; }
|
||||
|
||||
public OrderParcelReceiverInput? Receiver { get; set; }
|
||||
|
||||
public List<OrderStockBindSaveItemInput> BindItems { get; set; } = [];
|
||||
|
||||
@ -26,11 +26,6 @@ public class InternalUserListItemDto
|
||||
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用。
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
|
||||
public List<InternalUserRoleDto> Roles { get; set; } = [];
|
||||
}
|
||||
|
||||
|
||||
@ -16,11 +16,6 @@ public class UserProfileData
|
||||
/// 企业微信客服接入链接(微信外接入)。
|
||||
/// </summary>
|
||||
public string? WecomKfUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用;缺省按启用处理。
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
}
|
||||
|
||||
public class OmsUserCreateInput : UserCreateDto
|
||||
@ -34,11 +29,6 @@ public class OmsUserCreateInput : UserCreateDto
|
||||
public string? WechatQrCode { get; set; }
|
||||
|
||||
public string? WecomKfUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用,默认 true。
|
||||
/// </summary>
|
||||
public bool? IsEnabled { get; set; }
|
||||
}
|
||||
|
||||
public class OmsUserUpdateInput : UserUpdateDto
|
||||
@ -50,11 +40,6 @@ public class OmsUserUpdateInput : UserUpdateDto
|
||||
public string? WechatQrCode { get; set; }
|
||||
|
||||
public string? WecomKfUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用;不传则不改。
|
||||
/// </summary>
|
||||
public bool? IsEnabled { get; set; }
|
||||
}
|
||||
|
||||
public class OmsUserDetailDto : UserDto
|
||||
@ -68,11 +53,6 @@ public class OmsUserDetailDto : UserDto
|
||||
public string? WechatQrCode { get; set; }
|
||||
|
||||
public string? WecomKfUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用。
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -85,14 +65,3 @@ public class ResetUserPasswordInput
|
||||
/// </summary>
|
||||
public string NewPassword { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置用户启用状态入参。
|
||||
/// </summary>
|
||||
public class SetUserEnabledInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否启用。
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; set; }
|
||||
}
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Oms.Application.Dtos.System;
|
||||
|
||||
/// <summary>
|
||||
/// 国家/地区 Excel 导入入参。
|
||||
/// </summary>
|
||||
public class CountryImportInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 导入模式:incremental=按二字码新增或更新;full=按二字码新增或更新,并软删除文件中未出现的国家。
|
||||
/// </summary>
|
||||
public string ImportMode { get; set; } = ImportModes.Incremental;
|
||||
|
||||
/// <summary>
|
||||
/// Excel 文件。
|
||||
/// </summary>
|
||||
public IFormFile File { get; set; } = null!;
|
||||
|
||||
public static class ImportModes
|
||||
{
|
||||
public const string Incremental = "incremental";
|
||||
public const string Full = "full";
|
||||
}
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
namespace Oms.Application.Dtos.System;
|
||||
|
||||
/// <summary>
|
||||
/// 国家/地区 Excel 导入结果。
|
||||
/// </summary>
|
||||
public class CountryImportOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 成功写入条数。
|
||||
/// </summary>
|
||||
public int SuccessCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 跳过条数。
|
||||
/// </summary>
|
||||
public int SkipCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 失败条数。
|
||||
/// </summary>
|
||||
public int FailCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 全量导入时软删除的条数。
|
||||
/// </summary>
|
||||
public int DeletedCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 失败行明细。
|
||||
/// </summary>
|
||||
public List<CountryImportFailRow> Failures { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 导入失败行。
|
||||
/// </summary>
|
||||
public class CountryImportFailRow
|
||||
{
|
||||
/// <summary>
|
||||
/// Excel 行号(含表头,从 2 起为数据行)。
|
||||
/// </summary>
|
||||
public int RowNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 失败原因。
|
||||
/// </summary>
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
}
|
||||
@ -24,14 +24,4 @@ public class CountryOutput
|
||||
/// ISO 3166-1 alpha-2 国家二字码。
|
||||
/// </summary>
|
||||
public string CountryCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间。
|
||||
/// </summary>
|
||||
public DateTime CreateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间;未更新时为空。
|
||||
/// </summary>
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
}
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Oms.Application.Dtos.System;
|
||||
|
||||
/// <summary>
|
||||
/// 新增国家/地区入参。
|
||||
/// </summary>
|
||||
public class CreateCountryInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 国家中文名。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string ChineseName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 国家英文名。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string EnglishName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// ISO 3166-1 alpha-2 国家二字码。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string CountryCode { get; set; } = string.Empty;
|
||||
}
|
||||
@ -34,9 +34,4 @@ public class NoticeListInput
|
||||
/// 仅未读(相对当前登录主体)。
|
||||
/// </summary>
|
||||
public bool? OnlyUnread { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 管理端查看全部通知(仅非客户登录生效)。默认按当前主体可见范围过滤。
|
||||
/// </summary>
|
||||
public bool? Manage { get; set; }
|
||||
}
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Oms.Application.Dtos.System;
|
||||
|
||||
/// <summary>
|
||||
/// 更新国家/地区入参。
|
||||
/// </summary>
|
||||
public class UpdateCountryInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 国家中文名。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string ChineseName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 国家英文名。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string EnglishName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// ISO 3166-1 alpha-2 国家二字码。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string CountryCode { get; set; } = string.Empty;
|
||||
}
|
||||
@ -47,21 +47,6 @@ public class WarehouseSkuOutput
|
||||
/// </summary>
|
||||
public string? DeclareElement { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 产品标识编码。
|
||||
/// </summary>
|
||||
public string? ProductId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 非标产品标识编码。
|
||||
/// </summary>
|
||||
public string? NonStdProductId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标准产品标识编码(如 GTIN / UPC / EAN)。
|
||||
/// </summary>
|
||||
public string? StdProductId { get; set; }
|
||||
|
||||
public decimal? WeightG { get; set; }
|
||||
|
||||
public decimal? LengthCm { get; set; }
|
||||
@ -106,21 +91,6 @@ public class WarehouseSkuSaveInput
|
||||
/// </summary>
|
||||
public string? DeclareElement { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 产品标识编码。
|
||||
/// </summary>
|
||||
public string? ProductId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 非标产品标识编码。
|
||||
/// </summary>
|
||||
public string? NonStdProductId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 标准产品标识编码(如 GTIN / UPC / EAN)。
|
||||
/// </summary>
|
||||
public string? StdProductId { get; set; }
|
||||
|
||||
public decimal? WeightG { get; set; }
|
||||
|
||||
public decimal? LengthCm { get; set; }
|
||||
@ -288,7 +258,6 @@ public class WarehouseInboundSaveInput
|
||||
[Required]
|
||||
public int WarehouseId { get; set; }
|
||||
|
||||
/// <summary>唛头/快递单号,选填。</summary>
|
||||
public string? MarkReference { get; set; }
|
||||
|
||||
public string InboundType { get; set; } = "normal";
|
||||
@ -376,77 +345,6 @@ public class WarehouseInboundSummaryOutput
|
||||
public int Completed { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打印库内 SKU 标签入参。
|
||||
/// </summary>
|
||||
public class WarehouseSkuPrintLabelInput
|
||||
{
|
||||
/// <summary>
|
||||
/// SKU ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 客户 ID(管理端可选)。
|
||||
/// </summary>
|
||||
public int? CustomerId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打印库内 SKU 标签出参。
|
||||
/// </summary>
|
||||
public class WarehouseSkuPrintLabelOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 可打印的 PDF 公网地址。
|
||||
/// </summary>
|
||||
public string PrintUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// SKU 编码。
|
||||
/// </summary>
|
||||
public string SkuCode { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打印入库箱唛标签入参。
|
||||
/// </summary>
|
||||
public class WarehouseInboundPrintLabelsInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 入库预报 ID。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 客户 ID(管理端可选)。
|
||||
/// </summary>
|
||||
public int? CustomerId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打印入库箱唛标签出参(多箱合并为一个多页 PDF)。
|
||||
/// </summary>
|
||||
public class WarehouseInboundPrintLabelsOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 可打印的 PDF 公网地址。
|
||||
/// </summary>
|
||||
public string PrintUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 预报单号。
|
||||
/// </summary>
|
||||
public string NoticeNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// PDF 页数(通常等于箱数)。
|
||||
/// </summary>
|
||||
public int PageCount { get; set; }
|
||||
}
|
||||
|
||||
public class WarehouseParcelSummaryOutput
|
||||
{
|
||||
public int InTransit { get; set; }
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
namespace Oms.Application;
|
||||
|
||||
|
||||
public static class FakeTables
|
||||
{
|
||||
public static string User => "fk_user";
|
||||
public static string Role => "fk_role";
|
||||
public static string Menu => "fk_menu";
|
||||
public static string Organization => "fk_organization";
|
||||
public static string UserRole => "fk_user_role";
|
||||
public static string RolePermission => "fk_role_permission";
|
||||
}
|
||||
Binary file not shown.
@ -1,5 +0,0 @@
|
||||
# 面单 PDF 中文字体
|
||||
|
||||
`OmsLabelCjk.ttf`:随程序嵌入,供 QuestPDF 渲染箱唛/SKU 中文(避免默认字体缺字形显示 `?`/`□`)。
|
||||
|
||||
部署镜像无需再依赖系统 Noto;仍可通过环境变量 `OMS_LABEL_FONT` 覆盖为其它 ttf/otf。
|
||||
@ -1,171 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using Oms.Domain.OrdersAggregate.Consts;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
|
||||
namespace Oms.Application.Integrations.Itfx;
|
||||
|
||||
/// <summary>
|
||||
/// 解析 itfx getFile 成功响应中的 Data,并回写订单单号。
|
||||
/// </summary>
|
||||
internal static class ItfxGetFileDataParser
|
||||
{
|
||||
public const string StandardCode = "getFile";
|
||||
|
||||
public const int TypeDownstreamLabel = 21;
|
||||
public const int TypeLastMileLabel = 31;
|
||||
public const int TypePod = 180;
|
||||
|
||||
public sealed class GetFileData
|
||||
{
|
||||
public string? ServiceNo { get; init; }
|
||||
|
||||
public string? TrackNo { get; init; }
|
||||
|
||||
public string? ClientNo { get; init; }
|
||||
|
||||
public string? Url { get; init; }
|
||||
|
||||
public int? Type { get; init; }
|
||||
}
|
||||
|
||||
public static bool TryResolveFileType(int type, out string fileType)
|
||||
{
|
||||
fileType = type switch
|
||||
{
|
||||
TypeDownstreamLabel => OrderFileTypeCodes.DownstreamLabel,
|
||||
TypeLastMileLabel => OrderFileTypeCodes.LastMileLabel,
|
||||
TypePod => OrderFileTypeCodes.Pod,
|
||||
_ => string.Empty
|
||||
};
|
||||
return !string.IsNullOrEmpty(fileType);
|
||||
}
|
||||
|
||||
public static bool TryParse(string? responseBody, out GetFileData? data)
|
||||
{
|
||||
data = null;
|
||||
if (string.IsNullOrWhiteSpace(responseBody))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(responseBody);
|
||||
var root = doc.RootElement;
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryGetPropertyIgnoreCase(root, out var dataElement, "data", "Data")
|
||||
|| dataElement.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
data = new GetFileData
|
||||
{
|
||||
ServiceNo = ReadString(dataElement, "serviceNo", "ServiceNo"),
|
||||
TrackNo = ReadString(dataElement, "trackNo", "TrackNo"),
|
||||
ClientNo = ReadString(dataElement, "clientNo", "ClientNo"),
|
||||
Url = ReadString(dataElement, "url", "Url"),
|
||||
Type = ReadInt(dataElement, "type", "Type"),
|
||||
};
|
||||
return !string.IsNullOrWhiteSpace(data.ServiceNo)
|
||||
|| !string.IsNullOrWhiteSpace(data.TrackNo)
|
||||
|| !string.IsNullOrWhiteSpace(data.ClientNo)
|
||||
|| !string.IsNullOrWhiteSpace(data.Url);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 服务商单号 ← Data.serviceNo,跟踪号 ← Data.trackNo;空值不覆盖。
|
||||
/// </summary>
|
||||
public static bool ApplyToOrder(Order order, GetFileData data)
|
||||
{
|
||||
var changed = false;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(data.ServiceNo))
|
||||
{
|
||||
var value = data.ServiceNo.Trim();
|
||||
if (!string.Equals(order.DownstreamServiceNo, value, StringComparison.Ordinal))
|
||||
{
|
||||
order.DownstreamServiceNo = value;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(data.TrackNo))
|
||||
{
|
||||
var value = data.TrackNo.Trim();
|
||||
if (!string.Equals(order.TrackNo, value, StringComparison.Ordinal))
|
||||
{
|
||||
order.TrackNo = value;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
private static bool TryGetPropertyIgnoreCase(JsonElement root, out JsonElement value, params string[] names)
|
||||
{
|
||||
foreach (var name in names)
|
||||
{
|
||||
if (root.TryGetProperty(name, out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var prop in root.EnumerateObject())
|
||||
{
|
||||
foreach (var name in names)
|
||||
{
|
||||
if (prop.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
value = prop.Value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string? ReadString(JsonElement root, params string[] names)
|
||||
{
|
||||
if (!TryGetPropertyIgnoreCase(root, out var prop, names))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return prop.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => prop.GetString()?.Trim(),
|
||||
JsonValueKind.Number => prop.GetRawText(),
|
||||
JsonValueKind.Null => null,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static int? ReadInt(JsonElement root, params string[] names)
|
||||
{
|
||||
if (!TryGetPropertyIgnoreCase(root, out var prop, names))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return prop.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Number when prop.TryGetInt32(out var n) => n,
|
||||
JsonValueKind.String when int.TryParse(prop.GetString(), out var n) => n,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -20,29 +20,6 @@ internal static class PlatformShopCrypto
|
||||
}
|
||||
|
||||
public static long UnixSeconds() => DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
|
||||
/// <summary>
|
||||
/// TikTok Shop 202309 签名,对齐 WMS <c>TiktokApi.makeTiktokSign</c>:
|
||||
/// 查询参数按 key 排序后拼接 key+value,有请求体则原样接在后面,两侧包 appSecret,HMAC-SHA256 十六进制。
|
||||
/// </summary>
|
||||
public static string TiktokSign(string path, IReadOnlyDictionary<string, string> query, string? body, string appSecret)
|
||||
{
|
||||
var input = new StringBuilder(path);
|
||||
foreach (var key in query.Keys
|
||||
.Where(k => k is not ("sign" or "access_token"))
|
||||
.OrderBy(k => k, StringComparer.Ordinal))
|
||||
{
|
||||
input.Append(key).Append(query[key]);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(body))
|
||||
{
|
||||
input.Append(body);
|
||||
}
|
||||
|
||||
var payload = appSecret + input + appSecret;
|
||||
return HmacSha256Hex(payload, appSecret);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,5 @@
|
||||
using Fake.EventBus.Distributed;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Oms.Application.Messaging.OrderSideEffects;
|
||||
using Oms.Application.Messaging.ParcelCreate;
|
||||
using Oms.Application.Services.Orders.Batch;
|
||||
@ -27,15 +25,8 @@ internal sealed class SmallBagTrackOrderNotifier(
|
||||
return;
|
||||
}
|
||||
|
||||
var eventBus = scope.ServiceProvider.GetRequiredService<IDistributedEventBus>();
|
||||
await eventBus.PublishAsync(new OrderParcelTrackUpsertIntegrationEvent
|
||||
{
|
||||
OrderId = order.Id,
|
||||
CustomerId = customerId,
|
||||
PayloadJson = JsonConvert.SerializeObject(request),
|
||||
AppendInitialTrackOnSuccess = request.AppendInitialTrackOnSuccess,
|
||||
Authorization = request.Authorization
|
||||
});
|
||||
var publisher = scope.ServiceProvider.GetRequiredService<IOrderParcelCreateEventPublisher>();
|
||||
await publisher.PublishTrackUpsertAsync(order, request, customerId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -84,19 +75,8 @@ internal sealed class SmallBagTrackOrderNotifier(
|
||||
try
|
||||
{
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
if (orderId <= 0 || string.IsNullOrWhiteSpace(orderNo) || string.IsNullOrWhiteSpace(trackCode))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var eventBus = scope.ServiceProvider.GetRequiredService<IDistributedEventBus>();
|
||||
await eventBus.PublishAsync(new OrderLifecycleTrackAppendIntegrationEvent
|
||||
{
|
||||
OrderId = orderId,
|
||||
OrderNo = orderNo.Trim(),
|
||||
TrackCode = trackCode.Trim(),
|
||||
Authorization = string.IsNullOrWhiteSpace(authorization) ? null : authorization.Trim()
|
||||
});
|
||||
var publisher = scope.ServiceProvider.GetRequiredService<IOrderSideEffectEventPublisher>();
|
||||
await publisher.PublishLifecycleTrackAppendAsync(orderId, orderNo, trackCode, authorization);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Oms.Application.Messaging.TrackChange;
|
||||
|
||||
namespace Oms.Application.Integrations.SmallBagTrack;
|
||||
|
||||
@ -9,16 +8,12 @@ public static class SmallBagTrackServiceExtensions
|
||||
public static IServiceCollection AddOmsSmallBagTrack(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.Configure<SmallBagTrackOptions>(configuration.GetSection(SmallBagTrackOptions.SectionName));
|
||||
services.Configure<TrackChangeSubscribeOptions>(
|
||||
configuration.GetSection(TrackChangeSubscribeOptions.SectionName));
|
||||
services.AddHttpClient(SmallBagTrackOrderClient.HttpClientName);
|
||||
services.AddScoped<ISmallBagTrackConfigProvider, SmallBagTrackConfigProvider>();
|
||||
services.AddScoped<ISmallBagTrackUpsertRequestBuilder, SmallBagTrackUpsertRequestBuilder>();
|
||||
services.AddScoped<ISmallBagTrackOrderClient, SmallBagTrackOrderClient>();
|
||||
services.AddScoped<IPickupBigBagTrackSyncer, PickupBigBagTrackSyncer>();
|
||||
services.AddSingleton<ISmallBagTrackOrderNotifier, SmallBagTrackOrderNotifier>();
|
||||
services.AddScoped<ITrackChangeOrderStatusApplier, TrackChangeOrderStatusApplier>();
|
||||
services.AddHostedService<TrackChangeSubscriberHostedService>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,18 +67,15 @@ internal sealed class SmallBagTrackUpsertRequestBuilder(
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 轨迹侧 TrackNo 必填。有真实跟踪号用跟踪号;尚未下发时先用客户单号占位,后续有 TrackNo 再 Upsert 更新。
|
||||
/// </summary>
|
||||
internal static string ResolveTrackNo(Order order)
|
||||
{
|
||||
var trackNo = order.TrackNo?.Trim() ?? string.Empty;
|
||||
var trackNo = order.TrackNo?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(trackNo))
|
||||
{
|
||||
return trackNo;
|
||||
}
|
||||
|
||||
return order.ClientNo?.Trim() ?? string.Empty;
|
||||
return order.ServiceNo?.Trim() ?? string.Empty;
|
||||
}
|
||||
|
||||
internal static string? PickSupplierCodeFromProviders(
|
||||
|
||||
@ -63,10 +63,6 @@ public sealed class HiprintElementOptions
|
||||
[JsonPropertyName("title")]
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>设计器「测试数据」;无 field 的纯文本有时只把文案写在这里。</summary>
|
||||
[JsonPropertyName("testData")]
|
||||
public string? TestData { get; set; }
|
||||
|
||||
[JsonPropertyName("field")]
|
||||
public string? Field { get; set; }
|
||||
|
||||
|
||||
@ -105,15 +105,14 @@ public sealed class HiprintTemplateParser
|
||||
Src = ResolveValue(opt.Field, opt.Src ?? opt.Title, printData)
|
||||
};
|
||||
case "longtext":
|
||||
return BuildTextOp(
|
||||
opt, left, top, width, height, printData, wrap: true, element.PrintElementType?.Title);
|
||||
return BuildTextOp(opt, left, top, width, height, printData, wrap: true);
|
||||
case "text":
|
||||
default:
|
||||
{
|
||||
var textType = opt.TextType?.Trim().ToLowerInvariant();
|
||||
if (textType is "barcode" or "qrcode")
|
||||
{
|
||||
var content = ResolveValue(opt.Field, FirstNonEmpty(opt.Title, opt.TestData), printData);
|
||||
var content = ResolveValue(opt.Field, opt.Title, printData);
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
return null;
|
||||
@ -130,8 +129,7 @@ public sealed class HiprintTemplateParser
|
||||
};
|
||||
}
|
||||
|
||||
return BuildTextOp(
|
||||
opt, left, top, width, height, printData, wrap: false, element.PrintElementType?.Title);
|
||||
return BuildTextOp(opt, left, top, width, height, printData, wrap: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -143,38 +141,25 @@ public sealed class HiprintTemplateParser
|
||||
float width,
|
||||
float height,
|
||||
IReadOnlyDictionary<string, object?> printData,
|
||||
bool wrap,
|
||||
string? elementTypeTitle = null)
|
||||
bool wrap)
|
||||
{
|
||||
var value = ResolveValue(opt.Field, null, printData);
|
||||
// 与 vg-print 一致:无 field 时打印 title;有 field 时按 hideTitle 决定是否带标题
|
||||
var title = FirstNonEmpty(opt.Title, elementTypeTitle);
|
||||
string text;
|
||||
if (!string.IsNullOrWhiteSpace(opt.Field))
|
||||
{
|
||||
var hideTitle = opt.HideTitle != false; // null/true → 隐藏标题(OMS 字段默认 hideTitle:true)
|
||||
if (!hideTitle && !string.IsNullOrWhiteSpace(title) && !string.IsNullOrWhiteSpace(value))
|
||||
// 有 field:默认只打字段值;hideTitle=false 且有 title 时拼「标题:值」
|
||||
if (opt.HideTitle == false && !string.IsNullOrWhiteSpace(opt.Title) && !string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
text = $"{title}:{value}";
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
text = value!;
|
||||
}
|
||||
else if (!hideTitle && !string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
// 仅标题(值为空时仍保留自定义标题文案)
|
||||
text = title!;
|
||||
text = $"{opt.Title}:{value}";
|
||||
}
|
||||
else
|
||||
{
|
||||
text = string.Empty;
|
||||
text = value ?? string.Empty;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 纯自定义文本:title → testData → 元素类型名
|
||||
text = FirstNonEmpty(opt.Title, opt.TestData, elementTypeTitle) ?? string.Empty;
|
||||
text = opt.Title ?? string.Empty;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
@ -184,18 +169,15 @@ public sealed class HiprintTemplateParser
|
||||
|
||||
var bold = string.Equals(opt.FontWeight, "bold", StringComparison.OrdinalIgnoreCase)
|
||||
|| opt.FontWeight is "700" or "600";
|
||||
var fontSize = opt.FontSize is > 0 ? opt.FontSize.Value : 10f;
|
||||
// 设计器文本框常只有十余 pt 高,行高略大于字号时会被裁成「空白」
|
||||
var boxHeight = Math.Max(height, fontSize * 1.4f);
|
||||
|
||||
return new LabelTextOp
|
||||
{
|
||||
LeftPt = left,
|
||||
TopPt = top,
|
||||
WidthPt = width,
|
||||
HeightPt = boxHeight,
|
||||
HeightPt = height,
|
||||
Text = text,
|
||||
FontSizePt = fontSize,
|
||||
FontSizePt = opt.FontSize is > 0 ? opt.FontSize.Value : 10f,
|
||||
Bold = bold,
|
||||
ColorHex = opt.Color,
|
||||
Align = (opt.TextAlign ?? "left").ToLowerInvariant(),
|
||||
@ -203,9 +185,6 @@ public sealed class HiprintTemplateParser
|
||||
};
|
||||
}
|
||||
|
||||
private static string? FirstNonEmpty(params string?[] values) =>
|
||||
values.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x));
|
||||
|
||||
private static string? ResolveValue(
|
||||
string? field,
|
||||
string? fallback,
|
||||
|
||||
@ -1,120 +0,0 @@
|
||||
using System.Reflection;
|
||||
using QuestPDF.Drawing;
|
||||
|
||||
namespace Oms.Application.Labeling.Hiprint;
|
||||
|
||||
/// <summary>
|
||||
/// 面单 PDF 中文字体:QuestPDF 默认字体无 CJK;优先嵌入字体,避免依赖容器内 TTC。
|
||||
/// </summary>
|
||||
public static class LabelPdfFontRegistrar
|
||||
{
|
||||
public const string FamilyName = "OmsLabelSans";
|
||||
|
||||
private const string EmbeddedName = "Oms.Application.Fonts.OmsLabelCjk.ttf";
|
||||
|
||||
private static int _state; // 0=未试 1=成功 2=失败
|
||||
private static string? _source;
|
||||
private static string? _lastError;
|
||||
|
||||
/// <summary>
|
||||
/// 确保已注册可用中文字体;成功返回 <see cref="FamilyName"/>,否则 null。
|
||||
/// </summary>
|
||||
public static string? EnsureRegistered()
|
||||
{
|
||||
var state = Volatile.Read(ref _state);
|
||||
if (state == 1)
|
||||
{
|
||||
return FamilyName;
|
||||
}
|
||||
|
||||
if (state == 2)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var attempt in EnumerateAttempts())
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = attempt.Open();
|
||||
if (stream == null || stream.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 读入内存:Register 后不可依赖外部流生命周期;并规避部分 TTC 流问题
|
||||
using var ms = new MemoryStream();
|
||||
stream.CopyTo(ms);
|
||||
ms.Position = 0;
|
||||
FontManager.RegisterFontWithCustomName(FamilyName, ms);
|
||||
_source = attempt.Description;
|
||||
Interlocked.Exchange(ref _state, 1);
|
||||
return FamilyName;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_lastError = $"{attempt.Description}: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref _state, 2);
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string? RegisteredSource => _source;
|
||||
|
||||
public static string? LastError => _lastError;
|
||||
|
||||
private static IEnumerable<FontAttempt> EnumerateAttempts()
|
||||
{
|
||||
yield return new FontAttempt(
|
||||
"embedded:" + EmbeddedName,
|
||||
() => Assembly.GetExecutingAssembly().GetManifestResourceStream(EmbeddedName));
|
||||
|
||||
var fromEnv = Environment.GetEnvironmentVariable("OMS_LABEL_FONT");
|
||||
if (!string.IsNullOrWhiteSpace(fromEnv))
|
||||
{
|
||||
var path = fromEnv.Trim();
|
||||
yield return new FontAttempt("env:" + path, () => File.Exists(path) ? File.OpenRead(path) : null);
|
||||
}
|
||||
|
||||
var baseDir = AppContext.BaseDirectory;
|
||||
foreach (var name in new[]
|
||||
{
|
||||
"OmsLabelCjk.ttf",
|
||||
"NotoSansSC-Regular.otf",
|
||||
"NotoSansSC-Regular.ttf",
|
||||
"NotoSansCJKsc-Regular.otf",
|
||||
"simhei.ttf"
|
||||
})
|
||||
{
|
||||
var path = Path.Combine(baseDir, "Fonts", name);
|
||||
yield return new FontAttempt("file:" + path, () => File.Exists(path) ? File.OpenRead(path) : null);
|
||||
}
|
||||
|
||||
var winFonts = Environment.GetFolderPath(Environment.SpecialFolder.Fonts);
|
||||
if (!string.IsNullOrWhiteSpace(winFonts))
|
||||
{
|
||||
// 单文件 TTF 优先于 TTC(集合字体在部分 Skia 版本上注册失败/缺简体)
|
||||
foreach (var name in new[] { "simhei.ttf", "msyh.ttc", "msyhbd.ttc", "simsun.ttc" })
|
||||
{
|
||||
var path = Path.Combine(winFonts, name);
|
||||
yield return new FontAttempt("win:" + path, () => File.Exists(path) ? File.OpenRead(path) : null);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var path in new[]
|
||||
{
|
||||
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
|
||||
"/usr/share/fonts/truetype/arphic/uming.ttc",
|
||||
"/usr/share/fonts/opentype/noto/NotoSansCJKsc-Regular.otf",
|
||||
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
||||
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc"
|
||||
})
|
||||
{
|
||||
yield return new FontAttempt("linux:" + path, () => File.Exists(path) ? File.OpenRead(path) : null);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct FontAttempt(string Description, Func<Stream?> Open);
|
||||
}
|
||||
@ -14,62 +14,37 @@ public sealed class QuestPdfLabelRenderer(HiprintTemplateParser parser) : ILabel
|
||||
public Task<byte[]> RenderAsync(
|
||||
string templateJson,
|
||||
IReadOnlyDictionary<string, object?> printData,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
RenderManyAsync(templateJson, [printData], cancellationToken);
|
||||
|
||||
public Task<byte[]> RenderManyAsync(
|
||||
string templateJson,
|
||||
IReadOnlyList<IReadOnlyDictionary<string, object?>> printDataPages,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
EnsureLicense();
|
||||
var fontFamily = LabelPdfFontRegistrar.EnsureRegistered()
|
||||
?? throw new InvalidOperationException(
|
||||
"面单中文字体未就绪:" + (LabelPdfFontRegistrar.LastError ?? "未找到可用 CJK 字体"));
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (printDataPages == null || printDataPages.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("printDataPages 不能为空", nameof(printDataPages));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(templateJson))
|
||||
{
|
||||
throw new ArgumentException("templateJson 不能为空", nameof(templateJson));
|
||||
}
|
||||
|
||||
var bytes = Document.Create(container =>
|
||||
{
|
||||
for (var i = 0; i < printDataPages.Count; i++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var pageIr = parser.Parse(templateJson, printDataPages[i]);
|
||||
var pageIr = parser.Parse(templateJson, printData);
|
||||
var pageWidthPt = MmToPt(pageIr.WidthMm);
|
||||
var pageHeightPt = MmToPt(pageIr.HeightMm);
|
||||
|
||||
var bytes = Document.Create(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Size(pageWidthPt, pageHeightPt);
|
||||
page.Margin(0);
|
||||
// 页级默认中文字体;单元素再设字号/颜色。勿对嵌入 Regular 调 Bold(会回退缺字西文字体 → ???)
|
||||
page.DefaultTextStyle(x => x.FontFamily(fontFamily).FontColor(Colors.Black));
|
||||
page.Content().Layers(layers =>
|
||||
{
|
||||
layers.PrimaryLayer().Background(Colors.White);
|
||||
|
||||
foreach (var op in pageIr.Ops)
|
||||
{
|
||||
DrawOp(layers, op, fontFamily);
|
||||
DrawOp(layers, op);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}).GeneratePdf();
|
||||
|
||||
return Task.FromResult(bytes);
|
||||
}
|
||||
|
||||
private static void DrawOp(LayersDescriptor layers, LabelDrawOp op, string fontFamily)
|
||||
private static void DrawOp(LayersDescriptor layers, LabelDrawOp op)
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
@ -89,16 +64,16 @@ public sealed class QuestPdfLabelRenderer(HiprintTemplateParser parser) : ILabel
|
||||
_ => box.AlignLeft()
|
||||
};
|
||||
|
||||
aligned.AlignTop().Text(t =>
|
||||
var span = aligned
|
||||
.AlignTop()
|
||||
.Text(text.Text)
|
||||
.FontSize(text.FontSizePt)
|
||||
.FontColor(ParseColor(text.ColorHex));
|
||||
|
||||
if (text.Bold)
|
||||
{
|
||||
// 勿对嵌入 Regular 调用 Bold():会回退缺字西文字体,中文变成 ???
|
||||
var size = text.Bold ? text.FontSizePt * 1.02f : text.FontSizePt;
|
||||
t.DefaultTextStyle(style => style
|
||||
.FontFamily(fontFamily)
|
||||
.FontSize(size)
|
||||
.FontColor(ParseColor(text.ColorHex)));
|
||||
t.Span(text.Text ?? string.Empty);
|
||||
});
|
||||
span.Bold();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
@ -165,8 +140,6 @@ public sealed class QuestPdfLabelRenderer(HiprintTemplateParser parser) : ILabel
|
||||
}
|
||||
|
||||
QuestPDF.Settings.License = LicenseType.Community;
|
||||
// 缺字形时不抛,但仍应依赖嵌入 CJK 字体
|
||||
QuestPDF.Settings.CheckIfAllTextGlyphsAreAvailable = false;
|
||||
}
|
||||
|
||||
private static float MmToPt(float mm) => mm * 72f / 25.4f;
|
||||
|
||||
@ -5,19 +5,8 @@ namespace Oms.Application.Labeling;
|
||||
/// </summary>
|
||||
public interface ILabelPdfRenderer
|
||||
{
|
||||
/// <summary>
|
||||
/// 渲染单页 PDF。
|
||||
/// </summary>
|
||||
Task<byte[]> RenderAsync(
|
||||
string templateJson,
|
||||
IReadOnlyDictionary<string, object?> printData,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 同一模板按多组 PrintData 渲染多页,合并为单个 PDF。
|
||||
/// </summary>
|
||||
Task<byte[]> RenderManyAsync(
|
||||
string templateJson,
|
||||
IReadOnlyList<IReadOnlyDictionary<string, object?>> printDataPages,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@ -1,159 +0,0 @@
|
||||
using System.Globalization;
|
||||
using Oms.Domain.WarehouseAggregate.Entities;
|
||||
|
||||
namespace Oms.Application.Labeling;
|
||||
|
||||
/// <summary>
|
||||
/// 入库箱唛 PrintData(camelCase,与 hiprint 字段对齐;箱唛为主、SKU 为辅)。
|
||||
/// </summary>
|
||||
public static class InboundBoxLabelPrintDataBuilder
|
||||
{
|
||||
public static IReadOnlyDictionary<string, object?> Build(
|
||||
CInboundNotice notice,
|
||||
CInboundBox box,
|
||||
IReadOnlyList<CInboundBoxLine> lines,
|
||||
IReadOnlyDictionary<int, CSku> skuMap,
|
||||
string? warehouseName = null,
|
||||
string? customerName = null,
|
||||
string? customerCode = null,
|
||||
string? operatorName = null,
|
||||
DateTime? printTime = null,
|
||||
int pageIndex = 1,
|
||||
int pageCount = 1)
|
||||
{
|
||||
var barcode = ResolveBarcode(notice.NoticeNo, box);
|
||||
var skuLines = lines
|
||||
.OrderBy(x => x.Id)
|
||||
.Select(line =>
|
||||
{
|
||||
skuMap.TryGetValue(line.SkuId, out var sku);
|
||||
var skuCode = FirstNonEmpty(line.SkuCode, sku?.SkuCode) ?? string.Empty;
|
||||
var skuName = sku?.SkuName ?? string.Empty;
|
||||
return new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["skuCode"] = skuCode,
|
||||
["skuName"] = skuName,
|
||||
["qty"] = line.PlannedQty
|
||||
};
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var first = skuLines.FirstOrDefault();
|
||||
var skuSummary = string.Join(
|
||||
"\n",
|
||||
skuLines.Select(x =>
|
||||
{
|
||||
var code = Convert.ToString(x.GetValueOrDefault("skuCode")) ?? string.Empty;
|
||||
var name = Convert.ToString(x.GetValueOrDefault("skuName")) ?? string.Empty;
|
||||
var qty = Convert.ToString(x.GetValueOrDefault("qty")) ?? "0";
|
||||
return string.IsNullOrWhiteSpace(name)
|
||||
? $"{code}×{qty}"
|
||||
: $"{code} {name}×{qty}";
|
||||
}));
|
||||
|
||||
return new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["noticeNo"] = notice.NoticeNo,
|
||||
["markReference"] = notice.MarkReference,
|
||||
["inboundType"] = notice.InboundType,
|
||||
["receiptStatus"] = notice.ReceiptStatus,
|
||||
["remark"] = notice.Remark,
|
||||
|
||||
["boxId"] = box.Id,
|
||||
["boxSeqNo"] = box.BoxSeqNo,
|
||||
["boxCount"] = pageCount,
|
||||
["markBoxNo"] = box.MarkBoxNo,
|
||||
["barcode"] = barcode,
|
||||
|
||||
["warehouseName"] = warehouseName,
|
||||
["customerCode"] = customerCode,
|
||||
["customerName"] = customerName,
|
||||
|
||||
["weightG"] = box.WeightG,
|
||||
["lengthCm"] = box.LengthCm,
|
||||
["widthCm"] = box.WidthCm,
|
||||
["heightCm"] = box.HeightCm,
|
||||
["weightUnit"] = "G",
|
||||
["dimUnit"] = "CM",
|
||||
|
||||
["skuSummary"] = skuSummary,
|
||||
["skuCode"] = first?.GetValueOrDefault("skuCode"),
|
||||
["skuName"] = first?.GetValueOrDefault("skuName"),
|
||||
["skuQty"] = first?.GetValueOrDefault("qty"),
|
||||
["items"] = skuLines,
|
||||
|
||||
["pageIndex"] = pageIndex,
|
||||
["pageCount"] = pageCount,
|
||||
["boxIndexText"] = $"{pageIndex}/{pageCount}",
|
||||
|
||||
["printTime"] = (printTime ?? DateTime.Now).ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
|
||||
["operatorName"] = operatorName
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设计预览用 Mock PrintData。
|
||||
/// </summary>
|
||||
public static IReadOnlyDictionary<string, object?> BuildMock() =>
|
||||
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["noticeNo"] = "IN202610220001",
|
||||
["markReference"] = "SF1234567890",
|
||||
["inboundType"] = "normal",
|
||||
["receiptStatus"] = "forecasted",
|
||||
["remark"] = null,
|
||||
["boxId"] = 1,
|
||||
["boxSeqNo"] = "1",
|
||||
["boxCount"] = 2,
|
||||
["markBoxNo"] = "BOX-001",
|
||||
["barcode"] = "BOX-001",
|
||||
["warehouseName"] = "SZ-01",
|
||||
["customerCode"] = "AF0A1",
|
||||
["customerName"] = "Demo Customer",
|
||||
["weightG"] = 1200m,
|
||||
["lengthCm"] = 30m,
|
||||
["widthCm"] = 20m,
|
||||
["heightCm"] = 15m,
|
||||
["weightUnit"] = "G",
|
||||
["dimUnit"] = "CM",
|
||||
["skuSummary"] = "SKU001 T-Shirt×2\nSKU002 Pants×1",
|
||||
["skuCode"] = "SKU001",
|
||||
["skuName"] = "T-Shirt",
|
||||
["skuQty"] = 2,
|
||||
["items"] = new List<Dictionary<string, object?>>
|
||||
{
|
||||
new()
|
||||
{
|
||||
["skuCode"] = "SKU001",
|
||||
["skuName"] = "T-Shirt",
|
||||
["qty"] = 2
|
||||
},
|
||||
new()
|
||||
{
|
||||
["skuCode"] = "SKU002",
|
||||
["skuName"] = "Pants",
|
||||
["qty"] = 1
|
||||
}
|
||||
},
|
||||
["pageIndex"] = 1,
|
||||
["pageCount"] = 2,
|
||||
["boxIndexText"] = "1/2",
|
||||
["printTime"] = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
|
||||
["operatorName"] = "preview"
|
||||
};
|
||||
|
||||
public static string ResolveBarcode(string noticeNo, CInboundBox box)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(box.MarkBoxNo))
|
||||
{
|
||||
return box.MarkBoxNo.Trim();
|
||||
}
|
||||
|
||||
var seq = string.IsNullOrWhiteSpace(box.BoxSeqNo) ? "1" : box.BoxSeqNo.Trim();
|
||||
var no = string.IsNullOrWhiteSpace(noticeNo) ? "IN" : noticeNo.Trim();
|
||||
return $"{no}-{seq}";
|
||||
}
|
||||
|
||||
private static string? FirstNonEmpty(params string?[] values) =>
|
||||
values.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x));
|
||||
}
|
||||
@ -76,60 +76,13 @@ public static class LabelFieldDictionary
|
||||
]
|
||||
},
|
||||
new LabelFieldGroup
|
||||
{
|
||||
Code = "inboundBox",
|
||||
Title = "入库箱唛",
|
||||
Fields =
|
||||
[
|
||||
Field("noticeNo", "预报单号"),
|
||||
Field("markReference", "唛头参考"),
|
||||
Field("markBoxNo", "箱唛号"),
|
||||
Field("boxSeqNo", "箱序号"),
|
||||
Field("boxCount", "箱子总数"),
|
||||
Field("barcode", "条码内容"),
|
||||
Field("boxIndexText", "箱序页码"),
|
||||
Field("skuSummary", "SKU 摘要"),
|
||||
Field("skuCode", "首 SKU 编码"),
|
||||
Field("skuName", "首 SKU 名称"),
|
||||
Field("skuQty", "首 SKU 数量"),
|
||||
Field("weightG", "箱重量(g)"),
|
||||
Field("lengthCm", "箱长(cm)"),
|
||||
Field("widthCm", "箱宽(cm)"),
|
||||
Field("heightCm", "箱高(cm)")
|
||||
]
|
||||
},
|
||||
new LabelFieldGroup
|
||||
{
|
||||
Code = "warehouseSku",
|
||||
Title = "库内 SKU",
|
||||
Fields =
|
||||
[
|
||||
Field("skuCode", "SKU 编码"),
|
||||
Field("skuName", "SKU 名称"),
|
||||
Field("skuNameEn", "SKU 英文名"),
|
||||
Field("barcode", "条码内容"),
|
||||
Field("unitPrice", "单价"),
|
||||
Field("exHsCode", "出口海关编码"),
|
||||
Field("imHsCode", "进口海关编码"),
|
||||
Field("declareElement", "申报要素"),
|
||||
Field("weightG", "重量(g)"),
|
||||
Field("lengthCm", "长(cm)"),
|
||||
Field("widthCm", "宽(cm)"),
|
||||
Field("heightCm", "高(cm)"),
|
||||
Field("dimensions", "尺寸摘要"),
|
||||
Field("remark", "备注")
|
||||
]
|
||||
},
|
||||
new LabelFieldGroup
|
||||
{
|
||||
Code = "system",
|
||||
Title = "系统",
|
||||
Fields =
|
||||
[
|
||||
Field("printTime", "打印时间"),
|
||||
Field("operatorName", "操作人"),
|
||||
Field("pageIndex", "页码"),
|
||||
Field("pageCount", "总页数")
|
||||
Field("operatorName", "操作人")
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@ -53,7 +53,8 @@ public sealed class LabelPdfGenerationService(
|
||||
|
||||
var relativeUrl = await SavePdfAsync(order.ServiceNo, pdfBytes, cancellationToken);
|
||||
order.LabelTemplateId = templateId;
|
||||
await orderRepository.UpdateLabelTemplateIdAsync(order.Id, templateId.Value, cancellationToken);
|
||||
order.UpdateTime = DateTime.Now;
|
||||
await orderRepository.UpdateAsync(order);
|
||||
|
||||
await orderFileManager.ReplaceSystemLabelAsync(
|
||||
order.Id,
|
||||
|
||||
@ -1,94 +0,0 @@
|
||||
using System.Globalization;
|
||||
using Oms.Domain.WarehouseAggregate.Entities;
|
||||
|
||||
namespace Oms.Application.Labeling;
|
||||
|
||||
/// <summary>
|
||||
/// 库内 SKU 标签 PrintData(camelCase,与 hiprint 字段对齐)。
|
||||
/// </summary>
|
||||
public static class SkuLabelPrintDataBuilder
|
||||
{
|
||||
public static IReadOnlyDictionary<string, object?> Build(
|
||||
CSku sku,
|
||||
string? customerName = null,
|
||||
string? customerCode = null,
|
||||
string? warehouseName = null,
|
||||
string? operatorName = null,
|
||||
DateTime? printTime = null)
|
||||
{
|
||||
var barcode = string.IsNullOrWhiteSpace(sku.SkuCode) ? $"SKU-{sku.Id}" : sku.SkuCode.Trim();
|
||||
return new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["skuId"] = sku.Id,
|
||||
["skuCode"] = sku.SkuCode,
|
||||
["skuName"] = sku.SkuName,
|
||||
["skuNameEn"] = sku.SkuNameEn,
|
||||
["barcode"] = barcode,
|
||||
["skuImageUrl"] = sku.SkuImageUrl,
|
||||
["unitPrice"] = sku.UnitPrice,
|
||||
["exHsCode"] = sku.ExHsCode,
|
||||
["imHsCode"] = sku.ImHsCode,
|
||||
["declareElement"] = sku.DeclareElement,
|
||||
["productId"] = sku.ProductId,
|
||||
["nonStdProductId"] = sku.NonStdProductId,
|
||||
["stdProductId"] = sku.StdProductId,
|
||||
["weightG"] = sku.WeightG,
|
||||
["lengthCm"] = sku.LengthCm,
|
||||
["widthCm"] = sku.WidthCm,
|
||||
["heightCm"] = sku.HeightCm,
|
||||
["dimensions"] = FormatDimensions(sku.LengthCm, sku.WidthCm, sku.HeightCm),
|
||||
["remark"] = sku.Remark,
|
||||
["weightUnit"] = "G",
|
||||
["dimUnit"] = "CM",
|
||||
["customerCode"] = customerCode,
|
||||
["customerName"] = customerName,
|
||||
["warehouseName"] = warehouseName,
|
||||
["printTime"] = (printTime ?? DateTime.Now).ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
|
||||
["operatorName"] = operatorName
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设计预览用 Mock PrintData。
|
||||
/// </summary>
|
||||
public static IReadOnlyDictionary<string, object?> BuildMock() =>
|
||||
new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["skuId"] = 1,
|
||||
["skuCode"] = "SKU001",
|
||||
["skuName"] = "T-Shirt",
|
||||
["skuNameEn"] = "T-Shirt",
|
||||
["barcode"] = "SKU001",
|
||||
["skuImageUrl"] = null,
|
||||
["unitPrice"] = 9.9m,
|
||||
["exHsCode"] = "610910",
|
||||
["imHsCode"] = "610910",
|
||||
["declareElement"] = "棉质 T 恤",
|
||||
["productId"] = null,
|
||||
["nonStdProductId"] = null,
|
||||
["stdProductId"] = "06901234567890",
|
||||
["weightG"] = 200m,
|
||||
["lengthCm"] = 30m,
|
||||
["widthCm"] = 20m,
|
||||
["heightCm"] = 2m,
|
||||
["dimensions"] = "30×20×2",
|
||||
["remark"] = null,
|
||||
["weightUnit"] = "G",
|
||||
["dimUnit"] = "CM",
|
||||
["customerCode"] = "AF0A1",
|
||||
["customerName"] = "Demo Customer",
|
||||
["warehouseName"] = "SZ-01",
|
||||
["printTime"] = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
|
||||
["operatorName"] = "preview"
|
||||
};
|
||||
|
||||
private static string? FormatDimensions(decimal? length, decimal? width, decimal? height)
|
||||
{
|
||||
if (length is null && width is null && height is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return $"{length ?? 0}×{width ?? 0}×{height ?? 0}";
|
||||
}
|
||||
}
|
||||
@ -40,16 +40,20 @@ public static class McpToolCatalog
|
||||
new(
|
||||
ChannelGuide,
|
||||
"查渠道对客说明",
|
||||
"客户问某品名能不能寄、走什么渠道时调用。必填 cargoName;productCode/productName 可选(已知渠道时传入)。仅品名时按对客说明检索可走渠道。",
|
||||
"按销售产品代码或名称查询对客说明,判断客户品名是否可承运(禁运/可寄)。",
|
||||
"""
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"productCode": { "type": "string", "description": "销售产品代码,可选;客户已指定渠道时优先" },
|
||||
"productName": { "type": "string", "description": "销售产品/渠道名称关键词,可选" },
|
||||
"cargoName": { "type": "string", "description": "客户品名,如 lipstick、化妆品;必填" }
|
||||
"productCode": { "type": "string", "description": "销售产品代码,优先使用" },
|
||||
"productName": { "type": "string", "description": "销售产品名称关键词(无代码时用)" },
|
||||
"cargoName": { "type": "string", "description": "客户品名,用于对照说明判断可否承运" }
|
||||
},
|
||||
"required": ["cargoName"]
|
||||
"required": ["cargoName"],
|
||||
"anyOf": [
|
||||
{ "required": ["productCode"] },
|
||||
{ "required": ["productName"] }
|
||||
]
|
||||
}
|
||||
""",
|
||||
20),
|
||||
|
||||
@ -153,13 +153,6 @@ public class McpToolExecutor(
|
||||
var productName = GetString(root, "productName", "ProductName");
|
||||
var cargoName = GetString(root, "cargoName", "CargoName");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cargoName)
|
||||
&& string.IsNullOrWhiteSpace(productCode)
|
||||
&& string.IsNullOrWhiteSpace(productName))
|
||||
{
|
||||
return (false, "请至少提供 cargoName(客户品名),或 productCode / productName");
|
||||
}
|
||||
|
||||
var products = new List<Oms.Domain.ChannelsAggregate.Entities.SalesProduct>();
|
||||
if (!string.IsNullOrWhiteSpace(productCode))
|
||||
{
|
||||
@ -173,51 +166,14 @@ public class McpToolExecutor(
|
||||
var list = await salesProductRepository.GetListAsync(
|
||||
x => x.IsActive == EnableStatus.Enabled
|
||||
&& (x.ProductName.Contains(keyword) || x.ProductCode.Contains(keyword)));
|
||||
products.AddRange(list.Take(8));
|
||||
}
|
||||
|
||||
// 仅品名:在已启用且配置了对客说明的产品中检索可走渠道
|
||||
if (products.Count == 0 && !string.IsNullOrWhiteSpace(cargoName))
|
||||
{
|
||||
var keyword = cargoName.Trim();
|
||||
var withGuide = await salesProductRepository.GetListAsync(
|
||||
x => x.IsActive == EnableStatus.Enabled
|
||||
&& x.CustomerGuideMarkdown != null
|
||||
&& x.CustomerGuideMarkdown != "");
|
||||
|
||||
static bool ContainsIgnoreCase(string? haystack, string needle) =>
|
||||
!string.IsNullOrEmpty(haystack)
|
||||
&& haystack.Contains(needle, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var matched = withGuide
|
||||
.Where(p =>
|
||||
ContainsIgnoreCase(p.CustomerGuideMarkdown, keyword)
|
||||
|| ContainsIgnoreCase(p.ProductName, keyword)
|
||||
|| ContainsIgnoreCase(p.ProductCode, keyword))
|
||||
.Take(10)
|
||||
.ToList();
|
||||
|
||||
if (matched.Count > 0)
|
||||
{
|
||||
products.AddRange(matched);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (true, JsonSerializer.Serialize(new
|
||||
{
|
||||
items = Array.Empty<object>(),
|
||||
cargoName = keyword,
|
||||
hint = "未在已配置对客说明的渠道中命中该品名。请换关键词,或补充销售产品代码/名称后再查;不要擅自判定可寄。"
|
||||
}, JsonOptions));
|
||||
}
|
||||
products.AddRange(list.Take(5));
|
||||
}
|
||||
|
||||
if (products.Count == 0)
|
||||
{
|
||||
return (false, "未找到销售产品,请提供 productCode、productName 或 cargoName");
|
||||
return (false, "未找到销售产品,请提供 productCode 或 productName");
|
||||
}
|
||||
|
||||
var cargo = string.IsNullOrWhiteSpace(cargoName) ? null : cargoName.Trim();
|
||||
var items = products.Select(p =>
|
||||
{
|
||||
var guide = p.CustomerGuideMarkdown?.Trim() ?? string.Empty;
|
||||
@ -228,19 +184,14 @@ public class McpToolExecutor(
|
||||
productName = p.ProductName,
|
||||
hasGuide = !string.IsNullOrWhiteSpace(guide),
|
||||
customerGuideMarkdown = string.IsNullOrWhiteSpace(guide) ? null : guide,
|
||||
cargoNameToCheck = cargo,
|
||||
cargoNameToCheck = string.IsNullOrWhiteSpace(cargoName) ? null : cargoName.Trim(),
|
||||
hint = string.IsNullOrWhiteSpace(guide)
|
||||
? "该产品尚未配置对客说明,无法据此判断品名可否承运。"
|
||||
: "请根据对客说明判断 cargoName 是否可寄、是否适合该渠道;说明未提及的品名不要擅自判定可寄。"
|
||||
: "请根据对客说明判断 cargoName 是否可寄;说明未提及的品名不要擅自判定可寄。"
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
return (true, JsonSerializer.Serialize(new
|
||||
{
|
||||
items,
|
||||
cargoName = cargo,
|
||||
hint = "用一两句中文告诉客户:哪些渠道对客说明支持该品名、哪些不支持;可列出产品代码/名称。"
|
||||
}, JsonOptions));
|
||||
return (true, JsonSerializer.Serialize(new { items }, JsonOptions));
|
||||
}
|
||||
|
||||
private async Task<(bool, string)> TrackQueryAsync(string argumentsJson, CancellationToken cancellationToken)
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
namespace Oms.Application.Messaging.Agents;
|
||||
|
||||
/// <summary>代理返佣异步任务动作。</summary>
|
||||
public static class AgentCommissionJobActionCodes
|
||||
{
|
||||
/// <summary>下单/计费后写或刷新预计返佣。</summary>
|
||||
public const string Estimate = "estimate";
|
||||
|
||||
/// <summary>派送终态锁定为待结算。</summary>
|
||||
public const string Finalize = "finalize";
|
||||
|
||||
/// <summary>删单/全额退作废。</summary>
|
||||
public const string Void = "void";
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
using Fake.DependencyInjection;
|
||||
using Fake.EventBus;
|
||||
using Fake.UnitOfWork;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Oms.Application.Shared;
|
||||
|
||||
namespace Oms.Application.Messaging.Agents;
|
||||
|
||||
/// <summary>消费代理返佣异步任务。</summary>
|
||||
public sealed class AgentCommissionJobEventHandler(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IUnitOfWorkManager unitOfWorkManager,
|
||||
ILogger<AgentCommissionJobEventHandler> logger)
|
||||
: IEventHandler<AgentCommissionJobIntegrationEvent>, ITransientDependency
|
||||
{
|
||||
public async Task HandleAsync(
|
||||
AgentCommissionJobIntegrationEvent @event,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (@event.OrderId <= 0 || string.IsNullOrWhiteSpace(@event.Action))
|
||||
{
|
||||
logger.LogWarning(
|
||||
"AgentCommission job skipped invalid payload OrderId={OrderId} Action={Action}",
|
||||
@event.OrderId,
|
||||
@event.Action);
|
||||
return;
|
||||
}
|
||||
|
||||
using var unitOfWork = unitOfWorkManager.Begin(OmsOutboxUnitOfWork.Transactional());
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var manager = scope.ServiceProvider.GetRequiredService<AgentCommissionManager>();
|
||||
|
||||
try
|
||||
{
|
||||
var action = @event.Action.Trim();
|
||||
if (string.Equals(action, AgentCommissionJobActionCodes.Estimate, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await manager.TryEstimateOnOrderCreatedAsync(@event.OrderId, cancellationToken);
|
||||
}
|
||||
else if (string.Equals(action, AgentCommissionJobActionCodes.Finalize, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await manager.TryAccrueOnDeliveryTerminalAsync(
|
||||
@event.OrderId,
|
||||
@event.TriggerOrderStatus ?? string.Empty,
|
||||
cancellationToken);
|
||||
}
|
||||
else if (string.Equals(action, AgentCommissionJobActionCodes.Void, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await manager.TryVoidOnOrderCancelledAsync(
|
||||
@event.OrderId,
|
||||
@event.Reason ?? "异步作废返佣",
|
||||
cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogWarning(
|
||||
"AgentCommission job unknown Action={Action} OrderId={OrderId}",
|
||||
@event.Action,
|
||||
@event.OrderId);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 失败 Ack,避免打满队列;明细可后续补偿
|
||||
logger.LogWarning(
|
||||
ex,
|
||||
"AgentCommission job failed OrderId={OrderId} Action={Action}",
|
||||
@event.OrderId,
|
||||
@event.Action);
|
||||
}
|
||||
|
||||
await unitOfWork.CompleteAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
using Fake.EventBus.Distributed;
|
||||
|
||||
namespace Oms.Application.Messaging.Agents;
|
||||
|
||||
/// <summary>
|
||||
/// 代理返佣异步任务(预计 / 锁定 / 作废),走 Fake.EventBus Outbox。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class AgentCommissionJobIntegrationEvent : IntegrationEvent
|
||||
{
|
||||
public int OrderId { get; set; }
|
||||
|
||||
/// <summary><see cref="AgentCommissionJobActionCodes"/>。</summary>
|
||||
public string Action { get; set; } = AgentCommissionJobActionCodes.Estimate;
|
||||
|
||||
/// <summary>finalize 时的触发运单状态。</summary>
|
||||
public string? TriggerOrderStatus { get; set; }
|
||||
|
||||
/// <summary>void 时的原因备注。</summary>
|
||||
public string? Reason { get; set; }
|
||||
}
|
||||
8
src/Oms.Application/Messaging/IOrderBatchJobPublisher.cs
Normal file
8
src/Oms.Application/Messaging/IOrderBatchJobPublisher.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Oms.Application.Messaging;
|
||||
|
||||
public interface IOrderBatchJobPublisher
|
||||
{
|
||||
Task PublishAsync(OrderBatchJobMessage message, CancellationToken cancellationToken = default);
|
||||
|
||||
bool IsEnabled { get; }
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
using Fake.EntityFrameworkCore;
|
||||
using Fake.EventBus.Distributed;
|
||||
using Fake.EventBus.IntegrationEventLog;
|
||||
using Fake.UnitOfWork;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Oms.Application.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 可靠发布 IntegrationEvent:事务 UoW 内写入 Outbox(由 Fake.EventBus.IntegrationEventLog 投递)。
|
||||
/// </summary>
|
||||
internal static class OmsIntegrationEventPublisher
|
||||
{
|
||||
public static async Task PublishAsync(
|
||||
IDistributedEventBus eventBus,
|
||||
IUnitOfWorkManager unitOfWorkManager,
|
||||
ILogger logger,
|
||||
IntegrationEvent @event,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (OmsOutboxUnitOfWork.IsAmbientTransactional(unitOfWorkManager))
|
||||
{
|
||||
await eventBus.PublishAsync(@event, cancellationToken);
|
||||
logger.LogDebug(
|
||||
"Outbox enqueued {EventName} Id={EventId} (ambient TX)",
|
||||
@event.GetType().Name,
|
||||
@event.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
// 无环境事务时开启短事务只写 Outbox,并告警:业务落库与事件非原子。
|
||||
logger.LogWarning(
|
||||
"Publish {EventName} Id={EventId} without ambient transactional UoW; opening short outbox TX",
|
||||
@event.GetType().Name,
|
||||
@event.Id);
|
||||
|
||||
using var uow = unitOfWorkManager.Begin(OmsOutboxUnitOfWork.Transactional());
|
||||
// 先打开 Outbox DbContext 以产生可征用的 DbTransaction
|
||||
var dbProvider = uow.ServiceProvider.GetRequiredService<IEfDbContextProvider<IntegrationEventLogContext>>();
|
||||
await dbProvider.GetDbContextAsync();
|
||||
await eventBus.PublishAsync(@event, cancellationToken);
|
||||
await uow.CompleteAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public static async Task<bool> TryPublishAsync(
|
||||
IDistributedEventBus eventBus,
|
||||
IUnitOfWorkManager unitOfWorkManager,
|
||||
ILogger logger,
|
||||
IntegrationEvent @event,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await PublishAsync(eventBus, unitOfWorkManager, logger, @event, cancellationToken);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(
|
||||
ex,
|
||||
"Outbox publish failed {EventName} Id={EventId}",
|
||||
@event.GetType().Name,
|
||||
@event.Id);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -17,16 +17,6 @@ public static class OmsOutboxUnitOfWork
|
||||
IsolationLevel = IsolationLevel.ReadCommitted
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 与当前业务事务脱开的短事务,用于创单/预报成功之后的独立事件。
|
||||
/// </summary>
|
||||
public static UnitOfWorkAttribute Independent() => new()
|
||||
{
|
||||
IsTransactional = true,
|
||||
RequiresNew = true,
|
||||
IsolationLevel = IsolationLevel.ReadCommitted
|
||||
};
|
||||
|
||||
public static bool IsAmbientTransactional(IUnitOfWorkManager unitOfWorkManager) =>
|
||||
unitOfWorkManager.Current?.Context.IsTransactional == true;
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ public sealed class OrderBatchJobEventHandler(
|
||||
return;
|
||||
}
|
||||
|
||||
// 业务结果在本事务提交;轨迹/面单/自动触发由处理过程登记到提交后单独执行。
|
||||
// ForecastBilling 成功后继续写轨迹/面单/自动触发 Outbox,须事务性 UoW
|
||||
using var unitOfWork = unitOfWorkManager.Begin(OmsOutboxUnitOfWork.Transactional());
|
||||
await processor.ProcessAsync(@event.ToMessage(), cancellationToken);
|
||||
await unitOfWork.CompleteAsync(cancellationToken);
|
||||
|
||||
36
src/Oms.Application/Messaging/OrderBatchJobPublisher.cs
Normal file
36
src/Oms.Application/Messaging/OrderBatchJobPublisher.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using Fake.EventBus.Distributed;
|
||||
using Fake.UnitOfWork;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Oms.Application.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 通过 Fake 分布式事件总线(Outbox)发布订单批量任务。
|
||||
/// </summary>
|
||||
public sealed class OrderBatchJobPublisher(
|
||||
IDistributedEventBus eventBus,
|
||||
IUnitOfWorkManager unitOfWorkManager,
|
||||
IOptions<OrderBatchRabbitMqOptions> options,
|
||||
ILogger<OrderBatchJobPublisher> logger) : IOrderBatchJobPublisher
|
||||
{
|
||||
public bool IsEnabled => options.Value.Enabled;
|
||||
|
||||
public async Task PublishAsync(OrderBatchJobMessage message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!options.Value.Enabled)
|
||||
{
|
||||
throw new InvalidOperationException("Order batch RabbitMQ is disabled.");
|
||||
}
|
||||
|
||||
var @event = OrderBatchJobIntegrationEvent.FromMessage(message);
|
||||
await OmsIntegrationEventPublisher.PublishAsync(
|
||||
eventBus, unitOfWorkManager, logger, @event, cancellationToken);
|
||||
|
||||
logger.LogDebug(
|
||||
"Outbox enqueued order batch event {EventId} messageId={MessageId} action={Action}",
|
||||
@event.Id,
|
||||
message.MessageId,
|
||||
message.Action);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
using Oms.Application.Services.Orders.Issues;
|
||||
|
||||
namespace Oms.Application.Messaging.OrderSideEffects;
|
||||
|
||||
/// <summary>
|
||||
/// 订单侧后置副作用发布(生命周期轨迹、自动触发等)。
|
||||
/// </summary>
|
||||
public interface IOrderSideEffectEventPublisher
|
||||
{
|
||||
Task PublishLifecycleTrackAppendAsync(
|
||||
int orderId,
|
||||
string orderNo,
|
||||
string trackCode,
|
||||
string? authorization = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task PublishAutoTriggerFireAsync(
|
||||
int orderId,
|
||||
AutoTriggerTiming timing,
|
||||
int operatorUserId,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
using Fake.DependencyInjection;
|
||||
using Fake.EventBus;
|
||||
using Fake.UnitOfWork;
|
||||
using Oms.Application.Messaging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Oms.Application.Services.Orders.Issues;
|
||||
|
||||
@ -27,7 +26,7 @@ public sealed class OrderAutoTriggerFireEventHandler(
|
||||
return;
|
||||
}
|
||||
|
||||
using var unitOfWork = unitOfWorkManager.Begin(OmsOutboxUnitOfWork.Transactional());
|
||||
using var unitOfWork = unitOfWorkManager.Begin();
|
||||
try
|
||||
{
|
||||
await autoTriggerRuntimeService.FireByOrderIdAsync(
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using Fake.DependencyInjection;
|
||||
using Fake.EventBus;
|
||||
using Fake.UnitOfWork;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Oms.Application.Integrations.SmallBagTrack;
|
||||
|
||||
@ -7,6 +8,7 @@ namespace Oms.Application.Messaging.OrderSideEffects;
|
||||
|
||||
public sealed class OrderLifecycleTrackAppendEventHandler(
|
||||
ISmallBagTrackOrderClient trackClient,
|
||||
IUnitOfWorkManager unitOfWorkManager,
|
||||
ILogger<OrderLifecycleTrackAppendEventHandler> logger)
|
||||
: IEventHandler<OrderLifecycleTrackAppendIntegrationEvent>, ITransientDependency
|
||||
{
|
||||
@ -22,7 +24,7 @@ public sealed class OrderLifecycleTrackAppendEventHandler(
|
||||
return;
|
||||
}
|
||||
|
||||
// 纯外部 HTTP,无需事务;占 TX 只会拖长连接占用。
|
||||
using var unitOfWork = unitOfWorkManager.Begin();
|
||||
try
|
||||
{
|
||||
await trackClient.AppendLifecycleTrackAsync(
|
||||
@ -41,5 +43,7 @@ public sealed class OrderLifecycleTrackAppendEventHandler(
|
||||
@event.OrderNo,
|
||||
@event.TrackCode);
|
||||
}
|
||||
|
||||
await unitOfWork.CompleteAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
using Fake.EventBus.Distributed;
|
||||
using Fake.UnitOfWork;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Oms.Application.Services.Orders.Issues;
|
||||
|
||||
namespace Oms.Application.Messaging.OrderSideEffects;
|
||||
|
||||
/// <summary>
|
||||
/// 订单旁路副作用事件:事务内写 Outbox,Commit 后投递 RabbitMQ。
|
||||
/// </summary>
|
||||
public sealed class OrderSideEffectEventPublisher(
|
||||
IDistributedEventBus eventBus,
|
||||
IUnitOfWorkManager unitOfWorkManager,
|
||||
ILogger<OrderSideEffectEventPublisher> logger) : IOrderSideEffectEventPublisher
|
||||
{
|
||||
public async Task PublishLifecycleTrackAppendAsync(
|
||||
int orderId,
|
||||
string orderNo,
|
||||
string trackCode,
|
||||
string? authorization = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (orderId <= 0 || string.IsNullOrWhiteSpace(orderNo) || string.IsNullOrWhiteSpace(trackCode))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var @event = new OrderLifecycleTrackAppendIntegrationEvent
|
||||
{
|
||||
OrderId = orderId,
|
||||
OrderNo = orderNo.Trim(),
|
||||
TrackCode = trackCode.Trim(),
|
||||
Authorization = string.IsNullOrWhiteSpace(authorization) ? null : authorization.Trim()
|
||||
};
|
||||
|
||||
await OmsIntegrationEventPublisher.PublishAsync(
|
||||
eventBus, unitOfWorkManager, logger, @event, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task PublishAutoTriggerFireAsync(
|
||||
int orderId,
|
||||
AutoTriggerTiming timing,
|
||||
int operatorUserId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (orderId <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var @event = new OrderAutoTriggerFireIntegrationEvent
|
||||
{
|
||||
OrderId = orderId,
|
||||
Timing = timing.ToString(),
|
||||
OperatorUserId = operatorUserId
|
||||
};
|
||||
|
||||
await OmsIntegrationEventPublisher.PublishAsync(
|
||||
eventBus, unitOfWorkManager, logger, @event, cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
using Oms.Application.Dtos.Orders;
|
||||
using Oms.Application.Integrations.SmallBagTrack;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
|
||||
namespace Oms.Application.Messaging.ParcelCreate;
|
||||
|
||||
/// <summary>
|
||||
/// 创单后置副作用:发布到 Fake.EventBus.RabbitMQ;发布失败时回落同步执行。
|
||||
/// </summary>
|
||||
public interface IOrderParcelCreateEventPublisher
|
||||
{
|
||||
/// <summary>
|
||||
/// 轨迹订单 + 创建成功轨迹(A0101)合并为一个事件。
|
||||
/// </summary>
|
||||
Task PublishTrackUpsertAsync(
|
||||
Order order,
|
||||
SmallBagTrackUpsertRequest request,
|
||||
int? customerId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task PublishLabelGenerateAsync(
|
||||
Order order,
|
||||
int operatorUserId = 0,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task PublishCustoms9610UpsertAsync(
|
||||
Order order,
|
||||
string? platformCode = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 发布揽收挂接事件。返回 true 表示已入队;false 表示已同步回落执行(或参数不足未执行)。
|
||||
/// </summary>
|
||||
Task<bool> PublishPickupAttachAsync(
|
||||
OrderParcelCreateInput input,
|
||||
int orderId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 同步执行揽收挂接(用于创单 HTTP 需要回写 PickupNo 的场景)。
|
||||
/// </summary>
|
||||
Task<PickupOrderOutput?> ExecutePickupAttachAsync(
|
||||
OrderParcelCreateInput input,
|
||||
int orderId,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@ -0,0 +1,143 @@
|
||||
using Fake.EventBus.Distributed;
|
||||
using Fake.UnitOfWork;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Oms.Application.Dtos.Orders;
|
||||
using Oms.Application.Integrations.SmallBagTrack;
|
||||
using Oms.Application.Services.Orders;
|
||||
using Oms.Domain.OrdersAggregate.Consts;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
|
||||
namespace Oms.Application.Messaging.ParcelCreate;
|
||||
|
||||
/// <summary>
|
||||
/// 创单后置事件发布器:事务内写 Outbox(Fake.EventBus.IntegrationEventLog),Commit 后投递 RabbitMQ。
|
||||
/// 揽收 Publish 失败时由调用方同步 ExecutePickupAttachAsync 回落。
|
||||
/// </summary>
|
||||
public sealed class OrderParcelCreateEventPublisher(
|
||||
IDistributedEventBus eventBus,
|
||||
IUnitOfWorkManager unitOfWorkManager,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<OrderParcelCreateEventPublisher> logger) : IOrderParcelCreateEventPublisher
|
||||
{
|
||||
public async Task PublishTrackUpsertAsync(
|
||||
Order order,
|
||||
SmallBagTrackUpsertRequest request,
|
||||
int? customerId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var @event = new OrderParcelTrackUpsertIntegrationEvent
|
||||
{
|
||||
OrderId = order.Id,
|
||||
CustomerId = customerId,
|
||||
PayloadJson = JsonConvert.SerializeObject(request),
|
||||
AppendInitialTrackOnSuccess = request.AppendInitialTrackOnSuccess,
|
||||
Authorization = request.Authorization
|
||||
};
|
||||
|
||||
await OmsIntegrationEventPublisher.PublishAsync(
|
||||
eventBus, unitOfWorkManager, logger, @event, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task PublishLabelGenerateAsync(
|
||||
Order order,
|
||||
int operatorUserId = 0,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var @event = new OrderParcelLabelGenerateIntegrationEvent
|
||||
{
|
||||
OrderId = order.Id,
|
||||
OperatorUserId = operatorUserId
|
||||
};
|
||||
|
||||
await OmsIntegrationEventPublisher.PublishAsync(
|
||||
eventBus, unitOfWorkManager, logger, @event, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task PublishCustoms9610UpsertAsync(
|
||||
Order order,
|
||||
string? platformCode = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var @event = new OrderParcelCustoms9610UpsertIntegrationEvent
|
||||
{
|
||||
OrderId = order.Id,
|
||||
PlatformCode = platformCode
|
||||
};
|
||||
|
||||
await OmsIntegrationEventPublisher.PublishAsync(
|
||||
eventBus, unitOfWorkManager, logger, @event, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> PublishPickupAttachAsync(
|
||||
OrderParcelCreateInput input,
|
||||
int orderId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var @event = BuildPickupEvent(input, orderId);
|
||||
if (@event == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return await OmsIntegrationEventPublisher.TryPublishAsync(
|
||||
eventBus, unitOfWorkManager, logger, @event, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<PickupOrderOutput?> ExecutePickupAttachAsync(
|
||||
OrderParcelCreateInput input,
|
||||
int orderId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (orderId <= 0
|
||||
|| input.SenderAddressId is not > 0
|
||||
|| input.ExpectPickupTime is null
|
||||
|| input.ExpectPickupTime == default
|
||||
|| input.WarehouseId is not > 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var pickupService = scope.ServiceProvider.GetRequiredService<PickupOrderService>();
|
||||
return await pickupService.CreateForParcelInternalAsync(new PickupOrderCreateInput
|
||||
{
|
||||
CustomerId = input.CustomerId,
|
||||
SenderAddressId = input.SenderAddressId.Value,
|
||||
Direction = PickupDirectionCodes.NormalizeOrDefault(input.PickupDirection),
|
||||
WarehouseId = input.WarehouseId.Value,
|
||||
CarrierCode = input.CarrierCode,
|
||||
CarrierService = input.CarrierService,
|
||||
ExpectPickupTime = input.ExpectPickupTime.Value,
|
||||
OrderIds = [orderId],
|
||||
Remark = null
|
||||
});
|
||||
}
|
||||
|
||||
private static OrderParcelPickupAttachIntegrationEvent? BuildPickupEvent(
|
||||
OrderParcelCreateInput input,
|
||||
int orderId)
|
||||
{
|
||||
if (orderId <= 0
|
||||
|| input.SenderAddressId is not > 0
|
||||
|| input.ExpectPickupTime is null
|
||||
|| input.ExpectPickupTime == default
|
||||
|| input.WarehouseId is not > 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new OrderParcelPickupAttachIntegrationEvent
|
||||
{
|
||||
OrderId = orderId,
|
||||
CustomerId = input.CustomerId,
|
||||
SenderAddressId = input.SenderAddressId.Value,
|
||||
WarehouseId = input.WarehouseId.Value,
|
||||
PickupDirection = input.PickupDirection,
|
||||
CarrierCode = input.CarrierCode,
|
||||
CarrierService = input.CarrierService,
|
||||
ExpectPickupTime = input.ExpectPickupTime.Value
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,8 @@
|
||||
using Fake.DependencyInjection;
|
||||
using Fake.EventBus;
|
||||
using Fake.UnitOfWork;
|
||||
using Oms.Application.Messaging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Oms.Application.Services.Orders;
|
||||
using Oms.Domain.OrdersAggregate.Consts;
|
||||
using Oms.Domain.OrdersAggregate.Repositories;
|
||||
|
||||
namespace Oms.Application.Messaging.ParcelCreate;
|
||||
@ -23,13 +21,11 @@ public sealed class OrderParcelCustoms9610UpsertEventHandler(
|
||||
OrderParcelCustoms9610UpsertIntegrationEvent @event,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var unitOfWork = unitOfWorkManager.Begin(OmsOutboxUnitOfWork.Transactional());
|
||||
using var unitOfWork = unitOfWorkManager.Begin();
|
||||
var order = await orderRepository.FirstOrDefaultAsync(x => x.Id == @event.OrderId, cancellationToken);
|
||||
if (order == null
|
||||
|| string.Equals(order.OrderStatus, OrderStatusCodes.Deleted, StringComparison.OrdinalIgnoreCase))
|
||||
if (order == null)
|
||||
{
|
||||
logger.LogWarning("9610 upsert skipped, order missing/deleted OrderId={OrderId}", @event.OrderId);
|
||||
await unitOfWork.CompleteAsync(cancellationToken);
|
||||
logger.LogWarning("9610 upsert skipped, order missing OrderId={OrderId}", @event.OrderId);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
using Fake.DependencyInjection;
|
||||
using Fake.EventBus;
|
||||
using Fake.UnitOfWork;
|
||||
using Oms.Application.Messaging;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Oms.Application.Services.Orders;
|
||||
@ -21,7 +20,7 @@ public sealed class OrderParcelLabelGenerateEventHandler(
|
||||
OrderParcelLabelGenerateIntegrationEvent @event,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var unitOfWork = unitOfWorkManager.Begin(OmsOutboxUnitOfWork.Transactional());
|
||||
using var unitOfWork = unitOfWorkManager.Begin();
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var orderService = scope.ServiceProvider.GetRequiredService<OrderService>();
|
||||
var result = await orderService.GenerateLabelPdfForBatchAsync(@event.OrderId, cancellationToken);
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
using Fake.DependencyInjection;
|
||||
using Fake.EventBus;
|
||||
using Fake.UnitOfWork;
|
||||
using Oms.Application.Messaging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Oms.Application.Dtos.Orders;
|
||||
using Oms.Application.Services.Orders;
|
||||
@ -22,7 +21,7 @@ public sealed class OrderParcelPickupAttachEventHandler(
|
||||
OrderParcelPickupAttachIntegrationEvent @event,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var unitOfWork = unitOfWorkManager.Begin(OmsOutboxUnitOfWork.Transactional());
|
||||
using var unitOfWork = unitOfWorkManager.Begin();
|
||||
try
|
||||
{
|
||||
await pickupOrderService.CreateForParcelInternalAsync(new PickupOrderCreateInput
|
||||
|
||||
@ -24,8 +24,7 @@ public sealed class OrderParcelTrackUpsertEventHandler(
|
||||
OrderParcelTrackUpsertIntegrationEvent @event,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// 仅读库 + 外部 HTTP,不要占事务连接;失败抛出让总线重试。
|
||||
using var unitOfWork = unitOfWorkManager.Begin(new UnitOfWorkAttribute { IsTransactional = false });
|
||||
using var unitOfWork = unitOfWorkManager.Begin();
|
||||
|
||||
var request = await ResolveRequestAsync(@event, cancellationToken);
|
||||
request.AppendInitialTrackOnSuccess = @event.AppendInitialTrackOnSuccess;
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
namespace Oms.Application.Messaging.TrackChange;
|
||||
|
||||
/// <summary>
|
||||
/// 根据轨迹变更事件同步 OMS 出库后订单状态。
|
||||
/// </summary>
|
||||
public interface ITrackChangeOrderStatusApplier
|
||||
{
|
||||
/// <summary>
|
||||
/// 处理一条 TrackChange 消息;业务跳过不抛异常。
|
||||
/// </summary>
|
||||
Task ApplyAsync(TrackChangeEto eto, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
namespace Oms.Application.Messaging.TrackChange;
|
||||
|
||||
/// <summary>
|
||||
/// 轨迹中心轨迹变更广播(RabbitMQ topic:track.on_track_change_ex / TrackChange.{AppKey})。
|
||||
/// 字段与 TrackCenter.Application.Events.TrackChangeEto 对齐(Newtonsoft 默认 PascalCase)。
|
||||
/// </summary>
|
||||
public sealed class TrackChangeEto
|
||||
{
|
||||
/// <summary>轨迹唯一 id;操作失败时可能为 null。</summary>
|
||||
public string? TrackEsId { get; set; }
|
||||
|
||||
/// <summary>操作类型:1 新增,2 删除,3 修改。</summary>
|
||||
public int Option { get; set; }
|
||||
|
||||
/// <summary>操作是否成功。</summary>
|
||||
public bool IsSuccess { get; set; }
|
||||
|
||||
/// <summary>失败原因。</summary>
|
||||
public string? FailedReason { get; set; }
|
||||
|
||||
/// <summary>应用 AppKey。</summary>
|
||||
public string? AppKey { get; set; }
|
||||
|
||||
/// <summary>单号(OMS 侧对应运单号 ServiceNo)。</summary>
|
||||
public string? OrderNo { get; set; }
|
||||
|
||||
/// <summary>客户标识。</summary>
|
||||
public string? Customer { get; set; }
|
||||
|
||||
/// <summary>操作人。</summary>
|
||||
public string? OperationMan { get; set; }
|
||||
|
||||
/// <summary>轨迹内容代码。</summary>
|
||||
public string? TrackCode { get; set; }
|
||||
|
||||
/// <summary>轨迹内容。</summary>
|
||||
public string? Content { get; set; }
|
||||
|
||||
/// <summary>轨迹内容英文。</summary>
|
||||
public string? EnStatus { get; set; }
|
||||
|
||||
/// <summary>轨迹状态代码(如 D03 / D09 / D11)。</summary>
|
||||
public string? TrackStatusCode { get; set; }
|
||||
|
||||
/// <summary>轨迹发生地。</summary>
|
||||
public string? TrackPlace { get; set; }
|
||||
|
||||
/// <summary>轨迹发生时间。</summary>
|
||||
public DateTime TrackTime { get; set; }
|
||||
|
||||
/// <summary>轨迹发生地时区。</summary>
|
||||
public float TrackTimeZone { get; set; }
|
||||
|
||||
/// <summary>是否对外。</summary>
|
||||
public bool IsExternal { get; set; }
|
||||
|
||||
/// <summary>数据来源:1 外部服务商,2 内部系统,3 人工。</summary>
|
||||
public int Source { get; set; }
|
||||
|
||||
/// <summary>轨迹备注。</summary>
|
||||
public string? Remarks { get; set; }
|
||||
|
||||
/// <summary>扩展信息。</summary>
|
||||
public string? Extra { get; set; }
|
||||
}
|
||||
@ -1,186 +0,0 @@
|
||||
using Fake.DependencyInjection;
|
||||
using Fake.EventBus.Distributed;
|
||||
using Fake.UnitOfWork;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Oms.Application.Messaging.Agents;
|
||||
using Oms.Application.Services.Orders;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Domain.OrdersAggregate.Consts;
|
||||
using Oms.Domain.OrdersAggregate.Repositories;
|
||||
|
||||
namespace Oms.Application.Messaging.TrackChange;
|
||||
|
||||
/// <summary>
|
||||
/// 将轨迹状态 D03/D09/D11 同步到运单订单状态(运输中 / 派送失败 / 派送成功)。
|
||||
/// 规则对齐手动「出库后状态校正」:仅出库后四态可改;派送成功只进不出。
|
||||
/// </summary>
|
||||
public sealed class TrackChangeOrderStatusApplier(
|
||||
IOrderRepository orderRepository,
|
||||
OrderOperationLogWriter operationLogWriter,
|
||||
ReferralCommissionManager referralCommissionManager,
|
||||
IDistributedEventBus eventBus,
|
||||
MarketingCouponManager marketingCouponManager,
|
||||
IUnitOfWorkManager unitOfWorkManager,
|
||||
ILogger<TrackChangeOrderStatusApplier> logger) : ITrackChangeOrderStatusApplier, IScopedDependency
|
||||
{
|
||||
private static readonly HashSet<string> PostShipStatuses = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
OrderStatusCodes.PendingOnline,
|
||||
OrderStatusCodes.Shipping,
|
||||
OrderStatusCodes.DeliverySuccess,
|
||||
OrderStatusCodes.DeliveryFailed
|
||||
};
|
||||
|
||||
public async Task ApplyAsync(TrackChangeEto eto, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (eto == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!eto.IsSuccess)
|
||||
{
|
||||
logger.LogDebug(
|
||||
"TrackChange skip failed event OrderNo={OrderNo} Reason={Reason}",
|
||||
eto.OrderNo,
|
||||
eto.FailedReason);
|
||||
return;
|
||||
}
|
||||
|
||||
// 删除轨迹不回退 OMS 状态
|
||||
if (eto.Option == 2)
|
||||
{
|
||||
logger.LogDebug("TrackChange skip delete Option=2 OrderNo={OrderNo}", eto.OrderNo);
|
||||
return;
|
||||
}
|
||||
|
||||
var targetStatus = TrackStatusOrderStatusMapper.TryMapToOrderStatus(eto.TrackStatusCode);
|
||||
if (targetStatus == null)
|
||||
{
|
||||
logger.LogDebug(
|
||||
"TrackChange skip unmapped status OrderNo={OrderNo} TrackStatusCode={TrackStatusCode}",
|
||||
eto.OrderNo,
|
||||
eto.TrackStatusCode);
|
||||
return;
|
||||
}
|
||||
|
||||
var orderNo = eto.OrderNo?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(orderNo))
|
||||
{
|
||||
logger.LogWarning("TrackChange skip empty OrderNo TrackStatusCode={TrackStatusCode}", eto.TrackStatusCode);
|
||||
return;
|
||||
}
|
||||
|
||||
using var unitOfWork = unitOfWorkManager.Begin(OmsOutboxUnitOfWork.Transactional());
|
||||
|
||||
var order = await orderRepository.GetByServiceNoAsync(orderNo, cancellationToken);
|
||||
if (order == null)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"TrackChange order not found ServiceNo={OrderNo} TrackStatusCode={TrackStatusCode}",
|
||||
orderNo,
|
||||
eto.TrackStatusCode);
|
||||
await unitOfWork.CompleteAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var fromStatus = order.OrderStatus?.Trim() ?? string.Empty;
|
||||
if (!IsPostShipStatus(fromStatus))
|
||||
{
|
||||
logger.LogInformation(
|
||||
"TrackChange skip non post-ship status OrderId={OrderId} ServiceNo={ServiceNo} From={From} TrackStatusCode={TrackStatusCode}",
|
||||
order.Id,
|
||||
order.ServiceNo,
|
||||
fromStatus,
|
||||
eto.TrackStatusCode);
|
||||
await unitOfWork.CompleteAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(fromStatus, OrderStatusCodes.DeliverySuccess, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
logger.LogInformation(
|
||||
"TrackChange skip deliverySuccess terminal OrderId={OrderId} ServiceNo={ServiceNo} TrackStatusCode={TrackStatusCode}",
|
||||
order.Id,
|
||||
order.ServiceNo,
|
||||
eto.TrackStatusCode);
|
||||
await unitOfWork.CompleteAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(fromStatus, targetStatus, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
logger.LogDebug(
|
||||
"TrackChange idempotent skip OrderId={OrderId} ServiceNo={ServiceNo} Status={Status}",
|
||||
order.Id,
|
||||
order.ServiceNo,
|
||||
targetStatus);
|
||||
await unitOfWork.CompleteAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var beforeSnap = OrderOperationLogSnapshot.CaptureOrder(order);
|
||||
order.OrderStatus = targetStatus;
|
||||
order.UpdateTime = DateTime.Now;
|
||||
if (string.Equals(targetStatus, OrderStatusCodes.DeliverySuccess, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
order.SignTime = DateTime.Now;
|
||||
}
|
||||
|
||||
await orderRepository.UpdateAsync(order, cancellationToken: cancellationToken);
|
||||
|
||||
if (string.Equals(targetStatus, OrderStatusCodes.DeliverySuccess, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await referralCommissionManager.OnOrderDeliverySuccessAsync(order.Id, cancellationToken);
|
||||
// 重新加载,避免营销侧依赖最新字段
|
||||
order = await orderRepository.GetByServiceNoAsync(orderNo, cancellationToken) ?? order;
|
||||
await marketingCouponManager.OnFirstDeliverySuccessAsync(order, cancellationToken);
|
||||
await eventBus.PublishAsync(new AgentCommissionJobIntegrationEvent
|
||||
{
|
||||
OrderId = order.Id,
|
||||
Action = AgentCommissionJobActionCodes.Finalize,
|
||||
TriggerOrderStatus = OrderStatusCodes.DeliverySuccess
|
||||
});
|
||||
}
|
||||
else if (string.Equals(targetStatus, OrderStatusCodes.DeliveryFailed, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await eventBus.PublishAsync(new AgentCommissionJobIntegrationEvent
|
||||
{
|
||||
OrderId = order.Id,
|
||||
Action = AgentCommissionJobActionCodes.Finalize,
|
||||
TriggerOrderStatus = OrderStatusCodes.DeliveryFailed
|
||||
});
|
||||
}
|
||||
|
||||
order = await orderRepository.GetByServiceNoAsync(orderNo, cancellationToken) ?? order;
|
||||
var afterSnap = OrderOperationLogSnapshot.CaptureOrder(order);
|
||||
var fromName = OmsDocHelper.GetOrderStatusName(fromStatus) ?? fromStatus;
|
||||
var toName = OmsDocHelper.GetOrderStatusName(targetStatus) ?? targetStatus;
|
||||
var trackHint = string.IsNullOrWhiteSpace(eto.Content)
|
||||
? eto.TrackStatusCode
|
||||
: $"{eto.TrackStatusCode}/{eto.Content}";
|
||||
await operationLogWriter.TryWriteAsync(
|
||||
order,
|
||||
OrderOperationActionCodes.TrackStatusSync,
|
||||
beforeSnap,
|
||||
afterSnap,
|
||||
$"{fromName} → {toName};轨迹同步 {trackHint}",
|
||||
operatorId: 0,
|
||||
operatorName: string.IsNullOrWhiteSpace(eto.OperationMan) ? "轨迹中心" : eto.OperationMan.Trim(),
|
||||
operatorType: "system",
|
||||
source: "trackCenter");
|
||||
|
||||
await unitOfWork.CompleteAsync(cancellationToken);
|
||||
|
||||
logger.LogInformation(
|
||||
"TrackChange applied OrderId={OrderId} ServiceNo={ServiceNo} {From} → {To} TrackStatusCode={TrackStatusCode}",
|
||||
order.Id,
|
||||
order.ServiceNo,
|
||||
fromStatus,
|
||||
targetStatus,
|
||||
eto.TrackStatusCode);
|
||||
}
|
||||
|
||||
private static bool IsPostShipStatus(string? status) =>
|
||||
!string.IsNullOrWhiteSpace(status) && PostShipStatuses.Contains(status.Trim());
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
namespace Oms.Application.Messaging.TrackChange;
|
||||
|
||||
/// <summary>
|
||||
/// 订阅轨迹中心 TrackChange 广播的配置(Consul / 配置:Oms:TrackChangeSubscribe)。
|
||||
/// </summary>
|
||||
public class TrackChangeSubscribeOptions
|
||||
{
|
||||
public const string SectionName = "Oms:TrackChangeSubscribe";
|
||||
|
||||
/// <summary>是否启用订阅;默认 true。</summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 轨迹中心广播交换器;须与 TrackCenter GlobalConst.TrackChangeExchange 一致。
|
||||
/// </summary>
|
||||
public string Exchange { get; set; } = "track.on_track_change_ex";
|
||||
|
||||
/// <summary>
|
||||
/// 绑定路由键模板,{AppKey} 会替换为轨迹服务 AppKey。
|
||||
/// </summary>
|
||||
public string RoutingKeyTemplate { get; set; } = "TrackChange.{AppKey}";
|
||||
|
||||
/// <summary>
|
||||
/// 本地队列名模板;{AppKey} 会替换。为空时使用 oms.track_change.{AppKey}。
|
||||
/// </summary>
|
||||
public string? QueueNameTemplate { get; set; } = "oms.track_change.{AppKey}";
|
||||
|
||||
/// <summary>
|
||||
/// Fake.RabbitMQ 连接名;为空则使用 RabbitMqEventBusOptions.ConnectionName。
|
||||
/// </summary>
|
||||
public string? ConnectionName { get; set; }
|
||||
|
||||
/// <summary>消费预取数量。</summary>
|
||||
public ushort PrefetchCount { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// 覆盖轨迹 AppKey(一般留空,走 Oms:TrackService / 系统其他配置 track_service)。
|
||||
/// </summary>
|
||||
public string? AppKey { get; set; }
|
||||
|
||||
/// <summary>处理失败时是否重新入队。</summary>
|
||||
public bool FailedRequeue { get; set; } = true;
|
||||
}
|
||||
@ -1,202 +0,0 @@
|
||||
using System.Text;
|
||||
using Fake.EventBus.RabbitMQ;
|
||||
using Fake.RabbitMQ;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using Oms.Application.Integrations.SmallBagTrack;
|
||||
using RabbitMQ.Client;
|
||||
using RabbitMQ.Client.Events;
|
||||
|
||||
namespace Oms.Application.Messaging.TrackChange;
|
||||
|
||||
/// <summary>
|
||||
/// 订阅轨迹中心 TrackChange 广播,驱动 OMS 出库后订单状态变更。
|
||||
/// </summary>
|
||||
public sealed class TrackChangeSubscriberHostedService : BackgroundService
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly IRabbitMqConnectionPool _connectionPool;
|
||||
private readonly IOptions<TrackChangeSubscribeOptions> _subscribeOptions;
|
||||
private readonly IOptions<RabbitMqEventBusOptions> _eventBusOptions;
|
||||
private readonly ILogger<TrackChangeSubscriberHostedService> _logger;
|
||||
|
||||
private IModel? _channel;
|
||||
|
||||
public TrackChangeSubscriberHostedService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IRabbitMqConnectionPool connectionPool,
|
||||
IOptions<TrackChangeSubscribeOptions> subscribeOptions,
|
||||
IOptions<RabbitMqEventBusOptions> eventBusOptions,
|
||||
ILogger<TrackChangeSubscriberHostedService> logger)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
_connectionPool = connectionPool;
|
||||
_subscribeOptions = subscribeOptions;
|
||||
_eventBusOptions = eventBusOptions;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
var options = _subscribeOptions.Value;
|
||||
if (!options.Enabled)
|
||||
{
|
||||
_logger.LogInformation("TrackChange subscribe disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
string? appKey;
|
||||
try
|
||||
{
|
||||
appKey = await ResolveAppKeyAsync(stoppingToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "TrackChange subscribe failed to resolve AppKey; consumer not started");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(appKey))
|
||||
{
|
||||
_logger.LogWarning("TrackChange subscribe skipped: AppKey empty (configure Oms:TrackService or Oms:TrackChangeSubscribe:AppKey)");
|
||||
return;
|
||||
}
|
||||
|
||||
var exchange = string.IsNullOrWhiteSpace(options.Exchange)
|
||||
? "track.on_track_change_ex"
|
||||
: options.Exchange.Trim();
|
||||
var routingKey = (options.RoutingKeyTemplate ?? "TrackChange.{AppKey}")
|
||||
.Replace("{AppKey}", appKey, StringComparison.OrdinalIgnoreCase);
|
||||
var queueName = string.IsNullOrWhiteSpace(options.QueueNameTemplate)
|
||||
? $"oms.track_change.{appKey}"
|
||||
: options.QueueNameTemplate.Replace("{AppKey}", appKey, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var connectionName = string.IsNullOrWhiteSpace(options.ConnectionName)
|
||||
? _eventBusOptions.Value.ConnectionName
|
||||
: options.ConnectionName;
|
||||
|
||||
try
|
||||
{
|
||||
// Connection 由 Fake 连接池托管,禁止 Dispose
|
||||
var connection = _connectionPool.Get(connectionName);
|
||||
_channel = connection.CreateModel();
|
||||
_channel.ExchangeDeclare(exchange, ExchangeType.Topic, durable: true, autoDelete: false);
|
||||
_channel.QueueDeclare(queueName, durable: true, exclusive: false, autoDelete: false);
|
||||
_channel.QueueBind(queueName, exchange, routingKey);
|
||||
if (options.PrefetchCount > 0)
|
||||
{
|
||||
_channel.BasicQos(0, options.PrefetchCount, false);
|
||||
}
|
||||
|
||||
var consumer = new AsyncEventingBasicConsumer(_channel);
|
||||
consumer.Received += OnReceivedAsync;
|
||||
_channel.BasicConsume(queue: queueName, autoAck: false, consumer: consumer);
|
||||
|
||||
_logger.LogInformation(
|
||||
"TrackChange subscribe started Exchange={Exchange} RoutingKey={RoutingKey} Queue={Queue}",
|
||||
exchange,
|
||||
routingKey,
|
||||
queueName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "TrackChange subscribe failed to start");
|
||||
DisposeChannel();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(Timeout.Infinite, stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// shutdown
|
||||
}
|
||||
finally
|
||||
{
|
||||
DisposeChannel();
|
||||
_logger.LogInformation("TrackChange subscribe stopped");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnReceivedAsync(object sender, BasicDeliverEventArgs ea)
|
||||
{
|
||||
var channel = _channel;
|
||||
if (channel == null || !channel.IsOpen)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var options = _subscribeOptions.Value;
|
||||
try
|
||||
{
|
||||
var json = Encoding.UTF8.GetString(ea.Body.Span);
|
||||
var eto = JsonConvert.DeserializeObject<TrackChangeEto>(json);
|
||||
if (eto == null)
|
||||
{
|
||||
_logger.LogWarning("TrackChange deserialize null body");
|
||||
channel.BasicAck(ea.DeliveryTag, false);
|
||||
return;
|
||||
}
|
||||
|
||||
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||
var applier = scope.ServiceProvider.GetRequiredService<ITrackChangeOrderStatusApplier>();
|
||||
await applier.ApplyAsync(eto);
|
||||
|
||||
channel.BasicAck(ea.DeliveryTag, false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "TrackChange handle failed DeliveryTag={DeliveryTag}", ea.DeliveryTag);
|
||||
try
|
||||
{
|
||||
channel.BasicNack(ea.DeliveryTag, false, options.FailedRequeue);
|
||||
}
|
||||
catch (Exception nackEx)
|
||||
{
|
||||
_logger.LogError(nackEx, "TrackChange Nack failed DeliveryTag={DeliveryTag}", ea.DeliveryTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string?> ResolveAppKeyAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var overrideKey = _subscribeOptions.Value.AppKey?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(overrideKey))
|
||||
{
|
||||
return overrideKey;
|
||||
}
|
||||
|
||||
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||
var trackConfig = scope.ServiceProvider.GetRequiredService<ISmallBagTrackConfigProvider>();
|
||||
var settings = await trackConfig.GetAsync(cancellationToken);
|
||||
return string.IsNullOrWhiteSpace(settings.AppKey) ? null : settings.AppKey.Trim();
|
||||
}
|
||||
|
||||
private void DisposeChannel()
|
||||
{
|
||||
try
|
||||
{
|
||||
_channel?.Close();
|
||||
_channel?.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "TrackChange channel dispose ignored");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_channel = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
DisposeChannel();
|
||||
base.Dispose();
|
||||
}
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
using Oms.Domain.OrdersAggregate.Consts;
|
||||
|
||||
namespace Oms.Application.Messaging.TrackChange;
|
||||
|
||||
/// <summary>
|
||||
/// 轨迹状态码 → OMS 出库后订单状态。
|
||||
/// </summary>
|
||||
public static class TrackStatusOrderStatusMapper
|
||||
{
|
||||
/// <summary>运输中。</summary>
|
||||
public const string InTransit = "D03";
|
||||
|
||||
/// <summary>派送失败。</summary>
|
||||
public const string DeliveryFailed = "D09";
|
||||
|
||||
/// <summary>派送成功。</summary>
|
||||
public const string DeliverySuccess = "D11";
|
||||
|
||||
/// <summary>
|
||||
/// 将轨迹状态码映射为 OMS OrderStatus;未映射返回 null。
|
||||
/// </summary>
|
||||
public static string? TryMapToOrderStatus(string? trackStatusCode)
|
||||
{
|
||||
var code = trackStatusCode?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(code))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return code.ToUpperInvariant() switch
|
||||
{
|
||||
InTransit => OrderStatusCodes.Shipping,
|
||||
DeliveryFailed => OrderStatusCodes.DeliveryFailed,
|
||||
DeliverySuccess => OrderStatusCodes.DeliverySuccess,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -18,10 +18,10 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Fake.ObjectMapping.AutoMapper" Version="$(FakePackageVersion)" />
|
||||
<PackageReference Include="Fake.ObjectStorage" Version="$(FakePackageVersion)" />
|
||||
<PackageReference Include="Fake.EventBus.IntegrationEventLog" Version="$(FakePackageVersion)" />
|
||||
<PackageReference Include="Fake.EventBus.RabbitMQ" Version="$(FakePackageVersion)" />
|
||||
<PackageReference Include="Fake.ObjectMapping.AutoMapper" Version="$(FakePackageVersion)" />
|
||||
<PackageReference Include="Fake.ObjectStorage" Version="$(FakePackageVersion)" />
|
||||
<PackageReference Include="Fake.RabbitMQ" Version="$(FakePackageVersion)" />
|
||||
<PackageReference Include="Fake.Rbac.Application" Version="$(FakePackageVersion)" />
|
||||
<PackageReference Include="Fake.Rbac.Infrastructure" Version="$(FakePackageVersion)" />
|
||||
@ -41,13 +41,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Services\Ai\AiSkillSeedContent.json" />
|
||||
<!-- 面单 QuestPDF 中文:嵌入单文件 TTF,避免容器仅有 TTC/未装字体时出 ??? -->
|
||||
<EmbeddedResource Include="Fonts\OmsLabelCjk.ttf" LogicalName="Oms.Application.Fonts.OmsLabelCjk.ttf" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Fonts\OmsLabelCjk.ttf" CopyToOutputDirectory="PreserveNewest" Link="Fonts\OmsLabelCjk.ttf" />
|
||||
<None Include="Fonts\README.md" CopyToOutputDirectory="Never" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Oms.Application.Messaging;
|
||||
using Oms.Application.Messaging.OrderSideEffects;
|
||||
using Oms.Application.Messaging.ParcelCreate;
|
||||
|
||||
namespace Oms.Application;
|
||||
|
||||
@ -9,7 +11,10 @@ public static class OrderBatchMessagingServiceExtensions
|
||||
public static IServiceCollection AddOmsOrderBatchMessaging(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.Configure<OrderBatchRabbitMqOptions>(configuration.GetSection(OrderBatchRabbitMqOptions.SectionName));
|
||||
services.AddSingleton<IOrderBatchJobPublisher, OrderBatchJobPublisher>();
|
||||
services.AddSingleton<IOrderBatchQueueInspector, OrderBatchQueueInspector>();
|
||||
services.AddSingleton<IOrderParcelCreateEventPublisher, OrderParcelCreateEventPublisher>();
|
||||
services.AddSingleton<IOrderSideEffectEventPublisher, OrderSideEffectEventPublisher>();
|
||||
// 消费端由 Fake.EventBus.RabbitMQ(RabbitMqEventBus HostedService)托管
|
||||
services.AddHostedService<OrderBatchDlxMonitorHostedService>();
|
||||
return services;
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
using Fake.DependencyInjection;
|
||||
using Fake.Rbac.Infrastructure;
|
||||
using Fake.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Oms.Application.Dtos.Customers;
|
||||
|
||||
@ -42,17 +41,18 @@ public class CustomerServiceAdvisorResolver(FakeRbacDbContext dbContext) : IScop
|
||||
return null;
|
||||
}
|
||||
|
||||
var sql = $@"
|
||||
const string sql = """
|
||||
SELECT
|
||||
u.`Name` AS `Name`,
|
||||
u.`WechatId` AS `WechatId`,
|
||||
u.`WechatQrCode` AS `WechatQrCode`,
|
||||
u.`WecomKfUrl` AS `WecomKfUrl`
|
||||
FROM `{FakeTables.User}` u
|
||||
FROM `user` u
|
||||
WHERE u.`IsDeleted` = 0
|
||||
AND u.`userid` = {{0}}
|
||||
AND u.`userid` = {0}
|
||||
LIMIT 1
|
||||
";
|
||||
""";
|
||||
|
||||
return await dbContext.Database
|
||||
.SqlQueryRaw<AdvisorRow>(sql, userId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
@ -60,18 +60,18 @@ public class CustomerServiceAdvisorResolver(FakeRbacDbContext dbContext) : IScop
|
||||
|
||||
private async Task<AdvisorRow?> FindByNameOrAccountAsync(string key, CancellationToken cancellationToken)
|
||||
{
|
||||
var sql = $@"
|
||||
const string sql = """
|
||||
SELECT
|
||||
u.`Name` AS `Name`,
|
||||
u.`WechatId` AS `WechatId`,
|
||||
u.`WechatQrCode` AS `WechatQrCode`,
|
||||
u.`WecomKfUrl` AS `WecomKfUrl`
|
||||
FROM `{FakeTables.User}` u
|
||||
FROM `user` u
|
||||
WHERE u.`IsDeleted` = 0
|
||||
AND (u.`Name` = {{0}} OR u.`Account` = {{0}})
|
||||
AND (u.`Name` = {0} OR u.`Account` = {0})
|
||||
ORDER BY u.`CreateTime` ASC
|
||||
LIMIT 1
|
||||
";
|
||||
""";
|
||||
|
||||
return await dbContext.Database
|
||||
.SqlQueryRaw<AdvisorRow>(sql, key)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user