更新项目配置,新增设备同步模块,优化WebSocket和Swagger配置,增强SCADA系统的免登录接口,支持数据字典项和登录日志的免登录查询与记录。调整Java编译设置,确保更好的开发体验。

This commit is contained in:
geht
2026-04-28 10:23:58 +08:00
parent bbe46dcf2d
commit 142a0bdaba
1013 changed files with 41858 additions and 28 deletions

View File

@@ -0,0 +1,82 @@
using FluentValidation;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using ValidationResult = System.Windows.Controls.ValidationResult;
namespace YY.Admin.Core.FluentValidation
{
internal class FluentAutoValidationRule<T> : ValidationRule, IFluentAutoValidationRule where T : class
{
public IValidator<T>? Validator { get; set; }
/// <summary>
/// 当前绑定控件对应的属性名
/// </summary>
public string? PropertyName { get; set; }
// 👇 ValidationStep.UpdatedValue为true: 让Validate方法中value参数为BindingExpression类型而非值类型
public FluentAutoValidationRule() : base(ValidationStep.UpdatedValue, true) { }
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (Validator == null || string.IsNullOrEmpty(PropertyName))
return ValidationResult.ValidResult;
// 如果 value 不是 BindingExpression先尝试判断是否是 BindingGroup (兼容性保护)
if (value is not BindingExpression bindingExpr)
{
// 有时 WPF 在不同阶段会传入 BindingGroup 或其他类型,直接跳过初始化阶段的验证,避免误报
return ValidationResult.ValidResult;
}
// --- 更可靠地判断“初始化阶段的第一次验证” ---
// 如果绑定目标尚未挂到视觉树或控件未 Loaded则视为初始化不执行验证
var target = bindingExpr.Target as DependencyObject;
if (target != null)
{
// 1) 如果是 FrameworkElement 并且还没 Loaded则跳过
if (target is FrameworkElement fe && !fe.IsLoaded)
return ValidationResult.ValidResult;
// 2) 如果 PresentationSource 为空,也说明还没连接到可视树,跳过
var ps = System.Windows.PresentationSource.FromVisual(target as System.Windows.Media.Visual);
if (ps == null)
return ValidationResult.ValidResult;
// 3) 另外,如果控件不可见,也可能是初始化阶段,可根据需要跳过
if (target is UIElement ui && !ui.IsVisible)
return ValidationResult.ValidResult;
}
var dataItem = bindingExpr.DataItem;
if (dataItem == null)
return new ValidationResult(false, "绑定源为空");
// 解析 Path例如 "SysUser.Account"
var path = bindingExpr.ParentBinding.Path?.Path ?? string.Empty;
var rootPropName = path.Split('.')[0]; // SysUser
var modelProp = dataItem.GetType().GetProperty(rootPropName);
if (modelProp == null)
return ValidationResult.ValidResult;
var modelObj = modelProp.GetValue(dataItem);
if (modelObj is not T model)
return ValidationResult.ValidResult;
// 调用 FluentValidation 验证指定属性
var result = Validator.ValidateAsync(model, opts => opts.IncludeProperties(PropertyName))
.ConfigureAwait(false) // 同步等待TaskValidationRule 不支持 async → 只能同步等待。)
.GetAwaiter()
.GetResult();
var error = result.Errors.FirstOrDefault(e => e.PropertyName == PropertyName);
if (error != null)
return new ValidationResult(false, error.ErrorMessage);
return ValidationResult.ValidResult;
}
}
}

View File

