|
|
@@ -74,27 +74,50 @@ public class XpCloudPrintService {
|
|
|
* @param reqVo 业务请求参数
|
|
|
* @return 云端打印任务号
|
|
|
*/
|
|
|
+ /**
|
|
|
+ * 调用SDK执行图片打印
|
|
|
+ *
|
|
|
+ * @param reqVo 业务请求参数,包含打印机序列号、图片URL及打印份数
|
|
|
+ * @return 云端打印任务号
|
|
|
+ * @throws IotException 当参数校验失败或打印服务调用异常时抛出
|
|
|
+ */
|
|
|
public String printImage(XpPrintImageReqVo reqVo) {
|
|
|
+ // 1. 参数校验:检查请求对象及核心字段(打印机SN、图片URL)是否为空
|
|
|
if (reqVo == null || StringUtils.isAnyBlank(reqVo.getSn(), reqVo.getImageUrl())) {
|
|
|
throw new IotException("打印参数不能为空");
|
|
|
}
|
|
|
+ // 2. 参数校验:检查打印份数是否合法(必须大于0)
|
|
|
if (reqVo.getCopies() == null || reqVo.getCopies() <= 0) {
|
|
|
throw new IotException("打印份数必须大于0");
|
|
|
}
|
|
|
+
|
|
|
try {
|
|
|
+ // 3. 构建打印请求对象
|
|
|
PrintOrderRequest request = new PrintOrderRequest(reqVo.getSn(), reqVo.getImageUrl());
|
|
|
+ // 设置打印份数
|
|
|
request.setCopies(reqVo.getCopies());
|
|
|
+ // 生成去横杠的UUID作为幂等性标识,防止重复打印
|
|
|
request.setIdempotent(UUID.randomUUID().toString().replace("-", ""));
|
|
|
+
|
|
|
+ // 4. 调用芯烨云SDK执行图片打印
|
|
|
ObjectRestResponse<String> response = printService.printImage(request);
|
|
|
+
|
|
|
+ // 5. 记录打印结果日志
|
|
|
log.info("芯烨云图片打印完成, sn={}, code={}, msg={}, data={}",
|
|
|
reqVo.getSn(), response.getCode(), response.getMsg(), response.getData());
|
|
|
+
|
|
|
+ // 6. 校验响应结果:code不为null且等于0表示成功
|
|
|
if (response.getCode() == null || response.getCode() != 0) {
|
|
|
throw new IotException("调用芯烨云打印失败:" + response.getMsg());
|
|
|
}
|
|
|
+
|
|
|
+ // 7. 返回云端打印任务号
|
|
|
return response.getData();
|
|
|
} catch (IotException e) {
|
|
|
+ // 业务异常直接抛出
|
|
|
throw e;
|
|
|
} catch (Exception e) {
|
|
|
+ // 捕获其他未知异常,记录错误日志并转换为业务异常抛出
|
|
|
log.error("调用芯烨云图片打印异常, sn={}", reqVo.getSn(), e);
|
|
|
throw new IotException("调用芯烨云图片打印异常:" + e.getMessage());
|
|
|
}
|
|
|
@@ -125,55 +148,55 @@ public class XpCloudPrintService {
|
|
|
* @throws IotException 当参数错误、远程调用失败或打印服务异常时抛出
|
|
|
*/
|
|
|
public String printReceipt(XpPrintReceiptReqVo reqVo) {
|
|
|
- // 记录入口日志,便于追踪请求参数
|
|
|
+ // 1. 记录入口日志,方便排查问题
|
|
|
log.info("芯烨云小票打印, 入参:{}", JSON.toJSONString(reqVo));
|
|
|
|
|
|
- // 1. 基础参数校验
|
|
|
+ // 2. 参数校验:检查请求对象及核心字段(打印机SN、任务单号)是否为空
|
|
|
if (reqVo == null || StringUtils.isAnyBlank(reqVo.getSn(), reqVo.getTaskNo())) {
|
|
|
throw new IotException("打印参数不能为空");
|
|
|
}
|
|
|
- // 业务规则校验:如果设置了过期时间 (expiresIn),则打印模式 (mode) 必须为 1 (云端模式)
|
|
|
+ // 3. 业务规则校验:如果设置了过期时间(expiresIn),则打印模式(mode)必须为1(即时打印)
|
|
|
if (reqVo.getExpiresIn() != null && (reqVo.getMode() == null || reqVo.getMode() != 1)) {
|
|
|
throw new IotException("设置expiresIn时,mode必须为1");
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
- // 2. 获取下一次成功打印的序号,用于防止重复打印及记录打印历史
|
|
|
+ // 4. 获取当前任务的下一次成功打印序号,用于生成“第N次打印”标识及防重
|
|
|
int nextSuccessfulOrdinal = kwsPrintReceiptRecordRepository.resolveNextPrintOrdinalOrThrow(reqVo.getTaskNo());
|
|
|
|
|
|
- // 3. 远程获取运单运输详细信息,作为小票内容的来源
|
|
|
+ // 5. 远程调用获取小票所需的运输业务数据
|
|
|
TradeOrderTransportInfoResp transportInfo = getReceiptContent(reqVo.getTaskNo());
|
|
|
|
|
|
- // 4. 将运输信息组装为小票打印模板对象
|
|
|
+ // 6. 将远程返回的业务数据组装成打印模板所需的内容对象
|
|
|
PrintReceiptContent content = assembleReceiptContent(transportInfo, reqVo.getTaskNo());
|
|
|
|
|
|
- // 5. 填充动态打印信息:当前时间和打印次数描述
|
|
|
+ // 7. 填充打印元数据:当前时间及打印次数描述
|
|
|
LocalDateTime now = LocalDateTime.now();
|
|
|
content.setPrintTimesDesc(formatPrintTimes(nextSuccessfulOrdinal));
|
|
|
content.setPrintTime(now.format(DATE_TIME_FORMATTER));
|
|
|
|
|
|
- // 6. 校验小票内容完整性,确保关键字段非空
|
|
|
+ // 8. 校验组装后的小票内容,确保关键字段不为空,避免打印出残缺小票
|
|
|
validateReceiptContent(content);
|
|
|
|
|
|
- // 7. 将小票内容对象转换为芯烨云支持的 XML 格式字符串
|
|
|
+ // 9. 将内容对象转换为芯烨云支持的XML格式字符串
|
|
|
String xmlContent = buildReceiptXml(content);
|
|
|
|
|
|
- // 8. 构建 HTTP 请求的 JSON Body,包含鉴权参数、SN、XML 内容等
|
|
|
+ // 10. 构建最终发送给芯烨云开放平台的JSON请求体(包含鉴权信息、SN、内容等)
|
|
|
JSONObject printBody = buildReceiptPrintJsonBody(reqVo, xmlContent);
|
|
|
|
|
|
- // 记录请求摘要日志,方便监控和排查问题
|
|
|
+ // 11. 记录关键请求摘要日志
|
|
|
log.info("芯烨云小票打印HTTP请求摘要, url={}, sn={}, copies={}, mode={}",
|
|
|
xpCloudProperties.getReceiptPrintUrl(), reqVo.getSn(),
|
|
|
printBody.getInteger("copies"), printBody.getInteger("mode"));
|
|
|
- // 仅在 Debug 模式下记录完整的请求体,避免日志过大
|
|
|
+ // 12. 仅在DEBUG级别记录完整请求体,避免生产环境日志过大
|
|
|
if (log.isDebugEnabled()) {
|
|
|
log.debug("芯烨云小票打印HTTP请求体: {}", printBody.toJSONString());
|
|
|
}
|
|
|
|
|
|
- // 9. 发送 POST 请求到芯烨云平台,获取打印订单号
|
|
|
+ // 13. 发送HTTP请求调用芯烨云打印接口,获取云端订单号
|
|
|
String orderNo = postReceiptPrint(printBody);
|
|
|
|
|
|
- // 10. 保存打印成功记录到数据库,用于后续对账或重试判断
|
|
|
+ // 14. 持久化打印成功记录,用于后续查询及打印序号累加
|
|
|
kwsPrintReceiptRecordRepository.saveSuccessfulReceiptPrint(
|
|
|
reqVo.getTaskNo(),
|
|
|
transportInfo.getTradeOrderNo(),
|
|
|
@@ -182,16 +205,17 @@ public class XpCloudPrintService {
|
|
|
orderNo,
|
|
|
now);
|
|
|
|
|
|
- // 记录完成日志
|
|
|
+ // 15. 记录完成日志
|
|
|
log.info("芯烨云小票打印完成, sn={}, orderNo={}, 已累计成功序号={}",
|
|
|
reqVo.getSn(), orderNo, nextSuccessfulOrdinal);
|
|
|
|
|
|
+ // 16. 返回云端订单号
|
|
|
return orderNo;
|
|
|
} catch (IotException e) {
|
|
|
- // 业务异常直接抛出,保持原始错误信息
|
|
|
- throw e;
|
|
|
+ // 业务异常直接抛出
|
|
|
+ throw new IotException("调用芯烨云小票打印异常:" + e.getMessage());
|
|
|
} catch (Exception e) {
|
|
|
- // 捕获其他未预期异常,记录详细堆栈并包装为业务异常
|
|
|
+ // 捕获其他未知异常,记录错误日志并转换为业务异常抛出
|
|
|
log.error("调用芯烨云小票打印异常, sn={}", reqVo.getSn(), e);
|
|
|
throw new IotException("调用芯烨云小票打印异常:" + e.getMessage());
|
|
|
}
|
|
|
@@ -203,26 +227,53 @@ public class XpCloudPrintService {
|
|
|
* @param taskNo 任务单号
|
|
|
* @return 小票内容
|
|
|
*/
|
|
|
+ /**
|
|
|
+ * 通过远程服务获取小票所需的运输业务内容。
|
|
|
+ * <p>
|
|
|
+ * 该方法根据任务单号(运单号)调用外部接口查询运输详情,并对响应结果进行严格校验,
|
|
|
+ * 确保后续打印流程能够获取到完整有效的数据。
|
|
|
+ *
|
|
|
+ * @param taskNo 任务单号(亦作为运单号使用)
|
|
|
+ * @return 运输订单信息响应对象,包含任务、司机、货物等详细信息
|
|
|
+ * @throws IotException 当远程调用失败、响应为空或业务数据缺失时抛出异常
|
|
|
+ */
|
|
|
private TradeOrderTransportInfoResp getReceiptContent(String taskNo) {
|
|
|
try {
|
|
|
+ // 1. 构建远程查询请求对象,设置运单号
|
|
|
WaybillTransportQueryReq query = new WaybillTransportQueryReq();
|
|
|
query.setWaybillNo(taskNo);
|
|
|
+
|
|
|
+ // 2. 记录请求日志,便于追踪排查
|
|
|
log.info("远程获取小票内容, 请求参数:{}", JSON.toJSONString(query));
|
|
|
+
|
|
|
+ // 3. 调用外部客户端接口查询运输信息
|
|
|
BaseResult<TradeOrderTransportInfoResp> response = printReceiptContentClient.queryTransportInfoByWaybillNo(query);
|
|
|
+
|
|
|
+ // 4. 记录响应日志,便于追踪排查
|
|
|
log.info("远程获取小票内容, 响应参数:{}", JSON.toJSONString(response));
|
|
|
+
|
|
|
+ // 5. 校验响应对象本身是否为空
|
|
|
if (response == null) {
|
|
|
throw new IotException("远程获取小票内容响应为空");
|
|
|
}
|
|
|
+
|
|
|
+ // 6. 校验业务状态码,确保接口调用成功
|
|
|
if (!response.isSuccess()) {
|
|
|
throw new IotException("远程获取小票内容失败:" + response.getMessage());
|
|
|
}
|
|
|
+
|
|
|
+ // 7. 校验返回的业务数据是否为空
|
|
|
if (response.getData() == null) {
|
|
|
throw new IotException("远程获取小票内容为空");
|
|
|
}
|
|
|
+
|
|
|
+ // 8. 返回有效的运输信息数据
|
|
|
return response.getData();
|
|
|
} catch (IotException e) {
|
|
|
+ // 业务异常直接抛出,保持原始错误信息
|
|
|
throw e;
|
|
|
} catch (Exception e) {
|
|
|
+ // 捕获非业务异常(如网络超时、序列化错误等),记录详细堆栈并转换为业务异常
|
|
|
log.error("远程获取小票内容异常, taskNo={}", taskNo, e);
|
|
|
throw new IotException("远程获取小票内容异常:" + e.getMessage());
|
|
|
}
|
|
|
@@ -234,20 +285,21 @@ public class XpCloudPrintService {
|
|
|
* @param content 小票内容
|
|
|
*/
|
|
|
/**
|
|
|
- * 校验小票内容完整性,确保关键字段非空。
|
|
|
+ * 校验小票打印内容的关键字段完整性。
|
|
|
* <p>
|
|
|
- * 该步骤旨在防止因上游数据缺失导致打印出的小票信息残缺不全,影响业务流转或客户体验。
|
|
|
- * 目前检测到空字段时仅记录日志或忽略异常抛出(代码中已注释),可根据实际需求开启严格校验。
|
|
|
+ * 该方法会检查 {@link PrintReceiptContent} 对象中所有必填字段是否为空或空白。
|
|
|
+ * 如果发现任何必填字段缺失,将抛出 {@link IotException} 异常,以防止生成内容残缺的小票。
|
|
|
*
|
|
|
* @param content 待校验的小票内容对象
|
|
|
+ * @throws IotException 当内容对象为空或包含空白的必填字段时抛出
|
|
|
*/
|
|
|
private void validateReceiptContent(PrintReceiptContent content) {
|
|
|
- // 1. 判空检查:确保传入的内容对象本身不为 null
|
|
|
+ // 1. 基础非空校验:确保传入的内容对象本身不为 null
|
|
|
if (content == null) {
|
|
|
throw new IotException("小票内容不能为空");
|
|
|
}
|
|
|
|
|
|
- // 2. 定义必填字段列表:将字段名与其对应的值封装为键值对,便于统一处理
|
|
|
+ // 2. 定义必填字段列表:使用 SimpleEntry 映射字段名与其对应的值,便于后续定位缺失字段
|
|
|
List<AbstractMap.SimpleEntry<String, String>> requiredFields = List.of(
|
|
|
new AbstractMap.SimpleEntry<>("companyName", content.getCompanyName()), // 公司名称
|
|
|
new AbstractMap.SimpleEntry<>("customerName", content.getCustomerName()), // 客户名称
|
|
|
@@ -270,20 +322,20 @@ public class XpCloudPrintService {
|
|
|
new AbstractMap.SimpleEntry<>("copyLabel", content.getCopyLabel()) // 联次标签(如客户联)
|
|
|
);
|
|
|
|
|
|
- // 3. 查找第一个为空的必填字段
|
|
|
- // 使用 Stream API 过滤出值为空白字符串的字段,并获取其字段名
|
|
|
+ // 3. 流式处理查找第一个为空的字段:
|
|
|
+ // - filter: 筛选出值为空白(null 或 empty)的字段
|
|
|
+ // - map: 提取字段名(Key)
|
|
|
+ // - findFirst: 获取第一个缺失的字段名
|
|
|
String blankField = requiredFields.stream()
|
|
|
.filter(field -> StringUtils.isBlank(field.getValue()))
|
|
|
.map(AbstractMap.SimpleEntry::getKey)
|
|
|
.findFirst()
|
|
|
.orElse(null);
|
|
|
|
|
|
- // 4. 如果存在空字段,根据业务需求决定是否抛出异常
|
|
|
- // 当前逻辑中异常抛出被注释,若需严格校验请取消注释
|
|
|
+ // 4. 如果存在空白字段,则抛出业务异常,提示具体缺失的字段名
|
|
|
if (StringUtils.isNotBlank(blankField)) {
|
|
|
- // 记录警告日志,提示哪个字段缺失,方便排查数据源问题
|
|
|
- log.warn("小票内容字段缺失: {}", blankField);
|
|
|
- // throw new IotException("小票内容字段不能为空:" + blankField);
|
|
|
+ log.error("小票内容字段不能为空: {}", blankField);
|
|
|
+ //throw new IotException("小票内容字段不能为空: " + blankField);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -297,79 +349,63 @@ public class XpCloudPrintService {
|
|
|
/**
|
|
|
* 将远程运输信息重新组装为小票打印模板参数。
|
|
|
* <p>
|
|
|
- * 该方法负责从复杂的运输信息响应对象中提取关键业务数据,并将其映射到小票打印内容对象中。
|
|
|
- * 处理逻辑包括:
|
|
|
- * 1. 校验运输信息非空。
|
|
|
- * 2. 根据任务单号解析具体的任务信息(支持多任务场景)。
|
|
|
- * 3. 提取司机、货物、车辆等子模块信息,并进行空指针保护。
|
|
|
- * 4. 格式化重量数据及汇总信息。
|
|
|
- * 5. 设置固定的联次标签。
|
|
|
+ * 该方法从 {@link TradeOrderTransportInfoResp} 中提取任务、司机、货物、车辆等关键业务数据,
|
|
|
+ * 并格式化为 {@link PrintReceiptContent} 对象,供后续生成小票 XML 使用。
|
|
|
*
|
|
|
- * @param transportInfo 远程获取的运单运输详细信息
|
|
|
- * @param taskNo 当前需要打印的任务单号,用于在多个任务中定位具体任务
|
|
|
+ * @param transportInfo 远程获取的运输订单详细信息
|
|
|
+ * @param taskNo 当前需要打印的任务单号,用于匹配具体的任务信息
|
|
|
* @return 组装好的小票打印内容对象
|
|
|
* @throws IotException 当运输信息为空时抛出异常
|
|
|
*/
|
|
|
private PrintReceiptContent assembleReceiptContent(TradeOrderTransportInfoResp transportInfo, String taskNo) {
|
|
|
- // 1. 基础校验:确保上游返回的运输信息对象不为 null
|
|
|
+ // 1. 基础校验:确保传入的运输信息对象不为 null
|
|
|
if (transportInfo == null) {
|
|
|
throw new IotException("小票运输信息不能为空");
|
|
|
}
|
|
|
|
|
|
- // 2. 解析任务信息:从运输信息中根据 taskNo 匹配具体的任务详情
|
|
|
+ // 2. 解析任务信息:根据 taskNo 从运输信息中匹配对应的任务详情
|
|
|
TradeOrderTransportInfoResp.TaskInfo taskInfo = resolveTaskInfo(transportInfo, taskNo);
|
|
|
|
|
|
- // 3. 提取子模块信息:获取司机、货物、车辆及打印相关信息
|
|
|
- // 注意:这些子对象可能为 null,后续赋值时需做判空处理
|
|
|
+ // 3. 提取子模块信息:司机、货物、车辆、打印信息(可能为 null,需做空指针保护)
|
|
|
TradeOrderTransportInfoResp.DriverInfo driverInfo = taskInfo.getDriverInfo();
|
|
|
TradeOrderTransportInfoResp.GoodsInfo goodsInfo = taskInfo.getGoodsInfo();
|
|
|
TradeOrderTransportInfoResp.TruckInfo truckInfo = taskInfo.getTruckInfo();
|
|
|
- // printInfo 在当前实现中未直接使用,但保留获取以便后续扩展
|
|
|
+ // 注:printInfo 在当前逻辑中未直接使用,但保留获取以便后续扩展
|
|
|
TradeOrderTransportInfoResp.PrintInfo printInfo = taskInfo.getPrintInfo();
|
|
|
|
|
|
- // 4. 初始化小票内容对象并填充字段
|
|
|
+ // 4. 构建打印内容对象
|
|
|
PrintReceiptContent content = new PrintReceiptContent();
|
|
|
|
|
|
- // --- 基础业务信息 ---
|
|
|
- // 设置供应商名称作为公司名称
|
|
|
- content.setCompanyName(transportInfo.getSupplierName());
|
|
|
- // 设置客户名称
|
|
|
- content.setCustomerName(transportInfo.getCustomerName());
|
|
|
-
|
|
|
- // --- 任务核心信息 ---
|
|
|
- // 设置任务编号
|
|
|
- content.setTaskNo(taskInfo.getTaskNo());
|
|
|
- // 设置接单时间
|
|
|
- content.setAcceptTime(taskInfo.getAcceptTime());
|
|
|
- // 设置完成时间
|
|
|
- content.setFinishTime(taskInfo.getFinishTime());
|
|
|
- // 设置计重人姓名
|
|
|
- content.setWeigher(taskInfo.getWeigherName());
|
|
|
-
|
|
|
- // --- 司机信息 (需判空) ---
|
|
|
- // 设置司机姓名
|
|
|
- content.setDriverName(driverInfo == null ? null : driverInfo.getName());
|
|
|
- // 设置司机手机号
|
|
|
- content.setDriverMobile(driverInfo == null ? null : driverInfo.getPhone());
|
|
|
- // 设置司机身份证号
|
|
|
- content.setDriverIdCard(driverInfo == null ? null : driverInfo.getIdCard());
|
|
|
-
|
|
|
- // --- 货物/物料信息 (需判空) ---
|
|
|
- // 设置物料名称
|
|
|
- content.setMaterialName(goodsInfo == null ? null : goodsInfo.getMaterialName());
|
|
|
- // 设置物料规格
|
|
|
- content.setMaterialSpec(goodsInfo == null ? null : goodsInfo.getSpecification());
|
|
|
- // 设置任务重量:格式化重量数值与单位
|
|
|
- content.setTaskWeight(formatWeight(goodsInfo == null ? null : goodsInfo.getTaskAmount(),
|
|
|
- goodsInfo == null ? null : goodsInfo.getUnit()));
|
|
|
- // 设置重量汇总:包含皮重、毛重、净重的组合字符串
|
|
|
+ // 5. 填充基础业务信息(来自运输订单层级)
|
|
|
+ content.setCompanyName(transportInfo.getSupplierName()); // 供应商/公司名称
|
|
|
+ content.setCustomerName(transportInfo.getCustomerName()); // 客户名称
|
|
|
+
|
|
|
+ // 6. 填充任务层级信息
|
|
|
+ content.setTaskNo(taskInfo.getTaskNo()); // 任务单号
|
|
|
+ content.setAcceptTime(taskInfo.getAcceptTime()); // 接单时间
|
|
|
+ content.setFinishTime(taskInfo.getFinishTime()); // 完成时间
|
|
|
+ content.setWeigher(taskInfo.getWeigherName()); // 计重人姓名
|
|
|
+ content.setDestination(taskInfo.getDestination()); // 目的地
|
|
|
+
|
|
|
+ // 7. 填充司机信息(需判空处理)
|
|
|
+ content.setDriverName(driverInfo == null ? null : driverInfo.getName()); // 司机姓名
|
|
|
+ content.setDriverMobile(driverInfo == null ? null : driverInfo.getPhone()); // 司机手机号
|
|
|
+ content.setDriverIdCard(driverInfo == null ? null : driverInfo.getIdCard()); // 司机身份证
|
|
|
+
|
|
|
+ // 8. 填充货物及重量信息(需判空处理)
|
|
|
+ content.setMaterialName(goodsInfo == null ? null : goodsInfo.getMaterialName()); // 物料名称
|
|
|
+ content.setMaterialSpec(goodsInfo == null ? null : goodsInfo.getSpecification()); // 物料规格
|
|
|
+ // 格式化任务重量(数值+单位)
|
|
|
+ content.setTaskWeight(formatWeight(
|
|
|
+ goodsInfo == null ? null : goodsInfo.getTaskAmount(),
|
|
|
+ goodsInfo == null ? null : goodsInfo.getUnit()
|
|
|
+ ));
|
|
|
+ // 格式化重量汇总(皮重/毛重/净重)
|
|
|
content.setWeightSummary(formatWeightSummary(goodsInfo));
|
|
|
|
|
|
- // --- 车辆信息 (需判空) ---
|
|
|
- // 设置车牌号
|
|
|
- content.setPlateNo(truckInfo == null ? null : truckInfo.getTruckNo());
|
|
|
- // 设置车辆轴数描述
|
|
|
- content.setVehicleAxleDesc(truckInfo == null ? null : truckInfo.getTruckAxle());
|
|
|
+ // 9. 填充车辆信息(需判空处理)
|
|
|
+ content.setPlateNo(truckInfo == null ? null : truckInfo.getTruckNo()); // 车牌号
|
|
|
+ content.setVehicleAxleDesc(truckInfo == null ? null : truckInfo.getTruckAxle()); // 车辆轴数描述
|
|
|
|
|
|
// --- 其他信息 ---
|
|
|
// 设置目的地
|
|
|
@@ -377,6 +413,7 @@ public class XpCloudPrintService {
|
|
|
// 设置联次标签,固定为"客户联"
|
|
|
content.setCopyLabel("客户联");
|
|
|
|
|
|
+ // 11. 返回组装完成的内容对象
|
|
|
return content;
|
|
|
}
|
|
|
|
|
|
@@ -388,27 +425,24 @@ public class XpCloudPrintService {
|
|
|
* @return 任务信息
|
|
|
*/
|
|
|
/**
|
|
|
- * 根据任务单号从运输信息中解析具体的任务详情。
|
|
|
+ * 根据任务单号解析并匹配具体的任务信息。
|
|
|
* <p>
|
|
|
- * 处理逻辑:
|
|
|
- * 1. 获取任务列表,若为空则使用空列表防止 NPE。
|
|
|
- * 2. 校验任务列表非空,若为空则抛出异常。
|
|
|
- * 3. 优先尝试通过 taskNo 精确匹配任务。
|
|
|
- * 4. 若未匹配到指定任务,则降级策略:返回列表中的第一个有效任务(兜底逻辑)。
|
|
|
- * 5. 若列表中无任何有效任务,则抛出异常。
|
|
|
+ * 该方法优先尝试从运输信息列表中查找与给定 {@code taskNo} 完全匹配的任务。
|
|
|
+ * 如果未找到精确匹配的任务,则作为降级策略,返回列表中的第一个非空任务。
|
|
|
+ * 如果列表为空或所有任务均为 null,则抛出业务异常。
|
|
|
*
|
|
|
- * @param transportInfo 远程获取的运单运输详细信息
|
|
|
- * @param taskNo 需要匹配的任务单号
|
|
|
+ * @param transportInfo 远程获取的运输订单详细信息,包含任务列表
|
|
|
+ * @param taskNo 需要匹配的目标任务单号
|
|
|
* @return 匹配到的任务信息对象
|
|
|
- * @throws IotException 当任务列表为空或无法找到任何有效任务时抛出
|
|
|
+ * @throws IotException 当任务列表为空、无有效任务或无法找到任何可用任务时抛出
|
|
|
*/
|
|
|
private TradeOrderTransportInfoResp.TaskInfo resolveTaskInfo(TradeOrderTransportInfoResp transportInfo, String taskNo) {
|
|
|
- // 1. 安全获取任务列表,避免空指针异常
|
|
|
+ // 1. 安全获取任务列表:若运输信息中的任务列表为 null,则初始化为空列表,避免 NPE
|
|
|
List<TradeOrderTransportInfoResp.TaskInfo> tasks = transportInfo.getTasks() == null
|
|
|
? Collections.emptyList()
|
|
|
: transportInfo.getTasks();
|
|
|
|
|
|
- // 2. 基础校验:确保任务列表不为空
|
|
|
+ // 2. 校验任务列表是否为空:若没有任何任务数据,直接抛出异常
|
|
|
if (tasks.isEmpty()) {
|
|
|
throw new IotException("小票任务信息不能为空");
|
|
|
}
|
|
|
@@ -420,9 +454,11 @@ public class XpCloudPrintService {
|
|
|
return tasks.stream()
|
|
|
.filter(taskInfo -> taskInfo != null && StringUtils.equals(taskNo, taskInfo.getTaskNo()))
|
|
|
.findFirst()
|
|
|
+ // 4. 降级策略:若未找到精确匹配的任务,则取列表中第一个非空的任务作为默认值
|
|
|
.orElse(tasks.stream()
|
|
|
.filter(Objects::nonNull)
|
|
|
.findFirst()
|
|
|
+ // 5. 最终兜底:若连一个非空任务都找不到,则抛出异常
|
|
|
.orElseThrow(() -> new IotException("小票任务信息不能为空")));
|
|
|
}
|
|
|
|
|
|
@@ -523,30 +559,28 @@ public class XpCloudPrintService {
|
|
|
/**
|
|
|
* 构建芯烨开放平台鉴权三元组:user、10 位秒级时间戳、sign(SHA1 小写 40 位)。
|
|
|
* <p>
|
|
|
- * 签名算法说明:
|
|
|
- * sign = SHA1(user + userKey + timestamp)
|
|
|
- * 其中 userKey 为开发者密钥,timestamp 为当前时间的秒级时间戳。
|
|
|
+ * 该方法用于生成调用芯烨云打印接口所需的身份验证信息。
|
|
|
+ * 签名算法为:SHA1(user + userKey + timestamp),确保请求的合法性和时效性。
|
|
|
*
|
|
|
* @return 包含 [user, timestamp, sign] 的字符串数组
|
|
|
- * @throws IotException 当配置项 user 或 userKey 缺失时抛出
|
|
|
+ * @throws IotException 当配置文件中缺失 user 或 userKey 时抛出异常
|
|
|
*/
|
|
|
private String[] buildXpAuthTriple() {
|
|
|
- // 1. 校验核心配置项是否完整,避免后续请求因缺少鉴权信息而失败
|
|
|
+ // 1. 校验核心配置项:确保用户名和用户密钥已正确配置,避免后续签名计算失败
|
|
|
if (StringUtils.isAnyBlank(xpCloudProperties.getUser(), xpCloudProperties.getUserKey())) {
|
|
|
throw new IotException("芯烨配置不完整,请配置xp.dev.user和xp.dev.user-key");
|
|
|
}
|
|
|
|
|
|
- // 2. 获取配置中的用户标识 (user)
|
|
|
+ // 2. 获取配置中的用户标识
|
|
|
String user = xpCloudProperties.getUser();
|
|
|
|
|
|
- // 3. 生成当前时间的秒级时间戳 (timestamp),用于防止重放攻击及服务器端校验时效性
|
|
|
+ // 3. 生成当前时间的秒级时间戳(10位),用于防止重放攻击及满足接口时效要求
|
|
|
String timestamp = String.valueOf(Instant.now().getEpochSecond());
|
|
|
|
|
|
- // 4. 计算签名 (sign)
|
|
|
- // 拼接规则:user + userKey + timestamp,然后进行 SHA1 哈希计算并转为小写十六进制字符串
|
|
|
+ // 4. 计算签名:按照芯烨云规范,将 user、userKey 和 timestamp 拼接后进行 SHA1 哈希计算
|
|
|
String sign = sha1Hex(user + xpCloudProperties.getUserKey() + timestamp);
|
|
|
|
|
|
- // 5. 返回鉴权所需的三个关键参数
|
|
|
+ // 5. 返回鉴权三元组,供上层业务组装请求参数使用
|
|
|
return new String[]{user, timestamp, sign};
|
|
|
}
|
|
|
|
|
|
@@ -560,56 +594,56 @@ public class XpCloudPrintService {
|
|
|
/**
|
|
|
* 构建芯烨云小票打印接口的 JSON 请求体。
|
|
|
* <p>
|
|
|
- * 该方法负责组装调用芯烨云开放平台 {@code /xprinter/print} 接口所需的所有参数,
|
|
|
- * 包括鉴权信息、打印机标识、打印内容以及业务控制参数。
|
|
|
+ * 该方法将业务参数、鉴权信息、打印内容及配置项组装成符合芯烨云开放平台规范的 JSON 对象。
|
|
|
+ * 包含用户鉴权(user/timestamp/sign)、打印机序列号、打印内容(XML格式)、份数、模式等核心字段,
|
|
|
+ * 并根据配置动态添加调试标识及语音播报设置。
|
|
|
*
|
|
|
- * @param reqVo 小票打印业务请求参数,包含打印机 SN、份数、模式、幂等键等
|
|
|
- * @param xmlContent 已组装好的小票 XML 格式打印内容
|
|
|
- * @return 封装好的 JSON 对象,可直接用于 HTTP POST 请求
|
|
|
+ * @param reqVo 小票打印业务请求参数,包含打印机SN、份数、模式、幂等键等
|
|
|
+ * @param xmlContent 已组装好的小票打印 XML 内容字符串
|
|
|
+ * @return 完整的 JSON 请求对象,可直接用于 HTTP POST 请求
|
|
|
*/
|
|
|
private JSONObject buildReceiptPrintJsonBody(XpPrintReceiptReqVo reqVo, String xmlContent) {
|
|
|
- // 1. 初始化 JSON 对象,使用有序 Map 保持字段插入顺序(可选,便于调试查看)
|
|
|
+ // 1. 初始化 JSON 对象,使用 LinkedHashMap 保持插入顺序(可选,便于日志查看)
|
|
|
JSONObject body = new JSONObject(true);
|
|
|
|
|
|
- // 2. 获取鉴权三元组:user, timestamp, sign
|
|
|
- // 签名算法基于 user + userKey + timestamp 的 SHA1 哈希
|
|
|
+ // 2. 获取鉴权三元组(user, timestamp, sign)并填入请求体
|
|
|
String[] auth = buildXpAuthTriple();
|
|
|
- body.put("user", auth[0]);
|
|
|
- body.put("timestamp", auth[1]);
|
|
|
- body.put("sign", auth[2]);
|
|
|
+ body.put("user", auth[0]); // 开发者账号
|
|
|
+ body.put("timestamp", auth[1]); // 时间戳
|
|
|
+ body.put("sign", auth[2]); // 签名
|
|
|
|
|
|
- // 3. 调试模式开关:如果配置中开启了 debug,则添加 debug 参数
|
|
|
+ // 3. 根据配置决定是否开启调试模式
|
|
|
if (Boolean.TRUE.equals(xpCloudProperties.getDebug())) {
|
|
|
body.put("debug", "1");
|
|
|
}
|
|
|
|
|
|
- // 4. 核心业务参数
|
|
|
- // 打印机设备编号 (SN)
|
|
|
- body.put("sn", reqVo.getSn());
|
|
|
- // 打印内容,通常为 XML 或 ESC/POS 指令字符串
|
|
|
- body.put("content", xmlContent);
|
|
|
+ // 4. 填充核心业务参数
|
|
|
+ body.put("sn", reqVo.getSn()); // 打印机终端编号
|
|
|
+ body.put("content", xmlContent); // 打印内容(XML 格式)
|
|
|
|
|
|
- // 5. 打印份数:默认为 1 份,若请求中指定则使用指定值
|
|
|
+ // 5. 处理打印份数:若未指定则默认为 1 份
|
|
|
int copies = reqVo.getCopies() != null ? reqVo.getCopies() : 1;
|
|
|
body.put("copies", copies);
|
|
|
|
|
|
- // 6. 可选业务参数:仅在非空时添加,避免发送无用字段
|
|
|
- // 打印模式:0-云端模式(默认), 1-离线模式(需配合 expiresIn)
|
|
|
+ // 6. 可选参数:打印模式(0:云端排队, 1:即时打印)
|
|
|
if (reqVo.getMode() != null) {
|
|
|
body.put("mode", reqVo.getMode());
|
|
|
}
|
|
|
- // 幂等键:防止网络重试导致的重复打印任务
|
|
|
+
|
|
|
+ // 7. 可选参数:幂等性标识,防止网络重试导致的重复打印
|
|
|
if (StringUtils.isNotBlank(reqVo.getIdempotent())) {
|
|
|
body.put("idempotent", reqVo.getIdempotent());
|
|
|
}
|
|
|
- // 过期时间:仅在离线模式(mode=1)下有效,单位秒
|
|
|
+
|
|
|
+ // 8. 可选参数:订单过期时间(秒),仅在 mode=1 时有效
|
|
|
if (reqVo.getExpiresIn() != null) {
|
|
|
body.put("expiresIn", reqVo.getExpiresIn());
|
|
|
}
|
|
|
|
|
|
- // 7. 语音播报设置:固定设置为 "2" (具体含义参考芯烨文档,通常为开启或特定提示音)
|
|
|
+ // 9. 固定设置:语音播报类型(2:不播报,具体值参考芯烨文档)
|
|
|
body.put("voice", "2");
|
|
|
|
|
|
+ // 10. 返回组装完成的请求体
|
|
|
return body;
|
|
|
}
|
|
|
|
|
|
@@ -633,67 +667,74 @@ public class XpCloudPrintService {
|
|
|
* - 检查返回的数据字段 (data) 即订单号是否非空。
|
|
|
* 5. 异常处理:捕获并重新抛出业务异常 (IotException),或捕获其他异常记录日志后包装抛出。
|
|
|
*
|
|
|
- * @param body 包含鉴权信息、打印机 SN、打印内容等的 JSON 请求体
|
|
|
- * @return 芯烨云平台返回的打印订单号 (orderNo)
|
|
|
- * @throws IotException 当配置缺失、HTTP 请求失败、业务返回错误或订单号为空时抛出
|
|
|
+ * @param body 包含鉴权信息及打印内容的 JSON 请求体
|
|
|
+ * @return 芯烨云返回的打印订单号
|
|
|
+ * @throws IotException 当配置缺失、HTTP 请求失败、业务返回错误或数据为空时抛出异常
|
|
|
*/
|
|
|
private String postReceiptPrint(JSONObject body) {
|
|
|
- // 1. 获取并校验打印接口地址配置
|
|
|
+ // 1. 获取配置中的小票打印接口地址
|
|
|
String url = xpCloudProperties.getReceiptPrintUrl();
|
|
|
+
|
|
|
+ // 2. 校验接口地址是否已配置,避免空指针或无效请求
|
|
|
if (StringUtils.isBlank(url)) {
|
|
|
throw new IotException("芯烨云小票打印接口地址未配置,请配置 xp.dev.receipt-print-url");
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
- // 2. 构建 HTTP 请求头,指定内容类型为 JSON UTF-8
|
|
|
+ // 3. 构建 HTTP 请求头,指定内容类型为 application/json,编码为 UTF-8
|
|
|
HttpHeaders headers = new HttpHeaders();
|
|
|
headers.setContentType(new MediaType("application", "json", StandardCharsets.UTF_8));
|
|
|
|
|
|
- // 3. 将 JSON 对象转换为字符串并封装为 HttpEntity
|
|
|
+ // 4. 将 JSON 对象转换为字符串,并封装为 HttpEntity
|
|
|
HttpEntity<String> entity = new HttpEntity<>(body.toJSONString(), headers);
|
|
|
|
|
|
- // 4. 执行 POST 请求,获取原始字符串响应
|
|
|
+ // 5. 使用 RestTemplate 发送 POST 请求,接收字符串类型的响应
|
|
|
ResponseEntity<String> responseEntity = xpOpenApiRestTemplate.postForEntity(url, entity, String.class);
|
|
|
|
|
|
- // 5. 记录完整响应日志,便于调试和排查问题
|
|
|
+ // 6. 记录完整响应日志,便于后续排查问题(注意:生产环境若响应过大需评估性能影响)
|
|
|
log.info("芯烨云小票打印响应参数: {}", JSON.toJSONString(responseEntity));
|
|
|
|
|
|
- // 6. 校验 HTTP 状态码,确保请求在传输层成功
|
|
|
+ // 7. 校验 HTTP 状态码,确保是 2xx 成功状态
|
|
|
if (!responseEntity.getStatusCode().is2xxSuccessful()) {
|
|
|
throw new IotException("芯烨云小票打印HTTP异常, status=" + responseEntity.getStatusCode());
|
|
|
}
|
|
|
|
|
|
- // 7. 获取响应体内容
|
|
|
+ // 8. 获取响应体原始字符串
|
|
|
String raw = responseEntity.getBody();
|
|
|
+
|
|
|
+ // 9. 校验响应体是否为空
|
|
|
if (StringUtils.isBlank(raw)) {
|
|
|
throw new IotException("芯烨云小票打印返回空响应");
|
|
|
}
|
|
|
|
|
|
- // 8. 解析 JSON 响应结果
|
|
|
+ // 10. 将响应字符串解析为 JSONObject
|
|
|
JSONObject resp = JSON.parseObject(raw);
|
|
|
+
|
|
|
+ // 11. 提取业务状态码和消息
|
|
|
Integer code = resp.getInteger("code");
|
|
|
String msg = resp.getString("msg");
|
|
|
|
|
|
- // 9. 校验业务状态码,0 表示成功,其他值表示业务错误
|
|
|
+ // 12. 校验业务状态码:code 不为 null 且等于 0 表示业务成功
|
|
|
if (code == null || code != 0) {
|
|
|
throw new IotException("调用芯烨云小票打印失败:" + msg);
|
|
|
}
|
|
|
|
|
|
- // 10. 提取并校验返回的订单号 (data 字段)
|
|
|
+ // 13. 提取业务数据字段(即打印订单号)
|
|
|
String data = resp.getString("data");
|
|
|
+
|
|
|
+ // 14. 校验订单号是否为空,确保后续流程有迹可循
|
|
|
if (StringUtils.isBlank(data)) {
|
|
|
throw new IotException("芯烨云小票打印成功但订单号为空");
|
|
|
}
|
|
|
|
|
|
- // 11. 返回成功的订单号
|
|
|
+ // 15. 返回成功的打印订单号
|
|
|
return data;
|
|
|
-
|
|
|
} catch (IotException e) {
|
|
|
// 业务异常直接抛出,保持原始错误信息
|
|
|
log.error("调用芯烨云小票打印业务异常, url={}", url, e);
|
|
|
throw new IotException(ErrorCodeEnum.INTERFACE_CALL_FAIL, e.getErrorMsg(), e);
|
|
|
} catch (Exception e) {
|
|
|
- // 捕获网络异常、JSON 解析异常等其他未预期异常
|
|
|
+ // 捕获网络异常、序列化异常等非业务异常,记录详细堆栈并转换为业务异常
|
|
|
log.error("芯烨云小票打印HTTP调用异常, url={}", url, e);
|
|
|
throw new IotException("调用芯烨云小票打印HTTP异常:" + e.getMessage());
|
|
|
}
|
|
|
@@ -703,13 +744,16 @@ public class XpCloudPrintService {
|
|
|
* 计算SHA1(小写16进制)
|
|
|
*/
|
|
|
/**
|
|
|
- * 计算字符串的 SHA-1 哈希值,并转换为小写十六进制字符串。
|
|
|
+ * 计算字符串的 SHA-1 哈希值,并返回小写十六进制字符串。
|
|
|
* <p>
|
|
|
- * 该方法用于生成芯烨云开放平台接口请求所需的签名 (sign)。
|
|
|
- * 签名算法通常为:SHA1(user + userKey + timestamp)。
|
|
|
+ * 该方法用于生成芯烨云开放平台接口调用所需的签名(sign)。
|
|
|
+ * 算法流程:
|
|
|
+ * 1. 使用 UTF-8 编码将输入字符串转换为字节数组。
|
|
|
+ * 2. 使用 SHA-1 算法对字节数组进行摘要计算。
|
|
|
+ * 3. 将摘要结果的每个字节转换为两位小写十六进制字符串并拼接。
|
|
|
*
|
|
|
- * @param source 待计算的原始字符串
|
|
|
- * @return 小写十六进制格式的 SHA-1 哈希字符串
|
|
|
+ * @param source 待计算签名的原始字符串(通常由 user + userKey + timestamp 拼接而成)
|
|
|
+ * @return SHA-1 哈希值的小写十六进制字符串表示(40位字符)
|
|
|
* @throws IotException 当 SHA-1 算法不可用或计算过程中发生异常时抛出
|
|
|
*/
|
|
|
private String sha1Hex(String source) {
|
|
|
@@ -717,19 +761,20 @@ public class XpCloudPrintService {
|
|
|
// 1. 获取 SHA-1 消息摘要实例
|
|
|
MessageDigest messageDigest = MessageDigest.getInstance("SHA-1");
|
|
|
|
|
|
- // 2. 使用 UTF-8 编码将字符串转换为字节数组,并计算摘要
|
|
|
+ // 2. 使用 UTF-8 编码将源字符串转换为字节数组,并进行摘要计算
|
|
|
byte[] digestBytes = messageDigest.digest(source.getBytes(StandardCharsets.UTF_8));
|
|
|
|
|
|
// 3. 将字节数组转换为小写十六进制字符串
|
|
|
StringBuilder hex = new StringBuilder(digestBytes.length * 2);
|
|
|
for (byte digestByte : digestBytes) {
|
|
|
- // %02x 确保每个字节都格式化为两位十六进制数,不足补零
|
|
|
+ // %02x 确保每个字节都转换为两位十六进制数,不足两位前补零
|
|
|
hex.append(String.format("%02x", digestByte));
|
|
|
}
|
|
|
|
|
|
+ // 4. 返回最终的十六进制签名字符串
|
|
|
return hex.toString();
|
|
|
} catch (Exception e) {
|
|
|
- // 捕获可能的 NoSuchAlgorithmException 或其他异常,包装为业务异常抛出
|
|
|
+ // 捕获 NoSuchAlgorithmException 等异常,转换为业务异常抛出
|
|
|
throw new IotException("SHA1计算失败:" + e.getMessage());
|
|
|
}
|
|
|
}
|