193 lines
7.7 KiB
C#
193 lines
7.7 KiB
C#
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<string>();
|
|
}
|
|
|
|
public bool Valid { get; set; }
|
|
public decimal CalculatedHours { get; set; }
|
|
public IList<string> 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;
|
|
}
|
|
}
|
|
}
|