feat: enrich publish snapshots with downstream names and use API provider code for track ApiCode

Persist group/app/API provider display names in sales product and service channel rules publish snapshots, and resolve trajectory ApiCode from the service channel API provider code instead of forecast code.
This commit is contained in:
zzh 2026-07-23 10:23:56 +08:00
parent 6314790598
commit 15785cdf2b
9 changed files with 246 additions and 43 deletions

View File

@ -60,16 +60,31 @@ public class SalesProductAllocationProviderSnapshotDto
/// </summary>
public int? GroupId { get; set; }
/// <summary>
/// 服务商分组名称。
/// </summary>
public string? GroupName { get; set; }
/// <summary>
/// itfx API 服务商编码 supplierCode。
/// </summary>
public string? SupplierCode { get; set; }
/// <summary>
/// itfx API 服务商名称。
/// </summary>
public string? ApiServiceProviderName { get; set; }
/// <summary>
/// itfx 应用 AppKey(AppSecret 不落快照,运行时按 AppId 读取)。
/// </summary>
public string? AppKey { get; set; }
/// <summary>
/// itfx 应用名称。
/// </summary>
public string? AppName { get; set; }
/// <summary>
/// itfx 应用 Id。
/// </summary>

View File

@ -0,0 +1,47 @@
namespace Oms.Application.Dtos.Channels;
/// <summary>
/// 服务渠道下游对接元数据(发布快照固化,不含 AppSecret)。
/// </summary>
public class ServiceChannelDownstreamSnapshotDto
{
/// <summary>
/// 服务商分组 Id。
/// </summary>
public int? GroupId { get; set; }
/// <summary>
/// 服务商分组名称。
/// </summary>
public string? GroupName { get; set; }
/// <summary>
/// itfx 应用 Id。
/// </summary>
public int? AppId { get; set; }
/// <summary>
/// itfx 应用 AppKey。
/// </summary>
public string? AppKey { get; set; }
/// <summary>
/// itfx 应用名称。
/// </summary>
public string? AppName { get; set; }
/// <summary>
/// itfx API 服务商 Id。
/// </summary>
public int? ApiServiceProviderId { get; set; }
/// <summary>
/// itfx API 服务商编码 supplierCode。
/// </summary>
public string? SupplierCode { get; set; }
/// <summary>
/// itfx API 服务商名称。
/// </summary>
public string? ApiServiceProviderName { get; set; }
}

View File

@ -5,11 +5,15 @@ public sealed class ItfxAppMetadata
public string AppKey { get; set; } = string.Empty;
public string AppSecret { get; set; } = string.Empty;
public string? Name { get; set; }
}
public sealed class ItfxServiceProviderMetadata
{
public string Code { get; set; } = string.Empty;
public string? Name { get; set; }
}
public interface IItfxMetadataClient

View File

@ -30,10 +30,12 @@ internal sealed class ItfxMetadataClient(
return null;
}
var appName = ReadString(payload, "name", "Name", "appName", "AppName");
return new ItfxAppMetadata
{
AppKey = appKey.Trim(),
AppSecret = appSecret?.Trim() ?? string.Empty
AppSecret = appSecret?.Trim() ?? string.Empty,
Name = NullIfEmpty(appName)
};
}, cancellationToken);
@ -45,9 +47,17 @@ internal sealed class ItfxMetadataClient(
payload =>
{
var code = ReadString(payload, "code", "Code", "serviceProviderCode", "ServiceProviderCode");
return string.IsNullOrWhiteSpace(code)
? null
: new ItfxServiceProviderMetadata { Code = code.Trim() };
if (string.IsNullOrWhiteSpace(code))
{
return null;
}
var name = ReadString(payload, "name", "Name", "serviceProviderName", "ServiceProviderName");
return new ItfxServiceProviderMetadata
{
Code = code.Trim(),
Name = NullIfEmpty(name)
};
},
cancellationToken);
@ -140,4 +150,7 @@ internal sealed class ItfxMetadataClient(
return null;
}
private static string? NullIfEmpty(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}

View File

@ -18,7 +18,7 @@ public class SmallBagTrackUpsertRequest
public string Product { get; set; } = string.Empty;
/// <summary>
/// 轨迹拉取配置 code(对应轨迹 ServiceProviderCode;优先 ChannelForecastCode)。
/// 轨迹拉取配置 code(对应轨迹 ServiceProviderCode;取服务渠道 API 服务商 code)。
/// </summary>
public string ApiCode { get; set; } = string.Empty;

View File