@@ -0,0 +1,332 @@
using FluentValidation;
using HandyControl.Controls;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Media;
using static Dm.util.ByteArrayQueue;
using ComboBox = System.Windows.Controls.ComboBox;
using DatePicker = System.Windows.Controls.DatePicker;
using PasswordBox = System.Windows.Controls.PasswordBox;
using TextBox = System.Windows.Controls.TextBox;
namespace YY.Admin.Core.FluentValidation
{
public static class FluentValidationHelper
{
#region SkipValidation
public static readonly DependencyProperty SkipValidationProperty =
DependencyProperty.RegisterAttached(
"SkipValidation",
typeof(bool),
typeof(FluentValidationHelper),
new System.Windows.PropertyMetadata(false));
public static void SetSkipValidation(DependencyObject element, bool value) =>
element.SetValue(SkipValidationProperty, value);
public static bool GetSkipValidation(DependencyObject element) =>
(bool)element.GetValue(SkipValidationProperty);
#endregion
/// <summary>
/// 入口方法:从 ViewModel 自动获取 Validator并绑定到所有子控件
/// </summary>
public static void Attach(FrameworkElement root, Type validatorOwnerType)
{
if (root == null || validatorOwnerType == null)
return;
// 找出 validator 属性
var validatorProp = validatorOwnerType
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.FirstOrDefault(p =>
typeof(IValidator).IsAssignableFrom(p.PropertyType) &&
p.Name.EndsWith("Validator", StringComparison.OrdinalIgnoreCase));
if (validatorProp == null)
return;
var validatorInstance = root.DataContext != null
? validatorProp.GetValue(root.DataContext)
: null;
if (validatorInstance == null)
return;
var validatedType = validatorProp.PropertyType.GetInterfaces()
.FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IValidator<>))
?.GetGenericArguments()[0];
if (validatedType == null)
return;
AttachRecursive(root, validatorInstance, validatedType);
}
/// <summary>
/// 递归绑定所有子控件
/// </summary>
private static void AttachRecursive(DependencyObject parent, object validatorInstance, Type validatedType)
{
if (parent == null) return;
// 先尝试走逻辑树 —— 能穿透 ScrollViewer、Row、Col、ContentPresenter 等容器
foreach (var childObj in LogicalTreeHelper.GetChildren(parent).OfType<DependencyObject>())
{
var child = childObj;
if (child is FrameworkElement feLogical)
{
// ⚠️ 先设置 ErrorTemplate保证 skip 的控件也能显示样式
var template = feLogical.TryFindResource("BottomLeftErrorTemplate_ForInfoElement") as ControlTemplate;
if (template != null)
{
Validation.SetErrorTemplate(feLogical, template);
}
// 🧭 再判断是否跳过校验
if (GetSkipValidation(feLogical))
{
AttachRecursive(child, validatorInstance, validatedType);
continue;
}
// 🪄 普通控件 / PasswordBox
AttachValidationForElement(feLogical, validatorInstance, validatedType);
}
// 递归逻辑树的子元素
AttachRecursive(child, validatorInstance, validatedType);
}
// 再补充视觉树 —— 仅在 parent 是 Visual 或 Visual3D 时调用,避免 ColumnDefinition 等非 Visual 抛错
if (parent is Visual || parent is System.Windows.Media.Media3D.Visual3D)
{
int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < childrenCount; i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
if (child is FrameworkElement fe)
{
// ⚠️ 先设置 ErrorTemplate保证 skip 的控件也能显示样式
var template = fe.TryFindResource("BottomLeftErrorTemplate_ForInfoElement") as ControlTemplate;
if (template != null)
{
Validation.SetErrorTemplate(fe, template);
}
// 🧭 再判断是否跳过校验
if (GetSkipValidation(fe))
{
AttachRecursive(child, validatorInstance, validatedType);
continue;
}
// 🪄 普通控件 / PasswordBox
AttachValidationForElement(fe, validatorInstance, validatedType);
}
// 递归子元素
AttachRecursive(child, validatorInstance, validatedType);
}
}
}
#region
private static void AttachValidationForElement(FrameworkElement fe, object validatorInstance, Type validatedType)
{
var localValues = fe.GetLocalValueEnumerator();
while (localValues.MoveNext())
{
var entry = localValues.Current;
var binding = BindingOperations.GetBinding(fe, entry.Property);
if (binding == null) continue;
AddValidationRule(binding, validatorInstance, validatedType);
// 重新应用绑定才能生效
BindingOperations.ClearBinding(fe, entry.Property);
BindingOperations.SetBinding(fe, entry.Property, binding);
}
}
private static void AddValidationRule(Binding binding, object validatorInstance, Type validatedType)
{
var propertyPath = binding.Path?.Path;
if (string.IsNullOrEmpty(propertyPath)) return;
var propertyName = propertyPath.Split('.').Last();
var ruleType = typeof(FluentAutoValidationRule<>).MakeGenericType(validatedType);
var rule = (ValidationRule)Activator.CreateInstance(ruleType)!;
ruleType.GetProperty("Validator")?.SetValue(rule, validatorInstance);
ruleType.GetProperty("PropertyName")?.SetValue(rule, propertyName);
binding.ValidationRules.Add(rule);
}
#endregion
#region
public static bool ValidateAll<T>(FrameworkElement root, T model, IValidator<T> validator) where T : class
{
// 1⃣ 获取所有 skip 的属性名
var skipPropertyNames = GetSkipValidationPropertyNames(root);
// 2⃣ 获取模型所有属性名
var allPropertyNames = typeof(T)
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Select(p => p.Name)
.ToList();
// 3⃣ 排除 skip剩下的就是要验证的属性
var includePropertyNames = allPropertyNames
.Except(skipPropertyNames, StringComparer.Ordinal)
.ToArray();
// 4⃣ 验证时只验证这些属性
var result = validator.ValidateAsync(model, options =>
{
options.IncludeProperties(includePropertyNames);
}).ConfigureAwait(false) // 同步等待TaskValidationRule 不支持 async → 只能同步等待。)
.GetAwaiter()
.GetResult();
// 5⃣ 清除之前的错误
ClearAllValidationErrors(root);
// 6⃣ 只对未 skip 的属性标记错误
foreach (var error in result.Errors)
{
var controls = FindBoundControls(root, error.PropertyName);
foreach (var control in controls)
{
var depProperty = GetBoundDependencyProperty(control);
if (depProperty == null)
continue;
var bindingExpr = BindingOperations.GetBindingExpression(control, depProperty);
if (bindingExpr != null)
{
Validation.MarkInvalid(
bindingExpr,
new ValidationError(
new ExceptionValidationRule(),
bindingExpr,
error.ErrorMessage,
null));
}
}
}
return result.IsValid;
}
private static HashSet<string> GetSkipValidationPropertyNames(DependencyObject root)
{
var skipPropertyNames = new HashSet<string>(StringComparer.Ordinal);
foreach (var control in FindVisualChildren<FrameworkElement>(root))
{
if (!GetSkipValidation(control))
continue;
var dp = GetBoundDependencyProperty(control);
if (dp == null) continue;
var binding = BindingOperations.GetBinding(control, dp);
if (binding?.Path?.Path is string propertyPath && !string.IsNullOrEmpty(propertyPath))
{
var propertyName = propertyPath.Split('.').Last();
skipPropertyNames.Add(propertyName);
}
}
return skipPropertyNames;
}
public static void ClearAllValidationErrors(DependencyObject parent)
{
foreach (var control in FindVisualChildren<FrameworkElement>(parent))
{
if (GetSkipValidation(control))
continue;
var depProperty = GetBoundDependencyProperty(control);
if (depProperty == null) continue;
var expr = BindingOperations.GetBindingExpression(control, depProperty);
if (expr != null)
Validation.ClearInvalid(expr);
}
}
#endregion
#region
private static DependencyProperty? GetBoundDependencyProperty(FrameworkElement control)
{
return control switch
{
// 原生控件
TextBox => TextBox.TextProperty,
// ✅ WPF 原生 PasswordBox注意WPF 原生 PasswordBox 没有现成的依赖属性绑定 Password需要额外处理
PasswordBox => PasswordBoxHelper.PasswordProperty,
ComboBox cb => BindingOperations.GetBinding(cb, Selector.SelectedItemProperty) != null
? Selector.SelectedItemProperty
: BindingOperations.GetBinding(cb, Selector.SelectedValueProperty) != null
? Selector.SelectedValueProperty
: null,
DatePicker => DatePicker.SelectedDateProperty,
CheckBox => ToggleButton.IsCheckedProperty,
// HandyControl控件
HandyControl.Controls.PasswordBox => HandyControl.Controls.PasswordBox.UnsafePasswordProperty,
TimePicker => TimePicker.SelectedTimeProperty,
NumericUpDown => NumericUpDown.ValueProperty,
CheckComboBox => CheckComboBox.SelectedItemsProperty,
_ => null
};
}
private static IEnumerable<FrameworkElement> FindBoundControls(DependencyObject parent, string propertyName)
{
return FindVisualChildren<FrameworkElement>(parent)
.Where(fe =>
{
if (GetSkipValidation(fe)) return false;
var dp = GetBoundDependencyProperty(fe);
if (dp == null) return false;
var expr = BindingOperations.GetBindingExpression(fe, dp);
return expr != null && expr.ParentBinding.Path.Path.EndsWith(propertyName);
});
}
private static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj == null) yield break;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
if (child is T tChild)
yield return tChild;
foreach (var subChild in FindVisualChildren<T>(child))
yield return subChild;
}
}
#endregion
}
}

