feat:打印机器修改

This commit is contained in:
dzc committed 2026-09-04 16:23:49 +08:00
1 parent 9147aafec8
commit 991685ca48
28 files changed
+333 -193

No files matched your search

@@ -7,7 +7,7 @@ import lombok.Getter;
import java.util.Arrays;
/**
* 打印任务失败阶段,帮助管理端区分配置问题、服务端故障和终端故障。
* 打印任务失败阶段,帮助管理端区分配置问题、服务端故障和业务终端代理打印故障。
*/
@Getter
@AllArgsConstructor
@@ -19,10 +19,10 @@ public enum HardPrintFailureStageEnum {
/** 服务端读取业务数据、绘制 PNG 或上传文件失败。 */
IMAGE_GENERATION("IMAGE_GENERATION", "图片生成"),
/** 服务端向目标打印终端发布 MQTT 消息失败。 */
/** 服务端向绑定的来源业务终端发布 MQTT 消息失败。 */
MQTT_DISPATCH("MQTT_DISPATCH", "MQTT下发"),
/** 打印终端下载图片、连接 USB 或写入 ESC/POS 数据失败。 */
/** 来源业务终端下载图片、连接 USB 或写入 ESC/POS 数据失败。 */
TERMINAL_PRINT("TERMINAL_PRINT", "终端打印");
/** 持久化阶段编码。 */
@@ -20,10 +20,10 @@ public enum HardPrintStatusEnum {
/** 图片已生成并允许下发,历史枚举名保留为 GENERATE 以保持二进制兼容。 */
GENERATE("1", "待打印"),
/** 打印终端已确认完整 USB 数据和切纸命令写入成功。 */
/** 来源业务终端已确认完整 USB 数据和切纸命令写入成功。 */
PRINT("2", "已打印"),
/** 绑定、图片生成、MQTT 下发或终端打印阶段失败,可重试原任务。 */
/** 绑定、图片生成、MQTT 下发或业务终端代理打印阶段失败,可重试原任务。 */
FAILED("3", "打印失败"),
/** 核销回退等业务动作取消了尚未打印的任务。 */
@@ -26,7 +26,7 @@ public enum HardTypeEnum {
YUN_XFJ("9", "云版消费机"),
YYX_4G("10", "4G-云音响"),
YYX_WIFI("11", "wifi-云音响"),
/** 运行打印专用终端模式、通过 USB 驱动尔印 ESC/POS 打印机的设备。 */
/** 由绑定的超市或食堂 Android 终端代理驱动的无屏尔印 USB 打印机。 */
EY_PRINT("12", "尔印打印机器"),
;
private final String type;
@@ -48,7 +48,7 @@ public enum HardTypeEnum {
}
/**
* 判断指定设备类型是否为独立打印终端。
* 判断指定设备类型是否为尔印逻辑打印机资产。
*
* @param hardTypeEnum 待判断的设备类型
* @return 类型为 {@link #EY_PRINT} 时返回 {@code true}
@@ -6,10 +6,10 @@ import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* EY_PRINT 终端提交的打印结果回执。
* 食堂或超市终端代理无屏 EY_PRINT 提交的打印结果回执。
*/
@Data
@Schema(description = "打印终端结果回执")
@Schema(description = "业务终端代理打印结果回执")
public class HardPrintResultDto {
/** hard_invoice 打印任务主键。 */
@@ -22,7 +22,7 @@ public class HardPrintResultDto {
@Schema(description = "订单ID")
private Long orderId;
/** 终端实际处理的重试版本。 */
/** 来源业务终端实际处理的重试版本。 */
@NotNull(message = "重试版本不能为空")
@Min(value = 0, message = "重试版本不能小于0")
@Schema(description = "重试版本")
@@ -33,7 +33,7 @@ public class HardPrintResultDto {
@Schema(description = "是否成功")
private Boolean success;
/** 本版本在终端执行的 USB 尝试次数。 */
/** 本版本在来源业务终端执行的 USB 尝试次数。 */
@NotNull(message = "尝试次数不能为空")
@Min(value = 1, message = "尝试次数不能小于1")
@Schema(description = "终端尝试次数")
@@ -5,10 +5,10 @@ import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 创建业务终端与尔印打印终端绑定关系的请求。
* 创建业务终端与无屏尔印 USB 打印机绑定关系的请求。
*/
@Data
@Schema(description = "打印终端绑定请求")
@Schema(description = "USB 打印机绑定请求")
public class HardPrinterBindingDto {
/** SUPERMARKET 或 CANTEEN 来源终端 ID。 */
@@ -16,8 +16,8 @@ public class HardPrinterBindingDto {
@Schema(description = "来源业务终端ID")
private Long sourceHardId;
/** EY_PRINT 目标终端 ID。 */
@NotNull(message = "打印终端不能为空")
@Schema(description = "目标打印终端ID")
/** EY_PRINT 逻辑打印机资产 ID。 */
@NotNull(message = "打印机不能为空")
@Schema(description = "逻辑打印机资产ID")
private Long printerHardId;
}
@@ -6,15 +6,15 @@ import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 保存尔印打印终端 USB 与 ESC/POS 参数的请求。
* 保存无屏尔印 USB 打印机参数的请求。
*/
@Data
@Schema(description = "打印终端配置请求")
@Schema(description = "USB 打印机配置请求")
public class HardPrinterConfigDto {
/** EY_PRINT 设备 ID。 */
@NotNull(message = "打印终端不能为空")
@Schema(description = "打印终端ID")
/** EY_PRINT 逻辑打印机资产 ID。 */
@NotNull(message = "打印机不能为空")
@Schema(description = "逻辑打印机资产ID")
private Long printerHardId;
/** 真机型号;未联调前允许为空。 */
@@ -9,10 +9,10 @@ import jakarta.validation.constraints.Size;
import lombok.Data;
/**
* 尔印打印终端上报实际 USB 打印机信息的请求。
* 食堂或超市终端代理上报实际 USB 打印机信息的请求。
*/
@Data
@Schema(description = "打印终端实际 USB 打印机信息")
@Schema(description = "业务终端代理上报的 USB 打印机信息")
public class HardPrinterInfoReportDto {
/** USB 描述符中的厂商和产品名称组合,最长保存 128 个字符。 */
@@ -44,9 +44,9 @@ public class HardInvoiceEntity extends Model<HardInvoiceEntity> {
private Long hardId;
/**
* 实际接收任务的尔印打印终端 ID;历史记录和绑定失败任务允许为空。
* 任务路由关联的尔印逻辑打印机资产 ID;历史记录和绑定失败任务允许为空。
*/
@Schema(description="目标打印终端ID")
@Schema(description="逻辑打印机资产ID")
private Long printerHardId;
/**
@@ -74,13 +74,13 @@ public class HardInvoiceEntity extends Model<HardInvoiceEntity> {
private Integer printNo;
/**
* 人工重试版本;终端用任务 ID 和该版本共同判断是否允许再次打印。
* 人工重试版本;来源业务终端用任务 ID 和该版本共同判断是否允许再次打印。
*/
@Schema(description="重试版本")
private Integer retryVersion;
/**
* 打印终端上报的 USB 累计尝试次数。
* 来源业务终端代理上报的 USB 累计尝试次数。
*/
@Schema(description="终端尝试次数")
private Integer attemptCount;
@@ -98,7 +98,7 @@ public class HardInvoiceEntity extends Model<HardInvoiceEntity> {
private String lastError;
/**
* 打印终端确认完整 USB 数据和切纸命令写入成功的时间。
* 来源业务终端确认完整 USB 数据和切纸命令写入成功的时间。
*/
@Schema(description="打印成功时间")
private LocalDateTime printedAt;
@@ -17,7 +17,7 @@ import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 业务终端与尔印打印终端的一对一绑定关系。
* 业务 Android 终端与无屏尔印 USB 打印机资产的一对一绑定关系。
*
* <p>记录使用物理删除,数据库分别对来源和目标设备设置唯一键,确保任何时刻都不能出现多重绑定。</p>
*/
@@ -28,7 +28,7 @@ import java.time.LocalDateTime;
@TenantTable
@TableName("hard_printer_binding")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "业务终端与打印终端绑定关系")
@Schema(description = "业务终端与 USB 打印机绑定关系")
public class HardPrinterBindingEntity extends Model<HardPrinterBindingEntity> {
/** 绑定关系主键。 */
@@ -40,8 +40,8 @@ public class HardPrinterBindingEntity extends Model<HardPrinterBindingEntity> {
@Schema(description = "来源业务终端ID")
private Long sourceHardId;
/** 目标打印终端 ID,只允许 EY_PRINT。 */
@Schema(description = "目标打印终端ID")
/** 逻辑打印机资产 ID,只允许 EY_PRINT;设备本身不登录、不上报心跳。 */
@Schema(description = "逻辑打印机资产ID")
private Long printerHardId;
/** 双方共同所属商户 ID 快照。 */
@@ -17,7 +17,7 @@ import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 尔印打印终端的 USB 和 ESC/POS 参数配置。
* 无屏尔印 USB 打印机资产的 USB 和 ESC/POS 参数配置。
*/
@Data
@Builder
@@ -26,7 +26,7 @@ import java.time.LocalDateTime;
@TenantTable
@TableName("hard_printer_config")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "尔印打印终端配置")
@Schema(description = "尔印 USB 打印机配置")
public class HardPrinterConfigEntity extends Model<HardPrinterConfigEntity> {
/** 配置主键。 */
@@ -34,8 +34,8 @@ public class HardPrinterConfigEntity extends Model<HardPrinterConfigEntity> {
@Schema(description = "配置主键")
private Long id;
/** 配置所属 EY_PRINT 设备 ID。 */
@Schema(description = "打印终端ID")
/** 配置所属 EY_PRINT 逻辑打印机资产 ID。 */
@Schema(description = "逻辑打印机资产ID")
private Long printerHardId;
/** 真机联调后填写的打印机型号。 */
@@ -67,7 +67,7 @@ public class HardInvoiceVo extends HardInvoiceEntity implements BaseUserDeptInte
private String hardType;
/** 目标尔印打印终端名称。 */
/** 任务绑定的尔印 USB 打印机资产名称。 */
private String printerHardName;
/**
@@ -7,11 +7,11 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 打印终端绑定关系及两端设备展示信息。
* 业务终端与无屏 USB 打印机绑定关系及展示信息。
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "打印终端绑定详情")
@Schema(description = "USB 打印机绑定详情")
public class HardPrinterBindingVo extends HardPrinterBindingEntity {
/** 来源业务终端名称。 */
@@ -22,11 +22,11 @@ public class HardPrinterBindingVo extends HardPrinterBindingEntity {
@Schema(description = "来源业务终端类型")
private String sourceHardType;
/** 目标打印终端名称。 */
@Schema(description = "目标打印终端名称")
/** 逻辑打印机资产名称。 */
@Schema(description = "逻辑打印机资产名称")
private String printerHardName;
/** 目标打印终端当前配置;尚未保存时返回默认配置。 */
@Schema(description = "打印终端配置")
/** 目标 USB 打印机当前配置;尚未保存时返回默认配置。 */
@Schema(description = "USB 打印机配置")
private HardPrinterConfigEntity printerConfig;
}
@@ -59,16 +59,16 @@ public class HardManagerVo extends HardManagerEntity implements BaseDeptInterfac
*/
private String disconnectReason;
/** 当前来源业务终端所绑定的 EY_PRINT 设备 ID;未绑定或当前行为打印机时为空。 */
/** 当前来源业务终端所绑定的 EY_PRINT 打印机资产 ID;未绑定或当前行为打印机时为空。 */
private Long boundPrinterHardId;
/** 当前来源业务终端所绑定的 EY_PRINT 设备名称。 */
private String boundPrinterHardName;
/** 当前 EY_PRINT 反向绑定的来源业务终端 ID;未绑定或当前行为来源设备时为空。 */
/** 当前 EY_PRINT 打印机资产反向绑定的来源业务终端 ID;未绑定或当前行为来源设备时为空。 */
private Long boundSourceHardId;
/** 当前 EY_PRINT 反向绑定的来源业务终端名称。 */
/** 当前 EY_PRINT 打印机资产反向绑定的来源业务终端名称。 */
private String boundSourceHardName;
/** EY_PRINT 型号;未维护配置或非打印设备时为空。 */
@@ -23,16 +23,16 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 尔印打印终端绑定和配置管理接口。
* 业务终端与无屏尔印 USB 打印机的绑定和配置管理接口。
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/hard-printer")
@Tag(description = "hard-printer", name = "尔印打印终端管理")
@Tag(description = "hard-printer", name = "尔印 USB 打印机管理")
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
public class HardPrinterController {
/** 打印终端绑定和配置服务。 */
/** USB 打印机绑定和配置服务。 */
private final HardPrinterService hardPrinterService;
/**
@@ -49,13 +49,13 @@ public class HardPrinterController {
}
/**
* 查询来源设备可绑定的 EY_PRINT 终端。
* 查询来源设备可绑定的 EY_PRINT 打印机资产。
*
* @param sourceHardId 来源业务终端 ID
* @return 可绑定设备列表
*/
@GetMapping("/available")
@Operation(summary = "查询可绑定打印终端")
@Operation(summary = "查询可绑定打印机")
@PreAuthorize("@pms.hasPermission('hard_hardManager_printer_bind')")
public R<?> listAvailable(@RequestParam Long sourceHardId) {
return R.ok(hardPrinterService.listAvailablePrinters(sourceHardId));
@@ -75,14 +75,14 @@ public class HardPrinterController {
}
/**
* 创建来源与打印终端的一对一绑定。
* 创建来源业务终端与 USB 打印机的一对一绑定。
*
* @param dto 绑定请求
* @return 绑定详情
*/
@PostMapping("/binding")
@Operation(summary = "绑定打印终端")
@SysLog("绑定打印终端")
@Operation(summary = "绑定 USB 打印机")
@SysLog("绑定 USB 打印机")
@PreAuthorize("@pms.hasPermission('hard_hardManager_printer_bind')")
public R<?> bind(@Valid @RequestBody HardPrinterBindingDto dto) {
return R.ok(hardPrinterService.bind(dto));
@@ -95,8 +95,8 @@ public class HardPrinterController {
* @return 空成功响应
*/
@DeleteMapping("/binding/{sourceHardId}")
@Operation(summary = "解绑打印终端")
@SysLog("解绑打印终端")
@Operation(summary = "解绑 USB 打印机")
@SysLog("解绑 USB 打印机")
@PreAuthorize("@pms.hasPermission('hard_hardManager_printer_bind')")
public R<Void> unbind(@PathVariable Long sourceHardId) {
hardPrinterService.unbind(sourceHardId);
@@ -104,27 +104,27 @@ public class HardPrinterController {
}
/**
* 查询打印终端当前参数。
* 查询 USB 打印机当前参数。
*
* @param printerHardId EY_PRINT 设备 ID
* @param printerHardId EY_PRINT 逻辑打印机资产 ID
* @return 当前或默认配置
*/
@GetMapping("/config/{printerHardId}")
@Operation(summary = "查询打印终端配置")
@Operation(summary = "查询 USB 打印机配置")
@PreAuthorize("@pms.hasPermission('hard_hardManager_view')")
public R<?> getConfig(@PathVariable Long printerHardId) {
return R.ok(hardPrinterService.getConfig(printerHardId));
}
/**
* 保存打印终端参数。
* 保存 USB 打印机参数。
*
* @param dto 配置请求
* @return 保存后的配置
*/
@PutMapping("/config")
@Operation(summary = "保存打印终端配置")
@SysLog("保存打印终端配置")
@Operation(summary = "保存 USB 打印机配置")
@SysLog("保存 USB 打印机配置")
@PreAuthorize("@pms.hasPermission('hard_hardManager_printer_bind')")
public R<?> saveConfig(@Valid @RequestBody HardPrinterConfigDto dto) {
return R.ok(hardPrinterService.saveConfig(dto));
@@ -331,52 +331,52 @@ public class HardApiController {
}
/**
* 查询当前 EY_PRINT 终端绑定关系和打印机参数,用于登录后进入打印专用模式。
* 查询当前食堂或超市终端所绑定的无屏 USB 打印机及打印参数。
*
* @param hardId DEVICE-ID 请求头中的打印终端 ID
* @return 绑定来源和打印机配置
* @param hardId DEVICE-ID 请求头中的来源业务终端 ID
* @return 当前绑定和打印机配置;尚未绑定时数据为空
*/
@GetMapping("/printer/bootstrap")
@Operation(summary = "获取打印终端启动配置")
@Operation(summary = "获取业务终端打印机绑定")
public R<?> printerBootstrap(@RequestHeader("DEVICE-ID") Long hardId) {
return R.ok(hardPrinterService.getPrinterBinding(hardId));
return R.ok(hardPrinterService.getSourceBinding(hardId));
}
/**
* 保存当前 EY_PRINT 实际连接打印机的型号、VID 和 PID,目标设备只能由请求头确定。
* 由当前食堂或超市终端上报其实际连接打印机的型号、VID 和 PID。
*
* @param hardId DEVICE-ID 请求头中的打印终端 ID
* @param hardId DEVICE-ID 请求头中的来源业务终端 ID
* @param dto Android USB Host 枚举出的打印机信息
* @return 保留纸宽、切刀和图片模式后的完整打印机配置
*/
@PostMapping("/printer/info")
@Operation(summary = "上报打印终端实际打印机信息")
@Operation(summary = "业务终端上报 USB 打印机信息")
public R<?> reportPrinterInfo(@RequestHeader("DEVICE-ID") Long hardId,
@Valid @RequestBody HardPrinterInfoReportDto dto) {
return R.ok(hardPrinterService.reportDeviceInfo(hardId, dto));
}
/**
* 拉取当前 EY_PRINT 尚未完成的任务,补偿离线期间遗漏的 MQTT 消息。
* 拉取当前食堂或超市终端所绑定打印机尚未完成的任务,补偿离线期间遗漏的 MQTT 消息。
*
* @param hardId DEVICE-ID 请求头中的打印终端 ID
* @param hardId DEVICE-ID 请求头中的来源业务终端 ID
* @return 待打印任务列表
*/
@GetMapping("/printer/tasks")
@Operation(summary = "拉取打印终端待办")
@Operation(summary = "拉取业务终端代理打印待办")
public R<?> printerTasks(@RequestHeader("DEVICE-ID") Long hardId) {
return R.ok(hardInvoiceService.listPendingTasks(hardId));
}
/**
* 提交当前 EY_PRINT 的 USB 打印结果。
* 由当前食堂或超市终端代理提交 USB 打印结果。
*
* @param hardId DEVICE-ID 请求头中的打印终端 ID
* @param hardId DEVICE-ID 请求头中的来源业务终端 ID
* @param result 任务、订单、版本和结果
* @return 接受或幂等忽略时返回 {@code true}
*/
@PostMapping("/printer/result")
@Operation(summary = "提交打印终端结果")
@Operation(summary = "提交业务终端代理打印结果")
public R<Boolean> printerResult(@RequestHeader("DEVICE-ID") Long hardId,
@Valid @RequestBody HardPrintResultDto result) {
return R.ok(hardInvoiceService.reportPrintResult(hardId, result));
@@ -5,7 +5,7 @@ import com.bsd.pjcloud.hard.api.entity.HardPrinterBindingEntity;
import org.apache.ibatis.annotations.Mapper;
/**
* 打印终端一对一绑定关系数据访问接口。
* 业务终端与无屏 USB 打印机资产一对一绑定关系数据访问接口。
*/
@Mapper
public interface HardPrinterBindingMapper extends PjcloudBaseMapper<HardPrinterBindingEntity> {
@@ -5,7 +5,7 @@ import com.bsd.pjcloud.hard.api.entity.HardPrinterConfigEntity;
import org.apache.ibatis.annotations.Mapper;
/**
* 尔印打印终端参数数据访问接口。
* 尔印 USB 打印机资产参数数据访问接口。
*/
@Mapper
public interface HardPrinterConfigMapper extends PjcloudBaseMapper<HardPrinterConfigEntity> {
@@ -95,21 +95,21 @@ public interface HardInvoiceService extends IService<HardInvoiceEntity> {
int cancelPendingVoucherTasks(Long orderId);
/**
* 查询指定 EY_PRINT 终端的待打印任务,供启动、重连和定时补拉。
* 查询指定来源业务终端所绑定打印机的待打印任务,供启动、重连和定时补拉。
*
* @param printerHardId 目标打印终端 ID
* @param sourceHardId DEVICE-ID 请求头中的来源业务终端 ID
* @return 待打印任务列表
*/
List<HardInvoiceEntity> listPendingTasks(Long printerHardId);
List<HardInvoiceEntity> listPendingTasks(Long sourceHardId);
/**
* 校验终端身份、订单和版本后记录打印结果。
*
* @param printerHardId DEVICE-ID 请求头解析出的目标设备
* @param sourceHardId DEVICE-ID 请求头解析出的来源业务终端
* @param result 终端回执
* @return 接受或幂等忽略回执时返回 {@code true}
*/
Boolean reportPrintResult(Long printerHardId, HardPrintResultDto result);
Boolean reportPrintResult(Long sourceHardId, HardPrintResultDto result);
/**
* 跨租户扫描并处理待生成和待打印任务。
@@ -12,25 +12,25 @@ import com.bsd.pjcloud.hard.api.vo.manager.HardManagerVo;
import java.util.List;
/**
* 尔印打印终端绑定和配置服务。
* 业务终端与无屏尔印 USB 打印机的绑定和配置服务。
*/
public interface HardPrinterService {
/**
* 查询指定来源或打印设备当前绑定详情。
* 查询指定来源终端或打印机资产当前绑定详情。
*
* @param hardId 来源业务终端或打印终端 ID
* @param hardId 来源业务终端或打印机资产 ID
* @return 绑定详情;未绑定时返回 {@code null}
*/
HardPrinterBindingVo getBinding(Long hardId);
/**
* 按目标打印终端身份读取启动绑定,拒绝来源业务终端借用打印模式接口。
* 按当前来源业务终端身份读取其绑定打印机和打印参数。
*
* @param printerHardId DEVICE-ID 声明的 EY_PRINT 设备 ID
* @return 已绑定的来源终端及打印参数
* @param sourceHardId DEVICE-ID 声明的 SUPERMARKET/CANTEEN 设备 ID
* @return 当前绑定及打印参数;尚未绑定时返回 {@code null}
*/
HardPrinterBindingVo getPrinterBinding(Long printerHardId);
HardPrinterBindingVo getSourceBinding(Long sourceHardId);
/**
* 批量补充设备分页中的绑定关系和打印参数,避免管理端逐行请求。
@@ -40,10 +40,10 @@ public interface HardPrinterService {
void enrichDevices(List<HardManagerVo> devices);
/**
* 查询来源设备同商户下尚未绑定的 EY_PRINT 设备。
* 查询来源设备同商户下尚未绑定的 EY_PRINT 打印机资产。
*
* @param sourceHardId 来源业务终端 ID
* @return 可绑定打印终端列表
* @return 可绑定打印机资产列表
*/
List<HardManagerEntity> listAvailablePrinters(Long sourceHardId);
@@ -87,16 +87,16 @@ public interface HardPrinterService {
HardPrinterConfigEntity saveConfig(HardPrinterConfigDto dto);
/**
* 由当前打印终端上报实际打印机型号和 USB 标识,保留后台维护的 ESC/POS 参数。
* 由当前来源业务终端代理上报实际 USB 打印机型号和标识,保留后台维护的 ESC/POS 参数。
*
* @param printerHardId DEVICE-ID 请求头中的 EY_PRINT 设备 ID
* @param sourceHardId DEVICE-ID 请求头中的 SUPERMARKET/CANTEEN 设备 ID
* @param dto Android USB Host 读取到的型号、VID 和 PID
* @return 更新后的打印机配置
*/
HardPrinterConfigEntity reportDeviceInfo(Long printerHardId, HardPrinterInfoReportDto dto);
HardPrinterConfigEntity reportDeviceInfo(Long sourceHardId, HardPrinterInfoReportDto dto);
/**
* 按来源业务终端解析当前有效打印终端。
* 按来源业务终端解析当前有效打印机资产。
*
* @param sourceHardId 来源终端 ID
* @return 当前绑定;未绑定时返回 {@code null}
@@ -268,7 +268,7 @@ public class HardInvoiceServiceImpl extends ServiceImpl<HardInvoiceMapper, HardI
throw new RuntimeException("打印任务与订单不匹配");
}
if (detail.getPrinterHardId() != null) {
throw new RuntimeException("尔印打印终端必须使用带设备身份的打印结果接口");
throw new RuntimeException("尔印打印任务必须由绑定业务终端使用带设备身份的结果接口");
}
if (Objects.equals(detail.getStatus(), HardPrintStatusEnum.PRINT.getType())) return true;
if (!Objects.equals(detail.getStatus(), HardPrintStatusEnum.GENERATE.getType())) return false;
@@ -413,10 +413,11 @@ public class HardInvoiceServiceImpl extends ServiceImpl<HardInvoiceMapper, HardI
/** {@inheritDoc} */
@Override
public List<HardInvoiceEntity> listPendingTasks(Long printerHardId) {
requirePrinterIdentity(printerHardId);
public List<HardInvoiceEntity> listPendingTasks(Long sourceHardId) {
HardPrinterBindingEntity binding = requireSourceBinding(sourceHardId);
return baseMapper.selectList(Wrappers.<HardInvoiceEntity>lambdaQuery()
.eq(HardInvoiceEntity::getPrinterHardId, printerHardId)
.eq(HardInvoiceEntity::getHardId, sourceHardId)
.eq(HardInvoiceEntity::getPrinterHardId, binding.getPrinterHardId())
.eq(HardInvoiceEntity::getStatus, HardPrintStatusEnum.GENERATE.getType())
.isNotNull(HardInvoiceEntity::getPic)
.orderByAsc(HardInvoiceEntity::getCreateTime)
@@ -426,14 +427,15 @@ public class HardInvoiceServiceImpl extends ServiceImpl<HardInvoiceMapper, HardI
/** {@inheritDoc} */
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean reportPrintResult(Long printerHardId, HardPrintResultDto result) {
requirePrinterIdentity(printerHardId);
public Boolean reportPrintResult(Long sourceHardId, HardPrintResultDto result) {
HardPrinterBindingEntity binding = requireSourceBinding(sourceHardId);
HardInvoiceEntity task = baseMapper.selectById(result.getTaskId());
if (task == null || !Objects.equals(task.getOrderId(), result.getOrderId())) {
throw new RuntimeException("打印任务与订单不匹配");
}
if (!Objects.equals(task.getPrinterHardId(), printerHardId)) {
throw new RuntimeException("当前设备不是任务目标打印终端");
if (!Objects.equals(task.getHardId(), sourceHardId)
|| !Objects.equals(task.getPrinterHardId(), binding.getPrinterHardId())) {
throw new RuntimeException("当前业务终端不是该打印任务的绑定代理");
}
int currentVersion = ObjectUtil.defaultIfNull(task.getRetryVersion(), 0);
if (result.getRetryVersion() < currentVersion) return true;
@@ -511,7 +513,7 @@ public class HardInvoiceServiceImpl extends ServiceImpl<HardInvoiceMapper, HardI
Long normalizedSource = sourceHardId != null && sourceHardId > 0 ? sourceHardId : null;
HardPrinterBindingEntity binding = printerService.findBySourceHardId(normalizedSource);
boolean canDispatch = binding != null;
HardInvoiceEntity task = HardInvoiceEntity.builder().deptId(order.getDeptId()).hardId(normalizedSource)
HardInvoiceEntity task = HardInvoiceEntity.builder().deptId(resolveTaskDeptId(order)).hardId(normalizedSource)
.printerHardId(canDispatch ? binding.getPrinterHardId() : null).orderId(order.getId())
.printType(printType.getType()).printNo(printNo).retryVersion(0).attemptCount(0)
.status(canDispatch ? HardPrintStatusEnum.PENDING_GENERATION.getType() : HardPrintStatusEnum.FAILED.getType())
@@ -530,6 +532,19 @@ public class HardInvoiceServiceImpl extends ServiceImpl<HardInvoiceMapper, HardI
return task;
}
/**
* 解析打印任务的数据权限归属部门。设备订单优先沿用下单设备部门,App 预约订单没有设备上下文时
* 使用下单时固化的商户部门快照,避免把后续核销设备的部门误写成订单归属。
*
* @param order 创建打印任务的订单快照
* @return 可持久化到打印任务的部门 ID
*/
private Long resolveTaskDeptId(HardOrderEntity order) {
Long deptId = order.getDeptId() != null ? order.getDeptId() : order.getMerchantDeptId();
if (deptId == null) throw new RuntimeException("订单缺少归属部门,无法创建打印任务");
return deptId;
}
/**
* 在当前事务提交后把任务投递到硬件异步线程池;无事务时立即投递。
*/
@@ -596,7 +611,7 @@ public class HardInvoiceServiceImpl extends ServiceImpl<HardInvoiceMapper, HardI
}
/**
* 向目标 EY_PRINT 发布 MQTT 任务,代理接受后仍保持待打印状态。
* 向绑定来源业务终端发布 MQTT 任务,代理接受后仍保持待打印状态。
*/
private void dispatchTask(HardInvoiceEntity task) {
try {
@@ -604,7 +619,7 @@ public class HardInvoiceServiceImpl extends ServiceImpl<HardInvoiceMapper, HardI
if (order == null) throw new RuntimeException("订单不存在");
HardPrintMsg message = new HardPrintMsg();
message.setUserId(order.getUserId());
message.setHardId(task.getPrinterHardId());
message.setHardId(task.getHardId());
message.setSourceHardId(task.getHardId());
message.setTargetHardId(task.getPrinterHardId());
message.setMchDeptId(order.getMerchantDeptId());
@@ -743,12 +758,22 @@ public class HardInvoiceServiceImpl extends ServiceImpl<HardInvoiceMapper, HardI
return sourceHardId;
}
/** 校验 DEVICE-ID 确实属于 EY_PRINT 终端。 */
private void requirePrinterIdentity(Long printerHardId) {
if (printerHardId == null) throw new RuntimeException("DEVICE-ID不能为空");
HardManagerEntity printer = hardManagerService.getById(printerHardId);
HardTypeEnum type = printer == null ? null : HardTypeEnum.valueOfByType(printer.getType());
if (printer == null || !HardTypeEnum.isPrintHard(type)) throw new RuntimeException("当前设备不是尔印打印终端");
/**
* 校验 DEVICE-ID 属于食堂或超市 Android 终端,并返回其当前打印机绑定。
*
* @param sourceHardId DEVICE-ID 中的来源业务终端 ID
* @return 当前来源终端的一对一打印机绑定
*/
private HardPrinterBindingEntity requireSourceBinding(Long sourceHardId) {
if (sourceHardId == null) throw new RuntimeException("DEVICE-ID不能为空");
HardManagerEntity source = hardManagerService.getById(sourceHardId);
HardTypeEnum type = source == null ? null : HardTypeEnum.valueOfByType(source.getType());
if (source == null || (type != HardTypeEnum.SUPERMARKET && type != HardTypeEnum.CANTEEN)) {
throw new RuntimeException("当前设备不是超市或食堂业务终端");
}
HardPrinterBindingEntity binding = printerService.findBySourceHardId(sourceHardId);
if (binding == null) throw new RuntimeException("当前业务终端尚未绑定尔印打印机");
return binding;
}
/** 将异常写入指定任务,条件版本防止旧处理覆盖人工新重试。 */
@@ -20,10 +20,14 @@ import com.bsd.pjcloud.hard.mapper.HardPrinterBindingMapper;
import com.bsd.pjcloud.hard.mapper.HardPrinterConfigMapper;
import com.bsd.pjcloud.hard.mapper.manager.HardManagerMapper;
import com.bsd.pjcloud.hard.service.HardPrinterService;
import com.bsd.pjcloud.mqtt.api.feign.RemoteMqttEventService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.util.List;
import java.util.Map;
@@ -32,10 +36,11 @@ import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 尔印打印终端绑定和配置服务实现。
* 业务终端与无屏尔印 USB 打印机的绑定和配置服务实现。
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class HardPrinterServiceImpl implements HardPrinterService {
/** 默认纸张宽度,单位毫米。 */
@@ -65,6 +70,9 @@ public class HardPrinterServiceImpl implements HardPrinterService {
/** 打印任务 Mapper。 */
private final HardInvoiceMapper invoiceMapper;
/** MQTT 配置变更通知服务,用于让业务终端立即刷新打印机绑定。 */
private final RemoteMqttEventService mqttEventService;
/** {@inheritDoc} */
@Override
public HardPrinterBindingVo getBinding(Long hardId) {
@@ -78,13 +86,10 @@ public class HardPrinterServiceImpl implements HardPrinterService {
/** {@inheritDoc} */
@Override
public HardPrinterBindingVo getPrinterBinding(Long printerHardId) {
HardManagerEntity printer = requirePrinter(printerHardId);
HardPrinterBindingEntity binding = bindingMapper.selectOne(Wrappers.<HardPrinterBindingEntity>lambdaQuery()
.eq(HardPrinterBindingEntity::getPrinterHardId, printer.getId())
.last("limit 1"));
if (binding == null) throw new RuntimeException("尔印打印终端尚未绑定业务终端");
return assemble(binding);
public HardPrinterBindingVo getSourceBinding(Long sourceHardId) {
HardManagerEntity source = requireSource(sourceHardId);
HardPrinterBindingEntity binding = findBySourceHardId(source.getId());
return binding == null ? null : assemble(binding);
}
/** {@inheritDoc} */
@@ -173,13 +178,13 @@ public class HardPrinterServiceImpl implements HardPrinterService {
HardManagerEntity source = requireSource(dto.getSourceHardId());
HardManagerEntity printer = requirePrinter(dto.getPrinterHardId());
if (!Objects.equals(source.getTenantId(), printer.getTenantId())) {
throw new RuntimeException("来源终端和打印终端必须属于同一租户");
throw new RuntimeException("来源终端和打印机资产必须属于同一租户");
}
if (!Objects.equals(source.getMerchantId(), printer.getMerchantId())) {
throw new RuntimeException("来源终端和打印终端必须属于同一商户");
throw new RuntimeException("来源终端和打印机资产必须属于同一商户");
}
if (!Objects.equals(source.getDeptId(), printer.getDeptId())) {
throw new RuntimeException("来源终端和打印终端必须属于同一商户部门");
throw new RuntimeException("来源终端和打印机资产必须属于同一商户部门");
}
HardPrinterBindingEntity binding = HardPrinterBindingEntity.builder()
.sourceHardId(source.getId())
@@ -191,12 +196,13 @@ public class HardPrinterServiceImpl implements HardPrinterService {
try {
bindingMapper.insert(binding);
} catch (DuplicateKeyException exception) {
throw new RuntimeException("来源终端或打印终端已经绑定,请先解除原绑定", exception);
throw new RuntimeException("来源终端或打印机资产已经绑定,请先解除原绑定", exception);
}
if (configMapper.selectCount(Wrappers.<HardPrinterConfigEntity>lambdaQuery()
.eq(HardPrinterConfigEntity::getPrinterHardId, printer.getId())) == 0) {
configMapper.insert(defaultConfig(printer));
}
notifyBindingChangeAfterCommit(source.getDeptId());
return assemble(binding);
}
@@ -210,6 +216,7 @@ public class HardPrinterServiceImpl implements HardPrinterService {
throw new RuntimeException("存在待生成、待打印或打印失败任务,处理完成后才能解绑");
}
bindingMapper.deleteById(binding.getId());
notifyBindingChangeAfterCommit(binding.getDeptId());
}
/** {@inheritDoc} */
@@ -239,14 +246,17 @@ public class HardPrinterServiceImpl implements HardPrinterService {
config.setId(existing.getId());
configMapper.updateById(config);
}
HardPrinterBindingEntity binding = findByPrinterHardId(printer.getId());
if (binding != null) notifyBindingChangeAfterCommit(binding.getDeptId());
return configMapper.selectById(config.getId());
}
/** {@inheritDoc} */
@Override
@Transactional(rollbackFor = Exception.class)
public HardPrinterConfigEntity reportDeviceInfo(Long printerHardId, HardPrinterInfoReportDto dto) {
HardManagerEntity printer = requirePrinter(printerHardId);
public HardPrinterConfigEntity reportDeviceInfo(Long sourceHardId, HardPrinterInfoReportDto dto) {
HardPrinterBindingEntity binding = requireSourceBinding(sourceHardId);
HardManagerEntity printer = requirePrinter(binding.getPrinterHardId());
HardPrinterConfigEntity config = configMapper.selectOne(Wrappers.<HardPrinterConfigEntity>lambdaQuery()
.eq(HardPrinterConfigEntity::getPrinterHardId, printer.getId())
.last("limit 1"));
@@ -313,21 +323,74 @@ public class HardPrinterServiceImpl implements HardPrinterService {
}
/**
* 校验并返回 EY_PRINT 打印终端。
* 校验并返回 EY_PRINT 逻辑打印机资产。
*
* @param hardId 打印设备 ID
* @return 有效打印设备
* @return 有效打印机资产
*/
private HardManagerEntity requirePrinter(Long hardId) {
HardManagerEntity printer = hardManagerMapper.selectById(hardId);
HardTypeEnum type = printer == null ? null : HardTypeEnum.valueOfByType(printer.getType());
if (printer == null || !HardTypeEnum.isPrintHard(type)) {
throw new RuntimeException("目标设备必须是尔印打印终端");
throw new RuntimeException("目标设备必须是尔印打印机");
}
if (printer.getMerchantId() == null) throw new RuntimeException("打印终端未绑定商户");
if (printer.getMerchantId() == null) throw new RuntimeException("打印机未绑定商户");
return printer;
}
/**
* 校验当前 Android 来源终端并返回其一对一打印机绑定。
*
* @param sourceHardId DEVICE-ID 中的来源业务终端 ID
* @return 当前来源终端的有效绑定
*/
private HardPrinterBindingEntity requireSourceBinding(Long sourceHardId) {
HardManagerEntity source = requireSource(sourceHardId);
HardPrinterBindingEntity binding = findBySourceHardId(source.getId());
if (binding == null) throw new RuntimeException("当前业务终端尚未绑定尔印打印机");
return binding;
}
/**
* 查询逻辑打印机资产当前绑定的来源业务终端。
*
* @param printerHardId EY_PRINT 逻辑打印机资产 ID
* @return 当前一对一绑定;未绑定时为空
*/
private HardPrinterBindingEntity findByPrinterHardId(Long printerHardId) {
if (printerHardId == null || printerHardId <= 0) return null;
return bindingMapper.selectOne(Wrappers.<HardPrinterBindingEntity>lambdaQuery()
.eq(HardPrinterBindingEntity::getPrinterHardId, printerHardId)
.last("limit 1"));
}
/**
* 在本地事务提交后通知业务终端刷新绑定,MQTT 暂时不可用时不回滚已保存的数据。
*
* @param deptId 来源业务终端所属部门 ID;为空时跳过通知
*/
private void notifyBindingChangeAfterCommit(Long deptId) {
if (deptId == null) return;
Runnable notifyAction = () -> {
try {
mqttEventService.sendConfigChangeMsg(deptId);
} catch (Exception exception) {
log.warn("打印机绑定配置已保存,但MQTT刷新通知发送失败,deptId:{}", deptId, exception);
}
};
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
notifyAction.run();
return;
}
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
/** 本地绑定事务提交后再通知,避免终端读取到尚未提交的旧关系。 */
@Override
public void afterCommit() {
notifyAction.run();
}
});
}
/**
* 校验受首期协议约束的打印参数。
*
@@ -399,7 +462,7 @@ public class HardPrinterServiceImpl implements HardPrinterService {
/**
* 创建尚未持久化的首期默认打印配置。
*
* @param printer 打印终端
* @param printer 逻辑打印机资产
* @return 默认配置
*/
private HardPrinterConfigEntity defaultConfig(HardManagerEntity printer) {
@@ -2,6 +2,7 @@ package com.bsd.pjcloud.hard.service.impl.manager;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -67,7 +68,7 @@ public class HardManagerServiceImpl extends ServiceImpl<HardManagerMapper, HardM
private final RemoteDeptService deptService;
private final RemoteMqttEventService remoteMqttEventService;
/** 打印终端绑定服务,用于保护已绑定设备的关键属性和删除动作。 */
/** 打印机绑定服务,用于保护已绑定设备的关键属性和删除动作。 */
private final HardPrinterService hardPrinterService;
/**
* 为已完成数据权限过滤的设备分页批量补充终端运行状态。
@@ -147,11 +148,15 @@ public class HardManagerServiceImpl extends ServiceImpl<HardManagerMapper, HardM
dto.setHardGroupId(deptHardGroup.getHardGroupId());
dto.setDeptId(mch.getDeptId());
dto.setUniqueCode(dto.getUniqueCode());
// mq地址和端口
dto.setMqSocketAddress(mqttConfig.getAndroidAddress());
dto.setMqSocketPort(mqttConfig.getAndroidPort());
boolean logicalPrinter = HardTypeEnum.EY_PRINT.getType().equals(dto.getType());
// EY_PRINT 是由业务终端代理的无屏 USB 外设,不生成自身 MQTT 连接配置。
if (!logicalPrinter) {
dto.setMqSocketAddress(mqttConfig.getAndroidAddress());
dto.setMqSocketPort(mqttConfig.getAndroidPort());
}
// 插入
baseMapper.insert(dto);
if (logicalPrinter) return dto.getId();
// topic
HardManagerEntity updateObj = HardManagerEntity.builder().id(dto.getId()).build();
String topic = createMqConfigByAction(deptHardGroup.getHardGroupId(), null, null, 1) + "/#";
@@ -185,7 +190,10 @@ public class HardManagerServiceImpl extends ServiceImpl<HardManagerMapper, HardM
dto.setDeptId(mch.getDeptId());
dto.setUniqueCode(dto.getUniqueCode());
dto.setHardGroupId(deptHardGroup.getHardGroupId());
if (!ObjectUtil.equals(dto.getMerchantId(), detail.getMerchantId())) {
String effectiveType = StrUtil.blankToDefault(dto.getType(), detail.getType());
boolean logicalPrinter = HardTypeEnum.EY_PRINT.getType().equals(effectiveType);
// 无屏打印机资产不连接 MQTT;调整其归属时也不能生成 Android 终端凭据。
if (!logicalPrinter && !ObjectUtil.equals(dto.getMerchantId(), detail.getMerchantId())) {
// topic
String topic = createMqConfigByAction(deptHardGroup.getHardGroupId(), null, null, 1) + "/#";
dto.setMainTopic(topic);
@@ -197,8 +205,8 @@ public class HardManagerServiceImpl extends ServiceImpl<HardManagerMapper, HardM
dto.setMqUsername(mqUsername);
}
baseMapper.updateById(dto);
// 更新mq
remoteMqttEventService.sendConfigChangeMsg(detail.getDeptId());
// 普通业务终端更新后沿用原逻辑刷新 MQ 配置;打印机资产的绑定接口会通知实际代理终端。
if (!logicalPrinter) remoteMqttEventService.sendConfigChangeMsg(detail.getDeptId());
}
/** {@inheritDoc} */
@@ -216,8 +224,7 @@ public class HardManagerServiceImpl extends ServiceImpl<HardManagerMapper, HardM
throw new RuntimeException("设备不存在");
}
if (HardTypeEnum.EY_PRINT.getType().equals(detail.getType())) {
// 打印终端必须先由管理端完成一对一来源绑定,避免未归属设备登录后接收错误商户任务。
hardPrinterService.getPrinterBinding(detail.getId());
throw new RuntimeException("尔印打印机是无屏 USB 外设,请在绑定的超市或食堂终端上操作");
}
if (existUniqueCode(dto.getUniqueCode(), dto.getId())) {
throw new RuntimeException("该唯一编码已经被绑定");
@@ -147,14 +147,13 @@ public class HardTerminalStatusServiceImpl extends ServiceImpl<HardTerminalStatu
}
/**
* 判断设备是否属于支持在线监控的收银或尔印打印 Android 终端。
* 判断设备是否属于支持在线监控的食堂或超市 Android 终端。
*
* @param hardType {@code hard_manager.type} 的持久化编码
* @return 超市、食堂或尔印打印设备返回 {@code true}
* @return 超市或食堂终端返回 {@code true};无屏 EY_PRINT 外设不单独上报心跳
*/
private boolean isSupported(String hardType) {
return ObjectUtil.equals(HardTypeEnum.SUPERMARKET.getType(), hardType)
|| ObjectUtil.equals(HardTypeEnum.CANTEEN.getType(), hardType)
|| ObjectUtil.equals(HardTypeEnum.EY_PRINT.getType(), hardType);
|| ObjectUtil.equals(HardTypeEnum.CANTEEN.getType(), hardType);
}
}
@@ -3941,27 +3941,58 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
HardOrderTypeEnum orderType = HardOrderTypeEnum.valueOfByType(order.getOrderType());
boolean isCancel = validateCodeIsCancel(orderType, order.getId());
if (isCancel) throw new I18nBizException(ErrorCodes.HARD_QR_ALREADY_VERIFIED);
// 核销二维码
getSelf().cancelCode(order, userInfo);
LocalDateTime verificationTime = LocalDateTime.now();
HardOrderEntity updateObj = HardOrderEntity.builder()
.id(order.getId())
.androidOrderCode(dto.getAndroidOrderCode())
.hardId(dto.getHardwareId())
.orderStatus(HardOrderStatusEnum.CANCEL.getType())
.verificationTime(verificationTime)
.build();
boolean isReservationVerification = isReservationVerification(order);
int updated = isReservationVerification
? baseMapper.update(updateObj, new LambdaUpdateWrapper<HardOrderEntity>()
.eq(HardOrderEntity::getId, order.getId())
.eq(HardOrderEntity::getOrderStatus, HardOrderStatusEnum.PAY.getType()))
: baseMapper.updateById(updateObj);
if (updated == 0) {
throw new I18nBizException(ErrorCodes.HARD_VERIFY_STATUS_CHANGED);
boolean remoteCodeCancelled = false;
try {
// Wallet 是独立远程事务,成功返回后若 Hardware 本地事务失败,必须显式恢复预约二维码。
getSelf().cancelCode(order, userInfo);
remoteCodeCancelled = true;
LocalDateTime verificationTime = LocalDateTime.now();
HardOrderEntity updateObj = HardOrderEntity.builder()
.id(order.getId())
.androidOrderCode(dto.getAndroidOrderCode())
.hardId(dto.getHardwareId())
.orderStatus(HardOrderStatusEnum.CANCEL.getType())
.verificationTime(verificationTime)
.build();
int updated = isReservationVerification
? baseMapper.update(updateObj, new LambdaUpdateWrapper<HardOrderEntity>()
.eq(HardOrderEntity::getId, order.getId())
.eq(HardOrderEntity::getOrderStatus, HardOrderStatusEnum.PAY.getType()))
: baseMapper.updateById(updateObj);
if (updated == 0) {
throw new I18nBizException(ErrorCodes.HARD_VERIFY_STATUS_CHANGED);
}
if (isReservationVerification) createReservationVerificationArtifacts(order, verificationTime, dto.getHardwareId());
return true;
} catch (RuntimeException exception) {
if (isReservationVerification && remoteCodeCancelled) {
rollbackReservationCodeAfterLocalFailure(order, exception);
}
throw exception;
}
}
/**
* Hardware 本地核销事务失败后补偿已经提交的 Wallet 预约码状态,使订单仍可重新核销。补偿失败时
* 保留原始业务异常,并把恢复异常作为 suppressed exception 记录,便于运维识别需要人工修复的订单。
*
* @param order 本次核销失败的预约订单
* @param originalException 触发 Hardware 本地事务回滚的原始异常
*/
private void rollbackReservationCodeAfterLocalFailure(HardOrderEntity order, RuntimeException originalException) {
try {
R<Boolean> result = remoteCodeUseService.rollbackReservationCode(order.getId());
if (result != null && result.isOk() && Boolean.TRUE.equals(result.getData())) return;
String message = result == null ? "恢复预约核销码无响应"
: StrUtil.blankToDefault(result.getMsg(), "恢复预约核销码失败");
IllegalStateException rollbackException = new IllegalStateException(message);
originalException.addSuppressed(rollbackException);
log.error("预约核销本地事务失败且二维码补偿失败,orderId:{},message:{}", order.getId(), message);
} catch (RuntimeException rollbackException) {
originalException.addSuppressed(rollbackException);
log.error("预约核销本地事务失败且二维码补偿异常,orderId:{}", order.getId(), rollbackException);
}
if (isReservationVerification) createReservationVerificationArtifacts(order, verificationTime, dto.getHardwareId());
return true;
}
/**
@@ -95,7 +95,7 @@ class HardApiControllerTest {
assertEquals(10, result.get(0).getOriginalProductCount());
}
/** 验证上报目标只使用可信 DEVICE-ID,并将服务保存结果原样返回。 */
/** 验证来源终端代理上报只使用可信 DEVICE-ID,并将服务保存结果原样返回。 */
@Test
void reportPrinterInfoUsesTrustedDeviceHeader() {
HardPrinterInfoReportDto request = new HardPrinterInfoReportDto();
@@ -106,12 +106,12 @@ class HardApiControllerTest {
.printerHardId(20L).printerModel(request.getPrinterModel())
.usbVendorId(10473).usbProductId(649).paperWidth(80)
.autoCut("1").imageMode("GS_V0").build();
when(hardPrinterService.reportDeviceInfo(20L, request)).thenReturn(saved);
when(hardPrinterService.reportDeviceInfo(10L, request)).thenReturn(saved);
HardPrinterConfigEntity result = (HardPrinterConfigEntity) controller.reportPrinterInfo(20L, request).getData();
HardPrinterConfigEntity result = (HardPrinterConfigEntity) controller.reportPrinterInfo(10L, request).getData();
assertSame(saved, result);
verify(hardPrinterService).reportDeviceInfo(20L, request);
verify(hardPrinterService).reportDeviceInfo(10L, request);
}
/** 构造带原购、剩余数量和原行金额的购物车展示对象。 */
@@ -125,8 +125,10 @@ class HardInvoiceServiceImplTest {
@Test
void reportPrintResultExcludesCancelledStatusAtUpdateTime() {
HardInvoiceEntity pending = task(100L, HardPrintStatusEnum.GENERATE, 2);
pending.setHardId(10L);
pending.setPrinterHardId(20L);
when(managerService.getById(20L)).thenReturn(printer());
when(managerService.getById(10L)).thenReturn(source());
when(printerService.findBySourceHardId(10L)).thenReturn(binding());
when(invoiceMapper.selectById(100L)).thenReturn(pending);
when(invoiceMapper.update(isNull(), any(Wrapper.class))).thenReturn(0);
HardPrintResultDto result = new HardPrintResultDto();
@@ -136,7 +138,7 @@ class HardInvoiceServiceImplTest {
result.setSuccess(true);
result.setAttemptCount(1);
assertFalse(service.reportPrintResult(20L, result));
assertFalse(service.reportPrintResult(10L, result));
@SuppressWarnings("unchecked")
ArgumentCaptor<LambdaUpdateWrapper<HardInvoiceEntity>> wrapperCaptor =
@@ -151,8 +153,10 @@ class HardInvoiceServiceImplTest {
@Test
void lateFailureCannotOverwritePrintedTask() {
HardInvoiceEntity printed = task(100L, HardPrintStatusEnum.PRINT, 2);
printed.setHardId(10L);
printed.setPrinterHardId(20L);
when(managerService.getById(20L)).thenReturn(printer());
when(managerService.getById(10L)).thenReturn(source());
when(printerService.findBySourceHardId(10L)).thenReturn(binding());
when(invoiceMapper.selectById(100L)).thenReturn(printed);
HardPrintResultDto result = new HardPrintResultDto();
result.setTaskId(100L);
@@ -162,7 +166,7 @@ class HardInvoiceServiceImplTest {
result.setAttemptCount(3);
result.setErrorMessage("late error");
assertTrue(service.reportPrintResult(20L, result));
assertTrue(service.reportPrintResult(10L, result));
verify(invoiceMapper, never()).update(isNull(), any(Wrapper.class));
}
@@ -174,10 +178,6 @@ class HardInvoiceServiceImplTest {
return HardManagerEntity.builder().id(10L).type(HardTypeEnum.CANTEEN.getType()).merchantId(30L).build();
}
private static HardManagerEntity printer() {
return HardManagerEntity.builder().id(20L).type(HardTypeEnum.EY_PRINT.getType()).merchantId(30L).build();
}
private static HardPrinterBindingEntity binding() {
return HardPrinterBindingEntity.builder().sourceHardId(10L).printerHardId(20L).merchantId(30L).build();
}
@@ -3,6 +3,7 @@ package com.bsd.pjcloud.hard.service.impl;
import com.bsd.pjcloud.hard.api.constant.HardTypeEnum;
import com.bsd.pjcloud.hard.api.dto.HardPrinterConfigDto;
import com.bsd.pjcloud.hard.api.dto.HardPrinterInfoReportDto;
import com.bsd.pjcloud.hard.api.entity.HardPrinterBindingEntity;
import com.bsd.pjcloud.hard.api.entity.HardPrinterConfigEntity;
import com.bsd.pjcloud.hard.api.entity.manager.HardManagerEntity;
import com.bsd.pjcloud.hard.api.vo.HardPrinterBindingVo;
@@ -10,6 +11,7 @@ import com.bsd.pjcloud.hard.api.vo.manager.HardManagerVo;
import com.bsd.pjcloud.hard.mapper.HardPrinterBindingMapper;
import com.bsd.pjcloud.hard.mapper.HardPrinterConfigMapper;
import com.bsd.pjcloud.hard.mapper.manager.HardManagerMapper;
import com.bsd.pjcloud.mqtt.api.feign.RemoteMqttEventService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
@@ -33,19 +35,29 @@ import static org.mockito.Mockito.when;
class HardPrinterServiceImplTest {
private final HardManagerMapper managerMapper = mock(HardManagerMapper.class);
/** 打印配置数据访问对象,用于验证终端上报只修改允许字段。 */
/** 打印机绑定数据访问对象,用于把来源业务终端解析为目标打印机资产。 */
private final HardPrinterBindingMapper bindingMapper = mock(HardPrinterBindingMapper.class);
/** 打印配置数据访问对象,用于验证来源终端代理上报只修改允许字段。 */
private final HardPrinterConfigMapper configMapper = mock(HardPrinterConfigMapper.class);
/** MQTT 通知服务,用于隔离绑定配置刷新通知。 */
private final RemoteMqttEventService mqttEventService = mock(RemoteMqttEventService.class);
/** 使用真实业务方法和注入 Mapper 的被测服务。 */
private final HardPrinterServiceImpl service = mock(HardPrinterServiceImpl.class, CALLS_REAL_METHODS);
/** 为每个用例注入相同 Mapper,并准备一台有效的 EY_PRINT 设备。 */
/** 为每个用例注入相同 Mapper,并准备一台来源终端及其绑定的 EY_PRINT 打印机。 */
@BeforeEach
void setUp() {
ReflectionTestUtils.setField(service, "hardManagerMapper", managerMapper);
ReflectionTestUtils.setField(service, "bindingMapper", bindingMapper);
ReflectionTestUtils.setField(service, "configMapper", configMapper);
ReflectionTestUtils.setField(service, "mqttEventService", mqttEventService);
when(managerMapper.selectById(10L)).thenReturn(HardManagerEntity.builder()
.id(10L).type(HardTypeEnum.CANTEEN.getType()).merchantId(30L).tenantId(50L).build());
when(managerMapper.selectById(20L)).thenReturn(HardManagerEntity.builder()
.id(20L).type(HardTypeEnum.EY_PRINT.getType()).merchantId(30L).tenantId(50L).build());
when(bindingMapper.selectOne(any())).thenReturn(HardPrinterBindingEntity.builder()
.sourceHardId(10L).printerHardId(20L).merchantId(30L).tenantId(50L).build());
}
@Test
@@ -60,8 +72,6 @@ class HardPrinterServiceImplTest {
/** 未绑定打印机的普通设备应保留空配置,不能因空打印机 ID 导致列表查询失败。 */
@Test
void enrichDevicesSkipsPrinterConfigForUnboundDevice() {
HardPrinterBindingMapper bindingMapper = mock(HardPrinterBindingMapper.class);
ReflectionTestUtils.setField(service, "bindingMapper", bindingMapper);
HardManagerVo device = new HardManagerVo();
device.setId(10L);
device.setType(HardTypeEnum.SUPERMARKET.getType());
@@ -73,6 +83,14 @@ class HardPrinterServiceImplTest {
assertNull(device.getPrinterModel());
}
/** 未绑定来源业务终端查询绑定时应返回空,由客户端保留本地 USB 测试能力。 */
@Test
void getSourceBindingReturnsNullWhenBusinessTerminalIsUnbound() {
when(bindingMapper.selectOne(any())).thenReturn(null);
assertNull(service.getSourceBinding(10L));
}
@Test
void usbVidAndPidMustBeConfiguredTogether() {
HardPrinterConfigDto config = validConfig();
@@ -94,7 +112,7 @@ class HardPrinterServiceImplTest {
assertEquals("USB VID必须在0到65535之间", exception.getMessage());
}
/** 终端重新上报真机参数时必须保留管理端维护的纸宽、切刀和图片模式。 */
/** 来源终端重新上报真机参数时必须保留管理端维护的纸宽、切刀和图片模式。 */
@Test
void reportDeviceInfoPreservesEscPosConfiguration() {
HardPrinterConfigEntity existing = HardPrinterConfigEntity.builder()
@@ -103,7 +121,7 @@ class HardPrinterServiceImplTest {
.autoCut("0").imageMode("ESC_STAR").tenantId(50L).build();
when(configMapper.selectOne(any())).thenReturn(existing);
HardPrinterConfigEntity result = service.reportDeviceInfo(20L, reportRequest());
HardPrinterConfigEntity result = service.reportDeviceInfo(10L, reportRequest());
assertEquals("GEZHI / micro-printer", result.getPrinterModel());
assertEquals(10473, result.getUsbVendorId());
@@ -119,7 +137,7 @@ class HardPrinterServiceImplTest {
void reportDeviceInfoCreatesDefaultConfiguration() {
when(configMapper.selectOne(any())).thenReturn(null);
HardPrinterConfigEntity result = service.reportDeviceInfo(20L, reportRequest());
HardPrinterConfigEntity result = service.reportDeviceInfo(10L, reportRequest());
assertEquals(20L, result.getPrinterHardId());
assertEquals("GEZHI / micro-printer", result.getPrinterModel());
@@ -131,16 +149,13 @@ class HardPrinterServiceImplTest {
verify(configMapper).insert(result);
}
/** 普通业务终端不能借用设备信息接口覆盖打印终端配置。 */
/** EY_PRINT 资产不能冒充有屏 Android 来源终端上报自身配置。 */
@Test
void reportDeviceInfoRejectsNonPrinterDevice() {
when(managerMapper.selectById(21L)).thenReturn(HardManagerEntity.builder()
.id(21L).type(HardTypeEnum.CANTEEN.getType()).merchantId(30L).tenantId(50L).build());
void reportDeviceInfoRejectsPrinterAssetIdentity() {
RuntimeException exception = assertThrows(RuntimeException.class,
() -> service.reportDeviceInfo(21L, reportRequest()));
() -> service.reportDeviceInfo(20L, reportRequest()));
assertEquals("目标设备必须是尔印打印终端", exception.getMessage());
assertEquals("来源终端必须是超市或食堂设备", exception.getMessage());
}
/** 已绑定设备的部门属于一对一绑定身份,修改部门前必须先解绑。 */
@@ -9,7 +9,7 @@ public class HardPrintMsg extends MqttHardBaseMsg {
// 用户id
private Long userId;
// 设备id
// 实际接收 MQTT 并代理 USB 打印的食堂或超市终端 ID
private Long hardId;
/**
@@ -19,9 +19,9 @@ public class HardPrintMsg extends MqttHardBaseMsg {
private Long sourceHardId;
/**
* 实际接收 MQTT 并执行 USB 打印的 EY_PRINT 终端 ID。
* 后台绑定的 EY_PRINT 逻辑打印机资产 ID,用于客户端校验绑定关系。
*/
@Schema(description = "目标打印终端ID")
@Schema(description = "逻辑打印机资产ID")
private Long targetHardId;
// 设备部门id