新增生产-前缫看台排班,并导入机组车台与看台排班数据

功能:
- 后端新增 KtpbCtgs / KtpbDcgs / KtpbZbzs 三张表、KtpbService 与 api/prod/ktpb 接口
  (车头工、挡车工、值班长分页查询与维护,挡车工按车间+班次+机组整组替换保存)
- 挡车数按 F9 口径自动重算:接壤车台各计 0.5,保存时服务端重新计算并校验车台范围
- 前端新增 /prod/qianxian/ktpb 看台排班页(三页签 + 车间/班次/机组过滤 + 批量编辑弹窗)
- 新增通用职工选择弹窗 EmployeeSelect(单选/多选,按部门过滤)
- 生产-前缫菜单下新增「看台排班」,权限 prod:ktpb:list

数据导入:
- 重新生成 biz_prod_qs_jzct.json,新增 ktpb_ctg / ktpb_dcg / ktpb_zbz 三个内嵌资源
- F9 部门 Id 经 DeptData 映射到本系统部门;职工按原始雪花 Id 关联
- 剔除软删除/脏数据:机组车台 24 条、车头工 33 条、挡车工 202 条(34 组)、值班长 7 条
- 导入幂等:机组车台按车间+机组 upsert,排班按业务唯一键去重,已维护的组不覆盖

