using CampusErrand.Data; using CampusErrand.Models; using CampusErrand.Models.Dtos; using CampusErrand.Helpers; using CampusErrand.Services; using Microsoft.EntityFrameworkCore; namespace CampusErrand.Endpoints; public static class EarningEndpoints { public static void MapEarningEndpoints(this WebApplication app) { // 获取收益概览(四种金额状态) app.MapGet("/api/earnings", async (HttpContext httpContext, AppDbContext db) => { var userIdClaim = httpContext.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier); if (userIdClaim == null) return Results.Unauthorized(); var userId = int.Parse(userIdClaim.Value); // 先执行冻结解冻逻辑 await BusinessHelpers.UnfreezeEarnings(db); var earnings = await db.Earnings.Where(e => e.UserId == userId).ToListAsync(); var overview = new EarningsOverviewResponse { FrozenAmount = earnings.Where(e => e.Status == EarningStatus.Frozen).Sum(e => e.NetEarning), AvailableAmount = earnings.Where(e => e.Status == EarningStatus.Available).Sum(e => e.NetEarning), WithdrawingAmount = earnings.Where(e => e.Status == EarningStatus.Withdrawing).Sum(e => e.NetEarning), WithdrawnAmount = earnings.Where(e => e.Status == EarningStatus.Withdrawn).Sum(e => e.NetEarning) }; return Results.Ok(overview); }).RequireAuthorization(); // 获取收益记录 app.MapGet("/api/earnings/records", async (HttpContext httpContext, AppDbContext db) => { var userIdClaim = httpContext.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier); if (userIdClaim == null) return Results.Unauthorized(); var userId = int.Parse(userIdClaim.Value); var records = await db.Earnings .Where(e => e.UserId == userId) .Include(e => e.Order) .OrderByDescending(e => e.CreatedAt) .Select(e => new EarningRecordResponse { Id = e.Id, OrderId = e.OrderId, OrderNo = e.Order != null ? e.Order.OrderNo : "", OrderType = e.Order != null ? e.Order.OrderType.ToString() : "", GoodsAmount = e.GoodsAmount, Commission = e.Commission, PlatformFee = e.PlatformFee, NetEarning = e.NetEarning, Status = e.Status.ToString(), CompletedAt = e.Order != null ? e.Order.CompletedAt : null, CreatedAt = e.CreatedAt }) .ToListAsync(); return Results.Ok(records); }).RequireAuthorization(); // 获取提现记录 app.MapGet("/api/earnings/withdrawals", async (HttpContext httpContext, AppDbContext db) => { var userIdClaim = httpContext.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier); if (userIdClaim == null) return Results.Unauthorized(); var userId = int.Parse(userIdClaim.Value); var withdrawals = await db.Withdrawals .Where(w => w.UserId == userId) .OrderByDescending(w => w.CreatedAt) .Select(w => new WithdrawalRecordResponse { Id = w.Id, Amount = w.Amount, PaymentMethod = w.PaymentMethod.ToString(), Status = w.Status.ToString(), CreatedAt = w.CreatedAt }) .ToListAsync(); return Results.Ok(withdrawals); }).RequireAuthorization(); // 申请提现 app.MapPost("/api/earnings/withdraw", async (WithdrawRequest request, HttpContext httpContext, AppDbContext db) => { var userIdClaim = httpContext.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier); if (userIdClaim == null) return Results.Unauthorized(); var userId = int.Parse(userIdClaim.Value); // 金额校验:读取最低提现金额配置 var minWithdrawalConfig = await db.SystemConfigs.FirstOrDefaultAsync(c => c.Key == "min_withdrawal"); var minWithdrawal = 1.0m; if (minWithdrawalConfig != null && decimal.TryParse(minWithdrawalConfig.Value, out var configMinW)) minWithdrawal = configMinW; if (request.Amount < minWithdrawal) return Results.BadRequest(new { code = 400, message = $"提现金额不能低于{minWithdrawal}元" }); // 金额校验:小数点后最多 2 位 if (request.Amount != Math.Round(request.Amount, 2)) return Results.BadRequest(new { code = 400, message = "请输入正确的提现金额" }); // 收款方式默认微信零钱 var paymentMethod = PaymentMethod.WeChat; if (!string.IsNullOrEmpty(request.PaymentMethod)) Enum.TryParse(request.PaymentMethod, true, out paymentMethod); // 先执行冻结解冻逻辑 await BusinessHelpers.UnfreezeEarnings(db); // 计算可提现余额 var availableAmount = await db.Earnings .Where(e => e.UserId == userId && e.Status == EarningStatus.Available) .SumAsync(e => e.NetEarning); if (request.Amount > availableAmount) return Results.BadRequest(new { code = 400, message = "超出可提现范围" }); // 创建提现记录 var withdrawal = new Withdrawal { UserId = userId, Amount = request.Amount, PaymentMethod = paymentMethod, QrCodeImage = request.QrCodeImage, Status = WithdrawalStatus.Pending, CreatedAt = DateTime.UtcNow }; db.Withdrawals.Add(withdrawal); // 将对应金额的收益标记为提现中(按创建时间先进先出) var remainingAmount = request.Amount; var availableEarnings = await db.Earnings .Where(e => e.UserId == userId && e.Status == EarningStatus.Available) .OrderBy(e => e.CreatedAt) .ToListAsync(); foreach (var earning in availableEarnings) { if (remainingAmount <= 0) break; if (earning.NetEarning <= remainingAmount) { earning.Status = EarningStatus.Withdrawing; remainingAmount -= earning.NetEarning; } else { // 需要拆分收益记录:部分提现 earning.Status = EarningStatus.Withdrawing; var originalNet = earning.NetEarning; earning.NetEarning = remainingAmount; // 创建剩余部分的新收益记录 var remainingEarning = new Earning { UserId = earning.UserId, OrderId = earning.OrderId, GoodsAmount = earning.GoodsAmount, Commission = earning.Commission, PlatformFee = 0, NetEarning = originalNet - remainingAmount, Status = EarningStatus.Available, FrozenUntil = earning.FrozenUntil, CreatedAt = earning.CreatedAt }; db.Earnings.Add(remainingEarning); remainingAmount = 0; } } await db.SaveChangesAsync(); return Results.Created($"/api/earnings/withdrawals/{withdrawal.Id}", new WithdrawalRecordResponse { Id = withdrawal.Id, Amount = withdrawal.Amount, PaymentMethod = withdrawal.PaymentMethod.ToString(), Status = withdrawal.Status.ToString(), CreatedAt = withdrawal.CreatedAt }); }).RequireAuthorization(); // 管理端获取提现列表 app.MapGet("/api/admin/withdrawals", async (string? status, AppDbContext db) => { var query = db.Withdrawals .Include(w => w.User) .AsQueryable(); if (!string.IsNullOrEmpty(status) && Enum.TryParse(status, out var s)) query = query.Where(w => w.Status == s); var list = await query .OrderByDescending(w => w.CreatedAt) .Select(w => new { w.Id, w.UserId, UserNickname = w.User!.Nickname ?? ("用户" + w.UserId), w.Amount, PaymentMethod = w.PaymentMethod.ToString(), w.QrCodeImage, Status = w.Status.ToString(), w.CreatedAt, w.ProcessedAt }) .ToListAsync(); return Results.Ok(list); }).RequireAuthorization("AdminOnly"); // 管理端审核提现(通过/拒绝) app.MapPut("/api/admin/withdrawals/{id}", async (int id, AdminWithdrawalRequest request, AppDbContext db, WxPayService wxPay) => { var withdrawal = await db.Withdrawals.FindAsync(id); if (withdrawal == null) return Results.NotFound(new { code = 404, message = "提现记录不存在" }); if (withdrawal.Status != WithdrawalStatus.Pending && withdrawal.Status != WithdrawalStatus.Processing) return Results.BadRequest(new { code = 400, message = "该提现记录已处理完毕" }); if (request.Action == "approve") { // 先调用微信商家转账到零钱 var user = await db.Users.FindAsync(withdrawal.UserId); if (user == null || string.IsNullOrEmpty(user.OpenId)) return Results.BadRequest(new { code = 400, message = "用户信息异常,无法转账" }); var amountFen = (int)(withdrawal.Amount * 100); var batchNo = $"W{withdrawal.Id}_{DateTime.UtcNow:yyyyMMddHHmmss}"; var detailNo = $"D{withdrawal.Id}_{DateTime.UtcNow:yyyyMMddHHmmss}"; var (transferSuccess, transferError) = await wxPay.TransferToWallet(batchNo, detailNo, user.OpenId, amountFen, "跑腿提现到账"); if (!transferSuccess) { Console.WriteLine($"[提现] 转账失败: {transferError}"); return Results.BadRequest(new { code = 400, message = $"转账失败,请稍后重试", detail = transferError }); } withdrawal.Status = WithdrawalStatus.Completed; withdrawal.ProcessedAt = DateTime.UtcNow; // 将对应收益标记为已提现 var earnings = await db.Earnings .Where(e => e.UserId == withdrawal.UserId && e.Status == EarningStatus.Withdrawing) .OrderBy(e => e.CreatedAt) .ToListAsync(); var remaining = withdrawal.Amount; foreach (var earning in earnings) { if (remaining <= 0) break; if (earning.NetEarning <= remaining) { earning.Status = EarningStatus.Withdrawn; remaining -= earning.NetEarning; } else { earning.Status = EarningStatus.Withdrawn; remaining = 0; } } } else if (request.Action == "reject") { withdrawal.Status = WithdrawalStatus.Pending; withdrawal.ProcessedAt = DateTime.UtcNow; // 将对应收益退回待提现状态 var earnings = await db.Earnings .Where(e => e.UserId == withdrawal.UserId && e.Status == EarningStatus.Withdrawing) .OrderBy(e => e.CreatedAt) .ToListAsync(); var remaining = withdrawal.Amount; foreach (var earning in earnings) { if (remaining <= 0) break; if (earning.NetEarning <= remaining) { earning.Status = EarningStatus.Available; remaining -= earning.NetEarning; } else { earning.Status = EarningStatus.Available; remaining = 0; } } // 拒绝后将提现记录状态设为特殊标记(复用 Pending 但已有 ProcessedAt) // 实际上拒绝后应该删除或标记,这里直接删除提现记录 db.Withdrawals.Remove(withdrawal); } else if (request.Action == "processing") { withdrawal.Status = WithdrawalStatus.Processing; } else { return Results.BadRequest(new { code = 400, message = "无效操作,可选: approve, reject, processing" }); } await db.SaveChangesAsync(); return Results.Ok(new { message = "操作成功" }); }).RequireAuthorization("AdminOnly"); // 获取佣金规则 app.MapGet("/api/admin/commission-rules", async (AppDbContext db) => { var rules = await db.CommissionRules .OrderBy(r => r.MinAmount) .ToListAsync(); return Results.Ok(rules); }).RequireAuthorization("AdminOnly"); // 更新佣金规则 app.MapPut("/api/admin/commission-rules", async (List rules, AppDbContext db) => { // 清除旧规则 var existing = await db.CommissionRules.ToListAsync(); db.CommissionRules.RemoveRange(existing); // 添加新规则 foreach (var rule in rules) { db.CommissionRules.Add(new CommissionRule { MinAmount = rule.MinAmount, MaxAmount = rule.MaxAmount, RateType = rule.RateType, Rate = rule.Rate }); } await db.SaveChangesAsync(); return Results.Ok(await db.CommissionRules.OrderBy(r => r.MinAmount).ToListAsync()); }).RequireAuthorization("AdminOnly"); } }