Files
f9-web-ai-test/backend/F9WebAiTest.Application/Services/Prod/KaoqinService.cs
T
yanpeng200212 1f1b184411 新增工资/仓储/生产日报等模块,移除旧考勤模块
- 新增 PayRoll(工资)、Wms(仓储)、Prod(车速/缫丝/考勤/丝片检测/丝片台账/纤检日报/质量日报) 模块
- 新增对应 Domain 实体、Application DTO/Service、Api Controller 与前端页面
- 移除旧 Attendance 考勤模块(Controller/Service/Entity/DTO 及 5 个前端页面),由 Prod/Kaoqin 取代
- 调整路由、仪表盘、Lsjl 流丝记录实体与服务
- 更新业务种子数据与 DbSeeder
- 补充工作进度文档 14-27
2026-10-27 09:09:57 +08:00

891 lines
42 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/// <summary>
/// 职工考勤-考勤登记(CQDJ)。
/// 对应 F9 WinClient\F9.Business.Prod\KQ\CQDJ。
/// 保存语义与老系统一致:按(日期 + 部门 + 工种 + 班次)先软删旧登记与明细,再整批新增(不做增量 diff)。
/// </summary>
public interface IKaoqinService
{
/// <summary>取某「日期+部门+工种+班次」的整组登记(未登记的职工按默认 9 小时生成)</summary>
Task<CqdjViewDto> GetRegisterAsync(DateTime date, long depId, long worktypeId, int banci, string keyword);
/// <summary>整组保存(覆盖)</summary>
Task<int> SaveRegisterAsync(CqdjSaveDto dto);
/// <summary>删除整组登记(软删)</summary>
Task<int> DeleteRegisterAsync(DateTime date, long depId, long worktypeId, int banci);
/// <summary>「添加职工」可选职工(本部门在职、未登记过的)</summary>
Task<List<CqdjEmpOptionDto>> GetEmployeesAsync(long depId, long worktypeId, int banci);
// ==================== 考勤统计(CQTJ) ====================
/// <summary>统计分页(库中的月快照)</summary>
Task<PageResult<CqtjRowDto>> GetCqtjPagedAsync(int year, int month, long depId, long worktypeId,
string like, int pageIndex, int pageSize);
/// <summary>录入:库中已有快照则取库,否则按账期实时汇总</summary>
Task<CqtjViewDto> GetCqtjForEditAsync(int year, int month, long depId);
/// <summary>保存(先软删该 年/月/部门 旧快照,再整批新增)</summary>
Task<int> SaveCqtjAsync(CqtjSaveDto dto);
/// <summary>删除该 年/月/部门 的统计快照</summary>
Task<int> DeleteCqtjAsync(int year, int month, long depId);
/// <summary>一键生成:遍历全部车间,缺的才生成</summary>
Task<int> GenerateCqtjAllAsync(int year, int month);
/// <summary>全部删除:删除该年月的全部统计快照</summary>
Task<int> 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<Cqdj> _cqdjRepo;
private readonly IRepository<CqdjItem> _itemRepo;
private readonly IRepository<Cqtj> _cqtjRepo;
private readonly IRepository<SysDept.Department> _deptRepo;
private readonly IRepository<SysEmp.Employee> _empRepo;
private readonly IRepository<SysWt.WorkType> _wtRepo;
private readonly IRepository<ConfGz> _confGzRepo;
private readonly IRepository<ConfZq> _confZqRepo;
private readonly IRepository<ConfEmployee> _confEmpRepo;
private readonly IRepository<Gwsrkh> _gwsrkhRepo;
private readonly IRepository<SysZq.Zhangqi> _zhangqiRepo;
public KaoqinService(IRepository<Cqdj> cqdjRepo, IRepository<CqdjItem> itemRepo, IRepository<Cqtj> cqtjRepo,
IRepository<SysDept.Department> deptRepo, IRepository<SysEmp.Employee> empRepo,
IRepository<SysWt.WorkType> wtRepo, IRepository<ConfGz> confGzRepo, IRepository<ConfZq> confZqRepo,
IRepository<ConfEmployee> confEmpRepo, IRepository<Gwsrkh> gwsrkhRepo, IRepository<SysZq.Zhangqi> 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<CqdjViewDto> 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<CqdjItem>()
: 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<int> 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<CqdjItemDto>();
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<CqdjItemDto>();
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<int> 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<List<CqdjEmpOptionDto>> GetEmployeesAsync(long depId, long worktypeId, int banci)
{
if (depId <= 0) return new List<CqdjEmpOptionDto>();
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<PageResult<CqtjRowDto>> 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<CqtjRowDto> { Items = items, Total = total, PageIndex = pageIndex, PageSize = pageSize };
}
public async Task<CqtjViewDto> 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<int> 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<CqtjRowDto>()).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<int> 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<int> 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<int> 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<long, string> Depts = new();
public Dictionary<long, string> Worktypes = new();
public Dictionary<long, SysEmp.Employee> Emps = new();
}
private async Task<NameDicts> 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 ?? ""
};
/// <summary>账期(取不到时按自然月兜底)</summary>
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));
}
/// <summary>
/// 按账期汇总某部门的考勤统计(对齐 Page_Cqtj.CqtjViews)。
/// 全勤奖涉及「该职工在全部部门 / 工种」的合计出勤 / 请假 / 旷工,故登记数据按账期全量加载。
/// </summary>
private async Task<List<CqtjRowDto>> 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<CqtjRowDto>();
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<CqtjRowDto>();
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;
}
/// <summary>假期补助 + 请假分项时长(按明细的 请假类型 × 出勤工种 分组计算)</summary>
private static JiaqiBuzhu jiaqiBuzhuJisuan(List<ConfGz> confGzAll, List<CqdjItem> 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;
}
/// <summary>加班免扣请假时长:满勤且加班达到门槛时免扣固定时长</summary>
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;
}
/// <summary>带薪请假时长合计(跨部门,按该职工全部明细)</summary>
private static decimal sumDaixinHoursJisuan(List<CqdjItem> 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);
/// <summary>加班费(含学工加班:学工岗位薪资折算)</summary>
private static decimal jisuanJianbanJine(List<CqdjItem> items, List<ConfGz> confGzAll,
List<CqdjItem> 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);
}
/// <summary>流动工资:工种配置的固定流动工资(元/小时)× 流动时长</summary>
private static decimal liudongJineJisuan(List<CqdjItem> 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);
}
/// <summary>代班工资:代班所在工种岗位薪资 / 标准出勤时长 × 代班时长</summary>
private static decimal daibanJineJisuan(List<CqdjItem> items, List<ConfGz> 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);
}
/// <summary>出勤天次:(出勤 + 加班) &gt; 0 的天数</summary>
private static decimal jisuanCqTianci(List<Cqdj> list)
=> list.Count(x => x.HoursCq + x.HoursJb > 0);
/// <summary>法定假补助:补助小时 × 补助单价 + 工资小时 × 工资单价</summary>
private static decimal jisuanFadingBz(decimal hoursFadingBz, decimal hoursFadingGz, ConfGz cfg)
{
if (hoursFadingBz + hoursFadingGz <= 0) return 0;
return R2(hoursFadingBz * cfg.JiaqiJineFadingBz + hoursFadingGz * cfg.JiaqiJineFadingGz);
}
/// <summary>
/// 全勤奖(对齐 Page_Cqtj.quanqin)。
/// sumCqHours / sumQjHours / sumKgHours 为该职工全部门合计;depCqHours / depHoursQj 为本部门。
/// </summary>
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);
}
/// <summary>本部门在职职工(按工种 / 班次过滤,排除已离职、未入职、工种为 0)</summary>
private async Task<List<SysEmp.Employee>> 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();
}
/// <summary>出勤时长 = 正常 + 流动 + 代班 + 外派 + 工伤(不含加班)</summary>
private static decimal CalcHoursCq(List<CqdjItemDto> 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<CqdjItemDto> 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 ?? "",
};
}