feat: add points rule matcher service

This commit is contained in:
Sam 2026-02-09 18:56:55 +08:00
parent a92847530d
commit 9ee1e3bb72
3 changed files with 117 additions and 14 deletions

View File

@ -2,7 +2,6 @@
<PropertyGroup> <PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<!-- Third-party package versions --> <!-- Third-party package versions -->
<NetCorePalVersion>3.2.1</NetCorePalVersion> <NetCorePalVersion>3.2.1</NetCorePalVersion>
@ -13,14 +12,12 @@
<NetCorePalTestcontainerVersion>1.0.5</NetCorePalTestcontainerVersion> <NetCorePalTestcontainerVersion>1.0.5</NetCorePalTestcontainerVersion>
<NetCorePalAspireVersion>1.1.2</NetCorePalAspireVersion> <NetCorePalAspireVersion>1.1.2</NetCorePalAspireVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="10.0.0" /> <PackageVersion Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="10.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.1" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.1" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.0.0" /> <PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.0.0" />
<PackageVersion Include="AspNet.Security.OAuth.Feishu" Version="9.0.0" /> <PackageVersion Include="AspNet.Security.OAuth.Feishu" Version="9.0.0" />
<PackageVersion Include="AspNet.Security.OAuth.Weixin" Version="9.0.0" /> <PackageVersion Include="AspNet.Security.OAuth.Weixin" Version="9.0.0" />
<!-- Database providers - framework specific versions --> <!-- Database providers - framework specific versions -->
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0" />
@ -39,7 +36,6 @@
<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" /> <PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="Microsoft.CodeAnalysis" Version="4.12.0" /> <PackageVersion Include="Microsoft.CodeAnalysis" Version="4.12.0" />
<!-- CAP packages for .NET 9.0+ --> <!-- CAP packages for .NET 9.0+ -->
<PackageVersion Include="DotNetCore.CAP.Dashboard" Version="8.4.1" /> <PackageVersion Include="DotNetCore.CAP.Dashboard" Version="8.4.1" />
<PackageVersion Include="DotNetCore.CAP.RabbitMQ" Version="8.4.1" /> <PackageVersion Include="DotNetCore.CAP.RabbitMQ" Version="8.4.1" />
@ -50,13 +46,10 @@
<PackageVersion Include="DotNetCore.CAP.RedisStreams" Version="8.4.1" /> <PackageVersion Include="DotNetCore.CAP.RedisStreams" Version="8.4.1" />
<PackageVersion Include="DotNetCore.CAP.Pulsar" Version="8.4.1" /> <PackageVersion Include="DotNetCore.CAP.Pulsar" Version="8.4.1" />
<PackageVersion Include="DotNetCore.CAP.OpenTelemetry" Version="8.4.1" /> <PackageVersion Include="DotNetCore.CAP.OpenTelemetry" Version="8.4.1" />
<!-- FastEndpoints --> <!-- FastEndpoints -->
<PackageVersion Include="FastEndpoints" Version="$(FastEndpointsVersion)" /> <PackageVersion Include="FastEndpoints" Version="$(FastEndpointsVersion)" />
<PackageVersion Include="FastEndpoints.Swagger" Version="$(FastEndpointsVersion)" /> <PackageVersion Include="FastEndpoints.Swagger" Version="$(FastEndpointsVersion)" />
<PackageVersion Include="FastEndpoints.Swagger.Swashbuckle" Version="2.3.0" /> <PackageVersion Include="FastEndpoints.Swagger.Swashbuckle" Version="2.3.0" />
<!-- Other packages --> <!-- Other packages -->
<PackageVersion Include="FluentValidation.AspNetCore" Version="11.3.1" /> <PackageVersion Include="FluentValidation.AspNetCore" Version="11.3.1" />
<PackageVersion Include="FluentValidation.DependencyInjectionExtensions" Version="11.3.1" /> <PackageVersion Include="FluentValidation.DependencyInjectionExtensions" Version="11.3.1" />
@ -75,7 +68,6 @@
<PackageVersion Include="Serilog.Sinks.OpenTelemetry" Version="4.1.0" /> <PackageVersion Include="Serilog.Sinks.OpenTelemetry" Version="4.1.0" />
<PackageVersion Include="StackExchange.Redis" Version="2.9.32" /> <PackageVersion Include="StackExchange.Redis" Version="2.9.32" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="7.2.0" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="7.2.0" />
<!-- Aspire packages --> <!-- Aspire packages -->
<PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireVersion)" /> <PackageVersion Include="Aspire.Hosting.AppHost" Version="$(AspireVersion)" />
<PackageVersion Include="Aspire.Hosting.Docker" Version="13.1.0-preview.1.25616.3" /> <PackageVersion Include="Aspire.Hosting.Docker" Version="13.1.0-preview.1.25616.3" />
@ -103,7 +95,6 @@
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="$(OpenTelemetryVersion)" /> <PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="$(OpenTelemetryVersion)" />
<PackageVersion Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.13.0-beta.1" /> <PackageVersion Include="OpenTelemetry.Instrumentation.SqlClient" Version="1.13.0-beta.1" />
<PackageVersion Include="Npgsql.OpenTelemetry" Version="8.0.8" /> <PackageVersion Include="Npgsql.OpenTelemetry" Version="8.0.8" />
<!-- NetCorePal packages --> <!-- NetCorePal packages -->
<PackageVersion Include="NetCorePal.Context.AspNetCore" Version="$(NetCorePalVersion)" /> <PackageVersion Include="NetCorePal.Context.AspNetCore" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Context.CAP" Version="$(NetCorePalVersion)" /> <PackageVersion Include="NetCorePal.Context.CAP" Version="$(NetCorePalVersion)" />
@ -124,14 +115,13 @@
<PackageVersion Include="NetCorePal.Extensions.MicrosoftServiceDiscovery" Version="$(NetCorePalVersion)" /> <PackageVersion Include="NetCorePal.Extensions.MicrosoftServiceDiscovery" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.MultiEnv" Version="$(NetCorePalVersion)" /> <PackageVersion Include="NetCorePal.Extensions.MultiEnv" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.NewtonsoftJson" Version="$(NetCorePalVersion)" /> <PackageVersion Include="NetCorePal.Extensions.NewtonsoftJson" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.Primitives" Version="$(NetCorePalVersion)" /> <PackageVersion Include="NetCorePal.Extensions.Primitives" Version="3.2.1" />
<PackageVersion Include="NetCorePal.Extensions.Repository.EntityFrameworkCore" Version="$(NetCorePalVersion)" /> <PackageVersion Include="NetCorePal.Extensions.Repository.EntityFrameworkCore" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Extensions.Repository.EntityFrameworkCore.Snowflake" Version="$(NetCorePalVersion)" /> <PackageVersion Include="NetCorePal.Extensions.Repository.EntityFrameworkCore.Snowflake" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.OpenTelemetry.Diagnostics" Version="$(NetCorePalVersion)" /> <PackageVersion Include="NetCorePal.OpenTelemetry.Diagnostics" Version="$(NetCorePalVersion)" />
<PackageVersion Include="NetCorePal.Aspire.Hosting.DMDB" Version="$(NetCorePalAspireVersion)" /> <PackageVersion Include="NetCorePal.Aspire.Hosting.DMDB" Version="$(NetCorePalAspireVersion)" />
<PackageVersion Include="NetCorePal.Aspire.Hosting.OpenGauss" Version="$(NetCorePalAspireVersion)" /> <PackageVersion Include="NetCorePal.Aspire.Hosting.OpenGauss" Version="$(NetCorePalAspireVersion)" />
<PackageVersion Include="NetCorePal.Aspire.Hosting.MongoDB" Version="$(NetCorePalAspireVersion)" /> <PackageVersion Include="NetCorePal.Aspire.Hosting.MongoDB" Version="$(NetCorePalAspireVersion)" />
<!-- Testing packages --> <!-- Testing packages -->
<PackageVersion Include="Moq" Version="4.20.72" /> <PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="Testcontainers" Version="$(TestcontainersVersion)" /> <PackageVersion Include="Testcontainers" Version="$(TestcontainersVersion)" />
@ -150,7 +140,6 @@
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" /> <PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" /> <PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="FastEndpoints.Testing" Version="$(FastEndpointsVersion)" /> <PackageVersion Include="FastEndpoints.Testing" Version="$(FastEndpointsVersion)" />
<!-- Code analysis --> <!-- Code analysis -->
<PackageVersion Include="SonarAnalyzer.CSharp" Version="10.3.0.106239" /> <PackageVersion Include="SonarAnalyzer.CSharp" Version="10.3.0.106239" />
</ItemGroup> </ItemGroup>

