feat:更改页面

This commit is contained in:
dzc committed 2026-09-08 15:25:23 +08:00
1 parent 4e804a17cd
commit 8995bf89d6
174 files changed
+11342 -220

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);
}
}
@@ -23,8 +23,11 @@ public enum HardTypeEnum {
OTHER("6", "其他支付方式(第三方接口对接)"),
MERCHANT_ACT("7", "商户活动"),
FARMER("8", "共富商城(助农)"),
YUN_XFJ("9", "云版消费机"),
YYX("10", "4G-云音响"),
YYX_WIFI("11", "wifi-云音响"),
/** 由绑定的超市或食堂 Android 终端代理驱动的无屏尔印 USB 打印机。 */
EY_PRINT("12", "尔印打印机器"),
;
private final String type;
private final String description;
@@ -44,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,19 @@
package com.bsd.pjcloud.hard.api.constant;
/**
* 终端心跳场景。
*
* <p>枚举名称是终端与服务端之间的固定接口编码,不得随展示文案调整。</p>
*/
public enum TerminalHeartbeatSceneEnum {
/**
* 普通在线心跳;收到后清除之前的计划更新断开原因。
*/
ONLINE,
/**
* 自动更新安装前心跳;用于在应用重启期间保留可解释的离线原因。
*/
AUTO_UPDATE
}
@@ -15,6 +15,7 @@ public enum HardHourTypeEnum {
MORNING("1", "早餐"),
NOONING("2", "午餐"),
NIGHT("3", "晚餐"),
LATE_NIGHT("4", "夜间餐"),
;
private final String type;
private final String description;
@@ -0,0 +1,58 @@
package com.bsd.pjcloud.hard.api.constant.order;
import lombok.Getter;
import java.util.Arrays;
/**
* 订单证据图片角色,编码用于唯一位置约束和跨端展示。
*/
@Getter
public enum HardOrderEvidenceRoleEnum {
/**
* 人脸库中被命中的后台参考人脸,由服务端从受信文件中心复制为订单快照。
*/
FACE_REFERENCE("FACE_REFERENCE", "后台参考人脸"),
/**
* 终端实际通过比对的现场人脸裁剪图,由终端异步上传。
*/
FACE_LIVE("FACE_LIVE", "现场识别人脸"),
/**
* 菜谱识别的现场餐盘图片预留角色,当前版本不创建该类证据。
*/
MENU_LIVE("MENU_LIVE", "餐盘现场图");
/**
* 数据库和接口使用的稳定编码。
*/
private final String code;
/**
* PC 订单详情使用的中文名称。
*/
private final String description;
/**
* 构造订单证据图片角色。
*
* @param code 数据库与跨端协议编码
* @param description 中文展示名称
*/
HardOrderEvidenceRoleEnum(String code, String description) {
this.code = code;
this.description = description;
}
/**
* 按协议编码解析图片角色,未知编码返回 {@code null}。
*
* @param code 数据库或接口编码
* @return 对应角色;未知时返回 {@code null}
*/
public static HardOrderEvidenceRoleEnum valueOfCode(String code) {
return Arrays.stream(values()).filter(item -> item.code.equals(code)).findFirst().orElse(null);
}
}
@@ -0,0 +1,58 @@
package com.bsd.pjcloud.hard.api.constant.order;
import lombok.Getter;
import java.util.Arrays;
/**
* 服务端订单证据状态,表达证据是否仍待补传、已经可查看或已永久缺失。
*/
@Getter
public enum HardOrderEvidenceStatusEnum {
/**
* 订单已记录识别元数据,但图片仍等待终端上传或服务端复制。
*/
PENDING("PENDING", "待补传"),
/**
* 文件中心已经保存订单专属图片并可在订单详情中查看。
*/
AVAILABLE("AVAILABLE", "可查看"),
/**
* 本地文件过期、损坏或参考图片变化,已确认无法继续补齐。
*/
UNAVAILABLE("UNAVAILABLE", "永久缺失");
/**
* 数据库和接口使用的稳定编码。
*/
private final String code;
/**
* PC 订单详情使用的中文名称。
*/
private final String description;
/**
* 构造订单证据状态。
*
* @param code 数据库与跨端协议编码
* @param description 中文展示名称
*/
HardOrderEvidenceStatusEnum(String code, String description) {
this.code = code;
this.description = description;
}
/**
* 按协议编码解析证据状态,未知编码返回 {@code null}。
*
* @param code 数据库或接口编码
* @return 对应状态;未知时返回 {@code null}
*/
public static HardOrderEvidenceStatusEnum valueOfCode(String code) {
return Arrays.stream(values()).filter(item -> item.code.equals(code)).findFirst().orElse(null);
}
}
@@ -0,0 +1,53 @@
package com.bsd.pjcloud.hard.api.constant.order;
import lombok.Getter;
import java.util.Arrays;
/**
* 订单证据业务类型,编码同时用于数据库、终端 SQLite 和跨端接口,禁止随展示文案调整。
*/
@Getter
public enum HardOrderEvidenceTypeEnum {
/**
* 人脸识别证据,当前版本保存后台参考人脸和现场识别人脸。
*/
FACE_RECOGNITION("FACE_RECOGNITION", "人脸识别"),
/**
* 菜谱识别证据预留值,当前版本不执行菜谱识别或图片采集。
*/
MENU_RECOGNITION("MENU_RECOGNITION", "菜谱识别");
/**
* 数据库和接口使用的稳定编码。
*/
private final String code;
/**
* PC 订单详情使用的中文名称。
*/
private final String description;
/**
* 构造订单证据业务类型。
*
* @param code 数据库与跨端协议编码
* @param description 中文展示名称
*/
HardOrderEvidenceTypeEnum(String code, String description) {
this.code = code;
this.description = description;
}
/**
* 按协议编码解析证据类型,未知编码返回 {@code null} 供调用方拒绝非法请求。
*
* @param code 数据库或接口编码
* @return 对应类型;未知时返回 {@code null}
*/
public static HardOrderEvidenceTypeEnum valueOfCode(String code) {
return Arrays.stream(values()).filter(item -> item.code.equals(code)).findFirst().orElse(null);
}
}
@@ -20,6 +20,11 @@ public enum HardOrderStatusEnum {
REFUND("10", "退款"),
CANCEL("20", "已核销"),
UNFOLLOW("30", "已取消(智能柜)"),
TAKE_OUT_AWAIT("100", "待接单(外卖)"),
TAKE_OUT_ING("110", "配送中(外卖)"),
SHOP_WAIT_PAY_SUCCESS("200", "待发货/已支付(商城购买)"),
SHOP_WAIT_RECEIVE("210", "待收货(商城购买)"),
SHOP_WAIT_EVALUATE("220", "订单完成/待评价(商城购买)"),
;
private final String type;
private final String description;
@@ -21,6 +21,8 @@ public enum HardOrderTypeEnum {
LB("6", "宁波银行(美好生活)"),
FARMER("7", "共富商城(助农)"),
TOC_VISITOR("8", "toc访客购买(不记名用户)"),
XFJ("9", "消费机支付"),
TAKE_OUT("10", "外卖"),
;
private final String type;
private final String description;
@@ -0,0 +1,38 @@
package com.bsd.pjcloud.hard.api.constant.xfj;
import cn.hutool.core.util.ObjectUtil;
import com.bsd.pjcloud.hard.api.constant.order.HardPayTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 硬件授权类型
*/
@AllArgsConstructor
@Getter
public enum HardXfjTypeEnum {
QRCODE_PAY("10", "钱包二维码支付"),
WECHAT_QRCODE_PAY("11", "微信二维码支付"),
ALI_QRCODE_PAY("12", "支付宝二维码支付"),
CARD_PAY("20", "饭卡支付"),
;
private final String type;
private final String description;
public static HardXfjTypeEnum valueOfByType(String type) {
if (ObjectUtil.isEmpty(type)) return null;
return Arrays.stream(values()).filter(v -> ObjectUtil.equals(type, v.getType())).findFirst().orElse(null);
}
public static HardPayTypeEnum getHardPayType(HardXfjTypeEnum type) {
return switch (type) {
case QRCODE_PAY -> HardPayTypeEnum.WALLET_QR_CODE;
case WECHAT_QRCODE_PAY -> HardPayTypeEnum.WECHANT_SELF_QR_CODE_PAY;
case ALI_QRCODE_PAY -> HardPayTypeEnum.ALI_SELF_QR_CODE_PAY;
case CARD_PAY -> HardPayTypeEnum.WALLET_CARD;
};
}
}
@@ -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;
/**
* 创建业务终端与无屏尔印 USB 打印机绑定关系的请求。
*/
@Data
@Schema(description = "USB 打印机绑定请求")
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 打印机参数的请求。
*/
@Data
@Schema(description = "USB 打印机配置请求")
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;
}
@@ -0,0 +1,85 @@
package com.bsd.pjcloud.hard.api.dto.hard;
import com.bsd.pjcloud.hard.api.constant.TerminalHeartbeatSceneEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import java.util.Map;
/**
* 食堂、超市 Android 终端心跳请求。
*/
@Data
@Schema(description = "终端心跳请求")
public class TerminalHeartbeatDto {
/**
* APK 展示版本号,来源于终端当前安装包;取不到时允许为空。
*/
@Schema(description = "APK展示版本号")
private String apkVersionName;
/**
* APK 内部版本码,来源于终端当前安装包;取不到时允许为空。
*/
@Schema(description = "APK内部版本码")
private Long apkVersionCode;
/**
* 本次上报场景,使用 {@link TerminalHeartbeatSceneEnum} 的固定接口名称。
*/
@NotNull(message = "心跳场景不能为空")
@Schema(description = "心跳场景", requiredMode = Schema.RequiredMode.REQUIRED)
private TerminalHeartbeatSceneEnum scene;
/**
* Android 系统本次启动后的运行秒数,用于区分整机重启与应用进程重启。
*/
@Schema(description = "Android系统运行秒数")
private Long systemUptimeSeconds;
/**
* 当前应用进程运行秒数,用于识别应用崩溃或被系统重新拉起。
*/
@Schema(description = "应用进程运行秒数")
private Long appUptimeSeconds;
/**
* 最近上报时使用的网络类型,例如 ethernet、wifi、4g 或 offline。
*/
@Schema(description = "网络类型")
private String networkType;
/**
* 应用私有目录所在存储卷的可用空间,单位为字节。
*/
@Schema(description = "可用存储空间,单位字节")
private Long availableStorageBytes;
/**
* Root 最近检测状态,持久化值固定为 unknown、available 或 unavailable。
*/
@Pattern(regexp = "unknown|available|unavailable", message = "Root状态不正确")
@Schema(description = "Root状态")
private String rootStatus;
/**
* 当前应用是否已被 Android 配置为 Device Owner;无法读取时为空。
*/
@Schema(description = "是否为Device Owner")
private Boolean deviceOwner;
/**
* 最近上报时是否处于 Kiosk 锁定任务模式;无法读取时为空。
*/
@Schema(description = "是否处于Kiosk模式")
private Boolean kioskActive;
/**
* 可选外围硬件状态,以稳定功能编码映射布尔值,例如 printer、steelyard、cardReader、subScreen。
*/
@Schema(description = "可选外围硬件状态")
private Map<String, Boolean> deviceStatus;
}
@@ -0,0 +1,47 @@
package com.bsd.pjcloud.hard.api.dto.manager;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 终端日志管理端查询条件。
*/
@Data
@Schema(description = "终端日志文件查询条件")
public class HardTerminalLogFileQueryDto {
@Schema(description = "设备ID")
private Long hardId;
@Schema(description = "设备名称")
private String hardName;
@Schema(description = "设备唯一编号")
private String uniqueCode;
@Schema(description = "商户ID")
private Long merchantId;
@Schema(description = "部门ID")
private Long deptId;
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
@Schema(description = "日志日期开始")
private LocalDate logDateStart;
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
@Schema(description = "日志日期结束")
private LocalDate logDateEnd;
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
@Schema(description = "上传时间开始")
private LocalDateTime uploadTimeStart;
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
@Schema(description = "上传时间结束")
private LocalDateTime uploadTimeEnd;
}
@@ -0,0 +1,32 @@
package com.bsd.pjcloud.hard.api.dto.manager;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* 终端日志上传预检参数。
*/
@Data
@Schema(description = "终端日志上传预检参数")
public class HardTerminalLogPreflightDto {
@NotBlank(message = "日志相对路径不能为空")
@Size(max = 255, message = "日志相对路径不能超过255个字符")
@Schema(description = "终端日志根目录下的相对路径")
private String relativePath;
@NotNull(message = "文件大小不能为空")
@Min(value = 0, message = "文件大小不能小于0")
@Schema(description = "脱敏文件大小")
private Long fileSize;
@NotBlank(message = "文件哈希不能为空")
@Pattern(regexp = "(?i)^[0-9a-f]{32}$", message = "文件哈希必须为32位MD5")
@Schema(description = "脱敏文件MD5")
private String fileHash;
}
@@ -0,0 +1,43 @@
package com.bsd.pjcloud.hard.api.dto.manager;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 终端日志上传元数据。设备快照由服务端根据请求头中的设备ID填充。
*/
@Data
@Schema(description = "终端日志上传元数据")
public class HardTerminalLogUploadDto {
@NotBlank(message = "日志相对路径不能为空")
@Size(max = 255, message = "日志相对路径不能超过255个字符")
@Schema(description = "终端日志根目录下的相对路径")
private String relativePath;
@NotNull(message = "日志日期不能为空")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
@Schema(description = "日志所属日期")
private LocalDate logDate;
@NotNull(message = "本地修改时间不能为空")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
@Schema(description = "终端本地文件最后修改时间")
private LocalDateTime fileModifiedTime;
@Size(max = 64, message = "应用版本不能超过64个字符")
@Schema(description = "应用版本")
private String appVersion;
@NotBlank(message = "脱敏规则版本不能为空")
@Size(max = 32, message = "脱敏规则版本不能超过32个字符")
@Schema(description = "脱敏规则版本")
private String sanitizeVersion;
}
@@ -0,0 +1,130 @@
package com.bsd.pjcloud.hard.api.dto.order;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单创建和证据补传共用的识别元数据,不包含图片字节和服务端文件信息。
*/
@Data
@Schema(description = "订单识别证据元数据")
public class HardOrderEvidenceMetadataDto {
/**
* 证据业务类型,使用 HardOrderEvidenceTypeEnum 的稳定编码。
*/
@NotBlank(message = "证据类型不能为空")
@Size(max = 32, message = "证据类型不能超过32个字符")
@Schema(description = "证据类型")
private String evidenceType;
/**
* 图片角色,使用 HardOrderEvidenceRoleEnum 的稳定编码。
*/
@NotBlank(message = "证据图片角色不能为空")
@Size(max = 32, message = "证据图片角色不能超过32个字符")
@Schema(description = "证据图片角色")
private String evidenceRole;
/**
* 同类型同角色下的图片序号,从0开始;人脸证据固定为0,菜谱识别可递增。
*/
@NotNull(message = "证据图片序号不能为空")
@Min(value = 0, message = "证据图片序号不能小于0")
@Max(value = 99, message = "证据图片序号不能大于99")
@Schema(description = "证据图片序号")
private Integer sequenceNo;
/**
* 识别用户主键;未识别到平台用户时为空,不能由终端使用其他用户替代。
*/
@Schema(description = "识别用户主键")
private Long userId;
/**
* 虹软搜索样本编号;仅人脸识别角色使用,服务端据此校验后台参考人脸。
*/
@Size(max = 64, message = "人脸样本编号不能超过64个字符")
@Schema(description = "人脸样本编号")
private String faceSampleId;
/**
* 识别时终端人脸库记录的参考图片地址,仅作为服务端复制参考图的线索,不直接作为可访问地址返回。
*/
@Size(max = 512, message = "参考人脸地址不能超过512个字符")
@Schema(description = "参考人脸地址")
private String referenceImageUrl;
/**
* 识别时终端人脸快照对应的后台更新时间;旧终端允许为空,新终端用于识别同地址内容替换。
*/
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss[.SSS]", timezone = "Asia/Shanghai")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
@Schema(description = "参考人脸来源版本")
private LocalDateTime referenceImageVersion;
/**
* 识别时实际相似度,取值0到1;为空表示当前证据类型不使用相似度。
*/
@DecimalMin(value = "0", message = "相似度不能小于0")
@DecimalMax(value = "1", message = "相似度不能大于1")
@Schema(description = "识别相似度")
private BigDecimal similarity;
/**
* 识别时终端生效的相似度阈值快照,取值0到1。
*/
@DecimalMin(value = "0", message = "相似度阈值不能小于0")
@DecimalMax(value = "1", message = "相似度阈值不能大于1")
@Schema(description = "相似度阈值快照")
private BigDecimal similarityThreshold;
/**
* 终端实际完成识别的本地时间;为空时服务端仅保留订单创建时间。
*/
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss[.SSS]", timezone = "Asia/Shanghai")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
@Schema(description = "终端采集时间")
private LocalDateTime capturedAt;
/**
* 终端按原始证据文件计算的 SHA-256;图片尚未生成或永久缺失时允许为空。
*/
@Pattern(regexp = "(?i)^[0-9a-f]{64}$", message = "图片哈希必须为64位SHA-256")
@Schema(description = "图片SHA-256")
private String imageHash;
/**
* 终端证据文件大小,单位字节;参考图无法读取或文件尚未生成时允许为空。
*/
@Min(value = 0, message = "图片大小不能小于0")
@Schema(description = "图片大小,单位字节")
private Long imageSize;
/**
* 终端解码后的图片宽度,单位像素。
*/
@Min(value = 1, message = "图片宽度必须大于0")
@Schema(description = "图片宽度,单位像素")
private Integer imageWidth;
/**
* 终端解码后的图片高度,单位像素。
*/
@Min(value = 1, message = "图片高度必须大于0")
@Schema(description = "图片高度,单位像素")
private Integer imageHeight;
}
@@ -0,0 +1,45 @@
package com.bsd.pjcloud.hard.api.dto.order;
import cn.hutool.core.util.StrUtil;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 终端订单识别证据的订单定位参数。
*
* <p>消费机现场下单继续使用终端订单号对账;预约核销订单不是当前终端创建的,核销成功后改用
* 服务端订单 ID 定位。两种方式都必须结合请求头中的设备 ID 校验订单归属。</p>
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "订单识别证据的订单定位参数")
public abstract class HardOrderEvidenceOrderRefDto extends HardOrderEvidenceMetadataDto {
/**
* 终端本地稳定订单号,用于消费机现场下单后对账;预约核销证据允许为空。
*/
@Size(max = 128, message = "终端订单号不能超过128个字符")
@Schema(description = "终端订单号,与服务端订单ID二选一")
private String androidOrderCode;
/**
* 已存在的服务端订单主键,仅用于当前设备已经成功核销的预约订单;现场下单证据允许为空。
*/
@Positive(message = "服务端订单ID必须大于0")
@Schema(description = "服务端订单ID,与终端订单号二选一")
private Long orderId;
/**
* 校验至少提供一种订单定位信息,防止无条件查询或写入订单证据。
*
* @return 提供终端订单号或服务端订单 ID 时返回 {@code true}
*/
@AssertTrue(message = "终端订单号和服务端订单ID不能同时为空")
public boolean isOrderReferencePresent() {
return StrUtil.isNotBlank(androidOrderCode) || orderId != null;
}
}
@@ -0,0 +1,38 @@
package com.bsd.pjcloud.hard.api.dto.order;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 终端提交订单证据元数据或永久缺失结果的请求。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "订单证据元数据提交请求")
public class HardOrderEvidenceSubmitDto extends HardOrderEvidenceOrderRefDto {
/**
* 终端请求写入的目标状态,仅允许 PENDING 或 UNAVAILABLE;现场图片上传成功由服务端改为 AVAILABLE。
*/
@NotBlank(message = "证据状态不能为空")
@Size(max = 32, message = "证据状态不能超过32个字符")
@Schema(description = "证据状态")
private String status;
/**
* 永久缺失原因;状态为 UNAVAILABLE 时必填,禁止包含本地完整敏感路径。
*/
@Size(max = 500, message = "失败原因不能超过500个字符")
@Schema(description = "永久缺失原因")
private String failureReason;
/**
* 产生识别证据的终端应用版本,用于问题追溯。
*/
@Size(max = 64, message = "应用版本不能超过64个字符")
@Schema(description = "终端应用版本")
private String appVersion;
}
@@ -0,0 +1,22 @@
package com.bsd.pjcloud.hard.api.dto.order;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 终端上传订单现场证据图片时随 multipart 一并提交的元数据。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "订单现场证据上传请求")
public class HardOrderEvidenceUploadDto extends HardOrderEvidenceOrderRefDto {
/**
* 产生识别证据的终端应用版本,用于问题追溯。
*/
@Size(max = 64, message = "应用版本不能超过64个字符")
@Schema(description = "终端应用版本")
private String appVersion;
}
@@ -0,0 +1,51 @@
package com.bsd.pjcloud.hard.api.dto.order.xfj;
import com.bsd.pjcloud.hard.api.constant.xfj.HardXfjTypeEnum;
import com.bsd.pjcloud.thirdparty.constant.miniplatform.AppPlusPlatformTypeEnum;
import com.bsd.pjcloud.wallet.api.constant.WalletTypeEnum;
import lombok.Data;
@Data
public class HardXfjFastCreateOrderDto {
/**
* 用户id
*/
private Long userId;
/**
* 设备id
*/
private Long hardId;
/**
* 订单编号
*/
private String orderNo;
/**
* 支付金额(分)
*/
private int price;
/**
* 钱包类型
*/
private WalletTypeEnum walletType;
/**
* 消费机支付类型
*/
private HardXfjTypeEnum xfjPayType;
/**
* 终端支付类型
*/
private AppPlusPlatformTypeEnum platformType;
/**
* 付款码支付code
*/
private String devicePayCode;
}
@@ -0,0 +1,18 @@
package com.bsd.pjcloud.hard.api.dto.xfj;
import com.bsd.pjcloud.hard.api.entity.xfj.HardXfjLogEntity;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class HardXfjLogDto extends HardXfjLogEntity {
/**
* 设备名称
*/
private String hardName;
private LocalDateTime[] createTimeList;
}
@@ -0,0 +1,72 @@
package com.bsd.pjcloud.hard.api.dto.xfj;
import lombok.Data;
import java.util.List;
@Data
public class XfjConsumTransactionsDto {
/**
* 消费序号(年月日时分秒+2个字节序号(0~65535)),用于识别上传数据不重复
*/
private String Order;
/**
* 10进制卡序列号(实体卡号或虚拟卡号)
*/
private String CardNo;
/**
* 卡模式
* 0:实体卡
* 1:电子卡
*/
private Integer CardMode;
/**
* 交易模式
* 0: 刷卡扣费
* 1: 现金充值
* 2: 余额查询
* 3: 钱包转账
* 4: 现金退款
*/
private Integer Mode;
/**
* 扣费类型
* 0:手动
* 1:菜单
* 2:定值
* 3:取餐
* 4:计时
* 5:计次
*/
private Integer PayType;
/**
* 消费金额(Mode为2时,此字段传入值可能为0)
*/
private String Amount;
/**
* 菜品消费列表
*/
List<Menus> Menus;
@Data
static public class Menus {
/**
* 菜品编号(与菜品查询接口菜品编号一致)
*/
private String MenuID;
/**
* 菜品数量(支持小数)
*/
private String Count;
}
}
@@ -0,0 +1,13 @@
package com.bsd.pjcloud.hard.api.dto.xfj;
import lombok.Data;
@Data
public class XfjMenusDto {
/**
* 菜品编号(操作员在消费机键盘输入的数字)
*/
private String MenuNo;
}
@@ -0,0 +1,48 @@
package com.bsd.pjcloud.hard.api.dto.xfj;
import lombok.Data;
@Data
public class XfjOffLinesDto {
/**
* 终端设备ID
*/
private Integer DeviceNumber;
/**
* 消费序号(年月日时分秒+2个字节序号),用于识别上传数据不重复
*/
private String Order;
/**
* 扣费类型
* 0:手动模式
* 1:菜单模式
* 2:定值模式
*/
private Integer PayType;
/**
* 卡模式
* 0:实体卡
* 1:电子卡
*/
private Integer CardMode;
/**
* 消费时间年月日时分秒
*/
private String Time;
/**
* 卡号10进制(实体卡号或虚拟卡号)
*/
private String CardNo;
/**
* 扣费金额
*/
private String Money;
}
@@ -0,0 +1,25 @@
package com.bsd.pjcloud.hard.api.dto.xfj;
import lombok.Data;
@Data
public class XfjOrderQueryDto {
/**
* 消费序号(年月日时分秒+2个字节序号(0~65535)),用于识别上传数据不重复
*/
private String Order;
/**
* 10进制卡序列号(实体卡号或虚拟卡号)
*/
private String CardNo;
/**
* 卡模式
* 0:实体卡
* 1:电子卡
*/
private Integer CardMode;
}
@@ -0,0 +1,17 @@
package com.bsd.pjcloud.hard.api.dto.xfj;
import lombok.Data;
@Data
public class XfjOrderRefundDto {
/**
* 消费订单号(最大30字符)
*/
private String OrderSeralid;
/**
* 退款金额
*/
private String Amount;
}
@@ -0,0 +1,55 @@
package com.bsd.pjcloud.hard.api.dto.xfj;
import lombok.Data;
import java.util.List;
@Data
public class XfjQRCodeTransactionDto {
/**
* 交易模式
* 0:扣费
* 1:充值
* 3:信息查询
*/
private String Mode;
/**
* 扣费类型
* 0:手动
* 1:菜单
* 2:定值
* 3:取餐
* 4:计时
*/
private Integer PayType;
/**
* 消费金额(Mode为3时,此字段传入值可能为0)
*/
private String Amount;
/**
* 扫描到的二维码字符串(最大长度为200)
*/
private String QR;
private List<MenuItem> Menus;
@Data
public static class MenuItem {
/**
* 菜品编号(与菜品查询接口菜品编号一致)
*/
private String MenuID;
/**
* 菜品数量(支持小数)
*/
private String Count;
}
}
@@ -0,0 +1,13 @@
package com.bsd.pjcloud.hard.api.dto.xfj;
import lombok.Data;
@Data
public class XfjServerTimeDto {
/**
* 设备中的总白名单数
*/
private Integer WLSum;
}
@@ -0,0 +1,15 @@
package com.bsd.pjcloud.hard.api.dto.xfj;
import lombok.Data;
@Data
public class XfjTransactionDto {
/**
* 从最后一笔消费往前算的位置
* 1:就是最后一笔
* 2:就是倒数第二笔
*/
private Integer Number;
}
@@ -0,0 +1,13 @@
package com.bsd.pjcloud.hard.api.dto.xfj;
import lombok.Data;
@Data
public class XfjTransactionInquiryDto {
/**
* 二维码交易订单号(该值来自于第六及第七接口返回的QROrder)
*/
private String QROrder;
}
@@ -0,0 +1,23 @@
package com.bsd.pjcloud.hard.api.dto.xfj;
import lombok.Data;
@Data
public class XfjWhiteListDto {
/**
* 通讯序号
*/
private Integer CommID;
/**
* 页数
*/
private Long Page;
/**
* 页个数(一页最多有多少个名单)
*/
private Long PageNumber;
}
@@ -8,7 +8,7 @@ import com.bsd.pjcloud.common.core.util.TenantTable;
import java.time.LocalDateTime;
/**
* 发票记录
* 打印任务与凭证记录
*
* @author dzc
* @date 2025-01-05 14:17:32
@@ -20,7 +20,7 @@ import java.time.LocalDateTime;
@TenantTable
@TableName("hard_invoice")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "发票记录")
@Schema(description = "打印任务与凭证记录")
public class HardInvoiceEntity extends Model<HardInvoiceEntity> {
@@ -37,55 +37,109 @@ 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;
/**
* 创建人
*/
@TableField(fill = FieldFill.INSERT)
@TableField(fill = FieldFill.INSERT)
@Schema(description="创建人")
private String createBy;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
@TableField(fill = FieldFill.INSERT)
@Schema(description="创建时间")
private LocalDateTime createTime;
/**
* 修改人
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
@TableField(fill = FieldFill.INSERT_UPDATE)
@Schema(description="修改人")
private String updateBy;
/**
* 修改时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
@TableField(fill = FieldFill.INSERT_UPDATE)
@Schema(description="修改时间")
private LocalDateTime updateTime;
@@ -93,7 +147,7 @@ public class HardInvoiceEntity extends Model<HardInvoiceEntity> {
* 删除标记
*/
@TableLogic
@TableField(fill = FieldFill.INSERT)
@TableField(fill = FieldFill.INSERT)
@Schema(description="删除标记")
private String delFlag;
@@ -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;
/**
* 业务 Android 终端与无屏尔印 USB 打印机资产的一对一绑定关系。
*
* <p>记录使用物理删除,数据库分别对来源和目标设备设置唯一键,确保任何时刻都不能出现多重绑定。</p>
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TenantTable
@TableName("hard_printer_binding")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "业务终端与 USB 打印机绑定关系")
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 打印机资产的 USB 和 ESC/POS 参数配置。
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TenantTable
@TableName("hard_printer_config")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "尔印 USB 打印机配置")
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,191 @@
package com.bsd.pjcloud.hard.api.entity.manager;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.bsd.pjcloud.common.core.util.TenantTable;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 终端日志文件
*/
@Data
@TenantTable
@TableName("hard_terminal_log_file")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "终端日志文件")
public class HardTerminalLogFileEntity extends Model<HardTerminalLogFileEntity> {
/**
* 编号
*/
@TableId(type = IdType.AUTO)
@Schema(description = "编号")
private Long id;
/**
* 设备ID(hard_manager.id)
*/
@Schema(description = "设备ID(hard_manager.id)")
private Long hardId;
/**
* 上传时设备名称快照
*/
@Schema(description = "上传时设备名称快照")
private String hardName;
/**
* 上传时设备唯一编号快照
*/
@Schema(description = "上传时设备唯一编号快照")
private String uniqueCode;
/**
* 上传时设备类型快照
*/
@Schema(description = "上传时设备类型快照")
private String hardType;
/**
* 上传时商户ID快照
*/
@Schema(description = "上传时商户ID快照")
private Long merchantId;
/**
* 上传时部门ID快照
*/
@Schema(description = "上传时部门ID快照")
private Long deptId;
/**
* 日志所属日期
*/
@Schema(description = "日志所属日期")
private LocalDate logDate;
/**
* 终端日志根目录下的相对路径
*/
@Schema(description = "终端日志根目录下的相对路径")
private String relativePath;
/**
* 原日志文件名
*/
@Schema(description = "原日志文件名")
private String originalName;
/**
* 文件大小(字节)
*/
@Schema(description = "文件大小(字节)")
private Long fileSize;
/**
* 文件内容哈希(当前使用MD5)
*/
@Schema(description = "文件内容哈希(当前使用MD5)")
private String fileHash;
/**
* 终端本地文件最后修改时间
*/
@Schema(description = "终端本地文件最后修改时间")
private LocalDateTime fileModifiedTime;
/**
* 文件中心sys_file记录ID
*/
@Schema(description = "文件中心sys_file记录ID")
private Long sysFileId;
/**
* OSS对象文件名
*/
@Schema(description = "OSS对象文件名")
private String storageFileName;
/**
* 文件访问相对地址
*/
@Schema(description = "文件访问相对地址")
private String storageUrl;
/**
* 上传时应用版本
*/
@Schema(description = "上传时应用版本")
private String appVersion;
/**
* 日志脱敏规则版本
*/
@Schema(description = "日志脱敏规则版本")
private String sanitizeVersion;
/**
* 最近成功上传时间
*/
@Schema(description = "最近成功上传时间")
private LocalDateTime uploadTime;
/**
* 成功上传次数
*/
@Schema(description = "成功上传次数")
private Integer uploadCount;
/**
* 备注
*/
@Schema(description = "备注")
private String remark;
/**
* 创建人
*/
@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;
/**
* 删除标记
*/
@TableLogic
@TableField(fill = FieldFill.INSERT)
@Schema(description = "删除标记")
private String delFlag;
/**
* 租户ID
*/
@Schema(description = "租户ID")
private Long tenantId;
}
@@ -0,0 +1,146 @@
package com.bsd.pjcloud.hard.api.entity.manager;
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.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 食堂、超市 Android 终端的最新心跳状态。
*
* <p>每台设备只保留一条最新记录,在线布尔值由服务端按时间阈值实时计算。</p>
*/
@Data
@TenantTable
@TableName("hard_terminal_status")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "终端最新心跳状态")
public class HardTerminalStatusEntity extends Model<HardTerminalStatusEntity> {
/**
* 设备 ID,同时作为主键,对应 {@code hard_manager.id}。
*/
@TableId(type = IdType.INPUT)
@Schema(description = "设备ID")
private Long hardId;
/**
* 服务端最近收到心跳的时间,在线计算以此时间为准。
*/
@Schema(description = "最后在线时间")
private LocalDateTime lastOnlineTime;
/**
* 最近一次心跳场景,保存 {@code ONLINE} 或 {@code AUTO_UPDATE} 接口编码。
*/
@Schema(description = "最近心跳场景")
private String heartbeatScene;
/**
* 计划内断开原因;普通在线心跳会清空,当前仅保留“自动更新重启”。
*/
@Schema(description = "计划内断开原因")
private String disconnectReason;
/**
* 终端上报的 APK 展示版本号,无法读取时为空。
*/
@Schema(description = "APK展示版本号")
private String apkVersionName;
/**
* 终端上报的 APK 内部版本码,无法读取时为空。
*/
@Schema(description = "APK内部版本码")
private Long apkVersionCode;
/**
* Android 系统本次启动后的运行时长,单位为秒;终端无法读取时为空。
*/
@Schema(description = "Android系统运行秒数")
private Long systemUptimeSeconds;
/**
* 当前应用进程运行时长,单位为秒;终端无法读取时为空。
*/
@Schema(description = "应用进程运行秒数")
private Long appUptimeSeconds;
/**
* 最近一次心跳的网络类型,例如 ethernet、wifi、4g 或 offline。
*/
@Schema(description = "网络类型")
private String networkType;
/**
* 应用私有目录所在存储卷的可用空间,单位为字节。
*/
@Schema(description = "可用存储空间,单位字节")
private Long availableStorageBytes;
/**
* Root 最近检测状态,固定为 unknown、available 或 unavailable。
*/
@Schema(description = "Root状态")
private String rootStatus;
/**
* 最近一次心跳时当前应用是否为 Device Owner;无法读取时为空。
*/
@Schema(description = "是否为Device Owner")
private Boolean deviceOwner;
/**
* 最近一次心跳时是否处于 Kiosk 锁定任务模式;无法读取时为空。
*/
@Schema(description = "是否处于Kiosk模式")
private Boolean kioskActive;
/**
* 打印机、电子秤、读卡器和副屏等可选硬件状态 JSON 快照。
*/
@Schema(description = "可选外围硬件状态JSON")
private String deviceStatusJson;
/**
* 创建人;设备心跳没有后台用户上下文时为空。
*/
@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;
}
@@ -183,10 +183,16 @@ public class HardOrderEntity extends Model<HardOrderEntity> {
@Schema(description = "退款状态")
private String refundStatus;
/**
* 订单实际核销完成时间,后续订单更新不得覆盖
*/
@Schema(description = "订单实际核销完成时间")
private LocalDateTime verificationTime;
/**
* 订单取消时间(退款订单)
*/
@Schema(description = "订单取消时间")
@Schema(description = "订单取消时间(退款订单)")
private LocalDateTime applyCancelTime;
/**
@@ -0,0 +1,208 @@
package com.bsd.pjcloud.hard.api.entity.order;
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.TableLogic;
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.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 订单识别证据图片,从属于 hard_order 并长期保存争议处理所需的文件快照和识别元数据。
*/
@Data
@TenantTable
@TableName("hard_order_evidence_image")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "订单识别证据图片")
public class HardOrderEvidenceImageEntity extends Model<HardOrderEvidenceImageEntity> {
/**
* 证据记录自增主键。
*/
@TableId(type = IdType.AUTO)
@Schema(description = "证据记录主键")
private Long id;
/**
* 服务端订单主键,来源于 hard_order.id。
*/
@Schema(description = "订单主键")
private Long orderId;
/**
* 证据订单引用快照;现场订单取终端订单号,预约等服务端订单取订单号或服务端主键内部引用。
*/
@Schema(description = "证据订单引用快照")
private String androidOrderCode;
/**
* 证据业务类型,保存 HardOrderEvidenceTypeEnum.code。
*/
@Schema(description = "证据类型")
private String evidenceType;
/**
* 图片角色,保存 HardOrderEvidenceRoleEnum.code。
*/
@Schema(description = "证据图片角色")
private String evidenceRole;
/**
* 同一订单、类型和角色下的图片序号,从0开始。
*/
@Schema(description = "证据图片序号")
private Integer sequenceNo;
/**
* 识别用户主键快照;无法识别到平台用户时允许为空。
*/
@Schema(description = "识别用户主键")
private Long userId;
/**
* 虹软人脸样本编号快照,来源于 sys_user_face.auto_code。
*/
@Schema(description = "人脸样本编号")
private String faceSampleId;
/**
* 识别时终端命中的后台参考图片地址快照,仅用于与 UPMS 权威记录比对,不直接作为展示地址。
*/
@Schema(description = "参考人脸来源地址快照")
private String referenceImageUrl;
/**
* 终端识别相似度快照,取值0到1。
*/
@Schema(description = "识别相似度")
private BigDecimal similarity;
/**
* 终端识别相似度阈值快照,取值0到1。
*/
@Schema(description = "识别相似度阈值")
private BigDecimal similarityThreshold;
/**
* 产生证据的服务端设备主键,来源于 hard_order.hard_id。
*/
@Schema(description = "设备主键")
private Long hardId;
/**
* 终端实际完成识别的本地时间,用于与订单时间交叉核验。
*/
@Schema(description = "终端采集时间")
private LocalDateTime capturedAt;
/**
* 文件中心 sys_file 主键;状态非 AVAILABLE 时允许为空。
*/
@Schema(description = "文件中心记录主键")
private Long sysFileId;
/**
* 文件中心对象名,仅供服务端读取或下载,不由终端提交。
*/
@Schema(description = "文件中心对象名")
private String storageFileName;
/**
* PC 可使用统一请求层访问的文件相对地址。
*/
@Schema(description = "图片访问地址")
private String imageUrl;
/**
* 实际保存文件的 SHA-256,用于幂等和防止证据被静默替换。
*/
@Schema(description = "图片SHA-256")
private String imageHash;
/**
* 实际保存文件大小,单位字节。
*/
@Schema(description = "图片大小,单位字节")
private Long imageSize;
/**
* 实际解码图片宽度,单位像素。
*/
@Schema(description = "图片宽度,单位像素")
private Integer imageWidth;
/**
* 实际解码图片高度,单位像素。
*/
@Schema(description = "图片高度,单位像素")
private Integer imageHeight;
/**
* 证据状态,保存 HardOrderEvidenceStatusEnum.code。
*/
@Schema(description = "证据状态")
private String status;
/**
* 永久缺失或服务端复制失败的摘要,不保存终端完整文件路径。
*/
@Schema(description = "失败原因")
private String failureReason;
/**
* 产生证据的终端应用版本快照。
*/
@Schema(description = "终端应用版本")
private String appVersion;
/**
* 创建人,由 MyBatis 填充器写入。
*/
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建人")
private String createBy;
/**
* 证据占位记录创建时间,由 MyBatis 填充器写入。
*/
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建时间")
private LocalDateTime createTime;
/**
* 修改人,由 MyBatis 填充器在状态变化时写入。
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
@Schema(description = "修改人")
private String updateBy;
/**
* 最近一次状态、文件或错误信息更新时间。
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
@Schema(description = "修改时间")
private LocalDateTime updateTime;
/**
* 逻辑删除标记;订单退款、取消和删除不会联动改变该值。
*/
@TableLogic
@TableField(fill = FieldFill.INSERT)
@Schema(description = "删除标记")
private String delFlag;
/**
* 租户主键,由当前请求租户上下文写入并由数据层自动隔离。
*/
@Schema(description = "租户主键")
private Long tenantId;
}
@@ -0,0 +1,130 @@
package com.bsd.pjcloud.hard.api.entity.xfj;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.bsd.pjcloud.common.core.util.TenantTable;
import java.time.LocalDateTime;
/**
* 消费机日志
*/
@Data
@TenantTable
@TableName("hard_xfj_log")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "消费机日志")
public class HardXfjLogEntity extends Model<HardXfjLogEntity> {
/**
* 编号
*/
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "编号")
private Long id;
/**
* 设备唯一编号
*/
@Schema(description = "设备唯一编号")
private String uniqueCode;
/**
* 设备类型
*/
@Schema(description = "设备类型")
private String hardType;
/**
* 用户id
*/
@Schema(description = "用户id")
private String title;
/**
* headers
*/
@Schema(description = "headers")
private String headers;
/**
* res
*/
@Schema(description = "res")
private String res;
/**
* req
*/
@Schema(description = "req")
private String req;
/**
* error
*/
@Schema(description = "error")
private String error;
/**
* 开始时间
*/
@Schema(description = "开始时间")
private LocalDateTime startTime;
/**
* 结束时间
*/
@Schema(description = "结束时间")
private LocalDateTime endTime;
/**
* 耗时(毫秒)
*/
@Schema(description = "耗时(毫秒)")
private Long cost;
/**
* 创建人
*/
@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;
/**
* 删除标记
*/
@TableLogic
@TableField(fill = FieldFill.INSERT)
@Schema(description = "删除标记")
private String delFlag;
/**
* 租户ID
*/
@Schema(description = "租户ID")
private Long tenantId;
}
@@ -0,0 +1,565 @@
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 int PRODUCT_CARD_PADDING = 14;
/** 相邻兑换券商品卡片的垂直间距。 */
private static final int PRODUCT_CARD_GAP = 14;
/** 中文优先逻辑字体名称。 */
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, 42);
drawCentered(text, 16);
return this;
}
/**
* 绘制带黑色边框的大号打印次数提示,文字超过可用纸宽时自动换行。
*
* @param text 提示文本,例如“第 1 次打印”
* @return 当前绘制器
*/
public ThermalReceiptImage banner(String text) {
setFont(Font.BOLD, 44);
FontMetrics metrics = graphics.getFontMetrics();
List<String> lines = wrap(text == null ? "" : text, contentWidth() - 32, metrics);
int lineHeight = metrics.getHeight() + 4;
int height = lines.size() * lineHeight + 26;
ensureCapacity(height + 20);
graphics.setStroke(new BasicStroke(3F));
graphics.drawRect(HORIZONTAL_PADDING, cursorY, contentWidth(), height);
for (int index = 0; index < lines.size(); index++) {
String line = lines.get(index);
int x = HORIZONTAL_PADDING + Math.max(0, (contentWidth() - metrics.stringWidth(line)) / 2);
graphics.drawString(line, x, cursorY + 13 + index * lineHeight + metrics.getAscent());
}
cursorY += height + 20;
return this;
}
/**
* 绘制醒目的核销短码。
*
* @param shortCode 核销短码
* @return 当前绘制器
*/
public ThermalReceiptImage verificationCode(String shortCode) {
setFont(Font.BOLD, 64);
drawCentered(shortCode == null || shortCode.isBlank() ? "--" : shortCode, 18);
return this;
}
/**
* 绘制一行左右键值信息,值无法完整同行显示时整段从下一行开始并继续换行。
*
* @param label 左侧字段名
* @param value 右侧字段值
* @return 当前绘制器
*/
public ThermalReceiptImage keyValue(String label, String value) {
setFont(Font.PLAIN, 33);
String safeLabel = label == null ? "" : label;
String safeValue = value == null || value.isBlank() ? "--" : value;
FontMetrics metrics = graphics.getFontMetrics();
int labelWidth = metrics.stringWidth(safeLabel + ":");
int inlineValueWidth = contentWidth() - labelWidth - 12;
boolean stackValue = metrics.stringWidth(safeValue) > inlineValueWidth;
int valueWidth = stackValue ? contentWidth() : Math.max(120, inlineValueWidth);
List<String> lines = wrap(safeValue, valueWidth, metrics);
int valueLineOffset = stackValue ? 1 : 0;
int lineHeight = metrics.getHeight() + 6;
ensureCapacity((lines.size() + valueLineOffset) * lineHeight + 10);
graphics.drawString(safeLabel + ":", HORIZONTAL_PADDING, cursorY + metrics.getAscent());
for (int index = 0; index < lines.size(); index++) {
int x = stackValue ? HORIZONTAL_PADDING : HORIZONTAL_PADDING + labelWidth + 12;
graphics.drawString(lines.get(index), x,
cursorY + (index + valueLineOffset) * lineHeight + metrics.getAscent());
}
cursorY += (lines.size() + valueLineOffset) * lineHeight + 6;
return this;
}
/**
* 绘制商品明细区标题。
*
* @param text 标题文本
* @return 当前绘制器
*/
public ThermalReceiptImage sectionTitle(String text) {
setFont(Font.BOLD, 36);
ensureCapacity(60);
graphics.drawString(text == null ? "" : text, HORIZONTAL_PADDING, cursorY + graphics.getFontMetrics().getAscent());
cursorY += graphics.getFontMetrics().getHeight() + 10;
return this;
}
/**
* 绘制多订单合并兑换券中的紧凑订单摘要。
*
* <p>订单序号使用黑底标签形成扫描锚点,完整订单号及日期餐段均按实际像素宽度换行;相邻订单仅以
* 细线分隔,避免重复商品版式中的大段标题和留白。</p>
*
* @param index 当前订单在合并批次中的一号起始序号
* @param total 合并批次订单总数
* @param orderNo 对外展示的完整订单号
* @param orderDate 预约订餐日期
* @param mealName 餐段名称
* @return 当前绘制器
*/
public ThermalReceiptImage voucherOrderSummary(int index, int total, String orderNo,
String orderDate, String mealName) {
int safeTotal = Math.max(1, total);
int safeIndex = Math.min(Math.max(1, index), safeTotal);
String tag = "订单 " + safeIndex + "/" + safeTotal;
String safeOrderNo = orderNo == null || orderNo.isBlank() ? "--" : orderNo;
String summary = "订餐日期:" + (orderDate == null || orderDate.isBlank() ? "--" : orderDate)
+ " 餐段:" + (mealName == null || mealName.isBlank() ? "--" : mealName);
setFont(Font.BOLD, 25);
FontMetrics tagMetrics = graphics.getFontMetrics();
int tagWidth = tagMetrics.stringWidth(tag) + 20;
int tagHeight = tagMetrics.getHeight() + 6;
setFont(Font.BOLD, 31);
FontMetrics orderMetrics = graphics.getFontMetrics();
int orderX = HORIZONTAL_PADDING + tagWidth + 14;
int orderWidth = PAPER_WIDTH_PIXELS - HORIZONTAL_PADDING - orderX;
List<String> orderLines = wrap(safeOrderNo, Math.max(120, orderWidth), orderMetrics);
int orderLineHeight = orderMetrics.getHeight() + 4;
int headerHeight = Math.max(tagHeight, orderLines.size() * orderLineHeight);
setFont(Font.PLAIN, 29);
FontMetrics summaryMetrics = graphics.getFontMetrics();
List<String> summaryLines = wrap(summary, contentWidth(), summaryMetrics);
int summaryLineHeight = summaryMetrics.getHeight() + 4;
int separatorHeight = safeIndex < safeTotal ? 18 : 8;
int blockHeight = headerHeight + 10 + summaryLines.size() * summaryLineHeight + separatorHeight;
ensureCapacity(blockHeight);
int blockTop = cursorY;
graphics.setColor(Color.BLACK);
setFont(Font.BOLD, 25);
graphics.fillRect(HORIZONTAL_PADDING, blockTop, tagWidth, tagHeight);
graphics.setColor(Color.WHITE);
graphics.drawString(tag, HORIZONTAL_PADDING + 10, blockTop + 3 + tagMetrics.getAscent());
graphics.setColor(Color.BLACK);
setFont(Font.BOLD, 31);
for (int lineIndex = 0; lineIndex < orderLines.size(); lineIndex++) {
graphics.drawString(orderLines.get(lineIndex), orderX,
blockTop + lineIndex * orderLineHeight + orderMetrics.getAscent());
}
int summaryTop = blockTop + headerHeight + 10;
setFont(Font.PLAIN, 29);
for (int lineIndex = 0; lineIndex < summaryLines.size(); lineIndex++) {
graphics.drawString(summaryLines.get(lineIndex), HORIZONTAL_PADDING,
summaryTop + lineIndex * summaryLineHeight + summaryMetrics.getAscent());
}
if (safeIndex < safeTotal) {
int separatorY = summaryTop + summaryLines.size() * summaryLineHeight + 7;
graphics.setStroke(new BasicStroke(1F));
graphics.drawLine(HORIZONTAL_PADDING, separatorY,
PAPER_WIDTH_PIXELS - HORIZONTAL_PADDING, separatorY);
}
cursorY += blockHeight;
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);
}
/**
* 绘制兑换券商品卡片,使用文字标签和边框粗细同时区分组合商品与单品。
*
* <p>商品名称和组合内容均按真实像素宽度完整换行;价格区固定展示单价、数量和小计。
* 组合内容只解释套餐构成,不参与当前卡片金额计算。</p>
*
* @param name 商品快照名称;空值显示“未命名商品”
* @param quantity 部分退款后的剩余可履约数量
* @param unitPrice 商品快照单价,单位元且不含货币符号
* @param subtotal 剩余可履约数量对应的小计,单位元且不含货币符号
* @param combo 是否为组合商品
* @param componentItems 组合商品组成说明,每项应已包含对应数量;单品或无组成时可为空
* @return 当前绘制器
*/
public ThermalReceiptImage voucherProduct(String name, int quantity, String unitPrice, String subtotal,
boolean combo, List<String> componentItems) {
String typeLabel = combo ? "组合商品" : "单品";
String safeName = name == null || name.isBlank() ? "未命名商品" : name;
String safeUnitPrice = unitPrice == null || unitPrice.isBlank() ? "--" : unitPrice;
String safeSubtotal = subtotal == null || subtotal.isBlank() ? "--" : subtotal;
List<String> safeComponents = componentItems == null ? List.of() : componentItems.stream()
.filter(item -> item != null && !item.isBlank()).toList();
int innerWidth = contentWidth() - PRODUCT_CARD_PADDING * 2;
setFont(Font.BOLD, 28);
FontMetrics tagMetrics = graphics.getFontMetrics();
int tagHeight = tagMetrics.getHeight() + 6;
int tagWidth = tagMetrics.stringWidth(typeLabel) + 20;
setFont(Font.BOLD, 40);
FontMetrics nameMetrics = graphics.getFontMetrics();
List<String> nameLines = wrap(safeName, innerWidth, nameMetrics);
int nameLineHeight = nameMetrics.getHeight() + 4;
setFont(Font.PLAIN, 29);
FontMetrics priceMetrics = graphics.getFontMetrics();
String priceAndQuantity = "单价 ¥" + safeUnitPrice + " x" + Math.max(0, quantity);
String subtotalText = "小计 ¥" + safeSubtotal;
boolean splitPriceLine = priceMetrics.stringWidth(priceAndQuantity)
+ priceMetrics.stringWidth(subtotalText) + 24 > innerWidth;
int priceLineHeight = priceMetrics.getHeight() + 4;
setFont(Font.PLAIN, 29);
FontMetrics componentMetrics = graphics.getFontMetrics();
int componentLineHeight = componentMetrics.getHeight() + 3;
List<List<String>> componentLines = new ArrayList<>();
for (String component : safeComponents) {
componentLines.add(wrap("- " + component, innerWidth - CHILD_INDENT, componentMetrics));
}
int componentHeight = componentLines.stream().mapToInt(List::size).sum() * componentLineHeight;
int comboSectionHeight = combo && !componentLines.isEmpty()
? 18 + componentLineHeight + componentHeight : 0;
int cardHeight = PRODUCT_CARD_PADDING + tagHeight + 10 + nameLines.size() * nameLineHeight + 8
+ (splitPriceLine ? priceLineHeight * 2 : priceLineHeight) + comboSectionHeight
+ PRODUCT_CARD_PADDING;
ensureCapacity(cardHeight + PRODUCT_CARD_GAP);
int cardTop = cursorY;
int contentX = HORIZONTAL_PADDING + PRODUCT_CARD_PADDING;
int contentY = cardTop + PRODUCT_CARD_PADDING;
graphics.setColor(Color.BLACK);
graphics.setStroke(new BasicStroke(combo ? 4F : 2F));
graphics.drawRect(HORIZONTAL_PADDING, cardTop, contentWidth(), cardHeight);
setFont(Font.BOLD, 28);
if (combo) {
graphics.fillRect(contentX, contentY, tagWidth, tagHeight);
graphics.setColor(Color.WHITE);
graphics.drawString(typeLabel, contentX + 10, contentY + 3 + tagMetrics.getAscent());
graphics.setColor(Color.BLACK);
} else {
graphics.setStroke(new BasicStroke(2F));
graphics.drawRect(contentX, contentY, tagWidth, tagHeight);
graphics.drawString(typeLabel, contentX + 10, contentY + 3 + tagMetrics.getAscent());
}
contentY += tagHeight + 10;
setFont(Font.BOLD, 40);
for (String line : nameLines) {
graphics.drawString(line, contentX, contentY + nameMetrics.getAscent());
contentY += nameLineHeight;
}
contentY += 8;
setFont(Font.PLAIN, 29);
graphics.drawString(priceAndQuantity, contentX, contentY + priceMetrics.getAscent());
if (splitPriceLine) {
contentY += priceLineHeight;
}
graphics.drawString(subtotalText,
PAPER_WIDTH_PIXELS - HORIZONTAL_PADDING - PRODUCT_CARD_PADDING
- priceMetrics.stringWidth(subtotalText),
contentY + priceMetrics.getAscent());
contentY += priceLineHeight;
if (combo && !componentLines.isEmpty()) {
contentY += 8;
graphics.setStroke(new BasicStroke(1F));
graphics.drawLine(contentX, contentY,
PAPER_WIDTH_PIXELS - HORIZONTAL_PADDING - PRODUCT_CARD_PADDING, contentY);
contentY += 10;
setFont(Font.BOLD, 29);
graphics.drawString("组合内容", contentX, contentY + componentMetrics.getAscent());
contentY += componentLineHeight;
setFont(Font.PLAIN, 29);
for (List<String> lines : componentLines) {
for (String line : lines) {
graphics.drawString(line, contentX + CHILD_INDENT,
contentY + componentMetrics.getAscent());
contentY += componentLineHeight;
}
}
}
cursorY += cardHeight + PRODUCT_CARD_GAP;
return this;
}
/**
* 绘制兑换券商品合计,使用粗分隔线和大号金额形成票据金额层级。
*
* @param totalAmount 剩余可履约商品合计,单位元且不含货币符号
* @return 当前绘制器
*/
public ThermalReceiptImage voucherTotal(String totalAmount) {
String safeAmount = totalAmount == null || totalAmount.isBlank() ? "--" : totalAmount;
String label = "商品合计";
String amount = "¥" + safeAmount;
setFont(Font.BOLD, 42);
FontMetrics metrics = graphics.getFontMetrics();
int blockHeight = metrics.getHeight() + 30;
ensureCapacity(blockHeight + 14);
graphics.setColor(Color.BLACK);
graphics.setStroke(new BasicStroke(3F));
graphics.drawLine(HORIZONTAL_PADDING, cursorY,
PAPER_WIDTH_PIXELS - HORIZONTAL_PADDING, cursorY);
int baseline = cursorY + 15 + metrics.getAscent();
graphics.drawString(label, HORIZONTAL_PADDING, baseline);
graphics.drawString(amount, PAPER_WIDTH_PIXELS - HORIZONTAL_PADDING - metrics.stringWidth(amount), baseline);
graphics.drawLine(HORIZONTAL_PADDING, cursorY + blockHeight,
PAPER_WIDTH_PIXELS - HORIZONTAL_PADDING, cursorY + blockHeight);
cursorY += blockHeight + 14;
return this;
}
/**
* 使用指定尾列宽度绘制一条可换行明细。
*
* @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 ? 30 : 34);
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;
}
}
@@ -63,6 +63,11 @@ public class HardInvoiceVo extends HardInvoiceEntity implements BaseDeptInterfac
private String hardType;
/**
* 任务绑定的尔印 USB 打印机资产名称。
*/
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;
/**
* 业务终端与无屏 USB 打印机绑定关系及展示信息。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "USB 打印机绑定详情")
public class HardPrinterBindingVo extends HardPrinterBindingEntity {
/** 来源业务终端名称。 */
@Schema(description = "来源业务终端名称")
private String sourceHardName;
/** 来源业务终端类型。 */
@Schema(description = "来源业务终端类型")
private String sourceHardType;
/** 逻辑打印机资产名称。 */
@Schema(description = "逻辑打印机资产名称")
private String printerHardName;
/** 目标 USB 打印机当前配置;尚未保存时返回默认配置。 */
@Schema(description = "USB 打印机配置")
private HardPrinterConfigEntity printerConfig;
}
@@ -6,6 +6,11 @@ import com.bsd.pjcloud.hard.api.vo.base.BaseHardGroupInterface;
import com.bsd.pjcloud.wallet.api.vo.base.BaseMchInterface;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 设备管理视图,包含设备基础资料及仅用于接口返回的关联、运行状态信息。
*/
@Data
public class HardManagerVo extends HardManagerEntity implements BaseDeptInterface, BaseMchInterface, BaseHardGroupInterface {
@@ -29,4 +34,59 @@ public class HardManagerVo extends HardManagerEntity implements BaseDeptInterfac
private String hardGroupName;
/**
* 终端是否在线;超市、食堂设备返回布尔值,其他设备类型返回 {@code null} 表示暂不支持。
*/
private Boolean online;
/**
* 服务端最近收到终端心跳的时间;尚未上报或设备类型不支持时为空。
*/
private LocalDateTime lastOnlineTime;
/**
* 当前 APK 展示版本号,来源于终端最近一次心跳;未上报时为空。
*/
private String apkVersionName;
/**
* 当前 APK 内部版本码,来源于终端最近一次心跳;未上报时为空。
*/
private Long apkVersionCode;
/**
* 断开连接的可解释原因;除主动自动更新外均为基于心跳超时的推断结果。
*/
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;
}
@@ -0,0 +1,23 @@
package com.bsd.pjcloud.hard.api.vo.manager;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 终端日志设备聚合摘要。
*/
@Data
@Schema(description = "终端日志设备摘要")
public class HardTerminalLogDeviceVo {
private Long hardId;
private String hardName;
private String uniqueCode;
private String hardType;
private Long merchantId;
private Long deptId;
private Long fileCount;
private LocalDateTime latestUploadTime;
}
@@ -0,0 +1,27 @@
package com.bsd.pjcloud.hard.api.vo.manager;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* 终端日志文件树节点。
*/
@Data
@Schema(description = "终端日志文件树节点")
public class HardTerminalLogFileTreeVo {
private String key;
private String label;
private String relativePath;
private Boolean directory;
private Long fileId;
private Long fileSize;
private LocalDate logDate;
private LocalDateTime uploadTime;
private List<HardTerminalLogFileTreeVo> children = new ArrayList<>();
}
@@ -0,0 +1,19 @@
package com.bsd.pjcloud.hard.api.vo.manager;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
/**
* 终端日志上传预检结果。
*/
@Data
@Builder
@Schema(description = "终端日志上传预检结果")
public class HardTerminalLogPreflightVo {
private Long id;
private Boolean needUpload;
private String normalizedPath;
private String message;
}
@@ -0,0 +1,22 @@
package com.bsd.pjcloud.hard.api.vo.manager;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
/**
* 终端日志尾部预览。
*/
@Data
@Builder
@Schema(description = "终端日志预览")
public class HardTerminalLogPreviewVo {
private Long id;
private String originalName;
private String relativePath;
private String content;
private Integer lineCount;
private Long fileSize;
private Boolean truncated;
}
@@ -0,0 +1,21 @@
package com.bsd.pjcloud.hard.api.vo.manager;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
/**
* 终端日志上传结果。
*/
@Data
@Builder
@Schema(description = "终端日志上传结果")
public class HardTerminalLogUploadResultVo {
private Long id;
private Boolean uploaded;
private Boolean changed;
private String relativePath;
private Integer uploadCount;
private String message;
}
@@ -21,4 +21,14 @@ public class HardCarVo extends HardCarEntity {
// 购买次数【统计使用】
private Long payCount;
/**
* 已完成退款份数,只统计售后状态为完成的商品明细;空响应按零处理。
*/
private Integer refundedProductCount;
/**
* 当前剩余可履约份数,计算规则为原购份数减已完成退款份数,最小为零。
*/
private Integer effectiveProductCount;
}
@@ -0,0 +1,33 @@
package com.bsd.pjcloud.hard.api.vo.order;
import com.bsd.pjcloud.hard.api.entity.order.HardOrderEvidenceImageEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* PC 订单详情使用的订单证据图片,附带稳定编码对应的中文名称。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "订单识别证据图片详情")
public class HardOrderEvidenceImageVo extends HardOrderEvidenceImageEntity {
/**
* 证据类型中文名称,未知历史编码时回退原编码。
*/
@Schema(description = "证据类型名称")
private String evidenceTypeName;
/**
* 图片角色中文名称,未知历史编码时回退原编码。
*/
@Schema(description = "证据图片角色名称")
private String evidenceRoleName;
/**
* 证据状态中文名称,未知历史编码时回退原编码。
*/
@Schema(description = "证据状态名称")
private String statusName;
}
@@ -0,0 +1,34 @@
package com.bsd.pjcloud.hard.api.vo.order;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.util.List;
/**
* 终端按本地订单号对账时返回的服务端订单与证据快照。
*/
@Data
@Builder
@Schema(description = "订单证据对账结果")
public class HardOrderEvidenceResolveVo {
/**
* 服务端订单主键。
*/
@Schema(description = "服务端订单主键")
private Long orderId;
/**
* 服务端订单状态,终端仅在现有支付流程确认成功后上传证据。
*/
@Schema(description = "订单状态")
private String orderStatus;
/**
* 当前服务端已登记的证据位置和状态,用于终端幂等跳过成功项。
*/
@Schema(description = "证据列表")
private List<HardOrderEvidenceImageVo> evidenceImages;
}
@@ -0,0 +1,50 @@
package com.bsd.pjcloud.hard.api.vo.order;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
/**
* 终端提交元数据、复制参考图或上传现场图后的幂等处理结果。
*/
@Data
@Builder
@Schema(description = "订单证据处理结果")
public class HardOrderEvidenceUploadResultVo {
/**
* 服务端证据记录主键。
*/
@Schema(description = "证据记录主键")
private Long evidenceId;
/**
* 服务端订单主键。
*/
@Schema(description = "订单主键")
private Long orderId;
/**
* 处理后的证据状态。
*/
@Schema(description = "证据状态")
private String status;
/**
* AVAILABLE 状态下的图片访问地址,其他状态允许为空。
*/
@Schema(description = "图片访问地址")
private String imageUrl;
/**
* 是否实际上传或复制了新文件;幂等命中时为 false。
*/
@Schema(description = "是否新写入文件")
private Boolean uploaded;
/**
* 面向终端同步日志的简短处理结果。
*/
@Schema(description = "处理结果说明")
private String message;
}
@@ -74,4 +74,9 @@ public class HardOrderVo extends HardOrderEntity implements BaseUserDeptInterfac
*/
private Long walletTypeId;
/**
* 订单详情中的长期识别证据列表,按证据类型、角色和序号排序。
*/
private List<HardOrderEvidenceImageVo> evidenceImages;
}
@@ -0,0 +1,14 @@
package com.bsd.pjcloud.hard.api.vo.xfj;
import com.bsd.pjcloud.hard.api.entity.xfj.HardXfjLogEntity;
import lombok.Data;
@Data
public class HardXfjLogVo extends HardXfjLogEntity {
/**
* 设备名称
*/
private String hardName;
}
@@ -0,0 +1,25 @@
package com.bsd.pjcloud.hard.api.vo.xfj;
import lombok.Data;
@Data
public class XfjBaseVo {
public static final Integer STATUS_SUCCESS = 1;
public static final Integer STATUS_FAIL = 0;
public static final String DEFAULT_SUCCESS_MSG = "成功";
public static final String DEFAULT_FAIL_MSG = "服务端错误";
/**
* 1:成功
* 0:失败
*/
private Integer Status = STATUS_SUCCESS;
/**
* 错误消息(Status为0时屏幕显示此内容,最多不超过8个汉字)
*/
private String Msg = DEFAULT_SUCCESS_MSG;
}
@@ -0,0 +1,68 @@
package com.bsd.pjcloud.hard.api.vo.xfj;
import lombok.Data;
@Data
public class XfjConsumTransactionsVo extends XfjBaseVo {
/**
* 人员姓名
*/
private String Name;
/**
* 10进制卡序列号(实体卡号或虚拟卡号)
*/
private String CardNo;
/**
* 现金余额(允许两位小数)
*/
private String Money;
/**
* 补贴余额(允许两位小数)
*/
private String Subsidy;
/**
* 剩余次数
*/
private String Times;
/**
* 赠送余额(允许两位小数)
*/
private String Integral;
/**
* 入场时间(固定值:"")
*/
private String InTime;
/**
* 出场时间(固定值:"")
*/
private String OutTime;
/**
* 累计使用时间:天时分秒
*/
private String CumulativeTime;
/**
* 实际扣费金额(信息查询模式下返回"0")
*/
private String Amount;
/**
* 语音段(用于安卓系统设备)
*/
private String VoiceID;
/**
* 自定义显示文本(Status为1时传入,屏幕显示此值,内容内使用\r\n换行,最多支持4行,每行不超过8个汉字)
*/
private String Text;
}
@@ -0,0 +1,13 @@
package com.bsd.pjcloud.hard.api.vo.xfj;
import lombok.Data;
@Data
public class XfjCountsVo extends XfjBaseVo {
/**
* 自定义显示文本(Status为1时传入,屏幕显示此值,内容内使用\n换行,最多支持4行,每行不超过8个汉字)
*/
private String Text;
}
@@ -0,0 +1,13 @@
package com.bsd.pjcloud.hard.api.vo.xfj;
import lombok.Data;
@Data
public class XfjKeepAliveVo extends XfjBaseVo {
/**
* 二维码交易订单号,没定单号时为空
*/
private String Qrorder;
}
@@ -0,0 +1,18 @@
package com.bsd.pjcloud.hard.api.vo.xfj;
import lombok.Data;
@Data
public class XfjMenusVo extends XfjBaseVo {
/**
* 菜品名称
*/
private String Name;
/**
* 菜品单价
*/
private String Price;
}
@@ -0,0 +1,13 @@
package com.bsd.pjcloud.hard.api.vo.xfj;
import lombok.Data;
@Data
public class XfjOffLinesVo extends XfjBaseVo {
/**
* Status为1时返回此内容(返回是请求示例中的order信息)
*/
private String Order;
}
@@ -0,0 +1,66 @@
package com.bsd.pjcloud.hard.api.vo.xfj;
import lombok.Data;
@Data
public class XfjOrderQueryVo extends XfjBaseVo {
/**
* 人员姓名
*/
private String Name;
/**
* 10进制卡序列号(实体卡号或虚拟卡号)
*/
private String CardNo;
/**
* 现金余额(允许两位小数)
*/
private String Money;
/**
* 补贴余额(允许两位小数)
*/
private String Subsidy;
/**
* 剩余次数
*/
private String Times;
/**
* 赠送余额(允许两位小数)
*/
private String Integral;
/**
* 入场时间(固定值:"")
*/
private String InTime;
/**
* 出场时间(固定值:"")
*/
private String OutTime;
/**
* 累计使用时间:天时分秒
*/
private String CumulativeTime;
/**
* 实际扣费金额(信息查询模式下返回"0")
*/
private String Amount;
/**
* 语音段(用于安卓系统设备)
*/
private String VoiceID;
/**
* 自定义显示文本(Status为1时传入,屏幕显示此值,内容内使用\r\n换行,最多支持4行,每行不超过8个汉字)
*/
private String Text;
}
@@ -0,0 +1,47 @@
package com.bsd.pjcloud.hard.api.vo.xfj;
import lombok.Data;
@Data
public class XfjOrderRefundVo extends XfjBaseVo {
/**
* 人员姓名
*/
private String Name;
/**
* 10进制卡序列号(实体卡号或虚拟卡号)
*/
private String CardNo;
/**
* 现金余额(允许两位小数)
*/
private String Money;
/**
* 补贴余额(允许两位小数)
*/
private String Subsidy;
/**
* 剩余次数
*/
private String Times;
/**
* 赠送余额(允许两位小数)
*/
private String Integral;
/**
* 退款金额
*/
private String Amount;
/**
* 11 (写死 11)用于区分接口
*/
private Integer CmdType;
}
@@ -0,0 +1,13 @@
package com.bsd.pjcloud.hard.api.vo.xfj;
import lombok.Data;
@Data
public class XfjQRCodeTransactionVo extends XfjBaseVo {
/**
* 订单号
*/
private String Qrorder;
}
@@ -0,0 +1,24 @@
package com.bsd.pjcloud.hard.api.vo.xfj;
import lombok.Data;
@Data
public class XfjServerTimeVo extends XfjBaseVo {
/**
* 服务器时间,固定格式为yyyyMMddHHmmssd最后一位d代表星期,星期一为1,星期六为6,星期天为0
*/
private String Time;
/**
* 1:有白名单更新
* 0:无
*/
private Integer WLUptate;
/**
* 起始页数(当WLUptate为1却WLPage为0时,清除所有白名单)
*/
private Integer WLPage;
}
@@ -0,0 +1,66 @@
package com.bsd.pjcloud.hard.api.vo.xfj;
import lombok.Data;
@Data
public class XfjTransactionInquiryVo extends XfjBaseVo {
/**
* 人员姓名
*/
private String Name;
/**
* 10进制卡序列号(实体卡号或虚拟卡号)
*/
private String CardNo;
/**
* 现金余额(允许两位小数)
*/
private String Money;
/**
* 补贴余额(允许两位小数)
*/
private String Subsidy;
/**
* 剩余次数
*/
private String Times;
/**
* 赠送余额(允许两位小数)
*/
private String Integral;
/**
* 入场时间(固定值:"")
*/
private String InTime;
/**
* 出场时间(固定值:"")
*/
private String OutTime;
/**
* 累计使用时间:天时分秒
*/
private String CumulativeTime;
/**
* 实际扣费金额(信息查询模式下返回"0")
*/
private String Amount;
/**
* 语音段(用于安卓系统设备)
*/
private String VoiceID;
/**
* 自定义显示文本(Status为1时传入,屏幕显示此值,内容内使用\r\n换行,最多支持4行,每行不超过8个汉字)
*/
private String Text;
}
@@ -0,0 +1,49 @@
package com.bsd.pjcloud.hard.api.vo.xfj;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class XfjTransactionVo extends XfjBaseVo {
/**
* 编号 从最后一笔消费往前算的位置
* 1:就是最后一笔
* 2:就是倒数第二笔
*/
private String Number;
/**
* 消费时间(格式:yyyyMMddHHmmss)
*/
private String Time;
/**
* 卡号10进制(实体卡号或虚拟卡号)
*/
private String CardNo;
/**
* 姓名最大5个汉字
*/
private String Name;
/**
* 消费额
* - 10.00元
*/
private String Consume;
/**
* 余额
* - 10.00元
*/
private String Balance;
/**
* 消费订单号
*/
private String OrderSeralid;
}
@@ -0,0 +1,36 @@
package com.bsd.pjcloud.hard.api.vo.xfj;
import lombok.Data;
@Data
public class XfjWhiteListVo extends XfjBaseVo {
/**
* 返回设备提交的序号
*/
private Integer CommID;
/**
* 页数(获取那一页的数据)
*/
private Integer Page;
/**
* 名单个数
*/
private Integer PageLen;
/**
* 白名单数据(1|0000000001|1,序号|卡号|操作)
* 1:添加
* 0:删除
*/
private String WLData;
/**
* 1:继续获取
* 0:已经更新完成
*/
private Integer Uptate;
}