Browse Source

修改日志打印

chenxiaofei 1 tháng trước cách đây
mục cha
commit
7ee0022243

+ 317 - 43
iot-platform-manager/src/main/java/com/platform/service/XpCloudPrintService.java

@@ -5,6 +5,7 @@ import com.alibaba.fastjson.JSONObject;
 import com.platform.api.request.XpPrintImageReqVo;
 import com.platform.api.request.XpPrintReceiptReqVo;
 import com.platform.config.XpCloudProperties;
+import com.platform.enums.ErrorCodeEnum;
 import com.platform.exception.IotException;
 import com.platform.external.client.PrintReceiptContentClient;
 import com.platform.external.request.PrintReceiptContent;
@@ -73,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());
         }
@@ -105,32 +129,63 @@ public class XpCloudPrintService {
      * @param reqVo 小票订单打印请求
      * @return 云端打印任务号
      */
+    /**
+     * 调用芯烨云执行小票订单打印
+     *
+     * @param reqVo 小票订单打印请求,包含打印机SN、任务单号、打印模式等参数
+     * @return 云端打印任务订单号
+     * @throws IotException 当参数校验失败、获取内容异常或打印服务调用异常时抛出
+     */
     public String printReceipt(XpPrintReceiptReqVo reqVo) {
+        // 1. 记录入口日志,方便排查问题
         log.info("芯烨云小票打印, 入参:{}", JSON.toJSONString(reqVo));
+
+        // 2. 参数校验:检查请求对象及核心字段(打印机SN、任务单号)是否为空
         if (reqVo == null || StringUtils.isAnyBlank(reqVo.getSn(), reqVo.getTaskNo())) {
             throw new IotException("打印参数不能为空");
         }
+        // 3. 业务规则校验:如果设置了过期时间(expiresIn),则打印模式(mode)必须为1(即时打印)
         if (reqVo.getExpiresIn() != null && (reqVo.getMode() == null || reqVo.getMode() != 1)) {
             throw new IotException("设置expiresIn时,mode必须为1");
         }
+
         try {
+            // 4. 获取当前任务的下一次成功打印序号,用于生成“第N次打印”标识及防重
             int nextSuccessfulOrdinal = kwsPrintReceiptRecordRepository.resolveNextPrintOrdinalOrThrow(reqVo.getTaskNo());
+
+            // 5. 远程调用获取小票所需的运输业务数据
             TradeOrderTransportInfoResp transportInfo = getReceiptContent(reqVo.getTaskNo());
+
+            // 6. 将远程返回的业务数据组装成打印模板所需的内容对象
             PrintReceiptContent content = assembleReceiptContent(transportInfo, reqVo.getTaskNo());
+
+            // 7. 填充打印元数据:当前时间及打印次数描述
             LocalDateTime now = LocalDateTime.now();
             content.setPrintTimesDesc(formatPrintTimes(nextSuccessfulOrdinal));
             content.setPrintTime(now.format(DATE_TIME_FORMATTER));
+
+            // 8. 校验组装后的小票内容,确保关键字段不为空,避免打印出残缺小票
             validateReceiptContent(content);
+
+            // 9. 将内容对象转换为芯烨云支持的XML格式字符串
             String xmlContent = buildReceiptXml(content);
 
+            // 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"));
+            // 12. 仅在DEBUG级别记录完整请求体,避免生产环境日志过大
             if (log.isDebugEnabled()) {
                 log.debug("芯烨云小票打印HTTP请求体: {}", printBody.toJSONString());
             }
+
+            // 13. 发送HTTP请求调用芯烨云打印接口,获取云端订单号
             String orderNo = postReceiptPrint(printBody);
+
+            // 14. 持久化打印成功记录,用于后续查询及打印序号累加
             kwsPrintReceiptRecordRepository.saveSuccessfulReceiptPrint(
                     reqVo.getTaskNo(),
                     transportInfo.getTradeOrderNo(),
@@ -138,12 +193,18 @@ public class XpCloudPrintService {
                     xmlContent,
                     orderNo,
                     now);
+
+            // 15. 记录完成日志
             log.info("芯烨云小票打印完成, sn={}, orderNo={}, 已累计成功序号={}",
                     reqVo.getSn(), orderNo, nextSuccessfulOrdinal);
+
+            // 16. 返回云端订单号
             return orderNo;
         } catch (IotException e) {
+            // 业务异常直接抛出
             throw e;
         } catch (Exception e) {
+            // 捕获其他未知异常,记录错误日志并转换为业务异常抛出
             log.error("调用芯烨云小票打印异常, sn={}", reqVo.getSn(), e);
             throw new IotException("调用芯烨云小票打印异常:" + e.getMessage());
         }
@@ -155,26 +216,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());
         }
@@ -185,38 +273,57 @@ public class XpCloudPrintService {
      *
      * @param content 小票内容
      */
+    /**
+     * 校验小票打印内容的关键字段完整性。
+     * <p>
+     * 该方法会检查 {@link PrintReceiptContent} 对象中所有必填字段是否为空或空白。
+     * 如果发现任何必填字段缺失,将抛出 {@link IotException} 异常,以防止生成内容残缺的小票。
+     *
+     * @param content 待校验的小票内容对象
+     * @throws IotException 当内容对象为空或包含空白的必填字段时抛出
+     */
     private void validateReceiptContent(PrintReceiptContent content) {
+        // 1. 基础非空校验:确保传入的内容对象本身不为 null
         if (content == null) {
             throw new IotException("小票内容不能为空");
         }
+
+        // 2. 定义必填字段列表:使用 SimpleEntry 映射字段名与其对应的值,便于后续定位缺失字段
         List<AbstractMap.SimpleEntry<String, String>> requiredFields = List.of(
-                new AbstractMap.SimpleEntry<>("companyName", content.getCompanyName()),
-                new AbstractMap.SimpleEntry<>("customerName", content.getCustomerName()),
-                new AbstractMap.SimpleEntry<>("taskNo", content.getTaskNo()),
-                new AbstractMap.SimpleEntry<>("acceptTime", content.getAcceptTime()),
-                new AbstractMap.SimpleEntry<>("finishTime", content.getFinishTime()),
-                new AbstractMap.SimpleEntry<>("weigher", content.getWeigher()),
-                new AbstractMap.SimpleEntry<>("driverName", content.getDriverName()),
-                new AbstractMap.SimpleEntry<>("driverMobile", content.getDriverMobile()),
-                new AbstractMap.SimpleEntry<>("driverIdCard", content.getDriverIdCard()),
-                new AbstractMap.SimpleEntry<>("materialName", content.getMaterialName()),
-                new AbstractMap.SimpleEntry<>("materialSpec", content.getMaterialSpec()),
-                new AbstractMap.SimpleEntry<>("taskWeight", content.getTaskWeight()),
-                new AbstractMap.SimpleEntry<>("weightSummary", content.getWeightSummary()),
-                new AbstractMap.SimpleEntry<>("plateNo", content.getPlateNo()),
-                new AbstractMap.SimpleEntry<>("vehicleAxleDesc", content.getVehicleAxleDesc()),
-                new AbstractMap.SimpleEntry<>("destination", content.getDestination()),
-                new AbstractMap.SimpleEntry<>("printTime", content.getPrintTime()),
-                new AbstractMap.SimpleEntry<>("printTimesDesc", content.getPrintTimesDesc()),
-                new AbstractMap.SimpleEntry<>("copyLabel", content.getCopyLabel())
+                new AbstractMap.SimpleEntry<>("companyName", content.getCompanyName()),       // 公司名称
+                new AbstractMap.SimpleEntry<>("customerName", content.getCustomerName()),     // 客户名称
+                new AbstractMap.SimpleEntry<>("taskNo", content.getTaskNo()),                 // 任务单号
+                new AbstractMap.SimpleEntry<>("acceptTime", content.getAcceptTime()),         // 接单时间
+                new AbstractMap.SimpleEntry<>("finishTime", content.getFinishTime()),         // 完成时间
+                new AbstractMap.SimpleEntry<>("weigher", content.getWeigher()),               // 计重人
+                new AbstractMap.SimpleEntry<>("driverName", content.getDriverName()),         // 司机姓名
+                new AbstractMap.SimpleEntry<>("driverMobile", content.getDriverMobile()),     // 司机手机号
+                new AbstractMap.SimpleEntry<>("driverIdCard", content.getDriverIdCard()),     // 司机身份证
+                new AbstractMap.SimpleEntry<>("materialName", content.getMaterialName()),     // 物料名称
+                new AbstractMap.SimpleEntry<>("materialSpec", content.getMaterialSpec()),     // 物料规格
+                new AbstractMap.SimpleEntry<>("taskWeight", content.getTaskWeight()),         // 任务重量
+                new AbstractMap.SimpleEntry<>("weightSummary", content.getWeightSummary()),   // 重量汇总(皮/毛/净)
+                new AbstractMap.SimpleEntry<>("plateNo", content.getPlateNo()),               // 车牌号
+                new AbstractMap.SimpleEntry<>("vehicleAxleDesc", content.getVehicleAxleDesc()), // 车辆轴数描述
+                new AbstractMap.SimpleEntry<>("destination", content.getDestination()),       // 目的地
+                new AbstractMap.SimpleEntry<>("printTime", content.getPrintTime()),           // 打印时间
+                new AbstractMap.SimpleEntry<>("printTimesDesc", content.getPrintTimesDesc()), // 打印次数描述
+                new AbstractMap.SimpleEntry<>("copyLabel", content.getCopyLabel())            // 联次标签(如客户联)
         );
+
+        // 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. 如果存在空白字段,则抛出业务异常,提示具体缺失的字段名
         if (StringUtils.isNotBlank(blankField)) {
-           // throw new IotException("小票内容字段不能为空:" + blankField);
+            throw new IotException("小票内容字段不能为空: " + blankField);
         }
     }
 
@@ -227,35 +334,71 @@ public class XpCloudPrintService {
      * @param taskNo 任务单号
      * @return 小票打印模板参数
      */
+    /**
+     * 将远程运输信息重新组装为小票打印模板参数。
+     * <p>
+     * 该方法从 {@link TradeOrderTransportInfoResp} 中提取任务、司机、货物、车辆等关键业务数据,
+     * 并格式化为 {@link PrintReceiptContent} 对象,供后续生成小票 XML 使用。
+     *
+     * @param transportInfo 远程获取的运输订单详细信息
+     * @param taskNo        当前需要打印的任务单号,用于匹配具体的任务信息
+     * @return 组装好的小票打印内容对象
+     * @throws IotException 当运输信息为空时抛出异常
+     */
     private PrintReceiptContent assembleReceiptContent(TradeOrderTransportInfoResp transportInfo, String taskNo) {
+        // 1. 基础校验:确保传入的运输信息对象不为 null
         if (transportInfo == null) {
             throw new IotException("小票运输信息不能为空");
         }
+
+        // 2. 解析任务信息:根据 taskNo 从运输信息中匹配对应的任务详情
         TradeOrderTransportInfoResp.TaskInfo taskInfo = resolveTaskInfo(transportInfo, taskNo);
+
+        // 3. 提取子模块信息:司机、货物、车辆、打印信息(可能为 null,需做空指针保护)
         TradeOrderTransportInfoResp.DriverInfo driverInfo = taskInfo.getDriverInfo();
         TradeOrderTransportInfoResp.GoodsInfo goodsInfo = taskInfo.getGoodsInfo();
         TradeOrderTransportInfoResp.TruckInfo truckInfo = taskInfo.getTruckInfo();
+        // 注:printInfo 在当前逻辑中未直接使用,但保留获取以便后续扩展
         TradeOrderTransportInfoResp.PrintInfo printInfo = taskInfo.getPrintInfo();
 
+        // 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());
-        content.setDestination(taskInfo.getDestination());
+
+        // 9. 填充车辆信息(需判空处理)
+        content.setPlateNo(truckInfo == null ? null : truckInfo.getTruckNo());       // 车牌号
+        content.setVehicleAxleDesc(truckInfo == null ? null : truckInfo.getTruckAxle()); // 车辆轴数描述
+
+        // 10. 设置固定标识:联次标签
         content.setCopyLabel("客户联");
+
+        // 11. 返回组装完成的内容对象
         return content;
     }
 
@@ -266,19 +409,38 @@ public class XpCloudPrintService {
      * @param taskNo 任务单号
      * @return 任务信息
      */
+    /**
+     * 根据任务单号解析并匹配具体的任务信息。
+     * <p>
+     * 该方法优先尝试从运输信息列表中查找与给定 {@code taskNo} 完全匹配的任务。
+     * 如果未找到精确匹配的任务,则作为降级策略,返回列表中的第一个非空任务。
+     * 如果列表为空或所有任务均为 null,则抛出业务异常。
+     *
+     * @param transportInfo 远程获取的运输订单详细信息,包含任务列表
+     * @param taskNo        需要匹配的目标任务单号
+     * @return 匹配到的任务信息对象
+     * @throws IotException 当任务列表为空、无有效任务或无法找到任何可用任务时抛出
+     */
     private TradeOrderTransportInfoResp.TaskInfo resolveTaskInfo(TradeOrderTransportInfoResp transportInfo, String taskNo) {
+        // 1. 安全获取任务列表:若运输信息中的任务列表为 null,则初始化为空列表,避免 NPE
         List<TradeOrderTransportInfoResp.TaskInfo> tasks = transportInfo.getTasks() == null
                 ? Collections.emptyList()
                 : transportInfo.getTasks();
+
+        // 2. 校验任务列表是否为空:若没有任何任务数据,直接抛出异常
         if (tasks.isEmpty()) {
             throw new IotException("小票任务信息不能为空");
         }
+
+        // 3. 优先精确匹配:在任务流中查找任务单号与入参 taskNo 一致且非空的任务
         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("小票任务信息不能为空")));
     }
 
@@ -376,13 +538,31 @@ public class XpCloudPrintService {
      *
      * @return [user, timestamp, sign]
      */
+    /**
+     * 构建芯烨开放平台鉴权三元组:user、10 位秒级时间戳、sign(SHA1 小写 40 位)。
+     * <p>
+     * 该方法用于生成调用芯烨云打印接口所需的身份验证信息。
+     * 签名算法为:SHA1(user + userKey + timestamp),确保请求的合法性和时效性。
+     *
+     * @return 包含 [user, timestamp, sign] 的字符串数组
+     * @throws IotException 当配置文件中缺失 user 或 userKey 时抛出异常
+     */
     private String[] buildXpAuthTriple() {
+        // 1. 校验核心配置项:确保用户名和用户密钥已正确配置,避免后续签名计算失败
         if (StringUtils.isAnyBlank(xpCloudProperties.getUser(), xpCloudProperties.getUserKey())) {
             throw new IotException("芯烨配置不完整,请配置xp.dev.user和xp.dev.user-key");
         }
+
+        // 2. 获取配置中的用户标识
         String user = xpCloudProperties.getUser();
+
+        // 3. 生成当前时间的秒级时间戳(10位),用于防止重放攻击及满足接口时效要求
         String timestamp = String.valueOf(Instant.now().getEpochSecond());
+
+        // 4. 计算签名:按照芯烨云规范,将 user、userKey 和 timestamp 拼接后进行 SHA1 哈希计算
         String sign = sha1Hex(user + xpCloudProperties.getUserKey() + timestamp);
+
+        // 5. 返回鉴权三元组,供上层业务组装请求参数使用
         return new String[]{user, timestamp, sign};
     }
 
@@ -393,29 +573,59 @@ public class XpCloudPrintService {
      * @param xmlContent  模板 XML 正文
      * @return JSON 请求对象
      */
+    /**
+     * 构建芯烨云小票打印接口的 JSON 请求体。
+     * <p>
+     * 该方法将业务参数、鉴权信息、打印内容及配置项组装成符合芯烨云开放平台规范的 JSON 对象。
+     * 包含用户鉴权(user/timestamp/sign)、打印机序列号、打印内容(XML格式)、份数、模式等核心字段,
+     * 并根据配置动态添加调试标识及语音播报设置。
+     *
+     * @param reqVo      小票打印业务请求参数,包含打印机SN、份数、模式、幂等键等
+     * @param xmlContent 已组装好的小票打印 XML 内容字符串
+     * @return 完整的 JSON 请求对象,可直接用于 HTTP POST 请求
+     */
     private JSONObject buildReceiptPrintJsonBody(XpPrintReceiptReqVo reqVo, String xmlContent) {
+        // 1. 初始化 JSON 对象,使用 LinkedHashMap 保持插入顺序(可选,便于日志查看)
         JSONObject body = new JSONObject(true);
+
+        // 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. 根据配置决定是否开启调试模式
         if (Boolean.TRUE.equals(xpCloudProperties.getDebug())) {
             body.put("debug", "1");
         }
-        body.put("sn", reqVo.getSn());
-        body.put("content", xmlContent);
+
+        // 4. 填充核心业务参数
+        body.put("sn", reqVo.getSn());       // 打印机终端编号
+        body.put("content", xmlContent);     // 打印内容(XML 格式)
+
+        // 5. 处理打印份数:若未指定则默认为 1 份
         int copies = reqVo.getCopies() != null ? reqVo.getCopies() : 1;
         body.put("copies", copies);
+
+        // 6. 可选参数:打印模式(0:云端排队, 1:即时打印)
         if (reqVo.getMode() != null) {
             body.put("mode", reqVo.getMode());
         }
+
+        // 7. 可选参数:幂等性标识,防止网络重试导致的重复打印
         if (StringUtils.isNotBlank(reqVo.getIdempotent())) {
             body.put("idempotent", reqVo.getIdempotent());
         }
+
+        // 8. 可选参数:订单过期时间(秒),仅在 mode=1 时有效
         if (reqVo.getExpiresIn() != null) {
             body.put("expiresIn", reqVo.getExpiresIn());
         }
-        body.put("voice","2");
+
+        // 9. 固定设置:语音播报类型(2:不播报,具体值参考芯烨文档)
+        body.put("voice", "2");
+
+        // 10. 返回组装完成的请求体
         return body;
     }
 
@@ -425,38 +635,80 @@ public class XpCloudPrintService {
      * @param body 请求 JSON
      * @return {@code data} 字段订单号
      */
+    /**
+     * 发送 HTTP POST 请求调用芯烨云小票打印接口,并解析返回的订单号。
+     * <p>
+     * 该方法负责与芯烨云开放平台进行通信,发送组装好的 JSON 请求体,
+     * 并对响应状态码、业务代码及返回数据进行严格校验,确保打印任务成功提交。
+     *
+     * @param body 包含鉴权信息及打印内容的 JSON 请求体
+     * @return 芯烨云返回的打印订单号
+     * @throws IotException 当配置缺失、HTTP 请求失败、业务返回错误或数据为空时抛出异常
+     */
     private String postReceiptPrint(JSONObject body) {
+        // 1. 获取配置中的小票打印接口地址
         String url = xpCloudProperties.getReceiptPrintUrl();
+
+        // 2. 校验接口地址是否已配置,避免空指针或无效请求
         if (StringUtils.isBlank(url)) {
             throw new IotException("芯烨云小票打印接口地址未配置,请配置 xp.dev.receipt-print-url");
         }
+
         try {
+            // 3. 构建 HTTP 请求头,指定内容类型为 application/json,编码为 UTF-8
             HttpHeaders headers = new HttpHeaders();
             headers.setContentType(new MediaType("application", "json", StandardCharsets.UTF_8));
+
+            // 4. 将 JSON 对象转换为字符串,并封装为 HttpEntity
             HttpEntity<String> entity = new HttpEntity<>(body.toJSONString(), headers);
+
+            // 5. 使用 RestTemplate 发送 POST 请求,接收字符串类型的响应
             ResponseEntity<String> responseEntity = xpOpenApiRestTemplate.postForEntity(url, entity, String.class);
+
+            // 6. 记录完整响应日志,便于后续排查问题(注意:生产环境若响应过大需评估性能影响)
             log.info("芯烨云小票打印响应参数: {}", JSON.toJSONString(responseEntity));
+
+            // 7. 校验 HTTP 状态码,确保是 2xx 成功状态
             if (!responseEntity.getStatusCode().is2xxSuccessful()) {
                 throw new IotException("芯烨云小票打印HTTP异常, status=" + responseEntity.getStatusCode());
             }
+
+            // 8. 获取响应体原始字符串
             String raw = responseEntity.getBody();
+
+            // 9. 校验响应体是否为空
             if (StringUtils.isBlank(raw)) {
                 throw new IotException("芯烨云小票打印返回空响应");
             }
+
+            // 10. 将响应字符串解析为 JSONObject
             JSONObject resp = JSON.parseObject(raw);
+
+            // 11. 提取业务状态码和消息
             Integer code = resp.getInteger("code");
             String msg = resp.getString("msg");
+
+            // 12. 校验业务状态码:code 不为 null 且等于 0 表示业务成功
             if (code == null || code != 0) {
                 throw new IotException("调用芯烨云小票打印失败:" + msg);
             }
+
+            // 13. 提取业务数据字段(即打印订单号)
             String data = resp.getString("data");
+
+            // 14. 校验订单号是否为空,确保后续流程有迹可循
             if (StringUtils.isBlank(data)) {
                 throw new IotException("芯烨云小票打印成功但订单号为空");
             }
+
+            // 15. 返回成功的打印订单号
             return data;
         } catch (IotException e) {
-            throw e;
+            // 业务异常直接抛出,保持原始错误信息
+            log.error("调用芯烨云小票打印业务异常, url={}", url, e);
+            throw new IotException(ErrorCodeEnum.INTERFACE_CALL_FAIL, e.getErrorMsg(), e);
         } catch (Exception e) {
+            // 捕获网络异常、序列化异常等非业务异常,记录详细堆栈并转换为业务异常
             log.error("芯烨云小票打印HTTP调用异常, url={}", url, e);
             throw new IotException("调用芯烨云小票打印HTTP异常:" + e.getMessage());
         }
@@ -465,16 +717,38 @@ public class XpCloudPrintService {
     /**
      * 计算SHA1(小写16进制)
      */
+    /**
+     * 计算字符串的 SHA-1 哈希值,并返回小写十六进制字符串。
+     * <p>
+     * 该方法用于生成芯烨云开放平台接口调用所需的签名(sign)。
+     * 算法流程:
+     * 1. 使用 UTF-8 编码将输入字符串转换为字节数组。
+     * 2. 使用 SHA-1 算法对字节数组进行摘要计算。
+     * 3. 将摘要结果的每个字节转换为两位小写十六进制字符串并拼接。
+     *
+     * @param source 待计算签名的原始字符串(通常由 user + userKey + timestamp 拼接而成)
+     * @return SHA-1 哈希值的小写十六进制字符串表示(40位字符)
+     * @throws IotException 当 SHA-1 算法不可用或计算过程中发生异常时抛出
+     */
     private String sha1Hex(String source) {
         try {
+            // 1. 获取 SHA-1 消息摘要实例
             MessageDigest messageDigest = MessageDigest.getInstance("SHA-1");
+            
+            // 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 确保每个字节都转换为两位十六进制数,不足两位前补零
                 hex.append(String.format("%02x", digestByte));
             }
+            
+            // 4. 返回最终的十六进制签名字符串
             return hex.toString();
         } catch (Exception e) {
+            // 捕获 NoSuchAlgorithmException 等异常,转换为业务异常抛出
             throw new IotException("SHA1计算失败:" + e.getMessage());
         }
     }