From 77649c76ec1c3afcb93e9963243d637665b3a307 Mon Sep 17 00:00:00 2001
From: liujie <1793218484@qq.com>
Date: 星期五, 21 八月 2026 16:56:28 +0800
Subject: [PATCH] update
---
backend/src/main/java/com/tourguide/modules/order/domain/PaymentMethod.java | 15 +
backend/src/main/resources/application-dev.yml | 3
data/guide-applications/20563e26d1bc43c4a1aac5abc87ae480.jpg | 0
mingdaojingjiang_uniapp/mingdaojingjiang_uniapp/pages/order-pending-guide/index.vue | 86 ++++++
mingdaojingjiang_guide_app/services/order.js | 7
mingdaojingjiang_guide_app/pages/workbench/index.vue | 33 ++
backend/src/test/java/com/tourguide/modules/order/service/OrderRefundServiceTest.java | 17 +
backend/src/main/java/com/tourguide/api/guide/GuideTourOrderController.java | 6
mingdaojingjiang_uniapp/mingdaojingjiang_uniapp/pages/home/index.vue | 38 +-
backend/src/main/java/com/tourguide/modules/order/service/TourOrderPaymentService.java | 5
backend/src/main/java/com/tourguide/modules/order/service/GuideTourOrderService.java | 60 ++++
backend/src/main/java/com/tourguide/modules/order/service/MiniTourOrderCommandService.java | 23 +
backend/src/test/java/com/tourguide/modules/order/service/GuideTourOrderServiceTest.java | 91 +++++++
backend/src/test/java/com/tourguide/modules/order/service/MiniTourOrderCommandServiceTest.java | 134 ++++++---
mingdaojingjiang_uniapp/mingdaojingjiang_uniapp/pages/order-serving/index.vue | 232 ++++++++++++++---
data/guide-applications/1e3862aa6fda49d580017e360126062a.jpg | 0
backend/src/main/java/com/tourguide/modules/order/service/OrderRefundService.java | 8
data/guide-applications/b57f19850ed74770bf45906b9d3d2b19.jpg | 0
data/guide-applications/7eff1dce4fc54626bde8a8b05021b5fd.jpg | 0
backend/src/main/java/com/tourguide/modules/user/mapper/GuideReceivingSessionMapper.java | 8
20 files changed, 630 insertions(+), 136 deletions(-)
diff --git a/backend/src/main/java/com/tourguide/api/guide/GuideTourOrderController.java b/backend/src/main/java/com/tourguide/api/guide/GuideTourOrderController.java
index f2bb9ef..5c11ce8 100644
--- a/backend/src/main/java/com/tourguide/api/guide/GuideTourOrderController.java
+++ b/backend/src/main/java/com/tourguide/api/guide/GuideTourOrderController.java
@@ -31,4 +31,10 @@
@PathVariable Long id) {
return ApiResponse.success(orderService.detail(principal.getUserId(), id));
}
+
+ @PostMapping("/{id}/start")
+ public ApiResponse<GuideOrderResponse> start(@AuthenticationPrincipal AppUserPrincipal principal,
+ @PathVariable Long id) {
+ return ApiResponse.success(orderService.start(principal.getUserId(), id));
+ }
}
diff --git a/backend/src/main/java/com/tourguide/modules/order/domain/PaymentMethod.java b/backend/src/main/java/com/tourguide/modules/order/domain/PaymentMethod.java
new file mode 100644
index 0000000..ca67f2e
--- /dev/null
+++ b/backend/src/main/java/com/tourguide/modules/order/domain/PaymentMethod.java
@@ -0,0 +1,15 @@
+package com.tourguide.modules.order.domain;
+
+/** 订单支付方式。 */
+public enum PaymentMethod {
+ WECHAT,
+ TEMPORARY_BYPASS;
+
+ public String getCode() {
+ return name();
+ }
+
+ public boolean matches(String code) {
+ return name().equals(code);
+ }
+}
diff --git a/backend/src/main/java/com/tourguide/modules/order/service/GuideTourOrderService.java b/backend/src/main/java/com/tourguide/modules/order/service/GuideTourOrderService.java
index 214bc65..31135b0 100644
--- a/backend/src/main/java/com/tourguide/modules/order/service/GuideTourOrderService.java
+++ b/backend/src/main/java/com/tourguide/modules/order/service/GuideTourOrderService.java
@@ -8,9 +8,14 @@
import com.tourguide.modules.order.dto.GuideOrderResponse;
import com.tourguide.modules.order.entity.TourOrder;
import com.tourguide.modules.order.mapper.TourOrderMapper;
+import com.tourguide.modules.user.entity.GuideReceivingSession;
+import com.tourguide.modules.user.mapper.GuideRealtimeStatusMapper;
+import com.tourguide.modules.user.mapper.GuideReceivingSessionMapper;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
+import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.stream.Collectors;
@@ -23,9 +28,14 @@
private static final String TAB_COMPLETED = "completed";
private final TourOrderMapper orderMapper;
+ private final GuideReceivingSessionMapper sessionMapper;
+ private final GuideRealtimeStatusMapper realtimeStatusMapper;
- public GuideTourOrderService(TourOrderMapper orderMapper) {
+ public GuideTourOrderService(TourOrderMapper orderMapper, GuideReceivingSessionMapper sessionMapper,
+ GuideRealtimeStatusMapper realtimeStatusMapper) {
this.orderMapper = orderMapper;
+ this.sessionMapper = sessionMapper;
+ this.realtimeStatusMapper = realtimeStatusMapper;
}
public PageResponse<GuideOrderResponse> page(Long guideId, String tab, long page, long size) {
@@ -41,16 +51,56 @@
}
public GuideOrderResponse detail(Long guideId, Long orderId) {
- TourOrder order = orderMapper.selectOne(new LambdaQueryWrapper<TourOrder>()
- .eq(TourOrder::getId, orderId)
- .eq(TourOrder::getGuideId, guideId)
- .last("LIMIT 1"));
+ TourOrder order = findOwnedOrder(guideId, orderId);
if (order == null) {
throw new BusinessException(404, "订单不存在");
}
return GuideOrderResponse.from(order);
}
+ @Transactional
+ public GuideOrderResponse start(Long guideId, Long orderId) {
+ TourOrder order = findOwnedOrder(guideId, orderId);
+ if (order == null) {
+ throw new BusinessException(404, "订单不存在");
+ }
+ if (OrderStatus.IN_SERVICE.matches(order.getStatus())) {
+ return GuideOrderResponse.from(order);
+ }
+ if (!OrderStatus.PENDING_MEETING.matches(order.getStatus()) || order.getReceivingSessionId() == null) {
+ throw new BusinessException(400, "当前订单不能开始讲解");
+ }
+
+ GuideReceivingSession session = sessionMapper.selectForUpdate(order.getReceivingSessionId(), guideId);
+ if (session == null) {
+ throw new BusinessException(400, "接单会话不存在或已结束");
+ }
+ if ("IN_SERVICE".equals(session.getStatus())) {
+ order.setStatus(OrderStatus.IN_SERVICE.getCode());
+ return GuideOrderResponse.from(order);
+ }
+ if (!"RECEIVING".equals(session.getStatus()) && !"ASSIGNED".equals(session.getStatus())) {
+ throw new BusinessException(400, "接单会话已结束,不能开始讲解");
+ }
+
+ LocalDateTime startedAt = LocalDateTime.now();
+ if (sessionMapper.startSession(session.getId(), guideId) != 1
+ || orderMapper.startSessionOrders(session.getId(), startedAt) == 0) {
+ throw new BusinessException(409, "讲解状态已变化,请刷新后重试");
+ }
+ realtimeStatusMapper.disableReceiving(guideId);
+ order.setStatus(OrderStatus.IN_SERVICE.getCode());
+ order.setServiceStartedAt(startedAt);
+ return GuideOrderResponse.from(order);
+ }
+
+ private TourOrder findOwnedOrder(Long guideId, Long orderId) {
+ return orderMapper.selectOne(new LambdaQueryWrapper<TourOrder>()
+ .eq(TourOrder::getId, orderId)
+ .eq(TourOrder::getGuideId, guideId)
+ .last("LIMIT 1"));
+ }
+
private String normalizeTab(String tab) {
return StringUtils.hasText(tab) ? tab.trim() : TAB_ALL;
}
diff --git a/backend/src/main/java/com/tourguide/modules/order/service/MiniTourOrderCommandService.java b/backend/src/main/java/com/tourguide/modules/order/service/MiniTourOrderCommandService.java
index 8114c54..320daf1 100644
--- a/backend/src/main/java/com/tourguide/modules/order/service/MiniTourOrderCommandService.java
+++ b/backend/src/main/java/com/tourguide/modules/order/service/MiniTourOrderCommandService.java
@@ -10,6 +10,7 @@
import com.tourguide.modules.operation.mapper.ScenicPricingMapper;
import com.tourguide.modules.operation.service.PlatformSettingsService;
import com.tourguide.modules.order.domain.OrderStatus;
+import com.tourguide.modules.order.domain.PaymentMethod;
import com.tourguide.modules.order.domain.RefundStatus;
import com.tourguide.modules.order.dto.MiniOrderCreateRequest;
import com.tourguide.modules.order.dto.TourOrderResponse;
@@ -20,6 +21,7 @@
import com.tourguide.modules.user.entity.GuideUser;
import com.tourguide.modules.user.mapper.GuideUserMapper;
import com.tourguide.modules.user.service.MiniUserService;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -40,6 +42,8 @@
private final PlatformSettingsService platformSettingsService;
private final GuideNotificationService notificationService;
private final OrderRefundService orderRefundService;
+ private final GuideOrderMatchingService matchingService;
+ private final boolean skipPayment;
public MiniTourOrderCommandService(TourOrderMapper orderMapper,
MiniUserService miniUserService,
@@ -49,7 +53,9 @@
GuideUserMapper guideUserMapper,
PlatformSettingsService platformSettingsService,
GuideNotificationService notificationService,
- OrderRefundService orderRefundService) {
+ OrderRefundService orderRefundService,
+ GuideOrderMatchingService matchingService,
+ @Value("${tourguide.order.skip-payment:false}") boolean skipPayment) {
this.orderMapper = orderMapper;
this.miniUserService = miniUserService;
this.scenicSpotMapper = scenicSpotMapper;
@@ -59,6 +65,8 @@
this.platformSettingsService = platformSettingsService;
this.notificationService = notificationService;
this.orderRefundService = orderRefundService;
+ this.matchingService = matchingService;
+ this.skipPayment = skipPayment;
}
@Transactional
@@ -103,9 +111,20 @@
order.setMeetingPointCount(1);
Integer settlementDays = platformSettingsService.getSettings().getSettlementDays();
order.setSettlementDays(settlementDays == null ? 1 : Math.max(0, settlementDays));
- order.setStatus(OrderStatus.PENDING_PAYMENT.getCode());
+ if (skipPayment) {
+ order.setPaymentMethod(PaymentMethod.TEMPORARY_BYPASS.getCode());
+ order.setPaymentOrderNo("TEMP" + order.getOrderNo());
+ order.setPaymentAmount(subtotalAmount);
+ order.setPaymentTime(now);
+ order.setStatus(OrderStatus.PENDING_ACCEPT.getCode());
+ } else {
+ order.setStatus(OrderStatus.PENDING_PAYMENT.getCode());
+ }
order.setRefundStatus(RefundStatus.NONE.getCode());
orderMapper.insert(order);
+ if (skipPayment) {
+ matchingService.match(order.getId());
+ }
return TourOrderResponse.from(order);
}
diff --git a/backend/src/main/java/com/tourguide/modules/order/service/OrderRefundService.java b/backend/src/main/java/com/tourguide/modules/order/service/OrderRefundService.java
index b39088a..21d2b11 100644
--- a/backend/src/main/java/com/tourguide/modules/order/service/OrderRefundService.java
+++ b/backend/src/main/java/com/tourguide/modules/order/service/OrderRefundService.java
@@ -1,6 +1,7 @@
package com.tourguide.modules.order.service;
import com.tourguide.common.huifu.RequestSeqUtil;
+import com.tourguide.modules.order.domain.PaymentMethod;
import com.tourguide.modules.order.domain.RefundStatus;
import com.tourguide.modules.order.entity.TourOrder;
import com.tourguide.modules.order.mapper.TourOrderMapper;
@@ -11,6 +12,7 @@
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
+import java.time.LocalDateTime;
@Service
public class OrderRefundService {
@@ -40,6 +42,12 @@
order.setRefundStatus(RefundStatus.REFUNDING.getCode());
order.setRefundAmount(refundAmount);
order.setRefundMethod(order.getPaymentMethod());
+ if (PaymentMethod.TEMPORARY_BYPASS.matches(order.getPaymentMethod())) {
+ order.setRefundStatus(RefundStatus.REFUNDED.getCode());
+ order.setRefundTime(LocalDateTime.now());
+ orderMapper.updateById(order);
+ return;
+ }
orderMapper.updateById(order);
try {
paymentRefundGateway.refund(order, refundAmount);
diff --git a/backend/src/main/java/com/tourguide/modules/order/service/TourOrderPaymentService.java b/backend/src/main/java/com/tourguide/modules/order/service/TourOrderPaymentService.java
index 5ac0cfa..d21ebc8 100644
--- a/backend/src/main/java/com/tourguide/modules/order/service/TourOrderPaymentService.java
+++ b/backend/src/main/java/com/tourguide/modules/order/service/TourOrderPaymentService.java
@@ -3,6 +3,7 @@
import com.tourguide.common.exception.BusinessException;
import com.tourguide.common.huifu.RequestSeqUtil;
import com.tourguide.modules.order.domain.OrderStatus;
+import com.tourguide.modules.order.domain.PaymentMethod;
import com.tourguide.modules.order.entity.TourOrder;
import com.tourguide.modules.order.entity.TourOrderPaymentAttempt;
import com.tourguide.modules.order.mapper.TourOrderMapper;
@@ -56,7 +57,7 @@
attempt.setStatus(ATTEMPT_CREATED);
paymentAttemptMapper.insert(attempt);
- order.setPaymentMethod("WECHAT");
+ order.setPaymentMethod(PaymentMethod.WECHAT.getCode());
order.setPaymentOrderNo(reqSeqId);
orderMapper.updateById(order);
@@ -91,7 +92,7 @@
if (!OrderStatus.PENDING_PAYMENT.matches(order.getStatus())) {
throw new BusinessException(409, "订单状态不允许确认支付");
}
- order.setPaymentMethod("WECHAT");
+ order.setPaymentMethod(PaymentMethod.WECHAT.getCode());
order.setPaymentOrderNo(notice.getReqSeqId());
order.setPaymentAmount(notice.getAmount());
order.setPaymentTime(paidAt);
diff --git a/backend/src/main/java/com/tourguide/modules/user/mapper/GuideReceivingSessionMapper.java b/backend/src/main/java/com/tourguide/modules/user/mapper/GuideReceivingSessionMapper.java
index 1228b8c..dfb0389 100644
--- a/backend/src/main/java/com/tourguide/modules/user/mapper/GuideReceivingSessionMapper.java
+++ b/backend/src/main/java/com/tourguide/modules/user/mapper/GuideReceivingSessionMapper.java
@@ -9,6 +9,10 @@
import java.util.List;
public interface GuideReceivingSessionMapper extends BaseMapper<GuideReceivingSession> {
+ @Select("SELECT * FROM guide_receiving_session WHERE id = #{id} AND guide_id = #{guideId} " +
+ "AND deleted = 0 LIMIT 1 FOR UPDATE")
+ GuideReceivingSession selectForUpdate(@Param("id") Long id, @Param("guideId") Long guideId);
+
@Select("SELECT s.* FROM guide_receiving_session s " +
"INNER JOIN guide_realtime_status r ON r.guide_id = s.guide_id AND r.deleted = 0 " +
"INNER JOIN guide_user g ON g.id = s.guide_id AND g.deleted = 0 AND g.status = 1 " +
@@ -40,6 +44,10 @@
"WHERE id = #{id} AND deleted = 0 AND status = 'RECEIVING' AND service_start_at <= NOW()")
int startDueSession(Long id);
+ @Update("UPDATE guide_receiving_session SET status = 'IN_SERVICE', updated_at = NOW() " +
+ "WHERE id = #{id} AND guide_id = #{guideId} AND deleted = 0 AND status IN ('RECEIVING', 'ASSIGNED')")
+ int startSession(@Param("id") Long id, @Param("guideId") Long guideId);
+
@Update("UPDATE guide_receiving_session SET reminder_pushed_at = #{now}, updated_at = NOW() " +
"WHERE id = #{id} AND deleted = 0 AND status = 'RECEIVING' AND reminder_pushed_at IS NULL")
int claimServiceReminder(@Param("id") Long id, @Param("now") LocalDateTime now);
diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml
index ccfd80a..33ed400 100644
--- a/backend/src/main/resources/application-dev.yml
+++ b/backend/src/main/resources/application-dev.yml
@@ -14,6 +14,9 @@
tourguide:
guide-login:
fixed-verification-code: ${GUIDE_LOGIN_FIXED_CODE:}
+ order:
+ # 临时跳过微信支付;恢复真实支付时将默认值改为 false。
+ skip-payment: ${TOURGUIDE_ORDER_SKIP_PAYMENT:true}
logging:
level:
diff --git a/backend/src/test/java/com/tourguide/modules/order/service/GuideTourOrderServiceTest.java b/backend/src/test/java/com/tourguide/modules/order/service/GuideTourOrderServiceTest.java
new file mode 100644
index 0000000..ba2f178
--- /dev/null
+++ b/backend/src/test/java/com/tourguide/modules/order/service/GuideTourOrderServiceTest.java
@@ -0,0 +1,91 @@
+package com.tourguide.modules.order.service;
+
+import com.tourguide.common.exception.BusinessException;
+import com.tourguide.modules.order.domain.OrderStatus;
+import com.tourguide.modules.order.dto.GuideOrderResponse;
+import com.tourguide.modules.order.entity.TourOrder;
+import com.tourguide.modules.order.mapper.TourOrderMapper;
+import com.tourguide.modules.user.entity.GuideReceivingSession;
+import com.tourguide.modules.user.mapper.GuideRealtimeStatusMapper;
+import com.tourguide.modules.user.mapper.GuideReceivingSessionMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class GuideTourOrderServiceTest {
+ private TourOrderMapper orderMapper;
+ private GuideReceivingSessionMapper sessionMapper;
+ private GuideRealtimeStatusMapper realtimeStatusMapper;
+ private GuideTourOrderService service;
+
+ @BeforeEach
+ void setUp() {
+ orderMapper = mock(TourOrderMapper.class);
+ sessionMapper = mock(GuideReceivingSessionMapper.class);
+ realtimeStatusMapper = mock(GuideRealtimeStatusMapper.class);
+ service = new GuideTourOrderService(orderMapper, sessionMapper, realtimeStatusMapper);
+ }
+
+ @Test
+ void shouldStartAllOrdersInReceivingSession() {
+ TourOrder order = order(OrderStatus.PENDING_MEETING);
+ GuideReceivingSession session = session("ASSIGNED");
+ when(orderMapper.selectOne(any())).thenReturn(order);
+ when(sessionMapper.selectForUpdate(20L, 8L)).thenReturn(session);
+ when(sessionMapper.startSession(20L, 8L)).thenReturn(1);
+ when(orderMapper.startSessionOrders(eq(20L), any())).thenReturn(2);
+
+ GuideOrderResponse response = service.start(8L, 10L);
+
+ assertThat(response.getStatus()).isEqualTo("serving");
+ assertThat(response.getLectureStartTime()).isNotNull();
+ verify(realtimeStatusMapper).disableReceiving(8L);
+ }
+
+ @Test
+ void shouldTreatRepeatedStartAsSuccess() {
+ TourOrder order = order(OrderStatus.IN_SERVICE);
+ when(orderMapper.selectOne(any())).thenReturn(order);
+
+ GuideOrderResponse response = service.start(8L, 10L);
+
+ assertThat(response.getStatus()).isEqualTo("serving");
+ verify(sessionMapper, never()).selectForUpdate(any(), any());
+ }
+
+ @Test
+ void shouldRejectOrderOutsidePendingMeetingState() {
+ TourOrder order = order(OrderStatus.COMPLETED);
+ when(orderMapper.selectOne(any())).thenReturn(order);
+
+ assertThatThrownBy(() -> service.start(8L, 10L))
+ .isInstanceOf(BusinessException.class)
+ .hasMessage("当前订单不能开始讲解");
+ }
+
+ private TourOrder order(OrderStatus status) {
+ TourOrder order = new TourOrder();
+ order.setId(10L);
+ order.setGuideId(8L);
+ order.setReceivingSessionId(20L);
+ order.setStatus(status.getCode());
+ order.setPeopleCount(2);
+ return order;
+ }
+
+ private GuideReceivingSession session(String status) {
+ GuideReceivingSession session = new GuideReceivingSession();
+ session.setId(20L);
+ session.setGuideId(8L);
+ session.setStatus(status);
+ return session;
+ }
+}
diff --git a/backend/src/test/java/com/tourguide/modules/order/service/MiniTourOrderCommandServiceTest.java b/backend/src/test/java/com/tourguide/modules/order/service/MiniTourOrderCommandServiceTest.java
index 5cf75d4..de71e45 100644
--- a/backend/src/test/java/com/tourguide/modules/order/service/MiniTourOrderCommandServiceTest.java
+++ b/backend/src/test/java/com/tourguide/modules/order/service/MiniTourOrderCommandServiceTest.java
@@ -9,6 +9,7 @@
import com.tourguide.modules.operation.dto.PlatformSettingsResponse;
import com.tourguide.modules.operation.service.PlatformSettingsService;
import com.tourguide.modules.order.domain.OrderStatus;
+import com.tourguide.modules.order.domain.PaymentMethod;
import com.tourguide.modules.order.dto.MiniOrderCreateRequest;
import com.tourguide.modules.order.dto.TourOrderResponse;
import com.tourguide.modules.order.entity.TourOrder;
@@ -43,6 +44,7 @@
private PlatformSettingsService platformSettingsService;
private GuideNotificationService notificationService;
private OrderRefundService orderRefundService;
+ private GuideOrderMatchingService matchingService;
private MiniTourOrderCommandService commandService;
@BeforeEach
@@ -56,13 +58,92 @@
platformSettingsService = mock(PlatformSettingsService.class);
notificationService = mock(GuideNotificationService.class);
orderRefundService = mock(OrderRefundService.class);
+ matchingService = mock(GuideOrderMatchingService.class);
commandService = new MiniTourOrderCommandService(
orderMapper, miniUserService, scenicSpotMapper, meetingPointMapper, pricingMapper,
- guideUserMapper, platformSettingsService, notificationService, orderRefundService);
+ guideUserMapper, platformSettingsService, notificationService, orderRefundService,
+ matchingService, false);
}
@Test
void createShouldKeepOrderPendingPaymentUntilPaymentSucceeds() {
+ MiniOrderCreateRequest request = prepareCreateOrder();
+
+ TourOrderResponse response = commandService.create(7L, request);
+
+ ArgumentCaptor<TourOrder> captor = ArgumentCaptor.forClass(TourOrder.class);
+ verify(orderMapper).insert(captor.capture());
+ TourOrder order = captor.getValue();
+ assertThat(order.getStatus()).isEqualTo(OrderStatus.PENDING_PAYMENT.getCode());
+ assertThat(order.getPaymentMethod()).isNull();
+ assertThat(order.getPaymentOrderNo()).isNull();
+ assertThat(order.getPaymentAmount()).isNull();
+ assertThat(order.getPaymentTime()).isNull();
+ assertThat(order.getSettlementDays()).isEqualTo(7);
+ assertThat(response.getStatus()).isEqualTo(OrderStatus.PENDING_PAYMENT.getCode());
+ verify(matchingService, never()).match(any());
+ }
+
+ @Test
+ void createShouldRejectVisitorWithUnfinishedOrder() {
+ MiniUser visitor = new MiniUser();
+ visitor.setId(7L);
+ when(miniUserService.findEnabledById(7L)).thenReturn(visitor);
+ when(orderMapper.selectCount(any())).thenReturn(1L);
+
+ assertThatThrownBy(() -> commandService.create(7L, new MiniOrderCreateRequest()))
+ .isInstanceOf(com.tourguide.common.exception.BusinessException.class)
+ .hasMessage("您有一个未完成的订单,请先完成或取消后再下单");
+
+ verify(orderMapper, never()).insert(any(TourOrder.class));
+ }
+
+ @Test
+ void createShouldMarkOrderPaidAndStartMatchingWhenPaymentIsSkipped() {
+ MiniOrderCreateRequest request = prepareCreateOrder();
+ commandService = new MiniTourOrderCommandService(
+ orderMapper, miniUserService, scenicSpotMapper, meetingPointMapper, pricingMapper,
+ guideUserMapper, platformSettingsService, notificationService, orderRefundService,
+ matchingService, true);
+
+ TourOrderResponse response = commandService.create(7L, request);
+
+ ArgumentCaptor<TourOrder> captor = ArgumentCaptor.forClass(TourOrder.class);
+ verify(orderMapper).insert(captor.capture());
+ TourOrder order = captor.getValue();
+ assertThat(order.getStatus()).isEqualTo(OrderStatus.PENDING_ACCEPT.getCode());
+ assertThat(order.getPaymentMethod()).isEqualTo(PaymentMethod.TEMPORARY_BYPASS.getCode());
+ assertThat(order.getPaymentOrderNo()).startsWith("TEMPMJ");
+ assertThat(order.getPaymentAmount()).isEqualByComparingTo("144.00");
+ assertThat(order.getPaymentTime()).isNotNull();
+ assertThat(response.getStatus()).isEqualTo(OrderStatus.PENDING_ACCEPT.getCode());
+ verify(matchingService).match(99L);
+ }
+
+ @Test
+ void cancelPendingMeetingOrderShouldNotifyAssignedGuide() {
+ MiniUser visitor = new MiniUser();
+ visitor.setId(7L);
+ when(miniUserService.findEnabledById(7L)).thenReturn(visitor);
+
+ TourOrder order = new TourOrder();
+ order.setId(99L);
+ order.setOrderNo("MJ202607300001");
+ order.setVisitorId(7L);
+ order.setGuideId(8L);
+ order.setStatus(OrderStatus.PENDING_MEETING.getCode());
+ order.setPaymentTime(LocalDateTime.now());
+ order.setPaymentAmount(new BigDecimal("36.00"));
+ when(orderMapper.selectForUpdate(99L)).thenReturn(order);
+
+ commandService.cancel(7L, 99L);
+
+ assertThat(order.getStatus()).isEqualTo(OrderStatus.CANCELLED.getCode());
+ verify(orderRefundService).requestRefund(order);
+ verify(notificationService).notifyVisitorCancelledOrder(order);
+ }
+
+ private MiniOrderCreateRequest prepareCreateOrder() {
MiniUser visitor = new MiniUser();
visitor.setId(7L);
visitor.setNickname("游客");
@@ -104,55 +185,6 @@
request.setMeetingPointId(12L);
request.setServicePackageId(13L);
request.setPeopleCount(4);
-
- TourOrderResponse response = commandService.create(7L, request);
-
- ArgumentCaptor<TourOrder> captor = ArgumentCaptor.forClass(TourOrder.class);
- verify(orderMapper).insert(captor.capture());
- TourOrder order = captor.getValue();
- assertThat(order.getStatus()).isEqualTo(OrderStatus.PENDING_PAYMENT.getCode());
- assertThat(order.getPaymentMethod()).isNull();
- assertThat(order.getPaymentOrderNo()).isNull();
- assertThat(order.getPaymentAmount()).isNull();
- assertThat(order.getPaymentTime()).isNull();
- assertThat(order.getSettlementDays()).isEqualTo(7);
- assertThat(response.getStatus()).isEqualTo(OrderStatus.PENDING_PAYMENT.getCode());
- }
-
- @Test
- void createShouldRejectVisitorWithUnfinishedOrder() {
- MiniUser visitor = new MiniUser();
- visitor.setId(7L);
- when(miniUserService.findEnabledById(7L)).thenReturn(visitor);
- when(orderMapper.selectCount(any())).thenReturn(1L);
-
- assertThatThrownBy(() -> commandService.create(7L, new MiniOrderCreateRequest()))
- .isInstanceOf(com.tourguide.common.exception.BusinessException.class)
- .hasMessage("您有一个未完成的订单,请先完成或取消后再下单");
-
- verify(orderMapper, never()).insert(any(TourOrder.class));
- }
-
- @Test
- void cancelPendingMeetingOrderShouldNotifyAssignedGuide() {
- MiniUser visitor = new MiniUser();
- visitor.setId(7L);
- when(miniUserService.findEnabledById(7L)).thenReturn(visitor);
-
- TourOrder order = new TourOrder();
- order.setId(99L);
- order.setOrderNo("MJ202607300001");
- order.setVisitorId(7L);
- order.setGuideId(8L);
- order.setStatus(OrderStatus.PENDING_MEETING.getCode());
- order.setPaymentTime(LocalDateTime.now());
- order.setPaymentAmount(new BigDecimal("36.00"));
- when(orderMapper.selectForUpdate(99L)).thenReturn(order);
-
- commandService.cancel(7L, 99L);
-
- assertThat(order.getStatus()).isEqualTo(OrderStatus.CANCELLED.getCode());
- verify(orderRefundService).requestRefund(order);
- verify(notificationService).notifyVisitorCancelledOrder(order);
+ return request;
}
}
diff --git a/backend/src/test/java/com/tourguide/modules/order/service/OrderRefundServiceTest.java b/backend/src/test/java/com/tourguide/modules/order/service/OrderRefundServiceTest.java
index 31fae2c..c5d3ddf 100644
--- a/backend/src/test/java/com/tourguide/modules/order/service/OrderRefundServiceTest.java
+++ b/backend/src/test/java/com/tourguide/modules/order/service/OrderRefundServiceTest.java
@@ -1,6 +1,7 @@
package com.tourguide.modules.order.service;
import com.tourguide.modules.order.domain.RefundStatus;
+import com.tourguide.modules.order.domain.PaymentMethod;
import com.tourguide.modules.order.entity.TourOrder;
import com.tourguide.modules.order.mapper.TourOrderMapper;
import com.tourguide.modules.order.payment.PaymentRefundGateway;
@@ -71,6 +72,22 @@
org.mockito.ArgumentMatchers.any());
}
+ @Test
+ void temporaryBypassRefundShouldCompleteWithoutCallingGateway() {
+ TourOrder order = paidOrder();
+ order.setPaymentMethod(PaymentMethod.TEMPORARY_BYPASS.getCode());
+ order.setPaymentOrderNo("TEMPMJ202608030001");
+
+ refundService.requestRefund(order);
+
+ assertThat(order.getRefundStatus()).isEqualTo(RefundStatus.REFUNDED.getCode());
+ assertThat(order.getRefundTime()).isNotNull();
+ assertThat(order.getRefundMethod()).isEqualTo(PaymentMethod.TEMPORARY_BYPASS.getCode());
+ verify(refundGateway, never()).refund(org.mockito.ArgumentMatchers.any(),
+ org.mockito.ArgumentMatchers.any());
+ verify(orderMapper).updateById(order);
+ }
+
private TourOrder paidOrder() {
TourOrder order = new TourOrder();
order.setId(99L);
diff --git a/data/guide-applications/1e3862aa6fda49d580017e360126062a.jpg b/data/guide-applications/1e3862aa6fda49d580017e360126062a.jpg
new file mode 100644
index 0000000..821fb29
--- /dev/null
+++ b/data/guide-applications/1e3862aa6fda49d580017e360126062a.jpg
Binary files differ
diff --git a/data/guide-applications/20563e26d1bc43c4a1aac5abc87ae480.jpg b/data/guide-applications/20563e26d1bc43c4a1aac5abc87ae480.jpg
new file mode 100644
index 0000000..6b50d31
--- /dev/null
+++ b/data/guide-applications/20563e26d1bc43c4a1aac5abc87ae480.jpg
Binary files differ
diff --git a/data/guide-applications/7eff1dce4fc54626bde8a8b05021b5fd.jpg b/data/guide-applications/7eff1dce4fc54626bde8a8b05021b5fd.jpg
new file mode 100644
index 0000000..71c4529
--- /dev/null
+++ b/data/guide-applications/7eff1dce4fc54626bde8a8b05021b5fd.jpg
Binary files differ
diff --git a/data/guide-applications/b57f19850ed74770bf45906b9d3d2b19.jpg b/data/guide-applications/b57f19850ed74770bf45906b9d3d2b19.jpg
new file mode 100644
index 0000000..6b50d31
--- /dev/null
+++ b/data/guide-applications/b57f19850ed74770bf45906b9d3d2b19.jpg
Binary files differ
diff --git a/mingdaojingjiang_guide_app/pages/workbench/index.vue b/mingdaojingjiang_guide_app/pages/workbench/index.vue
index 07930d6..e445198 100644
--- a/mingdaojingjiang_guide_app/pages/workbench/index.vue
+++ b/mingdaojingjiang_guide_app/pages/workbench/index.vue
@@ -65,7 +65,13 @@
</view>
</view>
<button v-if="isServing" class="lecture-button" @tap="finishLecture">结束讲解</button>
- <button v-else class="lecture-button" @tap="startLecture">开始讲解</button>
+ <button
+ v-else
+ class="lecture-button"
+ :loading="startingLecture"
+ :disabled="startingLecture"
+ @tap="startLecture"
+ >开始讲解</button>
</template>
<template v-else>
<view class="empty-order">暂无订单请耐心等待</view>
@@ -136,7 +142,7 @@
updateGuideOnlineStatus
} from '../../services/guide'
import { openMeetingPointPage } from '../../services/map'
-import { getGuideOrders } from '../../services/order'
+import { getGuideOrders, startGuideLecture } from '../../services/order'
import { playNotificationAlert } from '../../services/message'
import { getWorkbenchData } from '../../services/workbench'
import { formatDuration, maskPhone } from '../../utils/format'
@@ -196,6 +202,7 @@
countdownTimer: null,
refreshTimer: null,
refreshing: false,
+ startingLecture: false,
dialog: {
visible: false,
title: '提示',
@@ -480,9 +487,27 @@
})
},
startLecture() {
- updateGuideOnlineStatus('serving', this.guide.receivingConfig || {}).then(() => {
- this.loadData()
+ const orderId = this.activeReceivingOrder.id
+ if (this.startingLecture) {
+ return
+ }
+ if (!orderId) {
+ uni.showToast({ title: '当前没有可开始的订单', icon: 'none' })
+ return
+ }
+ this.startingLecture = true
+ startGuideLecture(orderId).then((startedOrder) => {
+ this.updateGuide({ ...this.guide, onlineStatus: 'serving' })
+ this.updateOrders(this.orders.map((order) => (
+ order.id === startedOrder.id ? { ...order, ...startedOrder } : order
+ )))
+ this.setupCountdown()
uni.showToast({ title: '已开始讲解', icon: 'none' })
+ return this.loadData({ silent: true })
+ }).catch((error) => {
+ uni.showToast({ title: error.message || '开始讲解失败', icon: 'none' })
+ }).finally(() => {
+ this.startingLecture = false
})
},
finishLecture() {
diff --git a/mingdaojingjiang_guide_app/services/order.js b/mingdaojingjiang_guide_app/services/order.js
index c292d77..a9777fc 100644
--- a/mingdaojingjiang_guide_app/services/order.js
+++ b/mingdaojingjiang_guide_app/services/order.js
@@ -55,6 +55,13 @@
return request({ url: `/guide/orders/${orderId}` }).then(normalizeGuideOrder)
}
+export function startGuideLecture(orderId) {
+ return request({
+ url: `/guide/orders/${orderId}/start`,
+ method: 'POST'
+ }).then(normalizeGuideOrder)
+}
+
function normalizeOrderTab(filter) {
if (filter === 'active' || filter === 'pending') {
return 'accepted'
diff --git a/mingdaojingjiang_uniapp/mingdaojingjiang_uniapp/pages/home/index.vue b/mingdaojingjiang_uniapp/mingdaojingjiang_uniapp/pages/home/index.vue
index 89e0a76..96dd44b 100644
--- a/mingdaojingjiang_uniapp/mingdaojingjiang_uniapp/pages/home/index.vue
+++ b/mingdaojingjiang_uniapp/mingdaojingjiang_uniapp/pages/home/index.vue
@@ -323,14 +323,14 @@
const textMap = {
service: '立即呼叫讲解',
meeting: '下一步',
- payment: '确认支付'
+ payment: '确认下单'
}
return textMap[guideFlowStep.value] || textMap.service
})
-const guideActionLoadingText = computed(() => (guideFlowStep.value === 'payment' ? '支付中...' : '处理中...'))
+const guideActionLoadingText = computed(() => (guideFlowStep.value === 'payment' ? '下单中...' : '处理中...'))
-const guideActionSuccessText = computed(() => (guideFlowStep.value === 'payment' ? '支付成功' : '已完成'))
+const guideActionSuccessText = computed(() => (guideFlowStep.value === 'payment' ? '下单成功' : '已完成'))
const guideActionDisabled = computed(() => {
if (guideFlowStep.value === 'meeting') return !selectedMeetingPointId.value
@@ -902,19 +902,8 @@
const order = activeHomeOrder.value
if (!order || !order.id) return
- const routeMap = {
- pendingPay: '/pages/order-pending-pay/index',
- waiting: '/pages/order-waiting/index',
- pendingGuide: '/pages/order-pending-guide/index',
- serving: '/pages/order-serving/index',
- completed: '/pages/order-completed/index',
- pendingReview: '/pages/order-pending-review/index',
- canceled: '/pages/order-canceled/index'
- }
- const target = routeMap[order.status] || '/pages/orders/index'
-
uni.navigateTo({
- url: `${target}?orderId=${order.id}`
+ url: `${getActiveOrderRoute(order)}?orderId=${order.id}`
})
}
@@ -987,13 +976,13 @@
try {
paymentSubmitting.value = true
- const orderId = await createPendingGuideOrder()
+ const order = await createPendingGuideOrder()
paymentSubmitting.value = false
paymentSuccess.value = true
setTimeout(() => {
uni.navigateTo({
- url: `/pages/order-pending-pay/index?orderId=${orderId}`
+ url: `${getActiveOrderRoute(activeHomeOrder.value)}?orderId=${order.id}`
})
}, 220)
} catch (error) {
@@ -1035,7 +1024,20 @@
peopleCount: selectedServiceQuantity.value
})
activeHomeOrder.value = normalizeActiveHomeOrder(order)
- return order.id
+ return order
+}
+
+function getActiveOrderRoute(order) {
+ const routeMap = {
+ pendingPay: '/pages/order-pending-pay/index',
+ waiting: '/pages/order-waiting/index',
+ pendingGuide: '/pages/order-pending-guide/index',
+ serving: '/pages/order-serving/index',
+ completed: '/pages/order-completed/index',
+ pendingReview: '/pages/order-pending-review/index',
+ canceled: '/pages/order-canceled/index'
+ }
+ return routeMap[order && order.status] || '/pages/orders/index'
}
function normalizeActiveHomeOrder(order) {
diff --git a/mingdaojingjiang_uniapp/mingdaojingjiang_uniapp/pages/order-pending-guide/index.vue b/mingdaojingjiang_uniapp/mingdaojingjiang_uniapp/pages/order-pending-guide/index.vue
index f767a96..63ec5da 100644
--- a/mingdaojingjiang_uniapp/mingdaojingjiang_uniapp/pages/order-pending-guide/index.vue
+++ b/mingdaojingjiang_uniapp/mingdaojingjiang_uniapp/pages/order-pending-guide/index.vue
@@ -120,7 +120,7 @@
<script setup>
import { computed, ref } from 'vue'
-import { onLoad } from '@dcloudio/uni-app'
+import { onHide, onLoad, onShow, onUnload } from '@dcloudio/uni-app'
import AppNav from '@/components/app-nav/app-nav.vue'
import CancelOrderSheet from '@/components/cancel-order-sheet/cancel-order-sheet.vue'
import { cancelTourOrder, getTourOrder } from '@/services/order'
@@ -129,8 +129,15 @@
useLoginGuard('/pages/order-pending-guide/index')
+const ORDER_POLL_INTERVAL = 3000
+
const orderId = ref('')
const orderDetail = ref({})
+let orderPollTimer = null
+let orderRefreshing = false
+let pageActive = false
+let redirecting = false
+let loadErrorShown = false
const meetingPoint = computed(() => ({
longitude: Number(orderDetail.value.meetingPointLongitude || 0),
@@ -186,21 +193,86 @@
onLoad((options = {}) => {
orderId.value = options.orderId || options.id || ''
- loadOrderDetail()
})
-async function loadOrderDetail() {
+onShow(() => {
+ pageActive = true
+ redirecting = false
+ startOrderPolling()
+})
+
+onHide(() => {
+ pageActive = false
+ stopOrderPolling()
+})
+
+onUnload(() => {
+ pageActive = false
+ stopOrderPolling()
+})
+
+function startOrderPolling() {
+ stopOrderPolling()
if (!orderId.value) {
uni.showToast({ title: '订单参数缺失', icon: 'none' })
return
}
- try {
- orderDetail.value = await getTourOrder(orderId.value)
- } catch (error) {
- uni.showToast({ title: error.message || '订单加载失败', icon: 'none' })
+
+ refreshOrderStatus()
+ orderPollTimer = setInterval(refreshOrderStatus, ORDER_POLL_INTERVAL)
+}
+
+function stopOrderPolling() {
+ if (orderPollTimer) {
+ clearInterval(orderPollTimer)
+ orderPollTimer = null
}
}
+async function refreshOrderStatus() {
+ if (!pageActive || !orderId.value || orderRefreshing || redirecting) return
+
+ orderRefreshing = true
+ try {
+ const detail = await getTourOrder(orderId.value)
+ if (!pageActive) return
+ orderDetail.value = detail
+ loadErrorShown = false
+ redirectForOrderStatus(detail && detail.status)
+ } catch (error) {
+ if (!loadErrorShown) {
+ loadErrorShown = true
+ uni.showToast({ title: error.message || '订单状态更新失败', icon: 'none' })
+ }
+ console.warn('待讲解订单状态查询失败', error)
+ } finally {
+ orderRefreshing = false
+ }
+}
+
+function redirectForOrderStatus(status) {
+ const routeMap = {
+ PENDING_PAYMENT: '/pages/order-pending-pay/index',
+ PENDING_ACCEPT: '/pages/order-waiting/index',
+ IN_SERVICE: '/pages/order-serving/index',
+ COMPLETED: '/pages/order-pending-review/index',
+ CANCELLED: '/pages/order-canceled/index',
+ REFUNDED: '/pages/order-canceled/index'
+ }
+ const target = routeMap[status]
+ if (!target || redirecting) return
+
+ redirecting = true
+ stopOrderPolling()
+ uni.redirectTo({
+ url: `${target}?orderId=${orderId.value}`,
+ fail() {
+ redirecting = false
+ if (pageActive) startOrderPolling()
+ }
+ })
+}
+
function formatAmount(value) {
const amount = Number(value || 0)
return Number.isFinite(amount) ? amount.toFixed(2) : '0.00'
diff --git a/mingdaojingjiang_uniapp/mingdaojingjiang_uniapp/pages/order-serving/index.vue b/mingdaojingjiang_uniapp/mingdaojingjiang_uniapp/pages/order-serving/index.vue
index 52c83b4..ca7a30c 100644
--- a/mingdaojingjiang_uniapp/mingdaojingjiang_uniapp/pages/order-serving/index.vue
+++ b/mingdaojingjiang_uniapp/mingdaojingjiang_uniapp/pages/order-serving/index.vue
@@ -6,20 +6,20 @@
<view class="status-panel">
<text class="status-title">讲解中</text>
<text class="countdown-text">{{ countdownText }}</text>
- <view class="team-badge" :style="familyBadgeStyle">
- <text class="team-badge__text">家庭号 {{ familyGroup.number }}</text>
+ <view v-if="order.familyNo" class="team-badge" :style="familyBadgeStyle">
+ <text class="team-badge__text">家庭号 {{ order.familyNo }}</text>
</view>
</view>
<view class="detail-card">
<view class="guide-row">
<view class="guide-avatar">
- <image class="guide-avatar__icon" src="/static/ui-icons/user-blue.svg" mode="aspectFit" />
+ <image class="guide-avatar__icon" :src="order.guideAvatar || '/static/ui-icons/user-blue.svg'" mode="aspectFill" />
</view>
<view class="guide-main">
- <text class="guide-name">导游 · 赵四</text>
+ <text class="guide-name">导游 · {{ order.guideName }}</text>
<view class="guide-stars">
- <text v-for="item in 5" :key="item" class="guide-star">★</text>
+ <text v-for="item in 5" :key="item" class="guide-star" :class="{ 'guide-star--inactive': item > guideStarCount }">★</text>
</view>
</view>
<view class="phone-action" @tap="handleCallGuide">
@@ -42,7 +42,7 @@
<view class="price-row">
<view class="unit-block">
- <text class="unit-price">¥{{ order.unitPrice }}/人</text>
+ <text class="unit-price">¥{{ order.unitPrice }}{{ order.unit }}</text>
<text class="quantity">x{{ order.quantity }}</text>
</view>
<text class="total-price">¥{{ order.total }}</text>
@@ -68,34 +68,45 @@
</template>
<script setup>
-import { computed, onBeforeUnmount, ref } from 'vue'
+import { computed, ref } from 'vue'
+import { onHide, onLoad, onShow, onUnload } from '@dcloudio/uni-app'
import AppNav from '@/components/app-nav/app-nav.vue'
+import { getTourOrder } from '@/services/order'
+import { resolveProfileAssetUrl } from '@/services/profile'
import { useLoginGuard } from '@/utils/auth-guard'
useLoginGuard('/pages/order-serving/index')
-const order = {
- id: 'MJ20260612003',
- scenic: '青城山景区',
- service: '拼团·快讲',
- duration: '30分钟',
- meetingPoint: '青城山正门汇合',
- unitPrice: 36,
- quantity: 4,
- total: '366.00'
-}
+const ORDER_POLL_INTERVAL = 3000
-const familyGroup = {
- number: 1,
- color: '#278BFF'
-}
+const orderId = ref('')
+const orderDetail = ref({})
+const remainingSeconds = ref(0)
+let countdownTimer = null
+let orderPollTimer = null
+let orderRefreshing = false
+let pageActive = false
+let redirecting = false
+let loadErrorShown = false
-const remainingSeconds = ref(30 * 60 * 60)
-const timer = setInterval(() => {
- if (remainingSeconds.value > 0) {
- remainingSeconds.value -= 1
- }
-}, 1000)
+const order = computed(() => ({
+ id: orderDetail.value.id || orderId.value,
+ scenic: orderDetail.value.scenicName || '-',
+ service: formatServiceTitle(orderDetail.value.packageName, orderDetail.value.orderType),
+ duration: orderDetail.value.durationMinutes ? `${orderDetail.value.durationMinutes}分钟` : '-',
+ meetingPoint: orderDetail.value.meetingPointName || '-',
+ unitPrice: formatAmount(orderDetail.value.unitPrice),
+ unit: String(orderDetail.value.orderType || '').startsWith('GROUP_') ? '/人' : '/团',
+ quantity: Number(orderDetail.value.peopleCount || 1),
+ total: formatAmount(orderDetail.value.paymentAmount ?? orderDetail.value.amount),
+ familyNo: orderDetail.value.familyNo || '',
+ guideName: orderDetail.value.guideName || '未分配',
+ guideMobile: orderDetail.value.guideMobile || '',
+ guideAvatar: resolveProfileAssetUrl(orderDetail.value.guideAvatar),
+ guideRating: Number(orderDetail.value.guideRating || 0)
+}))
+
+const guideStarCount = computed(() => Math.max(0, Math.min(5, Math.round(order.value.guideRating))))
const countdownText = computed(() => {
const hours = Math.floor(remainingSeconds.value / 3600)
@@ -105,36 +116,159 @@
})
const familyBadgeStyle = computed(() => ({
- backgroundColor: familyGroup.color
+ backgroundColor: '#278BFF'
}))
-const detailRows = [
- { label: '下单时间', value: '2026-06-04 11:31:02' },
- { label: '接单时间', value: '2026-06-04 11:31:02' },
- { label: '支付金额', value: '¥366.00' },
- { label: '支付流水号', value: '46145113464411' },
- { label: '讲解开始时间', value: '2026-06-04 11:31:02' },
- { label: '讲解结束时间', value: '-' }
-]
+const detailRows = computed(() => [
+ { label: '下单时间', value: formatDateTime(orderDetail.value.placedAt) },
+ { label: '接单时间', value: formatDateTime(orderDetail.value.acceptedAt) },
+ { label: '支付金额', value: `¥${formatAmount(orderDetail.value.paymentAmount ?? orderDetail.value.amount)}` },
+ { label: '支付流水号', value: orderDetail.value.paymentOrderNo || '-' },
+ { label: '讲解开始时间', value: formatDateTime(orderDetail.value.serviceStartedAt) },
+ { label: '讲解结束时间', value: formatDateTime(orderDetail.value.completedAt) }
+])
-onBeforeUnmount(() => {
- clearInterval(timer)
+onLoad((options = {}) => {
+ orderId.value = options.orderId || options.id || ''
})
+
+onShow(() => {
+ pageActive = true
+ redirecting = false
+ startOrderPolling()
+ startCountdown()
+})
+
+onHide(() => {
+ pageActive = false
+ clearTimers()
+})
+
+onUnload(() => {
+ pageActive = false
+ clearTimers()
+})
+
+function startOrderPolling() {
+ if (!orderId.value) {
+ uni.showToast({ title: '订单参数缺失', icon: 'none' })
+ return
+ }
+
+ clearInterval(orderPollTimer)
+ refreshOrderStatus()
+ orderPollTimer = setInterval(refreshOrderStatus, ORDER_POLL_INTERVAL)
+}
+
+async function refreshOrderStatus() {
+ if (!pageActive || !orderId.value || orderRefreshing || redirecting) return
+
+ orderRefreshing = true
+ try {
+ const detail = await getTourOrder(orderId.value)
+ if (!pageActive) return
+ orderDetail.value = detail
+ loadErrorShown = false
+ syncCountdown(detail)
+ redirectForOrderStatus(detail && detail.status)
+ } catch (error) {
+ if (!loadErrorShown) {
+ loadErrorShown = true
+ uni.showToast({ title: error.message || '订单状态更新失败', icon: 'none' })
+ }
+ console.warn('讲解中订单状态查询失败', error)
+ } finally {
+ orderRefreshing = false
+ }
+}
+
+function syncCountdown(detail) {
+ const durationSeconds = Math.max(0, Number(detail && detail.durationMinutes) || 0) * 60
+ const startedAt = parseDateTime(detail && detail.serviceStartedAt)
+ const elapsedSeconds = startedAt ? Math.max(0, Math.floor((Date.now() - startedAt) / 1000)) : 0
+ remainingSeconds.value = Math.max(0, durationSeconds - elapsedSeconds)
+}
+
+function startCountdown() {
+ clearInterval(countdownTimer)
+ countdownTimer = setInterval(() => {
+ if (remainingSeconds.value > 0) remainingSeconds.value -= 1
+ }, 1000)
+}
+
+function redirectForOrderStatus(status) {
+ const routeMap = {
+ PENDING_PAYMENT: '/pages/order-pending-pay/index',
+ PENDING_ACCEPT: '/pages/order-waiting/index',
+ PENDING_MEETING: '/pages/order-pending-guide/index',
+ COMPLETED: '/pages/order-pending-review/index',
+ CANCELLED: '/pages/order-canceled/index',
+ REFUNDED: '/pages/order-canceled/index'
+ }
+ const target = routeMap[status]
+ if (!target || redirecting) return
+
+ redirecting = true
+ clearTimers()
+ uni.redirectTo({
+ url: `${target}?orderId=${orderId.value}`,
+ fail() {
+ redirecting = false
+ if (pageActive) {
+ startOrderPolling()
+ startCountdown()
+ }
+ }
+ })
+}
+
+function clearTimers() {
+ if (countdownTimer) {
+ clearInterval(countdownTimer)
+ countdownTimer = null
+ }
+ if (orderPollTimer) {
+ clearInterval(orderPollTimer)
+ orderPollTimer = null
+ }
+}
+
+function parseDateTime(value) {
+ if (!value) return 0
+ const timestamp = new Date(String(value).replace(' ', 'T')).getTime()
+ return Number.isFinite(timestamp) ? timestamp : 0
+}
function formatTime(value) {
return String(value).padStart(2, '0')
}
+function formatAmount(value) {
+ const amount = Number(value || 0)
+ return Number.isFinite(amount) ? amount.toFixed(2) : '0.00'
+}
+
+function formatDateTime(value) {
+ return value ? String(value).replace('T', ' ').slice(0, 19) : '-'
+}
+
+function formatServiceTitle(packageName, orderType) {
+ if (packageName) return String(packageName).replace(/\s*·\s*/g, '·')
+ const serviceTitleMap = {
+ GROUP_QUICK: '拼团·快讲',
+ GROUP_PREMIUM: '拼团·精讲',
+ PRIVATE_QUICK: '包团·快讲',
+ PRIVATE_PREMIUM: '包团·精讲'
+ }
+ return serviceTitleMap[orderType] || '讲解服务'
+}
+
function handleCallGuide() {
- uni.showActionSheet({
- itemList: ['拨打导游电话'],
- success() {
- uni.showToast({
- title: '电话联系开发中',
- icon: 'none'
- })
- }
- })
+ if (!order.value.guideMobile) {
+ uni.showToast({ title: '暂无导游联系电话', icon: 'none' })
+ return
+ }
+ uni.makePhoneCall({ phoneNumber: order.value.guideMobile })
}
</script>
@@ -256,6 +390,10 @@
line-height: 1;
}
+.guide-star--inactive {
+ color: #d8dde6;
+}
+
.phone-action {
display: flex;
flex-direction: column;
--
Gitblit v1.7.1