feat:修改设备列表、详情

feat:修改订单详情
feat:修改售后订单查询条件
feat:增加打印机设备及功能
This commit is contained in:
dzc committed 2026-09-04 15:14:51 +08:00
1 parent fc8afe4de7
commit 8514af0d9f
49 files changed
+3734 -222

No files matched your search

@@ -0,0 +1,44 @@
package com.bsd.pjcloud.hard.api.constant;
import cn.hutool.core.util.ObjectUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 打印任务失败阶段,帮助管理端区分配置问题、服务端故障和终端故障。
*/
@Getter
@AllArgsConstructor
public enum HardPrintFailureStageEnum {
/** 来源业务终端缺失、未绑定打印机或绑定关系失效。 */
BINDING("BINDING", "设备绑定"),
/** 服务端读取业务数据、绘制 PNG 或上传文件失败。 */
IMAGE_GENERATION("IMAGE_GENERATION", "图片生成"),
/** 服务端向目标打印终端发布 MQTT 消息失败。 */
MQTT_DISPATCH("MQTT_DISPATCH", "MQTT下发"),
/** 打印终端下载图片、连接 USB 或写入 ESC/POS 数据失败。 */
TERMINAL_PRINT("TERMINAL_PRINT", "终端打印");
/** 持久化阶段编码。 */
private final String stage;
/** 阶段中文说明。 */
private final String description;
/**
* 按持久化编码解析失败阶段。
*
* @param stage 阶段编码
* @return 匹配阶段;编码为空或未知时返回 {@code null}
*/
public static HardPrintFailureStageEnum valueOfByStage(String stage) {
if (ObjectUtil.isEmpty(stage)) return null;
return Arrays.stream(values()).filter(value -> ObjectUtil.equals(stage, value.stage)).findFirst().orElse(null);
}
}
@@ -4,23 +4,46 @@ import cn.hutool.core.util.ObjectUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
import javax.xml.stream.events.StartDocument;
import java.util.Arrays;
/**
* 打印状态
* 打印任务状态。
*
* <p>编码 {@code 1} 和 {@code 2} 分别兼容历史“已生成”和“已打印”记录。</p>
*/
@AllArgsConstructor
@Getter
public enum HardPrintStatusEnum {
GENERATE("1", "已生成"),
/** 任务已落库,等待后端生成并上传打印图片。 */
PENDING_GENERATION("0", "待生成"),
/** 图片已生成并允许下发,历史枚举名保留为 GENERATE 以保持二进制兼容。 */
GENERATE("1", "待打印"),
/** 打印终端已确认完整 USB 数据和切纸命令写入成功。 */
PRINT("2", "已打印"),
/** 绑定、图片生成、MQTT 下发或终端打印阶段失败,可重试原任务。 */
FAILED("3", "打印失败"),
/** 核销回退等业务动作取消了尚未打印的任务。 */
CANCELLED("4", "已取消"),
;
/** 持久化状态编码。 */
private final String type;
/** 状态中文说明。 */
private final String description;
/**
* 按持久化编码解析打印状态。
*
* @param type 状态编码
* @return 匹配状态;编码为空或未知时返回 {@code null}
*/
public static HardPrintStatusEnum valueOfByType(String type) {
if (ObjectUtil.isEmpty(type)) return null;
return Arrays.stream(values()).filter(v -> ObjectUtil.equals(type, v.getType())).findFirst().get();
return Arrays.stream(values()).filter(v -> ObjectUtil.equals(type, v.getType())).findFirst().orElse(null);
}
}
@@ -0,0 +1,38 @@
package com.bsd.pjcloud.hard.api.constant;
import cn.hutool.core.util.ObjectUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 打印凭证类型,用于选择后端图片模板和管理端业务动作。
*/
@Getter
@AllArgsConstructor
public enum HardPrintTypeEnum {
/** 支付订单的发票或消费小票。 */
INVOICE("1", "发票"),
/** 预约订单核销成功后生成的纸质兑换凭证。 */
REDEMPTION_VOUCHER("2", "兑换券");
/** 持久化类型编码。 */
private final String type;
/** 类型中文说明。 */
private final String description;
/**
* 按持久化编码解析打印类型。
*
* @param type 类型编码
* @return 匹配类型;编码为空或未知时返回 {@code null}
*/
public static HardPrintTypeEnum valueOfByType(String type) {
if (ObjectUtil.isEmpty(type)) return null;
return Arrays.stream(values()).filter(value -> ObjectUtil.equals(type, value.type)).findFirst().orElse(null);
}
}
@@ -26,6 +26,8 @@ public enum HardTypeEnum {
YUN_XFJ("9", "云版消费机"),
YYX_4G("10", "4G-云音响"),
YYX_WIFI("11", "wifi-云音响"),
/** 运行打印专用终端模式、通过 USB 驱动尔印 ESC/POS 打印机的设备。 */
EY_PRINT("12", "尔印打印机器"),
;
private final String type;
private final String description;
@@ -45,4 +47,15 @@ public enum HardTypeEnum {
return Arrays.stream(values()).filter(v -> ObjectUtil.equals(type, v.getType())).findFirst().get();
}
/**
* 判断指定设备类型是否为独立打印终端。
*
* @param hardTypeEnum 待判断的设备类型
* @return 类型为 {@link #EY_PRINT} 时返回 {@code true}
*/
public static boolean isPrintHard(HardTypeEnum hardTypeEnum) {
if (ObjectUtil.isEmpty(hardTypeEnum)) return false;
return List.of(EY_PRINT).contains(hardTypeEnum);
}
}
@@ -0,0 +1,32 @@
package com.bsd.pjcloud.hard.api.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 管理端创建发票或兑换券打印任务的请求。
*/
@Data
@Schema(description = "创建打印任务请求")
public class HardPrintRequestDto {
/** 待打印凭证所属订单 ID。 */
@NotNull(message = "订单ID不能为空")
@Schema(description = "订单ID")
private Long orderId;
/** 打印类型编码,1 发票、2 兑换券。 */
@NotBlank(message = "打印类型不能为空")
@Schema(description = "打印类型")
private String printType;
/** 来源业务终端 ID;订单没有来源终端且同商户存在多个绑定时必填。 */
@Schema(description = "来源业务终端ID")
private Long sourceHardId;
/** 已打印任务再次创建下一序号时必填的重打原因。 */
@Schema(description = "重打原因")
private String reprintReason;
}
@@ -0,0 +1,45 @@
package com.bsd.pjcloud.hard.api.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* EY_PRINT 终端提交的打印结果回执。
*/
@Data
@Schema(description = "打印终端结果回执")
public class HardPrintResultDto {
/** hard_invoice 打印任务主键。 */
@NotNull(message = "打印任务ID不能为空")
@Schema(description = "打印任务ID")
private Long taskId;
/** 任务所属订单 ID,用于阻断任务与订单错配。 */
@NotNull(message = "订单ID不能为空")
@Schema(description = "订单ID")
private Long orderId;
/** 终端实际处理的重试版本。 */
@NotNull(message = "重试版本不能为空")
@Min(value = 0, message = "重试版本不能小于0")
@Schema(description = "重试版本")
private Integer retryVersion;
/** 完整 USB 数据及切纸命令是否已经被接口接受。 */
@NotNull(message = "打印结果不能为空")
@Schema(description = "是否成功")
private Boolean success;
/** 本版本在终端执行的 USB 尝试次数。 */
@NotNull(message = "尝试次数不能为空")
@Min(value = 1, message = "尝试次数不能小于1")
@Schema(description = "终端尝试次数")
private Integer attemptCount;
/** 失败时的可诊断错误摘要,不得包含凭证或密码。 */
@Schema(description = "错误摘要")
private String errorMessage;
}
@@ -0,0 +1,31 @@
package com.bsd.pjcloud.hard.api.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 对既有打印任务执行失败重试或已打印重打的请求。
*/
@Data
@Schema(description = "打印任务重试或重打请求")
public class HardPrintRetryDto {
/** 既有 hard_invoice 打印任务主键。 */
@NotNull(message = "打印任务ID不能为空")
@Schema(description = "打印任务ID")
private Long historyId;
/**
* 失败任务需要补录的来源业务终端 ID。
*
* <p>仅当原任务因缺少来源终端而失败时使用;服务端仍会校验该终端与订单同商户,
* 并根据服务端绑定关系确定目标 EY_PRINT,客户端不能直接指定打印机。</p>
*/
@Schema(description = "补录的来源业务终端ID")
private Long sourceHardId;
/** 已打印任务重打时必须填写的原因;失败任务原任务重试时可为空。 */
@Schema(description = "重打原因")
private String reprintReason;
}
@@ -0,0 +1,23 @@
package com.bsd.pjcloud.hard.api.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 创建业务终端与尔印打印终端绑定关系的请求。
*/
@Data
@Schema(description = "打印终端绑定请求")
public class HardPrinterBindingDto {
/** SUPERMARKET 或 CANTEEN 来源终端 ID。 */
@NotNull(message = "来源终端不能为空")
@Schema(description = "来源业务终端ID")
private Long sourceHardId;
/** EY_PRINT 目标终端 ID。 */
@NotNull(message = "打印终端不能为空")
@Schema(description = "目标打印终端ID")
private Long printerHardId;
}
@@ -0,0 +1,46 @@
package com.bsd.pjcloud.hard.api.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 保存尔印打印终端 USB 与 ESC/POS 参数的请求。
*/
@Data
@Schema(description = "打印终端配置请求")
public class HardPrinterConfigDto {
/** EY_PRINT 设备 ID。 */
@NotNull(message = "打印终端不能为空")
@Schema(description = "打印终端ID")
private Long printerHardId;
/** 真机型号;未联调前允许为空。 */
@Schema(description = "打印机型号")
private String printerModel;
/** USB Vendor ID 十进制值;未联调前允许为空。 */
@Schema(description = "USB VID")
private Integer usbVendorId;
/** USB Product ID 十进制值;未联调前允许为空。 */
@Schema(description = "USB PID")
private Integer usbProductId;
/** 纸张宽度,首期只允许 80。 */
@NotNull(message = "纸宽不能为空")
@Schema(description = "纸宽,单位mm")
private Integer paperWidth;
/** 自动切刀开关,1 开启、0 关闭。 */
@NotBlank(message = "自动切刀开关不能为空")
@Schema(description = "自动切刀开关")
private String autoCut;
/** 图片指令模式,只允许 GS_V0 或 ESC_STAR。 */
@NotBlank(message = "图片指令模式不能为空")
@Schema(description = "图片指令模式")
private String imageMode;
}
@@ -0,0 +1,37 @@
package com.bsd.pjcloud.hard.api.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* 尔印打印终端上报实际 USB 打印机信息的请求。
*/
@Data
@Schema(description = "打印终端实际 USB 打印机信息")
public class HardPrinterInfoReportDto {
/** USB 描述符中的厂商和产品名称组合,最长保存 128 个字符。 */
@NotBlank(message = "打印机型号不能为空")
@Size(max = 128, message = "打印机型号不能超过128个字符")
@Schema(description = "实际打印机型号")
private String printerModel;
/** USB Vendor ID 十进制值,取值范围为无符号 16 位整数。 */
@NotNull(message = "USB VID不能为空")
@Min(value = 0, message = "USB VID必须在0到65535之间")
@Max(value = 65535, message = "USB VID必须在0到65535之间")
@Schema(description = "USB VID")
private Integer usbVendorId;
/** USB Product ID 十进制值,取值范围为无符号 16 位整数。 */
@NotNull(message = "USB PID不能为空")
@Min(value = 0, message = "USB PID必须在0到65535之间")
@Max(value = 65535, message = "USB PID必须在0到65535之间")
@Schema(description = "USB PID")
private Integer usbProductId;
}
@@ -17,6 +17,9 @@ public class HardAfterSaleDto extends HardAfterSaleEntity implements BaseUserIde
/** 售后申请用户主键集合;为空时不限制申请人。 */
private List<Long> userIds;
/** 关联原订单的用户主键集合;用于区分代订场景中的退款发起人和实际订单用户。 */
private List<Long> orderUserIds;
/** UPMS 用户身份协议编码集合;为空时不限制身份。 */
private List<String> identityTypes;
@@ -37,28 +37,82 @@ public class HardInvoiceEntity extends Model<HardInvoiceEntity> {
@Schema(description="部门id")
private Long deptId;
/**
* 设备id
*/
@Schema(description="设备id")
/**
* 来源业务终端 ID;历史记录继续保留原设备语义,不改写为打印机 ID。
*/
@Schema(description="来源业务终端ID")
private Long hardId;
/**
* 实际接收任务的尔印打印终端 ID;历史记录和绑定失败任务允许为空。
*/
@Schema(description="目标打印终端ID")
private Long printerHardId;
/**
* 硬件订单id
*/
@Schema(description="硬件订单id")
private Long orderId;
/**
* 状态
*/
@Schema(description="状态")
/**
* 打印任务状态,编码来自 HardPrintStatusEnum。
*/
@Schema(description="打印任务状态")
private String status;
/**
* 发票图片
*/
@Schema(description="发票图片")
/**
* 打印凭证类型,1 发票、2 兑换券;历史空值按发票兼容。
*/
@Schema(description="打印凭证类型")
private String printType;
/**
* 同一订单同一凭证类型的打印序号;重试同一任务时保持不变。
*/
@Schema(description="打印序号")
private Integer printNo;
/**
* 人工重试版本;终端用任务 ID 和该版本共同判断是否允许再次打印。
*/
@Schema(description="重试版本")
private Integer retryVersion;
/**
* 打印终端上报的 USB 累计尝试次数。
*/
@Schema(description="终端尝试次数")
private Integer attemptCount;
/**
* 最近失败阶段,编码来自 HardPrintFailureStageEnum。
*/
@Schema(description="失败阶段")
private String failureStage;
/**
* 最近失败摘要,不保存 MQTT 密码、访问令牌等敏感信息。
*/
@Schema(description="最近失败原因")
private String lastError;
/**
* 打印终端确认完整 USB 数据和切纸命令写入成功的时间。
*/
@Schema(description="打印成功时间")
private LocalDateTime printedAt;
/**
* 人工重打原因,自动首次打印或失败重试允许为空。
*/
@Schema(description="重打原因")
private String reprintReason;
/**
* 后端生成并上传的打印 PNG 地址;待生成和生成失败时允许为空。
*/
@Schema(description="打印图片")
private String pic;
/**
@@ -102,4 +156,4 @@ public class HardInvoiceEntity extends Model<HardInvoiceEntity> {
*/
@Schema(description="租户ID")
private Long tenantId;
}
}
@@ -0,0 +1,78 @@
package com.bsd.pjcloud.hard.api.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
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.baomidou.mybatisplus.extension.activerecord.Model;
import com.bsd.pjcloud.common.core.util.TenantTable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 业务终端与尔印打印终端的一对一绑定关系。
*
* <p>记录使用物理删除,数据库分别对来源和目标设备设置唯一键,确保任何时刻都不能出现多重绑定。</p>
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TenantTable
@TableName("hard_printer_binding")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "业务终端与打印终端绑定关系")
public class HardPrinterBindingEntity extends Model<HardPrinterBindingEntity> {
/** 绑定关系主键。 */
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "绑定关系主键")
private Long id;
/** 来源业务终端 ID,只允许 SUPERMARKET 或 CANTEEN。 */
@Schema(description = "来源业务终端ID")
private Long sourceHardId;
/** 目标打印终端 ID,只允许 EY_PRINT。 */
@Schema(description = "目标打印终端ID")
private Long printerHardId;
/** 双方共同所属商户 ID 快照。 */
@Schema(description = "商户ID")
private Long merchantId;
/** 双方共同所属商户部门 ID 快照。 */
@Schema(description = "商户部门ID")
private Long deptId;
/** 创建人。 */
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建人")
private String createBy;
/** 创建时间。 */
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建时间")
private LocalDateTime createTime;
/** 修改人。 */
@TableField(fill = FieldFill.INSERT_UPDATE)
@Schema(description = "修改人")
private String updateBy;
/** 修改时间。 */
@TableField(fill = FieldFill.INSERT_UPDATE)
@Schema(description = "修改时间")
private LocalDateTime updateTime;
/** 租户 ID。 */
@Schema(description = "租户ID")
private Long tenantId;
}
@@ -0,0 +1,88 @@
package com.bsd.pjcloud.hard.api.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
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.baomidou.mybatisplus.extension.activerecord.Model;
import com.bsd.pjcloud.common.core.util.TenantTable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 尔印打印终端的 USB 和 ESC/POS 参数配置。
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TenantTable
@TableName("hard_printer_config")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "尔印打印终端配置")
public class HardPrinterConfigEntity extends Model<HardPrinterConfigEntity> {
/** 配置主键。 */
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "配置主键")
private Long id;
/** 配置所属 EY_PRINT 设备 ID。 */
@Schema(description = "打印终端ID")
private Long printerHardId;
/** 真机联调后填写的打印机型号。 */
@Schema(description = "打印机型号")
private String printerModel;
/** USB Vendor ID,使用十进制持久化。 */
@Schema(description = "USB VID")
private Integer usbVendorId;
/** USB Product ID,使用十进制持久化。 */
@Schema(description = "USB PID")
private Integer usbProductId;
/** 纸张宽度,首期固定为 80mm。 */
@Schema(description = "纸宽,单位mm")
private Integer paperWidth;
/** 是否在完整数据写入后发送自动切纸命令,1 开启、0 关闭。 */
@Schema(description = "自动切刀开关")
private String autoCut;
/** 图片指令模式,GS_V0 为默认值,ESC_STAR 为兼容回退。 */
@Schema(description = "图片指令模式")
private String imageMode;
/** 创建人。 */
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建人")
private String createBy;
/** 创建时间。 */
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建时间")
private LocalDateTime createTime;
/** 修改人。 */
@TableField(fill = FieldFill.INSERT_UPDATE)
@Schema(description = "修改人")
private String updateBy;
/** 修改时间。 */
@TableField(fill = FieldFill.INSERT_UPDATE)
@Schema(description = "修改时间")
private LocalDateTime updateTime;
/** 租户 ID。 */
@Schema(description = "租户ID")
private Long tenantId;
}
@@ -0,0 +1,333 @@
package com.bsd.pjcloud.hard.api.utils;
import javax.imageio.ImageIO;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* 面向 80mm、576px ESC/POS 打印的动态高度 PNG 绘制器。
*
* <p>商品名称按真实像素宽度换行,数量列固定右对齐,保证长名称不会与数量错配。</p>
*/
public class ThermalReceiptImage {
/** 80mm 打印头常见有效像素宽度。 */
public static final int PAPER_WIDTH_PIXELS = 576;
/** 图片左右安全留白。 */
private static final int HORIZONTAL_PADDING = 24;
/** 图片顶部和底部安全留白。 */
private static final int VERTICAL_PADDING = 24;
/** 每次扩展画布的最小高度。 */
private static final int HEIGHT_GROWTH = 512;
/** 数量列保留宽度。 */
private static final int QUANTITY_COLUMN_WIDTH = 96;
/** 套餐组成项左侧缩进。 */
private static final int CHILD_INDENT = 32;
/** 中文优先逻辑字体名称。 */
private static final String FONT_FAMILY = "SansSerif";
/** 当前可扩展画布。 */
private BufferedImage canvas;
/** 当前绘制上下文。 */
private Graphics2D graphics;
/** 下一块内容的顶部 Y 坐标。 */
private int cursorY = VERTICAL_PADDING;
/**
* 创建 576px 宽的白底收据画布。
*/
public ThermalReceiptImage() {
canvas = new BufferedImage(PAPER_WIDTH_PIXELS, HEIGHT_GROWTH, BufferedImage.TYPE_INT_RGB);
graphics = canvas.createGraphics();
configureGraphics(graphics);
graphics.setColor(Color.WHITE);
graphics.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
graphics.setColor(Color.BLACK);
}
/**
* 绘制居中粗体标题。
*
* @param text 标题文本
* @return 当前绘制器
*/
public ThermalReceiptImage title(String text) {
setFont(Font.BOLD, 34);
drawCentered(text, 16);
return this;
}
/**
* 绘制带黑色边框的大号打印次数提示。
*
* @param text 提示文本,例如“第 1 次打印”
* @return 当前绘制器
*/
public ThermalReceiptImage banner(String text) {
setFont(Font.BOLD, 38);
FontMetrics metrics = graphics.getFontMetrics();
int height = metrics.getHeight() + 30;
ensureCapacity(height + 20);
graphics.setStroke(new BasicStroke(3F));
graphics.drawRect(HORIZONTAL_PADDING, cursorY, contentWidth(), height);
int x = HORIZONTAL_PADDING + Math.max(0, (contentWidth() - metrics.stringWidth(text)) / 2);
graphics.drawString(text, x, cursorY + 15 + metrics.getAscent());
cursorY += height + 20;
return this;
}
/**
* 绘制醒目的核销短码。
*
* @param shortCode 核销短码
* @return 当前绘制器
*/
public ThermalReceiptImage verificationCode(String shortCode) {
setFont(Font.BOLD, 52);
drawCentered(shortCode == null || shortCode.isBlank() ? "--" : shortCode, 18);
return this;
}
/**
* 绘制一行左右键值信息,值过长时在下一行继续换行。
*
* @param label 左侧字段名
* @param value 右侧字段值
* @return 当前绘制器
*/
public ThermalReceiptImage keyValue(String label, String value) {
setFont(Font.PLAIN, 25);
String safeLabel = label == null ? "" : label;
String safeValue = value == null || value.isBlank() ? "--" : value;
FontMetrics metrics = graphics.getFontMetrics();
int labelWidth = metrics.stringWidth(safeLabel + ":");
int valueWidth = contentWidth() - labelWidth - 12;
List<String> lines = wrap(safeValue, Math.max(120, valueWidth), metrics);
int lineHeight = metrics.getHeight() + 6;
ensureCapacity(lines.size() * lineHeight + 10);
graphics.drawString(safeLabel + ":", HORIZONTAL_PADDING, cursorY + metrics.getAscent());
for (int index = 0; index < lines.size(); index++) {
int x = index == 0 ? HORIZONTAL_PADDING + labelWidth + 12 : HORIZONTAL_PADDING + labelWidth + 12;
graphics.drawString(lines.get(index), x, cursorY + index * lineHeight + metrics.getAscent());
}
cursorY += lines.size() * lineHeight + 6;
return this;
}
/**
* 绘制商品明细区标题。
*
* @param text 标题文本
* @return 当前绘制器
*/
public ThermalReceiptImage sectionTitle(String text) {
setFont(Font.BOLD, 28);
ensureCapacity(48);
graphics.drawString(text == null ? "" : text, HORIZONTAL_PADDING, cursorY + graphics.getFontMetrics().getAscent());
cursorY += graphics.getFontMetrics().getHeight() + 10;
return this;
}
/**
* 绘制商品或套餐组成项,商品名换行且数量始终在首行右对齐。
*
* @param name 商品名称
* @param quantity 数量文本
* @param child 是否为套餐组成项
* @param emphasized 是否使用粗体标记套餐主项
* @return 当前绘制器
*/
public ThermalReceiptImage item(String name, String quantity, boolean child, boolean emphasized) {
return item(name, quantity, child, emphasized, QUANTITY_COLUMN_WIDTH);
}
/**
* 绘制带数量和金额摘要的发票商品行。
*
* @param name 商品名称
* @param quantity 数量或重量文本
* @param amount 含货币符号的商品总价
* @return 当前绘制器
*/
public ThermalReceiptImage invoiceItem(String name, String quantity, String amount) {
return item(name, String.format("%s %s", quantity, amount), false, false, 190);
}
/**
* 使用指定尾列宽度绘制一条可换行明细。
*
* @param name 左侧名称
* @param trailing 首行右对齐摘要
* @param child 是否为套餐组成项
* @param emphasized 是否使用粗体
* @param trailingWidth 尾列预留像素宽度
* @return 当前绘制器
*/
private ThermalReceiptImage item(String name, String trailing, boolean child, boolean emphasized,
int trailingWidth) {
setFont(emphasized ? Font.BOLD : Font.PLAIN, child ? 23 : 26);
FontMetrics metrics = graphics.getFontMetrics();
int indent = child ? CHILD_INDENT : 0;
String prefix = child ? "- " : "";
String safeName = prefix + (name == null || name.isBlank() ? "未命名商品" : name);
List<String> lines = wrap(safeName, contentWidth() - indent - trailingWidth, metrics);
int lineHeight = metrics.getHeight() + 5;
ensureCapacity(lines.size() * lineHeight + 8);
for (int index = 0; index < lines.size(); index++) {
graphics.drawString(lines.get(index), HORIZONTAL_PADDING + indent,
cursorY + index * lineHeight + metrics.getAscent());
}
String safeTrailing = trailing == null || trailing.isBlank() ? "x0" : trailing;
graphics.drawString(safeTrailing, PAPER_WIDTH_PIXELS - HORIZONTAL_PADDING - metrics.stringWidth(safeTrailing),
cursorY + metrics.getAscent());
cursorY += lines.size() * lineHeight + 6;
return this;
}
/**
* 绘制横向分隔线。
*
* @return 当前绘制器
*/
public ThermalReceiptImage divider() {
ensureCapacity(28);
graphics.setStroke(new BasicStroke(2F));
graphics.drawLine(HORIZONTAL_PADDING, cursorY + 10,
PAPER_WIDTH_PIXELS - HORIZONTAL_PADDING, cursorY + 10);
cursorY += 28;
return this;
}
/**
* 输出裁剪到实际高度的 PNG 字节。
*
* @return PNG 字节
* @throws IOException PNG 编码失败
*/
public byte[] toPngBytes() throws IOException {
int finalHeight = cursorY + VERTICAL_PADDING;
BufferedImage result = new BufferedImage(PAPER_WIDTH_PIXELS, finalHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D resultGraphics = result.createGraphics();
resultGraphics.setColor(Color.WHITE);
resultGraphics.fillRect(0, 0, result.getWidth(), result.getHeight());
resultGraphics.drawImage(canvas, 0, 0, null);
resultGraphics.dispose();
graphics.dispose();
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
ImageIO.write(result, "png", output);
return output.toByteArray();
}
}
/**
* 设置当前字体。
*
* @param style 字体样式
* @param size 字体像素大小
*/
private void setFont(int style, int size) {
graphics.setFont(new Font(FONT_FAMILY, style, size));
}
/**
* 绘制一行居中文本并推进游标。
*
* @param text 文本
* @param bottomSpace 底部留白
*/
private void drawCentered(String text, int bottomSpace) {
String safeText = text == null ? "" : text;
FontMetrics metrics = graphics.getFontMetrics();
ensureCapacity(metrics.getHeight() + bottomSpace);
int x = Math.max(HORIZONTAL_PADDING,
(PAPER_WIDTH_PIXELS - metrics.stringWidth(safeText)) / 2);
graphics.drawString(safeText, x, cursorY + metrics.getAscent());
cursorY += metrics.getHeight() + bottomSpace;
}
/**
* 按实际字体宽度拆分字符串,单个超宽字符仍至少独占一行。
*
* @param text 原文本
* @param maxWidth 最大像素宽度
* @param metrics 当前字体度量
* @return 非空行列表
*/
private List<String> wrap(String text, int maxWidth, FontMetrics metrics) {
List<String> lines = new ArrayList<>();
StringBuilder line = new StringBuilder();
String safeText = text == null ? "" : text;
for (int offset = 0; offset < safeText.length();) {
int codePoint = safeText.codePointAt(offset);
String character = new String(Character.toChars(codePoint));
if (line.length() > 0 && metrics.stringWidth(line + character) > maxWidth) {
lines.add(line.toString());
line.setLength(0);
}
line.append(character);
offset += Character.charCount(codePoint);
}
if (line.length() > 0 || lines.isEmpty()) lines.add(line.toString());
return lines;
}
/**
* 确保画布能够容纳下一块内容,并在扩容时保留字体、画笔和已绘制像素。
*
* @param requiredHeight 下一块内容需要的高度
*/
private void ensureCapacity(int requiredHeight) {
if (cursorY + requiredHeight <= canvas.getHeight()) return;
int newHeight = canvas.getHeight();
while (cursorY + requiredHeight > newHeight) newHeight += HEIGHT_GROWTH;
Font font = graphics.getFont();
Color color = graphics.getColor();
BufferedImage expanded = new BufferedImage(PAPER_WIDTH_PIXELS, newHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D expandedGraphics = expanded.createGraphics();
configureGraphics(expandedGraphics);
expandedGraphics.setColor(Color.WHITE);
expandedGraphics.fillRect(0, 0, expanded.getWidth(), expanded.getHeight());
expandedGraphics.drawImage(canvas, 0, 0, null);
expandedGraphics.setFont(font);
expandedGraphics.setColor(color);
graphics.dispose();
canvas = expanded;
graphics = expandedGraphics;
}
/**
* 配置文字抗锯齿和高质量图片渲染参数。
*
* @param target 待配置绘制上下文
*/
private static void configureGraphics(Graphics2D target) {
target.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
target.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
}
/**
* 计算扣除左右安全留白后的有效宽度。
*
* @return 内容宽度
*/
private static int contentWidth() {
return PAPER_WIDTH_PIXELS - HORIZONTAL_PADDING * 2;
}
}
@@ -67,6 +67,9 @@ public class HardInvoiceVo extends HardInvoiceEntity implements BaseUserDeptInte
private String hardType;
/** 目标尔印打印终端名称。 */
private String printerHardName;
/**
* 用户相关
*/
@@ -0,0 +1,32 @@
package com.bsd.pjcloud.hard.api.vo;
import com.bsd.pjcloud.hard.api.entity.HardPrinterBindingEntity;
import com.bsd.pjcloud.hard.api.entity.HardPrinterConfigEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 打印终端绑定关系及两端设备展示信息。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "打印终端绑定详情")
public class HardPrinterBindingVo extends HardPrinterBindingEntity {
/** 来源业务终端名称。 */
@Schema(description = "来源业务终端名称")
private String sourceHardName;
/** 来源业务终端类型。 */
@Schema(description = "来源业务终端类型")
private String sourceHardType;
/** 目标打印终端名称。 */
@Schema(description = "目标打印终端名称")
private String printerHardName;
/** 目标打印终端当前配置;尚未保存时返回默认配置。 */
@Schema(description = "打印终端配置")
private HardPrinterConfigEntity printerConfig;
}
@@ -59,4 +59,34 @@ public class HardManagerVo extends HardManagerEntity implements BaseDeptInterfac
*/
private String disconnectReason;
/** 当前来源业务终端所绑定的 EY_PRINT 设备 ID;未绑定或当前行为打印机时为空。 */
private Long boundPrinterHardId;
/** 当前来源业务终端所绑定的 EY_PRINT 设备名称。 */
private String boundPrinterHardName;
/** 当前 EY_PRINT 反向绑定的来源业务终端 ID;未绑定或当前行为来源设备时为空。 */
private Long boundSourceHardId;
/** 当前 EY_PRINT 反向绑定的来源业务终端名称。 */
private String boundSourceHardName;
/** EY_PRINT 型号;未维护配置或非打印设备时为空。 */
private String printerModel;
/** EY_PRINT 的 USB 厂商编号 VID;尚未真机回填时为空。 */
private Integer usbVendorId;
/** EY_PRINT 的 USB 产品编号 PID;尚未真机回填时为空。 */
private Integer usbProductId;
/** 热敏纸宽度,首期固定为 80mm。 */
private Integer paperWidth;
/** 自动切刀开关,1 开启、0 关闭。 */
private String autoCut;
/** 图片指令模式,首选 GS_V0,兼容 ESC_STAR。 */
private String imageMode;
}
@@ -5,6 +5,7 @@ import com.bsd.pjcloud.admin.api.vo.base.BaseUserIdentityInterface;
import com.bsd.pjcloud.admin.api.dto.user.SysUserStudentDto;
import com.bsd.pjcloud.admin.api.dto.user.SysUserTeacherDto;
import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity;
import com.bsd.pjcloud.hard.api.entity.order.HardTakeOutApptEntity;
import com.bsd.pjcloud.hard.api.vo.base.BaseHardInterface;
import com.bsd.pjcloud.wallet.api.vo.base.BaseMchInterface;
import com.bsd.pjcloud.wallet.api.vo.base.BaseWalletInterface;
@@ -105,6 +106,13 @@ public class HardOrderVo extends HardOrderEntity implements BaseUserDeptInterfac
private LocalTime apptEndTime;
/**
* 外卖订单的期望送达配置,来源于 hard_take_out_appt;非外卖或历史未记录时为空。
*
* <p>该字段只参与订单详情响应,不对应 hard_order 表持久化字段。</p>
*/
private HardTakeOutApptEntity takeOutAppt;
/**
* 付款人用户ID,来源于订单关联的父购物车。
*/
@@ -129,4 +137,28 @@ public class HardOrderVo extends HardOrderEntity implements BaseUserDeptInterfac
* 订单详情中的长期识别证据列表,按证据类型、角色和序号排序。
*/
private List<HardOrderEvidenceImageVo> evidenceImages;
/** 当前订单最新发票任务 ID;没有记录时为空。 */
private Long latestInvoiceTaskId;
/** 当前订单最新发票任务状态编码。 */
private String latestInvoiceStatus;
/** 当前订单最新发票打印序号。 */
private Integer latestInvoicePrintNo;
/** 当前订单最新发票任务的来源业务终端 ID。 */
private Long latestInvoiceSourceHardId;
/** 当前订单最新兑换券任务 ID;没有记录时为空。 */
private Long latestVoucherTaskId;
/** 当前订单最新兑换券任务状态编码。 */
private String latestVoucherStatus;
/** 当前订单最新兑换券打印序号。 */
private Integer latestVoucherPrintNo;
/** 当前订单最新兑换券任务的来源业务终端 ID。 */
private Long latestVoucherSourceHardId;
}