From 0cf54692efc067adca7fa4308af241f723930b58 Mon Sep 17 00:00:00 2001
From: liujie <1793218484@qq.com>
Date: 星期五, 31 七月 2026 17:54:56 +0800
Subject: [PATCH] update

---
 mingdaojingjiang_guide_app/utils/rating.js                                               |    4 
 backend/src/main/java/com/tourguide/modules/review/mapper/OrderReviewMapper.java         |   13 +
 backend/src/test/java/com/tourguide/modules/user/service/GuideReceivingServiceTest.java  |  133 +++++++++++++++++++
 backend/src/main/java/com/tourguide/modules/user/dto/MiniUserResponse.java               |    7 
 frontend/src/views/GuideUserDetailView.vue                                               |    3 
 mingdaojingjiang_guide_app/pages/workbench/index.vue                                     |    6 
 backend/src/main/java/com/tourguide/modules/user/service/GuideReceivingService.java      |    3 
 backend/src/main/java/com/tourguide/modules/order/mapper/TourOrderMapper.java            |   14 ++
 backend/src/test/java/com/tourguide/modules/user/service/MiniUserServiceStatsTest.java   |   91 +++++++++++++
 backend/src/main/java/com/tourguide/modules/user/service/MiniUserService.java            |   68 +++++++++
 mingdaojingjiang_guide_app/pages/rating/index.vue                                        |    4 
 backend/src/main/java/com/tourguide/modules/review/dto/VisitorReviewStatRow.java         |   11 +
 backend/src/test/java/com/tourguide/modules/user/service/MiniUserServiceProfileTest.java |    8 +
 backend/src/main/java/com/tourguide/modules/user/dto/MiniUserDetailResponse.java         |   13 +
 backend/src/main/java/com/tourguide/modules/order/dto/VisitorOrderStatRow.java           |   16 ++
 15 files changed, 377 insertions(+), 17 deletions(-)

diff --git a/backend/src/main/java/com/tourguide/modules/order/dto/VisitorOrderStatRow.java b/backend/src/main/java/com/tourguide/modules/order/dto/VisitorOrderStatRow.java
new file mode 100644
index 0000000..a44ac7d
--- /dev/null
+++ b/backend/src/main/java/com/tourguide/modules/order/dto/VisitorOrderStatRow.java
@@ -0,0 +1,16 @@
+package com.tourguide.modules.order.dto;
+
+import java.math.BigDecimal;
+
+public class VisitorOrderStatRow {
+    private Long visitorId;
+    private Long orderCount;
+    private BigDecimal totalConsumption;
+
+    public Long getVisitorId() { return visitorId; }
+    public void setVisitorId(Long visitorId) { this.visitorId = visitorId; }
+    public Long getOrderCount() { return orderCount; }
+    public void setOrderCount(Long orderCount) { this.orderCount = orderCount; }
+    public BigDecimal getTotalConsumption() { return totalConsumption; }
+    public void setTotalConsumption(BigDecimal totalConsumption) { this.totalConsumption = totalConsumption; }
+}
diff --git a/backend/src/main/java/com/tourguide/modules/order/mapper/TourOrderMapper.java b/backend/src/main/java/com/tourguide/modules/order/mapper/TourOrderMapper.java
index 614bfac..2ba2d77 100644
--- a/backend/src/main/java/com/tourguide/modules/order/mapper/TourOrderMapper.java
+++ b/backend/src/main/java/com/tourguide/modules/order/mapper/TourOrderMapper.java
@@ -3,11 +3,13 @@
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.tourguide.modules.order.entity.TourOrder;
 import com.tourguide.modules.order.dto.GuideTodayStatsResponse;
