package com.ruoyi.production.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.production.bean.dto.ProductionCostingQueryDto;
import com.ruoyi.production.bean.vo.ProductionCostingDetailVo;
import com.ruoyi.production.bean.vo.ProductionCostingRawVo;
import com.ruoyi.production.bean.vo.ProductionCostingSummaryVo;
import com.ruoyi.production.mapper.ProductionCostingDeptMapper;
import com.ruoyi.production.mapper.ProductionCostingFactorMapper;
import com.ruoyi.production.mapper.ProductionCostingMapper;
import com.ruoyi.production.mapper.ProductionCostingStandardMapper;
import com.ruoyi.production.mapper.ProductionCostingWorkdayMapper;
import com.ruoyi.production.pojo.ProductionCostingDept;
import com.ruoyi.production.pojo.ProductionCostingFactor;
import com.ruoyi.production.pojo.ProductionCostingStandard;
import com.ruoyi.production.pojo.ProductionCostingWorkday;
import com.ruoyi.production.service.ProductionCostingService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.DayOfWeek;
import java.time.YearMonth;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 生产核算服务实现
*
* 计算规则(严格按需求):
* 白盒部门:折算时长 = 月产量 × 标准数量 × 8
* 成品部门:月产折合小提 = 小提琴 + 大提琴×大提折算率 + 贝斯×贝斯折算率
* 折算时长 = 月产折合小提 ÷ 标准数量 × 8
* 月时间合计 = 折算时长求和
* 折合倍数 = 月时间合计 ÷(当月工作日 × 8)
*/
@Service
@RequiredArgsConstructor
public class ProductionCostingServiceImpl implements ProductionCostingService {
private static final BigDecimal EIGHT = new BigDecimal("8");
private static final BigDecimal ONE = new BigDecimal("1");
private static final String VIOLIN = "小提琴";
private static final String CELLO = "大提琴";
private static final String BASS = "贝斯";
private final ProductionCostingMapper productionCostingMapper;
private final ProductionCostingStandardMapper standardMapper;
private final ProductionCostingFactorMapper factorMapper;
private final ProductionCostingDeptMapper deptMapper;
private final ProductionCostingWorkdayMapper workdayMapper;
@Override
public List detailList(ProductionCostingQueryDto dto) {
ProductionCostingQueryDto query = dto == null ? new ProductionCostingQueryDto() : dto;
List rows = productionCostingMapper.selectCostingRows(query);
// 标准数量:成品(1) 按工序;白盒(2) 按 工序+型号
Map standardByProcess = new HashMap<>();
Map standardByProcessModel = new HashMap<>();
List standardList = standardMapper.selectList(null);
for (ProductionCostingStandard s : standardList) {
if (s.getProcessId() == null || s.getStandardQuantity() == null) {
continue;
}
if (Integer.valueOf(2).equals(s.getDeptType())) {
standardByProcessModel.put(key(s.getProcessId(), s.getProductModelId()), s.getStandardQuantity());
} else {
standardByProcess.put(s.getProcessId(), s.getStandardQuantity());
}
}
// 折算率:工序 + 员工
Map factorMap = new HashMap<>();
for (ProductionCostingFactor f : factorMapper.selectList(null)) {
factorMap.put(key(f.getProcessId(), f.getStaffId()), f);
}
List result = new ArrayList<>();
// ---------- 成品部门:按 员工 + 月份 + 工序 聚合 ----------
Map finishedGroup = new LinkedHashMap<>();
// ---------- 白盒部门:按 员工 + 月份 + 工序 + 型号 聚合 ----------
Map whiteGroup = new LinkedHashMap<>();
for (ProductionCostingRawVo row : rows) {
BigDecimal qty = nz(row.getQuantity());
boolean white = Integer.valueOf(2).equals(row.getDeptType());
if (white) {
String k = key(row.getStaffId(), row.getYearMonth(), row.getProcessId(), row.getProductModelId());
ProductionCostingDetailVo vo = whiteGroup.get(k);
if (vo == null) {
vo = new ProductionCostingDetailVo();
vo.setStaffId(row.getStaffId());
vo.setStaffName(row.getStaffName());
vo.setYearMonth(row.getYearMonth());
vo.setProcessId(row.getProcessId());
vo.setProcessName(row.getProcessName());
vo.setDeptType(row.getDeptType());
vo.setProductModelId(row.getProductModelId());
vo.setProductName(row.getProductName());
vo.setModelName(row.getModelName());
vo.setQuantity(BigDecimal.ZERO);
whiteGroup.put(k, vo);
}
vo.setQuantity(vo.getQuantity().add(qty));
} else {
String k = key(row.getStaffId(), row.getYearMonth(), row.getProcessId());
ProductionCostingDetailVo vo = finishedGroup.get(k);
if (vo == null) {
vo = new ProductionCostingDetailVo();
vo.setStaffId(row.getStaffId());
vo.setStaffName(row.getStaffName());
vo.setYearMonth(row.getYearMonth());
vo.setProcessId(row.getProcessId());
vo.setProcessName(row.getProcessName());
vo.setDeptType(row.getDeptType());
vo.setQuantity(BigDecimal.ZERO);
vo.setViolinQty(BigDecimal.ZERO);
vo.setCelloQty(BigDecimal.ZERO);
vo.setBassQty(BigDecimal.ZERO);
finishedGroup.put(k, vo);
}
vo.setQuantity(vo.getQuantity().add(qty));
if (VIOLIN.equals(row.getProductName())) {
vo.setViolinQty(vo.getViolinQty().add(qty));
} else if (CELLO.equals(row.getProductName())) {
vo.setCelloQty(vo.getCelloQty().add(qty));
} else if (BASS.equals(row.getProductName())) {
vo.setBassQty(vo.getBassQty().add(qty));
} else {
// 非乐器产品默认按小提琴当量 1:1 计入
vo.setViolinQty(vo.getViolinQty().add(qty));
}
}
}
// 成品:折算
for (ProductionCostingDetailVo vo : finishedGroup.values()) {
ProductionCostingFactor factor = factorMap.get(key(vo.getProcessId(), vo.getStaffId()));
BigDecimal violinFactor = factor == null ? ONE : nzDefault(factor.getViolinFactor(), ONE);
BigDecimal celloFactor = factor == null ? ONE : nzDefault(factor.getViolaFactor(), ONE);
BigDecimal bassFactor = factor == null ? ONE : nzDefault(factor.getBassFactor(), ONE);
vo.setViolinFactor(violinFactor);
vo.setCelloFactor(celloFactor);
vo.setBassFactor(bassFactor);
BigDecimal folded = nz(vo.getViolinQty()).multiply(violinFactor)
.add(nz(vo.getCelloQty()).multiply(celloFactor))
.add(nz(vo.getBassQty()).multiply(bassFactor))
.setScale(2, RoundingMode.HALF_UP);
vo.setFoldedQuantity(folded);
BigDecimal standard = standardByProcess.get(vo.getProcessId());
vo.setStandardQuantity(standard);
vo.setConvertedHours(calcFinishedHours(folded, standard));
}
// 白盒:折算时长 = 月产量 × 标准数量 × 8
for (ProductionCostingDetailVo vo : whiteGroup.values()) {
BigDecimal standard = standardByProcessModel.get(key(vo.getProcessId(), vo.getProductModelId()));
vo.setStandardQuantity(standard);
BigDecimal hours = nz(standard).compareTo(BigDecimal.ZERO) <= 0
? BigDecimal.ZERO
: nz(vo.getQuantity()).multiply(standard).multiply(EIGHT);
vo.setConvertedHours(hours.setScale(2, RoundingMode.HALF_UP));
}
result.addAll(finishedGroup.values());
result.addAll(whiteGroup.values());
return result;
}
@Override
public List summary(ProductionCostingQueryDto dto) {
ProductionCostingQueryDto query = dto == null ? new ProductionCostingQueryDto() : dto;
String yearMonth = (query.getYearMonth() == null || query.getYearMonth().isEmpty())
? YearMonth.now().toString() : query.getYearMonth();
query.setYearMonth(yearMonth);
int workDays = resolveWorkDays(yearMonth);
BigDecimal fullHours = new BigDecimal(workDays).multiply(EIGHT);
List details = detailList(query);
Map map = new LinkedHashMap<>();
for (ProductionCostingDetailVo d : details) {
ProductionCostingSummaryVo vo = map.get(d.getStaffId());
if (vo == null) {
vo = new ProductionCostingSummaryVo();
vo.setStaffId(d.getStaffId());
vo.setStaffName(d.getStaffName());
vo.setYearMonth(yearMonth);
vo.setOutput(BigDecimal.ZERO);
vo.setMonthTimeTotal(BigDecimal.ZERO);
vo.setWorkDays(workDays);
vo.setMonthHours(fullHours);
map.put(d.getStaffId(), vo);
}
vo.setOutput(vo.getOutput().add(nz(d.getQuantity())).setScale(2, RoundingMode.HALF_UP));
vo.setMonthTimeTotal(vo.getMonthTimeTotal().add(nz(d.getConvertedHours())).setScale(2, RoundingMode.HALF_UP));
}
List result = new ArrayList<>(map.values());
for (ProductionCostingSummaryVo vo : result) {
BigDecimal divisor = fullHours;
BigDecimal ratio = divisor.compareTo(BigDecimal.ZERO) == 0
? BigDecimal.ZERO
: vo.getMonthTimeTotal().divide(divisor, 2, RoundingMode.HALF_UP);
vo.setConversionRatio(ratio);
}
return result;
}
@Override
public Map workdayInfo(String yearMonth) {
String ym = (yearMonth == null || yearMonth.isEmpty()) ? YearMonth.now().toString() : yearMonth;
ProductionCostingWorkday cfg = findWorkday(ym);
int workDays = (cfg != null && cfg.getWorkDays() != null) ? cfg.getWorkDays() : workDays(ym);
Map map = new HashMap<>();
map.put("yearMonth", ym);
map.put("workDays", workDays);
map.put("dayHours", 8);
map.put("monthHours", new BigDecimal(workDays).multiply(EIGHT).setScale(2, RoundingMode.HALF_UP));
map.put("source", (cfg != null && cfg.getWorkDays() != null) ? "CONFIG" : "DEFAULT");
return map;
}
/* ================= 配置 CRUD ================= */
@Override
public List listDept() {
return deptMapper.selectDeptConfigs();
}
@Override
public boolean saveDept(ProductionCostingDept dept) {
return deptMapper.insert(dept) > 0;
}
@Override
public boolean updateDept(ProductionCostingDept dept) {
return deptMapper.updateById(dept) > 0;
}
@Override
public boolean removeDept(Long id) {
return deptMapper.deleteById(id) > 0;
}
@Override
public List listStandard(ProductionCostingStandard query) {
return standardMapper.selectStandardList(query == null ? new ProductionCostingStandard() : query);
}
@Override
public boolean saveStandard(ProductionCostingStandard standard) {
return standardMapper.insert(standard) > 0;
}
@Override
public boolean updateStandard(ProductionCostingStandard standard) {
return standardMapper.updateById(standard) > 0;
}
@Override
public boolean removeStandard(Long id) {
return standardMapper.deleteById(id) > 0;
}
@Override
public List listFactor(ProductionCostingFactor query) {
return factorMapper.selectFactorList(query == null ? new ProductionCostingFactor() : query);
}
@Override
public boolean saveFactor(ProductionCostingFactor factor) {
return factorMapper.insert(factor) > 0;
}
@Override
public boolean updateFactor(ProductionCostingFactor factor) {
return factorMapper.updateById(factor) > 0;
}
@Override
public boolean removeFactor(Long id) {
return factorMapper.deleteById(id) > 0;
}
@Override
public List listWorkday(ProductionCostingWorkday query) {
return workdayMapper.selectWorkdayList(query == null ? new ProductionCostingWorkday() : query);
}
@Override
public boolean saveWorkday(ProductionCostingWorkday workday) {
return workdayMapper.insert(workday) > 0;
}
@Override
public boolean updateWorkday(ProductionCostingWorkday workday) {
return workdayMapper.updateById(workday) > 0;
}
@Override
public boolean removeWorkday(Long id) {
return workdayMapper.deleteById(id) > 0;
}
/* ================= 工具方法 ================= */
/** 查月度工作日配置 */
private ProductionCostingWorkday findWorkday(String yearMonth) {
if (yearMonth == null || yearMonth.isEmpty()) {
return null;
}
return workdayMapper.selectOne(new LambdaQueryWrapper()
.eq(ProductionCostingWorkday::getYearMonth, yearMonth)
.last("limit 1"));
}
/** 工作日:优先取月度配置,未配置则按周一~周五 */
private int resolveWorkDays(String yearMonth) {
ProductionCostingWorkday cfg = findWorkday(yearMonth);
if (cfg != null && cfg.getWorkDays() != null) {
return cfg.getWorkDays();
}
return workDays(yearMonth);
}
/** 成品折算:折合小提琴 / 标准数量 * 8;标准数量<=0 返回 0 */
private BigDecimal calcFinishedHours(BigDecimal folded, BigDecimal standard) {
if (standard == null || standard.compareTo(BigDecimal.ZERO) <= 0) {
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
}
return folded.divide(standard, 8, RoundingMode.HALF_UP)
.multiply(EIGHT)
.setScale(2, RoundingMode.HALF_UP);
}
private int workDays(String yearMonth) {
try {
YearMonth ym = YearMonth.parse(yearMonth);
int count = 0;
for (int d = 1; d <= ym.lengthOfMonth(); d++) {
DayOfWeek dow = ym.atDay(d).getDayOfWeek();
if (dow != DayOfWeek.SATURDAY && dow != DayOfWeek.SUNDAY) {
count++;
}
}
return count;
} catch (Exception e) {
return 0;
}
}
private BigDecimal nz(BigDecimal v) {
return v == null ? BigDecimal.ZERO : v;
}
private BigDecimal nzDefault(BigDecimal v, BigDecimal def) {
return v == null ? def : v;
}
private String key(Object... parts) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < parts.length; i++) {
if (i > 0) {
sb.append(':');
}
sb.append(parts[i] == null ? "null" : parts[i]);
}
return sb.toString();
}
}