|
|
@@ -0,0 +1,193 @@
|
|
|
+package com.ylx.home.hot.service.impl;
|
|
|
+
|
|
|
+import cn.hutool.core.collection.CollUtil;
|
|
|
+import cn.hutool.core.util.ObjectUtil;
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
+import com.ylx.common.core.domain.R;
|
|
|
+import com.ylx.common.core.domain.entity.SysDictData;
|
|
|
+import com.ylx.common.utils.DictUtils;
|
|
|
+import com.ylx.common.utils.DistanceUtil;
|
|
|
+import com.ylx.home.hot.domain.dto.HotRecommendDTO;
|
|
|
+import com.ylx.home.hot.domain.vo.HotRecommendVO;
|
|
|
+import com.ylx.home.hot.service.HomeHotRecommendService;
|
|
|
+import com.ylx.massage.domain.MaTechnician;
|
|
|
+import com.ylx.massage.service.IMaTechnicianService;
|
|
|
+import com.ylx.merchant.domain.vo.MerchantWithAddressVO;
|
|
|
+import com.ylx.project.domain.Project;
|
|
|
+import com.ylx.project.service.ProjectService;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.data.redis.core.RedisTemplate;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+import static com.ylx.order.constant.OrderConstant.DICT_UNIT_TYPE;
|
|
|
+
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+public class HomeHotRecommendServiceImpl implements HomeHotRecommendService {
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private RedisTemplate<String, String> redisTemplate;
|
|
|
+ @Resource
|
|
|
+ private ProjectService projectService;
|
|
|
+ @Resource
|
|
|
+ private IMaTechnicianService maTechnicianService;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Page<HotRecommendVO> getHotRecommendPage(HotRecommendDTO dto) {
|
|
|
+
|
|
|
+ long pageSize = dto.getSize();
|
|
|
+ long pageNum = dto.getCurrent();
|
|
|
+ BigDecimal userLng = dto.getLongitude();
|
|
|
+ BigDecimal userLat = dto.getLatitude();
|
|
|
+
|
|
|
+
|
|
|
+ long offset = (pageNum - 1) * pageSize;
|
|
|
+ // 优化:多预取一倍ID,解决交替翻页断层
|
|
|
+ long fetchCount = pageSize * 2;
|
|
|
+ long end = offset + fetchCount - 1;
|
|
|
+ List<MerchantWithAddressVO> techWithAddrList;
|
|
|
+ List<Project> projectList;
|
|
|
+ boolean redisDown = false;
|
|
|
+ try {
|
|
|
+ // 1. Redis ZSet倒序分页获取ID
|
|
|
+ Set<String> techIdStrSet = redisTemplate.opsForZSet().reverseRange("hot:merchant:rank", offset, end);
|
|
|
+ Set<String> projectIdStrSet = redisTemplate.opsForZSet().reverseRange("hot:project:rank", offset, end);
|
|
|
+
|
|
|
+ // 转Long ID集合
|
|
|
+ List<Integer> techIds = techIdStrSet.stream().map(Integer::valueOf).collect(Collectors.toList());
|
|
|
+ List<Integer> projectIds = projectIdStrSet.stream().map(Integer::valueOf).collect(Collectors.toList());
|
|
|
+
|
|
|
+ // 2. 批量查库获取完整详情
|
|
|
+ techWithAddrList = this.maTechnicianService.selectTechWithAddressByIds(techIds);
|
|
|
+ projectList = this.projectService.listByIds(projectIds);
|
|
|
+
|
|
|
+ // 3. 内存二次过滤脏数据(缓存不一致兜底)
|
|
|
+ techWithAddrList = techWithAddrList.stream()
|
|
|
+ .filter(t -> t.getNStar() != null)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ projectList = projectList.stream().filter(p -> p.getIsDelete() == 0 && p.getStatus() == 0).collect(Collectors.toList());
|
|
|
+ } catch (Exception e) {
|
|
|
+ // Redis异常降级,切回纯MySQL分页方案
|
|
|
+ log.error("Redis ZSet查询异常,降级MySQL分页", e);
|
|
|
+ redisDown = true;
|
|
|
+ techWithAddrList = this.maTechnicianService.listValidTechWithAddress(offset, fetchCount);
|
|
|
+ projectList = this.projectService.selectProjectPage(offset, fetchCount);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 交替合并 商户-项目循环,截断pageSize
|
|
|
+ List<HotRecommendVO> mixResult = mergeMixData(techWithAddrList, projectList, pageSize, userLat, userLng);
|
|
|
+
|
|
|
+ // 5. 分页总数
|
|
|
+ Long totalTech, totalProject;
|
|
|
+ if (!redisDown) {
|
|
|
+ totalTech = redisTemplate.opsForZSet().zCard("hot:merchant:rank");
|
|
|
+ totalProject = redisTemplate.opsForZSet().zCard("hot:project:rank");
|
|
|
+ } else {
|
|
|
+ totalTech = this.maTechnicianService.countValidTech();
|
|
|
+ totalProject = this.projectService.countValidProject();
|
|
|
+ }
|
|
|
+ Page<HotRecommendVO> page = new Page<>(dto.getCurrent(), dto.getSize());
|
|
|
+ page.setRecords(mixResult);
|
|
|
+ page.setTotal(totalTech + totalProject);
|
|
|
+ return page;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 交替合并:商户1、项目1、商户2、项目2
|
|
|
+ */
|
|
|
+ private List<HotRecommendVO> mergeMixData(List<MerchantWithAddressVO> techList, List<Project> projectList, Long pageSize, BigDecimal userLat, BigDecimal userLng) {
|
|
|
+ List<HotRecommendVO> result = new ArrayList<>(Math.toIntExact(pageSize));
|
|
|
+ int maxLoop = Math.max(techList.size(), projectList.size());
|
|
|
+ for (int i = 0; i < maxLoop; i++) {
|
|
|
+ if (result.size() >= pageSize) break;
|
|
|
+ // 先组装商户卡片
|
|
|
+ if (i < techList.size()) {
|
|
|
+ HotRecommendVO vo = convertTechToVO(techList.get(i), userLat, userLng);
|
|
|
+ vo.setCardType(1);
|
|
|
+ result.add(vo);
|
|
|
+ if (result.size() >= pageSize) break;
|
|
|
+ }
|
|
|
+ // 再组装项目卡片
|
|
|
+ if (i < projectList.size()) {
|
|
|
+ HotRecommendVO vo = convertProjectToVO(projectList.get(i));
|
|
|
+ vo.setCardType(2);
|
|
|
+ result.add(vo);
|
|
|
+ if (result.size() >= pageSize) break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 商户实体转VO(包含距离计算、标签拆分)
|
|
|
+ *
|
|
|
+ * @param tech 商户实体
|
|
|
+ * @param userLat 用户纬度
|
|
|
+ * @param userLng 用户经度
|
|
|
+ * @return 商户卡片VO
|
|
|
+ */
|
|
|
+ private HotRecommendVO convertTechToVO(MerchantWithAddressVO tech, BigDecimal userLat, BigDecimal userLng) {
|
|
|
+ HotRecommendVO vo = new HotRecommendVO();
|
|
|
+ vo.setCardType(1);
|
|
|
+ vo.setTechId(tech.getId());
|
|
|
+ // 封面优先取形象照,无则头像
|
|
|
+ vo.setCoverImg(tech.getAvatar() != null ? tech.getAvatar() : tech.getTeAvatar());
|
|
|
+ vo.setTeNickName(tech.getTeNickName());
|
|
|
+ vo.setNStar(tech.getNStar());
|
|
|
+ vo.setServiceTotal(tech.getNNum());
|
|
|
+ // 计算距离
|
|
|
+ String distance = DistanceUtil.formatDisplay(userLat, userLng, tech.getLatitude(), tech.getLongitude());
|
|
|
+ vo.setDistance(distance);
|
|
|
+ // 拆分标签字符串为集合 te_project="调理,SPA"
|
|
|
+ String tagStr = tech.getTeProject();
|
|
|
+ if (tagStr != null && tagStr.trim().length() > 0) {
|
|
|
+ List<String> tagList = Arrays.stream(tagStr.split(","))
|
|
|
+ .map(String::trim)
|
|
|
+ .filter(s -> s.length() > 0)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ vo.setTagList(tagList);
|
|
|
+ }
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 项目实体转卡片VO
|
|
|
+ *
|
|
|
+ * @param project 项目实体
|
|
|
+ * @return 项目卡片VO
|
|
|
+ */
|
|
|
+ private HotRecommendVO convertProjectToVO(Project project) {
|
|
|
+ HotRecommendVO vo = new HotRecommendVO();
|
|
|
+ vo.setCardType(2);
|
|
|
+ vo.setProjectId(project.getId());
|
|
|
+ vo.setCoverImg(project.getCover());
|
|
|
+ vo.setProjectTitle(project.getTitle());
|
|
|
+ vo.setHighlight(project.getHighlight());
|
|
|
+ vo.setPriceMin(project.getPriceMin());
|
|
|
+ vo.setSalesCount(project.getSalesCompleted());
|
|
|
+ // 组装时长展示文案:standard_duration + unit_type
|
|
|
+ String durationText;
|
|
|
+ Integer unitType = project.getUnitType();
|
|
|
+ String unitTypeName = "未知单位";
|
|
|
+ List<SysDictData> dictList = DictUtils.getSortedDictCache(DICT_UNIT_TYPE);
|
|
|
+ if (CollUtil.isNotEmpty(dictList) && ObjectUtil.isNotNull(unitType)) {
|
|
|
+ unitTypeName = dictList.stream()
|
|
|
+ .filter(dict -> unitType.toString().equals(dict.getDictValue()))
|
|
|
+ .map(SysDictData::getDictLabel)
|
|
|
+ .findFirst()
|
|
|
+ .orElse("未知单位");
|
|
|
+ }
|
|
|
+ durationText = project.getStandardDuration() + unitTypeName;
|
|
|
+ vo.setDurationText(durationText);
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+}
|