using System.Text; using CampusErrand.Data; using CampusErrand.Models; using CampusErrand.Models.Dtos; using CampusErrand.Services; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; var builder = WebApplication.CreateBuilder(args); // 数据库配置 var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")!; builder.Services.AddDbContext(options => options.UseSqlServer(connectionString)); // Redis 分布式缓存配置 builder.Services.AddStackExchangeRedisCache(options => { options.Configuration = builder.Configuration["Redis:Configuration"]; options.InstanceName = "CampusErrand:"; }); // JWT 认证配置 var jwtConfig = builder.Configuration.GetSection("Jwt"); var secretKey = Encoding.UTF8.GetBytes(jwtConfig["Secret"]!); builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = jwtConfig["Issuer"], ValidAudience = jwtConfig["Audience"], IssuerSigningKey = new SymmetricSecurityKey(secretKey), ClockSkew = TimeSpan.Zero }; }); // 角色授权策略 builder.Services.AddAuthorizationBuilder() .AddPolicy("AdminOnly", policy => policy.RequireRole("Admin")) .AddPolicy("UserOrAdmin", policy => policy.RequireRole("User", "Runner", "Admin")); // CORS 配置 builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => { policy.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader(); }); }); // 注册 JWT 服务 builder.Services.AddSingleton(); // 注册微信服务 builder.Services.AddHttpClient(); // OpenAPI 文档(.NET 10 内置) builder.Services.AddOpenApi(); var app = builder.Build(); // 中间件管道 if (app.Environment.IsDevelopment()) { app.MapOpenApi(); // Swagger UI 用于可视化调试 app.UseSwaggerUI(options => { options.SwaggerEndpoint("/openapi/v1.json", "校园跑腿 API v1"); }); } app.UseCors(); app.UseAuthentication(); app.UseAuthorization(); // 微信手机号登录接口 app.MapPost("/api/auth/login", async ( WeChatLoginRequest request, IWeChatService weChatService, JwtService jwtService, AppDbContext db) => { // 1. 调用微信 code2Session 获取 session_key 和 openid var sessionResult = await weChatService.Code2SessionAsync(request.Code); if (!sessionResult.Success) { return Results.BadRequest(new { code = 400, message = sessionResult.ErrorMessage }); } // 2. 使用 session_key 解密手机号 string phoneNumber; try { phoneNumber = weChatService.DecryptPhoneNumber( sessionResult.SessionKey!, request.EncryptedData, request.Iv); } catch (Exception) { return Results.BadRequest(new { code = 400, message = "手机号解密失败" }); } // 3. 根据手机号查找或创建用户 var user = await db.Users.FirstOrDefaultAsync(u => u.Phone == phoneNumber); if (user == null) { user = new User { OpenId = sessionResult.OpenId!, Phone = phoneNumber, Nickname = $"用户{phoneNumber[^4..]}", CreatedAt = DateTime.UtcNow }; db.Users.Add(user); await db.SaveChangesAsync(); } else if (string.IsNullOrEmpty(user.OpenId) && !string.IsNullOrEmpty(sessionResult.OpenId)) { // 更新 OpenId(如果之前为空) user.OpenId = sessionResult.OpenId; await db.SaveChangesAsync(); } // 4. 生成 JWT 返回 var token = jwtService.GenerateToken(user.Id, user.Role.ToString()); return Results.Ok(new LoginResponse { Token = token, UserInfo = new UserInfo { Id = user.Id, Phone = user.Phone, Nickname = user.Nickname, AvatarUrl = user.AvatarUrl, Role = user.Role.ToString() } }); }); // 受保护的测试端点(用于验证认证和授权) app.MapGet("/api/protected", () => Results.Ok("ok")) .RequireAuthorization(); app.MapGet("/api/admin/protected", () => Results.Ok("admin ok")) .RequireAuthorization("AdminOnly"); // ========== 首页概览统计接口 ========== 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"); // ========== Banner 接口 ========== // 前端获取 Banner 列表(仅启用+按排序权重排列) app.MapGet("/api/banners", async (AppDbContext db) => { var banners = await db.Banners .Where(b => b.IsEnabled) .OrderBy(b => b.SortOrder) .Select(b => new BannerResponse { Id = b.Id, ImageUrl = b.ImageUrl, LinkType = b.LinkType.ToString(), LinkUrl = b.LinkUrl, SortOrder = b.SortOrder, IsEnabled = b.IsEnabled, CreatedAt = b.CreatedAt }) .ToListAsync(); return Results.Ok(banners); }); // 管理端获取全部 Banner 列表 app.MapGet("/api/admin/banners", async (AppDbContext db) => { var banners = await db.Banners .OrderBy(b => b.SortOrder) .Select(b => new BannerResponse { Id = b.Id, ImageUrl = b.ImageUrl, LinkType = b.LinkType.ToString(), LinkUrl = b.LinkUrl, SortOrder = b.SortOrder, IsEnabled = b.IsEnabled, CreatedAt = b.CreatedAt }) .ToListAsync(); return Results.Ok(banners); }).RequireAuthorization("AdminOnly"); // 创建 Banner app.MapPost("/api/admin/banners", async (BannerRequest request, AppDbContext db) => { // 校验 var errors = ValidateBannerRequest(request); if (errors.Count > 0) { return Results.BadRequest(new { code = 400, message = "校验失败", errors }); } if (!Enum.TryParse(request.LinkType, true, out var linkType) || !Enum.IsDefined(linkType)) { return Results.BadRequest(new { code = 400, message = "校验失败", errors = new[] { new { field = "linkType", message = "链接类型不合法" } } }); } var banner = new Banner { ImageUrl = request.ImageUrl, LinkType = linkType, LinkUrl = request.LinkUrl, SortOrder = request.SortOrder, IsEnabled = request.IsEnabled, CreatedAt = DateTime.UtcNow }; db.Banners.Add(banner); await db.SaveChangesAsync(); return Results.Created($"/api/admin/banners/{banner.Id}", new BannerResponse { Id = banner.Id, ImageUrl = banner.ImageUrl, LinkType = banner.LinkType.ToString(), LinkUrl = banner.LinkUrl, SortOrder = banner.SortOrder, IsEnabled = banner.IsEnabled, CreatedAt = banner.CreatedAt }); }).RequireAuthorization("AdminOnly"); // 更新 Banner app.MapPut("/api/admin/banners/{id}", async (int id, BannerRequest request, AppDbContext db) => { var banner = await db.Banners.FindAsync(id); if (banner == null) { return Results.NotFound(new { code = 404, message = "Banner 不存在" }); } var errors = ValidateBannerRequest(request); if (errors.Count > 0) { return Results.BadRequest(new { code = 400, message = "校验失败", errors }); } if (!Enum.TryParse(request.LinkType, true, out var linkType) || !Enum.IsDefined(linkType)) { return Results.BadRequest(new { code = 400, message = "校验失败", errors = new[] { new { field = "linkType", message = "链接类型不合法" } } }); } banner.ImageUrl = request.ImageUrl; banner.LinkType = linkType; banner.LinkUrl = request.LinkUrl; banner.SortOrder = request.SortOrder; banner.IsEnabled = request.IsEnabled; await db.SaveChangesAsync(); return Results.Ok(new BannerResponse { Id = banner.Id, ImageUrl = banner.ImageUrl, LinkType = banner.LinkType.ToString(), LinkUrl = banner.LinkUrl, SortOrder = banner.SortOrder, IsEnabled = banner.IsEnabled, CreatedAt = banner.CreatedAt }); }).RequireAuthorization("AdminOnly"); // 删除 Banner app.MapDelete("/api/admin/banners/{id}", async (int id, AppDbContext db) => { var banner = await db.Banners.FindAsync(id); if (banner == null) { return Results.NotFound(new { code = 404, message = "Banner 不存在" }); } db.Banners.Remove(banner); await db.SaveChangesAsync(); return Results.NoContent(); }).RequireAuthorization("AdminOnly"); // ========== 服务入口接口 ========== // 前端获取服务入口列表(仅启用+按排序权重排列) app.MapGet("/api/service-entries", async (AppDbContext db) => { var entries = await db.ServiceEntries .Where(e => e.IsEnabled) .OrderBy(e => e.SortOrder) .Select(e => new ServiceEntryResponse { Id = e.Id, Name = e.Name, IconUrl = e.IconUrl, PagePath = e.PagePath, SortOrder = e.SortOrder, IsEnabled = e.IsEnabled }) .ToListAsync(); return Results.Ok(entries); }); // 管理端获取全部服务入口列表 app.MapGet("/api/admin/service-entries", async (AppDbContext db) => { var entries = await db.ServiceEntries .OrderBy(e => e.SortOrder) .Select(e => new ServiceEntryResponse { Id = e.Id, Name = e.Name, IconUrl = e.IconUrl, PagePath = e.PagePath, SortOrder = e.SortOrder, IsEnabled = e.IsEnabled }) .ToListAsync(); return Results.Ok(entries); }).RequireAuthorization("AdminOnly"); // 更新服务入口 app.MapPut("/api/admin/service-entries/{id}", async (int id, ServiceEntryUpdateRequest request, AppDbContext db) => { var entry = await db.ServiceEntries.FindAsync(id); if (entry == null) { return Results.NotFound(new { code = 404, message = "服务入口不存在" }); } if (string.IsNullOrWhiteSpace(request.IconUrl)) { return Results.BadRequest(new { code = 400, message = "校验失败", errors = new[] { new { field = "iconUrl", message = "图标图片地址不能为空" } } }); } entry.IconUrl = request.IconUrl; entry.SortOrder = request.SortOrder; entry.IsEnabled = request.IsEnabled; await db.SaveChangesAsync(); return Results.Ok(new ServiceEntryResponse { Id = entry.Id, Name = entry.Name, IconUrl = entry.IconUrl, PagePath = entry.PagePath, SortOrder = entry.SortOrder, IsEnabled = entry.IsEnabled }); }).RequireAuthorization("AdminOnly"); // ========== 订单接口 ========== // 创建订单 app.MapPost("/api/orders", async (CreateOrderRequest request, HttpContext httpContext, AppDbContext db) => { // 获取当前用户 ID var userIdClaim = httpContext.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier); if (userIdClaim == null) return Results.Unauthorized(); var userId = int.Parse(userIdClaim.Value); // 校验订单类型 if (!Enum.TryParse(request.OrderType, true, out var orderType) || !Enum.IsDefined(orderType)) { return Results.BadRequest(new { code = 400, message = "订单类型不合法" }); } // 佣金校验:最低 1.0 元 if (request.Commission < 1.0m) { return Results.BadRequest(new { code = 400, message = "跑腿佣金不可低于1.0元" }); } // 佣金校验:小数点后最多 1 位 if (request.Commission != Math.Round(request.Commission, 1)) { return Results.BadRequest(new { code = 400, message = "跑腿佣金最多支持小数点后1位" }); } // 计算支付总金额 decimal totalAmount; decimal packingFee = 0; if (orderType == OrderType.Help || orderType == OrderType.Purchase || orderType == OrderType.Food) { // 美食街订单:自动计算打包费并加入商品总金额 if (orderType == OrderType.Food && request.FoodItems != null && request.FoodItems.Count > 0) { // 获取涉及的门店 ID 列表 var shopIds = request.FoodItems.Select(fi => fi.ShopId).Distinct().ToList(); var shops = await db.Shops.Where(s => shopIds.Contains(s.Id)).ToListAsync(); // 按门店计算打包费 foreach (var shop in shops) { var shopItems = request.FoodItems.Where(fi => fi.ShopId == shop.Id).ToList(); var totalQuantity = shopItems.Sum(fi => fi.Quantity); packingFee += CalculatePackingFee(shop.PackingFeeType, shop.PackingFeeAmount, totalQuantity); } // 计算菜品总金额 var dishTotal = request.FoodItems.Sum(fi => fi.Quantity * fi.UnitPrice); // 商品总金额 = 菜品总金额 + 打包费 request.GoodsAmount = dishTotal + packingFee; } // 商品+佣金类:支付总金额 = 商品总金额 + 跑腿佣金 if (request.GoodsAmount == null || request.GoodsAmount < 0) { return Results.BadRequest(new { code = 400, message = "商品总金额不能为空或负数" }); } totalAmount = request.GoodsAmount.Value + request.Commission; } else { // 佣金类(代取、代送):支付总金额 = 跑腿佣金 totalAmount = request.Commission; } // 生成订单编号 var orderNo = DateTime.UtcNow.ToString("yyyyMMddHHmmss") + Random.Shared.Next(100000, 999999).ToString(); var order = new Order { OrderNo = orderNo, OwnerId = userId, OrderType = orderType, Status = OrderStatus.Pending, ItemName = request.ItemName, PickupLocation = request.PickupLocation, DeliveryLocation = request.DeliveryLocation, Remark = request.Remark, Phone = request.Phone, Commission = request.Commission, GoodsAmount = request.GoodsAmount, TotalAmount = totalAmount, CreatedAt = DateTime.UtcNow }; db.Orders.Add(order); await db.SaveChangesAsync(); // 美食街订单:保存菜品详情 if (orderType == OrderType.Food && request.FoodItems != null) { foreach (var item in request.FoodItems) { db.FoodOrderItems.Add(new FoodOrderItem { OrderId = order.Id, ShopId = item.ShopId, DishId = item.DishId, Quantity = item.Quantity, UnitPrice = item.UnitPrice }); } await db.SaveChangesAsync(); } return Results.Created($"/api/orders/{order.Id}", new OrderResponse { Id = order.Id, OrderNo = order.OrderNo, OwnerId = order.OwnerId, RunnerId = order.RunnerId, OrderType = order.OrderType.ToString(), Status = order.Status.ToString(), ItemName = order.ItemName, PickupLocation = order.PickupLocation, DeliveryLocation = order.DeliveryLocation, Remark = order.Remark, Phone = order.Phone, // 创建者自己可以看到手机号 Commission = order.Commission, GoodsAmount = order.GoodsAmount, TotalAmount = order.TotalAmount, CreatedAt = order.CreatedAt }); }).RequireAuthorization(); // 获取订单详情(含手机号隐藏逻辑和按状态显示字段) app.MapGet("/api/orders/{id}", async (int id, HttpContext httpContext, AppDbContext db) => { var userIdClaim = httpContext.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier); if (userIdClaim == null) return Results.Unauthorized(); var currentUserId = int.Parse(userIdClaim.Value); var order = await db.Orders .Include(o => o.FoodOrderItems) .Include(o => o.Runner) .FirstOrDefaultAsync(o => o.Id == id); if (order == null) { return Results.NotFound(new { code = 404, message = "订单不存在" }); } // 手机号隐藏逻辑: // - 单主自己始终可见 // - 已接单且查询者为接单跑腿时可见 // - 其他情况隐藏 string? visiblePhone = null; if (currentUserId == order.OwnerId) { visiblePhone = order.Phone; } else if (order.RunnerId != null && currentUserId == order.RunnerId) { visiblePhone = order.Phone; } // 按状态显示字段: // - 待接单:接单时间、跑腿信息、完成信息为空 // - 进行中/待确认:显示接单时间和跑腿信息 // - 已完成:显示所有信息 DateTime? visibleAcceptedAt = null; string? runnerNickname = null; int? runnerUid = null; DateTime? visibleCompletedAt = null; string? visibleCompletionProof = null; if (order.Status != OrderStatus.Pending && order.Status != OrderStatus.Cancelled) { // 进行中及之后的状态显示接单时间和跑腿信息 visibleAcceptedAt = order.AcceptedAt; runnerNickname = order.Runner?.Nickname; runnerUid = order.RunnerId; } if (order.Status == OrderStatus.Completed || order.Status == OrderStatus.WaitConfirm) { // 已完成和待确认状态显示完成时间和凭证 visibleCompletedAt = order.CompletedAt; visibleCompletionProof = order.CompletionProof; } var response = new OrderResponse { Id = order.Id, OrderNo = order.OrderNo, OwnerId = order.OwnerId, RunnerId = order.RunnerId, OrderType = order.OrderType.ToString(), Status = order.Status.ToString(), ItemName = order.ItemName, PickupLocation = order.PickupLocation, DeliveryLocation = order.DeliveryLocation, Remark = order.Remark, Phone = visiblePhone, Commission = order.Commission, GoodsAmount = order.GoodsAmount, TotalAmount = order.TotalAmount, CompletionProof = visibleCompletionProof, IsReviewed = order.IsReviewed, CreatedAt = order.CreatedAt, AcceptedAt = visibleAcceptedAt, CompletedAt = visibleCompletedAt, RunnerNickname = runnerNickname, RunnerUid = runnerUid }; // 美食街订单附带菜品详情 if (order.OrderType == OrderType.Food && order.FoodOrderItems.Count > 0) { response.FoodItems = order.FoodOrderItems.Select(fi => new FoodOrderItemResponse { Id = fi.Id, ShopId = fi.ShopId, DishId = fi.DishId, Quantity = fi.Quantity, UnitPrice = fi.UnitPrice }).ToList(); } return Results.Ok(response); }).RequireAuthorization(); // ========== 订单状态管理接口========= // 获取订单大厅列表(仅返回 Pending 状态订单) app.MapGet("/api/orders/hall", async ( string? type, string? sort, AppDbContext db) => { // 基础查询:仅返回待接单订单 var query = db.Orders.Where(o => o.Status == OrderStatus.Pending); // 按订单类型筛选 if (!string.IsNullOrEmpty(type) && Enum.TryParse(type, true, out var orderType)) { query = query.Where(o => o.OrderType == orderType); } // 排序:佣金优先(默认)或距离优先(暂不支持距离,回退到佣金) query = sort?.ToLower() == "distance" ? query.OrderByDescending(o => o.Commission) // 距离排序需地图 API,暂用佣金排序 : query.OrderByDescending(o => o.Commission); var orders = await query.ToListAsync(); // 美食街订单需要额外查询门店数和菜品数 var foodOrderIds = orders .Where(o => o.OrderType == OrderType.Food) .Select(o => o.Id) .ToList(); var foodOrderStats = new Dictionary(); if (foodOrderIds.Count > 0) { var stats = await db.FoodOrderItems .Where(fi => foodOrderIds.Contains(fi.OrderId)) .GroupBy(fi => fi.OrderId) .Select(g => new { OrderId = g.Key, ShopCount = g.Select(fi => fi.ShopId).Distinct().Count(), DishCount = g.Sum(fi => fi.Quantity) }) .ToListAsync(); foreach (var s in stats) { foodOrderStats[s.OrderId] = (s.ShopCount, s.DishCount); } } var result = orders.Select(o => { var item = new OrderHallItemResponse { Id = o.Id, OrderNo = o.OrderNo, OrderType = o.OrderType.ToString(), Status = o.Status.ToString(), ItemName = o.ItemName, PickupLocation = o.PickupLocation, DeliveryLocation = o.DeliveryLocation, Remark = o.Remark, Commission = o.Commission, GoodsAmount = o.GoodsAmount, CreatedAt = o.CreatedAt }; // 美食街订单附加门店数和菜品数 if (o.OrderType == OrderType.Food && foodOrderStats.TryGetValue(o.Id, out var stat)) { item.ShopCount = stat.ShopCount; item.DishItemCount = stat.DishCount; } return item; }).ToList(); return Results.Ok(result); }).RequireAuthorization(); // 接取订单 app.MapPost("/api/orders/{id}/accept", async (int id, 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 certification = await db.RunnerCertifications .Where(c => c.UserId == userId && c.Status == CertificationStatus.Approved) .FirstOrDefaultAsync(); if (certification == null) { return Results.BadRequest(new { code = 400, message = "您尚未通过跑腿认证,无法接单" }); } // 检查用户是否被封禁 var user = await db.Users.FindAsync(userId); if (user != null && user.IsBanned) { return Results.BadRequest(new { code = 400, message = "您的跑腿身份已被封禁,无法接单" }); } // 使用乐观锁查询并更新订单(仅 Pending 状态可接单) var order = await db.Orders.FirstOrDefaultAsync(o => o.Id == 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.Pending) { return Results.Conflict(new { code = 409, message = "该订单已被接取" }); } // 不能接自己的订单 if (order.OwnerId == userId) { return Results.BadRequest(new { code = 400, message = "不能接取自己的订单" }); } // 状态转换:Pending → InProgress order.Status = OrderStatus.InProgress; order.RunnerId = userId; order.AcceptedAt = DateTime.UtcNow; try { await db.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { return Results.Conflict(new { code = 409, message = "该订单已被接取" }); } return Results.Ok(new AcceptOrderResponse { Id = order.Id, OrderNo = order.OrderNo, Status = order.Status.ToString(), RunnerId = userId, AcceptedAt = order.AcceptedAt.Value }); }).RequireAuthorization(); // ========== 订单状态管理接口 ========== // 取消订单(仅单主可取消待接单订单) app.MapPost("/api/orders/{id}/cancel", async (int id, 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 order = await db.Orders.FirstOrDefaultAsync(o => o.Id == id); if (order == null) return Results.NotFound(new { code = 404, message = "订单不存在" }); if (order.OwnerId != userId) return Results.BadRequest(new { code = 400, message = "仅单主可取消订单" }); if (order.Status != OrderStatus.Pending) return Results.BadRequest(new { code = 400, message = "仅待接单状态的订单可取消" }); order.Status = OrderStatus.Cancelled; await db.SaveChangesAsync(); return Results.Ok(new { id = order.Id, orderNo = order.OrderNo, status = order.Status.ToString() }); }).RequireAuthorization(); // 跑腿提交完成 app.MapPost("/api/orders/{id}/complete", async (int id, CompleteOrderRequest? 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 order = await db.Orders.FirstOrDefaultAsync(o => o.Id == id); if (order == null) return Results.NotFound(new { code = 404, message = "订单不存在" }); if (order.RunnerId != userId) return Results.BadRequest(new { code = 400, message = "仅接单跑腿可提交完成" }); if (order.Status != OrderStatus.InProgress) return Results.BadRequest(new { code = 400, message = "仅进行中的订单可提交完成" }); order.Status = OrderStatus.WaitConfirm; order.CompletionProof = request?.CompletionProof; order.CompletedAt = DateTime.UtcNow; await db.SaveChangesAsync(); return Results.Ok(new { id = order.Id, orderNo = order.OrderNo, status = order.Status.ToString(), completedAt = order.CompletedAt }); }).RequireAuthorization(); // 单主确认完成 app.MapPost("/api/orders/{id}/confirm", async (int id, 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 order = await db.Orders.FirstOrDefaultAsync(o => o.Id == id); if (order == null) return Results.NotFound(new { code = 404, message = "订单不存在" }); if (order.OwnerId != userId) return Results.BadRequest(new { code = 400, message = "仅单主可确认订单完成" }); if (order.Status != OrderStatus.WaitConfirm) return Results.BadRequest(new { code = 400, message = "仅待确认状态的订单可确认完成" }); order.Status = OrderStatus.Completed; // 创建收益记录(佣金分销计算) if (order.RunnerId.HasValue) { var commissionRules = await db.CommissionRules.OrderBy(r => r.MinAmount).ToListAsync(); var platformFee = CalculatePlatformFee(order.Commission, commissionRules); var netEarning = order.Commission - platformFee; // 获取冻结时间配置(默认 1 天) var freezeDaysConfig = await db.SystemConfigs.FirstOrDefaultAsync(c => c.Key == "freeze_days"); var freezeDays = 1; if (freezeDaysConfig != null && int.TryParse(freezeDaysConfig.Value, out var configDays)) { freezeDays = configDays; } var earning = new Earning { UserId = order.RunnerId.Value, OrderId = order.Id, GoodsAmount = order.GoodsAmount, Commission = order.Commission, PlatformFee = platformFee, NetEarning = netEarning, Status = EarningStatus.Frozen, FrozenUntil = DateTime.UtcNow.AddDays(freezeDays), CreatedAt = DateTime.UtcNow }; db.Earnings.Add(earning); } await db.SaveChangesAsync(); return Results.Ok(new { id = order.Id, orderNo = order.OrderNo, status = order.Status.ToString() }); }).RequireAuthorization(); // 单主拒绝完成(订单回到进行中) app.MapPost("/api/orders/{id}/reject", async (int id, 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 order = await db.Orders.FirstOrDefaultAsync(o => o.Id == id); if (order == null) return Results.NotFound(new { code = 404, message = "订单不存在" }); if (order.OwnerId != userId) return Results.BadRequest(new { code = 400, message = "仅单主可拒绝订单完成" }); if (order.Status != OrderStatus.WaitConfirm) return Results.BadRequest(new { code = 400, message = "仅待确认状态的订单可拒绝" }); order.Status = OrderStatus.InProgress; order.CompletedAt = null; order.CompletionProof = null; await db.SaveChangesAsync(); return Results.Ok(new { id = order.Id, orderNo = order.OrderNo, status = order.Status.ToString() }); }).RequireAuthorization(); // 我的订单(单主查看自己发布的订单) app.MapGet("/api/orders/mine", async (string? status, string? type, 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 query = db.Orders.Where(o => o.OwnerId == userId); // 按状态筛选 if (!string.IsNullOrEmpty(status) && Enum.TryParse(status, true, out var orderStatus)) query = query.Where(o => o.Status == orderStatus); // 按类型筛选 if (!string.IsNullOrEmpty(type) && Enum.TryParse(type, true, out var orderType)) query = query.Where(o => o.OrderType == orderType); var orders = await query .OrderByDescending(o => o.CreatedAt) .Select(o => new OrderListItemResponse { Id = o.Id, OrderNo = o.OrderNo, OwnerId = o.OwnerId, RunnerId = o.RunnerId, OrderType = o.OrderType.ToString(), Status = o.Status.ToString(), ItemName = o.ItemName, PickupLocation = o.PickupLocation, DeliveryLocation = o.DeliveryLocation, Remark = o.Remark, Commission = o.Commission, GoodsAmount = o.GoodsAmount, TotalAmount = o.TotalAmount, IsReviewed = o.IsReviewed, CreatedAt = o.CreatedAt, AcceptedAt = o.AcceptedAt, CompletedAt = o.CompletedAt }) .ToListAsync(); return Results.Ok(orders); }).RequireAuthorization(); // 我的接单(跑腿查看自己接取的订单) app.MapGet("/api/orders/taken", async (string? status, string? type, 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 query = db.Orders.Where(o => o.RunnerId == userId); // 按状态筛选 if (!string.IsNullOrEmpty(status) && Enum.TryParse(status, true, out var orderStatus)) query = query.Where(o => o.Status == orderStatus); // 按类型筛选 if (!string.IsNullOrEmpty(type) && Enum.TryParse(type, true, out var orderType)) query = query.Where(o => o.OrderType == orderType); var orders = await query .OrderByDescending(o => o.CreatedAt) .Select(o => new OrderListItemResponse { Id = o.Id, OrderNo = o.OrderNo, OwnerId = o.OwnerId, RunnerId = o.RunnerId, OrderType = o.OrderType.ToString(), Status = o.Status.ToString(), ItemName = o.ItemName, PickupLocation = o.PickupLocation, DeliveryLocation = o.DeliveryLocation, Remark = o.Remark, Commission = o.Commission, GoodsAmount = o.GoodsAmount, TotalAmount = o.TotalAmount, IsReviewed = o.IsReviewed, CreatedAt = o.CreatedAt, AcceptedAt = o.AcceptedAt, CompletedAt = o.CompletedAt }) .ToListAsync(); return Results.Ok(orders); }).RequireAuthorization(); // ========== 美食街门店接口 ========== // 前端获取门店列表(仅启用门店,含菜品种类数量) app.MapGet("/api/shops", async (AppDbContext db) => { var shops = await db.Shops .Where(s => s.IsEnabled) .Select(s => new ShopListResponse { Id = s.Id, Name = s.Name, Photo = s.Photo, Location = s.Location, DishCount = s.Dishes.Count(d => d.IsEnabled) }) .ToListAsync(); return Results.Ok(shops); }); // 前端获取门店详情(含 Banner 和菜品) app.MapGet("/api/shops/{id}", async (int id, AppDbContext db) => { var shop = await db.Shops .Include(s => s.ShopBanners) .Include(s => s.Dishes) .FirstOrDefaultAsync(s => s.Id == id); if (shop == null) { return Results.NotFound(new { code = 404, message = "门店不存在" }); } var response = new ShopDetailResponse { Id = shop.Id, Name = shop.Name, Photo = shop.Photo, Location = shop.Location, Notice = shop.Notice, PackingFeeType = shop.PackingFeeType.ToString(), PackingFeeAmount = shop.PackingFeeAmount, IsEnabled = shop.IsEnabled, Banners = shop.ShopBanners .OrderBy(b => b.SortOrder) .Select(b => new ShopBannerResponse { Id = b.Id, ImageUrl = b.ImageUrl, SortOrder = b.SortOrder }).ToList(), Dishes = shop.Dishes .Where(d => d.IsEnabled) .Select(d => new DishResponse { Id = d.Id, ShopId = d.ShopId, Name = d.Name, Photo = d.Photo, Price = d.Price, IsEnabled = d.IsEnabled }).ToList() }; return Results.Ok(response); }); // ========== 管理端门店接口 ========== // 管理端获取门店列表(全部) app.MapGet("/api/admin/shops", async (AppDbContext db) => { var shops = await db.Shops .Select(s => new AdminShopResponse { Id = s.Id, Name = s.Name, Photo = s.Photo, Location = s.Location, Notice = s.Notice, PackingFeeType = s.PackingFeeType.ToString(), PackingFeeAmount = s.PackingFeeAmount, IsEnabled = s.IsEnabled, DishCount = s.Dishes.Count }) .ToListAsync(); return Results.Ok(shops); }).RequireAuthorization("AdminOnly"); // 管理端创建门店 app.MapPost("/api/admin/shops", async (ShopRequest request, AppDbContext db) => { var errors = ValidateShopRequest(request); if (errors.Count > 0) { return Results.BadRequest(new { code = 400, message = "校验失败", errors }); } if (!Enum.TryParse(request.PackingFeeType, true, out var feeType) || !Enum.IsDefined(feeType)) { return Results.BadRequest(new { code = 400, message = "校验失败", errors = new[] { new { field = "packingFeeType", message = "打包费类型不合法" } } }); } var shop = new Shop { Name = request.Name, Photo = request.Photo, Location = request.Location, Notice = request.Notice, PackingFeeType = feeType, PackingFeeAmount = request.PackingFeeAmount, IsEnabled = request.IsEnabled }; db.Shops.Add(shop); await db.SaveChangesAsync(); return Results.Created($"/api/admin/shops/{shop.Id}", new AdminShopResponse { Id = shop.Id, Name = shop.Name, Photo = shop.Photo, Location = shop.Location, Notice = shop.Notice, PackingFeeType = shop.PackingFeeType.ToString(), PackingFeeAmount = shop.PackingFeeAmount, IsEnabled = shop.IsEnabled, DishCount = 0 }); }).RequireAuthorization("AdminOnly"); // 管理端更新门店 app.MapPut("/api/admin/shops/{id}", async (int id, ShopRequest request, AppDbContext db) => { var shop = await db.Shops.FindAsync(id); if (shop == null) { return Results.NotFound(new { code = 404, message = "门店不存在" }); } var errors = ValidateShopRequest(request); if (errors.Count > 0) { return Results.BadRequest(new { code = 400, message = "校验失败", errors }); } if (!Enum.TryParse(request.PackingFeeType, true, out var feeType) || !Enum.IsDefined(feeType)) { return Results.BadRequest(new { code = 400, message = "校验失败", errors = new[] { new { field = "packingFeeType", message = "打包费类型不合法" } } }); } shop.Name = request.Name; shop.Photo = request.Photo; shop.Location = request.Location; shop.Notice = request.Notice; shop.PackingFeeType = feeType; shop.PackingFeeAmount = request.PackingFeeAmount; shop.IsEnabled = request.IsEnabled; await db.SaveChangesAsync(); return Results.Ok(new AdminShopResponse { Id = shop.Id, Name = shop.Name, Photo = shop.Photo, Location = shop.Location, Notice = shop.Notice, PackingFeeType = shop.PackingFeeType.ToString(), PackingFeeAmount = shop.PackingFeeAmount, IsEnabled = shop.IsEnabled, DishCount = await db.Dishes.CountAsync(d => d.ShopId == shop.Id) }); }).RequireAuthorization("AdminOnly"); // 管理端删除门店 app.MapDelete("/api/admin/shops/{id}", async (int id, AppDbContext db) => { var shop = await db.Shops .Include(s => s.Dishes) .Include(s => s.ShopBanners) .FirstOrDefaultAsync(s => s.Id == id); if (shop == null) { return Results.NotFound(new { code = 404, message = "门店不存在" }); } // 级联删除菜品和门店 Banner db.Dishes.RemoveRange(shop.Dishes); db.ShopBanners.RemoveRange(shop.ShopBanners); db.Shops.Remove(shop); await db.SaveChangesAsync(); return Results.NoContent(); }).RequireAuthorization("AdminOnly"); // ========== 管理端门店 Banner 接口 ========== // 获取门店 Banner 列表 app.MapGet("/api/admin/shops/{shopId}/banners", async (int shopId, AppDbContext db) => { var shop = await db.Shops.FindAsync(shopId); if (shop == null) return Results.NotFound(new { code = 404, message = "门店不存在" }); var banners = await db.ShopBanners .Where(b => b.ShopId == shopId) .OrderBy(b => b.SortOrder) .Select(b => new ShopBannerResponse { Id = b.Id, ImageUrl = b.ImageUrl, SortOrder = b.SortOrder }) .ToListAsync(); return Results.Ok(banners); }).RequireAuthorization("AdminOnly"); // 创建门店 Banner app.MapPost("/api/admin/shops/{shopId}/banners", async (int shopId, ShopBannerRequest request, AppDbContext db) => { var shop = await db.Shops.FindAsync(shopId); if (shop == null) return Results.NotFound(new { code = 404, message = "门店不存在" }); if (string.IsNullOrWhiteSpace(request.ImageUrl)) return Results.BadRequest(new { code = 400, message = "校验失败", errors = new[] { new { field = "imageUrl", message = "图片地址不能为空" } } }); var banner = new ShopBanner { ShopId = shopId, ImageUrl = request.ImageUrl, SortOrder = request.SortOrder }; db.ShopBanners.Add(banner); await db.SaveChangesAsync(); return Results.Created($"/api/admin/shops/{shopId}/banners/{banner.Id}", new ShopBannerResponse { Id = banner.Id, ImageUrl = banner.ImageUrl, SortOrder = banner.SortOrder }); }).RequireAuthorization("AdminOnly"); // 删除门店 Banner app.MapDelete("/api/admin/shops/{shopId}/banners/{bannerId}", async (int shopId, int bannerId, AppDbContext db) => { var banner = await db.ShopBanners.FirstOrDefaultAsync(b => b.Id == bannerId && b.ShopId == shopId); if (banner == null) return Results.NotFound(new { code = 404, message = "门店 Banner 不存在" }); db.ShopBanners.Remove(banner); await db.SaveChangesAsync(); return Results.NoContent(); }).RequireAuthorization("AdminOnly"); // ========== 管理端菜品接口 ========== // 获取门店菜品列表 app.MapGet("/api/admin/shops/{shopId}/dishes", async (int shopId, AppDbContext db) => { var shop = await db.Shops.FindAsync(shopId); if (shop == null) { return Results.NotFound(new { code = 404, message = "门店不存在" }); } var dishes = await db.Dishes .Where(d => d.ShopId == shopId) .Select(d => new DishResponse { Id = d.Id, ShopId = d.ShopId, Name = d.Name, Photo = d.Photo, Price = d.Price, IsEnabled = d.IsEnabled }) .ToListAsync(); return Results.Ok(dishes); }).RequireAuthorization("AdminOnly"); // 创建菜品 app.MapPost("/api/admin/shops/{shopId}/dishes", async (int shopId, DishRequest request, AppDbContext db) => { var shop = await db.Shops.FindAsync(shopId); if (shop == null) { return Results.NotFound(new { code = 404, message = "门店不存在" }); } var errors = ValidateDishRequest(request); if (errors.Count > 0) { return Results.BadRequest(new { code = 400, message = "校验失败", errors }); } var dish = new Dish { ShopId = shopId, Name = request.Name, Photo = request.Photo, Price = request.Price, IsEnabled = request.IsEnabled }; db.Dishes.Add(dish); await db.SaveChangesAsync(); return Results.Created($"/api/admin/shops/{shopId}/dishes/{dish.Id}", new DishResponse { Id = dish.Id, ShopId = dish.ShopId, Name = dish.Name, Photo = dish.Photo, Price = dish.Price, IsEnabled = dish.IsEnabled }); }).RequireAuthorization("AdminOnly"); // 更新菜品 app.MapPut("/api/admin/shops/{shopId}/dishes/{dishId}", async (int shopId, int dishId, DishRequest request, AppDbContext db) => { var dish = await db.Dishes.FirstOrDefaultAsync(d => d.Id == dishId && d.ShopId == shopId); if (dish == null) { return Results.NotFound(new { code = 404, message = "菜品不存在" }); } var errors = ValidateDishRequest(request); if (errors.Count > 0) { return Results.BadRequest(new { code = 400, message = "校验失败", errors }); } dish.Name = request.Name; dish.Photo = request.Photo; dish.Price = request.Price; dish.IsEnabled = request.IsEnabled; await db.SaveChangesAsync(); return Results.Ok(new DishResponse { Id = dish.Id, ShopId = dish.ShopId, Name = dish.Name, Photo = dish.Photo, Price = dish.Price, IsEnabled = dish.IsEnabled }); }).RequireAuthorization("AdminOnly"); // 删除菜品 app.MapDelete("/api/admin/shops/{shopId}/dishes/{dishId}", async (int shopId, int dishId, AppDbContext db) => { var dish = await db.Dishes.FirstOrDefaultAsync(d => d.Id == dishId && d.ShopId == shopId); if (dish == null) { return Results.NotFound(new { code = 404, message = "菜品不存在" }); } db.Dishes.Remove(dish); await db.SaveChangesAsync(); return Results.NoContent(); }).RequireAuthorization("AdminOnly"); // ========== 跑腿认证接口 ========== // 提交跑腿认证申请 app.MapPost("/api/runner/certification", async (CertificationRequest 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 errors = new List(); if (string.IsNullOrWhiteSpace(request.RealName)) errors.Add(new { field = "realName", message = "姓名不能为空" }); if (string.IsNullOrWhiteSpace(request.Phone)) errors.Add(new { field = "phone", message = "手机号不能为空" }); if (errors.Count > 0) return Results.BadRequest(new { code = 400, message = "校验失败", errors }); // 检查是否已有待审核或已通过的认证 var existing = await db.RunnerCertifications .Where(c => c.UserId == userId && (c.Status == CertificationStatus.Pending || c.Status == CertificationStatus.Approved)) .FirstOrDefaultAsync(); if (existing != null) { if (existing.Status == CertificationStatus.Approved) return Results.BadRequest(new { code = 400, message = "您已通过跑腿认证" }); return Results.BadRequest(new { code = 400, message = "平台审核中" }); } var certification = new RunnerCertification { UserId = userId, RealName = request.RealName, Phone = request.Phone, Status = CertificationStatus.Pending, CreatedAt = DateTime.UtcNow }; db.RunnerCertifications.Add(certification); await db.SaveChangesAsync(); return Results.Created($"/api/runner/certification", 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(); // 查询跑腿认证状态 app.MapGet("/api/runner/certification", 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 certification = await db.RunnerCertifications .Where(c => c.UserId == userId) .OrderByDescending(c => c.CreatedAt) .FirstOrDefaultAsync(); if (certification == null) { return Results.Ok(new { status = "None", message = "未提交认证" }); } 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(); // 管理端获取认证列表 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 AdminCertificationResponse { Id = c.Id, UserId = c.UserId, RealName = c.RealName, Phone = c.Phone, Status = c.Status.ToString(), CreatedAt = c.CreatedAt, ReviewedAt = c.ReviewedAt, UserNickname = c.User != null ? c.User.Nickname : null, UserPhone = c.User != null ? c.User.Phone : null }) .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.MapPost("/api/orders/{id}/review", async (int id, SubmitReviewRequest 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 order = await db.Orders.FirstOrDefaultAsync(o => o.Id == id); if (order == null) return Results.NotFound(new { code = 404, message = "订单不存在" }); // 仅单主可评价 if (order.OwnerId != userId) return Results.BadRequest(new { code = 400, message = "仅单主可评价跑腿" }); // 仅已完成订单可评价 if (order.Status != OrderStatus.Completed) return Results.BadRequest(new { code = 400, message = "仅已完成的订单可评价" }); // 不可重复评价 if (order.IsReviewed) return Results.BadRequest(new { code = 400, message = "该订单已评价" }); // 校验评分范围 if (request.Rating < 1 || request.Rating > 5) return Results.BadRequest(new { code = 400, message = "评分必须在 1-5 之间" }); // 评分计算:1星=-2, 2星=-1, 3星=0, 4星=+1, 5星=+2 var scoreChange = request.Rating - 3; var review = new Review { OrderId = id, RunnerId = order.RunnerId!.Value, Rating = request.Rating, Content = request.Content, ScoreChange = scoreChange, CreatedAt = DateTime.UtcNow }; db.Reviews.Add(review); // 更新跑腿评分(限制在 0-100 之间) var runner = await db.Users.FindAsync(order.RunnerId!.Value); if (runner != null) { runner.RunnerScore = Math.Clamp(runner.RunnerScore + scoreChange, 0, 100); } // 标记订单已评价 order.IsReviewed = true; await db.SaveChangesAsync(); return Results.Ok(new ReviewResponse { Id = review.Id, OrderId = review.OrderId, RunnerId = review.RunnerId, Rating = review.Rating, ScoreChange = review.ScoreChange, IsDisabled = review.IsDisabled, CreatedAt = review.CreatedAt // 注意:不返回 Content,评价内容仅管理员可见 }); }).RequireAuthorization(); // 管理端获取评价列表 app.MapGet("/api/admin/reviews", async (int? runnerId, AppDbContext db) => { var query = db.Reviews .Include(r => r.Order) .Include(r => r.Runner) .AsQueryable(); if (runnerId.HasValue) query = query.Where(r => r.RunnerId == runnerId.Value); var reviews = await query .OrderByDescending(r => r.CreatedAt) .Select(r => new AdminReviewResponse { Id = r.Id, OrderId = r.OrderId, OrderNo = r.Order != null ? r.Order.OrderNo : "", RunnerId = r.RunnerId, RunnerNickname = r.Runner != null ? r.Runner.Nickname : null, Rating = r.Rating, Content = r.Content, ScoreChange = r.ScoreChange, IsDisabled = r.IsDisabled, CreatedAt = 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.MapPost("/api/orders/{id}/price-change", async (int id, PriceChangeRequest 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 order = await db.Orders.FirstOrDefaultAsync(o => o.Id == 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 = "当前订单状态不支持改价" }); // 仅单主或跑腿可发起改价 if (userId != order.OwnerId && userId != order.RunnerId) return Results.BadRequest(new { code = 400, message = "仅订单相关方可发起改价" }); // 校验改价类型 if (!Enum.TryParse(request.ChangeType, true, out var changeType) || !Enum.IsDefined(changeType)) return Results.BadRequest(new { code = 400, message = "改价类型不合法" }); // 代购和美食街订单才支持修改商品总额 if (changeType == PriceChangeType.GoodsAmount && order.OrderType != OrderType.Purchase && order.OrderType != OrderType.Food && order.OrderType != OrderType.Help) return Results.BadRequest(new { code = 400, message = "该订单类型不支持修改商品总额" }); // 检查是否有待处理的改价申请 var pendingChange = await db.PriceChanges .AnyAsync(pc => pc.OrderId == id && pc.Status == PriceChangeStatus.Pending); if (pendingChange) return Results.BadRequest(new { code = 400, message = "已有待处理的改价申请,请等待对方确认" }); // 获取原价 var originalPrice = changeType == PriceChangeType.Commission ? order.Commission : order.GoodsAmount ?? 0; if (request.NewPrice < 0) return Results.BadRequest(new { code = 400, message = "新价格不能为负数" }); var priceChange = new PriceChange { OrderId = id, InitiatorId = userId, ChangeType = changeType, OriginalPrice = originalPrice, NewPrice = request.NewPrice, Status = PriceChangeStatus.Pending, CreatedAt = DateTime.UtcNow }; db.PriceChanges.Add(priceChange); await db.SaveChangesAsync(); // 计算差额 var difference = request.NewPrice - originalPrice; return Results.Created($"/api/orders/{id}/price-change/{priceChange.Id}", new PriceChangeResponse { Id = priceChange.Id, OrderId = priceChange.OrderId, InitiatorId = priceChange.InitiatorId, ChangeType = priceChange.ChangeType.ToString(), OriginalPrice = priceChange.OriginalPrice, NewPrice = priceChange.NewPrice, Difference = difference, Status = priceChange.Status.ToString(), CreatedAt = priceChange.CreatedAt }); }).RequireAuthorization(); // 响应改价(同意或拒绝) app.MapPut("/api/orders/{id}/price-change/{changeId}", async (int id, int changeId, RespondPriceChangeRequest 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 priceChange = await db.PriceChanges .Include(pc => pc.Order) .FirstOrDefaultAsync(pc => pc.Id == changeId && pc.OrderId == id); if (priceChange == null) return Results.NotFound(new { code = 404, message = "改价记录不存在" }); if (priceChange.Status != PriceChangeStatus.Pending) return Results.BadRequest(new { code = 400, message = "该改价申请已处理" }); // 不能自己响应自己的改价申请 if (priceChange.InitiatorId == userId) return Results.BadRequest(new { code = 400, message = "不能响应自己的改价申请" }); // 仅订单相关方可响应 var order = priceChange.Order!; if (userId != order.OwnerId && userId != order.RunnerId) return Results.BadRequest(new { code = 400, message = "仅订单相关方可响应改价" }); if (!Enum.TryParse(request.Action, true, out var action) || (action != PriceChangeStatus.Accepted && action != PriceChangeStatus.Rejected)) return Results.BadRequest(new { code = 400, message = "操作不合法,仅支持 Accepted 或 Rejected" }); priceChange.Status = action; // 同意改价时更新订单金额 if (action == PriceChangeStatus.Accepted) { if (priceChange.ChangeType == PriceChangeType.Commission) { order.Commission = priceChange.NewPrice; } else { order.GoodsAmount = priceChange.NewPrice; } // 重新计算支付总金额 if (order.OrderType == OrderType.Help || order.OrderType == OrderType.Purchase || order.OrderType == OrderType.Food) { order.TotalAmount = (order.GoodsAmount ?? 0) + order.Commission; } else { order.TotalAmount = order.Commission; } } await db.SaveChangesAsync(); var difference = priceChange.NewPrice - priceChange.OriginalPrice; return Results.Ok(new PriceChangeResponse { Id = priceChange.Id, OrderId = priceChange.OrderId, InitiatorId = priceChange.InitiatorId, ChangeType = priceChange.ChangeType.ToString(), OriginalPrice = priceChange.OriginalPrice, NewPrice = priceChange.NewPrice, Difference = difference, Status = priceChange.Status.ToString(), CreatedAt = priceChange.CreatedAt }); }).RequireAuthorization(); // ========== 收益与提现接口 ========== // 获取收益概览(四种金额状态) 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 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(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 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/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"); // ========== 消息通知接口 ========== // 获取未读消息数 app.MapGet("/api/messages/unread-count", 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 user = await db.Users.FindAsync(userId); var visibleSystemMessages = await GetVisibleSystemMessages(db, userId, user); var visibleSystemMessageIds = visibleSystemMessages.Select(m => m.Id).ToHashSet(); // 已读的系统消息 ID var readSystemIds = await db.MessageReads .Where(r => r.UserId == userId && r.MessageType == MessageType.System) .Select(r => r.MessageId) .ToListAsync(); var systemUnread = visibleSystemMessageIds.Count(id => !readSystemIds.Contains(id)); // 订单通知:用户相关的订单中状态变更过的订单数(简化实现:统计用户相关的已接单/已完成/已取消订单) var orderNotifications = await db.Orders .Where(o => (o.OwnerId == userId || o.RunnerId == userId) && o.Status != OrderStatus.Pending) .Select(o => o.Id) .ToListAsync(); var readOrderNotificationIds = await db.MessageReads .Where(r => r.UserId == userId && r.MessageType == MessageType.OrderNotification) .Select(r => r.MessageId) .ToListAsync(); var orderNotificationUnread = orderNotifications.Count(id => !readOrderNotificationIds.Contains(id)); return Results.Ok(new UnreadCountResponse { SystemUnread = systemUnread, OrderNotificationUnread = orderNotificationUnread, TotalUnread = systemUnread + orderNotificationUnread }); }).RequireAuthorization(); // 获取系统消息列表 app.MapGet("/api/messages/system", 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 user = await db.Users.FindAsync(userId); var messages = await GetVisibleSystemMessages(db, userId, user); // 获取已读记录 var readIds = await db.MessageReads .Where(r => r.UserId == userId && r.MessageType == MessageType.System) .Select(r => r.MessageId) .ToListAsync(); var result = messages .OrderByDescending(m => m.CreatedAt) .Select(m => new SystemMessageResponse { Id = m.Id, Title = m.Title, ContentPreview = m.Content.Length > 100 ? m.Content[..100] + "…" : m.Content, ThumbnailUrl = m.ThumbnailUrl, CreatedAt = m.CreatedAt, IsRead = readIds.Contains(m.Id) }) .ToList(); // 标记所有为已读 foreach (var msg in messages.Where(m => !readIds.Contains(m.Id))) { db.MessageReads.Add(new MessageRead { UserId = userId, MessageType = MessageType.System, MessageId = msg.Id, ReadAt = DateTime.UtcNow }); } await db.SaveChangesAsync(); return Results.Ok(result); }).RequireAuthorization(); // 获取系统消息详情 app.MapGet("/api/messages/system/{id}", async (int id, HttpContext httpContext, AppDbContext db) => { var userIdClaim = httpContext.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier); if (userIdClaim == null) return Results.Unauthorized(); var message = await db.SystemMessages.FindAsync(id); if (message == null) return Results.NotFound(new { code = 404, message = "消息不存在" }); return Results.Ok(new SystemMessageDetailResponse { Id = message.Id, Title = message.Title, Content = message.Content, ThumbnailUrl = message.ThumbnailUrl, CreatedAt = message.CreatedAt }); }).RequireAuthorization(); // 获取订单通知列表 app.MapGet("/api/messages/order-notifications", async (string? category, 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 query = db.Orders .Where(o => (o.OwnerId == userId || o.RunnerId == userId) && o.Status != OrderStatus.Pending); // 按分类筛选 if (!string.IsNullOrEmpty(category)) { query = category.ToLower() switch { "accepted" => query.Where(o => o.Status == OrderStatus.InProgress), "completed" => query.Where(o => o.Status == OrderStatus.Completed), "cancelled" => query.Where(o => o.Status == OrderStatus.Cancelled), _ => query }; } var orders = await query .OrderByDescending(o => o.AcceptedAt ?? o.CreatedAt) .Select(o => new OrderNotificationResponse { Id = o.Id, OrderNo = o.OrderNo, OrderType = o.OrderType.ToString(), Title = o.Status == OrderStatus.InProgress ? "订单已被接取" : o.Status == OrderStatus.Completed ? "订单已完成" : o.Status == OrderStatus.Cancelled ? "订单已取消" : o.Status == OrderStatus.WaitConfirm ? "订单待确认" : o.Status == OrderStatus.Appealing ? "订单申诉中" : "订单状态变更", ItemName = o.ItemName, Status = o.Status.ToString(), CreatedAt = o.AcceptedAt ?? o.CreatedAt }) .ToListAsync(); // 标记订单通知为已读 var orderIds = orders.Select(o => o.Id).ToList(); var readIds = await db.MessageReads .Where(r => r.UserId == userId && r.MessageType == MessageType.OrderNotification && orderIds.Contains(r.MessageId)) .Select(r => r.MessageId) .ToListAsync(); foreach (var orderId in orderIds.Where(id => !readIds.Contains(id))) { db.MessageReads.Add(new MessageRead { UserId = userId, MessageType = MessageType.OrderNotification, MessageId = orderId, ReadAt = DateTime.UtcNow }); } await db.SaveChangesAsync(); return Results.Ok(orders); }).RequireAuthorization(); // 管理端发布系统通知 app.MapPost("/api/admin/notifications", async (CreateNotificationRequest request, AppDbContext db) => { // 校验 var errors = new List(); if (string.IsNullOrWhiteSpace(request.Title)) errors.Add(new { field = "title", message = "标题不能为空" }); if (string.IsNullOrWhiteSpace(request.Content)) errors.Add(new { field = "content", message = "正文不能为空" }); if (errors.Count > 0) return Results.BadRequest(new { code = 400, message = "校验失败", errors }); if (!Enum.TryParse(request.TargetType, true, out var targetType) || !Enum.IsDefined(targetType)) return Results.BadRequest(new { code = 400, message = "目标类型不合法" }); var message = new SystemMessage { Title = request.Title, Content = request.Content, ThumbnailUrl = request.ThumbnailUrl, TargetType = targetType, TargetUserIds = request.TargetUserIds != null ? System.Text.Json.JsonSerializer.Serialize(request.TargetUserIds) : null, CreatedAt = DateTime.UtcNow }; db.SystemMessages.Add(message); await db.SaveChangesAsync(); return Results.Created($"/api/admin/notifications/{message.Id}", new SystemMessageDetailResponse { Id = message.Id, Title = message.Title, Content = message.Content, ThumbnailUrl = message.ThumbnailUrl, CreatedAt = message.CreatedAt }); }).RequireAuthorization("AdminOnly"); // ========== 跑腿管理接口 ========== // 管理端获取跑腿列表 app.MapGet("/api/admin/runners", async (AppDbContext db) => { var runners = await db.Users .Where(u => db.RunnerCertifications.Any(c => c.UserId == u.Id && c.Status == CertificationStatus.Approved)) .Select(u => new { u.Id, u.Nickname, u.Phone, u.RunnerScore, u.IsBanned, u.CreatedAt }) .ToListAsync(); return Results.Ok(runners); }).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/orders", async (string? status, string? orderType, AppDbContext db) => { var query = db.Orders.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, o.RunnerId, 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.MapGet("/api/orders/{id}/appeals", async (int id, AppDbContext db) => { var appeals = await db.Appeals .Where(a => a.OrderId == id) .OrderByDescending(a => a.CreatedAt) .Select(a => new { a.Id, a.OrderId, a.Result, a.CreatedAt }) .ToListAsync(); return Results.Ok(appeals); }).RequireAuthorization(); // ========== 图片上传接口 ========== app.MapPost("/api/upload/image", async (IFormFile file, IConfiguration config) => { if (file == null || file.Length == 0) return Results.BadRequest(new { code = 400, message = "请选择要上传的图片" }); // 文件大小校验(默认 5MB) var maxSize = config.GetValue("Upload:MaxFileSizeBytes", 5242880); if (file.Length > maxSize) return Results.BadRequest(new { code = 400, message = $"图片大小不能超过 {maxSize / 1024 / 1024}MB" }); // 文件扩展名校验 var allowedExtensions = config.GetSection("Upload:AllowedExtensions").Get() ?? new[] { ".jpg", ".jpeg", ".png", ".gif", ".webp" }; var ext = Path.GetExtension(file.FileName).ToLowerInvariant(); if (!allowedExtensions.Contains(ext)) return Results.BadRequest(new { code = 400, message = $"不支持的图片格式,仅支持 {string.Join(", ", allowedExtensions)}" }); // 保存文件 var uploadDir = config.GetValue("Upload:Directory") ?? "uploads"; var fullDir = Path.Combine(Directory.GetCurrentDirectory(), uploadDir); Directory.CreateDirectory(fullDir); var fileName = $"{Guid.NewGuid()}{ext}"; var filePath = Path.Combine(fullDir, fileName); using (var stream = new FileStream(filePath, FileMode.Create)) { await file.CopyToAsync(stream); } var url = $"/{uploadDir}/{fileName}"; return Results.Ok(new UploadImageResponse { Url = url }); }).RequireAuthorization() .DisableAntiforgery(); // 静态文件服务(上传的图片) var uploadPath = Path.Combine(Directory.GetCurrentDirectory(), builder.Configuration.GetValue("Upload:Directory") ?? "uploads"); Directory.CreateDirectory(uploadPath); app.UseStaticFiles(new StaticFileOptions { FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(uploadPath), RequestPath = "/" + (builder.Configuration.GetValue("Upload:Directory") ?? "uploads") }); // ========== 系统配置接口 ========== // 获取客服二维码 app.MapGet("/api/config/qrcode", async (AppDbContext db) => { var config = await db.SystemConfigs.FirstOrDefaultAsync(c => c.Key == "qrcode"); return Results.Ok(new ConfigResponse { Key = "qrcode", Value = config?.Value ?? "", UpdatedAt = config?.UpdatedAt ?? DateTime.MinValue }); }); // 获取用户协议 app.MapGet("/api/config/agreement", async (AppDbContext db) => { var config = await db.SystemConfigs.FirstOrDefaultAsync(c => c.Key == "agreement"); return Results.Ok(new ConfigResponse { Key = "agreement", Value = config?.Value ?? "", UpdatedAt = config?.UpdatedAt ?? DateTime.MinValue }); }); // 获取隐私政策 app.MapGet("/api/config/privacy", async (AppDbContext db) => { var config = await db.SystemConfigs.FirstOrDefaultAsync(c => c.Key == "privacy"); return Results.Ok(new ConfigResponse { Key = "privacy", Value = config?.Value ?? "", UpdatedAt = config?.UpdatedAt ?? DateTime.MinValue }); }); // 获取提现说明 app.MapGet("/api/config/withdrawal-guide", async (AppDbContext db) => { var config = await db.SystemConfigs.FirstOrDefaultAsync(c => c.Key == "withdrawal_guide"); return Results.Ok(new ConfigResponse { Key = "withdrawal_guide", Value = config?.Value ?? "", UpdatedAt = config?.UpdatedAt ?? DateTime.MinValue }); }); // 管理端获取指定配置 app.MapGet("/api/admin/config/{key}", async (string key, AppDbContext db) => { var config = await db.SystemConfigs.FirstOrDefaultAsync(c => c.Key == key); return Results.Ok(new ConfigResponse { Key = key, Value = config?.Value ?? "", UpdatedAt = config?.UpdatedAt ?? DateTime.MinValue }); }).RequireAuthorization("AdminOnly"); // 管理端更新系统配置 app.MapPut("/api/admin/config/{key}", async (string key, UpdateConfigRequest request, AppDbContext db) => { // 允许的配置键白名单 var allowedKeys = new HashSet { "qrcode", "agreement", "privacy", "withdrawal_guide", "freeze_days" }; if (!allowedKeys.Contains(key)) return Results.BadRequest(new { code = 400, message = $"不支持的配置键: {key}" }); var config = await db.SystemConfigs.FirstOrDefaultAsync(c => c.Key == key); if (config == null) { config = new SystemConfig { Key = key, Value = request.Value, UpdatedAt = DateTime.UtcNow }; db.SystemConfigs.Add(config); } else { config.Value = request.Value; config.UpdatedAt = DateTime.UtcNow; } await db.SaveChangesAsync(); return Results.Ok(new ConfigResponse { Key = config.Key, Value = config.Value, UpdatedAt = config.UpdatedAt }); }).RequireAuthorization("AdminOnly"); // 管理员账号密码登录接口 app.MapPost("/api/admin/auth/login", async ( AdminLoginRequest request, JwtService jwtService, AppDbContext db, IConfiguration configuration) => { // 从配置读取管理员凭据 var adminUsername = configuration["Admin:Username"] ?? "admin"; var adminPassword = configuration["Admin:Password"] ?? "admin123"; if (request.Username != adminUsername || request.Password != adminPassword) { return Results.BadRequest(new { code = 400, message = "账号或密码错误" }); } // 查找或创建管理员用户 var admin = await db.Users.FirstOrDefaultAsync(u => u.Role == UserRole.Admin); if (admin == null) { admin = new User { OpenId = "admin", Phone = "admin", Nickname = "管理员", Role = UserRole.Admin, CreatedAt = DateTime.UtcNow }; db.Users.Add(admin); await db.SaveChangesAsync(); } var token = jwtService.GenerateToken(admin.Id, admin.Role.ToString()); return Results.Ok(new { token }); }); // 自动执行数据库迁移(仅关系型数据库,跳过 InMemory 测试环境) using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); if (db.Database.IsRelational() && !(db.Database.ProviderName?.Contains("InMemory") ?? false)) { db.Database.Migrate(); } } app.Run(); // Banner 请求校验辅助方法 static List ValidateBannerRequest(BannerRequest request) { var errors = new List(); if (string.IsNullOrWhiteSpace(request.ImageUrl)) errors.Add(new { field = "imageUrl", message = "图片地址不能为空" }); if (string.IsNullOrWhiteSpace(request.LinkUrl)) errors.Add(new { field = "linkUrl", message = "链接地址不能为空" }); if (string.IsNullOrWhiteSpace(request.LinkType)) errors.Add(new { field = "linkType", message = "链接类型不能为空" }); else if (!Enum.TryParse(request.LinkType, true, out var parsed) || !Enum.IsDefined(parsed)) errors.Add(new { field = "linkType", message = "链接类型不合法" }); return errors; } // 门店请求校验辅助方法 static List ValidateShopRequest(ShopRequest request) { var errors = new List(); if (string.IsNullOrWhiteSpace(request.Name)) errors.Add(new { field = "name", message = "门店名称不能为空" }); if (string.IsNullOrWhiteSpace(request.Photo)) errors.Add(new { field = "photo", message = "门店照片不能为空" }); if (string.IsNullOrWhiteSpace(request.Location)) errors.Add(new { field = "location", message = "门店位置不能为空" }); if (string.IsNullOrWhiteSpace(request.PackingFeeType)) errors.Add(new { field = "packingFeeType", message = "打包费类型不能为空" }); if (request.PackingFeeAmount < 0) errors.Add(new { field = "packingFeeAmount", message = "打包费金额不能为负数" }); return errors; } // 菜品请求校验辅助方法 static List ValidateDishRequest(DishRequest request) { var errors = new List(); if (string.IsNullOrWhiteSpace(request.Name)) errors.Add(new { field = "name", message = "菜品名称不能为空" }); if (string.IsNullOrWhiteSpace(request.Photo)) errors.Add(new { field = "photo", message = "菜品照片不能为空" }); if (request.Price < 0) errors.Add(new { field = "price", message = "菜品价格不能为负数" }); return errors; } /// /// 计算单个门店的打包费 /// Fixed 模式:固定金额,不论菜品数量 /// PerItem 模式:菜品总份数 × 单份打包费 /// static decimal CalculatePackingFee(PackingFeeType feeType, decimal feeAmount, int totalQuantity) { return feeType switch { PackingFeeType.Fixed => feeAmount, PackingFeeType.PerItem => feeAmount * totalQuantity, _ => 0m }; } /// /// 计算平台佣金分成 /// 根据佣金区间规则计算:百分比类型按比例,固定类型扣除固定金额 /// static decimal CalculatePlatformFee(decimal commission, List rules) { // 查找匹配的佣金区间 var matchedRule = rules .Where(r => commission >= r.MinAmount && (r.MaxAmount == null || commission <= r.MaxAmount)) .FirstOrDefault(); if (matchedRule == null) return 0m; return matchedRule.RateType switch { CommissionRateType.Percentage => Math.Round(commission * matchedRule.Rate / 100m, 2), CommissionRateType.Fixed => Math.Min(matchedRule.Rate, commission), _ => 0m }; } /// /// 获取用户可见的系统消息(按目标类型过滤) /// static async Task> GetVisibleSystemMessages(AppDbContext db, int userId, User? user) { var allMessages = await db.SystemMessages.ToListAsync(); return allMessages.Where(m => { if (m.TargetType == MessageTargetType.All) return true; if (m.TargetType == MessageTargetType.OrderUser) return user != null && user.Role != UserRole.Runner; if (m.TargetType == MessageTargetType.RunnerUser) return user != null && (user.Role == UserRole.Runner || user.Role == UserRole.Admin); if (m.TargetType == MessageTargetType.Specific && m.TargetUserIds != null) { try { var ids = System.Text.Json.JsonSerializer.Deserialize>(m.TargetUserIds); return ids != null && ids.Contains(userId); } catch { return false; } } return false; }).ToList(); } /// /// 解冻已到期的收益记录:将冻结期满的收益从 Frozen 变为 Available /// static async Task UnfreezeEarnings(AppDbContext db) { var now = DateTime.UtcNow; var frozenEarnings = await db.Earnings .Where(e => e.Status == EarningStatus.Frozen && e.FrozenUntil <= now) .ToListAsync(); foreach (var earning in frozenEarnings) { earning.Status = EarningStatus.Available; } if (frozenEarnings.Count > 0) { await db.SaveChangesAsync(); } } // 用于集成测试访问 public partial class Program { }