using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Dtos.Prod;
using F9WebAiTest.Domain.Common;
using F9WebAiTest.Domain.Entities.PayRoll;
using F9WebAiTest.Domain.Entities.Prod;
using F9WebAiTest.Domain.Enums.SYS.Departments;
using F9WebAiTest.Domain.Enums.SYS.Employees;
using SysZq = F9WebAiTest.Domain.Entities.SYS.Zq;
using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments;
using SysEmp = F9WebAiTest.Domain.Entities.SYS.Employees;
using SysWt = F9WebAiTest.Domain.Entities.SYS.WorkTypes;
namespace F9WebAiTest.Application.Services.Prod;
///
/// 职工考勤-考勤登记(CQDJ)。
/// 对应 F9 WinClient\F9.Business.Prod\KQ\CQDJ。
/// 保存语义与老系统一致:按(日期 + 部门 + 工种 + 班次)先软删旧登记与明细,再整批新增(不做增量 diff)。
///
public interface IKaoqinService
{
/// 取某「日期+部门+工种+班次」的整组登记(未登记的职工按默认 9 小时生成)
Task GetRegisterAsync(DateTime date, long depId, long worktypeId, int banci, string keyword);
/// 整组保存(覆盖)
Task SaveRegisterAsync(CqdjSaveDto dto);
/// 删除整组登记(软删)
Task DeleteRegisterAsync(DateTime date, long depId, long worktypeId, int banci);
/// 「添加职工」可选职工(本部门在职、未登记过的)
Task> GetEmployeesAsync(long depId, long worktypeId, int banci);
// ==================== 考勤统计(CQTJ) ====================
/// 统计分页(库中的月快照)
Task> GetCqtjPagedAsync(int year, int month, long depId, long worktypeId,
string like, int pageIndex, int pageSize);
/// 录入:库中已有快照则取库,否则按账期实时汇总
Task GetCqtjForEditAsync(int year, int month, long depId);
/// 保存(先软删该 年/月/部门 旧快照,再整批新增)
Task SaveCqtjAsync(CqtjSaveDto dto);
/// 删除该 年/月/部门 的统计快照
Task DeleteCqtjAsync(int year, int month, long depId);
/// 一键生成:遍历全部车间,缺的才生成
Task GenerateCqtjAllAsync(int year, int month);
/// 全部删除:删除该年月的全部统计快照
Task DeleteCqtjAllAsync(int year, int month);
}
public class KaoqinService : IKaoqinService
{
// 出勤类型(Cqdj_CqType_Enum,老系统硬编码)
private const int CqZhengchang = 1;
private const int CqQingjia = 2;
private const int CqLiudong = 3;
private const int CqJiaban = 4;
private const int CqDaiban = 5;
private const int CqWaipai = 6;
private const int CqKuanggong = 7;
private const int CqGongshang = 8;
private const decimal DefaultHours = 9m; // 新职工默认标准 / 出勤时长
private readonly IRepository _cqdjRepo;
private readonly IRepository _itemRepo;
private readonly IRepository _cqtjRepo;
private readonly IRepository _deptRepo;
private readonly IRepository _empRepo;
private readonly IRepository _wtRepo;
private readonly IRepository _confGzRepo;
private readonly IRepository _confZqRepo;
private readonly IRepository _confEmpRepo;
private readonly IRepository _gwsrkhRepo;
private readonly IRepository _zhangqiRepo;
public KaoqinService(IRepository cqdjRepo, IRepository itemRepo, IRepository cqtjRepo,
IRepository deptRepo, IRepository empRepo,
IRepository wtRepo, IRepository confGzRepo, IRepository confZqRepo,
IRepository confEmpRepo, IRepository gwsrkhRepo, IRepository zhangqiRepo)
{
_cqdjRepo = cqdjRepo;
_itemRepo = itemRepo;
_cqtjRepo = cqtjRepo;
_deptRepo = deptRepo;
_empRepo = empRepo;
_wtRepo = wtRepo;
_confGzRepo = confGzRepo;
_confZqRepo = confZqRepo;
_confEmpRepo = confEmpRepo;
_gwsrkhRepo = gwsrkhRepo;
_zhangqiRepo = zhangqiRepo;
}
// ==================== 查询 ====================
public async Task GetRegisterAsync(DateTime date, long depId, long worktypeId, int banci, string keyword)
{
if (depId <= 0) throw new ServiceException("请先选择部门");
var day = date.Date;
var nextDay = day.AddDays(1);
var depts = (await _deptRepo.GetAllAsync()).ToDictionary(d => d.Id, d => d.Name);
var wts = (await _wtRepo.GetAllAsync()).ToDictionary(w => w.Id, w => w.WorkName);
var emps = await LoadEmployeesAsync(depId, worktypeId, banci);
// 已保存的登记
var saved = await _cqdjRepo.FindAsync(x => x.Date >= day && x.Date < nextDay
&& x.DepId == depId && x.Banci == banci);
if (worktypeId > 0) saved = saved.Where(x => x.WorktypeId == worktypeId).ToList();
var savedByEmp = saved.GroupBy(x => x.EmployeeId).ToDictionary(g => g.Key, g => g.First());
var ids = saved.Select(x => x.Id).ToList();
var items = ids.Count == 0
? new List()
: await _itemRepo.FindAsync(x => ids.Contains(x.CqdjId));
var view = new CqdjViewDto { Date = day, DepId = depId, WorktypeId = worktypeId, Banci = banci };
foreach (var emp in emps)
{
if (!string.IsNullOrWhiteSpace(keyword))
{
var key = keyword.Trim();
if (!((emp.Name ?? "").Contains(key) || (emp.Code ?? "").Contains(key))) continue;
}
var row = new CqdjRowDto
{
EmployeeId = emp.Id,
EmployeeCode = emp.Code ?? "",
EmployeeName = emp.Name ?? "",
DepId = emp.DepId,
DepName = depts.TryGetValue(emp.DepId, out var dn) ? dn : "",
WorktypeId = emp.WorktypeId,
WorktypeName = wts.TryGetValue(emp.WorktypeId, out var wn) ? wn : "",
Banci = banci,
IsXuegong = !emp.IsZhuanzheng,
};
if (savedByEmp.TryGetValue(emp.Id, out var rec))
{
row.Id = rec.Id;
row.Saved = true;
row.IsXuegong = rec.IsXuegong;
row.HoursStandard = rec.HoursStandard;
row.HoursCq = rec.HoursCq;
row.HoursQj = rec.HoursQj;
row.HoursLd = rec.HoursLd;
row.HoursJb = rec.HoursJb;
row.HoursDb = rec.HoursDb;
row.HoursWp = rec.HoursWp;
row.HoursKg = rec.HoursKg;
row.HoursGs = rec.HoursGs;
row.CsYeban = rec.CsYeban;
row.Memo = rec.Memo ?? "";
row.Items = items.Where(i => i.CqdjId == rec.Id)
.OrderBy(i => i.Id)
.Select(ToItemDto).ToList();
}
else
{
// 未登记:默认标准 9 小时 + 一条「正常」明细
row.HoursStandard = DefaultHours;
row.HoursCq = DefaultHours;
row.Items.Add(new CqdjItemDto
{
DepId = emp.DepId,
WorktypeId = emp.WorktypeId,
TypeCq = CqZhengchang,
TypeQj = 0,
Hours = DefaultHours,
});
}
view.Rows.Add(row);
}
return view;
}
// ==================== 保存 ====================
public async Task SaveRegisterAsync(CqdjSaveDto dto)
{
if (dto == null) throw new ServiceException("保存内容为空");
if (dto.Date == default) throw new ServiceException("请选择考勤日期");
if (dto.DepId <= 0) throw new ServiceException("请选择部门");
if (dto.Rows == null || dto.Rows.Count == 0) throw new ServiceException("没有需要保存的职工");
var day = dto.Date.Date;
var nextDay = day.AddDays(1);
var empDict = (await _empRepo.GetAllAsync()).ToDictionary(e => e.Id);
// 校验
foreach (var row in dto.Rows)
{
var name = empDict.TryGetValue(row.EmployeeId, out var e) ? e.Name : $"#{row.EmployeeId}";
var items = row.Items ?? new List();
if (items.Count == 0) throw new ServiceException($"职工「{name}」没有考勤明细");
if (items.Count(x => x.TypeCq == CqZhengchang) != 1)
throw new ServiceException($"职工「{name}」必须且只能有一条「正常」明细");
foreach (var it in items)
{
if (it.TypeCq == CqQingjia && it.TypeQj == 0) throw new ServiceException($"职工「{name}」的请假明细未选择请假类型");
if (it.TypeCq != CqQingjia && it.TypeQj != 0) throw new ServiceException($"职工「{name}」的非请假明细不能带请假类型");
if (it.Hours < 0) throw new ServiceException($"职工「{name}」存在负时长");
}
if (CalcHoursCq(items) < 0) throw new ServiceException($"职工「{name}」的出勤时长不能为负,请检查请假 / 旷工时长");
}
// 整组覆盖:先软删该(日期 + 部门 + 工种 + 班次)的旧登记与明细
var old = await _cqdjRepo.FindAsync(x => x.Date >= day && x.Date < nextDay
&& x.DepId == dto.DepId && x.Banci == dto.Banci);
if (dto.WorktypeId > 0) old = old.Where(x => x.WorktypeId == dto.WorktypeId).ToList();
if (old.Count > 0)
{
var oldIds = old.Select(x => x.Id).ToList();
var oldItems = await _itemRepo.FindAsync(x => oldIds.Contains(x.CqdjId));
if (oldItems.Count > 0) await _itemRepo.DeleteRangeAsync(oldItems);
await _cqdjRepo.DeleteRangeAsync(old);
}
var count = 0;
foreach (var row in dto.Rows)
{
var emp = empDict.TryGetValue(row.EmployeeId, out var e) ? e : null;
var items = row.Items ?? new List();
var master = new Cqdj
{
Date = day,
DepId = dto.DepId,
WorktypeId = row.WorktypeId > 0 ? row.WorktypeId : (emp?.WorktypeId ?? 0),
EmployeeId = row.EmployeeId,
Banci = dto.Banci,
IsXuegong = row.IsXuegong,
HoursStandard = row.HoursStandard,
HoursCq = CalcHoursCq(items),
HoursQj = Sum(items, CqQingjia),
HoursLd = Sum(items, CqLiudong),
HoursJb = Sum(items, CqJiaban),
HoursDb = Sum(items, CqDaiban),
HoursWp = Sum(items, CqWaipai),
HoursKg = Sum(items, CqKuanggong),
HoursGs = Sum(items, CqGongshang),
CsYeban = row.CsYeban,
Memo = row.Memo ?? "",
};
await _cqdjRepo.AddAsync(master);
var entities = items.Select(it => new CqdjItem
{
CqdjId = master.Id,
Date = day,
DepId = it.DepId > 0 ? it.DepId : (row.DepId > 0 ? row.DepId : dto.DepId),
WorktypeId = it.WorktypeId > 0 ? it.WorktypeId : master.WorktypeId,
EmployeeId = row.EmployeeId,
Banci = dto.Banci,
TypeCq = it.TypeCq,
TypeQj = it.TypeQj,
Hours = it.Hours,
Jizu = it.Jizu ?? "",
CtStart = it.CtStart ?? "",
CtEnd = it.CtEnd ?? "",
DangcheNum = it.DangcheNum,
Memo = it.Memo ?? "",
}).ToList();
await _itemRepo.AddRangeAsync(entities);
count++;
}
return count;
}
// ==================== 删除 ====================
public async Task DeleteRegisterAsync(DateTime date, long depId, long worktypeId, int banci)
{
if (depId <= 0) throw new ServiceException("请选择部门");
var day = date.Date;
var nextDay = day.AddDays(1);
var old = await _cqdjRepo.FindAsync(x => x.Date >= day && x.Date < nextDay
&& x.DepId == depId && x.Banci == banci);
if (worktypeId > 0) old = old.Where(x => x.WorktypeId == worktypeId).ToList();
if (old.Count == 0) return 0;
var oldIds = old.Select(x => x.Id).ToList();
var oldItems = await _itemRepo.FindAsync(x => oldIds.Contains(x.CqdjId));
if (oldItems.Count > 0) await _itemRepo.DeleteRangeAsync(oldItems);
await _cqdjRepo.DeleteRangeAsync(old);
return old.Count;
}
// ==================== 下拉 ====================
public async Task> GetEmployeesAsync(long depId, long worktypeId, int banci)
{
if (depId <= 0) return new List();
var depts = (await _deptRepo.GetAllAsync()).ToDictionary(d => d.Id, d => d.Name);
var wts = (await _wtRepo.GetAllAsync()).ToDictionary(w => w.Id, w => w.WorkName);
var emps = await LoadEmployeesAsync(depId, worktypeId, banci);
return emps.Select(e => new CqdjEmpOptionDto
{
Id = e.Id,
Code = e.Code ?? "",
Name = e.Name ?? "",
DepId = e.DepId,
DepName = depts.TryGetValue(e.DepId, out var dn) ? dn : "",
WorktypeId = e.WorktypeId,
WorktypeName = wts.TryGetValue(e.WorktypeId, out var wn) ? wn : "",
IsXuegong = !e.IsZhuanzheng,
}).ToList();
}
// ==================== 考勤统计(CQTJ) ====================
public async Task> GetCqtjPagedAsync(int year, int month, long depId, long worktypeId,
string like, int pageIndex, int pageSize)
{
var dicts = await LoadNameDictsAsync();
var all = (await _cqtjRepo.GetAllAsync())
.Where(x => x.Year == year && x.Month == month);
if (depId > 0) all = all.Where(x => x.DepId == depId);
if (worktypeId > 0) all = all.Where(x => x.WorktypeId == worktypeId);
var rows = all.Select(x => ToCqtjDto(x, dicts)).ToList();
if (!string.IsNullOrWhiteSpace(like))
{
var key = like.Trim();
rows = rows.Where(x => (x.EmployeeName ?? "").Contains(key) || (x.EmployeeCode ?? "").Contains(key)).ToList();
}
var total = rows.Count;
var items = rows
.OrderBy(x => x.DepId).ThenBy(x => x.WorktypeId).ThenBy(x => x.EmployeeCode)
.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
return new PageResult { Items = items, Total = total, PageIndex = pageIndex, PageSize = pageSize };
}
public async Task GetCqtjForEditAsync(int year, int month, long depId)
{
if (depId <= 0) throw new ServiceException("请选择部门");
var dicts = await LoadNameDictsAsync();
var exist = (await _cqtjRepo.GetAllAsync())
.Where(x => x.Year == year && x.Month == month && x.DepId == depId)
.OrderBy(x => x.WorktypeId).ThenBy(x => x.EmployeeId).ToList();
var confZq = await _confZqRepo.FirstOrDefaultAsync(x => x.Year == year && x.Month == month);
var view = new CqtjViewDto
{
Year = year, Month = month, DepId = depId,
DepName = dicts.Depts.TryGetValue(depId, out var dn) ? dn : "",
QsQuanqinDays = confZq?.QsQuanqinDays ?? 0,
Exists = exist.Count > 0
};
view.Rows = exist.Count > 0
? exist.Select(x => ToCqtjDto(x, dicts)).ToList()
: await CalcCqtjAsync(year, month, depId);
return view;
}
public async Task SaveCqtjAsync(CqtjSaveDto dto)
{
if (dto == null) throw new ServiceException("保存内容为空");
if (dto.DepId <= 0) throw new ServiceException("请选择部门");
var zq = await _zhangqiRepo.FirstOrDefaultAsync(x => x.Year == dto.Year && x.Month == dto.Month);
if (zq != null && zq.IsZhaZhang) throw new ServiceException("账期已扎帐,无法修改!");
var old = (await _cqtjRepo.GetAllAsync())
.Where(x => x.Year == dto.Year && x.Month == dto.Month && x.DepId == dto.DepId).ToList();
if (old.Count > 0) await _cqtjRepo.DeleteRangeAsync(old);
var list = (dto.Rows ?? new List()).Select(x => new Cqtj
{
Year = dto.Year, Month = dto.Month,
DepId = dto.DepId, WorktypeId = x.WorktypeId, EmployeeId = x.EmployeeId,
DaysCqStandard = x.DaysCqStandard, DaysCqActual = x.DaysCqActual, CqTianci = x.CqTianci,
HoursCq = x.HoursCq, HoursQj = x.HoursQj, HoursLd = x.HoursLd, HoursJb = x.HoursJb,
HoursDb = x.HoursDb, HoursWp = x.HoursWp, HoursKg = x.HoursKg, CsYeban = x.CsYeban,
HoursQjShijia = x.HoursQjShijia, HoursQjBj = x.HoursQjBj, HoursQjHj = x.HoursQjHj,
HoursQjCj = x.HoursQjCj, HoursQjLx = x.HoursQjLx, HoursQjSangjia = x.HoursQjSangjia,
HoursQjGj = x.HoursQjGj, HoursQjNj = x.HoursQjNj, HoursQjTx = x.HoursQjTx,
HoursXgCq = x.HoursXgCq, HoursXgJiaban = x.HoursXgJiaban,
HoursFadingjiaqi = x.HoursFadingjiaqi, HoursQjQqmc = x.HoursQjQqmc, HoursQjQqcal = x.HoursQjQqcal,
JineQuanqin = x.JineQuanqin, JineJb = x.JineJb, JineLiudong = x.JineLiudong,
JineDaiban = x.JineDaiban, JineYb = x.JineYb, JineKgKk = x.JineKgKk,
JineJqBzHj = x.JineJqBzHj, JineJqBzBj = x.JineJqBzBj, JineJqBzLx = x.JineJqBzLx,
JineJqBzSangjia = x.JineJqBzSangjia, JineJqBzFading = x.JineJqBzFading,
JineJqBzTxnj = x.JineJqBzTxnj, JineJqBz = x.JineJqBz,
Memo = x.Memo ?? ""
}).ToList();
await _cqtjRepo.AddRangeAsync(list);
return list.Count;
}
public async Task DeleteCqtjAsync(int year, int month, long depId)
{
if (depId <= 0) throw new ServiceException("请选择部门");
var zq = await _zhangqiRepo.FirstOrDefaultAsync(x => x.Year == year && x.Month == month);
if (zq != null && zq.IsZhaZhang) throw new ServiceException("账期已扎帐,无法修改!");
var old = (await _cqtjRepo.GetAllAsync())
.Where(x => x.Year == year && x.Month == month && x.DepId == depId).ToList();
if (old.Count == 0) return 0;
await _cqtjRepo.DeleteRangeAsync(old);
return old.Count;
}
public async Task GenerateCqtjAllAsync(int year, int month)
{
var zq = await _zhangqiRepo.FirstOrDefaultAsync(x => x.Year == year && x.Month == month);
if (zq != null && zq.IsZhaZhang) throw new ServiceException("账期已扎帐,无法生成!");
var confZq = await _confZqRepo.FirstOrDefaultAsync(x => x.Year == year && x.Month == month);
if (confZq == null || confZq.QsQuanqinDays <= 0)
throw new ServiceException("请先录入薪资配置账期的前缫出勤天数!");
var depts = (await _deptRepo.GetAllAsync())
.Where(d => d.IsActive && d.ManageType != Department_ManageType_Enum.公司)
.ToList();
var saved = (await _cqtjRepo.GetAllAsync())
.Where(x => x.Year == year && x.Month == month)
.Select(x => x.DepId).Distinct().ToHashSet();
var count = 0;
foreach (var d in depts)
{
if (saved.Contains(d.Id)) continue;
var rows = await CalcCqtjAsync(year, month, d.Id);
if (rows.Count == 0) continue;
await SaveCqtjAsync(new CqtjSaveDto { Year = year, Month = month, DepId = d.Id, Rows = rows });
count++;
}
return count;
}
public async Task DeleteCqtjAllAsync(int year, int month)
{
var zq = await _zhangqiRepo.FirstOrDefaultAsync(x => x.Year == year && x.Month == month);
if (zq != null && zq.IsZhaZhang) throw new ServiceException("账期已扎帐,无法修改!");
var old = (await _cqtjRepo.GetAllAsync()).Where(x => x.Year == year && x.Month == month).ToList();
if (old.Count == 0) return 0;
await _cqtjRepo.DeleteRangeAsync(old);
return old.Count;
}
// ==================== 私有 ====================
private class NameDicts
{
public Dictionary Depts = new();
public Dictionary Worktypes = new();
public Dictionary Emps = new();
}
private async Task LoadNameDictsAsync() => new()
{
Depts = (await _deptRepo.GetAllAsync()).ToDictionary(d => d.Id, d => d.Name),
Worktypes = (await _wtRepo.GetAllAsync()).ToDictionary(w => w.Id, w => w.WorkName),
Emps = (await _empRepo.GetAllAsync()).ToDictionary(e => e.Id)
};
private static CqtjRowDto ToCqtjDto(Cqtj x, NameDicts d) => new()
{
Id = x.Id, Year = x.Year, Month = x.Month,
DepId = x.DepId, DepName = d.Depts.TryGetValue(x.DepId, out var dn) ? dn : "",
WorktypeId = x.WorktypeId, WorktypeName = d.Worktypes.TryGetValue(x.WorktypeId, out var wn) ? wn : "",
EmployeeId = x.EmployeeId,
EmployeeCode = d.Emps.TryGetValue(x.EmployeeId, out var e) ? (e.Code ?? "") : "",
EmployeeName = d.Emps.TryGetValue(x.EmployeeId, out var e2) ? (e2.Name ?? "") : "",
DaysCqStandard = x.DaysCqStandard, DaysCqActual = x.DaysCqActual, CqTianci = x.CqTianci,
HoursCq = x.HoursCq, HoursQj = x.HoursQj, HoursLd = x.HoursLd, HoursJb = x.HoursJb,
HoursDb = x.HoursDb, HoursWp = x.HoursWp, HoursKg = x.HoursKg, CsYeban = x.CsYeban,
HoursQjShijia = x.HoursQjShijia, HoursQjBj = x.HoursQjBj, HoursQjHj = x.HoursQjHj,
HoursQjCj = x.HoursQjCj, HoursQjLx = x.HoursQjLx, HoursQjSangjia = x.HoursQjSangjia,
HoursQjGj = x.HoursQjGj, HoursQjNj = x.HoursQjNj, HoursQjTx = x.HoursQjTx,
HoursXgCq = x.HoursXgCq, HoursXgJiaban = x.HoursXgJiaban,
HoursFadingjiaqi = x.HoursFadingjiaqi, HoursQjQqmc = x.HoursQjQqmc, HoursQjQqcal = x.HoursQjQqcal,
JineQuanqin = x.JineQuanqin, JineJb = x.JineJb, JineLiudong = x.JineLiudong,
JineDaiban = x.JineDaiban, JineYb = x.JineYb, JineKgKk = x.JineKgKk,
JineJqBzHj = x.JineJqBzHj, JineJqBzBj = x.JineJqBzBj, JineJqBzLx = x.JineJqBzLx,
JineJqBzSangjia = x.JineJqBzSangjia, JineJqBzFading = x.JineJqBzFading,
JineJqBzTxnj = x.JineJqBzTxnj, JineJqBz = x.JineJqBz,
Memo = x.Memo ?? ""
};
/// 账期(取不到时按自然月兜底)
private async Task<(DateTime Start, DateTime End)> GetZhangqiRangeAsync(int year, int month)
{
var zq = await _zhangqiRepo.FirstOrDefaultAsync(x => x.Year == year && x.Month == month);
if (zq != null) return (zq.StartDate.Date, zq.EndDate.Date.AddDays(1).AddSeconds(-1));
var start = new DateTime(year, month, 1);
return (start, start.AddMonths(1).AddSeconds(-1));
}
///
/// 按账期汇总某部门的考勤统计(对齐 Page_Cqtj.CqtjViews)。
/// 全勤奖涉及「该职工在全部部门 / 工种」的合计出勤 / 请假 / 旷工,故登记数据按账期全量加载。
///
private async Task> CalcCqtjAsync(int year, int month, long depId)
{
var (start, end) = await GetZhangqiRangeAsync(year, month);
var confZq = await _confZqRepo.FirstOrDefaultAsync(x => x.Year == year && x.Month == month);
var qsDays = confZq?.QsQuanqinDays ?? 0;
if (qsDays <= 0) throw new ServiceException("请先在【薪资配置账期】功能中设置当月前缫出勤天数!");
var dicts = await LoadNameDictsAsync();
var cqdjAll = await _cqdjRepo.FindAsync(x => x.Date >= start && x.Date <= end);
if (cqdjAll.Count == 0) return new List();
var ids = cqdjAll.Select(x => x.Id).ToList();
var itemsAll = await _itemRepo.FindAsync(x => ids.Contains(x.CqdjId));
var confGzAll = await _confGzRepo.GetAllAsync();
var confEmpAll = await _confEmpRepo.GetAllAsync();
var gwsrkhAll = (await _gwsrkhRepo.GetAllAsync()).Where(x => x.Year == year && x.Month == month).ToList();
var deptsAll = await _deptRepo.GetAllAsync();
// 每职工全部门合计(全勤奖口径)
var cqHoursByEmp = cqdjAll.GroupBy(x => x.EmployeeId).Select(g => new
{
EmployeeId = g.Key,
CqHours = g.Sum(x => x.HoursCq),
JiabanHours = g.Sum(x => x.HoursJb),
QjHours = g.Sum(x => x.HoursQj),
KgHours = g.Sum(x => x.HoursKg)
}).ToDictionary(x => x.EmployeeId);
var groups = cqdjAll.Where(x => x.DepId == depId)
.GroupBy(x => new { x.EmployeeId, x.DepId, x.WorktypeId })
.Select(g => new
{
g.Key.EmployeeId, g.Key.DepId, g.Key.WorktypeId,
HoursCq = g.Sum(x => x.HoursCq),
HoursQj = g.Sum(x => x.HoursQj),
HoursLd = g.Sum(x => x.HoursLd),
HoursJb = g.Sum(x => x.HoursJb),
HoursDb = g.Sum(x => x.HoursDb),
HoursWp = g.Sum(x => x.HoursWp),
HoursKg = g.Sum(x => x.HoursKg),
CsYeban = g.Sum(x => x.CsYeban),
List = g.ToList()
}).ToList();
var result = new List();
foreach (var item in groups)
{
if (item.HoursCq + item.HoursJb + item.HoursQj + item.HoursLd + item.HoursDb
+ item.HoursKg + item.CsYeban == 0) continue;
var empDepWtIds = item.List.Select(x => x.Id).ToHashSet();
var depItems = itemsAll.Where(x => empDepWtIds.Contains(x.CqdjId)).ToList();
var confEmp = confEmpAll.FirstOrDefault(x => x.EmployeeId == item.EmployeeId && x.WorktypeId == item.WorktypeId);
var confGz = confGzAll.FirstOrDefault(x => x.WorkTypeId == item.WorktypeId);
dicts.Emps.TryGetValue(item.EmployeeId, out var emp);
var row = new CqtjRowDto
{
Year = year, Month = month,
DepId = item.DepId, DepName = dicts.Depts.TryGetValue(item.DepId, out var dn) ? dn : "",
WorktypeId = item.WorktypeId, WorktypeName = dicts.Worktypes.TryGetValue(item.WorktypeId, out var wn) ? wn : "",
EmployeeId = item.EmployeeId,
EmployeeCode = emp?.Code ?? "", EmployeeName = emp?.Name ?? "",
HoursCq = item.HoursCq, HoursQj = item.HoursQj, HoursLd = item.HoursLd,
HoursJb = item.HoursJb, HoursDb = item.HoursDb, HoursWp = item.HoursWp,
HoursKg = item.HoursKg, CsYeban = item.CsYeban
};
if (confGz == null) { result.Add(row); continue; }
// 学工时长
var xgList = item.List.Where(x => x.IsXuegong).ToList();
var xgIds = xgList.Select(x => x.Id).ToHashSet();
var xgItems = itemsAll.Where(x => xgIds.Contains(x.CqdjId)).ToList();
row.HoursXgCq = xgList.Sum(x => x.HoursCq);
row.HoursXgJiaban = xgList.Sum(x => x.HoursJb);
// 法定假
var fadingBzHours = confEmp?.HoursFadingBz ?? 0;
var fadingGzHours = confEmp?.HoursFadingGz ?? 0;
var jineFadingBz = jisuanFadingBz(fadingBzHours, fadingGzHours, confGz);
row.HoursFadingjiaqi = R2(fadingBzHours + fadingGzHours);
// 假期补助
var bz = jiaqiBuzhuJisuan(confGzAll, depItems, jineFadingBz);
row.HoursQjShijia = bz.HoursShijia; row.HoursQjBj = bz.HoursBj; row.HoursQjHj = bz.HoursHj;
row.HoursQjCj = bz.HoursCj; row.HoursQjLx = bz.HoursLx; row.HoursQjSangjia = bz.HoursSangjia;
row.HoursQjGj = bz.HoursGj; row.HoursQjNj = bz.HoursNj; row.HoursQjTx = bz.HoursTx;
row.JineJqBzHj = bz.JineHj; row.JineJqBzBj = bz.JineBj; row.JineJqBzLx = bz.JineLx;
row.JineJqBzSangjia = bz.JineSangjia; row.JineJqBzFading = jineFadingBz;
row.JineJqBzTxnj = bz.JineTxnj; row.JineJqBz = bz.JineTotal;
var daixinQjHours = bz.HoursNj + bz.HoursTx + bz.HoursGj + bz.HoursLx + bz.HoursHj + bz.HoursSangjia + bz.HoursCj;
var empSum = cqHoursByEmp.TryGetValue(item.EmployeeId, out var s) ? s : null;
var sumCqHours = empSum?.CqHours ?? 0;
var sumJiabanHours = empSum?.JiabanHours ?? 0;
var sumQjHours = empSum?.QjHours ?? 0;
var sumKgHours = empSum?.KgHours ?? 0;
// 加班免扣请假 / 全勤计算请假
row.HoursQjQqmc = jisuanJiabanMkHours(confGz, sumJiabanHours, item.HoursQj, qsDays, item.HoursCq);
var sumDaixinHours = sumDaixinHoursJisuan(itemsAll, item.EmployeeId);
var qqJisuanQjHours = sumQjHours - row.HoursQjQqmc - sumDaixinHours;
var depQj = item.HoursQj - row.HoursQjQqmc - daixinQjHours;
row.HoursQjQqcal = depQj < 0 ? 0 : depQj;
// 全勤奖
var hoursPerday = confGz.HoursPerday;
if (sumCqHours <= 15 * hoursPerday && emp != null && emp.WorkStatus == Employee_WorkStatus_Enum.离职)
row.JineQuanqin = 0;
else if (confGz.JineQuanqin > 0)
row.JineQuanqin = quanqin(confGz, sumCqHours, sumQjHours, item.HoursCq, item.HoursJb,
row.HoursQjQqcal, qqJisuanQjHours, daixinQjHours, confEmp, sumDaixinHours, sumCqHours,
sumKgHours, emp, start, end, qsDays,
deptsAll.FirstOrDefault(d => d.Id == item.DepId), sumJiabanHours);
// 加班费 / 流动 / 代班 / 夜班 / 旷工
var gwsrkh = gwsrkhAll.FirstOrDefault(x => x.EmployeeId == item.EmployeeId);
row.JineJb = jisuanJianbanJine(depItems, confGzAll, xgItems, gwsrkh?.KhJieguo);
row.JineLiudong = liudongJineJisuan(depItems, confGz);
row.JineDaiban = daibanJineJisuan(depItems, confGzAll);
row.JineYb = R2(confGz.JineYeban * item.CsYeban);
row.JineKgKk = R2(confGz.JiaqiJineKuangongKk * item.HoursKg);
row.CqTianci = jisuanCqTianci(item.List);
row.DaysCqStandard = confGz.QqDaysStandard;
row.DaysCqActual = hoursPerday == 0 ? 0 : R2(item.HoursCq / hoursPerday);
result.Add(row);
}
return result;
}
// ---------- 口径函数(对齐 Page_Cqtj 的同名方法) ----------
private static decimal R2(decimal v) => Math.Round(v, 2, MidpointRounding.AwayFromZero);
private class JiaqiBuzhu
{
public decimal HoursShijia, HoursBj, HoursHj, HoursCj, HoursLx, HoursSangjia, HoursGj, HoursNj, HoursTx;
public decimal JineHj, JineBj, JineLx, JineSangjia, JineTxnj, JineTotal;
}
/// 假期补助 + 请假分项时长(按明细的 请假类型 × 出勤工种 分组计算)
private static JiaqiBuzhu jiaqiBuzhuJisuan(List confGzAll, List items, decimal jineFadingBz)
{
var r = new JiaqiBuzhu();
var groups = items.GroupBy(x => new { x.TypeQj, x.WorktypeId })
.Select(g => new { Hours = g.Sum(x => x.Hours), g.Key.TypeQj, g.Key.WorktypeId }).ToList();
foreach (var g in groups)
{
var cfg = confGzAll.FirstOrDefault(x => x.WorkTypeId == g.WorktypeId);
switch (g.TypeQj)
{
case 2: // 病假
r.HoursBj = g.Hours;
r.JineBj += R2((cfg?.JiaqiJineBingjiaBz ?? 0) * g.Hours); break;
case 10: // 轮休
r.HoursLx = g.Hours;
r.JineLx += R2((cfg?.JiaqiJineLunxiuBz ?? 0) * g.Hours); break;
case 4: // 婚假
r.HoursHj = g.Hours;
r.JineHj += R2((cfg?.JiaqiJineHunjiaBz ?? 0) * g.Hours); break;
case 5: // 丧假
r.HoursSangjia = g.Hours;
r.JineSangjia += R2((cfg?.JiaqiJineSangjiaBz ?? 0) * g.Hours); break;
case 3: // 产假
r.HoursCj = g.Hours; break;
case 1: // 事假
r.HoursShijia = g.Hours; break;
case 7: // 年假
r.HoursNj = g.Hours;
r.JineTxnj += R2((cfg?.Jiaqi_Jine_NianjiaBz ?? 0) * g.Hours); break;
case 6: // 公假
r.HoursGj = g.Hours; break;
case 9: // 调休
r.HoursTx = g.Hours;
r.JineTxnj += R2((cfg?.Jiaqi_Jine_NianjiaBz ?? 0) * g.Hours); break;
}
}
r.JineTotal = r.JineHj + r.JineBj + r.JineLx + r.JineSangjia + jineFadingBz + r.JineTxnj;
return r;
}
/// 加班免扣请假时长:满勤且加班达到门槛时免扣固定时长
private static decimal jisuanJiabanMkHours(ConfGz cfg, decimal sumJiabanHours, decimal hoursQj,
decimal qsDays, decimal depCqHours)
{
var manqinHours = qsDays * cfg.HoursPerday;
if (depCqHours + sumJiabanHours >= manqinHours && sumJiabanHours >= cfg.QqHoursDkMinjiaban && sumJiabanHours != 0)
return cfg.QqHoursDkQingjia;
return 0;
}
/// 带薪请假时长合计(跨部门,按该职工全部明细)
private static decimal sumDaixinHoursJisuan(List items, long employeeId)
=> items.Where(x => x.EmployeeId == employeeId && x.TypeQj is 10 or 7 or 6 or 9 or 4 or 3 or 5)
.Sum(x => x.Hours);
/// 加班费(含学工加班:学工岗位薪资折算)
private static decimal jisuanJianbanJine(List items, List confGzAll,
List xgItems, string khJieguo)
{
decimal jine = 0;
var groups = items.Where(x => x.TypeCq == CqJiaban)
.GroupBy(x => x.WorktypeId)
.Select(g => new { Hours = g.Sum(x => x.Hours), WorktypeId = g.Key }).ToList();
foreach (var g in groups)
{
var cfg = confGzAll.FirstOrDefault(x => x.WorkTypeId == g.WorktypeId);
decimal price = cfg?.JiabanJineBz ?? 0;
if (!string.IsNullOrWhiteSpace(khJieguo) && cfg != null)
{
price = khJieguo switch
{
"欠合格" => cfg.Jiaban_GwsrkhJineQianhege,
"期望" => cfg.Jiaban_GwsrkhJineQiwang,
"合格" => cfg.Jiaban_GwsrkhJineHege,
"胜任" => cfg.Jiaban_GwsrkhJineShengren,
"超胜任" => cfg.Jiaban_GwsrkhJineChaoshengren,
_ => cfg.JiabanJineBz
};
}
jine += g.Hours * price;
}
var xgGroups = xgItems.Where(x => x.TypeCq == CqJiaban)
.GroupBy(x => x.WorktypeId)
.Select(g => new { Hours = g.Sum(x => x.Hours), WorktypeId = g.Key }).ToList();
foreach (var g in xgGroups)
{
var cfg = confGzAll.FirstOrDefault(x => x.WorkTypeId == g.WorktypeId);
if (cfg == null) continue;
var std = cfg.QqDaysStandard * cfg.HoursPerday;
if (std == 0) continue;
jine += cfg.XgJineGangwei / std * g.Hours;
}
return R2(jine);
}
/// 流动工资:工种配置的固定流动工资(元/小时)× 流动时长
private static decimal liudongJineJisuan(List items, ConfGz cfg)
{
decimal jine = 0;
var groups = items.Where(x => x.TypeCq == CqLiudong)
.GroupBy(x => new { x.DepId, x.WorktypeId })
.Select(g => new { Hours = g.Sum(x => x.Hours) }).ToList();
foreach (var g in groups) jine += R2(cfg.LiudongJineGuding * g.Hours);
return R2(jine);
}
/// 代班工资:代班所在工种岗位薪资 / 标准出勤时长 × 代班时长
private static decimal daibanJineJisuan(List items, List confGzAll)
{
decimal jine = 0;
var groups = items.Where(x => x.TypeCq == CqDaiban)
.GroupBy(x => new { x.DepId, x.WorktypeId })
.Select(g => new { Hours = g.Sum(x => x.Hours), g.Key.WorktypeId }).ToList();
foreach (var g in groups)
{
var cfg = confGzAll.FirstOrDefault(x => x.WorkTypeId == g.WorktypeId);
if (cfg == null) continue;
var std = cfg.QqDaysStandard * cfg.HoursPerday;
if (std == 0) continue;
jine += cfg.JineGangwei / std * g.Hours;
}
return R2(jine);
}
/// 出勤天次:(出勤 + 加班) > 0 的天数
private static decimal jisuanCqTianci(List list)
=> list.Count(x => x.HoursCq + x.HoursJb > 0);
/// 法定假补助:补助小时 × 补助单价 + 工资小时 × 工资单价
private static decimal jisuanFadingBz(decimal hoursFadingBz, decimal hoursFadingGz, ConfGz cfg)
{
if (hoursFadingBz + hoursFadingGz <= 0) return 0;
return R2(hoursFadingBz * cfg.JiaqiJineFadingBz + hoursFadingGz * cfg.JiaqiJineFadingGz);
}
///
/// 全勤奖(对齐 Page_Cqtj.quanqin)。
/// sumCqHours / sumQjHours / sumKgHours 为该职工全部门合计;depCqHours / depHoursQj 为本部门。
///
private decimal quanqin(ConfGz cfg, decimal sumCqHours, decimal sumQjHours, decimal depCqHours, decimal hoursJb,
decimal depHoursQj, decimal qqJisuanQjHours, decimal daixinQjHours, ConfEmployee confEmp,
decimal sumDaixinHours, decimal sumCqHours2, decimal sumKgHours, SysEmp.Employee emp,
DateTime zqStart, DateTime zqEnd, decimal qsDays, SysDept.Department dep, decimal sumJiabanHours)
{
var hoursPerday = cfg.HoursPerday;
var jineQuanqin = confEmp != null && confEmp.IsXuegong ? cfg.XgJineQuanqin : cfg.JineQuanqin;
var qqHoursMaxQj = cfg.QqHoursMaxQj;
var standardCqHours = cfg.QqDaysStandard * hoursPerday;
if (hoursPerday <= 0) return 0;
var ruzhi = emp?.RuzhiDate;
var lizhi = emp?.LizhiDate;
bool InPeriod(DateTime? d) => d.HasValue && d.Value.Date >= zqStart.Date && d.Value.Date <= zqEnd.Date;
// 1、总旷工超过半天(含半天)不计全勤
if (sumKgHours >= hoursPerday / 2) return 0;
// 2、出勤总时长小于等于 3 天不计全勤
if (sumCqHours <= hoursPerday * 3) return 0;
if (sumCqHours != depCqHours) // 存在多部门 / 多工种出勤
{
var shijiCqDays = (sumCqHours + sumDaixinHours) / hoursPerday;
if (InPeriod(lizhi))
return standardCqHours == 0 ? 0 : R2(jineQuanqin / standardCqHours * depCqHours);
if (InPeriod(ruzhi))
{
if (shijiCqDays >= qsDays) return sumCqHours == 0 ? 0 : jineQuanqin / sumCqHours * depCqHours;
if (shijiCqDays < 3) return 0;
return standardCqHours == 0 ? 0 : R2(jineQuanqin / standardCqHours * depCqHours);
}
var chushi = (sumCqHours + sumDaixinHours) / hoursPerday >= 15
? (sumCqHours == 0 ? 0 : jineQuanqin / sumCqHours * depCqHours)
: (standardCqHours == 0 ? 0 : jineQuanqin / standardCqHours * depCqHours);
if (qqJisuanQjHours >= qqHoursMaxQj) return 0;
return qqHoursMaxQj == 0 ? 0 : R2(chushi - (chushi / qqHoursMaxQj) * qqJisuanQjHours);
}
var shijiDays = (sumCqHours + sumDaixinHours) / hoursPerday;
if (InPeriod(lizhi))
return standardCqHours == 0 ? 0 : R2(jineQuanqin / standardCqHours * sumCqHours);
if (InPeriod(ruzhi))
{
if (shijiDays >= qsDays) return jineQuanqin;
if (shijiDays < 3) return 0;
return standardCqHours == 0 ? 0 : R2(jineQuanqin / standardCqHours * sumCqHours);
}
var chushi2 = (sumCqHours + daixinQjHours) / hoursPerday >= 15
? jineQuanqin
: (standardCqHours == 0 ? 0 : jineQuanqin / standardCqHours * sumCqHours);
// 设备办全职:出勤含加班达到前缫出勤天数且不低于标准全勤时长时,请假不扣全勤
if (dep != null && dep.DutyType == Department_DutyType_Enum.设备办)
{
var cqhours = sumCqHours + daixinQjHours + sumJiabanHours;
if (cqhours / hoursPerday >= qsDays && cqhours >= standardCqHours) return R2(chushi2);
}
if (qqJisuanQjHours >= qqHoursMaxQj) return 0;
return qqHoursMaxQj == 0 ? 0 : R2(chushi2 - (chushi2 / qqHoursMaxQj) * depHoursQj);
}
/// 本部门在职职工(按工种 / 班次过滤,排除已离职、未入职、工种为 0)
private async Task> LoadEmployeesAsync(long depId, long worktypeId, int banci)
{
var all = await _empRepo.GetAllAsync();
return all.Where(e => e.DepId == depId
&& e.WorktypeId != 0
&& e.WorkStatus != Employee_WorkStatus_Enum.离职
&& (worktypeId <= 0 || e.WorktypeId == worktypeId)
&& (banci <= 0 || e.Banci == null || (int)e.Banci == banci))
.OrderBy(e => e.Code).ThenBy(e => e.Name)
.ToList();
}
/// 出勤时长 = 正常 + 流动 + 代班 + 外派 + 工伤(不含加班)
private static decimal CalcHoursCq(List items)
=> items.Where(x => x.TypeCq == CqZhengchang || x.TypeCq == CqLiudong || x.TypeCq == CqDaiban
|| x.TypeCq == CqWaipai || x.TypeCq == CqGongshang)
.Sum(x => x.Hours);
private static decimal Sum(List items, int typeCq)
=> items.Where(x => x.TypeCq == typeCq).Sum(x => x.Hours);
private static CqdjItemDto ToItemDto(CqdjItem x) => new()
{
Id = x.Id,
DepId = x.DepId,
WorktypeId = x.WorktypeId,
TypeCq = x.TypeCq,
TypeQj = x.TypeQj,
Hours = x.Hours,
Jizu = x.Jizu ?? "",
CtStart = x.CtStart ?? "",
CtEnd = x.CtEnd ?? "",
DangcheNum = x.DangcheNum,
Memo = x.Memo ?? "",
};
}