其他:同步工作进度-12 文档与开发数据库快照
This commit is contained in:
yanpeng200212
2026-10-22 10:49:13 +08:00
parent a8890c81e9
commit a30d1cdcce
16 changed files with 2123 additions and 422 deletions
@@ -27,6 +27,7 @@ public static class DependencyInjection
services.AddScoped<IXdjyService, XdjyService>();
services.AddScoped<IHbjyService, HbjyService>();
services.AddScoped<IChejianBanciService, ChejianBanciService>();
services.AddScoped<IKtpbService, KtpbService>();
services.AddScoped<INotificationService, NotificationService>();
services.AddScoped<IReportService, ReportService>();
@@ -0,0 +1,70 @@
using F9WebAiTest.Domain.Enums.SYS.Employees;
namespace F9WebAiTest.Application.Dtos.Prod;
// ==================== 车头工看台排班 ====================
/// <summary>车头工看台排班(列表/编辑)</summary>
public class KtpbctgReadDto_View
{
public long Id { get; set; }
public long DepId { get; set; }
public string DeptName { get; set; } = "";
public Prod_Banci_Enum Banci { get; set; }
public string BanciName { get; set; } = "";
public int Jizu { get; set; }
public long Ygid1 { get; set; }
public string Ygid1Name { get; set; } = "";
public long Ygid2 { get; set; }
public string Ygid2Name { get; set; } = "";
}
// ==================== 挡车工看台排班 ====================
/// <summary>挡车工看台排班(列表/编辑)</summary>
public class KtpbdcgReadDto_View
{
public long Id { get; set; }
public long DepId { get; set; }
public string DeptName { get; set; } = "";
public Prod_Banci_Enum Banci { get; set; }
public string BanciName { get; set; } = "";
public int Jizu { get; set; }
public int ChetaiStart { get; set; }
public int ChetaiEnd { get; set; }
public long YgId { get; set; }
public string YgName { get; set; } = "";
public double Dangcheshu { get; set; }
}
/// <summary>挡车工批量编辑:一个「车间 + 班次 + 机组」下的整组排班替换</summary>
public class KtpbdcgBatchDto
{
public long DepId { get; set; }
public Prod_Banci_Enum Banci { get; set; }
public int Jizu { get; set; }
/// <summary>该组下的全部挡车工排班(为空表示清空该组)</summary>
public List<KtpbdcgReadDto_View> Items { get; set; } = new();
}
// ==================== 值班长排班 ====================
/// <summary>值班长排班(列表/编辑)</summary>
public class KtpbzbzReadDto_View
{
public long Id { get; set; }
public long DepId { get; set; }
public string DeptName { get; set; } = "";
public Prod_Banci_Enum Banci { get; set; }
public string BanciName { get; set; } = "";
public long YgId { get; set; }
public string YgName { get; set; } = "";
}
/// <summary>机组下拉项(来源于机组车台)</summary>
public class KtpbJizuOptionDto
{
public int Jizu { get; set; }
public int ChetaiStart { get; set; }
public int ChetaiEnd { get; set; }
}
@@ -0,0 +1,443 @@
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.Departments;
using F9WebAiTest.Domain.Enums.SYS.Employees;
using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments;
using SysEmp = F9WebAiTest.Domain.Entities.SYS.Employees;
namespace F9WebAiTest.Application.Services.Prod;
/// <summary>
/// 看台排班(生产-前缫):车头工 / 挡车工 / 值班长三类排班的查询与维护。
/// 对应 F9 WinClient\F9.Business.Prod\QS\KTPB 下的 Page_KTPB 与三个编辑页。
/// </summary>
public interface IKtpbService
{
/// <summary>车间下拉(职责类型=前缫)</summary>
Task<List<ChejianbanciDepOptionDto>> GetQianxianDepsAsync();
/// <summary>指定车间的机组下拉(含车台范围,来源于机组车台)</summary>
Task<List<KtpbJizuOptionDto>> GetJizuOptionsAsync(long depId);
// 车头工
Task<PageResult<KtpbctgReadDto_View>> GetCtgPagedAsync(long depId, Prod_Banci_Enum? banci, int? jizu,
string keyword, int pageIndex, int pageSize);
Task<KtpbctgReadDto_View> GetCtgByIdAsync(long id);
Task CreateCtgAsync(KtpbctgReadDto_View dto);
Task UpdateCtgAsync(long id, KtpbctgReadDto_View dto);
Task DeleteCtgAsync(long id);
// 挡车工
Task<PageResult<KtpbdcgReadDto_View>> GetDcgPagedAsync(long depId, Prod_Banci_Enum? banci, int? jizu,
string keyword, int pageIndex, int pageSize);
/// <summary>取某个「车间+班次+机组」下的整组排班(批量编辑弹窗用)</summary>
Task<List<KtpbdcgReadDto_View>> GetDcgGroupAsync(long depId, Prod_Banci_Enum banci, int jizu);
/// <summary>整组替换:先删除该组旧排班,再按提交内容重建(挡车数由服务端重算)</summary>
Task<int> SaveDcgBatchAsync(KtpbdcgBatchDto dto);
Task DeleteDcgAsync(long id);
// 值班长
Task<PageResult<KtpbzbzReadDto_View>> GetZbzPagedAsync(long depId, Prod_Banci_Enum? banci,
string keyword, int pageIndex, int pageSize);
Task<KtpbzbzReadDto_View> GetZbzByIdAsync(long id);
Task CreateZbzAsync(KtpbzbzReadDto_View dto);
Task UpdateZbzAsync(long id, KtpbzbzReadDto_View dto);
Task DeleteZbzAsync(long id);
}
public class KtpbService : IKtpbService
{
private readonly IRepository<KtpbCtg> _ctgRepo;
private readonly IRepository<KtpbDcg> _dcgRepo;
private readonly IRepository<KtpbZbz> _zbzRepo;
private readonly IRepository<JizuChetai> _jzctRepo;
private readonly IRepository<SysDept.Department> _deptRepo;
private readonly IRepository<SysEmp.Employee> _empRepo;
public KtpbService(IRepository<KtpbCtg> ctgRepo, IRepository<KtpbDcg> dcgRepo, IRepository<KtpbZbz> zbzRepo,
IRepository<JizuChetai> jzctRepo, IRepository<SysDept.Department> deptRepo,
IRepository<SysEmp.Employee> empRepo)
{
_ctgRepo = ctgRepo;
_dcgRepo = dcgRepo;
_zbzRepo = zbzRepo;
_jzctRepo = jzctRepo;
_deptRepo = deptRepo;
_empRepo = empRepo;
}
// ==================== 下拉 ====================
public async Task<List<ChejianbanciDepOptionDto>> GetQianxianDepsAsync()
{
var depts = await _deptRepo.GetAllAsync();
return depts
.Where(d => d.DutyType == Department_DutyType_Enum.前缫)
.OrderBy(d => d.SortOrder).ThenBy(d => d.Id)
.Select(d => new ChejianbanciDepOptionDto { Id = d.Id, Name = d.Name })
.ToList();
}
public async Task<List<KtpbJizuOptionDto>> GetJizuOptionsAsync(long depId)
{
var list = await _jzctRepo.FindAsync(x => x.DepartmentId == depId);
return list.OrderBy(x => x.Jizu)
.Select(x => new KtpbJizuOptionDto
{
Jizu = x.Jizu,
ChetaiStart = x.ChetaiStart,
ChetaiEnd = x.ChetaiEnd
})
.ToList();
}
// ==================== 车头工 ====================
public async Task<PageResult<KtpbctgReadDto_View>> GetCtgPagedAsync(long depId, Prod_Banci_Enum? banci,
int? jizu, string keyword, int pageIndex, int pageSize)
{
if (pageIndex <= 0) pageIndex = 1;
if (pageSize <= 0) pageSize = 20;
var deptDict = await GetDeptDictAsync();
var empDict = await GetEmpDictAsync();
var all = await _ctgRepo.GetAllAsync();
var q = all.AsEnumerable();
if (depId > 0) q = q.Where(x => x.DeptId == depId);
if (banci.HasValue) q = q.Where(x => x.Banci == banci.Value);
if (jizu.HasValue && jizu.Value > 0) q = q.Where(x => x.Jizu == jizu.Value);
var views = q.OrderBy(x => x.DeptId).ThenBy(x => x.Banci).ThenBy(x => x.Jizu)
.Select(x => ToCtgView(x, deptDict, empDict)).ToList();
if (!string.IsNullOrWhiteSpace(keyword))
{
var kw = keyword.Trim();
views = views.Where(x => x.DeptName.Contains(kw) || x.BanciName.Contains(kw)
|| x.Jizu.ToString().Contains(kw) || x.Ygid1Name.Contains(kw) || x.Ygid2Name.Contains(kw)).ToList();
}
return Paged(views, pageIndex, pageSize);
}
public async Task<KtpbctgReadDto_View> GetCtgByIdAsync(long id)
{
var e = await _ctgRepo.GetByIdAsync(id) ?? throw new ServiceException("记录不存在", 404);
return ToCtgView(e, await GetDeptDictAsync(), await GetEmpDictAsync());
}
public async Task CreateCtgAsync(KtpbctgReadDto_View dto)
{
await ValidateGroupAsync(dto.DepId, dto.Banci, dto.Jizu, 0);
if (dto.Ygid1 <= 0) throw new ServiceException("请选择车头工1");
await _ctgRepo.AddAsync(new KtpbCtg
{
DeptId = dto.DepId,
Banci = dto.Banci,
Jizu = dto.Jizu,
Ygid1 = dto.Ygid1,
Ygid2 = dto.Ygid2
});
await _ctgRepo.SaveChangesAsync();
}
public async Task UpdateCtgAsync(long id, KtpbctgReadDto_View dto)
{
var e = await _ctgRepo.GetByIdAsync(id) ?? throw new ServiceException("记录不存在", 404);
await ValidateGroupAsync(dto.DepId, dto.Banci, dto.Jizu, id);
if (dto.Ygid1 <= 0) throw new ServiceException("请选择车头工1");
e.DeptId = dto.DepId;
e.Banci = dto.Banci;
e.Jizu = dto.Jizu;
e.Ygid1 = dto.Ygid1;
e.Ygid2 = dto.Ygid2;
await _ctgRepo.UpdateAsync(e);
await _ctgRepo.SaveChangesAsync();
}
public async Task DeleteCtgAsync(long id)
{
var e = await _ctgRepo.GetByIdAsync(id) ?? throw new ServiceException("记录不存在", 404);
await _ctgRepo.DeleteAsync(e);
await _ctgRepo.SaveChangesAsync();
}
// ==================== 挡车工 ====================
public async Task<PageResult<KtpbdcgReadDto_View>> GetDcgPagedAsync(long depId, Prod_Banci_Enum? banci,
int? jizu, string keyword, int pageIndex, int pageSize)
{
if (pageIndex <= 0) pageIndex = 1;
if (pageSize <= 0) pageSize = 20;
var deptDict = await GetDeptDictAsync();
var empDict = await GetEmpDictAsync();
var all = await _dcgRepo.GetAllAsync();
var q = all.AsEnumerable();
if (depId > 0) q = q.Where(x => x.DeptId == depId);
if (banci.HasValue) q = q.Where(x => x.Banci == banci.Value);
if (jizu.HasValue && jizu.Value > 0) q = q.Where(x => x.Jizu == jizu.Value);
var views = q.OrderBy(x => x.DeptId).ThenBy(x => x.Banci).ThenBy(x => x.Jizu)
.ThenBy(x => x.ChetaiStart)
.Select(x => ToDcgView(x, deptDict, empDict)).ToList();
if (!string.IsNullOrWhiteSpace(keyword))
{
var kw = keyword.Trim();
views = views.Where(x => x.DeptName.Contains(kw) || x.BanciName.Contains(kw)
|| x.YgName.Contains(kw) || x.Jizu.ToString().Contains(kw)
|| x.ChetaiStart.ToString().Contains(kw) || x.ChetaiEnd.ToString().Contains(kw)).ToList();
}
return Paged(views, pageIndex, pageSize);
}
public async Task<List<KtpbdcgReadDto_View>> GetDcgGroupAsync(long depId, Prod_Banci_Enum banci, int jizu)
{
if (depId <= 0) throw new ServiceException("请选择车间");
var deptDict = await GetDeptDictAsync();
var empDict = await GetEmpDictAsync();
var list = await _dcgRepo.FindAsync(x => x.DeptId == depId && x.Banci == banci && x.Jizu == jizu);
return list.OrderBy(x => x.ChetaiStart).ThenBy(x => x.ChetaiEnd)
.Select(x => ToDcgView(x, deptDict, empDict)).ToList();
}
public async Task<int> SaveDcgBatchAsync(KtpbdcgBatchDto dto)
{
if (dto.DepId <= 0) throw new ServiceException("请选择车间");
if (dto.Jizu <= 0) throw new ServiceException("请选择机组");
var items = dto.Items ?? new List<KtpbdcgReadDto_View>();
foreach (var it in items)
{
if (it.YgId <= 0) throw new ServiceException("存在未选择挡车工的记录");
if (it.ChetaiStart <= 0) throw new ServiceException($"挡车工「{it.YgName}」的起始车台必须大于 0");
if (it.ChetaiEnd < it.ChetaiStart)
throw new ServiceException($"挡车工「{it.YgName}」的结束车台不能小于起始车台");
}
var jzct = await _jzctRepo.FirstOrDefaultAsync(x => x.DepartmentId == dto.DepId && x.Jizu == dto.Jizu);
if (jzct != null)
{
foreach (var it in items)
{
if (it.ChetaiStart < jzct.ChetaiStart || it.ChetaiEnd > jzct.ChetaiEnd)
throw new ServiceException(
$"挡车工「{it.YgName}」的车台范围 {it.ChetaiStart}-{it.ChetaiEnd} 超出该机组车台范围 " +
$"{jzct.ChetaiStart}-{jzct.ChetaiEnd}");
}
}
// 整组替换:先删后建(对齐 F9 BatchReplaceKtpbdcgCommand)
var olds = await _dcgRepo.FindAsync(x => x.DeptId == dto.DepId && x.Banci == dto.Banci && x.Jizu == dto.Jizu);
if (olds.Count > 0) await _dcgRepo.HardDeleteRangeAsync(olds);
var dangche = RecalcDangcheshu(items);
foreach (var it in items)
{
await _dcgRepo.AddAsync(new KtpbDcg
{
DeptId = dto.DepId,
Banci = dto.Banci,
Jizu = dto.Jizu,
ChetaiStart = it.ChetaiStart,
ChetaiEnd = it.ChetaiEnd,
YgId = it.YgId,
Dangcheshu = dangche.GetValueOrDefault(BuildDcgKey(it.ChetaiStart, it.ChetaiEnd, it.YgId), 0)
});
}
await _dcgRepo.SaveChangesAsync();
return items.Count;
}
public async Task DeleteDcgAsync(long id)
{
var e = await _dcgRepo.GetByIdAsync(id) ?? throw new ServiceException("记录不存在", 404);
await _dcgRepo.DeleteAsync(e);
await _dcgRepo.SaveChangesAsync();
}
/// <summary>
/// 挡车数计算(移植 F9 Page_KTPBDCG_Edit.CalculateSingleDangcheshu):
/// 每个车台默认权重 1;若该车台被多条记录共享,且同时是某记录的起始点与某记录的结束点(接壤车台),权重取 0.5。
/// </summary>
private static Dictionary<string, double> RecalcDangcheshu(List<KtpbdcgReadDto_View> records)
{
var result = new Dictionary<string, double>();
if (records.Count == 0) return result;
var counts = new Dictionary<int, int>();
var starts = new HashSet<int>();
var ends = new HashSet<int>();
foreach (var r in records)
{
starts.Add(r.ChetaiStart);
ends.Add(r.ChetaiEnd);
for (var i = r.ChetaiStart; i <= r.ChetaiEnd; i++)
counts[i] = counts.GetValueOrDefault(i) + 1;
}
foreach (var r in records)
{
double total = 0;
for (var i = r.ChetaiStart; i <= r.ChetaiEnd; i++)
{
double weight = 1;
if (counts.GetValueOrDefault(i) > 1 && starts.Contains(i) && ends.Contains(i)) weight = 0.5;
total += weight;
}
result[BuildDcgKey(r.ChetaiStart, r.ChetaiEnd, r.YgId)] = Math.Round(total, 2);
}
return result;
}
private static string BuildDcgKey(int start, int end, long ygId) => $"{start}-{end}-{ygId}";
// ==================== 值班长 ====================
public async Task<PageResult<KtpbzbzReadDto_View>> GetZbzPagedAsync(long depId, Prod_Banci_Enum? banci,
string keyword, int pageIndex, int pageSize)
{
if (pageIndex <= 0) pageIndex = 1;
if (pageSize <= 0) pageSize = 20;
var deptDict = await GetDeptDictAsync();
var empDict = await GetEmpDictAsync();
var all = await _zbzRepo.GetAllAsync();
var q = all.AsEnumerable();
if (depId > 0) q = q.Where(x => x.DeptId == depId);
if (banci.HasValue) q = q.Where(x => x.Banci == banci.Value);
var views = q.OrderBy(x => x.DeptId).ThenBy(x => x.Banci).ThenBy(x => x.Id)
.Select(x => ToZbzView(x, deptDict, empDict)).ToList();
if (!string.IsNullOrWhiteSpace(keyword))
{
var kw = keyword.Trim();
views = views.Where(x => x.DeptName.Contains(kw) || x.BanciName.Contains(kw) || x.YgName.Contains(kw))
.ToList();
}
return Paged(views, pageIndex, pageSize);
}
public async Task<KtpbzbzReadDto_View> GetZbzByIdAsync(long id)
{
var e = await _zbzRepo.GetByIdAsync(id) ?? throw new ServiceException("记录不存在", 404);
return ToZbzView(e, await GetDeptDictAsync(), await GetEmpDictAsync());
}
public async Task CreateZbzAsync(KtpbzbzReadDto_View dto)
{
if (dto.DepId <= 0) throw new ServiceException("请选择车间");
if (dto.YgId <= 0) throw new ServiceException("请选择值班长");
var dup = await _zbzRepo.AnyAsync(x => x.DeptId == dto.DepId && x.Banci == dto.Banci && x.YgId == dto.YgId);
if (dup) throw new ServiceException("该车间该班次已存在此值班长排班");
await _zbzRepo.AddAsync(new KtpbZbz { DeptId = dto.DepId, Banci = dto.Banci, YgId = dto.YgId });
await _zbzRepo.SaveChangesAsync();
}
public async Task UpdateZbzAsync(long id, KtpbzbzReadDto_View dto)
{
var e = await _zbzRepo.GetByIdAsync(id) ?? throw new ServiceException("记录不存在", 404);
if (dto.DepId <= 0) throw new ServiceException("请选择车间");
if (dto.YgId <= 0) throw new ServiceException("请选择值班长");
var dup = await _zbzRepo.AnyAsync(x => x.Id != id && x.DeptId == dto.DepId
&& x.Banci == dto.Banci && x.YgId == dto.YgId);
if (dup) throw new ServiceException("该车间该班次已存在此值班长排班");
e.DeptId = dto.DepId;
e.Banci = dto.Banci;
e.YgId = dto.YgId;
await _zbzRepo.UpdateAsync(e);
await _zbzRepo.SaveChangesAsync();
}
public async Task DeleteZbzAsync(long id)
{
var e = await _zbzRepo.GetByIdAsync(id) ?? throw new ServiceException("记录不存在", 404);
await _zbzRepo.DeleteAsync(e);
await _zbzRepo.SaveChangesAsync();
}
// ==================== 公共 ====================
/// <summary>车头工:同一「车间+班次+机组」只能有一条排班</summary>
private async Task ValidateGroupAsync(long depId, Prod_Banci_Enum banci, int jizu, long excludeId)
{
if (depId <= 0) throw new ServiceException("请选择车间");
if (jizu <= 0) throw new ServiceException("请选择机组");
var dup = await _ctgRepo.AnyAsync(x => x.Id != excludeId && x.DeptId == depId
&& x.Banci == banci && x.Jizu == jizu);
if (dup) throw new ServiceException("该车间该班次该机组已有车头工排班");
}
private async Task<Dictionary<long, string>> GetDeptDictAsync()
=> (await _deptRepo.GetAllAsync()).ToDictionary(d => d.Id, d => d.Name);
private async Task<Dictionary<long, string>> GetEmpDictAsync()
=> (await _empRepo.GetAllAsync()).ToDictionary(e => e.Id, e => e.Name);
private static KtpbctgReadDto_View ToCtgView(KtpbCtg x, Dictionary<long, string> deptDict,
Dictionary<long, string> empDict) => new()
{
Id = x.Id,
DepId = x.DeptId,
DeptName = deptDict.GetValueOrDefault(x.DeptId, ""),
Banci = x.Banci,
BanciName = x.Banci.ToString(),
Jizu = x.Jizu,
Ygid1 = x.Ygid1,
Ygid1Name = empDict.GetValueOrDefault(x.Ygid1, ""),
Ygid2 = x.Ygid2,
Ygid2Name = empDict.GetValueOrDefault(x.Ygid2, "")
};
private static KtpbdcgReadDto_View ToDcgView(KtpbDcg x, Dictionary<long, string> deptDict,
Dictionary<long, string> empDict) => new()
{
Id = x.Id,
DepId = x.DeptId,
DeptName = deptDict.GetValueOrDefault(x.DeptId, ""),
Banci = x.Banci,
BanciName = x.Banci.ToString(),
Jizu = x.Jizu,
ChetaiStart = x.ChetaiStart,
ChetaiEnd = x.ChetaiEnd,
YgId = x.YgId,
YgName = empDict.GetValueOrDefault(x.YgId, ""),
Dangcheshu = x.Dangcheshu
};
private static KtpbzbzReadDto_View ToZbzView(KtpbZbz x, Dictionary<long, string> deptDict,
Dictionary<long, string> empDict) => new()
{
Id = x.Id,
DepId = x.DeptId,
DeptName = deptDict.GetValueOrDefault(x.DeptId, ""),
Banci = x.Banci,
BanciName = x.Banci.ToString(),
YgId = x.YgId,
YgName = empDict.GetValueOrDefault(x.YgId, "")
};
private static PageResult<T> Paged<T>(List<T> all, int pageIndex, int pageSize) => new()
{
Items = all.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(),
Total = all.Count,
PageIndex = pageIndex,
PageSize = pageSize
};
}