All checks were successful
continuous-integration/drone/push Build is passing
692 lines
29 KiB
C#
692 lines
29 KiB
C#
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,
|
||
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");
|
||
}
|
||
}
|