using System; using System.Collections.Generic; namespace Lskj.CommandKernel { public sealed class LeaveRequestDraft { public string EmployeeId { get; set; } public string LeaveTypeCode { get; set; } public string FlowTypeCode { get; set; } public DateTime StartLocal { get; set; } public DateTime EndLocal { get; set; } public decimal RequestedHours { get; set; } public string Reason { get; set; } public bool SubmitAfterSave { get; set; } } public sealed class LeaveValidationOptions { public LeaveValidationOptions() { MaximumCalendarDays = 31; MinimumReasonLength = 2; HoursTolerance = 0.01m; } public string CurrentEmployeeId { get; set; } public bool CanApplyForOthers { get; set; } public bool AllowPastStart { get; set; } public int MaximumCalendarDays { get; set; } public int MinimumReasonLength { get; set; } public decimal HoursTolerance { get; set; } } public interface IWorkingTimeCalculator { decimal CalculateHours(string employeeId, DateTime startLocal, DateTime endLocal); } public interface ILeaveConflictProvider { bool HasConflict(string employeeId, DateTime startLocal, DateTime endLocal); } public sealed class LeaveValidationResult { public LeaveValidationResult() { Issues = new List(); } public bool Valid { get; set; } public decimal CalculatedHours { get; set; } public IList Issues { get; private set; } } public static class LeaveRequestValidator { public static LeaveValidationResult ValidateShape( LeaveRequestDraft draft, LeaveValidationOptions options) { if (draft == null) throw new ArgumentNullException("draft"); ValidateOptions(options); LeaveValidationResult result = new LeaveValidationResult(); if (!SafeCode(draft.EmployeeId, 64)) result.Issues.Add("请假员工编码格式无效。"); if (!SafeCode(draft.LeaveTypeCode, 64)) result.Issues.Add("请假类型必须是有效的 ERP 编码。"); if (!SafeCode(draft.FlowTypeCode, 64)) result.Issues.Add("请假流转类别必须是有效的 ERP 编码。"); if (draft.StartLocal.Kind != DateTimeKind.Unspecified || draft.EndLocal.Kind != DateTimeKind.Unspecified) { result.Issues.Add("请假时间必须是不带 Z 或时区偏移的本地时间。"); } if (draft.StartLocal.Year < 1900 || draft.StartLocal.Year > 2100 || draft.EndLocal.Year < 1900 || draft.EndLocal.Year > 2100) result.Issues.Add("请假时间超出允许范围。"); if (draft.StartLocal >= draft.EndLocal) result.Issues.Add("请假结束时间必须晚于开始时间。"); if (draft.EndLocal - draft.StartLocal > TimeSpan.FromDays(options.MaximumCalendarDays)) result.Issues.Add("请假跨度超过系统允许的最大天数。"); if (draft.RequestedHours < 0 || draft.RequestedHours > 744m) result.Issues.Add("请求时数超出允许范围。"); string reason = (draft.Reason ?? string.Empty).Trim(); if (reason.Length < options.MinimumReasonLength) result.Issues.Add("请假原因过短。"); if (reason.Length > 500 || HasControl(reason)) result.Issues.Add("请假原因过长或包含控制字符。"); result.Valid = result.Issues.Count == 0; return result; } public static LeaveValidationResult Validate( LeaveRequestDraft draft, LeaveValidationOptions options, IWorkingTimeCalculator workingTime, ILeaveConflictProvider conflicts, DateTime nowLocal) { if (draft == null) throw new ArgumentNullException("draft"); if (options == null) throw new ArgumentNullException("options"); if (workingTime == null) throw new ArgumentNullException("workingTime"); if (conflicts == null) throw new ArgumentNullException("conflicts"); LeaveValidationResult result = ValidateShape(draft, options); if (!options.CanApplyForOthers && !Same(draft.EmployeeId, options.CurrentEmployeeId)) { result.Issues.Add("当前用户无权代他人申请请假。"); } if (nowLocal.Kind == DateTimeKind.Utc) result.Issues.Add("ERP 当前时间不是有效的本地时间。"); if (!options.AllowPastStart && draft.StartLocal < nowLocal) result.Issues.Add("不能申请已开始的历史请假。"); if (result.Issues.Count == 0) { result.CalculatedHours = workingTime.CalculateHours( draft.EmployeeId, draft.StartLocal, draft.EndLocal); if (result.CalculatedHours <= 0) result.Issues.Add("所选时间段没有可申请的工作时数。"); if (draft.RequestedHours > 0 && Math.Abs(draft.RequestedHours - result.CalculatedHours) > options.HoursTolerance) { result.Issues.Add("请求时数与员工日历计算结果不一致。"); } if (conflicts.HasConflict(draft.EmployeeId, draft.StartLocal, draft.EndLocal)) result.Issues.Add("该员工在所选时间段已有请假或冲突申请。"); } result.Valid = result.Issues.Count == 0; return result; } private static bool Same(string left, string right) { return string.Equals( (left ?? string.Empty).Trim(), (right ?? string.Empty).Trim(), StringComparison.OrdinalIgnoreCase); } internal static bool SafeCode(string value, int maximum) { string text = (value ?? string.Empty).Trim(); if (text.Length == 0 || text.Length > maximum) return false; foreach (char item in text) { if (!char.IsLetterOrDigit(item) && item != '_' && item != '-' && item != '.' && item != ':') return false; } return true; } internal static bool SafeText( string value, int minimum, int maximum, bool optional) { string text = (value ?? string.Empty).Trim(); if (text.Length == 0) return optional; return text.Length >= minimum && text.Length <= maximum && !HasControl(text); } internal static void ValidateOptions(LeaveValidationOptions options) { if (options == null) throw new ArgumentNullException("options"); if (options.MaximumCalendarDays <= 0 || options.MaximumCalendarDays > 31 || options.MinimumReasonLength < 0 || options.MinimumReasonLength > 500 || options.HoursTolerance < 0 || options.HoursTolerance > 1) throw new ArgumentOutOfRangeException("options", "请假校验配置无效。"); } private static bool HasControl(string value) { foreach (char item in value ?? string.Empty) if (char.IsControl(item)) return true; return false; } } }