feat: add ERP agent pet bridge and startup guide
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user