admin/src/Oms.Application/Shared/MarketingInviteRewardCalculator.cs
zzh f972a28d79
All checks were successful
Build and Deploy / deploy (push) Successful in 3m50s
feat: 落地滚动会员、券发放与邀请分成奖励
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 17:40:24 +08:00

142 lines
3.6 KiB
C#

namespace Oms.Application.Shared;
public class InviteRewardRuleSnapshot
{
public decimal MinPaidBaseFreight { get; set; } = 50M;
public decimal FirstOrderRate { get; set; } = 0.05M;
public decimal FirstOrderCap { get; set; } = 50M;
public decimal FollowOrderRate { get; set; } = 0.03M;
public decimal FollowOrderCap { get; set; } = 30M;
public int FollowWindowDays { get; set; } = 90;
public int MaxRewardOrders { get; set; } = 6;
public int SettleBusinessDays { get; set; } = 3;
public string MinInviteLevelCode { get; set; } = "V1";
}
public class InviteRewardDecision
{
public bool Eligible { get; set; }
public int OrderIndex { get; set; }
public decimal Rate { get; set; }
public decimal Cap { get; set; }
public decimal Amount { get; set; }
}
public static class MarketingInviteRewardCalculator
{
public static InviteRewardDecision Resolve(
decimal paidBaseFreight,
int completedOrderIndex,
DateTime refereeRegisterTime,
DateTime completeTime,
InviteRewardRuleSnapshot rule)
{
var decision = new InviteRewardDecision { OrderIndex = completedOrderIndex };
if (completedOrderIndex <= 0 || paidBaseFreight < rule.MinPaidBaseFreight)
{
return decision;
}
if (completedOrderIndex == 1)
{
decision.Eligible = true;
decision.Rate = rule.FirstOrderRate;
decision.Cap = rule.FirstOrderCap;
decision.Amount = Cap(paidBaseFreight * rule.FirstOrderRate, rule.FirstOrderCap);
return decision;
}
if (completedOrderIndex > rule.MaxRewardOrders)
{
return decision;
}
var windowEnd = refereeRegisterTime.Date.AddDays(rule.FollowWindowDays);
if (completeTime.Date > windowEnd)
{
return decision;
}
decision.Eligible = true;
decision.Rate = rule.FollowOrderRate;
decision.Cap = rule.FollowOrderCap;
decision.Amount = Cap(paidBaseFreight * rule.FollowOrderRate, rule.FollowOrderCap);
return decision;
}
public static DateTime AddBusinessDays(DateTime start, int days)
{
if (days <= 0)
{
return start;
}
var cursor = start;
var added = 0;
while (added < days)
{
cursor = cursor.AddDays(1);
if (cursor.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday)
{
continue;
}
added++;
}
return cursor;
}
public static int ParseLevelRank(string? levelCode)
{
if (string.IsNullOrWhiteSpace(levelCode))
{
return 0;
}
var code = levelCode.Trim();
if (code.Length >= 2 && (code[0] == 'V' || code[0] == 'v') && int.TryParse(code[1..], out var n))
{
return n;
}
return code.ToLowerInvariant() switch
{
"normal" => 0,
"silver" => 2,
"gold" => 3,
"diamond" => 4,
_ => 0
};
}
public static bool CanInvite(string? memberLevelCode, string? minInviteLevelCode)
{
return ParseLevelRank(memberLevelCode) >= ParseLevelRank(
string.IsNullOrWhiteSpace(minInviteLevelCode) ? "V1" : minInviteLevelCode);
}
private static decimal Cap(decimal amount, decimal cap)
{
var value = Math.Round(Math.Max(0M, amount), 2, MidpointRounding.AwayFromZero);
if (cap > 0)
{
value = Math.Min(value, cap);
}
return value;
}
}