feat: add 服务端

This commit is contained in:
GukSang.Jin
2026-02-25 15:41:00 +08:00
parent d57133e072
commit 833031864b
14 changed files with 469 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
using Microsoft.AspNetCore.Mvc;
using PetWash.Api.Models;
using PetWash.Api.Services;
namespace PetWash.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly OrderService _orderService;
public OrdersController(OrderService orderService)
{
_orderService = orderService;
}
[HttpPost]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderRequest request)
{
try
{
var order = await _orderService.CreateOrderAsync(request.PackageId);
return Ok(order);
}
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
}
[HttpPost("{id}/payment")]
public async Task<IActionResult> ConfirmPayment(int id)
{
var order = await _orderService.ConfirmPaymentAsync(id);
if (order == null)
return NotFound();
return Ok(order);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetOrder(int id)
{
var order = await _orderService.GetOrderAsync(id);
if (order == null)
return NotFound();
return Ok(order);
}
[HttpPut("{id}/status")]
public async Task<IActionResult> UpdateStatus(int id, [FromBody] UpdateStatusRequest request)
{
var order = await _orderService.UpdateOrderStatusAsync(id, request.Status);
if (order == null)
return NotFound();
return Ok(order);
}
}
public record CreateOrderRequest(int PackageId);
public record UpdateStatusRequest(OrderStatus Status);

View File

