diff --git a/backend/F9WebAiTest.Api/Controllers/Attendance/AttendanceController.cs b/backend/F9WebAiTest.Api/Controllers/Attendance/AttendanceController.cs deleted file mode 100644 index 2ed70c5..0000000 --- a/backend/F9WebAiTest.Api/Controllers/Attendance/AttendanceController.cs +++ /dev/null @@ -1,103 +0,0 @@ -using F9WebAiTest.Application.Common; -using F9WebAiTest.Application.Dtos; -using F9WebAiTest.Application.Dtos.Attendance; -using F9WebAiTest.Application.Services; -using F9WebAiTest.Application.Services.Attendance; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace F9WebAiTest.Api.Controllers.Attendance; - -[ApiController] -[Route("api/attendance")] -[Authorize] -public class AttendanceController : ControllerBase -{ - private readonly IAttendanceService _service; - - public AttendanceController(IAttendanceService service) - { - _service = service; - } - - // POST /api/attendance/punch 打卡 - [HttpPost("punch")] - public async Task Punch() - { - var result = await _service.PunchAsync(GetUserId()); - return Ok(ApiResult.Ok(result)); - } - - // GET /api/attendance/my 我的考勤记录 - [HttpGet("my")] - public async Task GetMy([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 10) - { - var result = await _service.GetMyRecordsAsync(GetUserId(), pageIndex, pageSize); - return Ok(ApiResult>.Ok(result)); - } - - // GET /api/attendance/my/summary 我的考勤统计 - [HttpGet("my/summary")] - public async Task GetMySummary([FromQuery] string month) - { - var result = await _service.GetMySummaryAsync(GetUserId(), month); - return Ok(ApiResult.Ok(result)); - } - - // GET /api/attendance 全部考勤(管理员) - [HttpGet] - public async Task GetAll([FromQuery] AttendanceQueryDto query) - { - var result = await _service.GetAllRecordsAsync(query); - return Ok(ApiResult>.Ok(result)); - } - - // POST /api/attendance/leave 申请请假 - [HttpPost("leave")] - public async Task ApplyLeave([FromBody] CreateLeaveDto dto) - { - try - { - var result = await _service.ApplyLeaveAsync(GetUserId(), dto); - return Ok(ApiResult.Ok(result, "申请已提交")); - } - catch (ServiceException ex) - { - return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); - } - } - - // GET /api/attendance/leave/my 我的请假 - [HttpGet("leave/my")] - public async Task GetMyLeaves() - { - var result = await _service.GetMyLeavesAsync(GetUserId()); - return Ok(ApiResult>.Ok(result)); - } - - // GET /api/attendance/leave 全部请假(管理员) - [HttpGet("leave")] - public async Task GetAllLeaves([FromQuery] string status) - { - var result = await _service.GetAllLeavesAsync(status); - return Ok(ApiResult>.Ok(result)); - } - - // PUT /api/attendance/leave/{id}/approve 审批请假 - [HttpPut("leave/{id}/approve")] - public async Task ApproveLeave(long id, [FromBody] ApproveLeaveDto dto) - { - try - { - var approverName = User.FindFirst("realName")?.Value ?? "管理员"; - var result = await _service.ApproveLeaveAsync(id, GetUserId(), approverName, dto); - return Ok(ApiResult.Ok(result, "审批完成")); - } - catch (ServiceException ex) - { - return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); - } - } - - private long GetUserId() => long.Parse(User.FindFirst("id")!.Value); -} diff --git a/backend/F9WebAiTest.Api/Controllers/PayRoll/CalcLdcctController.cs b/backend/F9WebAiTest.Api/Controllers/PayRoll/CalcLdcctController.cs new file mode 100644 index 0000000..b761824 --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/PayRoll/CalcLdcctController.cs @@ -0,0 +1,66 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.PayRoll; +using F9WebAiTest.Application.Services.PayRoll; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.PayRoll; + +/// 薪资计算-挡车/车头产量计算(对应 F9 薪资-挡车车头产量计算 CALCLDCCT) +[ApiController] +[Route("api/payroll/calcldcct")] +[Authorize] +public class CalcLdcctController : ControllerBase +{ + private readonly ICalcLdcctService _svc; + public CalcLdcctController(ICalcLdcctService svc) => _svc = svc; + + /// 列表(账期起止 + 部门 + 工种 + 姓名/庄口关键字) + [HttpGet("list")] + public async Task GetPaged([FromQuery] DateTime start, [FromQuery] DateTime end, + [FromQuery] long? depId = null, [FromQuery] long? worktypeId = null, + [FromQuery] string like = "", [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 50) + => Ok(ApiResult>.Ok( + await _svc.GetPagedAsync(pageIndex, pageSize, start, end, depId, worktypeId, like ?? string.Empty))); + + /// 一键计算(挡车工):整账期 + 部门 + 工种全量覆盖 + [HttpPost("calc-dcg")] + public async Task CalcDcg([FromBody] CalcLdcctCalcDto dto) + { + try { return Ok(ApiResult.Ok(await _svc.CalcDcgAsync(dto), "计算完成")); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + /// 一键计算(车头工):依赖已存的挡车工产量 + [HttpPost("calc-ctg")] + public async Task CalcCtg([FromBody] CalcLdcctCalcDto dto) + { + try { return Ok(ApiResult.Ok(await _svc.CalcCtgAsync(dto), "计算完成")); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + /// 保存人工微调结果(整账期 + 部门 + 工种全量覆盖) + [HttpPost("save")] + public async Task Save([FromBody] CalcLdcctSaveDto dto) + { + try + { + var count = await _svc.SaveAsync(dto); + return Ok(ApiResult.Ok(new { count }, "保存成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + /// 删除整账期 + 部门 + 工种的计算结果 + [HttpDelete] + public async Task Delete([FromQuery] DateTime start, [FromQuery] DateTime end, + [FromQuery] long depId, [FromQuery] long worktypeId) + { + try + { + await _svc.DeleteAsync(start, end, depId, worktypeId); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/PayRoll/ConfEmployeeController.cs b/backend/F9WebAiTest.Api/Controllers/PayRoll/ConfEmployeeController.cs new file mode 100644 index 0000000..f203bfe --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/PayRoll/ConfEmployeeController.cs @@ -0,0 +1,101 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.PayRoll; +using F9WebAiTest.Application.Services.PayRoll; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.PayRoll; + +/// 薪资配置-账期(CONFZQ):考勤统计的「前缫出勤天数」 +[ApiController] +[Route("api/payroll/confzq")] +[Authorize] +public class ConfZqController : ControllerBase +{ + private readonly IConfZqService _svc; + public ConfZqController(IConfZqService svc) => _svc = svc; + + private IActionResult Fail(ServiceException ex) => StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); + + [HttpGet("")] + public async Task List([FromQuery] int? year = null) + => Ok(ApiResult>.Ok(await _svc.GetListAsync(year))); + + [HttpGet("{year:int}/{month:int}")] + public async Task Get(int year, int month) + => Ok(ApiResult.Ok(await _svc.GetByYearMonthAsync(year, month))); + + [HttpPost("")] + public async Task Save([FromBody] ConfZqDto dto) + { + try { return Ok(ApiResult.Ok(new { id = await _svc.SaveAsync(dto) }, "保存成功")); } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("{id:long}")] + public async Task Delete(long id) + { + try { await _svc.DeleteAsync(id); return Ok(ApiResult.Ok(null, "删除成功")); } + catch (ServiceException ex) { return Fail(ex); } + } +} + +/// 薪资配置-职工(CONFEMPLOYEE):法定假小时数 / 是否学工 +[ApiController] +[Route("api/payroll/confemployee")] +[Authorize] +public class ConfEmployeeController : ControllerBase +{ + private readonly IConfEmployeeService _svc; + public ConfEmployeeController(IConfEmployeeService svc) => _svc = svc; + + private IActionResult Fail(ServiceException ex) => StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); + + [HttpGet("")] + public async Task List([FromQuery] long? employeeId = null, [FromQuery] long? worktypeId = null) + => Ok(ApiResult>.Ok(await _svc.GetListAsync(employeeId, worktypeId))); + + [HttpPost("")] + public async Task Save([FromBody] ConfEmployeeDto dto) + { + try { return Ok(ApiResult.Ok(new { id = await _svc.SaveAsync(dto) }, "保存成功")); } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("{id:long}")] + public async Task Delete(long id) + { + try { await _svc.DeleteAsync(id); return Ok(ApiResult.Ok(null, "删除成功")); } + catch (ServiceException ex) { return Fail(ex); } + } +} + +/// 岗位胜任考核:考核结果决定加班费单价档位 +[ApiController] +[Route("api/payroll/gwsrkh")] +[Authorize] +public class GwsrkhController : ControllerBase +{ + private readonly IGwsrkhService _svc; + public GwsrkhController(IGwsrkhService svc) => _svc = svc; + + private IActionResult Fail(ServiceException ex) => StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); + + [HttpGet("")] + public async Task List([FromQuery] int year, [FromQuery] int month, [FromQuery] long depId = 0) + => Ok(ApiResult>.Ok(await _svc.GetListAsync(year, month, depId))); + + [HttpPost("")] + public async Task Save([FromBody] GwsrkhDto dto) + { + try { return Ok(ApiResult.Ok(new { id = await _svc.SaveAsync(dto) }, "保存成功")); } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("{id:long}")] + public async Task Delete(long id) + { + try { await _svc.DeleteAsync(id); return Ok(ApiResult.Ok(null, "删除成功")); } + catch (ServiceException ex) { return Fail(ex); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/PayRoll/PayRollConfController.cs b/backend/F9WebAiTest.Api/Controllers/PayRoll/PayRollConfController.cs new file mode 100644 index 0000000..2270a6b --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/PayRoll/PayRollConfController.cs @@ -0,0 +1,230 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.PayRoll; +using F9WebAiTest.Application.Services.PayRoll; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.PayRoll; + +// ==================== 薪资配置-工艺庄口 ==================== + +/// 薪资配置-工艺庄口(挡车车头产量计算的丝量/单价/系数基准) +[ApiController] +[Route("api/payroll/confgyzk")] +[Authorize] +public class ConfGyzkController : ControllerBase +{ + private readonly IConfGyzkService _svc; + public ConfGyzkController(IConfGyzkService svc) => _svc = svc; + + [HttpGet("list")] + public async Task GetList([FromQuery] int? year = null, [FromQuery] int? month = null) + => Ok(ApiResult>.Ok(await _svc.GetListAsync(year, month))); + + [HttpGet("{id}")] + public async Task GetForEdit(long id) + { + try { return Ok(ApiResult.Ok(await _svc.GetForEditAsync(id))); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpPost("save")] + public async Task Save([FromBody] ConfGyzkSaveDto dto) + { + try + { + var id = await _svc.SaveAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try + { + await _svc.DeleteAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} + +// ==================== 薪资配置-工种 ==================== + +/// 薪资配置-工种/岗位(标准上班时长、标准出勤天数及各项金额) +[ApiController] +[Route("api/payroll/confgz")] +[Authorize] +public class ConfGzController : ControllerBase +{ + private readonly IConfGzService _svc; + public ConfGzController(IConfGzService svc) => _svc = svc; + + [HttpGet("list")] + public async Task GetList() => Ok(ApiResult>.Ok(await _svc.GetListAsync())); + + [HttpGet("{id}")] + public async Task GetForEdit(long id) + { + try { return Ok(ApiResult.Ok(await _svc.GetForEditAsync(id))); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpPost("save")] + public async Task Save([FromBody] ConfGzSaveDto dto) + { + try + { + var id = await _svc.SaveAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try + { + await _svc.DeleteAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} + +// ==================== 薪资配置-挡车/车头岗位 ==================== + +/// 薪资配置-挡车/车头岗位(按绪数档的岗位基数金额、车头工金额) +[ApiController] +[Route("api/payroll/confdcg")] +[Authorize] +public class ConfDcgController : ControllerBase +{ + private readonly IConfDcgService _svc; + public ConfDcgController(IConfDcgService svc) => _svc = svc; + + [HttpGet("list")] + public async Task GetList() => Ok(ApiResult>.Ok(await _svc.GetListAsync())); + + [HttpGet("{id}")] + public async Task GetForEdit(long id) + { + try { return Ok(ApiResult.Ok(await _svc.GetForEditAsync(id))); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpPost("save")] + public async Task Save([FromBody] ConfDcgSaveDto dto) + { + try + { + var id = await _svc.SaveAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try + { + await _svc.DeleteAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} + +// ==================== 挡车纤度奖扣 ==================== + +/// 挡车纤度奖扣(按规格 + 纤度配置野纤的奖励/扣款) +[ApiController] +[Route("api/payroll/dcxdjk")] +[Authorize] +public class DcxdjkController : ControllerBase +{ + private readonly IDcxdjkService _svc; + public DcxdjkController(IDcxdjkService svc) => _svc = svc; + + [HttpGet("list")] + public async Task GetList([FromQuery] string guige = "") + => Ok(ApiResult>.Ok(await _svc.GetListAsync(guige ?? string.Empty))); + + [HttpGet("{id}")] + public async Task GetForEdit(long id) + { + try { return Ok(ApiResult.Ok(await _svc.GetForEditAsync(id))); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpPost("save")] + public async Task Save([FromBody] DcxdjkSaveDto dto) + { + try + { + var id = await _svc.SaveAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try + { + await _svc.DeleteAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} + +// ==================== 薪资价格配置 ==================== + +/// 薪资价格配置(如「薪资.前缫」下的挡车工/车头工计件等级奖扣单价) +[ApiController] +[Route("api/payroll/xzjg")] +[Authorize] +public class XzjgController : ControllerBase +{ + private readonly IXzjgService _svc; + public XzjgController(IXzjgService svc) => _svc = svc; + + [HttpGet("list")] + public async Task GetList([FromQuery] string group = "") + => Ok(ApiResult>.Ok(await _svc.GetListAsync(group ?? string.Empty))); + + [HttpGet("{id}")] + public async Task GetForEdit(long id) + { + try { return Ok(ApiResult.Ok(await _svc.GetForEditAsync(id))); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpPost("save")] + public async Task Save([FromBody] XzjgSaveDto dto) + { + try + { + var id = await _svc.SaveAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try + { + await _svc.DeleteAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/Prod/CdsController.cs b/backend/F9WebAiTest.Api/Controllers/Prod/CdsController.cs new file mode 100644 index 0000000..b3dec22 --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/Prod/CdsController.cs @@ -0,0 +1,55 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Application.Services.Prod; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.Prod; + +/// 秤大丝(生产-后缫):以落丝记录为主表,按车台录入丝量 +[ApiController] +[Route("api/prod/cds")] +[Authorize] +public class CdsController : ControllerBase +{ + private readonly ICdsService _svc; + public CdsController(ICdsService svc) => _svc = svc; + + /// 秤大丝列表(落丝记录 + 秤丝汇总) + [HttpGet("list")] + public async Task GetPaged([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20, + [FromQuery] DateTime? date = null, [FromQuery] long? deptId = null) + => Ok(ApiResult>.Ok(await _svc.GetPagedAsync(pageIndex, pageSize, date, deptId))); + + /// 称重弹窗数据(车台明细;首次称重按车台范围初始化) + [HttpGet("{lsjlId}")] + public async Task GetEdit(long lsjlId) + { + try { return Ok(ApiResult.Ok(await _svc.GetEditAsync(lsjlId))); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + /// 保存秤大丝(车台明细先删后建 + 汇总指标) + [HttpPost("save")] + public async Task Save([FromBody] CdsSaveDto dto) + { + try + { + await _svc.SaveAsync(dto); + return Ok(ApiResult.Ok(null, "保存成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + /// 清除称重数据(汇总 + 车台明细) + [HttpDelete("{lsjlId}")] + public async Task Delete(long lsjlId) + { + try + { + await _svc.DeleteAsync(lsjlId); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/Prod/GlxdController.cs b/backend/F9WebAiTest.Api/Controllers/Prod/GlxdController.cs new file mode 100644 index 0000000..263821a --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/Prod/GlxdController.cs @@ -0,0 +1,34 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Application.Services.Prod; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.Prod; + +/// 公量纤度(生产-后缫):按「日期 + 车间」整日批量录入 +[ApiController] +[Route("api/prod/glxd")] +[Authorize] +public class GlxdController : ControllerBase +{ + private readonly IGlxdService _svc; + public GlxdController(IGlxdService svc) => _svc = svc; + + /// 公量纤度列表(落丝记录 + 纤度检验平均纤度 + 公量纤度) + [HttpGet("list")] + public async Task GetList([FromQuery] DateTime? date = null, [FromQuery] long? deptId = null) + => Ok(ApiResult>.Ok(await _svc.GetListAsync(date, deptId))); + + /// 批量保存公量纤度 + [HttpPost("save")] + public async Task Save([FromBody] GlxdSaveDto dto) + { + try + { + await _svc.SaveAsync(dto); + return Ok(ApiResult.Ok(null, "保存成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/Prod/KaoqinController.cs b/backend/F9WebAiTest.Api/Controllers/Prod/KaoqinController.cs new file mode 100644 index 0000000..d681c1d --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/Prod/KaoqinController.cs @@ -0,0 +1,100 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Application.Services.Prod; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.Prod; + +/// +/// 生产-考勤:考勤登记(CQDJ)。 +/// 对应 F9 WinClient\F9.Business.Prod\KQ\CQDJ。 +/// +[ApiController] +[Route("api/prod/kaoqin")] +[Authorize] +public class KaoqinController : ControllerBase +{ + private readonly IKaoqinService _svc; + public KaoqinController(IKaoqinService svc) => _svc = svc; + + private IActionResult Fail(ServiceException ex) => StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); + + /// 整组登记查询(日期 + 部门 + 工种 + 班次) + [HttpGet("cqdj")] + public async Task GetRegister([FromQuery] DateTime date, [FromQuery] long depId, + [FromQuery] long worktypeId = 0, [FromQuery] int banci = 1, [FromQuery] string keyword = "") + { + try { return Ok(ApiResult.Ok(await _svc.GetRegisterAsync(date, depId, worktypeId, banci, keyword ?? ""))); } + catch (ServiceException ex) { return Fail(ex); } + } + + /// 整组覆盖保存 + [HttpPost("cqdj")] + public async Task SaveRegister([FromBody] CqdjSaveDto dto) + { + try { return Ok(ApiResult.Ok(new { count = await _svc.SaveRegisterAsync(dto) }, "保存成功")); } + catch (ServiceException ex) { return Fail(ex); } + } + + /// 删除整组登记 + [HttpDelete("cqdj")] + public async Task DeleteRegister([FromQuery] DateTime date, [FromQuery] long depId, + [FromQuery] long worktypeId = 0, [FromQuery] int banci = 1) + { + try { return Ok(ApiResult.Ok(new { count = await _svc.DeleteRegisterAsync(date, depId, worktypeId, banci) }, "删除成功")); } + catch (ServiceException ex) { return Fail(ex); } + } + + /// 「添加职工」可选职工 + [HttpGet("cqdj/employees")] + public async Task Employees([FromQuery] long depId, [FromQuery] long worktypeId = 0, [FromQuery] int banci = 1) + => Ok(ApiResult>.Ok(await _svc.GetEmployeesAsync(depId, worktypeId, banci))); + + // ==================== 考勤统计(CQTJ) ==================== + + /// 统计分页(库中的月快照) + [HttpGet("cqtj")] + public async Task CqtjList([FromQuery] int year, [FromQuery] int month, [FromQuery] long depId = 0, + [FromQuery] long worktypeId = 0, [FromQuery] string like = "", [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20) + => Ok(ApiResult>.Ok( + await _svc.GetCqtjPagedAsync(year, month, depId, worktypeId, like ?? "", pageIndex, pageSize))); + + /// 录入:已有快照取库,否则按账期实时汇总 + [HttpGet("cqtj/edit")] + public async Task CqtjEdit([FromQuery] int year, [FromQuery] int month, [FromQuery] long depId) + { + try { return Ok(ApiResult.Ok(await _svc.GetCqtjForEditAsync(year, month, depId))); } + catch (ServiceException ex) { return Fail(ex); } + } + + /// 保存(整组覆盖) + [HttpPost("cqtj")] + public async Task CqtjSave([FromBody] CqtjSaveDto dto) + { + try { return Ok(ApiResult.Ok(new { count = await _svc.SaveCqtjAsync(dto) }, "保存成功")); } + catch (ServiceException ex) { return Fail(ex); } + } + + /// 删除该年月的统计快照(depId=0 表示全部部门) + [HttpDelete("cqtj")] + public async Task CqtjDelete([FromQuery] int year, [FromQuery] int month, [FromQuery] long depId = 0) + { + try + { + var count = depId > 0 + ? await _svc.DeleteCqtjAsync(year, month, depId) + : await _svc.DeleteCqtjAllAsync(year, month); + return Ok(ApiResult.Ok(new { count }, "删除成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + /// 一键生成:遍历全部车间,缺的才生成 + [HttpPost("cqtj/generate")] + public async Task CqtjGenerate([FromBody] CqtjGenerateDto dto) + { + try { return Ok(ApiResult.Ok(new { count = await _svc.GenerateCqtjAllAsync(dto.Year, dto.Month) }, "生成完成")); } + catch (ServiceException ex) { return Fail(ex); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/Prod/SpjcController.cs b/backend/F9WebAiTest.Api/Controllers/Prod/SpjcController.cs new file mode 100644 index 0000000..e1003a8 --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/Prod/SpjcController.cs @@ -0,0 +1,54 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Application.Services.Prod; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.Prod; + +/// 丝片检查(生产-后缫):按「日期 + 车间 + 工艺庄口 + 规格」录入疵点片数 +[ApiController] +[Route("api/prod/spjc")] +[Authorize] +public class SpjcController : ControllerBase +{ + private readonly ISpjcService _svc; + public SpjcController(ISpjcService svc) => _svc = svc; + + /// 丝片检查列表(日期 + 车间 + 关键字) + [HttpGet("list")] + public async Task GetPaged([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20, + [FromQuery] DateTime? date = null, [FromQuery] long? deptId = null, [FromQuery] string like = "") + => Ok(ApiResult>.Ok( + await _svc.GetPagedAsync(pageIndex, pageSize, date, deptId, like ?? string.Empty))); + + /// 修改回填 + [HttpGet("{id}")] + public async Task GetForEdit(long id) + { + try { return Ok(ApiResult.Ok(await _svc.GetForEditAsync(id))); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpPost("save")] + public async Task Save([FromBody] SpjcSaveDto dto) + { + try + { + var id = await _svc.SaveAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try + { + await _svc.DeleteAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/Prod/SptzController.cs b/backend/F9WebAiTest.Api/Controllers/Prod/SptzController.cs new file mode 100644 index 0000000..ef36955 --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/Prod/SptzController.cs @@ -0,0 +1,58 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Application.Services.Prod; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.Prod; + +/// 丝片台帐(生产-后缫):每日一条,按账期区间查询 +[ApiController] +[Route("api/prod/sptz")] +[Authorize] +public class SptzController : ControllerBase +{ + private readonly ISptzService _svc; + public SptzController(ISptzService svc) => _svc = svc; + + /// 丝片台帐列表(账期起止) + [HttpGet("list")] + public async Task GetPaged([FromQuery] DateTime dateFrom, [FromQuery] DateTime dateTo, + [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 31) + => Ok(ApiResult>.Ok(await _svc.GetPagedAsync(pageIndex, pageSize, dateFrom, dateTo))); + + /// 修改回填 + [HttpGet("{id}")] + public async Task GetForEdit(long id) + { + try { return Ok(ApiResult.Ok(await _svc.GetForEditAsync(id))); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + /// 新增默认值:上一本台账 + 上存,并按当日「丝片检查」带出整理片数 + [HttpGet("defaults")] + public async Task GetDefaults([FromQuery] DateTime date) + => Ok(ApiResult.Ok(await _svc.GetDefaultsAsync(date))); + + [HttpPost("save")] + public async Task Save([FromBody] SptzSaveDto dto) + { + try + { + var id = await _svc.SaveAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try + { + await _svc.DeleteAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/Prod/XjrbController.cs b/backend/F9WebAiTest.Api/Controllers/Prod/XjrbController.cs new file mode 100644 index 0000000..895dd0c --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/Prod/XjrbController.cs @@ -0,0 +1,143 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Application.Services.Prod; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.Prod; + +/// +/// 生产-选剥茧(对应 F9 WinClient\F9.Business.Prod\XBJ): +/// 选茧日报(含盈亏 / 联统率)、选茧计划及其明细、选茧班组与班组职工。 +/// +[ApiController] +[Route("api/prod/xjrb")] +[Authorize] +public class XjrbController : ControllerBase +{ + private readonly IXjrbService _svc; + public XjrbController(IXjrbService svc) => _svc = svc; + + private IActionResult Fail(ServiceException ex) => StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); + + // ==================== 选茧日报 ==================== + + [HttpGet("")] + public async Task List([FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null, + [FromQuery] string like = "", [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20) + => Ok(ApiResult>.Ok(await _svc.GetPagedAsync(start, end, like ?? "", pageIndex, pageSize))); + + [HttpGet("{id:long}")] + public async Task Get(long id) + => Ok(ApiResult.Ok(await _svc.GetByIdAsync(id))); + + [HttpPost("")] + public async Task Save([FromBody] XjrbSaveDto dto) + { + try { return Ok(ApiResult.Ok(new { id = await _svc.SaveAsync(dto) }, "保存成功")); } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("{id:long}")] + public async Task Delete(long id) + { + try + { + await _svc.DeleteAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + /// 计算盈亏 / 联统率(前端保存前调用,把结果写入表单) + [HttpPost("count-yingkui")] + public async Task CountYingkui([FromBody] XjrbSaveDto dto) + { + try { return Ok(ApiResult.Ok(await _svc.CountYingkuiAsync(dto))); } + catch (ServiceException ex) { return Fail(ex); } + } + + // ==================== 选茧计划 ==================== + + [HttpGet("plan")] + public async Task PlanList([FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null, + [FromQuery] string like = "", [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20) + => Ok(ApiResult>.Ok(await _svc.GetPlanPagedAsync(start, end, like ?? "", pageIndex, pageSize))); + + [HttpGet("plan/{id:long}")] + public async Task PlanGet(long id) + => Ok(ApiResult.Ok(await _svc.GetPlanByIdAsync(id))); + + [HttpPost("plan")] + public async Task PlanSave([FromBody] XjrbPlanSaveDto dto) + { + try { return Ok(ApiResult.Ok(new { id = await _svc.SavePlanAsync(dto) }, "保存成功")); } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("plan/{id:long}")] + public async Task PlanDelete(long id) + { + try + { + await _svc.DeletePlanAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + // ==================== 选茧计划明细 ==================== + + [HttpGet("plan/{planId:long}/items")] + public async Task PlanItems(long planId) + => Ok(ApiResult>.Ok(await _svc.GetPlanItemsAsync(planId))); + + [HttpPost("plan/item")] + public async Task PlanItemSave([FromBody] XjrbItemSaveDto dto) + { + try { return Ok(ApiResult.Ok(new { id = await _svc.SavePlanItemAsync(dto) }, "保存成功")); } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("plan/item/{id:long}")] + public async Task PlanItemDelete(long id) + { + try + { + await _svc.DeletePlanItemAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + /// 计划维度盈亏(联统率按包数口径) + [HttpPost("plan/count-yingkui")] + public async Task PlanCountYingkui([FromBody] XjrbItemSaveDto dto) + { + try { return Ok(ApiResult.Ok(await _svc.CountPlanYingkuiAsync(dto))); } + catch (ServiceException ex) { return Fail(ex); } + } + + // ==================== 选茧班组 ==================== + + [HttpGet("banzu")] + public async Task BanzuList([FromQuery] string banci = "", [FromQuery] int? zu = null, + [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20) + => Ok(ApiResult>.Ok(await _svc.GetBanzuPagedAsync(banci ?? "", zu, pageIndex, pageSize))); + + [HttpGet("banzu/{id:long}/zhigong")] + public async Task BanzuZhigong(long id) + => Ok(ApiResult>.Ok(await _svc.GetBanzuZhigongAsync(id))); + + /// 班组「编辑录入」:按班次整体覆盖保存职工与组别 + [HttpPost("banzu/zhigong")] + public async Task BanzuZhigongSave([FromBody] List dtos) + { + try + { + await _svc.SaveBanzuZhigongAsync(dtos ?? new List()); + return Ok(ApiResult.Ok(null, "保存成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/Prod/ZjrbController.cs b/backend/F9WebAiTest.Api/Controllers/Prod/ZjrbController.cs new file mode 100644 index 0000000..3e5d9dc --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/Prod/ZjrbController.cs @@ -0,0 +1,90 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Application.Services.Prod; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.Prod; + +/// 煮茧日报(生产-前缫):主表 + 机组 / 班次 / 机组班次 +[ApiController] +[Route("api/prod/zjrb")] +[Authorize] +public class ZjrbController : ControllerBase +{ + private readonly IZjrbService _svc; + public ZjrbController(IZjrbService svc) => _svc = svc; + + // ---------- 查询 ---------- + + /// 日报列表(账期起止 + 车间 + 庄口/规格关键字) + [HttpGet("list")] + public async Task GetPaged([FromQuery] DateTime dateFrom, [FromQuery] DateTime dateTo, + [FromQuery] long depId = 0, [FromQuery] string like = "", [FromQuery] int pageIndex = 1, + [FromQuery] int pageSize = 20) + => Ok(ApiResult>.Ok( + await _svc.GetPagedAsync(dateFrom, dateTo, depId, like ?? string.Empty, pageIndex, pageSize))); + + /// 某条日报的机组明细 + [HttpGet("{id}/jizus")] + public async Task GetJizus(long id) + { + try { return Ok(ApiResult>.Ok(await _svc.GetJizuListAsync(id))); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + /// 修改回填(主表 + 班次 + 机组 + 机组班次) + [HttpGet("{id}")] + public async Task GetForEdit(long id) + { + try { return Ok(ApiResult.Ok(await _svc.GetForEditAsync(id))); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + /// 新增默认值:按上一日结存带出上存、每桶茧量、工艺落丝桶数,并按机组车台生成机组行 + [HttpGet("defaults")] + public async Task GetDefaults([FromQuery] DateTime date, [FromQuery] long depId, + [FromQuery] long gyzkId, [FromQuery] string guige = "") + { + try { return Ok(ApiResult.Ok(await _svc.BuildDefaultsAsync(date, depId, gyzkId, guige))); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + // ---------- 保存 / 删除 ---------- + + [HttpPost("save")] + public async Task Save([FromBody] ZjrbSaveDto dto) + { + try + { + var id = await _svc.SaveAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try + { + await _svc.DeleteAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + // ---------- 计算缫丝时长 ---------- + + /// 按「日期 + 车间」整日重算机组缫丝时长(单班 9 小时 / 双班 18 小时,按茧量比例分配) + [HttpPost("calculate-saosi-hours")] + public async Task CalculateSaosiHours([FromQuery] DateTime date, [FromQuery] long depId) + { + try + { + var changed = await _svc.CalculateJizuSaosiHoursAsync(date, depId); + return Ok(ApiResult.Ok(changed, changed > 0 ? $"已更新 {changed} 条机组缫丝时长" : "缫丝时长无变化")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/Wms/WmsController.cs b/backend/F9WebAiTest.Api/Controllers/Wms/WmsController.cs new file mode 100644 index 0000000..743ee8b --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/Wms/WmsController.cs @@ -0,0 +1,328 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Wms; +using F9WebAiTest.Application.Services.Wms; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.Wms; + +/// +/// 仓储(WMS):原料(1) / 副产品(2) / 中间品(3) / 生丝(4) / 物资(5) +/// 对应 F9 WinClient\F9.Business.WMS 五个模块的入库 / 出库 / 库存台账及其扩展功能。 +/// +[ApiController] +[Route("api/wms")] +[Authorize] +public class WmsController : ControllerBase +{ + private readonly IWmsService _svc; + public WmsController(IWmsService svc) => _svc = svc; + + private IActionResult Fail(ServiceException ex) => StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); + + /// 下拉选项:单据类型 / 物资 / 原料庄口 / 工艺庄口 / 部门 / 往来单位 + [HttpGet("options")] + public async Task Options([FromQuery] int module) + => Ok(ApiResult.Ok(await _svc.GetOptionsAsync(module))); + + // ==================== 入库 ==================== + + [HttpGet("ruku")] + public async Task RukuList([FromQuery] int module, [FromQuery] DateTime? start = null, + [FromQuery] DateTime? end = null, [FromQuery] int? type = null, [FromQuery] string like = "", + [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20) + => Ok(ApiResult>.Ok( + await _svc.GetRukuPagedAsync(module, start, end, type, like ?? "", pageIndex, pageSize))); + + [HttpGet("ruku/{id:long}")] + public async Task RukuGet(long id) + { + try { return Ok(ApiResult.Ok(await _svc.GetRukuAsync(id))); } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpPost("ruku")] + public async Task RukuSave([FromBody] WmsRukuSaveDto dto) + { + try + { + var id = await _svc.SaveRukuAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("ruku/{id:long}")] + public async Task RukuDelete(long id) + { + try + { + await _svc.DeleteRukuAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + /// 采购报销(物资入库单置为已报销) + [HttpPost("ruku/baoxiao")] + public async Task Baoxiao([FromBody] List ids) + { + try + { + var count = await _svc.BaoxiaoAsync(ids ?? new List()); + return Ok(ApiResult.Ok(new { count }, "报销成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + // ==================== 出库 ==================== + + [HttpGet("chuku")] + public async Task ChukuList([FromQuery] int module, [FromQuery] DateTime? start = null, + [FromQuery] DateTime? end = null, [FromQuery] int? type = null, [FromQuery] string like = "", + [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20) + => Ok(ApiResult>.Ok( + await _svc.GetChukuPagedAsync(module, start, end, type, like ?? "", pageIndex, pageSize))); + + [HttpGet("chuku/{id:long}")] + public async Task ChukuGet(long id) + { + try { return Ok(ApiResult.Ok(await _svc.GetChukuAsync(id))); } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpPost("chuku")] + public async Task ChukuSave([FromBody] WmsChukuSaveDto dto) + { + try + { + var id = await _svc.SaveChukuAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("chuku/{id:long}")] + public async Task ChukuDelete(long id) + { + try + { + await _svc.DeleteChukuAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + // ==================== 库存台账 ==================== + + [HttpGet("taizhang/dates")] + public async Task TaizhangDates([FromQuery] int module, [FromQuery] int kind = 0) + => Ok(ApiResult>.Ok(await _svc.GetTzDatesAsync(module, kind))); + + [HttpGet("taizhang")] + public async Task Taizhang([FromQuery] int module, [FromQuery] DateTime date, [FromQuery] int kind = 0, + [FromQuery] bool regenerate = false) + { + try { return Ok(ApiResult.Ok(await _svc.GetTaizhangAsync(module, kind, date, regenerate))); } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpPost("taizhang")] + public async Task TaizhangSave([FromBody] WmsTzSaveDto dto) + { + try + { + var id = await _svc.SaveTaizhangAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("taizhang/{id:long}")] + public async Task TaizhangDelete(long id) + { + try + { + await _svc.DeleteTaizhangAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + // ==================== 库存 / 出入库流水 ==================== + + [HttpGet("kucun")] + public async Task Kucun([FromQuery] int module, [FromQuery] string like = "") + => Ok(ApiResult>.Ok(await _svc.GetKucunAsync(module, like ?? ""))); + + [HttpGet("crk")] + public async Task Crk([FromQuery] int module, [FromQuery] DateTime start, [FromQuery] DateTime end, + [FromQuery] long? wzmlId = null) + => Ok(ApiResult>.Ok(await _svc.GetCrkAsync(module, start, end, wzmlId))); + + // ==================== 生丝批次 / 包件 ==================== + + [HttpGet("pici")] + public async Task PiciList([FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null, + [FromQuery] int kcStatus = 0, [FromQuery] string like = "", + [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20) + => Ok(ApiResult>.Ok( + await _svc.GetPiciPagedAsync(start, end, kcStatus, like ?? "", pageIndex, pageSize))); + + [HttpGet("pici/{id:long}")] + public async Task PiciGet(long id) + { + try { return Ok(ApiResult.Ok(await _svc.GetPiciAsync(id))); } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpPost("pici")] + public async Task PiciSave([FromBody] WmsPiciSaveDto dto) + { + try + { + var id = await _svc.SavePiciAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("pici/{id:long}")] + public async Task PiciDelete(long id) + { + try + { + await _svc.DeletePiciAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + /// 生丝出库选批次(批次 + 未出库包件) + [HttpGet("pici/chuku-select")] + public async Task PiciChukuSelect([FromQuery] long wzmlId = 0, [FromQuery] string excludePiciIds = "") + => Ok(ApiResult>.Ok(await _svc.GetPiciChukuSelectAsync(wzmlId, excludePiciIds ?? ""))); + + /// 批次质检 + [HttpGet("pici/qc/{piciId:long}")] + public async Task PiciQcGet(long piciId) + => Ok(ApiResult.Ok(await _svc.GetPiciQcAsync(piciId))); + + [HttpPost("pici/qc")] + public async Task PiciQcSave([FromBody] WmsPiciQcDto dto) + { + try + { + await _svc.SavePiciQcAsync(dto); + return Ok(ApiResult.Ok(null, "保存成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + /// 批次纤度检验 + [HttpGet("pici/xdjy/{piciId:long}")] + public async Task PiciQcXdjyGet(long piciId) + => Ok(ApiResult>.Ok(await _svc.GetPiciQcXdjyAsync(piciId))); + + [HttpPost("pici/xdjy")] + public async Task PiciQcXdjySave([FromBody] List items) + { + try + { + await _svc.SavePiciQcXdjyAsync(items ?? new List()); + return Ok(ApiResult.Ok(null, "保存成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + // ==================== 原料采购 ==================== + + [HttpGet("caigou")] + public async Task CaigouList([FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null) + => Ok(ApiResult>.Ok(await _svc.GetCaigouAsync(start, end))); + + [HttpPost("caigou")] + public async Task CaigouSave([FromBody] WmsCaigouDto dto) + { + try + { + var id = await _svc.SaveCaigouAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("caigou/{id:long}")] + public async Task CaigouDelete(long id) + { + try + { + await _svc.DeleteCaigouAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + // ==================== 原料翻包计划 ==================== + + [HttpGet("fbplan")] + public async Task FbplanList([FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null) + => Ok(ApiResult>.Ok(await _svc.GetFbplanAsync(start, end))); + + [HttpPost("fbplan")] + public async Task FbplanSave([FromBody] WmsFbplanDto dto) + { + try + { + var id = await _svc.SaveFbplanAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("fbplan/{id:long}")] + public async Task FbplanDelete(long id) + { + try + { + await _svc.DeleteFbplanAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + // ==================== 物资采购计划 ==================== + + [HttpGet("cgplan")] + public async Task CgplanList([FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null) + => Ok(ApiResult>.Ok(await _svc.GetCgplanAsync(start, end))); + + [HttpGet("cgplan/{id:long}")] + public async Task CgplanGet(long id) + { + try { return Ok(ApiResult.Ok(await _svc.GetCgplanAsync(id))); } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpPost("cgplan")] + public async Task CgplanSave([FromBody] WmsCgplanSaveDto dto) + { + try + { + var id = await _svc.SaveCgplanAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("cgplan/{id:long}")] + public async Task CgplanDelete(long id) + { + try + { + await _svc.DeleteCgplanAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } +} diff --git a/backend/F9WebAiTest.Api/f9_web_ai_test_rbac.db b/backend/F9WebAiTest.Api/f9_web_ai_test_rbac.db index a168e42..86b7205 100644 Binary files a/backend/F9WebAiTest.Api/f9_web_ai_test_rbac.db and b/backend/F9WebAiTest.Api/f9_web_ai_test_rbac.db differ diff --git a/backend/F9WebAiTest.Application/DependencyInjection.cs b/backend/F9WebAiTest.Application/DependencyInjection.cs index fbe044b..28415f0 100644 --- a/backend/F9WebAiTest.Application/DependencyInjection.cs +++ b/backend/F9WebAiTest.Application/DependencyInjection.cs @@ -1,5 +1,4 @@ using F9WebAiTest.Application.Services; -using F9WebAiTest.Application.Services.Attendance; using F9WebAiTest.Application.Services.Auth; using F9WebAiTest.Application.Services.Base; using F9WebAiTest.Application.Services.Notice; @@ -18,7 +17,6 @@ public static class DependencyInjection { services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -29,6 +27,28 @@ public static class DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + // 生产-选剥茧:选茧日报(含盈亏 / 联统率)/ 选茧计划及其明细 / 选茧班组及职工 + // (对齐 F9 WinClient\F9.Business.Prod\XBJ) + services.AddScoped(); + // 生产-后缫:秤大丝 / 公量纤度 / 丝片检查 / 丝片台帐(对齐 F9 WinClient\F9.Business.Prod\HS) + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + // 生产-考勤:考勤登记 + 考勤统计(对齐 F9 WinClient\F9.Business.Prod\KQ\CQDJ / CQTJ) + services.AddScoped(); + // 薪资:挡车/车头产量计算及其依赖配置(对齐 F9 WinClient\F9.Business.PayRoll\PAYROLL) + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + // 考勤统计依赖:薪资配置-账期 / 薪资配置-职工 / 岗位胜任考核 + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -36,6 +56,9 @@ public static class DependencyInjection services.AddScoped(); services.AddScoped(); + // 仓储模块:原料 / 副产品 / 中间品 / 生丝 / 物资(对齐 F9 WinClient\F9.Business.WMS) + services.AddScoped(); + // 报表数据提供者(按 Config.DsName 解析,自动注册所有实现) foreach (var t in typeof(DependencyInjection).Assembly.GetTypes() .Where(t => !t.IsAbstract && typeof(IReportDataProvider).IsAssignableFrom(t))) diff --git a/backend/F9WebAiTest.Application/Dtos/Attendance/AttendanceDtos.cs b/backend/F9WebAiTest.Application/Dtos/Attendance/AttendanceDtos.cs deleted file mode 100644 index 3058471..0000000 --- a/backend/F9WebAiTest.Application/Dtos/Attendance/AttendanceDtos.cs +++ /dev/null @@ -1,87 +0,0 @@ -namespace F9WebAiTest.Application.Dtos.Attendance; - -public class AttendanceDto -{ - public long Id { get; set; } - public long EmployeeId { get; set; } - public string EmployeeName { get; set; } - public string DepartmentName { get; set; } - public DateTime WorkDate { get; set; } - public DateTime? SignInTime { get; set; } - public DateTime? SignOutTime { get; set; } - public string SignInStatus { get; set; } - public string SignOutStatus { get; set; } - public string Remark { get; set; } -} - -public class PunchResultDto -{ - public string Type { get; set; } // signIn / signOut - public DateTime Time { get; set; } - public string Status { get; set; } // 正常/迟到/早退 - public string Message { get; set; } -} - -public class AttendanceQueryDto -{ - public string Date { get; set; } = ""; // yyyy-MM-dd - public long? DepartmentId { get; set; } - public string Keyword { get; set; } = ""; - public int PageIndex { get; set; } = 1; - public int PageSize { get; set; } = 10; -} - -public class AttendanceSummaryDto -{ - public int NormalDays { get; set; } - public int LateDays { get; set; } - public int EarlyLeaveDays { get; set; } - public int AbsentDays { get; set; } - public int LeaveDays { get; set; } - public int TotalDays { get; set; } -} - -public class LeaveRequestDto -{ - public long Id { get; set; } - public long EmployeeId { get; set; } - public string EmployeeName { get; set; } - public string DepartmentName { get; set; } - public string Type { get; set; } - public DateTime StartTime { get; set; } - public DateTime EndTime { get; set; } - public string Reason { get; set; } - public string Status { get; set; } - public int CurrentOrderNo { get; set; } // 当前审批到第几步 - public string ApproverName { get; set; } - public string ApproveRemark { get; set; } - public DateTime? ApprovedAt { get; set; } - public DateTime CreatedAt { get; set; } - public List Approvers { get; set; } = new(); // 审批流程节点 -} - -public class LeaveApproverDto -{ - public long Id { get; set; } - public long ApproverId { get; set; } - public string ApproverName { get; set; } - public int OrderNo { get; set; } // 序号(审批顺序) - public string Status { get; set; } // 待审批/已批准/已驳回 - public string ApproveRemark { get; set; } - public DateTime? ApprovedAt { get; set; } -} - -public class CreateLeaveDto -{ - public string Type { get; set; } = "事假"; - public DateTime StartTime { get; set; } - public DateTime EndTime { get; set; } - public string Reason { get; set; } = ""; - public List ApproverIds { get; set; } = new(); // 审批人ID列表(顺序=审批顺序) -} - -public class ApproveLeaveDto -{ - public bool Approved { get; set; } - public string Remark { get; set; } = ""; -} diff --git a/backend/F9WebAiTest.Application/Dtos/PayRoll/CalcLdcctDtos.cs b/backend/F9WebAiTest.Application/Dtos/PayRoll/CalcLdcctDtos.cs new file mode 100644 index 0000000..a3bb28b --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/PayRoll/CalcLdcctDtos.cs @@ -0,0 +1,131 @@ +namespace F9WebAiTest.Application.Dtos.PayRoll; + +/// 挡车车头产量计算-列表行 +public class CalcLdcctListItemDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public long EmployeeId { get; set; } + public string EmployeeName { get; set; } = ""; + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public long WorktypeId { get; set; } + public string WorktypeName { get; set; } = ""; + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public string Guige { get; set; } = ""; + public string Chetai { get; set; } = ""; + public int Jizu { get; set; } + public int Banci { get; set; } + public string BanciName { get; set; } = ""; + + public decimal HoursKantai { get; set; } // 看台时长 + public decimal Dangcheshu { get; set; } // 挡车数 + public string Dengji { get; set; } = ""; // 等级 + public decimal Dengjifen { get; set; } // 等级分 + + // 产量 + public decimal GyzkTaichan { get; set; } // 薪资台产(来自工艺庄口配置) + public decimal ClDanjiaBase { get; set; } + public decimal ClDanjiaDengji { get; set; } + public decimal ClDanjiaActual { get; set; } + public decimal ClSiliang { get; set; } + + // 质量 + public decimal GyzkSaozhe { get; set; } // 薪资缫折(工艺指标缫折) + public decimal ZlSaozhe { get; set; } + public decimal ZlCenterxd { get; set; } + public decimal ZlYexiannum { get; set; } + public decimal ZlQingjie { get; set; } + public decimal ZlJiejing { get; set; } + public decimal ZlErdu { get; set; } + public decimal ZlSandu { get; set; } + + // 金额 + public decimal JineCl_Original { get; set; } + public decimal Xishu_JineCl { get; set; } + public decimal JineCl { get; set; } + public decimal JineZlSaozhe { get; set; } + public decimal JineZlCenterxd { get; set; } + public decimal JineZlYexian { get; set; } + public decimal JineZlQingjie { get; set; } + public decimal JineZlJiejing { get; set; } + public decimal JineZlErdu { get; set; } + public decimal JineZlSandu { get; set; } + + /// 质量金额合计(负数为扣款) + public decimal JineZlTotal { get; set; } +} + +/// 挡车车头产量计算-保存行(整账期 + 部门 + 工种全量覆盖) +public class CalcLdcctSaveItemDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public long EmployeeId { get; set; } + public long DepId { get; set; } + public long WorktypeId { get; set; } + public int Jizu { get; set; } + public int Banci { get; set; } + public long GyzkId { get; set; } + public string Guige { get; set; } = ""; + public string Chetai { get; set; } = ""; + + public decimal HoursKantai { get; set; } + public decimal Dangcheshu { get; set; } + public string Dengji { get; set; } = ""; + public decimal Dengjifen { get; set; } + + public decimal ClDanjiaBase { get; set; } + public decimal ClDanjiaDengji { get; set; } + public decimal ClDanjiaActual { get; set; } + public decimal ClSiliang { get; set; } + + public decimal ZlSaozhe { get; set; } + public decimal ZlCenterxd { get; set; } + public decimal ZlYexiannum { get; set; } + public decimal ZlQingjie { get; set; } + public decimal ZlJiejing { get; set; } + public decimal ZlErdu { get; set; } + public decimal ZlSandu { get; set; } + + public decimal JineCl_Original { get; set; } + public decimal Xishu_JineCl { get; set; } + public decimal JineCl { get; set; } + public decimal JineZlSaozhe { get; set; } + public decimal JineZlCenterxd { get; set; } + public decimal JineZlYexian { get; set; } + public decimal JineZlQingjie { get; set; } + public decimal JineZlJiejing { get; set; } + public decimal JineZlErdu { get; set; } + public decimal JineZlSandu { get; set; } +} + +/// 整账期保存请求 +public class CalcLdcctSaveDto +{ + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public long DepId { get; set; } + public long WorktypeId { get; set; } + public List Items { get; set; } = new(); +} + +/// 计算请求(一键计算) +public class CalcLdcctCalcDto +{ + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public long DepId { get; set; } + public long WorktypeId { get; set; } +} + +/// 计算结果 +public class CalcLdcctResultDto +{ + public int Count { get; set; } // 生成条数 + public decimal SumSiliang { get; set; } // 丝量合计(g) + public decimal SumJineCl { get; set; } // 产量金额合计 + public decimal SumJineZl { get; set; } // 质量金额合计 + public List Warnings { get; set; } = new(); +} diff --git a/backend/F9WebAiTest.Application/Dtos/PayRoll/ConfEmployeeDtos.cs b/backend/F9WebAiTest.Application/Dtos/PayRoll/ConfEmployeeDtos.cs new file mode 100644 index 0000000..8c708b2 --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/PayRoll/ConfEmployeeDtos.cs @@ -0,0 +1,40 @@ +namespace F9WebAiTest.Application.Dtos.PayRoll; + +// 考勤统计(CQTJ)依赖配置:薪资配置-账期 / 薪资配置-职工 / 岗位胜任考核 + +public class ConfZqDto +{ + public long Id { get; set; } + public int Year { get; set; } + public int Month { get; set; } + public decimal QsQuanqinDays { get; set; } + public string Memo { get; set; } = ""; +} + +public class ConfEmployeeDto +{ + public long Id { get; set; } + public long EmployeeId { get; set; } + public string EmployeeCode { get; set; } = ""; + public string EmployeeName { get; set; } = ""; + public long WorktypeId { get; set; } + public string WorktypeName { get; set; } = ""; + public decimal HoursFadingBz { get; set; } + public decimal HoursFadingGz { get; set; } + public bool IsXuegong { get; set; } + public string Memo { get; set; } = ""; +} + +public class GwsrkhDto +{ + public long Id { get; set; } + public int Year { get; set; } + public int Month { get; set; } + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public long EmployeeId { get; set; } + public string EmployeeCode { get; set; } = ""; + public string EmployeeName { get; set; } = ""; + public string KhJieguo { get; set; } = ""; + public string Memo { get; set; } = ""; +} diff --git a/backend/F9WebAiTest.Application/Dtos/PayRoll/PayRollConfDtos.cs b/backend/F9WebAiTest.Application/Dtos/PayRoll/PayRollConfDtos.cs new file mode 100644 index 0000000..c36fd18 --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/PayRoll/PayRollConfDtos.cs @@ -0,0 +1,204 @@ +namespace F9WebAiTest.Application.Dtos.PayRoll; + +// ==================== 薪资配置-工艺庄口 ==================== + +public class ConfGyzkListItemDto +{ + public long Id { get; set; } + public int Year { get; set; } + public int Month { get; set; } + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public string Guige { get; set; } = ""; + public decimal QsTaichan { get; set; } + public string QsDengji { get; set; } = ""; + public decimal QsDengjifen { get; set; } + public decimal SaoZhe { get; set; } + public int MeiTongJianLiang { get; set; } + public int LuoSiTongShu { get; set; } + public decimal MeiCheSiLiang { get; set; } + public decimal DcgJijianXishu { get; set; } + public decimal CtgJijianXishu { get; set; } + public decimal GyzkLtwxKs { get; set; } + public decimal GyzkLtwxl { get; set; } + public decimal XjChanliangPrice { get; set; } +} + +public class ConfGyzkSaveDto +{ + public long Id { get; set; } + public int Year { get; set; } + public int Month { get; set; } + public long GyzkId { get; set; } + public string Guige { get; set; } = ""; + public decimal QsTaichan { get; set; } + public string QsDengji { get; set; } = ""; + public decimal QsDengjifen { get; set; } + public decimal SaoZhe { get; set; } + public int MeiTongJianLiang { get; set; } + public int LuoSiTongShu { get; set; } + public decimal MeiCheSiLiang { get; set; } + public decimal DcgJijianXishu { get; set; } + public decimal CtgJijianXishu { get; set; } + public decimal GyzkLtwxKs { get; set; } + public decimal GyzkLtwxl { get; set; } + public decimal XjChanliangPrice { get; set; } +} + +// ==================== 薪资配置-工种 ==================== + +public class ConfGzListItemDto +{ + public long Id { get; set; } + public long WorkTypeId { get; set; } + public string WorkTypeName { get; set; } = ""; + public decimal HoursPerday { get; set; } + public decimal QqDaysStandard { get; set; } + public decimal QqHoursMaxQj { get; set; } + public decimal QqHoursDkMinjiaban { get; set; } + public decimal QqHoursDkQingjia { get; set; } + public decimal XgJineQuanqin { get; set; } + public decimal XgJineGangwei { get; set; } + public decimal JineNiangong { get; set; } + public decimal JineQuanqin { get; set; } + public decimal JineGangwei { get; set; } + public decimal JineYuekhJixiao { get; set; } + public decimal JineGwkh { get; set; } + public decimal JineYueticheng { get; set; } + public decimal JineJtkhYear { get; set; } + public decimal JineJisuanjishu { get; set; } + public decimal JineYeban { get; set; } + public decimal JineJiaotongButie { get; set; } + public decimal LiudongJineGuding { get; set; } + public decimal JiaqiJineKuangongKk { get; set; } + public decimal JiaqiJineFadingGz { get; set; } + public decimal JiaqiJineFadingBz { get; set; } + public decimal JiaqiJineHunjiaBz { get; set; } + public decimal JiaqiJineSangjiaBz { get; set; } + public decimal JiaqiJineBingjiaBz { get; set; } + public decimal JiaqiJineLunxiuBz { get; set; } + public decimal JiabanJineBz { get; set; } + public decimal Jiaqi_Jine_TiaoxiuBz { get; set; } + public decimal Jiaqi_Jine_NianjiaBz { get; set; } + public decimal GwsrkhJineQianhege { get; set; } + public decimal GwsrkhJineQiwang { get; set; } + public decimal GwsrkhJineHege { get; set; } + public decimal GwsrkhJineShengren { get; set; } + public decimal GwsrkhJineChaoshengren { get; set; } + public decimal Jiaban_GwsrkhJineQianhege { get; set; } + public decimal Jiaban_GwsrkhJineQiwang { get; set; } + public decimal Jiaban_GwsrkhJineHege { get; set; } + public decimal Jiaban_GwsrkhJineShengren { get; set; } + public decimal Jiaban_GwsrkhJineChaoshengren { get; set; } +} + +public class ConfGzSaveDto +{ + public long Id { get; set; } + public long WorkTypeId { get; set; } + public decimal HoursPerday { get; set; } + public decimal QqDaysStandard { get; set; } + public decimal QqHoursMaxQj { get; set; } + public decimal QqHoursDkMinjiaban { get; set; } + public decimal QqHoursDkQingjia { get; set; } + public decimal XgJineQuanqin { get; set; } + public decimal XgJineGangwei { get; set; } + public decimal JineNiangong { get; set; } + public decimal JineQuanqin { get; set; } + public decimal JineGangwei { get; set; } + public decimal JineYuekhJixiao { get; set; } + public decimal JineGwkh { get; set; } + public decimal JineYueticheng { get; set; } + public decimal JineJtkhYear { get; set; } + public decimal JineJisuanjishu { get; set; } + public decimal JineYeban { get; set; } + public decimal JineJiaotongButie { get; set; } + public decimal LiudongJineGuding { get; set; } + public decimal JiaqiJineKuangongKk { get; set; } + public decimal JiaqiJineFadingGz { get; set; } + public decimal JiaqiJineFadingBz { get; set; } + public decimal JiaqiJineHunjiaBz { get; set; } + public decimal JiaqiJineSangjiaBz { get; set; } + public decimal JiaqiJineBingjiaBz { get; set; } + public decimal JiaqiJineLunxiuBz { get; set; } + public decimal JiabanJineBz { get; set; } + public decimal Jiaqi_Jine_TiaoxiuBz { get; set; } + public decimal Jiaqi_Jine_NianjiaBz { get; set; } + public decimal GwsrkhJineQianhege { get; set; } + public decimal GwsrkhJineQiwang { get; set; } + public decimal GwsrkhJineHege { get; set; } + public decimal GwsrkhJineShengren { get; set; } + public decimal GwsrkhJineChaoshengren { get; set; } + public decimal Jiaban_GwsrkhJineQianhege { get; set; } + public decimal Jiaban_GwsrkhJineQiwang { get; set; } + public decimal Jiaban_GwsrkhJineHege { get; set; } + public decimal Jiaban_GwsrkhJineShengren { get; set; } + public decimal Jiaban_GwsrkhJineChaoshengren { get; set; } +} + +// ==================== 薪资配置-挡车/车头岗位 ==================== + +public class ConfDcgListItemDto +{ + public long Id { get; set; } + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public decimal JineJisuanjishu50 { get; set; } + public decimal JineJisuanjishu60 { get; set; } + public decimal JineJisuanjishu70 { get; set; } + public decimal JineJisuanjishu100 { get; set; } + public decimal JineCtg { get; set; } +} + +public class ConfDcgSaveDto +{ + public long Id { get; set; } + public long DepId { get; set; } + public decimal JineJisuanjishu50 { get; set; } + public decimal JineJisuanjishu60 { get; set; } + public decimal JineJisuanjishu70 { get; set; } + public decimal JineJisuanjishu100 { get; set; } + public decimal JineCtg { get; set; } +} + +// ==================== 挡车纤度奖扣 ==================== + +public class DcxdjkListItemDto +{ + public long Id { get; set; } + public string GuiGe { get; set; } = ""; + public decimal XianDu { get; set; } + public decimal JiangLi { get; set; } + public decimal KouKuan { get; set; } +} + +public class DcxdjkSaveDto +{ + public long Id { get; set; } + public string GuiGe { get; set; } = ""; + public decimal XianDu { get; set; } + public decimal JiangLi { get; set; } + public decimal KouKuan { get; set; } +} + +// ==================== 薪资价格配置 ==================== + +public class XzjgListItemDto +{ + public long Id { get; set; } + public string Group { get; set; } = ""; + public string Key { get; set; } = ""; + public string Unit { get; set; } = ""; + public decimal Price { get; set; } + public string Memo { get; set; } = ""; +} + +public class XzjgSaveDto +{ + public long Id { get; set; } + public string Group { get; set; } = ""; + public string Key { get; set; } = ""; + public string Unit { get; set; } = ""; + public decimal Price { get; set; } + public string Memo { get; set; } = ""; +} diff --git a/backend/F9WebAiTest.Application/Dtos/Prod/HsDtos.cs b/backend/F9WebAiTest.Application/Dtos/Prod/HsDtos.cs new file mode 100644 index 0000000..0fe3028 --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/Prod/HsDtos.cs @@ -0,0 +1,105 @@ +namespace F9WebAiTest.Application.Dtos.Prod; + +// ==================== 秤大丝(成筒丝 CDS) ==================== + +/// 秤大丝列表行(落丝记录 + 秤丝汇总) +public class CdsListItemDto +{ + public long LsjlId { get; set; } + public DateTime WorkDate { get; set; } + public long DeptId { get; set; } + public string DeptName { get; set; } = ""; + public int Jizu { get; set; } + public int Huishu { get; set; } + public string Guige { get; set; } = ""; + public string ZhuangKouName { get; set; } = ""; + public string Yanse { get; set; } = ""; + public bool Isbanche { get; set; } + public string IsbancheName { get; set; } = ""; + public int Chetaishu { get; set; } + public int ChetaiStart { get; set; } + public int ChetaiEnd { get; set; } + public double Hours { get; set; } + public int Tongshu { get; set; } + public int Meitongjianliang { get; set; } + public int YingchengNum { get; set; } // 应秤数 = 车台数 × 4 × (半车 ? 0.5 : 1) + public bool IsWeighed { get; set; } // 是否已秤 + public int Num { get; set; } // 已秤数 + public int Siliang { get; set; } // 已秤丝量(g) + public int Jianliang { get; set; } // 茧量(g) + public decimal Saozhe { get; set; } // 缫折 + public decimal Taichan { get; set; } // 台产 +} + +/// 秤大丝称重弹窗数据 +public class CdsEditDto +{ + public long LsjlId { get; set; } + public string DeptName { get; set; } = ""; + public int Jizu { get; set; } + public int Huishu { get; set; } + public string ZhuangKouName { get; set; } = ""; + public string Yanse { get; set; } = ""; + public bool Isbanche { get; set; } + public string IsbancheName { get; set; } = ""; + public string Guige { get; set; } = ""; + public int ChetaiStart { get; set; } + public int ChetaiEnd { get; set; } + public int Chetaishu { get; set; } + public int YingchengNum { get; set; } + public List Chetais { get; set; } = new(); +} + +/// 秤大丝车台明细 DTO +public class CdsChetaiDto +{ + public int Chetaihao { get; set; } + public int Sl1 { get; set; } + public int Sl2 { get; set; } + public int Sl3 { get; set; } + public int Sl4 { get; set; } + public int Num { get; set; } // 个数 + public int Siliang { get; set; } // 小计丝量 +} + +/// 秤大丝保存请求 +public class CdsSaveDto +{ + public long LsjlId { get; set; } + public List Chetais { get; set; } = new(); +} + +// ==================== 公量纤度 ==================== + +/// 公量纤度列表行(落丝记录 + 纤度检验平均纤度 + 公量纤度) +public class GlxdListItemDto +{ + public long LsjlId { get; set; } + public DateTime WorkDate { get; set; } + public long DeptId { get; set; } + public string DeptName { get; set; } = ""; + public int Jizu { get; set; } + public int Huishu { get; set; } + public string Guige { get; set; } = ""; + public string ZhuangKouName { get; set; } = ""; + public string Yanse { get; set; } = ""; + public bool Isbanche { get; set; } + public string IsbancheName { get; set; } = ""; + public int Chetaishu { get; set; } + public double Avgxiandu { get; set; } // 平均纤度(来自纤度检验) + public decimal Gongliangxiandu { get; set; } // 公量纤度 +} + +/// 公量纤度批量保存请求 +public class GlxdSaveDto +{ + public DateTime Date { get; set; } + public long DeptId { get; set; } + public List Items { get; set; } = new(); +} + +public class GlxdSaveItemDto +{ + public long LsjlId { get; set; } + public decimal Gongliangxiandu { get; set; } +} diff --git a/backend/F9WebAiTest.Application/Dtos/Prod/HsTzDtos.cs b/backend/F9WebAiTest.Application/Dtos/Prod/HsTzDtos.cs new file mode 100644 index 0000000..529c997 --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/Prod/HsTzDtos.cs @@ -0,0 +1,129 @@ +namespace F9WebAiTest.Application.Dtos.Prod; + +// ==================== 丝片检查 ==================== + +/// 丝片检查列表行 +public class SpjcListItemDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public long DeptId { get; set; } + public string DeptName { get; set; } = ""; + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public string Guige { get; set; } = ""; + public int Pianshu { get; set; } + public int PianshuXuegong { get; set; } + + public int QsJiahua { get; set; } + public int QsYouwu { get; set; } + public int QsHeidian { get; set; } + public int QsShangsi { get; set; } + public int QsYetiao { get; set; } + public int QsXiaosipian { get; set; } + public int QsQita { get; set; } + public int QsHeji { get; set; } + + public int HsYouwu { get; set; } + public int HsShangsi { get; set; } + public int HsShuangsi { get; set; } + public int HsZhisi { get; set; } + public int HsZhantiao { get; set; } + public int HsYingjiao { get; set; } + public int HsGuojiao { get; set; } + public int HsShouchu { get; set; } + public int HsWuran { get; set; } + public int HsLiansi { get; set; } + public int HsWdmt { get; set; } + public int HsMaoluan { get; set; } + public int HsQita { get; set; } + public int HsHeji { get; set; } + + public string Memo { get; set; } = ""; +} + +/// 丝片检查新增/修改:所有数量前端录入,合计由服务端重算 +public class SpjcSaveDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public long DeptId { get; set; } + public string DeptName { get; set; } = ""; // 仅用于编辑回填显示 + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; // 仅用于编辑回填显示 + public string Guige { get; set; } = ""; + public int Pianshu { get; set; } + public int PianshuXuegong { get; set; } + + public int QsJiahua { get; set; } + public int QsYouwu { get; set; } + public int QsHeidian { get; set; } + public int QsShangsi { get; set; } + public int QsYetiao { get; set; } + public int QsXiaosipian { get; set; } + public int QsQita { get; set; } + + public int HsYouwu { get; set; } + public int HsShangsi { get; set; } + public int HsShuangsi { get; set; } + public int HsZhisi { get; set; } + public int HsZhantiao { get; set; } + public int HsYingjiao { get; set; } + public int HsGuojiao { get; set; } + public int HsShouchu { get; set; } + public int HsWuran { get; set; } + public int HsLiansi { get; set; } + public int HsWdmt { get; set; } + public int HsMaoluan { get; set; } + public int HsQita { get; set; } + + public string Memo { get; set; } = ""; +} + +// ==================== 丝片台帐 ==================== + +/// 丝片台帐列表行 +public class SptzListItemDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public DateTime DateLast { get; set; } + public int JsShangcun { get; set; } + public int JsXiasi { get; set; } + public int JsJinsi { get; set; } + public int JsSrylJrxz { get; set; } + public int JsJincun { get; set; } + public int FsFsps { get; set; } + public int BsBsps { get; set; } + public int ZlZhengpin { get; set; } + public int ZlTichu { get; set; } + public string Memo { get; set; } = ""; +} + +/// 丝片台帐新增默认值:上一日本台账 + 按当日「丝片检查」汇总出的整理片数 +public class SptzDefaultsDto +{ + public DateTime Date { get; set; } + public DateTime? DateLast { get; set; } // 上一本台账日期(无则为 null) + public int JsShangcun { get; set; } // 上存 = 上一日今存 + public DateTime? LatestDate { get; set; } // 当前最新台账日期(用于「无法新增该日期台账」校验) + public int ZlTichu { get; set; } // 剔除片数 = Σ丝片检查前缫合计 + public int ZlZhengpin { get; set; } // 正品片数 = Σ(总片数 + 学工片数) − 剔除 + public bool HasSpjc { get; set; } // 当日是否存在丝片检查数据 +} + +/// 丝片台帐保存请求 +public class SptzSaveDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public DateTime DateLast { get; set; } + public int JsShangcun { get; set; } + public int JsXiasi { get; set; } + public int JsJinsi { get; set; } + public int FsFsps { get; set; } + public int BsBsps { get; set; } + public int ZlZhengpin { get; set; } + public int ZlTichu { get; set; } + public string Memo { get; set; } = ""; +} diff --git a/backend/F9WebAiTest.Application/Dtos/Prod/KaoqinDtos.cs b/backend/F9WebAiTest.Application/Dtos/Prod/KaoqinDtos.cs new file mode 100644 index 0000000..0711dbd --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/Prod/KaoqinDtos.cs @@ -0,0 +1,187 @@ +namespace F9WebAiTest.Application.Dtos.Prod; + +// ==================== 考勤登记(CQDJ) ==================== + +/// 登记明细(读 / 写共用) +public class CqdjItemDto +{ + public long Id { get; set; } + public long DepId { get; set; } // 出勤部门(流动 / 代班时可为其它部门) + public long WorktypeId { get; set; } // 出勤工种 + public int TypeCq { get; set; } // 出勤类型(1正常 2请假 3流动 4加班 5代班 6外派 7旷工 8工伤) + public int TypeQj { get; set; } // 请假类型(0无 1事假 2病假 3产假 4婚假 5丧假 6公假 7年假 9调休 10轮休) + public decimal Hours { get; set; } + public string Jizu { get; set; } = ""; + public string CtStart { get; set; } = ""; + public string CtEnd { get; set; } = ""; + public int DangcheNum { get; set; } + public string Memo { get; set; } = ""; +} + +/// 登记行(一人一天) +public class CqdjRowDto +{ + public long Id { get; set; } // 已保存的主表 Id(0=未保存) + public long EmployeeId { get; set; } + public string EmployeeCode { get; set; } = ""; + public string EmployeeName { get; set; } = ""; + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public long WorktypeId { get; set; } + public string WorktypeName { get; set; } = ""; + public int Banci { get; set; } + public bool IsXuegong { get; set; } // 学工(未转正) + public bool Saved { get; set; } // 是否已入库 + + public decimal HoursStandard { get; set; } + public decimal HoursCq { get; set; } + public decimal HoursQj { get; set; } + public decimal HoursLd { get; set; } + public decimal HoursJb { get; set; } + public decimal HoursDb { get; set; } + public decimal HoursWp { get; set; } + public decimal HoursKg { get; set; } + public decimal HoursGs { get; set; } + public int CsYeban { get; set; } + public string Memo { get; set; } = ""; + + public List Items { get; set; } = new(); +} + +/// 登记查询结果(一次性返回整组,前端整组编辑) +public class CqdjViewDto +{ + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public long DepId { get; set; } + public long WorktypeId { get; set; } + public int Banci { get; set; } + public List Rows { get; set; } = new(); +} + +/// 整组保存入参(先软删该 日期+部门+工种+班次 的旧登记,再整批新增) +public class CqdjSaveDto +{ + public DateTime Date { get; set; } + public long DepId { get; set; } + public long WorktypeId { get; set; } // 0 = 全部工种 + public int Banci { get; set; } + public List Rows { get; set; } = new(); +} + +public class CqdjSaveRowDto +{ + public long EmployeeId { get; set; } + public long DepId { get; set; } // 0 = 取职工所在部门 + public long WorktypeId { get; set; } // 0 = 取职工工种 + public bool IsXuegong { get; set; } + public decimal HoursStandard { get; set; } + public int CsYeban { get; set; } + public string Memo { get; set; } = ""; + public List Items { get; set; } = new(); +} + +/// 「添加职工」下拉选项 +public class CqdjEmpOptionDto +{ + public long Id { get; set; } + public string Code { get; set; } = ""; + public string Name { get; set; } = ""; + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public long WorktypeId { get; set; } + public string WorktypeName { get; set; } = ""; + public bool IsXuegong { get; set; } +} + +// ==================== 考勤统计(CQTJ) ==================== + +/// 统计行(一人一行,月快照) +public class CqtjRowDto +{ + public long Id { get; set; } + public int Year { get; set; } + public int Month { get; set; } + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public long WorktypeId { get; set; } + public string WorktypeName { get; set; } = ""; + public long EmployeeId { get; set; } + public string EmployeeCode { get; set; } = ""; + public string EmployeeName { get; set; } = ""; + + public decimal DaysCqStandard { get; set; } + public decimal DaysCqActual { get; set; } + public decimal CqTianci { get; set; } + + public decimal HoursCq { get; set; } + public decimal HoursQj { get; set; } + public decimal HoursLd { get; set; } + public decimal HoursJb { get; set; } + public decimal HoursDb { get; set; } + public decimal HoursWp { get; set; } + public decimal HoursKg { get; set; } + public int CsYeban { get; set; } + + public decimal HoursQjShijia { get; set; } + public decimal HoursQjBj { get; set; } + public decimal HoursQjHj { get; set; } + public decimal HoursQjCj { get; set; } + public decimal HoursQjLx { get; set; } + public decimal HoursQjSangjia { get; set; } + public decimal HoursQjGj { get; set; } + public decimal HoursQjNj { get; set; } + public decimal HoursQjTx { get; set; } + + public decimal HoursXgCq { get; set; } + public decimal HoursXgJiaban { get; set; } + + public decimal HoursFadingjiaqi { get; set; } + public decimal HoursQjQqmc { get; set; } + public decimal HoursQjQqcal { get; set; } + + public decimal JineQuanqin { get; set; } + public decimal JineJb { get; set; } + public decimal JineLiudong { get; set; } + public decimal JineDaiban { get; set; } + public decimal JineYb { get; set; } + public decimal JineKgKk { get; set; } + + public decimal JineJqBzHj { get; set; } + public decimal JineJqBzBj { get; set; } + public decimal JineJqBzLx { get; set; } + public decimal JineJqBzSangjia { get; set; } + public decimal JineJqBzFading { get; set; } + public decimal JineJqBzTxnj { get; set; } + public decimal JineJqBz { get; set; } + + public string Memo { get; set; } = ""; +} + +/// 统计整组保存(先软删该 年/月/部门 的旧快照,再整批新增) +public class CqtjSaveDto +{ + public int Year { get; set; } + public int Month { get; set; } + public long DepId { get; set; } + public List Rows { get; set; } = new(); +} + +/// 一键生成入参 +public class CqtjGenerateDto +{ + public int Year { get; set; } + public int Month { get; set; } +} + +/// 统计录入视图(附加展示用的账期信息) +public class CqtjViewDto +{ + public int Year { get; set; } + public int Month { get; set; } + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public decimal QsQuanqinDays { get; set; } // 薪资配置账期的前缫出勤天数 + public bool Exists { get; set; } // true=库中已有快照,false=本次实时算出 + public List Rows { get; set; } = new(); +} diff --git a/backend/F9WebAiTest.Application/Dtos/Prod/LsjlDtos.cs b/backend/F9WebAiTest.Application/Dtos/Prod/LsjlDtos.cs index 76dbc2a..aa9e3e8 100644 --- a/backend/F9WebAiTest.Application/Dtos/Prod/LsjlDtos.cs +++ b/backend/F9WebAiTest.Application/Dtos/Prod/LsjlDtos.cs @@ -18,6 +18,8 @@ public class LsjlDto public bool Isbanche { get; set; } public string IsbancheName { get; set; } public double Hours { get; set; } + public int Tongshu { get; set; } + public int Meitongjianliang { get; set; } public string Guige { get; set; } public double Chesu { get; set; } } @@ -36,6 +38,8 @@ public class LsjlCreateDto public int Chetaishu { get; set; } public bool Isbanche { get; set; } public double Hours { get; set; } + public int Tongshu { get; set; } + public int Meitongjianliang { get; set; } public string Guige { get; set; } = ""; public double Chesu { get; set; } } diff --git a/backend/F9WebAiTest.Application/Dtos/Prod/XjrbDtos.cs b/backend/F9WebAiTest.Application/Dtos/Prod/XjrbDtos.cs new file mode 100644 index 0000000..c04abbb --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/Prod/XjrbDtos.cs @@ -0,0 +1,228 @@ +using F9WebAiTest.Domain.Entities.Prod; + +namespace F9WebAiTest.Application.Dtos.Prod; + +// ==================== 选茧日报 ==================== + +public class XjrbListDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + + public int YlLrBaoshu { get; set; } + public decimal YlLrZhongliang { get; set; } + + public int LtjBaoshu { get; set; } + public decimal LtjZhongliang { get; set; } + public int HwjjBaoshu { get; set; } + public decimal HwjjZhongliang { get; set; } + public int LjjBaoshu { get; set; } + public decimal LjjZhongliang { get; set; } + public int MjBaoshu { get; set; } + public decimal MjZhongliang { get; set; } + public int SgjBaoshu { get; set; } + public decimal SgjZhongliang { get; set; } + public int XjjBaoshu { get; set; } + public decimal XjjZhongliang { get; set; } + public int JyBaoshu { get; set; } + public decimal JyZhongliang { get; set; } + public int JhBaoshu { get; set; } + public decimal JhZhongliang { get; set; } + + public decimal TotalZhongliang { get; set; } + public int ThBaoshu { get; set; } + public decimal ThZhongliang { get; set; } + public decimal LiantongRate { get; set; } + public decimal Yingkui { get; set; } +} + +public class XjrbSaveDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public long GyzkId { get; set; } + + public int YlLrBaoshu { get; set; } + public decimal YlLrZhongliang { get; set; } + + public int LtjBaoshu { get; set; } + public decimal LtjZhongliang { get; set; } + public int HwjjBaoshu { get; set; } + public decimal HwjjZhongliang { get; set; } + public int LjjBaoshu { get; set; } + public decimal LjjZhongliang { get; set; } + public int MjBaoshu { get; set; } + public decimal MjZhongliang { get; set; } + public int SgjBaoshu { get; set; } + public decimal SgjZhongliang { get; set; } + public int XjjBaoshu { get; set; } + public decimal XjjZhongliang { get; set; } + public int JyBaoshu { get; set; } + public decimal JyZhongliang { get; set; } + public int JhBaoshu { get; set; } + public decimal JhZhongliang { get; set; } + + public int ThBaoshu { get; set; } + public decimal ThZhongliang { get; set; } + public decimal LiantongRate { get; set; } + public decimal Yingkui { get; set; } +} + +/// 盈亏 / 联统率计算结果(对齐 F9 GetXjrbCountYingkuiQuery 返回字典) +public class XjrbYingkuiDto +{ + public decimal LiantongRate { get; set; } + public decimal Yingkui { get; set; } + public DateTime StartDate { get; set; } + public string StartDateText => StartDate.ToString("yyyy-MM-dd"); +} + +// ==================== 选茧计划 ==================== + +public class XjrbPlanListDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public long YllingliaodanId { get; set; } + public string YllingliaodanCode { get; set; } = ""; + + public int YlBaoshu { get; set; } + public decimal YlZhongliang { get; set; } + + public int LtjBaoshu { get; set; } + public decimal LtjZhongliang { get; set; } + public int HwjjBaoshu { get; set; } + public decimal HwjjZhongliang { get; set; } + public int WjjBaoshu { get; set; } + public decimal WjjZhongliang { get; set; } + public int SgjBaoshu { get; set; } + public decimal SgjZhongliang { get; set; } + public int XjjBaoshu { get; set; } + public decimal XjjZhongliang { get; set; } + public int JyBaoshu { get; set; } + public decimal JyZhongliang { get; set; } + public int JhBaoshu { get; set; } + public decimal JhZhongliang { get; set; } + + public decimal TotalZhongliang { get; set; } + public decimal LiantongRate { get; set; } + public decimal Yingkui { get; set; } + public bool IsFinish { get; set; } +} + +public class XjrbPlanSaveDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public long GyzkId { get; set; } + public long YllingliaodanId { get; set; } + + public int YlBaoshu { get; set; } + public decimal YlZhongliang { get; set; } + public int LtjBaoshu { get; set; } + public decimal LtjZhongliang { get; set; } + public int HwjjBaoshu { get; set; } + public decimal HwjjZhongliang { get; set; } + public int WjjBaoshu { get; set; } + public decimal WjjZhongliang { get; set; } + public int SgjBaoshu { get; set; } + public decimal SgjZhongliang { get; set; } + public int XjjBaoshu { get; set; } + public decimal XjjZhongliang { get; set; } + public int JyBaoshu { get; set; } + public decimal JyZhongliang { get; set; } + public int JhBaoshu { get; set; } + public decimal JhZhongliang { get; set; } +} + +// ==================== 选茧计划明细 ==================== + +public class XjrbItemDto +{ + public long Id { get; set; } + public long XjrbPlanId { get; set; } + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + + public int LtjBaoshu { get; set; } + public decimal LtjZhongliang { get; set; } + public int HwjjBaoshu { get; set; } + public decimal HwjjZhongliang { get; set; } + public int WjjBaoshu { get; set; } + public decimal WjjZhongliang { get; set; } + public int SgjBaoshu { get; set; } + public decimal SgjZhongliang { get; set; } + public int XjjBaoshu { get; set; } + public decimal XjjZhongliang { get; set; } + public int JyBaoshu { get; set; } + public decimal JyZhongliang { get; set; } + public int JhBaoshu { get; set; } + public decimal JhZhongliang { get; set; } + + public decimal TotalZhongliang { get; set; } + public bool IsFinish { get; set; } +} + +public class XjrbItemSaveDto +{ + public long Id { get; set; } + public long XjrbPlanId { get; set; } + public DateTime Date { get; set; } + + public int LtjBaoshu { get; set; } + public decimal LtjZhongliang { get; set; } + public int HwjjBaoshu { get; set; } + public decimal HwjjZhongliang { get; set; } + public int WjjBaoshu { get; set; } + public decimal WjjZhongliang { get; set; } + public int SgjBaoshu { get; set; } + public decimal SgjZhongliang { get; set; } + public int XjjBaoshu { get; set; } + public decimal XjjZhongliang { get; set; } + public int JyBaoshu { get; set; } + public decimal JyZhongliang { get; set; } + public int JhBaoshu { get; set; } + public decimal JhZhongliang { get; set; } + + public bool IsFinish { get; set; } +} + +// ==================== 选茧班组 ==================== + +public class XjpbBanzuDto +{ + public long Id { get; set; } + public string Banci { get; set; } = ""; + public int Zu { get; set; } +} + +public class XjpbBanzuZhigongDto +{ + public long Id { get; set; } + public long BanzuId { get; set; } + public string Banci { get; set; } = ""; + public int Zu { get; set; } + public long YgId { get; set; } + public string YgName { get; set; } = ""; + public string YgPhone { get; set; } = ""; + public int Sort { get; set; } +} + +/// 班组「编辑录入」保存项:职工 + 是否选中 + 组别(对齐 F9 XjpbBanzuZhigongCombinedDto) +public class XjpbBanzuZhigongSaveDto +{ + public long Id { get; set; } + public string Banci { get; set; } = ""; + public int Zu { get; set; } + public long YgId { get; set; } + public string YgName { get; set; } = ""; + public string YgPhone { get; set; } = ""; + public int Sort { get; set; } + public bool Selected { get; set; } +} diff --git a/backend/F9WebAiTest.Application/Dtos/Prod/ZjrbDtos.cs b/backend/F9WebAiTest.Application/Dtos/Prod/ZjrbDtos.cs new file mode 100644 index 0000000..9fcc04b --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/Prod/ZjrbDtos.cs @@ -0,0 +1,111 @@ +using F9WebAiTest.Domain.Enums.SYS.Employees; + +namespace F9WebAiTest.Application.Dtos.Prod; + +// ==================== 煮茧日报 ==================== + +/// 煮茧日报列表行(主表 + 机组桶数合计) +public class ZjrbReadDto_View +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public string Guige { get; set; } = ""; + public DateTime? LastDate { get; set; } + public int StandardLuositongshu { get; set; } + public int MeitongJianliang { get; set; } + + public int BaoshuShangcun { get; set; } + public int BaoshuLingru { get; set; } + public int BaoshuSaosi { get; set; } + public int BaoshuTuihui { get; set; } + public int BaoshuJincun { get; set; } + + public decimal JianliangShangcun { get; set; } + public decimal JianliangLingru { get; set; } + public decimal JianliangSaosi { get; set; } + public decimal JianliangTuihui { get; set; } + public decimal JianliangJincun { get; set; } + + // 机组桶数合计(列表下方明细/列展示用) + public int TongshuShangcun { get; set; } + public int TongshuJinsao { get; set; } + public int TongshuJinluo { get; set; } + public int TongshuJincun { get; set; } + public decimal JizuSaosiHours { get; set; } +} + +/// 煮茧日报-机组成员 +public class ZjrbJizuDto +{ + public long Id { get; set; } + public long ZjrbId { get; set; } + public int Jizu { get; set; } + public decimal JizuSaosiHours { get; set; } + public int TongshuShangcun { get; set; } + /// 甲班今缫桶数(存于机组班次表) + public int TongshuJinsaoJiaban { get; set; } + /// 乙班今缫桶数(存于机组班次表) + public int TongshuJinsaoYiban { get; set; } + /// 今缫桶数 = 甲班 + 乙班(只读合计) + public int TongshuJinsao { get; set; } + public int TongshuJinluo { get; set; } + public int TongshuJincun { get; set; } +} + +/// 煮茧日报-班次成员(包数 / 重量) +public class ZjrbBanciDto +{ + public long Id { get; set; } + public long ZjrbId { get; set; } + public Prod_Banci_Enum Banci { get; set; } + public string BanciName { get; set; } = ""; + + public int BaoshuShangcun { get; set; } + public int BaoshuLingru { get; set; } + public int BaoshuSaosi { get; set; } + public int BaoshuTuihui { get; set; } + public int BaoshuJincun { get; set; } + + public decimal JianliangShangcun { get; set; } + public decimal JianliangLingru { get; set; } + public decimal JianliangSaosi { get; set; } + public decimal JianliangTuihui { get; set; } + public decimal JianliangJincun { get; set; } +} + +/// 煮茧日报整条保存(主表 + 班次 + 机组 + 机组班次) +public class ZjrbSaveDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public string Guige { get; set; } = ""; + public DateTime? LastDate { get; set; } + public int StandardLuositongshu { get; set; } + public int MeitongJianliang { get; set; } + + /// 联统茧标重(kg/包),来自参数配置「生产\前缫\煮茧 → 联统茧标重」,未配置为 0 + public decimal LiantongBiaozhong { get; set; } + + public int BaoshuShangcun { get; set; } + public int BaoshuLingru { get; set; } + public int BaoshuSaosi { get; set; } + public int BaoshuTuihui { get; set; } + public int BaoshuJincun { get; set; } + + public decimal JianliangShangcun { get; set; } + public decimal JianliangLingru { get; set; } + public decimal JianliangSaosi { get; set; } + public decimal JianliangTuihui { get; set; } + public decimal JianliangJincun { get; set; } + + public List Bancis { get; set; } = new(); + public List Jizus { get; set; } = new(); +} diff --git a/backend/F9WebAiTest.Application/Dtos/Wms/WmsDtos.cs b/backend/F9WebAiTest.Application/Dtos/Wms/WmsDtos.cs new file mode 100644 index 0000000..d404c07 --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/Wms/WmsDtos.cs @@ -0,0 +1,695 @@ +using F9WebAiTest.Domain.Entities.Wms; + +namespace F9WebAiTest.Application.Dtos.Wms; + +/// 下拉选项(单据类型 / 台账项目 / 往来单位 / 部门 / 庄口 / 物资) +public class WmsOptionDto +{ + public long Id { get; set; } + public string Name { get; set; } = ""; + public string Code { get; set; } = ""; + public string Ext { get; set; } = ""; +} + +public class WmsTypeOptionDto +{ + public int Value { get; set; } + public string Name { get; set; } = ""; +} + +public class WmsOptionsDto +{ + public List RukuTypes { get; set; } = new(); + public List ChukuTypes { get; set; } = new(); + public List Wuzi { get; set; } = new(); + public List YuanliaoZhuangkou { get; set; } = new(); + public List CraftZhuangkou { get; set; } = new(); + public List Departments { get; set; } = new(); + public List Gongyingshangs { get; set; } = new(); +} + +// ==================== 入库 ==================== + +public class WmsRukuListDto +{ + public long Id { get; set; } + public int Module { get; set; } + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public int Type { get; set; } + public string TypeName { get; set; } = ""; + public string Code { get; set; } = ""; + public long GysId { get; set; } + public string GysName { get; set; } = ""; + public long KhId { get; set; } + public string KhName { get; set; } = ""; + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public string Lianxiren { get; set; } = ""; + public string Phone { get; set; } = ""; + public string Carno { get; set; } = ""; + public string Kuguan { get; set; } = ""; + public string Caigouyuan { get; set; } = ""; + public string PayStatus { get; set; } = ""; + public int RkStatus { get; set; } + public string RkStatusName { get; set; } = ""; + public int BxStatus { get; set; } + public string BxStatusName { get; set; } = ""; + public string Description { get; set; } = ""; + public int Jianshu { get; set; } + public int BaojianNum { get; set; } + public decimal Jingzhong { get; set; } + public decimal Shuliang { get; set; } + public decimal Jine { get; set; } + public int DetailCount { get; set; } +} + +public class WmsRukuDetailDto +{ + public long Id { get; set; } + public long RukuId { get; set; } + public long WzmlId { get; set; } + public string WzmlName { get; set; } = ""; + public string WzmlCode { get; set; } = ""; + public string Unit { get; set; } = ""; + public long YlzkId { get; set; } + public string YlzkName { get; set; } = ""; + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public int Jianshu { get; set; } + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public decimal BiaobaoZhongliang { get; set; } + public decimal BaozhuangZhongliang { get; set; } + public decimal Shuliang { get; set; } + public decimal Danjia { get; set; } + public decimal Jine { get; set; } + public decimal Sunhao { get; set; } + public string PiciCodes { get; set; } = ""; + public int PiciNum { get; set; } + public int BaojianNum { get; set; } + public string PiciIds { get; set; } = ""; + public string Banci { get; set; } = ""; + public string JidiWaigou { get; set; } = ""; + public List Bangmadans { get; set; } = new(); +} + +public class WmsBangmadanDto +{ + public long Id { get; set; } + public long DetailId { get; set; } + public bool IsChuku { get; set; } + public int Jianshu { get; set; } + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public DateTime? GuobangShijian { get; set; } +} + +public class WmsRukuEditDto +{ + public long Id { get; set; } + public int Module { get; set; } + public DateTime Date { get; set; } + public int Type { get; set; } + public string Code { get; set; } = ""; + public long GysId { get; set; } + public string GysName { get; set; } = ""; + public long KhId { get; set; } + public string KhName { get; set; } = ""; + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public string Lianxiren { get; set; } = ""; + public string Phone { get; set; } = ""; + public string Carno { get; set; } = ""; + public string Kuguan { get; set; } = ""; + public string Caigouyuan { get; set; } = ""; + public string PayStatus { get; set; } = ""; + public int RkStatus { get; set; } + public int BxStatus { get; set; } + public string Description { get; set; } = ""; + public List Details { get; set; } = new(); +} + +public class WmsRukuSaveDto +{ + public long Id { get; set; } + public int Module { get; set; } + public DateTime Date { get; set; } + public int Type { get; set; } + public string Code { get; set; } = ""; + public long GysId { get; set; } + public long KhId { get; set; } + public long GyzkId { get; set; } + public long DepId { get; set; } + public string Lianxiren { get; set; } = ""; + public string Phone { get; set; } = ""; + public string Carno { get; set; } = ""; + public string Kuguan { get; set; } = ""; + public string Caigouyuan { get; set; } = ""; + public string PayStatus { get; set; } = ""; + public int RkStatus { get; set; } + public int BxStatus { get; set; } + public string Description { get; set; } = ""; + public List Details { get; set; } = new(); +} + +public class WmsRukuDetailSaveDto +{ + public long Id { get; set; } + public long WzmlId { get; set; } + public long YlzkId { get; set; } + public long GyzkId { get; set; } + public long DepId { get; set; } + public int Jianshu { get; set; } + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public decimal BiaobaoZhongliang { get; set; } + public decimal BaozhuangZhongliang { get; set; } + public decimal Shuliang { get; set; } + public decimal Danjia { get; set; } + public decimal Jine { get; set; } + public decimal Sunhao { get; set; } + public string PiciCodes { get; set; } = ""; + public int PiciNum { get; set; } + public int BaojianNum { get; set; } + public string PiciIds { get; set; } = ""; + public string Banci { get; set; } = ""; + public string JidiWaigou { get; set; } = ""; + public List Bangmadans { get; set; } = new(); +} + +// ==================== 出库 ==================== + +public class WmsChukuListDto +{ + public long Id { get; set; } + public int Module { get; set; } + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public int Type { get; set; } + public string TypeName { get; set; } = ""; + public string Code { get; set; } = ""; + public long WldwId { get; set; } + public string WldwName { get; set; } = ""; + public long KhId { get; set; } + public string KhName { get; set; } = ""; + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public long LingliaoDepId { get; set; } + public string LingliaoDepName { get; set; } = ""; + public long UseDepId { get; set; } + public string UseDepName { get; set; } = ""; + public long DbdepId { get; set; } + public string DbdepName { get; set; } = ""; + public string Lingliaoren { get; set; } = ""; + public string Kuguan { get; set; } = ""; + public string UseLocation { get; set; } = ""; + public int CkStatus { get; set; } + public string CkStatusName { get; set; } = ""; + public bool IsHongchong { get; set; } + public string Description { get; set; } = ""; + public int Jianshu { get; set; } + public decimal Jingzhong { get; set; } + public decimal Shuliang { get; set; } + public decimal Jine { get; set; } + public int DetailCount { get; set; } +} + +public class WmsChukuDetailDto +{ + public long Id { get; set; } + public long ChukuId { get; set; } + public long WzmlId { get; set; } + public string WzmlName { get; set; } = ""; + public string WzmlCode { get; set; } = ""; + public string Unit { get; set; } = ""; + public long YlzkId { get; set; } + public string YlzkName { get; set; } = ""; + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public int Jianshu { get; set; } + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public decimal BiaobaoZhongliang { get; set; } + public decimal BaozhuangZhongliang { get; set; } + public decimal Shuliang { get; set; } + public decimal Danjia { get; set; } + public decimal Jine { get; set; } + public decimal KucunNum { get; set; } + public string PiciCodes { get; set; } = ""; + public int PiciNum { get; set; } + public string PcAndBjIds { get; set; } = ""; + public string Lingyongren { get; set; } = ""; + public string Banci { get; set; } = ""; + public string JidiWaigou { get; set; } = ""; + public List Bangmadans { get; set; } = new(); +} + +public class WmsChukuEditDto +{ + public long Id { get; set; } + public int Module { get; set; } + public DateTime Date { get; set; } + public int Type { get; set; } + public string Code { get; set; } = ""; + public long WldwId { get; set; } + public string WldwName { get; set; } = ""; + public long KhId { get; set; } + public string KhName { get; set; } = ""; + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public long LingliaoDepId { get; set; } + public string LingliaoDepName { get; set; } = ""; + public long UseDepId { get; set; } + public string UseDepName { get; set; } = ""; + public long DbdepId { get; set; } + public string DbdepName { get; set; } = ""; + public string Lingliaoren { get; set; } = ""; + public string Kuguan { get; set; } = ""; + public string UseLocation { get; set; } = ""; + public int CkStatus { get; set; } + public bool IsHongchong { get; set; } + public long HongchongId { get; set; } + public string Description { get; set; } = ""; + public List Details { get; set; } = new(); +} + +public class WmsChukuSaveDto +{ + public long Id { get; set; } + public int Module { get; set; } + public DateTime Date { get; set; } + public int Type { get; set; } + public string Code { get; set; } = ""; + public long WldwId { get; set; } + public long KhId { get; set; } + public long GyzkId { get; set; } + public long LingliaoDepId { get; set; } + public long UseDepId { get; set; } + public long DbdepId { get; set; } + public string Lingliaoren { get; set; } = ""; + public string Kuguan { get; set; } = ""; + public string UseLocation { get; set; } = ""; + public int CkStatus { get; set; } + public string Description { get; set; } = ""; + public List Details { get; set; } = new(); +} + +public class WmsChukuDetailSaveDto +{ + public long Id { get; set; } + public long WzmlId { get; set; } + public long YlzkId { get; set; } + public long GyzkId { get; set; } + public long DepId { get; set; } + public int Jianshu { get; set; } + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public decimal BiaobaoZhongliang { get; set; } + public decimal BaozhuangZhongliang { get; set; } + public decimal Shuliang { get; set; } + public decimal Danjia { get; set; } + public decimal Jine { get; set; } + public decimal KucunNum { get; set; } + public string PiciCodes { get; set; } = ""; + public int PiciNum { get; set; } + public string PcAndBjIds { get; set; } = ""; + public string Lingyongren { get; set; } = ""; + public string Banci { get; set; } = ""; + public string JidiWaigou { get; set; } = ""; + public List Bangmadans { get; set; } = new(); +} + +// ==================== 库存台账 ==================== + +public class WmsTzDateDto +{ + public long Id { get; set; } + public int Module { get; set; } + public int Kind { get; set; } + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public DateTime LastDate { get; set; } + public string LastDateText => LastDate.ToString("yyyy-MM-dd"); + public int ItemCount { get; set; } +} + +public class WmsTzItemDto +{ + public long Id { get; set; } + public long TzDateId { get; set; } + public long ItemId { get; set; } + public string SubKey { get; set; } = ""; + public string ItemName { get; set; } = ""; + public string ItemCode { get; set; } = ""; + public decimal Ydayjianshu { get; set; } + public decimal Ydayjingzhong { get; set; } + public decimal Tdayrukujianshu { get; set; } + public decimal Tdayrukujingzhong { get; set; } + public decimal Tdaychukujianshu { get; set; } + public decimal Tdaychukujingzhong { get; set; } + public decimal Yingkui { get; set; } + public decimal Tdayjianshu { get; set; } + public decimal Tdayjingzhong { get; set; } + public decimal Yangjianjianshu { get; set; } + public bool AllChuku { get; set; } + public string Description { get; set; } = ""; +} + +public class WmsTzEditDto +{ + public long TzDateId { get; set; } + public int Module { get; set; } + public int Kind { get; set; } + public DateTime Date { get; set; } + public DateTime LastDate { get; set; } + public List Items { get; set; } = new(); +} + +public class WmsTzSaveDto +{ + public long TzDateId { get; set; } + public int Module { get; set; } + public int Kind { get; set; } + public DateTime Date { get; set; } + public List Items { get; set; } = new(); +} + +public class WmsTzGenerateDto +{ + public int Module { get; set; } + public int Kind { get; set; } + public DateTime Date { get; set; } +} + +// ==================== 库存 / 流水 ==================== + +public class WmsKucunDto +{ + public long WzmlId { get; set; } + public string WzmlName { get; set; } = ""; + public string WzmlCode { get; set; } = ""; + public string Unit { get; set; } = ""; + public string Guige { get; set; } = ""; + public decimal Shuliang { get; set; } + public decimal Danjia { get; set; } + public decimal Jine { get; set; } +} + +public class WmsCrkDto +{ + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public bool IsRuku { get; set; } + public string Kind => IsRuku ? "入库" : "出库"; + public string Code { get; set; } = ""; + public string TypeName { get; set; } = ""; + public long WzmlId { get; set; } + public string WzmlName { get; set; } = ""; + public string WzmlCode { get; set; } = ""; + public string Unit { get; set; } = ""; + public decimal Shuliang { get; set; } + public decimal Danjia { get; set; } + public decimal Jine { get; set; } + public string Description { get; set; } = ""; +} + +// ==================== 生丝批次 / 包件 ==================== + +public class WmsPiciListDto +{ + public long Id { get; set; } + public string Code { get; set; } = ""; + public int CodeType { get; set; } + public int Year { get; set; } + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public DateTime? ScDate { get; set; } + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public long WzmlId { get; set; } + public string WzmlName { get; set; } = ""; + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public string Guige { get; set; } = ""; + public string JidiWaigou { get; set; } = ""; + public string YoujiPutong { get; set; } = ""; + public int Baoshu { get; set; } + public int StartBaojianNum { get; set; } + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public decimal Gongliang { get; set; } + public int KcStatus { get; set; } + public string KcStatusName { get; set; } = ""; + public long RkItemId { get; set; } + public DateTime? RkDate { get; set; } + public DateTime CkLastDate { get; set; } + public int CkBaoshu { get; set; } + public decimal CkJingzhong { get; set; } + public int KcBaoshu { get; set; } + public string Dengji { get; set; } = ""; + public decimal Qingjie { get; set; } + public decimal Jiejing { get; set; } + public decimal Piancha { get; set; } + public decimal Zongcha { get; set; } + public decimal Qieduan { get; set; } + public string Description { get; set; } = ""; + public int BaojianNum { get; set; } +} + +public class WmsPiciSaveDto +{ + public long Id { get; set; } + public string Code { get; set; } = ""; + public int CodeType { get; set; } + public DateTime Date { get; set; } + public DateTime? ScDate { get; set; } + public long GyzkId { get; set; } + public long WzmlId { get; set; } + public long DepId { get; set; } + public string Guige { get; set; } = ""; + public string JidiWaigou { get; set; } = ""; + public string YoujiPutong { get; set; } = ""; + public int Baoshu { get; set; } + public int StartBaojianNum { get; set; } + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public decimal Gongliang { get; set; } + public int KcStatus { get; set; } + public string Dengji { get; set; } = ""; + public decimal Qingjie { get; set; } + public decimal Jiejing { get; set; } + public decimal Piancha { get; set; } + public decimal Zongcha { get; set; } + public decimal Qieduan { get; set; } + public string Description { get; set; } = ""; + public List Baojians { get; set; } = new(); +} + +public class WmsPiciBaojianDto +{ + public long Id { get; set; } + public long PiciId { get; set; } + public int Baojianhao { get; set; } + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public decimal Gongliang { get; set; } + public decimal Huichaolv { get; set; } + public bool CkIsck { get; set; } + public long CkItemId { get; set; } + public DateTime? CkDate { get; set; } + public decimal CkJingzhong { get; set; } + public string Description { get; set; } = ""; +} + +/// 生丝出库选批次(批次 + 未出库包件),对齐 F9 SSPici_GetChukuSelectAsync +public class WmsPiciChukuSelectDto +{ + public long PiciId { get; set; } + public string PiciCode { get; set; } = ""; + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public string Guige { get; set; } = ""; + public string WzmlName { get; set; } = ""; + public string GyzkName { get; set; } = ""; + public string DepName { get; set; } = ""; + public string JidiWaigou { get; set; } = ""; + public int Baoshu { get; set; } // 组批包数 + public int KcBaoshu { get; set; } // 库存(未出库)包数 + public decimal Jingzhong { get; set; } // 组批净重 + public decimal KcJingzhong { get; set; } // 库存净重 + public string Description { get; set; } = ""; + public List Baojians { get; set; } = new(); +} + +/// 生丝批次-质检 +public class WmsPiciQcDto +{ + public long Id { get; set; } + public long PiciId { get; set; } + public string Pingwei { get; set; } = ""; + public string Seze { get; set; } = ""; + public decimal AvgHuichaolv { get; set; } + public decimal AvgGongliangXiandu { get; set; } + public decimal XianduPiancha { get; set; } + public decimal XianduZdpiancha { get; set; } + public decimal Qingjie { get; set; } + public decimal Jiejing { get; set; } + public int JunyunYd { get; set; } + public int JunyunEd { get; set; } + public int JunyunSd { get; set; } + public int Qieduan { get; set; } + public string DuanlieQiangdu { get; set; } = ""; + public string DuanlieShenchanglv { get; set; } = ""; + public int Baohe { get; set; } + public string WgWg { get; set; } = ""; + public string WgYanse { get; set; } = ""; + public string WgGuangze { get; set; } = ""; + public string WgShougan { get; set; } = ""; + public int? QjZhuyao { get; set; } + public int? QjCiyaoFeisi { get; set; } + public int? QjCiyaoDacao { get; set; } + public int? QjCiyaoNianfucao { get; set; } + public int? QjCiyaoDachangjie { get; set; } + public int? QjCiyaoZhongluoxuan { get; set; } + public int? QjPutongXiaocao { get; set; } + public int? QjPutongChangjie { get; set; } + public int? QjPutongLuoxuan { get; set; } + public int? QjPutongHuanjie { get; set; } + public int? Jj100 { get; set; } + public int? Jj95 { get; set; } + public int? Jj90 { get; set; } + public int? Jj85 { get; set; } + public int? Jj80 { get; set; } + public int? Jj75 { get; set; } + public int? Jj70 { get; set; } + public int? Jj65 { get; set; } + public int? Jj60 { get; set; } + public int? Jj55 { get; set; } + public int? Jj50 { get; set; } + public int? Jj45 { get; set; } + public string JjYuanyin { get; set; } = ""; + public string Description { get; set; } = ""; +} + +/// 生丝批次-纤度检验 +public class WmsPiciQcXdjyDto +{ + public long Id { get; set; } + public long PiciId { get; set; } + public decimal Xiandu { get; set; } + public int Num { get; set; } +} + +// ==================== 原料采购 / 翻包计划 ==================== + +public class WmsCaigouDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public string Code { get; set; } = ""; + public long GysId { get; set; } + public string GysName { get; set; } = ""; + public long YlzkId { get; set; } + public string YlzkName { get; set; } = ""; + public int Jianshu { get; set; } + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public decimal Danjia { get; set; } + public decimal Jine { get; set; } + public string Description { get; set; } = ""; +} + +public class WmsFbplanDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public string Code { get; set; } = ""; + public long YlzkId { get; set; } + public string YlzkName { get; set; } = ""; + public long WzmlId { get; set; } + public string WzmlName { get; set; } = ""; + public int Jianshu { get; set; } + public decimal Jingzhong { get; set; } + public int FbType { get; set; } + public string FbTypeName { get; set; } = ""; + public int RukuStatus { get; set; } + public string RukuStatusName { get; set; } = ""; + public string Description { get; set; } = ""; + public decimal YiwanchengJianshu { get; set; } + public decimal YiwanchengJingzhong { get; set; } + public List Records { get; set; } = new(); +} + +public class WmsFbjlDto +{ + public long Id { get; set; } + public long FbplanId { get; set; } + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public int Jianshu { get; set; } + public decimal Jingzhong { get; set; } + public string Description { get; set; } = ""; +} + +// ==================== 物资采购计划 ==================== + +public class WmsCgplanListDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public string Code { get; set; } = ""; + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public int Status { get; set; } + public string StatusName { get; set; } = ""; + public string Description { get; set; } = ""; + public decimal Shuliang { get; set; } + public decimal Jine { get; set; } + public int ItemCount { get; set; } +} + +public class WmsCgplanItemDto +{ + public long Id { get; set; } + public long CgplanId { get; set; } + public long WzmlId { get; set; } + public string WzmlName { get; set; } = ""; + public string WzmlCode { get; set; } = ""; + public string Unit { get; set; } = ""; + public decimal Shuliang { get; set; } + public decimal Danjia { get; set; } + public decimal Jine { get; set; } + public string Description { get; set; } = ""; +} + +public class WmsCgplanSaveDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public string Code { get; set; } = ""; + public long DepId { get; set; } + public int Status { get; set; } + public string Description { get; set; } = ""; + public List Items { get; set; } = new(); +} diff --git a/backend/F9WebAiTest.Application/Services/Attendance/AttendanceService.cs b/backend/F9WebAiTest.Application/Services/Attendance/AttendanceService.cs deleted file mode 100644 index 4aa3bb1..0000000 --- a/backend/F9WebAiTest.Application/Services/Attendance/AttendanceService.cs +++ /dev/null @@ -1,573 +0,0 @@ -using System.Text.Json; -using F9WebAiTest.Application.Common; -using F9WebAiTest.Application.Dtos; -using F9WebAiTest.Application.Dtos.Attendance; -using F9WebAiTest.Application.Services.Notice; -using F9WebAiTest.Domain.Common; -using F9WebAiTest.Domain.Entities; -using F9WebAiTest.Domain.Entities.Attendance; -using F9WebAiTest.Domain.Entities.Notice; -using RBAC = F9WebAiTest.Domain.Entities.SYS.RBAC; -using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments; -using MediatR; - -namespace F9WebAiTest.Application.Services.Attendance; - -public class AttendanceService : IAttendanceService -{ - private readonly IRepository _attRepo; - private readonly IRepository _leaveRepo; - private readonly IRepository _approverRepo; - private readonly IRepository _empRepo; - private readonly IRepository _deptRepo; - private readonly IRepository _roleRepo; - private readonly IRepository _empRoleRepo; - private readonly IRepository _msgRepo; - private readonly IRepository _receiverRepo; - private readonly IRepository _todoRepo; - private readonly IRepository _typeRepo; - private readonly IMediator _mediator; - - // 上下班时间(可配置,这里简化固定) - private static readonly TimeSpan WorkStart = new(9, 0, 0); - private static readonly TimeSpan WorkEnd = new(18, 0, 0); - - public AttendanceService( - IRepository attRepo, - IRepository leaveRepo, - IRepository approverRepo, - IRepository empRepo, - IRepository deptRepo, - IRepository roleRepo, - IRepository empRoleRepo, - IRepository msgRepo, - IRepository receiverRepo, - IRepository todoRepo, - IRepository typeRepo, - IMediator mediator) - { - _attRepo = attRepo; - _leaveRepo = leaveRepo; - _approverRepo = approverRepo; - _empRepo = empRepo; - _deptRepo = deptRepo; - _roleRepo = roleRepo; - _empRoleRepo = empRoleRepo; - _msgRepo = msgRepo; - _receiverRepo = receiverRepo; - _todoRepo = todoRepo; - _typeRepo = typeRepo; - _mediator = mediator; - } - - public async Task PunchAsync(long employeeId) - { - var now = DateTime.Now; - var today = now.Date; - - var record = await _attRepo.FirstOrDefaultAsync(a => a.EmployeeId == employeeId && a.WorkDate == today); - - // 签到 - if (record == null) - { - var status = now.TimeOfDay <= WorkStart.Add(TimeSpan.FromMinutes(10)) ? "正常" : "迟到"; - var newRecord = new AttendanceRecord - { - EmployeeId = employeeId, - WorkDate = today, - SignInTime = now, - SignInStatus = status, - SignOutStatus = "未签退" - }; - await _attRepo.AddAsync(newRecord); - await _attRepo.SaveChangesAsync(); - - return new PunchResultDto - { - Type = "signIn", - Time = now, - Status = status, - Message = status == "正常" ? "签到成功,辛苦了!" : "签到成功(已迟到)" - }; - } - - // 已签到,进行签退 - if (record.SignOutTime == null) - { - var status = now.TimeOfDay >= WorkEnd ? "正常" : "早退"; - record.SignOutTime = now; - record.SignOutStatus = status; - await _attRepo.UpdateAsync(record); - await _attRepo.SaveChangesAsync(); - - return new PunchResultDto - { - Type = "signOut", - Time = now, - Status = status, - Message = status == "正常" ? "签退成功,明天见!" : "签退成功(早退)" - }; - } - - // 已签退 - return new PunchResultDto - { - Type = "done", - Time = now, - Status = "已完成", - Message = "今日已签到和签退,无需重复打卡" - }; - } - - public async Task> GetMyRecordsAsync(long employeeId, int pageIndex, int pageSize) - { - var records = (await _attRepo.FindAsync(a => a.EmployeeId == employeeId)) - .OrderByDescending(a => a.WorkDate) - .ToList(); - var total = records.Count; - var items = records.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); - - var emp = await _empRepo.GetByIdAsync(employeeId); - var dept = emp != null ? await _deptRepo.GetByIdAsync(emp.DepartmentId) : null; - - return new PageResult - { - Items = items.Select(a => new AttendanceDto - { - Id = a.Id, - EmployeeId = a.EmployeeId, - EmployeeName = emp?.UserName ?? "", - DepartmentName = dept?.Name ?? "", - WorkDate = a.WorkDate, - SignInTime = a.SignInTime, - SignOutTime = a.SignOutTime, - SignInStatus = a.SignInStatus, - SignOutStatus = a.SignOutStatus, - Remark = a.Remark - }).ToList(), - Total = total, - PageIndex = pageIndex, - PageSize = pageSize - }; - } - - public async Task GetMySummaryAsync(long employeeId, string month) - { - // month 格式 yyyy-MM,默认当前月 - if (string.IsNullOrWhiteSpace(month)) - { - month = DateTime.Now.ToString("yyyy-MM"); - } - var start = DateTime.Parse(month + "-01"); - var end = start.AddMonths(1); - - var records = await _attRepo.FindAsync(a => - a.EmployeeId == employeeId && a.WorkDate >= start && a.WorkDate < end); - - var summary = new AttendanceSummaryDto(); - foreach (var r in records) - { - summary.TotalDays++; - if (r.SignInStatus == "迟到") summary.LateDays++; - if (r.SignOutStatus == "早退") summary.EarlyLeaveDays++; - if (r.SignInTime == null) summary.AbsentDays++; - } - summary.NormalDays = summary.TotalDays - summary.LateDays - summary.EarlyLeaveDays - summary.AbsentDays; - - // 请假天数 - var leaves = await _leaveRepo.FindAsync(l => - l.EmployeeId == employeeId && l.Status == "已批准" && l.StartTime < end && l.EndTime >= start); - summary.LeaveDays = leaves.Sum(l => (l.EndTime - l.StartTime).Days + 1); - - return summary; - } - - public async Task> GetAllRecordsAsync(AttendanceQueryDto query) - { - var records = await _attRepo.GetAllAsync(); - var filtered = records.AsQueryable(); - - if (!string.IsNullOrWhiteSpace(query.Date)) - { - var date = DateTime.Parse(query.Date).Date; - filtered = filtered.Where(a => a.WorkDate == date); - } - - var emps = await _empRepo.GetAllAsync(); - var depts = await _deptRepo.GetAllAsync(); - var empDict = emps.ToDictionary(e => e.Id, e => e); - var deptDict = depts.ToDictionary(d => d.Id, d => d.Name); - - if (query.DepartmentId.HasValue) - { - var deptEmpIds = emps.Where(e => e.DepartmentId == query.DepartmentId.Value).Select(e => e.Id).ToHashSet(); - filtered = filtered.Where(a => deptEmpIds.Contains(a.EmployeeId)); - } - if (!string.IsNullOrWhiteSpace(query.Keyword)) - { - var kwEmpIds = emps.Where(e => e.UserName.Contains(query.Keyword) || e.LoginName.Contains(query.Keyword)) - .Select(e => e.Id).ToHashSet(); - filtered = filtered.Where(a => kwEmpIds.Contains(a.EmployeeId)); - } - - var total = filtered.LongCount(); - var items = filtered.OrderByDescending(a => a.WorkDate).ThenByDescending(a => a.SignInTime) - .Skip((query.PageIndex - 1) * query.PageSize).Take(query.PageSize).ToList(); - - return new PageResult - { - Items = items.Select(a => - { - var emp = empDict.GetValueOrDefault(a.EmployeeId); - return new AttendanceDto - { - Id = a.Id, - EmployeeId = a.EmployeeId, - EmployeeName = emp?.UserName ?? "", - DepartmentName = emp != null ? deptDict.GetValueOrDefault(emp.DepartmentId, "") : "", - WorkDate = a.WorkDate, - SignInTime = a.SignInTime, - SignOutTime = a.SignOutTime, - SignInStatus = a.SignInStatus, - SignOutStatus = a.SignOutStatus, - Remark = a.Remark - }; - }).ToList(), - Total = total, - PageIndex = query.PageIndex, - PageSize = query.PageSize - }; - } - - public async Task ApplyLeaveAsync(long employeeId, CreateLeaveDto dto) - { - if (dto.EndTime < dto.StartTime) - throw new ServiceException("结束时间不能早于开始时间"); - - // 确定审批人:优先使用前端选择的审批人,为空则回退到管理员 - var approverIds = (dto.ApproverIds ?? new List()).Where(id => id > 0).Distinct().ToList(); - if (approverIds.Count == 0) - approverIds = await GetApproverIdsAsync(); - if (approverIds.Count == 0) - throw new ServiceException("没有可用的审批人,请联系管理员"); - - var firstApprover = await _empRepo.GetByIdAsync(approverIds[0]); - var leave = new LeaveRequest - { - EmployeeId = employeeId, - Type = dto.Type, - StartTime = dto.StartTime, - EndTime = dto.EndTime, - Reason = dto.Reason ?? "", - CurrentOrderNo = 1, - ApproverId = approverIds[0], - ApproverName = firstApprover?.UserName ?? "" - }; - await _leaveRepo.AddAsync(leave); - await _leaveRepo.SaveChangesAsync(); - - // 创建审批节点(序号 = 审批顺序) - for (var i = 0; i < approverIds.Count; i++) - { - var emp = await _empRepo.GetByIdAsync(approverIds[i]); - await _approverRepo.AddAsync(new LeaveApprover - { - LeaveId = leave.Id, - ApproverId = approverIds[i], - ApproverName = emp?.UserName ?? "", - OrderNo = i + 1 - }); - } - await _approverRepo.SaveChangesAsync(); - - // 只给第一个审批人发待办(顺序审批,后续审批人待前一节点通过后才收到) - await SendLeaveApprovalAsync(leave, approverIds[0], 1, approverIds.Count); - - // 业务单据已创建,推送事件让各端刷新(多端同步) - await _mediator.Publish(new BusinessChangedEvent("LeaveApproval", leave.Id.ToString(), "Created")); - - return await ToLeaveDto(leave); - } - - public async Task> GetMyLeavesAsync(long employeeId) - { - var leaves = await _leaveRepo.FindAsync(l => l.EmployeeId == employeeId); - var result = new List(); - foreach (var l in leaves.OrderByDescending(l => l.CreatedAt)) - { - result.Add(await ToLeaveDto(l)); - } - return result; - } - - public async Task> GetAllLeavesAsync(string status) - { - var leaves = await _leaveRepo.GetAllAsync(); - if (!string.IsNullOrWhiteSpace(status) && status != "全部") - { - leaves = leaves.Where(l => l.Status == status).ToList(); - } - var result = new List(); - foreach (var l in leaves.OrderByDescending(l => l.CreatedAt)) - { - result.Add(await ToLeaveDto(l)); - } - return result; - } - - public async Task ApproveLeaveAsync(long leaveId, long approverId, string approverName, ApproveLeaveDto dto) - { - var leave = await _leaveRepo.GetByIdAsync(leaveId); - if (leave == null) throw new ServiceException("请假申请不存在", 404); - if (leave.Status != "待审批") throw new ServiceException("该申请已处理"); - - var approvers = (await _approverRepo.FindAsync(a => a.LeaveId == leaveId)) - .OrderBy(a => a.OrderNo).ToList(); - - // 校验当前审批节点 - var current = approvers.FirstOrDefault(a => a.OrderNo == leave.CurrentOrderNo); - if (current == null) throw new ServiceException("审批节点不存在", 404); - if (current.ApproverId != approverId) - throw new ServiceException($"当前应由「{current.ApproverName}」审批,您无权操作", 403); - if (current.Status != "待审批") throw new ServiceException("该节点已处理"); - - // 更新当前节点 - var handleResult = dto.Approved ? "已批准" : "已驳回"; - current.Status = handleResult; - current.ApproveRemark = dto.Remark ?? ""; - current.ApprovedAt = DateTime.Now; - await _approverRepo.UpdateAsync(current); - - if (!dto.Approved) - { - // 驳回:整体驳回,结束流程 - leave.Status = "已驳回"; - leave.ApproverId = approverId; - leave.ApproverName = approverName; - leave.ApproveRemark = dto.Remark ?? ""; - leave.ApprovedAt = DateTime.Now; - await _leaveRepo.UpdateAsync(leave); - await _leaveRepo.SaveChangesAsync(); - await _approverRepo.SaveChangesAsync(); - - // 流程终结:完成该单所有待办 + 标记消息已读,避免残留未读红点 - await FinalizeLeaveApprovalAsync(leaveId, handleResult); - await NotifyLeaveResultAsync(leave, false, dto.Remark); - await _mediator.Publish(new BusinessChangedEvent("LeaveApproval", leaveId.ToString(), "StatusChanged")); - return await ToLeaveDto(leave); - } - - // 批准:判断是否还有下一级审批人 - var next = approvers.FirstOrDefault(a => a.OrderNo == leave.CurrentOrderNo + 1); - if (next != null) - { - // 流转到下一级审批人 - leave.CurrentOrderNo = next.OrderNo; - leave.ApproverId = next.ApproverId; - leave.ApproverName = next.ApproverName; - await _leaveRepo.UpdateAsync(leave); - await _leaveRepo.SaveChangesAsync(); - await _approverRepo.SaveChangesAsync(); - - // 完成当前审批人待办并标记其消息已读,再把同一条消息流转给下一审批人 - await CompleteCurrentApproverAsync(leaveId, approverId, handleResult); - await SendLeaveApprovalAsync(leave, next.ApproverId, next.OrderNo, approvers.Count); - } - else - { - // 最后一级审批人批准:整体批准 - leave.Status = "已批准"; - leave.ApproverId = approverId; - leave.ApproverName = approverName; - leave.ApproveRemark = dto.Remark ?? ""; - leave.ApprovedAt = DateTime.Now; - await _leaveRepo.UpdateAsync(leave); - await _leaveRepo.SaveChangesAsync(); - await _approverRepo.SaveChangesAsync(); - - // 流程终结 - await FinalizeLeaveApprovalAsync(leaveId, handleResult); - await NotifyLeaveResultAsync(leave, true, dto.Remark); - } - - // 业务状态已变更,推送事件让各端刷新(多端同步) - await _mediator.Publish(new BusinessChangedEvent("LeaveApproval", leaveId.ToString(), "StatusChanged")); - return await ToLeaveDto(leave); - } - - /// 查询审批人(admin 角色用户)ID 列表(未指定审批人时的兜底) - private async Task> GetApproverIdsAsync() - { - var adminRole = await _roleRepo.FirstOrDefaultAsync(r => r.Code == "admin"); - if (adminRole == null) return new List(); - var relations = await _empRoleRepo.FindAsync(ur => ur.RoleId == adminRole.Id); - return relations.Select(ur => ur.UserId).Distinct().ToList(); - } - - /// - /// 请假审批待办消息:一张请假单仅保留一条,随审批流转复用。 - /// 首次创建,流转时更新内容与当前审批人并重置为待投递(投递服务幂等,只推新审批人)。 - /// - private async Task SendLeaveApprovalAsync(LeaveRequest leave, long targetApproverId, int currentLevel, int totalLevel) - { - var businessType = NotificationTypeEnum.LeaveApproval.ToString(); - var businessId = leave.Id.ToString(); - var content = $"类型:{leave.Type},时间:{leave.StartTime:MM-dd HH:mm} ~ {leave.EndTime:MM-dd HH:mm},事由:{leave.Reason}(第 {currentLevel}/{totalLevel} 级审批)"; - - var msg = await _msgRepo.FirstOrDefaultAsync(m => - m.BusinessType == businessType && m.BusinessId == businessId); - - if (msg == null) - { - var type = await _typeRepo.FirstOrDefaultAsync(t => t.Code == businessType); - var applicant = await _empRepo.GetByIdAsync(leave.EmployeeId); - msg = new NotificationMessage - { - NotificationTypeId = type?.Id ?? 0, - MessageTypeCode = businessType, - BusinessType = businessType, - BusinessId = businessId, - Title = $"{applicant?.UserName ?? "员工"} 提交了请假申请", - Content = content, - Priority = NotificationPriority.Important, - Status = NotificationStatus.Ready, - ForceReceivers = JsonSerializer.Serialize(new List { targetApproverId }), - CreatorId = leave.EmployeeId, - CreatorName = applicant?.UserName ?? "", - Category = NotificationCategory.Todo, - RoutePath = !string.IsNullOrWhiteSpace(type?.RouteTemplate) - ? type.RouteTemplate.Replace("{businessId}", businessId) - : $"/attendance/approve?leaveId={businessId}" - }; - await _msgRepo.AddAsync(msg); - await _msgRepo.SaveChangesAsync(); - } - else - { - // 复用:更新内容与当前审批人,重置为待投递,投递服务幂等只会推送给新审批人 - msg.Content = content; - msg.ForceReceivers = JsonSerializer.Serialize(new List { targetApproverId }); - msg.Status = NotificationStatus.Ready; - await _msgRepo.UpdateAsync(msg); - await _msgRepo.SaveChangesAsync(); - } - - // 当前审批人的待办(幂等:已存在则跳过) - var existTodo = await _todoRepo.FirstOrDefaultAsync(t => t.MessageId == msg.Id && t.AssigneeId == targetApproverId); - if (existTodo == null) - { - await _todoRepo.AddAsync(new TodoItem - { - MessageId = msg.Id, - AssigneeId = targetApproverId, - TodoStatus = TodoItemStatus.Pending - }); - await _todoRepo.SaveChangesAsync(); - } - } - - /// 完成当前审批人的待办,并标记其消息已读(流转时调用,避免已处理人残留未读红点) - private async Task CompleteCurrentApproverAsync(long leaveId, long approverId, string handleResult) - { - var msgs = await _msgRepo.FindAsync(m => - m.BusinessType == NotificationTypeEnum.LeaveApproval.ToString() && m.BusinessId == leaveId.ToString()); - var msgIds = msgs.Select(m => m.Id).ToList(); - if (msgIds.Count == 0) return; - - var todos = await _todoRepo.FindAsync(t => - msgIds.Contains(t.MessageId) && t.AssigneeId == approverId && t.TodoStatus == TodoItemStatus.Pending); - foreach (var t in todos) - { - t.TodoStatus = TodoItemStatus.Completed; - t.CompletedTime = DateTime.Now; - t.HandleResult = handleResult; - await _todoRepo.UpdateAsync(t); - } - - var receivers = await _receiverRepo.FindAsync(r => - msgIds.Contains(r.MessageId) && r.UserId == approverId && r.ReadStatus == NotificationReadStatus.Unread); - foreach (var r in receivers) - { - r.ReadStatus = NotificationReadStatus.Read; - r.ReadTime = DateTime.Now; - await _receiverRepo.UpdateAsync(r); - } - } - - /// 流程终结:完成该单所有待处理待办,并标记所有未读消息为已读 - private async Task FinalizeLeaveApprovalAsync(long leaveId, string handleResult) - { - var msgs = await _msgRepo.FindAsync(m => - m.BusinessType == NotificationTypeEnum.LeaveApproval.ToString() && m.BusinessId == leaveId.ToString()); - var msgIds = msgs.Select(m => m.Id).ToList(); - if (msgIds.Count == 0) return; - - var todos = await _todoRepo.FindAsync(t => msgIds.Contains(t.MessageId) && t.TodoStatus == TodoItemStatus.Pending); - foreach (var t in todos) - { - t.TodoStatus = TodoItemStatus.Completed; - t.CompletedTime = DateTime.Now; - t.HandleResult = handleResult; - await _todoRepo.UpdateAsync(t); - } - - var receivers = await _receiverRepo.FindAsync(r => msgIds.Contains(r.MessageId) && r.ReadStatus == NotificationReadStatus.Unread); - foreach (var r in receivers) - { - r.ReadStatus = NotificationReadStatus.Read; - r.ReadTime = DateTime.Now; - await _receiverRepo.UpdateAsync(r); - } - } - - /// 通知申请人审批结果 - private async Task NotifyLeaveResultAsync(LeaveRequest leave, bool approved, string remark) - { - await _mediator.Send(new SendNotificationCommand( - NotificationTypeEnum.LeaveResult, - BusinessId: leave.Id.ToString(), - Title: approved ? "请假申请已批准" : "请假申请已驳回", - Content: $"您的请假({leave.Type},{leave.StartTime:MM-dd HH:mm} ~ {leave.EndTime:MM-dd HH:mm})已{(approved ? "批准" : "驳回")}。审批意见:{remark}", - ForceReceivers: new List { leave.EmployeeId } - )); - } - - private async Task ToLeaveDto(LeaveRequest l) - { - var emp = await _empRepo.GetByIdAsync(l.EmployeeId); - var dept = emp != null ? await _deptRepo.GetByIdAsync(emp.DepartmentId) : null; - - var approvers = (await _approverRepo.FindAsync(a => a.LeaveId == l.Id)) - .OrderBy(a => a.OrderNo) - .Select(a => new LeaveApproverDto - { - Id = a.Id, - ApproverId = a.ApproverId, - ApproverName = a.ApproverName, - OrderNo = a.OrderNo, - Status = a.Status, - ApproveRemark = a.ApproveRemark, - ApprovedAt = a.ApprovedAt - }) - .ToList(); - - return new LeaveRequestDto - { - Id = l.Id, - EmployeeId = l.EmployeeId, - EmployeeName = emp?.UserName ?? "", - DepartmentName = dept?.Name ?? "", - Type = l.Type, - StartTime = l.StartTime, - EndTime = l.EndTime, - Reason = l.Reason, - Status = l.Status, - CurrentOrderNo = l.CurrentOrderNo, - ApproverName = l.ApproverName, - ApproveRemark = l.ApproveRemark, - ApprovedAt = l.ApprovedAt, - CreatedAt = l.CreatedAt, - Approvers = approvers - }; - } -} diff --git a/backend/F9WebAiTest.Application/Services/Attendance/IAttendanceService.cs b/backend/F9WebAiTest.Application/Services/Attendance/IAttendanceService.cs deleted file mode 100644 index 2814976..0000000 --- a/backend/F9WebAiTest.Application/Services/Attendance/IAttendanceService.cs +++ /dev/null @@ -1,26 +0,0 @@ -using F9WebAiTest.Application.Common; -using F9WebAiTest.Application.Dtos; -using F9WebAiTest.Application.Dtos.Attendance; - -namespace F9WebAiTest.Application.Services.Attendance; - -public interface IAttendanceService -{ - // 打卡 - Task PunchAsync(long employeeId); - - // 我的考勤记录(分页) - Task> GetMyRecordsAsync(long employeeId, int pageIndex, int pageSize); - - // 我的考勤统计 - Task GetMySummaryAsync(long employeeId, string month); - - // 全部考勤记录(管理员) - Task> GetAllRecordsAsync(AttendanceQueryDto query); - - // 请假 - Task ApplyLeaveAsync(long employeeId, CreateLeaveDto dto); - Task> GetMyLeavesAsync(long employeeId); - Task> GetAllLeavesAsync(string status); - Task ApproveLeaveAsync(long leaveId, long approverId, string approverName, ApproveLeaveDto dto); -} diff --git a/backend/F9WebAiTest.Application/Services/PayRoll/CalcLdcctService.cs b/backend/F9WebAiTest.Application/Services/PayRoll/CalcLdcctService.cs new file mode 100644 index 0000000..59e7e6f --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/PayRoll/CalcLdcctService.cs @@ -0,0 +1,819 @@ +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; + +/// +/// 薪资计算-立缫挡车车头(挡车工 / 车头工 产量计算) +/// 对应 F9 WinClient\F9.Business.PayRoll\PAYROLL\CALCLDCCT(Page_CalcLdcct + Page_CalcLdcct_Edit) +/// 数据源:看台记录(挡车/车头)、煮茧日报(今缫桶数)、落丝记录(茧量)、秤大丝(丝量/缫折)、 +/// 纤度检验(纤度明细/均方差/总差)、黑板检验(二度/三度/清洁/洁净)、生丝规格、生丝定级指标。 +/// 配置:工艺庄口薪资配置、工种薪资配置、挡车岗位薪资配置、挡车纤度奖扣、薪资价格配置。 +/// +public interface ICalcLdcctService +{ + Task> GetPagedAsync(int pageIndex, int pageSize, DateTime start, DateTime end, + long? depId, long? worktypeId, string like); + Task CalcDcgAsync(CalcLdcctCalcDto dto); + Task CalcCtgAsync(CalcLdcctCalcDto dto); + Task SaveAsync(CalcLdcctSaveDto dto); + Task DeleteAsync(DateTime start, DateTime end, long depId, long worktypeId); +} + +public class CalcLdcctService : ICalcLdcctService +{ + private readonly IRepository _repo; + private readonly IRepository _dcgRepo; + private readonly IRepository _ctgRepo; + private readonly IRepository _zjrbRepo; + private readonly IRepository _zjrbJizuRepo; + private readonly IRepository _lsjlRepo; + private readonly IRepository _cdsLsjlRepo; + private readonly IRepository _cdsChetaiRepo; + private readonly IRepository _xdjyLsjlRepo; + private readonly IRepository _xdjyRecordRepo; + private readonly IRepository _hbjyRepo; + private readonly IRepository _confGyzkRepo; + private readonly IRepository _confGzRepo; + private readonly IRepository _confDcgRepo; + private readonly IRepository _xdjkRepo; + private readonly IRepository _xzjgRepo; + private readonly IRepository _guigeRepo; + private readonly IRepository _ssdjzbRepo; + private readonly IRepository _zkRepo; + private readonly IRepository _depRepo; + private readonly IRepository _empRepo; + private readonly IRepository _wtRepo; + + public CalcLdcctService( + IRepository repo, + IRepository dcgRepo, + IRepository ctgRepo, + IRepository zjrbRepo, + IRepository zjrbJizuRepo, + IRepository lsjlRepo, + IRepository cdsLsjlRepo, + IRepository cdsChetaiRepo, + IRepository xdjyLsjlRepo, + IRepository xdjyRecordRepo, + IRepository hbjyRepo, + IRepository confGyzkRepo, + IRepository confGzRepo, + IRepository confDcgRepo, + IRepository xdjkRepo, + IRepository xzjgRepo, + IRepository guigeRepo, + IRepository ssdjzbRepo, + IRepository zkRepo, + IRepository depRepo, + IRepository empRepo, + IRepository 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> 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 + { + 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 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(); + + // 按「日 + 机组」组织 + 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 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(); + + 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(); + var saozheList = new List(); + 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 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 DcgKtjl = new(); + public List 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> LsjlIdsByJizuDayGyzk = new(); + public Dictionary CdsLsjl = new(); + public Dictionary> CdsChetai = new(); + public Dictionary> XdjyByLsjl = new(); + public Dictionary> HbjyByLsjl = new(); + public Dictionary> ConfByGyzk = new(); + public Dictionary GuigeDict = new(); + public Dictionary> YeXianJk = new(); + public List Djzb = new(); + public ConfGz ConfGz = null!; + public ConfDcg ConfDcg = null!; + public decimal DcgJjDengJiJkPrice; + public decimal CtgJjDengJiJkPrice; + public List 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 YeXianJiangKou(string guige) => + YeXianJk.TryGetValue(guige ?? "", out var l) ? l : new List(); + } + + private async Task 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(); + 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 PriceAsync(string key) + { + var e = await _xzjgRepo.FirstOrDefaultAsync(x => x.Group == PriceGroup && x.Key == key); + return e?.Price ?? 0m; + } + + private async Task PersistAsync(List 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(); + + 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(); + var cdsSum = ctx.CdsLsjl.TryGetValue(lid, out var cs) ? cs : null; + var xdjy = ctx.XdjyByLsjl.TryGetValue(lid, out var xd) ? xd : new List(); + var hbjy = ctx.HbjyByLsjl.TryGetValue(lid, out var hb) ? hb : new List(); + + 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; + } + + /// 车台看台比例 = 本人该台工时 / 该台所有看台人工时之和 + 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; + } + + /// 野纤扣款:按规格取挡车纤度奖扣表,≤2 条取扣款最大的 2 条求和 + 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); + } + + /// 按数量展开:整数部分逐条取,小数部分按最后一条比例折算 + private static List ExpandCount(decimal count, List list) + { + var result = new List(); + 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; + } + + // ==================== 内部:等级 ==================== + + /// 挡车工等级:木桶短板法,六项各自定级后取最低分 + 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(); + 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 target, IEnumerable src) + { + var best = src.OrderByDescending(z => z.GradeScore).FirstOrDefault(); + if (best != null) target.Add(best); + } + + /// 车头工等级:按挡车工丝量加权平均等级分后回落取整档 + private (string Dengji, decimal Dengjifen) CalCtgDengji(CalcContext ctx, List 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); + } + + // ==================== 内部:单价 / 金额 ==================== + + /// 按绪数档取岗位基数金额:<60→50档,<70→60档,<100→70档,否则 100 档 + 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; + } + + /// 缫折考核:±2kg 死区不奖不扣,超出部分按封顶 clamp,每 1kg 每 100kg 丝 奖扣 price 元 + 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() }; +} diff --git a/backend/F9WebAiTest.Application/Services/PayRoll/ConfEmployeeService.cs b/backend/F9WebAiTest.Application/Services/PayRoll/ConfEmployeeService.cs new file mode 100644 index 0000000..222afa6 --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/PayRoll/ConfEmployeeService.cs @@ -0,0 +1,225 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.PayRoll; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities.PayRoll; +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; + +/// 薪资配置-账期(CONFZQ):考勤统计的「前缫出勤天数」来源 +public interface IConfZqService +{ + Task> GetListAsync(int? year); + Task GetByYearMonthAsync(int year, int month); + Task SaveAsync(ConfZqDto dto); + Task DeleteAsync(long id); +} + +/// 薪资配置-职工(CONFEMPLOYEE):法定假小时数 / 是否学工 +public interface IConfEmployeeService +{ + Task> GetListAsync(long? employeeId, long? worktypeId); + Task SaveAsync(ConfEmployeeDto dto); + Task DeleteAsync(long id); +} + +/// 岗位胜任考核:考核结果决定加班费单价档位 +public interface IGwsrkhService +{ + Task> GetListAsync(int year, int month, long depId); + Task SaveAsync(GwsrkhDto dto); + Task DeleteAsync(long id); +} + +public class ConfZqService : IConfZqService +{ + private readonly IRepository _repo; + public ConfZqService(IRepository repo) => _repo = repo; + + public async Task> GetListAsync(int? year) + { + var all = await _repo.GetAllAsync(); + var q = all.AsQueryable(); + if (year.HasValue) q = q.Where(x => x.Year == year.Value); + return q.ToList().OrderByDescending(x => x.Year).ThenByDescending(x => x.Month) + .Select(x => new ConfZqDto + { + Id = x.Id, Year = x.Year, Month = x.Month, + QsQuanqinDays = x.QsQuanqinDays, Memo = x.Memo ?? "" + }).ToList(); + } + + public async Task GetByYearMonthAsync(int year, int month) + { + var hit = await _repo.FirstOrDefaultAsync(x => x.Year == year && x.Month == month); + return hit == null + ? new ConfZqDto { Year = year, Month = month } + : new ConfZqDto { Id = hit.Id, Year = hit.Year, Month = hit.Month, QsQuanqinDays = hit.QsQuanqinDays, Memo = hit.Memo ?? "" }; + } + + public async Task SaveAsync(ConfZqDto dto) + { + if (dto.Year <= 0 || dto.Month <= 0) throw new ServiceException("请填写年度与月份"); + var exist = await _repo.FirstOrDefaultAsync(x => x.Year == dto.Year && x.Month == dto.Month && x.Id != dto.Id); + if (exist != null) throw new ServiceException($"{dto.Year} 年 {dto.Month} 月的账期薪资配置已存在"); + + ConfZq entity; + if (dto.Id > 0) + { + entity = await _repo.GetByIdAsync(dto.Id); + if (entity == null) throw new ServiceException("配置不存在", 404); + } + else entity = new ConfZq(); + + entity.Year = dto.Year; + entity.Month = dto.Month; + entity.QsQuanqinDays = dto.QsQuanqinDays; + entity.Memo = dto.Memo ?? ""; + + if (dto.Id > 0) await _repo.UpdateAsync(entity); else await _repo.AddAsync(entity); + return entity.Id; + } + + public async Task DeleteAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("配置不存在", 404); + await _repo.DeleteAsync(e); + } +} + +public class ConfEmployeeService : IConfEmployeeService +{ + private readonly IRepository _repo; + private readonly IRepository _empRepo; + private readonly IRepository _wtRepo; + + public ConfEmployeeService(IRepository repo, IRepository empRepo, + IRepository wtRepo) + { + _repo = repo; _empRepo = empRepo; _wtRepo = wtRepo; + } + + public async Task> GetListAsync(long? employeeId, long? worktypeId) + { + var all = await _repo.GetAllAsync(); + var q = all.AsQueryable(); + if (employeeId.HasValue && employeeId.Value > 0) q = q.Where(x => x.EmployeeId == employeeId.Value); + if (worktypeId.HasValue && worktypeId.Value > 0) q = q.Where(x => x.WorktypeId == worktypeId.Value); + var emps = (await _empRepo.GetAllAsync()).ToDictionary(e => e.Id); + var wts = (await _wtRepo.GetAllAsync()).ToDictionary(w => w.Id, w => w.WorkName); + return q.ToList().OrderBy(x => x.EmployeeId).Select(x => new ConfEmployeeDto + { + Id = x.Id, + EmployeeId = x.EmployeeId, + EmployeeCode = emps.TryGetValue(x.EmployeeId, out var e) ? (e.Code ?? "") : "", + EmployeeName = emps.TryGetValue(x.EmployeeId, out var e2) ? (e2.Name ?? "") : "", + WorktypeId = x.WorktypeId, + WorktypeName = wts.TryGetValue(x.WorktypeId, out var wn) ? wn : "", + HoursFadingBz = x.HoursFadingBz, + HoursFadingGz = x.HoursFadingGz, + IsXuegong = x.IsXuegong, + Memo = x.Memo ?? "" + }).ToList(); + } + + public async Task SaveAsync(ConfEmployeeDto dto) + { + if (dto.EmployeeId <= 0) throw new ServiceException("请选择职工"); + var exist = await _repo.FirstOrDefaultAsync(x => x.EmployeeId == dto.EmployeeId && x.WorktypeId == dto.WorktypeId && x.Id != dto.Id); + if (exist != null) throw new ServiceException("该职工该工种的薪资配置已存在"); + + ConfEmployee entity; + if (dto.Id > 0) + { + entity = await _repo.GetByIdAsync(dto.Id); + if (entity == null) throw new ServiceException("配置不存在", 404); + } + else entity = new ConfEmployee(); + + entity.EmployeeId = dto.EmployeeId; + entity.WorktypeId = dto.WorktypeId; + entity.HoursFadingBz = dto.HoursFadingBz; + entity.HoursFadingGz = dto.HoursFadingGz; + entity.IsXuegong = dto.IsXuegong; + entity.Memo = dto.Memo ?? ""; + + if (dto.Id > 0) await _repo.UpdateAsync(entity); else await _repo.AddAsync(entity); + return entity.Id; + } + + public async Task DeleteAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("配置不存在", 404); + await _repo.DeleteAsync(e); + } +} + +public class GwsrkhService : IGwsrkhService +{ + private readonly IRepository _repo; + private readonly IRepository _depRepo; + private readonly IRepository _empRepo; + + public GwsrkhService(IRepository repo, IRepository depRepo, + IRepository empRepo) + { + _repo = repo; _depRepo = depRepo; _empRepo = empRepo; + } + + public async Task> GetListAsync(int year, int month, long depId) + { + var all = await _repo.GetAllAsync(); + var q = all.AsQueryable().Where(x => x.Year == year && x.Month == month); + if (depId > 0) q = q.Where(x => x.DepId == depId); + var depts = (await _depRepo.GetAllAsync()).ToDictionary(d => d.Id, d => d.Name); + var emps = (await _empRepo.GetAllAsync()).ToDictionary(e => e.Id); + return q.ToList().OrderBy(x => x.EmployeeId).Select(x => new GwsrkhDto + { + Id = x.Id, Year = x.Year, Month = x.Month, + DepId = x.DepId, DepName = depts.TryGetValue(x.DepId, out var dn) ? dn : "", + EmployeeId = x.EmployeeId, + EmployeeCode = emps.TryGetValue(x.EmployeeId, out var e) ? (e.Code ?? "") : "", + EmployeeName = emps.TryGetValue(x.EmployeeId, out var e2) ? (e2.Name ?? "") : "", + KhJieguo = x.KhJieguo ?? "", + Memo = x.Memo ?? "" + }).ToList(); + } + + public async Task SaveAsync(GwsrkhDto dto) + { + if (dto.EmployeeId <= 0) throw new ServiceException("请选择职工"); + if (string.IsNullOrWhiteSpace(dto.KhJieguo)) throw new ServiceException("请选择考核结果"); + var exist = await _repo.FirstOrDefaultAsync(x => x.Year == dto.Year && x.Month == dto.Month + && x.EmployeeId == dto.EmployeeId && x.Id != dto.Id); + if (exist != null) throw new ServiceException("该职工本月考核结果已存在"); + + Gwsrkh entity; + if (dto.Id > 0) + { + entity = await _repo.GetByIdAsync(dto.Id); + if (entity == null) throw new ServiceException("记录不存在", 404); + } + else entity = new Gwsrkh(); + + entity.Year = dto.Year; + entity.Month = dto.Month; + entity.DepId = dto.DepId; + entity.EmployeeId = dto.EmployeeId; + entity.KhJieguo = dto.KhJieguo.Trim(); + entity.Memo = dto.Memo ?? ""; + + if (dto.Id > 0) await _repo.UpdateAsync(entity); else await _repo.AddAsync(entity); + return entity.Id; + } + + public async Task DeleteAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("记录不存在", 404); + await _repo.DeleteAsync(e); + } +} diff --git a/backend/F9WebAiTest.Application/Services/PayRoll/PayRollConfService.cs b/backend/F9WebAiTest.Application/Services/PayRoll/PayRollConfService.cs new file mode 100644 index 0000000..28d263f --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/PayRoll/PayRollConfService.cs @@ -0,0 +1,493 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.PayRoll; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities.Base; +using F9WebAiTest.Domain.Entities.PayRoll; +using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments; +using SysWt = F9WebAiTest.Domain.Entities.SYS.WorkTypes; + +namespace F9WebAiTest.Application.Services.PayRoll; + +// 薪资计算依赖的 5 套配置(对应 F9 PAYROLL 下的 CONFGYZK / CONFGZ / CONFDCG / DCXDJK / XinZiJiaGePeiZhi) + +// ==================== 薪资配置-工艺庄口 ==================== + +public interface IConfGyzkService +{ + Task> GetListAsync(int? year, int? month); + Task GetForEditAsync(long id); + Task SaveAsync(ConfGyzkSaveDto dto); + Task DeleteAsync(long id); +} + +public class ConfGyzkService : IConfGyzkService +{ + private readonly IRepository _repo; + private readonly IRepository _zkRepo; + + public ConfGyzkService(IRepository repo, IRepository zkRepo) + { + _repo = repo; + _zkRepo = zkRepo; + } + + public async Task> GetListAsync(int? year, int? month) + { + var all = await _repo.GetAllAsync(); + var q = all.AsQueryable(); + if (year.HasValue) q = q.Where(x => x.Year == year.Value); + if (month.HasValue) q = q.Where(x => x.Month == month.Value); + var items = q.OrderByDescending(x => x.Year).ThenByDescending(x => x.Month).ThenBy(x => x.GyzkId).ToList(); + + var zks = await _zkRepo.GetAllAsync(); + var zkDict = zks.ToDictionary(z => z.Id, z => z.ZhuangKouName); + + return items.Select(x => new ConfGyzkListItemDto + { + Id = x.Id, Year = x.Year, Month = x.Month, GyzkId = x.GyzkId, + GyzkName = zkDict.GetValueOrDefault(x.GyzkId, ""), + Guige = x.Guige, QsTaichan = x.QsTaichan, QsDengji = x.QsDengji, QsDengjifen = x.QsDengjifen, + SaoZhe = x.SaoZhe, MeiTongJianLiang = x.MeiTongJianLiang, LuoSiTongShu = x.LuoSiTongShu, + MeiCheSiLiang = x.MeiCheSiLiang, DcgJijianXishu = x.DcgJijianXishu, CtgJijianXishu = x.CtgJijianXishu, + GyzkLtwxKs = x.GyzkLtwxKs, GyzkLtwxl = x.GyzkLtwxl, XjChanliangPrice = x.XjChanliangPrice + }).ToList(); + } + + public async Task GetForEditAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("工艺庄口薪资配置不存在或已删除", 404); + return new ConfGyzkSaveDto + { + Id = e.Id, Year = e.Year, Month = e.Month, GyzkId = e.GyzkId, Guige = e.Guige, + QsTaichan = e.QsTaichan, QsDengji = e.QsDengji, QsDengjifen = e.QsDengjifen, SaoZhe = e.SaoZhe, + MeiTongJianLiang = e.MeiTongJianLiang, LuoSiTongShu = e.LuoSiTongShu, MeiCheSiLiang = e.MeiCheSiLiang, + DcgJijianXishu = e.DcgJijianXishu, CtgJijianXishu = e.CtgJijianXishu, + GyzkLtwxKs = e.GyzkLtwxKs, GyzkLtwxl = e.GyzkLtwxl, XjChanliangPrice = e.XjChanliangPrice + }; + } + + public async Task SaveAsync(ConfGyzkSaveDto dto) + { + if (dto.GyzkId == 0) throw new ServiceException("请选择工艺庄口"); + if (dto.Year <= 0 || dto.Month <= 0) throw new ServiceException("年度/月份无效"); + + ConfGyzk e; + if (dto.Id > 0) + { + e = await _repo.GetByIdAsync(dto.Id); + if (e == null) throw new ServiceException("工艺庄口薪资配置不存在或已删除", 404); + var dup = await _repo.FirstOrDefaultAsync(x => x.Id != dto.Id && x.Year == dto.Year + && x.Month == dto.Month && x.GyzkId == dto.GyzkId); + if (dup != null) throw new ServiceException("该年度月份已存在此庄口的配置!"); + } + else + { + var dup = await _repo.AnyAsync(x => x.Year == dto.Year && x.Month == dto.Month && x.GyzkId == dto.GyzkId); + if (dup) throw new ServiceException("该年度月份已存在此庄口的配置!"); + e = new ConfGyzk(); + await _repo.AddAsync(e); + } + + e.Year = dto.Year; + e.Month = dto.Month; + e.GyzkId = dto.GyzkId; + e.Guige = dto.Guige ?? ""; + e.QsTaichan = dto.QsTaichan; + e.QsDengji = dto.QsDengji ?? ""; + e.QsDengjifen = dto.QsDengjifen; + e.SaoZhe = dto.SaoZhe; + e.MeiTongJianLiang = dto.MeiTongJianLiang; + e.LuoSiTongShu = dto.LuoSiTongShu; + e.MeiCheSiLiang = dto.MeiCheSiLiang; + e.DcgJijianXishu = dto.DcgJijianXishu; + e.CtgJijianXishu = dto.CtgJijianXishu; + e.GyzkLtwxKs = dto.GyzkLtwxKs; + e.GyzkLtwxl = dto.GyzkLtwxl; + e.XjChanliangPrice = dto.XjChanliangPrice; + + await _repo.UpdateAsync(e); + return e.Id; + } + + public async Task DeleteAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("工艺庄口薪资配置不存在或已删除", 404); + await _repo.HardDeleteAsync(e); + } +} + +// ==================== 薪资配置-工种 ==================== + +public interface IConfGzService +{ + Task> GetListAsync(); + Task GetForEditAsync(long id); + Task SaveAsync(ConfGzSaveDto dto); + Task DeleteAsync(long id); +} + +public class ConfGzService : IConfGzService +{ + private readonly IRepository _repo; + private readonly IRepository _wtRepo; + + public ConfGzService(IRepository repo, IRepository wtRepo) + { + _repo = repo; + _wtRepo = wtRepo; + } + + public async Task> GetListAsync() + { + var items = await _repo.GetAllAsync(); + var wts = await _wtRepo.GetAllAsync(); + var wtDict = wts.ToDictionary(w => w.Id, w => w.WorkName); + return items.Select(x => ToDto(x, wtDict)).ToList(); + } + + public async Task GetForEditAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("工种薪资配置不存在或已删除", 404); + return FromEntity(e); + } + + public async Task SaveAsync(ConfGzSaveDto dto) + { + if (dto.WorkTypeId == 0) throw new ServiceException("请选择工种"); + + ConfGz e; + if (dto.Id > 0) + { + e = await _repo.GetByIdAsync(dto.Id); + if (e == null) throw new ServiceException("工种薪资配置不存在或已删除", 404); + var dup = await _repo.FirstOrDefaultAsync(x => x.Id != dto.Id && x.WorkTypeId == dto.WorkTypeId); + if (dup != null) throw new ServiceException("该工种已存在薪资配置!"); + } + else + { + var dup = await _repo.AnyAsync(x => x.WorkTypeId == dto.WorkTypeId); + if (dup) throw new ServiceException("该工种已存在薪资配置!"); + e = new ConfGz(); + await _repo.AddAsync(e); + } + Apply(e, dto); + await _repo.UpdateAsync(e); + return e.Id; + } + + public async Task DeleteAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("工种薪资配置不存在或已删除", 404); + await _repo.HardDeleteAsync(e); + } + + private static ConfGzListItemDto ToDto(ConfGz x, Dictionary wtDict) => new() + { + Id = x.Id, WorkTypeId = x.WorkTypeId, WorkTypeName = wtDict.GetValueOrDefault(x.WorkTypeId, ""), + HoursPerday = x.HoursPerday, QqDaysStandard = x.QqDaysStandard, QqHoursMaxQj = x.QqHoursMaxQj, + QqHoursDkMinjiaban = x.QqHoursDkMinjiaban, QqHoursDkQingjia = x.QqHoursDkQingjia, + XgJineQuanqin = x.XgJineQuanqin, XgJineGangwei = x.XgJineGangwei, JineNiangong = x.JineNiangong, + JineQuanqin = x.JineQuanqin, JineGangwei = x.JineGangwei, JineYuekhJixiao = x.JineYuekhJixiao, + JineGwkh = x.JineGwkh, JineYueticheng = x.JineYueticheng, JineJtkhYear = x.JineJtkhYear, + JineJisuanjishu = x.JineJisuanjishu, JineYeban = x.JineYeban, JineJiaotongButie = x.JineJiaotongButie, + LiudongJineGuding = x.LiudongJineGuding, JiaqiJineKuangongKk = x.JiaqiJineKuangongKk, + JiaqiJineFadingGz = x.JiaqiJineFadingGz, JiaqiJineFadingBz = x.JiaqiJineFadingBz, + JiaqiJineHunjiaBz = x.JiaqiJineHunjiaBz, JiaqiJineSangjiaBz = x.JiaqiJineSangjiaBz, + JiaqiJineBingjiaBz = x.JiaqiJineBingjiaBz, JiaqiJineLunxiuBz = x.JiaqiJineLunxiuBz, + JiabanJineBz = x.JiabanJineBz, Jiaqi_Jine_TiaoxiuBz = x.Jiaqi_Jine_TiaoxiuBz, + Jiaqi_Jine_NianjiaBz = x.Jiaqi_Jine_NianjiaBz, GwsrkhJineQianhege = x.GwsrkhJineQianhege, + GwsrkhJineQiwang = x.GwsrkhJineQiwang, GwsrkhJineHege = x.GwsrkhJineHege, + GwsrkhJineShengren = x.GwsrkhJineShengren, GwsrkhJineChaoshengren = x.GwsrkhJineChaoshengren, + Jiaban_GwsrkhJineQianhege = x.Jiaban_GwsrkhJineQianhege, Jiaban_GwsrkhJineQiwang = x.Jiaban_GwsrkhJineQiwang, + Jiaban_GwsrkhJineHege = x.Jiaban_GwsrkhJineHege, Jiaban_GwsrkhJineShengren = x.Jiaban_GwsrkhJineShengren, + Jiaban_GwsrkhJineChaoshengren = x.Jiaban_GwsrkhJineChaoshengren + }; + + private static ConfGzSaveDto FromEntity(ConfGz x) => new() + { + Id = x.Id, WorkTypeId = x.WorkTypeId, HoursPerday = x.HoursPerday, QqDaysStandard = x.QqDaysStandard, + QqHoursMaxQj = x.QqHoursMaxQj, QqHoursDkMinjiaban = x.QqHoursDkMinjiaban, QqHoursDkQingjia = x.QqHoursDkQingjia, + XgJineQuanqin = x.XgJineQuanqin, XgJineGangwei = x.XgJineGangwei, JineNiangong = x.JineNiangong, + JineQuanqin = x.JineQuanqin, JineGangwei = x.JineGangwei, JineYuekhJixiao = x.JineYuekhJixiao, + JineGwkh = x.JineGwkh, JineYueticheng = x.JineYueticheng, JineJtkhYear = x.JineJtkhYear, + JineJisuanjishu = x.JineJisuanjishu, JineYeban = x.JineYeban, JineJiaotongButie = x.JineJiaotongButie, + LiudongJineGuding = x.LiudongJineGuding, JiaqiJineKuangongKk = x.JiaqiJineKuangongKk, + JiaqiJineFadingGz = x.JiaqiJineFadingGz, JiaqiJineFadingBz = x.JiaqiJineFadingBz, + JiaqiJineHunjiaBz = x.JiaqiJineHunjiaBz, JiaqiJineSangjiaBz = x.JiaqiJineSangjiaBz, + JiaqiJineBingjiaBz = x.JiaqiJineBingjiaBz, JiaqiJineLunxiuBz = x.JiaqiJineLunxiuBz, + JiabanJineBz = x.JiabanJineBz, Jiaqi_Jine_TiaoxiuBz = x.Jiaqi_Jine_TiaoxiuBz, + Jiaqi_Jine_NianjiaBz = x.Jiaqi_Jine_NianjiaBz, GwsrkhJineQianhege = x.GwsrkhJineQianhege, + GwsrkhJineQiwang = x.GwsrkhJineQiwang, GwsrkhJineHege = x.GwsrkhJineHege, + GwsrkhJineShengren = x.GwsrkhJineShengren, GwsrkhJineChaoshengren = x.GwsrkhJineChaoshengren, + Jiaban_GwsrkhJineQianhege = x.Jiaban_GwsrkhJineQianhege, Jiaban_GwsrkhJineQiwang = x.Jiaban_GwsrkhJineQiwang, + Jiaban_GwsrkhJineHege = x.Jiaban_GwsrkhJineHege, Jiaban_GwsrkhJineShengren = x.Jiaban_GwsrkhJineShengren, + Jiaban_GwsrkhJineChaoshengren = x.Jiaban_GwsrkhJineChaoshengren + }; + + private static void Apply(ConfGz e, ConfGzSaveDto d) + { + e.WorkTypeId = d.WorkTypeId; + e.HoursPerday = d.HoursPerday; e.QqDaysStandard = d.QqDaysStandard; + e.QqHoursMaxQj = d.QqHoursMaxQj; e.QqHoursDkMinjiaban = d.QqHoursDkMinjiaban; + e.QqHoursDkQingjia = d.QqHoursDkQingjia; e.XgJineQuanqin = d.XgJineQuanqin; + e.XgJineGangwei = d.XgJineGangwei; e.JineNiangong = d.JineNiangong; + e.JineQuanqin = d.JineQuanqin; e.JineGangwei = d.JineGangwei; + e.JineYuekhJixiao = d.JineYuekhJixiao; e.JineGwkh = d.JineGwkh; + e.JineYueticheng = d.JineYueticheng; e.JineJtkhYear = d.JineJtkhYear; + e.JineJisuanjishu = d.JineJisuanjishu; e.JineYeban = d.JineYeban; + e.JineJiaotongButie = d.JineJiaotongButie; e.LiudongJineGuding = d.LiudongJineGuding; + e.JiaqiJineKuangongKk = d.JiaqiJineKuangongKk; e.JiaqiJineFadingGz = d.JiaqiJineFadingGz; + e.JiaqiJineFadingBz = d.JiaqiJineFadingBz; e.JiaqiJineHunjiaBz = d.JiaqiJineHunjiaBz; + e.JiaqiJineSangjiaBz = d.JiaqiJineSangjiaBz; e.JiaqiJineBingjiaBz = d.JiaqiJineBingjiaBz; + e.JiaqiJineLunxiuBz = d.JiaqiJineLunxiuBz; e.JiabanJineBz = d.JiabanJineBz; + e.Jiaqi_Jine_TiaoxiuBz = d.Jiaqi_Jine_TiaoxiuBz; e.Jiaqi_Jine_NianjiaBz = d.Jiaqi_Jine_NianjiaBz; + e.GwsrkhJineQianhege = d.GwsrkhJineQianhege; e.GwsrkhJineQiwang = d.GwsrkhJineQiwang; + e.GwsrkhJineHege = d.GwsrkhJineHege; e.GwsrkhJineShengren = d.GwsrkhJineShengren; + e.GwsrkhJineChaoshengren = d.GwsrkhJineChaoshengren; + e.Jiaban_GwsrkhJineQianhege = d.Jiaban_GwsrkhJineQianhege; + e.Jiaban_GwsrkhJineQiwang = d.Jiaban_GwsrkhJineQiwang; + e.Jiaban_GwsrkhJineHege = d.Jiaban_GwsrkhJineHege; + e.Jiaban_GwsrkhJineShengren = d.Jiaban_GwsrkhJineShengren; + e.Jiaban_GwsrkhJineChaoshengren = d.Jiaban_GwsrkhJineChaoshengren; + } +} + +// ==================== 薪资配置-挡车/车头岗位 ==================== + +public interface IConfDcgService +{ + Task> GetListAsync(); + Task GetForEditAsync(long id); + Task SaveAsync(ConfDcgSaveDto dto); + Task DeleteAsync(long id); +} + +public class ConfDcgService : IConfDcgService +{ + private readonly IRepository _repo; + private readonly IRepository _depRepo; + + public ConfDcgService(IRepository repo, IRepository depRepo) + { + _repo = repo; + _depRepo = depRepo; + } + + public async Task> GetListAsync() + { + var items = await _repo.GetAllAsync(); + var deps = await _depRepo.GetAllAsync(); + var depDict = deps.ToDictionary(d => d.Id, d => d.Name); + return items.Select(x => new ConfDcgListItemDto + { + Id = x.Id, DepId = x.DepId, DepName = depDict.GetValueOrDefault(x.DepId, ""), + JineJisuanjishu50 = x.JineJisuanjishu50, JineJisuanjishu60 = x.JineJisuanjishu60, + JineJisuanjishu70 = x.JineJisuanjishu70, JineJisuanjishu100 = x.JineJisuanjishu100, + JineCtg = x.JineCtg + }).ToList(); + } + + public async Task GetForEditAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("挡车岗位薪资配置不存在或已删除", 404); + return new ConfDcgSaveDto + { + Id = e.Id, DepId = e.DepId, JineJisuanjishu50 = e.JineJisuanjishu50, + JineJisuanjishu60 = e.JineJisuanjishu60, JineJisuanjishu70 = e.JineJisuanjishu70, + JineJisuanjishu100 = e.JineJisuanjishu100, JineCtg = e.JineCtg + }; + } + + public async Task SaveAsync(ConfDcgSaveDto dto) + { + if (dto.DepId == 0) throw new ServiceException("请选择部门"); + + ConfDcg e; + if (dto.Id > 0) + { + e = await _repo.GetByIdAsync(dto.Id); + if (e == null) throw new ServiceException("挡车岗位薪资配置不存在或已删除", 404); + var dup = await _repo.FirstOrDefaultAsync(x => x.Id != dto.Id && x.DepId == dto.DepId); + if (dup != null) throw new ServiceException("该部门已存在挡车岗位薪资配置!"); + } + else + { + var dup = await _repo.AnyAsync(x => x.DepId == dto.DepId); + if (dup) throw new ServiceException("该部门已存在挡车岗位薪资配置!"); + e = new ConfDcg(); + await _repo.AddAsync(e); + } + e.DepId = dto.DepId; + e.JineJisuanjishu50 = dto.JineJisuanjishu50; + e.JineJisuanjishu60 = dto.JineJisuanjishu60; + e.JineJisuanjishu70 = dto.JineJisuanjishu70; + e.JineJisuanjishu100 = dto.JineJisuanjishu100; + e.JineCtg = dto.JineCtg; + await _repo.UpdateAsync(e); + return e.Id; + } + + public async Task DeleteAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("挡车岗位薪资配置不存在或已删除", 404); + await _repo.HardDeleteAsync(e); + } +} + +// ==================== 挡车纤度奖扣 ==================== + +public interface IDcxdjkService +{ + Task> GetListAsync(string guige); + Task GetForEditAsync(long id); + Task SaveAsync(DcxdjkSaveDto dto); + Task DeleteAsync(long id); +} + +public class DcxdjkService : IDcxdjkService +{ + private readonly IRepository _repo; + + public DcxdjkService(IRepository repo) => _repo = repo; + + public async Task> GetListAsync(string guige) + { + var items = await _repo.GetAllAsync(); + var q = items.AsQueryable(); + if (!string.IsNullOrWhiteSpace(guige)) q = q.Where(x => x.GuiGe == guige); + return q.OrderBy(x => x.GuiGe).ThenBy(x => x.XianDu).ToList() + .Select(x => new DcxdjkListItemDto + { + Id = x.Id, GuiGe = x.GuiGe, XianDu = x.XianDu, JiangLi = x.JiangLi, KouKuan = x.KouKuan + }).ToList(); + } + + public async Task GetForEditAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("挡车纤度奖扣不存在或已删除", 404); + return new DcxdjkSaveDto { Id = e.Id, GuiGe = e.GuiGe, XianDu = e.XianDu, JiangLi = e.JiangLi, KouKuan = e.KouKuan }; + } + + public async Task SaveAsync(DcxdjkSaveDto dto) + { + if (string.IsNullOrWhiteSpace(dto.GuiGe)) throw new ServiceException("请填写规格"); + + DangCheXianDuJiangKou e; + if (dto.Id > 0) + { + e = await _repo.GetByIdAsync(dto.Id); + if (e == null) throw new ServiceException("挡车纤度奖扣不存在或已删除", 404); + var dup = await _repo.FirstOrDefaultAsync(x => x.Id != dto.Id && x.GuiGe == dto.GuiGe && x.XianDu == dto.XianDu); + if (dup != null) throw new ServiceException("该规格已存在相同纤度的奖扣配置!"); + } + else + { + var dup = await _repo.AnyAsync(x => x.GuiGe == dto.GuiGe && x.XianDu == dto.XianDu); + if (dup) throw new ServiceException("该规格已存在相同纤度的奖扣配置!"); + e = new DangCheXianDuJiangKou(); + await _repo.AddAsync(e); + } + e.GuiGe = dto.GuiGe.Trim(); + e.XianDu = dto.XianDu; + e.JiangLi = dto.JiangLi; + e.KouKuan = dto.KouKuan; + await _repo.UpdateAsync(e); + return e.Id; + } + + public async Task DeleteAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("挡车纤度奖扣不存在或已删除", 404); + await _repo.HardDeleteAsync(e); + } +} + +// ==================== 薪资价格配置 ==================== + +public interface IXzjgService +{ + Task> GetListAsync(string group); + Task GetForEditAsync(long id); + Task SaveAsync(XzjgSaveDto dto); + Task DeleteAsync(long id); + Task GetPriceAsync(string group, string key); +} + +public class XzjgService : IXzjgService +{ + private readonly IRepository _repo; + + public XzjgService(IRepository repo) => _repo = repo; + + public async Task> GetListAsync(string group) + { + var items = await _repo.GetAllAsync(); + var q = items.AsQueryable(); + if (!string.IsNullOrWhiteSpace(group)) q = q.Where(x => x.Group == group); + return q.OrderBy(x => x.Group).ThenBy(x => x.Key).ToList() + .Select(x => new XzjgListItemDto + { + Id = x.Id, Group = x.Group, Key = x.Key, Unit = x.Unit, Price = x.Price, Memo = x.Memo + }).ToList(); + } + + public async Task GetForEditAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("薪资价格配置不存在或已删除", 404); + return new XzjgSaveDto { Id = e.Id, Group = e.Group, Key = e.Key, Unit = e.Unit, Price = e.Price, Memo = e.Memo }; + } + + public async Task SaveAsync(XzjgSaveDto dto) + { + if (string.IsNullOrWhiteSpace(dto.Group)) throw new ServiceException("请填写分组"); + if (string.IsNullOrWhiteSpace(dto.Key)) throw new ServiceException("请填写项目"); + var grp = dto.Group.Trim(); + var key = dto.Key.Trim(); + + XinZiJiaGePeiZhi e; + if (dto.Id > 0) + { + e = await _repo.GetByIdAsync(dto.Id); + if (e == null) throw new ServiceException("薪资价格配置不存在或已删除", 404); + var dup = await _repo.FirstOrDefaultAsync(x => x.Id != dto.Id && x.Group == grp && x.Key == key); + if (dup != null) throw new ServiceException("该分组已存在相同的项目!"); + } + else + { + var dup = await _repo.AnyAsync(x => x.Group == grp && x.Key == key); + if (dup) throw new ServiceException("该分组已存在相同的项目!"); + e = new XinZiJiaGePeiZhi(); + await _repo.AddAsync(e); + } + e.Group = grp; + e.Key = key; + e.Unit = dto.Unit ?? ""; + e.Price = dto.Price; + e.Memo = dto.Memo ?? ""; + await _repo.UpdateAsync(e); + return e.Id; + } + + public async Task DeleteAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("薪资价格配置不存在或已删除", 404); + await _repo.HardDeleteAsync(e); + } + + /// 取价:无配置返回 0 + public async Task GetPriceAsync(string group, string key) + { + var e = await _repo.FirstOrDefaultAsync(x => x.Group == group && x.Key == key); + return e?.Price ?? 0m; + } +} diff --git a/backend/F9WebAiTest.Application/Services/Prod/CdsService.cs b/backend/F9WebAiTest.Application/Services/Prod/CdsService.cs new file mode 100644 index 0000000..ef06a74 --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Prod/CdsService.cs @@ -0,0 +1,257 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities.Base; +using F9WebAiTest.Domain.Entities.Prod; +using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments; + +namespace F9WebAiTest.Application.Services.Prod; + +/// 秤大丝:以落丝记录为主表,按车台录入 4 个丝量值并汇总把数/丝量/缫折/台产 +public interface ICdsService +{ + Task> GetPagedAsync(int pageIndex, int pageSize, DateTime? date, long? deptId); + Task GetEditAsync(long lsjlId); + Task SaveAsync(CdsSaveDto dto); + Task DeleteAsync(long lsjlId); +} + +public class CdsService : ICdsService +{ + private readonly IRepository _lsjlRepo; + private readonly IRepository _cdsLsjlRepo; + private readonly IRepository _cdsChetaiRepo; + private readonly IRepository _deptRepo; + private readonly IRepository _zkRepo; + + public CdsService( + IRepository lsjlRepo, + IRepository cdsLsjlRepo, + IRepository cdsChetaiRepo, + IRepository deptRepo, + IRepository zkRepo) + { + _lsjlRepo = lsjlRepo; + _cdsLsjlRepo = cdsLsjlRepo; + _cdsChetaiRepo = cdsChetaiRepo; + _deptRepo = deptRepo; + _zkRepo = zkRepo; + } + + public async Task> GetPagedAsync(int pageIndex, int pageSize, DateTime? date, long? deptId) + { + var lsjls = await _lsjlRepo.GetAllAsync(); + var q = lsjls.AsQueryable(); + if (date.HasValue) q = q.Where(x => x.WorkDate.Date == date.Value.Date); + if (deptId.HasValue) q = q.Where(x => x.DeptId == deptId.Value); + + var depts = await _deptRepo.GetAllAsync(); + var deptDict = depts.ToDictionary(d => d.Id, d => d.Name); + var zks = await _zkRepo.GetAllAsync(); + var zkDict = zks.ToDictionary(z => z.Id, z => z.ZhuangKouName); + var sums = (await _cdsLsjlRepo.GetAllAsync()).ToDictionary(x => x.LsjlId); + + var total = q.LongCount(); + var items = q.OrderByDescending(x => x.WorkDate).ThenBy(x => x.Jizu).ThenBy(x => x.Huishu) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + return new PageResult + { + Items = items.Select(x => + { + sums.TryGetValue(x.Id, out var sum); + return new CdsListItemDto + { + LsjlId = x.Id, + WorkDate = x.WorkDate, + DeptId = x.DeptId, + DeptName = deptDict.GetValueOrDefault(x.DeptId, ""), + Jizu = x.Jizu, + Huishu = x.Huishu, + Guige = x.Guige, + ZhuangKouName = zkDict.GetValueOrDefault(x.GyzkId, ""), + Yanse = x.Yanse, + Isbanche = x.Isbanche, + IsbancheName = x.Isbanche ? "半车" : "整车", + Chetaishu = x.Chetaishu, + ChetaiStart = x.ChetaiStart, + ChetaiEnd = x.ChetaiEnd, + Hours = x.Hours, + Tongshu = x.Tongshu, + Meitongjianliang = x.Meitongjianliang, + YingchengNum = YingchengNum(x), + IsWeighed = sum != null, + Num = sum?.Num ?? 0, + Siliang = sum?.Siliang ?? 0, + Jianliang = sum?.Jianliang ?? 0, + Saozhe = sum?.Saozhe ?? 0, + Taichan = sum?.Taichan ?? 0 + }; + }).ToList(), + Total = total, PageIndex = pageIndex, PageSize = pageSize + }; + } + + public async Task GetEditAsync(long lsjlId) + { + var lsjl = await _lsjlRepo.GetByIdAsync(lsjlId); + if (lsjl == null) throw new ServiceException("落丝记录不存在", 404); + + var dept = await _deptRepo.GetByIdAsync(lsjl.DeptId); + var zk = await _zkRepo.GetByIdAsync(lsjl.GyzkId); + + var records = await _cdsChetaiRepo.FindAsync(x => x.LsjlId == lsjlId); + List chetais; + if (records.Count > 0) + { + chetais = records.OrderBy(r => r.Chetaihao).Select(ToDto).ToList(); + } + else + { + // 首次称重:按车台范围初始化空行 + chetais = new List(); + for (var i = lsjl.ChetaiStart; i <= lsjl.ChetaiEnd; i++) + chetais.Add(new CdsChetaiDto { Chetaihao = i }); + } + + return new CdsEditDto + { + LsjlId = lsjl.Id, + DeptName = dept?.Name ?? "", + Jizu = lsjl.Jizu, + Huishu = lsjl.Huishu, + ZhuangKouName = zk?.ZhuangKouName ?? "", + Yanse = lsjl.Yanse, + Isbanche = lsjl.Isbanche, + IsbancheName = lsjl.Isbanche ? "半车" : "整车", + Guige = lsjl.Guige, + ChetaiStart = lsjl.ChetaiStart, + ChetaiEnd = lsjl.ChetaiEnd, + Chetaishu = lsjl.Chetaishu, + YingchengNum = YingchengNum(lsjl), + Chetais = chetais + }; + } + + public async Task SaveAsync(CdsSaveDto dto) + { + if (dto.LsjlId == 0) throw new ServiceException("落丝记录无效"); + var lsjl = await _lsjlRepo.GetByIdAsync(dto.LsjlId); + if (lsjl == null) throw new ServiceException("落丝记录不存在", 404); + + if (dto.Chetais == null || dto.Chetais.Count == 0) throw new ServiceException("请先录入丝量数据"); + + var invalid = dto.Chetais.Where(i => i.Chetaihao < lsjl.ChetaiStart || i.Chetaihao > lsjl.ChetaiEnd).ToList(); + if (invalid.Count > 0) throw new ServiceException($"车台号超出范围 [{lsjl.ChetaiStart}-{lsjl.ChetaiEnd}]"); + + // 车台明细:个数 = 丝量 > 0 的个数;小计丝量 = 4 个丝量之和 + var items = dto.Chetais.Select(i => + { + var t = new CdsChetaiDto + { + Chetaihao = i.Chetaihao, + Sl1 = Math.Max(0, i.Sl1), Sl2 = Math.Max(0, i.Sl2), + Sl3 = Math.Max(0, i.Sl3), Sl4 = Math.Max(0, i.Sl4) + }; + CalChetai(t); + return t; + }).OrderBy(i => i.Chetaihao).ToList(); + + var num = items.Sum(x => x.Num); + if (num == 0) throw new ServiceException("请至少录入一个丝量值"); + + // 车台明细先删后建 + var olds = await _cdsChetaiRepo.FindAsync(x => x.LsjlId == dto.LsjlId); + if (olds.Count > 0) await _cdsChetaiRepo.HardDeleteRangeAsync(olds); + + foreach (var t in items) + { + await _cdsChetaiRepo.AddAsync(new CdsChetai + { + LsjlId = dto.LsjlId, + Chetaihao = t.Chetaihao, + Sl1 = t.Sl1, Sl2 = t.Sl2, Sl3 = t.Sl3, Sl4 = t.Sl4, + Num = t.Num, Siliang = t.Siliang + }); + } + + var siliang = items.Sum(x => x.Siliang); + + // 汇总:茧量 / 缫折 / 实绩台产 + var cdsLsjl = await _cdsLsjlRepo.FirstOrDefaultAsync(x => x.LsjlId == dto.LsjlId); + if (cdsLsjl == null) + { + cdsLsjl = new CdsLsjl { LsjlId = dto.LsjlId }; + await _cdsLsjlRepo.AddAsync(cdsLsjl); + } + cdsLsjl.Num = num; + cdsLsjl.Siliang = siliang; + CalZhibiao(cdsLsjl, lsjl); + await _cdsLsjlRepo.UpdateAsync(cdsLsjl); + } + + /// 清除称重数据(汇总 + 车台明细物理删除,便于重新称重) + public async Task DeleteAsync(long lsjlId) + { + if (lsjlId == 0) throw new ServiceException("落丝记录无效"); + var chetais = await _cdsChetaiRepo.FindAsync(x => x.LsjlId == lsjlId); + if (chetais.Count > 0) await _cdsChetaiRepo.HardDeleteRangeAsync(chetais); + + var lsjlSum = await _cdsLsjlRepo.FirstOrDefaultAsync(x => x.LsjlId == lsjlId); + if (lsjlSum != null) await _cdsLsjlRepo.HardDeleteAsync(lsjlSum); + } + + // ==================== 计算口径(对齐 F9 Cds_Lsjl_CalZhibiaoCommand) ==================== + + /// 应秤数 = 车台数 × 4 ×(半车 0.5 / 整车 1) + private static int YingchengNum(LsjlRecord lsjl) + => (int)Math.Round(lsjl.Chetaishu * 4 * (lsjl.Isbanche ? 0.5 : 1), MidpointRounding.AwayFromZero); + + private static void CalChetai(CdsChetaiDto item) + { + item.Num = (item.Sl1 > 0 ? 1 : 0) + (item.Sl2 > 0 ? 1 : 0) + (item.Sl3 > 0 ? 1 : 0) + (item.Sl4 > 0 ? 1 : 0); + item.Siliang = item.Sl1 + item.Sl2 + item.Sl3 + item.Sl4; + } + + /// + /// 茧量 = 落丝桶数 × 每桶茧量;缫折 = 茧量 / 丝量 × 100;实绩台产 = 丝量 / 落丝时长 / 车台数。 + /// F9 口径:缫折 ≤ 100 视为异常数据,不落指标。 + /// + private static void CalZhibiao(CdsLsjl target, LsjlRecord lsjl) + { + if (lsjl.Meitongjianliang == 0 || lsjl.Tongshu == 0) + { + target.Jianliang = 0; target.Saozhe = 0; target.Taichan = 0; + return; + } + if (target.Num <= 0 || target.Siliang <= 0 || lsjl.Hours <= 0 || lsjl.Chetaishu <= 0) + { + target.Jianliang = 0; target.Saozhe = 0; target.Taichan = 0; + return; + } + + var jianliang = lsjl.Tongshu * lsjl.Meitongjianliang; + var saozhe = Math.Round((decimal)jianliang / target.Siliang * 100, 2, MidpointRounding.AwayFromZero); + var taichan = Math.Round((decimal)target.Siliang / (decimal)lsjl.Hours / lsjl.Chetaishu, 2, MidpointRounding.AwayFromZero); + + if (saozhe > 100) + { + target.Jianliang = jianliang; + target.Saozhe = saozhe; + target.Taichan = taichan; + } + else + { + target.Jianliang = jianliang; + target.Saozhe = 0; + target.Taichan = 0; + } + } + + private static CdsChetaiDto ToDto(CdsChetai r) => new() + { + Chetaihao = r.Chetaihao, + Sl1 = r.Sl1, Sl2 = r.Sl2, Sl3 = r.Sl3, Sl4 = r.Sl4, + Num = r.Num, Siliang = r.Siliang + }; +} diff --git a/backend/F9WebAiTest.Application/Services/Prod/GlxdService.cs b/backend/F9WebAiTest.Application/Services/Prod/GlxdService.cs new file mode 100644 index 0000000..c250c3b --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Prod/GlxdService.cs @@ -0,0 +1,109 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities.Base; +using F9WebAiTest.Domain.Entities.Prod; +using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments; + +namespace F9WebAiTest.Application.Services.Prod; + +/// 公量纤度:以落丝记录为主表,取纤度检验的平均纤度,由质检录入公量纤度(按日期 + 车间整日批量保存) +public interface IGlxdService +{ + Task> GetListAsync(DateTime? date, long? deptId); + Task SaveAsync(GlxdSaveDto dto); +} + +public class GlxdService : IGlxdService +{ + private readonly IRepository _lsjlRepo; + private readonly IRepository _glxdRepo; + private readonly IRepository _xdjyLsjlRepo; + private readonly IRepository _deptRepo; + private readonly IRepository _zkRepo; + + public GlxdService( + IRepository lsjlRepo, + IRepository glxdRepo, + IRepository xdjyLsjlRepo, + IRepository deptRepo, + IRepository zkRepo) + { + _lsjlRepo = lsjlRepo; + _glxdRepo = glxdRepo; + _xdjyLsjlRepo = xdjyLsjlRepo; + _deptRepo = deptRepo; + _zkRepo = zkRepo; + } + + public async Task> GetListAsync(DateTime? date, long? deptId) + { + var lsjls = await _lsjlRepo.GetAllAsync(); + var q = lsjls.AsQueryable(); + if (date.HasValue) q = q.Where(x => x.WorkDate.Date == date.Value.Date); + if (deptId.HasValue) q = q.Where(x => x.DeptId == deptId.Value); + + var items = q.OrderBy(x => x.DeptId).ThenBy(x => x.Jizu).ThenBy(x => x.Huishu).ToList(); + if (items.Count == 0) return new List(); + + var depts = await _deptRepo.GetAllAsync(); + var deptDict = depts.ToDictionary(d => d.Id, d => d.Name); + var zks = await _zkRepo.GetAllAsync(); + var zkDict = zks.ToDictionary(z => z.Id, z => z.ZhuangKouName); + var glxds = (await _glxdRepo.GetAllAsync()).ToDictionary(x => x.LsjlId); + var xdjySums = (await _xdjyLsjlRepo.GetAllAsync()).ToDictionary(x => x.LsjlId); + + return items.Select(x => + { + glxds.TryGetValue(x.Id, out var g); + xdjySums.TryGetValue(x.Id, out var xs); + return new GlxdListItemDto + { + LsjlId = x.Id, + WorkDate = x.WorkDate, + DeptId = x.DeptId, + DeptName = deptDict.GetValueOrDefault(x.DeptId, ""), + Jizu = x.Jizu, + Huishu = x.Huishu, + Guige = x.Guige, + ZhuangKouName = zkDict.GetValueOrDefault(x.GyzkId, ""), + Yanse = x.Yanse, + Isbanche = x.Isbanche, + IsbancheName = x.Isbanche ? "半车" : "整车", + Chetaishu = x.Chetaishu, + Avgxiandu = xs?.Avgxiandu ?? 0, + Gongliangxiandu = g?.Gongliangxiandu ?? 0 + }; + }).ToList(); + } + + public async Task SaveAsync(GlxdSaveDto dto) + { + if (dto.Items == null || dto.Items.Count == 0) throw new ServiceException("没有需要保存的公量纤度"); + + // 校验落丝记录存在,并按落丝记录 Id 去重 + var dict = new Dictionary(); + foreach (var it in dto.Items) + { + if (it.LsjlId == 0) throw new ServiceException("存在无效的落丝记录"); + var lsjl = await _lsjlRepo.GetByIdAsync(it.LsjlId); + if (lsjl == null) throw new ServiceException($"落丝记录 {it.LsjlId} 不存在", 404); + dict[it.LsjlId] = it.Gongliangxiandu; + } + + foreach (var (lsjlId, value) in dict) + { + var e = await _glxdRepo.FirstOrDefaultAsync(x => x.LsjlId == lsjlId); + if (e == null) + { + e = new Glxd { LsjlId = lsjlId, Gongliangxiandu = value }; + await _glxdRepo.AddAsync(e); + } + else + { + e.Gongliangxiandu = value; + await _glxdRepo.UpdateAsync(e); + } + } + } +} diff --git a/backend/F9WebAiTest.Application/Services/Prod/KaoqinService.cs b/backend/F9WebAiTest.Application/Services/Prod/KaoqinService.cs new file mode 100644 index 0000000..2cbb01a --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Prod/KaoqinService.cs @@ -0,0 +1,890 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities.PayRoll; +using F9WebAiTest.Domain.Entities.Prod; +using F9WebAiTest.Domain.Enums.SYS.Departments; +using F9WebAiTest.Domain.Enums.SYS.Employees; +using SysZq = F9WebAiTest.Domain.Entities.SYS.Zq; +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; + +/// +/// 职工考勤-考勤登记(CQDJ)。 +/// 对应 F9 WinClient\F9.Business.Prod\KQ\CQDJ。 +/// 保存语义与老系统一致:按(日期 + 部门 + 工种 + 班次)先软删旧登记与明细,再整批新增(不做增量 diff)。 +/// +public interface IKaoqinService +{ + /// 取某「日期+部门+工种+班次」的整组登记(未登记的职工按默认 9 小时生成) + Task GetRegisterAsync(DateTime date, long depId, long worktypeId, int banci, string keyword); + + /// 整组保存(覆盖) + Task SaveRegisterAsync(CqdjSaveDto dto); + + /// 删除整组登记(软删) + Task DeleteRegisterAsync(DateTime date, long depId, long worktypeId, int banci); + + /// 「添加职工」可选职工(本部门在职、未登记过的) + Task> GetEmployeesAsync(long depId, long worktypeId, int banci); + + // ==================== 考勤统计(CQTJ) ==================== + + /// 统计分页(库中的月快照) + Task> GetCqtjPagedAsync(int year, int month, long depId, long worktypeId, + string like, int pageIndex, int pageSize); + + /// 录入:库中已有快照则取库,否则按账期实时汇总 + Task GetCqtjForEditAsync(int year, int month, long depId); + + /// 保存(先软删该 年/月/部门 旧快照,再整批新增) + Task SaveCqtjAsync(CqtjSaveDto dto); + + /// 删除该 年/月/部门 的统计快照 + Task DeleteCqtjAsync(int year, int month, long depId); + + /// 一键生成:遍历全部车间,缺的才生成 + Task GenerateCqtjAllAsync(int year, int month); + + /// 全部删除:删除该年月的全部统计快照 + Task DeleteCqtjAllAsync(int year, int month); +} + +public class KaoqinService : IKaoqinService +{ + // 出勤类型(Cqdj_CqType_Enum,老系统硬编码) + private const int CqZhengchang = 1; + private const int CqQingjia = 2; + private const int CqLiudong = 3; + private const int CqJiaban = 4; + private const int CqDaiban = 5; + private const int CqWaipai = 6; + private const int CqKuanggong = 7; + private const int CqGongshang = 8; + + private const decimal DefaultHours = 9m; // 新职工默认标准 / 出勤时长 + + private readonly IRepository _cqdjRepo; + private readonly IRepository _itemRepo; + private readonly IRepository _cqtjRepo; + private readonly IRepository _deptRepo; + private readonly IRepository _empRepo; + private readonly IRepository _wtRepo; + private readonly IRepository _confGzRepo; + private readonly IRepository _confZqRepo; + private readonly IRepository _confEmpRepo; + private readonly IRepository _gwsrkhRepo; + private readonly IRepository _zhangqiRepo; + + public KaoqinService(IRepository cqdjRepo, IRepository itemRepo, IRepository cqtjRepo, + IRepository deptRepo, IRepository empRepo, + IRepository wtRepo, IRepository confGzRepo, IRepository confZqRepo, + IRepository confEmpRepo, IRepository gwsrkhRepo, IRepository zhangqiRepo) + { + _cqdjRepo = cqdjRepo; + _itemRepo = itemRepo; + _cqtjRepo = cqtjRepo; + _deptRepo = deptRepo; + _empRepo = empRepo; + _wtRepo = wtRepo; + _confGzRepo = confGzRepo; + _confZqRepo = confZqRepo; + _confEmpRepo = confEmpRepo; + _gwsrkhRepo = gwsrkhRepo; + _zhangqiRepo = zhangqiRepo; + } + + // ==================== 查询 ==================== + + public async Task GetRegisterAsync(DateTime date, long depId, long worktypeId, int banci, string keyword) + { + if (depId <= 0) throw new ServiceException("请先选择部门"); + + var day = date.Date; + var nextDay = day.AddDays(1); + var depts = (await _deptRepo.GetAllAsync()).ToDictionary(d => d.Id, d => d.Name); + var wts = (await _wtRepo.GetAllAsync()).ToDictionary(w => w.Id, w => w.WorkName); + var emps = await LoadEmployeesAsync(depId, worktypeId, banci); + + // 已保存的登记 + var saved = await _cqdjRepo.FindAsync(x => x.Date >= day && x.Date < nextDay + && x.DepId == depId && x.Banci == banci); + if (worktypeId > 0) saved = saved.Where(x => x.WorktypeId == worktypeId).ToList(); + var savedByEmp = saved.GroupBy(x => x.EmployeeId).ToDictionary(g => g.Key, g => g.First()); + + var ids = saved.Select(x => x.Id).ToList(); + var items = ids.Count == 0 + ? new List() + : await _itemRepo.FindAsync(x => ids.Contains(x.CqdjId)); + + var view = new CqdjViewDto { Date = day, DepId = depId, WorktypeId = worktypeId, Banci = banci }; + + foreach (var emp in emps) + { + if (!string.IsNullOrWhiteSpace(keyword)) + { + var key = keyword.Trim(); + if (!((emp.Name ?? "").Contains(key) || (emp.Code ?? "").Contains(key))) continue; + } + + var row = new CqdjRowDto + { + EmployeeId = emp.Id, + EmployeeCode = emp.Code ?? "", + EmployeeName = emp.Name ?? "", + DepId = emp.DepId, + DepName = depts.TryGetValue(emp.DepId, out var dn) ? dn : "", + WorktypeId = emp.WorktypeId, + WorktypeName = wts.TryGetValue(emp.WorktypeId, out var wn) ? wn : "", + Banci = banci, + IsXuegong = !emp.IsZhuanzheng, + }; + + if (savedByEmp.TryGetValue(emp.Id, out var rec)) + { + row.Id = rec.Id; + row.Saved = true; + row.IsXuegong = rec.IsXuegong; + row.HoursStandard = rec.HoursStandard; + row.HoursCq = rec.HoursCq; + row.HoursQj = rec.HoursQj; + row.HoursLd = rec.HoursLd; + row.HoursJb = rec.HoursJb; + row.HoursDb = rec.HoursDb; + row.HoursWp = rec.HoursWp; + row.HoursKg = rec.HoursKg; + row.HoursGs = rec.HoursGs; + row.CsYeban = rec.CsYeban; + row.Memo = rec.Memo ?? ""; + row.Items = items.Where(i => i.CqdjId == rec.Id) + .OrderBy(i => i.Id) + .Select(ToItemDto).ToList(); + } + else + { + // 未登记:默认标准 9 小时 + 一条「正常」明细 + row.HoursStandard = DefaultHours; + row.HoursCq = DefaultHours; + row.Items.Add(new CqdjItemDto + { + DepId = emp.DepId, + WorktypeId = emp.WorktypeId, + TypeCq = CqZhengchang, + TypeQj = 0, + Hours = DefaultHours, + }); + } + + view.Rows.Add(row); + } + + return view; + } + + // ==================== 保存 ==================== + + public async Task SaveRegisterAsync(CqdjSaveDto dto) + { + if (dto == null) throw new ServiceException("保存内容为空"); + if (dto.Date == default) throw new ServiceException("请选择考勤日期"); + if (dto.DepId <= 0) throw new ServiceException("请选择部门"); + if (dto.Rows == null || dto.Rows.Count == 0) throw new ServiceException("没有需要保存的职工"); + + var day = dto.Date.Date; + var nextDay = day.AddDays(1); + var empDict = (await _empRepo.GetAllAsync()).ToDictionary(e => e.Id); + + // 校验 + foreach (var row in dto.Rows) + { + var name = empDict.TryGetValue(row.EmployeeId, out var e) ? e.Name : $"#{row.EmployeeId}"; + var items = row.Items ?? new List(); + if (items.Count == 0) throw new ServiceException($"职工「{name}」没有考勤明细"); + if (items.Count(x => x.TypeCq == CqZhengchang) != 1) + throw new ServiceException($"职工「{name}」必须且只能有一条「正常」明细"); + foreach (var it in items) + { + if (it.TypeCq == CqQingjia && it.TypeQj == 0) throw new ServiceException($"职工「{name}」的请假明细未选择请假类型"); + if (it.TypeCq != CqQingjia && it.TypeQj != 0) throw new ServiceException($"职工「{name}」的非请假明细不能带请假类型"); + if (it.Hours < 0) throw new ServiceException($"职工「{name}」存在负时长"); + } + if (CalcHoursCq(items) < 0) throw new ServiceException($"职工「{name}」的出勤时长不能为负,请检查请假 / 旷工时长"); + } + + // 整组覆盖:先软删该(日期 + 部门 + 工种 + 班次)的旧登记与明细 + var old = await _cqdjRepo.FindAsync(x => x.Date >= day && x.Date < nextDay + && x.DepId == dto.DepId && x.Banci == dto.Banci); + if (dto.WorktypeId > 0) old = old.Where(x => x.WorktypeId == dto.WorktypeId).ToList(); + if (old.Count > 0) + { + var oldIds = old.Select(x => x.Id).ToList(); + var oldItems = await _itemRepo.FindAsync(x => oldIds.Contains(x.CqdjId)); + if (oldItems.Count > 0) await _itemRepo.DeleteRangeAsync(oldItems); + await _cqdjRepo.DeleteRangeAsync(old); + } + + var count = 0; + foreach (var row in dto.Rows) + { + var emp = empDict.TryGetValue(row.EmployeeId, out var e) ? e : null; + var items = row.Items ?? new List(); + + var master = new Cqdj + { + Date = day, + DepId = dto.DepId, + WorktypeId = row.WorktypeId > 0 ? row.WorktypeId : (emp?.WorktypeId ?? 0), + EmployeeId = row.EmployeeId, + Banci = dto.Banci, + IsXuegong = row.IsXuegong, + HoursStandard = row.HoursStandard, + HoursCq = CalcHoursCq(items), + HoursQj = Sum(items, CqQingjia), + HoursLd = Sum(items, CqLiudong), + HoursJb = Sum(items, CqJiaban), + HoursDb = Sum(items, CqDaiban), + HoursWp = Sum(items, CqWaipai), + HoursKg = Sum(items, CqKuanggong), + HoursGs = Sum(items, CqGongshang), + CsYeban = row.CsYeban, + Memo = row.Memo ?? "", + }; + await _cqdjRepo.AddAsync(master); + + var entities = items.Select(it => new CqdjItem + { + CqdjId = master.Id, + Date = day, + DepId = it.DepId > 0 ? it.DepId : (row.DepId > 0 ? row.DepId : dto.DepId), + WorktypeId = it.WorktypeId > 0 ? it.WorktypeId : master.WorktypeId, + EmployeeId = row.EmployeeId, + Banci = dto.Banci, + TypeCq = it.TypeCq, + TypeQj = it.TypeQj, + Hours = it.Hours, + Jizu = it.Jizu ?? "", + CtStart = it.CtStart ?? "", + CtEnd = it.CtEnd ?? "", + DangcheNum = it.DangcheNum, + Memo = it.Memo ?? "", + }).ToList(); + await _itemRepo.AddRangeAsync(entities); + count++; + } + + return count; + } + + // ==================== 删除 ==================== + + public async Task DeleteRegisterAsync(DateTime date, long depId, long worktypeId, int banci) + { + if (depId <= 0) throw new ServiceException("请选择部门"); + var day = date.Date; + var nextDay = day.AddDays(1); + var old = await _cqdjRepo.FindAsync(x => x.Date >= day && x.Date < nextDay + && x.DepId == depId && x.Banci == banci); + if (worktypeId > 0) old = old.Where(x => x.WorktypeId == worktypeId).ToList(); + if (old.Count == 0) return 0; + var oldIds = old.Select(x => x.Id).ToList(); + var oldItems = await _itemRepo.FindAsync(x => oldIds.Contains(x.CqdjId)); + if (oldItems.Count > 0) await _itemRepo.DeleteRangeAsync(oldItems); + await _cqdjRepo.DeleteRangeAsync(old); + return old.Count; + } + + // ==================== 下拉 ==================== + + public async Task> GetEmployeesAsync(long depId, long worktypeId, int banci) + { + if (depId <= 0) return new List(); + var depts = (await _deptRepo.GetAllAsync()).ToDictionary(d => d.Id, d => d.Name); + var wts = (await _wtRepo.GetAllAsync()).ToDictionary(w => w.Id, w => w.WorkName); + var emps = await LoadEmployeesAsync(depId, worktypeId, banci); + return emps.Select(e => new CqdjEmpOptionDto + { + Id = e.Id, + Code = e.Code ?? "", + Name = e.Name ?? "", + DepId = e.DepId, + DepName = depts.TryGetValue(e.DepId, out var dn) ? dn : "", + WorktypeId = e.WorktypeId, + WorktypeName = wts.TryGetValue(e.WorktypeId, out var wn) ? wn : "", + IsXuegong = !e.IsZhuanzheng, + }).ToList(); + } + + // ==================== 考勤统计(CQTJ) ==================== + + public async Task> GetCqtjPagedAsync(int year, int month, long depId, long worktypeId, + string like, int pageIndex, int pageSize) + { + var dicts = await LoadNameDictsAsync(); + var all = (await _cqtjRepo.GetAllAsync()) + .Where(x => x.Year == year && x.Month == month); + if (depId > 0) all = all.Where(x => x.DepId == depId); + if (worktypeId > 0) all = all.Where(x => x.WorktypeId == worktypeId); + + var rows = all.Select(x => ToCqtjDto(x, dicts)).ToList(); + if (!string.IsNullOrWhiteSpace(like)) + { + var key = like.Trim(); + rows = rows.Where(x => (x.EmployeeName ?? "").Contains(key) || (x.EmployeeCode ?? "").Contains(key)).ToList(); + } + + var total = rows.Count; + var items = rows + .OrderBy(x => x.DepId).ThenBy(x => x.WorktypeId).ThenBy(x => x.EmployeeCode) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + return new PageResult { Items = items, Total = total, PageIndex = pageIndex, PageSize = pageSize }; + } + + public async Task GetCqtjForEditAsync(int year, int month, long depId) + { + if (depId <= 0) throw new ServiceException("请选择部门"); + var dicts = await LoadNameDictsAsync(); + var exist = (await _cqtjRepo.GetAllAsync()) + .Where(x => x.Year == year && x.Month == month && x.DepId == depId) + .OrderBy(x => x.WorktypeId).ThenBy(x => x.EmployeeId).ToList(); + + var confZq = await _confZqRepo.FirstOrDefaultAsync(x => x.Year == year && x.Month == month); + var view = new CqtjViewDto + { + Year = year, Month = month, DepId = depId, + DepName = dicts.Depts.TryGetValue(depId, out var dn) ? dn : "", + QsQuanqinDays = confZq?.QsQuanqinDays ?? 0, + Exists = exist.Count > 0 + }; + view.Rows = exist.Count > 0 + ? exist.Select(x => ToCqtjDto(x, dicts)).ToList() + : await CalcCqtjAsync(year, month, depId); + return view; + } + + public async Task SaveCqtjAsync(CqtjSaveDto dto) + { + if (dto == null) throw new ServiceException("保存内容为空"); + if (dto.DepId <= 0) throw new ServiceException("请选择部门"); + var zq = await _zhangqiRepo.FirstOrDefaultAsync(x => x.Year == dto.Year && x.Month == dto.Month); + if (zq != null && zq.IsZhaZhang) throw new ServiceException("账期已扎帐,无法修改!"); + + var old = (await _cqtjRepo.GetAllAsync()) + .Where(x => x.Year == dto.Year && x.Month == dto.Month && x.DepId == dto.DepId).ToList(); + if (old.Count > 0) await _cqtjRepo.DeleteRangeAsync(old); + + var list = (dto.Rows ?? new List()).Select(x => new Cqtj + { + Year = dto.Year, Month = dto.Month, + DepId = dto.DepId, WorktypeId = x.WorktypeId, EmployeeId = x.EmployeeId, + DaysCqStandard = x.DaysCqStandard, DaysCqActual = x.DaysCqActual, CqTianci = x.CqTianci, + HoursCq = x.HoursCq, HoursQj = x.HoursQj, HoursLd = x.HoursLd, HoursJb = x.HoursJb, + HoursDb = x.HoursDb, HoursWp = x.HoursWp, HoursKg = x.HoursKg, CsYeban = x.CsYeban, + HoursQjShijia = x.HoursQjShijia, HoursQjBj = x.HoursQjBj, HoursQjHj = x.HoursQjHj, + HoursQjCj = x.HoursQjCj, HoursQjLx = x.HoursQjLx, HoursQjSangjia = x.HoursQjSangjia, + HoursQjGj = x.HoursQjGj, HoursQjNj = x.HoursQjNj, HoursQjTx = x.HoursQjTx, + HoursXgCq = x.HoursXgCq, HoursXgJiaban = x.HoursXgJiaban, + HoursFadingjiaqi = x.HoursFadingjiaqi, HoursQjQqmc = x.HoursQjQqmc, HoursQjQqcal = x.HoursQjQqcal, + JineQuanqin = x.JineQuanqin, JineJb = x.JineJb, JineLiudong = x.JineLiudong, + JineDaiban = x.JineDaiban, JineYb = x.JineYb, JineKgKk = x.JineKgKk, + JineJqBzHj = x.JineJqBzHj, JineJqBzBj = x.JineJqBzBj, JineJqBzLx = x.JineJqBzLx, + JineJqBzSangjia = x.JineJqBzSangjia, JineJqBzFading = x.JineJqBzFading, + JineJqBzTxnj = x.JineJqBzTxnj, JineJqBz = x.JineJqBz, + Memo = x.Memo ?? "" + }).ToList(); + await _cqtjRepo.AddRangeAsync(list); + return list.Count; + } + + public async Task DeleteCqtjAsync(int year, int month, long depId) + { + if (depId <= 0) throw new ServiceException("请选择部门"); + var zq = await _zhangqiRepo.FirstOrDefaultAsync(x => x.Year == year && x.Month == month); + if (zq != null && zq.IsZhaZhang) throw new ServiceException("账期已扎帐,无法修改!"); + var old = (await _cqtjRepo.GetAllAsync()) + .Where(x => x.Year == year && x.Month == month && x.DepId == depId).ToList(); + if (old.Count == 0) return 0; + await _cqtjRepo.DeleteRangeAsync(old); + return old.Count; + } + + public async Task GenerateCqtjAllAsync(int year, int month) + { + var zq = await _zhangqiRepo.FirstOrDefaultAsync(x => x.Year == year && x.Month == month); + if (zq != null && zq.IsZhaZhang) throw new ServiceException("账期已扎帐,无法生成!"); + var confZq = await _confZqRepo.FirstOrDefaultAsync(x => x.Year == year && x.Month == month); + if (confZq == null || confZq.QsQuanqinDays <= 0) + throw new ServiceException("请先录入薪资配置账期的前缫出勤天数!"); + + var depts = (await _deptRepo.GetAllAsync()) + .Where(d => d.IsActive && d.ManageType != Department_ManageType_Enum.公司) + .ToList(); + var saved = (await _cqtjRepo.GetAllAsync()) + .Where(x => x.Year == year && x.Month == month) + .Select(x => x.DepId).Distinct().ToHashSet(); + + var count = 0; + foreach (var d in depts) + { + if (saved.Contains(d.Id)) continue; + var rows = await CalcCqtjAsync(year, month, d.Id); + if (rows.Count == 0) continue; + await SaveCqtjAsync(new CqtjSaveDto { Year = year, Month = month, DepId = d.Id, Rows = rows }); + count++; + } + return count; + } + + public async Task DeleteCqtjAllAsync(int year, int month) + { + var zq = await _zhangqiRepo.FirstOrDefaultAsync(x => x.Year == year && x.Month == month); + if (zq != null && zq.IsZhaZhang) throw new ServiceException("账期已扎帐,无法修改!"); + var old = (await _cqtjRepo.GetAllAsync()).Where(x => x.Year == year && x.Month == month).ToList(); + if (old.Count == 0) return 0; + await _cqtjRepo.DeleteRangeAsync(old); + return old.Count; + } + + // ==================== 私有 ==================== + + private class NameDicts + { + public Dictionary Depts = new(); + public Dictionary Worktypes = new(); + public Dictionary Emps = new(); + } + + private async Task LoadNameDictsAsync() => new() + { + Depts = (await _deptRepo.GetAllAsync()).ToDictionary(d => d.Id, d => d.Name), + Worktypes = (await _wtRepo.GetAllAsync()).ToDictionary(w => w.Id, w => w.WorkName), + Emps = (await _empRepo.GetAllAsync()).ToDictionary(e => e.Id) + }; + + private static CqtjRowDto ToCqtjDto(Cqtj x, NameDicts d) => new() + { + Id = x.Id, Year = x.Year, Month = x.Month, + DepId = x.DepId, DepName = d.Depts.TryGetValue(x.DepId, out var dn) ? dn : "", + WorktypeId = x.WorktypeId, WorktypeName = d.Worktypes.TryGetValue(x.WorktypeId, out var wn) ? wn : "", + EmployeeId = x.EmployeeId, + EmployeeCode = d.Emps.TryGetValue(x.EmployeeId, out var e) ? (e.Code ?? "") : "", + EmployeeName = d.Emps.TryGetValue(x.EmployeeId, out var e2) ? (e2.Name ?? "") : "", + DaysCqStandard = x.DaysCqStandard, DaysCqActual = x.DaysCqActual, CqTianci = x.CqTianci, + HoursCq = x.HoursCq, HoursQj = x.HoursQj, HoursLd = x.HoursLd, HoursJb = x.HoursJb, + HoursDb = x.HoursDb, HoursWp = x.HoursWp, HoursKg = x.HoursKg, CsYeban = x.CsYeban, + HoursQjShijia = x.HoursQjShijia, HoursQjBj = x.HoursQjBj, HoursQjHj = x.HoursQjHj, + HoursQjCj = x.HoursQjCj, HoursQjLx = x.HoursQjLx, HoursQjSangjia = x.HoursQjSangjia, + HoursQjGj = x.HoursQjGj, HoursQjNj = x.HoursQjNj, HoursQjTx = x.HoursQjTx, + HoursXgCq = x.HoursXgCq, HoursXgJiaban = x.HoursXgJiaban, + HoursFadingjiaqi = x.HoursFadingjiaqi, HoursQjQqmc = x.HoursQjQqmc, HoursQjQqcal = x.HoursQjQqcal, + JineQuanqin = x.JineQuanqin, JineJb = x.JineJb, JineLiudong = x.JineLiudong, + JineDaiban = x.JineDaiban, JineYb = x.JineYb, JineKgKk = x.JineKgKk, + JineJqBzHj = x.JineJqBzHj, JineJqBzBj = x.JineJqBzBj, JineJqBzLx = x.JineJqBzLx, + JineJqBzSangjia = x.JineJqBzSangjia, JineJqBzFading = x.JineJqBzFading, + JineJqBzTxnj = x.JineJqBzTxnj, JineJqBz = x.JineJqBz, + Memo = x.Memo ?? "" + }; + + /// 账期(取不到时按自然月兜底) + private async Task<(DateTime Start, DateTime End)> GetZhangqiRangeAsync(int year, int month) + { + var zq = await _zhangqiRepo.FirstOrDefaultAsync(x => x.Year == year && x.Month == month); + if (zq != null) return (zq.StartDate.Date, zq.EndDate.Date.AddDays(1).AddSeconds(-1)); + var start = new DateTime(year, month, 1); + return (start, start.AddMonths(1).AddSeconds(-1)); + } + + /// + /// 按账期汇总某部门的考勤统计(对齐 Page_Cqtj.CqtjViews)。 + /// 全勤奖涉及「该职工在全部部门 / 工种」的合计出勤 / 请假 / 旷工,故登记数据按账期全量加载。 + /// + private async Task> CalcCqtjAsync(int year, int month, long depId) + { + var (start, end) = await GetZhangqiRangeAsync(year, month); + var confZq = await _confZqRepo.FirstOrDefaultAsync(x => x.Year == year && x.Month == month); + var qsDays = confZq?.QsQuanqinDays ?? 0; + if (qsDays <= 0) throw new ServiceException("请先在【薪资配置账期】功能中设置当月前缫出勤天数!"); + + var dicts = await LoadNameDictsAsync(); + var cqdjAll = await _cqdjRepo.FindAsync(x => x.Date >= start && x.Date <= end); + if (cqdjAll.Count == 0) return new List(); + var ids = cqdjAll.Select(x => x.Id).ToList(); + var itemsAll = await _itemRepo.FindAsync(x => ids.Contains(x.CqdjId)); + + var confGzAll = await _confGzRepo.GetAllAsync(); + var confEmpAll = await _confEmpRepo.GetAllAsync(); + var gwsrkhAll = (await _gwsrkhRepo.GetAllAsync()).Where(x => x.Year == year && x.Month == month).ToList(); + var deptsAll = await _deptRepo.GetAllAsync(); + + // 每职工全部门合计(全勤奖口径) + var cqHoursByEmp = cqdjAll.GroupBy(x => x.EmployeeId).Select(g => new + { + EmployeeId = g.Key, + CqHours = g.Sum(x => x.HoursCq), + JiabanHours = g.Sum(x => x.HoursJb), + QjHours = g.Sum(x => x.HoursQj), + KgHours = g.Sum(x => x.HoursKg) + }).ToDictionary(x => x.EmployeeId); + + var groups = cqdjAll.Where(x => x.DepId == depId) + .GroupBy(x => new { x.EmployeeId, x.DepId, x.WorktypeId }) + .Select(g => new + { + g.Key.EmployeeId, g.Key.DepId, g.Key.WorktypeId, + HoursCq = g.Sum(x => x.HoursCq), + HoursQj = g.Sum(x => x.HoursQj), + HoursLd = g.Sum(x => x.HoursLd), + HoursJb = g.Sum(x => x.HoursJb), + HoursDb = g.Sum(x => x.HoursDb), + HoursWp = g.Sum(x => x.HoursWp), + HoursKg = g.Sum(x => x.HoursKg), + CsYeban = g.Sum(x => x.CsYeban), + List = g.ToList() + }).ToList(); + + var result = new List(); + foreach (var item in groups) + { + if (item.HoursCq + item.HoursJb + item.HoursQj + item.HoursLd + item.HoursDb + + item.HoursKg + item.CsYeban == 0) continue; + + var empDepWtIds = item.List.Select(x => x.Id).ToHashSet(); + var depItems = itemsAll.Where(x => empDepWtIds.Contains(x.CqdjId)).ToList(); + var confEmp = confEmpAll.FirstOrDefault(x => x.EmployeeId == item.EmployeeId && x.WorktypeId == item.WorktypeId); + var confGz = confGzAll.FirstOrDefault(x => x.WorkTypeId == item.WorktypeId); + dicts.Emps.TryGetValue(item.EmployeeId, out var emp); + + var row = new CqtjRowDto + { + Year = year, Month = month, + DepId = item.DepId, DepName = dicts.Depts.TryGetValue(item.DepId, out var dn) ? dn : "", + WorktypeId = item.WorktypeId, WorktypeName = dicts.Worktypes.TryGetValue(item.WorktypeId, out var wn) ? wn : "", + EmployeeId = item.EmployeeId, + EmployeeCode = emp?.Code ?? "", EmployeeName = emp?.Name ?? "", + HoursCq = item.HoursCq, HoursQj = item.HoursQj, HoursLd = item.HoursLd, + HoursJb = item.HoursJb, HoursDb = item.HoursDb, HoursWp = item.HoursWp, + HoursKg = item.HoursKg, CsYeban = item.CsYeban + }; + + if (confGz == null) { result.Add(row); continue; } + + // 学工时长 + var xgList = item.List.Where(x => x.IsXuegong).ToList(); + var xgIds = xgList.Select(x => x.Id).ToHashSet(); + var xgItems = itemsAll.Where(x => xgIds.Contains(x.CqdjId)).ToList(); + row.HoursXgCq = xgList.Sum(x => x.HoursCq); + row.HoursXgJiaban = xgList.Sum(x => x.HoursJb); + + // 法定假 + var fadingBzHours = confEmp?.HoursFadingBz ?? 0; + var fadingGzHours = confEmp?.HoursFadingGz ?? 0; + var jineFadingBz = jisuanFadingBz(fadingBzHours, fadingGzHours, confGz); + row.HoursFadingjiaqi = R2(fadingBzHours + fadingGzHours); + + // 假期补助 + var bz = jiaqiBuzhuJisuan(confGzAll, depItems, jineFadingBz); + row.HoursQjShijia = bz.HoursShijia; row.HoursQjBj = bz.HoursBj; row.HoursQjHj = bz.HoursHj; + row.HoursQjCj = bz.HoursCj; row.HoursQjLx = bz.HoursLx; row.HoursQjSangjia = bz.HoursSangjia; + row.HoursQjGj = bz.HoursGj; row.HoursQjNj = bz.HoursNj; row.HoursQjTx = bz.HoursTx; + row.JineJqBzHj = bz.JineHj; row.JineJqBzBj = bz.JineBj; row.JineJqBzLx = bz.JineLx; + row.JineJqBzSangjia = bz.JineSangjia; row.JineJqBzFading = jineFadingBz; + row.JineJqBzTxnj = bz.JineTxnj; row.JineJqBz = bz.JineTotal; + + var daixinQjHours = bz.HoursNj + bz.HoursTx + bz.HoursGj + bz.HoursLx + bz.HoursHj + bz.HoursSangjia + bz.HoursCj; + var empSum = cqHoursByEmp.TryGetValue(item.EmployeeId, out var s) ? s : null; + var sumCqHours = empSum?.CqHours ?? 0; + var sumJiabanHours = empSum?.JiabanHours ?? 0; + var sumQjHours = empSum?.QjHours ?? 0; + var sumKgHours = empSum?.KgHours ?? 0; + + // 加班免扣请假 / 全勤计算请假 + row.HoursQjQqmc = jisuanJiabanMkHours(confGz, sumJiabanHours, item.HoursQj, qsDays, item.HoursCq); + var sumDaixinHours = sumDaixinHoursJisuan(itemsAll, item.EmployeeId); + var qqJisuanQjHours = sumQjHours - row.HoursQjQqmc - sumDaixinHours; + var depQj = item.HoursQj - row.HoursQjQqmc - daixinQjHours; + row.HoursQjQqcal = depQj < 0 ? 0 : depQj; + + // 全勤奖 + var hoursPerday = confGz.HoursPerday; + if (sumCqHours <= 15 * hoursPerday && emp != null && emp.WorkStatus == Employee_WorkStatus_Enum.离职) + row.JineQuanqin = 0; + else if (confGz.JineQuanqin > 0) + row.JineQuanqin = quanqin(confGz, sumCqHours, sumQjHours, item.HoursCq, item.HoursJb, + row.HoursQjQqcal, qqJisuanQjHours, daixinQjHours, confEmp, sumDaixinHours, sumCqHours, + sumKgHours, emp, start, end, qsDays, + deptsAll.FirstOrDefault(d => d.Id == item.DepId), sumJiabanHours); + + // 加班费 / 流动 / 代班 / 夜班 / 旷工 + var gwsrkh = gwsrkhAll.FirstOrDefault(x => x.EmployeeId == item.EmployeeId); + row.JineJb = jisuanJianbanJine(depItems, confGzAll, xgItems, gwsrkh?.KhJieguo); + row.JineLiudong = liudongJineJisuan(depItems, confGz); + row.JineDaiban = daibanJineJisuan(depItems, confGzAll); + row.JineYb = R2(confGz.JineYeban * item.CsYeban); + row.JineKgKk = R2(confGz.JiaqiJineKuangongKk * item.HoursKg); + + row.CqTianci = jisuanCqTianci(item.List); + row.DaysCqStandard = confGz.QqDaysStandard; + row.DaysCqActual = hoursPerday == 0 ? 0 : R2(item.HoursCq / hoursPerday); + + result.Add(row); + } + return result; + } + + // ---------- 口径函数(对齐 Page_Cqtj 的同名方法) ---------- + + private static decimal R2(decimal v) => Math.Round(v, 2, MidpointRounding.AwayFromZero); + + private class JiaqiBuzhu + { + public decimal HoursShijia, HoursBj, HoursHj, HoursCj, HoursLx, HoursSangjia, HoursGj, HoursNj, HoursTx; + public decimal JineHj, JineBj, JineLx, JineSangjia, JineTxnj, JineTotal; + } + + /// 假期补助 + 请假分项时长(按明细的 请假类型 × 出勤工种 分组计算) + private static JiaqiBuzhu jiaqiBuzhuJisuan(List confGzAll, List items, decimal jineFadingBz) + { + var r = new JiaqiBuzhu(); + var groups = items.GroupBy(x => new { x.TypeQj, x.WorktypeId }) + .Select(g => new { Hours = g.Sum(x => x.Hours), g.Key.TypeQj, g.Key.WorktypeId }).ToList(); + foreach (var g in groups) + { + var cfg = confGzAll.FirstOrDefault(x => x.WorkTypeId == g.WorktypeId); + switch (g.TypeQj) + { + case 2: // 病假 + r.HoursBj = g.Hours; + r.JineBj += R2((cfg?.JiaqiJineBingjiaBz ?? 0) * g.Hours); break; + case 10: // 轮休 + r.HoursLx = g.Hours; + r.JineLx += R2((cfg?.JiaqiJineLunxiuBz ?? 0) * g.Hours); break; + case 4: // 婚假 + r.HoursHj = g.Hours; + r.JineHj += R2((cfg?.JiaqiJineHunjiaBz ?? 0) * g.Hours); break; + case 5: // 丧假 + r.HoursSangjia = g.Hours; + r.JineSangjia += R2((cfg?.JiaqiJineSangjiaBz ?? 0) * g.Hours); break; + case 3: // 产假 + r.HoursCj = g.Hours; break; + case 1: // 事假 + r.HoursShijia = g.Hours; break; + case 7: // 年假 + r.HoursNj = g.Hours; + r.JineTxnj += R2((cfg?.Jiaqi_Jine_NianjiaBz ?? 0) * g.Hours); break; + case 6: // 公假 + r.HoursGj = g.Hours; break; + case 9: // 调休 + r.HoursTx = g.Hours; + r.JineTxnj += R2((cfg?.Jiaqi_Jine_NianjiaBz ?? 0) * g.Hours); break; + } + } + r.JineTotal = r.JineHj + r.JineBj + r.JineLx + r.JineSangjia + jineFadingBz + r.JineTxnj; + return r; + } + + /// 加班免扣请假时长:满勤且加班达到门槛时免扣固定时长 + private static decimal jisuanJiabanMkHours(ConfGz cfg, decimal sumJiabanHours, decimal hoursQj, + decimal qsDays, decimal depCqHours) + { + var manqinHours = qsDays * cfg.HoursPerday; + if (depCqHours + sumJiabanHours >= manqinHours && sumJiabanHours >= cfg.QqHoursDkMinjiaban && sumJiabanHours != 0) + return cfg.QqHoursDkQingjia; + return 0; + } + + /// 带薪请假时长合计(跨部门,按该职工全部明细) + private static decimal sumDaixinHoursJisuan(List items, long employeeId) + => items.Where(x => x.EmployeeId == employeeId && x.TypeQj is 10 or 7 or 6 or 9 or 4 or 3 or 5) + .Sum(x => x.Hours); + + /// 加班费(含学工加班:学工岗位薪资折算) + private static decimal jisuanJianbanJine(List items, List confGzAll, + List xgItems, string khJieguo) + { + decimal jine = 0; + var groups = items.Where(x => x.TypeCq == CqJiaban) + .GroupBy(x => x.WorktypeId) + .Select(g => new { Hours = g.Sum(x => x.Hours), WorktypeId = g.Key }).ToList(); + foreach (var g in groups) + { + var cfg = confGzAll.FirstOrDefault(x => x.WorkTypeId == g.WorktypeId); + decimal price = cfg?.JiabanJineBz ?? 0; + if (!string.IsNullOrWhiteSpace(khJieguo) && cfg != null) + { + price = khJieguo switch + { + "欠合格" => cfg.Jiaban_GwsrkhJineQianhege, + "期望" => cfg.Jiaban_GwsrkhJineQiwang, + "合格" => cfg.Jiaban_GwsrkhJineHege, + "胜任" => cfg.Jiaban_GwsrkhJineShengren, + "超胜任" => cfg.Jiaban_GwsrkhJineChaoshengren, + _ => cfg.JiabanJineBz + }; + } + jine += g.Hours * price; + } + + var xgGroups = xgItems.Where(x => x.TypeCq == CqJiaban) + .GroupBy(x => x.WorktypeId) + .Select(g => new { Hours = g.Sum(x => x.Hours), WorktypeId = g.Key }).ToList(); + foreach (var g in xgGroups) + { + var cfg = confGzAll.FirstOrDefault(x => x.WorkTypeId == g.WorktypeId); + if (cfg == null) continue; + var std = cfg.QqDaysStandard * cfg.HoursPerday; + if (std == 0) continue; + jine += cfg.XgJineGangwei / std * g.Hours; + } + return R2(jine); + } + + /// 流动工资:工种配置的固定流动工资(元/小时)× 流动时长 + private static decimal liudongJineJisuan(List items, ConfGz cfg) + { + decimal jine = 0; + var groups = items.Where(x => x.TypeCq == CqLiudong) + .GroupBy(x => new { x.DepId, x.WorktypeId }) + .Select(g => new { Hours = g.Sum(x => x.Hours) }).ToList(); + foreach (var g in groups) jine += R2(cfg.LiudongJineGuding * g.Hours); + return R2(jine); + } + + /// 代班工资:代班所在工种岗位薪资 / 标准出勤时长 × 代班时长 + private static decimal daibanJineJisuan(List items, List confGzAll) + { + decimal jine = 0; + var groups = items.Where(x => x.TypeCq == CqDaiban) + .GroupBy(x => new { x.DepId, x.WorktypeId }) + .Select(g => new { Hours = g.Sum(x => x.Hours), g.Key.WorktypeId }).ToList(); + foreach (var g in groups) + { + var cfg = confGzAll.FirstOrDefault(x => x.WorkTypeId == g.WorktypeId); + if (cfg == null) continue; + var std = cfg.QqDaysStandard * cfg.HoursPerday; + if (std == 0) continue; + jine += cfg.JineGangwei / std * g.Hours; + } + return R2(jine); + } + + /// 出勤天次:(出勤 + 加班) > 0 的天数 + private static decimal jisuanCqTianci(List list) + => list.Count(x => x.HoursCq + x.HoursJb > 0); + + /// 法定假补助:补助小时 × 补助单价 + 工资小时 × 工资单价 + private static decimal jisuanFadingBz(decimal hoursFadingBz, decimal hoursFadingGz, ConfGz cfg) + { + if (hoursFadingBz + hoursFadingGz <= 0) return 0; + return R2(hoursFadingBz * cfg.JiaqiJineFadingBz + hoursFadingGz * cfg.JiaqiJineFadingGz); + } + + /// + /// 全勤奖(对齐 Page_Cqtj.quanqin)。 + /// sumCqHours / sumQjHours / sumKgHours 为该职工全部门合计;depCqHours / depHoursQj 为本部门。 + /// + private decimal quanqin(ConfGz cfg, decimal sumCqHours, decimal sumQjHours, decimal depCqHours, decimal hoursJb, + decimal depHoursQj, decimal qqJisuanQjHours, decimal daixinQjHours, ConfEmployee confEmp, + decimal sumDaixinHours, decimal sumCqHours2, decimal sumKgHours, SysEmp.Employee emp, + DateTime zqStart, DateTime zqEnd, decimal qsDays, SysDept.Department dep, decimal sumJiabanHours) + { + var hoursPerday = cfg.HoursPerday; + var jineQuanqin = confEmp != null && confEmp.IsXuegong ? cfg.XgJineQuanqin : cfg.JineQuanqin; + var qqHoursMaxQj = cfg.QqHoursMaxQj; + var standardCqHours = cfg.QqDaysStandard * hoursPerday; + if (hoursPerday <= 0) return 0; + + var ruzhi = emp?.RuzhiDate; + var lizhi = emp?.LizhiDate; + bool InPeriod(DateTime? d) => d.HasValue && d.Value.Date >= zqStart.Date && d.Value.Date <= zqEnd.Date; + + // 1、总旷工超过半天(含半天)不计全勤 + if (sumKgHours >= hoursPerday / 2) return 0; + // 2、出勤总时长小于等于 3 天不计全勤 + if (sumCqHours <= hoursPerday * 3) return 0; + + if (sumCqHours != depCqHours) // 存在多部门 / 多工种出勤 + { + var shijiCqDays = (sumCqHours + sumDaixinHours) / hoursPerday; + if (InPeriod(lizhi)) + return standardCqHours == 0 ? 0 : R2(jineQuanqin / standardCqHours * depCqHours); + + if (InPeriod(ruzhi)) + { + if (shijiCqDays >= qsDays) return sumCqHours == 0 ? 0 : jineQuanqin / sumCqHours * depCqHours; + if (shijiCqDays < 3) return 0; + return standardCqHours == 0 ? 0 : R2(jineQuanqin / standardCqHours * depCqHours); + } + + var chushi = (sumCqHours + sumDaixinHours) / hoursPerday >= 15 + ? (sumCqHours == 0 ? 0 : jineQuanqin / sumCqHours * depCqHours) + : (standardCqHours == 0 ? 0 : jineQuanqin / standardCqHours * depCqHours); + + if (qqJisuanQjHours >= qqHoursMaxQj) return 0; + return qqHoursMaxQj == 0 ? 0 : R2(chushi - (chushi / qqHoursMaxQj) * qqJisuanQjHours); + } + + var shijiDays = (sumCqHours + sumDaixinHours) / hoursPerday; + if (InPeriod(lizhi)) + return standardCqHours == 0 ? 0 : R2(jineQuanqin / standardCqHours * sumCqHours); + + if (InPeriod(ruzhi)) + { + if (shijiDays >= qsDays) return jineQuanqin; + if (shijiDays < 3) return 0; + return standardCqHours == 0 ? 0 : R2(jineQuanqin / standardCqHours * sumCqHours); + } + + var chushi2 = (sumCqHours + daixinQjHours) / hoursPerday >= 15 + ? jineQuanqin + : (standardCqHours == 0 ? 0 : jineQuanqin / standardCqHours * sumCqHours); + + // 设备办全职:出勤含加班达到前缫出勤天数且不低于标准全勤时长时,请假不扣全勤 + if (dep != null && dep.DutyType == Department_DutyType_Enum.设备办) + { + var cqhours = sumCqHours + daixinQjHours + sumJiabanHours; + if (cqhours / hoursPerday >= qsDays && cqhours >= standardCqHours) return R2(chushi2); + } + + if (qqJisuanQjHours >= qqHoursMaxQj) return 0; + return qqHoursMaxQj == 0 ? 0 : R2(chushi2 - (chushi2 / qqHoursMaxQj) * depHoursQj); + } + + /// 本部门在职职工(按工种 / 班次过滤,排除已离职、未入职、工种为 0) + private async Task> LoadEmployeesAsync(long depId, long worktypeId, int banci) + { + var all = await _empRepo.GetAllAsync(); + return all.Where(e => e.DepId == depId + && e.WorktypeId != 0 + && e.WorkStatus != Employee_WorkStatus_Enum.离职 + && (worktypeId <= 0 || e.WorktypeId == worktypeId) + && (banci <= 0 || e.Banci == null || (int)e.Banci == banci)) + .OrderBy(e => e.Code).ThenBy(e => e.Name) + .ToList(); + } + + /// 出勤时长 = 正常 + 流动 + 代班 + 外派 + 工伤(不含加班) + private static decimal CalcHoursCq(List items) + => items.Where(x => x.TypeCq == CqZhengchang || x.TypeCq == CqLiudong || x.TypeCq == CqDaiban + || x.TypeCq == CqWaipai || x.TypeCq == CqGongshang) + .Sum(x => x.Hours); + + private static decimal Sum(List items, int typeCq) + => items.Where(x => x.TypeCq == typeCq).Sum(x => x.Hours); + + private static CqdjItemDto ToItemDto(CqdjItem x) => new() + { + Id = x.Id, + DepId = x.DepId, + WorktypeId = x.WorktypeId, + TypeCq = x.TypeCq, + TypeQj = x.TypeQj, + Hours = x.Hours, + Jizu = x.Jizu ?? "", + CtStart = x.CtStart ?? "", + CtEnd = x.CtEnd ?? "", + DangcheNum = x.DangcheNum, + Memo = x.Memo ?? "", + }; +} diff --git a/backend/F9WebAiTest.Application/Services/Prod/LsjlService.cs b/backend/F9WebAiTest.Application/Services/Prod/LsjlService.cs index c34cf1e..cf83a3d 100644 --- a/backend/F9WebAiTest.Application/Services/Prod/LsjlService.cs +++ b/backend/F9WebAiTest.Application/Services/Prod/LsjlService.cs @@ -96,6 +96,7 @@ public class LsjlService : ILsjlService e.LuosiTime = d.LuosiTime; e.Yanse = d.Yanse; e.Huishu = d.Huishu; e.ChetaiStart = d.ChetaiStart; e.ChetaiEnd = d.ChetaiEnd; e.Chetaishu = d.Chetaishu; e.Isbanche = d.Isbanche; e.Hours = d.Hours; e.Guige = d.Guige; e.Chesu = d.Chesu; + e.Tongshu = d.Tongshu; e.Meitongjianliang = d.Meitongjianliang; return e; } @@ -110,7 +111,8 @@ public class LsjlService : ILsjlService LuosiTime = e.LuosiTime, Yanse = e.Yanse, Huishu = e.Huishu, ChetaiStart = e.ChetaiStart, ChetaiEnd = e.ChetaiEnd, Chetaishu = e.Chetaishu, Isbanche = e.Isbanche, IsbancheName = e.Isbanche ? "半车" : "整车", - Hours = e.Hours, Guige = e.Guige, Chesu = e.Chesu + Hours = e.Hours, Tongshu = e.Tongshu, Meitongjianliang = e.Meitongjianliang, + Guige = e.Guige, Chesu = e.Chesu }; } @@ -123,7 +125,8 @@ public class LsjlService : ILsjlService LuosiTime = e.LuosiTime, Yanse = e.Yanse, Huishu = e.Huishu, ChetaiStart = e.ChetaiStart, ChetaiEnd = e.ChetaiEnd, Chetaishu = e.Chetaishu, Isbanche = e.Isbanche, IsbancheName = e.Isbanche ? "半车" : "整车", - Hours = e.Hours, Guige = e.Guige, Chesu = e.Chesu + Hours = e.Hours, Tongshu = e.Tongshu, Meitongjianliang = e.Meitongjianliang, + Guige = e.Guige, Chesu = e.Chesu }; } } diff --git a/backend/F9WebAiTest.Application/Services/Prod/SpjcService.cs b/backend/F9WebAiTest.Application/Services/Prod/SpjcService.cs new file mode 100644 index 0000000..4e76ef1 --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Prod/SpjcService.cs @@ -0,0 +1,188 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities.Base; +using F9WebAiTest.Domain.Entities.Prod; +using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments; + +namespace F9WebAiTest.Application.Services.Prod; + +/// 丝片检查:按「日期 + 车间 + 工艺庄口 + 规格」记录前缫/后缫疵点片数 +public interface ISpjcService +{ + Task> GetPagedAsync(int pageIndex, int pageSize, DateTime? date, long? deptId, string like); + Task GetForEditAsync(long id); + Task SaveAsync(SpjcSaveDto dto); + Task DeleteAsync(long id); +} + +public class SpjcService : ISpjcService +{ + private readonly IRepository _spjcRepo; + private readonly IRepository _deptRepo; + private readonly IRepository _zkRepo; + + public SpjcService( + IRepository spjcRepo, + IRepository deptRepo, + IRepository zkRepo) + { + _spjcRepo = spjcRepo; + _deptRepo = deptRepo; + _zkRepo = zkRepo; + } + + public async Task> GetPagedAsync(int pageIndex, int pageSize, DateTime? date, long? deptId, string like) + { + var all = await _spjcRepo.GetAllAsync(); + var q = all.AsQueryable(); + if (date.HasValue) q = q.Where(x => x.Date.Date == date.Value.Date); + if (deptId.HasValue) q = q.Where(x => x.DeptId == deptId.Value); + + var depts = await _deptRepo.GetAllAsync(); + var deptDict = depts.ToDictionary(d => d.Id, d => d.Name); + var zks = await _zkRepo.GetAllAsync(); + var zkDict = zks.ToDictionary(z => z.Id, z => z.ZhuangKouName); + + if (!string.IsNullOrWhiteSpace(like)) + { + var kwDeptIds = depts.Where(d => d.Name.Contains(like)).Select(d => d.Id).ToHashSet(); + var kwZkIds = zks.Where(z => z.ZhuangKouName.Contains(like)).Select(z => z.Id).ToHashSet(); + q = q.Where(x => kwDeptIds.Contains(x.DeptId) || kwZkIds.Contains(x.GyzkId) || x.Guige.Contains(like)); + } + + var total = q.LongCount(); + var items = q.OrderByDescending(x => x.Date).ThenByDescending(x => x.Id) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + return new PageResult + { + Items = items.Select(x => ToListDto(x, deptDict, zkDict)).ToList(), + Total = total, PageIndex = pageIndex, PageSize = pageSize + }; + } + + public async Task GetForEditAsync(long id) + { + var e = await _spjcRepo.GetByIdAsync(id); + if (e == null) throw new ServiceException("丝片检查不存在或已删除", 404); + + var dept = await _deptRepo.GetByIdAsync(e.DeptId); + var zk = await _zkRepo.GetByIdAsync(e.GyzkId); + + return new SpjcSaveDto + { + Id = e.Id, + Date = e.Date, + DeptId = e.DeptId, + DeptName = dept?.Name ?? "", + GyzkId = e.GyzkId, + GyzkName = zk?.ZhuangKouName ?? "", + Guige = e.Guige, + Pianshu = e.Pianshu, + PianshuXuegong = e.PianshuXuegong, + QsJiahua = e.QsJiahua, QsYouwu = e.QsYouwu, QsHeidian = e.QsHeidian, + QsShangsi = e.QsShangsi, QsYetiao = e.QsYetiao, QsXiaosipian = e.QsXiaosipian, QsQita = e.QsQita, + HsYouwu = e.HsYouwu, HsShangsi = e.HsShangsi, HsShuangsi = e.HsShuangsi, HsZhisi = e.HsZhisi, + HsZhantiao = e.HsZhantiao, HsYingjiao = e.HsYingjiao, HsGuojiao = e.HsGuojiao, + HsShouchu = e.HsShouchu, HsWuran = e.HsWuran, HsLiansi = e.HsLiansi, HsWdmt = e.HsWdmt, + HsMaoluan = e.HsMaoluan, HsQita = e.HsQita, + Memo = e.Memo + }; + } + + public async Task SaveAsync(SpjcSaveDto dto) + { + if (dto.DeptId == 0) throw new ServiceException("请选择车间"); + if (dto.GyzkId == 0) throw new ServiceException("请选择工艺庄口"); + if (string.IsNullOrWhiteSpace(dto.Guige)) throw new ServiceException("请选择规格"); + + var date = dto.Date.Date; + var guige = dto.Guige.Trim(); + + Spjc e; + if (dto.Id > 0) + { + e = await _spjcRepo.GetByIdAsync(dto.Id); + if (e == null) throw new ServiceException("丝片检查不存在或已删除", 404); + + var dup = await _spjcRepo.FirstOrDefaultAsync(x => x.Id != dto.Id && x.Date.Date == date + && x.DeptId == dto.DeptId && x.GyzkId == dto.GyzkId + && x.Guige == guige); + if (dup != null) throw new ServiceException("已存在相同的丝片检查!"); + } + else + { + var exists = await _spjcRepo.AnyAsync(x => x.Date.Date == date && x.DeptId == dto.DeptId + && x.GyzkId == dto.GyzkId && x.Guige == guige); + if (exists) throw new ServiceException("已存在相同的丝片检查!"); + e = new Spjc(); + await _spjcRepo.AddAsync(e); + } + + e.Date = date; + e.DeptId = dto.DeptId; + e.GyzkId = dto.GyzkId; + e.Guige = guige; + e.Pianshu = dto.Pianshu; + e.PianshuXuegong = dto.PianshuXuegong; + e.QsJiahua = dto.QsJiahua; + e.QsYouwu = dto.QsYouwu; + e.QsHeidian = dto.QsHeidian; + e.QsShangsi = dto.QsShangsi; + e.QsYetiao = dto.QsYetiao; + e.QsXiaosipian = dto.QsXiaosipian; + e.QsQita = dto.QsQita; + e.HsYouwu = dto.HsYouwu; + e.HsShangsi = dto.HsShangsi; + e.HsShuangsi = dto.HsShuangsi; + e.HsZhisi = dto.HsZhisi; + e.HsZhantiao = dto.HsZhantiao; + e.HsYingjiao = dto.HsYingjiao; + e.HsGuojiao = dto.HsGuojiao; + e.HsShouchu = dto.HsShouchu; + e.HsWuran = dto.HsWuran; + e.HsLiansi = dto.HsLiansi; + e.HsWdmt = dto.HsWdmt; + e.HsMaoluan = dto.HsMaoluan; + e.HsQita = dto.HsQita; + e.Memo = dto.Memo ?? ""; + + // 合计由服务端重算(与 F9 的 Qs_heji / Hs_heji 一致) + e.QsHeji = e.QsJiahua + e.QsYouwu + e.QsHeidian + e.QsShangsi + e.QsYetiao + e.QsXiaosipian + e.QsQita; + e.HsHeji = e.HsYouwu + e.HsShangsi + e.HsShuangsi + e.HsZhisi + e.HsZhantiao + e.HsYingjiao + + e.HsGuojiao + e.HsShouchu + e.HsWuran + e.HsLiansi + e.HsWdmt + e.HsMaoluan + e.HsQita; + + await _spjcRepo.UpdateAsync(e); + return e.Id; + } + + public async Task DeleteAsync(long id) + { + var e = await _spjcRepo.GetByIdAsync(id); + if (e == null) throw new ServiceException("丝片检查不存在或已删除", 404); + // 业务键带唯一索引,物理删除避免「删除后重建」撞索引(与煮茧日报同一策略) + await _spjcRepo.HardDeleteAsync(e); + } + + private static SpjcListItemDto ToListDto(Spjc x, Dictionary deptDict, Dictionary zkDict) => new() + { + Id = x.Id, + Date = x.Date, + DeptId = x.DeptId, + DeptName = deptDict.GetValueOrDefault(x.DeptId, ""), + GyzkId = x.GyzkId, + GyzkName = zkDict.GetValueOrDefault(x.GyzkId, ""), + Guige = x.Guige, + Pianshu = x.Pianshu, + PianshuXuegong = x.PianshuXuegong, + QsJiahua = x.QsJiahua, QsYouwu = x.QsYouwu, QsHeidian = x.QsHeidian, + QsShangsi = x.QsShangsi, QsYetiao = x.QsYetiao, QsXiaosipian = x.QsXiaosipian, + QsQita = x.QsQita, QsHeji = x.QsHeji, + HsYouwu = x.HsYouwu, HsShangsi = x.HsShangsi, HsShuangsi = x.HsShuangsi, HsZhisi = x.HsZhisi, + HsZhantiao = x.HsZhantiao, HsYingjiao = x.HsYingjiao, HsGuojiao = x.HsGuojiao, + HsShouchu = x.HsShouchu, HsWuran = x.HsWuran, HsLiansi = x.HsLiansi, HsWdmt = x.HsWdmt, + HsMaoluan = x.HsMaoluan, HsQita = x.HsQita, HsHeji = x.HsHeji, + Memo = x.Memo + }; +} diff --git a/backend/F9WebAiTest.Application/Services/Prod/SptzService.cs b/backend/F9WebAiTest.Application/Services/Prod/SptzService.cs new file mode 100644 index 0000000..5c3b63f --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Prod/SptzService.cs @@ -0,0 +1,159 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities.Prod; + +namespace F9WebAiTest.Application.Services.Prod; + +/// 丝片台帐:每日一条,记录浸丝 / 返丝 / 编丝 / 整理片数 +public interface ISptzService +{ + Task> GetPagedAsync(int pageIndex, int pageSize, DateTime dateFrom, DateTime dateTo); + Task GetForEditAsync(long id); + Task GetDefaultsAsync(DateTime date); + Task SaveAsync(SptzSaveDto dto); + Task DeleteAsync(long id); +} + +public class SptzService : ISptzService +{ + private readonly IRepository _sptzRepo; + private readonly IRepository _spjcRepo; + + public SptzService(IRepository sptzRepo, IRepository spjcRepo) + { + _sptzRepo = sptzRepo; + _spjcRepo = spjcRepo; + } + + public async Task> GetPagedAsync(int pageIndex, int pageSize, DateTime dateFrom, DateTime dateTo) + { + var all = await _sptzRepo.GetAllAsync(); + var from = dateFrom.Date; + var to = dateTo.Date; + var q = all.AsQueryable().Where(x => x.Date.Date >= from && x.Date.Date <= to); + + var total = q.LongCount(); + var items = q.OrderBy(x => x.Date).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + return new PageResult + { + Items = items.Select(ToListDto).ToList(), + Total = total, PageIndex = pageIndex, PageSize = pageSize + }; + } + + public async Task GetForEditAsync(long id) + { + var e = await _sptzRepo.GetByIdAsync(id); + if (e == null) throw new ServiceException("丝片台帐不存在或已删除", 404); + + return new SptzSaveDto + { + Id = e.Id, + Date = e.Date, + DateLast = e.DateLast, + JsShangcun = e.JsShangcun, + JsXiasi = e.JsXiasi, + JsJinsi = e.JsJinsi, + FsFsps = e.FsFsps, + BsBsps = e.BsBsps, + ZlZhengpin = e.ZlZhengpin, + ZlTichu = e.ZlTichu, + Memo = e.Memo + }; + } + + /// + /// 新增默认值:上一本台账日期 + 其上存(= 上一日今存),并按当日「丝片检查」汇总出整理正品/剔除片数。 + /// + public async Task GetDefaultsAsync(DateTime date) + { + var target = date.Date; + var all = await _sptzRepo.GetAllAsync(); + + var latest = all.Where(x => x.Date.Date < target).OrderByDescending(x => x.Date).FirstOrDefault(); + var latestAny = all.OrderByDescending(x => x.Date).FirstOrDefault(); + + var spjcs = (await _spjcRepo.GetAllAsync()).Where(x => x.Date.Date == target).ToList(); + var tichu = spjcs.Sum(x => x.QsJiahua + x.QsYouwu + x.QsHeidian + x.QsShangsi + + x.QsYetiao + x.QsXiaosipian + x.QsQita); + var zhengpin = spjcs.Sum(x => x.Pianshu + x.PianshuXuegong) - tichu; + + return new SptzDefaultsDto + { + Date = target, + DateLast = latest?.Date, + JsShangcun = latest?.JsJincun ?? 0, + LatestDate = latestAny?.Date, + ZlTichu = tichu, + ZlZhengpin = zhengpin, + HasSpjc = spjcs.Count > 0 + }; + } + + public async Task SaveAsync(SptzSaveDto dto) + { + var date = dto.Date.Date; + + Sptz e; + if (dto.Id > 0) + { + e = await _sptzRepo.GetByIdAsync(dto.Id); + if (e == null) throw new ServiceException("丝片台帐不存在或已删除", 404); + + var dup = await _sptzRepo.FirstOrDefaultAsync(x => x.Id != dto.Id && x.Date.Date == date); + if (dup != null) throw new ServiceException($"已存在 {date:yyyy-MM-dd} 的丝片台帐!"); + } + else + { + var exists = await _sptzRepo.AnyAsync(x => x.Date.Date == date); + if (exists) throw new ServiceException($"已存在 {date:yyyy-MM-dd} 的丝片台帐!"); + e = new Sptz(); + await _sptzRepo.AddAsync(e); + } + + e.Date = date; + e.DateLast = dto.DateLast == default ? date.AddDays(-1) : dto.DateLast.Date; + e.JsShangcun = dto.JsShangcun; + e.JsXiasi = dto.JsXiasi; + e.JsJinsi = dto.JsJinsi; + e.FsFsps = dto.FsFsps; + e.BsBsps = dto.BsBsps; + e.ZlZhengpin = dto.ZlZhengpin; + e.ZlTichu = dto.ZlTichu; + e.Memo = dto.Memo ?? ""; + + // 派生值由服务端重算(与 F9 一致):上日遗留/今日新增 = 上存 − 浸丝;今存 = 下丝 + 上日遗留/今日新增 + e.JsSrylJrxz = e.JsShangcun - e.JsJinsi; + e.JsJincun = e.JsXiasi + e.JsSrylJrxz; + + await _sptzRepo.UpdateAsync(e); + return e.Id; + } + + public async Task DeleteAsync(long id) + { + var e = await _sptzRepo.GetByIdAsync(id); + if (e == null) throw new ServiceException("丝片台帐不存在或已删除", 404); + // 日期带唯一索引,物理删除避免「删除后重建」撞索引(与煮茧日报同一策略) + await _sptzRepo.HardDeleteAsync(e); + } + + private static SptzListItemDto ToListDto(Sptz x) => new() + { + Id = x.Id, + Date = x.Date, + DateLast = x.DateLast, + JsShangcun = x.JsShangcun, + JsXiasi = x.JsXiasi, + JsJinsi = x.JsJinsi, + JsSrylJrxz = x.JsSrylJrxz, + JsJincun = x.JsJincun, + FsFsps = x.FsFsps, + BsBsps = x.BsBsps, + ZlZhengpin = x.ZlZhengpin, + ZlTichu = x.ZlTichu, + Memo = x.Memo + }; +} diff --git a/backend/F9WebAiTest.Application/Services/Prod/XjrbService.cs b/backend/F9WebAiTest.Application/Services/Prod/XjrbService.cs new file mode 100644 index 0000000..d3ba427 --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Prod/XjrbService.cs @@ -0,0 +1,496 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities.Base; +using F9WebAiTest.Domain.Entities.Prod; + +namespace F9WebAiTest.Application.Services.Prod; + +/// +/// 选剥茧(对应 F9 生产-选剥茧 XBJ):选茧日报(含盈亏/联统率)、选茧计划及其明细、选茧班组及职工。 +/// +public interface IXjrbService +{ + // ---- 选茧日报 ---- + Task> GetPagedAsync(DateTime? start, DateTime? end, string like, int pageIndex, int pageSize); + Task GetByIdAsync(long id); + Task SaveAsync(XjrbSaveDto dto); + Task DeleteAsync(long id); + Task CountYingkuiAsync(XjrbSaveDto dto); + + // ---- 选茧计划 ---- + Task> GetPlanPagedAsync(DateTime? start, DateTime? end, string like, int pageIndex, int pageSize); + Task GetPlanByIdAsync(long id); + Task SavePlanAsync(XjrbPlanSaveDto dto); + Task DeletePlanAsync(long id); + + // ---- 选茧计划明细 ---- + Task> GetPlanItemsAsync(long planId); + Task SavePlanItemAsync(XjrbItemSaveDto dto); + Task DeletePlanItemAsync(long id); + Task CountPlanYingkuiAsync(XjrbItemSaveDto dto); + + // ---- 选茧班组 ---- + Task> GetBanzuPagedAsync(string banci, int? zu, int pageIndex, int pageSize); + Task> GetBanzuZhigongAsync(long banzuId); + Task SaveBanzuZhigongAsync(List dtos); +} + +public class XjrbService : IXjrbService +{ + private readonly IRepository _xjrbRepo; + private readonly IRepository _planRepo; + private readonly IRepository _itemRepo; + private readonly IRepository _banzuRepo; + private readonly IRepository _zgRepo; + private readonly IRepository _gyzkRepo; + + public XjrbService( + IRepository xjrbRepo, IRepository planRepo, IRepository itemRepo, + IRepository banzuRepo, IRepository zgRepo, IRepository gyzkRepo) + { + _xjrbRepo = xjrbRepo; _planRepo = planRepo; _itemRepo = itemRepo; + _banzuRepo = banzuRepo; _zgRepo = zgRepo; _gyzkRepo = gyzkRepo; + } + + private static decimal Round2(decimal v) => Math.Round(v, 2); + + private async Task> GyzkDictAsync() + => (await _gyzkRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.ZhuangKouName); + + // ==================== 选茧日报 ==================== + + public async Task> GetPagedAsync(DateTime? start, DateTime? end, string like, int pageIndex, int pageSize) + { + var all = await _xjrbRepo.GetAllAsync(); + var q = all.AsQueryable(); + if (start.HasValue) q = q.Where(x => x.Date.Date >= start.Value.Date); + if (end.HasValue) q = q.Where(x => x.Date.Date <= end.Value.Date); + + var gyzkDict = await GyzkDictAsync(); + if (!string.IsNullOrWhiteSpace(like)) + { + var key = like.Trim(); + q = q.Where(x => x.GyzkId > 0 && gyzkDict.ContainsKey(x.GyzkId) && gyzkDict[x.GyzkId].Contains(key)); + } + + var total = q.LongCount(); + var items = q.OrderByDescending(x => x.Date).ThenByDescending(x => x.Id) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + return new PageResult + { + Items = items.Select(ToDto).ToList(), + Total = total, PageIndex = pageIndex, PageSize = pageSize + }; + } + + private static XjrbListDto ToDto(XjrbRecord x) => new() + { + Id = x.Id, Date = x.Date, GyzkId = x.GyzkId, + YlLrBaoshu = x.YlLrBaoshu, YlLrZhongliang = x.YlLrZhongliang, + LtjBaoshu = x.LtjBaoshu, LtjZhongliang = x.LtjZhongliang, + HwjjBaoshu = x.HwjjBaoshu, HwjjZhongliang = x.HwjjZhongliang, + LjjBaoshu = x.LjjBaoshu, LjjZhongliang = x.LjjZhongliang, + MjBaoshu = x.MjBaoshu, MjZhongliang = x.MjZhongliang, + SgjBaoshu = x.SgjBaoshu, SgjZhongliang = x.SgjZhongliang, + XjjBaoshu = x.XjjBaoshu, XjjZhongliang = x.XjjZhongliang, + JyBaoshu = x.JyBaoshu, JyZhongliang = x.JyZhongliang, + JhBaoshu = x.JhBaoshu, JhZhongliang = x.JhZhongliang, + TotalZhongliang = x.TotalZhongliang, + ThBaoshu = x.ThBaoshu, ThZhongliang = x.ThZhongliang, + LiantongRate = x.LiantongRate, Yingkui = x.Yingkui + }; + + public async Task GetByIdAsync(long id) + { + var x = await _xjrbRepo.GetByIdAsync(id); + if (x == null) throw new ServiceException("选茧日报不存在", 404); + return new XjrbSaveDto + { + Id = x.Id, Date = x.Date, GyzkId = x.GyzkId, + YlLrBaoshu = x.YlLrBaoshu, YlLrZhongliang = x.YlLrZhongliang, + LtjBaoshu = x.LtjBaoshu, LtjZhongliang = x.LtjZhongliang, + HwjjBaoshu = x.HwjjBaoshu, HwjjZhongliang = x.HwjjZhongliang, + LjjBaoshu = x.LjjBaoshu, LjjZhongliang = x.LjjZhongliang, + MjBaoshu = x.MjBaoshu, MjZhongliang = x.MjZhongliang, + SgjBaoshu = x.SgjBaoshu, SgjZhongliang = x.SgjZhongliang, + XjjBaoshu = x.XjjBaoshu, XjjZhongliang = x.XjjZhongliang, + JyBaoshu = x.JyBaoshu, JyZhongliang = x.JyZhongliang, + JhBaoshu = x.JhBaoshu, JhZhongliang = x.JhZhongliang, + ThBaoshu = x.ThBaoshu, ThZhongliang = x.ThZhongliang, + LiantongRate = x.LiantongRate, Yingkui = x.Yingkui + }; + } + + public async Task SaveAsync(XjrbSaveDto dto) + { + if (dto.Date == default) throw new ServiceException("请选择选茧日期"); + if (dto.GyzkId <= 0) throw new ServiceException("请选择工艺庄口"); + + XjrbRecord entity; + var isNew = dto.Id == 0; + if (isNew) entity = new XjrbRecord(); + else + { + entity = await _xjrbRepo.GetByIdAsync(dto.Id); + if (entity == null) throw new ServiceException("选茧日报不存在", 404); + } + + entity.Date = dto.Date.Date; + entity.GyzkId = dto.GyzkId; + entity.YlLrBaoshu = dto.YlLrBaoshu; + entity.YlLrZhongliang = dto.YlLrZhongliang; + entity.LtjBaoshu = dto.LtjBaoshu; entity.LtjZhongliang = dto.LtjZhongliang; + entity.HwjjBaoshu = dto.HwjjBaoshu; entity.HwjjZhongliang = dto.HwjjZhongliang; + entity.LjjBaoshu = dto.LjjBaoshu; entity.LjjZhongliang = dto.LjjZhongliang; + entity.MjBaoshu = dto.MjBaoshu; entity.MjZhongliang = dto.MjZhongliang; + entity.SgjBaoshu = dto.SgjBaoshu; entity.SgjZhongliang = dto.SgjZhongliang; + entity.XjjBaoshu = dto.XjjBaoshu; entity.XjjZhongliang = dto.XjjZhongliang; + entity.JyBaoshu = dto.JyBaoshu; entity.JyZhongliang = dto.JyZhongliang; + entity.JhBaoshu = dto.JhBaoshu; entity.JhZhongliang = dto.JhZhongliang; + entity.ThBaoshu = dto.ThBaoshu; entity.ThZhongliang = dto.ThZhongliang; + entity.LiantongRate = dto.LiantongRate; + entity.Yingkui = dto.Yingkui; + // 合计重量 = 8 类茧重量之和(不含退回、不含领入) + entity.TotalZhongliang = Round2(entity.LtjZhongliang + entity.HwjjZhongliang + entity.LjjZhongliang + + entity.MjZhongliang + entity.SgjZhongliang + entity.XjjZhongliang + entity.JyZhongliang + entity.JhZhongliang); + + if (isNew) await _xjrbRepo.AddAsync(entity); else await _xjrbRepo.UpdateAsync(entity); + return entity.Id; + } + + public async Task DeleteAsync(long id) + { + var x = await _xjrbRepo.GetByIdAsync(id); + if (x == null) throw new ServiceException("选茧日报不存在", 404); + await _xjrbRepo.HardDeleteAsync(x); + } + + /// + /// 计算盈亏 / 联统率(对齐 F9 GetXjrbCountYingkuiQuery + GetXjrbLingruStartDateQuery)。 + /// 起始日期:由近及远跳过「联统率 = 0(未结算)」的日报,直到碰到已结算的一条为止; + /// 累计区间 = [起始日期, 当前日期];累加当前录入值 + 区间内历史日报(排除自身)。 + /// 联统率 = 累计联统茧重量 ÷ 累计干茧领入重量 × 100;盈亏 = 累计选出总重量 − 累计干茧领入重量。 + /// + public async Task CountYingkuiAsync(XjrbSaveDto dto) + { + if (dto.GyzkId <= 0) throw new ServiceException("请选择工艺庄口"); + + var date = dto.Date.Date; + var all = await _xjrbRepo.GetAllAsync(); + var history = all.Where(x => x.GyzkId == dto.GyzkId && x.Date.Date < date && x.Id != dto.Id) + .OrderByDescending(x => x.Date).ToList(); + + // 步骤 1:往回跳过未结算(LiantongRate == 0)的记录,确定起始日期 + var startDate = date; + foreach (var item in history) + { + if (item.LiantongRate != 0) break; + startDate = item.Date.Date; + } + + // 步骤 2:累计区间(闭区间,排除自身) + var range = history.Where(x => x.Date.Date >= startDate && x.Date.Date <= date).ToList(); + + // 步骤 3:累加(初始值 = 当前表单输入值) + decimal ltj = dto.LtjZhongliang; + decimal yllr = dto.YlLrZhongliang; + decimal allW = dto.LtjZhongliang + dto.HwjjZhongliang + dto.LjjZhongliang + dto.MjZhongliang + + dto.SgjZhongliang + dto.XjjZhongliang + dto.JyZhongliang + dto.JhZhongliang; + foreach (var item in range) + { + ltj += item.LtjZhongliang; + yllr += item.YlLrZhongliang; + allW += item.LtjZhongliang + item.HwjjZhongliang + item.LjjZhongliang + item.MjZhongliang + + item.SgjZhongliang + item.XjjZhongliang + item.JyZhongliang + item.JhZhongliang; + } + + // 步骤 4:公式 + var rate = yllr != 0 ? Round2(ltj / yllr * 100) : 0; + var yingkui = Round2(allW - yllr); + + return new XjrbYingkuiDto { LiantongRate = rate, Yingkui = yingkui, StartDate = startDate }; + } + + // ==================== 选茧计划 ==================== + + public async Task> GetPlanPagedAsync(DateTime? start, DateTime? end, string like, int pageIndex, int pageSize) + { + var all = await _planRepo.GetAllAsync(); + var q = all.AsQueryable(); + if (start.HasValue) q = q.Where(x => x.Date.Date >= start.Value.Date); + if (end.HasValue) q = q.Where(x => x.Date.Date <= end.Value.Date); + + var gyzkDict = await GyzkDictAsync(); + if (!string.IsNullOrWhiteSpace(like)) + { + var key = like.Trim(); + q = q.Where(x => x.GyzkId > 0 && gyzkDict.ContainsKey(x.GyzkId) && gyzkDict[x.GyzkId].Contains(key)); + } + + var total = q.LongCount(); + var items = q.OrderByDescending(x => x.Date).ThenByDescending(x => x.Id) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + return new PageResult + { + Items = items.Select(ToPlanDto).ToList(), + Total = total, PageIndex = pageIndex, PageSize = pageSize + }; + } + + private static XjrbPlanListDto ToPlanDto(XjrbPlan x) => new() + { + Id = x.Id, Date = x.Date, GyzkId = x.GyzkId, YllingliaodanId = x.YllingliaodanId, + YlBaoshu = x.YlBaoshu, YlZhongliang = x.YlZhongliang, + LtjBaoshu = x.LtjBaoshu, LtjZhongliang = x.LtjZhongliang, + HwjjBaoshu = x.HwjjBaoshu, HwjjZhongliang = x.HwjjZhongliang, + WjjBaoshu = x.WjjBaoshu, WjjZhongliang = x.WjjZhongliang, + SgjBaoshu = x.SgjBaoshu, SgjZhongliang = x.SgjZhongliang, + XjjBaoshu = x.XjjBaoshu, XjjZhongliang = x.XjjZhongliang, + JyBaoshu = x.JyBaoshu, JyZhongliang = x.JyZhongliang, + JhBaoshu = x.JhBaoshu, JhZhongliang = x.JhZhongliang, + TotalZhongliang = x.TotalZhongliang, LiantongRate = x.LiantongRate, + Yingkui = x.Yingkui, IsFinish = x.IsFinish + }; + + public async Task GetPlanByIdAsync(long id) + { + var x = await _planRepo.GetByIdAsync(id); + if (x == null) throw new ServiceException("选茧计划不存在", 404); + return new XjrbPlanSaveDto + { + Id = x.Id, Date = x.Date, GyzkId = x.GyzkId, YllingliaodanId = x.YllingliaodanId, + YlBaoshu = x.YlBaoshu, YlZhongliang = x.YlZhongliang, + LtjBaoshu = x.LtjBaoshu, LtjZhongliang = x.LtjZhongliang, + HwjjBaoshu = x.HwjjBaoshu, HwjjZhongliang = x.HwjjZhongliang, + WjjBaoshu = x.WjjBaoshu, WjjZhongliang = x.WjjZhongliang, + SgjBaoshu = x.SgjBaoshu, SgjZhongliang = x.SgjZhongliang, + XjjBaoshu = x.XjjBaoshu, XjjZhongliang = x.XjjZhongliang, + JyBaoshu = x.JyBaoshu, JyZhongliang = x.JyZhongliang, + JhBaoshu = x.JhBaoshu, JhZhongliang = x.JhZhongliang + }; + } + + public async Task SavePlanAsync(XjrbPlanSaveDto dto) + { + if (dto.Date == default) throw new ServiceException("请选择日期"); + if (dto.GyzkId <= 0) throw new ServiceException("请选择工艺庄口"); + + XjrbPlan entity; + var isNew = dto.Id == 0; + if (isNew) entity = new XjrbPlan(); + else + { + entity = await _planRepo.GetByIdAsync(dto.Id); + if (entity == null) throw new ServiceException("选茧计划不存在", 404); + if (entity.IsFinish) throw new ServiceException("该计划已选茧完成,不能修改"); + } + + entity.Date = dto.Date.Date; + entity.GyzkId = dto.GyzkId; + entity.YllingliaodanId = dto.YllingliaodanId; + entity.YlBaoshu = dto.YlBaoshu; entity.YlZhongliang = dto.YlZhongliang; + entity.LtjBaoshu = dto.LtjBaoshu; entity.LtjZhongliang = dto.LtjZhongliang; + entity.HwjjBaoshu = dto.HwjjBaoshu; entity.HwjjZhongliang = dto.HwjjZhongliang; + entity.WjjBaoshu = dto.WjjBaoshu; entity.WjjZhongliang = dto.WjjZhongliang; + entity.SgjBaoshu = dto.SgjBaoshu; entity.SgjZhongliang = dto.SgjZhongliang; + entity.XjjBaoshu = dto.XjjBaoshu; entity.XjjZhongliang = dto.XjjZhongliang; + entity.JyBaoshu = dto.JyBaoshu; entity.JyZhongliang = dto.JyZhongliang; + entity.JhBaoshu = dto.JhBaoshu; entity.JhZhongliang = dto.JhZhongliang; + entity.TotalZhongliang = Round2(entity.LtjZhongliang + entity.HwjjZhongliang + entity.WjjZhongliang + + entity.SgjZhongliang + entity.XjjZhongliang + entity.JyZhongliang + entity.JhZhongliang); + + if (isNew) await _planRepo.AddAsync(entity); else await _planRepo.UpdateAsync(entity); + return entity.Id; + } + + public async Task DeletePlanAsync(long id) + { + var x = await _planRepo.GetByIdAsync(id); + if (x == null) throw new ServiceException("选茧计划不存在", 404); + if (x.IsFinish) throw new ServiceException("该计划已选茧完成,不能删除"); + var items = await _itemRepo.FindAsync(i => i.XjrbPlanId == id); + if (items.Count > 0) await _itemRepo.HardDeleteRangeAsync(items); + await _planRepo.HardDeleteAsync(x); + } + + // ==================== 选茧计划明细 ==================== + + public async Task> GetPlanItemsAsync(long planId) + { + var items = await _itemRepo.FindAsync(x => x.XjrbPlanId == planId); + return items.OrderBy(x => x.Date).ThenBy(x => x.Id).Select(x => new XjrbItemDto + { + Id = x.Id, XjrbPlanId = x.XjrbPlanId, Date = x.Date, + LtjBaoshu = x.LtjBaoshu, LtjZhongliang = x.LtjZhongliang, + HwjjBaoshu = x.HwjjBaoshu, HwjjZhongliang = x.HwjjZhongliang, + WjjBaoshu = x.WjjBaoshu, WjjZhongliang = x.WjjZhongliang, + SgjBaoshu = x.SgjBaoshu, SgjZhongliang = x.SgjZhongliang, + XjjBaoshu = x.XjjBaoshu, XjjZhongliang = x.XjjZhongliang, + JyBaoshu = x.JyBaoshu, JyZhongliang = x.JyZhongliang, + JhBaoshu = x.JhBaoshu, JhZhongliang = x.JhZhongliang, + TotalZhongliang = x.TotalZhongliang, IsFinish = x.IsFinish + }).ToList(); + } + + public async Task SavePlanItemAsync(XjrbItemSaveDto dto) + { + if (dto.XjrbPlanId <= 0) throw new ServiceException("计划无效"); + if (dto.Date == default) throw new ServiceException("请选择选茧日期"); + + var plan = await _planRepo.GetByIdAsync(dto.XjrbPlanId); + if (plan == null) throw new ServiceException("选茧计划不存在", 404); + if (plan.IsFinish) throw new ServiceException("该计划已选茧完成,不能修改明细"); + + XjrbItem entity; + var isNew = dto.Id == 0; + if (isNew) entity = new XjrbItem { XjrbPlanId = dto.XjrbPlanId }; + else + { + entity = await _itemRepo.GetByIdAsync(dto.Id); + if (entity == null) throw new ServiceException("选茧明细不存在", 404); + } + + entity.Date = dto.Date.Date; + entity.LtjBaoshu = dto.LtjBaoshu; entity.LtjZhongliang = dto.LtjZhongliang; + entity.HwjjBaoshu = dto.HwjjBaoshu; entity.HwjjZhongliang = dto.HwjjZhongliang; + entity.WjjBaoshu = dto.WjjBaoshu; entity.WjjZhongliang = dto.WjjZhongliang; + entity.SgjBaoshu = dto.SgjBaoshu; entity.SgjZhongliang = dto.SgjZhongliang; + entity.XjjBaoshu = dto.XjjBaoshu; entity.XjjZhongliang = dto.XjjZhongliang; + entity.JyBaoshu = dto.JyBaoshu; entity.JyZhongliang = dto.JyZhongliang; + entity.JhBaoshu = dto.JhBaoshu; entity.JhZhongliang = dto.JhZhongliang; + entity.TotalZhongliang = Round2(entity.LtjZhongliang + entity.HwjjZhongliang + entity.WjjZhongliang + + entity.SgjZhongliang + entity.XjjZhongliang + entity.JyZhongliang + entity.JhZhongliang); + entity.IsFinish = dto.IsFinish; + + if (isNew) await _itemRepo.AddAsync(entity); else await _itemRepo.UpdateAsync(entity); + + // 勾选「选茧完成」时把盈亏回写计划(对齐 F9 UpdateXjrbPlanYingkuiCommand) + if (dto.IsFinish) + { + var yk = await CountPlanYingkuiAsync(dto); + plan.TotalZhongliang = Round2(plan.TotalZhongliang + entity.TotalZhongliang); + plan.Yingkui = yk.Yingkui; + plan.LiantongRate = yk.LiantongRate; + plan.IsFinish = true; + await _planRepo.UpdateAsync(plan); + } + + return entity.Id; + } + + public async Task DeletePlanItemAsync(long id) + { + var x = await _itemRepo.GetByIdAsync(id); + if (x == null) throw new ServiceException("选茧明细不存在", 404); + var plan = await _planRepo.GetByIdAsync(x.XjrbPlanId); + if (plan != null && plan.IsFinish) throw new ServiceException("该计划已选茧完成,不能删除明细"); + await _itemRepo.HardDeleteAsync(x); + } + + /// + /// 计划维度盈亏(对齐 F9 GetXjrbPlanYingkuiQuery,口径与日报不同): + /// 联统率按「包数」:联统茧包数 ÷ 各类茧包数之和 × 100;盈亏 = 累计选出总重量 − 计划原料领入重量。 + /// 注:老系统仓储里 wjjBaoshu / wjjZhongliang 源字段交叉写反(疑似 bug),此处已按正确字段聚合。 + /// + public async Task CountPlanYingkuiAsync(XjrbItemSaveDto dto) + { + var plan = await _planRepo.GetByIdAsync(dto.XjrbPlanId); + if (plan == null) throw new ServiceException("选茧计划不存在", 404); + + var items = (await _itemRepo.FindAsync(x => x.XjrbPlanId == dto.XjrbPlanId && x.Id != dto.Id)).ToList(); + + decimal ltjZ = dto.LtjZhongliang, hwjjZ = dto.HwjjZhongliang, wjjZ = dto.WjjZhongliang, + sgjZ = dto.SgjZhongliang, xjjZ = dto.XjjZhongliang, jyZ = dto.JyZhongliang, jhZ = dto.JhZhongliang; + decimal ltjB = dto.LtjBaoshu, hwjjB = dto.HwjjBaoshu, wjjB = dto.WjjBaoshu, + sgjB = dto.SgjBaoshu, xjjB = dto.XjjBaoshu, jyB = dto.JyBaoshu, jhB = dto.JhBaoshu; + + foreach (var it in items) + { + ltjZ += it.LtjZhongliang; hwjjZ += it.HwjjZhongliang; wjjZ += it.WjjZhongliang; + sgjZ += it.SgjZhongliang; xjjZ += it.XjjZhongliang; jyZ += it.JyZhongliang; jhZ += it.JhZhongliang; + ltjB += it.LtjBaoshu; hwjjB += it.HwjjBaoshu; wjjB += it.WjjBaoshu; + sgjB += it.SgjBaoshu; xjjB += it.XjjBaoshu; jyB += it.JyBaoshu; jhB += it.JhBaoshu; + } + + var totalZhongliang = Round2(ltjZ + hwjjZ + wjjZ + sgjZ + xjjZ + jyZ + jhZ); + var baoshuSum = ltjB + hwjjB + wjjB + sgjB + xjjB + jyB + jhB; + var rate = baoshuSum != 0 ? Round2(ltjB / (decimal)baoshuSum * 100) : 0; + + return new XjrbYingkuiDto { LiantongRate = rate, Yingkui = Round2(totalZhongliang - plan.YlZhongliang), StartDate = plan.Date }; + } + + // ==================== 选茧班组 ==================== + + public async Task> GetBanzuPagedAsync(string banci, int? zu, int pageIndex, int pageSize) + { + var all = await _banzuRepo.GetAllAsync(); + var q = all.AsQueryable(); + if (!string.IsNullOrWhiteSpace(banci)) q = q.Where(x => x.Banci == banci); + if (zu.HasValue) q = q.Where(x => x.Zu == zu.Value); + + var total = q.LongCount(); + var items = q.OrderBy(x => x.Banci).ThenBy(x => x.Zu) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + return new PageResult + { + Items = items.Select(x => new XjpbBanzuDto { Id = x.Id, Banci = x.Banci, Zu = x.Zu }).ToList(), + Total = total, PageIndex = pageIndex, PageSize = pageSize + }; + } + + public async Task> GetBanzuZhigongAsync(long banzuId) + { + if (banzuId <= 0) return new List(); + var list = await _zgRepo.FindAsync(x => x.BanzuId == banzuId); + return list.OrderBy(x => x.Sort).ThenBy(x => x.Id).Select(x => new XjpbBanzuZhigongDto + { + Id = x.Id, BanzuId = x.BanzuId, Banci = x.Banci, Zu = x.Zu, + YgId = x.YgId, YgName = x.YgName, YgPhone = x.YgPhone, Sort = x.Sort + }).ToList(); + } + + /// + /// 班组「编辑录入」批量保存(对齐 F9 XjpbBanzuZhigong_SaveCommand): + /// 按班次整体覆盖——先清该班次旧职工与班组,再把「已勾选且有组别」的职工写入,并重建班组。 + /// + public async Task SaveBanzuZhigongAsync(List dtos) + { + var list = dtos ?? new List(); + var banci = list.FirstOrDefault()?.Banci ?? ""; + if (string.IsNullOrWhiteSpace(banci)) throw new ServiceException("班次无效"); + + // 清理该班次旧数据 + var oldZg = await _zgRepo.FindAsync(x => x.Banci == banci); + if (oldZg.Count > 0) await _zgRepo.HardDeleteRangeAsync(oldZg); + var oldBanzu = await _banzuRepo.FindAsync(x => x.Banci == banci); + if (oldBanzu.Count > 0) await _banzuRepo.HardDeleteRangeAsync(oldBanzu); + + var picked = list.Where(x => x.Selected && x.Zu > 0).ToList(); + if (picked.Count == 0) return; + + // 重建班组(班次 + 组 去重) + var banzuDict = new Dictionary(); + foreach (var zu in picked.Select(x => x.Zu).Distinct().OrderBy(x => x)) + { + var banzu = new XjpbBanzu { Banci = banci, Zu = zu }; + await _banzuRepo.AddAsync(banzu); + banzuDict[zu] = banzu.Id; + } + + var sort = 0; + foreach (var p in picked) + { + await _zgRepo.AddAsync(new XjpbBanzuZhigong + { + BanzuId = banzuDict.TryGetValue(p.Zu, out var bid) ? bid : 0, + Banci = banci, Zu = p.Zu, + YgId = p.YgId, YgName = p.YgName ?? "", YgPhone = p.YgPhone ?? "", + Sort = p.Sort > 0 ? p.Sort : ++sort + }); + } + } +} diff --git a/backend/F9WebAiTest.Application/Services/Prod/ZjrbService.cs b/backend/F9WebAiTest.Application/Services/Prod/ZjrbService.cs new file mode 100644 index 0000000..82e9898 --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Prod/ZjrbService.cs @@ -0,0 +1,609 @@ +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.Entities.SYS.Registries; +using F9WebAiTest.Domain.Enums.SYS.Employees; +using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments; + +namespace F9WebAiTest.Application.Services.Prod; + +/// +/// 煮茧日报(生产-前缫):某日某车间某庄口某规格一条日报,含「机组」「班次」「机组班次」三层明细。 +/// 对应 F9 Page_Zjrb(列表)、Page_Zjrb_EditV2(录入)、Zjrb_SaveCommand / Zjrb_DelCommand / +/// CalculateJizuSaosiHoursCommand(计算缫丝时长)。 +/// +/// 计算口径(与 F9 前端 Page_Zjrb_EditV2 一致,服务端保存时重算): +/// 机组.今缫桶数 = 甲班 + 乙班 +/// 机组.今存桶数 = 上存 + 今缫 − 今落(今落为 0 且余量够一个标准桶时自动补「工艺落丝桶数」) +/// 班次.今缫重量 = 该班次机组今缫桶数 × 每桶茧量 ÷ 1000 +/// 班次.领入/退回重量 = 包数 × 联统茧标重(标重未配置则保留录入值) +/// 班次.今缫包数 = 今缫重量 ÷ 联统茧标重(四舍五入取整) +/// 班次.今存 = 上存 + 领入 − 今缫 − 退回 +/// 主表 = 甲班 + 乙班 +/// +public interface IZjrbService +{ + /// 日报列表(账期起止 + 车间 + 关键字) + Task> GetPagedAsync(DateTime dateFrom, DateTime dateTo, long depId, + string like, int pageIndex, int pageSize); + + /// 某条日报的机组明细(列表页下半表) + Task> GetJizuListAsync(long zjrbId); + + /// 修改回填:主表 + 班次 + 机组 + 机组班次 + Task GetForEditAsync(long id); + + /// 新增:按上一日结存带出上存、每桶茧量、工艺落丝桶数,并按机组车台生成机组行 + Task BuildDefaultsAsync(DateTime date, long depId, long gyzkId, string guige); + + /// 保存(整条替换:主表 upsert + 三层明细先删后建) + Task SaveAsync(ZjrbSaveDto dto); + + /// 删除(四张表级联物理删除) + Task DeleteAsync(long id); + + /// 计算缫丝时长(整日整车间重算:单班 9 小时 / 双班 18 小时,按茧量比例分配) + Task CalculateJizuSaosiHoursAsync(DateTime date, long depId); +} + +public class ZjrbService : IZjrbService +{ + /// 单班 / 双班的机组标准时长 + private const decimal SingleBanciHours = 9m; + private const decimal TwoBanciHours = 18m; + + /// 联统茧标重的参数配置位置(与 F9 一致) + private const string RegistryCategory = @"生产\前缫\煮茧"; + private const string RegistryKey = "联统茧标重"; + + private readonly IRepository _zjrbRepo; + private readonly IRepository _jizuRepo; + private readonly IRepository _banciRepo; + private readonly IRepository _jizuBanciRepo; + private readonly IRepository _jzctRepo; + private readonly IRepository _cjbcRepo; + private readonly IRepository _gyzkRepo; + private readonly IRepository _registryRepo; + private readonly IRepository _deptRepo; + + public ZjrbService(IRepository zjrbRepo, IRepository jizuRepo, + IRepository banciRepo, IRepository jizuBanciRepo, + IRepository jzctRepo, IRepository cjbcRepo, + IRepository gyzkRepo, IRepository registryRepo, + IRepository deptRepo) + { + _zjrbRepo = zjrbRepo; + _jizuRepo = jizuRepo; + _banciRepo = banciRepo; + _jizuBanciRepo = jizuBanciRepo; + _jzctRepo = jzctRepo; + _cjbcRepo = cjbcRepo; + _gyzkRepo = gyzkRepo; + _registryRepo = registryRepo; + _deptRepo = deptRepo; + } + + // ==================== 查询 ==================== + + public async Task> GetPagedAsync(DateTime dateFrom, DateTime dateTo, long depId, + string like, int pageIndex, int pageSize) + { + if (pageIndex <= 0) pageIndex = 1; + if (pageSize <= 0) pageSize = 20; + + var from = dateFrom.Date; + var to = dateTo.Date; + if (to < from) (from, to) = (to, from); + + var ctx = await BuildContextAsync(); + + var rows = await _zjrbRepo.FindAsync(x => x.Date >= from && x.Date <= to); + if (depId > 0) rows = rows.Where(x => x.DeptId == depId).ToList(); + + if (!string.IsNullOrWhiteSpace(like)) + { + var kw = like.Trim(); + rows = rows.Where(x => + ctx.GyzkNames.GetValueOrDefault(x.GyzkId, "").Contains(kw, StringComparison.OrdinalIgnoreCase) + || (x.Guige ?? "").Contains(kw, StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + + var total = rows.Count; + var pageRows = rows + .OrderByDescending(x => x.Date) + .ThenBy(x => ctx.DeptNames.GetValueOrDefault(x.DeptId, "")) + .ThenBy(x => x.GyzkId) + .ThenBy(x => x.Guige) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + var ids = pageRows.Select(x => x.Id).ToList(); + var jizus = ids.Count == 0 + ? new List() + : await _jizuRepo.FindAsync(x => ids.Contains(x.ZjrbId)); + + var items = pageRows.Select(x => + { + var jz = jizus.Where(j => j.ZjrbId == x.Id).ToList(); + return new ZjrbReadDto_View + { + Id = x.Id, + Date = x.Date, + DepId = x.DeptId, + DepName = ctx.DeptNames.GetValueOrDefault(x.DeptId, ""), + GyzkId = x.GyzkId, + GyzkName = ctx.GyzkNames.GetValueOrDefault(x.GyzkId, ""), + Guige = x.Guige ?? "", + LastDate = x.LastDate, + StandardLuositongshu = x.StandardLuositongshu, + MeitongJianliang = x.MeitongJianliang, + BaoshuShangcun = x.BaoshuShangcun, + BaoshuLingru = x.BaoshuLingru, + BaoshuSaosi = x.BaoshuSaosi, + BaoshuTuihui = x.BaoshuTuihui, + BaoshuJincun = x.BaoshuJincun, + JianliangShangcun = x.JianliangShangcun, + JianliangLingru = x.JianliangLingru, + JianliangSaosi = x.JianliangSaosi, + JianliangTuihui = x.JianliangTuihui, + JianliangJincun = x.JianliangJincun, + TongshuShangcun = jz.Sum(j => j.TongshuShangcun), + TongshuJinsao = jz.Sum(j => j.TongshuJinsao), + TongshuJinluo = jz.Sum(j => j.TongshuJinluo), + TongshuJincun = jz.Sum(j => j.TongshuJincun), + JizuSaosiHours = jz.Sum(j => j.JizuSaosiHours) + }; + }).ToList(); + + return new PageResult + { + Items = items, + Total = total, + PageIndex = pageIndex, + PageSize = pageSize + }; + } + + public async Task> GetJizuListAsync(long zjrbId) + { + var jizus = (await _jizuRepo.FindAsync(x => x.ZjrbId == zjrbId)).OrderBy(x => x.Jizu).ToList(); + var jb = await _jizuBanciRepo.FindAsync(x => x.ZjrbId == zjrbId); + return jizus.Select(j => ToJizuDto(j, jb)).ToList(); + } + + public async Task GetForEditAsync(long id) + { + var master = await _zjrbRepo.GetByIdAsync(id); + if (master == null) throw new ServiceException("煮茧日报不存在或已删除"); + + var ctx = await BuildContextAsync(); + var banciRows = (await _banciRepo.FindAsync(x => x.ZjrbId == id)).OrderBy(x => x.Banci).ToList(); + var jizuRows = (await _jizuRepo.FindAsync(x => x.ZjrbId == id)).OrderBy(x => x.Jizu).ToList(); + var jbRows = await _jizuBanciRepo.FindAsync(x => x.ZjrbId == id); + + var dto = new ZjrbSaveDto + { + Id = master.Id, + Date = master.Date, + DepId = master.DeptId, + DepName = ctx.DeptNames.GetValueOrDefault(master.DeptId, ""), + GyzkId = master.GyzkId, + GyzkName = ctx.GyzkNames.GetValueOrDefault(master.GyzkId, ""), + Guige = master.Guige ?? "", + LastDate = master.LastDate, + StandardLuositongshu = master.StandardLuositongshu, + MeitongJianliang = master.MeitongJianliang, + LiantongBiaozhong = await GetLiantongBiaozhongAsync(), + BaoshuShangcun = master.BaoshuShangcun, + BaoshuLingru = master.BaoshuLingru, + BaoshuSaosi = master.BaoshuSaosi, + BaoshuTuihui = master.BaoshuTuihui, + BaoshuJincun = master.BaoshuJincun, + JianliangShangcun = master.JianliangShangcun, + JianliangLingru = master.JianliangLingru, + JianliangSaosi = master.JianliangSaosi, + JianliangTuihui = master.JianliangTuihui, + JianliangJincun = master.JianliangJincun, + Bancis = banciRows.Select(ToBanciDto).ToList(), + Jizus = jizuRows.Select(j => ToJizuDto(j, jbRows)).ToList() + }; + + // 历史数据可能缺班次行(或车间班次配置调整过),按配置补齐甲乙班 + var need = (await GetBanciKindsAsync(master.DeptId)); + foreach (var b in need) + { + if (dto.Bancis.All(x => x.Banci != b)) + dto.Bancis.Add(new ZjrbBanciDto { Banci = b, BanciName = b.ToString() }); + } + dto.Bancis = dto.Bancis.OrderBy(x => x.Banci).ToList(); + + return dto; + } + + public async Task BuildDefaultsAsync(DateTime date, long depId, long gyzkId, string guige) + { + if (depId <= 0) throw new ServiceException("请选择车间"); + + var ctx = await BuildContextAsync(); + var d = date.Date; + var g = (guige ?? string.Empty).Trim(); + + // 上一日:同车间 + 同庄口(+ 同规格)的最近一条日报 + var candidates = await _zjrbRepo.FindAsync(x => x.Date < d && x.DeptId == depId && x.GyzkId == gyzkId); + var last = candidates + .Where(x => string.IsNullOrEmpty(g) || (x.Guige ?? "") == g) + .OrderByDescending(x => x.Date) + .FirstOrDefault(); + + var dto = new ZjrbSaveDto + { + Date = d, + DepId = depId, + DepName = ctx.DeptNames.GetValueOrDefault(depId, ""), + GyzkId = gyzkId, + GyzkName = ctx.GyzkNames.GetValueOrDefault(gyzkId, ""), + Guige = g, + LiantongBiaozhong = await GetLiantongBiaozhongAsync() + }; + + List lastBancis = new(); + List lastJizus = new(); + if (last != null) + { + dto.LastDate = last.Date; + dto.StandardLuositongshu = last.StandardLuositongshu; + dto.MeitongJianliang = last.MeitongJianliang; + lastBancis = await _banciRepo.FindAsync(x => x.ZjrbId == last.Id); + lastJizus = await _jizuRepo.FindAsync(x => x.ZjrbId == last.Id); + } + + // 班次行:按车间班次配置;未配置则固定甲乙两班 + foreach (var b in await GetBanciKindsAsync(depId)) + { + var prev = lastBancis.FirstOrDefault(x => x.Banci == b); + dto.Bancis.Add(new ZjrbBanciDto + { + Banci = b, + BanciName = b.ToString(), + BaoshuShangcun = prev?.BaoshuJincun ?? 0, + JianliangShangcun = prev?.JianliangJincun ?? 0 + }); + } + + // 机组行:按机组车台配置;上存桶数取上一日该机组今存桶数 + var jizus = (await _jzctRepo.FindAsync(x => x.DepartmentId == depId)) + .Select(x => x.Jizu).Distinct().OrderBy(x => x).ToList(); + foreach (var jz in jizus) + { + var prev = lastJizus.FirstOrDefault(x => x.Jizu == jz); + dto.Jizus.Add(new ZjrbJizuDto + { + Jizu = jz, + TongshuShangcun = prev?.TongshuJincun ?? 0 + }); + } + + return dto; + } + + // ==================== 保存 / 删除 ==================== + + public async Task SaveAsync(ZjrbSaveDto dto) + { + if (dto == null) throw new ServiceException("参数不能为空"); + if (dto.Date == default) throw new ServiceException("请选择日期"); + if (dto.DepId <= 0) throw new ServiceException("请选择车间"); + if (dto.GyzkId <= 0) throw new ServiceException("请选择工艺庄口"); + if (dto.MeitongJianliang <= 0) throw new ServiceException("每桶茧量必须大于 0"); + if (dto.StandardLuositongshu <= 0) throw new ServiceException("工艺落丝桶数必须大于 0"); + if (dto.Bancis == null || dto.Bancis.Count == 0) throw new ServiceException("请至少保留一个班次行"); + + var d = dto.Date.Date; + var guige = (dto.Guige ?? string.Empty).Trim(); + + // 唯一性:同 日期 + 车间 + 庄口 + 规格 + var exists = await _zjrbRepo.AnyAsync(x => + x.Id != dto.Id && x.Date == d && x.DeptId == dto.DepId && x.GyzkId == dto.GyzkId && x.Guige == guige); + if (exists) throw new ServiceException("已存在相同的煮茧日报!"); + + Normalize(dto, guige); + + long id = dto.Id; + if (id > 0) + { + var master = await _zjrbRepo.GetByIdAsync(id); + if (master == null) throw new ServiceException("煮茧日报不存在或已删除"); + CopyMaster(dto, master); + await _zjrbRepo.UpdateAsync(master); + await DeleteChildrenAsync(id); + } + else + { + var master = new ZjrbRecord { LastDate = dto.LastDate }; + CopyMaster(dto, master); + await _zjrbRepo.AddAsync(master); + id = master.Id; + } + + // 班次 + foreach (var b in dto.Bancis) + { + await _banciRepo.AddAsync(new ZjrbBanci + { + ZjrbId = id, + Banci = b.Banci, + BaoshuShangcun = b.BaoshuShangcun, + BaoshuLingru = b.BaoshuLingru, + BaoshuSaosi = b.BaoshuSaosi, + BaoshuTuihui = b.BaoshuTuihui, + BaoshuJincun = b.BaoshuJincun, + JianliangShangcun = b.JianliangShangcun, + JianliangLingru = b.JianliangLingru, + JianliangSaosi = b.JianliangSaosi, + JianliangTuihui = b.JianliangTuihui, + JianliangJincun = b.JianliangJincun + }); + } + + // 机组 + 机组班次(机组班次挂在刚生成的机组 Id 上) + foreach (var j in dto.Jizus) + { + var jizu = await _jizuRepo.AddAsync(new ZjrbJizu + { + ZjrbId = id, + Jizu = j.Jizu, + JizuSaosiHours = j.JizuSaosiHours, + TongshuShangcun = j.TongshuShangcun, + TongshuJinsao = j.TongshuJinsaoJiaban + j.TongshuJinsaoYiban, + TongshuJinluo = j.TongshuJinluo, + TongshuJincun = j.TongshuJincun + }); + + foreach (var b in dto.Bancis) + { + await _jizuBanciRepo.AddAsync(new ZjrbJizuBanci + { + ZjrbId = id, + ZjrbJizuId = jizu.Id, + Jizu = j.Jizu, + Banci = b.Banci, + TongshuJinsao = b.Banci == Prod_Banci_Enum.甲 ? j.TongshuJinsaoJiaban : j.TongshuJinsaoYiban + }); + } + } + + await _zjrbRepo.SaveChangesAsync(); + return id; + } + + public async Task DeleteAsync(long id) + { + var master = await _zjrbRepo.GetByIdAsync(id); + if (master == null) throw new ServiceException("煮茧日报不存在或已删除"); + await DeleteChildrenAsync(id); + await _zjrbRepo.HardDeleteAsync(master); + } + + private async Task DeleteChildrenAsync(long zjrbId) + { + var jizuRows = await _jizuRepo.FindAsync(x => x.ZjrbId == zjrbId); + if (jizuRows.Count > 0) await _jizuRepo.HardDeleteRangeAsync(jizuRows); + + var banciRows = await _banciRepo.FindAsync(x => x.ZjrbId == zjrbId); + if (banciRows.Count > 0) await _banciRepo.HardDeleteRangeAsync(banciRows); + + var jbRows = await _jizuBanciRepo.FindAsync(x => x.ZjrbId == zjrbId); + if (jbRows.Count > 0) await _jizuBanciRepo.HardDeleteRangeAsync(jbRows); + } + + // ==================== 计算缫丝时长 ==================== + + public async Task CalculateJizuSaosiHoursAsync(DateTime date, long depId) + { + if (depId <= 0) throw new ServiceException("请选择车间"); + var d = date.Date; + + var reports = await _zjrbRepo.FindAsync(x => x.Date == d && x.DeptId == depId); + if (reports.Count == 0) throw new ServiceException("该日该车间没有煮茧日报,无法计算缫丝时长"); + + var reportMap = reports.ToDictionary(x => x.Id); + var ids = reportMap.Keys.ToList(); + var jizus = await _jizuRepo.FindAsync(x => ids.Contains(x.ZjrbId)); + if (jizus.Count == 0) throw new ServiceException("该日该车间没有机组明细,无法计算缫丝时长"); + var jbRows = await _jizuBanciRepo.FindAsync(x => ids.Contains(x.ZjrbId)); + + // 双班判据:该机组「今缫桶数 > 0」的班次个数为 2 → 18 小时,否则 9 小时 + var hasTwoBanci = jizus.Select(x => x.Jizu).Distinct().ToDictionary( + jz => jz, + jz => jbRows.Where(x => x.Jizu == jz && x.TongshuJinsao > 0) + .Select(x => x.Banci).Distinct().Count() == 2); + + // 机组总茧量 = Σ(每桶茧量 × 机组今缫桶数) + var totalJianliang = jizus.GroupBy(x => x.Jizu).ToDictionary( + g => g.Key, + g => g.Sum(x => (decimal)reportMap[x.ZjrbId].MeitongJianliang * x.TongshuJinsao)); + + var changed = 0; + var groups = jizus.GroupBy(x => new + { + reportMap[x.ZjrbId].GyzkId, + Guige = reportMap[x.ZjrbId].Guige ?? "", + x.Jizu + }); + + foreach (var group in groups) + { + var jz = group.Key.Jizu; + var totalHours = hasTwoBanci.GetValueOrDefault(jz) ? TwoBanciHours : SingleBanciHours; + var jizuTotal = totalJianliang.GetValueOrDefault(jz); + var current = group.Sum(x => (decimal)reportMap[x.ZjrbId].MeitongJianliang * x.TongshuJinsao); + var ratio = jizuTotal > 0 ? current / jizuTotal : 0m; + var newHours = Math.Round(ratio * totalHours, 1, MidpointRounding.AwayFromZero); + + foreach (var item in group) + { + if (item.JizuSaosiHours == newHours) continue; + item.JizuSaosiHours = newHours; + await _jizuRepo.UpdateAsync(item); + changed++; + } + } + + return changed; + } + + // ==================== 内部:口径重算 / 上下文 ==================== + + /// 按 F9 编辑页口径重算派生字段(机组今存、班次重量与今存、主表合计) + private static void Normalize(ZjrbSaveDto dto, string guige) + { + dto.Guige = guige; + + foreach (var j in dto.Jizus) + { + if (j.JizuSaosiHours < 0) throw new ServiceException($"机组{j.Jizu}的缫丝时长不能为负数"); + if (j.JizuSaosiHours > 20) throw new ServiceException($"机组{j.Jizu}缫丝时长不能大于 20"); + j.TongshuJinsao = j.TongshuJinsaoJiaban + j.TongshuJinsaoYiban; + if (j.TongshuJinsao > 0 && j.JizuSaosiHours <= 0) + throw new ServiceException($"机组{j.Jizu}今缫桶数大于 0 时,请输入缫丝时长"); + + var pre = j.TongshuShangcun + j.TongshuJinsao - j.TongshuJinluo; + if (j.TongshuJinluo == 0 && dto.StandardLuositongshu > 0 && pre >= dto.StandardLuositongshu) + { + j.TongshuJinluo = dto.StandardLuositongshu; + pre -= dto.StandardLuositongshu; + } + j.TongshuJincun = pre; + } + + foreach (var b in dto.Bancis) + { + if (b.BaoshuShangcun > 0 && b.JianliangShangcun <= 0) + throw new ServiceException($"{b.Banci}班上存包数大于 0 时,请填写上存重量"); + + var tongshu = dto.Jizus.Sum(j => b.Banci == Prod_Banci_Enum.甲 + ? j.TongshuJinsaoJiaban + : j.TongshuJinsaoYiban); + + b.JianliangSaosi = Math.Round(tongshu * (decimal)dto.MeitongJianliang / 1000m, 1, + MidpointRounding.AwayFromZero); + + if (dto.LiantongBiaozhong > 0) + { + b.JianliangLingru = Math.Round(b.BaoshuLingru * dto.LiantongBiaozhong, 1, + MidpointRounding.AwayFromZero); + b.JianliangTuihui = Math.Round(b.BaoshuTuihui * dto.LiantongBiaozhong, 1, + MidpointRounding.AwayFromZero); + b.BaoshuSaosi = (int)Math.Round(b.JianliangSaosi / dto.LiantongBiaozhong, 0, + MidpointRounding.AwayFromZero); + } + + b.JianliangJincun = b.JianliangShangcun + b.JianliangLingru - b.JianliangSaosi - b.JianliangTuihui; + b.BaoshuJincun = b.BaoshuShangcun + b.BaoshuLingru - b.BaoshuSaosi - b.BaoshuTuihui; + } + + // 主表 = 甲班 + 乙班 + dto.BaoshuShangcun = dto.Bancis.Sum(x => x.BaoshuShangcun); + dto.BaoshuLingru = dto.Bancis.Sum(x => x.BaoshuLingru); + dto.BaoshuSaosi = dto.Bancis.Sum(x => x.BaoshuSaosi); + dto.BaoshuTuihui = dto.Bancis.Sum(x => x.BaoshuTuihui); + dto.BaoshuJincun = dto.Bancis.Sum(x => x.BaoshuJincun); + dto.JianliangShangcun = dto.Bancis.Sum(x => x.JianliangShangcun); + dto.JianliangLingru = dto.Bancis.Sum(x => x.JianliangLingru); + dto.JianliangSaosi = dto.Bancis.Sum(x => x.JianliangSaosi); + dto.JianliangTuihui = dto.Bancis.Sum(x => x.JianliangTuihui); + dto.JianliangJincun = dto.Bancis.Sum(x => x.JianliangJincun); + } + + private static void CopyMaster(ZjrbSaveDto dto, ZjrbRecord master) + { + master.Date = dto.Date.Date; + master.DeptId = dto.DepId; + master.GyzkId = dto.GyzkId; + master.Guige = dto.Guige ?? ""; + master.LastDate = dto.LastDate; + master.StandardLuositongshu = dto.StandardLuositongshu; + master.MeitongJianliang = dto.MeitongJianliang; + master.BaoshuShangcun = dto.BaoshuShangcun; + master.BaoshuLingru = dto.BaoshuLingru; + master.BaoshuSaosi = dto.BaoshuSaosi; + master.BaoshuTuihui = dto.BaoshuTuihui; + master.BaoshuJincun = dto.BaoshuJincun; + master.JianliangShangcun = dto.JianliangShangcun; + master.JianliangLingru = dto.JianliangLingru; + master.JianliangSaosi = dto.JianliangSaosi; + master.JianliangTuihui = dto.JianliangTuihui; + master.JianliangJincun = dto.JianliangJincun; + } + + private static ZjrbBanciDto ToBanciDto(ZjrbBanci x) => new() + { + Id = x.Id, + ZjrbId = x.ZjrbId, + Banci = x.Banci, + BanciName = x.Banci.ToString(), + BaoshuShangcun = x.BaoshuShangcun, + BaoshuLingru = x.BaoshuLingru, + BaoshuSaosi = x.BaoshuSaosi, + BaoshuTuihui = x.BaoshuTuihui, + BaoshuJincun = x.BaoshuJincun, + JianliangShangcun = x.JianliangShangcun, + JianliangLingru = x.JianliangLingru, + JianliangSaosi = x.JianliangSaosi, + JianliangTuihui = x.JianliangTuihui, + JianliangJincun = x.JianliangJincun + }; + + private static ZjrbJizuDto ToJizuDto(ZjrbJizu x, List jbRows) => new() + { + Id = x.Id, + ZjrbId = x.ZjrbId, + Jizu = x.Jizu, + JizuSaosiHours = x.JizuSaosiHours, + TongshuShangcun = x.TongshuShangcun, + TongshuJinsaoJiaban = jbRows.FirstOrDefault(b => b.ZjrbJizuId == x.Id && b.Banci == Prod_Banci_Enum.甲) + ?.TongshuJinsao ?? 0, + TongshuJinsaoYiban = jbRows.FirstOrDefault(b => b.ZjrbJizuId == x.Id && b.Banci == Prod_Banci_Enum.乙) + ?.TongshuJinsao ?? 0, + TongshuJinsao = x.TongshuJinsao, + TongshuJinluo = x.TongshuJinluo, + TongshuJincun = x.TongshuJincun + }; + + /// 车间班次配置;未配置时固定甲乙两班 + private async Task> GetBanciKindsAsync(long depId) + { + var cfgs = await _cjbcRepo.FindAsync(x => x.DeptId == depId); + if (cfgs.Count == 0) return new List { Prod_Banci_Enum.甲, Prod_Banci_Enum.乙 }; + return cfgs.Select(x => x.Banci).Distinct().OrderBy(x => x).ToList(); + } + + private async Task GetLiantongBiaozhongAsync() + { + var row = await _registryRepo.FirstOrDefaultAsync(x => + x.Category == RegistryCategory && x.KeyName == RegistryKey); + if (row == null || string.IsNullOrWhiteSpace(row.Value)) return 0m; + return decimal.TryParse(row.Value, out var v) ? v : 0m; + } + + private sealed class Ctx + { + public Dictionary DeptNames { get; init; } = new(); + public Dictionary GyzkNames { get; init; } = new(); + } + + private async Task BuildContextAsync() + { + var depts = await _deptRepo.GetAllAsync(); + var gyzks = await _gyzkRepo.GetAllAsync(); + return new Ctx + { + DeptNames = depts.ToDictionary(d => d.Id, d => d.Name), + GyzkNames = gyzks.ToDictionary(z => z.Id, z => z.ProductionDisplayName is { Length: > 0 } n + ? n + : z.ZhuangKouName) + }; + } +} diff --git a/backend/F9WebAiTest.Application/Services/Wms/WmsService.cs b/backend/F9WebAiTest.Application/Services/Wms/WmsService.cs new file mode 100644 index 0000000..94daf99 --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Wms/WmsService.cs @@ -0,0 +1,1903 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Wms; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities.Base; +using F9WebAiTest.Domain.Entities.Wms; +using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments.Department; +using SysGys = F9WebAiTest.Domain.Entities.SYS.Gongyingshangs.Gongyingshang; +using SysWz = F9WebAiTest.Domain.Entities.SYS.WuziMulus.WuziMulu; +using SysYlzk = F9WebAiTest.Domain.Entities.SYS.YuanliaoZhuangkous.YuanliaoZhuangkou; +using WuziGroup = F9WebAiTest.Domain.Entities.SYS.WuziMulus.WuziGroup; + +namespace F9WebAiTest.Application.Services.Wms; + +/// +/// 仓储(WMS)统一服务:原料 / 副产品 / 在制品 / 生丝 / 物资 五个模块。 +/// 对齐 F9 WinClient\F9.Business.WMS:入库、出库、库存台账(在制品另有笼筒架台账), +/// 以及各模块扩展:原料采购 / 翻包计划、生丝批次包件、物资库存 / 出入库流水 / 采购计划 / 采购报销。 +/// 五个模块结构一致,用 Module(Wms_Module_Enum)+ Kind(0库存台账 1笼筒架台账)区分。 +/// +public interface IWmsService +{ + Task GetOptionsAsync(int module); + + Task> GetRukuPagedAsync(int module, DateTime? start, DateTime? end, int? type, string like, int pageIndex, int pageSize); + Task GetRukuAsync(long id); + Task SaveRukuAsync(WmsRukuSaveDto dto); + Task DeleteRukuAsync(long id); + Task BaoxiaoAsync(List rukuIds); + + Task> GetChukuPagedAsync(int module, DateTime? start, DateTime? end, int? type, string like, int pageIndex, int pageSize); + Task GetChukuAsync(long id); + Task SaveChukuAsync(WmsChukuSaveDto dto); + Task DeleteChukuAsync(long id); + + Task> GetTzDatesAsync(int module, int kind); + Task GetTaizhangAsync(int module, int kind, DateTime date, bool regenerate); + Task SaveTaizhangAsync(WmsTzSaveDto dto); + Task DeleteTaizhangAsync(long id); + + Task> GetKucunAsync(int module, string like); + Task> GetCrkAsync(int module, DateTime start, DateTime end, long? wzmlId); + + Task> GetPiciPagedAsync(DateTime? start, DateTime? end, int kcStatus, string like, int pageIndex, int pageSize); + Task GetPiciAsync(long id); + Task SavePiciAsync(WmsPiciSaveDto dto); + Task DeletePiciAsync(long id); + Task> GetPiciChukuSelectAsync(long wzmlId, string excludePiciIds); + Task GetPiciQcAsync(long piciId); + Task SavePiciQcAsync(WmsPiciQcDto dto); + Task> GetPiciQcXdjyAsync(long piciId); + Task SavePiciQcXdjyAsync(List items); + + Task> GetCaigouAsync(DateTime? start, DateTime? end); + Task SaveCaigouAsync(WmsCaigouDto dto); + Task DeleteCaigouAsync(long id); + + Task> GetFbplanAsync(DateTime? start, DateTime? end); + Task SaveFbplanAsync(WmsFbplanDto dto); + Task DeleteFbplanAsync(long id); + + Task> GetCgplanAsync(DateTime? start, DateTime? end); + Task GetCgplanAsync(long id); + Task SaveCgplanAsync(WmsCgplanSaveDto dto); + Task DeleteCgplanAsync(long id); +} + +public class WmsService : IWmsService +{ + private readonly IRepository _rkRepo; + private readonly IRepository _rkDetailRepo; + private readonly IRepository _bmdRepo; + private readonly IRepository _ckRepo; + private readonly IRepository _ckDetailRepo; + private readonly IRepository _tzDateRepo; + private readonly IRepository _tzItemRepo; + private readonly IRepository _piciRepo; + private readonly IRepository _baojianRepo; + private readonly IRepository _piciChukuRepo; + private readonly IRepository _piciQcRepo; + private readonly IRepository _piciQcXdjyRepo; + private readonly IRepository _kucunRepo; + private readonly IRepository _caigouRepo; + private readonly IRepository _fbplanRepo; + private readonly IRepository _fbjlRepo; + private readonly IRepository _cgplanRepo; + private readonly IRepository _cgplanItemRepo; + private readonly IRepository _wzRepo; + private readonly IRepository _wzGroupRepo; + private readonly IRepository _ylzkRepo; + private readonly IRepository _gysRepo; + private readonly IRepository _depRepo; + private readonly IRepository _gyzkRepo; + + public WmsService( + IRepository rkRepo, IRepository rkDetailRepo, IRepository bmdRepo, + IRepository ckRepo, IRepository ckDetailRepo, + IRepository tzDateRepo, IRepository tzItemRepo, + IRepository piciRepo, IRepository baojianRepo, + IRepository piciChukuRepo, IRepository piciQcRepo, IRepository piciQcXdjyRepo, + IRepository kucunRepo, + IRepository caigouRepo, IRepository fbplanRepo, IRepository fbjlRepo, + IRepository cgplanRepo, IRepository cgplanItemRepo, + IRepository wzRepo, IRepository wzGroupRepo, IRepository ylzkRepo, + IRepository gysRepo, IRepository depRepo, IRepository gyzkRepo) + { + _rkRepo = rkRepo; _rkDetailRepo = rkDetailRepo; _bmdRepo = bmdRepo; + _ckRepo = ckRepo; _ckDetailRepo = ckDetailRepo; + _tzDateRepo = tzDateRepo; _tzItemRepo = tzItemRepo; + _piciRepo = piciRepo; _baojianRepo = baojianRepo; + _piciChukuRepo = piciChukuRepo; _piciQcRepo = piciQcRepo; _piciQcXdjyRepo = piciQcXdjyRepo; + _kucunRepo = kucunRepo; + _caigouRepo = caigouRepo; _fbplanRepo = fbplanRepo; _fbjlRepo = fbjlRepo; + _cgplanRepo = cgplanRepo; _cgplanItemRepo = cgplanItemRepo; + _wzRepo = wzRepo; _wzGroupRepo = wzGroupRepo; _ylzkRepo = ylzkRepo; + _gysRepo = gysRepo; _depRepo = depRepo; _gyzkRepo = gyzkRepo; + } + + // ==================== 模块元数据(对齐 F9 枚举) ==================== + + private static readonly Dictionary ModuleTypes = new() + { + // 原料:YLRk_Type 采购1/调拨2/其他3;YLCk_Type 领料1/销售2/样茧3/其他4 + { 1, (new[] { 1, 2, 3 }, new[] { "采购", "调拨", "其他" }, new[] { 1, 2, 3, 4 }, new[] { "领料", "销售", "样茧", "其他" }) }, + // 副产品:FcpRk_Type 生产1/试缫2/其他5;FcpCk_Type 销售1/调拨2/其他3 + { 2, (new[] { 1, 2, 5 }, new[] { "生产", "试缫", "其他" }, new[] { 1, 2, 3 }, new[] { "销售", "调拨", "其他" }) }, + // 在制品:ZJP_Rk_Type 选茧0/试缫2/退料3/其他5;ZJP_Ck_Type 领料0/销售1/其他2 + { 3, (new[] { 0, 2, 3, 5 }, new[] { "选茧", "试缫", "退料", "其他" }, new[] { 0, 1, 2 }, new[] { "领料", "销售", "其他" }) }, + // 生丝:SSRk_Type 生产0/退货1/其他2;SSCk_Type 销售1/其他5 + { 4, (new[] { 0, 1, 2 }, new[] { "生产", "退货", "其他" }, new[] { 1, 5 }, new[] { "销售", "其他" }) }, + // 物资:WJRk_Type 采购1/冲账2/盘盈3/其他4;WJCk_Type 领料1/销售2/调拨3/报废4/其他5 + { 5, (new[] { 1, 2, 3, 4 }, new[] { "采购", "冲账", "盘盈", "其他" }, new[] { 1, 2, 3, 4, 5 }, new[] { "领料", "销售", "调拨", "报废", "其他" }) } + }; + + private static string RkTypeName(int module, int type) + { + if (!ModuleTypes.TryGetValue(module, out var t)) return type.ToString(); + var i = Array.IndexOf(t.Rk, type); + return i >= 0 ? t.RkNames[i] : type.ToString(); + } + + private static string CkTypeName(int module, int type) + { + if (!ModuleTypes.TryGetValue(module, out var t)) return type.ToString(); + var i = Array.IndexOf(t.Ck, type); + return i >= 0 ? t.CkNames[i] : type.ToString(); + } + + /// 台账项目口径:原料=原料庄口;笼筒架=工艺庄口;其余=物资目录 + private static bool IsYlzkItem(int module, int kind) => module == (int)Wms_Module_Enum.原料 && kind == 0; + private static bool IsGyzkItem(int module, int kind) => module == (int)Wms_Module_Enum.中间品 && kind == 1; + + /// 台账两列口径:物资=数量/金额;其余=件数/净重 + private static bool IsWujizl(int module) => module == (int)Wms_Module_Enum.物资; + + /// 仓储模块 → 物资分组的业务类型(原料1 / 中间品2 / 生丝3 / 副产品4 / 五金5) + private static int WuziBizTypeOfModule(int module) => module switch + { + (int)Wms_Module_Enum.原料 => 1, + (int)Wms_Module_Enum.中间品 => 2, + (int)Wms_Module_Enum.生丝 => 3, + (int)Wms_Module_Enum.副产品 => 4, + _ => 5 + }; + + private static string BxStatusName(int v) => v switch { 1 => "待报销", 2 => "已报销", _ => "无" }; + private static string RkStatusName(int v) => v == 1 ? "已入库" : "未入库"; + private static string CkStatusName(int v) => v == 1 ? "已出库" : "未出库"; + private static string FbTypeName(int v) => v switch { 1 => "翻包中", 2 => "完成", _ => "暂停" }; + private static string RukuStatusName(int v) => v switch { 1 => "有效", 2 => "作废", _ => "无" }; + private static string KcStatusName(int v) => v switch { 1 => "待入库", 2 => "已入库", 3 => "部分出库", 4 => "已出库", _ => "全部" }; + private static string CgplanStatusName(int v) => v switch { 1 => "已确认", 2 => "已完成", _ => "草稿" }; + + // ==================== 基础数据 ==================== + + public async Task GetOptionsAsync(int module) + { + var result = new WmsOptionsDto(); + if (ModuleTypes.TryGetValue(module, out var t)) + { + for (var i = 0; i < t.Rk.Length; i++) result.RukuTypes.Add(new WmsTypeOptionDto { Value = t.Rk[i], Name = t.RkNames[i] }); + for (var i = 0; i < t.Ck.Length; i++) result.ChukuTypes.Add(new WmsTypeOptionDto { Value = t.Ck[i], Name = t.CkNames[i] }); + } + + // 各仓储模块只加载自己对应的物资分组(物资分组 BusinessType:原料1/中间品2/生丝3/副产品4/五金5) + var bizType = WuziBizTypeOfModule(module); + var groupIds = (await _wzGroupRepo.GetAllAsync()) + .Where(g => (int)g.BusinessType == bizType).Select(g => g.Id).ToHashSet(); + var wzs = (await _wzRepo.GetAllAsync()).Where(x => groupIds.Contains(x.GroupId)).ToList(); + result.Wuzi = wzs.OrderBy(x => x.Code).Select(x => new WmsOptionDto { Id = x.Id, Name = x.Name, Code = x.Code, Ext = x.Unit }).ToList(); + + var ylzks = await _ylzkRepo.GetAllAsync(); + result.YuanliaoZhuangkou = ylzks.OrderByDescending(x => x.Year).ThenBy(x => x.Code) + .Select(x => new WmsOptionDto { Id = x.Id, Name = x.Name, Code = x.Code }).ToList(); + + var gyzks = await _gyzkRepo.GetAllAsync(); + result.CraftZhuangkou = gyzks.Select(x => new WmsOptionDto { Id = x.Id, Name = x.ZhuangKouName }).ToList(); + + var deps = await _depRepo.GetAllAsync(); + result.Departments = deps.Select(x => new WmsOptionDto { Id = x.Id, Name = x.Name }).ToList(); + + var gyss = await _gysRepo.GetAllAsync(); + result.Gongyingshangs = gyss.Select(x => new WmsOptionDto { Id = x.Id, Name = x.Name, Ext = x.Phone }).ToList(); + + return result; + } + + private async Task> WzDictAsync() + => (await _wzRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + + private async Task> WzFullDictAsync() + => (await _wzRepo.GetAllAsync()).ToDictionary(x => x.Id, x => (x.Name, x.Code, x.Unit, x.Guige)); + + // ==================== 入库 ==================== + + public async Task> GetRukuPagedAsync(int module, DateTime? start, DateTime? end, int? type, string like, int pageIndex, int pageSize) + { + var all = await _rkRepo.GetAllAsync(); + var q = all.AsQueryable().Where(x => x.Module == (Wms_Module_Enum)module); + if (start.HasValue) q = q.Where(x => x.Date.Date >= start.Value.Date); + if (end.HasValue) q = q.Where(x => x.Date.Date <= end.Value.Date); + if (type.HasValue) q = q.Where(x => x.Type == type.Value); + + var gysDict = (await _gysRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var ylzkDict = (await _ylzkRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var gyzkDict = (await _gyzkRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.ZhuangKouName); + var depDict = (await _depRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + + var details = await _rkDetailRepo.GetAllAsync(); + var sumDict = details.Where(d => d.RukuId > 0) + .GroupBy(d => d.RukuId) + .ToDictionary(g => g.Key, g => new + { + Jianshu = g.Sum(x => x.Jianshu), + BaojianNum = g.Sum(x => x.BaojianNum), + Jingzhong = g.Sum(x => x.Jingzhong), + Shuliang = g.Sum(x => x.Shuliang), + Jine = g.Sum(x => x.Jine), + Count = g.Count() + }); + + if (!string.IsNullOrWhiteSpace(like)) + { + var key = like.Trim(); + q = q.Where(x => x.Code.Contains(key) || x.Kuguan.Contains(key) || x.Description.Contains(key) + || x.Caigouyuan.Contains(key) + || (x.GysId > 0 && gysDict.ContainsKey(x.GysId) && gysDict[x.GysId].Contains(key))); + } + + var total = q.LongCount(); + var items = q.OrderByDescending(x => x.Date).ThenByDescending(x => x.Code) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + return new PageResult + { + Items = items.Select(x => + { + sumDict.TryGetValue(x.Id, out var s); + return new WmsRukuListDto + { + Id = x.Id, Module = module, Date = x.Date, Type = x.Type, TypeName = RkTypeName(module, x.Type), + Code = x.Code, GysId = x.GysId, GysName = gysDict.GetValueOrDefault(x.GysId, ""), + KhId = x.KhId, KhName = gysDict.GetValueOrDefault(x.KhId, ""), + GyzkId = x.GyzkId, GyzkName = gyzkDict.GetValueOrDefault(x.GyzkId, ""), + DepId = x.DepId, DepName = depDict.GetValueOrDefault(x.DepId, ""), + Lianxiren = x.Lianxiren, Phone = x.Phone, Carno = x.Carno, Kuguan = x.Kuguan, + Caigouyuan = x.Caigouyuan, PayStatus = x.PayStatus, + RkStatus = x.RkStatus, RkStatusName = RkStatusName(x.RkStatus), + BxStatus = x.BxStatus, BxStatusName = BxStatusName(x.BxStatus), + Description = x.Description, + Jianshu = s?.Jianshu ?? 0, BaojianNum = s?.BaojianNum ?? 0, Jingzhong = s?.Jingzhong ?? 0, + Shuliang = s?.Shuliang ?? 0, Jine = s?.Jine ?? 0, DetailCount = s?.Count ?? 0 + }; + }).ToList(), + Total = total, PageIndex = pageIndex, PageSize = pageSize + }; + } + + public async Task GetRukuAsync(long id) + { + var ruku = await _rkRepo.GetByIdAsync(id); + if (ruku == null) throw new ServiceException("入库单不存在", 404); + var module = (int)ruku.Module; + + var details = await _rkDetailRepo.FindAsync(x => x.RukuId == id); + var bmds = await _bmdRepo.GetAllAsync(); + var bmdDict = bmds.Where(b => !b.IsChuku).GroupBy(b => b.DetailId) + .ToDictionary(g => g.Key, g => g.OrderBy(x => x.Id).ToList()); + + var wzDict = await WzFullDictAsync(); + var ylzkDict = (await _ylzkRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var gyzkDict = (await _gyzkRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.ZhuangKouName); + var depDict = (await _depRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var gysDict = (await _gysRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + + return new WmsRukuEditDto + { + Id = ruku.Id, Module = module, Date = ruku.Date, Type = ruku.Type, Code = ruku.Code, + GysId = ruku.GysId, GysName = gysDict.GetValueOrDefault(ruku.GysId, ""), + KhId = ruku.KhId, KhName = gysDict.GetValueOrDefault(ruku.KhId, ""), + GyzkId = ruku.GyzkId, GyzkName = gyzkDict.GetValueOrDefault(ruku.GyzkId, ""), + DepId = ruku.DepId, DepName = depDict.GetValueOrDefault(ruku.DepId, ""), + Lianxiren = ruku.Lianxiren, Phone = ruku.Phone, Carno = ruku.Carno, Kuguan = ruku.Kuguan, + Caigouyuan = ruku.Caigouyuan, PayStatus = ruku.PayStatus, + RkStatus = ruku.RkStatus, BxStatus = ruku.BxStatus, Description = ruku.Description, + Details = details.OrderBy(x => x.Id).Select(d => + { + wzDict.TryGetValue(d.WzmlId, out var wz); + return new WmsRukuDetailDto + { + Id = d.Id, RukuId = d.RukuId, WzmlId = d.WzmlId, + WzmlName = wz.Name ?? "", WzmlCode = wz.Code ?? "", Unit = wz.Unit ?? "", + YlzkId = d.YlzkId, YlzkName = ylzkDict.GetValueOrDefault(d.YlzkId, ""), + GyzkId = d.GyzkId, GyzkName = gyzkDict.GetValueOrDefault(d.GyzkId, ""), + DepId = d.DepId, DepName = depDict.GetValueOrDefault(d.DepId, ""), + Jianshu = d.Jianshu, Maozhong = d.Maozhong, Pizhong = d.Pizhong, Jingzhong = d.Jingzhong, + BiaobaoZhongliang = d.BiaobaoZhongliang, BaozhuangZhongliang = d.BaozhuangZhongliang, + Shuliang = d.Shuliang, Danjia = d.Danjia, Jine = d.Jine, Sunhao = d.Sunhao, + PiciCodes = d.PiciCodes, PiciNum = d.PiciNum, BaojianNum = d.BaojianNum, + PiciIds = d.PiciIds, + Banci = d.Banci, JidiWaigou = d.JidiWaigou, + Bangmadans = bmdDict.TryGetValue(d.Id, out var list) + ? list.Select(b => new WmsBangmadanDto + { + Id = b.Id, DetailId = b.DetailId, IsChuku = b.IsChuku, Jianshu = b.Jianshu, + Maozhong = b.Maozhong, Pizhong = b.Pizhong, Jingzhong = b.Jingzhong, GuobangShijian = b.GuobangShijian + }).ToList() + : new List() + }; + }).ToList() + }; + } + + public async Task SaveRukuAsync(WmsRukuSaveDto dto) + { + var module = dto.Module; + if (module < 1 || module > 5) throw new ServiceException("模块无效"); + if (dto.Date == default) throw new ServiceException("请选择入库日期"); + if (dto.Details == null || dto.Details.Count == 0) throw new ServiceException("请至少录入一条入库明细"); + + WmsRuku entity; + var isNew = dto.Id == 0; + if (isNew) + { + entity = new WmsRuku { Module = (Wms_Module_Enum)module }; + } + else + { + entity = await _rkRepo.GetByIdAsync(dto.Id); + if (entity == null) throw new ServiceException("入库单不存在", 404); + // 物资模块:先回滚库存,再按新明细重算 + if (module == (int)Wms_Module_Enum.物资) await RollbackWjKucunAsync(dto.Id, true); + } + + entity.Date = dto.Date.Date; + entity.Type = dto.Type; + entity.Code = (dto.Code ?? "").Trim(); + entity.GysId = dto.GysId; + entity.KhId = dto.KhId; + entity.GyzkId = dto.GyzkId; + entity.DepId = dto.DepId; + entity.Lianxiren = dto.Lianxiren ?? ""; + entity.Phone = dto.Phone ?? ""; + entity.Carno = dto.Carno ?? ""; + entity.Kuguan = dto.Kuguan ?? ""; + entity.Caigouyuan = dto.Caigouyuan ?? ""; + entity.PayStatus = dto.PayStatus ?? ""; + entity.RkStatus = dto.RkStatus; + entity.BxStatus = dto.BxStatus; + entity.Description = dto.Description ?? ""; + + // 单据编号唯一(同模块 + 同日期) + if (!string.IsNullOrWhiteSpace(entity.Code)) + { + var dup = await _rkRepo.FirstOrDefaultAsync(x => x.Id != entity.Id && x.Module == (Wms_Module_Enum)module + && x.Date == entity.Date && x.Code == entity.Code); + if (dup != null) throw new ServiceException($"单据编号「{entity.Code}」在该日期已存在"); + } + + if (isNew) await _rkRepo.AddAsync(entity); else await _rkRepo.UpdateAsync(entity); + + // 明细先删后建(物理删除,避免软删残留影响汇总) + var olds = await _rkDetailRepo.FindAsync(x => x.RukuId == entity.Id); + if (olds.Count > 0) + { + // 生丝:编辑前回滚旧批次的入库状态 + if (module == (int)Wms_Module_Enum.生丝) + await RollbackPiciRukuAsync(olds); + var oldBmds = await _bmdRepo.GetAllAsync(); + var del = oldBmds.Where(b => !b.IsChuku && olds.Any(o => o.Id == b.DetailId)).ToList(); + if (del.Count > 0) await _bmdRepo.HardDeleteRangeAsync(del); + await _rkDetailRepo.HardDeleteRangeAsync(olds); + } + + foreach (var d in dto.Details) + { + if (d.WzmlId == 0 && d.YlzkId == 0 && d.GyzkId == 0) throw new ServiceException("入库明细必须选择物资 / 庄口"); + + // 磅码单汇总:件数 / 毛重 / 皮重 / 净重 + var bmds = d.Bangmadans ?? new List(); + foreach (var b in bmds) b.Jingzhong = Round2(b.Maozhong - b.Pizhong); + + var detail = new WmsRukuDetail + { + RukuId = entity.Id, + WzmlId = d.WzmlId, YlzkId = d.YlzkId, GyzkId = d.GyzkId, DepId = d.DepId, + BiaobaoZhongliang = d.BiaobaoZhongliang, BaozhuangZhongliang = d.BaozhuangZhongliang, + Shuliang = d.Shuliang, Danjia = d.Danjia, + Sunhao = d.Sunhao, PiciCodes = d.PiciCodes ?? "", PiciNum = d.PiciNum, BaojianNum = d.BaojianNum, + PiciIds = d.PiciIds ?? "", + Banci = d.Banci ?? "", JidiWaigou = d.JidiWaigou ?? "" + }; + + if (bmds.Count > 0) + { + detail.Jianshu = bmds.Sum(b => b.Jianshu); + detail.Maozhong = Round2(bmds.Sum(b => b.Maozhong)); + detail.Pizhong = Round2(bmds.Sum(b => b.Pizhong)); + detail.Jingzhong = Round2(bmds.Sum(b => b.Jingzhong)); + } + else + { + detail.Jianshu = d.Jianshu; + detail.Maozhong = d.Maozhong; + detail.Pizhong = d.Pizhong; + detail.Jingzhong = d.Jingzhong; + } + + // 净重 = 毛重 − 皮重 − 包装重量 − 运损(各模块未使用的项为 0) + if (detail.Jingzhong == 0) + detail.Jingzhong = Round2(detail.Maozhong - detail.Pizhong - detail.BaozhuangZhongliang - detail.Sunhao); + + // 物资:金额 = 数量 × 单价(冲账允许负数) + if (module == (int)Wms_Module_Enum.物资) + { + if (detail.Sunhao == 0 && Math.Abs(d.Jine) > 0) + detail.Jine = d.Jine; + else + detail.Jine = Round2(detail.Shuliang * detail.Danjia); + if (detail.Shuliang != 0 && detail.Jine != 0 && detail.Danjia == 0) + detail.Danjia = Round4(detail.Jine / detail.Shuliang); + } + else + { + detail.Jine = d.Jine; + } + + await _rkDetailRepo.AddAsync(detail); + + // 生丝入库:按批次ID更新批次入库状态(对齐 F9 SSRk_CreateCommand.UpdateRk) + if (module == (int)Wms_Module_Enum.生丝 && !string.IsNullOrWhiteSpace(detail.PiciIds)) + await UpdatePiciRukuAsync(detail.PiciIds, detail.Id, entity.Date); + + foreach (var b in bmds) + { + await _bmdRepo.AddAsync(new WmsBangmadan + { + DetailId = detail.Id, IsChuku = false, Jianshu = b.Jianshu, + Maozhong = b.Maozhong, Pizhong = b.Pizhong, Jingzhong = b.Jingzhong, + GuobangShijian = b.GuobangShijian + }); + } + + // 物资模块:入库即增库存 + if (module == (int)Wms_Module_Enum.物资) + await ApplyWjKucunAsync(detail.WzmlId, detail.Shuliang, detail.Jine); + } + + return entity.Id; + } + + public async Task DeleteRukuAsync(long id) + { + var ruku = await _rkRepo.GetByIdAsync(id); + if (ruku == null) throw new ServiceException("入库单不存在", 404); + + if (ruku.Module == Wms_Module_Enum.物资) await RollbackWjKucunAsync(id, true); + + var details = await _rkDetailRepo.FindAsync(x => x.RukuId == id); + if (ruku.Module == Wms_Module_Enum.生丝) await RollbackPiciRukuAsync(details); + var bmds = await _bmdRepo.GetAllAsync(); + var del = bmds.Where(b => !b.IsChuku && details.Any(d => d.Id == b.DetailId)).ToList(); + if (del.Count > 0) await _bmdRepo.HardDeleteRangeAsync(del); + if (details.Count > 0) await _rkDetailRepo.HardDeleteRangeAsync(details); + // 物理删除:单据编号有唯一索引,软删残留会阻止重新创建同号单据 + await _rkRepo.HardDeleteAsync(ruku); + } + + /// 采购报销:把入库单置为「已报销」(物资模块) + public async Task BaoxiaoAsync(List rukuIds) + { + if (rukuIds == null || rukuIds.Count == 0) throw new ServiceException("请选择要报销的入库单"); + var count = 0; + foreach (var id in rukuIds) + { + var ruku = await _rkRepo.GetByIdAsync(id); + if (ruku == null) continue; + ruku.BxStatus = 2; + await _rkRepo.UpdateAsync(ruku); + count++; + } + return count; + } + + // ==================== 出库 ==================== + + public async Task> GetChukuPagedAsync(int module, DateTime? start, DateTime? end, int? type, string like, int pageIndex, int pageSize) + { + var all = await _ckRepo.GetAllAsync(); + var q = all.AsQueryable().Where(x => x.Module == (Wms_Module_Enum)module); + if (start.HasValue) q = q.Where(x => x.Date.Date >= start.Value.Date); + if (end.HasValue) q = q.Where(x => x.Date.Date <= end.Value.Date); + if (type.HasValue) q = q.Where(x => x.Type == type.Value); + + var gysDict = (await _gysRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var gyzkDict = (await _gyzkRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.ZhuangKouName); + var depDict = (await _depRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var wzDict = await WzDictAsync(); + + var details = await _ckDetailRepo.GetAllAsync(); + var sumDict = details.GroupBy(d => d.ChukuId) + .ToDictionary(g => g.Key, g => new + { + Jianshu = g.Sum(x => x.Jianshu), + Jingzhong = g.Sum(x => x.Jingzhong), + Shuliang = g.Sum(x => x.Shuliang), + Jine = g.Sum(x => x.Jine), + Count = g.Count() + }); + + if (!string.IsNullOrWhiteSpace(like)) + { + var key = like.Trim(); + q = q.Where(x => x.Code.Contains(key) || x.Kuguan.Contains(key) || x.Lingliaoren.Contains(key) + || x.Description.Contains(key) + || (x.WldwId > 0 && gysDict.ContainsKey(x.WldwId) && gysDict[x.WldwId].Contains(key))); + } + + var total = q.LongCount(); + var items = q.OrderByDescending(x => x.Date).ThenByDescending(x => x.Code) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + return new PageResult + { + Items = items.Select(x => + { + sumDict.TryGetValue(x.Id, out var s); + return new WmsChukuListDto + { + Id = x.Id, Module = module, Date = x.Date, Type = x.Type, TypeName = CkTypeName(module, x.Type), + Code = x.Code, + WldwId = x.WldwId, WldwName = gysDict.GetValueOrDefault(x.WldwId, ""), + KhId = x.KhId, KhName = gysDict.GetValueOrDefault(x.KhId, ""), + GyzkId = x.GyzkId, GyzkName = gyzkDict.GetValueOrDefault(x.GyzkId, ""), + LingliaoDepId = x.LingliaoDepId, LingliaoDepName = depDict.GetValueOrDefault(x.LingliaoDepId, ""), + UseDepId = x.UseDepId, UseDepName = depDict.GetValueOrDefault(x.UseDepId, ""), + DbdepId = x.DbdepId, DbdepName = depDict.GetValueOrDefault(x.DbdepId, ""), + Lingliaoren = x.Lingliaoren, Kuguan = x.Kuguan, UseLocation = x.UseLocation, + CkStatus = x.CkStatus, CkStatusName = CkStatusName(x.CkStatus), IsHongchong = x.IsHongchong, + Description = x.Description, + Jianshu = s?.Jianshu ?? 0, Jingzhong = s?.Jingzhong ?? 0, + Shuliang = s?.Shuliang ?? 0, Jine = s?.Jine ?? 0, DetailCount = s?.Count ?? 0 + }; + }).ToList(), + Total = total, PageIndex = pageIndex, PageSize = pageSize + }; + } + + public async Task GetChukuAsync(long id) + { + var chuku = await _ckRepo.GetByIdAsync(id); + if (chuku == null) throw new ServiceException("出库单不存在", 404); + + var details = await _ckDetailRepo.FindAsync(x => x.ChukuId == id); + var bmds = await _bmdRepo.GetAllAsync(); + var bmdDict = bmds.Where(b => b.IsChuku).GroupBy(b => b.DetailId) + .ToDictionary(g => g.Key, g => g.OrderBy(x => x.Id).ToList()); + + var wzDict = await WzFullDictAsync(); + var ylzkDict = (await _ylzkRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var gyzkDict = (await _gyzkRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.ZhuangKouName); + var depDict = (await _depRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var gysDict = (await _gysRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + + return new WmsChukuEditDto + { + Id = chuku.Id, Module = (int)chuku.Module, Date = chuku.Date, Type = chuku.Type, Code = chuku.Code, + WldwId = chuku.WldwId, WldwName = gysDict.GetValueOrDefault(chuku.WldwId, ""), + KhId = chuku.KhId, KhName = gysDict.GetValueOrDefault(chuku.KhId, ""), + GyzkId = chuku.GyzkId, GyzkName = gyzkDict.GetValueOrDefault(chuku.GyzkId, ""), + LingliaoDepId = chuku.LingliaoDepId, LingliaoDepName = depDict.GetValueOrDefault(chuku.LingliaoDepId, ""), + UseDepId = chuku.UseDepId, UseDepName = depDict.GetValueOrDefault(chuku.UseDepId, ""), + DbdepId = chuku.DbdepId, DbdepName = depDict.GetValueOrDefault(chuku.DbdepId, ""), + Lingliaoren = chuku.Lingliaoren, Kuguan = chuku.Kuguan, UseLocation = chuku.UseLocation, + CkStatus = chuku.CkStatus, IsHongchong = chuku.IsHongchong, HongchongId = chuku.HongchongId, + Description = chuku.Description, + Details = details.OrderBy(x => x.Id).Select(d => + { + wzDict.TryGetValue(d.WzmlId, out var wz); + return new WmsChukuDetailDto + { + Id = d.Id, ChukuId = d.ChukuId, WzmlId = d.WzmlId, + WzmlName = wz.Name ?? "", WzmlCode = wz.Code ?? "", Unit = wz.Unit ?? "", + YlzkId = d.YlzkId, YlzkName = ylzkDict.GetValueOrDefault(d.YlzkId, ""), + GyzkId = d.GyzkId, GyzkName = gyzkDict.GetValueOrDefault(d.GyzkId, ""), + DepId = d.DepId, DepName = depDict.GetValueOrDefault(d.DepId, ""), + Jianshu = d.Jianshu, Maozhong = d.Maozhong, Pizhong = d.Pizhong, Jingzhong = d.Jingzhong, + BiaobaoZhongliang = d.BiaobaoZhongliang, BaozhuangZhongliang = d.BaozhuangZhongliang, + Shuliang = d.Shuliang, Danjia = d.Danjia, Jine = d.Jine, KucunNum = d.KucunNum, + PiciCodes = d.PiciCodes, PiciNum = d.PiciNum, PcAndBjIds = d.PcAndBjIds, + Lingyongren = d.Lingyongren, Banci = d.Banci, + JidiWaigou = d.JidiWaigou, + Bangmadans = bmdDict.TryGetValue(d.Id, out var list) + ? list.Select(b => new WmsBangmadanDto + { + Id = b.Id, DetailId = b.DetailId, IsChuku = b.IsChuku, Jianshu = b.Jianshu, + Maozhong = b.Maozhong, Pizhong = b.Pizhong, Jingzhong = b.Jingzhong, GuobangShijian = b.GuobangShijian + }).ToList() + : new List() + }; + }).ToList() + }; + } + + public async Task SaveChukuAsync(WmsChukuSaveDto dto) + { + var module = dto.Module; + if (module < 1 || module > 5) throw new ServiceException("模块无效"); + if (dto.Date == default) throw new ServiceException("请选择出库日期"); + if (dto.Details == null || dto.Details.Count == 0) throw new ServiceException("请至少录入一条出库明细"); + + WmsChuku entity; + var isNew = dto.Id == 0; + if (isNew) + { + entity = new WmsChuku { Module = (Wms_Module_Enum)module }; + } + else + { + entity = await _ckRepo.GetByIdAsync(dto.Id); + if (entity == null) throw new ServiceException("出库单不存在", 404); + if (module == (int)Wms_Module_Enum.物资) await RollbackWjKucunAsync(dto.Id, false); + } + + entity.Date = dto.Date.Date; + entity.Type = dto.Type; + entity.Code = (dto.Code ?? "").Trim(); + entity.WldwId = dto.WldwId; + entity.KhId = dto.KhId; + entity.GyzkId = dto.GyzkId; + entity.LingliaoDepId = dto.LingliaoDepId; + entity.UseDepId = dto.UseDepId; + entity.DbdepId = dto.DbdepId; + entity.Lingliaoren = dto.Lingliaoren ?? ""; + entity.Kuguan = dto.Kuguan ?? ""; + entity.UseLocation = dto.UseLocation ?? ""; + entity.CkStatus = dto.CkStatus; + entity.CkTime = dto.CkStatus == 1 ? DateTime.Now : null; + entity.Description = dto.Description ?? ""; + + if (!string.IsNullOrWhiteSpace(entity.Code)) + { + var dup = await _ckRepo.FirstOrDefaultAsync(x => x.Id != entity.Id && x.Module == (Wms_Module_Enum)module + && x.Date == entity.Date && x.Code == entity.Code); + if (dup != null) throw new ServiceException($"单据编号「{entity.Code}」在该日期已存在"); + } + + if (isNew) await _ckRepo.AddAsync(entity); else await _ckRepo.UpdateAsync(entity); + + var olds = await _ckDetailRepo.FindAsync(x => x.ChukuId == entity.Id); + if (olds.Count > 0) + { + // 生丝:编辑前回滚旧批次的包件出库状态 + if (module == (int)Wms_Module_Enum.生丝) + await RollbackPiciChukuAsync(olds); + var oldBmds = await _bmdRepo.GetAllAsync(); + var del = oldBmds.Where(b => b.IsChuku && olds.Any(o => o.Id == b.DetailId)).ToList(); + if (del.Count > 0) await _bmdRepo.HardDeleteRangeAsync(del); + await _ckDetailRepo.HardDeleteRangeAsync(olds); + } + + var kucunDict = (await _kucunRepo.GetAllAsync()).ToDictionary(x => x.WzmlId); + + foreach (var d in dto.Details) + { + if (d.WzmlId == 0 && d.YlzkId == 0 && d.GyzkId == 0) throw new ServiceException("出库明细必须选择物资 / 庄口"); + + var bmds = d.Bangmadans ?? new List(); + foreach (var b in bmds) b.Jingzhong = Round2(b.Maozhong - b.Pizhong); + + var detail = new WmsChukuDetail + { + ChukuId = entity.Id, + WzmlId = d.WzmlId, YlzkId = d.YlzkId, GyzkId = d.GyzkId, DepId = d.DepId, + BiaobaoZhongliang = d.BiaobaoZhongliang, BaozhuangZhongliang = d.BaozhuangZhongliang, + Shuliang = d.Shuliang, Danjia = d.Danjia, KucunNum = d.KucunNum, + PiciCodes = d.PiciCodes ?? "", PiciNum = d.PiciNum, PcAndBjIds = d.PcAndBjIds ?? "", + Lingyongren = d.Lingyongren ?? "", + Banci = d.Banci ?? "", JidiWaigou = d.JidiWaigou ?? "" + }; + + if (bmds.Count > 0) + { + detail.Jianshu = bmds.Sum(b => b.Jianshu); + detail.Maozhong = Round2(bmds.Sum(b => b.Maozhong)); + detail.Pizhong = Round2(bmds.Sum(b => b.Pizhong)); + detail.Jingzhong = Round2(bmds.Sum(b => b.Jingzhong)); + } + else + { + detail.Jianshu = d.Jianshu; + detail.Maozhong = d.Maozhong; + detail.Pizhong = d.Pizhong; + detail.Jingzhong = d.Jingzhong; + } + if (detail.Jingzhong == 0) + detail.Jingzhong = Round2(detail.Maozhong - detail.Pizhong - detail.BaozhuangZhongliang); + + if (module == (int)Wms_Module_Enum.物资) + { + if (detail.Shuliang <= 0) throw new ServiceException("出库数量必须大于0"); + + // 库存校验 + 金额口径(F9 WJ_Chuku_Detail.CalJine) + kucunDict.TryGetValue(detail.WzmlId, out var kc); + var kcNum = kc?.Shuliang ?? d.KucunNum; + var kcJine = kc?.Jine ?? 0; + var kcDanjia = kc?.Danjia ?? 0; + detail.KucunNum = kcNum; + if (detail.Shuliang > kcNum) throw new ServiceException("库存数量不足"); + + if (detail.Shuliang == kcNum) + { + detail.Jine = kcJine; + detail.Danjia = detail.Shuliang != 0 ? Round4(detail.Jine / detail.Shuliang) : 0; + } + else + { + detail.Danjia = kcDanjia; + detail.Jine = Round2(detail.Shuliang * detail.Danjia); + } + + await ApplyWjKucunAsync(detail.WzmlId, -detail.Shuliang, -detail.Jine); + } + else + { + detail.Jine = d.Jine; + } + + await _ckDetailRepo.AddAsync(detail); + + foreach (var b in bmds) + { + await _bmdRepo.AddAsync(new WmsBangmadan + { + DetailId = detail.Id, IsChuku = true, Jianshu = b.Jianshu, + Maozhong = b.Maozhong, Pizhong = b.Pizhong, Jingzhong = b.Jingzhong, + GuobangShijian = b.GuobangShijian + }); + } + + // 生丝出库:按批次+包件更新(对齐 F9 SSCk_CreateCommand.Update_Ck) + if (module == (int)Wms_Module_Enum.生丝 && !string.IsNullOrWhiteSpace(detail.PcAndBjIds)) + await UpdatePiciChukuAsync(detail.PcAndBjIds, detail.Id, entity.Date); + } + + return entity.Id; + } + + public async Task DeleteChukuAsync(long id) + { + var chuku = await _ckRepo.GetByIdAsync(id); + if (chuku == null) throw new ServiceException("出库单不存在", 404); + + if (chuku.Module == Wms_Module_Enum.物资) await RollbackWjKucunAsync(id, false); + + var details = await _ckDetailRepo.FindAsync(x => x.ChukuId == id); + if (chuku.Module == Wms_Module_Enum.生丝) await RollbackPiciChukuAsync(details); + var bmds = await _bmdRepo.GetAllAsync(); + var del = bmds.Where(b => b.IsChuku && details.Any(d => d.Id == b.DetailId)).ToList(); + if (del.Count > 0) await _bmdRepo.HardDeleteRangeAsync(del); + if (details.Count > 0) await _ckDetailRepo.HardDeleteRangeAsync(details); + await _ckRepo.HardDeleteAsync(chuku); + } + + // ==================== 库存台账 ==================== + + public async Task> GetTzDatesAsync(int module, int kind) + { + var dates = await _tzDateRepo.GetAllAsync(); + var items = await _tzItemRepo.GetAllAsync(); + var countDict = items.GroupBy(x => x.TzDateId).ToDictionary(g => g.Key, g => g.Count()); + + return dates.Where(x => x.Module == (Wms_Module_Enum)module && x.Kind == kind) + .OrderByDescending(x => x.Date) + .Select(x => new WmsTzDateDto + { + Id = x.Id, Module = module, Kind = kind, Date = x.Date, LastDate = x.LastDate, + ItemCount = countDict.GetValueOrDefault(x.Id, 0) + }).ToList(); + } + + /// 读取(或生成)指定日期台账 + public async Task GetTaizhangAsync(int module, int kind, DateTime date, bool regenerate) + { + var tzDate = date.Date; + var existing = await _tzDateRepo.FirstOrDefaultAsync(x => x.Module == (Wms_Module_Enum)module + && x.Kind == kind && x.Date == tzDate); + + if (existing == null) + { + var latest = (await _tzDateRepo.GetAllAsync()) + .Where(x => x.Module == (Wms_Module_Enum)module && x.Kind == kind) + .OrderByDescending(x => x.Date).FirstOrDefault(); + if (latest != null && tzDate <= latest.Date) + throw new ServiceException($"只能生成晚于 {latest.Date:yyyy-MM-dd} 的台账"); + + // F9:无上一台账时 LastDate 取 1970-01-01,即首份台账汇总全部历史出入库 + var lastDate = latest?.Date ?? new DateTime(1970, 1, 1); + var generated = await GenerateItemsAsync(module, kind, tzDate, lastDate); + return new WmsTzEditDto + { + TzDateId = 0, Module = module, Kind = kind, Date = tzDate, LastDate = lastDate, Items = generated + }; + } + + if (regenerate) + { + var generated = await GenerateItemsAsync(module, kind, tzDate, existing.LastDate); + return new WmsTzEditDto + { + TzDateId = existing.Id, Module = module, Kind = kind, Date = tzDate, LastDate = existing.LastDate, Items = generated + }; + } + + var items = await _tzItemRepo.FindAsync(x => x.TzDateId == existing.Id); + return new WmsTzEditDto + { + TzDateId = existing.Id, Module = module, Kind = kind, Date = existing.Date, LastDate = existing.LastDate, + Items = items.OrderBy(x => x.Id).Select(ToTzItemDto).ToList() + }; + } + + /// + /// 生成台账:上日结存(上一台账中「件数或净重不为 0」的行)+ 区间入库/出库汇总 → 计算今日结存。 + /// 对齐 F9 YL_Kucun_Taizhang_Edit.GenerateTaizhang / CalTdayJiecun。 + /// + private async Task> GenerateItemsAsync(int module, int kind, DateTime tzDate, DateTime lastDate) + { + var from = kind == 1 ? lastDate : lastDate.AddDays(1); // 笼筒架台账含上日(F9 区间 Yesterday~Date) + var to = tzDate.AddDays(1).AddMilliseconds(-1); + + var result = new List(); + + var lastItems = await _tzItemRepo.GetAllAsync(); + var lastDict = lastItems.Where(x => x.Module == (Wms_Module_Enum)module && x.Kind == kind && x.Date == lastDate) + .ToList(); + foreach (var last in lastDict) + { + if (last.Tdayjingzhong != 0 || last.Tdayjianshu != 0) + { + result.Add(new WmsTzItemDto + { + ItemId = last.ItemId, SubKey = last.SubKey, ItemName = last.ItemName, ItemCode = last.ItemCode, + Ydayjianshu = last.Tdayjianshu, Ydayjingzhong = last.Tdayjingzhong, + Yangjianjianshu = last.Yangjianjianshu + }); + } + } + + var rukuSum = await SumRukuAsync(module, kind, from, to); + var chukuSum = await SumChukuAsync(module, kind, from, to); + + void Merge(List rows, bool isRuku) + { + foreach (var r in rows) + { + var hit = result.FirstOrDefault(x => x.ItemId == r.ItemId && x.SubKey == r.SubKey); + if (hit == null) + { + hit = new WmsTzItemDto { ItemId = r.ItemId, SubKey = r.SubKey }; + result.Add(hit); + } + if (isRuku) { hit.Tdayrukujianshu = r.Jianshu; hit.Tdayrukujingzhong = r.Jingzhong; } + else { hit.Tdaychukujianshu = r.Jianshu; hit.Tdaychukujingzhong = r.Jingzhong; } + } + } + Merge(rukuSum, true); + Merge(chukuSum, false); + + var nameDict = await BuildItemNameDictAsync(module, kind, result.Select(x => x.ItemId).Distinct().ToList()); + foreach (var item in result) + { + if (nameDict.TryGetValue(item.ItemId, out var nm)) { item.ItemName = nm.Name; item.ItemCode = nm.Code; } + CalTdayJiecun(item, module); + } + + return result.OrderBy(x => x.ItemName).ToList(); + } + + /// 今日结存 = 上日 + 入库 − 出库 + 盈亏;全部出库 = 上日有结存且今日结存为 0(对齐 F9 CalTdayJiecun) + private static void CalTdayJiecun(WmsTzItemDto item, int module) + { + item.Tdayjianshu = item.Ydayjianshu + item.Tdayrukujianshu - item.Tdaychukujianshu; + item.Tdayjingzhong = Round2(item.Ydayjingzhong + item.Tdayrukujingzhong - item.Tdaychukujingzhong + item.Yingkui); + if (module != (int)Wms_Module_Enum.物资) + item.AllChuku = item.Ydayjingzhong != 0 && item.Tdayjingzhong == 0; + } + + public async Task SaveTaizhangAsync(WmsTzSaveDto dto) + { + var module = dto.Module; + var tzDate = dto.Date.Date; + + if (dto.Items == null || dto.Items.Count == 0) throw new ServiceException("台账明细不能为空"); + + // 校验(对齐 F9 YL_Kucun_Taizhang_Edit 校验规则) + foreach (var it in dto.Items) + { + var expect = Round2(it.Ydayjingzhong + it.Tdayrukujingzhong - it.Tdaychukujingzhong + it.Yingkui); + if (expect != Round2(it.Tdayjingzhong)) + throw new ServiceException($"「{it.ItemName}」今日结存重量有误(应为 {expect})"); + if (it.Tdayjingzhong < 0) + throw new ServiceException($"「{it.ItemName}」结存重量不能小于0"); + if (module == (int)Wms_Module_Enum.原料 && it.AllChuku && it.Yangjianjianshu != 0) + throw new ServiceException($"「{it.ItemName}」全部出库时样茧件数必须为0"); + } + + WmsKcTzDate tz; + if (dto.TzDateId > 0) + { + tz = await _tzDateRepo.GetByIdAsync(dto.TzDateId); + if (tz == null) throw new ServiceException("台账不存在", 404); + } + else + { + var exist = await _tzDateRepo.FirstOrDefaultAsync(x => x.Module == (Wms_Module_Enum)module + && x.Kind == dto.Kind && x.Date == tzDate); + if (exist != null) throw new ServiceException("该日期台账已存在"); + + var latest = (await _tzDateRepo.GetAllAsync()) + .Where(x => x.Module == (Wms_Module_Enum)module && x.Kind == dto.Kind) + .OrderByDescending(x => x.Date).FirstOrDefault(); + if (latest != null && tzDate <= latest.Date) + throw new ServiceException($"只能生成晚于 {latest.Date:yyyy-MM-dd} 的台账"); + + tz = new WmsKcTzDate + { + Module = (Wms_Module_Enum)module, Kind = dto.Kind, Date = tzDate, + LastDate = latest?.Date ?? new DateTime(1970, 1, 1) + }; + await _tzDateRepo.AddAsync(tz); + } + + var olds = await _tzItemRepo.FindAsync(x => x.TzDateId == tz.Id); + if (olds.Count > 0) await _tzItemRepo.HardDeleteRangeAsync(olds); + + foreach (var it in dto.Items) + { + await _tzItemRepo.AddAsync(new WmsKcTzItem + { + TzDateId = tz.Id, Module = (Wms_Module_Enum)module, Kind = dto.Kind, Date = tzDate, + ItemId = it.ItemId, SubKey = it.SubKey ?? "", ItemName = it.ItemName ?? "", ItemCode = it.ItemCode ?? "", + Ydayjianshu = it.Ydayjianshu, Ydayjingzhong = it.Ydayjingzhong, + Tdayrukujianshu = it.Tdayrukujianshu, Tdayrukujingzhong = it.Tdayrukujingzhong, + Tdaychukujianshu = it.Tdaychukujianshu, Tdaychukujingzhong = it.Tdaychukujingzhong, + Yingkui = it.Yingkui, Tdayjianshu = it.Tdayjianshu, Tdayjingzhong = it.Tdayjingzhong, + Yangjianjianshu = it.Yangjianjianshu, AllChuku = it.AllChuku, Description = it.Description ?? "" + }); + } + + return tz.Id; + } + + public async Task DeleteTaizhangAsync(long id) + { + var tz = await _tzDateRepo.GetByIdAsync(id); + if (tz == null) throw new ServiceException("台账不存在", 404); + + var items = await _tzItemRepo.FindAsync(x => x.TzDateId == id); + if (items.Count > 0) await _tzItemRepo.HardDeleteRangeAsync(items); + await _tzDateRepo.HardDeleteAsync(tz); + } + + // ==================== 汇总辅助 ==================== + + private class SumRow + { + public long ItemId { get; set; } + public string SubKey { get; set; } = ""; + public decimal Jianshu { get; set; } + public decimal Jingzhong { get; set; } + } + + private long ItemKeyOf(WmsRukuDetail d, int module, int kind) + => IsYlzkItem(module, kind) ? d.YlzkId : (IsGyzkItem(module, kind) ? d.GyzkId : d.WzmlId); + + private string SubKeyOf(WmsRukuDetail d, int module) + => module == (int)Wms_Module_Enum.生丝 ? (d.JidiWaigou ?? "") : ""; + + private long ItemKeyOf(WmsChukuDetail d, int module, int kind) + => IsYlzkItem(module, kind) ? d.YlzkId : (IsGyzkItem(module, kind) ? d.GyzkId : d.WzmlId); + + private string SubKeyOf(WmsChukuDetail d, int module) + => module == (int)Wms_Module_Enum.生丝 ? (d.JidiWaigou ?? "") : ""; + + private async Task> SumRukuAsync(int module, int kind, DateTime from, DateTime to) + { + var rukus = (await _rkRepo.GetAllAsync()) + .Where(x => x.Module == (Wms_Module_Enum)module && x.Date >= from && x.Date <= to).ToList(); + if (rukus.Count == 0) return new List(); + var ids = rukus.Select(x => x.Id).ToHashSet(); + var details = (await _rkDetailRepo.GetAllAsync()).Where(x => ids.Contains(x.RukuId)).ToList(); + + if (IsGyzkItem(module, kind)) + { + var ltjWzmlId = await GetLtjWzmlIdAsync(); + if (ltjWzmlId > 0) details = details.Where(x => x.WzmlId == ltjWzmlId).ToList(); + } + + return details.GroupBy(d => new { Key = ItemKeyOf(d, module, kind), Sub = SubKeyOf(d, module) }) + .Select(g => new SumRow + { + ItemId = g.Key.Key, + SubKey = g.Key.Sub, + Jianshu = module == (int)Wms_Module_Enum.物资 ? g.Sum(x => x.Shuliang) + : module == (int)Wms_Module_Enum.生丝 ? g.Sum(x => x.PiciNum) + : g.Sum(x => x.Jianshu), + Jingzhong = module == (int)Wms_Module_Enum.物资 ? Round2(g.Sum(x => x.Jine)) : Round2(g.Sum(x => x.Jingzhong)) + }) + .Where(x => x.ItemId != 0) + .ToList(); + } + + private async Task> SumChukuAsync(int module, int kind, DateTime from, DateTime to) + { + var chukus = (await _ckRepo.GetAllAsync()) + .Where(x => x.Module == (Wms_Module_Enum)module && x.Date >= from && x.Date <= to).ToList(); + if (chukus.Count == 0) return new List(); + var ids = chukus.Select(x => x.Id).ToHashSet(); + var details = (await _ckDetailRepo.GetAllAsync()).Where(x => ids.Contains(x.ChukuId)).ToList(); + + if (IsGyzkItem(module, kind)) + { + var ltjWzmlId = await GetLtjWzmlIdAsync(); + if (ltjWzmlId > 0) details = details.Where(x => x.WzmlId == ltjWzmlId).ToList(); + } + + return details.GroupBy(d => new { Key = ItemKeyOf(d, module, kind), Sub = SubKeyOf(d, module) }) + .Select(g => new SumRow + { + ItemId = g.Key.Key, + SubKey = g.Key.Sub, + Jianshu = module == (int)Wms_Module_Enum.物资 ? g.Sum(x => x.Shuliang) + : module == (int)Wms_Module_Enum.生丝 ? g.Sum(x => x.PiciNum) + : g.Sum(x => x.Jianshu), + Jingzhong = module == (int)Wms_Module_Enum.物资 ? Round2(g.Sum(x => x.Jine)) : Round2(g.Sum(x => x.Jingzhong)) + }) + .Where(x => x.ItemId != 0) + .ToList(); + } + + /// 笼筒架台账只统计「联统茧」物资(注册表「仓储\联统茧物资ID」) + private async Task GetLtjWzmlIdAsync() + { + var wzs = await _wzRepo.GetAllAsync(); + var hit = wzs.FirstOrDefault(x => x.Name.Contains("联统")); + return hit?.Id ?? 0; + } + + private async Task> BuildItemNameDictAsync(int module, int kind, List ids) + { + var dict = new Dictionary(); + if (IsYlzkItem(module, kind)) + { + foreach (var x in await _ylzkRepo.GetAllAsync()) dict[x.Id] = (x.Name, x.Code); + } + else if (IsGyzkItem(module, kind)) + { + foreach (var x in await _gyzkRepo.GetAllAsync()) dict[x.Id] = (x.ZhuangKouName, ""); + } + else + { + foreach (var x in await _wzRepo.GetAllAsync()) dict[x.Id] = (x.Name, x.Code); + } + return dict; + } + + private static WmsTzItemDto ToTzItemDto(WmsKcTzItem x) => new() + { + Id = x.Id, TzDateId = x.TzDateId, ItemId = x.ItemId, SubKey = x.SubKey, + ItemName = x.ItemName, ItemCode = x.ItemCode, + Ydayjianshu = x.Ydayjianshu, Ydayjingzhong = x.Ydayjingzhong, + Tdayrukujianshu = x.Tdayrukujianshu, Tdayrukujingzhong = x.Tdayrukujingzhong, + Tdaychukujianshu = x.Tdaychukujianshu, Tdaychukujingzhong = x.Tdaychukujingzhong, + Yingkui = x.Yingkui, Tdayjianshu = x.Tdayjianshu, Tdayjingzhong = x.Tdayjingzhong, + Yangjianjianshu = x.Yangjianjianshu, AllChuku = x.AllChuku, Description = x.Description + }; + + // ==================== 物资库存 / 出入库流水 ==================== + + /// 物资模块库存:按物资目录汇总(入库 − 出库) + public async Task> GetKucunAsync(int module, string like) + { + var wzFull = await WzFullDictAsync(); + var result = new List(); + + if (module == (int)Wms_Module_Enum.物资) + { + var kcs = await _kucunRepo.GetAllAsync(); + foreach (var kc in kcs) + { + wzFull.TryGetValue(kc.WzmlId, out var wz); + result.Add(new WmsKucunDto + { + WzmlId = kc.WzmlId, WzmlName = wz.Name ?? "", WzmlCode = wz.Code ?? "", + Unit = wz.Unit ?? "", Guige = wz.Guige ?? "", + Shuliang = kc.Shuliang, Danjia = kc.Danjia, Jine = kc.Jine + }); + } + } + else + { + // 其余模块:用最近一次台账的「今日结存」作为库存 + var dates = (await _tzDateRepo.GetAllAsync()) + .Where(x => x.Module == (Wms_Module_Enum)module && x.Kind == 0) + .OrderByDescending(x => x.Date).FirstOrDefault(); + if (dates != null) + { + var items = await _tzItemRepo.FindAsync(x => x.TzDateId == dates.Id); + foreach (var it in items.Where(x => x.Tdayjianshu != 0 || x.Tdayjingzhong != 0)) + { + wzFull.TryGetValue(it.ItemId, out var wz); + result.Add(new WmsKucunDto + { + WzmlId = it.ItemId, WzmlName = it.ItemName, WzmlCode = it.ItemCode, + Unit = wz.Unit ?? "", Guige = wz.Guige ?? "", + Shuliang = it.Tdayjianshu, Jine = it.Tdayjingzhong + }); + } + } + } + + if (!string.IsNullOrWhiteSpace(like)) + { + var key = like.Trim(); + result = result.Where(x => x.WzmlName.Contains(key) || x.WzmlCode.Contains(key)).ToList(); + } + return result.OrderBy(x => x.WzmlCode).ToList(); + } + + /// 出入库流水(物资模块) + public async Task> GetCrkAsync(int module, DateTime start, DateTime end, long? wzmlId) + { + var wzFull = await WzFullDictAsync(); + var list = new List(); + + var rukus = (await _rkRepo.GetAllAsync()) + .Where(x => x.Module == (Wms_Module_Enum)module && x.Date.Date >= start.Date && x.Date.Date <= end.Date).ToList(); + var rkIds = rukus.Select(x => x.Id).ToHashSet(); + var rkDetails = (await _rkDetailRepo.GetAllAsync()).Where(x => rkIds.Contains(x.RukuId)).ToList(); + var rkDict = rukus.ToDictionary(x => x.Id); + + foreach (var d in rkDetails) + { + if (wzmlId.HasValue && d.WzmlId != wzmlId.Value) continue; + wzFull.TryGetValue(d.WzmlId, out var wz); + rkDict.TryGetValue(d.RukuId, out var rk); + list.Add(new WmsCrkDto + { + Date = rk?.Date ?? default, IsRuku = true, Code = rk?.Code ?? "", + TypeName = RkTypeName(module, rk?.Type ?? 0), + WzmlId = d.WzmlId, WzmlName = wz.Name ?? "", WzmlCode = wz.Code ?? "", Unit = wz.Unit ?? "", + Shuliang = d.Shuliang, Danjia = d.Danjia, Jine = d.Jine, Description = "" + }); + } + + var chukus = (await _ckRepo.GetAllAsync()) + .Where(x => x.Module == (Wms_Module_Enum)module && x.Date.Date >= start.Date && x.Date.Date <= end.Date).ToList(); + var ckIds = chukus.Select(x => x.Id).ToHashSet(); + var ckDetails = (await _ckDetailRepo.GetAllAsync()).Where(x => ckIds.Contains(x.ChukuId)).ToList(); + var ckDict = chukus.ToDictionary(x => x.Id); + + foreach (var d in ckDetails) + { + if (wzmlId.HasValue && d.WzmlId != wzmlId.Value) continue; + wzFull.TryGetValue(d.WzmlId, out var wz); + ckDict.TryGetValue(d.ChukuId, out var ck); + list.Add(new WmsCrkDto + { + Date = ck?.Date ?? default, IsRuku = false, Code = ck?.Code ?? "", + TypeName = CkTypeName(module, ck?.Type ?? 0), + WzmlId = d.WzmlId, WzmlName = wz.Name ?? "", WzmlCode = wz.Code ?? "", Unit = wz.Unit ?? "", + Shuliang = -d.Shuliang, Danjia = d.Danjia, Jine = -d.Jine, Description = "" + }); + } + + return list.OrderByDescending(x => x.Date).ThenBy(x => x.Code).ToList(); + } + + /// 物资库存增减(入库为正、出库为负) + private async Task ApplyWjKucunAsync(long wzmlId, decimal shuliang, decimal jine) + { + if (wzmlId == 0) return; + var kc = await _kucunRepo.FirstOrDefaultAsync(x => x.WzmlId == wzmlId); + if (kc == null) + { + kc = new WmsKucun { WzmlId = wzmlId }; + await _kucunRepo.AddAsync(kc); + } + // 出库前已做库存校验,正常业务不会出现负数;删除单据回滚时可能出现负数, + // 这里不做截断,保证「入库回滚」与「出库回滚」严格对称,不会造成库存漂移。 + kc.Shuliang = Round4(kc.Shuliang + shuliang); + kc.Jine = Round2(kc.Jine + jine); + kc.Danjia = kc.Shuliang != 0 ? Round4(kc.Jine / kc.Shuliang) : 0; + await _kucunRepo.UpdateAsync(kc); + } + + /// 回滚单据对物资库存的影响(修改 / 删除前调用) + private async Task RollbackWjKucunAsync(long billId, bool isRuku) + { + if (isRuku) + { + var details = await _rkDetailRepo.FindAsync(x => x.RukuId == billId); + foreach (var d in details) await ApplyWjKucunAsync(d.WzmlId, -d.Shuliang, -d.Jine); + } + else + { + var details = await _ckDetailRepo.FindAsync(x => x.ChukuId == billId); + foreach (var d in details) await ApplyWjKucunAsync(d.WzmlId, d.Shuliang, d.Jine); + } + } + + // ==================== 生丝批次 / 包件 ==================== + + public async Task> GetPiciPagedAsync(DateTime? start, DateTime? end, int kcStatus, string like, int pageIndex, int pageSize) + { + var all = await _piciRepo.GetAllAsync(); + var q = all.AsQueryable(); + if (start.HasValue) q = q.Where(x => x.Date.Date >= start.Value.Date); + if (end.HasValue) q = q.Where(x => x.Date.Date <= end.Value.Date); + if (kcStatus > 0) q = q.Where(x => x.KcStatus == kcStatus); + + var wzDict = await WzDictAsync(); + var gyzkDict = (await _gyzkRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.ZhuangKouName); + var depDict = (await _depRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var baojianDict = (await _baojianRepo.GetAllAsync()).GroupBy(x => x.PiciId) + .ToDictionary(g => g.Key, g => g.Count()); + + if (!string.IsNullOrWhiteSpace(like)) + { + var key = like.Trim(); + q = q.Where(x => x.Code.Contains(key) || x.Guige.Contains(key)); + } + + var total = q.LongCount(); + var items = q.OrderByDescending(x => x.Date).ThenByDescending(x => x.Code) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + return new PageResult + { + Items = items.Select(x => new WmsPiciListDto + { + Id = x.Id, Code = x.Code, CodeType = x.CodeType, Year = x.Year, Date = x.Date, ScDate = x.ScDate, + GyzkId = x.GyzkId, GyzkName = gyzkDict.GetValueOrDefault(x.GyzkId, ""), + WzmlId = x.WzmlId, WzmlName = wzDict.GetValueOrDefault(x.WzmlId, ""), + DepId = x.DepId, DepName = depDict.GetValueOrDefault(x.DepId, ""), + Guige = x.Guige, JidiWaigou = x.JidiWaigou, YoujiPutong = x.YoujiPutong, + Baoshu = x.Baoshu, StartBaojianNum = x.StartBaojianNum, + Maozhong = x.Maozhong, Pizhong = x.Pizhong, Jingzhong = x.Jingzhong, Gongliang = x.Gongliang, + KcStatus = x.KcStatus, KcStatusName = KcStatusName(x.KcStatus), + RkItemId = x.RkItemId, RkDate = x.RkDate, + CkLastDate = x.CkLastDate, CkBaoshu = x.CkBaoshu, CkJingzhong = x.CkJingzhong, KcBaoshu = x.KcBaoshu, + Dengji = x.Dengji, Qingjie = x.Qingjie, Jiejing = x.Jiejing, + Piancha = x.Piancha, Zongcha = x.Zongcha, Qieduan = x.Qieduan, + Description = x.Description, BaojianNum = baojianDict.GetValueOrDefault(x.Id, 0) + }).ToList(), + Total = total, PageIndex = pageIndex, PageSize = pageSize + }; + } + + public async Task GetPiciAsync(long id) + { + var pici = await _piciRepo.GetByIdAsync(id); + if (pici == null) throw new ServiceException("批次不存在", 404); + var baojians = await _baojianRepo.FindAsync(x => x.PiciId == id); + + return new WmsPiciSaveDto + { + Id = pici.Id, Code = pici.Code, CodeType = pici.CodeType, Date = pici.Date, ScDate = pici.ScDate, + GyzkId = pici.GyzkId, WzmlId = pici.WzmlId, DepId = pici.DepId, Guige = pici.Guige, + JidiWaigou = pici.JidiWaigou, YoujiPutong = pici.YoujiPutong, + Baoshu = pici.Baoshu, StartBaojianNum = pici.StartBaojianNum, + Maozhong = pici.Maozhong, Pizhong = pici.Pizhong, Jingzhong = pici.Jingzhong, Gongliang = pici.Gongliang, + KcStatus = pici.KcStatus, Dengji = pici.Dengji, Qingjie = pici.Qingjie, Jiejing = pici.Jiejing, + Piancha = pici.Piancha, Zongcha = pici.Zongcha, Qieduan = pici.Qieduan, Description = pici.Description, + Baojians = baojians.OrderBy(x => x.Baojianhao).Select(b => new WmsPiciBaojianDto + { + Id = b.Id, PiciId = b.PiciId, Baojianhao = b.Baojianhao, + Maozhong = b.Maozhong, Pizhong = b.Pizhong, Jingzhong = b.Jingzhong, + Gongliang = b.Gongliang, Huichaolv = b.Huichaolv, + CkIsck = b.CkIsck, CkItemId = b.CkItemId, CkDate = b.CkDate, CkJingzhong = b.CkJingzhong, + Description = b.Description + }).ToList() + }; + } + + public async Task SavePiciAsync(WmsPiciSaveDto dto) + { + if (string.IsNullOrWhiteSpace(dto.Code)) throw new ServiceException("请填写批号"); + + // 批号格式校验(对齐 F9 Page_PiciBaojian_Edit):正品 10 位数字;其他 31EE-7 位数字 + var codeOk = dto.CodeType == 0 + ? System.Text.RegularExpressions.Regex.IsMatch(dto.Code.Trim(), @"^\d{10}$") + : System.Text.RegularExpressions.Regex.IsMatch(dto.Code.Trim(), @"^31EE-\d{7}$"); + if (!codeOk) + throw new ServiceException("编号格式错误:正品编号类型 示例6100000001;其他编号类型 示例31EE-2600001"); + + WmsPici entity; + var isNew = dto.Id == 0; + if (isNew) + { + var exist = await _piciRepo.FirstOrDefaultAsync(x => x.Code == dto.Code); + if (exist != null) throw new ServiceException("批号已存在"); + entity = new WmsPici(); + } + else + { + entity = await _piciRepo.GetByIdAsync(dto.Id); + if (entity == null) throw new ServiceException("批次不存在", 404); + } + + entity.Code = dto.Code.Trim(); + entity.CodeType = dto.CodeType; + entity.Date = dto.Date.Date; + entity.Year = dto.Date.Year; + entity.ScDate = dto.ScDate; + entity.GyzkId = dto.GyzkId; + entity.WzmlId = dto.WzmlId; + entity.DepId = dto.DepId; + entity.Guige = dto.Guige ?? ""; + entity.JidiWaigou = dto.JidiWaigou ?? ""; + entity.YoujiPutong = dto.YoujiPutong ?? ""; + entity.Baoshu = dto.Baoshu; + entity.StartBaojianNum = dto.StartBaojianNum; + entity.Dengji = dto.Dengji ?? ""; + entity.Qingjie = dto.Qingjie; + entity.Jiejing = dto.Jiejing; + entity.Piancha = dto.Piancha; + entity.Zongcha = dto.Zongcha; + entity.Qieduan = dto.Qieduan; + entity.Description = dto.Description ?? ""; + + // 包件:净重 = 毛重 − 皮重;公量 = 净重 × (1 + 11%) / (1 + 回潮率%) + var baojians = dto.Baojians ?? new List(); + foreach (var b in baojians) + { + b.Jingzhong = Round2(b.Maozhong - b.Pizhong); + b.Gongliang = Round2(b.Jingzhong * 1.11m / (1 + b.Huichaolv / 100m)); + } + + if (baojians.Count > 0) + { + entity.Maozhong = Round2(baojians.Sum(b => b.Maozhong)); + entity.Pizhong = Round2(baojians.Sum(b => b.Pizhong)); + entity.Jingzhong = Round2(baojians.Sum(b => b.Jingzhong)); + entity.Gongliang = Round2(baojians.Sum(b => b.Gongliang)); + entity.Baoshu = baojians.Count; + } + else + { + entity.Maozhong = dto.Maozhong; + entity.Pizhong = dto.Pizhong; + entity.Jingzhong = dto.Jingzhong; + entity.Gongliang = dto.Gongliang; + } + + if (entity.KcStatus == 0) entity.KcStatus = 1; // 待入库 + + if (isNew) await _piciRepo.AddAsync(entity); else await _piciRepo.UpdateAsync(entity); + + var olds = await _baojianRepo.FindAsync(x => x.PiciId == entity.Id); + if (olds.Count > 0) await _baojianRepo.HardDeleteRangeAsync(olds); + foreach (var b in baojians) + { + await _baojianRepo.AddAsync(new WmsPiciBaojian + { + PiciId = entity.Id, Baojianhao = b.Baojianhao, + Maozhong = b.Maozhong, Pizhong = b.Pizhong, Jingzhong = b.Jingzhong, + Gongliang = b.Gongliang, Huichaolv = b.Huichaolv, Description = b.Description ?? "" + }); + } + + return entity.Id; + } + + public async Task DeletePiciAsync(long id) + { + var pici = await _piciRepo.GetByIdAsync(id); + if (pici == null) throw new ServiceException("批次不存在", 404); + var baojians = await _baojianRepo.FindAsync(x => x.PiciId == id); + if (baojians.Count > 0) await _baojianRepo.HardDeleteRangeAsync(baojians); + // 物理删除:批号有唯一索引,软删残留会阻止重新创建同号批次 + await _piciRepo.HardDeleteAsync(pici); + } + + // ==================== 生丝批次:入库 / 出库状态流转(对齐 F9 SSRk/SSCk CQRS) ==================== + + private class PcAndBjRef { public long PiciId { get; set; } public List PiciBaojianIds { get; set; } = new(); } + + /// 入库:按批次ID把批次置为「已入库」(对齐 F9 SSRk_CreateCommand.UpdateRk) + private async Task UpdatePiciRukuAsync(string piciIds, long rkItemId, DateTime rkDate) + { + foreach (var pid in SplitLongs(piciIds)) + { + var pici = await _piciRepo.GetByIdAsync(pid); + if (pici == null) continue; + pici.RkItemId = rkItemId; + pici.RkDate = rkDate; + pici.KcStatus = 2; // 已入库 + pici.KcBaoshu = pici.Baoshu; + pici.CkBaoshu = 0; + pici.CkJingzhong = 0; + await _piciRepo.UpdateAsync(pici); + } + } + + /// 入库回滚:把已入库且未出库的批次退回「待入库」(编辑/删除入库单时调用) + private async Task RollbackPiciRukuAsync(List details) + { + var ids = details.Where(d => !string.IsNullOrWhiteSpace(d.PiciIds)) + .SelectMany(d => SplitLongs(d.PiciIds)).Distinct().ToList(); + foreach (var pid in ids) + { + var pici = await _piciRepo.GetByIdAsync(pid); + if (pici == null) continue; + if (pici.CkBaoshu > 0 || pici.KcStatus == 3 || pici.KcStatus == 4) + throw new ServiceException($"批次「{pici.Code}」已出库,不能修改/删除入库单"); + pici.RkItemId = 0; + pici.RkDate = null; + pici.KcStatus = 1; // 待入库 + pici.KcBaoshu = 0; + await _piciRepo.UpdateAsync(pici); + } + } + + /// 出库:按「批次+包件」标记包件已出库并更新批次库存状态(对齐 F9 SSCk_CreateCommand.Update_Ck) + private async Task UpdatePiciChukuAsync(string pcAndBjIds, long ckItemId, DateTime ckDate) + { + List refs; + try + { + refs = System.Text.Json.JsonSerializer.Deserialize>(pcAndBjIds, + new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new(); + } + catch { refs = new(); } + + var ckDate0 = ckDate.Date; + foreach (var r in refs) + { + if (r.PiciBaojianIds == null || r.PiciBaojianIds.Count == 0) continue; + var pici = await _piciRepo.GetByIdAsync(r.PiciId); + if (pici == null) continue; + + // 校验(对齐 F9):待入库不可出库;已全部出库不可再出库 + if (pici.KcStatus == 1) throw new ServiceException($"批次「{pici.Code}」尚未入库,不能出库"); + if (pici.KcStatus == 4) throw new ServiceException($"批次「{pici.Code}」已全部出库"); + + var baojians = await _baojianRepo.FindAsync(x => x.PiciId == r.PiciId); + var selected = baojians.Where(x => r.PiciBaojianIds.Contains(x.Id)).ToList(); + if (selected.Count == 0) continue; + + foreach (var bj in selected) + { + if (bj.CkIsck) continue; + bj.CkIsck = true; + bj.CkItemId = ckItemId; + bj.CkDate = ckDate0; + bj.CkJingzhong = bj.Jingzhong; + await _baojianRepo.UpdateAsync(bj); + } + + var ckBaoshu = selected.Count; + var ckJingzhong = Round2(selected.Sum(x => x.Jingzhong)); + + await _piciChukuRepo.AddAsync(new WmsPiciChuku + { + PiciId = pici.Id, CkItemId = ckItemId, CkBaoshu = ckBaoshu, + CkDate = ckDate0, CkJingzhong = ckJingzhong + }); + + pici.CkItemId = ckItemId; + pici.CkLastDate = ckDate0; + pici.CkBaoshu += ckBaoshu; + pici.CkJingzhong = Round2(pici.CkJingzhong + ckJingzhong); + pici.KcBaoshu = pici.Baoshu - pici.CkBaoshu; + pici.KcStatus = pici.CkBaoshu >= pici.Baoshu ? 4 : 3; // 全部出库 / 部分出库 + await _piciRepo.UpdateAsync(pici); + } + } + + /// 出库回滚:撤销包件出库标记并回退批次库存状态(编辑/删除出库单时调用) + private async Task RollbackPiciChukuAsync(List details) + { + var itemIds = details.Select(d => d.Id).ToList(); + if (itemIds.Count == 0) return; + + var bjs = await _baojianRepo.GetAllAsync(); + var hitBjs = bjs.Where(x => itemIds.Contains(x.CkItemId)).ToList(); + foreach (var bj in hitBjs) + { + bj.CkIsck = false; + bj.CkItemId = 0; + bj.CkDate = null; + bj.CkJingzhong = 0; + await _baojianRepo.UpdateAsync(bj); + } + + var chukus = await _piciChukuRepo.GetAllAsync(); + var hitCks = chukus.Where(x => itemIds.Contains(x.CkItemId)).ToList(); + if (hitCks.Count > 0) await _piciChukuRepo.HardDeleteRangeAsync(hitCks); + + // 按批次重算库存状态 + var piciIds = hitCks.Select(x => x.PiciId).Distinct().ToList(); + foreach (var pid in piciIds) + { + var pici = await _piciRepo.GetByIdAsync(pid); + if (pici == null) continue; + var remain = chukus.Where(x => x.PiciId == pid && !hitCks.Contains(x)).ToList(); + pici.CkBaoshu = remain.Sum(x => x.CkBaoshu); + pici.CkJingzhong = Round2(remain.Sum(x => x.CkJingzhong)); + pici.KcBaoshu = pici.Baoshu - pici.CkBaoshu; + pici.CkItemId = remain.Count > 0 ? remain.Max(x => x.CkItemId) : 0; + pici.CkLastDate = remain.Count > 0 ? (remain.Max(x => x.CkDate) ?? default) : default; + pici.KcStatus = pici.CkBaoshu == 0 ? 2 : (pici.CkBaoshu >= pici.Baoshu ? 4 : 3); + await _piciRepo.UpdateAsync(pici); + } + } + + private static List SplitLongs(string s) + => (s ?? "").Split(new[] { ',', ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => { long.TryParse(x.Trim(), out var v); return v; }) + .Where(v => v > 0).Distinct().ToList(); + + /// 生丝出库选择批次+包件(对齐 F9 SSPici_GetChukuSelectAsync) + public async Task> GetPiciChukuSelectAsync(long wzmlId, string excludePiciIds) + { + var all = await _piciRepo.GetAllAsync(); + var q = all.Where(x => x.KcStatus == 2 || x.KcStatus == 3); // 已入库 / 部分出库 + if (wzmlId > 0) q = q.Where(x => x.WzmlId == wzmlId); + var excl = SplitLongs(excludePiciIds); + if (excl.Count > 0) q = q.Where(x => !excl.Contains(x.Id)); + + var wzDict = await WzDictAsync(); + var gyzkDict = (await _gyzkRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.ZhuangKouName); + var depDict = (await _depRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var baojians = await _baojianRepo.GetAllAsync(); + + return q.OrderByDescending(x => x.Date).ThenBy(x => x.Code).Select(p => + { + var bjs = baojians.Where(b => b.PiciId == p.Id).OrderBy(b => b.Baojianhao).ToList(); + var kcBjs = bjs.Where(b => !b.CkIsck).ToList(); + return new WmsPiciChukuSelectDto + { + PiciId = p.Id, PiciCode = p.Code, Date = p.Date, Guige = p.Guige, + WzmlName = wzDict.GetValueOrDefault(p.WzmlId, ""), + GyzkName = gyzkDict.GetValueOrDefault(p.GyzkId, ""), + DepName = depDict.GetValueOrDefault(p.DepId, ""), + JidiWaigou = p.JidiWaigou, + Baoshu = p.Baoshu, KcBaoshu = kcBjs.Count, + Jingzhong = p.Jingzhong, KcJingzhong = Round2(kcBjs.Sum(x => x.Jingzhong)), + Description = p.Description, + Baojians = bjs.Select(b => new WmsPiciBaojianDto + { + Id = b.Id, PiciId = b.PiciId, Baojianhao = b.Baojianhao, + Maozhong = b.Maozhong, Pizhong = b.Pizhong, Jingzhong = b.Jingzhong, + Gongliang = b.Gongliang, Huichaolv = b.Huichaolv, + CkIsck = b.CkIsck, CkItemId = b.CkItemId, CkDate = b.CkDate, CkJingzhong = b.CkJingzhong, + Description = b.Description + }).ToList() + }; + }).ToList(); + } + + /// 读取批次质检 + public async Task GetPiciQcAsync(long piciId) + { + var qc = await _piciQcRepo.FirstOrDefaultAsync(x => x.PiciId == piciId); + return qc == null ? new WmsPiciQcDto { PiciId = piciId } : ToQcDto(qc); + } + + public async Task SavePiciQcAsync(WmsPiciQcDto dto) + { + if (dto.PiciId <= 0) throw new ServiceException("批次无效"); + var qc = await _piciQcRepo.FirstOrDefaultAsync(x => x.PiciId == dto.PiciId); + var isNew = qc == null; + if (isNew) qc = new WmsPiciQc { PiciId = dto.PiciId }; + + qc.Pingwei = dto.Pingwei ?? ""; qc.Seze = dto.Seze ?? ""; + qc.AvgHuichaolv = dto.AvgHuichaolv; qc.AvgGongliangXiandu = dto.AvgGongliangXiandu; + qc.XianduPiancha = dto.XianduPiancha; qc.XianduZdpiancha = dto.XianduZdpiancha; + qc.Qingjie = dto.Qingjie; qc.Jiejing = dto.Jiejing; + qc.JunyunYd = dto.JunyunYd; qc.JunyunEd = dto.JunyunEd; qc.JunyunSd = dto.JunyunSd; + qc.Qieduan = dto.Qieduan; + qc.DuanlieQiangdu = dto.DuanlieQiangdu ?? ""; qc.DuanlieShenchanglv = dto.DuanlieShenchanglv ?? ""; + qc.Baohe = dto.Baohe; + qc.WgWg = dto.WgWg ?? ""; qc.WgYanse = dto.WgYanse ?? ""; qc.WgGuangze = dto.WgGuangze ?? ""; qc.WgShougan = dto.WgShougan ?? ""; + qc.QjZhuyao = dto.QjZhuyao; qc.QjCiyaoFeisi = dto.QjCiyaoFeisi; qc.QjCiyaoDacao = dto.QjCiyaoDacao; + qc.QjCiyaoNianfucao = dto.QjCiyaoNianfucao; qc.QjCiyaoDachangjie = dto.QjCiyaoDachangjie; + qc.QjCiyaoZhongluoxuan = dto.QjCiyaoZhongluoxuan; + qc.QjPutongXiaocao = dto.QjPutongXiaocao; qc.QjPutongChangjie = dto.QjPutongChangjie; + qc.QjPutongLuoxuan = dto.QjPutongLuoxuan; qc.QjPutongHuanjie = dto.QjPutongHuanjie; + qc.Jj100 = dto.Jj100; qc.Jj95 = dto.Jj95; qc.Jj90 = dto.Jj90; qc.Jj85 = dto.Jj85; qc.Jj80 = dto.Jj80; + qc.Jj75 = dto.Jj75; qc.Jj70 = dto.Jj70; qc.Jj65 = dto.Jj65; qc.Jj60 = dto.Jj60; qc.Jj55 = dto.Jj55; + qc.Jj50 = dto.Jj50; qc.Jj45 = dto.Jj45; + qc.JjYuanyin = dto.JjYuanyin ?? ""; qc.Description = dto.Description ?? ""; + + if (isNew) await _piciQcRepo.AddAsync(qc); else await _piciQcRepo.UpdateAsync(qc); + } + + private static WmsPiciQcDto ToQcDto(WmsPiciQc qc) => new() + { + Id = qc.Id, PiciId = qc.PiciId, Pingwei = qc.Pingwei, Seze = qc.Seze, + AvgHuichaolv = qc.AvgHuichaolv, AvgGongliangXiandu = qc.AvgGongliangXiandu, + XianduPiancha = qc.XianduPiancha, XianduZdpiancha = qc.XianduZdpiancha, + Qingjie = qc.Qingjie, Jiejing = qc.Jiejing, + JunyunYd = qc.JunyunYd, JunyunEd = qc.JunyunEd, JunyunSd = qc.JunyunSd, Qieduan = qc.Qieduan, + DuanlieQiangdu = qc.DuanlieQiangdu, DuanlieShenchanglv = qc.DuanlieShenchanglv, Baohe = qc.Baohe, + WgWg = qc.WgWg, WgYanse = qc.WgYanse, WgGuangze = qc.WgGuangze, WgShougan = qc.WgShougan, + QjZhuyao = qc.QjZhuyao, QjCiyaoFeisi = qc.QjCiyaoFeisi, QjCiyaoDacao = qc.QjCiyaoDacao, + QjCiyaoNianfucao = qc.QjCiyaoNianfucao, QjCiyaoDachangjie = qc.QjCiyaoDachangjie, + QjCiyaoZhongluoxuan = qc.QjCiyaoZhongluoxuan, + QjPutongXiaocao = qc.QjPutongXiaocao, QjPutongChangjie = qc.QjPutongChangjie, + QjPutongLuoxuan = qc.QjPutongLuoxuan, QjPutongHuanjie = qc.QjPutongHuanjie, + Jj100 = qc.Jj100, Jj95 = qc.Jj95, Jj90 = qc.Jj90, Jj85 = qc.Jj85, Jj80 = qc.Jj80, + Jj75 = qc.Jj75, Jj70 = qc.Jj70, Jj65 = qc.Jj65, Jj60 = qc.Jj60, Jj55 = qc.Jj55, + Jj50 = qc.Jj50, Jj45 = qc.Jj45, JjYuanyin = qc.JjYuanyin, Description = qc.Description + }; + + /// 读取批次纤度检验 + public async Task> GetPiciQcXdjyAsync(long piciId) + { + var list = await _piciQcXdjyRepo.FindAsync(x => x.PiciId == piciId); + return list.OrderBy(x => x.Id).Select(x => new WmsPiciQcXdjyDto + { Id = x.Id, PiciId = x.PiciId, Xiandu = x.Xiandu, Num = x.Num }).ToList(); + } + + public async Task SavePiciQcXdjyAsync(List items) + { + var piciId = items?.FirstOrDefault()?.PiciId ?? 0; + if (piciId <= 0) throw new ServiceException("批次无效"); + var olds = await _piciQcXdjyRepo.FindAsync(x => x.PiciId == piciId); + if (olds.Count > 0) await _piciQcXdjyRepo.HardDeleteRangeAsync(olds); + foreach (var it in items ?? new List()) + { + await _piciQcXdjyRepo.AddAsync(new WmsPiciQcXdjy + { PiciId = piciId, Xiandu = it.Xiandu, Num = it.Num }); + } + } + + // ==================== 原料采购 ==================== + + public async Task> GetCaigouAsync(DateTime? start, DateTime? end) + { + var all = await _caigouRepo.GetAllAsync(); + var q = all.AsQueryable(); + if (start.HasValue) q = q.Where(x => x.Date.Date >= start.Value.Date); + if (end.HasValue) q = q.Where(x => x.Date.Date <= end.Value.Date); + + var gysDict = (await _gysRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var ylzkDict = (await _ylzkRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + + return q.OrderByDescending(x => x.Date).ThenByDescending(x => x.Code).Select(x => new WmsCaigouDto + { + Id = x.Id, Date = x.Date, Code = x.Code, + GysId = x.GysId, GysName = gysDict.GetValueOrDefault(x.GysId, ""), + YlzkId = x.YlzkId, YlzkName = ylzkDict.GetValueOrDefault(x.YlzkId, ""), + Jianshu = x.Jianshu, Maozhong = x.Maozhong, Pizhong = x.Pizhong, Jingzhong = x.Jingzhong, + Danjia = x.Danjia, Jine = x.Jine, Description = x.Description + }).ToList(); + } + + public async Task SaveCaigouAsync(WmsCaigouDto dto) + { + WmsCaigou entity; + var isNew = dto.Id == 0; + if (isNew) entity = new WmsCaigou(); + else + { + entity = await _caigouRepo.GetByIdAsync(dto.Id); + if (entity == null) throw new ServiceException("采购记录不存在", 404); + } + + entity.Date = dto.Date.Date; + entity.Code = dto.Code ?? ""; + entity.GysId = dto.GysId; + entity.YlzkId = dto.YlzkId; + entity.Jianshu = dto.Jianshu; + entity.Maozhong = dto.Maozhong; + entity.Pizhong = dto.Pizhong; + entity.Jingzhong = dto.Jingzhong != 0 ? dto.Jingzhong : Round2(dto.Maozhong - dto.Pizhong); + entity.Danjia = dto.Danjia; + entity.Jine = dto.Jine != 0 ? dto.Jine : Round2(entity.Jingzhong * dto.Danjia); + entity.Description = dto.Description ?? ""; + + if (isNew) await _caigouRepo.AddAsync(entity); else await _caigouRepo.UpdateAsync(entity); + return entity.Id; + } + + public async Task DeleteCaigouAsync(long id) + { + var entity = await _caigouRepo.GetByIdAsync(id); + if (entity == null) throw new ServiceException("采购记录不存在", 404); + await _caigouRepo.DeleteAsync(entity); + } + + // ==================== 原料翻包计划 ==================== + + public async Task> GetFbplanAsync(DateTime? start, DateTime? end) + { + var all = await _fbplanRepo.GetAllAsync(); + var q = all.AsQueryable(); + if (start.HasValue) q = q.Where(x => x.Date.Date >= start.Value.Date); + if (end.HasValue) q = q.Where(x => x.Date.Date <= end.Value.Date); + + var ylzkDict = (await _ylzkRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var wzDict = await WzDictAsync(); + var jls = await _fbjlRepo.GetAllAsync(); + var jlDict = jls.GroupBy(x => x.FbplanId).ToDictionary(g => g.Key, g => g.OrderBy(x => x.Date).ToList()); + + return q.OrderByDescending(x => x.Date).ThenByDescending(x => x.Code).ToList().Select(x => + { + jlDict.TryGetValue(x.Id, out var records); + records ??= new List(); + return new WmsFbplanDto + { + Id = x.Id, Date = x.Date, Code = x.Code, + YlzkId = x.YlzkId, YlzkName = ylzkDict.GetValueOrDefault(x.YlzkId, ""), + WzmlId = x.WzmlId, WzmlName = wzDict.GetValueOrDefault(x.WzmlId, ""), + Jianshu = x.Jianshu, Jingzhong = x.Jingzhong, + FbType = x.FbType, FbTypeName = FbTypeName(x.FbType), + RukuStatus = x.RukuStatus, RukuStatusName = RukuStatusName(x.RukuStatus), + Description = x.Description, + YiwanchengJianshu = records.Sum(r => r.Jianshu), + YiwanchengJingzhong = records.Sum(r => r.Jingzhong), + Records = records.Select(r => new WmsFbjlDto + { + Id = r.Id, FbplanId = r.FbplanId, Date = r.Date, + Jianshu = r.Jianshu, Jingzhong = r.Jingzhong, Description = r.Description + }).ToList() + }; + }).ToList(); + } + + public async Task SaveFbplanAsync(WmsFbplanDto dto) + { + WmsFbplan entity; + var isNew = dto.Id == 0; + if (isNew) entity = new WmsFbplan(); + else + { + entity = await _fbplanRepo.GetByIdAsync(dto.Id); + if (entity == null) throw new ServiceException("翻包计划不存在", 404); + } + + entity.Date = dto.Date.Date; + entity.Code = dto.Code ?? ""; + entity.YlzkId = dto.YlzkId; + entity.WzmlId = dto.WzmlId; + entity.Jianshu = dto.Jianshu; + entity.Jingzhong = dto.Jingzhong; + entity.FbType = dto.FbType == 0 ? 1 : dto.FbType; + entity.RukuStatus = dto.RukuStatus == 0 ? 1 : dto.RukuStatus; + entity.Description = dto.Description ?? ""; + + if (isNew) await _fbplanRepo.AddAsync(entity); else await _fbplanRepo.UpdateAsync(entity); + + // 翻包记录整体覆盖 + var olds = await _fbjlRepo.FindAsync(x => x.FbplanId == entity.Id); + if (olds.Count > 0) await _fbjlRepo.HardDeleteRangeAsync(olds); + foreach (var r in dto.Records ?? new List()) + { + await _fbjlRepo.AddAsync(new WmsFbjl + { + FbplanId = entity.Id, Date = r.Date.Date, Jianshu = r.Jianshu, Jingzhong = r.Jingzhong, + Description = r.Description ?? "" + }); + } + + return entity.Id; + } + + public async Task DeleteFbplanAsync(long id) + { + var entity = await _fbplanRepo.GetByIdAsync(id); + if (entity == null) throw new ServiceException("翻包计划不存在", 404); + var jls = await _fbjlRepo.FindAsync(x => x.FbplanId == id); + if (jls.Count > 0) await _fbjlRepo.HardDeleteRangeAsync(jls); + await _fbplanRepo.DeleteAsync(entity); + } + + // ==================== 物资采购计划 ==================== + + public async Task> GetCgplanAsync(DateTime? start, DateTime? end) + { + var all = await _cgplanRepo.GetAllAsync(); + var q = all.AsQueryable(); + if (start.HasValue) q = q.Where(x => x.Date.Date >= start.Value.Date); + if (end.HasValue) q = q.Where(x => x.Date.Date <= end.Value.Date); + + var depDict = (await _depRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var items = await _cgplanItemRepo.GetAllAsync(); + var itemDict = items.GroupBy(x => x.CgplanId).ToDictionary(g => g.Key, g => g.ToList()); + + return q.OrderByDescending(x => x.Date).ThenByDescending(x => x.Code).ToList().Select(x => + { + itemDict.TryGetValue(x.Id, out var list); + list ??= new List(); + return new WmsCgplanListDto + { + Id = x.Id, Date = x.Date, Code = x.Code, + DepId = x.DepId, DepName = depDict.GetValueOrDefault(x.DepId, ""), + Status = x.Status, StatusName = CgplanStatusName(x.Status), + Description = x.Description, + ItemCount = list.Count, + Shuliang = list.Sum(i => i.Shuliang), + Jine = list.Sum(i => i.Jine) + }; + }).ToList(); + } + + public async Task GetCgplanAsync(long id) + { + var plan = await _cgplanRepo.GetByIdAsync(id); + if (plan == null) throw new ServiceException("采购计划不存在", 404); + var items = await _cgplanItemRepo.FindAsync(x => x.CgplanId == id); + var wzFull = await WzFullDictAsync(); + + return new WmsCgplanSaveDto + { + Id = plan.Id, Date = plan.Date, Code = plan.Code, DepId = plan.DepId, Status = plan.Status, + Description = plan.Description, + Items = items.OrderBy(x => x.Id).Select(i => + { + wzFull.TryGetValue(i.WzmlId, out var wz); + return new WmsCgplanItemDto + { + Id = i.Id, CgplanId = i.CgplanId, WzmlId = i.WzmlId, + WzmlName = wz.Name ?? "", WzmlCode = wz.Code ?? "", Unit = wz.Unit ?? "", + Shuliang = i.Shuliang, Danjia = i.Danjia, Jine = i.Jine, Description = i.Description + }; + }).ToList() + }; + } + + public async Task SaveCgplanAsync(WmsCgplanSaveDto dto) + { + WmsCgplan entity; + var isNew = dto.Id == 0; + if (isNew) entity = new WmsCgplan(); + else + { + entity = await _cgplanRepo.GetByIdAsync(dto.Id); + if (entity == null) throw new ServiceException("采购计划不存在", 404); + } + + entity.Date = dto.Date.Date; + entity.Code = dto.Code ?? ""; + entity.DepId = dto.DepId; + entity.Status = dto.Status; + entity.Description = dto.Description ?? ""; + + if (isNew) await _cgplanRepo.AddAsync(entity); else await _cgplanRepo.UpdateAsync(entity); + + var olds = await _cgplanItemRepo.FindAsync(x => x.CgplanId == entity.Id); + if (olds.Count > 0) await _cgplanItemRepo.HardDeleteRangeAsync(olds); + + foreach (var i in dto.Items ?? new List()) + { + if (i.WzmlId == 0) continue; + var jine = i.Jine != 0 ? i.Jine : Round2(i.Shuliang * i.Danjia); + await _cgplanItemRepo.AddAsync(new WmsCgplanItem + { + CgplanId = entity.Id, WzmlId = i.WzmlId, Shuliang = i.Shuliang, + Danjia = i.Danjia, Jine = jine, Description = i.Description ?? "" + }); + } + + return entity.Id; + } + + public async Task DeleteCgplanAsync(long id) + { + var entity = await _cgplanRepo.GetByIdAsync(id); + if (entity == null) throw new ServiceException("采购计划不存在", 404); + var items = await _cgplanItemRepo.FindAsync(x => x.CgplanId == id); + if (items.Count > 0) await _cgplanItemRepo.HardDeleteRangeAsync(items); + await _cgplanRepo.DeleteAsync(entity); + } + + // ==================== 工具 ==================== + + private static decimal Round2(decimal v) => Math.Round(v, 2, MidpointRounding.AwayFromZero); + private static decimal Round4(decimal v) => Math.Round(v, 4, MidpointRounding.AwayFromZero); +} diff --git a/backend/F9WebAiTest.Domain/Entities/Attendance/AttendanceRecord.cs b/backend/F9WebAiTest.Domain/Entities/Attendance/AttendanceRecord.cs deleted file mode 100644 index b10c635..0000000 --- a/backend/F9WebAiTest.Domain/Entities/Attendance/AttendanceRecord.cs +++ /dev/null @@ -1,18 +0,0 @@ -using FreeSql.DataAnnotations; -using F9WebAiTest.Domain.Common; - -namespace F9WebAiTest.Domain.Entities.Attendance; - -/// 考勤打卡记录 -[Table(Name = "AttendanceRecords")] -[Index("IX_AttendanceRecords_EmployeeId_WorkDate", "EmployeeId, WorkDate", IsUnique = true)] -public class AttendanceRecord : BaseEntity -{ - public long EmployeeId { get; set; } - public DateTime WorkDate { get; set; } // 考勤日期 - public DateTime? SignInTime { get; set; } // 签到时间 - public DateTime? SignOutTime { get; set; } // 签退时间 - public string SignInStatus { get; set; } = ""; // 正常/迟到/缺卡 - public string SignOutStatus { get; set; } = ""; // 正常/早退/缺卡 - public string Remark { get; set; } = ""; -} diff --git a/backend/F9WebAiTest.Domain/Entities/Attendance/LeaveApprover.cs b/backend/F9WebAiTest.Domain/Entities/Attendance/LeaveApprover.cs deleted file mode 100644 index 1accc77..0000000 --- a/backend/F9WebAiTest.Domain/Entities/Attendance/LeaveApprover.cs +++ /dev/null @@ -1,18 +0,0 @@ -using FreeSql.DataAnnotations; -using F9WebAiTest.Domain.Common; - -namespace F9WebAiTest.Domain.Entities.Attendance; - -/// 请假审批节点(多级审批流程,OrderNo 表示审批顺序) -[Table(Name = "LeaveApprovers")] -[Index("IX_LeaveApprovers_LeaveId_OrderNo", "LeaveId, OrderNo", IsUnique = true)] -public class LeaveApprover : BaseEntity -{ - public long LeaveId { get; set; } - public long ApproverId { get; set; } // 审批人ID - public string ApproverName { get; set; } = ""; // 审批人姓名(冗余) - public int OrderNo { get; set; } // 序号(从1开始,表示审批顺序) - public string Status { get; set; } = "待审批"; // 待审批/已批准/已驳回 - public string ApproveRemark { get; set; } = ""; - public DateTime? ApprovedAt { get; set; } -} diff --git a/backend/F9WebAiTest.Domain/Entities/Attendance/LeaveRequest.cs b/backend/F9WebAiTest.Domain/Entities/Attendance/LeaveRequest.cs deleted file mode 100644 index f40f1a3..0000000 --- a/backend/F9WebAiTest.Domain/Entities/Attendance/LeaveRequest.cs +++ /dev/null @@ -1,21 +0,0 @@ -using FreeSql.DataAnnotations; -using F9WebAiTest.Domain.Common; - -namespace F9WebAiTest.Domain.Entities.Attendance; - -/// 请假申请 -[Table(Name = "LeaveRequests")] -public class LeaveRequest : BaseEntity -{ - public long EmployeeId { get; set; } - public string Type { get; set; } = "事假"; // 事假/病假/年假/调休/婚假/产假 - public DateTime StartTime { get; set; } - public DateTime EndTime { get; set; } - public string Reason { get; set; } = ""; - public string Status { get; set; } = "待审批"; // 待审批/已批准/已驳回 - public int CurrentOrderNo { get; set; } = 1; // 当前审批到第几步(多级审批) - public long? ApproverId { get; set; } - public string ApproverName { get; set; } = ""; - public string ApproveRemark { get; set; } = ""; - public DateTime? ApprovedAt { get; set; } -} diff --git a/backend/F9WebAiTest.Domain/Entities/PayRoll/CalcLdcct.cs b/backend/F9WebAiTest.Domain/Entities/PayRoll/CalcLdcct.cs new file mode 100644 index 0000000..f8fb2c8 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/PayRoll/CalcLdcct.cs @@ -0,0 +1,55 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.PayRoll; + +/// +/// 薪资计算-立缫挡车车头(挡车工 / 车头工 产量计算明细) +/// 唯一键:日期 + 部门 + 班次 + 机组 + 职工 + 工种 + 工艺庄口 + 规格 + 车台 +/// +[Table(Name = "CalcLdccts")] +[Index("IX_CalcLdccts_Biz", "Date, DepId, Banci, Jizu, EmployeeId, WorktypeId, GyzkId, Guige, Chetai", IsUnique = true)] +public class CalcLdcct : BaseEntity +{ + public DateTime Date { get; set; } // 日期 + public long EmployeeId { get; set; } // 职工 + public long DepId { get; set; } // 部门 + public long WorktypeId { get; set; } // 工种 + public int Jizu { get; set; } // 机组 + public int Banci { get; set; } // 班次(0=甲 1=乙,对应 Prod_Banci_Enum) + public long GyzkId { get; set; } // 工艺庄口 + public string Guige { get; set; } = ""; // 规格 + public string Chetai { get; set; } = ""; // 车台(车头工为空) + + public decimal HoursKantai { get; set; } // 看台时长 + public decimal Dangcheshu { get; set; } // 挡车数 + public string Dengji { get; set; } = ""; // 等级 + public decimal Dengjifen { get; set; } // 等级分 + + // ---- 产量 ---- + public decimal ClDanjiaBase { get; set; } // 基础单价 + public decimal ClDanjiaDengji { get; set; } // 等级单价增减 + public decimal ClDanjiaActual { get; set; } // 实际单价 + public decimal ClSiliang { get; set; } // 丝量(g) + + // ---- 质量 ---- + public decimal ZlSaozhe { get; set; } // 缫折 + public decimal ZlCenterxd { get; set; } // 中心纤度数 + public decimal ZlYexiannum { get; set; } // 野纤数 + public decimal ZlQingjie { get; set; } // 清洁扣款片数 + public decimal ZlJiejing { get; set; } // 洁净扣款片数 + public decimal ZlErdu { get; set; } // 二度 + public decimal ZlSandu { get; set; } // 三度 + + // ---- 金额 ---- + public decimal JineCl_Original { get; set; } // 金额-产量-初始值 + public decimal Xishu_JineCl { get; set; } // 产量金额系数 + public decimal JineCl { get; set; } // 金额-产量 + public decimal JineZlSaozhe { get; set; } // 缫折考核 + public decimal JineZlCenterxd { get; set; } // 中心纤度奖 + public decimal JineZlYexian { get; set; } // 野纤扣 + public decimal JineZlQingjie { get; set; } // 清洁扣 + public decimal JineZlJiejing { get; set; } // 洁净扣 + public decimal JineZlErdu { get; set; } // 二度扣 + public decimal JineZlSandu { get; set; } // 三度扣 +} diff --git a/backend/F9WebAiTest.Domain/Entities/PayRoll/Conf.cs b/backend/F9WebAiTest.Domain/Entities/PayRoll/Conf.cs new file mode 100644 index 0000000..85f2421 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/PayRoll/Conf.cs @@ -0,0 +1,118 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.PayRoll; + +// 薪资配置(对应 F9 WinClient\F9.Business.PayRoll\PAYROLL 下的 CONFGYZK / CONFGZ / CONFDCG / DCXDJK / XinZiJiaGePeiZhi) + +/// 薪资配置-工艺庄口(按 年度 + 月份 + 工艺庄口 唯一) +[Table(Name = "ConfGyzks")] +[Index("IX_ConfGyzks_Year_Month_GyzkId", "Year, Month, GyzkId", IsUnique = true)] +public class ConfGyzk : BaseEntity +{ + public int Year { get; set; } // 年度 + public int Month { get; set; } // 月份 + public long GyzkId { get; set; } // 工艺庄口 + public string Guige { get; set; } = ""; // 规格 + + public decimal QsTaichan { get; set; } // 台产(工艺台时产量,单价分母) + public string QsDengji { get; set; } = ""; // 等级 + public decimal QsDengjifen { get; set; } // 等级分 + public decimal SaoZhe { get; set; } // 工艺指标缫折 + public int MeiTongJianLiang { get; set; } // 每桶茧量 + public int LuoSiTongShu { get; set; } // 落丝桶数 + public decimal MeiCheSiLiang { get; set; } // 每车丝量(kg) + public decimal DcgJijianXishu { get; set; } // 挡车工计件系数 + public decimal CtgJijianXishu { get; set; } // 车头工计件系数 + public decimal GyzkLtwxKs { get; set; } // 工艺-联统误选颗数 + public decimal GyzkLtwxl { get; set; } // 工艺-联统误选率 + public decimal XjChanliangPrice { get; set; } // 选茧产量单价 +} + +/// 薪资配置-工种/岗位(按 工种 唯一) +[Table(Name = "ConfGzs")] +[Index("IX_ConfGzs_WorkTypeId", "WorkTypeId", IsUnique = true)] +public class ConfGz : BaseEntity +{ + public long WorkTypeId { get; set; } // 工种/岗位 + + public decimal HoursPerday { get; set; } // 标准日工作时长 + public decimal QqDaysStandard { get; set; } // 全勤-标准出勤天数 + public decimal QqHoursMaxQj { get; set; } // 全勤-最大请假时长 + public decimal QqHoursDkMinjiaban { get; set; } // 全勤-满勤时请假免扣全勤最小加班时长 + public decimal QqHoursDkQingjia { get; set; } // 全勤-超过加班天数后免扣全勤请假时长 + + public decimal XgJineQuanqin { get; set; } // 学工-全勤奖金额 + public decimal XgJineGangwei { get; set; } // 学工-岗位薪资 + + public decimal JineNiangong { get; set; } // 年功工资 + public decimal JineQuanqin { get; set; } // 全勤奖金额 + public decimal JineGangwei { get; set; } // 岗位薪资 + public decimal JineYuekhJixiao { get; set; } // 月度绩效考核金额 + public decimal JineGwkh { get; set; } // 岗位考核 + public decimal JineYueticheng { get; set; } // 月度提成 + public decimal JineJtkhYear { get; set; } // 年度考核计提 + public decimal JineJisuanjishu { get; set; } // 计算基数工资 + public decimal JineYeban { get; set; } // 夜班补助金额/次 + public decimal JineJiaotongButie { get; set; } // 交通补贴/天 + public decimal LiudongJineGuding { get; set; } // 固定流动工资/小时 + + public decimal JiaqiJineKuangongKk { get; set; } // 旷工扣除/小时 + public decimal JiaqiJineFadingGz { get; set; } // 法定假工资/小时 + public decimal JiaqiJineFadingBz { get; set; } // 法定假补助/小时 + public decimal JiaqiJineHunjiaBz { get; set; } // 婚假补助/小时 + public decimal JiaqiJineSangjiaBz { get; set; } // 丧假补助/小时 + public decimal JiaqiJineBingjiaBz { get; set; } // 病假补助/小时 + public decimal JiaqiJineLunxiuBz { get; set; } // 轮休补助/小时 + public decimal JiabanJineBz { get; set; } // 加班补助/小时 + public decimal Jiaqi_Jine_TiaoxiuBz { get; set; } // 调休补助 + public decimal Jiaqi_Jine_NianjiaBz { get; set; } // 年假补助 + + public decimal GwsrkhJineQianhege { get; set; } // 岗位胜任考核-欠合格 + public decimal GwsrkhJineQiwang { get; set; } // 岗位胜任考核-期望 + public decimal GwsrkhJineHege { get; set; } // 岗位胜任考核-合格 + public decimal GwsrkhJineShengren { get; set; } // 岗位胜任考核-胜任 + public decimal GwsrkhJineChaoshengren { get; set; } // 岗位胜任考核-超胜任 + + public decimal Jiaban_GwsrkhJineQianhege { get; set; } // 加班工资-欠合格 + public decimal Jiaban_GwsrkhJineQiwang { get; set; } // 加班工资-期望 + public decimal Jiaban_GwsrkhJineHege { get; set; } // 加班工资-合格 + public decimal Jiaban_GwsrkhJineShengren { get; set; } // 加班工资-胜任 + public decimal Jiaban_GwsrkhJineChaoshengren { get; set; } // 加班工资-超胜任 +} + +/// 薪资配置-挡车/车头岗位(按 部门 唯一) +[Table(Name = "ConfDcgs")] +[Index("IX_ConfDcgs_DepId", "DepId", IsUnique = true)] +public class ConfDcg : BaseEntity +{ + public long DepId { get; set; } // 部门 + public decimal JineJisuanjishu50 { get; set; } // 50绪计算基数工资 + public decimal JineJisuanjishu60 { get; set; } // 60绪计算基数工资 + public decimal JineJisuanjishu70 { get; set; } // 70绪计算基数工资 + public decimal JineJisuanjishu100 { get; set; } // 100绪计算基数工资 + public decimal JineCtg { get; set; } // 车头工金额 +} + +/// 挡车纤度奖扣(按 规格 + 纤度 唯一) +[Table(Name = "DangCheXianDuJiangKous")] +[Index("IX_Dcxdjk_Guige_Xiandu", "GuiGe, XianDu", IsUnique = true)] +public class DangCheXianDuJiangKou : BaseEntity +{ + public string GuiGe { get; set; } = ""; // 规格 + public decimal XianDu { get; set; } // 纤度 + public decimal JiangLi { get; set; } // 奖励 + public decimal KouKuan { get; set; } // 扣款 +} + +/// 薪资价格配置(按 分组 + 项目 唯一) +[Table(Name = "XinZiJiaGePeiZhis")] +[Index("IX_Xzjg_Group_Key", "Group, Key", IsUnique = true)] +public class XinZiJiaGePeiZhi : BaseEntity +{ + public string Group { get; set; } = ""; // 分组(如「薪资.前缫」) + public string Key { get; set; } = ""; // 项目(如「挡车工计件等级奖扣单价」) + public string Unit { get; set; } = ""; // 单位 + public decimal Price { get; set; } // 价格 + public string Memo { get; set; } = ""; // 备注 +} diff --git a/backend/F9WebAiTest.Domain/Entities/PayRoll/ConfEmployee.cs b/backend/F9WebAiTest.Domain/Entities/PayRoll/ConfEmployee.cs new file mode 100644 index 0000000..b5c8f14 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/PayRoll/ConfEmployee.cs @@ -0,0 +1,45 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.PayRoll; + +// 考勤统计(CQTJ)依赖的三套配置: +// CONFZQ 薪资配置-账期、CONFEMPLOYEE 薪资配置-职工、GWsrkh 岗位胜任考核 +// 对应 F9 WinClient\F9.Business.PayRoll\PAYROLL\CONFZQ / CONFEMPLOYEE 与 CalXzMain 的岗位胜任考核。 + +/// 薪资配置-账期(按 年度 + 月份 唯一):考勤统计用「前缫出勤天数 QsQuanqinDays」判断满勤 +[Table(Name = "ConfZqs")] +[Index("IX_ConfZqs_Year_Month", "Year, Month", IsUnique = true)] +public class ConfZq : BaseEntity +{ + public int Year { get; set; } + public int Month { get; set; } + public decimal QsQuanqinDays { get; set; } // 前缫出勤天数(必填,考勤统计满勤/加班免扣口径) + public string Memo { get; set; } = ""; +} + +/// 薪资配置-职工(按 职工 + 工种 唯一):法定假补助/工资小时数、是否学工 +[Table(Name = "ConfEmployees")] +[Index("IX_ConfEmployees_Emp_WorkType", "EmployeeId, WorktypeId", IsUnique = true)] +public class ConfEmployee : BaseEntity +{ + public long EmployeeId { get; set; } + public long WorktypeId { get; set; } + public decimal HoursFadingBz { get; set; } // 法定假-补助小时 + public decimal HoursFadingGz { get; set; } // 法定假-工资小时 + public bool IsXuegong { get; set; } // 是否学工(全勤奖取学工标准) + public string Memo { get; set; } = ""; +} + +/// 岗位胜任考核(按 年度+月份+职工 唯一):考核结果决定加班费单价档位 +[Table(Name = "Gwsrkhs")] +[Index("IX_Gwsrkhs_Year_Month_Emp", "Year, Month, EmployeeId", IsUnique = true)] +public class Gwsrkh : BaseEntity +{ + public int Year { get; set; } + public int Month { get; set; } + public long DepId { get; set; } + public long EmployeeId { get; set; } + public string KhJieguo { get; set; } = ""; // 欠合格 / 期望 / 合格 / 胜任 / 超胜任 + public string Memo { get; set; } = ""; +} diff --git a/backend/F9WebAiTest.Domain/Entities/Prod/Cds.cs b/backend/F9WebAiTest.Domain/Entities/Prod/Cds.cs new file mode 100644 index 0000000..f5508b4 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Prod/Cds.cs @@ -0,0 +1,32 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Prod; + +/// 秤大丝-落丝汇总(每个落丝记录一条) +[Table(Name = "CdsLsjls")] +[Index("IX_CdsLsjls_LsjlId", "LsjlId", IsUnique = true)] +public class CdsLsjl : BaseEntity +{ + public long LsjlId { get; set; } // 落丝记录 + public int Num { get; set; } // 已秤数(把数) + public int Siliang { get; set; } // 已秤丝量(g) + public int Jianliang { get; set; } // 茧量(g)= 落丝桶数 × 每桶茧量 + public decimal Saozhe { get; set; } // 缫折 = 茧量 / 丝量 × 100 + public decimal Taichan { get; set; } // 实绩台产 = 丝量 / 落丝时长 / 车台数 +} + +/// 秤大丝-车台明细(每车台一行,4 个丝量值,单位 g) +[Table(Name = "CdsChetais")] +[Index("IX_CdsChetais_LsjlId_Chetaihao", "LsjlId, Chetaihao", IsUnique = true)] +public class CdsChetai : BaseEntity +{ + public long LsjlId { get; set; } // 落丝记录 + public int Chetaihao { get; set; } // 车台号 + public int Sl1 { get; set; } // 丝量1 + public int Sl2 { get; set; } // 丝量2 + public int Sl3 { get; set; } // 丝量3 + public int Sl4 { get; set; } // 丝量4 + public int Num { get; set; } // 个数 = 丝量 > 0 的个数 + public int Siliang { get; set; } // 小计丝量 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Prod/Cqdj.cs b/backend/F9WebAiTest.Domain/Entities/Prod/Cqdj.cs new file mode 100644 index 0000000..4f28005 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Prod/Cqdj.cs @@ -0,0 +1,55 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Prod; + +/// +/// 考勤登记主表:一人一天一条(唯一:日期 + 职工)。 +/// 对应 F9 WinClient\F9.Business.Prod\KQ\CQDJ 的 Cqdj 实体。 +/// +[Table(Name = "biz_prod_kq_cqdj")] +public class Cqdj : BaseEntity +{ + public DateTime Date { get; set; } // 考勤日期 + public long DepId { get; set; } // 部门(车间) + public long WorktypeId { get; set; } // 工种 + public long EmployeeId { get; set; } // 职工 + public int Banci { get; set; } // 班次(Prod_Banci_Enum:甲=1 乙=2) + public bool IsXuegong { get; set; } // 是否学工(未转正) + + public decimal HoursStandard { get; set; } // 标准时长 + public decimal HoursCq { get; set; } // 出勤时长(含正常/流动/代班/外派/工伤,不含加班) + public decimal HoursQj { get; set; } // 请假时长 + public decimal HoursLd { get; set; } // 流动时长 + public decimal HoursJb { get; set; } // 加班时长 + public decimal HoursDb { get; set; } // 代班时长 + public decimal HoursWp { get; set; } // 外派时长 + public decimal HoursKg { get; set; } // 旷工时长 + public decimal HoursGs { get; set; } // 工伤时长 + + public int CsYeban { get; set; } // 夜班次数 + public string Memo { get; set; } = ""; // 备注 +} + +/// +/// 考勤登记明细:一人一天多行,按出勤类型拆分。 +/// 对应 F9 WinClient\F9.Business.Prod\KQ\CQDJ 的 Cqdj_Item 实体。 +/// +[Table(Name = "biz_prod_kq_cqdj_item")] +public class CqdjItem : BaseEntity +{ + public long CqdjId { get; set; } // 主表 Id + public DateTime Date { get; set; } // 考勤日期 + public long DepId { get; set; } // 出勤部门(流动/代班时可能是别的部门) + public long WorktypeId { get; set; } // 出勤工种 + public long EmployeeId { get; set; } // 职工 + public int Banci { get; set; } // 班次 + public int TypeCq { get; set; } // 出勤类型(Cqdj_CqType_Enum) + public int TypeQj { get; set; } // 请假类型(Cqdj_QjType_Enum,非请假为 0) + public decimal Hours { get; set; } // 时长 + public string Jizu { get; set; } = ""; // 机组 + public string CtStart { get; set; } = ""; // 开始车台 + public string CtEnd { get; set; } = ""; // 结束车台 + public int DangcheNum { get; set; } // 挡车数 + public string Memo { get; set; } = ""; // 备注 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Prod/Cqtj.cs b/backend/F9WebAiTest.Domain/Entities/Prod/Cqtj.cs new file mode 100644 index 0000000..07cd561 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Prod/Cqtj.cs @@ -0,0 +1,74 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Prod; + +/// +/// 考勤统计(月快照,一人一行)。 +/// 对应 F9 WinClient\F9.Business.Prod\KQ\CQTJ 的 Cqtj 实体。 +/// 数据由「录入 / 一键生成」按账期从考勤登记(Cqdj + CqdjItem)汇总后落库, +/// 落库后可在统计页手工调整(年假 / 公假 / 调休 / 法定假 / 全勤相关时长与金额等)。 +/// +[Table(Name = "biz_prod_kq_cqtj")] +[Index("IX_Cqtj_Year_Month_DepId", "Year, Month, DepId", false)] +public class Cqtj : BaseEntity +{ + public int Year { get; set; } + public int Month { get; set; } + public long DepId { get; set; } + public long WorktypeId { get; set; } + public long EmployeeId { get; set; } + + // ===== 出勤天数 ===== + public decimal DaysCqStandard { get; set; } // 标准出勤天数(工种配置 QqDaysStandard) + public decimal DaysCqActual { get; set; } // 实际出勤天数 = 本部门出勤时长 / HoursPerday + public decimal CqTianci { get; set; } // 出勤天次:(出勤 + 加班) > 0 的天数 + + // ===== 时长 ===== + public decimal HoursCq { get; set; } // 出勤 + public decimal HoursQj { get; set; } // 请假(合计) + public decimal HoursLd { get; set; } // 流动 + public decimal HoursJb { get; set; } // 加班 + public decimal HoursDb { get; set; } // 代班 + public decimal HoursWp { get; set; } // 外派 + public decimal HoursKg { get; set; } // 旷工 + public int CsYeban { get; set; } // 夜班次数 + + // ===== 请假分项时长 ===== + public decimal HoursQjShijia { get; set; } // 事假 + public decimal HoursQjBj { get; set; } // 病假 + public decimal HoursQjHj { get; set; } // 婚假 + public decimal HoursQjCj { get; set; } // 产假 + public decimal HoursQjLx { get; set; } // 轮休 + public decimal HoursQjSangjia { get; set; } // 丧假 + public decimal HoursQjGj { get; set; } // 公假 + public decimal HoursQjNj { get; set; } // 年假 + public decimal HoursQjTx { get; set; } // 调休 + + // ===== 学工时长 ===== + public decimal HoursXgCq { get; set; } // 学工出勤 + public decimal HoursXgJiaban { get; set; } // 学工加班 + + // ===== 法定假 / 全勤相关时长 ===== + public decimal HoursFadingjiaqi { get; set; } // 法定假期时长 = 职工配置 法定补助 + 法定工资 + public decimal HoursQjQqmc { get; set; } // 全勤免扣请假(加班免扣) + public decimal HoursQjQqcal { get; set; } // 全勤计算请假(本部门) + + // ===== 金额 ===== + public decimal JineQuanqin { get; set; } // 全勤奖 + public decimal JineJb { get; set; } // 加班费 + public decimal JineLiudong { get; set; } // 流动工资 + public decimal JineDaiban { get; set; } // 代班工资 + public decimal JineYb { get; set; } // 夜班补助 + public decimal JineKgKk { get; set; } // 旷工考核 + + public decimal JineJqBzHj { get; set; } // 婚假补助 + public decimal JineJqBzBj { get; set; } // 病假补助 + public decimal JineJqBzLx { get; set; } // 轮休补助 + public decimal JineJqBzSangjia { get; set; } // 丧假补助 + public decimal JineJqBzFading { get; set; } // 法定假补助 + public decimal JineJqBzTxnj { get; set; } // 调休 + 年假补助 + public decimal JineJqBz { get; set; } // 假期补助合计 + + public string Memo { get; set; } = ""; +} diff --git a/backend/F9WebAiTest.Domain/Entities/Prod/Glxd.cs b/backend/F9WebAiTest.Domain/Entities/Prod/Glxd.cs new file mode 100644 index 0000000..3107630 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Prod/Glxd.cs @@ -0,0 +1,13 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Prod; + +/// 公量纤度(每个落丝记录一条,由质检录入) +[Table(Name = "Glxds")] +[Index("IX_Glxds_LsjlId", "LsjlId", IsUnique = true)] +public class Glxd : BaseEntity +{ + public long LsjlId { get; set; } // 落丝记录 + public decimal Gongliangxiandu { get; set; } // 公量纤度 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Prod/LsjlRecord.cs b/backend/F9WebAiTest.Domain/Entities/Prod/LsjlRecord.cs index f01ffc2..3525b1f 100644 --- a/backend/F9WebAiTest.Domain/Entities/Prod/LsjlRecord.cs +++ b/backend/F9WebAiTest.Domain/Entities/Prod/LsjlRecord.cs @@ -20,6 +20,8 @@ public class LsjlRecord : BaseEntity public int Chetaishu { get; set; } // 车台数 public bool Isbanche { get; set; } // 是否半车 public double Hours { get; set; } // 缫丝时长 + public int Tongshu { get; set; } // 落丝桶数(秤大丝「茧量」口径需要) + public int Meitongjianliang { get; set; } // 每桶茧量(g,秤大丝「茧量」口径需要) public string Guige { get; set; } = ""; // 规格 public double Chesu { get; set; } // 车速 } diff --git a/backend/F9WebAiTest.Domain/Entities/Prod/Spjc.cs b/backend/F9WebAiTest.Domain/Entities/Prod/Spjc.cs new file mode 100644 index 0000000..8eef1d1 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Prod/Spjc.cs @@ -0,0 +1,46 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Prod; + +/// 丝片检查(按「日期 + 车间 + 工艺庄口 + 规格」唯一) +[Table(Name = "Spjcs")] +[Index("IX_Spjcs_Date_DepId_GyzkId_Guige", "Date, DeptId, GyzkId, Guige", IsUnique = true)] +public class Spjc : BaseEntity +{ + public DateTime Date { get; set; } // 日期 + public long DeptId { get; set; } // 车间 + public long GyzkId { get; set; } // 工艺庄口 + public string Guige { get; set; } = ""; // 规格 + + public int Pianshu { get; set; } // 总片数 + public int PianshuXuegong { get; set; } // 学工片数 + + // ---- 前缫 ---- + public int QsJiahua { get; set; } // 夹花 + public int QsYouwu { get; set; } // 油污 + public int QsHeidian { get; set; } // 黑点 + public int QsShangsi { get; set; } // 伤丝 + public int QsYetiao { get; set; } // 野条 + public int QsXiaosipian { get; set; } // 小丝片 + public int QsQita { get; set; } // 其他 + public int QsHeji { get; set; } // 合计 + + // ---- 后缫 ---- + public int HsYouwu { get; set; } // 油污 + public int HsShangsi { get; set; } // 伤丝 + public int HsShuangsi { get; set; } // 双丝 + public int HsZhisi { get; set; } // 直丝 + public int HsZhantiao { get; set; } // 粘条 + public int HsYingjiao { get; set; } // 硬胶 + public int HsGuojiao { get; set; } // 过胶 + public int HsShouchu { get; set; } // 手触 + public int HsWuran { get; set; } // 污染 + public int HsLiansi { get; set; } // 连丝 + public int HsWdmt { get; set; } // 无底面头 + public int HsMaoluan { get; set; } // 毛乱 + public int HsQita { get; set; } // 其他 + public int HsHeji { get; set; } // 合计 + + public string Memo { get; set; } = ""; // 备注 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Prod/Sptz.cs b/backend/F9WebAiTest.Domain/Entities/Prod/Sptz.cs new file mode 100644 index 0000000..220bf6b --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Prod/Sptz.cs @@ -0,0 +1,29 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Prod; + +/// 丝片台帐(每日一条):浸丝 / 返丝 / 编丝 / 整理 +[Table(Name = "Sptzs")] +[Index("IX_Sptzs_Date", "Date", IsUnique = true)] +public class Sptz : BaseEntity +{ + public DateTime Date { get; set; } // 日期 + public DateTime DateLast { get; set; } // 上一日(上一本台账日期) + + // ---- 浸丝 ---- + public int JsShangcun { get; set; } // 上存 = 上一日今存 + public int JsXiasi { get; set; } // 下丝 + public int JsJinsi { get; set; } // 浸丝 + public int JsSrylJrxz { get; set; } // 上日遗留/今日新增 = 上存 − 浸丝 + public int JsJincun { get; set; } // 今存 = 下丝 + 上日遗留/今日新增 + + public int FsFsps { get; set; } // 返丝片数 + public int BsBsps { get; set; } // 编丝片数 + + // ---- 整理 ---- + public int ZlZhengpin { get; set; } // 正品片数 + public int ZlTichu { get; set; } // 剔除片数 + + public string Memo { get; set; } = ""; // 备注 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Prod/XjpbBanzu.cs b/backend/F9WebAiTest.Domain/Entities/Prod/XjpbBanzu.cs new file mode 100644 index 0000000..44b4c78 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Prod/XjpbBanzu.cs @@ -0,0 +1,32 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Prod; + +// 选茧班组(对应 F9 生产-选剥茧 XBJ\XJBZ 两张表:班组 / 班组职工) +// 数据口径: +// 1) XjpbBanzu 班组:【班次(甲/乙)+ 选茧组】一条,来自职工分配时勾选的组别。 +// 2) XjpbBanzuZhigong 班组职工:某班组下的职工及顺序,来源是「选剥茧车间」下工种为「选茧工」的在职职工。 +// 注意:老系统班组由「编辑录入」时勾选职工+组别后保存生成(XjpbBanzuZhigong_SaveCommand), +// 班组列表本身没有独立的新增入口,移植后同样以「编辑录入」批量保存为准。 + +/// 选茧班组(F9 biz_prod_xbj_xjpb_banzu) +[Table(Name = "XjpbBanzus")] +public class XjpbBanzu : BaseEntity +{ + public string Banci { get; set; } = ""; // 班次(甲 / 乙) + public int Zu { get; set; } // 选茧组 +} + +/// 选茧班组职工(F9 biz_prod_xbj_xjpb_banzu_zhigong) +[Table(Name = "XjpbBanzuZhigongs")] +public class XjpbBanzuZhigong : BaseEntity +{ + public long BanzuId { get; set; } // 选茧班组ID + public string Banci { get; set; } = ""; // 班次(甲 / 乙) + public int Zu { get; set; } // 选茧组 + public long YgId { get; set; } // 职工ID + public string YgName { get; set; } = ""; // 职工姓名 + public string YgPhone { get; set; } = ""; // 职工电话 + public int Sort { get; set; } // 顺序 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Prod/Xjrb.cs b/backend/F9WebAiTest.Domain/Entities/Prod/Xjrb.cs new file mode 100644 index 0000000..96bbe86 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Prod/Xjrb.cs @@ -0,0 +1,113 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Prod; + +// 选茧日报(对应 F9 生产-选剥茧 XBJ\XJRB 三张表:日报主表 / 计划 / 计划明细) +// 数据口径: +// 1) XjrbRecord 日报:某日某工艺庄口一条,记录干茧领入与 8 类茧(联统/好五级/六级/绵/双宫/下脚/茧衣/茧花)的 +// 包数与重量,另含「退回」、合计重量、联统率、盈亏。 +// 2) XjrbPlan 计划:按领料单(原料出库单)下达的选茧计划,含计划的 8 类茧与选茧盈亏、是否完成。 +// 3) XjrbItem 计划明细:挂在计划下的每日选茧进度行(外键 XjrbPlanId),完成勾选后回写计划盈亏。 +// +// 盈亏 / 联统率口径(对齐 F9 GetXjrbCountYingkuiQuery): +// 累计区间 = 同庄口下 Date < 当前日期 的历史日报(含当前录入值) +// yllr = Σ 干茧领入重量(含当前) +// ltj = Σ 联统茧重量(含当前) +// all = Σ (联统+好五级+六级+绵+双宫+下脚+茧衣+茧花) 重量(含当前,不含退回) +// 联统率 = yllr != 0 ? (ltj / yllr) * 100 : 0 +// 盈亏 = all - yllr + +/// 选茧日报主表(F9 biz_prod_xbj_xjrb) +[Table(Name = "XjrbRecords")] +public class XjrbRecord : BaseEntity +{ + public DateTime Date { get; set; } // 选茧日期 + public long GyzkId { get; set; } // 工艺庄口 + + public int YlLrBaoshu { get; set; } // 干茧领入包数 + public decimal YlLrZhongliang { get; set; } // 干茧领入重量 + + public int LtjBaoshu { get; set; } // 联统茧包数 + public decimal LtjZhongliang { get; set; } // 联统茧重量 + public int HwjjBaoshu { get; set; } // 好五级茧包数 + public decimal HwjjZhongliang { get; set; } // 好五级茧重量 + public int LjjBaoshu { get; set; } // 六级茧包数 + public decimal LjjZhongliang { get; set; } // 六级茧重量 + public int MjBaoshu { get; set; } // 绵茧包数 + public decimal MjZhongliang { get; set; } // 绵茧重量 + public int SgjBaoshu { get; set; } // 双宫茧包数 + public decimal SgjZhongliang { get; set; } // 双宫茧重量 + public int XjjBaoshu { get; set; } // 下脚茧包数 + public decimal XjjZhongliang { get; set; } // 下脚茧重量 + public int JyBaoshu { get; set; } // 茧衣包数 + public decimal JyZhongliang { get; set; } // 茧衣重量 + public int JhBaoshu { get; set; } // 茧花包数 + public decimal JhZhongliang { get; set; } // 茧花重量 + + public decimal TotalZhongliang { get; set; } // 合计重量(上述 8 项重量之和) + + public int ThBaoshu { get; set; } // 退回包数 + public decimal ThZhongliang { get; set; } // 退回重量 + + public decimal LiantongRate { get; set; } // 联统率(%) + public decimal Yingkui { get; set; } // 盈亏 +} + +/// 选茧计划(F9 biz_prod_xbj_xjrb_plan):按领料单下达,含计划盈亏与完成标记 +[Table(Name = "XjrbPlans")] +public class XjrbPlan : BaseEntity +{ + public DateTime Date { get; set; } // 日期 + public long GyzkId { get; set; } // 工艺庄口 + public long YllingliaodanId { get; set; } // 领料单(原料出库单)ID + + public int YlBaoshu { get; set; } // 原料领入包数 + public decimal YlZhongliang { get; set; } // 原料领入重量 + + public int LtjBaoshu { get; set; } // 联统茧包数 + public decimal LtjZhongliang { get; set; } // 联统茧重量 + public int HwjjBaoshu { get; set; } // 好五级茧包数 + public decimal HwjjZhongliang { get; set; } // 好五级茧重量 + public int WjjBaoshu { get; set; } // 五级茧包数 + public decimal WjjZhongliang { get; set; } // 五级茧重量 + public int SgjBaoshu { get; set; } // 双宫茧包数 + public decimal SgjZhongliang { get; set; } // 双宫茧重量 + public int XjjBaoshu { get; set; } // 下脚茧包数 + public decimal XjjZhongliang { get; set; } // 下脚茧重量 + public int JyBaoshu { get; set; } // 茧衣包数 + public decimal JyZhongliang { get; set; } // 茧衣重量 + public int JhBaoshu { get; set; } // 茧花包数 + public decimal JhZhongliang { get; set; } // 茧花重量 + + public decimal TotalZhongliang { get; set; } // 合计重量 + public decimal LiantongRate { get; set; } // 联统率(%) + public decimal Yingkui { get; set; } // 选茧盈亏 + public bool IsFinish { get; set; } // 是否选茧完成(完成后禁止改动明细) +} + +/// 选茧计划明细(F9 biz_prod_xbj_xjrb_item):挂在计划下的每日选茧进度行 +[Table(Name = "XjrbItems")] +public class XjrbItem : BaseEntity +{ + public long XjrbPlanId { get; set; } // 选茧计划ID + public DateTime Date { get; set; } // 选茧日期 + + public int LtjBaoshu { get; set; } // 联统茧包数 + public decimal LtjZhongliang { get; set; } // 联统茧重量 + public int HwjjBaoshu { get; set; } // 好五级茧包数 + public decimal HwjjZhongliang { get; set; } // 好五级茧重量 + public int WjjBaoshu { get; set; } // 五级茧包数 + public decimal WjjZhongliang { get; set; } // 五级茧重量 + public int SgjBaoshu { get; set; } // 双宫茧包数 + public decimal SgjZhongliang { get; set; } // 双宫茧重量 + public int XjjBaoshu { get; set; } // 下脚茧包数 + public decimal XjjZhongliang { get; set; } // 下脚茧重量 + public int JyBaoshu { get; set; } // 茧衣包数 + public decimal JyZhongliang { get; set; } // 茧衣重量 + public int JhBaoshu { get; set; } // 茧花包数 + public decimal JhZhongliang { get; set; } // 茧花重量 + + public decimal TotalZhongliang { get; set; } // 合计重量 + public bool IsFinish { get; set; } // 是否选茧完成(勾选后回写计划盈亏) +} diff --git a/backend/F9WebAiTest.Domain/Entities/Prod/Zjrb.cs b/backend/F9WebAiTest.Domain/Entities/Prod/Zjrb.cs new file mode 100644 index 0000000..ffda436 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Prod/Zjrb.cs @@ -0,0 +1,79 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Enums.SYS.Employees; + +namespace F9WebAiTest.Domain.Entities.Prod; + +// 煮茧日报(对应 F9 生产-前缫 的 ZJRB 四张表:日报主表 / 机组 / 班次 / 机组班次) +// 数据口径:某日某车间某庄口某规格一条日报,主表存「班次合计」,机组存桶数,机组班次存甲乙班今缫桶数。 + +/// 煮茧日报主表(biz_prod_qs_zjrb) +[Table(Name = "ZjrbRecords")] +public class ZjrbRecord : BaseEntity +{ + public DateTime Date { get; set; } // 日期 + public long DeptId { get; set; } // 车间(前缫) + public long GyzkId { get; set; } // 工艺庄口 + public string Guige { get; set; } = ""; // 规格 + public DateTime? LastDate { get; set; } // 上一日(写入时标记,取上一日一律按 Date < 当前日期) + public int StandardLuositongshu { get; set; } // 工艺落丝桶数(标准落丝桶数) + public int MeitongJianliang { get; set; } // 每桶茧量(g) + + // 包数(今日 = 甲班 + 乙班) + public int BaoshuShangcun { get; set; } // 上存包数 + public int BaoshuLingru { get; set; } // 领入包数 + public int BaoshuSaosi { get; set; } // 今缫包数 + public int BaoshuTuihui { get; set; } // 退回包数 + public int BaoshuJincun { get; set; } // 今存包数 + + // 重量(kg,今日 = 甲班 + 乙班) + public decimal JianliangShangcun { get; set; } // 上存重量 + public decimal JianliangLingru { get; set; } // 领入重量 + public decimal JianliangSaosi { get; set; } // 今缫重量 + public decimal JianliangTuihui { get; set; } // 退回重量 + public decimal JianliangJincun { get; set; } // 今存重量 +} + +/// 煮茧日报-机组(biz_prod_qs_zjrb_jizu):今存桶数 = 上存 + 今缫 - 今落 +[Table(Name = "ZjrbJizus")] +public class ZjrbJizu : BaseEntity +{ + public long ZjrbId { get; set; } // 煮茧日报ID + public int Jizu { get; set; } // 机组 + public decimal JizuSaosiHours { get; set; } // 缫丝时长(小时,上限 20) + public int TongshuShangcun { get; set; } // 上存桶数 + public int TongshuJinsao { get; set; } // 今缫桶数(= 甲班 + 乙班) + public int TongshuJinluo { get; set; } // 今落桶数 + public int TongshuJincun { get; set; } // 今存桶数 +} + +/// 煮茧日报-班次(biz_prod_qs_zjrb_banci):车间级甲乙班的包数/重量 +[Table(Name = "ZjrbBancis")] +public class ZjrbBanci : BaseEntity +{ + public long ZjrbId { get; set; } // 煮茧日报ID + public Prod_Banci_Enum Banci { get; set; } // 班次(甲/乙) + + public int BaoshuShangcun { get; set; } // 上存包数 + public int BaoshuLingru { get; set; } // 领入包数 + public int BaoshuSaosi { get; set; } // 今缫包数 + public int BaoshuTuihui { get; set; } // 退回包数 + public int BaoshuJincun { get; set; } // 今存包数 + + public decimal JianliangShangcun { get; set; } // 上存重量 + public decimal JianliangLingru { get; set; } // 领入重量 + public decimal JianliangSaosi { get; set; } // 今缫重量 + public decimal JianliangTuihui { get; set; } // 退回重量 + public decimal JianliangJincun { get; set; } // 今存重量 +} + +/// 煮茧日报-机组班次(biz_prod_qs_zjrb_jizu_banci):只存每机组每班次的今缫桶数 +[Table(Name = "ZjrbJizuBancis")] +public class ZjrbJizuBanci : BaseEntity +{ + public long ZjrbId { get; set; } // 煮茧日报ID + public long ZjrbJizuId { get; set; } // 煮茧日报机组ID + public int Jizu { get; set; } // 机组 + public Prod_Banci_Enum Banci { get; set; } // 班次(甲/乙) + public int TongshuJinsao { get; set; } // 今缫桶数 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Wms/Wms.cs b/backend/F9WebAiTest.Domain/Entities/Wms/Wms.cs new file mode 100644 index 0000000..7996c8a --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Wms/Wms.cs @@ -0,0 +1,378 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Wms; + +// 仓储(对应 F9 WinClient\F9.Business.WMS 的五个模块:原料 / 副产品 / 中间品 / 生丝 / 物资) +// 五个模块的「入库 / 出库 / 库存台账」结构完全一致,仅「台账项目」口径不同: +// 原料 = 原料庄口,副产品/中间品/生丝/物资 = 物资目录(中间品另有一套按工艺庄口的「笼筒架台账」) +// 因此统一为一套表 + Module 维度(F9 里是五套同名表),字段为五个模块字段的并集。 + +/// 仓储模块 +public enum Wms_Module_Enum +{ + 原料 = 1, + 副产品 = 2, + 中间品 = 3, + 生丝 = 4, + 物资 = 5 +} + +/// 入库单(五模块共用) +[Table(Name = "WmsRukus")] +[Index("IX_WmsRukus_Module_Date_Code", "Module, Date, Code", IsUnique = true)] +public class WmsRuku : BaseEntity +{ + public Wms_Module_Enum Module { get; set; } // 模块 + public DateTime Date { get; set; } // 入库日期 + public int Type { get; set; } // 入库类型(各模块枚举值,见 WmsTypes) + public string Code { get; set; } = ""; // 单据编号 + public long GysId { get; set; } // 供应商(原料/物资) + public long KhId { get; set; } // 客户(副产品) + public long GyzkId { get; set; } // 工艺庄口(副产品入库) + public long DepId { get; set; } // 部门(副产品/在制品) + public string Lianxiren { get; set; } = ""; // 联系人(原料) + public string Phone { get; set; } = ""; // 联系电话(原料) + public string Carno { get; set; } = ""; // 车牌号(原料) + public string Kuguan { get; set; } = ""; // 库管员 + public string Caigouyuan { get; set; } = ""; // 采购员(物资) + public string PayStatus { get; set; } = ""; // 支付类型(物资) + public int RkStatus { get; set; } // 入库状态(物资:0未入库 1已入库) + public int BxStatus { get; set; } // 报销状态(物资:0无 1待报销 2已报销) + public string Description { get; set; } = ""; // 备注 +} + +/// 入库明细(五模块共用) +[Table(Name = "WmsRukuDetails")] +public class WmsRukuDetail : BaseEntity +{ + public long RukuId { get; set; } + public long WzmlId { get; set; } // 物资目录 + public long YlzkId { get; set; } // 原料庄口(原料) + public long GyzkId { get; set; } // 工艺庄口(在制品) + public long DepId { get; set; } // 部门(在制品) + public int Jianshu { get; set; } // 件数 + public decimal Maozhong { get; set; } // 毛重 + public decimal Pizhong { get; set; } // 皮重 + public decimal Jingzhong { get; set; } // 净重 + public decimal BiaobaoZhongliang { get; set; } // 标包重量(在制品) + public decimal BaozhuangZhongliang { get; set; } // 包装重量(在制品) + public decimal Shuliang { get; set; } // 数量(物资) + public decimal Danjia { get; set; } // 单价 + public decimal Jine { get; set; } // 金额 + public decimal Sunhao { get; set; } // 运损(原料) + public string PiciCodes { get; set; } = ""; // 批次号(生丝) + public int PiciNum { get; set; } // 批数(生丝) + public int BaojianNum { get; set; } // 包件数(生丝) + public string PiciIds { get; set; } = ""; // 批次ID(生丝,逗号分隔,入库按批次关联) + public string Banci { get; set; } = ""; // 班次(在制品出库) + public string JidiWaigou { get; set; } = ""; // 基地 / 外购(生丝) +} + +/// 磅码单(原料入库/出库过磅明细) +[Table(Name = "WmsBangmadans")] +public class WmsBangmadan : BaseEntity +{ + public long DetailId { get; set; } // 出/入库明细 Id + public bool IsChuku { get; set; } // false=入库 true=出库 + public int Jianshu { get; set; } // 件数 + public decimal Maozhong { get; set; } // 毛重 + public decimal Pizhong { get; set; } // 皮重 + public decimal Jingzhong { get; set; } // 净重 = 毛重 − 皮重 + public DateTime? GuobangShijian { get; set; } // 过磅时间 +} + +/// 出库单(五模块共用) +[Table(Name = "WmsChukus")] +[Index("IX_WmsChukus_Module_Date_Code", "Module, Date, Code", IsUnique = true)] +public class WmsChuku : BaseEntity +{ + public Wms_Module_Enum Module { get; set; } + public DateTime Date { get; set; } // 出库日期 + public int Type { get; set; } // 出库类型 + public string Code { get; set; } = ""; // 单据编号 + public long WldwId { get; set; } // 往来单位/客户(生丝/原料销售) + public long KhId { get; set; } // 客户(副产品) + public long GyzkId { get; set; } // 工艺庄口(原料领料) + public long LingliaoDepId { get; set; } // 领料部门(物资) + public long UseDepId { get; set; } // 使用部门(物资) + public long DbdepId { get; set; } // 调拨部门(物资/副产品) + public string Lingliaoren { get; set; } = ""; // 领料人 + public string Kuguan { get; set; } = ""; // 库管员 + public string UseLocation { get; set; } = ""; // 使用位置(物资) + public int CkStatus { get; set; } // 出库状态(物资:0未出库 1已出库) + public DateTime? CkTime { get; set; } // 出库时间(物资) + public bool IsHongchong { get; set; } // 是否红冲(物资) + public long HongchongId { get; set; } // 红冲单据(物资) + public string Description { get; set; } = ""; // 备注 +} + +/// 出库明细(五模块共用) +[Table(Name = "WmsChukuDetails")] +public class WmsChukuDetail : BaseEntity +{ + public long ChukuId { get; set; } + public long WzmlId { get; set; } // 物资目录 + public long YlzkId { get; set; } // 原料庄口(原料) + public long GyzkId { get; set; } // 工艺庄口(在制品) + public long DepId { get; set; } // 部门(在制品) + public int Jianshu { get; set; } + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public decimal BiaobaoZhongliang { get; set; } + public decimal BaozhuangZhongliang { get; set; } + public decimal Shuliang { get; set; } // 数量(物资) + public decimal Danjia { get; set; } + public decimal Jine { get; set; } + public decimal KucunNum { get; set; } // 库存数量(物资,用于金额分摊) + public string PiciCodes { get; set; } = ""; // 批次号(生丝) + public int PiciNum { get; set; } + public string PcAndBjIds { get; set; } = ""; // 批次+包件ID(生丝,JSON:[{piciId,piciBaojianIds}...]) + public string Lingyongren { get; set; } = ""; // 领用人(物资) + public string Banci { get; set; } = ""; // 班次(在制品) + public string JidiWaigou { get; set; } = ""; // 基地 / 外购(生丝) +} + +/// 库存台账-日期(一天一条;在制品的「笼筒架台账」用 Kind=1 区分) +[Table(Name = "WmsKcTzDates")] +[Index("IX_WmsKcTzDates_Module_Kind_Date", "Module, Kind, Date", IsUnique = true)] +public class WmsKcTzDate : BaseEntity +{ + public Wms_Module_Enum Module { get; set; } + public int Kind { get; set; } // 0=库存台账 1=笼筒架台账(在制品) + public DateTime Date { get; set; } // 台账日期 + public DateTime LastDate { get; set; } // 上一台账日期 +} + +/// 库存台账-明细(按台账项目一行) +[Table(Name = "WmsKcTzItems")] +[Index("IX_WmsKcTzItems_TzDateId_ItemId", "TzDateId, ItemId, SubKey", IsUnique = true)] +public class WmsKcTzItem : BaseEntity +{ + public long TzDateId { get; set; } + public Wms_Module_Enum Module { get; set; } + public int Kind { get; set; } + public DateTime Date { get; set; } // 台账日期 + public long ItemId { get; set; } // 台账项目(原料=原料庄口,其余=物资目录,笼筒架=工艺庄口) + public string SubKey { get; set; } = ""; // 项目辅助维度(生丝=基地/外购) + public string ItemName { get; set; } = ""; // 项目名称快照 + public string ItemCode { get; set; } = ""; // 项目编号快照(物资台账) + public decimal Ydayjianshu { get; set; } // 上日结存件数(物资台账=数量) + public decimal Ydayjingzhong { get; set; } // 上日结存净重(物资台账=金额) + public decimal Tdayrukujianshu { get; set; } // 今日入库件数 + public decimal Tdayrukujingzhong { get; set; } // 今日入库净重 + public decimal Tdaychukujianshu { get; set; } // 今日出库件数 + public decimal Tdaychukujingzhong { get; set; } // 今日出库净重 + public decimal Yingkui { get; set; } // 盈亏 + public decimal Tdayjianshu { get; set; } // 今日结存件数 + public decimal Tdayjingzhong { get; set; } // 今日结存净重 + public decimal Yangjianjianshu { get; set; } // 样茧件数(原料) + public bool AllChuku { get; set; } // 全部出库 + public string Description { get; set; } = ""; +} + +/// 生丝批次(对应 F9 SS_Pici) +[Table(Name = "WmsPicis")] +[Index("IX_WmsPicis_Code", "Code", IsUnique = true)] +public class WmsPici : BaseEntity +{ + public string Code { get; set; } = ""; // 批号 + public int CodeType { get; set; } // 批号类型 + public int Year { get; set; } + public DateTime Date { get; set; } // 织批日期 + public DateTime? ScDate { get; set; } // 生产日期 + public long GyzkId { get; set; } // 工艺庄口 + public long WzmlId { get; set; } // 物资目录(生丝规格) + public long DepId { get; set; } // 部门 + public string Guige { get; set; } = ""; // 规格 + public string JidiWaigou { get; set; } = ""; // 基地 / 外购 + public string YoujiPutong { get; set; } = ""; // 优质 / 普通 + public int Baoshu { get; set; } // 包数 + public int StartBaojianNum { get; set; } // 起始包件号 + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public decimal Gongliang { get; set; } // 公量 + public int KcStatus { get; set; } // 0全部 1待入库 2已入库 3部分出库 4已出库 + public long RkItemId { get; set; } // 关联入库明细 + public DateTime? RkDate { get; set; } // 入库日期 + public long CkItemId { get; set; } // 关联出库明细(最后一笔) + public DateTime CkLastDate { get; set; } // 最后出库日期 + public int CkBaoshu { get; set; } // 已出库包数 + public decimal CkJingzhong { get; set; } // 已出库净重 + public int KcBaoshu { get; set; } // 库存包数 + public string Dengji { get; set; } = ""; // 等级 + public decimal Qingjie { get; set; } // 清洁 + public decimal Jiejing { get; set; } // 洁净 + public decimal Piancha { get; set; } // 偏差 + public decimal Zongcha { get; set; } // 总差 + public decimal Qieduan { get; set; } // 切断 + public string Description { get; set; } = ""; +} + +/// 生丝批次-包件(对应 F9 SS_Pici_Baojian) +[Table(Name = "WmsPiciBaojians")] +public class WmsPiciBaojian : BaseEntity +{ + public long PiciId { get; set; } + public int Baojianhao { get; set; } // 包件号 + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public decimal Gongliang { get; set; } // 公量 + public decimal Huichaolv { get; set; } // 实测回潮率(%) + public bool CkIsck { get; set; } // 是否已出库 + public long CkItemId { get; set; } // 出库明细ID + public DateTime? CkDate { get; set; } // 出库日期 + public decimal CkJingzhong { get; set; } // 出库净重 + public string Description { get; set; } = ""; +} + +/// 生丝批次-出库记录(对应 F9 SS_Pici_Chuku) +[Table(Name = "WmsPiciChukus")] +public class WmsPiciChuku : BaseEntity +{ + public long PiciId { get; set; } + public long CkItemId { get; set; } // 出库明细ID + public int CkBaoshu { get; set; } // 出库包数 + public DateTime? CkDate { get; set; } + public decimal CkJingzhong { get; set; } // 出库净重 +} + +/// 生丝批次-质检(对应 F9 SS_Pici_Qc) +[Table(Name = "WmsPiciQcs")] +[Index("IX_WmsPiciQcs_PiciId", "PiciId", IsUnique = true)] +public class WmsPiciQc : BaseEntity +{ + public long PiciId { get; set; } // 生丝批次ID + public string Pingwei { get; set; } = ""; // 品味 + public string Seze { get; set; } = ""; // 检验-色泽 + public decimal AvgHuichaolv { get; set; } // 平均回潮率 + public decimal AvgGongliangXiandu { get; set; } // 平均公量纤度 + public decimal XianduPiancha { get; set; } // 纤度偏差 + public decimal XianduZdpiancha { get; set; } // 纤度最大偏差 + public decimal Qingjie { get; set; } // 清洁 + public decimal Jiejing { get; set; } // 洁净 + public int JunyunYd { get; set; } // 均匀一度变化 + public int JunyunEd { get; set; } // 均匀二度变化 + public int JunyunSd { get; set; } // 均匀三度变化 + public int Qieduan { get; set; } // 切断次数 + public string DuanlieQiangdu { get; set; } = ""; // 断裂强度 + public string DuanlieShenchanglv { get; set; } = ""; // 断裂伸长率 + public int Baohe { get; set; } // 抱合 + public string WgWg { get; set; } = ""; // 外观-外观检验 + public string WgYanse { get; set; } = ""; // 外观-颜色 + public string WgGuangze { get; set; } = ""; // 外观-光泽 + public string WgShougan { get; set; } = ""; // 外观-手感 + public int? QjZhuyao { get; set; } // 清洁-主要疵点 + public int? QjCiyaoFeisi { get; set; } // 清洁-次要疵点-废丝 + public int? QjCiyaoDacao { get; set; } // 清洁-次要疵点-大糙 + public int? QjCiyaoNianfucao { get; set; } // 清洁-次要疵点-粘附糙 + public int? QjCiyaoDachangjie { get; set; } // 清洁-次要疵点-大长结 + public int? QjCiyaoZhongluoxuan { get; set; } // 清洁-次要疵点-重螺旋 + public int? QjPutongXiaocao { get; set; } // 清洁-普通疵点-小糙 + public int? QjPutongChangjie { get; set; } // 清洁-普通疵点-长结 + public int? QjPutongLuoxuan { get; set; } // 清洁-普通疵点-螺旋 + public int? QjPutongHuanjie { get; set; } // 清洁-普通疵点-环结及裂丝 + public int? Jj100 { get; set; } // 洁净100 + public int? Jj95 { get; set; } + public int? Jj90 { get; set; } + public int? Jj85 { get; set; } + public int? Jj80 { get; set; } + public int? Jj75 { get; set; } + public int? Jj70 { get; set; } + public int? Jj65 { get; set; } + public int? Jj60 { get; set; } + public int? Jj55 { get; set; } + public int? Jj50 { get; set; } + public int? Jj45 { get; set; } + public string JjYuanyin { get; set; } = ""; // 降级原因 + public string Description { get; set; } = ""; // 备注 +} + +/// 生丝批次-纤度检验(对应 F9 SS_Pici_Qc_Xdjy) +[Table(Name = "WmsPiciQcXdjys")] +public class WmsPiciQcXdjy : BaseEntity +{ + public long PiciId { get; set; } + public decimal Xiandu { get; set; } + public int Num { get; set; } +} + +/// 物资库存(对应 F9 WJ_Kucun,按物资目录一行) +[Table(Name = "WmsKucuns")] +[Index("IX_WmsKucuns_WzmlId", "WzmlId", IsUnique = true)] +public class WmsKucun : BaseEntity +{ + public long WzmlId { get; set; } + public decimal Shuliang { get; set; } // 库存数量 + public decimal Danjia { get; set; } // 库存单价(加权平均) + public decimal Jine { get; set; } // 库存金额 +} + +/// 原料采购(对应 F9 Yuanliao_Caigou) +[Table(Name = "WmsCaigous")] +public class WmsCaigou : BaseEntity +{ + public DateTime Date { get; set; } + public string Code { get; set; } = ""; // 采购单号 + public long GysId { get; set; } // 供应商(往来单位) + public long YlzkId { get; set; } // 原料庄口 + public int Jianshu { get; set; } + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public decimal Danjia { get; set; } + public decimal Jine { get; set; } + public string Description { get; set; } = ""; +} + +/// 原料翻包计划(对应 F9 Fbplan) +[Table(Name = "WmsFbplans")] +public class WmsFbplan : BaseEntity +{ + public DateTime Date { get; set; } // 翻包日期 + public string Code { get; set; } = ""; // 计划号 + public long YlzkId { get; set; } // 原料庄口 + public long WzmlId { get; set; } // 物资目录 + public int Jianshu { get; set; } // 计划件数 + public decimal Jingzhong { get; set; } // 计划净重 + public int FbType { get; set; } // 1翻包中 2完成 3暂停 + public int RukuStatus { get; set; } // 1有效 2作废 3无 + public string Description { get; set; } = ""; +} + +/// 原料翻包记录(对应 F9 Fbjl) +[Table(Name = "WmsFbjls")] +public class WmsFbjl : BaseEntity +{ + public long FbplanId { get; set; } + public DateTime Date { get; set; } + public int Jianshu { get; set; } + public decimal Jingzhong { get; set; } + public string Description { get; set; } = ""; +} + +/// 物资采购计划(对应 F9 WJ_Cgplan) +[Table(Name = "WmsCgplans")] +public class WmsCgplan : BaseEntity +{ + public DateTime Date { get; set; } + public string Code { get; set; } = ""; // 计划号 + public long DepId { get; set; } // 申请部门 + public int Status { get; set; } // 0草稿 1已确认 2已完成 + public string Description { get; set; } = ""; +} + +/// 物资采购计划明细(对应 F9 WJ_Cgplan_Item) +[Table(Name = "WmsCgplanItems")] +public class WmsCgplanItem : BaseEntity +{ + public long CgplanId { get; set; } + public long WzmlId { get; set; } + public decimal Shuliang { get; set; } + public decimal Danjia { get; set; } + public decimal Jine { get; set; } + public string Description { get; set; } = ""; +} diff --git a/backend/F9WebAiTest.Infrastructure/Data/BusinessSeedData.cs b/backend/F9WebAiTest.Infrastructure/Data/BusinessSeedData.cs index 35a3aa6..8637ed3 100644 --- a/backend/F9WebAiTest.Infrastructure/Data/BusinessSeedData.cs +++ b/backend/F9WebAiTest.Infrastructure/Data/BusinessSeedData.cs @@ -242,6 +242,20 @@ public static class BusinessSeedData } AddMenu(fsql, houxianDirId, "纤度检验", "/prod/houxian/xdjy", "prod/xdjy/index", "EditPen", "prod:xdjy:list", 1); AddMenu(fsql, houxianDirId, "黑板检验", "/prod/houxian/hbjy", "prod/hbjy/index", "Notebook", "prod:hbjy:list", 2); + AddMenu(fsql, houxianDirId, "秤大丝", "/prod/houxian/cds", "prod/cds/index", "Box", "prod:cds:list", 3); + AddMenu(fsql, houxianDirId, "公量纤度", "/prod/houxian/glxd", "prod/glxd/index", "TrendCharts", "prod:glxd:list", 4); + AddMenu(fsql, houxianDirId, "丝片检查", "/prod/houxian/spjc", "prod/spjc/index", "Document", "prod:spjc:list", 5); + AddMenu(fsql, houxianDirId, "丝片台帐", "/prod/houxian/sptz", "prod/sptz/index", "Files", "prod:sptz:list", 6); + + // 生产-选剥茧(对齐 F9 WinClient\F9.Business.Prod\XBJ):选茧日报 / 选茧计划 / 选茧班组 + var xuanjianDirId = fsql.Select().Where(m => m.Path == "/prod/xuanjian").First()?.Id ?? 0; + if (xuanjianDirId == 0) + { + xuanjianDirId = DbSeeder.InsertAndGetId(fsql, new Menu { Name = "生产-选剥茧", Type = "directory", Path = "/prod/xuanjian", Icon = "Grid", Sort = 7 }); + } + AddMenu(fsql, xuanjianDirId, "选茧日报", "/prod/xuanjian/rb", "prod/xuanjian/index", "Document", "prod:xuanjian:list", 1); + AddMenu(fsql, xuanjianDirId, "选茧计划", "/prod/xuanjian/plan", "prod/xuanjian/index", "Calendar", "prod:xuanjian:plan:list", 2); + AddMenu(fsql, xuanjianDirId, "选茧班组", "/prod/xuanjian/banzu", "prod/xuanjian/index", "User", "prod:xuanjian:banzu:list", 3); var adminRole = fsql.Select().Where(r => r.Code == "admin").First(); if (adminRole != null) diff --git a/backend/F9WebAiTest.Infrastructure/Data/DbSeeder.cs b/backend/F9WebAiTest.Infrastructure/Data/DbSeeder.cs index 4ed5e2e..9af5010 100644 --- a/backend/F9WebAiTest.Infrastructure/Data/DbSeeder.cs +++ b/backend/F9WebAiTest.Infrastructure/Data/DbSeeder.cs @@ -23,6 +23,9 @@ public static class DbSeeder // 1. 建表(CodeFirst SyncStructure,增量同步,兼容旧库) SyncStructure(fsql); + // 1.1 仓储单据采用物理删除:清理历史软删残留,避免唯一索引阻止重建同号单据 + CleanWmsSoftDeletedRows(fsql); + // 2. 默认角色 if (!fsql.Select().Any()) { @@ -48,8 +51,8 @@ public static class DbSeeder }).ExecuteAffrows(); } - // 4. 考勤菜单(增量) - SeedAttendanceMenus(fsql); + // 4. 旧「考勤管理」模块已移除,替换为「生产-职工考勤」(CQDJ/CQTJ)。 + // 不再创建 /attendance 菜单;库中历史记录需在「系统管理-菜单管理」删除。 // 5. 业务基础数据(部门树 + 机组车台 + 庄口 + 规格 + 定级指标 + 菜单) BusinessSeedData.Seed(fsql); @@ -94,6 +97,12 @@ public static class DbSeeder // 6.695 质量模块(工艺执行书 / 样茧试缫) SeedQcMenus(fsql, snowflake); + // 6.696 薪资模块(挡车/车头产量计算 + 其依赖配置) + SeedPayRollMenus(fsql, snowflake); + + // 6.697 仓储模块(原料 / 副产品 / 在制品 / 生丝 / 物资) + SeedWmsMenus(fsql, snowflake); + // 6.71 报表中心菜单(/report 目录 + 报表中心/报表管理两个页面) SeedReportMenus(fsql, snowflake); // 6.72 旧版内嵌模板(sys_reports.template_data)迁移为独立模板实例 ReportFile @@ -174,6 +183,39 @@ public static class DbSeeder EnsureColumn(fsql, "NotificationMessages", "Category", "INTEGER NOT NULL DEFAULT 0"); EnsureColumn(fsql, "NotificationMessages", "RoutePath", "TEXT NOT NULL DEFAULT ''"); EnsureColumn(fsql, "sys_reports", "report_file_id", "INTEGER NOT NULL DEFAULT 0"); + // 落丝记录补「落丝桶数 / 每桶茧量」:生产-后缫「秤大丝」算茧量、缫折、台产需要 + EnsureColumn(fsql, "LsjlRecords", "Tongshu", "INTEGER NOT NULL DEFAULT 0"); + EnsureColumn(fsql, "LsjlRecords", "Meitongjianliang", "INTEGER NOT NULL DEFAULT 0"); + + // 生丝:出入库按批次/包件关联(对齐 F9 SS_Rk_Item.PiciIds / SS_Ck_Item.PcAndBjIds) + EnsureColumn(fsql, "WmsRukuDetails", "PiciIds", "TEXT NOT NULL DEFAULT ''"); + EnsureColumn(fsql, "WmsChukuDetails", "PcAndBjIds", "TEXT NOT NULL DEFAULT ''"); + // 生丝批次:出库汇总字段(对齐 F9 SS_Pici.CK_LastDate / Ck_Baoshu / Ck_Jingzhong / Kc_Baoshu / Rk_Date) + EnsureColumn(fsql, "WmsPicis", "RkDate", "TEXT NULL"); + EnsureColumn(fsql, "WmsPicis", "CkLastDate", "TEXT NULL"); + EnsureColumn(fsql, "WmsPicis", "CkBaoshu", "INTEGER NOT NULL DEFAULT 0"); + EnsureColumn(fsql, "WmsPicis", "CkJingzhong", "REAL NOT NULL DEFAULT 0"); + EnsureColumn(fsql, "WmsPicis", "KcBaoshu", "INTEGER NOT NULL DEFAULT 0"); + // 生丝包件:出库标记(对齐 F9 SS_Pici_Baojian.Ck_Isck / Ck_Item_Id / Ck_date / Ck_Jingzhong) + EnsureColumn(fsql, "WmsPiciBaojians", "CkIsck", "INTEGER NOT NULL DEFAULT 0"); + EnsureColumn(fsql, "WmsPiciBaojians", "CkItemId", "INTEGER NOT NULL DEFAULT 0"); + EnsureColumn(fsql, "WmsPiciBaojians", "CkDate", "TEXT NULL"); + EnsureColumn(fsql, "WmsPiciBaojians", "CkJingzhong", "REAL NOT NULL DEFAULT 0"); + } + + /// 仓储单据(入库 / 出库)改用物理删除,这里清理历史软删残留行 + private static void CleanWmsSoftDeletedRows(IFreeSql fsql) + { + try + { + fsql.Ado.ExecuteNonQuery("DELETE FROM WmsRukus WHERE IsDeleted = 1;"); + fsql.Ado.ExecuteNonQuery("DELETE FROM WmsChukus WHERE IsDeleted = 1;"); + fsql.Ado.ExecuteNonQuery("DELETE FROM WmsPicis WHERE IsDeleted = 1;"); + } + catch (Exception ex) + { + Console.WriteLine($"[DbSeeder] 仓储软删残留清理跳过:{ex.Message}"); + } } /// 为已有表补加列(列已存在则忽略) @@ -731,6 +773,27 @@ public static class DbSeeder Console.WriteLine("[DbSeeder] 已清理旧权限单元 base:jizuchetai:list"); } + // 1.9) 清理旧「考勤管理」(/attendance):前端页面组件已随「生产-职工考勤」移植删除, + // 菜单留着会出现「点了打不开(空白页)」,这里按路径整体移除。 + var legacyAtt = fsql.Select().Where(m => m.Path.StartsWith("/attendance")).ToList(); + foreach (var m in legacyAtt) + { + fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_role_permissions WHERE permission_id = {m.Id}"); + fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_menus WHERE id = {m.Id}"); + } + if (legacyAtt.Count > 0) + Console.WriteLine($"[DbSeeder] 已清理旧考勤菜单 {legacyAtt.Count} 条(页面组件已移除)"); + + var legacyAttPerms = fsql.Ado.ExecuteDataTable("SELECT id FROM sys_rbac_permissions WHERE code LIKE 'attendance:%'"); + foreach (DataRow permRow in legacyAttPerms.Rows) + { + var permId = Convert.ToInt64(permRow["id"]); + fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_role_permissions WHERE permission_id = {permId}"); + fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_permissions WHERE id = {permId}"); + } + if (legacyAttPerms.Rows.Count > 0) + Console.WriteLine($"[DbSeeder] 已清理旧考勤权限单元 {legacyAttPerms.Rows.Count} 条"); + EnsureTopMenuWithChildren(fsql, snowflake, "/prod/qianxian", "生产-前缫", "Cpu", 5, new (string Name, string Path, string Component, string Permission, string Icon, int Sort)[] { @@ -738,7 +801,8 @@ public static class DbSeeder ("落丝记录", "/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/ktjl", "prod/ktjl/index", "prod:ktjl:list", "Tickets", 5) + ("看台记录", "/prod/qianxian/ktjl", "prod/ktjl/index", "prod:ktjl:list", "Tickets", 5), + ("煮茧日报", "/prod/qianxian/zjrb", "prod/zjrb/index", "prod:zjrb:list", "Notebook", 6) }); // 子菜单顺序归一(EnsureTopMenuWithChildren 只补新菜单,不调整已存在菜单的排序) @@ -746,7 +810,7 @@ public static class DbSeeder { ("/prod/qianxian/jizuchetai", 1), ("/prod/qianxian/lsjl", 2), ("/prod/qianxian/chejianbanci", 3), ("/prod/qianxian/ktpb", 4), - ("/prod/qianxian/ktjl", 5) + ("/prod/qianxian/ktjl", 5), ("/prod/qianxian/zjrb", 6) }) { fsql.Update() @@ -759,9 +823,68 @@ public static class DbSeeder new (string Name, string Path, string Component, string Permission, string Icon, int Sort)[] { ("纤度检验", "/prod/houxian/xdjy", "prod/xdjy/index", "prod:xdjy:list", "EditPen", 1), - ("黑板检验", "/prod/houxian/hbjy", "prod/hbjy/index", "prod:hbjy:list", "Notebook", 2) + ("黑板检验", "/prod/houxian/hbjy", "prod/hbjy/index", "prod:hbjy:list", "Notebook", 2), + ("秤大丝", "/prod/houxian/cds", "prod/cds/index", "prod:cds:list", "Box", 3), + ("公量纤度", "/prod/houxian/glxd", "prod/glxd/index", "prod:glxd:list", "TrendCharts", 4), + ("丝片检查", "/prod/houxian/spjc", "prod/spjc/index", "prod:spjc:list", "Document", 5), + ("丝片台帐", "/prod/houxian/sptz", "prod/sptz/index", "prod:sptz:list", "Files", 6) }); + // 生产-选剥茧(对齐 F9 WinClient\F9.Business.Prod\XBJ):选茧日报 / 选茧计划 / 选茧班组 + EnsureTopMenuWithChildren(fsql, snowflake, "/prod/xuanjian", "生产-选剥茧", "Grid", 7, + new (string Name, string Path, string Component, string Permission, string Icon, int Sort)[] + { + ("选茧日报", "/prod/xuanjian/rb", "prod/xuanjian/index", "prod:xuanjian:list", "Document", 1), + ("选茧计划", "/prod/xuanjian/plan", "prod/xuanjian/index", "prod:xuanjian:plan:list", "Calendar", 2), + ("选茧班组", "/prod/xuanjian/banzu", "prod/xuanjian/index", "prod:xuanjian:banzu:list", "User", 3) + }); + + // 生产-考勤(对齐 F9 WinClient\F9.Business.Prod\KQ):考勤登记 + EnsureTopMenuWithChildren(fsql, snowflake, "/prod/kaoqin", "生产-考勤", "Clock", 8, + new (string Name, string Path, string Component, string Permission, string Icon, int Sort)[] + { + ("考勤登记", "/prod/kaoqin/cqdj", "prod/kaoqin/index", "prod:kaoqin:cqdj:list", "EditPen", 1), + ("考勤统计", "/prod/kaoqin/cqtj", "prod/kaoqin/cqtj/index", "prod:kaoqin:cqtj:list", "DataAnalysis", 2) + }); + + // 子菜单顺序归一 + foreach (var (path, sort) in new (string Path, int Sort)[] + { + ("/prod/kaoqin/cqdj", 1), ("/prod/kaoqin/cqtj", 2) + }) + { + fsql.Update() + .Set(x => x.SortOrder, sort) + .Where(x => x.Path == path && x.SortOrder != sort) + .ExecuteAffrows(); + } + + // 子菜单顺序归一 + foreach (var (path, sort) in new (string Path, int Sort)[] + { + ("/prod/xuanjian/rb", 1), ("/prod/xuanjian/plan", 2), ("/prod/xuanjian/banzu", 3) + }) + { + fsql.Update() + .Set(x => x.SortOrder, sort) + .Where(x => x.Path == path && x.SortOrder != sort) + .ExecuteAffrows(); + } + + // 子菜单顺序归一 + foreach (var (path, sort) in new (string Path, int Sort)[] + { + ("/prod/houxian/xdjy", 1), ("/prod/houxian/hbjy", 2), + ("/prod/houxian/cds", 3), ("/prod/houxian/glxd", 4), + ("/prod/houxian/spjc", 5), ("/prod/houxian/sptz", 6) + }) + { + fsql.Update() + .Set(x => x.SortOrder, sort) + .Where(x => x.Path == path && x.SortOrder != sort) + .ExecuteAffrows(); + } + // 3) 规范一级菜单顺序 foreach (var (path, sort) in new (string Path, int Sort)[] { @@ -791,6 +914,87 @@ public static class DbSeeder /// 质量模块菜单:一级目录「质量」+ 工艺执行书 / 样茧试缫 两个页面。 /// 模块落在「生产-后缫」之后、消息中心之前。 /// + private static void SeedPayRollMenus(IFreeSql fsql, ISnowflakeIdGenerator snowflake) + { + try + { + EnsureTopMenuWithChildren(fsql, snowflake, "/payroll", "薪资", "Money", 8, + new (string Name, string Path, string Component, string Permission, string Icon, int Sort)[] + { + ("挡车车头产量计算", "/payroll/calcldcct", "payroll/calcldcct/index", "payroll:calcldcct:list", "Coin", 1), + ("工艺庄口薪资配置", "/payroll/confgyzk", "payroll/confgyzk/index", "payroll:confgyzk:list", "Setting", 2), + ("工种薪资配置", "/payroll/confgz", "payroll/confgz/index", "payroll:confgz:list", "Setting", 3), + ("挡车岗位薪资配置", "/payroll/confdcg", "payroll/confdcg/index", "payroll:confdcg:list", "Setting", 4), + ("挡车纤度奖扣", "/payroll/dcxdjk", "payroll/dcxdjk/index", "payroll:dcxdjk:list", "Setting", 5), + ("薪资价格配置", "/payroll/xzjg", "payroll/xzjg/index", "payroll:xzjg:list", "Setting", 6), + // 考勤统计依赖的三套配置(CONFZQ / CONFEMPLOYEE / 岗位胜任考核) + ("账期薪资配置", "/payroll/confzq", "payroll/confzq/index", "payroll:confzq:list", "Calendar", 7), + ("职工薪资配置", "/payroll/confemployee", "payroll/confemployee/index", "payroll:confemployee:list", "User", 8), + ("岗位胜任考核", "/payroll/gwsrkh", "payroll/gwsrkh/index", "payroll:gwsrkh:list", "Trophy", 9) + }); + + // 一级菜单顺序归一:质量(7) < 薪资(8) < 通知(9) < 系统(10) < 报表(11) + foreach (var (path, sort) in new (string Path, int Sort)[] + { + ("/attendance", 2), ("/base", 3), ("/employee", 4), + ("/prod/qianxian", 5), ("/prod/houxian", 6), ("/qc", 7), + ("/payroll", 8), ("/notification", 9), ("/sys", 10), ("/report", 11) + }) + { + fsql.Update() + .Set(x => x.SortOrder, sort) + .Where(x => x.ParentId == 0 && x.Path == path && x.SortOrder != sort) + .ExecuteAffrows(); + } + + Console.WriteLine("[DbSeeder] 薪资模块菜单已就绪(挡车车头产量计算 + 五套薪资配置)"); + } + catch (Exception ex) + { + Console.WriteLine($"[DbSeeder] 薪资模块菜单种子跳过:{ex.Message}"); + } + } + + /// + /// 仓储模块菜单:一级目录「仓储」+ 五个模块页面(原料 / 副产品 / 在制品 / 生丝 / 物资)。 + /// 对齐 F9 WinClient\F9.Business.WMS 的五个子模块,落在一级菜单「薪资」之后。 + /// + private static void SeedWmsMenus(IFreeSql fsql, ISnowflakeIdGenerator snowflake) + { + try + { + EnsureTopMenuWithChildren(fsql, snowflake, "/wms", "仓储", "Box", 9, + new (string Name, string Path, string Component, string Permission, string Icon, int Sort)[] + { + ("原料", "/wms/yuanliao", "wms/yuanliao/index", "wms:yuanliao:list", "Coin", 1), + ("副产品", "/wms/fuchanpin", "wms/fuchanpin/index", "wms:fuchanpin:list", "Goods", 2), + ("中间品", "/wms/zjp", "wms/zjp/index", "wms:zjp:list", "Grid", 3), + ("生丝", "/wms/ss", "wms/ss/index", "wms:ss:list", "ShoppingCart", 4), + ("物资", "/wms/wj", "wms/wj/index", "wms:wj:list", "Tools", 5) + }); + + // 一级菜单顺序归一:质量(7) < 薪资(8) < 仓储(9) < 通知(10) < 系统(11) < 报表(12) + foreach (var (path, sort) in new (string Path, int Sort)[] + { + ("/attendance", 2), ("/base", 3), ("/employee", 4), + ("/prod/qianxian", 5), ("/prod/houxian", 6), ("/qc", 7), + ("/payroll", 8), ("/wms", 9), ("/notification", 10), ("/sys", 11), ("/report", 12) + }) + { + fsql.Update() + .Set(x => x.SortOrder, sort) + .Where(x => x.ParentId == 0 && x.Path == path && x.SortOrder != sort) + .ExecuteAffrows(); + } + + Console.WriteLine("[DbSeeder] 仓储模块菜单已就绪(原料 / 副产品 / 在制品 / 生丝 / 物资)"); + } + catch (Exception ex) + { + Console.WriteLine($"[DbSeeder] 仓储模块菜单种子跳过:{ex.Message}"); + } + } + private static void SeedQcMenus(IFreeSql fsql, ISnowflakeIdGenerator snowflake) { try @@ -862,9 +1066,10 @@ public static class DbSeeder child.SetWebInfo(path, component, permission); fsql.Insert(child).ExecuteAffrows(); } - else if (!child.IsActive || child.IconSvg != icon || child.ParentId != dir.Id) + else if (!child.IsActive || child.IconSvg != icon || child.ParentId != dir.Id || child.Name != name) { fsql.Update() + .Set(x => x.Name, name) .Set(x => x.IconSvg, icon) .Set(x => x.IsActive, true) .Set(x => x.ParentId, dir.Id) diff --git a/tools/report-designer/F9ReportDesignerHandler.ps1 b/tools/report-designer/F9ReportDesignerHandler.ps1 new file mode 100644 index 0000000..b1c8820 --- /dev/null +++ b/tools/report-designer/F9ReportDesignerHandler.ps1 @@ -0,0 +1,117 @@ +# F9 报表设计器协议宿主 +# 注册表 HKCR\f9designer\shell\open\command 指向本脚本: +# powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File "<本脚本>" "%1" +# 前端唤起:ReportDesign.vue 的 openGridDesigner() +# f9designer://open?base=&rid=<报表Id>&token=&code=<报表编号> +# +# 流程:解析协议参数 -> 下载模板 .grf -> 用锐浪 Grid++Report 7 设计器打开(阻塞等待关闭) +# -> 关闭后把模板回传后端 -> 清理临时文件 +# 日志:tools/report-designer/logs/designer_yyyyMMdd.log(排查"点了没反应"时看这里) + +param([string]$Url) + +$ErrorActionPreference = 'Stop' +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$logDir = Join-Path $scriptDir 'logs' +if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } +$log = Join-Path $logDir ('designer_{0:yyyyMMdd}.log' -f (Get-Date)) + +function Write-Log($msg) { + $line = '[{0}] {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $msg + try { Add-Content -Path $log -Value $line -Encoding UTF8 } catch { } +} + +# 失败时弹窗提示(注册表里是 -WindowStyle Hidden,不弹窗的话用户只会看到"点了没反应") +function Show-Error($msg) { + Write-Log "错误: $msg" + try { + Add-Type -AssemblyName System.Windows.Forms + [System.Windows.Forms.MessageBox]::Show( + $msg + "`n`n详细日志:" + $log, 'F9 报表设计器', 'OK', 'Error') | Out-Null + } catch { } +} + +Write-Log '========== 唤起 ==========' +Write-Log "Url: $Url" + +try { + # ---------- 1. 解析协议参数 ---------- + # 关键:Windows Shell 会在协议主机后补一个 "/",实际收到的是 + # f9designer://open/?base=...&rid=... + # 所以不能用固定前缀剥离,直接取第一个 "?" 之后的全部内容。 + $qIndex = if ($Url) { $Url.IndexOf('?') } else { -1 } + $raw = if ($qIndex -ge 0) { $Url.Substring($qIndex + 1) } else { '' } + $qs = @{} + foreach ($part in ($raw -split '&')) { + if ([string]::IsNullOrWhiteSpace($part)) { continue } + $kv = $part -split '=', 2 + if ($kv.Length -eq 2) { + $key = [Uri]::UnescapeDataString($kv[0]).Trim() + $qs[$key] = [Uri]::UnescapeDataString($kv[1]) + } + } + $base = ($qs['base'] + '').TrimEnd('/') + $rid = $qs['rid'] + $token = $qs['token'] + $code = $qs['code'] + Write-Log "base=$base rid=$rid code=$code" + + if (-not $base) { + $base = 'http://localhost:5173' + Write-Log "未收到 base 参数,回退默认 $base" + } + if (-not $rid) { throw "缺少 rid 参数(Url=$Url)" } + + # ---------- 2. 定位锐浪设计器 ---------- + # 可用环境变量 F9_GR7_DESIGNER 覆盖,默认 D:\GridReport7\gr7designer64.exe + $designer = $env:F9_GR7_DESIGNER + if ([string]::IsNullOrWhiteSpace($designer)) { $designer = 'D:\GridReport7\gr7designer64.exe' } + if (-not (Test-Path $designer)) { throw "未找到锐浪设计器:$designer (可设置环境变量 F9_GR7_DESIGNER 指定)" } + Write-Log "设计器: $designer" + + # ---------- 3. 下载模板 ---------- + $tmp = Join-Path $env:TEMP ('f9_{0}_{1}.grf' -f $rid, [Guid]::NewGuid().ToString('N').Substring(0, 8)) + # 下载接口为 [AllowAnonymous],token 走 query 的 access_token + $dl = "$base/api/sys/report/$rid/template?access_token=$token" + Write-Log "下载: $dl" + try { + Invoke-WebRequest -Uri $dl -OutFile $tmp -UseBasicParsing + Write-Log ('已下载 {0} 字节' -f (Get-Item $tmp).Length) + } catch { + Write-Log "下载失败(新报表可能尚无模板),改用空白模板继续:$($_.Exception.Message)" + Set-Content -Path $tmp -Value '' -Encoding UTF8 + } + + # ---------- 4. 打开设计器并等待关闭 ---------- + Write-Log '启动设计器(等待关闭)...' + $proc = Start-Process -FilePath $designer -ArgumentList ('"{0}"' -f $tmp) -PassThru + $proc.WaitForExit() + Write-Log ('设计器已关闭,退出码 {0}' -f $proc.ExitCode) + + # ---------- 5. 回传模板 ---------- + if ((Get-Item $tmp).Length -gt 0) { + Write-Log '上传模板...' + Add-Type -AssemblyName System.Net.Http + $client = New-Object System.Net.Http.HttpClient + if ($token) { $client.DefaultRequestHeaders.TryAddWithoutValidation('Authorization', "Bearer $token") | Out-Null } + $content = New-Object System.Net.Http.MultipartFormDataContent + $fs = [System.IO.File]::OpenRead($tmp) + try { + $sc = New-Object System.Net.Http.StreamContent($fs) + $content.Add($sc, 'file', [System.IO.Path]::GetFileName($tmp)) + $resp = $client.PostAsync("$base/api/sys/report/$rid/template", $content).Result + Write-Log ('上传结果: {0}' -f $resp.StatusCode) + } finally { + $fs.Close() + $client.Dispose() + } + } else { + Write-Log '模板为空,跳过上传' + } + + Remove-Item $tmp -Force -ErrorAction SilentlyContinue + Write-Log '完成' +} catch { + Show-Error $($_.Exception.Message) + Write-Log $_.ScriptStackTrace +} diff --git a/web/src/router/index.js b/web/src/router/index.js index e9e524a..ae54711 100644 --- a/web/src/router/index.js +++ b/web/src/router/index.js @@ -21,20 +21,13 @@ export const constantRoutes = [ name: 'Dashboard', component: () => import('../views/dashboard/index.vue'), meta: { title: '首页', icon: 'HomeFilled' } - }, - // 请假审批:静态注册,所有登录用户可访问(待办消息"去处理"跳转入口,不依赖菜单权限) - { - path: 'attendance/approve', - name: 'AttendanceApprove', - component: () => import('../views/attendance/approve/index.vue'), - meta: { title: '请假审批', icon: 'Checked' } } ] } ]; // 已静态注册的路由(不随菜单动态注册,避免重复) -const STATIC_ROUTES = ['/attendance/approve']; +const STATIC_ROUTES = []; // 组件映射:后端菜单的 component 字段 -> 前端组件 export const componentMap = { @@ -43,11 +36,6 @@ export const componentMap = { 'system/role/index': () => import('../views/system/role/index.vue'), 'system/menu/index': () => import('../views/system/menu/index.vue'), 'system/log/index': () => import('../views/system/log/index.vue'), - 'attendance/punch/index': () => import('../views/attendance/punch/index.vue'), - 'attendance/my/index': () => import('../views/attendance/my/index.vue'), - 'attendance/leave/index': () => import('../views/attendance/leave/index.vue'), - 'attendance/records/index': () => import('../views/attendance/records/index.vue'), - 'attendance/approve/index': () => import('../views/attendance/approve/index.vue'), 'base/zhuangkou/index': () => import('../views/base/zhuangkou/index.vue'), 'base/guige/index': () => import('../views/base/guige/index.vue'), 'base/dingjizhibiao/index': () => import('../views/base/dingjizhibiao/index.vue'), @@ -60,6 +48,11 @@ export const componentMap = { 'base/wuzimulu/index': () => import('../views/base/wuzimulu/index.vue'), 'base/gongyingshang/index': () => import('../views/base/gongyingshang/index.vue'), 'base/employee/index': () => import('../views/base/employee/index.vue'), + 'wms/yuanliao/index': () => import('../views/wms/yuanliao/index.vue'), + 'wms/fuchanpin/index': () => import('../views/wms/fuchanpin/index.vue'), + 'wms/zjp/index': () => import('../views/wms/zjp/index.vue'), + 'wms/ss/index': () => import('../views/wms/ss/index.vue'), + 'wms/wj/index': () => import('../views/wms/wj/index.vue'), 'qc/gyzxs/index': () => import('../views/qc/gyzxs/index.vue'), 'qc/yjss/index': () => import('../views/qc/yjss/index.vue'), 'prod/lsjl/index': () => import('../views/prod/lsjl/index.vue'), @@ -67,6 +60,26 @@ export const componentMap = { 'prod/hbjy/index': () => import('../views/prod/hbjy/index.vue'), 'prod/ktpb/index': () => import('../views/prod/ktpb/index.vue'), 'prod/ktjl/index': () => import('../views/prod/ktjl/index.vue'), + 'prod/zjrb/index': () => import('../views/prod/zjrb/index.vue'), + 'prod/cds/index': () => import('../views/prod/cds/index.vue'), + 'prod/glxd/index': () => import('../views/prod/glxd/index.vue'), + 'prod/spjc/index': () => import('../views/prod/spjc/index.vue'), + // 生产-选剥茧:选茧日报 / 选茧计划 / 选茧班组(对齐 F9 WinClient\F9.Business.Prod\XBJ) + 'prod/xuanjian/index': () => import('../views/prod/xuanjian/index.vue'), + // 生产-考勤:考勤登记 / 考勤统计(对齐 F9 WinClient\F9.Business.Prod\KQ\CQDJ / CQTJ) + 'prod/kaoqin/index': () => import('../views/prod/kaoqin/index.vue'), + 'prod/kaoqin/cqtj/index': () => import('../views/prod/kaoqin/cqtj/index.vue'), + // 考勤统计依赖的薪资配置(CONFZQ / CONFEMPLOYEE / 岗位胜任考核) + 'payroll/confzq/index': () => import('../views/payroll/confzq/index.vue'), + 'payroll/confemployee/index': () => import('../views/payroll/confemployee/index.vue'), + 'payroll/gwsrkh/index': () => import('../views/payroll/gwsrkh/index.vue'), + 'prod/sptz/index': () => import('../views/prod/sptz/index.vue'), + 'payroll/calcldcct/index': () => import('../views/payroll/calcldcct/index.vue'), + 'payroll/confgyzk/index': () => import('../views/payroll/confgyzk/index.vue'), + 'payroll/confgz/index': () => import('../views/payroll/confgz/index.vue'), + 'payroll/confdcg/index': () => import('../views/payroll/confdcg/index.vue'), + 'payroll/dcxdjk/index': () => import('../views/payroll/dcxdjk/index.vue'), + 'payroll/xzjg/index': () => import('../views/payroll/xzjg/index.vue'), 'notification/messages/index': () => import('../views/notification/messages/index.vue'), 'notification/subscribe/index': () => import('../views/notification/subscribe/index.vue'), 'notification/type/index': () => import('../views/notification/type/index.vue'), diff --git a/web/src/views/attendance/approve/index.vue b/web/src/views/attendance/approve/index.vue deleted file mode 100644 index c698ec6..0000000 --- a/web/src/views/attendance/approve/index.vue +++ /dev/null @@ -1,154 +0,0 @@ - - - - - diff --git a/web/src/views/attendance/leave/index.vue b/web/src/views/attendance/leave/index.vue deleted file mode 100644 index c4b7ab4..0000000 --- a/web/src/views/attendance/leave/index.vue +++ /dev/null @@ -1,141 +0,0 @@ - - - - - diff --git a/web/src/views/attendance/my/index.vue b/web/src/views/attendance/my/index.vue deleted file mode 100644 index dab1a2c..0000000 --- a/web/src/views/attendance/my/index.vue +++ /dev/null @@ -1,160 +0,0 @@ - - - - - diff --git a/web/src/views/attendance/punch/index.vue b/web/src/views/attendance/punch/index.vue deleted file mode 100644 index 653d015..0000000 --- a/web/src/views/attendance/punch/index.vue +++ /dev/null @@ -1,207 +0,0 @@ - - - - - diff --git a/web/src/views/attendance/records/index.vue b/web/src/views/attendance/records/index.vue deleted file mode 100644 index 58fdf7d..0000000 --- a/web/src/views/attendance/records/index.vue +++ /dev/null @@ -1,133 +0,0 @@ - - - - - diff --git a/web/src/views/dashboard/index.vue b/web/src/views/dashboard/index.vue index 71c3632..163d419 100644 --- a/web/src/views/dashboard/index.vue +++ b/web/src/views/dashboard/index.vue @@ -10,7 +10,6 @@ const quickLinks = [ { title: '落丝记录', desc: '前缫 · 落丝管理', icon: 'DataLine', path: '/prod/qianxian/lsjl', color: '#1e6b52' }, { title: '纤度检验', desc: '后缫 · 纤度 / 野纤等级判定', icon: 'ScaleToOriginal', path: '/prod/houxian/xdjy', color: '#c9a54a' }, { title: '黑板检验', desc: '后缫 · 清洁 / 洁净等级', icon: 'Grid', path: '/prod/houxian/hbjy', color: '#3f8f6e' }, - { title: '考勤打卡', desc: '上下班签到签退', icon: 'AlarmClock', path: '/attendance/punch', color: '#b8873a' } ]; function go(path) { router.push(path); } diff --git a/web/src/views/payroll/calcldcct/index.vue b/web/src/views/payroll/calcldcct/index.vue new file mode 100644 index 0000000..18637a6 --- /dev/null +++ b/web/src/views/payroll/calcldcct/index.vue @@ -0,0 +1,299 @@ + + + + + diff --git a/web/src/views/payroll/confdcg/index.vue b/web/src/views/payroll/confdcg/index.vue new file mode 100644 index 0000000..e87ab8b --- /dev/null +++ b/web/src/views/payroll/confdcg/index.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/web/src/views/payroll/confemployee/index.vue b/web/src/views/payroll/confemployee/index.vue new file mode 100644 index 0000000..5176394 --- /dev/null +++ b/web/src/views/payroll/confemployee/index.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/web/src/views/payroll/confgyzk/index.vue b/web/src/views/payroll/confgyzk/index.vue new file mode 100644 index 0000000..b1f7368 --- /dev/null +++ b/web/src/views/payroll/confgyzk/index.vue @@ -0,0 +1,182 @@ + + + + + diff --git a/web/src/views/payroll/confgz/index.vue b/web/src/views/payroll/confgz/index.vue new file mode 100644 index 0000000..103871c --- /dev/null +++ b/web/src/views/payroll/confgz/index.vue @@ -0,0 +1,217 @@ + + + + + diff --git a/web/src/views/payroll/confzq/index.vue b/web/src/views/payroll/confzq/index.vue new file mode 100644 index 0000000..489589f --- /dev/null +++ b/web/src/views/payroll/confzq/index.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/web/src/views/payroll/dcxdjk/index.vue b/web/src/views/payroll/dcxdjk/index.vue new file mode 100644 index 0000000..30a2d0b --- /dev/null +++ b/web/src/views/payroll/dcxdjk/index.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/web/src/views/payroll/gwsrkh/index.vue b/web/src/views/payroll/gwsrkh/index.vue new file mode 100644 index 0000000..0d855e3 --- /dev/null +++ b/web/src/views/payroll/gwsrkh/index.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/web/src/views/payroll/xzjg/index.vue b/web/src/views/payroll/xzjg/index.vue new file mode 100644 index 0000000..886d7d6 --- /dev/null +++ b/web/src/views/payroll/xzjg/index.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/web/src/views/prod/cds/index.vue b/web/src/views/prod/cds/index.vue new file mode 100644 index 0000000..bbebfeb --- /dev/null +++ b/web/src/views/prod/cds/index.vue @@ -0,0 +1,279 @@ + + + + + diff --git a/web/src/views/prod/glxd/index.vue b/web/src/views/prod/glxd/index.vue new file mode 100644 index 0000000..9a09cf4 --- /dev/null +++ b/web/src/views/prod/glxd/index.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/web/src/views/prod/kaoqin/cqtj/index.vue b/web/src/views/prod/kaoqin/cqtj/index.vue new file mode 100644 index 0000000..00f0d0d --- /dev/null +++ b/web/src/views/prod/kaoqin/cqtj/index.vue @@ -0,0 +1,312 @@ + + + + + diff --git a/web/src/views/prod/kaoqin/index.vue b/web/src/views/prod/kaoqin/index.vue new file mode 100644 index 0000000..377bd7c --- /dev/null +++ b/web/src/views/prod/kaoqin/index.vue @@ -0,0 +1,608 @@ + + + + + diff --git a/web/src/views/prod/lsjl/index.vue b/web/src/views/prod/lsjl/index.vue index cfc38d7..58453c3 100644 --- a/web/src/views/prod/lsjl/index.vue +++ b/web/src/views/prod/lsjl/index.vue @@ -28,7 +28,8 @@ const editId = ref(null); const form = reactive({ workDate: new Date(), deptId: null, gyzkId: null, jizu: 1, luosiTime: '', yanse: '', huishu: 0, chetaiStart: 0, chetaiEnd: 0, - chetaishu: 0, isbanche: false, hours: 0, guige: '', chesu: 0 + chetaishu: 0, isbanche: false, hours: 0, tongshu: 0, meitongjianliang: 0, + guige: '', chesu: 0 }); async function load() { @@ -86,7 +87,8 @@ function openCreate() { Object.assign(form, { workDate: new Date(), deptId: deptOptions.value[0]?.id || null, gyzkId: null, jizu: 1, luosiTime: '', yanse: yanseOptions[0] || '', huishu: 0, chetaiStart: 0, chetaiEnd: 0, - chetaishu: 0, isbanche: false, hours: 0, guige: guigeOptions.value[0] || '', chesu: 0 + chetaishu: 0, isbanche: false, hours: 0, tongshu: 0, meitongjianliang: 0, + guige: guigeOptions.value[0] || '', chesu: 0 }); zkName.value = ''; dialogVisible.value = true; @@ -100,7 +102,9 @@ function openEdit(row) { workDate: row.workDate, deptId: row.deptId, gyzkId: row.gyzkId, jizu: row.jizu, luosiTime: row.luosiTime, yanse: row.yanse, huishu: row.huishu, chetaiStart: row.chetaiStart, chetaiEnd: row.chetaiEnd, chetaishu: row.chetaishu, - isbanche: row.isbanche, hours: row.hours, guige: row.guige, chesu: row.chesu + isbanche: row.isbanche, hours: row.hours, + tongshu: row.tongshu || 0, meitongjianliang: row.meitongjianliang || 0, + guige: row.guige, chesu: row.chesu }); zkName.value = row.zhuangKouName || ''; dialogVisible.value = true; @@ -249,6 +253,18 @@ onMounted(() => { load(); loadDepts(); loadGuige(); }); + + + + + + + + + + + + diff --git a/web/src/views/prod/spjc/index.vue b/web/src/views/prod/spjc/index.vue new file mode 100644 index 0000000..a80b613 --- /dev/null +++ b/web/src/views/prod/spjc/index.vue @@ -0,0 +1,287 @@ + + + + + diff --git a/web/src/views/prod/sptz/index.vue b/web/src/views/prod/sptz/index.vue new file mode 100644 index 0000000..de7b7c6 --- /dev/null +++ b/web/src/views/prod/sptz/index.vue @@ -0,0 +1,272 @@ + + + + + diff --git a/web/src/views/prod/xuanjian/XjpbBanzuPanel.vue b/web/src/views/prod/xuanjian/XjpbBanzuPanel.vue new file mode 100644 index 0000000..b58790a --- /dev/null +++ b/web/src/views/prod/xuanjian/XjpbBanzuPanel.vue @@ -0,0 +1,208 @@ + + + + + diff --git a/web/src/views/prod/xuanjian/XjrbPanel.vue b/web/src/views/prod/xuanjian/XjrbPanel.vue new file mode 100644 index 0000000..4523cac --- /dev/null +++ b/web/src/views/prod/xuanjian/XjrbPanel.vue @@ -0,0 +1,390 @@ + + + + + diff --git a/web/src/views/prod/xuanjian/XjrbPlanPanel.vue b/web/src/views/prod/xuanjian/XjrbPlanPanel.vue new file mode 100644 index 0000000..9052f5b --- /dev/null +++ b/web/src/views/prod/xuanjian/XjrbPlanPanel.vue @@ -0,0 +1,404 @@ + + + + + diff --git a/web/src/views/prod/xuanjian/index.vue b/web/src/views/prod/xuanjian/index.vue new file mode 100644 index 0000000..0b945dc --- /dev/null +++ b/web/src/views/prod/xuanjian/index.vue @@ -0,0 +1,30 @@ + + + diff --git a/web/src/views/prod/zjrb/ZjrbEditDialog.vue b/web/src/views/prod/zjrb/ZjrbEditDialog.vue new file mode 100644 index 0000000..77ef53c --- /dev/null +++ b/web/src/views/prod/zjrb/ZjrbEditDialog.vue @@ -0,0 +1,531 @@ + + + + + diff --git a/web/src/views/prod/zjrb/index.vue b/web/src/views/prod/zjrb/index.vue new file mode 100644 index 0000000..ef50776 --- /dev/null +++ b/web/src/views/prod/zjrb/index.vue @@ -0,0 +1,354 @@ + + + + + diff --git a/web/src/views/wms/components/ChukuPanel.vue b/web/src/views/wms/components/ChukuPanel.vue new file mode 100644 index 0000000..3b3763c --- /dev/null +++ b/web/src/views/wms/components/ChukuPanel.vue @@ -0,0 +1,715 @@ + + + + + diff --git a/web/src/views/wms/components/PiciSelectDialog.vue b/web/src/views/wms/components/PiciSelectDialog.vue new file mode 100644 index 0000000..83ac940 --- /dev/null +++ b/web/src/views/wms/components/PiciSelectDialog.vue @@ -0,0 +1,278 @@ + + + + + diff --git a/web/src/views/wms/components/RukuPanel.vue b/web/src/views/wms/components/RukuPanel.vue new file mode 100644 index 0000000..77dd37b --- /dev/null +++ b/web/src/views/wms/components/RukuPanel.vue @@ -0,0 +1,722 @@ + + + + + diff --git a/web/src/views/wms/components/SsPici.vue b/web/src/views/wms/components/SsPici.vue new file mode 100644 index 0000000..003424d --- /dev/null +++ b/web/src/views/wms/components/SsPici.vue @@ -0,0 +1,528 @@ + + + + + diff --git a/web/src/views/wms/components/TaizhangPanel.vue b/web/src/views/wms/components/TaizhangPanel.vue new file mode 100644 index 0000000..dbe87cb --- /dev/null +++ b/web/src/views/wms/components/TaizhangPanel.vue @@ -0,0 +1,295 @@ + + + + + diff --git a/web/src/views/wms/components/WjCgbx.vue b/web/src/views/wms/components/WjCgbx.vue new file mode 100644 index 0000000..83927c9 --- /dev/null +++ b/web/src/views/wms/components/WjCgbx.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/web/src/views/wms/components/WjCgplan.vue b/web/src/views/wms/components/WjCgplan.vue new file mode 100644 index 0000000..b0ef3c9 --- /dev/null +++ b/web/src/views/wms/components/WjCgplan.vue @@ -0,0 +1,240 @@ + + + + + diff --git a/web/src/views/wms/components/WjCrk.vue b/web/src/views/wms/components/WjCrk.vue new file mode 100644 index 0000000..50f9fb3 --- /dev/null +++ b/web/src/views/wms/components/WjCrk.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/web/src/views/wms/components/WjKucun.vue b/web/src/views/wms/components/WjKucun.vue new file mode 100644 index 0000000..3cd2aef --- /dev/null +++ b/web/src/views/wms/components/WjKucun.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/web/src/views/wms/components/YlCaigou.vue b/web/src/views/wms/components/YlCaigou.vue new file mode 100644 index 0000000..a108332 --- /dev/null +++ b/web/src/views/wms/components/YlCaigou.vue @@ -0,0 +1,191 @@ + + + + + diff --git a/web/src/views/wms/components/YlFbplan.vue b/web/src/views/wms/components/YlFbplan.vue new file mode 100644 index 0000000..78cb53d --- /dev/null +++ b/web/src/views/wms/components/YlFbplan.vue @@ -0,0 +1,262 @@ + + + + + diff --git a/web/src/views/wms/fuchanpin/index.vue b/web/src/views/wms/fuchanpin/index.vue new file mode 100644 index 0000000..d9c59a2 --- /dev/null +++ b/web/src/views/wms/fuchanpin/index.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/web/src/views/wms/moduleConfig.js b/web/src/views/wms/moduleConfig.js new file mode 100644 index 0000000..16613e5 --- /dev/null +++ b/web/src/views/wms/moduleConfig.js @@ -0,0 +1,153 @@ +// 仓储(WMS)五个模块的差异化配置:对齐 F9 WinClient\F9.Business.WMS +// module: 1原料 2副产品 3中间品 4生丝 5物资 + +export const WMS_MODULES = { + 1: { + name: '原料', + detailItem: 'ylzk', // 出入库明细按「原料庄口」 + tzItem: 'ylzk', // 库存台账按「原料庄口」 + valueMode: 'weight', // 件数 / 净重 + showSunhao: true, // 运损 + showBangmadan: true, // 磅码单 + showYangjian: true, // 样茧件数(台账) + ruku: { gys: true, lianxiren: true, phone: true, carno: true, kuguan: true }, + chuku: { gyzk: true, kh: true, kuguan: true }, + extras: ['caigou', 'fbplan'] + }, + 2: { + name: '副产品', + detailItem: 'wuzi', + tzItem: 'wuzi', + valueMode: 'weight', + ruku: { kh: true, gyzk: true, dep: true, kuguan: true }, + chuku: { kh: true, dbdep: true, kuguan: true }, + extras: [] + }, + 3: { + name: '中间品', + detailItem: 'wuzi', + tzItem: 'wuzi', + valueMode: 'weight', + showBiaobao: true, // 标包重量 / 包装重量 + showGyzkInDetail: true, + showDepInDetail: true, + showBanci: true, // 班次 + ruku: { dep: true, kuguan: true }, + chuku: { kh: true, banci: true, kuguan: true }, + extras: ['ltj'] // 笼筒架台账 + }, + 4: { + name: '生丝', + detailItem: 'wuzi', + tzItem: 'wuzi', + valueMode: 'weight', + showPici: true, // 批次号 / 批数 + showJidiWaigou: true, // 基地 / 外购 + jianshuLabel: '批数', + ruku: { kuguan: true }, + chuku: { kh: true, kuguan: true }, + extras: ['pici'] // 批次包件 + }, + 5: { + name: '物资', + detailItem: 'wuzi', + tzItem: 'wuzi', + valueMode: 'amount', // 数量 / 金额 + showAmount: true, + ruku: { gys: true, caigouyuan: true, pay: true, rkStatus: true, kuguan: true }, + chuku: { lldep: true, usedep: true, dbdep: true, lingliaoren: true, useLocation: true, ckStatus: true, kuguan: true }, + extras: ['kucun', 'crk', 'cgplan', 'cgbx'] + } +}; + +export function moduleMeta(module) { + return WMS_MODULES[Number(module)] || WMS_MODULES[5]; +} + +// 台账两列表头 +export function valueLabels(module) { + const m = moduleMeta(module); + return m.valueMode === 'amount' + ? { jianshu: '数量', jingzhong: '金额', yingkui: '金额盈亏', unit2: '元' } + : { jianshu: m.jianshuLabel || '件数', jingzhong: '重量', yingkui: '重量盈亏', unit2: 'kg' }; +} + +export function itemLabel(module, kind = 0) { + const m = moduleMeta(module); + if (kind === 1) return '工艺庄口'; + return m.tzItem === 'ylzk' ? '原料庄口' : '物资名称'; +} + +export const KC_STATUS_OPTIONS = [ + { value: 0, label: '全部' }, + { value: 1, label: '待入库' }, + { value: 2, label: '已入库' }, + { value: 3, label: '部分出库' }, + { value: 4, label: '已出库' } +]; + +export const FB_TYPE_OPTIONS = [ + { value: 1, label: '翻包中' }, + { value: 2, label: '完成' }, + { value: 3, label: '暂停' } +]; + +export const RUKU_STATUS_OPTIONS = [ + { value: 1, label: '有效' }, + { value: 2, label: '作废' }, + { value: 3, label: '无' } +]; + +export const CGPLAN_STATUS_OPTIONS = [ + { value: 0, label: '草稿' }, + { value: 1, label: '已确认' }, + { value: 2, label: '已完成' } +]; + +export function fmt2(v) { + const n = Number(v || 0); + return n.toFixed(2); +} + +// 提交前清洗:下拉清空后为 null,而后端数值型(long / int / decimal)不接受 null, +// 这里统一把 null / undefined 转成 0(日期型保持 null)。 +const KEEP_NULL_KEYS = ['scDate', 'guobangShijian', 'ckTime', 'hongchongId']; +const NUMERIC_KEYS = [ + 'id', 'jianshu', 'maozhong', 'pizhong', 'jingzhong', 'biaobaoZhongliang', 'baozhuangZhongliang', + 'shuliang', 'danjia', 'jine', 'sunhao', 'kucunNum', 'piciNum', 'baojianNum', 'baoshu', + 'startBaojianNum', 'huichaolv', 'gongliang', 'yingkui', 'yangjianjianshu', + 'qingjie', 'jiejing', 'piancha', 'zongcha', 'qieduan', + 'type', 'rkStatus', 'bxStatus', 'ckStatus', 'status', 'fbType', 'rukuStatus', 'codeType', 'kcStatus', 'kind' +]; + +export function cleanPayload(obj) { + if (obj === null || obj === undefined) return 0; + if (Array.isArray(obj)) return obj.map((x) => (x && typeof x === 'object' ? cleanPayload(x) : x)); + if (typeof obj !== 'object' || obj instanceof Date) return obj; + + const out = {}; + Object.keys(obj).forEach((k) => { + const v = obj[k]; + if (v === null || v === undefined) { + if (KEEP_NULL_KEYS.includes(k)) out[k] = null; + else if (/Id$/.test(k) || NUMERIC_KEYS.includes(k)) out[k] = 0; + else out[k] = ''; + return; + } + if (Array.isArray(v)) { + out[k] = v.map((x) => (x && typeof x === 'object' ? cleanPayload(x) : x)); + return; + } + if (typeof v === 'object' && !(v instanceof Date)) { + out[k] = cleanPayload(v); + return; + } + out[k] = v; + }); + return out; +} + +export function today() { + const d = new Date(); + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; +} diff --git a/web/src/views/wms/ss/index.vue b/web/src/views/wms/ss/index.vue new file mode 100644 index 0000000..27ccfbe --- /dev/null +++ b/web/src/views/wms/ss/index.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/web/src/views/wms/wj/index.vue b/web/src/views/wms/wj/index.vue new file mode 100644 index 0000000..2953868 --- /dev/null +++ b/web/src/views/wms/wj/index.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/web/src/views/wms/yuanliao/index.vue b/web/src/views/wms/yuanliao/index.vue new file mode 100644 index 0000000..bfb33d6 --- /dev/null +++ b/web/src/views/wms/yuanliao/index.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/web/src/views/wms/zjp/index.vue b/web/src/views/wms/zjp/index.vue new file mode 100644 index 0000000..c6b3d64 --- /dev/null +++ b/web/src/views/wms/zjp/index.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/工作进度/工作进度-14.md b/工作进度/工作进度-14.md new file mode 100644 index 0000000..d442154 --- /dev/null +++ b/工作进度/工作进度-14.md @@ -0,0 +1,143 @@ +# 工作进度-14:生产-前缫「煮茧日报」落地 + +> 时间:2026-10-22(承接工作进度-13) +> 需求:把 `D:\AntaiProject - 副本` 生产-前缫的「煮茧日报(ZJRB)」移植进本系统。 +> 参考实现:`WinClient\F9.Business.Prod\QS\ZJRB\`(Page_Zjrb、Page_Zjrb_EditV2)、 +> `Application\CQRS\Business\PROD\QS\ZJRB\`(Zjrb_SaveCommand / Zjrb_DelCommand / CalculateJizuSaosiHoursCommand)、 +> `Domain\Entities\Business\PROD\QS\`(Zjrb / ZjrbJizu / Zjrb_Banci / ZjrbJizuBanci)。 + +--- + +## 一、功能设计 + +``` +生产-前缫 +├── 机组车台 +├── 落丝记录 +├── 车间班次 +├── 看台排班 +├── 看台记录 +└── 煮茧日报(本次新增) /prod/qianxian/zjrb + 过滤:账期(月份)+ 车间(前缫)+ 庄口/规格关键字 + 主表:日期、上日、车间、庄口、规格;上存/领入/今缫/退回/今存(包数 + 重量,多级表头) + 选中行 → 下方联动展示该日报的「机组」桶数明细(含合计行) + 按钮:删除、新增、修改、计算缫丝时长 +``` + +| F9 行为 | 本系统实现 | +|---|---| +| 列表按「账期(月份)+ 车间 + 关键字」查询,无分页 | 同口径,另加后端分页(`pageIndex/pageSize`) | +| 上表主表 + 下表机组联动(点击行刷新机组) | 同左(主表 `row-click` → `GET /{id}/jizus`) | +| `删除`:四张表级联删除 | `DELETE /api/prod/zjrb/{id}`(主表 + 班次 + 机组 + 机组班次物理删除) | +| `新增`:按上一日结存带出上存、每桶茧量、工艺落丝桶数 | `GET /defaults?date=&depId=&gyzkId=&guige=` | +| `修改`:回填主表 + 班次 + 机组 + 机组班次 | `GET /{id}` | +| `保存`:主表 + 三层明细一起提交 | `POST /save`(主表 upsert + 三层明细先删后建) | +| `计算缫丝时长`:按「日期 + 车间」整日重算 | `POST /calculate-saosi-hours?date=&depId=` | + +**计算口径**(移植 `Page_Zjrb_EditV2` 的 `CalItem` / `Cal_Zhibiao` / `Cal_Zhibiao_Main`,前后端同口径、服务端保存时重算): + +``` +机组.今缫桶数 = 甲班今缫 + 乙班今缫 +机组.今存桶数 = 上存 + 今缫 − 今落 + (今落为 0 且余量 ≥ 工艺落丝桶数时,自动把今落置为工艺落丝桶数再扣减) +班次.今缫重量 = 该班次机组今缫桶数 × 每桶茧量 ÷ 1000 (每桶茧量单位 g,重量 kg) +班次.领入/退回重量 = 包数 × 联统茧标重 +班次.今缫包数 = 今缫重量 ÷ 联统茧标重(四舍五入取整) +班次.今存 = 上存 + 领入 − 今缫 − 退回 +主表 = 甲班 + 乙班(包数/重量各 5 项) +``` + +**校验**(对齐 F9 `AddValidRules`,前端拦截 + 服务端兜底):每桶茧量 > 0、工艺落丝桶数 > 0、上存包数 > 0 时上存重量必须 > 0、机组今缫桶数 > 0 时缫丝时长必须 > 0、机组缫丝时长 ≤ 20、同「日期 + 车间 + 庄口 + 规格」唯一。 + +--- + +## 二、后端 + +### 2.1 实体(`Domain/Entities/Prod/Zjrb.cs`) + +| 类 | 表 | 主要字段 | +|---|---|---| +| `ZjrbRecord` | `ZjrbRecords` | Date、DeptId、GyzkId、Guige、LastDate、StandardLuositongshu、MeitongJianliang + 包数/重量各 5 项 | +| `ZjrbJizu` | `ZjrbJizus` | ZjrbId、Jizu、JizuSaosiHours、TongshuShangcun/Jinsao/Jinluo/Jincun | +| `ZjrbBanci` | `ZjrbBancis` | ZjrbId、Banci(甲/乙)、包数/重量各 5 项 | +| `ZjrbJizuBanci` | `ZjrbJizuBancis` | ZjrbId、ZjrbJizuId、Jizu、Banci、TongshuJinsao | + +> 均继承 `BaseEntity`(自增主键 + 软删除),表结构由启动时 `CodeFirst.SyncStructure` 自动创建,无需迁移。 +> F9 的 `Zjrb_Banci.BanciId`(指向车间班次)在本系统直接落 `Prod_Banci_Enum`(甲=1/乙=2),与看台排班/看台记录一致。 + +### 2.2 服务与接口(`ZjrbService` / `ZjrbController`,路由 `api/prod/zjrb`) + +``` +GET /api/prod/zjrb/list?dateFrom=&dateTo=&depId=&like=&pageIndex=&pageSize= +GET /api/prod/zjrb/{id} 修改回填(主表 + 班次 + 机组) +GET /api/prod/zjrb/{id}/jizus 机组明细(列表页下半表) +GET /api/prod/zjrb/defaults?date=&depId=&gyzkId=&guige= 新增默认值 +POST /api/prod/zjrb/save 整条保存 +DELETE /api/prod/zjrb/{id} 级联删除 +POST /api/prod/zjrb/calculate-saosi-hours?date=&depId= 计算缫丝时长 +``` + +- 新增默认值来源:同「车间 + 庄口(+ 规格)」下 `Date < 目标日期` 的最近一条日报 + → 上存包数/重量 = 上一日今存;每桶茧量、工艺落丝桶数、`LastDate` 一并继承; + 班次行按「车间班次」配置(未配置则甲乙两班)生成,机组行按「机组车台」生成; +- 联统茧标重取参数配置 `生产\前缫\煮茧 → 联统茧标重`(本库已配置为 18),随默认值/回填一并下发,弹窗内可临时调整; +- `CalculateJizuSaosiHours` 算法(对齐 F9):机组「今缫桶数 > 0」的班次数为 2 → 18 小时,否则 9 小时; + 按「庄口 + 规格 + 机组」分组,以 `Σ(每桶茧量 × 机组今缫桶数)` 为权重把机组总时长按比例分配到各条日报的机组行。 + +### 2.3 菜单 + +`DbSeeder.SeedProdMenus` 生产-前缫子菜单新增第 6 项,并做排序归一: + +| 顺序 | 菜单 | 路径 | 组件 | 权限标识 | +|---|---|---|---|---| +| 6 | 煮茧日报 | /prod/qianxian/zjrb | prod/zjrb/index | prod:zjrb:list | + +启动日志:`[DbSeeder] 生产模块菜单已就绪(生产-前缫 / 生产-后缫)`,`/auth/menus` 已返回 6 个子菜单。 + +--- + +## 三、前端 + +| 文件 | 说明 | +|---|---| +| `views/prod/zjrb/index.vue` | 日报列表(账期月份 + 车间 + 关键字)、主表多级表头、选中行联动机组明细、合计行、删除/新增/修改/计算缫丝时长 | +| `views/prod/zjrb/ZjrbEditDialog.vue` | 录入弹窗:基础信息 + 班次网格(甲乙班 5 组包数重量)+ 机组网格(时长/上存/甲乙班今缫/今落),改动即时重算 | +| `router/index.js` | `componentMap` 注册 `prod/zjrb/index` | + +交互要点: +- 新增时选「工艺庄口 / 规格」后自动带出上一日结存与机组行,并用 `ElMessage` 提示「未维护机组车台」等异常; +- 弹窗内今缫重量/今缫包数/今存/今缫桶数/今存桶数均为只读派生值,避免与口径冲突; +- 删除与计算缫丝时长均二次确认(计算会覆盖该日该车间全部机组时长)。 + +--- + +## 四、验证记录 + +| 项 | 结果 | +|---|---| +| `dotnet build`(Domain/Application/Infrastructure) | ✅ 0 错误 | +| `dotnet build`(Api,临时输出目录,规避运行中进程文件锁) | ✅ 0 错误 | +| `vite build` | ✅ 通过(仅既有体积/SignalR 注释警告) | +| 菜单 | ✅ 生产-前缫 6 个子菜单,煮茧日报排第 6 | +| 保存(甲乙班 + 2 机组,每桶茧量 600g、标重 18) | ✅ 主表今缫 = round(54/18) = 3 包 / 54 kg;主表五组值 = 甲乙相加 | +| 机组今存自动补落丝 | ✅ 机组 上存 50 + 今缫 60 = 110 < 170(工艺落丝桶数)→ 今落 0、今存 110 | +| 计算缫丝时长 | ✅ 机组1(单班)9 h、机组2(甲乙班均有今缫)18 h,提示「已更新 1 条机组缫丝时长」 | +| 新增默认值 | ✅ 次日带出:上日 2026-10-22、上存 28 包 / 1686 kg、机组上存 110 / 110、每桶茧量 600、落丝桶数 170、标重 18 | +| 唯一性 | ✅ 同「日期 + 车间 + 庄口 + 规格」再存返回 400「已存在相同的煮茧日报!」 | +| 修改 | ✅ 甲班领入 20 → 30 包后:今存 36 + 14 = 50 包 / 1068 + 456 = 1524 kg | +| 删除 | ✅ 删除后列表为该日 0 条、回读返回「煮茧日报不存在或已删除」 | +| 删除后重建无残留 | ✅ 机组桶数 90 / 130 / 0 / 220 未翻倍(三层明细先删后建生效) | +| 测试数据清理 | ✅ 验证用日报已全部删除,未在库中留下业务数据 | + +> 验证方式:用 `-p:BaseOutputPath=obj\verify\` 另编一份 Api,在 3001 端口以真实库启动(`ConnectionStrings__Default` + `Kestrel__Endpoints__Http__Url` 覆盖),验证完已停止该实例。 +> 3000 端口的后端进程已重启(旧进程无 `api/prod/zjrb` 控制器,首次进入菜单会 404 并提示「请求失败,请稍后重试」);重启后 `GET /api/prod/zjrb/list` 返回 200,`/auth/menus` 生产-前缫为 6 项。 + +--- + +## 五、说明与后续 + +1. **未导入历史数据**:本次没有随附煮茧日报的导出数据(xlsx/csv),故未做 `ProdSeedData` 扩展;如需把 F9 的 `biz_prod_qs_zjrb*` 四张表导入,可按工作进度-13 的 CSV 内嵌 + 幂等策略追加(主表按「日期+车间+庄口+规格」、机组按「日期+机组」、班次按「日期+班次」)。 +2. **保存语义更严格**:F9 的 `Zjrb_SaveCommand` 是逐行 upsert(前端删行不会落库),本系统为「整条替换」(三层明细先删后建),行被移除时会真正删除。 +3. **机组行不再过滤全 0 行**:与 F9 现用保存命令一致(旧 `CreateZjrbCommand` 会过滤三桶数全 0 的机组行)。 +4. **联统茧标重**:读取参数配置 `生产\前缫\煮茧 → 联统茧标重`(当前 18);未配置时重量换算按录入值保留,不会清零。 +5. **待接入**:① 计件工资(F9 `CalcLdcct` 用「机组班次今缫桶数 × 每桶茧量」)已具备数据基础,可在薪酬模块直接取用;② 落丝记录联动(F9 用煮茧的每桶茧量/机组落丝桶数回填落丝记录)——本系统落丝记录暂无桶数/每桶茧量字段,需先扩展;③ 报表「煮茧日报月报 / 机组缫丝时长 / 联统茧盈亏」可基于 `ZjrbRecords + ZjrbJizus` 直接做数据源。 diff --git a/工作进度/工作进度-15.md b/工作进度/工作进度-15.md new file mode 100644 index 0000000..7bc98bf --- /dev/null +++ b/工作进度/工作进度-15.md @@ -0,0 +1,162 @@ +# 工作进度-15:生产-后缫完善(秤大丝 / 公量纤度 / 丝片检查 / 丝片台帐) + +> 时间:2026-10-24(承接工作进度-14) +> 需求:把 `D:\AntaiProject - 副本` 生产-后缫(`WinClient\F9.Business.Prod\HS\`)尚未落地的模块移植进本系统。 +> 参考实现:`WinClient\F9.Business.Prod\HS\`(CDS / GLXD / SPJC / SPTZ)、 +> `Application\CQRS\Business\PROD\HS\`(Cds_SaveCommand / Cds_Lsjl_CalZhibiaoCommand / Glxd_Update / Spjc_*/Sptz_*)、 +> `Domain\Entities\Business\PROD\HS\`(Cds_Chetai / Cds_Lsjl / Glxd / Spjc / Sptz)。 +> 移植后本系统「生产-后缫」子菜单由 2 项补齐到 6 项。 + +--- + +## 一、功能设计 + +``` +生产-后缫 +├── 纤度检验 (已有) +├── 黑板检验 (已有) +├── 秤大丝 /prod/houxian/cds 本次新增 +├── 公量纤度 /prod/houxian/glxd 本次新增 +├── 丝片检查 /prod/houxian/spjc 本次新增 +└── 丝片台帐 /prod/houxian/sptz 本次新增 +``` + +| F9 行为 | 本系统实现 | +|---|---| +| **秤大丝**:列表按「日期 + 车间」列出落丝记录,行 → 弹窗「秤大丝」,按车台录入丝量1~4 | `GET /api/prod/cds/list`、`GET /{lsjlId}`、`POST /save`、`DELETE /{lsjlId}` | +| 汇总 = Σ个数 / Σ丝量,并算茧量、缫折、台产(`Cds_Lsjl_CalZhibiaoCommand`) | 服务端保存时重算,列表直接展示 | +| **公量纤度**:按「日期 + 车间」整日批量录入,左侧带出纤度检验的平均纤度 | `GET /api/prod/glxd/list`、`POST /save`(整日提交) | +| **丝片检查**:按「日期 + 车间 + 工艺庄口 + 规格」记录前缫 7 项 / 后缫 12 项疵点片数 | `GET /api/prod/spjc/list`、`GET /{id}`、`POST /save`、`DELETE /{id}` | +| 前缫合计 / 后缫合计自动求和(`Page_Spjc_Edit` 的 getter) | 前端即时重算 + 服务端落库前再算一次 | +| **丝片台帐**:按账期月份列出每日台帐;新增时上一日 / 上存取上一本台账 | `GET /api/prod/sptz/list?dateFrom=&dateTo=`、`GET /defaults?date=`、`POST /save`、`DELETE /{id}` | +| 台帐派生值:上日遗留/今日新增 = 上存 − 浸丝;今存 = 下丝 + 上日遗留/今日新增 | 前端即时算 + 服务端保存时重算(同 F9 `CalSryl_Jrxz` / `CalJc`) | +| 台帐「整理片数」可按当日丝片检查汇总取数(F9 弹窗里的取数按钮) | `GET /defaults` 一并返回 `zlTichu` / `zlZhengpin` | +| 新增台帐时若已存在更晚日期的台帐 → 拦截 | 同上(`LatestDate`),前端也做二次校验 | + +**计算口径**: + +``` +秤大丝:车台.个数 = 丝量1~4 中 >0 的个数;车台.小计丝量 = Σ丝量 + 汇总.已秤数 = Σ个数;汇总.已秤丝量 = Σ小计丝量(g) + 应秤数 = 车台数 × 4 ×(半车 0.5 / 整车 1);待秤数 = 应秤数 − 已秤数 + 茧量 = 落丝桶数 × 每桶茧量(g) + 缫折 = 茧量 ÷ 丝量 × 100(≤100 视为异常,不落指标,与 F9 一致) + 台产 = 丝量 ÷ 落丝时长 ÷ 车台数 +丝片台帐:上日遗留/今日新增 = 上存 − 浸丝;今存 = 下丝 + 上日遗留/今日新增 + 整理剔除 = Σ当日丝片检查前缫合计 + 整理正品 = Σ(总片数 + 学工片数) − 剔除 +``` + +--- + +## 二、后端 + +### 2.1 实体(`Domain/Entities/Prod/`) + +| 类 | 文件 | 表 | 主要字段 | +|---|---|---|---| +| `CdsLsjl` | `Cds.cs` | `CdsLsjls` | LsjlId(唯一)、Num、Siliang、Jianliang、Saozhe、Taichan | +| `CdsChetai` | `Cds.cs` | `CdsChetais` | LsjlId + Chetaihao(唯一)、Sl1~Sl4、Num、Siliang | +| `Glxd` | `Glxd.cs` | `Glxds` | LsjlId(唯一)、Gongliangxiandu | +| `Spjc` | `Spjc.cs` | `Spjcs` | Date + DeptId + GyzkId + Guige(唯一)、总/学工片数、Qs×7 + QsHeji、Hs×12 + HsHeji、Memo | +| `Sptz` | `Sptz.cs` | `Sptzs` | Date(唯一)、DateLast、Js×5、Fs/Bs 片数、Zl 正品/剔除、Memo | +| `LsjlRecord` | `LsjlRecord.cs` | `LsjlRecords` | **新增**:Tongshu(落丝桶数)、Meitongjianliang(每桶茧量 g) | + +> 均继承 `BaseEntity`(自增主键 + 软删除),建表沿用 `DbSeeder.SyncStructure`; +> `LsjlRecords` 是存量表,`SyncStructure` 不给旧表加列,故在 `SyncStructure` 末尾追加两条 +> `EnsureColumn(fsql, "LsjlRecords", "Tongshu"/"Meitongjianliang", "INTEGER NOT NULL DEFAULT 0")`。 + +### 2.2 服务与接口(路由统一到 `api/prod/*`) + +``` +GET /api/prod/cds/list?pageIndex=&pageSize=&date=&deptId= 秤大丝列表(落丝记录 + 秤丝汇总) +GET /api/prod/cds/{lsjlId} 称重弹窗(首次按车台范围初始化空行) +POST /api/prod/cds/save 车台明细先删后建 + 汇总指标重算 +DELETE /api/prod/cds/{lsjlId} 清除称重数据(汇总 + 明细,F9 的 Cds_Lsjl_Delete) + +GET /api/prod/glxd/list?date=&deptId= 公量纤度列表(含纤度检验平均纤度) +POST /api/prod/glxd/save 按日期 + 车间整日批量 upsert + +GET /api/prod/spjc/list?pageIndex=&pageSize=&date=&deptId=&like= +GET /api/prod/spjc/{id} 修改回填 +POST /api/prod/spjc/save 合计服务端重算 +DELETE /api/prod/spjc/{id} 物理删除(业务键带唯一索引) + +GET /api/prod/sptz/list?dateFrom=&dateTo=&pageIndex=&pageSize= 账期区间查询 +GET /api/prod/sptz/{id} 修改回填 +GET /api/prod/sptz/defaults?date= 上一本台账 + 上存 + 丝片检查取数 +POST /api/prod/sptz/save 派生值服务端重算 +DELETE /api/prod/sptz/{id} 物理删除(日期唯一) +``` + +要点: +- 唯一性校验在前:丝片检查按「日期+车间+庄口+规格」、丝片台帐按「日期」查重(修改时排除自身),命中返回 400; +- 删除走 `HardDelete`(与煮茧日报同一策略),避免「删除后重建」撞唯一索引; +- 秤大丝「车台明细先删后建」,保证移除的行真正删除。 + +### 2.3 菜单 + +`DbSeeder.SeedProdMenus` 生产-后缫子菜单扩到 6 项,并做排序归一;`BusinessSeedData.SeedMenus` 同步追加同名菜单,避免两套种子不一致: + +| 顺序 | 菜单 | 路径 | 组件 | 权限标识 | +|---|---|---|---|---| +| 1 | 纤度检验 | /prod/houxian/xdjy | prod/xdjy/index | prod:xdjy:list | +| 2 | 黑板检验 | /prod/houxian/hbjy | prod/hbjy/index | prod:hbjy:list | +| 3 | 秤大丝 | /prod/houxian/cds | prod/cds/index | prod:cds:list | +| 4 | 公量纤度 | /prod/houxian/glxd | prod/glxd/index | prod:glxd:list | +| 5 | 丝片检查 | /prod/houxian/spjc | prod/spjc/index | prod:spjc:list | +| 6 | 丝片台帐 | /prod/houxian/sptz | prod/sptz/index | prod:sptz:list | + +--- + +## 三、前端 + +| 文件 | 说明 | +|---|---| +| `views/prod/cds/index.vue` | 秤大丝列表(日期 + 车间)+ 称重弹窗(车台 × 丝量1~4、个数/小计即时重算、汇总栏、偏轻/偏重着色)+ 清除称重 | +| `views/prod/glxd/index.vue` | 公量纤度:按「日期 + 车间」一次加载,表格内直接编辑,整日批量保存 | +| `views/prod/spjc/index.vue` | 丝片检查:多级表头(前缫 / 后缫)+ 新增/修改/删除弹窗(合计只读实时汇总)+ 工艺庄口选择页 | +| `views/prod/sptz/index.vue` | 丝片台帐:账期月份查询 + 多级表头(浸丝/返丝/编丝/整理)+ 新增/修改/删除,派生值只读,「取丝片检查」按钮 | +| `views/prod/lsjl/index.vue` | 落丝记录表单补「落丝桶数 / 每桶茧量」(秤大丝算茧量、缫折、台产的输入) | +| `router/index.js` | `componentMap` 注册 `prod/cds/index`、`prod/glxd/index`、`prod/spjc/index`、`prod/sptz/index` | + +交互要点: +- 丝量 ≤900 g 标绿(偏轻)、≥1200 g 标橙(偏重),与 F9 `CellFormatting` 一致; +- 丝片检查 / 丝片台帐的合计、上日遗留今日新增、今存均为只读派生值,避免与服务端口径冲突; +- 丝片台帐新增时不允许早于已有最新台账日期(对齐 F9「已生成 X 台账,无法新增该日期台账」)。 + +--- + +## 四、验证记录 + +| 项 | 结果 | +|---|---| +| `dotnet build`(Domain / Application / Infrastructure) | ✅ 0 错误 | +| `dotnet build`(Api,临时输出目录 `obj\verify\`) | ✅ 0 错误 | +| IDE 诊断(新增 4 个 vue + `router/index.js` + `lsjl/index.vue`) | ✅ 0 | +| `vite build` | ✅ 通过(仅既有 chunk 体积警告) | +| 启动种子 | ✅ `[DbSeeder] 生产模块菜单已就绪`,无结构同步异常;`EnsureColumn` 补列生效 | +| 菜单与权限 | ✅ `/auth/login` 返回的 permissions 含 `prod:cds:list` / `prod:glxd:list` / `prod:spjc:list` / `prod:sptz:list` | +| 四个列表接口 | ✅ 200(`cds` total=3、`glxd` / `spjc` / `sptz` 空表 200) | +| 丝片检查保存 | ✅ 前缫合计 13、后缫合计 55(服务端重算) | +| 丝片检查唯一性 | ✅ 同「日期+车间+庄口+规格」再存被拒(400),未产生第二条 | +| 丝片台帐派生值 | ✅ 上存 120 − 浸丝 40 = 80;下丝 80 + 80 = 今存 160 | +| 丝片台帐滚动 | ✅ 次日 `defaults`:`dateLast=2026-10-20`、`jsShangcun=160`(= 上一日今存) | +| 丝片台帐取数 | ✅ 当日丝片检查 Σ前缫合计 = 13 → 剔除 13;Σ(总片数+学工片数) 110 − 13 = 正品 97 | +| 秤大丝称重 | ✅ 车台 21(900/1100)+ 22(1000)→ 已秤数 3、已秤丝量 3000 g | +| 秤大丝指标 | ✅ 茧量 100×600 = 60000、缫折 = 60000/3000×100 = 2000、台产 = 3000/9/20 = 16.67 | +| 删除后无残留 | ✅ 删除秤大丝/丝片检查/丝片台帐后列表回到 0 条,「删除后重建」不再撞唯一索引 | +| 测试数据清理 | ✅ 验证用的丝片检查、丝片台帐、秤大丝数据已全部删除;`LsjlRecords.Tongshu/Meitongjianliang` 已还原为 0 | + +> 验证方式:另编一份 Api 到 `obj\verify\`,以真实库在 3000 端口启动(原无进程占用该端口),验证完已停止实例并删除临时输出与日志。 +> Node 不在默认 PATH 里,需先 `set PATH=C:\Users\32453\.workbuddy\binaries\node\versions\22.22.2-6;%PATH%` 才能跑 vite;最终 `vite build` 已通过。 + +--- + +## 五、说明与后续 + +1. **落丝记录新增两个字段**:`Tongshu`(落丝桶数)、`Meitongjianliang`(每桶茧量)是秤大丝算茧量/缫折/台产的必需输入,F9 的 `Lsjl` 实体也有这两个字段(本系统此前漏移植),已在落丝记录表单补上录入项。 +2. **秤大丝删除**:F9 客户端没有删除按钮(只有 `Cds_Lsjl_DeleteCommand` 未暴露),本系统在列表加了「删除」,用于清除误录的称重数据;默认按 F9 的粒度整条(汇总 + 车台明细)物理删除。 +3. **未移植 F9 的本地设备能力**:`Page_Cds_Edit` 依赖串口电子秤 + 语音播报 + 本地缓存(`CacheLocalfileService`),Web 端不支持,改为直接录入;偏重/偏轻的语音提示折算UI 上的单元格着色。 +4. **未移植 HS\XCJC(现场检查)**:F9 只有 `Xcjc` 实体、没有对应页面(未见 `Page_Xcjc*`),本次不做;如有需要可单独加一组「温度/湿度/车速」维护页。 +5. **待接入**:① 秤大丝/丝片检查/丝片台帐的 Grid++ 打印`(报表中心目前只有 `lsjl.grf`);② 历史的 `biz_prod_hs_cds_*` / `biz_prod_hs_spjc` / `biz_prod_hs_sptz` 数据导出后可按工作进度-13 的 CSV 内嵌 + 幂等策略导入;③ 缫折/台产可作为成本核算与计件工资的数据源。 diff --git a/工作进度/工作进度-16.md b/工作进度/工作进度-16.md new file mode 100644 index 0000000..fb3db8d --- /dev/null +++ b/工作进度/工作进度-16.md @@ -0,0 +1,211 @@ +# 工作进度-16:薪资-挡车车头产量计算(CALCLDCCT)及其全部依赖 + +> 时间:2026-10-24(承接工作进度-15) +> 需求:把 `D:\AntaiProject - 副本\trunk\WinClient\F9.Business.PayRoll\PAYROLL\CALCLDCCT` 模块移植进本系统, +> 并补齐它依赖的全部功能(配置 + 数据源)。 +> 参考实现:`WinClient\F9.Business.PayRoll\PAYROLL\CALCLDCCT\Page_CalcLdcct.cs`(3067 行)、`Page_CalcLdcct_Edit.cs`、 +> `Application\CQRS\Business\PayRoll\CALCLDCCT\`(CalcLdcct_SaveCommand / CalcLdcct_DeleteByDateRangeDepCommand …)、 +> `Domain\Entities\Business\PayRoll\`(CalcLdcct / CONFGYZK / CONFGZ / CONFDCG / DCXDJK / XinZiJiaGePeiZhi)。 +> 落库表:F9 `biz_payroll_calcldcct` → 本系统 `CalcLdccts`。 + +--- + +## 一、功能设计 + +``` +薪资 +├── 挡车车头产量计算 /payroll/calcldcct 本次新增(核心) +├── 工艺庄口薪资配置 /payroll/confgyzk 本次新增(依赖配置) +├── 工种薪资配置 /payroll/confgz 本次新增(依赖配置) +├── 挡车岗位薪资配置 /payroll/confdcg 本次新增(依赖配置) +├── 挡车纤度奖扣 /payroll/dcxdjk 本次新增(依赖配置) +└── 薪资价格配置 /payroll/xzjg 本次新增(依赖配置) +``` + +| F9 行为 | 本系统实现 | +|---|---| +| 查询:年月(→账期)+ 部门 + 工种 + 姓名/庄口 Like | `GET /api/payroll/calcldcct/list` | +| 一键计算(挡车工):部门穿梭框多选 → 逐部门计算 → 对账报告 | `POST /api/payroll/calcldcct/calc-dcg`(单部门;返回条数/丝量/金额/告警) | +| 一键计算(车头工):**强依赖已存的挡车工产量** | `POST /api/payroll/calcldcct/calc-ctg` | +| 录入:单部门计算后弹窗人工微调(可改产量初始值 / 系数 / 金额-产量) | 表格内 3 列直接编辑 + 「保存微调」整账期覆盖(`POST /save`) | +| 删除:按 账期 × 部门 × 工种 整批删除 | `DELETE /api/payroll/calcldcct?start=&end=&depId=&worktypeId=` | +| 5 套薪资配置各自的增删改查 | 5 个 `api/payroll/conf*` `/dcxdjk` `/xzjg` 控制器 | + +### 计算口径(全部对齐 F9,服务端兜底重算) + +``` +【丝量】 +每桶丝量 = 每车丝量(kg) × 1000 / 标准落丝桶数 +甲乙合计丝量 = Round(Σ今缫桶数 × 每桶丝量, 0) (煮茧日报不分班) +班次丝量 = 甲乙合计 × Σ(挡车数×工时)_班 / Σ(挡车数×工时)_机组日 (挡车) + = 甲乙合计 × Σ(20×工时)_班 / Σ(20×工时)_机组日 (车头,固定 20 台/组) +个人丝量 = 班次丝量 × 挡车数×工时 / Σ(挡车数×工时)_同机组同班 (挡车) + = 班次丝量 × 工时 / (最长工时 × 2) (车头,2 人分摊) +看台时长 = Round(个人工时 × 庄口今缫桶数占比, 1),单庄口时等于个人工时 + +【等级】 +挡车工:木桶短板法 —— 均方差/总差/清洁分/洁净分/二度/三度各自在「生丝定级指标(标准=个人)」里定级,取最低分 +车头工:按挡车工丝量加权平均等级分后回落取整档;无挡车工产量时取庄口配置的 QsDengji/QsDengjifen +未下丝时:等级/等级分/缫折全部回落到庄口配置 + +【单价】 +挡车:绪数 = 挡车数 × 20;<60→50档基数,<70→60档,<100→70档,否则 100 档 + 基础单价 = 岗位基数金额 / 标准出勤天数 / 挡车数 / 标准上班时长 / 庄口工艺台产 +车头:基础单价 = 车头岗位金额 / 标准出勤天数 / 9 / 20 / 庄口工艺台产 × 2 +等级单价增减 = (等级分 − 工艺等级分) / 0.1 × 等级奖扣单价 / 10 / 1000 +实际单价 = 基础单价 + 等级单价增减 + +【质量】 +缫折 = Σ(车台茧量×看台比例) × 100 / Σ(车台丝量×看台比例) + 车台茧量 = 落丝茧量 × 该车台丝量占落丝丝量的比例(茧量只记在落丝层,需按丝量摊到车台) +中心纤度数 = 纤度值 == 生丝规格.中心纤度 的计数 × 看台比例 +野纤数 = 纤度 ≤ 野纤下限 或 ≥ 野纤上限 的计数 × 看台比例 +清洁扣款片数 = (特大糙 + 大糙 + 长结) × 看台比例 +洁净扣款片数 = (85 + 80 + 75 + 70 分档) × 看台比例 +二度/三度 = 黑板检验匀二度/匀三度 × 看台比例 +两班(甲乙)时:中心纤度/野纤/清洁/洁净/二度/三度及其扣款全部 × 0.5 + +【金额】 +产量金额初始值 = 实际单价 × 丝量; 产量金额 = 实际单价 × 丝量 × 计件系数 +缫折考核 = ±2kg 死区不奖不扣,超出扣掉死区后 clamp(挡车 ±10 / 车头 ±3),每 1kg 每 100kg 丝 5元(挡车)/1元(车头) +中心纤度奖 = 中心纤度数 × 1 元 +野纤扣 = −挡车纤度奖扣表扣款(≥2 条取扣款最大的 2 条求和) +清洁扣 = −(特大糙×2 + 大糙×0.5 + 长结×2) +洁净扣 = −(85×0.2 + 80×1 + 75×2 + 70×5) +二度扣/三度扣 = −二度×2 / −三度×20 +``` + +> 关键常量:`20`(每台绪数、车头 20 台/组)、`9`/`2`(车头)、`0.1`(等级分步长)、 +> `0.5`(两班折半)、`1.0/0.4/0.1`(清洁扣分基数)、`2`(缫折死区)、`5/1`(缫折单价)、`225`(F9 弹窗均值折算,本版未移植)。 +> 全程 `MidpointRounding.AwayFromZero`。 + +--- + +## 二、后端 + +### 2.1 实体(`Domain/Entities/PayRoll/`) + +| 类 | 文件 | 表 | 说明 | +|---|---|---|---| +| `CalcLdcct` | `CalcLdcct.cs` | `CalcLdccts` | 产量计算明细,唯一键 `Date+DepId+Banci+Jizu+EmployeeId+WorktypeId+GyzkId+Guige+Chetai` | +| `ConfGyzk` | `Conf.cs` | `ConfGyzks` | 工艺庄口薪资配置,唯一键 `Year+Month+GyzkId` | +| `ConfGz` | `Conf.cs` | `ConfGzs` | 工种薪资配置(40 项),唯一键 `WorkTypeId` | +| `ConfDcg` | `Conf.cs` | `ConfDcgs` | 挡车/车头岗位薪资配置,唯一键 `DepId` | +| `DangCheXianDuJiangKou` | `Conf.cs` | `DangCheXianDuJiangKous` | 挡车纤度奖扣,唯一键 `GuiGe+XianDu` | +| `XinZiJiaGePeiZhi` | `Conf.cs` | `XinZiJiaGePeiZhis` | 薪资价格配置,唯一键 `Group+Key` | + +均继承 `BaseEntity`,建表沿用 `DbSeeder.SyncStructure`。 + +### 2.2 服务 + +| 服务 | 文件 | 说明 | +|---|---|---| +| `ICalcLdcctService` / `CalcLdcctService` | `Services/PayRoll/CalcLdcctService.cs` | 列表 + 挡车/车头一键计算 + 保存 + 删除,内含全部计算口径 | +| `IConfGyzkService` / `ConfGyzkService` | `Services/PayRoll/PayRollConfService.cs` | 工艺庄口配置 CRUD | +| `IConfGzService` / `ConfGzService` | 同上 | 工种配置 CRUD(含 40 项金额) | +| `IConfDcgService` / `ConfDcgService` | 同上 | 挡车岗位配置 CRUD | +| `IDcxdjkService` / `DcxdjkService` | 同上 | 挡车纤度奖扣 CRUD | +| `IXzjgService` / `XzjgService` | 同上 | 薪资价格 CRUD + `GetPriceAsync(group,key)` | + +数据源(全部为系统已有模块,本次只做读取):看台记录 `KtjlDcg/KtjlCtg`、煮茧日报 `ZjrbRecord/ZjrbJizu`、 +落丝记录 `LsjlRecord`、秤大丝 `CdsLsjl/CdsChetai`、纤度检验 `XdjyLsjl/XdjyRecord`、黑板检验 `HbjyRecord`、 +生丝规格 `ShengsiGuige`、生丝定级指标 `ShengsiDingjizhibiao`、工艺庄口 `CraftZhuangKou`。 + +### 2.3 接口 + +``` +GET /api/payroll/calcldcct/list?start=&end=&depId=&worktypeId=&like=&pageIndex=&pageSize= +POST /api/payroll/calcldcct/calc-dcg 一键计算(挡车工) +POST /api/payroll/calcldcct/calc-ctg 一键计算(车头工) +POST /api/payroll/calcldcct/save 整账期×部门×工种全量覆盖(人工微调) +DELETE /api/payroll/calcldcct?start=&end=&depId=&worktypeId= + +GET /api/payroll/confgyzk/list?year=&month= POST /save GET /{id} DELETE /{id} +GET /api/payroll/confgz/list POST /save GET /{id} DELETE /{id} +GET /api/payroll/confdcg/list POST /save GET /{id} DELETE /{id} +GET /api/payroll/dcxdjk/list?guige= POST /save GET /{id} DELETE /{id} +GET /api/payroll/xzjg/list?group= POST /save GET /{id} DELETE /{id} +``` + +要点: +- 计算与保存都是「整账期 + 部门 + 工种全量覆盖」:先物理删除旧数据再重建,避免唯一索引冲突(与煮茧日报同一策略); +- 车头工计算必须已有挡车工产量,否则等级回落到庄口配置并给出告警; +- 所有告警通过 `CalcLdcctResultDto.Warnings` 返回,前端 `ElMessageBox.alert` 展示。 + +### 2.4 菜单 + +新增一级菜单「薪资」`/payroll`(排序 8,质量 7 与通知 9 之间),6 个子菜单: + +| 顺序 | 菜单 | 路径 | 组件 | 权限标识 | +|---|---|---|---|---| +| 1 | 挡车车头产量计算 | /payroll/calcldcct | payroll/calcldcct/index | payroll:calcldcct:list | +| 2 | 工艺庄口薪资配置 | /payroll/confgyzk | payroll/confgyzk/index | payroll:confgyzk:list | +| 3 | 工种薪资配置 | /payroll/confgz | payroll/confgz/index | payroll:confgz:list | +| 4 | 挡车岗位薪资配置 | /payroll/confdcg | payroll/confdcg/index | payroll:confdcg:list | +| 5 | 挡车纤度奖扣 | /payroll/dcxdjk | payroll/dcxdjk/index | payroll:dcxdjk:list | +| 6 | 薪资价格配置 | /payroll/xzjg | payroll/xzjg/index | payroll:xzjg:list | + +同时把一级菜单顺序归一为:`/qc` 7、`/payroll` 8、`/notification` 9、`/sys` 10、`/report` 11。 + +--- + +## 三、前端 + +| 文件 | 说明 | +|---|---| +| `views/payroll/calcldcct/index.vue` | 年月(→账期) + 部门 + 工种查询;多级表头(产量 / 质量 / 金额 / 金额-质量);一键计算(挡车/车头)、保存微调(产量初始值/系数/金额-产量三列直接编辑)、删除;底部显示当前页产量金额合计 | +| `views/payroll/confgyzk/index.vue` | 工艺庄口薪资配置 CRUD,庄口用 `CraftZhuangKouSelect` 选择 | +| `views/payroll/confgz/index.vue` | 工种薪资配置 CRUD,40 项按「出勤与工时/岗位与奖金/假期与加班/学工/岗位胜任考核/加班工资」分组 | +| `views/payroll/confdcg/index.vue` | 挡车岗位薪资配置 CRUD(50/60/70/100 绪基数金额 + 车头工金额) | +| `views/payroll/dcxdjk/index.vue` | 挡车纤度奖扣 CRUD | +| `views/payroll/xzjg/index.vue` | 薪资价格配置 CRUD,`薪资.前缫` 分组下预置「挡车工/车头工计件等级奖扣单价」 | +| `router/index.js` | `componentMap` 注册上述 6 个页面 | + +--- + +## 四、验证记录(全部由助手自行执行,无需人工点击) + +| 项 | 结果 | +|---|---| +| `dotnet build`(Domain / Application / Infrastructure / Api) | ✅ 0 错误 | +| `npm run build`(Node 在 `C:\Users\32453\.workbuddy\binaries\node\versions\22.22.2-6`) | ✅ `✓ built in 14.88s`(仅既有 chunk 体积警告) | +| IDE 诊断(6 个 vue + router) | ✅ 0 | +| 菜单与权限 | ✅ `/auth/menus` 返回「薪资」+ 6 个子菜单 | +| 6 个接口连通性 | ✅ 全部 200(空表) | +| 挡车工一键计算 | ✅ 生成 1 条/庄口;丝量 = 桶数100 × (10kg×1000/4) = 250000 g | +| 挡车工单价 | ✅ 挡车数 20 → 绪数 400 → 100 档;基础单价 = 4000/28/20/9/100 = 0.0079365 | +| 挡车工等级 | ✅ 生丝定级指标 192 条参与木桶短板,定出 `6A00 / 12`;等级增减 = (12−5)/0.1×0.3/10/1000 = 0.0021 | +| 挡车工实际单价 / 产量金额 | ✅ 0.0100365;0.0100365 × 250000 × 1 = 2509.13 | +| 挡车工缫折 | ✅ 落丝茧量 60000 g、秤大丝丝量 25000 g → 缫折 240(**修复前为 4800,见下方问题**) | +| 挡车工缫折考核 | ✅ (300−240)−2 = 58 → 封顶 10 → 10 × (250000/1000/100) × 5 = +125 | +| 车头工一键计算 | ✅ 丝量 = 250000 × 9/(9×2) = 125000;基础单价 = 2000/28/9/20/100×2 = 0.0079365;缫折考核 = 3 × 1.25 × 1 = 3.75 | +| 车头工依赖挡车工 | ✅ 无挡车工产量时给出「工种薪资配置缺失」类告警并按配置回落 | +| 人工微调保存 | ✅ 产量初始值改 999.99 后保存,回读 `999.99 / 系数1 / 金额999.99` | +| 整账期删除 | ✅ 删除后 `total=0`,且「删除后重建」不再撞唯一索引 | +| 班次显示 | ✅ 修正为 `甲=1 / 乙=2`(初版误按 0/1 映射,已修) | +| 测试数据清理 | ✅ 播种的煮茧日报、看台记录、落丝记录、秤大丝、5 套配置、计算结果全部删除;验证脚本 `verify-payroll.ps1` 已删除 | + +### 过程中发现并修复的真实 bug + +1. **缫折被放大 20 倍**:初版按「车台数 × 落丝茧量」累加分子,导致 20 台时缫折 = 4800(正确 240)。 + 修正为:茧量只记在落丝层,按「车台丝量 / 落丝丝量」摊到车台后再按看台比例摊到人。 +2. **班次名称错位**:`Prod_Banci_Enum` 是 `甲=1 / 乙=2`,初版按 `0/1` 映射,甲班被显示成乙班。 + +--- + +## 五、移植取舍与后续 + +1. **未移植 F9 弹窗顶部的 4 个均值标签**(挡60/挡70/挡100/车头平均,`Σ金额 / Σ看台时长 × 225`):这是展示用的派生统计, + 本版在列表底部给出「当前页产量金额合计」;如需可按同样公式补在弹窗/工具栏。 +2. **未移植 F9 的「部门穿梭框多选」一键计算**:Web 端改为先选部门再点按钮,单部门计算(语义等价,逐个部门重复操作即可)。 +3. **野纤「奖励」分支未使用**:与 F9 一致——挡车工路径下 `JineZlYexian` 只取 `-扣款`,奖励分支被丢弃。 +4. **未移植的 F9 薪资子模块**(本次范围外,可按需继续):`CalXz`(薪资核算主表)、`Cmcl`(产量)、`QSXSCL`、`QTKH`、`SBRYKH` 等。 +5. **待接入**:① Grid++ 打印(报表中心目前只有 `lsjl.grf`);② 历史 `biz_payroll_calcldcct` 数据可按工作进度-13 的 CSV 内嵌 + 幂等策略导入; + ③ 本模块产出的「金额-产量 + 金额-质量」可直接作为计件工资与成本核算的数据源。 + +### 环境备注 + +- 本机 `node` / `npm` 不在默认 PATH,需先 `set PATH=C:\Users\32453\.workbuddy\binaries\node\versions\22.22.2-6;%PATH%`。 +- Windows PowerShell 5.1 的 `Invoke-RestMethod` 默认按 ASCII 提交请求体,写中文会变成 `?`; + 验证脚本改为 `[System.Text.Encoding]::UTF8.GetBytes()` + `application/json; charset=utf-8`,并用 UTF-8 BOM 保存脚本。 +- 煮茧日报的「已存在相同的煮茧日报」校验会命中已软删记录,删除后无法在同一「日期+车间+庄口+规格」重建(既有模块行为,本次未改动)。 diff --git a/工作进度/工作进度-17.md b/工作进度/工作进度-17.md new file mode 100644 index 0000000..7f8c8f3 --- /dev/null +++ b/工作进度/工作进度-17.md @@ -0,0 +1,233 @@ +# 工作进度-17:仓储模块(WMS)五模块移植 + +## 一、任务目标 + +把 `D:\AntaiProject - 副本\trunk\WinClient\F9.Business.WMS` 下的五个子模块**按原业务逻辑一模一样**移植到本系统,UI 做适度美化: + +| 模块 | 原 F9 目录 | 本系统菜单 | +| --- | --- | --- | +| 原料 | `F9.Business.WMS\Yuanliao` | 仓储 / 原料 | +| 副产品 | `F9.Business.WMS\Fuchanpin` | 仓储 / 副产品 | +| 中间品 | `F9.Business.WMS\ZJP` | 仓储 / 中间品 | +| 生丝 | `F9.Business.WMS\SS` | 仓储 / 生丝 | +| 物资 | `F9.Business.WMS\WJ` | 仓储 / 物资 | + +## 二、建模方案(统一模型 + 模块/口径区分) + +五个模块的单据结构高度一致,因此没有建 5 套重复表,而是一套表 + 两个维度: + +- `Wms_Module_Enum`:原料=1 / 副产品=2 / 中间品=3 / 生丝=4 / 物资=5 +- `Kind`:0=库存台账,1=笼筒架台账(中间品专用) + +### 新增实体(Domain/Entities/Wms/Wms.cs) + +| 实体 | 表 | 说明 | +| --- | --- | --- | +| `WmsRuku` / `WmsRukuDetail` / `WmsBangmadan` | `WmsRukus` / `WmsRukuDetails` / `WmsBangmadans` | 入库单、入库明细、磅码单 | +| `WmsChuku` / `WmsChukuDetail` | `WmsChukus` / `WmsChukuDetails` | 出库单、出库明细 | +| `WmsKcTzDate` / `WmsKcTzItem` | `WmsKcTzDates` / `WmsKcTzItems` | 库存台账(日期 + 明细) | +| `WmsPici` / `WmsPiciBaojian` | `WmsPicis` / `WmsPiciBaojians` | 生丝批次、包件 | +| `WmsKucun` | `WmsKucuns` | 物资库存(按物资目录) | +| `WmsCaigou` | `WmsCaigous` | 原料采购 | +| `WmsFbplan` / `WmsFbjl` | `WmsFbplans` / `WmsFbjls` | 原料翻包计划 / 翻包记录 | +| `WmsCgplan` / `WmsCgplanItem` | `WmsCgplans` / `WmsCgplanItems` | 物资采购计划 / 明细 | + +### 台账项目口径(与 F9 一致) + +- 原料:按**原料庄口**;中间品笼筒架台账:按**工艺庄口**(且只统计「联统茧」物资) +- 其余:按**物资目录**;生丝额外按 `SubKey`(基地 / 外购)分组 + +### 两列数值口径 + +- 原料 / 副产品 / 中间品 / 生丝:**件数 / 净重**(生丝件数列取「批数」) +- 物资:**数量 / 金额** + +## 三、对齐 F9 的业务规则 + +### 1. 单据类型枚举(完整照搬 F9) + +| 模块 | 入库类型 | 出库类型 | +| --- | --- | --- | +| 原料 | 采购1 / 调拨2 / 其他3 | 领料1 / 销售2 / 样茧3 / 其他4 | +| 副产品 | 生产1 / 试缫2 / 其他5 | 销售1 / 调拨2 / 其他3 | +| 中间品 | 选茧0 / 试缫2 / 退料3 / 其他5 | 领料0 / 销售1 / 其他2 | +| 生丝 | 生产0 / 退货1 / 其他2 | 销售1 / 其他5 | +| 物资 | 采购1 / 冲账2 / 盘盈3 / 其他4 | 领料1 / 销售2 / 调拨3 / 报废4 / 其他5 | + +### 2. 重量 / 金额计算 + +- 磅码单:净重 = 毛重 − 皮重;明细件数 / 毛重 / 皮重 / 净重由磅码单汇总 +- 明细净重 = 毛重 − 皮重 − 包装重量 − 运损(未使用的项为 0) +- 物资:金额 = 数量 × 单价;库存单价 = 库存金额 ÷ 库存数量(4 位小数) +- 物资出库金额(`WJ_Chuku_Detail.CalJine`): + - 出库数量 = 库存数量 → 金额 = 库存金额,单价 = 金额 ÷ 数量 + - 否则 → 单价 = 库存单价,金额 = 数量 × 单价 +- 物资出入库实时更新库存(加权平均单价);删除 / 修改单据时回滚,保证严格对称 + +### 3. 库存台账生成(`YL_Kucun_Taizhang_Edit.GenerateTaizhang / CalTdayJiecun`) + +- 上日结存:取上一台账中「件数或净重不为 0」的行 +- 汇总区间:`LastDate + 1天` ~ `TzDate`(笼筒架台账为 `LastDate` ~ `TzDate`,与 F9 的 Yesterday~Date 一致) +- 无上一台账时 `LastDate = 1970-01-01`(与 F9 `Page_YL_Kucun_Taizhang.cs` 一致,首份台账汇总全部历史) +- 今日结存件数 = 上日 + 入库 − 出库 +- 今日结存重量 = Round(上日 + 入库 − 出库 + 盈亏, 2, AwayFromZero) +- 全部出库 = 上日结存 ≠ 0 且今日结存 = 0;勾选后把结存转入盈亏(取消则转回) +- 只能生成**晚于**最新台账日期的台账 + +### 4. 台账保存校验(与 F9 一致) + +- 结存重量恒等式:`上日 + 入库 − 出库 + 盈亏 = 今日结存` +- 结存重量不能小于 0 +- 原料:全部出库时样茧件数必须为 0 + +### 5. 生丝批次包件 + +- 净重 = 毛重 − 皮重;公量 = 净重 × (1 + 11%) ÷ (1 + 实测回潮率%) +- 批次库存状态:待入库1 / 已入库2 / 部分出库3 / 已出库4;生丝出库后按批次号更新为「已出库」 + +### 6. 物资扩展 + +- 采购计划(草稿 / 已确认 / 已完成)+ 计划明细 +- 采购报销:入库单 `BxStatus` 无0 / 待报销1 / 已报销2,支持批量报销 +- 库存查询、出入库流水(入库为正、出库为负) + +## 四、后端实现 + +| 文件 | 说明 | +| --- | --- | +| `Domain/Entities/Wms/Wms.cs` | 全部 WMS 实体与 `Wms_Module_Enum` | +| `Application/Dtos/Wms/WmsDtos.cs` | 出入库 / 明细 / 磅码单 / 台账 / 批次 / 采购 DTO | +| `Application/Services/Wms/WmsService.cs` | 统一服务:五模块出入库、台账、库存、流水、批次、采购 | +| `Api/Controllers/Wms/WmsController.cs` | `api/wms/*` 接口 | +| `Application/DependencyInjection.cs` | 注册 `IWmsService` | +| `Infrastructure/Data/DbSeeder.cs` | 一级菜单「仓储」+ 5 个子菜单;清理软删残留 | + +### 接口清单(`/api/wms`) + +``` +GET /options?module= 类型 / 物资 / 原料庄口 / 工艺庄口 / 部门 / 往来单位 +GET /ruku?module=&start=&end=&type=&like= +GET /ruku/{id} POST /ruku DELETE /ruku/{id} +POST /ruku/baoxiao 采购报销(物资) +GET /chuku?... GET /chuku/{id} POST /chuku DELETE /chuku/{id} +GET /taizhang/dates?module=&kind= +GET /taizhang?module=&kind=&date=®enerate= +POST /taizhang DELETE /taizhang/{id} +GET /kucun?module= GET /crk?module=&start=&end=&wzmlId= +GET /pici GET /pici/{id} POST /pici DELETE /pici/{id} +GET /caigou POST /caigou DELETE /caigou/{id} +GET /fbplan POST /fbplan DELETE /fbplan/{id} +GET /cgplan GET /cgplan/{id} POST /cgplan DELETE /cgplan/{id} +``` + +### 设计要点 + +- 单据编号(模块 + 日期 + 单号)唯一,重复时返回友好提示 +- 出入库单、批次采用**物理删除**:避免软删残留撞唯一索引而无法重建同号单据 +- 启动时清理历史软删残留(`WmsRukus` / `WmsChukus` / `WmsPicis`) + +## 五、前端实现 + +| 文件 | 说明 | +| --- | --- | +| `views/wms/moduleConfig.js` | 五模块差异化配置(项目口径、字段显示、数值口径、枚举选项) | +| `views/wms/components/RukuPanel.vue` | 通用入库(含磅码单弹窗、明细合计) | +| `views/wms/components/ChukuPanel.vue` | 通用出库(物资显示库存数、自动算金额) | +| `views/wms/components/TaizhangPanel.vue` | 通用台账(生成 / 编辑 / 盈亏 / 全部出库 / 保存 / 删除) | +| `views/wms/components/WjKucun.vue` | 物资库存 | +| `views/wms/components/WjCrk.vue` | 物资出入库流水 | +| `views/wms/components/WjCgplan.vue` | 物资采购计划 | +| `views/wms/components/WjCgbx.vue` | 物资采购报销 | +| `views/wms/components/SsPici.vue` | 生丝批次 + 包件(公量自动计算) | +| `views/wms/components/YlCaigou.vue` | 原料采购 | +| `views/wms/components/YlFbplan.vue` | 原料翻包计划 + 翻包记录 | +| `views/wms/{yuanliao,fuchanpin,zjp,ss,wj}/index.vue` | 五个模块页面(Tab 组织) | +| `router/index.js` | 注册 5 个页面组件 | + +### UI 优化点 + +- 统一卡片式布局 + Tab 分区,顶部带模块说明 +- 查询栏(日期区间 / 类型 / 关键字)与操作按钮分离,右侧对齐 +- 单据编辑弹窗:表头 4 列栅格表单,明细用可编辑表格(下拉 + 数字框),底部实时合计 +- 磅码单独立弹窗,自动算净重并汇总 +- 台账:结存列加粗高亮,盈亏 / 全部出库可交互,底部合计条 +- 状态用 `el-tag` 着色(库存状态、报销状态、翻包状态、采购计划状态) + +## 六、菜单 + +一级菜单「仓储」(`/wms`,排序 9,位于「薪资」之后),五个子菜单: + +| 名称 | 路径 | 组件 | +| --- | --- | --- | +| 原料 | `/wms/yuanliao` | `wms/yuanliao/index`(入库 / 出库 / 库存台账 / 原料采购 / 翻包计划) | +| 副产品 | `/wms/fuchanpin` | `wms/fuchanpin/index`(入库 / 出库 / 库存台账) | +| 中间品 | `/wms/zjp` | `wms/zjp/index`(入库 / 出库 / 库存台账 / 笼筒架台账) | +| 生丝 | `/wms/ss` | `wms/ss/index`(入库 / 出库 / 库存台账 / 批次包件) | +| 物资 | `/wms/wj` | `wms/wj/index`(入库 / 出库 / 库存台账 / 库存 / 出入库流水 / 采购计划 / 采购报销) | + +## 七、验证结果(全流程脚本自动执行,已清理测试数据) + +``` +[1] 登录 OK +[2] 菜单:仓储 children=5(原料 / 副产品 / 中间品 / 生丝 / 物资) +[3] 选项:原料模块 入库类型3 / 出库类型4 / 原料庄口347 / 物资1000 +[4] 原料入库:件数10 净重945(1000−50−5)✓ +[5] 原料出库:件数4 净重380 ✓ +[6] 台账生成:上日0 + 入库10/945 − 出库4/380 = 结存6/565 ✓ +[7] 台账保存 ✓ +[8] 次日台账:上日结存6/565 正确结转 ✓ +[9] 物资入库:库存 100 / 1000 / 单价10 ✓ +[10] 物资出库40:库存 60 / 600 / 单价10(CalJine 规则)✓ +[11] 生丝批次:净重120 公量119.46(59 + 60.46)✓ +[12] 原料采购 ✓ [13] 翻包计划 ✓ [14] 采购计划 ✓ [15] 批量报销 count=1 ✓ +[16] 清理完成,物资库存归零 ✓ +``` + +第二批(副产品 / 中间品 / 生丝 + 笼筒架台账): + +``` +[2 副产品] 入库6/570 出库2/190 结存4/380 ✓ +[3 中间品] 入库6/570 出库2/190 结存4/380 ✓ +[4 生丝] 入库2批/570 出库1批/190 结存1批/380(按基地分组)✓ +[3 笼筒架台账] 按工艺庄口统计 结存4/380 ✓ +``` + +- 后端 `dotnet build`:0 错误 +- 前端 `npm run build`:构建成功 +- 服务运行中:后端 `http://localhost:3000`、前端 `http://localhost:5173` + +## 八、后续修正(2026-10-26) + +1. **「在制品」更名为「中间品」** + - 枚举 `Wms_Module_Enum.在制品` → `中间品`(数据库存值仍为 3,不影响历史数据) + - 菜单「仓储 / 中间品」、页面标题、模块配置同步更名 + +2. **按仓储区分物资分组** + - 物资分组 `WuziGroup.BusinessType`:原料1 / 中间品2 / 生丝3 / 副产品4 / 五金5 + - `/api/wms/options?module=` 只返回本模块所属分组的物资: + + | 仓储模块 | 分组业务类型 | 物资数(现有数据) | 示例 | + | --- | --- | --- | --- | + | 原料 | 原料(1) | 2 | 0101 优茧、0102 普茧 | + | 副产品 | 副产品(4) | 39 | 040101 好五级茧 … | + | 中间品 | 中间品(2) | 1 | 0301 联统 | + | 生丝 | 生丝(3) | 6 | 070101 正品丝20/22 … | + | 物资 | 五金(5) | 952 | 02010063 轴承盒 … | + + - 笼筒架台账的物资过滤同步改为匹配名称含「联统」的物资(原为「联统茧」) + +3. **修复「新增页面点击确认保存报错」** + - 原因:下拉框清空后前端提交 `null`,而后端 DTO 的 `long/int/decimal` 不接受 `null`, + `System.Text.Json` 直接返回 400(`The JSON value could not be converted to System.Int64`) + - 修复:新增 `cleanPayload()`(`views/wms/moduleConfig.js`),提交前把 `null/undefined` 统一转成 `0` + (`scDate`、`guobangShijian`、`ckTime` 等日期型保持 `null`),所有 WMS 保存接口统一走该函数 + - 验证:中间品入库(件数3 净重285=300−15)、物资入库(10×5=50)均保存成功,测试数据已清理 + +## 九、说明与后续可选项 + +- 笼筒架台账只统计名称含「联统茧」的物资(与 F9 一致;未配置该物资时不过滤) +- 生丝公量按 11% 公定回潮率计算(F9 标准值) +- 已对齐 F9 的五个模块核心逻辑;如后续需要,可再补充: + - 生丝混装(`SS_Pici_Hunzhuang`) + - 物资调拨单、报废单的独立审批流 + - 各模块台账的报表打印模板 diff --git a/工作进度/工作进度-18.md b/工作进度/工作进度-18.md new file mode 100644 index 0000000..c74f73b --- /dev/null +++ b/工作进度/工作进度-18.md @@ -0,0 +1,170 @@ +# 工作进度-18:仓储 · 生丝 模块操作逻辑对齐 F9 + +> **协作规则(长期生效)**:**每次对话结尾都要生成工作进度**。 +> 即在每轮对话结束前,把本轮完成的内容追加为 `工作进度/工作进度-N.md`,并在文档中保留本条规则,方便下一轮接续。 + +## 一、任务目标 + +新系统(.NET 8 + Vue 3)的生丝模块功能已具备,但**页面操作逻辑与老系统不一致**。 +目标:以老系统 `D:\AntaiProject - 副本\trunk\WinClient\F9.Business.WMS\SS` 为基准,把生丝模块的**操作逻辑改成与老系统一模一样**。 + +对齐范围(本轮确认): + +- 生丝入库 / 出库的**批次驱动录入** +- 生丝**批次报检**的行内操作(过磅 / 包件 / 公量 / 质检 / 纤度检验) +- 出入库**主从联动** +- 财务审核:**本轮不做**,用户选择后续单独处理 + +## 二、核心差异(新系统改造前的问题) + +| 环节 | 老系统做法 | 新系统改造前 | +| --- | --- | --- | +| 入库明细 | 点「选择生丝批次」→ 勾选**待入库批次**,自动带出物资 / 批次号 / 批数 / 包件数 / 毛皮净重 | 手动「添加明细」→ 选物资 + 手填批次号 + 手填重量 | +| 出库明细 | 点「选择批次 + 包件」→ 勾选批次下未出库包件 | 手动录入,批次与包件脱节 | +| 批次状态 | 批次 / 包件级跟踪出库(`Ck_Isck`、`Ck_Baoshu` 等) | 仅按批次号字符串整体置"已出库",包件无跟踪 | +| 批号格式 | 正品 10 位数字 / 其他 `31EE-7位` | 无校验 | +| 主从联动 | 主表点行 → 下方明细联动 | 无,明细只在编辑弹窗 | +| 批次报检操作 | 过磅 / 包件 / 公量 / 质检 / 纤度检验 5 个行内操作 | 只有编辑、删除 | +| 规格 | 下拉选择(生丝规格) | 手填文本框 | +| 生产车间 | 取职责类型为「前缫」的车间 | 取全部部门,未筛选 | + +## 三、数据模型变更 + +### 新增字段(`Domain/Entities/Wms/Wms.cs`) + +| 实体 | 新增字段 | 对应 F9 | +| --- | --- | --- | +| `WmsPici` | `RkItemId`、`RkDate`、`CkLastDate`、`CkBaoshu`、`CkJingzhong`、`KcBaoshu` | `Rk_Item_Id`、`Rk_Date`、`CK_LastDate`、`Ck_Baoshu`、`Ck_Jingzhong`、`Kc_Baoshu` | +| `WmsPiciBaojian` | `CkIsck`、`CkItemId`、`CkDate`、`CkJingzhong` | 包件级出库标记 | +| `WmsRukuDetail` | `PiciIds` | `SS_Rk_Item.PiciIds` | +| `WmsChukuDetail` | `PcAndBjIds` | `SS_Ck_Item.PcAndBjIds` | + +### 新增实体 + +| 实体 | 表 | 说明 | +| --- | --- | --- | +| `WmsPiciChuku` | `WmsPiciChukus` | 批次出库记录(回滚时按此重算库存状态) | +| `WmsPiciQc` | `WmsPiciQcs` | 批次质检 | +| `WmsPiciQcXdjy` | `WmsPiciQcXdjys` | 批次纤度检验 | + +### 建表 / 加列(`DbSeeder.cs`) + +- 新表:由 `SyncStructure` 反射扫描 `BaseEntity` 子类自动建立 +- 旧表加列:`EnsureColumn` 兜底(幂等,列已存在则忽略) + +## 四、后端改动 + +### 1. 状态流转(`WmsService.cs`,对齐 F9 CQRS) + +- **入库**:`UpdatePiciRukuAsync` 按 `PiciIds` 置批次"已入库",写 `RkItemId` / `RkDate`,`KcBaoshu = Baoshu` +- **入库回滚**:`RollbackPiciRukuAsync`,若批次已出库则抛错禁止修改 / 删除 +- **出库**:`UpdatePiciChukuAsync` 按 `PcAndBjIds`(JSON)标记包件 `CkIsck`,写 `WmsPiciChuku`;批次按出库包件数流转「部分出库 / 已出库」 +- **出库回滚**:`RollbackPiciChukuAsync` 撤销包件标记、删除出库记录、按剩余记录重算批次状态 +- **校验**:待入库不可出库、已全部出库不可再出库 + +> 注意:`PcAndBjIds` 前端序列化为 camelCase(`piciId` / `piciBaojianIds`), +> 反序列化时必须带 `PropertyNameCaseInsensitive = true`,否则字段匹配不上。 + +### 2. 批号格式校验(`SavePiciAsync`) + +对齐 F9 `Page_PiciBaojian_Edit`: + +- 正品(`CodeType = 0`):`^\d{10}$` +- 其他(`CodeType = 1`):`^31EE-\d{7}$` + +### 3. 新增接口(`WmsController.cs`) + +| 方法 | 路由 | 说明 | +| --- | --- | --- | +| GET | `/api/wms/pici/chuku-select` | 出库选批次(含未出库包件),支持按物资过滤、排除已选批次 | +| GET | `/api/wms/pici/qc/{piciId}` | 读取批次质检 | +| POST | `/api/wms/pici/qc` | 保存批次质检 | +| GET | `/api/wms/pici/xdjy/{piciId}` | 读取纤度检验 | +| POST | `/api/wms/pici/xdjy` | 保存纤度检验 | + +### 4. 入库主表补"包数" + +`WmsRukuListDto` 新增 `BaojianNum`,由明细 `BaojianNum` 汇总(出库主表老系统无包数列,故未加)。 + +## 五、前端改动 + +### 1. 新建 `PiciSelectDialog.vue` + +生丝批次选择弹窗,两种模式复用: + +- `mode="ruku"`:拉待入库批次(`GET /wms/pici?kcStatus=1`),勾选批次 +- `mode="chuku"`:拉批次 + 包件(`GET /wms/pici/chuku-select`),展开行勾选包件, + 批次勾选 ↔ 包件勾选双向联动,已出库包件禁用 + +### 2. `RukuPanel.vue`(生丝入库) + +- 明细工具栏:生丝改为「选择生丝批次」,不再自动加空白行 +- 入库类型:仅显示「生产(0) / 退货(1)」(对齐 F9"其他暂未开放") +- 按物资分组生成明细,回填 `piciIds` / `piciCodes` / 批数 / 包件数 / 毛皮净重 +- 物资、批次号、包件数改为**只读**(由批次带出) +- 主表:新增「包数」列;新增展开行主从联动 +- 保存校验:生丝明细必须有 `piciIds` + +### 3. `ChukuPanel.vue`(生丝出库) + +- 明细工具栏:生丝改为「选择批次包件」 +- 出库类型:仅显示「销售(1)」 +- 往来单位(客户)改标签为「往来单位」并**必选** +- 明细回填 `pcAndBjIds`(JSON `[{piciId, piciBaojianIds}]`) +- 主表:展开行主从联动 + +### 4. `SsPici.vue`(批次报检) + +- 行内操作补齐:**过磅 / 包件 / 公量 / 质检 / 纤度检验** + - 过磅:仅「待入库」状态可用(否则提示并阻止) + - 包件、公量:复用批次编辑弹窗(编辑包件毛皮重 / 回潮率) + - 质检:新增质检弹窗(对接 `/wms/pici/qc`) + - 纤度检验:新增明细表格弹窗(对接 `/wms/pici/xdjy`) +- **规格**:由手填 `el-input` 改为 `el-select`,数据来自 `GET /shengsiguige/all` + (与 `prod/lsjl` 落丝记录取数口径一致) +- **生产车间**:由"全部部门"改为**仅取职责类型为「前缫」的车间** + ```js + import { depOptions, DUTY } from '../../../utils/departments'; + const opts = await depOptions({ duty: [DUTY.前缫] }, { flat: true }); + deptOptions.value = opts.map((o) => ({ id: Number(o.key), name: o.value, ...(o.raw || {}) })); + ``` + 列表列名同步改为「生产车间」 +- 保存校验补齐(对齐 F9 `AddValidateRuls`):日期、规格、品名(生丝规格)、批号、基地外购 + +> 变量名注意:组件内部门列表变量已改名 `deptOptions`,避免与工具函数 `depOptions` 重名冲突。 + +## 六、涉及文件 + +**后端** + +- `backend/F9WebAiTest.Domain/Entities/Wms/Wms.cs` +- `backend/F9WebAiTest.Application/Dtos/Wms/WmsDtos.cs` +- `backend/F9WebAiTest.Application/Services/Wms/WmsService.cs` +- `backend/F9WebAiTest.Api/Controllers/Wms/WmsController.cs` +- `backend/F9WebAiTest.Infrastructure/Data/DbSeeder.cs` + +**前端** + +- `web/src/views/wms/components/PiciSelectDialog.vue`(新建) +- `web/src/views/wms/components/RukuPanel.vue` +- `web/src/views/wms/components/ChukuPanel.vue` +- `web/src/views/wms/components/SsPici.vue` + +## 七、验证情况 + +- 后端 `F9WebAiTest.Application` 编译通过:**0 错误** +- 解决方案整体编译的报错均为 `MSB3021 / MSB3027`(**后端 API 进程正在运行,DLL 被锁定无法复制**),非代码问题 +- 前端 lint:0 错误 + +## 八、待办 / 遗留 + +1. **财务审核 / 反审核**:老系统生丝出入库均有「财务审核状态」列与批量审核 / 反审核按钮, + 新系统尚未实现(本轮用户选择暂缓,后续单独处理) +2. 生丝出库「选择调拨通知单」自动填充明细:老系统有(`Page_Dbtz_Select`),新系统未实现 +3. 老系统批次报检的「混装」功能在 F9 中已被注释,故未移植 + +## 九、下次接续提示 + +- 重启后端服务后,新表 / 新列才会在启动时建立 +- 继续做财务审核时,需新增通用审核记录与生丝出入库审核接口,并在两个列表页加「财务审核状态」列 + 批量审核 / 反审核按钮 +- **每轮对话结尾记得生成本工作进度文档** diff --git a/工作进度/工作进度-19.md b/工作进度/工作进度-19.md new file mode 100644 index 0000000..258f430 --- /dev/null +++ b/工作进度/工作进度-19.md @@ -0,0 +1,153 @@ +# 工作进度-19:生产 · 选剥茧 模块移植(后端完成) + +> **协作规则(长期生效)**:**每次对话结尾都要生成工作进度**。 +> 即在每轮对话结束前,把本轮完成的内容追加为 `工作进度/工作进度-N.md`,并在文档中保留本条规则,方便下一轮接续。 + +## 一、任务目标 + +将老系统「生产 - 选剥茧」模块移植到新系统。老系统位置:`D:\AntaiProject - 副本\trunk\WinClient\F9.Business.Prod\XBJ`。 + +模块构成(老系统两个子功能 + 三张表): + +| 老系统 | 说明 | +| --- | --- | +| `XBJ\XJRB\Page_Xjrb.cs` | **选茧日报**:某日某工艺庄口一条,记录干茧领入与 8 类茧 | +| `XBJ\XJRB\Page_XjrbPlan.cs` | **选茧计划**:按领料单(原料出库单)下达,含计划盈亏与完成标记 | +| `XBJ\XJRB\Page_XjrbItem_Edit.cs` | **计划明细**:挂在计划下的每日选茧进度行 | +| `XBJ\XJBZ\Page_xjpbbanzu.cs` | **选茧班组**:班次(甲/乙)+ 选茧组,含班组职工 | + +新系统此前**没有**该模块(`prod/zjrb` 是煮茧日报,不是选茧日报),需从零建。 + +## 二、核心业务规则(移植关键,务必照此实现) + +### 1. 选茧日报字段(8 类茧 + 退回) + +干茧领入(包数/重量)、联统茧、好五级茧、六级茧、绵茧、双宫茧、下脚茧、茧衣、茧花(各包数/重量)、 +退回(包数/重量)、合计重量、联统率、盈亏。 +合计重量 = 上述 8 类重量之和(**不含退回、不含领入**)。 + +### 2. 盈亏 / 联统率计算(对齐 F9 `GetXjrbCountYingkuiQuery` + `GetXjrbLingruStartDateQuery`) + +``` +步骤 1 起始日期(GetLingruStartDate): + history = 同庄口 且 Date < 当前日期 的日报,按日期降序(排除自身) + startDate = 当前日期 + for item in history(由新到旧): + if item.LiantongRate != 0: break // 遇第一条已结算记录停止 + startDate = item.Date // 否则继续往前推 +步骤 2 累计区间 = [startDate, 当前日期] 闭区间(排除自身) +步骤 3 累加(初值 = 当前表单输入值): + ltj = 当前联统茧重量 + Σ item.LtjZhongliang + yllr = 当前干茧领入重量 + Σ item.Yl_lr_zhongliang + all = 当前 8 类重量之和 + Σ item 同样 8 类重量之和 +步骤 4 公式: + 联统率 = yllr != 0 ? (ltj / yllr) * 100 : 0 + 盈亏 = all - yllr +``` + +**要点**:起始日期不是简单取"上月结存日",而是**往回跳过所有 `LiantongRate == 0`(未结算)的记录**, +遇到第一条已结算记录才停 —— 这是最容易实现错的地方。 + +### 3. 计划维度盈亏(口径与日报**不同**) + +``` +联统率 = 联统茧包数 ÷ (联统+好五级+五级+双宫+下脚+茧衣+茧花 包数之和) × 100 ← 按「包数」 +盈亏 = 累计选出总重量 − 计划原料领入重量(YlZhongliang) +``` + +> 老系统仓储里 `wjjBaoshu` / `wjjZhongliang` 的源字段**交叉写反**(疑似 bug), +> 本移植**已按正确字段聚合**(`SUM(WjjBaoshu)`→包数、`SUM(WjjZhongliang)`→重量), +> 若业务上必须与老系统完全一致(含 bug),可改回。 + +### 4. 标包重(老系统在客户端计算,尚未移植) + +老系统从注册表 `生产\选剥茧` 读取各品类标包重,**重量 = 包数 × 标包重**: + +| 注册表键 | 说明 | +| --- | --- | +| 联统茧标包重 | | +| 好五级茧标包重 | | +| 六级茧标包重 | | +| 棉茧标包重 | | +| 双宫茧标包重 | | +| 下脚茧标包重 | | +| 茧衣标包重 | | + +茧花、退回**无**标包重,手工录入。 +前端实现时需提供该配置入口(新系统暂无等价的注册表服务,待定:可做成常量配置或新增配置表)。 + +### 5. 选茧班组 + +- 班组 = 班次(甲/乙)+ 选茧组;班组职工来自「选剥茧车间」下工种为「选茧工」的在职职工 +- **没有独立新增入口**:由「编辑录入」勾选职工 + 填组别后整体保存(按班次覆盖:先清该班次旧职工与班组,再重建) +- 计划完成(`IsFinish`)后禁止修改/删除明细 + +## 三、本次完成(后端) + +### 新建文件 + +| 文件 | 内容 | +| --- | --- | +| `Domain/Entities/Prod/Xjrb.cs` | `XjrbRecord` / `XjrbPlan` / `XjrbItem` 三个实体(表 `XjrbRecords`、`XjrbPlans`、`XjrbItems`) | +| `Domain/Entities/Prod/XjpbBanzu.cs` | `XjpbBanzu` / `XjpbBanzuZhigong`(表 `XjpbBanzus`、`XjpbBanzuZhigongs`) | +| `Application/Dtos/Prod/XjrbDtos.cs` | 日报 / 计划 / 明细 / 班组 的列表与保存 DTO,含 `XjrbYingkuiDto` | +| `Application/Services/Prod/XjrbService.cs` | `IXjrbService` + 实现(盈亏、计划、班组全部逻辑) | +| `Api/Controllers/Prod/XjrbController.cs` | 路由前缀 `api/prod/xjrb` | + +### 修改文件 + +- `Application/DependencyInjection.cs`:新增 `services.AddScoped();` + (新系统 DI 是**手动注册**,不是反射扫描,新增 Service 必须补这一行) + +### 接口清单(`api/prod/xjrb`) + +| 方法 | 路由 | 说明 | +| --- | --- | --- | +| GET | `` | 日报分页(start / end / like 庄口关键字) | +| GET | `{id}` | 日报详情 | +| POST | `` | 保存日报 | +| DELETE | `{id}` | 删除日报 | +| POST | `count-yingkui` | 计算盈亏 / 联统率(保存前调用回填表单) | +| GET | `plan` | 计划分页 | +| GET | `plan/{id}` | 计划详情 | +| POST | `plan` | 保存计划 | +| DELETE | `plan/{id}` | 删除计划(已完成则拒绝) | +| GET | `plan/{planId}/items` | 计划明细列表 | +| POST | `plan/item` | 保存明细(勾选完成后回写计划盈亏) | +| DELETE | `plan/item/{id}` | 删除明细 | +| POST | `plan/count-yingkui` | 计划维度盈亏 | +| GET | `banzu` | 班组分页 | +| GET | `banzu/{id}/zhigong` | 班组职工 | +| POST | `banzu/zhigong` | 班组「编辑录入」批量保存 | + +## 四、踩坑记录 + +1. **`BaseEntity` 的命名空间**:在 `F9WebAiTest.Domain.Common`,实体文件必须 + `using F9WebAiTest.Domain.Common;`,光有 `FreeSql.DataAnnotations` 不够(会报 CS0246)。 +2. **DI 手动注册**:新增 Service 必须在 `DependencyInjection.cs` 里 `AddScoped`,否则运行时报未注册。 +3. **编译 Api 会被运行中的进程锁死 DLL**(MSB3021 / MSB3027)。 + 验证代码是否编译通过的正确做法:筛选 `error CS`(忽略 MSB),或单独编译 Application 项目。 + +## 五、验证情况 + +- `F9WebAiTest.Application` 编译:**已成功生成** +- `F9WebAiTest.Api` 编译:无 `error CS`(Controller 通过) +- 新表由 `DbSeeder.SyncStructure` 反射扫描 `BaseEntity` 子类**自动建表**,无需手写迁移 + +## 六、待办(下一轮) + +1. **前端页面**(`web/src/views/prod/xuanjian/`): + - 选茧日报列表 + 编辑弹窗(含「计算盈亏」按钮,调用 `count-yingkui` 回填) + - 选茧计划列表 + 计划明细(主从联动) + - 选茧班组(班组列表 + 职工联动 + 「编辑录入」弹窗) +2. **标包重配置**:确定新系统的存放方式(建议做成系统配置或常量),前端按 `重量 = 包数 × 标包重` 自动算 +3. **路由 + 菜单**:`web/src/router/index.js` 加路由,菜单种子数据加「生产 - 选剥茧」 +4. 「编辑录入」取职工:需要「选剥茧车间 + 选茧工工种」筛选员工的接口(需确认新系统员工/工种接口) +5. 重启后端服务后新表才会建立 + +## 七、下次接续提示 + +- 前端实现时,选茧日报的**「计算盈亏」按钮是核心交互**:填完包数/重量后点它,把返回的 + `liantongRate` / `yingkui` / `startDate`(提示"从 xxx 开始计算")回填到表单,再保存 +- 计划明细勾选「选茧完成」保存时会直接把计划置为已完成并回写盈亏,之后不可再改 +- **每轮对话结尾记得生成本工作进度文档** diff --git a/工作进度/工作进度-20.md b/工作进度/工作进度-20.md new file mode 100644 index 0000000..f532fa7 --- /dev/null +++ b/工作进度/工作进度-20.md @@ -0,0 +1,76 @@ +# 工作进度-20:生产 · 选剥茧 前端完成(模块可运行) + +> **协作规则(长期生效)**:**每次对话结尾都要生成工作进度**。 +> 即在每轮对话结束前,把本轮完成的内容追加为 `工作进度/工作进度-N.md`,并在文档中保留本条规则,方便下一轮接续。 + +> 承接 [工作进度-19](./工作进度-19.md)(后端完成)。本轮完成前端,模块至此可运行。 + +## 一、本次完成(前端) + +新建目录 `web/src/views/prod/xuanjian/`: + +| 文件 | 内容 | +| --- | --- | +| `index.vue` | Tab 容器;三个菜单共用本组件,**按路由末段自动定位 Tab**(`/rb`、`/plan`、`/banzu`) | +| `XjrbPanel.vue` | **选茧日报**:查询、多表头列表(8 类茧各自包数/重量)、编辑弹窗、「计算盈亏」按钮 | +| `XjrbPlanPanel.vue` | **选茧计划**:列表 + **展开行主从联动**显示计划明细,明细增删改,明细内「计算盈亏」 | +| `XjpbBanzuPanel.vue` | **选茧班组**:班组列表 + 展开行显示职工,「编辑录入」弹窗按班次批量分配职工与组别 | + +### 关键实现 + +- **日报「计算盈亏」**:填完包数/重量后点按钮 → `POST /prod/xjrb/count-yingkui` → + 回填 `liantongRate` / `yingkui`,并 `ElMessage` 提示"从 xxx 开始累计"(即后端返回的 `startDateText`) +- **标包重**:老系统从注册表 `生产\选剥茧` 读取(重量 = 包数 × 标包重,茧花/退回手工录入)。 + 本次**先用 `localStorage`(key `xuanjian_biaobaozhong`)临时存放**并带「按标包重换算」按钮; + 折叠面板可编辑 7 个品类的标包重(联统/好五级/五级/绵/双宫/下脚/茧衣) +- **计划维度**联统率按**包数**口径(与日报按重量不同),明细勾选「选茧完成」保存后计划置为已完成、不可再改 +- **班组「编辑录入」**:按班次整体覆盖(后端先清该班次旧职工与班组,再重建) + +### 接口对接要点(踩坑) + +| 事项 | 正确写法 | +| --- | --- | +| 工艺庄口选择 | 用组件 `web/src/components/select/CraftZhuangKouSelect.vue`,`@confirm` 回调参数含 `id` / `zhuangKouName` / `productionDisplayName` | +| 员工列表 | `GET /sys/employee`,参数是 **`depId`**、**`worktypeId`**(注意不是 workTypeId)、`banci`、`workStatus`、`like` | +| 工种列表 | `GET /sys/worktype`,返回 `res.data.items` | +| 员工筛选口径 | 部门 = 职责类型「选剥茧」的车间(`depOptions({ duty: [DUTY.选剥茧] })`)+ 工种名含「选茧」 | +| 路由 | `web/src/router/index.js` 的路径映射表,新增 `'prod/xuanjian/index'` | + +## 二、注册与菜单 + +1. **路由**(`web/src/router/index.js`): + ```js + 'prod/xuanjian/index': () => import('../views/prod/xuanjian/index.vue'), + ``` +2. **菜单**(`backend/F9WebAiTest.Infrastructure/Data/BusinessSeedData.cs`): + 新建一级目录「生产-选剥茧」(`/prod/xuanjian`,Sort 7),下挂三个菜单: + - 选茧日报 → `/prod/xuanjian/rb` + - 选茧计划 → `/prod/xuanjian/plan` + - 选茧班组 → `/prod/xuanjian/banzu` + 组件均为 `prod/xuanjian/index` + +> ⚠️ `DbSeeder.cs` 里**另有一处**生产菜单的批量插入(`SeedProdMenus`,含"纤度检验/黑板检验…"元组列表)。 +> 若启动后菜单未出现,需在那一处同步补上选剥茧的三个菜单项(本轮只改了 `BusinessSeedData.cs`)。 + +## 三、验证情况 + +- 前端 4 个新文件 lint:**0 错误** +- `F9WebAiTest.Infrastructure` 编译:无 `error CS`(菜单种子数据通过) +- 后端(19 轮):Application 已成功生成、Api 无 CS 错误 + +## 四、待办 / 可优化 + +1. **标包重改为注册表**:新系统有 `/sys/registry` 接口(老系统正是存注册表 `生产\选剥茧`)。 + 当前用 localStorage 是临时方案,建议改为后端注册表读写,实现多端一致 +2. **领料单选择**:计划里的「领料单ID」目前是手填数字框,老系统是选择原料出库单, + 应改为下拉/弹窗选择并回填 `yllingliaodanCode` +3. **日报/计划关联**:老系统计划可按领料单生成日报,当前两侧独立,未做联动 +4. 菜单如未显示,补 `DbSeeder.SeedProdMenus` +5. 启动前需**重启后端服务**(新表在启动时建立;且 Api 进程会锁定 DLL 导致编译复制失败) + +## 五、下次接续提示 + +- 选剥茧三个页面都挂在 `prod/xuanjian/index.vue`,改 Tab 逻辑看 `tabOf(route.path)` +- 日报盈亏口径最易改错:起始日期由近及远跳过 `LiantongRate == 0` 的记录,见 19 轮文档第二节 +- 计划维度联统率用**包数**,日报用**重量**,两者分母不同,勿混用 +- **每轮对话结尾记得生成本工作进度文档** diff --git a/工作进度/工作进度-21.md b/工作进度/工作进度-21.md new file mode 100644 index 0000000..fab188b --- /dev/null +++ b/工作进度/工作进度-21.md @@ -0,0 +1,74 @@ +# 工作进度-21:修复「生产-选剥茧」菜单未显示 + +> **协作规则(长期生效)**:**每次对话结尾都要生成工作进度**。 +> 即在每轮对话结束前,把本轮完成的内容追加为 `工作进度/工作进度-N.md`,并在文档中保留本条规则,方便下一轮接续。 + +## 一、问题 + +用户反馈:**项目里没有"生产-选剥茧"模块,前端看不到菜单页面**。 + +## 二、根因(重要,后续加菜单必须记住) + +项目中**有两处**菜单种子,但**只有 `DbSeeder` 是主流程**: + +| 文件 | 实体 | 是否主流程 | 是否处理角色权限 | +| --- | --- | --- | --- | +| `Infrastructure/Data/DbSeeder.cs`(`SeedProdMenus` → `EnsureTopMenuWithChildren`) | **`SysMenu`** | ✅ 是(生产菜单实际来源) | ✅ 自动 `EnsureMenuPermission` 关联 admin | +| `Infrastructure/Data/BusinessSeedData.cs`(`AddMenu`) | `Menu` | ❌ 否(上轮误改此处) | 需另走 adminRole 循环 | + +20 轮只改了 `BusinessSeedData.cs`,所以菜单**没有生效**。 + +**结论:以后新增菜单,必须改 `DbSeeder.cs` 的 `SeedProdMenus`,用 `EnsureTopMenuWithChildren`。** + +## 三、修复 + +在 `DbSeeder.cs`「生产-后缫」之后新增(Sort 7): + +```csharp +EnsureTopMenuWithChildren(fsql, snowflake, "/prod/xuanjian", "生产-选剥茧", "Grid", 7, + new (string Name, string Path, string Component, string Permission, string Icon, int Sort)[] + { + ("选茧日报", "/prod/xuanjian/rb", "prod/xuanjian/index", "prod:xuanjian:list", "Document", 1), + ("选茧计划", "/prod/xuanjian/plan", "prod/xuanjian/index", "prod:xuanjian:plan:list", "Calendar", 2), + ("选茧班组", "/prod/xuanjian/banzu", "prod/xuanjian/index", "prod:xuanjian:banzu:list", "User", 3) + }); +``` + +并补「子菜单顺序归一」(`/prod/xuanjian/rb`=1、`/plan`=2、`/banzu`=3)。 + +`EnsureTopMenuWithChildren` 会: +1. 建/更新一级目录(`/prod/xuanjian`,`MenuType_Enum.分组`) +2. 建子菜单(不存在才插入) +3. `EnsureMenuPermission` 关联 admin 角色 → 无需另外配权限 + +`BusinessSeedData.cs` 里 20 轮加的那段**保留**(`AddMenu` 有 `if (!Any(m => m.Path == path))` 幂等保护,不会重复)。 + +## 四、前端路由(已确认存在) + +`web/src/router/index.js`: +```js +'prod/xuanjian/index': () => import('../views/prod/xuanjian/index.vue'), +``` +菜单的 `Component` 字段 `prod/xuanjian/index` 与之对应,三个菜单共用该组件, +由 `index.vue` 内 `tabOf(route.path)` 按路由末段自动切 Tab(rb / plan / banzu)。 + +## 五、验证 + +- `F9WebAiTest.Infrastructure` 编译:无 `error CS` + +## 六、用户需要做的 + +1. **重启后端服务**(菜单在启动时 seed;且运行中 Api 进程锁 DLL 会导致编译复制失败) +2. 重启后**重新登录 / 刷新**(菜单随登录接口下发) +3. 用 **admin** 角色查看(权限只关联了 admin;其他角色需在角色管理里勾选) + +## 七、遗留 + +- 非 admin 角色需在「角色管理」手动勾选选剥茧三个菜单 +- 标包重仍是 localStorage 临时方案,建议改 `/sys/registry`(见 20 轮) +- 计划里「领料单ID」为手填数字框,老系统是选择原料出库单 + +## 八、下次接续提示 + +- 新增菜单一律改 **`DbSeeder.SeedProdMenus`**(`SysMenu` + `EnsureTopMenuWithChildren`),不要再只改 `BusinessSeedData` +- **每轮对话结尾记得生成本工作进度文档** diff --git a/工作进度/工作进度-22.md b/工作进度/工作进度-22.md new file mode 100644 index 0000000..863e138 --- /dev/null +++ b/工作进度/工作进度-22.md @@ -0,0 +1,75 @@ +# 工作进度-22:重启前后端(使选剥茧菜单与代码生效) + +> **协作规则(长期生效)**:**每次对话结尾都要生成工作进度**。 +> 即在每轮对话结束前,把本轮完成的内容追加为 `工作进度/工作进度-21.md` 之外的 `工作进度-N.md`, +> 并在文档中保留本条规则,方便下一轮接续。 + +## 一、任务 + +重启前端与后端,使 19~21 轮的选剥茧模块(新表 / 新接口 / 新菜单)生效。 + +## 二、进程定位(本项目实际端口) + +| 服务 | 端口 | 重启前 PID | 重启后 PID | +| --- | --- | --- | --- | +| 后端 `F9WebAiTest.Api` | **3000** | 23880(正是锁 DLL 的那个) | 8976 | +| 前端 vite | **5173** | 21392 | 13696 | + +- 后端端口来自 `backend/F9WebAiTest.Api/Properties/launchSettings.json` 的 `applicationUrl` +- 前端端口 vite 默认 5173 + +## 三、重启步骤(可复用) + +```powershell +# 1) 按 PID 停止(先 netstat 确认,勿误杀其它 dotnet/node) +Stop-Process -Id <后端PID> -Force +Stop-Process -Id <前端PID> -Force + +# 2) 启动后端(新窗口,非阻塞) +Start-Process -FilePath "dotnet" -ArgumentList "run" ` + -WorkingDirectory "d:/f9webaitest/f9-web-ai-test/backend/F9WebAiTest.Api" -WindowStyle Minimized + +# 3) 启动前端(见下方「坑」:npm/node 不在 PATH) +Start-Process -FilePath "C:\Users\32453\.workbuddy\binaries\node\versions\22.22.2-6\node.exe" ` + -ArgumentList "node_modules/vite/bin/vite.js" ` + -WorkingDirectory "d:/f9webaitest/f9-web-ai-test/web" -WindowStyle Minimized +``` + +## 四、坑(下次直接照做) + +1. **npm / node 不在 PATH**:本机 node 由 workbuddy 管理,路径为 + `C:\Users\32453\.workbuddy\binaries\node\versions\22.22.2-6\node.exe`。 + `Get-Command npm/node` 返回空 → 必须用**完整路径**启动,不能 `npm run dev`。 +2. **`netstat | Select-String ":5|:8"` 会漏掉 3000**:过滤端口时注意 3000 不含 5/8,别误判"后端没运行"。 +3. **后端 DLL 被运行中的 Api 进程锁定**(MSB3021 / MSB3027):编译前先停后端,或只编译 + Application/Infrastructure 项目并单独筛 `error CS`。 +4. 多个 dotnet 进程并存(3916 / 9636 / 11548 / 29256 …),**必须按端口定位**再杀,不能 + `Stop-Process -Name dotnet` 全杀。 + +## 五、验证结果 + +| 检查项 | 结果 | +| --- | --- | +| 后端 `GET /api/prod/xjrb` | **401**(已注册、需认证)→ Controller 已加载 | +| 前端 `http://localhost:5173` | **200** → vite 正常 | +| 数据库 `backend/F9WebAiTest.Api/f9_web_ai_test_rbac.db` | 最后修改 **2026/10/26 10:49**(重启时刻)→ seed 已执行,菜单已写入 | + +> 注:`bin/Debug/net8.0/` 下另有一份同名 db(10/20 的旧副本),**实际运行时用的是项目根目录下那份**。 + +## 六、用户待确认 + +1. **重新登录**(菜单随登录接口下发),用 **admin** 角色查看左侧是否出现「生产-选剥匣」→ + 选茧日报 / 选茧计划 / 选茧班组 +2. 非 admin 角色需在「角色管理」手动勾选这三个菜单 + +## 七、遗留(承接 20/21 轮) + +- 标包重仍是 localStorage 临时方案,建议改 `/sys/registry` +- 计划里「领料单ID」为手填数字框,老系统是选择原料出库单 +- 日报与计划暂未做联动 + +## 八、下次接续提示 + +- 改后端代码后必须**重启后端**才生效;改前端 vite 会自动热更新 +- 重启前记得先停后端,否则 DLL 锁定导致编译复制失败 +- **每轮对话结尾记得生成本工作进度文档** diff --git a/工作进度/工作进度-23.md b/工作进度/工作进度-23.md new file mode 100644 index 0000000..a5a383b --- /dev/null +++ b/工作进度/工作进度-23.md @@ -0,0 +1,149 @@ +# 工作进度-23:移除现有考勤 + 移植「生产-职工考勤」调研(移植待续) + +> **协作规则(长期生效)**:**每次对话结尾都要生成工作进度**。 +> 即在每轮对话结束前,把本轮完成的内容追加为 `工作进度/工作进度-N.md`,并在文档中保留本条规则,方便下一轮接续。 + +## 一、任务 + +1. **移除**新系统现有考勤(`attendance`:打卡/请假/审批/记录/我的) +2. **移植**老系统「生产-职工考勤」:`D:\AntaiProject - 副本\trunk\WinClient\F9.Business.Prod\KQ` + - `CQDJ` 考勤登记 + - `CQTJ` 考勤统计 + +## 二、本轮完成:移除现有考勤 ✅ + +### 后端删除 +- `Application/Services/Attendance/IAttendanceService.cs`、`AttendanceService.cs` +- `Application/Dtos/Attendance/AttendanceDtos.cs` +- `Api/Controllers/Attendance/AttendanceController.cs` +- `Domain/Entities/Attendance/`:`LeaveRequest.cs`、`LeaveApprover.cs`、`AttendanceRecord.cs` +- `DependencyInjection.cs`:删除 `AddScoped()`, + **并删除 `using F9WebAiTest.Application.Services.Attendance;`**(否则 CS0234) + +### 前端删除 +- `web/src/views/attendance/` 下 5 个页面:`approve/ leave/ my/ punch/ records/` 的 `index.vue` +- `router/index.js`:删除静态路由 `attendance/approve`、`STATIC_ROUTES` 置空、删除 5 条组件映射 +- `views/dashboard/index.vue`:删除「考勤打卡」快捷卡片 + +### 菜单 +- `DbSeeder.cs`:移除 `SeedAttendanceMenus(fsql);` 调用(方法体保留为死代码,未调用即不生效) +- ⚠️ **库中已存在的 `/attendance` 菜单记录不会自动删除**,需在「系统管理-菜单管理」手动删, + 或在 seed 中补清理逻辑(注意 `DbSeeder` 里菜单实体有 `SysMenu` 与 `Menu` 两种引用,别写错类型) + +### 编译 +- 解决方案编译:**无 error CS** ✅ + +> 注:`views/notification/type/index.vue` 里 `/attendance/approve?leaveId={businessId}` 只是 +> **placeholder 示例文本**,不影响运行,未改(建议后续改成中性示例)。 + +## 三、老系统「职工考勤」调研结论(移植依据) + +### 1. 实体 + +**考勤登记主表 `Cqdj`**(表 `biz_prod_kq_cqdj`,一人一天一条) +`Date / Dep_Id / Worktype_Id / Employee_Id / Banci / Is_xuegtong` +`Hours_standard / Hours_cq / Hours_qj / Hours_ld / Hours_jb / Hours_db / Hours_wp / Hours_kg / Hours_gs` +`Cs_yeban(夜班次数) / Memo` +唯一约束:`(Date, Employee_Id)`(一人一天一条);索引 `(Date,Dep_Id,Worktype_Id,Banci)` + +**登记明细 `Cqdj_Item`**(表 `biz_prod_kq_cqdj_item`,一人一天多行,按出勤类型拆) +`Cqdj_id / Dep_id / Worktype_id / Employee_id / Banci / Type_cq / Type_qj / Hours` +`Jizu / Ct_start / Ct_end / Dangche_num / Memo` + +**考勤统计 `Cqtj`**(表 `biz_prod_kq_cqtj`,**月快照**,非实时) +`Year / Month / Dep_id / Worktype_id / Employee_id` +`Days_cq_standard / Days_cq_actual / Hours_cq / Hours_qj` +`Hours_qj_shijia/bj/hj/cj/lx/sangjia/gj/nj/tx`(事病婚产轮休丧公年调) +`Hours_ld / Hours_jb / Hours_db / Hours_wp / Hours_kg / Cs_yeban` +`Hours_fadingjiaqi / Hours_qj_qqmc / Hours_qj_qqcal` +`Jine_quanqin / Jine_jb / Jine_jq_bz_hj/sangjia/bj/lx/fading/txnj / Jine_jq_bz` +`Jine_yb / Jine_kg_kk / Jine_liudong / Jine_daiban` +`Hours_xg_cq / Hours_xg_jiaban / Cq_tianci` + +### 2. 枚举(硬编码,无配置表) + +- 出勤类型 `Cqdj_CqType_Enum`:正常=1 请假=2 流动=3 加班=4 代班=5 外派=6 旷工=7 工伤=8 +- 请假类型 `Cqdj_QjType_Enum`:无=0 事假=1 病假=2 产假=3 婚假=4 丧假=5 公假=6 年假=7 调休=9 轮休=10 + - 校验:出勤类型=请假时请假类型≠无;非请假时请假类型必须为无 +- 班次 `Prod_Banci_Enum`:甲=1 乙=2 + +### 3. 考勤登记(CQDJ)规则 + +- 查询:日期 + **部门(必选)** + 工种(可空=全部) + 班次(默认甲) + 关键字 +- 主列表列:部门/工种/姓名/班次/学工/标准时长/出勤/请假/流动/加班/代班/外派/工伤/旷工/夜班/备注 +- 明细(选中职工联动):出勤部门/工种/姓名/班次/出勤类型/请假类型/时长/备注 +- **核心汇总公式**: + ``` + 正常明细.Hours = 标准 - 请假 - 旷工 - 流动 - 外派 - 代班 - 工伤 (倒挤) + Hours_cq = Σ(正常 + 流动 + 代班 + 外派 + 工伤) (不含加班) + 其余 Hours_qj/kg/ld/jb/wp/db/gs = 各类型明细求和 + 校验 Hours_cq >= 0 + ``` +- 约束:每人最多一条「正常」明细;不能删最后一条正常记录;保存前校验每人必须有正常记录 +- 新职工默认:`Hours_standard=9`、`Hours_cq=9`、明细 `Hours=9`、类型=正常/无;`Is_xuegong = !Base_is_zhuanzheng` +- **保存方式**:`Cqdj_SaveCommand` = 按 `(日期,部门,工种,班次)` **先软删旧登记与明细,再整批新增**(不做增量 diff) +- 按钮:设标准时长/设出勤时长/加班/流动/请假/公假/代班/夜班/添加职工/删除职工/刷新 +- 「添加其他车间」(`Page_Cqdj_addOtherChejian_select.cs`)**老系统中无任何页面引用**,属遗留未启用功能,可不移植 + +### 4. 考勤统计(CQTJ)规则 + +- 维度:**年/月(账期)+ 部门 + 工种 + 职工**(一人一行) +- 数据来源:**快照**。点「录入」时:若该 年/月/部门 已有统计则取库;否则由 `Cqtj_TongjiQuery` 取账期内 + 全部 Cqdj + Cqdj_Item + 薪资配置 + 职工,在客户端计算后 `Cqtj_Save`(先软删该年月部门旧数据再整批插入) +- 派生公式(关键): + ``` + 实际出勤天数 = Round(本部门 Hours_cq / 工种配置.HoursPerday, 2) + 出勤天次 = 该人该部门登记中 (Hours_cq + Hours_jb) > 0 的天数计数 + 夜班补助 = 工种配置.JineYeban × Cs_yeban + 旷工考核 = 工种配置.JiaqiJineKuangongKk × Hours_kg + 流动工资 = Σ 工种配置.LiudongJineGuding × 流动时长 + 代班工资 = Σ (代班部门工种.JineGangwei / (QqDaysStandard×HoursPerday)) × 代班时长 + 带薪时长 = 轮休+年假+公假+调休+婚假+产假+丧假 + 加班免扣请假 = 若(出勤+总加班) ≥ QsQuanqinDays×HoursPerday 且 总加班 ≥ QqHoursDkMinjiaban + → QqHoursDkQingjia,否则 0 + 全勤计算请假 = 总请假 - 加班免扣 - 带薪时长 + 假期补助 = 病假×BingjiaBz + 轮休×LunxiuBz + 婚假×HunjiaBz + 丧假×SangjiaBz + + (年假+调休)×NianjiaBz + 法定假补助 + (事假/产假/公假只计时长不计金额) + 加班费 = Σ 加班时长 × 单价(单价取岗位胜任考核 5 档对应 Jiaban_gwsrkhJine*,无考核取 JiabanJineBz) + 法定假期时长 = 职工配置.Hours_FadingBz + Hours_FadingGz + ``` +- **全勤奖 `quanqin()` 最复杂**(老系统在客户端 `Page_Cqtj.cs:837`,服务端 Command 是空实现): + 学工用 `XgJineQuanqin`;总旷工 ≥ 半天 → 0;总出勤 ≤ 3 天 → 0;当月离职且出勤 ≤15 天 → 0; + 单部门/工种:出勤 ≥15 天给满,否则 `全勤奖/(QqDaysStandard×HoursPerday) × 出勤时长`; + 多部门:`全勤奖/总出勤时长 × 本部门出勤时长`;入职日在账期内按标准折算;设备办全职特殊口径; + 最终:`全勤计算请假 ≥ QqHoursMaxQj → 0`,否则 `初始全勤奖 - 初始全勤奖/QqHoursMaxQj × 全勤计算请假` + +### 5. 依赖的基础数据(移植前必须确认新系统是否具备) + +| 依赖 | 说明 | +| --- | --- | +| 部门 | 排除「公司」类型,`Department_DutyType_Enum`(前缫=25/后缫=30/选剥茧=15/设备办=35) | +| 职工 | `Employee_GetByDepartmentIdView(DepId, WorkTypeId, Banci)`;用 `Base_is_zhuanzheng`(→学工)、`Base_RuzhiDate`、`LizhiDate`、`Base_WrokStatus`;`Base_WorktypeId=0` 过滤掉 | +| 工种 | `WorkType_GetByDepartmentIdView`;注册表 `基础数据\工种` 存 挡车工/车头工/值班长/选茧工 ID | +| 账期 `Biz_Base_Zhangqi` | `Zhangqi_GetByDate`、`Zhangqi_GetIsZhaZhangByDate`(**扎帐即禁止增删改**) | +| 薪资配置-工种 CONFGZ | HoursPerday、JineQuanqin、XgJineQuanqin、QqDaysStandard、QqHoursMaxQj、QqHoursDkMinjiaban、QqHoursDkQingjia、JiaqiJine*、JineYeban、JiaqiJineKuangongKk、LiudongJineGuding、JineGangwei、JiabanJineBz、Jiaban_gwsrkhJine*(5档) | +| 薪资配置-职工 CONFEMPLOYEE | `Hours_FadingBz`、`Hours_FadingGz`、`IsXuegong` | +| 薪资配置账期 CONFZQ | `QsQuanqinDays`(前缫出勤天数,必填) | +| 岗位胜任考核 | 决定加班单价档位 | + +## 四、待办(下一轮实施移植) + +1. 后端实体:`Cqdj` / `CqdjItem` / `Cqtj`(放 `Domain/Entities/Prod/`,继承 `BaseEntity`, + **必须 `using F9WebAiTest.Domain.Common;`**) +2. DTO + Service + Controller(路由建议 `api/prod/kaoqin`),含: + - 登记分页 / 按(日期,部门,工种,班次)整批保存(先软删后新增)/ 删除 + - 统计分页 / 生成(快照)/ 编辑保存 / 删除 +3. 前端页面(`web/src/views/prod/kaoqin/`):考勤登记(主从联动 + 明细按出勤类型)、考勤统计 +4. 菜单:在 `DbSeeder.SeedProdMenus` 用 `EnsureTopMenuWithChildren` 加(与选剥茧同法) + - ⚠️ 加菜单必须改 **`DbSeeder.SeedProdMenus`(`SysMenu`)**,不要只改 `BusinessSeedData` +5. 确认新系统是否已有薪资配置(CONFGZ/CONFZQ/CONFEMPLOYEE)——**统计页强依赖**,缺失则需先补齐或先做简化版统计 +6. 库中历史 `/attendance` 菜单手动删除 + +## 五、下次接续提示 + +- 出勤/请假/班次是**硬编码枚举**,前端下拉直接写死,不用查表 +- 登记保存是「整批覆盖」不是增量 diff,实现时别做反 +- 统计是**快照**(生成后落库),不是实时 SQL 汇总 +- 移植顺序建议:**先做考勤登记(CQDJ)**,统计(CQTJ)因强依赖薪资配置,放到薪资配置确认后再做 +- **每轮对话结尾记得生成本工作进度文档** diff --git a/工作进度/工作进度-24.md b/工作进度/工作进度-24.md new file mode 100644 index 0000000..af14fc9 --- /dev/null +++ b/工作进度/工作进度-24.md @@ -0,0 +1,78 @@ +# 工作进度-24:重启前后端 + 修复「报表设计管理器」打不开 + +> **协作规则(长期生效)**:**每次对话结尾都要生成工作进度**。 +> 即在每轮对话结束前,把本轮完成的内容追加为 `工作进度/工作进度-N.md`,并在文档中保留本条规则,方便下一轮接续。 + +## 一、重启前后端 ✅ + +| 服务 | 端口 | 重启后 PID | +| --- | --- | --- | +| 后端 `F9WebAiTest.Api` | 3000 | 26028 | +| 前端 vite | 5173 | 25640 | + +(复用 22 轮方法:按端口定位 PID → `Stop-Process` → `dotnet run` + node 完整路径启动 vite。 +npm/node 不在 PATH,必须用 `C:\Users\32453\.workbuddy\binaries\node\versions\22.22.2-6\node.exe`。) + +## 二、报表设计管理器:根因与修复 ✅ + +### 调用链 + +``` +报表管理中心 views/report/manage/index.vue 点列表行「设计」 + → openDesign(row):designOnOpen=true、selectedId=row.id、designerKey++ + → components/report/ReportEditor.vue 的 init() 末尾: + if (props.openDesign && savedId.value) designVisible.value = true; + → 全屏 el-dialog 渲染 components/report/ReportDesign.vue(v-if="designVisible && savedId") + → ReportDesign 内「用本机锐浪 7 设计器编辑」按钮 → openGridDesigner() + window.location.href = 'f9designer://open?base=..&rid=..&token=..&code=..' +``` + +### 根因(实测逐层排查得出) + +| 环节 | 实测结果 | +| --- | --- | +| `f9designer://` 协议注册 | ✅ **已注册**(`HKCR\f9designer`,含 `URL Protocol`) | +| 协议 command | `powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File "D:\f9webaitest\f9-web-ai-test\tools\report-designer\F9ReportDesignerHandler.ps1" "%1"` | +| **该宿主脚本** | ❌ **不存在**(`tools\report-designer\` 整个目录都没有)→ **根因** | +| 锐浪设计器 `gr7designer64.exe` | ✅ 已安装:`D:\GridReport7\gr7designer64.exe` | +| 后端接口 | ✅ 齐全(见下) | + +因为用了 `-WindowStyle Hidden`,powershell 启动失败时**没有任何提示** → 用户看到的就是"点了没反应/打不开"。 + +> 修正:此前 24 轮初稿判断"项目内没有配套协议注册文件"不准确 —— 协议已注册, +> 缺的只是被协议指向的那个**宿主脚本**(属于应入库但漏掉的资产)。 + +### 后端接口(脚本依赖) + +| 用途 | 接口 | 说明 | +| --- | --- | --- | +| 下载模板 | `GET /api/sys/report/{id}/template?access_token={token}` | `[AllowAnonymous]`,token 走 query;另有 `.grf` / `.json` 别名 | +| 上传模板 | `POST /api/sys/report/{id}/template` | `IFormFile` 参数名 **`file`**;注释明确"保留给 f9designer:// 协议回传使用" | + +### 修复:新建宿主脚本 + +`tools/report-designer/F9ReportDesignerHandler.ps1`(4434 字节,PowerShell 语法检查通过): + +1. 解析 `f9designer://open?base=&rid=&token=&code=`(URL 解码) +2. 定位设计器:默认 `D:\GridReport7\gr7designer64.exe`,可用环境变量 `F9_GR7_DESIGNER` 覆盖 +3. 下载模板到 `%TEMP%`(下载失败则建空白模板,兼容"新报表无模板") +4. `Start-Process -Wait` 打开设计器并**阻塞等待关闭** +5. 关闭后用 `System.Net.Http.MultipartFormDataContent` 上传(带 `Authorization: Bearer`) +6. 全程写日志 `tools/report-designer/logs/designer_yyyyMMdd.log` + +> 兼容性说明:用 .NET `HttpClient` 手工构造 multipart,而不是 PowerShell 7 的 `-Form`, +> 因为注册表里调用的是 `powershell.exe`(Windows PowerShell 5.1 不支持 `-Form`)。 + +### 待用户验证 + +在报表中心点「设计」→「用本机锐浪 7 设计器编辑」,应弹出锐浪设计器;关闭后模板自动回传。 +若仍无反应,看 `tools/report-designer/logs/designer_yyyyMMdd.log`。 + +## 三、下次接续提示 + +- 报表相关文件:`views/report/manage/index.vue`、`views/report/center/index.vue`、 + `components/report/ReportEditor.vue`、`components/report/ReportDesign.vue`、`utils/gridreport/*`、 + 后端 `Services/Reports/ReportService.cs` + `Controllers/SysReportController.cs` +- 设计面板打开前提:报表**必须已保存**(`v-if="designVisible && savedId"`),未保存的报表点了无反应 +- 设计面板有两条路径:① 可视化画布(纯前端、无外部依赖)② 本机锐浪设计器(需本协议链) +- **每轮对话结尾记得生成本工作进度文档** diff --git a/工作进度/工作进度-25.md b/工作进度/工作进度-25.md new file mode 100644 index 0000000..a5c357c --- /dev/null +++ b/工作进度/工作进度-25.md @@ -0,0 +1,127 @@ +# 工作进度-25:修复「报表设计打不开」+ 补上「生产-考勤 · 考勤登记」 + +> **协作规则(长期生效)**:**每次对话结尾都要生成工作进度**。 +> 即在每轮对话结束前,把本轮完成的内容追加为 `工作进度/工作进度-N.md`,并在文档中保留本条规则,方便下一轮接续。 + +## 一、用户反馈 + +1. 报表设计页面打不开(之前可以用,今天突然不行) +2. 生产-考勤也没有 + +## 二、报表问题:两个根因,都已修复 ✅ + +### 根因 A:vite 开发服务器挂死(最直接的"打不开") + +| 检查 | 结果 | +| --- | --- | +| 后端 3000 | ✅ 正常(`/sys/report` 列表、模板下载、预览全部 200) | +| 前端 5173 早期请求 | ✅ 200(`report/manage`、`ReportEditor`、`ReportDesign`、`grlib/grhtml5-7-min.js`) | +| 稍后 `/`、`/src/views/prod/lsjl/index.vue` | ❌ **全部超时**(进程 25640 仍在 LISTENING,CPU 近乎 0 → 卡死不服务) | + +→ 页面请求全部挂住,表现为"点了没反应 / 打不开"。 + +**修复**:按端口定位 PID → `Stop-Process -Id 25640 -Force` → 用完整路径 node 重启 vite(npm 不在 PATH): + +```powershell +Start-Process -FilePath 'C:\Users\32453\.workbuddy\binaries\node\versions\22.22.2-6\node.exe' ` + -ArgumentList 'node_modules/vite/bin/vite.js' -WorkingDirectory 'd:/f9webaitest/f9-web-ai-test/web' -WindowStyle Minimized +``` + +重启后 PID **26328**:`/` 200、`prod/kaoqin/index.vue` 200、`report/manage/index.vue` 200。 + +### 根因 B:f9designer:// 宿主脚本解析 URL 失败(点「设计器编辑」无反应) + +日志 `tools/report-designer/logs/designer_20261026.log` 里今天 6 次唤起全部报错: + +``` +错误: 缺少 base / rid 参数(Url=f9designer://open/?base=...) +``` + +原因:**Windows Shell 会在协议主机后补一个 `/`**,实际收到的是 `f9designer://open/?base=...`, +而脚本按固定前缀 `^f9designer://open\?` 剥离失败 → `base` 解析为空 → 直接 throw。 +注册表里是 `-WindowStyle Hidden`,失败无提示 → 用户看到"点了没反应"。 + +**修复** `tools/report-designer/F9ReportDesignerHandler.ps1`: + +1. 解析改为「取第一个 `?` 之后的全部内容」,兼容 `open?` 与 `open/?` +2. `base` 缺失时回退 `http://localhost:5173`(并记日志) +3. 新增 `Show-Error`:失败时弹 Windows MessageBox(含日志路径),不再静默 +4. ⚠️ **踩坑**:编辑工具把文件存成了 **UTF-8 无 BOM**,Windows PowerShell 5.1 按 ANSI 读取导致中文注释/字符串乱码、脚本解析失败。 + 已用 `UTF8Encoding($true)` 重写为 **UTF-8 with BOM**,`ParseFile` 复检通过。 + **下次改这个脚本后务必确认 BOM 仍在**(`[System.IO.File]::ReadAllBytes($p)[0..2]` 应为 239,187,191)。 + +**端到端实测**(用 `F9_GR7_DESIGNER` 指向一个立刻退出的程序代替设计器): + +``` +base=http://localhost:5173 rid=141374578868883456 code=PROD_QS_1 +已下载 8300 字节 → 启动设计器(等待关闭)→ 设计器已关闭 → 上传结果: OK → 完成 +``` + +模板回传后长度仍为 7639 字节、报表 `hasTemplate=True`,未损坏。 + +## 三、生产-考勤:本轮补上「考勤登记(CQDJ)」✅ + +> 「没有」的原因:23 轮按计划**删除了旧考勤**(`/attendance`),而「生产-职工考勤」移植尚未开始。 + +### 后端(新增) + +| 文件 | 说明 | +| --- | --- | +| `Domain/Entities/Prod/Cqdj.cs` | `Cqdj`(表 `biz_prod_kq_cqdj`)+ `CqdjItem`(表 `biz_prod_kq_cqdj_item`),继承 `BaseEntity`,**自动建表**(`DbSeeder.SyncStructure` 反射扫描) | +| `Application/Dtos/Prod/KaoqinDtos.cs` | `CqdjItemDto / CqdjRowDto / CqdjViewDto / CqdjSaveDto / CqdjEmpOptionDto` | +| `Application/Services/Prod/KaoqinService.cs` | 接口+实现同文件:整组查询 / 整组覆盖保存 / 整组删除 / 可选职工 | +| `Api/Controllers/Prod/KaoqinController.cs` | `[Route("api/prod/kaoqin")]` `[Authorize]`:`GET|POST|DELETE /cqdj`、`GET /cqdj/employees` | +| `Application/DependencyInjection.cs` | `services.AddScoped();` | + +规则按 23 轮调研落地: + +- 查询口径:日期 + 部门(必选)+ 工种(0=全部)+ 班次(甲1/乙2)+ 关键字 +- 职工来源:本部门、工种≠0、非离职;未登记者生成默认值 9 小时 + 一条「正常」明细 +- **正常明细倒挤**:`正常 = 标准 - 请假 - 旷工 - 流动 - 外派 - 代班 - 工伤`;`出勤 = 正常+流动+代班+外派+工伤` +- 保存 = **先软删该(日期+部门+工种+班次)旧登记与明细,再整批新增**(非增量 diff) +- 校验:每人必须有且仅有一条「正常」;请假必须选请假类型、非请假必须为「无」;出勤不能为负 + +### 前端(新增) + +- `web/src/views/prod/kaoqin/index.vue`:上表(职工行 + 标准时长/夜班/备注 + 各时长实时汇总)下表(选中职工的明细:出勤类型/请假类型/时长/机组/车台/挡车数/备注), + 工具栏含 查询 / 设标准时长 / 添加职工 / 删除整组 / 保存 +- `web/src/router/index.js`:`'prod/kaoqin/index': () => import('../views/prod/kaoqin/index.vue')` + +### 菜单(`DbSeeder.SeedProdMenus`) + +```csharp +EnsureTopMenuWithChildren(fsql, snowflake, "/prod/kaoqin", "生产-考勤", "Clock", 8, + new[] { ("考勤登记", "/prod/kaoqin/cqdj", "prod/kaoqin/index", "prod:kaoqin:cqdj:list", "EditPen", 1) }); +``` + +同时**清理了遗留旧考勤菜单**(页面组件已删,留着会点开空白页): +删除 `sys_rbac_menus` 中 `path LIKE '/attendance%'` 的记录及其 `role_permissions`, +并删除 `code LIKE 'attendance:%'` 的权限单元。 + +### 验证 + +| 项 | 结果 | +| --- | --- | +| 解决方案编译 | **0 个错误** | +| `GET /auth/menus` | ✅ 出现 `生产-考勤 → 考勤登记`,旧「考勤管理」已消失 | +| `GET /prod/kaoqin/cqdj?date=2026-10-26&depId=34&banci=1` | ✅ 46 人,每人默认 9 小时 + 1 条正常明细 | +| `POST /prod/kaoqin/cqdj` | ✅ `count=46`;复查 `saved=46` | +| `DELETE /prod/kaoqin/cqdj` | ✅ `count=46`;复查 `saved=0`(测试数据已清理) | +| 前端模块编译 | ✅ vite 200 | + +## 四、待办 + +1. **考勤统计(CQTJ)**:月快照,强依赖工种薪资配置 `ConfGz`(已有)+ 职工薪资配置 CONFEMPLOYEE / 账期薪资配置 CONFZQ(**后端还没有,需先建**) +2. 老系统「添加其他车间」功能无页面引用,不移植 +3. 报表设计:可视化画布与锐浪设计器二选一(用设计器保存后产物为 `_WR7_`,画布将不可解析) +4. `views/notification/type/index.vue` 里的 `/attendance/approve?leaveId=` 仍是示例文本,建议改成中性示例 + +## 五、下次接续提示 + +- 改后端代码后**必须重启后端**(先按端口 Stop-Process,否则 DLL/obj 锁定导致编译失败);改前端 vite 热更新,但 **vite 卡死时要按端口杀掉重启** +- 登录 admin / 123456;后端 3000(PID 23968),前端 5173(PID 26328) +- 考勤相关文件:`Domain/Entities/Prod/Cqdj.cs`、`Dtos/Prod/KaoqinDtos.cs`、 + `Services/Prod/KaoqinService.cs`、`Controllers/Prod/KaoqinController.cs`、`views/prod/kaoqin/index.vue` +- 报表相关:`views/report/manage/index.vue`、`components/report/ReportEditor.vue`、`ReportDesign.vue`、 + `tools/report-designer/F9ReportDesignerHandler.ps1`(**务必保持 UTF-8 BOM**) +- **每轮对话结尾记得生成本工作进度文档** diff --git a/工作进度/工作进度-26.md b/工作进度/工作进度-26.md new file mode 100644 index 0000000..96fc50d --- /dev/null +++ b/工作进度/工作进度-26.md @@ -0,0 +1,106 @@ +# 工作进度-26:参考老系统完成整个考勤模块(CQDJ 补齐 + CQTJ 新建 + 依赖配置) + +> **协作规则(长期生效)**:**每次对话结尾都要生成工作进度**。 +> 即在每轮对话结束前,把本轮完成的内容追加为 `工作进度/工作进度-N.md`,并在文档中保留本条规则,方便下一轮接续。 + +## 一、需求 + +用户要求:**完善整个考勤模块,不要删减**,并给出老系统参考目录 +`D:\AntaiProject - 副本\trunk\WinClient\F9.Business.Prod\KQ`(CQDJ 考勤登记 + CQTJ 考勤统计)。 + +## 二、老系统源码研读结论(KQ 目录) + +| 文件 | 大小 | 结论 | +| --- | --- | --- | +| `CQDJ\Page_Cqdj.cs` | 19KB | 查询口径:日期 + 部门 + 工种 + 班次;录入/删除整组、初始化为正常出勤;「从指定日期复制」 | +| `CQDJ\Page_Cqdj_Edit.cs` | 77KB | 工具栏 8 个按钮(标准/出勤/加班/流动/事假/公假/代班/夜班)+ 添加/删除职工 + 刷新;保存整组覆盖、倒挤「正常」 | +| `CQDJ\Page_Cqdj_addOtherChejian_select.cs` | — | 确认**无引用**,遗留功能,不移植 | +| `CQTJ\Page_Cqtj.cs` | 84KB | 统计是**月快照**;录入时库中有快照取库,否则 `CqtjViews` 实时汇总;含 `quanqin` 全勤奖、`jiaqiBuzhuJisuan` 假期补助、`jisuanJiabanMkHours` 加班免扣、`jisuanJianbanJine` 加班费、`liudongJineJisuan` 流动、`daibanJineJisuan` 代班、`jisuanCqTianci` 出勤天次、`Yijianshengcheng` 一键生成 | +| `CQTJ\Page_Cqtj_Edit.cs` | 7.5KB | 可编辑列:年假/公假/调休/法定假/全勤免扣/全勤计算/全勤奖/加班费/流动工资/代班工资/假期补助分项与合计/夜班补助/旷工考核 | + +CQTJ 强依赖(老系统在 PayRoll 侧): +- **CONFZQ 薪资配置-账期**:`QsQuanqinDays` 前缫出勤天数(为 0 时直接抛错不让录入) +- **CONFEMPLOYEE 薪资配置-职工**:`Hours_FadingBz` / `Hours_FadingGz` / `IsXuegong` +- **岗位胜任考核**:考核结果决定加班费单价档位(无记录时回落 `JiabanJineBz`) + +## 三、本轮新增 / 修改 + +### 后端(新增) + +| 文件 | 说明 | +| --- | --- | +| `Domain/Entities/Prod/Cqtj.cs` | **考勤统计实体**(`biz_prod_kq_cqtj`),含天数/时长/请假分项/学工/法定假/全勤相关时长/全部金额字段 | +| `Domain/Entities/PayRoll/ConfEmployee.cs` | `ConfZq`(`ConfZqs`)、`ConfEmployee`(`ConfEmployees`)、`Gwsrkh`(`Gwsrkhs`) | +| `Dtos/Prod/KaoqinDtos.cs` | 追加 `CqtjRowDto / CqtjSaveDto / CqtjGenerateDto / CqtjViewDto` | +| `Dtos/PayRoll/ConfEmployeeDtos.cs` | `ConfZqDto / ConfEmployeeDto / GwsrkhDto` | +| `Services/Prod/KaoqinService.cs` | 追加 CQTJ:分页、录入(取库 or 实时汇总)、保存(整组覆盖)、删除、一键生成、全部删除;并新增 8 个口径函数 | +| `Services/PayRoll/ConfEmployeeService.cs` | `IConfZqService / IConfEmployeeService / IGwsrkhService` 接口 + 实现 | +| `Api/Controllers/Prod/KaoqinController.cs` | 追加 `GET/POST/DELETE /cqtj`、`GET /cqtj/edit`、`POST /cqtj/generate` | +| `Api/Controllers/PayRoll/ConfEmployeeController.cs` | `api/payroll/confzq`、`api/payroll/confemployee`、`api/payroll/gwsrkh` | +| `Application/DependencyInjection.cs` | 注册 `IConfZqService / IConfEmployeeService / IGwsrkhService` | +| `Infrastructure/Data/DbSeeder.cs` | 菜单:生产-考勤 增「考勤统计」;薪资 增「账期薪资配置 / 职工薪资配置 / 岗位胜任考核」 | + +### 后端口径(严格对齐老系统) + +- **正常倒挤**:`正常 = 标准 - 请假 - 旷工 - 流动 - 外派 - 代班 - 工伤`;`出勤 = 正常 + 流动 + 代班 + 外派 + 工伤`(不含加班) +- **出勤天数**:`标准 = 工种配置 QqDaysStandard`;`实际 = 本部门出勤时长 / HoursPerday`;`天次 = (出勤+加班)>0 的天数` +- **全勤奖** `quanqin()`(完整移植,含多部门 / 多工种分支): + 1. 总旷工 ≥ 半天 → 0;2. 总出勤 ≤ 3 天 → 0; + 3. 多部门:`∑出勤+带薪 ≥ 15 天` 按实际出勤折算,否则按标准出勤折算;离职 / 新入职分别处理; + 4. 单部门:`∑出勤+带薪请假 ≥ 15 天` 取全额,否则 `全勤奖/标准出勤时长×总出勤`; + 5. 设备办全职:出勤含加班达前缫天数且不低于标准 → 请假不扣; + 6. 其余:`全勤奖 - (全勤奖 / QqHoursMaxQj) × 请假时长` +- **假期补助**:按明细「请假类型 × 出勤工种」分组,病假/轮休/婚假/丧假/法定假/年假/调休分别乘工种单价;`法定假 = 补助小时×JiaqiJineFadingBz + 工资小时×JiaqiJineFadingGz` +- **加班免扣请假**:`出勤+加班 ≥ 前缫出勤天数×HoursPerday 且 加班 ≥ QqHoursDkMinjiaban` → 免扣 `QqHoursDkQingjia` +- **加班费**:按加班明细工种取单价;有岗位胜任考核时按 欠合格/期望/合格/胜任/超胜任 取 `Jiaban_GwsrkhJine*`,无记录取 `JiabanJineBz`;学工加班 = `XgJineGangwei /(QqDaysStandard×HoursPerday) × 时长` +- **流动** = `LiudongJineGuding × 时长`;**代班** = `JineGangwei /(QqDaysStandard×HoursPerday) × 时长`;**夜班** = `JineYeban × 夜班次数`;**旷工考核** = `JiaqiJineKuangongKk × 旷工时长` +- **扎帐校验**:账期 `IsZhaZhang` 时禁止保存 / 删除 / 生成 + +### 前端 + +| 文件 | 说明 | +| --- | --- | +| `views/prod/kaoqin/index.vue`(改) | 补齐工具栏:刷新 / 复制指定日期 / 设标准时长 / 设出勤时长 / **加班 / 流动 / 事假 / 公假 / 代班 / 夜班** / 添加职工 / 删除职工 / 删除整组 / 保存;职工表加多选列;明细表加「出勤部门 / 出勤工种」(代班、流动跨车间);保存前「出勤超标准」二次确认;新增复制日期弹窗(可勾选初始化为正常出勤) | +| `views/prod/kaoqin/cqtj/index.vue`(新) | 考勤统计:年月 + 部门 + 工种 + 关键字;分组表头(出勤天数 / 时长 / 学工 / 请假分项 / 全勤与金额);录入(弹窗内可编辑带「*」字段)、删除、**一键生成**、全部删除;分页 | +| `views/payroll/confzq/index.vue`(新) | 账期薪资配置:年度/月份/前缫出勤天数 | +| `views/payroll/confemployee/index.vue`(新) | 职工薪资配置:职工/工种/法定补助h/法定工资h/学工 | +| `views/payroll/gwsrkh/index.vue`(新) | 岗位胜任考核:年度/月份/部门/职工/考核结果 | +| `router/index.js`(改) | 新增 5 条 componentMap | + +## 四、验证 + +| 项 | 结果 | +| --- | --- | +| 解决方案编译 | **0 个错误** | +| `GET /auth/menus` | ✅ 生产-考勤 → 考勤登记 / 考勤统计;薪资 → …/账期薪资配置 / 职工薪资配置 / 岗位胜任考核 | +| 账期薪资配置 | ✅ POST 2026/10 QsQuanqinDays=26 成功 | +| 考勤登记 | ✅ 46 人保存成功(含 1 人加班 2h + 病假 1h + 夜班 1) | +| 实时汇总 | ✅ `曾红君 出勤=8 加班=2 请假=1 实际天=0.89 天次=1 加班费=30 病假补助=5 补助合计=5 夜班补助=20`(与手算一致:正常 9-1=8,加班费 2×15,病假 1×5,夜班 1×20) | +| 统计保存 / 列表 / 取库 | ✅ 保存 46 → 列表 total=46 → 再录入 `exists=True` | +| 删除 / 一键生成 | ✅ 删除 46;一键生成 1 个车间 | +| 前端 5 个页面 | ✅ vite 全部 200,无 lint | + +测试数据(2026-10-26 的 46 条登记、统计快照、测试用工种薪资配置 3 条、账期配置 1 条)**已全部清理**。 + +## 五、待办 / 已知差异 + +1. **跨部门重复考勤提示**:老系统在加载时提示「X 职工已在 Y 部门录入」,本轮未移植(需额外接口),后续补 +2. **岗位胜任考核**放在【薪资】下(老系统属 `CalXzMain` 薪资模块),仅按年月维护考核结果,未做考核流程 +3. 统计页的**列宽 / 分组表头**在窄屏下需横向滚动,后续可加列配置 +4. 报表设计器脚本 `F9ReportDesignerHandler.ps1` **改完必须确认 UTF-8 BOM 仍在** +5. `views/notification/type/index.vue` 里仍有 `/attendance/approve?leaveId=` 示例文本 + +## 六、下次接续提示 + +- 改后端先按端口 `Stop-Process` 再编译,否则 DLL 锁定;vite 卡死时同样按端口杀掉重启 + (当前:后端 3000 PID 17000,前端 5173 PID 26328) +- 登录 admin / 123456 +- 考勤相关文件: + - 后端 `Domain/Entities/Prod/Cqdj.cs`、`Cqtj.cs`、`Domain/Entities/PayRoll/ConfEmployee.cs` + - `Dtos/Prod/KaoqinDtos.cs`、`Dtos/PayRoll/ConfEmployeeDtos.cs` + - `Services/Prod/KaoqinService.cs`(CQDJ + CQTJ 全部口径)、`Services/PayRoll/ConfEmployeeService.cs` + - `Controllers/Prod/KaoqinController.cs`、`Controllers/PayRoll/ConfEmployeeController.cs` + - 前端 `views/prod/kaoqin/index.vue`、`views/prod/kaoqin/cqtj/index.vue`、 + `views/payroll/confzq|confemployee|gwsrkh/index.vue` +- 老系统参考:`D:\AntaiProject - 副本\trunk\WinClient\F9.Business.Prod\KQ\` +- **每轮对话结尾记得生成本工作进度文档** diff --git a/工作进度/工作进度-27.md b/工作进度/工作进度-27.md new file mode 100644 index 0000000..575bb5b --- /dev/null +++ b/工作进度/工作进度-27.md @@ -0,0 +1,39 @@ +# 工作进度-27 + +> 规则原文:每次对话结尾都要生成工作进度 + +## 本轮任务 + +回答用户提问:**本项目采用的架构是什么**(纯咨询,无代码改动)。 + +## 本轮完成内容 + +1. 梳理并确认项目整体架构: + - 总体:前后端分离 + 单体后端四层分层(DDD + CQRS),IIS 单站点同源部署(`wwwroot` 静态资源 + `MapFallbackToFile("index.html")`)。 + - 后端 .NET 8 四层:`F9WebAiTest.Domain` / `.Application` / `.Infrastructure` / `.Api`,依赖方向 `Api → Application → Domain ← Infrastructure`。 + - CQRS + MediatR 12.4.1(`Application/CQRS/SYS/**/Commands|Queries`,45 个文件),读写仓储分离。 + - 工作单元 `IUnitOfWork_F9`、雪花 ID 单例、FreeSql 软删除全局过滤器。 + - 领域事件 `BusinessChangedEvent` → MediatR Handler → SignalR `/hubs/notification` 推送。 + - RBAC:用户–角色–权限–菜单,JWT Bearer;SignalR 与报表下载走 query string `access_token`。 + - `LongToStringJsonConverter` 解决雪花 ID 精度丢失。 + - 前端 `web/`:Vue 3 + Vite + Element Plus + Pinia + Vue Router + axios,NSwag 生成 `src/api/client.ts`,`v-permission` 按钮级权限,动态路由。 + - 数据层:SQLite + FreeSql(CodeFirst + `DbSeeder` 种子),枚举映射 int 兼容旧库。 + - 遗留/辅助目录:`client/` + `server/`(早期「社区生活圈」Express + Vue3 原型)、`TempleRun/`(旧 WinForms 参考)、`tools/`、`deploy-iis.ps1`、`update-iis.ps1`。 + +## 改动文件 + +无(本轮为架构说明,未修改任何代码)。 + +## 发现的问题(待修正) + +- 根目录 `README.md` 中「数据库:SQLite(EF Core)」的表述已过时,实际 ORM 为 **FreeSql**;README 也缺少 CQRS/MediatR、SignalR、DDD 分层等当前架构描述。 + +## 验证情况 + +- 已通过目录浏览、README、`F9WebAiTest.Api.csproj`、`Program.cs`、`Infrastructure/DependencyInjection.cs`、`web/package.json`、`server/package.json`、`client/package.json` 交叉核对确认。 +- 未编译、未运行(无代码改动)。 + +## 待办 + +1. (可选)更新 `README.md`:EF Core → FreeSql,补充 CQRS/MediatR、SignalR 实时推送、IIS 同源部署说明。 +2. 继续按既有计划推进业务模块迁移。