package com.tourguide.modules.review.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.tourguide.common.api.PageResponse; import com.tourguide.common.exception.BusinessException; import com.tourguide.modules.order.domain.OrderStatus; import com.tourguide.modules.order.entity.TourOrder; import com.tourguide.modules.order.mapper.TourOrderMapper; import com.tourguide.modules.review.dto.OrderReviewCreateRequest; import com.tourguide.modules.review.dto.OrderReviewResponse; import com.tourguide.modules.review.dto.GuideRatingItemResponse; import com.tourguide.modules.review.dto.GuideRatingResponse; import com.tourguide.modules.review.entity.OrderReview; import com.tourguide.modules.review.mapper.OrderReviewMapper; import com.tourguide.modules.user.entity.GuideUser; import com.tourguide.modules.user.mapper.GuideUserMapper; import com.tourguide.modules.user.service.GuideAutoDisableService; 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.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; import java.math.RoundingMode; import java.util.UUID; import java.util.stream.Collectors; @Service public class OrderReviewService { private final OrderReviewMapper reviewMapper; private final TourOrderMapper orderMapper; private final GuideUserMapper guideUserMapper; private final GuideAutoDisableService autoDisableService; public OrderReviewService(OrderReviewMapper reviewMapper, TourOrderMapper orderMapper, GuideUserMapper guideUserMapper, GuideAutoDisableService autoDisableService) { this.reviewMapper = reviewMapper; this.orderMapper = orderMapper; this.guideUserMapper = guideUserMapper; this.autoDisableService = autoDisableService; } public PageResponse page(long page, long size, String keyword, Integer status, Integer score, String scenicName, LocalDate startDate, LocalDate endDate) { LambdaQueryWrapper query = new LambdaQueryWrapper(); if (StringUtils.hasText(keyword)) { String searchKeyword = keyword.trim(); query.and(wrapper -> wrapper.like(OrderReview::getVisitorMobile, searchKeyword) .or().like(OrderReview::getGuideMobile, searchKeyword) .or().like(OrderReview::getGuideName, searchKeyword) .or().like(OrderReview::getOrderNo, searchKeyword) .or().like(OrderReview::getReviewNo, searchKeyword)); } query.eq(status != null, OrderReview::getStatus, status) .eq(score != null, OrderReview::getScore, score) .eq(StringUtils.hasText(scenicName), OrderReview::getScenicName, scenicName) .ge(startDate != null, OrderReview::getReviewedAt, startDate == null ? null : startDate.atStartOfDay()) .lt(endDate != null, OrderReview::getReviewedAt, endDate == null ? null : endDate.plusDays(1).atStartOfDay()) .orderByDesc(OrderReview::getId); Page result = reviewMapper.selectPage(new Page(page, size), query); return new PageResponse(result.getRecords().stream() .map(OrderReviewResponse::new).collect(Collectors.toList()), result.getTotal(), result.getCurrent(), result.getSize()); } public List scenicOptions() { return reviewMapper.selectScenicOptions(); } public GuideRatingResponse guideRatings(Long guideId, LocalDate date, long pageNum, long pageSize) { GuideUser guide = guideUserMapper.selectById(guideId); if (guide == null || guide.getStatus() == null || guide.getStatus() != 1) { throw new BusinessException(401, "导游用户不存在或已停用"); } long normalizedPageNum = Math.max(pageNum, 1L); long normalizedPageSize = Math.min(Math.max(pageSize, 1L), 100L); LambdaQueryWrapper query = new LambdaQueryWrapper() .eq(OrderReview::getGuideId, guideId) .eq(OrderReview::getStatus, 1) .ge(date != null, OrderReview::getReviewedAt, date == null ? null : date.atStartOfDay()) .lt(date != null, OrderReview::getReviewedAt, date == null ? null : date.plusDays(1).atStartOfDay()) .orderByDesc(OrderReview::getReviewedAt) .orderByDesc(OrderReview::getId); Page result = reviewMapper.selectPage( new Page(normalizedPageNum, normalizedPageSize), query); BigDecimal score = guide.getRating() == null ? BigDecimal.ZERO : guide.getRating(); return new GuideRatingResponse( score, result.getRecords().stream().map(GuideRatingItemResponse::new).collect(Collectors.toList()), result.getTotal(), result.getCurrent(), result.getSize()); } @Transactional public OrderReviewResponse create(Long visitorId, OrderReviewCreateRequest request) { TourOrder order = orderMapper.selectById(request.getOrderId()); if (order == null) throw new BusinessException(404, "订单不存在"); if (order.getVisitorId() == null || !order.getVisitorId().equals(visitorId)) { throw new BusinessException(403, "只能评价本人订单"); } if (!OrderStatus.COMPLETED.matches(order.getStatus())) { throw new BusinessException(409, "仅已完成订单可以评价"); } if (order.getGuideId() == null) throw new BusinessException(409, "订单尚未关联导游"); if (reviewMapper.countByOrderId(order.getId()) > 0) throw new BusinessException(409, "该订单已经评价"); OrderReview review = new OrderReview(); review.setReviewNo(generateReviewNo()); review.setOrderId(order.getId()); review.setOrderNo(order.getOrderNo()); review.setOrderPlacedAt(order.getPlacedAt()); review.setVisitorId(order.getVisitorId()); review.setVisitorNickname(order.getVisitorNickname()); review.setVisitorMobile(order.getVisitorMobile()); review.setGuideId(order.getGuideId()); review.setGuideName(order.getGuideName()); review.setGuideMobile(order.getGuideMobile()); review.setScenicName(order.getScenicName()); review.setScore(request.getScore()); review.setContent(request.getContent()); review.setReviewedAt(LocalDateTime.now()); review.setStatus(1); review.setDeleted(0); reviewMapper.insert(review); refreshGuideRating(order.getGuideId()); return new OrderReviewResponse(review); } public OrderReviewResponse detailByOrder(Long visitorId, Long orderId) { TourOrder order = orderMapper.selectById(orderId); if (order == null) throw new BusinessException(404, "order not found"); if (order.getVisitorId() == null || !order.getVisitorId().equals(visitorId)) { throw new BusinessException(403, "cannot view another visitor's review"); } OrderReview review = reviewMapper.selectOne(new LambdaQueryWrapper() .eq(OrderReview::getOrderId, orderId) .eq(OrderReview::getStatus, 1) .last("LIMIT 1")); if (review == null) throw new BusinessException(404, "review not found"); return new OrderReviewResponse(review); } @Transactional public void delete(Long id) { OrderReview review = reviewMapper.selectById(id); if (review == null) throw new BusinessException(404, "评价不存在"); review.setDeletedAt(LocalDateTime.now()); reviewMapper.updateById(review); reviewMapper.deleteById(id); if (review.getGuideId() != null) refreshGuideRating(review.getGuideId()); } private void refreshGuideRating(Long guideId) { GuideUser guide = guideUserMapper.selectById(guideId); if (guide == null) return; BigDecimal average = reviewMapper.selectAverageScore(guideId); guide.setRating(average == null ? BigDecimal.ZERO : average.setScale(2, RoundingMode.HALF_UP)); guideUserMapper.updateById(guide); autoDisableService.disableGuideIfEligible(guideId); } private String generateReviewNo() { String time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); return "RV" + time + UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase(); } }