|
|
@@ -1,6 +1,7 @@
|
|
|
package com.platform.service;
|
|
|
|
|
|
import com.alibaba.fastjson.JSON;
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
import com.platform.api.request.XpPrintImageReqVo;
|
|
|
import com.platform.api.request.XpPrintReceiptReqVo;
|
|
|
import com.platform.config.XpCloudProperties;
|
|
|
@@ -16,8 +17,13 @@ import jakarta.annotation.Resource;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
|
+import org.springframework.beans.factory.annotation.Qualifier;
|
|
|
+import org.springframework.http.HttpEntity;
|
|
|
+import org.springframework.http.HttpHeaders;
|
|
|
+import org.springframework.http.MediaType;
|
|
|
+import org.springframework.http.ResponseEntity;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
-import org.springframework.transaction.annotation.Transactional;
|
|
|
+import org.springframework.web.client.RestTemplate;
|
|
|
|
|
|
import java.nio.charset.StandardCharsets;
|
|
|
import java.security.MessageDigest;
|
|
|
@@ -47,6 +53,12 @@ public class XpCloudPrintService {
|
|
|
|
|
|
private final KwsPrintReceiptRecordRepository kwsPrintReceiptRecordRepository;
|
|
|
|
|
|
+ /**
|
|
|
+ * 调用芯烨云 {@code open.xpyun.net} 开放接口(小票 {@code /xprinter/print})的 HTTP 客户端。
|
|
|
+ */
|
|
|
+ @Qualifier("xpOpenApiRestTemplate")
|
|
|
+ private final RestTemplate xpOpenApiRestTemplate;
|
|
|
+
|
|
|
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
|
|
|
|
/**
|
|
|
@@ -93,7 +105,6 @@ public class XpCloudPrintService {
|
|
|
* @param reqVo 小票订单打印请求
|
|
|
* @return 云端打印任务号
|
|
|
*/
|
|
|
- @Transactional(rollbackFor = Exception.class)
|
|
|
public String printReceipt(XpPrintReceiptReqVo reqVo) {
|
|
|
log.info("芯烨云小票打印, 入参:{}", JSON.toJSONString(reqVo));
|
|
|
if (reqVo == null || StringUtils.isAnyBlank(reqVo.getSn(), reqVo.getTaskNo())) {
|
|
|
@@ -103,40 +114,33 @@ public class XpCloudPrintService {
|
|
|
throw new IotException("设置expiresIn时,mode必须为1");
|
|
|
}
|
|
|
try {
|
|
|
+ int nextSuccessfulOrdinal = kwsPrintReceiptRecordRepository.resolveNextPrintOrdinalOrThrow(reqVo.getTaskNo());
|
|
|
TradeOrderTransportInfoResp transportInfo = getReceiptContent(reqVo.getTaskNo());
|
|
|
PrintReceiptContent content = assembleReceiptContent(transportInfo, reqVo.getTaskNo());
|
|
|
LocalDateTime now = LocalDateTime.now();
|
|
|
- int printCount = kwsPrintReceiptRecordRepository.reservePrintCount(reqVo.getTaskNo(), transportInfo.getTradeOrderNo(), reqVo.getSn(),now);
|
|
|
- content.setPrintTimesDesc(formatPrintTimes(printCount));
|
|
|
+ content.setPrintTimesDesc(formatPrintTimes(nextSuccessfulOrdinal));
|
|
|
content.setPrintTime(now.format(DATE_TIME_FORMATTER));
|
|
|
validateReceiptContent(content);
|
|
|
String xmlContent = buildReceiptXml(content);
|
|
|
- PrintOrderRequest request = new PrintOrderRequest(reqVo.getSn(), xmlContent);
|
|
|
- fillCommonParams(request);
|
|
|
- request.setDirect(reqVo.getDirect());
|
|
|
- request.setCopies(reqVo.getCopies());
|
|
|
- request.setMode(reqVo.getMode());
|
|
|
- if (StringUtils.isNotBlank(reqVo.getIdempotent())) {
|
|
|
- request.setIdempotent(reqVo.getIdempotent());
|
|
|
- }
|
|
|
- if (reqVo.getRealTime() != null) {
|
|
|
- request.setRealTime(reqVo.getRealTime());
|
|
|
- }
|
|
|
- if (reqVo.getSupportNativeInstruction() != null) {
|
|
|
- request.setSupportNativeInstruction(reqVo.getSupportNativeInstruction());
|
|
|
- }
|
|
|
- if (reqVo.getExpiresIn() != null) {
|
|
|
- request.setExpiresIn(reqVo.getExpiresIn());
|
|
|
- }
|
|
|
- log.info("芯烨云小票打印,请求参数:{}", JSON.toJSONString(request));
|
|
|
- ObjectRestResponse<String> response = printService.print(request);
|
|
|
- log.info("芯烨云小票打印完成, sn={}, code={}, msg={}, data={}",
|
|
|
- reqVo.getSn(), response.getCode(), response.getMsg(), response.getData());
|
|
|
- if (response.getCode() == null || response.getCode() != 0) {
|
|
|
- throw new IotException("调用芯烨云小票打印失败:" + response.getMsg());
|
|
|
+
|
|
|
+ JSONObject printBody = buildReceiptPrintJsonBody(reqVo, xmlContent);
|
|
|
+ log.info("芯烨云小票打印HTTP请求摘要, url={}, sn={}, copies={}, mode={}",
|
|
|
+ xpCloudProperties.getReceiptPrintUrl(), reqVo.getSn(),
|
|
|
+ printBody.getInteger("copies"), printBody.getInteger("mode"));
|
|
|
+ if (log.isDebugEnabled()) {
|
|
|
+ log.debug("芯烨云小票打印HTTP请求体: {}", printBody.toJSONString());
|
|
|
}
|
|
|
- log.info("芯烨云小票打印次数记录完成, taskNo={}, printCount={}", content.getTaskNo(), printCount);
|
|
|
- return response.getData();
|
|
|
+ String orderNo = postReceiptPrint(printBody);
|
|
|
+ kwsPrintReceiptRecordRepository.saveSuccessfulReceiptPrint(
|
|
|
+ reqVo.getTaskNo(),
|
|
|
+ transportInfo.getTradeOrderNo(),
|
|
|
+ reqVo.getSn(),
|
|
|
+ xmlContent,
|
|
|
+ orderNo,
|
|
|
+ now);
|
|
|
+ log.info("芯烨云小票打印完成, sn={}, orderNo={}, 已累计成功序号={}",
|
|
|
+ reqVo.getSn(), orderNo, nextSuccessfulOrdinal);
|
|
|
+ return orderNo;
|
|
|
} catch (IotException e) {
|
|
|
throw e;
|
|
|
} catch (Exception e) {
|
|
|
@@ -212,7 +216,7 @@ public class XpCloudPrintService {
|
|
|
.findFirst()
|
|
|
.orElse(null);
|
|
|
if (StringUtils.isNotBlank(blankField)) {
|
|
|
- throw new IotException("小票内容字段不能为空:" + blankField);
|
|
|
+ // throw new IotException("小票内容字段不能为空:" + blankField);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -361,15 +365,101 @@ public class XpCloudPrintService {
|
|
|
* 填充公共参数:user、timestamp、sign
|
|
|
*/
|
|
|
private void fillCommonParams(PrintOrderRequest request) {
|
|
|
+ String[] auth = buildXpAuthTriple();
|
|
|
+ request.setUser(auth[0]);
|
|
|
+ request.setTimestamp(auth[1]);
|
|
|
+ request.setSign(auth[2]);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建芯烨开放平台鉴权三元组:user、10 位秒级时间戳、sign(SHA1 小写 40 位)。
|
|
|
+ *
|
|
|
+ * @return [user, timestamp, sign]
|
|
|
+ */
|
|
|
+ private String[] buildXpAuthTriple() {
|
|
|
if (StringUtils.isAnyBlank(xpCloudProperties.getUser(), xpCloudProperties.getUserKey())) {
|
|
|
throw new IotException("芯烨配置不完整,请配置xp.dev.user和xp.dev.user-key");
|
|
|
}
|
|
|
String user = xpCloudProperties.getUser();
|
|
|
String timestamp = String.valueOf(Instant.now().getEpochSecond());
|
|
|
String sign = sha1Hex(user + xpCloudProperties.getUserKey() + timestamp);
|
|
|
- request.setUser(user);
|
|
|
- request.setTimestamp(timestamp);
|
|
|
- request.setSign(sign);
|
|
|
+ return new String[]{user, timestamp, sign};
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 组装「打印小票订单」接口请求体(与 https://open.xpyun.net 文档字段一致)。
|
|
|
+ *
|
|
|
+ * @param reqVo 业务入参(含打印机编号、可选 mode/copies/idempotent 等)
|
|
|
+ * @param xmlContent 模板 XML 正文
|
|
|
+ * @return JSON 请求对象
|
|
|
+ */
|
|
|
+ private JSONObject buildReceiptPrintJsonBody(XpPrintReceiptReqVo reqVo, String xmlContent) {
|
|
|
+ JSONObject body = new JSONObject(true);
|
|
|
+ String[] auth = buildXpAuthTriple();
|
|
|
+ body.put("user", auth[0]);
|
|
|
+ body.put("timestamp", auth[1]);
|
|
|
+ body.put("sign", auth[2]);
|
|
|
+ if (Boolean.TRUE.equals(xpCloudProperties.getDebug())) {
|
|
|
+ body.put("debug", "1");
|
|
|
+ }
|
|
|
+ body.put("sn", reqVo.getSn());
|
|
|
+ body.put("content", xmlContent);
|
|
|
+ int copies = reqVo.getCopies() != null ? reqVo.getCopies() : 1;
|
|
|
+ body.put("copies", copies);
|
|
|
+ if (reqVo.getMode() != null) {
|
|
|
+ body.put("mode", reqVo.getMode());
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(reqVo.getIdempotent())) {
|
|
|
+ body.put("idempotent", reqVo.getIdempotent());
|
|
|
+ }
|
|
|
+ if (reqVo.getExpiresIn() != null) {
|
|
|
+ body.put("expiresIn", reqVo.getExpiresIn());
|
|
|
+ }
|
|
|
+ body.put("voice","2");
|
|
|
+ return body;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * POST 调用开放平台「打印小票订单」接口并解析打印订单号。
|
|
|
+ *
|
|
|
+ * @param body 请求 JSON
|
|
|
+ * @return {@code data} 字段订单号
|
|
|
+ */
|
|
|
+ private String postReceiptPrint(JSONObject body) {
|
|
|
+ String url = xpCloudProperties.getReceiptPrintUrl();
|
|
|
+ if (StringUtils.isBlank(url)) {
|
|
|
+ throw new IotException("芯烨云小票打印接口地址未配置,请配置 xp.dev.receipt-print-url");
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ HttpHeaders headers = new HttpHeaders();
|
|
|
+ headers.setContentType(new MediaType("application", "json", StandardCharsets.UTF_8));
|
|
|
+ HttpEntity<String> entity = new HttpEntity<>(body.toJSONString(), headers);
|
|
|
+ ResponseEntity<String> responseEntity = xpOpenApiRestTemplate.postForEntity(url, entity, String.class);
|
|
|
+ log.info("芯烨云小票打印响应参数: {}", JSON.toJSONString(responseEntity));
|
|
|
+ if (!responseEntity.getStatusCode().is2xxSuccessful()) {
|
|
|
+ throw new IotException("芯烨云小票打印HTTP异常, status=" + responseEntity.getStatusCode());
|
|
|
+ }
|
|
|
+ String raw = responseEntity.getBody();
|
|
|
+ if (StringUtils.isBlank(raw)) {
|
|
|
+ throw new IotException("芯烨云小票打印返回空响应");
|
|
|
+ }
|
|
|
+ JSONObject resp = JSON.parseObject(raw);
|
|
|
+ Integer code = resp.getInteger("code");
|
|
|
+ String msg = resp.getString("msg");
|
|
|
+ if (code == null || code != 0) {
|
|
|
+ throw new IotException("调用芯烨云小票打印失败:" + msg);
|
|
|
+ }
|
|
|
+ String data = resp.getString("data");
|
|
|
+ if (StringUtils.isBlank(data)) {
|
|
|
+ throw new IotException("芯烨云小票打印成功但订单号为空");
|
|
|
+ }
|
|
|
+ return data;
|
|
|
+ } catch (IotException e) {
|
|
|
+ throw e;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("芯烨云小票打印HTTP调用异常, url={}", url, e);
|
|
|
+ throw new IotException("调用芯烨云小票打印HTTP异常:" + e.getMessage());
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|