using CampusErrand.Data; using CampusErrand.Models; using CampusErrand.Models.Dtos; using CampusErrand.Helpers; using CampusErrand.Services; using Microsoft.EntityFrameworkCore; using System.Text.Json; 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(), RejectReason = w.RejectReason, 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, 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), UserUid = w.User!.Uid ?? w.UserId.ToString(), w.Amount, PaymentMethod = w.PaymentMethod.ToString(), 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 outBillNo = $"W{withdrawal.Id}T{DateTime.UtcNow:yyyyMMddHHmmss}"; // 构造回调地址 var baseUrl = app.Configuration["BaseUrl"] ?? "https://your-domain.com"; var notifyUrl = $"{baseUrl}/api/notify/transfer"; var transferResult = await wxPay.TransferToWallet(outBillNo, user.OpenId, amountFen, notifyUrl, "跑腿提现到账"); if (!transferResult.Success) { Console.WriteLine($"[提现] 转账失败: {transferResult.ErrorMessage}"); // 解析微信错误信息,返回友好提示 var friendlyMsg = "转账失败,请稍后重试"; try { var errJson = JsonSerializer.Deserialize(transferResult.ErrorMessage ?? "{}"); var errCode = errJson.TryGetProperty("code", out var c) ? c.GetString() : ""; var errMessage = errJson.TryGetProperty("message", out var m) ? m.GetString() : ""; friendlyMsg = errCode switch { "NOT_ENOUGH" => "商户账户余额不足,请充值后重试", "NO_AUTH" => "没有转账权限,请检查商户平台配置", "PARAM_ERROR" => $"参数错误:{errMessage}", "INVALID_REQUEST" => $"请求无效:{errMessage}", "FREQUENCY_LIMIT_EXCEED" => "请求频率超限,请稍后重试", _ => $"转账失败:{errMessage}" }; } catch { } return Results.BadRequest(new { code = 400, message = friendlyMsg }); } // 保存转账信息,等待用户确认收款 withdrawal.Status = WithdrawalStatus.WaitConfirm; withdrawal.TransferBillNo = transferResult.TransferBillNo; withdrawal.PackageInfo = transferResult.PackageInfo; withdrawal.ProcessedAt = DateTime.UtcNow; } else if (request.Action == "reject") { if (string.IsNullOrWhiteSpace(request.Reason)) return Results.BadRequest(new { code = 400, message = "请填写拒绝理由" }); withdrawal.Status = WithdrawalStatus.Rejected; withdrawal.ProcessedAt = DateTime.UtcNow; withdrawal.RejectReason = request.Reason.Trim(); // 将对应收益退回待提现状态 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; } } } 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"); // 小程序端:获取待确认收款的提现记录(含 package_info) app.MapGet("/api/earnings/withdrawals/pending-confirm", 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 list = await db.Withdrawals .Where(w => w.UserId == userId && w.Status == WithdrawalStatus.WaitConfirm) .OrderByDescending(w => w.ProcessedAt) .Select(w => new { w.Id, w.Amount, w.TransferBillNo, w.PackageInfo, w.ProcessedAt }) .ToListAsync(); return Results.Ok(list); }).RequireAuthorization(); // 微信转账结果回调 app.MapPost("/api/notify/transfer", async (HttpContext httpContext, AppDbContext db, WxPayService wxPay) => { using var reader = new StreamReader(httpContext.Request.Body); var body = await reader.ReadToEndAsync(); var serialNo = httpContext.Request.Headers["Wechatpay-Serial"].ToString(); var timestamp = httpContext.Request.Headers["Wechatpay-Timestamp"].ToString(); var nonce = httpContext.Request.Headers["Wechatpay-Nonce"].ToString(); var signature = httpContext.Request.Headers["Wechatpay-Signature"].ToString(); try { // 用转账专用方法解密回调数据 var notifyResult = wxPay.DecryptTransferNotify(body); if (notifyResult == null) { Console.WriteLine("[转账回调] 解密失败"); return Results.Json(new { code = "FAIL", message = "解密失败" }, statusCode: 500); } var outBillNo = notifyResult.OutBillNo; var transferBillNo = notifyResult.TransferBillNo; var state = notifyResult.State; Console.WriteLine($"[转账回调] outBillNo={outBillNo}, transferBillNo={transferBillNo}, state={state}"); // 通过微信转账单号精确匹配提现记录 var withdrawal = await db.Withdrawals .FirstOrDefaultAsync(w => w.TransferBillNo == transferBillNo && w.Status == WithdrawalStatus.WaitConfirm); if (withdrawal == null) { Console.WriteLine($"[转账回调] 未找到匹配的提现记录: {transferBillNo}"); return Results.Json(new { code = "SUCCESS", message = "OK" }); } if (state == "SUCCESS" || state == "ACCEPTED") { // 转账成功,标记提现完成 if (withdrawal != null && withdrawal.Status == WithdrawalStatus.WaitConfirm) { withdrawal.Status = WithdrawalStatus.Completed; // 将对应收益标记为已提现 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; } } await db.SaveChangesAsync(); Console.WriteLine($"[转账回调] 提现完成: withdrawalId={withdrawal.Id}"); } } else if (state == "FAIL" || state == "CLOSED") { // 转账失败/关闭,退回收益 if (withdrawal != null && withdrawal.Status == WithdrawalStatus.WaitConfirm) { withdrawal.Status = WithdrawalStatus.Pending; 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; } } db.Withdrawals.Remove(withdrawal); await db.SaveChangesAsync(); Console.WriteLine($"[转账回调] 转账失败/关闭,已退回: withdrawalId={withdrawal.Id}"); } } return Results.Json(new { code = "SUCCESS", message = "OK" }); } catch (Exception ex) { Console.WriteLine($"[转账回调] 处理异常: {ex.Message}"); return Results.Json(new { code = "FAIL", message = ex.Message }, statusCode: 500); } }); } }