Files
sake_batch/src/main/java/com/wb/domainserver/acidlivy/AciDlivyTasklet.java

323 lines
14 KiB
Java

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<AciDlivyOrderVO> 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<String> requestEntity = new HttpEntity<>("{}", headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(
aciKeyUrl,
HttpMethod.POST,
requestEntity,
String.class
);
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> 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<Void> entity = new HttpEntity<>(headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
log.debug("[AciDlivyBatch] API 응답 원문 (ORDR_INNB={}): {}", orderVO.getOrdrInnb(), response.getBody());
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> 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<Map> 모두 대응)
Object dataObj = body.get("Data");
if (dataObj == null) {
return null;
}
List<Map<String, Object>> dataList;
if (dataObj instanceof List) {
dataList = (List<Map<String, Object>>) dataObj;
} else if (dataObj instanceof Map) {
dataList = List.of((Map<String, Object>) dataObj);
} else {
return null;
}
// OverseaSellerNo == ORDR_DLVY_INNB 매칭 → BLNO 추출
for (Map<String, Object> 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<DataMap> 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);
}
}