package com.kaidi.oa.web; import com.kaidi.oa.common.ApiException; import com.kaidi.oa.common.ApiResp; import com.kaidi.oa.common.Money; import com.kaidi.oa.common.NotFoundException; import com.kaidi.oa.domain.FertMps; import com.kaidi.oa.repository.FertMpsRepository; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.math.BigDecimal; import java.time.Instant; import java.time.LocalDate; import java.time.format.DateTimeParseException; import java.util.List; /** * 生物质肥料制造中心·主生产计划 MPS(需求 §2,补深审计 PARTIAL[med])。 * * 跨批次产能/排产/发酵周期整体编排:自动算净生产需求与最迟投料日期(按发酵周期反推), * 状态机 草稿→已下达→执行中→已完成,登记完工进度。 * 写口受 AuthInterceptor default-deny(ADMIN/APPROVER) 保护,前缀已登记入 FINANCE/SENSITIVE_READ。 */ @RestController @RequestMapping("/api/oa/fert-mps") public class FertMpsController { private final FertMpsRepository repo; public FertMpsController(FertMpsRepository repo) { this.repo = repo; } @GetMapping public ApiResp> list(@RequestParam(required = false) String status, @RequestParam(required = false) String period) { if (status != null && !status.isBlank()) return ApiResp.ok(repo.findByStatus(status)); if (period != null && !period.isBlank()) return ApiResp.ok(repo.findByPeriod(period)); return ApiResp.ok(repo.findAll()); } @GetMapping("/{id}") public ApiResp get(@PathVariable Long id) { return ApiResp.ok(find(id)); } public record MpsRequest(String planNo, String period, String productName, String productType, Double demandQty, Double openingStock, Double inProductionQty, Double safetyStock, Integer fermentDays, String planFinishDate, String owner, String remark) { } @PostMapping public ApiResp create(@RequestBody MpsRequest req) { if (req.productName() == null || req.productName().isBlank()) { throw new ApiException(400, "产品名称(productName) 不能为空"); } FertMps m = new FertMps(); m.setPlanNo(req.planNo() == null || req.planNo().isBlank() ? "MPS-" + (repo.count() + 1) : req.planNo().trim()); m.setStatus("草稿"); m.setFinishedQty(BigDecimal.ZERO); m.setCreatedAt(Instant.now()); apply(m, req); return ApiResp.ok(repo.save(m)); } @PutMapping("/{id}") public ApiResp update(@PathVariable Long id, @RequestBody MpsRequest req) { FertMps m = find(id); if ("已完成".equals(m.getStatus())) { throw new ApiException(409, "已完成计划不可编辑"); } apply(m, req); return ApiResp.ok(repo.save(m)); } private void apply(FertMps m, MpsRequest req) { if (req.productName() != null && !req.productName().isBlank()) m.setProductName(req.productName().trim()); m.setPeriod(req.period()); m.setProductType(req.productType()); BigDecimal demand = Money.of(req.demandQty()); BigDecimal opening = Money.of(req.openingStock()); BigDecimal inProd = Money.of(req.inProductionQty()); BigDecimal safety = Money.of(req.safetyStock()); m.setDemandQty(demand); m.setOpeningStock(opening); m.setInProductionQty(inProd); m.setSafetyStock(safety); // 净生产需求 = 需求 + 安全库存 - 期初库存 - 在产,下限 0。 BigDecimal net = demand.add(safety).subtract(opening).subtract(inProd).max(BigDecimal.ZERO); m.setNetProductionQty(net); m.setFermentDays(req.fermentDays()); m.setPlanFinishDate(req.planFinishDate()); m.setLatestChargeDate(backCalcCharge(req.planFinishDate(), req.fermentDays())); m.setOwner(req.owner()); m.setRemark(req.remark()); } /** 最迟投料日期 = 计划完成日 - 发酵周期天数。 */ private static String backCalcCharge(String planFinishDate, Integer fermentDays) { if (planFinishDate == null || planFinishDate.isBlank() || fermentDays == null || fermentDays <= 0) { return null; } try { return LocalDate.parse(planFinishDate.trim()).minusDays(fermentDays).toString(); } catch (DateTimeParseException e) { return null; } } @PostMapping("/{id}/release") public ApiResp release(@PathVariable Long id) { FertMps m = find(id); if (!"草稿".equals(m.getStatus())) { throw new ApiException(409, "仅草稿计划可下达,当前:" + m.getStatus()); } m.setStatus("已下达"); return ApiResp.ok(repo.save(m)); } public record ProgressRequest(Double finishedQty) { } @PostMapping("/{id}/progress") public ApiResp progress(@PathVariable Long id, @RequestBody ProgressRequest req) { FertMps m = find(id); if ("草稿".equals(m.getStatus()) || "已完成".equals(m.getStatus())) { throw new ApiException(409, "计划需已下达/执行中才能登记完工进度,当前:" + m.getStatus()); } BigDecimal add = Money.of(req.finishedQty()); if (add.signum() <= 0) { throw new ApiException(400, "完工量必须大于 0"); } m.setFinishedQty(Money.of(m.getFinishedQty().add(add))); m.setStatus("执行中"); if (m.getNetProductionQty().signum() > 0 && m.getFinishedQty().compareTo(m.getNetProductionQty()) >= 0) { m.setStatus("已完成"); } return ApiResp.ok(repo.save(m)); } @DeleteMapping("/{id}") public ApiResp delete(@PathVariable Long id) { repo.delete(find(id)); return ApiResp.ok(); } private FertMps find(Long id) { return repo.findById(id).orElseThrow(() -> new NotFoundException("主生产计划不存在:" + id)); } }