@ -1,5 +1,7 @@
using Fake.DependencyInjection;
using Oms.Application.Dtos.Orders;
using Oms.Application.Integrations.Itfx;
using Oms.Domain.ChannelsAggregate.Entities;
using Oms.Domain.ChannelsAggregate.Repositories;
using Oms.Domain.CustomersAggregate.Entities;
using Oms.Domain.CustomersAggregate.Repositories;
@ -18,12 +20,14 @@ public interface ISmallBagTrackUpsertRequestBuilder
}
/// <summary>
/// 组装 UpsertSmallBag 请求:ApiCode 优先 ChannelForecastCode,TrackNo 缺省用 ServiceNo。
/// 组装 UpsertSmallBag 请求:ApiCode 取服务渠道分组关联的 API 服务商 code,TrackNo 缺省用 ServiceNo。
/// </summary>
internal sealed class SmallBagTrackUpsertRequestBuilder(
IOrderRepository orderRepository,
ICustomerRepository customerRepository,
IServiceChannelRepository serviceChannelRepository,
IServiceChannelGroupRepository serviceChannelGroupRepository,
IItfxMetadataClient itfxMetadataClient,
ISalesProductRepository salesProductRepository) : ISmallBagTrackUpsertRequestBuilder, IScopedDependency
{
public async Task<SmallBagTrackUpsertRequest> BuildAsync(
@ -81,29 +85,66 @@ internal sealed class SmallBagTrackUpsertRequestBuilder(
private async Task<string> ResolveTrackApiCodeAsync(Order order, CancellationToken cancellationToken)
{
var fromOrder = order.ChannelForecastCode?.Trim();
var fromOrder = order.DownstreamSupplierCode?.Trim();
if (!string.IsNullOrWhiteSpace(fromOrder))
{
return fromOrder;
}
var channelCode = order.ChannelCode?.Trim();
if (!string.IsNullOrWhiteSpace(channelCode))
var fromChannel = await ResolveApiCodeFromChannelCodeAsync(order.ChannelCode, cancellationToken);
if (!string.IsNullOrWhiteSpace(fromChannel))
{
var channel = await serviceChannelRepository.GetByChannelCodeAsync(channelCode, cancellationToken);
var forecast = channel?.ForecastCode?.Trim();
if (!string.IsNullOrWhiteSpace(forecast))
{
return forecast;
}
return fromChannel;
}
var provisional = await ResolveProvisionalApiCodeFromProductAsync(order, cancellationToken);
return provisional ?? string.Empty;
}
private async Task<string?> ResolveApiCodeFromChannelCodeAsync(
string? channelCode,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(channelCode))
{
return null;
}
var channel = await serviceChannelRepository.GetByChannelCodeAsync(channelCode.Trim(), cancellationToken);
if (channel == null)
{
return null;
}
return await ResolveApiCodeFromServiceChannelAsync(channel, cancellationToken);
}
private async Task<string?> ResolveApiCodeFromServiceChannelAsync(
ServiceChannel channel,
CancellationToken cancellationToken)
{
if (!channel.GroupId.HasValue || channel.GroupId.Value <= 0)
{
return null;
}
var group = await serviceChannelGroupRepository.FirstOrDefaultAsync(
x => x.Id == channel.GroupId.Value,
cancellationToken);
if (group == null || group.ApiServiceProviderId <= 0)
{
return null;
}
var serviceProvider = await itfxMetadataClient.GetServiceProviderAsync(
group.ApiServiceProviderId,
cancellationToken);
var code = serviceProvider?.Code?.Trim();
return string.IsNullOrWhiteSpace(code) ? null : code;
}
/// <summary>
/// 创单尚未排货时,用销售产品排货规则里首个渠道的 ForecastCode 作为轨迹 ApiCode 预占位。
/// 创单尚未排货时,用销售产品排货规则里首个服务渠道的 API 服务商 code 作为轨迹 ApiCode 预占位。
/// </summary>
private async Task<string?> ResolveProvisionalApiCodeFromProductAsync(
Order order,
@ -135,10 +176,10 @@ internal sealed class SmallBagTrackUpsertRequestBuilder(
var channels = await serviceChannelRepository.GetListAsync(x => channelIds.Contains(x.Id));
foreach (var channel in channels.OrderBy(x => x.Id))
{
var forecast = channel.ForecastCode?.Trim();
if (!string.IsNullOrWhiteSpace(forecast))
var apiCode = await ResolveApiCodeFromServiceChannelAsync(channel, cancellationToken);
if (!string.IsNullOrWhiteSpace(apiCode))
{
return forecast;
return apiCode;
}
}

View File

@ -194,9 +194,20 @@ public partial class ServiceChannelService
private async Task<ServiceChannelRulesPublishSnapshot> BuildRulesPublishSnapshotAsync(int serviceChannelId)
{
var entity = await GetEntityOrThrowAsync(serviceChannelId);
ServiceChannelDownstreamSnapshotDto? downstream = null;
if (entity.GroupId is > 0)
{
var group = await ServiceChannelGroupRepository.FirstOrDefaultAsync(x => x.Id == entity.GroupId.Value);
if (group != null)
{
downstream = await AllocationProviderSnapshotEnricher.BuildDownstreamFromGroupAsync(group);
}
}
return new ServiceChannelRulesPublishSnapshot
{
Channel = MapChannelToSaveInput(entity)
Channel = MapChannelToSaveInput(entity),
Downstream = downstream
};
}
@ -520,5 +531,7 @@ public partial class ServiceChannelService
private sealed class ServiceChannelRulesPublishSnapshot
{
public ServiceChannelSaveInput Channel { get; set; } = new();
public ServiceChannelDownstreamSnapshotDto? Downstream { get; set; }
}
}

View File

@ -20,6 +20,8 @@ public partial class ServiceChannelService : ApplicationService
private IServiceChannelRepository ServiceChannelRepository => LazyServiceProvider.GetRequiredService<IServiceChannelRepository>();
private IServiceChannelGroupRepository ServiceChannelGroupRepository => LazyServiceProvider.GetRequiredService<IServiceChannelGroupRepository>();
private AllocationProviderSnapshotEnricher AllocationProviderSnapshotEnricher =>
LazyServiceProvider.GetRequiredService<AllocationProviderSnapshotEnricher>();
private OmsRbacUserDisplayNameResolver UserDisplayNameResolver =>
LazyServiceProvider.GetRequiredService<OmsRbacUserDisplayNameResolver>();

View File

@ -61,32 +61,36 @@ public class AllocationProviderSnapshotEnricher(
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;
}
}
await ApplyGroupDownstreamFieldsAsync(provider, group, cancellationToken);
}
}
public async Task<ServiceChannelDownstreamSnapshotDto?> BuildDownstreamFromGroupAsync(
ServiceChannelGroup group,
CancellationToken cancellationToken = default)
{
var snapshot = new ServiceChannelDownstreamSnapshotDto
{
GroupId = group.Id,
GroupName = NullIfEmpty(group.Name),
AppId = group.AppId > 0 ? group.AppId : null,
ApiServiceProviderId = group.ApiServiceProviderId > 0 ? group.ApiServiceProviderId : null
};
await FillItfxMetadataAsync(snapshot, cancellationToken);
return snapshot.GroupId.HasValue
|| !string.IsNullOrWhiteSpace(snapshot.GroupName)
|| snapshot.AppId is > 0
|| !string.IsNullOrWhiteSpace(snapshot.AppKey)
|| !string.IsNullOrWhiteSpace(snapshot.AppName)
|| snapshot.ApiServiceProviderId is > 0
|| !string.IsNullOrWhiteSpace(snapshot.SupplierCode)
|| !string.IsNullOrWhiteSpace(snapshot.ApiServiceProviderName)
? snapshot
: null;
}
public static void ApplyChannelFields(SalesProductAllocationProviderSnapshotDto provider, ServiceChannel channel)
{
provider.ChannelCode = NullIfEmpty(channel.ChannelCode);
@ -110,6 +114,70 @@ public class AllocationProviderSnapshotEnricher(
?? NullIfEmpty(channelFallback?.ForecastCode);
}
private async Task ApplyGroupDownstreamFieldsAsync(
SalesProductAllocationProviderSnapshotDto provider,
ServiceChannelGroup group,
CancellationToken cancellationToken)
{
provider.GroupId = group.Id;
provider.GroupName = NullIfEmpty(group.Name);
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;
}
provider.AppName = NullIfEmpty(app?.Name);
}
if (provider.ApiServiceProviderId is > 0)
{
var sp = await itfxMetadataClient.GetServiceProviderAsync(
provider.ApiServiceProviderId.Value,
cancellationToken);
if (!string.IsNullOrWhiteSpace(sp?.Code))
{
provider.SupplierCode = sp.Code;
}
provider.ApiServiceProviderName = NullIfEmpty(sp?.Name);
}
}
private async Task FillItfxMetadataAsync(
ServiceChannelDownstreamSnapshotDto snapshot,
CancellationToken cancellationToken)
{
if (snapshot.AppId is > 0)
{
var app = await itfxMetadataClient.GetAppAsync(snapshot.AppId.Value, cancellationToken);
if (!string.IsNullOrWhiteSpace(app?.AppKey))
{
snapshot.AppKey = app.AppKey;
}
snapshot.AppName = NullIfEmpty(app?.Name);
}
if (snapshot.ApiServiceProviderId is > 0)
{
var sp = await itfxMetadataClient.GetServiceProviderAsync(
snapshot.ApiServiceProviderId.Value,
cancellationToken);
if (!string.IsNullOrWhiteSpace(sp?.Code))
{
snapshot.SupplierCode = sp.Code;
}
snapshot.ApiServiceProviderName = NullIfEmpty(sp?.Name);
}
}
private static string? NullIfEmpty(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();