View File

@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace YY.Admin.Core.FluentValidation
{
internal interface IFluentAutoValidationRule
{
}
}

View File

@@ -0,0 +1,15 @@
using System.Globalization;
using System.Windows.Controls;
using ValidationResult = System.Windows.Controls.ValidationResult;
namespace YY.Admin.Core.FluentValidation
{
public class MyFluentValidation : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
Console.WriteLine($"[FluentAutoValidationRule] Validate 调用,当前值:{value ?? "null"}");
return ValidationResult.ValidResult;
}
}
}

View File

@@ -0,0 +1,82 @@
using System.Windows;
using System.Windows.Controls;
namespace YY.Admin.Core.FluentValidation
{
public static class PasswordBoxHelper
{
public static readonly DependencyProperty PasswordProperty =
DependencyProperty.RegisterAttached(
"Password",
typeof(string),
typeof(PasswordBoxHelper),
new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnPasswordPropertyChanged));
public static readonly DependencyProperty AttachProperty =
DependencyProperty.RegisterAttached(
"Attach",
typeof(bool),
typeof(PasswordBoxHelper),
new System.Windows.PropertyMetadata(false, OnAttachChanged));
public static void SetAttach(DependencyObject dp, bool value)
{
dp.SetValue(AttachProperty, value);
}
public static bool GetAttach(DependencyObject dp)
{
return (bool)dp.GetValue(AttachProperty);
}
public static string GetPassword(DependencyObject dp)
{
return (string)dp.GetValue(PasswordProperty);
}
public static void SetPassword(DependencyObject dp, string value)
{
dp.SetValue(PasswordProperty, value);
}
private static void OnAttachChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (sender is PasswordBox passwordBox)
{
if ((bool)e.OldValue)
{
passwordBox.PasswordChanged -= PasswordChanged;
}
if ((bool)e.NewValue)
{
passwordBox.PasswordChanged += PasswordChanged;
}
}
}
private static void OnPasswordPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (sender is PasswordBox passwordBox)
{
// 防止无限循环
passwordBox.PasswordChanged -= PasswordChanged;
if (passwordBox.Password != (e.NewValue ?? ""))
{
passwordBox.Password = e.NewValue?.ToString() ?? "";
}
passwordBox.PasswordChanged += PasswordChanged;
}
}
private static void PasswordChanged(object sender, RoutedEventArgs e)
{
if (sender is PasswordBox passwordBox)
{
SetPassword(passwordBox, passwordBox.Password);
}
}
}
}