campus-errand/server/Endpoints/EarningEndpoints.cs
18631081161 2d0c71721d
All checks were successful
continuous-integration/drone/push Build is passing
改bug
2026-03-29 21:13:50 +08:00

331 lines
14 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 CampusErrand.Helpers;
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);
// 金额校验:最低 1 元
if (request.Amount < 1.0m)
return Results.BadRequest(new { code = 400, message = "提现金额不能低于1元" });
// 金额校验:小数点后最多 2 位
if (request.Amount != Math.Round(request.Amount, 2))
return Results.BadRequest(new { code = 400, message = "请输入正确的提现金额" });
// 收款方式校验
if (!Enum.TryParse<PaymentMethod>(request.PaymentMethod, true, out var paymentMethod) || !Enum.IsDefined(paymentMethod))
return Results.BadRequest(new { code = 400, message = "收款方式不合法" });
// 收款二维码校验
if (string.IsNullOrWhiteSpace(request.QrCodeImage))
return Results.BadRequest(new { code = 400, message = "收款二维码不能为空" });
// 先执行冻结解冻逻辑
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<WithdrawalStatus>(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) =>
{
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")
{
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<CommissionRule> 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");
}
}