diff --git a/Cardiopulmonarybypasssystems/App.xaml.cs b/Cardiopulmonarybypasssystems/App.xaml.cs index 557a18d..73eb7b7 100644 --- a/Cardiopulmonarybypasssystems/App.xaml.cs +++ b/Cardiopulmonarybypasssystems/App.xaml.cs @@ -19,14 +19,30 @@ public partial class App : Application var services = new ServiceCollection(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); services.AddSingleton(); services.AddSingleton(); _serviceProvider = services.BuildServiceProvider(); + var passwordAccessService = _serviceProvider.GetRequiredService(); + var passwordStatus = passwordAccessService.GetStatus(); + if (passwordStatus.ShouldPromptAtStartup) + { + var passwordWindow = _serviceProvider.GetRequiredService(); + var passwordResult = passwordWindow.ShowDialog(); + if (passwordResult != true || !passwordAccessService.GetStatus().CanLaunch) + { + Shutdown(); + return; + } + } + var mainWindow = _serviceProvider.GetRequiredService(); mainWindow.Show(); } diff --git a/Cardiopulmonarybypasssystems/Models/PasswordAccessStatus.cs b/Cardiopulmonarybypasssystems/Models/PasswordAccessStatus.cs new file mode 100644 index 0000000..5ea45c8 --- /dev/null +++ b/Cardiopulmonarybypasssystems/Models/PasswordAccessStatus.cs @@ -0,0 +1,16 @@ +namespace Cardiopulmonarybypasssystems.Models; + +public sealed class PasswordAccessStatus +{ + public int Stage { get; init; } + public bool CanLaunch { get; init; } + public bool IsPermanent { get; init; } + public bool ShouldPromptAtStartup { get; init; } + public bool CanSubmitPassword { get; init; } + public bool RequiresPassword => !CanLaunch; + public string StageText { get; init; } = string.Empty; + public string StatusTitle { get; init; } = string.Empty; + public string StatusDetail { get; init; } = string.Empty; + public int RemainingDays { get; init; } + public DateTime? ExpiresAt { get; init; } +} diff --git a/Cardiopulmonarybypasssystems/Services/IPasswordAccessService.cs b/Cardiopulmonarybypasssystems/Services/IPasswordAccessService.cs new file mode 100644 index 0000000..21c1d1f --- /dev/null +++ b/Cardiopulmonarybypasssystems/Services/IPasswordAccessService.cs @@ -0,0 +1,9 @@ +using Cardiopulmonarybypasssystems.Models; + +namespace Cardiopulmonarybypasssystems.Services; + +public interface IPasswordAccessService +{ + PasswordAccessStatus GetStatus(); + bool TryUnlock(string password, out PasswordAccessStatus status, out string message); +} diff --git a/Cardiopulmonarybypasssystems/Services/PasswordAccessService.cs b/Cardiopulmonarybypasssystems/Services/PasswordAccessService.cs new file mode 100644 index 0000000..228a274 --- /dev/null +++ b/Cardiopulmonarybypasssystems/Services/PasswordAccessService.cs @@ -0,0 +1,228 @@ +using System.IO; +using System.Text.Json; +using Cardiopulmonarybypasssystems.Models; + +namespace Cardiopulmonarybypasssystems.Services; + +public sealed class PasswordAccessService : IPasswordAccessService +{ + private const int ExpirationReminderDays = 3; + private const string SettingsDirectoryName = "Cardiopulmonarybypasssystems"; + private const string StatusFileName = "password-access.json"; + public const string FirstStagePassword = "152026"; + public const string SecondStagePassword = "302026"; + private const int FirstStageDays = 15; + private const int SecondStageDays = 30; + + private static readonly string StatusFilePath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + SettingsDirectoryName, + StatusFileName); + + public PasswordAccessStatus GetStatus() + { + var state = LoadState(); + return BuildStatus(state, persistState: true); + } + + public bool TryUnlock(string password, out PasswordAccessStatus status, out string message) + { + var normalizedPassword = password?.Trim() ?? string.Empty; + var state = LoadState(); + var currentStatus = BuildStatus(state, persistState: true); + + if (currentStatus.IsPermanent) + { + status = currentStatus; + message = "当前版本已永久有效,不需要再输入密码。"; + return true; + } + + if (currentStatus.Stage == 1 && currentStatus.CanLaunch) + { + status = currentStatus; + message = "第 1 阶段仍在有效期内,无需再次输入密码。"; + return true; + } + + if (currentStatus.Stage == 2 && currentStatus.CanLaunch) + { + status = currentStatus; + message = "第 2 阶段仍在有效期内,无需再次输入密码。"; + return true; + } + + var nextStage = currentStatus.Stage switch + { + <= 0 => 1, + 1 => 2, + _ => 0 + }; + + if (nextStage == 0) + { + status = BuildStatus(state, persistState: true); + message = "当前版本已转为永久有效,不再需要密码。"; + return true; + } + + var expectedPassword = nextStage == 1 ? FirstStagePassword : SecondStagePassword; + if (!string.Equals(normalizedPassword, expectedPassword, StringComparison.Ordinal)) + { + status = currentStatus; + message = $"密码错误,请输入第 {nextStage} 次时效密码。"; + return false; + } + + var now = DateTime.Now; + var expiresAt = now.AddDays(nextStage == 1 ? FirstStageDays : SecondStageDays); + var nextState = new PasswordAccessState + { + Stage = nextStage, + ActivatedAt = now, + ExpiresAt = expiresAt + }; + + SaveState(nextState); + status = BuildStatus(nextState, persistState: false); + message = nextStage == 1 + ? $"第 1 次时效已生效,有效期至 {expiresAt:yyyy-MM-dd HH:mm}。" + : $"第 2 次时效已生效,有效期至 {expiresAt:yyyy-MM-dd HH:mm};到期后自动转为永久有效。"; + return true; + } + + private static PasswordAccessStatus BuildStatus(PasswordAccessState? state, bool persistState) + { + if (state is null || state.Stage <= 0) + { + return new PasswordAccessStatus + { + Stage = 0, + CanLaunch = false, + IsPermanent = false, + ShouldPromptAtStartup = true, + CanSubmitPassword = true, + StageText = "当前阶段:未开始", + StatusTitle = "需要输入第 1 次时效密码", + StatusDetail = $"输入 15 天密码后进入系统。", + RemainingDays = 0 + }; + } + + if (state.Stage >= 3) + { + return new PasswordAccessStatus + { + Stage = 3, + CanLaunch = true, + IsPermanent = true, + ShouldPromptAtStartup = false, + CanSubmitPassword = false, + StageText = "当前阶段:永久有效", + StatusTitle = "系统已永久有效", + StatusDetail = "当前可直接进入系统。", + RemainingDays = int.MaxValue + }; + } + + var now = DateTime.Now; + if (state.ExpiresAt > now) + { + var remainingDays = Math.Max(1, (int)Math.Ceiling((state.ExpiresAt.Value - now).TotalDays)); + return new PasswordAccessStatus + { + Stage = state.Stage, + CanLaunch = true, + IsPermanent = false, + ShouldPromptAtStartup = state.Stage == 1 && remainingDays <= ExpirationReminderDays, + CanSubmitPassword = state.Stage == 1 && remainingDays <= ExpirationReminderDays, + StageText = state.Stage == 1 ? "当前阶段:第 1 次时效(15 天)" : "当前阶段:第 2 次时效(30 天)", + StatusTitle = state.Stage == 1 && remainingDays <= ExpirationReminderDays ? "第 1 次时效即将到期" : "时效有效", + StatusDetail = state.Stage == 1 && remainingDays <= ExpirationReminderDays + ? $"剩余 {remainingDays} 天,可提前输入 30 天密码。" + : $"当前可直接进入系统。", + RemainingDays = remainingDays, + ExpiresAt = state.ExpiresAt + }; + } + + if (state.Stage == 1) + { + return new PasswordAccessStatus + { + Stage = 1, + CanLaunch = false, + IsPermanent = false, + ShouldPromptAtStartup = true, + CanSubmitPassword = true, + StageText = "当前阶段:第 1 次时效已到期", + StatusTitle = "需要输入第 2 次时效密码", + StatusDetail = $"输入 30 天密码后继续使用。", + RemainingDays = 0, + ExpiresAt = state.ExpiresAt + }; + } + + var permanentState = new PasswordAccessState + { + Stage = 3, + ActivatedAt = state.ActivatedAt, + ExpiresAt = null + }; + + if (persistState) + { + SaveState(permanentState); + } + + return new PasswordAccessStatus + { + Stage = 3, + CanLaunch = true, + IsPermanent = true, + ShouldPromptAtStartup = false, + CanSubmitPassword = false, + StageText = "当前阶段:永久有效", + StatusTitle = "系统已永久有效", + StatusDetail = "当前可直接进入系统。", + RemainingDays = int.MaxValue + }; + } + + private static PasswordAccessState? LoadState() + { + try + { + if (!File.Exists(StatusFilePath)) + { + return null; + } + + var json = File.ReadAllText(StatusFilePath); + return JsonSerializer.Deserialize(json); + } + catch + { + return null; + } + } + + private static void SaveState(PasswordAccessState state) + { + var directory = Path.GetDirectoryName(StatusFilePath); + if (!string.IsNullOrWhiteSpace(directory)) + { + Directory.CreateDirectory(directory); + } + + var json = JsonSerializer.Serialize(state, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(StatusFilePath, json); + } + + private sealed class PasswordAccessState + { + public int Stage { get; set; } + public DateTime ActivatedAt { get; set; } + public DateTime? ExpiresAt { get; set; } + } +} diff --git a/Cardiopulmonarybypasssystems/StartupPasswordWindow.xaml b/Cardiopulmonarybypasssystems/StartupPasswordWindow.xaml new file mode 100644 index 0000000..cd009fb --- /dev/null +++ b/Cardiopulmonarybypasssystems/StartupPasswordWindow.xaml @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +