feat:修改设备列表、详情
feat:修改订单详情 feat:修改售后订单查询条件 feat:增加打印机设备及功能
This commit is contained in:
1 parent
fc8afe4de7
commit
8514af0d9f
49 files changed
+3734
-222
No files matched your search
@@ -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;
|
||||
+75
-14
@@ -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
|
||||
|
||||
+44
@@ -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);
|
||||
}
|
||||
}
|
||||
+27
-4
@@ -4,23 +4,46 @@ import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import javax.xml.stream.events.StartDocument;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 打印状态
|
||||
* 打印任务状态。
|
||||
*
|
||||
* <p>编码 {@code 1} 和 {@code 2} 分别兼容历史“已生成”和“已打印”记录。</p>
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum HardPrintStatusEnum {
|
||||
GENERATE("1", "已生成"),
|
||||
/** 任务已落库,等待后端生成并上传打印图片。 */
|
||||
PENDING_GENERATION("0", "待生成"),
|
||||
|
||||
/** 图片已生成并允许下发,历史枚举名保留为 GENERATE 以保持二进制兼容。 */
|
||||
GENERATE("1", "待打印"),
|
||||
|
||||
/** 打印终端已确认完整 USB 数据和切纸命令写入成功。 */
|
||||
PRINT("2", "已打印"),
|
||||
|
||||
/** 绑定、图片生成、MQTT 下发或终端打印阶段失败,可重试原任务。 */
|
||||
FAILED("3", "打印失败"),
|
||||
|
||||
/** 核销回退等业务动作取消了尚未打印的任务。 */
|
||||
CANCELLED("4", "已取消"),
|
||||
;
|
||||
|
||||
/** 持久化状态编码。 */
|
||||
private final String type;
|
||||
|
||||
/** 状态中文说明。 */
|
||||
private final String description;
|
||||
|
||||
/**
|
||||
* 按持久化编码解析打印状态。
|
||||
*
|
||||
* @param type 状态编码
|
||||
* @return 匹配状态;编码为空或未知时返回 {@code null}
|
||||
*/
|
||||
public static HardPrintStatusEnum valueOfByType(String type) {
|
||||
if (ObjectUtil.isEmpty(type)) return null;
|
||||
return Arrays.stream(values()).filter(v -> ObjectUtil.equals(type, v.getType())).findFirst().get();
|
||||
return Arrays.stream(values()).filter(v -> ObjectUtil.equals(type, v.getType())).findFirst().orElse(null);
|
||||
}
|
||||
}
|
||||
+38
@@ -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);
|
||||
}
|
||||
}
|
||||
+13
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+32
@@ -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;
|
||||
}
|
||||
+45
@@ -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;
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.bsd.pjcloud.hard.api.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 对既有打印任务执行失败重试或已打印重打的请求。
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "打印任务重试或重打请求")
|
||||
public class HardPrintRetryDto {
|
||||
|
||||
/** 既有 hard_invoice 打印任务主键。 */
|
||||
@NotNull(message = "打印任务ID不能为空")
|
||||
@Schema(description = "打印任务ID")
|
||||
private Long historyId;
|
||||
|
||||
/**
|
||||
* 失败任务需要补录的来源业务终端 ID。
|
||||
*
|
||||
* <p>仅当原任务因缺少来源终端而失败时使用;服务端仍会校验该终端与订单同商户,
|
||||
* 并根据服务端绑定关系确定目标 EY_PRINT,客户端不能直接指定打印机。</p>
|
||||
*/
|
||||
@Schema(description = "补录的来源业务终端ID")
|
||||
private Long sourceHardId;
|
||||
|
||||
/** 已打印任务重打时必须填写的原因;失败任务原任务重试时可为空。 */
|
||||
@Schema(description = "重打原因")
|
||||
private String reprintReason;
|
||||
}
|
||||
+23
@@ -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;
|
||||
}
|
||||
+46
@@ -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;
|
||||
}
|
||||
+37
@@ -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;
|
||||
}
|
||||
+3
@@ -17,6 +17,9 @@ public class HardAfterSaleDto extends HardAfterSaleEntity implements BaseUserIde
|
||||
/** 售后申请用户主键集合;为空时不限制申请人。 */
|
||||
private List<Long> userIds;
|
||||
|
||||
/** 关联原订单的用户主键集合;用于区分代订场景中的退款发起人和实际订单用户。 */
|
||||
private List<Long> orderUserIds;
|
||||
|
||||
/** UPMS 用户身份协议编码集合;为空时不限制身份。 */
|
||||
private List<String> identityTypes;
|
||||
|
||||
|
||||
+67
-13
@@ -37,28 +37,82 @@ public class HardInvoiceEntity extends Model<HardInvoiceEntity> {
|
||||
@Schema(description="部门id")
|
||||
private Long deptId;
|
||||
|
||||
/**
|
||||
* 设备id
|
||||
*/
|
||||
@Schema(description="设备id")
|
||||
/**
|
||||
* 来源业务终端 ID;历史记录继续保留原设备语义,不改写为打印机 ID。
|
||||
*/
|
||||
@Schema(description="来源业务终端ID")
|
||||
private Long hardId;
|
||||
|
||||
/**
|
||||
* 实际接收任务的尔印打印终端 ID;历史记录和绑定失败任务允许为空。
|
||||
*/
|
||||
@Schema(description="目标打印终端ID")
|
||||
private Long printerHardId;
|
||||
|
||||
/**
|
||||
* 硬件订单id
|
||||
*/
|
||||
@Schema(description="硬件订单id")
|
||||
private Long orderId;
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
@Schema(description="状态")
|
||||
/**
|
||||
* 打印任务状态,编码来自 HardPrintStatusEnum。
|
||||
*/
|
||||
@Schema(description="打印任务状态")
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 发票图片
|
||||
*/
|
||||
@Schema(description="发票图片")
|
||||
/**
|
||||
* 打印凭证类型,1 发票、2 兑换券;历史空值按发票兼容。
|
||||
*/
|
||||
@Schema(description="打印凭证类型")
|
||||
private String printType;
|
||||
|
||||
/**
|
||||
* 同一订单同一凭证类型的打印序号;重试同一任务时保持不变。
|
||||
*/
|
||||
@Schema(description="打印序号")
|
||||
private Integer printNo;
|
||||
|
||||
/**
|
||||
* 人工重试版本;终端用任务 ID 和该版本共同判断是否允许再次打印。
|
||||
*/
|
||||
@Schema(description="重试版本")
|
||||
private Integer retryVersion;
|
||||
|
||||
/**
|
||||
* 打印终端上报的 USB 累计尝试次数。
|
||||
*/
|
||||
@Schema(description="终端尝试次数")
|
||||
private Integer attemptCount;
|
||||
|
||||
/**
|
||||
* 最近失败阶段,编码来自 HardPrintFailureStageEnum。
|
||||
*/
|
||||
@Schema(description="失败阶段")
|
||||
private String failureStage;
|
||||
|
||||
/**
|
||||
* 最近失败摘要,不保存 MQTT 密码、访问令牌等敏感信息。
|
||||
*/
|
||||
@Schema(description="最近失败原因")
|
||||
private String lastError;
|
||||
|
||||
/**
|
||||
* 打印终端确认完整 USB 数据和切纸命令写入成功的时间。
|
||||
*/
|
||||
@Schema(description="打印成功时间")
|
||||
private LocalDateTime printedAt;
|
||||
|
||||
/**
|
||||
* 人工重打原因,自动首次打印或失败重试允许为空。
|
||||
*/
|
||||
@Schema(description="重打原因")
|
||||
private String reprintReason;
|
||||
|
||||
/**
|
||||
* 后端生成并上传的打印 PNG 地址;待生成和生成失败时允许为空。
|
||||
*/
|
||||
@Schema(description="打印图片")
|
||||
private String pic;
|
||||
|
||||
/**
|
||||
@@ -102,4 +156,4 @@ public class HardInvoiceEntity extends Model<HardInvoiceEntity> {
|
||||
*/
|
||||
@Schema(description="租户ID")
|
||||
private Long tenantId;
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package com.bsd.pjcloud.hard.api.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.activerecord.Model;
|
||||
import com.bsd.pjcloud.common.core.util.TenantTable;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 业务终端与尔印打印终端的一对一绑定关系。
|
||||
*
|
||||
* <p>记录使用物理删除,数据库分别对来源和目标设备设置唯一键,确保任何时刻都不能出现多重绑定。</p>
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TenantTable
|
||||
@TableName("hard_printer_binding")
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "业务终端与打印终端绑定关系")
|
||||
public class HardPrinterBindingEntity extends Model<HardPrinterBindingEntity> {
|
||||
|
||||
/** 绑定关系主键。 */
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "绑定关系主键")
|
||||
private Long id;
|
||||
|
||||
/** 来源业务终端 ID,只允许 SUPERMARKET 或 CANTEEN。 */
|
||||
@Schema(description = "来源业务终端ID")
|
||||
private Long sourceHardId;
|
||||
|
||||
/** 目标打印终端 ID,只允许 EY_PRINT。 */
|
||||
@Schema(description = "目标打印终端ID")
|
||||
private Long printerHardId;
|
||||
|
||||
/** 双方共同所属商户 ID 快照。 */
|
||||
@Schema(description = "商户ID")
|
||||
private Long merchantId;
|
||||
|
||||
/** 双方共同所属商户部门 ID 快照。 */
|
||||
@Schema(description = "商户部门ID")
|
||||
private Long deptId;
|
||||
|
||||
/** 创建人。 */
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@Schema(description = "创建人")
|
||||
private String createBy;
|
||||
|
||||
/** 创建时间。 */
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/** 修改人。 */
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
@Schema(description = "修改人")
|
||||
private String updateBy;
|
||||
|
||||
/** 修改时间。 */
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
@Schema(description = "修改时间")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/** 租户 ID。 */
|
||||
@Schema(description = "租户ID")
|
||||
private Long tenantId;
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package com.bsd.pjcloud.hard.api.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.activerecord.Model;
|
||||
import com.bsd.pjcloud.common.core.util.TenantTable;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 尔印打印终端的 USB 和 ESC/POS 参数配置。
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TenantTable
|
||||
@TableName("hard_printer_config")
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "尔印打印终端配置")
|
||||
public class HardPrinterConfigEntity extends Model<HardPrinterConfigEntity> {
|
||||
|
||||
/** 配置主键。 */
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
@Schema(description = "配置主键")
|
||||
private Long id;
|
||||
|
||||
/** 配置所属 EY_PRINT 设备 ID。 */
|
||||
@Schema(description = "打印终端ID")
|
||||
private Long printerHardId;
|
||||
|
||||
/** 真机联调后填写的打印机型号。 */
|
||||
@Schema(description = "打印机型号")
|
||||
private String printerModel;
|
||||
|
||||
/** USB Vendor ID,使用十进制持久化。 */
|
||||
@Schema(description = "USB VID")
|
||||
private Integer usbVendorId;
|
||||
|
||||
/** USB Product ID,使用十进制持久化。 */
|
||||
@Schema(description = "USB PID")
|
||||
private Integer usbProductId;
|
||||
|
||||
/** 纸张宽度,首期固定为 80mm。 */
|
||||
@Schema(description = "纸宽,单位mm")
|
||||
private Integer paperWidth;
|
||||
|
||||
/** 是否在完整数据写入后发送自动切纸命令,1 开启、0 关闭。 */
|
||||
@Schema(description = "自动切刀开关")
|
||||
private String autoCut;
|
||||
|
||||
/** 图片指令模式,GS_V0 为默认值,ESC_STAR 为兼容回退。 */
|
||||
@Schema(description = "图片指令模式")
|
||||
private String imageMode;
|
||||
|
||||
/** 创建人。 */
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@Schema(description = "创建人")
|
||||
private String createBy;
|
||||
|
||||
/** 创建时间。 */
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/** 修改人。 */
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
@Schema(description = "修改人")
|
||||
private String updateBy;
|
||||
|
||||
/** 修改时间。 */
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
@Schema(description = "修改时间")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/** 租户 ID。 */
|
||||
@Schema(description = "租户ID")
|
||||
private Long tenantId;
|
||||
}
|
||||
+333
@@ -0,0 +1,333 @@
|
||||
package com.bsd.pjcloud.hard.api.utils;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.BasicStroke;
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.awt.FontMetrics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 面向 80mm、576px ESC/POS 打印的动态高度 PNG 绘制器。
|
||||
*
|
||||
* <p>商品名称按真实像素宽度换行,数量列固定右对齐,保证长名称不会与数量错配。</p>
|
||||
*/
|
||||
public class ThermalReceiptImage {
|
||||
|
||||
/** 80mm 打印头常见有效像素宽度。 */
|
||||
public static final int PAPER_WIDTH_PIXELS = 576;
|
||||
|
||||
/** 图片左右安全留白。 */
|
||||
private static final int HORIZONTAL_PADDING = 24;
|
||||
|
||||
/** 图片顶部和底部安全留白。 */
|
||||
private static final int VERTICAL_PADDING = 24;
|
||||
|
||||
/** 每次扩展画布的最小高度。 */
|
||||
private static final int HEIGHT_GROWTH = 512;
|
||||
|
||||
/** 数量列保留宽度。 */
|
||||
private static final int QUANTITY_COLUMN_WIDTH = 96;
|
||||
|
||||
/** 套餐组成项左侧缩进。 */
|
||||
private static final int CHILD_INDENT = 32;
|
||||
|
||||
/** 中文优先逻辑字体名称。 */
|
||||
private static final String FONT_FAMILY = "SansSerif";
|
||||
|
||||
/** 当前可扩展画布。 */
|
||||
private BufferedImage canvas;
|
||||
|
||||
/** 当前绘制上下文。 */
|
||||
private Graphics2D graphics;
|
||||
|
||||
/** 下一块内容的顶部 Y 坐标。 */
|
||||
private int cursorY = VERTICAL_PADDING;
|
||||
|
||||
/**
|
||||
* 创建 576px 宽的白底收据画布。
|
||||
*/
|
||||
public ThermalReceiptImage() {
|
||||
canvas = new BufferedImage(PAPER_WIDTH_PIXELS, HEIGHT_GROWTH, BufferedImage.TYPE_INT_RGB);
|
||||
graphics = canvas.createGraphics();
|
||||
configureGraphics(graphics);
|
||||
graphics.setColor(Color.WHITE);
|
||||
graphics.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||
graphics.setColor(Color.BLACK);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制居中粗体标题。
|
||||
*
|
||||
* @param text 标题文本
|
||||
* @return 当前绘制器
|
||||
*/
|
||||
public ThermalReceiptImage title(String text) {
|
||||
setFont(Font.BOLD, 34);
|
||||
drawCentered(text, 16);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制带黑色边框的大号打印次数提示。
|
||||
*
|
||||
* @param text 提示文本,例如“第 1 次打印”
|
||||
* @return 当前绘制器
|
||||
*/
|
||||
public ThermalReceiptImage banner(String text) {
|
||||
setFont(Font.BOLD, 38);
|
||||
FontMetrics metrics = graphics.getFontMetrics();
|
||||
int height = metrics.getHeight() + 30;
|
||||
ensureCapacity(height + 20);
|
||||
graphics.setStroke(new BasicStroke(3F));
|
||||
graphics.drawRect(HORIZONTAL_PADDING, cursorY, contentWidth(), height);
|
||||
int x = HORIZONTAL_PADDING + Math.max(0, (contentWidth() - metrics.stringWidth(text)) / 2);
|
||||
graphics.drawString(text, x, cursorY + 15 + metrics.getAscent());
|
||||
cursorY += height + 20;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制醒目的核销短码。
|
||||
*
|
||||
* @param shortCode 核销短码
|
||||
* @return 当前绘制器
|
||||
*/
|
||||
public ThermalReceiptImage verificationCode(String shortCode) {
|
||||
setFont(Font.BOLD, 52);
|
||||
drawCentered(shortCode == null || shortCode.isBlank() ? "--" : shortCode, 18);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制一行左右键值信息,值过长时在下一行继续换行。
|
||||
*
|
||||
* @param label 左侧字段名
|
||||
* @param value 右侧字段值
|
||||
* @return 当前绘制器
|
||||
*/
|
||||
public ThermalReceiptImage keyValue(String label, String value) {
|
||||
setFont(Font.PLAIN, 25);
|
||||
String safeLabel = label == null ? "" : label;
|
||||
String safeValue = value == null || value.isBlank() ? "--" : value;
|
||||
FontMetrics metrics = graphics.getFontMetrics();
|
||||
int labelWidth = metrics.stringWidth(safeLabel + ":");
|
||||
int valueWidth = contentWidth() - labelWidth - 12;
|
||||
List<String> lines = wrap(safeValue, Math.max(120, valueWidth), metrics);
|
||||
int lineHeight = metrics.getHeight() + 6;
|
||||
ensureCapacity(lines.size() * lineHeight + 10);
|
||||
graphics.drawString(safeLabel + ":", HORIZONTAL_PADDING, cursorY + metrics.getAscent());
|
||||
for (int index = 0; index < lines.size(); index++) {
|
||||
int x = index == 0 ? HORIZONTAL_PADDING + labelWidth + 12 : HORIZONTAL_PADDING + labelWidth + 12;
|
||||
graphics.drawString(lines.get(index), x, cursorY + index * lineHeight + metrics.getAscent());
|
||||
}
|
||||
cursorY += lines.size() * lineHeight + 6;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制商品明细区标题。
|
||||
*
|
||||
* @param text 标题文本
|
||||
* @return 当前绘制器
|
||||
*/
|
||||
public ThermalReceiptImage sectionTitle(String text) {
|
||||
setFont(Font.BOLD, 28);
|
||||
ensureCapacity(48);
|
||||
graphics.drawString(text == null ? "" : text, HORIZONTAL_PADDING, cursorY + graphics.getFontMetrics().getAscent());
|
||||
cursorY += graphics.getFontMetrics().getHeight() + 10;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制商品或套餐组成项,商品名换行且数量始终在首行右对齐。
|
||||
*
|
||||
* @param name 商品名称
|
||||
* @param quantity 数量文本
|
||||
* @param child 是否为套餐组成项
|
||||
* @param emphasized 是否使用粗体标记套餐主项
|
||||
* @return 当前绘制器
|
||||
*/
|
||||
public ThermalReceiptImage item(String name, String quantity, boolean child, boolean emphasized) {
|
||||
return item(name, quantity, child, emphasized, QUANTITY_COLUMN_WIDTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制带数量和金额摘要的发票商品行。
|
||||
*
|
||||
* @param name 商品名称
|
||||
* @param quantity 数量或重量文本
|
||||
* @param amount 含货币符号的商品总价
|
||||
* @return 当前绘制器
|
||||
*/
|
||||
public ThermalReceiptImage invoiceItem(String name, String quantity, String amount) {
|
||||
return item(name, String.format("%s %s", quantity, amount), false, false, 190);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用指定尾列宽度绘制一条可换行明细。
|
||||
*
|
||||
* @param name 左侧名称
|
||||
* @param trailing 首行右对齐摘要
|
||||
* @param child 是否为套餐组成项
|
||||
* @param emphasized 是否使用粗体
|
||||
* @param trailingWidth 尾列预留像素宽度
|
||||
* @return 当前绘制器
|
||||
*/
|
||||
private ThermalReceiptImage item(String name, String trailing, boolean child, boolean emphasized,
|
||||
int trailingWidth) {
|
||||
setFont(emphasized ? Font.BOLD : Font.PLAIN, child ? 23 : 26);
|
||||
FontMetrics metrics = graphics.getFontMetrics();
|
||||
int indent = child ? CHILD_INDENT : 0;
|
||||
String prefix = child ? "- " : "";
|
||||
String safeName = prefix + (name == null || name.isBlank() ? "未命名商品" : name);
|
||||
List<String> lines = wrap(safeName, contentWidth() - indent - trailingWidth, metrics);
|
||||
int lineHeight = metrics.getHeight() + 5;
|
||||
ensureCapacity(lines.size() * lineHeight + 8);
|
||||
for (int index = 0; index < lines.size(); index++) {
|
||||
graphics.drawString(lines.get(index), HORIZONTAL_PADDING + indent,
|
||||
cursorY + index * lineHeight + metrics.getAscent());
|
||||
}
|
||||
String safeTrailing = trailing == null || trailing.isBlank() ? "x0" : trailing;
|
||||
graphics.drawString(safeTrailing, PAPER_WIDTH_PIXELS - HORIZONTAL_PADDING - metrics.stringWidth(safeTrailing),
|
||||
cursorY + metrics.getAscent());
|
||||
cursorY += lines.size() * lineHeight + 6;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制横向分隔线。
|
||||
*
|
||||
* @return 当前绘制器
|
||||
*/
|
||||
public ThermalReceiptImage divider() {
|
||||
ensureCapacity(28);
|
||||
graphics.setStroke(new BasicStroke(2F));
|
||||
graphics.drawLine(HORIZONTAL_PADDING, cursorY + 10,
|
||||
PAPER_WIDTH_PIXELS - HORIZONTAL_PADDING, cursorY + 10);
|
||||
cursorY += 28;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出裁剪到实际高度的 PNG 字节。
|
||||
*
|
||||
* @return PNG 字节
|
||||
* @throws IOException PNG 编码失败
|
||||
*/
|
||||
public byte[] toPngBytes() throws IOException {
|
||||
int finalHeight = cursorY + VERTICAL_PADDING;
|
||||
BufferedImage result = new BufferedImage(PAPER_WIDTH_PIXELS, finalHeight, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D resultGraphics = result.createGraphics();
|
||||
resultGraphics.setColor(Color.WHITE);
|
||||
resultGraphics.fillRect(0, 0, result.getWidth(), result.getHeight());
|
||||
resultGraphics.drawImage(canvas, 0, 0, null);
|
||||
resultGraphics.dispose();
|
||||
graphics.dispose();
|
||||
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
||||
ImageIO.write(result, "png", output);
|
||||
return output.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前字体。
|
||||
*
|
||||
* @param style 字体样式
|
||||
* @param size 字体像素大小
|
||||
*/
|
||||
private void setFont(int style, int size) {
|
||||
graphics.setFont(new Font(FONT_FAMILY, style, size));
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制一行居中文本并推进游标。
|
||||
*
|
||||
* @param text 文本
|
||||
* @param bottomSpace 底部留白
|
||||
*/
|
||||
private void drawCentered(String text, int bottomSpace) {
|
||||
String safeText = text == null ? "" : text;
|
||||
FontMetrics metrics = graphics.getFontMetrics();
|
||||
ensureCapacity(metrics.getHeight() + bottomSpace);
|
||||
int x = Math.max(HORIZONTAL_PADDING,
|
||||
(PAPER_WIDTH_PIXELS - metrics.stringWidth(safeText)) / 2);
|
||||
graphics.drawString(safeText, x, cursorY + metrics.getAscent());
|
||||
cursorY += metrics.getHeight() + bottomSpace;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按实际字体宽度拆分字符串,单个超宽字符仍至少独占一行。
|
||||
*
|
||||
* @param text 原文本
|
||||
* @param maxWidth 最大像素宽度
|
||||
* @param metrics 当前字体度量
|
||||
* @return 非空行列表
|
||||
*/
|
||||
private List<String> wrap(String text, int maxWidth, FontMetrics metrics) {
|
||||
List<String> lines = new ArrayList<>();
|
||||
StringBuilder line = new StringBuilder();
|
||||
String safeText = text == null ? "" : text;
|
||||
for (int offset = 0; offset < safeText.length();) {
|
||||
int codePoint = safeText.codePointAt(offset);
|
||||
String character = new String(Character.toChars(codePoint));
|
||||
if (line.length() > 0 && metrics.stringWidth(line + character) > maxWidth) {
|
||||
lines.add(line.toString());
|
||||
line.setLength(0);
|
||||
}
|
||||
line.append(character);
|
||||
offset += Character.charCount(codePoint);
|
||||
}
|
||||
if (line.length() > 0 || lines.isEmpty()) lines.add(line.toString());
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保画布能够容纳下一块内容,并在扩容时保留字体、画笔和已绘制像素。
|
||||
*
|
||||
* @param requiredHeight 下一块内容需要的高度
|
||||
*/
|
||||
private void ensureCapacity(int requiredHeight) {
|
||||
if (cursorY + requiredHeight <= canvas.getHeight()) return;
|
||||
int newHeight = canvas.getHeight();
|
||||
while (cursorY + requiredHeight > newHeight) newHeight += HEIGHT_GROWTH;
|
||||
Font font = graphics.getFont();
|
||||
Color color = graphics.getColor();
|
||||
BufferedImage expanded = new BufferedImage(PAPER_WIDTH_PIXELS, newHeight, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D expandedGraphics = expanded.createGraphics();
|
||||
configureGraphics(expandedGraphics);
|
||||
expandedGraphics.setColor(Color.WHITE);
|
||||
expandedGraphics.fillRect(0, 0, expanded.getWidth(), expanded.getHeight());
|
||||
expandedGraphics.drawImage(canvas, 0, 0, null);
|
||||
expandedGraphics.setFont(font);
|
||||
expandedGraphics.setColor(color);
|
||||
graphics.dispose();
|
||||
canvas = expanded;
|
||||
graphics = expandedGraphics;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置文字抗锯齿和高质量图片渲染参数。
|
||||
*
|
||||
* @param target 待配置绘制上下文
|
||||
*/
|
||||
private static void configureGraphics(Graphics2D target) {
|
||||
target.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
target.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算扣除左右安全留白后的有效宽度。
|
||||
*
|
||||
* @return 内容宽度
|
||||
*/
|
||||
private static int contentWidth() {
|
||||
return PAPER_WIDTH_PIXELS - HORIZONTAL_PADDING * 2;
|
||||
}
|
||||
}
|
||||
+3
@@ -67,6 +67,9 @@ public class HardInvoiceVo extends HardInvoiceEntity implements BaseUserDeptInte
|
||||
|
||||
private String hardType;
|
||||
|
||||
/** 目标尔印打印终端名称。 */
|
||||
private String printerHardName;
|
||||
|
||||
/**
|
||||
* 用户相关
|
||||
*/
|
||||
|
||||
+32
@@ -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;
|
||||
}
|
||||
+30
@@ -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;
|
||||
|
||||
}
|
||||
+32
@@ -5,6 +5,7 @@ import com.bsd.pjcloud.admin.api.vo.base.BaseUserIdentityInterface;
|
||||
import com.bsd.pjcloud.admin.api.dto.user.SysUserStudentDto;
|
||||
import com.bsd.pjcloud.admin.api.dto.user.SysUserTeacherDto;
|
||||
import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity;
|
||||
import com.bsd.pjcloud.hard.api.entity.order.HardTakeOutApptEntity;
|
||||
import com.bsd.pjcloud.hard.api.vo.base.BaseHardInterface;
|
||||
import com.bsd.pjcloud.wallet.api.vo.base.BaseMchInterface;
|
||||
import com.bsd.pjcloud.wallet.api.vo.base.BaseWalletInterface;
|
||||
@@ -105,6 +106,13 @@ public class HardOrderVo extends HardOrderEntity implements BaseUserDeptInterfac
|
||||
|
||||
private LocalTime apptEndTime;
|
||||
|
||||
/**
|
||||
* 外卖订单的期望送达配置,来源于 hard_take_out_appt;非外卖或历史未记录时为空。
|
||||
*
|
||||
* <p>该字段只参与订单详情响应,不对应 hard_order 表持久化字段。</p>
|
||||
*/
|
||||
private HardTakeOutApptEntity takeOutAppt;
|
||||
|
||||
/**
|
||||
* 付款人用户ID,来源于订单关联的父购物车。
|
||||
*/
|
||||
@@ -129,4 +137,28 @@ public class HardOrderVo extends HardOrderEntity implements BaseUserDeptInterfac
|
||||
* 订单详情中的长期识别证据列表,按证据类型、角色和序号排序。
|
||||
*/
|
||||
private List<HardOrderEvidenceImageVo> evidenceImages;
|
||||
|
||||
/** 当前订单最新发票任务 ID;没有记录时为空。 */
|
||||
private Long latestInvoiceTaskId;
|
||||
|
||||
/** 当前订单最新发票任务状态编码。 */
|
||||
private String latestInvoiceStatus;
|
||||
|
||||
/** 当前订单最新发票打印序号。 */
|
||||
private Integer latestInvoicePrintNo;
|
||||
|
||||
/** 当前订单最新发票任务的来源业务终端 ID。 */
|
||||
private Long latestInvoiceSourceHardId;
|
||||
|
||||
/** 当前订单最新兑换券任务 ID;没有记录时为空。 */
|
||||
private Long latestVoucherTaskId;
|
||||
|
||||
/** 当前订单最新兑换券任务状态编码。 */
|
||||
private String latestVoucherStatus;
|
||||
|
||||
/** 当前订单最新兑换券打印序号。 */
|
||||
private Integer latestVoucherPrintNo;
|
||||
|
||||
/** 当前订单最新兑换券任务的来源业务终端 ID。 */
|
||||
private Long latestVoucherSourceHardId;
|
||||
}
|
||||
+47
-1
@@ -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<HardInvoiceEntity> export(HardInvoiceEntity hardInvoice, Long[] ids) {
|
||||
return hardInvoiceService.list(Wrappers.lambdaQuery(hardInvoice).in(ArrayUtil.isNotEmpty(ids), HardInvoiceEntity::getId, ids));
|
||||
}
|
||||
}
|
||||
}
|
||||
+132
@@ -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<Void> 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));
|
||||
}
|
||||
}
|
||||
+71
@@ -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<Boolean> 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 = "正在处理中,请勿重复操作")
|
||||
|
||||
+2
-2
@@ -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<HardManagerEntity> export(HardManagerEntity hardManager, Long[] ids) {
|
||||
return hardManagerService.list(Wrappers.lambdaQuery(hardManager).in(ArrayUtil.isNotEmpty(ids), HardManagerEntity::getId, ids));
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
@@ -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<HardPrinterBindingEntity> {
|
||||
}
|
||||
+12
@@ -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<HardPrinterConfigEntity> {
|
||||
}
|
||||
+3
-1
@@ -23,6 +23,7 @@ public interface HardManagerMapper extends PjcloudBaseMapper<HardManagerEntity>
|
||||
MPJLambdaWrapper<HardManagerEntity> wrapper = new MPJLambdaWrapper<HardManagerEntity>()
|
||||
.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<HardManagerEntity>
|
||||
default Page<HardManagerVo> scopePageList(Page page, HardManagerDto dto) {
|
||||
LambdaQueryWrapper<HardManagerEntity> wrapper = new LambdaQueryWrapper<HardManagerEntity>()
|
||||
.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<HardManagerEntity>
|
||||
list.setRecords(BeanUtils.toBean(list.getRecords(), HardManagerVo.class));
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
+81
-1
@@ -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<HardInvoiceEntity> {
|
||||
|
||||
/**
|
||||
* 按数据权限分页查询打印任务。
|
||||
*
|
||||
* @param page 分页参数
|
||||
* @param dto 查询条件
|
||||
* @return 打印任务分页
|
||||
*/
|
||||
Page<HardInvoiceVo> scopeJoinPageList(Page page, HardInvoiceDto dto);
|
||||
|
||||
/**
|
||||
@@ -37,4 +51,70 @@ public interface HardInvoiceService extends IService<HardInvoiceEntity> {
|
||||
* 根据订单信息打印
|
||||
*/
|
||||
Boolean printByOrder(HardOrderEntity order);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在预约核销事务内幂等创建兑换券任务,不执行图片或网络操作。
|
||||
*
|
||||
* @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<HardInvoiceEntity> listPendingTasks(Long printerHardId);
|
||||
|
||||
/**
|
||||
* 校验终端身份、订单和版本后记录打印结果。
|
||||
*
|
||||
* @param printerHardId DEVICE-ID 请求头解析出的目标设备
|
||||
* @param result 终端回执
|
||||
* @return 接受或幂等忽略回执时返回 {@code true}
|
||||
*/
|
||||
Boolean reportPrintResult(Long printerHardId, HardPrintResultDto result);
|
||||
|
||||
/**
|
||||
* 跨租户扫描并处理待生成和待打印任务。
|
||||
*
|
||||
* @return 本轮处理任务数量
|
||||
*/
|
||||
int processDueTasks();
|
||||
}
|
||||
+9
-1
@@ -21,6 +21,14 @@ public interface HardManagerService extends IService<HardManagerEntity> {
|
||||
|
||||
void edit(HardManagerDto dto);
|
||||
|
||||
/**
|
||||
* 删除设备前校验其打印绑定和待处理任务。
|
||||
*
|
||||
* @param ids 待删除设备 ID 列表
|
||||
* @return 实际删除结果
|
||||
*/
|
||||
boolean removeWithPrinterCheck(List<Long> ids);
|
||||
|
||||
HardManagerVo bind(HardBindDto dto);
|
||||
|
||||
HardManagerVo getByHardId(Long hardId);
|
||||
@@ -41,4 +49,4 @@ public interface HardManagerService extends IService<HardManagerEntity> {
|
||||
* 判断当前用户是否为合作商家超市管理员
|
||||
*/
|
||||
boolean isSuperMarketAdmin(PjcloudUser user);
|
||||
}
|
||||
}
|
||||
+120
@@ -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<HardManagerVo> devices);
|
||||
|
||||
/**
|
||||
* 查询来源设备同商户下尚未绑定的 EY_PRINT 设备。
|
||||
*
|
||||
* @param sourceHardId 来源业务终端 ID
|
||||
* @return 可绑定打印终端列表
|
||||
*/
|
||||
List<HardManagerEntity> listAvailablePrinters(Long sourceHardId);
|
||||
|
||||
/**
|
||||
* 在当前操作人数据权限内查询商户下已经绑定打印机的业务终端,供无来源订单人工打印时选择。
|
||||
*
|
||||
* @param merchantId 商户 ID
|
||||
* @return 已绑定来源业务终端列表
|
||||
*/
|
||||
List<HardManagerEntity> 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);
|
||||
}
|
||||
+719
-164
File diff suppressed because it is too large.
Load diff
+435
@@ -0,0 +1,435 @@
|
||||
package com.bsd.pjcloud.hard.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.bsd.pjcloud.common.core.util.BeanUtils;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScope;
|
||||
import com.bsd.pjcloud.hard.api.constant.HardPrintStatusEnum;
|
||||
import com.bsd.pjcloud.hard.api.constant.HardTypeEnum;
|
||||
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.HardInvoiceEntity;
|
||||
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 com.bsd.pjcloud.hard.mapper.HardInvoiceMapper;
|
||||
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 lombok.RequiredArgsConstructor;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 尔印打印终端绑定和配置服务实现。
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class HardPrinterServiceImpl implements HardPrinterService {
|
||||
|
||||
/** 默认纸张宽度,单位毫米。 */
|
||||
private static final int DEFAULT_PAPER_WIDTH = 80;
|
||||
|
||||
/** 默认 GS v 0 图片光栅指令模式。 */
|
||||
private static final String IMAGE_MODE_GS_V0 = "GS_V0";
|
||||
|
||||
/** ESC * 图片指令兼容回退模式。 */
|
||||
private static final String IMAGE_MODE_ESC_STAR = "ESC_STAR";
|
||||
|
||||
/** 开关开启编码。 */
|
||||
private static final String ENABLED = "1";
|
||||
|
||||
/** 开关关闭编码。 */
|
||||
private static final String DISABLED = "0";
|
||||
|
||||
/** 绑定关系 Mapper。 */
|
||||
private final HardPrinterBindingMapper bindingMapper;
|
||||
|
||||
/** 打印机配置 Mapper。 */
|
||||
private final HardPrinterConfigMapper configMapper;
|
||||
|
||||
/** 设备 Mapper。 */
|
||||
private final HardManagerMapper hardManagerMapper;
|
||||
|
||||
/** 打印任务 Mapper。 */
|
||||
private final HardInvoiceMapper invoiceMapper;
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public HardPrinterBindingVo getBinding(Long hardId) {
|
||||
if (hardId == null) return null;
|
||||
HardPrinterBindingEntity binding = bindingMapper.selectOne(Wrappers.<HardPrinterBindingEntity>lambdaQuery()
|
||||
.and(wrapper -> wrapper.eq(HardPrinterBindingEntity::getSourceHardId, hardId)
|
||||
.or().eq(HardPrinterBindingEntity::getPrinterHardId, hardId))
|
||||
.last("limit 1"));
|
||||
return binding == null ? null : assemble(binding);
|
||||
}
|
||||
|
||||
/** {@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);
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public void enrichDevices(List<HardManagerVo> devices) {
|
||||
if (devices == null || devices.isEmpty()) return;
|
||||
List<Long> deviceIds = devices.stream().map(HardManagerVo::getId).filter(Objects::nonNull).toList();
|
||||
if (deviceIds.isEmpty()) return;
|
||||
List<HardPrinterBindingEntity> bindings = bindingMapper.selectList(
|
||||
Wrappers.<HardPrinterBindingEntity>lambdaQuery()
|
||||
.in(HardPrinterBindingEntity::getSourceHardId, deviceIds)
|
||||
.or().in(HardPrinterBindingEntity::getPrinterHardId, deviceIds));
|
||||
List<Long> relatedIds = java.util.stream.Stream.concat(deviceIds.stream(), bindings.stream()
|
||||
.flatMap(binding -> java.util.stream.Stream.of(binding.getSourceHardId(), binding.getPrinterHardId()))
|
||||
.filter(Objects::nonNull)).distinct().toList();
|
||||
Map<Long, HardManagerEntity> managerMap = hardManagerMapper.selectBatchIds(relatedIds).stream()
|
||||
.collect(Collectors.toMap(HardManagerEntity::getId, Function.identity(), (left, right) -> left));
|
||||
List<Long> printerIds = java.util.stream.Stream.concat(
|
||||
bindings.stream().map(HardPrinterBindingEntity::getPrinterHardId),
|
||||
devices.stream().filter(device -> HardTypeEnum.EY_PRINT.getType().equals(device.getType()))
|
||||
.map(HardManagerVo::getId))
|
||||
.filter(Objects::nonNull).distinct().toList();
|
||||
Map<Long, HardPrinterConfigEntity> configMap = printerIds.isEmpty() ? Map.of() : configMapper.selectList(
|
||||
Wrappers.<HardPrinterConfigEntity>lambdaQuery()
|
||||
.in(HardPrinterConfigEntity::getPrinterHardId, printerIds))
|
||||
.stream().collect(Collectors.toMap(HardPrinterConfigEntity::getPrinterHardId,
|
||||
Function.identity(), (left, right) -> left));
|
||||
Map<Long, HardPrinterBindingEntity> sourceBindingMap = bindings.stream()
|
||||
.collect(Collectors.toMap(HardPrinterBindingEntity::getSourceHardId,
|
||||
Function.identity(), (left, right) -> left));
|
||||
Map<Long, HardPrinterBindingEntity> printerBindingMap = bindings.stream()
|
||||
.collect(Collectors.toMap(HardPrinterBindingEntity::getPrinterHardId,
|
||||
Function.identity(), (left, right) -> left));
|
||||
for (HardManagerVo device : devices) {
|
||||
HardPrinterBindingEntity sourceBinding = sourceBindingMap.get(device.getId());
|
||||
HardPrinterBindingEntity printerBinding = printerBindingMap.get(device.getId());
|
||||
if (sourceBinding != null) {
|
||||
device.setBoundPrinterHardId(sourceBinding.getPrinterHardId());
|
||||
device.setBoundPrinterHardName(resolveName(managerMap.get(sourceBinding.getPrinterHardId())));
|
||||
}
|
||||
if (printerBinding != null) {
|
||||
device.setBoundSourceHardId(printerBinding.getSourceHardId());
|
||||
device.setBoundSourceHardName(resolveName(managerMap.get(printerBinding.getSourceHardId())));
|
||||
}
|
||||
Long printerId = HardTypeEnum.EY_PRINT.getType().equals(device.getType())
|
||||
? device.getId() : device.getBoundPrinterHardId();
|
||||
// 未绑定的普通设备没有目标打印机,不能使用空键访问 Map.of() 返回的不可变空映射。
|
||||
HardPrinterConfigEntity config = printerId == null ? null : configMap.get(printerId);
|
||||
if (config != null) fillPrinterConfig(device, config);
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public List<HardManagerEntity> listAvailablePrinters(Long sourceHardId) {
|
||||
HardManagerEntity source = requireSource(sourceHardId);
|
||||
List<Long> boundPrinterIds = bindingMapper.selectList(Wrappers.<HardPrinterBindingEntity>lambdaQuery()
|
||||
.select(HardPrinterBindingEntity::getPrinterHardId))
|
||||
.stream().map(HardPrinterBindingEntity::getPrinterHardId).filter(Objects::nonNull).toList();
|
||||
return hardManagerMapper.selectList(Wrappers.<HardManagerEntity>lambdaQuery()
|
||||
.eq(HardManagerEntity::getType, HardTypeEnum.EY_PRINT.getType())
|
||||
.eq(HardManagerEntity::getMerchantId, source.getMerchantId())
|
||||
.eq(HardManagerEntity::getDeptId, source.getDeptId())
|
||||
.notIn(!boundPrinterIds.isEmpty(), HardManagerEntity::getId, boundPrinterIds)
|
||||
.orderByAsc(HardManagerEntity::getName));
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public List<HardManagerEntity> listBoundSources(Long merchantId) {
|
||||
if (merchantId == null) throw new RuntimeException("商户不能为空");
|
||||
List<Long> sourceIds = bindingMapper.selectList(Wrappers.<HardPrinterBindingEntity>lambdaQuery()
|
||||
.eq(HardPrinterBindingEntity::getMerchantId, merchantId)
|
||||
.select(HardPrinterBindingEntity::getSourceHardId))
|
||||
.stream().map(HardPrinterBindingEntity::getSourceHardId).filter(Objects::nonNull).toList();
|
||||
if (sourceIds.isEmpty()) return List.of();
|
||||
return hardManagerMapper.selectListByScope(Wrappers.<HardManagerEntity>lambdaQuery()
|
||||
.in(HardManagerEntity::getId, sourceIds)
|
||||
.orderByAsc(HardManagerEntity::getName), DataScope.of());
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public HardPrinterBindingVo bind(HardPrinterBindingDto dto) {
|
||||
HardManagerEntity source = requireSource(dto.getSourceHardId());
|
||||
HardManagerEntity printer = requirePrinter(dto.getPrinterHardId());
|
||||
if (!Objects.equals(source.getTenantId(), printer.getTenantId())) {
|
||||
throw new RuntimeException("来源终端和打印终端必须属于同一租户");
|
||||
}
|
||||
if (!Objects.equals(source.getMerchantId(), printer.getMerchantId())) {
|
||||
throw new RuntimeException("来源终端和打印终端必须属于同一商户");
|
||||
}
|
||||
if (!Objects.equals(source.getDeptId(), printer.getDeptId())) {
|
||||
throw new RuntimeException("来源终端和打印终端必须属于同一商户部门");
|
||||
}
|
||||
HardPrinterBindingEntity binding = HardPrinterBindingEntity.builder()
|
||||
.sourceHardId(source.getId())
|
||||
.printerHardId(printer.getId())
|
||||
.merchantId(source.getMerchantId())
|
||||
.deptId(source.getDeptId())
|
||||
.tenantId(source.getTenantId())
|
||||
.build();
|
||||
try {
|
||||
bindingMapper.insert(binding);
|
||||
} catch (DuplicateKeyException exception) {
|
||||
throw new RuntimeException("来源终端或打印终端已经绑定,请先解除原绑定", exception);
|
||||
}
|
||||
if (configMapper.selectCount(Wrappers.<HardPrinterConfigEntity>lambdaQuery()
|
||||
.eq(HardPrinterConfigEntity::getPrinterHardId, printer.getId())) == 0) {
|
||||
configMapper.insert(defaultConfig(printer));
|
||||
}
|
||||
return assemble(binding);
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void unbind(Long sourceHardId) {
|
||||
HardPrinterBindingEntity binding = findBySourceHardId(sourceHardId);
|
||||
if (binding == null) return;
|
||||
if (hasPendingTask(binding.getSourceHardId(), binding.getPrinterHardId())) {
|
||||
throw new RuntimeException("存在待生成、待打印或打印失败任务,处理完成后才能解绑");
|
||||
}
|
||||
bindingMapper.deleteById(binding.getId());
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public HardPrinterConfigEntity getConfig(Long printerHardId) {
|
||||
HardManagerEntity printer = requirePrinter(printerHardId);
|
||||
HardPrinterConfigEntity config = configMapper.selectOne(Wrappers.<HardPrinterConfigEntity>lambdaQuery()
|
||||
.eq(HardPrinterConfigEntity::getPrinterHardId, printerHardId)
|
||||
.last("limit 1"));
|
||||
return config == null ? defaultConfig(printer) : config;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public HardPrinterConfigEntity saveConfig(HardPrinterConfigDto dto) {
|
||||
HardManagerEntity printer = requirePrinter(dto.getPrinterHardId());
|
||||
validateConfig(dto);
|
||||
HardPrinterConfigEntity existing = configMapper.selectOne(Wrappers.<HardPrinterConfigEntity>lambdaQuery()
|
||||
.eq(HardPrinterConfigEntity::getPrinterHardId, printer.getId())
|
||||
.last("limit 1"));
|
||||
HardPrinterConfigEntity config = BeanUtils.toBean(dto, HardPrinterConfigEntity.class);
|
||||
config.setTenantId(printer.getTenantId());
|
||||
if (existing == null) {
|
||||
configMapper.insert(config);
|
||||
} else {
|
||||
config.setId(existing.getId());
|
||||
configMapper.updateById(config);
|
||||
}
|
||||
return configMapper.selectById(config.getId());
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public HardPrinterConfigEntity reportDeviceInfo(Long printerHardId, HardPrinterInfoReportDto dto) {
|
||||
HardManagerEntity printer = requirePrinter(printerHardId);
|
||||
HardPrinterConfigEntity config = configMapper.selectOne(Wrappers.<HardPrinterConfigEntity>lambdaQuery()
|
||||
.eq(HardPrinterConfigEntity::getPrinterHardId, printer.getId())
|
||||
.last("limit 1"));
|
||||
boolean create = config == null;
|
||||
if (create) config = defaultConfig(printer);
|
||||
config.setPrinterModel(StrUtil.trim(dto.getPrinterModel()));
|
||||
config.setUsbVendorId(dto.getUsbVendorId());
|
||||
config.setUsbProductId(dto.getUsbProductId());
|
||||
if (create) {
|
||||
configMapper.insert(config);
|
||||
} else {
|
||||
configMapper.updateById(config);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public HardPrinterBindingEntity findBySourceHardId(Long sourceHardId) {
|
||||
if (sourceHardId == null || sourceHardId <= 0) return null;
|
||||
return bindingMapper.selectOne(Wrappers.<HardPrinterBindingEntity>lambdaQuery()
|
||||
.eq(HardPrinterBindingEntity::getSourceHardId, sourceHardId)
|
||||
.last("limit 1"));
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public void validateDeviceChange(HardManagerEntity before, HardManagerEntity after) {
|
||||
if (before == null || after == null || after.getId() == null) return;
|
||||
boolean changesBindingIdentity = !Objects.equals(before.getType(), after.getType())
|
||||
|| !Objects.equals(before.getMerchantId(), after.getMerchantId())
|
||||
|| !Objects.equals(before.getDeptId(), after.getDeptId());
|
||||
if (changesBindingIdentity && getBinding(before.getId()) != null) {
|
||||
throw new RuntimeException("已绑定打印关系的设备不能直接修改类型、商户或部门,请先处理任务并解绑");
|
||||
}
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public void validateDeviceRemoval(Long hardId) {
|
||||
HardPrinterBindingVo binding = getBinding(hardId);
|
||||
if (binding != null) {
|
||||
throw new RuntimeException("已绑定打印关系的设备不能删除,请先处理任务并解绑");
|
||||
}
|
||||
if (hasPendingTask(hardId, hardId)) {
|
||||
throw new RuntimeException("设备存在待生成、待打印或打印失败任务,不能删除");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并返回来源业务终端。
|
||||
*
|
||||
* @param hardId 来源设备 ID
|
||||
* @return 有效来源设备
|
||||
*/
|
||||
private HardManagerEntity requireSource(Long hardId) {
|
||||
HardManagerEntity source = hardManagerMapper.selectById(hardId);
|
||||
HardTypeEnum type = source == null ? null : HardTypeEnum.valueOfByType(source.getType());
|
||||
if (source == null || (type != HardTypeEnum.SUPERMARKET && type != HardTypeEnum.CANTEEN)) {
|
||||
throw new RuntimeException("来源终端必须是超市或食堂设备");
|
||||
}
|
||||
if (source.getMerchantId() == null) throw new RuntimeException("来源终端未绑定商户");
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并返回 EY_PRINT 打印终端。
|
||||
*
|
||||
* @param hardId 打印设备 ID
|
||||
* @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("目标设备必须是尔印打印终端");
|
||||
}
|
||||
if (printer.getMerchantId() == null) throw new RuntimeException("打印终端未绑定商户");
|
||||
return printer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验受首期协议约束的打印参数。
|
||||
*
|
||||
* @param dto 待保存配置
|
||||
*/
|
||||
private void validateConfig(HardPrinterConfigDto dto) {
|
||||
if (!Objects.equals(dto.getPaperWidth(), DEFAULT_PAPER_WIDTH)) {
|
||||
throw new RuntimeException("首期打印纸宽只支持80mm");
|
||||
}
|
||||
if (!ENABLED.equals(dto.getAutoCut()) && !DISABLED.equals(dto.getAutoCut())) {
|
||||
throw new RuntimeException("自动切刀开关只能为0或1");
|
||||
}
|
||||
if (!IMAGE_MODE_GS_V0.equals(dto.getImageMode()) && !IMAGE_MODE_ESC_STAR.equals(dto.getImageMode())) {
|
||||
throw new RuntimeException("图片指令模式只支持GS_V0或ESC_STAR");
|
||||
}
|
||||
if ((dto.getUsbVendorId() == null) != (dto.getUsbProductId() == null)) {
|
||||
throw new RuntimeException("USB VID和PID必须同时填写或同时留空");
|
||||
}
|
||||
if (dto.getUsbVendorId() != null && (dto.getUsbVendorId() < 0 || dto.getUsbVendorId() > 65535)) {
|
||||
throw new RuntimeException("USB VID必须在0到65535之间");
|
||||
}
|
||||
if (dto.getUsbProductId() != null && (dto.getUsbProductId() < 0 || dto.getUsbProductId() > 65535)) {
|
||||
throw new RuntimeException("USB PID必须在0到65535之间");
|
||||
}
|
||||
dto.setPrinterModel(StrUtil.trim(dto.getPrinterModel()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断来源或目标设备是否仍有关联的非终态任务。
|
||||
*
|
||||
* @param sourceHardId 来源设备 ID
|
||||
* @param printerHardId 目标设备 ID
|
||||
* @return 存在待生成、待打印或失败任务时返回 {@code true}
|
||||
*/
|
||||
private boolean hasPendingTask(Long sourceHardId, Long printerHardId) {
|
||||
List<String> pendingStatuses = List.of(HardPrintStatusEnum.PENDING_GENERATION.getType(),
|
||||
HardPrintStatusEnum.GENERATE.getType(), HardPrintStatusEnum.FAILED.getType());
|
||||
return invoiceMapper.selectCount(Wrappers.<HardInvoiceEntity>lambdaQuery()
|
||||
.in(HardInvoiceEntity::getStatus, pendingStatuses)
|
||||
.and(wrapper -> wrapper.eq(HardInvoiceEntity::getHardId, sourceHardId)
|
||||
.or().eq(HardInvoiceEntity::getPrinterHardId, printerHardId))) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取关联设备名称,不存在时返回空值以便前端展示未配置状态。
|
||||
*
|
||||
* @param manager 关联设备
|
||||
* @return 设备名称或空值
|
||||
*/
|
||||
private static String resolveName(HardManagerEntity manager) {
|
||||
return manager == null ? null : manager.getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* 把打印参数投影到设备分页行。
|
||||
*
|
||||
* @param target 设备分页行
|
||||
* @param config 当前打印配置
|
||||
*/
|
||||
private static void fillPrinterConfig(HardManagerVo target, HardPrinterConfigEntity config) {
|
||||
target.setPrinterModel(config.getPrinterModel());
|
||||
target.setUsbVendorId(config.getUsbVendorId());
|
||||
target.setUsbProductId(config.getUsbProductId());
|
||||
target.setPaperWidth(config.getPaperWidth());
|
||||
target.setAutoCut(config.getAutoCut());
|
||||
target.setImageMode(config.getImageMode());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建尚未持久化的首期默认打印配置。
|
||||
*
|
||||
* @param printer 打印终端
|
||||
* @return 默认配置
|
||||
*/
|
||||
private HardPrinterConfigEntity defaultConfig(HardManagerEntity printer) {
|
||||
return HardPrinterConfigEntity.builder()
|
||||
.printerHardId(printer.getId())
|
||||
.paperWidth(DEFAULT_PAPER_WIDTH)
|
||||
.autoCut(ENABLED)
|
||||
.imageMode(IMAGE_MODE_GS_V0)
|
||||
.tenantId(printer.getTenantId())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 装配绑定双方名称和打印机配置。
|
||||
*
|
||||
* @param binding 绑定实体
|
||||
* @return 管理端展示对象
|
||||
*/
|
||||
private HardPrinterBindingVo assemble(HardPrinterBindingEntity binding) {
|
||||
HardPrinterBindingVo result = BeanUtils.toBean(binding, HardPrinterBindingVo.class);
|
||||
HardManagerEntity source = hardManagerMapper.selectById(binding.getSourceHardId());
|
||||
HardManagerEntity printer = hardManagerMapper.selectById(binding.getPrinterHardId());
|
||||
if (source != null) {
|
||||
result.setSourceHardName(source.getName());
|
||||
result.setSourceHardType(source.getType());
|
||||
}
|
||||
if (printer != null) {
|
||||
result.setPrinterHardName(printer.getName());
|
||||
result.setPrinterConfig(getConfig(printer.getId()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+8
-2
@@ -66,7 +66,6 @@ public class ReservationMenuQueryServiceImpl implements ReservationMenuQueryServ
|
||||
/** 食堂菜品远程服务,用于批量补充订单商品类型和套餐组成。 */
|
||||
private final RemoteCanteenSpuService remoteCanteenSpuService;
|
||||
|
||||
/** {@inheritDoc} */
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public ReservationMenuQueryVo query(ReservationMenuQueryDto dto) {
|
||||
@@ -135,8 +134,15 @@ public class ReservationMenuQueryServiceImpl implements ReservationMenuQueryServ
|
||||
return cars != null && cars.stream().anyMatch(car -> resolveEffectiveProductCount(car) > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验可选 DEVICE-ID 是否属于订单商户,并保留缺少请求头时的空来源语义。
|
||||
*
|
||||
* @param order 待核销预约订单
|
||||
* @param hardId 请求头提供的来源设备 ID;未提供时为空
|
||||
* @return 已校验的来源设备 ID;请求头缺失时返回 {@code null}
|
||||
*/
|
||||
private Long resolveVerificationHardId(HardOrderEntity order, Long hardId) {
|
||||
if (hardId == null) return -1L;
|
||||
if (hardId == null) return null;
|
||||
HardManagerEntity hard = hardManagerService.getById(hardId);
|
||||
if (hard == null || !Objects.equals(hard.getMerchantId(), order.getMerchantId())) {
|
||||
throw new RuntimeException("当前设备不能核销该预约订单");
|
||||
|
||||
+36
@@ -17,6 +17,7 @@ import com.bsd.pjcloud.common.core.util.CollectionUtils;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.security.service.PjcloudUser;
|
||||
import com.bsd.pjcloud.hard.api.dto.HardGroupDto;
|
||||
import com.bsd.pjcloud.hard.api.constant.HardTypeEnum;
|
||||
import com.bsd.pjcloud.hard.api.dto.manager.HardManagerDto;
|
||||
import com.bsd.pjcloud.hard.api.dto.hard.CurrentUserHardTypeInfoDto;
|
||||
import com.bsd.pjcloud.hard.api.dto.hard.HardBindDto;
|
||||
@@ -28,6 +29,7 @@ import com.bsd.pjcloud.hard.mapper.HardGroupMapper;
|
||||
import com.bsd.pjcloud.hard.mapper.manager.HardManagerMapper;
|
||||
import com.bsd.pjcloud.hard.service.HardDoorManagerService;
|
||||
import com.bsd.pjcloud.hard.service.HardManagerService;
|
||||
import com.bsd.pjcloud.hard.service.HardPrinterService;
|
||||
import com.bsd.pjcloud.hard.service.HardTerminalStatusService;
|
||||
import com.bsd.pjcloud.hard.mqtt.config.MqttConfig;
|
||||
import com.bsd.pjcloud.hard.utils.MqTopicUtil;
|
||||
@@ -64,6 +66,9 @@ public class HardManagerServiceImpl extends ServiceImpl<HardManagerMapper, HardM
|
||||
private final RemoteMchService mchService;
|
||||
private final RemoteDeptService deptService;
|
||||
private final RemoteMqttEventService remoteMqttEventService;
|
||||
|
||||
/** 打印终端绑定服务,用于保护已绑定设备的关键属性和删除动作。 */
|
||||
private final HardPrinterService hardPrinterService;
|
||||
/**
|
||||
* 为已完成数据权限过滤的设备分页批量补充终端运行状态。
|
||||
*/
|
||||
@@ -123,6 +128,7 @@ public class HardManagerServiceImpl extends ServiceImpl<HardManagerMapper, HardM
|
||||
HardUtil.convertHardGroupList(list.getRecords(), CollectionUtils.convertMap(groupList, HardGroupEntity::getId));
|
||||
}
|
||||
}
|
||||
hardPrinterService.enrichDevices(list.getRecords());
|
||||
hardTerminalStatusService.enrich(list.getRecords());
|
||||
}
|
||||
return list;
|
||||
@@ -167,6 +173,7 @@ public class HardManagerServiceImpl extends ServiceImpl<HardManagerMapper, HardM
|
||||
if (ObjectUtil.isEmpty(detail)) {
|
||||
throw new RuntimeException("设备不存在");
|
||||
}
|
||||
hardPrinterService.validateDeviceChange(detail, dto);
|
||||
MchManageVo mch = mchService.getById(dto.getMerchantId()).getData();
|
||||
if (ObjectUtil.isEmpty(mch)) {
|
||||
throw new RuntimeException("商户不存在");
|
||||
@@ -194,12 +201,24 @@ public class HardManagerServiceImpl extends ServiceImpl<HardManagerMapper, HardM
|
||||
remoteMqttEventService.sendConfigChangeMsg(detail.getDeptId());
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public boolean removeWithPrinterCheck(List<Long> ids) {
|
||||
if (ids == null || ids.isEmpty()) return false;
|
||||
ids.forEach(hardPrinterService::validateDeviceRemoval);
|
||||
return removeBatchByIds(ids);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HardManagerVo bind(HardBindDto dto) {
|
||||
HardManagerEntity detail = baseMapper.selectById(dto.getId());
|
||||
if (ObjectUtil.isEmpty(detail)) {
|
||||
throw new RuntimeException("设备不存在");
|
||||
}
|
||||
if (HardTypeEnum.EY_PRINT.getType().equals(detail.getType())) {
|
||||
// 打印终端必须先由管理端完成一对一来源绑定,避免未归属设备登录后接收错误商户任务。
|
||||
hardPrinterService.getPrinterBinding(detail.getId());
|
||||
}
|
||||
if (existUniqueCode(dto.getUniqueCode(), dto.getId())) {
|
||||
throw new RuntimeException("该唯一编码已经被绑定");
|
||||
}
|
||||
@@ -211,11 +230,28 @@ public class HardManagerServiceImpl extends ServiceImpl<HardManagerMapper, HardM
|
||||
@Override
|
||||
public HardManagerVo getByHardId(Long hardId) {
|
||||
HardManagerEntity hard = baseMapper.selectById(hardId);
|
||||
if (ObjectUtil.isEmpty(hard)) {
|
||||
return null;
|
||||
}
|
||||
HardManagerVo vo = BeanUtils.toBean(hard, HardManagerVo.class);
|
||||
List<HardManagerVo> records = List.of(vo);
|
||||
if (ObjectUtil.isNotEmpty(hard.getDeptId())) {
|
||||
R<Map<Long, SysDept>> deptResponse = deptService.getDeptByDeptIds(List.of(hard.getDeptId()));
|
||||
if (ObjectUtil.isNotEmpty(deptResponse.getData())) {
|
||||
DataUtil.convertDept(vo, deptResponse.getData().get(hard.getDeptId()));
|
||||
}
|
||||
}
|
||||
MchManageVo mch = mchService.getById(hard.getMerchantId()).getData();
|
||||
if (ObjectUtil.isNotEmpty(mch)) {
|
||||
WalletDataUtil.convertMch(vo, mch);
|
||||
vo.setMerchantDeptId(mch.getDeptId());
|
||||
}
|
||||
if (ObjectUtil.isNotEmpty(hard.getHardGroupId())) {
|
||||
HardUtil.convertHardGroup(vo, hardGroupMapper.selectById(hard.getHardGroupId()));
|
||||
}
|
||||
// 单设备详情与分页列表保持同一运行语义,避免刷新详情页后丢失绑定和心跳状态。
|
||||
hardPrinterService.enrichDevices(records);
|
||||
hardTerminalStatusService.enrich(records);
|
||||
return vo;
|
||||
}
|
||||
|
||||
|
||||
+4
-3
@@ -147,13 +147,14 @@ public class HardTerminalStatusServiceImpl extends ServiceImpl<HardTerminalStatu
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断设备是否属于支持在线监控的超市或食堂 Android 终端。
|
||||
* 判断设备是否属于支持在线监控的收银或尔印打印 Android 终端。
|
||||
*
|
||||
* @param hardType {@code hard_manager.type} 的持久化编码
|
||||
* @return 超市或食堂设备返回 {@code true}
|
||||
* @return 超市、食堂或尔印打印设备返回 {@code true}
|
||||
*/
|
||||
private boolean isSupported(String hardType) {
|
||||
return ObjectUtil.equals(HardTypeEnum.SUPERMARKET.getType(), hardType)
|
||||
|| ObjectUtil.equals(HardTypeEnum.CANTEEN.getType(), hardType);
|
||||
|| ObjectUtil.equals(HardTypeEnum.CANTEEN.getType(), hardType)
|
||||
|| ObjectUtil.equals(HardTypeEnum.EY_PRINT.getType(), hardType);
|
||||
}
|
||||
}
|
||||
+39
-7
@@ -434,13 +434,19 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台订单详情在原有关联信息基础上补充长期识别证据,App 与终端详情继续使用不含证据的 join。
|
||||
* 后台订单详情补充长期识别证据和外卖送达配置,App 与终端继续使用不含这些扩展信息的 join。
|
||||
*/
|
||||
@Override
|
||||
public HardOrderVo joinWithEvidence(Long id) {
|
||||
HardOrderVo detail = getSelf().join(id);
|
||||
if (detail != null) {
|
||||
detail.setEvidenceImages(hardOrderEvidenceImageService.listByOrderId(id));
|
||||
// 外卖期望送达配置独立存储,仅后台详情按订单主键补充,避免扩大 App 与终端查询开销。
|
||||
if (ObjectUtil.equals(detail.getOrderType(), HardOrderTypeEnum.TAKE_OUT.getType())) {
|
||||
detail.setTakeOutAppt(hardTakeOutApptService.getOne(
|
||||
new LambdaQueryWrapper<HardTakeOutApptEntity>().eq(HardTakeOutApptEntity::getOrderId, id),
|
||||
false));
|
||||
}
|
||||
}
|
||||
return detail;
|
||||
}
|
||||
@@ -3945,10 +3951,8 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
|
||||
.orderStatus(HardOrderStatusEnum.CANCEL.getType())
|
||||
.verificationTime(verificationTime)
|
||||
.build();
|
||||
boolean isMealVerification = ObjectUtil.equals(order.getSysType(), HardTypeEnum.CANTEEN.getType())
|
||||
&& ObjectUtil.equals(order.getOrderType(), HardOrderTypeEnum.RESERVE.getType())
|
||||
&& ObjectUtil.equals(order.getOrderStatus(), HardOrderStatusEnum.PAY.getType());
|
||||
int updated = isMealVerification
|
||||
boolean isReservationVerification = isReservationVerification(order);
|
||||
int updated = isReservationVerification
|
||||
? baseMapper.update(updateObj, new LambdaUpdateWrapper<HardOrderEntity>()
|
||||
.eq(HardOrderEntity::getId, order.getId())
|
||||
.eq(HardOrderEntity::getOrderStatus, HardOrderStatusEnum.PAY.getType()))
|
||||
@@ -3956,10 +3960,37 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
|
||||
if (updated == 0) {
|
||||
throw new I18nBizException(ErrorCodes.HARD_VERIFY_STATUS_CHANGED);
|
||||
}
|
||||
if (isMealVerification) {
|
||||
if (isReservationVerification) createReservationVerificationArtifacts(order, verificationTime, dto.getHardwareId());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断已加锁订单是否为超市或食堂的待核销预约,统一限定状态条件更新和兑换券任务创建范围。
|
||||
*
|
||||
* @param order 已持有数据库行锁的订单快照
|
||||
* @return 超市或食堂预约订单仍为已支付状态时返回 {@code true}
|
||||
*/
|
||||
private static boolean isReservationVerification(HardOrderEntity order) {
|
||||
return order != null
|
||||
&& List.of(HardTypeEnum.SUPERMARKET.getType(), HardTypeEnum.CANTEEN.getType()).contains(order.getSysType())
|
||||
&& ObjectUtil.equals(order.getOrderType(), HardOrderTypeEnum.RESERVE.getType())
|
||||
&& ObjectUtil.equals(order.getOrderStatus(), HardOrderStatusEnum.PAY.getType());
|
||||
}
|
||||
|
||||
/**
|
||||
* 在核销事务内创建预约兑换券;食堂专属取餐消息继续只为食堂订单生成。
|
||||
*
|
||||
* @param order 已完成业务校验的预约订单
|
||||
* @param verificationTime 本次核销时间
|
||||
* @param sourceHardId DEVICE-ID 解析出的实际核销终端;缺失时为空
|
||||
*/
|
||||
private void createReservationVerificationArtifacts(HardOrderEntity order, LocalDateTime verificationTime,
|
||||
Long sourceHardId) {
|
||||
if (ObjectUtil.equals(order.getSysType(), HardTypeEnum.CANTEEN.getType())) {
|
||||
createMealVerifiedOutbox(order, verificationTime);
|
||||
}
|
||||
return true;
|
||||
// 打印任务与核销状态同事务提交;图片、文件和MQTT仅在提交后异步执行。
|
||||
hardInvoiceService.createRedemptionVoucherTask(order, sourceHardId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3987,6 +4018,7 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
|
||||
throw new I18nBizException(ErrorCodes.HARD_ROLLBACK_STATUS_CHANGED);
|
||||
}
|
||||
|
||||
hardInvoiceService.cancelPendingVoucherTasks(order.getId());
|
||||
if (ObjectUtil.equals(order.getSysType(), HardTypeEnum.CANTEEN.getType())) {
|
||||
stopPendingMealVerifiedOutbox(order);
|
||||
hardBroadcastService.rollbackDoneBroadcast(order);
|
||||
|
||||
+30
-1
@@ -6,8 +6,17 @@
|
||||
<id property="id" column="id"/>
|
||||
<result property="deptId" column="dept_id"/>
|
||||
<result property="hardId" column="hard_id"/>
|
||||
<result property="orderId" column="hard_order_id"/>
|
||||
<result property="orderId" column="order_id"/>
|
||||
<result property="printerHardId" column="printer_hard_id"/>
|
||||
<result property="printType" column="print_type"/>
|
||||
<result property="printNo" column="print_no"/>
|
||||
<result property="retryVersion" column="retry_version"/>
|
||||
<result property="attemptCount" column="attempt_count"/>
|
||||
<result property="status" column="status"/>
|
||||
<result property="failureStage" column="failure_stage"/>
|
||||
<result property="lastError" column="last_error"/>
|
||||
<result property="printedAt" column="printed_at"/>
|
||||
<result property="reprintReason" column="reprint_reason"/>
|
||||
<result property="pic" column="pic"/>
|
||||
<result property="createBy" column="create_by"/>
|
||||
<result property="createTime" column="create_time"/>
|
||||
@@ -21,8 +30,17 @@
|
||||
select a.id,
|
||||
a.dept_id as invoiceDeptId,
|
||||
a.hard_id,
|
||||
a.printer_hard_id,
|
||||
a.order_id,
|
||||
a.print_type,
|
||||
a.print_no,
|
||||
a.retry_version,
|
||||
a.attempt_count,
|
||||
a.status,
|
||||
a.failure_stage,
|
||||
a.last_error,
|
||||
a.printed_at,
|
||||
a.reprint_reason,
|
||||
a.pic,
|
||||
a.create_by,
|
||||
a.create_time,
|
||||
@@ -34,15 +52,25 @@
|
||||
|
||||
<sql id="whereA">
|
||||
a.del_flag = '0'
|
||||
<if test="dto.orderId != null">
|
||||
and a.order_id = #{dto.orderId}
|
||||
</if>
|
||||
<if test="dto.createTimeList != null and dto.createTimeList.length == 2">
|
||||
and a.create_time between #{dto.createTimeList[0]} and #{dto.createTimeList[1]}
|
||||
</if>
|
||||
<if test="dto.printType != null and dto.printType != ''">
|
||||
and a.print_type = #{dto.printType}
|
||||
</if>
|
||||
<if test="dto.status != null and dto.status != ''">
|
||||
and a.status = #{dto.status}
|
||||
</if>
|
||||
</sql>
|
||||
|
||||
<select id="scopePageList" resultType="com.bsd.pjcloud.hard.api.vo.HardInvoiceVo">
|
||||
<include refid="selectA"/>
|
||||
m.name as hardName,
|
||||
m.type as hardType,
|
||||
printer.name as printerHardName,
|
||||
o.hard_id as orderHardId,
|
||||
o.user_id as orderUserId,
|
||||
o.user_id as userId,
|
||||
@@ -57,6 +85,7 @@
|
||||
o.pay_time as orderPayTime
|
||||
from hard_invoice a
|
||||
left join hard_manager m on m.id = a.hard_id
|
||||
left join hard_manager printer on printer.id = a.printer_hard_id
|
||||
left join hard_order o on o.id = a.order_id
|
||||
<where>
|
||||
<include refid="whereA"/>
|
||||
|
||||
+6
@@ -99,6 +99,12 @@
|
||||
#{v}
|
||||
</foreach>
|
||||
</if>
|
||||
<if test="dto.orderUserIds != null and dto.orderUserIds.size() != 0">
|
||||
and o.user_id in
|
||||
<foreach collection="dto.orderUserIds" item="v" open="(" close=")" separator=",">
|
||||
#{v}
|
||||
</foreach>
|
||||
</if>
|
||||
<if test="dto.orderStatusCodes != null and dto.orderStatusCodes.size() != 0">
|
||||
and o.order_status in
|
||||
<foreach collection="dto.orderStatusCodes" item="v" open="(" close=")" separator=",">
|
||||
|
||||
+23
-1
@@ -355,7 +355,15 @@
|
||||
m.dept_id as hardTypeDeptId,
|
||||
s.id as afterSaleId,
|
||||
s.no as afterSaleNo,
|
||||
p.user_id as payerUserId
|
||||
p.user_id as payerUserId,
|
||||
latest_invoice.id as latestInvoiceTaskId,
|
||||
latest_invoice.status as latestInvoiceStatus,
|
||||
latest_invoice.print_no as latestInvoicePrintNo,
|
||||
latest_invoice.hard_id as latestInvoiceSourceHardId,
|
||||
latest_voucher.id as latestVoucherTaskId,
|
||||
latest_voucher.status as latestVoucherStatus,
|
||||
latest_voucher.print_no as latestVoucherPrintNo,
|
||||
latest_voucher.hard_id as latestVoucherSourceHardId
|
||||
from hard_order a
|
||||
left join hard_manager m on m.id = a.hard_id
|
||||
<!-- 多次退款后只关联最新售后单,避免订单列表被一对多关联放大并返回不确定的售后 ID。 -->
|
||||
@@ -366,6 +374,20 @@
|
||||
and latest_sale.del_flag = '0'
|
||||
)
|
||||
left join hard_parent_car p on p.id = a.parent_car_id and p.del_flag = '0'
|
||||
left join hard_invoice latest_invoice on latest_invoice.id = (
|
||||
select max(invoice_task.id)
|
||||
from hard_invoice invoice_task
|
||||
where invoice_task.order_id = a.id
|
||||
and (invoice_task.print_type = '1' or invoice_task.print_type is null)
|
||||
and invoice_task.del_flag = '0'
|
||||
)
|
||||
left join hard_invoice latest_voucher on latest_voucher.id = (
|
||||
select max(voucher_task.id)
|
||||
from hard_invoice voucher_task
|
||||
where voucher_task.order_id = a.id
|
||||
and voucher_task.print_type = '2'
|
||||
and voucher_task.del_flag = '0'
|
||||
)
|
||||
<where>
|
||||
<include refid="whereA"/>
|
||||
<include refid="appVisibilityWhere"/>
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package com.bsd.pjcloud.hard.api.utils;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/** 80mm 发票与兑换券 PNG 的宽度、换行、层级和动态高度测试。 */
|
||||
class ThermalReceiptImageTest {
|
||||
|
||||
@Test
|
||||
void longProductNamesGrowCanvasAndKeepQuantityInRightColumn() throws Exception {
|
||||
ThermalReceiptImage receipt = new ThermalReceiptImage()
|
||||
.title("预约核销兑换券")
|
||||
.banner("第 2 次打印")
|
||||
.sectionTitle("剩余可履约商品");
|
||||
String longName = "超长套餐主商品名称".repeat(12);
|
||||
for (int index = 0; index < 24; index++) {
|
||||
receipt.item(longName + index, "x12", false, true)
|
||||
.item("套餐组成商品" + index, "x24", true, false);
|
||||
}
|
||||
|
||||
BufferedImage image = ImageIO.read(new ByteArrayInputStream(receipt.toPngBytes()));
|
||||
|
||||
assertEquals(ThermalReceiptImage.PAPER_WIDTH_PIXELS, image.getWidth());
|
||||
assertTrue(image.getHeight() > 512, "多行商品应触发画布动态扩容");
|
||||
assertTrue(hasDarkPixel(image, 480, image.getWidth(), 0, image.getHeight()),
|
||||
"数量列应在右侧保留独立对齐区域");
|
||||
}
|
||||
|
||||
@Test
|
||||
void invoiceAndVoucherImagesAlwaysUse576PixelPaperWidth() throws Exception {
|
||||
BufferedImage invoice = ImageIO.read(new ByteArrayInputStream(new ThermalReceiptImage()
|
||||
.title("发票").invoiceItem("独立单品", "2", "¥12.00").toPngBytes()));
|
||||
BufferedImage voucher = ImageIO.read(new ByteArrayInputStream(new ThermalReceiptImage()
|
||||
.title("兑换券").banner("第 1 次打印").verificationCode("123456").toPngBytes()));
|
||||
|
||||
assertEquals(576, invoice.getWidth());
|
||||
assertEquals(576, voucher.getWidth());
|
||||
}
|
||||
|
||||
private static boolean hasDarkPixel(BufferedImage image, int fromX, int toX, int fromY, int toY) {
|
||||
for (int y = fromY; y < toY; y++) {
|
||||
for (int x = fromX; x < toX; x++) {
|
||||
int rgb = image.getRGB(x, y) & 0xFFFFFF;
|
||||
if (rgb < 0x606060) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+28
-1
@@ -2,6 +2,8 @@ package com.bsd.pjcloud.hard.controller.hard;
|
||||
|
||||
import com.bsd.pjcloud.hard.api.constant.HardTypeEnum;
|
||||
import com.bsd.pjcloud.hard.api.constant.order.HardOrderTypeEnum;
|
||||
import com.bsd.pjcloud.hard.api.dto.HardPrinterInfoReportDto;
|
||||
import com.bsd.pjcloud.hard.api.entity.HardPrinterConfigEntity;
|
||||
import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity;
|
||||
import com.bsd.pjcloud.hard.api.vo.order.HardCarVo;
|
||||
import com.bsd.pjcloud.hard.service.HardApiService;
|
||||
@@ -11,6 +13,7 @@ import com.bsd.pjcloud.hard.service.HardManagerService;
|
||||
import com.bsd.pjcloud.hard.service.HardOrderService;
|
||||
import com.bsd.pjcloud.hard.service.HardOrderSyncService;
|
||||
import com.bsd.pjcloud.hard.service.HardOrderTotalService;
|
||||
import com.bsd.pjcloud.hard.service.HardPrinterService;
|
||||
import com.bsd.pjcloud.hard.service.HardTerminalStatusService;
|
||||
import com.bsd.pjcloud.hard.service.HardTerminalFaceService;
|
||||
import com.bsd.pjcloud.hard.service.ReservationMenuQueryService;
|
||||
@@ -20,7 +23,9 @@ import org.junit.jupiter.api.Test;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
@@ -34,6 +39,9 @@ class HardApiControllerTest {
|
||||
/** 发票服务,本组测试不调用。 */
|
||||
private final HardInvoiceService hardInvoiceService = mock(HardInvoiceService.class);
|
||||
|
||||
/** 尔印打印绑定服务,本组购物车测试不调用。 */
|
||||
private final HardPrinterService hardPrinterService = mock(HardPrinterService.class);
|
||||
|
||||
/** 购物车服务,提供带退款统计的订单商品。 */
|
||||
private final HardCarService hardCarService = mock(HardCarService.class);
|
||||
|
||||
@@ -63,7 +71,7 @@ class HardApiControllerTest {
|
||||
|
||||
/** 被测 Android 硬件接口控制器。 */
|
||||
private final HardApiController controller = new HardApiController(hardManagerService, hardInvoiceService,
|
||||
hardCarService, hardOrderService, hardOrderSyncService, hardApiService, hardOrderTotalService,
|
||||
hardPrinterService, hardCarService, hardOrderService, hardOrderSyncService, hardApiService, hardOrderTotalService,
|
||||
reservationMenuQueryService, verificationRecordService, hardTerminalFaceService,
|
||||
hardTerminalStatusService);
|
||||
|
||||
@@ -87,6 +95,25 @@ class HardApiControllerTest {
|
||||
assertEquals(10, result.get(0).getOriginalProductCount());
|
||||
}
|
||||
|
||||
/** 验证上报目标只使用可信 DEVICE-ID,并将服务保存结果原样返回。 */
|
||||
@Test
|
||||
void reportPrinterInfoUsesTrustedDeviceHeader() {
|
||||
HardPrinterInfoReportDto request = new HardPrinterInfoReportDto();
|
||||
request.setPrinterModel("GEZHI / micro-printer");
|
||||
request.setUsbVendorId(10473);
|
||||
request.setUsbProductId(649);
|
||||
HardPrinterConfigEntity saved = HardPrinterConfigEntity.builder()
|
||||
.printerHardId(20L).printerModel(request.getPrinterModel())
|
||||
.usbVendorId(10473).usbProductId(649).paperWidth(80)
|
||||
.autoCut("1").imageMode("GS_V0").build();
|
||||
when(hardPrinterService.reportDeviceInfo(20L, request)).thenReturn(saved);
|
||||
|
||||
HardPrinterConfigEntity result = (HardPrinterConfigEntity) controller.reportPrinterInfo(20L, request).getData();
|
||||
|
||||
assertSame(saved, result);
|
||||
verify(hardPrinterService).reportDeviceInfo(20L, request);
|
||||
}
|
||||
|
||||
/** 构造带原购、剩余数量和原行金额的购物车展示对象。 */
|
||||
private static HardCarVo car(int originalCount, int effectiveCount, long totalPrice) {
|
||||
HardCarVo car = new HardCarVo();
|
||||
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
package com.bsd.pjcloud.hard.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
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.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.HardOrderEntity;
|
||||
import com.bsd.pjcloud.hard.mapper.HardInvoiceMapper;
|
||||
import com.bsd.pjcloud.hard.mapper.order.HardOrderMapper;
|
||||
import com.bsd.pjcloud.hard.service.HardManagerService;
|
||||
import com.bsd.pjcloud.hard.service.HardOrderService;
|
||||
import com.bsd.pjcloud.hard.service.HardPrinterService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Answers.CALLS_REAL_METHODS;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 打印任务手工创建、失败重试和终端回执的并发保护测试。
|
||||
*/
|
||||
class HardInvoiceServiceImplTest {
|
||||
|
||||
private final HardInvoiceMapper invoiceMapper = mock(HardInvoiceMapper.class);
|
||||
private final HardOrderMapper orderMapper = mock(HardOrderMapper.class);
|
||||
private final HardOrderService orderService = mock(HardOrderService.class);
|
||||
private final HardManagerService managerService = mock(HardManagerService.class);
|
||||
private final HardPrinterService printerService = mock(HardPrinterService.class);
|
||||
private final ThreadPoolTaskExecutor taskExecutor = mock(ThreadPoolTaskExecutor.class);
|
||||
private final HardInvoiceServiceImpl service = mock(HardInvoiceServiceImpl.class, CALLS_REAL_METHODS);
|
||||
|
||||
/** 只注入本组测试访问的依赖,其他外部服务保持隔离。 */
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), "test"),
|
||||
HardInvoiceEntity.class);
|
||||
ReflectionTestUtils.setField(service, "baseMapper", invoiceMapper);
|
||||
ReflectionTestUtils.setField(service, "hardOrderMapper", orderMapper);
|
||||
ReflectionTestUtils.setField(service, "orderService", orderService);
|
||||
ReflectionTestUtils.setField(service, "hardManagerService", managerService);
|
||||
ReflectionTestUtils.setField(service, "printerService", printerService);
|
||||
ReflectionTestUtils.setField(service, "hardTaskExecutor", taskExecutor);
|
||||
}
|
||||
|
||||
/** 手工创建任务必须先锁定订单,已有待处理任务时直接复用。 */
|
||||
@Test
|
||||
void createPrintTaskLocksOrderAndReusesOpenTask() {
|
||||
HardOrderEntity order = order();
|
||||
HardInvoiceEntity pending = task(100L, HardPrintStatusEnum.PENDING_GENERATION, 0);
|
||||
when(orderMapper.selectByIdForUpdate(1L)).thenReturn(order);
|
||||
when(invoiceMapper.selectOne(any(Wrapper.class))).thenReturn(pending);
|
||||
HardPrintRequestDto request = new HardPrintRequestDto();
|
||||
request.setOrderId(1L);
|
||||
request.setPrintType(HardPrintTypeEnum.INVOICE.getType());
|
||||
|
||||
HardInvoiceEntity result = service.createPrintTask(request);
|
||||
|
||||
assertSame(pending, result);
|
||||
verify(orderMapper).selectByIdForUpdate(1L);
|
||||
verify(invoiceMapper, never()).insert(any(HardInvoiceEntity.class));
|
||||
}
|
||||
|
||||
/** 两个重试请求读到同一失败版本时,未抢到更新的请求不得再次唤醒调度。 */
|
||||
@Test
|
||||
void retryTaskSchedulesOnlyWhenConditionalUpdateWins() {
|
||||
HardInvoiceEntity failed = task(100L, HardPrintStatusEnum.FAILED, 2);
|
||||
failed.setHardId(10L);
|
||||
failed.setPic("voucher.png");
|
||||
HardInvoiceEntity concurrent = task(100L, HardPrintStatusEnum.GENERATE, 3);
|
||||
concurrent.setHardId(10L);
|
||||
concurrent.setPrinterHardId(20L);
|
||||
when(invoiceMapper.selectById(100L)).thenReturn(failed, concurrent);
|
||||
when(orderService.getById(1L)).thenReturn(order());
|
||||
when(managerService.getById(10L)).thenReturn(source());
|
||||
when(printerService.findBySourceHardId(10L)).thenReturn(binding());
|
||||
when(invoiceMapper.update(isNull(), any(Wrapper.class))).thenReturn(0);
|
||||
|
||||
HardInvoiceEntity result = service.retryTask(100L, 10L);
|
||||
|
||||
assertSame(concurrent, result);
|
||||
verify(taskExecutor, never()).execute(any(Runnable.class));
|
||||
}
|
||||
|
||||
/** 重打指定的旧记录已被并发请求生成下一任务时,直接复用新任务。 */
|
||||
@Test
|
||||
void reprintTaskReusesConcurrentOpenTask() {
|
||||
HardInvoiceEntity printed = task(100L, HardPrintStatusEnum.PRINT, 0);
|
||||
HardInvoiceEntity pending = task(101L, HardPrintStatusEnum.PENDING_GENERATION, 0);
|
||||
pending.setPrintNo(2);
|
||||
when(invoiceMapper.selectById(100L)).thenReturn(printed, printed);
|
||||
when(orderMapper.selectByIdForUpdate(1L)).thenReturn(order());
|
||||
when(invoiceMapper.selectOne(any(Wrapper.class))).thenReturn(pending);
|
||||
|
||||
HardInvoiceEntity result = service.reprintTask(100L, "纸张损坏");
|
||||
|
||||
assertSame(pending, result);
|
||||
verify(orderMapper).selectByIdForUpdate(1L);
|
||||
verify(invoiceMapper, never()).insert(any(HardInvoiceEntity.class));
|
||||
}
|
||||
|
||||
/** 回执写入条件必须排除已取消,避免核销回退后的迟到成功回执复活任务。 */
|
||||
@Test
|
||||
void reportPrintResultExcludesCancelledStatusAtUpdateTime() {
|
||||
HardInvoiceEntity pending = task(100L, HardPrintStatusEnum.GENERATE, 2);
|
||||
pending.setPrinterHardId(20L);
|
||||
when(managerService.getById(20L)).thenReturn(printer());
|
||||
when(invoiceMapper.selectById(100L)).thenReturn(pending);
|
||||
when(invoiceMapper.update(isNull(), any(Wrapper.class))).thenReturn(0);
|
||||
HardPrintResultDto result = new HardPrintResultDto();
|
||||
result.setTaskId(100L);
|
||||
result.setOrderId(1L);
|
||||
result.setRetryVersion(2);
|
||||
result.setSuccess(true);
|
||||
result.setAttemptCount(1);
|
||||
|
||||
assertFalse(service.reportPrintResult(20L, result));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<LambdaUpdateWrapper<HardInvoiceEntity>> wrapperCaptor =
|
||||
ArgumentCaptor.forClass(LambdaUpdateWrapper.class);
|
||||
verify(invoiceMapper).update(isNull(), wrapperCaptor.capture());
|
||||
wrapperCaptor.getValue().getSqlSegment();
|
||||
Map<String, Object> values = wrapperCaptor.getValue().getParamNameValuePairs();
|
||||
assertTrue(values.containsValue(HardPrintStatusEnum.CANCELLED.getType()));
|
||||
}
|
||||
|
||||
/** 已打印后到达的失败回执幂等返回成功,不能再写回失败状态。 */
|
||||
@Test
|
||||
void lateFailureCannotOverwritePrintedTask() {
|
||||
HardInvoiceEntity printed = task(100L, HardPrintStatusEnum.PRINT, 2);
|
||||
printed.setPrinterHardId(20L);
|
||||
when(managerService.getById(20L)).thenReturn(printer());
|
||||
when(invoiceMapper.selectById(100L)).thenReturn(printed);
|
||||
HardPrintResultDto result = new HardPrintResultDto();
|
||||
result.setTaskId(100L);
|
||||
result.setOrderId(1L);
|
||||
result.setRetryVersion(2);
|
||||
result.setSuccess(false);
|
||||
result.setAttemptCount(3);
|
||||
result.setErrorMessage("late error");
|
||||
|
||||
assertTrue(service.reportPrintResult(20L, result));
|
||||
verify(invoiceMapper, never()).update(isNull(), any(Wrapper.class));
|
||||
}
|
||||
|
||||
private static HardOrderEntity order() {
|
||||
return HardOrderEntity.builder().id(1L).hardId(10L).merchantId(30L).deptId(40L).tenantId(50L).build();
|
||||
}
|
||||
|
||||
private static HardManagerEntity source() {
|
||||
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();
|
||||
}
|
||||
|
||||
private static HardInvoiceEntity task(Long id, HardPrintStatusEnum status, int retryVersion) {
|
||||
return HardInvoiceEntity.builder().id(id).orderId(1L).printType(HardPrintTypeEnum.INVOICE.getType())
|
||||
.printNo(1).retryVersion(retryVersion).status(status.getType()).build();
|
||||
}
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
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.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 com.bsd.pjcloud.hard.mapper.HardPrinterBindingMapper;
|
||||
import com.bsd.pjcloud.hard.mapper.HardPrinterConfigMapper;
|
||||
import com.bsd.pjcloud.hard.mapper.manager.HardManagerMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Answers.CALLS_REAL_METHODS;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/** 尔印设备类型与 USB 配置约束测试。 */
|
||||
class HardPrinterServiceImplTest {
|
||||
|
||||
private final HardManagerMapper managerMapper = mock(HardManagerMapper.class);
|
||||
/** 打印配置数据访问对象,用于验证终端上报只修改允许字段。 */
|
||||
private final HardPrinterConfigMapper configMapper = mock(HardPrinterConfigMapper.class);
|
||||
|
||||
/** 使用真实业务方法和注入 Mapper 的被测服务。 */
|
||||
private final HardPrinterServiceImpl service = mock(HardPrinterServiceImpl.class, CALLS_REAL_METHODS);
|
||||
|
||||
/** 为每个用例注入相同 Mapper,并准备一台有效的 EY_PRINT 设备。 */
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
ReflectionTestUtils.setField(service, "hardManagerMapper", managerMapper);
|
||||
ReflectionTestUtils.setField(service, "configMapper", configMapper);
|
||||
when(managerMapper.selectById(20L)).thenReturn(HardManagerEntity.builder()
|
||||
.id(20L).type(HardTypeEnum.EY_PRINT.getType()).merchantId(30L).tenantId(50L).build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void hardTypeRecognizesOnlyErYinPrinter() {
|
||||
assertEquals("12", HardTypeEnum.EY_PRINT.getType());
|
||||
assertEquals("尔印打印机器", HardTypeEnum.EY_PRINT.getDescription());
|
||||
assertTrue(HardTypeEnum.isPrintHard(HardTypeEnum.EY_PRINT));
|
||||
assertFalse(HardTypeEnum.isPrintHard(HardTypeEnum.CANTEEN));
|
||||
assertFalse(HardTypeEnum.isPrintHard(null));
|
||||
}
|
||||
|
||||
/** 未绑定打印机的普通设备应保留空配置,不能因空打印机 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());
|
||||
when(bindingMapper.selectList(any())).thenReturn(List.of());
|
||||
when(managerMapper.selectBatchIds(List.of(10L))).thenReturn(List.of(
|
||||
HardManagerEntity.builder().id(10L).type(HardTypeEnum.SUPERMARKET.getType()).build()));
|
||||
|
||||
assertDoesNotThrow(() -> service.enrichDevices(List.of(device)));
|
||||
assertNull(device.getPrinterModel());
|
||||
}
|
||||
|
||||
@Test
|
||||
void usbVidAndPidMustBeConfiguredTogether() {
|
||||
HardPrinterConfigDto config = validConfig();
|
||||
config.setUsbVendorId(1234);
|
||||
|
||||
RuntimeException exception = assertThrows(RuntimeException.class, () -> service.saveConfig(config));
|
||||
|
||||
assertEquals("USB VID和PID必须同时填写或同时留空", exception.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void usbIdsMustFitUnsignedSixteenBitRange() {
|
||||
HardPrinterConfigDto config = validConfig();
|
||||
config.setUsbVendorId(65536);
|
||||
config.setUsbProductId(1);
|
||||
|
||||
RuntimeException exception = assertThrows(RuntimeException.class, () -> service.saveConfig(config));
|
||||
|
||||
assertEquals("USB VID必须在0到65535之间", exception.getMessage());
|
||||
}
|
||||
|
||||
/** 终端重新上报真机参数时必须保留管理端维护的纸宽、切刀和图片模式。 */
|
||||
@Test
|
||||
void reportDeviceInfoPreservesEscPosConfiguration() {
|
||||
HardPrinterConfigEntity existing = HardPrinterConfigEntity.builder()
|
||||
.id(100L).printerHardId(20L).printerModel("旧型号")
|
||||
.usbVendorId(1).usbProductId(2).paperWidth(80)
|
||||
.autoCut("0").imageMode("ESC_STAR").tenantId(50L).build();
|
||||
when(configMapper.selectOne(any())).thenReturn(existing);
|
||||
|
||||
HardPrinterConfigEntity result = service.reportDeviceInfo(20L, reportRequest());
|
||||
|
||||
assertEquals("GEZHI / micro-printer", result.getPrinterModel());
|
||||
assertEquals(10473, result.getUsbVendorId());
|
||||
assertEquals(649, result.getUsbProductId());
|
||||
assertEquals(80, result.getPaperWidth());
|
||||
assertEquals("0", result.getAutoCut());
|
||||
assertEquals("ESC_STAR", result.getImageMode());
|
||||
verify(configMapper).updateById(existing);
|
||||
}
|
||||
|
||||
/** 首次上报时应创建默认 80mm、自动切刀和 GS v 0 配置。 */
|
||||
@Test
|
||||
void reportDeviceInfoCreatesDefaultConfiguration() {
|
||||
when(configMapper.selectOne(any())).thenReturn(null);
|
||||
|
||||
HardPrinterConfigEntity result = service.reportDeviceInfo(20L, reportRequest());
|
||||
|
||||
assertEquals(20L, result.getPrinterHardId());
|
||||
assertEquals("GEZHI / micro-printer", result.getPrinterModel());
|
||||
assertEquals(10473, result.getUsbVendorId());
|
||||
assertEquals(649, result.getUsbProductId());
|
||||
assertEquals(80, result.getPaperWidth());
|
||||
assertEquals("1", result.getAutoCut());
|
||||
assertEquals("GS_V0", result.getImageMode());
|
||||
verify(configMapper).insert(result);
|
||||
}
|
||||
|
||||
/** 普通业务终端不能借用设备信息接口覆盖打印终端配置。 */
|
||||
@Test
|
||||
void reportDeviceInfoRejectsNonPrinterDevice() {
|
||||
when(managerMapper.selectById(21L)).thenReturn(HardManagerEntity.builder()
|
||||
.id(21L).type(HardTypeEnum.CANTEEN.getType()).merchantId(30L).tenantId(50L).build());
|
||||
|
||||
RuntimeException exception = assertThrows(RuntimeException.class,
|
||||
() -> service.reportDeviceInfo(21L, reportRequest()));
|
||||
|
||||
assertEquals("目标设备必须是尔印打印终端", exception.getMessage());
|
||||
}
|
||||
|
||||
/** 已绑定设备的部门属于一对一绑定身份,修改部门前必须先解绑。 */
|
||||
@Test
|
||||
void boundDeviceCannotChangeDepartment() {
|
||||
HardManagerEntity before = HardManagerEntity.builder()
|
||||
.id(20L).type(HardTypeEnum.EY_PRINT.getType()).merchantId(30L).deptId(40L).build();
|
||||
HardManagerEntity after = HardManagerEntity.builder()
|
||||
.id(20L).type(HardTypeEnum.EY_PRINT.getType()).merchantId(30L).deptId(41L).build();
|
||||
doReturn(new HardPrinterBindingVo()).when(service).getBinding(20L);
|
||||
|
||||
RuntimeException exception = assertThrows(RuntimeException.class,
|
||||
() -> service.validateDeviceChange(before, after));
|
||||
|
||||
assertEquals("已绑定打印关系的设备不能直接修改类型、商户或部门,请先处理任务并解绑", exception.getMessage());
|
||||
}
|
||||
|
||||
private static HardPrinterConfigDto validConfig() {
|
||||
HardPrinterConfigDto config = new HardPrinterConfigDto();
|
||||
config.setPrinterHardId(20L);
|
||||
config.setPaperWidth(80);
|
||||
config.setAutoCut("1");
|
||||
config.setImageMode("GS_V0");
|
||||
return config;
|
||||
}
|
||||
|
||||
/** 构造现场 GEZHI 打印机对应的上报请求。 */
|
||||
private static HardPrinterInfoReportDto reportRequest() {
|
||||
HardPrinterInfoReportDto request = new HardPrinterInfoReportDto();
|
||||
request.setPrinterModel(" GEZHI / micro-printer ");
|
||||
request.setUsbVendorId(10473);
|
||||
request.setUsbProductId(649);
|
||||
return request;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -324,7 +324,7 @@ class ReservationMenuQueryServiceImplTest {
|
||||
ArgumentCaptor<HandCancelCodeDto> cancelCaptor = ArgumentCaptor.forClass(HandCancelCodeDto.class);
|
||||
verify(hardOrderService).handCancelCode(cancelCaptor.capture());
|
||||
assertEquals(1L, cancelCaptor.getValue().getOrderId());
|
||||
assertEquals(-1L, cancelCaptor.getValue().getHardwareId());
|
||||
assertNull(cancelCaptor.getValue().getHardwareId());
|
||||
}
|
||||
|
||||
/** 验证部分退款成功的已支付订单仍可核销,全部退款仍由订单状态校验拦截。 */
|
||||
|
||||
+2
@@ -23,6 +23,7 @@ import com.bsd.pjcloud.hard.api.vo.order.HardCanteenProductTotalVo;
|
||||
import com.bsd.pjcloud.hard.api.vo.order.excel.HardCanteenProductApptExcelVo;
|
||||
import com.bsd.pjcloud.hard.mapper.order.HardCarMapper;
|
||||
import com.bsd.pjcloud.hard.mapper.order.HardOrderMapper;
|
||||
import com.bsd.pjcloud.hard.support.HardUserIdentitySupport;
|
||||
import com.bsd.pjcloud.wallet.api.feign.RemoteMchService;
|
||||
import com.bsd.pjcloud.wallet.api.vo.MchManageVo;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -88,6 +89,7 @@ class HardCanteenTotalServiceImplTest {
|
||||
service = new HardCanteenTotalServiceImpl(
|
||||
mock(HardOrderMapper.class),
|
||||
hardCarMapper,
|
||||
mock(HardUserIdentitySupport.class),
|
||||
remoteDeptService,
|
||||
remoteMchService,
|
||||
mock(RemoteDataScopeService.class),
|
||||
|
||||
+35
-5
@@ -1,23 +1,29 @@
|
||||
package com.bsd.pjcloud.hard.service.impl.order;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.core.exception.ErrorCodes;
|
||||
import com.bsd.pjcloud.hard.api.constant.HardTypeEnum;
|
||||
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.hard.HandCancelCodeDto;
|
||||
import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity;
|
||||
import com.bsd.pjcloud.hard.api.entity.order.HardMessageOutboxEntity;
|
||||
import com.bsd.pjcloud.hard.api.vo.order.HardCarVo;
|
||||
import com.bsd.pjcloud.hard.mapper.order.HardMessageOutboxMapper;
|
||||
import com.bsd.pjcloud.hard.mapper.order.HardOrderMapper;
|
||||
import com.bsd.pjcloud.hard.service.HardApptReservationService;
|
||||
import com.bsd.pjcloud.hard.service.HardBroadcastService;
|
||||
import com.bsd.pjcloud.hard.service.HardCarService;
|
||||
import com.bsd.pjcloud.hard.service.HardInvoiceService;
|
||||
import com.bsd.pjcloud.wallet.api.feign.RemoteCodeUseService;
|
||||
import com.bsd.pjcloud.wallet.api.vo.mch.MchAutoRefundConfigVo;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.LocalDate;
|
||||
@@ -57,6 +63,9 @@ class HardOrderServiceImplVerificationTest {
|
||||
/** 取餐广播服务,用于验证食堂预约回退会重新启用待取餐提示。 */
|
||||
private final HardBroadcastService hardBroadcastService = mock(HardBroadcastService.class);
|
||||
|
||||
/** 打印任务服务,用于验证回退会取消尚未打印的兑换券。 */
|
||||
private final HardInvoiceService hardInvoiceService = mock(HardInvoiceService.class);
|
||||
|
||||
/** 被测订单服务,仅执行真实方法,未涉及的庞大依赖保持空值。 */
|
||||
private final HardOrderServiceImpl service = mock(HardOrderServiceImpl.class, CALLS_REAL_METHODS);
|
||||
|
||||
@@ -65,11 +74,16 @@ class HardOrderServiceImplVerificationTest {
|
||||
*/
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), "test"),
|
||||
HardOrderEntity.class);
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), "test"),
|
||||
HardMessageOutboxEntity.class);
|
||||
ReflectionTestUtils.setField(service, "baseMapper", hardOrderMapper);
|
||||
ReflectionTestUtils.setField(service, "hardCarService", hardCarService);
|
||||
ReflectionTestUtils.setField(service, "remoteCodeUseService", remoteCodeUseService);
|
||||
ReflectionTestUtils.setField(service, "hardMessageOutboxMapper", hardMessageOutboxMapper);
|
||||
ReflectionTestUtils.setField(service, "hardBroadcastService", hardBroadcastService);
|
||||
ReflectionTestUtils.setField(service, "hardInvoiceService", hardInvoiceService);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,7 +96,7 @@ class HardOrderServiceImplVerificationTest {
|
||||
|
||||
RuntimeException exception = assertThrows(RuntimeException.class, () -> service.handCancelCode(dto));
|
||||
|
||||
assertEquals("该预约订单正在退款,不能核销", exception.getMessage());
|
||||
assertEquals(ErrorCodes.HARD_RESERVE_REFUNDING_NO_VERIFY, exception.getMessage());
|
||||
verify(hardCarService, never()).getByOrderId(anyLong());
|
||||
}
|
||||
|
||||
@@ -100,7 +114,22 @@ class HardOrderServiceImplVerificationTest {
|
||||
|
||||
RuntimeException exception = assertThrows(RuntimeException.class, () -> service.handCancelCode(dto));
|
||||
|
||||
assertEquals("该预约订单商品已全部退款,不能核销", exception.getMessage());
|
||||
assertEquals(ErrorCodes.HARD_RESERVE_FULL_REFUND_NO_VERIFY, exception.getMessage());
|
||||
}
|
||||
|
||||
/** 超市预约核销与食堂预约共用兑换券任务,但不得创建食堂专属取餐消息。 */
|
||||
@Test
|
||||
void supermarketReservationCreatesVoucherWithoutMealOutbox() {
|
||||
HardOrderEntity order = reservationOrder(HardRefundStatusEnum.NONE, HardOrderStatusEnum.PAY);
|
||||
order.setSysType(HardTypeEnum.SUPERMARKET.getType());
|
||||
LocalDateTime verificationTime = LocalDateTime.of(2026, 9, 4, 12, 0);
|
||||
|
||||
Boolean printable = ReflectionTestUtils.invokeMethod(service, "isReservationVerification", order);
|
||||
ReflectionTestUtils.invokeMethod(service, "createReservationVerificationArtifacts", order, verificationTime, 100L);
|
||||
|
||||
assertTrue(Boolean.TRUE.equals(printable));
|
||||
verify(hardInvoiceService).createRedemptionVoucherTask(order, 100L);
|
||||
verify(hardMessageOutboxMapper, never()).insert(any(HardMessageOutboxEntity.class));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,6 +148,7 @@ class HardOrderServiceImplVerificationTest {
|
||||
assertTrue(service.rollbackReserveVerification(1L));
|
||||
|
||||
verify(remoteCodeUseService).rollbackReservationCode(1L);
|
||||
verify(hardInvoiceService).cancelPendingVoucherTasks(1L);
|
||||
verify(hardBroadcastService).rollbackDoneBroadcast(order);
|
||||
}
|
||||
|
||||
@@ -133,7 +163,7 @@ class HardOrderServiceImplVerificationTest {
|
||||
RuntimeException exception = assertThrows(RuntimeException.class,
|
||||
() -> service.rollbackReserveVerification(1L));
|
||||
|
||||
assertEquals("仅已核销订单支持退回到核销前", exception.getMessage());
|
||||
assertEquals(ErrorCodes.HARD_ROLLBACK_VERIFIED_ONLY, exception.getMessage());
|
||||
verify(remoteCodeUseService, never()).rollbackReservationCode(anyLong());
|
||||
}
|
||||
|
||||
@@ -148,7 +178,7 @@ class HardOrderServiceImplVerificationTest {
|
||||
RuntimeException exception = assertThrows(RuntimeException.class,
|
||||
() -> service.rollbackReserveVerification(1L));
|
||||
|
||||
assertEquals("该预约订单正在退款,不能退回到核销前", exception.getMessage());
|
||||
assertEquals(ErrorCodes.HARD_ROLLBACK_REFUNDING, exception.getMessage());
|
||||
verify(remoteCodeUseService, never()).rollbackReservationCode(anyLong());
|
||||
}
|
||||
|
||||
@@ -164,7 +194,7 @@ class HardOrderServiceImplVerificationTest {
|
||||
RuntimeException exception = assertThrows(RuntimeException.class,
|
||||
() -> service.rollbackReserveVerification(1L));
|
||||
|
||||
assertEquals("当前业务类型不支持退回到核销前", exception.getMessage());
|
||||
assertEquals(ErrorCodes.HARD_ROLLBACK_BIZ_UNSUPPORTED, exception.getMessage());
|
||||
verify(remoteCodeUseService, never()).rollbackReservationCode(anyLong());
|
||||
}
|
||||
|
||||
|
||||
+31
@@ -1,6 +1,7 @@
|
||||
package com.bsd.pjcloud.mqtt.dto.hard;
|
||||
|
||||
import com.bsd.pjcloud.mqtt.dto.MqttHardBaseMsg;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@@ -11,6 +12,18 @@ public class HardPrintMsg extends MqttHardBaseMsg {
|
||||
// 设备id
|
||||
private Long hardId;
|
||||
|
||||
/**
|
||||
* 产生订单或执行核销的来源业务终端 ID。
|
||||
*/
|
||||
@Schema(description = "来源业务终端ID")
|
||||
private Long sourceHardId;
|
||||
|
||||
/**
|
||||
* 实际接收 MQTT 并执行 USB 打印的 EY_PRINT 终端 ID。
|
||||
*/
|
||||
@Schema(description = "目标打印终端ID")
|
||||
private Long targetHardId;
|
||||
|
||||
// 设备部门id
|
||||
private Long mchDeptId;
|
||||
|
||||
@@ -20,6 +33,24 @@ public class HardPrintMsg extends MqttHardBaseMsg {
|
||||
// 订单id
|
||||
private Long orderId;
|
||||
|
||||
/**
|
||||
* 打印凭证类型,1 发票、2 兑换券。
|
||||
*/
|
||||
@Schema(description = "打印凭证类型")
|
||||
private String printType;
|
||||
|
||||
/**
|
||||
* 同订单同凭证类型的打印序号。
|
||||
*/
|
||||
@Schema(description = "打印序号")
|
||||
private Integer printNo;
|
||||
|
||||
/**
|
||||
* 人工失败重试版本,同一版本的重复 MQTT 消息必须幂等去重。
|
||||
*/
|
||||
@Schema(description = "重试版本")
|
||||
private Integer retryVersion;
|
||||
|
||||
// 安卓订单id
|
||||
private String androidOrderCode;
|
||||
|
||||
|
||||
Reference in new issue
Block a user