using CampusErrand.Data; using CampusErrand.Models; using CampusErrand.Models.Dtos; using CampusErrand.Services; using CampusErrand.Helpers; using Microsoft.EntityFrameworkCore; namespace CampusErrand.Endpoints; public static class OrderEndpoints { public static void MapOrderEndpoints(this WebApplication app) { // 创建订单 app.MapPost("/api/orders", async (CreateOrderRequest request, HttpContext httpContext, AppDbContext db, WxPayService wxPay) => { // 获取当前用户 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 = "订单类型不合法" }); } // 佣金校验:读取最低佣金配置 var minCommissionConfig = await db.SystemConfigs.FirstOrDefaultAsync(c => c.Key == "min_commission"); var minCommission = 1.0m; if (minCommissionConfig != null && decimal.TryParse(minCommissionConfig.Value, out var configMin)) minCommission = configMin; if (request.Commission < minCommission) { return Results.BadRequest(new { code = 400, message = $"跑腿佣金不可低于{minCommission}元" }); } // 佣金校验:小数点后最多 2 位 if (request.Commission != Math.Round(request.Commission, 2)) { return Results.BadRequest(new { code = 400, message = "跑腿佣金最多支持小数点后2位" }); } // 计算支付总金额 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 += BusinessHelpers.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.Unpaid, 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(); } // 调用微信支付 JSAPI 下单 var user = await db.Users.FindAsync(userId); if (user == null || string.IsNullOrEmpty(user.OpenId)) return Results.BadRequest(new { code = 400, message = "用户信息异常,无法发起支付" }); var notifyUrl = $"{httpContext.Request.Scheme}://{httpContext.Request.Host}/api/pay/notify"; var typeLabel = orderType switch { OrderType.Pickup => "代取", OrderType.Delivery => "代送", OrderType.Help => "万能帮", OrderType.Purchase => "代购", OrderType.Food => "美食街", _ => "跑腿" }; var payResult = await wxPay.CreateJsapiOrder(order.OrderNo, order.TotalAmount, $"校园跑腿-{typeLabel}", user.OpenId, notifyUrl); if (!payResult.Success) { return Results.BadRequest(new { code = 400, message = "支付下单失败", detail = payResult.ErrorMessage }); } return Results.Created($"/api/orders/{order.Id}", new { Id = order.Id, OrderNo = order.OrderNo, TotalAmount = order.TotalAmount, PaymentParams = new { timeStamp = payResult.PaymentParams!.TimeStamp, nonceStr = payResult.PaymentParams.NonceStr, package_ = payResult.PaymentParams.Package, signType = payResult.PaymentParams.SignType, paySign = payResult.PaymentParams.PaySign } }); }).RequireAuthorization(); // 获取当前用户的聊天订单列表(已接单的订单,按时间排序) app.MapGet("/api/orders/chat-list", async (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 rawOrders = await db.Orders .Where(o => o.RunnerId != null && (o.OwnerId == currentUserId || o.RunnerId == currentUserId) && o.Status != OrderStatus.Cancelled) .OrderByDescending(o => o.CompletedAt ?? o.AcceptedAt ?? o.CreatedAt) .Select(o => new { OrderId = o.Id, o.OrderNo, OrderType = o.OrderType.ToString(), o.ItemName, Status = o.Status.ToString(), o.Commission, o.OwnerId, o.RunnerId, OwnerNickname = o.Owner!.Nickname, OwnerAvatar = o.Owner!.AvatarUrl, RunnerNickname = o.Runner!.Nickname, RunnerAvatar = o.Runner!.AvatarUrl, LastTime = o.CompletedAt ?? o.AcceptedAt ?? o.CreatedAt }) .ToListAsync(); var orders = rawOrders.Select(o => { var isOwner = o.OwnerId == currentUserId; var targetId = isOwner ? o.RunnerId : (int?)o.OwnerId; var nickname = isOwner ? o.RunnerNickname : o.OwnerNickname; var avatar = isOwner ? o.RunnerAvatar : o.OwnerAvatar; if (string.IsNullOrWhiteSpace(nickname)) nickname = $"用户{targetId}"; return new { o.OrderId, o.OrderNo, o.OrderType, o.ItemName, o.Status, o.Commission, TargetUserId = targetId, TargetNickname = nickname, TargetAvatar = avatar ?? "", o.LastTime }; }); return Results.Ok(orders); }).RequireAuthorization(); // 根据对方用户ID查找最近的关联订单(用于聊天页显示订单卡片) app.MapGet("/api/orders/by-chat-user/{targetUserId}", async (int targetUserId, 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 .Where(o => (o.OwnerId == currentUserId && o.RunnerId == targetUserId) || (o.OwnerId == targetUserId && o.RunnerId == currentUserId)) .OrderByDescending(o => o.CreatedAt) .FirstOrDefaultAsync(); if (order == null) return Results.Ok(new { found = false }); return Results.Ok(new { found = true, orderId = order.Id }); }).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).ThenInclude(fi => fi.Shop) .Include(o => o.FoodOrderItems).ThenInclude(fi => fi.Dish) .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; } // 跑腿手机号:从认证记录中获取,仅单主可见 string? runnerPhone = null; if (currentUserId == order.OwnerId && order.RunnerId != null) { var cert = await db.RunnerCertifications .Where(c => c.UserId == order.RunnerId && c.Status == CertificationStatus.Approved) .OrderByDescending(c => c.CreatedAt) .FirstOrDefaultAsync(); runnerPhone = cert?.Phone; } 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, RunnerPhone = runnerPhone }; // 查询佣金抽成信息(有 Earning 记录用实际值,否则实时计算预估值) if (order.RunnerId.HasValue) { var earning = await db.Earnings .Where(e => e.OrderId == order.Id) .FirstOrDefaultAsync(); if (earning != null) { response.PlatformFee = earning.PlatformFee; response.NetEarning = earning.NetEarning; } else { // 未完成订单:实时计算预估抽成 var rules = await db.CommissionRules.OrderBy(r => r.MinAmount).ToListAsync(); var fee = BusinessHelpers.CalculatePlatformFee(order.Commission, rules); response.PlatformFee = fee; response.NetEarning = order.Commission - fee; } } // 美食街订单附带菜品详情 if (order.OrderType == OrderType.Food && order.FoodOrderItems.Count > 0) { response.FoodItems = order.FoodOrderItems.Select(fi => new FoodOrderItemResponse { Id = fi.Id, ShopId = fi.ShopId, ShopName = fi.Shop?.Name ?? "未知门店", DishId = fi.DishId, DishName = fi.Dish?.Name ?? "未知菜品", DishPhoto = fi.Dish?.Photo, 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() switch { "commission" => query.OrderByDescending(o => o.Commission), "distance" => query.OrderByDescending(o => o.CreatedAt), // 距离排序需地址坐标,暂按时间排序 _ => query.OrderByDescending(o => o.CreatedAt) // 默认按时间排序 }; 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); }).AllowAnonymous(); // 接单 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/pay/notify", async (HttpContext httpContext, AppDbContext db) => { // 读取请求体 using var reader = new StreamReader(httpContext.Request.Body); var body = await reader.ReadToEndAsync(); var serialNo = httpContext.Request.Headers["Wechatpay-Serial"].ToString(); var timestamp = httpContext.Request.Headers["Wechatpay-Timestamp"].ToString(); var nonce = httpContext.Request.Headers["Wechatpay-Nonce"].ToString(); var signature = httpContext.Request.Headers["Wechatpay-Signature"].ToString(); var wxPay = httpContext.RequestServices.GetRequiredService(); var result = wxPay.VerifyAndDecryptNotify(serialNo, timestamp, nonce, signature, body); if (result == null) { return Results.Json(new { code = "FAIL", message = "解密失败" }, statusCode: 500); } if (result.TradeState == "SUCCESS") { // 支付成功,将待支付订单改为待接单 var order = await db.Orders.FirstOrDefaultAsync(o => o.OrderNo == result.OrderNo); if (order != null && order.Status == OrderStatus.Unpaid) { order.Status = OrderStatus.Pending; await db.SaveChangesAsync(); Console.WriteLine($"[微信支付] 支付成功,订单已上架: {result.OrderNo}, 金额: {result.TotalAmount}分"); } } return Results.Json(new { code = "SUCCESS", message = "OK" }); }).AllowAnonymous(); // 前端支付成功后确认订单(本地开发用,生产环境由回调处理) app.MapPost("/api/orders/{id}/pay-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 && o.OwnerId == userId); if (order == null) return Results.NotFound(new { code = 404, message = "订单不存在" }); if (order.Status == OrderStatus.Unpaid) { order.Status = OrderStatus.Pending; await db.SaveChangesAsync(); } return Results.Ok(new { id = order.Id, status = order.Status.ToString() }); }).RequireAuthorization(); // 取消订单(仅单主可取消待支付或待接单订单,自动退款) app.MapPost("/api/orders/{id}/cancel", async (int id, HttpContext httpContext, AppDbContext db, WxPayService wxPay) => { 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 && order.Status != OrderStatus.Unpaid) return Results.BadRequest(new { code = 400, message = "仅待支付或待接单状态的订单可取消" }); var wasPaid = order.Status == OrderStatus.Pending; // 已支付的才需要退款 order.Status = OrderStatus.Cancelled; await db.SaveChangesAsync(); // 已支付的订单发起微信退款(原路返回) var refundSuccess = false; if (wasPaid) { try { var totalFen = (int)(order.TotalAmount * 100); var refundNo = $"R{order.OrderNo}"; refundSuccess = await wxPay.Refund(order.OrderNo, refundNo, totalFen, totalFen, "用户取消订单"); Console.WriteLine($"[订单取消] {order.OrderNo} 退款{(refundSuccess ? "成功" : "失败")}"); } catch (Exception ex) { Console.WriteLine($"[订单取消] {order.OrderNo} 退款异常: {ex.Message}"); } } return Results.Ok(new { id = order.Id, orderNo = order.OrderNo, status = order.Status.ToString(), refundSuccess }); }).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 = BusinessHelpers.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.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.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, WxPayService wxPay) => { 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; var difference = priceChange.NewPrice - priceChange.OriginalPrice; object? paymentParams = null; var refundSuccess = false; if (action == PriceChangeStatus.Accepted) { var oldTotal = order.TotalAmount; 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; var totalDiff = order.TotalAmount - oldTotal; // 需要补价:创建微信支付订单 if (totalDiff > 0) { try { var owner = await db.Users.FindAsync(order.OwnerId); if (owner != null && !string.IsNullOrEmpty(owner.OpenId)) { var notifyUrl = $"{httpContext.Request.Scheme}://{httpContext.Request.Host}/api/pay/notify"; var payOrderNo = $"P{order.OrderNo}{DateTime.UtcNow:mmss}"; var payResult = await wxPay.CreateJsapiOrder(payOrderNo, totalDiff, "校园跑腿-改价补缴", owner.OpenId, notifyUrl); if (payResult.Success) { paymentParams = new { timeStamp = payResult.PaymentParams!.TimeStamp, nonceStr = payResult.PaymentParams.NonceStr, package_ = payResult.PaymentParams.Package, signType = payResult.PaymentParams.SignType, paySign = payResult.PaymentParams.PaySign }; } } } catch (Exception ex) { Console.WriteLine($"[改价] 补价支付创建失败: {ex.Message}"); } } // 需要退款:自动退款 else if (totalDiff < 0) { try { var refundAmount = Math.Abs(totalDiff); var refundFen = (int)(refundAmount * 100); var totalFen = (int)(oldTotal * 100); var refundNo = $"RP{order.OrderNo}{DateTime.UtcNow:mmss}"; refundSuccess = await wxPay.Refund(order.OrderNo, refundNo, totalFen, refundFen, "改价退款"); Console.WriteLine($"[改价] 退款{(refundSuccess ? "成功" : "失败")}: ¥{refundAmount}"); } catch (Exception ex) { Console.WriteLine($"[改价] 退款失败: {ex.Message}"); } } } await db.SaveChangesAsync(); return Results.Ok(new { 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, PaymentParams = paymentParams, RefundSuccess = refundSuccess }); }).RequireAuthorization(); // 获取订单申诉记录 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(); } }