@@ -0,0 +1,34 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using PetWash.Api.Data;
namespace PetWash.Api.Controllers;
[ApiController]
[Route("api/[controller]")]
public class PackagesController : ControllerBase
{
private readonly PetWashDbContext _context;
public PackagesController(PetWashDbContext context)
{
_context = context;
}
[HttpGet]
public async Task<IActionResult> GetPackages()
{
var packages = await _context.Packages.ToListAsync();
return Ok(packages);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetPackage(int id)
{
var package = await _context.Packages.FindAsync(id);
if (package == null)
return NotFound();
return Ok(package);
}
}

View File

@@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore;
using PetWash.Api.Models;
namespace PetWash.Api.Data;
public class PetWashDbContext : DbContext
{
public PetWashDbContext(DbContextOptions<PetWashDbContext> options) : base(options) { }
public DbSet<Package> Packages { get; set; }
public DbSet<Order> Orders { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// 初始化套餐数据
modelBuilder.Entity<Package>().HasData(
new Package { Id = 1, Name = "基础清洗", Price = 50, DurationMinutes = 15, Description = "基础清洗服务,适合小型宠物" },
new Package { Id = 2, Name = "标准清洗", Price = 80, DurationMinutes = 25, Description = "标准清洗+烘干,适合中型宠物" },
new Package { Id = 3, Name = "豪华清洗", Price = 120, DurationMinutes = 35, Description = "深度清洗+烘干+护理,适合大型宠物" }
);
}
}

View File

@@ -0,0 +1,10 @@
namespace PetWash.Api.Models;
public class DeviceStatus
{
public string Status { get; set; } = "Idle";
public bool DoorOpen { get; set; }
public bool IsWashing { get; set; }
public int? CurrentOrderId { get; set; }
public DateTime LastUpdated { get; set; }
}

View File

@@ -0,0 +1,26 @@
namespace PetWash.Api.Models;
public class Order
{
public int Id { get; set; }
public int PackageId { get; set; }
public Package? Package { get; set; }
public DateTime CreatedAt { get; set; }
public OrderStatus Status { get; set; }
public bool IsPaid { get; set; }
public DateTime? PaidAt { get; set; }
public DateTime? StartedAt { get; set; }
public DateTime? CompletedAt { get; set; }
}
public enum OrderStatus
{
Created,
WaitingPayment,
Paid,
DoorOpened,
DoorClosed,
Washing,
Completed,
Cancelled
}

View File

@@ -0,0 +1,10 @@
namespace PetWash.Api.Models;
public class Package
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public int DurationMinutes { get; set; }
public string Description { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MQTTnet.AspNetCore" Version="4.3.7.1207" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,6 @@
@PetWash.Api_HostAddress = http://localhost:5274
GET {{PetWash.Api_HostAddress}}/weatherforecast/
Accept: application/json
###

51
PetWash.Api/Program.cs Normal file
View File

@@ -0,0 +1,51 @@
using Microsoft.EntityFrameworkCore;
using PetWash.Api.Data;
using PetWash.Api.Services;
var builder = WebApplication.CreateBuilder(args);
// 添加数据库
builder.Services.AddDbContext<PetWashDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection") ?? "Data Source=petwash.db"));
// 添加服务
builder.Services.AddSingleton<MqttService>();
builder.Services.AddHostedService(provider => provider.GetRequiredService<MqttService>());
builder.Services.AddScoped<OrderService>();
// 添加CORS
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// 初始化数据库
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<PetWashDbContext>();
db.Database.EnsureCreated();
}
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseCors();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

View File

@@ -0,0 +1,41 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:58807",
"sslPort": 44392
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5274",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7203;http://localhost:5274",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,68 @@
using MQTTnet;
using MQTTnet.Server;
using System.Text;
using System.Text.Json;
namespace PetWash.Api.Services;
public class MqttService : IHostedService
{
private readonly MqttServer _mqttServer;
private readonly ILogger<MqttService> _logger;
public MqttService(ILogger<MqttService> logger)
{
_logger = logger;
var optionsBuilder = new MqttServerOptionsBuilder()
.WithDefaultEndpoint()
.WithDefaultEndpointPort(1883);
_mqttServer = new MqttFactory().CreateMqttServer(optionsBuilder.Build());
_mqttServer.ClientConnectedAsync += OnClientConnected;
_mqttServer.ClientDisconnectedAsync += OnClientDisconnected;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
await _mqttServer.StartAsync();
_logger.LogInformation("MQTT Broker started on port 1883");
}
public async Task StopAsync(CancellationToken cancellationToken)
{
await _mqttServer.StopAsync();
_logger.LogInformation("MQTT Broker stopped");
}
private Task OnClientConnected(ClientConnectedEventArgs args)
{
_logger.LogInformation($"Client connected: {args.ClientId}");
return Task.CompletedTask;
}
private Task OnClientDisconnected(ClientDisconnectedEventArgs args)
{
_logger.LogInformation($"Client disconnected: {args.ClientId}");
return Task.CompletedTask;
}
public async Task PublishAsync(string topic, object payload)
{
var json = JsonSerializer.Serialize(payload);
var message = new MqttApplicationMessageBuilder()
.WithTopic(topic)
.WithPayload(json)
.WithQualityOfServiceLevel(MQTTnet.Protocol.MqttQualityOfServiceLevel.AtLeastOnce)
.WithRetainFlag(false)
.Build();
await _mqttServer.InjectApplicationMessage(new InjectedMqttApplicationMessage(message)
{
SenderClientId = "PetWash.Api"
});
_logger.LogInformation($"Published to {topic}: {json}");
}
}

View File

@@ -0,0 +1,96 @@
using Microsoft.EntityFrameworkCore;
using PetWash.Api.Data;
using PetWash.Api.Models;
namespace PetWash.Api.Services;
public class OrderService
{
private readonly PetWashDbContext _context;
private readonly MqttService _mqttService;
private readonly ILogger<OrderService> _logger;
public OrderService(PetWashDbContext context, MqttService mqttService, ILogger<OrderService> logger)
{
_context = context;
_mqttService = mqttService;
_logger = logger;
}
public async Task<Order> CreateOrderAsync(int packageId)
{
var package = await _context.Packages.FindAsync(packageId);
if (package == null)
throw new ArgumentException("套餐不存在");
var order = new Order
{
PackageId = packageId,
CreatedAt = DateTime.Now,
Status = OrderStatus.WaitingPayment,
IsPaid = false
};
_context.Orders.Add(order);
await _context.SaveChangesAsync();
_logger.LogInformation($"订单创建成功: OrderId={order.Id}, PackageId={packageId}");
return order;
}
public async Task<Order?> ConfirmPaymentAsync(int orderId)
{
var order = await _context.Orders.Include(o => o.Package).FirstOrDefaultAsync(o => o.Id == orderId);
if (order == null) return null;
order.IsPaid = true;
order.PaidAt = DateTime.Now;
order.Status = OrderStatus.Paid;
await _context.SaveChangesAsync();
// 支付成功后通过MQTT发送开门指令
await _mqttService.PublishAsync("device/command", new
{
command = "open_door",
orderId = order.Id,
timestamp = DateTime.Now
});
_logger.LogInformation($"订单支付成功,已发送开门指令: OrderId={orderId}");
return order;
}
public async Task<Order?> UpdateOrderStatusAsync(int orderId, OrderStatus status)
{
var order = await _context.Orders.FindAsync(orderId);
if (order == null) return null;
order.Status = status;
if (status == OrderStatus.DoorClosed)
{
order.StartedAt = DateTime.Now;
// 门关闭后,发送开始清洗指令
await _mqttService.PublishAsync("device/command", new
{
command = "start_wash",
orderId = order.Id,
durationMinutes = order.Package?.DurationMinutes ?? 15,
timestamp = DateTime.Now
});
}
else if (status == OrderStatus.Completed)
{
order.CompletedAt = DateTime.Now;
}
await _context.SaveChangesAsync();
_logger.LogInformation($"订单状态更新: OrderId={orderId}, Status={status}");
return order;
}
public async Task<Order?> GetOrderAsync(int orderId)
{
return await _context.Orders.Include(o => o.Package).FirstOrDefaultAsync(o => o.Id == orderId);
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,12 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Data Source=petwash.db"
}
}