This commit is contained in:
dzc committed 2026-09-10 13:55:01 +08:00
1 parent b254ef0783
commit 3effd6f9be
13 files changed
+275 -24

No files matched your search

@@ -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;
}
@@ -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
*/
@@ -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<List<ADCanteenDailyMenuListVO>> getCanteenListByDateRange(
@Validated @RequestBody DailyMenuDateRangeQueryDTO request) {
return R.ok(canteenDailyMenuService.getCanteenMenuListByDateRange(request));
}
// --------------APP---------------
@@ -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<CanteenDailyMe
List<ADCanteenDailyMenuListVO> 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<ADCanteenDailyMenuListVO> getCanteenMenuListByDateRange(@Param("request") DailyMenuDateRangeQueryDTO request,
DataScope dataScope);
Page<DailyMenuAppVO> selectTimeFoodPage(@Param("page")Page page, DailyMenuDTO dailyMenuDTO);
@@ -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<CanteenDailyMenuEntity
*/
List<ADCanteenDailyMenuListVO> getCanteenMenuListScope(String day, Integer hourType, Long categoryId, Long merchantId);
/**
* 按闭区间查询收银终端多日菜谱,返回字段与单日查询保持一致,
* 每条记录额外携带供应日期,供终端按日期分桶落库。
*
* @param request 日期范围、可选筛选条件和收银终端所属商户
* @return 按日期、餐段和商品排序的菜谱列表
*/
List<ADCanteenDailyMenuListVO> getCanteenMenuListByDateRange(DailyMenuDateRangeQueryDTO request);
Page selectTimeFoodPage(Page page, DailyMenuDTO dailyMenuDTO);
@@ -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<CanteenDailyMenuMap
return baseMapper.getCanteenMenuList(day, hourType, categoryId,merchantId, DataScope.of());
}
/**
* 按闭区间查询收银终端多日菜谱。
*
* @param request 日期范围、可选筛选条件和收银终端所属商户
* @return 按日期、餐段和商品排序升序排列的菜谱列表
*/
@Override
public List<ADCanteenDailyMenuListVO> 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<DailyMenuAppVO> pageData = baseMapper.selectTimeFoodPage(page, dailyMenuDTO);
@@ -19,6 +19,7 @@
<resultMap id="adCanteenDailyMenuListVO" type="com.bsd.pjcloud.canteen.vo.ADCanteenDailyMenuListVO">
<id property="id" column="id"/>
<result property="saleDate" column="start_time"/>
<result property="name" column="name"/>
<result property="spuId" column="spu_id"/>
<result property="hourType" column="hour_type"/>
@@ -61,6 +62,7 @@
SELECT
sp.id as spu_id,
t.id,
t.start_time,
sp.name,
sp.pic_url,
t.hour_type,
@@ -90,6 +92,38 @@
</if>
</where>
</select>
<select id="getCanteenMenuListByDateRange" resultMap="adCanteenDailyMenuListVO">
SELECT
sp.id as spu_id,
t.id,
t.start_time,
sp.name,
sp.pic_url,
t.hour_type,
sp.price,
sp.stock,
sp.category_id,
sp.sort,
sp.is_price_increased,
<!-- 数据权限拦截器会在当前查询外层按部门和创建人过滤,必须在临时结果集中透传这两个字段。 -->
t.dept_id,
t.create_by
FROM canteen_daily_menu t
LEFT JOIN canteen_spu sp
on t.spu_id=sp.id
<where>
sp.del_flag = 0 and t.del_flag = 0
AND t.start_time BETWEEN #{request.startDate} AND #{request.endDate}
<if test="request.categoryId != null">
AND sp.category_id = #{request.categoryId}
</if>
<if test="request.hourType != null">
AND t.hour_type = #{request.hourType}
</if>
AND t.merchant_id = #{request.merchantId}
</where>
order by t.start_time asc, t.hour_type asc, sp.sort asc
</select>
<select id="selectTimeFoodPage" resultType="com.bsd.pjcloud.canteen.vo.app.DailyMenuAppVO"
parameterType="com.bsd.pjcloud.canteen.dto.app.DailyMenuDTO">
SELECT m.id,
@@ -34,4 +34,9 @@ public class HardOfflineUserSyncDto {
*/
@Min(value = 0, message = "离线人员同步快照上界无效")
private Long snapshotMaxId;
/**
* 是否同步全租户人员;仅非生产调试使用,生产端点行为与本部门一致。
*/
private Boolean allTenant;
}
@@ -27,11 +27,11 @@ import com.bsd.pjcloud.wallet.api.feign.RemoteWalletService;
import com.bsd.pjcloud.wallet.api.vo.MchManageVo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -57,6 +57,7 @@ public class HardApiServiceImpl implements HardApiService {
private final RemoteDeptService remoteDeptService;
private final HardManagerMapper hardManagerMapper;
private final HardManagerService hardManagerService;
private final Environment environment;
@Override
public HardAdUserVo getUserById(Long userId, HardTypeEnum type) {
@@ -121,12 +122,13 @@ public class HardApiServiceImpl implements HardApiService {
}
/**
* 按设备绑定部门树返回脱敏后的离线人员游标快照。
* 按设备绑定部门树(或全租户)返回脱敏后的离线人员游标快照。
*
* <p>部门范围、钱包类型全部由 DEVICE-ID 对应的真实绑定关系解析;姓名只保留
* 首字符,其余以星号替代,且不返回手机号和头像。未绑定当前类型钱包、卡号为空
* 或钱包已冻结的用户不进入快照;游标始终按部门全部用户主键推进,被过滤用户
* 不会造成分页重复。</p>
* 或钱包已冻结的用户不进入快照;游标始终按用户主键推进,被过滤用户不会造成
* 分页重复。allTenant 仅用于非生产调试的全租户同步,生产环境调用时仍按设备
* 绑定部门返回。</p>
*/
@Override
public HardOfflineUserSyncSnapshotVo syncOfflineUserSnapshot(Long hardId, HardOfflineUserSyncDto dto) {
@@ -140,27 +142,25 @@ public class HardApiServiceImpl implements HardApiService {
? WalletTypeEnum.EAT
: WalletTypeEnum.PAY;
List<Long> deptIds = resolveDepartmentTree(trustedDevice.merchantDeptId());
if (deptIds.isEmpty()) {
final boolean allTenant = Boolean.TRUE.equals(dto.getAllTenant()) && !isProdProfile();
final List<Long> deptIds = allTenant
? null
: resolveDepartmentTree(trustedDevice.merchantDeptId());
if (!allTenant && deptIds.isEmpty()) {
throw new RuntimeException("设备绑定部门不存在或已删除");
}
R<List<SysUser>> 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<SysUser> 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<Long> 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<SysUser> fetchedPage = users.stream()
.filter(user -> user.getUserId() > normalizedAfterId
&& user.getUserId() <= normalizedSnapshotMaxId)
.limit(normalizedLimit + 1L)
.toList();
// 多取一条用于判断是否还有下一页;用户过滤发生在钱包聚合之后,不影响游标推进。
List<SysUser> fetchedPage = fetchUserCursorPage(deptIds, normalizedAfterId, normalizedLimit + 1);
boolean hasMore = fetchedPage.size() > normalizedLimit;
List<SysUser> pageUsers = hasMore ? fetchedPage.subList(0, normalizedLimit) : fetchedPage;
@@ -187,6 +184,34 @@ public class HardApiServiceImpl implements HardApiService {
return snapshot;
}
/**
* 从用户服务按主键游标取一页部门范围(空为全租户)内的用户并过滤无效记录。
*/
private List<SysUser> fetchUserCursorPage(List<Long> deptIds, long afterId, int limit) {
R<List<SysUser>> 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;
}
/**
* 将一页部门用户组装为脱敏离线人员记录;食堂归属按部门聚合缓存一次查询。
*/
@@ -146,6 +146,23 @@ public interface RemoteUserService {
@NoToken
R<List<SysUser>> getUserIdListByDeptIdList(@RequestParam("deptIdList") List<Long> deptIdList);
/**
* 离线人员同步的固定快照上界;不传部门时为全租户,仅非生产调试同步使用。
*/
@GetMapping("/user/inner/offline-user-max-id")
@NoToken
R<Long> getOfflineUserMaxId(@RequestParam(value = "deptIds", required = false) List<Long> deptIds);
/**
* 离线人员同步的游标用户页;按主键升序返回大于 afterId 的一页用户。
*/
@GetMapping("/user/inner/offline-user-cursor")
@NoToken
R<List<SysUser>> getOfflineUserCursor(
@RequestParam(value = "deptIds", required = false) List<Long> deptIds,
@RequestParam("afterId") Long afterId,
@RequestParam("limit") Integer limit);
/**
* 通过用户名查询用户列表
*
@@ -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<Long> offlineUserMaxId(
@RequestParam(value = "deptIds", required = false) List<Long> 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<List<SysUser>> offlineUserCursor(
@RequestParam(value = "deptIds", required = false) List<Long> deptIds,
@RequestParam Long afterId,
@RequestParam Integer limit) {
return R.ok(userService.listByUserIdCursor(deptIds, afterId, limit));
}
/**
* 根据部门id查询用户列表
@@ -196,6 +196,24 @@ public interface SysUserService extends IService<SysUser> {
*/
List<SysUser> listUserIdByDeptIds(List<Long> deptIdList);
/**
* 查询部门范围内(不传部门时为全租户)的最大用户主键,作为离线人员同步的固定快照上界。
*
* @param deptIdList 部门ID列表;空表示全租户
* @return 最大用户主键;无有效用户时为 0
*/
Long maxUserIdByDeptIds(List<Long> deptIdList);
/**
* 按用户主键升序游标查询部门范围内(不传部门时为全租户)的一页用户。
*
* @param deptIdList 部门ID列表;空表示全租户
* @param afterId 排他游标,返回主键严格大于该值的用户
* @param limit 返回条数上限,服务端钳制在 1 到 501 之间
* @return 按主键升序的一页用户
*/
List<SysUser> listByUserIdCursor(List<Long> deptIdList, Long afterId, Integer limit);
/**
* 重置用户密码
*
@@ -954,6 +954,26 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
return baseMapper.selectList(Wrappers.<SysUser>lambdaQuery().in(SysUser::getDeptId, deptIdList));
}
@Override
public Long maxUserIdByDeptIds(List<Long> deptIdList) {
SysUser top = baseMapper.selectOne(Wrappers.<SysUser>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<SysUser> listByUserIdCursor(List<Long> deptIdList, Long afterId, Integer limit) {
int normalizedLimit = limit == null ? 1 : Math.max(1, Math.min(limit, 501));
return baseMapper.selectList(Wrappers.<SysUser>lambdaQuery()
.gt(SysUser::getUserId, afterId == null ? 0L : afterId)
.in(CollUtil.isNotEmpty(deptIdList), SysUser::getDeptId, deptIdList)
.orderByAsc(SysUser::getUserId)
.last("limit " + normalizedLimit));
}
@Override
public List<UserVO> getUserDeptByIds(List<Long> userIds) {
return baseMapper.selectVoListByScope(new UserDTO(), userIds.toArray(new Long[0]), null);