Files
f9-web-ai-test/backend/F9WebAiTest.Application/Services/PayRoll/CalcLdcctService.cs
T
yanpeng200212 1f1b184411 新增工资/仓储/生产日报等模块,移除旧考勤模块
- 新增 PayRoll(工资)、Wms(仓储)、Prod(车速/缫丝/考勤/丝片检测/丝片台账/纤检日报/质量日报) 模块
- 新增对应 Domain 实体、Application DTO/Service、Api Controller 与前端页面
- 移除旧 Attendance 考勤模块(Controller/Service/Entity/DTO 及 5 个前端页面),由 Prod/Kaoqin 取代
- 调整路由、仪表盘、Lsjl 流丝记录实体与服务
- 更新业务种子数据与 DbSeeder
- 补充工作进度文档 14-27
2026-10-27 09:09:57 +08:00

820 lines
44 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.PayRoll;
using F9WebAiTest.Domain.Common;
using F9WebAiTest.Domain.Entities.Base;
using F9WebAiTest.Domain.Entities.PayRoll;
using F9WebAiTest.Domain.Entities.Prod;
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.PayRoll;
/// <summary>
/// 薪资计算-立缫挡车车头(挡车工 / 车头工 产量计算)
/// 对应 F9 WinClient\F9.Business.PayRoll\PAYROLL\CALCLDCCT(Page_CalcLdcct + Page_CalcLdcct_Edit)
/// 数据源:看台记录(挡车/车头)、煮茧日报(今缫桶数)、落丝记录(茧量)、秤大丝(丝量/缫折)、
/// 纤度检验(纤度明细/均方差/总差)、黑板检验(二度/三度/清洁/洁净)、生丝规格、生丝定级指标。
/// 配置:工艺庄口薪资配置、工种薪资配置、挡车岗位薪资配置、挡车纤度奖扣、薪资价格配置。
/// </summary>
public interface ICalcLdcctService
{
Task<PageResult<CalcLdcctListItemDto>> GetPagedAsync(int pageIndex, int pageSize, DateTime start, DateTime end,
long? depId, long? worktypeId, string like);
Task<CalcLdcctResultDto> CalcDcgAsync(CalcLdcctCalcDto dto);
Task<CalcLdcctResultDto> CalcCtgAsync(CalcLdcctCalcDto dto);
Task<int> SaveAsync(CalcLdcctSaveDto dto);
Task DeleteAsync(DateTime start, DateTime end, long depId, long worktypeId);
}
public class CalcLdcctService : ICalcLdcctService
{
private readonly IRepository<CalcLdcct> _repo;
private readonly IRepository<KtjlDcg> _dcgRepo;
private readonly IRepository<KtjlCtg> _ctgRepo;
private readonly IRepository<ZjrbRecord> _zjrbRepo;
private readonly IRepository<ZjrbJizu> _zjrbJizuRepo;
private readonly IRepository<LsjlRecord> _lsjlRepo;
private readonly IRepository<CdsLsjl> _cdsLsjlRepo;
private readonly IRepository<CdsChetai> _cdsChetaiRepo;
private readonly IRepository<XdjyLsjl> _xdjyLsjlRepo;
private readonly IRepository<XdjyRecord> _xdjyRecordRepo;
private readonly IRepository<HbjyRecord> _hbjyRepo;
private readonly IRepository<ConfGyzk> _confGyzkRepo;
private readonly IRepository<ConfGz> _confGzRepo;
private readonly IRepository<ConfDcg> _confDcgRepo;
private readonly IRepository<DangCheXianDuJiangKou> _xdjkRepo;
private readonly IRepository<XinZiJiaGePeiZhi> _xzjgRepo;
private readonly IRepository<ShengsiGuige> _guigeRepo;
private readonly IRepository<ShengsiDingjizhibiao> _ssdjzbRepo;
private readonly IRepository<CraftZhuangKou> _zkRepo;
private readonly IRepository<SysDept.Department> _depRepo;
private readonly IRepository<SysEmp.Employee> _empRepo;
private readonly IRepository<SysWt.WorkType> _wtRepo;
public CalcLdcctService(
IRepository<CalcLdcct> repo,
IRepository<KtjlDcg> dcgRepo,
IRepository<KtjlCtg> ctgRepo,
IRepository<ZjrbRecord> zjrbRepo,
IRepository<ZjrbJizu> zjrbJizuRepo,
IRepository<LsjlRecord> lsjlRepo,
IRepository<CdsLsjl> cdsLsjlRepo,
IRepository<CdsChetai> cdsChetaiRepo,
IRepository<XdjyLsjl> xdjyLsjlRepo,
IRepository<XdjyRecord> xdjyRecordRepo,
IRepository<HbjyRecord> hbjyRepo,
IRepository<ConfGyzk> confGyzkRepo,
IRepository<ConfGz> confGzRepo,
IRepository<ConfDcg> confDcgRepo,
IRepository<DangCheXianDuJiangKou> xdjkRepo,
IRepository<XinZiJiaGePeiZhi> xzjgRepo,
IRepository<ShengsiGuige> guigeRepo,
IRepository<ShengsiDingjizhibiao> ssdjzbRepo,
IRepository<CraftZhuangKou> zkRepo,
IRepository<SysDept.Department> depRepo,
IRepository<SysEmp.Employee> empRepo,
IRepository<SysWt.WorkType> wtRepo)
{
_repo = repo; _dcgRepo = dcgRepo; _ctgRepo = ctgRepo;
_zjrbRepo = zjrbRepo; _zjrbJizuRepo = zjrbJizuRepo; _lsjlRepo = lsjlRepo;
_cdsLsjlRepo = cdsLsjlRepo; _cdsChetaiRepo = cdsChetaiRepo;
_xdjyLsjlRepo = xdjyLsjlRepo; _xdjyRecordRepo = xdjyRecordRepo; _hbjyRepo = hbjyRepo;
_confGyzkRepo = confGyzkRepo; _confGzRepo = confGzRepo; _confDcgRepo = confDcgRepo;
_xdjkRepo = xdjkRepo; _xzjgRepo = xzjgRepo; _guigeRepo = guigeRepo; _ssdjzbRepo = ssdjzbRepo;
_zkRepo = zkRepo; _depRepo = depRepo; _empRepo = empRepo; _wtRepo = wtRepo;
}
// ==================== 常量(对齐 F9 Page_CalcLdcct) ====================
private const decimal XushuPerTai = 20m; // 每台绪数
private const decimal CtgTaiShu = 20m; // 车头工台时按 20 台/组
private const decimal CtgHoursDiv = 9m; // 车头基础单价 /9(标准上班时长)
private const decimal CtgRenShu = 2m; // 车头标准人数 2
private const decimal DengjiStep = 0.1m; // 等级分步长
private const int SaozheDeadZone = 2; // 缫折死区 ±2kg
private const int DcgSaozheCap = 10; // 挡车缫折封顶 ±10
private const int CtgSaozheCap = 3; // 车头缫折封顶 ±3
private const string PriceGroup = "薪资.前缫";
private const string DcgDengjiPriceKey = "挡车工计件等级奖扣单价";
private const string CtgDengjiPriceKey = "车头工计件等级奖扣单价";
// ==================== 查询 ====================
public async Task<PageResult<CalcLdcctListItemDto>> GetPagedAsync(int pageIndex, int pageSize, DateTime start,
DateTime end, long? depId, long? worktypeId, string like)
{
var all = await _repo.GetAllAsync();
var from = start.Date;
var to = end.Date;
var q = all.AsQueryable().Where(x => x.Date.Date >= from && x.Date.Date <= to);
if (depId.HasValue) q = q.Where(x => x.DepId == depId.Value);
if (worktypeId.HasValue) q = q.Where(x => x.WorktypeId == worktypeId.Value);
var deps = await _depRepo.GetAllAsync();
var emps = await _empRepo.GetAllAsync();
var wts = await _wtRepo.GetAllAsync();
var zks = await _zkRepo.GetAllAsync();
var confGyzks = await _confGyzkRepo.GetAllAsync();
var depDict = deps.ToDictionary(d => d.Id, d => d.Name);
var empDict = emps.ToDictionary(e => e.Id, e => e.Name);
var wtDict = wts.ToDictionary(w => w.Id, w => w.WorkName);
var zkDict = zks.ToDictionary(z => z.Id, z => z.ZhuangKouName);
var confDict = confGyzks.GroupBy(c => $"{c.Year}-{c.Month}-{c.GyzkId}")
.ToDictionary(g => g.Key, g => g.OrderByDescending(c => c.Id).First());
if (!string.IsNullOrWhiteSpace(like))
{
var empIds = emps.Where(e => e.Name.Contains(like)).Select(e => e.Id).ToHashSet();
var zkIds = zks.Where(z => z.ZhuangKouName.Contains(like)).Select(z => z.Id).ToHashSet();
q = q.Where(x => empIds.Contains(x.EmployeeId) || zkIds.Contains(x.GyzkId));
}
var total = q.LongCount();
var items = q.OrderBy(x => x.Date).ThenBy(x => x.Jizu).ThenBy(x => x.Banci).ThenBy(x => x.EmployeeId)
.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
return new PageResult<CalcLdcctListItemDto>
{
Items = items.Select(x =>
{
confDict.TryGetValue($"{x.Date.Year}-{x.Date.Month}-{x.GyzkId}", out var cfg);
return new CalcLdcctListItemDto
{
Id = x.Id, Date = x.Date, EmployeeId = x.EmployeeId,
EmployeeName = empDict.GetValueOrDefault(x.EmployeeId, ""),
DepId = x.DepId, DepName = depDict.GetValueOrDefault(x.DepId, ""),
WorktypeId = x.WorktypeId, WorktypeName = wtDict.GetValueOrDefault(x.WorktypeId, ""),
GyzkId = x.GyzkId, GyzkName = zkDict.GetValueOrDefault(x.GyzkId, ""),
Guige = x.Guige, Chetai = x.Chetai, Jizu = x.Jizu, Banci = x.Banci,
BanciName = BanciName(x.Banci),
HoursKantai = x.HoursKantai, Dangcheshu = x.Dangcheshu,
Dengji = x.Dengji, Dengjifen = x.Dengjifen,
GyzkTaichan = cfg?.QsTaichan ?? 0, GyzkSaozhe = cfg?.SaoZhe ?? 0,
ClDanjiaBase = x.ClDanjiaBase, ClDanjiaDengji = x.ClDanjiaDengji,
ClDanjiaActual = x.ClDanjiaActual, ClSiliang = x.ClSiliang,
ZlSaozhe = x.ZlSaozhe, ZlCenterxd = x.ZlCenterxd, ZlYexiannum = x.ZlYexiannum,
ZlQingjie = x.ZlQingjie, ZlJiejing = x.ZlJiejing, ZlErdu = x.ZlErdu, ZlSandu = x.ZlSandu,
JineCl_Original = x.JineCl_Original, Xishu_JineCl = x.Xishu_JineCl, JineCl = x.JineCl,
JineZlSaozhe = x.JineZlSaozhe, JineZlCenterxd = x.JineZlCenterxd, JineZlYexian = x.JineZlYexian,
JineZlQingjie = x.JineZlQingjie, JineZlJiejing = x.JineZlJiejing,
JineZlErdu = x.JineZlErdu, JineZlSandu = x.JineZlSandu,
JineZlTotal = x.JineZlSaozhe + x.JineZlCenterxd + x.JineZlYexian + x.JineZlQingjie
+ x.JineZlJiejing + x.JineZlErdu + x.JineZlSandu
};
}).ToList(),
Total = total, PageIndex = pageIndex, PageSize = pageSize
};
}
// ==================== 一键计算:挡车工 ====================
public async Task<CalcLdcctResultDto> CalcDcgAsync(CalcLdcctCalcDto dto)
{
var ctx = await BuildContextAsync(dto);
var result = new CalcLdcctResultDto();
if (ctx.DcgKtjl.Count == 0)
{
result.Warnings.Add("该账期该部门没有挡车工看台记录");
return result;
}
var rows = new List<CalcLdcct>();
// 按「日 + 机组」组织
var jizuDays = ctx.DcgKtjl.GroupBy(k => (k.Date.Date, k.Jizu));
foreach (var jd in jizuDays)
{
var (date, jizu) = jd.Key;
var ktjls = jd.Where(k => !k.NoChanliang).ToList();
if (ktjls.Count == 0) continue;
// 该机组当日各庄口的今缫桶数(煮茧日报不分班)
var gyzkTongshu = ctx.GyzkTongshuByJizuDay.TryGetValue((date, jizu), out var gt)
? gt : new List<(long GyzkId, string Guige, int Tongshu, int StdLuosi)>();
var tongshuSum = gyzkTongshu.Sum(g => g.Tongshu);
var banciNum = ktjls.Select(k => k.Banci).Distinct().Count();
var half = banciNum >= 2 ? 0.5m : 1m;
foreach (var (gyzkId, guige, tongshu, stdLuosi) in gyzkTongshu)
{
var cfg = ctx.ConfGyzk(gyzkId, date);
if (cfg == null)
{
result.Warnings.Add($"{date:yyyy-MM-dd} 机组 {jizu} 庄口缺少薪资配置,已跳过");
continue;
}
var guigeCfg = ctx.Guige(guige);
// 每桶丝量 = 每车丝量(kg→g) / 标准落丝桶数
if (stdLuosi <= 0 || cfg.MeiCheSiLiang <= 0)
{
result.Warnings.Add($"{date:yyyy-MM-dd} 庄口 {cfg.Guige} 缺少每车丝量/标准落丝桶数,已跳过");
continue;
}
var meitongSiliang = cfg.MeiCheSiLiang * 1000m / stdLuosi;
var jiayiSumSiliang = Math.Round(tongshu * meitongSiliang, 0, MidpointRounding.AwayFromZero);
var rate = tongshuSum > 0 ? (decimal)tongshu / tongshuSum : 0m;
// 按班次拆分丝量:按 台时 = 挡车数 × 时长 加权
var jizuDayTaishi = ktjls.Sum(k => k.DangcheNum * k.Hours);
foreach (var banciGroup in ktjls.GroupBy(k => k.Banci))
{
var banciTaishi = banciGroup.Sum(k => k.DangcheNum * k.Hours);
var banciSiliang = jizuDayTaishi > 0
? Math.Round(jiayiSumSiliang * (banciTaishi / jizuDayTaishi), 0, MidpointRounding.AwayFromZero)
: 0m;
var groupTaishi = banciTaishi;
foreach (var k in banciGroup)
{
// 看台时长:按庄口今缫桶数占比分摊个人工时
var hoursKantai = Math.Round(k.Hours * rate, 1, MidpointRounding.AwayFromZero);
var siliang = groupTaishi > 0
? Math.Round(banciSiliang * (k.DangcheNum * k.Hours / groupTaishi), 0, MidpointRounding.AwayFromZero)
: 0m;
// ---- 质量(按车台看台比例分摊) ----
var quality = CalcDcgQuality(ctx, date, jizu, gyzkId, k, half, cfg, guigeCfg);
// ---- 等级(木桶短板) ----
var (dengji, dengjifen) = CalDengJi(ctx, guige, quality.Junfangcha, quality.Zongcha,
quality.QingjieFen, quality.JiejingFen, quality.Erdu, quality.Sandu, cfg);
// ---- 单价 ----
var xushu = k.DangcheNum * XushuPerTai;
var gangweiBase = GetDcgDanJiaBase(xushu, ctx.ConfDcg);
if (ctx.ConfGz == null || ctx.ConfGz.QqDaysStandard <= 0 || ctx.ConfGz.HoursPerday <= 0
|| cfg.QsTaichan <= 0 || k.DangcheNum <= 0)
{
result.Warnings.Add($"工种薪资配置或庄口台产缺失,{date:yyyy-MM-dd} {k.EmployeeId} 已跳过");
continue;
}
var danjiaBase = Math.Round(gangweiBase / ctx.ConfGz.QqDaysStandard / k.DangcheNum
/ ctx.ConfGz.HoursPerday / cfg.QsTaichan, 7, MidpointRounding.AwayFromZero);
if (danjiaBase == 0) continue;
var danjiaDengji = Math.Round(
(dengjifen - cfg.QsDengjifen) / DengjiStep * ctx.DcgJjDengJiJkPrice / 10m / 1000m,
7, MidpointRounding.AwayFromZero);
var danjiaActual = Math.Round(danjiaBase + danjiaDengji, 7, MidpointRounding.AwayFromZero);
// ---- 金额 ----
var e = new CalcLdcct
{
Date = date, EmployeeId = k.EmployeeId, DepId = dto.DepId, WorktypeId = dto.WorktypeId,
Jizu = jizu, Banci = (int)k.Banci, GyzkId = gyzkId, Guige = guige,
Chetai = k.ChetaiStart == k.ChetaiEnd ? k.ChetaiStart.ToString() : $"{k.ChetaiStart}-{k.ChetaiEnd}",
HoursKantai = hoursKantai, Dangcheshu = k.DangcheNum, Dengji = dengji, Dengjifen = dengjifen,
ClDanjiaBase = danjiaBase, ClDanjiaDengji = danjiaDengji, ClDanjiaActual = danjiaActual,
ClSiliang = siliang,
ZlSaozhe = quality.Saozhe, ZlCenterxd = quality.CenterNum, ZlYexiannum = quality.YexianNum,
ZlQingjie = quality.QingjiePianshu, ZlJiejing = quality.JiejingPianshu,
ZlErdu = quality.Erdu, ZlSandu = quality.Sandu,
Xishu_JineCl = cfg.DcgJijianXishu
};
e.JineCl_Original = Math.Round(danjiaActual * siliang, 2, MidpointRounding.AwayFromZero);
e.JineCl = Math.Round(danjiaActual * siliang * cfg.DcgJijianXishu, 2, MidpointRounding.AwayFromZero);
e.JineZlSaozhe = CalJineSaozhe(cfg.SaoZhe, quality.Saozhe, siliang, DcgSaozheCap, 5m);
e.JineZlCenterxd = Math.Round(quality.CenterNum * 1m, 2, MidpointRounding.AwayFromZero);
e.JineZlYexian = -Math.Round(quality.YexianKouKuan, 2, MidpointRounding.AwayFromZero);
e.JineZlQingjie = -Math.Round(quality.QingjieKouKuan, 2, MidpointRounding.AwayFromZero);
e.JineZlJiejing = -Math.Round(quality.JiejingKouKuan, 2, MidpointRounding.AwayFromZero);
e.JineZlErdu = -Math.Round(quality.Erdu * 2m, 2, MidpointRounding.AwayFromZero);
e.JineZlSandu = -Math.Round(quality.Sandu * 20m, 2, MidpointRounding.AwayFromZero);
rows.Add(e);
}
}
}
}
return await PersistAsync(rows, dto, result);
}
// ==================== 一键计算:车头工 ====================
public async Task<CalcLdcctResultDto> CalcCtgAsync(CalcLdcctCalcDto dto)
{
var ctx = await BuildContextAsync(dto);
var result = new CalcLdcctResultDto();
if (ctx.CtgKtjl.Count == 0)
{
result.Warnings.Add("该账期该部门没有车头工看台记录");
return result;
}
// 车头工强依赖已存的挡车工产量
var dcgRows = (await _repo.GetAllAsync())
.Where(x => x.Date.Date >= dto.StartDate.Date && x.Date.Date <= dto.EndDate.Date
&& x.DepId == dto.DepId && x.WorktypeId == ctx.DcgWorktypeId).ToList();
var rows = new List<CalcLdcct>();
var jizuDays = ctx.CtgKtjl.GroupBy(k => (k.Date.Date, k.Jizu));
foreach (var jd in jizuDays)
{
var (date, jizu) = jd.Key;
var ktjls = jd.Where(k => !k.NoChanliang).ToList();
if (ktjls.Count == 0) continue;
var gyzkTongshu = ctx.GyzkTongshuByJizuDay.TryGetValue((date, jizu), out var gt)
? gt : new List<(long GyzkId, string Guige, int Tongshu, int StdLuosi)>();
var tongshuSum = gyzkTongshu.Sum(g => g.Tongshu);
var banciNum = ktjls.Select(k => k.Banci).Distinct().Count();
var half = banciNum >= 2 ? 0.5m : 1m;
foreach (var (gyzkId, guige, tongshu, stdLuosi) in gyzkTongshu)
{
var cfg = ctx.ConfGyzk(gyzkId, date);
if (cfg == null)
{
result.Warnings.Add($"{date:yyyy-MM-dd} 机组 {jizu} 庄口缺少薪资配置,已跳过");
continue;
}
if (stdLuosi <= 0 || cfg.MeiCheSiLiang <= 0)
{
result.Warnings.Add($"{date:yyyy-MM-dd} 庄口 {cfg.Guige} 缺少每车丝量/标准落丝桶数,已跳过");
continue;
}
var meitongSiliang = cfg.MeiCheSiLiang * 1000m / stdLuosi;
var jiayiSumSiliang = Math.Round(tongshu * meitongSiliang, 0, MidpointRounding.AwayFromZero);
var rate = tongshuSum > 0 ? (decimal)tongshu / tongshuSum : 0m;
// 全组汇总质量(落丝主表,不分摊)
var lsjlIds = ctx.LsjlIdsByJizuDayGyzk.TryGetValue((date, jizu, gyzkId), out var ids)
? ids : new List<long>();
var saozheList = new List<decimal>();
var erduSum = 0m; var sanduSum = 0m;
foreach (var lid in lsjlIds)
{
if (ctx.CdsLsjl.TryGetValue(lid, out var cds) && cds.Siliang > 0 && cds.Jianliang > 0)
saozheList.Add((decimal)cds.Jianliang * 100m / cds.Siliang);
if (ctx.HbjyByLsjl.TryGetValue(lid, out var hb))
{
erduSum += hb.Sum(h => h.YdErdu);
sanduSum += hb.Sum(h => h.YdSandu);
}
}
var zlSaozhe = saozheList.Count > 0
? Math.Round(saozheList.Average(), 0, MidpointRounding.AwayFromZero) : cfg.SaoZhe;
var jizuDayTaishi = ktjls.Sum(k => CtgTaiShu * k.Hours);
foreach (var banciGroup in ktjls.GroupBy(k => k.Banci))
{
var banciTaishi = banciGroup.Sum(k => CtgTaiShu * k.Hours);
var banciSiliang = jizuDayTaishi > 0
? Math.Round(jiayiSumSiliang * (banciTaishi / jizuDayTaishi), 0, MidpointRounding.AwayFromZero)
: 0m;
var maxHours = banciGroup.Max(k => k.Hours);
foreach (var k in banciGroup)
{
var hoursKantai = Math.Round(k.Hours * rate, 1, MidpointRounding.AwayFromZero);
var siliang = maxHours > 0
? Math.Round(banciSiliang * (k.Hours / (maxHours * CtgRenShu)), 0, MidpointRounding.AwayFromZero)
: 0m;
// 等级:按挡车工丝量加权平均等级分再回落取整档
var (dengji, dengjifen) = CalCtgDengji(ctx, dcgRows, date, jizu, gyzkId, guige, cfg);
if (ctx.ConfGz == null || ctx.ConfGz.QqDaysStandard <= 0 || cfg.QsTaichan <= 0)
{
result.Warnings.Add($"工种薪资配置缺失,{date:yyyy-MM-dd} {k.EmployeeId} 已跳过");
continue;
}
var danjiaBase = Math.Round(ctx.ConfDcg.JineCtg / ctx.ConfGz.QqDaysStandard / CtgHoursDiv
/ CtgTaiShu / cfg.QsTaichan * CtgRenShu, 7, MidpointRounding.AwayFromZero);
if (danjiaBase == 0) continue;
var danjiaDengji = Math.Round(
(dengjifen - cfg.QsDengjifen) / DengjiStep * ctx.CtgJjDengJiJkPrice / 10m / 1000m,
7, MidpointRounding.AwayFromZero);
var danjiaActual = Math.Round(danjiaBase + danjiaDengji, 7, MidpointRounding.AwayFromZero);
var e = new CalcLdcct
{
Date = date, EmployeeId = k.EmployeeId, DepId = dto.DepId, WorktypeId = dto.WorktypeId,
Jizu = jizu, Banci = (int)k.Banci, GyzkId = gyzkId, Guige = guige, Chetai = "",
HoursKantai = hoursKantai, Dangcheshu = 0, Dengji = dengji, Dengjifen = dengjifen,
ClDanjiaBase = danjiaBase, ClDanjiaDengji = danjiaDengji, ClDanjiaActual = danjiaActual,
ClSiliang = siliang,
ZlSaozhe = zlSaozhe, ZlCenterxd = 0, ZlYexiannum = 0, ZlQingjie = 0, ZlJiejing = 0,
ZlErdu = Math.Round(erduSum * half, 2, MidpointRounding.AwayFromZero),
ZlSandu = Math.Round(sanduSum * half, 2, MidpointRounding.AwayFromZero),
Xishu_JineCl = cfg.CtgJijianXishu
};
e.JineCl_Original = Math.Round(danjiaActual * siliang, 2, MidpointRounding.AwayFromZero);
e.JineCl = Math.Round(danjiaActual * siliang * cfg.CtgJijianXishu, 2, MidpointRounding.AwayFromZero);
e.JineZlSaozhe = CalJineSaozhe(cfg.SaoZhe, zlSaozhe, siliang, CtgSaozheCap, 1m);
e.JineZlCenterxd = 0; e.JineZlYexian = 0; e.JineZlQingjie = 0; e.JineZlJiejing = 0;
e.JineZlErdu = -Math.Round(e.ZlErdu * 2m, 2, MidpointRounding.AwayFromZero);
e.JineZlSandu = -Math.Round(e.ZlSandu * 20m, 2, MidpointRounding.AwayFromZero);
rows.Add(e);
}
}
}
}
return await PersistAsync(rows, dto, result);
}
// ==================== 保存 / 删除 ====================
public async Task<int> SaveAsync(CalcLdcctSaveDto dto)
{
var olds = (await _repo.GetAllAsync())
.Where(x => x.Date.Date >= dto.StartDate.Date && x.Date.Date <= dto.EndDate.Date
&& x.DepId == dto.DepId && x.WorktypeId == dto.WorktypeId).ToList();
if (olds.Count > 0) await _repo.HardDeleteRangeAsync(olds);
foreach (var it in dto.Items)
{
var e = new CalcLdcct
{
Date = it.Date.Date, EmployeeId = it.EmployeeId, DepId = it.DepId, WorktypeId = it.WorktypeId,
Jizu = it.Jizu, Banci = it.Banci, GyzkId = it.GyzkId, Guige = it.Guige ?? "", Chetai = it.Chetai ?? "",
HoursKantai = it.HoursKantai, Dangcheshu = it.Dangcheshu, Dengji = it.Dengji ?? "", Dengjifen = it.Dengjifen,
ClDanjiaBase = it.ClDanjiaBase, ClDanjiaDengji = it.ClDanjiaDengji, ClDanjiaActual = it.ClDanjiaActual,
ClSiliang = it.ClSiliang,
ZlSaozhe = it.ZlSaozhe, ZlCenterxd = it.ZlCenterxd, ZlYexiannum = it.ZlYexiannum,
ZlQingjie = it.ZlQingjie, ZlJiejing = it.ZlJiejing, ZlErdu = it.ZlErdu, ZlSandu = it.ZlSandu,
JineCl_Original = it.JineCl_Original, Xishu_JineCl = it.Xishu_JineCl, JineCl = it.JineCl,
JineZlSaozhe = it.JineZlSaozhe, JineZlCenterxd = it.JineZlCenterxd, JineZlYexian = it.JineZlYexian,
JineZlQingjie = it.JineZlQingjie, JineZlJiejing = it.JineZlJiejing,
JineZlErdu = it.JineZlErdu, JineZlSandu = it.JineZlSandu
};
await _repo.AddAsync(e);
}
return dto.Items.Count;
}
public async Task DeleteAsync(DateTime start, DateTime end, long depId, long worktypeId)
{
var olds = (await _repo.GetAllAsync())
.Where(x => x.Date.Date >= start.Date && x.Date.Date <= end.Date
&& x.DepId == depId && x.WorktypeId == worktypeId).ToList();
if (olds.Count > 0) await _repo.HardDeleteRangeAsync(olds);
}
// ==================== 内部:上下文 ====================
private class CalcContext
{
public List<KtjlDcg> DcgKtjl = new();
public List<KtjlCtg> CtgKtjl = new();
public long DcgWorktypeId;
public Dictionary<(DateTime, int), List<(long GyzkId, string Guige, int Tongshu, int StdLuosi)>> GyzkTongshuByJizuDay = new();
public Dictionary<(DateTime, int, long), List<long>> LsjlIdsByJizuDayGyzk = new();
public Dictionary<long, CdsLsjl> CdsLsjl = new();
public Dictionary<long, List<CdsChetai>> CdsChetai = new();
public Dictionary<long, List<XdjyRecord>> XdjyByLsjl = new();
public Dictionary<long, List<HbjyRecord>> HbjyByLsjl = new();
public Dictionary<long, List<ConfGyzk>> ConfByGyzk = new();
public Dictionary<string, ShengsiGuige> GuigeDict = new();
public Dictionary<string, List<DangCheXianDuJiangKou>> YeXianJk = new();
public List<ShengsiDingjizhibiao> Djzb = new();
public ConfGz ConfGz = null!;
public ConfDcg ConfDcg = null!;
public decimal DcgJjDengJiJkPrice;
public decimal CtgJjDengJiJkPrice;
public List<string> Warnings = new();
public ConfGyzk ConfGyzk(long gyzkId, DateTime date) =>
ConfByGyzk.TryGetValue(gyzkId, out var list)
? list.FirstOrDefault(c => c.Year == date.Year && c.Month == date.Month)
?? list.OrderByDescending(c => c.Year).ThenByDescending(c => c.Month).FirstOrDefault()
: null;
public ShengsiGuige Guige(string guige) =>
GuigeDict.TryGetValue(guige ?? "", out var g) ? g : null;
public List<DangCheXianDuJiangKou> YeXianJiangKou(string guige) =>
YeXianJk.TryGetValue(guige ?? "", out var l) ? l : new List<DangCheXianDuJiangKou>();
}
private async Task<CalcContext> BuildContextAsync(CalcLdcctCalcDto dto)
{
var ctx = new CalcContext();
var from = dto.StartDate.Date;
var to = dto.EndDate.Date;
ctx.DcgKtjl = (await _dcgRepo.GetAllAsync())
.Where(x => x.Date.Date >= from && x.Date.Date <= to && x.DeptId == dto.DepId).ToList();
ctx.CtgKtjl = (await _ctgRepo.GetAllAsync())
.Where(x => x.Date.Date >= from && x.Date.Date <= to && x.DeptId == dto.DepId).ToList();
// 挡车工工种:取看台记录中出现最多的工种(车头计算需按挡车工工种取已存产量)
ctx.DcgWorktypeId = ctx.DcgKtjl.GroupBy(k => k.WorktypeId).OrderByDescending(g => g.Count())
.Select(g => g.Key).FirstOrDefault();
// 煮茧日报:日 + 机组 → 各庄口今缫桶数(不分班)
var zjrbs = (await _zjrbRepo.GetAllAsync())
.Where(x => x.Date.Date >= from && x.Date.Date <= to && x.DeptId == dto.DepId).ToList();
var zjrbJizus = await _zjrbJizuRepo.GetAllAsync();
foreach (var z in zjrbs)
{
foreach (var jz in zjrbJizus.Where(j => j.ZjrbId == z.Id))
{
var key = (z.Date.Date, jz.Jizu);
if (!ctx.GyzkTongshuByJizuDay.TryGetValue(key, out var list))
ctx.GyzkTongshuByJizuDay[key] = list = new List<(long, string, int, int)>();
list.Add((z.GyzkId, z.Guige, jz.TongshuJinsao, z.StandardLuositongshu));
}
}
// 落丝记录:日 + 机组 + 庄口 → lsjlId 列表
var lsjls = (await _lsjlRepo.GetAllAsync())
.Where(x => x.WorkDate.Date >= from && x.WorkDate.Date <= to && x.DeptId == dto.DepId).ToList();
foreach (var l in lsjls)
{
var key = (l.WorkDate.Date, l.Jizu, l.GyzkId);
if (!ctx.LsjlIdsByJizuDayGyzk.TryGetValue(key, out var list))
ctx.LsjlIdsByJizuDayGyzk[key] = list = new List<long>();
list.Add(l.Id);
}
var allLsjlIds = lsjls.Select(l => l.Id).ToHashSet();
ctx.CdsLsjl = (await _cdsLsjlRepo.GetAllAsync()).Where(x => allLsjlIds.Contains(x.LsjlId))
.GroupBy(x => x.LsjlId).ToDictionary(g => g.Key, g => g.First());
ctx.CdsChetai = (await _cdsChetaiRepo.GetAllAsync()).Where(x => allLsjlIds.Contains(x.LsjlId))
.GroupBy(x => x.LsjlId).ToDictionary(g => g.Key, g => g.ToList());
ctx.XdjyByLsjl = (await _xdjyRecordRepo.GetAllAsync()).Where(x => allLsjlIds.Contains(x.LsjlId))
.GroupBy(x => x.LsjlId).ToDictionary(g => g.Key, g => g.ToList());
ctx.HbjyByLsjl = (await _hbjyRepo.GetAllAsync()).Where(x => allLsjlIds.Contains(x.LsjlId))
.GroupBy(x => x.LsjlId).ToDictionary(g => g.Key, g => g.ToList());
_ = await _xdjyLsjlRepo.GetAllAsync(); // 纤度汇总(等级参考,保留实体引用)
ctx.ConfByGyzk = (await _confGyzkRepo.GetAllAsync())
.GroupBy(c => c.GyzkId).ToDictionary(g => g.Key, g => g.ToList());
ctx.GuigeDict = (await _guigeRepo.GetAllAsync())
.GroupBy(g => g.Guige).ToDictionary(g => g.Key, g => g.First());
ctx.Djzb = (await _ssdjzbRepo.GetAllAsync()).Where(x => x.Standard == 2).ToList(); // 2=个人
ctx.YeXianJk = (await _xdjkRepo.GetAllAsync())
.GroupBy(x => x.GuiGe).ToDictionary(g => g.Key, g => g.OrderBy(x => x.XianDu).ToList());
ctx.ConfGz = await _confGzRepo.FirstOrDefaultAsync(x => x.WorkTypeId == dto.WorktypeId);
ctx.ConfDcg = await _confDcgRepo.FirstOrDefaultAsync(x => x.DepId == dto.DepId)
?? new ConfDcg { DepId = dto.DepId };
ctx.DcgJjDengJiJkPrice = await PriceAsync(DcgDengjiPriceKey);
ctx.CtgJjDengJiJkPrice = await PriceAsync(CtgDengjiPriceKey);
return ctx;
}
private async Task<decimal> PriceAsync(string key)
{
var e = await _xzjgRepo.FirstOrDefaultAsync(x => x.Group == PriceGroup && x.Key == key);
return e?.Price ?? 0m;
}
private async Task<CalcLdcctResultDto> PersistAsync(List<CalcLdcct> rows, CalcLdcctCalcDto dto, CalcLdcctResultDto result)
{
await SaveAsync(new CalcLdcctSaveDto
{
StartDate = dto.StartDate, EndDate = dto.EndDate, DepId = dto.DepId, WorktypeId = dto.WorktypeId,
Items = rows.Select(r => new CalcLdcctSaveItemDto
{
Date = r.Date, EmployeeId = r.EmployeeId, DepId = r.DepId, WorktypeId = r.WorktypeId,
Jizu = r.Jizu, Banci = r.Banci, GyzkId = r.GyzkId, Guige = r.Guige, Chetai = r.Chetai,
HoursKantai = r.HoursKantai, Dangcheshu = r.Dangcheshu, Dengji = r.Dengji, Dengjifen = r.Dengjifen,
ClDanjiaBase = r.ClDanjiaBase, ClDanjiaDengji = r.ClDanjiaDengji, ClDanjiaActual = r.ClDanjiaActual,
ClSiliang = r.ClSiliang,
ZlSaozhe = r.ZlSaozhe, ZlCenterxd = r.ZlCenterxd, ZlYexiannum = r.ZlYexiannum,
ZlQingjie = r.ZlQingjie, ZlJiejing = r.ZlJiejing, ZlErdu = r.ZlErdu, ZlSandu = r.ZlSandu,
JineCl_Original = r.JineCl_Original, Xishu_JineCl = r.Xishu_JineCl, JineCl = r.JineCl,
JineZlSaozhe = r.JineZlSaozhe, JineZlCenterxd = r.JineZlCenterxd, JineZlYexian = r.JineZlYexian,
JineZlQingjie = r.JineZlQingjie, JineZlJiejing = r.JineZlJiejing,
JineZlErdu = r.JineZlErdu, JineZlSandu = r.JineZlSandu
}).ToList()
});
result.Count = rows.Count;
result.SumSiliang = rows.Sum(r => r.ClSiliang);
result.SumJineCl = Math.Round(rows.Sum(r => r.JineCl), 2, MidpointRounding.AwayFromZero);
result.SumJineZl = Math.Round(rows.Sum(r => r.JineZlSaozhe + r.JineZlCenterxd + r.JineZlYexian
+ r.JineZlQingjie + r.JineZlJiejing + r.JineZlErdu + r.JineZlSandu), 2, MidpointRounding.AwayFromZero);
return result;
}
// ==================== 内部:挡车工质量 ====================
private class DcgQuality
{
public decimal Saozhe;
public decimal CenterNum;
public decimal YexianNum;
public decimal YexianKouKuan;
public decimal QingjiePianshu;
public decimal QingjieKouKuan;
public decimal JiejingPianshu;
public decimal JiejingKouKuan;
public decimal Erdu;
public decimal Sandu;
public double Junfangcha;
public double Zongcha;
public decimal QingjieFen;
public decimal JiejingFen;
}
private DcgQuality CalcDcgQuality(CalcContext ctx, DateTime date, int jizu, long gyzkId, KtjlDcg k,
decimal half, ConfGyzk cfg, ShengsiGuige guigeCfg)
{
var q = new DcgQuality();
var lsjlIds = ctx.LsjlIdsByJizuDayGyzk.TryGetValue((date, jizu, gyzkId), out var ids)
? ids : new List<long>();
decimal jianliangNum = 0m, siliangDen = 0m;
double junfangchaSum = 0, zongchaSum = 0;
int junfangchaCnt = 0;
decimal erduSum = 0m, sanduSum = 0m;
decimal qjTdc = 0m, qjDc = 0m, qjCj = 0m;
decimal jj85 = 0m, jj80 = 0m, jj75 = 0m, jj70 = 0m;
decimal pianshuSum = 0m, qingjieDeduct = 0m, jiejingDeduct = 0m;
foreach (var lid in lsjlIds)
{
var chetais = ctx.CdsChetai.TryGetValue(lid, out var ct) ? ct : new List<CdsChetai>();
var cdsSum = ctx.CdsLsjl.TryGetValue(lid, out var cs) ? cs : null;
var xdjy = ctx.XdjyByLsjl.TryGetValue(lid, out var xd) ? xd : new List<XdjyRecord>();
var hbjy = ctx.HbjyByLsjl.TryGetValue(lid, out var hb) ? hb : new List<HbjyRecord>();
foreach (var ctItem in chetais)
{
if (ctItem.Chetaihao < k.ChetaiStart || ctItem.Chetaihao > k.ChetaiEnd) continue;
var rate = ChetaiRate(ctx, date, jizu, ctItem.Chetaihao, k);
// 缫折:茧量只记在落丝层,按「车台丝量占落丝丝量的比例」摊到车台,再看台比例摊到人
var lsjlSiliang = cdsSum?.Siliang ?? 0;
var lsjlJianliang = cdsSum?.Jianliang ?? 0;
var chetaiShare = lsjlSiliang > 0 ? (decimal)ctItem.Siliang / lsjlSiliang : 0m;
jianliangNum += lsjlJianliang * chetaiShare * rate;
siliangDen += ctItem.Siliang * rate;
// 中心纤度 / 野纤(纤度检验明细)
foreach (var x in xdjy.Where(x => x.Chetaihao == ctItem.Chetaihao))
{
var xds = new[] { x.Xd1, x.Xd2, x.Xd3, x.Xd4 }.Where(v => v > 0).ToArray();
if (guigeCfg != null)
{
q.CenterNum += xds.Count(v => Math.Abs(v - guigeCfg.CenterXiandu) < 0.0001) * rate;
q.YexianNum += xds.Count(v => v <= guigeCfg.XiaoyeLolimit || v >= guigeCfg.XiaoyeUplimit) * rate;
}
if (xds.Length > 0)
{
var avg = xds.Average();
var jfc = Math.Sqrt(xds.Sum(v => (v - avg) * (v - avg)) / xds.Length);
junfangchaSum += jfc; junfangchaCnt++;
var n = Math.Max(1, (int)Math.Ceiling(xds.Length * 0.02));
var sorted = xds.OrderBy(v => v).ToArray();
var minAvg = sorted.Take(n).Average();
var maxAvg = sorted.Skip(xds.Length - n).Average();
zongchaSum += Math.Max(Math.Abs(minAvg - avg), Math.Abs(maxAvg - avg));
}
}
// 黑板检验:二度/三度/清洁/洁净
foreach (var h in hbjy.Where(h => h.Chetaihao == ctItem.Chetaihao))
{
erduSum += h.YdErdu * rate;
sanduSum += h.YdSandu * rate;
qjTdc += h.QjZycdTdc * rate;
qjDc += h.QjCycdDc * rate;
qjCj += h.QjPtcdCj * rate;
jj85 += h.Jj85 * rate; jj80 += h.Jj80 * rate;
jj75 += h.Jj75 * rate; jj70 += h.Jj70 * rate;
pianshuSum += h.Pianshu * rate;
qingjieDeduct += (h.QjZycdTdc * 1.0m
+ (h.QjCycdFs + h.QjCycdDc + h.QjCycdNfc + h.QjCycdDcj + h.QjCycdZlx) * 0.4m
+ (h.QjPtcdXc + h.QjPtcdCj + h.QjPtcdLx + h.QjPtcdHjls) * 0.1m) * rate;
var jjCounts = new[] { h.Jj95, h.Jj90, h.Jj85, h.Jj80, h.Jj75, h.Jj70, h.Jj65, h.Jj60 };
for (var i = 0; i < jjCounts.Length; i++) jiejingDeduct += jjCounts[i] * (i + 1) * rate;
}
}
}
q.Saozhe = siliangDen > 0 ? Math.Round(jianliangNum * 100m / siliangDen, 0, MidpointRounding.AwayFromZero)
: cfg.SaoZhe;
q.Erdu = Math.Round(erduSum * half, 2, MidpointRounding.AwayFromZero);
q.Sandu = Math.Round(sanduSum * half, 2, MidpointRounding.AwayFromZero);
q.CenterNum = Math.Round(q.CenterNum * half, 2, MidpointRounding.AwayFromZero);
q.YexianNum = Math.Round(q.YexianNum * half, 2, MidpointRounding.AwayFromZero);
q.QingjiePianshu = Math.Round((qjTdc + qjDc + qjCj) * half, 2, MidpointRounding.AwayFromZero);
q.JiejingPianshu = Math.Round((jj85 + jj80 + jj75 + jj70) * half, 2, MidpointRounding.AwayFromZero);
q.QingjieKouKuan = (qjTdc * 2m + qjDc * 0.5m + qjCj * 2m) * half;
q.JiejingKouKuan = (jj85 * 0.2m + jj80 * 1m + jj75 * 2m + jj70 * 5m) * half;
q.YexianKouKuan = CalYeXianKouKuan(ctx, cfg.Guige, q.YexianNum);
q.Junfangcha = junfangchaCnt > 0 ? Math.Round(junfangchaSum / junfangchaCnt, 2) : 0;
q.Zongcha = junfangchaCnt > 0 ? Math.Round(zongchaSum / junfangchaCnt, 2) : 0;
q.QingjieFen = pianshuSum > 0
? Math.Round((pianshuSum - qingjieDeduct) * 100m / pianshuSum, 2, MidpointRounding.AwayFromZero) : 100m;
q.JiejingFen = pianshuSum > 0
? Math.Round((pianshuSum * 100m - jiejingDeduct * 5m) / pianshuSum, 2, MidpointRounding.AwayFromZero) : 100m;
return q;
}
/// <summary>车台看台比例 = 本人该台工时 / 该台所有看台人工时之和</summary>
private decimal ChetaiRate(CalcContext ctx, DateTime date, int jizu, int chetaihao, KtjlDcg k)
{
var same = ctx.DcgKtjl.Where(x => x.Date.Date == date && x.Jizu == jizu && x.Banci == k.Banci
&& x.ChetaiStart <= chetaihao && x.ChetaiEnd >= chetaihao).ToList();
var sum = same.Sum(x => x.Hours);
return sum > 0 ? k.Hours / sum : 0m;
}
/// <summary>野纤扣款:按规格取挡车纤度奖扣表,≤2 条取扣款最大的 2 条求和</summary>
private decimal CalYeXianKouKuan(CalcContext ctx, string guige, decimal yexianNum)
{
if (yexianNum <= 0) return 0m;
var list = ctx.YeXianJiangKou(guige);
if (list.Count == 0) return 0m;
var items = ExpandCount(yexianNum, list);
if (items.Count >= 2) return items.OrderByDescending(i => i.KouKuan).Take(2).Sum(i => i.KouKuan);
return items.Sum(i => i.KouKuan);
}
/// <summary>按数量展开:整数部分逐条取,小数部分按最后一条比例折算</summary>
private static List<DangCheXianDuJiangKou> ExpandCount(decimal count, List<DangCheXianDuJiangKou> list)
{
var result = new List<DangCheXianDuJiangKou>();
var intPart = (int)Math.Floor(count);
var frac = count - intPart;
for (var i = 0; i < intPart; i++) result.Add(list[i % list.Count]);
if (frac > 0) result.Add(list[intPart % list.Count]);
return result;
}
// ==================== 内部:等级 ====================
/// <summary>挡车工等级:木桶短板法,六项各自定级后取最低分</summary>
private (string Dengji, decimal Dengjifen) CalDengJi(CalcContext ctx, string guige, double piancha,
double dacha, decimal qingjie, decimal jiejing, decimal erdu, decimal sandu, ConfGyzk cfg)
{
var zbs = ctx.Djzb.Where(z => z.Guige == guige).ToList();
if (zbs.Count == 0) return (cfg.QsDengji, cfg.QsDengjifen);
var candidates = new List<ShengsiDingjizhibiao>();
AddBest(candidates, zbs.Where(z => z.Piancha >= piancha));
AddBest(candidates, zbs.Where(z => z.Dacha >= dacha));
AddBest(candidates, zbs.Where(z => z.Qingjie <= (double)qingjie));
AddBest(candidates, zbs.Where(z => z.Jiejing <= (double)jiejing));
AddBest(candidates, zbs.Where(z => z.Erdu >= (double)erdu));
AddBest(candidates, zbs.Where(z => z.Sandu >= (double)sandu));
if (candidates.Count == 0) return (cfg.QsDengji, cfg.QsDengjifen);
var worst = candidates.OrderBy(z => z.GradeScore).First();
return (worst.Grade, (decimal)worst.GradeScore);
}
private static void AddBest(List<ShengsiDingjizhibiao> target, IEnumerable<ShengsiDingjizhibiao> src)
{
var best = src.OrderByDescending(z => z.GradeScore).FirstOrDefault();
if (best != null) target.Add(best);
}
/// <summary>车头工等级:按挡车工丝量加权平均等级分后回落取整档</summary>
private (string Dengji, decimal Dengjifen) CalCtgDengji(CalcContext ctx, List<CalcLdcct> dcgRows,
DateTime date, int jizu, long gyzkId, string guige, ConfGyzk cfg)
{
var related = dcgRows.Where(x => x.Date.Date == date && x.Jizu == jizu && x.GyzkId == gyzkId).ToList();
var siliangSum = related.Sum(x => x.ClSiliang);
if (siliangSum <= 0) return (cfg.QsDengji, cfg.QsDengjifen);
var weighted = related.Sum(x => x.ClSiliang * x.Dengjifen) / siliangSum;
var zbs = ctx.Djzb.Where(z => z.Guige == guige && (decimal)z.GradeScore <= weighted)
.OrderByDescending(z => z.GradeScore).FirstOrDefault();
return zbs != null ? (zbs.Grade, (decimal)zbs.GradeScore) : (cfg.QsDengji, cfg.QsDengjifen);
}
// ==================== 内部:单价 / 金额 ====================
/// <summary>按绪数档取岗位基数金额:&lt;60→50档,&lt;70→60档,&lt;100→70档,否则 100 档</summary>
private static decimal GetDcgDanJiaBase(decimal xushu, ConfDcg conf)
{
if (xushu < 60m) return conf.JineJisuanjishu50;
if (xushu < 70m) return conf.JineJisuanjishu60;
if (xushu < 100m) return conf.JineJisuanjishu70;
return conf.JineJisuanjishu100;
}
/// <summary>缫折考核:±2kg 死区不奖不扣,超出部分按封顶 clamp,每 1kg 每 100kg 丝 奖扣 price 元</summary>
private static decimal CalJineSaozhe(decimal saoZheCfg, decimal saozhe, decimal siliang, int cap, decimal price)
{
if (Math.Abs(saoZheCfg - saozhe) <= SaozheDeadZone) return 0m;
var diff = saoZheCfg - saozhe;
diff = diff < 0 ? diff + SaozheDeadZone : diff - SaozheDeadZone;
if (diff > cap) diff = cap;
if (diff < -cap) diff = -cap;
return Math.Round(diff * (siliang / 1000m / 100m) * price, 2, MidpointRounding.AwayFromZero);
}
private static string BanciName(int banci) => banci switch { 1 => "甲", 2 => "乙", _ => banci.ToString() };
}