This commit is contained in:
GukSang.Jin
2026-03-18 13:53:44 +08:00
parent 54b3448e31
commit 0a884fa6cb
20 changed files with 1700 additions and 110 deletions

View File

@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using PetWash.Api.Data;
using PetWash.Api.Models;
namespace PetWash.Api.Controllers;
@@ -18,7 +19,9 @@ public class PackagesController : ControllerBase
[HttpGet]
public async Task<IActionResult> GetPackages()
{
var packages = await _context.Packages.ToListAsync();
var packages = await _context.Packages
.OrderBy(x => x.Id)
.ToListAsync();
return Ok(packages);
}
@@ -31,4 +34,71 @@ public class PackagesController : ControllerBase
return Ok(package);
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdatePackage(int id, [FromBody] UpdatePackageRequest request)
{
var package = await _context.Packages.FindAsync(id);
if (package == null)
{
return NotFound();
}
if (request.Price <= 0)
{
return BadRequest("套餐金额必须大于 0。");
}
if (request.FirstSprayWaterTime <= 0 ||
request.AfterShampoo1SprayTime <= 0 ||
request.AfterShampoo2SprayTime <= 0 ||
request.AfterShampoo3SprayTime <= 0 ||
request.SprayShampoo1Time <= 0 ||
request.SprayShampoo2Time <= 0 ||
request.SprayShampoo3Time <= 0 ||
request.HotAirTime <= 0 ||
request.ColdAirTime <= 0 ||
request.UvSterilizationTime <= 0)
{
return BadRequest("套餐流程时间必须全部大于 0。");
}
package.Price = request.Price;
package.FirstSprayWaterTime = request.FirstSprayWaterTime;
package.AfterShampoo1SprayTime = request.AfterShampoo1SprayTime;
package.AfterShampoo2SprayTime = request.AfterShampoo2SprayTime;
package.AfterShampoo3SprayTime = request.AfterShampoo3SprayTime;
package.SprayShampoo1Time = request.SprayShampoo1Time;
package.SprayShampoo2Time = request.SprayShampoo2Time;
package.SprayShampoo3Time = request.SprayShampoo3Time;
package.HotAirTime = request.HotAirTime;
package.ColdAirTime = request.ColdAirTime;
package.UvSterilizationTime = request.UvSterilizationTime;
package.DurationMinutes = request.FirstSprayWaterTime
+ request.AfterShampoo1SprayTime
+ request.AfterShampoo2SprayTime
+ request.AfterShampoo3SprayTime
+ request.SprayShampoo1Time
+ request.SprayShampoo2Time
+ request.SprayShampoo3Time
+ request.HotAirTime
+ request.ColdAirTime
+ request.UvSterilizationTime;
await _context.SaveChangesAsync();
return Ok(package);
}
}
public record UpdatePackageRequest(
decimal Price,
int FirstSprayWaterTime,
int AfterShampoo1SprayTime,
int AfterShampoo2SprayTime,
int AfterShampoo3SprayTime,
int SprayShampoo1Time,
int SprayShampoo2Time,
int SprayShampoo3Time,
int HotAirTime,
int ColdAirTime,
int UvSterilizationTime);

View File

@@ -13,12 +13,6 @@ public class PetWashDbContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// 初始化套餐数据
modelBuilder.Entity<Package>().HasData(
new Package { Id = 1, Name = "套餐1", Price = 50, DurationMinutes = 38, Description = "适用于小型犬" },
new Package { Id = 2, Name = "套餐2", Price = 80, DurationMinutes = 48, Description = "适用于中型犬" },
new Package { Id = 3, Name = "套餐3", Price = 120, DurationMinutes = 60, Description = "适用于大型犬" }
);
modelBuilder.Entity<Package>().HasData(PackageCatalog.DefaultPackages.ToArray());
}
}

View File

@@ -7,4 +7,14 @@ public class Package
public decimal Price { get; set; }
public int DurationMinutes { get; set; }
public string Description { get; set; } = string.Empty;
public int FirstSprayWaterTime { get; set; }
public int AfterShampoo1SprayTime { get; set; }
public int AfterShampoo2SprayTime { get; set; }
public int AfterShampoo3SprayTime { get; set; }
public int SprayShampoo1Time { get; set; }
public int SprayShampoo2Time { get; set; }
public int SprayShampoo3Time { get; set; }
public int HotAirTime { get; set; }
public int ColdAirTime { get; set; }
public int UvSterilizationTime { get; set; }
}

View File

@@ -0,0 +1,62 @@
namespace PetWash.Api.Models;
public static class PackageCatalog
{
public static IReadOnlyList<Package> DefaultPackages { get; } =
[
new Package
{
Id = 1,
Name = "小型犬套餐",
Price = 0.01m,
DurationMinutes = 20,
Description = "适用于小型犬,洗护流程较短",
FirstSprayWaterTime = 2,
AfterShampoo1SprayTime = 2,
AfterShampoo2SprayTime = 2,
AfterShampoo3SprayTime = 2,
SprayShampoo1Time = 1,
SprayShampoo2Time = 1,
SprayShampoo3Time = 1,
HotAirTime = 5,
ColdAirTime = 2,
UvSterilizationTime = 2
},
new Package
{
Id = 2,
Name = "中型犬套餐",
Price = 0.02m,
DurationMinutes = 26,
Description = "适用于中型犬,洗护流程适中",
FirstSprayWaterTime = 3,
AfterShampoo1SprayTime = 3,
AfterShampoo2SprayTime = 3,
AfterShampoo3SprayTime = 3,
SprayShampoo1Time = 1,
SprayShampoo2Time = 1,
SprayShampoo3Time = 1,
HotAirTime = 6,
ColdAirTime = 3,
UvSterilizationTime = 2
},
new Package
{
Id = 3,
Name = "大型犬套餐",
Price = 0.03m,
DurationMinutes = 37,
Description = "适用于大型犬,洗护流程更长",
FirstSprayWaterTime = 4,
AfterShampoo1SprayTime = 4,
AfterShampoo2SprayTime = 4,
AfterShampoo3SprayTime = 4,
SprayShampoo1Time = 2,
SprayShampoo2Time = 2,
SprayShampoo3Time = 2,
HotAirTime = 8,
ColdAirTime = 4,
UvSterilizationTime = 3
}
];
}

View File

