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>
|
||||
private async Task CreatePublishRecordAsync(int salesProductId, string operatorType, string? description)
|
||||
{
|
||||
var product = await GetProductOrThrowAsync(salesProductId);
|
||||
object snapshot;
|
||||
if (operatorType == PriceOperatorType)
|
||||
{
|
||||
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;
|
||||
}
|
||||
object snapshot = operatorType == PriceOperatorType
|
||||
? await BuildPricePublishSnapshotAsync(salesProductId)
|
||||
: await BuildRulesPublishSnapshotAsync(salesProductId);
|
||||
|
||||
// Content 中保存完整快照,回滚时直接从快照恢复,避免依赖当前库内状态。
|
||||
await SalesProductRepository.InsertPublishRecordAsync(new SalesProductPublishRecord
|
||||
|
||||
@ -408,6 +408,18 @@ public partial class SalesProductService : ApplicationService
|
||||
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>
|
||||
|
||||
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