Bläddra i källkod

企业微信考勤与工资计算重构及参数配置

重构工资计算逻辑,新增PayrollComputation_v1与PayrollConfig,支持更细致的考勤、假勤、补卡规则,参数集中配置。升级企业微信考勤数据结构,优化API接口与领域模型,增强参数校验和异常处理,提升系统灵活性与可维护性。
Lyyyi 6 dagar sedan
förälder
incheckning
5ec8db73df

+ 46 - 1
OASystem/OASystem.Api/Controllers/AITestController.cs

@@ -1,6 +1,9 @@
 using Flurl.Http.Configuration;
+using Humanizer;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.Options;
+using NodaTime;
+using OASystem.API.OAMethodLib;
 using OASystem.API.OAMethodLib.DeepSeekAPI;
 using OASystem.API.OAMethodLib.DoubaoAPI;
 using OASystem.API.OAMethodLib.Hotmail;
@@ -10,6 +13,7 @@ using OASystem.API.OAMethodLib.QiYeWeChatAPI;
 using OASystem.API.OAMethodLib.Quartz.Business;
 using OASystem.Domain.AesEncryption;
 using OASystem.Domain.Entities.Customer;
+using OASystem.Domain.Entities.PersonnelModule;
 using OASystem.Domain.ViewModels.QiYeWeChat;
 using OASystem.RedisRepository;
 using System.IdentityModel.Tokens.Jwt;
@@ -34,6 +38,7 @@ namespace OASystem.API.Controllers
         private readonly IMicrosoftGraphMailboxService _microsoftGraphMailboxService;
         private readonly IOptionsMonitor<MicrosoftGraphMailboxOptions> _microsoftGraphMailboxOptions;
         private readonly MessageRepository _message;
+        private readonly SqlSugarClient _sqlSugar;
 
         private readonly IDeepSeekService _deepSeekService;
 
@@ -48,7 +53,8 @@ namespace OASystem.API.Controllers
             IMicrosoftGraphMailboxService microsoftGraphMailboxService,
             IOptionsMonitor<MicrosoftGraphMailboxOptions> microsoftGraphMailboxOptions,
             IDeepSeekService deepSeekService,
-            MessageRepository message
+            MessageRepository message,
+            SqlSugarClient sqlSugar
             )
         {
             _hunyuanService = hunyuanService;
@@ -62,6 +68,7 @@ namespace OASystem.API.Controllers
             _deepSeekService = deepSeekService;
             _microsoftGraphMailboxOptions = microsoftGraphMailboxOptions;
             _message = message;
+            _sqlSugar = sqlSugar;
         }
 
         #region 企业微信发送邮件测试
@@ -714,5 +721,43 @@ namespace OASystem.API.Controllers
 
         #endregion
 
+        #region 工资计算测试
+
+        /// <summary>
+        /// 工资计算测试 - 企微考勤信息
+        /// </summary>
+        [HttpPost("SalaryCalculator")]
+        public async Task<ActionResult<string>> SalaryCalculator()
+        {
+            // 计算本月工资起止时间 比如是2月的1号-28号,那就是2月1号的零点到3月1号的零点 
+            DateTime startDt = Convert.ToDateTime("2026-03-01");
+            DateTime endDt = Convert.ToDateTime("2026-03-31"); 
+            string thisYearMonth = "2026-03"; //本月工资年月
+            string preYearMonth = "2026-03"; //上月工资年月
+
+            // 检查是否存在
+            //var isExists = await _sqlSugar.Queryable<Pm_WageSheet>().AnyAsync(it => it.YearMonth == thisYearMonth);
+            //if (isExists) return Ok(JsonView(false, $"{thisYearMonth} 工资数据已存在。"));
+
+            var preWageSheetItems = await _sqlSugar.Queryable<Pm_WageSheet>()
+                .Where(it => it.IsDel == 0 && it.YearMonth == preYearMonth)
+                .ToListAsync();
+
+            if (!preWageSheetItems.Any()) return Ok(JsonView(false, "上月工资数据不存在。"));
+
+            //处理上个月同月同人 多条数据
+            List<Pm_WageSheet> preWageSheetItems1 = preWageSheetItems
+                .GroupBy(it => new { it.YearMonth, it.UserId })
+                .Select(it => it.FirstOrDefault(item => item.Basic != 0))
+                .Where(it => it != null)
+                .ToList()!;
+
+            var res = await PayrollComputation_v1.SalaryCalculatorAsync(preWageSheetItems1,208, thisYearMonth, startDt, endDt);
+
+            return Ok(res);
+        }
+
+        #endregion
+
     }
 }

+ 57 - 87
OASystem/OASystem.Api/Controllers/PersonnelModuleController.cs

@@ -26,6 +26,7 @@ using System.Diagnostics;
 using System.Globalization;
 using static OASystem.API.OAMethodLib.JWTHelper;
 using OASystem.Domain.Dtos.QiYeWeChat;
+using Result = OASystem.Domain.Result;
 
 namespace OASystem.API.Controllers
 {
@@ -348,27 +349,13 @@ namespace OASystem.API.Controllers
         [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
         public async Task<IActionResult> GetWageSheetList(WageSheetListDto dto)
         {
-            if (string.IsNullOrEmpty(dto.YearMonth)) return Ok(JsonView(false, "请选择日期格式"));
-
-            //验证日期格式
             if (!DateTime.TryParse(dto.YearMonth, out DateTime yearMonthDt))
-            {
                 return Ok(JsonView(false, "无效的日期格式"));
-            }
-
-            //参数处理
-            //string ymFormat = "yyyy-MM";
-            //bool yearMonthDttIsValid = DateTime.TryParseExact(dto.YearMonth, ymFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime yearMonthDt);
-            //if (!yearMonthDttIsValid)
-            //{
-            //    _result.Msg = "年月格式错误!正确时间格式:yyyy-MM  ";
-            //    return Ok(JsonView(false, _result.Msg));
-            //}
 
             //获取月工资数据
             string yearMonth = yearMonthDt.ToString("yyyy-MM");
 
-            if (dto.PortType == 1)
+            if (dto.PortType == 1 || dto.PortType == 2 || dto.PortType == 3)
             {
                 _result = await _wageSheetRep.Get_WageSheet_ListByYearMonthAsync(yearMonth);
                 if (_result.Code != 0)
@@ -376,10 +363,6 @@ namespace OASystem.API.Controllers
                     return Ok(JsonView(false, _result.Msg));
                 }
             }
-            else if (dto.PortType == 2)
-            { }
-            else if (dto.PortType == 3)
-            { }
             else
             {
                 return Ok(JsonView(false, "请选择正确的端口参数"));
@@ -397,7 +380,7 @@ namespace OASystem.API.Controllers
         [ProducesResponseType(typeof(JsonView), StatusCodes.Status200OK)]
         public async Task<IActionResult> GetWageSheetById(WageSheetInfoDto dto)
         {
-            if (dto.PortType == 1)
+            if (dto.PortType == 1 || dto.PortType == 2 || dto.PortType == 3)
             {
                 _result = await _wageSheetRep.Get_WageSheet_InfoByIdAsync(dto.Id);
                 if (_result.Code != 0)
@@ -405,10 +388,6 @@ namespace OASystem.API.Controllers
                     return Ok(JsonView(false, _result.Msg));
                 }
             }
-            else if (dto.PortType == 2)
-            { }
-            else if (dto.PortType == 3)
-            { }
             else
             {
                 return Ok(JsonView(false, "请选择正确的端口参数"));
@@ -481,7 +460,7 @@ namespace OASystem.API.Controllers
                     }
                     else
                     {
-                        salary = PayrollComputation.ConvertToDecimal(salary / pm_WageSheet.WorkDays * pm_WageSheet.RegularDays + pm_WageSheet.Mealsupplement + pm_WageSheet.OtherHandle);
+                        salary = PayrollComputation1.ConvertToDecimal(salary / pm_WageSheet.WorkDays * pm_WageSheet.RegularDays + pm_WageSheet.Mealsupplement + pm_WageSheet.OtherHandle);
                     }
                 }
 
@@ -493,7 +472,6 @@ namespace OASystem.API.Controllers
 
                 #endregion
 
-
                 _result = await _wageSheetRep.Post_WageSheet_AddOrEditAsync(dto, pm_WageSheet);
                 if (_result.Code != 0)
                 {
@@ -502,7 +480,6 @@ namespace OASystem.API.Controllers
             }
             catch (Exception ex)
             {
-
                 return Ok(JsonView(false, ex.Message));
             }
 
@@ -520,55 +497,61 @@ namespace OASystem.API.Controllers
             Result result = new();
             Stopwatch sw = new();
             sw.Start();
-            //参数处理
-            string ymFormat = "yyyy-MM";
-            string dtFormat = "yyyy-MM-dd";
-            bool yearMonthDtIsValid = DateTime.TryParseExact(dto.yearMonth, ymFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime yearMonthDt);
-            bool startDtIsValid = DateTime.TryParseExact(dto.startDt, dtFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime startDt);
-            bool endDtIsValid = DateTime.TryParseExact(dto.endDt, dtFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime endDt);
 
-            if (!yearMonthDtIsValid) return Ok(JsonView(false, "年月格式错误!正确时间格式:yyyy-MM  "));
-            if (!startDtIsValid) return Ok(JsonView(false, "开始日期格式错误!正确时间格式:yyyy-MM-dd  "));
-            if (!endDtIsValid) return Ok(JsonView(false, "结束格式错误!正确时间格式:yyyy-MM-dd  "));
+            #region 参数验证
+
+            if (!DateTime.TryParseExact(dto.yearMonth, "yyyy-MM", CultureInfo.InvariantCulture, DateTimeStyles.None, out var yearMonthDt))
+                return Ok(JsonView(false, "年月格式错误! 正确格式: yyyy-MM"));
+
+            if (!DateTime.TryParseExact(dto.startDt, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var startDt))
+                return Ok(JsonView(false, "开始日期格式错误! 正确格式: yyyy-MM-dd"));
+
+            if (!DateTime.TryParseExact(dto.endDt, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var endDt))
+                return Ok(JsonView(false, "结束日期格式错误! 正确格式: yyyy-MM-dd"));
+
+            // 确保逻辑区间正确
+            if (startDt > endDt)
+                return Ok(JsonView(false, "开始时间不能晚于结束时间"));
+
+            #endregion
 
             string thisYearMonth = dto.yearMonth;
             string preYearMonth = yearMonthDt.AddMonths(-1).ToString("yyyy-MM");
 
-            //计算本月工资起止时间 比如是2月的1号-28号,那就是2月1号的零点到3月1号的零点 
+            // 计算本月工资起止时间 比如是2月的1号-28号,那就是2月1号的零点到3月1号的零点 
             DateTime thisStartDt = startDt;
             DateTime thisEndDt = endDt; //
 
-            //本月工资是否有数据 有数据则不计算
-            result = await _wageSheetRep.Get_WageSheet_ListByYearMonthAsync(thisYearMonth);
-            if (result.Code == 0)
-            {
-                return Ok(JsonView(false, thisYearMonth + " 工资数据已存在,若无人员工资请手动添加!"));
-            }
+            // 检查是否存在
+            var isExists = await _sqlSugar.Queryable<Pm_WageSheet>().AnyAsync(it => it.YearMonth == dto.yearMonth);
+            if (isExists) return Ok(JsonView(false, $"{dto.yearMonth} 工资数据已存在。"));
+
+            var preWageSheetItems = await _sqlSugar.Queryable<Pm_WageSheet>()
+                .Where(it => it.IsDel == 0 && it.YearMonth == preYearMonth)
+                .ToListAsync();
+
+            if (!preWageSheetItems.Any()) return Ok(JsonView(false, "上月工资数据不存在。"));
 
-            //获取上个月工资信息
-            List<Pm_WageSheet> preWageSheetItems = await _wageSheetRep._sqlSugar.Queryable<Pm_WageSheet>().Where(it => it.IsDel == 0 && it.YearMonth == preYearMonth).ToListAsync();
-            preWageSheetItems = preWageSheetItems.OrderBy(it => it.UserId).ToList();
-            if (preWageSheetItems.Count <= 0)
-            {
-                return Ok(JsonView(false, thisYearMonth + " 上月工资数据不存在,请手动添加!"));
-            }
             //处理上个月同月同人 多条数据
-            List<Pm_WageSheet> preWageSheetItems1 = new();
-            //preWageSheetItems1 = preWageSheetItems.GroupBy(it => new { it.YearMonth, it.UserId })
-            //                                      .Select(it => it.FirstOrDefault(item => item.Basic != 0))
-            //                                      .ToList();
-            preWageSheetItems1 = preWageSheetItems
+            List<Pm_WageSheet> preWageSheetItems1 =  preWageSheetItems
                 .GroupBy(it => new { it.YearMonth, it.UserId })
                 .Select(it => it.FirstOrDefault(item => item.Basic != 0))
                 .Where(it => it != null)
                 .ToList()!;
 
-            //获取OA系统内所有用户
-            List<UserNameView> userNames = _usersRep._sqlSugar.SqlQueryable<UserNameView>("Select Id,CnName From Sys_Users").ToList();
+            ////获取OA系统内所有用户
+            //var userNames = _usersRep._sqlSugar.Queryable<Sys_Users>()
+            //    .Where(x => x.IsDel == 0)
+            //    .Select(x => new UserNameView
+            //    {
+            //        Id = x.Id,
+            //        CnName = x.CnName
+            //    })
+            //    .ToList();
 
-            List<Pm_WageSheet> wageSheets = new();
+            //_result = await PayrollComputation1.SalaryCalculatorAsync(preWageSheetItems1, userNames, dto.UserId, thisYearMonth, thisStartDt, thisEndDt);
 
-            _result = await PayrollComputation.SalaryCalculatorAsync(preWageSheetItems1, userNames, dto.UserId, thisYearMonth, thisStartDt, thisEndDt);
+            _result = await PayrollComputation_v1.SalaryCalculatorAsync(preWageSheetItems1, 208, thisYearMonth, startDt, endDt);
 
             #region 批量添加
 
@@ -577,7 +560,7 @@ namespace OASystem.API.Controllers
                 return Ok(JsonView(false, _result.Msg));
             }
 
-            wageSheets = _result.Data;
+            List<Pm_WageSheet> wageSheets = _result.Data;
 
             var add = await _wageSheetRep._sqlSugar.Insertable(wageSheets).ExecuteCommandAsync();
             if (add <= 0)
@@ -587,23 +570,6 @@ namespace OASystem.API.Controllers
 
             #endregion
 
-            #region 处理返回数据
-            //List <WageSheetItemInfoView> wageSheetItems = new List<WageSheetItemInfoView>();
-            //wageSheetItems = _mapper.Map<List<WageSheetItemInfoView>>(wageSheets);
-            //wageSheetItems = wageSheetItems.Select(it => 
-            //        {
-            //            UserNameView? uName1 = new UserNameView();
-            //            UserNameView? uName2 = new UserNameView();
-            //            uName1 = userNames.Where(it1 => it.UserId == it1.Id).FirstOrDefault();
-            //            if (uName1 != null)  it.Name = uName1.CnName;
-
-            //            uName2 = userNames.Where(it1 => it.LastUpdateUserId == it1.Id).FirstOrDefault();
-            //            if (uName2 != null) it.LastUpdateUserName = uName2.CnName;
-
-            //            return it; }
-            //        ).ToList();
-            #endregion
-
             sw.Stop();
             return Ok(JsonView(true, "操作成功! 耗时:" + (sw.ElapsedMilliseconds / 1000) + "s"));
         }
