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