+import com.tourguide.modules.order.dto.VisitorOrderStatRow;
 import org.apache.ibatis.annotations.Select;
 import org.apache.ibatis.annotations.Update;
 import org.apache.ibatis.annotations.Param;
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
+import java.util.Collection;
 import java.util.List;
 
 public interface TourOrderMapper extends BaseMapper<TourOrder> {
@@ -23,6 +25,18 @@
     @Select("SELECT DISTINCT scenic_name FROM tour_order WHERE deleted = 0 AND scenic_name IS NOT NULL AND scenic_name <> '' ORDER BY scenic_name")
     List<String> selectScenicOptions();
 
+    @Select({
+            "<script>",
+            "SELECT visitor_id AS visitorId, COUNT(*) AS orderCount, ",
+            "COALESCE(SUM(CASE WHEN status NOT IN ('PENDING_PAYMENT', 'CANCELLED', 'REFUNDED') ",
+            "THEN COALESCE(payment_amount, amount) ELSE 0 END), 0) AS totalConsumption ",
+            "FROM tour_order WHERE deleted = 0 AND visitor_id IN ",
+            "<foreach collection='visitorIds' item='visitorId' open='(' separator=',' close=')'>#{visitorId}</foreach>",
+            "GROUP BY visitor_id",
+            "</script>"
+    })
+    List<VisitorOrderStatRow> selectVisitorOrderStats(@Param("visitorIds") Collection<Long> visitorIds);
+
     @Select("SELECT COALESCE(SUM(guide_earning_amount), 0) FROM tour_order " +
             "WHERE deleted = 0 AND guide_id = #{guideId} AND status = 'COMPLETED' " +
             "AND guide_earning_amount IS NOT NULL " +
diff --git a/backend/src/main/java/com/tourguide/modules/review/dto/VisitorReviewStatRow.java b/backend/src/main/java/com/tourguide/modules/review/dto/VisitorReviewStatRow.java
new file mode 100644
index 0000000..cb7f47d
--- /dev/null
+++ b/backend/src/main/java/com/tourguide/modules/review/dto/VisitorReviewStatRow.java
@@ -0,0 +1,11 @@
+package com.tourguide.modules.review.dto;
+
+public class VisitorReviewStatRow {
+    private Long visitorId;
+    private Long reviewCount;
+
+    public Long getVisitorId() { return visitorId; }
+    public void setVisitorId(Long visitorId) { this.visitorId = visitorId; }
+    public Long getReviewCount() { return reviewCount; }
+    public void setReviewCount(Long reviewCount) { this.reviewCount = reviewCount; }
+}
diff --git a/backend/src/main/java/com/tourguide/modules/review/mapper/OrderReviewMapper.java b/backend/src/main/java/com/tourguide/modules/review/mapper/OrderReviewMapper.java
index 701d495..a1ca538 100644
--- a/backend/src/main/java/com/tourguide/modules/review/mapper/OrderReviewMapper.java
+++ b/backend/src/main/java/com/tourguide/modules/review/mapper/OrderReviewMapper.java
@@ -2,8 +2,11 @@
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.tourguide.modules.review.entity.OrderReview;
+import com.tourguide.modules.review.dto.VisitorReviewStatRow;
+import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 import java.math.BigDecimal;
+import java.util.Collection;
 import java.util.List;
 
 public interface OrderReviewMapper extends BaseMapper<OrderReview> {
@@ -13,6 +16,16 @@
     @Select("SELECT COUNT(*) FROM tour_order_review WHERE order_id = #{orderId} AND deleted = 0")
     long countByOrderId(Long orderId);
 
+    @Select({
+            "<script>",
+            "SELECT visitor_id AS visitorId, COUNT(*) AS reviewCount ",
+            "FROM tour_order_review WHERE deleted = 0 AND status = 1 AND visitor_id IN ",
+            "<foreach collection='visitorIds' item='visitorId' open='(' separator=',' close=')'>#{visitorId}</foreach>",
+            "GROUP BY visitor_id",
+            "</script>"
+    })
+    List<VisitorReviewStatRow> selectVisitorReviewStats(@Param("visitorIds") Collection<Long> visitorIds);
+
     @Select("SELECT COALESCE(AVG(score), 0) FROM tour_order_review WHERE guide_id = #{guideId} AND status = 1 AND deleted = 0")
     BigDecimal selectAverageScore(Long guideId);
 }
diff --git a/backend/src/main/java/com/tourguide/modules/user/dto/MiniUserDetailResponse.java b/backend/src/main/java/com/tourguide/modules/user/dto/MiniUserDetailResponse.java
index 34aee14..9473013 100644
--- a/backend/src/main/java/com/tourguide/modules/user/dto/MiniUserDetailResponse.java
+++ b/backend/src/main/java/com/tourguide/modules/user/dto/MiniUserDetailResponse.java
@@ -21,7 +21,7 @@
     private final BigDecimal totalConsumption;
     private final long reviewCount;
 
-    private MiniUserDetailResponse(MiniUser user) {
+    private MiniUserDetailResponse(MiniUser user, long orderCount, BigDecimal totalConsumption, long reviewCount) {
         this.id = user.getId();
         this.username = user.getUsername();
         this.nickname = user.getNickname();
@@ -32,12 +32,15 @@
         this.realNameVerified = StringUtils.hasText(user.getRealName()) && StringUtils.hasText(user.getIdCardNo());
         this.status = user.getStatus();
         this.createdAt = user.getCreatedAt();
-        this.orderCount = 0L;
-        this.totalConsumption = BigDecimal.ZERO;
-        this.reviewCount = 0L;
+        this.orderCount = orderCount;
+        this.totalConsumption = totalConsumption == null ? BigDecimal.ZERO : totalConsumption;
+        this.reviewCount = reviewCount;
     }
 
-    public static MiniUserDetailResponse from(MiniUser user) { return new MiniUserDetailResponse(user); }
+    public static MiniUserDetailResponse from(MiniUser user) { return from(user, 0L, BigDecimal.ZERO, 0L); }
+    public static MiniUserDetailResponse from(MiniUser user, long orderCount, BigDecimal totalConsumption, long reviewCount) {
+        return new MiniUserDetailResponse(user, orderCount, totalConsumption, reviewCount);
+    }
     public Long getId() { return id; }
     public String getUsername() { return username; }
     public String getNickname() { return nickname; }
diff --git a/backend/src/main/java/com/tourguide/modules/user/dto/MiniUserResponse.java b/backend/src/main/java/com/tourguide/modules/user/dto/MiniUserResponse.java
index fddff7f..5a89d1b 100644
--- a/backend/src/main/java/com/tourguide/modules/user/dto/MiniUserResponse.java
+++ b/backend/src/main/java/com/tourguide/modules/user/dto/MiniUserResponse.java
@@ -18,7 +18,7 @@
     private final LocalDateTime createdAt;
     private final long orderCount;
 
-    private MiniUserResponse(MiniUser user) {
+    private MiniUserResponse(MiniUser user, long orderCount) {
         this.id = user.getId();
         this.username = user.getUsername();
         this.nickname = user.getNickname();
@@ -29,9 +29,10 @@
         this.realNameVerified = StringUtils.hasText(user.getRealName()) && StringUtils.hasText(user.getIdCardNo());
         this.status = user.getStatus();
         this.createdAt = user.getCreatedAt();
-        this.orderCount = 0L;
+        this.orderCount = orderCount;
     }
-    public static MiniUserResponse from(MiniUser user) { return new MiniUserResponse(user); }
+    public static MiniUserResponse from(MiniUser user) { return from(user, 0L); }
+    public static MiniUserResponse from(MiniUser user, long orderCount) { return new MiniUserResponse(user, orderCount); }
     public Long getId() { return id; }
     public String getUsername() { return username; }
     public String getNickname() { return nickname; }
diff --git a/backend/src/main/java/com/tourguide/modules/user/service/GuideReceivingService.java b/backend/src/main/java/com/tourguide/modules/user/service/GuideReceivingService.java
index 170a7c4..53b13e7 100644
--- a/backend/src/main/java/com/tourguide/modules/user/service/GuideReceivingService.java
+++ b/backend/src/main/java/com/tourguide/modules/user/service/GuideReceivingService.java
@@ -216,6 +216,9 @@
     }
 
     private boolean premiumEnabled(GuideUser guide, PlatformSettingsResponse settings) {
+        if (guide.getAutoDisableWhitelisted() != null && guide.getAutoDisableWhitelisted() == 1) {
+            return true;
+        }
         long orderCount = guide.getAcceptedOrderCount() == null ? 0 : guide.getAcceptedOrderCount();
         BigDecimal rating = guide.getRating() == null ? BigDecimal.ZERO : guide.getRating();
         BigDecimal requiredRating = settings.getPreciseDispatchRating() == null ? BigDecimal.ZERO : settings.getPreciseDispatchRating();
diff --git a/backend/src/main/java/com/tourguide/modules/user/service/MiniUserService.java b/backend/src/main/java/com/tourguide/modules/user/service/MiniUserService.java
index b1072b4..bf18110 100644
--- a/backend/src/main/java/com/tourguide/modules/user/service/MiniUserService.java
+++ b/backend/src/main/java/com/tourguide/modules/user/service/MiniUserService.java
@@ -5,6 +5,10 @@
 import com.tourguide.common.api.PageResponse;
 import com.tourguide.common.exception.BusinessException;
 import com.tourguide.common.validation.ChineseIdCardValidator;
+import com.tourguide.modules.order.dto.VisitorOrderStatRow;
+import com.tourguide.modules.order.mapper.TourOrderMapper;
+import com.tourguide.modules.review.dto.VisitorReviewStatRow;
+import com.tourguide.modules.review.mapper.OrderReviewMapper;
 import com.tourguide.modules.user.dto.MiniProfileUpdateRequest;
 import com.tourguide.modules.user.dto.MiniRealNameUpdateRequest;
 import com.tourguide.modules.user.dto.MiniUserDetailResponse;
@@ -14,15 +18,26 @@
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.StringUtils;
+import java.math.BigDecimal;
 import java.time.LocalDate;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 
 /** 仅处理小程序用户,管理后台只能通过本服务执行查询或状态管理。 */
 @Service
 public class MiniUserService {
     private final MiniUserMapper userMapper;
+    private final TourOrderMapper orderMapper;
+    private final OrderReviewMapper reviewMapper;
 
-    public MiniUserService(MiniUserMapper userMapper) { this.userMapper = userMapper; }
+    public MiniUserService(MiniUserMapper userMapper, TourOrderMapper orderMapper, OrderReviewMapper reviewMapper) {
+        this.userMapper = userMapper;
+        this.orderMapper = orderMapper;
+        this.reviewMapper = reviewMapper;
+    }
 
     public MiniUser findEnabledById(Long id) {
         MiniUser user = userMapper.selectById(id);
@@ -55,14 +70,55 @@
                 .lt(endDate != null, MiniUser::getCreatedAt, endDate == null ? null : endDate.plusDays(1).atStartOfDay())
                 .orderByDesc(MiniUser::getId);
         Page<MiniUser> result = userMapper.selectPage(new Page<MiniUser>(page, size), query);
-        return new PageResponse<MiniUserResponse>(result.getRecords().stream().map(MiniUserResponse::from).collect(Collectors.toList()),
+        List<MiniUser> users = result.getRecords();
+        Map<Long, UserStats> statsByUserId = loadUserStats(
+                users.stream().map(MiniUser::getId).collect(Collectors.toList()));
+        return new PageResponse<MiniUserResponse>(users.stream()
+                .map(user -> MiniUserResponse.from(user, statsOf(statsByUserId, user.getId()).orderCount))
+                .collect(Collectors.toList()),
                 result.getTotal(), result.getCurrent(), result.getSize());
     }
 
     public MiniUserDetailResponse detail(Long id) {
         MiniUser user = userMapper.selectById(id);
         if (user == null) { throw new BusinessException(404, "游客不存在"); }
-        return MiniUserDetailResponse.from(user);
+        UserStats stats = statsOf(loadUserStats(Collections.singletonList(id)), id);
+        return MiniUserDetailResponse.from(user, stats.orderCount, stats.totalConsumption, stats.reviewCount);
+    }
+
+    private Map<Long, UserStats> loadUserStats(List<Long> visitorIds) {
+        if (visitorIds.isEmpty()) { return Collections.emptyMap(); }
+        Map<Long, UserStats> statsByUserId = new HashMap<Long, UserStats>();
+        for (Long visitorId : visitorIds) {
+            statsByUserId.put(visitorId, new UserStats());
+        }
+        List<VisitorOrderStatRow> orderRows = orderMapper.selectVisitorOrderStats(visitorIds);
+        if (orderRows != null) {
+            for (VisitorOrderStatRow row : orderRows) {
+                if (row.getVisitorId() == null) { continue; }
+                UserStats stats = statsByUserId.computeIfAbsent(row.getVisitorId(), ignored -> new UserStats());
+                stats.orderCount = defaultLong(row.getOrderCount());
+                stats.totalConsumption = row.getTotalConsumption() == null ? BigDecimal.ZERO : row.getTotalConsumption();
+            }
+        }
+        List<VisitorReviewStatRow> reviewRows = reviewMapper.selectVisitorReviewStats(visitorIds);
+        if (reviewRows != null) {
+            for (VisitorReviewStatRow row : reviewRows) {
+                if (row.getVisitorId() == null) { continue; }
+                statsByUserId.computeIfAbsent(row.getVisitorId(), ignored -> new UserStats())
+                        .reviewCount = defaultLong(row.getReviewCount());
+            }
+        }
+        return statsByUserId;
+    }
+
+    private static UserStats statsOf(Map<Long, UserStats> statsByUserId, Long userId) {
+        UserStats stats = statsByUserId.get(userId);
+        return stats == null ? new UserStats() : stats;
+    }
+
+    private static long defaultLong(Long value) {
+        return value == null ? 0L : value;
     }
 
     @Transactional
@@ -106,4 +162,10 @@
         user.setStatus(status);
         userMapper.updateById(user);
     }
+
+    private static final class UserStats {
+        private long orderCount;
+        private BigDecimal totalConsumption = BigDecimal.ZERO;
+        private long reviewCount;
+    }
 }
diff --git a/backend/src/test/java/com/tourguide/modules/user/service/GuideReceivingServiceTest.java b/backend/src/test/java/com/tourguide/modules/user/service/GuideReceivingServiceTest.java
new file mode 100644
index 0000000..b5e4c36
--- /dev/null
+++ b/backend/src/test/java/com/tourguide/modules/user/service/GuideReceivingServiceTest.java
@@ -0,0 +1,133 @@
+package com.tourguide.modules.user.service;
+
+import com.tourguide.modules.operation.dto.PlatformSettingsResponse;
+import com.tourguide.modules.operation.entity.OperationScenicSpot;
+import com.tourguide.modules.operation.entity.ScenicPricing;
+import com.tourguide.modules.operation.mapper.OperationScenicSpotMapper;
+import com.tourguide.modules.operation.mapper.ScenicMeetingPointMapper;
+import com.tourguide.modules.operation.mapper.ScenicPricingMapper;
+import com.tourguide.modules.operation.service.PlatformSettingsService;
+import com.tourguide.modules.order.mapper.TourOrderMapper;
+import com.tourguide.modules.user.dto.GuideReceivingOptionResponse;
+import com.tourguide.modules.user.entity.GuideScenicConfig;
+import com.tourguide.modules.user.entity.GuideUser;
+import com.tourguide.modules.user.mapper.GuideRealtimeStatusMapper;
+import com.tourguide.modules.user.mapper.GuideReceivingSessionMapper;
+import com.tourguide.modules.user.mapper.GuideScenicConfigMapper;
+import com.tourguide.modules.user.mapper.GuideTeamScenicConfigMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class GuideReceivingServiceTest {
+    private GuideUserService guideUserService;
+    private GuideScenicConfigMapper guideScenicMapper;
+    private OperationScenicSpotMapper scenicMapper;
+    private ScenicMeetingPointMapper meetingPointMapper;
+    private ScenicPricingMapper pricingMapper;
+    private GuideReceivingSessionMapper sessionMapper;
+    private GuideRealtimeStatusMapper realtimeStatusMapper;
+    private PlatformSettingsService platformSettingsService;
+    private GuideReceivingService service;
+
+    @BeforeEach
+    void setUp() {
+        guideUserService = mock(GuideUserService.class);
+        guideScenicMapper = mock(GuideScenicConfigMapper.class);
+        GuideTeamScenicConfigMapper teamScenicMapper = mock(GuideTeamScenicConfigMapper.class);
+        scenicMapper = mock(OperationScenicSpotMapper.class);
+        meetingPointMapper = mock(ScenicMeetingPointMapper.class);
+        pricingMapper = mock(ScenicPricingMapper.class);
+        sessionMapper = mock(GuideReceivingSessionMapper.class);
+        realtimeStatusMapper = mock(GuideRealtimeStatusMapper.class);
+        platformSettingsService = mock(PlatformSettingsService.class);
+        TourOrderMapper orderMapper = mock(TourOrderMapper.class);
+        service = new GuideReceivingService(guideUserService, guideScenicMapper, teamScenicMapper, scenicMapper,
+                meetingPointMapper, pricingMapper, sessionMapper, realtimeStatusMapper, platformSettingsService,
+                orderMapper);
+    }
+
+    @Test
+    void shouldEnablePremiumOptionsForWhitelistedGuideBelowRuleThresholds() {
+        when(guideUserService.findEnabledById(8L)).thenReturn(whitelistedGuide());
+        when(platformSettingsService.getSettings()).thenReturn(strictSettings());
+        when(guideScenicMapper.selectList(any())).thenReturn(Collections.singletonList(scenicConfig()));
+        when(scenicMapper.selectList(any())).thenReturn(Collections.singletonList(scenicSpot()));
+        when(meetingPointMapper.selectList(any())).thenReturn(Collections.emptyList());
+        when(pricingMapper.selectList(any())).thenReturn(Collections.singletonList(premiumPricing()));
+
+        GuideReceivingOptionResponse response = service.options(8L);
+
+        assertThat(response.isPremiumEnabled()).isTrue();
+    }
+
+    @Test
+    void shouldDisablePremiumOptionsForNonWhitelistedGuideBelowRuleThresholds() {
+        when(guideUserService.findEnabledById(8L)).thenReturn(nonWhitelistedGuide());
+        when(platformSettingsService.getSettings()).thenReturn(strictSettings());
+        when(guideScenicMapper.selectList(any())).thenReturn(Collections.singletonList(scenicConfig()));
+        when(scenicMapper.selectList(any())).thenReturn(Collections.singletonList(scenicSpot()));
+        when(meetingPointMapper.selectList(any())).thenReturn(Collections.emptyList());
+        when(pricingMapper.selectList(any())).thenReturn(Collections.singletonList(premiumPricing()));
+
+        GuideReceivingOptionResponse response = service.options(8L);
+
+        assertThat(response.isPremiumEnabled()).isFalse();
+    }
+
+    private GuideUser whitelistedGuide() {
+        GuideUser guide = new GuideUser();
+        guide.setId(8L);
+        guide.setAcceptedOrderCount(0L);
+        guide.setRating(BigDecimal.ZERO);
+        guide.setAutoDisableWhitelisted(1);
+        guide.setStatus(GuideUserService.STATUS_ENABLED);
+        return guide;
+    }
+
+    private GuideUser nonWhitelistedGuide() {
+        GuideUser guide = whitelistedGuide();
+        guide.setAutoDisableWhitelisted(0);
+        return guide;
+    }
+
+    private GuideScenicConfig scenicConfig() {
+        GuideScenicConfig config = new GuideScenicConfig();
+        config.setGuideId(8L);
+        config.setScenicName("Test Scenic");
+        return config;
+    }
+
+    private OperationScenicSpot scenicSpot() {
+        OperationScenicSpot scenic = new OperationScenicSpot();
+        scenic.setId(3L);
+        scenic.setScenicName("Test Scenic");
+        scenic.setStatus(1);
+        return scenic;
+    }
+
+    private ScenicPricing premiumPricing() {
+        ScenicPricing pricing = new ScenicPricing();
+        pricing.setId(9L);
+        pricing.setScenicSpotId(3L);
+        pricing.setServiceType("GROUP_PREMIUM");
+        pricing.setDurationMinutes(60);
+        pricing.setMinParticipants(20);
+        pricing.setMaxParticipants(30);
+        return pricing;
+    }
+
+    private PlatformSettingsResponse strictSettings() {
+        PlatformSettingsResponse settings = new PlatformSettingsResponse();
+        settings.setPreciseDispatchOrderCount(10);
+        settings.setPreciseDispatchRating(new BigDecimal("4.8"));
+        return settings;
+    }
+}
diff --git a/backend/src/test/java/com/tourguide/modules/user/service/MiniUserServiceProfileTest.java b/backend/src/test/java/com/tourguide/modules/user/service/MiniUserServiceProfileTest.java
index a6f81f0..2bc82ec 100644
--- a/backend/src/test/java/com/tourguide/modules/user/service/MiniUserServiceProfileTest.java
+++ b/backend/src/test/java/com/tourguide/modules/user/service/MiniUserServiceProfileTest.java
@@ -5,6 +5,8 @@
 import com.tourguide.modules.user.dto.MiniUserResponse;
 import com.tourguide.modules.user.entity.MiniUser;
 import com.tourguide.modules.user.mapper.MiniUserMapper;
+import com.tourguide.modules.order.mapper.TourOrderMapper;
+import com.tourguide.modules.review.mapper.OrderReviewMapper;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
@@ -16,13 +18,17 @@
 
 class MiniUserServiceProfileTest {
     private MiniUserMapper userMapper;
+    private TourOrderMapper orderMapper;
+    private OrderReviewMapper reviewMapper;
     private MiniUserService userService;
     private MiniUser user;
 
     @BeforeEach
     void setUp() {
         userMapper = mock(MiniUserMapper.class);
-        userService = new MiniUserService(userMapper);
+        orderMapper = mock(TourOrderMapper.class);
+        reviewMapper = mock(OrderReviewMapper.class);
+        userService = new MiniUserService(userMapper, orderMapper, reviewMapper);
         user = new MiniUser();
         user.setId(1L);
         user.setUsername("wx_user");
diff --git a/backend/src/test/java/com/tourguide/modules/user/service/MiniUserServiceStatsTest.java b/backend/src/test/java/com/tourguide/modules/user/service/MiniUserServiceStatsTest.java
new file mode 100644
index 0000000..cac6de3
--- /dev/null
+++ b/backend/src/test/java/com/tourguide/modules/user/service/MiniUserServiceStatsTest.java
@@ -0,0 +1,91 @@
+package com.tourguide.modules.user.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.tourguide.common.api.PageResponse;
+import com.tourguide.modules.order.dto.VisitorOrderStatRow;
+import com.tourguide.modules.order.mapper.TourOrderMapper;
+import com.tourguide.modules.review.dto.VisitorReviewStatRow;
+import com.tourguide.modules.review.mapper.OrderReviewMapper;
+import com.tourguide.modules.user.dto.MiniUserDetailResponse;
+import com.tourguide.modules.user.dto.MiniUserResponse;
+import com.tourguide.modules.user.entity.MiniUser;
+import com.tourguide.modules.user.mapper.MiniUserMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class MiniUserServiceStatsTest {
+    private MiniUserMapper userMapper;
+    private TourOrderMapper orderMapper;
+    private OrderReviewMapper reviewMapper;
+    private MiniUserService userService;
+
+    @BeforeEach
+    void setUp() {
+        userMapper = mock(MiniUserMapper.class);
+        orderMapper = mock(TourOrderMapper.class);
+        reviewMapper = mock(OrderReviewMapper.class);
+        userService = new MiniUserService(userMapper, orderMapper, reviewMapper);
+    }
+
+    @Test
+    void pageShouldReturnVisitorOrderCount() {
+        MiniUser user = miniUser(7L);
+        Page<MiniUser> page = new Page<MiniUser>(1, 10);
+        page.setRecords(Collections.singletonList(user));
+        page.setTotal(1L);
+        when(userMapper.selectPage(any(), any())).thenReturn(page);
+        when(orderMapper.selectVisitorOrderStats(any())).thenReturn(Collections.singletonList(orderStat(7L, 1L)));
+        when(reviewMapper.selectVisitorReviewStats(any())).thenReturn(Collections.emptyList());
+
+        PageResponse<MiniUserResponse> response = userService.page(1, 10, null, null, null, null);
+
+        assertThat(response.getRecords()).hasSize(1);
+        assertThat(response.getRecords().get(0).getOrderCount()).isEqualTo(1L);
+    }
+
+    @Test
+    void detailShouldReturnVisitorConsumptionStats() {
+        MiniUser user = miniUser(7L);
+        when(userMapper.selectById(7L)).thenReturn(user);
+        when(orderMapper.selectVisitorOrderStats(any())).thenReturn(Collections.singletonList(orderStat(7L, 1L)));
+        when(reviewMapper.selectVisitorReviewStats(any())).thenReturn(Collections.singletonList(reviewStat(7L, 2L)));
+
+        MiniUserDetailResponse response = userService.detail(7L);
+
+        assertThat(response.getOrderCount()).isEqualTo(1L);
+        assertThat(response.getTotalConsumption()).isEqualByComparingTo("36.00");
+        assertThat(response.getReviewCount()).isEqualTo(2L);
+    }
+
+    private MiniUser miniUser(Long id) {
+        MiniUser user = new MiniUser();
+        user.setId(id);
+        user.setUsername("mini_" + id);
+        user.setNickname("Visitor " + id);
+        user.setStatus(1);
+        return user;
+    }
+
+    private VisitorOrderStatRow orderStat(Long visitorId, Long orderCount) {
+        VisitorOrderStatRow row = new VisitorOrderStatRow();
+        row.setVisitorId(visitorId);
+        row.setOrderCount(orderCount);
+        row.setTotalConsumption(new BigDecimal("36.00"));
+        return row;
+    }
+
+    private VisitorReviewStatRow reviewStat(Long visitorId, Long reviewCount) {
+        VisitorReviewStatRow row = new VisitorReviewStatRow();
+        row.setVisitorId(visitorId);
+        row.setReviewCount(reviewCount);
+        return row;
+    }
+}
diff --git a/frontend/src/views/GuideUserDetailView.vue b/frontend/src/views/GuideUserDetailView.vue
index c6e6d53..3c90eb7 100644
--- a/frontend/src/views/GuideUserDetailView.vue
+++ b/frontend/src/views/GuideUserDetailView.vue
@@ -186,8 +186,7 @@
               </div>
               <div class="document-actions">
                 <el-link v-if="user[item.key]" type="primary" :href="user[item.key]" target="_blank">查看大图</el-link>
-                <el-input v-model="user[item.key]" clearable placeholder="请输入图片地址" />
-                <el-button type="primary" plain :icon="Upload" @click="ElMessage.info('图片上传服务接入后可直接上传,当前请填写图片地址')">上传</el-button>
+                <el-button type="primary" plain :icon="Upload" @click="ElMessage.info('图片上传服务接入后可直接上传')">上传</el-button>
               </div>
             </div>
           </div>
diff --git a/mingdaojingjiang_guide_app/pages/rating/index.vue b/mingdaojingjiang_guide_app/pages/rating/index.vue
index 6bfda00..3b849b9 100644
--- a/mingdaojingjiang_guide_app/pages/rating/index.vue
+++ b/mingdaojingjiang_guide_app/pages/rating/index.vue
@@ -43,6 +43,7 @@
 <script>
 import GuideNavBar from '../../components/guide-nav-bar/guide-nav-bar.vue'
 import { getGuideRatings } from '../../services/rating'
+import { formatRatingStars } from '../../utils/rating'
 import { backOrHome } from '../../utils/router'
 
 export default {
@@ -88,8 +89,7 @@
         })
     },
     starText(score) {
-      const count = Math.max(0, Math.min(5, Math.round(Number(score) || 0)))
-      return `${'★'.repeat(count)}${'☆'.repeat(5 - count)}`
+      return formatRatingStars(score)
     },
     goBack() {
       if (this.isLeaving) {
diff --git a/mingdaojingjiang_guide_app/pages/workbench/index.vue b/mingdaojingjiang_guide_app/pages/workbench/index.vue
index d68c95f..07930d6 100644
--- a/mingdaojingjiang_guide_app/pages/workbench/index.vue
+++ b/mingdaojingjiang_guide_app/pages/workbench/index.vue
@@ -97,7 +97,7 @@
               <text class="guide-name">{{ guide.name || '赵四' }}</text>
               <text v-if="auditStatus !== 'approved'" class="audit-tag">{{ auditStatusText }}</text>
             </view>
-            <view v-if="auditStatus === 'approved'" class="stars">★★★★★</view>
+            <view v-if="auditStatus === 'approved'" class="stars">{{ drawerRatingStars }}</view>
           </view>
           <image v-if="auditStatus === 'approved'" class="drawer-arrow" src="/static/icons/chevron-right-white.svg" mode="aspectFit" />
         </view>
@@ -140,6 +140,7 @@
 import { playNotificationAlert } from '../../services/message'
 import { getWorkbenchData } from '../../services/workbench'
 import { formatDuration, maskPhone } from '../../utils/format'
+import { formatRatingStars } from '../../utils/rating'
 import { routeTo } from '../../utils/router'
 import { getStatusBarHeight, upxToPx } from '../../utils/system'
 
@@ -239,6 +240,9 @@
       }
       return this.drawerMenus.filter((item) => item.text === '设置')
     },
+    drawerRatingStars() {
+      return formatRatingStars(this.guide.rating)
+    },
     isReceiving() {
       return ['online', 'serving'].includes(this.guide.onlineStatus) || this.receivingOrders.length > 0
     },
diff --git a/mingdaojingjiang_guide_app/utils/rating.js b/mingdaojingjiang_guide_app/utils/rating.js
new file mode 100644
index 0000000..1463b4a
--- /dev/null
+++ b/mingdaojingjiang_guide_app/utils/rating.js
@@ -0,0 +1,4 @@
+export function formatRatingStars(score) {
+  const count = Math.max(0, Math.min(5, Math.round(Number(score) || 0)))
+  return `${'★'.repeat(count)}${'☆'.repeat(5 - count)}`
+}

--
Gitblit v1.7.1