@@ -620,18 +586,22 @@ namespace OASystem.API.Controllers
             Stopwatch sw = new();
             sw.Start();
 
-            //参数处理
-            string ymFormat = "yyyy-MM";
-            string ymdFormat = "yyyy-MM-dd";
-            bool yearMonthDtIsValid = DateTime.TryParseExact(dto.YearMonth, ymFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime yearMonthDt);
-            bool startDtIsValid = DateTime.TryParseExact(dto.StartDate, ymdFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime startDt);
-            bool endDtIsValid = DateTime.TryParseExact(dto.EndDate, ymdFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime endDt);
+            #region 参数验证
 
-            if (!yearMonthDtIsValid) return Ok(JsonView(false, "年月格式错误!正确时间格式:yyyy-MM  "));
+            if (!DateTime.TryParseExact(dto.YearMonth, "yyyy-MM", CultureInfo.InvariantCulture, DateTimeStyles.None, out var yearMonthDt))
+                return Ok(JsonView(false, "年月格式错误! 正确格式: yyyy-MM"));
+
+            if (!DateTime.TryParseExact(dto.StartDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var startDt))
+                return Ok(JsonView(false, "开始日期格式错误! 正确格式: yyyy-MM-dd"));
 
-            if (!startDtIsValid) return Ok(JsonView(false, "开始时间格式错误!正确时间格式:yyyy-MM-dd  "));
+            if (!DateTime.TryParseExact(dto.EndDate, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var endDt))
+                return Ok(JsonView(false, "结束日期格式错误! 正确格式: yyyy-MM-dd"));
 
-            if (!yearMonthDtIsValid) return Ok(JsonView(false, "结束时间格式错误!正确时间格式:yyyy-MM-dd  "));
+            // 确保逻辑区间正确
+            if (startDt > endDt)
+                return Ok(JsonView(false, "开始时间不能晚于结束时间"));
+
+            #endregion
 
             List<Pm_WageSheet> wageSheets = new();
             Pm_WageSheet wageSheet = _mapper.Map<Pm_WageSheet>(dto);
@@ -645,7 +615,7 @@ namespace OASystem.API.Controllers
             //获取OA系统内所有用户
             List<UserNameView> userNames = _usersRep._sqlSugar.SqlQueryable<UserNameView>("Select Id,CnName From Sys_Users").ToList();
 
-            _result = await PayrollComputation.SalaryCalculatorAsync(wageSheets, userNames, dto.UserId, dto.YearMonth, startDt, endDt);
+            _result = await PayrollComputation1.SalaryCalculatorAsync(wageSheets, userNames, dto.UserId, dto.YearMonth, startDt, endDt);
 
             if (_result.Code != 0)
             {

+ 1 - 1
OASystem/OASystem.Api/Controllers/ResourceController.cs

@@ -2865,7 +2865,7 @@ Inner Join Sys_Department as d With(Nolock) On u.DepId=d.Id Where m.Id={0} ", _m
                 cleanJson = cleanJson.Split("```")[1].Split("```")[0];
 
             return JsonConvert.DeserializeObject<T>(cleanJson.Trim());
-        }
+        }   
 
         /// <summary>
         ///  商邀资料AI 混元AI续写(SSE流式推送)

+ 31 - 64
OASystem/OASystem.Api/OAMethodLib/PayrollComputation.cs

@@ -8,13 +8,17 @@ namespace OASystem.API.OAMethodLib
     /// <summary>
     /// 工资计算
     /// </summary>
-    public static class PayrollComputation
+    public static class PayrollComputation1
     {
         private static Result _result = new Result();
         private static readonly IQiYeWeChatApiService _qiYeWeChatApiService = AutofacIocManager.Instance.GetService<IQiYeWeChatApiService>();
         private static readonly UsersRepository _usersRep = AutofacIocManager.Instance.GetService<UsersRepository>();
         private static readonly IMapper _mapper = AutofacIocManager.Instance.GetService<IMapper>();
-        private static readonly decimal _chengDuMinimumWage = 2100.00M * 0.80M; //员工在病假医疗期限内的病假工资按照成都市最低工资标准的 80%发放 
+        /// <summary>
+        /// 成都最低工资
+        /// *员工在病假医疗期限内的病假工资按照成都市最低工资标准的 80%发放 
+        /// </summary>
+        private static readonly decimal _chengDuMinimumWage = 2100.00M * 0.80M; 
 
         /// <summary>
         /// 计算工资
@@ -84,12 +88,11 @@ namespace OASystem.API.OAMethodLib
             sys_Calendars = await _usersRep._sqlSugar.SqlQueryable<Sys_Calendar>(sys_sql).ToListAsync();
 
             //筛选出工作日日报
-            List<Root> workday_userRoots = checkInDayDataView.datas.Where(it => it.base_info.day_type == 0 && it.base_info.record_type == 1).ToList();
+            List<CheckInDayRoot> workday_userRoots = checkInDayDataView.datas.Where(it => it.base_info.day_type == 0 && it.base_info.record_type == 1).ToList();
 
             //工作日日报
             workday_userRoots = workday_userRoots.OrderBy(it => it.base_info.date).ToList();
 
-
             #region 特殊日期-不用打卡日期信息
 
             //获取企业打卡规则
@@ -102,25 +105,23 @@ namespace OASystem.API.OAMethodLib
 
             int spe_offdays = 0;
 
-            GroupItem group = corpCheckInRole.group.Where(it => it.groupid == 4).FirstOrDefault();
-            if (group != null)
-            {
-                foreach (var item in group.spe_offdays)
-                {
-                    if (item.begtime_dt >= startDt && item.endtime_dt <= endDt)
-                    {
-                        if (item.endtime_dt > startDt)
-                        {
-                            TimeSpan ts = item.endtime_dt - item.begtime_dt;
-                            spe_offdays = (ts.Days + 1);
-                        }
-                    }
-                }
-            }
+            //GroupItem group = corpCheckInRole.group.Where(it => it.GroupId == 4).FirstOrDefault();
+            //if (group != null)
+            //{
+            //    foreach (var item in group.spe_offdays)
+            //    {
+            //        if (item.begtime_dt >= startDt && item.endtime_dt <= endDt)
+            //        {
+            //            if (item.endtime_dt > startDt)
+            //            {
+            //                TimeSpan ts = item.endtime_dt - item.begtime_dt;
+            //                spe_offdays = (ts.Days + 1);
+            //            }
+            //        }
+            //    }
+            //}
             #endregion
 
-
-
             //获取 请假类型 Sp_Detail.template_id
             string leave_template_id = "C4NzTJCh1onCUK915rRkvy7Fh5Vqz4YbiEV9jrBY1";
             List<VacationLeaveTypeView> vacationLeaveTypes = await GetVacationLeaveTypes(leave_template_id);
@@ -160,21 +161,6 @@ namespace OASystem.API.OAMethodLib
                 //wx_useridDic.Add("宋夏雨", "songxiayu");
                 //wx_useridDic.Add("王思雨", "ysw");
                 //wx_useridDic.Add("汪燕平", "yime");
-                //wx_useridDic.Add("张海麟", "fred.chang@pan-american-intl.com");
-                //wx_useridDic.Add("张海麟", "fred.chang@pan-american-intl.com");
-                //wx_useridDic.Add("张海麟", "fred.chang@pan-american-intl.com");
-                //wx_useridDic.Add("张海麟", "fred.chang@pan-american-intl.com");
-                //wx_useridDic.Add("张海麟", "fred.chang@pan-american-intl.com");
-                //wx_useridDic.Add("张海麟", "fred.chang@pan-american-intl.com");
-                //wx_useridDic.Add("张海麟", "fred.chang@pan-american-intl.com");
-                //wx_useridDic.Add("张海麟", "fred.chang@pan-american-intl.com");
-                //wx_useridDic.Add("张海麟", "fred.chang@pan-american-intl.com");
-                //wx_useridDic.Add("张海麟", "fred.chang@pan-american-intl.com");
-                //wx_useridDic.Add("张海麟", "fred.chang@pan-american-intl.com");
-                //wx_useridDic.Add("张海麟", "fred.chang@pan-american-intl.com");
-                //wx_useridDic.Add("张海麟", "fred.chang@pan-american-intl.com");
-
-
                 #endregion
 
                 foreach (var pm_wsInfo in pm_WageSheetDattaSources)
@@ -225,11 +211,7 @@ namespace OASystem.API.OAMethodLib
                     Ex_Items ex_Items_cc = new Ex_Items() { Type = "出差" };   //出差
                     #endregion
 
-
-                    List<Root> userRoots = new List<Root>();
-
-                    //string wx_userid = "";
-
+                    List<CheckInDayRoot> userRoots = new List<CheckInDayRoot>();
 
                     if (_name == "蔡雯")
                     {
@@ -262,7 +244,7 @@ namespace OASystem.API.OAMethodLib
 
                         string acctid = userRoots[0].base_info.acctid;
                         List<Ex_Item> ex_reissuecard_Items = new List<Ex_Item>(); //打卡类型 数据
-                        List<Sp_items> acc_sp_items = new List<Sp_items>(); //审批数据
+                        List<SpItem> acc_sp_items = new List<SpItem>(); //审批数据
 
                         //int user_probationary_bk_num = 0;
 
@@ -287,9 +269,9 @@ namespace OASystem.API.OAMethodLib
 
                         foreach (var root in userRoots)
                         {
-                            List<Holiday_infos> holiday_Infos = root.holiday_infos; //当天假勤信息
-                            List<Exception_infos> exception_infos = root.exception_infos; //当天校准状态信息
-                            List<Sp_items> sp_Items = root.sp_items;//当天假勤统计信息
+                            List<HolidayInfo> holiday_Infos = root.holiday_infos; //当天假勤信息
+                            List<ExceptionInfo> exception_infos = root.exception_infos; //当天校准状态信息
+                            List<SpItem> sp_Items = root.sp_items;//当天假勤统计信息
                             if (sp_Items.Count > 0)
                             {
                                 sp_Items = sp_Items.Where(it => it.count > 0).ToList();
@@ -323,7 +305,7 @@ namespace OASystem.API.OAMethodLib
                                     SubTypeId = 4,
                                     SubType = "旷工",
                                     Duration = timelength,
-                                    StartTimeDt = Convert.ToDateTime(root.base_info.dateDt.ToString("yyyy-MM-dd HH:mm:ss")),
+                                    StartTimeDt = Convert.ToDateTime(root.base_info.DateDt.ToString("yyyy-MM-dd HH:mm:ss")),
                                     Unit = "分钟",
                                 };
 
@@ -737,7 +719,7 @@ namespace OASystem.API.OAMethodLib
                         }
                         #endregion
 
-                        #region 迟到早旷工 日期排序
+                        #region 迟到早退旷工 日期排序
 
                         if (ex_reissuecard_Items.Count > 0)
                         {
@@ -754,7 +736,7 @@ namespace OASystem.API.OAMethodLib
                         //类型:1 - 请假;2 - 补卡;3 - 出差;4 - 外出;100 - 外勤
                         leaveNum = acc_sp_items.Where(it => it.type == 1).ToList().Count();
                         reissuecardNum = acc_sp_items.Where(it => it.type == 2).ToList().Count();
-                        List<Sp_items> acc_sp_items_evection = new List<Sp_items>();
+                        List<SpItem> acc_sp_items_evection = new List<SpItem>();
                         acc_sp_items_evection = acc_sp_items.Where(it => it.type == 3).ToList();
                         evectionNum = acc_sp_items_evection.Count();
 
@@ -1928,7 +1910,6 @@ namespace OASystem.API.OAMethodLib
             }
         }
 
-
         /// <summary>
         /// 计算类型费用
         /// 病假 事假 计算 按小时计算
@@ -2339,21 +2320,6 @@ namespace OASystem.API.OAMethodLib
             return num;
         }
 
-        /// <summary>
-        /// 打卡数据
-        /// 异常数据 统计
-        /// </summary>
-        /// <returns></returns>
-        private static int ExceptionStatistics(List<Exception_Info> datas, int type)
-        {
-            int num = 0;
-
-            Exception_Info _Info = datas.Where(it => it.exception == type).FirstOrDefault();
-            if (_Info != null) { num = _Info.count; }
-
-            return num;
-        }
-
         /// <summary>
         /// 获取时间段内除周末 节假日外的 工作日
         /// </summary>
@@ -2388,5 +2354,6 @@ namespace OASystem.API.OAMethodLib
             IEnumerable<System.Reflection.PropertyInfo> property = from pi in t.GetProperties() where pi.Name.ToLower() == field.ToLower() select pi;
             return property.First().GetValue(info, null);
         }
+
     }
 }

+ 903 - 0
OASystem/OASystem.Api/OAMethodLib/PayrollComputation_v1.cs

