feat: 规则发布预览接口并抽取快照组装逻辑
新增 GET publish/preview 返回含黑名单/偏远/计重/进位明细的完整 rules 快照,便于核对 Content。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
2c69e8e5b8
commit
d8a98fbbc8
@ -155,56 +155,70 @@ public partial class SalesProductService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 组装规则发布快照(不落库),含黑名单/偏远/计重/进位明细。
|
||||||
|
/// </summary>
|
||||||
|
private async Task<SalesProductRulesPublishSnapshotDto> BuildRulesPublishSnapshotAsync(int salesProductId)
|
||||||
|
{
|
||||||
|
var product = await GetProductOrThrowAsync(salesProductId);
|
||||||
|
var rulesSnapshot = new SalesProductRulesPublishSnapshot
|
||||||
|
{
|
||||||
|
Product = MapProductToRulesSaveInput(product),
|
||||||
|
Providers = (await GetAllocationCoreAsync(salesProductId)).Providers
|
||||||
|
};
|
||||||
|
await RulesSnapshotHelper.ApplyRulesToSnapshotAsync(
|
||||||
|
rulesSnapshot,
|
||||||
|
product,
|
||||||
|
KpiAreaRepository,
|
||||||
|
WeightRuleRepository,
|
||||||
|
WeightRoundingRepository);
|
||||||
|
return rulesSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 组装价格发布快照(不落库)。
|
||||||
|
/// </summary>
|
||||||
|
private async Task<SalesProductPricePublishSnapshotDto> BuildPricePublishSnapshotAsync(int salesProductId)
|
||||||
|
{
|
||||||
|
var product = await GetProductOrThrowAsync(salesProductId);
|
||||||
|
var priceSnapshot = new SalesProductPricePublishSnapshot
|
||||||
|
{
|
||||||
|
SalesProductId = salesProductId,
|
||||||
|
Prices = await GetPriceOutputsAsync(salesProductId)
|
||||||
|
};
|
||||||
|
await PriceAreaSnapshotHelper.ApplyPriceAreaToSnapshotAsync(priceSnapshot, product, KpiAreaRepository);
|
||||||
|
return priceSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SalesProductSaveInput MapProductToRulesSaveInput(SalesProduct product) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = product.Id,
|
||||||
|
ProductCode = product.ProductCode,
|
||||||
|
ProductName = product.ProductName,
|
||||||
|
ProductGroup = product.ProductGroup,
|
||||||
|
MinTimeliness = product.MinTimeliness,
|
||||||
|
MaxTimeliness = product.MaxTimeliness,
|
||||||
|
CustomerWaybill = product.CustomerWaybill,
|
||||||
|
BlacklistAreaId = product.BlacklistAreaId,
|
||||||
|
RemoteAreaId = product.RemoteAreaId,
|
||||||
|
PriceAreaId = product.PriceAreaId,
|
||||||
|
WeightRuleId = product.WeightRuleId,
|
||||||
|
WeightRoundingId = product.WeightRoundingId,
|
||||||
|
ProductDescription = product.ProductDescription,
|
||||||
|
PreStorageScript = product.PreStorageScript,
|
||||||
|
IsActive = product.IsActive,
|
||||||
|
Scheme = product.Scheme
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 为当前配置生成发布快照。
|
/// 为当前配置生成发布快照。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task CreatePublishRecordAsync(int salesProductId, string operatorType, string? description)
|
private async Task CreatePublishRecordAsync(int salesProductId, string operatorType, string? description)
|
||||||
{
|
{
|
||||||
var product = await GetProductOrThrowAsync(salesProductId);
|
object snapshot = operatorType == PriceOperatorType
|
||||||
object snapshot;
|
? await BuildPricePublishSnapshotAsync(salesProductId)
|
||||||
if (operatorType == PriceOperatorType)
|
: await BuildRulesPublishSnapshotAsync(salesProductId);
|
||||||
{
|
|
||||||
var priceSnapshot = new SalesProductPricePublishSnapshot
|
|
||||||
{
|
|
||||||
SalesProductId = salesProductId,
|
|
||||||
Prices = await GetPriceOutputsAsync(salesProductId)
|
|
||||||
};
|
|
||||||
await PriceAreaSnapshotHelper.ApplyPriceAreaToSnapshotAsync(priceSnapshot, product, KpiAreaRepository);
|
|
||||||
snapshot = priceSnapshot;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var rulesSnapshot = new SalesProductRulesPublishSnapshot
|
|
||||||
{
|
|
||||||
Product = new SalesProductSaveInput
|
|
||||||
{
|
|
||||||
Id = product.Id,
|
|
||||||
ProductCode = product.ProductCode,
|
|
||||||
ProductName = product.ProductName,
|
|
||||||
ProductGroup = product.ProductGroup,
|
|
||||||
MinTimeliness = product.MinTimeliness,
|
|
||||||
MaxTimeliness = product.MaxTimeliness,
|
|
||||||
CustomerWaybill = product.CustomerWaybill,
|
|
||||||
BlacklistAreaId = product.BlacklistAreaId,
|
|
||||||
RemoteAreaId = product.RemoteAreaId,
|
|
||||||
PriceAreaId = product.PriceAreaId,
|
|
||||||
WeightRuleId = product.WeightRuleId,
|
|
||||||
WeightRoundingId = product.WeightRoundingId,
|
|
||||||
ProductDescription = product.ProductDescription,
|
|
||||||
PreStorageScript = product.PreStorageScript,
|
|
||||||
IsActive = product.IsActive,
|
|
||||||
Scheme = product.Scheme
|
|
||||||
},
|
|
||||||
Providers = (await GetAllocationCoreAsync(salesProductId)).Providers
|
|
||||||
};
|
|
||||||
await RulesSnapshotHelper.ApplyRulesToSnapshotAsync(
|
|
||||||
rulesSnapshot,
|
|
||||||
product,
|
|
||||||
KpiAreaRepository,
|
|
||||||
WeightRuleRepository,
|
|
||||||
WeightRoundingRepository);
|
|
||||||
snapshot = rulesSnapshot;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content 中保存完整快照,回滚时直接从快照恢复,避免依赖当前库内状态。
|
// Content 中保存完整快照,回滚时直接从快照恢复,避免依赖当前库内状态。
|
||||||
await SalesProductRepository.InsertPublishRecordAsync(new SalesProductPublishRecord
|
await SalesProductRepository.InsertPublishRecordAsync(new SalesProductPublishRecord
|
||||||
|
|||||||
@ -408,6 +408,18 @@ public partial class SalesProductService : ApplicationService
|
|||||||
return GetPublishListAsync(input, RulesOperatorType);
|
return GetPublishListAsync(input, RulesOperatorType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 预览即将发布的快照 JSON(不落库),用于核对黑名单/计重等是否已固化进 Content。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("/channel/sales-product/publish/preview")]
|
||||||
|
public async Task<object> PreviewPublishSnapshotAsync([FromQuery] int salesProductId, [FromQuery] string operatorType)
|
||||||
|
{
|
||||||
|
var normalized = NormalizeOperatorType(operatorType);
|
||||||
|
return normalized == PriceOperatorType
|
||||||
|
? await BuildPricePublishSnapshotAsync(salesProductId)
|
||||||
|
: await BuildRulesPublishSnapshotAsync(salesProductId);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 执行销售产品发布。
|
/// 执行销售产品发布。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
88
src/Oms.Tests/RulesSnapshotHelperPublishTests.cs
Normal file
88
src/Oms.Tests/RulesSnapshotHelperPublishTests.cs
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
|
using Oms.Application.Dtos.Orders;
|
||||||
|
using Oms.Application.Shared;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Entities;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||||
|
|
||||||
|
namespace Oms.Tests;
|
||||||
|
|
||||||
|
public class RulesSnapshotHelperPublishTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task ApplyRulesToSnapshotAsync_ShouldEmbedBlacklistRemoteWeightAndRounding()
|
||||||
|
{
|
||||||
|
var product = new SalesProduct
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
ProductCode = "test01",
|
||||||
|
BlacklistAreaId = 10,
|
||||||
|
RemoteAreaId = 20,
|
||||||
|
WeightRuleId = 1,
|
||||||
|
WeightRoundingId = 2
|
||||||
|
};
|
||||||
|
|
||||||
|
var kpiMock = new Mock<IKpiAreaRepository>();
|
||||||
|
kpiMock.Setup(x => x.FirstOrDefaultAsync(It.IsAny<System.Linq.Expressions.Expression<Func<KpiArea, bool>>>(), default))
|
||||||
|
.ReturnsAsync((System.Linq.Expressions.Expression<Func<KpiArea, bool>> _, CancellationToken _) =>
|
||||||
|
new KpiArea { Id = 10, Name = "黑名单区" });
|
||||||
|
kpiMock.Setup(x => x.GetAllInfosByAreaIdAsync(10, default))
|
||||||
|
.ReturnsAsync(
|
||||||
|
[
|
||||||
|
new KpiAreaInfo { Id = 1, AreaId = 10, CountryCode = "US", LeftPostcode = "90000", RightPostcode = "90010" }
|
||||||
|
]);
|
||||||
|
kpiMock.Setup(x => x.GetAllInfosByAreaIdAsync(20, default))
|
||||||
|
.ReturnsAsync(
|
||||||
|
[
|
||||||
|
new KpiAreaInfo { Id = 2, AreaId = 20, CountryCode = "US", LeftPostcode = "06000", RightPostcode = "06999" }
|
||||||
|
]);
|
||||||
|
|
||||||
|
var weightRuleMock = new Mock<IWeightRuleRepository>();
|
||||||
|
weightRuleMock.Setup(x => x.FirstOrDefaultAsync(It.IsAny<System.Linq.Expressions.Expression<Func<WeightRule, bool>>>(), default))
|
||||||
|
.ReturnsAsync(new WeightRule { Id = 1, Name = "标准计重" });
|
||||||
|
weightRuleMock.Setup(x => x.GetAllInfosByRuleIdAsync(1, default))
|
||||||
|
.ReturnsAsync(
|
||||||
|
[
|
||||||
|
new WeightRuleInfo
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
RuleId = 1,
|
||||||
|
Condition = "gw>vw",
|
||||||
|
BillableWeight = "gw",
|
||||||
|
VolumetricFactor = "6000"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
var roundingMock = new Mock<IWeightRoundingRepository>();
|
||||||
|
roundingMock.Setup(x => x.FirstOrDefaultAsync(It.IsAny<System.Linq.Expressions.Expression<Func<WeightRounding, bool>>>(), default))
|
||||||
|
.ReturnsAsync(new WeightRounding { Id = 2, Name = "标准进位" });
|
||||||
|
roundingMock.Setup(x => x.GetAllInfosByRoundingIdAsync(2, default))
|
||||||
|
.ReturnsAsync(
|
||||||
|
[
|
||||||
|
new WeightRoundingInfo { Id = 1, RoundingId = 2, StartValue = 0, EndValue = 20, Public = "0.5" }
|
||||||
|
]);
|
||||||
|
|
||||||
|
var snapshot = new SalesProductRulesPublishSnapshotDto();
|
||||||
|
await RulesSnapshotHelper.ApplyRulesToSnapshotAsync(
|
||||||
|
snapshot,
|
||||||
|
product,
|
||||||
|
kpiMock.Object,
|
||||||
|
weightRuleMock.Object,
|
||||||
|
roundingMock.Object);
|
||||||
|
|
||||||
|
snapshot.BlacklistAreaId.Should().Be(10);
|
||||||
|
snapshot.BlacklistAreaInfos.Should().HaveCount(1);
|
||||||
|
snapshot.RemoteAreaId.Should().Be(20);
|
||||||
|
snapshot.RemoteAreaInfos.Should().HaveCount(1);
|
||||||
|
snapshot.WeightRuleId.Should().Be(1);
|
||||||
|
snapshot.WeightRuleInfos.Should().HaveCount(1);
|
||||||
|
snapshot.WeightRoundingId.Should().Be(2);
|
||||||
|
snapshot.WeightRoundingInfos.Should().HaveCount(1);
|
||||||
|
|
||||||
|
var json = OmsDocHelper.SerializeJson(snapshot);
|
||||||
|
json.Should().Contain("BlacklistAreaInfos");
|
||||||
|
json.Should().Contain("WeightRuleInfos");
|
||||||
|
json.Should().Contain("WeightRoundingInfos");
|
||||||
|
json.Should().Contain("RemoteAreaInfos");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user