package cn.iocoder.yudao.module.mes.controller.admin.pro.batch;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.mes.controller.admin.pro.bagcode.vo.MesProBagCodeRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchPreGenerateReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchPreGenerateRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchTraceRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.batch.vo.MesProBatchWorkerRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.pallet.vo.MesProPalletRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchWorkerDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO;
import cn.iocoder.yudao.module.mes.framework.qrcode.QrCodeImageUtils;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.service.pro.bagcode.MesProBagCodeService;
import cn.iocoder.yudao.module.mes.service.pro.batch.MesProBatchService;
import cn.iocoder.yudao.module.mes.service.pro.pallet.MesProPalletService;
import cn.iocoder.yudao.module.mes.service.pro.qrexport.MesProQrLabelAssembler;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_BATCH_NOT_EXISTS;
@Tag(name = "管理后台 - MES 生产批次")
@RestController
@RequestMapping("/mes/pro/batch")
@Validated
public class MesProBatchController {
/**
* 批次追溯响应内嵌的袋码列表上限
*
* 这是响应体积的保护,与二维码导出无关:袋码总量由 bagTotal 返回,列表本身只作展示用途。
* 导出路径不再有数量上限。
*/
private static final int MAX_TRACE_BAG_LIST_SIZE = 2000;
@Resource
private MesProBatchService batchService;
@Resource
private AdminUserApi adminUserApi;
@Resource
private MesMdmItemService mdmItemService;
@Resource
private MesProPalletService palletService;
@Resource
private MesProBagCodeService bagCodeService;
@Resource
private MesProQrLabelAssembler labelAssembler;
@PostMapping("/create")
@Operation(summary = "创建生产批次(一次录入业务信息,自动生成批次号)")
@PreAuthorize("@ss.hasPermission('mes:pro-batch:create')")
public CommonResult createBatch(@Valid @RequestBody MesProBatchSaveReqVO createReqVO) {
return success(batchService.createBatch(createReqVO));
}
@PostMapping("/pre-generate")
@Operation(summary = "预生成生产批次二维码(仅生成临时批次码,不录入业务信息)")
@PreAuthorize("@ss.hasPermission('mes:pro-batch:create')")
public CommonResult> preGenerateBatch(
@Valid @RequestBody MesProBatchPreGenerateReqVO preGenerateReqVO) {
List list = batchService.preGenerateBatch(preGenerateReqVO);
return success(BeanUtils.toBean(list, MesProBatchPreGenerateRespVO.class));
}
@PutMapping("/fill-info")
@Operation(summary = "补录生产批次业务信息(生成正式批次号,仅临时批次可用)")
@PreAuthorize("@ss.hasPermission('mes:pro-batch:update')")
public CommonResult fillBatchInfo(@Valid @RequestBody MesProBatchSaveReqVO fillReqVO) {
batchService.fillBatchInfo(fillReqVO);
return success(true);
}
@PutMapping("/update")
@Operation(summary = "更新生产批次")
@PreAuthorize("@ss.hasPermission('mes:pro-batch:update')")
public CommonResult updateBatch(@Valid @RequestBody MesProBatchSaveReqVO updateReqVO) {
batchService.updateBatch(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除生产批次")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('mes:pro-batch:delete')")
public CommonResult deleteBatch(@RequestParam("id") Long id) {
batchService.deleteBatch(id);
return success(true);
}
@PutMapping("/update-status")
@Operation(summary = "更新生产批次状态")
@Parameters({
@Parameter(name = "id", description = "编号", required = true, example = "1024"),
@Parameter(name = "status", description = "目标状态(字典 mes_pro_batch_status)", required = true, example = "20")
})
@PreAuthorize("@ss.hasPermission('mes:pro-batch:update')")
public CommonResult updateBatchStatus(@RequestParam("id") Long id,
@RequestParam("status") Integer status) {
batchService.updateBatchStatus(id, status);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得生产批次详情(含人员配置)")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('mes:pro-batch:query')")
public CommonResult getBatch(@RequestParam("id") Long id) {
MesProBatchDO batch = batchService.getBatch(id);
if (batch == null) {
return success(null);
}
MesProBatchRespVO respVO = buildBatchRespVOList(Collections.singletonList(batch)).get(0);
// 填充人员配置
respVO.setWorkers(buildWorkerRespVOList(batchService.getBatchWorkerListByBatchId(id)));
return success(respVO);
}
@GetMapping("/page")
@Operation(summary = "获得生产批次分页")
@PreAuthorize("@ss.hasPermission('mes:pro-batch:query')")
public CommonResult> getBatchPage(@Valid MesProBatchPageReqVO pageReqVO) {
PageResult pageResult = batchService.getBatchPage(pageReqVO);
return success(new PageResult<>(buildBatchRespVOList(pageResult.getList()), pageResult.getTotal()));
}
@GetMapping("/simple-list")
@Operation(summary = "获得生产批次精简列表(供下拉选择)")
@PreAuthorize("@ss.hasPermission('mes:pro-batch:query')")
public CommonResult> getBatchSimpleList() {
MesProBatchPageReqVO pageReqVO = new MesProBatchPageReqVO();
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List list = batchService.getBatchPage(pageReqVO).getList();
return success(BeanUtils.toBean(list, MesProBatchRespVO.class));
}
@GetMapping("/export-excel")
@Operation(summary = "导出生产批次 Excel")
@PreAuthorize("@ss.hasPermission('mes:pro-batch:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportBatchExcel(@Valid MesProBatchPageReqVO pageReqVO,
HttpServletResponse response) throws IOException {
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List list = batchService.getBatchPage(pageReqVO).getList();
List voList = buildBatchRespVOList(list);
ExcelUtils.write(response, "生产批次.xls", "数据", MesProBatchRespVO.class, voList);
}
@GetMapping("/get-by-code")
@Operation(summary = "扫码追溯:根据批次号(二维码内容)反查批次、人员、托盘、袋码信息")
@Parameter(name = "code", description = "批次号(二维码内容)", required = true, example = "PC20260818L01001")
@PreAuthorize("@ss.hasPermission('mes:pro-batch:query')")
public CommonResult getBatchByCode(@RequestParam("code") String code) {
MesProBatchDO batch = batchService.getBatchByScanCode(code);
if (batch == null) {
throw exception(PRO_BATCH_NOT_EXISTS);
}
return success(buildBatchTraceRespVO(batch));
}
@GetMapping("/export-qr")
@Operation(summary = "批量导出批次二维码图片 ZIP(批次图 + 该批次全部托盘图)")
@Parameter(name = "batchId", description = "生产批次编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('mes:pro-batch:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportBatchQr(@RequestParam("batchId") Long batchId,
HttpServletResponse response) throws IOException {
MesProBatchDO batch = batchService.getBatch(batchId);
if (batch == null) {
throw exception(PRO_BATCH_NOT_EXISTS);
}
List pallets = palletService.getPalletListByBatchId(batchId);
String qrCode = StrUtil.blankToDefault(batch.getCode(), batch.getTempCode());
QrCodeImageUtils.writeZip(response, "批次二维码-" + qrCode, labelAssembler.buildBatchLabels(batch, pallets));
}
// ==================== 拼接 VO ====================
@PostMapping("/generate-business-code")
@Operation(summary = "补生成已有生产批次业务码,已有码值不变")
@PreAuthorize("@ss.hasPermission('mes:pro-batch:create')")
public CommonResult generateBusinessCodes(@Valid @RequestBody
cn.iocoder.yudao.module.mes.controller.admin.pro.vo.MesProBusinessCodeGenerateReqVO reqVO) {
return success(batchService.generateBusinessCodes(reqVO.getIds()));
}
@GetMapping("/export-business-qr")
@Operation(summary = "导出批次及所属托盘业务二维码,内容为纯业务码值")
@PreAuthorize("@ss.hasPermission('mes:pro-batch:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportBusinessQr(@RequestParam("batchId") Long batchId,
HttpServletResponse response) throws IOException {
MesProBatchDO batch = batchService.getBatch(batchId);
if (batch == null) throw exception(PRO_BATCH_NOT_EXISTS);
if (cn.iocoder.yudao.module.mes.enums.pro.MesProBatchStatusEnum.CANCELED.getStatus().equals(batch.getStatus()))
throw new cn.iocoder.yudao.framework.common.exception.ServiceException(400, "已作废批次不可导出业务二维码");
List pallets = palletService.getPalletListByBatchId(batchId).stream()
.filter(record -> !cn.iocoder.yudao.module.mes.enums.pro.MesProPalletStatusEnum.CANCELED.getStatus().equals(record.getStatus())).toList();
QrCodeImageUtils.writeZip(response, "批次业务二维码-" + batch.getBusinessCode(),
labelAssembler.buildBatchBusinessLabels(batch, pallets));
}
private List buildBatchRespVOList(List list) {
if (CollUtil.isEmpty(list)) {
return Collections.emptyList();
}
// 1. 获得关联数据(责任人);品种已下沉到袋码数据包,批次列表不再展示
Map userMap = adminUserApi.getUserMap(
convertSet(list, MesProBatchDO::getOwnerUserId));
Map itemMap = mdmItemService.getItemMap(convertSet(list, MesProBatchDO::getItemId));
return BeanUtils.toBean(list, MesProBatchRespVO.class, vo -> {
MapUtils.findAndThen(userMap, vo.getOwnerUserId(), user -> vo.setOwnerUserName(user.getNickname()));
MapUtils.findAndThen(itemMap, vo.getItemId(), item -> { vo.setItemCode(item.getCode()); vo.setItemName(item.getName()); });
});
}
private List buildWorkerRespVOList(List workers) {
if (CollUtil.isEmpty(workers)) {
return Collections.emptyList();
}
Set userIds = new HashSet<>(convertList(workers, MesProBatchWorkerDO::getUserId));
Map userMap = adminUserApi.getUserMap(userIds);
return BeanUtils.toBean(workers, MesProBatchWorkerRespVO.class, vo ->
MapUtils.findAndThen(userMap, vo.getUserId(), user -> vo.setUserName(user.getNickname())));
}
private MesProBatchTraceRespVO buildBatchTraceRespVO(MesProBatchDO batch) {
MesProBatchTraceRespVO respVO = BeanUtils.toBean(batch, MesProBatchTraceRespVO.class);
// 品种信息
if (batch.getItemId() != null) {
MesMdmItemDO item = mdmItemService.getItem(batch.getItemId());
if (item != null) {
respVO.setItemCode(item.getCode())
.setItemName(item.getName())
.setItemSpecification(item.getSpecification());
}
}
// 责任人
if (batch.getOwnerUserId() != null) {
AdminUserRespDTO user = adminUserApi.getUser(batch.getOwnerUserId());
if (user != null) {
respVO.setOwnerUserName(user.getNickname());
}
}
// 人员配置
respVO.setWorkers(buildWorkerRespVOList(batchService.getBatchWorkerListByBatchId(batch.getId())));
// 托盘列表
respVO.setPallets(BeanUtils.toBean(palletService.getPalletListByBatchId(batch.getId()), MesProPalletRespVO.class));
// 袋码列表(封顶 MAX_TRACE_BAG_LIST_SIZE,bagTotal 记录总数)
List bags = bagCodeService.getBagCodeListByBatchId(batch.getId());
respVO.setPendingBagQuantity((int) bags.stream().filter(b -> !Integer.valueOf(30).equals(b.getStatus()) && b.getInboundEventId() == null).count());
respVO.setBagTotal(bags.size());
if (bags.size() > MAX_TRACE_BAG_LIST_SIZE) {
bags = bags.subList(0, MAX_TRACE_BAG_LIST_SIZE);
}
respVO.setBags(buildBagCodeRespVOList(bags));
return respVO;
}
private List buildBagCodeRespVOList(List list) {
if (CollUtil.isEmpty(list)) {
return Collections.emptyList();
}
// 获得批次的产线信息,拼接袋码的产线编码/名称
Map batchMap = convertMap(
batchService.getBatchList(convertSet(list, MesProBagCodeDO::getBatchId)), MesProBatchDO::getId);
return BeanUtils.toBean(list, MesProBagCodeRespVO.class, vo ->
MapUtils.findAndThen(batchMap, vo.getBatchId(), batch ->
vo.setLineCode(batch.getLineCode()).setLineName(batch.getLineName())));
}
}