@@ -0,0 +1,903 @@
+using OASystem.API.OAMethodLib.QiYeWeChatAPI;
+using OASystem.Domain.Entities.PersonnelModule;
+using OASystem.Domain.ViewModels.CRM;
+using OASystem.Domain.ViewModels.PersonnelModule;
+using OASystem.Domain.ViewModels.QiYeWeChat;
+using System.Diagnostics.Tracing;
+
+namespace OASystem.API.OAMethodLib;
+
+/// <summary>
+/// 工资计算公共参数配置
+/// </summary>
+public static class PayrollConfig
+{
+    // ========== 基础参数 ==========
+    public const decimal ChengduMinimumWage = 2100m;
+    public const decimal SickLeaveWageRatio = 0.8m;
+    public const decimal WorkHoursPerDay = 7.5m;
+    public const decimal MealSubsidyPerDay = 10m;
+
+    // ========== 迟到早退规则 ==========
+    public const int FreeTotalMinutes = 10;           // 累计免罚分钟(超过此值才罚款)
+    public const decimal FinePerEvent = 50m;          // 每次事件罚款金额
+    public const int WorkStartHour = 9;
+    public const int WorkEndHour = 18;
+    public const int DefaultAllowSeconds = 300;       // 默认允许迟到/早退秒数(当规则未设置时使用)
+
+    // ========== 旷工判定 ==========
+    public const int HalfDayMissMinutes = 60;         // 半日旷工下限(分钟)
+    public const int FullDayMissMinutes = 180;        // 全日旷工阈值(分钟)
+
+    // ========== 补卡规则 ==========
+    public const int ProbationFreeCardCount = 2;
+    public const int RegularFreeCardCount = 3;
+    public const decimal PunchCorrectionLowFine = 10m;
+    public const bool PunchCorrectionToAbsentAfterFourth = true;
+
+    // ========== 请假餐补规则 ==========
+    public const int MealDeductionLeaveHours = 3;
+}
+
+/// <summary>
+/// 工资计算
+/// </summary>
+public static class PayrollComputation_v1
+{
+    private static Result _result = new Result();
+    private static readonly IQiYeWeChatApiService _qiYeWeChatApiService = AutofacIocManager.Instance.GetService<IQiYeWeChatApiService>();
+    private static readonly SqlSugarClient _sqlSugar = AutofacIocManager.Instance.GetService<SqlSugarClient>();
+
+    // =============================== 内部类定义 ===============================
+
+    /// <summary>
+    /// 解析后的打卡规则(仅用于当前计算,不缓存)
+    /// </summary>
+    private class ParsedCheckInRule
+    {
+        public uint GroupId { get; set; }
+        public int FlexOnDutyTime { get; set; } = 0;      // 允许迟到秒数
+        public int FlexOffDutyTime { get; set; } = 0;     // 允许早退秒数
+        public HashSet<DateTime> SpecialWorkDays { get; set; } = new HashSet<DateTime>();
+        public HashSet<DateTime> SpecialOffDays { get; set; } = new HashSet<DateTime>();
+    }
+
+    /// <summary>
+    /// 考勤扣款结果
+    /// </summary>
+    private class AttendancePenaltyResult
+    {
+        public decimal LateFine { get; set; }
+        public decimal EarlyFine { get; set; }
+        public decimal AbsenteeismDeduction { get; set; }
+        public decimal MealDeduction { get; set; }
+        public decimal TotalDeduction => LateFine + EarlyFine + AbsenteeismDeduction;
+        public List<ExItem> Details { get; set; } = new List<ExItem>();
+    }
+
+    /// <summary>
+    /// 假勤扣款结果
+    /// </summary>
+    private class LeaveDeductionResult
+    {
+        public decimal PersonalLeaveTotal { get; set; }
+        public decimal SickLeaveTotal { get; set; }
+        public decimal MealDeduction { get; set; }
+        public decimal TotalDeduction => PersonalLeaveTotal + SickLeaveTotal;
+        public List<ExItem> Details { get; set; } = new List<ExItem>();
+    }
+
+    /// <summary>
+    /// 补卡扣款结果
+    /// </summary>
+    private class PunchCorrectionResult
+    {
+        public decimal MissPunchFine { get; set; }
+        public decimal AbsenteeismDeduction { get; set; }
+        public decimal MealDeduction { get; set; }
+        public decimal TotalDeduction => MissPunchFine + AbsenteeismDeduction;
+        public List<ExItem> Details { get; set; } = new List<ExItem>();
+    }
+
+    /// <summary>
+    /// 扣款明细项(用于序列化)
+    /// </summary>
+    private class ExItem
+    {
+        public int SubTypeId { get; set; }
+        public string? SubType { get; set; }
+        public decimal Deduction { get; set; } = 0.00M;
+        public decimal MealDeduction { get; set; } = 0.00M;
+        public DateTime StartTimeDt { get; set; }
+        public DateTime EndTimeDt { get; set; }
+        public decimal Duration { get; set; }
+        public string Unit { get; set; } = "小时";
+        public string? Reason { get; set; }
+        public DateTime Apply_time_dt { get; set; }
+        public List<string>? Approval_name { get; set; }
+    }
+
+    /// <summary>
+    /// 扣款分类容器
+    /// </summary>
+    private class ExItems
+    {
+        public string? Type { get; set; }
+        public object? ExItemInfo { get; set; }
+    }
+
+    /// <summary>
+    /// 请假/调休/出差明细(用于跨月拆分)
+    /// </summary>
+    private class LeaveDetailItem
+    {
+        public int TypeId { get; set; }
+        public string TypeName { get; set; } = "";
+        public DateTime Start { get; set; }
+        public DateTime End { get; set; }
+        public decimal Days { get; set; }
+        public decimal Hours { get; set; }
+        public string DtType { get; set; } = "";
+        public decimal NewDuration { get; set; }
+        public Slice_info SliceInfo { get; set; } = new Slice_info();
+        public DateTime ApplyDt { get; set; }
+        public bool IsBusinessTrip { get; set; }
+    }
+
+    /// <summary>
+    /// 判断是否为工作日(基于规则中的特殊日期和系统日历)
+    /// </summary>
+    private static bool IsWorkDayForRule( DateTime date, List<Sys_Calendar> sysCalendars)
+    {
+        var calendar = sysCalendars.FirstOrDefault(c => c.Dt == date.ToString("yyyy-MM-dd"));
+        if (calendar != null)
+            return calendar.IsWorkDay;
+
+        // 默认周一到周五为工作日
+        return date.DayOfWeek != DayOfWeek.Saturday && date.DayOfWeek != DayOfWeek.Sunday;
+    }
+
+    /// <summary>
+    /// 计算工资
+    /// </summary>
+    public static async Task<Result> SalaryCalculatorAsync(
+        List<Pm_WageSheet> pm_WageSheetDattaSources, int userId, string thisYearMonth, DateTime startDt, DateTime endDt)
+    {
+        if (pm_WageSheetDattaSources.Count <= 0)
+        {
+            _result.Msg = "计算工资传入数据为空!";
+            return _result;
+        }
+
+        // 获取当月应出勤天数
+        int workDays = await GetWorkDays(thisYearMonth);
+        if (workDays <= 0)
+        {
+            _result.Msg = $"{thisYearMonth} 工作日未设置,请前往《工作日管理页面》设置!";
+            return _result;
+        }
+
+        var userIds = pm_WageSheetDattaSources.Select(ws => ws.UserId).Distinct().ToList();
+
+        var userIdInfos = await GetSysUserWeChatIds(userIds);
+        if (!userIdInfos.Any())
+        {
+            _result.Msg = $"企业微信Id获取失败,请联系管理员!";
+            return _result;
+        }
+
+        List<string> qyWhchatIdList = userIdInfos.Select(it => it.WeChatId ?? "").Distinct().ToList();
+
+        // 获取打卡日报数据(包含 exception_infos)
+        CheckInDayDataView checkInDayDataView = await _qiYeWeChatApiService.GetCheckInDayDataAsync(qyWhchatIdList, startDt, endDt);
+        if (checkInDayDataView.errcode != 0)
+        {
+            _result.Msg = $"【企业微信】【打卡】【获取时间段内所有日打卡】【Msg】{checkInDayDataView.errmsg}";
+            return _result;
+        }
+
+        // 获取系统日历(用于判断工作日)
+        List<Sys_Calendar> sys_Calendars = await GetSysCalendars(startDt, endDt);
+
+        string _name = "";
+        try
+        {
+            foreach (var pm_wsInfo in pm_WageSheetDattaSources)
+            {
+                _name = userIdInfos.Find(it => it.Id == pm_wsInfo.UserId)?.CnName ?? "Unknown";
+
+                // 获取员工当月的打卡日报数据(按日期分组)
+                var userDailyRecords = GetUserDailyRecords(checkInDayDataView, _name);
+
+                // 计算日薪
+                decimal amountPayable = GetTotalSalaryBase(pm_wsInfo);
+                decimal dailyWage = ConvertToDecimal(amountPayable / workDays);
+                decimal sickLeaveDailyWage = ConvertToDecimal((PayrollConfig.ChengduMinimumWage * PayrollConfig.SickLeaveWageRatio) / workDays);
+
+                // 获取员工企业微信ID(用于获取审批数据)
+                string acctid = userDailyRecords.Count > 0 ? userDailyRecords.First().Value.base_info.acctid : "";
+
+                // 计算考勤扣款(使用日报中的 exception_infos)
+                var attendanceResult = CalculateAttendanceFromDailyRecords(userDailyRecords, dailyWage, sys_Calendars);
+
+                // 计算假勤扣款(请假、出差等)
+                var leaveResult = new LeaveDeductionResult();
+                // 计算补卡扣款
+                var punchResult = new PunchCorrectionResult();
+
+                // 当月假勤次数
+                var spCount = userDailyRecords.Sum(x => x.Value.sp_items?.Sum(y => y.count) ?? 0);
+
+                //当月存在假勤的情况,执行假勤扣除
+                if (spCount > 0)
+                {
+                    leaveResult = await CalculateLeaveDeductionAsync(acctid, dailyWage, sickLeaveDailyWage, amountPayable, workDays, startDt, endDt, sys_Calendars);
+                    punchResult = await CalculatePunchCorrectionAsync(acctid, pm_wsInfo.Floats, dailyWage, startDt, endDt);
+                }
+
+                // 计算餐补
+                int actualWorkDays = userDailyRecords.Count(r => IsWorkDayForRule(r.Key, sys_Calendars));
+                decimal mealSubsidy = actualWorkDays * PayrollConfig.MealSubsidyPerDay;
+                decimal mealDeductionTotal = attendanceResult.MealDeduction + leaveResult.MealDeduction + punchResult.MealDeduction;
+                decimal mealActual = mealSubsidy - mealDeductionTotal;
+
+                // 汇总扣款
+                decimal totalDeduction = attendanceResult.TotalDeduction + leaveResult.TotalDeduction + punchResult.TotalDeduction
+                    + pm_wsInfo.WithholdingInsurance + pm_wsInfo.ReservedFunds + pm_wsInfo.OtherDeductions;
+
+                // 应发合计
+                decimal shouldTotal = amountPayable + mealActual + pm_wsInfo.OtherHandle;
+                decimal afterTax = Math.Floor((shouldTotal - totalDeduction - pm_wsInfo.WithholdingTax) * 100) / 100;
+
+                // 组装扣款明细JSON
+                var exItemsList = new List<ExItems>();
+                if (attendanceResult.Details.Count > 0)
+                    exItemsList.Add(new ExItems { Type = "打卡", ExItemInfo = attendanceResult.Details });
+                if (leaveResult.Details.Count > 0)
+                    exItemsList.Add(new ExItems { Type = "假勤", ExItemInfo = leaveResult.Details });
+                if (punchResult.Details.Count > 0)
+                    exItemsList.Add(new ExItems { Type = "补卡", ExItemInfo = punchResult.Details });
+
+                string exItemsRemark = exItemsList.Count > 0 ? JsonConvert.SerializeObject(exItemsList) : "";
+
+                // 更新工资表
+                UpdateWageSheet(pm_wsInfo, thisYearMonth, startDt, endDt, workDays, actualWorkDays,
+                    mealActual, leaveResult, attendanceResult, punchResult, shouldTotal, totalDeduction, afterTax, userId, exItemsRemark);
+            }
+        }
+        catch (Exception ex)
+        {
+            _result.Msg = $"【{_name}】【Msg:{ex.Message}】";
+            return _result;
+        }
+
+        _result.Code = 0;
+        _result.Data = pm_WageSheetDattaSources;
+        return _result;
+    }
+
+    // =============================== 辅助方法 ===============================
+
+    private static decimal GetTotalSalaryBase(Pm_WageSheet wageSheet)
+    {
+        return wageSheet.Basic + wageSheet.Floats + wageSheet.PostAllowance + wageSheet.InformationSecurityFee + wageSheet.OtherSubsidies;
+    }
+
+    private static async Task<List<Sys_Calendar>> GetSysCalendars(DateTime startDt, DateTime endDt)
+    {
+        string sql = string.Format("Select * From Sys_Calendar Where Isdel = 0 And Dt between '{0}' And '{1}'",
+            startDt.ToString("yyyy-MM-dd"), endDt.ToString("yyyy-MM-dd"));
+        return await _sqlSugar.SqlQueryable<Sys_Calendar>(sql).ToListAsync();
+    }
+
+    private static async Task<List<UserWeChatIdView>> GetSysUserWeChatIds(List<int> ints)
+    {
+        return await _sqlSugar.Queryable<Sys_Users>().Where(u => ints.Contains(u.Id) && u.IsDel == 0)
+            .Select(u => new UserWeChatIdView { Id = u.Id, CnName = u.CnName, WeChatId = u.QiyeChatUserId })
+            .ToListAsync();
+    }
+
+    /// <summary>
+    /// 获取员工每日的打卡日报数据(按日期索引)
+    /// </summary>
+    private static Dictionary<DateTime, CheckInDayRoot> GetUserDailyRecords(CheckInDayDataView view, string name)
+    {
+        var dict = new Dictionary<DateTime, CheckInDayRoot>();
+
+        var allRecords = view.datas ?? new List<CheckInDayRoot>();
+        IEnumerable<CheckInDayRoot> userRecords  = allRecords.Where(r => r.base_info.name == name || r.base_info.name.Contains(name)).OrderBy(x => x.base_info.DateDt);
+        
+        foreach (var record in userRecords)
+        {
+            DateTime date = record.base_info.DateDt.Date;
+            dict[date] = record;
+        }
+
+        return dict;
+    }
+
+    /// <summary>
+    /// 基于日报数据中的 exception_infos 计算考勤扣款(次数累计 + 分时段罚款)
+    /// </summary>
+    private static AttendancePenaltyResult CalculateAttendanceFromDailyRecords(
+        Dictionary<DateTime, CheckInDayRoot> dailyRecords,
+        decimal dailyWage,
+        List<Sys_Calendar> sysCalendars)
+    {
+        var result = new AttendancePenaltyResult();
+        // 统计本月内迟到/早退次数(仅统计时长<60分钟的事件)
+        int lateEarlyCount = 0;        // 总次数(用于判定是否超过2次)
+        int lateCount = 0;             // 迟到次数(保留,暂未独立使用)
+        int earlyCount = 0;            // 早退次数
+
+        foreach (var kvp in dailyRecords)
+        {
+            DateTime date = kvp.Key;
+            var record = kvp.Value;
+
+            // 判断是否为工作日(若不是工作日,不计算考勤扣款)
+            bool isWorkDay = IsWorkDayForRule(date, sysCalendars);
+            if (!isWorkDay) continue;
+
+            var exceptions = record.exception_infos ?? new List<ExceptionInfo>();
+            foreach (var ex in exceptions)
+            {
+                int exceptionType = ex.exception;   // 1迟到 2早退 3缺卡 4旷工
+                int minutes = ex.duration / 60;     // 时长(分钟)
+                string reason = $"{date:yyyy-MM-dd} ";
+                decimal deduction = 0m;
+
+                switch (exceptionType)
+                {
+                    case 1: // 迟到
+                        if (minutes < 60)
+                        {
+                            // 迟到时长小于60分钟,计入次数,并根据分钟数确定罚款
+                            lateEarlyCount++;
+                            lateCount++;
+
+                            if (minutes < 10)   // 不足10分钟:第3次起罚50
+                            {
+                                if (lateEarlyCount >= 3)
+                                {
+                                    deduction = PayrollConfig.FinePerEvent; // 50元
+                                    result.LateFine += deduction;
+                                    reason += $"迟到{minutes}分钟(第{lateEarlyCount}次),按次数罚款{deduction}元";
+                                }
+                                else
+                                {
+                                    reason += $"迟到{minutes}分钟(第{lateEarlyCount}次),前2次不处罚";
+                                }
+                            }
+                            else if (minutes >= 10 && minutes < 30)  // 9:10~9:30 => 罚款20
+                            {
+                                deduction = 20m;
+                                result.LateFine += deduction;
+                                reason += $"迟到{minutes}分钟,罚款{deduction}元";
+                            }
+                            else if (minutes >= 30 && minutes < 60)  // 9:30~9:59 => 罚款50
+                            {
+                                deduction = PayrollConfig.FinePerEvent; // 50元
+                                result.LateFine += deduction;
+                                reason += $"迟到{minutes}分钟,罚款{deduction}元";
+                            }
+
+                            result.Details.Add(new ExItem
+                            {
+                                SubTypeId = 1,
+                                SubType = "迟到",
+                                Deduction = deduction,
+                                StartTimeDt = date,
+                                Reason = reason
+                            });
+                        }
+                        else
+                        {
+                            // 迟到≥60分钟,按旷工处理
+                            if (minutes >= PayrollConfig.HalfDayMissMinutes && minutes <= PayrollConfig.FullDayMissMinutes)
+                            {
+                                decimal ded = ConvertToDecimal(dailyWage / 2);
+                                result.AbsenteeismDeduction += ded;
+                                result.MealDeduction += PayrollConfig.MealSubsidyPerDay;
+                                result.Details.Add(new ExItem
+                                {
+                                    SubTypeId = 4,
+                                    SubType = "旷工(迟到超时)",
+                                    Deduction = ded,
+                                    StartTimeDt = date,
+                                    Reason = reason + $"迟到{minutes}分钟,按旷工半天处理,扣除餐补"
+                                });
+                            }
+                            else // minutes > 180
+                            {
+                                decimal ded = ConvertToDecimal(dailyWage);
+                                result.AbsenteeismDeduction += ded;
+                                result.MealDeduction += PayrollConfig.MealSubsidyPerDay;
+                                result.Details.Add(new ExItem
+                                {
+                                    SubTypeId = 4,
+                                    SubType = "旷工(迟到超时)",
+                                    Deduction = ded,
+                                    StartTimeDt = date,
+                                    Reason = reason + $"迟到{minutes}分钟,按旷工全天处理,扣除餐补"
+                                });
+                            }
+                        }
+                        break;
+
+                    case 2: // 早退
+                        if (minutes < 60)
+                        {
+                            lateEarlyCount++;
+                            earlyCount++;
+
+                            if (minutes < 10)
+                            {
+                                if (lateEarlyCount >= 3)
+                                {
+                                    deduction = PayrollConfig.FinePerEvent;
+                                    result.EarlyFine += deduction;
+                                    reason += $"早退{minutes}分钟(第{lateEarlyCount}次),按次数罚款{deduction}元";
+                                }
+                                else
+                                {
+                                    reason += $"早退{minutes}分钟(第{lateEarlyCount}次),前2次不处罚";
+                                }
+                            }
+                            else if (minutes >= 10 && minutes < 30)
+                            {
+                                deduction = 20m;
+                                result.EarlyFine += deduction;
+                                reason += $"早退{minutes}分钟,罚款{deduction}元";
+                            }
+                            else if (minutes >= 30 && minutes < 60)
+                            {
+                                deduction = PayrollConfig.FinePerEvent; // 50元
+                                result.EarlyFine += deduction;
+                                reason += $"早退{minutes}分钟,罚款{deduction}元";
+                            }
+
+                            result.Details.Add(new ExItem
+                            {
+                                SubTypeId = 2,
+                                SubType = "早退",
+                                Deduction = deduction,
+                                StartTimeDt = date,
+                                Reason = reason
+                            });
+                        }
+                        else
+                        {
+                            // 早退≥60分钟,按旷工处理
+                            if (minutes >= PayrollConfig.HalfDayMissMinutes && minutes <= PayrollConfig.FullDayMissMinutes)
+                            {
+                                decimal ded = ConvertToDecimal(dailyWage / 2);
+                                result.AbsenteeismDeduction += ded;
+                                result.MealDeduction += PayrollConfig.MealSubsidyPerDay;
+                                result.Details.Add(new ExItem
+                                {
+                                    SubTypeId = 4,
+                                    SubType = "旷工(早退超时)",
+                                    Deduction = ded,
+                                    StartTimeDt = date,
+                                    Reason = reason + $"早退{minutes}分钟,按旷工半天处理,扣除餐补"
+                                });
+                            }
+                            else
+                            {
+                                decimal ded = ConvertToDecimal(dailyWage);
+                                result.AbsenteeismDeduction += ded;
+                                result.MealDeduction += PayrollConfig.MealSubsidyPerDay;
+                                result.Details.Add(new ExItem
+                                {
+                                    SubTypeId = 4,
+                                    SubType = "旷工(早退超时)",
+                                    Deduction = ded,
+                                    StartTimeDt = date,
+                                    Reason = reason + $"早退{minutes}分钟,按旷工全天处理,扣除餐补"
+                                });
+                            }
+                        }
+                        break;
+
+                    // 缺卡、旷工处理保持不变
+                    case 3:
+                        {
+                            decimal ded = ConvertToDecimal(dailyWage / 2);
+                            result.AbsenteeismDeduction += ded;
+                            result.MealDeduction += PayrollConfig.MealSubsidyPerDay;
+                            result.Details.Add(new ExItem
+                            {
+                                SubTypeId = 4,
+                                SubType = "旷工(缺卡)",
+                                Deduction = ded,
+                                StartTimeDt = date,
+                                Reason = reason + "缺卡,按半天旷工处理,扣除餐补"
+                            });
+                        }
+                        break;
+                    case 4:
+                        if (minutes >= PayrollConfig.HalfDayMissMinutes && minutes <= PayrollConfig.FullDayMissMinutes)
+                        {
+                            decimal ded = ConvertToDecimal(dailyWage / 2);
+                            result.AbsenteeismDeduction += ded;
+                            result.MealDeduction += PayrollConfig.MealSubsidyPerDay;
+                            result.Details.Add(new ExItem
+                            {
+                                SubTypeId = 4,
+                                SubType = "旷工",
+                                Deduction = ded,
+                                StartTimeDt = date,
+                                Reason = reason + "旷工半天,扣除餐补"
+                            });
+                        }
+                        else
+                        {
+                            decimal ded = ConvertToDecimal(dailyWage);
+                            result.AbsenteeismDeduction += ded;
+                            result.MealDeduction += PayrollConfig.MealSubsidyPerDay;
+                            result.Details.Add(new ExItem
+                            {
+                                SubTypeId = 4,
+                                SubType = "旷工",
+                                Deduction = ded,
+                                StartTimeDt = date,
+                                Reason = reason + "旷工全天,扣除餐补"
+                            });
+                        }
+                        break;
+                }
+            }
+        }
+        return result;
+    }
+
+    /// <summary>
+    /// 计算假勤扣款(请假、出差等)
+    /// </summary>
+    private static async Task<LeaveDeductionResult> CalculateLeaveDeductionAsync(string acctid, decimal dailyWage, decimal sickLeaveDailyWage,
+        decimal amountPayable, int workDays, DateTime startDt, DateTime endDt, List<Sys_Calendar> sysCalendars)
+    {
+        var result = new LeaveDeductionResult();
+
+        if (string.IsNullOrEmpty(acctid))
+            return result;
+
+        // 获取请假审批
+        List<Sp_Detail> spLeaveDetails = await _qiYeWeChatApiService.GetApprovalDetailsAsync(startDt, endDt, acctid, 2, 1);
+
+        if (spLeaveDetails.Count > 0)
+        {
+            var leaveDetails = new List<LeaveDetailItem>();
+            foreach (var sp in spLeaveDetails)
+            {
+                var applyData = sp.apply_data;
+                if (applyData == null) continue;
+
+                var vacationControl = applyData.contents?.FirstOrDefault(c => c.control == "Vacation");
+                if (vacationControl?.value?.vacation == null) continue;
+
+                var vac = vacationControl.value.vacation;
+                var attendance = vac.attendance;
+                var selector = vac.selector;
+                if (selector?.options == null || selector.options.Count == 0) continue;
+
+                int leaveType = int.Parse(selector.options[0].key);
+                var dateRange = attendance.date_range;
+
+                // 只处理当月的假勤
+                if (dateRange.new_begin_dt < startDt || dateRange.new_begin_dt > endDt) continue;
+
+                var sliceInfo = (leaveType == 2 || leaveType == 3) ? attendance.slice_info : new Slice_info();
+
+                var splitItems = SplitLeaveByMonth(dateRange, startDt, endDt, sysCalendars, leaveType, sliceInfo);
+                leaveDetails.AddRange(splitItems);
+            }
+
+            if (leaveDetails.Any()) leaveDetails = leaveDetails.OrderBy(x => x.Start).ToList();
+
+            foreach (var item in leaveDetails)
+            {
+                decimal leaveMeal = 0;
+                decimal deduction = 0;
+                bool isMealDeduct = (item.Hours >= PayrollConfig.MealDeductionLeaveHours || item.Days >= 0.5m);
+
+                if (item.TypeId == 2) // 事假
+                {
+                    deduction = ConvertToDecimal(dailyWage * item.Days);
+                    if (deduction == 0 && item.Hours > 0)
+                        deduction = ConvertToDecimal((dailyWage / PayrollConfig.WorkHoursPerDay) * item.Hours);
+                    result.PersonalLeaveTotal += deduction;
+                }
+                else if (item.TypeId == 3) // 病假
+                {
+                    decimal deductPerDay = ConvertToDecimal(dailyWage - sickLeaveDailyWage);
+                    if (deductPerDay > 0)
+                    {
+                        deduction = ConvertToDecimal(deductPerDay * item.Days);
+                        if (deduction == 0 && item.Hours > 0)
+                            deduction = ConvertToDecimal((deductPerDay / PayrollConfig.WorkHoursPerDay) * item.Hours);
+                    }
+                    result.SickLeaveTotal += deduction;
+                }
+                // 年假、调休假不扣薪、视情况扣除餐补
+                if (isMealDeduct)
+                {
+                    if (item.Hours >=0 || item.Days >= 0.5m)
+                    {
+                        leaveMeal = PayrollConfig.MealSubsidyPerDay;
+                    }
+                }
+                    
+                result.MealDeduction += leaveMeal;
+
+                // 产生扣款时记录
+
+                if ((deduction + leaveMeal) > 0m)
+                {
+                    result.Details.Add(new ExItem
+                    {
+                        SubTypeId = item.TypeId,
+                        SubType = GetLeaveTypeName(item.TypeId),
+                        Deduction = deduction,
+                        MealDeduction = leaveMeal,
+                        StartTimeDt = item.Start,
+                        EndTimeDt = item.End,
+                        Duration = item.Days > 0 ? item.Days : item.Hours,
+                        Unit = item.Days > 0 ? "天" : "小时",
+                        Reason = $"{GetLeaveTypeName(item.TypeId)} {(item.Days > 0 ? item.Days.ToString() + "天" : item.Hours.ToString() + "小时")}"
+                    });
+                }
+            }
+        }
+
+        #region 处理出差
+        // 处理出差
+        List<Sp_Detail> spTripDetails = await _qiYeWeChatApiService.GetApprovalDetailsAsync(startDt, endDt, acctid, 2, 3);
+        if (spTripDetails.Count > 0)
+        {
+            foreach (var sp in spTripDetails)
+            {
+                var applyData = sp.apply_data;
+                if (applyData == null) continue;
+
+                var attendanceControl = applyData.contents?.FirstOrDefault(c => c.control == "Attendance");
+                if (attendanceControl?.value?.attendance == null) continue;
+
+                var dateRange = attendanceControl.value.attendance.date_range;
+                var splitTrips = SplitTripByMonth(dateRange, startDt, endDt);
+
+                foreach (var trip in splitTrips)
+                {
+                    decimal tripMeal = trip.Days * PayrollConfig.MealSubsidyPerDay;
+                    result.MealDeduction += tripMeal;
+                    result.Details.Add(new ExItem
+                    {
+                        SubTypeId = 5,
+                        SubType = "出差",
+                        Deduction = 0,
+                        MealDeduction = tripMeal,
+                        StartTimeDt = trip.Start,
+                        EndTimeDt = trip.End,
+                        Duration = trip.Days,
+                        Unit = "天",
+                        Reason = $"出差 {trip.Days}天,无餐补"
+                    });
+                }
+            }
+        }
+        #endregion
+
+        return result;
+    }
+
+    /// <summary>
+    /// 计算补卡扣款
+    /// </summary>
+    private static async Task<PunchCorrectionResult> CalculatePunchCorrectionAsync(string acctid, decimal floats, decimal dailyWage,
+        DateTime startDt, DateTime endDt)
+    {
+        var result = new PunchCorrectionResult();
+        if (string.IsNullOrEmpty(acctid)) return result;
+
+        var bukaDetails = await _qiYeWeChatApiService.GetApprovalDetailsAsync(startDt, endDt, acctid, 2, 2);
+        if (bukaDetails.Count == 0) return result;
+
+        bool isProbation = floats == 0;
+        int freeCount = isProbation ? PayrollConfig.ProbationFreeCardCount : PayrollConfig.RegularFreeCardCount;
+        int bukaNum = 1;
+
+        foreach (var sp in bukaDetails)
+        {
+            var applyData = sp.apply_data;
+            if (applyData == null) continue;
+
+            var punchCorrControl = applyData.contents?.FirstOrDefault(c => c.control == "PunchCorrection");
+            if (punchCorrControl?.value?.punch_correction == null) continue;
+
+            DateTime missTime = punchCorrControl.value.punch_correction.time_dt;
+            DateTime missDate = missTime.Date;
+
+            if (missDate < startDt || missDate > endDt) continue;
+
+            if (bukaNum <= freeCount + 1)
+            {
+                result.MissPunchFine += PayrollConfig.PunchCorrectionLowFine;
+                result.Details.Add(new ExItem
+                {
+                    SubTypeId = 7,
+                    SubType = "打卡补卡",
+                    Deduction = PayrollConfig.PunchCorrectionLowFine,
+                    StartTimeDt = missTime,
+                    Reason = $"第{bukaNum}次补卡,罚款{PayrollConfig.PunchCorrectionLowFine}元"
+                });
+            }
+            else if (PayrollConfig.PunchCorrectionToAbsentAfterFourth)
+            {
+                // 第4次起按旷工半天处理
+                decimal ded = ConvertToDecimal(dailyWage / 2);
+                result.AbsenteeismDeduction += ded;
+                result.MealDeduction += PayrollConfig.MealSubsidyPerDay;
+                result.Details.Add(new ExItem
+                {
+                    SubTypeId = 4,
+                    SubType = "旷工(补卡超限)",
+                    Deduction = ded,
+                    StartTimeDt = missDate,
+                    Reason = $"第{bukaNum}次补卡超限,按旷工半天处理"
+                });
+            }
+            bukaNum++;
+        }
+
+        return result;
+    }
+
+    /// <summary>
+    /// 出差按月份拆分(支持 halfday / hour 类型,按自然日扣除餐补)
+    /// </summary>
+    private static List<LeaveDetailItem> SplitTripByMonth(Date_range dateRange, DateTime monthStart, DateTime monthEnd)
+    {
+        var result = new List<LeaveDetailItem>();
+        if (dateRange == null) return result;
+
+        // 验证起止时间有效性
+        DateTime start = dateRange.new_begin_dt;
+        DateTime end = dateRange.new_end_dt;
+        if (start > end) (start, end) = (end, start);
+
+        // 跨月范围检查
+        if (end < monthStart || start > monthEnd) return result;
+
+        DateTime actualStart = start < monthStart ? monthStart : start;
+        DateTime actualEnd = end > monthEnd ? monthEnd : end;
+
+        // 计算该月内的出差天数(按自然日,包含首尾)
+        int days = (actualEnd.Date - actualStart.Date).Days + 1;
+
+        // 如果出差跨月,且开始/结束是半天,仍按整天扣除餐补(业务规则:出差即无餐补)
+        result.Add(new LeaveDetailItem
+        {
+            TypeId = 5,
+            TypeName = "出差",
+            Start = actualStart,
+            End = actualEnd,
+            Days = days,
+            Hours = days * PayrollConfig.WorkHoursPerDay,  // 仅用于记录,不影响扣款
+            DtType = "halfday",
+            IsBusinessTrip = true
+        });
+        return result;
+    }
+
+    /// <summary>
+    /// 请假按月份拆分(支持 halfday / hour,区分工作日)
+    /// </summary>
+    private static List<LeaveDetailItem> SplitLeaveByMonth(Date_range dateRange, DateTime monthStart, DateTime monthEnd,
+        List<Sys_Calendar> sysCalendars, int leaveType, Slice_info? sliceInfo)
+    {
+        var result = new List<LeaveDetailItem>();
+        if (dateRange == null) return result;
+
+        DateTime start = dateRange.new_begin_dt;
+        DateTime end = dateRange.new_end_dt;
+        if (start > end) (start, end) = (end, start);
+
+        if (end < monthStart || start > monthEnd) return result;
+
+        DateTime actualStart = start < monthStart ? monthStart : start;
+        DateTime actualEnd = end > monthEnd ? monthEnd : end;
+
+        decimal days = 0, hours = 0;
+        for (var dt = actualStart.Date; dt <= actualEnd.Date; dt = dt.AddDays(1))
+        {
+            bool isWorkDay = sysCalendars.Any(c => c.Dt == dt.ToString("yyyy-MM-dd") && c.IsWorkDay);
+            if (!isWorkDay) continue;
+
+            if (dt == actualStart.Date && dt == actualEnd.Date)
+            {
+                if (dateRange.type.Equals("halfday"))
+                {
+                    days = dateRange.new_duration / 86400m;
+                }
+                else if (dateRange.type.Equals("hour"))
+                {
+                    hours = dateRange.new_duration / 3600m;
+                }
+            }
+            else if (dt == actualStart.Date || dt == actualEnd.Date)
+            {
+                days += 0.5M;
+                hours += PayrollConfig.WorkHoursPerDay / 2;
+            }
+            else
+            {
+                days += 1;
+                hours += PayrollConfig.WorkHoursPerDay;
+            }
+        }
+
+        result.Add(new LeaveDetailItem
+        {
+            TypeId = leaveType,
+            TypeName = GetLeaveTypeName(leaveType),
+            Start = actualStart,
+            End = actualEnd,
+            Days = days,
+            Hours = hours,
+            DtType = dateRange.type
+        });
+        return result;
+    }
+
+    private static string GetLeaveTypeName(int typeId)
+    {
+        return typeId switch
+        {
+            1 => "年假",
+            2 => "事假",
+            3 => "病假",
+            4 => "调休假",
+            5 => "出差",
+            _ => "其他"
+        };
+    }
+
+    private static void UpdateWageSheet(Pm_WageSheet ws, string yearMonth, DateTime start, DateTime end,
+        int workDays, int actualWorkDays, decimal mealActual, LeaveDeductionResult leave,
+        AttendancePenaltyResult attendance, PunchCorrectionResult punch,
+        decimal shouldTotal, decimal totalDeduction, decimal afterTax, int userId, string exItemsRemark)
+    {
+        ws.YearMonth = yearMonth;
+        ws.StartDate = start.ToString("yyyy-MM-dd");
+        ws.EndDate = end.ToString("yyyy-MM-dd");
+        ws.WorkDays = workDays;
+        ws.RegularDays = actualWorkDays;
+        ws.SickLeave = leave.SickLeaveTotal;
+        ws.SomethingFalse = leave.PersonalLeaveTotal;
+        ws.LateTo = attendance.LateFine;
+        ws.LeaveEarly = attendance.EarlyFine;
+        ws.Absenteeism = attendance.AbsenteeismDeduction + punch.AbsenteeismDeduction;
+        ws.NotPunch = punch.MissPunchFine;
+        ws.Mealsupplement = mealActual;
+        ws.Should = ConvertToDecimal(shouldTotal);
+        ws.TotalDeductions = ConvertToDecimal(totalDeduction);
+        ws.TotalRealHair = ConvertToDecimal(afterTax);
+        ws.AfterTax = ConvertToDecimal(afterTax);
+        ws.Ex_ItemsRemark = exItemsRemark;
+        ws.LastUpdateUserId = userId;
+        ws.LastUpdateDt = DateTime.Now;
+    }
+
+    public static async Task<int> GetWorkDays(string yearMonth)
+    {
+        string sql = $"Select * From Pm_WageIssueWorkingDay Where Isdel = 0 And YearMonth = '{yearMonth}'";
+        var data = await _sqlSugar.SqlQueryable<WageYearMonthView>(sql).FirstAsync();
+        return data?.Workdays ?? 0;
+    }
+
+    public static decimal ConvertToDecimal(decimal value)
+    {
+        return Math.Floor(value * 100) / 100;
+    }
+}

