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

- 新增 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
};
}