更新时效密码

This commit is contained in:
GukSang.Jin
2026-04-13 12:44:39 +08:00
parent 631af5f682
commit 183a7dd72e
7 changed files with 567 additions and 0 deletions

View File

@@ -19,14 +19,30 @@ public partial class App : Application
var services = new ServiceCollection();
services.AddSingleton<IStandardRepository, StandardRepository>();
services.AddSingleton<IPasswordAccessService, PasswordAccessService>();
services.AddSingleton<IModbusTelemetryService, ModbusTelemetryService>();
services.AddSingleton<IRs485PumpFlowService, Rs485PumpFlowService>();
services.AddSingleton<IPumpActuationService, Rs485PumpActuationService>();
services.AddTransient<StartupPasswordViewModel>();
services.AddTransient<StartupPasswordWindow>();
services.AddSingleton<MainViewModel>();
services.AddSingleton<MainWindow>();
_serviceProvider = services.BuildServiceProvider();
var passwordAccessService = _serviceProvider.GetRequiredService<IPasswordAccessService>();
var passwordStatus = passwordAccessService.GetStatus();
if (passwordStatus.ShouldPromptAtStartup)
{
var passwordWindow = _serviceProvider.GetRequiredService<StartupPasswordWindow>();
var passwordResult = passwordWindow.ShowDialog();
if (passwordResult != true || !passwordAccessService.GetStatus().CanLaunch)
{
Shutdown();
return;
}
}
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
mainWindow.Show();
}

View File

@@ -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; }
}

View File

@@ -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);
}

View File

@@ -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<PasswordAccessState>(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; }
}
}

View File

@@ -0,0 +1,137 @@
<Window x:Class="Cardiopulmonarybypasssystems.StartupPasswordWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="系统进入验证"
Width="640"
Height="500"
Loaded="Window_Loaded"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize">
<Grid Margin="18">
<Border CornerRadius="22"
Background="#FFF7FAFB"
BorderBrush="#FFD7E2E6"
BorderThickness="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="126" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border Grid.Row="0"
CornerRadius="22,22,0,0"
Padding="24,20">
<Border.Background>
<LinearGradientBrush StartPoint="0,0"
EndPoint="1,1">
<GradientStop Color="#FF123744"
Offset="0" />
<GradientStop Color="#FF0F5F75"
Offset="0.55" />
<GradientStop Color="#FF2F7D7A"
Offset="1" />
</LinearGradientBrush>
</Border.Background>
<StackPanel VerticalAlignment="Center">
<TextBlock FontSize="26"
FontWeight="Bold"
Foreground="White"
Text="系统进入验证" />
<Border Margin="0,10,0,0"
Padding="10,6"
HorizontalAlignment="Left"
Background="#1FFFFFFF"
CornerRadius="12">
<TextBlock FontSize="12"
FontWeight="SemiBold"
Foreground="White"
Text="{Binding StageText}" />
</Border>
</StackPanel>
</Border>
<StackPanel Grid.Row="1"
Margin="24,16,24,14">
<Border
Padding="14,10"
Background="#FFEAF3F5"
CornerRadius="14">
<DockPanel LastChildFill="False">
<StackPanel DockPanel.Dock="Left"
Orientation="Horizontal">
<TextBlock FontSize="12"
Foreground="#FF52636B"
Text="到期:" />
<TextBlock FontSize="13"
FontWeight="Bold"
Foreground="{StaticResource HeaderBrush}"
Text="{Binding ExpiresAtText}" />
</StackPanel>
<StackPanel DockPanel.Dock="Right"
Orientation="Horizontal">
<TextBlock FontSize="12"
Foreground="#FF52636B"
Text="剩余:" />
<TextBlock FontSize="13"
FontWeight="Bold"
Foreground="{StaticResource HeaderBrush}"
Text="{Binding RemainingText}" />
</StackPanel>
</DockPanel>
</Border>
<Border Margin="0,16,0,0"
Padding="16"
Background="White"
BorderBrush="#FFDCE7EA"
BorderThickness="1"
CornerRadius="16">
<StackPanel>
<TextBlock FontSize="12"
FontWeight="SemiBold"
Foreground="{StaticResource HeaderBrush}"
Text="密码输入" />
<TextBox x:Name="PasswordInput"
Margin="0,10,0,0"
MinHeight="38"
FontSize="15"
Text="{Binding PasswordText, UpdateSourceTrigger=PropertyChanged}"
TextChanged="PasswordInput_OnTextChanged" />
</StackPanel>
</Border>
</StackPanel>
<Border Grid.Row="2"
Padding="18,14"
Background="White"
BorderBrush="#FFE2EBEE"
BorderThickness="1,1,0,0"
CornerRadius="0,0,22,22">
<UniformGrid Columns="3">
<Button Width="112"
Margin="0,0,10,0"
Command="{Binding CloseCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=Window}}"
Background="#FF667E88"
Content="退出" />
<Button Width="112"
Margin="0,0,10,0"
Command="{Binding ContinueCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=Window}}"
Background="#FF2B8F6A"
Content="继续进入"
IsEnabled="{Binding CanContinue}" />
<Button Width="112"
Margin="0,0,10,0"
Command="{Binding SubmitCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=Window}}"
Background="#FF0F5F75"
Content="提交密码"
IsEnabled="{Binding CanSubmit}" />
</UniformGrid>
</Border>
</Grid>
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,29 @@
using System.Windows;
using System.Windows.Input;
using Cardiopulmonarybypasssystems.ViewModels;
namespace Cardiopulmonarybypasssystems;
public partial class StartupPasswordWindow : Window
{
private readonly StartupPasswordViewModel _viewModel;
public StartupPasswordWindow(StartupPasswordViewModel viewModel)
{
InitializeComponent();
_viewModel = viewModel;
DataContext = viewModel;
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
PasswordInput.Focus();
Keyboard.Focus(PasswordInput);
PasswordInput.SelectAll();
}
private void PasswordInput_OnTextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
_viewModel.PasswordText = PasswordInput.Text;
}
}