+ 1 - 2
OASystem/OASystem.Api/OAMethodLib/QiYeWeChatAPI/IQiYeWeChatApiService.cs

@@ -148,7 +148,7 @@ namespace OASystem.API.OAMethodLib.QiYeWeChatAPI
         int size,
         List<FilterCondition> filters);
 
-        //// <summary>
+        /// <summary>
         /// 批量获取审批详情
         /// <param name="startDt"></param>
         /// <param name="endDt"></param>
@@ -196,7 +196,6 @@ namespace OASystem.API.OAMethodLib.QiYeWeChatAPI
         /// <returns></returns>
         Task<JournalDetailView> GetJournalRecordDetailAsync(string journaluuid);
 
-
         /// <summary>
         /// hook 发送团组出发信息给财务群
         /// </summary>

+ 0 - 5
OASystem/OASystem.Api/OAMethodLib/QiYeWeChatAPI/QiYeWeChatApiService.cs

@@ -526,7 +526,6 @@ namespace OASystem.API.OAMethodLib.QiYeWeChatAPI
             corpCheckInRuleView = System.Text.Json.JsonSerializer.Deserialize<CorpCheckInRuleView>(stringResponse,
                 new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
 
-
             return corpCheckInRuleView;
         }
 
@@ -989,7 +988,6 @@ namespace OASystem.API.OAMethodLib.QiYeWeChatAPI
             checkInDataView = System.Text.Json.JsonSerializer.Deserialize<CheckInDataView>(stringResponse,
                 new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
 
-
             if (centerDt < endDt)
             {
                 checkInData_Req.starttime = centerTs;
@@ -1010,7 +1008,6 @@ namespace OASystem.API.OAMethodLib.QiYeWeChatAPI
                 return checkInDataView;
             }
 
-
             return checkInDataView;
         }
 