@@ -1,12 +1,14 @@
using System.Data;
using System.Data.Common;
using Microsoft.EntityFrameworkCore;
using PetWash.Api.Data;
using PetWash.Api.Models;
using PetWash.Api.Services;
var builder = WebApplication.CreateBuilder(args);
// 配置数据库(支持 SQLite 和 MySQL
var dbProvider = builder.Configuration.GetValue<string>("DatabaseProvider") ?? "Sqlite";
var connectionString = dbProvider == "MySql"
var connectionString = dbProvider == "MySql"
? builder.Configuration.GetConnectionString("MySqlConnection")
: builder.Configuration.GetConnectionString("DefaultConnection");
@@ -23,7 +25,6 @@ builder.Services.AddDbContext<PetWashDbContext>(options =>
}
});
// 添加服务
builder.Services.AddSingleton<MqttService>();
builder.Services.AddHostedService(provider => provider.GetRequiredService<MqttService>());
builder.Services.AddScoped<OrderService>();
@@ -34,7 +35,6 @@ builder.Services.AddHttpClient<WeChatPayService>(client =>
client.Timeout = TimeSpan.FromSeconds(15);
});
// 添加CORS
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
@@ -51,36 +51,79 @@ builder.Services.AddSwaggerGen();
var app = builder.Build();
// 初始化数据库
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<PetWashDbContext>();
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
try
{
logger.LogInformation("开始初始化数据库...");
// 确保数据库已创建
logger.LogInformation("Starting database initialization.");
var created = db.Database.EnsureCreated();
if (created)
{
logger.LogInformation("数据库创建成功,种子数据已插入");
logger.LogInformation("Database created and seed data inserted.");
}
else
{
logger.LogInformation("数据库已存在");
logger.LogInformation("Database already exists.");
}
EnsurePackageTimingColumns(db, logger);
foreach (var package in PackageCatalog.DefaultPackages)
{
var existingPackage = db.Packages.FirstOrDefault(x => x.Id == package.Id);
if (existingPackage is null)
{
db.Packages.Add(new Package
{
Id = package.Id,
Name = package.Name,
Price = package.Price,
DurationMinutes = package.DurationMinutes,
Description = package.Description,
FirstSprayWaterTime = package.FirstSprayWaterTime,
AfterShampoo1SprayTime = package.AfterShampoo1SprayTime,
AfterShampoo2SprayTime = package.AfterShampoo2SprayTime,
AfterShampoo3SprayTime = package.AfterShampoo3SprayTime,
SprayShampoo1Time = package.SprayShampoo1Time,
SprayShampoo2Time = package.SprayShampoo2Time,
SprayShampoo3Time = package.SprayShampoo3Time,
HotAirTime = package.HotAirTime,
ColdAirTime = package.ColdAirTime,
UvSterilizationTime = package.UvSterilizationTime
});
}
else
{
existingPackage.Name = package.Name;
existingPackage.Price = package.Price;
existingPackage.DurationMinutes = package.DurationMinutes;
existingPackage.Description = package.Description;
existingPackage.FirstSprayWaterTime = package.FirstSprayWaterTime;
existingPackage.AfterShampoo1SprayTime = package.AfterShampoo1SprayTime;
existingPackage.AfterShampoo2SprayTime = package.AfterShampoo2SprayTime;
existingPackage.AfterShampoo3SprayTime = package.AfterShampoo3SprayTime;
existingPackage.SprayShampoo1Time = package.SprayShampoo1Time;
existingPackage.SprayShampoo2Time = package.SprayShampoo2Time;
existingPackage.SprayShampoo3Time = package.SprayShampoo3Time;
existingPackage.HotAirTime = package.HotAirTime;
existingPackage.ColdAirTime = package.ColdAirTime;
existingPackage.UvSterilizationTime = package.UvSterilizationTime;
}
}
db.SaveChanges();
}
catch (Exception ex)
{
logger.LogError(ex, "数据库初始化失败");
logger.LogError(ex, "Database initialization failed.");
throw;
}
}
// 启用 Swagger所有环境
app.UseSwagger();
app.UseSwaggerUI();
@@ -90,3 +133,56 @@ app.UseAuthorization();
app.MapControllers();
app.Run();
static void EnsurePackageTimingColumns(PetWashDbContext db, ILogger logger)
{
var providerName = db.Database.ProviderName ?? string.Empty;
var isSqlite = providerName.Contains("Sqlite", StringComparison.OrdinalIgnoreCase);
var missingColumns = GetMissingPackageTimingColumns(db.Database.GetDbConnection(), isSqlite);
foreach (var column in missingColumns)
{
var sql = isSqlite
? $"ALTER TABLE Packages ADD COLUMN {column.Name} INTEGER NOT NULL DEFAULT {column.DefaultValue};"
: $"ALTER TABLE Packages ADD COLUMN {column.Name} INT NOT NULL DEFAULT {column.DefaultValue};";
db.Database.ExecuteSqlRaw(sql);
logger.LogInformation("Added missing package timing column {ColumnName}", column.Name);
}
}
static List<(string Name, int DefaultValue)> GetMissingPackageTimingColumns(DbConnection connection, bool isSqlite)
{
var expectedColumns = new List<(string Name, int DefaultValue)>
{
("FirstSprayWaterTime", 2),
("AfterShampoo1SprayTime", 2),
("AfterShampoo2SprayTime", 2),
("AfterShampoo3SprayTime", 2),
("SprayShampoo1Time", 1),
("SprayShampoo2Time", 1),
("SprayShampoo3Time", 1),
("HotAirTime", 5),
("ColdAirTime", 2),
("UvSterilizationTime", 2)
};
if (connection.State != ConnectionState.Open)
{
connection.Open();
}
using var command = connection.CreateCommand();
command.CommandText = isSqlite ? "PRAGMA table_info(Packages);" : "SHOW COLUMNS FROM Packages;";
using var reader = command.ExecuteReader();
var existingColumns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
while (reader.Read())
{
existingColumns.Add(reader.GetString(isSqlite ? 1 : 0));
}
return expectedColumns
.Where(column => !existingColumns.Contains(column.Name))
.ToList();
}

View File

@@ -12,13 +12,13 @@
},
"DatabaseProvider": "Sqlite",
"WeChatPay": {
"AppId": "",
"AppId": "wxa27a3e3cfce7ae19",
"MerchantId": "1107066208",
"CertificateSerialNumber": "",
"CertificateSerialNumber": "3243AE8427384A692FBAA92C5EC5887BEF1988FD",
"PrivateKeyPath": "",
"PrivateKeyPem": "",
"NotifyUrl": "",
"ApiV3Key": "",
"ApiV3Key": "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDMlj1LkO9Cfg3LWBYpe9GBn7vWgLE6kqEG1ohaxbaPxA6OwuGn0XQZfRBbJmncSXGLYahQ7T0OvFBIp8SyYm6q9kol8c9naxd+KxjMrx/qSWqwEJ76meBNK6LBYBVFTobg47cexpyR1TOZK0EFBGJQU2yQ1nsuQczVvq+WaSn4+kVENWf+o2g2nFS1VXNBIjL0/C8vXbz/0Y8k6ecH5mbmy/t+YR6X4TsiIAzIxIcfMMNhVCwqKLsu3D20N0ViYbKToHWIXi8wS8dyruHqQ1lZVJV/fF7pdI36HFI94enksCZrDb1LVFjL+4ccE04MJLIEZSH73RrOFkLaRzn8pwBbAgMBAAECggEAY7kD7baa+XVKMgkg3F2vVJjQzZDzUpKwjQ27b0uaXl95nRrfNZcCGX59n4CM70SZZRBYJAJP1cP",
"PlatformPublicKeyPath": "",
"PlatformPublicKeyPem": "",
"PlatformPublicKeySerial": "",

View File

@@ -7,4 +7,49 @@ public class Package
public decimal Price { get; set; }
public int DurationMinutes { get; set; }
public string Description { get; set; } = string.Empty;
public int FirstSprayWaterTime { get; set; }
public int AfterShampoo1SprayTime { get; set; }
public int AfterShampoo2SprayTime { get; set; }
public int AfterShampoo3SprayTime { get; set; }
public int SprayShampoo1Time { get; set; }
public int SprayShampoo2Time { get; set; }
public int SprayShampoo3Time { get; set; }
public int HotAirTime { get; set; }
public int ColdAirTime { get; set; }
public int UvSterilizationTime { get; set; }
public string DisplayPrice => Price.ToString("F2");
public string DisplayDuration => $"{DurationMinutes} 分钟";
public string DisplaySummary => $"{Description} | 总时长 {DurationMinutes} 分钟";
public string DisplayTag => Id switch
{
1 => "小型犬",
2 => "中型犬",
3 => "大型犬",
_ => "宠物套餐"
};
public string CardBackground => Id switch
{
1 => "#2E7D32",
2 => "#1565C0",
3 => "#6A1B9A",
_ => "#D84315"
};
public string CardDetailBackground => Id switch
{
1 => "#1B5E20",
2 => "#0D47A1",
3 => "#4A148C",
_ => "#BF360C"
};
public string CardPriceBackground => Id switch
{
1 => "#43A047",
2 => "#1E88E5",
3 => "#8E24AA",
_ => "#FF6F00"
};
}

View File

@@ -27,4 +27,10 @@
<Resource Include="Images\qrcode.png" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,70 @@
namespace PetWashControl.Services;
public sealed class AdminAccessService
{
private readonly ConfigurationService _config;
private int _failedAttempts;
private DateTimeOffset? _lockedUntil;
public AdminAccessService(ConfigurationService config)
{
_config = config;
}
public AdminAuthResult TryAuthenticate(string username, string password)
{
if (_lockedUntil is { } lockedUntil && lockedUntil > DateTimeOffset.Now)
{
return AdminAuthResult.Locked(GetLockoutMessage(lockedUntil));
}
if (username == _config.AdminUsername && password == _config.AdminPassword)
{
_failedAttempts = 0;
_lockedUntil = null;
return AdminAuthResult.Success();
}
_failedAttempts++;
var maxAttempts = Math.Max(1, _config.AdminMaxFailedAttempts);
if (_failedAttempts >= maxAttempts)
{
_failedAttempts = 0;
_lockedUntil = DateTimeOffset.Now.AddMinutes(Math.Max(1, _config.AdminLockoutMinutes));
return AdminAuthResult.Locked(GetLockoutMessage(_lockedUntil.Value));
}
var remainingAttempts = maxAttempts - _failedAttempts;
return AdminAuthResult.Failed($"账号或密码错误,还可尝试 {remainingAttempts} 次。");
}
public string? GetCurrentLockoutMessage()
{
if (_lockedUntil is not { } lockedUntil || lockedUntil <= DateTimeOffset.Now)
{
_lockedUntil = null;
return null;
}
return GetLockoutMessage(lockedUntil);
}
private static string GetLockoutMessage(DateTimeOffset lockedUntil)
{
var remaining = lockedUntil - DateTimeOffset.Now;
if (remaining < TimeSpan.Zero)
{
remaining = TimeSpan.Zero;
}
var minutes = Math.Max(0, (int)Math.Ceiling(remaining.TotalMinutes));
return $"登录失败次数过多,请 {minutes} 分钟后再试。";
}
}
public readonly record struct AdminAuthResult(bool IsSuccess, bool IsLocked, string Message)
{
public static AdminAuthResult Success() => new(true, false, string.Empty);
public static AdminAuthResult Failed(string message) => new(false, false, message);
public static AdminAuthResult Locked(string message) => new(false, true, message);
}

View File

@@ -98,4 +98,30 @@ public class ApiService
throw new Exception($"更新订单状态失败: {ex.Message}", ex);
}
}
public async Task<Package?> UpdatePackageAsync(Package package)
{
try
{
var response = await _httpClient.PutAsJsonAsync($"api/packages/{package.Id}", new
{
package.Price,
package.FirstSprayWaterTime,
package.AfterShampoo1SprayTime,
package.AfterShampoo2SprayTime,
package.AfterShampoo3SprayTime,
package.SprayShampoo1Time,
package.SprayShampoo2Time,
package.SprayShampoo3Time,
package.HotAirTime,
package.ColdAirTime,
package.UvSterilizationTime
});
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<Package>();
}
catch (HttpRequestException ex)
{
throw new Exception($"保存套餐配置失败: {ex.Message}", ex);
}
}
}

View File

@@ -1,34 +1,176 @@
using System.IO;
using System.Text.Json;
namespace PetWashControl.Services;
public class ConfigurationService
{
// https://localhost:7203/ 本地
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
private static readonly string ConfigFilePath = Path.Combine(AppContext.BaseDirectory, "appsettings.json");
public string ApiBaseUrl { get; set; } = "http://101.132.182.216:8080/";
public string MqttBrokerHost { get; set; } = "101.132.182.216";
public int MqttBrokerPort { get; set; } = 1883;
public string MqttApiKey { get; set; } = "dc240ab5ec";
public string MqttId { get; set; } = "13064";
public string MqttClientId { get; set; } = "PetWashControl";
// Modbus TCP 配置
public string ModbusIpAddress { get; set; } = "127.0.0.1";
public int ModbusPort { get; set; } = 502;
public byte ModbusSlaveId { get; set; } = 1;
public int ModbusConnectTimeoutMs { get; set; } = 5000;
public int ModbusReadTimeoutMs { get; set; } = 3000;
public int PaymentCheckIntervalSeconds { get; set; } = 2;
public int WashSimulationSeconds { get; set; } = 10;
// 洗护流程时间参数(单位:分钟)
public int FirstSprayWaterTime { get; set; } = 2; // 首次喷水时间
public int AfterShampoo1SprayTime { get; set; } = 2; // 沐浴1后喷水时间
public int AfterShampoo2SprayTime { get; set; } = 2; // 沐浴2后喷水时间
public int AfterShampoo3SprayTime { get; set; } = 2; // 沐浴3后喷水时间
public int SprayShampoo1Time { get; set; } = 1; // 喷沐浴露1时间
public int SprayShampoo2Time { get; set; } = 1; // 喷沐浴露2时间
public int SprayShampoo3Time { get; set; } = 1; // 喷沐浴露3时间
public int ColdAirTime { get; set; } = 2; // 冷风机时间
public int HotAirTime { get; set; } = 5; // 热风机时间
public int UvSterilizationTime { get; set; } = 3; // 紫外线杀菌时间
public int FirstSprayWaterTime { get; set; } = 2;
public int AfterShampoo1SprayTime { get; set; } = 2;
public int AfterShampoo2SprayTime { get; set; } = 2;
public int AfterShampoo3SprayTime { get; set; } = 2;
public int SprayShampoo1Time { get; set; } = 1;
public int SprayShampoo2Time { get; set; } = 1;
public int SprayShampoo3Time { get; set; } = 1;
public int ColdAirTime { get; set; } = 2;
public int HotAirTime { get; set; } = 5;
public int UvSterilizationTime { get; set; } = 3;
public string AdminUsername { get; set; } = "admin";
public string AdminPassword { get; set; } = "123456";
public int AdminMaxFailedAttempts { get; set; } = 5;
public int AdminLockoutMinutes { get; set; } = 15;
public ConfigurationService()
{
Load();
}
public void Save()
{
var fileContent = JsonSerializer.Serialize(ToModel(), JsonOptions);
File.WriteAllText(ConfigFilePath, fileContent);
}
private void Load()
{
if (!File.Exists(ConfigFilePath))
{
Save();
return;
}
try
{
var fileContent = File.ReadAllText(ConfigFilePath);
var model = JsonSerializer.Deserialize<ConfigurationModel>(fileContent, JsonOptions);
if (model == null)
{
Save();
return;
}
ApplyModel(model);
}
catch
{
Save();
}
}
private void ApplyModel(ConfigurationModel model)
{
ApiBaseUrl = model.ApiBaseUrl;
MqttBrokerHost = model.MqttBrokerHost;
MqttBrokerPort = model.MqttBrokerPort;
MqttApiKey = model.MqttApiKey;
MqttId = model.MqttId;
MqttClientId = model.MqttClientId;
ModbusIpAddress = model.ModbusIpAddress;
ModbusPort = model.ModbusPort;
ModbusSlaveId = model.ModbusSlaveId;
ModbusConnectTimeoutMs = model.ModbusConnectTimeoutMs;
ModbusReadTimeoutMs = model.ModbusReadTimeoutMs;
PaymentCheckIntervalSeconds = model.PaymentCheckIntervalSeconds;
WashSimulationSeconds = model.WashSimulationSeconds;
FirstSprayWaterTime = model.FirstSprayWaterTime;
AfterShampoo1SprayTime = model.AfterShampoo1SprayTime;
AfterShampoo2SprayTime = model.AfterShampoo2SprayTime;
AfterShampoo3SprayTime = model.AfterShampoo3SprayTime;
SprayShampoo1Time = model.SprayShampoo1Time;
SprayShampoo2Time = model.SprayShampoo2Time;
SprayShampoo3Time = model.SprayShampoo3Time;
ColdAirTime = model.ColdAirTime;
HotAirTime = model.HotAirTime;
UvSterilizationTime = model.UvSterilizationTime;
AdminUsername = model.AdminUsername;
AdminPassword = model.AdminPassword;
AdminMaxFailedAttempts = Math.Max(1, model.AdminMaxFailedAttempts);
AdminLockoutMinutes = Math.Max(1, model.AdminLockoutMinutes);
}
private ConfigurationModel ToModel()
{
return new ConfigurationModel
{
ApiBaseUrl = ApiBaseUrl,
MqttBrokerHost = MqttBrokerHost,
MqttBrokerPort = MqttBrokerPort,
MqttApiKey = MqttApiKey,
MqttId = MqttId,
MqttClientId = MqttClientId,
ModbusIpAddress = ModbusIpAddress,
ModbusPort = ModbusPort,
ModbusSlaveId = ModbusSlaveId,
ModbusConnectTimeoutMs = ModbusConnectTimeoutMs,
ModbusReadTimeoutMs = ModbusReadTimeoutMs,
PaymentCheckIntervalSeconds = PaymentCheckIntervalSeconds,
WashSimulationSeconds = WashSimulationSeconds,
FirstSprayWaterTime = FirstSprayWaterTime,
AfterShampoo1SprayTime = AfterShampoo1SprayTime,
AfterShampoo2SprayTime = AfterShampoo2SprayTime,
AfterShampoo3SprayTime = AfterShampoo3SprayTime,
SprayShampoo1Time = SprayShampoo1Time,
SprayShampoo2Time = SprayShampoo2Time,
SprayShampoo3Time = SprayShampoo3Time,
ColdAirTime = ColdAirTime,
HotAirTime = HotAirTime,
UvSterilizationTime = UvSterilizationTime,
AdminUsername = AdminUsername,
AdminPassword = AdminPassword,
AdminMaxFailedAttempts = Math.Max(1, AdminMaxFailedAttempts),
AdminLockoutMinutes = Math.Max(1, AdminLockoutMinutes)
};
}
private sealed class ConfigurationModel
{
public string ApiBaseUrl { get; set; } = "http://101.132.182.216:8080/";
public string MqttBrokerHost { get; set; } = "101.132.182.216";
public int MqttBrokerPort { get; set; } = 1883;
public string MqttApiKey { get; set; } = "dc240ab5ec";
public string MqttId { get; set; } = "13064";
public string MqttClientId { get; set; } = "PetWashControl";
public string ModbusIpAddress { get; set; } = "127.0.0.1";
public int ModbusPort { get; set; } = 502;
public byte ModbusSlaveId { get; set; } = 1;
public int ModbusConnectTimeoutMs { get; set; } = 5000;
public int ModbusReadTimeoutMs { get; set; } = 3000;
public int PaymentCheckIntervalSeconds { get; set; } = 2;
public int WashSimulationSeconds { get; set; } = 10;
public int FirstSprayWaterTime { get; set; } = 2;
public int AfterShampoo1SprayTime { get; set; } = 2;
public int AfterShampoo2SprayTime { get; set; } = 2;
public int AfterShampoo3SprayTime { get; set; } = 2;
public int SprayShampoo1Time { get; set; } = 1;
public int SprayShampoo2Time { get; set; } = 1;
public int SprayShampoo3Time { get; set; } = 1;
public int ColdAirTime { get; set; } = 2;
public int HotAirTime { get; set; } = 5;
public int UvSterilizationTime { get; set; } = 3;
public string AdminUsername { get; set; } = "admin";
public string AdminPassword { get; set; } = "123456";
public int AdminMaxFailedAttempts { get; set; } = 5;
public int AdminLockoutMinutes { get; set; } = 15;
}
}

