修复导入Excel功能工程号自增; 添加选择工程号自动填写玻璃id列表
| | |
| | | package com.mes.device.controller; |
| | | |
| | | import com.mes.device.entity.EngineeringSequence; |
| | | import com.mes.device.entity.GlassInfo; |
| | | import com.mes.device.service.EngineeringSequenceService; |
| | | import com.mes.device.service.GlassInfoService; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.http.HttpStatus; |
| | | import org.springframework.http.ResponseEntity; |
| | | import org.springframework.util.CollectionUtils; |
| | | import org.springframework.web.bind.annotation.PostMapping; |
| | | import org.springframework.web.bind.annotation.RequestBody; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | import org.springframework.web.bind.annotation.*; |
| | | import org.springframework.web.client.RestTemplate; |
| | | |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.stream.Collectors; |
| | | |
| | | /** |
| | | * @author :huang |
| | |
| | | public class GlassInfoImportController { |
| | | |
| | | private final GlassInfoService glassInfoService; |
| | | private final EngineeringSequenceService engineeringSequenceService; |
| | | private final RestTemplate restTemplate = new RestTemplate(); |
| | | |
| | | |
| | |
| | | Map<String, Object> payload = glassInfoService.buildEngineerImportPayload(excelRows); |
| | | log.info("构建的 MES 导入数据: {}", payload); |
| | | |
| | | // 从payload中提取工程号(payload中使用的是engineerId) |
| | | String engineeringId = (String) payload.get("engineerId"); |
| | | if (engineeringId == null || engineeringId.isEmpty()) { |
| | | // 如果payload中没有engineerId,尝试从glassInfolList中获取 |
| | | @SuppressWarnings("unchecked") |
| | | List<Map<String, Object>> glassInfoList = (List<Map<String, Object>>) payload.get("glassInfolList"); |
| | | if (glassInfoList != null && !glassInfoList.isEmpty()) { |
| | | Object firstEngineerId = glassInfoList.get(0).get("engineerId"); |
| | | if (firstEngineerId != null) { |
| | | engineeringId = firstEngineerId.toString(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | String mesEngineeringImportUrl = glassInfoService.getMesEngineeringImportUrl(); |
| | | |
| | | try { |
| | | ResponseEntity<Map> mesResp = restTemplate.postForEntity(mesEngineeringImportUrl, payload, Map.class); |
| | | Map<String, Object> mesBody = mesResp.getBody(); |
| | | |
| | | // 检查MES响应是否真正成功(不仅检查HTTP状态码,还要检查响应体中的code字段) |
| | | boolean mesSuccess = false; |
| | | if (mesResp.getStatusCode().is2xxSuccessful() && mesBody != null) { |
| | | Object codeObj = mesBody.get("code"); |
| | | if (codeObj != null) { |
| | | int code = codeObj instanceof Number ? ((Number) codeObj).intValue() : |
| | | Integer.parseInt(String.valueOf(codeObj)); |
| | | // MES成功通常返回code=200或0 |
| | | mesSuccess = (code == 200 || code == 0); |
| | | } else { |
| | | // 如果没有code字段,认为HTTP 2xx就是成功 |
| | | mesSuccess = true; |
| | | } |
| | | } |
| | | |
| | | // 只有MES导入真正成功时,才保存玻璃信息到本地数据库,并关联engineering_id |
| | | if (mesSuccess && engineeringId != null) { |
| | | try { |
| | | glassInfoService.saveGlassInfosFromExcel(excelRows, engineeringId); |
| | | log.info("MES导入成功,已保存玻璃信息到本地数据库,工程号: {}", engineeringId); |
| | | } catch (Exception e) { |
| | | log.error("MES导入成功,但保存玻璃信息到本地数据库失败: engineeringId={}", engineeringId, e); |
| | | // 即使保存失败,也返回MES的成功响应,但记录错误日志 |
| | | } |
| | | } else { |
| | | log.warn("MES导入未成功,不保存玻璃信息到本地数据库: engineeringId={}, mesSuccess={}", engineeringId, mesSuccess); |
| | | } |
| | | |
| | | // 直接返回 MES 的响应,让前端根据响应体中的 code 字段判断是否成功 |
| | | return ResponseEntity.status(mesResp.getStatusCode()).body(mesResp.getBody()); |
| | | return ResponseEntity.status(mesResp.getStatusCode()).body(mesBody); |
| | | } catch (org.springframework.web.client.ResourceAccessException e) { |
| | | // 连接超时或无法连接 |
| | | log.error("转发 MES 导入接口失败(连接问题) url={}, error={}", mesEngineeringImportUrl, e.getMessage(), e); |
| | |
| | | return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 查询所有工程号列表 |
| | | */ |
| | | @GetMapping("/engineering/list") |
| | | public ResponseEntity<?> getEngineeringList() { |
| | | try { |
| | | List<EngineeringSequence> list = engineeringSequenceService.list(); |
| | | List<Map<String, Object>> result = list.stream() |
| | | .map(seq -> { |
| | | Map<String, Object> map = new HashMap<>(); |
| | | map.put("engineeringId", seq.getEngineeringId()); |
| | | map.put("date", seq.getDate()); |
| | | map.put("sequence", seq.getSequence()); |
| | | return map; |
| | | }) |
| | | .collect(Collectors.toList()); |
| | | return ResponseEntity.ok(result); |
| | | } catch (Exception e) { |
| | | log.error("查询工程号列表失败", e); |
| | | Map<String, Object> errorResponse = new HashMap<>(); |
| | | errorResponse.put("code", 500); |
| | | errorResponse.put("message", "查询工程号列表失败: " + e.getMessage()); |
| | | return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 根据工程号查询对应的玻璃ID列表 |
| | | */ |
| | | @GetMapping("/engineering/{engineeringId}/glassIds") |
| | | public ResponseEntity<?> getGlassIdsByEngineeringId(@PathVariable String engineeringId) { |
| | | try { |
| | | List<GlassInfo> glassInfos = glassInfoService.getGlassInfosByEngineeringId(engineeringId); |
| | | List<String> glassIds = glassInfos.stream() |
| | | .map(GlassInfo::getGlassId) |
| | | .filter(id -> id != null && !id.trim().isEmpty()) |
| | | .collect(Collectors.toList()); |
| | | |
| | | Map<String, Object> result = new HashMap<>(); |
| | | result.put("engineeringId", engineeringId); |
| | | result.put("glassIds", glassIds); |
| | | result.put("count", glassIds.size()); |
| | | |
| | | return ResponseEntity.ok(result); |
| | | } catch (Exception e) { |
| | | log.error("根据工程号查询玻璃ID列表失败: engineeringId={}", engineeringId, e); |
| | | Map<String, Object> errorResponse = new HashMap<>(); |
| | | errorResponse.put("code", 500); |
| | | errorResponse.put("message", "查询玻璃ID列表失败: " + e.getMessage()); |
| | | return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 删除工程号及其关联的玻璃信息 |
| | | */ |
| | | @DeleteMapping("/engineering/{engineeringId}") |
| | | public ResponseEntity<?> deleteEngineering(@PathVariable String engineeringId) { |
| | | try { |
| | | // 1. 删除glass_info表中对应工程号的玻璃信息 |
| | | int deletedGlassCount = glassInfoService.deleteGlassInfosByEngineeringId(engineeringId); |
| | | |
| | | // 2. 删除engineering_sequence表中的工程号记录(逻辑删除) |
| | | EngineeringSequence sequence = engineeringSequenceService.getOne( |
| | | new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<EngineeringSequence>() |
| | | .eq(EngineeringSequence::getEngineeringId, engineeringId) |
| | | .eq(EngineeringSequence::getIsDeleted, 0) // 只查询未删除的记录 |
| | | .last("LIMIT 1") |
| | | ); |
| | | |
| | | boolean deletedSequence = false; |
| | | if (sequence != null) { |
| | | // removeById 会自动使用逻辑删除(因为实体类有 @TableLogic 注解) |
| | | deletedSequence = engineeringSequenceService.removeById(sequence.getId()); |
| | | log.info("已删除工程号记录: engineeringId={}, sequenceId={}", engineeringId, sequence.getId()); |
| | | } else { |
| | | log.warn("未找到工程号记录: engineeringId={}", engineeringId); |
| | | } |
| | | |
| | | Map<String, Object> result = new HashMap<>(); |
| | | result.put("engineeringId", engineeringId); |
| | | result.put("deletedGlassCount", deletedGlassCount); |
| | | result.put("deletedSequence", deletedSequence); |
| | | result.put("success", true); |
| | | result.put("message", String.format("已删除工程号 %s,共删除 %d 条玻璃信息", engineeringId, deletedGlassCount)); |
| | | |
| | | log.info("删除工程号成功: engineeringId={}, deletedGlassCount={}, deletedSequence={}", |
| | | engineeringId, deletedGlassCount, deletedSequence); |
| | | |
| | | return ResponseEntity.ok(result); |
| | | } catch (Exception e) { |
| | | log.error("删除工程号失败: engineeringId={}", engineeringId, e); |
| | | Map<String, Object> errorResponse = new HashMap<>(); |
| | | errorResponse.put("code", 500); |
| | | errorResponse.put("message", "删除工程号失败: " + e.getMessage()); |
| | | errorResponse.put("success", false); |
| | | return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); |
| | | } |
| | | } |
| | | } |
| | | |
| | | |
| | |
| | | @TableField("work_line") |
| | | private Integer workLine; |
| | | |
| | | @ApiModelProperty(value = "工程号(关联 engineering_sequence 表)", example = "P25010801") |
| | | @TableField("engineering_id") |
| | | private String engineeringId; |
| | | |
| | | @ApiModelProperty(value = "状态:0-初始保存, 1-已扫码交互", example = "0") |
| | | @TableField("state") |
| | | private Integer state; |
| | | |
| | | @ApiModelProperty(value = "创建时间") |
| | | @TableField(value = "created_time", fill = FieldFill.INSERT) |
| | | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
| | |
| | | */ |
| | | @Select("SELECT * FROM glass_info WHERE status = #{status} AND is_deleted = 0 ORDER BY created_time DESC") |
| | | List<GlassInfo> selectByStatus(@Param("status") String status); |
| | | |
| | | /** |
| | | * 根据工程号查询玻璃ID列表 |
| | | * |
| | | * @param engineeringId 工程号 |
| | | * @return 玻璃信息列表 |
| | | */ |
| | | @Select("SELECT * FROM glass_info WHERE engineering_id = #{engineeringId} AND is_deleted = 0") |
| | | List<GlassInfo> selectByEngineeringId(@Param("engineeringId") String engineeringId); |
| | | } |
| | | |
| | |
| | | * @param date 日期 |
| | | * @return 最大序号,如果没有记录返回0 |
| | | */ |
| | | @Select("SELECT COALESCE(MAX(sequence), 0) FROM engineering_sequence WHERE date = #{date} AND is_deleted = 0") |
| | | @Select("SELECT COALESCE(MAX(sequence), 0) FROM engineering_sequence WHERE DATE(date) = DATE(#{date})") |
| | | Integer selectMaxSequenceByDate(@Param("date") Date date); |
| | | |
| | | /** |
| | | * 查询指定日期的最大序号并加行锁,避免并发生成重复序号 |
| | | * |
| | | * @param date 日期 |
| | | * @return 最大序号,如果没有记录返回0 |
| | | */ |
| | | @Select("SELECT COALESCE(MAX(sequence), 0) FROM engineering_sequence WHERE date = #{date} AND is_deleted = 0 FOR UPDATE") |
| | | Integer selectMaxSequenceByDateForUpdate(@Param("date") Date date); |
| | | |
| | | /** |
| | | * 根据工程号查询工程序号信息 |
| | |
| | | * @return 符合 MES 接口要求的请求体 Map |
| | | */ |
| | | Map<String, Object> buildEngineerImportPayload(List<Map<String, Object>> excelRows); |
| | | |
| | | /** |
| | | * 根据工程号查询玻璃信息列表 |
| | | * |
| | | * @param engineeringId 工程号 |
| | | * @return 玻璃信息列表 |
| | | */ |
| | | List<GlassInfo> getGlassInfosByEngineeringId(String engineeringId); |
| | | |
| | | /** |
| | | * 从Excel数据保存玻璃信息到本地数据库,并关联engineering_id |
| | | * |
| | | * @param excelRows Excel行数据 |
| | | * @param engineeringId 工程号 |
| | | */ |
| | | void saveGlassInfosFromExcel(List<Map<String, Object>> excelRows, String engineeringId); |
| | | |
| | | /** |
| | | * 扫码交互后更新玻璃信息状态(将state从0改为1) |
| | | * |
| | | * @param glassId 玻璃ID |
| | | * @param width 宽度(可选) |
| | | * @param height 高度(可选) |
| | | * @param workLine 产线编号(可选) |
| | | * @return 是否更新成功 |
| | | */ |
| | | boolean updateGlassStateAfterScan(String glassId, Integer width, Integer height, Integer workLine); |
| | | |
| | | /** |
| | | * 根据工程号删除玻璃信息 |
| | | * |
| | | * @param engineeringId 工程号 |
| | | * @return 删除的玻璃数量 |
| | | */ |
| | | int deleteGlassInfosByEngineeringId(String engineeringId); |
| | | } |
| | | |
| | |
| | | |
| | | /** |
| | | * 工程序号信息服务实现类 |
| | | * |
| | | * |
| | | * @author mes |
| | | * @since 2024-11-20 |
| | | * @since 2025-11-20 |
| | | */ |
| | | @Slf4j |
| | | @Service |
| | | public class EngineeringSequenceServiceImpl extends ServiceImpl<EngineeringSequenceMapper, EngineeringSequence> implements EngineeringSequenceService { |
| | | |
| | | // 日期格式化器(线程不安全,使用ThreadLocal保证线程安全) |
| | | private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyMMdd"); |
| | | // 修复:使用ThreadLocal保证DateTimeFormatter的线程安全 |
| | | private static final ThreadLocal<DateTimeFormatter> DATE_FORMATTER_THREAD_LOCAL = ThreadLocal.withInitial( |
| | | () -> DateTimeFormatter.ofPattern("yyMMdd") |
| | | ); |
| | | |
| | | // 重试间隔(毫秒),使用随机数避免并发请求同时重试 |
| | | private static final int RETRY_INTERVAL_MIN = 50; |
| | | private static final int RETRY_INTERVAL_MAX = 200; |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public String generateAndSaveEngineeringId(Date date) { |
| | | // 乐观重试,防止并发写入造成重复键 |
| | | int retry = 0; |
| | | final int maxRetry = 5; |
| | | while (true) { |
| | | try { |
| | | // 1. 查询当天最大序号,并加行锁避免并发重复 |
| | | Integer maxSequence = baseMapper.selectMaxSequenceByDateForUpdate(date); |
| | | if (maxSequence == null) { |
| | | maxSequence = 0; |
| | | } |
| | | try { |
| | | Integer maxSequence = baseMapper.selectMaxSequenceByDate(date); |
| | | maxSequence = (maxSequence == null) ? 0 : maxSequence; |
| | | int newSequence = maxSequence + 1; |
| | | |
| | | // 2. 序号自增1 |
| | | int newSequence = maxSequence + 1; |
| | | LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); |
| | | String dateStr = DATE_FORMATTER_THREAD_LOCAL.get().format(localDate); |
| | | String engineeringId = "P" + dateStr + String.format("%02d", newSequence); |
| | | |
| | | // 3. 生成工程号:P + yyMMdd + 两位序号 |
| | | LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); |
| | | String dateStr = localDate.format(DATE_FORMATTER); |
| | | String engineeringId = "P" + dateStr + String.format("%02d", newSequence); |
| | | EngineeringSequence engineeringSequence = new EngineeringSequence(); |
| | | engineeringSequence.setEngineeringId(engineeringId); |
| | | engineeringSequence.setDate(date); |
| | | engineeringSequence.setSequence(newSequence); |
| | | engineeringSequence.setCreatedTime(new Date()); |
| | | engineeringSequence.setUpdatedTime(new Date()); |
| | | engineeringSequence.setCreatedBy("system"); |
| | | engineeringSequence.setUpdatedBy("system"); |
| | | |
| | | // 4. 保存到数据库 |
| | | EngineeringSequence engineeringSequence = new EngineeringSequence(); |
| | | engineeringSequence.setEngineeringId(engineeringId); |
| | | engineeringSequence.setDate(date); |
| | | engineeringSequence.setSequence(newSequence); |
| | | engineeringSequence.setCreatedTime(new Date()); |
| | | engineeringSequence.setUpdatedTime(new Date()); |
| | | engineeringSequence.setCreatedBy("system"); |
| | | engineeringSequence.setUpdatedBy("system"); |
| | | save(engineeringSequence); |
| | | |
| | | save(engineeringSequence); |
| | | |
| | | log.info("生成工程号成功: engineeringId={}, date={}, sequence={}", engineeringId, date, newSequence); |
| | | return engineeringId; |
| | | |
| | | } catch (DuplicateKeyException dup) { |
| | | // 并发导致的唯一键冲突,重试 |
| | | if (++retry > maxRetry) { |
| | | log.error("生成工程号重试超过上限, date={}", date, dup); |
| | | throw new RuntimeException("生成工程号失败", dup); |
| | | } |
| | | log.warn("工程号生成发生并发冲突,准备重试,第{}次,date={}", retry, date); |
| | | } catch (Exception e) { |
| | | log.error("生成工程号失败, date={}", date, e); |
| | | throw new RuntimeException("生成工程号失败", e); |
| | | } |
| | | log.info("生成工程号成功: engineeringId={}, date={}, sequence={}", engineeringId, date, newSequence); |
| | | return engineeringId; |
| | | } catch (DuplicateKeyException dup) { |
| | | log.error("生成工程号唯一键冲突: date={}", date, dup); |
| | | throw new RuntimeException("生成工程号失败", dup); |
| | | } catch (Exception e) { |
| | | log.error("生成工程号失败, date={}", date, e); |
| | | throw new RuntimeException("生成工程号失败", e); |
| | | } |
| | | } |
| | | } |
| | | |
| | | } |
| | |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.util.CollectionUtils; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.util.*; |
| | | import java.util.stream.Collectors; |
| | | |
| | |
| | | return result; |
| | | } |
| | | |
| | | // 工程号生成:使用数据库自增序号,避免重复 |
| | | // 工程号生成:每次导入都生成新的工程号(使用数据库自增序号,避免重复) |
| | | final String engineerId = engineeringSequenceService.generateAndSaveEngineeringId(new Date()); |
| | | final String filmsIdDefault = firstValue(excelRows, "filmsId", "白玻"); |
| | | final double thicknessDefault = parseDouble(firstValue(excelRows, "thickness"), 0d); |
| | |
| | | |
| | | List<Map<String, Object>> glassInfolList = excelRows.stream() |
| | | .flatMap(row -> { |
| | | int qty = (int) parseDouble(row.getOrDefault("quantity", 1), 1); |
| | | if (qty <= 0) qty = 1; |
| | | Object qtyObj = row.getOrDefault("quantity", 1); |
| | | int qty = parseDouble(qtyObj, 1) > 0 ? (int) parseDouble(qtyObj, 1) : 1; |
| | | |
| | | String glassId = str(row.get("glassId")); |
| | | String filmsId = strOrDefault(row.get("filmsId"), filmsIdDefaultFinal); |
| | | String flowCardId = str(row.get("flowCardId")); |
| | |
| | | double thickness = parseDouble(row.get("thickness"), thicknessDefaultFinal); |
| | | |
| | | int finalQty = qty; |
| | | log.info("解析到数量:row={}, quantity={}, 最终qty={}", row, qtyObj, finalQty); |
| | | return range(0, qty).mapToObj(idx -> { |
| | | String finalGlassId = finalQty > 1 ? glassId + "_" + (idx + 1) : glassId; |
| | | String finalFlowCardId = flowCardId.isEmpty() ? finalGlassId : flowCardId; |
| | | String baseGlassId = engineerIdFinal + glassId; |
| | | String finalGlassId = finalQty > 1 ? baseGlassId + "_" + (idx + 1) : baseGlassId; |
| | | |
| | | String baseFlowCardId = flowCardId.isEmpty() ? baseGlassId : flowCardId; |
| | | String finalFlowCardSequence = baseFlowCardId + "/" + (idx + 1); |
| | | String finalFlowCardId = baseFlowCardId; |
| | | |
| | | Map<String, Object> m = new HashMap<>(); |
| | | m.put("xAxis", 0); |
| | | m.put("xCoordinate", 0); |
| | |
| | | m.put("combine", 0); |
| | | m.put("markIcon", ""); |
| | | m.put("filmRemove", 0); |
| | | m.put("flowCardSequence", flowCardId + "/" + (idx + 1)); |
| | | m.put("flowCardSequence", finalFlowCardSequence); |
| | | m.put("process", ""); |
| | | m.put("rawAngle", 0); |
| | | m.put("graphNo", 0); |
| | |
| | | return Math.round(v * 100.0) / 100.0; |
| | | } |
| | | |
| | | @Override |
| | | public List<GlassInfo> getGlassInfosByEngineeringId(String engineeringId) { |
| | | if (engineeringId == null || engineeringId.trim().isEmpty()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | try { |
| | | return baseMapper.selectByEngineeringId(engineeringId.trim()); |
| | | } catch (Exception e) { |
| | | log.error("根据工程号查询玻璃信息失败, engineeringId={}", engineeringId, e); |
| | | return Collections.emptyList(); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public void saveGlassInfosFromExcel(List<Map<String, Object>> excelRows, String engineeringId) { |
| | | if (excelRows == null || excelRows.isEmpty() || engineeringId == null || engineeringId.trim().isEmpty()) { |
| | | return; |
| | | } |
| | | |
| | | List<GlassInfo> glassInfos = new ArrayList<>(); |
| | | Date now = new Date(); |
| | | |
| | | for (Map<String, Object> row : excelRows) { |
| | | String glassId = str(row.get("glassId")); |
| | | if (glassId == null || glassId.trim().isEmpty()) { |
| | | continue; |
| | | } |
| | | |
| | | int qty = (int) parseDouble(row.getOrDefault("quantity", 1), 1); |
| | | if (qty <= 0) qty = 1; |
| | | |
| | | double width = parseDouble(row.get("width"), 0d); |
| | | double height = parseDouble(row.get("height"), 0d); |
| | | double thickness = parseDouble(row.get("thickness"), 0d); |
| | | |
| | | // 与导入规则保持一致:glassId 前加工程号前缀,数量>1时追加序号 |
| | | String baseGlassId = engineeringId.trim() + glassId; |
| | | for (int idx = 0; idx < qty; idx++) { |
| | | String finalGlassId = qty > 1 ? baseGlassId + "_" + (idx + 1) : baseGlassId; |
| | | |
| | | GlassInfo glassInfo = new GlassInfo(); |
| | | glassInfo.setGlassId(finalGlassId); |
| | | glassInfo.setEngineeringId(engineeringId.trim()); |
| | | glassInfo.setGlassLength((int) Math.round(height)); |
| | | glassInfo.setGlassWidth((int) Math.round(width)); |
| | | glassInfo.setGlassThickness(BigDecimal.valueOf(thickness)); |
| | | glassInfo.setStatus(GlassInfo.Status.ACTIVE); |
| | | glassInfo.setState(0); |
| | | glassInfo.setCreatedTime(now); |
| | | glassInfo.setUpdatedTime(now); |
| | | glassInfo.setCreatedBy("system"); |
| | | glassInfo.setUpdatedBy("system"); |
| | | |
| | | glassInfos.add(glassInfo); |
| | | } |
| | | } |
| | | |
| | | if (!glassInfos.isEmpty()) { |
| | | batchSaveOrUpdateGlassInfo(glassInfos); |
| | | log.info("已保存 {} 条玻璃信息到本地数据库,工程号: {}", glassInfos.size(), engineeringId); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public boolean updateGlassStateAfterScan(String glassId, Integer width, Integer height, Integer workLine) { |
| | | if (glassId == null || glassId.trim().isEmpty()) { |
| | | return false; |
| | | } |
| | | |
| | | try { |
| | | // 查询已存在的玻璃信息 |
| | | GlassInfo existing = baseMapper.selectByGlassId(glassId.trim()); |
| | | if (existing == null) { |
| | | log.debug("玻璃信息不存在,无法更新状态: glassId={}", glassId); |
| | | return false; |
| | | } |
| | | |
| | | // 更新状态为1(已扫码交互) |
| | | LambdaUpdateWrapper<GlassInfo> wrapper = new LambdaUpdateWrapper<>(); |
| | | wrapper.eq(GlassInfo::getGlassId, glassId.trim()) |
| | | .eq(GlassInfo::getIsDeleted, 0) |
| | | .set(GlassInfo::getState, 1) |
| | | .set(GlassInfo::getUpdatedTime, new Date()) |
| | | .set(GlassInfo::getUpdatedBy, "system"); |
| | | |
| | | // 如果提供了尺寸信息,也更新尺寸 |
| | | if (width != null) { |
| | | wrapper.set(GlassInfo::getGlassWidth, width); |
| | | } |
| | | if (height != null) { |
| | | wrapper.set(GlassInfo::getGlassLength, height); |
| | | } |
| | | if (workLine != null) { |
| | | wrapper.set(GlassInfo::getWorkLine, workLine); |
| | | } |
| | | |
| | | boolean updated = this.update(wrapper); |
| | | if (updated) { |
| | | log.info("已更新玻璃信息状态为已扫码交互: glassId={}, state=1", glassId); |
| | | } |
| | | return updated; |
| | | } catch (Exception e) { |
| | | log.error("更新玻璃信息状态失败: glassId={}", glassId, e); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public int deleteGlassInfosByEngineeringId(String engineeringId) { |
| | | if (engineeringId == null || engineeringId.trim().isEmpty()) { |
| | | return 0; |
| | | } |
| | | |
| | | try { |
| | | // 先查询要删除的数量(删除前) |
| | | LambdaQueryWrapper<GlassInfo> countWrapper = new LambdaQueryWrapper<>(); |
| | | countWrapper.eq(GlassInfo::getEngineeringId, engineeringId.trim()) |
| | | .eq(GlassInfo::getIsDeleted, 0); // 查询未删除的记录 |
| | | long count = this.count(countWrapper); |
| | | |
| | | // 使用MyBatis-Plus的remove方法,会根据@TableLogic自动进行逻辑删除 |
| | | LambdaQueryWrapper<GlassInfo> removeWrapper = new LambdaQueryWrapper<>(); |
| | | removeWrapper.eq(GlassInfo::getEngineeringId, engineeringId.trim()) |
| | | .eq(GlassInfo::getIsDeleted, 0); // 只删除未删除的记录 |
| | | |
| | | boolean result = this.remove(removeWrapper); |
| | | if (result) { |
| | | log.info("已删除工程号下的玻璃信息: engineeringId={}, count={}", engineeringId, count); |
| | | return (int) count; |
| | | } |
| | | return 0; |
| | | } catch (Exception e) { |
| | | log.error("删除工程号下的玻璃信息失败: engineeringId={}", engineeringId, e); |
| | | return 0; |
| | | } |
| | | } |
| | | |
| | | } |
| | | |
| | |
| | | // 4. 清空plcRequest和plcGlassId(只清除PLC字段) |
| | | clearPlcRequestFields(deviceConfig, serializer); |
| | | |
| | | // 5. 更新玻璃信息状态:将state从0改为1(已扫码交互) |
| | | boolean updated = glassInfoService.updateGlassStateAfterScan(glassId, rawWidth, rawHeight, workLine); |
| | | if (!updated) { |
| | | log.warn("更新玻璃信息状态失败,玻璃可能不存在: glassId={}", glassId); |
| | | // 不返回错误,继续执行,因为可能是新玻璃还未导入 |
| | | } |
| | | |
| | | // 6. 将扫描到的玻璃ID保存到共享数据中(供大车设备定时器读取) |
| | | saveScannedGlassId(params, glassId); |
| | | |
| | | // 5. 保存玻璃信息到数据库 |
| | | GlassInfo glassInfo = buildGlassInfo(glassId, rawWidth, rawHeight, workLine); |
| | | boolean saved = glassInfoService.saveOrUpdateGlassInfo(glassInfo); |
| | | if (!saved) { |
| | | return buildResult(deviceConfig, "scanOnce", false, "保存玻璃信息失败: " + glassId, null); |
| | | } |
| | | |
| | | // 6. 将扫描到的玻璃ID保存到共享数据中(供大车设备定时器读取) |
| | | saveScannedGlassId(params, glassId); |
| | | |
| | | String msg = String.format("玻璃[%s] 尺寸[表宽:%s x 长:%s] 已接收并入库,workLine=%s", |
| | | glassId, |
| | | rawWidth != null ? rawWidth + "mm" : "-", |
| | | rawHeight != null ? rawHeight + "mm" : "-", |
| | | workLine != null ? workLine : "-"); |
| | | Map<String, Object> resultData = new HashMap<>(); |
| | | resultData.put("glassIds", Collections.singletonList(glassId)); |
| | | if (workLine != null) { |
| | | resultData.put("workLine", workLine); |
| | | } |
| | | return buildResult(deviceConfig, "scanOnce", true, msg, resultData); |
| | | Integer intervalMs = config != null ? config.getScanIntervalMs() : null; |
| | | String msg = String.format("玻璃[%s] 尺寸[表宽:%s x 长:%s] 已接收,workLine=%s,扫描间隔=%s", |
| | | glassId, |
| | | rawWidth != null ? rawWidth + "mm" : "-", |
| | | rawHeight != null ? rawHeight + "mm" : "-", |
| | | workLine != null ? workLine : "-", |
| | | intervalMs != null ? intervalMs + "ms" : "-"); |
| | | Map<String, Object> resultData = new HashMap<>(); |
| | | resultData.put("glassIds", Collections.singletonList(glassId)); |
| | | if (workLine != null) { |
| | | resultData.put("workLine", workLine); |
| | | } |
| | | return buildResult(deviceConfig, "scanOnce", true, msg, resultData); |
| | | } |
| | | |
| | | /** |
| | |
| | | glassInfo.setGlassId(glassId.trim()); |
| | | // mesWidth=表宽 -> glassWidth, mesHeight=长 -> glassLength |
| | | if (width != null) { |
| | | glassInfo.setGlassWidth(width); // 表宽 |
| | | glassInfo.setGlassWidth(width); |
| | | } |
| | | if (height != null) { |
| | | glassInfo.setGlassLength(height); // 长 |
| | | glassInfo.setGlassLength(height); |
| | | } |
| | | glassInfo.setStatus(GlassInfo.Status.PENDING); |
| | | if (workLine != null) { |
| | |
| | | // 从配置中获取workLine,用于过滤(配置中是Integer类型) |
| | | Integer workLine = getLogicParam(logicParams, "workLine", null); |
| | | |
| | | // 查询最近2分钟内的玻璃记录(扩大时间窗口,确保不遗漏) |
| | | Date twoMinutesAgo = new Date(System.currentTimeMillis() - 120000); |
| | | |
| | | // 查询state=1的玻璃记录(已扫码交互完成,等待卧转立处理) |
| | | LambdaQueryWrapper<GlassInfo> wrapper = new LambdaQueryWrapper<>(); |
| | | wrapper.in(GlassInfo::getStatus, GlassInfo.Status.PENDING, GlassInfo.Status.ACTIVE) |
| | | .ge(GlassInfo::getCreatedTime, twoMinutesAgo) |
| | | .eq(GlassInfo::getState, 1) // 只查询state=1的玻璃(已扫码完成) |
| | | .orderByDesc(GlassInfo::getCreatedTime) |
| | | .last("LIMIT 20"); // 限制查询数量,避免过多 |
| | | |
| | |
| | | // 写入玻璃数量 |
| | | payload.put("plcGlassCount", count); |
| | | |
| | | // 写入卧转立编号(优先从任务参数获取,其次从设备配置获取) |
| | | Integer inPosition = null; |
| | | // 写入卧转立编号(优先从任务参数获取,其次从设备配置获取,直接写入编号,不进行位置映射) |
| | | Object inPosition = null; |
| | | if (params != null) { |
| | | try { |
| | | Object ctxObj = params.get("_taskContext"); |
| | | if (ctxObj instanceof com.mes.task.model.TaskExecutionContext) { |
| | | com.mes.task.model.TaskExecutionContext ctx = |
| | | (com.mes.task.model.TaskExecutionContext) ctxObj; |
| | | Object positionObj = ctx.getParameters().getExtra() != null |
| | | inPosition = ctx.getParameters().getExtra() != null |
| | | ? ctx.getParameters().getExtra().get("inPosition") : null; |
| | | if (positionObj instanceof Number) { |
| | | inPosition = ((Number) positionObj).intValue(); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | log.debug("从任务参数获取卧转立编号失败: deviceId={}", deviceConfig.getId(), e); |
| | |
| | | inPosition = getLogicParam(logicParams, "inPosition", null); |
| | | } |
| | | if (inPosition != null) { |
| | | // 直接写入编号本身,不进行位置映射转换 |
| | | payload.put("inPosition", inPosition); |
| | | log.info("写入卧转立编号: deviceId={}, inPosition={}", deviceConfig.getId(), inPosition); |
| | | } else { |
| | |
| | | |
| | | log.debug("出片大车设备定时器检测到已处理的玻璃信息: taskId={}, deviceId={}, glassCount={}", |
| | | task.getTaskId(), device.getId(), processedGlassIds.size()); |
| | | |
| | | // 需等待大理片笼完成全部玻璃的处理后再出片 |
| | | @SuppressWarnings("unchecked") |
| | | List<String> initialGlassIds = (List<String>) context.getSharedData().get("initialGlassIds"); |
| | | if (!CollectionUtils.isEmpty(initialGlassIds) |
| | | && !processedGlassIds.containsAll(initialGlassIds)) { |
| | | // 部分玻璃尚未由大理片笼处理完成,保持等待 |
| | | deviceCoordinationService.syncDeviceStatus(device, |
| | | DeviceCoordinationService.DeviceStatus.WAITING, context); |
| | | if (!TaskStepDetail.Status.PENDING.name().equals(step.getStatus())) { |
| | | step.setStatus(TaskStepDetail.Status.PENDING.name()); |
| | | step.setSuccessMessage("等待大理片笼处理全部玻璃后再出片"); |
| | | taskStepDetailMapper.updateById(step); |
| | | notificationService.notifyStepUpdate(task.getTaskId(), step); |
| | | } |
| | | log.debug("出片大车等待大理片笼完成全部玻璃: taskId={}, deviceId={}, processed={}, initial={}", |
| | | task.getTaskId(), device.getId(), processedGlassIds.size(), initialGlassIds.size()); |
| | | return; |
| | | } |
| | | |
| | | // 执行出片操作 |
| | | Map<String, Object> checkParams = new HashMap<>(); |
| | |
| | | method: 'post', |
| | | data |
| | | }) |
| | | }, |
| | | /** |
| | | * 查询所有工程号列表 |
| | | */ |
| | | getEngineeringList() { |
| | | return request({ |
| | | url: `${BASE_URL}/engineering/list`, |
| | | method: 'get' |
| | | }) |
| | | }, |
| | | /** |
| | | * 根据工程号查询对应的玻璃ID列表 |
| | | */ |
| | | getGlassIdsByEngineeringId(engineeringId) { |
| | | return request({ |
| | | url: `${BASE_URL}/engineering/${engineeringId}/glassIds`, |
| | | method: 'get' |
| | | }) |
| | | }, |
| | | /** |
| | | * 删除工程号及其关联的玻璃信息 |
| | | */ |
| | | deleteEngineering(engineeringId) { |
| | | return request({ |
| | | url: `${BASE_URL}/engineering/${engineeringId}`, |
| | | method: 'delete' |
| | | }) |
| | | } |
| | | } |
| | | |
| | |
| | | <el-input-number |
| | | v-model="config.inPosition" |
| | | :min="0" |
| | | :max="1000" |
| | | :max="1999" |
| | | :step="1" |
| | | style="width: 100%;" |
| | | /> |
| | |
| | | </div> |
| | | |
| | | <el-form :model="form" label-width="120px" :rules="rules" ref="formRef"> |
| | | <div style="width: 350px; margin-bottom: 12px; margin-left: 120px;"> |
| | | <el-select |
| | | v-model="selectedEngineeringId" |
| | | placeholder="选择工程号(选择后自动填充玻璃ID)" |
| | | clearable |
| | | filterable |
| | | :disabled="!group" |
| | | :loading="engineeringListLoading" |
| | | @change="handleEngineeringChange" |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="item in engineeringList" |
| | | :key="item.engineeringId" |
| | | :label="item.engineeringId" |
| | | :value="item.engineeringId" |
| | | > |
| | | <div style="display: flex; justify-content: space-between; align-items: center; width: 100%;"> |
| | | <div style="flex: 1;"> |
| | | <span>{{ item.engineeringId }}</span> |
| | | <span style="margin-left: 8px; color: #8492a6; font-size: 12px"> |
| | | {{ item.date ? new Date(item.date).toLocaleDateString() : '' }} |
| | | </span> |
| | | </div> |
| | | <el-button |
| | | type="danger" |
| | | link |
| | | size="small" |
| | | :loading="deletingEngineeringId === item.engineeringId" |
| | | @click.stop="handleDeleteEngineering(item.engineeringId)" |
| | | style="margin-left: 8px; padding: 0 4px;" |
| | | > |
| | | <el-icon><Delete /></el-icon> |
| | | </el-button> |
| | | </div> |
| | | </el-option> |
| | | </el-select> |
| | | </div> |
| | | |
| | | <el-form-item label="玻璃ID列表" prop="glassIds"> |
| | | <el-input |
| | | v-model="glassIdsInput" |
| | | type="textarea" |
| | | :rows="4" |
| | | placeholder="可选:输入玻璃ID,将使用输入的ID进行测试" |
| | | placeholder="可选:输入玻璃ID,将使用输入的ID进行测试(或通过上方选择工程号自动填充)" |
| | | show-word-limit |
| | | :maxlength="5000" |
| | | /> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, reactive, ref, watch } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { computed, reactive, ref, watch, onMounted } from 'vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { Delete, Promotion, Upload } from '@element-plus/icons-vue' |
| | | import * as XLSX from 'xlsx' |
| | | import { multiDeviceTaskApi } from '@/api/device/multiDeviceTask' |
| | |
| | | const loadDeviceLoading = ref(false) |
| | | const fileInputRef = ref(null) |
| | | |
| | | // 工程号相关 |
| | | const selectedEngineeringId = ref('') |
| | | const engineeringList = ref([]) |
| | | const engineeringListLoading = ref(false) |
| | | const glassIdsLoading = ref(false) |
| | | const deletingEngineeringId = ref('') |
| | | |
| | | watch( |
| | | () => props.group, |
| | | () => { |
| | | glassIdsInput.value = '' |
| | | selectedEngineeringId.value = '' |
| | | fetchLoadDevice() |
| | | fetchEngineeringList() |
| | | } |
| | | ) |
| | | |
| | | // 组件挂载时加载工程号列表 |
| | | onMounted(() => { |
| | | fetchEngineeringList() |
| | | }) |
| | | |
| | | const glassIds = computed(() => { |
| | | if (!glassIdsInput.value) return [] |
| | |
| | | .map((item) => item.trim()) |
| | | .filter((item) => item.length > 0) |
| | | }) |
| | | |
| | | // 获取工程号列表 |
| | | const fetchEngineeringList = async () => { |
| | | try { |
| | | engineeringListLoading.value = true |
| | | const response = await engineeringApi.getEngineeringList() |
| | | |
| | | if (Array.isArray(response)) { |
| | | engineeringList.value = response |
| | | } else if (Array.isArray(response?.data)) { |
| | | engineeringList.value = response.data |
| | | } else { |
| | | engineeringList.value = [] |
| | | } |
| | | // 按日期倒序排列 |
| | | engineeringList.value.sort((a, b) => { |
| | | const dateA = a.date ? new Date(a.date).getTime() : 0 |
| | | const dateB = b.date ? new Date(b.date).getTime() : 0 |
| | | return dateB - dateA |
| | | }) |
| | | } catch (error) { |
| | | console.error('获取工程号列表失败:', error) |
| | | ElMessage.error(error?.message || '获取工程号列表失败') |
| | | engineeringList.value = [] |
| | | } finally { |
| | | engineeringListLoading.value = false |
| | | } |
| | | } |
| | | |
| | | // 处理工程号选择变化 |
| | | const handleEngineeringChange = async (engineeringId) => { |
| | | if (!engineeringId) { |
| | | // 清空选择时,不清空已输入的玻璃ID,让用户保留 |
| | | return |
| | | } |
| | | |
| | | try { |
| | | glassIdsLoading.value = true |
| | | const response = await engineeringApi.getGlassIdsByEngineeringId(engineeringId) |
| | | |
| | | const glassIds = response?.glassIds || response?.data?.glassIds || [] |
| | | |
| | | if (glassIds.length > 0) { |
| | | glassIdsInput.value = glassIds.join('\n') |
| | | ElMessage.success(`已加载工程号 ${engineeringId} 的 ${glassIds.length} 个玻璃ID`) |
| | | } else { |
| | | ElMessage.warning(`工程号 ${engineeringId} 下没有找到玻璃ID`) |
| | | } |
| | | } catch (error) { |
| | | console.error('获取玻璃ID列表失败:', error) |
| | | ElMessage.error(error?.message || '获取玻璃ID列表失败') |
| | | } finally { |
| | | glassIdsLoading.value = false |
| | | } |
| | | } |
| | | |
| | | // 处理删除工程号 |
| | | const handleDeleteEngineering = async (engineeringId) => { |
| | | if (!engineeringId) { |
| | | return |
| | | } |
| | | |
| | | try { |
| | | await ElMessageBox.confirm( |
| | | `确定要删除工程号 "${engineeringId}" 及其关联的所有玻璃信息吗?此操作不可恢复!`, |
| | | '确认删除', |
| | | { |
| | | confirmButtonText: '确定删除', |
| | | cancelButtonText: '取消', |
| | | type: 'warning', |
| | | dangerouslyUseHTMLString: false |
| | | } |
| | | ) |
| | | |
| | | deletingEngineeringId.value = engineeringId |
| | | const response = await engineeringApi.deleteEngineering(engineeringId) |
| | | |
| | | const result = response?.data || response |
| | | if (result?.success !== false) { |
| | | const deletedCount = result?.deletedGlassCount || 0 |
| | | ElMessage.success(`已删除工程号 ${engineeringId},共删除 ${deletedCount} 条玻璃信息`) |
| | | |
| | | // 如果删除的是当前选中的工程号,清空选择 |
| | | if (selectedEngineeringId.value === engineeringId) { |
| | | selectedEngineeringId.value = '' |
| | | glassIdsInput.value = '' |
| | | } |
| | | |
| | | // 刷新工程号列表 |
| | | await fetchEngineeringList() |
| | | } else { |
| | | throw new Error(result?.message || '删除失败') |
| | | } |
| | | } catch (error) { |
| | | if (error !== 'cancel') { |
| | | console.error('删除工程号失败:', error) |
| | | ElMessage.error(error?.message || '删除工程号失败') |
| | | } |
| | | } finally { |
| | | deletingEngineeringId.value = '' |
| | | } |
| | | } |
| | | |
| | | const normalizeType = (type) => (type || '').trim().toUpperCase() |
| | | |
| | |
| | | : `成功导入 ${glassDataList.length} 条玻璃数据` |
| | | ElMessage.success(successMsg) |
| | | |
| | | // 将导入的玻璃ID填充到输入框,方便用户查看和编辑 |
| | | const glassIds = glassDataList.map(item => item.glassId).filter(id => id) |
| | | if (glassIds.length > 0) { |
| | | glassIdsInput.value = glassIds.join('\n') |
| | | // 成功后刷新工程号下拉列表,并选中最新工程号 |
| | | try { |
| | | await fetchEngineeringList() |
| | | if (engineerId) { |
| | | selectedEngineeringId.value = engineerId |
| | | // 刷新并回填后端保存的 glassId(带工程号前缀),避免使用前端原始值 |
| | | await handleEngineeringChange(engineerId) |
| | | } |
| | | } catch (refreshErr) { |
| | | console.error('刷新工程号列表失败:', refreshErr) |
| | | } |
| | | } else { |
| | | // MES 接口返回失败 |