diff --git a/.gitignore b/.gitignore
index 9e2781708..765142c86 100644
--- a/.gitignore
+++ b/.gitignore
@@ -61,3 +61,10 @@ gen_code
### node ###
node_modules
+
+### AI 辅助工具目录 ###
+.v2c/
+.video_agent/
+.mimosa/
+.cloudbase/
+.zcode/
diff --git a/db/20260908_add_hard_order_evidence_image.sql b/db/20260908_add_hard_order_evidence_image.sql
new file mode 100644
index 000000000..8817ca8fb
--- /dev/null
+++ b/db/20260908_add_hard_order_evidence_image.sql
@@ -0,0 +1,44 @@
+-- 订单识别证据图片表(迁移自 hq-platform 20260823_add_hard_order_evidence_image.sql)。
+-- 本表独立于 hard_order,退款、撤销和订单逻辑删除均不自动清理证据文件或证据记录。
+CREATE TABLE IF NOT EXISTS `pjcloudx_hardware`.`hard_order_evidence_image`
+(
+ `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '证据记录自增主键',
+ `order_id` bigint(20) NOT NULL COMMENT '订单主键,来源hard_order.id',
+ `android_order_code` varchar(128) NOT NULL COMMENT '终端本地稳定订单号快照',
+ `evidence_type` varchar(32) NOT NULL COMMENT '证据类型,FACE_RECOGNITION或MENU_RECOGNITION',
+ `evidence_role` varchar(32) NOT NULL COMMENT '图片角色,FACE_REFERENCE、FACE_LIVE或MENU_LIVE',
+ `sequence_no` int(11) NOT NULL DEFAULT 0 COMMENT '同类型同角色图片序号,从0开始',
+ `user_id` bigint(20) DEFAULT NULL COMMENT '识别用户主键快照',
+ `face_sample_id` varchar(64) DEFAULT NULL COMMENT '虹软人脸样本编号快照',
+ `reference_image_url` varchar(512) DEFAULT NULL COMMENT '识别时后台参考人脸来源地址快照,不作为展示地址',
+ `similarity` decimal(8, 6) DEFAULT NULL COMMENT '识别相似度快照,取值0至1',
+ `similarity_threshold` decimal(8, 6) DEFAULT NULL COMMENT '终端生效的相似度阈值快照,取值0至1',
+ `hard_id` bigint(20) NOT NULL COMMENT '产生证据的设备主键',
+ `captured_at` datetime DEFAULT NULL COMMENT '终端实际采集时间',
+ `sys_file_id` bigint(20) DEFAULT NULL COMMENT '文件中心sys_file记录主键',
+ `storage_file_name` varchar(255) DEFAULT NULL COMMENT '文件中心对象名',
+ `image_url` varchar(512) DEFAULT NULL COMMENT '订单证据图片访问地址',
+ `image_hash` char(64) DEFAULT NULL COMMENT '实际文件SHA-256哈希',
+ `image_size` bigint(20) DEFAULT NULL COMMENT '实际文件大小,单位字节',
+ `image_width` int(11) DEFAULT NULL COMMENT '实际图片宽度,单位像素',
+ `image_height` int(11) DEFAULT NULL COMMENT '实际图片高度,单位像素',
+ `status` varchar(32) NOT NULL COMMENT '证据状态,PENDING、AVAILABLE或UNAVAILABLE',
+ `failure_reason` varchar(500) DEFAULT NULL COMMENT '永久缺失或复制失败摘要,不保存终端完整路径',
+ `app_version` varchar(64) DEFAULT NULL COMMENT '产生证据的终端应用版本',
+ `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 '修改时间',
+ `del_flag` char(1) DEFAULT '0' COMMENT '逻辑删除标记,0有效,1删除',
+ `tenant_id` bigint(20) NOT NULL COMMENT '租户主键',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_hard_order_evidence_position`
+ (`tenant_id`, `order_id`, `evidence_type`, `evidence_role`, `sequence_no`, `del_flag`),
+ KEY `idx_hard_order_evidence_android_order`
+ (`tenant_id`, `hard_id`, `android_order_code`, `del_flag`),
+ KEY `idx_hard_order_evidence_status`
+ (`tenant_id`, `status`, `update_time`, `del_flag`)
+) ENGINE = InnoDB
+ DEFAULT CHARSET = utf8mb4
+ COLLATE = utf8mb4_general_ci
+ COMMENT = '硬件订单识别证据图片';
diff --git a/db/20260908_add_hard_order_verification_time.sql b/db/20260908_add_hard_order_verification_time.sql
new file mode 100644
index 000000000..363d63e1c
--- /dev/null
+++ b/db/20260908_add_hard_order_verification_time.sql
@@ -0,0 +1,23 @@
+-- 硬件订单新增独立核销时间(迁移自 hq-platform 20260823_add_hard_order_verification_time.sql)。
+-- 历史二维码核销订单只能使用最后更新时间近似回填;外卖等非核销完成状态不参与回填。
+-- 新核销订单由业务事务写入准确时间,后续订单更新不再改变该值。
+
+SET @verification_time_ddl = IF(
+ (SELECT COUNT(*) FROM `information_schema`.`columns`
+ WHERE `table_schema` = 'pjcloudx_hardware' AND `table_name` = 'hard_order'
+ AND `column_name` = 'verification_time') = 0,
+ 'ALTER TABLE `pjcloudx_hardware`.`hard_order` ADD COLUMN `verification_time` datetime DEFAULT NULL COMMENT ''订单实际核销完成时间,后续订单更新不得覆盖'' AFTER `refund_status`, ADD KEY `idx_hard_order_verification_time` (`tenant_id`, `verification_time`, `del_flag`)',
+ 'SELECT 1'
+);
+PREPARE verification_time_stmt FROM @verification_time_ddl;
+EXECUTE verification_time_stmt;
+DEALLOCATE PREPARE verification_time_stmt;
+
+UPDATE `pjcloudx_hardware`.`hard_order`
+SET `verification_time` = `update_time`
+WHERE ((`order_status` = '20' AND `order_type` IN ('1', '2', '3', '4'))
+ OR `pay_type` IN ('5', '6', '50'))
+ AND `verification_time` IS NULL
+ AND `update_time` IS NOT NULL;
+
+SET @verification_time_ddl = NULL;
diff --git a/db/20260908_add_hard_printer.sql b/db/20260908_add_hard_printer.sql
new file mode 100644
index 000000000..28d950ad1
--- /dev/null
+++ b/db/20260908_add_hard_printer.sql
@@ -0,0 +1,206 @@
+-- 尔印独立打印终端、终端一对一绑定及持久化打印任务(迁移自 hq-platform 20260904_add_er_yin_printer.sql,仅保留 DDL)。
+-- 字典(hard_type 类型12、hard_print_type、hard_print_status)与按钮权限请按 db/20260908_权限菜单字典待添加.txt 手动添加。
+-- 每个结构变更独立判断,可在兼容数据库自动提交部分DDL后安全重跑整个脚本。
+
+ALTER TABLE `pjcloudx_hardware`.`hard_invoice`
+ MODIFY COLUMN `hard_id` bigint(20) NULL COMMENT '来源业务或核销终端id',
+ MODIFY COLUMN `pic` varchar(512) NULL COMMENT '后端生成的打印PNG地址';
+
+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配置';
+
+SET @er_yin_ddl = NULL;
diff --git a/db/20260908_add_hard_terminal_log_file.sql b/db/20260908_add_hard_terminal_log_file.sql
new file mode 100644
index 000000000..046527ec5
--- /dev/null
+++ b/db/20260908_add_hard_terminal_log_file.sql
@@ -0,0 +1,38 @@
+-- 终端日志文件表(hq-platform 无独立建表脚本,本 DDL 依据 HardTerminalLogFileEntity 与查询场景编写)。
+-- 记录 Android 终端上传到文件中心的日志文件索引,同一文件重复上传时累加 upload_count 并更新地址。
+CREATE TABLE IF NOT EXISTS `pjcloudx_hardware`.`hard_terminal_log_file`
+(
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
+ `hard_id` bigint NOT NULL COMMENT '设备ID(hard_manager.id)',
+ `hard_name` varchar(128) DEFAULT NULL COMMENT '上传时设备名称快照',
+ `unique_code` varchar(128) DEFAULT NULL COMMENT '上传时设备唯一编号快照',
+ `hard_type` char(10) DEFAULT NULL COMMENT '上传时设备类型快照',
+ `merchant_id` bigint DEFAULT NULL COMMENT '上传时商户ID快照',
+ `dept_id` bigint DEFAULT NULL COMMENT '上传时部门ID快照',
+ `log_date` date NOT NULL COMMENT '日志所属日期',
+ `relative_path` varchar(512) NOT NULL COMMENT '终端日志根目录下的相对路径',
+ `original_name` varchar(255) DEFAULT NULL COMMENT '原日志文件名',
+ `file_size` bigint DEFAULT NULL COMMENT '文件大小(字节)',
+ `file_hash` char(32) DEFAULT NULL COMMENT '文件内容哈希(当前使用MD5)',
+ `file_modified_time` datetime DEFAULT NULL COMMENT '终端本地文件最后修改时间',
+ `sys_file_id` bigint DEFAULT NULL COMMENT '文件中心sys_file记录ID',
+ `storage_file_name` varchar(255) DEFAULT NULL COMMENT 'OSS对象文件名',
+ `storage_url` varchar(512) DEFAULT NULL COMMENT '文件访问相对地址',
+ `app_version` varchar(64) DEFAULT NULL COMMENT '上传时应用版本',
+ `sanitize_version` varchar(64) DEFAULT NULL COMMENT '日志脱敏规则版本',
+ `upload_time` datetime DEFAULT NULL COMMENT '最近成功上传时间',
+ `upload_count` int NOT NULL DEFAULT 1 COMMENT '成功上传次数',
+ `remark` varchar(512) DEFAULT NULL COMMENT '备注',
+ `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 '修改时间',
+ `del_flag` char(1) DEFAULT '0' COMMENT '删除标记',
+ `tenant_id` bigint NOT NULL COMMENT '租户ID',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_hard_terminal_log_position` (`tenant_id`, `hard_id`, `relative_path`, `del_flag`),
+ KEY `idx_hard_terminal_log_date` (`tenant_id`, `hard_id`, `log_date`, `del_flag`),
+ KEY `idx_hard_terminal_log_hash` (`tenant_id`, `file_hash`)
+) ENGINE = InnoDB
+ DEFAULT CHARSET = utf8mb4
+ COLLATE = utf8mb4_general_ci COMMENT ='终端日志文件';
diff --git a/db/20260908_add_hard_terminal_status.sql b/db/20260908_add_hard_terminal_status.sql
new file mode 100644
index 000000000..71d932f34
--- /dev/null
+++ b/db/20260908_add_hard_terminal_status.sql
@@ -0,0 +1,25 @@
+-- 终端心跳状态表(迁移自 hq-platform 20260730_add_hard_terminal_status.sql)。
+-- 食堂、超市 Android 终端心跳状态。在线状态由服务端按最后心跳时间实时计算,不冗余落库。
+CREATE TABLE IF NOT EXISTS `pjcloudx_hardware`.`hard_terminal_status` (
+ `hard_id` bigint NOT NULL COMMENT '设备ID,对应hard_manager.id',
+ `last_online_time` datetime NOT NULL COMMENT '服务端最近收到心跳的时间',
+ `heartbeat_scene` varchar(32) NOT NULL COMMENT '最近心跳场景(ONLINE,AUTO_UPDATE)',
+ `disconnect_reason` varchar(128) DEFAULT NULL COMMENT '计划内断开原因,目前用于标记自动更新重启',
+ `apk_version_name` varchar(64) DEFAULT NULL COMMENT '终端上报的APK展示版本号',
+ `apk_version_code` bigint DEFAULT NULL COMMENT '终端上报的APK内部版本码',
+ `system_uptime_seconds` bigint DEFAULT NULL COMMENT 'Android系统本次启动后运行秒数',
+ `app_uptime_seconds` bigint DEFAULT NULL COMMENT '终端App本次进程运行秒数',
+ `network_type` varchar(32) DEFAULT NULL COMMENT '最近心跳使用的网络类型',
+ `available_storage_bytes` bigint DEFAULT NULL COMMENT '应用存储卷最近可用空间,单位字节',
+ `root_status` varchar(16) DEFAULT NULL COMMENT 'Root状态(unknown,available,unavailable)',
+ `device_owner` tinyint(1) DEFAULT NULL COMMENT '当前应用是否为Device Owner',
+ `kiosk_active` tinyint(1) DEFAULT NULL COMMENT '最近心跳时是否处于Kiosk锁定任务模式',
+ `device_status_json` json DEFAULT NULL COMMENT '打印机、电子秤、读卡器、副屏等可选硬件状态快照',
+ `create_by` varchar(64) DEFAULT NULL COMMENT '创建人',
+ `create_time` datetime NOT NULL COMMENT '创建时间',
+ `update_by` varchar(64) DEFAULT NULL COMMENT '修改人',
+ `update_time` datetime NOT NULL COMMENT '修改时间',
+ `tenant_id` bigint NOT NULL COMMENT '租户ID',
+ PRIMARY KEY (`hard_id`),
+ KEY `idx_hard_terminal_status_tenant_online` (`tenant_id`, `last_online_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='食堂超市终端心跳状态';
diff --git a/db/20260908_add_hard_xfj_log.sql b/db/20260908_add_hard_xfj_log.sql
new file mode 100644
index 000000000..6dcff40e7
--- /dev/null
+++ b/db/20260908_add_hard_xfj_log.sql
@@ -0,0 +1,30 @@
+-- 消费机日志表(迁移自 hq-platform 800pjcloudx_hardware.sql 末段)。
+-- 记录消费机终端 HTTP 调用日志(req/res/error/耗时),unique_code 唯一。
+CREATE TABLE IF NOT EXISTS `pjcloudx_hardware`.`hard_xfj_log`
+(
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
+ `unique_code` varchar(64) NOT NULL COMMENT '设备唯一编号',
+ `hard_type` varchar(32) NOT NULL COMMENT '设备类型',
+ `title` varchar(64) NOT NULL COMMENT '接口标题',
+ `headers` varchar(2048) COMMENT 'headers',
+ `res` varchar(2048) COMMENT 'res',
+ `req` varchar(2048) COMMENT 'req',
+ `error` varchar(2048) COMMENT 'error',
+ `start_time` datetime DEFAULT NULL COMMENT '开始时间',
+ `end_time` datetime DEFAULT NULL COMMENT '结束时间',
+ `cost` int DEFAULT 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`),
+ UNIQUE KEY `idx_unique_code` (`unique_code`),
+ KEY `idx_hard_type` (`hard_type`),
+ KEY `idx_title` (`title`),
+ KEY `idx_start_time` (`start_time`),
+ KEY `idx_end_time` (`end_time`)
+) ENGINE = InnoDB
+ DEFAULT CHARSET = utf8mb4
+ COLLATE = utf8mb4_general_ci COMMENT ='消费机日志';
diff --git a/db/20260908_add_wallet_transaction_tags_remark.sql b/db/20260908_add_wallet_transaction_tags_remark.sql
new file mode 100644
index 000000000..b35c43a26
--- /dev/null
+++ b/db/20260908_add_wallet_transaction_tags_remark.sql
@@ -0,0 +1,7 @@
+-- 为钱包流水补充财务统计标签和业务备注;历史数据保持为空。
+-- tags 保存 wallet_transaction_tag 字典值,多个值使用英文逗号分隔;remark 前后端统一限制为 256 个字符。
+ALTER TABLE wallet_transaction
+ ADD COLUMN tags varchar(512) DEFAULT NULL COMMENT '财务统计标签,多个wallet_transaction_tag字典值使用英文逗号分隔' AFTER balance;
+
+ALTER TABLE wallet_transaction
+ ADD COLUMN remark varchar(256) DEFAULT NULL COMMENT '流水备注,最多256个字符' AFTER tags;
diff --git a/db/20260908_权限菜单字典待添加.txt b/db/20260908_权限菜单字典待添加.txt
new file mode 100644
index 000000000..31752705d
--- /dev/null
+++ b/db/20260908_权限菜单字典待添加.txt
@@ -0,0 +1,158 @@
+================================================================
+hard 模块迁移(2026-09-08)—— 权限/菜单/字典 手动添加清单
+================================================================
+说明:以下内容需要在 pjcloudx 主库(sys_menu / sys_dict / sys_dict_item)手动添加,
+可以直接在「系统管理-菜单管理 / 字典管理」界面配置,也可以执行文末附带的 SQL(幂等,可重复执行)。
+每项都标注了用途,若目标环境已存在同名项请跳过。
+
+----------------------------------------------------------------
+一、按钮权限(sys_menu,menu_type='2')
+----------------------------------------------------------------
+挂载位置建议:挂在对应页面的父菜单下(与 hard_hardManager_view 同级)。
+
+1. hard_hardTerminalLogFile_view 终端日志-查看 (终端日志页面 + 设备详情页日志区)
+2. hard_hardTerminalLogFile_preview 终端日志-预览
+3. hard_hardTerminalLogFile_down 终端日志-下载
+4. hard_hardTerminalLogFile_pull 终端日志-拉取
+5. hard_hardManager_printer_bind 设备管理-绑定打印机 (设备详情页打印配置区)
+6. hard_hardManager_export 设备管理-导出 (设备列表导出按钮,若已有则跳过)
+7. hard_hardInvoice_voucher_reprint 发票凭证-重打兑换券(打印任务弹窗重打,若不需要可不加)
+
+----------------------------------------------------------------
+二、页面菜单(sys_menu,menu_type='1',动态路由)
+----------------------------------------------------------------
+1. 终端日志(机器日志)
+ 路径 path:/hard/hard-terminal-log-file/index
+ 权限标识:hard_hardTerminalLogFile_view
+2. 消费机日志
+ 路径 path:/hard/xfj/xfj-log/index
+ 权限标识:(查看权限可留空或自定 hard_xfjLog_view,页面本身无按钮权限)
+
+说明:
+- 设备详情页 /hard/hard-manager/details 已由前端静态路由注册(isHide),无需配菜单。
+- 售后订单详情 order-refund 沿用目标库已有菜单,无需改动。
+
+----------------------------------------------------------------
+三、字典(sys_dict + sys_dict_item)
+----------------------------------------------------------------
+1. hard_type(设备类型,已有字典,补充缺失项):
+ - 9 云版消费机
+ - 12 尔印打印机器
+ (若已有 10/11 云音响等项则不动)
+2. 新增 hard_print_type(打印凭证类型):
+ - 1 发票
+ - 2 兑换券
+3. 新增 hard_print_status(打印任务状态):
+ - 0 待生成
+ - 1 待打印
+ - 2 已打印
+ - 3 打印失败
+ - 4 已取消
+4. 可选:hard_appt_reserve_mode(预约模式,售后详情页预约信息列显示用;
+ 目标库若无此字典该列显示空,不影响其他功能)
+
+----------------------------------------------------------------
+四、附:可直接执行的幂等 SQL(按需使用,执行前请备份)
+----------------------------------------------------------------
+-- 1. hard_type 补类型 9/12(每个已配置 hard_type 的租户幂等插入)
+SET @dict_item_id_base = (SELECT GREATEST(COALESCE(MAX(`id`), 0), 2026090800000000000) 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`, `remarks`, `del_flag`, `tenant_id`)
+SELECT @dict_item_id_base + ROW_NUMBER() OVER (ORDER BY d.`tenant_id`, item.`item_value`),
+ d.`id`, item.`item_value`, item.`label`, 'hard_type', item.`label`, CAST(item.`item_value` AS SIGNED),
+ 'system', NOW(), item.`label`, '0', d.`tenant_id`
+FROM `pjcloudx`.`sys_dict` d
+JOIN (
+ SELECT '9' AS `item_value`, '云版消费机' AS `label`
+ UNION ALL SELECT '12', '尔印打印机器'
+) item
+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` = item.`item_value` AND i.`del_flag` = '0'
+ );
+SET @dict_item_id_base = NULL;
+
+-- 2. hard_print_type / hard_print_status 字典(按租户幂等创建)
+SET @dict_id_base = (SELECT GREATEST(COALESCE(MAX(`id`), 0), 2026090800000000000) 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 @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 @dict_id_base = NULL;
+
+SET @dict_item_id_base = (SELECT GREATEST(COALESCE(MAX(`id`), 0), 2026090800000000000) 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 @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 @dict_item_id_base = NULL;
+
+-- 3. 按钮权限(挂到已有父权限所在菜单下,menu_type='2')
+-- 先在界面确认父菜单,或使用下面按 permission 定位父菜单的幂等插入:
+SET @menu_id_base = (SELECT GREATEST(COALESCE(MAX(`menu_id`), 0), 2026090800000000000) 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 @menu_id_base + ROW_NUMBER() OVER (ORDER BY p.`tenant_id`, p.`menu_id`, item.`permission`),
+ item.`name`, item.`permission`, p.`menu_id`, '1', 20, '0', '0', '2', 'system', NOW(), '0', p.`tenant_id`
+FROM `pjcloudx`.`sys_menu` p
+JOIN (
+ SELECT 'hard_hardTerminalLogFile_view' AS `permission`, '终端日志查看' AS `name`, 'hard_hardManager_view' AS `parent_permission`
+ UNION ALL SELECT 'hard_hardTerminalLogFile_preview', '终端日志预览', 'hard_hardManager_view'
+ UNION ALL SELECT 'hard_hardTerminalLogFile_down', '终端日志下载', 'hard_hardManager_view'
+ UNION ALL SELECT 'hard_hardTerminalLogFile_pull', '终端日志拉取', 'hard_hardManager_view'
+ UNION ALL SELECT 'hard_hardManager_printer_bind', '绑定打印机', 'hard_hardManager_view'
+ UNION ALL SELECT 'hard_hardManager_export', '导出设备', 'hard_hardManager_view'
+ UNION ALL SELECT 'hard_hardInvoice_voucher_reprint', '重打兑换券', 'hard_hardInvoice_view'
+) item ON item.`parent_permission` = p.`permission`
+WHERE p.`del_flag` = '0'
+ AND NOT EXISTS (
+ SELECT 1 FROM `pjcloudx`.`sys_menu` e
+ WHERE e.`tenant_id` = p.`tenant_id` AND e.`permission` = item.`permission` AND e.`del_flag` = '0'
+ );
+SET @menu_id_base = NULL;
+
+-- 4. 页面菜单(终端日志 / 消费机日志)—— parent_id 请替换为实际的「设备管理/硬件」目录菜单ID
+-- (建议直接在菜单管理界面添加,下面仅作字段参考)
+-- INSERT INTO `pjcloudx`.`sys_menu`
+-- (`menu_id`,`name`,`permission`,`path`,`parent_id`,`icon`,`visible`,`sort_order`,`keep_alive`,`embedded`,`menu_type`,`create_by`,`create_time`,`del_flag`,`tenant_id`)
+-- VALUES
+-- (自定义ID, '终端日志', 'hard_hardTerminalLogFile_view', '/hard/hard-terminal-log-file/index', <父菜单ID>, NULL, '1', 100, '0', NULL, '1', 'admin', NOW(), '0', 1),
+-- (自定义ID, '消费机日志', NULL, '/hard/xfj/xfj-log/index', <父菜单ID>, NULL, '1', 101, '0', NULL, '1', 'admin', NOW(), '0', 1);
+
+----------------------------------------------------------------
+五、数据库迁移 SQL 执行顺序(pj_service_cloud/db/ 下本次新增)
+----------------------------------------------------------------
+1. 20260908_add_hard_terminal_status.sql
+2. 20260908_add_hard_terminal_log_file.sql
+3. 20260908_add_hard_order_evidence_image.sql
+4. 20260908_add_hard_printer.sql (含 hard_invoice 打印任务化,注意先备份)
+5. 20260908_add_hard_xfj_log.sql
+6. 20260908_add_hard_order_verification_time.sql
diff --git a/db/800pjcloudx_hardware.sql b/db/800pjcloudx_hardware.sql
index d93d5c60c..877edbf2a 100644
--- a/db/800pjcloudx_hardware.sql
+++ b/db/800pjcloudx_hardware.sql
@@ -177,22 +177,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
@@ -311,6 +372,50 @@ CREATE TABLE hard_order
PRIMARY KEY (id)
) ENGINE = InnoDB COMMENT ='订单表';
+-- ----------------------------
+-- Table structure for hard_order_evidence_image
+-- ----------------------------
+DROP TABLE IF EXISTS `hard_order_evidence_image`;
+CREATE TABLE hard_order_evidence_image
+(
+ id bigint NOT NULL AUTO_INCREMENT COMMENT '证据记录自增主键',
+ order_id bigint NOT NULL COMMENT '订单主键,来源hard_order.id',
+ android_order_code varchar(128) NOT NULL COMMENT '终端本地稳定订单号快照',
+ evidence_type varchar(32) NOT NULL COMMENT '证据类型,FACE_RECOGNITION或MENU_RECOGNITION',
+ evidence_role varchar(32) NOT NULL COMMENT '图片角色,FACE_REFERENCE、FACE_LIVE或MENU_LIVE',
+ sequence_no int NOT NULL DEFAULT 0 COMMENT '同类型同角色图片序号,从0开始',
+ user_id bigint DEFAULT NULL COMMENT '识别用户主键快照',
+ face_sample_id varchar(64) DEFAULT NULL COMMENT '虹软人脸样本编号快照',
+ reference_image_url varchar(512) DEFAULT NULL COMMENT '识别时后台参考人脸来源地址快照,不作为展示地址',
+ similarity decimal(8, 6) DEFAULT NULL COMMENT '识别相似度快照,取值0至1',
+ similarity_threshold decimal(8, 6) DEFAULT NULL COMMENT '终端生效的相似度阈值快照,取值0至1',
+ hard_id bigint NOT NULL COMMENT '产生证据的设备主键',
+ captured_at datetime DEFAULT NULL COMMENT '终端实际采集时间',
+ sys_file_id bigint DEFAULT NULL COMMENT '文件中心sys_file记录主键',
+ storage_file_name varchar(255) DEFAULT NULL COMMENT '文件中心对象名',
+ image_url varchar(512) DEFAULT NULL COMMENT '订单证据图片访问地址',
+ image_hash char(64) DEFAULT NULL COMMENT '实际文件SHA-256哈希',
+ image_size bigint DEFAULT NULL COMMENT '实际文件大小,单位字节',
+ image_width int DEFAULT NULL COMMENT '实际图片宽度,单位像素',
+ image_height int DEFAULT NULL COMMENT '实际图片高度,单位像素',
+ status varchar(32) NOT NULL COMMENT '证据状态,PENDING、AVAILABLE或UNAVAILABLE',
+ failure_reason varchar(500) DEFAULT NULL COMMENT '永久缺失或复制失败摘要,不保存终端完整路径',
+ app_version varchar(64) DEFAULT NULL COMMENT '产生证据的终端应用版本',
+ 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 '修改时间',
+ del_flag char(1) DEFAULT '0' COMMENT '逻辑删除标记,0有效,1删除',
+ tenant_id bigint NOT NULL COMMENT '租户主键',
+ PRIMARY KEY (id),
+ UNIQUE KEY uk_hard_order_evidence_position
+ (tenant_id, order_id, evidence_type, evidence_role, sequence_no, del_flag),
+ KEY idx_hard_order_evidence_android_order
+ (tenant_id, hard_id, android_order_code, del_flag),
+ KEY idx_hard_order_evidence_status
+ (tenant_id, status, update_time, del_flag)
+) ENGINE = InnoDB COMMENT ='硬件订单识别证据图片';
+
-- ----------------------------
-- Table structure for hard_after_sale
-- ----------------------------
@@ -599,4 +704,35 @@ CREATE TABLE hard_third_mi_order
del_flag char(1) COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '删除标记',
tenant_id bigint DEFAULT NULL COMMENT '租户ID',
PRIMARY KEY (id)
-) ENGINE = InnoDB COMMENT ='硬件商品免密订单';
\ No newline at end of file
+) ENGINE = InnoDB COMMENT ='硬件商品免密订单';
+
+-- ----------------------------
+-- Table structure for hard_xfj_log
+-- ----------------------------
+DROP TABLE IF EXISTS `hard_xfj_log`;
+CREATE TABLE hard_xfj_log
+(
+ id bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
+ unique_code varchar(64) NOT NULL COMMENT '设备唯一编号',
+ hard_type varchar(32) NOT NULL COMMENT '设备类型',
+ title varchar(64) NOT NULL COMMENT '接口标题',
+ headers varchar(2048) COMMENT 'headers',
+ res varchar(2048) COMMENT 'res',
+ req varchar(2048) COMMENT 'req',
+ error varchar(2048) COMMENT 'error',
+ start_time datetime DEFAULT NULL COMMENT '开始时间',
+ end_time datetime DEFAULT NULL COMMENT '结束时间',
+ cost int DEFAULT 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),
+ UNIQUE KEY idx_unique_code (unique_code),
+ KEY idx_hard_type (hard_type),
+ KEY idx_title (title),
+ KEY idx_start_time (start_time),
+ KEY idx_end_time (end_time)
+) ENGINE = InnoDB COMMENT ='消费机日志';
\ No newline at end of file
diff --git a/db/800pjcloudx_wallet.sql b/db/800pjcloudx_wallet.sql
index 7fafbd4e2..f5e6b8a6a 100644
--- a/db/800pjcloudx_wallet.sql
+++ b/db/800pjcloudx_wallet.sql
@@ -374,6 +374,8 @@ CREATE TABLE wallet_transaction
title varchar(128) NOT NULL COMMENT '流水标题',
price int NOT NULL COMMENT '交易金额, 单位分',
balance int NOT NULL COMMENT '余额, 单位分',
+ tags varchar(512) DEFAULT NULL COMMENT '财务统计标签,多个wallet_transaction_tag字典值使用英文逗号分隔',
+ remark varchar(256) DEFAULT NULL COMMENT '流水备注,最多256个字符',
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 '修改人',
diff --git a/db/slowlog_optimization_20260826/01_add_indexes.sql b/db/slowlog_optimization_20260826/01_add_indexes.sql
new file mode 100644
index 000000000..13f857db3
--- /dev/null
+++ b/db/slowlog_optimization_20260826/01_add_indexes.sql
@@ -0,0 +1,44 @@
+-- 线上慢 SQL 索引优化(OceanBase MySQL 模式)
+-- 执行前请确认同名索引不存在;建议低峰期逐条执行并观察建索引耗时。
+-- 本文件只包含索引 DDL,不包含任何数据修改。
+
+-- P0:订单列表按租户、删除标记、创建时间倒序分页。
+CREATE INDEX idx_hard_order_tenant_del_ct_uid
+ ON pjcloudx_hardware.hard_order (tenant_id, del_flag, create_time, user_id);
+
+-- P0:订单号 + 订单类型 + 状态精确计数。
+CREATE INDEX idx_hard_order_tenant_del_order_lookup
+ ON pjcloudx_hardware.hard_order (tenant_id, del_flag, order_no, order_type, order_status);
+
+-- P0:设备侧订单号精确查询。
+CREATE INDEX idx_hard_order_tenant_del_android_code
+ ON pjcloudx_hardware.hard_order (tenant_id, del_flag, android_order_code);
+
+-- P0:每个订单取最新售后记录(GROUP BY order_id + MAX(id))。
+CREATE INDEX idx_hard_after_sale_tenant_order_id
+ ON pjcloudx_hardware.hard_after_sale (tenant_id, order_id, id);
+
+-- P0:钱包流水按业务类型、时间倒序分页;biz_type 为 TINYTEXT,使用前缀索引。
+CREATE INDEX idx_wallet_tx_tenant_del_ct_uid
+ ON pjcloudx_wallet.wallet_transaction (tenant_id, del_flag, create_time, user_id);
+
+-- P0:钱包流水按业务类型、时间倒序分页;biz_type 为 TINYTEXT,使用前缀索引。
+CREATE INDEX idx_wallet_tx_tenant_del_biz_ct_uid
+ ON pjcloudx_wallet.wallet_transaction (tenant_id, del_flag, biz_type(8), create_time, user_id);
+
+-- P0:流程历史按办理人查询并按结束时间倒序。
+CREATE INDEX idx_act_hi_actinst_assignee_end
+ ON pjcloudx_flow.act_hi_actinst (ASSIGNEE_, END_TIME_);
+
+-- P1:支付状态 + 支付时间统计。先观察 P0 上线后的实际收益,再决定是否执行。
+-- CREATE INDEX idx_hard_order_tenant_del_status_ptime_mch
+-- ON pjcloudx_hardware.hard_order (tenant_id, del_flag, order_status, pay_time, merchant_id);
+
+-- P1:指定商户部门的支付统计。索引较宽,按接口频率决定是否执行。
+-- CREATE INDEX idx_hard_order_tenant_del_mdept_status_mt_ptime
+-- ON pjcloudx_hardware.hard_order
+-- (tenant_id, del_flag, merchant_dept_id, order_status, merchant_type, pay_time);
+
+-- P1:按钱包所属部门定位钱包 ID;维表较小时收益有限。
+-- CREATE INDEX idx_wallet_manage_tenant_dept_id
+-- ON pjcloudx_wallet.wallet_manage (tenant_id, dept_id, id);
diff --git a/db/slowlog_optimization_20260826/README.md b/db/slowlog_optimization_20260826/README.md
new file mode 100644
index 000000000..72323b122
--- /dev/null
+++ b/db/slowlog_optimization_20260826/README.md
@@ -0,0 +1,54 @@
+# 线上慢 SQL 索引优化分析(2026-08-26)
+
+## 数据范围
+
+- 来源:`/Users/dengzhangchen/Downloads/rm-1fyg92tzhbt23g9hw_slowlog_*.csv`
+- 原始记录:638 条;按完整请求字段去重后 608 条;63 个 SQL Hash。
+- 数据库分布:`pjcloudx_hardware` 451 条、`pjcloudx_wallet` 153 条、`pjcloudx_flow` 4 条。
+- 主要扫描表:`hard_order`、`hard_after_sale`、`wallet_transaction`、`act_hi_actinst`。
+- 测试连接:OceanBase CE 4.3.5.5,MySQL 兼容模式;读取 `SHOW CREATE TABLE`、`SHOW INDEX`、`EXPLAIN`,并在测试库临时创建/回滚候选索引;未在生产库执行 DDL。
+
+## 结论
+
+### 强推荐,先上线
+
+| 优先级 | 数据库/表 | 建议索引 | 主要覆盖日志 | 依据 |
+|---|---|---|---|---|
+| P0 | `pjcloudx_hardware.hard_order` | `(tenant_id, del_flag, create_time, user_id)` | 订单列表、分页 COUNT,Hash `270048891%7C1391`、`528608244%7C616` 等 | 现有索引的第三列是 `merchant_id` 或 `user_id`,无法同时支撑租户+删除标记后的时间倒序;临时索引验证后由扫描+TOP-N 变为反向范围扫描。 |
+| P0 | `pjcloudx_hardware.hard_order` | `(tenant_id, del_flag, order_no, order_type, order_status)` | 订单号精确 COUNT,Hash `_1019382644%7C138` | 原执行计划只能在统计索引上按 `order_status` 扫描,再回表过滤订单号/类型;临时索引验证为三列等值范围扫描。 |
+| P0 | `pjcloudx_hardware.hard_order` | `(tenant_id, del_flag, android_order_code)` | 设备订单号查询,Hash `_986870313%7C628` | 原计划扫描约 98 万行后回表过滤 `android_order_code`;临时索引验证为等值范围扫描。 |
+| P0 | `pjcloudx_hardware.hard_after_sale` | `(tenant_id, order_id, id)` | 订单列表中的“每单最新售后”子查询,覆盖 233 条相关记录 | 原表只有主键,`GROUP BY order_id + MAX(id)` 为全表扫描;临时索引验证后为有序范围扫描并使用 Merge Group By。 |
+| P0 | `pjcloudx_wallet.wallet_transaction` | `(tenant_id, del_flag, create_time, user_id)` | 无业务类型条件的钱包流水首页,Hash `698590166%7C408` | 原计划全表扫描并排序;临时索引验证后为反向范围扫描,直接取前 10 行。 |
+| P0 | `pjcloudx_wallet.wallet_transaction` | `(tenant_id, del_flag, biz_type(8), create_time, user_id)` | 钱包流水按业务类型筛选列表/COUNT,Hash `2036241023%7C466`、`255738862%7C620` 等 | `biz_type` 是 `TINYTEXT`,必须使用前缀索引;临时索引验证后可按租户、删除标记、业务类型及时间范围扫描。 |
+| P0 | `pjcloudx_flow.act_hi_actinst` | `(ASSIGNEE_, END_TIME_)` | 流程历史待办查询,Hash `_2096433074%7C126` | 原计划全表扫描并排序;临时索引验证后为反向范围扫描,直接满足 `ORDER BY END_TIME_ DESC LIMIT 10`。 |
+
+### 条件推荐,先观察资源与收益
+
+| 优先级 | 建议索引 | 适用场景 | 说明 |
+|---|---|---|---|
+| P1 | `pjcloudx_hardware.hard_order (tenant_id, del_flag, order_status, pay_time, merchant_id)` | 按支付状态+支付时间汇总,Hash `_1655261703%7C470` 等 | 临时索引可将范围缩小到状态/时间,但仍需要回表和聚合;建议 P0 上线后按 `EXPLAIN` 和实际耗时决定。 |
+| P1 | `pjcloudx_hardware.hard_order (tenant_id, del_flag, merchant_dept_id, order_status, merchant_type, pay_time)` | 指定商户部门的支付统计,Hash `1634334540%7C358` 等 | 对指定部门过滤收益明显;宽索引写放大较大,先对高频租户/接口评估。 |
+| P1 | `pjcloudx_wallet.wallet_manage (tenant_id, dept_id, id)` | 钱包按部门聚合 | 仅用于小维表定位钱包 ID;表规模较小时收益有限,适合与钱包统计接口一起验证。 |
+
+## 暂不建议只靠加索引解决
+
+- 统计类 SQL 使用派生表、外层权限过滤和 `ORDER BY CASE`,索引只能减少底层扫描,不能消除派生表物化/聚合成本;建议后续改为先过滤再聚合,并移除 COUNT 查询中的无意义排序。
+- `hard_order` 订单详情列表反复 `LEFT JOIN` 最新售后子查询;P0 索引后仍建议将“最新售后 ID”改为窗口/预聚合或单独维护字段。
+- `UPDATE hard_door_cabinet ... WHERE id = ? AND del_flag = ? AND tenant_id = ?` 已命中主键 `id`,慢点更可能是锁等待、事务或网络;加索引不能解决,需结合 `SHOW PROCESSLIST`、锁等待和事务耗时排查。
+
+## 上线步骤
+
+1. 低峰期逐条执行 `01_add_indexes.sql`,每条 DDL 单独观察完成时间和租户延迟。
+2. 执行前确认同名索引不存在;OceanBase 建索引可能持续几十秒到数分钟,不要重复提交。
+3. DDL 完成后对代表性 SQL 执行 `EXPLAIN`,确认使用新索引且不出现 `TABLE FULL SCAN`。
+4. 观察 15–30 分钟慢 SQL、CPU、IO、事务提交延迟;若写入延迟明显,再评估 P1 索引。
+5. 统计接口仍慢时,优先改写 SQL,再考虑继续增加宽复合索引。
+
+## 验证记录
+
+临时索引均已在测试库创建并回滚,最终检查无 `tmp_%` 索引残留。验证结果:
+
+- 订单时间分页:`TABLE RANGE SCAN ... Reverse`,不再全表扫描/大范围 TOP-N。
+- 钱包业务流水:`TABLE RANGE SCAN ... Reverse`,`biz_type(8)` 前缀索引可用。
+- 最新售后:`MERGE GROUP BY` + `TABLE RANGE SCAN`,不再全表扫描。
+- 流程历史:`TABLE RANGE SCAN ... Reverse`,不再全表扫描。
diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/app/OnlineSpuComboItemVO.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/app/OnlineSpuComboItemVO.java
new file mode 100644
index 000000000..4517706df
--- /dev/null
+++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/app/OnlineSpuComboItemVO.java
@@ -0,0 +1,27 @@
+package com.bsd.pjcloud.canteen.vo.app;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Data
+@Schema(description = "套餐组成项(App)")
+public class OnlineSpuComboItemVO {
+
+ @Schema(description = "组成菜品 SPU id")
+ private Long childSpuId;
+
+ @Schema(description = "组成菜品名称")
+ private String name;
+
+ @Schema(description = "组成菜品封面")
+ private String picUrl;
+
+ @Schema(description = "份数")
+ private Integer quantity;
+
+ @Schema(description = "排序")
+ private Integer sortOrder;
+
+ @Schema(description = "组成菜品单价(分,仅展示)")
+ private Integer price;
+}
diff --git a/pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/constant/CacheConstants.java b/pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/constant/CacheConstants.java
index 581ae65da..5450d3480 100644
--- a/pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/constant/CacheConstants.java
+++ b/pjcloud-common/pjcloud-common-core/src/main/java/com/bsd/pjcloud/common/core/constant/CacheConstants.java
@@ -169,6 +169,8 @@ public interface CacheConstants {
String HARDER_ORDER_NON_PAY = "hard:order:non_pay";
+ String HARD_XFJ_WHITE_LIST_LOCK = "hard:xfj:whitelist:lock";
+
/* ************************************* end 订单相关 end ************************************* */
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardPrintFailureStageEnum.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardPrintFailureStageEnum.java
new file mode 100644
index 000000000..065e764e4
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardPrintFailureStageEnum.java
@@ -0,0 +1,44 @@
+package com.bsd.pjcloud.hard.api.constant;
+
+import cn.hutool.core.util.ObjectUtil;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 打印任务失败阶段,帮助管理端区分配置问题、服务端故障和业务终端代理打印故障。
+ */
+@Getter
+@AllArgsConstructor
+public enum HardPrintFailureStageEnum {
+
+ /** 来源业务终端缺失、未绑定打印机或绑定关系失效。 */
+ BINDING("BINDING", "设备绑定"),
+
+ /** 服务端读取业务数据、绘制 PNG 或上传文件失败。 */
+ IMAGE_GENERATION("IMAGE_GENERATION", "图片生成"),
+
+ /** 服务端向绑定的来源业务终端发布 MQTT 消息失败。 */
+ MQTT_DISPATCH("MQTT_DISPATCH", "MQTT下发"),
+
+ /** 来源业务终端下载图片、连接 USB 或写入 ESC/POS 数据失败。 */
+ TERMINAL_PRINT("TERMINAL_PRINT", "终端打印");
+
+ /** 持久化阶段编码。 */
+ private final String stage;
+
+ /** 阶段中文说明。 */
+ private final String description;
+
+ /**
+ * 按持久化编码解析失败阶段。
+ *
+ * @param stage 阶段编码
+ * @return 匹配阶段;编码为空或未知时返回 {@code null}
+ */
+ public static HardPrintFailureStageEnum valueOfByStage(String stage) {
+ if (ObjectUtil.isEmpty(stage)) return null;
+ return Arrays.stream(values()).filter(value -> ObjectUtil.equals(stage, value.stage)).findFirst().orElse(null);
+ }
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardPrintStatusEnum.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardPrintStatusEnum.java
index 9a0eb75d9..4de0d8815 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardPrintStatusEnum.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardPrintStatusEnum.java
@@ -4,23 +4,46 @@ import cn.hutool.core.util.ObjectUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
-import javax.xml.stream.events.StartDocument;
import java.util.Arrays;
/**
- * 打印状态
+ * 打印任务状态。
+ *
+ *
编码 {@code 1} 和 {@code 2} 分别兼容历史“已生成”和“已打印”记录。
*/
@AllArgsConstructor
@Getter
public enum HardPrintStatusEnum {
- GENERATE("1", "已生成"),
+ /** 任务已落库,等待后端生成并上传打印图片。 */
+ PENDING_GENERATION("0", "待生成"),
+
+ /** 图片已生成并允许下发,历史枚举名保留为 GENERATE 以保持二进制兼容。 */
+ GENERATE("1", "待打印"),
+
+ /** 来源业务终端已确认完整 USB 数据和切纸命令写入成功。 */
PRINT("2", "已打印"),
+
+ /** 绑定、图片生成、MQTT 下发或业务终端代理打印阶段失败,可重试原任务。 */
+ FAILED("3", "打印失败"),
+
+ /** 核销回退等业务动作取消了尚未打印的任务。 */
+ CANCELLED("4", "已取消"),
;
+
+ /** 持久化状态编码。 */
private final String type;
+
+ /** 状态中文说明。 */
private final String description;
+ /**
+ * 按持久化编码解析打印状态。
+ *
+ * @param type 状态编码
+ * @return 匹配状态;编码为空或未知时返回 {@code null}
+ */
public static HardPrintStatusEnum valueOfByType(String type) {
if (ObjectUtil.isEmpty(type)) return null;
- return Arrays.stream(values()).filter(v -> ObjectUtil.equals(type, v.getType())).findFirst().get();
+ return Arrays.stream(values()).filter(v -> ObjectUtil.equals(type, v.getType())).findFirst().orElse(null);
}
}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardPrintTypeEnum.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardPrintTypeEnum.java
new file mode 100644
index 000000000..bea9223dc
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardPrintTypeEnum.java
@@ -0,0 +1,38 @@
+package com.bsd.pjcloud.hard.api.constant;
+
+import cn.hutool.core.util.ObjectUtil;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 打印凭证类型,用于选择后端图片模板和管理端业务动作。
+ */
+@Getter
+@AllArgsConstructor
+public enum HardPrintTypeEnum {
+
+ /** 支付订单的发票或消费小票。 */
+ INVOICE("1", "发票"),
+
+ /** 预约订单核销成功后生成的纸质兑换凭证。 */
+ REDEMPTION_VOUCHER("2", "兑换券");
+
+ /** 持久化类型编码。 */
+ private final String type;
+
+ /** 类型中文说明。 */
+ private final String description;
+
+ /**
+ * 按持久化编码解析打印类型。
+ *
+ * @param type 类型编码
+ * @return 匹配类型;编码为空或未知时返回 {@code null}
+ */
+ public static HardPrintTypeEnum valueOfByType(String type) {
+ if (ObjectUtil.isEmpty(type)) return null;
+ return Arrays.stream(values()).filter(value -> ObjectUtil.equals(type, value.type)).findFirst().orElse(null);
+ }
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardTypeEnum.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardTypeEnum.java
index 73afb35bf..35109ccfc 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardTypeEnum.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/HardTypeEnum.java
@@ -23,8 +23,11 @@ public enum HardTypeEnum {
OTHER("6", "其他支付方式(第三方接口对接)"),
MERCHANT_ACT("7", "商户活动"),
FARMER("8", "共富商城(助农)"),
+ YUN_XFJ("9", "云版消费机"),
YYX("10", "4G-云音响"),
YYX_WIFI("11", "wifi-云音响"),
+ /** 由绑定的超市或食堂 Android 终端代理驱动的无屏尔印 USB 打印机。 */
+ EY_PRINT("12", "尔印打印机器"),
;
private final String type;
private final String description;
@@ -44,4 +47,15 @@ public enum HardTypeEnum {
return Arrays.stream(values()).filter(v -> ObjectUtil.equals(type, v.getType())).findFirst().get();
}
+ /**
+ * 判断指定设备类型是否为尔印逻辑打印机资产。
+ *
+ * @param hardTypeEnum 待判断的设备类型
+ * @return 类型为 {@link #EY_PRINT} 时返回 {@code true}
+ */
+ public static boolean isPrintHard(HardTypeEnum hardTypeEnum) {
+ if (ObjectUtil.isEmpty(hardTypeEnum)) return false;
+ return List.of(EY_PRINT).contains(hardTypeEnum);
+ }
+
}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/TerminalHeartbeatSceneEnum.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/TerminalHeartbeatSceneEnum.java
new file mode 100644
index 000000000..02ae89603
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/TerminalHeartbeatSceneEnum.java
@@ -0,0 +1,19 @@
+package com.bsd.pjcloud.hard.api.constant;
+
+/**
+ * 终端心跳场景。
+ *
+ * 枚举名称是终端与服务端之间的固定接口编码,不得随展示文案调整。
+ */
+public enum TerminalHeartbeatSceneEnum {
+
+ /**
+ * 普通在线心跳;收到后清除之前的计划更新断开原因。
+ */
+ ONLINE,
+
+ /**
+ * 自动更新安装前心跳;用于在应用重启期间保留可解释的离线原因。
+ */
+ AUTO_UPDATE
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/order/HardHourTypeEnum.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/order/HardHourTypeEnum.java
index 2fe85d8ef..eb206c679 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/order/HardHourTypeEnum.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/order/HardHourTypeEnum.java
@@ -15,6 +15,7 @@ public enum HardHourTypeEnum {
MORNING("1", "早餐"),
NOONING("2", "午餐"),
NIGHT("3", "晚餐"),
+ LATE_NIGHT("4", "夜间餐"),
;
private final String type;
private final String description;
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/order/HardOrderEvidenceRoleEnum.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/order/HardOrderEvidenceRoleEnum.java
new file mode 100644
index 000000000..64e0846f0
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/order/HardOrderEvidenceRoleEnum.java
@@ -0,0 +1,58 @@
+package com.bsd.pjcloud.hard.api.constant.order;
+
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 订单证据图片角色,编码用于唯一位置约束和跨端展示。
+ */
+@Getter
+public enum HardOrderEvidenceRoleEnum {
+
+ /**
+ * 人脸库中被命中的后台参考人脸,由服务端从受信文件中心复制为订单快照。
+ */
+ FACE_REFERENCE("FACE_REFERENCE", "后台参考人脸"),
+
+ /**
+ * 终端实际通过比对的现场人脸裁剪图,由终端异步上传。
+ */
+ FACE_LIVE("FACE_LIVE", "现场识别人脸"),
+
+ /**
+ * 菜谱识别的现场餐盘图片预留角色,当前版本不创建该类证据。
+ */
+ MENU_LIVE("MENU_LIVE", "餐盘现场图");
+
+ /**
+ * 数据库和接口使用的稳定编码。
+ */
+ private final String code;
+
+ /**
+ * PC 订单详情使用的中文名称。
+ */
+ private final String description;
+
+ /**
+ * 构造订单证据图片角色。
+ *
+ * @param code 数据库与跨端协议编码
+ * @param description 中文展示名称
+ */
+ HardOrderEvidenceRoleEnum(String code, String description) {
+ this.code = code;
+ this.description = description;
+ }
+
+ /**
+ * 按协议编码解析图片角色,未知编码返回 {@code null}。
+ *
+ * @param code 数据库或接口编码
+ * @return 对应角色;未知时返回 {@code null}
+ */
+ public static HardOrderEvidenceRoleEnum valueOfCode(String code) {
+ return Arrays.stream(values()).filter(item -> item.code.equals(code)).findFirst().orElse(null);
+ }
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/order/HardOrderEvidenceStatusEnum.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/order/HardOrderEvidenceStatusEnum.java
new file mode 100644
index 000000000..f74b4c130
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/order/HardOrderEvidenceStatusEnum.java
@@ -0,0 +1,58 @@
+package com.bsd.pjcloud.hard.api.constant.order;
+
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 服务端订单证据状态,表达证据是否仍待补传、已经可查看或已永久缺失。
+ */
+@Getter
+public enum HardOrderEvidenceStatusEnum {
+
+ /**
+ * 订单已记录识别元数据,但图片仍等待终端上传或服务端复制。
+ */
+ PENDING("PENDING", "待补传"),
+
+ /**
+ * 文件中心已经保存订单专属图片并可在订单详情中查看。
+ */
+ AVAILABLE("AVAILABLE", "可查看"),
+
+ /**
+ * 本地文件过期、损坏或参考图片变化,已确认无法继续补齐。
+ */
+ UNAVAILABLE("UNAVAILABLE", "永久缺失");
+
+ /**
+ * 数据库和接口使用的稳定编码。
+ */
+ private final String code;
+
+ /**
+ * PC 订单详情使用的中文名称。
+ */
+ private final String description;
+
+ /**
+ * 构造订单证据状态。
+ *
+ * @param code 数据库与跨端协议编码
+ * @param description 中文展示名称
+ */
+ HardOrderEvidenceStatusEnum(String code, String description) {
+ this.code = code;
+ this.description = description;
+ }
+
+ /**
+ * 按协议编码解析证据状态,未知编码返回 {@code null}。
+ *
+ * @param code 数据库或接口编码
+ * @return 对应状态;未知时返回 {@code null}
+ */
+ public static HardOrderEvidenceStatusEnum valueOfCode(String code) {
+ return Arrays.stream(values()).filter(item -> item.code.equals(code)).findFirst().orElse(null);
+ }
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/order/HardOrderEvidenceTypeEnum.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/order/HardOrderEvidenceTypeEnum.java
new file mode 100644
index 000000000..53101e286
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/order/HardOrderEvidenceTypeEnum.java
@@ -0,0 +1,53 @@
+package com.bsd.pjcloud.hard.api.constant.order;
+
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 订单证据业务类型,编码同时用于数据库、终端 SQLite 和跨端接口,禁止随展示文案调整。
+ */
+@Getter
+public enum HardOrderEvidenceTypeEnum {
+
+ /**
+ * 人脸识别证据,当前版本保存后台参考人脸和现场识别人脸。
+ */
+ FACE_RECOGNITION("FACE_RECOGNITION", "人脸识别"),
+
+ /**
+ * 菜谱识别证据预留值,当前版本不执行菜谱识别或图片采集。
+ */
+ MENU_RECOGNITION("MENU_RECOGNITION", "菜谱识别");
+
+ /**
+ * 数据库和接口使用的稳定编码。
+ */
+ private final String code;
+
+ /**
+ * PC 订单详情使用的中文名称。
+ */
+ private final String description;
+
+ /**
+ * 构造订单证据业务类型。
+ *
+ * @param code 数据库与跨端协议编码
+ * @param description 中文展示名称
+ */
+ HardOrderEvidenceTypeEnum(String code, String description) {
+ this.code = code;
+ this.description = description;
+ }
+
+ /**
+ * 按协议编码解析证据类型,未知编码返回 {@code null} 供调用方拒绝非法请求。
+ *
+ * @param code 数据库或接口编码
+ * @return 对应类型;未知时返回 {@code null}
+ */
+ public static HardOrderEvidenceTypeEnum valueOfCode(String code) {
+ return Arrays.stream(values()).filter(item -> item.code.equals(code)).findFirst().orElse(null);
+ }
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/order/HardOrderStatusEnum.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/order/HardOrderStatusEnum.java
index 742a76838..a4c84a9c5 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/order/HardOrderStatusEnum.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/order/HardOrderStatusEnum.java
@@ -20,6 +20,11 @@ public enum HardOrderStatusEnum {
REFUND("10", "退款"),
CANCEL("20", "已核销"),
UNFOLLOW("30", "已取消(智能柜)"),
+ TAKE_OUT_AWAIT("100", "待接单(外卖)"),
+ TAKE_OUT_ING("110", "配送中(外卖)"),
+ SHOP_WAIT_PAY_SUCCESS("200", "待发货/已支付(商城购买)"),
+ SHOP_WAIT_RECEIVE("210", "待收货(商城购买)"),
+ SHOP_WAIT_EVALUATE("220", "订单完成/待评价(商城购买)"),
;
private final String type;
private final String description;
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/order/HardOrderTypeEnum.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/order/HardOrderTypeEnum.java
index 4f1e1fd25..fd2380239 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/order/HardOrderTypeEnum.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/order/HardOrderTypeEnum.java
@@ -21,6 +21,8 @@ public enum HardOrderTypeEnum {
LB("6", "宁波银行(美好生活)"),
FARMER("7", "共富商城(助农)"),
TOC_VISITOR("8", "toc访客购买(不记名用户)"),
+ XFJ("9", "消费机支付"),
+ TAKE_OUT("10", "外卖"),
;
private final String type;
private final String description;
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/xfj/HardXfjTypeEnum.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/xfj/HardXfjTypeEnum.java
new file mode 100644
index 000000000..f2ece356a
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/xfj/HardXfjTypeEnum.java
@@ -0,0 +1,38 @@
+package com.bsd.pjcloud.hard.api.constant.xfj;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.bsd.pjcloud.hard.api.constant.order.HardPayTypeEnum;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * 硬件授权类型
+ */
+@AllArgsConstructor
+@Getter
+public enum HardXfjTypeEnum {
+
+ QRCODE_PAY("10", "钱包二维码支付"),
+ WECHAT_QRCODE_PAY("11", "微信二维码支付"),
+ ALI_QRCODE_PAY("12", "支付宝二维码支付"),
+ CARD_PAY("20", "饭卡支付"),
+ ;
+ private final String type;
+ private final String description;
+
+ public static HardXfjTypeEnum valueOfByType(String type) {
+ if (ObjectUtil.isEmpty(type)) return null;
+ return Arrays.stream(values()).filter(v -> ObjectUtil.equals(type, v.getType())).findFirst().orElse(null);
+ }
+
+ public static HardPayTypeEnum getHardPayType(HardXfjTypeEnum type) {
+ return switch (type) {
+ case QRCODE_PAY -> HardPayTypeEnum.WALLET_QR_CODE;
+ case WECHAT_QRCODE_PAY -> HardPayTypeEnum.WECHANT_SELF_QR_CODE_PAY;
+ case ALI_QRCODE_PAY -> HardPayTypeEnum.ALI_SELF_QR_CODE_PAY;
+ case CARD_PAY -> HardPayTypeEnum.WALLET_CARD;
+ };
+ }
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrintRequestDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrintRequestDto.java
new file mode 100644
index 000000000..556b94251
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrintRequestDto.java
@@ -0,0 +1,32 @@
+package com.bsd.pjcloud.hard.api.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * 管理端创建发票或兑换券打印任务的请求。
+ */
+@Data
+@Schema(description = "创建打印任务请求")
+public class HardPrintRequestDto {
+
+ /** 待打印凭证所属订单 ID。 */
+ @NotNull(message = "订单ID不能为空")
+ @Schema(description = "订单ID")
+ private Long orderId;
+
+ /** 打印类型编码,1 发票、2 兑换券。 */
+ @NotBlank(message = "打印类型不能为空")
+ @Schema(description = "打印类型")
+ private String printType;
+
+ /** 来源业务终端 ID;订单没有来源终端且同商户存在多个绑定时必填。 */
+ @Schema(description = "来源业务终端ID")
+ private Long sourceHardId;
+
+ /** 已打印任务再次创建下一序号时必填的重打原因。 */
+ @Schema(description = "重打原因")
+ private String reprintReason;
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrintResultDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrintResultDto.java
new file mode 100644
index 000000000..d40276456
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrintResultDto.java
@@ -0,0 +1,45 @@
+package com.bsd.pjcloud.hard.api.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * 食堂或超市终端代理无屏 EY_PRINT 提交的打印结果回执。
+ */
+@Data
+@Schema(description = "业务终端代理打印结果回执")
+public class HardPrintResultDto {
+
+ /** hard_invoice 打印任务主键。 */
+ @NotNull(message = "打印任务ID不能为空")
+ @Schema(description = "打印任务ID")
+ private Long taskId;
+
+ /** 任务所属订单 ID,用于阻断任务与订单错配。 */
+ @NotNull(message = "订单ID不能为空")
+ @Schema(description = "订单ID")
+ private Long orderId;
+
+ /** 来源业务终端实际处理的重试版本。 */
+ @NotNull(message = "重试版本不能为空")
+ @Min(value = 0, message = "重试版本不能小于0")
+ @Schema(description = "重试版本")
+ private Integer retryVersion;
+
+ /** 完整 USB 数据及切纸命令是否已经被接口接受。 */
+ @NotNull(message = "打印结果不能为空")
+ @Schema(description = "是否成功")
+ private Boolean success;
+
+ /** 本版本在来源业务终端执行的 USB 尝试次数。 */
+ @NotNull(message = "尝试次数不能为空")
+ @Min(value = 1, message = "尝试次数不能小于1")
+ @Schema(description = "终端尝试次数")
+ private Integer attemptCount;
+
+ /** 失败时的可诊断错误摘要,不得包含凭证或密码。 */
+ @Schema(description = "错误摘要")
+ private String errorMessage;
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrintRetryDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrintRetryDto.java
new file mode 100644
index 000000000..ca940e5c4
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrintRetryDto.java
@@ -0,0 +1,31 @@
+package com.bsd.pjcloud.hard.api.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * 对既有打印任务执行失败重试或已打印重打的请求。
+ */
+@Data
+@Schema(description = "打印任务重试或重打请求")
+public class HardPrintRetryDto {
+
+ /** 既有 hard_invoice 打印任务主键。 */
+ @NotNull(message = "打印任务ID不能为空")
+ @Schema(description = "打印任务ID")
+ private Long historyId;
+
+ /**
+ * 失败任务需要补录的来源业务终端 ID。
+ *
+ * 仅当原任务因缺少来源终端而失败时使用;服务端仍会校验该终端与订单同商户,
+ * 并根据服务端绑定关系确定目标 EY_PRINT,客户端不能直接指定打印机。
+ */
+ @Schema(description = "补录的来源业务终端ID")
+ private Long sourceHardId;
+
+ /** 已打印任务重打时必须填写的原因;失败任务原任务重试时可为空。 */
+ @Schema(description = "重打原因")
+ private String reprintReason;
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrinterBindingDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrinterBindingDto.java
new file mode 100644
index 000000000..cd1a4cf8e
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrinterBindingDto.java
@@ -0,0 +1,23 @@
+package com.bsd.pjcloud.hard.api.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * 创建业务终端与无屏尔印 USB 打印机绑定关系的请求。
+ */
+@Data
+@Schema(description = "USB 打印机绑定请求")
+public class HardPrinterBindingDto {
+
+ /** SUPERMARKET 或 CANTEEN 来源终端 ID。 */
+ @NotNull(message = "来源终端不能为空")
+ @Schema(description = "来源业务终端ID")
+ private Long sourceHardId;
+
+ /** EY_PRINT 逻辑打印机资产 ID。 */
+ @NotNull(message = "打印机不能为空")
+ @Schema(description = "逻辑打印机资产ID")
+ private Long printerHardId;
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrinterConfigDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrinterConfigDto.java
new file mode 100644
index 000000000..1e4bff1b3
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrinterConfigDto.java
@@ -0,0 +1,46 @@
+package com.bsd.pjcloud.hard.api.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+/**
+ * 保存无屏尔印 USB 打印机参数的请求。
+ */
+@Data
+@Schema(description = "USB 打印机配置请求")
+public class HardPrinterConfigDto {
+
+ /** EY_PRINT 逻辑打印机资产 ID。 */
+ @NotNull(message = "打印机不能为空")
+ @Schema(description = "逻辑打印机资产ID")
+ private Long printerHardId;
+
+ /** 真机型号;未联调前允许为空。 */
+ @Schema(description = "打印机型号")
+ private String printerModel;
+
+ /** USB Vendor ID 十进制值;未联调前允许为空。 */
+ @Schema(description = "USB VID")
+ private Integer usbVendorId;
+
+ /** USB Product ID 十进制值;未联调前允许为空。 */
+ @Schema(description = "USB PID")
+ private Integer usbProductId;
+
+ /** 纸张宽度,首期只允许 80。 */
+ @NotNull(message = "纸宽不能为空")
+ @Schema(description = "纸宽,单位mm")
+ private Integer paperWidth;
+
+ /** 自动切刀开关,1 开启、0 关闭。 */
+ @NotBlank(message = "自动切刀开关不能为空")
+ @Schema(description = "自动切刀开关")
+ private String autoCut;
+
+ /** 图片指令模式,只允许 GS_V0 或 ESC_STAR。 */
+ @NotBlank(message = "图片指令模式不能为空")
+ @Schema(description = "图片指令模式")
+ private String imageMode;
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrinterInfoReportDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrinterInfoReportDto.java
new file mode 100644
index 000000000..c7c5312cf
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardPrinterInfoReportDto.java
@@ -0,0 +1,37 @@
+package com.bsd.pjcloud.hard.api.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+/**
+ * 食堂或超市终端代理上报实际 USB 打印机信息的请求。
+ */
+@Data
+@Schema(description = "业务终端代理上报的 USB 打印机信息")
+public class HardPrinterInfoReportDto {
+
+ /** USB 描述符中的厂商和产品名称组合,最长保存 128 个字符。 */
+ @NotBlank(message = "打印机型号不能为空")
+ @Size(max = 128, message = "打印机型号不能超过128个字符")
+ @Schema(description = "实际打印机型号")
+ private String printerModel;
+
+ /** USB Vendor ID 十进制值,取值范围为无符号 16 位整数。 */
+ @NotNull(message = "USB VID不能为空")
+ @Min(value = 0, message = "USB VID必须在0到65535之间")
+ @Max(value = 65535, message = "USB VID必须在0到65535之间")
+ @Schema(description = "USB VID")
+ private Integer usbVendorId;
+
+ /** USB Product ID 十进制值,取值范围为无符号 16 位整数。 */
+ @NotNull(message = "USB PID不能为空")
+ @Min(value = 0, message = "USB PID必须在0到65535之间")
+ @Max(value = 65535, message = "USB PID必须在0到65535之间")
+ @Schema(description = "USB PID")
+ private Integer usbProductId;
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/hard/TerminalHeartbeatDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/hard/TerminalHeartbeatDto.java
new file mode 100644
index 000000000..a5b7d93b6
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/hard/TerminalHeartbeatDto.java
@@ -0,0 +1,85 @@
+package com.bsd.pjcloud.hard.api.dto.hard;
+
+import com.bsd.pjcloud.hard.api.constant.TerminalHeartbeatSceneEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+import lombok.Data;
+
+import java.util.Map;
+
+/**
+ * 食堂、超市 Android 终端心跳请求。
+ */
+@Data
+@Schema(description = "终端心跳请求")
+public class TerminalHeartbeatDto {
+
+ /**
+ * APK 展示版本号,来源于终端当前安装包;取不到时允许为空。
+ */
+ @Schema(description = "APK展示版本号")
+ private String apkVersionName;
+
+ /**
+ * APK 内部版本码,来源于终端当前安装包;取不到时允许为空。
+ */
+ @Schema(description = "APK内部版本码")
+ private Long apkVersionCode;
+
+ /**
+ * 本次上报场景,使用 {@link TerminalHeartbeatSceneEnum} 的固定接口名称。
+ */
+ @NotNull(message = "心跳场景不能为空")
+ @Schema(description = "心跳场景", requiredMode = Schema.RequiredMode.REQUIRED)
+ private TerminalHeartbeatSceneEnum scene;
+
+ /**
+ * Android 系统本次启动后的运行秒数,用于区分整机重启与应用进程重启。
+ */
+ @Schema(description = "Android系统运行秒数")
+ private Long systemUptimeSeconds;
+
+ /**
+ * 当前应用进程运行秒数,用于识别应用崩溃或被系统重新拉起。
+ */
+ @Schema(description = "应用进程运行秒数")
+ private Long appUptimeSeconds;
+
+ /**
+ * 最近上报时使用的网络类型,例如 ethernet、wifi、4g 或 offline。
+ */
+ @Schema(description = "网络类型")
+ private String networkType;
+
+ /**
+ * 应用私有目录所在存储卷的可用空间,单位为字节。
+ */
+ @Schema(description = "可用存储空间,单位字节")
+ private Long availableStorageBytes;
+
+ /**
+ * Root 最近检测状态,持久化值固定为 unknown、available 或 unavailable。
+ */
+ @Pattern(regexp = "unknown|available|unavailable", message = "Root状态不正确")
+ @Schema(description = "Root状态")
+ private String rootStatus;
+
+ /**
+ * 当前应用是否已被 Android 配置为 Device Owner;无法读取时为空。
+ */
+ @Schema(description = "是否为Device Owner")
+ private Boolean deviceOwner;
+
+ /**
+ * 最近上报时是否处于 Kiosk 锁定任务模式;无法读取时为空。
+ */
+ @Schema(description = "是否处于Kiosk模式")
+ private Boolean kioskActive;
+
+ /**
+ * 可选外围硬件状态,以稳定功能编码映射布尔值,例如 printer、steelyard、cardReader、subScreen。
+ */
+ @Schema(description = "可选外围硬件状态")
+ private Map deviceStatus;
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/manager/HardTerminalLogFileQueryDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/manager/HardTerminalLogFileQueryDto.java
new file mode 100644
index 000000000..fd347365c
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/manager/HardTerminalLogFileQueryDto.java
@@ -0,0 +1,47 @@
+package com.bsd.pjcloud.hard.api.dto.manager;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * 终端日志管理端查询条件。
+ */
+@Data
+@Schema(description = "终端日志文件查询条件")
+public class HardTerminalLogFileQueryDto {
+
+ @Schema(description = "设备ID")
+ private Long hardId;
+
+ @Schema(description = "设备名称")
+ private String hardName;
+
+ @Schema(description = "设备唯一编号")
+ private String uniqueCode;
+
+ @Schema(description = "商户ID")
+ private Long merchantId;
+
+ @Schema(description = "部门ID")
+ private Long deptId;
+
+ @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
+ @Schema(description = "日志日期开始")
+ private LocalDate logDateStart;
+
+ @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
+ @Schema(description = "日志日期结束")
+ private LocalDate logDateEnd;
+
+ @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
+ @Schema(description = "上传时间开始")
+ private LocalDateTime uploadTimeStart;
+
+ @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
+ @Schema(description = "上传时间结束")
+ private LocalDateTime uploadTimeEnd;
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/manager/HardTerminalLogPreflightDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/manager/HardTerminalLogPreflightDto.java
new file mode 100644
index 000000000..a2c6ecaf8
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/manager/HardTerminalLogPreflightDto.java
@@ -0,0 +1,32 @@
+package com.bsd.pjcloud.hard.api.dto.manager;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+/**
+ * 终端日志上传预检参数。
+ */
+@Data
+@Schema(description = "终端日志上传预检参数")
+public class HardTerminalLogPreflightDto {
+
+ @NotBlank(message = "日志相对路径不能为空")
+ @Size(max = 255, message = "日志相对路径不能超过255个字符")
+ @Schema(description = "终端日志根目录下的相对路径")
+ private String relativePath;
+
+ @NotNull(message = "文件大小不能为空")
+ @Min(value = 0, message = "文件大小不能小于0")
+ @Schema(description = "脱敏文件大小")
+ private Long fileSize;
+
+ @NotBlank(message = "文件哈希不能为空")
+ @Pattern(regexp = "(?i)^[0-9a-f]{32}$", message = "文件哈希必须为32位MD5")
+ @Schema(description = "脱敏文件MD5")
+ private String fileHash;
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/manager/HardTerminalLogUploadDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/manager/HardTerminalLogUploadDto.java
new file mode 100644
index 000000000..8d8868cf1
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/manager/HardTerminalLogUploadDto.java
@@ -0,0 +1,43 @@
+package com.bsd.pjcloud.hard.api.dto.manager;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * 终端日志上传元数据。设备快照由服务端根据请求头中的设备ID填充。
+ */
+@Data
+@Schema(description = "终端日志上传元数据")
+public class HardTerminalLogUploadDto {
+
+ @NotBlank(message = "日志相对路径不能为空")
+ @Size(max = 255, message = "日志相对路径不能超过255个字符")
+ @Schema(description = "终端日志根目录下的相对路径")
+ private String relativePath;
+
+ @NotNull(message = "日志日期不能为空")
+ @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
+ @Schema(description = "日志所属日期")
+ private LocalDate logDate;
+
+ @NotNull(message = "本地修改时间不能为空")
+ @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
+ @Schema(description = "终端本地文件最后修改时间")
+ private LocalDateTime fileModifiedTime;
+
+ @Size(max = 64, message = "应用版本不能超过64个字符")
+ @Schema(description = "应用版本")
+ private String appVersion;
+
+ @NotBlank(message = "脱敏规则版本不能为空")
+ @Size(max = 32, message = "脱敏规则版本不能超过32个字符")
+ @Schema(description = "脱敏规则版本")
+ private String sanitizeVersion;
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderEvidenceMetadataDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderEvidenceMetadataDto.java
new file mode 100644
index 000000000..585bbd7bd
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderEvidenceMetadataDto.java
@@ -0,0 +1,130 @@
+package com.bsd.pjcloud.hard.api.dto.order;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.DecimalMax;
+import jakarta.validation.constraints.DecimalMin;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 订单创建和证据补传共用的识别元数据,不包含图片字节和服务端文件信息。
+ */
+@Data
+@Schema(description = "订单识别证据元数据")
+public class HardOrderEvidenceMetadataDto {
+
+ /**
+ * 证据业务类型,使用 HardOrderEvidenceTypeEnum 的稳定编码。
+ */
+ @NotBlank(message = "证据类型不能为空")
+ @Size(max = 32, message = "证据类型不能超过32个字符")
+ @Schema(description = "证据类型")
+ private String evidenceType;
+
+ /**
+ * 图片角色,使用 HardOrderEvidenceRoleEnum 的稳定编码。
+ */
+ @NotBlank(message = "证据图片角色不能为空")
+ @Size(max = 32, message = "证据图片角色不能超过32个字符")
+ @Schema(description = "证据图片角色")
+ private String evidenceRole;
+
+ /**
+ * 同类型同角色下的图片序号,从0开始;人脸证据固定为0,菜谱识别可递增。
+ */
+ @NotNull(message = "证据图片序号不能为空")
+ @Min(value = 0, message = "证据图片序号不能小于0")
+ @Max(value = 99, message = "证据图片序号不能大于99")
+ @Schema(description = "证据图片序号")
+ private Integer sequenceNo;
+
+ /**
+ * 识别用户主键;未识别到平台用户时为空,不能由终端使用其他用户替代。
+ */
+ @Schema(description = "识别用户主键")
+ private Long userId;
+
+ /**
+ * 虹软搜索样本编号;仅人脸识别角色使用,服务端据此校验后台参考人脸。
+ */
+ @Size(max = 64, message = "人脸样本编号不能超过64个字符")
+ @Schema(description = "人脸样本编号")
+ private String faceSampleId;
+
+ /**
+ * 识别时终端人脸库记录的参考图片地址,仅作为服务端复制参考图的线索,不直接作为可访问地址返回。
+ */
+ @Size(max = 512, message = "参考人脸地址不能超过512个字符")
+ @Schema(description = "参考人脸地址")
+ private String referenceImageUrl;
+
+ /**
+ * 识别时终端人脸快照对应的后台更新时间;旧终端允许为空,新终端用于识别同地址内容替换。
+ */
+ @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss[.SSS]", timezone = "Asia/Shanghai")
+ @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
+ @Schema(description = "参考人脸来源版本")
+ private LocalDateTime referenceImageVersion;
+
+ /**
+ * 识别时实际相似度,取值0到1;为空表示当前证据类型不使用相似度。
+ */
+ @DecimalMin(value = "0", message = "相似度不能小于0")
+ @DecimalMax(value = "1", message = "相似度不能大于1")
+ @Schema(description = "识别相似度")
+ private BigDecimal similarity;
+
+ /**
+ * 识别时终端生效的相似度阈值快照,取值0到1。
+ */
+ @DecimalMin(value = "0", message = "相似度阈值不能小于0")
+ @DecimalMax(value = "1", message = "相似度阈值不能大于1")
+ @Schema(description = "相似度阈值快照")
+ private BigDecimal similarityThreshold;
+
+ /**
+ * 终端实际完成识别的本地时间;为空时服务端仅保留订单创建时间。
+ */
+ @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss[.SSS]", timezone = "Asia/Shanghai")
+ @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
+ @Schema(description = "终端采集时间")
+ private LocalDateTime capturedAt;
+
+ /**
+ * 终端按原始证据文件计算的 SHA-256;图片尚未生成或永久缺失时允许为空。
+ */
+ @Pattern(regexp = "(?i)^[0-9a-f]{64}$", message = "图片哈希必须为64位SHA-256")
+ @Schema(description = "图片SHA-256")
+ private String imageHash;
+
+ /**
+ * 终端证据文件大小,单位字节;参考图无法读取或文件尚未生成时允许为空。
+ */
+ @Min(value = 0, message = "图片大小不能小于0")
+ @Schema(description = "图片大小,单位字节")
+ private Long imageSize;
+
+ /**
+ * 终端解码后的图片宽度,单位像素。
+ */
+ @Min(value = 1, message = "图片宽度必须大于0")
+ @Schema(description = "图片宽度,单位像素")
+ private Integer imageWidth;
+
+ /**
+ * 终端解码后的图片高度,单位像素。
+ */
+ @Min(value = 1, message = "图片高度必须大于0")
+ @Schema(description = "图片高度,单位像素")
+ private Integer imageHeight;
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderEvidenceOrderRefDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderEvidenceOrderRefDto.java
new file mode 100644
index 000000000..510a7e4e8
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderEvidenceOrderRefDto.java
@@ -0,0 +1,45 @@
+package com.bsd.pjcloud.hard.api.dto.order;
+
+import cn.hutool.core.util.StrUtil;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.AssertTrue;
+import jakarta.validation.constraints.Positive;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 终端订单识别证据的订单定位参数。
+ *
+ * 消费机现场下单继续使用终端订单号对账;预约核销订单不是当前终端创建的,核销成功后改用
+ * 服务端订单 ID 定位。两种方式都必须结合请求头中的设备 ID 校验订单归属。
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Schema(description = "订单识别证据的订单定位参数")
+public abstract class HardOrderEvidenceOrderRefDto extends HardOrderEvidenceMetadataDto {
+
+ /**
+ * 终端本地稳定订单号,用于消费机现场下单后对账;预约核销证据允许为空。
+ */
+ @Size(max = 128, message = "终端订单号不能超过128个字符")
+ @Schema(description = "终端订单号,与服务端订单ID二选一")
+ private String androidOrderCode;
+
+ /**
+ * 已存在的服务端订单主键,仅用于当前设备已经成功核销的预约订单;现场下单证据允许为空。
+ */
+ @Positive(message = "服务端订单ID必须大于0")
+ @Schema(description = "服务端订单ID,与终端订单号二选一")
+ private Long orderId;
+
+ /**
+ * 校验至少提供一种订单定位信息,防止无条件查询或写入订单证据。
+ *
+ * @return 提供终端订单号或服务端订单 ID 时返回 {@code true}
+ */
+ @AssertTrue(message = "终端订单号和服务端订单ID不能同时为空")
+ public boolean isOrderReferencePresent() {
+ return StrUtil.isNotBlank(androidOrderCode) || orderId != null;
+ }
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderEvidenceSubmitDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderEvidenceSubmitDto.java
new file mode 100644
index 000000000..343b09788
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderEvidenceSubmitDto.java
@@ -0,0 +1,38 @@
+package com.bsd.pjcloud.hard.api.dto.order;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 终端提交订单证据元数据或永久缺失结果的请求。
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Schema(description = "订单证据元数据提交请求")
+public class HardOrderEvidenceSubmitDto extends HardOrderEvidenceOrderRefDto {
+
+ /**
+ * 终端请求写入的目标状态,仅允许 PENDING 或 UNAVAILABLE;现场图片上传成功由服务端改为 AVAILABLE。
+ */
+ @NotBlank(message = "证据状态不能为空")
+ @Size(max = 32, message = "证据状态不能超过32个字符")
+ @Schema(description = "证据状态")
+ private String status;
+
+ /**
+ * 永久缺失原因;状态为 UNAVAILABLE 时必填,禁止包含本地完整敏感路径。
+ */
+ @Size(max = 500, message = "失败原因不能超过500个字符")
+ @Schema(description = "永久缺失原因")
+ private String failureReason;
+
+ /**
+ * 产生识别证据的终端应用版本,用于问题追溯。
+ */
+ @Size(max = 64, message = "应用版本不能超过64个字符")
+ @Schema(description = "终端应用版本")
+ private String appVersion;
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderEvidenceUploadDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderEvidenceUploadDto.java
new file mode 100644
index 000000000..4b8f33633
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderEvidenceUploadDto.java
@@ -0,0 +1,22 @@
+package com.bsd.pjcloud.hard.api.dto.order;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 终端上传订单现场证据图片时随 multipart 一并提交的元数据。
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Schema(description = "订单现场证据上传请求")
+public class HardOrderEvidenceUploadDto extends HardOrderEvidenceOrderRefDto {
+
+ /**
+ * 产生识别证据的终端应用版本,用于问题追溯。
+ */
+ @Size(max = 64, message = "应用版本不能超过64个字符")
+ @Schema(description = "终端应用版本")
+ private String appVersion;
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/xfj/HardXfjFastCreateOrderDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/xfj/HardXfjFastCreateOrderDto.java
new file mode 100644
index 000000000..60df1bf4d
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/xfj/HardXfjFastCreateOrderDto.java
@@ -0,0 +1,51 @@
+package com.bsd.pjcloud.hard.api.dto.order.xfj;
+
+import com.bsd.pjcloud.hard.api.constant.xfj.HardXfjTypeEnum;
+import com.bsd.pjcloud.thirdparty.constant.miniplatform.AppPlusPlatformTypeEnum;
+import com.bsd.pjcloud.wallet.api.constant.WalletTypeEnum;
+import lombok.Data;
+
+@Data
+public class HardXfjFastCreateOrderDto {
+
+ /**
+ * 用户id
+ */
+ private Long userId;
+
+ /**
+ * 设备id
+ */
+ private Long hardId;
+
+ /**
+ * 订单编号
+ */
+ private String orderNo;
+
+ /**
+ * 支付金额(分)
+ */
+ private int price;
+
+ /**
+ * 钱包类型
+ */
+ private WalletTypeEnum walletType;
+
+ /**
+ * 消费机支付类型
+ */
+ private HardXfjTypeEnum xfjPayType;
+
+ /**
+ * 终端支付类型
+ */
+ private AppPlusPlatformTypeEnum platformType;
+
+ /**
+ * 付款码支付code
+ */
+ private String devicePayCode;
+
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/xfj/HardXfjLogDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/xfj/HardXfjLogDto.java
new file mode 100644
index 000000000..484765e44
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/xfj/HardXfjLogDto.java
@@ -0,0 +1,18 @@
+package com.bsd.pjcloud.hard.api.dto.xfj;
+
+import com.bsd.pjcloud.hard.api.entity.xfj.HardXfjLogEntity;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+public class HardXfjLogDto extends HardXfjLogEntity {
+
+ /**
+ * 设备名称
+ */
+ private String hardName;
+
+ private LocalDateTime[] createTimeList;
+
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/xfj/XfjConsumTransactionsDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/xfj/XfjConsumTransactionsDto.java
new file mode 100644
index 000000000..0e4edb731
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/xfj/XfjConsumTransactionsDto.java
@@ -0,0 +1,72 @@
+package com.bsd.pjcloud.hard.api.dto.xfj;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class XfjConsumTransactionsDto {
+
+ /**
+ * 消费序号(年月日时分秒+2个字节序号(0~65535)),用于识别上传数据不重复
+ */
+ private String Order;
+
+ /**
+ * 10进制卡序列号(实体卡号或虚拟卡号)
+ */
+ private String CardNo;
+
+ /**
+ * 卡模式
+ * 0:实体卡
+ * 1:电子卡
+ */
+ private Integer CardMode;
+
+ /**
+ * 交易模式
+ * 0: 刷卡扣费
+ * 1: 现金充值
+ * 2: 余额查询
+ * 3: 钱包转账
+ * 4: 现金退款
+ */
+ private Integer Mode;
+
+ /**
+ * 扣费类型
+ * 0:手动
+ * 1:菜单
+ * 2:定值
+ * 3:取餐
+ * 4:计时
+ * 5:计次
+ */
+ private Integer PayType;
+
+ /**
+ * 消费金额(Mode为2时,此字段传入值可能为0)
+ */
+ private String Amount;
+
+ /**
+ * 菜品消费列表
+ */
+ List Menus;
+
+ @Data
+ static public class Menus {
+
+ /**
+ * 菜品编号(与菜品查询接口菜品编号一致)
+ */
+ private String MenuID;
+
+ /**
+ * 菜品数量(支持小数)
+ */
+ private String Count;
+
+ }
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/xfj/XfjMenusDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/xfj/XfjMenusDto.java
new file mode 100644
index 000000000..f266612b8
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/xfj/XfjMenusDto.java
@@ -0,0 +1,13 @@
+package com.bsd.pjcloud.hard.api.dto.xfj;
+
+import lombok.Data;
+
+@Data
+public class XfjMenusDto {
+
+ /**
+ * 菜品编号(操作员在消费机键盘输入的数字)
+ */
+ private String MenuNo;
+
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/xfj/XfjOffLinesDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/xfj/XfjOffLinesDto.java
new file mode 100644
index 000000000..daa9bc76b
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/xfj/XfjOffLinesDto.java
@@ -0,0 +1,48 @@
+package com.bsd.pjcloud.hard.api.dto.xfj;
+
+import lombok.Data;
+
+@Data
+public class XfjOffLinesDto {
+
+ /**
+ * 终端设备ID
+ */
+ private Integer DeviceNumber;
+
+ /**
+ * 消费序号(年月日时分秒+2个字节序号),用于识别上传数据不重复
+ */
+ private String Order;
+
+ /**
+ * 扣费类型
+ * 0:手动模式
+ * 1:菜单模式
+ * 2:定值模式
+ */
+ private Integer PayType;
+
+ /**
+ * 卡模式
+ * 0:实体卡
+ * 1:电子卡
+ */
+ private Integer CardMode;
+
+ /**
+ * 消费时间年月日时分秒
+ */
+ private String Time;
+
+ /**
+ * 卡号10进制(实体卡号或虚拟卡号)
+ */
+ private String CardNo;
+
+ /**
+ * 扣费金额
+ */
+ private String Money;
+
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/xfj/XfjOrderQueryDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/xfj/XfjOrderQueryDto.java
new file mode 100644
index 000000000..324ef273a
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/xfj/XfjOrderQueryDto.java
@@ -0,0 +1,25 @@
+package com.bsd.pjcloud.hard.api.dto.xfj;
+
+import lombok.Data;
+
+@Data
+public class XfjOrderQueryDto {
+
+ /**
+ * 消费序号(年月日时分秒+2个字节序号(0~65535)),用于识别上传数据不重复
+ */
+ private String Order;
+
+ /**
+ * 10进制卡序列号(实体卡号或虚拟卡号)
+ */
+ private String CardNo;
+
+ /**
+ * 卡模式
+ * 0:实体卡
+ * 1:电子卡
+ */
+ private Integer CardMode;
+
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/xfj/XfjOrderRefundDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/xfj/XfjOrderRefundDto.java
new file mode 100644
index 000000000..6098006ef
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/xfj/XfjOrderRefundDto.java
@@ -0,0 +1,17 @@
+package com.bsd.pjcloud.hard.api.dto.xfj;
+
+import lombok.Data;
+
+@Data
+public class XfjOrderRefundDto {
+
+ /**
+ * 消费订单号(最大30字符)
+ */
+ private String OrderSeralid;
+
+ /**
+ * 退款金额
+ */
+ private String Amount;
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/xfj/XfjQRCodeTransactionDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/xfj/XfjQRCodeTransactionDto.java
new file mode 100644
index 000000000..0ccc8b8d7
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/xfj/XfjQRCodeTransactionDto.java
@@ -0,0 +1,55 @@
+package com.bsd.pjcloud.hard.api.dto.xfj;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class XfjQRCodeTransactionDto {
+
+ /**
+ * 交易模式
+ * 0:扣费
+ * 1:充值
+ * 3:信息查询
+ */
+ private String Mode;
+
+ /**
+ * 扣费类型
+ * 0:手动
+ * 1:菜单
+ * 2:定值
+ * 3:取餐
+ * 4:计时
+ */
+ private Integer PayType;
+
+ /**
+ * 消费金额(Mode为3时,此字段传入值可能为0)
+ */
+ private String Amount;
+
+ /**
+ * 扫描到的二维码字符串(最大长度为200)
+ */
+ private String QR;
+
+ private List