diff --git a/db/20260904_add_er_yin_printer.sql b/db/20260904_add_er_yin_printer.sql
new file mode 100644
index 00000000..87070122
--- /dev/null
+++ b/db/20260904_add_er_yin_printer.sql
@@ -0,0 +1,317 @@
+-- 尔印独立打印终端、终端一对一绑定及持久化打印任务。
+-- 先执行硬件库结构,再执行UPMS字典和按钮权限;所有新增MQTT字段保持向后兼容。
+-- 本脚本需要 MySQL 8.x 兼容的窗口函数,历史序号回填和批量主键分配使用 ROW_NUMBER()。
+
+ALTER TABLE `pjcloudx_hardware`.`hard_invoice`
+ MODIFY COLUMN `hard_id` bigint(20) NULL COMMENT '来源业务或核销终端id',
+ MODIFY COLUMN `pic` varchar(512) NULL COMMENT '后端生成的打印PNG地址';
+
+-- 每个结构变更独立判断,可在兼容数据库自动提交部分DDL后安全重跑整个脚本。
+SET @er_yin_ddl = IF(
+ (SELECT COUNT(*) FROM `information_schema`.`columns`
+ WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
+ AND `column_name` = 'printer_hard_id') = 0,
+ 'ALTER TABLE `pjcloudx_hardware`.`hard_invoice` ADD COLUMN `printer_hard_id` bigint(20) NULL COMMENT ''目标尔印打印终端id'' AFTER `hard_id`',
+ 'SELECT 1'
+);
+PREPARE er_yin_stmt FROM @er_yin_ddl;
+EXECUTE er_yin_stmt;
+DEALLOCATE PREPARE er_yin_stmt;
+
+SET @er_yin_ddl = IF(
+ (SELECT COUNT(*) FROM `information_schema`.`columns`
+ WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
+ AND `column_name` = 'print_type') = 0,
+ 'ALTER TABLE `pjcloudx_hardware`.`hard_invoice` ADD COLUMN `print_type` char(2) NOT NULL DEFAULT ''1'' COMMENT ''打印类型,1发票、2兑换券'' AFTER `order_id`',
+ 'SELECT 1'
+);
+PREPARE er_yin_stmt FROM @er_yin_ddl;
+EXECUTE er_yin_stmt;
+DEALLOCATE PREPARE er_yin_stmt;
+
+SET @er_yin_ddl = IF(
+ (SELECT COUNT(*) FROM `information_schema`.`columns`
+ WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
+ AND `column_name` = 'print_no') = 0,
+ 'ALTER TABLE `pjcloudx_hardware`.`hard_invoice` ADD COLUMN `print_no` int(11) NOT NULL DEFAULT 1 COMMENT ''同订单同类型打印序号'' AFTER `print_type`',
+ 'SELECT 1'
+);
+PREPARE er_yin_stmt FROM @er_yin_ddl;
+EXECUTE er_yin_stmt;
+DEALLOCATE PREPARE er_yin_stmt;
+
+SET @er_yin_ddl = IF(
+ (SELECT COUNT(*) FROM `information_schema`.`columns`
+ WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
+ AND `column_name` = 'retry_version') = 0,
+ 'ALTER TABLE `pjcloudx_hardware`.`hard_invoice` ADD COLUMN `retry_version` int(11) NOT NULL DEFAULT 0 COMMENT ''人工失败重试版本'' AFTER `print_no`',
+ 'SELECT 1'
+);
+PREPARE er_yin_stmt FROM @er_yin_ddl;
+EXECUTE er_yin_stmt;
+DEALLOCATE PREPARE er_yin_stmt;
+
+SET @er_yin_ddl = IF(
+ (SELECT COUNT(*) FROM `information_schema`.`columns`
+ WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
+ AND `column_name` = 'attempt_count') = 0,
+ 'ALTER TABLE `pjcloudx_hardware`.`hard_invoice` ADD COLUMN `attempt_count` int(11) NOT NULL DEFAULT 0 COMMENT ''终端USB累计尝试次数'' AFTER `retry_version`',
+ 'SELECT 1'
+);
+PREPARE er_yin_stmt FROM @er_yin_ddl;
+EXECUTE er_yin_stmt;
+DEALLOCATE PREPARE er_yin_stmt;
+
+SET @er_yin_ddl = IF(
+ (SELECT COUNT(*) FROM `information_schema`.`columns`
+ WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
+ AND `column_name` = 'failure_stage') = 0,
+ 'ALTER TABLE `pjcloudx_hardware`.`hard_invoice` ADD COLUMN `failure_stage` varchar(32) NULL COMMENT ''BINDING、IMAGE_GENERATION、MQTT_DISPATCH或TERMINAL_PRINT'' AFTER `status`',
+ 'SELECT 1'
+);
+PREPARE er_yin_stmt FROM @er_yin_ddl;
+EXECUTE er_yin_stmt;
+DEALLOCATE PREPARE er_yin_stmt;
+
+SET @er_yin_ddl = IF(
+ (SELECT COUNT(*) FROM `information_schema`.`columns`
+ WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
+ AND `column_name` = 'last_error') = 0,
+ 'ALTER TABLE `pjcloudx_hardware`.`hard_invoice` ADD COLUMN `last_error` varchar(2000) NULL COMMENT ''最近失败摘要'' AFTER `failure_stage`',
+ 'SELECT 1'
+);
+PREPARE er_yin_stmt FROM @er_yin_ddl;
+EXECUTE er_yin_stmt;
+DEALLOCATE PREPARE er_yin_stmt;
+
+SET @er_yin_ddl = IF(
+ (SELECT COUNT(*) FROM `information_schema`.`columns`
+ WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
+ AND `column_name` = 'printed_at') = 0,
+ 'ALTER TABLE `pjcloudx_hardware`.`hard_invoice` ADD COLUMN `printed_at` datetime NULL COMMENT ''终端确认完整USB数据和切纸命令写入成功时间'' AFTER `last_error`',
+ 'SELECT 1'
+);
+PREPARE er_yin_stmt FROM @er_yin_ddl;
+EXECUTE er_yin_stmt;
+DEALLOCATE PREPARE er_yin_stmt;
+
+SET @er_yin_ddl = IF(
+ (SELECT COUNT(*) FROM `information_schema`.`columns`
+ WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
+ AND `column_name` = 'reprint_reason') = 0,
+ 'ALTER TABLE `pjcloudx_hardware`.`hard_invoice` ADD COLUMN `reprint_reason` varchar(500) NULL COMMENT ''人工重打或再次核销原因'' AFTER `printed_at`',
+ 'SELECT 1'
+);
+PREPARE er_yin_stmt FROM @er_yin_ddl;
+EXECUTE er_yin_stmt;
+DEALLOCATE PREPARE er_yin_stmt;
+
+-- 唯一索引尚未建立才回填历史发票,避免迁移完成后重跑时覆盖新产生的兑换券任务。
+SET @er_yin_ddl = IF(
+ (SELECT COUNT(*) FROM `information_schema`.`statistics`
+ WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
+ AND `index_name` = 'uk_hard_invoice_order_type_no') = 0,
+ 'UPDATE `pjcloudx_hardware`.`hard_invoice` invoice
+ JOIN (
+ SELECT ranked.`id`, ranked.`print_no`
+ FROM (
+ SELECT `id`, ROW_NUMBER() OVER (
+ PARTITION BY `tenant_id`, `order_id`
+ ORDER BY `create_time`, `id`
+ ) AS `print_no`
+ FROM `pjcloudx_hardware`.`hard_invoice`
+ ) ranked
+ ) ranked_rows ON ranked_rows.`id` = invoice.`id`
+ SET invoice.`print_type` = ''1'',
+ invoice.`print_no` = ranked_rows.`print_no`,
+ invoice.`retry_version` = 0,
+ invoice.`attempt_count` = 0',
+ 'SELECT 1'
+);
+PREPARE er_yin_stmt FROM @er_yin_ddl;
+EXECUTE er_yin_stmt;
+DEALLOCATE PREPARE er_yin_stmt;
+
+SET @er_yin_ddl = IF(
+ (SELECT COUNT(*) FROM `information_schema`.`statistics`
+ WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
+ AND `index_name` = 'uk_hard_invoice_order_type_no') = 0,
+ 'ALTER TABLE `pjcloudx_hardware`.`hard_invoice` ADD UNIQUE KEY `uk_hard_invoice_order_type_no` (`tenant_id`, `order_id`, `print_type`, `print_no`)',
+ 'SELECT 1'
+);
+PREPARE er_yin_stmt FROM @er_yin_ddl;
+EXECUTE er_yin_stmt;
+DEALLOCATE PREPARE er_yin_stmt;
+
+SET @er_yin_ddl = IF(
+ (SELECT COUNT(*) FROM `information_schema`.`statistics`
+ WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
+ AND `index_name` = 'idx_hard_invoice_printer_status') = 0,
+ 'ALTER TABLE `pjcloudx_hardware`.`hard_invoice` ADD KEY `idx_hard_invoice_printer_status` (`tenant_id`, `printer_hard_id`, `status`, `update_time`, `del_flag`)',
+ 'SELECT 1'
+);
+PREPARE er_yin_stmt FROM @er_yin_ddl;
+EXECUTE er_yin_stmt;
+DEALLOCATE PREPARE er_yin_stmt;
+
+SET @er_yin_ddl = IF(
+ (SELECT COUNT(*) FROM `information_schema`.`statistics`
+ WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_invoice'
+ AND `index_name` = 'idx_hard_invoice_source_status') = 0,
+ 'ALTER TABLE `pjcloudx_hardware`.`hard_invoice` ADD KEY `idx_hard_invoice_source_status` (`tenant_id`, `hard_id`, `status`, `del_flag`)',
+ 'SELECT 1'
+);
+PREPARE er_yin_stmt FROM @er_yin_ddl;
+EXECUTE er_yin_stmt;
+DEALLOCATE PREPARE er_yin_stmt;
+
+CREATE TABLE IF NOT EXISTS `pjcloudx_hardware`.`hard_printer_binding`
+(
+ `id` bigint(20) NOT NULL COMMENT '绑定关系主键',
+ `source_hard_id` bigint(20) NOT NULL COMMENT 'SUPERMARKET或CANTEEN来源业务终端id',
+ `printer_hard_id` bigint(20) NOT NULL COMMENT 'EY_PRINT目标打印终端id',
+ `merchant_id` bigint(20) NOT NULL COMMENT '双方共同所属商户id快照',
+ `dept_id` bigint(20) NOT NULL COMMENT '双方共同所属商户部门id快照',
+ `create_by` varchar(64) DEFAULT NULL COMMENT '创建人',
+ `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+ `update_by` varchar(64) DEFAULT NULL COMMENT '修改人',
+ `update_time` datetime DEFAULT NULL COMMENT '修改时间',
+ `tenant_id` bigint(20) NOT NULL COMMENT '租户ID',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_hard_printer_binding_source` (`tenant_id`, `source_hard_id`),
+ UNIQUE KEY `uk_hard_printer_binding_printer` (`tenant_id`, `printer_hard_id`),
+ KEY `idx_hard_printer_binding_merchant` (`tenant_id`, `merchant_id`)
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci
+ COMMENT = '业务终端与尔印打印终端一对一绑定';
+
+CREATE TABLE IF NOT EXISTS `pjcloudx_hardware`.`hard_printer_config`
+(
+ `id` bigint(20) NOT NULL COMMENT '配置主键',
+ `printer_hard_id` bigint(20) NOT NULL COMMENT 'EY_PRINT打印终端id',
+ `printer_model` varchar(128) DEFAULT NULL COMMENT '打印机型号',
+ `usb_vendor_id` int(11) DEFAULT NULL COMMENT 'USB Vendor ID十进制值',
+ `usb_product_id` int(11) DEFAULT NULL COMMENT 'USB Product ID十进制值',
+ `paper_width` int(11) NOT NULL DEFAULT 80 COMMENT '纸宽,单位mm,首期固定80',
+ `auto_cut` char(1) NOT NULL DEFAULT '1' COMMENT '自动切刀开关,1开启、0关闭',
+ `image_mode` varchar(16) NOT NULL DEFAULT 'GS_V0' COMMENT '图片指令模式,GS_V0或ESC_STAR',
+ `create_by` varchar(64) DEFAULT NULL COMMENT '创建人',
+ `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+ `update_by` varchar(64) DEFAULT NULL COMMENT '修改人',
+ `update_time` datetime DEFAULT NULL COMMENT '修改时间',
+ `tenant_id` bigint(20) NOT NULL COMMENT '租户ID',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_hard_printer_config_printer` (`tenant_id`, `printer_hard_id`)
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci
+ COMMENT = '尔印打印终端USB与ESC/POS配置';
+
+-- 字典和权限主键使用迁移日期种子或现有最大ID后的连续号,与项目其他迁移保持一致。
+SET @er_yin_dict_item_id_base = (
+ SELECT GREATEST(COALESCE(MAX(`id`), 0), 2026090400000000000)
+ FROM `pjcloudx`.`sys_dict_item`
+);
+
+-- 在每个已经配置hard_type的租户中加入尔印打印终端。
+INSERT INTO `pjcloudx`.`sys_dict_item`
+(`id`, `dict_id`, `item_value`, `label`, `dict_type`, `description`, `sort_order`, `create_by`, `create_time`, `remarks`, `del_flag`, `tenant_id`)
+SELECT @er_yin_dict_item_id_base + ROW_NUMBER() OVER (ORDER BY d.`tenant_id`, d.`id`),
+ d.`id`, '12', '尔印打印机器', 'hard_type', '独立USB打印终端', 12,
+ 'system', NOW(), '尔印80mm ESC/POS打印终端', '0', d.`tenant_id`
+FROM `pjcloudx`.`sys_dict` d
+WHERE d.`dict_type` = 'hard_type'
+ AND d.`del_flag` = '0'
+ AND NOT EXISTS (
+ SELECT 1 FROM `pjcloudx`.`sys_dict_item` i
+ WHERE i.`tenant_id` = d.`tenant_id` AND i.`dict_type` = 'hard_type'
+ AND i.`item_value` = '12' AND i.`del_flag` = '0'
+ );
+
+-- 打印类型和状态字典按租户幂等创建,供管理端筛选与状态展示。
+SET @er_yin_dict_id_base = (
+ SELECT GREATEST(COALESCE(MAX(`id`), 0), 2026090400000000000)
+ FROM `pjcloudx`.`sys_dict`
+);
+
+INSERT INTO `pjcloudx`.`sys_dict`
+(`id`, `dict_type`, `description`, `create_by`, `create_time`, `remarks`, `system_flag`, `del_flag`, `tenant_id`)
+SELECT @er_yin_dict_id_base + ROW_NUMBER() OVER (ORDER BY tenant.`tenant_id`, t.`dict_type`),
+ t.`dict_type`, t.`description`, 'system', NOW(), t.`description`, '1', '0', tenant.`tenant_id`
+FROM (SELECT DISTINCT `tenant_id` FROM `pjcloudx`.`sys_dict` WHERE `del_flag` = '0') tenant
+CROSS JOIN (
+ SELECT 'hard_print_type' AS `dict_type`, '打印凭证类型' AS `description`
+ UNION ALL SELECT 'hard_print_status', '打印任务状态'
+) t
+WHERE NOT EXISTS (
+ SELECT 1 FROM `pjcloudx`.`sys_dict` d
+ WHERE d.`tenant_id` = tenant.`tenant_id` AND d.`dict_type` = t.`dict_type` AND d.`del_flag` = '0'
+ );
+
+SET @er_yin_dict_item_id_base = (
+ SELECT GREATEST(COALESCE(MAX(`id`), 0), 2026090400000000000)
+ FROM `pjcloudx`.`sys_dict_item`
+);
+
+INSERT INTO `pjcloudx`.`sys_dict_item`
+(`id`, `dict_id`, `item_value`, `label`, `dict_type`, `description`, `sort_order`, `create_by`, `create_time`, `del_flag`, `tenant_id`)
+SELECT @er_yin_dict_item_id_base + ROW_NUMBER() OVER (
+ ORDER BY d.`tenant_id`, d.`dict_type`, item.`sort_order`, item.`item_value`
+ ),
+ d.`id`, item.`item_value`, item.`label`, d.`dict_type`, item.`label`, item.`sort_order`,
+ 'system', NOW(), '0', d.`tenant_id`
+FROM `pjcloudx`.`sys_dict` d
+JOIN (
+ SELECT 'hard_print_type' AS `dict_type`, '1' AS `item_value`, '发票' AS `label`, 1 AS `sort_order`
+ UNION ALL SELECT 'hard_print_type', '2', '兑换券', 2
+ UNION ALL SELECT 'hard_print_status', '0', '待生成', 0
+ UNION ALL SELECT 'hard_print_status', '1', '待打印', 1
+ UNION ALL SELECT 'hard_print_status', '2', '已打印', 2
+ UNION ALL SELECT 'hard_print_status', '3', '打印失败', 3
+ UNION ALL SELECT 'hard_print_status', '4', '已取消', 4
+) item ON item.`dict_type` = d.`dict_type`
+WHERE d.`del_flag` = '0'
+ AND NOT EXISTS (
+ SELECT 1 FROM `pjcloudx`.`sys_dict_item` i
+ WHERE i.`tenant_id` = d.`tenant_id` AND i.`dict_type` = d.`dict_type`
+ AND i.`item_value` = item.`item_value` AND i.`del_flag` = '0'
+ );
+
+-- 在已有设备管理与打印记录菜单下补充按钮权限,不假定各环境菜单主键。
+SET @er_yin_menu_id_base = (
+ SELECT GREATEST(COALESCE(MAX(`menu_id`), 0), 2026090400000000000)
+ FROM `pjcloudx`.`sys_menu`
+);
+
+INSERT INTO `pjcloudx`.`sys_menu`
+(`menu_id`, `name`, `permission`, `parent_id`, `visible`, `sort_order`, `keep_alive`, `embedded`, `menu_type`, `create_by`, `create_time`, `del_flag`, `tenant_id`)
+SELECT @er_yin_menu_id_base + ROW_NUMBER() OVER (ORDER BY parent_menu.`tenant_id`, parent_menu.`menu_id`),
+ '绑定打印机', 'hard_hardManager_printer_bind', parent_menu.`menu_id`,
+ '1', 20, '0', '0', '2', 'system', NOW(), '0', parent_menu.`tenant_id`
+FROM `pjcloudx`.`sys_menu` parent_menu
+WHERE parent_menu.`permission` = 'hard_hardManager_view' AND parent_menu.`del_flag` = '0'
+ AND NOT EXISTS (
+ SELECT 1 FROM `pjcloudx`.`sys_menu` existing
+ WHERE existing.`tenant_id` = parent_menu.`tenant_id`
+ AND existing.`permission` = 'hard_hardManager_printer_bind' AND existing.`del_flag` = '0'
+ );
+
+SET @er_yin_menu_id_base = (
+ SELECT GREATEST(COALESCE(MAX(`menu_id`), 0), 2026090400000000000)
+ FROM `pjcloudx`.`sys_menu`
+);
+
+INSERT INTO `pjcloudx`.`sys_menu`
+(`menu_id`, `name`, `permission`, `parent_id`, `visible`, `sort_order`, `keep_alive`, `embedded`, `menu_type`, `create_by`, `create_time`, `del_flag`, `tenant_id`)
+SELECT @er_yin_menu_id_base + ROW_NUMBER() OVER (ORDER BY parent_menu.`tenant_id`, parent_menu.`menu_id`),
+ '重打兑换券', 'hard_hardInvoice_voucher_reprint', parent_menu.`menu_id`,
+ '1', 20, '0', '0', '2', 'system', NOW(), '0', parent_menu.`tenant_id`
+FROM `pjcloudx`.`sys_menu` parent_menu
+WHERE parent_menu.`permission` = 'hard_hardInvoice_view' AND parent_menu.`del_flag` = '0'
+ AND NOT EXISTS (
+ SELECT 1 FROM `pjcloudx`.`sys_menu` existing
+ WHERE existing.`tenant_id` = parent_menu.`tenant_id`
+ AND existing.`permission` = 'hard_hardInvoice_voucher_reprint' AND existing.`del_flag` = '0'
+ );
+
+SET @er_yin_dict_id_base = NULL;
+SET @er_yin_dict_item_id_base = NULL;
+SET @er_yin_menu_id_base = NULL;
+SET @er_yin_ddl = NULL;
diff --git a/db/800pjcloudx_hardware.sql b/db/800pjcloudx_hardware.sql
index 173d7d84..e5934cc3 100644
--- a/db/800pjcloudx_hardware.sql
+++ b/db/800pjcloudx_hardware.sql
@@ -167,22 +167,83 @@ CREATE TABLE hard_group
DROP TABLE IF EXISTS `hard_invoice`;
CREATE TABLE hard_invoice
(
- id bigint NOT NULL COMMENT '编号',
- dept_id bigint NOT NULL COMMENT '部门id',
- hard_id bigint NOT NULL COMMENT '设备id',
- order_id bigint NOT NULL COMMENT '硬件订单id',
- status char(10) NOT NULL COMMENT '状态',
- pic varchar(256) NOT NULL COMMENT '发票图片',
- create_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
- create_time datetime DEFAULT NULL COMMENT '创建时间',
- update_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '修改人',
- update_time datetime DEFAULT NULL COMMENT '修改时间',
- del_flag char(1) COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '删除标记',
- tenant_id bigint DEFAULT NULL COMMENT '租户ID',
- PRIMARY KEY (id)
+ id bigint NOT NULL COMMENT '编号',
+ dept_id bigint NOT NULL COMMENT '部门id',
+ hard_id bigint DEFAULT NULL COMMENT '来源业务或核销终端id',
+ printer_hard_id bigint DEFAULT NULL COMMENT '目标尔印打印终端id',
+ order_id bigint NOT NULL COMMENT '硬件订单id',
+ print_type char(2) NOT NULL DEFAULT '1' COMMENT '打印类型,1发票、2兑换券',
+ print_no int NOT NULL DEFAULT 1 COMMENT '同订单同类型打印序号',
+ retry_version int NOT NULL DEFAULT 0 COMMENT '人工失败重试版本',
+ attempt_count int NOT NULL DEFAULT 0 COMMENT '终端USB累计尝试次数',
+ status char(10) NOT NULL DEFAULT '0' COMMENT '状态,0待生成、1待打印、2已打印、3打印失败、4已取消',
+ failure_stage varchar(32) DEFAULT NULL COMMENT '失败阶段',
+ last_error varchar(2000) DEFAULT NULL COMMENT '最近失败摘要',
+ printed_at datetime DEFAULT NULL COMMENT '终端确认写入成功时间',
+ reprint_reason varchar(500) DEFAULT NULL COMMENT '人工重打或再次核销原因',
+ pic varchar(512) DEFAULT NULL COMMENT '后端生成的打印PNG地址',
+ create_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
+ create_time datetime DEFAULT NULL COMMENT '创建时间',
+ update_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '修改人',
+ update_time datetime DEFAULT NULL COMMENT '修改时间',
+ del_flag char(1) COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '删除标记',
+ tenant_id bigint DEFAULT NULL COMMENT '租户ID',
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_hard_invoice_order_type_no (tenant_id, order_id, print_type, print_no),
+ KEY idx_hard_invoice_printer_status (tenant_id, printer_hard_id, status, update_time, del_flag),
+ KEY idx_hard_invoice_source_status (tenant_id, hard_id, status, del_flag)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
- COLLATE = utf8mb4_general_ci COMMENT ='发票记录';
+ COLLATE = utf8mb4_general_ci COMMENT ='打印任务与凭证记录';
+
+-- ----------------------------
+-- Table structure for hard_printer_binding
+-- ----------------------------
+DROP TABLE IF EXISTS `hard_printer_binding`;
+CREATE TABLE hard_printer_binding
+(
+ id bigint NOT NULL COMMENT '绑定关系主键',
+ source_hard_id bigint NOT NULL COMMENT 'SUPERMARKET或CANTEEN来源业务终端id',
+ printer_hard_id bigint NOT NULL COMMENT 'EY_PRINT目标打印终端id',
+ merchant_id bigint NOT NULL COMMENT '双方共同所属商户id快照',
+ dept_id bigint NOT NULL COMMENT '双方共同所属商户部门id快照',
+ create_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
+ create_time datetime DEFAULT NULL COMMENT '创建时间',
+ update_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '修改人',
+ update_time datetime DEFAULT NULL COMMENT '修改时间',
+ tenant_id bigint NOT NULL COMMENT '租户ID',
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_hard_printer_binding_source (tenant_id, source_hard_id),
+ UNIQUE KEY uk_hard_printer_binding_printer (tenant_id, printer_hard_id),
+ KEY idx_hard_printer_binding_merchant (tenant_id, merchant_id)
+) ENGINE = InnoDB
+ DEFAULT CHARSET = utf8mb4
+ COLLATE = utf8mb4_general_ci COMMENT ='业务终端与尔印打印终端一对一绑定';
+
+-- ----------------------------
+-- Table structure for hard_printer_config
+-- ----------------------------
+DROP TABLE IF EXISTS `hard_printer_config`;
+CREATE TABLE hard_printer_config
+(
+ id bigint NOT NULL COMMENT '配置主键',
+ printer_hard_id bigint NOT NULL COMMENT 'EY_PRINT打印终端id',
+ printer_model varchar(128) DEFAULT NULL COMMENT '打印机型号',
+ usb_vendor_id int DEFAULT NULL COMMENT 'USB Vendor ID十进制值',
+ usb_product_id int DEFAULT NULL COMMENT 'USB Product ID十进制值',
+ paper_width int NOT NULL DEFAULT 80 COMMENT '纸宽,单位mm,首期固定80',
+ auto_cut char(1) NOT NULL DEFAULT '1' COMMENT '自动切刀开关,1开启、0关闭',
+ image_mode varchar(16) NOT NULL DEFAULT 'GS_V0' COMMENT '图片指令模式,GS_V0或ESC_STAR',
+ create_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
+ create_time datetime DEFAULT NULL COMMENT '创建时间',
+ update_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '修改人',
+ update_time datetime DEFAULT NULL COMMENT '修改时间',
+ tenant_id bigint NOT NULL COMMENT '租户ID',
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_hard_printer_config_printer (tenant_id, printer_hard_id)
+) ENGINE = InnoDB
+ DEFAULT CHARSET = utf8mb4
+ COLLATE = utf8mb4_general_ci COMMENT ='尔印打印终端USB与ESC/POS配置';
-- ----------------------------
-- Table structure for hard_car
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardPrintFailureStageEnum.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardPrintFailureStageEnum.java
new file mode 100644
index 00000000..b59481ae
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardPrintFailureStageEnum.java
@@ -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);
+ }
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardPrintStatusEnum.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardPrintStatusEnum.java
index 9a0eb75d..2e67b1aa 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardPrintStatusEnum.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardPrintStatusEnum.java
@@ -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;
/**
- * 打印状态
+ * 打印任务状态。
+ *
+ *
编码 {@code 1} 和 {@code 2} 分别兼容历史“已生成”和“已打印”记录。
*/
@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);
}
}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardPrintTypeEnum.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardPrintTypeEnum.java
new file mode 100644
index 00000000..bea9223d
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardPrintTypeEnum.java
@@ -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);
+ }
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardTypeEnum.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardTypeEnum.java
index 39fcf538..f668557f 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardTypeEnum.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardTypeEnum.java
@@ -26,6 +26,8 @@ public enum HardTypeEnum {
YUN_XFJ("9", "云版消费机"),
YYX_4G("10", "4G-云音响"),
YYX_WIFI("11", "wifi-云音响"),
+ /** 运行打印专用终端模式、通过 USB 驱动尔印 ESC/POS 打印机的设备。 */
+ EY_PRINT("12", "尔印打印机器"),
;
private final String type;
private final String description;
@@ -45,4 +47,15 @@ public enum HardTypeEnum {
return Arrays.stream(values()).filter(v -> ObjectUtil.equals(type, v.getType())).findFirst().get();
}
+ /**
+ * 判断指定设备类型是否为独立打印终端。
+ *
+ * @param hardTypeEnum 待判断的设备类型
+ * @return 类型为 {@link #EY_PRINT} 时返回 {@code true}
+ */
+ public static boolean isPrintHard(HardTypeEnum hardTypeEnum) {
+ if (ObjectUtil.isEmpty(hardTypeEnum)) return false;
+ return List.of(EY_PRINT).contains(hardTypeEnum);
+ }
+
}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrintRequestDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrintRequestDto.java
new file mode 100644
index 00000000..556b9425
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrintRequestDto.java
@@ -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;
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrintResultDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrintResultDto.java
new file mode 100644
index 00000000..d3d42b54
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrintResultDto.java
@@ -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;
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrintRetryDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrintRetryDto.java
new file mode 100644
index 00000000..ca940e5c
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrintRetryDto.java
@@ -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。
+ *
+ * 仅当原任务因缺少来源终端而失败时使用;服务端仍会校验该终端与订单同商户,
+ * 并根据服务端绑定关系确定目标 EY_PRINT,客户端不能直接指定打印机。
+ */
+ @Schema(description = "补录的来源业务终端ID")
+ private Long sourceHardId;
+
+ /** 已打印任务重打时必须填写的原因;失败任务原任务重试时可为空。 */
+ @Schema(description = "重打原因")
+ private String reprintReason;
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrinterBindingDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrinterBindingDto.java
new file mode 100644
index 00000000..75632964
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrinterBindingDto.java
@@ -0,0 +1,23 @@
+package com.bsd.pjcloud.hard.api.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * 创建业务终端与尔印打印终端绑定关系的请求。
+ */
+@Data
+@Schema(description = "打印终端绑定请求")
+public class HardPrinterBindingDto {
+
+ /** SUPERMARKET 或 CANTEEN 来源终端 ID。 */
+ @NotNull(message = "来源终端不能为空")
+ @Schema(description = "来源业务终端ID")
+ private Long sourceHardId;
+
+ /** EY_PRINT 目标终端 ID。 */
+ @NotNull(message = "打印终端不能为空")
+ @Schema(description = "目标打印终端ID")
+ private Long printerHardId;
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrinterConfigDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrinterConfigDto.java
new file mode 100644
index 00000000..28d56d19
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrinterConfigDto.java
@@ -0,0 +1,46 @@
+package com.bsd.pjcloud.hard.api.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * 保存尔印打印终端 USB 与 ESC/POS 参数的请求。
+ */
+@Data
+@Schema(description = "打印终端配置请求")
+public class HardPrinterConfigDto {
+
+ /** EY_PRINT 设备 ID。 */
+ @NotNull(message = "打印终端不能为空")
+ @Schema(description = "打印终端ID")
+ private Long printerHardId;
+
+ /** 真机型号;未联调前允许为空。 */
+ @Schema(description = "打印机型号")
+ private String printerModel;
+
+ /** USB Vendor ID 十进制值;未联调前允许为空。 */
+ @Schema(description = "USB VID")
+ private Integer usbVendorId;
+
+ /** USB Product ID 十进制值;未联调前允许为空。 */
+ @Schema(description = "USB PID")
+ private Integer usbProductId;
+
+ /** 纸张宽度,首期只允许 80。 */
+ @NotNull(message = "纸宽不能为空")
+ @Schema(description = "纸宽,单位mm")
+ private Integer paperWidth;
+
+ /** 自动切刀开关,1 开启、0 关闭。 */
+ @NotBlank(message = "自动切刀开关不能为空")
+ @Schema(description = "自动切刀开关")
+ private String autoCut;
+
+ /** 图片指令模式,只允许 GS_V0 或 ESC_STAR。 */
+ @NotBlank(message = "图片指令模式不能为空")
+ @Schema(description = "图片指令模式")
+ private String imageMode;
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrinterInfoReportDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrinterInfoReportDto.java
new file mode 100644
index 00000000..6c50bdcf
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrinterInfoReportDto.java
@@ -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;
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardAfterSaleDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardAfterSaleDto.java
index e113b130..dd7e7063 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardAfterSaleDto.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardAfterSaleDto.java
@@ -17,6 +17,9 @@ public class HardAfterSaleDto extends HardAfterSaleEntity implements BaseUserIde
/** 售后申请用户主键集合;为空时不限制申请人。 */
private List userIds;
+ /** 关联原订单的用户主键集合;用于区分代订场景中的退款发起人和实际订单用户。 */
+ private List orderUserIds;
+
/** UPMS 用户身份协议编码集合;为空时不限制身份。 */
private List identityTypes;
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/HardInvoiceEntity.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/HardInvoiceEntity.java
index b7e4b2c9..b94a5b6c 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/HardInvoiceEntity.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/HardInvoiceEntity.java
@@ -37,28 +37,82 @@ public class HardInvoiceEntity extends Model {
@Schema(description="部门id")
private Long deptId;
- /**
- * 设备id
- */
- @Schema(description="设备id")
+ /**
+ * 来源业务终端 ID;历史记录继续保留原设备语义,不改写为打印机 ID。
+ */
+ @Schema(description="来源业务终端ID")
private Long hardId;
+ /**
+ * 实际接收任务的尔印打印终端 ID;历史记录和绑定失败任务允许为空。
+ */
+ @Schema(description="目标打印终端ID")
+ private Long printerHardId;
+
/**
* 硬件订单id
*/
@Schema(description="硬件订单id")
private Long orderId;
- /**
- * 状态
- */
- @Schema(description="状态")
+ /**
+ * 打印任务状态,编码来自 HardPrintStatusEnum。
+ */
+ @Schema(description="打印任务状态")
private String status;
- /**
- * 发票图片
- */
- @Schema(description="发票图片")
+ /**
+ * 打印凭证类型,1 发票、2 兑换券;历史空值按发票兼容。
+ */
+ @Schema(description="打印凭证类型")
+ private String printType;
+
+ /**
+ * 同一订单同一凭证类型的打印序号;重试同一任务时保持不变。
+ */
+ @Schema(description="打印序号")
+ private Integer printNo;
+
+ /**
+ * 人工重试版本;终端用任务 ID 和该版本共同判断是否允许再次打印。
+ */
+ @Schema(description="重试版本")
+ private Integer retryVersion;
+
+ /**
+ * 打印终端上报的 USB 累计尝试次数。
+ */
+ @Schema(description="终端尝试次数")
+ private Integer attemptCount;
+
+ /**
+ * 最近失败阶段,编码来自 HardPrintFailureStageEnum。
+ */
+ @Schema(description="失败阶段")
+ private String failureStage;
+
+ /**
+ * 最近失败摘要,不保存 MQTT 密码、访问令牌等敏感信息。
+ */
+ @Schema(description="最近失败原因")
+ private String lastError;
+
+ /**
+ * 打印终端确认完整 USB 数据和切纸命令写入成功的时间。
+ */
+ @Schema(description="打印成功时间")
+ private LocalDateTime printedAt;
+
+ /**
+ * 人工重打原因,自动首次打印或失败重试允许为空。
+ */
+ @Schema(description="重打原因")
+ private String reprintReason;
+
+ /**
+ * 后端生成并上传的打印 PNG 地址;待生成和生成失败时允许为空。
+ */
+ @Schema(description="打印图片")
private String pic;
/**
@@ -102,4 +156,4 @@ public class HardInvoiceEntity extends Model {
*/
@Schema(description="租户ID")
private Long tenantId;
-}
\ No newline at end of file
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/HardPrinterBindingEntity.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/HardPrinterBindingEntity.java
new file mode 100644
index 00000000..ec456307
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/HardPrinterBindingEntity.java
@@ -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;
+
+/**
+ * 业务终端与尔印打印终端的一对一绑定关系。
+ *
+ * 记录使用物理删除,数据库分别对来源和目标设备设置唯一键,确保任何时刻都不能出现多重绑定。
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@TenantTable
+@TableName("hard_printer_binding")
+@EqualsAndHashCode(callSuper = true)
+@Schema(description = "业务终端与打印终端绑定关系")
+public class HardPrinterBindingEntity extends Model {
+
+ /** 绑定关系主键。 */
+ @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;
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/HardPrinterConfigEntity.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/HardPrinterConfigEntity.java
new file mode 100644
index 00000000..b053e41d
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/HardPrinterConfigEntity.java
@@ -0,0 +1,88 @@
+package com.bsd.pjcloud.hard.api.entity;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import com.bsd.pjcloud.common.core.util.TenantTable;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+
+/**
+ * 尔印打印终端的 USB 和 ESC/POS 参数配置。
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@TenantTable
+@TableName("hard_printer_config")
+@EqualsAndHashCode(callSuper = true)
+@Schema(description = "尔印打印终端配置")
+public class HardPrinterConfigEntity extends Model {
+
+ /** 配置主键。 */
+ @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;
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/utils/ThermalReceiptImage.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/utils/ThermalReceiptImage.java
new file mode 100644
index 00000000..1cd63990
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/utils/ThermalReceiptImage.java
@@ -0,0 +1,333 @@
+package com.bsd.pjcloud.hard.api.utils;
+
+import javax.imageio.ImageIO;
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.FontMetrics;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 面向 80mm、576px ESC/POS 打印的动态高度 PNG 绘制器。
+ *
+ * 商品名称按真实像素宽度换行,数量列固定右对齐,保证长名称不会与数量错配。
+ */
+public class ThermalReceiptImage {
+
+ /** 80mm 打印头常见有效像素宽度。 */
+ public static final int PAPER_WIDTH_PIXELS = 576;
+
+ /** 图片左右安全留白。 */
+ private static final int HORIZONTAL_PADDING = 24;
+
+ /** 图片顶部和底部安全留白。 */
+ private static final int VERTICAL_PADDING = 24;
+
+ /** 每次扩展画布的最小高度。 */
+ private static final int HEIGHT_GROWTH = 512;
+
+ /** 数量列保留宽度。 */
+ private static final int QUANTITY_COLUMN_WIDTH = 96;
+
+ /** 套餐组成项左侧缩进。 */
+ private static final int CHILD_INDENT = 32;
+
+ /** 中文优先逻辑字体名称。 */
+ private static final String FONT_FAMILY = "SansSerif";
+
+ /** 当前可扩展画布。 */
+ private BufferedImage canvas;
+
+ /** 当前绘制上下文。 */
+ private Graphics2D graphics;
+
+ /** 下一块内容的顶部 Y 坐标。 */
+ private int cursorY = VERTICAL_PADDING;
+
+ /**
+ * 创建 576px 宽的白底收据画布。
+ */
+ public ThermalReceiptImage() {
+ canvas = new BufferedImage(PAPER_WIDTH_PIXELS, HEIGHT_GROWTH, BufferedImage.TYPE_INT_RGB);
+ graphics = canvas.createGraphics();
+ configureGraphics(graphics);
+ graphics.setColor(Color.WHITE);
+ graphics.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
+ graphics.setColor(Color.BLACK);
+ }
+
+ /**
+ * 绘制居中粗体标题。
+ *
+ * @param text 标题文本
+ * @return 当前绘制器
+ */
+ public ThermalReceiptImage title(String text) {
+ setFont(Font.BOLD, 34);
+ drawCentered(text, 16);
+ return this;
+ }
+
+ /**
+ * 绘制带黑色边框的大号打印次数提示。
+ *
+ * @param text 提示文本,例如“第 1 次打印”
+ * @return 当前绘制器
+ */
+ public ThermalReceiptImage banner(String text) {
+ setFont(Font.BOLD, 38);
+ FontMetrics metrics = graphics.getFontMetrics();
+ int height = metrics.getHeight() + 30;
+ ensureCapacity(height + 20);
+ graphics.setStroke(new BasicStroke(3F));
+ graphics.drawRect(HORIZONTAL_PADDING, cursorY, contentWidth(), height);
+ int x = HORIZONTAL_PADDING + Math.max(0, (contentWidth() - metrics.stringWidth(text)) / 2);
+ graphics.drawString(text, x, cursorY + 15 + metrics.getAscent());
+ cursorY += height + 20;
+ return this;
+ }
+
+ /**
+ * 绘制醒目的核销短码。
+ *
+ * @param shortCode 核销短码
+ * @return 当前绘制器
+ */
+ public ThermalReceiptImage verificationCode(String shortCode) {
+ setFont(Font.BOLD, 52);
+ drawCentered(shortCode == null || shortCode.isBlank() ? "--" : shortCode, 18);
+ return this;
+ }
+
+ /**
+ * 绘制一行左右键值信息,值过长时在下一行继续换行。
+ *
+ * @param label 左侧字段名
+ * @param value 右侧字段值
+ * @return 当前绘制器
+ */
+ public ThermalReceiptImage keyValue(String label, String value) {
+ setFont(Font.PLAIN, 25);
+ String safeLabel = label == null ? "" : label;
+ String safeValue = value == null || value.isBlank() ? "--" : value;
+ FontMetrics metrics = graphics.getFontMetrics();
+ int labelWidth = metrics.stringWidth(safeLabel + ":");
+ int valueWidth = contentWidth() - labelWidth - 12;
+ List lines = wrap(safeValue, Math.max(120, valueWidth), metrics);
+ int lineHeight = metrics.getHeight() + 6;
+ ensureCapacity(lines.size() * lineHeight + 10);
+ graphics.drawString(safeLabel + ":", HORIZONTAL_PADDING, cursorY + metrics.getAscent());
+ for (int index = 0; index < lines.size(); index++) {
+ int x = index == 0 ? HORIZONTAL_PADDING + labelWidth + 12 : HORIZONTAL_PADDING + labelWidth + 12;
+ graphics.drawString(lines.get(index), x, cursorY + index * lineHeight + metrics.getAscent());
+ }
+ cursorY += lines.size() * lineHeight + 6;
+ return this;
+ }
+
+ /**
+ * 绘制商品明细区标题。
+ *
+ * @param text 标题文本
+ * @return 当前绘制器
+ */
+ public ThermalReceiptImage sectionTitle(String text) {
+ setFont(Font.BOLD, 28);
+ ensureCapacity(48);
+ graphics.drawString(text == null ? "" : text, HORIZONTAL_PADDING, cursorY + graphics.getFontMetrics().getAscent());
+ cursorY += graphics.getFontMetrics().getHeight() + 10;
+ return this;
+ }
+
+ /**
+ * 绘制商品或套餐组成项,商品名换行且数量始终在首行右对齐。
+ *
+ * @param name 商品名称
+ * @param quantity 数量文本
+ * @param child 是否为套餐组成项
+ * @param emphasized 是否使用粗体标记套餐主项
+ * @return 当前绘制器
+ */
+ public ThermalReceiptImage item(String name, String quantity, boolean child, boolean emphasized) {
+ return item(name, quantity, child, emphasized, QUANTITY_COLUMN_WIDTH);
+ }
+
+ /**
+ * 绘制带数量和金额摘要的发票商品行。
+ *
+ * @param name 商品名称
+ * @param quantity 数量或重量文本
+ * @param amount 含货币符号的商品总价
+ * @return 当前绘制器
+ */
+ public ThermalReceiptImage invoiceItem(String name, String quantity, String amount) {
+ return item(name, String.format("%s %s", quantity, amount), false, false, 190);
+ }
+
+ /**
+ * 使用指定尾列宽度绘制一条可换行明细。
+ *
+ * @param name 左侧名称
+ * @param trailing 首行右对齐摘要
+ * @param child 是否为套餐组成项
+ * @param emphasized 是否使用粗体
+ * @param trailingWidth 尾列预留像素宽度
+ * @return 当前绘制器
+ */
+ private ThermalReceiptImage item(String name, String trailing, boolean child, boolean emphasized,
+ int trailingWidth) {
+ setFont(emphasized ? Font.BOLD : Font.PLAIN, child ? 23 : 26);
+ FontMetrics metrics = graphics.getFontMetrics();
+ int indent = child ? CHILD_INDENT : 0;
+ String prefix = child ? "- " : "";
+ String safeName = prefix + (name == null || name.isBlank() ? "未命名商品" : name);
+ List 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 wrap(String text, int maxWidth, FontMetrics metrics) {
+ List 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;
+ }
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/HardInvoiceVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/HardInvoiceVo.java
index 77a0a1d1..31f3f961 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/HardInvoiceVo.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/HardInvoiceVo.java
@@ -67,6 +67,9 @@ public class HardInvoiceVo extends HardInvoiceEntity implements BaseUserDeptInte
private String hardType;
+ /** 目标尔印打印终端名称。 */
+ private String printerHardName;
+
/**
* 用户相关
*/
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/HardPrinterBindingVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/HardPrinterBindingVo.java
new file mode 100644
index 00000000..196e35ea
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/HardPrinterBindingVo.java
@@ -0,0 +1,32 @@
+package com.bsd.pjcloud.hard.api.vo;
+
+import com.bsd.pjcloud.hard.api.entity.HardPrinterBindingEntity;
+import com.bsd.pjcloud.hard.api.entity.HardPrinterConfigEntity;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 打印终端绑定关系及两端设备展示信息。
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Schema(description = "打印终端绑定详情")
+public class HardPrinterBindingVo extends HardPrinterBindingEntity {
+
+ /** 来源业务终端名称。 */
+ @Schema(description = "来源业务终端名称")
+ private String sourceHardName;
+
+ /** 来源业务终端类型。 */
+ @Schema(description = "来源业务终端类型")
+ private String sourceHardType;
+
+ /** 目标打印终端名称。 */
+ @Schema(description = "目标打印终端名称")
+ private String printerHardName;
+
+ /** 目标打印终端当前配置;尚未保存时返回默认配置。 */
+ @Schema(description = "打印终端配置")
+ private HardPrinterConfigEntity printerConfig;
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/manager/HardManagerVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/manager/HardManagerVo.java
index cf8c83e3..e48a8d26 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/manager/HardManagerVo.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/manager/HardManagerVo.java
@@ -59,4 +59,34 @@ public class HardManagerVo extends HardManagerEntity implements BaseDeptInterfac
*/
private String disconnectReason;
+ /** 当前来源业务终端所绑定的 EY_PRINT 设备 ID;未绑定或当前行为打印机时为空。 */
+ private Long boundPrinterHardId;
+
+ /** 当前来源业务终端所绑定的 EY_PRINT 设备名称。 */
+ private String boundPrinterHardName;
+
+ /** 当前 EY_PRINT 反向绑定的来源业务终端 ID;未绑定或当前行为来源设备时为空。 */
+ private Long boundSourceHardId;
+
+ /** 当前 EY_PRINT 反向绑定的来源业务终端名称。 */
+ private String boundSourceHardName;
+
+ /** EY_PRINT 型号;未维护配置或非打印设备时为空。 */
+ private String printerModel;
+
+ /** EY_PRINT 的 USB 厂商编号 VID;尚未真机回填时为空。 */
+ private Integer usbVendorId;
+
+ /** EY_PRINT 的 USB 产品编号 PID;尚未真机回填时为空。 */
+ private Integer usbProductId;
+
+ /** 热敏纸宽度,首期固定为 80mm。 */
+ private Integer paperWidth;
+
+ /** 自动切刀开关,1 开启、0 关闭。 */
+ private String autoCut;
+
+ /** 图片指令模式,首选 GS_V0,兼容 ESC_STAR。 */
+ private String imageMode;
+
}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderVo.java
index 1fe36132..407e340a 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderVo.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderVo.java
@@ -5,6 +5,7 @@ import com.bsd.pjcloud.admin.api.vo.base.BaseUserIdentityInterface;
import com.bsd.pjcloud.admin.api.dto.user.SysUserStudentDto;
import com.bsd.pjcloud.admin.api.dto.user.SysUserTeacherDto;
import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity;
+import com.bsd.pjcloud.hard.api.entity.order.HardTakeOutApptEntity;
import com.bsd.pjcloud.hard.api.vo.base.BaseHardInterface;
import com.bsd.pjcloud.wallet.api.vo.base.BaseMchInterface;
import com.bsd.pjcloud.wallet.api.vo.base.BaseWalletInterface;
@@ -105,6 +106,13 @@ public class HardOrderVo extends HardOrderEntity implements BaseUserDeptInterfac
private LocalTime apptEndTime;
+ /**
+ * 外卖订单的期望送达配置,来源于 hard_take_out_appt;非外卖或历史未记录时为空。
+ *
+ * 该字段只参与订单详情响应,不对应 hard_order 表持久化字段。
+ */
+ private HardTakeOutApptEntity takeOutAppt;
+
/**
* 付款人用户ID,来源于订单关联的父购物车。
*/
@@ -129,4 +137,28 @@ public class HardOrderVo extends HardOrderEntity implements BaseUserDeptInterfac
* 订单详情中的长期识别证据列表,按证据类型、角色和序号排序。
*/
private List evidenceImages;
+
+ /** 当前订单最新发票任务 ID;没有记录时为空。 */
+ private Long latestInvoiceTaskId;
+
+ /** 当前订单最新发票任务状态编码。 */
+ private String latestInvoiceStatus;
+
+ /** 当前订单最新发票打印序号。 */
+ private Integer latestInvoicePrintNo;
+
+ /** 当前订单最新发票任务的来源业务终端 ID。 */
+ private Long latestInvoiceSourceHardId;
+
+ /** 当前订单最新兑换券任务 ID;没有记录时为空。 */
+ private Long latestVoucherTaskId;
+
+ /** 当前订单最新兑换券任务状态编码。 */
+ private String latestVoucherStatus;
+
+ /** 当前订单最新兑换券打印序号。 */
+ private Integer latestVoucherPrintNo;
+
+ /** 当前订单最新兑换券任务的来源业务终端 ID。 */
+ private Long latestVoucherSourceHardId;
}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/HardInvoiceController.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/HardInvoiceController.java
index 8f48b633..71fbfaab 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/HardInvoiceController.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/HardInvoiceController.java
@@ -9,6 +9,8 @@ import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.idempotent.annotation.Idempotent;
import com.bsd.pjcloud.common.log.annotation.SysLog;
import com.bsd.pjcloud.hard.api.dto.HardInvoiceDto;
+import com.bsd.pjcloud.hard.api.dto.HardPrintRequestDto;
+import com.bsd.pjcloud.hard.api.dto.HardPrintRetryDto;
import com.bsd.pjcloud.hard.api.entity.HardInvoiceEntity;
import com.bsd.pjcloud.hard.service.HardInvoiceService;
import org.springframework.security.access.prepost.PreAuthorize;
@@ -22,6 +24,7 @@ import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
+import jakarta.validation.Valid;
/**
* 发票记录
@@ -48,6 +51,49 @@ public class HardInvoiceController {
return R.ok(hardInvoiceService.print(orderId));
}
+ /**
+ * 创建发票或补建兑换券打印任务;已打印兑换券必须使用带专用权限的重打接口。
+ *
+ * @param request 订单、打印类型和可选来源终端
+ * @return 创建、复用或失败重试后的打印任务
+ */
+ @Operation(summary = "创建打印任务", description = "创建发票或补建兑换券打印任务")
+ @SysLog("创建打印任务")
+ @Idempotent(expireTime = 5, key = "'/hard-invoice/print:' + #request.getOrderId() + ':' + #request.getPrintType()", info = "正在创建打印任务,请勿重复操作")
+ @PostMapping("/print")
+ @PreAuthorize("@pms.hasPermission('hard_hardInvoice_admin_print')")
+ public R> createPrintTask(@Valid @RequestBody HardPrintRequestDto request) {
+ return R.ok(hardInvoiceService.createPrintTask(request));
+ }
+
+ /**
+ * 重试原失败任务,重试版本增加但打印序号不变。
+ *
+ * @param request 失败任务 ID,以及原任务缺少来源时可补录的来源终端 ID
+ * @return 重试后的任务
+ */
+ @Operation(summary = "重试失败打印任务")
+ @SysLog("重试失败打印任务")
+ @PostMapping("/retry")
+ @PreAuthorize("@pms.hasPermission('hard_hardInvoice_admin_print')")
+ public R> retry(@Valid @RequestBody HardPrintRetryDto request) {
+ return R.ok(hardInvoiceService.retryTask(request.getHistoryId(), request.getSourceHardId()));
+ }
+
+ /**
+ * 对已打印凭证创建下一打印序号并记录人工原因。
+ *
+ * @param request 已打印任务 ID 和重打原因
+ * @return 新打印任务
+ */
+ @Operation(summary = "人工重打兑换券")
+ @SysLog("人工重打兑换券")
+ @PostMapping("/reprint")
+ @PreAuthorize("@pms.hasPermission('hard_hardInvoice_voucher_reprint')")
+ public R> reprint(@Valid @RequestBody HardPrintRetryDto request) {
+ return R.ok(hardInvoiceService.reprintTask(request.getHistoryId(), request.getReprintReason()));
+ }
+
@Operation(summary = "分页查询", description = "分页查询")
@GetMapping("/page")
@PreAuthorize("@pms.hasPermission('hard_hardInvoice_view')")
@@ -100,4 +146,4 @@ public class HardInvoiceController {
public List export(HardInvoiceEntity hardInvoice, Long[] ids) {
return hardInvoiceService.list(Wrappers.lambdaQuery(hardInvoice).in(ArrayUtil.isNotEmpty(ids), HardInvoiceEntity::getId, ids));
}
-}
\ No newline at end of file
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/HardPrinterController.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/HardPrinterController.java
new file mode 100644
index 00000000..6cd07888
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/HardPrinterController.java
@@ -0,0 +1,132 @@
+package com.bsd.pjcloud.hard.controller;
+
+import com.bsd.pjcloud.common.core.util.R;
+import com.bsd.pjcloud.common.log.annotation.SysLog;
+import com.bsd.pjcloud.hard.api.dto.HardPrinterBindingDto;
+import com.bsd.pjcloud.hard.api.dto.HardPrinterConfigDto;
+import com.bsd.pjcloud.hard.service.HardPrinterService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 尔印打印终端绑定和配置管理接口。
+ */
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/hard-printer")
+@Tag(description = "hard-printer", name = "尔印打印终端管理")
+@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
+public class HardPrinterController {
+
+ /** 打印终端绑定和配置服务。 */
+ private final HardPrinterService hardPrinterService;
+
+ /**
+ * 查询指定设备当前绑定详情。
+ *
+ * @param hardId 来源或打印设备 ID
+ * @return 绑定详情
+ */
+ @GetMapping("/binding/{hardId}")
+ @Operation(summary = "查询打印机绑定")
+ @PreAuthorize("@pms.hasPermission('hard_hardManager_view')")
+ public R> getBinding(@PathVariable Long hardId) {
+ return R.ok(hardPrinterService.getBinding(hardId));
+ }
+
+ /**
+ * 查询来源设备可绑定的 EY_PRINT 终端。
+ *
+ * @param sourceHardId 来源业务终端 ID
+ * @return 可绑定设备列表
+ */
+ @GetMapping("/available")
+ @Operation(summary = "查询可绑定打印终端")
+ @PreAuthorize("@pms.hasPermission('hard_hardManager_printer_bind')")
+ public R> listAvailable(@RequestParam Long sourceHardId) {
+ return R.ok(hardPrinterService.listAvailablePrinters(sourceHardId));
+ }
+
+ /**
+ * 查询商户下已经绑定打印机的来源终端。
+ *
+ * @param merchantId 商户 ID
+ * @return 来源终端列表
+ */
+ @GetMapping("/sources")
+ @Operation(summary = "查询已绑定打印机的业务终端")
+ @PreAuthorize("@pms.hasPermission('hard_hardInvoice_admin_print')")
+ public R> listBoundSources(@RequestParam Long merchantId) {
+ return R.ok(hardPrinterService.listBoundSources(merchantId));
+ }
+
+ /**
+ * 创建来源与打印终端的一对一绑定。
+ *
+ * @param dto 绑定请求
+ * @return 绑定详情
+ */
+ @PostMapping("/binding")
+ @Operation(summary = "绑定打印终端")
+ @SysLog("绑定打印终端")
+ @PreAuthorize("@pms.hasPermission('hard_hardManager_printer_bind')")
+ public R> bind(@Valid @RequestBody HardPrinterBindingDto dto) {
+ return R.ok(hardPrinterService.bind(dto));
+ }
+
+ /**
+ * 解除来源终端的打印机绑定。
+ *
+ * @param sourceHardId 来源业务终端 ID
+ * @return 空成功响应
+ */
+ @DeleteMapping("/binding/{sourceHardId}")
+ @Operation(summary = "解绑打印终端")
+ @SysLog("解绑打印终端")
+ @PreAuthorize("@pms.hasPermission('hard_hardManager_printer_bind')")
+ public R unbind(@PathVariable Long sourceHardId) {
+ hardPrinterService.unbind(sourceHardId);
+ return R.ok();
+ }
+
+ /**
+ * 查询打印终端当前参数。
+ *
+ * @param printerHardId EY_PRINT 设备 ID
+ * @return 当前或默认配置
+ */
+ @GetMapping("/config/{printerHardId}")
+ @Operation(summary = "查询打印终端配置")
+ @PreAuthorize("@pms.hasPermission('hard_hardManager_view')")
+ public R> getConfig(@PathVariable Long printerHardId) {
+ return R.ok(hardPrinterService.getConfig(printerHardId));
+ }
+
+ /**
+ * 保存打印终端参数。
+ *
+ * @param dto 配置请求
+ * @return 保存后的配置
+ */
+ @PutMapping("/config")
+ @Operation(summary = "保存打印终端配置")
+ @SysLog("保存打印终端配置")
+ @PreAuthorize("@pms.hasPermission('hard_hardManager_printer_bind')")
+ public R> saveConfig(@Valid @RequestBody HardPrinterConfigDto dto) {
+ return R.ok(hardPrinterService.saveConfig(dto));
+ }
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/hard/HardApiController.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/hard/HardApiController.java
index 7556bdaa..4cddf6e0 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/hard/HardApiController.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/hard/HardApiController.java
@@ -15,6 +15,9 @@ import com.bsd.pjcloud.hard.api.constant.HardTypeEnum;
import com.bsd.pjcloud.hard.api.constant.order.HardPayTypeEnum;
import com.bsd.pjcloud.hard.api.constant.order.HardOrderTypeEnum;
import com.bsd.pjcloud.hard.api.dto.async.SyncOfflineReqVo;
+import com.bsd.pjcloud.hard.api.dto.HardPrintRequestDto;
+import com.bsd.pjcloud.hard.api.dto.HardPrintResultDto;
+import com.bsd.pjcloud.hard.api.dto.HardPrinterInfoReportDto;
import com.bsd.pjcloud.hard.api.dto.hard.*;
import com.bsd.pjcloud.hard.api.dto.order.HardChartLatitudeVo;
import com.bsd.pjcloud.hard.api.dto.order.HardCreateCarDto;
@@ -55,6 +58,7 @@ public class HardApiController {
private final HardManagerService hardManagerService;
private final HardInvoiceService hardInvoiceService;
+ private final HardPrinterService hardPrinterService;
private final HardCarService hardCarService;
private final HardOrderService hardwareOrderService;
private final HardOrderSyncService hardOrderSyncService;
@@ -326,6 +330,73 @@ public class HardApiController {
return R.ok(hardInvoiceService.printInvoice(orderId, historyId));
}
+ /**
+ * 查询当前 EY_PRINT 终端绑定关系和打印机参数,用于登录后进入打印专用模式。
+ *
+ * @param hardId DEVICE-ID 请求头中的打印终端 ID
+ * @return 绑定来源和打印机配置
+ */
+ @GetMapping("/printer/bootstrap")
+ @Operation(summary = "获取打印终端启动配置")
+ public R> printerBootstrap(@RequestHeader("DEVICE-ID") Long hardId) {
+ return R.ok(hardPrinterService.getPrinterBinding(hardId));
+ }
+
+ /**
+ * 保存当前 EY_PRINT 实际连接打印机的型号、VID 和 PID,目标设备只能由请求头确定。
+ *
+ * @param hardId DEVICE-ID 请求头中的打印终端 ID
+ * @param dto Android USB Host 枚举出的打印机信息
+ * @return 保留纸宽、切刀和图片模式后的完整打印机配置
+ */
+ @PostMapping("/printer/info")
+ @Operation(summary = "上报打印终端实际打印机信息")
+ public R> reportPrinterInfo(@RequestHeader("DEVICE-ID") Long hardId,
+ @Valid @RequestBody HardPrinterInfoReportDto dto) {
+ return R.ok(hardPrinterService.reportDeviceInfo(hardId, dto));
+ }
+
+ /**
+ * 拉取当前 EY_PRINT 尚未完成的任务,补偿离线期间遗漏的 MQTT 消息。
+ *
+ * @param hardId DEVICE-ID 请求头中的打印终端 ID
+ * @return 待打印任务列表
+ */
+ @GetMapping("/printer/tasks")
+ @Operation(summary = "拉取打印终端待办")
+ public R> printerTasks(@RequestHeader("DEVICE-ID") Long hardId) {
+ return R.ok(hardInvoiceService.listPendingTasks(hardId));
+ }
+
+ /**
+ * 提交当前 EY_PRINT 的 USB 打印结果。
+ *
+ * @param hardId DEVICE-ID 请求头中的打印终端 ID
+ * @param result 任务、订单、版本和结果
+ * @return 接受或幂等忽略时返回 {@code true}
+ */
+ @PostMapping("/printer/result")
+ @Operation(summary = "提交打印终端结果")
+ public R printerResult(@RequestHeader("DEVICE-ID") Long hardId,
+ @Valid @RequestBody HardPrintResultDto result) {
+ return R.ok(hardInvoiceService.reportPrintResult(hardId, result));
+ }
+
+ /**
+ * 业务终端按可信 DEVICE-ID 创建打印任务,禁止请求体跨商户指定来源设备。
+ *
+ * @param hardId DEVICE-ID 请求头中的来源业务终端 ID
+ * @param request 订单和打印类型
+ * @return 创建、复用或重试后的任务
+ */
+ @PostMapping("/printer/task")
+ @Operation(summary = "业务终端创建打印任务")
+ public R> createPrinterTask(@RequestHeader("DEVICE-ID") Long hardId,
+ @Valid @RequestBody HardPrintRequestDto request) {
+ request.setSourceHardId(hardId);
+ return R.ok(hardInvoiceService.createPrintTask(request));
+ }
+
@PostMapping("/sync-offline-order")
@Operation(summary = "同步离线订单")
@Idempotent(expireTime = 1, info = "正在处理中,请勿重复操作")
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/manager/HardManagerController.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/manager/HardManagerController.java
index f7ea48a6..aad5ddf8 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/manager/HardManagerController.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/manager/HardManagerController.java
@@ -90,7 +90,7 @@ public class HardManagerController {
@DeleteMapping
@PreAuthorize("@pms.hasPermission('hard_hardManager_del')")
public R removeById(@RequestBody Long[] ids) {
- return R.ok(hardManagerService.removeBatchByIds(CollUtil.toList(ids)));
+ return R.ok(hardManagerService.removeWithPrinterCheck(CollUtil.toList(ids)));
}
@ResponseExcel
@@ -99,4 +99,4 @@ public class HardManagerController {
public List export(HardManagerEntity hardManager, Long[] ids) {
return hardManagerService.list(Wrappers.lambdaQuery(hardManager).in(ArrayUtil.isNotEmpty(ids), HardManagerEntity::getId, ids));
}
-}
\ No newline at end of file
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/job/HardPrintTaskJob.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/job/HardPrintTaskJob.java
new file mode 100644
index 00000000..449a6f65
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/job/HardPrintTaskJob.java
@@ -0,0 +1,32 @@
+package com.bsd.pjcloud.hard.job;
+
+import com.bsd.pjcloud.hard.service.HardInvoiceService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+/**
+ * 持久化打印任务补偿调度器,恢复进程退出或 MQTT 离线期间遗留的任务。
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class HardPrintTaskJob {
+
+ /** 打印任务服务。 */
+ private final HardInvoiceService hardInvoiceService;
+
+ /**
+ * 每十秒扫描待生成任务,并按固定间隔补偿下发待打印任务。
+ */
+ @Scheduled(fixedDelay = 10000L, initialDelay = 20000L)
+ public void processDueTasks() {
+ try {
+ int processed = hardInvoiceService.processDueTasks();
+ if (processed > 0) log.info("打印任务补偿扫描完成,processed:{}", processed);
+ } catch (Exception exception) {
+ log.error("打印任务补偿扫描异常", exception);
+ }
+ }
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/HardPrinterBindingMapper.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/HardPrinterBindingMapper.java
new file mode 100644
index 00000000..5bfb5698
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/HardPrinterBindingMapper.java
@@ -0,0 +1,12 @@
+package com.bsd.pjcloud.hard.mapper;
+
+import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
+import com.bsd.pjcloud.hard.api.entity.HardPrinterBindingEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 打印终端一对一绑定关系数据访问接口。
+ */
+@Mapper
+public interface HardPrinterBindingMapper extends PjcloudBaseMapper {
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/HardPrinterConfigMapper.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/HardPrinterConfigMapper.java
new file mode 100644
index 00000000..fe7ba412
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/HardPrinterConfigMapper.java
@@ -0,0 +1,12 @@
+package com.bsd.pjcloud.hard.mapper;
+
+import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
+import com.bsd.pjcloud.hard.api.entity.HardPrinterConfigEntity;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 尔印打印终端参数数据访问接口。
+ */
+@Mapper
+public interface HardPrinterConfigMapper extends PjcloudBaseMapper {
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/manager/HardManagerMapper.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/manager/HardManagerMapper.java
index 65e6d7a8..e6c4ca29 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/manager/HardManagerMapper.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/manager/HardManagerMapper.java
@@ -23,6 +23,7 @@ public interface HardManagerMapper extends PjcloudBaseMapper
MPJLambdaWrapper wrapper = new MPJLambdaWrapper()
.selectAll(HardManagerEntity.class)
.likeIfExists(HardManagerEntity::getName, dto.getName())
+ .likeIfExists(HardManagerEntity::getUniqueCode, dto.getUniqueCode())
.eqIfExists(HardManagerEntity::getDeptId, dto.getDeptId())
.eqIfExists(HardManagerEntity::getType, dto.getType())
.eqIfExists(HardManagerEntity::getMerchantId, dto.getMerchantId())
@@ -43,6 +44,7 @@ public interface HardManagerMapper extends PjcloudBaseMapper
default Page scopePageList(Page page, HardManagerDto dto) {
LambdaQueryWrapper wrapper = new LambdaQueryWrapper()
.like(ObjectUtil.isNotEmpty(dto.getName()), HardManagerEntity::getName, dto.getName())
+ .like(ObjectUtil.isNotEmpty(dto.getUniqueCode()), HardManagerEntity::getUniqueCode, dto.getUniqueCode())
.eq(ObjectUtil.isNotEmpty(dto.getDeptId()), HardManagerEntity::getDeptId, dto.getDeptId())
.eq(ObjectUtil.isNotEmpty(dto.getType()), HardManagerEntity::getType, dto.getType())
.eq(ObjectUtil.isNotEmpty(dto.getMerchantId()), HardManagerEntity::getMerchantId, dto.getMerchantId())
@@ -54,4 +56,4 @@ public interface HardManagerMapper extends PjcloudBaseMapper
list.setRecords(BeanUtils.toBean(list.getRecords(), HardManagerVo.class));
return list;
}
-}
\ No newline at end of file
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardInvoiceService.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardInvoiceService.java
index 624b7f6b..fae73d06 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardInvoiceService.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardInvoiceService.java
@@ -3,12 +3,26 @@ package com.bsd.pjcloud.hard.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bsd.pjcloud.hard.api.dto.HardInvoiceDto;
+import com.bsd.pjcloud.hard.api.dto.HardPrintRequestDto;
+import com.bsd.pjcloud.hard.api.dto.HardPrintResultDto;
import com.bsd.pjcloud.hard.api.entity.HardInvoiceEntity;
import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity;
import com.bsd.pjcloud.hard.api.vo.HardInvoiceVo;
+import java.util.List;
+
+/**
+ * 打印凭证图片、持久化任务及终端回执服务。
+ */
public interface HardInvoiceService extends IService {
+ /**
+ * 按数据权限分页查询打印任务。
+ *
+ * @param page 分页参数
+ * @param dto 查询条件
+ * @return 打印任务分页
+ */
Page scopeJoinPageList(Page page, HardInvoiceDto dto);
/**
@@ -37,4 +51,70 @@ public interface HardInvoiceService extends IService {
* 根据订单信息打印
*/
Boolean printByOrder(HardOrderEntity order);
-}
\ No newline at end of file
+
+ /**
+ * 在预约核销事务内幂等创建兑换券任务,不执行图片或网络操作。
+ *
+ * @param order 已持有行锁的预约订单快照
+ * @param sourceHardId 实际核销终端 ID;缺少请求头时为非正数
+ * @return 创建或已存在的兑换券任务
+ */
+ HardInvoiceEntity createRedemptionVoucherTask(HardOrderEntity order, Long sourceHardId);
+
+ /**
+ * 管理端创建首张发票或补建兑换券任务。
+ *
+ * @param request 打印请求
+ * @return 创建、复用或重试后的任务
+ */
+ HardInvoiceEntity createPrintTask(HardPrintRequestDto request);
+
+ /**
+ * 重试失败任务并增加 retryVersion,打印序号保持不变。
+ *
+ * @param historyId 打印任务 ID
+ * @return 重试后的任务
+ */
+ HardInvoiceEntity retryTask(Long historyId, Long sourceHardId);
+
+ /**
+ * 对已打印任务创建下一打印序号,原因必须填写。
+ *
+ * @param historyId 已打印任务 ID
+ * @param reason 人工重打原因
+ * @return 新打印任务
+ */
+ HardInvoiceEntity reprintTask(Long historyId, String reason);
+
+ /**
+ * 核销回退时取消该订单所有未打印兑换券任务。
+ *
+ * @param orderId 订单 ID
+ * @return 被取消任务数量
+ */
+ int cancelPendingVoucherTasks(Long orderId);
+
+ /**
+ * 查询指定 EY_PRINT 终端的待打印任务,供启动、重连和定时补拉。
+ *
+ * @param printerHardId 目标打印终端 ID
+ * @return 待打印任务列表
+ */
+ List listPendingTasks(Long printerHardId);
+
+ /**
+ * 校验终端身份、订单和版本后记录打印结果。
+ *
+ * @param printerHardId DEVICE-ID 请求头解析出的目标设备
+ * @param result 终端回执
+ * @return 接受或幂等忽略回执时返回 {@code true}
+ */
+ Boolean reportPrintResult(Long printerHardId, HardPrintResultDto result);
+
+ /**
+ * 跨租户扫描并处理待生成和待打印任务。
+ *
+ * @return 本轮处理任务数量
+ */
+ int processDueTasks();
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardManagerService.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardManagerService.java
index cef7f293..ecc7a503 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardManagerService.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardManagerService.java
@@ -21,6 +21,14 @@ public interface HardManagerService extends IService {
void edit(HardManagerDto dto);
+ /**
+ * 删除设备前校验其打印绑定和待处理任务。
+ *
+ * @param ids 待删除设备 ID 列表
+ * @return 实际删除结果
+ */
+ boolean removeWithPrinterCheck(List ids);
+
HardManagerVo bind(HardBindDto dto);
HardManagerVo getByHardId(Long hardId);
@@ -41,4 +49,4 @@ public interface HardManagerService extends IService {
* 判断当前用户是否为合作商家超市管理员
*/
boolean isSuperMarketAdmin(PjcloudUser user);
-}
\ No newline at end of file
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardPrinterService.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardPrinterService.java
new file mode 100644
index 00000000..9ca8b37d
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardPrinterService.java
@@ -0,0 +1,120 @@
+package com.bsd.pjcloud.hard.service;
+
+import com.bsd.pjcloud.hard.api.dto.HardPrinterBindingDto;
+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;
+import com.bsd.pjcloud.hard.api.vo.manager.HardManagerVo;
+
+import java.util.List;
+
+/**
+ * 尔印打印终端绑定和配置服务。
+ */
+public interface HardPrinterService {
+
+ /**
+ * 查询指定来源或打印设备当前绑定详情。
+ *
+ * @param hardId 来源业务终端或打印终端 ID
+ * @return 绑定详情;未绑定时返回 {@code null}
+ */
+ HardPrinterBindingVo getBinding(Long hardId);
+
+ /**
+ * 按目标打印终端身份读取启动绑定,拒绝来源业务终端借用打印模式接口。
+ *
+ * @param printerHardId DEVICE-ID 声明的 EY_PRINT 设备 ID
+ * @return 已绑定的来源终端及打印参数
+ */
+ HardPrinterBindingVo getPrinterBinding(Long printerHardId);
+
+ /**
+ * 批量补充设备分页中的绑定关系和打印参数,避免管理端逐行请求。
+ *
+ * @param devices 已完成数据权限过滤的当前分页设备
+ */
+ void enrichDevices(List devices);
+
+ /**
+ * 查询来源设备同商户下尚未绑定的 EY_PRINT 设备。
+ *
+ * @param sourceHardId 来源业务终端 ID
+ * @return 可绑定打印终端列表
+ */
+ List listAvailablePrinters(Long sourceHardId);
+
+ /**
+ * 在当前操作人数据权限内查询商户下已经绑定打印机的业务终端,供无来源订单人工打印时选择。
+ *
+ * @param merchantId 商户 ID
+ * @return 已绑定来源业务终端列表
+ */
+ List listBoundSources(Long merchantId);
+
+ /**
+ * 创建严格一对一绑定并初始化打印机默认配置。
+ *
+ * @param dto 来源和目标设备
+ * @return 新绑定详情
+ */
+ HardPrinterBindingVo bind(HardPrinterBindingDto dto);
+
+ /**
+ * 在没有待处理任务时解除指定来源终端绑定。
+ *
+ * @param sourceHardId 来源业务终端 ID
+ */
+ void unbind(Long sourceHardId);
+
+ /**
+ * 查询打印机配置,不存在时返回尚未持久化的默认配置。
+ *
+ * @param printerHardId EY_PRINT 设备 ID
+ * @return 当前或默认配置
+ */
+ HardPrinterConfigEntity getConfig(Long printerHardId);
+
+ /**
+ * 保存打印机型号、USB 和 ESC/POS 参数。
+ *
+ * @param dto 打印机配置
+ * @return 保存后的配置
+ */
+ HardPrinterConfigEntity saveConfig(HardPrinterConfigDto dto);
+
+ /**
+ * 由当前打印终端上报实际打印机型号和 USB 标识,保留后台维护的 ESC/POS 参数。
+ *
+ * @param printerHardId DEVICE-ID 请求头中的 EY_PRINT 设备 ID
+ * @param dto Android USB Host 读取到的型号、VID 和 PID
+ * @return 更新后的打印机配置
+ */
+ HardPrinterConfigEntity reportDeviceInfo(Long printerHardId, HardPrinterInfoReportDto dto);
+
+ /**
+ * 按来源业务终端解析当前有效打印终端。
+ *
+ * @param sourceHardId 来源终端 ID
+ * @return 当前绑定;未绑定时返回 {@code null}
+ */
+ HardPrinterBindingEntity findBySourceHardId(Long sourceHardId);
+
+ /**
+ * 校验已绑定设备不能直接变更类型、商户或部门。
+ *
+ * @param before 修改前设备
+ * @param after 修改请求
+ */
+ void validateDeviceChange(HardManagerEntity before, HardManagerEntity after);
+
+ /**
+ * 校验设备删除前已经解除打印绑定且没有待处理任务。
+ *
+ * @param hardId 待删除设备 ID
+ */
+ void validateDeviceRemoval(Long hardId);
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/HardInvoiceServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/HardInvoiceServiceImpl.java
index c1e4fada..35078724 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/HardInvoiceServiceImpl.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/HardInvoiceServiceImpl.java
@@ -3,44 +3,58 @@ package com.bsd.pjcloud.hard.service.impl;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.ObjectUtil;
-import cn.hutool.extra.spring.SpringUtil;
-import com.alibaba.fastjson.JSON;
+import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bsd.pjcloud.admin.api.dto.UserInfo;
import com.bsd.pjcloud.admin.api.entity.SysDept;
-import com.bsd.pjcloud.admin.api.entity.SysUser;
import com.bsd.pjcloud.admin.api.feign.RemoteDeptService;
import com.bsd.pjcloud.admin.api.feign.RemoteFileService;
import com.bsd.pjcloud.admin.api.feign.RemoteUserService;
import com.bsd.pjcloud.admin.api.utils.DataUtil;
+import com.bsd.pjcloud.canteen.api.feign.RemoteCanteenSpuService;
+import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity;
+import com.bsd.pjcloud.canteen.vo.app.OnlineSpuComboItemVO;
import com.bsd.pjcloud.common.core.util.BeanUtils;
import com.bsd.pjcloud.common.core.util.CollectionUtils;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.data.datascope.DataScope;
+import com.bsd.pjcloud.common.data.tenant.TenantBroker;
+import com.bsd.pjcloud.common.data.tenant.TenantContextHolder;
+import com.bsd.pjcloud.hard.api.constant.HardPrintFailureStageEnum;
import com.bsd.pjcloud.hard.api.constant.HardPrintStatusEnum;
+import com.bsd.pjcloud.hard.api.constant.HardPrintTypeEnum;
import com.bsd.pjcloud.hard.api.constant.HardTypeEnum;
+import com.bsd.pjcloud.hard.api.constant.order.HardHourTypeEnum;
+import com.bsd.pjcloud.hard.api.constant.order.HardOrderStatusEnum;
import com.bsd.pjcloud.hard.api.constant.order.HardOrderTypeEnum;
+import com.bsd.pjcloud.hard.api.constant.refund.HardRefundStatusEnum;
import com.bsd.pjcloud.hard.api.dto.HardInvoiceDto;
+import com.bsd.pjcloud.hard.api.dto.HardPrintRequestDto;
+import com.bsd.pjcloud.hard.api.dto.HardPrintResultDto;
import com.bsd.pjcloud.hard.api.entity.HardInvoiceEntity;
+import com.bsd.pjcloud.hard.api.entity.HardPrinterBindingEntity;
import com.bsd.pjcloud.hard.api.entity.manager.HardManagerEntity;
import com.bsd.pjcloud.hard.api.entity.order.HardCarEntity;
import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity;
-import com.bsd.pjcloud.hard.api.utils.Drawing2D;
+import com.bsd.pjcloud.hard.api.utils.ThermalReceiptImage;
import com.bsd.pjcloud.hard.api.vo.HardInvoiceVo;
-import com.bsd.pjcloud.hard.api.vo.manager.HardManagerVo;
+import com.bsd.pjcloud.hard.api.vo.order.HardCarVo;
import com.bsd.pjcloud.hard.mapper.HardInvoiceMapper;
-import com.bsd.pjcloud.hard.mapper.manager.HardManagerMapper;
import com.bsd.pjcloud.hard.mapper.order.HardOrderMapper;
import com.bsd.pjcloud.hard.service.HardCarService;
import com.bsd.pjcloud.hard.service.HardInvoiceService;
import com.bsd.pjcloud.hard.service.HardManagerService;
import com.bsd.pjcloud.hard.service.HardOrderService;
+import com.bsd.pjcloud.hard.service.HardPrinterService;
import com.bsd.pjcloud.hard.support.HardUserIdentitySupport;
import com.bsd.pjcloud.mqtt.api.feign.RemoteMqttEventService;
import com.bsd.pjcloud.mqtt.dto.hard.HardPrintMsg;
import com.bsd.pjcloud.wallet.api.constant.WalletTypeEnum;
+import com.bsd.pjcloud.wallet.api.entity.WalletCodeUseEntity;
+import com.bsd.pjcloud.wallet.api.feign.RemoteCodeUseService;
import com.bsd.pjcloud.wallet.api.feign.RemoteMchService;
import com.bsd.pjcloud.wallet.api.feign.RemoteWalletService;
import com.bsd.pjcloud.wallet.api.utils.WalletDataUtil;
@@ -48,72 +62,118 @@ import com.bsd.pjcloud.wallet.api.vo.MchManageVo;
import com.bsd.pjcloud.wallet.api.vo.WalletManageVo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.dao.DuplicateKeyException;
import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionSynchronization;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.multipart.MultipartFile;
-import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
-import java.util.ArrayList;
+import java.time.LocalDateTime;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
/**
- * 发票记录
+ * 打印凭证记录与持久化任务服务实现。
*
- * @author dzc
- * @date 2025-01-05 14:17:32
+ * 业务事务只创建任务;图片上传、MQTT 下发和终端打印均在事务提交后异步执行。
*/
@Validated
@Slf4j
@RequiredArgsConstructor
@Service
-public class HardInvoiceServiceImpl extends ServiceImpl implements HardInvoiceService {
+public class HardInvoiceServiceImpl extends ServiceImpl
+ implements HardInvoiceService {
- static private final String TAG = "发票记录";
+ /** 核销回退后再次核销时自动记录的重打原因。 */
+ private static final String REVERIFY_REASON = "核销回退后再次核销";
+ /** MQTT 待打印任务允许补偿重投的最小间隔,单位秒。 */
+ private static final long REDISPATCH_INTERVAL_SECONDS = 30L;
+
+ /** 单轮跨租户扫描任务上限。 */
+ private static final int SCAN_LIMIT = 100;
+
+ /** 错误摘要数据库最大长度。 */
+ private static final int ERROR_MAX_LENGTH = 2000;
+
+ /** 设备管理服务。 */
private final HardManagerService hardManagerService;
+
+ /** 订单服务。 */
private final HardOrderService orderService;
+
+ /** 订单 Mapper,用于串行化同一订单的人工创建与重打序号分配。 */
+ private final HardOrderMapper hardOrderMapper;
+
+ /** 订单商品服务。 */
private final HardCarService carService;
- private final HardOrderMapper hardOrderMapper;
- private final HardManagerMapper hardManagerMapper;
+ /** 打印机绑定服务。 */
+ private final HardPrinterService printerService;
+ /** 用户远程服务。 */
private final RemoteUserService userService;
+
+ /** 部门远程服务。 */
private final RemoteDeptService deptService;
+
+ /** 商户远程服务。 */
private final RemoteMchService remoteMchService;
+
+ /** 钱包远程服务。 */
private final RemoteWalletService walletService;
+
+ /** 文件上传远程服务。 */
private final RemoteFileService fileService;
+
+ /** MQTT 消息远程服务。 */
private final RemoteMqttEventService mqttEventService;
- /** 开票记录用户身份筛选与展示资料装配器。 */
+ /** 预约核销码远程服务。 */
+ private final RemoteCodeUseService remoteCodeUseService;
+
+ /** 食堂菜品和套餐组成远程服务。 */
+ private final RemoteCanteenSpuService remoteCanteenSpuService;
+
+ /** 硬件业务异步线程池。 */
+ private final ThreadPoolTaskExecutor hardTaskExecutor;
+
+ /** 打印记录用户身份筛选与展示资料装配器。 */
private final HardUserIdentitySupport userIdentitySupport;
+ /** {@inheritDoc} */
@Override
public Page scopeJoinPageList(Page page, HardInvoiceDto dto) {
prepareIdentityQuery(dto);
Page list = baseMapper.scopePageList(page, dto, DataScope.of());
if (ObjectUtil.isNotEmpty(list.getRecords())) {
- // 部门
- List deptIds = list.getRecords().stream().map(HardInvoiceVo::getDeptId).filter(ObjectUtil::isNotEmpty).distinct().toList();
+ List deptIds = list.getRecords().stream().map(HardInvoiceVo::getDeptId)
+ .filter(ObjectUtil::isNotEmpty).distinct().toList();
if (ObjectUtil.isNotEmpty(deptIds)) {
R