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

功能:
- 后端新增 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
@@ -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>