campus-errand/server/Helpers/BusinessHelpers.cs
18631081161 3f23dee33f
All checks were successful
continuous-integration/drone/push Build is passing
佣金
2026-04-05 14:10:51 +08:00

207 lines
7.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using CampusErrand.Data;
using CampusErrand.Models;
using CampusErrand.Models.Dtos;
using Microsoft.EntityFrameworkCore;
namespace CampusErrand.Helpers;
/// <summary>
/// 业务辅助方法集合
/// </summary>
public static class BusinessHelpers
{
/// <summary>
/// Banner 请求校验
/// </summary>
internal static List<object> ValidateBannerRequest(BannerRequest request)
{
var errors = new List<object>();
if (string.IsNullOrWhiteSpace(request.ImageUrl))
errors.Add(new { field = "imageUrl", message = "图片地址不能为空" });
if (string.IsNullOrWhiteSpace(request.LinkUrl))
errors.Add(new { field = "linkUrl", message = "链接地址不能为空" });
if (string.IsNullOrWhiteSpace(request.LinkType))
errors.Add(new { field = "linkType", message = "链接类型不能为空" });
else if (!Enum.TryParse<LinkType>(request.LinkType, true, out var parsed) || !Enum.IsDefined(parsed))
errors.Add(new { field = "linkType", message = "链接类型不合法" });
return errors;
}
/// <summary>
/// 门店请求校验
/// </summary>
internal static List<object> ValidateShopRequest(ShopRequest request)
{
var errors = new List<object>();
if (string.IsNullOrWhiteSpace(request.Name))
errors.Add(new { field = "name", message = "门店名称不能为空" });
if (string.IsNullOrWhiteSpace(request.Photo))
errors.Add(new { field = "photo", message = "门店照片不能为空" });
if (string.IsNullOrWhiteSpace(request.Location))
errors.Add(new { field = "location", message = "门店位置不能为空" });
if (string.IsNullOrWhiteSpace(request.PackingFeeType))
errors.Add(new { field = "packingFeeType", message = "打包费类型不能为空" });
if (request.PackingFeeAmount < 0)
errors.Add(new { field = "packingFeeAmount", message = "打包费金额不能为负数" });
return errors;
}
/// <summary>
/// 菜品请求校验
/// </summary>
internal static List<object> ValidateDishRequest(DishRequest request)
{
var errors = new List<object>();
if (string.IsNullOrWhiteSpace(request.Name))
errors.Add(new { field = "name", message = "菜品名称不能为空" });
if (string.IsNullOrWhiteSpace(request.Photo))
errors.Add(new { field = "photo", message = "菜品照片不能为空" });
if (request.Price < 0)
errors.Add(new { field = "price", message = "菜品价格不能为负数" });
return errors;
}
/// <summary>
/// 计算单个门店的打包费
/// Fixed 模式:固定金额,不论菜品数量
/// PerItem 模式:菜品总份数 × 单份打包费
/// </summary>
internal static decimal CalculatePackingFee(PackingFeeType feeType, decimal feeAmount, int totalQuantity)
{
return feeType switch
{
PackingFeeType.Fixed => feeAmount,
PackingFeeType.PerItem => feeAmount * totalQuantity,
_ => 0m
};
}
/// <summary>
/// 计算平台佣金分成
/// 根据佣金区间规则计算:百分比类型按比例,固定类型扣除固定金额
/// </summary>
internal static decimal CalculatePlatformFee(decimal commission, List<CommissionRule> rules)
{
// 查找匹配的佣金区间
var matchedRule = rules
.Where(r => commission >= r.MinAmount && (r.MaxAmount == null || commission <= r.MaxAmount))
.FirstOrDefault();
if (matchedRule == null) return 0m;
return matchedRule.RateType switch
{
CommissionRateType.Percentage => Math.Round(commission * matchedRule.Rate / 100m, 2),
CommissionRateType.Fixed => Math.Min(matchedRule.Rate, commission),
_ => 0m
};
}
/// <summary>
/// 获取用户可见的系统消息(按目标类型过滤)
/// </summary>
internal static async Task<List<SystemMessage>> GetVisibleSystemMessages(AppDbContext db, int userId, User? user)
{
var allMessages = await db.SystemMessages.ToListAsync();
return allMessages.Where(m =>
{
if (m.TargetType == MessageTargetType.All) return true;
if (m.TargetType == MessageTargetType.OrderUser)
return user != null && user.Role != UserRole.Runner;
if (m.TargetType == MessageTargetType.RunnerUser)
return user != null && (user.Role == UserRole.Runner || user.Role == UserRole.Admin);
if (m.TargetType == MessageTargetType.Specific && m.TargetUserIds != null)
{
try
{
var ids = System.Text.Json.JsonSerializer.Deserialize<List<int>>(m.TargetUserIds);
return ids != null && ids.Contains(userId);
}
catch { return false; }
}
return false;
}).ToList();
}
/// <summary>
/// 解冻已到期的收益记录:将冻结期满的收益从 Frozen 变为 Available
/// </summary>
internal static async Task UnfreezeEarnings(AppDbContext db)
{
var now = DateTime.UtcNow;
var frozenEarnings = await db.Earnings
.Where(e => e.Status == EarningStatus.Frozen && e.FrozenUntil <= now)
.ToListAsync();
foreach (var earning in frozenEarnings)
{
earning.Status = EarningStatus.Available;
}
if (frozenEarnings.Count > 0)
{
await db.SaveChangesAsync();
}
}
/// <summary>
/// 自动确认超过24小时未处理的待确认订单
/// </summary>
internal static async Task AutoConfirmExpiredOrders(AppDbContext db)
{
var cutoff = DateTime.UtcNow.AddHours(-24);
var expiredOrders = await db.Orders
.Where(o => o.Status == OrderStatus.WaitConfirm && o.CompletedAt != null && o.CompletedAt <= cutoff)
.ToListAsync();
foreach (var order in expiredOrders)
{
order.Status = OrderStatus.Completed;
// 计算佣金收益(商品金额平台不抽成,全额给跑腿)
var rules = await db.CommissionRules.OrderBy(r => r.MinAmount).ToListAsync();
var platformFee = CalculatePlatformFee(order.Commission, rules);
var netEarning = order.Commission - platformFee + (order.GoodsAmount ?? 0);
var freezeDaysConfig = await db.SystemConfigs.FirstOrDefaultAsync(c => c.Key == "freeze_days");
var freezeDays = 1;
if (freezeDaysConfig != null && int.TryParse(freezeDaysConfig.Value, out var configDays))
freezeDays = configDays;
db.Earnings.Add(new Earning
{
UserId = order.RunnerId!.Value,
OrderId = order.Id,
GoodsAmount = order.GoodsAmount,
Commission = order.Commission,
PlatformFee = platformFee,
NetEarning = netEarning,
Status = EarningStatus.Frozen,
FrozenUntil = DateTime.UtcNow.AddDays(freezeDays),
CreatedAt = DateTime.UtcNow
});
}
if (expiredOrders.Count > 0)
{
await db.SaveChangesAsync();
Console.WriteLine($"[定时任务] 自动确认了 {expiredOrders.Count} 个超时订单");
}
}
/// <summary>
/// 生成唯一的随机6位数字UID
/// </summary>
internal static async Task<string> GenerateUniqueUid(AppDbContext db)
{
for (var i = 0; i < 100; i++)
{
var uid = Random.Shared.Next(100000, 999999).ToString();
var exists = await db.Users.AnyAsync(u => u.Uid == uid);
if (!exists) return uid;
}
// 极端情况用7位
return Random.Shared.Next(1000000, 9999999).ToString();
}
}