package com.wb.domainserver.acidlivy; import com.fasterxml.jackson.databind.ObjectMapper; import com.wb.domainserver.util.dataaccess.DataMap; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.core.step.tasklet.Tasklet; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.stereotype.Component; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.support.TransactionTemplate; import org.springframework.web.client.RestTemplate; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.Map; // ACI 출고자료 조회 및 출고처리 배치 Tasklet // 흐름: gc_order_shipping(ORDR_STTUS_CD=35) 조회 → 토큰 발급 → ACI API 호출 → BLNO 추출 → 출고처리 @Slf4j @Component @RequiredArgsConstructor public class AciDlivyTasklet implements Tasklet { private final AciDlivyMapper aciDlivyMapper; private final PlatformTransactionManager transactionManager; @Value("${acikey.url}") private String aciKeyUrl; @Value("${aci.dlivy.api-url}") private String aciDlivyApiUrl; @Value("${acikey.userid}") private String userId; @Value("${acikey.apikey}") private String apiKey; private static final String HDRYCPN_CD = "code22"; // ACI 택배사 코드 private static final String DLIVY_STTUS_CD = "55"; // 출고완료 코드 @Override public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { log.info("[AciDlivyBatch] 배치 시작"); // 토큰 발급 (yaml 고정 apikey → ACIKey 토큰 발급, 메모리에만 유지) String aciKeyToken = issueAciKeyToken(); // 대상 주문 목록 조회 (ORDR_STTUS_CD = '35') List targetList = aciDlivyMapper.selectAciDlivyTargetList(); log.info("[AciDlivyBatch] 대상 주문 건수: {}", targetList.size()); if (targetList.isEmpty()) { log.info("[AciDlivyBatch] 처리할 대상 주문이 없습니다."); return RepeatStatus.FINISHED; } int successCnt = 0; int failCnt = 0; int skipCnt = 0; // 건별로 독립 트랜잭션 (REQUIRES_NEW) 으로 처리 - 한 건 실패가 다른 건에 영향 없도록 TransactionTemplate txTemplate = new TransactionTemplate(transactionManager); txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); for (AciDlivyOrderVO orderVO : targetList) { try { // 건별 ACI 출고자료 API 호출 → BLNO 추출 String blno = callAciDlivyApi(orderVO, aciKeyToken); if (blno == null) { log.info("[AciDlivyBatch] 스킵 - ORDR_DLVY_INNB={}, ORDR_INNB={} (BLNO 없음 또는 조건 미충족)", orderVO.getOrdrDlvyInnb(), orderVO.getOrdrInnb()); skipCnt++; } else { orderVO.setBlno(blno); orderVO.setHdrycpnCd(HDRYCPN_CD); orderVO.setDlivySttusCd(DLIVY_STTUS_CD); // 건별 처리 (독립 트랜잭션) txTemplate.execute(status -> { processDelivery(orderVO); return null; }); successCnt++; log.info("[AciDlivyBatch] 출고처리 완료 - ORDR_DLVY_INNB={}, BLNO={}", orderVO.getOrdrDlvyInnb(), blno); } } catch (Exception e) { log.error("[AciDlivyBatch] 출고처리 실패 - ORDR_DLVY_INNB={}, 오류={}", orderVO.getOrdrDlvyInnb(), e.getMessage(), e); failCnt++; } // 건별 API 호출 간격 유지 (과부하 방지 - 1초 대기) try { Thread.sleep(1000); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); break; } } log.info("[AciDlivyBatch] 배치 완료 - 성공: {}, 실패: {}, 스킵: {}", successCnt, failCnt, skipCnt); return RepeatStatus.FINISHED; } /** * ACIKey 토큰 발급 * yaml 고정 apikey를 APIKEY Header로 사용하여 POST 호출 → 발급받은 토큰은 메모리에만 유지 */ private String issueAciKeyToken() { try { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("USERID", userId); headers.set("APIKEY", apiKey); HttpEntity requestEntity = new HttpEntity<>("{}", headers); RestTemplate restTemplate = new RestTemplate(); ResponseEntity response = restTemplate.exchange( aciKeyUrl, HttpMethod.POST, requestEntity, String.class ); ObjectMapper objectMapper = new ObjectMapper(); Map body = objectMapper.readValue(response.getBody(), Map.class); String status = (String) body.get("Status"); String aciKey = (String) body.get("ACIKey"); String errMsg = (String) body.get("ErrMsg"); if ("success".equals(status) && aciKey != null) { log.info("[AciDlivyBatch] ACIKey 토큰 발급 성공"); return aciKey; } else { throw new RuntimeException("ACIKey 토큰 발급 실패. Status: " + status + ", ErrMsg: " + errMsg); } } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException("ACIKey 토큰 발급 오류", e); } } /** * ACI 출고자료 API 호출 * Content-Type이 text/html로 오는 경우를 대비해 JSON을 String으로 받아 ObjectMapper 파싱 * @param aciKeyToken 발급받은 ACIKey 토큰 * @return BLNO(운송장번호), 없으면 null */ private String callAciDlivyApi(AciDlivyOrderVO orderVO, String aciKeyToken) { try { HttpHeaders headers = new HttpHeaders(); headers.set("UserID", userId); headers.set("ACIKey", aciKeyToken); String url = aciDlivyApiUrl + "?searchType=1&orderNumber=" + orderVO.getOrdrInnb(); HttpEntity entity = new HttpEntity<>(headers); RestTemplate restTemplate = new RestTemplate(); ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); log.debug("[AciDlivyBatch] API 응답 원문 (ORDR_INNB={}): {}", orderVO.getOrdrInnb(), response.getBody()); ObjectMapper objectMapper = new ObjectMapper(); Map body = objectMapper.readValue(response.getBody(), Map.class); String status = (String) body.get("Status"); log.debug("[AciDlivyBatch] API Status={} (ORDR_INNB={})", status, orderVO.getOrdrInnb()); // 출고/발송완료 상태 검증 (정확한 Status값은 실제 응답 확인 후 조정 필요) if (!"A10".equals(status)) { log.info("[AciDlivyBatch] API 출고 미완료 - ORDR_INNB={}, Status={}", orderVO.getOrdrInnb(), status); return null; } // Data 파싱 (단일 Map 또는 List 모두 대응) Object dataObj = body.get("Data"); if (dataObj == null) { return null; } List> dataList; if (dataObj instanceof List) { dataList = (List>) dataObj; } else if (dataObj instanceof Map) { dataList = List.of((Map) dataObj); } else { return null; } // OverseaSellerNo == ORDR_DLVY_INNB 매칭 → BLNO 추출 for (Map item : dataList) { Object sellerNo = item.get("OverseaSellerNo"); if (sellerNo != null && orderVO.getOrdrDlvyInnb().equals(String.valueOf(sellerNo).trim())) { Object blno = item.get("BLNO"); if (blno != null && !String.valueOf(blno).isBlank()) { return String.valueOf(blno).trim(); } } } log.info("[AciDlivyBatch] OverseaSellerNo 매칭 없음 - ORDR_DLVY_INNB={}", orderVO.getOrdrDlvyInnb()); return null; } catch (Exception e) { log.error("[AciDlivyBatch] API 호출 오류 - ORDR_INNB={}, 오류={}", orderVO.getOrdrInnb(), e.getMessage(), e); return null; } } /** * 출고처리 (순서: gc_goods_export → gc_goods_export_item → gc_order_item_option * → gc_order_shipping → gc_order → 출고로그 → 주문로그) * gc_order_shipping/gc_order 상태변경은 모든 아이템 처리 완료 후 일괄 수행 */ private void processDelivery(AciDlivyOrderVO orderVO) { // ① 주문 상품 옵션 목록 먼저 조회 (0건이면 출고처리 전체 스킵) List itemList = aciDlivyMapper.selectAciDlivyTargetItemList(orderVO); if (itemList.isEmpty()) { log.warn("[AciDlivyBatch] 주문 상품 옵션 없음 - 출고처리 스킵 (ORDR_DLVY_INNB={})", orderVO.getOrdrDlvyInnb()); return; } log.debug("[AciDlivyBatch] 주문 상품 옵션 수={} (ORDR_DLVY_INNB={})", itemList.size(), orderVO.getOrdrDlvyInnb()); // ② DLIVY_INNB = ORDR_DLVY_INNB (gc_order_shipping.ORDR_DLVY_INNB → gc_goods_export.DLIVY_INNB) orderVO.setDlivyInnb(orderVO.getOrdrDlvyInnb()); // ③ dlivyCdNb 생성 (DLIVY_INNB 확정 후, INSERT 전) String dlivyCdNb = "D" + new SimpleDateFormat("yyMMddHH").format(new Date()) + orderVO.getDlivyInnb(); orderVO.setDlivyCdNb(dlivyCdNb); // ④ gc_goods_export INSERT/UPDATE (dlivyCdNb 직접 포함) int exportCnt = aciDlivyMapper.selectGoodsExportCount(orderVO); if (exportCnt > 0) { aciDlivyMapper.updateGoodsExport(orderVO); } else { aciDlivyMapper.insertGoodsExport(orderVO); } // ⑤ 주문 상품 옵션별 처리 (gc_goods_export_item, gc_order_item_option) for (DataMap itemMap : itemList) { Object qyObj = itemMap.get("qy"); if (qyObj == null) continue; int qy; try { qy = Integer.parseInt(String.valueOf(qyObj)); } catch (NumberFormatException e) { continue; } if (qy <= 0) continue; itemMap.put("sopmalCd", orderVO.getSopmalCd()); itemMap.put("ordrDlvyInnb", orderVO.getOrdrDlvyInnb()); itemMap.put("dlivyInnb", orderVO.getDlivyInnb()); itemMap.put("ordrInnb", orderVO.getOrdrInnb()); itemMap.put("hdrycpnCd", HDRYCPN_CD); itemMap.put("invcnb", orderVO.getBlno()); itemMap.put("dlivySttusCd", DLIVY_STTUS_CD); // gc_goods_export_item 처리 int exportItemCnt = aciDlivyMapper.selectGoodsExportItemCount(itemMap); if (exportItemCnt > 0) { aciDlivyMapper.updateGoodsExportItem(itemMap); } else { aciDlivyMapper.insertGoodsExportItem(itemMap); } // gc_order_item_option 상태 UPDATE aciDlivyMapper.updateOrderItemOptionStatus(itemMap); // 출고 로그 INSERT (건별) DataMap exportLogMap = new DataMap(); exportLogMap.put("ordrInnb", orderVO.getOrdrInnb()); exportLogMap.put("ordrDlvyInnb", orderVO.getOrdrDlvyInnb()); exportLogMap.put("sopmalCd", orderVO.getSopmalCd()); exportLogMap.put("dlivyInnb", orderVO.getDlivyInnb()); exportLogMap.put("dlivyGoodsCdNb", itemMap.get("ordrGoodsOptnInnb")); exportLogMap.put("reqstProcsSttusCd", DLIVY_STTUS_CD); exportLogMap.put("mssageCn", "[ACI 배치] ORDR_INNB:" + orderVO.getOrdrInnb() + " 출고처리"); exportLogMap.put("regusr", 0); aciDlivyMapper.insertGoodsExportLog(exportLogMap); } // ⑥ gc_order_shipping 상태 UPDATE (모든 item_option 처리 완료 후 일괄) aciDlivyMapper.updateOrderShippingStatus(orderVO); // ⑦ gc_order 상태 UPDATE (모든 shipping 처리 완료 후 일괄) aciDlivyMapper.updateOrderStatus(orderVO); // ⑧ 주문 로그 INSERT DataMap orderLogMap = new DataMap(); orderLogMap.put("ordrInnb", orderVO.getOrdrInnb()); orderLogMap.put("sopmalCd", orderVO.getSopmalCd()); orderLogMap.put("logSj", "출고처리(" + dlivyCdNb + ")"); orderLogMap.put("logDtlCn", "[ACI 배치] 배치 출고처리 완료"); orderLogMap.put("aditInfoCn", null); orderLogMap.put("dlivyCdNb", dlivyCdNb); orderLogMap.put("refundCdNb", null); orderLogMap.put("logDvCd", "01"); orderLogMap.put("dvCd", "BATCH"); orderLogMap.put("opetrNm", "ACI 배치"); orderLogMap.put("qy", 0); orderLogMap.put("amt", 0); aciDlivyMapper.insertOrderLog(orderLogMap); } }