新增工资/仓储/生产日报等模块,移除旧考勤模块

- 新增 PayRoll(工资)、Wms(仓储)、Prod(车速/缫丝/考勤/丝片检测/丝片台账/纤检日报/质量日报) 模块
- 新增对应 Domain 实体、Application DTO/Service、Api Controller 与前端页面
- 移除旧 Attendance 考勤模块(Controller/Service/Entity/DTO 及 5 个前端页面),由 Prod/Kaoqin 取代
- 调整路由、仪表盘、Lsjl 流丝记录实体与服务
- 更新业务种子数据与 DbSeeder
- 补充工作进度文档 14-27
This commit is contained in:
yanpeng200212
2026-10-27 09:09:57 +08:00
parent 69c4052a64
commit 1f1b184411
119 changed files with 21576 additions and 1668 deletions
@@ -0,0 +1,257 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Dtos.Prod;
using F9WebAiTest.Domain.Common;
using F9WebAiTest.Domain.Entities.Base;
using F9WebAiTest.Domain.Entities.Prod;
using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments;
namespace F9WebAiTest.Application.Services.Prod;
/// <summary>秤大丝:以落丝记录为主表,按车台录入 4 个丝量值并汇总把数/丝量/缫折/台产</summary>
public interface ICdsService
{
Task<PageResult<CdsListItemDto>> GetPagedAsync(int pageIndex, int pageSize, DateTime? date, long? deptId);
Task<CdsEditDto> GetEditAsync(long lsjlId);
Task SaveAsync(CdsSaveDto dto);
Task DeleteAsync(long lsjlId);
}
public class CdsService : ICdsService
{
private readonly IRepository<LsjlRecord> _lsjlRepo;
private readonly IRepository<CdsLsjl> _cdsLsjlRepo;
private readonly IRepository<CdsChetai> _cdsChetaiRepo;
private readonly IRepository<SysDept.Department> _deptRepo;
private readonly IRepository<CraftZhuangKou> _zkRepo;
public CdsService(
IRepository<LsjlRecord> lsjlRepo,
IRepository<CdsLsjl> cdsLsjlRepo,
IRepository<CdsChetai> cdsChetaiRepo,
IRepository<SysDept.Department> deptRepo,
IRepository<CraftZhuangKou> zkRepo)
{
_lsjlRepo = lsjlRepo;
_cdsLsjlRepo = cdsLsjlRepo;
_cdsChetaiRepo = cdsChetaiRepo;
_deptRepo = deptRepo;
_zkRepo = zkRepo;
}
public async Task<PageResult<CdsListItemDto>> GetPagedAsync(int pageIndex, int pageSize, DateTime? date, long? deptId)
{
var lsjls = await _lsjlRepo.GetAllAsync();
var q = lsjls.AsQueryable();
if (date.HasValue) q = q.Where(x => x.WorkDate.Date == date.Value.Date);
if (deptId.HasValue) q = q.Where(x => x.DeptId == deptId.Value);
var depts = await _deptRepo.GetAllAsync();
var deptDict = depts.ToDictionary(d => d.Id, d => d.Name);
var zks = await _zkRepo.GetAllAsync();
var zkDict = zks.ToDictionary(z => z.Id, z => z.ZhuangKouName);
var sums = (await _cdsLsjlRepo.GetAllAsync()).ToDictionary(x => x.LsjlId);
var total = q.LongCount();
var items = q.OrderByDescending(x => x.WorkDate).ThenBy(x => x.Jizu).ThenBy(x => x.Huishu)
.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
return new PageResult<CdsListItemDto>
{
Items = items.Select(x =>
{
sums.TryGetValue(x.Id, out var sum);
return new CdsListItemDto
{
LsjlId = x.Id,
WorkDate = x.WorkDate,
DeptId = x.DeptId,
DeptName = deptDict.GetValueOrDefault(x.DeptId, ""),
Jizu = x.Jizu,
Huishu = x.Huishu,
Guige = x.Guige,
ZhuangKouName = zkDict.GetValueOrDefault(x.GyzkId, ""),
Yanse = x.Yanse,
Isbanche = x.Isbanche,
IsbancheName = x.Isbanche ? "半车" : "整车",
Chetaishu = x.Chetaishu,
ChetaiStart = x.ChetaiStart,
ChetaiEnd = x.ChetaiEnd,
Hours = x.Hours,
Tongshu = x.Tongshu,
Meitongjianliang = x.Meitongjianliang,
YingchengNum = YingchengNum(x),
IsWeighed = sum != null,
Num = sum?.Num ?? 0,
Siliang = sum?.Siliang ?? 0,
Jianliang = sum?.Jianliang ?? 0,
Saozhe = sum?.Saozhe ?? 0,
Taichan = sum?.Taichan ?? 0
};
}).ToList(),
Total = total, PageIndex = pageIndex, PageSize = pageSize
};
}
public async Task<CdsEditDto> GetEditAsync(long lsjlId)
{
var lsjl = await _lsjlRepo.GetByIdAsync(lsjlId);
if (lsjl == null) throw new ServiceException("落丝记录不存在", 404);
var dept = await _deptRepo.GetByIdAsync(lsjl.DeptId);
var zk = await _zkRepo.GetByIdAsync(lsjl.GyzkId);
var records = await _cdsChetaiRepo.FindAsync(x => x.LsjlId == lsjlId);
List<CdsChetaiDto> chetais;
if (records.Count > 0)
{
chetais = records.OrderBy(r => r.Chetaihao).Select(ToDto).ToList();
}
else
{
// 首次称重:按车台范围初始化空行
chetais = new List<CdsChetaiDto>();
for (var i = lsjl.ChetaiStart; i <= lsjl.ChetaiEnd; i++)
chetais.Add(new CdsChetaiDto { Chetaihao = i });
}
return new CdsEditDto
{
LsjlId = lsjl.Id,
DeptName = dept?.Name ?? "",
Jizu = lsjl.Jizu,
Huishu = lsjl.Huishu,
ZhuangKouName = zk?.ZhuangKouName ?? "",
Yanse = lsjl.Yanse,
Isbanche = lsjl.Isbanche,
IsbancheName = lsjl.Isbanche ? "半车" : "整车",
Guige = lsjl.Guige,
ChetaiStart = lsjl.ChetaiStart,
ChetaiEnd = lsjl.ChetaiEnd,
Chetaishu = lsjl.Chetaishu,
YingchengNum = YingchengNum(lsjl),
Chetais = chetais
};
}
public async Task SaveAsync(CdsSaveDto dto)
{
if (dto.LsjlId == 0) throw new ServiceException("落丝记录无效");
var lsjl = await _lsjlRepo.GetByIdAsync(dto.LsjlId);
if (lsjl == null) throw new ServiceException("落丝记录不存在", 404);
if (dto.Chetais == null || dto.Chetais.Count == 0) throw new ServiceException("请先录入丝量数据");
var invalid = dto.Chetais.Where(i => i.Chetaihao < lsjl.ChetaiStart || i.Chetaihao > lsjl.ChetaiEnd).ToList();
if (invalid.Count > 0) throw new ServiceException($"车台号超出范围 [{lsjl.ChetaiStart}-{lsjl.ChetaiEnd}]");
// 车台明细:个数 = 丝量 > 0 的个数;小计丝量 = 4 个丝量之和
var items = dto.Chetais.Select(i =>
{
var t = new CdsChetaiDto
{
Chetaihao = i.Chetaihao,
Sl1 = Math.Max(0, i.Sl1), Sl2 = Math.Max(0, i.Sl2),
Sl3 = Math.Max(0, i.Sl3), Sl4 = Math.Max(0, i.Sl4)
};
CalChetai(t);
return t;
}).OrderBy(i => i.Chetaihao).ToList();
var num = items.Sum(x => x.Num);
if (num == 0) throw new ServiceException("请至少录入一个丝量值");
// 车台明细先删后建
var olds = await _cdsChetaiRepo.FindAsync(x => x.LsjlId == dto.LsjlId);
if (olds.Count > 0) await _cdsChetaiRepo.HardDeleteRangeAsync(olds);
foreach (var t in items)
{
await _cdsChetaiRepo.AddAsync(new CdsChetai
{
LsjlId = dto.LsjlId,
Chetaihao = t.Chetaihao,
Sl1 = t.Sl1, Sl2 = t.Sl2, Sl3 = t.Sl3, Sl4 = t.Sl4,
Num = t.Num, Siliang = t.Siliang
});
}
var siliang = items.Sum(x => x.Siliang);
// 汇总:茧量 / 缫折 / 实绩台产
var cdsLsjl = await _cdsLsjlRepo.FirstOrDefaultAsync(x => x.LsjlId == dto.LsjlId);
if (cdsLsjl == null)
{
cdsLsjl = new CdsLsjl { LsjlId = dto.LsjlId };
await _cdsLsjlRepo.AddAsync(cdsLsjl);
}
cdsLsjl.Num = num;
cdsLsjl.Siliang = siliang;
CalZhibiao(cdsLsjl, lsjl);
await _cdsLsjlRepo.UpdateAsync(cdsLsjl);
}
/// <summary>清除称重数据(汇总 + 车台明细物理删除,便于重新称重)</summary>
public async Task DeleteAsync(long lsjlId)
{
if (lsjlId == 0) throw new ServiceException("落丝记录无效");
var chetais = await _cdsChetaiRepo.FindAsync(x => x.LsjlId == lsjlId);
if (chetais.Count > 0) await _cdsChetaiRepo.HardDeleteRangeAsync(chetais);
var lsjlSum = await _cdsLsjlRepo.FirstOrDefaultAsync(x => x.LsjlId == lsjlId);
if (lsjlSum != null) await _cdsLsjlRepo.HardDeleteAsync(lsjlSum);
}
// ==================== 计算口径(对齐 F9 Cds_Lsjl_CalZhibiaoCommand) ====================
/// <summary>应秤数 = 车台数 × 4 ×(半车 0.5 / 整车 1)</summary>
private static int YingchengNum(LsjlRecord lsjl)
=> (int)Math.Round(lsjl.Chetaishu * 4 * (lsjl.Isbanche ? 0.5 : 1), MidpointRounding.AwayFromZero);
private static void CalChetai(CdsChetaiDto item)
{
item.Num = (item.Sl1 > 0 ? 1 : 0) + (item.Sl2 > 0 ? 1 : 0) + (item.Sl3 > 0 ? 1 : 0) + (item.Sl4 > 0 ? 1 : 0);
item.Siliang = item.Sl1 + item.Sl2 + item.Sl3 + item.Sl4;
}
/// <summary>
/// 茧量 = 落丝桶数 × 每桶茧量;缫折 = 茧量 / 丝量 × 100;实绩台产 = 丝量 / 落丝时长 / 车台数。
/// F9 口径:缫折 ≤ 100 视为异常数据,不落指标。
/// </summary>
private static void CalZhibiao(CdsLsjl target, LsjlRecord lsjl)
{
if (lsjl.Meitongjianliang == 0 || lsjl.Tongshu == 0)
{
target.Jianliang = 0; target.Saozhe = 0; target.Taichan = 0;
return;
}
if (target.Num <= 0 || target.Siliang <= 0 || lsjl.Hours <= 0 || lsjl.Chetaishu <= 0)
{
target.Jianliang = 0; target.Saozhe = 0; target.Taichan = 0;
return;
}
var jianliang = lsjl.Tongshu * lsjl.Meitongjianliang;
var saozhe = Math.Round((decimal)jianliang / target.Siliang * 100, 2, MidpointRounding.AwayFromZero);
var taichan = Math.Round((decimal)target.Siliang / (decimal)lsjl.Hours / lsjl.Chetaishu, 2, MidpointRounding.AwayFromZero);
if (saozhe > 100)
{
target.Jianliang = jianliang;
target.Saozhe = saozhe;
target.Taichan = taichan;
}
else
{
target.Jianliang = jianliang;
target.Saozhe = 0;
target.Taichan = 0;
}
}
private static CdsChetaiDto ToDto(CdsChetai r) => new()
{
Chetaihao = r.Chetaihao,
Sl1 = r.Sl1, Sl2 = r.Sl2, Sl3 = r.Sl3, Sl4 = r.Sl4,
Num = r.Num, Siliang = r.Siliang
};
}
@@ -0,0 +1,109 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Dtos.Prod;
using F9WebAiTest.Domain.Common;
using F9WebAiTest.Domain.Entities.Base;
using F9WebAiTest.Domain.Entities.Prod;
using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments;
namespace F9WebAiTest.Application.Services.Prod;
/// <summary>公量纤度:以落丝记录为主表,取纤度检验的平均纤度,由质检录入公量纤度(按日期 + 车间整日批量保存)</summary>
public interface IGlxdService
{
Task<List<GlxdListItemDto>> GetListAsync(DateTime? date, long? deptId);
Task SaveAsync(GlxdSaveDto dto);
}
public class GlxdService : IGlxdService
{
private readonly IRepository<LsjlRecord> _lsjlRepo;
private readonly IRepository<Glxd> _glxdRepo;
private readonly IRepository<XdjyLsjl> _xdjyLsjlRepo;
private readonly IRepository<SysDept.Department> _deptRepo;
private readonly IRepository<CraftZhuangKou> _zkRepo;
public GlxdService(
IRepository<LsjlRecord> lsjlRepo,
IRepository<Glxd> glxdRepo,
IRepository<XdjyLsjl> xdjyLsjlRepo,
IRepository<SysDept.Department> deptRepo,
IRepository<CraftZhuangKou> zkRepo)
{
_lsjlRepo = lsjlRepo;
_glxdRepo = glxdRepo;
_xdjyLsjlRepo = xdjyLsjlRepo;
_deptRepo = deptRepo;
_zkRepo = zkRepo;
}
public async Task<List<GlxdListItemDto>> GetListAsync(DateTime? date, long? deptId)
{
var lsjls = await _lsjlRepo.GetAllAsync();
var q = lsjls.AsQueryable();
if (date.HasValue) q = q.Where(x => x.WorkDate.Date == date.Value.Date);
if (deptId.HasValue) q = q.Where(x => x.DeptId == deptId.Value);
var items = q.OrderBy(x => x.DeptId).ThenBy(x => x.Jizu).ThenBy(x => x.Huishu).ToList();
if (items.Count == 0) return new List<GlxdListItemDto>();
var depts = await _deptRepo.GetAllAsync();
var deptDict = depts.ToDictionary(d => d.Id, d => d.Name);
var zks = await _zkRepo.GetAllAsync();
var zkDict = zks.ToDictionary(z => z.Id, z => z.ZhuangKouName);
var glxds = (await _glxdRepo.GetAllAsync()).ToDictionary(x => x.LsjlId);
var xdjySums = (await _xdjyLsjlRepo.GetAllAsync()).ToDictionary(x => x.LsjlId);
return items.Select(x =>
{
glxds.TryGetValue(x.Id, out var g);
xdjySums.TryGetValue(x.Id, out var xs);
return new GlxdListItemDto
{
LsjlId = x.Id,
WorkDate = x.WorkDate,
DeptId = x.DeptId,
DeptName = deptDict.GetValueOrDefault(x.DeptId, ""),
Jizu = x.Jizu,
Huishu = x.Huishu,
Guige = x.Guige,
ZhuangKouName = zkDict.GetValueOrDefault(x.GyzkId, ""),
Yanse = x.Yanse,
Isbanche = x.Isbanche,
IsbancheName = x.Isbanche ? "半车" : "整车",
Chetaishu = x.Chetaishu,
Avgxiandu = xs?.Avgxiandu ?? 0,
Gongliangxiandu = g?.Gongliangxiandu ?? 0
};
}).ToList();
}
public async Task SaveAsync(GlxdSaveDto dto)
{
if (dto.Items == null || dto.Items.Count == 0) throw new ServiceException("没有需要保存的公量纤度");
// 校验落丝记录存在,并按落丝记录 Id 去重
var dict = new Dictionary<long, decimal>();
foreach (var it in dto.Items)
{
if (it.LsjlId == 0) throw new ServiceException("存在无效的落丝记录");
var lsjl = await _lsjlRepo.GetByIdAsync(it.LsjlId);
if (lsjl == null) throw new ServiceException($"落丝记录 {it.LsjlId} 不存在", 404);
dict[it.LsjlId] = it.Gongliangxiandu;
}
foreach (var (lsjlId, value) in dict)
{
var e = await _glxdRepo.FirstOrDefaultAsync(x => x.LsjlId == lsjlId);
if (e == null)
{
e = new Glxd { LsjlId = lsjlId, Gongliangxiandu = value };
await _glxdRepo.AddAsync(e);
}
else
{
e.Gongliangxiandu = value;
await _glxdRepo.UpdateAsync(e);
}
}
}
}
@@ -0,0 +1,890 @@
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 ?? "",
};
}
@@ -96,6 +96,7 @@ public class LsjlService : ILsjlService
e.LuosiTime = d.LuosiTime; e.Yanse = d.Yanse; e.Huishu = d.Huishu;
e.ChetaiStart = d.ChetaiStart; e.ChetaiEnd = d.ChetaiEnd; e.Chetaishu = d.Chetaishu;
e.Isbanche = d.Isbanche; e.Hours = d.Hours; e.Guige = d.Guige; e.Chesu = d.Chesu;
e.Tongshu = d.Tongshu; e.Meitongjianliang = d.Meitongjianliang;
return e;
}
@@ -110,7 +111,8 @@ public class LsjlService : ILsjlService
LuosiTime = e.LuosiTime, Yanse = e.Yanse, Huishu = e.Huishu,
ChetaiStart = e.ChetaiStart, ChetaiEnd = e.ChetaiEnd, Chetaishu = e.Chetaishu,
Isbanche = e.Isbanche, IsbancheName = e.Isbanche ? "半车" : "整车",
Hours = e.Hours, Guige = e.Guige, Chesu = e.Chesu
Hours = e.Hours, Tongshu = e.Tongshu, Meitongjianliang = e.Meitongjianliang,
Guige = e.Guige, Chesu = e.Chesu
};
}
@@ -123,7 +125,8 @@ public class LsjlService : ILsjlService
LuosiTime = e.LuosiTime, Yanse = e.Yanse, Huishu = e.Huishu,
ChetaiStart = e.ChetaiStart, ChetaiEnd = e.ChetaiEnd, Chetaishu = e.Chetaishu,
Isbanche = e.Isbanche, IsbancheName = e.Isbanche ? "半车" : "整车",
Hours = e.Hours, Guige = e.Guige, Chesu = e.Chesu
Hours = e.Hours, Tongshu = e.Tongshu, Meitongjianliang = e.Meitongjianliang,
Guige = e.Guige, Chesu = e.Chesu
};
}
}
@@ -0,0 +1,188 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Dtos.Prod;
using F9WebAiTest.Domain.Common;
using F9WebAiTest.Domain.Entities.Base;
using F9WebAiTest.Domain.Entities.Prod;
using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments;
namespace F9WebAiTest.Application.Services.Prod;
/// <summary>丝片检查:按「日期 + 车间 + 工艺庄口 + 规格」记录前缫/后缫疵点片数</summary>
public interface ISpjcService
{
Task<PageResult<SpjcListItemDto>> GetPagedAsync(int pageIndex, int pageSize, DateTime? date, long? deptId, string like);
Task<SpjcSaveDto> GetForEditAsync(long id);
Task<long> SaveAsync(SpjcSaveDto dto);
Task DeleteAsync(long id);
}
public class SpjcService : ISpjcService
{
private readonly IRepository<Spjc> _spjcRepo;
private readonly IRepository<SysDept.Department> _deptRepo;
private readonly IRepository<CraftZhuangKou> _zkRepo;
public SpjcService(
IRepository<Spjc> spjcRepo,
IRepository<SysDept.Department> deptRepo,
IRepository<CraftZhuangKou> zkRepo)
{
_spjcRepo = spjcRepo;
_deptRepo = deptRepo;
_zkRepo = zkRepo;
}
public async Task<PageResult<SpjcListItemDto>> GetPagedAsync(int pageIndex, int pageSize, DateTime? date, long? deptId, string like)
{
var all = await _spjcRepo.GetAllAsync();
var q = all.AsQueryable();
if (date.HasValue) q = q.Where(x => x.Date.Date == date.Value.Date);
if (deptId.HasValue) q = q.Where(x => x.DeptId == deptId.Value);
var depts = await _deptRepo.GetAllAsync();
var deptDict = depts.ToDictionary(d => d.Id, d => d.Name);
var zks = await _zkRepo.GetAllAsync();
var zkDict = zks.ToDictionary(z => z.Id, z => z.ZhuangKouName);
if (!string.IsNullOrWhiteSpace(like))
{
var kwDeptIds = depts.Where(d => d.Name.Contains(like)).Select(d => d.Id).ToHashSet();
var kwZkIds = zks.Where(z => z.ZhuangKouName.Contains(like)).Select(z => z.Id).ToHashSet();
q = q.Where(x => kwDeptIds.Contains(x.DeptId) || kwZkIds.Contains(x.GyzkId) || x.Guige.Contains(like));
}
var total = q.LongCount();
var items = q.OrderByDescending(x => x.Date).ThenByDescending(x => x.Id)
.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
return new PageResult<SpjcListItemDto>
{
Items = items.Select(x => ToListDto(x, deptDict, zkDict)).ToList(),
Total = total, PageIndex = pageIndex, PageSize = pageSize
};
}
public async Task<SpjcSaveDto> GetForEditAsync(long id)
{
var e = await _spjcRepo.GetByIdAsync(id);
if (e == null) throw new ServiceException("丝片检查不存在或已删除", 404);
var dept = await _deptRepo.GetByIdAsync(e.DeptId);
var zk = await _zkRepo.GetByIdAsync(e.GyzkId);
return new SpjcSaveDto
{
Id = e.Id,
Date = e.Date,
DeptId = e.DeptId,
DeptName = dept?.Name ?? "",
GyzkId = e.GyzkId,
GyzkName = zk?.ZhuangKouName ?? "",
Guige = e.Guige,
Pianshu = e.Pianshu,
PianshuXuegong = e.PianshuXuegong,
QsJiahua = e.QsJiahua, QsYouwu = e.QsYouwu, QsHeidian = e.QsHeidian,
QsShangsi = e.QsShangsi, QsYetiao = e.QsYetiao, QsXiaosipian = e.QsXiaosipian, QsQita = e.QsQita,
HsYouwu = e.HsYouwu, HsShangsi = e.HsShangsi, HsShuangsi = e.HsShuangsi, HsZhisi = e.HsZhisi,
HsZhantiao = e.HsZhantiao, HsYingjiao = e.HsYingjiao, HsGuojiao = e.HsGuojiao,
HsShouchu = e.HsShouchu, HsWuran = e.HsWuran, HsLiansi = e.HsLiansi, HsWdmt = e.HsWdmt,
HsMaoluan = e.HsMaoluan, HsQita = e.HsQita,
Memo = e.Memo
};
}
public async Task<long> SaveAsync(SpjcSaveDto dto)
{
if (dto.DeptId == 0) throw new ServiceException("请选择车间");
if (dto.GyzkId == 0) throw new ServiceException("请选择工艺庄口");
if (string.IsNullOrWhiteSpace(dto.Guige)) throw new ServiceException("请选择规格");
var date = dto.Date.Date;
var guige = dto.Guige.Trim();
Spjc e;
if (dto.Id > 0)
{
e = await _spjcRepo.GetByIdAsync(dto.Id);
if (e == null) throw new ServiceException("丝片检查不存在或已删除", 404);
var dup = await _spjcRepo.FirstOrDefaultAsync(x => x.Id != dto.Id && x.Date.Date == date
&& x.DeptId == dto.DeptId && x.GyzkId == dto.GyzkId
&& x.Guige == guige);
if (dup != null) throw new ServiceException("已存在相同的丝片检查!");
}
else
{
var exists = await _spjcRepo.AnyAsync(x => x.Date.Date == date && x.DeptId == dto.DeptId
&& x.GyzkId == dto.GyzkId && x.Guige == guige);
if (exists) throw new ServiceException("已存在相同的丝片检查!");
e = new Spjc();
await _spjcRepo.AddAsync(e);
}
e.Date = date;
e.DeptId = dto.DeptId;
e.GyzkId = dto.GyzkId;
e.Guige = guige;
e.Pianshu = dto.Pianshu;
e.PianshuXuegong = dto.PianshuXuegong;
e.QsJiahua = dto.QsJiahua;
e.QsYouwu = dto.QsYouwu;
e.QsHeidian = dto.QsHeidian;
e.QsShangsi = dto.QsShangsi;
e.QsYetiao = dto.QsYetiao;
e.QsXiaosipian = dto.QsXiaosipian;
e.QsQita = dto.QsQita;
e.HsYouwu = dto.HsYouwu;
e.HsShangsi = dto.HsShangsi;
e.HsShuangsi = dto.HsShuangsi;
e.HsZhisi = dto.HsZhisi;
e.HsZhantiao = dto.HsZhantiao;
e.HsYingjiao = dto.HsYingjiao;
e.HsGuojiao = dto.HsGuojiao;
e.HsShouchu = dto.HsShouchu;
e.HsWuran = dto.HsWuran;
e.HsLiansi = dto.HsLiansi;
e.HsWdmt = dto.HsWdmt;
e.HsMaoluan = dto.HsMaoluan;
e.HsQita = dto.HsQita;
e.Memo = dto.Memo ?? "";
// 合计由服务端重算(与 F9 的 Qs_heji / Hs_heji 一致)
e.QsHeji = e.QsJiahua + e.QsYouwu + e.QsHeidian + e.QsShangsi + e.QsYetiao + e.QsXiaosipian + e.QsQita;
e.HsHeji = e.HsYouwu + e.HsShangsi + e.HsShuangsi + e.HsZhisi + e.HsZhantiao + e.HsYingjiao
+ e.HsGuojiao + e.HsShouchu + e.HsWuran + e.HsLiansi + e.HsWdmt + e.HsMaoluan + e.HsQita;
await _spjcRepo.UpdateAsync(e);
return e.Id;
}
public async Task DeleteAsync(long id)
{
var e = await _spjcRepo.GetByIdAsync(id);
if (e == null) throw new ServiceException("丝片检查不存在或已删除", 404);
// 业务键带唯一索引,物理删除避免「删除后重建」撞索引(与煮茧日报同一策略)
await _spjcRepo.HardDeleteAsync(e);
}
private static SpjcListItemDto ToListDto(Spjc x, Dictionary<long, string> deptDict, Dictionary<long, string> zkDict) => new()
{
Id = x.Id,
Date = x.Date,
DeptId = x.DeptId,
DeptName = deptDict.GetValueOrDefault(x.DeptId, ""),
GyzkId = x.GyzkId,
GyzkName = zkDict.GetValueOrDefault(x.GyzkId, ""),
Guige = x.Guige,
Pianshu = x.Pianshu,
PianshuXuegong = x.PianshuXuegong,
QsJiahua = x.QsJiahua, QsYouwu = x.QsYouwu, QsHeidian = x.QsHeidian,
QsShangsi = x.QsShangsi, QsYetiao = x.QsYetiao, QsXiaosipian = x.QsXiaosipian,
QsQita = x.QsQita, QsHeji = x.QsHeji,
HsYouwu = x.HsYouwu, HsShangsi = x.HsShangsi, HsShuangsi = x.HsShuangsi, HsZhisi = x.HsZhisi,
HsZhantiao = x.HsZhantiao, HsYingjiao = x.HsYingjiao, HsGuojiao = x.HsGuojiao,
HsShouchu = x.HsShouchu, HsWuran = x.HsWuran, HsLiansi = x.HsLiansi, HsWdmt = x.HsWdmt,
HsMaoluan = x.HsMaoluan, HsQita = x.HsQita, HsHeji = x.HsHeji,
Memo = x.Memo
};
}
@@ -0,0 +1,159 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Dtos.Prod;
using F9WebAiTest.Domain.Common;
using F9WebAiTest.Domain.Entities.Prod;
namespace F9WebAiTest.Application.Services.Prod;
/// <summary>丝片台帐:每日一条,记录浸丝 / 返丝 / 编丝 / 整理片数</summary>
public interface ISptzService
{
Task<PageResult<SptzListItemDto>> GetPagedAsync(int pageIndex, int pageSize, DateTime dateFrom, DateTime dateTo);
Task<SptzSaveDto> GetForEditAsync(long id);
Task<SptzDefaultsDto> GetDefaultsAsync(DateTime date);
Task<long> SaveAsync(SptzSaveDto dto);
Task DeleteAsync(long id);
}
public class SptzService : ISptzService
{
private readonly IRepository<Sptz> _sptzRepo;
private readonly IRepository<Spjc> _spjcRepo;
public SptzService(IRepository<Sptz> sptzRepo, IRepository<Spjc> spjcRepo)
{
_sptzRepo = sptzRepo;
_spjcRepo = spjcRepo;
}
public async Task<PageResult<SptzListItemDto>> GetPagedAsync(int pageIndex, int pageSize, DateTime dateFrom, DateTime dateTo)
{
var all = await _sptzRepo.GetAllAsync();
var from = dateFrom.Date;
var to = dateTo.Date;
var q = all.AsQueryable().Where(x => x.Date.Date >= from && x.Date.Date <= to);
var total = q.LongCount();
var items = q.OrderBy(x => x.Date).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
return new PageResult<SptzListItemDto>
{
Items = items.Select(ToListDto).ToList(),
Total = total, PageIndex = pageIndex, PageSize = pageSize
};
}
public async Task<SptzSaveDto> GetForEditAsync(long id)
{
var e = await _sptzRepo.GetByIdAsync(id);
if (e == null) throw new ServiceException("丝片台帐不存在或已删除", 404);
return new SptzSaveDto
{
Id = e.Id,
Date = e.Date,
DateLast = e.DateLast,
JsShangcun = e.JsShangcun,
JsXiasi = e.JsXiasi,
JsJinsi = e.JsJinsi,
FsFsps = e.FsFsps,
BsBsps = e.BsBsps,
ZlZhengpin = e.ZlZhengpin,
ZlTichu = e.ZlTichu,
Memo = e.Memo
};
}
/// <summary>
/// 新增默认值:上一本台账日期 + 其上存(= 上一日今存),并按当日「丝片检查」汇总出整理正品/剔除片数。
/// </summary>
public async Task<SptzDefaultsDto> GetDefaultsAsync(DateTime date)
{
var target = date.Date;
var all = await _sptzRepo.GetAllAsync();
var latest = all.Where(x => x.Date.Date < target).OrderByDescending(x => x.Date).FirstOrDefault();
var latestAny = all.OrderByDescending(x => x.Date).FirstOrDefault();
var spjcs = (await _spjcRepo.GetAllAsync()).Where(x => x.Date.Date == target).ToList();
var tichu = spjcs.Sum(x => x.QsJiahua + x.QsYouwu + x.QsHeidian + x.QsShangsi
+ x.QsYetiao + x.QsXiaosipian + x.QsQita);
var zhengpin = spjcs.Sum(x => x.Pianshu + x.PianshuXuegong) - tichu;
return new SptzDefaultsDto
{
Date = target,
DateLast = latest?.Date,
JsShangcun = latest?.JsJincun ?? 0,
LatestDate = latestAny?.Date,
ZlTichu = tichu,
ZlZhengpin = zhengpin,
HasSpjc = spjcs.Count > 0
};
}
public async Task<long> SaveAsync(SptzSaveDto dto)
{
var date = dto.Date.Date;
Sptz e;
if (dto.Id > 0)
{
e = await _sptzRepo.GetByIdAsync(dto.Id);
if (e == null) throw new ServiceException("丝片台帐不存在或已删除", 404);
var dup = await _sptzRepo.FirstOrDefaultAsync(x => x.Id != dto.Id && x.Date.Date == date);
if (dup != null) throw new ServiceException($"已存在 {date:yyyy-MM-dd} 的丝片台帐!");
}
else
{
var exists = await _sptzRepo.AnyAsync(x => x.Date.Date == date);
if (exists) throw new ServiceException($"已存在 {date:yyyy-MM-dd} 的丝片台帐!");
e = new Sptz();
await _sptzRepo.AddAsync(e);
}
e.Date = date;
e.DateLast = dto.DateLast == default ? date.AddDays(-1) : dto.DateLast.Date;
e.JsShangcun = dto.JsShangcun;
e.JsXiasi = dto.JsXiasi;
e.JsJinsi = dto.JsJinsi;
e.FsFsps = dto.FsFsps;
e.BsBsps = dto.BsBsps;
e.ZlZhengpin = dto.ZlZhengpin;
e.ZlTichu = dto.ZlTichu;
e.Memo = dto.Memo ?? "";
// 派生值由服务端重算(与 F9 一致):上日遗留/今日新增 = 上存 − 浸丝;今存 = 下丝 + 上日遗留/今日新增
e.JsSrylJrxz = e.JsShangcun - e.JsJinsi;
e.JsJincun = e.JsXiasi + e.JsSrylJrxz;
await _sptzRepo.UpdateAsync(e);
return e.Id;
}
public async Task DeleteAsync(long id)
{
var e = await _sptzRepo.GetByIdAsync(id);
if (e == null) throw new ServiceException("丝片台帐不存在或已删除", 404);
// 日期带唯一索引,物理删除避免「删除后重建」撞索引(与煮茧日报同一策略)
await _sptzRepo.HardDeleteAsync(e);
}
private static SptzListItemDto ToListDto(Sptz x) => new()
{
Id = x.Id,
Date = x.Date,
DateLast = x.DateLast,
JsShangcun = x.JsShangcun,
JsXiasi = x.JsXiasi,
JsJinsi = x.JsJinsi,
JsSrylJrxz = x.JsSrylJrxz,
JsJincun = x.JsJincun,
FsFsps = x.FsFsps,
BsBsps = x.BsBsps,
ZlZhengpin = x.ZlZhengpin,
ZlTichu = x.ZlTichu,
Memo = x.Memo
};
}
@@ -0,0 +1,496 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Dtos.Prod;
using F9WebAiTest.Domain.Common;
using F9WebAiTest.Domain.Entities.Base;
using F9WebAiTest.Domain.Entities.Prod;
namespace F9WebAiTest.Application.Services.Prod;
/// <summary>
/// 选剥茧(对应 F9 生产-选剥茧 XBJ):选茧日报(含盈亏/联统率)、选茧计划及其明细、选茧班组及职工。
/// </summary>
public interface IXjrbService
{
// ---- 选茧日报 ----
Task<PageResult<XjrbListDto>> GetPagedAsync(DateTime? start, DateTime? end, string like, int pageIndex, int pageSize);
Task<XjrbSaveDto> GetByIdAsync(long id);
Task<long> SaveAsync(XjrbSaveDto dto);
Task DeleteAsync(long id);
Task<XjrbYingkuiDto> CountYingkuiAsync(XjrbSaveDto dto);
// ---- 选茧计划 ----
Task<PageResult<XjrbPlanListDto>> GetPlanPagedAsync(DateTime? start, DateTime? end, string like, int pageIndex, int pageSize);
Task<XjrbPlanSaveDto> GetPlanByIdAsync(long id);
Task<long> SavePlanAsync(XjrbPlanSaveDto dto);
Task DeletePlanAsync(long id);
// ---- 选茧计划明细 ----
Task<List<XjrbItemDto>> GetPlanItemsAsync(long planId);
Task<long> SavePlanItemAsync(XjrbItemSaveDto dto);
Task DeletePlanItemAsync(long id);
Task<XjrbYingkuiDto> CountPlanYingkuiAsync(XjrbItemSaveDto dto);
// ---- 选茧班组 ----
Task<PageResult<XjpbBanzuDto>> GetBanzuPagedAsync(string banci, int? zu, int pageIndex, int pageSize);
Task<List<XjpbBanzuZhigongDto>> GetBanzuZhigongAsync(long banzuId);
Task SaveBanzuZhigongAsync(List<XjpbBanzuZhigongSaveDto> dtos);
}
public class XjrbService : IXjrbService
{
private readonly IRepository<XjrbRecord> _xjrbRepo;
private readonly IRepository<XjrbPlan> _planRepo;
private readonly IRepository<XjrbItem> _itemRepo;
private readonly IRepository<XjpbBanzu> _banzuRepo;
private readonly IRepository<XjpbBanzuZhigong> _zgRepo;
private readonly IRepository<CraftZhuangKou> _gyzkRepo;
public XjrbService(
IRepository<XjrbRecord> xjrbRepo, IRepository<XjrbPlan> planRepo, IRepository<XjrbItem> itemRepo,
IRepository<XjpbBanzu> banzuRepo, IRepository<XjpbBanzuZhigong> zgRepo, IRepository<CraftZhuangKou> gyzkRepo)
{
_xjrbRepo = xjrbRepo; _planRepo = planRepo; _itemRepo = itemRepo;
_banzuRepo = banzuRepo; _zgRepo = zgRepo; _gyzkRepo = gyzkRepo;
}
private static decimal Round2(decimal v) => Math.Round(v, 2);
private async Task<Dictionary<long, string>> GyzkDictAsync()
=> (await _gyzkRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.ZhuangKouName);
// ==================== 选茧日报 ====================
public async Task<PageResult<XjrbListDto>> GetPagedAsync(DateTime? start, DateTime? end, string like, int pageIndex, int pageSize)
{
var all = await _xjrbRepo.GetAllAsync();
var q = all.AsQueryable();
if (start.HasValue) q = q.Where(x => x.Date.Date >= start.Value.Date);
if (end.HasValue) q = q.Where(x => x.Date.Date <= end.Value.Date);
var gyzkDict = await GyzkDictAsync();
if (!string.IsNullOrWhiteSpace(like))
{
var key = like.Trim();
q = q.Where(x => x.GyzkId > 0 && gyzkDict.ContainsKey(x.GyzkId) && gyzkDict[x.GyzkId].Contains(key));
}
var total = q.LongCount();
var items = q.OrderByDescending(x => x.Date).ThenByDescending(x => x.Id)
.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
return new PageResult<XjrbListDto>
{
Items = items.Select(ToDto).ToList(),
Total = total, PageIndex = pageIndex, PageSize = pageSize
};
}
private static XjrbListDto ToDto(XjrbRecord x) => new()
{
Id = x.Id, Date = x.Date, GyzkId = x.GyzkId,
YlLrBaoshu = x.YlLrBaoshu, YlLrZhongliang = x.YlLrZhongliang,
LtjBaoshu = x.LtjBaoshu, LtjZhongliang = x.LtjZhongliang,
HwjjBaoshu = x.HwjjBaoshu, HwjjZhongliang = x.HwjjZhongliang,
LjjBaoshu = x.LjjBaoshu, LjjZhongliang = x.LjjZhongliang,
MjBaoshu = x.MjBaoshu, MjZhongliang = x.MjZhongliang,
SgjBaoshu = x.SgjBaoshu, SgjZhongliang = x.SgjZhongliang,
XjjBaoshu = x.XjjBaoshu, XjjZhongliang = x.XjjZhongliang,
JyBaoshu = x.JyBaoshu, JyZhongliang = x.JyZhongliang,
JhBaoshu = x.JhBaoshu, JhZhongliang = x.JhZhongliang,
TotalZhongliang = x.TotalZhongliang,
ThBaoshu = x.ThBaoshu, ThZhongliang = x.ThZhongliang,
LiantongRate = x.LiantongRate, Yingkui = x.Yingkui
};
public async Task<XjrbSaveDto> GetByIdAsync(long id)
{
var x = await _xjrbRepo.GetByIdAsync(id);
if (x == null) throw new ServiceException("选茧日报不存在", 404);
return new XjrbSaveDto
{
Id = x.Id, Date = x.Date, GyzkId = x.GyzkId,
YlLrBaoshu = x.YlLrBaoshu, YlLrZhongliang = x.YlLrZhongliang,
LtjBaoshu = x.LtjBaoshu, LtjZhongliang = x.LtjZhongliang,
HwjjBaoshu = x.HwjjBaoshu, HwjjZhongliang = x.HwjjZhongliang,
LjjBaoshu = x.LjjBaoshu, LjjZhongliang = x.LjjZhongliang,
MjBaoshu = x.MjBaoshu, MjZhongliang = x.MjZhongliang,
SgjBaoshu = x.SgjBaoshu, SgjZhongliang = x.SgjZhongliang,
XjjBaoshu = x.XjjBaoshu, XjjZhongliang = x.XjjZhongliang,
JyBaoshu = x.JyBaoshu, JyZhongliang = x.JyZhongliang,
JhBaoshu = x.JhBaoshu, JhZhongliang = x.JhZhongliang,
ThBaoshu = x.ThBaoshu, ThZhongliang = x.ThZhongliang,
LiantongRate = x.LiantongRate, Yingkui = x.Yingkui
};
}
public async Task<long> SaveAsync(XjrbSaveDto dto)
{
if (dto.Date == default) throw new ServiceException("请选择选茧日期");
if (dto.GyzkId <= 0) throw new ServiceException("请选择工艺庄口");
XjrbRecord entity;
var isNew = dto.Id == 0;
if (isNew) entity = new XjrbRecord();
else
{
entity = await _xjrbRepo.GetByIdAsync(dto.Id);
if (entity == null) throw new ServiceException("选茧日报不存在", 404);
}
entity.Date = dto.Date.Date;
entity.GyzkId = dto.GyzkId;
entity.YlLrBaoshu = dto.YlLrBaoshu;
entity.YlLrZhongliang = dto.YlLrZhongliang;
entity.LtjBaoshu = dto.LtjBaoshu; entity.LtjZhongliang = dto.LtjZhongliang;
entity.HwjjBaoshu = dto.HwjjBaoshu; entity.HwjjZhongliang = dto.HwjjZhongliang;
entity.LjjBaoshu = dto.LjjBaoshu; entity.LjjZhongliang = dto.LjjZhongliang;
entity.MjBaoshu = dto.MjBaoshu; entity.MjZhongliang = dto.MjZhongliang;
entity.SgjBaoshu = dto.SgjBaoshu; entity.SgjZhongliang = dto.SgjZhongliang;
entity.XjjBaoshu = dto.XjjBaoshu; entity.XjjZhongliang = dto.XjjZhongliang;
entity.JyBaoshu = dto.JyBaoshu; entity.JyZhongliang = dto.JyZhongliang;
entity.JhBaoshu = dto.JhBaoshu; entity.JhZhongliang = dto.JhZhongliang;
entity.ThBaoshu = dto.ThBaoshu; entity.ThZhongliang = dto.ThZhongliang;
entity.LiantongRate = dto.LiantongRate;
entity.Yingkui = dto.Yingkui;
// 合计重量 = 8 类茧重量之和(不含退回、不含领入)
entity.TotalZhongliang = Round2(entity.LtjZhongliang + entity.HwjjZhongliang + entity.LjjZhongliang
+ entity.MjZhongliang + entity.SgjZhongliang + entity.XjjZhongliang + entity.JyZhongliang + entity.JhZhongliang);
if (isNew) await _xjrbRepo.AddAsync(entity); else await _xjrbRepo.UpdateAsync(entity);
return entity.Id;
}
public async Task DeleteAsync(long id)
{
var x = await _xjrbRepo.GetByIdAsync(id);
if (x == null) throw new ServiceException("选茧日报不存在", 404);
await _xjrbRepo.HardDeleteAsync(x);
}
/// <summary>
/// 计算盈亏 / 联统率(对齐 F9 GetXjrbCountYingkuiQuery + GetXjrbLingruStartDateQuery)。
/// 起始日期:由近及远跳过「联统率 = 0(未结算)」的日报,直到碰到已结算的一条为止;
/// 累计区间 = [起始日期, 当前日期];累加当前录入值 + 区间内历史日报(排除自身)。
/// 联统率 = 累计联统茧重量 ÷ 累计干茧领入重量 × 100;盈亏 = 累计选出总重量 − 累计干茧领入重量。
/// </summary>
public async Task<XjrbYingkuiDto> CountYingkuiAsync(XjrbSaveDto dto)
{
if (dto.GyzkId <= 0) throw new ServiceException("请选择工艺庄口");
var date = dto.Date.Date;
var all = await _xjrbRepo.GetAllAsync();
var history = all.Where(x => x.GyzkId == dto.GyzkId && x.Date.Date < date && x.Id != dto.Id)
.OrderByDescending(x => x.Date).ToList();
// 步骤 1:往回跳过未结算(LiantongRate == 0)的记录,确定起始日期
var startDate = date;
foreach (var item in history)
{
if (item.LiantongRate != 0) break;
startDate = item.Date.Date;
}
// 步骤 2:累计区间(闭区间,排除自身)
var range = history.Where(x => x.Date.Date >= startDate && x.Date.Date <= date).ToList();
// 步骤 3:累加(初始值 = 当前表单输入值)
decimal ltj = dto.LtjZhongliang;
decimal yllr = dto.YlLrZhongliang;
decimal allW = dto.LtjZhongliang + dto.HwjjZhongliang + dto.LjjZhongliang + dto.MjZhongliang
+ dto.SgjZhongliang + dto.XjjZhongliang + dto.JyZhongliang + dto.JhZhongliang;
foreach (var item in range)
{
ltj += item.LtjZhongliang;
yllr += item.YlLrZhongliang;
allW += item.LtjZhongliang + item.HwjjZhongliang + item.LjjZhongliang + item.MjZhongliang
+ item.SgjZhongliang + item.XjjZhongliang + item.JyZhongliang + item.JhZhongliang;
}
// 步骤 4:公式
var rate = yllr != 0 ? Round2(ltj / yllr * 100) : 0;
var yingkui = Round2(allW - yllr);
return new XjrbYingkuiDto { LiantongRate = rate, Yingkui = yingkui, StartDate = startDate };
}
// ==================== 选茧计划 ====================
public async Task<PageResult<XjrbPlanListDto>> GetPlanPagedAsync(DateTime? start, DateTime? end, string like, int pageIndex, int pageSize)
{
var all = await _planRepo.GetAllAsync();
var q = all.AsQueryable();
if (start.HasValue) q = q.Where(x => x.Date.Date >= start.Value.Date);
if (end.HasValue) q = q.Where(x => x.Date.Date <= end.Value.Date);
var gyzkDict = await GyzkDictAsync();
if (!string.IsNullOrWhiteSpace(like))
{
var key = like.Trim();
q = q.Where(x => x.GyzkId > 0 && gyzkDict.ContainsKey(x.GyzkId) && gyzkDict[x.GyzkId].Contains(key));
}
var total = q.LongCount();
var items = q.OrderByDescending(x => x.Date).ThenByDescending(x => x.Id)
.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
return new PageResult<XjrbPlanListDto>
{
Items = items.Select(ToPlanDto).ToList(),
Total = total, PageIndex = pageIndex, PageSize = pageSize
};
}
private static XjrbPlanListDto ToPlanDto(XjrbPlan x) => new()
{
Id = x.Id, Date = x.Date, GyzkId = x.GyzkId, YllingliaodanId = x.YllingliaodanId,
YlBaoshu = x.YlBaoshu, YlZhongliang = x.YlZhongliang,
LtjBaoshu = x.LtjBaoshu, LtjZhongliang = x.LtjZhongliang,
HwjjBaoshu = x.HwjjBaoshu, HwjjZhongliang = x.HwjjZhongliang,
WjjBaoshu = x.WjjBaoshu, WjjZhongliang = x.WjjZhongliang,
SgjBaoshu = x.SgjBaoshu, SgjZhongliang = x.SgjZhongliang,
XjjBaoshu = x.XjjBaoshu, XjjZhongliang = x.XjjZhongliang,
JyBaoshu = x.JyBaoshu, JyZhongliang = x.JyZhongliang,
JhBaoshu = x.JhBaoshu, JhZhongliang = x.JhZhongliang,
TotalZhongliang = x.TotalZhongliang, LiantongRate = x.LiantongRate,
Yingkui = x.Yingkui, IsFinish = x.IsFinish
};
public async Task<XjrbPlanSaveDto> GetPlanByIdAsync(long id)
{
var x = await _planRepo.GetByIdAsync(id);
if (x == null) throw new ServiceException("选茧计划不存在", 404);
return new XjrbPlanSaveDto
{
Id = x.Id, Date = x.Date, GyzkId = x.GyzkId, YllingliaodanId = x.YllingliaodanId,
YlBaoshu = x.YlBaoshu, YlZhongliang = x.YlZhongliang,
LtjBaoshu = x.LtjBaoshu, LtjZhongliang = x.LtjZhongliang,
HwjjBaoshu = x.HwjjBaoshu, HwjjZhongliang = x.HwjjZhongliang,
WjjBaoshu = x.WjjBaoshu, WjjZhongliang = x.WjjZhongliang,
SgjBaoshu = x.SgjBaoshu, SgjZhongliang = x.SgjZhongliang,
XjjBaoshu = x.XjjBaoshu, XjjZhongliang = x.XjjZhongliang,
JyBaoshu = x.JyBaoshu, JyZhongliang = x.JyZhongliang,
JhBaoshu = x.JhBaoshu, JhZhongliang = x.JhZhongliang
};
}
public async Task<long> SavePlanAsync(XjrbPlanSaveDto dto)
{
if (dto.Date == default) throw new ServiceException("请选择日期");
if (dto.GyzkId <= 0) throw new ServiceException("请选择工艺庄口");
XjrbPlan entity;
var isNew = dto.Id == 0;
if (isNew) entity = new XjrbPlan();
else
{
entity = await _planRepo.GetByIdAsync(dto.Id);
if (entity == null) throw new ServiceException("选茧计划不存在", 404);
if (entity.IsFinish) throw new ServiceException("该计划已选茧完成,不能修改");
}
entity.Date = dto.Date.Date;
entity.GyzkId = dto.GyzkId;
entity.YllingliaodanId = dto.YllingliaodanId;
entity.YlBaoshu = dto.YlBaoshu; entity.YlZhongliang = dto.YlZhongliang;
entity.LtjBaoshu = dto.LtjBaoshu; entity.LtjZhongliang = dto.LtjZhongliang;
entity.HwjjBaoshu = dto.HwjjBaoshu; entity.HwjjZhongliang = dto.HwjjZhongliang;
entity.WjjBaoshu = dto.WjjBaoshu; entity.WjjZhongliang = dto.WjjZhongliang;
entity.SgjBaoshu = dto.SgjBaoshu; entity.SgjZhongliang = dto.SgjZhongliang;
entity.XjjBaoshu = dto.XjjBaoshu; entity.XjjZhongliang = dto.XjjZhongliang;
entity.JyBaoshu = dto.JyBaoshu; entity.JyZhongliang = dto.JyZhongliang;
entity.JhBaoshu = dto.JhBaoshu; entity.JhZhongliang = dto.JhZhongliang;
entity.TotalZhongliang = Round2(entity.LtjZhongliang + entity.HwjjZhongliang + entity.WjjZhongliang
+ entity.SgjZhongliang + entity.XjjZhongliang + entity.JyZhongliang + entity.JhZhongliang);
if (isNew) await _planRepo.AddAsync(entity); else await _planRepo.UpdateAsync(entity);
return entity.Id;
}
public async Task DeletePlanAsync(long id)
{
var x = await _planRepo.GetByIdAsync(id);
if (x == null) throw new ServiceException("选茧计划不存在", 404);
if (x.IsFinish) throw new ServiceException("该计划已选茧完成,不能删除");
var items = await _itemRepo.FindAsync(i => i.XjrbPlanId == id);
if (items.Count > 0) await _itemRepo.HardDeleteRangeAsync(items);
await _planRepo.HardDeleteAsync(x);
}
// ==================== 选茧计划明细 ====================
public async Task<List<XjrbItemDto>> GetPlanItemsAsync(long planId)
{
var items = await _itemRepo.FindAsync(x => x.XjrbPlanId == planId);
return items.OrderBy(x => x.Date).ThenBy(x => x.Id).Select(x => new XjrbItemDto
{
Id = x.Id, XjrbPlanId = x.XjrbPlanId, Date = x.Date,
LtjBaoshu = x.LtjBaoshu, LtjZhongliang = x.LtjZhongliang,
HwjjBaoshu = x.HwjjBaoshu, HwjjZhongliang = x.HwjjZhongliang,
WjjBaoshu = x.WjjBaoshu, WjjZhongliang = x.WjjZhongliang,
SgjBaoshu = x.SgjBaoshu, SgjZhongliang = x.SgjZhongliang,
XjjBaoshu = x.XjjBaoshu, XjjZhongliang = x.XjjZhongliang,
JyBaoshu = x.JyBaoshu, JyZhongliang = x.JyZhongliang,
JhBaoshu = x.JhBaoshu, JhZhongliang = x.JhZhongliang,
TotalZhongliang = x.TotalZhongliang, IsFinish = x.IsFinish
}).ToList();
}
public async Task<long> SavePlanItemAsync(XjrbItemSaveDto dto)
{
if (dto.XjrbPlanId <= 0) throw new ServiceException("计划无效");
if (dto.Date == default) throw new ServiceException("请选择选茧日期");
var plan = await _planRepo.GetByIdAsync(dto.XjrbPlanId);
if (plan == null) throw new ServiceException("选茧计划不存在", 404);
if (plan.IsFinish) throw new ServiceException("该计划已选茧完成,不能修改明细");
XjrbItem entity;
var isNew = dto.Id == 0;
if (isNew) entity = new XjrbItem { XjrbPlanId = dto.XjrbPlanId };
else
{
entity = await _itemRepo.GetByIdAsync(dto.Id);
if (entity == null) throw new ServiceException("选茧明细不存在", 404);
}
entity.Date = dto.Date.Date;
entity.LtjBaoshu = dto.LtjBaoshu; entity.LtjZhongliang = dto.LtjZhongliang;
entity.HwjjBaoshu = dto.HwjjBaoshu; entity.HwjjZhongliang = dto.HwjjZhongliang;
entity.WjjBaoshu = dto.WjjBaoshu; entity.WjjZhongliang = dto.WjjZhongliang;
entity.SgjBaoshu = dto.SgjBaoshu; entity.SgjZhongliang = dto.SgjZhongliang;
entity.XjjBaoshu = dto.XjjBaoshu; entity.XjjZhongliang = dto.XjjZhongliang;
entity.JyBaoshu = dto.JyBaoshu; entity.JyZhongliang = dto.JyZhongliang;
entity.JhBaoshu = dto.JhBaoshu; entity.JhZhongliang = dto.JhZhongliang;
entity.TotalZhongliang = Round2(entity.LtjZhongliang + entity.HwjjZhongliang + entity.WjjZhongliang
+ entity.SgjZhongliang + entity.XjjZhongliang + entity.JyZhongliang + entity.JhZhongliang);
entity.IsFinish = dto.IsFinish;
if (isNew) await _itemRepo.AddAsync(entity); else await _itemRepo.UpdateAsync(entity);
// 勾选「选茧完成」时把盈亏回写计划(对齐 F9 UpdateXjrbPlanYingkuiCommand)
if (dto.IsFinish)
{
var yk = await CountPlanYingkuiAsync(dto);
plan.TotalZhongliang = Round2(plan.TotalZhongliang + entity.TotalZhongliang);
plan.Yingkui = yk.Yingkui;
plan.LiantongRate = yk.LiantongRate;
plan.IsFinish = true;
await _planRepo.UpdateAsync(plan);
}
return entity.Id;
}
public async Task DeletePlanItemAsync(long id)
{
var x = await _itemRepo.GetByIdAsync(id);
if (x == null) throw new ServiceException("选茧明细不存在", 404);
var plan = await _planRepo.GetByIdAsync(x.XjrbPlanId);
if (plan != null && plan.IsFinish) throw new ServiceException("该计划已选茧完成,不能删除明细");
await _itemRepo.HardDeleteAsync(x);
}
/// <summary>
/// 计划维度盈亏(对齐 F9 GetXjrbPlanYingkuiQuery,口径与日报不同):
/// 联统率按「包数」:联统茧包数 ÷ 各类茧包数之和 × 100;盈亏 = 累计选出总重量 − 计划原料领入重量。
/// 注:老系统仓储里 wjjBaoshu / wjjZhongliang 源字段交叉写反(疑似 bug),此处已按正确字段聚合。
/// </summary>
public async Task<XjrbYingkuiDto> CountPlanYingkuiAsync(XjrbItemSaveDto dto)
{
var plan = await _planRepo.GetByIdAsync(dto.XjrbPlanId);
if (plan == null) throw new ServiceException("选茧计划不存在", 404);
var items = (await _itemRepo.FindAsync(x => x.XjrbPlanId == dto.XjrbPlanId && x.Id != dto.Id)).ToList();
decimal ltjZ = dto.LtjZhongliang, hwjjZ = dto.HwjjZhongliang, wjjZ = dto.WjjZhongliang,
sgjZ = dto.SgjZhongliang, xjjZ = dto.XjjZhongliang, jyZ = dto.JyZhongliang, jhZ = dto.JhZhongliang;
decimal ltjB = dto.LtjBaoshu, hwjjB = dto.HwjjBaoshu, wjjB = dto.WjjBaoshu,
sgjB = dto.SgjBaoshu, xjjB = dto.XjjBaoshu, jyB = dto.JyBaoshu, jhB = dto.JhBaoshu;
foreach (var it in items)
{
ltjZ += it.LtjZhongliang; hwjjZ += it.HwjjZhongliang; wjjZ += it.WjjZhongliang;
sgjZ += it.SgjZhongliang; xjjZ += it.XjjZhongliang; jyZ += it.JyZhongliang; jhZ += it.JhZhongliang;
ltjB += it.LtjBaoshu; hwjjB += it.HwjjBaoshu; wjjB += it.WjjBaoshu;
sgjB += it.SgjBaoshu; xjjB += it.XjjBaoshu; jyB += it.JyBaoshu; jhB += it.JhBaoshu;
}
var totalZhongliang = Round2(ltjZ + hwjjZ + wjjZ + sgjZ + xjjZ + jyZ + jhZ);
var baoshuSum = ltjB + hwjjB + wjjB + sgjB + xjjB + jyB + jhB;
var rate = baoshuSum != 0 ? Round2(ltjB / (decimal)baoshuSum * 100) : 0;
return new XjrbYingkuiDto { LiantongRate = rate, Yingkui = Round2(totalZhongliang - plan.YlZhongliang), StartDate = plan.Date };
}
// ==================== 选茧班组 ====================
public async Task<PageResult<XjpbBanzuDto>> GetBanzuPagedAsync(string banci, int? zu, int pageIndex, int pageSize)
{
var all = await _banzuRepo.GetAllAsync();
var q = all.AsQueryable();
if (!string.IsNullOrWhiteSpace(banci)) q = q.Where(x => x.Banci == banci);
if (zu.HasValue) q = q.Where(x => x.Zu == zu.Value);
var total = q.LongCount();
var items = q.OrderBy(x => x.Banci).ThenBy(x => x.Zu)
.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
return new PageResult<XjpbBanzuDto>
{
Items = items.Select(x => new XjpbBanzuDto { Id = x.Id, Banci = x.Banci, Zu = x.Zu }).ToList(),
Total = total, PageIndex = pageIndex, PageSize = pageSize
};
}
public async Task<List<XjpbBanzuZhigongDto>> GetBanzuZhigongAsync(long banzuId)
{
if (banzuId <= 0) return new List<XjpbBanzuZhigongDto>();
var list = await _zgRepo.FindAsync(x => x.BanzuId == banzuId);
return list.OrderBy(x => x.Sort).ThenBy(x => x.Id).Select(x => new XjpbBanzuZhigongDto
{
Id = x.Id, BanzuId = x.BanzuId, Banci = x.Banci, Zu = x.Zu,
YgId = x.YgId, YgName = x.YgName, YgPhone = x.YgPhone, Sort = x.Sort
}).ToList();
}
/// <summary>
/// 班组「编辑录入」批量保存(对齐 F9 XjpbBanzuZhigong_SaveCommand):
/// 按班次整体覆盖——先清该班次旧职工与班组,再把「已勾选且有组别」的职工写入,并重建班组。
/// </summary>
public async Task SaveBanzuZhigongAsync(List<XjpbBanzuZhigongSaveDto> dtos)
{
var list = dtos ?? new List<XjpbBanzuZhigongSaveDto>();
var banci = list.FirstOrDefault()?.Banci ?? "";
if (string.IsNullOrWhiteSpace(banci)) throw new ServiceException("班次无效");
// 清理该班次旧数据
var oldZg = await _zgRepo.FindAsync(x => x.Banci == banci);
if (oldZg.Count > 0) await _zgRepo.HardDeleteRangeAsync(oldZg);
var oldBanzu = await _banzuRepo.FindAsync(x => x.Banci == banci);
if (oldBanzu.Count > 0) await _banzuRepo.HardDeleteRangeAsync(oldBanzu);
var picked = list.Where(x => x.Selected && x.Zu > 0).ToList();
if (picked.Count == 0) return;
// 重建班组(班次 + 组 去重)
var banzuDict = new Dictionary<int, long>();
foreach (var zu in picked.Select(x => x.Zu).Distinct().OrderBy(x => x))
{
var banzu = new XjpbBanzu { Banci = banci, Zu = zu };
await _banzuRepo.AddAsync(banzu);
banzuDict[zu] = banzu.Id;
}
var sort = 0;
foreach (var p in picked)
{
await _zgRepo.AddAsync(new XjpbBanzuZhigong
{
BanzuId = banzuDict.TryGetValue(p.Zu, out var bid) ? bid : 0,
Banci = banci, Zu = p.Zu,
YgId = p.YgId, YgName = p.YgName ?? "", YgPhone = p.YgPhone ?? "",
Sort = p.Sort > 0 ? p.Sort : ++sort
});
}
}
}
@@ -0,0 +1,609 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Dtos.Prod;
using F9WebAiTest.Domain.Common;
using F9WebAiTest.Domain.Entities.Base;
using F9WebAiTest.Domain.Entities.Prod;
using F9WebAiTest.Domain.Entities.SYS.Registries;
using F9WebAiTest.Domain.Enums.SYS.Employees;
using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments;
namespace F9WebAiTest.Application.Services.Prod;
/// <summary>
/// 煮茧日报(生产-前缫):某日某车间某庄口某规格一条日报,含「机组」「班次」「机组班次」三层明细。
/// 对应 F9 Page_Zjrb(列表)、Page_Zjrb_EditV2(录入)、Zjrb_SaveCommand / Zjrb_DelCommand /
/// CalculateJizuSaosiHoursCommand(计算缫丝时长)。
///
/// 计算口径(与 F9 前端 Page_Zjrb_EditV2 一致,服务端保存时重算):
/// 机组.今缫桶数 = 甲班 + 乙班
/// 机组.今存桶数 = 上存 + 今缫 − 今落(今落为 0 且余量够一个标准桶时自动补「工艺落丝桶数」)
/// 班次.今缫重量 = 该班次机组今缫桶数 × 每桶茧量 ÷ 1000
/// 班次.领入/退回重量 = 包数 × 联统茧标重(标重未配置则保留录入值)
/// 班次.今缫包数 = 今缫重量 ÷ 联统茧标重(四舍五入取整)
/// 班次.今存 = 上存 + 领入 − 今缫 − 退回
/// 主表 = 甲班 + 乙班
/// </summary>
public interface IZjrbService
{
/// <summary>日报列表(账期起止 + 车间 + 关键字)</summary>
Task<PageResult<ZjrbReadDto_View>> GetPagedAsync(DateTime dateFrom, DateTime dateTo, long depId,
string like, int pageIndex, int pageSize);
/// <summary>某条日报的机组明细(列表页下半表)</summary>
Task<List<ZjrbJizuDto>> GetJizuListAsync(long zjrbId);
/// <summary>修改回填:主表 + 班次 + 机组 + 机组班次</summary>
Task<ZjrbSaveDto> GetForEditAsync(long id);
/// <summary>新增:按上一日结存带出上存、每桶茧量、工艺落丝桶数,并按机组车台生成机组行</summary>
Task<ZjrbSaveDto> BuildDefaultsAsync(DateTime date, long depId, long gyzkId, string guige);
/// <summary>保存(整条替换:主表 upsert + 三层明细先删后建)</summary>
Task<long> SaveAsync(ZjrbSaveDto dto);
/// <summary>删除(四张表级联物理删除)</summary>
Task DeleteAsync(long id);
/// <summary>计算缫丝时长(整日整车间重算:单班 9 小时 / 双班 18 小时,按茧量比例分配)</summary>
Task<int> CalculateJizuSaosiHoursAsync(DateTime date, long depId);
}
public class ZjrbService : IZjrbService
{
/// <summary>单班 / 双班的机组标准时长</summary>
private const decimal SingleBanciHours = 9m;
private const decimal TwoBanciHours = 18m;
/// <summary>联统茧标重的参数配置位置(与 F9 一致)</summary>
private const string RegistryCategory = @"生产\前缫\煮茧";
private const string RegistryKey = "联统茧标重";
private readonly IRepository<ZjrbRecord> _zjrbRepo;
private readonly IRepository<ZjrbJizu> _jizuRepo;
private readonly IRepository<ZjrbBanci> _banciRepo;
private readonly IRepository<ZjrbJizuBanci> _jizuBanciRepo;
private readonly IRepository<JizuChetai> _jzctRepo;
private readonly IRepository<ChejianBanci> _cjbcRepo;
private readonly IRepository<CraftZhuangKou> _gyzkRepo;
private readonly IRepository<Registry> _registryRepo;
private readonly IRepository<SysDept.Department> _deptRepo;
public ZjrbService(IRepository<ZjrbRecord> zjrbRepo, IRepository<ZjrbJizu> jizuRepo,
IRepository<ZjrbBanci> banciRepo, IRepository<ZjrbJizuBanci> jizuBanciRepo,
IRepository<JizuChetai> jzctRepo, IRepository<ChejianBanci> cjbcRepo,
IRepository<CraftZhuangKou> gyzkRepo, IRepository<Registry> registryRepo,
IRepository<SysDept.Department> deptRepo)
{
_zjrbRepo = zjrbRepo;
_jizuRepo = jizuRepo;
_banciRepo = banciRepo;
_jizuBanciRepo = jizuBanciRepo;
_jzctRepo = jzctRepo;
_cjbcRepo = cjbcRepo;
_gyzkRepo = gyzkRepo;
_registryRepo = registryRepo;
_deptRepo = deptRepo;
}
// ==================== 查询 ====================
public async Task<PageResult<ZjrbReadDto_View>> GetPagedAsync(DateTime dateFrom, DateTime dateTo, long depId,
string like, int pageIndex, int pageSize)
{
if (pageIndex <= 0) pageIndex = 1;
if (pageSize <= 0) pageSize = 20;
var from = dateFrom.Date;
var to = dateTo.Date;
if (to < from) (from, to) = (to, from);
var ctx = await BuildContextAsync();
var rows = await _zjrbRepo.FindAsync(x => x.Date >= from && x.Date <= to);
if (depId > 0) rows = rows.Where(x => x.DeptId == depId).ToList();
if (!string.IsNullOrWhiteSpace(like))
{
var kw = like.Trim();
rows = rows.Where(x =>
ctx.GyzkNames.GetValueOrDefault(x.GyzkId, "").Contains(kw, StringComparison.OrdinalIgnoreCase)
|| (x.Guige ?? "").Contains(kw, StringComparison.OrdinalIgnoreCase))
.ToList();
}
var total = rows.Count;
var pageRows = rows
.OrderByDescending(x => x.Date)
.ThenBy(x => ctx.DeptNames.GetValueOrDefault(x.DeptId, ""))
.ThenBy(x => x.GyzkId)
.ThenBy(x => x.Guige)
.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
var ids = pageRows.Select(x => x.Id).ToList();
var jizus = ids.Count == 0
? new List<ZjrbJizu>()
: await _jizuRepo.FindAsync(x => ids.Contains(x.ZjrbId));
var items = pageRows.Select(x =>
{
var jz = jizus.Where(j => j.ZjrbId == x.Id).ToList();
return new ZjrbReadDto_View
{
Id = x.Id,
Date = x.Date,
DepId = x.DeptId,
DepName = ctx.DeptNames.GetValueOrDefault(x.DeptId, ""),
GyzkId = x.GyzkId,
GyzkName = ctx.GyzkNames.GetValueOrDefault(x.GyzkId, ""),
Guige = x.Guige ?? "",
LastDate = x.LastDate,
StandardLuositongshu = x.StandardLuositongshu,
MeitongJianliang = x.MeitongJianliang,
BaoshuShangcun = x.BaoshuShangcun,
BaoshuLingru = x.BaoshuLingru,
BaoshuSaosi = x.BaoshuSaosi,
BaoshuTuihui = x.BaoshuTuihui,
BaoshuJincun = x.BaoshuJincun,
JianliangShangcun = x.JianliangShangcun,
JianliangLingru = x.JianliangLingru,
JianliangSaosi = x.JianliangSaosi,
JianliangTuihui = x.JianliangTuihui,
JianliangJincun = x.JianliangJincun,
TongshuShangcun = jz.Sum(j => j.TongshuShangcun),
TongshuJinsao = jz.Sum(j => j.TongshuJinsao),
TongshuJinluo = jz.Sum(j => j.TongshuJinluo),
TongshuJincun = jz.Sum(j => j.TongshuJincun),
JizuSaosiHours = jz.Sum(j => j.JizuSaosiHours)
};
}).ToList();
return new PageResult<ZjrbReadDto_View>
{
Items = items,
Total = total,
PageIndex = pageIndex,
PageSize = pageSize
};
}
public async Task<List<ZjrbJizuDto>> GetJizuListAsync(long zjrbId)
{
var jizus = (await _jizuRepo.FindAsync(x => x.ZjrbId == zjrbId)).OrderBy(x => x.Jizu).ToList();
var jb = await _jizuBanciRepo.FindAsync(x => x.ZjrbId == zjrbId);
return jizus.Select(j => ToJizuDto(j, jb)).ToList();
}
public async Task<ZjrbSaveDto> GetForEditAsync(long id)
{
var master = await _zjrbRepo.GetByIdAsync(id);
if (master == null) throw new ServiceException("煮茧日报不存在或已删除");
var ctx = await BuildContextAsync();
var banciRows = (await _banciRepo.FindAsync(x => x.ZjrbId == id)).OrderBy(x => x.Banci).ToList();
var jizuRows = (await _jizuRepo.FindAsync(x => x.ZjrbId == id)).OrderBy(x => x.Jizu).ToList();
var jbRows = await _jizuBanciRepo.FindAsync(x => x.ZjrbId == id);
var dto = new ZjrbSaveDto
{
Id = master.Id,
Date = master.Date,
DepId = master.DeptId,
DepName = ctx.DeptNames.GetValueOrDefault(master.DeptId, ""),
GyzkId = master.GyzkId,
GyzkName = ctx.GyzkNames.GetValueOrDefault(master.GyzkId, ""),
Guige = master.Guige ?? "",
LastDate = master.LastDate,
StandardLuositongshu = master.StandardLuositongshu,
MeitongJianliang = master.MeitongJianliang,
LiantongBiaozhong = await GetLiantongBiaozhongAsync(),
BaoshuShangcun = master.BaoshuShangcun,
BaoshuLingru = master.BaoshuLingru,
BaoshuSaosi = master.BaoshuSaosi,
BaoshuTuihui = master.BaoshuTuihui,
BaoshuJincun = master.BaoshuJincun,
JianliangShangcun = master.JianliangShangcun,
JianliangLingru = master.JianliangLingru,
JianliangSaosi = master.JianliangSaosi,
JianliangTuihui = master.JianliangTuihui,
JianliangJincun = master.JianliangJincun,
Bancis = banciRows.Select(ToBanciDto).ToList(),
Jizus = jizuRows.Select(j => ToJizuDto(j, jbRows)).ToList()
};
// 历史数据可能缺班次行(或车间班次配置调整过),按配置补齐甲乙班
var need = (await GetBanciKindsAsync(master.DeptId));
foreach (var b in need)
{
if (dto.Bancis.All(x => x.Banci != b))
dto.Bancis.Add(new ZjrbBanciDto { Banci = b, BanciName = b.ToString() });
}
dto.Bancis = dto.Bancis.OrderBy(x => x.Banci).ToList();
return dto;
}
public async Task<ZjrbSaveDto> BuildDefaultsAsync(DateTime date, long depId, long gyzkId, string guige)
{
if (depId <= 0) throw new ServiceException("请选择车间");
var ctx = await BuildContextAsync();
var d = date.Date;
var g = (guige ?? string.Empty).Trim();
// 上一日:同车间 + 同庄口(+ 同规格)的最近一条日报
var candidates = await _zjrbRepo.FindAsync(x => x.Date < d && x.DeptId == depId && x.GyzkId == gyzkId);
var last = candidates
.Where(x => string.IsNullOrEmpty(g) || (x.Guige ?? "") == g)
.OrderByDescending(x => x.Date)
.FirstOrDefault();
var dto = new ZjrbSaveDto
{
Date = d,
DepId = depId,
DepName = ctx.DeptNames.GetValueOrDefault(depId, ""),
GyzkId = gyzkId,
GyzkName = ctx.GyzkNames.GetValueOrDefault(gyzkId, ""),
Guige = g,
LiantongBiaozhong = await GetLiantongBiaozhongAsync()
};
List<ZjrbBanci> lastBancis = new();
List<ZjrbJizu> lastJizus = new();
if (last != null)
{
dto.LastDate = last.Date;
dto.StandardLuositongshu = last.StandardLuositongshu;
dto.MeitongJianliang = last.MeitongJianliang;
lastBancis = await _banciRepo.FindAsync(x => x.ZjrbId == last.Id);
lastJizus = await _jizuRepo.FindAsync(x => x.ZjrbId == last.Id);
}
// 班次行:按车间班次配置;未配置则固定甲乙两班
foreach (var b in await GetBanciKindsAsync(depId))
{
var prev = lastBancis.FirstOrDefault(x => x.Banci == b);
dto.Bancis.Add(new ZjrbBanciDto
{
Banci = b,
BanciName = b.ToString(),
BaoshuShangcun = prev?.BaoshuJincun ?? 0,
JianliangShangcun = prev?.JianliangJincun ?? 0
});
}
// 机组行:按机组车台配置;上存桶数取上一日该机组今存桶数
var jizus = (await _jzctRepo.FindAsync(x => x.DepartmentId == depId))
.Select(x => x.Jizu).Distinct().OrderBy(x => x).ToList();
foreach (var jz in jizus)
{
var prev = lastJizus.FirstOrDefault(x => x.Jizu == jz);
dto.Jizus.Add(new ZjrbJizuDto
{
Jizu = jz,
TongshuShangcun = prev?.TongshuJincun ?? 0
});
}
return dto;
}
// ==================== 保存 / 删除 ====================
public async Task<long> SaveAsync(ZjrbSaveDto dto)
{
if (dto == null) throw new ServiceException("参数不能为空");
if (dto.Date == default) throw new ServiceException("请选择日期");
if (dto.DepId <= 0) throw new ServiceException("请选择车间");
if (dto.GyzkId <= 0) throw new ServiceException("请选择工艺庄口");
if (dto.MeitongJianliang <= 0) throw new ServiceException("每桶茧量必须大于 0");
if (dto.StandardLuositongshu <= 0) throw new ServiceException("工艺落丝桶数必须大于 0");
if (dto.Bancis == null || dto.Bancis.Count == 0) throw new ServiceException("请至少保留一个班次行");
var d = dto.Date.Date;
var guige = (dto.Guige ?? string.Empty).Trim();
// 唯一性:同 日期 + 车间 + 庄口 + 规格
var exists = await _zjrbRepo.AnyAsync(x =>
x.Id != dto.Id && x.Date == d && x.DeptId == dto.DepId && x.GyzkId == dto.GyzkId && x.Guige == guige);
if (exists) throw new ServiceException("已存在相同的煮茧日报!");
Normalize(dto, guige);
long id = dto.Id;
if (id > 0)
{
var master = await _zjrbRepo.GetByIdAsync(id);
if (master == null) throw new ServiceException("煮茧日报不存在或已删除");
CopyMaster(dto, master);
await _zjrbRepo.UpdateAsync(master);
await DeleteChildrenAsync(id);
}
else
{
var master = new ZjrbRecord { LastDate = dto.LastDate };
CopyMaster(dto, master);
await _zjrbRepo.AddAsync(master);
id = master.Id;
}
// 班次
foreach (var b in dto.Bancis)
{
await _banciRepo.AddAsync(new ZjrbBanci
{
ZjrbId = id,
Banci = b.Banci,
BaoshuShangcun = b.BaoshuShangcun,
BaoshuLingru = b.BaoshuLingru,
BaoshuSaosi = b.BaoshuSaosi,
BaoshuTuihui = b.BaoshuTuihui,
BaoshuJincun = b.BaoshuJincun,
JianliangShangcun = b.JianliangShangcun,
JianliangLingru = b.JianliangLingru,
JianliangSaosi = b.JianliangSaosi,
JianliangTuihui = b.JianliangTuihui,
JianliangJincun = b.JianliangJincun
});
}
// 机组 + 机组班次(机组班次挂在刚生成的机组 Id 上)
foreach (var j in dto.Jizus)
{
var jizu = await _jizuRepo.AddAsync(new ZjrbJizu
{
ZjrbId = id,
Jizu = j.Jizu,
JizuSaosiHours = j.JizuSaosiHours,
TongshuShangcun = j.TongshuShangcun,
TongshuJinsao = j.TongshuJinsaoJiaban + j.TongshuJinsaoYiban,
TongshuJinluo = j.TongshuJinluo,
TongshuJincun = j.TongshuJincun
});
foreach (var b in dto.Bancis)
{
await _jizuBanciRepo.AddAsync(new ZjrbJizuBanci
{
ZjrbId = id,
ZjrbJizuId = jizu.Id,
Jizu = j.Jizu,
Banci = b.Banci,
TongshuJinsao = b.Banci == Prod_Banci_Enum.甲 ? j.TongshuJinsaoJiaban : j.TongshuJinsaoYiban
});
}
}
await _zjrbRepo.SaveChangesAsync();
return id;
}
public async Task DeleteAsync(long id)
{
var master = await _zjrbRepo.GetByIdAsync(id);
if (master == null) throw new ServiceException("煮茧日报不存在或已删除");
await DeleteChildrenAsync(id);
await _zjrbRepo.HardDeleteAsync(master);
}
private async Task DeleteChildrenAsync(long zjrbId)
{
var jizuRows = await _jizuRepo.FindAsync(x => x.ZjrbId == zjrbId);
if (jizuRows.Count > 0) await _jizuRepo.HardDeleteRangeAsync(jizuRows);
var banciRows = await _banciRepo.FindAsync(x => x.ZjrbId == zjrbId);
if (banciRows.Count > 0) await _banciRepo.HardDeleteRangeAsync(banciRows);
var jbRows = await _jizuBanciRepo.FindAsync(x => x.ZjrbId == zjrbId);
if (jbRows.Count > 0) await _jizuBanciRepo.HardDeleteRangeAsync(jbRows);
}
// ==================== 计算缫丝时长 ====================
public async Task<int> CalculateJizuSaosiHoursAsync(DateTime date, long depId)
{
if (depId <= 0) throw new ServiceException("请选择车间");
var d = date.Date;
var reports = await _zjrbRepo.FindAsync(x => x.Date == d && x.DeptId == depId);
if (reports.Count == 0) throw new ServiceException("该日该车间没有煮茧日报,无法计算缫丝时长");
var reportMap = reports.ToDictionary(x => x.Id);
var ids = reportMap.Keys.ToList();
var jizus = await _jizuRepo.FindAsync(x => ids.Contains(x.ZjrbId));
if (jizus.Count == 0) throw new ServiceException("该日该车间没有机组明细,无法计算缫丝时长");
var jbRows = await _jizuBanciRepo.FindAsync(x => ids.Contains(x.ZjrbId));
// 双班判据:该机组「今缫桶数 > 0」的班次个数为 2 → 18 小时,否则 9 小时
var hasTwoBanci = jizus.Select(x => x.Jizu).Distinct().ToDictionary(
jz => jz,
jz => jbRows.Where(x => x.Jizu == jz && x.TongshuJinsao > 0)
.Select(x => x.Banci).Distinct().Count() == 2);
// 机组总茧量 = Σ(每桶茧量 × 机组今缫桶数)
var totalJianliang = jizus.GroupBy(x => x.Jizu).ToDictionary(
g => g.Key,
g => g.Sum(x => (decimal)reportMap[x.ZjrbId].MeitongJianliang * x.TongshuJinsao));
var changed = 0;
var groups = jizus.GroupBy(x => new
{
reportMap[x.ZjrbId].GyzkId,
Guige = reportMap[x.ZjrbId].Guige ?? "",
x.Jizu
});
foreach (var group in groups)
{
var jz = group.Key.Jizu;
var totalHours = hasTwoBanci.GetValueOrDefault(jz) ? TwoBanciHours : SingleBanciHours;
var jizuTotal = totalJianliang.GetValueOrDefault(jz);
var current = group.Sum(x => (decimal)reportMap[x.ZjrbId].MeitongJianliang * x.TongshuJinsao);
var ratio = jizuTotal > 0 ? current / jizuTotal : 0m;
var newHours = Math.Round(ratio * totalHours, 1, MidpointRounding.AwayFromZero);
foreach (var item in group)
{
if (item.JizuSaosiHours == newHours) continue;
item.JizuSaosiHours = newHours;
await _jizuRepo.UpdateAsync(item);
changed++;
}
}
return changed;
}
// ==================== 内部:口径重算 / 上下文 ====================
/// <summary>按 F9 编辑页口径重算派生字段(机组今存、班次重量与今存、主表合计)</summary>
private static void Normalize(ZjrbSaveDto dto, string guige)
{
dto.Guige = guige;
foreach (var j in dto.Jizus)
{
if (j.JizuSaosiHours < 0) throw new ServiceException($"机组{j.Jizu}的缫丝时长不能为负数");
if (j.JizuSaosiHours > 20) throw new ServiceException($"机组{j.Jizu}缫丝时长不能大于 20");
j.TongshuJinsao = j.TongshuJinsaoJiaban + j.TongshuJinsaoYiban;
if (j.TongshuJinsao > 0 && j.JizuSaosiHours <= 0)
throw new ServiceException($"机组{j.Jizu}今缫桶数大于 0 时,请输入缫丝时长");
var pre = j.TongshuShangcun + j.TongshuJinsao - j.TongshuJinluo;
if (j.TongshuJinluo == 0 && dto.StandardLuositongshu > 0 && pre >= dto.StandardLuositongshu)
{
j.TongshuJinluo = dto.StandardLuositongshu;
pre -= dto.StandardLuositongshu;
}
j.TongshuJincun = pre;
}
foreach (var b in dto.Bancis)
{
if (b.BaoshuShangcun > 0 && b.JianliangShangcun <= 0)
throw new ServiceException($"{b.Banci}班上存包数大于 0 时,请填写上存重量");
var tongshu = dto.Jizus.Sum(j => b.Banci == Prod_Banci_Enum.甲
? j.TongshuJinsaoJiaban
: j.TongshuJinsaoYiban);
b.JianliangSaosi = Math.Round(tongshu * (decimal)dto.MeitongJianliang / 1000m, 1,
MidpointRounding.AwayFromZero);
if (dto.LiantongBiaozhong > 0)
{
b.JianliangLingru = Math.Round(b.BaoshuLingru * dto.LiantongBiaozhong, 1,
MidpointRounding.AwayFromZero);
b.JianliangTuihui = Math.Round(b.BaoshuTuihui * dto.LiantongBiaozhong, 1,
MidpointRounding.AwayFromZero);
b.BaoshuSaosi = (int)Math.Round(b.JianliangSaosi / dto.LiantongBiaozhong, 0,
MidpointRounding.AwayFromZero);
}
b.JianliangJincun = b.JianliangShangcun + b.JianliangLingru - b.JianliangSaosi - b.JianliangTuihui;
b.BaoshuJincun = b.BaoshuShangcun + b.BaoshuLingru - b.BaoshuSaosi - b.BaoshuTuihui;
}
// 主表 = 甲班 + 乙班
dto.BaoshuShangcun = dto.Bancis.Sum(x => x.BaoshuShangcun);
dto.BaoshuLingru = dto.Bancis.Sum(x => x.BaoshuLingru);
dto.BaoshuSaosi = dto.Bancis.Sum(x => x.BaoshuSaosi);
dto.BaoshuTuihui = dto.Bancis.Sum(x => x.BaoshuTuihui);
dto.BaoshuJincun = dto.Bancis.Sum(x => x.BaoshuJincun);
dto.JianliangShangcun = dto.Bancis.Sum(x => x.JianliangShangcun);
dto.JianliangLingru = dto.Bancis.Sum(x => x.JianliangLingru);
dto.JianliangSaosi = dto.Bancis.Sum(x => x.JianliangSaosi);
dto.JianliangTuihui = dto.Bancis.Sum(x => x.JianliangTuihui);
dto.JianliangJincun = dto.Bancis.Sum(x => x.JianliangJincun);
}
private static void CopyMaster(ZjrbSaveDto dto, ZjrbRecord master)
{
master.Date = dto.Date.Date;
master.DeptId = dto.DepId;
master.GyzkId = dto.GyzkId;
master.Guige = dto.Guige ?? "";
master.LastDate = dto.LastDate;
master.StandardLuositongshu = dto.StandardLuositongshu;
master.MeitongJianliang = dto.MeitongJianliang;
master.BaoshuShangcun = dto.BaoshuShangcun;
master.BaoshuLingru = dto.BaoshuLingru;
master.BaoshuSaosi = dto.BaoshuSaosi;
master.BaoshuTuihui = dto.BaoshuTuihui;
master.BaoshuJincun = dto.BaoshuJincun;
master.JianliangShangcun = dto.JianliangShangcun;
master.JianliangLingru = dto.JianliangLingru;
master.JianliangSaosi = dto.JianliangSaosi;
master.JianliangTuihui = dto.JianliangTuihui;
master.JianliangJincun = dto.JianliangJincun;
}
private static ZjrbBanciDto ToBanciDto(ZjrbBanci x) => new()
{
Id = x.Id,
ZjrbId = x.ZjrbId,
Banci = x.Banci,
BanciName = x.Banci.ToString(),
BaoshuShangcun = x.BaoshuShangcun,
BaoshuLingru = x.BaoshuLingru,
BaoshuSaosi = x.BaoshuSaosi,
BaoshuTuihui = x.BaoshuTuihui,
BaoshuJincun = x.BaoshuJincun,
JianliangShangcun = x.JianliangShangcun,
JianliangLingru = x.JianliangLingru,
JianliangSaosi = x.JianliangSaosi,
JianliangTuihui = x.JianliangTuihui,
JianliangJincun = x.JianliangJincun
};
private static ZjrbJizuDto ToJizuDto(ZjrbJizu x, List<ZjrbJizuBanci> jbRows) => new()
{
Id = x.Id,
ZjrbId = x.ZjrbId,
Jizu = x.Jizu,
JizuSaosiHours = x.JizuSaosiHours,
TongshuShangcun = x.TongshuShangcun,
TongshuJinsaoJiaban = jbRows.FirstOrDefault(b => b.ZjrbJizuId == x.Id && b.Banci == Prod_Banci_Enum.甲)
?.TongshuJinsao ?? 0,
TongshuJinsaoYiban = jbRows.FirstOrDefault(b => b.ZjrbJizuId == x.Id && b.Banci == Prod_Banci_Enum.乙)
?.TongshuJinsao ?? 0,
TongshuJinsao = x.TongshuJinsao,
TongshuJinluo = x.TongshuJinluo,
TongshuJincun = x.TongshuJincun
};
/// <summary>车间班次配置;未配置时固定甲乙两班</summary>
private async Task<List<Prod_Banci_Enum>> GetBanciKindsAsync(long depId)
{
var cfgs = await _cjbcRepo.FindAsync(x => x.DeptId == depId);
if (cfgs.Count == 0) return new List<Prod_Banci_Enum> { Prod_Banci_Enum.甲, Prod_Banci_Enum.乙 };
return cfgs.Select(x => x.Banci).Distinct().OrderBy(x => x).ToList();
}
private async Task<decimal> GetLiantongBiaozhongAsync()
{
var row = await _registryRepo.FirstOrDefaultAsync(x =>
x.Category == RegistryCategory && x.KeyName == RegistryKey);
if (row == null || string.IsNullOrWhiteSpace(row.Value)) return 0m;
return decimal.TryParse(row.Value, out var v) ? v : 0m;
}
private sealed class Ctx
{
public Dictionary<long, string> DeptNames { get; init; } = new();
public Dictionary<long, string> GyzkNames { get; init; } = new();
}
private async Task<Ctx> BuildContextAsync()
{
var depts = await _deptRepo.GetAllAsync();
var gyzks = await _gyzkRepo.GetAllAsync();
return new Ctx
{
DeptNames = depts.ToDictionary(d => d.Id, d => d.Name),
GyzkNames = gyzks.ToDictionary(z => z.Id, z => z.ProductionDisplayName is { Length: > 0 } n
? n
: z.ZhuangKouName)
};
}
}