using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using Mapster; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using NCC.Common.Core.Manager; using NCC.Common.Enum; using NCC.Common.Extension; using NCC.Common.Filter; using NCC.Common.Helper; using NCC.Dependency; using NCC.DynamicApiController; using NCC.Extend.Entitys.Dto.LqAttendanceSummary; using NCC.Extend.Entitys.Enum; using NCC.Extend.Entitys.lq_attendance_summary; using NCC.Extend.Interfaces.LqAttendanceSummary; using NCC.FriendlyException; using NCC.System.Entitys.Permission; using SqlSugar; using Yitter.IdGenerator; namespace NCC.Extend { /// /// 考勤汇总服务 /// [ApiDescriptionSettings(Tag = "绿纤考勤汇总服务", Name = "LqAttendanceSummary", Order = 200)] [Route("api/Extend/[controller]")] public class LqAttendanceSummaryService : ILqAttendanceSummaryService, IDynamicApiController, ITransient { private readonly ISqlSugarClient _db; private readonly IUserManager _userManager; private readonly ILogger _logger; /// /// 构造函数 /// /// 数据库客户端 /// 用户管理器 /// 日志记录器 public LqAttendanceSummaryService(ISqlSugarClient db, IUserManager userManager, ILogger logger) { _db = db; _userManager = userManager; _logger = logger; } #region 上传Excel文件导入考勤汇总数据 /// /// 上传Excel文件导入考勤汇总数据 /// /// /// 上传Excel文件批量导入考勤汇总数据 /// /// Excel文件格式要求: /// - 第一行必须是标题行 /// - 列顺序:员工姓名、员工电话、年份、月份、出勤天数、请假天数、休息天数、备注 /// - 支持.xlsx和.xls格式 /// /// 示例请求: /// POST /api/Extend/LqAttendanceSummary/ImportAttendanceDataFromExcel /// Content-Type: multipart/form-data /// /// 参数说明: /// - file: Excel文件(必填) /// /// Excel文件 /// 导入结果 /// 导入成功 /// 文件格式错误 /// 服务器错误 [HttpPost("ImportAttendanceDataFromExcel")] public async Task ImportAttendanceDataFromExcel(IFormFile file) { try { if (file == null || file.Length == 0) { throw NCCException.Oh("请选择要上传的Excel文件"); } // 检查文件格式 var allowedExtensions = new[] { ".xlsx", ".xls" }; var fileExtension = Path.GetExtension(file.FileName).ToLowerInvariant(); if (!allowedExtensions.Contains(fileExtension)) { throw NCCException.Oh("只支持.xlsx和.xls格式的Excel文件"); } var importData = new List(); // 保存临时文件 var tempFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + Path.GetExtension(file.FileName)); try { using (var stream = new FileStream(tempFilePath, FileMode.Create)) { await file.CopyToAsync(stream); } // 使用ExcelImportHelper读取Excel文件 var dataTable = ExcelImportHelper.ToDataTable(tempFilePath, 0, 0); if (dataTable.Rows.Count == 0) { throw NCCException.Oh("Excel文件中没有数据行"); } // 根据列名查找列索引(支持多种可能的列名) int GetColumnIndex(string[] possibleNames) { foreach (var name in possibleNames) { for (int col = 0; col < dataTable.Columns.Count; col++) { var columnName = dataTable.Columns[col].ColumnName?.Trim(); if (columnName == name || columnName?.Contains(name) == true) { return col; } } } return -1; } var nameColIndex = GetColumnIndex(new[] { "员工姓名", "姓名", "员工", "姓名" }); var phoneColIndex = GetColumnIndex(new[] { "员工电话", "电话", "手机", "手机号", "联系电话" }); var yearColIndex = GetColumnIndex(new[] { "年份", "年" }); var monthColIndex = GetColumnIndex(new[] { "月份", "月" }); var workDaysColIndex = GetColumnIndex(new[] { "出勤天数", "出勤", "工作天数", "上班天数" }); var leaveDaysColIndex = GetColumnIndex(new[] { "请假天数", "请假", "事假天数" }); var restDaysColIndex = GetColumnIndex(new[] { "休息天数", "休息", "休假天数" }); var remarkColIndex = GetColumnIndex(new[] { "备注", "说明", "备注信息" }); // 验证必需的列是否存在 if (nameColIndex == -1) throw NCCException.Oh("Excel文件中未找到'员工姓名'列"); if (phoneColIndex == -1) throw NCCException.Oh("Excel文件中未找到'员工电话'列"); if (yearColIndex == -1) throw NCCException.Oh("Excel文件中未找到'年份'列"); if (monthColIndex == -1) throw NCCException.Oh("Excel文件中未找到'月份'列"); // 从第1行开始读取数据(跳过标题行) for (int i = 1; i < dataTable.Rows.Count; i++) { try { var row = dataTable.Rows[i]; var employeeName = row[nameColIndex]?.ToString()?.Trim(); var employeePhone = row[phoneColIndex]?.ToString()?.Trim(); // 处理年份:可能是数字、日期或字符串 string yearText = null; if (yearColIndex >= 0 && row[yearColIndex] != null && row[yearColIndex] != DBNull.Value) { if (row[yearColIndex] is DateTime dt) { yearText = dt.Year.ToString(); } else if (row[yearColIndex] is double d) { yearText = ((int)d).ToString(); } else { yearText = row[yearColIndex].ToString()?.Trim(); } } // 处理月份:可能是数字、日期或字符串 string monthText = null; if (monthColIndex >= 0 && row[monthColIndex] != null && row[monthColIndex] != DBNull.Value) { if (row[monthColIndex] is DateTime dt) { monthText = dt.Month.ToString(); } else if (row[monthColIndex] is double d) { monthText = ((int)d).ToString(); } else { monthText = row[monthColIndex].ToString()?.Trim(); } } var workDaysText = workDaysColIndex >= 0 ? row[workDaysColIndex]?.ToString()?.Trim() : null; var leaveDaysText = leaveDaysColIndex >= 0 ? row[leaveDaysColIndex]?.ToString()?.Trim() : null; var restDaysText = restDaysColIndex >= 0 ? row[restDaysColIndex]?.ToString()?.Trim() : null; var remark = remarkColIndex >= 0 ? row[remarkColIndex]?.ToString()?.Trim() : null; // 跳过空行 if (string.IsNullOrEmpty(employeeName) && string.IsNullOrEmpty(employeePhone)) { continue; } // 验证必填字段 if (string.IsNullOrEmpty(employeeName)) { throw new Exception($"第{i + 1}行:员工姓名不能为空"); } if (string.IsNullOrEmpty(employeePhone)) { throw new Exception($"第{i + 1}行:员工电话不能为空"); } // 解析数值字段 int year = 0; int month = 0; // 解析年份:支持纯数字、日期格式、中文格式 if (string.IsNullOrEmpty(yearText)) { throw new Exception($"第{i + 1}行:年份不能为空"); } // 尝试直接解析为整数 if (int.TryParse(yearText, out year)) { // 成功解析 } // 尝试解析日期格式(如:2025-11-01 或 2025/11/01) else if (DateTime.TryParse(yearText, out DateTime yearDate)) { year = yearDate.Year; } // 尝试解析中文格式(如:2025年) else if (yearText.Contains("年")) { var yearMatch = Regex.Match(yearText, @"(\d{4})年"); if (yearMatch.Success && int.TryParse(yearMatch.Groups[1].Value, out year)) { // 成功解析 } else { throw new Exception($"第{i + 1}行:年份格式错误,无法解析:{yearText}"); } } else { throw new Exception($"第{i + 1}行:年份格式错误,无法解析。实际值:\"{yearText}\"(类型:{yearText?.GetType().Name})"); } // 验证年份范围 if (year < 2020 || year > 2030) { throw new Exception($"第{i + 1}行:年份必须在2020-2030之间"); } // 解析月份:支持纯数字、日期格式、中文格式 if (string.IsNullOrEmpty(monthText)) { throw new Exception($"第{i + 1}行:月份不能为空"); } // 尝试直接解析为整数 if (int.TryParse(monthText, out month)) { // 成功解析 } // 尝试解析日期格式(如:2025-11-01 或 2025/11/01) else if (DateTime.TryParse(monthText, out DateTime monthDate)) { month = monthDate.Month; } // 尝试解析中文格式(如:11月) else if (monthText.Contains("月")) { var monthMatch = Regex.Match(monthText, @"(\d{1,2})月"); if (monthMatch.Success && int.TryParse(monthMatch.Groups[1].Value, out month)) { // 成功解析 } else { throw new Exception($"第{i + 1}行:月份格式错误,无法解析:{monthText}"); } } // 尝试解析"年-月"格式(如:2025-11) else if (monthText.Contains("-") || monthText.Contains("/")) { var parts = monthText.Split(new[] { "-", "/" }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length >= 2 && int.TryParse(parts[1], out month)) { // 成功解析 } else { throw new Exception($"第{i + 1}行:月份格式错误,无法解析:{monthText}"); } } else { throw new Exception($"第{i + 1}行:月份格式错误,无法解析。实际值:\"{monthText}\"(类型:{monthText?.GetType().Name})"); } // 验证月份范围 if (month < 1 || month > 12) { throw new Exception($"第{i + 1}行:月份必须在1-12之间"); } decimal.TryParse(workDaysText, out decimal workDays); decimal.TryParse(leaveDaysText, out decimal leaveDays); decimal.TryParse(restDaysText, out decimal restDays); var item = new LqAttendanceSummaryImportInput { EmployeeName = employeeName, EmployeePhone = employeePhone, Year = year, Month = month, WorkDays = workDays, LeaveDays = leaveDays, RestDays = restDays, Remark = remark }; importData.Add(item); } catch (Exception ex) { throw new Exception($"第{i + 1}行数据解析失败: {ex.Message}"); } } } finally { // 清理临时文件 if (File.Exists(tempFilePath)) { File.Delete(tempFilePath); } } if (!importData.Any()) { throw NCCException.Oh("Excel文件中没有有效的数据行"); } // 处理导入数据 return await ProcessImportData(importData); } catch (Exception ex) { _logger.LogError(ex, "上传Excel文件导入考勤汇总数据失败"); throw NCCException.Oh($"上传Excel文件导入考勤汇总数据失败: {ex.Message}"); } } #endregion #region 处理导入数据 /// /// 处理导入数据 /// /// 导入数据列表 /// 导入结果 private async Task ProcessImportData(List importData) { var successCount = 0; var failCount = 0; var errorMessages = new List(); var entitiesToInsert = new List(); var entitiesToUpdate = new List(); foreach (var item in importData) { try { // 1. 根据员工姓名和电话查找用户ID var user = await _db.Queryable() .Where(u => u.RealName == item.EmployeeName && u.MobilePhone == item.EmployeePhone) .FirstAsync(); if (user == null) { errorMessages.Add($"员工 {item.EmployeeName}({item.EmployeePhone}) 不存在"); failCount++; continue; } // 2. 检查是否已存在相同记录 var existingRecord = await _db.Queryable() .Where(a => a.UserId == user.Id && a.Year == item.Year && a.Month == item.Month) .FirstAsync(); if (existingRecord != null) { // 更新现有记录 existingRecord.EmployeeStatus = 1; // 默认在职 existingRecord.WorkDays = item.WorkDays; existingRecord.LeaveDays = item.LeaveDays; existingRecord.RestDays = item.RestDays; existingRecord.Remark = item.Remark; existingRecord.UpdateUser = _userManager.UserId; existingRecord.UpdateTime = DateTime.Now; entitiesToUpdate.Add(existingRecord); } else { // 创建新记录 var entity = new LqAttendanceSummaryEntity { Id = YitIdHelper.NextId().ToString(), UserId = user.Id, Year = item.Year, Month = item.Month, EmployeeStatus = 1, // 默认在职 WorkDays = item.WorkDays, LeaveDays = item.LeaveDays, RestDays = item.RestDays, Remark = item.Remark, CreateUser = _userManager.UserId, CreateTime = DateTime.Now, UpdateUser = _userManager.UserId, UpdateTime = DateTime.Now, IsEffective = 1 }; entitiesToInsert.Add(entity); } successCount++; } catch (Exception ex) { errorMessages.Add($"处理员工 {item.EmployeeName} 数据失败: {ex.Message}"); failCount++; _logger.LogError(ex, $"导入考勤数据失败 - 员工: {item.EmployeeName}"); } } // 批量插入和更新 if (entitiesToInsert.Any()) { await _db.Insertable(entitiesToInsert).ExecuteCommandAsync(); } if (entitiesToUpdate.Any()) { await _db.Updateable(entitiesToUpdate).ExecuteCommandAsync(); } return new { success = true, message = "导入完成", data = new { totalCount = importData.Count, successCount = successCount, failCount = failCount, insertCount = entitiesToInsert.Count, updateCount = entitiesToUpdate.Count, errorMessages = errorMessages } }; } #endregion #region 获取考勤汇总列表 /// /// 获取考勤汇总列表 /// /// 请求参数 /// [HttpGet("")] public async Task GetList([FromQuery] LqAttendanceSummaryListQueryInput input) { var sidx = input.sidx == null ? "id" : input.sidx; var sord = input.sort == null ? "desc" : input.sort; var data = await _db.Queryable() .Where(a => a.IsEffective == StatusEnum.有效.GetHashCode()) .WhereIF(!string.IsNullOrEmpty(input.UserId), a => a.UserId == input.UserId) .WhereIF(input.Year.HasValue, a => a.Year == input.Year) .WhereIF(input.Month.HasValue, a => a.Month == input.Month) .WhereIF(input.EmployeeStatus.HasValue, a => a.EmployeeStatus == input.EmployeeStatus) .Select(a => new LqAttendanceSummaryListOutput { id = a.Id, userId = a.UserId, userName = SqlFunc.Subqueryable().Where(u => u.Id == a.UserId).Select(u => u.RealName), year = a.Year, month = a.Month, employeeStatus = a.EmployeeStatus, workDays = a.WorkDays, leaveDays = a.LeaveDays, restDays = a.RestDays, remark = a.Remark, createUser = a.CreateUser, createUserName = SqlFunc.Subqueryable().Where(u => u.Id == a.CreateUser).Select(u => u.RealName), createTime = a.CreateTime, updateUser = a.UpdateUser, updateUserName = SqlFunc.Subqueryable().Where(u => u.Id == a.UpdateUser).Select(u => u.RealName), updateTime = a.UpdateTime, isEffective = a.IsEffective, }) .MergeTable() .OrderBy(sidx + " " + input.sort) .ToPagedListAsync(input.currentPage, input.pageSize); return PageResult.SqlSugarPageResult(data); } #endregion #region 清空某一个月的数据 /// /// 清空某一个月的数据 /// /// 年份 /// 月份 /// [HttpDelete("DeleteByMonth/{year}/{month}")] public async Task DeleteByMonth(string year, string month) { int yearInt = int.Parse(year); int monthInt = int.Parse(month); await _db.Deleteable().Where(p => p.Year == yearInt && p.Month == monthInt).ExecuteCommandAsync(); return new { success = true, message = "清空成功" }; } #endregion #region 生成健康师考勤模拟数据 /// /// 生成健康师考勤模拟数据 /// /// /// 为所有健康师生成指定年月的考勤模拟数据 /// /// 规则: /// - 80%以上的健康师出勤天数 >= 21天 /// - 其他健康师出勤天数随机生成(0-20天) /// - 员工状态默认为1(在职) /// /// 示例请求: /// ```json /// { /// "year": 2025, /// "month": 11 /// } /// ``` /// /// 年份 /// 月份 /// 生成结果 /// 成功生成考勤模拟数据 /// 参数错误 /// 生成失败 [HttpPost("GenerateMockData/{year}/{month}")] public async Task GenerateMockData(int year, int month) { try { // 验证参数 if (year < 2000 || year > 3000) { throw NCCException.Oh("年份参数无效"); } if (month < 1 || month > 12) { throw NCCException.Oh("月份参数无效,应在1-12之间"); } // 查询所有健康师 var healthCoaches = await _db.Queryable() .Where(u => u.Gw == "健康师" && u.EnabledMark == 1 && (u.DeleteMark == null || u.DeleteMark == 0)) .Select(u => new { u.Id, u.RealName }) .ToListAsync(); if (!healthCoaches.Any()) { throw NCCException.Oh("未找到健康师用户"); } // 检查是否已存在该年月的考勤数据 var existingRecords = await _db.Queryable() .Where(a => a.Year == year && a.Month == month) .CountAsync(); if (existingRecords > 0) { throw NCCException.Oh($"已存在{year}年{month}月的考勤数据,请先清空后再生成"); } var totalCount = healthCoaches.Count; var highAttendanceCount = (int)Math.Ceiling(totalCount * 0.8); // 80%以上,向上取整 var lowAttendanceCount = totalCount - highAttendanceCount; var random = new Random(); var entitiesToInsert = new List(); var now = DateTime.Now; // 生成考勤数据 for (int i = 0; i < totalCount; i++) { var healthCoach = healthCoaches[i]; decimal workDays; decimal leaveDays; decimal restDays; if (i < highAttendanceCount) { // 80%以上的健康师:出勤天数 >= 21天 workDays = random.Next(21, 31); // 21-30天 var remainingDays = 30 - workDays; // 剩余天数分配给请假和休息 if (remainingDays > 0) { leaveDays = random.Next(0, (int)remainingDays + 1); restDays = remainingDays - leaveDays; } else { leaveDays = 0; restDays = 0; } } else { // 其他20%的健康师:出勤天数 < 21天 workDays = random.Next(0, 21); // 0-20天 var remainingDays = 30 - workDays; // 剩余天数分配给请假和休息 if (remainingDays > 0) { leaveDays = random.Next(0, (int)remainingDays + 1); restDays = remainingDays - leaveDays; } else { leaveDays = 0; restDays = 0; } } var entity = new LqAttendanceSummaryEntity { Id = YitIdHelper.NextId().ToString(), UserId = healthCoach.Id, Year = year, Month = month, EmployeeStatus = 1, // 在职 WorkDays = workDays, LeaveDays = leaveDays, RestDays = restDays, Remark = "模拟数据", CreateUser = _userManager.UserId ?? "system", CreateTime = now, UpdateUser = _userManager.UserId ?? "system", UpdateTime = now, IsEffective = 1 }; entitiesToInsert.Add(entity); } // 批量插入 await _db.Insertable(entitiesToInsert).ExecuteCommandAsync(); // 统计结果 var highAttendanceActual = entitiesToInsert.Count(e => e.WorkDays >= 21); var highAttendancePercentage = (decimal)highAttendanceActual / totalCount * 100; return new { success = true, message = "生成考勤模拟数据成功", data = new { year = year, month = month, totalCount = totalCount, highAttendanceCount = highAttendanceActual, highAttendancePercentage = Math.Round(highAttendancePercentage, 2), generatedCount = entitiesToInsert.Count } }; } catch (Exception ex) { _logger.LogError(ex, $"生成健康师考勤模拟数据失败 - 年份: {year}, 月份: {month}"); throw NCCException.Oh($"生成健康师考勤模拟数据失败:{ex.Message}"); } } #endregion } }