diff --git a/db/20260824_add_third_payment_task.sql b/db/20260824_add_third_payment_task.sql index 7fc7ded9..31468234 100644 --- a/db/20260824_add_third_payment_task.sql +++ b/db/20260824_add_third_payment_task.sql @@ -31,8 +31,8 @@ CREATE TABLE IF NOT EXISTS `third_payment_task` `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` (`status`, `next_run_time`, `priority`, `id`), - KEY `idx_third_payment_task_lease` (`status`, `lease_expire_time`), + 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 = '第三方支付任务事实'; @@ -57,3 +57,97 @@ CREATE TABLE IF NOT EXISTS `third_payment_task_attempt` 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 new file mode 100644 index 00000000..25115867 --- /dev/null +++ b/db/20260825_add_third_mch_fz_relation.sql @@ -0,0 +1,79 @@ +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_add_wallet_transaction_remark.sql b/db/20260825_add_wallet_transaction_remark.sql new file mode 100644 index 00000000..ee5f5b73 --- /dev/null +++ b/db/20260825_add_wallet_transaction_remark.sql @@ -0,0 +1,3 @@ +-- 为每笔钱包流水增加可选业务备注;历史流水保持为空,前后端统一限制为 256 个字符。 +ALTER TABLE wallet_transaction + ADD COLUMN remark varchar(256) DEFAULT NULL COMMENT '流水备注,最多256个字符' AFTER tags; diff --git a/db/800pjcloudx_wallet.sql b/db/800pjcloudx_wallet.sql index c4583ce2..462e9448 100644 --- a/db/800pjcloudx_wallet.sql +++ b/db/800pjcloudx_wallet.sql @@ -456,6 +456,7 @@ CREATE TABLE wallet_transaction price int NOT NULL COMMENT '交易金额, 单位分', balance int NOT NULL COMMENT '余额, 单位分', tags varchar(512) DEFAULT NULL COMMENT '财务统计标签,多个wallet_transaction_tag字典值使用英文逗号分隔', + remark varchar(256) DEFAULT NULL COMMENT '流水备注,最多256个字符', create_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人', create_time datetime DEFAULT NULL COMMENT '创建时间', update_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '修改人', diff --git a/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 0a2829e9..a25062a1 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 @@ -56,6 +56,11 @@ public class HardOrderDto extends HardOrderEntity implements BaseIsIncludeTreeIn */ private List saleSourceCodes; + /** + * 商品名称模糊查询条件,仅供购物车商品统计 SQL 筛选使用,不对应订单表持久化字段。 + */ + private String productName; + private LocalDateTime[] refundTimeList; /** 预计使用日期范围。 */ 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 cfb4aeb2..96f057a1 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 @@ -3,23 +3,27 @@ package com.bsd.pjcloud.hard.api.vo.order; import lombok.Data; /** - * 食堂预约菜品导出聚合记录。 + * 食堂菜品销售三级导出的聚合记录。 * - *

同一模型承载商户部门聚合和商品聚合两种查询结果,商品聚合时额外填充商品字段;金额统一使用分。

+ *

同一模型承载日期餐段、商户部门和商品三种聚合查询结果,商品聚合时额外填充商品字段; + * 统计时间已按销售来源统一转换为业务发生日期,金额统一使用分。

