feat(order): 规则快照下游元数据、订单绑定字段与系统其他配置
排货发布 enrich Providers 并确认渠道时写入 t_order 下游四字段;接入 itfx 元数据客户端;系统其他配置与小包轨迹 DB 配置;手动轨迹 UpsertSmallBag 接口。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
fe30d071e6
commit
545f0825f8
26
db.sql
26
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',
|
||||
|
||||
77
scripts/debug-upsert-small-bag.ps1
Normal file
77
scripts/debug-upsert-small-bag.ps1
Normal file
@ -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
|
||||
}
|
||||
6
sql/migrate_t_order_downstream_integration.sql
Normal file
6
sql/migrate_t_order_downstream_integration.sql
Normal file
@ -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`;
|
||||
21
sql/migrate_t_system_other_config.sql
Normal file
21
sql/migrate_t_system_other_config.sql
Normal file
@ -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='系统其他配置表';
|
||||
@ -44,4 +44,39 @@ public class SalesProductAllocationProviderSnapshotDto
|
||||
/// 指定客户主键集合,空表示全部。
|
||||
/// </summary>
|
||||
public List<int> CustomerIds { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 服务渠道编码(发布快照固化)。
|
||||
/// </summary>
|
||||
public string? ChannelCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 渠道预报编码 ForecastCode(发布快照固化)。
|
||||
/// </summary>
|
||||
public string? ForecastCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务商分组 Id。
|
||||
/// </summary>
|
||||
public int? GroupId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// itfx API 服务商编码 supplierCode。
|
||||
/// </summary>
|
||||
public string? SupplierCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// itfx 应用 AppKey(AppSecret 不落快照,运行时按 AppId 读取)。
|
||||
/// </summary>
|
||||
public string? AppKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// itfx 应用 Id。
|
||||
/// </summary>
|
||||
public int? AppId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// itfx API 服务商 Id。
|
||||
/// </summary>
|
||||
public int? ApiServiceProviderId { get; set; }
|
||||
}
|
||||
|
||||
@ -40,6 +40,26 @@ public class OrderOutput
|
||||
/// </summary>
|
||||
public string? ChannelCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 下游 API 服务商编码 supplierCode。
|
||||
/// </summary>
|
||||
public string? DownstreamSupplierCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 下游 itfx 应用 AppKey。
|
||||
/// </summary>
|
||||
public string? DownstreamAppKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 下游 itfx 应用 Id(用于运行时读取 AppSecret)。
|
||||
/// </summary>
|
||||
public int? DownstreamAppId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 渠道预报编码 ForecastCode。
|
||||
/// </summary>
|
||||
public string? ChannelForecastCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务渠道名称。
|
||||
/// </summary>
|
||||
@ -75,6 +95,31 @@ public class OrderOutput
|
||||
/// </summary>
|
||||
public int? CustomerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 幂等请求标识。
|
||||
/// </summary>
|
||||
public string? RequestId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 出口报关方式。
|
||||
/// </summary>
|
||||
public string? ExportCustomsMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 申报币种。
|
||||
/// </summary>
|
||||
public string? DeclaredCurrency { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 申报总金额。
|
||||
/// </summary>
|
||||
public decimal? DeclaredSum { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 包裹数量。
|
||||
/// </summary>
|
||||
public int? Quantity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否 COD。
|
||||
/// </summary>
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
namespace Oms.Application.Dtos.Orders;
|
||||
|
||||
/// <summary>
|
||||
/// 调试 UpsertSmallBag 入参(字段均可选,缺省使用示例值)。
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
namespace Oms.Application.Dtos.System;
|
||||
|
||||
/// <summary>
|
||||
/// 系统其他配置新增入参。
|
||||
/// </summary>
|
||||
public class SystemOtherConfigCreateInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 配置编码。
|
||||
/// </summary>
|
||||
public string ConfigCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 配置名称。
|
||||
/// </summary>
|
||||
public string ConfigName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用。
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务地址。
|
||||
/// </summary>
|
||||
public string? BaseUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// AppKey。
|
||||
/// </summary>
|
||||
public string? AppKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// AppSecret。
|
||||
/// </summary>
|
||||
public string? AppSecret { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 超时秒数。
|
||||
/// </summary>
|
||||
public int TimeoutSeconds { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// 备注。
|
||||
/// </summary>
|
||||
public string? Remark { get; set; }
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
namespace Oms.Application.Dtos.System;
|
||||
|
||||
/// <summary>
|
||||
/// 系统其他配置列表查询入参。
|
||||
/// </summary>
|
||||
public class SystemOtherConfigListInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 页码,从 1 开始。
|
||||
/// </summary>
|
||||
public int Page { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数。
|
||||
/// </summary>
|
||||
public int PageSize { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// 关键词,匹配配置编码或名称。
|
||||
/// </summary>
|
||||
public string? Keyword { get; set; }
|
||||
}
|
||||
62
src/Oms.Application/Dtos/System/SystemOtherConfigOutput.cs
Normal file
62
src/Oms.Application/Dtos/System/SystemOtherConfigOutput.cs
Normal file
@ -0,0 +1,62 @@
|
||||
namespace Oms.Application.Dtos.System;
|
||||
|
||||
/// <summary>
|
||||
/// 系统其他配置列表项出参。
|
||||
/// </summary>
|
||||
public class SystemOtherConfigOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键。
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 配置编码。
|
||||
/// </summary>
|
||||
public string ConfigCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 配置名称。
|
||||
/// </summary>
|
||||
public string ConfigName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用。
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务地址。
|
||||
/// </summary>
|
||||
public string? BaseUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// AppKey。
|
||||
/// </summary>
|
||||
public string? AppKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// AppSecret(列表已脱敏)。
|
||||
/// </summary>
|
||||
public string? AppSecret { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 超时秒数。
|
||||
/// </summary>
|
||||
public int TimeoutSeconds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注。
|
||||
/// </summary>
|
||||
public string? Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间。
|
||||
/// </summary>
|
||||
public DateTime CreateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间。
|
||||
/// </summary>
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
namespace Oms.Application.Dtos.System;
|
||||
|
||||
/// <summary>
|
||||
/// 系统其他配置更新入参。
|
||||
/// </summary>
|
||||
public class SystemOtherConfigUpdateInput : SystemOtherConfigCreateInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键。
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
}
|
||||
20
src/Oms.Application/Integrations/Itfx/IItfxMetadataClient.cs
Normal file
20
src/Oms.Application/Integrations/Itfx/IItfxMetadataClient.cs
Normal file
@ -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<ItfxAppMetadata?> GetAppAsync(int appId, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<ItfxServiceProviderMetadata?> GetServiceProviderAsync(
|
||||
int serviceProviderId,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
16
src/Oms.Application/Integrations/Itfx/ItfxGatewayOptions.cs
Normal file
16
src/Oms.Application/Integrations/Itfx/ItfxGatewayOptions.cs
Normal file
@ -0,0 +1,16 @@
|
||||
namespace Oms.Application.Integrations.Itfx;
|
||||
|
||||
/// <summary>
|
||||
/// itfx 网关配置。
|
||||
/// </summary>
|
||||
public class ItfxGatewayOptions
|
||||
{
|
||||
public const string SectionName = "Oms:Itfx";
|
||||
|
||||
/// <summary>
|
||||
/// itfx 服务根地址,如 http://api.nanafa.com/itfx。
|
||||
/// </summary>
|
||||
public string BaseUrl { get; set; } = "http://api.nanafa.com/itfx";
|
||||
|
||||
public int TimeoutSeconds { get; set; } = 30;
|
||||
}
|
||||
135
src/Oms.Application/Integrations/Itfx/ItfxMetadataClient.cs
Normal file
135
src/Oms.Application/Integrations/Itfx/ItfxMetadataClient.cs
Normal file
@ -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<ItfxGatewayOptions> options,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<ItfxMetadataClient> logger) : IItfxMetadataClient, IScopedDependency
|
||||
{
|
||||
public const string HttpClientName = nameof(ItfxMetadataClient);
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
public Task<ItfxAppMetadata?> GetAppAsync(int appId, CancellationToken cancellationToken = default) =>
|
||||
GetAsync<ItfxAppMetadata>($"/App/GetInfo?id={appId}", payload =>
|
||||
{
|
||||
var appKey = ReadString(payload, "appKey", "AppKey");
|
||||
return string.IsNullOrWhiteSpace(appKey)
|
||||
? null
|
||||
: new ItfxAppMetadata { AppKey = appKey.Trim() };
|
||||
}, cancellationToken);
|
||||
|
||||
public Task<ItfxServiceProviderMetadata?> GetServiceProviderAsync(
|
||||
int serviceProviderId,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
GetAsync<ItfxServiceProviderMetadata>(
|
||||
$"/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<T?> GetAsync<T>(
|
||||
string relativeUrl,
|
||||
Func<JsonElement, T?> 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;
|
||||
}
|
||||
}
|
||||
@ -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<ItfxGatewayOptions>(configuration.GetSection(ItfxGatewayOptions.SectionName));
|
||||
services.AddHttpClient(ItfxMetadataClient.HttpClientName);
|
||||
services.AddScoped<IItfxMetadataClient, ItfxMetadataClient>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@ -3,4 +3,9 @@ namespace Oms.Application.Integrations.SmallBagTrack;
|
||||
public interface ISmallBagTrackOrderClient
|
||||
{
|
||||
Task UpsertAsync(SmallBagTrackUpsertRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 同步调用并返回完整请求/响应(仅调试)。
|
||||
/// </summary>
|
||||
Task<SmallBagTrackUpsertResult> UpsertDebugAsync(SmallBagTrackUpsertRequest request, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 小包轨迹运行时配置读取。
|
||||
/// </summary>
|
||||
public interface ISmallBagTrackConfigProvider
|
||||
{
|
||||
Task<SmallBagTrackOptions> GetAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
internal sealed class SmallBagTrackConfigProvider(
|
||||
ISystemOtherConfigRepository repository,
|
||||
IOptions<SmallBagTrackOptions> options) : ISmallBagTrackConfigProvider, IScopedDependency
|
||||
{
|
||||
public async Task<SmallBagTrackOptions> 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();
|
||||
}
|
||||
@ -17,6 +17,16 @@ public class SmallBagTrackOptions
|
||||
/// </summary>
|
||||
public string BaseUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 应用唯一标识(appKey)。
|
||||
/// </summary>
|
||||
public string AppKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 应用密钥(appSecret),用于签名,勿泄露。
|
||||
/// </summary>
|
||||
public string AppSecret { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// HTTP 超时(秒)。
|
||||
/// </summary>
|
||||
|
||||
@ -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<SmallBagTrackOptions> options,
|
||||
ISmallBagTrackConfigProvider configProvider,
|
||||
ILogger<SmallBagTrackOrderClient> 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<SmallBagTrackUpsertResult> 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
|
||||
};
|
||||
}
|
||||
|
||||
@ -9,7 +9,8 @@ public static class SmallBagTrackServiceExtensions
|
||||
{
|
||||
services.Configure<SmallBagTrackOptions>(configuration.GetSection(SmallBagTrackOptions.SectionName));
|
||||
services.AddHttpClient(SmallBagTrackOrderClient.HttpClientName);
|
||||
services.AddSingleton<ISmallBagTrackOrderClient, SmallBagTrackOrderClient>();
|
||||
services.AddScoped<ISmallBagTrackConfigProvider, SmallBagTrackConfigProvider>();
|
||||
services.AddScoped<ISmallBagTrackOrderClient, SmallBagTrackOrderClient>();
|
||||
services.AddSingleton<ISmallBagTrackOrderNotifier, SmallBagTrackOrderNotifier>();
|
||||
return services;
|
||||
}
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Oms.Application.Integrations.SmallBagTrack;
|
||||
|
||||
/// <summary>
|
||||
/// 轨迹服务 API 签名:appKey={}&timestamp={}&appSecret={} → MD5 大写。
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
namespace Oms.Application.Integrations.SmallBagTrack;
|
||||
|
||||
/// <summary>
|
||||
/// POST /OrderV2/UpsertSmallBag 完整请求体(含公共参数 + 业务字段)。
|
||||
/// </summary>
|
||||
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
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
namespace Oms.Application.Integrations.SmallBagTrack;
|
||||
|
||||
/// <summary>
|
||||
/// UpsertSmallBag 调用结果(调试用)。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
@ -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<OmsApplicationModule>();
|
||||
});
|
||||
context.Services.AddOmsSmallBagTrack(context.Services.GetConfiguration());
|
||||
context.Services.AddOmsItfxIntegration(context.Services.GetConfiguration());
|
||||
}
|
||||
}
|
||||
|
||||
@ -175,6 +175,7 @@ public partial class SalesProductService
|
||||
KpiAreaRepository,
|
||||
WeightRuleRepository,
|
||||
WeightRoundingRepository);
|
||||
await AllocationProviderSnapshotEnricher.EnrichAsync(rulesSnapshot.Providers);
|
||||
return rulesSnapshot;
|
||||
}
|
||||
|
||||
|
||||
@ -23,6 +23,8 @@ public partial class SalesProductService : ApplicationService
|
||||
private IKpiAreaRepository KpiAreaRepository => LazyServiceProvider.GetRequiredService<IKpiAreaRepository>();
|
||||
private IWeightRuleRepository WeightRuleRepository => LazyServiceProvider.GetRequiredService<IWeightRuleRepository>();
|
||||
private IWeightRoundingRepository WeightRoundingRepository => LazyServiceProvider.GetRequiredService<IWeightRoundingRepository>();
|
||||
private AllocationProviderSnapshotEnricher AllocationProviderSnapshotEnricher =>
|
||||
LazyServiceProvider.GetRequiredService<AllocationProviderSnapshotEnricher>();
|
||||
|
||||
/// <summary>
|
||||
/// 查询销售产品列表。
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<IWeightRuleRepository>();
|
||||
private IWeightRoundingRepository WeightRoundingRepository => LazyServiceProvider.GetRequiredService<IWeightRoundingRepository>();
|
||||
private IKpiAreaRepository KpiAreaRepository => LazyServiceProvider.GetRequiredService<IKpiAreaRepository>();
|
||||
private AllocationProviderSnapshotEnricher AllocationProviderSnapshotEnricher =>
|
||||
LazyServiceProvider.GetRequiredService<AllocationProviderSnapshotEnricher>();
|
||||
|
||||
private async Task<OrderParcelCreateOutput> 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<SalesProductAllocationProviderSnapshotDto> 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<List<int>>(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<List<int>>(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按订单同步轨迹(UpsertSmallBag),返回完整请求与响应便于联调。
|
||||
/// </summary>
|
||||
[HttpPost("/order/track-upsert-small-bag")]
|
||||
public async Task<SmallBagTrackUpsertResult> 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<SmallBagTrackUpsertRequest> 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<ISmallBagTrackOrderClient>();
|
||||
|
||||
/// <summary>
|
||||
/// 调试:同步调用轨迹 UpsertSmallBag,返回完整请求与响应(Swagger 可测)。
|
||||
/// </summary>
|
||||
[HttpPost("/order/debug/track-upsert-small-bag")]
|
||||
public Task<SmallBagTrackUpsertResult> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
197
src/Oms.Application/Services/System/SystemOtherConfigService.cs
Normal file
197
src/Oms.Application/Services/System/SystemOtherConfigService.cs
Normal file
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 系统其他配置 API 服务。
|
||||
/// </summary>
|
||||
[ApiExplorerSettings(GroupName = "OMS-System")]
|
||||
public class SystemOtherConfigService : ApplicationService
|
||||
{
|
||||
private const string SecretMask = "******";
|
||||
|
||||
private ISystemOtherConfigRepository SystemOtherConfigRepository =>
|
||||
LazyServiceProvider.GetRequiredService<ISystemOtherConfigRepository>();
|
||||
|
||||
/// <summary>
|
||||
/// 查询系统其他配置列表。
|
||||
/// </summary>
|
||||
[HttpGet("/system/other-config/list")]
|
||||
public async Task<OmsPageResult<SystemOtherConfigOutput>> 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<string, bool> { [nameof(SystemOtherConfig.Id)] = false });
|
||||
var total = await SystemOtherConfigRepository.CountAsync(predicate);
|
||||
|
||||
return new OmsPageResult<SystemOtherConfigOutput>
|
||||
{
|
||||
Items = items.Select(x => MapOutput(x, maskSecret: true)).ToList(),
|
||||
Total = total
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询系统其他配置详情。
|
||||
/// </summary>
|
||||
[HttpGet("/system/other-config/detail")]
|
||||
public async Task<SystemOtherConfigOutput> GetDetailAsync([FromQuery] int id)
|
||||
{
|
||||
var entity = await GetConfigOrThrowAsync(id);
|
||||
return MapOutput(entity, maskSecret: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增系统其他配置。
|
||||
/// </summary>
|
||||
[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)
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新系统其他配置。
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除系统其他配置。
|
||||
/// </summary>
|
||||
[HttpPost("/system/other-config/delete")]
|
||||
public async Task DeleteAsync(IdInput input)
|
||||
{
|
||||
var entity = await GetConfigOrThrowAsync(input.Id);
|
||||
await SystemOtherConfigRepository.DeleteAsync(entity);
|
||||
}
|
||||
|
||||
private static Expression<Func<SystemOtherConfig, bool>> BuildListPredicate(SystemOtherConfigListInput input)
|
||||
{
|
||||
var keyword = input.Keyword?.Trim();
|
||||
return x =>
|
||||
string.IsNullOrWhiteSpace(keyword) ||
|
||||
x.ConfigCode.Contains(keyword) ||
|
||||
x.ConfigName.Contains(keyword);
|
||||
}
|
||||
|
||||
private async Task<SystemOtherConfig> 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;
|
||||
}
|
||||
122
src/Oms.Application/Shared/AllocationProviderSnapshotEnricher.cs
Normal file
122
src/Oms.Application/Shared/AllocationProviderSnapshotEnricher.cs
Normal file
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 为排货 Provider 快照补充渠道与 itfx 下游对接元数据。
|
||||
/// </summary>
|
||||
public class AllocationProviderSnapshotEnricher(
|
||||
IServiceChannelRepository serviceChannelRepository,
|
||||
IServiceChannelGroupRepository serviceChannelGroupRepository,
|
||||
IItfxMetadataClient itfxMetadataClient) : IScopedDependency
|
||||
{
|
||||
public async Task EnrichAsync(
|
||||
IList<SalesProductAllocationProviderSnapshotDto> 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));
|
||||
}
|
||||
@ -18,6 +18,26 @@ public class Order : OmsAuditedAggregateRoot
|
||||
|
||||
public string? ChannelCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 下游 API 服务商编码(itfx supplierCode)。
|
||||
/// </summary>
|
||||
public string? DownstreamSupplierCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 下游 itfx 应用 AppKey。
|
||||
/// </summary>
|
||||
public string? DownstreamAppKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 下游 itfx 应用 Id。
|
||||
/// </summary>
|
||||
public int? DownstreamAppId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 渠道预报编码(ForecastCode / ApiCode)。
|
||||
/// </summary>
|
||||
public string? ChannelForecastCode { get; set; }
|
||||
|
||||
public string? ConsigneeCountry { get; set; }
|
||||
|
||||
public string? ConsigneeCountryName { get; set; }
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
namespace Oms.Domain.SystemAggregate.Consts;
|
||||
|
||||
/// <summary>
|
||||
/// 系统其他配置编码常量。
|
||||
/// </summary>
|
||||
public static class SystemOtherConfigCodes
|
||||
{
|
||||
/// <summary>
|
||||
/// 小包轨迹服务配置。
|
||||
/// </summary>
|
||||
public const string TrackService = "track_service";
|
||||
}
|
||||
24
src/Oms.Domain/SystemAggregate/Entities/SystemOtherConfig.cs
Normal file
24
src/Oms.Domain/SystemAggregate/Entities/SystemOtherConfig.cs
Normal file
@ -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; }
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
using Fake.Domain.Repositories;
|
||||
using Oms.Domain.SystemAggregate.Entities;
|
||||
|
||||
namespace Oms.Domain.SystemAggregate.Repositories;
|
||||
|
||||
public interface ISystemOtherConfigRepository : IRepository<SystemOtherConfig>
|
||||
{
|
||||
Task<SystemOtherConfig?> GetByConfigCodeAsync(string configCode, CancellationToken cancellationToken = default);
|
||||
}
|
||||
14
src/Oms.SqlSugarCore/System/SystemOtherConfigRepository.cs
Normal file
14
src/Oms.SqlSugarCore/System/SystemOtherConfigRepository.cs
Normal file
@ -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<SystemOtherConfig>, ISystemOtherConfigRepository, IScopedDependency
|
||||
{
|
||||
public Task<SystemOtherConfig?> GetByConfigCodeAsync(string configCode, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return FirstOrDefaultAsync(x => x.ConfigCode == configCode, cancellationToken);
|
||||
}
|
||||
}
|
||||
19
src/Oms.Tests/SmallBagTrackSignHelperTests.cs
Normal file
19
src/Oms.Tests/SmallBagTrackSignHelperTests.cs
Normal file
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user