campus-errand/server/Endpoints/AdminEndpoints.cs
18631081161 b359070a0e
All checks were successful
continuous-integration/drone/push Build is passing
聊天修改
2026-04-02 01:09:02 +08:00

666 lines
28 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.Services;
using Microsoft.EntityFrameworkCore;
namespace CampusErrand.Endpoints;
public static class AdminEndpoints
{
public static void MapAdminEndpoints(this WebApplication app)
{
// 首页概览统计接口
app.MapGet("/api/admin/dashboard", async (AppDbContext db) =>
{
var today = DateTime.UtcNow.Date;
// 用户统计
var totalUsers = await db.Users.CountAsync();
var todayUsers = await db.Users.CountAsync(u => u.CreatedAt >= today);
// 订单统计
var totalOrders = await db.Orders.CountAsync();
var todayOrders = await db.Orders.CountAsync(o => o.CreatedAt >= today);
var pendingOrders = await db.Orders.CountAsync(o => o.Status == OrderStatus.Pending);
var inProgressOrders = await db.Orders.CountAsync(o => o.Status == OrderStatus.InProgress);
var completedOrders = await db.Orders.CountAsync(o => o.Status == OrderStatus.Completed);
// 收益统计
var totalEarnings = await db.Earnings.SumAsync(e => (decimal?)e.Commission) ?? 0;
var todayEarnings = await db.Earnings.Where(e => e.CreatedAt >= today).SumAsync(e => (decimal?)e.Commission) ?? 0;
// 跑腿认证统计
var pendingCertifications = await db.RunnerCertifications.CountAsync(c => c.Status == CertificationStatus.Pending);
var approvedRunners = await db.RunnerCertifications.CountAsync(c => c.Status == CertificationStatus.Approved);
// 门店统计
var totalShops = await db.Shops.CountAsync();
var enabledShops = await db.Shops.CountAsync(s => s.IsEnabled);
// 最近7天订单趋势
var sevenDaysAgo = today.AddDays(-6);
var orderTrend = await db.Orders
.Where(o => o.CreatedAt >= sevenDaysAgo)
.GroupBy(o => o.CreatedAt.Date)
.Select(g => new { Date = g.Key, Count = g.Count() })
.OrderBy(x => x.Date)
.ToListAsync();
// 补全7天数据没有订单的日期补0
var trendList = Enumerable.Range(0, 7).Select(i =>
{
var date = sevenDaysAgo.AddDays(i);
var count = orderTrend.FirstOrDefault(x => x.Date == date)?.Count ?? 0;
return new { Date = date.ToString("MM-dd"), Count = count };
}).ToList();
// 订单类型分布
var orderTypeDistribution = await db.Orders
.GroupBy(o => o.OrderType)
.Select(g => new { Type = g.Key.ToString(), Count = g.Count() })
.ToListAsync();
return Results.Ok(new
{
users = new { total = totalUsers, today = todayUsers },
orders = new { total = totalOrders, today = todayOrders, pending = pendingOrders, inProgress = inProgressOrders, completed = completedOrders },
earnings = new { total = totalEarnings, today = todayEarnings },
runners = new { pendingCertifications, approved = approvedRunners },
shops = new { total = totalShops, enabled = enabledShops },
orderTrend = trendList,
orderTypeDistribution
});
}).RequireAuthorization("AdminOnly");
app.MapGet("/api/admin/protected", () => Results.Ok("admin ok"))
.RequireAuthorization("AdminOnly");
// 管理端获取用户列表
app.MapGet("/api/admin/users", async (string? keyword, AppDbContext db) =>
{
var query = db.Users.AsQueryable();
// 关键词搜索昵称、手机号、ID
if (!string.IsNullOrWhiteSpace(keyword))
{
var kw = keyword.Trim();
if (int.TryParse(kw, out var uid))
{
query = query.Where(u => u.Id == uid || u.Nickname.Contains(kw) || u.Phone.Contains(kw));
}
else
{
query = query.Where(u => u.Nickname.Contains(kw) || u.Phone.Contains(kw));
}
}
var users = await query.OrderByDescending(u => u.CreatedAt)
.Select(u => new
{
u.Id,
u.Uid,
u.Nickname,
u.AvatarUrl,
u.Phone,
Role = u.Role.ToString(),
u.RunnerScore,
u.IsBanned,
u.CreatedAt,
OrderCount = db.Orders.Count(o => o.OwnerId == u.Id)
})
.ToListAsync();
return Results.Ok(users);
}).RequireAuthorization("AdminOnly");
// 管理端封禁/解封用户
app.MapPut("/api/admin/users/{id}/ban", async (int id, BanRunnerRequest request, AppDbContext db) =>
{
var user = await db.Users.FindAsync(id);
if (user == null)
return Results.NotFound(new { code = 404, message = "用户不存在" });
user.IsBanned = request.IsBanned;
await db.SaveChangesAsync();
return Results.Ok(new { user.Id, user.IsBanned });
}).RequireAuthorization("AdminOnly");
// 管理端删除用户
app.MapDelete("/api/admin/users/{id}", async (int id, AppDbContext db) =>
{
var user = await db.Users.FindAsync(id);
if (user == null)
return Results.NotFound(new { code = 404, message = "用户不存在" });
if (user.Role == UserRole.Admin)
return Results.BadRequest(new { code = 400, message = "不能删除管理员账号" });
try
{
// 获取该用户作为单主的订单ID列表
var ownedOrderIds = await db.Orders.Where(o => o.OwnerId == id).Select(o => o.Id).ToListAsync();
if (ownedOrderIds.Count > 0)
{
// 先删除订单的子表数据
db.FoodOrderItems.RemoveRange(db.FoodOrderItems.Where(f => ownedOrderIds.Contains(f.OrderId)));
db.PriceChanges.RemoveRange(db.PriceChanges.Where(p => ownedOrderIds.Contains(p.OrderId)));
db.Appeals.RemoveRange(db.Appeals.Where(a => ownedOrderIds.Contains(a.OrderId)));
db.Reviews.RemoveRange(db.Reviews.Where(r => ownedOrderIds.Contains(r.OrderId)));
db.Earnings.RemoveRange(db.Earnings.Where(e => ownedOrderIds.Contains(e.OrderId)));
await db.SaveChangesAsync();
// 再删除订单
db.Orders.RemoveRange(db.Orders.Where(o => ownedOrderIds.Contains(o.Id)));
await db.SaveChangesAsync();
}
// 将该用户作为跑腿的订单,清除跑腿关联
var takenOrders = await db.Orders.Where(o => o.RunnerId == id).ToListAsync();
foreach (var order in takenOrders)
{
order.RunnerId = null;
order.AcceptedAt = null;
if (order.Status == OrderStatus.InProgress || order.Status == OrderStatus.WaitConfirm)
order.Status = OrderStatus.Pending;
}
await db.SaveChangesAsync();
// 删除用户自身的关联数据
db.RunnerCertifications.RemoveRange(db.RunnerCertifications.Where(c => c.UserId == id));
db.Reviews.RemoveRange(db.Reviews.Where(r => r.RunnerId == id));
db.Earnings.RemoveRange(db.Earnings.Where(e => e.UserId == id));
db.Withdrawals.RemoveRange(db.Withdrawals.Where(w => w.UserId == id));
db.MessageReads.RemoveRange(db.MessageReads.Where(m => m.UserId == id));
db.PriceChanges.RemoveRange(db.PriceChanges.Where(p => p.InitiatorId == id));
await db.SaveChangesAsync();
// 最后删除用户
db.Users.Remove(user);
await db.SaveChangesAsync();
return Results.Ok(new { message = "用户已删除" });
}
catch (Exception ex)
{
Console.WriteLine($"[管理端] 删除用户失败: {ex.InnerException?.Message ?? ex.Message}");
return Results.BadRequest(new { code = 400, message = $"删除失败: {ex.InnerException?.Message ?? ex.Message}" });
}
}).RequireAuthorization("AdminOnly");
// 管理端获取跑腿列表
app.MapGet("/api/admin/runners", async (AppDbContext db) =>
{
var runners = await db.RunnerCertifications
.Where(c => c.Status == CertificationStatus.Approved)
.Join(db.Users, c => c.UserId, u => u.Id, (c, u) => new
{
u.Id,
u.Uid,
u.Nickname,
Phone = c.Phone,
u.RunnerScore,
u.IsBanned,
u.CreatedAt
})
.ToListAsync();
// 查询每个跑腿的评价统计
var runnerIds = runners.Select(r => r.Id).ToList();
var reviewStats = await db.Reviews
.Where(r => runnerIds.Contains(r.RunnerId) && !r.IsDisabled)
.GroupBy(r => r.RunnerId)
.Select(g => new
{
RunnerId = g.Key,
AvgRating = Math.Round(g.Average(r => (double)r.Rating), 1),
ReviewCount = g.Count()
})
.ToListAsync();
var statsMap = reviewStats.ToDictionary(s => s.RunnerId);
var result = runners.Select(r =>
{
statsMap.TryGetValue(r.Id, out var stats);
return new
{
r.Id, r.Nickname, r.Phone, r.RunnerScore, r.IsBanned, r.CreatedAt,
// 评价星级基于评分计算每20分一颗星最低1星最高5星
StarRating = Math.Round(Math.Clamp(r.RunnerScore / 20.0, 1.0, 5.0), 1),
ReviewCount = stats?.ReviewCount ?? 0
};
}).ToList();
return Results.Ok(result);
}).RequireAuthorization("AdminOnly");
// 管理端封禁/解封跑腿
app.MapPut("/api/admin/runners/{id}/ban", async (int id, BanRunnerRequest request, AppDbContext db) =>
{
var user = await db.Users.FindAsync(id);
if (user == null)
return Results.NotFound(new { code = 404, message = "用户不存在" });
// 确认该用户是已认证的跑腿
var hasCert = await db.RunnerCertifications
.AnyAsync(c => c.UserId == id && c.Status == CertificationStatus.Approved);
if (!hasCert)
return Results.BadRequest(new { code = 400, message = "该用户不是已认证的跑腿" });
user.IsBanned = request.IsBanned;
await db.SaveChangesAsync();
return Results.Ok(new
{
user.Id,
user.Nickname,
user.Phone,
user.RunnerScore,
user.IsBanned
});
}).RequireAuthorization("AdminOnly");
// 管理端查看跑腿评价记录
app.MapGet("/api/admin/runners/{id}/reviews", async (int id, AppDbContext db) =>
{
var reviews = await db.Reviews
.Where(r => r.RunnerId == id)
.Include(r => r.Order)
.OrderByDescending(r => r.CreatedAt)
.Select(r => new
{
r.Id,
r.OrderId,
OrderNo = r.Order != null ? r.Order.OrderNo : "",
OrderType = r.Order != null ? r.Order.OrderType.ToString() : "",
r.Rating,
r.Content,
r.ScoreChange,
r.IsDisabled,
r.CreatedAt
})
.ToListAsync();
return Results.Ok(reviews);
}).RequireAuthorization("AdminOnly");
// 管理端获取评价列表
app.MapGet("/api/admin/reviews", async (int? runnerId, int? ownerId, AppDbContext db) =>
{
var query = db.Reviews
.Include(r => r.Order).ThenInclude(o => o!.Owner)
.Include(r => r.Runner)
.AsQueryable();
if (runnerId.HasValue)
query = query.Where(r => r.RunnerId == runnerId.Value);
if (ownerId.HasValue)
query = query.Where(r => r.Order != null && r.Order.OwnerId == ownerId.Value);
var reviews = await query
.OrderByDescending(r => r.CreatedAt)
.Select(r => new
{
r.Id,
r.OrderId,
OrderNo = r.Order != null ? r.Order.OrderNo : "",
OwnerId = r.Order != null ? r.Order.OwnerId : 0,
OwnerNickname = r.Order != null && r.Order.Owner != null ? r.Order.Owner.Nickname : "",
r.RunnerId,
RunnerNickname = r.Runner != null ? r.Runner.Nickname : "",
r.Rating,
r.Content,
r.ScoreChange,
r.IsDisabled,
r.CreatedAt
})
.ToListAsync();
return Results.Ok(reviews);
}).RequireAuthorization("AdminOnly");
// 管理端禁用评价
app.MapPut("/api/admin/reviews/{id}/disable", async (int id, AppDbContext db) =>
{
var review = await db.Reviews.FindAsync(id);
if (review == null)
return Results.NotFound(new { code = 404, message = "评价不存在" });
if (review.IsDisabled)
return Results.BadRequest(new { code = 400, message = "该评价已被禁用" });
review.IsDisabled = true;
// 回退该评价对跑腿分数的影响
var runner = await db.Users.FindAsync(review.RunnerId);
if (runner != null)
{
runner.RunnerScore = Math.Clamp(runner.RunnerScore - review.ScoreChange, 0, 100);
}
await db.SaveChangesAsync();
return Results.Ok(new ReviewResponse
{
Id = review.Id,
OrderId = review.OrderId,
RunnerId = review.RunnerId,
Rating = review.Rating,
Content = review.Content,
ScoreChange = review.ScoreChange,
IsDisabled = review.IsDisabled,
CreatedAt = review.CreatedAt
});
}).RequireAuthorization("AdminOnly");
// 管理端获取订单列表
app.MapGet("/api/admin/orders", async (string? status, string? orderType, AppDbContext db) =>
{
var query = db.Orders
.Include(o => o.Owner)
.Include(o => o.Runner)
.AsQueryable();
if (!string.IsNullOrEmpty(status) && Enum.TryParse<OrderStatus>(status, true, out var s))
query = query.Where(o => o.Status == s);
if (!string.IsNullOrEmpty(orderType) && Enum.TryParse<OrderType>(orderType, true, out var t))
query = query.Where(o => o.OrderType == t);
var orders = await query
.OrderByDescending(o => o.CreatedAt)
.Select(o => new
{
o.Id,
o.OrderNo,
o.OwnerId,
OwnerUid = o.Owner != null ? o.Owner.Uid : "",
OwnerNickname = o.Owner != null ? o.Owner.Nickname : "",
o.RunnerId,
RunnerUid = o.Runner != null ? o.Runner.Uid : "",
RunnerNickname = o.Runner != null ? o.Runner.Nickname : "",
OrderType = o.OrderType.ToString(),
Status = o.Status.ToString(),
o.ItemName,
o.DeliveryLocation,
o.Commission,
o.GoodsAmount,
o.TotalAmount,
o.CreatedAt,
o.AcceptedAt,
o.CompletedAt
})
.ToListAsync();
return Results.Ok(orders);
}).RequireAuthorization("AdminOnly");
// 管理端将订单状态改为申诉中
app.MapPost("/api/admin/orders/{id}/appeal", async (int id, AppDbContext db) =>
{
var order = await db.Orders.FindAsync(id);
if (order == null)
return Results.NotFound(new { code = 404, message = "订单不存在" });
if (order.Status != OrderStatus.InProgress && order.Status != OrderStatus.WaitConfirm)
return Results.BadRequest(new { code = 400, message = "当前订单状态不支持申诉" });
order.Status = OrderStatus.Appealing;
await db.SaveChangesAsync();
return Results.Ok(new { id = order.Id, orderNo = order.OrderNo, status = order.Status.ToString() });
}).RequireAuthorization("AdminOnly");
// 管理端处理申诉结果
app.MapPost("/api/admin/orders/{id}/appeal/resolve", async (int id, ResolveAppealRequest request, AppDbContext db) =>
{
var order = await db.Orders.FindAsync(id);
if (order == null)
return Results.NotFound(new { code = 404, message = "订单不存在" });
if (order.Status != OrderStatus.Appealing)
return Results.BadRequest(new { code = 400, message = "仅申诉中的订单可处理" });
if (string.IsNullOrWhiteSpace(request.Result))
return Results.BadRequest(new { code = 400, message = "处理结果不能为空" });
if (!Enum.TryParse<OrderStatus>(request.NewStatus, true, out var newStatus)
|| (newStatus != OrderStatus.Completed && newStatus != OrderStatus.Cancelled))
return Results.BadRequest(new { code = 400, message = "目标状态不合法,仅支持 Completed 或 Cancelled" });
// 创建申诉记录
var appeal = new Appeal
{
OrderId = id,
Result = request.Result,
CreatedAt = DateTime.UtcNow
};
db.Appeals.Add(appeal);
// 更新订单状态
order.Status = newStatus;
if (newStatus == OrderStatus.Completed && order.CompletedAt == null)
{
order.CompletedAt = DateTime.UtcNow;
}
await db.SaveChangesAsync();
return Results.Ok(new
{
id = order.Id,
orderNo = order.OrderNo,
status = order.Status.ToString(),
appealId = appeal.Id,
appealResult = appeal.Result,
appealCreatedAt = appeal.CreatedAt
});
}).RequireAuthorization("AdminOnly");
// 管理端取消订单(含退款)
app.MapPost("/api/admin/orders/{id}/cancel", async (int id, AdminCancelOrderRequest request, AppDbContext db, WxPayService wxPay) =>
{
var order = await db.Orders.FindAsync(id);
if (order == null)
return Results.NotFound(new { code = 404, message = "订单不存在" });
if (order.Status == OrderStatus.Cancelled)
return Results.BadRequest(new { code = 400, message = "订单已取消" });
if (order.Status == OrderStatus.Completed)
return Results.BadRequest(new { code = 400, message = "已完成的订单不能取消" });
if (string.IsNullOrWhiteSpace(request.Reason))
return Results.BadRequest(new { code = 400, message = "请填写取消原因" });
// 更新订单状态
var wasPending = order.Status == OrderStatus.Pending || order.Status == OrderStatus.InProgress
|| order.Status == OrderStatus.WaitConfirm || order.Status == OrderStatus.Appealing;
order.Status = OrderStatus.Cancelled;
await db.SaveChangesAsync();
// 只对已支付的订单发起退款Unpaid 状态无需退款)
var refundResult = false;
if (wasPending)
{
var totalFen = (int)(order.TotalAmount * 100);
var refundNo = $"R{order.OrderNo}";
refundResult = await wxPay.Refund(order.OrderNo, refundNo, totalFen, totalFen, request.Reason);
Console.WriteLine($"[管理端] 取消订单 {order.OrderNo},原因:{request.Reason},退款:{(refundResult ? "" : "")}");
}
else
{
Console.WriteLine($"[管理端] 取消订单 {order.OrderNo}(未支付,无需退款),原因:{request.Reason}");
refundResult = true; // 未支付不需要退款,视为成功
}
return Results.Ok(new
{
id = order.Id,
orderNo = order.OrderNo,
status = order.Status.ToString(),
refundSuccess = refundResult,
reason = request.Reason
});
}).RequireAuthorization("AdminOnly");
// 管理端获取认证列表
app.MapGet("/api/admin/certifications", async (string? status, AppDbContext db) =>
{
var query = db.RunnerCertifications
.Include(c => c.User)
.AsQueryable();
// 按状态筛选
if (!string.IsNullOrEmpty(status) && Enum.TryParse<CertificationStatus>(status, true, out var certStatus))
{
query = query.Where(c => c.Status == certStatus);
}
var certifications = await query
.OrderByDescending(c => c.CreatedAt)
.Select(c => new
{
c.Id,
c.UserId,
UserUid = c.User != null ? c.User.Uid : "",
c.RealName,
c.Phone,
Status = c.Status.ToString(),
c.CreatedAt,
c.ReviewedAt,
UserNickname = c.User != null ? c.User.Nickname : "",
UserPhone = c.User != null ? c.User.Phone : ""
})
.ToListAsync();
return Results.Ok(certifications);
}).RequireAuthorization("AdminOnly");
// 管理端审核认证
app.MapPut("/api/admin/certifications/{id}", async (int id, ReviewCertificationRequest request, AppDbContext db) =>
{
var certification = await db.RunnerCertifications
.Include(c => c.User)
.FirstOrDefaultAsync(c => c.Id == id);
if (certification == null)
{
return Results.NotFound(new { code = 404, message = "认证记录不存在" });
}
if (certification.Status != CertificationStatus.Pending)
{
return Results.BadRequest(new { code = 400, message = "该认证已审核" });
}
if (!Enum.TryParse<CertificationStatus>(request.Status, true, out var newStatus)
|| (newStatus != CertificationStatus.Approved && newStatus != CertificationStatus.Rejected))
{
return Results.BadRequest(new { code = 400, message = "审核结果不合法,仅支持 Approved 或 Rejected" });
}
certification.Status = newStatus;
certification.ReviewedAt = DateTime.UtcNow;
// 审核通过时,更新用户角色为 Runner
if (newStatus == CertificationStatus.Approved && certification.User != null)
{
certification.User.Role = UserRole.Runner;
}
await db.SaveChangesAsync();
return Results.Ok(new CertificationResponse
{
Id = certification.Id,
UserId = certification.UserId,
RealName = certification.RealName,
Phone = certification.Phone,
Status = certification.Status.ToString(),
CreatedAt = certification.CreatedAt,
ReviewedAt = certification.ReviewedAt
});
}).RequireAuthorization("AdminOnly");
// 管理端聊天列表
app.MapGet("/api/admin/chat-list", async (AppDbContext db) =>
{
var orders = await db.Orders
.Where(o => o.RunnerId != null && o.Status != OrderStatus.Cancelled)
.OrderByDescending(o => o.CompletedAt ?? o.AcceptedAt ?? o.CreatedAt)
.Select(o => new
{
o.Id,
o.OrderNo,
OrderType = o.OrderType.ToString(),
o.ItemName,
Status = o.Status.ToString(),
o.Commission,
OwnerId = o.OwnerId,
OwnerUid = o.Owner!.Uid,
OwnerNickname = o.Owner!.Nickname,
OwnerAvatar = o.Owner!.AvatarUrl,
RunnerId = o.RunnerId,
RunnerUid = o.Runner!.Uid,
RunnerNickname = o.Runner!.Nickname,
RunnerAvatar = o.Runner!.AvatarUrl,
CreatedAt = o.CreatedAt
})
.ToListAsync();
var result = orders.Select(o => new
{
o.Id,
o.OrderNo,
o.OrderType,
o.ItemName,
o.Status,
o.Commission,
o.OwnerId,
OwnerUid = string.IsNullOrWhiteSpace(o.OwnerUid) ? o.OwnerId.ToString() : o.OwnerUid,
OwnerNickname = string.IsNullOrWhiteSpace(o.OwnerNickname) ? $"用户{o.OwnerId}" : o.OwnerNickname,
o.OwnerAvatar,
o.RunnerId,
RunnerUid = string.IsNullOrWhiteSpace(o.RunnerUid) ? o.RunnerId.ToString() : o.RunnerUid,
RunnerNickname = string.IsNullOrWhiteSpace(o.RunnerNickname) ? $"用户{o.RunnerId}" : o.RunnerNickname,
o.RunnerAvatar,
o.CreatedAt
});
return Results.Ok(result);
}).RequireAuthorization("AdminOnly");
// 管理端拉取聊天记录
app.MapGet("/api/admin/chat-messages", async (string? groupId, int? ownerUserId, int? runnerUserId, TencentIMService imService) =>
{
try
{
// 优先用群ID拉取群聊模式
if (!string.IsNullOrEmpty(groupId))
{
var result = await imService.GetGroupMessagesAsync(groupId);
return Results.Ok(result);
}
// 兼容旧数据用C2C拉取
if (ownerUserId.HasValue && runnerUserId.HasValue)
{
var fromImId = $"user_{ownerUserId}";
var toImId = $"user_{runnerUserId}";
var result = await imService.GetRoamMessagesAsync(fromImId, toImId);
return Results.Ok(result);
}
return Results.BadRequest(new { code = 400, message = "请提供 groupId 或 ownerUserId+runnerUserId" });
}
catch (Exception ex)
{
return Results.BadRequest(new { code = 400, message = $"拉取聊天记录失败: {ex.Message}" });
}
}).RequireAuthorization("AdminOnly");
}
}