using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Mapster;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NCC.Common.Core.Manager;
using NCC.Common.Enum;
using NCC.Common.Filter;
using NCC.Dependency;
using NCC.DynamicApiController;
using NCC.Extend.Entitys.Dto.LqAttendanceSummary;
using NCC.Extend.Entitys.Enum;
using NCC.Extend.Entitys.lq_attendance_group;
using NCC.Extend.Entitys.lq_attendance_record;
using NCC.Extend.Entitys.lq_attendance_rest_group;
using NCC.Extend.Entitys.lq_attendance_summary;
using NCC.Extend.Entitys.lq_mdxx;
using NCC.Extend.Interfaces.LqAttendanceRecord;
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;
private readonly ILqAttendanceRecordService _attendanceRecordService;
///
/// 构造函数
///
/// 数据库客户端
/// 用户管理器
/// 日志记录器
/// 考勤打卡服务(从打卡记录汇总写入汇总表)
public LqAttendanceSummaryService(
ISqlSugarClient db,
IUserManager userManager,
ILogger logger,
ILqAttendanceRecordService attendanceRecordService)
{
_db = db;
_userManager = userManager;
_logger = logger;
_attendanceRecordService = attendanceRecordService;
}
#region 从考勤打卡记录同步汇总
///
/// 从考勤打卡记录按自然月重新汇总到 lq_attendance_summary(覆盖该月已有数据)
///
///
/// 人选与月度考勤矩阵、员工花名册「当月在职」一致(见 Skill historical-on-job-inference):
/// 有当月有效汇总且员工状态为在职则入选;无汇总时按主档入职/离职边界推断。
/// 出勤/请假/休息天数按每日一条打卡记录(同日取最新一条)的状态统计:正常+迟到→出勤,请假+病假→请假,休息→休息。
/// 未来月不允许调用(仅认已有汇总)。
///
/// 年
/// 月
/// 可选,与月度矩阵相同(姓名/账号/门店/分组)
/// 可选考勤分组 ID
/// 同步结果
[HttpPost("SyncFromAttendanceRecords/{year}/{month}")]
public Task SyncFromAttendanceRecords(
int year,
int month,
[FromQuery] string keyword = null,
[FromQuery] string attendanceGroupId = null)
{
return _attendanceRecordService.SyncMonthSummaryFromAttendanceRecordsAsync(year, month, keyword, attendanceGroupId);
}
#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);
var pageRows = data.list == null
? new List()
: data.list.ToList();
await FillSummaryListDerivedFromRecordsAsync(pageRows);
return PageResult.SqlSugarPageResult(data);
}
///
/// 按分页结果从打卡记录补全:当月考勤组文案 + 应休/到岗/请假/迟到早退/旷工及可算扣款。
///
private async Task FillSummaryListDerivedFromRecordsAsync(List items)
{
if (items == null || items.Count == 0)
{
return;
}
var userIds = items.Select(x => x.userId).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToList();
if (userIds.Count == 0)
{
return;
}
var monthBounds = items.Select(x => (x.year, x.month)).Distinct().ToList();
var minStart = monthBounds.Min(m => new DateTime(m.year, m.month, 1));
var maxEnd = monthBounds.Max(m => new DateTime(m.year, m.month, 1).AddMonths(1));
var records = await _db.Queryable()
.Where(x => x.IsEffective == StatusEnum.有效.GetHashCode())
.Where(x => x.AttendanceDate >= minStart && x.AttendanceDate < maxEnd)
.Where(x => userIds.Contains(x.UserId))
.ToListAsync();
var users = await _db.Queryable()
.Where(u => userIds.Contains(u.Id) && u.DeleteMark == null)
.Select(u => new { u.Id, u.Mdid, u.AttendanceRestGroupId })
.ToListAsync();
var mdids = users.Select(u => u.Mdid).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToList();
var stores = mdids.Count == 0
? new List()
: await _db.Queryable()
.Where(s => mdids.Contains(s.Id))
.ToListAsync();
var storeById = stores.Where(x => !string.IsNullOrEmpty(x.Id)).GroupBy(x => x.Id).ToDictionary(x => x.Key, x => x.First());
var groupIds = stores.Select(s => s.AttendanceGroupId).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToList();
var groups = groupIds.Count == 0
? new List()
: await _db.Queryable()
.Where(g => groupIds.Contains(g.Id) && SqlFunc.IsNull(g.DeleteMark, 0) == 0)
.ToListAsync();
var groupById = groups.GroupBy(x => x.Id).ToDictionary(x => x.Key, x => x.First());
var groupNameById = groupById.ToDictionary(x => x.Key, x => x.Value.GroupName ?? string.Empty);
var defaultRestIdsFromShift = groups
.Select(g => g.DefaultRestGroupId)
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct()
.ToList();
var defaultRestGroups = defaultRestIdsFromShift.Count == 0
? new List()
: await _db.Queryable()
.Where(r => defaultRestIdsFromShift.Contains(r.Id) && SqlFunc.IsNull(r.DeleteMark, 0) == 0)
.ToListAsync();
var monthlyRestByDefaultRestId = defaultRestGroups
.Where(x => !string.IsNullOrEmpty(x.Id))
.GroupBy(x => x.Id)
.ToDictionary(x => x.Key, x => x.First().MonthlyRestDays);
var monthlyRestByGroupId = groupById.ToDictionary(
x => x.Key,
x =>
{
var g = x.Value;
if (!string.IsNullOrWhiteSpace(g.DefaultRestGroupId)
&& monthlyRestByDefaultRestId.TryGetValue(g.DefaultRestGroupId, out var mr))
{
return mr;
}
return 0;
});
var restGroupIds = users.Select(u => u.AttendanceRestGroupId).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToList();
var restGroups = restGroupIds.Count == 0
? new List()
: await _db.Queryable()
.Where(r => restGroupIds.Contains(r.Id) && SqlFunc.IsNull(r.DeleteMark, 0) == 0)
.ToListAsync();
var restGroupById = restGroups.GroupBy(x => x.Id).ToDictionary(x => x.Key, x => x.First());
var monthlyRestByRestGroupId = restGroupById.ToDictionary(x => x.Key, x => x.Value.MonthlyRestDays);
var profileNameByUserId = users.ToDictionary(
u => u.Id,
u =>
{
var gid = !string.IsNullOrWhiteSpace(u.Mdid) && storeById.TryGetValue(u.Mdid.Trim(), out var st)
? st.AttendanceGroupId
: null;
return !string.IsNullOrWhiteSpace(gid) && groupNameById.TryGetValue(gid, out var gn)
? gn
: string.Empty;
});
var profileMonthlyRestByUserId = users.ToDictionary(
u => u.Id,
u =>
{
if (!string.IsNullOrWhiteSpace(u.AttendanceRestGroupId)
&& monthlyRestByRestGroupId.TryGetValue(u.AttendanceRestGroupId, out var mrr))
{
return (int)mrr;
}
var gid = !string.IsNullOrWhiteSpace(u.Mdid) && storeById.TryGetValue(u.Mdid.Trim(), out var st)
? st.AttendanceGroupId
: null;
if (string.IsNullOrWhiteSpace(gid)
|| !monthlyRestByGroupId.TryGetValue(gid, out var mr))
{
return 0;
}
return (int)mr;
});
var recordsByUserMonth = records
.GroupBy(r => (r.UserId, r.AttendanceDate.Year, r.AttendanceDate.Month))
.ToDictionary(g => g.Key, g => g.ToList());
foreach (var item in items)
{
if (string.IsNullOrWhiteSpace(item.userId))
{
item.attendanceGroupDisplay = "无";
continue;
}
recordsByUserMonth.TryGetValue((item.userId, item.year, item.month), out var monthRecs);
profileNameByUserId.TryGetValue(item.userId, out var profileName);
item.attendanceGroupDisplay =
AttendanceMonthGroupDisplayHelper.BuildDisplay(monthRecs, profileName ?? string.Empty);
profileMonthlyRestByUserId.TryGetValue(item.userId, out var fallbackRest);
var perDay = AttendanceSummaryMonthMetricsHelper.CollapseToLatestPerDay(monthRecs);
var metrics = AttendanceSummaryMonthMetricsHelper.Compute(perDay, fallbackRest, null);
item.monthEntitledRestDays = metrics.EntitledRestDays;
item.monthOnDutyDays = metrics.OnDutyDays;
item.monthCompositeWorkDays = metrics.CompositeWorkDays;
item.monthPaidLeaveDays = metrics.PaidLeaveDays;
item.monthLeaveDaysTotal = metrics.LeaveDaysTotal;
item.monthLateEarlyDays = metrics.LateEarlyDays;
item.monthAbsenteeismDays = metrics.AbsenteeismDays;
item.monthLateDeductionAmount = metrics.LateDeductionAmount;
item.monthLeaveDeductionAmount = metrics.LeaveDeductionAmount;
item.monthAbsenteeismDeductionAmount = metrics.AbsenteeismDeductionAmount;
}
}
#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
}
}