View File

@@ -0,0 +1,132 @@
using System.Reflection;
using System.Windows;
using Cardiopulmonarybypasssystems.Models;
using Cardiopulmonarybypasssystems.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace Cardiopulmonarybypasssystems.ViewModels;
public partial class StartupPasswordViewModel : ObservableObject
{
private readonly IPasswordAccessService _passwordAccessService;
public StartupPasswordViewModel(IPasswordAccessService passwordAccessService)
{
_passwordAccessService = passwordAccessService;
ProductName = "心肺转流检测系统";
SoftwareVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "1.0.0";
RefreshStatus();
}
[ObservableProperty]
private string productName = string.Empty;
[ObservableProperty]
private string softwareVersion = string.Empty;
[ObservableProperty]
private string passwordText = string.Empty;
[ObservableProperty]
private string statusTitle = string.Empty;
[ObservableProperty]
private string statusDetail = string.Empty;
[ObservableProperty]
private string stageText = string.Empty;
[ObservableProperty]
private string expiresAtText = "--";
[ObservableProperty]
private string remainingText = "--";
[ObservableProperty]
private string footerHint = "规则:第 1 个密码 15 天,第 2 个密码 30 天,第二次到期后自动永久有效。";
[ObservableProperty]
private bool canSubmit;
public bool CanContinue => _passwordAccessService.GetStatus().CanLaunch;
[RelayCommand]
private void Submit(Window? window)
{
if (!_passwordAccessService.TryUnlock(PasswordText, out _, out var message))
{
RefreshStatus();
StatusDetail = message;
return;
}
PasswordText = string.Empty;
RefreshStatus();
StatusDetail = message;
if (_passwordAccessService.GetStatus().CanLaunch && window is not null)
{
window.DialogResult = true;
window.Close();
}
}
[RelayCommand]
private void Continue(Window? window)
{
if (!_passwordAccessService.GetStatus().CanLaunch || window is null)
{
return;
}
window.DialogResult = true;
window.Close();
}
[RelayCommand]
private void Close(Window? window)
{
window?.Close();
}
[RelayCommand]
private void AppendPasswordCharacter(string? value)
{
if (string.IsNullOrEmpty(value))
{
return;
}
PasswordText += value;
}
[RelayCommand]
private void BackspacePassword()
{
if (string.IsNullOrEmpty(PasswordText))
{
return;
}
PasswordText = PasswordText[..^1];
}
[RelayCommand]
private void ClearPassword()
{
PasswordText = string.Empty;
}
private void RefreshStatus()
{
var status = _passwordAccessService.GetStatus();
StatusTitle = status.StatusTitle;
StatusDetail = status.StatusDetail;
StageText = status.StageText;
ExpiresAtText = status.ExpiresAt?.ToString("yyyy-MM-dd HH:mm") ?? (status.IsPermanent ? "永久有效" : "--");
RemainingText = status.IsPermanent ? "永久" : status.RemainingDays > 0 ? $"{status.RemainingDays} 天" : "需输入密码";
CanSubmit = status.CanSubmitPassword;
OnPropertyChanged(nameof(CanContinue));
}
}