@@ -1085,7 +1082,6 @@ namespace OASystem.API.OAMethodLib.QiYeWeChatAPI
                 return checkInDayDataView;
             }
 
-
             //获取打卡数据 token
             Access_TokenView access_Token = await GetTokenAsync(2);
             if (access_Token.errcode != 0)
@@ -1170,7 +1166,6 @@ namespace OASystem.API.OAMethodLib.QiYeWeChatAPI
                     new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
             }
 
-
             return checkInDayDataView;
         }
 

+ 1 - 1
OASystem/OASystem.Api/Program.cs

@@ -15,6 +15,7 @@ using OASystem.API.OAMethodLib.Hotmail;
 using OASystem.API.OAMethodLib.Hub.Hubs;
 using OASystem.API.OAMethodLib.HunYuanAPI;
 using OASystem.API.OAMethodLib.JuHeAPI;
+using OASystem.API.OAMethodLib.MicrosoftGraphMailbox;
 using OASystem.API.OAMethodLib.QiYeWeChatAPI;
 using OASystem.API.OAMethodLib.Quartz.Jobs;
 using OASystem.API.OAMethodLib.SignalR.HubService;
@@ -29,7 +30,6 @@ using TencentCloud.Common;
 using TencentCloud.Common.Profile;
 using TencentCloud.Hunyuan.V20230901;
 using static OASystem.API.Middlewares.RateLimitMiddleware;
-using OASystem.API.OAMethodLib.MicrosoftGraphMailbox;
 
 Console.Title = $"FMGJ OASystem Server";
 var builder = WebApplication.CreateBuilder(args);

+ 15 - 1
OASystem/OASystem.Api/appsettings.json

@@ -614,5 +614,19 @@
       "Tenant": "common",
       "RedirectUri": "http://localhost:5256/api/AITest/auth/callback"
     }
-  ]
+  ],
+  //
+  "Payroll": {
+    "ChengDuMinimumWage": 2100.00,
+    "SickLeaveWageRatio": 0.80,
+    "MealSubsidyPerDay": 10.00,
+    "WorkHoursPerDay": 7.50,
+    "LateEarlyAccumulatedFreeMinutes": 10,
+    "LateEarlyPenaltyAmount": 50.00,
+    "HalfDayAbsenteeismMinutes": 60,
+    "FullDayAbsenteeismMinutes": 180,
+    "ReissuePenaltyThreshold": 3,
+    "ReissuePenaltyAmount": 10.00,
+    "MealDeductionThresholdHours": 3.0
+  }
 }

+ 0 - 2
OASystem/OASystem.Domain/Entities/PersonnelModule/Pm_WageSheet.cs

@@ -98,8 +98,6 @@ namespace OASystem.Domain.Entities.PersonnelModule
         [SugarColumn(IsNullable = true, ColumnDataType = "decimal(8,2)")]
         public decimal TrafficSubsidies { get; set; }
 
-
-
         /// <summary>
         /// 操作奖金 弃用
         /// </summary>

+ 10 - 0
OASystem/OASystem.Domain/Result.cs

@@ -21,6 +21,16 @@ namespace OASystem.Domain
             Msg = msg;
             Data = data;
         }
+
+        public static Result Success(dynamic? data = null)
+        {
+            return new Result(0, "操作成功", data);
+        }
+
+        public static Result Fail(string msg = "操作失败", dynamic? data = null)
+        {
+            return new Result(-1, msg, data);
+        }
     }
 
 

+ 2 - 2
OASystem/OASystem.Domain/ViewModels/PersonnelModule/WageSheetView.cs

@@ -70,7 +70,6 @@ namespace OASystem.Domain.ViewModels.PersonnelModule
         /// </summary>
         public int RegularDays { get; set; }
 
-
         /// <summary>
         /// 基本工资
         /// </summary>
@@ -531,7 +530,6 @@ namespace OASystem.Domain.ViewModels.PersonnelModule
         /// </summary>
         public decimal Deduction { get; set; } = 0.00M;
 
-
         /// <summary>
         /// 餐补扣款金额
         /// </summary>
@@ -573,6 +571,8 @@ namespace OASystem.Domain.ViewModels.PersonnelModule
         public List<string>? Approval_name { get; set; }
     }
 
+
+
     #endregion
 
     #region 法定节假日

+ 218 - 114
OASystem/OASystem.Domain/ViewModels/QiYeWeChat/CheckInDataView.cs

@@ -1,128 +1,232 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+namespace OASystem.Domain.ViewModels.QiYeWeChat;
 
