feat:以通道的方式展示摄像头

This commit is contained in:
dzc committed 2026-09-17 18:30:11 +08:00
1 parent b44fe6f659
commit 10404de331
8 files changed
+273 -31

No files matched your search

@@ -81,6 +81,12 @@ public class HardOrderDto extends HardOrderEntity implements BaseIsIncludeTreeIn
/** 预约用餐日期下限,用于查询今天及之后的未核销预约。 */
private LocalDateTime apptUseDateStart;
/**
* 统一用餐日期范围:预约单按预约使用日匹配,线下等其他类型按支付时间匹配;
* 仅 ward 系列用餐接口使用。
*/
private LocalDateTime[] mealDateRange;
private List<String> orderTypeCodes;
private List<String> orderPayTypeCodes;
@@ -133,6 +133,12 @@ public class HardOrderVo extends HardOrderEntity implements BaseUserDeptInterfac
*/
private Boolean canViewReserveCode;
/**
* 统一用餐日期(yyyy-MM-dd):预约单取预约使用日,线下等其他类型取支付日;
* 仅 ward 系列用餐接口回填,不对应 hard_order 表持久化字段。
*/
private String mealDate;
/**
* 订单详情中的长期识别证据列表,按证据类型、角色和序号排序。
*/
@@ -6,10 +6,10 @@ import lombok.Data;
import java.time.LocalDate;
/**
* 预约菜谱日历地图的单日聚合:按用餐日统计未退款预约订单的核销情况。
* 用餐日历地图的单日聚合:按统一用餐日统计未退款订单的核销与消费情况。
*/
@Data
@Schema(description = "预约菜谱日历地图单日聚合")
@Schema(description = "用餐日历地图单日聚合")
public class WardMenuMapDayVo {
@Schema(description = "用餐日")
@@ -20,4 +20,7 @@ public class WardMenuMapDayVo {
@Schema(description = "已核销预约单数(订单状态 20)")
private Integer verifiedCount;
@Schema(description = "线下等其他类型订单数(预约外全类型,已支付口径)")
private Integer offlineCount;
}
@@ -177,7 +177,7 @@ public class HardOrderController {
return R.ok(orders);
}
@Operation(summary = "app查询预约菜谱日历地图", description = "按日期范围聚合本人或被监护人每天的预约核销情况,整单退款的订单不展示")
@Operation(summary = "app查询用餐日历地图", description = "按日期范围聚合本人或被监护人每天的用餐情况:预约待核销/已核销与线下等其他类型订单数,整单退款的订单不统计")
@ApiCryptoAes
@GetMapping("/app/ward-menu-map")
public R<List<WardMenuMapDayVo>> wardMenuMap(@RequestParam(required = false) List<Long> dinerUserIds,
@@ -190,22 +190,46 @@ public class HardOrderController {
: hardwareOrderService.validateReserveDinerUserIds(user.getId(), dinerUserIds));
dto.setIsFindCarList(false);
dto.setIsFindApptList(true);
dto.setApptIdOrApptRuleIdNotNull(true);
dto.setOrderType(HardOrderTypeEnum.RESERVE.getType());
// 只统计待核销(5)与已核销(20);退款中(10)、已取消(30)和未支付订单不进入地图。
dto.setOrderStatusCodes(List.of(HardOrderStatusEnum.PAY.getType(), HardOrderStatusEnum.CANCEL.getType()));
dto.setApptUseDateTimeList(new LocalDateTime[]{
// 只统计待核销(5)、线下本地支付(6)与已核销(20);退款中(10)、已取消(30)和未支付订单不进入地图。
dto.setOrderStatusCodes(List.of(
HardOrderStatusEnum.PAY.getType(),
HardOrderStatusEnum.OFFLINE_PAY.getType(),
HardOrderStatusEnum.CANCEL.getType()));
// 统一用餐日期口径:预约单按预约使用日匹配,线下等其他类型按支付时间匹配。
dto.setMealDateRange(new LocalDateTime[]{
LocalDate.parse(startDate).atStartOfDay(),
LocalDate.parse(endDate).atTime(23, 59, 59),
});
return R.ok(hardwareOrderService.wardMenuMap(dto));
}
@Operation(summary = "app查询预约菜谱某日订单明细", description = "查询某天本人或被监护人的预约订单明细,整单退款的订单不返回")
@Operation(summary = "app查询用餐某日订单明细", description = "查询某天本人或被监护人的全类型用餐订单明细,整单退款的订单不返回")
@ApiCryptoAes
@GetMapping("/app/ward-menu-day-orders")
public R<List<HardOrderVo>> wardMenuDayOrders(@RequestParam(required = false) List<Long> dinerUserIds,
@RequestParam String date) {
return R.ok(wardMenuOrdersResponse(dinerUserIds, date, date));
}
@Operation(summary = "app查询用餐订单窗口列表", description = "按日期窗口查询本人或被监护人的全类型用餐订单明细,整单退款的订单不返回")
@ApiCryptoAes
@GetMapping("/app/ward-menu-orders")
public R<List<HardOrderVo>> wardMenuOrders(@RequestParam(required = false) List<Long> dinerUserIds,
@RequestParam String startDate,
@RequestParam String endDate) {
return R.ok(wardMenuOrdersResponse(dinerUserIds, startDate, endDate));
}
/**
* 组装 ward 系列用餐明细查询:校验查看对象后按统一用餐日期窗口查询全类型订单,
* 某日明细与日期窗口明细共用同一口径,避免两接口统计范围出现偏差。
*
* @param dinerUserIds 查看对象用户 ID,空表示当前登录用户本人
* @param startDate 用餐日期窗口起(含),格式 yyyy-MM-dd
* @param endDate 用餐日期窗口止(含),格式 yyyy-MM-dd
* @return 已回填统一用餐日期和操作权限的全类型用餐订单
*/
private List<HardOrderVo> wardMenuOrdersResponse(List<Long> dinerUserIds, String startDate, String endDate) {
HardOrderDto dto = new HardOrderDto();
PjcloudUser user = SecurityUtils.getUser();
dto.setUserIds(ObjectUtil.isEmpty(dinerUserIds)
@@ -213,17 +237,19 @@ public class HardOrderController {
: hardwareOrderService.validateReserveDinerUserIds(user.getId(), dinerUserIds));
dto.setIsFindCarList(true);
dto.setIsFindApptList(true);
dto.setApptIdOrApptRuleIdNotNull(true);
dto.setOrderType(HardOrderTypeEnum.RESERVE.getType());
// 与日历地图同口径:只返回待核销(5)与已核销(20),整单退款在服务层过滤。
dto.setOrderStatusCodes(List.of(HardOrderStatusEnum.PAY.getType(), HardOrderStatusEnum.CANCEL.getType()));
dto.setApptUseDateTimeList(new LocalDateTime[]{
LocalDate.parse(date).atStartOfDay(),
LocalDate.parse(date).atTime(23, 59, 59),
// 与用餐日历地图同口径:待核销(5)、线下本地支付(6)、已核销(20),整单退款在服务层过滤。
dto.setOrderStatusCodes(List.of(
HardOrderStatusEnum.PAY.getType(),
HardOrderStatusEnum.OFFLINE_PAY.getType(),
HardOrderStatusEnum.CANCEL.getType()));
// 统一用餐日期口径:预约单按预约使用日匹配,线下等其他类型按支付时间匹配。
dto.setMealDateRange(new LocalDateTime[]{
LocalDate.parse(startDate).atStartOfDay(),
LocalDate.parse(endDate).atTime(23, 59, 59),
});
List<HardOrderVo> orders = hardwareOrderService.wardMenuDayOrders(dto);
List<HardOrderVo> orders = hardwareOrderService.wardMenuOrders(dto);
hardwareOrderService.applyAppPermissions(orders, user.getId());
return R.ok(orders);
return orders;
}
@@ -198,15 +198,20 @@ public interface HardOrderService extends IService<HardOrderEntity> {
List<Long> validateReserveDinerUserIds(Long payerUserId, List<Long> dinerUserIds);
/**
* app 预约菜谱日历地图:按用餐日聚合未退款预约订单的待核销/已核销数量。
* app 用餐日历地图:按统一用餐日聚合未退款订单,预约单分待核销/已核销,其余类型计线下消费。
*/
List<WardMenuMapDayVo> wardMenuMap(HardOrderDto dto);
/**
* app 预约菜谱某日明细:返回当天未整单退款的待核销/已核销预约订单。
* app 用餐某日明细:返回当天未整单退款的全类型用餐订单,并回填统一用餐日期。
*/
List<HardOrderVo> wardMenuDayOrders(HardOrderDto dto);
/**
* app 用餐订单窗口列表:返回日期窗口内未整单退款的全类型用餐订单,并回填统一用餐日期。
*/
List<HardOrderVo> wardMenuOrders(HardOrderDto dto);
/**
* app创建食堂外卖订单
*/
@@ -2466,41 +2466,71 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
}
/**
* app 预约菜谱日历地图:按用餐日聚合未退款预约订单的待核销/已核销数量。
* 整单退完(退款金额为正且不低于实付金额)的订单不再计入。
* app 用餐日历地图:按统一用餐日聚合未退款订单,预约单分待核销/已核销计数,
* 线下消费、外卖等其他类型统一计入线下消费数。整单退完(退款金额为正且不低于实付金额)的订单不再计入。
*/
@Override
public List<WardMenuMapDayVo> wardMenuMap(HardOrderDto dto) {
List<HardOrderVo> orders = scopeList(dto, null);
Map<LocalDate, WardMenuMapDayVo> dayMap = new TreeMap<>();
for (HardOrderVo order : orders) {
if (order.getApptUseDate() == null || isFullyRefundedOrder(order)) {
LocalDate mealDate = resolveOrderMealDate(order);
if (mealDate == null || isFullyRefundedOrder(order)) {
continue;
}
WardMenuMapDayVo day = dayMap.computeIfAbsent(order.getApptUseDate(), date -> {
WardMenuMapDayVo day = dayMap.computeIfAbsent(mealDate, date -> {
WardMenuMapDayVo item = new WardMenuMapDayVo();
item.setApptUseDate(date);
item.setPendingCount(0);
item.setVerifiedCount(0);
item.setOfflineCount(0);
return item;
});
if (HardOrderStatusEnum.PAY.getType().equals(order.getOrderStatus())) {
day.setPendingCount(day.getPendingCount() + 1);
} else if (HardOrderStatusEnum.CANCEL.getType().equals(order.getOrderStatus())) {
day.setVerifiedCount(day.getVerifiedCount() + 1);
if (HardOrderTypeEnum.RESERVE.getType().equals(order.getOrderType())) {
if (HardOrderStatusEnum.PAY.getType().equals(order.getOrderStatus())) {
day.setPendingCount(day.getPendingCount() + 1);
} else if (HardOrderStatusEnum.CANCEL.getType().equals(order.getOrderStatus())) {
day.setVerifiedCount(day.getVerifiedCount() + 1);
}
} else {
day.setOfflineCount(day.getOfflineCount() + 1);
}
}
return new ArrayList<>(dayMap.values());
}
/**
* app 预约菜谱某日明细:返回当天未整单退款的待核销/已核销预约订单,与日历地图同口径。
* app 用餐某日明细:返回当天未整单退款的全类型用餐订单,并回填统一用餐日期,与日历地图同口径。
*/
@Override
public List<HardOrderVo> wardMenuDayOrders(HardOrderDto dto) {
return scopeList(dto, null).stream()
.filter(order -> !isFullyRefundedOrder(order))
return wardMenuOrders(dto);
}
/**
* app 用餐订单窗口列表:返回统一用餐日期窗口内未整单退款的全类型用餐订单,并回填统一用餐日期。
* 无法解析用餐日期的订单(预约缺使用日且无支付时间)不返回,避免前端落入脏数据兜底日。
*/
@Override
public List<HardOrderVo> wardMenuOrders(HardOrderDto dto) {
List<HardOrderVo> orders = scopeList(dto, null).stream()
.filter(order -> resolveOrderMealDate(order) != null && !isFullyRefundedOrder(order))
.toList();
for (HardOrderVo order : orders) {
order.setMealDate(resolveOrderMealDate(order).toString());
}
return orders;
}
/**
* 解析订单统一用餐日期:预约单取预约使用日,线下消费、外卖等其他类型取支付日;
* 两者皆缺失时返回 null,调用方按无法归日处理。
*/
private LocalDate resolveOrderMealDate(HardOrderVo order) {
if (HardOrderTypeEnum.RESERVE.getType().equals(order.getOrderType()) && order.getApptUseDate() != null) {
return order.getApptUseDate();
}
return order.getPayTime() == null ? null : order.getPayTime().toLocalDate();
}
/**
@@ -333,6 +333,19 @@
</if>
</sql>
<!--
统一用餐日期口径:预约单(order_type='2')按预约使用日匹配,
线下消费、外卖等其他类型按支付时间匹配;仅 ward 系列用餐接口设置该范围。
-->
<sql id="mealDateRangeWhere">
<if test="dto.mealDateRange != null and dto.mealDateRange.length == 2">
and (
(a.order_type = '2' and a.appt_use_date between #{dto.mealDateRange[0]} and #{dto.mealDateRange[1]})
or (a.order_type &lt;&gt; '2' and a.pay_time between #{dto.mealDateRange[0]} and #{dto.mealDateRange[1]})
)
</if>
</sql>
<!-- App 主键分页和总数统计共用相同的关联筛选,确保页记录与 total 语义一致。 -->
<sql id="appRelationFilterWhere">
<if test="dto.hardTypeCode != null and dto.hardTypeCode != ''">
@@ -431,6 +444,7 @@
<where>
<include refid="whereA"/>
<include refid="appVisibilityWhere"/>
<include refid="mealDateRangeWhere"/>
<if test="dto.hardTypeCode != null and dto.hardTypeCode != '' ">
and m.type = #{dto.hardTypeCode}
</if>
@@ -0,0 +1,152 @@
package com.bsd.pjcloud.hard.service.impl.order;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.bsd.pjcloud.hard.api.constant.order.HardOrderStatusEnum;
import com.bsd.pjcloud.hard.api.constant.order.HardOrderTypeEnum;
import com.bsd.pjcloud.hard.api.dto.order.HardOrderDto;
import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity;
import com.bsd.pjcloud.hard.api.vo.order.HardOrderVo;
import com.bsd.pjcloud.hard.api.vo.order.WardMenuMapDayVo;
import com.bsd.pjcloud.hard.mapper.order.HardOrderMapper;
import com.bsd.pjcloud.hard.support.HardUserIdentitySupport;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Answers.CALLS_REAL_METHODS;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* 用餐日历地图与窗口明细测试:预约单按状态分待核销/已核销计数,线下等其他类型统一计入线下消费,
* 整单退款订单不统计,返回明细回填统一用餐日期。
*/
class HardOrderServiceImplWardMenuTest {
/** 订单 Mapper,scopeList 打桩返回构造订单,隔离 SQL 与展示装配逻辑。 */
private final HardOrderMapper hardOrderMapper = mock(HardOrderMapper.class);
/** 身份筛选支持,默认关闭身份条件避免测试触发用户主键预筛选。 */
private final HardUserIdentitySupport userIdentitySupport = mock(HardUserIdentitySupport.class);
/** 被测订单服务,仅执行真实方法。 */
private final HardOrderServiceImpl service = mock(HardOrderServiceImpl.class, CALLS_REAL_METHODS);
/**
* 为被测服务注入查询链路实际访问的基础依赖。
*/
@BeforeEach
void setUp() {
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), "test"),
HardOrderEntity.class);
ReflectionTestUtils.setField(service, "baseMapper", hardOrderMapper);
ReflectionTestUtils.setField(service, "userIdentitySupport", userIdentitySupport);
when(userIdentitySupport.hasIdentityQuery(any())).thenReturn(false);
}
/**
* 同一天内:待核销预约、已核销预约、线下消费分别计数;整单退款订单不计入任何维度。
*/
@Test
void wardMenuMapAggregatesReserveStatusAndOfflineCount() {
LocalDate mealDate = LocalDate.of(2026, 9, 18);
List<HardOrderVo> orders = List.of(
order(HardOrderTypeEnum.RESERVE.getType(), HardOrderStatusEnum.PAY.getType(), mealDate, null, 1500, 0),
order(HardOrderTypeEnum.RESERVE.getType(), HardOrderStatusEnum.CANCEL.getType(), mealDate, null, 1200, 0),
order(HardOrderTypeEnum.BELOW.getType(), HardOrderStatusEnum.CANCEL.getType(), null,
LocalDateTime.of(2026, 9, 18, 12, 10), 800, 0),
// 整单退款:退款金额不低于实付金额,不进入地图
order(HardOrderTypeEnum.BELOW.getType(), HardOrderStatusEnum.CANCEL.getType(), null,
LocalDateTime.of(2026, 9, 18, 18, 30), 900, 900));
when(hardOrderMapper.scopeList(any(HardOrderDto.class), isNull())).thenReturn(orders);
List<WardMenuMapDayVo> days = service.wardMenuMap(new HardOrderDto());
assertEquals(1, days.size());
WardMenuMapDayVo day = days.get(0);
assertEquals(mealDate, day.getApptUseDate());
assertEquals(1, day.getPendingCount());
assertEquals(1, day.getVerifiedCount());
assertEquals(1, day.getOfflineCount());
}
/**
* 预约单按预约使用日归组,线下等其他类型按支付日归组,两类订单落在不同日期时分别出条目。
*/
@Test
void wardMenuMapGroupsByUnifiedMealDate() {
List<HardOrderVo> orders = List.of(
order(HardOrderTypeEnum.RESERVE.getType(), HardOrderStatusEnum.PAY.getType(),
LocalDate.of(2026, 9, 20), null, 1500, 0),
order(HardOrderTypeEnum.TAKE_OUT.getType(), HardOrderStatusEnum.PAY.getType(), null,
LocalDateTime.of(2026, 9, 17, 19, 0), 2000, 0));
when(hardOrderMapper.scopeList(any(HardOrderDto.class), isNull())).thenReturn(orders);
List<WardMenuMapDayVo> days = service.wardMenuMap(new HardOrderDto());
assertEquals(2, days.size());
assertEquals(LocalDate.of(2026, 9, 17), days.get(0).getApptUseDate());
assertEquals(1, days.get(0).getOfflineCount());
assertEquals(LocalDate.of(2026, 9, 20), days.get(1).getApptUseDate());
assertEquals(1, days.get(1).getPendingCount());
}
/**
* 窗口明细回填统一用餐日期:预约单取预约使用日,线下等其他类型取支付日;
* 无法解析用餐日期的订单不返回。
*/
@Test
void wardMenuOrdersFillsUnifiedMealDate() {
List<HardOrderVo> orders = List.of(
order(HardOrderTypeEnum.RESERVE.getType(), HardOrderStatusEnum.PAY.getType(),
LocalDate.of(2026, 9, 20), null, 1500, 0),
order(HardOrderTypeEnum.BELOW.getType(), HardOrderStatusEnum.CANCEL.getType(), null,
LocalDateTime.of(2026, 9, 18, 12, 10), 800, 0),
// 无预约使用日且无支付时间的脏数据,不返回
order(HardOrderTypeEnum.BELOW.getType(), HardOrderStatusEnum.CANCEL.getType(), null, null, 100, 0));
when(hardOrderMapper.scopeList(any(HardOrderDto.class), isNull())).thenReturn(orders);
List<HardOrderVo> result = service.wardMenuOrders(new HardOrderDto());
assertEquals(2, result.size());
assertTrue(result.stream().anyMatch(order -> "2026-09-20".equals(order.getMealDate())));
assertTrue(result.stream().anyMatch(order -> "2026-09-18".equals(order.getMealDate())));
}
/** 测试订单 ID 序号,仅保证列表内 ID 唯一。 */
private long orderIdSequence = 0L;
/**
* 构造一笔用餐订单快照。
*
* @param orderType 订单类型协议编码
* @param orderStatus 订单状态协议编码
* @param apptUseDate 预约使用日;非预约订单传 null
* @param payTime 支付完成时间;预约订单可传 null
* @param payPrice 实付金额,单位为分
* @param refundPrice 累计已完成退款金额,单位为分
* @return 可供聚合逻辑使用的订单
*/
private HardOrderVo order(String orderType, String orderStatus, LocalDate apptUseDate,
LocalDateTime payTime, int payPrice, int refundPrice) {
HardOrderVo item = new HardOrderVo();
item.setId(++orderIdSequence);
item.setOrderType(orderType);
item.setOrderStatus(orderStatus);
item.setApptUseDate(apptUseDate);
item.setPayTime(payTime);
item.setPayPrice(payPrice);
item.setRefundPrice(refundPrice);
return item;
}
}