All checks were successful
continuous-integration/drone/push Build is passing
207 lines
7.9 KiB
C#
207 lines
7.9 KiB
C#
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();
|
||
}
|
||
}
|