diff --git a/db/20260825_add_third_mch_fz_relation.sql b/db/20260825_add_third_mch_fz_relation.sql index 251158679..6ffc39cc4 100644 --- a/db/20260825_add_third_mch_fz_relation.sql +++ b/db/20260825_add_third_mch_fz_relation.sql @@ -54,13 +54,29 @@ SET @relation_task_column_exists = ( ); 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`)', + 'ALTER TABLE `third_payment_task` ADD COLUMN `fz_relation_id` bigint DEFAULT NULL COMMENT ''商户分账关系主键'' AFTER `fz_order_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 @relation_task_index_exists = ( + SELECT COUNT(1) + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'third_payment_task' + AND INDEX_NAME = 'idx_third_payment_task_relation' +); +SET @relation_task_index_sql = IF( + @relation_task_index_exists = 0, + 'ALTER TABLE `third_payment_task` ADD INDEX `idx_third_payment_task_relation` (`tenant_id`, `fz_relation_id`)', + 'SELECT 1' +); +PREPARE relation_task_index_stmt FROM @relation_task_index_sql; +EXECUTE relation_task_index_stmt; +DEALLOCATE PREPARE relation_task_index_stmt; + -- 支付订单固定引用创建时使用的已绑定关系,后续分账不跟随平台接收账号配置漂移。 SET @pay_relation_column_exists = ( SELECT COUNT(1) @@ -71,9 +87,25 @@ SET @pay_relation_column_exists = ( ); 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`)', + 'ALTER TABLE `third_pay_order` ADD COLUMN `fz_relation_id` bigint DEFAULT NULL COMMENT ''支付创建时绑定的平台分佣关系主键'' AFTER `mch_pay_setting_json`', 'SELECT 1' ); PREPARE pay_relation_column_stmt FROM @pay_relation_column_sql; EXECUTE pay_relation_column_stmt; DEALLOCATE PREPARE pay_relation_column_stmt; + +SET @pay_relation_index_exists = ( + SELECT COUNT(1) + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'third_pay_order' + AND INDEX_NAME = 'idx_third_pay_order_fz_relation' +); +SET @pay_relation_index_sql = IF( + @pay_relation_index_exists = 0, + 'ALTER TABLE `third_pay_order` ADD INDEX `idx_third_pay_order_fz_relation` (`tenant_id`, `fz_relation_id`)', + 'SELECT 1' +); +PREPARE pay_relation_index_stmt FROM @pay_relation_index_sql; +EXECUTE pay_relation_index_stmt; +DEALLOCATE PREPARE pay_relation_index_stmt; 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 2eac5ae67..4ea972c02 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 52af6d62d..f15ab986b 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 37ec0c8fe..b2c852e45 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 28d6ec525..e90e5bf4e 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 10450a427..828d157ca 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 a25062a12..732377b4e 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 94e3622fc..5e62601a9 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,5 +1,8 @@ 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; @@ -7,7 +10,8 @@ import java.time.LocalDateTime; import java.util.List; @Data -public class HardSpuRepertoryDto extends HardSpuRepertoryEntity { +/** 硬件商品库存更新记录查询条件,支持按上架用户身份快速筛选。 */ +public class HardSpuRepertoryDto extends HardSpuRepertoryEntity implements BaseUserIdentityQueryInterface { private LocalDateTime[] createTimeList; @@ -15,4 +19,16 @@ public class HardSpuRepertoryDto extends HardSpuRepertoryEntity { 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/vo/HardAuthVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/HardAuthVo.java index 4baf063ea..30347ec5d 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 229679387..12d3a11eb 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 ec71266bb..f0974aa8b 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 0cd57feb6..77a0a1d12 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 49b3e4ada..e265efba8 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,19 @@ 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.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.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 BaseUserDeptInterface, BaseUserIdentityInterface, BaseDeptInterface, BaseOrderInterface { /** * 自动退款任务状态;为空表示当前售后仍走人工审核,非空时前端不得展示审核或取消操作。 */ @@ -40,8 +44,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 96f057a1f..843d866ec 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/HardOrderVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderVo.java index ffe257dd6..afa0bf539 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; @@ -28,6 +32,15 @@ public class HardOrderVo extends HardOrderEntity implements BaseUserDeptInterfac 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 052836be2..d9157f4b1 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,5 +1,9 @@ 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; @@ -7,7 +11,8 @@ import com.bsd.pjcloud.wallet.api.vo.base.BaseMchInterface; import lombok.Data; @Data -public class HardSpuRepertoryVo extends HardSpuRepertoryEntity implements BaseMchInterface, BaseUserInterface, BaseHardInterface { +/** 硬件商品库存更新记录展示对象,包含上架用户主资料、部门和身份信息。 */ +public class HardSpuRepertoryVo extends HardSpuRepertoryEntity implements BaseMchInterface, BaseUserInterface, BaseUserDeptInterface, BaseUserIdentityInterface, BaseHardInterface { /** * 商品相关 @@ -45,6 +50,52 @@ public class HardSpuRepertoryVo extends HardSpuRepertoryEntity implements BaseMc private String userAvatar; + /** 用户标准手机号字段,供统一身份组件展示;与历史 userMobile 字段并存。 */ + private String phone; + + /** 用户当前所属部门主键,由 UPMS 用户资料回填。 */ + private Long userDeptId; + + /** 用户当前所属部门名称,由 UPMS 用户资料回填。 */ + private String userDeptName; + + /** 用户所属部门展示名称,供用户信息单元格读取。 */ + private String deptName; + + /** 用户拥有的 UPMS 身份协议编码集合。 */ + private java.util.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; + } + /** * 设备相关 */ 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 260489129..b89696d75 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 0b5094857..1841b6cc1 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 d0ae09808..22ed964e1 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; @@ -150,17 +152,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/excel/HardCanteenProductApptExportMergeHandler.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/excel/HardCanteenProductApptExportMergeHandler.java index fa9811afe..7e2d5497a 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 16a988937..8f918f78e 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/HardCarMapper.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/mapper/order/HardCarMapper.java index ef4e23031..9a1e5485a 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/HardCanteenApptTotalExportService.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardCanteenApptTotalExportService.java index 53172794d..2ec1b5251 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 7502b74ea..ab70e8e0d 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/impl/HardAuthServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/HardAuthServiceImpl.java index ee9821deb..c0fd9052a 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 eb2a746f2..301eeb9bc 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菜品导出以预约使用日期为批次边界,每完成一个日期就推进一次 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> response = remoteDeptService.getDeptByDeptIds(ids); - if (response == null || !response.isOk() || response.getData() == null) return Map.of(); - return response.getData().values().stream().filter(item -> item.getDeptId() != null) - .collect(Collectors.toMap(SysDept::getDeptId, SysDept::getName, (left, right) -> left)); - } catch (Exception exception) { - log.warn("查询导出部门名称失败,使用空名称", exception); - return Map.of(); - } - } - - /** 查询餐段字典。 */ - private Map fetchHourNames() { - try { - R> response = remoteDictService.getDictByType(MEAL_SEGMENT_DICT_TYPE); - if (response == null || !response.isOk() || response.getData() == null) return Map.of(); - return response.getData().stream().filter(item -> StrUtil.isNotBlank(item.getItemValue())) - .collect(Collectors.toMap(SysDictItem::getItemValue, SysDictItem::getLabel, (left, right) -> left)); - } catch (Exception exception) { - log.warn("查询导出餐段字典失败,使用餐段编码", exception); - return Map.of(); - } - } - - /** 复制菜品统计查询条件,避免异步任务与请求对象共享可变数组和集合。 */ - private HardOrderDto copyProductQuery(HardOrderDto source) { - if (source == null) throw new IllegalArgumentException("菜品预约导出查询条件不能为空"); - HardOrderDto target = new HardOrderDto(); - target.setApptUseDateTimeList(source.getApptUseDateTimeList() == null ? null : source.getApptUseDateTimeList().clone()); - target.setHourType(source.getHourType()); - target.setMerchantId(source.getMerchantId()); - target.setUserDeptIds(copyList(source.getUserDeptIds())); - target.setIsInclude(source.getIsInclude()); - target.setMerchantDeptIds(copyList(source.getMerchantDeptIds())); - target.setMchIds(copyList(source.getMchIds())); - target.setOrderType(HardOrderTypeEnum.RESERVE.getType()); - target.setApptIdOrApptRuleIdNotNull(true); - target.setOrderStatusCodes(HardOrderStatusEnum.getPaySuccessCodes()); - return target; - } - - /** 获取查询日期闭区间。 */ - private List getQueryDates(HardOrderDto query) { - if (query.getApptUseDateTimeList() == null || query.getApptUseDateTimeList().length != 2 - || query.getApptUseDateTimeList()[0] == null || query.getApptUseDateTimeList()[1] == null) { - throw new IllegalArgumentException("预约时间不能为空"); - } - LocalDate start = query.getApptUseDateTimeList()[0].toLocalDate(); - LocalDate end = query.getApptUseDateTimeList()[1].toLocalDate(); - if (end.isBefore(start)) throw new IllegalArgumentException("预约结束时间不能早于开始时间"); - List dates = new ArrayList<>(); - for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) dates.add(date); - return dates; - } - - /** 构造商户部门分组键。 */ - private String groupKey(HardCanteenProductApptExportRecord record) { - return record.getApptDate() + "|" + record.getHourType() + "|" + record.getMerchantId() + "|" + record.getUserDeptId(); - } - - /** 构造日期餐段分组键,用于从日期餐段汇总行定位其下属商户部门统计。 */ - private String summaryGroupKey(HardCanteenProductApptExportRecord record) { - return record.getApptDate() + "|" + record.getHourType(); - } - - /** 构造商品每日菜谱缓存键。 */ - private String menuKey(HardCanteenProductApptExportRecord record, LocalDate date) { - return record.getMerchantId() + "|" + date + "|" + record.getHourType() + "|" + record.getProductId(); - } - - /** 将空数值按零处理。 */ - private long valueOrZero(Number value) { - return value == null ? 0L : value.longValue(); - } - - /** 将分转换为元,保持两位小数。 */ - private BigDecimal centsToYuan(Number cents) { - return BigDecimal.valueOf(valueOrZero(cents)).movePointLeft(2).setScale(2, RoundingMode.HALF_UP); - } - - /** - * 将商品明细展示金额从元转换回分,用于按最终商品行重新计算日期餐段和部门合计。 - * - *

商品行已经按两位小数输出,因此这里使用半舍入恢复整数分,避免直接使用 double 导致合计出现尾差。

- * - * @param yuan 商品明细展示金额,单位为元;为空按零处理 - * @return 金额,单位为分 - */ - private long yuanToCents(BigDecimal yuan) { - return yuan == null ? 0L : yuan.movePointRight(2).setScale(0, RoundingMode.HALF_UP).longValue(); - } - - /** 解析餐段名称,字典缺失时保留原始编码。 */ - private String resolveHourName(String hourType, Map hourNames) { - return StrUtil.blankToDefault(hourNames.get(hourType), StrUtil.blankToDefault(hourType, "未标记餐段")); - } - - /** 上传已关闭的 Excel 临时文件并提取下载地址。 */ - private String upload(Path path, String fileName) throws Exception { - MultipartFile file = new MockMultipartFile("file", encodeFileName(fileName), EXCEL_CONTENT_TYPE, - Files.readAllBytes(path)); - R response = remoteFileService.innerUpload(file, EXPORT_FILE_DIR, null, PRODUCT_EXPORT_FILE_TYPE); - if (response == null || !response.isOk() || !(response.getData() instanceof Map data) - || data.get("url") == null || StrUtil.isBlank(data.get("url").toString())) { - throw new IllegalStateException("文件中心未返回菜品预约导出下载地址"); - } - return data.get("url").toString(); - } - - /** 按文件中心约定编码中文文件名。 */ - private String encodeFileName(String fileName) { - return new String(fileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1); - } - } 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 b8656b814..43bc32871 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 @@ -4,12 +4,16 @@ 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.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.alibaba.excel.write.metadata.WriteSheet; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.bsd.pjcloud.admin.api.entity.SysDept; import com.bsd.pjcloud.admin.api.entity.SysDictItem; 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.admin.api.feign.RemoteFileService; import com.bsd.pjcloud.canteen.api.feign.RemoteCanteenCategoryService; import com.bsd.pjcloud.canteen.api.feign.RemoteDailyMenuService; import com.bsd.pjcloud.canteen.entity.CanteenCategoryEntity; @@ -18,7 +22,13 @@ 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; +import com.bsd.pjcloud.common.core.util.R; +import com.bsd.pjcloud.common.asynctask.service.AsyncTaskExecutorService; +import com.bsd.pjcloud.common.asynctask.service.AsyncTaskProgressReporter; +import com.bsd.pjcloud.common.asynctask.service.AsyncTaskResult; +import com.bsd.pjcloud.common.asynctask.vo.AsyncTaskProgressVO; import com.bsd.pjcloud.common.data.datascope.DataScope; +import com.bsd.pjcloud.common.excel.stream.BatchExcelExportTemplate; import com.bsd.pjcloud.common.security.service.PjcloudUser; import com.bsd.pjcloud.hard.api.constant.order.HardCanteenStatisticsLevelEnum; import com.bsd.pjcloud.hard.api.constant.order.HardOrderStatusEnum; @@ -36,6 +46,7 @@ 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.HardCanteenProductApptExcelVo; +import com.bsd.pjcloud.hard.excel.HardCanteenProductApptExportMergeHandler; import com.bsd.pjcloud.hard.mapper.order.HardCarMapper; import com.bsd.pjcloud.hard.mapper.order.HardOrderMapper; import com.bsd.pjcloud.hard.service.HardCanteenTotalService; @@ -44,14 +55,20 @@ import com.bsd.pjcloud.wallet.api.feign.RemoteMchService; import com.bsd.pjcloud.wallet.api.vo.MchManageVo; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; +import org.springframework.web.multipart.MultipartFile; import java.math.BigDecimal; import java.math.RoundingMode; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.DayOfWeek; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalAdjusters; import java.util.ArrayList; import java.util.Comparator; @@ -83,6 +100,60 @@ public class HardCanteenTotalServiceImpl implements HardCanteenTotalService { /** 当前用户没有可见食堂时使用的不可命中主键,防止空集合被解释为不限制商户。 */ private static final Long NO_VISIBLE_CANTEEN_ID = Long.MIN_VALUE; + /** 菜品统计异步导出的 Redis 业务命名空间。 */ + private static final String PRODUCT_SALES_EXPORT_REDIS_KEY = "hard:canteen-sales:export:product"; + + /** 部门统计异步导出的 Redis 业务命名空间。 */ + private static final String DEPARTMENT_SALES_EXPORT_REDIS_KEY = "hard:canteen-sales:export:department"; + + /** 菜品统计异步导出任务展示名称。 */ + private static final String PRODUCT_SALES_EXPORT_TASK_NAME = "食堂菜品销售统计导出"; + + /** 部门统计异步导出任务展示名称。 */ + private static final String DEPARTMENT_SALES_EXPORT_TASK_NAME = "食堂菜品部门统计导出"; + + /** 异步导出提交时使用的占位总数,后台开始后会替换为实际日期批次数。 */ + private static final int SALES_EXPORT_INITIAL_TOTAL = 1; + + /** 流式写完日期批次后的工作簿收尾和文件上传进度步数。 */ + private static final int SALES_EXPORT_EXTRA_PROGRESS_STEPS = 2; + + /** 三级统计 Excel 的双层表头行数。 */ + private static final int SALES_EXPORT_HEADER_ROW_COUNT = 2; + + /** 单个三级统计工作表允许写入的最大业务行数。 */ + private static final int MAX_SALES_EXPORT_ROWS = + BatchExcelExportTemplate.calculateMaxDetailRowCount(SALES_EXPORT_HEADER_ROW_COUNT, 0); + + /** 统一销售统计异步导出的工作表名称。 */ + private static final String SALES_EXPORT_SHEET_NAME = "食堂销售统计"; + + /** 统一销售统计异步导出的文件中心目录。 */ + private static final String SALES_EXPORT_FILE_DIR = "hard-export"; + + /** 菜品统计导出文件的文件中心业务类型。 */ + private static final String PRODUCT_SALES_EXPORT_FILE_TYPE = "HARD_CANTEEN_PRODUCT_SALES_EXPORT"; + + /** 部门统计导出文件的文件中心业务类型。 */ + private static final String DEPARTMENT_SALES_EXPORT_FILE_TYPE = "HARD_CANTEEN_DEPARTMENT_SALES_EXPORT"; + + /** 导出聚合记录中的日期餐段汇总层级编码,必须与 Mapper SQL 返回值一致。 */ + private static final String SALES_EXPORT_RECORD_LEVEL_SUMMARY = "SUMMARY"; + + /** 导出聚合记录中的商户部门层级编码,必须与 Mapper SQL 返回值一致。 */ + private static final String SALES_EXPORT_RECORD_LEVEL_MERCHANT = "MERCHANT"; + + /** 导出聚合记录中的商品层级编码,必须与 Mapper SQL 返回值一致。 */ + private static final String SALES_EXPORT_RECORD_LEVEL_PRODUCT = "PRODUCT"; + + /** XLSX 文件的标准 MIME 类型。 */ + private static final String SALES_EXPORT_EXCEL_CONTENT_TYPE = + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + + /** 异步导出文件名使用的时间格式,保证同一用户连续导出时文件名可区分。 */ + private static final DateTimeFormatter SALES_EXPORT_FILE_TIME_FORMATTER = + DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + private final HardOrderMapper hardOrderMapper; private final HardCarMapper hardCarMapper; @@ -97,6 +168,12 @@ public class HardCanteenTotalServiceImpl implements HardCanteenTotalService { /** 餐段字典远程服务,仅在生成统计导出文案时读取 daily_menu。 */ private final RemoteDictService remoteDictService; + /** 文件中心远程服务,用于保存异步导出完成后的 XLSX 文件。 */ + private final RemoteFileService remoteFileService; + + /** 通用异步任务执行器,用于恢复租户和用户上下文并持久化导出进度。 */ + private final AsyncTaskExecutorService asyncTaskExecutorService; + /** * 菜品销量按照分类查询 */ @@ -381,46 +458,31 @@ public class HardCanteenTotalServiceImpl implements HardCanteenTotalService { ? assembleDepartmentMainRows(mapperRows, includeSourceRows) : mapperRows; fillSaleSourceFields(list); - if (ObjectUtil.isNotEmpty(list) && ObjectUtil.isNotEmpty(dto.getTotalIsFindChildren()) && dto.getTotalIsFindChildren()) { - // 限制最大线程数为10 - int threadCount = Math.min(list.size(), 10); - ExecutorService executorService = threadCount > 0 ? Executors.newFixedThreadPool(threadCount) : null; - try { - List> futures = list.stream() - .map(v -> CompletableFuture.runAsync(() -> { - // 查询时间段 - HardOrderDto deptQuery = BeanUtil.toBean(dto, HardOrderDto.class); - deptQuery.setMerchantId(v.getMchId()); - if (sourceStatistics) { - deptQuery.setCreateTimeStr(v.getCreateTime() == null ? null : v.getCreateTime().toLocalDate().toString()); - deptQuery.setOrderApptUseDateStr(null); - } - List deptMapperRows = sourceStatistics - ? hardCarMapper.listByHourTypeStats(deptQuery) - : hardCarMapper.listByHourType(deptQuery); - List deptList = sourceStatistics - ? assembleDepartmentChildRows(deptMapperRows, v.getMchId(), includeSourceRows) - : deptMapperRows; - - for (HardCanteenProductTotalVo j : deptList) { - if (ObjectUtil.isNotEmpty(v.getMchId())) j.setMchId(v.getMchId()); - fillSaleSourceFields(j); - } - - if (!sourceStatistics) { - v.setDeptCount(deptList.stream().map(HardCanteenProductTotalVo::getUserDeptId).distinct().toList().size()); - } - v.setChildren(deptList); - if (sourceStatistics && includeSourceRows) { - for (HardCanteenProductTotalVo sourceRow : v.getSourceRows()) { - sourceRow.setChildren(departmentSourceChildren(deptList, sourceRow.getSaleSource())); + if (ObjectUtil.isNotEmpty(list) && Boolean.TRUE.equals(dto.getTotalIsFindChildren())) { + if (sourceStatistics) { + populateDepartmentStatisticsChildren(dto, list, includeSourceRows); + } else { + // 旧接口仍保留原有并发查询方式,避免无统计日期的历史调用发生结构变化。 + int threadCount = Math.min(list.size(), 10); + ExecutorService executorService = threadCount > 0 ? Executors.newFixedThreadPool(threadCount) : null; + try { + List> futures = list.stream() + .map(v -> CompletableFuture.runAsync(() -> { + HardOrderDto deptQuery = BeanUtil.toBean(dto, HardOrderDto.class); + deptQuery.setMerchantId(v.getMchId()); + List deptList = hardCarMapper.listByHourType(deptQuery); + for (HardCanteenProductTotalVo child : deptList) { + if (ObjectUtil.isNotEmpty(v.getMchId())) child.setMchId(v.getMchId()); } - } - }, executorService)) - .toList(); - CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); - } finally { - if (executorService != null) executorService.shutdown(); + v.setDeptCount(deptList.stream().map(HardCanteenProductTotalVo::getUserDeptId) + .distinct().toList().size()); + v.setChildren(deptList); + }, executorService)) + .toList(); + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + } finally { + if (executorService != null) executorService.shutdown(); + } } } @@ -490,6 +552,37 @@ public class HardCanteenTotalServiceImpl implements HardCanteenTotalService { .toList(); } + /** + * 一次批量查询全部日期和食堂的部门餐段行,再按主表维度装配展开数据。 + * + *

旧实现按每个“日期 + 食堂”主行执行一次 SQL,日期或食堂增多时会形成 N+1;新版 SQL 已返回 + * 食堂主键,因此可在相同筛选范围内一次聚合后按稳定键拆分,同时保持来源行和合计行的独立去重口径。

+ * + * @param dto 已补全可见食堂、来源和统计日期的查询条件 + * @param mainRows 日期食堂合计主行,每行可能挂载来源拆分 + * @param includeSourceRows 是否为合计行挂载来源拆分;单来源标签页为 false + */ + private void populateDepartmentStatisticsChildren(HardOrderDto dto, + List mainRows, + boolean includeSourceRows) { + List allChildRows = hardCarMapper.listByHourTypeStats(dto); + Map> childRowsByMainScope = ObjectUtil.isEmpty(allChildRows) + ? Map.of() + : allChildRows.stream().collect(Collectors.groupingBy(this::departmentMainScopeKey, + LinkedHashMap::new, Collectors.toList())); + for (HardCanteenProductTotalVo mainRow : mainRows) { + List deptList = assembleDepartmentChildRows( + childRowsByMainScope.getOrDefault(departmentMainScopeKey(mainRow), List.of()), + mainRow.getMchId(), includeSourceRows); + mainRow.setChildren(deptList); + if (includeSourceRows) { + for (HardCanteenProductTotalVo sourceRow : mainRow.getSourceRows()) { + sourceRow.setChildren(departmentSourceChildren(deptList, sourceRow.getSaleSource())); + } + } + } + } + /** * 从餐段合计行中提取指定来源的部门餐段行,供主表来源行展开时复用同一批查询结果。 * @@ -580,72 +673,530 @@ public class HardCanteenTotalServiceImpl implements HardCanteenTotalService { } /** - * 导出菜品统计,直接复用来源统计 SQL,确保导出与主表的业务发生时间口径一致。 + * 导出日期餐段、商户部门和商品三级销售统计。 * - * @param dto 来源统计请求 - * @param dataScope 部门数据权限 - * @return Excel 导出行 + *

每个日期餐段先输出当前来源筛选的跨来源合计块,再输出各已选来源块;合计人数由数据库独立去重, + * 不能通过来源人数相加得到。两个统计页面调用同一实现,因此筛选、退款扣减、套餐展开和合计规则保持一致。

+ * + * @param dto 统计日期、来源、餐段、食堂和部门筛选条件 + * @param dataScope 数据权限上下文,继续透传给 Mapper 拦截器 + * @return 按日期餐段、来源口径、商户部门和商品顺序排列的 Excel 行 */ @Override - public List exportProduct(HardOrderDto dto, DataScope dataScope) { - if (!isSourceStatistics(dto)) { - throw new IllegalArgumentException("导出必须选择统计日期范围"); + public List exportSales(HardOrderDto dto, DataScope dataScope) { + HardOrderDto exportQuery = prepareSalesExportQuery(dto); + List rows = new ArrayList<>(); + for (LocalDate date : getExportDates(exportQuery)) { + HardOrderDto dateQuery = createExportDateQuery(exportQuery, date); + rows.addAll(buildSalesExportRows(dateQuery, date, dataScope)); } - normalizeSaleSources(dto); - List rows = hardCarMapper.listProductTotalStats(dto, dataScope).stream() - .filter(row -> ObjectUtil.equals(row.getStatisticsLevel(), HardCanteenStatisticsLevelEnum.HOUR_SOURCE.getCode())) - .toList(); - fillSaleSourceFields(rows); - Map hourTypeNames = getHourTypeNames(); - if (ObjectUtil.isEmpty(rows)) return List.of(); - return rows.stream().map(row -> { - HardCanteenProductTotalExcelVo excel = new HardCanteenProductTotalExcelVo(); - excel.setStatisticsDate(row.getCreateTime() == null ? null : row.getCreateTime().toLocalDate()); - excel.setHourTypeName(hourTypeNames.getOrDefault(row.getHourType(), row.getHourType())); - excel.setSaleSourceName(row.getSaleSourceName()); - excel.setPayUserCount(row.getPayUserCount()); - excel.setTimeCount(row.getTimeCount()); - excel.setPayCount(row.getPayCount()); - excel.setTotalPrice(centsToYuan(row.getTotalPrice())); - return excel; - }).toList(); + return rows; } /** - * 导出部门统计,复用部门主表及子行装配结果后扁平化为 Excel 行。 + * 提交菜品统计异步分批导出任务。 * - * @param dto 来源统计请求 - * @param user 当前登录用户 - * @param dataScope 部门数据权限 - * @return Excel 导出行 + * @param dto 页面当前的统计日期、来源、餐段、食堂和部门筛选条件 + * @return 可供页面轮询的初始任务进度 */ @Override - public List exportDept(HardOrderDto dto, PjcloudUser user, DataScope dataScope) { + public AsyncTaskProgressVO submitProductSalesExport(HardOrderDto dto) { + return submitSalesExport(dto, PRODUCT_SALES_EXPORT_REDIS_KEY, PRODUCT_SALES_EXPORT_TASK_NAME, + "食堂菜品销售统计", PRODUCT_SALES_EXPORT_FILE_TYPE); + } + + /** + * 提交部门统计异步分批导出任务。 + * + * @param dto 页面当前的统计日期、来源、餐段、食堂和部门筛选条件 + * @return 可供页面轮询的初始任务进度 + */ + @Override + public AsyncTaskProgressVO submitDepartmentSalesExport(HardOrderDto dto) { + return submitSalesExport(dto, DEPARTMENT_SALES_EXPORT_REDIS_KEY, DEPARTMENT_SALES_EXPORT_TASK_NAME, + "食堂菜品部门统计", DEPARTMENT_SALES_EXPORT_FILE_TYPE); + } + + /** + * 冻结请求快照和当前用户可见食堂后提交统一销售统计导出任务。 + * + *

权限范围在请求线程完成收敛,后台线程只读取独立 DTO,避免调用方继续修改数组或列表导致导出条件漂移。

+ * + * @param dto 页面提交的销售统计筛选条件 + * @param redisKey 当前导出入口独立使用的 Redis 业务命名空间 + * @param taskName 进度弹窗展示的任务名称 + * @param fileNamePrefix 下载文件名称前缀 + * @param fileType 文件中心保存的业务类型编码 + * @return 已创建的初始任务进度 + */ + private AsyncTaskProgressVO submitSalesExport(HardOrderDto dto, String redisKey, String taskName, + String fileNamePrefix, String fileType) { if (!isSourceStatistics(dto)) { throw new IllegalArgumentException("导出必须选择统计日期范围"); } - dto.setTotalIsFindChildren(true); - List rows = listByDept(dto, user, dataScope); - if (ObjectUtil.isEmpty(rows)) return List.of(); - Map hourTypeNames = getHourTypeNames(); - List result = new ArrayList<>(); - for (HardCanteenProductTotalVo parent : rows) { - if (ObjectUtil.isEmpty(parent.getChildren())) continue; - for (HardCanteenProductTotalVo child : parent.getChildren()) { - List exportRows = ObjectUtil.isNotEmpty(child.getSourceRows()) - ? child.getSourceRows() : List.of(child); - for (HardCanteenProductTotalVo exportRow : exportRows) { - HardCanteenDeptTotalExcelVo excel = new HardCanteenDeptTotalExcelVo(); - excel.setStatisticsDate(parent.getCreateTime() == null ? null : parent.getCreateTime().toLocalDate()); - excel.setMerchantName(parent.getMchName()); - excel.setSaleSourceName(resolveDepartmentExportSourceName(exportRow, dto)); - excel.setUserDeptName(exportRow.getUserDeptName()); - excel.setHourTypeName(hourTypeNames.getOrDefault(exportRow.getHourType(), exportRow.getHourType())); - excel.setPayUserCount(exportRow.getPayUserCount()); - excel.setTimeCount(exportRow.getTimeCount()); - excel.setPayCount(exportRow.getPayCount()); - excel.setTotalPrice(centsToYuan(exportRow.getTotalPrice())); - result.add(excel); + HardOrderDto querySnapshot = prepareSalesExportQuery(copyExportQuery(dto)); + return asyncTaskExecutorService.submitWithResult(redisKey, taskName, SALES_EXPORT_INITIAL_TOTAL, + reporter -> exportSalesToFile(querySnapshot, fileNamePrefix, fileType, reporter)); + } + + /** + * 校验并复制导出查询,同时把当前用户可见食堂范围写入独立快照。 + * + * @param source 控制器接收的原始筛选条件 + * @return 已规范化来源并冻结食堂权限的独立查询快照;传入对象继续保留同步接口原有的条件补全行为 + */ + private HardOrderDto prepareSalesExportQuery(HardOrderDto source) { + if (!isSourceStatistics(source)) { + throw new IllegalArgumentException("导出必须选择统计日期范围"); + } + normalizeSaleSources(source); + applyExportMerchantScope(source); + return copyExportQuery(source); + } + + /** + * 在异步线程中按自然日查询并流式写入三级统计 Excel,完成后上传文件中心。 + * + *

每个自然日完成 SQL 聚合后立即写盘并释放当日行,日期跨度增大时 JVM 不会同时持有全部导出结果。 + * 进度总数由日期批次数、工作簿收尾和文件上传三部分组成。

+ * + * @param query 已冻结权限范围的销售统计查询 + * @param fileNamePrefix 下载文件名称前缀 + * @param fileType 文件中心业务类型编码 + * @param reporter 当前异步任务进度报告器 + * @return 文件中心下载地址和业务文件名 + */ + private AsyncTaskResult exportSalesToFile(HardOrderDto query, String fileNamePrefix, String fileType, + AsyncTaskProgressReporter reporter) { + List dates = getExportDates(query); + reporter.updateTotal(dates.size() + SALES_EXPORT_EXTRA_PROGRESS_STEPS, + "已拆分为 " + dates.size() + " 个日期批次,正在生成统计文件"); + String fileName = fileNamePrefix + "_" + + LocalDateTime.now().format(SALES_EXPORT_FILE_TIME_FORMATTER) + ".xlsx"; + Path outputPath = null; + try { + outputPath = Files.createTempFile("canteen-sales-export-", ".xlsx"); + long exportedRows = writeSalesExportWorkbook(outputPath, query, dates, reporter); + reporter.advance("统计文件已完成写入,共 " + exportedRows + " 行,正在上传文件中心"); + String fileUrl = uploadSalesExportWorkbook(outputPath, fileName, fileType); + reporter.advance("统计文件已上传,共 " + exportedRows + " 行"); + return AsyncTaskResult.file(fileUrl, fileName); + } catch (IllegalArgumentException | IllegalStateException exception) { + throw exception; + } catch (Exception exception) { + log.error("|{}|异步导出生成或上传失败,fileName:{}", TAG, fileName, exception); + throw new IllegalStateException("生成食堂销售统计导出文件失败", exception); + } finally { + deleteTemporarySalesExportFile(outputPath); + } + } + + /** + * 按自然日批次查询三级统计行并写入同一个流式工作簿。 + * + * @param outputPath 上传前使用的临时 XLSX 文件 + * @param query 已冻结的完整筛选条件 + * @param dates 从开始日期到结束日期的有序自然日列表 + * @param reporter 当前任务进度报告器 + * @return 实际写入的业务行总数 + */ + private long writeSalesExportWorkbook(Path outputPath, HardOrderDto query, List dates, + AsyncTaskProgressReporter reporter) { + long exportedRows = 0L; + try (ExcelWriter excelWriter = EasyExcel.write(outputPath.toFile(), HardCanteenProductApptExcelVo.class) + .registerWriteHandler(new HardCanteenProductApptExportMergeHandler()).build()) { + WriteSheet writeSheet = EasyExcel.writerSheet(SALES_EXPORT_SHEET_NAME).build(); + for (int index = 0; index < dates.size(); index++) { + LocalDate date = dates.get(index); + HardOrderDto dateQuery = createExportDateQuery(query, date); + List dateRows = buildSalesExportRows(dateQuery, date, null); + long nextExportedRows = exportedRows + dateRows.size(); + if (nextExportedRows > MAX_SALES_EXPORT_ROWS) { + throw new IllegalStateException("导出数据超过单个 Excel 工作表支持的 " + + MAX_SALES_EXPORT_ROWS + " 行,请缩小统计日期或筛选范围后重试"); + } + if (!dateRows.isEmpty()) { + excelWriter.write(dateRows, writeSheet); + } + exportedRows = nextExportedRows; + reporter.advance("已处理 " + (index + 1) + " / " + dates.size() + " 个日期批次,已写入 " + + exportedRows + " 行"); + } + } + if (exportedRows == 0L) { + throw new IllegalStateException("当前筛选条件没有可导出的食堂销售统计数据"); + } + return exportedRows; + } + + /** + * 创建单个自然日的查询副本,并保留用户在首尾日期选择的具体时间边界。 + * + * @param query 已冻结的完整日期范围查询 + * @param date 当前待处理的自然日 + * @return 日期范围已收敛到当前自然日的独立查询 + */ + private HardOrderDto createExportDateQuery(HardOrderDto query, LocalDate date) { + HardOrderDto dateQuery = copyExportQuery(query); + LocalDateTime rangeStart = query.getStatisticsDateTimeList()[0]; + LocalDateTime rangeEnd = query.getStatisticsDateTimeList()[1]; + LocalDateTime dayStart = date.atStartOfDay(); + LocalDateTime dayEnd = date.atTime(23, 59, 59); + dateQuery.setStatisticsDateTimeList(new LocalDateTime[]{ + rangeStart.isAfter(dayStart) ? rangeStart : dayStart, + rangeEnd.isBefore(dayEnd) ? rangeEnd : dayEnd + }); + return dateQuery; + } + + /** + * 将已关闭的完整 XLSX 临时文件上传到统一文件中心。 + * + * @param outputPath 已完成流式写入的临时文件 + * @param fileName 用户下载时使用的 UTF-8 文件名 + * @param fileType 文件中心业务类型编码 + * @return 文件中心返回的下载地址 + * @throws Exception 文件读取或远程上传失败时抛出 + */ + private String uploadSalesExportWorkbook(Path outputPath, String fileName, String fileType) throws Exception { + MultipartFile exportFile = new MockMultipartFile("file", encodeSalesExportFileName(fileName), + SALES_EXPORT_EXCEL_CONTENT_TYPE, Files.readAllBytes(outputPath)); + R uploadResult = remoteFileService.innerUpload(exportFile, SALES_EXPORT_FILE_DIR, null, fileType); + if (uploadResult == null || !uploadResult.isOk() + || !(uploadResult.getData() instanceof Map resultData)) { + throw new IllegalStateException("文件中心未返回有效的食堂销售统计导出结果"); + } + Object resultUrl = resultData.get("url"); + if (resultUrl == null || StrUtil.isBlank(resultUrl.toString())) { + throw new IllegalStateException("文件中心未返回食堂销售统计导出下载地址"); + } + return resultUrl.toString(); + } + + /** + * 按文件服务现有编码约定转换中文原始文件名。 + * + * @param fileName 用户看到的 UTF-8 文件名 + * @return 文件中心上传接口使用的原始文件名 + */ + private String encodeSalesExportFileName(String fileName) { + return new String(fileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1); + } + + /** + * 清理异步导出的临时文件,清理失败只记录告警而不覆盖任务的原始结果。 + * + * @param outputPath 待删除的临时文件路径,允许为空 + */ + private void deleteTemporarySalesExportFile(Path outputPath) { + if (outputPath == null) { + return; + } + try { + Files.deleteIfExists(outputPath); + } catch (Exception exception) { + log.warn("|{}|清理销售统计导出临时文件失败,path:{}", TAG, outputPath, exception); + } + } + + /** + * 将当前用户可见食堂范围冻结到导出请求中。 + * + *

调用方传入的食堂也必须与可见范围取交集;无可见食堂时使用不可命中主键,避免空集合被 SQL + * 解释为“不限制食堂”。

+ * + * @param dto 待补充食堂权限范围的导出请求 + */ + private void applyExportMerchantScope(HardOrderDto dto) { + MchManageDto scopeQuery = new MchManageDto(); + scopeQuery.setTypesList(List.of(MerchantTypeEnum.CANTEEN.getType())); + List visibleMerchants = remoteMchService.scopeList(scopeQuery).getData(); + List visibleIds = ObjectUtil.isEmpty(visibleMerchants) ? List.of() + : visibleMerchants.stream().map(MchManageVo::getId).filter(Objects::nonNull).distinct().toList(); + if (visibleIds.isEmpty()) { + dto.setMchIds(List.of(NO_VISIBLE_CANTEEN_ID)); + return; + } + if (dto.getMerchantId() != null && !visibleIds.contains(dto.getMerchantId())) { + dto.setMchIds(List.of(NO_VISIBLE_CANTEEN_ID)); + return; + } + if (ObjectUtil.isEmpty(dto.getMchIds())) { + dto.setMchIds(visibleIds); + return; + } + List allowedIds = dto.getMchIds().stream().filter(visibleIds::contains).distinct().toList(); + dto.setMchIds(allowedIds.isEmpty() ? List.of(NO_VISIBLE_CANTEEN_ID) : allowedIds); + } + + /** + * 查询一个自然日的三层销售数据并转换为 Excel 行。 + * + * @param query 已收敛到单个自然日的查询条件 + * @param fixedDate 当前批次业务日期 + * @param dataScope 数据权限上下文 + * @return 当前日期下合计和各来源的日期餐段、商户部门、商品明细及商品统计行 + */ + private List buildSalesExportRows(HardOrderDto query, LocalDate fixedDate, + DataScope dataScope) { + List exportRecords = hardCarMapper.listSalesExportRows(query, dataScope); + List summaries = filterSalesExportRecords( + exportRecords, SALES_EXPORT_RECORD_LEVEL_SUMMARY); + if (ObjectUtil.isEmpty(summaries)) { + return List.of(); + } + List merchants = filterSalesExportRecords( + exportRecords, SALES_EXPORT_RECORD_LEVEL_MERCHANT); + List products = filterSalesExportRecords( + exportRecords, SALES_EXPORT_RECORD_LEVEL_PRODUCT); + Map merchantNames = fetchExportMerchantNames(merchants); + Map departmentNames = fetchExportDepartmentNames(merchants); + Map hourNames = getHourTypeNames(); + Map menuMap = fetchExportDailyMenus(products, fixedDate); + Map> merchantGroups = merchants.stream() + .collect(Collectors.groupingBy(this::exportSummaryGroupKey, LinkedHashMap::new, Collectors.toList())); + Map> productGroups = products.stream() + .collect(Collectors.groupingBy(this::exportGroupKey, LinkedHashMap::new, Collectors.toList())); + List rows = new ArrayList<>(); + for (HardCanteenProductApptExportRecord summary : summaries) { + LocalDate statisticsDate = parseExportDate(summary, fixedDate); + if (statisticsDate == null) { + log.warn("{}:销售导出汇总缺少业务日期,hourType:{},跳过该记录", TAG, summary.getHourType()); + continue; + } + String summaryKey = exportSummaryGroupKey(summary); + 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 = exportGroupKey(merchant); + List detailRows = productGroups + .getOrDefault(merchantKey, List.of()).stream() + .flatMap(product -> toExportProductRows(product, + menuMap.get(exportMenuKey(product, statisticsDate))).stream()) + .toList(); + List statisticRows = buildExportProductStatistics(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(statisticsDate); + summaryRow.setSummaryHourType(resolveHourName(summary.getHourType(), hourNames)); + summaryRow.setSummarySaleSourceName(resolveExportSourceName(summary.getSaleSource())); + summaryRow.setSummaryPayUserCount(valueOrZero(summary.getPayUserCount())); + summaryRow.setSummaryTimeCount(valueOrZero(summary.getTimeCount())); + summaryRow.setSummaryPayCount(summaryProductCount); + summaryRow.setSummaryTotalPrice(centsToYuan(summaryTotalPrice)); + summaryRow.setRowLevel("SUMMARY"); + rows.add(summaryRow); + for (HardCanteenProductApptExportRecord merchant : merchantRows) { + String merchantKey = exportGroupKey(merchant); + HardCanteenProductApptExcelVo merchantRow = new HardCanteenProductApptExcelVo(); + merchantRow.setMerchantName(merchantNames.getOrDefault(merchant.getMerchantId(), "商户信息未记录")); + merchantRow.setDepartmentName(departmentNames.getOrDefault(merchant.getUserDeptId(), "部门信息未记录")); + merchantRow.setMerchantDate(statisticsDate); + merchantRow.setMerchantHourType(resolveHourName(summary.getHourType(), hourNames)); + merchantRow.setMerchantPayUserCount(valueOrZero(merchant.getPayUserCount())); + merchantRow.setMerchantTimeCount(valueOrZero(merchant.getTimeCount())); + merchantRow.setMerchantPayCount(detailCountsByMerchant.getOrDefault( + merchantKey, valueOrZero(merchant.getPayCount()))); + merchantRow.setMerchantTotalPrice(centsToYuan(detailPricesByMerchant.getOrDefault( + merchantKey, valueOrZero(merchant.getTotalPrice())))); + merchantRow.setRowLevel("MERCHANT"); + rows.add(merchantRow); + rows.addAll(detailRowsByMerchant.getOrDefault(merchantKey, List.of())); + } + } + return rows; + } + + /** + * 从一次 SQL 返回的三级聚合记录中提取指定层级。 + * + * @param records 当前自然日的全部导出聚合记录;允许为空 + * @param recordLevel Mapper 返回的 SUMMARY、MERCHANT 或 PRODUCT 层级编码 + * @return 保持数据库稳定排序的指定层级记录 + */ + private List filterSalesExportRecords( + List records, String recordLevel) { + if (ObjectUtil.isEmpty(records)) { + return List.of(); + } + return records.stream().filter(record -> ObjectUtil.equals(record.getRecordLevel(), recordLevel)).toList(); + } + + /** + * 将商品聚合行按业务当天每日菜谱中的套餐组成展开。 + * + *

组成商品数量按套餐数量乘组成份数计算,金额按菜谱单价权重使用最大余数法分摊,保证组成行金额 + * 之和严格等于订单快照金额。找不到套餐菜谱时保留原商品行。

+ * + * @param product 商品聚合记录 + * @param menu 与商户、日期、餐段和商品匹配的每日菜谱 + * @return 原商品行或套餐组成商品行 + */ + private List toExportProductRows( + HardCanteenProductApptExportRecord product, DailyMenuAppVO menu) { + List items = menu != null && ObjectUtil.isNotEmpty(menu.getComboItems()) + ? menu.getComboItems() : List.of(); + if (items.isEmpty()) { + return List.of(toExportProductRow(product.getProductName(), valueOrZero(product.getPayCount()), + valueOrZero(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<>(); + 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 { + itemPrices[items.size() - 1] = absoluteTotalCents; + } + List rows = new ArrayList<>(); + for (int index = 0; index < items.size(); index++) { + OnlineSpuComboItemVO item = items.get(index); + long itemCount = packageCount * Math.max(1, valueOrZero(item.getQuantity())); + rows.add(toExportProductRow(StrUtil.blankToDefault(item.getName(), "未命名商品"), + itemCount, itemPrices[index] * sign)); + } + return rows; + } + + /** + * 创建一个商品明细导出行。 + * + * @param productName 商品展示名称 + * @param productCount 有效商品数量 + * @param totalPrice 商品总价,单位为分 + * @return 只填写商品明细列的导出行 + */ + private HardCanteenProductApptExcelVo toExportProductRow(String productName, long productCount, + long totalPrice) { + HardCanteenProductApptExcelVo row = new HardCanteenProductApptExcelVo(); + row.setProductName(StrUtil.blankToDefault(productName, "未命名商品")); + row.setProductCount(productCount); + row.setProductTotalPrice(centsToYuan(totalPrice)); + row.setRowLevel("PRODUCT"); + return row; + } + + /** + * 在当前商户部门商品明细右侧生成按名称合并的商品统计。 + * + * @param detailRows 已扣减有效退款并完成套餐展开的商品明细 + * @return 按规范化商品名称聚合的商品统计行 + */ + private List buildExportProductStatistics( + List detailRows) { + Map statistics = new LinkedHashMap<>(); + for (HardCanteenProductApptExcelVo detailRow : detailRows) { + String displayName = StrUtil.blankToDefault(detailRow.getProductName(), "未命名商品").trim(); + String statisticKey = normalizeExportProductName(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)); + row.setRowLevel("PRODUCT_STATISTIC"); + 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 products 当前日期的商品聚合记录 + * @param statisticsDate 当前业务发生日期 + * @return 商户、日期、餐段和商品主键到每日菜谱的映射;远程查询失败的项不写入 + */ + private Map fetchExportDailyMenus( + List products, LocalDate statisticsDate) { + Map result = new HashMap<>(); + if (statisticsDate == null || ObjectUtil.isEmpty(products)) { + return result; + } + List merchantIds = products.stream().map(HardCanteenProductApptExportRecord::getMerchantId) + .filter(Objects::nonNull).distinct().toList(); + for (Long merchantId : merchantIds) { + List hourTypes = products.stream() + .filter(item -> Objects.equals(item.getMerchantId(), merchantId)) + .map(HardCanteenProductApptExportRecord::getHourType) + .filter(StrUtil::isNotBlank).distinct().toList(); + for (String hourType : hourTypes) { + try { + R> response = remoteDailyMenuService.getCanteenList( + statisticsDate.toString(), Integer.valueOf(hourType), null, merchantId); + if (response != null && response.isOk() && response.getData() != null) { + response.getData().forEach(menu -> { + if (menu.getSpuId() != null) { + result.put(merchantId + "|" + statisticsDate + "|" + hourType + "|" + + menu.getSpuId(), menu); + } + }); + } + } catch (Exception exception) { + log.warn("{}:查询套餐组成失败,保留原商品行,merchantId:{},date:{},hourType:{}", + TAG, merchantId, statisticsDate, hourType, exception); } } } @@ -653,20 +1204,212 @@ public class HardCanteenTotalServiceImpl implements HardCanteenTotalService { } /** - * 解析部门导出的具体销售来源名称。 + * 批量补全三级导出中的食堂名称。 * - *

多来源导出使用来源拆分行自身名称;单来源查询为避免页面重复行不会挂载来源拆分, - * 此时使用已经校验过的请求来源补全名称。

- * - * @param row 待导出的部门餐段统计行 - * @param dto 已完成来源标准化的统计请求 - * @return 可追溯的具体来源中文名称;来源异常时返回空值 + * @param rows 商户部门聚合记录 + * @return 商户主键到名称的映射;远程查询失败时返回空映射 */ - private String resolveDepartmentExportSourceName(HardCanteenProductTotalVo row, HardOrderDto dto) { - if (row != null && ObjectUtil.isNotEmpty(row.getSaleSourceName())) return row.getSaleSourceName(); - if (dto == null || ObjectUtil.isEmpty(dto.getSaleSourceCodes()) || dto.getSaleSourceCodes().size() != 1) return null; - HardSaleSourceEnum source = HardSaleSourceEnum.valueOfCode(dto.getSaleSourceCodes().get(0)); - return source == null ? null : source.getDescription(); + private Map fetchExportMerchantNames(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("{}:查询导出食堂名称失败", TAG, exception); + return Map.of(); + } + } + + /** + * 批量补全三级导出中的用户部门名称。 + * + * @param rows 商户部门聚合记录 + * @return 部门主键到名称的映射;远程查询失败时返回空映射 + */ + private Map fetchExportDepartmentNames(List rows) { + List ids = rows.stream().map(HardCanteenProductApptExportRecord::getUserDeptId) + .filter(Objects::nonNull).distinct().toList(); + if (ids.isEmpty()) { + return Map.of(); + } + try { + R> response = remoteDeptService.getDeptByDeptIds(ids); + if (response == null || !response.isOk() || response.getData() == null) { + return Map.of(); + } + return response.getData().values().stream().filter(item -> item.getDeptId() != null) + .collect(Collectors.toMap(SysDept::getDeptId, SysDept::getName, + (left, right) -> left)); + } catch (Exception exception) { + log.warn("{}:查询导出部门名称失败", TAG, exception); + return Map.of(); + } + } + + /** + * 复制三级导出查询,避免按日处理时改写控制器接收的时间范围。 + * + * @param source 已完成日期、来源和食堂权限校验的原请求 + * @return 可独立修改统计日期范围的请求副本 + */ + private HardOrderDto copyExportQuery(HardOrderDto source) { + HardOrderDto target = BeanUtil.toBean(source, HardOrderDto.class); + target.setStatisticsDateTimeList(source.getStatisticsDateTimeList() == null + ? null : source.getStatisticsDateTimeList().clone()); + target.setSaleSourceCodes(source.getSaleSourceCodes() == null + ? null : new ArrayList<>(source.getSaleSourceCodes())); + target.setMchIds(source.getMchIds() == null ? null : new ArrayList<>(source.getMchIds())); + target.setDeptIds(source.getDeptIds() == null ? null : new ArrayList<>(source.getDeptIds())); + target.setUserDeptIds(source.getUserDeptIds() == null + ? null : new ArrayList<>(source.getUserDeptIds())); + target.setMerchantDeptIds(source.getMerchantDeptIds() == null + ? null : new ArrayList<>(source.getMerchantDeptIds())); + target.setScopeDeptIds(source.getScopeDeptIds() == null + ? null : new ArrayList<>(source.getScopeDeptIds())); + return target; + } + + /** + * 将导出统计日期闭区间展开为自然日列表。 + * + * @param query 已通过日期范围校验的导出请求 + * @return 从开始日期到结束日期的有序闭区间 + */ + private List getExportDates(HardOrderDto query) { + LocalDate start = query.getStatisticsDateTimeList()[0].toLocalDate(); + LocalDate end = query.getStatisticsDateTimeList()[1].toLocalDate(); + List dates = new ArrayList<>(); + for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) { + dates.add(date); + } + return dates; + } + + /** + * 解析导出记录的业务日期。 + * + * @param row 日期餐段聚合记录 + * @param fixedDate 当前按日批次的已知日期 + * @return 可用业务日期;两者均为空或格式错误时返回空 + */ + private LocalDate parseExportDate(HardCanteenProductApptExportRecord row, LocalDate fixedDate) { + if (fixedDate != null) { + return fixedDate; + } + try { + return StrUtil.isBlank(row.getStatisticsDate()) ? null : LocalDate.parse(row.getStatisticsDate()); + } catch (Exception exception) { + return null; + } + } + + /** + * 构造日期餐段及来源口径分组键。 + * + * @param record 任一层级的销售聚合记录 + * @return 日期、餐段和来源组成的稳定键,空来源表示跨来源合计 + */ + private String exportSummaryGroupKey(HardCanteenProductApptExportRecord record) { + return record.getStatisticsDate() + "|" + record.getHourType() + "|" + + ObjectUtil.defaultIfNull(record.getSaleSource(), "TOTAL"); + } + + /** + * 构造商户部门及来源口径分组键。 + * + * @param record 商户部门或商品聚合记录 + * @return 日期、餐段、来源、商户和部门组成的稳定键 + */ + private String exportGroupKey(HardCanteenProductApptExportRecord record) { + return exportSummaryGroupKey(record) + "|" + record.getMerchantId() + "|" + record.getUserDeptId(); + } + + /** + * 构造每日菜谱套餐缓存键。 + * + * @param record 商品聚合记录 + * @param statisticsDate 业务发生日期 + * @return 商户、日期、餐段和商品主键组成的稳定键 + */ + private String exportMenuKey(HardCanteenProductApptExportRecord record, LocalDate statisticsDate) { + return record.getMerchantId() + "|" + statisticsDate + "|" + record.getHourType() + + "|" + record.getProductId(); + } + + /** + * 解析来源口径中文名称。 + * + * @param saleSource 销售来源协议编码;为空表示所选来源合计 + * @return 合计、线下购买、预约核销或未知来源文案 + */ + private String resolveExportSourceName(String saleSource) { + if (StrUtil.isBlank(saleSource)) { + return "合计"; + } + HardSaleSourceEnum source = HardSaleSourceEnum.valueOfCode(saleSource); + return source == null ? "未知来源" : source.getDescription(); + } + + /** + * 解析餐段名称,字典缺失时保留协议编码。 + * + * @param hourType 餐段协议编码 + * @param hourNames 餐段字典映射 + * @return 餐段名称或原编码 + */ + private String resolveHourName(String hourType, Map hourNames) { + return StrUtil.blankToDefault(hourNames.get(hourType), StrUtil.blankToDefault(hourType, "未标记餐段")); + } + + /** + * 规范化商品名称中的换行和连续空白。 + * + * @param productName 商品展示名称 + * @return 可用于同名商品聚合的稳定名称 + */ + private String normalizeExportProductName(String productName) { + return productName.replaceAll("\\s+", " ").trim(); + } + + /** + * 将空数值按零处理。 + * + * @param value 可空数量或金额 + * @return 原数值的长整型表示,空值返回零 + */ + private long valueOrZero(Number value) { + return value == null ? 0L : value.longValue(); + } + + /** + * 将分转换为保留两位小数的元。 + * + * @param cents 金额,单位为分 + * @return 金额,单位为元 + */ + private BigDecimal centsToYuan(Number cents) { + return BigDecimal.valueOf(valueOrZero(cents)).movePointLeft(2) + .setScale(2, RoundingMode.HALF_UP); + } + + /** + * 将两位小数的元转换为整数分,供商品明细重新累计合计。 + * + * @param yuan 金额,单位为元 + * @return 金额,单位为分;空值返回零 + */ + private long yuanToCents(BigDecimal yuan) { + return yuan == null ? 0L + : yuan.movePointRight(2).setScale(0, RoundingMode.HALF_UP).longValue(); } /** @@ -677,25 +1420,20 @@ public class HardCanteenTotalServiceImpl implements HardCanteenTotalService { private Map getHourTypeNames() { try { List items = remoteDictService.getDictByType("daily_menu").getData(); - if (ObjectUtil.isEmpty(items)) return Map.of(); - return items.stream().filter(item -> ObjectUtil.isNotEmpty(item.getItemValue()) && ObjectUtil.isNotEmpty(item.getLabel())) - .collect(Collectors.toMap(SysDictItem::getItemValue, SysDictItem::getLabel, (left, right) -> left)); + if (ObjectUtil.isEmpty(items)) { + return Map.of(); + } + return items.stream() + .filter(item -> ObjectUtil.isNotEmpty(item.getItemValue()) + && ObjectUtil.isNotEmpty(item.getLabel())) + .collect(Collectors.toMap(SysDictItem::getItemValue, SysDictItem::getLabel, + (left, right) -> left)); } catch (Exception exception) { log.warn("{}:读取餐段字典失败,导出将保留餐段编码", TAG, exception); return Map.of(); } } - /** - * 将持久化使用的分转换为导出展示使用的元,空金额按零处理。 - * - * @param cents 金额,单位为分 - * @return 保留两位小数的元金额 - */ - private BigDecimal centsToYuan(Long cents) { - return BigDecimal.valueOf(ObjectUtil.defaultIfNull(cents, 0L)).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP); - } - /** * 判断是否启用来源统计查询。未传统计日期时保留旧 SQL,兼容历史页面和其他服务调用方。 * diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java index a70c072a7..db9b15389 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java @@ -91,6 +91,7 @@ import com.bsd.pjcloud.hard.mapper.HardThirdMiOrderMapper; import com.bsd.pjcloud.hard.mapper.order.HardMessageOutboxMapper; import com.bsd.pjcloud.hard.mapper.order.HardOrderMapper; import com.bsd.pjcloud.hard.service.*; +import com.bsd.pjcloud.hard.support.HardUserIdentitySupport; import com.bsd.pjcloud.supermarket.api.feign.RemoteMallProductService; import com.bsd.pjcloud.supermarket.api.feign.RemoteShopSkuService; import com.bsd.pjcloud.supermarket.api.feign.RemoteSupermarketSpuService; @@ -217,6 +218,8 @@ public class HardOrderServiceImpl extends ServiceImpl pagejoin(Page page, HardOrderDto dto) { + applyIdentityQuery(dto); Page list = baseMapper.pageJoin(page, dto); disposeList(list.getRecords(), dto); return list; @@ -251,6 +255,7 @@ public class HardOrderServiceImpl extends ServiceImpl scopeList(HardOrderDto dto, DataScope dataScope) { + applyIdentityQuery(dto); List list = baseMapper.scopeList(dto, dataScope); disposeList(list, dto); return list; @@ -305,6 +310,7 @@ public class HardOrderServiceImpl extends ServiceImpl scopePageList(Page page, HardOrderDto dto, DataScope dataScope) { + applyIdentityQuery(dto); if (dto.getAppViewerUserId() != null) { Page list = appVisibilityPageList(page, dto, dataScope); disposeList(list.getRecords(), dto); @@ -351,6 +357,7 @@ public class HardOrderServiceImpl extends ServiceImpl noRecogScopePageJoin(Page page, HardOrderDto dto, DataScope dataScope) { + applyIdentityQuery(dto); dto.setOrderPayTypeCodes(List.of(HardPayTypeEnum.HARD_DOOR_FREE_PAY.getType())); dto.setOrderStatus(HardOrderStatusEnum.CREATE.getType()); Page list = baseMapper.scopePageList(page, dto, dataScope); @@ -371,10 +378,7 @@ public class HardOrderServiceImpl extends ServiceImpl findUserFuture = CompletableFuture.runAsync(() -> { RequestContextHolder.setRequestAttributes(requestAttributes); if (ObjectUtil.isNotEmpty(detail.getUserId())) { - List userVoList = userService.getUserDeptByIds(List.of(detail.getUserId())).getData(); - if (ObjectUtil.isNotEmpty(userVoList) && ObjectUtil.isNotEmpty(userVoList.get(0))) { - DataUtil.convertUserDept(detail, userVoList.get(0)); - } + userIdentitySupport.fillUserInfo(List.of(detail), true); } }); // 商户 @@ -4839,20 +4843,7 @@ public class HardOrderServiceImpl extends ServiceImpl deptFuture = CompletableFuture.runAsync(() -> { RequestContextHolder.setRequestAttributes(requestAttributes); - List userIds = list.stream().map(HardOrderVo::getUserId).filter(ObjectUtil::isNotEmpty).distinct().toList(); - if (ObjectUtil.isNotEmpty(userIds)) { - List userList = userService.getUserDeptByIds(userIds).getData(); - if (ObjectUtil.isNotEmpty(userList)) { - Map map = CollectionUtils.convertMap(userList, UserVO::getUserId); - for (HardOrderVo item : list) { - UserVO userVO = map.get(item.getUserId()); - if (ObjectUtil.isEmpty(userVO)) continue; - item.setUsername(userVO.getName()); - item.setUserMobile(userVO.getPhone()); - item.setUserAvatar(userVO.getAvatar()); - } - } - } + userIdentitySupport.fillUserInfo(list, true); }); // 当前部门 CompletableFuture currentDeptFuture = CompletableFuture.runAsync(() -> { @@ -4935,4 +4926,21 @@ public class HardOrderServiceImpl extends ServiceImpl scopePageJoin(Page page, HardSpuRepertoryDto dto, DataScope dataScope) { + prepareIdentityQuery(dto); Page pageList = baseMapper.scopePageJoin(page, dto, dataScope); disposeList(pageList.getRecords(), dto); return pageList; @@ -246,11 +244,20 @@ public class HardSpuRepertoryServiceImpl extends ServiceImpl userIds = list.stream().map(HardSpuRepertoryVo::getUserId).distinct().toList(); - R> userList = remoteUserService.getUserById(new ArrayList<>(userIds)); - if (ObjectUtil.isNotEmpty(userList.getData())) { - DataUtil.convertUserList(list, CollectionUtils.convertMap(userList.getData(), SysUser::getUserId)); - } + // 用户身份、姓名、手机号、头像及部门统一由 UPMS 公共装配器回填。 + userIdentitySupport.fillUserInfo(list, true); } -} \ No newline at end of file + + /** 将库存上架记录的身份条件转换为最终用户主键集合,保持原有 userIds SQL 过滤契约。 */ + private void prepareIdentityQuery(HardSpuRepertoryDto 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/resources/mapper/HardAuthMapper.xml b/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/HardAuthMapper.xml index 6a2633937..32d14f06f 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/HardAuthMapper.xml +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/HardAuthMapper.xml @@ -41,6 +41,12 @@ and a.user_id = #{dto.userId} + + and a.user_id in + + #{v} + + and a.hard_id = #{dto.hardId} @@ -70,4 +76,4 @@ order by a.create_time desc , a.id desc - \ No newline at end of file + diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/HardBroadcastMapper.xml b/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/HardBroadcastMapper.xml index 80964a436..78dc2ab08 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/HardBroadcastMapper.xml +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/HardBroadcastMapper.xml @@ -51,6 +51,12 @@ and a.user_id = #{dto.userId} + + and a.user_id in + + #{v} + + and a.hard_id = #{dto.hardId} @@ -87,4 +93,4 @@ order by a.create_time desc, a.id desc - \ No newline at end of file + diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/HardInvoiceMapper.xml b/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/HardInvoiceMapper.xml index 9aee10fea..c404588d7 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/HardInvoiceMapper.xml +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/HardInvoiceMapper.xml @@ -84,4 +84,4 @@ order by a.create_time desc, a.id desc - \ No newline at end of file + diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardAfterSaleMapper.xml b/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardAfterSaleMapper.xml index 89a5f2b85..1d88329d5 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardAfterSaleMapper.xml +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardAfterSaleMapper.xml @@ -101,4 +101,4 @@ order by a.create_time desc,a.id desc - \ No newline at end of file + 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 1237b6049..f10256ab7 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 @@ -57,6 +57,21 @@ + + + ( + select refund_item.parent_car_id, + refund_item.product_id, + sum(ifnull(refund_item.product_count, 0)) as product_count, + sum(ifnull(refund_item.total_price, 0)) as total_price + from hard_after_sale_car refund_item + inner join hard_after_sale refund_order on refund_order.id = refund_item.refund_id + and refund_order.del_flag = '0' and refund_order.status = '50' + where refund_item.del_flag = '0' + group by refund_item.parent_car_id, refund_item.product_id + ) refund_car + + + + + and o.merchant_id = #{dto.merchantId} + + + and o.merchant_id in + + #{v} + + + + and o.hour_type = #{dto.hourType} + + + + + and o.dept_id in + + + and o.dept_id not in + + + + #{v} + + + + + + and o.user_dept_id in + + + and o.user_dept_id not in + + + + #{v} + + + + + + + + and a.product_name like concat('%', #{dto.productName}, '%') + + + + + + ( + select refund_item.parent_car_id, + refund_item.product_id, + sum(ifnull(refund_item.product_count, 0)) as product_count, + sum(ifnull(refund_item.total_price, 0)) as total_price + from hard_after_sale_car refund_item + inner join hard_after_sale refund_order on refund_order.id = refund_item.refund_id + and refund_order.del_flag = '0' and refund_order.status = '50' + inner join hard_order o on o.parent_car_id = refund_item.parent_car_id + and o.del_flag = '0' and o.merchant_type = '1' and o.order_type = '2' + and (o.appt_id is not null or o.appt_rule_id is not null) + and o.order_status = '20' and o.verification_time is not null + and o.hour_type is not null and o.hour_type not in ('-1') + where refund_item.del_flag = '0' + + and o.verification_time between #{dto.statisticsDateTimeList[0]} and #{dto.statisticsDateTimeList[1]} + + + group by refund_item.parent_car_id, refund_item.product_id + ) refund_car + + - ( + + select DATE_FORMAT(o.pay_time, '%Y-%m-%d') as stat_date, o.pay_time as stat_time, o.id as order_id, @@ -142,7 +228,14 @@ where a.del_flag = '0' and o.pay_time is not null and o.order_status in ('5', '20') + + and o.pay_time between #{dto.statisticsDateTimeList[0]} and #{dto.statisticsDateTimeList[1]} + + + union all + + select DATE_FORMAT(o.verification_time, '%Y-%m-%d') as stat_date, o.verification_time as stat_time, o.id as order_id, @@ -153,13 +246,13 @@ o.merchant_dept_id, o.hour_type, a.product_id, - a.product_name, - a.product_pic, - a.product_price, - a.product_count, - a.total_price, + max(a.product_name) as product_name, + max(a.product_pic) as product_pic, + max(a.product_price) as product_price, + greatest(sum(ifnull(a.product_count, 0)) - ifnull(refund_car.product_count, 0), 0) as product_count, + greatest(sum(ifnull(a.total_price, 0)) - ifnull(refund_car.total_price, 0), 0) as total_price, 'RESERVATION_VERIFIED' as sale_source - from + from hard_car a inner join hard_parent_car p on a.parent_car_id = p.id and p.del_flag = '0' inner join hard_order o on o.parent_car_id = p.id and o.del_flag = '0' and o.merchant_type = '1' and o.order_type = '2' @@ -167,7 +260,20 @@ and o.order_status = '20' and o.verification_time is not null and o.hour_type is not null and o.hour_type not in ('-1') - ) sale_stats + left join + on refund_car.parent_car_id = a.parent_car_id + and refund_car.product_id = a.product_id + where a.del_flag = '0' + + and o.verification_time between #{dto.statisticsDateTimeList[0]} and #{dto.statisticsDateTimeList[1]} + + + + group by o.id, o.user_id, o.merchant_id, o.user_dept_id, o.dept_id, o.merchant_dept_id, + o.hour_type, o.verification_time, a.product_id, refund_car.product_count, refund_car.total_price + having sum(ifnull(a.product_count, 0)) > ifnull(refund_car.product_count, 0) + + @@ -644,22 +750,32 @@ 综合菜品销售导出为每个日期餐段同时生成跨来源合计和来源拆分。 三层 SQL 使用相同的来源、业务时间、食堂和部门筛选,保证页面与导出以及上下层之间口径一致。 --> - - - - - @@ -318,4 +336,4 @@ - \ No newline at end of file + diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/main/resources/mapper/recharge/WalletRechargeInvoiceMapper.xml b/pjcloud-wallet/pjcloud-wallet-biz/src/main/resources/mapper/recharge/WalletRechargeInvoiceMapper.xml index b12ec2360..ff39cf633 100644 --- a/pjcloud-wallet/pjcloud-wallet-biz/src/main/resources/mapper/recharge/WalletRechargeInvoiceMapper.xml +++ b/pjcloud-wallet/pjcloud-wallet-biz/src/main/resources/mapper/recharge/WalletRechargeInvoiceMapper.xml @@ -84,7 +84,17 @@ - \ No newline at end of file + diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/main/resources/mapper/rule/WalletRuleTransactionMapper.xml b/pjcloud-wallet/pjcloud-wallet-biz/src/main/resources/mapper/rule/WalletRuleTransactionMapper.xml index 3ca473bd7..bfa302016 100644 --- a/pjcloud-wallet/pjcloud-wallet-biz/src/main/resources/mapper/rule/WalletRuleTransactionMapper.xml +++ b/pjcloud-wallet/pjcloud-wallet-biz/src/main/resources/mapper/rule/WalletRuleTransactionMapper.xml @@ -83,6 +83,12 @@ and t.user_id = #{dto.userId} + + and t.user_id in + + #{v} + + @@ -103,4 +109,4 @@ group by a.id order by a.create_time desc,a.id desc - \ No newline at end of file + diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/test/java/com/bsd/pjcloud/wallet/service/impl/mch/MchPaySettingServiceImplTest.java b/pjcloud-wallet/pjcloud-wallet-biz/src/test/java/com/bsd/pjcloud/wallet/service/impl/mch/MchPaySettingServiceImplTest.java index 11e14279e..b9f71d522 100644 --- a/pjcloud-wallet/pjcloud-wallet-biz/src/test/java/com/bsd/pjcloud/wallet/service/impl/mch/MchPaySettingServiceImplTest.java +++ b/pjcloud-wallet/pjcloud-wallet-biz/src/test/java/com/bsd/pjcloud/wallet/service/impl/mch/MchPaySettingServiceImplTest.java @@ -2,6 +2,7 @@ package com.bsd.pjcloud.wallet.service.impl.mch; import cn.hutool.json.JSONUtil; import com.bsd.pjcloud.admin.api.feign.RemoteUserService; +import com.bsd.pjcloud.common.core.util.R; import com.bsd.pjcloud.thirdparty.api.feign.RemoterThirdAppPlusPayService; import com.bsd.pjcloud.wallet.api.dto.mch.MchPaySettingDto; import com.bsd.pjcloud.wallet.api.entity.mch.MchPaySettingEntity; @@ -31,7 +32,7 @@ class MchPaySettingServiceImplTest { @Mock private MchPaySettingMapper mchPaySettingMapper; - /** 商户服务 Mock,仅补齐异步刷新依赖。 */ + /** 商户服务 Mock,用于同步刷新默认平台分佣关系。 */ @Mock private MchManageService mchManageService; @@ -39,7 +40,7 @@ class MchPaySettingServiceImplTest { @Mock private RemoteUserService remoteUserService; - /** 第三方支付远程服务 Mock,仅补齐异步刷新依赖。 */ + /** 第三方支付远程服务 Mock,用于同步确认关系事实已接收。 */ @Mock private RemoterThirdAppPlusPayService thirdAppPlusPayService; @@ -81,6 +82,8 @@ class MchPaySettingServiceImplTest { MchPaySettingEntity stored = storedAliPaySetting(); when(mchPaySettingMapper.selectCount(any())).thenReturn(1L); when(mchPaySettingMapper.selectById(stored.getId())).thenReturn(stored); + when(mchManageService.join(stored.getMchId())).thenReturn(new com.bsd.pjcloud.wallet.api.vo.MchManageVo()); + when(thirdAppPlusPayService.remoterChangeMch(any())).thenReturn(R.ok(true)); MchPaySettingEntity.AliPaySetting incomingSetting = new MchPaySettingEntity.AliPaySetting(); incomingSetting.setSubMchid("smid-1001"); incomingSetting.setAccountKey("secondary");