diff --git a/db/20260805_add_third_aggregate_pay.sql b/db/20260805_add_third_aggregate_pay.sql
deleted file mode 100644
index 734e27f8..00000000
--- a/db/20260805_add_third_aggregate_pay.sql
+++ /dev/null
@@ -1,37 +0,0 @@
--- 支付宝与微信聚合订单码支付。
--- 聚合会话与实际第三方支付订单分表保存,避免改变已被多个调用方使用的 third_pay_order 契约。
--- 微信统一使用服务商平台公众号配置和各商户既有 sub_mchid,不新增逐商户公众号配置表。
-
-CREATE TABLE `third_aggregate_pay_session`
-(
- `id` bigint NOT NULL COMMENT '聚合会话主键',
- `aggregate_no` varchar(64) NOT NULL COMMENT '聚合支付会话唯一编号',
- `access_token_hash` char(64) NOT NULL COMMENT '收银URL随机访问令牌SHA-256摘要',
- `access_token_ciphertext` varchar(512) NOT NULL COMMENT '收银URL随机访问令牌Jasypt密文,仅用于幂等复用',
- `biz_type` varchar(32) NOT NULL COMMENT '支付业务类型编码,首版为YXFJ_PAY对应编码',
- `biz_id` varchar(128) NOT NULL COMMENT '调用方业务订单唯一标识',
- `user_id` bigint NOT NULL COMMENT '付款用户ID,匿名消费机订单为-1',
- `mch_id` bigint NOT NULL COMMENT '收款商户ID',
- `price` int NOT NULL COMMENT '应付金额,单位为人民币分',
- `status` varchar(32) NOT NULL COMMENT '聚合会话状态编码',
- `selected_platform` varchar(32) DEFAULT NULL COMMENT '首个有效扫码选定的支付平台编码',
- `pay_no` varchar(128) DEFAULT NULL COMMENT '绑定的third_pay_order.pay_no',
- `payer_identity_hash` char(64) DEFAULT NULL COMMENT '支付宝用户ID或微信服务商公众号sp_openid的SHA-256摘要',
- `expire_time` datetime NOT NULL COMMENT '聚合二维码与第三方子订单服务端失效时间',
- `resolved_time` datetime 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 '修改时间',
- `del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除标记,0有效,1删除',
- `tenant_id` bigint NOT NULL COMMENT '租户ID',
- PRIMARY KEY (`id`),
- UNIQUE KEY `uk_third_aggregate_pay_no` (`tenant_id`, `aggregate_no`),
- UNIQUE KEY `uk_third_aggregate_access_token` (`tenant_id`, `access_token_hash`),
- UNIQUE KEY `uk_third_aggregate_child_pay_no` (`tenant_id`, `pay_no`),
- KEY `idx_third_aggregate_biz` (`tenant_id`, `biz_type`, `biz_id`, `create_time`),
- KEY `idx_third_aggregate_job` (`status`, `expire_time`)
-) ENGINE = InnoDB
- DEFAULT CHARSET = utf8mb4
- COLLATE = utf8mb4_general_ci
- COMMENT = '支付宝与微信聚合订单码支付会话';
diff --git a/db/20260824_add_third_payment_task.sql b/db/20260824_add_third_payment_task.sql
deleted file mode 100644
index 31468234..00000000
--- a/db/20260824_add_third_payment_task.sql
+++ /dev/null
@@ -1,153 +0,0 @@
--- thirdparty 支付任务事实表与执行尝试表。
--- Quartz 是唯一周期触发入口;任务表只保存可恢复事实、租约和幂等状态。
-CREATE TABLE IF NOT EXISTS `third_payment_task`
-(
- `id` bigint NOT NULL COMMENT '任务主键',
- `tenant_id` bigint NOT NULL COMMENT '租户ID',
- `task_type` varchar(48) NOT NULL COMMENT '任务类型编码',
- `task_key` varchar(192) NOT NULL COMMENT '租户内稳定幂等任务键',
- `pay_order_id` bigint DEFAULT NULL COMMENT 'third_pay_order主键',
- `fz_order_id` bigint DEFAULT NULL COMMENT 'third_fz_order主键',
- `aggregate_session_id` bigint DEFAULT NULL COMMENT 'third_aggregate_pay_session主键',
- `pay_no` varchar(128) DEFAULT NULL COMMENT '平台支付单号',
- `refund_no` varchar(128) DEFAULT NULL COMMENT '平台退款单号',
- `fz_no` varchar(128) DEFAULT NULL COMMENT '平台分账单号',
- `aggregate_no` varchar(128) DEFAULT NULL COMMENT '聚合会话单号',
- `status` varchar(16) NOT NULL COMMENT '任务状态:READY/PROCESSING/RETRY/SUCCESS/FAILED/ALERT/CANCELLED',
- `next_run_time` datetime NOT NULL COMMENT '下一次可执行时间',
- `priority` int NOT NULL DEFAULT 0 COMMENT '任务优先级,数值越大越优先',
- `lease_owner` varchar(128) DEFAULT NULL COMMENT 'Worker实例标识',
- `lease_token` varchar(64) DEFAULT NULL COMMENT '本次执行租约令牌',
- `lease_expire_time` datetime DEFAULT NULL COMMENT '租约失效时间',
- `attempt_count` int NOT NULL DEFAULT 0 COMMENT '累计执行次数',
- `last_external_status` varchar(64) DEFAULT NULL COMMENT '最近一次渠道状态',
- `last_error_code` varchar(128) DEFAULT NULL COMMENT '最近一次错误码',
- `last_error_message` varchar(1024) DEFAULT NULL COMMENT '最近一次错误摘要',
- `finished_time` datetime 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 '更新时间',
- `del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除标记',
- PRIMARY KEY (`id`),
- UNIQUE KEY `uk_third_payment_task_tenant_key` (`tenant_id`, `task_key`),
- KEY `idx_third_payment_task_due` (`task_type`, `status`, `next_run_time`, `priority`, `id`),
- KEY `idx_third_payment_task_lease` (`task_type`, `status`, `lease_expire_time`),
- KEY `idx_third_payment_task_pay_no` (`tenant_id`, `pay_no`),
- KEY `idx_third_payment_task_fz_no` (`tenant_id`, `fz_no`)
-) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '第三方支付任务事实';
-
-CREATE TABLE IF NOT EXISTS `third_payment_task_attempt`
-(
- `id` bigint NOT NULL COMMENT '尝试记录主键',
- `task_id` bigint NOT NULL COMMENT '任务主键',
- `tenant_id` bigint NOT NULL COMMENT '租户ID',
- `attempt_no` int NOT NULL COMMENT '任务内尝试序号',
- `trigger_source` varchar(32) NOT NULL COMMENT '触发来源:QUARTZ/CALLBACK/SYNC/MANUAL',
- `worker_id` varchar(128) DEFAULT NULL COMMENT 'Worker实例标识',
- `lease_token` varchar(64) DEFAULT NULL COMMENT '执行租约令牌',
- `request_snapshot` text DEFAULT NULL COMMENT '脱敏请求快照',
- `response_snapshot` text DEFAULT NULL COMMENT '脱敏响应快照',
- `external_status` varchar(64) DEFAULT NULL COMMENT '渠道状态',
- `error_code` varchar(128) DEFAULT NULL COMMENT '错误码',
- `error_message` varchar(1024) DEFAULT NULL COMMENT '错误摘要',
- `started_time` datetime NOT NULL COMMENT '开始时间',
- `finished_time` datetime DEFAULT NULL COMMENT '结束时间',
- PRIMARY KEY (`id`),
- KEY `idx_third_payment_attempt_task` (`task_id`, `attempt_no`),
- KEY `idx_third_payment_attempt_tenant_time` (`tenant_id`, `started_time`)
-) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '第三方支付任务执行尝试';
-
--- Quartz 会按业务事实持续物化任务。百万级订单量下,如果业务表只有主键,每个固定触发器都会退化为全表扫描。
--- 以下索引只覆盖实际扫描谓词和租户内业务号关联;使用 information_schema 判断保证迁移可以重复执行。
-SET @index_ddl = IF(
- EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE()
- AND table_name = 'third_pay_order' AND index_name = 'idx_third_pay_order_task_scan'),
- 'SELECT ''idx_third_pay_order_task_scan already exists'' AS message',
- 'ALTER TABLE `third_pay_order` ADD INDEX `idx_third_pay_order_task_scan` (`status`, `del_flag`, `pay_platform_type`, `update_time`, `id`)'
-);
-PREPARE index_stmt FROM @index_ddl;
-EXECUTE index_stmt;
-DEALLOCATE PREPARE index_stmt;
-
-SET @index_ddl = IF(
- EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE()
- AND table_name = 'third_pay_order' AND index_name = 'idx_third_pay_order_fz_scan'),
- 'SELECT ''idx_third_pay_order_fz_scan already exists'' AS message',
- 'ALTER TABLE `third_pay_order` ADD INDEX `idx_third_pay_order_fz_scan` (`status`, `enable_fz`, `del_flag`, `pay_time`, `id`)'
-);
-PREPARE index_stmt FROM @index_ddl;
-EXECUTE index_stmt;
-DEALLOCATE PREPARE index_stmt;
-
-SET @index_ddl = IF(
- EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE()
- AND table_name = 'third_pay_order' AND index_name = 'idx_third_pay_order_callback_scan'),
- 'SELECT ''idx_third_pay_order_callback_scan already exists'' AS message',
- 'ALTER TABLE `third_pay_order` ADD INDEX `idx_third_pay_order_callback_scan` (`status`, `is_biz_callback`, `del_flag`, `update_time`, `id`)'
-);
-PREPARE index_stmt FROM @index_ddl;
-EXECUTE index_stmt;
-DEALLOCATE PREPARE index_stmt;
-
-SET @index_ddl = IF(
- EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE()
- AND table_name = 'third_pay_order' AND index_name = 'idx_third_pay_order_close_scan'),
- 'SELECT ''idx_third_pay_order_close_scan already exists'' AS message',
- 'ALTER TABLE `third_pay_order` ADD INDEX `idx_third_pay_order_close_scan` (`status`, `biz_type`, `del_flag`, `create_time`, `id`)'
-);
-PREPARE index_stmt FROM @index_ddl;
-EXECUTE index_stmt;
-DEALLOCATE PREPARE index_stmt;
-
-SET @index_ddl = IF(
- EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE()
- AND table_name = 'third_pay_order' AND index_name = 'idx_third_pay_order_tenant_pay_no'),
- 'SELECT ''idx_third_pay_order_tenant_pay_no already exists'' AS message',
- 'ALTER TABLE `third_pay_order` ADD INDEX `idx_third_pay_order_tenant_pay_no` (`tenant_id`, `pay_no`, `del_flag`)'
-);
-PREPARE index_stmt FROM @index_ddl;
-EXECUTE index_stmt;
-DEALLOCATE PREPARE index_stmt;
-
-SET @index_ddl = IF(
- EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE()
- AND table_name = 'third_fz_order' AND index_name = 'idx_third_fz_order_task_scan'),
- 'SELECT ''idx_third_fz_order_task_scan already exists'' AS message',
- 'ALTER TABLE `third_fz_order` ADD INDEX `idx_third_fz_order_task_scan` (`status`, `del_flag`, `update_time`, `id`)'
-);
-PREPARE index_stmt FROM @index_ddl;
-EXECUTE index_stmt;
-DEALLOCATE PREPARE index_stmt;
-
-SET @index_ddl = IF(
- EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE()
- AND table_name = 'third_fz_order' AND index_name = 'idx_third_fz_order_tenant_pay_no'),
- 'SELECT ''idx_third_fz_order_tenant_pay_no already exists'' AS message',
- 'ALTER TABLE `third_fz_order` ADD INDEX `idx_third_fz_order_tenant_pay_no` (`tenant_id`, `pay_no`, `del_flag`)'
-);
-PREPARE index_stmt FROM @index_ddl;
-EXECUTE index_stmt;
-DEALLOCATE PREPARE index_stmt;
-
-SET @index_ddl = IF(
- EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE()
- AND table_name = 'third_aggregate_pay_session' AND index_name = 'idx_third_aggregate_task_scan'),
- 'SELECT ''idx_third_aggregate_task_scan already exists'' AS message',
- 'ALTER TABLE `third_aggregate_pay_session` ADD INDEX `idx_third_aggregate_task_scan` (`status`, `del_flag`, `update_time`, `id`)'
-);
-PREPARE index_stmt FROM @index_ddl;
-EXECUTE index_stmt;
-DEALLOCATE PREPARE index_stmt;
-
-SET @index_ddl = IF(
- EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE()
- AND table_name = 'third_aggregate_pay_session' AND index_name = 'idx_third_aggregate_close_scan'),
- 'SELECT ''idx_third_aggregate_close_scan already exists'' AS message',
- 'ALTER TABLE `third_aggregate_pay_session` ADD INDEX `idx_third_aggregate_close_scan` (`status`, `del_flag`, `create_time`, `id`)'
-);
-PREPARE index_stmt FROM @index_ddl;
-EXECUTE index_stmt;
-DEALLOCATE PREPARE index_stmt;
-
--- 生产执行建议:低峰期使用在线 DDL 能力建索引,完成后 ANALYZE TABLE 并用真实状态分布执行 EXPLAIN ANALYZE。
diff --git a/db/20260825_add_third_mch_fz_relation.sql b/db/20260825_add_third_mch_fz_relation.sql
deleted file mode 100644
index 25115867..00000000
--- a/db/20260825_add_third_mch_fz_relation.sql
+++ /dev/null
@@ -1,79 +0,0 @@
-USE `pjcloudx_thirdparty`;
-
--- 商户默认平台分佣关系。微信关系由 sub_mchid 区分,支付宝关系由调用应用、授权商户和接收账号共同确定。
--- 表中只保存渠道标识和脱敏调用快照,不保存私钥、公钥或完整 app_auth_token。
-CREATE TABLE IF NOT EXISTS `third_mch_fz_relation`
-(
- `id` bigint NOT NULL COMMENT '关系主键,应用侧雪花算法生成',
- `tenant_id` bigint NOT NULL COMMENT '租户ID,与 mch_manage 所属租户一致',
- `mch_id` bigint NOT NULL COMMENT 'mch_manage 商户主键',
- `pay_setting_id` bigint NOT NULL COMMENT 'mch_pay_setting 支付配置主键',
- `platform_type` varchar(32) NOT NULL COMMENT '支付平台类型:10微信、20支付宝',
- `relation_scene` varchar(64) NOT NULL COMMENT '本地关系场景,默认平台分佣为 DEFAULT_PLATFORM_COMMISSION',
- `channel_app_id` varchar(64) NOT NULL COMMENT '微信服务商AppID或支付宝平台AppID快照',
- `channel_sub_mch_id` varchar(64) NOT NULL COMMENT '微信子商户号或支付宝直付通smid/ip_role_id',
- `account_key` varchar(64) DEFAULT NULL COMMENT '支付宝命名平台账号键;默认账号及微信为空',
- `receiver_type` varchar(32) NOT NULL COMMENT '渠道接收方类型,如MERCHANT_ID、userId、loginName',
- `receiver_account` varchar(128) NOT NULL COMMENT '平台分佣接收账号;管理端展示时必须脱敏',
- `receiver_name` varchar(128) DEFAULT NULL COMMENT '平台分佣接收方名称快照',
- `channel_relation_type` varchar(64) DEFAULT NULL COMMENT '渠道关系类型;微信默认SERVICE_PROVIDER,支付宝为空',
- `out_request_no` varchar(64) NOT NULL COMMENT '支付宝绑定稳定幂等请求号;微信作为本地稳定审计号',
- `status` varchar(32) NOT NULL COMMENT 'PENDING_BIND、BINDING、BOUND、BIND_FAILED或SUPERSEDED',
- `is_default` char(1) NOT NULL DEFAULT '1' COMMENT '是否为当前支付配置的默认平台分佣关系:0否、1是',
- `bind_req` text DEFAULT NULL COMMENT '脱敏后的关系绑定请求快照',
- `bind_res` text DEFAULT NULL COMMENT '渠道关系绑定或查询响应快照',
- `bind_error` varchar(1000) DEFAULT NULL COMMENT '最近一次绑定错误摘要,不含凭据',
- `last_external_status` varchar(128) DEFAULT NULL COMMENT '最近一次渠道结果码或外部状态',
- `last_query_time` datetime DEFAULT NULL COMMENT '最近一次关系绑定或查询时间',
- `bound_time` datetime DEFAULT NULL COMMENT '渠道明确确认关系已绑定的时间',
- `create_by` varchar(32) DEFAULT NULL COMMENT '创建人',
- `create_time` datetime DEFAULT NULL COMMENT '创建时间',
- `update_by` varchar(32) DEFAULT NULL COMMENT '更新人',
- `update_time` datetime DEFAULT NULL COMMENT '更新时间',
- `del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除标记:0正常、1删除',
- PRIMARY KEY (`id`),
- UNIQUE KEY `uk_third_mch_fz_relation_identity`
- (`tenant_id`, `pay_setting_id`, `platform_type`, `channel_app_id`, `channel_sub_mch_id`,
- `receiver_type`, `receiver_account`, `relation_scene`),
- KEY `idx_third_mch_fz_relation_default`
- (`tenant_id`, `mch_id`, `platform_type`, `is_default`, `status`, `del_flag`),
- KEY `idx_third_mch_fz_relation_task`
- (`status`, `is_default`, `update_time`, `id`)
-) ENGINE = InnoDB
- DEFAULT CHARSET = utf8mb4
- COLLATE = utf8mb4_general_ci
- COMMENT ='商户第三方支付分账关系';
-
--- 支付任务通过结构化主键定位关系事实,禁止从 task_key 字符串反解析业务主键。
-SET @relation_task_column_exists = (
- SELECT COUNT(1)
- FROM information_schema.COLUMNS
- WHERE TABLE_SCHEMA = DATABASE()
- AND TABLE_NAME = 'third_payment_task'
- AND COLUMN_NAME = 'fz_relation_id'
-);
-SET @relation_task_column_sql = IF(
- @relation_task_column_exists = 0,
- 'ALTER TABLE `third_payment_task` ADD COLUMN `fz_relation_id` bigint DEFAULT NULL COMMENT ''商户分账关系主键'' AFTER `fz_order_id`, ADD INDEX `idx_third_payment_task_relation` (`tenant_id`, `fz_relation_id`)',
- 'SELECT 1'
-);
-PREPARE relation_task_column_stmt FROM @relation_task_column_sql;
-EXECUTE relation_task_column_stmt;
-DEALLOCATE PREPARE relation_task_column_stmt;
-
--- 支付订单固定引用创建时使用的已绑定关系,后续分账不跟随平台接收账号配置漂移。
-SET @pay_relation_column_exists = (
- SELECT COUNT(1)
- FROM information_schema.COLUMNS
- WHERE TABLE_SCHEMA = DATABASE()
- AND TABLE_NAME = 'third_pay_order'
- AND COLUMN_NAME = 'fz_relation_id'
-);
-SET @pay_relation_column_sql = IF(
- @pay_relation_column_exists = 0,
- 'ALTER TABLE `third_pay_order` ADD COLUMN `fz_relation_id` bigint DEFAULT NULL COMMENT ''支付创建时绑定的平台分佣关系主键'' AFTER `mch_pay_setting_json`, ADD INDEX `idx_third_pay_order_fz_relation` (`tenant_id`, `fz_relation_id`)',
- 'SELECT 1'
-);
-PREPARE pay_relation_column_stmt FROM @pay_relation_column_sql;
-EXECUTE pay_relation_column_stmt;
-DEALLOCATE PREPARE pay_relation_column_stmt;
diff --git a/db/20260825_third_payment_schema.sql b/db/20260825_third_payment_schema.sql
new file mode 100644
index 00000000..9841ed27
--- /dev/null
+++ b/db/20260825_third_payment_schema.sql
@@ -0,0 +1,362 @@
+-- thirdparty 支付、聚合支付、任务补偿和商户分账关系统一迁移脚本。
+-- 线上只执行本文件;脚本可重复执行,兼容全新数据库和已经执行部分旧迁移的数据库。
+-- 本脚本不删除支付任务、订单、分账关系或历史配置数据;仅在迁移核对通过后清理旧配置字段。
+
+USE `pjcloudx_thirdparty`;
+
+-- 一、聚合订单码支付会话
+CREATE TABLE IF NOT EXISTS `third_aggregate_pay_session`
+(
+ `id` bigint NOT NULL COMMENT '聚合会话主键',
+ `aggregate_no` varchar(64) NOT NULL COMMENT '聚合支付会话唯一编号',
+ `access_token_hash` char(64) NOT NULL COMMENT '收银URL随机访问令牌SHA-256摘要',
+ `access_token_ciphertext` varchar(512) NOT NULL COMMENT '收银URL随机访问令牌Jasypt密文,仅用于幂等复用',
+ `biz_type` varchar(32) NOT NULL COMMENT '支付业务类型编码,首版为YXFJ_PAY对应编码',
+ `biz_id` varchar(128) NOT NULL COMMENT '调用方业务订单唯一标识',
+ `user_id` bigint NOT NULL COMMENT '付款用户ID,匿名消费机订单为-1',
+ `mch_id` bigint NOT NULL COMMENT '收款商户ID',
+ `price` int NOT NULL COMMENT '应付金额,单位为人民币分',
+ `status` varchar(32) NOT NULL COMMENT '聚合会话状态编码',
+ `selected_platform` varchar(32) DEFAULT NULL COMMENT '首个有效扫码选定的支付平台编码',
+ `pay_no` varchar(128) DEFAULT NULL COMMENT '绑定的third_pay_order.pay_no',
+ `payer_identity_hash` char(64) DEFAULT NULL COMMENT '支付宝用户ID或微信服务商公众号sp_openid的SHA-256摘要',
+ `expire_time` datetime NOT NULL COMMENT '聚合二维码与第三方子订单服务端失效时间',
+ `resolved_time` datetime 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 '修改时间',
+ `del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除标记,0有效,1删除',
+ `tenant_id` bigint NOT NULL COMMENT '租户ID',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_third_aggregate_pay_no` (`tenant_id`, `aggregate_no`),
+ UNIQUE KEY `uk_third_aggregate_access_token` (`tenant_id`, `access_token_hash`),
+ UNIQUE KEY `uk_third_aggregate_child_pay_no` (`tenant_id`, `pay_no`),
+ KEY `idx_third_aggregate_biz` (`tenant_id`, `biz_type`, `biz_id`, `create_time`),
+ KEY `idx_third_aggregate_job` (`status`, `expire_time`)
+) ENGINE = InnoDB
+ DEFAULT CHARSET = utf8mb4
+ COLLATE = utf8mb4_general_ci
+ COMMENT = '支付宝与微信聚合订单码支付会话';
+
+-- 二、Quartz 触发的可恢复支付任务和执行尝试
+CREATE TABLE IF NOT EXISTS `third_payment_task`
+(
+ `id` bigint NOT NULL COMMENT '任务主键',
+ `tenant_id` bigint NOT NULL COMMENT '租户ID',
+ `task_type` varchar(48) NOT NULL COMMENT '任务类型编码',
+ `task_key` varchar(192) NOT NULL COMMENT '租户内稳定幂等任务键',
+ `pay_order_id` bigint DEFAULT NULL COMMENT 'third_pay_order主键',
+ `fz_order_id` bigint DEFAULT NULL COMMENT 'third_fz_order主键',
+ `fz_relation_id` bigint DEFAULT NULL COMMENT 'third_mch_fz_relation主键',
+ `aggregate_session_id` bigint DEFAULT NULL COMMENT 'third_aggregate_pay_session主键',
+ `pay_no` varchar(128) DEFAULT NULL COMMENT '平台支付单号',
+ `refund_no` varchar(128) DEFAULT NULL COMMENT '平台退款单号',
+ `fz_no` varchar(128) DEFAULT NULL COMMENT '平台分账单号',
+ `aggregate_no` varchar(128) DEFAULT NULL COMMENT '聚合会话单号',
+ `status` varchar(16) NOT NULL COMMENT '任务状态:READY/PROCESSING/RETRY/SUCCESS/FAILED/ALERT/CANCELLED',
+ `next_run_time` datetime NOT NULL COMMENT '下一次可执行时间',
+ `priority` int NOT NULL DEFAULT 0 COMMENT '任务优先级,数值越大越优先',
+ `lease_owner` varchar(128) DEFAULT NULL COMMENT 'Worker实例标识',
+ `lease_token` varchar(64) DEFAULT NULL COMMENT '本次执行租约令牌',
+ `lease_expire_time` datetime DEFAULT NULL COMMENT '租约失效时间',
+ `attempt_count` int NOT NULL DEFAULT 0 COMMENT '累计执行次数',
+ `last_external_status` varchar(64) DEFAULT NULL COMMENT '最近一次渠道状态',
+ `last_error_code` varchar(128) DEFAULT NULL COMMENT '最近一次错误码',
+ `last_error_message` varchar(1024) DEFAULT NULL COMMENT '最近一次错误摘要',
+ `finished_time` datetime 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 '更新时间',
+ `del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除标记',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_third_payment_task_tenant_key` (`tenant_id`, `task_key`),
+ KEY `idx_third_payment_task_due` (`task_type`, `status`, `next_run_time`, `priority`, `id`),
+ KEY `idx_third_payment_task_lease` (`task_type`, `status`, `lease_expire_time`),
+ KEY `idx_third_payment_task_pay_no` (`tenant_id`, `pay_no`),
+ KEY `idx_third_payment_task_fz_no` (`tenant_id`, `fz_no`),
+ KEY `idx_third_payment_task_relation` (`tenant_id`, `fz_relation_id`)
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '第三方支付任务事实';
+
+CREATE TABLE IF NOT EXISTS `third_payment_task_attempt`
+(
+ `id` bigint NOT NULL COMMENT '尝试记录主键',
+ `task_id` bigint NOT NULL COMMENT '任务主键',
+ `tenant_id` bigint NOT NULL COMMENT '租户ID',
+ `attempt_no` int NOT NULL COMMENT '任务内尝试序号',
+ `trigger_source` varchar(32) NOT NULL COMMENT '触发来源:QUARTZ/CALLBACK/SYNC/MANUAL',
+ `worker_id` varchar(128) DEFAULT NULL COMMENT 'Worker实例标识',
+ `lease_token` varchar(64) DEFAULT NULL COMMENT '执行租约令牌',
+ `request_snapshot` text DEFAULT NULL COMMENT '脱敏请求快照',
+ `response_snapshot` text DEFAULT NULL COMMENT '脱敏响应快照',
+ `external_status` varchar(64) DEFAULT NULL COMMENT '渠道状态',
+ `error_code` varchar(128) DEFAULT NULL COMMENT '错误码',
+ `error_message` varchar(1024) DEFAULT NULL COMMENT '错误摘要',
+ `started_time` datetime NOT NULL COMMENT '开始时间',
+ `finished_time` datetime DEFAULT NULL COMMENT '结束时间',
+ PRIMARY KEY (`id`),
+ KEY `idx_third_payment_attempt_task` (`task_id`, `attempt_no`),
+ KEY `idx_third_payment_attempt_tenant_time` (`tenant_id`, `started_time`)
+) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '第三方支付任务执行尝试';
+
+-- 三、商户支付分账关系(微信、支付宝共用)
+CREATE TABLE IF NOT EXISTS `third_mch_fz_relation`
+(
+ `id` bigint NOT NULL COMMENT '关系主键,应用侧雪花算法生成',
+ `tenant_id` bigint NOT NULL COMMENT '租户ID,与mch_manage所属租户一致',
+ `mch_id` bigint NOT NULL COMMENT 'mch_manage商户主键',
+ `pay_setting_id` bigint NOT NULL COMMENT 'mch_pay_setting支付配置主键',
+ `platform_type` varchar(32) NOT NULL COMMENT '支付平台类型:10微信、20支付宝',
+ `relation_scene` varchar(64) NOT NULL COMMENT '本地关系场景,默认平台分佣为DEFAULT_PLATFORM_COMMISSION',
+ `channel_app_id` varchar(64) NOT NULL COMMENT '微信服务商AppID或支付宝平台AppID快照',
+ `channel_sub_mch_id` varchar(64) NOT NULL COMMENT '微信子商户号或历史渠道子商户标识快照',
+ `account_key` varchar(64) DEFAULT NULL COMMENT '支付宝命名平台账号键;默认账号及微信为空',
+ `app_auth_token` varchar(512) DEFAULT NULL COMMENT '支付宝第三方代调用授权令牌;仅服务端渠道调用使用',
+ `receiver_type` varchar(32) NOT NULL COMMENT '渠道接收方类型,如MERCHANT_ID、userId、loginName',
+ `receiver_account` varchar(128) NOT NULL COMMENT '平台分佣接收账号;管理端展示时必须脱敏',
+ `receiver_name` varchar(128) DEFAULT NULL COMMENT '平台分佣接收方名称快照',
+ `fz_ratio` decimal(6,3) NOT NULL DEFAULT '0.000' COMMENT '平台抽佣比例,单位百分比,范围0到100,精确到0.001%',
+ `channel_relation_type` varchar(64) DEFAULT NULL COMMENT '渠道关系类型;微信默认SERVICE_PROVIDER,支付宝允许为空',
+ `out_request_no` varchar(64) NOT NULL COMMENT '支付宝绑定稳定幂等请求号;微信作为本地稳定审计号',
+ `status` varchar(32) NOT NULL COMMENT 'PENDING_BIND、BINDING、BOUND、BIND_FAILED或SUPERSEDED',
+ `is_default` char(1) NOT NULL DEFAULT '1' COMMENT '是否为当前支付配置默认关系:0否、1是',
+ `bind_req` text DEFAULT NULL COMMENT '脱敏后的关系绑定请求快照',
+ `bind_res` text DEFAULT NULL COMMENT '渠道关系绑定或查询响应快照',
+ `bind_error` varchar(1000) DEFAULT NULL COMMENT '最近一次绑定错误摘要,不含凭据',
+ `last_external_status` varchar(128) DEFAULT NULL COMMENT '最近一次渠道结果码或外部状态',
+ `last_query_time` datetime DEFAULT NULL COMMENT '最近一次关系绑定或查询时间',
+ `bound_time` datetime DEFAULT NULL COMMENT '渠道明确确认关系已绑定的时间',
+ `create_by` varchar(32) DEFAULT NULL COMMENT '创建人',
+ `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+ `update_by` varchar(32) DEFAULT NULL COMMENT '更新人',
+ `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+ `del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '逻辑删除标记:0正常、1删除',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_third_mch_fz_relation_identity`
+ (`tenant_id`, `pay_setting_id`, `platform_type`, `channel_app_id`, `channel_sub_mch_id`,
+ `receiver_type`, `receiver_account`, `relation_scene`),
+ KEY `idx_third_mch_fz_relation_default`
+ (`tenant_id`, `mch_id`, `platform_type`, `is_default`, `status`, `del_flag`),
+ KEY `idx_third_mch_fz_relation_task`
+ (`status`, `is_default`, `update_time`, `id`)
+) ENGINE = InnoDB
+ DEFAULT CHARSET = utf8mb4
+ COLLATE = utf8mb4_general_ci
+ COMMENT = '商户第三方支付分账关系';
+
+-- 四、旧环境补齐关系引用字段。每个字段独立判断,避免一个缺表/缺列阻断后续迁移。
+SET @task_relation_column_exists = (
+ SELECT COUNT(1) FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_payment_task' AND COLUMN_NAME = 'fz_relation_id'
+);
+SET @task_relation_column_sql = IF(
+ @task_relation_column_exists = 0,
+ 'ALTER TABLE `third_payment_task` ADD COLUMN `fz_relation_id` bigint DEFAULT NULL COMMENT ''third_mch_fz_relation主键''',
+ 'SELECT 1'
+);
+PREPARE task_relation_column_stmt FROM @task_relation_column_sql;
+EXECUTE task_relation_column_stmt;
+DEALLOCATE PREPARE task_relation_column_stmt;
+
+SET @pay_relation_column_exists = (
+ SELECT COUNT(1) FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_pay_order' AND COLUMN_NAME = 'fz_relation_id'
+);
+SET @pay_order_table_exists = (
+ SELECT COUNT(1) FROM information_schema.TABLES
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_pay_order'
+);
+SET @pay_relation_column_sql = IF(
+ @pay_order_table_exists = 1 AND @pay_relation_column_exists = 0,
+ 'ALTER TABLE `third_pay_order` ADD COLUMN `fz_relation_id` bigint DEFAULT NULL COMMENT ''支付创建时绑定的平台分佣关系主键''',
+ 'SELECT 1'
+);
+PREPARE pay_relation_column_stmt FROM @pay_relation_column_sql;
+EXECUTE pay_relation_column_stmt;
+DEALLOCATE PREPARE pay_relation_column_stmt;
+
+SET @relation_auth_column_exists = (
+ SELECT COUNT(1) FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_mch_fz_relation' AND COLUMN_NAME = 'app_auth_token'
+);
+SET @relation_auth_column_sql = IF(
+ @relation_auth_column_exists = 0,
+ 'ALTER TABLE `third_mch_fz_relation` ADD COLUMN `app_auth_token` varchar(512) DEFAULT NULL COMMENT ''支付宝第三方代调用授权令牌;仅服务端渠道调用使用''',
+ 'SELECT 1'
+);
+PREPARE relation_auth_column_stmt FROM @relation_auth_column_sql;
+EXECUTE relation_auth_column_stmt;
+DEALLOCATE PREPARE relation_auth_column_stmt;
+
+SET @relation_ratio_column_exists = (
+ SELECT COUNT(1) FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_mch_fz_relation' AND COLUMN_NAME = 'fz_ratio'
+);
+SET @relation_ratio_column_sql = IF(
+ @relation_ratio_column_exists = 0,
+ 'ALTER TABLE `third_mch_fz_relation` ADD COLUMN `fz_ratio` decimal(6,3) NOT NULL DEFAULT ''0.000'' COMMENT ''平台抽佣比例,单位百分比,范围0到100,精确到0.001%''',
+ 'ALTER TABLE `third_mch_fz_relation` MODIFY COLUMN `fz_ratio` decimal(6,3) NOT NULL DEFAULT ''0.000'' COMMENT ''平台抽佣比例,单位百分比,范围0到100,精确到0.001%'''
+);
+PREPARE relation_ratio_column_stmt FROM @relation_ratio_column_sql;
+EXECUTE relation_ratio_column_stmt;
+DEALLOCATE PREPARE relation_ratio_column_stmt;
+
+-- 五、补齐任务和业务扫描索引。索引已存在时不重复创建。
+SET @index_ddl = IF(EXISTS (SELECT 1 FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_payment_task' AND INDEX_NAME = 'idx_third_payment_task_relation'), 'SELECT 1', 'ALTER TABLE `third_payment_task` ADD INDEX `idx_third_payment_task_relation` (`tenant_id`, `fz_relation_id`)');
+PREPARE index_stmt FROM @index_ddl; EXECUTE index_stmt; DEALLOCATE PREPARE index_stmt;
+
+SET @pay_task_scan_ready = (SELECT COUNT(DISTINCT COLUMN_NAME) = 5 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_pay_order' AND COLUMN_NAME IN ('status', 'del_flag', 'pay_platform_type', 'update_time', 'id'));
+SET @pay_fz_scan_ready = (SELECT COUNT(DISTINCT COLUMN_NAME) = 5 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_pay_order' AND COLUMN_NAME IN ('status', 'enable_fz', 'del_flag', 'pay_time', 'id'));
+SET @pay_callback_scan_ready = (SELECT COUNT(DISTINCT COLUMN_NAME) = 5 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_pay_order' AND COLUMN_NAME IN ('status', 'is_biz_callback', 'del_flag', 'update_time', 'id'));
+SET @pay_close_scan_ready = (SELECT COUNT(DISTINCT COLUMN_NAME) = 5 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_pay_order' AND COLUMN_NAME IN ('status', 'biz_type', 'del_flag', 'create_time', 'id'));
+SET @pay_tenant_no_ready = (SELECT COUNT(DISTINCT COLUMN_NAME) = 3 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_pay_order' AND COLUMN_NAME IN ('tenant_id', 'pay_no', 'del_flag'));
+SET @pay_relation_ready = (SELECT COUNT(DISTINCT COLUMN_NAME) = 2 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_pay_order' AND COLUMN_NAME IN ('tenant_id', 'fz_relation_id'));
+SET @fz_task_scan_ready = (SELECT COUNT(DISTINCT COLUMN_NAME) = 4 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_fz_order' AND COLUMN_NAME IN ('status', 'del_flag', 'update_time', 'id'));
+SET @fz_tenant_no_ready = (SELECT COUNT(DISTINCT COLUMN_NAME) = 3 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_fz_order' AND COLUMN_NAME IN ('tenant_id', 'pay_no', 'del_flag'));
+SET @aggregate_task_scan_ready = (SELECT COUNT(DISTINCT COLUMN_NAME) = 4 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_aggregate_pay_session' AND COLUMN_NAME IN ('status', 'del_flag', 'update_time', 'id'));
+SET @aggregate_close_scan_ready = (SELECT COUNT(DISTINCT COLUMN_NAME) = 4 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_aggregate_pay_session' AND COLUMN_NAME IN ('status', 'del_flag', 'create_time', 'id'));
+
+SET @index_ddl = IF(@pay_task_scan_ready = 1 AND NOT EXISTS (SELECT 1 FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_pay_order' AND INDEX_NAME = 'idx_third_pay_order_task_scan'), 'ALTER TABLE `third_pay_order` ADD INDEX `idx_third_pay_order_task_scan` (`status`, `del_flag`, `pay_platform_type`, `update_time`, `id`)', 'SELECT 1');
+PREPARE index_stmt FROM @index_ddl; EXECUTE index_stmt; DEALLOCATE PREPARE index_stmt;
+SET @index_ddl = IF(@pay_fz_scan_ready = 1 AND NOT EXISTS (SELECT 1 FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_pay_order' AND INDEX_NAME = 'idx_third_pay_order_fz_scan'), 'ALTER TABLE `third_pay_order` ADD INDEX `idx_third_pay_order_fz_scan` (`status`, `enable_fz`, `del_flag`, `pay_time`, `id`)', 'SELECT 1');
+PREPARE index_stmt FROM @index_ddl; EXECUTE index_stmt; DEALLOCATE PREPARE index_stmt;
+SET @index_ddl = IF(@pay_callback_scan_ready = 1 AND NOT EXISTS (SELECT 1 FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_pay_order' AND INDEX_NAME = 'idx_third_pay_order_callback_scan'), 'ALTER TABLE `third_pay_order` ADD INDEX `idx_third_pay_order_callback_scan` (`status`, `is_biz_callback`, `del_flag`, `update_time`, `id`)', 'SELECT 1');
+PREPARE index_stmt FROM @index_ddl; EXECUTE index_stmt; DEALLOCATE PREPARE index_stmt;
+SET @index_ddl = IF(@pay_close_scan_ready = 1 AND NOT EXISTS (SELECT 1 FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_pay_order' AND INDEX_NAME = 'idx_third_pay_order_close_scan'), 'ALTER TABLE `third_pay_order` ADD INDEX `idx_third_pay_order_close_scan` (`status`, `biz_type`, `del_flag`, `create_time`, `id`)', 'SELECT 1');
+PREPARE index_stmt FROM @index_ddl; EXECUTE index_stmt; DEALLOCATE PREPARE index_stmt;
+SET @index_ddl = IF(@pay_tenant_no_ready = 1 AND NOT EXISTS (SELECT 1 FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_pay_order' AND INDEX_NAME = 'idx_third_pay_order_tenant_pay_no'), 'ALTER TABLE `third_pay_order` ADD INDEX `idx_third_pay_order_tenant_pay_no` (`tenant_id`, `pay_no`, `del_flag`)', 'SELECT 1');
+PREPARE index_stmt FROM @index_ddl; EXECUTE index_stmt; DEALLOCATE PREPARE index_stmt;
+SET @index_ddl = IF(@pay_relation_ready = 1 AND NOT EXISTS (SELECT 1 FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_pay_order' AND INDEX_NAME = 'idx_third_pay_order_fz_relation'), 'ALTER TABLE `third_pay_order` ADD INDEX `idx_third_pay_order_fz_relation` (`tenant_id`, `fz_relation_id`)', 'SELECT 1');
+PREPARE index_stmt FROM @index_ddl; EXECUTE index_stmt; DEALLOCATE PREPARE index_stmt;
+SET @index_ddl = IF(@fz_task_scan_ready = 1 AND NOT EXISTS (SELECT 1 FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_fz_order' AND INDEX_NAME = 'idx_third_fz_order_task_scan'), 'ALTER TABLE `third_fz_order` ADD INDEX `idx_third_fz_order_task_scan` (`status`, `del_flag`, `update_time`, `id`)', 'SELECT 1');
+PREPARE index_stmt FROM @index_ddl; EXECUTE index_stmt; DEALLOCATE PREPARE index_stmt;
+SET @index_ddl = IF(@fz_tenant_no_ready = 1 AND NOT EXISTS (SELECT 1 FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_fz_order' AND INDEX_NAME = 'idx_third_fz_order_tenant_pay_no'), 'ALTER TABLE `third_fz_order` ADD INDEX `idx_third_fz_order_tenant_pay_no` (`tenant_id`, `pay_no`, `del_flag`)', 'SELECT 1');
+PREPARE index_stmt FROM @index_ddl; EXECUTE index_stmt; DEALLOCATE PREPARE index_stmt;
+SET @index_ddl = IF(@aggregate_task_scan_ready = 1 AND NOT EXISTS (SELECT 1 FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_aggregate_pay_session' AND INDEX_NAME = 'idx_third_aggregate_task_scan'), 'ALTER TABLE `third_aggregate_pay_session` ADD INDEX `idx_third_aggregate_task_scan` (`status`, `del_flag`, `update_time`, `id`)', 'SELECT 1');
+PREPARE index_stmt FROM @index_ddl; EXECUTE index_stmt; DEALLOCATE PREPARE index_stmt;
+SET @index_ddl = IF(@aggregate_close_scan_ready = 1 AND NOT EXISTS (SELECT 1 FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'third_aggregate_pay_session' AND INDEX_NAME = 'idx_third_aggregate_close_scan'), 'ALTER TABLE `third_aggregate_pay_session` ADD INDEX `idx_third_aggregate_close_scan` (`status`, `del_flag`, `create_time`, `id`)', 'SELECT 1');
+PREPARE index_stmt FROM @index_ddl; EXECUTE index_stmt; DEALLOCATE PREPARE index_stmt;
+
+-- 六、迁移钱包历史配置。只有关系完整、JSON合法且数据已核对时,才清理旧配置键和旧比例列。
+SET @wallet_table_exists = (
+ SELECT COUNT(1) FROM information_schema.TABLES
+ WHERE TABLE_SCHEMA = 'pjcloudx_wallet' AND TABLE_NAME = 'mch_pay_setting'
+);
+SET @legacy_ratio_column_exists = (
+ SELECT COUNT(1) FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = 'pjcloudx_wallet' AND TABLE_NAME = 'mch_pay_setting' AND COLUMN_NAME = 'fz_ratio'
+);
+SET @legacy_invalid_json_count = 0;
+SET @legacy_invalid_json_sql = IF(
+ @wallet_table_exists = 1,
+ 'SELECT COUNT(1) INTO @legacy_invalid_json_count FROM `pjcloudx_wallet`.`mch_pay_setting` WHERE platform_type IN (''10'', ''20'') AND setting_json IS NOT NULL AND TRIM(setting_json) <> '''' AND JSON_VALID(setting_json) = 0',
+ 'SELECT 1'
+);
+PREPARE legacy_invalid_json_stmt FROM @legacy_invalid_json_sql;
+EXECUTE legacy_invalid_json_stmt;
+DEALLOCATE PREPARE legacy_invalid_json_stmt;
+
+SET @legacy_channel_update_sql = IF(
+ @wallet_table_exists = 1 AND @legacy_invalid_json_count = 0,
+ 'UPDATE `pjcloudx_thirdparty`.`third_mch_fz_relation` r JOIN `pjcloudx_wallet`.`mch_pay_setting` s ON s.id = r.pay_setting_id AND s.mch_id = r.mch_id AND s.tenant_id = r.tenant_id SET r.channel_sub_mch_id = CASE WHEN NULLIF(TRIM(r.channel_sub_mch_id), '''') IS NULL THEN NULLIF(NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(s.setting_json, ''$.subMchid''))), ''''), ''null'') ELSE r.channel_sub_mch_id END, r.account_key = CASE WHEN s.platform_type = ''20'' AND NULLIF(TRIM(r.account_key), '''') IS NULL THEN NULLIF(NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(s.setting_json, ''$.accountKey''))), ''''), ''null'') ELSE r.account_key END, r.app_auth_token = CASE WHEN s.platform_type = ''20'' AND NULLIF(TRIM(r.app_auth_token), '''') IS NULL THEN NULLIF(NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(s.setting_json, ''$.appAuthToken''))), ''''), ''null'') ELSE r.app_auth_token END WHERE s.platform_type IN (''10'', ''20'') AND JSON_VALID(s.setting_json) = 1 AND r.del_flag = ''0'' AND r.is_default = ''1''',
+ 'SELECT 1'
+);
+PREPARE legacy_channel_update_stmt FROM @legacy_channel_update_sql;
+EXECUTE legacy_channel_update_stmt;
+DEALLOCATE PREPARE legacy_channel_update_stmt;
+
+SET @legacy_ratio_update_sql = IF(
+ @wallet_table_exists = 1 AND @legacy_ratio_column_exists = 1,
+ 'UPDATE `pjcloudx_thirdparty`.`third_mch_fz_relation` r JOIN `pjcloudx_wallet`.`mch_pay_setting` s ON s.id = r.pay_setting_id AND s.mch_id = r.mch_id AND s.tenant_id = r.tenant_id SET r.fz_ratio = LEAST(GREATEST(COALESCE(s.fz_ratio, 0), 0), 100) WHERE s.del_flag = ''0'' AND r.del_flag = ''0'' AND r.is_default = ''1'' AND COALESCE(s.fz_ratio, 0) > 0 AND COALESCE(r.fz_ratio, 0) = 0',
+ 'SELECT 1'
+);
+PREPARE legacy_ratio_update_stmt FROM @legacy_ratio_update_sql;
+EXECUTE legacy_ratio_update_stmt;
+DEALLOCATE PREPARE legacy_ratio_update_stmt;
+
+SET @legacy_channel_unmigrated_count = 0;
+SET @legacy_channel_unmigrated_sql = IF(
+ @wallet_table_exists = 1 AND @legacy_invalid_json_count = 0,
+ 'SELECT COUNT(1) INTO @legacy_channel_unmigrated_count FROM `pjcloudx_wallet`.`mch_pay_setting` s WHERE s.del_flag = ''0'' AND s.platform_type IN (''10'', ''20'') AND JSON_VALID(s.setting_json) = 1 AND (NULLIF(NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(s.setting_json, ''$.subMchid''))), ''''), ''null'') IS NOT NULL OR NULLIF(NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(s.setting_json, ''$.accountKey''))), ''''), ''null'') IS NOT NULL OR NULLIF(NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(s.setting_json, ''$.appAuthToken''))), ''''), ''null'') IS NOT NULL) AND NOT EXISTS (SELECT 1 FROM `pjcloudx_thirdparty`.`third_mch_fz_relation` r WHERE r.tenant_id = s.tenant_id AND r.mch_id = s.mch_id AND r.pay_setting_id = s.id AND r.platform_type = s.platform_type AND r.is_default = ''1'' AND r.del_flag = ''0'' AND (NULLIF(NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(s.setting_json, ''$.subMchid''))), ''''), ''null'') IS NULL OR r.channel_sub_mch_id = NULLIF(NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(s.setting_json, ''$.subMchid''))), ''''), ''null'')) AND (s.platform_type <> ''20'' OR NULLIF(NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(s.setting_json, ''$.accountKey''))), ''''), ''null'') IS NULL OR r.account_key = NULLIF(NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(s.setting_json, ''$.accountKey''))), ''''), ''null'')) AND (s.platform_type <> ''20'' OR NULLIF(NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(s.setting_json, ''$.appAuthToken''))), ''''), ''null'') IS NULL OR r.app_auth_token = NULLIF(NULLIF(TRIM(JSON_UNQUOTE(JSON_EXTRACT(s.setting_json, ''$.appAuthToken''))), ''''), ''null'')))',
+ 'SELECT 1'
+);
+PREPARE legacy_channel_unmigrated_stmt FROM @legacy_channel_unmigrated_sql;
+EXECUTE legacy_channel_unmigrated_stmt;
+DEALLOCATE PREPARE legacy_channel_unmigrated_stmt;
+
+SET @legacy_ratio_unmigrated_count = 0;
+SET @legacy_ratio_unmigrated_sql = IF(
+ @wallet_table_exists = 1 AND @legacy_ratio_column_exists = 1,
+ 'SELECT COUNT(1) INTO @legacy_ratio_unmigrated_count FROM `pjcloudx_wallet`.`mch_pay_setting` s WHERE COALESCE(s.fz_ratio, 0) > 0 AND s.del_flag = ''0'' AND NOT EXISTS (SELECT 1 FROM `pjcloudx_thirdparty`.`third_mch_fz_relation` r WHERE r.tenant_id = s.tenant_id AND r.mch_id = s.mch_id AND r.pay_setting_id = s.id AND r.is_default = ''1'' AND r.fz_ratio > 0 AND r.del_flag = ''0'')',
+ 'SELECT 1'
+);
+PREPARE legacy_ratio_unmigrated_stmt FROM @legacy_ratio_unmigrated_sql;
+EXECUTE legacy_ratio_unmigrated_stmt;
+DEALLOCATE PREPARE legacy_ratio_unmigrated_stmt;
+
+SET @legacy_json_cleanup_sql = IF(
+ @wallet_table_exists = 1 AND @legacy_invalid_json_count = 0 AND @legacy_channel_unmigrated_count = 0 AND @legacy_ratio_unmigrated_count = 0,
+ 'UPDATE `pjcloudx_wallet`.`mch_pay_setting` SET setting_json = CASE WHEN JSON_LENGTH(JSON_REMOVE(setting_json, ''$.subMchid'', ''$.accountKey'', ''$.appAuthToken'')) = 0 THEN NULL ELSE JSON_REMOVE(setting_json, ''$.subMchid'', ''$.accountKey'', ''$.appAuthToken'') END WHERE platform_type IN (''10'', ''20'') AND JSON_VALID(setting_json) = 1',
+ 'SELECT 1'
+);
+PREPARE legacy_json_cleanup_stmt FROM @legacy_json_cleanup_sql;
+EXECUTE legacy_json_cleanup_stmt;
+DEALLOCATE PREPARE legacy_json_cleanup_stmt;
+
+SET @drop_legacy_ratio_sql = IF(
+ @wallet_table_exists = 1 AND @legacy_ratio_column_exists = 1 AND @legacy_invalid_json_count = 0 AND @legacy_channel_unmigrated_count = 0 AND @legacy_ratio_unmigrated_count = 0,
+ 'ALTER TABLE `pjcloudx_wallet`.`mch_pay_setting` DROP COLUMN `fz_ratio`',
+ 'SELECT 1'
+);
+PREPARE drop_legacy_ratio_stmt FROM @drop_legacy_ratio_sql;
+EXECUTE drop_legacy_ratio_stmt;
+DEALLOCATE PREPARE drop_legacy_ratio_stmt;
+
+-- 七、输出迁移结果。出现BLOCKED时不要上线新代码,先处理对应数据或数据库前置问题。
+SET @required_table_count = (
+ SELECT COUNT(DISTINCT TABLE_NAME)
+ FROM information_schema.TABLES
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME IN ('third_aggregate_pay_session', 'third_payment_task',
+ 'third_payment_task_attempt', 'third_mch_fz_relation',
+ 'third_pay_order', 'third_fz_order')
+);
+SET @required_column_count = (
+ SELECT COUNT(1)
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND ((TABLE_NAME = 'third_mch_fz_relation' AND COLUMN_NAME IN ('app_auth_token', 'fz_ratio'))
+ OR (TABLE_NAME = 'third_payment_task' AND COLUMN_NAME = 'fz_relation_id')
+ OR (TABLE_NAME = 'third_pay_order' AND COLUMN_NAME = 'fz_relation_id'))
+);
+SET @skipped_scan_index_count =
+ (@pay_task_scan_ready = 0) + (@pay_fz_scan_ready = 0) + (@pay_callback_scan_ready = 0)
+ + (@pay_close_scan_ready = 0) + (@pay_tenant_no_ready = 0) + (@pay_relation_ready = 0)
+ + (@fz_task_scan_ready = 0) + (@fz_tenant_no_ready = 0)
+ + (@aggregate_task_scan_ready = 0) + (@aggregate_close_scan_ready = 0);
+
+SELECT CASE
+ WHEN @required_table_count <> 6 THEN 'BLOCKED_REQUIRED_THIRD_TABLE_NOT_FOUND'
+ WHEN @required_column_count <> 4 THEN 'BLOCKED_REQUIRED_THIRD_COLUMN_NOT_FOUND'
+ WHEN @skipped_scan_index_count > 0 THEN 'BLOCKED_SCAN_INDEX_SOURCE_COLUMN_NOT_FOUND'
+ WHEN @wallet_table_exists = 0 THEN 'BLOCKED_WALLET_TABLE_NOT_FOUND'
+ WHEN @legacy_invalid_json_count > 0 THEN 'BLOCKED_INVALID_SETTING_JSON'
+ WHEN @legacy_channel_unmigrated_count > 0 THEN 'BLOCKED_UNMIGRATED_CHANNEL_CONFIG'
+ WHEN @legacy_ratio_unmigrated_count > 0 THEN 'BLOCKED_UNMIGRATED_POSITIVE_RATIO'
+ ELSE 'SUCCESS_THIRD_PAYMENT_SCHEMA_MIGRATED'
+ END AS migration_status,
+ @legacy_invalid_json_count AS invalid_setting_json_count,
+ @legacy_channel_unmigrated_count AS unmigrated_channel_config_count,
+ @legacy_ratio_unmigrated_count AS unmigrated_positive_ratio_count,
+ @required_table_count AS required_third_table_count,
+ @required_column_count AS required_third_column_count,
+ @skipped_scan_index_count AS skipped_scan_index_count;
+
+SELECT table_name, column_name, column_type
+FROM information_schema.COLUMNS
+WHERE table_schema = DATABASE()
+ AND ((table_name = 'third_mch_fz_relation' AND column_name IN ('app_auth_token', 'fz_ratio'))
+ OR (table_name = 'third_payment_task' AND column_name = 'fz_relation_id')
+ OR (table_name = 'third_pay_order' AND column_name = 'fz_relation_id'))
+ORDER BY table_name, ordinal_position;
diff --git a/db/800pjcloudx_wallet.sql b/db/800pjcloudx_wallet.sql
index 462e9448..deffa17b 100644
--- a/db/800pjcloudx_wallet.sql
+++ b/db/800pjcloudx_wallet.sql
@@ -164,9 +164,6 @@ CREATE TABLE mch_pay_setting
use_scope varchar(256) NOT NULL COMMENT '使用范围(直接支付,钱包充值)',
setting_json text DEFAULT NULL COMMENT '支付json配置',
- fz_ratio int NOT NULL COMMENT '分账比例(最高100%即分给对方商户的%)',
-
-
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/pjcloud-common/pjcloud-common-data/src/main/java/com/bsd/pjcloud/common/data/datascope/DataScopeInnerInterceptor.java b/pjcloud-common/pjcloud-common-data/src/main/java/com/bsd/pjcloud/common/data/datascope/DataScopeInnerInterceptor.java
index 293b6a47..913f1931 100644
--- a/pjcloud-common/pjcloud-common-data/src/main/java/com/bsd/pjcloud/common/data/datascope/DataScopeInnerInterceptor.java
+++ b/pjcloud-common/pjcloud-common-data/src/main/java/com/bsd/pjcloud/common/data/datascope/DataScopeInnerInterceptor.java
@@ -2,7 +2,6 @@ package com.bsd.pjcloud.common.data.datascope;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
-import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONUtil;
@@ -53,19 +52,23 @@ public class DataScopeInnerInterceptor implements DataScopeInterceptor {
List deptIds = dataScope.getDeptList();
- // 1.无数据权限限制,则直接返回 0 条数据
- if (CollUtil.isEmpty(deptIds) && StrUtil.isBlank(dataScope.getUsername())) {
+ boolean hasDeptScope = CollUtil.isNotEmpty(deptIds);
+ boolean hasUserScope = StrUtil.isNotBlank(dataScope.getUsername())
+ && StrUtil.isNotBlank(dataScope.getScopeUserName());
+
+ // 1.无可用权限条件时直接返回 0 条数据,避免生成非法的 IN (),也不能因本人字段被禁用而放开查询。
+ if (!hasDeptScope && !hasUserScope) {
originalSql = String.format("SELECT %s FROM (%s) temp_data_scope WHERE 1 = 2",
dataScope.getFunc().getType(), originalSql);
}
// 2.如果为本人权限 + 部门权限控制
- else if (StrUtil.isNotBlank(dataScope.getUsername()) && ObjectUtil.isNotEmpty(dataScope.getScopeUserName()) && CollUtil.isNotEmpty(deptIds)) {
+ else if (hasUserScope && hasDeptScope) {
String join = CollectionUtil.join(deptIds, ",");
originalSql = String.format("SELECT %s FROM (%s) temp_data_scope WHERE temp_data_scope.%s = '%s' OR temp_data_scope.%s IN (%s)",
dataScope.getFunc().getType(), originalSql, dataScope.getScopeUserName(), dataScope.getUsername(), dataScope.getScopeDeptName(), join);
}
// 3. 如果为本人
- else if (StrUtil.isNotBlank(dataScope.getUsername()) && ObjectUtil.isNotEmpty(dataScope.getScopeUserName())) {
+ else if (hasUserScope) {
originalSql = String.format("SELECT %s FROM (%s) temp_data_scope WHERE temp_data_scope.%s = '%s'",
dataScope.getFunc().getType(), originalSql, dataScope.getScopeUserName(), dataScope.getUsername());
}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/refund/HardAutoRefundTriggerTypeEnum.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/refund/HardAutoRefundTriggerTypeEnum.java
index b28ea530..6633dcee 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/refund/HardAutoRefundTriggerTypeEnum.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/constant/refund/HardAutoRefundTriggerTypeEnum.java
@@ -26,7 +26,12 @@ public enum HardAutoRefundTriggerTypeEnum {
/**
* 管理端业务人员对已到期候选订单确认后执行的逐单退款,不复用技术重试权限。
*/
- EXPIRED_MANUAL("40", "后台逐单过期退款", false);
+ EXPIRED_MANUAL("40", "后台逐单过期退款", false),
+
+ /**
+ * 管理端按商户当前配置立即重扫历史订单后触发的自动退款,失败后继续进入既有自动重试状态机。
+ */
+ EXPIRED_RESCAN("50", "后台立即重新扫描", true);
/**
* 数据库存储的稳定触发编码。
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardAuthDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardAuthDto.java
index 2eac5ae6..4ea972c0 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardAuthDto.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardAuthDto.java
@@ -1,12 +1,31 @@
package com.bsd.pjcloud.hard.api.dto;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserStudentDto;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserTeacherDto;
+import com.bsd.pjcloud.admin.api.vo.base.BaseUserIdentityQueryInterface;
import com.bsd.pjcloud.hard.api.entity.HardAuthEntity;
import lombok.Data;
import java.time.LocalDate;
+import java.util.List;
@Data
-public class HardAuthDto extends HardAuthEntity {
+public class HardAuthDto extends HardAuthEntity implements BaseUserIdentityQueryInterface {
+
+ /** 身份预筛选后用于授权记录查询的用户主键集合。 */
+ private List userIds;
+
+ /** UPMS 用户身份协议编码集合;为空时不限制身份。 */
+ private List identityTypes;
+
+ /** 学生身份扩展查询条件;对象为空或字段均为空时不限制学生资料。 */
+ private SysUserStudentDto studentInfo;
+
+ /** 老师身份扩展查询条件;对象为空或老师学号为空时不限制老师资料。 */
+ private SysUserTeacherDto teacherInfo;
+
+ /** 人脸录入状态;1 表示已录入,0 表示未录入,空值表示不限制。 */
+ private String hasFace;
private LocalDate[] createTimeList;
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardBroadcastDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardBroadcastDto.java
index 52af6d62..f15ab986 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardBroadcastDto.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardBroadcastDto.java
@@ -1,12 +1,31 @@
package com.bsd.pjcloud.hard.api.dto;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserStudentDto;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserTeacherDto;
+import com.bsd.pjcloud.admin.api.vo.base.BaseUserIdentityQueryInterface;
import com.bsd.pjcloud.hard.api.entity.HardBroadcastEntity;
import lombok.Data;
import java.time.LocalDate;
+import java.util.List;
@Data
-public class HardBroadcastDto extends HardBroadcastEntity {
+public class HardBroadcastDto extends HardBroadcastEntity implements BaseUserIdentityQueryInterface {
+
+ /** 身份预筛选后用于广播记录查询的用户主键集合。 */
+ private List userIds;
+
+ /** UPMS 用户身份协议编码集合;为空时不限制身份。 */
+ private List identityTypes;
+
+ /** 学生身份扩展查询条件;对象为空或字段均为空时不限制学生资料。 */
+ private SysUserStudentDto studentInfo;
+
+ /** 老师身份扩展查询条件;对象为空或老师学号为空时不限制老师资料。 */
+ private SysUserTeacherDto teacherInfo;
+
+ /** 人脸录入状态;1 表示已录入,0 表示未录入,空值表示不限制。 */
+ private String hasFace;
private LocalDate[] createTimeList;
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardDoorFlowDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardDoorFlowDto.java
index 37ec0c8f..b2c852e4 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardDoorFlowDto.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardDoorFlowDto.java
@@ -1,12 +1,31 @@
package com.bsd.pjcloud.hard.api.dto;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserStudentDto;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserTeacherDto;
+import com.bsd.pjcloud.admin.api.vo.base.BaseUserIdentityQueryInterface;
import com.bsd.pjcloud.hard.api.entity.HardDoorFlowEntity;
import lombok.Data;
import java.time.LocalDate;
+import java.util.List;
@Data
-public class HardDoorFlowDto extends HardDoorFlowEntity {
+public class HardDoorFlowDto extends HardDoorFlowEntity implements BaseUserIdentityQueryInterface {
+
+ /** 身份预筛选后用于开门流程查询的用户主键集合。 */
+ private List userIds;
+
+ /** UPMS 用户身份协议编码集合;为空时不限制身份。 */
+ private List identityTypes;
+
+ /** 学生身份扩展查询条件;对象为空或字段均为空时不限制学生资料。 */
+ private SysUserStudentDto studentInfo;
+
+ /** 老师身份扩展查询条件;对象为空或老师学号为空时不限制老师资料。 */
+ private SysUserTeacherDto teacherInfo;
+
+ /** 人脸录入状态;1 表示已录入,0 表示未录入,空值表示不限制。 */
+ private String hasFace;
private LocalDate[] createTimeList;
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardInvoiceDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardInvoiceDto.java
index 28d6ec52..e90e5bf4 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardInvoiceDto.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/HardInvoiceDto.java
@@ -1,5 +1,8 @@
package com.bsd.pjcloud.hard.api.dto;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserStudentDto;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserTeacherDto;
+import com.bsd.pjcloud.admin.api.vo.base.BaseUserIdentityQueryInterface;
import com.bsd.pjcloud.hard.api.entity.HardInvoiceEntity;
import lombok.Data;
@@ -7,12 +10,24 @@ import java.time.LocalDate;
import java.util.List;
@Data
-public class HardInvoiceDto extends HardInvoiceEntity {
+public class HardInvoiceDto extends HardInvoiceEntity implements BaseUserIdentityQueryInterface {
private List deptIds;
private List userIds;
+ /** UPMS 用户身份协议编码集合;为空时不限制身份。 */
+ private List identityTypes;
+
+ /** 学生身份扩展查询条件;对象为空或字段均为空时不限制学生资料。 */
+ private SysUserStudentDto studentInfo;
+
+ /** 老师身份扩展查询条件;对象为空或老师学号为空时不限制老师资料。 */
+ private SysUserTeacherDto teacherInfo;
+
+ /** 人脸录入状态;1 表示已录入,0 表示未录入,空值表示不限制。 */
+ private String hasFace;
+
private List merchantIds;
private String hardType;
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardAfterSaleDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardAfterSaleDto.java
index 10450a42..828d157c 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardAfterSaleDto.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardAfterSaleDto.java
@@ -1,5 +1,8 @@
package com.bsd.pjcloud.hard.api.dto.order;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserStudentDto;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserTeacherDto;
+import com.bsd.pjcloud.admin.api.vo.base.BaseUserIdentityQueryInterface;
import com.bsd.pjcloud.hard.api.entity.order.HardAfterSaleEntity;
import lombok.Data;
@@ -8,10 +11,22 @@ import java.time.LocalDateTime;
import java.util.List;
@Data
-public class HardAfterSaleDto extends HardAfterSaleEntity {
+public class HardAfterSaleDto extends HardAfterSaleEntity implements BaseUserIdentityQueryInterface {
private List userIds;
+ /** UPMS 用户身份协议编码集合;为空时不限制身份。 */
+ private List identityTypes;
+
+ /** 学生身份扩展查询条件;对象为空或字段均为空时不限制学生资料。 */
+ private SysUserStudentDto studentInfo;
+
+ /** 老师身份扩展查询条件;对象为空或老师学号为空时不限制老师资料。 */
+ private SysUserTeacherDto teacherInfo;
+
+ /** 人脸录入状态;1 表示已录入,0 表示未录入,空值表示不限制。 */
+ private String hasFace;
+
private LocalDate[] createTimeList;
private List mchIds;
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderDto.java
index a25062a1..732377b4 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderDto.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderDto.java
@@ -1,6 +1,9 @@
package com.bsd.pjcloud.hard.api.dto.order;
import com.bsd.pjcloud.admin.api.vo.base.BaseIsIncludeTreeInterface;
+import com.bsd.pjcloud.admin.api.vo.base.BaseUserIdentityQueryInterface;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserStudentDto;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserTeacherDto;
import com.bsd.pjcloud.common.core.constant.enums.DateRangeTypeEnum;
import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity;
import lombok.Data;
@@ -9,8 +12,9 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
+/** 硬件订单后台查询条件,身份字段用于按订单用户的身份资料筛选。 */
@Data
-public class HardOrderDto extends HardOrderEntity implements BaseIsIncludeTreeInterface {
+public class HardOrderDto extends HardOrderEntity implements BaseIsIncludeTreeInterface, BaseUserIdentityQueryInterface {
private DateRangeTypeEnum dateRangeType = DateRangeTypeEnum.YESTERDAY;
@@ -131,4 +135,16 @@ public class HardOrderDto extends HardOrderEntity implements BaseIsIncludeTreeIn
* `apptId`或者`appt_rule_id`不为null(筛选预约订单)
*/
Boolean apptIdOrApptRuleIdNotNull = false;
+
+ /** 用户身份协议编码集合;为空表示不限制身份。 */
+ private List identityTypes;
+
+ /** 学生身份扩展筛选条件。 */
+ private SysUserStudentDto studentInfo;
+
+ /** 老师身份扩展筛选条件。 */
+ private SysUserTeacherDto teacherInfo;
+
+ /** 人脸录入状态:1 已录入、0 未录入、空值表示全部。 */
+ private String hasFace;
}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/spu/HardSpuRepertoryDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/spu/HardSpuRepertoryDto.java
index 94e3622f..eebb0bd5 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/spu/HardSpuRepertoryDto.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/spu/HardSpuRepertoryDto.java
@@ -1,18 +1,37 @@
package com.bsd.pjcloud.hard.api.dto.spu;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserStudentDto;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserTeacherDto;
+import com.bsd.pjcloud.admin.api.vo.base.BaseUserIdentityQueryInterface;
import com.bsd.pjcloud.hard.api.entity.spu.HardSpuRepertoryEntity;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
+/** 硬件商品库存更新记录查询条件,支持按上架用户身份快速筛选。 */
@Data
-public class HardSpuRepertoryDto extends HardSpuRepertoryEntity {
+public class HardSpuRepertoryDto extends HardSpuRepertoryEntity implements BaseUserIdentityQueryInterface {
+ /** 库存更新记录创建时间闭区间;数组长度为 2 时生效。 */
private LocalDateTime[] createTimeList;
+ /** 身份预筛选后用于库存记录查询的上架用户主键集合。 */
private List userIds;
+ /** 目标柜门主键;为空时不限制柜门。 */
private Long cabinetId;
+ /** UPMS 用户身份协议编码集合;为空表示不限制身份。 */
+ private List identityTypes;
+
+ /** 学生身份扩展查询条件;对象为空或字段均为空时不限制学生资料。 */
+ private SysUserStudentDto studentInfo;
+
+ /** 老师身份扩展查询条件;对象为空或老师学号为空时不限制老师资料。 */
+ private SysUserTeacherDto teacherInfo;
+
+ /** 人脸录入状态;1 表示已录入,0 表示未录入,空值表示不限制。 */
+ private String hasFace;
+
}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/order/HardOrderEntity.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/order/HardOrderEntity.java
index d124723d..1665a719 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/order/HardOrderEntity.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/order/HardOrderEntity.java
@@ -163,11 +163,11 @@ public class HardOrderEntity extends Model {
@Schema(description = "过期预约退款配置ID快照")
private Long expiredRefundConfigId;
- /** 预约订单支付成功时解析出的实际餐段结束时间;为空表示无法安全确定过期资格。 */
- @Schema(description = "过期预约餐段实际结束时间快照")
+ /** 预约下单校验通过时固化的用餐日核销有效期结束时间,固定为 apptUseDate 当天 23:59:59;不受退款开关影响。 */
+ @Schema(description = "过期预约用餐日核销结束时间快照")
private LocalDateTime expiredRefundUsageEndTime;
- /** 实际餐段结束时间加配置延迟后的过期退款触发时间。 */
+ /** 用餐日核销有效期结束时间加配置延迟后的过期退款触发时间。 */
@Schema(description = "过期预约退款触发时间")
private LocalDateTime expiredRefundDeadline;
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/HardAuthVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/HardAuthVo.java
index 4baf063e..30347ec5 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/HardAuthVo.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/HardAuthVo.java
@@ -1,6 +1,9 @@
package com.bsd.pjcloud.hard.api.vo;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserStudentDto;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserTeacherDto;
import com.bsd.pjcloud.admin.api.vo.base.BaseUserDeptInterface;
+import com.bsd.pjcloud.admin.api.vo.base.BaseUserIdentityInterface;
import com.bsd.pjcloud.hard.api.entity.HardAuthEntity;
import com.bsd.pjcloud.hard.api.vo.base.BaseHardInterface;
import lombok.Data;
@@ -8,7 +11,7 @@ import lombok.Data;
import java.util.List;
@Data
-public class HardAuthVo extends HardAuthEntity implements BaseHardInterface, BaseUserDeptInterface {
+public class HardAuthVo extends HardAuthEntity implements BaseHardInterface, BaseUserDeptInterface, BaseUserIdentityInterface {
/**
* 设备相关
@@ -26,10 +29,22 @@ public class HardAuthVo extends HardAuthEntity implements BaseHardInterface, Bas
private String userMobile;
+ /** 用户标准手机号字段,供身份组件展示;与历史 userMobile 同步回填。 */
+ private String phone;
+
private String userAvatar;
private Long userDeptId;
private String userDeptName;
+ /** 用户拥有的 UPMS 身份协议编码集合。 */
+ private List identityTypes;
+
+ /** 用户的学生身份扩展资料;无学生身份时为空。 */
+ private SysUserStudentDto studentInfo;
+
+ /** 用户的老师身份扩展资料;无老师身份时为空。 */
+ private SysUserTeacherDto teacherInfo;
+
}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/HardBroadcastVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/HardBroadcastVo.java
index 22967938..12d3a11e 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/HardBroadcastVo.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/HardBroadcastVo.java
@@ -1,6 +1,9 @@
package com.bsd.pjcloud.hard.api.vo;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserStudentDto;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserTeacherDto;
import com.bsd.pjcloud.admin.api.vo.base.BaseUserDeptInterface;
+import com.bsd.pjcloud.admin.api.vo.base.BaseUserIdentityInterface;
import com.bsd.pjcloud.hard.api.entity.HardBroadcastEntity;
import com.bsd.pjcloud.hard.api.vo.base.BaseHardInterface;
import lombok.Data;
@@ -8,7 +11,7 @@ import lombok.Data;
import java.util.List;
@Data
-public class HardBroadcastVo extends HardBroadcastEntity implements BaseHardInterface, BaseUserDeptInterface {
+public class HardBroadcastVo extends HardBroadcastEntity implements BaseHardInterface, BaseUserDeptInterface, BaseUserIdentityInterface {
/**
* 设备相关
@@ -26,12 +29,24 @@ public class HardBroadcastVo extends HardBroadcastEntity implements BaseHardInte
private String userMobile;
+ /** 用户标准手机号字段,供身份组件展示;与历史 userMobile 同步回填。 */
+ private String phone;
+
private String userAvatar;
private Long userDeptId;
private String userDeptName;
+ /** 用户拥有的 UPMS 身份协议编码集合。 */
+ private List identityTypes;
+
+ /** 用户的学生身份扩展资料;无学生身份时为空。 */
+ private SysUserStudentDto studentInfo;
+
+ /** 用户的老师身份扩展资料;无老师身份时为空。 */
+ private SysUserTeacherDto teacherInfo;
+
/**
* 订单相关
*/
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/HardDoorFlowVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/HardDoorFlowVo.java
index ec71266b..f0974aa8 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/HardDoorFlowVo.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/HardDoorFlowVo.java
@@ -1,6 +1,9 @@
package com.bsd.pjcloud.hard.api.vo;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserStudentDto;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserTeacherDto;
import com.bsd.pjcloud.admin.api.vo.base.BaseUserDeptInterface;
+import com.bsd.pjcloud.admin.api.vo.base.BaseUserIdentityInterface;
import com.bsd.pjcloud.hard.api.entity.HardAuthEntity;
import com.bsd.pjcloud.hard.api.entity.HardDoorFlowEntity;
import com.bsd.pjcloud.hard.api.vo.base.BaseHardInterface;
@@ -9,7 +12,7 @@ import lombok.Data;
import java.util.List;
@Data
-public class HardDoorFlowVo extends HardDoorFlowEntity implements BaseHardInterface, BaseUserDeptInterface {
+public class HardDoorFlowVo extends HardDoorFlowEntity implements BaseHardInterface, BaseUserDeptInterface, BaseUserIdentityInterface {
/**
* 设备相关
@@ -27,12 +30,24 @@ public class HardDoorFlowVo extends HardDoorFlowEntity implements BaseHardInterf
private String userMobile;
+ /** 用户标准手机号字段,供身份组件展示;与历史 userMobile 同步回填。 */
+ private String phone;
+
private String userAvatar;
private Long userDeptId;
private String userDeptName;
+ /** 用户拥有的 UPMS 身份协议编码集合。 */
+ private List identityTypes;
+
+ /** 用户的学生身份扩展资料;无学生身份时为空。 */
+ private SysUserStudentDto studentInfo;
+
+ /** 用户的老师身份扩展资料;无老师身份时为空。 */
+ private SysUserTeacherDto teacherInfo;
+
private HardAuthEntity hardAuth;
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/HardInvoiceVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/HardInvoiceVo.java
index 0cd57feb..77a0a1d1 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/HardInvoiceVo.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/HardInvoiceVo.java
@@ -1,7 +1,10 @@
package com.bsd.pjcloud.hard.api.vo;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserStudentDto;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserTeacherDto;
import com.bsd.pjcloud.admin.api.vo.base.BaseDeptInterface;
-import com.bsd.pjcloud.admin.api.vo.base.BaseUserInterface;
+import com.bsd.pjcloud.admin.api.vo.base.BaseUserDeptInterface;
+import com.bsd.pjcloud.admin.api.vo.base.BaseUserIdentityInterface;
import com.bsd.pjcloud.hard.api.entity.HardInvoiceEntity;
import com.bsd.pjcloud.hard.api.vo.base.BaseHardInterface;
import com.bsd.pjcloud.hard.api.vo.base.BaseOrderInterface;
@@ -9,9 +12,10 @@ import com.bsd.pjcloud.wallet.api.vo.base.BaseMchInterface;
import lombok.Data;
import java.time.LocalDateTime;
+import java.util.List;
@Data
-public class HardInvoiceVo extends HardInvoiceEntity implements BaseDeptInterface, BaseMchInterface, BaseOrderInterface, BaseHardInterface, BaseUserInterface {
+public class HardInvoiceVo extends HardInvoiceEntity implements BaseUserDeptInterface, BaseUserIdentityInterface, BaseDeptInterface, BaseMchInterface, BaseOrderInterface, BaseHardInterface {
private Long historyId;
@@ -72,6 +76,24 @@ public class HardInvoiceVo extends HardInvoiceEntity implements BaseDeptInterfac
private String userMobile;
+ /** 用户标准手机号字段,供身份组件展示;与历史 userMobile 同步回填。 */
+ private String phone;
+
private String userAvatar;
+ /** UPMS 当前用户部门主键,用于公共用户信息组件的标准别名。 */
+ private Long userDeptId;
+
+ /** UPMS 当前用户部门名称,用于公共用户信息组件的标准别名。 */
+ private String userDeptName;
+
+ /** 用户拥有的 UPMS 身份协议编码集合。 */
+ private List identityTypes;
+
+ /** 用户的学生身份扩展资料;无学生身份时为空。 */
+ private SysUserStudentDto studentInfo;
+
+ /** 用户的老师身份扩展资料;无老师身份时为空。 */
+ private SysUserTeacherDto teacherInfo;
+
}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardAfterSaleVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardAfterSaleVo.java
index 49b3e4ad..c4a1faac 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardAfterSaleVo.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardAfterSaleVo.java
@@ -1,15 +1,25 @@
package com.bsd.pjcloud.hard.api.vo.order;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserStudentDto;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserTeacherDto;
import com.bsd.pjcloud.admin.api.vo.base.BaseDeptInterface;
+import com.bsd.pjcloud.admin.api.vo.base.BaseUserDeptInterface;
+import com.bsd.pjcloud.admin.api.vo.base.BaseUserIdentityInterface;
import com.bsd.pjcloud.admin.api.vo.base.BaseUserInterface;
import com.bsd.pjcloud.hard.api.entity.order.HardAfterSaleEntity;
import com.bsd.pjcloud.hard.api.vo.base.BaseOrderInterface;
import lombok.Data;
import java.time.LocalDateTime;
+import java.util.List;
+/**
+ * 硬件订单售后详情。
+ *
+ * 同时保留历史用户、用户部门和用户身份协议,兼容售后详情的旧用户装配逻辑及管理端身份展示。
+ */
@Data
-public class HardAfterSaleVo extends HardAfterSaleEntity implements BaseUserInterface, BaseDeptInterface, BaseOrderInterface {
+public class HardAfterSaleVo extends HardAfterSaleEntity implements BaseUserInterface, BaseUserDeptInterface, BaseUserIdentityInterface, BaseDeptInterface, BaseOrderInterface {
/**
* 自动退款任务状态;为空表示当前售后仍走人工审核,非空时前端不得展示审核或取消操作。
*/
@@ -40,8 +50,26 @@ public class HardAfterSaleVo extends HardAfterSaleEntity implements BaseUserInte
private String userMobile;
+ /** 用户标准手机号字段,供身份组件展示;与历史 userMobile 同步回填。 */
+ private String phone;
+
private String userAvatar;
+ /** UPMS 当前用户部门主键,用于公共用户信息组件的标准别名。 */
+ private Long userDeptId;
+
+ /** UPMS 当前用户部门名称,用于公共用户信息组件的标准别名。 */
+ private String userDeptName;
+
+ /** 用户拥有的 UPMS 身份协议编码集合。 */
+ private List identityTypes;
+
+ /** 用户的学生身份扩展资料;无学生身份时为空。 */
+ private SysUserStudentDto studentInfo;
+
+ /** 用户的老师身份扩展资料;无老师身份时为空。 */
+ private SysUserTeacherDto teacherInfo;
+
/**
* 部门相关
*/
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenProductApptExportRecord.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenProductApptExportRecord.java
index 96f057a1..843d866e 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenProductApptExportRecord.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardCanteenProductApptExportRecord.java
@@ -11,6 +11,11 @@ import lombok.Data;
@Data
public class HardCanteenProductApptExportRecord {
+ /**
+ * 导出聚合层级,固定为 SUMMARY、MERCHANT 或 PRODUCT;仅用于一次查询后拆分三级结果,不持久化。
+ */
+ private String recordLevel;
+
/** 业务发生日期,格式为 yyyy-MM-dd;线下取支付日期,预约取核销日期。 */
private String statisticsDate;
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardExpiredRefundRescanResultVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardExpiredRefundRescanResultVo.java
new file mode 100644
index 00000000..dcfc0c56
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardExpiredRefundRescanResultVo.java
@@ -0,0 +1,36 @@
+package com.bsd.pjcloud.hard.api.vo.order;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 商户过期预约退款立即重扫结果,供管理端展示本轮任务补建和退款执行数量。
+ */
+@Data
+@Schema(description = "过期预约退款立即重扫结果")
+public class HardExpiredRefundRescanResultVo {
+
+ /** 本轮读取的已支付、未核销每日菜谱订单数量。 */
+ @Schema(description = "扫描订单数量")
+ private Integer scannedOrderCount;
+
+ /** 本轮首次补建的过期预约退款任务数量。 */
+ @Schema(description = "新建任务数量")
+ private Integer createdTaskCount;
+
+ /** 本轮按当前延迟和退库存配置刷新的未尝试任务数量。 */
+ @Schema(description = "刷新任务数量")
+ private Integer refreshedTaskCount;
+
+ /** 本轮已到达当前触发时间并立即进入退款状态机的任务数量。 */
+ @Schema(description = "立即执行任务数量")
+ private Integer executedTaskCount;
+
+ /** 本轮立即执行后已完成钱包退款和 Hardware 状态更新的任务数量。 */
+ @Schema(description = "退款成功任务数量")
+ private Integer successTaskCount;
+
+ /** 因餐段时间无法解析、已有资金尝试或存在冲突业务状态而未同步的订单数量。 */
+ @Schema(description = "跳过订单数量")
+ private Integer skippedOrderCount;
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderVo.java
index ffe257dd..1fe36132 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderVo.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderVo.java
@@ -1,6 +1,9 @@
package com.bsd.pjcloud.hard.api.vo.order;
import com.bsd.pjcloud.admin.api.vo.base.BaseUserDeptInterface;
+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.vo.base.BaseHardInterface;
import com.bsd.pjcloud.wallet.api.vo.base.BaseMchInterface;
@@ -11,8 +14,9 @@ import lombok.Data;
import java.time.LocalTime;
import java.util.List;
+/** 硬件订单管理端展示对象,包含公共用户主资料、部门及身份信息。 */
@Data
-public class HardOrderVo extends HardOrderEntity implements BaseUserDeptInterface, BaseMchInterface, BaseHardInterface {
+public class HardOrderVo extends HardOrderEntity implements BaseUserDeptInterface, BaseUserIdentityInterface, BaseMchInterface, BaseHardInterface {
/** 统计明细来源编码;线下订单为 OFFLINE_PURCHASE,预约已核销订单为 RESERVATION_VERIFIED。 */
private String saleSource;
@@ -26,8 +30,20 @@ public class HardOrderVo extends HardOrderEntity implements BaseUserDeptInterfac
private String userMobile;
+ /** 用户标准手机号字段,供统一身份组件展示;与历史 userMobile 字段并存。 */
+ private String phone;
+
private String userAvatar;
+ /** 用户身份协议编码集合;没有身份时为空集合或空值。 */
+ private List identityTypes;
+
+ /** 学生身份扩展资料;没有学生身份时为空。 */
+ private SysUserStudentDto studentInfo;
+
+ /** 老师身份扩展资料;没有老师身份时为空。 */
+ private SysUserTeacherDto teacherInfo;
+
/**
* 部门相关
*/
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/spu/HardSpuRepertoryVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/spu/HardSpuRepertoryVo.java
index 052836be..58a369c3 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/spu/HardSpuRepertoryVo.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/spu/HardSpuRepertoryVo.java
@@ -1,59 +1,128 @@
package com.bsd.pjcloud.hard.api.vo.spu;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserStudentDto;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserTeacherDto;
+import com.bsd.pjcloud.admin.api.vo.base.BaseUserDeptInterface;
+import com.bsd.pjcloud.admin.api.vo.base.BaseUserIdentityInterface;
import com.bsd.pjcloud.admin.api.vo.base.BaseUserInterface;
import com.bsd.pjcloud.hard.api.entity.spu.HardSpuRepertoryEntity;
import com.bsd.pjcloud.hard.api.vo.base.BaseHardInterface;
import com.bsd.pjcloud.wallet.api.vo.base.BaseMchInterface;
import lombok.Data;
+import java.util.List;
+
+/** 硬件商品库存更新记录展示对象,包含上架用户主资料、部门和身份信息。 */
@Data
-public class HardSpuRepertoryVo extends HardSpuRepertoryEntity implements BaseMchInterface, BaseUserInterface, BaseHardInterface {
+public class HardSpuRepertoryVo extends HardSpuRepertoryEntity implements BaseMchInterface, BaseUserInterface, BaseUserDeptInterface, BaseUserIdentityInterface, BaseHardInterface {
/**
* 商品相关
*/
+ /** 本次更新对应的商品名称。 */
private String spuName;
+ /** 商品分类主键。 */
private Long spuCategoryId;
+ /** 商品所属商户主键。 */
private Long spuMchId;
+ /** 商品所在硬件柜门数量。 */
private Long hardDoorNumber;
/**
* 分类相关
*/
+ /** 商品分类名称。 */
private String spuCategoryName;
/**
* 商户相关
*/
+ /** 商品所属商户主键,供公共商户展示契约读取。 */
private Long merchantId;
+ /** 商品所属商户名称。 */
private String merchantName;
+ /** 商品所属商户 Logo 地址。 */
private String logoPic;
+ /** 商品所属商户类型协议编码。 */
private String merchantType;
/**
* 用户相关
*/
+ /** 上架用户姓名,保留历史接口字段名。 */
private String username;
+ /** 上架用户手机号,保留历史接口字段名。 */
private String userMobile;
+ /** 上架用户头像地址,保留历史接口字段名。 */
private String userAvatar;
+ /** 用户标准手机号字段,供统一身份组件展示;与历史 userMobile 字段并存。 */
+ private String phone;
+
+ /** 用户当前所属部门主键,由 UPMS 用户资料回填。 */
+ private Long userDeptId;
+
+ /** 用户当前所属部门名称,由 UPMS 用户资料回填。 */
+ private String userDeptName;
+
+ /** 用户所属部门展示名称,供用户信息单元格读取。 */
+ private String deptName;
+
+ /** 用户拥有的 UPMS 身份协议编码集合。 */
+ private List identityTypes;
+
+ /** 用户的学生身份扩展资料;无学生身份时为空。 */
+ private SysUserStudentDto studentInfo;
+
+ /** 用户的老师身份扩展资料;无老师身份时为空。 */
+ private SysUserTeacherDto teacherInfo;
+
+ /** 获取 UPMS 返回的用户当前部门主键。 */
+ @Override
+ public Long getUserDeptId() {
+ return userDeptId;
+ }
+
+ /** 保存 UPMS 返回的用户当前部门主键。 */
+ @Override
+ public void setUserDeptId(Long userDeptId) {
+ this.userDeptId = userDeptId;
+ }
+
+ /** 获取 UPMS 返回的用户当前部门名称。 */
+ @Override
+ public String getUserDeptName() {
+ return userDeptName;
+ }
+
+ /** 保存 UPMS 返回的用户当前部门名称,并同步公共展示字段。 */
+ @Override
+ public void setUserDeptName(String userDeptName) {
+ this.userDeptName = userDeptName;
+ this.deptName = userDeptName;
+ }
+
/**
* 设备相关
*/
+ /** 执行库存更新的硬件主键。 */
private Long hardId;
+ /** 执行库存更新的硬件名称。 */
private String hardName;
+ /** 执行库存更新的硬件类型协议编码。 */
private String hardType;
+ /** 执行库存更新的硬件设备编号。 */
private String deviceCode;
}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardAutoRefundTaskController.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardAutoRefundTaskController.java
index faecc1da..fa5149d9 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardAutoRefundTaskController.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardAutoRefundTaskController.java
@@ -10,6 +10,7 @@ import com.bsd.pjcloud.hard.api.dto.order.HardAutoRefundTaskPageDto;
import com.bsd.pjcloud.hard.api.entity.order.HardAutoRefundAttemptLogEntity;
import com.bsd.pjcloud.hard.api.entity.order.HardAutoRefundTaskEntity;
import com.bsd.pjcloud.hard.api.vo.order.HardAutoRefundTaskVo;
+import com.bsd.pjcloud.hard.api.vo.order.HardExpiredRefundRescanResultVo;
import com.bsd.pjcloud.hard.api.vo.order.app.HardAppAutoRefundTaskVo;
import com.bsd.pjcloud.hard.service.HardAfterSaleService;
import com.bsd.pjcloud.hard.service.HardAutoRefundTaskService;
@@ -62,7 +63,7 @@ public class HardAutoRefundTaskController {
* 按当前用户商户数据范围分页查询自动退款任务。
*
* @param page 分页参数
- * @param dto 筛选条件
+ * @param dto 筛选条件
* @return 任务分页
*/
@GetMapping("/page")
@@ -81,24 +82,40 @@ public class HardAutoRefundTaskController {
}
/**
- * 查询已到期且仍可整单退款的每日菜谱预约订单;失败或重试耗尽任务不混入候选。
+ * 按商户详情页传入的精确商户 ID 查询已到期且仍可整单退款的每日菜谱预约订单;失败或重试耗尽任务不混入候选。
+ *
+ * @param page 分页参数
+ * @param dto 候选查询条件,其中商户 ID 必填
+ * @return 当前商户的待过期退款订单分页
*/
@GetMapping("/expired-candidates")
@Operation(summary = "查询待过期预约退款订单")
@PreAuthorize("@pms.hasPermission('hard_hardAfterSale_view')")
- public R> expiredCandidates(@ParameterObject Page page,
- @ParameterObject HardAutoRefundTaskPageDto dto) {
- List scopedMchIds = listScopedMerchantIds();
- if (scopedMchIds.isEmpty()) {
- Page empty = new Page<>(page.getCurrent(), page.getSize(), 0L);
- empty.setRecords(List.of());
- return R.ok(empty);
- }
- dto.setMchIds(scopedMchIds);
+ public R> expiredCandidates(@ParameterObject Page page, @ParameterObject HardAutoRefundTaskPageDto dto) {
+ validateMerchantScope(dto.getMchId());
+ // 详情接口只能查询一个已校验商户,禁止客户端利用内部批量字段扩大查询范围。
+ dto.setMchIds(null);
return R.ok(taskService.pageExpiredCandidates(page, dto));
}
- /** 管理端确认后逐单执行过期预约整单退款。 */
+ /**
+ * 按商户当前配置立即重扫历史过期预约订单;开关关闭时只同步暂停任务,开关开启时执行已到期任务。
+ *
+ * @param mchId 商户详情页传入的精确商户 ID
+ * @return 本轮扫描、任务同步和退款执行数量
+ */
+ @PostMapping("/expired-rescan")
+ @Operation(summary = "立即重新扫描过期预约退款订单")
+ @Idempotent(expireTime = 5, info = "正在重新扫描,请勿重复操作")
+ @PreAuthorize("@pms.hasPermission('hard_hardAfterSale_adminRefund')")
+ public R expiredRescan(@RequestParam Long mchId) {
+ validateMerchantScope(mchId);
+ return R.ok(taskService.rescanExpiredReserveOrders(mchId, SecurityUtils.getUser().getId()));
+ }
+
+ /**
+ * 管理端确认后逐单执行过期预约整单退款。
+ */
@PostMapping("/{id}/expired-refund")
@Operation(summary = "过期预约订单退款")
@Idempotent(expireTime = 5, info = "正在退款,请勿重复操作")
@@ -168,6 +185,26 @@ public class HardAutoRefundTaskController {
}
}
+ /**
+ * 校验单个商户属于当前管理用户数据范围,供商户详情页的精确查询使用。
+ *
+ * @param mchId 详情页传入的商户 ID
+ */
+ private void validateMerchantScope(Long mchId) {
+ if (mchId == null) {
+ throw new RuntimeException("商户不能为空");
+ }
+ MchManageDto query = MchManageDto.builder()
+ .ids(List.of(mchId))
+ .typesList(List.of(MerchantTypeEnum.CANTEEN.getType()))
+ .build();
+ R> response = remoteMchService.scopeList(query);
+ if (response == null || !response.isOk() || response.getData() == null
+ || response.getData().stream().noneMatch(item -> item != null && Objects.equals(item.getId(), mchId))) {
+ throw new RuntimeException("无权访问当前商户");
+ }
+ }
+
/**
* 查询当前管理用户可访问的食堂商户 ID;远程异常拒绝请求,不能降级为全量任务。
*
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardCanteenApptTotalController.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardCanteenApptTotalController.java
index 26048912..b89696d7 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardCanteenApptTotalController.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardCanteenApptTotalController.java
@@ -8,8 +8,6 @@ import com.bsd.pjcloud.common.core.constant.enums.MerchantTypeEnum;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.excel.annotation.ResponseExcel;
import com.bsd.pjcloud.common.log.annotation.SysLog;
-import com.bsd.pjcloud.common.security.service.PjcloudUser;
-import com.bsd.pjcloud.common.security.util.SecurityUtils;
import com.bsd.pjcloud.hard.api.constant.order.HardOrderTypeEnum;
import com.bsd.pjcloud.hard.api.dto.order.HardCanteenUserApptQueryDto;
import com.bsd.pjcloud.hard.api.dto.order.HardOrderDto;
@@ -18,11 +16,8 @@ import com.bsd.pjcloud.hard.api.vo.order.HardCanteenUserApptOrderVo;
import com.bsd.pjcloud.hard.api.vo.order.HardCanteenUserApptVo;
import com.bsd.pjcloud.hard.api.vo.order.HardCanteenUserApptStatisticsVo;
import com.bsd.pjcloud.hard.api.vo.order.excel.HardCanteenUserApptExcelVo;
-import com.bsd.pjcloud.hard.api.vo.order.excel.HardCanteenProductApptExcelVo;
-import com.bsd.pjcloud.hard.excel.HardCanteenProductApptExportMergeHandler;
import com.bsd.pjcloud.hard.service.HardCanteenApptTotalService;
import com.bsd.pjcloud.hard.service.HardCanteenApptTotalExportService;
-import com.bsd.pjcloud.hard.service.HardCanteenTotalService;
import com.bsd.pjcloud.wallet.api.dto.MchManageDto;
import com.bsd.pjcloud.wallet.api.feign.RemoteMchService;
import com.bsd.pjcloud.wallet.api.vo.MchManageVo;
@@ -55,9 +50,6 @@ import java.util.List;
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
public class HardCanteenApptTotalController {
- /** 无可见食堂时使用的不可命中商户主键,避免空集合被 SQL 当作“不筛选”而造成越权导出。 */
- private static final Long NO_VISIBLE_CANTEEN_ID = Long.MIN_VALUE;
-
/**
* 食堂预约统计服务,负责菜品和用户维度的预约聚合。
*/
@@ -68,11 +60,6 @@ public class HardCanteenApptTotalController {
*/
private final HardCanteenApptTotalExportService canteenApptTotalExportService;
- /**
- * 通用食堂统计服务,复用部门聚合以及商户、部门信息补全能力。
- */
- private final HardCanteenTotalService canteenTotalService;
-
/**
* 商户远程服务,用于限定 App 食堂范围。
*/
@@ -106,47 +93,6 @@ public class HardCanteenApptTotalController {
/* ************************************* end app end ************************************* */
- /**
- * 按预约使用日期统计食堂预约部门。
- *
- * @param dto 预约统计筛选条件,其中 {@code apptUseDateTimeList} 表示预约使用日期范围
- * @return 按食堂聚合并包含部门、用餐时间段明细的预约统计结果
- */
- @Operation(summary = "食堂预约部门统计", description = "按预约使用日期统计食堂预约部门")
- @ApiCryptoAes
- @PostMapping("/list-by-dept")
- public R> listByDept(@RequestBody HardOrderDto dto) {
- // 预约统计必须以预约使用日期为唯一时间口径,避免调用方残留支付时间条件导致交叉筛选。
- dto.setPayTimeList(null);
- dto.setOrderType(HardOrderTypeEnum.RESERVE.getType());
- dto.setTotalIsFindChildren(true);
- dto.setApptIdOrApptRuleIdNotNull(true);
- PjcloudUser user = SecurityUtils.getUser();
- return R.ok(canteenTotalService.listByDept(dto, user, null));
- }
-
- /**
- * 分页查询后台食堂菜品预约统计。
- *
- * @param page 分页参数
- * @param dto 预约订单筛选条件
- * @return 菜品预约统计分页
- */
- @Operation(summary = "食堂菜品预约统计", description = "食堂菜品预约统计")
- @PostMapping("/proudct-reserv-total")
- public R> productReservTotal(@ParameterObject Page page, @RequestBody HardOrderDto dto) {
- // 查询商户
- MchManageDto mchManageDto = new MchManageDto();
- mchManageDto.setTypesList(List.of(MerchantTypeEnum.CANTEEN.getType()));
- List mchList = remoteMchService.scopeList(mchManageDto).getData();
- if (ObjectUtil.isNotEmpty(mchList) && ObjectUtil.isEmpty(dto.getMchIds())) {
- dto.setMchIds(mchList.stream().map(MchManageVo::getId).toList());
- }
- dto.setOrderType(HardOrderTypeEnum.RESERVE.getType());
- dto.setApptIdOrApptRuleIdNotNull(true);
- return R.ok(canteenApptTotalService.productReservTotal(page, dto, null));
- }
-
/**
* 分页查询用户在指定用餐日期范围内每天的预约情况。
*
@@ -228,67 +174,4 @@ public class HardCanteenApptTotalController {
return canteenApptTotalExportService.userReservationExport(query);
}
- /**
- * 提交菜品预约统计异步导出任务。
- *
- * @param query 菜品预约日期、餐段、食堂和部门筛选条件
- * @return 供前端轮询的初始任务进度
- */
- @Operation(summary = "菜品预约统计异步导出", description = "按日期分批生成菜品预约三级统计 Excel")
- @SysLog("菜品预约统计异步导出")
- @PreAuthorize("@pms.hasPermission('hard_canteenProductApptTotal_export')")
- @PostMapping("/product-reserv-export-task")
- public R submitProductReservationExport(@RequestBody HardOrderDto query) {
- applyCanteenMerchantScope(query);
- return R.ok(canteenApptTotalExportService.submitProductReservationExport(query), "菜品预约导出任务已提交");
- }
-
- /**
- * 同步菜品预约统计导出接口,保留给已有调用方;管理端页面使用异步任务接口。
- *
- * @param query 菜品预约统计筛选条件
- * @return 按日期餐段、商户部门和商品三级组织的自定义 Excel 行
- */
- @Operation(summary = "菜品预约统计导出", description = "导出日期餐段、商户部门和商品三级菜品预约统计")
- @SysLog("菜品预约统计导出")
- @PreAuthorize("@pms.hasPermission('hard_canteenProductApptTotal_export')")
- @ResponseExcel(name = "菜品预约统计", inMemory = true, writeHandler = HardCanteenProductApptExportMergeHandler.class)
- @PostMapping("/product-reserv-export")
- public List productReservationExport(@RequestBody HardOrderDto query) {
- applyCanteenMerchantScope(query);
- return canteenApptTotalExportService.productReservationExport(query);
- }
-
- /**
- * 为后台菜品预约导出补充当前操作者可见的食堂范围。
- *
- * 分页查询接口已有相同约束;导出任务在异步线程执行前必须冻结这份范围,避免未选择食堂时
- * 直接按空 {@code mchIds} 查询到当前权限之外的商户数据。
- *
- * @param query 菜品预约导出的请求条件;为空时保留给导出服务做参数校验
- */
- private void applyCanteenMerchantScope(HardOrderDto query) {
- if (query == null) {
- return;
- }
- MchManageDto mchManageDto = new MchManageDto();
- mchManageDto.setTypesList(List.of(MerchantTypeEnum.CANTEEN.getType()));
- R> scopeResponse = remoteMchService.scopeList(mchManageDto);
- List mchList = scopeResponse == null ? null : scopeResponse.getData();
- List visibleMerchantIds = ObjectUtil.isEmpty(mchList) ? List.of()
- : mchList.stream().map(MchManageVo::getId).filter(ObjectUtil::isNotEmpty).distinct().toList();
- if (ObjectUtil.isEmpty(visibleMerchantIds)) {
- query.setMchIds(List.of(NO_VISIBLE_CANTEEN_ID));
- return;
- }
- if (ObjectUtil.isEmpty(query.getMchIds())) {
- query.setMchIds(visibleMerchantIds);
- return;
- }
- // 即使调用方手工携带 mchIds,也只能导出当前操作者实际可见的食堂。
- List allowedMerchantIds = query.getMchIds().stream().filter(visibleMerchantIds::contains).toList();
- query.setMchIds(ObjectUtil.isEmpty(allowedMerchantIds)
- ? List.of(NO_VISIBLE_CANTEEN_ID) : allowedMerchantIds);
- }
-
}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardCanteenDeptTotalController.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardCanteenDeptTotalController.java
index 0b509485..1841b6cc 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardCanteenDeptTotalController.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardCanteenDeptTotalController.java
@@ -2,6 +2,7 @@ package com.bsd.pjcloud.hard.controller.order;
import com.bsd.pjcloud.common.api.encrypt.annotation.crypto.ApiCryptoAes;
+import com.bsd.pjcloud.common.asynctask.vo.AsyncTaskProgressVO;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.data.datascope.DataScope;
import com.bsd.pjcloud.common.excel.annotation.ResponseExcel;
@@ -10,7 +11,8 @@ import com.bsd.pjcloud.common.security.service.PjcloudUser;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
import com.bsd.pjcloud.hard.api.dto.order.HardOrderDto;
import com.bsd.pjcloud.hard.api.vo.order.HardCanteenProductTotalVo;
-import com.bsd.pjcloud.hard.api.vo.order.excel.HardCanteenDeptTotalExcelVo;
+import com.bsd.pjcloud.hard.api.vo.order.excel.HardCanteenProductApptExcelVo;
+import com.bsd.pjcloud.hard.excel.HardCanteenProductApptExportMergeHandler;
import com.bsd.pjcloud.hard.service.HardCanteenTotalService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@@ -58,11 +60,25 @@ public class HardCanteenDeptTotalController {
@Operation(summary = "食堂菜品部门统计导出", description = "按日期、食堂、来源、部门和餐段导出销售统计")
@SysLog("食堂菜品部门统计导出")
@PreAuthorize("@pms.hasPermission('hard_canteenDeptTotal_export')")
- @ResponseExcel(name = "食堂菜品部门统计")
+ @ResponseExcel(name = "食堂菜品部门统计", inMemory = true,
+ writeHandler = HardCanteenProductApptExportMergeHandler.class)
@PostMapping("/export-dept")
- public List exportDept(@RequestBody HardOrderDto dto) {
- PjcloudUser user = SecurityUtils.getUser();
- return canteenTotalService.exportDept(dto, user, null);
+ public List exportDept(@RequestBody HardOrderDto dto) {
+ return canteenTotalService.exportSales(dto, null);
+ }
+
+ /**
+ * 提交食堂部门销售统计异步导出任务。
+ *
+ * @param dto 来源统计日期、来源、食堂、餐段和部门筛选
+ * @return 可供前端轮询的初始任务进度,成功终态包含文件下载地址
+ */
+ @Operation(summary = "食堂菜品部门统计异步导出", description = "按自然日分批查询并流式生成部门销售统计 Excel")
+ @SysLog("食堂菜品部门统计异步导出")
+ @PreAuthorize("@pms.hasPermission('hard_canteenDeptTotal_export')")
+ @PostMapping("/export-dept-task")
+ public R submitDepartmentSalesExport(@RequestBody HardOrderDto dto) {
+ return R.ok(canteenTotalService.submitDepartmentSalesExport(dto), "食堂菜品部门统计导出任务已提交");
}
}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardCanteenTotalController.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardCanteenTotalController.java
index d0ae0980..ea59db71 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardCanteenTotalController.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardCanteenTotalController.java
@@ -1,6 +1,7 @@
package com.bsd.pjcloud.hard.controller.order;
import com.bsd.pjcloud.common.api.encrypt.annotation.crypto.ApiCryptoAes;
+import com.bsd.pjcloud.common.asynctask.vo.AsyncTaskProgressVO;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -23,7 +24,8 @@ import com.bsd.pjcloud.hard.api.vo.order.HardCarVo;
import com.bsd.pjcloud.hard.api.vo.order.app.HardAppProductByCanteenVo;
import com.bsd.pjcloud.hard.api.vo.order.app.HardAppProductByClassifyVo;
import com.bsd.pjcloud.hard.api.vo.order.app.HardAppProductLikeTopVo;
-import com.bsd.pjcloud.hard.api.vo.order.excel.HardCanteenProductTotalExcelVo;
+import com.bsd.pjcloud.hard.api.vo.order.excel.HardCanteenProductApptExcelVo;
+import com.bsd.pjcloud.hard.excel.HardCanteenProductApptExportMergeHandler;
import com.bsd.pjcloud.hard.service.HardCanteenTotalService;
import com.bsd.pjcloud.wallet.api.dto.MchManageDto;
import com.bsd.pjcloud.wallet.api.feign.RemoteMchService;
@@ -113,6 +115,67 @@ public class HardCanteenTotalController {
return R.ok(canteenTotalService.proudctMchQuery(page, dto));
}
+ /**
+ * 按销售发生时间分页查询 App 食堂综合销售统计。
+ *
+ * 线下购买按支付时间统计,预约订单按核销时间统计;返回结构与 PC 菜品销售统计一致,
+ * 并且只允许查询当前商户管理员可见的食堂。
+ *
+ * @param page 按自然日分页的分页参数
+ * @param dto 统计日期、来源和餐段筛选条件
+ * @return 日期、餐段和来源多维销售统计分页
+ */
+ @ApiCryptoAes
+ @Operation(summary = "App食堂综合销售统计", description = "线下按支付时间、预约按核销时间查询日期餐段来源统计")
+ @PostMapping("/app/proudct-total")
+ public R> appProductTotal(@ParameterObject Page page,
+ @RequestBody HardOrderDto dto) {
+ if (!applyAppCanteenMerchantScope(dto)) {
+ return R.ok(new Page());
+ }
+ return R.ok(canteenTotalService.productTotal(page, dto, null));
+ }
+
+ /**
+ * 查询 App 食堂综合销售统计中指定日期、餐段和来源的部门明细。
+ *
+ * @param page 保留的分页参数,与 PC 同名接口保持调用形式一致
+ * @param dto 统计日期、来源、餐段和部门筛选条件
+ * @return 当前用户可见食堂范围内的部门销售汇总
+ */
+ @ApiCryptoAes
+ @Operation(summary = "App食堂综合销售部门明细", description = "查询日期餐段来源对应的部门销售统计")
+ @PostMapping("/app/proudct-total-by-dept")
+ public R> appProductTotalByDept(@ParameterObject Page page,
+ @RequestBody HardOrderDto dto) {
+ if (!applyAppCanteenMerchantScope(dto)) {
+ return R.ok(List.of());
+ }
+ return R.ok(canteenTotalService.productTotalByDept(page, dto, null));
+ }
+
+ /**
+ * 将 App 统计查询的食堂条件收敛到当前商户管理员的数据范围。
+ *
+ * @param dto 待补全食堂范围的统计请求,已有食堂条件会与可见范围取交集
+ * @return 是否仍存在可查询的食堂;false 表示调用方应直接返回空数据
+ */
+ private boolean applyAppCanteenMerchantScope(HardOrderDto dto) {
+ PjcloudUser user = SecurityUtils.getUser();
+ List allowedMchIds = remoteMchService
+ .getSelfMchIdList(user.getId(), List.of(MerchantTypeEnum.CANTEEN)).getData();
+ if (ObjectUtil.isEmpty(allowedMchIds)) {
+ dto.setMchIds(List.of());
+ return false;
+ }
+ List requestedMchIds = dto.getMchIds();
+ List scopedMchIds = ObjectUtil.isEmpty(requestedMchIds)
+ ? allowedMchIds
+ : requestedMchIds.stream().filter(allowedMchIds::contains).distinct().toList();
+ dto.setMchIds(scopedMchIds);
+ return ObjectUtil.isNotEmpty(scopedMchIds);
+ }
+
/* ************************************* start app start ************************************* */
@Operation(summary = "根据部门 食堂菜品购买统计", description = "根据部门 食堂菜品购买统计")
@PostMapping("/proudct-total-by-dept")
@@ -150,17 +213,28 @@ public class HardCanteenTotalController {
*/
@Operation(summary = "食堂菜品销售统计导出", description = "按日期、餐段和来源导出食堂菜品销售统计")
@SysLog("食堂菜品销售统计导出")
- @PreAuthorize("@pms.hasPermission('hard_canteenProductTotal_export')")
- @ResponseExcel(name = "食堂菜品销售统计")
+ @PreAuthorize("@pms.hasPermission('hard_canteenProductTotal_export') or "
+ + "@pms.hasPermission('hard_canteenProductApptTotal_export')")
+ @ResponseExcel(name = "食堂菜品销售统计", inMemory = true,
+ writeHandler = HardCanteenProductApptExportMergeHandler.class)
@PostMapping("/export-product")
- public List exportProduct(@RequestBody HardOrderDto dto) {
- MchManageDto mchManageDto = new MchManageDto();
- mchManageDto.setTypesList(List.of(MerchantTypeEnum.CANTEEN.getType()));
- List mchList = remoteMchService.scopeList(mchManageDto).getData();
- if (ObjectUtil.isNotEmpty(mchList) && ObjectUtil.isEmpty(dto.getMchIds())) {
- dto.setMchIds(mchList.stream().map(MchManageVo::getId).toList());
- }
- return canteenTotalService.exportProduct(dto, null);
+ public List exportProduct(@RequestBody HardOrderDto dto) {
+ return canteenTotalService.exportSales(dto, null);
+ }
+
+ /**
+ * 提交食堂菜品销售统计异步导出任务。
+ *
+ * @param dto 来源统计日期、来源、食堂、餐段和部门筛选
+ * @return 可供前端轮询的初始任务进度,成功终态包含文件下载地址
+ */
+ @Operation(summary = "食堂菜品销售统计异步导出", description = "按自然日分批查询并流式生成销售统计 Excel")
+ @SysLog("食堂菜品销售统计异步导出")
+ @PreAuthorize("@pms.hasPermission('hard_canteenProductTotal_export') or "
+ + "@pms.hasPermission('hard_canteenProductApptTotal_export')")
+ @PostMapping("/export-product-task")
+ public R submitProductSalesExport(@RequestBody HardOrderDto dto) {
+ return R.ok(canteenTotalService.submitProductSalesExport(dto), "食堂菜品销售统计导出任务已提交");
}
/**
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderController.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderController.java
index b9936a98..c770ec0a 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderController.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderController.java
@@ -2,30 +2,23 @@ package com.bsd.pjcloud.hard.controller.order;
import com.bsd.pjcloud.common.api.encrypt.annotation.crypto.ApiCryptoAes;
-import cn.hutool.core.date.DatePattern;
-import cn.hutool.core.date.LocalDateTimeUtil;
-import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.util.ObjectUtil;
-import com.alibaba.excel.EasyExcel;
-import com.alibaba.excel.ExcelWriter;
-import com.alibaba.excel.write.metadata.WriteSheet;
-import com.alibaba.excel.write.metadata.fill.FillConfig;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.bsd.pjcloud.admin.api.dto.UserInfo;
import com.bsd.pjcloud.admin.api.feign.RemoteUserService;
+import com.bsd.pjcloud.common.asynctask.vo.AsyncTaskProgressVO;
import com.bsd.pjcloud.common.core.constant.enums.MerchantTypeEnum;
-import com.bsd.pjcloud.common.core.util.BeanUtils;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.data.datascope.DataScope;
import com.bsd.pjcloud.common.data.datascope.DataScopeFuncEnum;
import com.bsd.pjcloud.common.idempotent.annotation.Idempotent;
+import com.bsd.pjcloud.common.log.annotation.SysLog;
import com.bsd.pjcloud.common.security.annotation.Inner;
import com.bsd.pjcloud.common.security.service.PjcloudUser;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
import com.bsd.pjcloud.hard.api.constant.HardTypeEnum;
-import com.bsd.pjcloud.hard.api.constant.order.HardHourTypeEnum;
import com.bsd.pjcloud.hard.api.constant.order.HardOrderStatusEnum;
import com.bsd.pjcloud.hard.api.constant.order.HardOrderTypeEnum;
import com.bsd.pjcloud.hard.api.constant.order.HardPayTypeEnum;
@@ -53,7 +46,6 @@ import com.bsd.pjcloud.hard.api.vo.order.app.HardAppCreateReserveOrdersVo;
import com.bsd.pjcloud.hard.api.vo.order.app.HardAppScanPayOrderVo;
import com.bsd.pjcloud.hard.api.vo.order.app.HardAppCreateShopOrderVo;
import com.bsd.pjcloud.hard.api.vo.order.app.HardTakeOutDeliveryTimeOptionVO;
-import com.bsd.pjcloud.hard.api.vo.order.excel.HardOrderExcelVo;
import com.bsd.pjcloud.hard.mapper.order.HardOrderMapper;
import com.bsd.pjcloud.hard.api.vo.order.HardOnlineCanteenStatsVo;
import com.bsd.pjcloud.hard.service.*;
@@ -74,14 +66,8 @@ import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.format.annotation.DateTimeFormat;
-import java.io.IOException;
import java.time.LocalDate;
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.time.LocalDateTime;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
/**
* 订单表
@@ -100,6 +86,9 @@ public class HardOrderController {
private final HardOrderSyncService hardOrderSyncService;
private final HardCanteenTotalService canteenTotalService;
+ /** 订单导出服务,负责异步任务、分批模板写入和文件中心上传。 */
+ private final HardOrderExportService hardOrderExportService;
+
private final HardOrderMapper hardOrderMapper;
private final RemoteMchService mchService;
@@ -214,47 +203,67 @@ public class HardOrderController {
return R.ok(hardwareOrderService.pagejoin(page, dto));
}
- @Operation(summary = "app查询食堂订餐的所有商户统计", description = "app查询食堂订餐的所有商户统计")
+ /**
+ * 按预约用餐时间统计当前商户管理员可见食堂的预约订单数量。
+ *
+ * @param dto 预约订单状态和预约用餐时间范围;创建、支付等其他时间条件会被忽略
+ * @return 各食堂在预约时间范围内的订单统计
+ */
+ @Operation(summary = "app查询食堂订餐的所有商户统计", description = "按预约用餐时间查询食堂订餐商户统计")
@GetMapping("/app/caneen-rserve-mch-total")
public R appCaneenRserveMchTotal(@ParameterObject HardOrderDto dto) {
DataScope dataScope = null;
-
PjcloudUser user = SecurityUtils.getUser();
+ normalizeAppReservationUseDateQuery(dto);
dto.setOrderType(HardOrderTypeEnum.RESERVE.getType());
dto.setTotalIsFindChildren(false);
dto.setApptIdOrApptRuleIdNotNull(true);
return R.ok(canteenTotalService.listByDept(dto, user, dataScope));
}
- @Operation(summary = "app查询食堂订餐订单列表", description = "app查询食堂订餐订单列表")
+ /**
+ * 按预约用餐时间分页查询当前商户管理员可核销的食堂预约订单。
+ *
+ * @param page 分页参数
+ * @param dto 订单状态、食堂和预约用餐时间范围;创建、支付等其他时间条件会被忽略
+ * @return 按预约用餐日期倒序排列的预约订单分页
+ */
+ @Operation(summary = "app查询食堂订餐订单列表", description = "按预约用餐时间查询食堂订餐订单列表")
@GetMapping("/app/caneen-rserve-page-join")
public R appCaneenRservePageJoin(@ParameterObject Page page, @ParameterObject HardOrderDto dto) {
+ normalizeAppReservationUseDateQuery(dto);
dto.setIsFindCarList(true);
dto.setIsFindApptList(true);
dto.setOrderType(HardOrderTypeEnum.RESERVE.getType());
dto.setOrderStatusCodes(List.of(HardOrderStatusEnum.PAY.getType(), HardOrderStatusEnum.CANCEL.getType()));
dto.setApptIdOrApptRuleIdNotNull(true);
-
DataScope dataScope = null;
-
// 查询商户
- MchManageDto mchManageDto = new MchManageDto();
- mchManageDto.setTypesList(List.of(MerchantTypeEnum.CANTEEN.getType()));
- List mchList = mchService.scopeList(mchManageDto).getData();
- if (ObjectUtil.isEmpty(mchList)) {
+ List mchIdList = mchService.getSelfMchIdList(SecurityUtils.getUser().getId(), List.of(MerchantTypeEnum.CANTEEN)).getData();
+ if (ObjectUtil.isEmpty(mchIdList)) {
return R.ok(new Page());
}
- List allowedMchIds = mchList.stream().map(MchManageVo::getId).toList();
- dto.setMchIds(ObjectUtil.isEmpty(dto.getMchIds())
- ? allowedMchIds
- : dto.getMchIds().stream().filter(allowedMchIds::contains).toList());
+ dto.setMchIds(ObjectUtil.isEmpty(dto.getMchIds()) ? mchIdList : dto.getMchIds().stream().filter(mchIdList::contains).toList());
if (ObjectUtil.isEmpty(dto.getMchIds())) {
return R.ok(new Page());
}
-
return R.ok(hardwareOrderService.scopePageList(page, dto, dataScope));
}
+ /**
+ * 统一商户核销页面的预约时间口径,避免历史创建时间或支付时间参数与预约日期形成交叉筛选。
+ *
+ * @param dto 当前两个核销查询接口接收的订单筛选条件,方法会原地清除非预约日期条件
+ */
+ private void normalizeAppReservationUseDateQuery(HardOrderDto dto) {
+ dto.setCreateTimeList(null);
+ dto.setCreateTimeStr(null);
+ dto.setPayTimeList(null);
+ dto.setUpdateTimeList(null);
+ dto.setVerificationTimeList(null);
+ dto.setStatisticsDateTimeList(null);
+ }
+
@Operation(summary = "app查询食堂订单列表", description = "app查询订单列表")
@ApiCryptoAes
@GetMapping("/app/caneen-page-join")
@@ -652,77 +661,30 @@ public class HardOrderController {
return R.ok(hardwareOrderService.shipment(dto));
}
+ /**
+ * 提交订单异步分批导出任务。
+ *
+ * @param dto 订单页面当前筛选条件;orderIds 非空时仅导出勾选订单
+ * @return 可供前端轮询的初始任务进度,成功终态包含文件下载地址
+ */
+ @Operation(summary = "订单记录异步导出", description = "按订单筛选条件分批查询并流式生成 Excel")
+ @SysLog("订单记录异步导出")
+ @PostMapping("/export-task")
+ @PreAuthorize("@pms.hasPermission('hard_hardwareOrder_export')")
+ public R submitExport(@RequestBody HardOrderDto dto) {
+ return R.ok(hardOrderExportService.submitExport(dto), "订单导出任务已提交");
+ }
+
+ /**
+ * 兼容仍使用旧下载方式的订单页面,同步响应内部也由独立导出服务分批生成。
+ *
+ * @param response 当前 HTTP 下载响应
+ * @param dto 订单页面当前筛选条件
+ */
+ @Operation(summary = "订单记录导出", description = "兼容旧页面的同步订单导出")
@GetMapping("/export")
@PreAuthorize("@pms.hasPermission('hard_hardwareOrder_export')")
- public void export(HttpServletResponse response, HardOrderDto dto) throws IOException {
- DataScope dataScope = DataScope.of().func(DataScopeFuncEnum.ALL);
- dataScope.setScopeDeptName("merchant_dept_id");
- PjcloudUser user = SecurityUtils.getUser();
- CurrentUserHardTypeInfoDto hardTypeInfo = hardManagerService.getCurrentUserHardTypeInfo(user);
- dto.setMchTypeCodes(hardTypeInfo.getHardTypeList());
- // 查询商户和下面的子商户
- List mchDtoIds = dto.getMchIds();
- if (ObjectUtil.isNotEmpty(mchDtoIds) && mchDtoIds.size() == 1) {
- Long mchId = mchDtoIds.get(0);
- List mchList = mchService.getByParentId(mchId).getData();
- if (ObjectUtil.isNotEmpty(mchList)) {
- List mchIds = mchList.stream().map(MchManageVo::getId).toList();
- mchDtoIds.addAll(mchIds);
- dto.setMchIds(mchDtoIds);
- }
- }
- List list = hardwareOrderService.scopeList(dto, DataScope.of());
- List excelList = BeanUtils.toBean(list, HardOrderExcelVo.class);
- try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).withTemplate(ResourceUtil.getStream("file/订单交易汇总.xlsx")).build()) {
- WriteSheet writeSheet = EasyExcel.writerSheet().build();
- FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
- Map map = new HashMap();
- if (ObjectUtil.isNotEmpty(dto.getPayTimeList())) {
- map.put("timeSpan", " 到 ");
- map.put("startTime", LocalDateTimeUtil.format(dto.getPayTimeList()[0], DatePattern.NORM_DATETIME_PATTERN));
- map.put("endTime", LocalDateTimeUtil.format(dto.getPayTimeList()[1], DatePattern.NORM_DATETIME_PATTERN));
- }
- if (ObjectUtil.isNotEmpty(excelList)) {
- BigDecimal totalPriceSum = BigDecimal.ZERO;
- BigDecimal extraPriceSum = BigDecimal.ZERO;
- BigDecimal payPriceSum = BigDecimal.ZERO;
- Long lineNumber = 1L;
- for (HardOrderExcelVo v : excelList) {
- v.setLineNumber(lineNumber);
- lineNumber++;
- if (ObjectUtil.isNotEmpty(v.getTotalPrice())) {
- totalPriceSum = totalPriceSum.add(BigDecimal.valueOf(v.getTotalPrice()));
- v.setTotalPrice(BigDecimal.valueOf(v.getTotalPrice()).divide(BigDecimal.valueOf(100)).setScale(2, RoundingMode.HALF_UP).doubleValue());
- }
- if (ObjectUtil.isNotEmpty(v.getExtraPrice())) {
- extraPriceSum = extraPriceSum.add(BigDecimal.valueOf(v.getExtraPrice()));
- v.setExtraPrice(BigDecimal.valueOf(v.getExtraPrice()).divide(BigDecimal.valueOf(100)).setScale(2, RoundingMode.HALF_UP).doubleValue());
- }
- if (ObjectUtil.isNotEmpty(v.getPayPrice())) {
- payPriceSum = payPriceSum.add(BigDecimal.valueOf(v.getPayPrice()));
- v.setPayPrice(BigDecimal.valueOf(v.getPayPrice()).divide(BigDecimal.valueOf(100)).setScale(2, RoundingMode.HALF_UP).doubleValue());
- }
- if (ObjectUtil.isNotEmpty(v.getHourType())) {
- HardHourTypeEnum hardHourTypeEnum = HardHourTypeEnum.valueOfByType(v.getHourType());
- if (ObjectUtil.isNotEmpty(hardHourTypeEnum)) {
- v.setHourType(hardHourTypeEnum.getDescription());
- } else {
- v.setHourType("");
- }
- }
- if (ObjectUtil.isNotEmpty(v.getPayType())) {
- HardPayTypeEnum hardPayTypeEnum = HardPayTypeEnum.valueOfByType(v.getPayType());
- if (ObjectUtil.isNotEmpty(hardPayTypeEnum)) {
- v.setPayType(hardPayTypeEnum.getDescription());
- }
- }
- }
- map.put("totalPriceSum", totalPriceSum.divide(BigDecimal.valueOf(100)).setScale(2, RoundingMode.HALF_UP).toString());
- map.put("extraPriceSum", extraPriceSum.divide(BigDecimal.valueOf(100)).setScale(2, RoundingMode.HALF_UP).toString());
- map.put("payPriceSum", payPriceSum.divide(BigDecimal.valueOf(100)).setScale(2, RoundingMode.HALF_UP).toString());
- }
- excelWriter.fill(map, writeSheet);
- excelWriter.fill(excelList, fillConfig, writeSheet);
- }
+ public void export(HttpServletResponse response, HardOrderDto dto) {
+ hardOrderExportService.export(response, dto);
}
}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/excel/HardCanteenProductApptExportMergeHandler.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/excel/HardCanteenProductApptExportMergeHandler.java
index fa9811af..7e2d5497 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/excel/HardCanteenProductApptExportMergeHandler.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/excel/HardCanteenProductApptExportMergeHandler.java
@@ -47,7 +47,7 @@ public class HardCanteenProductApptExportMergeHandler implements RowWriteHandler
/** 日期分组边框线型,参考模板使用中等线宽突出每天的数据边界。 */
private static final BorderStyle DATE_GROUP_BORDER_STYLE = BorderStyle.MEDIUM;
- /** 日期分组边框颜色,按预约日期顺序交替使用蓝色和橙色。 */
+ /** 日期分组边框颜色,按统计日期顺序交替使用蓝色和橙色。 */
private static final byte[][] DATE_GROUP_BORDER_COLORS = {
{0x5B, (byte) 0x9B, (byte) 0xD5},
{(byte) 0xED, 0x7D, 0x31}
@@ -75,13 +75,13 @@ public class HardCanteenProductApptExportMergeHandler implements RowWriteHandler
/** 最近写出的数据行号,用于工作簿结束时收尾最后一组。 */
private int lastDataRow = -1;
- /** 当前预约日期分组序号,同一天的所有餐段共用一个序号和边框颜色。 */
+ /** 当前统计日期分组序号,同一天的所有餐段和来源块共用一个序号和边框颜色。 */
private int dateGroupIndex = -1;
- /** 当前预约日期的单元格值标识,用于判断新餐段是否仍属于同一天。 */
+ /** 当前统计日期的单元格值标识,用于判断新餐段或来源块是否仍属于同一天。 */
private String currentDateKey;
- /** 当前导出工作表;本处理器只注册到菜品预约导出的单个工作表。 */
+ /** 当前导出工作表;本处理器只注册到菜品销售统计导出的单个工作表。 */
private Sheet currentSheet;
/**
@@ -203,14 +203,14 @@ public class HardCanteenProductApptExportMergeHandler implements RowWriteHandler
}
/**
- * 为当前预约日期设置整组外框:当天首个餐段首行加顶部边框,当天所有行加左右边框,末行加底部边框。
+ * 为当前统计日期设置整组外框:当天首个来源块首行加顶部边框,当天所有行加左右边框,末行加底部边框。
*
* 流式工作簿会刷出较早的数据行,分组结束时无法可靠回读整组行,因此左右边框必须随行写出;
* 新日期开始时,最近写出的上一天末行仍在流式窗口内,可补齐上一天的本色底边。
*
* @param row 当前数据行
* @param holder 当前工作簿持有者,用于创建彩色边框样式
- * @param groupIndex 当前预约日期分组序号,未识别到日期时为负数
+ * @param groupIndex 当前统计日期分组序号,未识别到日期时为负数
* @param topBorder 是否为当天首行并设置顶部边框
* @param bottomBorder 是否为当天末行并设置底部边框
*/
@@ -284,10 +284,10 @@ public class HardCanteenProductApptExportMergeHandler implements RowWriteHandler
}
/**
- * 获取日期汇总单元格的稳定值标识;同一预约日期的不同餐段会得到相同标识。
+ * 获取日期汇总单元格的稳定值标识;同一统计日期的不同餐段和来源块会得到相同标识。
*
* @param row 当前日期餐段汇总行
- * @param columnIndex 预约日期所在列索引
+ * @param columnIndex 统计日期所在列索引
* @return 用于判断日期是否变化的非空值标识
*/
private String getCellValueKey(Row row, int columnIndex) {
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/HardDoorFlowMapper.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/HardDoorFlowMapper.java
index 16a98893..8f918f78 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/HardDoorFlowMapper.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/HardDoorFlowMapper.java
@@ -20,6 +20,7 @@ public interface HardDoorFlowMapper extends PjcloudBaseMapper wrapper = new MPJLambdaQueryWrapper()
.selectAll(HardDoorFlowEntity.class)
.eq(ObjectUtil.isNotEmpty(dto.getUserId()), HardDoorFlowEntity::getUserId, dto.getUserId())
+ .in(ObjectUtil.isNotEmpty(dto.getUserIds()), HardDoorFlowEntity::getUserId, dto.getUserIds())
.eq(ObjectUtil.isNotEmpty(dto.getBizType()), HardDoorFlowEntity::getBizType, dto.getBizType())
.eq(ObjectUtil.isNotEmpty(dto.getHardDoorId()), HardDoorFlowEntity::getHardDoorId, dto.getHardDoorId())
.eq(ObjectUtil.isNotEmpty(dto.getCabinetNo()), HardDoorFlowEntity::getCabinetNo, dto.getCabinetNo())
@@ -35,4 +36,4 @@ public interface HardDoorFlowMapper extends PjcloudBaseMapper
Page scopePageList(Page page, @Param("dto") HardInvoiceDto dto, @Param("dataScope") DataScope dataScope);
-}
\ No newline at end of file
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/HardAutoRefundTaskMapper.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/HardAutoRefundTaskMapper.java
index 911c3d71..56e69579 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/HardAutoRefundTaskMapper.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/HardAutoRefundTaskMapper.java
@@ -5,10 +5,12 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper;
import com.bsd.pjcloud.hard.api.dto.order.HardAutoRefundTaskPageDto;
import com.bsd.pjcloud.hard.api.entity.order.HardAutoRefundTaskEntity;
+import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDateTime;
+import java.util.List;
/**
* 自动退款任务 Mapper,任务执行通过主键行锁串行化多实例竞争。
@@ -64,4 +66,12 @@ public interface HardAutoRefundTaskMapper extends PjcloudBaseMapper pageExpiredCandidates(Page page,
@Param("dto") HardAutoRefundTaskPageDto dto,
@Param("now") LocalDateTime now);
+
+ /**
+ * 查询指定商户仍处于已支付未核销状态且没有冲突售后的每日菜谱钱包订单,供立即重扫补建任务。
+ *
+ * @param mchId 精确商户 ID
+ * @return 按订单主键升序排列的可重扫订单
+ */
+ List listExpiredRescanOrders(@Param("mchId") Long mchId);
}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/HardCarMapper.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/HardCarMapper.java
index ef4e2303..9a1e5485 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/HardCarMapper.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/HardCarMapper.java
@@ -49,8 +49,8 @@ public interface HardCarMapper extends PjcloudBaseMapper {
/**
* 查询菜品多维统计行且不应用数据库分页。
*
- * 同一次查询通过 ROLLUP 返回当日合计、餐段合计和餐段来源三层数据,保证交易人数在每个层级独立去重;
- * 服务层按完整自然日分页,导出仅选择餐段来源层。
+ * 同一次查询通过统一来源 CTE 返回当日合计、来源合计、餐段合计和餐段来源四层数据,
+ * 保证交易人数在每个层级独立去重;服务层按完整自然日分页。
*
* @param dto 统计日期、来源、食堂、餐段和部门筛选
* @param dataScope 订单所属部门数据范围;为空时不附加范围条件
@@ -70,28 +70,15 @@ public interface HardCarMapper extends PjcloudBaseMapper {
Page productTotalByApptUseDate(Page page, @Param("dto") HardOrderDto dto, DataScope dataScope);
/**
- * 查询菜品销售三级导出的日期餐段汇总及来源拆分。
+ * 一次查询菜品销售三级导出的汇总、商户部门和商品聚合行。
+ *
+ * SQL 只构造一次来源 CTE,再按记录层级分别聚合,避免三个查询重复扫描订单、购物车和退款数据。
*
* @param dto 统计日期、来源、餐段、食堂和部门筛选条件
- * @return 每个日期餐段的跨来源合计及各来源统计,交易人数在当前层级独立去重
+ * @param dataScope 订单所属部门数据范围;为空时不附加范围条件
+ * @return 按层级、日期、餐段、来源、商户部门和商品稳定排序的三级聚合行
*/
- List listSalesExportSummary(@Param("dto") HardOrderDto dto);
-
- /**
- * 查询菜品销售三级导出的商户部门汇总及来源拆分。
- *
- * @param dto 统计日期、来源、餐段、食堂和部门筛选条件
- * @return 每个日期餐段下商户部门的跨来源合计及各来源统计
- */
- List listSalesExportMerchant(@Param("dto") HardOrderDto dto);
-
- /**
- * 查询菜品销售三级导出的商品汇总及来源拆分。
- *
- * @param dto 统计日期、来源、餐段、食堂和部门筛选条件
- * @return 每个商户部门块下商品的跨来源合计及各来源统计
- */
- List listSalesExportProduct(@Param("dto") HardOrderDto dto);
+ List listSalesExportRows(@Param("dto") HardOrderDto dto, DataScope dataScope);
Page proudctQuery(Page page, @Param("dto") HardCarDto dto);
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardApptReservationService.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardApptReservationService.java
index fa8a34ee..2f96b3b9 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardApptReservationService.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardApptReservationService.java
@@ -39,7 +39,7 @@ public interface HardApptReservationService {
* @param apptDate 实际用餐日期
* @param apptRuleId 周期预约规则 ID;动态模式为空
* @param eligibilityDeadline 当前已校验预约窗口的截止时间,使用服务器现有预约时间口径
- * @param usageEndDateTime 实际用餐餐段的结束时间;无法从有效餐段唯一解析时为空,禁止用于过期退款
+ * @param usageEndDateTime 实际用餐日的核销有效期结束时间,固定为该日 23:59:59,供订单固化后重扫使用
*/
record ReservationValidation(HardApptEntity appt, String hourType, LocalDate apptDate, Long apptRuleId,
LocalDateTime eligibilityDeadline, LocalDateTime usageEndDateTime) {
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardAutoRefundTaskService.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardAutoRefundTaskService.java
index 212fb749..3c5695af 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardAutoRefundTaskService.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardAutoRefundTaskService.java
@@ -9,6 +9,7 @@ import com.bsd.pjcloud.hard.api.entity.order.HardAutoRefundAttemptLogEntity;
import com.bsd.pjcloud.hard.api.entity.order.HardAutoRefundTaskEntity;
import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity;
import com.bsd.pjcloud.hard.api.vo.order.HardAutoRefundTaskVo;
+import com.bsd.pjcloud.hard.api.vo.order.HardExpiredRefundRescanResultVo;
import com.bsd.pjcloud.hard.api.vo.order.app.HardAppAutoRefundTaskVo;
import java.util.List;
@@ -63,6 +64,15 @@ public interface HardAutoRefundTaskService extends IService pageExpiredCandidates(Page page, HardAutoRefundTaskPageDto dto);
+ /**
+ * 按商户当前过期预约配置重扫历史已支付未核销订单,补建或刷新任务并立即执行已到期任务。
+ *
+ * @param mchId 需要重扫的商户 ID
+ * @param operatorUserId 发起重扫的后台用户 ID,用于尝试日志审计
+ * @return 本轮扫描、任务同步和退款执行数量
+ */
+ HardExpiredRefundRescanResultVo rescanExpiredReserveOrders(Long mchId, Long operatorUserId);
+
/**
* 查询任务的只读尝试日志,按尝试序号升序返回。
*
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardCanteenApptTotalExportService.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardCanteenApptTotalExportService.java
index 53172794..2ec1b525 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardCanteenApptTotalExportService.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardCanteenApptTotalExportService.java
@@ -2,8 +2,6 @@ package com.bsd.pjcloud.hard.service;
import com.bsd.pjcloud.common.asynctask.vo.AsyncTaskProgressVO;
import com.bsd.pjcloud.hard.api.dto.order.HardCanteenUserApptQueryDto;
-import com.bsd.pjcloud.hard.api.dto.order.HardOrderDto;
-import com.bsd.pjcloud.hard.api.vo.order.excel.HardCanteenProductApptExcelVo;
import com.bsd.pjcloud.hard.api.vo.order.excel.HardCanteenUserApptExcelVo;
import java.util.List;
@@ -31,20 +29,4 @@ public interface HardCanteenApptTotalExportService {
* @return 用户预约导出行
*/
List userReservationExport(HardCanteenUserApptQueryDto query);
-
- /**
- * 提交菜品预约异步导出任务。
- *
- * @param query 菜品预约统计筛选条件
- * @return 可供前端轮询的初始任务进度
- */
- AsyncTaskProgressVO submitProductReservationExport(HardOrderDto query);
-
- /**
- * 查询菜品预约自定义 Excel 行,供同步下载和异步写入复用。
- *
- * @param query 菜品预约统计筛选条件
- * @return 按日期餐段、商户部门、商品三级结构排列的导出行
- */
- List productReservationExport(HardOrderDto query);
}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardCanteenTotalService.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardCanteenTotalService.java
index 7502b74e..ab70e8e0 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardCanteenTotalService.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardCanteenTotalService.java
@@ -2,6 +2,7 @@ package com.bsd.pjcloud.hard.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.bsd.pjcloud.common.asynctask.vo.AsyncTaskProgressVO;
import com.bsd.pjcloud.common.data.datascope.DataScope;
import com.bsd.pjcloud.common.security.service.PjcloudUser;
import com.bsd.pjcloud.hard.api.dto.order.HardCarDto;
@@ -78,4 +79,20 @@ public interface HardCanteenTotalService {
* @return 日期餐段、商户部门、商品三级排列的导出行
*/
List exportSales(HardOrderDto dto, DataScope dataScope);
+
+ /**
+ * 提交食堂菜品销售统计异步导出任务。
+ *
+ * @param dto 统计日期、来源、餐段、食堂和部门筛选条件
+ * @return 可供前端轮询的初始任务进度,成功终态包含文件下载地址
+ */
+ AsyncTaskProgressVO submitProductSalesExport(HardOrderDto dto);
+
+ /**
+ * 提交食堂部门销售统计异步导出任务。
+ *
+ * @param dto 统计日期、来源、餐段、食堂和部门筛选条件
+ * @return 可供前端轮询的初始任务进度,成功终态包含文件下载地址
+ */
+ AsyncTaskProgressVO submitDepartmentSalesExport(HardOrderDto dto);
}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderExportService.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderExportService.java
new file mode 100644
index 00000000..037c8de2
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderExportService.java
@@ -0,0 +1,30 @@
+package com.bsd.pjcloud.hard.service;
+
+import com.bsd.pjcloud.common.asynctask.vo.AsyncTaskProgressVO;
+import com.bsd.pjcloud.hard.api.dto.order.HardOrderDto;
+import jakarta.servlet.http.HttpServletResponse;
+
+/**
+ * 硬件订单导出服务。
+ *
+ * 该服务独立承担导出条件冻结、数据权限、分批查询、模板写入、进度上报和文件中心上传,
+ * 订单查询服务只保留订单业务查询职责。
+ */
+public interface HardOrderExportService {
+
+ /**
+ * 提交硬件订单异步分批导出任务。
+ *
+ * @param dto 订单页面当前筛选条件;orderIds 非空时仅导出勾选订单
+ * @return 可供前端轮询的初始任务进度,成功终态包含文件下载地址
+ */
+ AsyncTaskProgressVO submitExport(HardOrderDto dto);
+
+ /**
+ * 兼容旧页面的同步导出入口,同样按页查询并通过临时文件流式生成工作簿。
+ *
+ * @param response 当前 HTTP 下载响应,方法会写入完整 XLSX 文件
+ * @param dto 订单页面当前筛选条件
+ */
+ void export(HttpServletResponse response, HardOrderDto dto);
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/HardApptReservationServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/HardApptReservationServiceImpl.java
index 43a8c4ba..2dddc62b 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/HardApptReservationServiceImpl.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/HardApptReservationServiceImpl.java
@@ -358,11 +358,10 @@ public class HardApptReservationServiceImpl implements HardApptReservationServic
throw new RuntimeException(availability.unavailableReason());
}
return new ReservationValidation(appt, appt.getHourType(), targetDate, null, availability.endDateTime(),
- availability.endDateTime());
+ resolveUsageEndDateTime(targetDate));
}
- private ReservationValidation validateCycleReservation(Long mchId, Long apptRuleId, String hourType,
- LocalDate apptDate, LocalDateTime now) {
+ private ReservationValidation validateCycleReservation(Long mchId, Long apptRuleId, String hourType, LocalDate apptDate, LocalDateTime now) {
if (apptDate == null || HardHourTypeEnum.valueOfByType(hourType) == null) {
throw new RuntimeException("周期预约需要选择预约日期和餐端");
}
@@ -379,33 +378,18 @@ public class HardApptReservationServiceImpl implements HardApptReservationServic
throw new RuntimeException(match.availability().unavailableReason());
}
return new ReservationValidation(null, hourType, apptDate, selected.getId(), match.availability().endDateTime(),
- resolveCycleUsageEndDateTime(mchId, hourType, apptDate));
+ resolveUsageEndDateTime(apptDate));
}
/**
- * 周期预约的开放窗口结束时间不是餐段结束时间;这里按已启用的实际 hard_appt 餐段解析,
- * 无法唯一解析时返回空值,调用方会放弃过期退款快照以避免误退。
+ * 预约订单的核销有效期按实际用餐日全天计算,统一固化为当天 23:59:59。
+ * 该口径不读取当前餐段配置,避免预约配置高频调整后影响已创建订单。
*
- * @param mchId 订单所属商户 ID
- * @param hourType 订单选中的标准餐段编码
* @param apptDate 实际用餐日期
- * @return 匹配餐段的实际结束时间;缺失、重复或时间配置异常时返回 {@code null}
+ * @return 当天 23:59:59;用餐日期为空时返回 {@code null}
*/
- private LocalDateTime resolveCycleUsageEndDateTime(Long mchId, String hourType, LocalDate apptDate) {
- List candidates = hardApptService.list(new LambdaQueryWrapper()
- .eq(HardApptEntity::getMchId, mchId)
- .eq(HardApptEntity::getHourType, hourType)
- .eq(HardApptEntity::getStatus, YesNoEnum.YES.getCode())
- .orderByAsc(HardApptEntity::getId));
- if (candidates.size() != 1) {
- return null;
- }
- HardApptEntity appt = candidates.get(0);
- HardApptTimeTypeEnum endType = HardApptTimeTypeEnum.valueOfByType(appt.getEndTimeType());
- if (endType == null || appt.getEndTime() == null) {
- return null;
- }
- return apptDate.plusDays(endType.getDay()).atTime(appt.getEndTime());
+ private LocalDateTime resolveUsageEndDateTime(LocalDate apptDate) {
+ return apptDate == null ? null : apptDate.atTime(23, 59, 59);
}
private CycleMatch matchCycleRule(HardApptCycleRuleVo rule, String hourType,
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/HardAuthServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/HardAuthServiceImpl.java
index ee9821de..c0fd9052 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/HardAuthServiceImpl.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/HardAuthServiceImpl.java
@@ -34,6 +34,7 @@ import com.bsd.pjcloud.hard.api.vo.order.HardAppCreateThirdDoorOrderVo;
import com.bsd.pjcloud.hard.mapper.HardAuthMapper;
import com.bsd.pjcloud.hard.mapper.manager.HardManagerMapper;
import com.bsd.pjcloud.hard.service.*;
+import com.bsd.pjcloud.hard.support.HardUserIdentitySupport;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -62,8 +63,12 @@ public class HardAuthServiceImpl extends ServiceImpl scopePageJoin(Page page, HardAuthDto dto, DataScope dataScope) {
+ prepareIdentityQuery(dto);
Page pageList = baseMapper.scopePageJoin(page, dto, dataScope);
disposeList(pageList.getRecords(), dto);
return pageList;
@@ -237,14 +242,18 @@ public class HardAuthServiceImpl extends ServiceImpl userIds = list.stream().map(HardAuthVo::getUserId).distinct().toList();
- if (ObjectUtil.isNotEmpty(userIds)) {
- R> userList = remoteUserService.getUserDeptByIds(new ArrayList<>(userIds));
- if (ObjectUtil.isNotEmpty(userList.getData())) {
- DataUtil.convertUserDeptList(list, CollectionUtils.convertMap(userList.getData(), UserVO::getUserId));
- }
- }
+ userIdentitySupport.fillUserInfo(list, true);
+ }
+
+ /** 将授权用户身份条件预筛选为授权记录可直接使用的用户主键集合。 */
+ private void prepareIdentityQuery(HardAuthDto dto) {
+ if (!userIdentitySupport.hasIdentityQuery(dto)) return;
+ dto.setUserIds(userIdentitySupport.findIdentityUserIds(dto, dto.getUserId(), dto.getUserIds()));
+ dto.setUserId(null);
+ dto.setIdentityTypes(null);
+ dto.setStudentInfo(null);
+ dto.setTeacherInfo(null);
+ dto.setHasFace(null);
}
/**
@@ -268,4 +277,4 @@ public class HardAuthServiceImpl extends ServiceImpl scopePageJoin(Page page, HardBroadcastDto dto, DataScope dataScope) {
+ prepareIdentityQuery(dto);
Page pageList = baseMapper.scopePageJoin(page, dto, dataScope);
disposeList(pageList.getRecords(), dto);
return pageList;
@@ -310,14 +315,18 @@ public class HardBroadcastServiceImpl extends ServiceImpl userIds = list.stream().map(HardBroadcastVo::getUserId).distinct().toList();
- if (ObjectUtil.isNotEmpty(userIds)) {
- R> userList = remoteUserService.getUserDeptByIds(new ArrayList<>(userIds));
- if (ObjectUtil.isNotEmpty(userList.getData())) {
- DataUtil.convertUserDeptList(list, CollectionUtils.convertMap(userList.getData(), UserVO::getUserId));
- }
- }
+ userIdentitySupport.fillUserInfo(list, true);
+ }
+
+ /** 将广播预定用户身份条件预筛选为广播记录可直接使用的用户主键集合。 */
+ private void prepareIdentityQuery(HardBroadcastDto dto) {
+ if (!userIdentitySupport.hasIdentityQuery(dto)) return;
+ dto.setUserIds(userIdentitySupport.findIdentityUserIds(dto, dto.getUserId(), dto.getUserIds()));
+ dto.setUserId(null);
+ dto.setIdentityTypes(null);
+ dto.setStudentInfo(null);
+ dto.setTeacherInfo(null);
+ dto.setHasFace(null);
}
}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/HardDoorFlowServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/HardDoorFlowServiceImpl.java
index eb2a746f..301eeb9b 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/HardDoorFlowServiceImpl.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/HardDoorFlowServiceImpl.java
@@ -41,6 +41,7 @@ import com.bsd.pjcloud.hard.mapper.HardDoorFlowMapper;
import com.bsd.pjcloud.hard.mapper.HardThirdMiOrderMapper;
import com.bsd.pjcloud.hard.mapper.manager.HardManagerMapper;
import com.bsd.pjcloud.hard.service.*;
+import com.bsd.pjcloud.hard.support.HardUserIdentitySupport;
import com.bsd.pjcloud.thirdparty.api.feign.RemoterThirdDoorApiService;
import com.bsd.pjcloud.thirdparty.api.feign.RemoterThirdKsService;
import com.bsd.pjcloud.thirdparty.dto.door.*;
@@ -88,11 +89,15 @@ public class HardDoorFlowServiceImpl extends ServiceImpl scopePageJoin(Page page, HardDoorFlowDto dto, DataScope dataScope) {
+ prepareIdentityQuery(dto);
Page pageList = baseMapper.scopePageJoin(page, dto, dataScope);
disposeList(pageList.getRecords(), dto);
return pageList;
@@ -427,14 +432,18 @@ public class HardDoorFlowServiceImpl extends ServiceImpl userIds = list.stream().map(HardDoorFlowVo::getUserId).distinct().toList();
- if (ObjectUtil.isNotEmpty(userIds)) {
- R> userList = remoteUserService.getUserDeptByIds(new ArrayList<>(userIds));
- if (ObjectUtil.isNotEmpty(userList.getData())) {
- DataUtil.convertUserDeptList(list, CollectionUtils.convertMap(userList.getData(), UserVO::getUserId));
- }
- }
+ userIdentitySupport.fillUserInfo(list, true);
+ }
+
+ /** 将开门流程用户身份条件预筛选为流程记录可直接使用的用户主键集合。 */
+ private void prepareIdentityQuery(HardDoorFlowDto dto) {
+ if (!userIdentitySupport.hasIdentityQuery(dto)) return;
+ dto.setUserIds(userIdentitySupport.findIdentityUserIds(dto, dto.getUserId(), dto.getUserIds()));
+ dto.setUserId(null);
+ dto.setIdentityTypes(null);
+ dto.setStudentInfo(null);
+ dto.setTeacherInfo(null);
+ dto.setHasFace(null);
}
private void disposeList(HardDoorFlowVo detail) {
@@ -458,4 +467,4 @@ public class HardDoorFlowServiceImpl extends ServiceImpl scopeJoinPageList(Page page, HardInvoiceDto dto) {
+ prepareIdentityQuery(dto);
Page list = baseMapper.scopePageList(page, dto, DataScope.of());
if (ObjectUtil.isNotEmpty(list.getRecords())) {
// 部门
@@ -102,14 +107,7 @@ public class HardInvoiceServiceImpl extends ServiceImpl userIds = list.getRecords().stream().map(HardInvoiceVo::getOrderUserId).filter(ObjectUtil::isNotEmpty).distinct().toList();
- if (ObjectUtil.isNotEmpty(userIds)) {
- R> userList = userService.getUserById(new ArrayList<>(userIds));
- if (ObjectUtil.isNotEmpty(userList.getData())) {
- DataUtil.convertUserList(list.getRecords(), CollectionUtils.convertMap(userList.getData(), SysUser::getUserId));
- }
- }
+ userIdentitySupport.fillUserInfo(list.getRecords(), true);
// 商户
List mchIds = list.getRecords().stream().map(HardInvoiceVo::getMerchantId).distinct().toList();
if (ObjectUtil.isNotEmpty(mchIds)) {
@@ -122,6 +120,20 @@ public class HardInvoiceServiceImpl extends ServiceImpl pageJoin(Page page, HardAfterSaleDto dto) {
+ prepareIdentityQuery(dto);
Page list = baseMapper.pageJoin(page, dto);
if (ObjectUtil.isNotEmpty(list.getRecords())) {
- // 用户
- List userIds = list.getRecords().stream().map(HardAfterSaleVo::getUserId).filter(ObjectUtil::isNotEmpty).distinct().toList();
- if (ObjectUtil.isNotEmpty(userIds)) {
- R> userList = userService.getUserById(new ArrayList<>(userIds));
- if (ObjectUtil.isNotEmpty(userList.getData())) {
- DataUtil.convertUserList(list.getRecords(), CollectionUtils.convertMap(userList.getData(), SysUser::getUserId));
- }
- }
+ userIdentitySupport.fillUserInfo(list.getRecords(), true);
// 部门
List deptIds = list.getRecords().stream().map(HardAfterSaleVo::getDeptId).filter(ObjectUtil::isNotEmpty).distinct().toList();
if (ObjectUtil.isNotEmpty(deptIds)) {
@@ -151,16 +149,10 @@ public class HardAfterSaleServiceImpl extends ServiceImpl scopePageList(Page page, HardAfterSaleDto dto) {
+ prepareIdentityQuery(dto);
Page list = baseMapper.scopePageJoinList(page, dto, null);
if (ObjectUtil.isNotEmpty(list.getRecords())) {
- // 用户
- List userIds = list.getRecords().stream().map(HardAfterSaleVo::getUserId).filter(ObjectUtil::isNotEmpty).distinct().toList();
- if (ObjectUtil.isNotEmpty(userIds)) {
- R> userList = userService.getUserById(new ArrayList<>(userIds));
- if (ObjectUtil.isNotEmpty(userList.getData())) {
- DataUtil.convertUserList(list.getRecords(), CollectionUtils.convertMap(userList.getData(), SysUser::getUserId));
- }
- }
+ userIdentitySupport.fillUserInfo(list.getRecords(), true);
// 部门
List deptIds = list.getRecords().stream().map(HardAfterSaleVo::getDeptId).filter(ObjectUtil::isNotEmpty).distinct().toList();
if (ObjectUtil.isNotEmpty(deptIds)) {
@@ -174,6 +166,21 @@ public class HardAfterSaleServiceImpl extends ServiceImpl pageExpiredCandidates(Page page,
- HardAutoRefundTaskPageDto dto) {
+ public Page pageExpiredCandidates(Page page, HardAutoRefundTaskPageDto dto) {
LocalDateTime now = LocalDateTime.now();
Page entityPage = baseMapper.pageExpiredCandidates(page, dto, now);
List records = BeanUtils.toBean(entityPage.getRecords(), HardAutoRefundTaskVo.class);
@@ -981,7 +1003,193 @@ public class HardAutoRefundTaskServiceImpl extends ServiceImpl MAX_EXPIRED_RESERVE_DELAY_HOURS) {
+ throw new RuntimeException("过期预约退款延迟必须在1到168小时之间");
+ }
+ boolean enabled = Objects.equals(config.getExpiredReserveRefundEnabled(), YesNoEnum.YES.getCode());
+ String restoreInventory = Objects.equals(config.getExpiredReserveRefundRestoreInventory(), YesNoEnum.YES.getCode()) ? YesNoEnum.YES.getCode() : YesNoEnum.NO.getCode();
+ List orders = baseMapper.listExpiredRescanOrders(mchId);
+ HardExpiredRefundRescanResultVo result = new HardExpiredRefundRescanResultVo();
+ result.setScannedOrderCount(orders == null ? 0 : orders.size());
+ result.setCreatedTaskCount(0);
+ result.setRefreshedTaskCount(0);
+ result.setExecutedTaskCount(0);
+ result.setSuccessTaskCount(0);
+ result.setSkippedOrderCount(0);
+ if (orders == null || orders.isEmpty()) {
+ return result;
+ }
+
+ LocalDateTime now = LocalDateTime.now();
+ for (HardOrderEntity order : orders) {
+ boolean taskSynced = false;
+ try {
+ LocalDateTime usageEndTime = resolveExpiredRefundUsageEndTimeSnapshot(order);
+ if (usageEndTime == null) {
+ result.setSkippedOrderCount(result.getSkippedOrderCount() + 1);
+ log.warn("|{}|立即重扫跳过缺少可靠订单时间快照的订单,orderId:{}", TAG, order.getId());
+ continue;
+ }
+ LocalDateTime triggerTime = usageEndTime.plusHours(delayHours);
+ LockInfo syncLock = lockTemplate.lock(REFUND_ORDER_LOCK_PREFIX + order.getId(),
+ REFUND_ORDER_LOCK_EXPIRE_MILLIS, REFUND_ORDER_LOCK_WAIT_MILLIS);
+ if (syncLock == null) {
+ result.setSkippedOrderCount(result.getSkippedOrderCount() + 1);
+ continue;
+ }
+ int syncResult;
+ try {
+ syncResult = getSelf().syncExpiredRescanTask(order, config, triggerTime, restoreInventory, enabled);
+ } finally {
+ try {
+ lockTemplate.releaseLock(syncLock);
+ } catch (Exception exception) {
+ log.warn("|{}|释放立即重扫订单锁失败,orderId:{},exceptionType:{}", TAG, order.getId(),
+ exception.getClass().getSimpleName());
+ }
+ }
+ if (syncResult == RESCAN_SYNC_CREATED) {
+ result.setCreatedTaskCount(result.getCreatedTaskCount() + 1);
+ } else if (syncResult == RESCAN_SYNC_REFRESHED) {
+ result.setRefreshedTaskCount(result.getRefreshedTaskCount() + 1);
+ } else {
+ result.setSkippedOrderCount(result.getSkippedOrderCount() + 1);
+ continue;
+ }
+ taskSynced = true;
+ if (enabled && !triggerTime.isAfter(now)) {
+ HardAutoRefundTaskEntity task = baseMapper.selectOne(new LambdaQueryWrapper()
+ .eq(HardAutoRefundTaskEntity::getOrderId, order.getId())
+ .eq(HardAutoRefundTaskEntity::getTaskType, HardAutoRefundTaskTypeEnum.EXPIRED_RESERVE.getType()));
+ if (task != null && Objects.equals(task.getStatus(), HardAutoRefundTaskStatusEnum.PENDING.getStatus())) {
+ result.setExecutedTaskCount(result.getExecutedTaskCount() + 1);
+ execute(task.getId(), HardAutoRefundTriggerTypeEnum.EXPIRED_RESCAN, operatorUserId);
+ HardAutoRefundTaskEntity completed = getById(task.getId());
+ if (completed != null && Objects.equals(completed.getStatus(), HardAutoRefundTaskStatusEnum.SUCCESS.getStatus())) {
+ result.setSuccessTaskCount(result.getSuccessTaskCount() + 1);
+ }
+ }
+ }
+ } catch (Exception exception) {
+ if (!taskSynced) {
+ result.setSkippedOrderCount(result.getSkippedOrderCount() + 1);
+ }
+ log.warn("|{}|立即重扫订单失败,orderId:{},exceptionType:{}", TAG, order.getId(),
+ exception.getClass().getSimpleName());
+ }
+ }
+ return result;
+ }
+
+ /**
+ * 解析订单自身保存的过期退款时间快照。新订单优先使用专用时间字段;历史订单仅使用订单上的预约日期
+ * 兜底为当天 23:59:59,绝不读取当前预约配置或旧的自动退款截止时间。
+ *
+ * @param order 待重扫的已支付预约订单
+ * @return 可信的核销有效期结束时间;订单没有预约日期时返回 {@code null}
+ */
+ private LocalDateTime resolveExpiredRefundUsageEndTimeSnapshot(HardOrderEntity order) {
+ if (order == null) {
+ return null;
+ }
+ if (order.getExpiredRefundUsageEndTime() != null) {
+ return order.getExpiredRefundUsageEndTime();
+ }
+ return order.getApptUseDate() == null ? null : order.getApptUseDate().atTime(23, 59, 59);
+ }
+
+ /**
+ * 同步单个历史订单的过期任务。已成功、已创建售后或已产生资金尝试的任务不允许被新配置覆盖,避免重扫造成重复退款。
+ *
+ * @param order 历史已支付未核销订单
+ * @param config 当前商户过期退款配置
+ * @param triggerTime 按当前延迟重新计算的触发时间
+ * @param restoreInventory 当前退库存快照
+ * @param enabled 当前过期退款开关
+ * @return 新建、刷新或跳过结果编码
+ */
+ @Transactional(rollbackFor = Exception.class)
+ public int syncExpiredRescanTask(HardOrderEntity order, MchAutoRefundConfigVo config,
+ LocalDateTime triggerTime, String restoreInventory, boolean enabled) {
+ if (order == null || order.getId() == null || config == null || config.getId() == null || triggerTime == null) {
+ return RESCAN_SYNC_SKIPPED;
+ }
+ HardOrderEntity currentOrder = hardOrderMapper.selectByIdForUpdate(order.getId());
+ if (currentOrder == null || !Objects.equals(currentOrder.getMerchantId(), config.getMchId())
+ || !Objects.equals(currentOrder.getOrderType(), HardOrderTypeEnum.RESERVE.getType())
+ || !Objects.equals(currentOrder.getOrderStatus(), HardOrderStatusEnum.PAY.getType())
+ || !Objects.equals(currentOrder.getPayType(), HardPayTypeEnum.WALLET_DEFAULT.getType())
+ || currentOrder.getPayPrice() == null || currentOrder.getPayPrice() <= 0
+ || currentOrder.getRefundPrice() != null && currentOrder.getRefundPrice() > 0) {
+ return RESCAN_SYNC_SKIPPED;
+ }
+ Long conflictingAfterSaleCount = afterSaleMapper.selectCount(new LambdaQueryWrapper()
+ .eq(HardAfterSaleEntity::getOrderId, currentOrder.getId())
+ .in(HardAfterSaleEntity::getStatus,
+ List.of(HardSaleStatusEnum.APPLY.getStatus(), HardSaleStatusEnum.COMPLETE.getStatus())));
+ if (conflictingAfterSaleCount != null && conflictingAfterSaleCount > 0) {
+ return RESCAN_SYNC_SKIPPED;
+ }
+ HardAutoRefundTaskEntity existing = baseMapper.selectOne(new LambdaQueryWrapper()
+ .eq(HardAutoRefundTaskEntity::getOrderId, currentOrder.getId())
+ .eq(HardAutoRefundTaskEntity::getTaskType, HardAutoRefundTaskTypeEnum.EXPIRED_RESERVE.getType()));
+ if (existing != null) {
+ boolean refreshableStatus = List.of(HardAutoRefundTaskStatusEnum.PENDING.getStatus(),
+ HardAutoRefundTaskStatusEnum.SWITCH_DISABLED.getStatus(),
+ HardAutoRefundTaskStatusEnum.CONFIG_UNAVAILABLE.getStatus()).contains(existing.getStatus());
+ if (!refreshableStatus || !Objects.equals(existing.getMchId(), currentOrder.getMerchantId())
+ || existing.getAfterSaleId() != null || safeCount(existing.getAutoAttemptCount()) > 0) {
+ return RESCAN_SYNC_SKIPPED;
+ }
+ existing.setConfigId(config.getId());
+ existing.setTriggerTime(triggerTime);
+ existing.setRestoreInventory(restoreInventory);
+ existing.setStatus(enabled ? HardAutoRefundTaskStatusEnum.PENDING.getStatus()
+ : HardAutoRefundTaskStatusEnum.SWITCH_DISABLED.getStatus());
+ existing.setNextRetryTime(enabled ? triggerTime : null);
+ existing.setLastError(null);
+ if (baseMapper.updateById(existing) != 1) {
+ throw new RuntimeException("刷新过期预约退款任务失败");
+ }
+ return RESCAN_SYNC_REFRESHED;
+ }
+ HardAutoRefundTaskEntity task = HardAutoRefundTaskEntity.builder()
+ .taskType(HardAutoRefundTaskTypeEnum.EXPIRED_RESERVE.getType())
+ .orderId(currentOrder.getId()).mchId(currentOrder.getMerchantId())
+ .configId(config.getId()).triggerTime(triggerTime)
+ .restoreInventory(restoreInventory)
+ .status(enabled ? HardAutoRefundTaskStatusEnum.PENDING.getStatus()
+ : HardAutoRefundTaskStatusEnum.SWITCH_DISABLED.getStatus())
+ .autoAttemptCount(0).totalAttemptCount(0)
+ .nextRetryTime(enabled ? triggerTime : null)
+ .build();
+ if (baseMapper.insert(task) != 1) {
+ throw new RuntimeException("创建过期预约退款任务失败");
+ }
+ return RESCAN_SYNC_CREATED;
+ }
+
+ /**
+ * 批量补充过期候选订单日期和餐段,避免把配置变更后的规则重新解析为退款时间。
+ */
private void fillOrderData(List records) {
List orderIds = records.stream().map(HardAutoRefundTaskVo::getOrderId).filter(Objects::nonNull).distinct().toList();
if (orderIds.isEmpty()) return;
@@ -1092,7 +1300,7 @@ public class HardAutoRefundTaskServiceImpl extends ServiceImpl expired.ne(HardAutoRefundTaskEntity::getTaskType,
HardAutoRefundTaskTypeEnum.EXPIRED_RESERVE.getType())
.or(expiredType -> expiredType.eq(HardAutoRefundTaskEntity::getTaskType,
- HardAutoRefundTaskTypeEnum.EXPIRED_RESERVE.getType())
+ HardAutoRefundTaskTypeEnum.EXPIRED_RESERVE.getType())
.le(HardAutoRefundTaskEntity::getTriggerTime, now)))))
.orderByAsc(HardAutoRefundTaskEntity::getNextRetryTime)
.orderByAsc(HardAutoRefundTaskEntity::getId)
@@ -1187,7 +1395,7 @@ public class HardAutoRefundTaskServiceImpl extends ServiceImpl菜品导出以预约使用日期为批次边界,每完成一个日期就推进一次 Redis 进度;Excel 数据始终通过
- * Easy Excel 追加写入,避免把整个日期范围的商品明细一次性保留在内存中。
+ * 按权限用户分批查询每日预约并流式写入 Excel,避免长日期范围和大量用户一次性占用过多内存。
*/
@Slf4j
@Validated
@@ -131,33 +120,9 @@ public class HardCanteenApptTotalExportServiceImpl implements HardCanteenApptTot
private static final DateTimeFormatter EXCEL_DATE_TIME_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
- /** 菜品预约导出进度的 Redis Key 命名空间。 */
- private static final String PRODUCT_EXPORT_REDIS_KEY = "hard:canteen-product-appt:export";
-
- /** 菜品预约导出任务名称。 */
- private static final String PRODUCT_EXPORT_TASK_NAME = "菜品预约统计导出";
-
- /** 异步任务创建时的占位总数,真实日期数量在后台查询后更新。 */
- private static final int INITIAL_PROGRESS_TOTAL = 1;
-
- /** 菜品预约导出工作表名称。 */
- private static final String PRODUCT_EXPORT_SHEET_NAME = "菜品预约统计";
-
- /** 文件中心存储目录。 */
- private static final String EXPORT_FILE_DIR = "hard-export";
-
- /** 文件中心业务类型编码。 */
- private static final String PRODUCT_EXPORT_FILE_TYPE = "HARD_CANTEEN_PRODUCT_APPT_EXPORT";
-
- /** Excel 文件 MIME 类型。 */
- private static final String EXCEL_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
-
/** 用餐时间段字典类型。 */
private static final String MEAL_SEGMENT_DICT_TYPE = "daily_menu";
- /** Excel 文件名时间格式。 */
- private static final DateTimeFormatter FILE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
-
/** 用户预约查询允许的最大自然月跨度,开始和结束日期均计入查询范围。 */
private static final long MAX_QUERY_MONTHS = 2L;
@@ -206,9 +171,6 @@ public class HardCanteenApptTotalExportServiceImpl implements HardCanteenApptTot
/** 菜品预约聚合 Mapper。 */
private final HardCarMapper hardCarMapper;
- /** 每日菜谱远程服务,用于展开套餐组成。 */
- private final RemoteDailyMenuService remoteDailyMenuService;
-
/** 部门远程服务,用于补齐预约人所属部门名称。 */
private final RemoteDeptService remoteDeptService;
@@ -1303,459 +1265,4 @@ public class HardCanteenApptTotalExportServiceImpl implements HardCanteenApptTot
}
}
- /**
- * 提交菜品预约异步导出任务。
- *
- * @param query 菜品预约统计筛选条件
- * @return 初始异步任务进度
- */
- @Override
- public AsyncTaskProgressVO submitProductReservationExport(HardOrderDto query) {
- HardOrderDto querySnapshot = copyProductQuery(query);
- return asyncTaskExecutorService.submitWithResult(PRODUCT_EXPORT_REDIS_KEY, PRODUCT_EXPORT_TASK_NAME,
- INITIAL_PROGRESS_TOTAL, reporter -> exportProducts(querySnapshot, reporter));
- }
-
- /**
- * 同步查询菜品预约自定义 Excel 行。
- *
- * @param query 菜品预约统计筛选条件
- * @return 三级层级导出行
- */
- @Override
- public List productReservationExport(HardOrderDto query) {
- HardOrderDto querySnapshot = copyProductQuery(query);
- List rows = new ArrayList<>();
- for (LocalDate date : getQueryDates(querySnapshot)) {
- HardOrderDto dateQuery = copyProductQuery(querySnapshot);
- dateQuery.setApptUseDateTimeList(new LocalDateTime[]{date.atStartOfDay(), date.atTime(23, 59, 59)});
- rows.addAll(buildProductRows(dateQuery, date));
- }
- return rows;
- }
-
- /**
- * 后台按日期批次生成菜品预约工作簿并上传文件中心。
- *
- * @param query 后台线程独占的查询快照
- * @param reporter 当前异步任务进度报告器
- * @return 文件中心结果
- */
- private AsyncTaskResult exportProducts(HardOrderDto query, AsyncTaskProgressReporter reporter) {
- List dates = getQueryDates(query);
- reporter.updateTotal(Math.max(1, dates.size() + 1), "已确定 " + dates.size() + " 个用餐日期,开始分批导出");
- Path outputPath = null;
- String fileName = "菜品预约统计_" + LocalDateTime.now().format(FILE_TIME_FORMATTER) + ".xlsx";
- try {
- outputPath = Files.createTempFile("canteen-product-appt-export-", ".xlsx");
- long rowCount;
- try (ExcelWriter writer = EasyExcel.write(outputPath.toFile(), HardCanteenProductApptExcelVo.class)
- .registerWriteHandler(new HardCanteenProductApptExportMergeHandler()).build()) {
- WriteSheet sheet = EasyExcel.writerSheet(PRODUCT_EXPORT_SHEET_NAME).build();
- rowCount = 0L;
- for (LocalDate date : dates) {
- HardOrderDto dateQuery = copyProductQuery(query);
- dateQuery.setApptUseDateTimeList(new LocalDateTime[]{date.atStartOfDay(), date.atTime(23, 59, 59)});
- List batchRows = buildProductRows(dateQuery, date);
- if (!batchRows.isEmpty()) {
- writer.write(batchRows, sheet);
- rowCount += batchRows.size();
- }
- reporter.advance("已处理 " + date + ",写入 " + rowCount + " 行");
- }
- if (rowCount == 0L) {
- throw new IllegalStateException("没有符合当前条件的菜品预约数据");
- }
- }
- String fileUrl = upload(outputPath, fileName);
- reporter.advance("菜品预约导出文件已生成");
- return AsyncTaskResult.file(fileUrl, fileName);
- } catch (IllegalStateException exception) {
- throw exception;
- } catch (Exception exception) {
- log.error("菜品预约统计导出失败,fileName:{}", fileName, exception);
- throw new IllegalStateException("生成菜品预约导出文件失败", exception);
- } finally {
- if (outputPath != null) {
- try {
- Files.deleteIfExists(outputPath);
- } catch (Exception exception) {
- log.warn("删除菜品预约导出临时文件失败,path:{}", outputPath, exception);
- }
- }
- }
- }
-
- /**
- * 查询一个日期批次的三层数据并转换为 Excel 行。
- *
- * @param query 日期批次查询条件
- * @param fixedDate 已知的批次日期;同步查询传空时从查询结果读取
- * @return 当前批次的汇总、商户部门、商品明细和商品统计行
- */
- private List buildProductRows(HardOrderDto query, LocalDate fixedDate) {
- List summaries = hardCarMapper.listApptExportSummary(query);
- List merchants = hardCarMapper.listApptExportMerchant(query);
- List products = hardCarMapper.listApptExportProduct(query);
- if (summaries == null || summaries.isEmpty()) {
- return List.of();
- }
- Map merchantNames = fetchMerchantNames(merchants);
- Map departmentNames = fetchDepartmentNames(merchants);
- Map hourNames = fetchHourNames();
- Map menuMap = fetchDailyMenus(products, fixedDate);
- Map> merchantGroups = merchants.stream()
- .collect(Collectors.groupingBy(this::summaryGroupKey, LinkedHashMap::new, Collectors.toList()));
- Map> productGroups = products.stream()
- .collect(Collectors.groupingBy(this::groupKey, LinkedHashMap::new, Collectors.toList()));
- List rows = new ArrayList<>();
- for (HardCanteenProductTotalVo summary : summaries) {
- LocalDate date = fixedDate != null ? fixedDate
- : summary.getCreateTime() == null ? null : summary.getCreateTime().toLocalDate();
- if (date == null) {
- log.warn("菜品预约导出汇总记录缺少预约日期,hourType:{},跳过该记录", summary.getHourType());
- continue;
- }
- String hourType = summary.getHourType();
- String summaryKey = date + "|" + hourType;
- List merchantRows = merchantGroups.getOrDefault(summaryKey, List.of());
- Map> detailRowsByMerchant = new LinkedHashMap<>();
- Map detailCountsByMerchant = new HashMap<>();
- Map detailPricesByMerchant = new HashMap<>();
- long summaryProductCount = 0L;
- long summaryTotalPrice = 0L;
- for (HardCanteenProductApptExportRecord merchant : merchantRows) {
- String merchantKey = groupKey(merchant);
- List detailRows = productGroups
- .getOrDefault(merchantKey, List.of()).stream()
- .flatMap(product -> toProductRows(product,
- menuMap.get(menuKey(product, fixedDate != null ? fixedDate : date))).stream())
- .toList();
- List statisticRows = buildProductStatistics(detailRows);
- long detailCount = detailRows.stream().mapToLong(detail -> valueOrZero(detail.getProductCount())).sum();
- long detailPrice = detailRows.stream().mapToLong(detail -> yuanToCents(detail.getProductTotalPrice())).sum();
- // 每日菜谱查询失败时仍保留 SQL 的有效预约统计,避免导出因远程菜谱暂时不可用而丢失金额。
- if (detailRows.isEmpty()) {
- detailCount = valueOrZero(merchant.getPayCount());
- detailPrice = valueOrZero(merchant.getTotalPrice());
- }
- List merchantProductRows = new ArrayList<>(
- detailRows.size() + statisticRows.size());
- merchantProductRows.addAll(detailRows);
- merchantProductRows.addAll(statisticRows);
- detailRowsByMerchant.put(merchantKey, merchantProductRows);
- detailCountsByMerchant.put(merchantKey, detailCount);
- detailPricesByMerchant.put(merchantKey, detailPrice);
- summaryProductCount += detailCount;
- summaryTotalPrice += detailPrice;
- }
- if (merchantRows.isEmpty()) {
- summaryProductCount = valueOrZero(summary.getPayCount());
- summaryTotalPrice = valueOrZero(summary.getTotalPrice());
- }
- HardCanteenProductApptExcelVo summaryRow = new HardCanteenProductApptExcelVo();
- summaryRow.setSummaryDate(date);
- summaryRow.setSummaryHourType(resolveHourName(hourType, hourNames));
- summaryRow.setSummaryPayUserCount(valueOrZero(summary.getPayUserCount()));
- summaryRow.setSummaryTimeCount(valueOrZero(summary.getTimeCount()));
- // 汇总数量必须与下方已展开的商品明细一致;套餐按组成商品份数计入,而不是只计套餐份数。
- summaryRow.setSummaryPayCount(summaryProductCount);
- summaryRow.setSummaryTotalPrice(centsToYuan(summaryTotalPrice));
- rows.add(summaryRow);
- for (HardCanteenProductApptExportRecord merchant : merchantRows) {
- HardCanteenProductApptExcelVo merchantRow = new HardCanteenProductApptExcelVo();
- merchantRow.setMerchantName(merchantNames.getOrDefault(merchant.getMerchantId(), "商户信息未记录"));
- merchantRow.setDepartmentName(departmentNames.getOrDefault(merchant.getUserDeptId(), "部门信息未记录"));
- merchantRow.setMerchantDate(date);
- merchantRow.setMerchantHourType(resolveHourName(hourType, hourNames));
- merchantRow.setMerchantPayUserCount(valueOrZero(merchant.getPayUserCount()));
- merchantRow.setMerchantTimeCount(valueOrZero(merchant.getTimeCount()));
- String merchantKey = groupKey(merchant);
- merchantRow.setMerchantPayCount(detailCountsByMerchant.getOrDefault(merchantKey,
- valueOrZero(merchant.getPayCount())));
- merchantRow.setMerchantTotalPrice(centsToYuan(detailPricesByMerchant.getOrDefault(merchantKey,
- valueOrZero(merchant.getTotalPrice()))));
- rows.add(merchantRow);
- rows.addAll(detailRowsByMerchant.getOrDefault(merchantKey, List.of()));
- }
- }
- return rows;
- }
-
- /**
- * 将商品聚合行按预约当天每日菜谱中的套餐组成展开。
- *
- * 组成商品数量按“套餐预约数量 × 套餐内份数”计算;组成商品金额按菜谱单价权重使用最大余数法分摊,
- * 从而保证所有组成行金额之和严格等于套餐订单快照金额,并避免极小金额因四舍五入产生负数尾差。
- *
- * @param product 订单购物车商品聚合记录,数量和金额均为套餐商品口径
- * @param menu 与商户、预约日期、餐段和商品 SPU 匹配的每日菜谱;为空时保留原商品行
- * @return 单品原始行,或按套餐组成顺序生成的商品明细行
- */
- private List toProductRows(HardCanteenProductApptExportRecord product, DailyMenuAppVO menu) {
- List items = menu != null && menu.getComboItems() != null
- && !menu.getComboItems().isEmpty() ? menu.getComboItems() : List.of();
- if (items.isEmpty()) {
- return List.of(toProductRow(product.getProductName(), valueOrZero(product.getPayCount()),
- product.getTotalPrice()));
- }
- long packageCount = valueOrZero(product.getPayCount());
- long totalCents = valueOrZero(product.getTotalPrice());
- long sign = totalCents < 0 ? -1L : 1L;
- long absoluteTotalCents = Math.abs(totalCents);
- long baseAmount = items.stream().mapToLong(item -> Math.max(0, valueOrZero(item.getPrice()))
- * Math.max(1, valueOrZero(item.getQuantity()))).sum();
- long[] itemPrices = new long[items.size()];
- long[] itemWeights = new long[items.size()];
- long allocated = 0L;
- List remainderIndexes = new ArrayList<>();
- List result = new ArrayList<>();
- for (int index = 0; index < items.size(); index++) {
- OnlineSpuComboItemVO item = items.get(index);
- long itemWeight = Math.max(0, valueOrZero(item.getPrice())) * Math.max(1, valueOrZero(item.getQuantity()));
- itemWeights[index] = itemWeight;
- if (baseAmount > 0) {
- BigDecimal exactPrice = BigDecimal.valueOf(absoluteTotalCents)
- .multiply(BigDecimal.valueOf(itemWeight))
- .divide(BigDecimal.valueOf(baseAmount), 6, RoundingMode.DOWN);
- itemPrices[index] = exactPrice.setScale(0, RoundingMode.FLOOR).longValue();
- allocated += itemPrices[index];
- remainderIndexes.add(index);
- }
- }
- if (baseAmount > 0) {
- long remainder = absoluteTotalCents - allocated;
- // 每项先向下取整,再按小数余数从大到小补分,保证分摊值不为负且合计不丢分。
- remainderIndexes.sort(Comparator.comparing((Integer index) -> {
- BigDecimal exactPrice = BigDecimal.valueOf(absoluteTotalCents)
- .multiply(BigDecimal.valueOf(itemWeights[index]))
- .divide(BigDecimal.valueOf(baseAmount), 6, RoundingMode.DOWN);
- return exactPrice.subtract(BigDecimal.valueOf(itemPrices[index]));
- }).reversed());
- for (int index = 0; index < remainderIndexes.size() && remainder > 0; index++, remainder--) {
- itemPrices[remainderIndexes.get(index)]++;
- }
- } else if (!items.isEmpty()) {
- itemPrices[items.size() - 1] = absoluteTotalCents;
- }
- for (int index = 0; index < items.size(); index++) {
- OnlineSpuComboItemVO item = items.get(index);
- long itemCount = packageCount * Math.max(1, valueOrZero(item.getQuantity()));
- long itemPrice = itemPrices[index] * sign;
- result.add(toProductRow(StrUtil.blankToDefault(item.getName(), "未命名商品"), itemCount, itemPrice));
- }
- return result;
- }
-
- /** 创建单个商品明细行。 */
- private HardCanteenProductApptExcelVo toProductRow(String name, long count, long totalPrice) {
- HardCanteenProductApptExcelVo row = new HardCanteenProductApptExcelVo();
- row.setProductName(name);
- row.setProductCount(count);
- row.setProductTotalPrice(centsToYuan(totalPrice));
- return row;
- }
-
- /**
- * 在当前商户部门的商品明细右侧写入按商品名称合并后的统计结果。
- *
- * 套餐组成商品可能由多个套餐分别展开成同名明细,因此不能按原套餐 SPU 汇总;这里使用压缩空白后的名称作为键,
- * 保留首次出现的展示名称,并使用整数分累计金额,确保统计总数量、总价格与左侧有效明细严格一致。
- *
- * @param detailRows 同一预约日期、餐段、商户和部门下,已经扣减完成退款并完成套餐展开的商品明细
- * @return 独立的商品统计行,顺序与商品名称首次出现的顺序一致
- */
- private List buildProductStatistics(
- List detailRows) {
- Map statistics = new LinkedHashMap<>();
- for (HardCanteenProductApptExcelVo detailRow : detailRows) {
- String displayName = StrUtil.blankToDefault(detailRow.getProductName(), "未命名商品").trim();
- String statisticKey = normalizeProductName(displayName);
- HardCanteenProductApptExcelVo statistic = statistics.computeIfAbsent(statisticKey, key -> {
- HardCanteenProductApptExcelVo row = new HardCanteenProductApptExcelVo();
- row.setStatisticProductName(displayName);
- row.setStatisticProductCount(0L);
- row.setStatisticProductTotalPrice(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
- return row;
- });
- statistic.setStatisticProductCount(valueOrZero(statistic.getStatisticProductCount())
- + valueOrZero(detailRow.getProductCount()));
- long statisticTotalPrice = yuanToCents(statistic.getStatisticProductTotalPrice())
- + yuanToCents(detailRow.getProductTotalPrice());
- statistic.setStatisticProductTotalPrice(centsToYuan(statisticTotalPrice));
- }
- return new ArrayList<>(statistics.values());
- }
-
- /**
- * 规范化商品名称中的换行和连续空白,用于合并仅排版不同的同名套餐组成商品。
- *
- * @param productName 首次出现的商品展示名称
- * @return 去除首尾空白并把连续空白压缩为单个空格的统计键
- */
- private String normalizeProductName(String productName) {
- return productName.replaceAll("\\s+", " ").trim();
- }
-
- /** 查询当前批次涉及的每日菜谱,并以商户、日期、餐段和 SPU 组成缓存键。 */
- private Map fetchDailyMenus(List products,
- LocalDate fixedDate) {
- Map result = new HashMap<>();
- LocalDate date = fixedDate;
- if (date == null) return result;
- List merchantIds = products.stream().map(HardCanteenProductApptExportRecord::getMerchantId)
- .filter(Objects::nonNull).distinct().toList();
- for (Long merchantId : merchantIds) {
- for (String hourType : products.stream().filter(item -> Objects.equals(item.getMerchantId(), merchantId))
- .map(HardCanteenProductApptExportRecord::getHourType).filter(StrUtil::isNotBlank).distinct().toList()) {
- try {
- Integer hour = Integer.valueOf(hourType);
- R> response = remoteDailyMenuService.getCanteenList(date.toString(), hour, null, merchantId);
- if (response != null && response.isOk() && response.getData() != null) {
- response.getData().forEach(menu -> {
- if (menu.getSpuId() != null) result.put(merchantId + "|" + date + "|" + hourType + "|" + menu.getSpuId(), menu);
- });
- }
- } catch (Exception exception) {
- log.warn("查询每日菜谱套餐组成失败,保留原商品行,merchantId:{},date:{},hourType:{}",
- merchantId, date, hourType, exception.getMessage());
- }
- }
- }
- return result;
- }
-
- /** 批量查询商户名称。 */
- private Map fetchMerchantNames(List rows) {
- List ids = rows.stream().map(HardCanteenProductApptExportRecord::getMerchantId).filter(Objects::nonNull).distinct().toList();
- if (ids.isEmpty()) return Map.of();
- try {
- R> response = remoteMchService.getByIds(ids);
- if (response == null || !response.isOk() || response.getData() == null) return Map.of();
- return response.getData().stream().filter(item -> item.getId() != null)
- .collect(Collectors.toMap(MchManageVo::getId, MchManageVo::getName, (left, right) -> left));
- } catch (Exception exception) {
- log.warn("查询导出商户名称失败,使用空名称", exception);
- return Map.of();
- }
- }
-
- /** 批量查询部门名称。 */
- private Map fetchDepartmentNames(List rows) {
- List ids = rows.stream().map(HardCanteenProductApptExportRecord::getUserDeptId).filter(Objects::nonNull).distinct().toList();
- if (ids.isEmpty()) return Map.of();
- try {
- R