*/ @Data public class HardCanteenProductApptExportRecord { - /** 预约使用日期,格式为 yyyy-MM-dd。 */ - private String apptDate; + /** 业务发生日期,格式为 yyyy-MM-dd;线下取支付日期,预约取核销日期。 */ + private String statisticsDate; /** 用餐时间段协议编码。 */ private String hourType; + /** 销售来源协议编码;为空表示当前筛选范围内的跨来源合计。 */ + private String saleSource; + /** 食堂商户主键。 */ private Long merchantId; - /** 预约用户所属部门主键。 */ + /** 购买用户所属部门主键。 */ private Long userDeptId; /** 商品主键;商户部门聚合记录为空。 */ @@ -28,15 +32,15 @@ public class HardCanteenProductApptExportRecord { /** 订单快照中的商品名称;商户部门聚合记录为空。 */ private String productName; - /** 去重预约人数。 */ + /** 当前统计层级内去重后的交易人数。 */ private Long payUserCount; - /** 去重预约次数。 */ + /** 当前统计层级内去重后的订单数量。 */ private Long timeCount; - /** 预约商品总数量。 */ + /** 有效商品总数量,预约来源已扣减退款完成数量。 */ private Long payCount; - /** 预约商品总价,单位为分。 */ + /** 有效商品总价,单位为分;预约来源已扣减退款完成金额。 */ private Long totalPrice; } diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/excel/HardCanteenProductApptExcelVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/excel/HardCanteenProductApptExcelVo.java index bdf91045..f83e31a4 100644 --- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/excel/HardCanteenProductApptExcelVo.java +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/excel/HardCanteenProductApptExcelVo.java @@ -9,32 +9,36 @@ import java.math.BigDecimal; import java.time.LocalDate; /** - * 食堂预约菜品统计自定义导出行。 + * 食堂菜品销售统计自定义导出行。 * - *

一组日期和餐段按汇总行、商户部门行、商品明细行组织;商品统计在同一商户部门块内按名称合并套餐展开后的明细, - * 不同层级只填写所属列,配合自定义合并处理器形成树形报表。

+ *

一个日期、餐段和来源口径按汇总行、商户部门行、商品明细行组织;来源口径包含筛选范围合计及各销售来源。 + * 商品统计在同一商户部门块内按名称合并套餐展开后的明细,不同层级只填写所属列,配合自定义合并处理器形成树形报表。

*/ @Data public class HardCanteenProductApptExcelVo { - /** 日期餐段汇总中的预约日期。 */ - @ExcelProperty({"日期餐段汇总", "预约时间"}) + /** 日期餐段汇总中的业务发生日期。 */ + @ExcelProperty({"日期餐段汇总", "统计日期"}) private LocalDate summaryDate; /** 日期餐段汇总中的餐段编码或字典名称。 */ - @ExcelProperty({"日期餐段汇总", "预约时间段"}) + @ExcelProperty({"日期餐段汇总", "餐段"}) private String summaryHourType; - /** 日期餐段汇总中的去重预约人数。 */ - @ExcelProperty({"日期餐段汇总", "预约人数"}) + /** 日期餐段汇总的来源口径,取值为筛选范围合计、线下购买或预约核销。 */ + @ExcelProperty({"日期餐段汇总", "来源口径"}) + private String summarySaleSourceName; + + /** 日期餐段汇总中的去重交易人数。 */ + @ExcelProperty({"日期餐段汇总", "交易人数"}) private Long summaryPayUserCount; - /** 日期餐段汇总中的预约次数。 */ - @ExcelProperty({"日期餐段汇总", "预约次数"}) + /** 日期餐段汇总中的交易次数。 */ + @ExcelProperty({"日期餐段汇总", "交易次数"}) private Long summaryTimeCount; /** 日期餐段汇总中的商品总数量。 */ - @ExcelProperty({"日期餐段汇总", "预约商品数量"}) + @ExcelProperty({"日期餐段汇总", "商品数量"}) private Long summaryPayCount; /** 日期餐段汇总中的总价,单位为元。 */ @@ -50,24 +54,24 @@ public class HardCanteenProductApptExcelVo { @ExcelProperty({"商户部门统计", "部门"}) private String departmentName; - /** 商户部门统计中的预约日期。 */ - @ExcelProperty({"商户部门统计", "预约时间"}) + /** 商户部门统计中的业务发生日期。 */ + @ExcelProperty({"商户部门统计", "统计日期"}) private LocalDate merchantDate; /** 商户部门统计中的餐段名称。 */ - @ExcelProperty({"商户部门统计", "预约时间段"}) + @ExcelProperty({"商户部门统计", "餐段"}) private String merchantHourType; - /** 商户部门统计中的去重预约人数。 */ - @ExcelProperty({"商户部门统计", "预约人数"}) + /** 商户部门统计中的去重交易人数。 */ + @ExcelProperty({"商户部门统计", "交易人数"}) private Long merchantPayUserCount; - /** 商户部门统计中的预约次数。 */ - @ExcelProperty({"商户部门统计", "预约次数"}) + /** 商户部门统计中的交易次数。 */ + @ExcelProperty({"商户部门统计", "交易次数"}) private Long merchantTimeCount; - /** 商户部门统计中的商品总数量。 */ - @ExcelProperty({"商户部门统计", "预约商品数量"}) + /** 商户部门统计中的有效商品总数量。 */ + @ExcelProperty({"商户部门统计", "商品数量"}) private Long merchantPayCount; /** 商户部门统计中的总价,单位为元。 */ @@ -92,7 +96,7 @@ public class HardCanteenProductApptExcelVo { @ExcelProperty({"商品统计", "商品名称"}) private String statisticProductName; - /** 商品统计中的商品总数量,按退款扣减及套餐展开后的有效明细累计。 */ + /** 商品统计中的商品总数量,预约按退款扣减,套餐按组成商品展开后的有效明细累计。 */ @ExcelProperty({"商品统计", "商品总数量"}) private Long statisticProductCount; @@ -101,7 +105,7 @@ public class HardCanteenProductApptExcelVo { @NumberFormat("#,##0.00") private BigDecimal statisticProductTotalPrice; - /** 导出行层级,仅供合并处理器识别,不写入 Excel。 */ + /** 导出行层级,仅供合并处理器和测试识别,不写入 Excel。 */ @ExcelIgnore private String rowLevel; } 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 b8c95bab..fa9811af 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 @@ -21,25 +21,25 @@ import java.util.HashMap; import java.util.Map; /** - * 食堂预约菜品统计导出合并处理器。 + * 食堂菜品销售统计三级导出合并处理器。 * - *

数据行通过各层级专属列识别:日期餐段行填写第 0 列,商户部门行填写第 6 列,商品行填写第 14 列; - * 第 17 至 19 列展示同一商户部门内按名称合并后的商品统计。 + *

数据行通过各层级专属列识别:日期餐段来源行填写第 0 列,商户部门行填写第 7 列,商品行填写第 15 列; + * 第 18 至 20 列展示同一商户部门内按名称合并后的商品统计。 * Easy Excel 使用流式工作簿,较早的数据行会在关闭工作簿前刷出内存,因此必须在逐行写出时记录分组边界并及时合并。

*/ public class HardCanteenProductApptExportMergeHandler implements RowWriteHandler, WorkbookWriteHandler { /** 日期餐段汇总列。 */ - private static final int[] SUMMARY_COLUMNS = {0, 1, 2, 3, 4, 5}; + private static final int[] SUMMARY_COLUMNS = {0, 1, 2, 3, 4, 5, 6}; /** 商户部门统计列。 */ - private static final int[] MERCHANT_COLUMNS = {6, 7, 8, 9, 10, 11, 12, 13}; + private static final int[] MERCHANT_COLUMNS = {7, 8, 9, 10, 11, 12, 13, 14}; /** Easy Excel 双层表头占用两行,数据从第三行开始。 */ private static final int DATA_START_ROW = 2; /** 导出列总数,用于逐行应用居中样式。 */ - private static final int COLUMN_COUNT = 20; + private static final int COLUMN_COUNT = 21; /** 表头字体大小,单位为磅;使用紧凑字号降低双层表头占用空间。 */ private static final short HEAD_FONT_SIZE = 10; @@ -57,7 +57,7 @@ public class HardCanteenProductApptExportMergeHandler implements RowWriteHandler * 各列宽度,单位为 Excel 字符宽度;常规统计列保持紧凑,商品名称列单独加宽以展示更多名称内容。 */ private static final int[] COLUMN_WIDTHS = { - 12, 11, 10, 10, 14, 11, 14, 14, 12, 11, 10, 10, 14, 11, 32, 9, 11, 32, 14, 12 + 12, 11, 16, 10, 10, 14, 11, 14, 14, 12, 11, 10, 10, 14, 11, 32, 9, 11, 32, 14, 12 }; /** 原样式索引到居中样式的缓存,避免每个数据单元格重复创建样式。 */ @@ -113,7 +113,7 @@ public class HardCanteenProductApptExportMergeHandler implements RowWriteHandler applyCenteredStyle(row, writeSheetHolder.getParentWriteWorkbookHolder()); int rowIndex = row.getRowNum(); boolean summaryRow = hasValue(row, 0); - boolean merchantRow = hasValue(row, 6); + boolean merchantRow = hasValue(row, 7); boolean newDateGroup = false; if (summaryRow) { String summaryDateKey = getCellValueKey(row, 0); @@ -260,7 +260,7 @@ public class HardCanteenProductApptExportMergeHandler implements RowWriteHandler CellStyle copiedStyle = holder.getWorkbook().createCellStyle(); copiedStyle.cloneStyleFrom(originalStyle); if (!(copiedStyle instanceof XSSFCellStyle xssfStyle)) { - throw new IllegalStateException("菜品预约导出仅支持 XLSX 彩色边框样式"); + throw new IllegalStateException("菜品销售统计导出仅支持 XLSX 彩色边框样式"); } XSSFColor color = new XSSFColor(DATE_GROUP_BORDER_COLORS[colorIndex]); if (leftBorder) { 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 6fcabb89..ef4e2303 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 @@ -69,14 +69,29 @@ public interface HardCarMapper extends PjcloudBaseMapper { Page productTotalByApptUseDate(Page page, @Param("dto") HardOrderDto dto, DataScope dataScope); - /** 按预约日期和餐段查询导出所需的日期餐段总计。 */ - List listApptExportSummary(@Param("dto") HardOrderDto dto); + /** + * 查询菜品销售三级导出的日期餐段汇总及来源拆分。 + * + * @param dto 统计日期、来源、餐段、食堂和部门筛选条件 + * @return 每个日期餐段的跨来源合计及各来源统计,交易人数在当前层级独立去重 + */ + List listSalesExportSummary(@Param("dto") HardOrderDto dto); - /** 按预约日期、餐段、商户和部门查询导出商户部门统计。 */ - List listApptExportMerchant(@Param("dto") HardOrderDto dto); + /** + * 查询菜品销售三级导出的商户部门汇总及来源拆分。 + * + * @param dto 统计日期、来源、餐段、食堂和部门筛选条件 + * @return 每个日期餐段下商户部门的跨来源合计及各来源统计 + */ + List listSalesExportMerchant(@Param("dto") HardOrderDto dto); - /** 按预约日期、餐段、商户、部门和商品查询导出商品明细。 */ - List listApptExportProduct(@Param("dto") HardOrderDto dto); + /** + * 查询菜品销售三级导出的商品汇总及来源拆分。 + * + * @param dto 统计日期、来源、餐段、食堂和部门筛选条件 + * @return 每个商户部门块下商品的跨来源合计及各来源统计 + */ + List listSalesExportProduct(@Param("dto") HardOrderDto dto); Page proudctQuery(Page page, @Param("dto") HardCarDto dto); 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 45518495..7502b74e 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 @@ -15,8 +15,7 @@ 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.HardCanteenDeptTotalExcelVo; -import com.bsd.pjcloud.hard.api.vo.order.excel.HardCanteenProductTotalExcelVo; +import com.bsd.pjcloud.hard.api.vo.order.excel.HardCanteenProductApptExcelVo; import java.util.List; import java.util.Map; @@ -69,21 +68,14 @@ public interface HardCanteenTotalService { List listByDept(HardOrderDto dto, PjcloudUser user, DataScope dataScope); /** - * 按页面相同筛选条件导出菜品销售统计。 + * 按页面筛选条件导出统一的菜品销售三级统计。 * - * @param dto 来源统计请求 - * @param dataScope 部门数据权限;为空时沿用控制器既有范围 - * @return 按日期、餐段和来源拆分的导出行 - */ - List exportProduct(HardOrderDto dto, DataScope dataScope); - - /** - * 按页面相同筛选条件导出部门销售统计。 + *

每个日期餐段同时生成当前来源范围的合计块以及各来源块,每个块继续按商户部门和商品展开; + * 菜品统计页与部门统计页共用该方法,避免两套导出口径再次分叉。

* - * @param dto 来源统计请求 - * @param user 当前登录用户,用于沿用商户范围查询 + * @param dto 统计日期、来源、餐段、食堂和部门筛选条件 * @param dataScope 部门数据权限;为空时沿用控制器既有范围 - * @return 按日期、食堂、来源、部门和餐段拆分的导出行 + * @return 日期餐段、商户部门、商品三级排列的导出行 */ - List exportDept(HardOrderDto dto, PjcloudUser user, DataScope dataScope); + List exportSales(HardOrderDto dto, DataScope dataScope); } diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCanteenTotalServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCanteenTotalServiceImpl.java index 9fc2cc23..b8656b81 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCanteenTotalServiceImpl.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCanteenTotalServiceImpl.java @@ -3,6 +3,7 @@ package com.bsd.pjcloud.hard.service.impl.order; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.bsd.pjcloud.admin.api.entity.SysDept; import com.bsd.pjcloud.admin.api.entity.SysDictItem; @@ -10,7 +11,10 @@ import com.bsd.pjcloud.admin.api.feign.RemoteDataScopeService; import com.bsd.pjcloud.admin.api.feign.RemoteDeptService; import com.bsd.pjcloud.admin.api.feign.RemoteDictService; import com.bsd.pjcloud.canteen.api.feign.RemoteCanteenCategoryService; +import com.bsd.pjcloud.canteen.api.feign.RemoteDailyMenuService; import com.bsd.pjcloud.canteen.entity.CanteenCategoryEntity; +import com.bsd.pjcloud.canteen.vo.app.DailyMenuAppVO; +import com.bsd.pjcloud.canteen.vo.app.OnlineSpuComboItemVO; import com.bsd.pjcloud.common.core.constant.enums.MerchantTypeEnum; import com.bsd.pjcloud.common.core.constant.enums.YesNoEnum; import com.bsd.pjcloud.common.core.util.CollectionUtils; @@ -24,14 +28,14 @@ import com.bsd.pjcloud.hard.api.dto.order.HardCarDto; import com.bsd.pjcloud.hard.api.dto.order.app.HardAppProductByCanteenDto; import com.bsd.pjcloud.hard.api.dto.order.app.HardAppProductByClassifyDto; import com.bsd.pjcloud.hard.api.dto.order.app.HardAppProductLikeTopDto; +import com.bsd.pjcloud.hard.api.vo.order.HardCanteenProductApptExportRecord; import com.bsd.pjcloud.hard.api.vo.order.HardCanteenProductTotalVo; import com.bsd.pjcloud.hard.api.dto.order.HardOrderDto; 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.HardCanteenDeptTotalExcelVo; -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.mapper.order.HardCarMapper; import com.bsd.pjcloud.hard.mapper.order.HardOrderMapper; import com.bsd.pjcloud.hard.service.HardCanteenTotalService; @@ -50,11 +54,14 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.temporal.TemporalAdjusters; import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; @@ -73,6 +80,9 @@ public class HardCanteenTotalServiceImpl implements HardCanteenTotalService { static private final String TAG = "食堂统计"; + /** 当前用户没有可见食堂时使用的不可命中主键,防止空集合被解释为不限制商户。 */ + private static final Long NO_VISIBLE_CANTEEN_ID = Long.MIN_VALUE; + private final HardOrderMapper hardOrderMapper; private final HardCarMapper hardCarMapper; @@ -81,6 +91,9 @@ public class HardCanteenTotalServiceImpl implements HardCanteenTotalService { private final RemoteDataScopeService dataScopeService; private final RemoteCanteenCategoryService canteenCategoryService; + /** 每日菜谱远程服务,用于在三级导出中按业务日期展开套餐组成商品。 */ + private final RemoteDailyMenuService remoteDailyMenuService; + /** 餐段字典远程服务,仅在生成统计导出文案时读取 daily_menu。 */ private final RemoteDictService remoteDictService; diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardCarMapper.xml b/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardCarMapper.xml index ed7348bd..1237b604 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardCarMapper.xml +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardCarMapper.xml @@ -640,138 +640,164 @@ order by DATE_FORMAT(o.appt_use_date, '%Y-%m-%d') desc, o.hour_type - - + with sale_stats as ( + select * from + ) + select export_rows.statisticsDate, + export_rows.hourType, + export_rows.saleSource, + export_rows.payUserCount, + export_rows.timeCount, + export_rows.payCount, + export_rows.totalPrice + from ( + select sale_stats.stat_date as statisticsDate, + sale_stats.hour_type as hourType, + null as saleSource, + count(distinct sale_stats.user_id) as payUserCount, + count(distinct sale_stats.order_id) as timeCount, + sum(sale_stats.product_count) as payCount, + sum(sale_stats.total_price) as totalPrice + from sale_stats + + + + group by sale_stats.stat_date, sale_stats.hour_type + union all + select sale_stats.stat_date as statisticsDate, + sale_stats.hour_type as hourType, + sale_stats.sale_source as saleSource, + count(distinct sale_stats.user_id) as payUserCount, + count(distinct sale_stats.order_id) as timeCount, + sum(sale_stats.product_count) as payCount, + sum(sale_stats.total_price) as totalPrice + from sale_stats + + + + group by sale_stats.stat_date, sale_stats.hour_type, sale_stats.sale_source + ) export_rows + order by export_rows.statisticsDate, + export_rows.hourType, + case when export_rows.saleSource is null then 0 + when export_rows.saleSource = 'OFFLINE_PURCHASE' then 1 else 2 end - - + with sale_stats as ( + select * from + ) + select export_rows.statisticsDate, + export_rows.hourType, + export_rows.saleSource, + export_rows.merchantId, + export_rows.userDeptId, + export_rows.payUserCount, + export_rows.timeCount, + export_rows.payCount, + export_rows.totalPrice + from ( + select sale_stats.stat_date as statisticsDate, + sale_stats.hour_type as hourType, + null as saleSource, + sale_stats.mch_id as merchantId, + sale_stats.user_dept_id as userDeptId, + count(distinct sale_stats.user_id) as payUserCount, + count(distinct sale_stats.order_id) as timeCount, + sum(sale_stats.product_count) as payCount, + sum(sale_stats.total_price) as totalPrice + from sale_stats + + + + group by sale_stats.stat_date, sale_stats.hour_type, sale_stats.mch_id, sale_stats.user_dept_id + union all + select sale_stats.stat_date as statisticsDate, + sale_stats.hour_type as hourType, + sale_stats.sale_source as saleSource, + sale_stats.mch_id as merchantId, + sale_stats.user_dept_id as userDeptId, + count(distinct sale_stats.user_id) as payUserCount, + count(distinct sale_stats.order_id) as timeCount, + sum(sale_stats.product_count) as payCount, + sum(sale_stats.total_price) as totalPrice + from sale_stats + + + + group by sale_stats.stat_date, sale_stats.hour_type, sale_stats.sale_source, + sale_stats.mch_id, sale_stats.user_dept_id + ) export_rows + order by export_rows.statisticsDate, + export_rows.hourType, + case when export_rows.saleSource is null then 0 + when export_rows.saleSource = 'OFFLINE_PURCHASE' then 1 else 2 end, + export_rows.merchantId, + export_rows.userDeptId - - + with sale_stats as ( + select * from + ) + select export_rows.statisticsDate, + export_rows.hourType, + export_rows.saleSource, + export_rows.merchantId, + export_rows.userDeptId, + export_rows.productId, + export_rows.productName, + export_rows.payCount, + export_rows.totalPrice + from ( + select sale_stats.stat_date as statisticsDate, + sale_stats.hour_type as hourType, + null as saleSource, + sale_stats.mch_id as merchantId, + sale_stats.user_dept_id as userDeptId, + sale_stats.product_id as productId, + sale_stats.product_name as productName, + sum(sale_stats.product_count) as payCount, + sum(sale_stats.total_price) as totalPrice + from sale_stats + + + + group by sale_stats.stat_date, sale_stats.hour_type, sale_stats.mch_id, sale_stats.user_dept_id, + sale_stats.product_id, sale_stats.product_name + union all + select sale_stats.stat_date as statisticsDate, + sale_stats.hour_type as hourType, + sale_stats.sale_source as saleSource, + sale_stats.mch_id as merchantId, + sale_stats.user_dept_id as userDeptId, + sale_stats.product_id as productId, + sale_stats.product_name as productName, + sum(sale_stats.product_count) as payCount, + sum(sale_stats.total_price) as totalPrice + from sale_stats + + + + group by sale_stats.stat_date, sale_stats.hour_type, sale_stats.sale_source, + sale_stats.mch_id, sale_stats.user_dept_id, sale_stats.product_id, sale_stats.product_name + ) export_rows + order by export_rows.statisticsDate, + export_rows.hourType, + case when export_rows.saleSource is null then 0 + when export_rows.saleSource = 'OFFLINE_PURCHASE' then 1 else 2 end, + export_rows.merchantId, + export_rows.userDeptId, + export_rows.productName -