View File

@ -0,0 +1,114 @@
using Fengling.Member.Application.Dtos;
using Fengling.Member.Domain.Aggregates.PointsRuleModel;
using Fengling.Member.Domain.Aggregates.PointsRuleModel.Enums;
using Fengling.Member.Domain.Repositories;
using NetCorePal.Extensions.Domain;
namespace Fengling.Member.Application.Services;
public class PointsRuleMatcher
{
private readonly IPointsRuleRepository _ruleRepository;
public PointsRuleMatcher(IPointsRuleRepository ruleRepository)
{
_ruleRepository = ruleRepository;
}
public async Task<PointsCalculationResultDto> CalculatePointsAsync(CodeInfoDto codeInfo)
{
var result = new PointsCalculationResultDto(
Success: false,
Points: 0,
ExpireAt: DateTime.MinValue,
AppliedRuleId: null,
Message: "No matching rule found",
MatchedDimensions: new List<string>()
);
var allRules = await _ruleRepository.GetActiveRulesAsync();
var matchedRules = new List<(PointsRule Rule, int MatchCount)>();
foreach (var rule in allRules)
{
var matchCount = CountMatchingConditions(rule, codeInfo);
if (matchCount > 0)
{
matchedRules.Add((rule, matchCount));
}
}
if (!matchedRules.Any())
{
return result;
}
var bestMatch = matchedRules
.OrderByDescending(x => x.MatchCount)
.ThenByDescending(x => x.Rule.Priority)
.First();
var matchedRule = bestMatch.Rule;
int points = matchedRule.BasePoints;
if (matchedRule.RuleType == RuleType.PriceWeighted &&
decimal.TryParse(codeInfo.ProductPrice, out var price))
{
points = (int)(price * (matchedRule.WeightFactor ?? 1m));
}
var matchedDimensions = GetMatchedDimensions(matchedRule, codeInfo);
return new PointsCalculationResultDto(
Success: true,
Points: points,
ExpireAt: DateTime.UtcNow.AddDays(matchedRule.ValidityDays),
AppliedRuleId: (matchedRule.Id as IStronglyTypedId<Guid>)?.Id,
Message: "Success",
MatchedDimensions: matchedDimensions
);
}
private int CountMatchingConditions(PointsRule rule, CodeInfoDto codeInfo)
{
var count = 0;
foreach (var condition in rule.Conditions)
{
if (MatchesCondition(condition, codeInfo))
{
count++;
}
}
return count;
}
private bool MatchesCondition(PointsRuleCondition condition, CodeInfoDto codeInfo)
{
return condition.DimensionValue switch
{
"*" => true,
_ => condition.DimensionType switch
{
DimensionType.Product => condition.DimensionValue == codeInfo.ProductId,
DimensionType.Dealer => condition.DimensionValue == codeInfo.DealerId,
DimensionType.Distributor => condition.DimensionValue == codeInfo.DistributorId,
DimensionType.Store => condition.DimensionValue == codeInfo.StoreId,
_ => false
}
};
}
private List<string> GetMatchedDimensions(PointsRule rule, CodeInfoDto codeInfo)
{
var matched = new List<string>();
foreach (var condition in rule.Conditions)
{
if (MatchesCondition(condition, codeInfo))
{
matched.Add($"{condition.DimensionType}:{condition.DimensionValue}");
}
}
return matched;
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>