campus-errand/server/Endpoints/OrderEndpoints.cs
18631081161 2d0c71721d
All checks were successful
continuous-integration/drone/push Build is passing
改bug
2026-03-29 21:13:50 +08:00

1004 lines
43 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using CampusErrand.Data;
using CampusErrand.Models;
using CampusErrand.Models.Dtos;
using CampusErrand.Services;
using 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<OrderType>(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 += 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.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();
}
// 调用微信支付 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<OrderType>(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<int, (int ShopCount, int DishCount)>();
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<WxPayService>();
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")
{
// 支付成功,更新订单状态(如果还是 Pending 说明支付完成)
var order = await db.Orders.FirstOrDefaultAsync(o => o.OrderNo == result.OrderNo);
if (order != null)
{
Console.WriteLine($"[微信支付] 支付成功: {result.OrderNo}, 金额: {result.TotalAmount}分");
}
}
return Results.Json(new { code = "SUCCESS", message = "OK" });
}).AllowAnonymous();
// 取消订单(仅单主可取消待接单订单)
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 = 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<OrderStatus>(status, true, out var orderStatus))
query = query.Where(o => o.Status == orderStatus);
// 按类型筛选
if (!string.IsNullOrEmpty(type) && Enum.TryParse<OrderType>(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<OrderStatus>(status, true, out var orderStatus))
query = query.Where(o => o.Status == orderStatus);
// 按类型筛选
if (!string.IsNullOrEmpty(type) && Enum.TryParse<OrderType>(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<PriceChangeType>(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<PriceChangeStatus>(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/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();
}
}