新增生产-前缫看台记录,并导入车头工/挡车工看台记录历史数据

功能:
- 后端新增 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 文档与开发数据库快照
This commit is contained in:
yanpeng200212
2026-10-22 11:16:29 +08:00
parent 2e4c045818
commit 69c4052a64
15 changed files with 52228 additions and 2 deletions
@@ -0,0 +1,108 @@
using F9WebAiTest.Application.Common;
using F9WebAiTest.Application.Dtos.Prod;
using F9WebAiTest.Application.Services.Prod;
using F9WebAiTest.Domain.Common;
using F9WebAiTest.Domain.Enums.SYS.Employees;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace F9WebAiTest.Api.Controllers.Prod;
/// <summary>看台记录(生产-前缫):车头工 / 挡车工</summary>
[ApiController]
[Route("api/prod/ktjl")]
[Authorize]
public class KtjlController : ControllerBase
{
private readonly IKtjlService _svc;
public KtjlController(IKtjlService svc) => _svc = svc;
// ---------- 查询 ----------
/// <summary>挡车工看台记录(某日某车间某班次)</summary>
[HttpGet("dcg")]
public async Task<IActionResult> GetDcg([FromQuery] DateTime date, [FromQuery] long depId,
[FromQuery] Prod_Banci_Enum banci = Prod_Banci_Enum.甲, [FromQuery] string like = "")
=> Ok(ApiResult<List<KtjldcgReadDto_View>>.Ok(
await _svc.GetDcgAsync(date, depId, banci, like ?? string.Empty)));
/// <summary>车头工看台记录</summary>
[HttpGet("ctg")]
public async Task<IActionResult> GetCtg([FromQuery] DateTime date, [FromQuery] long depId,
[FromQuery] Prod_Banci_Enum banci = Prod_Banci_Enum.甲, [FromQuery] string like = "")
=> Ok(ApiResult<List<KtjlctgReadDto_View>>.Ok(
await _svc.GetCtgAsync(date, depId, banci, like ?? string.Empty)));
// ---------- 录入(新增):按看台排班带出默认记录 ----------
[HttpGet("dcg/defaults")]
public async Task<IActionResult> GetDcgDefaults([FromQuery] DateTime date, [FromQuery] long depId,
[FromQuery] Prod_Banci_Enum banci = Prod_Banci_Enum.甲)
{
try { return Ok(ApiResult<List<KtjldcgReadDto_View>>.Ok(await _svc.BuildDcgDefaultsAsync(date, depId, banci))); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
[HttpGet("ctg/defaults")]
public async Task<IActionResult> GetCtgDefaults([FromQuery] DateTime date, [FromQuery] long depId,
[FromQuery] Prod_Banci_Enum banci = Prod_Banci_Enum.甲)
{
try { return Ok(ApiResult<List<KtjlctgReadDto_View>>.Ok(await _svc.BuildCtgDefaultsAsync(date, depId, banci))); }
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 整组保存 ----------
[HttpPost("dcg/batch")]
public async Task<IActionResult> SaveDcg([FromBody] KtjldcgBatchDto dto)
{
try
{
var count = await _svc.SaveDcgAsync(dto);
return Ok(ApiResult<object>.Ok(count, count > 0 ? $"已保存 {count} 条看台记录" : "已清空该班次看台记录"));
}
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
[HttpPost("ctg/batch")]
public async Task<IActionResult> SaveCtg([FromBody] KtjlctgBatchDto dto)
{
try
{
var count = await _svc.SaveCtgAsync(dto);
return Ok(ApiResult<object>.Ok(count, count > 0 ? $"已保存 {count} 条看台记录" : "已清空该班次看台记录"));
}
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
// ---------- 整组删除 ----------
[HttpDelete("dcg")]
public async Task<IActionResult> DeleteDcg([FromQuery] DateTime date, [FromQuery] long depId,
[FromQuery] Prod_Banci_Enum banci = Prod_Banci_Enum.甲)
{
try
{
await _svc.DeleteDcgAsync(date, depId, banci);
return Ok(ApiResult<object>.Ok(null, "删除成功"));
}
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
[HttpDelete("ctg")]
public async Task<IActionResult> DeleteCtg([FromQuery] DateTime date, [FromQuery] long depId,
[FromQuery] Prod_Banci_Enum banci = Prod_Banci_Enum.甲)
{
try
{
await _svc.DeleteCtgAsync(date, depId, banci);
return Ok(ApiResult<object>.Ok(null, "删除成功"));
}
catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult<object>.Fail(ex.Message, ex.Code)); }
}
/// <summary>工种下拉</summary>
[HttpGet("worktypes")]
public async Task<IActionResult> GetWorktypes()
=> Ok(ApiResult<List<ProdWorktypeOptionDto>>.Ok(await _svc.GetWorktypeOptionsAsync()));
}
Binary file not shown.
@@ -28,6 +28,7 @@ public static class DependencyInjection
services.AddScoped<IHbjyService, HbjyService>();
services.AddScoped<IChejianBanciService, ChejianBanciService>();
services.AddScoped<IKtpbService, KtpbService>();
services.AddScoped<IKtjlService, KtjlService>();
services.AddScoped<INotificationService, NotificationService>();
services.AddScoped<IReportService, ReportService>();
@@ -0,0 +1,67 @@
using F9WebAiTest.Domain.Enums.SYS.Employees;
namespace F9WebAiTest.Application.Dtos.Prod;
// ==================== 车头工看台记录 ====================
public class KtjlctgReadDto_View
{
public long Id { get; set; }
public DateTime Date { get; set; }
public long DepId { get; set; }
public string DepName { get; set; } = "";
public Prod_Banci_Enum Banci { get; set; }
public string BanciName { get; set; } = "";
public int Jizu { get; set; }
public decimal Hours { get; set; }
public long EmployeeId { get; set; }
public string EmployeeName { get; set; } = "";
public long EmployeeDepId { get; set; }
public string EmployeeDepName { get; set; } = "";
public long WorktypeId { get; set; }
public string WorktypeName { get; set; } = "";
public bool NoChanliang { get; set; }
}
// ==================== 挡车工看台记录 ====================
public class KtjldcgReadDto_View
{
public long Id { get; set; }
public DateTime Date { get; set; }
public long DepId { get; set; }
public string DepName { 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 decimal DangcheNum { get; set; }
public decimal Hours { get; set; }
public long EmployeeId { get; set; }
public string EmployeeName { get; set; } = "";
public long EmployeeDepId { get; set; }
public string EmployeeDepName { get; set; } = "";
public long WorktypeId { get; set; }
public string WorktypeName { get; set; } = "";
public bool NoChanliang { get; set; }
}
// ==================== 整组保存 ====================
/// <summary>看台记录整组保存(按 日期+车间+班次 先删后建,对齐 F9 的 SaveCommand)</summary>
public class KtjlctgBatchDto
{
public DateTime Date { get; set; }
public long DepId { get; set; }
public Prod_Banci_Enum Banci { get; set; }
public List<KtjlctgReadDto_View> Items { get; set; } = new();
}
public class KtjldcgBatchDto
{
public DateTime Date { get; set; }
public long DepId { get; set; }
public Prod_Banci_Enum Banci { get; set; }
public List<KtjldcgReadDto_View> Items { get; set; } = new();
}
@@ -0,0 +1,378 @@
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
};
}
@@ -0,0 +1,43 @@
using FreeSql.DataAnnotations;
using F9WebAiTest.Domain.Common;
using F9WebAiTest.Domain.Enums.SYS.Employees;
namespace F9WebAiTest.Domain.Entities.Prod;
// 看台记录(对应 F9 生产-前缫 的 KTJL 两张表:车头工 / 挡车工)
// 记录 = 某日某车间某班次的实际看台情况(由看台排班带出后再调整:车位、时长、代班、不计产量)。
/// <summary>车头工看台记录(biz_prod_qs_ktjl_ctg)</summary>
[Table(Name = "KtjlCtgs")]
public class KtjlCtg : BaseEntity
{
public DateTime Date { get; set; } // 日期
public long DeptId { get; set; } // 车间
public Prod_Banci_Enum Banci { get; set; } // 班次(甲/乙)
public int Jizu { get; set; } // 机组
public decimal Hours { get; set; } // 时长(小时)
public long EmployeeId { get; set; } // 车头工
public long EmployeeDepId { get; set; } // 职工所属部门
public long WorktypeId { get; set; } // 工种
public string WorktypeName { get; set; } = ""; // 工种名称(历史快照)
public bool NoChanliang { get; set; } // 不计产量
}
/// <summary>挡车工看台记录(biz_prod_qs_ktjl_dcg)</summary>
[Table(Name = "KtjlDcgs")]
public class KtjlDcg : BaseEntity
{
public DateTime Date { get; set; } // 日期
public long DeptId { get; set; } // 车间
public Prod_Banci_Enum Banci { get; set; } // 班次
public int Jizu { get; set; } // 机组
public int ChetaiStart { get; set; } // 开始车位
public int ChetaiEnd { get; set; } // 结束车位
public decimal DangcheNum { get; set; } // 挡车数(边界车位与他人共用时各计 0.5)
public decimal Hours { get; set; } // 时长(小时)
public long EmployeeId { get; set; } // 挡车工
public long EmployeeDepId { get; set; } // 职工所属部门
public long WorktypeId { get; set; } // 工种
public string WorktypeName { get; set; } = ""; // 工种名称(历史快照)
public bool NoChanliang { get; set; } // 不计产量
}
@@ -737,14 +737,16 @@ public static class DbSeeder
("机组车台", "/prod/qianxian/jizuchetai", "base/jizuchetai/index", "prod:jizuchetai:list", "SetUp", 1),
("落丝记录", "/prod/qianxian/lsjl", "prod/lsjl/index", "prod:lsjl:list", "Document", 2),
("车间班次", "/prod/qianxian/chejianbanci", "base/chejianbanci/index", "prod:chejianbanci:list", "Timer", 3),
("看台排班", "/prod/qianxian/ktpb", "prod/ktpb/index", "prod:ktpb:list", "Grid", 4)
("看台排班", "/prod/qianxian/ktpb", "prod/ktpb/index", "prod:ktpb:list", "Grid", 4),
("看台记录", "/prod/qianxian/ktjl", "prod/ktjl/index", "prod:ktjl:list", "Tickets", 5)
});
// 子菜单顺序归一(EnsureTopMenuWithChildren 只补新菜单,不调整已存在菜单的排序)
foreach (var (path, sort) in new (string Path, int Sort)[]
{
("/prod/qianxian/jizuchetai", 1), ("/prod/qianxian/lsjl", 2),
("/prod/qianxian/chejianbanci", 3), ("/prod/qianxian/ktpb", 4)
("/prod/qianxian/chejianbanci", 3), ("/prod/qianxian/ktpb", 4),
("/prod/qianxian/ktjl", 5)
})
{
fsql.Update<SysMenu>()
@@ -8,6 +8,7 @@ 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.Infrastructure.Data;
@@ -24,6 +25,8 @@ public static class ProdSeedData
private const string KtpbCtgResource = "F9WebAiTest.Infrastructure.Data.SeedData.biz_prod_qs_ktpb_ctg.json";
private const string KtpbDcgResource = "F9WebAiTest.Infrastructure.Data.SeedData.biz_prod_qs_ktpb_dcg.json";
private const string KtpbZbzResource = "F9WebAiTest.Infrastructure.Data.SeedData.biz_prod_qs_ktpb_zbz.json";
private const string KtjlCtgResource = "F9WebAiTest.Infrastructure.Data.SeedData.biz_prod_qs_ktjl_ctg.csv";
private const string KtjlDcgResource = "F9WebAiTest.Infrastructure.Data.SeedData.biz_prod_qs_ktjl_dcg.csv";
private static readonly JsonSerializerOptions JsonOpts = new()
{
@@ -39,6 +42,9 @@ public static class ProdSeedData
SeedKtpbCtg(fsql);
SeedKtpbDcg(fsql);
SeedKtpbZbz(fsql);
// 看台记录(历史流水,一次性导入)
SeedKtjlCtg(fsql);
SeedKtjlDcg(fsql);
}
// ==================== 机组车台 ====================
@@ -310,6 +316,146 @@ public static class ProdSeedData
}
}
// ==================== 看台记录 ====================
/// <summary>
/// 车头工看台记录(历史流水):一次性导入,表内已有数据则跳过。
/// 数据格式:date|depId|banci|jizu|hours|employeeId|employeeDepId|worktypeId|noChanliang
/// </summary>
private static void SeedKtjlCtg(IFreeSql fsql)
{
try
{
if (fsql.Select<KtjlCtg>().Any()) { return; }
var lines = LoadCsv(KtjlCtgResource);
if (lines.Count == 0) return;
var deptMap = BuildDeptMap(fsql);
var empIds = BuildEmpIdSet(fsql);
var wtNames = fsql.Select<SysWt.WorkType>().ToList().ToDictionary(w => w.Id, w => w.WorkName);
var batch = new List<KtjlCtg>(1000);
int added = 0, skipped = 0;
foreach (var line in lines)
{
var f = line.Split('|');
if (f.Length < 9) { skipped++; continue; }
if (!DateTime.TryParse(f[0], out var date)) { skipped++; continue; }
if (!deptMap.TryGetValue(long.Parse(f[1]), out var depId)) { skipped++; continue; }
if (!empIds.Contains(long.Parse(f[5]))) { skipped++; continue; }
var wtId = long.Parse(f[7]);
batch.Add(new KtjlCtg
{
Date = date.Date,
DeptId = depId,
Banci = (Prod_Banci_Enum)int.Parse(f[2]),
Jizu = int.Parse(f[3]),
Hours = decimal.Parse(f[4]),
EmployeeId = long.Parse(f[5]),
EmployeeDepId = deptMap.TryGetValue(long.Parse(f[6]), out var ed) ? ed : depId,
WorktypeId = wtId,
WorktypeName = wtNames.GetValueOrDefault(wtId, ""),
NoChanliang = f[8] == "1" || f[8].Equals("true", StringComparison.OrdinalIgnoreCase)
});
if (batch.Count >= 1000)
{
fsql.Insert(batch).ExecuteAffrows();
added += batch.Count;
batch.Clear();
}
}
if (batch.Count > 0)
{
fsql.Insert(batch).ExecuteAffrows();
added += batch.Count;
}
Console.WriteLine($"[ProdSeed] 车头工看台记录:来源 {lines.Count} 行,导入 {added} 行,跳过 {skipped} 行");
}
catch (Exception ex)
{
Console.WriteLine($"[ProdSeed] 车头工看台记录导入跳过:{ex.Message}");
}
}
/// <summary>
/// 挡车工看台记录(历史流水):一次性导入,表内已有数据则跳过。
/// 数据格式:date|depId|banci|jizu|chetaiStart|chetaiEnd|dangcheNum|hours|employeeId|employeeDepId|worktypeId|noChanliang
/// </summary>
private static void SeedKtjlDcg(IFreeSql fsql)
{
try
{
if (fsql.Select<KtjlDcg>().Any()) { return; }
var lines = LoadCsv(KtjlDcgResource);
if (lines.Count == 0) return;
var deptMap = BuildDeptMap(fsql);
var empIds = BuildEmpIdSet(fsql);
var wtNames = fsql.Select<SysWt.WorkType>().ToList().ToDictionary(w => w.Id, w => w.WorkName);
var batch = new List<KtjlDcg>(1000);
int added = 0, skipped = 0;
foreach (var line in lines)
{
var f = line.Split('|');
if (f.Length < 12) { skipped++; continue; }
if (!DateTime.TryParse(f[0], out var date)) { skipped++; continue; }
if (!deptMap.TryGetValue(long.Parse(f[1]), out var depId)) { skipped++; continue; }
if (!empIds.Contains(long.Parse(f[8]))) { skipped++; continue; }
var wtId = long.Parse(f[10]);
batch.Add(new KtjlDcg
{
Date = date.Date,
DeptId = depId,
Banci = (Prod_Banci_Enum)int.Parse(f[2]),
Jizu = int.Parse(f[3]),
ChetaiStart = int.Parse(f[4]),
ChetaiEnd = int.Parse(f[5]),
DangcheNum = decimal.Parse(f[6]),
Hours = decimal.Parse(f[7]),
EmployeeId = long.Parse(f[8]),
EmployeeDepId = deptMap.TryGetValue(long.Parse(f[9]), out var ed) ? ed : depId,
WorktypeId = wtId,
WorktypeName = wtNames.GetValueOrDefault(wtId, ""),
NoChanliang = f[11] == "1" || f[11].Equals("true", StringComparison.OrdinalIgnoreCase)
});
if (batch.Count >= 1000)
{
fsql.Insert(batch).ExecuteAffrows();
added += batch.Count;
batch.Clear();
}
}
if (batch.Count > 0)
{
fsql.Insert(batch).ExecuteAffrows();
added += batch.Count;
}
Console.WriteLine($"[ProdSeed] 挡车工看台记录:来源 {lines.Count} 行,导入 {added} 行,跳过 {skipped} 行");
}
catch (Exception ex)
{
Console.WriteLine($"[ProdSeed] 挡车工看台记录导入跳过:{ex.Message}");
}
}
private static List<string> LoadCsv(string resourceName)
{
var asm = Assembly.GetExecutingAssembly();
using var stream = asm.GetManifestResourceStream(resourceName);
if (stream == null) return new List<string>();
using var reader = new StreamReader(stream);
var list = new List<string>();
while (!reader.EndOfStream)
{
var line = reader.ReadLine();
if (!string.IsNullOrWhiteSpace(line)) list.Add(line);
}
return list;
}
// ==================== 公共 ====================
/// <summary>本系统存在的职工 Id 集合(F9 职工导出时保留了原始雪花 Id,可直接比对)</summary>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -15,6 +15,8 @@
<ItemGroup>
<!-- 工种基础数据种子(来源于 F9 系统 biz_base_worktype 三张表导出) -->
<EmbeddedResource Include="Data\SeedData\*.json" />
<!-- 看台记录历史流水(体量较大,采用紧凑 CSV 内嵌) -->
<EmbeddedResource Include="Data\SeedData\*.csv" />
<!-- 报表中心示例 .grf 模板(GR6.8.5.0 格式;GR7 设计器向下兼容,可直接打开编辑) -->
<EmbeddedResource Include="Assets\ReportTemplates\*.grf" />
</ItemGroup>