-namespace OASystem.Domain.ViewModels.QiYeWeChat
+/// <summary>
+/// 打卡记录数据
+/// </summary>
+public class CheckInDataView : ResponseBase
 {
+    public List<CheckInDataInfo> checkindata { get; set; }
+}
+
+public class CheckInDataInfo
+{
+    /// <summary>
+    /// 用户id
+    /// </summary>
+    public string userid { get; set; }
+
+    /// <summary>
+    /// 打卡规则名称
+    /// </summary>
+    public string groupname { get; set; }
+
+    /// <summary>
+    /// 打卡类型。字符串,目前有:上班打卡,下班打卡,外出打卡
+    /// </summary>
+    public string checkin_type { get; set; }
+
+    /// <summary>
+    /// 异常类型,字符串,包括:时间异常,地点异常,未打卡,wifi异常,非常用设备。如果有多个异常,以分号间隔
+    /// </summary>
+    public string exception_type { get; set; }
+
     /// <summary>
-    /// 打卡记录数据
+    /// 打卡时间。Unix时间戳
     /// </summary>
-    public class CheckInDataView : ResponseBase
+    public long checkin_time { get; set; }
+
+    /// <summary>
+    /// 打卡时间。Unix时间戳
+    /// </summary>
+    public DateTime checkin_time_dt
     {
-        public List<CheckInDataInfo> checkindata { get; set; }
+        get
+        {
+            return new DateTime(checkin_time * 10000000 + 621355968000000000L).ToLocalTime();
+        }
     }
 
-    public class CheckInDataInfo
+    /// <summary>
+    /// 打卡地点title
+    /// </summary>
+    public string location_title { get; set; }
+
+    /// <summary>
+    /// 打卡地点详情
+    /// </summary>
+    public string location_detail { get; set; }
+
+    /// <summary>
+    /// 打卡wifi名称
+    /// </summary>
+    public string wifiname { get; set; }
+
+    /// <summary>
+    /// 打卡备注
+    /// </summary>
+    public string notes { get; set; }
+
+    /// <summary>
+    /// 打卡的MAC地址/bssid
+    /// </summary>
+    public string wifimac { get; set; }
+
+    /// <summary>
+    /// 打卡的附件media_id,可使用media/get获取附件
+    /// </summary>
+    public List<string> mediaids { get; set; }
+
+    /// <summary>
+    /// 位置打卡地点纬度,是实际纬度的1000000倍,与腾讯地图一致采用GCJ-02坐标系统标准
+    /// </summary>
+    public long lat { get; set; }
+
+    /// <summary>
+    /// 位置打卡地点经度,是实际经度的1000000倍,与腾讯地图一致采用GCJ-02坐标系统标准
+    /// </summary>
+    public long lng { get; set; }
+
+    /// <summary>
+    /// 打卡设备id
+    /// </summary>
+    public string deviceid { get; set; }
+
+    /// <summary>
+    /// 标准打卡时间,指此次打卡时间对应的标准上班时间或标准下班时间
+    /// </summary>
+    public long sch_checkin_time { get; set; }
+
+    /// <summary>
+    /// 打卡时间。Unix时间戳
+    /// </summary>
+    public DateTime sch_checkin_time_dt
     {
-        /// <summary>
-        /// 用户id
-        /// </summary>
-        public string userid { get; set; }
-
-        /// <summary>
-        /// 打卡规则名称
-        /// </summary>
-        public string groupname { get; set; }
-
-        /// <summary>
-        /// 打卡类型。字符串,目前有:上班打卡,下班打卡,外出打卡
-        /// </summary>
-        public string checkin_type { get; set; }
-
-        /// <summary>
-        /// 异常类型,字符串,包括:时间异常,地点异常,未打卡,wifi异常,非常用设备。如果有多个异常,以分号间隔
-        /// </summary>
-        public string exception_type { get; set; }
-
-        /// <summary>
-        /// 打卡时间。Unix时间戳
-        /// </summary>
-        public long checkin_time { get; set; }
-
-        /// <summary>
-        /// 打卡时间。Unix时间戳
-        /// </summary>
-        public DateTime checkin_time_dt {
-            get
-            {
-                return new DateTime(checkin_time * 10000000 + 621355968000000000L).ToLocalTime();
-            }
+        get
+        {
+            return new DateTime(sch_checkin_time * 10000000 + 621355968000000000L).ToLocalTime();
         }
+    }
+
+    /// <summary>
+    /// 规则id,表示打卡记录所属规则的id
+    /// </summary>
+    public int groupid { get; set; }
+    /// <summary>
+    /// 班次id,表示打卡记录所属规则中,所属班次的id
+    /// </summary>
+    public int schedule_id { get; set; }
+    /// <summary>
+    /// 时段id,表示打卡记录所属规则中,某一班次中的某一时段的id,如上下班时间为9:00-12:00、13:00-18:00的班次中,9:00-12:00为其中一组时段
+    /// </summary>
+    public int timeline_id { get; set; }
+
+    // ==================== 扩展属性(用于业务判断) ====================
+
+    /// <summary>
+    /// 是否为未打卡异常
+    /// </summary>
+    public bool IsMissPunch => !string.IsNullOrEmpty(exception_type) && exception_type.Contains("未打卡");
+
+    /// <summary>
+    /// 是否为上班打卡
+    /// </summary>
+    public bool IsMorningCheckIn => checkin_type == "上班打卡";
+
+    /// <summary>
+    /// 是否为下班打卡
+    /// </summary>
+    public bool IsEveningCheckIn => checkin_type == "下班打卡";
+
+    /// <summary>
+    /// 是否为外出打卡
+    /// </summary>
+    public bool IsOutCheckIn => checkin_type == "外出打卡";
+
+    /// <summary>
+    /// 是否为正常打卡(无异常)
+    /// </summary>
+    public bool IsNormalCheckIn => string.IsNullOrEmpty(exception_type) || exception_type == "正常";
+
+    /// <summary>
+    /// 是否为时间异常
+    /// </summary>
+    public bool IsTimeException => !string.IsNullOrEmpty(exception_type) && exception_type.Contains("时间异常");
+
+    /// <summary>
+    /// 是否为地点异常
+    /// </summary>
+    public bool IsLocationException => !string.IsNullOrEmpty(exception_type) && exception_type.Contains("地点异常");
+
+    /// <summary>
+    /// 是否为WiFi异常
+    /// </summary>
+    public bool IsWifiException => !string.IsNullOrEmpty(exception_type) && exception_type.Contains("wifi异常");
+
+    /// <summary>
+    /// 是否为非常用设备
+    /// </summary>
+    public bool IsUnusualDevice => !string.IsNullOrEmpty(exception_type) && exception_type.Contains("非常用设备");
 
-        /// <summary>
-        /// 打卡地点title
-        /// </summary>
-        public string location_title { get; set; }
-
-        /// <summary>
-        /// 打卡地点详情
-        /// </summary>
-        public string location_detail { get; set; }
-
-        /// <summary>
-        /// 打卡wifi名称
-        /// </summary>
-        public string wifiname { get; set; }
-
-        /// <summary>
-        /// 打卡备注
-        /// </summary>
-        public string notes { get; set; }
-
-        /// <summary>
-        /// 打卡的MAC地址/bssid
-        /// </summary>
-        public string wifimac { get; set; }
-
-        /// <summary>
-        /// 打卡的附件media_id,可使用media/get获取附件
-        /// </summary>
-        public List<string> mediaids { get; set; }
-
-        /// <summary>
-        /// 位置打卡地点纬度,是实际纬度的1000000倍,与腾讯地图一致采用GCJ-02坐标系统标准
-        /// </summary>
-        public long lat { get; set; }
-
-        /// <summary>
-        /// 位置打卡地点经度,是实际经度的1000000倍,与腾讯地图一致采用GCJ-02坐标系统标准
-        /// </summary>
-        public long lng { get; set; }
-
-        /// <summary>
-        /// 打卡设备id
-        /// </summary>
-        public string deviceid { get; set; }
-
-        /// <summary>
-        /// 标准打卡时间,指此次打卡时间对应的标准上班时间或标准下班时间
-        /// </summary>
-        public long sch_checkin_time { get; set; }
-
-        /// <summary>
-        /// 打卡时间。Unix时间戳
-        /// </summary>
-        public DateTime sch_checkin_time_dt
+    /// <summary>
+    /// 获取异常类型描述
+    /// </summary>
+    public string GetExceptionDescription()
+    {
+        if (string.IsNullOrEmpty(exception_type))
+            return "正常";
+
+        return exception_type;
+    }
+
+    /// <summary>
+    /// 获取打卡类型描述
+    /// </summary>
+    public string GetCheckInTypeDescription()
+    {
+        return checkin_type switch
         {
-            get
-            {
-                return new DateTime(sch_checkin_time * 10000000 + 621355968000000000L).ToLocalTime();
-            }
-        }
+            "上班打卡" => "上班打卡",
+            "下班打卡" => "下班打卡",
+            "外出打卡" => "外出打卡",
+            _ => checkin_type
+        };
+    }
 
-        /// <summary>
-        /// 规则id,表示打卡记录所属规则的id
-        /// </summary>
-        public int groupid { get; set; }
-        /// <summary>
-        /// 班次id,表示打卡记录所属规则中,所属班次的id
-        /// </summary>
-        public int schedule_id { get; set; }
-        /// <summary>
-        /// 时段id,表示打卡记录所属规则中,某一班次中的某一时段的id,如上下班时间为9:00-12:00、13:00-18:00的班次中,9:00-12:00为其中一组时段
-        /// </summary>
-        public int timeline_id { get; set; }
+    /// <summary>
+    /// 获取打卡时间(HH:mm格式)
+    /// </summary>
+    public string GetCheckInTimeString()
+    {
+        return checkin_time_dt.ToString("HH:mm:ss");
+    }
+
+    /// <summary>
+    /// 获取标准打卡时间(HH:mm格式)
+    /// </summary>
+    public string GetStandardCheckInTimeString()
+    {
+        return sch_checkin_time_dt.ToString("HH:mm:ss");
+    }
+
+    /// <summary>
+    /// 判断是否为上午打卡(9:00 - 12:00)
+    /// </summary>
+    public bool IsMorningTimeSlot()
+    {
+        var timeOfDay = checkin_time_dt.TimeOfDay;
+        var morningStart = TimeSpan.FromHours(8);
+        var morningEnd = TimeSpan.FromHours(12);
+        return timeOfDay >= morningStart && timeOfDay <= morningEnd;
+    }
+
+    /// <summary>
+    /// 判断是否为下午打卡(13:30 - 18:00)
+    /// </summary>
+    public bool IsAfternoonTimeSlot()
+    {
+        var timeOfDay = checkin_time_dt.TimeOfDay;
+        var afternoonStart = TimeSpan.FromHours(13).Add(TimeSpan.FromMinutes(30));
+        var afternoonEnd = TimeSpan.FromHours(18).Add(TimeSpan.FromMinutes(30));
+        return timeOfDay >= afternoonStart && timeOfDay <= afternoonEnd;
     }
 }

+ 240 - 234
OASystem/OASystem.Domain/ViewModels/QiYeWeChat/CheckInDayDataView.cs

@@ -1,240 +1,246 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+namespace OASystem.Domain.ViewModels.QiYeWeChat;
 
-namespace OASystem.Domain.ViewModels.QiYeWeChat
+/// <summary>
+/// 打卡日报数据响应
+/// </summary>
+public class CheckInDayDataView : ResponseBase
 {
+    /// <summary>日报数据列表</summary>
+    public List<CheckInDayRoot>? datas { get; set; }
+}
+
+/// <summary>
+/// 日报单条数据根对象
+/// </summary>
+public class CheckInDayRoot
+{
+    /// <summary>基础信息</summary>
+    public BaseInfo? base_info { get; set; }
+
+    /// <summary>汇总信息</summary>
+    public SummaryInfo? summary_info { get; set; }
+
+    /// <summary>假勤相关信息列表</summary>
+    public List<HolidayInfo>? holiday_infos { get; set; }
+
+    /// <summary>校准状态信息列表</summary>
+    public List<ExceptionInfo>? exception_infos { get; set; }
+
+    /// <summary>加班信息</summary>
+    public OtInfo? ot_info { get; set; }
+
+    /// <summary>假勤统计信息列表</summary>
+    public List<SpItem>? sp_items { get; set; }
+}
+
+/// <summary>
+/// 假勤相关信息
+/// </summary>
+public class HolidayInfo
+{
+    /// <summary>假勤申请id</summary>
+    public string? sp_number { get; set; }
+
+    /// <summary>假勤信息摘要-标题信息</summary>
+    public SpTitle? sp_title { get; set; }
+
+    /// <summary>假勤信息摘要-描述信息</summary>
+    public SpDescription? sp_description { get; set; }
+}
+
+/// <summary>
+/// 假勤标题信息
+/// </summary>
+public class SpTitle
+{
+    /// <summary>多语言描述列表</summary>
+    public List<LanguageText>? data { get; set; }
+}
+
+/// <summary>
+/// 假勤描述信息
+/// </summary>
+public class SpDescription
+{
+    /// <summary>多语言描述列表</summary>
+    public List<LanguageText>? data { get; set; }
+}
+
+/// <summary>
+/// 多语言文本项
+/// </summary>
+public class LanguageText
+{
+    /// <summary>文本内容</summary>
+    public string? text { get; set; }
+
+    /// <summary>语言类型,如 "zh_CN"</summary>
+    public string? lang { get; set; }
+}
+
+/// <summary>
+/// 打卡时段(上班/下班时间)
+/// </summary>
+public class CheckinTimeItem
+{
+    /// <summary>上班时间(距离0点秒数)</summary>
+    public int work_sec { get; set; }
+
+    /// <summary>下班时间(距离0点秒数)</summary>
+    public int off_work_sec { get; set; }
+}
+
+/// <summary>
+/// 打卡规则信息
+/// </summary>
+public class RuleInfo
+{
+    /// <summary>规则ID</summary>
+    public uint groupid { get; set; }
+
+    /// <summary>规则名称</summary>
+    public string? groupname { get; set; }
+
+    /// <summary>班次ID(仅按班次上下班有效)</summary>
+    public int scheduleid { get; set; }
+
+    /// <summary>班次名称(仅按班次上下班有效)</summary>
+    public string? schedulename { get; set; }
+
+    /// <summary>打卡时间列表(仅固定上下班规则有效)</summary>
+    public List<CheckinTimeItem>? checkintime { get; set; }
+}
+
+/// <summary>
+/// 基础信息
+/// </summary>
+public class BaseInfo
+{
+    /// <summary>日报日期(Unix时间戳,秒)</summary>
+    public long date { get; set; }
+
+    /// <summary>日报日期(DateTime本地时间)</summary>
+    public DateTime DateDt => DateTimeOffset.FromUnixTimeSeconds(date).LocalDateTime;
+
     /// <summary>
-    /// 打卡日报数据 View
+    /// 记录类型
+    /// 1-固定上下班;2-外出(此报表中不会出现);3-按班次上下班;4-自由签到;5-加班;7-无规则
     /// </summary>
-    public class CheckInDayDataView : ResponseBase
-    {
-
-        public List<Root> datas { get; set; }
-
-    }
-
-    public class Root
-    {
-        /// <summary>
-        /// 
-        /// </summary>
-        public Base_info? base_info { get; set; }
-        /// <summary>
-        /// 
-        /// </summary>
-        public Summary_info? summary_info { get; set; }
-        /// <summary>
-        /// 
-        /// </summary>
-        public List<Holiday_infos>? holiday_infos { get; set; }
-        /// <summary>
-        /// 
-        /// </summary>
-        public List<Exception_infos>? exception_infos { get; set; }
-        /// <summary>
-        /// 
-        /// </summary>
-        public Ot_info? ot_info { get; set; }
-        /// <summary>
-        /// 
-        /// </summary>
-        public List<Sp_items>? sp_items { get; set; }
-    }
-
-    public class Exception_infos
-    {
-        /// <summary>
-        /// 校准状态类型:1-迟到;2-早退;3-缺卡;4-旷工;5-地点异常;6-设备异常
-        /// </summary>
-        public int exception { get; set; }
-
-        /// <summary>
-        /// 当日此异常的次数
-        /// </summary>
-        public int count { get; set; }
-
-        /// <summary>
-        /// 当日此异常的时长(迟到/早退/旷工才有值)
-        /// </summary>
-        public int duration { get; set; }
-    }
-
-    public class Holiday_infos
-    {
-        public string? sp_number { get; set; }
-
-        public Sp_title? sp_title { get; set; }
-
-        public Sp_description? sp_description { get; set; }
-    }
-
-
-    public class Sp_title
-    {
-        public List<TitleItem>? data { get; set; }
-
-    }
-
-
-    public class Sp_description
-    {
-
-        public List<TitleItem>? data { get; set; }
-    }
-
-
-    public class CheckintimeItem
-    {
-        /// <summary>
-        /// 
-        /// </summary>
-        public int work_sec { get; set; }
-        /// <summary>
-        /// 
-        /// </summary>
-        public int off_work_sec { get; set; }
-    }
-
-    public class Rule_info
-    {
-        /// <summary>
-        /// 
-        /// </summary>
-        public int groupid { get; set; }
-        /// <summary>
-        /// 普通白班
-        /// </summary>
-        public string groupname { get; set; }
-        /// <summary>
-        /// 
-        /// </summary>
-        public int scheduleid { get; set; }
-        /// <summary>
-        /// 
-        /// </summary>
-        public string schedulename { get; set; }
-        /// <summary>
-        /// 
-        /// </summary>
-        public List<CheckintimeItem> checkintime { get; set; }
-    }
-
-    public class Base_info
-    {
-        /// <summary>
-        /// 日报日期
-        /// </summary>
-        public long date { get; set; }
-
-        /// <summary>
-        /// 日报日期 dt
-        /// </summary>
-        public DateTime dateDt {
-            get
-            {
-                return new DateTime(date * 10000000 + 621355968000000000L).ToLocalTime();
-            }
-        }
-
-        /// <summary>
-        /// 记录类型:1-固定上下班;2-外出(此报表中不会出现外出打卡数据);3-按班次上下班;4-自由签到;5-加班;7-无规则
-        /// </summary>
-        public int record_type { get; set; }
-        /// <summary>
-        /// 打卡人员姓名
-        /// </summary>
-        public string name { get; set; }
-        /// <summary>
-        /// 打卡人员别名
-        /// </summary>
-        public string name_ex { get; set; }
-        /// <summary>
-        /// 打卡人员所在部门,会显示所有所在部门
-        /// </summary>
-        public string departs_name { get; set; }
-        /// <summary>
-        /// 打卡人员账号,即userid
-        /// </summary>
-        public string acctid { get; set; }
-        /// <summary>
-        /// 
-        /// </summary>
-        public Rule_info rule_info { get; set; }
-        /// <summary>
-        /// 日报类型:0-工作日日报;1-休息日日报
-        /// </summary>
-        public int day_type { get; set; }
-    }
-
-    public class Summary_info
-    {
-        /// <summary>
-        /// 
-        /// </summary>
-        public int checkin_count { get; set; }
-        /// <summary>
-        /// 
-        /// </summary>
-        public int regular_work_sec { get; set; }
-        /// <summary>
-        /// 
-        /// </summary>
-        public int standard_work_sec { get; set; }
-        /// <summary>
-        /// 
-        /// </summary>
-        public int earliest_time { get; set; }
-        /// <summary>
-        /// 
-        /// </summary>
-        public int lastest_time { get; set; }
-    }
-
-    public class Ot_info
-    {
-        /// <summary>
-        /// 
-        /// </summary>
-        public int ot_status { get; set; }
-        /// <summary>
-        /// 
-        /// </summary>
-        public int ot_duration { get; set; }
-        /// <summary>
-        /// 
-        /// </summary>
-        public List<long>? exception_duration { get; set; }
-    }
-
-    public class Sp_items
-    {
-        /// <summary>
-        /// 
-        /// </summary>
-        public int type { get; set; }
-        /// <summary>
-        /// 
-        /// </summary>
-        public int vacation_id { get; set; }
-        /// <summary>
-        /// 
-        /// </summary>
-        public int count { get; set; }
-        /// <summary>
-        /// 
-        /// </summary>
-        public int duration { get; set; }
-        /// <summary>
-        /// 
-        /// </summary>
-        public int time_type { get; set; }
-        /// <summary>
-        /// 年假
-        /// </summary>
-        public string name { get; set; }
-    }
+    public int record_type { get; set; }
+
+    /// <summary>打卡人员姓名</summary>
+    public string? name { get; set; }
+
+    /// <summary>打卡人员别名</summary>
+    public string? name_ex { get; set; }
+
+    /// <summary>打卡人员所在部门(多个部门用分号分隔)</summary>
+    public string? departs_name { get; set; }
+
+    /// <summary>打卡人员账号(userid)</summary>
+    public string? acctid { get; set; }
+
+    /// <summary>打卡人员所属规则信息</summary>
+    public RuleInfo? rule_info { get; set; }
+
+    /// <summary>日报类型:0-工作日日报;1-休息日日报</summary>
+    public int day_type { get; set; }
+}
+
+/// <summary>
+/// 汇总信息
+/// </summary>
+public class SummaryInfo
+{
+    /// <summary>当日打卡次数</summary>
+    public int checkin_count { get; set; }
+
+    /// <summary>当日实际工作时长(秒)</summary>
+    public int regular_work_sec { get; set; }
+
+    /// <summary>当日标准工作时长(秒)</summary>
+    public int standard_work_sec { get; set; }
+
+    /// <summary>当日最早打卡时间(秒)</summary>
+    public int earliest_time { get; set; }
+
+    /// <summary>当日最晚打卡时间(秒)</summary>
+    public int lastest_time { get; set; }
+}
+
+/// <summary>
+/// 校准状态信息(异常打卡)
+/// </summary>
+public class ExceptionInfo
+{
+    /// <summary>
+    /// 校准状态类型
+    /// 1-迟到;2-早退;3-缺卡;4-旷工;5-地点异常;6-设备异常
+    /// </summary>
+    public int exception { get; set; }
+
+    /// <summary>当日此异常的次数</summary>
+    public int count { get; set; }
+
+    /// <summary>当日此异常的时长(秒,迟到/早退/旷工才有值)</summary>
+    public int duration { get; set; }
+}
+
+/// <summary>
+/// 加班信息
+/// </summary>
+public class OtInfo
+{
+    /// <summary>加班状态:0-无加班;1-正常;2-缺时长</summary>
+    public int ot_status { get; set; }
+
+    /// <summary>加班时长(秒)</summary>
+    public int ot_duration { get; set; }
+
+    /// <summary>加班不足的时长列表(ot_status=2时有效)</summary>
+    public List<int>? exception_duration { get; set; }
+
+    /// <summary>工作日加班记为调休(秒)</summary>
+    public int workday_over_as_vacation { get; set; }
+
+    /// <summary>工作日加班记为加班费(秒)</summary>
+    public int workday_over_as_money { get; set; }
+
+    /// <summary>休息日加班记为调休(秒)</summary>
+    public int restday_over_as_vacation { get; set; }
+
+    /// <summary>休息日加班记为加班费(秒)</summary>
+    public int restday_over_as_money { get; set; }
+
+    /// <summary>节假日加班记为调休(秒)</summary>
+    public int holiday_over_as_vacation { get; set; }
+
+    /// <summary>节假日加班记为加班费(秒)</summary>
+    public int holiday_over_as_money { get; set; }
+}
+
+/// <summary>
+/// 假勤统计项
+/// </summary>
+public class SpItem
+{
+    /// <summary>
+    /// 类型:1-请假;2-补卡;3-出差;4-外出;15-审批打卡;100-外勤
+    /// </summary>
+    public int type { get; set; }
+
+    /// <summary>具体请假类型id(当type为1时有效)</summary>
+    public int vacation_id { get; set; }
+
+    /// <summary>当日假勤次数</summary>
+    public int count { get; set; }
+
+    /// <summary>当日假勤时长(秒)</summary>
+    public int duration { get; set; }
+
+    /// <summary>时长单位:0-按天 1-按小时</summary>
+    public int time_type { get; set; }
 
+    /// <summary>统计项名称,如“年假”</summary>
+    public string? name { get; set; }
 }

