Files
f9-web-ai-test/backend/F9WebAiTest.Application/Services/Prod/KtjlService.cs
T
yanpeng200212 69c4052a64 新增生产-前缫看台记录,并导入车头工/挡车工看台记录历史数据
功能:
- 后端新增 KtjlCtgs / KtjlDcgs 两张表、KtjlService 与 api/prod/ktjl 接口
  (按日期+车间+班次查询、整组先删后建保存、整组删除、按看台排班带出默认记录、工种下拉)
- 挡车数按 F9 口径自动重算(与他人接壤的边界车位各扣 0.5),机组由车位反查机组车台
- 前端新增 /prod/qianxian/ktjl 看台记录页:挡车工/车头工双页签、删除、录入(新增)、录入(指定日期)
- 录入弹窗校验对齐 F9:个人时长不超标准、挡车数/看台时长匹配、车头工每机组≥2 人且时长为标准时长整数倍
- 生产-前缫菜单新增「看台记录」,权限 prod:ktjl:list

数据导入:
- 新增内嵌资源 biz_prod_qs_ktjl_ctg.csv / biz_prod_qs_ktjl_dcg.csv(约 5 万行,采用紧凑 CSV)
- 剔除软删除行、日期异常行、部门无法映射与职工不存在的行;日期由 Excel 序列号转换
- 车头工 12693 行、挡车工 38048 行入库,日期范围 2025-12-16 ~ 2026-09-11
- 表内已有数据则整体跳过,分批 1000 行插入

其他:同步工作进度-13 文档与开发数据库快照
2026-10-22 11:16:29 +08:00

