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:
parent
6314790598
commit
15785cdf2b
@ -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>
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>();
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user