diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/DailyMenuDateRangeQueryDTO.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/DailyMenuDateRangeQueryDTO.java
new file mode 100644
index 000000000..d6d7b5246
--- /dev/null
+++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/dto/DailyMenuDateRangeQueryDTO.java
@@ -0,0 +1,38 @@
+package com.bsd.pjcloud.canteen.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.time.LocalDate;
+
+/**
+ * 收银终端按日期范围查询每日菜谱的请求参数。
+ */
+@Data
+@Schema(description = "收银终端每日菜谱日期范围查询参数")
+public class DailyMenuDateRangeQueryDTO {
+
+ /** 菜谱起始日期,查询闭区间包含当天。 */
+ @NotNull(message = "菜谱开始日期不能为空")
+ @Schema(description = "菜谱起始日期,格式为 yyyy-MM-dd", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDate startDate;
+
+ /** 菜谱结束日期,查询闭区间包含当天。 */
+ @NotNull(message = "菜谱结束日期不能为空")
+ @Schema(description = "菜谱结束日期,格式为 yyyy-MM-dd", requiredMode = Schema.RequiredMode.REQUIRED)
+ private LocalDate endDate;
+
+ /** 餐段协议编码;为空时查询全部餐段。 */
+ @Schema(description = "餐段编码;为空表示全部餐段")
+ private Integer hourType;
+
+ /** 菜品分类 ID;为空时查询全部分类。 */
+ @Schema(description = "菜品分类 ID;为空表示全部分类")
+ private Long categoryId;
+
+ /** 收银终端所属商户 ID,用于隔离不同商户的每日菜谱。 */
+ @NotNull(message = "每日菜谱商户不能为空")
+ @Schema(description = "收银终端所属商户 ID", requiredMode = Schema.RequiredMode.REQUIRED)
+ private Long merchantId;
+}
diff --git a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/ADCanteenDailyMenuListVO.java b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/ADCanteenDailyMenuListVO.java
index ecb1f1636..f708ef8fd 100644
--- a/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/ADCanteenDailyMenuListVO.java
+++ b/pjcloud-canteen/pjcloud-canteen-api/src/main/java/com/bsd/pjcloud/canteen/vo/ADCanteenDailyMenuListVO.java
@@ -3,6 +3,8 @@ package com.bsd.pjcloud.canteen.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
+import java.time.LocalDate;
+
@Data
public class ADCanteenDailyMenuListVO {
/**
@@ -10,6 +12,11 @@ public class ADCanteenDailyMenuListVO {
*/
private Long id;
+ /**
+ * 菜谱供应日期,yyyy-MM-dd;收银终端多日同步时以此归属到具体日期
+ */
+ private LocalDate saleDate;
+
/**
* spuid
*/
diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenDailyMenuController.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenDailyMenuController.java
index 715d8f8d2..e3475f1bc 100644
--- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenDailyMenuController.java
+++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/controller/CanteenDailyMenuController.java
@@ -10,6 +10,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.bsd.pjcloud.canteen.dto.CanteenDailyMenuCVDTO;
import com.bsd.pjcloud.canteen.dto.DailyMenuAll;
+import com.bsd.pjcloud.canteen.dto.DailyMenuDateRangeQueryDTO;
import com.bsd.pjcloud.canteen.dto.app.DailyMenuDTO;
import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity;
import com.bsd.pjcloud.canteen.service.CanteenSpuService;
@@ -198,6 +199,20 @@ public class CanteenDailyMenuController {
return R.ok(canteenDailyMenuService.getCanteenMenuListScope(day, hourType, categoryId,merchantId));
}
+ /**
+ * 按闭区间(含起止两天)查询收银终端多日菜谱,字段与单日接口一致,
+ * 每条记录额外携带供应日期 saleDate,便于终端按日期落库。
+ *
+ * @param request 日期范围、可选筛选条件和收银终端所属商户
+ * @return 指定日期范围内的每日菜谱列表;无数据时返回空列表
+ */
+ @PostMapping("/ad/getCanteenListByDateRange")
+ @SysLog("按日期范围查询每日菜品列表")
+ public R> getCanteenListByDateRange(
+ @Validated @RequestBody DailyMenuDateRangeQueryDTO request) {
+ return R.ok(canteenDailyMenuService.getCanteenMenuListByDateRange(request));
+ }
+
// --------------APP---------------
diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenDailyMenuMapper.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenDailyMenuMapper.java
index dcc9b3c59..1219f37b9 100644
--- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenDailyMenuMapper.java
+++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/mapper/CanteenDailyMenuMapper.java
@@ -1,6 +1,7 @@
package com.bsd.pjcloud.canteen.mapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.bsd.pjcloud.canteen.dto.DailyMenuDateRangeQueryDTO;
import com.bsd.pjcloud.canteen.dto.app.DailyMenuDTO;
import com.bsd.pjcloud.canteen.vo.ADCanteenDailyMenuListVO;
import com.bsd.pjcloud.canteen.vo.DailyMenuVO;
@@ -31,6 +32,16 @@ public interface CanteenDailyMenuMapper extends PjcloudBaseMapper getCanteenMenuList(@Param("day") String day, @Param("hourType") Integer hourType,
@Param("categoryId") Long categoryId, @Param("merchantId") Long merchantId,DataScope dataScope);
+ /**
+ * 按闭区间查询收银终端多日菜谱展示数据,字段与单日查询保持一致。
+ *
+ * @param request 日期范围、可选筛选条件和商户 ID
+ * @param dataScope 当前数据权限范围
+ * @return 按日期、餐段和商品排序的菜谱列表
+ */
+ List getCanteenMenuListByDateRange(@Param("request") DailyMenuDateRangeQueryDTO request,
+ DataScope dataScope);
+
Page selectTimeFoodPage(@Param("page")Page page, DailyMenuDTO dailyMenuDTO);
diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenDailyMenuService.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenDailyMenuService.java
index 90b7be021..d3c6d9509 100644
--- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenDailyMenuService.java
+++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/CanteenDailyMenuService.java
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bsd.pjcloud.canteen.dto.CanteenDailyMenuCVDTO;
import com.bsd.pjcloud.canteen.dto.DailyMenuAll;
+import com.bsd.pjcloud.canteen.dto.DailyMenuDateRangeQueryDTO;
import com.bsd.pjcloud.canteen.dto.app.DailyMenuDTO;
import com.bsd.pjcloud.canteen.entity.CanteenDailyMenuEntity;
import com.bsd.pjcloud.canteen.vo.ADCanteenDailyMenuListVO;
@@ -51,6 +52,15 @@ public interface CanteenDailyMenuService extends IService getCanteenMenuListScope(String day, Integer hourType, Long categoryId, Long merchantId);
+ /**
+ * 按闭区间查询收银终端多日菜谱,返回字段与单日查询保持一致,
+ * 每条记录额外携带供应日期,供终端按日期分桶落库。
+ *
+ * @param request 日期范围、可选筛选条件和收银终端所属商户
+ * @return 按日期、餐段和商品排序的菜谱列表
+ */
+ List getCanteenMenuListByDateRange(DailyMenuDateRangeQueryDTO request);
+
Page selectTimeFoodPage(Page page, DailyMenuDTO dailyMenuDTO);
diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDailyMenuServiceImpl.java b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDailyMenuServiceImpl.java
index b631fb63a..5f0d521b1 100644
--- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDailyMenuServiceImpl.java
+++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/java/com/bsd/pjcloud/canteen/service/impl/CanteenDailyMenuServiceImpl.java
@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bsd.pjcloud.canteen.dto.CanteenDailyMenuCVDTO;
import com.bsd.pjcloud.canteen.dto.DailyMenuAll;
+import com.bsd.pjcloud.canteen.dto.DailyMenuDateRangeQueryDTO;
import com.bsd.pjcloud.canteen.dto.app.DailyMenuDTO;
import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity;
import com.bsd.pjcloud.canteen.mapper.CanteenSpuMapper;
@@ -142,6 +143,26 @@ public class CanteenDailyMenuServiceImpl extends ServiceImpl getCanteenMenuListByDateRange(DailyMenuDateRangeQueryDTO request) {
+ if (request == null || request.getStartDate() == null || request.getEndDate() == null) {
+ throw new IllegalArgumentException("菜谱开始日期与结束日期不能为空");
+ }
+ if (request.getMerchantId() == null) {
+ throw new IllegalArgumentException("每日菜谱商户不能为空");
+ }
+ if (request.getEndDate().isBefore(request.getStartDate())) {
+ throw new IllegalArgumentException("菜谱结束日期不能早于开始日期");
+ }
+ return baseMapper.getCanteenMenuListByDateRange(request, DataScope.of());
+ }
+
@Override
public Page selectTimeFoodPage(Page page, DailyMenuDTO dailyMenuDTO) {
Page pageData = baseMapper.selectTimeFoodPage(page, dailyMenuDTO);
diff --git a/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenDailyMenuMapper.xml b/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenDailyMenuMapper.xml
index 7da7b1a8f..38029a9b2 100644
--- a/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenDailyMenuMapper.xml
+++ b/pjcloud-canteen/pjcloud-canteen-biz/src/main/resources/mapper/CanteenDailyMenuMapper.xml
@@ -19,6 +19,7 @@
*/
@Override
public HardOfflineUserSyncSnapshotVo syncOfflineUserSnapshot(Long hardId, HardOfflineUserSyncDto dto) {
@@ -140,27 +142,25 @@ public class HardApiServiceImpl implements HardApiService {
? WalletTypeEnum.EAT
: WalletTypeEnum.PAY;
- List deptIds = resolveDepartmentTree(trustedDevice.merchantDeptId());
- if (deptIds.isEmpty()) {
+ final boolean allTenant = Boolean.TRUE.equals(dto.getAllTenant()) && !isProdProfile();
+ final List deptIds = allTenant
+ ? null
+ : resolveDepartmentTree(trustedDevice.merchantDeptId());
+ if (!allTenant && deptIds.isEmpty()) {
throw new RuntimeException("设备绑定部门不存在或已删除");
}
- R> userResponse = userService.getUserIdListByDeptIdList(deptIds);
- if (userResponse == null || !userResponse.isOk() || userResponse.getData() == null) {
- String message = userResponse == null || StrUtil.isBlank(userResponse.getMsg())
- ? "查询部门用户失败"
- : userResponse.getMsg();
- throw new RuntimeException(message);
- }
- List users = userResponse.getData().stream()
- .filter(user -> user.getUserId() != null && StrUtil.isNotBlank(user.getName()))
- .sorted(Comparator.comparing(SysUser::getUserId))
- .toList();
-
long normalizedAfterId = dto.getAfterId();
Long requestedSnapshotMaxId = dto.getSnapshotMaxId();
if (requestedSnapshotMaxId == null) {
- requestedSnapshotMaxId = users.isEmpty() ? 0L : users.get(users.size() - 1).getUserId();
+ R maxIdResponse = userService.getOfflineUserMaxId(deptIds);
+ if (maxIdResponse == null || !maxIdResponse.isOk()) {
+ String message = maxIdResponse == null || StrUtil.isBlank(maxIdResponse.getMsg())
+ ? "查询离线人员快照上界失败"
+ : maxIdResponse.getMsg();
+ throw new RuntimeException(message);
+ }
+ requestedSnapshotMaxId = maxIdResponse.getData() == null ? 0L : maxIdResponse.getData();
}
final long normalizedSnapshotMaxId = requestedSnapshotMaxId;
if (normalizedAfterId > normalizedSnapshotMaxId) {
@@ -168,11 +168,8 @@ public class HardApiServiceImpl implements HardApiService {
}
int normalizedLimit = dto.getLimit() == null ? OFFLINE_USER_SYNC_DEFAULT_LIMIT : dto.getLimit();
- List fetchedPage = users.stream()
- .filter(user -> user.getUserId() > normalizedAfterId
- && user.getUserId() <= normalizedSnapshotMaxId)
- .limit(normalizedLimit + 1L)
- .toList();
+ // 多取一条用于判断是否还有下一页;用户过滤发生在钱包聚合之后,不影响游标推进。
+ List fetchedPage = fetchUserCursorPage(deptIds, normalizedAfterId, normalizedLimit + 1);
boolean hasMore = fetchedPage.size() > normalizedLimit;
List pageUsers = hasMore ? fetchedPage.subList(0, normalizedLimit) : fetchedPage;
@@ -187,6 +184,34 @@ public class HardApiServiceImpl implements HardApiService {
return snapshot;
}
+ /**
+ * 从用户服务按主键游标取一页部门范围(空为全租户)内的用户并过滤无效记录。
+ */
+ private List fetchUserCursorPage(List deptIds, long afterId, int limit) {
+ R> response = userService.getOfflineUserCursor(deptIds, afterId, limit);
+ if (response == null || !response.isOk() || response.getData() == null) {
+ String message = response == null || StrUtil.isBlank(response.getMsg())
+ ? "查询部门用户失败"
+ : response.getMsg();
+ throw new RuntimeException(message);
+ }
+ return response.getData().stream()
+ .filter(user -> user.getUserId() != null && StrUtil.isNotBlank(user.getName()))
+ .toList();
+ }
+
+ /**
+ * 当前服务是否运行在生产配置;生产环境强制收敛为部门范围同步。
+ */
+ private boolean isProdProfile() {
+ for (String profile : environment.getActiveProfiles()) {
+ if (StrUtil.containsIgnoreCase(profile, "prod")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* 将一页部门用户组装为脱敏离线人员记录;食堂归属按部门聚合缓存一次查询。
*/
diff --git a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/feign/RemoteUserService.java b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/feign/RemoteUserService.java
index 651949d25..65d29b438 100644
--- a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/feign/RemoteUserService.java
+++ b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/feign/RemoteUserService.java
@@ -146,6 +146,23 @@ public interface RemoteUserService {
@NoToken
R> getUserIdListByDeptIdList(@RequestParam("deptIdList") List deptIdList);
+ /**
+ * 离线人员同步的固定快照上界;不传部门时为全租户,仅非生产调试同步使用。
+ */
+ @GetMapping("/user/inner/offline-user-max-id")
+ @NoToken
+ R getOfflineUserMaxId(@RequestParam(value = "deptIds", required = false) List deptIds);
+
+ /**
+ * 离线人员同步的游标用户页;按主键升序返回大于 afterId 的一页用户。
+ */
+ @GetMapping("/user/inner/offline-user-cursor")
+ @NoToken
+ R> getOfflineUserCursor(
+ @RequestParam(value = "deptIds", required = false) List deptIds,
+ @RequestParam("afterId") Long afterId,
+ @RequestParam("limit") Integer limit);
+
/**
* 通过用户名查询用户列表
*
diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java
index 05080e9d2..8b661e420 100644
--- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java
+++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java
@@ -437,6 +437,36 @@ public class SysUserController {
return R.ok(userService.listUserIdByDeptIds(CollUtil.toList(deptIdList)));
}
+ /**
+ * 离线人员同步的固定快照上界:部门树内(不传部门时为全租户)的最大用户主键。
+ *
+ * @param deptIds 部门ID列表;空表示全租户,仅非生产调试同步使用
+ * @return 最大用户主键;无有效用户时为 0
+ */
+ @Inner
+ @GetMapping("/inner/offline-user-max-id")
+ public R offlineUserMaxId(
+ @RequestParam(value = "deptIds", required = false) List deptIds) {
+ return R.ok(userService.maxUserIdByDeptIds(deptIds));
+ }
+
+ /**
+ * 离线人员同步的游标用户页:按主键升序返回大于 afterId 的一页用户。
+ *
+ * @param deptIds 部门ID列表;空表示全租户
+ * @param afterId 排他游标
+ * @param limit 返回条数上限,服务端钳制在 1 到 501 之间
+ * @return 按主键升序的一页用户
+ */
+ @Inner
+ @GetMapping("/inner/offline-user-cursor")
+ public R> offlineUserCursor(
+ @RequestParam(value = "deptIds", required = false) List deptIds,
+ @RequestParam Long afterId,
+ @RequestParam Integer limit) {
+ return R.ok(userService.listByUserIdCursor(deptIds, afterId, limit));
+ }
+
/**
* 根据部门id查询用户列表
diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysUserService.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysUserService.java
index 6664304d8..9e61b1f15 100644
--- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysUserService.java
+++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysUserService.java
@@ -196,6 +196,24 @@ public interface SysUserService extends IService {
*/
List listUserIdByDeptIds(List deptIdList);
+ /**
+ * 查询部门范围内(不传部门时为全租户)的最大用户主键,作为离线人员同步的固定快照上界。
+ *
+ * @param deptIdList 部门ID列表;空表示全租户
+ * @return 最大用户主键;无有效用户时为 0
+ */
+ Long maxUserIdByDeptIds(List deptIdList);
+
+ /**
+ * 按用户主键升序游标查询部门范围内(不传部门时为全租户)的一页用户。
+ *
+ * @param deptIdList 部门ID列表;空表示全租户
+ * @param afterId 排他游标,返回主键严格大于该值的用户
+ * @param limit 返回条数上限,服务端钳制在 1 到 501 之间
+ * @return 按主键升序的一页用户
+ */
+ List listByUserIdCursor(List deptIdList, Long afterId, Integer limit);
+
/**
* 重置用户密码
*
diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java
index b6da12e7c..db2d39a3d 100644
--- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java
+++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java
@@ -954,6 +954,26 @@ public class SysUserServiceImpl extends ServiceImpl impl
return baseMapper.selectList(Wrappers.lambdaQuery().in(SysUser::getDeptId, deptIdList));
}
+ @Override
+ public Long maxUserIdByDeptIds(List deptIdList) {
+ SysUser top = baseMapper.selectOne(Wrappers.lambdaQuery()
+ .select(SysUser::getUserId)
+ .in(CollUtil.isNotEmpty(deptIdList), SysUser::getDeptId, deptIdList)
+ .orderByDesc(SysUser::getUserId)
+ .last("limit 1"));
+ return top == null || top.getUserId() == null ? 0L : top.getUserId();
+ }
+
+ @Override
+ public List listByUserIdCursor(List deptIdList, Long afterId, Integer limit) {
+ int normalizedLimit = limit == null ? 1 : Math.max(1, Math.min(limit, 501));
+ return baseMapper.selectList(Wrappers.lambdaQuery()
+ .gt(SysUser::getUserId, afterId == null ? 0L : afterId)
+ .in(CollUtil.isNotEmpty(deptIdList), SysUser::getDeptId, deptIdList)
+ .orderByAsc(SysUser::getUserId)
+ .last("limit " + normalizedLimit));
+ }
+
@Override
public List getUserDeptByIds(List userIds) {
return baseMapper.selectVoListByScope(new UserDTO(), userIds.toArray(new Long[0]), null);