379 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Dtos.Prod;
using F9WebAiTest.Domain.Common;
using F9WebAiTest.Domain.Entities.Base;
using F9WebAiTest.Domain.Entities.Prod;
using F9WebAiTest.Domain.Enums.SYS.Employees;
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>
/// 看台记录(生产-前缫):按「日期 + 车间 + 班次」维护车头工 / 挡车工的实际看台情况。
/// 对应 F9 Page_Ktjl(列表)、Page_Ktjl_Dcg_Edit / Page_Ktjl_Ctg_Edit(录入弹窗)与
/// Ktjl_Dcg_SaveCommand / Ktjl_Ctg_SaveCommand(整组先删后建)。
/// </summary>
public interface IKtjlService
{
// 查询
Task<List<KtjlctgReadDto_View>> GetCtgAsync(DateTime date, long depId, Prod_Banci_Enum banci, string like);
Task<List<KtjldcgReadDto_View>> GetDcgAsync(DateTime date, long depId, Prod_Banci_Enum banci, string like);
// 保存 / 删除(整组)
Task<int> SaveCtgAsync(KtjlctgBatchDto dto);
Task<int> SaveDcgAsync(KtjldcgBatchDto dto);
Task DeleteCtgAsync(DateTime date, long depId, Prod_Banci_Enum banci);
Task DeleteDcgAsync(DateTime date, long depId, Prod_Banci_Enum banci);
/// <summary>录入(新增):按看台排班 + 该车间工种职工,带出默认记录</summary>
Task<List<KtjlctgReadDto_View>> BuildCtgDefaultsAsync(DateTime date, long depId, Prod_Banci_Enum banci);
Task<List<KtjldcgReadDto_View>> BuildDcgDefaultsAsync(DateTime date, long depId, Prod_Banci_Enum banci);
/// <summary>工种下拉(用于筛选/展示)</summary>
Task<List<ProdWorktypeOptionDto>> GetWorktypeOptionsAsync();
}
public class ProdWorktypeOptionDto
{
public long Id { get; set; }
public string Name { get; set; } = "";
}
public class KtjlService : IKtjlService
{
private const decimal DefaultHours = 9m;
private readonly IRepository<KtjlCtg> _ctgRepo;
private readonly IRepository<KtjlDcg> _dcgRepo;
private readonly IRepository<KtpbCtg> _ktpbCtgRepo;
private readonly IRepository<KtpbDcg> _ktpbDcgRepo;
private readonly IRepository<JizuChetai> _jzctRepo;
private readonly IRepository<SysDept.Department> _deptRepo;
private readonly IRepository<SysEmp.Employee> _empRepo;
private readonly IRepository<SysWt.WorkType> _wtRepo;
public KtjlService(IRepository<KtjlCtg> ctgRepo, IRepository<KtjlDcg> dcgRepo,
IRepository<KtpbCtg> ktpbCtgRepo, IRepository<KtpbDcg> ktpbDcgRepo,
IRepository<JizuChetai> jzctRepo, IRepository<SysDept.Department> deptRepo,
IRepository<SysEmp.Employee> empRepo, IRepository<SysWt.WorkType> wtRepo)
{
_ctgRepo = ctgRepo;
_dcgRepo = dcgRepo;
_ktpbCtgRepo = ktpbCtgRepo;
_ktpbDcgRepo = ktpbDcgRepo;
_jzctRepo = jzctRepo;
_deptRepo = deptRepo;
_empRepo = empRepo;
_wtRepo = wtRepo;
}
// ==================== 查询 ====================
public async Task<List<KtjlctgReadDto_View>> GetCtgAsync(DateTime date, long depId, Prod_Banci_Enum banci,
string like)
{
var d = date.Date;
var ctx = await BuildContextAsync();
var rows = (await _ctgRepo.FindAsync(x => x.Date == d && x.DeptId == depId && x.Banci == banci))
.OrderBy(x => x.Jizu).ThenBy(x => x.Hours).ToList();
var views = rows.Select(x => ToCtgView(x, ctx)).ToList();
return Filter(views, like, v => $"{v.EmployeeName}{v.WorktypeName}{v.EmployeeDepName}{v.Jizu}");
}
public async Task<List<KtjldcgReadDto_View>> GetDcgAsync(DateTime date, long depId, Prod_Banci_Enum banci,
string like)
{
var d = date.Date;
var ctx = await BuildContextAsync();
var rows = (await _dcgRepo.FindAsync(x => x.Date == d && x.DeptId == depId && x.Banci == banci))
.OrderBy(x => x.Jizu).ThenBy(x => x.ChetaiStart).ThenBy(x => x.Hours).ToList();
var views = rows.Select(x => ToDcgView(x, ctx)).ToList();
return Filter(views, like,
v => $"{v.EmployeeName}{v.WorktypeName}{v.EmployeeDepName}{v.Jizu}{v.ChetaiStart}{v.ChetaiEnd}");
}
private static List<T> Filter<T>(List<T> rows, string like, Func<T, string> selector)
{
if (string.IsNullOrWhiteSpace(like)) return rows;
var kw = like.Trim();
return rows.Where(x => selector(x).Contains(kw, StringComparison.OrdinalIgnoreCase)).ToList();
}
// ==================== 保存 ====================
public async Task<int> SaveCtgAsync(KtjlctgBatchDto dto)
{
if (dto.DepId <= 0) throw new ServiceException("请选择车间");
var items = dto.Items ?? new List<KtjlctgReadDto_View>();
foreach (var it in items)
{
if (it.EmployeeId <= 0) throw new ServiceException("不能有空数据(存在未选择职工的记录)");
if (it.Hours < 0) throw new ServiceException($"「{it.EmployeeName}」的时长不能为负数");
}
var d = dto.Date.Date;
await DeleteCtgCoreAsync(d, dto.DepId, dto.Banci);
foreach (var it in items)
{
await _ctgRepo.AddAsync(new KtjlCtg
{
Date = d,
DeptId = dto.DepId,
Banci = dto.Banci,
Jizu = it.Jizu,
Hours = it.Hours,
EmployeeId = it.EmployeeId,
EmployeeDepId = it.EmployeeDepId,
WorktypeId = it.WorktypeId,
WorktypeName = it.WorktypeName ?? string.Empty,
NoChanliang = it.NoChanliang
});
}
await _ctgRepo.SaveChangesAsync();
return items.Count;
}
public async Task<int> SaveDcgAsync(KtjldcgBatchDto dto)
{
if (dto.DepId <= 0) throw new ServiceException("请选择车间");
var items = dto.Items ?? new List<KtjldcgReadDto_View>();
foreach (var it in items)
{
if (it.EmployeeId <= 0) throw new ServiceException("不能有空数据(存在未选择职工的记录)");
if (it.ChetaiStart <= 0) throw new ServiceException($"「{it.EmployeeName}」请填写开始车位");
if (it.ChetaiEnd < it.ChetaiStart)
throw new ServiceException($"「{it.EmployeeName}」结束车位不能小于开始车位");
if (it.Hours < 0) throw new ServiceException($"「{it.EmployeeName}」的时长不能为负数");
}
var d = dto.Date.Date;
var jzcts = (await _jzctRepo.FindAsync(x => x.DepartmentId == dto.DepId)).OrderBy(x => x.Jizu).ToList();
// 挡车数 / 机组 由服务端重算(对齐 F9 CalDangcheNum + CalJizu)
foreach (var it in items)
{
var startShared = items.Any(x => x.EmployeeId != it.EmployeeId && x.ChetaiEnd == it.ChetaiStart);
var endShared = items.Any(x => x.EmployeeId != it.EmployeeId && x.ChetaiStart == it.ChetaiEnd);
it.DangcheNum = it.ChetaiEnd + 1 - it.ChetaiStart
- (startShared ? 0.5m : 0m) - (endShared ? 0.5m : 0m);
var jzct = jzcts.FirstOrDefault(x => x.ChetaiStart <= it.ChetaiEnd && x.ChetaiEnd >= it.ChetaiStart);
if (jzct != null) it.Jizu = jzct.Jizu;
}
await DeleteDcgCoreAsync(d, dto.DepId, dto.Banci);
foreach (var it in items)
{
await _dcgRepo.AddAsync(new KtjlDcg
{
Date = d,
DeptId = dto.DepId,
Banci = dto.Banci,
Jizu = it.Jizu,
ChetaiStart = it.ChetaiStart,
ChetaiEnd = it.ChetaiEnd,
DangcheNum = it.DangcheNum,
Hours = it.Hours,
EmployeeId = it.EmployeeId,
EmployeeDepId = it.EmployeeDepId,
WorktypeId = it.WorktypeId,
WorktypeName = it.WorktypeName ?? string.Empty,
NoChanliang = it.NoChanliang
});
}
await _dcgRepo.SaveChangesAsync();
return items.Count;
}
public async Task DeleteCtgAsync(DateTime date, long depId, Prod_Banci_Enum banci)
{
if (depId <= 0) throw new ServiceException("请选择车间");
await DeleteCtgCoreAsync(date.Date, depId, banci);
await _ctgRepo.SaveChangesAsync();
}
public async Task DeleteDcgAsync(DateTime date, long depId, Prod_Banci_Enum banci)
{
if (depId <= 0) throw new ServiceException("请选择车间");
await DeleteDcgCoreAsync(date.Date, depId, banci);
await _dcgRepo.SaveChangesAsync();
}
private async Task DeleteCtgCoreAsync(DateTime date, long depId, Prod_Banci_Enum banci)
{
var olds = await _ctgRepo.FindAsync(x => x.Date == date && x.DeptId == depId && x.Banci == banci);
if (olds.Count > 0) await _ctgRepo.HardDeleteRangeAsync(olds);
}
private async Task DeleteDcgCoreAsync(DateTime date, long depId, Prod_Banci_Enum banci)
{
var olds = await _dcgRepo.FindAsync(x => x.Date == date && x.DeptId == depId && x.Banci == banci);
if (olds.Count > 0) await _dcgRepo.HardDeleteRangeAsync(olds);
}
// ==================== 默认记录(录入(新增)) ====================
public async Task<List<KtjlctgReadDto_View>> BuildCtgDefaultsAsync(DateTime date, long depId,
Prod_Banci_Enum banci)
{
var ctx = await BuildContextAsync();
var deptName = ctx.DeptNames.GetValueOrDefault(depId, "");
var plans = (await _ktpbCtgRepo.FindAsync(x => x.DeptId == depId && x.Banci == banci)).OrderBy(x => x.Jizu);
var result = new List<KtjlctgReadDto_View>();
foreach (var p in plans)
{
foreach (var ygId in new[] { p.Ygid1, p.Ygid2 })
{
if (ygId <= 0 || !ctx.Emps.TryGetValue(ygId, out var emp)) continue;
result.Add(new KtjlctgReadDto_View
{
Date = date.Date,
DepId = depId,
DepName = deptName,
Banci = banci,
BanciName = banci.ToString(),
Jizu = p.Jizu,
Hours = DefaultHours,
EmployeeId = emp.Id,
EmployeeName = emp.Name,
EmployeeDepId = emp.DepId,
EmployeeDepName = ctx.DeptNames.GetValueOrDefault(emp.DepId, ""),
WorktypeId = emp.WorktypeId,
WorktypeName = ctx.WorktypeNames.GetValueOrDefault(emp.WorktypeId, ""),
NoChanliang = !emp.IsZhuanzheng
});
}
}
return result;
}
public async Task<List<KtjldcgReadDto_View>> BuildDcgDefaultsAsync(DateTime date, long depId,
Prod_Banci_Enum banci)
{
var ctx = await BuildContextAsync();
var deptName = ctx.DeptNames.GetValueOrDefault(depId, "");
var plans = (await _ktpbDcgRepo.FindAsync(x => x.DeptId == depId && x.Banci == banci))
.OrderBy(x => x.Jizu).ThenBy(x => x.ChetaiStart);
var result = new List<KtjldcgReadDto_View>();
foreach (var p in plans)
{
if (p.YgId <= 0 || !ctx.Emps.TryGetValue(p.YgId, out var emp)) continue;
result.Add(new KtjldcgReadDto_View
{
Date = date.Date,
DepId = depId,
DepName = deptName,
Banci = banci,
BanciName = banci.ToString(),
Jizu = p.Jizu,
ChetaiStart = p.ChetaiStart,
ChetaiEnd = p.ChetaiEnd,
DangcheNum = (decimal)p.Dangcheshu,
Hours = DefaultHours,
EmployeeId = emp.Id,
EmployeeName = emp.Name,
EmployeeDepId = emp.DepId,
EmployeeDepName = ctx.DeptNames.GetValueOrDefault(emp.DepId, ""),
WorktypeId = emp.WorktypeId,
WorktypeName = ctx.WorktypeNames.GetValueOrDefault(emp.WorktypeId, ""),
NoChanliang = !emp.IsZhuanzheng
});
}
return result;
}
public async Task<List<ProdWorktypeOptionDto>> GetWorktypeOptionsAsync()
=> (await _wtRepo.GetAllAsync()).OrderBy(x => x.SortOrder).ThenBy(x => x.Id)
.Select(x => new ProdWorktypeOptionDto { Id = x.Id, Name = x.WorkName })
.ToList();
// ==================== 上下文 ====================
private sealed class Ctx
{
public Dictionary<long, string> DeptNames { get; init; } = new();
public Dictionary<long, string> WorktypeNames { get; init; } = new();
public Dictionary<long, EmpInfo> Emps { get; init; } = new();
}
private sealed class EmpInfo
{
public long Id { get; init; }
public string Name { get; init; } = "";
public long DepId { get; init; }
public long WorktypeId { get; init; }
public bool IsZhuanzheng { get; init; }
}
private async Task<Ctx> BuildContextAsync()
{
var depts = await _deptRepo.GetAllAsync();
var wts = await _wtRepo.GetAllAsync();
var emps = await _empRepo.GetAllAsync();
return new Ctx
{
DeptNames = depts.ToDictionary(d => d.Id, d => d.Name),
WorktypeNames = wts.ToDictionary(w => w.Id, w => w.WorkName),
Emps = emps.ToDictionary(e => e.Id, e => new EmpInfo
{
Id = e.Id,
Name = e.Name,
DepId = e.DepId,
WorktypeId = e.WorktypeId,
IsZhuanzheng = e.IsZhuanzheng
})
};
}
private static KtjlctgReadDto_View ToCtgView(KtjlCtg x, Ctx ctx) => new()
{
Id = x.Id,
Date = x.Date,
DepId = x.DeptId,
DepName = ctx.DeptNames.GetValueOrDefault(x.DeptId, ""),
Banci = x.Banci,
BanciName = x.Banci.ToString(),
Jizu = x.Jizu,
Hours = x.Hours,
EmployeeId = x.EmployeeId,
EmployeeName = ctx.Emps.GetValueOrDefault(x.EmployeeId)?.Name ?? "",
EmployeeDepId = x.EmployeeDepId,
EmployeeDepName = ctx.DeptNames.GetValueOrDefault(x.EmployeeDepId, ""),
WorktypeId = x.WorktypeId,
WorktypeName = string.IsNullOrWhiteSpace(x.WorktypeName)
? ctx.WorktypeNames.GetValueOrDefault(x.WorktypeId, "")
: x.WorktypeName,
NoChanliang = x.NoChanliang
};
private static KtjldcgReadDto_View ToDcgView(KtjlDcg x, Ctx ctx) => new()
{
Id = x.Id,
Date = x.Date,
DepId = x.DeptId,
DepName = ctx.DeptNames.GetValueOrDefault(x.DeptId, ""),
Banci = x.Banci,
BanciName = x.Banci.ToString(),
Jizu = x.Jizu,
ChetaiStart = x.ChetaiStart,
ChetaiEnd = x.ChetaiEnd,
DangcheNum = x.DangcheNum,
Hours = x.Hours,
EmployeeId = x.EmployeeId,
EmployeeName = ctx.Emps.GetValueOrDefault(x.EmployeeId)?.Name ?? "",
EmployeeDepId = x.EmployeeDepId,
EmployeeDepName = ctx.DeptNames.GetValueOrDefault(x.EmployeeDepId, ""),
WorktypeId = x.WorktypeId,
WorktypeName = string.IsNullOrWhiteSpace(x.WorktypeName)
? ctx.WorktypeNames.GetValueOrDefault(x.WorktypeId, "")
: x.WorktypeName,
NoChanliang = x.NoChanliang
};
}