+ 632 - 50
OASystem/OASystem.Domain/ViewModels/QiYeWeChat/CheckInView.cs

@@ -3,6 +3,7 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
+using System.Text.Json.Serialization;
 using System.Threading.Tasks;
 
 namespace OASystem.Domain.ViewModels.QiYeWeChat
@@ -15,81 +16,662 @@ namespace OASystem.Domain.ViewModels.QiYeWeChat
     /// </summary>
     public class CorpCheckInRuleView : ResponseBase
     {
-        /// <summary>
-        /// 企业规则信息列表
-        /// </summary>
-        public List<GroupItem>? group { get; set; }
+        /// <summary>打卡规则列表</summary>
+        public List<CheckInGroup> group { get; set; } = new();
+
+        /// <summary>更新时间戳</summary>
+        public int uptime { get; set; }
     }
 
-    public class GroupItem
+    /// <summary>
+    /// 打卡规则组
+    /// </summary>
+    public class CheckInGroup
     {
-        /// <summary>
-        /// 打卡规则类型,1:固定时间上下班;2:按班次上下班;3:自由上下班
-        /// </summary>
-        public int grouptype { get; set; }
+        /// <summary>打卡规则ID</summary>
+        public uint groupid { get; set; }
 
-        /// <summary>
-        /// 打卡规则id
-        /// </summary>
-        public int groupid { get; set; }
+        /// <summary>打卡规则名称</summary>
+        public string groupname { get; set; } = string.Empty;
 
         /// <summary>
-        /// 打卡规则名称
+        /// 打卡规则类型
+        /// 1:固定时间上下班;2:按班次上下班;3:自由上下班
         /// </summary>
-        public string groupname { get; set; }
+        public uint grouptype { get; set; }
+
+        /// <summary>打卡时间配置(支持多个时段,如早/晚班)</summary>
+        public List<CheckInDate> checkindate { get; set; } = new();
+
+        /// <summary>特殊日期-必须打卡日期</summary>
+        public List<SpecialWorkDay> spe_workdays { get; set; } = new();
+
+        /// <summary>特殊日期-不用打卡日期</summary>
+        public List<SpecialOffDay> spe_offdays { get; set; } = new();
+
+        /// <summary>是否同步法定节假日</summary>
+        public bool sync_holidays { get; set; }
+
+        /// <summary>是否非工作日允许打卡</summary>
+        public bool allow_checkin_offworkday { get; set; }
+
+        /// <summary>是否允许提交补卡申请</summary>
+        public bool allow_apply_offworkday { get; set; }
+
+        /// <summary>每月最多补卡次数,-1表示不限制</summary>
+        public int allow_apply_bk_cnt { get; set; } = -1;
+
+        /// <summary>允许补卡时限(天),-1表示不限制</summary>
+        public int allow_apply_bk_day_limit { get; set; } = -1;
+
+        /// <summary>打卡人员范围</summary>
+        public CheckInRange range { get; set; } = new();
+
+        /// <summary>白名单用户ID列表</summary>
+        public List<string> white_users { get; set; } = new();
+
+        /// <summary>创建时间戳</summary>
+        public int create_time { get; set; }
+
+        /// <summary>打卡方式:0-手机;2-智慧考勤机;3-手机+智慧考勤机</summary>
+        public int type { get; set; }
+
+        /// <summary>是否必须拍照打卡</summary>
+        public bool need_photo { get; set; }
+
+        /// <summary>备注时是否允许上传本地图片</summary>
+        public bool note_can_use_local_pic { get; set; }
+
+        /// <summary>WiFi打卡信息列表</summary>
+        public List<WifiInfo> wifimac_infos { get; set; } = new();
+
+        /// <summary>位置打卡信息列表</summary>
+        public List<LocationInfo> loc_infos { get; set; } = new();
+
+        /// <summary>范围外打卡处理方式:0-不允许打卡;1-允许打卡但标记异常;2-允许打卡且不标记异常</summary>
+        public int option_out_range { get; set; }
+
+        /// <summary>是否开启人脸识别打卡</summary>
+        public bool use_face_detect { get; set; }
+
+        /// <summary>规则创建人userid</summary>
+        public string create_userid { get; set; } = string.Empty;
+
+        /// <summary>规则最近编辑人userid</summary>
+        public string update_userid { get; set; } = string.Empty;
+
+        /// <summary>排班信息(仅规则类型为按班次上下班时有效)</summary>
+        public List<ScheduleInfo> schedulelist { get; set; } = new();
+
+        /// <summary>汇报对象信息</summary>
+        public ReporterInfo reporterinfo { get; set; } = new();
+
+        /// <summary>加班规则V1(旧版)</summary>
+        public OtInfo ot_info { get; set; } = new();
 
         /// <summary>
-        /// 特殊日期-不用打卡日期时间戳
+        /// 以下为补充字段(根据实际JSON)
         /// </summary>
-        public List<spe_offdaysItem> spe_offdays { get; set; }
+
+        /// <summary>下班打卡时间间隔(秒)</summary>
+        public int offwork_interval_time { get; set; }
+
+        /// <summary>是否开启人脸活体检测</summary>
+        public bool open_face_live_detect { get; set; }
+
+        /// <summary>次月补卡限制次数,-1表示不限制</summary>
+        public int buka_limit_next_month { get; set; }
+
+        /// <summary>加班规则V2(新版)</summary>
+        public OtInfoV2 ot_info_v2 { get; set; } = new();
+
+        /// <summary>是否同步外出打卡记录</summary>
+        public bool sync_out_checkin { get; set; }
+
+        /// <summary>补卡提醒配置</summary>
+        public BukaRemind buka_remind { get; set; } = new();
+
+        /// <summary>补卡限制类型(0-不限制,1-仅限本人,2-禁止补卡等)</summary>
+        public int buka_restriction { get; set; }
+
+        /// <summary>是否开启特殊日期打卡(如节假日配置)</summary>
+        public bool open_sp_checkin { get; set; }
+
+        /// <summary>打卡方式类型扩展字段</summary>
+        public int checkin_method_type { get; set; }
     }
 
     /// <summary>
-    /// 特殊日期-不用打卡日期时间戳
+    /// 打卡日期配置(某一天或一组工作日的规则)
     /// </summary>
