backend/src/main/java/com/tourguide/modules/order/dto/VisitorOrderStatRow.java
New file @@ -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; } } 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 " + backend/src/main/java/com/tourguide/modules/review/dto/VisitorReviewStatRow.java
New file @@ -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; } } 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); } 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; } 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; } 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(); 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; } } backend/src/test/java/com/tourguide/modules/user/service/GuideReceivingServiceTest.java
New file @@ -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; } } 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"); backend/src/test/java/com/tourguide/modules/user/service/MiniUserServiceStatsTest.java
New file @@ -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; } } 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> 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) { 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 }, mingdaojingjiang_guide_app/utils/rating.js
New file @@ -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)}` }