diff --git a/PetWash.Api/Controllers/OrdersController.cs b/PetWash.Api/Controllers/OrdersController.cs new file mode 100644 index 0000000..e8ce945 --- /dev/null +++ b/PetWash.Api/Controllers/OrdersController.cs @@ -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 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 ConfirmPayment(int id) + { + var order = await _orderService.ConfirmPaymentAsync(id); + if (order == null) + return NotFound(); + + return Ok(order); + } + + [HttpGet("{id}")] + public async Task GetOrder(int id) + { + var order = await _orderService.GetOrderAsync(id); + if (order == null) + return NotFound(); + + return Ok(order); + } + + [HttpPut("{id}/status")] + public async Task 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); diff --git a/PetWash.Api/Controllers/PackagesController.cs b/PetWash.Api/Controllers/PackagesController.cs new file mode 100644 index 0000000..a5236e5 --- /dev/null +++ b/PetWash.Api/Controllers/PackagesController.cs @@ -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 GetPackages() + { + var packages = await _context.Packages.ToListAsync(); + return Ok(packages); + } + + [HttpGet("{id}")] + public async Task GetPackage(int id) + { + var package = await _context.Packages.FindAsync(id); + if (package == null) + return NotFound(); + + return Ok(package); + } +} diff --git a/PetWash.Api/Data/PetWashDbContext.cs b/PetWash.Api/Data/PetWashDbContext.cs new file mode 100644 index 0000000..a716393 --- /dev/null +++ b/PetWash.Api/Data/PetWashDbContext.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore; +using PetWash.Api.Models; + +namespace PetWash.Api.Data; + +public class PetWashDbContext : DbContext +{ + public PetWashDbContext(DbContextOptions options) : base(options) { } + + public DbSet Packages { get; set; } + public DbSet Orders { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + // 初始化套餐数据 + modelBuilder.Entity().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 = "深度清洗+烘干+护理,适合大型宠物" } + ); + } +} diff --git a/PetWash.Api/Models/DeviceStatus.cs b/PetWash.Api/Models/DeviceStatus.cs new file mode 100644 index 0000000..efee2f0 --- /dev/null +++ b/PetWash.Api/Models/DeviceStatus.cs @@ -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; } +} diff --git a/PetWash.Api/Models/Order.cs b/PetWash.Api/Models/Order.cs new file mode 100644 index 0000000..5f35df1 --- /dev/null +++ b/PetWash.Api/Models/Order.cs @@ -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 +} diff --git a/PetWash.Api/Models/Package.cs b/PetWash.Api/Models/Package.cs new file mode 100644 index 0000000..ee6534f --- /dev/null +++ b/PetWash.Api/Models/Package.cs @@ -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; +} diff --git a/PetWash.Api/PetWash.Api.csproj b/PetWash.Api/PetWash.Api.csproj new file mode 100644 index 0000000..91404dd --- /dev/null +++ b/PetWash.Api/PetWash.Api.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/PetWash.Api/PetWash.Api.http b/PetWash.Api/PetWash.Api.http new file mode 100644 index 0000000..50bf7f1 --- /dev/null +++ b/PetWash.Api/PetWash.Api.http @@ -0,0 +1,6 @@ +@PetWash.Api_HostAddress = http://localhost:5274 + +GET {{PetWash.Api_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/PetWash.Api/Program.cs b/PetWash.Api/Program.cs new file mode 100644 index 0000000..0d9aeff --- /dev/null +++ b/PetWash.Api/Program.cs @@ -0,0 +1,51 @@ +using Microsoft.EntityFrameworkCore; +using PetWash.Api.Data; +using PetWash.Api.Services; + +var builder = WebApplication.CreateBuilder(args); + +// 添加数据库 +builder.Services.AddDbContext(options => + options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection") ?? "Data Source=petwash.db")); + +// 添加服务 +builder.Services.AddSingleton(); +builder.Services.AddHostedService(provider => provider.GetRequiredService()); +builder.Services.AddScoped(); + +// 添加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(); + db.Database.EnsureCreated(); +} + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseCors(); +app.UseHttpsRedirection(); +app.UseAuthorization(); +app.MapControllers(); + +app.Run(); diff --git a/PetWash.Api/Properties/launchSettings.json b/PetWash.Api/Properties/launchSettings.json new file mode 100644 index 0000000..f2c4d0c --- /dev/null +++ b/PetWash.Api/Properties/launchSettings.json @@ -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" + } + } + } +} diff --git a/PetWash.Api/Services/MqttService.cs b/PetWash.Api/Services/MqttService.cs new file mode 100644 index 0000000..1d0fe54 --- /dev/null +++ b/PetWash.Api/Services/MqttService.cs @@ -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 _logger; + + public MqttService(ILogger 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}"); + } +} diff --git a/PetWash.Api/Services/OrderService.cs b/PetWash.Api/Services/OrderService.cs new file mode 100644 index 0000000..4bd9f9a --- /dev/null +++ b/PetWash.Api/Services/OrderService.cs @@ -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 _logger; + + public OrderService(PetWashDbContext context, MqttService mqttService, ILogger logger) + { + _context = context; + _mqttService = mqttService; + _logger = logger; + } + + public async Task 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 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 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 GetOrderAsync(int orderId) + { + return await _context.Orders.Include(o => o.Package).FirstOrDefaultAsync(o => o.Id == orderId); + } +} diff --git a/PetWash.Api/appsettings.Development.json b/PetWash.Api/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/PetWash.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/PetWash.Api/appsettings.json b/PetWash.Api/appsettings.json new file mode 100644 index 0000000..d5a869d --- /dev/null +++ b/PetWash.Api/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "Data Source=petwash.db" + } +}