diff --git a/EOM.TSHotelManagement.Common/EncryptedFieldAttribute.cs b/EOM.TSHotelManagement.Common/EncryptedFieldAttribute.cs new file mode 100644 index 0000000000000000000000000000000000000000..c7114a5a2dd01fb201bf1536865f783904ed1bb5 --- /dev/null +++ b/EOM.TSHotelManagement.Common/EncryptedFieldAttribute.cs @@ -0,0 +1,12 @@ +namespace EOM.TSHotelManagement.Common; + +[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)] +public class EncryptedFieldAttribute : Attribute +{ + public string ProtectorPurpose { get; } + + public EncryptedFieldAttribute(string protectorPurpose) + { + ProtectorPurpose = protectorPurpose; + } +} diff --git a/EOM.TSHotelManagement.Common/Helper/SqlFilterBuilder.cs b/EOM.TSHotelManagement.Common/Helper/SqlFilterBuilder.cs index d09e6cdd6e0e71f841dc264c30e539d6806ae8b0..e764812f67c8edfa9748c328b90eaf2c6707879d 100644 --- a/EOM.TSHotelManagement.Common/Helper/SqlFilterBuilder.cs +++ b/EOM.TSHotelManagement.Common/Helper/SqlFilterBuilder.cs @@ -15,6 +15,10 @@ namespace EOM.TSHotelManagement.Common /// - 指定单个日期字段范围查询:BuildExpression<Entity, Dto>(dto, "DateField") /// - 多个日期字段范围查询:通过 dto.DateRangeDto.Ranges 字典设置多个字段的范围 /// - 自定义映射字典:BuildExpression<Entity, Dto>(dto, null, propertyMapping) 或 BuildExpression<Entity, Dto>(dto, dateFieldName, propertyMapping) + /// + /// 加密字段精确查询支持: + /// - 通过 encryptedFields 字典指定需要加密的字段,key为实体属性名,value为加密函数 + /// - 加密字段只进行精确匹配,不进行模糊匹配,确保安全性 /// public static class SqlFilterBuilder { @@ -32,19 +36,309 @@ namespace EOM.TSHotelManagement.Common public static Expressionable BuildExpression(TDto dto, bool skipZeroForNumeric = false) where TEntity : class, new() { - return BuildExpression(dto, null, null, skipZeroForNumeric); + return BuildExpression(dto, null, null, null, skipZeroForNumeric); } public static Expressionable BuildExpression(TDto dto, string dateFieldName, bool skipZeroForNumeric = false) where TEntity : class, new() { - return BuildExpression(dto, dateFieldName, null,skipZeroForNumeric); + return BuildExpression(dto, dateFieldName, null, null, skipZeroForNumeric); } public static Expressionable BuildExpression(TDto dto, Dictionary propertyMapping, bool skipZeroForNumeric = false) where TEntity : class, new() { - return BuildExpression(dto, null, propertyMapping, skipZeroForNumeric); + return BuildExpression(dto, null, propertyMapping, null, skipZeroForNumeric); + } + + public static Expressionable BuildExpression( + TDto dto, + string dateFieldName, + Dictionary propertyMapping, + Dictionary> encryptedFields, + bool skipZeroForNumeric = false) + where TEntity : class, new() + { + var where = Expressionable.Create(); + if (dto == null) return where; + + var dtoType = typeof(TDto); + var entityType = typeof(TEntity); + var dtoProps = dtoType.GetProperties(BindingFlags.Public | BindingFlags.Instance); + + var mapping = propertyMapping ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + var encryptedFieldDict = encryptedFields ?? new Dictionary>(); + + DateRangeDto dateRangeDto = null; + var dateRangeProp = dtoProps.FirstOrDefault(p => p.PropertyType == typeof(DateRangeDto)); + if (dateRangeProp != null) + { + dateRangeDto = dateRangeProp.GetValue(dto) as DateRangeDto; + } + + if (dateRangeDto == null) + { + var startDateProp = dtoProps.FirstOrDefault(p => p.Name.Contains("StartDate", StringComparison.OrdinalIgnoreCase)); + var endDateProp = dtoProps.FirstOrDefault(p => p.Name.Contains("EndDate", StringComparison.OrdinalIgnoreCase)); + if (startDateProp != null && endDateProp != null) + { + var startValue = startDateProp.GetValue(dto); + var endValue = endDateProp.GetValue(dto); + if (startValue != null && endValue != null) + { + DateTime? startDt = null; + DateTime? endDt = null; + if (startValue is DateTime dtStart) + { + startDt = dtStart; + } + else if (startValue is DateOnly dStart) + { + startDt = dStart.ToDateTime(TimeOnly.MinValue); + } + if (endValue is DateTime dtEnd) + { + endDt = dtEnd; + } + else if (endValue is DateOnly dEnd) + { + endDt = dEnd.ToDateTime(TimeOnly.MinValue); + } + if (startDt.HasValue && endDt.HasValue) + { + dateRangeDto = new DateRangeDto + { + Start = startDt, + End = endDt + }; + } + } + } + } + + if (!string.IsNullOrEmpty(dateFieldName) && dateRangeDto != null) + { + var targetProp = entityType.GetProperty(dateFieldName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); + if (targetProp != null && dateRangeDto.Start.HasValue) + { + if (TryBuildComparisonExpression(targetProp, dateRangeDto.Start.Value, ComparisonType.GreaterOrEqual, out var startExpr)) + { + where = where.And(startExpr); + } + } + if (targetProp != null && dateRangeDto.End.HasValue) + { + if (TryBuildComparisonExpression(targetProp, dateRangeDto.End.Value, ComparisonType.LessOrEqual, out var endExpr)) + { + where = where.And(endExpr); + } + } + } + + if (dateRangeDto != null && string.IsNullOrEmpty(dateFieldName)) + { + var (startProp, endProp) = FindDateRangePair(entityType); + if (startProp != null && endProp != null) + { + if (dateRangeDto.End.HasValue) + { + if (TryBuildComparisonExpression(startProp, dateRangeDto.End.Value, ComparisonType.LessOrEqual, out var startExpr)) + { + where = where.And(startExpr); + } + } + if (dateRangeDto.Start.HasValue) + { + if (TryBuildComparisonExpression(endProp, dateRangeDto.Start.Value, ComparisonType.GreaterOrEqual, out var endExpr)) + { + where = where.And(endExpr); + } + } + } + } + + var nonStringConditions = Expressionable.Create(); + + foreach (var dprop in dtoProps) + { + var rawValue = dprop.GetValue(dto); + if (rawValue == null) continue; + + if (skipZeroForNumeric && IsNumericZeroValue(rawValue)) + { + continue; + } + + var dname = dprop.Name; + + if (dprop.PropertyType == typeof(DateRangeDto)) continue; + + if (dprop.PropertyType == typeof(DateOnly) && (DateOnly)rawValue == DateOnly.MinValue) rawValue = null; + + if (dname == "Start" || dname == "End") continue; + + PropertyInfo entityProp = null; + if (mapping.TryGetValue(dname, out var mappedPropName)) + { + entityProp = entityType.GetProperty(mappedPropName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); + + if (entityProp != null) + { + if (entityProp.PropertyType != typeof(string)) + { + if (rawValue == null) continue; + if (TryBuildEqualityExpression(entityProp, rawValue, out var eqExpr)) + { + nonStringConditions = nonStringConditions.And(eqExpr); + } + } + continue; + } + } + else + { + var rangeBase = GetRangeBaseName(dname, out var rangeType); + if (rangeBase != null) + { + var targetProp = entityType.GetProperty(rangeBase, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); + if (targetProp != null) + { + if (TryBuildComparisonExpression(targetProp, rawValue, rangeType == RangeType.Start ? ComparisonType.GreaterOrEqual : ComparisonType.LessOrEqual, out var expr)) + { + where = where.And(expr); + } + } + continue; + } + + entityProp = entityType.GetProperty(dname, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); + } + + if (entityProp == null) continue; + + if (entityProp.PropertyType != typeof(string)) + { + if (rawValue == null) continue; + if (TryBuildEqualityExpression(entityProp, rawValue, out var eqExpr)) + { + nonStringConditions = nonStringConditions.And(eqExpr); + } + continue; + } + + if (encryptedFieldDict.ContainsKey(entityProp.Name)) + { + var encryptFunc = encryptedFieldDict[entityProp.Name]; + if (encryptFunc != null && rawValue is string strValue && !string.IsNullOrEmpty(strValue)) + { + var encryptedValue = encryptFunc(strValue); + if (!string.IsNullOrEmpty(encryptedValue)) + { + if (TryBuildEqualityExpression(entityProp, encryptedValue, out var eqExpr)) + { + nonStringConditions = nonStringConditions.And(eqExpr); + } + } + } + } + } + + if (nonStringConditions != null) + { + var nonStringExpr = nonStringConditions.ToExpression(); + if (nonStringExpr != null) + { + where = where.And(nonStringExpr); + } + } + + var stringExactConditions = Expressionable.Create(); + var stringContainsConditions = Expressionable.Create(); + var hasStringProperties = false; + + foreach (var dprop in dtoProps) + { + var rawValue = dprop.GetValue(dto); + if (rawValue == null) continue; + + if (skipZeroForNumeric && IsNumericZeroValue(rawValue)) + { + continue; + } + + var dname = dprop.Name; + + if (dprop.PropertyType == typeof(DateRangeDto)) continue; + + if (dname == "Start" || dname == "End") continue; + + PropertyInfo entityProp = null; + if (mapping.TryGetValue(dname, out var mappedPropName)) + { + entityProp = entityType.GetProperty(mappedPropName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); + } + else + { + var rangeBase = GetRangeBaseName(dname, out var rangeType); + if (rangeBase != null) + { + continue; + } + + entityProp = entityType.GetProperty(dname, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); + } + + if (entityProp == null) continue; + + if (entityProp.PropertyType == typeof(string)) + { + if (encryptedFieldDict.ContainsKey(entityProp.Name)) + { + continue; + } + + hasStringProperties = true; + var s = rawValue as string; + if (!string.IsNullOrEmpty(s)) + { + var exactExpr = BuildStringExactExpression(entityProp.Name, s); + if (exactExpr != null) + { + stringExactConditions = stringExactConditions.And(exactExpr); + } + + var containsExpr = BuildStringContainsExpression(entityProp.Name, s); + if (containsExpr != null) + { + stringContainsConditions = stringContainsConditions.And(containsExpr); + } + } + } + } + + if (hasStringProperties) + { + var exactExpr = stringExactConditions.ToExpression(); + var containsExpr = stringContainsConditions.ToExpression(); + + if (exactExpr != null && containsExpr != null) + { + var param = exactExpr.Parameters[0]; + var combinedBody = Expression.OrElse(exactExpr.Body, containsExpr.Body); + var combinedStringExpr = Expression.Lambda>(combinedBody, param); + where = where.And(combinedStringExpr); + } + else if (exactExpr != null) + { + where = where.And(exactExpr); + } + else if (containsExpr != null) + { + where = where.And(containsExpr); + } + } + + return where; } /// diff --git a/EOM.TSHotelManagement.Domain/Business/Reser/Reser.cs b/EOM.TSHotelManagement.Domain/Business/Reser/Reser.cs index 93bbf613e0c20fdf48cfb24dafa52e88562a6564..47c5cd228bad8b8a38aab59d2019447526c7e7da 100644 --- a/EOM.TSHotelManagement.Domain/Business/Reser/Reser.cs +++ b/EOM.TSHotelManagement.Domain/Business/Reser/Reser.cs @@ -22,6 +22,7 @@ * *模块说明:预约类 */ +using EOM.TSHotelManagement.Common; using SqlSugar; using System; @@ -71,6 +72,7 @@ namespace EOM.TSHotelManagement.Domain IsNullable = false, Length = 256 )] + [EncryptedField("ReservationInfoProtector")] public string ReservationPhoneNumber { get; set; } /// diff --git a/EOM.TSHotelManagement.Domain/Common/Personnel.cs b/EOM.TSHotelManagement.Domain/Common/Personnel.cs index f383a2625a9915314bacf61d7a1edf0b6f4f439b..d2e39ea8c08e6b83ab616592ce1bbe6234a14954 100644 --- a/EOM.TSHotelManagement.Domain/Common/Personnel.cs +++ b/EOM.TSHotelManagement.Domain/Common/Personnel.cs @@ -1,3 +1,4 @@ +using EOM.TSHotelManagement.Common; using SqlSugar; using System; using System.Collections.Generic; @@ -10,8 +11,12 @@ public class Personnel : SoftDeleteEntity [SugarColumn(ColumnName = "name", IsNullable = false, ColumnDescription = "姓名", Length = 250)] public string Name { get; set; } = string.Empty; [SugarColumn(ColumnName = "phone_number", IsNullable = false, ColumnDescription = "电话号码", Length = 256)] + [EncryptedField("EmployeeInfoProtector")] + [EncryptedField("CustomerInfoProtector")] public string PhoneNumber { get; set; } = string.Empty; [SugarColumn(ColumnName = "id_number", IsNullable = false, ColumnDescription = "证件号码", Length = 256)] + [EncryptedField("EmployeeInfoProtector")] + [EncryptedField("CustomerInfoProtector")] public string IdCardNumber { get; set; } = string.Empty; [SugarColumn(ColumnName = "address", IsNullable = false, ColumnDescription = "联系地址", Length = 500)] public string Address { get; set; } = string.Empty; diff --git a/EOM.TSHotelManagement.Service/Business/Customer/CustomerService.cs b/EOM.TSHotelManagement.Service/Business/Customer/CustomerService.cs index 36605721f785c53883acbf363fda6fd7981912d4..ef56ec08257747dc34f4bb83a56499b26c3a4d3a 100644 --- a/EOM.TSHotelManagement.Service/Business/Customer/CustomerService.cs +++ b/EOM.TSHotelManagement.Service/Business/Customer/CustomerService.cs @@ -238,7 +238,12 @@ namespace EOM.TSHotelManagement.Service.Business.Customer { readCustomerInputDto ??= new ReadCustomerInputDto(); - var where = SqlFilterBuilder.BuildExpression(readCustomerInputDto, nameof(Domain.Customer.DateOfBirth)); + var encryptedFields = new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + { nameof(Domain.Customer.PhoneNumber), dataProtector.EncryptCustomerData }, + { nameof(Domain.Customer.IdCardNumber), dataProtector.EncryptCustomerData } + }; + var where = SqlFilterBuilder.BuildExpression(readCustomerInputDto, nameof(Domain.Customer.DateOfBirth), null, encryptedFields); var query = custoRepository.AsQueryable(); var whereExpression = where.ToExpression(); if (whereExpression != null) diff --git a/EOM.TSHotelManagement.Service/Business/Reser/ReserService.cs b/EOM.TSHotelManagement.Service/Business/Reser/ReserService.cs index 4a6d4bd4da1d33719c8ee2ac7069137395825e9e..d68f28ad62692894d6d71a22fdd2706c23d459d5 100644 --- a/EOM.TSHotelManagement.Service/Business/Reser/ReserService.cs +++ b/EOM.TSHotelManagement.Service/Business/Reser/ReserService.cs @@ -27,11 +27,15 @@ namespace EOM.TSHotelManagement.Service.Business.Reser .Cast() .ToDictionary(a => a.ToString(), a => EnumHelper.GetEnumDescription(a) ?? string.Empty, StringComparer.OrdinalIgnoreCase); + var encryptedFields = new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + { nameof(Domain.Reser.ReservationPhoneNumber), dataProtector.EncryptReserData } + }; var where = SqlFilterBuilder.BuildExpression(filterInput, new Dictionary { { nameof(ReadReserInputDto.ReservationStartDateStart), nameof(Domain.Reser.ReservationStartDate) }, { nameof(ReadReserInputDto.ReservationStartDateEnd), nameof(Domain.Reser.ReservationEndDate) } - }); + }, null, encryptedFields); var query = reserRepository.AsQueryable(); var whereExpression = where.ToExpression(); diff --git a/EOM.TSHotelManagement.Service/Employee/EmployeeService.cs b/EOM.TSHotelManagement.Service/Employee/EmployeeService.cs index 2177cbf97922e1d5d080dd72a4180c8e2a116e35..1558e1a111468b469fc425c2cdb56a461e6e032a 100644 --- a/EOM.TSHotelManagement.Service/Employee/EmployeeService.cs +++ b/EOM.TSHotelManagement.Service/Employee/EmployeeService.cs @@ -170,7 +170,12 @@ namespace EOM.TSHotelManagement.Service.Employee { readEmployeeInputDto ??= new ReadEmployeeInputDto(); - var where = SqlFilterBuilder.BuildExpression(readEmployeeInputDto, nameof(Domain.Employee.HireDate)); + var encryptedFields = new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + { nameof(Domain.Employee.PhoneNumber), dataProtector.EncryptEmployeeData }, + { nameof(Domain.Employee.IdCardNumber), dataProtector.EncryptEmployeeData } + }; + var where = SqlFilterBuilder.BuildExpression(readEmployeeInputDto, nameof(Domain.Employee.HireDate), null, encryptedFields); var query = workerRepository.AsQueryable(); var whereExpression = where.ToExpression(); if (whereExpression != null) diff --git a/EOM.TSHotelManagement.Service/SystemManagement/Administrator/AdminService.cs b/EOM.TSHotelManagement.Service/SystemManagement/Administrator/AdminService.cs index e9f9e44e4249a5ea8fe8a833aad5ccfb1016dffe..05c2fc0c8f7f27cb3761438e933d09ac49a5fc1d 100644 --- a/EOM.TSHotelManagement.Service/SystemManagement/Administrator/AdminService.cs +++ b/EOM.TSHotelManagement.Service/SystemManagement/Administrator/AdminService.cs @@ -28,6 +28,7 @@ using EOM.TSHotelManagement.Domain; using jvncorelib.EntityLib; using Microsoft.Extensions.Logging; using SqlSugar; +using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Net.Mail; using System.Security.Claims; @@ -282,7 +283,12 @@ namespace EOM.TSHotelManagement.Service { readAdministratorInputDto ??= new ReadAdministratorInputDto(); - var where = SqlFilterBuilder.BuildExpression(readAdministratorInputDto); + var encryptedFields = new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + { nameof(Administrator.PhoneNumber), dataProtector.EncryptAdministratorData }, + { nameof(Administrator.IdCardNumber), dataProtector.EncryptAdministratorData } + }; + var where = SqlFilterBuilder.BuildExpression(readAdministratorInputDto, null, null, encryptedFields); var query = adminRepository.AsQueryable(); var whereExpression = where.ToExpression(); if (whereExpression != null)