diff --git a/db/20260823_add_hard_order_verification_time.sql b/db/20260823_add_hard_order_verification_time.sql
new file mode 100644
index 00000000..344c3c96
--- /dev/null
+++ b/db/20260823_add_hard_order_verification_time.sql
@@ -0,0 +1,16 @@
+-- 硬件订单新增独立核销时间。
+-- 历史二维码核销订单只能使用最后更新时间近似回填;外卖等非核销完成状态不参与回填。
+-- 新核销订单由业务事务写入准确时间,后续订单更新不再改变该值。
+
+ALTER TABLE `pjcloudx_hardware`.`hard_order`
+ ADD COLUMN `verification_time` datetime DEFAULT NULL
+ COMMENT '订单实际核销完成时间,后续订单更新不得覆盖' AFTER `refund_status`,
+ ADD KEY `idx_hard_order_verification_time`
+ (`tenant_id`, `verification_time`, `del_flag`);
+
+UPDATE `pjcloudx_hardware`.`hard_order`
+SET `verification_time` = `update_time`
+WHERE ((`order_status` = '20' AND `order_type` IN ('1', '2', '3', '4'))
+ OR `pay_type` IN ('5', '6', '50'))
+ AND `verification_time` IS NULL
+ AND `update_time` IS NOT NULL;
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 e7ba33ec..80487abf 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
@@ -38,9 +38,12 @@ public class HardOrderDto extends HardOrderEntity implements BaseIsIncludeTreeIn
private LocalDateTime[] createTimeList;
- /** 更新时间范围采用左闭右开区间,供按订单实际状态变更时间查询。 */
+ /** 更新时间范围采用左闭右开区间,供兼容旧调用方按订单状态变更时间查询。 */
private LocalDateTime[] updateTimeList;
+ /** 订单核销时间闭区间,仅按订单实际核销完成时间查询。 */
+ private LocalDateTime[] verificationTimeList;
+
private LocalDateTime[] payTimeList;
private LocalDateTime[] refundTimeList;
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderEvidenceOrderRefDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderEvidenceOrderRefDto.java
new file mode 100644
index 00000000..510a7e4e
--- /dev/null
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderEvidenceOrderRefDto.java
@@ -0,0 +1,45 @@
+package com.bsd.pjcloud.hard.api.dto.order;
+
+import cn.hutool.core.util.StrUtil;
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.AssertTrue;
+import jakarta.validation.constraints.Positive;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 终端订单识别证据的订单定位参数。
+ *
+ *
消费机现场下单继续使用终端订单号对账;预约核销订单不是当前终端创建的,核销成功后改用
+ * 服务端订单 ID 定位。两种方式都必须结合请求头中的设备 ID 校验订单归属。
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Schema(description = "订单识别证据的订单定位参数")
+public abstract class HardOrderEvidenceOrderRefDto extends HardOrderEvidenceMetadataDto {
+
+ /**
+ * 终端本地稳定订单号,用于消费机现场下单后对账;预约核销证据允许为空。
+ */
+ @Size(max = 128, message = "终端订单号不能超过128个字符")
+ @Schema(description = "终端订单号,与服务端订单ID二选一")
+ private String androidOrderCode;
+
+ /**
+ * 已存在的服务端订单主键,仅用于当前设备已经成功核销的预约订单;现场下单证据允许为空。
+ */
+ @Positive(message = "服务端订单ID必须大于0")
+ @Schema(description = "服务端订单ID,与终端订单号二选一")
+ private Long orderId;
+
+ /**
+ * 校验至少提供一种订单定位信息,防止无条件查询或写入订单证据。
+ *
+ * @return 提供终端订单号或服务端订单 ID 时返回 {@code true}
+ */
+ @AssertTrue(message = "终端订单号和服务端订单ID不能同时为空")
+ public boolean isOrderReferencePresent() {
+ return StrUtil.isNotBlank(androidOrderCode) || orderId != null;
+ }
+}
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderEvidenceSubmitDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderEvidenceSubmitDto.java
index 99cd0168..343b0978 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderEvidenceSubmitDto.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderEvidenceSubmitDto.java
@@ -12,15 +12,7 @@ import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "订单证据元数据提交请求")
-public class HardOrderEvidenceSubmitDto extends HardOrderEvidenceMetadataDto {
-
- /**
- * 终端本地稳定订单号,用于接口响应丢失后的服务端订单对账。
- */
- @NotBlank(message = "终端订单号不能为空")
- @Size(max = 128, message = "终端订单号不能超过128个字符")
- @Schema(description = "终端订单号")
- private String androidOrderCode;
+public class HardOrderEvidenceSubmitDto extends HardOrderEvidenceOrderRefDto {
/**
* 终端请求写入的目标状态,仅允许 PENDING 或 UNAVAILABLE;现场图片上传成功由服务端改为 AVAILABLE。
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderEvidenceUploadDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderEvidenceUploadDto.java
index 1b02f844..4b8f3363 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderEvidenceUploadDto.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderEvidenceUploadDto.java
@@ -1,7 +1,6 @@
package com.bsd.pjcloud.hard.api.dto.order;
import io.swagger.v3.oas.annotations.media.Schema;
-import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -12,15 +11,7 @@ import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "订单现场证据上传请求")
-public class HardOrderEvidenceUploadDto extends HardOrderEvidenceMetadataDto {
-
- /**
- * 终端本地稳定订单号,服务端通过该编号解析真实订单主键。
- */
- @NotBlank(message = "终端订单号不能为空")
- @Size(max = 128, message = "终端订单号不能超过128个字符")
- @Schema(description = "终端订单号")
- private String androidOrderCode;
+public class HardOrderEvidenceUploadDto extends HardOrderEvidenceOrderRefDto {
/**
* 产生识别证据的终端应用版本,用于问题追溯。
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/order/HardOrderEntity.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/order/HardOrderEntity.java
index 7b73513d..d124723d 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/order/HardOrderEntity.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/order/HardOrderEntity.java
@@ -235,6 +235,12 @@ public class HardOrderEntity extends Model {
@Schema(description = "退款状态")
private String refundStatus;
+ /**
+ * 订单核销完成时间;仅在订单实际完成核销时写入,避免被后续退款等订单更新覆盖。
+ */
+ @Schema(description = "订单核销时间")
+ private LocalDateTime verificationTime;
+
/**
* 订单取消时间(退款订单)
*/
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/order/HardOrderEvidenceImageEntity.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/order/HardOrderEvidenceImageEntity.java
index 77bd85aa..e7e6c59b 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/order/HardOrderEvidenceImageEntity.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/entity/order/HardOrderEvidenceImageEntity.java
@@ -39,9 +39,9 @@ public class HardOrderEvidenceImageEntity extends Model resolve(@RequestHeader("DEVICE-ID") Long hardId,
- @RequestParam String androidOrderCode) {
- return R.ok(hardOrderEvidenceImageService.resolve(hardId, androidOrderCode));
+ @RequestParam(required = false) String androidOrderCode,
+ @RequestParam(required = false) Long orderId) {
+ return R.ok(hardOrderEvidenceImageService.resolve(hardId, androidOrderCode, orderId));
}
/**
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderEvidenceImageService.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderEvidenceImageService.java
index f46577a8..2312cabb 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderEvidenceImageService.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderEvidenceImageService.java
@@ -19,13 +19,14 @@ import java.util.List;
public interface HardOrderEvidenceImageService extends IService {
/**
- * 按终端订单号对账服务端订单和已登记证据。
+ * 按终端订单号或服务端订单 ID 对账订单和已登记证据。
*
* @param hardId 请求头中的当前设备主键
- * @param androidOrderCode 终端本地稳定订单号
+ * @param androidOrderCode 终端本地稳定订单号,预约核销证据允许为空
+ * @param orderId 服务端订单主键,现场下单证据允许为空
* @return 服务端订单及证据状态
*/
- HardOrderEvidenceResolveVo resolve(Long hardId, String androidOrderCode);
+ HardOrderEvidenceResolveVo resolve(Long hardId, String androidOrderCode, Long orderId);
/**
* 提交证据元数据、触发参考图复制或报告永久缺失。
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/VerificationRecordServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/VerificationRecordServiceImpl.java
index c4a17493..b9364e4d 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/VerificationRecordServiceImpl.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/VerificationRecordServiceImpl.java
@@ -22,15 +22,28 @@ import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
+/** 终端预约核销记录查询服务,统一使用订单独立核销时间完成筛选和展示。 */
@Service
@RequiredArgsConstructor
public class VerificationRecordServiceImpl implements VerificationRecordService {
+ /** 当前接口支持的预约核销类型协议值。 */
private static final String TYPE_RESERVATION = "RESERVATION";
+ /** 设备服务,用于校验请求设备存在且与查询设备一致。 */
private final HardManagerService hardManagerService;
+
+ /** 订单服务,用于按设备、订单类型、状态和核销时间分页查询。 */
private final HardOrderService hardOrderService;
+ /**
+ * 分页查询指定设备在日期范围内完成的预约核销记录。
+ *
+ * @param page 分页参数,页码最小为 1、每页最大为 50
+ * @param hardId 当前请求头中的设备主键
+ * @param dto 核销类型、设备和日期范围查询条件
+ * @return 已按核销时间倒序排列的终端核销记录分页
+ */
@Override
public Page page(Page> page, Long hardId, VerificationRecordQueryDto dto) {
if (!Objects.equals(hardId, dto.getHardId())) {
@@ -57,7 +70,7 @@ public class VerificationRecordServiceImpl implements VerificationRecordService
query.setOrderStatusCodes(List.of(HardOrderStatusEnum.CANCEL.getType()));
// 活动核销历史上复用了预约订单类型,支付方式范围需明确排除 ACT_CANCEL。
query.setExcludeOrderPayTypeCodes(List.of(HardPayTypeEnum.ACT_CANCEL.getType()));
- query.setUpdateTimeList(new LocalDateTime[]{startDate.atStartOfDay(), endDate.plusDays(1).atStartOfDay()});
+ query.setVerificationTimeList(new LocalDateTime[]{startDate.atStartOfDay(), endDate.atTime(23, 59, 59)});
long current = Math.max(1, page.getCurrent());
long size = Math.min(50, Math.max(1, page.getSize()));
@@ -71,6 +84,12 @@ public class VerificationRecordServiceImpl implements VerificationRecordService
return result;
}
+ /**
+ * 将订单查询结果转换为终端核销记录展示对象。
+ *
+ * @param order 已完成核销的预约订单
+ * @return 终端核销记录
+ */
private static VerificationRecordVo convert(HardOrderVo order) {
VerificationRecordVo vo = new VerificationRecordVo();
vo.setOrderId(order.getId());
@@ -87,7 +106,15 @@ public class VerificationRecordServiceImpl implements VerificationRecordService
return vo;
}
+ /**
+ * 解析核销记录的展示时间;新数据使用独立核销时间,旧数据按更新时间、支付时间、创建时间依次降级。
+ *
+ * @param order 已核销订单
+ * @return 可供终端展示的核销时间,历史数据全部缺失时返回 {@code null}
+ */
private static LocalDateTime resolveVerificationTime(HardOrderVo order) {
+ if (order.getVerificationTime() != null) return order.getVerificationTime();
+ // 历史订单在新增独立字段前没有核销时间,兼容返回最近更新时间,避免旧记录在终端列表中变为空。
if (order.getUpdateTime() != null) return order.getUpdateTime();
if (order.getPayTime() != null) return order.getPayTime();
return order.getCreateTime();
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCanteenApptTotalExportServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCanteenApptTotalExportServiceImpl.java
index 8a8698af..8a09f42f 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCanteenApptTotalExportServiceImpl.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCanteenApptTotalExportServiceImpl.java
@@ -885,7 +885,7 @@ public class HardCanteenApptTotalExportServiceImpl implements HardCanteenApptTot
detail.setMerchantName(merchantNameMap.get(order.getMerchantId()));
detail.setTotalPrice(order.getTotalPrice());
detail.setOrderStatus(order.getOrderStatus());
- detail.setUpdateTime(order.getUpdateTime());
+ detail.setUpdateTime(order.getVerificationTime() != null ? order.getVerificationTime() : order.getUpdateTime());
detail.setUpdateBy(order.getUpdateBy());
return detail;
}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCanteenApptTotalServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCanteenApptTotalServiceImpl.java
index 414e6cf0..86004770 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCanteenApptTotalServiceImpl.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardCanteenApptTotalServiceImpl.java
@@ -773,7 +773,7 @@ public class HardCanteenApptTotalServiceImpl implements HardCanteenApptTotalServ
detail.setMerchantName(merchantNameMap.get(order.getMerchantId()));
detail.setTotalPrice(order.getTotalPrice());
detail.setOrderStatus(order.getOrderStatus());
- detail.setUpdateTime(order.getUpdateTime());
+ detail.setUpdateTime(order.getVerificationTime() != null ? order.getVerificationTime() : order.getUpdateTime());
detail.setUpdateBy(order.getUpdateBy());
return detail;
}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderEvidenceImageServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderEvidenceImageServiceImpl.java
index dd1f3583..f54f3ba1 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderEvidenceImageServiceImpl.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderEvidenceImageServiceImpl.java
@@ -101,6 +101,11 @@ public class HardOrderEvidenceImageServiceImpl extends ServiceImpl预约订单只有在核销成功后才会把 {@code hard_id} 更新为当前设备,因此终端即使提前知道
+ * 服务端订单 ID,也不能在核销前或其他设备上写入证据。
+ *
+ * @param hardId 当前设备主键
+ * @param androidOrderCode 终端本地订单号,允许为空
+ * @param orderId 服务端订单主键,允许为空
+ * @param requirePaid 是否要求订单处于已支付、已退款或已核销状态
+ * @return 属于当前设备的订单
*/
- private HardOrderEntity requireOrder(Long hardId, String androidOrderCode, boolean requirePaid) {
+ private HardOrderEntity requireOrder(Long hardId, String androidOrderCode, Long orderId, boolean requirePaid) {
if (hardId == null) throw new RuntimeException("DEVICE-ID不能为空");
- if (StrUtil.isBlank(androidOrderCode)) throw new RuntimeException("终端订单号不能为空");
+ if (StrUtil.isBlank(androidOrderCode) && orderId == null) {
+ throw new RuntimeException("终端订单号和服务端订单ID不能同时为空");
+ }
HardManagerEntity hard = hardManagerMapper.selectById(hardId);
if (hard == null) throw new RuntimeException("当前租户下设备不存在");
- HardOrderEntity order = hardOrderMapper.selectOne(new LambdaQueryWrapper()
+ LambdaQueryWrapper query = new LambdaQueryWrapper()
.eq(HardOrderEntity::getHardId, hardId)
- .eq(HardOrderEntity::getAndroidOrderCode, androidOrderCode));
+ .eq(orderId != null, HardOrderEntity::getId, orderId)
+ .eq(StrUtil.isNotBlank(androidOrderCode), HardOrderEntity::getAndroidOrderCode, androidOrderCode);
+ HardOrderEntity order = hardOrderMapper.selectOne(query);
if (order == null) throw new RuntimeException("订单不存在或不属于当前设备");
if (requirePaid && !HardOrderStatusEnum.getPayPrice().contains(order.getOrderStatus())) {
throw new RuntimeException("订单尚未确认支付成功,暂不能保存识别证据");
@@ -332,13 +355,29 @@ public class HardOrderEvidenceImageServiceImpl extends ServiceImpl
+
+ and a.verification_time between #{dto.verificationTimeList[0]} and #{dto.verificationTimeList[1]}
+
and a.appt_use_date between #{dto.apptUseDateTimeList[0]} and #{dto.apptUseDateTimeList[1]}
@@ -315,6 +319,9 @@
+
+ order by a.verification_time desc, a.id desc
+
order by a.update_time desc, a.id desc
@@ -338,6 +345,9 @@
+
+ order by a.verification_time desc, a.id desc
+
order by a.update_time desc, a.id desc