From 545f0825f8f5b1d1f890ade39592b9e34cece68d Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Mon, 29 Jun 2026 11:31:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(order):=20=E8=A7=84=E5=88=99=E5=BF=AB?= =?UTF-8?q?=E7=85=A7=E4=B8=8B=E6=B8=B8=E5=85=83=E6=95=B0=E6=8D=AE=E3=80=81?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E7=BB=91=E5=AE=9A=E5=AD=97=E6=AE=B5=E4=B8=8E?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E5=85=B6=E4=BB=96=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 排货发布 enrich Providers 并确认渠道时写入 t_order 下游四字段;接入 itfx 元数据客户端;系统其他配置与小包轨迹 DB 配置;手动轨迹 UpsertSmallBag 接口。 Co-authored-by: Cursor --- db.sql | 26 +++ scripts/debug-upsert-small-bag.ps1 | 77 +++++++ ...migrate_t_order_downstream_integration.sql | 6 + sql/migrate_t_system_other_config.sql | 21 ++ ...lesProductAllocationProviderSnapshotDto.cs | 35 ++++ .../Dtos/Orders/OrderOutput.cs | 45 ++++ .../Dtos/Orders/OrderTrackUpsertDebugInput.cs | 31 +++ .../System/SystemOtherConfigCreateInput.cs | 47 +++++ .../Dtos/System/SystemOtherConfigListInput.cs | 22 ++ .../Dtos/System/SystemOtherConfigOutput.cs | 62 ++++++ .../System/SystemOtherConfigUpdateInput.cs | 12 ++ .../Integrations/Itfx/IItfxMetadataClient.cs | 20 ++ .../Integrations/Itfx/ItfxGatewayOptions.cs | 16 ++ .../Integrations/Itfx/ItfxMetadataClient.cs | 135 ++++++++++++ .../Itfx/ItfxServiceExtensions.cs | 15 ++ .../ISmallBagTrackOrderClient.cs | 5 + .../SmallBagTrackConfigProvider.cs | 41 ++++ .../SmallBagTrack/SmallBagTrackOptions.cs | 10 + .../SmallBagTrack/SmallBagTrackOrderClient.cs | 82 ++++++-- .../SmallBagTrackServiceExtensions.cs | 3 +- .../SmallBagTrack/SmallBagTrackSignHelper.cs | 29 +++ .../SmallBagTrackUpsertApiRequest.cs | 49 +++++ .../SmallBagTrackUpsertResult.cs | 21 ++ src/Oms.Application/OmsApplicationModule.cs | 2 + .../Channels/SalesProductService.Helpers.cs | 1 + .../Services/Channels/SalesProductService.cs | 2 + .../Services/Orders/OrderService.Helpers.cs | 9 + .../Orders/OrderService.ParcelBilling.cs | 138 +++++++++--- .../System/SystemOtherConfigService.cs | 197 ++++++++++++++++++ .../AllocationProviderSnapshotEnricher.cs | 122 +++++++++++ .../OrdersAggregate/Entities/Order.cs | 20 ++ .../Consts/SystemOtherConfigCodes.cs | 12 ++ .../Entities/SystemOtherConfig.cs | 24 +++ .../ISystemOtherConfigRepository.cs | 9 + .../System/SystemOtherConfigRepository.cs | 14 ++ src/Oms.Tests/SmallBagTrackSignHelperTests.cs | 19 ++ .../appsettings.Development.json | 13 ++ 37 files changed, 1349 insertions(+), 43 deletions(-) create mode 100644 scripts/debug-upsert-small-bag.ps1 create mode 100644 sql/migrate_t_order_downstream_integration.sql create mode 100644 sql/migrate_t_system_other_config.sql create mode 100644 src/Oms.Application/Dtos/Orders/OrderTrackUpsertDebugInput.cs create mode 100644 src/Oms.Application/Dtos/System/SystemOtherConfigCreateInput.cs create mode 100644 src/Oms.Application/Dtos/System/SystemOtherConfigListInput.cs create mode 100644 src/Oms.Application/Dtos/System/SystemOtherConfigOutput.cs create mode 100644 src/Oms.Application/Dtos/System/SystemOtherConfigUpdateInput.cs create mode 100644 src/Oms.Application/Integrations/Itfx/IItfxMetadataClient.cs create mode 100644 src/Oms.Application/Integrations/Itfx/ItfxGatewayOptions.cs create mode 100644 src/Oms.Application/Integrations/Itfx/ItfxMetadataClient.cs create mode 100644 src/Oms.Application/Integrations/Itfx/ItfxServiceExtensions.cs create mode 100644 src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackConfigProvider.cs create mode 100644 src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackSignHelper.cs create mode 100644 src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertApiRequest.cs create mode 100644 src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertResult.cs create mode 100644 src/Oms.Application/Services/System/SystemOtherConfigService.cs create mode 100644 src/Oms.Application/Shared/AllocationProviderSnapshotEnricher.cs create mode 100644 src/Oms.Domain/SystemAggregate/Consts/SystemOtherConfigCodes.cs create mode 100644 src/Oms.Domain/SystemAggregate/Entities/SystemOtherConfig.cs create mode 100644 src/Oms.Domain/SystemAggregate/Repositories/ISystemOtherConfigRepository.cs create mode 100644 src/Oms.SqlSugarCore/System/SystemOtherConfigRepository.cs create mode 100644 src/Oms.Tests/SmallBagTrackSignHelperTests.cs diff --git a/db.sql b/db.sql index d0a2ac3..3ec60e7 100644 --- a/db.sql +++ b/db.sql @@ -10,6 +10,7 @@ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; +DROP TABLE IF EXISTS `t_system_other_config`; DROP TABLE IF EXISTS `t_warehouse_address`; DROP TABLE IF EXISTS `t_notice_read`; DROP TABLE IF EXISTS `t_notice`; @@ -58,6 +59,10 @@ CREATE TABLE `t_order` ( `TrackNo` VARCHAR(64) NULL COMMENT '跟踪号', `ProductCode` VARCHAR(128) NULL COMMENT '产品编码', `ChannelCode` VARCHAR(128) NULL COMMENT '渠道编码', + `DownstreamSupplierCode` VARCHAR(64) NULL COMMENT '下游 API 服务商编码 supplierCode', + `DownstreamAppKey` VARCHAR(128) NULL COMMENT '下游 itfx 应用 AppKey', + `DownstreamAppId` INT NULL COMMENT '下游 itfx 应用 Id', + `ChannelForecastCode` VARCHAR(128) NULL COMMENT '渠道预报编码 ForecastCode', `ConsigneeCountry` VARCHAR(8) NULL COMMENT '国家二字码', `ConsigneeCountryName` VARCHAR(64) NULL COMMENT '国家中文名称', `CargoAttribute` VARCHAR(32) NULL COMMENT '货物属性:general/electronics/sensitive', @@ -828,6 +833,27 @@ CREATE TABLE `t_dict` ( KEY `idx_dict_type_sort` (`DictType`, `DictSort`, `Id`) ) COMMENT='系统字典表'; +CREATE TABLE `t_system_other_config` ( + `Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增,步长 1', + `ConfigCode` VARCHAR(64) NOT NULL COMMENT '配置编码,如 track_service', + `ConfigName` VARCHAR(128) NOT NULL COMMENT '配置名称', + `Enabled` TINYINT NOT NULL DEFAULT 0 COMMENT '是否启用:0-否,1-是', + `BaseUrl` VARCHAR(512) NULL COMMENT '服务地址', + `AppKey` VARCHAR(128) NULL COMMENT 'AppKey', + `AppSecret` VARCHAR(256) NULL COMMENT 'AppSecret', + `TimeoutSeconds` INT NOT NULL DEFAULT 30 COMMENT '超时秒数', + `Remark` VARCHAR(512) NULL COMMENT '备注', + `IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是', + `ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳', + `CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人', + `CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `UpdateUserId` INT NULL COMMENT '更新人', + `UpdateTime` DATETIME NULL COMMENT '更新时间', + PRIMARY KEY (`Id`), + UNIQUE KEY `uk_system_other_config_code` (`ConfigCode`), + KEY `idx_system_other_config_enabled` (`Enabled`, `Id`) +) COMMENT='系统其他配置表'; + CREATE TABLE `t_notice` ( `Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增,步长 1', `NoticeType` VARCHAR(16) NOT NULL COMMENT '通知类型:notice/message', diff --git a/scripts/debug-upsert-small-bag.ps1 b/scripts/debug-upsert-small-bag.ps1 new file mode 100644 index 0000000..d25c40c --- /dev/null +++ b/scripts/debug-upsert-small-bag.ps1 @@ -0,0 +1,77 @@ +# 单独调试 POST /OrderV2/UpsertSmallBag(与 SmallBagTrackOrderClient 一致) +param( + [string]$BaseUrl = "http://api.nanafa.com/track", + [string]$AppKey = "W2606001", + [string]$AppSecret = "ziK8UzlQ39m3kdL346XEBg8nljY8os52", + [string]$OrderNo = "", + [string]$Client = "TEST001" +) + +function Get-SmallBagSign { + param([string]$Key, [long]$Ts, [string]$Secret) + $signString = "appKey=$Key×tamp=$Ts&appSecret=$Secret" + $md5 = [System.Security.Cryptography.MD5]::Create() + $bytes = [System.Text.Encoding]::UTF8.GetBytes($signString) + $hash = $md5.ComputeHash($bytes) + -join ($hash | ForEach-Object { $_.ToString("X2") }) +} + +if ([string]::IsNullOrWhiteSpace($OrderNo)) { + $OrderNo = "LP-DEBUG-" + (Get-Date -Format "yyyyMMddHHmmss") +} + +$timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() +$sign = Get-SmallBagSign -Key $AppKey -Ts $timestamp -Secret $AppSecret + +$body = @{ + appKey = $AppKey + timestamp = $timestamp + sign = $sign + client = $Client + orderNo = $OrderNo + trackNo = "" + otherNo = "DEBUG-CLIENT-001" + channel = "test01" + product = "test01" + apiCode = "" + consignee = @{ + countryCode = "US" + province = "California" + postcode = "90001" + city = "Los Angeles" + email = "debug@example.com" + } +} | ConvertTo-Json -Depth 5 -Compress + +$url = ($BaseUrl.TrimEnd('/')) + "/OrderV2/UpsertSmallBag" + +Write-Host "========== UpsertSmallBag Debug ==========" -ForegroundColor Cyan +Write-Host "URL: $url" +Write-Host "Timestamp: $timestamp" +Write-Host "Sign: $sign" +Write-Host "Body:" +Write-Host ($body | ConvertFrom-Json | ConvertTo-Json -Depth 5) +Write-Host "========================================" -ForegroundColor Cyan + +try { + $response = Invoke-WebRequest -Uri $url -Method POST ` + -ContentType "application/json; charset=utf-8" ` + -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) ` + -UseBasicParsing + Write-Host "HTTP $($response.StatusCode)" -ForegroundColor Green + Write-Host $response.Content +} +catch { + if ($_.Exception.Response) { + $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream()) + $errBody = $reader.ReadToEnd() + Write-Host "HTTP $([int]$_.Exception.Response.StatusCode)" -ForegroundColor Red + if ($errBody) { Write-Host $errBody } else { Write-Host "(empty body — 多为签名/鉴权失败,请核对 appKey/appSecret 或联系轨迹平台)" } + } + else { + Write-Host $_.Exception.Message -ForegroundColor Red + } + Write-Host "" + Write-Host "Tip: start OMS and POST /order/debug/track-upsert-small-bag with optional body fields." -ForegroundColor Yellow + exit 1 +} diff --git a/sql/migrate_t_order_downstream_integration.sql b/sql/migrate_t_order_downstream_integration.sql new file mode 100644 index 0000000..e8ebf4c --- /dev/null +++ b/sql/migrate_t_order_downstream_integration.sql @@ -0,0 +1,6 @@ +-- 订单下游对接字段(排货确认渠道时固化,供 itfx 预报/轨迹使用) +ALTER TABLE `t_order` + ADD COLUMN `DownstreamSupplierCode` VARCHAR(64) NULL COMMENT '下游 API 服务商编码 supplierCode' AFTER `ChannelCode`, + ADD COLUMN `DownstreamAppKey` VARCHAR(128) NULL COMMENT '下游 itfx 应用 AppKey' AFTER `DownstreamSupplierCode`, + ADD COLUMN `DownstreamAppId` INT NULL COMMENT '下游 itfx 应用 Id' AFTER `DownstreamAppKey`, + ADD COLUMN `ChannelForecastCode` VARCHAR(128) NULL COMMENT '渠道预报编码 ForecastCode' AFTER `DownstreamAppId`; diff --git a/sql/migrate_t_system_other_config.sql b/sql/migrate_t_system_other_config.sql new file mode 100644 index 0000000..4353a91 --- /dev/null +++ b/sql/migrate_t_system_other_config.sql @@ -0,0 +1,21 @@ +-- 系统其他配置表(含 OMS 小包轨迹服务配置) +CREATE TABLE IF NOT EXISTS `t_system_other_config` ( + `Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增,步长 1', + `ConfigCode` VARCHAR(64) NOT NULL COMMENT '配置编码,如 track_service', + `ConfigName` VARCHAR(128) NOT NULL COMMENT '配置名称', + `Enabled` TINYINT NOT NULL DEFAULT 0 COMMENT '是否启用:0-否,1-是', + `BaseUrl` VARCHAR(512) NULL COMMENT '服务地址', + `AppKey` VARCHAR(128) NULL COMMENT 'AppKey', + `AppSecret` VARCHAR(256) NULL COMMENT 'AppSecret', + `TimeoutSeconds` INT NOT NULL DEFAULT 30 COMMENT '超时秒数', + `Remark` VARCHAR(512) NULL COMMENT '备注', + `IsDeleted` TINYINT NOT NULL DEFAULT 0 COMMENT '软删除:0-否,1-是', + `ConcurrencyStamp` CHAR(36) NOT NULL DEFAULT (UUID()) COMMENT '并发戳', + `CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人', + `CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `UpdateUserId` INT NULL COMMENT '更新人', + `UpdateTime` DATETIME NULL COMMENT '更新时间', + PRIMARY KEY (`Id`), + UNIQUE KEY `uk_system_other_config_code` (`ConfigCode`), + KEY `idx_system_other_config_enabled` (`Enabled`, `Id`) +) COMMENT='系统其他配置表'; diff --git a/src/Oms.Application/Dtos/Channels/SalesProductAllocationProviderSnapshotDto.cs b/src/Oms.Application/Dtos/Channels/SalesProductAllocationProviderSnapshotDto.cs index b513545..fa6baa4 100644 --- a/src/Oms.Application/Dtos/Channels/SalesProductAllocationProviderSnapshotDto.cs +++ b/src/Oms.Application/Dtos/Channels/SalesProductAllocationProviderSnapshotDto.cs @@ -44,4 +44,39 @@ public class SalesProductAllocationProviderSnapshotDto /// 指定客户主键集合,空表示全部。 /// public List CustomerIds { get; set; } = []; + + /// + /// 服务渠道编码(发布快照固化)。 + /// + public string? ChannelCode { get; set; } + + /// + /// 渠道预报编码 ForecastCode(发布快照固化)。 + /// + public string? ForecastCode { get; set; } + + /// + /// 服务商分组 Id。 + /// + public int? GroupId { get; set; } + + /// + /// itfx API 服务商编码 supplierCode。 + /// + public string? SupplierCode { get; set; } + + /// + /// itfx 应用 AppKey(AppSecret 不落快照,运行时按 AppId 读取)。 + /// + public string? AppKey { get; set; } + + /// + /// itfx 应用 Id。 + /// + public int? AppId { get; set; } + + /// + /// itfx API 服务商 Id。 + /// + public int? ApiServiceProviderId { get; set; } } diff --git a/src/Oms.Application/Dtos/Orders/OrderOutput.cs b/src/Oms.Application/Dtos/Orders/OrderOutput.cs index 7a30ac9..26c2c28 100644 --- a/src/Oms.Application/Dtos/Orders/OrderOutput.cs +++ b/src/Oms.Application/Dtos/Orders/OrderOutput.cs @@ -40,6 +40,26 @@ public class OrderOutput /// public string? ChannelCode { get; set; } + /// + /// 下游 API 服务商编码 supplierCode。 + /// + public string? DownstreamSupplierCode { get; set; } + + /// + /// 下游 itfx 应用 AppKey。 + /// + public string? DownstreamAppKey { get; set; } + + /// + /// 下游 itfx 应用 Id(用于运行时读取 AppSecret)。 + /// + public int? DownstreamAppId { get; set; } + + /// + /// 渠道预报编码 ForecastCode。 + /// + public string? ChannelForecastCode { get; set; } + /// /// 服务渠道名称。 /// @@ -75,6 +95,31 @@ public class OrderOutput /// public int? CustomerId { get; set; } + /// + /// 幂等请求标识。 + /// + public string? RequestId { get; set; } + + /// + /// 出口报关方式。 + /// + public string? ExportCustomsMode { get; set; } + + /// + /// 申报币种。 + /// + public string? DeclaredCurrency { get; set; } + + /// + /// 申报总金额。 + /// + public decimal? DeclaredSum { get; set; } + + /// + /// 包裹数量。 + /// + public int? Quantity { get; set; } + /// /// 是否 COD。 /// diff --git a/src/Oms.Application/Dtos/Orders/OrderTrackUpsertDebugInput.cs b/src/Oms.Application/Dtos/Orders/OrderTrackUpsertDebugInput.cs new file mode 100644 index 0000000..18e39b4 --- /dev/null +++ b/src/Oms.Application/Dtos/Orders/OrderTrackUpsertDebugInput.cs @@ -0,0 +1,31 @@ +namespace Oms.Application.Dtos.Orders; + +/// +/// 调试 UpsertSmallBag 入参(字段均可选,缺省使用示例值)。 +/// +public class OrderTrackUpsertDebugInput +{ + public string? Client { get; set; } + + public string? OrderNo { get; set; } + + public string? TrackNo { get; set; } + + public string? OtherNo { get; set; } + + public string? Channel { get; set; } + + public string? Product { get; set; } + + public string? ApiCode { get; set; } + + public string? CountryCode { get; set; } + + public string? Province { get; set; } + + public string? Postcode { get; set; } + + public string? City { get; set; } + + public string? Email { get; set; } +} diff --git a/src/Oms.Application/Dtos/System/SystemOtherConfigCreateInput.cs b/src/Oms.Application/Dtos/System/SystemOtherConfigCreateInput.cs new file mode 100644 index 0000000..44de4b9 --- /dev/null +++ b/src/Oms.Application/Dtos/System/SystemOtherConfigCreateInput.cs @@ -0,0 +1,47 @@ +namespace Oms.Application.Dtos.System; + +/// +/// 系统其他配置新增入参。 +/// +public class SystemOtherConfigCreateInput +{ + /// + /// 配置编码。 + /// + public string ConfigCode { get; set; } = string.Empty; + + /// + /// 配置名称。 + /// + public string ConfigName { get; set; } = string.Empty; + + /// + /// 是否启用。 + /// + public bool Enabled { get; set; } + + /// + /// 服务地址。 + /// + public string? BaseUrl { get; set; } + + /// + /// AppKey。 + /// + public string? AppKey { get; set; } + + /// + /// AppSecret。 + /// + public string? AppSecret { get; set; } + + /// + /// 超时秒数。 + /// + public int TimeoutSeconds { get; set; } = 30; + + /// + /// 备注。 + /// + public string? Remark { get; set; } +} diff --git a/src/Oms.Application/Dtos/System/SystemOtherConfigListInput.cs b/src/Oms.Application/Dtos/System/SystemOtherConfigListInput.cs new file mode 100644 index 0000000..f7dc263 --- /dev/null +++ b/src/Oms.Application/Dtos/System/SystemOtherConfigListInput.cs @@ -0,0 +1,22 @@ +namespace Oms.Application.Dtos.System; + +/// +/// 系统其他配置列表查询入参。 +/// +public class SystemOtherConfigListInput +{ + /// + /// 页码,从 1 开始。 + /// + public int Page { get; set; } = 1; + + /// + /// 每页条数。 + /// + public int PageSize { get; set; } = 30; + + /// + /// 关键词,匹配配置编码或名称。 + /// + public string? Keyword { get; set; } +} diff --git a/src/Oms.Application/Dtos/System/SystemOtherConfigOutput.cs b/src/Oms.Application/Dtos/System/SystemOtherConfigOutput.cs new file mode 100644 index 0000000..7854cf4 --- /dev/null +++ b/src/Oms.Application/Dtos/System/SystemOtherConfigOutput.cs @@ -0,0 +1,62 @@ +namespace Oms.Application.Dtos.System; + +/// +/// 系统其他配置列表项出参。 +/// +public class SystemOtherConfigOutput +{ + /// + /// 主键。 + /// + public int Id { get; set; } + + /// + /// 配置编码。 + /// + public string ConfigCode { get; set; } = string.Empty; + + /// + /// 配置名称。 + /// + public string ConfigName { get; set; } = string.Empty; + + /// + /// 是否启用。 + /// + public bool Enabled { get; set; } + + /// + /// 服务地址。 + /// + public string? BaseUrl { get; set; } + + /// + /// AppKey。 + /// + public string? AppKey { get; set; } + + /// + /// AppSecret(列表已脱敏)。 + /// + public string? AppSecret { get; set; } + + /// + /// 超时秒数。 + /// + public int TimeoutSeconds { get; set; } + + /// + /// 备注。 + /// + public string? Remark { get; set; } + + /// + /// 创建时间。 + /// + public DateTime CreateTime { get; set; } + + /// + /// 更新时间。 + /// + public DateTime? UpdateTime { get; set; } +} diff --git a/src/Oms.Application/Dtos/System/SystemOtherConfigUpdateInput.cs b/src/Oms.Application/Dtos/System/SystemOtherConfigUpdateInput.cs new file mode 100644 index 0000000..0d3f2c8 --- /dev/null +++ b/src/Oms.Application/Dtos/System/SystemOtherConfigUpdateInput.cs @@ -0,0 +1,12 @@ +namespace Oms.Application.Dtos.System; + +/// +/// 系统其他配置更新入参。 +/// +public class SystemOtherConfigUpdateInput : SystemOtherConfigCreateInput +{ + /// + /// 主键。 + /// + public int Id { get; set; } +} diff --git a/src/Oms.Application/Integrations/Itfx/IItfxMetadataClient.cs b/src/Oms.Application/Integrations/Itfx/IItfxMetadataClient.cs new file mode 100644 index 0000000..c960b1f --- /dev/null +++ b/src/Oms.Application/Integrations/Itfx/IItfxMetadataClient.cs @@ -0,0 +1,20 @@ +namespace Oms.Application.Integrations.Itfx; + +public sealed class ItfxAppMetadata +{ + public string AppKey { get; set; } = string.Empty; +} + +public sealed class ItfxServiceProviderMetadata +{ + public string Code { get; set; } = string.Empty; +} + +public interface IItfxMetadataClient +{ + Task GetAppAsync(int appId, CancellationToken cancellationToken = default); + + Task GetServiceProviderAsync( + int serviceProviderId, + CancellationToken cancellationToken = default); +} diff --git a/src/Oms.Application/Integrations/Itfx/ItfxGatewayOptions.cs b/src/Oms.Application/Integrations/Itfx/ItfxGatewayOptions.cs new file mode 100644 index 0000000..19366bd --- /dev/null +++ b/src/Oms.Application/Integrations/Itfx/ItfxGatewayOptions.cs @@ -0,0 +1,16 @@ +namespace Oms.Application.Integrations.Itfx; + +/// +/// itfx 网关配置。 +/// +public class ItfxGatewayOptions +{ + public const string SectionName = "Oms:Itfx"; + + /// + /// itfx 服务根地址,如 http://api.nanafa.com/itfx。 + /// + public string BaseUrl { get; set; } = "http://api.nanafa.com/itfx"; + + public int TimeoutSeconds { get; set; } = 30; +} diff --git a/src/Oms.Application/Integrations/Itfx/ItfxMetadataClient.cs b/src/Oms.Application/Integrations/Itfx/ItfxMetadataClient.cs new file mode 100644 index 0000000..79aaf7b --- /dev/null +++ b/src/Oms.Application/Integrations/Itfx/ItfxMetadataClient.cs @@ -0,0 +1,135 @@ +using System.Net.Http.Headers; +using System.Text.Json; +using Fake.DependencyInjection; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Oms.Application.Integrations.Itfx; + +internal sealed class ItfxMetadataClient( + IHttpClientFactory httpClientFactory, + IOptions options, + IHttpContextAccessor httpContextAccessor, + ILogger logger) : IItfxMetadataClient, IScopedDependency +{ + public const string HttpClientName = nameof(ItfxMetadataClient); + + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + public Task GetAppAsync(int appId, CancellationToken cancellationToken = default) => + GetAsync($"/App/GetInfo?id={appId}", payload => + { + var appKey = ReadString(payload, "appKey", "AppKey"); + return string.IsNullOrWhiteSpace(appKey) + ? null + : new ItfxAppMetadata { AppKey = appKey.Trim() }; + }, cancellationToken); + + public Task GetServiceProviderAsync( + int serviceProviderId, + CancellationToken cancellationToken = default) => + GetAsync( + $"/ServiceProvider/GetServiceProvider?serviceProviderId={serviceProviderId}", + payload => + { + var code = ReadString(payload, "code", "Code", "serviceProviderCode", "ServiceProviderCode"); + return string.IsNullOrWhiteSpace(code) + ? null + : new ItfxServiceProviderMetadata { Code = code.Trim() }; + }, + cancellationToken); + + private async Task GetAsync( + string relativeUrl, + Func map, + CancellationToken cancellationToken) + where T : class + { + var settings = options.Value; + if (string.IsNullOrWhiteSpace(settings.BaseUrl)) + { + logger.LogDebug("Itfx BaseUrl not configured, skip {Url}", relativeUrl); + return null; + } + + var client = httpClientFactory.CreateClient(HttpClientName); + client.Timeout = TimeSpan.FromSeconds(Math.Max(5, settings.TimeoutSeconds)); + ForwardAuthorization(client); + + var url = $"{settings.BaseUrl.Trim().TrimEnd('/')}{relativeUrl}"; + try + { + using var response = await client.GetAsync(url, cancellationToken); + if (!response.IsSuccessStatusCode) + { + logger.LogWarning("Itfx metadata request failed {Url}: {StatusCode}", url, (int)response.StatusCode); + return null; + } + + await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); + using var document = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); + var payload = UnwrapPayload(document.RootElement); + return payload.HasValue ? map(payload.Value) : null; + } + catch (Exception ex) + { + logger.LogWarning(ex, "Itfx metadata request error {Url}", url); + return null; + } + } + + private void ForwardAuthorization(HttpClient client) + { + var authorization = httpContextAccessor.HttpContext?.Request.Headers.Authorization.ToString(); + if (!string.IsNullOrWhiteSpace(authorization)) + { + client.DefaultRequestHeaders.Authorization = AuthenticationHeaderValue.Parse(authorization); + } + } + + private static JsonElement? UnwrapPayload(JsonElement root) + { + if (root.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined) + { + return null; + } + + if (root.TryGetProperty("data", out var data) && data.ValueKind is not JsonValueKind.Null and not JsonValueKind.Undefined) + { + return data; + } + + if (root.TryGetProperty("Data", out var dataPascal) && dataPascal.ValueKind is not JsonValueKind.Null and not JsonValueKind.Undefined) + { + return dataPascal; + } + + return root; + } + + private static string? ReadString(JsonElement payload, params string[] names) + { + foreach (var name in names) + { + if (payload.TryGetProperty(name, out var property)) + { + var value = property.ValueKind switch + { + JsonValueKind.String => property.GetString(), + JsonValueKind.Number => property.GetRawText(), + _ => null + }; + if (!string.IsNullOrWhiteSpace(value)) + { + return value; + } + } + } + + return null; + } +} diff --git a/src/Oms.Application/Integrations/Itfx/ItfxServiceExtensions.cs b/src/Oms.Application/Integrations/Itfx/ItfxServiceExtensions.cs new file mode 100644 index 0000000..f326f3d --- /dev/null +++ b/src/Oms.Application/Integrations/Itfx/ItfxServiceExtensions.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Oms.Application.Integrations.Itfx; + +public static class ItfxServiceExtensions +{ + public static IServiceCollection AddOmsItfxIntegration(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration.GetSection(ItfxGatewayOptions.SectionName)); + services.AddHttpClient(ItfxMetadataClient.HttpClientName); + services.AddScoped(); + return services; + } +} diff --git a/src/Oms.Application/Integrations/SmallBagTrack/ISmallBagTrackOrderClient.cs b/src/Oms.Application/Integrations/SmallBagTrack/ISmallBagTrackOrderClient.cs index 2f27dc1..bc339a2 100644 --- a/src/Oms.Application/Integrations/SmallBagTrack/ISmallBagTrackOrderClient.cs +++ b/src/Oms.Application/Integrations/SmallBagTrack/ISmallBagTrackOrderClient.cs @@ -3,4 +3,9 @@ namespace Oms.Application.Integrations.SmallBagTrack; public interface ISmallBagTrackOrderClient { Task UpsertAsync(SmallBagTrackUpsertRequest request, CancellationToken cancellationToken = default); + + /// + /// 同步调用并返回完整请求/响应(仅调试)。 + /// + Task UpsertDebugAsync(SmallBagTrackUpsertRequest request, CancellationToken cancellationToken = default); } diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackConfigProvider.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackConfigProvider.cs new file mode 100644 index 0000000..c6e271e --- /dev/null +++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackConfigProvider.cs @@ -0,0 +1,41 @@ +using Fake.DependencyInjection; +using Microsoft.Extensions.Options; +using Oms.Domain.SystemAggregate.Consts; +using Oms.Domain.SystemAggregate.Repositories; + +namespace Oms.Application.Integrations.SmallBagTrack; + +/// +/// 小包轨迹运行时配置读取。 +/// +public interface ISmallBagTrackConfigProvider +{ + Task GetAsync(CancellationToken cancellationToken = default); +} + +internal sealed class SmallBagTrackConfigProvider( + ISystemOtherConfigRepository repository, + IOptions options) : ISmallBagTrackConfigProvider, IScopedDependency +{ + public async Task GetAsync(CancellationToken cancellationToken = default) + { + var fallback = options.Value; + var entity = await repository.GetByConfigCodeAsync(SystemOtherConfigCodes.TrackService, cancellationToken); + if (entity == null) + { + return fallback; + } + + return new SmallBagTrackOptions + { + Enabled = entity.Enabled, + BaseUrl = Coalesce(entity.BaseUrl, fallback.BaseUrl), + AppKey = Coalesce(entity.AppKey, fallback.AppKey), + AppSecret = Coalesce(entity.AppSecret, fallback.AppSecret), + TimeoutSeconds = entity.TimeoutSeconds > 0 ? entity.TimeoutSeconds : fallback.TimeoutSeconds + }; + } + + private static string Coalesce(string? primary, string fallback) => + string.IsNullOrWhiteSpace(primary) ? fallback : primary.Trim(); +} diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOptions.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOptions.cs index 4475d18..c4aed55 100644 --- a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOptions.cs +++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOptions.cs @@ -17,6 +17,16 @@ public class SmallBagTrackOptions /// public string BaseUrl { get; set; } = string.Empty; + /// + /// 应用唯一标识(appKey)。 + /// + public string AppKey { get; set; } = string.Empty; + + /// + /// 应用密钥(appSecret),用于签名,勿泄露。 + /// + public string AppSecret { get; set; } = string.Empty; + /// /// HTTP 超时(秒)。 /// diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderClient.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderClient.cs index 7663a52..bb708a9 100644 --- a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderClient.cs +++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderClient.cs @@ -1,41 +1,95 @@ using System.Net.Http.Json; +using System.Text.Json; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace Oms.Application.Integrations.SmallBagTrack; internal sealed class SmallBagTrackOrderClient( IHttpClientFactory httpClientFactory, - IOptions options, + ISmallBagTrackConfigProvider configProvider, ILogger logger) : ISmallBagTrackOrderClient { + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }; + public const string HttpClientName = nameof(SmallBagTrackOrderClient); public async Task UpsertAsync(SmallBagTrackUpsertRequest request, CancellationToken cancellationToken = default) { - var settings = options.Value; - if (!settings.Enabled || string.IsNullOrWhiteSpace(settings.BaseUrl)) + var result = await UpsertDebugAsync(request, cancellationToken); + if (result.Skipped) { - logger.LogDebug("UpsertSmallBag skipped: TrackService disabled or BaseUrl empty"); + if (!string.IsNullOrWhiteSpace(result.SkipReason)) + { + logger.LogDebug("UpsertSmallBag skipped for order {OrderNo}: {Reason}", request.OrderNo, result.SkipReason); + } + return; } - var url = $"{settings.BaseUrl.Trim().TrimEnd('/')}/OrderV2/UpsertSmallBag"; - var client = httpClientFactory.CreateClient(HttpClientName); - client.Timeout = TimeSpan.FromSeconds(Math.Max(5, settings.TimeoutSeconds)); - - using var response = await client.PostAsJsonAsync(url, request, cancellationToken); - if (!response.IsSuccessStatusCode) + if (!result.Success) { - var body = await response.Content.ReadAsStringAsync(cancellationToken); logger.LogWarning( "UpsertSmallBag failed for order {OrderNo}: {StatusCode} {Body}", request.OrderNo, - (int)response.StatusCode, - body); + result.StatusCode, + result.ResponseBody); return; } logger.LogInformation("UpsertSmallBag succeeded for order {OrderNo}", request.OrderNo); } + + public async Task UpsertDebugAsync( + SmallBagTrackUpsertRequest request, + CancellationToken cancellationToken = default) + { + var settings = await configProvider.GetAsync(cancellationToken); + if (!settings.Enabled) + { + return Skipped("TrackService disabled"); + } + + if (string.IsNullOrWhiteSpace(settings.BaseUrl)) + { + return Skipped("BaseUrl empty"); + } + + if (string.IsNullOrWhiteSpace(settings.AppKey) || string.IsNullOrWhiteSpace(settings.AppSecret)) + { + return Skipped("AppKey or AppSecret not configured"); + } + + var appKey = settings.AppKey.Trim(); + var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + var sign = SmallBagTrackSignHelper.GenerateSign(appKey, timestamp, settings.AppSecret.Trim()); + var payload = SmallBagTrackUpsertApiRequest.FromBusinessRequest(request, appKey, timestamp, sign); + var url = $"{settings.BaseUrl.Trim().TrimEnd('/')}/OrderV2/UpsertSmallBag"; + var requestJson = JsonSerializer.Serialize(payload, JsonOptions); + + var client = httpClientFactory.CreateClient(HttpClientName); + client.Timeout = TimeSpan.FromSeconds(Math.Max(5, settings.TimeoutSeconds)); + + using var response = await client.PostAsJsonAsync(url, payload, cancellationToken); + var body = await response.Content.ReadAsStringAsync(cancellationToken); + return new SmallBagTrackUpsertResult + { + Skipped = false, + Success = response.IsSuccessStatusCode, + Url = url, + RequestJson = requestJson, + StatusCode = (int)response.StatusCode, + ResponseBody = body + }; + } + + private static SmallBagTrackUpsertResult Skipped(string reason) => + new() + { + Skipped = true, + SkipReason = reason + }; } diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackServiceExtensions.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackServiceExtensions.cs index cc20a45..74cc34b 100644 --- a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackServiceExtensions.cs +++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackServiceExtensions.cs @@ -9,7 +9,8 @@ public static class SmallBagTrackServiceExtensions { services.Configure(configuration.GetSection(SmallBagTrackOptions.SectionName)); services.AddHttpClient(SmallBagTrackOrderClient.HttpClientName); - services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); services.AddSingleton(); return services; } diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackSignHelper.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackSignHelper.cs new file mode 100644 index 0000000..87ee043 --- /dev/null +++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackSignHelper.cs @@ -0,0 +1,29 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Oms.Application.Integrations.SmallBagTrack; + +/// +/// 轨迹服务 API 签名:appKey={}&timestamp={}&appSecret={} → MD5 大写。 +/// +internal static class SmallBagTrackSignHelper +{ + public static string GenerateSign(string appKey, long timestamp, string appSecret) + { + var signString = $"appKey={appKey}×tamp={timestamp}&appSecret={appSecret}"; + return CalculateMd5Hash(signString); + } + + private static string CalculateMd5Hash(string input) + { + var inputBytes = Encoding.UTF8.GetBytes(input); + var hashBytes = MD5.HashData(inputBytes); + var sb = new StringBuilder(hashBytes.Length * 2); + foreach (var b in hashBytes) + { + sb.Append(b.ToString("X2")); + } + + return sb.ToString(); + } +} diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertApiRequest.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertApiRequest.cs new file mode 100644 index 0000000..f1bef48 --- /dev/null +++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertApiRequest.cs @@ -0,0 +1,49 @@ +namespace Oms.Application.Integrations.SmallBagTrack; + +/// +/// POST /OrderV2/UpsertSmallBag 完整请求体(含公共参数 + 业务字段)。 +/// +internal sealed class SmallBagTrackUpsertApiRequest +{ + public string AppKey { get; set; } = string.Empty; + + public long Timestamp { get; set; } + + public string Sign { get; set; } = string.Empty; + + public string Client { get; set; } = string.Empty; + + public string OrderNo { get; set; } = string.Empty; + + public string TrackNo { get; set; } = string.Empty; + + public string OtherNo { get; set; } = string.Empty; + + public string Channel { get; set; } = string.Empty; + + public string Product { get; set; } = string.Empty; + + public string ApiCode { get; set; } = string.Empty; + + public SmallBagTrackConsigneeRequest Consignee { get; set; } = new(); + + public static SmallBagTrackUpsertApiRequest FromBusinessRequest( + SmallBagTrackUpsertRequest request, + string appKey, + long timestamp, + string sign) => + new() + { + AppKey = appKey, + Timestamp = timestamp, + Sign = sign, + Client = request.Client, + OrderNo = request.OrderNo, + TrackNo = request.TrackNo, + OtherNo = request.OtherNo, + Channel = request.Channel, + Product = request.Product, + ApiCode = request.ApiCode, + Consignee = request.Consignee + }; +} diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertResult.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertResult.cs new file mode 100644 index 0000000..f200d8a --- /dev/null +++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertResult.cs @@ -0,0 +1,21 @@ +namespace Oms.Application.Integrations.SmallBagTrack; + +/// +/// UpsertSmallBag 调用结果(调试用)。 +/// +public class SmallBagTrackUpsertResult +{ + public bool Skipped { get; set; } + + public string? SkipReason { get; set; } + + public bool Success { 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; +} diff --git a/src/Oms.Application/OmsApplicationModule.cs b/src/Oms.Application/OmsApplicationModule.cs index 73499e3..f8ec1ad 100644 --- a/src/Oms.Application/OmsApplicationModule.cs +++ b/src/Oms.Application/OmsApplicationModule.cs @@ -3,6 +3,7 @@ using Fake.ObjectMapping.AutoMapper; using Fake.Security.Claims; using Microsoft.Extensions.DependencyInjection; using Oms.Application.Integrations.SmallBagTrack; +using Oms.Application.Integrations.Itfx; using Oms.Domain; namespace Oms.Application; @@ -24,5 +25,6 @@ public class OmsApplicationModule : FakeModule options.ScanProfiles(); }); context.Services.AddOmsSmallBagTrack(context.Services.GetConfiguration()); + context.Services.AddOmsItfxIntegration(context.Services.GetConfiguration()); } } diff --git a/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs b/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs index 3c63596..18fcd22 100644 --- a/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs +++ b/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs @@ -175,6 +175,7 @@ public partial class SalesProductService KpiAreaRepository, WeightRuleRepository, WeightRoundingRepository); + await AllocationProviderSnapshotEnricher.EnrichAsync(rulesSnapshot.Providers); return rulesSnapshot; } diff --git a/src/Oms.Application/Services/Channels/SalesProductService.cs b/src/Oms.Application/Services/Channels/SalesProductService.cs index 0efc684..b6a023b 100644 --- a/src/Oms.Application/Services/Channels/SalesProductService.cs +++ b/src/Oms.Application/Services/Channels/SalesProductService.cs @@ -23,6 +23,8 @@ public partial class SalesProductService : ApplicationService private IKpiAreaRepository KpiAreaRepository => LazyServiceProvider.GetRequiredService(); private IWeightRuleRepository WeightRuleRepository => LazyServiceProvider.GetRequiredService(); private IWeightRoundingRepository WeightRoundingRepository => LazyServiceProvider.GetRequiredService(); + private AllocationProviderSnapshotEnricher AllocationProviderSnapshotEnricher => + LazyServiceProvider.GetRequiredService(); /// /// 查询销售产品列表。 diff --git a/src/Oms.Application/Services/Orders/OrderService.Helpers.cs b/src/Oms.Application/Services/Orders/OrderService.Helpers.cs index a5f72df..137f2e9 100644 --- a/src/Oms.Application/Services/Orders/OrderService.Helpers.cs +++ b/src/Oms.Application/Services/Orders/OrderService.Helpers.cs @@ -93,6 +93,10 @@ public partial class OrderService ? product.ProductName : null, ChannelCode = order.ChannelCode, + DownstreamSupplierCode = order.DownstreamSupplierCode, + DownstreamAppKey = order.DownstreamAppKey, + DownstreamAppId = order.DownstreamAppId, + ChannelForecastCode = order.ChannelForecastCode, ChannelName = !string.IsNullOrWhiteSpace(order.ChannelCode) && channelMap.TryGetValue(order.ChannelCode.Trim(), out var channel) ? channel.ChannelName @@ -105,6 +109,11 @@ public partial class OrderService ? warehouse.WarehouseName : null, CustomerId = order.CustomerId, + RequestId = order.RequestId, + ExportCustomsMode = order.ExportCustomsMode, + DeclaredCurrency = order.DeclaredCurrency, + DeclaredSum = order.DeclaredSum, + Quantity = order.Quantity, IsCod = order.IsCod > 0, CodCurrency = order.CodCurrency, CodSum = order.CodSum, diff --git a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs index 1d3147b..e862146 100644 --- a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs +++ b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs @@ -1,4 +1,5 @@ using Fake.Application; +using Microsoft.AspNetCore.Mvc; using Oms.Application.Dtos.Channels; using Oms.Application.Dtos.Orders; using Oms.Application.Integrations.SmallBagTrack; @@ -28,6 +29,8 @@ public partial class OrderService private IWeightRuleRepository WeightRuleRepository => LazyServiceProvider.GetRequiredService(); private IWeightRoundingRepository WeightRoundingRepository => LazyServiceProvider.GetRequiredService(); private IKpiAreaRepository KpiAreaRepository => LazyServiceProvider.GetRequiredService(); + private AllocationProviderSnapshotEnricher AllocationProviderSnapshotEnricher => + LazyServiceProvider.GetRequiredService(); private async Task CreateParcelCoreAsync(OrderParcelCreateInput input) { @@ -471,6 +474,7 @@ public partial class OrderService } var selectedChannel = AllocationResolver.SelectChannel(scheme, candidates); + var selectedCandidate = candidates.First(x => x.Channel.Id == selectedChannel.Id); var channelPriceBindingFinal = await ResolveChannelPriceBindingAsync(selectedChannel.Id, bindTime); var payableFees = ParcelBillingCalculator.CalculatePayableFees( channelPriceBindingFinal.Prices, @@ -489,6 +493,10 @@ public partial class OrderService } order.ChannelCode = selectedChannel.ChannelCode; + AllocationProviderSnapshotEnricher.ApplyDownstreamFieldsToOrder( + order, + selectedCandidate.Provider, + selectedChannel); if (payableFees.Count > 0) { await OrderRepository.DeletePayableFeesByStageAsync(order.Id, chargingStage); @@ -500,29 +508,39 @@ public partial class OrderService SalesProduct product, SalesProductRulesPublishSnapshotDto rulesSnapshot) { + List providers; if (rulesSnapshot.Providers.Count > 0) { - return rulesSnapshot.Providers; + providers = rulesSnapshot.Providers; + if (providers.Any(AllocationProviderSnapshotEnricher.NeedsDownstreamEnrichment)) + { + await AllocationProviderSnapshotEnricher.EnrichAsync(providers); + } + } + else + { + var entities = await SalesProductRepository.GetAllocationProvidersAsync(product.Id); + var channelIds = entities.Where(x => x.ServiceChannelId.HasValue).Select(x => x.ServiceChannelId!.Value).Distinct().ToArray(); + var channels = channelIds.Length == 0 + ? [] + : await ServiceChannelRepository.GetListAsync(x => channelIds.Contains(x.Id)); + var channelMap = channels.ToDictionary(x => x.Id, x => x.ChannelName); + + providers = entities.Select(provider => PublishSnapshotSlimmer.SlimAllocationProvider(new SalesProductAllocationProviderOutput + { + Id = provider.Id, + SalesProductId = provider.SalesProductId, + SerialNumber = provider.SerialNumber, + ProviderName = provider.ProviderName, + ServiceChannelId = provider.ServiceChannelId, + ServiceChannelName = provider.ServiceChannelId.HasValue && channelMap.TryGetValue(provider.ServiceChannelId.Value, out var name) ? name : null, + Weight = provider.Weight, + CustomerIds = OmsDocHelper.DeserializeJson>(provider.CustomerIds) ?? [] + })).ToList(); + await AllocationProviderSnapshotEnricher.EnrichAsync(providers); } - var entities = await SalesProductRepository.GetAllocationProvidersAsync(product.Id); - var channelIds = entities.Where(x => x.ServiceChannelId.HasValue).Select(x => x.ServiceChannelId!.Value).Distinct().ToArray(); - var channels = channelIds.Length == 0 - ? [] - : await ServiceChannelRepository.GetListAsync(x => channelIds.Contains(x.Id)); - var channelMap = channels.ToDictionary(x => x.Id, x => x.ChannelName); - - return entities.Select(provider => PublishSnapshotSlimmer.SlimAllocationProvider(new SalesProductAllocationProviderOutput - { - Id = provider.Id, - SalesProductId = provider.SalesProductId, - SerialNumber = provider.SerialNumber, - ProviderName = provider.ProviderName, - ServiceChannelId = provider.ServiceChannelId, - ServiceChannelName = provider.ServiceChannelId.HasValue && channelMap.TryGetValue(provider.ServiceChannelId.Value, out var name) ? name : null, - Weight = provider.Weight, - CustomerIds = OmsDocHelper.DeserializeJson>(provider.CustomerIds) ?? [] - })).ToList(); + return providers; } private sealed class ChannelPriceBinding @@ -1425,14 +1443,47 @@ public partial class OrderService Customer? customer, OrderParcelReceiverInput? receiver) { - var apiCode = string.Empty; - if (!string.IsNullOrWhiteSpace(order.ChannelCode)) + var request = await BuildSmallBagTrackUpsertRequestAsync(order, receiver); + SmallBagTrackOrderNotifier.Enqueue(request); + } + + /// + /// 按订单同步轨迹(UpsertSmallBag),返回完整请求与响应便于联调。 + /// + [HttpPost("/order/track-upsert-small-bag")] + public async Task TrackUpsertSmallBagAsync(OrderActionInput input) + { + var order = await GetOrderOrThrowAsync(input.Id); + if (string.IsNullOrWhiteSpace(order.ServiceNo)) + { + throw new BusinessException("订单运单号为空,无法同步轨迹"); + } + + var request = await BuildSmallBagTrackUpsertRequestAsync(order); + return await SmallBagTrackOrderClient.UpsertDebugAsync(request); + } + + private async Task BuildSmallBagTrackUpsertRequestAsync( + Order order, + OrderParcelReceiverInput? receiverInput = null) + { + Customer? customer = null; + if (order.CustomerId.HasValue) + { + customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value); + } + + var recipients = await OrderRepository.GetRecipientsByOrderIdsAsync([order.Id]); + var recipient = recipients.FirstOrDefault(); + + var apiCode = order.ChannelForecastCode?.Trim() ?? string.Empty; + if (string.IsNullOrWhiteSpace(apiCode) && !string.IsNullOrWhiteSpace(order.ChannelCode)) { var channel = await ServiceChannelRepository.GetByChannelCodeAsync(order.ChannelCode.Trim()); apiCode = channel?.ForecastCode?.Trim() ?? string.Empty; } - var request = new SmallBagTrackUpsertRequest + return new SmallBagTrackUpsertRequest { Client = customer?.CustomerCode?.Trim() ?? string.Empty, OrderNo = order.ServiceNo, @@ -1443,15 +1494,46 @@ public partial class OrderService ApiCode = apiCode, Consignee = new SmallBagTrackConsigneeRequest { - CountryCode = receiver?.CountryCode?.Trim() ?? order.ConsigneeCountry?.Trim() ?? string.Empty, - Province = receiver?.Province?.Trim() ?? string.Empty, - Postcode = receiver?.ZipCode?.Trim() ?? string.Empty, - City = receiver?.City?.Trim() ?? string.Empty, - Email = receiver?.Email?.Trim() ?? string.Empty + CountryCode = receiverInput?.CountryCode?.Trim() ?? order.ConsigneeCountry?.Trim() ?? string.Empty, + Province = receiverInput?.Province?.Trim() ?? recipient?.Province?.Trim() ?? string.Empty, + Postcode = receiverInput?.ZipCode?.Trim() ?? recipient?.ZipCode?.Trim() ?? string.Empty, + City = receiverInput?.City?.Trim() ?? recipient?.City?.Trim() ?? string.Empty, + Email = receiverInput?.Email?.Trim() ?? recipient?.Email?.Trim() ?? string.Empty + } + }; + } + + private ISmallBagTrackOrderClient SmallBagTrackOrderClient => + LazyServiceProvider.GetRequiredService(); + + /// + /// 调试:同步调用轨迹 UpsertSmallBag,返回完整请求与响应(Swagger 可测)。 + /// + [HttpPost("/order/debug/track-upsert-small-bag")] + public Task DebugTrackUpsertSmallBagAsync([FromBody] OrderTrackUpsertDebugInput? input) + { + input ??= new OrderTrackUpsertDebugInput(); + var suffix = DateTime.Now.ToString("yyyyMMddHHmmss"); + var request = new SmallBagTrackUpsertRequest + { + Client = string.IsNullOrWhiteSpace(input.Client) ? "TEST001" : input.Client.Trim(), + OrderNo = string.IsNullOrWhiteSpace(input.OrderNo) ? $"LP-DEBUG-{suffix}" : input.OrderNo.Trim(), + TrackNo = input.TrackNo?.Trim() ?? string.Empty, + OtherNo = string.IsNullOrWhiteSpace(input.OtherNo) ? $"DEBUG-{suffix}" : input.OtherNo.Trim(), + Channel = string.IsNullOrWhiteSpace(input.Channel) ? "test01" : input.Channel.Trim(), + Product = string.IsNullOrWhiteSpace(input.Product) ? "test01" : input.Product.Trim(), + ApiCode = input.ApiCode?.Trim() ?? string.Empty, + Consignee = new SmallBagTrackConsigneeRequest + { + CountryCode = string.IsNullOrWhiteSpace(input.CountryCode) ? "US" : input.CountryCode.Trim(), + Province = input.Province?.Trim() ?? "California", + Postcode = input.Postcode?.Trim() ?? "90001", + City = input.City?.Trim() ?? "Los Angeles", + Email = input.Email?.Trim() ?? "debug@example.com" } }; - SmallBagTrackOrderNotifier.Enqueue(request); + return SmallBagTrackOrderClient.UpsertDebugAsync(request); } /// diff --git a/src/Oms.Application/Services/System/SystemOtherConfigService.cs b/src/Oms.Application/Services/System/SystemOtherConfigService.cs new file mode 100644 index 0000000..7f88981 --- /dev/null +++ b/src/Oms.Application/Services/System/SystemOtherConfigService.cs @@ -0,0 +1,197 @@ +using System.Linq.Expressions; +using Fake.Application; +using Microsoft.AspNetCore.Mvc; +using Oms.Application.Dtos.Shared; +using Oms.Application.Dtos.System; +using Oms.Application.Shared; +using Oms.Domain.SystemAggregate.Consts; +using Oms.Domain.SystemAggregate.Entities; +using Oms.Domain.SystemAggregate.Repositories; + +namespace Oms.Application.Services.System; + +/// +/// 系统其他配置 API 服务。 +/// +[ApiExplorerSettings(GroupName = "OMS-System")] +public class SystemOtherConfigService : ApplicationService +{ + private const string SecretMask = "******"; + + private ISystemOtherConfigRepository SystemOtherConfigRepository => + LazyServiceProvider.GetRequiredService(); + + /// + /// 查询系统其他配置列表。 + /// + [HttpGet("/system/other-config/list")] + public async Task> GetListAsync([FromQuery] SystemOtherConfigListInput input) + { + var predicate = BuildListPredicate(input); + var pageIndex = OmsDocHelper.NormalizePage(input.Page); + var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize); + var items = await SystemOtherConfigRepository.GetPagedListAsync( + predicate, + pageIndex, + pageSize, + new Dictionary { [nameof(SystemOtherConfig.Id)] = false }); + var total = await SystemOtherConfigRepository.CountAsync(predicate); + + return new OmsPageResult + { + Items = items.Select(x => MapOutput(x, maskSecret: true)).ToList(), + Total = total + }; + } + + /// + /// 查询系统其他配置详情。 + /// + [HttpGet("/system/other-config/detail")] + public async Task GetDetailAsync([FromQuery] int id) + { + var entity = await GetConfigOrThrowAsync(id); + return MapOutput(entity, maskSecret: false); + } + + /// + /// 新增系统其他配置。 + /// + [HttpPost("/system/other-config/create")] + public async Task CreateAsync(SystemOtherConfigCreateInput input) + { + await ValidateInputAsync(input, null); + await SystemOtherConfigRepository.InsertAsync(new SystemOtherConfig + { + ConfigCode = input.ConfigCode.Trim(), + ConfigName = input.ConfigName.Trim(), + Enabled = input.Enabled, + BaseUrl = NormalizeOptional(input.BaseUrl), + AppKey = NormalizeOptional(input.AppKey), + AppSecret = NormalizeOptional(input.AppSecret), + TimeoutSeconds = NormalizeTimeoutSeconds(input.TimeoutSeconds), + Remark = NormalizeOptional(input.Remark) + }); + } + + /// + /// 更新系统其他配置。 + /// + [HttpPost("/system/other-config/update")] + public async Task UpdateAsync(SystemOtherConfigUpdateInput input) + { + var entity = await GetConfigOrThrowAsync(input.Id); + await ValidateInputAsync(input, entity.Id); + entity.ConfigCode = input.ConfigCode.Trim(); + entity.ConfigName = input.ConfigName.Trim(); + entity.Enabled = input.Enabled; + entity.BaseUrl = NormalizeOptional(input.BaseUrl); + entity.AppKey = NormalizeOptional(input.AppKey); + if (!ShouldKeepExistingSecret(input.AppSecret)) + { + entity.AppSecret = NormalizeOptional(input.AppSecret); + } + + entity.TimeoutSeconds = NormalizeTimeoutSeconds(input.TimeoutSeconds); + entity.Remark = NormalizeOptional(input.Remark); + await SystemOtherConfigRepository.UpdateAsync(entity); + } + + /// + /// 删除系统其他配置。 + /// + [HttpPost("/system/other-config/delete")] + public async Task DeleteAsync(IdInput input) + { + var entity = await GetConfigOrThrowAsync(input.Id); + await SystemOtherConfigRepository.DeleteAsync(entity); + } + + private static Expression> BuildListPredicate(SystemOtherConfigListInput input) + { + var keyword = input.Keyword?.Trim(); + return x => + string.IsNullOrWhiteSpace(keyword) || + x.ConfigCode.Contains(keyword) || + x.ConfigName.Contains(keyword); + } + + private async Task GetConfigOrThrowAsync(int id) + { + var entity = await SystemOtherConfigRepository.FirstOrDefaultAsync(x => x.Id == id); + if (entity == null) + { + throw new BusinessException("配置不存在"); + } + + return entity; + } + + private async Task ValidateInputAsync(SystemOtherConfigCreateInput input, int? excludeId) + { + if (string.IsNullOrWhiteSpace(input.ConfigCode)) + { + throw new BusinessException("配置编码不能为空"); + } + + if (string.IsNullOrWhiteSpace(input.ConfigName)) + { + throw new BusinessException("配置名称不能为空"); + } + + var configCode = input.ConfigCode.Trim(); + var existed = await SystemOtherConfigRepository.GetByConfigCodeAsync(configCode); + if (existed != null && existed.Id != excludeId) + { + throw new BusinessException("配置编码已存在"); + } + + if (string.Equals(configCode, SystemOtherConfigCodes.TrackService, StringComparison.OrdinalIgnoreCase)) + { + if (string.IsNullOrWhiteSpace(input.BaseUrl)) + { + throw new BusinessException("轨迹服务地址不能为空"); + } + + if (string.IsNullOrWhiteSpace(input.AppKey)) + { + throw new BusinessException("轨迹服务 AppKey 不能为空"); + } + + if (excludeId == null && ShouldKeepExistingSecret(input.AppSecret)) + { + throw new BusinessException("轨迹服务 AppSecret 不能为空"); + } + } + } + + private static SystemOtherConfigOutput MapOutput(SystemOtherConfig entity, bool maskSecret) + { + return new SystemOtherConfigOutput + { + Id = entity.Id, + ConfigCode = entity.ConfigCode, + ConfigName = entity.ConfigName, + Enabled = entity.Enabled, + BaseUrl = entity.BaseUrl, + AppKey = entity.AppKey, + AppSecret = maskSecret ? MaskSecret(entity.AppSecret) : entity.AppSecret, + TimeoutSeconds = entity.TimeoutSeconds, + Remark = entity.Remark, + CreateTime = entity.CreateTime, + UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime) + }; + } + + private static string? MaskSecret(string? secret) => + string.IsNullOrWhiteSpace(secret) ? secret : SecretMask; + + private static bool ShouldKeepExistingSecret(string? secret) => + string.IsNullOrWhiteSpace(secret) || string.Equals(secret.Trim(), SecretMask, StringComparison.Ordinal); + + private static string? NormalizeOptional(string? value) => + string.IsNullOrWhiteSpace(value) ? null : value.Trim(); + + private static int NormalizeTimeoutSeconds(int timeoutSeconds) => + timeoutSeconds <= 0 ? 30 : timeoutSeconds; +} diff --git a/src/Oms.Application/Shared/AllocationProviderSnapshotEnricher.cs b/src/Oms.Application/Shared/AllocationProviderSnapshotEnricher.cs new file mode 100644 index 0000000..60117d2 --- /dev/null +++ b/src/Oms.Application/Shared/AllocationProviderSnapshotEnricher.cs @@ -0,0 +1,122 @@ +using Fake.DependencyInjection; +using Oms.Application.Dtos.Channels; +using Oms.Application.Integrations.Itfx; +using Oms.Domain.ChannelsAggregate.Entities; +using Oms.Domain.ChannelsAggregate.Repositories; + +namespace Oms.Application.Shared; + +/// +/// 为排货 Provider 快照补充渠道与 itfx 下游对接元数据。 +/// +public class AllocationProviderSnapshotEnricher( + IServiceChannelRepository serviceChannelRepository, + IServiceChannelGroupRepository serviceChannelGroupRepository, + IItfxMetadataClient itfxMetadataClient) : IScopedDependency +{ + public async Task EnrichAsync( + IList providers, + CancellationToken cancellationToken = default) + { + if (providers.Count == 0) + { + return; + } + + var channelIds = providers + .Where(x => x.ServiceChannelId.HasValue && x.ServiceChannelId.Value > 0) + .Select(x => x.ServiceChannelId!.Value) + .Distinct() + .ToArray(); + if (channelIds.Length == 0) + { + return; + } + + var channels = await serviceChannelRepository.GetListAsync(x => channelIds.Contains(x.Id)); + var channelMap = channels.ToDictionary(x => x.Id); + + var groupIds = channels + .Where(x => x.GroupId.HasValue && x.GroupId.Value > 0) + .Select(x => x.GroupId!.Value) + .Distinct() + .ToArray(); + var groups = groupIds.Length == 0 + ? [] + : await serviceChannelGroupRepository.GetListAsync(x => groupIds.Contains(x.Id)); + var groupMap = groups.ToDictionary(x => x.Id); + + foreach (var provider in providers) + { + if (!provider.ServiceChannelId.HasValue + || !channelMap.TryGetValue(provider.ServiceChannelId.Value, out var channel)) + { + continue; + } + + ApplyChannelFields(provider, channel); + + if (!channel.GroupId.HasValue || !groupMap.TryGetValue(channel.GroupId.Value, out var group)) + { + continue; + } + + provider.GroupId = group.Id; + provider.AppId = group.AppId > 0 ? group.AppId : null; + provider.ApiServiceProviderId = group.ApiServiceProviderId > 0 ? group.ApiServiceProviderId : null; + + if (provider.AppId is > 0) + { + var app = await itfxMetadataClient.GetAppAsync(provider.AppId.Value, cancellationToken); + if (!string.IsNullOrWhiteSpace(app?.AppKey)) + { + provider.AppKey = app.AppKey; + } + } + + if (provider.ApiServiceProviderId is > 0) + { + var sp = await itfxMetadataClient.GetServiceProviderAsync( + provider.ApiServiceProviderId.Value, + cancellationToken); + if (!string.IsNullOrWhiteSpace(sp?.Code)) + { + provider.SupplierCode = sp.Code; + } + } + } + } + + public static void ApplyChannelFields(SalesProductAllocationProviderSnapshotDto provider, ServiceChannel channel) + { + provider.ChannelCode = NullIfEmpty(channel.ChannelCode); + provider.ForecastCode = NullIfEmpty(channel.ForecastCode); + provider.GroupId = channel.GroupId; + if (string.IsNullOrWhiteSpace(provider.ServiceChannelName)) + { + provider.ServiceChannelName = NullIfEmpty(channel.ChannelName); + } + } + + public static void ApplyDownstreamFieldsToOrder( + Domain.OrdersAggregate.Entities.Order order, + SalesProductAllocationProviderSnapshotDto provider, + ServiceChannel? channelFallback = null) + { + order.DownstreamSupplierCode = NullIfEmpty(provider.SupplierCode); + order.DownstreamAppKey = NullIfEmpty(provider.AppKey); + order.DownstreamAppId = provider.AppId; + order.ChannelForecastCode = NullIfEmpty(provider.ForecastCode) + ?? NullIfEmpty(channelFallback?.ForecastCode); + } + + private static string? NullIfEmpty(string? value) => + string.IsNullOrWhiteSpace(value) ? null : value.Trim(); + + public static bool NeedsDownstreamEnrichment(SalesProductAllocationProviderSnapshotDto provider) => + provider.ServiceChannelId is > 0 + && (string.IsNullOrWhiteSpace(provider.ChannelCode) + || string.IsNullOrWhiteSpace(provider.ForecastCode) + || string.IsNullOrWhiteSpace(provider.SupplierCode) + || string.IsNullOrWhiteSpace(provider.AppKey)); +} diff --git a/src/Oms.Domain/OrdersAggregate/Entities/Order.cs b/src/Oms.Domain/OrdersAggregate/Entities/Order.cs index afee3d1..da02ff5 100644 --- a/src/Oms.Domain/OrdersAggregate/Entities/Order.cs +++ b/src/Oms.Domain/OrdersAggregate/Entities/Order.cs @@ -18,6 +18,26 @@ public class Order : OmsAuditedAggregateRoot public string? ChannelCode { get; set; } + /// + /// 下游 API 服务商编码(itfx supplierCode)。 + /// + public string? DownstreamSupplierCode { get; set; } + + /// + /// 下游 itfx 应用 AppKey。 + /// + public string? DownstreamAppKey { get; set; } + + /// + /// 下游 itfx 应用 Id。 + /// + public int? DownstreamAppId { get; set; } + + /// + /// 渠道预报编码(ForecastCode / ApiCode)。 + /// + public string? ChannelForecastCode { get; set; } + public string? ConsigneeCountry { get; set; } public string? ConsigneeCountryName { get; set; } diff --git a/src/Oms.Domain/SystemAggregate/Consts/SystemOtherConfigCodes.cs b/src/Oms.Domain/SystemAggregate/Consts/SystemOtherConfigCodes.cs new file mode 100644 index 0000000..f549e00 --- /dev/null +++ b/src/Oms.Domain/SystemAggregate/Consts/SystemOtherConfigCodes.cs @@ -0,0 +1,12 @@ +namespace Oms.Domain.SystemAggregate.Consts; + +/// +/// 系统其他配置编码常量。 +/// +public static class SystemOtherConfigCodes +{ + /// + /// 小包轨迹服务配置。 + /// + public const string TrackService = "track_service"; +} diff --git a/src/Oms.Domain/SystemAggregate/Entities/SystemOtherConfig.cs b/src/Oms.Domain/SystemAggregate/Entities/SystemOtherConfig.cs new file mode 100644 index 0000000..4b994ca --- /dev/null +++ b/src/Oms.Domain/SystemAggregate/Entities/SystemOtherConfig.cs @@ -0,0 +1,24 @@ +using Oms.Domain.Shared; +using SqlSugar; + +namespace Oms.Domain.SystemAggregate.Entities; + +[SugarTable("t_system_other_config")] +public class SystemOtherConfig : OmsAuditedAggregateRoot +{ + public string ConfigCode { get; set; } = string.Empty; + + public string ConfigName { get; set; } = string.Empty; + + public bool Enabled { get; set; } + + public string? BaseUrl { get; set; } + + public string? AppKey { get; set; } + + public string? AppSecret { get; set; } + + public int TimeoutSeconds { get; set; } = 30; + + public string? Remark { get; set; } +} diff --git a/src/Oms.Domain/SystemAggregate/Repositories/ISystemOtherConfigRepository.cs b/src/Oms.Domain/SystemAggregate/Repositories/ISystemOtherConfigRepository.cs new file mode 100644 index 0000000..b1b3ffe --- /dev/null +++ b/src/Oms.Domain/SystemAggregate/Repositories/ISystemOtherConfigRepository.cs @@ -0,0 +1,9 @@ +using Fake.Domain.Repositories; +using Oms.Domain.SystemAggregate.Entities; + +namespace Oms.Domain.SystemAggregate.Repositories; + +public interface ISystemOtherConfigRepository : IRepository +{ + Task GetByConfigCodeAsync(string configCode, CancellationToken cancellationToken = default); +} diff --git a/src/Oms.SqlSugarCore/System/SystemOtherConfigRepository.cs b/src/Oms.SqlSugarCore/System/SystemOtherConfigRepository.cs new file mode 100644 index 0000000..e71892b --- /dev/null +++ b/src/Oms.SqlSugarCore/System/SystemOtherConfigRepository.cs @@ -0,0 +1,14 @@ +using Fake.DependencyInjection; +using Oms.Domain.SystemAggregate.Entities; +using Oms.Domain.SystemAggregate.Repositories; +using Oms.SqlSugarCore.Repositories; + +namespace Oms.SqlSugarCore.System; + +public class SystemOtherConfigRepository : OmsSqlSugarRepository, ISystemOtherConfigRepository, IScopedDependency +{ + public Task GetByConfigCodeAsync(string configCode, CancellationToken cancellationToken = default) + { + return FirstOrDefaultAsync(x => x.ConfigCode == configCode, cancellationToken); + } +} diff --git a/src/Oms.Tests/SmallBagTrackSignHelperTests.cs b/src/Oms.Tests/SmallBagTrackSignHelperTests.cs new file mode 100644 index 0000000..e376bb9 --- /dev/null +++ b/src/Oms.Tests/SmallBagTrackSignHelperTests.cs @@ -0,0 +1,19 @@ +using FluentAssertions; +using Oms.Application.Integrations.SmallBagTrack; + +namespace Oms.Tests; + +public class SmallBagTrackSignHelperTests +{ + [Fact] + public void GenerateSign_ShouldMatchMd5UppercaseRule() + { + const string appKey = "W2606001"; + const long timestamp = 1700000000; + const string appSecret = "ziK8UzlQ39m3kdL346XEBg8nljY8os52"; + + var sign = SmallBagTrackSignHelper.GenerateSign(appKey, timestamp, appSecret); + + sign.Should().Be("A744D764DEE7C925AD6750257A4DEEE7"); + } +} diff --git a/src/SimpleAdmin.Api/appsettings.Development.json b/src/SimpleAdmin.Api/appsettings.Development.json index 60c2475..2a495eb 100644 --- a/src/SimpleAdmin.Api/appsettings.Development.json +++ b/src/SimpleAdmin.Api/appsettings.Development.json @@ -50,5 +50,18 @@ "EncodingAesKey": "", "OAuthRedirectBase": "https://oms.nanafa.com" } + }, + "Oms": { + "TrackService": { + "Enabled": true, + "BaseUrl": "http://api.nanafa.com/track", + "AppKey": "W2606001", + "AppSecret": "ziK8UzlQ39m3kdL346XEBg8nljY8os52", + "TimeoutSeconds": 30 + }, + "Itfx": { + "BaseUrl": "http://api.nanafa.com/itfx", + "TimeoutSeconds": 30 + } } } \ No newline at end of file