View File

@@ -25,6 +25,18 @@ public partial class WashStep : ObservableObject
private bool _isActive;
}
public sealed record PackageTimingProfile(
int FirstSprayWaterTime,
int AfterShampoo1SprayTime,
int AfterShampoo2SprayTime,
int AfterShampoo3SprayTime,
int SprayShampoo1Time,
int SprayShampoo2Time,
int SprayShampoo3Time,
int HotAirTime,
int ColdAirTime,
int UvSterilizationTime);
public partial class MainViewModel : ObservableObject
{
private readonly ApiService _apiService;
@@ -34,6 +46,7 @@ public partial class MainViewModel : ObservableObject
private readonly LogService _logger;
public event Action<string>? ViewChanged;
public event Action? OpenPackageManagementRequested;
[ObservableProperty]
private ObservableCollection<Package> _packages = new();
@@ -121,6 +134,42 @@ public partial class MainViewModel : ObservableObject
[ObservableProperty]
private bool _isCustomerServiceDialogOpen;
[ObservableProperty]
private Package? _editingPackage;
[ObservableProperty]
private decimal _editingPackagePrice = 0.01m;
[ObservableProperty]
private int _editingPackageFirstSprayWaterTime = 2;
[ObservableProperty]
private int _editingPackageAfterShampoo1SprayTime = 2;
[ObservableProperty]
private int _editingPackageAfterShampoo2SprayTime = 2;
[ObservableProperty]
private int _editingPackageAfterShampoo3SprayTime = 2;
[ObservableProperty]
private int _editingPackageSprayShampoo1Time = 1;
[ObservableProperty]
private int _editingPackageSprayShampoo2Time = 1;
[ObservableProperty]
private int _editingPackageSprayShampoo3Time = 1;
[ObservableProperty]
private int _editingPackageColdAirTime = 2;
[ObservableProperty]
private int _editingPackageHotAirTime = 5;
[ObservableProperty]
private int _editingPackageUvSterilizationTime = 2;
[ObservableProperty]
private int _firstSprayWaterTime = 2;
@@ -179,6 +228,18 @@ public partial class MainViewModel : ObservableObject
private bool _isPaymentPolling;
private bool _isCheckingPaymentStatus;
public int EditingPackageTotalDuration =>
EditingPackageFirstSprayWaterTime +
EditingPackageAfterShampoo1SprayTime +
EditingPackageAfterShampoo2SprayTime +
EditingPackageAfterShampoo3SprayTime +
EditingPackageSprayShampoo1Time +
EditingPackageSprayShampoo2Time +
EditingPackageSprayShampoo3Time +
EditingPackageHotAirTime +
EditingPackageColdAirTime +
EditingPackageUvSterilizationTime;
public MainViewModel()
{
_config = new ConfigurationService();
@@ -458,6 +519,11 @@ public partial class MainViewModel : ObservableObject
_logger.LogInfo($"切换到设置界面,之前的视图: {_previousView}");
}
public void ShowSettingsFromAdmin()
{
ShowSettings();
}
[RelayCommand]
private void ShowInstruction()
{
@@ -504,6 +570,7 @@ public partial class MainViewModel : ObservableObject
_config.ColdAirTime = ColdAirTime;
_config.HotAirTime = HotAirTime;
_config.UvSterilizationTime = UvSterilizationTime;
_config.Save();
_logger.LogInfo($"本地参数已更新 - 首次喷水:{FirstSprayWaterTime}min, 沐浴1后:{AfterShampoo1SprayTime}min, " +
$"沐浴2后:{AfterShampoo2SprayTime}min, 沐浴3后:{AfterShampoo3SprayTime}min, " +
@@ -570,6 +637,75 @@ public partial class MainViewModel : ObservableObject
}
}
private PackageTimingProfile GetPackageTimingProfile(Package package)
{
if (package.FirstSprayWaterTime > 0 &&
package.AfterShampoo1SprayTime > 0 &&
package.AfterShampoo2SprayTime > 0 &&
package.AfterShampoo3SprayTime > 0 &&
package.SprayShampoo1Time > 0 &&
package.SprayShampoo2Time > 0 &&
package.SprayShampoo3Time > 0 &&
package.HotAirTime > 0 &&
package.ColdAirTime > 0 &&
package.UvSterilizationTime > 0)
{
return new PackageTimingProfile(
package.FirstSprayWaterTime,
package.AfterShampoo1SprayTime,
package.AfterShampoo2SprayTime,
package.AfterShampoo3SprayTime,
package.SprayShampoo1Time,
package.SprayShampoo2Time,
package.SprayShampoo3Time,
package.HotAirTime,
package.ColdAirTime,
package.UvSterilizationTime);
}
return new PackageTimingProfile(
_config.FirstSprayWaterTime,
_config.AfterShampoo1SprayTime,
_config.AfterShampoo2SprayTime,
_config.AfterShampoo3SprayTime,
_config.SprayShampoo1Time,
_config.SprayShampoo2Time,
_config.SprayShampoo3Time,
_config.HotAirTime,
_config.ColdAirTime,
_config.UvSterilizationTime);
}
private void ApplyPackageTimingProfile(PackageTimingProfile profile)
{
FirstSprayWaterTime = profile.FirstSprayWaterTime;
AfterShampoo1SprayTime = profile.AfterShampoo1SprayTime;
AfterShampoo2SprayTime = profile.AfterShampoo2SprayTime;
AfterShampoo3SprayTime = profile.AfterShampoo3SprayTime;
SprayShampoo1Time = profile.SprayShampoo1Time;
SprayShampoo2Time = profile.SprayShampoo2Time;
SprayShampoo3Time = profile.SprayShampoo3Time;
HotAirTime = profile.HotAirTime;
ColdAirTime = profile.ColdAirTime;
UvSterilizationTime = profile.UvSterilizationTime;
}
private async Task ApplyPackageTimingProfileAsync(Package package)
{
var profile = GetPackageTimingProfile(package);
ApplyPackageTimingProfile(profile);
_logger.LogInfo(
$"套餐参数已切换: PackageId={package.Id}, FirstSpray={FirstSprayWaterTime}, Shampoo1={SprayShampoo1Time}, " +
$"Rinse1={AfterShampoo1SprayTime}, Shampoo2={SprayShampoo2Time}, Rinse2={AfterShampoo2SprayTime}, " +
$"Conditioner={SprayShampoo3Time}, Rinse3={AfterShampoo3SprayTime}, HotAir={HotAirTime}, ColdAir={ColdAirTime}, UV={UvSterilizationTime}");
if (IsModbusConnected)
{
await SaveParametersToDeviceAsync();
}
}
// 参数调整命令
[RelayCommand]
private void IncreaseFirstSprayWater() => FirstSprayWaterTime = Math.Min(FirstSprayWaterTime + 1, 60);
@@ -695,30 +831,7 @@ public partial class MainViewModel : ObservableObject
try
{
// 创建订单
var createOrderResponse = await _apiService.CreateOrderAsync(package.Id);
CurrentOrder = createOrderResponse?.Order;
PaymentCodeUrl = createOrderResponse?.CodeUrl ?? "";
PaymentOutTradeNo = createOrderResponse?.OutTradeNo ?? "";
PaymentExpiresAt = createOrderResponse?.ExpiresAt;
IsPaymentExpired = false;
PaymentStatusText = "等待扫码支付";
RefreshPaymentCountdown();
PaymentQrCodeImage = string.IsNullOrWhiteSpace(PaymentCodeUrl)
? null
: BuildPaymentQrCodeImage(PaymentCodeUrl);
if (CurrentOrder == null || PaymentQrCodeImage == null || string.IsNullOrWhiteSpace(PaymentOutTradeNo))
{
throw new InvalidOperationException("Payment QR code was not returned by the server.");
}
StatusMessage = $"订单创建成功,请扫码支付 ¥{package.Price}";
_logger.LogInfo($"订单创建成功订单ID: {CurrentOrder?.Id}");
// 切换到二维码支付界面
CurrentView = "QRCode";
ViewChanged?.Invoke("QRCode");
StartPaymentStatusPolling();
await CreatePaymentOrderAsync(package, true);
}
catch (Exception ex)
{
@@ -748,28 +861,11 @@ public partial class MainViewModel : ObservableObject
try
{
_logger.LogInfo($"创建订单套餐ID: {SelectedPackage.Id}");
var createOrderResponse = await _apiService.CreateOrderAsync(SelectedPackage.Id);
CurrentOrder = createOrderResponse?.Order;
PaymentCodeUrl = createOrderResponse?.CodeUrl ?? "";
PaymentOutTradeNo = createOrderResponse?.OutTradeNo ?? "";
PaymentExpiresAt = createOrderResponse?.ExpiresAt;
IsPaymentExpired = false;
PaymentStatusText = "等待扫码支付";
RefreshPaymentCountdown();
PaymentQrCodeImage = string.IsNullOrWhiteSpace(PaymentCodeUrl)
? null
: BuildPaymentQrCodeImage(PaymentCodeUrl);
if (CurrentOrder == null || PaymentQrCodeImage == null || string.IsNullOrWhiteSpace(PaymentOutTradeNo))
{
throw new InvalidOperationException("Payment QR code was not returned by the server.");
}
StatusMessage = $"订单创建成功,订单号: {CurrentOrder?.Id},请支付 ¥{SelectedPackage.Price}";
_logger.LogInfo($"订单创建成功订单ID: {CurrentOrder?.Id}");
await CreatePaymentOrderAsync(SelectedPackage, false);
}
catch (Exception ex)
{
StopPaymentStatusPolling();
_logger.LogError("创建订单失败", ex);
StatusMessage = $"创建订单失败: {ex.Message}";
MessageBox.Show($"创建订单失败\n\n{ex.Message}", "错误",
@@ -808,6 +904,71 @@ public partial class MainViewModel : ObservableObject
}
}
[RelayCommand]
private async Task RegeneratePaymentQrAsync()
{
if (SelectedPackage == null)
{
return;
}
try
{
StopPaymentStatusPolling();
PaymentStatusText = "正在刷新二维码...";
StatusMessage = "正在刷新二维码...";
if (CurrentOrder != null && !CurrentOrder.IsPaid)
{
await _apiService.UpdateOrderStatusAsync(CurrentOrder.Id, OrderStatus.Cancelled);
}
await CreatePaymentOrderAsync(SelectedPackage, true);
}
catch (Exception ex)
{
_logger.LogError("刷新支付二维码失败", ex);
PaymentStatusText = "二维码刷新失败";
StatusMessage = $"二维码刷新失败: {ex.Message}";
MessageBox.Show($"二维码刷新失败\n\n{ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private async Task CreatePaymentOrderAsync(Package package, bool navigateToQrCode)
{
_logger.LogInfo($"创建订单套餐ID: {package.Id}");
await ApplyPackageTimingProfileAsync(package);
var createOrderResponse = await _apiService.CreateOrderAsync(package.Id);
CurrentOrder = createOrderResponse?.Order;
PaymentCodeUrl = createOrderResponse?.CodeUrl ?? "";
PaymentOutTradeNo = createOrderResponse?.OutTradeNo ?? "";
PaymentExpiresAt = createOrderResponse?.ExpiresAt;
IsPaymentExpired = false;
PaymentStatusText = "等待扫码支付";
RefreshPaymentCountdown();
PaymentQrCodeImage = string.IsNullOrWhiteSpace(PaymentCodeUrl)
? null
: BuildPaymentQrCodeImage(PaymentCodeUrl);
if (CurrentOrder == null || PaymentQrCodeImage == null || string.IsNullOrWhiteSpace(PaymentOutTradeNo))
{
throw new InvalidOperationException("Payment QR code was not returned by the server.");
}
StatusMessage = $"订单创建成功,请扫码支付 ¥{package.DisplayPrice}";
_logger.LogInfo($"订单创建成功订单ID: {CurrentOrder?.Id}");
if (navigateToQrCode)
{
CurrentView = "QRCode";
ViewChanged?.Invoke("QRCode");
}
StartPaymentStatusPolling();
}
private static BitmapImage BuildPaymentQrCodeImage(string codeUrl)
{
using var generator = new QRCodeGenerator();
@@ -1379,6 +1540,154 @@ public partial class MainViewModel : ObservableObject
});
}
[RelayCommand]
private void OpenPackageManagement()
{
if (Packages.Count == 0)
{
StatusMessage = "套餐列表未加载";
return;
}
EditingPackage ??= Packages[0];
OpenPackageManagementRequested?.Invoke();
}
[RelayCommand]
private async Task SavePackageConfigAsync()
{
if (EditingPackage == null)
{
MessageBox.Show("请先选择要编辑的套餐。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
if (CurrentOrder != null && !CurrentOrder.IsPaid)
{
MessageBox.Show("当前有未完成支付订单,不能修改套餐配置。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
if (EditingPackagePrice <= 0 ||
EditingPackageFirstSprayWaterTime <= 0 ||
EditingPackageAfterShampoo1SprayTime <= 0 ||
EditingPackageAfterShampoo2SprayTime <= 0 ||
EditingPackageAfterShampoo3SprayTime <= 0 ||
EditingPackageSprayShampoo1Time <= 0 ||
EditingPackageSprayShampoo2Time <= 0 ||
EditingPackageSprayShampoo3Time <= 0 ||
EditingPackageHotAirTime <= 0 ||
EditingPackageColdAirTime <= 0 ||
EditingPackageUvSterilizationTime <= 0)
{
MessageBox.Show("套餐金额和每个流程时间都必须大于 0。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
try
{
StatusMessage = $"正在保存 {EditingPackage.Name} 配置...";
var packageToSave = new Package
{
Id = EditingPackage.Id,
Name = EditingPackage.Name,
Description = EditingPackage.Description,
Price = EditingPackagePrice,
DurationMinutes = EditingPackageTotalDuration,
FirstSprayWaterTime = EditingPackageFirstSprayWaterTime,
AfterShampoo1SprayTime = EditingPackageAfterShampoo1SprayTime,
AfterShampoo2SprayTime = EditingPackageAfterShampoo2SprayTime,
AfterShampoo3SprayTime = EditingPackageAfterShampoo3SprayTime,
SprayShampoo1Time = EditingPackageSprayShampoo1Time,
SprayShampoo2Time = EditingPackageSprayShampoo2Time,
SprayShampoo3Time = EditingPackageSprayShampoo3Time,
HotAirTime = EditingPackageHotAirTime,
ColdAirTime = EditingPackageColdAirTime,
UvSterilizationTime = EditingPackageUvSterilizationTime
};
var savedPackage = await _apiService.UpdatePackageAsync(packageToSave)
?? throw new InvalidOperationException("Server did not return the updated package.");
ReplacePackageInCollection(savedPackage);
EditingPackage = savedPackage;
if (SelectedPackage?.Id == savedPackage.Id)
{
SelectedPackage = savedPackage;
await ApplyPackageTimingProfileAsync(savedPackage);
}
if (CurrentOrder?.PackageId == savedPackage.Id && CurrentOrder.IsPaid)
{
CurrentOrder.Package = savedPackage;
}
StatusMessage = $"{savedPackage.Name} 配置已保存";
MessageBox.Show(
$"{savedPackage.Name} 已更新。\n\n金额: {savedPackage.DisplayPrice} 元\n总时长: {savedPackage.DurationMinutes} 分钟",
"保存成功",
MessageBoxButton.OK,
MessageBoxImage.Information);
}
catch (Exception ex)
{
_logger.LogError("保存套餐配置失败", ex);
StatusMessage = $"保存套餐配置失败: {ex.Message}";
MessageBox.Show($"保存套餐配置失败\n\n{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
partial void OnEditingPackageChanged(Package? value)
{
if (value != null)
{
LoadEditingPackage(value);
}
}
partial void OnEditingPackageFirstSprayWaterTimeChanged(int value) => OnPropertyChanged(nameof(EditingPackageTotalDuration));
partial void OnEditingPackageAfterShampoo1SprayTimeChanged(int value) => OnPropertyChanged(nameof(EditingPackageTotalDuration));
partial void OnEditingPackageAfterShampoo2SprayTimeChanged(int value) => OnPropertyChanged(nameof(EditingPackageTotalDuration));
partial void OnEditingPackageAfterShampoo3SprayTimeChanged(int value) => OnPropertyChanged(nameof(EditingPackageTotalDuration));
partial void OnEditingPackageSprayShampoo1TimeChanged(int value) => OnPropertyChanged(nameof(EditingPackageTotalDuration));
partial void OnEditingPackageSprayShampoo2TimeChanged(int value) => OnPropertyChanged(nameof(EditingPackageTotalDuration));
partial void OnEditingPackageSprayShampoo3TimeChanged(int value) => OnPropertyChanged(nameof(EditingPackageTotalDuration));
partial void OnEditingPackageHotAirTimeChanged(int value) => OnPropertyChanged(nameof(EditingPackageTotalDuration));
partial void OnEditingPackageColdAirTimeChanged(int value) => OnPropertyChanged(nameof(EditingPackageTotalDuration));
partial void OnEditingPackageUvSterilizationTimeChanged(int value) => OnPropertyChanged(nameof(EditingPackageTotalDuration));
private void ReplacePackageInCollection(Package updatedPackage)
{
for (var i = 0; i < Packages.Count; i++)
{
if (Packages[i].Id == updatedPackage.Id)
{
Packages[i] = updatedPackage;
return;
}
}
Packages.Add(updatedPackage);
}
private void LoadEditingPackage(Package package)
{
EditingPackagePrice = package.Price;
EditingPackageFirstSprayWaterTime = package.FirstSprayWaterTime;
EditingPackageAfterShampoo1SprayTime = package.AfterShampoo1SprayTime;
EditingPackageAfterShampoo2SprayTime = package.AfterShampoo2SprayTime;
EditingPackageAfterShampoo3SprayTime = package.AfterShampoo3SprayTime;
EditingPackageSprayShampoo1Time = package.SprayShampoo1Time;
EditingPackageSprayShampoo2Time = package.SprayShampoo2Time;
EditingPackageSprayShampoo3Time = package.SprayShampoo3Time;
EditingPackageHotAirTime = package.HotAirTime;
EditingPackageColdAirTime = package.ColdAirTime;
EditingPackageUvSterilizationTime = package.UvSterilizationTime;
OnPropertyChanged(nameof(EditingPackageTotalDuration));
}
private void UpdateTemperatures(string stepName, int currentTime, int totalTime)
{
// 根据不同步骤模拟温度变化

View File

@@ -0,0 +1,97 @@
<Window x:Class="PetWashControl.Views.AdminLoginWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="管理员验证"
Width="420"
Height="340"
WindowStartupLocation="CenterOwner"
ResizeMode="NoResize"
Background="#F5F7FA">
<Grid Margin="28">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="系统设置"
FontSize="30"
FontWeight="Bold"
Foreground="#1F2937"/>
<TextBlock Grid.Row="1"
Margin="0,8,0,0"
Text="请输入管理员账号和密码"
FontSize="15"
Foreground="#6B7280"/>
<StackPanel Grid.Row="2"
Margin="0,28,0,0">
<TextBlock Text="管理员账号"
FontSize="14"
Foreground="#374151"/>
<TextBox x:Name="UsernameTextBox"
Margin="0,8,0,0"
Height="42"
Padding="10"
FontSize="16"/>
</StackPanel>
<StackPanel Grid.Row="3"
Margin="0,18,0,0">
<TextBlock Text="管理员密码"
FontSize="14"
Foreground="#374151"/>
<PasswordBox x:Name="PasswordInput"
Margin="0,8,0,0"
Height="42"
Padding="10"
FontSize="16"
KeyDown="PasswordInput_KeyDown"/>
</StackPanel>
<Border Grid.Row="4"
x:Name="ErrorContainer"
Margin="0,18,0,0"
Background="#FEF2F2"
CornerRadius="12"
Padding="12"
Visibility="Collapsed">
<TextBlock x:Name="ErrorTextBlock"
Text="账号或密码错误"
Foreground="#B91C1C"
FontSize="14"/>
</Border>
<Grid Grid.Row="5"
Margin="0,24,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="12"/>
<ColumnDefinition Width="120"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1"
Content="取消"
Height="44"
Background="#CBD5E1"
Foreground="#0F172A"
BorderThickness="0"
IsCancel="True"
Click="CancelButton_Click"/>
<Button Grid.Column="3"
Content="确定"
Height="44"
Background="#1976D2"
Foreground="White"
BorderThickness="0"
IsDefault="True"
Click="LoginButton_Click"/>
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,69 @@
using PetWashControl.Services;
using System.Windows;
using System.Windows.Input;
namespace PetWashControl.Views;
public partial class AdminLoginWindow : Window
{
private readonly AdminAccessService _adminAccessService;
public bool IsAuthenticated { get; private set; }
public AdminLoginWindow(AdminAccessService adminAccessService)
{
_adminAccessService = adminAccessService;
InitializeComponent();
Loaded += (_, _) =>
{
UsernameTextBox.Focus();
ShowMessage(_adminAccessService.GetCurrentLockoutMessage());
};
}
private void LoginButton_Click(object sender, RoutedEventArgs e)
{
var result = _adminAccessService.TryAuthenticate(UsernameTextBox.Text, PasswordInput.Password);
if (result.IsSuccess)
{
IsAuthenticated = true;
DialogResult = true;
Close();
return;
}
IsAuthenticated = false;
ShowMessage(result.Message);
PasswordInput.Clear();
PasswordInput.Focus();
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
private void PasswordInput_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
LoginButton_Click(sender, new RoutedEventArgs());
}
}
private void ShowMessage(string? message)
{
if (string.IsNullOrWhiteSpace(message))
{
ErrorContainer.Visibility = Visibility.Collapsed;
ErrorTextBlock.Visibility = Visibility.Collapsed;
ErrorTextBlock.Text = string.Empty;
return;
}
ErrorContainer.Visibility = Visibility.Visible;
ErrorTextBlock.Visibility = Visibility.Visible;
ErrorTextBlock.Text = message;
}
}

View File

@@ -155,7 +155,7 @@
HorizontalAlignment="Right"
VerticalAlignment="Center"
Cursor="Hand"
Command="{Binding ShowSettingsCommand}">
Command="{Binding ShowSettingsCommand}" Visibility="Collapsed">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
@@ -451,6 +451,35 @@
</TextBlock>
<!-- 警告提示 -->
<Border Background="#E8F5E9"
CornerRadius="18"
Padding="18,14"
Margin="0,0,0,20">
<StackPanel>
<TextBlock Text="本次洗护套餐"
FontSize="15"
Foreground="#2E7D32"
HorizontalAlignment="Center"
Margin="0,0,0,6"/>
<TextBlock Text="{Binding SelectedPackage.Name}"
FontSize="22"
FontWeight="Bold"
Foreground="#1B5E20"
HorizontalAlignment="Center"
Margin="0,0,0,6"/>
<TextBlock Text="{Binding SelectedPackage.DisplaySummary}"
FontSize="14"
Foreground="#33691E"
HorizontalAlignment="Center"
TextAlignment="Center"
Margin="0,0,0,4"/>
<TextBlock HorizontalAlignment="Center">
<Run Text="支付金额 ¥" Foreground="#2E7D32" FontSize="14"/>
<Run Text="{Binding SelectedPackage.DisplayPrice}" Foreground="#2E7D32" FontSize="16" FontWeight="Bold"/>
</TextBlock>
</StackPanel>
</Border>
<Border Background="#FFF3E0"
CornerRadius="18"
Padding="20,15"
@@ -567,7 +596,7 @@
Height="70"
Margin="5"
Style="{StaticResource RoundButton}"
Command="{Binding ShowSettingsCommand}"/>
Command="{Binding ShowSettingsCommand}" Visibility="Collapsed"/>
</UniformGrid>
</Border>
</Grid>
@@ -649,6 +678,7 @@
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Style="{StaticResource PackageCard}"
Background="{Binding CardBackground}"
MouseLeftButtonDown="Package_Click">
<Grid>
<Grid.RowDefinitions>
@@ -667,8 +697,20 @@
Margin="0,0,0,15"/>
<!-- 适用犬型和时长 -->
<Border Grid.Row="0"
Background="#33FFFFFF"
CornerRadius="10"
Padding="10,4"
HorizontalAlignment="Right"
VerticalAlignment="Top">
<TextBlock Text="{Binding DisplayTag}"
FontSize="14"
FontWeight="SemiBold"
Foreground="White"/>
</Border>
<Border Grid.Row="1"
Background="#BF360C"
Background="{Binding CardDetailBackground}"
CornerRadius="10"
Padding="15,10"
Margin="0,0,0,15">
@@ -693,13 +735,13 @@
<!-- 价格 -->
<Border Grid.Row="2"
Background="#FF6F00"
Background="{Binding CardPriceBackground}"
CornerRadius="15"
Padding="20,12">
<TextBlock HorizontalAlignment="Center"
Foreground="White">
<Run Text="¥" FontSize="28" FontWeight="Bold"/>
<Run Text="{Binding Price}" FontSize="42" FontWeight="Bold"/>
<Run Text="{Binding DisplayPrice}" FontSize="42" FontWeight="Bold"/>
</TextBlock>
</Border>
</Grid>
@@ -786,7 +828,14 @@
Foreground="#2E7D32"
HorizontalAlignment="Center"
Margin="0,0,0,12"/>
<TextBlock Text="{Binding SelectedPackage.DisplaySummary}"
FontSize="15"
Foreground="#5D4037"
HorizontalAlignment="Center"
TextAlignment="Center"
Margin="0,0,0,16"/>
<!-- 支付金额 -->
<Border Background="#FFE0B2"
CornerRadius="15"
@@ -803,7 +852,7 @@
Foreground="#FF6F00"
FontSize="28"
FontWeight="Bold"/>
<Run Text="{Binding SelectedPackage.Price}"
<Run Text="{Binding SelectedPackage.DisplayPrice}"
Foreground="#FF6F00"
FontSize="44"
FontWeight="Bold"/>
@@ -856,6 +905,8 @@
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="15"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="15"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="0"
@@ -881,6 +932,28 @@
</Button>
<Button Grid.Column="2"
Content="刷新二维码"
Height="50"
FontSize="16"
FontWeight="Bold"
Background="#1976D2"
Foreground="White"
BorderThickness="0"
Cursor="Hand"
Command="{Binding RegeneratePaymentQrCommand}">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="12">
<Border.Effect>
<DropShadowEffect Color="#000000" BlurRadius="6" ShadowDepth="2" Opacity="0.3"/>
</Border.Effect>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
</Button>
<Button Grid.Column="4"
Content="取消支付"
Height="50"
FontSize="16"
@@ -948,7 +1021,7 @@
HorizontalAlignment="Right"
VerticalAlignment="Center"
Cursor="Hand"
Command="{Binding ShowSettingsCommand}">
Command="{Binding ShowSettingsCommand}" Visibility="Collapsed">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
@@ -1986,6 +2059,29 @@
</Grid>
<!-- 保存按钮 -->
<Button Content="保存设置"
FontSize="24"
FontWeight="Bold"
Height="60"
Background="#4CAF50"
Foreground="White"
BorderThickness="0"
Margin="0,20,0,0"
Cursor="Hand"
Command="{Binding OpenPackageManagementCommand}">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="15">
<Border.Effect>
<DropShadowEffect Color="#000000" BlurRadius="8" ShadowDepth="3" Opacity="0.3"/>
</Border.Effect>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Button.Template>
</Button>
<Button Content="保存设置"
FontSize="24"
FontWeight="Bold"
@@ -2016,6 +2112,13 @@
</Grid>
<!-- 使用说明弹窗 -->
<Border Width="120"
Height="120"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Background="Transparent"
MouseLeftButtonDown="AdminHotspot_MouseLeftButtonDown"/>
<Grid Visibility="{Binding IsInstructionDialogOpen, Converter={StaticResource BoolToVisibilityConverter}}"
Background="#80000000">
<Border Background="White"

View File

@@ -1,19 +1,26 @@
using PetWashControl.ViewModels;
using PetWashControl.Models;
using PetWashControl.Services;
using PetWashControl.ViewModels;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace PetWashControl.Views;
public partial class MainWindow : Window
{
private readonly MainViewModel _viewModel;
private readonly AdminAccessService _adminAccessService;
private PackageManagementWindow? _packageManagementWindow;
public MainWindow()
{
InitializeComponent();
_adminAccessService = new AdminAccessService(new ConfigurationService());
_viewModel = new MainViewModel();
_viewModel.ViewChanged += OnViewChanged;
_viewModel.OpenPackageManagementRequested += OnOpenPackageManagementRequested;
DataContext = _viewModel;
Loaded += MainWindow_Loaded;
}
@@ -21,18 +28,17 @@ public partial class MainWindow : Window
private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
await _viewModel.InitializeAsync();
ConfigureSettingsButtons();
}
private void OnViewChanged(string viewName)
{
// 隐藏所有视图
IdleView.Visibility = Visibility.Collapsed;
PaymentView.Visibility = Visibility.Collapsed;
QRCodeView.Visibility = Visibility.Collapsed;
WashingView.Visibility = Visibility.Collapsed;
SettingsView.Visibility = Visibility.Collapsed;
// 显示指定视图
switch (viewName)
{
case "Idle":
@@ -60,4 +66,73 @@ public partial class MainWindow : Window
_viewModel.SelectPackageCommand.Execute(package);
}
}
}
private void AdminHotspot_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.ClickCount < 2)
{
return;
}
var loginWindow = new AdminLoginWindow(_adminAccessService)
{
Owner = this
};
var dialogResult = loginWindow.ShowDialog();
if (dialogResult == true && loginWindow.IsAuthenticated)
{
_viewModel.ShowSettingsFromAdmin();
}
}
private void OnOpenPackageManagementRequested()
{
if (_packageManagementWindow?.IsVisible == true)
{
_packageManagementWindow.Activate();
return;
}
_packageManagementWindow = new PackageManagementWindow(_viewModel)
{
Owner = this
};
_packageManagementWindow.Closed += (_, _) => _packageManagementWindow = null;
_packageManagementWindow.ShowDialog();
}
private void ConfigureSettingsButtons()
{
foreach (var button in FindVisualChildren<Button>(SettingsView))
{
if (ReferenceEquals(button.Command, _viewModel.OpenPackageManagementCommand))
{
button.Content = "套餐管理";
button.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#1976D2"));
}
else if (ReferenceEquals(button.Command, _viewModel.SaveSettingsCommand))
{
button.Content = "保存设置";
button.Background = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#4CAF50"));
}
}
}
private static IEnumerable<T> FindVisualChildren<T>(DependencyObject root) where T : DependencyObject
{
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(root); i++)
{
var child = VisualTreeHelper.GetChild(root, i);
if (child is T typedChild)
{
yield return typedChild;
}
foreach (var nestedChild in FindVisualChildren<T>(child))
{
yield return nestedChild;
}
}
}
}

View File

@@ -0,0 +1,350 @@
<Window x:Class="PetWashControl.Views.PackageManagementWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="套餐管理"
Width="920"
Height="780"
WindowStartupLocation="CenterOwner"
ResizeMode="CanMinimize"
Background="#F3F6FB">
<Grid Margin="24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Border Grid.Row="0"
Background="#1976D2"
CornerRadius="20"
Padding="24"
Margin="0,0,0,20">
<StackPanel>
<TextBlock Text="套餐管理"
FontSize="28"
FontWeight="Bold"
Foreground="White"/>
<TextBlock Text="维护三套犬型套餐的金额和每个洗护流程时间,保存后前台下次选中套餐会自动写入寄存器。"
Margin="0,8,0,0"
FontSize="14"
Foreground="#DCEBFF"
TextWrapping="Wrap"/>
</StackPanel>
</Border>
<ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto">
<StackPanel>
<Border Background="White"
CornerRadius="18"
Padding="24"
Margin="0,0,0,20">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="选择套餐"
FontSize="16"
FontWeight="SemiBold"
Foreground="#1F2937"
Margin="0,0,0,10"/>
<ComboBox ItemsSource="{Binding Packages}"
SelectedItem="{Binding EditingPackage}"
DisplayMemberPath="Name"
FontSize="16"
Padding="10"
Height="42"/>
<Grid Margin="0,20,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0"
Background="#EEF7FF"
CornerRadius="14"
Padding="18">
<StackPanel>
<TextBlock Text="套餐金额"
FontSize="14"
Foreground="#4B5563"/>
<TextBox Text="{Binding EditingPackagePrice, UpdateSourceTrigger=LostFocus, StringFormat=F2}"
Margin="0,10,0,0"
Padding="10"
FontSize="22"
FontWeight="Bold"
BorderBrush="#1976D2"
BorderThickness="2"/>
<TextBlock Text="单位:元"
Margin="0,10,0,0"
FontSize="13"
Foreground="#6B7280"/>
</StackPanel>
</Border>
<Border Grid.Column="2"
Background="#F0FDF4"
CornerRadius="14"
Padding="18">
<StackPanel>
<TextBlock Text="套餐总时长"
FontSize="14"
Foreground="#4B5563"/>
<TextBlock Text="{Binding EditingPackageTotalDuration, StringFormat={}{0} 分钟}"
Margin="0,12,0,0"
FontSize="22"
FontWeight="Bold"
Foreground="#15803D"/>
<TextBlock Text="保存时自动同步到套餐总时长"
Margin="0,10,0,0"
FontSize="13"
Foreground="#6B7280"/>
</StackPanel>
</Border>
</Grid>
</StackPanel>
<Border Grid.Column="2"
Background="#FFF7ED"
CornerRadius="14"
Padding="18">
<StackPanel>
<TextBlock Text="保存说明"
FontSize="16"
FontWeight="SemiBold"
Foreground="#9A3412"/>
<TextBlock Text="1. 当前有未支付订单时禁止修改。"
Margin="0,14,0,0"
FontSize="14"
Foreground="#7C2D12"/>
<TextBlock Text="2. 总时长由各流程分钟数自动累加。"
Margin="0,8,0,0"
FontSize="14"
Foreground="#7C2D12"/>
<TextBlock Text="3. 若当前已选中这个套餐,保存后会立即重写当前设备流程参数。"
Margin="0,8,0,0"
FontSize="14"
Foreground="#7C2D12"
TextWrapping="Wrap"/>
</StackPanel>
</Border>
</Grid>
</Border>
<Border Background="White"
CornerRadius="18"
Padding="24">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="24"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="清洗流程"
FontSize="18"
FontWeight="Bold"
Foreground="#1F2937"
Margin="0,0,0,14"/>
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="140"/>
</Grid.ColumnDefinitions>
<TextBlock Text="首次冲水"
VerticalAlignment="Center"
FontSize="15"
Foreground="#374151"/>
<TextBox Grid.Column="1"
Text="{Binding EditingPackageFirstSprayWaterTime, UpdateSourceTrigger=LostFocus}"
Padding="10"
FontSize="15"/>
</Grid>
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="140"/>
</Grid.ColumnDefinitions>
<TextBlock Text="沐浴1后冲水"
VerticalAlignment="Center"
FontSize="15"
Foreground="#374151"/>
<TextBox Grid.Column="1"
Text="{Binding EditingPackageAfterShampoo1SprayTime, UpdateSourceTrigger=LostFocus}"
Padding="10"
FontSize="15"/>
</Grid>
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="140"/>
</Grid.ColumnDefinitions>
<TextBlock Text="沐浴2后冲水"
VerticalAlignment="Center"
FontSize="15"
Foreground="#374151"/>
<TextBox Grid.Column="1"
Text="{Binding EditingPackageAfterShampoo2SprayTime, UpdateSourceTrigger=LostFocus}"
Padding="10"
FontSize="15"/>
</Grid>
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="140"/>
</Grid.ColumnDefinitions>
<TextBlock Text="沐浴3后冲水"
VerticalAlignment="Center"
FontSize="15"
Foreground="#374151"/>
<TextBox Grid.Column="1"
Text="{Binding EditingPackageAfterShampoo3SprayTime, UpdateSourceTrigger=LostFocus}"
Padding="10"
FontSize="15"/>
</Grid>
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="140"/>
</Grid.ColumnDefinitions>
<TextBlock Text="沐浴液 1"
VerticalAlignment="Center"
FontSize="15"
Foreground="#374151"/>
<TextBox Grid.Column="1"
Text="{Binding EditingPackageSprayShampoo1Time, UpdateSourceTrigger=LostFocus}"
Padding="10"
FontSize="15"/>
</Grid>
</StackPanel>
<StackPanel Grid.Column="2">
<TextBlock Text="烘干与护理"
FontSize="18"
FontWeight="Bold"
Foreground="#1F2937"
Margin="0,0,0,14"/>
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="140"/>
</Grid.ColumnDefinitions>
<TextBlock Text="沐浴液 2"
VerticalAlignment="Center"
FontSize="15"
Foreground="#374151"/>
<TextBox Grid.Column="1"
Text="{Binding EditingPackageSprayShampoo2Time, UpdateSourceTrigger=LostFocus}"
Padding="10"
FontSize="15"/>
</Grid>
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="140"/>
</Grid.ColumnDefinitions>
<TextBlock Text="护理液"
VerticalAlignment="Center"
FontSize="15"
Foreground="#374151"/>
<TextBox Grid.Column="1"
Text="{Binding EditingPackageSprayShampoo3Time, UpdateSourceTrigger=LostFocus}"
Padding="10"
FontSize="15"/>
</Grid>
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="140"/>
</Grid.ColumnDefinitions>
<TextBlock Text="热风"
VerticalAlignment="Center"
FontSize="15"
Foreground="#374151"/>
<TextBox Grid.Column="1"
Text="{Binding EditingPackageHotAirTime, UpdateSourceTrigger=LostFocus}"
Padding="10"
FontSize="15"/>
</Grid>
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="140"/>
</Grid.ColumnDefinitions>
<TextBlock Text="冷风"
VerticalAlignment="Center"
FontSize="15"
Foreground="#374151"/>
<TextBox Grid.Column="1"
Text="{Binding EditingPackageColdAirTime, UpdateSourceTrigger=LostFocus}"
Padding="10"
FontSize="15"/>
</Grid>
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="140"/>
</Grid.ColumnDefinitions>
<TextBlock Text="紫外杀菌"
VerticalAlignment="Center"
FontSize="15"
Foreground="#374151"/>
<TextBox Grid.Column="1"
Text="{Binding EditingPackageUvSterilizationTime, UpdateSourceTrigger=LostFocus}"
Padding="10"
FontSize="15"/>
</Grid>
</StackPanel>
</Grid>
</Border>
</StackPanel>
</ScrollViewer>
<Grid Grid.Row="2"
Margin="0,20,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="160"/>
<ColumnDefinition Width="16"/>
<ColumnDefinition Width="160"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1"
Content="关闭"
Height="52"
FontSize="18"
FontWeight="SemiBold"
Background="#CBD5E1"
Foreground="#0F172A"
BorderThickness="0"
Click="CloseButton_Click"/>
<Button Grid.Column="3"
Content="保存套餐"
Height="52"
FontSize="18"
FontWeight="SemiBold"
Background="#16A34A"
Foreground="White"
BorderThickness="0"
Command="{Binding SavePackageConfigCommand}"/>
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,18 @@
using PetWashControl.ViewModels;
using System.Windows;
namespace PetWashControl.Views;
public partial class PackageManagementWindow : Window
{
public PackageManagementWindow(MainViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
Close();
}
}

View File

@@ -0,0 +1,29 @@
{
"apiBaseUrl": "http://101.132.182.216:8080/",
"mqttBrokerHost": "101.132.182.216",
"mqttBrokerPort": 1883,
"mqttApiKey": "dc240ab5ec",
"mqttId": "13064",
"mqttClientId": "PetWashControl",
"modbusIpAddress": "127.0.0.1",
"modbusPort": 502,
"modbusSlaveId": 1,
"modbusConnectTimeoutMs": 5000,
"modbusReadTimeoutMs": 3000,
"paymentCheckIntervalSeconds": 2,
"washSimulationSeconds": 10,
"firstSprayWaterTime": 2,
"afterShampoo1SprayTime": 2,
"afterShampoo2SprayTime": 2,
"afterShampoo3SprayTime": 2,
"sprayShampoo1Time": 1,
"sprayShampoo2Time": 1,
"sprayShampoo3Time": 1,
"coldAirTime": 2,
"hotAirTime": 5,
"uvSterilizationTime": 3,
"adminUsername": "admin",
"adminPassword": "123456",
"adminMaxFailedAttempts": 5,
"adminLockoutMinutes": 15
}

View File

@@ -1,24 +1,29 @@
-- PetWash 数据库初始化脚本
-- PetWash database initialization script
-- 删除已存在的数据库(如果存在)
DROP DATABASE IF EXISTS petwash;
-- 创建数据库
CREATE DATABASE petwash CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用数据库
USE petwash;
-- 创建套餐表
CREATE TABLE Packages (
Id INT AUTO_INCREMENT PRIMARY KEY,
Id INT PRIMARY KEY,
Name VARCHAR(100) NOT NULL,
Price DECIMAL(10, 2) NOT NULL,
DurationMinutes INT NOT NULL,
Description VARCHAR(500)
Description VARCHAR(500),
FirstSprayWaterTime INT NOT NULL,
AfterShampoo1SprayTime INT NOT NULL,
AfterShampoo2SprayTime INT NOT NULL,
AfterShampoo3SprayTime INT NOT NULL,
SprayShampoo1Time INT NOT NULL,
SprayShampoo2Time INT NOT NULL,
SprayShampoo3Time INT NOT NULL,
HotAirTime INT NOT NULL,
ColdAirTime INT NOT NULL,
UvSterilizationTime INT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 创建订单表
CREATE TABLE Orders (
Id INT AUTO_INCREMENT PRIMARY KEY,
PackageId INT NOT NULL,
@@ -31,12 +36,26 @@ CREATE TABLE Orders (
FOREIGN KEY (PackageId) REFERENCES Packages(Id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 插入初始套餐数据
INSERT INTO Packages (Id, Name, Price, DurationMinutes, Description) VALUES
(1, '套餐1', 50.00, 38, '适用于小型犬'),
(2, '套餐2', 80.00, 48, '适用于中型犬'),
(3, '套餐3', 120.00, 60, '适用于大型犬');
INSERT INTO Packages (
Id,
Name,
Price,
DurationMinutes,
Description,
FirstSprayWaterTime,
AfterShampoo1SprayTime,
AfterShampoo2SprayTime,
AfterShampoo3SprayTime,
SprayShampoo1Time,
SprayShampoo2Time,
SprayShampoo3Time,
HotAirTime,
ColdAirTime,
UvSterilizationTime
) VALUES
(1, '小型犬套餐', 0.01, 20, '适用于小型犬,洗护流程较短', 2, 2, 2, 2, 1, 1, 1, 5, 2, 2),
(2, '中型犬套餐', 0.02, 26, '适用于中型犬,洗护流程适中', 3, 3, 3, 3, 1, 1, 1, 6, 3, 2),
(3, '大型犬套餐', 0.03, 37, '适用于大型犬,洗护流程更长', 4, 4, 4, 4, 2, 2, 2, 8, 4, 3);
-- 显示创建结果
SELECT '数据库初始化完成' AS Status;
SELECT * FROM Packages;