feat: 卖价/规则发布钉 KPI 分区包,快照只存引用
All checks were successful
Build and Deploy / deploy (push) Successful in 1m0s
All checks were successful
Build and Deploy / deploy (push) Successful in 1m0s
分区明细独立版本化并缓存加载,计费包缺失严格失败;同步态接口不再拉全文。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b38499963d
commit
bb8c0a906c
56
sql/migrate_kpi_area_publish_record.sql
Normal file
56
sql/migrate_kpi_area_publish_record.sql
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
-- 分区包发布表(价区 / 黑名单 / 偏远):卖价/规则发布钉包,计费按 version 加载
|
||||||
|
SET @db := DATABASE();
|
||||||
|
|
||||||
|
SET @tbl := (
|
||||||
|
SELECT COUNT(1)
|
||||||
|
FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = @db
|
||||||
|
AND TABLE_NAME = 't_kpi_area_publish_record'
|
||||||
|
);
|
||||||
|
SET @sql := IF(
|
||||||
|
@tbl = 0,
|
||||||
|
'CREATE TABLE `t_kpi_area_publish_record` (
|
||||||
|
`Id` INT NOT NULL AUTO_INCREMENT COMMENT ''主键'',
|
||||||
|
`AreaId` INT NOT NULL COMMENT ''所属分区表 t_kpi_area.Id'',
|
||||||
|
`AreaType` VARCHAR(32) NOT NULL COMMENT ''分区类型:price=价格分区,blacklist=黑名单,remote=偏远'',
|
||||||
|
`Version` VARCHAR(32) NOT NULL COMMENT ''包版本号(yyyyMMddHHmmssfff)'',
|
||||||
|
`Status` TINYINT NOT NULL DEFAULT 1 COMMENT ''状态:1=生效 Active,2=失效 Inactive'',
|
||||||
|
`ContentHash` VARCHAR(64) NOT NULL COMMENT ''规范化分区明细 Content 的 SHA256(hex),同 AreaId+Hash 去重复用'',
|
||||||
|
`Content` JSON NULL COMMENT ''分区明细快照 JSON(KpiAreaSnapshotRuleDto 数组)'',
|
||||||
|
`PublishUser` VARCHAR(64) NOT NULL DEFAULT '''' COMMENT ''发布人'',
|
||||||
|
`PublishTime` DATETIME NOT NULL COMMENT ''发布时间'',
|
||||||
|
`Description` VARCHAR(512) NULL COMMENT ''发布说明'',
|
||||||
|
`IsDeleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''软删除:0=否,1=是'',
|
||||||
|
`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_kpi_pack_area_hash` (`AreaId`, `ContentHash`),
|
||||||
|
KEY `idx_kpi_pack_area_ver` (`AreaId`, `Version`),
|
||||||
|
KEY `idx_kpi_pack_area_type_time` (`AreaId`, `AreaType`, `PublishTime`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=''KPI分区发布包(价区/黑名单/偏远明细不可变快照)''',
|
||||||
|
'SELECT ''t_kpi_area_publish_record already exists'' AS info'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- 表已存在时回填表/字段注释(可重复执行;不包进 PREPARE)
|
||||||
|
ALTER TABLE `t_kpi_area_publish_record`
|
||||||
|
COMMENT = 'KPI分区发布包(价区/黑名单/偏远明细不可变快照)',
|
||||||
|
MODIFY COLUMN `Id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
|
MODIFY COLUMN `AreaId` INT NOT NULL COMMENT '所属分区表 t_kpi_area.Id',
|
||||||
|
MODIFY COLUMN `AreaType` VARCHAR(32) NOT NULL COMMENT '分区类型:price=价格分区,blacklist=黑名单,remote=偏远',
|
||||||
|
MODIFY COLUMN `Version` VARCHAR(32) NOT NULL COMMENT '包版本号(yyyyMMddHHmmssfff)',
|
||||||
|
MODIFY COLUMN `Status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1=生效 Active,2=失效 Inactive',
|
||||||
|
MODIFY COLUMN `ContentHash` VARCHAR(64) NOT NULL COMMENT '规范化分区明细 Content 的 SHA256(hex),同 AreaId+Hash 去重复用',
|
||||||
|
MODIFY COLUMN `Content` JSON NULL COMMENT '分区明细快照 JSON(KpiAreaSnapshotRuleDto 数组)',
|
||||||
|
MODIFY COLUMN `PublishUser` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '发布人',
|
||||||
|
MODIFY COLUMN `PublishTime` DATETIME NOT NULL COMMENT '发布时间',
|
||||||
|
MODIFY COLUMN `Description` VARCHAR(512) NULL COMMENT '发布说明',
|
||||||
|
MODIFY COLUMN `IsDeleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '软删除:0=否,1=是',
|
||||||
|
MODIFY COLUMN `CreateUserId` INT NOT NULL DEFAULT 0 COMMENT '创建人',
|
||||||
|
MODIFY COLUMN `CreateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
MODIFY COLUMN `UpdateUserId` INT NULL COMMENT '更新人',
|
||||||
|
MODIFY COLUMN `UpdateTime` DATETIME NULL COMMENT '更新时间';
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
namespace Oms.Application.Dtos.Channels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// KPI 分区包版本头/详情。
|
||||||
|
/// </summary>
|
||||||
|
public class KpiAreaPublishRecordOutput
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public int AreaId { get; set; }
|
||||||
|
|
||||||
|
public string AreaType { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Version { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Status { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string ContentHash { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string PublishUser { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public DateTime PublishTime { get; set; }
|
||||||
|
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>仅 get 接口返回正文。</summary>
|
||||||
|
public object? Content { get; set; }
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
namespace Oms.Application.Dtos.Channels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 销售产品发布同步态(轻量,不含快照正文)。
|
||||||
|
/// </summary>
|
||||||
|
public class SalesProductPublishSyncStatusOutput
|
||||||
|
{
|
||||||
|
/// <summary>相对最新生效版本是否有待发布变更。</summary>
|
||||||
|
public bool Pending { get; set; }
|
||||||
|
|
||||||
|
/// <summary>最新生效版本号;无则 null。</summary>
|
||||||
|
public string? OnlineVersion { get; set; }
|
||||||
|
|
||||||
|
/// <summary>最新生效发布时间。</summary>
|
||||||
|
public DateTime? OnlinePublishTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>待发布快照内容哈希(规范化后)。</summary>
|
||||||
|
public string? PendingContentHash { get; set; }
|
||||||
|
|
||||||
|
/// <summary>线上快照内容哈希。</summary>
|
||||||
|
public string? OnlineContentHash { get; set; }
|
||||||
|
}
|
||||||
17
src/Oms.Application/Dtos/Orders/KpiAreaPackRefDto.cs
Normal file
17
src/Oms.Application/Dtos/Orders/KpiAreaPackRefDto.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
namespace Oms.Application.Dtos.Orders;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 卖价/规则快照内对分区包的引用(明细在 t_kpi_area_publish_record)。
|
||||||
|
/// </summary>
|
||||||
|
public class KpiAreaPackRefDto
|
||||||
|
{
|
||||||
|
public int AreaId { get; set; }
|
||||||
|
|
||||||
|
public string? AreaName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>分区包 Version(yyyyMMddHHmmss)。</summary>
|
||||||
|
public string PackVersion { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>包 ContentHash,便于同步态对比。</summary>
|
||||||
|
public string? ContentHash { get; set; }
|
||||||
|
}
|
||||||
@ -1,9 +1,10 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
using Oms.Application.Dtos.Channels;
|
using Oms.Application.Dtos.Channels;
|
||||||
|
|
||||||
namespace Oms.Application.Dtos.Orders;
|
namespace Oms.Application.Dtos.Orders;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 销售产品价格发布快照(与渠道发布 JSON 同构)。
|
/// 销售产品价格发布快照。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SalesProductPricePublishSnapshotDto
|
public class SalesProductPricePublishSnapshotDto
|
||||||
{
|
{
|
||||||
@ -13,7 +14,7 @@ public class SalesProductPricePublishSnapshotDto
|
|||||||
public int SalesProductId { get; set; }
|
public int SalesProductId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 价格分区 ID(发布时固化,查价/计费优先用快照内分区规则)。
|
/// 价格分区 ID(单表时冗余展示;以 PriceAreaRefs 为准)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? PriceAreaId { get; set; }
|
public int? PriceAreaId { get; set; }
|
||||||
|
|
||||||
@ -23,8 +24,14 @@ public class SalesProductPricePublishSnapshotDto
|
|||||||
public string? PriceAreaName { get; set; }
|
public string? PriceAreaName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 价格分区明细(国家/邮编等),发布时从 t_kpi_area_info 固化;邮编为闭区间。
|
/// 价格分区包引用(发布时钉包;明细在 t_kpi_area_publish_record)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
public List<KpiAreaPackRefDto> PriceAreaRefs { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 运行时 hydrate / LIVE 用分区明细,不落发布 JSON。
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
public List<KpiAreaSnapshotRuleDto> PriceAreaInfos { get; set; } = [];
|
public List<KpiAreaSnapshotRuleDto> PriceAreaInfos { get; set; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -34,7 +41,7 @@ public class SalesProductPricePublishSnapshotDto
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 销售产品规则发布快照(与渠道发布 JSON 同构)。
|
/// 销售产品规则发布快照。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SalesProductRulesPublishSnapshotDto
|
public class SalesProductRulesPublishSnapshotDto
|
||||||
{
|
{
|
||||||
@ -49,7 +56,7 @@ public class SalesProductRulesPublishSnapshotDto
|
|||||||
public List<SalesProductAllocationProviderSnapshotDto> Providers { get; set; } = [];
|
public List<SalesProductAllocationProviderSnapshotDto> Providers { get; set; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 黑名单分区 ID(发布时固化)。
|
/// 黑名单分区 ID。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? BlacklistAreaId { get; set; }
|
public int? BlacklistAreaId { get; set; }
|
||||||
|
|
||||||
@ -59,12 +66,18 @@ public class SalesProductRulesPublishSnapshotDto
|
|||||||
public string? BlacklistAreaName { get; set; }
|
public string? BlacklistAreaName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 黑名单分区明细,发布时从 t_kpi_area_info 固化;邮编为闭区间。
|
/// 黑名单分区包引用。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
public KpiAreaPackRefDto? BlacklistAreaRef { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 运行时 hydrate / LIVE 用,不落发布 JSON。
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
public List<KpiAreaSnapshotRuleDto> BlacklistAreaInfos { get; set; } = [];
|
public List<KpiAreaSnapshotRuleDto> BlacklistAreaInfos { get; set; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 偏远分区 ID(发布时固化)。
|
/// 偏远分区 ID。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? RemoteAreaId { get; set; }
|
public int? RemoteAreaId { get; set; }
|
||||||
|
|
||||||
@ -74,8 +87,14 @@ public class SalesProductRulesPublishSnapshotDto
|
|||||||
public string? RemoteAreaName { get; set; }
|
public string? RemoteAreaName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 偏远分区明细,发布时从 t_kpi_area_info 固化;邮编为闭区间。
|
/// 偏远分区包引用。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
public KpiAreaPackRefDto? RemoteAreaRef { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 运行时 hydrate / LIVE 用,不落发布 JSON。
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
public List<KpiAreaSnapshotRuleDto> RemoteAreaInfos { get; set; } = [];
|
public List<KpiAreaSnapshotRuleDto> RemoteAreaInfos { get; set; } = [];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -7,6 +7,7 @@ using Oms.Application.Dtos.Shared;
|
|||||||
using Oms.Application.Shared;
|
using Oms.Application.Shared;
|
||||||
using Oms.Domain.ChannelsAggregate.Entities;
|
using Oms.Domain.ChannelsAggregate.Entities;
|
||||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||||
|
using Oms.Domain.Shared;
|
||||||
using Oms.Domain.SystemAggregate.Repositories;
|
using Oms.Domain.SystemAggregate.Repositories;
|
||||||
|
|
||||||
namespace Oms.Application.Services.Channels;
|
namespace Oms.Application.Services.Channels;
|
||||||
@ -390,4 +391,63 @@ public class KpiAreaService : ApplicationService
|
|||||||
UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime)
|
UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 分区包版本列表(不含正文)。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("/channel/kpi-area/publish/list")]
|
||||||
|
public async Task<List<KpiAreaPublishRecordOutput>> GetPublishListAsync([FromQuery] int areaId)
|
||||||
|
{
|
||||||
|
if (areaId <= 0)
|
||||||
|
{
|
||||||
|
throw new Fake.Application.BusinessException("分区表 Id 无效");
|
||||||
|
}
|
||||||
|
|
||||||
|
await GetAreaOrThrowAsync(areaId);
|
||||||
|
var headers = await KpiAreaRepository.GetPublishHeadersByAreaIdAsync(areaId);
|
||||||
|
return headers.Select(MapPublishHeader).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单条分区包(含正文)。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("/channel/kpi-area/publish/get")]
|
||||||
|
public async Task<KpiAreaPublishRecordOutput> GetPublishRecordAsync([FromQuery] int id)
|
||||||
|
{
|
||||||
|
var entity = await KpiAreaRepository.GetPublishByIdAsync(id)
|
||||||
|
?? throw new Fake.Application.BusinessException("分区包不存在");
|
||||||
|
return MapPublishDetail(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<KpiArea> GetAreaOrThrowAsync(int areaId)
|
||||||
|
{
|
||||||
|
var area = await KpiAreaRepository.FirstOrDefaultAsync(x => x.Id == areaId);
|
||||||
|
if (area == null)
|
||||||
|
{
|
||||||
|
throw new Fake.Application.BusinessException("分区表不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
return area;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static KpiAreaPublishRecordOutput MapPublishHeader(KpiAreaPublishRecord entity) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = entity.Id,
|
||||||
|
AreaId = entity.AreaId,
|
||||||
|
AreaType = entity.AreaType,
|
||||||
|
Version = entity.Version,
|
||||||
|
Status = EnumDisplayNames.Of(entity.Status),
|
||||||
|
ContentHash = entity.ContentHash,
|
||||||
|
PublishUser = entity.PublishUser,
|
||||||
|
PublishTime = entity.PublishTime,
|
||||||
|
Description = entity.Description
|
||||||
|
};
|
||||||
|
|
||||||
|
private static KpiAreaPublishRecordOutput MapPublishDetail(KpiAreaPublishRecord entity)
|
||||||
|
{
|
||||||
|
var output = MapPublishHeader(entity);
|
||||||
|
output.Content = OmsDocHelper.DeserializeJsonObject(entity.Content);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,8 +38,28 @@ public partial class SalesProductService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var warnings = new List<string>();
|
var warnings = new List<string>();
|
||||||
var priceSnapshot = await BuildPricePublishSnapshotAsync(product.Id);
|
// 模拟用当前草稿:LIVE Infos,不强制走已发布包
|
||||||
var rulesSnapshot = await BuildRulesPublishSnapshotAsync(product.Id);
|
var priceSnapshot = new SalesProductPricePublishSnapshotDto
|
||||||
|
{
|
||||||
|
SalesProductId = product.Id,
|
||||||
|
Prices = (await GetPriceOutputsAsync(product.Id)).Select(PublishSnapshotSlimmer.SlimPrice).ToList()
|
||||||
|
};
|
||||||
|
await PriceAreaSnapshotHelper.ApplyLivePriceAreaInfosAsync(
|
||||||
|
priceSnapshot,
|
||||||
|
product,
|
||||||
|
KpiAreaRepository,
|
||||||
|
KpiAreaPackService);
|
||||||
|
var rulesSnapshot = new SalesProductRulesPublishSnapshotDto
|
||||||
|
{
|
||||||
|
Product = MapProductToRulesSaveInput(product)
|
||||||
|
};
|
||||||
|
await RulesSnapshotHelper.ApplyLiveRulesToSnapshotAsync(
|
||||||
|
rulesSnapshot,
|
||||||
|
product,
|
||||||
|
KpiAreaRepository,
|
||||||
|
WeightRuleRepository,
|
||||||
|
WeightRoundingRepository,
|
||||||
|
KpiAreaPackService);
|
||||||
|
|
||||||
var weight = new OrderWeight
|
var weight = new OrderWeight
|
||||||
{
|
{
|
||||||
@ -261,7 +281,7 @@ public partial class SalesProductService
|
|||||||
return ParcelBillingCalculator.ApplyRounding(rawWeight, ranges);
|
return ParcelBillingCalculator.ApplyRounding(rawWeight, ranges);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> IsSimulateRemoteHitAsync(
|
private Task<bool> IsSimulateRemoteHitAsync(
|
||||||
SalesProduct product,
|
SalesProduct product,
|
||||||
SalesProductRulesPublishSnapshotDto rulesSnapshot,
|
SalesProductRulesPublishSnapshotDto rulesSnapshot,
|
||||||
string countryCode,
|
string countryCode,
|
||||||
@ -269,24 +289,17 @@ public partial class SalesProductService
|
|||||||
string? city,
|
string? city,
|
||||||
string? zipCode)
|
string? zipCode)
|
||||||
{
|
{
|
||||||
var areaId = product.RemoteAreaId is > 0 ? product.RemoteAreaId : rulesSnapshot.RemoteAreaId;
|
|
||||||
if (areaId is not > 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rulesSnapshot.RemoteAreaInfos is { Count: > 0 })
|
if (rulesSnapshot.RemoteAreaInfos is { Count: > 0 })
|
||||||
{
|
{
|
||||||
return GeoPartitionMatcher.IsHit(
|
return Task.FromResult(GeoPartitionMatcher.IsHit(
|
||||||
rulesSnapshot.RemoteAreaInfos,
|
rulesSnapshot.RemoteAreaInfos,
|
||||||
countryCode,
|
countryCode,
|
||||||
province,
|
province,
|
||||||
city,
|
city,
|
||||||
zipCode);
|
zipCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
var rules = await KpiAreaRepository.GetAllInfosByAreaIdAsync(areaId.Value);
|
return Task.FromResult(false);
|
||||||
return GeoPartitionMatcher.IsHit(rules, countryCode, province, city, zipCode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string?> ResolveSimulateWarehouseNameAsync(int? warehouseId)
|
private async Task<string?> ResolveSimulateWarehouseNameAsync(int? warehouseId)
|
||||||
|
|||||||
@ -290,13 +290,15 @@ public partial class SalesProductService
|
|||||||
product,
|
product,
|
||||||
KpiAreaRepository,
|
KpiAreaRepository,
|
||||||
WeightRuleRepository,
|
WeightRuleRepository,
|
||||||
WeightRoundingRepository);
|
WeightRoundingRepository,
|
||||||
|
KpiAreaPackService,
|
||||||
|
ResolvePublishUser());
|
||||||
await AllocationProviderSnapshotEnricher.EnrichAsync(rulesSnapshot.Providers);
|
await AllocationProviderSnapshotEnricher.EnrichAsync(rulesSnapshot.Providers);
|
||||||
return rulesSnapshot;
|
return rulesSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 组装价格发布快照(不落库)。
|
/// 组装价格发布快照(不落库);价区钉分区包 Refs。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<SalesProductPricePublishSnapshotDto> BuildPricePublishSnapshotAsync(int salesProductId)
|
private async Task<SalesProductPricePublishSnapshotDto> BuildPricePublishSnapshotAsync(int salesProductId)
|
||||||
{
|
{
|
||||||
@ -306,10 +308,20 @@ public partial class SalesProductService
|
|||||||
SalesProductId = salesProductId,
|
SalesProductId = salesProductId,
|
||||||
Prices = (await GetPriceOutputsAsync(salesProductId)).Select(PublishSnapshotSlimmer.SlimPrice).ToList()
|
Prices = (await GetPriceOutputsAsync(salesProductId)).Select(PublishSnapshotSlimmer.SlimPrice).ToList()
|
||||||
};
|
};
|
||||||
await PriceAreaSnapshotHelper.ApplyPriceAreaToSnapshotAsync(priceSnapshot, product, KpiAreaRepository);
|
await PriceAreaSnapshotHelper.ApplyPriceAreaToSnapshotAsync(
|
||||||
|
priceSnapshot,
|
||||||
|
product,
|
||||||
|
KpiAreaRepository,
|
||||||
|
KpiAreaPackService,
|
||||||
|
ResolvePublishUser());
|
||||||
return priceSnapshot;
|
return priceSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string ResolvePublishUser() =>
|
||||||
|
string.IsNullOrWhiteSpace(CurrentUser.UserName)
|
||||||
|
? CurrentUser.GetId<int>().ToString()
|
||||||
|
: CurrentUser.UserName!;
|
||||||
|
|
||||||
private static SalesProductSaveInput MapProductToRulesSaveInput(SalesProduct product) =>
|
private static SalesProductSaveInput MapProductToRulesSaveInput(SalesProduct product) =>
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -23,6 +23,7 @@ public partial class SalesProductService : ApplicationService
|
|||||||
private IKpiAreaRepository KpiAreaRepository => LazyServiceProvider.GetRequiredService<IKpiAreaRepository>();
|
private IKpiAreaRepository KpiAreaRepository => LazyServiceProvider.GetRequiredService<IKpiAreaRepository>();
|
||||||
private IWeightRuleRepository WeightRuleRepository => LazyServiceProvider.GetRequiredService<IWeightRuleRepository>();
|
private IWeightRuleRepository WeightRuleRepository => LazyServiceProvider.GetRequiredService<IWeightRuleRepository>();
|
||||||
private IWeightRoundingRepository WeightRoundingRepository => LazyServiceProvider.GetRequiredService<IWeightRoundingRepository>();
|
private IWeightRoundingRepository WeightRoundingRepository => LazyServiceProvider.GetRequiredService<IWeightRoundingRepository>();
|
||||||
|
private KpiAreaPackService KpiAreaPackService => LazyServiceProvider.GetRequiredService<KpiAreaPackService>();
|
||||||
private AllocationProviderSnapshotEnricher AllocationProviderSnapshotEnricher =>
|
private AllocationProviderSnapshotEnricher AllocationProviderSnapshotEnricher =>
|
||||||
LazyServiceProvider.GetRequiredService<AllocationProviderSnapshotEnricher>();
|
LazyServiceProvider.GetRequiredService<AllocationProviderSnapshotEnricher>();
|
||||||
private OmsRbacUserDisplayNameResolver UserDisplayNameResolver =>
|
private OmsRbacUserDisplayNameResolver UserDisplayNameResolver =>
|
||||||
@ -408,6 +409,67 @@ public partial class SalesProductService : ApplicationService
|
|||||||
return OmsDocHelper.ToSnapshotApiContent(snapshot)!;
|
return OmsDocHelper.ToSnapshotApiContent(snapshot)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发布同步态(轻量):对比待发布与最新生效快照,不回传正文。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("/channel/sales-product/publish/sync-status")]
|
||||||
|
public async Task<SalesProductPublishSyncStatusOutput> GetPublishSyncStatusAsync(
|
||||||
|
[FromQuery] int salesProductId,
|
||||||
|
[FromQuery] string operatorType)
|
||||||
|
{
|
||||||
|
var normalized = NormalizeOperatorType(operatorType);
|
||||||
|
await GetProductOrThrowAsync(salesProductId);
|
||||||
|
|
||||||
|
object pendingSnapshot = normalized == PriceOperatorType
|
||||||
|
? await BuildPricePublishSnapshotAsync(salesProductId)
|
||||||
|
: await BuildRulesPublishSnapshotAsync(salesProductId);
|
||||||
|
var pendingJson = OmsDocHelper.SerializeSnapshotJson(pendingSnapshot) ?? string.Empty;
|
||||||
|
var pendingHash = ComputeContentHash(pendingJson);
|
||||||
|
|
||||||
|
var headers = await SalesProductRepository.GetAllPublishRecordHeadersAsync(salesProductId, normalized);
|
||||||
|
var onlineHeader = headers
|
||||||
|
.Where(x => x.Status == Domain.Shared.Enums.EsSnapshotStatus.Active)
|
||||||
|
.OrderByDescending(x => x.PublishTime)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (onlineHeader == null)
|
||||||
|
{
|
||||||
|
return new SalesProductPublishSyncStatusOutput
|
||||||
|
{
|
||||||
|
Pending = true,
|
||||||
|
OnlineVersion = null,
|
||||||
|
OnlinePublishTime = null,
|
||||||
|
PendingContentHash = pendingHash,
|
||||||
|
OnlineContentHash = null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var onlineRecord = await SalesProductRepository.GetPublishRecordByIdAsync(onlineHeader.Id);
|
||||||
|
var onlineJson = onlineRecord?.Content ?? string.Empty;
|
||||||
|
// 线上 Content 与待发布统一经反序列化再序列化,避免字段顺序噪声
|
||||||
|
object? onlineNormalized = normalized == PriceOperatorType
|
||||||
|
? OmsDocHelper.DeserializePriceSnapshot(onlineJson)
|
||||||
|
: OmsDocHelper.DeserializeRulesSnapshot(onlineJson);
|
||||||
|
var onlineCanon = OmsDocHelper.SerializeSnapshotJson(onlineNormalized) ?? string.Empty;
|
||||||
|
var onlineHash = ComputeContentHash(onlineCanon);
|
||||||
|
|
||||||
|
return new SalesProductPublishSyncStatusOutput
|
||||||
|
{
|
||||||
|
Pending = !string.Equals(pendingHash, onlineHash, StringComparison.Ordinal),
|
||||||
|
OnlineVersion = onlineHeader.Version,
|
||||||
|
OnlinePublishTime = onlineHeader.PublishTime,
|
||||||
|
PendingContentHash = pendingHash,
|
||||||
|
OnlineContentHash = onlineHash
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ComputeContentHash(string content)
|
||||||
|
{
|
||||||
|
var hash = global::System.Security.Cryptography.SHA256.HashData(
|
||||||
|
global::System.Text.Encoding.UTF8.GetBytes(content));
|
||||||
|
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 执行销售产品发布。
|
/// 执行销售产品发布。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -51,6 +51,7 @@ public partial class OrderService
|
|||||||
private IWeightRuleRepository WeightRuleRepository => LazyServiceProvider.GetRequiredService<IWeightRuleRepository>();
|
private IWeightRuleRepository WeightRuleRepository => LazyServiceProvider.GetRequiredService<IWeightRuleRepository>();
|
||||||
private IWeightRoundingRepository WeightRoundingRepository => LazyServiceProvider.GetRequiredService<IWeightRoundingRepository>();
|
private IWeightRoundingRepository WeightRoundingRepository => LazyServiceProvider.GetRequiredService<IWeightRoundingRepository>();
|
||||||
private IKpiAreaRepository KpiAreaRepository => LazyServiceProvider.GetRequiredService<IKpiAreaRepository>();
|
private IKpiAreaRepository KpiAreaRepository => LazyServiceProvider.GetRequiredService<IKpiAreaRepository>();
|
||||||
|
private KpiAreaPackService KpiAreaPackService => LazyServiceProvider.GetRequiredService<KpiAreaPackService>();
|
||||||
private AllocationProviderSnapshotEnricher AllocationProviderSnapshotEnricher =>
|
private AllocationProviderSnapshotEnricher AllocationProviderSnapshotEnricher =>
|
||||||
LazyServiceProvider.GetRequiredService<AllocationProviderSnapshotEnricher>();
|
LazyServiceProvider.GetRequiredService<AllocationProviderSnapshotEnricher>();
|
||||||
|
|
||||||
@ -552,7 +553,7 @@ public partial class OrderService
|
|||||||
throw new BusinessException("未命中规则快照版本(内存/Redis/DB)");
|
throw new BusinessException("未命中规则快照版本(内存/Redis/DB)");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ProductSnapshotBinding
|
var binding = new ProductSnapshotBinding
|
||||||
{
|
{
|
||||||
RulesVersion = rulesRecord?.Version ?? $"LIVE-{product.Id}",
|
RulesVersion = rulesRecord?.Version ?? $"LIVE-{product.Id}",
|
||||||
PriceVersion = priceRecord?.Version ?? $"LIVE-{product.Id}",
|
PriceVersion = priceRecord?.Version ?? $"LIVE-{product.Id}",
|
||||||
@ -561,6 +562,8 @@ public partial class OrderService
|
|||||||
Rules = rulesSnapshot,
|
Rules = rulesSnapshot,
|
||||||
Price = priceSnapshot
|
Price = priceSnapshot
|
||||||
};
|
};
|
||||||
|
await KpiAreaPackService.HydrateBindingAsync(binding);
|
||||||
|
return binding;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ProductSnapshotBinding> ResolveSnapshotBindingFromOrderAsync(SalesProduct product, Order order)
|
private async Task<ProductSnapshotBinding> ResolveSnapshotBindingFromOrderAsync(SalesProduct product, Order order)
|
||||||
@ -632,7 +635,7 @@ public partial class OrderService
|
|||||||
priceEffectiveTime = priceRecord.PublishTime;
|
priceEffectiveTime = priceRecord.PublishTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ProductSnapshotBinding
|
var binding = new ProductSnapshotBinding
|
||||||
{
|
{
|
||||||
RulesVersion = rulesVersion,
|
RulesVersion = rulesVersion,
|
||||||
PriceVersion = priceVersion,
|
PriceVersion = priceVersion,
|
||||||
@ -641,6 +644,8 @@ public partial class OrderService
|
|||||||
Rules = rulesSnapshot,
|
Rules = rulesSnapshot,
|
||||||
Price = priceSnapshot
|
Price = priceSnapshot
|
||||||
};
|
};
|
||||||
|
await KpiAreaPackService.HydrateBindingAsync(binding);
|
||||||
|
return binding;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string?> ResolveWarehouseNameAsync(int? warehouseId)
|
private async Task<string?> ResolveWarehouseNameAsync(int? warehouseId)
|
||||||
@ -1412,12 +1417,13 @@ public partial class OrderService
|
|||||||
Scheme = product.Scheme
|
Scheme = product.Scheme
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
await RulesSnapshotHelper.ApplyRulesToSnapshotAsync(
|
await RulesSnapshotHelper.ApplyLiveRulesToSnapshotAsync(
|
||||||
snapshot,
|
snapshot,
|
||||||
product,
|
product,
|
||||||
KpiAreaRepository,
|
KpiAreaRepository,
|
||||||
WeightRuleRepository,
|
WeightRuleRepository,
|
||||||
WeightRoundingRepository);
|
WeightRoundingRepository,
|
||||||
|
KpiAreaPackService);
|
||||||
snapshot.Providers = await ResolveAllocationProvidersAsync(product, snapshot);
|
snapshot.Providers = await ResolveAllocationProvidersAsync(product, snapshot);
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
@ -1455,7 +1461,11 @@ public partial class OrderService
|
|||||||
: []
|
: []
|
||||||
})).ToList()
|
})).ToList()
|
||||||
};
|
};
|
||||||
await PriceAreaSnapshotHelper.ApplyPriceAreaToSnapshotAsync(snapshot, product, KpiAreaRepository);
|
await PriceAreaSnapshotHelper.ApplyLivePriceAreaInfosAsync(
|
||||||
|
snapshot,
|
||||||
|
product,
|
||||||
|
KpiAreaRepository,
|
||||||
|
KpiAreaPackService);
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1484,7 +1494,7 @@ public partial class OrderService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> IsBlacklistHitAsync(
|
private Task<bool> IsBlacklistHitAsync(
|
||||||
SalesProduct product,
|
SalesProduct product,
|
||||||
SalesProductRulesPublishSnapshotDto? rulesSnapshot,
|
SalesProductRulesPublishSnapshotDto? rulesSnapshot,
|
||||||
string countryCode,
|
string countryCode,
|
||||||
@ -1492,27 +1502,26 @@ public partial class OrderService
|
|||||||
string? city,
|
string? city,
|
||||||
string? zipCode)
|
string? zipCode)
|
||||||
{
|
{
|
||||||
var areaId = ResolveEffectiveBlacklistAreaId(product, rulesSnapshot);
|
if (rulesSnapshot?.BlacklistAreaRef != null && rulesSnapshot.BlacklistAreaInfos is not { Count: > 0 })
|
||||||
if (!areaId.HasValue)
|
|
||||||
{
|
{
|
||||||
return false;
|
throw new BusinessException(
|
||||||
|
$"黑名单分区包未加载: areaId={rulesSnapshot.BlacklistAreaRef.AreaId} version={rulesSnapshot.BlacklistAreaRef.PackVersion}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rulesSnapshot?.BlacklistAreaInfos is { Count: > 0 })
|
if (rulesSnapshot?.BlacklistAreaInfos is { Count: > 0 })
|
||||||
{
|
{
|
||||||
return GeoPartitionMatcher.IsHit(
|
return Task.FromResult(GeoPartitionMatcher.IsHit(
|
||||||
rulesSnapshot.BlacklistAreaInfos,
|
rulesSnapshot.BlacklistAreaInfos,
|
||||||
countryCode,
|
countryCode,
|
||||||
province,
|
province,
|
||||||
city,
|
city,
|
||||||
zipCode);
|
zipCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
var rules = await KpiAreaRepository.GetAllInfosByAreaIdAsync(areaId.Value);
|
return Task.FromResult(false);
|
||||||
return GeoPartitionMatcher.IsHit(rules, countryCode, province, city, zipCode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> IsRemoteHitAsync(
|
private Task<bool> IsRemoteHitAsync(
|
||||||
SalesProduct product,
|
SalesProduct product,
|
||||||
SalesProductRulesPublishSnapshotDto? rulesSnapshot,
|
SalesProductRulesPublishSnapshotDto? rulesSnapshot,
|
||||||
string countryCode,
|
string countryCode,
|
||||||
@ -1520,24 +1529,23 @@ public partial class OrderService
|
|||||||
string? city,
|
string? city,
|
||||||
string? zipCode)
|
string? zipCode)
|
||||||
{
|
{
|
||||||
var areaId = ResolveEffectiveRemoteAreaId(product, rulesSnapshot);
|
if (rulesSnapshot?.RemoteAreaRef != null && rulesSnapshot.RemoteAreaInfos is not { Count: > 0 })
|
||||||
if (!areaId.HasValue)
|
|
||||||
{
|
{
|
||||||
return false;
|
throw new BusinessException(
|
||||||
|
$"偏远分区包未加载: areaId={rulesSnapshot.RemoteAreaRef.AreaId} version={rulesSnapshot.RemoteAreaRef.PackVersion}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rulesSnapshot?.RemoteAreaInfos is { Count: > 0 })
|
if (rulesSnapshot?.RemoteAreaInfos is { Count: > 0 })
|
||||||
{
|
{
|
||||||
return GeoPartitionMatcher.IsHit(
|
return Task.FromResult(GeoPartitionMatcher.IsHit(
|
||||||
rulesSnapshot.RemoteAreaInfos,
|
rulesSnapshot.RemoteAreaInfos,
|
||||||
countryCode,
|
countryCode,
|
||||||
province,
|
province,
|
||||||
city,
|
city,
|
||||||
zipCode);
|
zipCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
var rules = await KpiAreaRepository.GetAllInfosByAreaIdAsync(areaId.Value);
|
return Task.FromResult(false);
|
||||||
return GeoPartitionMatcher.IsHit(rules, countryCode, province, city, zipCode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int? ResolveEffectiveBlacklistAreaId(
|
private static int? ResolveEffectiveBlacklistAreaId(
|
||||||
@ -1607,7 +1615,7 @@ public partial class OrderService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IReadOnlyList<string>?> ResolvePriceZoneKeysAsync(
|
private Task<IReadOnlyList<string>?> ResolvePriceZoneKeysAsync(
|
||||||
SalesProduct product,
|
SalesProduct product,
|
||||||
SalesProductRulesPublishSnapshotDto? rulesSnapshot,
|
SalesProductRulesPublishSnapshotDto? rulesSnapshot,
|
||||||
SalesProductPricePublishSnapshotDto? priceSnapshot,
|
SalesProductPricePublishSnapshotDto? priceSnapshot,
|
||||||
@ -1616,42 +1624,24 @@ public partial class OrderService
|
|||||||
string? city,
|
string? city,
|
||||||
string? zipCode)
|
string? zipCode)
|
||||||
{
|
{
|
||||||
|
if (priceSnapshot?.PriceAreaRefs is { Count: > 0 }
|
||||||
|
&& priceSnapshot.PriceAreaInfos is not { Count: > 0 })
|
||||||
|
{
|
||||||
|
throw new BusinessException("价格分区包未加载,无法匹配分区");
|
||||||
|
}
|
||||||
|
|
||||||
if (priceSnapshot?.PriceAreaInfos is { Count: > 0 })
|
if (priceSnapshot?.PriceAreaInfos is { Count: > 0 })
|
||||||
{
|
{
|
||||||
return ParcelBillingCalculator.TryResolveZoneKeysFromPriceSnapshot(
|
return Task.FromResult(ParcelBillingCalculator.TryResolveZoneKeysFromPriceSnapshot(
|
||||||
priceSnapshot,
|
priceSnapshot,
|
||||||
countryCode,
|
countryCode,
|
||||||
province,
|
province,
|
||||||
city,
|
city,
|
||||||
zipCode);
|
zipCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
var priceAreaId = ResolveEffectivePriceAreaId(product, rulesSnapshot, priceSnapshot);
|
// 无分区配置:返回 null;禁止回读线上 KPI(正式路径须走包)
|
||||||
if (!priceAreaId.HasValue)
|
return Task.FromResult<IReadOnlyList<string>?>(null);
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var area = await KpiAreaRepository.FirstOrDefaultAsync(x => x.Id == priceAreaId.Value);
|
|
||||||
if (area == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var rules = await KpiAreaRepository.GetAllInfosByAreaIdAsync(priceAreaId.Value);
|
|
||||||
var matched = GeoPartitionMatcher.MatchEntity(
|
|
||||||
rules,
|
|
||||||
countryCode,
|
|
||||||
province,
|
|
||||||
city,
|
|
||||||
zipCode);
|
|
||||||
if (matched == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var keys = ParcelBillingCalculator.BuildPriceZoneKeys(matched, area);
|
|
||||||
return keys.Count > 0 ? keys : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ApplyChargeableWeightAsync(
|
private async Task ApplyChargeableWeightAsync(
|
||||||
|
|||||||
@ -737,7 +737,13 @@ public partial class OrderService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 仅国家+重量:分区规则带邮编时无法精确命中,按「同国家」分区给出估算价;
|
// 仅国家+重量:分区规则带邮编时无法精确命中,按「同国家」分区给出估算价;
|
||||||
// 快照中完全没有该国家时不跨国家兜底。
|
// 须已 hydrate;禁止回读线上 KPI。
|
||||||
|
if (snapshotBinding.Price?.PriceAreaRefs is { Count: > 0 }
|
||||||
|
&& snapshotBinding.Price.PriceAreaInfos is not { Count: > 0 })
|
||||||
|
{
|
||||||
|
throw new BusinessException("价格分区包未加载,无法估算分区");
|
||||||
|
}
|
||||||
|
|
||||||
if (snapshotBinding.Price?.PriceAreaInfos is { Count: > 0 })
|
if (snapshotBinding.Price?.PriceAreaInfos is { Count: > 0 })
|
||||||
{
|
{
|
||||||
foreach (var zoneKey in ParcelBillingCalculator.CollectCountryEstimateZoneKeys(
|
foreach (var zoneKey in ParcelBillingCalculator.CollectCountryEstimateZoneKeys(
|
||||||
@ -746,42 +752,6 @@ public partial class OrderService
|
|||||||
{
|
{
|
||||||
attempts.Add(new QuoteZoneAttempt([zoneKey], true, zoneKey));
|
attempts.Add(new QuoteZoneAttempt([zoneKey], true, zoneKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
return attempts;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 无快照分区明细时,退回库表:同国家且不要求邮编的规则
|
|
||||||
var priceAreaId = ResolveEffectivePriceAreaId(product, snapshotBinding.Rules, snapshotBinding.Price);
|
|
||||||
if (priceAreaId is > 0)
|
|
||||||
{
|
|
||||||
var area = await KpiAreaRepository.FirstOrDefaultAsync(x => x.Id == priceAreaId.Value);
|
|
||||||
var rules = await KpiAreaRepository.GetAllInfosByAreaIdAsync(priceAreaId.Value);
|
|
||||||
foreach (var rule in rules)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(rule.CountryCode)
|
|
||||||
&& !string.Equals(rule.CountryCode, context.CountryCode, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(rule.Province) || !string.IsNullOrWhiteSpace(rule.City))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var keys = ParcelBillingCalculator.BuildPriceZoneKeys(rule, area);
|
|
||||||
if (keys.Count == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attempts.Any(x => x.ZoneKeys.SequenceEqual(keys, StringComparer.OrdinalIgnoreCase)))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
attempts.Add(new QuoteZoneAttempt(keys.ToList(), true, keys[0]));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return attempts;
|
return attempts;
|
||||||
|
|||||||
@ -25,6 +25,7 @@ public class PickupOrderFeePushService : IScopedDependency
|
|||||||
private readonly IWeightRoundingRepository _weightRoundingRepository;
|
private readonly IWeightRoundingRepository _weightRoundingRepository;
|
||||||
private readonly IKpiAreaRepository _kpiAreaRepository;
|
private readonly IKpiAreaRepository _kpiAreaRepository;
|
||||||
private readonly IWarehouseAddressRepository _warehouseAddressRepository;
|
private readonly IWarehouseAddressRepository _warehouseAddressRepository;
|
||||||
|
private readonly KpiAreaPackService _kpiAreaPackService;
|
||||||
|
|
||||||
public PickupOrderFeePushService(
|
public PickupOrderFeePushService(
|
||||||
IPickupOrderRepository pickupOrderRepository,
|
IPickupOrderRepository pickupOrderRepository,
|
||||||
@ -33,7 +34,8 @@ public class PickupOrderFeePushService : IScopedDependency
|
|||||||
IWeightRuleRepository weightRuleRepository,
|
IWeightRuleRepository weightRuleRepository,
|
||||||
IWeightRoundingRepository weightRoundingRepository,
|
IWeightRoundingRepository weightRoundingRepository,
|
||||||
IKpiAreaRepository kpiAreaRepository,
|
IKpiAreaRepository kpiAreaRepository,
|
||||||
IWarehouseAddressRepository warehouseAddressRepository)
|
IWarehouseAddressRepository warehouseAddressRepository,
|
||||||
|
KpiAreaPackService kpiAreaPackService)
|
||||||
{
|
{
|
||||||
_pickupOrderRepository = pickupOrderRepository;
|
_pickupOrderRepository = pickupOrderRepository;
|
||||||
_salesProductRepository = salesProductRepository;
|
_salesProductRepository = salesProductRepository;
|
||||||
@ -42,6 +44,7 @@ public class PickupOrderFeePushService : IScopedDependency
|
|||||||
_weightRoundingRepository = weightRoundingRepository;
|
_weightRoundingRepository = weightRoundingRepository;
|
||||||
_kpiAreaRepository = kpiAreaRepository;
|
_kpiAreaRepository = kpiAreaRepository;
|
||||||
_warehouseAddressRepository = warehouseAddressRepository;
|
_warehouseAddressRepository = warehouseAddressRepository;
|
||||||
|
_kpiAreaPackService = kpiAreaPackService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PickupOpenApiPushFeeResult> PushFeeAsync(PickupOpenApiPushFeeInput? input)
|
public async Task<PickupOpenApiPushFeeResult> PushFeeAsync(PickupOpenApiPushFeeInput? input)
|
||||||
@ -245,7 +248,11 @@ public class PickupOrderFeePushService : IScopedDependency
|
|||||||
SalesProductId = product.Id,
|
SalesProductId = product.Id,
|
||||||
Prices = (await GetSellPriceOutputsAsync(product.Id)).Select(PublishSnapshotSlimmer.SlimPrice).ToList()
|
Prices = (await GetSellPriceOutputsAsync(product.Id)).Select(PublishSnapshotSlimmer.SlimPrice).ToList()
|
||||||
};
|
};
|
||||||
await PriceAreaSnapshotHelper.ApplyPriceAreaToSnapshotAsync(snapshot, product, _kpiAreaRepository);
|
await PriceAreaSnapshotHelper.ApplyLivePriceAreaInfosAsync(
|
||||||
|
snapshot,
|
||||||
|
product,
|
||||||
|
_kpiAreaRepository,
|
||||||
|
_kpiAreaPackService);
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
317
src/Oms.Application/Shared/KpiAreaPackService.cs
Normal file
317
src/Oms.Application/Shared/KpiAreaPackService.cs
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Fake.Application;
|
||||||
|
using Fake.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Oms.Application.Dtos.Orders;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Entities;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||||
|
using Oms.Domain.Shared.Enums;
|
||||||
|
|
||||||
|
namespace Oms.Application.Shared;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// KPI 分区包:发布时 Ensure(ContentHash 去重),计费按 version 加载(缓存),缺失严格失败。
|
||||||
|
/// </summary>
|
||||||
|
public class KpiAreaPackService : IScopedDependency
|
||||||
|
{
|
||||||
|
public const string AreaTypePrice = "price";
|
||||||
|
public const string AreaTypeBlacklist = "blacklist";
|
||||||
|
public const string AreaTypeRemote = "remote";
|
||||||
|
|
||||||
|
private static readonly MemoryCache PackCache = new(new MemoryCacheOptions
|
||||||
|
{
|
||||||
|
SizeLimit = 256
|
||||||
|
});
|
||||||
|
|
||||||
|
private readonly IKpiAreaRepository _kpiAreaRepository;
|
||||||
|
|
||||||
|
public KpiAreaPackService(IKpiAreaRepository kpiAreaRepository)
|
||||||
|
{
|
||||||
|
_kpiAreaRepository = kpiAreaRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按当前库明细组包;同 AreaId+ContentHash 复用已有 Version。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<KpiAreaPackRefDto> EnsurePackAsync(
|
||||||
|
int areaId,
|
||||||
|
string areaType,
|
||||||
|
string? publishUser = null,
|
||||||
|
string? description = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (areaId <= 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException("分区表 Id 无效");
|
||||||
|
}
|
||||||
|
|
||||||
|
var area = await _kpiAreaRepository.FirstOrDefaultAsync(x => x.Id == areaId, cancellationToken)
|
||||||
|
?? throw new BusinessException($"分区表不存在: {areaId}");
|
||||||
|
|
||||||
|
var infos = await _kpiAreaRepository.GetAllInfosByAreaIdAsync(areaId, cancellationToken);
|
||||||
|
var rules = infos.Select(info => KpiAreaSnapshotMapper.MapToSnapshotRule(info, area.Name)).ToList();
|
||||||
|
PublishSnapshotNormalizer.NormalizeKpiAreas(rules);
|
||||||
|
rules = rules.OrderBy(x => x.Id).ToList();
|
||||||
|
|
||||||
|
var contentJson = OmsDocHelper.SerializeSnapshotJson(rules) ?? "[]";
|
||||||
|
var contentHash = ComputeSha256Hex(contentJson);
|
||||||
|
|
||||||
|
var existing = await _kpiAreaRepository.FindPublishByAreaIdAndContentHashAsync(
|
||||||
|
areaId,
|
||||||
|
contentHash,
|
||||||
|
cancellationToken);
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
return ToRef(existing, area.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
var version = DateTime.Now.ToString("yyyyMMddHHmmssfff");
|
||||||
|
var record = new KpiAreaPublishRecord
|
||||||
|
{
|
||||||
|
AreaId = areaId,
|
||||||
|
AreaType = string.IsNullOrWhiteSpace(areaType) ? area.AreaType : areaType.Trim(),
|
||||||
|
Version = version,
|
||||||
|
Status = EsSnapshotStatus.Active,
|
||||||
|
ContentHash = contentHash,
|
||||||
|
Content = contentJson,
|
||||||
|
PublishUser = string.IsNullOrWhiteSpace(publishUser) ? "system" : publishUser.Trim(),
|
||||||
|
PublishTime = DateTime.Now,
|
||||||
|
Description = description
|
||||||
|
};
|
||||||
|
record = await _kpiAreaRepository.InsertPublishRecordAsync(record, cancellationToken);
|
||||||
|
PackCache.Set(CacheKey(areaId, record.Version), rules, new MemoryCacheEntryOptions
|
||||||
|
{
|
||||||
|
Size = 1,
|
||||||
|
SlidingExpiration = TimeSpan.FromHours(6)
|
||||||
|
});
|
||||||
|
return ToRef(record, area.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按引用加载分区规则;包不存在则严格失败。
|
||||||
|
/// </summary>
|
||||||
|
public async Task<List<KpiAreaSnapshotRuleDto>> GetRulesOrThrowAsync(
|
||||||
|
KpiAreaPackRefDto? packRef,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (packRef == null || packRef.AreaId <= 0 || string.IsNullOrWhiteSpace(packRef.PackVersion))
|
||||||
|
{
|
||||||
|
throw new BusinessException("分区包引用无效");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await GetRulesOrThrowAsync(packRef.AreaId, packRef.PackVersion, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<KpiAreaSnapshotRuleDto>> GetRulesOrThrowAsync(
|
||||||
|
int areaId,
|
||||||
|
string packVersion,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var key = CacheKey(areaId, packVersion);
|
||||||
|
if (PackCache.TryGetValue(key, out List<KpiAreaSnapshotRuleDto>? cached) && cached != null)
|
||||||
|
{
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
var record = await _kpiAreaRepository.GetPublishByAreaIdAndVersionAsync(
|
||||||
|
areaId,
|
||||||
|
packVersion,
|
||||||
|
cancellationToken);
|
||||||
|
if (record == null || string.IsNullOrWhiteSpace(record.Content))
|
||||||
|
{
|
||||||
|
throw new BusinessException($"分区包不存在: areaId={areaId} version={packVersion}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var rules = OmsDocHelper.DeserializeJson<List<KpiAreaSnapshotRuleDto>>(record.Content) ?? [];
|
||||||
|
PublishSnapshotNormalizer.NormalizeKpiAreas(rules);
|
||||||
|
PackCache.Set(key, rules, new MemoryCacheEntryOptions
|
||||||
|
{
|
||||||
|
Size = 1,
|
||||||
|
SlidingExpiration = TimeSpan.FromHours(6)
|
||||||
|
});
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将卖价快照中的 PriceAreaRefs hydrate 到 PriceAreaInfos(运行时,不回写库)。
|
||||||
|
/// </summary>
|
||||||
|
public async Task HydratePriceSnapshotAsync(
|
||||||
|
SalesProductPricePublishSnapshotDto snapshot,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// LIVE/模拟:无 Refs,Infos 已由 LoadLive 填充,勿清空
|
||||||
|
if (snapshot.PriceAreaRefs is not { Count: > 0 })
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var merged = new List<KpiAreaSnapshotRuleDto>();
|
||||||
|
var seen = new HashSet<int>();
|
||||||
|
foreach (var packRef in snapshot.PriceAreaRefs)
|
||||||
|
{
|
||||||
|
var rules = await GetRulesOrThrowAsync(packRef, cancellationToken);
|
||||||
|
foreach (var rule in rules)
|
||||||
|
{
|
||||||
|
if (seen.Add(rule.Id))
|
||||||
|
{
|
||||||
|
merged.Add(rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot.PriceAreaInfos = merged;
|
||||||
|
if (snapshot.PriceAreaRefs.Count == 1)
|
||||||
|
{
|
||||||
|
snapshot.PriceAreaId = snapshot.PriceAreaRefs[0].AreaId;
|
||||||
|
snapshot.PriceAreaName = snapshot.PriceAreaRefs[0].AreaName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
snapshot.PriceAreaId = null;
|
||||||
|
snapshot.PriceAreaName = string.Join(
|
||||||
|
"、",
|
||||||
|
snapshot.PriceAreaRefs
|
||||||
|
.Select(x => x.AreaName)
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将规则快照中的黑名单/偏远 Ref hydrate 到 Infos。
|
||||||
|
/// </summary>
|
||||||
|
public async Task HydrateRulesSnapshotAsync(
|
||||||
|
SalesProductRulesPublishSnapshotDto snapshot,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (snapshot.BlacklistAreaRef != null)
|
||||||
|
{
|
||||||
|
snapshot.BlacklistAreaInfos = await GetRulesOrThrowAsync(snapshot.BlacklistAreaRef, cancellationToken);
|
||||||
|
snapshot.BlacklistAreaId = snapshot.BlacklistAreaRef.AreaId;
|
||||||
|
snapshot.BlacklistAreaName = snapshot.BlacklistAreaRef.AreaName;
|
||||||
|
}
|
||||||
|
// else:LIVE 保留已有 Infos
|
||||||
|
|
||||||
|
if (snapshot.RemoteAreaRef != null)
|
||||||
|
{
|
||||||
|
snapshot.RemoteAreaInfos = await GetRulesOrThrowAsync(snapshot.RemoteAreaRef, cancellationToken);
|
||||||
|
snapshot.RemoteAreaId = snapshot.RemoteAreaRef.AreaId;
|
||||||
|
snapshot.RemoteAreaName = snapshot.RemoteAreaRef.AreaName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task HydrateBindingAsync(
|
||||||
|
ProductSnapshotBinding binding,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await HydratePriceSnapshotAsync(binding.Price, cancellationToken);
|
||||||
|
await HydrateRulesSnapshotAsync(binding.Rules, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从当前库装载分区规则到 Infos(仅 LIVE/模拟;不写包、不写 Ref)。
|
||||||
|
/// </summary>
|
||||||
|
public async Task LoadLiveInfosIntoPriceSnapshotAsync(
|
||||||
|
SalesProductPricePublishSnapshotDto snapshot,
|
||||||
|
IReadOnlyList<int> areaIds,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var merged = new List<KpiAreaSnapshotRuleDto>();
|
||||||
|
var seen = new HashSet<int>();
|
||||||
|
var names = new List<string>();
|
||||||
|
foreach (var areaId in areaIds.Where(id => id > 0).Distinct())
|
||||||
|
{
|
||||||
|
var area = await _kpiAreaRepository.FirstOrDefaultAsync(x => x.Id == areaId, cancellationToken);
|
||||||
|
if (!string.IsNullOrWhiteSpace(area?.Name))
|
||||||
|
{
|
||||||
|
names.Add(area.Name.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
var infos = await _kpiAreaRepository.GetAllInfosByAreaIdAsync(areaId, cancellationToken);
|
||||||
|
foreach (var info in infos)
|
||||||
|
{
|
||||||
|
if (!seen.Add(info.Id))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
merged.Add(KpiAreaSnapshotMapper.MapToSnapshotRule(info, area?.Name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishSnapshotNormalizer.NormalizeKpiAreas(merged);
|
||||||
|
snapshot.PriceAreaInfos = merged;
|
||||||
|
snapshot.PriceAreaRefs = [];
|
||||||
|
if (areaIds.Count == 1)
|
||||||
|
{
|
||||||
|
snapshot.PriceAreaId = areaIds[0];
|
||||||
|
snapshot.PriceAreaName = names.Count > 0 ? names[0] : null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
snapshot.PriceAreaId = null;
|
||||||
|
snapshot.PriceAreaName = names.Count > 0 ? string.Join("、", names) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LoadLiveInfosIntoRulesSnapshotAsync(
|
||||||
|
SalesProductRulesPublishSnapshotDto snapshot,
|
||||||
|
int? blacklistAreaId,
|
||||||
|
int? remoteAreaId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
snapshot.BlacklistAreaRef = null;
|
||||||
|
snapshot.RemoteAreaRef = null;
|
||||||
|
if (blacklistAreaId is > 0)
|
||||||
|
{
|
||||||
|
var area = await _kpiAreaRepository.FirstOrDefaultAsync(x => x.Id == blacklistAreaId.Value, cancellationToken);
|
||||||
|
var infos = await _kpiAreaRepository.GetAllInfosByAreaIdAsync(blacklistAreaId.Value, cancellationToken);
|
||||||
|
var rules = infos.Select(i => KpiAreaSnapshotMapper.MapToSnapshotRule(i, area?.Name)).ToList();
|
||||||
|
PublishSnapshotNormalizer.NormalizeKpiAreas(rules);
|
||||||
|
snapshot.BlacklistAreaId = blacklistAreaId;
|
||||||
|
snapshot.BlacklistAreaName = area?.Name;
|
||||||
|
snapshot.BlacklistAreaInfos = rules;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
snapshot.BlacklistAreaId = null;
|
||||||
|
snapshot.BlacklistAreaName = null;
|
||||||
|
snapshot.BlacklistAreaInfos = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteAreaId is > 0)
|
||||||
|
{
|
||||||
|
var area = await _kpiAreaRepository.FirstOrDefaultAsync(x => x.Id == remoteAreaId.Value, cancellationToken);
|
||||||
|
var infos = await _kpiAreaRepository.GetAllInfosByAreaIdAsync(remoteAreaId.Value, cancellationToken);
|
||||||
|
var rules = infos.Select(i => KpiAreaSnapshotMapper.MapToSnapshotRule(i, area?.Name)).ToList();
|
||||||
|
PublishSnapshotNormalizer.NormalizeKpiAreas(rules);
|
||||||
|
snapshot.RemoteAreaId = remoteAreaId;
|
||||||
|
snapshot.RemoteAreaName = area?.Name;
|
||||||
|
snapshot.RemoteAreaInfos = rules;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
snapshot.RemoteAreaId = null;
|
||||||
|
snapshot.RemoteAreaName = null;
|
||||||
|
snapshot.RemoteAreaInfos = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static KpiAreaPackRefDto ToRef(KpiAreaPublishRecord record, string? areaName) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
AreaId = record.AreaId,
|
||||||
|
AreaName = areaName,
|
||||||
|
PackVersion = record.Version,
|
||||||
|
ContentHash = record.ContentHash
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string CacheKey(int areaId, string version) =>
|
||||||
|
$"kpi-area-pack:{areaId}:{version}";
|
||||||
|
|
||||||
|
private static string ComputeSha256Hex(string content)
|
||||||
|
{
|
||||||
|
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(content));
|
||||||
|
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,18 +6,19 @@ using Oms.Domain.ChannelsAggregate.Repositories;
|
|||||||
namespace Oms.Application.Shared;
|
namespace Oms.Application.Shared;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将价格分区(t_kpi_area + t_kpi_area_info)固化进卖价发布快照。
|
/// 卖价发布:解析价区表 Id 并钉分区包引用(不内嵌 Infos)。
|
||||||
/// 支持卖价明细引用多张价格分区表:合并各表明细规则写入快照。
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class PriceAreaSnapshotHelper
|
public static class PriceAreaSnapshotHelper
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 按产品 PriceAreaId(或从卖价明细分区推断的全部 AreaId)加载分区头与明细写入快照。
|
/// 写入 PriceAreaRefs(Ensure 分区包);Infos 仅 LIVE 路径由 PackService.LoadLive 填充。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static async Task ApplyPriceAreaToSnapshotAsync(
|
public static async Task ApplyPriceAreaToSnapshotAsync(
|
||||||
SalesProductPricePublishSnapshotDto snapshot,
|
SalesProductPricePublishSnapshotDto snapshot,
|
||||||
SalesProduct product,
|
SalesProduct product,
|
||||||
IKpiAreaRepository kpiAreaRepository,
|
IKpiAreaRepository kpiAreaRepository,
|
||||||
|
KpiAreaPackService packService,
|
||||||
|
string? publishUser = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var areaIds = await CollectResolvedPriceAreaIdsAsync(
|
var areaIds = await CollectResolvedPriceAreaIdsAsync(
|
||||||
@ -25,53 +26,59 @@ public static class PriceAreaSnapshotHelper
|
|||||||
snapshot,
|
snapshot,
|
||||||
kpiAreaRepository,
|
kpiAreaRepository,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
snapshot.PriceAreaInfos = [];
|
||||||
if (areaIds.Count == 0)
|
if (areaIds.Count == 0)
|
||||||
{
|
{
|
||||||
snapshot.PriceAreaId = null;
|
snapshot.PriceAreaId = null;
|
||||||
snapshot.PriceAreaName = null;
|
snapshot.PriceAreaName = null;
|
||||||
snapshot.PriceAreaInfos = [];
|
snapshot.PriceAreaRefs = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var mergedInfos = new List<KpiAreaSnapshotRuleDto>();
|
var refs = new List<KpiAreaPackRefDto>();
|
||||||
var seenInfoIds = new HashSet<int>();
|
|
||||||
var areaNames = new List<string>();
|
|
||||||
|
|
||||||
foreach (var areaId in areaIds)
|
foreach (var areaId in areaIds)
|
||||||
{
|
{
|
||||||
var area = await kpiAreaRepository.FirstOrDefaultAsync(x => x.Id == areaId, cancellationToken);
|
refs.Add(await packService.EnsurePackAsync(
|
||||||
var areaName = area?.Name;
|
areaId,
|
||||||
if (!string.IsNullOrWhiteSpace(areaName))
|
KpiAreaPackService.AreaTypePrice,
|
||||||
{
|
publishUser,
|
||||||
areaNames.Add(areaName.Trim());
|
description: "sales-product-price-publish",
|
||||||
|
cancellationToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
var infos = await kpiAreaRepository.GetAllInfosByAreaIdAsync(areaId, cancellationToken);
|
snapshot.PriceAreaRefs = refs;
|
||||||
foreach (var info in infos)
|
if (refs.Count == 1)
|
||||||
{
|
{
|
||||||
if (!seenInfoIds.Add(info.Id))
|
snapshot.PriceAreaId = refs[0].AreaId;
|
||||||
{
|
snapshot.PriceAreaName = refs[0].AreaName;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
mergedInfos.Add(KpiAreaSnapshotMapper.MapToSnapshotRule(info, areaName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
snapshot.PriceAreaInfos = mergedInfos;
|
|
||||||
if (areaIds.Count == 1)
|
|
||||||
{
|
|
||||||
snapshot.PriceAreaId = areaIds[0];
|
|
||||||
snapshot.PriceAreaName = areaNames.Count > 0 ? areaNames[0] : null;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// 多表:不再落单一 PriceAreaId,名称拼接便于人工查看
|
|
||||||
snapshot.PriceAreaId = null;
|
snapshot.PriceAreaId = null;
|
||||||
snapshot.PriceAreaName = areaNames.Count > 0 ? string.Join("、", areaNames) : null;
|
snapshot.PriceAreaName = string.Join(
|
||||||
|
"、",
|
||||||
|
refs.Select(x => x.AreaName).Where(x => !string.IsNullOrWhiteSpace(x)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// LIVE/模拟:不钉包,直接从当前库装载 Infos。
|
||||||
|
/// </summary>
|
||||||
|
public static async Task ApplyLivePriceAreaInfosAsync(
|
||||||
|
SalesProductPricePublishSnapshotDto snapshot,
|
||||||
|
SalesProduct product,
|
||||||
|
IKpiAreaRepository kpiAreaRepository,
|
||||||
|
KpiAreaPackService packService,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var areaIds = await CollectResolvedPriceAreaIdsAsync(
|
||||||
|
product,
|
||||||
|
snapshot,
|
||||||
|
kpiAreaRepository,
|
||||||
|
cancellationToken);
|
||||||
|
await packService.LoadLiveInfosIntoPriceSnapshotAsync(snapshot, areaIds, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 收集卖价明细(及产品 PriceAreaId)可解析到的全部价格分区表 Id,去重保序。
|
/// 收集卖价明细(及产品 PriceAreaId)可解析到的全部价格分区表 Id,去重保序。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -106,9 +113,6 @@ public static class PriceAreaSnapshotHelper
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 兼容旧调用:仅返回第一个可解析 AreaId(多表场景请用 CollectResolvedPriceAreaIdsAsync)。
|
|
||||||
/// </summary>
|
|
||||||
internal static async Task<int?> ResolvePriceAreaIdAsync(
|
internal static async Task<int?> ResolvePriceAreaIdAsync(
|
||||||
SalesProduct product,
|
SalesProduct product,
|
||||||
SalesProductPricePublishSnapshotDto snapshot,
|
SalesProductPricePublishSnapshotDto snapshot,
|
||||||
|
|||||||
@ -1,17 +1,16 @@
|
|||||||
using Oms.Application.Dtos.Orders;
|
using Oms.Application.Dtos.Orders;
|
||||||
using Oms.Application.Services.Orders;
|
|
||||||
using Oms.Domain.ChannelsAggregate.Entities;
|
using Oms.Domain.ChannelsAggregate.Entities;
|
||||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||||
|
|
||||||
namespace Oms.Application.Shared;
|
namespace Oms.Application.Shared;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将黑名单/偏远/计重/进位规则固化进销售产品规则发布快照。
|
/// 规则发布:黑名单/偏远钉分区包;计重/进位仍内嵌明细。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class RulesSnapshotHelper
|
public static class RulesSnapshotHelper
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 按产品配置加载 KPI 分区、计重规则、进位区间写入规则快照。
|
/// 发布路径:黑名单/偏远 Ensure 包写 Ref;计重/进位写 Infos。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static async Task ApplyRulesToSnapshotAsync(
|
public static async Task ApplyRulesToSnapshotAsync(
|
||||||
SalesProductRulesPublishSnapshotDto snapshot,
|
SalesProductRulesPublishSnapshotDto snapshot,
|
||||||
@ -19,29 +18,37 @@ public static class RulesSnapshotHelper
|
|||||||
IKpiAreaRepository kpiAreaRepository,
|
IKpiAreaRepository kpiAreaRepository,
|
||||||
IWeightRuleRepository weightRuleRepository,
|
IWeightRuleRepository weightRuleRepository,
|
||||||
IWeightRoundingRepository weightRoundingRepository,
|
IWeightRoundingRepository weightRoundingRepository,
|
||||||
|
KpiAreaPackService packService,
|
||||||
|
string? publishUser = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await ApplyKpiAreaToSnapshotAsync(
|
await ApplyKpiAreaPackRefAsync(
|
||||||
snapshot,
|
snapshot,
|
||||||
product.BlacklistAreaId,
|
product.BlacklistAreaId,
|
||||||
kpiAreaRepository,
|
KpiAreaPackService.AreaTypeBlacklist,
|
||||||
(s, areaId, areaName, infos) =>
|
packService,
|
||||||
|
publishUser,
|
||||||
|
(s, areaId, areaName, packRef) =>
|
||||||
{
|
{
|
||||||
s.BlacklistAreaId = areaId;
|
s.BlacklistAreaId = areaId;
|
||||||
s.BlacklistAreaName = areaName;
|
s.BlacklistAreaName = areaName;
|
||||||
s.BlacklistAreaInfos = infos;
|
s.BlacklistAreaRef = packRef;
|
||||||
|
s.BlacklistAreaInfos = [];
|
||||||
},
|
},
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
await ApplyKpiAreaToSnapshotAsync(
|
await ApplyKpiAreaPackRefAsync(
|
||||||
snapshot,
|
snapshot,
|
||||||
product.RemoteAreaId,
|
product.RemoteAreaId,
|
||||||
kpiAreaRepository,
|
KpiAreaPackService.AreaTypeRemote,
|
||||||
(s, areaId, areaName, infos) =>
|
packService,
|
||||||
|
publishUser,
|
||||||
|
(s, areaId, areaName, packRef) =>
|
||||||
{
|
{
|
||||||
s.RemoteAreaId = areaId;
|
s.RemoteAreaId = areaId;
|
||||||
s.RemoteAreaName = areaName;
|
s.RemoteAreaName = areaName;
|
||||||
s.RemoteAreaInfos = infos;
|
s.RemoteAreaRef = packRef;
|
||||||
|
s.RemoteAreaInfos = [];
|
||||||
},
|
},
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
@ -49,6 +56,27 @@ public static class RulesSnapshotHelper
|
|||||||
await ApplyWeightRoundingToSnapshotAsync(snapshot, product, weightRoundingRepository, cancellationToken);
|
await ApplyWeightRoundingToSnapshotAsync(snapshot, product, weightRoundingRepository, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// LIVE/模拟:黑名单/偏远从当前库装 Infos,不写 Ref。
|
||||||
|
/// </summary>
|
||||||
|
public static async Task ApplyLiveRulesToSnapshotAsync(
|
||||||
|
SalesProductRulesPublishSnapshotDto snapshot,
|
||||||
|
SalesProduct product,
|
||||||
|
IKpiAreaRepository kpiAreaRepository,
|
||||||
|
IWeightRuleRepository weightRuleRepository,
|
||||||
|
IWeightRoundingRepository weightRoundingRepository,
|
||||||
|
KpiAreaPackService packService,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await packService.LoadLiveInfosIntoRulesSnapshotAsync(
|
||||||
|
snapshot,
|
||||||
|
product.BlacklistAreaId,
|
||||||
|
product.RemoteAreaId,
|
||||||
|
cancellationToken);
|
||||||
|
await ApplyWeightRuleToSnapshotAsync(snapshot, product, weightRuleRepository, cancellationToken);
|
||||||
|
await ApplyWeightRoundingToSnapshotAsync(snapshot, product, weightRoundingRepository, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 从快照内嵌黑名单/偏远明细匹配地址(不查库)。
|
/// 从快照内嵌黑名单/偏远明细匹配地址(不查库)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -67,38 +95,34 @@ public static class RulesSnapshotHelper
|
|||||||
return GeoPartitionMatcher.IsHit(infos, countryCode, province, city, zipCode);
|
return GeoPartitionMatcher.IsHit(infos, countryCode, province, city, zipCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 将快照内嵌计重明细转为实体供计算器使用。
|
|
||||||
/// </summary>
|
|
||||||
public static List<WeightRuleInfo> ToWeightRuleEntities(IReadOnlyList<WeightRuleSnapshotRuleDto> infos) =>
|
public static List<WeightRuleInfo> ToWeightRuleEntities(IReadOnlyList<WeightRuleSnapshotRuleDto> infos) =>
|
||||||
infos.Select(PublishSnapshotSlimmer.ToWeightRuleEntity).ToList();
|
infos.Select(PublishSnapshotSlimmer.ToWeightRuleEntity).ToList();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 将快照内嵌进位区间转为实体供计算器使用。
|
|
||||||
/// </summary>
|
|
||||||
public static List<WeightRoundingInfo> ToWeightRoundingEntities(IReadOnlyList<WeightRoundingSnapshotRangeDto> infos) =>
|
public static List<WeightRoundingInfo> ToWeightRoundingEntities(IReadOnlyList<WeightRoundingSnapshotRangeDto> infos) =>
|
||||||
infos.Select(PublishSnapshotSlimmer.ToWeightRoundingEntity).ToList();
|
infos.Select(PublishSnapshotSlimmer.ToWeightRoundingEntity).ToList();
|
||||||
|
|
||||||
private static async Task ApplyKpiAreaToSnapshotAsync(
|
private static async Task ApplyKpiAreaPackRefAsync(
|
||||||
SalesProductRulesPublishSnapshotDto snapshot,
|
SalesProductRulesPublishSnapshotDto snapshot,
|
||||||
int? areaId,
|
int? areaId,
|
||||||
IKpiAreaRepository kpiAreaRepository,
|
string areaType,
|
||||||
Action<SalesProductRulesPublishSnapshotDto, int?, string?, List<KpiAreaSnapshotRuleDto>> assign,
|
KpiAreaPackService packService,
|
||||||
|
string? publishUser,
|
||||||
|
Action<SalesProductRulesPublishSnapshotDto, int?, string?, KpiAreaPackRefDto?> assign,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (!areaId.HasValue || areaId.Value <= 0)
|
if (!areaId.HasValue || areaId.Value <= 0)
|
||||||
{
|
{
|
||||||
assign(snapshot, null, null, []);
|
assign(snapshot, null, null, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var area = await kpiAreaRepository.FirstOrDefaultAsync(x => x.Id == areaId.Value, cancellationToken);
|
var packRef = await packService.EnsurePackAsync(
|
||||||
var infos = await kpiAreaRepository.GetAllInfosByAreaIdAsync(areaId.Value, cancellationToken);
|
areaId.Value,
|
||||||
assign(
|
areaType,
|
||||||
snapshot,
|
publishUser,
|
||||||
areaId,
|
description: "sales-product-rules-publish",
|
||||||
area?.Name,
|
cancellationToken);
|
||||||
infos.Select(info => KpiAreaSnapshotMapper.MapToSnapshotRule(info)).ToList());
|
assign(snapshot, areaId, packRef.AreaName, packRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task ApplyWeightRuleToSnapshotAsync(
|
private static async Task ApplyWeightRuleToSnapshotAsync(
|
||||||
|
|||||||
@ -0,0 +1,33 @@
|
|||||||
|
using Oms.Domain.Shared;
|
||||||
|
using Oms.Domain.Shared.Enums;
|
||||||
|
using SqlSugar;
|
||||||
|
|
||||||
|
namespace Oms.Domain.ChannelsAggregate.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// KPI 分区发布包(价区/黑名单/偏远明细不可变快照)。
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("t_kpi_area_publish_record")]
|
||||||
|
public class KpiAreaPublishRecord : OmsAuditedEntity
|
||||||
|
{
|
||||||
|
public int AreaId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>price / blacklist / remote</summary>
|
||||||
|
public string AreaType { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Version { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public EsSnapshotStatus Status { get; set; } = EsSnapshotStatus.Active;
|
||||||
|
|
||||||
|
public string ContentHash { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[SugarColumn(ColumnDataType = "json", IsNullable = true)]
|
||||||
|
public string? Content { get; set; }
|
||||||
|
|
||||||
|
public string PublishUser { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public DateTime PublishTime { get; set; }
|
||||||
|
|
||||||
|
[SugarColumn(IsNullable = true, Length = 512)]
|
||||||
|
public string? Description { get; set; }
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
using Oms.Domain.ChannelsAggregate.Entities;
|
||||||
|
|
||||||
|
namespace Oms.Domain.ChannelsAggregate.Repositories;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// KPI 分区发布包读写(可由 <see cref="IKpiAreaRepository"/> 一并实现)。
|
||||||
|
/// </summary>
|
||||||
|
public interface IKpiAreaPublishRepository
|
||||||
|
{
|
||||||
|
Task<KpiAreaPublishRecord?> FindPublishByAreaIdAndContentHashAsync(
|
||||||
|
int areaId,
|
||||||
|
string contentHash,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<KpiAreaPublishRecord?> GetPublishByAreaIdAndVersionAsync(
|
||||||
|
int areaId,
|
||||||
|
string version,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<List<KpiAreaPublishRecord>> GetPublishHeadersByAreaIdAsync(
|
||||||
|
int areaId,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<KpiAreaPublishRecord?> GetPublishByIdAsync(
|
||||||
|
int id,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<KpiAreaPublishRecord> InsertPublishRecordAsync(
|
||||||
|
KpiAreaPublishRecord record,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@ -3,7 +3,7 @@ using Oms.Domain.ChannelsAggregate.Entities;
|
|||||||
|
|
||||||
namespace Oms.Domain.ChannelsAggregate.Repositories;
|
namespace Oms.Domain.ChannelsAggregate.Repositories;
|
||||||
|
|
||||||
public interface IKpiAreaRepository : IRepository<KpiArea>
|
public interface IKpiAreaRepository : IRepository<KpiArea>, IKpiAreaPublishRepository
|
||||||
{
|
{
|
||||||
Task<KpiArea?> GetByAppKeyTypeAndNameAsync(string appKey, string areaType, string name, CancellationToken cancellationToken = default);
|
Task<KpiArea?> GetByAppKeyTypeAndNameAsync(string appKey, string areaType, string name, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ using Fake.DependencyInjection;
|
|||||||
using Oms.Domain.ChannelsAggregate.Entities;
|
using Oms.Domain.ChannelsAggregate.Entities;
|
||||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||||
using Oms.SqlSugarCore.Repositories;
|
using Oms.SqlSugarCore.Repositories;
|
||||||
|
using SqlSugar;
|
||||||
|
|
||||||
namespace Oms.SqlSugarCore.Channels;
|
namespace Oms.SqlSugarCore.Channels;
|
||||||
|
|
||||||
@ -64,4 +65,81 @@ public class KpiAreaRepository : OmsSqlSugarRepository<KpiArea>, IKpiAreaReposit
|
|||||||
.OrderBy(x => x.Id)
|
.OrderBy(x => x.Id)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<KpiAreaPublishRecord?> FindPublishByAreaIdAndContentHashAsync(
|
||||||
|
int areaId,
|
||||||
|
string contentHash,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var db = await GetDbContextAsync(cancellationToken);
|
||||||
|
var rows = await db.Queryable<KpiAreaPublishRecord>()
|
||||||
|
.Where(x => x.AreaId == areaId && x.ContentHash == contentHash)
|
||||||
|
.OrderBy(x => x.Id, OrderByType.Desc)
|
||||||
|
.Take(1)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
return rows.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<KpiAreaPublishRecord?> GetPublishByAreaIdAndVersionAsync(
|
||||||
|
int areaId,
|
||||||
|
string version,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(version))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var db = await GetDbContextAsync(cancellationToken);
|
||||||
|
var rows = await db.Queryable<KpiAreaPublishRecord>()
|
||||||
|
.Where(x => x.AreaId == areaId && x.Version == version)
|
||||||
|
.OrderBy(x => x.Id, OrderByType.Desc)
|
||||||
|
.Take(1)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
return rows.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<KpiAreaPublishRecord>> GetPublishHeadersByAreaIdAsync(
|
||||||
|
int areaId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var db = await GetDbContextAsync(cancellationToken);
|
||||||
|
return await db.Queryable<KpiAreaPublishRecord>()
|
||||||
|
.Where(x => x.AreaId == areaId)
|
||||||
|
.OrderBy(x => x.PublishTime, OrderByType.Desc)
|
||||||
|
.Select(x => new KpiAreaPublishRecord
|
||||||
|
{
|
||||||
|
Id = x.Id,
|
||||||
|
AreaId = x.AreaId,
|
||||||
|
AreaType = x.AreaType,
|
||||||
|
Version = x.Version,
|
||||||
|
Status = x.Status,
|
||||||
|
ContentHash = x.ContentHash,
|
||||||
|
PublishUser = x.PublishUser,
|
||||||
|
PublishTime = x.PublishTime,
|
||||||
|
Description = x.Description,
|
||||||
|
CreateTime = x.CreateTime
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<KpiAreaPublishRecord?> GetPublishByIdAsync(
|
||||||
|
int id,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var db = await GetDbContextAsync(cancellationToken);
|
||||||
|
var rows = await db.Queryable<KpiAreaPublishRecord>()
|
||||||
|
.Where(x => x.Id == id)
|
||||||
|
.Take(1)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
return rows.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<KpiAreaPublishRecord> InsertPublishRecordAsync(
|
||||||
|
KpiAreaPublishRecord record,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var db = await GetDbContextAsync(cancellationToken);
|
||||||
|
return await db.Insertable(record).ExecuteReturnEntityAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
using System.Linq.Expressions;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Oms.Application.Dtos.Orders;
|
using Oms.Application.Dtos.Orders;
|
||||||
@ -10,7 +11,7 @@ namespace Oms.Tests;
|
|||||||
public class RulesSnapshotHelperPublishTests
|
public class RulesSnapshotHelperPublishTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ApplyRulesToSnapshotAsync_ShouldEmbedBlacklistRemoteWeightAndRounding()
|
public async Task ApplyRulesToSnapshotAsync_ShouldPinBlacklistRemotePacksAndEmbedWeight()
|
||||||
{
|
{
|
||||||
var product = new SalesProduct
|
var product = new SalesProduct
|
||||||
{
|
{
|
||||||
@ -22,10 +23,16 @@ public class RulesSnapshotHelperPublishTests
|
|||||||
WeightRoundingId = 2
|
WeightRoundingId = 2
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var areas = new Dictionary<int, KpiArea>
|
||||||
|
{
|
||||||
|
[10] = new() { Id = 10, Name = "黑名单区", AreaType = "blacklist" },
|
||||||
|
[20] = new() { Id = 20, Name = "偏远区", AreaType = "remote" }
|
||||||
|
};
|
||||||
|
|
||||||
var kpiMock = new Mock<IKpiAreaRepository>();
|
var kpiMock = new Mock<IKpiAreaRepository>();
|
||||||
kpiMock.Setup(x => x.FirstOrDefaultAsync(It.IsAny<System.Linq.Expressions.Expression<Func<KpiArea, bool>>>(), default))
|
kpiMock.Setup(x => x.FirstOrDefaultAsync(It.IsAny<Expression<Func<KpiArea, bool>>>(), default))
|
||||||
.ReturnsAsync((System.Linq.Expressions.Expression<Func<KpiArea, bool>> _, CancellationToken _) =>
|
.ReturnsAsync((Expression<Func<KpiArea, bool>> expr, CancellationToken _) =>
|
||||||
new KpiArea { Id = 10, Name = "黑名单区" });
|
areas.Values.FirstOrDefault(expr.Compile()));
|
||||||
kpiMock.Setup(x => x.GetAllInfosByAreaIdAsync(10, default))
|
kpiMock.Setup(x => x.GetAllInfosByAreaIdAsync(10, default))
|
||||||
.ReturnsAsync(
|
.ReturnsAsync(
|
||||||
[
|
[
|
||||||
@ -36,9 +43,17 @@ public class RulesSnapshotHelperPublishTests
|
|||||||
[
|
[
|
||||||
new KpiAreaInfo { Id = 2, AreaId = 20, CountryCode = "US", LeftPostcode = "06000", RightPostcode = "06999" }
|
new KpiAreaInfo { Id = 2, AreaId = 20, CountryCode = "US", LeftPostcode = "06000", RightPostcode = "06999" }
|
||||||
]);
|
]);
|
||||||
|
kpiMock.Setup(x => x.FindPublishByAreaIdAndContentHashAsync(It.IsAny<int>(), It.IsAny<string>(), default))
|
||||||
|
.ReturnsAsync((KpiAreaPublishRecord?)null);
|
||||||
|
kpiMock.Setup(x => x.InsertPublishRecordAsync(It.IsAny<KpiAreaPublishRecord>(), default))
|
||||||
|
.ReturnsAsync((KpiAreaPublishRecord r, CancellationToken _) =>
|
||||||
|
{
|
||||||
|
r.Id = r.AreaId * 100;
|
||||||
|
return r;
|
||||||
|
});
|
||||||
|
|
||||||
var weightRuleMock = new Mock<IWeightRuleRepository>();
|
var weightRuleMock = new Mock<IWeightRuleRepository>();
|
||||||
weightRuleMock.Setup(x => x.FirstOrDefaultAsync(It.IsAny<System.Linq.Expressions.Expression<Func<WeightRule, bool>>>(), default))
|
weightRuleMock.Setup(x => x.FirstOrDefaultAsync(It.IsAny<Expression<Func<WeightRule, bool>>>(), default))
|
||||||
.ReturnsAsync(new WeightRule { Id = 1, Name = "标准计重" });
|
.ReturnsAsync(new WeightRule { Id = 1, Name = "标准计重" });
|
||||||
weightRuleMock.Setup(x => x.GetAllInfosByRuleIdAsync(1, default))
|
weightRuleMock.Setup(x => x.GetAllInfosByRuleIdAsync(1, default))
|
||||||
.ReturnsAsync(
|
.ReturnsAsync(
|
||||||
@ -54,7 +69,7 @@ public class RulesSnapshotHelperPublishTests
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
var roundingMock = new Mock<IWeightRoundingRepository>();
|
var roundingMock = new Mock<IWeightRoundingRepository>();
|
||||||
roundingMock.Setup(x => x.FirstOrDefaultAsync(It.IsAny<System.Linq.Expressions.Expression<Func<WeightRounding, bool>>>(), default))
|
roundingMock.Setup(x => x.FirstOrDefaultAsync(It.IsAny<Expression<Func<WeightRounding, bool>>>(), default))
|
||||||
.ReturnsAsync(new WeightRounding { Id = 2, Name = "标准进位" });
|
.ReturnsAsync(new WeightRounding { Id = 2, Name = "标准进位" });
|
||||||
roundingMock.Setup(x => x.GetAllInfosByRoundingIdAsync(2, default))
|
roundingMock.Setup(x => x.GetAllInfosByRoundingIdAsync(2, default))
|
||||||
.ReturnsAsync(
|
.ReturnsAsync(
|
||||||
@ -62,31 +77,30 @@ public class RulesSnapshotHelperPublishTests
|
|||||||
new WeightRoundingInfo { Id = 1, RoundingId = 2, StartValue = 0, EndValue = 20, Public = "0.5" }
|
new WeightRoundingInfo { Id = 1, RoundingId = 2, StartValue = 0, EndValue = 20, Public = "0.5" }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
var packService = new KpiAreaPackService(kpiMock.Object);
|
||||||
var snapshot = new SalesProductRulesPublishSnapshotDto();
|
var snapshot = new SalesProductRulesPublishSnapshotDto();
|
||||||
await RulesSnapshotHelper.ApplyRulesToSnapshotAsync(
|
await RulesSnapshotHelper.ApplyRulesToSnapshotAsync(
|
||||||
snapshot,
|
snapshot,
|
||||||
product,
|
product,
|
||||||
kpiMock.Object,
|
kpiMock.Object,
|
||||||
weightRuleMock.Object,
|
weightRuleMock.Object,
|
||||||
roundingMock.Object);
|
roundingMock.Object,
|
||||||
|
packService,
|
||||||
|
"tester");
|
||||||
|
|
||||||
snapshot.BlacklistAreaId.Should().Be(10);
|
snapshot.BlacklistAreaId.Should().Be(10);
|
||||||
snapshot.BlacklistAreaInfos.Should().HaveCount(1);
|
snapshot.BlacklistAreaRef.Should().NotBeNull();
|
||||||
snapshot.RemoteAreaId.Should().Be(20);
|
snapshot.BlacklistAreaRef!.PackVersion.Should().NotBeNullOrWhiteSpace();
|
||||||
snapshot.RemoteAreaInfos.Should().HaveCount(1);
|
snapshot.BlacklistAreaInfos.Should().BeEmpty();
|
||||||
snapshot.WeightRuleId.Should().Be(1);
|
snapshot.RemoteAreaRef.Should().NotBeNull();
|
||||||
|
snapshot.RemoteAreaRef!.AreaId.Should().Be(20);
|
||||||
snapshot.WeightRuleInfos.Should().HaveCount(1);
|
snapshot.WeightRuleInfos.Should().HaveCount(1);
|
||||||
snapshot.WeightRoundingId.Should().Be(2);
|
|
||||||
snapshot.WeightRoundingInfos.Should().HaveCount(1);
|
snapshot.WeightRoundingInfos.Should().HaveCount(1);
|
||||||
|
|
||||||
var json = OmsDocHelper.SerializeSnapshotJson(snapshot);
|
var json = OmsDocHelper.SerializeSnapshotJson(snapshot)!;
|
||||||
json.Should().Contain("PostcodeRange");
|
json.Should().Contain("blacklistAreaRef");
|
||||||
json.Should().Contain("(90000,90010]");
|
json.Should().Contain("remoteAreaRef");
|
||||||
json.Should().Contain("WeightRuleInfos");
|
json.Should().NotContain("blacklistAreaInfos");
|
||||||
json.Should().Contain("WeightRoundingInfos");
|
json.Should().NotContain("remoteAreaInfos");
|
||||||
json.Should().Contain("RemoteAreaInfos");
|
|
||||||
json.Should().NotContain("CreateTime");
|
|
||||||
json.Should().NotContain("UpdateTime");
|
|
||||||
json.Should().NotContain("LeftPostcode");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user