campus-errand/server/Program.cs
2026-03-17 18:51:49 +08:00

2864 lines
98 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 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<AppDbContext>(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<JwtService>();
// 注册微信服务
builder.Services.AddHttpClient<IWeChatService, WeChatService>();
// 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()
}
});
});
// 微信快捷登录(仅需 code通过 openid 注册/登录)
app.MapPost("/api/auth/wx-login", async (
WxLoginRequest request,
IWeChatService weChatService,
JwtService jwtService,
AppDbContext db) =>
{
// 1. 调用微信 code2Session 获取 openid
Console.WriteLine($"[wx-login] 收到 code: {request.Code}");
var sessionResult = await weChatService.Code2SessionAsync(request.Code);
if (!sessionResult.Success)
{
return Results.BadRequest(new { code = 400, message = sessionResult.ErrorMessage });
}
// 2. 根据 openid 查找或创建用户
var openId = sessionResult.OpenId!;
var user = await db.Users.FirstOrDefaultAsync(u => u.OpenId == openId);
if (user == null)
{
// 自动注册,手机号暂时留空
var randomSuffix = Random.Shared.Next(1000, 9999).ToString();
user = new User
{
OpenId = openId,
Phone = "",
Nickname = $"微信用户{randomSuffix}",
CreatedAt = DateTime.UtcNow
};
db.Users.Add(user);
await db.SaveChangesAsync();
}
// 3. 生成 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<LinkType>(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<LinkType>(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/admin/service-entries", async (ServiceEntryCreateRequest request, AppDbContext db) =>
{
if (string.IsNullOrWhiteSpace(request.Name))
return Results.BadRequest(new { code = 400, message = "服务名称不能为空" });
if (string.IsNullOrWhiteSpace(request.IconUrl))
return Results.BadRequest(new { code = 400, message = "背景图片不能为空" });
if (string.IsNullOrWhiteSpace(request.PagePath))
return Results.BadRequest(new { code = 400, message = "跳转路径不能为空" });
var entry = new ServiceEntry
{
Name = request.Name,
IconUrl = request.IconUrl,
PagePath = request.PagePath,
SortOrder = request.SortOrder,
IsEnabled = request.IsEnabled
};
db.ServiceEntries.Add(entry);
await db.SaveChangesAsync();
return Results.Created($"/api/admin/service-entries/{entry.Id}", new ServiceEntryResponse
{
Id = entry.Id,
Name = entry.Name,
IconUrl = entry.IconUrl,
PagePath = entry.PagePath,
SortOrder = entry.SortOrder,
IsEnabled = entry.IsEnabled
});
}).RequireAuthorization("AdminOnly");
// 删除服务入口
app.MapDelete("/api/admin/service-entries/{id}", async (int id, AppDbContext db) =>
{
var entry = await db.ServiceEntries.FindAsync(id);
if (entry == null)
return Results.NotFound(new { code = 404, message = "服务入口不存在" });
db.ServiceEntries.Remove(entry);
await db.SaveChangesAsync();
return Results.NoContent();
}).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<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 += 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).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;
}
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,
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);
}).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<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.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<PackingFeeType>(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<PackingFeeType>(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<object>();
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<CertificationStatus>(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<CertificationStatus>(request.Status, true, out var newStatus)
|| (newStatus != CertificationStatus.Approved && newStatus != CertificationStatus.Rejected))
{
return Results.BadRequest(new { code = 400, message = "审核结果不合法,仅支持 Approved 或 Rejected" });
}
certification.Status = newStatus;
certification.ReviewedAt = DateTime.UtcNow;
// 审核通过时,更新用户角色为 Runner
if (newStatus == CertificationStatus.Approved && certification.User != null)
{
certification.User.Role = UserRole.Runner;
}
await db.SaveChangesAsync();
return Results.Ok(new CertificationResponse
{
Id = certification.Id,
UserId = certification.UserId,
RealName = certification.RealName,
Phone = certification.Phone,
Status = certification.Status.ToString(),
CreatedAt = certification.CreatedAt,
ReviewedAt = certification.ReviewedAt
});
}).RequireAuthorization("AdminOnly");
// ========== 评价接口 ==========
// 提交评价(单主评价跑腿)
app.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<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/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<PaymentMethod>(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<CommissionRule> 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<object>();
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<MessageTargetType>(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<OrderStatus>(status, true, out var s))
query = query.Where(o => o.Status == s);
if (!string.IsNullOrEmpty(orderType) && Enum.TryParse<OrderType>(orderType, true, out var t))
query = query.Where(o => o.OrderType == t);
var orders = await query
.OrderByDescending(o => o.CreatedAt)
.Select(o => new
{
o.Id,
o.OrderNo,
o.OwnerId,
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<OrderStatus>(request.NewStatus, true, out var newStatus)
|| (newStatus != OrderStatus.Completed && newStatus != OrderStatus.Cancelled))
return Results.BadRequest(new { code = 400, message = "目标状态不合法,仅支持 Completed 或 Cancelled" });
// 创建申诉记录
var appeal = new Appeal
{
OrderId = id,
Result = request.Result,
CreatedAt = DateTime.UtcNow
};
db.Appeals.Add(appeal);
// 更新订单状态
order.Status = newStatus;
if (newStatus == OrderStatus.Completed && order.CompletedAt == null)
{
order.CompletedAt = DateTime.UtcNow;
}
await db.SaveChangesAsync();
return Results.Ok(new
{
id = order.Id,
orderNo = order.OrderNo,
status = order.Status.ToString(),
appealId = appeal.Id,
appealResult = appeal.Result,
appealCreatedAt = appeal.CreatedAt
});
}).RequireAuthorization("AdminOnly");
// 获取订单申诉记录
app.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<long>("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<string[]>()
?? 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)}" });
// 上传到腾讯云 COS
var cosConfig = new COSXML.CosXmlConfig.Builder()
.IsHttps(true)
.SetRegion(config["COS:Region"])
.Build();
var credential = new COSXML.Auth.DefaultQCloudCredentialProvider(
config["COS:SecretId"], config["COS:SecretKey"], 600);
var cosXml = new COSXML.CosXmlServer(cosConfig, credential);
var bucket = config["COS:Bucket"]!;
var cosKey = $"uploads/{DateTime.UtcNow:yyyyMMdd}/{Guid.NewGuid()}{ext}";
// 将上传文件写入临时文件
var tempPath = Path.GetTempFileName();
try
{
using (var stream = new FileStream(tempPath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
var putRequest = new COSXML.Model.Object.PutObjectRequest(bucket, cosKey, tempPath);
var putResult = cosXml.PutObject(putRequest);
if (putResult.httpCode != 200)
return Results.BadRequest(new { code = 400, message = "图片上传失败" });
var url = $"{config["COS:BaseUrl"]}/{cosKey}";
return Results.Ok(new UploadImageResponse { Url = url });
}
finally
{
if (File.Exists(tempPath)) File.Delete(tempPath);
}
}).RequireAuthorization()
.DisableAntiforgery();
// ========== 系统配置接口 ==========
// 获取页面顶图配置
app.MapGet("/api/config/page-banner/{page}", async (string page, AppDbContext db) =>
{
var key = $"page_banner_{page}";
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
});
});
// 获取客服二维码
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<string>
{
"qrcode", "agreement", "privacy", "withdrawal_guide", "freeze_days",
"page_banner_pickup", "page_banner_delivery", "page_banner_help", "page_banner_purchase", "page_banner_food",
"page_banner_order-hall"
};
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<AppDbContext>();
if (db.Database.IsRelational() && !(db.Database.ProviderName?.Contains("InMemory") ?? false))
{
db.Database.Migrate();
}
}
app.Run();
// Banner 请求校验辅助方法
static List<object> ValidateBannerRequest(BannerRequest request)
{
var errors = new List<object>();
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<LinkType>(request.LinkType, true, out var parsed) || !Enum.IsDefined(parsed))
errors.Add(new { field = "linkType", message = "链接类型不合法" });
return errors;
}
// 门店请求校验辅助方法
static List<object> ValidateShopRequest(ShopRequest request)
{
var errors = new List<object>();
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<object> ValidateDishRequest(DishRequest request)
{
var errors = new List<object>();
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;
}
/// <summary>
/// 计算单个门店的打包费
/// Fixed 模式:固定金额,不论菜品数量
/// PerItem 模式:菜品总份数 × 单份打包费
/// </summary>
static decimal CalculatePackingFee(PackingFeeType feeType, decimal feeAmount, int totalQuantity)
{
return feeType switch
{
PackingFeeType.Fixed => feeAmount,
PackingFeeType.PerItem => feeAmount * totalQuantity,
_ => 0m
};
}
/// <summary>
/// 计算平台佣金分成
/// 根据佣金区间规则计算:百分比类型按比例,固定类型扣除固定金额
/// </summary>
static decimal CalculatePlatformFee(decimal commission, List<CommissionRule> 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
};
}
/// <summary>
/// 获取用户可见的系统消息(按目标类型过滤)
/// </summary>
static async Task<List<SystemMessage>> 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<List<int>>(m.TargetUserIds);
return ids != null && ids.Contains(userId);
}
catch { return false; }
}
return false;
}).ToList();
}
/// <summary>
/// 解冻已到期的收益记录:将冻结期满的收益从 Frozen 变为 Available
/// </summary>
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 { }