Files
yanpeng200212 a30d1cdcce 新增生产-前缫看台排班,并导入机组车台与看台排班数据
功能:
- 后端新增 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 文档与开发数据库快照
2026-10-22 10:49:13 +08:00

444 lines
18 KiB
C#
Raw Permalink 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.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
};
}