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(status, true, out var s)) query = query.Where(o => o.Status == s); if (!string.IsNullOrEmpty(orderType) && Enum.TryParse(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(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(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(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, o.ImGroupId, 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.ImGroupId, 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"); // 管理端删除聊天记录(解散IM群并清除订单群ID) app.MapDelete("/api/admin/chat-list/{groupId}", async (string groupId, AppDbContext db, TencentIMService imService) => { try { // 解散 IM 群 await imService.DestroyGroupAsync(groupId); // 清除订单的群ID var order = await db.Orders.FirstOrDefaultAsync(o => o.ImGroupId == groupId); if (order != null) { order.ImGroupId = null; await db.SaveChangesAsync(); } return Results.Ok(new { message = "已删除" }); } catch (Exception ex) { return Results.BadRequest(new { code = 400, message = $"删除失败: {ex.Message}" }); } }).RequireAuthorization("AdminOnly"); } }