chenxiaofei 1 månad sedan
förälder
incheckning
7ad5fad6d3

+ 4 - 145
iot-platform-manager/src/main/java/com/platform/api/request/XpPrintReceiptReqVo.java

@@ -28,12 +28,11 @@ public class XpPrintReceiptReqVo implements Serializable {
     private String sn;
 
     /**
-     * 打印内容对象(后端按模板拼接为XML字符串)
+     * 任务单号,用于远程查询小票内容
      */
-    @Valid
-    @NotNull(message = "content不能为空")
-    @Schema(description = "小票内容对象,后端按模板拼接为XML", requiredMode = Schema.RequiredMode.REQUIRED)
-    private Content content;
+    @NotBlank(message = "taskNo不能为空")
+    @Schema(description = "任务单号,用于远程查询小票内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "T910980632597012480")
+    private String taskNo;
 
     /**
      * 是否透传
@@ -83,144 +82,4 @@ public class XpPrintReceiptReqVo implements Serializable {
     @Max(value = 86399, message = "expiresIn必须小于86400")
     @Schema(description = "订单有效期(秒),仅mode=1时生效,取值1~86399", example = "600")
     private Integer expiresIn;
-
-    /**
-     * 小票模板内容对象
-     */
-    @Data
-    @Schema(name = "XpPrintReceiptContent", description = "小票模板内容对象")
-    public static class Content {
-        /**
-         * 公司名称
-         */
-        @NotBlank(message = "content.companyName不能为空")
-        @Schema(description = "公司名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "四川顺采建筑材料有限公司")
-        private String companyName;
-
-        /**
-         * 客户名称
-         */
-        @NotBlank(message = "content.customerName不能为空")
-        @Schema(description = "客户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "峨眉山市朝宣矿业有限公司")
-        private String customerName;
-
-        /**
-         * 任务单号
-         */
-        @NotBlank(message = "content.taskNo不能为空")
-        @Schema(description = "任务单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "T910980632597012480")
-        private String taskNo;
-
-        /**
-         * 接单时间
-         */
-        @NotBlank(message = "content.acceptTime不能为空")
-        @Schema(description = "接单时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-05-07 19:51:48")
-        private String acceptTime;
-
-        /**
-         * 完成时间
-         */
-        @NotBlank(message = "content.finishTime不能为空")
-        @Schema(description = "完成时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-05-07 20:29:05")
-        private String finishTime;
-
-        /**
-         * 司磅员
-         */
-        @NotBlank(message = "content.weigher不能为空")
-        @Schema(description = "司磅员", requiredMode = Schema.RequiredMode.REQUIRED, example = "方丽")
-        private String weigher;
-
-        /**
-         * 司机姓名
-         */
-        @NotBlank(message = "content.driverName不能为空")
-        @Schema(description = "司机姓名", requiredMode = Schema.RequiredMode.REQUIRED, example = "胡俸元")
-        private String driverName;
-
-        /**
-         * 司机手机号
-         */
-        @NotBlank(message = "content.driverMobile不能为空")
-        @Schema(description = "司机手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13990674184")
-        private String driverMobile;
-
-        /**
-         * 司机身份证号
-         */
-        @NotBlank(message = "content.driverIdCard不能为空")
-        @Schema(description = "司机身份证号(可脱敏)", requiredMode = Schema.RequiredMode.REQUIRED, example = "51900****1718")
-        private String driverIdCard;
-
-        /**
-         * 物料名称
-         */
-        @NotBlank(message = "content.materialName不能为空")
-        @Schema(description = "物料名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "石灰石")
-        private String materialName;
-
-        /**
-         * 物料规格
-         */
-        @NotBlank(message = "content.materialSpec不能为空")
-        @Schema(description = "物料规格", requiredMode = Schema.RequiredMode.REQUIRED, example = "80mm以下")
-        private String materialSpec;
-
-        /**
-         * 任务重量
-         */
-        @NotBlank(message = "content.taskWeight不能为空")
-        @Schema(description = "任务重量", requiredMode = Schema.RequiredMode.REQUIRED, example = "27.3 吨")
-        private String taskWeight;
-
-        /**
-         * 重量汇总
-         */
-        @NotBlank(message = "content.weightSummary不能为空")
-        @Schema(description = "重量汇总(皮重/毛重/净重)", requiredMode = Schema.RequiredMode.REQUIRED, example = "12.54吨   毛重:36.83吨   净重:24.29吨")
-        private String weightSummary;
-
-        /**
-         * 车牌号
-         */
-        @NotBlank(message = "content.plateNo不能为空")
-        @Schema(description = "车牌号", requiredMode = Schema.RequiredMode.REQUIRED, example = "川LA8659")
-        private String plateNo;
-
-        /**
-         * 车辆轴数描述
-         */
-        @NotBlank(message = "content.vehicleAxleDesc不能为空")
-        @Schema(description = "车辆轴数描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "货车 四轴车")
-        private String vehicleAxleDesc;
-
-        /**
-         * 目的地
-         */
-        @NotBlank(message = "content.destination不能为空")
-        @Schema(description = "目的地", requiredMode = Schema.RequiredMode.REQUIRED, example = "四川省-乐山市-峨眉山市")
-        private String destination;
-
-        /**
-         * 打印时间
-         */
-        @NotBlank(message = "content.printTime不能为空")
-        @Schema(description = "打印时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-05-07 20:29:11")
-        private String printTime;
-
-        /**
-         * 打印次数描述
-         */
-        @NotBlank(message = "content.printTimesDesc不能为空")
-        @Schema(description = "打印次数描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "第 1 次打印")
-        private String printTimesDesc;
-
-        /**
-         * 页脚联次文案,例:客户联(1/2)
-         */
-        @NotBlank(message = "content.copyLabel不能为空")
-        @Schema(description = "页脚联次文案", requiredMode = Schema.RequiredMode.REQUIRED, example = "客户联(1/2)")
-        private String copyLabel;
-    }
 }

+ 90 - 0
iot-platform-manager/src/main/java/com/platform/entity/KwsPrintReceiptRecord.java

@@ -0,0 +1,90 @@
+package com.platform.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 小票打印次数记录。
+ *
+ * @author assistant
+ */
+@Data
+@Accessors(chain = true)
+@TableName("kws_print_receipt_record")
+public class KwsPrintReceiptRecord implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID。
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 任务单号。
+     */
+    @TableField("task_no")
+    private String taskNo;
+
+    /**
+     * 运单号。
+     */
+    @TableField("waybill_no")
+    private String waybillNo;
+
+    /**
+     * 贸易订单号。
+     */
+    @TableField("trade_order_no")
+    private String tradeOrderNo;
+
+    /**
+     * 打印机编号。
+     */
+    @TableField("printer_sn")
+    private String printerSn;
+
+    /**
+     * 已成功打印次数。
+     */
+    @TableField("print_count")
+    private Integer printCount;
+
+    /**
+     * 最后打印时间。
+     */
+    @TableField("last_print_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date lastPrintTime;
+
+    /**
+     * 创建时间。
+     */
+    @TableField("create_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createTime;
+
+    /**
+     * 更新时间。
+     */
+    @TableField("update_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updateTime;
+
+    /**
+     * 删除标识:0-正常,1-删除。
+     */
+    @TableField("del_flag")
+    private Integer delFlag;
+}

+ 29 - 0
iot-platform-manager/src/main/java/com/platform/external/client/PrintReceiptContentClient.java

@@ -0,0 +1,29 @@
+package com.platform.external.client;
+
+import com.platform.external.request.WaybillTransportQueryReq;
+import com.platform.external.response.TradeOrderTransportInfoResp;
+import com.platform.result.BaseResult;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+/**
+ * 小票打印内容远程服务 Feign 客户端。
+ *
+ * @author assistant
+ */
+@FeignClient(
+        name = "${external-system.print-receipt.service-id:sckw-ng-transport}",
+        path = "${external-system.print-receipt.base-path:/kwtWaybillOrder}"
+)
+public interface PrintReceiptContentClient {
+
+    /**
+     * 按任务单号查询小票打印内容。
+     *
+     * @param request 小票内容查询参数
+     * @return 小票打印内容
+     */
+    @PostMapping("${external-system.print-receipt.content-path:/waybill/transport-info}")
+    BaseResult<TradeOrderTransportInfoResp> queryTransportInfoByWaybillNo(@RequestBody WaybillTransportQueryReq request);
+}

+ 143 - 0
iot-platform-manager/src/main/java/com/platform/external/request/PrintReceiptContent.java

@@ -0,0 +1,143 @@
+package com.platform.external.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+/**
+ * 小票模板内容对象
+ */
+@Data
+public class PrintReceiptContent {
+    /**
+     * 公司名称
+     */
+    @NotBlank(message = "公司名称不能为空")
+    private String companyName;
+
+    /**
+     * 客户名称
+     */
+    @NotBlank(message = "客户名称不能为空")
+    @Schema(description = "客户名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "峨眉山市朝宣矿业有限公司")
+    private String customerName;
+
+    /**
+     * 任务单号
+     */
+    @NotBlank(message = "任务单号不能为空")
+    @Schema(description = "任务单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "T910980632597012480")
+    private String taskNo;
+
+    /**
+     * 接单时间
+     */
+    @NotBlank(message = "接单时间不能为空")
+    @Schema(description = "接单时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-05-07 19:51:48")
+    private String acceptTime;
+
+    /**
+     * 完成时间
+     */
+    @NotBlank(message = "完成时间不能为空")
+    @Schema(description = "完成时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-05-07 20:29:05")
+    private String finishTime;
+
+    /**
+     * 司磅员
+     */
+    @NotBlank(message = "司磅员不能为空")
+    @Schema(description = "司磅员", requiredMode = Schema.RequiredMode.REQUIRED, example = "方丽")
+    private String weigher;
+
+    /**
+     * 司机姓名
+     */
+    @NotBlank(message = "司机姓名不能为空")
+    @Schema(description = "司机姓名", requiredMode = Schema.RequiredMode.REQUIRED, example = "胡俸元")
+    private String driverName;
+
+    /**
+     * 司机手机号
+     */
+    @NotBlank(message = "司机手机号不能为空")
+    @Schema(description = "司机手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13990674184")
+    private String driverMobile;
+
+    /**
+     * 司机身份证号
+     */
+    @NotBlank(message = "司机身份证号不能为空")
+    @Schema(description = "司机身份证号(可脱敏)", requiredMode = Schema.RequiredMode.REQUIRED, example = "51900****1718")
+    private String driverIdCard;
+
+    /**
+     * 物料名称
+     */
+    @NotBlank(message = "物料名称不能为空")
+    @Schema(description = "物料名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "石灰石")
+    private String materialName;
+
+    /**
+     * 物料规格
+     */
+    @NotBlank(message = "物料规格不能为空")
+    @Schema(description = "物料规格", requiredMode = Schema.RequiredMode.REQUIRED, example = "80mm以下")
+    private String materialSpec;
+
+    /**
+     * 任务重量
+     */
+    @NotBlank(message = "任务重量不能为空")
+    @Schema(description = "任务重量", requiredMode = Schema.RequiredMode.REQUIRED, example = "27.3 吨")
+    private String taskWeight;
+
+    /**
+     * 重量汇总
+     */
+    @NotBlank(message = "重量汇总不能为空")
+    @Schema(description = "重量汇总(皮重/毛重/净重)", requiredMode = Schema.RequiredMode.REQUIRED, example = "12.54吨   毛重:36.83吨   净重:24.29吨")
+    private String weightSummary;
+
+    /**
+     * 车牌号
+     */
+    @NotBlank(message = "车牌号不能为空")
+    @Schema(description = "车牌号", requiredMode = Schema.RequiredMode.REQUIRED, example = "川LA8659")
+    private String plateNo;
+
+    /**
+     * 车辆轴数描述
+     */
+    @NotBlank(message = "车辆轴数描述不能为空")
+    @Schema(description = "车辆轴数描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "货车 四轴车")
+    private String vehicleAxleDesc;
+
+    /**
+     * 目的地
+     */
+    @NotBlank(message = "目的地不能为空")
+    @Schema(description = "目的地", requiredMode = Schema.RequiredMode.REQUIRED, example = "四川省-乐山市-峨眉山市")
+    private String destination;
+
+    /**
+     * 打印时间
+     */
+    @NotBlank(message = "打印时间不能为空")
+    @Schema(description = "打印时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-05-07 20:29:11")
+    private String printTime;
+
+    /**
+     * 打印次数描述
+     */
+    @NotBlank(message = "打印次数描述不能为空")
+    @Schema(description = "打印次数描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "第 1 次打印")
+    private String printTimesDesc;
+
+    /**
+     * 页脚联次文案,例:客户联(1/2)
+     */
+    @NotBlank(message = "页脚联次文案不能为空")
+    @Schema(description = "页脚联次文案", requiredMode = Schema.RequiredMode.REQUIRED, example = "客户联(1/2)")
+    private String copyLabel;
+}

+ 29 - 0
iot-platform-manager/src/main/java/com/platform/external/request/WaybillTransportQueryReq.java

@@ -0,0 +1,29 @@
+package com.platform.external.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 运单运输信息查询请求参数。
+ *
+ * @author system
+ * @date 2026-05-08
+ */
+@Data
+@Schema(description = "运单运输信息查询请求")
+public class WaybillTransportQueryReq implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 9050156770193841826L;
+
+    /**
+     * 运单号。
+     */
+    @NotBlank(message = "运单号不能为空")
+    @Schema(description = "运单号", requiredMode = Schema.RequiredMode.REQUIRED)
+    private String waybillNo;
+}

+ 160 - 0
iot-platform-manager/src/main/java/com/platform/external/response/TradeOrderTransportInfoResp.java

@@ -0,0 +1,160 @@
+package com.platform.external.response;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 贸易订单运输信息响应。
+ *
+ * @author system
+ * @date 2026-05-08
+ */
+@Data
+@Schema(description = "贸易订单运输信息响应")
+public class TradeOrderTransportInfoResp implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 2825054269070244593L;
+
+    @Schema(description = "贸易订单号")
+    private String tradeOrderNo;
+
+    @Schema(description = "供应企业名称")
+    private String supplierName;
+
+    @Schema(description = "客户名称")
+    private String customerName;
+
+    @Schema(description = "任务信息列表")
+    private List<TaskInfo> tasks = new ArrayList<>();
+
+    /**
+     * 单车任务信息。
+     */
+    @Data
+    @Schema(description = "单车任务信息")
+    public static class TaskInfo implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 4264229244065846224L;
+
+        @Schema(description = "任务编号")
+        private String taskNo;
+
+        @Schema(description = "接单时间")
+        private String acceptTime;
+
+        @Schema(description = "完成时间")
+        private String finishTime;
+
+        @Schema(description = "计重人")
+        private String weigherName;
+
+        @Schema(description = "司机信息")
+        private DriverInfo driverInfo;
+
+        @Schema(description = "货物信息")
+        private GoodsInfo goodsInfo;
+
+        @Schema(description = "车辆信息")
+        private TruckInfo truckInfo;
+
+        @Schema(description = "目的地")
+        private String destination;
+
+        @Schema(description = "打印信息")
+        private PrintInfo printInfo;
+    }
+
+    /**
+     * 司机信息。
+     */
+    @Data
+    @Schema(description = "司机信息")
+    public static class DriverInfo implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 4681187619812752711L;
+
+        @Schema(description = "姓名")
+        private String name;
+
+        @Schema(description = "手机号")
+        private String phone;
+
+        @Schema(description = "脱敏身份证号")
+        private String idCard;
+    }
+
+    /**
+     * 任务货物信息。
+     */
+    @Data
+    @Schema(description = "任务货物信息")
+    public static class GoodsInfo implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 7576326426794710595L;
+
+        @Schema(description = "物料")
+        private String materialName;
+
+        @Schema(description = "规格")
+        private String specification;
+
+        @Schema(description = "任务量")
+        private BigDecimal taskAmount;
+
+        @Schema(description = "皮重")
+        private BigDecimal tareWeight;
+
+        @Schema(description = "毛重")
+        private BigDecimal grossWeight;
+
+        @Schema(description = "净重")
+        private BigDecimal netWeight;
+
+        @Schema(description = "单位")
+        private String unit;
+    }
+
+    /**
+     * 车辆信息。
+     */
+    @Data
+    @Schema(description = "车辆信息")
+    public static class TruckInfo implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = 4193011453340089506L;
+
+        @Schema(description = "车牌号")
+        private String truckNo;
+
+        @Schema(description = "车辆轴数")
+        private String truckAxle;
+    }
+
+    /**
+     * 打印信息。
+     */
+    @Data
+    @Schema(description = "打印信息")
+    public static class PrintInfo implements Serializable {
+
+        @Serial
+        private static final long serialVersionUID = -6068786676826461870L;
+
+        @Schema(description = "打印时间")
+        private String printTime;
+
+        @Schema(description = "打印次数")
+        private Integer printCount;
+    }
+}

+ 14 - 0
iot-platform-manager/src/main/java/com/platform/mapper/KwsPrintReceiptRecordDao.java

@@ -0,0 +1,14 @@
+package com.platform.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.platform.entity.KwsPrintReceiptRecord;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 小票打印次数记录 Mapper。
+ *
+ * @author assistant
+ */
+@Mapper
+public interface KwsPrintReceiptRecordDao extends BaseMapper<KwsPrintReceiptRecord> {
+}

+ 82 - 0
iot-platform-manager/src/main/java/com/platform/service/KwsPrintReceiptRecordRepository.java

@@ -0,0 +1,82 @@
+package com.platform.service;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.platform.entity.KwsPrintReceiptRecord;
+import com.platform.exception.IotException;
+import com.platform.mapper.KwsPrintReceiptRecordDao;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Repository;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Date;
+
+/**
+ * 小票打印次数记录仓储。
+ *
+ * @author assistant
+ */
+@Repository
+public class KwsPrintReceiptRecordRepository extends ServiceImpl<KwsPrintReceiptRecordDao, KwsPrintReceiptRecord> {
+
+    /**
+     * 预占并返回本次打印次数。
+     *
+     * @param taskNo 任务单号
+     * @param tradeOrderNo 贸易订单号
+     * @param printerSn 打印机编号
+     * @return 本次打印次数
+     */
+    public int reservePrintCount(String taskNo, String tradeOrderNo, String printerSn, LocalDateTime now) {
+        if (StringUtils.isBlank(taskNo)) {
+            throw new IotException("任务单号不能为空");
+        }
+        if (StringUtils.isBlank(printerSn)) {
+            throw new IotException("打印机编号不能为空");
+        }
+        Date date = Date.from(now.atZone(ZoneId.systemDefault()).toInstant());
+        KwsPrintReceiptRecord record = getByTaskNo(taskNo);
+        if (record == null) {
+            KwsPrintReceiptRecord newRecord = new KwsPrintReceiptRecord()
+                    .setTaskNo(taskNo)
+                    .setTradeOrderNo(StringUtils.defaultString(tradeOrderNo))
+                    .setPrinterSn(printerSn)
+                    .setPrintCount(1)
+                    .setLastPrintTime(date)
+                    .setCreateTime(date)
+                    .setUpdateTime(date)
+                    .setDelFlag(0);
+            if (!save(newRecord)) {
+                throw new IotException("新增小票打印次数记录失败");
+            }
+            return 1;
+        }
+        int nextPrintCount = record.getPrintCount() == null || record.getPrintCount() <= 0
+                ? 1
+                : record.getPrintCount() + 1;
+        record.setTradeOrderNo(StringUtils.defaultString(tradeOrderNo));
+        record.setPrinterSn(printerSn);
+        record.setPrintCount(nextPrintCount);
+        record.setLastPrintTime(date);
+        record.setUpdateTime(date);
+        record.setDelFlag(0);
+        if (!updateById(record)) {
+            throw new IotException("更新小票打印次数记录失败");
+        }
+        return nextPrintCount;
+    }
+
+    /**
+     * 根据任务单号查询打印次数记录。
+     *
+     * @param taskNo 任务单号
+     * @return 打印次数记录
+     */
+    private KwsPrintReceiptRecord getByTaskNo(String taskNo) {
+        return getOne(Wrappers.<KwsPrintReceiptRecord>lambdaQuery()
+                .eq(KwsPrintReceiptRecord::getTaskNo, taskNo)
+                .eq(KwsPrintReceiptRecord::getDelFlag, 0)
+                .last("limit 1"));
+    }
+}

+ 213 - 3
iot-platform-manager/src/main/java/com/platform/service/XpCloudPrintService.java

@@ -5,6 +5,11 @@ import com.platform.api.request.XpPrintImageReqVo;
 import com.platform.api.request.XpPrintReceiptReqVo;
 import com.platform.config.XpCloudProperties;
 import com.platform.exception.IotException;
+import com.platform.external.client.PrintReceiptContentClient;
+import com.platform.external.request.PrintReceiptContent;
+import com.platform.external.request.WaybillTransportQueryReq;
+import com.platform.external.response.TradeOrderTransportInfoResp;
+import com.platform.result.BaseResult;
 import io.github.dv996coding.vo.ObjectRestResponse;
 import io.github.dv996coding.vo.PrintOrderRequest;
 import jakarta.annotation.Resource;
@@ -12,10 +17,18 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
 import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.util.AbstractMap;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
 import java.util.UUID;
 
 /**
@@ -30,6 +43,12 @@ public class XpCloudPrintService {
 
     private final XpCloudProperties xpCloudProperties;
 
+    private final PrintReceiptContentClient printReceiptContentClient;
+
+    private final KwsPrintReceiptRecordRepository kwsPrintReceiptRecordRepository;
+
+    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
     /**
      * 直接使用@Resource注入SDK服务
      */
@@ -74,16 +93,24 @@ public class XpCloudPrintService {
      * @param reqVo 小票订单打印请求
      * @return 云端打印任务号
      */
+    @Transactional(rollbackFor = Exception.class)
     public String printReceipt(XpPrintReceiptReqVo reqVo) {
         log.info("芯烨云小票打印, 入参:{}", JSON.toJSONString(reqVo));
-        if (reqVo == null || reqVo.getContent() == null || StringUtils.isAnyBlank(reqVo.getSn())) {
+        if (reqVo == null || StringUtils.isAnyBlank(reqVo.getSn(), reqVo.getTaskNo())) {
             throw new IotException("打印参数不能为空");
         }
         if (reqVo.getExpiresIn() != null && (reqVo.getMode() == null || reqVo.getMode() != 1)) {
             throw new IotException("设置expiresIn时,mode必须为1");
         }
         try {
-            String xmlContent = buildReceiptXml(reqVo.getContent());
+            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.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());
@@ -108,17 +135,200 @@ public class XpCloudPrintService {
             if (response.getCode() == null || response.getCode() != 0) {
                 throw new IotException("调用芯烨云小票打印失败:" + response.getMsg());
             }
+            log.info("芯烨云小票打印次数记录完成, taskNo={}, printCount={}", content.getTaskNo(), printCount);
             return response.getData();
+        } catch (IotException e) {
+            throw e;
         } catch (Exception e) {
             log.error("调用芯烨云小票打印异常, sn={}", reqVo.getSn(), e);
             throw new IotException("调用芯烨云小票打印异常:" + e.getMessage());
         }
     }
 
+    /**
+     * 通过远程服务获取小票内容。
+     *
+     * @param taskNo 任务单号
+     * @return 小票内容
+     */
+    private TradeOrderTransportInfoResp getReceiptContent(String taskNo) {
+        try {
+            WaybillTransportQueryReq query = new WaybillTransportQueryReq();
+            query.setWaybillNo(taskNo);
+            log.info("远程获取小票内容, 请求参数:{}", JSON.toJSONString(query));
+            BaseResult<TradeOrderTransportInfoResp> response = printReceiptContentClient.queryTransportInfoByWaybillNo(query);
+            log.info("远程获取小票内容, 响应参数:{}", JSON.toJSONString(response));
+            if (response == null) {
+                throw new IotException("远程获取小票内容响应为空");
+            }
+            if (!response.isSuccess()) {
+                throw new IotException("远程获取小票内容失败:" + response.getMessage());
+            }
+            if (response.getData() == null) {
+                throw new IotException("远程获取小票内容为空");
+            }
+            return response.getData();
+        } catch (IotException e) {
+            throw e;
+        } catch (Exception e) {
+            log.error("远程获取小票内容异常, taskNo={}", taskNo, e);
+            throw new IotException("远程获取小票内容异常:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 校验远程返回的小票内容,避免空字段导致打印内容不完整。
+     *
+     * @param content 小票内容
+     */
+    private void validateReceiptContent(PrintReceiptContent content) {
+        if (content == null) {
+            throw new IotException("小票内容不能为空");
+        }
+        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())
+        );
+        String blankField = requiredFields.stream()
+                .filter(field -> StringUtils.isBlank(field.getValue()))
+                .map(AbstractMap.SimpleEntry::getKey)
+                .findFirst()
+                .orElse(null);
+        if (StringUtils.isNotBlank(blankField)) {
+            throw new IotException("小票内容字段不能为空:" + blankField);
+        }
+    }
+
+    /**
+     * 将远程运输信息重新组装为小票打印模板参数。
+     *
+     * @param transportInfo 远程运输信息
+     * @param taskNo 任务单号
+     * @return 小票打印模板参数
+     */
+    private PrintReceiptContent assembleReceiptContent(TradeOrderTransportInfoResp transportInfo, String taskNo) {
+        if (transportInfo == null) {
+            throw new IotException("小票运输信息不能为空");
+        }
+        TradeOrderTransportInfoResp.TaskInfo taskInfo = resolveTaskInfo(transportInfo, taskNo);
+        TradeOrderTransportInfoResp.DriverInfo driverInfo = taskInfo.getDriverInfo();
+        TradeOrderTransportInfoResp.GoodsInfo goodsInfo = taskInfo.getGoodsInfo();
+        TradeOrderTransportInfoResp.TruckInfo truckInfo = taskInfo.getTruckInfo();
+        TradeOrderTransportInfoResp.PrintInfo printInfo = taskInfo.getPrintInfo();
+
+        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;
+    }
+
+    /**
+     * 按任务单号匹配任务信息,未匹配时取第一条任务。
+     *
+     * @param transportInfo 远程运输信息
+     * @param taskNo 任务单号
+     * @return 任务信息
+     */
+    private TradeOrderTransportInfoResp.TaskInfo resolveTaskInfo(TradeOrderTransportInfoResp transportInfo, String taskNo) {
+        List<TradeOrderTransportInfoResp.TaskInfo> tasks = transportInfo.getTasks() == null
+                ? Collections.emptyList()
+                : transportInfo.getTasks();
+        if (tasks.isEmpty()) {
+            throw new IotException("小票任务信息不能为空");
+        }
+        return tasks.stream()
+                .filter(taskInfo -> taskInfo != null && StringUtils.equals(taskNo, taskInfo.getTaskNo()))
+                .findFirst()
+                .orElse(tasks.stream()
+                        .filter(Objects::nonNull)
+                        .findFirst()
+                        .orElseThrow(() -> new IotException("小票任务信息不能为空")));
+    }
+
+    /**
+     * 格式化重量与单位。
+     *
+     * @param weight 重量
+     * @param unit 单位
+     * @return 重量展示文案
+     */
+    private String formatWeight(BigDecimal weight, String unit) {
+        if (weight == null) {
+            return null;
+        }
+        String weightText = weight.stripTrailingZeros().toPlainString();
+        return StringUtils.isBlank(unit) ? weightText : weightText + " " + unit;
+    }
+
+    /**
+     * 组装皮重、毛重、净重汇总文案。
+     *
+     * @param goodsInfo 货物信息
+     * @return 重量汇总文案
+     */
+    private String formatWeightSummary(TradeOrderTransportInfoResp.GoodsInfo goodsInfo) {
+        if (goodsInfo == null) {
+            return null;
+        }
+        if (goodsInfo.getTareWeight() == null || goodsInfo.getGrossWeight() == null || goodsInfo.getNetWeight() == null) {
+            return null;
+        }
+        String unit = goodsInfo.getUnit();
+        return "皮重:" + formatWeight(goodsInfo.getTareWeight(), unit)
+                + "  毛重:" + formatWeight(goodsInfo.getGrossWeight(), unit)
+                + "  净重:" + formatWeight(goodsInfo.getNetWeight(), unit);
+    }
+
+    /**
+     * 组装打印次数文案。
+     *
+     * @param printCount 打印次数
+     * @return 打印次数文案
+     */
+    private String formatPrintTimes(Integer printCount) {
+        if (printCount == null || printCount <= 0) {
+            return null;
+        }
+        return "第 " + printCount + " 次打印";
+    }
+
     /**
      * 按前端对象拼接小票XML内容
      */
-    private String buildReceiptXml(XpPrintReceiptReqVo.Content content) {
+    private String buildReceiptXml(PrintReceiptContent content) {
         String line = "------------------------";
         return "\n<CB>" + content.getCompanyName()
                 + "\n<CB>" + line

+ 18 - 0
sql/2026/05/2026_05_08_kws_print_receipt_record_create.sql

@@ -0,0 +1,18 @@
+CREATE TABLE `kws_print_receipt_record`
+(
+    `id`               bigint       NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+    `task_no`          varchar(64)  NOT NULL DEFAULT '' COMMENT '任务单号',
+    `waybill_no`       varchar(64)  NOT NULL DEFAULT '' COMMENT '运单号',
+    `trade_order_no`   varchar(64)  NOT NULL DEFAULT '' COMMENT '贸易订单号',
+    `printer_sn`       varchar(64)  NOT NULL DEFAULT '' COMMENT '打印机编号',
+    `print_count`      int          NOT NULL DEFAULT 0 COMMENT '已成功打印次数',
+    `last_print_time`  datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后打印时间',
+    `create_time`      datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time`      datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    `del_flag`         int          NOT NULL DEFAULT 0 COMMENT '删除标识: 0-正常, 1-删除',
+    PRIMARY KEY (`id`) USING BTREE,
+    UNIQUE KEY `uk_task_no` (`task_no`) USING BTREE,
+    KEY `idx_waybill_no` (`waybill_no`) USING BTREE,
+    KEY `idx_trade_order_no` (`trade_order_no`) USING BTREE,
+    KEY `idx_last_print_time` (`last_print_time`) USING BTREE
+) COMMENT ='小票打印次数记录表';