-    public class spe_offdaysItem
+    public class CheckInDate
     {
-        /// <summary>
-        /// 特殊日期备注
-        /// </summary>
-        public string notes { get; set; }
+        /// <summary>工作日:0-星期日,1-6-星期一到星期六</summary>
+        public List<int> workdays { get; set; } = new();
+
+        /// <summary>上下班时段列表(支持一天多个时段)</summary>
+        public List<CheckInTime> checkintime { get; set; } = new();
+
+        /// <summary>弹性时间设置(单位:秒)</summary>
+        public int flex_time { get; set; }
+
+        /// <summary>下班是否需要打卡(整体规则)</summary>
+        public bool noneed_offwork { get; set; }
+
+        /// <summary>打卡时间限制(毫秒),如限制必须提前多久打卡</summary>
+        public int limit_aheadtime { get; set; }
+
+        /// <summary>允许迟到时间(秒)</summary>
+        public int flex_on_duty_time { get; set; }
+
+        /// <summary>允许早退时间(秒)</summary>
+        public int flex_off_duty_time { get; set; }
+
+        /// <summary>是否允许弹性时间(如弹性上下班)</summary>
+        public bool allow_flex { get; set; }
+
+        /// <summary>迟到规则(如晚走次日可晚到)</summary>
+        public LateRule late_rule { get; set; } = new();
+
+        /// <summary>最多允许早到时间(秒)</summary>
+        public int max_allow_arrive_early { get; set; }
+
+        /// <summary>最多允许迟到时间(秒)</summary>
+        public int max_allow_arrive_late { get; set; }
+
+        /// <summary>大小周配置(可选)</summary>
+        public BiweeklyInfo biweekly { get; set; } = new();
+    }
+
+    /// <summary>
+    /// 上下班打卡时段(具体的上班时间、下班时间)
+    /// </summary>
+    public class CheckInTime
+    {
+        /// <summary>时段ID(适用于一天多个时段)</summary>
+        public int time_id { get; set; }
+
+        /// <summary>上班时间(距离当天0点的秒数)</summary>
+        public int work_sec { get; set; }
+
+        /// <summary>下班时间(距离当天0点的秒数)</summary>
+        public int off_work_sec { get; set; }
+
+        /// <summary>上班提醒时间(秒)</summary>
+        public int remind_work_sec { get; set; }
+
+        /// <summary>下班提醒时间(秒)</summary>
+        public int remind_off_work_sec { get; set; }
+
+        /// <summary>最早休息开始时间(秒)</summary>
+        public int rest_begin_time { get; set; }
+
+        /// <summary>最早休息结束时间(秒)</summary>
+        public int rest_end_time { get; set; }
+
+        /// <summary>是否允许休息</summary>
+        public bool allow_rest { get; set; }
+
+        /// <summary>最早可打卡上班时间(秒)</summary>
+        public int earliest_work_sec { get; set; }
+
+        /// <summary>最晚可打卡上班时间(秒)</summary>
+        public int latest_work_sec { get; set; }
+
+        /// <summary>最早可打卡下班时间(秒)</summary>
+        public int earliest_off_work_sec { get; set; }
+
+        /// <summary>最晚可打卡下班时间(秒)</summary>
+        public int latest_off_work_sec { get; set; }
+
+        /// <summary>上班是否需要打卡</summary>
+        public bool no_need_checkon { get; set; }
+
+        /// <summary>下班是否需要打卡(时段级)</summary>
+        public bool no_need_checkoff { get; set; }
+
+        /// <summary>休息时段列表(一天内可能有多个休息)</summary>
+        public List<RestTime> rest_times { get; set; } = new();
+
+        /// <summary>上班时间(TimeSpan格式,仅用于辅助)</summary>
+        public TimeSpan WorkTime => TimeSpan.FromSeconds(work_sec);
+
+        /// <summary>下班时间(TimeSpan格式,仅用于辅助)</summary>
+        public TimeSpan OffWorkTime => TimeSpan.FromSeconds(off_work_sec);
+    }
+
+    /// <summary>
+    /// 特殊工作日(必须打卡)
+    /// </summary>
+    public class SpecialWorkDay
+    {
+        /// <summary>日期时间戳</summary>
+        public int timestamp { get; set; }
+
+        /// <summary>备注</summary>
+        public string notes { get; set; } = string.Empty;
+
+        /// <summary>打卡时间配置(可覆盖默认时段)</summary>
+        public List<CheckInTime> checkintime { get; set; } = new();
+
+        /// <summary>日期(本地时间)</summary>
+        public DateTime Date => DateTimeOffset.FromUnixTimeSeconds(timestamp).LocalDateTime;
+    }
+
+    /// <summary>
+    /// 特殊休息日(不用打卡)
+    /// </summary>
+    public class SpecialOffDay
+    {
+        /// <summary>日期时间戳</summary>
+        public int timestamp { get; set; }
+
+        /// <summary>备注</summary>
+        public string notes { get; set; } = string.Empty;
+
+        /// <summary>日期(本地时间)</summary>
+        public DateTime Date => DateTimeOffset.FromUnixTimeSeconds(timestamp).LocalDateTime;
+    }
+
+    /// <summary>
+    /// 打卡人员范围
+    /// </summary>
+    public class CheckInRange
+    {
+        /// <summary>成员ID列表</summary>
+        public List<string> userid { get; set; } = new();
+
+        /// <summary>部门ID列表</summary>
+        public List<string> party_id { get; set; } = new();
+
+        /// <summary>标签ID列表</summary>
+        public List<int> tagid { get; set; } = new();
+    }
+
+    /// <summary>
+    /// WiFi打卡信息
+    /// </summary>
+    public class WifiInfo
+    {
+        /// <summary>WiFi名称</summary>
+        public string wifiname { get; set; } = string.Empty;
+
+        /// <summary>WiFi MAC地址 或 BSSID</summary>
+        public string wifimac { get; set; } = string.Empty;
+
+        /// <summary>BSSID(部分场景使用)</summary>
+        public string bssid { get; set; } = string.Empty;
+    }
+
+    /// <summary>
+    /// 位置打卡信息
+    /// </summary>
+    public class LocationInfo
+    {
+        /// <summary>纬度(实际纬度 × 1,000,000)</summary>
+        public long lat { get; set; }
+
+        /// <summary>经度(实际经度 × 1,000,000)</summary>
+        public long lng { get; set; }
+
+        /// <summary>位置名称</summary>
+        public string loc_title { get; set; } = string.Empty;
+
+        /// <summary>位置详细地址</summary>
+        public string loc_detail { get; set; } = string.Empty;
+
+        /// <summary>允许打卡范围(米)</summary>
+        public int distance { get; set; }
+    }
+
+    /// <summary>
+    /// 排班信息(按班次上下班时使用)
+    /// </summary>
+    public class ScheduleInfo
+    {
+        /// <summary>班次ID</summary>
+        public int schedule_id { get; set; }
+
+        /// <summary>班次名称</summary>
+        public string schedule_name { get; set; } = string.Empty;
+
+        /// <summary>班次上下班时段信息</summary>
+        public List<TimeSection> time_section { get; set; } = new();
+
+        /// <summary>允许提前打卡时间(秒)</summary>
+        public int limit_aheadtime { get; set; }
+
+        /// <summary>下班后超过此秒数不允许打下班卡</summary>
+        public int limit_offtime { get; set; }
+
+        /// <summary>下班不需要打卡</summary>
+        public bool noneed_offwork { get; set; }
+
+        /// <summary>是否允许弹性时间</summary>
+        public bool allow_flex { get; set; }
+
+        /// <summary>允许迟到时间(秒)</summary>
+        public int flex_on_duty_time { get; set; }
+
+        /// <summary>允许早退时间(秒)</summary>
+        public int flex_off_duty_time { get; set; }
+
+        /// <summary>迟到规则</summary>
+        public LateRule late_rule { get; set; } = new();
+    }
+
+    /// <summary>
+    /// 时段信息(班次中的某个时段)
+    /// </summary>
+    public class TimeSection
+    {
+        /// <summary>时段ID</summary>
+        public int time_id { get; set; }
 
-        public long timestamp { get; set; }
+        /// <summary>上班时间(秒)</summary>
+        public int work_sec { get; set; }
 
-        public List<long> checkintime { get; set; }
+        /// <summary>下班时间(秒)</summary>
+        public int off_work_sec { get; set; }
 
+        /// <summary>上班提醒时间(秒)</summary>
+        public int remind_work_sec { get; set; }
+
+        /// <summary>下班提醒时间(秒)</summary>
+        public int remind_off_work_sec { get; set; }
+
+        /// <summary>休息时间列表</summary>
+        public List<RestTime> rest_times { get; set; } = new();
+
+        /// <summary>最早休息开始时间(秒)</summary>
+        public int rest_begin_time { get; set; }
+
+        /// <summary>最早休息结束时间(秒)</summary>
+        public int rest_end_time { get; set; }
+
+        /// <summary>是否允许休息</summary>
+        public bool allow_rest { get; set; }
+    }
+
+    /// <summary>
+    /// 休息时段
+    /// </summary>
+    public class RestTime
+    {
+        /// <summary>休息开始时间(秒)</summary>
+        public int rest_begin_time { get; set; }
+
+        /// <summary>休息结束时间(秒)</summary>
+        public int rest_end_time { get; set; }
+    }
+
+    /// <summary>
+    /// 迟到规则(晚走次日可晚到)
+    /// </summary>
+    public class LateRule
+    {
+        /// <summary>是否允许超时下班后次日弹性上班</summary>
+        public bool allow_offwork_after_time { get; set; }
+
+        /// <summary>具体时间规则列表</summary>
+        public List<TimeRule> timerules { get; set; } = new();
+    }
+
+    /// <summary>
+    /// 时间规则(加班与次日弹性关系)
+    /// </summary>
+    public class TimeRule
+    {
+        /// <summary>晚走的时间(秒),即加班时长</summary>
+        public int offwork_after_time { get; set; }
+
+        /// <summary>第二天允许迟到的弹性时间(秒)</summary>
+        public int onwork_flex_time { get; set; }
+    }
+
+    /// <summary>
+    /// 汇报对象信息
+    /// </summary>
+    public class ReporterInfo
+    {
+        /// <summary>汇报对象列表</summary>
+        public List<Reporter> reporters { get; set; } = new();
+
+        /// <summary>汇报对象更新时间戳</summary>
+        public int updatetime { get; set; }
+    }
+
+    /// <summary>
+    /// 汇报人
+    /// </summary>
+    public class Reporter
+    {
+        /// <summary>汇报人userid</summary>
+        public string userid { get; set; } = string.Empty;
+    }
+
+    /// <summary>
+    /// 加班时长计算规则(基于打卡时间)
+    /// </summary>
+    public class OtCheckInfo
+    {
+        /// <summary>工作日加班开始时间(下班后经过此秒数才算加班)</summary>
+        public int ot_workingday_time_start { get; set; }
+
+        /// <summary>工作日最短加班时长(秒)</summary>
+        public int ot_workingday_time_min { get; set; }
+
+        /// <summary>工作日最长加班时长(秒)</summary>
+        public int ot_workingday_time_max { get; set; }
+
+        /// <summary>非工作日最短加班时长(秒)</summary>
+        public int ot_nonworkingday_time_min { get; set; }
+
+        /// <summary>非工作日最长加班时长(秒)</summary>
+        public int ot_nonworkingday_time_max { get; set; }
+
+        /// <summary>非工作日加班跨天时间(秒)</summary>
+        public int ot_nonworkingday_spanday_time { get; set; }
+
+        /// <summary>工作日加班休息扣除配置</summary>
+        public RestConfig ot_workingday_restinfo { get; set; } = new();
+
+        /// <summary>非工作日加班休息扣除配置</summary>
+        public RestConfig ot_nonworkingday_restinfo { get; set; } = new();
+    }
+
+    /// <summary>
+    /// 休息时间扣除配置(加班时长中扣除休息时间)
+    /// </summary>
+    public class RestConfig
+    {
+        /// <summary>扣除类型:0-不扣除;1-按固定时间扣除;2-按加班时长阶梯扣除</summary>
         public int type { get; set; }
 
-        /// <summary>
-        /// 开始时间戳
-        /// </summary>
-        public long begtime { get; set; }
+        /// <summary>固定时间扣除规则(type=1时有效)</summary>
+        public FixTimeRule fix_time_rule { get; set; } = new();
 
-        /// <summary>
-        /// 开始时间
-        /// </summary>
-        public DateTime begtime_dt
-        {
-            get
-            {
-                return new DateTime(begtime * 10000000 + 621355968000000000L).ToLocalTime();
-            }
-        }
+        /// <summary>阶梯扣除规则(type=2时有效)</summary>
+        public CalOtTimeRule cal_ottime_rule { get; set; } = new();
+    }
+
+    /// <summary>
+    /// 固定时间扣除规则
+    /// </summary>
+    public class FixTimeRule
+    {
+        /// <summary>固定休息开始时间(秒)</summary>
+        public int fix_time_begin_sec { get; set; }
 
-        public long endtime { get; set; }
+        /// <summary>固定休息结束时间(秒)</summary>
+        public int fix_time_end_sec { get; set; }
+    }
 
-        /// <summary>
-        /// 开始时间
-        /// </summary>
-        public DateTime endtime_dt
-        {
-            get
-            {
-                return new DateTime(endtime * 10000000 + 621355968000000000L).ToLocalTime();
-            }
-        }
+    /// <summary>
+    /// 阶梯扣除规则(按加班时长扣除)
+    /// </summary>
+    public class CalOtTimeRule
+    {
+        /// <summary>扣除条件列表</summary>
+        public List<OtTimeItem> items { get; set; } = new();
+    }
+
+    /// <summary>
+    /// 加班时长扣除条件项
+    /// </summary>
+    public class OtTimeItem
+    {
+        /// <summary>加班满此秒数后触发扣除</summary>
+        public int ot_time { get; set; }
+
+        /// <summary>对应扣除的休息秒数</summary>
+        public int rest_time { get; set; }
+    }
+
+    /// <summary>
+    /// 加班规则V2(新版,结构更细化)
+    /// </summary>
+    public class OtInfoV2
+    {
+        /// <summary>工作日加班配置</summary>
+        public WorkdayConf workdayconf { get; set; } = new();
+
+        /// <summary>休息日加班配置</summary>
+        public RestdayConf restdayconf { get; set; } = new();
+
+        /// <summary>法定节假日加班配置</summary>
+        public HolidayConf holidayconf { get; set; } = new();
+
+        /// <summary>配置更新时间戳</summary>
+        public int updatetime { get; set; }
+
+        /// <summary>时间单位配置(如加班时长舍入规则)</summary>
+        public TimeUnitConfig time_unit_config { get; set; } = new();
+    }
+
+    /// <summary>
+    /// 工作日加班配置
+    /// </summary>
+    public class WorkdayConf
+    {
+        /// <summary>是否允许加班</summary>
+        public bool allow_ot { get; set; }
+
+        /// <summary>加班计算类型:2-以打卡时间为准(常见)</summary>
+        public int type { get; set; }
+
+        /// <summary>申请/打卡相关配置</summary>
+        public ApplyInfo apply { get; set; } = new();
+
+        /// <summary>是否启用加班转调休/加班费</summary>
+        public bool ot_trans_enable { get; set; }
+
+        /// <summary>转换类型:1-转调休;2-转加班费</summary>
+        public int ot_trans_type { get; set; }
+
+        /// <summary>假期转换配置(如转调休比例)</summary>
+        public Vacation vacation { get; set; } = new();
+
+        /// <summary>加班时长范围限制(秒)</summary>
+        public int ot_time_range { get; set; }
     }
 
+    /// <summary>
+    /// 休息日加班配置(继承WorkdayConf)
+    /// </summary>
+    public class RestdayConf : WorkdayConf { }
+
+    /// <summary>
+    /// 法定节假日加班配置(继承WorkdayConf)
+    /// </summary>
+    public class HolidayConf : WorkdayConf { }
+
+    /// <summary>
+    /// 申请或打卡相关配置
+    /// </summary>
+    //public class ApplyInfo
+    //{
+    //    /// <summary>休息扣除配置</summary>
+    //    public RestInfo restinfo { get; set; } = new();
+
+    //    /// <summary>加班开始时间(秒,用于指定从几点开始算加班)</summary>
+    //    public int ot_time_start { get; set; }
+    //}
+
+    /// <summary>
+    /// 休息扣除信息(V2版本使用)
+    /// </summary>
+    public class RestInfo
+    {
+        /// <summary>休息扣除类型:0-不扣除;1-指定固定时段扣除</summary>
+        public int type { get; set; }
+
+        /// <summary>固定扣除时段列表</summary>
+        public List<FixTimeRule> fix_time_rule_list { get; set; } = new();
+    }
+
+    ///// <summary>
+    ///// 假期转换配置
+    ///// </summary>
+    //public class Vacation
+    //{
+    //    /// <summary>转换比例(如100表示1小时加班转1小时调休)</summary>
+    //    public int trans_ratio { get; set; }
+
+    //    /// <summary>是否同步到假期余额</summary>
+    //    public bool sync_vacation { get; set; }
+    //}
+
+    /// <summary>
+    /// 时间单位配置(加班时长舍入规则)
+    /// </summary>
+    public class TimeUnitConfig
+    {
+        /// <summary>加班时间单位:1-分钟;2-小时</summary>
+        public int ot_time_unit { get; set; }
+
+        /// <summary>每日最大加班时长(秒)</summary>
+        public int perday_duration_secs { get; set; }
+
+        /// <summary>舍入方式:1-向上舍入;2-向下舍入;3-四舍五入</summary>
+        public int rounding_method { get; set; }
+
+        /// <summary>舍入精度:如以30分钟为单位</summary>
+        public int rounding_precision { get; set; }
+
+        /// <summary>步长(秒),用于限制加班时长必须为此值的倍数</summary>
+        public int step_size { get; set; }
+    }
+
+    /// <summary>
+    /// 补卡提醒配置
+    /// </summary>
+    public class BukaRemind
+    {
+        /// <summary>是否开启补卡提醒</summary>
+        public bool open_remind { get; set; }
+
+        /// <summary>每月第几天提醒补卡(如1表示每月1日提醒)</summary>
+        public int buka_remind_day { get; set; }
+
+        /// <summary>每月提醒补卡的月份偏移(0-当月,1-次月)</summary>
+        public int buka_remind_month { get; set; }
+    }
+
+    /// <summary>
+    /// 大小周配置
+    /// </summary>
+    public class BiweeklyInfo
+    {
+        /// <summary>是否开启大小周</summary>
+        public bool enable_weekday_recurrence { get; set; }
+
+        /// <summary>奇数周工作日(0-6,0周日)</summary>
+        public List<int> odd_workdays { get; set; } = new();
+
+        /// <summary>偶数周工作日(0-6,0周日)</summary>
+        public List<int> even_workdays { get; set; } = new();
+    }
 
     #endregion
 

+ 4 - 0
OASystem/OASystem.Domain/ViewModels/System/UserInfoView.cs

@@ -201,4 +201,8 @@ namespace OASystem.Domain.ViewModels.System
         /// </summary>
         public string CnName { get; set; }
     }
+    public class UserWeChatIdView : UserNameView
+    {
+        public string WeChatId { get; set; }
+    }
 }