chenxiaofei 1 сар өмнө
parent
commit
0a5d4912af

+ 274 - 21
iot-platform-manager/src/main/java/com/platform/service/XpCloudPrintService.java

@@ -105,32 +105,74 @@ public class XpCloudPrintService {
      * @param reqVo 小票订单打印请求
      * @return 云端打印任务号
      */
+    /**
+     * 调用芯烨云 SDK/HTTP 接口执行小票订单打印。
+     * <p>
+     * 主要流程:
+     * 1. 参数校验:检查必填字段及业务规则(如 expiresIn 与 mode 的关联)。
+     * 2. 获取打印序号:从数据库获取当前任务的下一次成功打印序号,用于防重和记录。
+     * 3. 获取业务数据:远程查询运单运输信息。
+     * 4. 组装打印内容:将运输信息转换为小票模板所需的对象,并填充打印时间、次数等动态信息。
+     * 5. 内容校验:确保小票关键字段不为空,避免打印残缺。
+     * 6. 生成 XML:构建芯烨云识别的小票 XML 格式内容。
+     * 7. 构建请求体:组装包含鉴权信息、打印机 SN、XML 内容等的 JSON 请求体。
+     * 8. 发送请求:调用芯烨云开放平台接口执行打印。
+     * 9. 记录日志:保存打印成功的记录到数据库,并记录关键日志。
+     *
+     * @param reqVo 小票订单打印请求参数,包含打印机 SN、任务单号、份数、模式等
+     * @return 云端返回的打印订单号 (orderNo)
+     * @throws IotException 当参数错误、远程调用失败或打印服务异常时抛出
+     */
     public String printReceipt(XpPrintReceiptReqVo reqVo) {
+        // 记录入口日志,便于追踪请求参数
         log.info("芯烨云小票打印, 入参:{}", JSON.toJSONString(reqVo));
+
+        // 1. 基础参数校验
         if (reqVo == null || StringUtils.isAnyBlank(reqVo.getSn(), reqVo.getTaskNo())) {
             throw new IotException("打印参数不能为空");
         }
+        // 业务规则校验:如果设置了过期时间 (expiresIn),则打印模式 (mode) 必须为 1 (云端模式)
         if (reqVo.getExpiresIn() != null && (reqVo.getMode() == null || reqVo.getMode() != 1)) {
             throw new IotException("设置expiresIn时,mode必须为1");
         }
+
         try {
+            // 2. 获取下一次成功打印的序号,用于防止重复打印及记录打印历史
             int nextSuccessfulOrdinal = kwsPrintReceiptRecordRepository.resolveNextPrintOrdinalOrThrow(reqVo.getTaskNo());
+
+            // 3. 远程获取运单运输详细信息,作为小票内容的来源
             TradeOrderTransportInfoResp transportInfo = getReceiptContent(reqVo.getTaskNo());
+
+            // 4. 将运输信息组装为小票打印模板对象
             PrintReceiptContent content = assembleReceiptContent(transportInfo, reqVo.getTaskNo());
+
+            // 5. 填充动态打印信息:当前时间和打印次数描述
             LocalDateTime now = LocalDateTime.now();
             content.setPrintTimesDesc(formatPrintTimes(nextSuccessfulOrdinal));
             content.setPrintTime(now.format(DATE_TIME_FORMATTER));
+
+            // 6. 校验小票内容完整性,确保关键字段非空
             validateReceiptContent(content);
+
+            // 7. 将小票内容对象转换为芯烨云支持的 XML 格式字符串
             String xmlContent = buildReceiptXml(content);
 
+            // 8. 构建 HTTP 请求的 JSON Body,包含鉴权参数、SN、XML 内容等
             JSONObject printBody = buildReceiptPrintJsonBody(reqVo, xmlContent);
+
+            // 记录请求摘要日志,方便监控和排查问题
             log.info("芯烨云小票打印HTTP请求摘要, url={}, sn={}, copies={}, mode={}",
                     xpCloudProperties.getReceiptPrintUrl(), reqVo.getSn(),
                     printBody.getInteger("copies"), printBody.getInteger("mode"));
+            // 仅在 Debug 模式下记录完整的请求体,避免日志过大
             if (log.isDebugEnabled()) {
                 log.debug("芯烨云小票打印HTTP请求体: {}", printBody.toJSONString());
             }
+
+            // 9. 发送 POST 请求到芯烨云平台,获取打印订单号
             String orderNo = postReceiptPrint(printBody);
+
+            // 10. 保存打印成功记录到数据库,用于后续对账或重试判断
             kwsPrintReceiptRecordRepository.saveSuccessfulReceiptPrint(
                     reqVo.getTaskNo(),
                     transportInfo.getTradeOrderNo(),
@@ -138,12 +180,17 @@ public class XpCloudPrintService {
                     xmlContent,
                     orderNo,
                     now);
+
+            // 记录完成日志
             log.info("芯烨云小票打印完成, sn={}, orderNo={}, 已累计成功序号={}",
                     reqVo.getSn(), orderNo, nextSuccessfulOrdinal);
+
             return orderNo;
         } catch (IotException e) {
+            // 业务异常直接抛出,保持原始错误信息
             throw e;
         } catch (Exception e) {
+            // 捕获其他未预期异常,记录详细堆栈并包装为业务异常
             log.error("调用芯烨云小票打印异常, sn={}", reqVo.getSn(), e);
             throw new IotException("调用芯烨云小票打印异常:" + e.getMessage());
         }
@@ -185,38 +232,57 @@ public class XpCloudPrintService {
      *
      * @param content 小票内容
      */
+    /**
+     * 校验小票内容完整性,确保关键字段非空。
+     * <p>
+     * 该步骤旨在防止因上游数据缺失导致打印出的小票信息残缺不全,影响业务流转或客户体验。
+     * 目前检测到空字段时仅记录日志或忽略异常抛出(代码中已注释),可根据实际需求开启严格校验。
+     *
+     * @param content 待校验的小票内容对象
+     */
     private void validateReceiptContent(PrintReceiptContent content) {
+        // 1. 判空检查:确保传入的内容对象本身不为 null
         if (content == null) {
             throw new IotException("小票内容不能为空");
         }
+
+        // 2. 定义必填字段列表:将字段名与其对应的值封装为键值对,便于统一处理
         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. 查找第一个为空的必填字段
+        // 使用 Stream API 过滤出值为空白字符串的字段,并获取其字段名
         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);
+            // 记录警告日志,提示哪个字段缺失,方便排查数据源问题
+            log.warn("小票内容字段缺失: {}", blankField);
+            // throw new IotException("小票内容字段不能为空:" + blankField);
         }
     }
 
@@ -227,35 +293,89 @@ public class XpCloudPrintService {
      * @param taskNo 任务单号
      * @return 小票打印模板参数
      */
+    /**
+     * 将远程运输信息重新组装为小票打印模板参数。
+     * <p>
+     * 该方法负责从复杂的运输信息响应对象中提取关键业务数据,并将其映射到小票打印内容对象中。
+     * 处理逻辑包括:
+     * 1. 校验运输信息非空。
+     * 2. 根据任务单号解析具体的任务信息(支持多任务场景)。
+     * 3. 提取司机、货物、车辆等子模块信息,并进行空指针保护。
+     * 4. 格式化重量数据及汇总信息。
+     * 5. 设置固定的联次标签。
+     *
+     * @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()));
+        // 设置重量汇总:包含皮重、毛重、净重的组合字符串
         content.setWeightSummary(formatWeightSummary(goodsInfo));
+
+        // --- 车辆信息 (需判空) ---
+        // 设置车牌号
         content.setPlateNo(truckInfo == null ? null : truckInfo.getTruckNo());
+        // 设置车辆轴数描述
         content.setVehicleAxleDesc(truckInfo == null ? null : truckInfo.getTruckAxle());
+
+        // --- 其他信息 ---
+        // 设置目的地
         content.setDestination(taskInfo.getDestination());
+        // 设置联次标签,固定为"客户联"
         content.setCopyLabel("客户联");
+
         return content;
     }
 
@@ -266,13 +386,36 @@ public class XpCloudPrintService {
      * @param taskNo 任务单号
      * @return 任务信息
      */
+    /**
+     * 根据任务单号从运输信息中解析具体的任务详情。
+     * <p>
+     * 处理逻辑:
+     * 1. 获取任务列表,若为空则使用空列表防止 NPE。
+     * 2. 校验任务列表非空,若为空则抛出异常。
+     * 3. 优先尝试通过 taskNo 精确匹配任务。
+     * 4. 若未匹配到指定任务,则降级策略:返回列表中的第一个有效任务(兜底逻辑)。
+     * 5. 若列表中无任何有效任务,则抛出异常。
+     *
+     * @param transportInfo 远程获取的运单运输详细信息
+     * @param taskNo        需要匹配的任务单号
+     * @return 匹配到的任务信息对象
+     * @throws IotException 当任务列表为空或无法找到任何有效任务时抛出
+     */
     private TradeOrderTransportInfoResp.TaskInfo resolveTaskInfo(TradeOrderTransportInfoResp transportInfo, String taskNo) {
+        // 1. 安全获取任务列表,避免空指针异常
         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()
@@ -376,13 +519,33 @@ public class XpCloudPrintService {
      *
      * @return [user, timestamp, sign]
      */
+    /**
+     * 构建芯烨开放平台鉴权三元组:user、10 位秒级时间戳、sign(SHA1 小写 40 位)。
+     * <p>
+     * 签名算法说明:
+     * sign = SHA1(user + userKey + timestamp)
+     * 其中 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. 获取配置中的用户标识 (user)
         String user = xpCloudProperties.getUser();
+
+        // 3. 生成当前时间的秒级时间戳 (timestamp),用于防止重放攻击及服务器端校验时效性
         String timestamp = String.valueOf(Instant.now().getEpochSecond());
+
+        // 4. 计算签名 (sign)
+        // 拼接规则:user + userKey + timestamp,然后进行 SHA1 哈希计算并转为小写十六进制字符串
         String sign = sha1Hex(user + xpCloudProperties.getUserKey() + timestamp);
+
+        // 5. 返回鉴权所需的三个关键参数
         return new String[]{user, timestamp, sign};
     }
 
@@ -393,29 +556,59 @@ public class XpCloudPrintService {
      * @param xmlContent  模板 XML 正文
      * @return JSON 请求对象
      */
+    /**
+     * 构建芯烨云小票打印接口的 JSON 请求体。
+     * <p>
+     * 该方法负责组装调用芯烨云开放平台 {@code /xprinter/print} 接口所需的所有参数,
+     * 包括鉴权信息、打印机标识、打印内容以及业务控制参数。
+     *
+     * @param reqVo      小票打印业务请求参数,包含打印机 SN、份数、模式、幂等键等
+     * @param xmlContent 已组装好的小票 XML 格式打印内容
+     * @return 封装好的 JSON 对象,可直接用于 HTTP POST 请求
+     */
     private JSONObject buildReceiptPrintJsonBody(XpPrintReceiptReqVo reqVo, String xmlContent) {
+        // 1. 初始化 JSON 对象,使用有序 Map 保持字段插入顺序(可选,便于调试查看)
         JSONObject body = new JSONObject(true);
+
+        // 2. 获取鉴权三元组:user, timestamp, sign
+        // 签名算法基于 user + userKey + timestamp 的 SHA1 哈希
         String[] auth = buildXpAuthTriple();
         body.put("user", auth[0]);
         body.put("timestamp", auth[1]);
         body.put("sign", auth[2]);
+
+        // 3. 调试模式开关:如果配置中开启了 debug,则添加 debug 参数
         if (Boolean.TRUE.equals(xpCloudProperties.getDebug())) {
             body.put("debug", "1");
         }
+
+        // 4. 核心业务参数
+        // 打印机设备编号 (SN)
         body.put("sn", reqVo.getSn());
+        // 打印内容,通常为 XML 或 ESC/POS 指令字符串
         body.put("content", xmlContent);
+
+        // 5. 打印份数:默认为 1 份,若请求中指定则使用指定值
         int copies = reqVo.getCopies() != null ? reqVo.getCopies() : 1;
         body.put("copies", copies);
+
+        // 6. 可选业务参数:仅在非空时添加,避免发送无用字段
+        // 打印模式:0-云端模式(默认), 1-离线模式(需配合 expiresIn)
         if (reqVo.getMode() != null) {
             body.put("mode", reqVo.getMode());
         }
+        // 幂等键:防止网络重试导致的重复打印任务
         if (StringUtils.isNotBlank(reqVo.getIdempotent())) {
             body.put("idempotent", reqVo.getIdempotent());
         }
+        // 过期时间:仅在离线模式(mode=1)下有效,单位秒
         if (reqVo.getExpiresIn() != null) {
             body.put("expiresIn", reqVo.getExpiresIn());
         }
-        body.put("voice","2");
+
+        // 7. 语音播报设置:固定设置为 "2" (具体含义参考芯烨文档,通常为开启或特定提示音)
+        body.put("voice", "2");
+
         return body;
     }
 
@@ -425,38 +618,80 @@ public class XpCloudPrintService {
      * @param body 请求 JSON
      * @return {@code data} 字段订单号
      */
+    /**
+     * 发送 HTTP POST 请求调用芯烨云小票打印接口,并解析返回的订单号。
+     * <p>
+     * 主要流程:
+     * 1. 校验配置:确保打印接口 URL 已正确配置。
+     * 2. 构建请求:设置 Content-Type 为 application/json; charset=utf-8,并将 JSON Body 封装为 HttpEntity。
+     * 3. 发送请求:使用 RestTemplate 发起 POST 请求。
+     * 4. 响应校验:
+     *    - 检查 HTTP 状态码是否为 2xx 成功状态。
+     *    - 检查响应体是否非空。
+     *    - 解析 JSON 响应,检查业务代码 (code) 是否为 0 (成功)。
+     *    - 检查返回的数据字段 (data) 即订单号是否非空。
+     * 5. 异常处理:捕获并重新抛出业务异常 (IotException),或捕获其他异常记录日志后包装抛出。
+     *
+     * @param body 包含鉴权信息、打印机 SN、打印内容等的 JSON 请求体
+     * @return 芯烨云平台返回的打印订单号 (orderNo)
+     * @throws IotException 当配置缺失、HTTP 请求失败、业务返回错误或订单号为空时抛出
+     */
     private String postReceiptPrint(JSONObject body) {
+        // 1. 获取并校验打印接口地址配置
         String url = xpCloudProperties.getReceiptPrintUrl();
         if (StringUtils.isBlank(url)) {
             throw new IotException("芯烨云小票打印接口地址未配置,请配置 xp.dev.receipt-print-url");
         }
+
         try {
+            // 2. 构建 HTTP 请求头,指定内容类型为 JSON UTF-8
             HttpHeaders headers = new HttpHeaders();
             headers.setContentType(new MediaType("application", "json", StandardCharsets.UTF_8));
+
+            // 3. 将 JSON 对象转换为字符串并封装为 HttpEntity
             HttpEntity<String> entity = new HttpEntity<>(body.toJSONString(), headers);
+
+            // 4. 执行 POST 请求,获取原始字符串响应
             ResponseEntity<String> responseEntity = xpOpenApiRestTemplate.postForEntity(url, entity, String.class);
+
+            // 5. 记录完整响应日志,便于调试和排查问题
             log.info("芯烨云小票打印响应参数: {}", JSON.toJSONString(responseEntity));
+
+            // 6. 校验 HTTP 状态码,确保请求在传输层成功
             if (!responseEntity.getStatusCode().is2xxSuccessful()) {
                 throw new IotException("芯烨云小票打印HTTP异常, status=" + responseEntity.getStatusCode());
             }
+
+            // 7. 获取响应体内容
             String raw = responseEntity.getBody();
             if (StringUtils.isBlank(raw)) {
                 throw new IotException("芯烨云小票打印返回空响应");
             }
+
+            // 8. 解析 JSON 响应结果
             JSONObject resp = JSON.parseObject(raw);
             Integer code = resp.getInteger("code");
             String msg = resp.getString("msg");
+
+            // 9. 校验业务状态码,0 表示成功,其他值表示业务错误
             if (code == null || code != 0) {
                 throw new IotException("调用芯烨云小票打印失败:" + msg);
             }
+
+            // 10. 提取并校验返回的订单号 (data 字段)
             String data = resp.getString("data");
             if (StringUtils.isBlank(data)) {
                 throw new IotException("芯烨云小票打印成功但订单号为空");
             }
+
+            // 11. 返回成功的订单号
             return data;
+
         } catch (IotException e) {
+            // 业务异常直接抛出,保持原始错误信息
             throw e;
         } catch (Exception e) {
+            // 捕获网络异常、JSON 解析异常等其他未预期异常
             log.error("芯烨云小票打印HTTP调用异常, url={}", url, e);
             throw new IotException("调用芯烨云小票打印HTTP异常:" + e.getMessage());
         }
@@ -465,16 +700,34 @@ public class XpCloudPrintService {
     /**
      * 计算SHA1(小写16进制)
      */
+    /**
+     * 计算字符串的 SHA-1 哈希值,并转换为小写十六进制字符串。
+     * <p>
+     * 该方法用于生成芯烨云开放平台接口请求所需的签名 (sign)。
+     * 签名算法通常为:SHA1(user + userKey + timestamp)。
+     *
+     * @param source 待计算的原始字符串
+     * @return 小写十六进制格式的 SHA-1 哈希字符串
+     * @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));
             }
+            
             return hex.toString();
         } catch (Exception e) {
+            // 捕获可能的 NoSuchAlgorithmException 或其他异常,包装为业务异常抛出
             throw new IotException("SHA1计算失败:" + e.getMessage());
         }
     }